Компоненты
Структура
Компоненты — это механизм инкапсуляции элементов представления, упрощающий организацию и/или повторное использование кода.
Любой JavaScript-объект, имеющий метод view
, является компонентом Mithril.js. Компоненты можно использовать с помощью утилиты m()
:
// Определите ваш компонент
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// Используйте свой компонент
m(Example);
// Эквивалентный HTML
// <div>Hello</div>
Методы жизненного цикла
Компоненты могут иметь те же методы жизненного цикла, что и узлы виртуального DOM. Обратите внимание, что vnode
передается в качестве аргумента каждому методу жизненного цикла компонентов, а также в view
(в onbeforeupdate
дополнительно передается предыдущий vnode
):
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('initialized');
},
oncreate: function (vnode) {
console.log('DOM created');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM updated');
},
onbeforeremove: function (vnode) {
console.log('exit animation can start');
return new Promise(function (resolve) {
// Вызвать после завершения анимации
resolve();
});
},
onremove: function (vnode) {
console.log('removing DOM element');
},
view: function (vnode) {
return 'hello';
},
};
Как и другие типы узлов виртуального DOM, компоненты могут иметь дополнительные методы жизненного цикла, определенные при использовании в качестве типов vnode.
function initialize(vnode) {
console.log('initialized as vnode');
}
m(ComponentWithHooks, { oninit: initialize });
Методы жизненного цикла в vnodes не заменяют методы компонента, и наоборот. Методы жизненного цикла компонента всегда выполняются после соответствующего метода vnode.
Будьте внимательны и не используйте имена методов жизненного цикла компонентов для имен ваших собственных функций обратного вызова в vnodes.
Чтобы узнать больше о методах жизненного цикла, см. страницу методов жизненного цикла.
Передача данных в компоненты
Данные могут быть переданы экземплярам компонентов путем передачи объекта attrs
в качестве второго параметра в функции hyperscript:
m(Example, { name: 'Floyd' });
К этим данным можно получить доступ в представлении компонента или методах жизненного цикла через vnode.attrs
:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
Примечание: Методы жизненного цикла также могут быть определены в объекте attrs
, поэтому вам следует избегать использования их имен для ваших собственных обратных вызовов, поскольку они также будут вызываться самим Mithril.js. Используйте их в attrs
только тогда, когда вы конкретно хотите использовать их в качестве методов жизненного цикла.
Состояние
Как и все узлы виртуального DOM, компонентные vnodes могут иметь состояние. Состояние компонента полезно для реализации объектно-ориентированных архитектур, для инкапсуляции и для разделения ответственности.
Обратите внимание, что в отличие от многих других фреймворков, изменение состояния компонента не вызывает перерисовки интерфейса или обновления DOM. Вместо этого перерисовки происходят при срабатывании обработчиков событий, при завершении HTTP-запросов, сделанных с помощью m.request, или когда браузер переходит к другим маршрутам. Механизмы состояния компонентов Mithril.js просто существуют для удобства приложений.
Если происходит изменение состояния, которое не является результатом ни одного из вышеперечисленных условий (например, после setTimeout
), то вы можете использовать m.redraw()
для запуска перерисовки вручную.
Состояние компонента-замыкания
В приведенных выше примерах каждый компонент определяется как POJO (Plain Old JavaScript Object - простой старый JavaScript объект), который используется Mithril.js внутри как прототип для экземпляров этого компонента. Можно использовать состояние компонента с POJO (как мы обсудим ниже), но это не самый чистый или простой подход. Для этого мы будем использовать компонент-замыкание, который является просто функцией-оберткой, которая возвращает экземпляр POJO компонента, который, в свою очередь, имеет свою собственную замкнутую область видимости.
С компонентом-замыканием состояние можно просто поддерживать переменными, которые объявлены внутри внешней функции:
function ComponentWithState(initialVnode) {
// Переменная состояния компонента, уникальная для каждого экземпляра
var count = 0;
// Экземпляр POJO компонента: любой объект с
// функцией view, возвращающей vnode
return {
oninit: function (vnode) {
console.log('init a closure component');
},
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Increment count'
)
);
},
};
}
Любые функции, объявленные внутри замыкания, также имеют доступ к его переменным состояния.
function ComponentWithState(initialVnode) {
var count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
return {
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: increment,
},
'Increment'
),
m(
'button',
{
onclick: decrement,
},
'Decrement'
)
);
},
};
}
Компоненты-замыкания используются так же, как и POJO, например, m(ComponentWithState, { passedData: ... })
.
Главное преимущество компонентов-замыканий заключается в том, что нам не нужно беспокоиться о привязке this
при присоединении обратных вызовов обработчиков событий. Фактически, this
вообще никогда не используется, и нам никогда не приходится думать о неоднозначности контекста this
.
Состояние POJO компонента
В целом рекомендуется использовать замыкания для управления состоянием компонента. Однако, если у вас есть причина управлять состоянием в POJO, состояние компонента можно получить тремя способами:
На этапе инициализации
Для POJO компонентов объект компонента является прототипом каждого экземпляра компонента, поэтому любое свойство, определенное в объекте компонента, будет доступно как свойство vnode.state
. Это позволяет выполнять простую инициализацию состояния "blueprint".
В приведенном ниже примере data
становится свойством объекта vnode.state
компонента ComponentWithInitialState
.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// Эквивалентный HTML
// <div>Initial content</div>
Через vnode.state
Как видите, состояние также доступно через свойство vnode.state
, которое доступно для всех методов жизненного цикла, а также для метода view
компонента.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// Эквивалентный HTML
// <div>Hello</div>
Через ключевое слово this
Состояние также доступно через ключевое слово this
, которое доступно для всех методов жизненного цикла, а также для метода view
компонента.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// Эквивалентный HTML
// <div>Hello</div>
Имейте в виду, что при использовании функций ES5 значение this
во вложенных анонимных функциях не является экземпляром компонента. Есть два рекомендуемых способа обойти это ограничение JavaScript: использовать стрелочные функции или, если они не поддерживаются, использовать vnode.state
.
Классы
Если это соответствует вашим потребностям (например, в объектно-ориентированных проектах), компоненты также можно писать с использованием классов:
class ClassComponent {
constructor(vnode) {
this.kind = 'class component';
}
view() {
return m('div', `Hello from a ${this.kind}`);
}
oncreate() {
console.log(`A ${this.kind} was created`);
}
}
Классовые компоненты должны определять метод view()
, обнаруженный через .prototype.view
, чтобы дерево отображалось.
Их можно использовать так же, как и обычные компоненты.
// ПРИМЕР: через m.render
m.render(document.body, m(ClassComponent));
// ПРИМЕР: через m.mount
m.mount(document.body, ClassComponent);
// ПРИМЕР: через m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// ПРИМЕР: композиция компонентов
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
Состояние классового компонента
С классами состоянием можно управлять с помощью свойств и методов экземпляра класса и получать доступ через this
:
class ComponentWithState {
constructor(vnode) {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
view() {
return m(
'div',
m('p', 'Count: ', this.count),
m(
'button',
{
onclick: () => {
this.increment();
},
},
'Increment'
),
m(
'button',
{
onclick: () => {
this.decrement();
},
},
'Decrement'
)
);
}
}
Обратите внимание, что мы должны использовать стрелочные функции для обратных вызовов обработчиков событий, чтобы контекст this
можно было правильно ссылаться.
Смешивание видов компонентов
Компоненты можно свободно смешивать. Классовый компонент может иметь компоненты-замыкания или POJO в качестве дочерних элементов и т. д.
Специальные атрибуты
Mithril.js придает специальную семантику нескольким ключам свойств, поэтому вам обычно следует избегать их использования в обычных атрибутах компонентов.
- Методы жизненного цикла:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
иonremove
key
, который используется для отслеживания идентичности в фрагментах с ключамиtag
, который используется для отличения vnodes от обычных объектов атрибутов и других вещей, которые не являются объектами vnode.
Избегайте анти-паттернов
Хотя Mithril.js является гибким, некоторые шаблоны кода не рекомендуются:
Избегайте "толстых" компонентов
Вообще говоря, "толстый" компонент - это компонент, который имеет пользовательские методы экземпляра. Другими словами, вам следует избегать присоединения функций к vnode.state
или this
. Чрезвычайно редко встречается логика, которая логически вписывается в метод экземпляра компонента и которая не может быть повторно использована другими компонентами. Довольно часто бывает, что эта логика может понадобиться другому компоненту в будущем.
Легче рефакторить код, если эта логика помещена в слой данных, чем если она привязана к состоянию компонента.
Рассмотрим этот "толстый" компонент:
// views/Login.js
// НЕ РЕКОМЕНДУЕТСЯ
var Login = {
username: '',
password: '',
setUsername: function (value) {
this.username = value;
},
setPassword: function (value) {
this.password = value;
},
canSubmit: function () {
return this.username !== '' && this.password !== '';
},
login: function () {
/*...*/
},
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
this.setUsername(e.target.value);
},
value: this.username,
}),
m('input[type=password]', {
oninput: function (e) {
this.setPassword(e.target.value);
},
value: this.password,
}),
m(
'button',
{ disabled: !this.canSubmit(), onclick: this.login },
'Login'
),
]);
},
};
Обычно, в контексте более крупного приложения, компонент входа в систему, подобный приведенному выше, существует наряду с компонентами для регистрации пользователей и восстановления пароля. Представьте, что мы хотим иметь возможность предварительно заполнять поле электронной почты при переходе с экрана входа в систему на экраны регистрации или восстановления пароля (или наоборот), чтобы пользователю не нужно было повторно вводить свой адрес электронной почты, если он случайно заполнил не ту страницу (или, возможно, вы хотите перенаправить пользователя на форму регистрации, если имя пользователя не найдено).
Сразу же мы видим, что совместное использование полей username
и password
из этого компонента с другим затруднено. Это связано с тем, что "толстый" компонент инкапсулирует свое состояние, что по определению затрудняет доступ к этому состоянию извне.
Имеет смысл рефакторить этот компонент и вынести код состояния из компонента в слой данных приложения. Это может быть так же просто, как создание нового модуля:
// models/Auth.js
// ПРЕДПОЧТИТЕЛЬНО
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
canSubmit: function () {
return Auth.username !== '' && Auth.password !== '';
},
login: function () {
/*...*/
},
};
module.exports = Auth;
Затем мы можем очистить компонент:
// views/Login.js
// ПРЕДПОЧТИТЕЛЬНО
var Auth = require('../models/Auth');
var Login = {
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
Auth.setUsername(e.target.value);
},
value: Auth.username,
}),
m('input[type=password]', {
oninput: function (e) {
Auth.setPassword(e.target.value);
},
value: Auth.password,
}),
m(
'button',
{
disabled: !Auth.canSubmit(),
onclick: Auth.login,
},
'Login'
),
]);
},
};
Таким образом, модуль Auth
теперь является источником истины для состояния, связанного с аутентификацией, и компонент Register
может легко получить доступ к этим данным и даже повторно использовать такие методы, как canSubmit
, если это необходимо. Кроме того, если требуется код проверки (например, для поля электронной почты), вам нужно только изменить setEmail
, и это изменение будет выполнять проверку электронной почты для любого компонента, который изменяет поле электронной почты.
В качестве бонуса обратите внимание, что нам больше не нужно использовать .bind
для сохранения ссылки на состояние для обработчиков событий компонента.
Не передавайте vnode.attrs
сам по себе другим vnodes
Иногда вам может потребоваться сохранить интерфейс гибким, а реализацию более простой, пересылая атрибуты определенному дочернему компоненту или элементу, в данном случае модальному окну Bootstrap. Может возникнуть соблазн передать атрибуты vnode следующим образом:
// НЕ РЕКОМЕНДУЕТСЯ
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// передача `vnode.attrs` здесь ^
// ...
]);
},
};
Если вы сделаете это, как указано выше, вы можете столкнуться с проблемами при его использовании:
var MyModal = {
view: function () {
return m(
Modal,
{
// Это переключает его дважды, поэтому он не отображается
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
Вместо этого следует передавать отдельные атрибуты в vnodes:
// ПРЕДПОЧТИТЕЛЬНО
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// передача `attrs:` здесь ^
// ...
]);
},
};
// Пример
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Это переключает его один раз
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
Не манипулируйте children
Если компонент имеет определенное мнение о том, как он применяет атрибуты или дочерние элементы, вам следует перейти к использованию пользовательских атрибутов.
Часто желательно определить несколько наборов дочерних элементов, например, если компонент имеет настраиваемые заголовок и содержимое.
Избегайте деструктуризации свойства children
для этой цели.
// НЕ РЕКОМЕНДУЕТСЯ
var Header = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.children[0]),
m('.tagline', vnode.children[1]),
]);
},
};
m(Header, [m('h1', 'My title'), m('h2', 'Lorem ipsum')]);
// Неудачный пример использования
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
Приведенный выше компонент нарушает предположение о том, что дочерние элементы будут выводиться в том же непрерывном формате, в котором они были получены. Трудно понять компонент, не прочитав его реализацию. Вместо этого используйте атрибуты в качестве именованных параметров и зарезервируйте children
для единообразного дочернего содержимого:
// ПРЕДПОЧТИТЕЛЬНО
var BetterHeader = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.attrs.title),
m('.tagline', vnode.attrs.tagline),
]);
},
};
m(BetterHeader, {
title: m('h1', 'My title'),
tagline: m('h2', 'Lorem ipsum'),
});
// Более понятный пример использования
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
Определяйте компоненты статически, вызывайте их динамически
Избегайте создания определений компонентов внутри представлений
Если вы создаете компонент из метода view
(либо непосредственно в строке, либо путем вызова функции, которая это делает), каждая перерисовка будет иметь другой клон компонента. При сравнении компонентных vnodes, если компонент, на который ссылается новый vnode, не строго равен компоненту, на который ссылается старый компонент, предполагается, что эти два компонента являются разными компонентами, даже если они в конечном итоге выполняют эквивалентный код. Это означает, что компоненты, созданные динамически через фабрику, всегда будут создаваться заново.
По этой причине следует избегать воссоздания компонентов. Вместо этого используйте компоненты идиоматически.
// НЕ РЕКОМЕНДУЕТСЯ
var ComponentFactory = function (greeting) {
// Создает новый компонент при каждом вызове
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// Повторный вызов воссоздает div с нуля, вместо того, чтобы ничего не делать
m.render(document.body, m(ComponentFactory('hello')));
// ПРЕДПОЧТИТЕЛЬНО
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// Повторный вызов не изменяет DOM
m.render(document.body, m(Component, { greeting: 'hello' }));
Избегайте создания экземпляров компонентов вне представлений
И наоборот, по аналогичным причинам, если экземпляр компонента создан вне представления, будущие перерисовки будут выполнять проверку равенства узла и пропускать его. Поэтому экземпляры компонентов всегда следует создавать внутри представлений:
// НЕ РЕКОМЕНДУЕТСЯ
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
var counter = m(Counter);
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), counter];
},
});
В приведенном выше примере нажатие кнопки компонента счетчика увеличит количество его состояний, но его представление не будет запущено, потому что vnode, представляющий компонент, имеет ту же ссылку, и поэтому процесс отрисовки не выполняет их сравнение (diff). Вы всегда должны вызывать компоненты в представлении, чтобы убедиться, что создан новый vnode:
// ПРЕДПОЧТИТЕЛЬНО
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), m(Counter)];
},
});