Переход с 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, которые!= null
Accept: 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 second
v2.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 second
m.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 пропустит эту часть дерева, не проверяя мутации и не запуская какие-либо методы жизненного цикла в поддереве. Документация компонента содержит более подробную информацию по этому вопросу.