Переход с v0.2.x
v1.x и v2.x в значительной степени совместимы с API v0.2.x, однако существуют некоторые несовместимые изменения. Переход на v2.x практически идентичен переходу на v1.x, поэтому приведённые ниже замечания в основном относятся к обеим версиям.
Если вы планируете миграцию, рассмотрите возможность использования инструмента mithril-codemods для автоматизации наиболее простых преобразований.
m.prop удалён
В v2.x m.prop() был заменён более мощной микробиблиотекой потоков, но он больше не входит в ядро Mithril.js. Информацию об использовании модуля Streams можно найти в документации.
v0.2.x
var m = require('mithril');
var num = m.prop(1);v2.x
var m = require('mithril');
var prop = require('mithril/stream');
var num = prop(1);
var doubled = num.map(function (n) {
return n * 2;
});m.component удалён
В v0.2.x компоненты можно было создавать с помощью m(Component) или m.component(Component). В v2.x поддерживается только m(Component).
v0.2.x
// Эти записи эквивалентны
m.component(Component);
m(Component);v2.x
m(Component);m.withAttr удалён
В v0.2.x обработчики событий могли использовать oninput: m.withAttr("value", func) и аналогичные конструкции. В v2.x рекомендуется считывать значения непосредственно из свойства target объекта события. Это хорошо сочеталось с m.prop, но поскольку он был удалён в пользу внешнего решения, а в v1.x не наблюдалось широкого и идиоматичного использования потоков, m.withAttr потерял большую часть своей полезности.
v0.2.x
var value = m.prop('');
// В вашем представлении
m('input[type=text]', {
value: value(),
oninput: m.withAttr('value', value),
});v2.x
var value = '';
// В вашем представлении
m('input[type=text]', {
value: value,
oninput: function (ev) {
value = ev.target.value;
},
});m.version удалён
Этот метод редко использовался, и вы всегда можете добавить его самостоятельно. Для определения доступности тех или иных функций рекомендуется использовать обнаружение возможностей (feature detection). API v2.x спроектирован таким образом, чтобы это облегчить.
Функция config
В v0.2.x Mithril.js предоставлял один метод жизненного цикла, config. v2.x предоставляет гораздо более детальный контроль над жизненным циклом vnode-элементов.
v0.2.x
m('div', {
config: function (element, isInitialized) {
// Запускается при каждой перерисовке
// isInitialized - это логическое значение, показывающее, был ли узел добавлен в DOM
},
});v2.x
Больше информации об этих новых методах доступно в lifecycle-methods.md.
m('div', {
// Вызывается до создания DOM-узла
oninit: function (vnode) {
/*...*/
},
// Вызывается после создания DOM-узла
oncreate: function (vnode) {
/*...*/
},
// Вызывается перед обновлением узла, возвращает false для отмены обновления
onbeforeupdate: function (vnode, old) {
/*...*/
},
// Вызывается после обновления узла
onupdate: function (vnode) {
/*...*/
},
// Вызывается перед удалением узла, возвращает Promise, который разрешается, когда
// узел готов к удалению из DOM
onbeforeremove: function (vnode) {
/*...*/
},
// Вызывается перед удалением узла, но после того, как onbeforeremove вызывает done()
onremove: function (vnode) {
/*...*/
},
});Если доступен, DOM-элемент vnode можно получить через vnode.dom.
Изменения в поведении перерисовки
Механизм рендеринга Mithril.js по-прежнему основан на полуавтоматических глобальных перерисовках, однако некоторые API и принципы работы были изменены:
Больше нет блокировок перерисовки
В v0.2.x Mithril.js допускал «блокировки перерисовки», которые временно предотвращали выполнение логики перерисовки: по умолчанию m.request блокировал цикл перерисовки во время выполнения и разблокировал его, когда все ожидающие запросы были завершены. Аналогичное поведение можно было вызвать вручную с помощью m.startComputation() и m.endComputation(). Эти API и связанное с ними поведение были удалены в v2.x без замены. Блокировка перерисовки может приводить к ошибкам в пользовательском интерфейсе, поскольку проблемы в одной части приложения не должны препятствовать обновлению других частей представления, отражающих изменения.
Отмена перерисовки из обработчиков событий
m.mount() и m.route() по-прежнему автоматически запускают перерисовку после выполнения обработчика DOM-события. Отмена этих перерисовок из ваших обработчиков событий теперь выполняется путём установки свойства redraw переданного объекта события в значение false.
v0.2.x
m('div', {
onclick: function (e) {
m.redraw.strategy('none');
},
});v2.x
m('div', {
onclick: function (e) {
e.redraw = false;
},
});Синхронная перерисовка изменена
В v0.2.x можно было принудительно выполнить немедленную перерисовку Mithril.js, передав значение true в функцию m.redraw(). В v2.x эта функциональность была разделена на два разных метода для большей ясности.
v0.2.x
m.redraw(true); // выполняет синхронную перерисовку немедленноv2.x
m.redraw(); // планирует перерисовку на следующий кадр requestAnimationFrame
m.redraw.sync(); // немедленно вызывает перерисовку и ждет ее завершенияm.startComputation/m.endComputation удалены
Они считаются антипаттернами и имеют ряд проблемных крайних случаев, поэтому они были удалены без замены в v2.x.
Функция controller компонента
В v2.x свойство controller в компонентах упразднено. Вместо него следует использовать oninit.
v0.2.x
m.mount(document.body, {
controller: function () {
var ctrl = this;
ctrl.fooga = 1;
},
view: function (ctrl) {
return m('p', ctrl.fooga);
},
});v2.x
m.mount(document.body, {
oninit: function (vnode) {
vnode.state.fooga = 1;
},
view: function (vnode) {
return m('p', vnode.state.fooga);
},
});
// ИЛИ
m.mount(document.body, {
// this привязан к vnode.state по умолчанию
oninit: function (vnode) {
this.fooga = 1;
},
view: function (vnode) {
return m('p', this.fooga);
},
});Аргументы компонента
В v2.x аргументы компонента должны передаваться в виде объекта. Простые типы данных, такие как String, Number или Boolean, будут интерпретированы как текстовые дочерние элементы. Доступ к аргументам внутри компонента осуществляется путём их чтения из объекта vnode.attrs.
v0.2.x
var Component = {
controller: function (options) {
// options.fooga === 1
},
view: function (ctrl, options) {
// options.fooga === 1
},
};
m('div', m.component(Component, { fooga: 1 }));v2.x
var Component = {
oninit: function (vnode) {
// vnode.attrs.fooga === 1
},
view: function (vnode) {
// vnode.attrs.fooga === 1
},
};
m('div', m(Component, { fooga: 1 }));Дочерние элементы vnode компонента
В v0.2.x дочерние элементы vnode компонента не были нормализованы, а просто передавались в качестве дополнительных аргументов, и они также не были сглажены. Внутри это работало путём возврата частично применённого компонента, который сравнивался с предыдущим состоянием на основе факта частичного применения. В v2.x дочерние элементы vnode компонента передаются через vnode.children как разрешённый массив дочерних элементов, но, как и в v0.2.x, отдельные дочерние элементы не нормализуются, и массив дочерних элементов не выравнивается.
v0.2.x
var Component = {
controller: function (value, renderProp) {
// value === "value"
// typeof renderProp === "function"
},
view: function (ctrl, value, renderProp) {
// value === "value"
// typeof renderProp === "function"
},
};
m(
'div',
m.component(Component, 'value', function (key) {
return 'child';
})
);v2.x
var Component = {
oninit: function (vnode) {
// vnode.children[0] === "value"
// typeof vnode.children[1] === "function"
},
view: function (vnode) {
// vnode.children[0] === "value"
// typeof vnode.children[1] === "function"
},
};
m(
'div',
m(Component, 'value', function (key) {
return 'child';
})
);Дочерние элементы DOM vnode
В v0.2.x дочерние элементы DOM-узлов представлялись буквально, без какой-либо нормализации, за исключением использования дочерних элементов напрямую, если присутствует только один дочерний массив. Он возвращал структуру, больше похожую на эту, со строками, представленными буквально.
m("div", "value", ["nested"])
// Становится:
{
tag: "div",
attrs: {},
children: [
"value",
["nested"],
]
}В v2.x дочерние элементы DOM vnode нормализуются в объекты с единообразной структурой.
m("div", "value", ["nested"])
// Становится примерно:
{
tag: "div",
attrs: null,
children: [
{tag: "#", children: "value"},
{tag: "[", children: [
{tag: "#", children: "nested"},
]},
]
}Если в DOM vnode присутствует только один текстовый дочерний элемент, вместо этого он устанавливает для свойства text это значение.
m("div", "value")
// Становится примерно:
{
tag: "div",
attrs: null,
text: "",
children: undefined,
}См. документацию по vnode для получения более подробной информации о структуре vnode v2.x и о том, как всё нормализуется.
Большинство свойств vnode v2.x здесь опущены для краткости.
Ключи
В v0.2.x вы могли свободно смешивать vnode с ключами и без них.
В v2.x списки дочерних элементов как фрагментов, так и элементов должны быть либо все с ключами, либо все без ключей. Пустые элементы также считаются элементами без ключа и теперь учитываются при проверке.
Если вам нужно обойти это ограничение, используйте шаблон фрагмента, содержащего один vnode, например [m("div", {key: whatever})].
Параметры view()
В v0.2.x функциям представления передавалась ссылка на экземпляр controller и (необязательно) любые параметры, переданные компоненту. В v2.x им передаётся только объект vnode, аналогично функции oninit.
v0.2.x
m.mount(document.body, {
controller: function () {},
view: function (ctrl, options) {
// ...
},
});v2.x
m.mount(document.body, {
oninit: function (vnode) {
// ...
},
view: function (vnode) {
// Используйте vnode.state вместо ctrl
// Используйте vnode.attrs вместо options
},
});Передача компонентов в m()
В v0.2.x вы могли передавать компоненты в качестве второго аргумента m() без какой-либо необходимой обёртки. В v2.x для обеспечения единообразия компоненты всегда должны быть обёрнуты в вызов m().
v0.2.x
m('div', Component);v2.x
m('div', m(Component));Передача vnode в m.mount() и m.route()
В v0.2.x m.mount(element, component) разрешал передавать vnode в качестве второго аргумента вместо компонентов (даже если это не было задокументировано). Аналогично, m.route(element, defaultRoute, routes) принимал vnode в качестве значений в объекте routes.
В v2.x в обоих случаях ожидается передача именно компонентов.
v0.2.x
m.mount(element, m('i', 'hello'));
m.mount(element, m(Component, attrs));
m.route(element, '/', {
'/': m('b', 'bye'),
});v2.x
m.mount(element, {
view: function () {
return m('i', 'hello');
},
});
m.mount(element, {
view: function () {
return m(Component, attrs);
},
});
m.route(element, '/', {
'/': {
view: function () {
return m('b', 'bye');
},
},
});m.route.mode
В версии v1.x это заменено на m.route.prefix = prefix, где prefix — любой префикс. Если он начинается с #, используется хеш-режим, ? — режим поиска, а любой другой символ (или пустая строка) — режим пути. Поддерживаются и комбинации, например, m.route.prefix = "/path/#!" или ?#.
По умолчанию теперь используется префикс #! (hashbang) вместо просто #. Если вы использовали поведение по умолчанию и хотите сохранить существующие URL-адреса, укажите m.route.prefix = "#" перед инициализацией маршрутов.
v0.2.x
m.route.mode = 'hash';
m.route.mode = 'pathname';
m.route.mode = 'search';v2.x
// Прямые соответствия
m.route.prefix = '#';
m.route.prefix = '';
m.route.prefix = '?';m.route() и якорные теги
Обработка маршрутизируемых ссылок теперь использует специальный встроенный компонент вместо атрибута config. Если вы использовали config: m.route на <button> и подобных элементах, укажите имя этого тега, используя атрибут selector: "button".
v0.2.x
// При нажатии на эту ссылку будет загружен маршрут "/path" вместо перехода по ссылке
m('a', {
href: '/path',
config: m.route,
});v2.x
// При нажатии на эту ссылку будет загружен маршрут "/path" вместо перехода по ссылке
m(m.route.Link, {
href: '/path',
});Шаблоны путей
В v1.x существовало три отдельных синтаксиса шаблонов путей, которые, хотя и были похожи, имели 2 отдельных разработанных синтаксиса и 3 разные реализации. Это было довольно произвольно, и параметры обычно не экранировались. Теперь все либо кодируется, если это :key, либо остается без изменений, если это :key.... Если что-то неожиданно кодируется, используйте :key.... Все просто.
Вот как это влияет на каждый метод:
URL-адреса m.request
Компоненты пути в v2.x экранируются автоматически при интерполяции и считывают свои значения из params. В v0.2.x m.request({url: "/user/:name/photos/:id", data: {name: "a/b", id: "c/d"}}) отправлял бы запрос с URL-адресом /user/a%2Fb/photos/c/d. В v2.x соответствующий m.request({url: "/user/:name/photos/:id", params: {name: "a/b", id: "c/d"}}) отправит запрос на /user/a%2Fb/photos/c%2Fd. Если вы намеренно хотите интерполировать ключ без экранирования, используйте :key....
Интерполяции во встроенных строках запроса, например, в /api/search?q=:query, не выполняются в v2.x. Передавайте их через params с соответствующими именами ключей вместо указания в строке запроса.
Обратите внимание, что это также относится к m.jsonp. При переходе с m.request + dataType: "jsonp" на m.jsonp вам также необходимо помнить об этом.
Пути m.route(route, params, shouldReplaceHistoryEntry)
Теперь они допускают интерполяции и работают идентично m.request.
Шаблоны маршрутов m.route
Ключи пути вида :key... возвращают URL-адрес, декодированный в v1.x, но возвращают необработанный URL-адрес в v2.x.
Ранее, такие конструкции, как :key.md, ошибочно принимались, при этом значение результирующего параметра устанавливалось в keymd: "...". Это больше не так - .md теперь является частью шаблона, а не имени параметра.
Чтение/запись текущего маршрута
В v0.2.x все взаимодействия с текущим маршрутом осуществлялись через m.route(). В v2.x это разделено на две функции.
v0.2.x
// Получение текущего маршрута
m.route();
// Установка нового маршрута
m.route('/other/route');v2.x
// Получение текущего маршрута
m.route.get();
// Установка нового маршрута
m.route.set('/other/route');Доступ к параметрам маршрута
В v0.2.x чтение параметров маршрута полностью осуществлялось через m.route.param(). Этот API по-прежнему доступен в v2.x, и, кроме того, любые параметры маршрута передаются в виде свойств в объекте attrs на виртуальном узле.
v0.2.x
m.route(document.body, '/booga', {
'/:attr': {
controller: function () {
m.route.param('attr'); // "booga"
},
view: function () {
m.route.param('attr'); // "booga"
},
},
});v2.x
m.route(document.body, '/booga', {
'/:attr': {
oninit: function (vnode) {
vnode.attrs.attr; // "booga"
m.route.param('attr'); // "booga"
},
view: function (vnode) {
vnode.attrs.attr; // "booga"
m.route.param('attr'); // "booga"
},
},
});Создание/Разбор строк запроса
v0.2.x использовал методы, связанные с m.route, m.route.buildQueryString() и m.route.parseQueryString(). В v2.x они разделены и перемещены в корень m.
v0.2.x
var qs = m.route.buildQueryString({ a: 1 });
var obj = m.route.parseQueryString('a=1');v2.x
var qs = m.buildQueryString({ a: 1 });
var obj = m.parseQueryString('a=1');Кроме того, в v2.x {key: undefined} сериализуется как key=undefined с помощью m.buildQueryString и методов, которые его используют, например, m.request. В v0.2.x ключ был опущен, и это перешло в m.request. Если вы ранее полагались на это, измените свой код, чтобы полностью опустить ключи из объекта. Если вам необходимо сохранить поведение v0.2.x, и вы не можете легко удалить ключи со значением undefined, используйте простую утилиту для удаления таких ключей из объекта.
// Вызывайте всякий раз, когда вам нужно опустить параметры `undefined` из объекта.
function omitUndefineds(object) {
var result = {};
for (var key in object) {
if ({}.hasOwnProperty.call(object, key)) {
var value = object[key];
if (Array.isArray(value)) {
result[key] = value.map(omitUndefineds);
} else if (value != null && typeof value === 'object') {
result[key] = omitUndefineds(value);
} else if (value !== undefined) {
result[key] = value;
}
}
}
return result;
}Предотвращение размонтирования
Теперь невозможно предотвратить размонтирование с помощью e.preventDefault() в onunload. Вместо этого вы должны явно вызвать m.route.set, когда будут выполнены ожидаемые условия.
v0.2.x
var Component = {
controller: function () {
this.onunload = function (e) {
if (condition) e.preventDefault();
};
},
view: function () {
return m('a[href=/]', { config: m.route });
},
};v2.x
var Component = {
view: function () {
return m('a', {
onclick: function () {
if (!condition) m.route.set('/');
},
});
},
};Запуск кода при удалении компонента
Компоненты больше не вызывают this.onunload, когда они удаляются. Теперь они используют стандартизированный хук жизненного цикла onremove.
v0.2.x
var Component = {
controller: function () {
this.onunload = function (e) {
// ...
};
},
view: function () {
// ...
},
};v2.x
var Component = {
onremove: function() {
// ...
}
view: function() {
// ...
}
}m.request
Промисы, возвращаемые m.request, больше не являются геттер-сеттерами m.prop. Кроме того, initialValue, unwrapSuccess и unwrapError больше не являются поддерживаемыми опциями.
Запросы больше не имеют семантики m.startComputation/m.endComputation. Вместо этого перерисовки всегда запускаются при завершении цепочки промисов запроса (если не установлено background: true).
Параметр data теперь разделен на params — параметры запроса, интерполированные в URL-адрес и добавленные к запросу, и body — тело для отправки в базовом XHR.
В v0.2.x вы использовали бы dataType: "jsonp" для инициирования JSONP-запроса. В v2.x теперь вы используете m.jsonp, который имеет в основном тот же API, что и m.request, без частей, связанных с XHR.
v0.2.x
var data = m.request({
method: 'GET',
url: 'https://api.github.com/',
initialValue: [],
});
setTimeout(function () {
console.log(data());
}, 1000);
m.request({
method: 'POST',
url: 'https://api.github.com/',
data: someJson,
});v2.x
var data = [];
m.request({
method: 'GET',
url: 'https://api.github.com/',
}).then(function (responseBody) {
data = responseBody;
});
setTimeout(function () {
console.log(data); // note: not a getter-setter
}, 1000);
m.request({
method: 'POST',
url: 'https://api.github.com/',
body: someJson,
});
// ИЛИ
var data = [];
m.request('https://api.github.com/').then(function (responseBody) {
data = responseBody;
});
setTimeout(function () {
console.log(data); // note: not a getter-setter
}, 1000);
m.request('https://api.github.com/', {
method: 'POST',
body: someJson,
});Кроме того, если опция extract передана в m.request, возвращаемое значение предоставленной функции будет использоваться непосредственно для разрешения промиса запроса, а обратный вызов deserialize игнорируется.
Заголовки m.request
В v0.2.x Mithril.js не устанавливал заголовков по умолчанию в запросах. Теперь он устанавливает до 2 заголовков:
Content-Type: application/json; charset=utf-8для запросов с телами JSON, которые!= nullAccept: application/json, text/*для запросов, ожидающих ответы JSON
Заголовок Content-Type вызовет предварительную выборку CORS, так как он не входит в список CORS-safelisted request header из-за указанного типа контента. Это может привести к новым ошибкам в зависимости от конфигурации CORS на вашем сервере. Если у вас возникнут проблемы с этим, вам может потребоваться переопределить этот заголовок, передав headers: {"Content-Type": "text/plain"}. (Заголовок Accept ничего не вызывает, поэтому вам не нужно его переопределять.)
Единственные типы контента, которые спецификация Fetch позволяет избежать проверок предварительной выборки CORS, — это application/x-www-form-urlencoded, multipart/form-data и text/plain. Он не разрешает ничего другого и намеренно запрещает JSON.
m.deferred удален
v0.2.x использовал свой собственный пользовательский объект асинхронного контракта, представленный как m.deferred, который использовался в качестве основы для m.request. В v2.x, вместо этого, следует использовать Promises. В ситуациях, где ранее использовался m.deferred, следует использовать Promises.
v0.2.x
var greetAsync = function () {
var deferred = m.deferred();
setTimeout(function () {
deferred.resolve('hello');
}, 1000);
return deferred.promise;
};
greetAsync()
.then(function (value) {
return value + ' world';
})
.then(function (value) {
console.log(value);
}); //logs "hello world" after 1 secondv2.x
var greetAsync = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve('hello');
}, 1000);
});
};
greetAsync()
.then(function (value) {
return value + ' world';
})
.then(function (value) {
console.log(value);
}); //logs "hello world" after 1 secondm.sync удален
В v2.x, благодаря использованию Promises, соответствующих стандартам, m.sync является избыточным. Используйте Promise.all вместо этого.
v0.2.x
m.sync([
m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
m.request({
method: 'GET',
url: 'https://api.github.com/users/dead-claudia',
}),
]).then(function (users) {
console.log('Contributors:', users[0].name, 'and', users[1].name);
});v2.x
Promise.all([
m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
m.request({
method: 'GET',
url: 'https://api.github.com/users/dead-claudia',
}),
]).then(function (users) {
console.log('Contributors:', users[0].name, 'and', users[1].name);
});Требуется пространство имен xlink
В v0.2.x пространство имен xlink было единственным поддерживаемым пространством имен атрибутов, и оно поддерживалось с помощью специального поведения. Теперь синтаксический анализ пространств имен полностью поддерживается, и атрибуты с пространством имен должны явно объявлять свое пространство имен.
v0.2.x
m(
'svg',
// the `href` attribute is namespaced automatically
m("image[href='image.gif']")
);v2.x
m(
'svg',
// Пространство имен, указанное пользователем для атрибута `href`
m("image[xlink:href='image.gif']")
);Вложенные массивы в представлениях
Теперь массивы представляют фрагменты, которые структурно значимы в виртуальном DOM v2.x. В то время как вложенные массивы в v0.2.x были бы сведены в один непрерывный список виртуальных узлов для целей дифференцирования, v2.x сохраняет структуру массива - дочерние элементы любого данного массива не считаются одноуровневыми с дочерними элементами смежных массивов.
Проверки равенства vnode
Если виртуальный узел строго равен виртуальному узлу, занимающему его место в последней отрисовке, v2.x пропустит эту часть дерева, не проверяя мутации и не запуская какие-либо методы жизненного цикла в поддереве. Документация компонента содержит более подробную информацию по этому вопросу.