Вы находитесь на странице: 1из 25

extjs

#extjs
1

1: extjs 2

Examples 2

Hello World - Via Sencha Cmd 2

Hello World - 4

2: ExtJS AJAX 9

Examples 9

3: MVC / MVVM - 10

Examples 10

10

ExtJS 4 MVC CRUD 10

4: 16

16

Examples 16

, 16

5: 18

Examples 18

18

18

18

19

vs 19

: 19

: 19

20

20
22
Около
You can share this PDF with anyone you feel could benefit from it, downloaded the latest version
from: extjs

It is an unofficial and free extjs ebook created for educational purposes. All the content is extracted
from Stack Overflow Documentation, which is written by many hardworking individuals at Stack
Overflow. It is neither affiliated with Stack Overflow nor official extjs.

The content is released under Creative Commons BY-SA, and the list of contributors to each
chapter are provided in the credits section at the end of this book. Images may be copyright of
their respective owners unless otherwise specified. All trademarks and registered trademarks are
the property of their respective company owners.

Use the content presented in this book at your own risk; it is not guaranteed to be correct nor
accurate, please send your feedback and corrections to info@zzzprojects.com

https://riptutorial.com/ru/home 1
глава 1: Начало работы с extjs
замечания
ExtJS - это платформа JavaScript от Sencha для создания богатых интернет-приложений.
Он может похвастаться одной из крупнейших библиотек готовых модульных компонентов
пользовательского интерфейса.

Начиная с версии 5.0, Sencha выступает за использование архитектуры Model-View-


ViewModel (MVVM) на своей платформе. Он также поддерживает поддержку архитектуры
Model-View-Controller (MVC), которая была основным стилем архитектуры,
поддерживаемым до версии 4.x.

Кроме того, Sencha сосредоточилась на оснащении ExtJS мобильными центрическими и


гибкими возможностями веб-приложений. Его прежняя структура Sencha Touch была
интегрирована с ExtJS с версии 6.0 с усилиями по объединению клиентских баз и
консолидации избыточности в новой комбинированной структуре.

Версии

Версия Дата выхода

6,2 2016-06-14

- 6,0 2015-08-28

5.0 2014-06-02

4,0 2011-04-26

3.0 2009-07-06

2,0 2007-12-04

1,1 2007-04-15

Examples

Создание приложения Hello World - Via Sencha Cmd

Этот пример демонстрирует создание базового приложения в ExtJS с использованием


Sencha Cmd для загрузки процесса - этот метод автоматически генерирует некоторый код
и структуру скелета для проекта.

https://riptutorial.com/ru/home 2
Откройте консольное окно и измените рабочий каталог на подходящее пространство для
работы. В том же окне и в каталоге выполните следующую команду для создания нового
приложения.

> sencha -sdk /path/to/ext-sdk generate app HelloWorld ./HelloWorld

Примечание. Флаг -sdk указывает местоположение каталога, извлеченного из каркасного


архива.

В ExtJS 6+ Sencha объединили рамки ExtJS и Touch в единую кодовую базу,


дифференцированную терминами классический и современный соответственно. Для
простоты, если вы не хотите настраивать таргетинг на мобильные устройства, в команде
может быть указан дополнительный флаг, чтобы уменьшить беспорядок в рабочей
области.

> sencha -sdk /path/to/ext-sdk generate app -classic HelloWorld ./HelloWorld

Без какой-либо дополнительной конфигурации полностью функциональное демо-


приложение должно теперь находиться в локальном каталоге. Теперь измените рабочий
каталог на новый каталог проекта HelloWorld и запустите:

> sencha app watch

При этом проект компилируется с использованием профиля построения по умолчанию и


запускается простой HTTP-сервер, который позволяет просматривать приложение
локально через веб-браузер. По умолчанию на порт 1841 .

Установка и настройка

Типичное использование ExtJS использует структуру для создания одностраничных


богатых приложений (RIA). Самый простой способ начать использовать Sencha Cmd ,
инструмент построения CLI, охватывающий большинство общих проблем в жизненном
цикле развертывания, в первую очередь:

• управление пакетами и зависимостями


• компиляция / комплектация кода и его обобщение
• управление стратегиями построения для разных целей и платформ

» Скачать Sencha Cmd

Второй шаг - загрузить SDK, ExtJS - это коммерческий продукт - для получения копии, один
из:

• Войти в Sencha Поддержка версии лицензий на продажу ( страница продукта )

https://riptutorial.com/ru/home 3
• обратиться за оценочной копией, которая будет действительна в течение 30 дней
• запросить версию GPL v3 для проектов с открытым исходным кодом
(обратите внимание, что вы не сможете получить доступ к последней версии с
помощью этой опции)

После загрузки SDK убедитесь, что архив извлечен, прежде чем продолжить.

Примечание . См. Официальную документацию « Начало работы» для всестороннего


руководства по проектам ExtJS.

После установки Sencha Cmd его можно проверить, открыв консольное окно и запустив
его:

> sencha help

Теперь у нас есть инструменты, необходимые для создания и развертывания приложений


ExtJS, обратите внимание на расположение каталога, в котором был извлечен SDK,
поскольку это потребуется в следующих примерах.

Создание приложения Hello World - в ручном режиме

Давайте начнем использовать ExtJS для создания простого веб-приложения.

Мы создадим простое веб-приложение, которое будет иметь только одну физическую


страницу (aspx / html). Как минимум, каждое приложение ExtJS будет содержать один
HTML и один файл JavaScript - обычно index.html и app.js.

Файл index.html или ваша страница по умолчанию будут содержать ссылки на CSS и
JavaScript-код ExtJS вместе с вашим файлом app.js, содержащим код для вашего
приложения (в основном, отправную точку вашего веб-приложения).

Давайте создадим простое веб-приложение, которое будет использовать компоненты


библиотеки ExtJS:

Шаг 1. Создание пустого веб-приложения.

Как показано на скриншоте, я создал пустое веб-приложение. Чтобы сделать его простым,
вы можете использовать любой проект веб-приложения в редакторе или в IDE по вашему
выбору.

https://riptutorial.com/ru/home 4
Шаг 2: добавьте веб-страницу по умолчанию

Если вы создали пустое веб-приложение, нам нужно включить веб-страницу, которая


будет стартовой страницей нашего приложения.

Шаг 3: Добавить Ext Js Ссылки на Default.aspx

Этот шаг показывает, как мы используем библиотеку extJS. Как показано на скриншоте в
Default.aspx, я только что назвал 3 файла:

• Ext-all.js
• доб-all.css
• app.js

Sencha сотрудничает с CacheFly, глобальной контентной сетью, для предоставления


бесплатного хоста CDN для платформы ExtJS. В этом примере я использовал библиотеку

https://riptutorial.com/ru/home 5
CDN Ext, однако мы могли использовать те же файлы (ext-all.js и ext-all.css) из нашего
каталога проектов или в качестве резервных копий в случае недоступности CDN.

Обратившись к app.js, он будет загружен в браузер, и это будет отправной точкой для
нашего приложения.

Помимо этих файлов, у нас есть местозаполнитель, где будет отображаться


пользовательский интерфейс. В этом примере у нас есть div с id «whitespace», который мы
будем использовать позже для визуализации интерфейса.

<script type="text/javascript" src="http://cdn.sencha.com/ext/trial/5.0.0/build/ext-


all.js"></script>

<link rel="stylesheet" type="text/css"

href="http://cdn.sencha.com/ext/trial/5.0.0/build/packages/ext-theme-
neptune/build/resources/ext-theme-neptune-all.css"/>

<script src="app/app.js"></script>

Шаг 4. Добавьте папку приложения & app.js в свой веб-проект.

ExtJS предоставляет нам способ управления кодом в шаблоне MVC. Как показано на
скриншоте, у нас есть контейнерная папка для нашего приложения ExtJS, в данном случае
«приложение». Эта папка будет содержать весь наш код приложения, разделенный на
различные папки, т. Е. Модель, представление, контроллер, хранилище и т. Д. В настоящее
время он имеет только файл app.js.

https://riptutorial.com/ru/home 6
Шаг 5: Напишите свой код в app.js

App.js является отправной точкой нашего приложения; для этого образца я только что
использовал минимальную конфигурацию, необходимую для запуска приложения.

Ext.application представляет собой приложение ExtJS, которое выполняет несколько


действий . Он создает глобальную переменную « SenchaApp », указанную в
конфигурации имен, и все классы приложений (модели, представления, контроллеры,
магазины) будут находиться в одном пространстве имен. Запуск - это функция, которая
вызывается автоматически, когда все приложение готово (все классы загружаются
должным образом).

В этом примере мы создаем Panel с некоторой конфигурацией и визуализируем его на


заполнителе, который мы предоставили в Default.aspx.

Ext.application({
name: 'SenchaApp',
launch: function () {
Ext.create('Ext.panel.Panel', {
title: 'Sencha App',
width: 300,
height: 300,
bodyPadding:10,
renderTo: 'whitespace',
html:'Hello World'
});
}
});

Скриншот результатов

Когда вы запустите это веб-приложение с Default.aspx в качестве начальной страницы, в


браузере появится следующее окно.

https://riptutorial.com/ru/home 7
Прочитайте Начало работы с extjs онлайн: https://riptutorial.com/ru/extjs/topic/819/начало-
работы-с-extjs

https://riptutorial.com/ru/home 8
глава 2: ExtJS AJAX
Вступление
Один экземпляр класса [ Ext.data.Connection ] [1]. Этот класс используется для связи со
стороной вашего сервера. [1]: HTTP:
//docs.sencha.com/extjs/6.0.1/classic/src/Connection.js.html#Ext.data.Connection

Examples

Основной запрос

Некоторые свойства класса Ext.Data.Connection

свойства подробности

url Адрес запроса

timeout Время ожидания в миллисекундах

success Возвращение к успеху

failure Возврат при неудаче

Ext.Ajax.on("beforerequest", function(conn , options , eOpts) {


console.log("beforerequest");
});
Ext.Ajax.on("requestcomplete", function(conn , response , options , eOpts) {
console.log("requestcomplete");
});
Ext.Ajax.on("requestexception", function(conn , response , options , eOpts) {
console.log("requestexception");
});

Ext.Ajax.request({
url: 'mypath/sample.json',
timeout: 60000,
success: function(response, opts) {
var obj = Ext.decode(response.responseText);
console.log(obj);
},
failure: function(response, opts) {
console.log('server-side failure with status code ' + response.status);
}
});

Прочитайте ExtJS AJAX онлайн: https://riptutorial.com/ru/extjs/topic/8134/extjs-ajax

https://riptutorial.com/ru/home 9
глава 3: MVC / MVVM - Архитектура
приложения
Examples

Введение в модели

Модель представляет собой некоторый объект данных в приложении. Например, вы


можете иметь в своем приложении такую модель, как: Fruit, Car, Building и т. Д. Модели
обычно используются в магазинах. Вот пример того, как вы бы определили новый класс
модели. например

Ext.define('MyApp.model.Person', {
extend: 'Ext.data.Model',
fields: [
{name: 'name', type: 'string'},
{name: 'surname', type: 'string'},
{name: 'age', type: 'int'}
],

getFullName: function() {
return this.get('name') + " " + this.get('surname');
}
});

После определения нашего модельного класса мы, возможно, хотели бы создать его
экземпляр и, вероятно, вызвать некоторые методы. Например:

// Create person instance


var person = Ext.create('MyApp.model.Person', {
name : 'Jon',
surname: 'Doe',
age : 24
});

alert(person.getFullName()); // Display person full name

Пример приложения ExtJS 4 MVC CRUD

Демо онлайн здесь: http://ext4all.com/post/extjs-4-mvc-application-architecture.html

Определите модель:

// /scripts/app/model/User.js
Ext.define('AM.model.User', {
extend: 'Ext.data.Model',
fields: ['id', 'name', 'email']
});

https://riptutorial.com/ru/home 10
Определите хранилище с прокси:

// /scripts/app/store/Users.js
Ext.define('AM.store.Users', {
extend: 'Ext.data.Store',
model: 'AM.model.User',
autoLoad: true,
autoSync: true,
proxy: {
type: 'ajax',
limitParam: 'size',
startParam: undefined,
api: {
create: '/user/add',
read: '/user/list',
update: '/user/update',
destroy: '/user/delete'
},
reader: {
type: 'json',
root: 'data',
successProperty: 'success'
},
writer: {
type: 'json',
writeAllFields: false
}
}
});

Определить добавить пользовательский вид - это окно с формой внутри:

// /scripts/app/view/user/Add.js
Ext.define('AM.view.user.Add', {
extend: 'Ext.window.Window',
alias: 'widget.useradd',
title: 'Add User',
layout: 'fit',
autoShow: true,
initComponent: function () {
this.items = [
{
xtype: 'form',
bodyStyle: {
background: 'none',
padding: '10px',
border: '0'
},
items: [
{
xtype: 'textfield',
name: 'name',
allowBlank: false,
fieldLabel: 'Name'
},
{
xtype: 'textfield',
name: 'email',
allowBlank: false,

https://riptutorial.com/ru/home 11
vtype: 'email',
fieldLabel: 'Email'
}
]
}
];
this.buttons = [
{
text: 'Save',
action: 'save'
},
{
text: 'Cancel',
scope: this,
handler: this.close
}
];
this.callParent(arguments);
}
});

Определить пользовательский вид редактирования - это также окно с формой внутри:

// /scripts/app/view/user/Edit.js
Ext.define('AM.view.user.Edit', {
extend: 'Ext.window.Window',
alias: 'widget.useredit',
title: 'Edit User',
layout: 'fit',
autoShow: true,
initComponent: function () {
this.items = [
{
xtype: 'form',
bodyStyle: {
background: 'none',
padding: '10px',
border: '0'
},
items: [
{
xtype: 'textfield',
name: 'name',
allowBlank: false,
fieldLabel: 'Name'
},
{
xtype: 'textfield',
name: 'email',
allowBlank: false,
vtype: 'email',
fieldLabel: 'Email'
}
]
}
];
this.buttons = [
{
text: 'Save',
action: 'save'

https://riptutorial.com/ru/home 12
},
{
text: 'Cancel',
scope: this,
handler: this.close
}
];
this.callParent(arguments);
}
});

Определить вид списка пользователей - это сетка с столбцами Id, Name, Email

// /scripts/app/view/user/List.js
Ext.define('AM.view.user.List', {
extend: 'Ext.grid.Panel',
alias: 'widget.userlist',
title: 'All Users',
store: 'Users',
initComponent: function () {
this.tbar = [{
text: 'Create User', action: 'create'
}];
this.columns = [
{ header: 'Id', dataIndex: 'id', width: 50 },
{ header: 'Name', dataIndex: 'name', flex: 1 },
{ header: 'Email', dataIndex: 'email', flex: 1 }
];
this.addEvents('removeitem');
this.actions = {
removeitem: Ext.create('Ext.Action', {
text: 'Remove User',
handler: function () { this.fireEvent('removeitem', this.getSelected()) },
scope: this
})
};
var contextMenu = Ext.create('Ext.menu.Menu', {
items: [
this.actions.removeitem
]
});
this.on({
itemcontextmenu: function (view, rec, node, index, e) {
e.stopEvent();
contextMenu.showAt(e.getXY());
return false;
}
});
this.callParent(arguments);
},
getSelected: function () {
var sm = this.getSelectionModel();
var rs = sm.getSelection();
if (rs.length) {
return rs[0];
}
return null;
}
});

https://riptutorial.com/ru/home 13
Определите контроллер для обработки событий просмотров:

// /scripts/app/controller/Users.js
Ext.define('AM.controller.Users', {
extend: 'Ext.app.Controller',
stores: [
'Users'
],
models: [
'User'
],
views: [
'user.List',
'user.Add',
'user.Edit'
],
init: function () {
this.control({
'userlist': {
itemdblclick: this.editUser,
removeitem: this.removeUser
},
'userlist > toolbar > button[action=create]': {
click: this.onCreateUser
},
'useradd button[action=save]': {
click: this.doCreateUser
},
'useredit button[action=save]': {
click: this.updateUser
}
});
},
editUser: function (grid, record) {
var view = Ext.widget('useredit');
view.down('form').loadRecord(record);
},
removeUser: function (user) {
Ext.Msg.confirm('Remove User', 'Are you sure?', function (button) {
if (button == 'yes') {
this.getUsersStore().remove(user);
}
}, this);
},
onCreateUser: function () {
var view = Ext.widget('useradd');
},
doCreateUser: function (button) {
var win = button.up('window'),
form = win.down('form'),
values = form.getValues(),
store = this.getUsersStore();
if (form.getForm().isValid()) {
store.add(values);
win.close();
}
},
updateUser: function (button) {
var win = button.up('window'),
form = win.down('form'),
record = form.getRecord(),

https://riptutorial.com/ru/home 14
values = form.getValues(),
store = this.getUsersStore();
if (form.getForm().isValid()) {
record.set(values);
win.close();
}
}
});

Определите свое приложение в app.js:

// /scripts/app/app.js
Ext.Loader.setConfig({ enabled: true });

Ext.application({
name: 'AM',
appFolder: 'scripts/app',
controllers: [
'Users'
],
launch: function () {
Ext.create('Ext.container.Viewport', {
layout: 'border',
items: {
xtype: 'userlist',
region: 'center',
margins: '5 5 5 5'
}
});
}
});

Демо онлайн здесь: http://ext4all.com/post/extjs-4-mvc-application-architecture.html

Прочитайте MVC / MVVM - Архитектура приложения онлайн:


https://riptutorial.com/ru/extjs/topic/3854/mvc---mvvm---архитектура-приложения

https://riptutorial.com/ru/home 15
глава 4: Модель события
Вступление
ExtJS защищает использование и прослушивание событий между классами. При запуске
событий и прослушивании уволенных событий классы не требуют «грязного» знания
классовой структуры друг друга и предотвращения сочетания кода. Кроме того, события
позволяют легко прослушивать несколько экземпляров одного и того же компонента,
разрешая общий прослушиватель для всех объектов с одним и тем же селектором.
Наконец, другие классы также могут использовать уже существующие события.

Examples

Контроллеры, прослушивающие компоненты

Ext.define('App.Duck', {
extend: 'Ext.Component',
alias: 'widget.duck',
initComponent: function () {
this.callParent(arguments);
this._quack();
},
_quack: function () {
console.log('The duck says "Quack!"');
this.fireEvent('quack');
},
feed: function () {
console.log('The duck looks content.');
},
poke: function () {
this._quack();
}
});

var feedController = Ext.create('Ext.app.Controller', {


listen: {
components: {
duck: {
quack: 'feedDuck'
}
}
},
feedDuck: function (duck) {
duck.feed();
}
});

var countController = Ext.create('Ext.app.Controller', {


listen: {
components: {
duck: {
quack: 'addCount'

https://riptutorial.com/ru/home 16
}
}
},
quackCount: 0,
addCount: function (duck) {
this.quackCount++;
console.log('There have been this many quacks: ' + this.quackCount);
}
});

var firstDuck = Ext.create('App.Duck');


// The duck says "Quack!"
// The duck looks content.
// There have been this many quacks: 1
var secondDuck = Ext.create('App.Duck');
// The duck says "Quack!"
// The duck looks content.
// There have been this many quacks: 2
firstDuck.poke();
// The duck says "Quack!"
// The duck looks content.
// There have been this many quacks: 3

Прочитайте Модель события онлайн: https://riptutorial.com/ru/extjs/topic/9314/модель-события

https://riptutorial.com/ru/home 17
глава 5: Общие проблемы и лучшие
практики
Examples

Разделение проблем

Хуже

ViewController:

// ...
myMethod: function () {
this.getView().lookup('myhappyfield').setValue(100);
}
//...

Посмотреть:

//...
items: [
{
xtype: 'textfield',
reference: 'myhappyfield'
}
]
//...

Лучше

ViewController:

// ...
myMethod: function () {
this.getView().setHappiness(100);
}
//...

Посмотреть:

//...
items: [
{
xtype: 'textfield',
reference: 'myhappyfield'
}
],
setHappiness: function (happiness) {

https://riptutorial.com/ru/home 18
this.lookup('myhappyfield').setValue(happiness);
}
//...

объяснение

В этом примере два фрагмента кода выполняют одну и ту же задачу. Однако в случае
myhappyfield изменения ссылки на изменения myhappyfield или методологии определения
«счастья», первый подход требует изменений в каждом месте ссылки.

С раздельными проблемами (последний пример), представление обеспечивает


абстрактный способ изменить «счастье», которое могут использовать другие классы.
Обработка запросов и компонентов хранится в одном месте (прямо рядом с самим
представлением!), И призывы к абстрактному методу не нуждаются в изменении.

Несмотря на то, что контроллер может разрешить прохождение через слои представления,
настоятельно рекомендуется абстрагировать это поведение на методы в представлении.
Таким образом, представление может предоставлять стандартизированные способы
воздействия на него других классов и минимизировать или исключать изменения в других
классах при изменении структуры представления.

Расширить vs Переопределить

Заменяет:

Файл переопределения:

Ext.define('MyApp.override.CornField',
override: 'Ext.form.field.Text',
initComponent: function () {
this.callParent(arguments);
this.setValue('Corn!');
}
);

Использовать в приложении:

{
xtype: 'textfield'
}

Расширения:

Файл переопределения:

Ext.define('MyApp.form.field.CornField',
extend: 'Ext.form.field.Text',
alias: 'widget.cornfield',

https://riptutorial.com/ru/home 19
initComponent: function () {
this.callParent(arguments);
this.setValue('Corn!');
}
);

Использовать в приложении:

{
xtype: 'cornfield'
}

объяснение

ExtJS предоставляет два основных способа изменить поведение существующих классов:


расширение их и переопределение. У каждого есть преимущества и недостатки, которые
следует учитывать перед их использованием.

расширения

Расширение класса создает новый класс, который наследует его поведение и


конфигурацию от своего родителя. Создавая новый класс через расширение, повторная
конфигурация и поведенческие изменения могут быть сделаны в центральном месте и
повторно использованы во всем приложении. Самое большое преимущество расширения
заключается в том, что родительский класс остается неповрежденным и доступен для
более простых случаев использования, когда расширенное поведение нежелательно.

Примеры хороших вариантов использования для расширений включают пользовательские


поля форм со специальным поведением, специализированные модальные модели и
пользовательские компоненты в целом.

Переопределение

Переопределение класса изменяет поведение существующего класса на месте.


Конфигурация и методы в переопределении полностью заменяют их родительские классы,
создавая новые конфигурации и поведение по умолчанию, которые заполняются во всем
приложении. Переопределения следует использовать экономно из-за деструктивного
характера их использования; расширенный класс может, как правило, предоставлять те
же преимущества, оставляя без внимания родительский класс.

Однако переопределения могут обеспечить преимущества в некоторых ситуациях.


Хорошие примеры использования включают исправление ошибок в существующих классах,
изменение поведения прокси для добавления дополнительной информации к запросам,
таких как данные токена или сеанса, и, как правило, введение определенного поведения в
поведение по умолчанию в приложении.

Отдельные переопределения от исправлений ошибок

https://riptutorial.com/ru/home 20
В ExtJS вы можете переопределить практически любой метод фреймворка и заменить его
на свой собственный. Это позволяет изменять существующие классы без прямого
изменения исходного кода ExtJS.

Иногда вам может понадобиться улучшить существующий класс или обеспечить разумное
свойство по умолчанию для класса.

Например, вы можете захотеть, чтобы все поля данных модели допускали нулевые
значения.

Ext.define('MyApp.override.DataField', {
override: 'Ext.data.field.Field',
allowNull: true
});

В других случаях вам нужно исправить что-то, что нарушено в рамках.

Ниже приведен пример исправления ошибок с документацией. Обратите внимание, что имя
класса содержит «fix», а не «override». Фактическое имя не важно, но разделение.

Ext.define('MyApp.fix.FieldBase', {
override: 'Ext.form.field.Base',
/**
* Add a description of what this fix does.
* Be sure to add URLs to important reference information!
*
* You can also include some of your own tags to help identify
* when the problem started and what Sencha bug ticket it relates to.
*
* @extversion 5.1.1
* @extbug EXTJS-15302
*/
publishValue: function () {
this.publishState('value', this.getValue());
}
});

Теперь, когда приходит время перейти на следующую версию ExtJS, есть только одно
место, которое вам нужно проверить, чтобы узнать, какие из исправлений ошибок можно
удалить.

Прочитайте Общие проблемы и лучшие практики онлайн:


https://riptutorial.com/ru/extjs/topic/5412/общие-проблемы-и-лучшие-практики

https://riptutorial.com/ru/home 21
кредиты
S.
Главы Contributors
No

Начало работы с Ben Rhys-Lewis, Community, David Millar, Dharmesh Hadiyal,


1
extjs Emissary, srinivasarao, UDID

2 ExtJS AJAX Alexandre N.

MVC / MVVM -
3 Архитектура CD.., Emissary, Giorgi Moniava, khmurach
приложения

4 Модель события David Millar

Общие проблемы и
5 David Millar, Emissary, John Krull
лучшие практики

https://riptutorial.com/ru/home 22

Оценить