从 v0.2.x 迁移
v1.x 和 v2.x 在很大程度上与 v0.2.x 的 API 兼容,但仍然存在一些重大更改。迁移到 v2.x 的过程与 v0.2.x 类似,因此以下说明主要适用于两者。
如果您正在迁移,请考虑使用 mithril-codemods 工具来帮助自动化最简单的迁移过程。
m.prop 已移除
在 v2.x 中,m.prop() 被改造为一个功能更强大的流微型库,但它已不再是核心部分。您可以在文档中阅读有关如何使用可选的 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 中,可以直接从事件的目标对象读取属性。它曾经与 m.prop 配合良好,但由于 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 已移除
它的用途通常不大,您可以随时自行添加回来。建议使用功能检测来了解可用的功能,并且 v2.x API 旨在更好地实现这一点。
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,表示节点准备好被删除
onbeforeremove: function (vnode) {
/*...*/
},
// 在删除节点之前调用,但在 onbeforeremove 调用 done() 之后执行
onremove: function (vnode) {
/*...*/
},
});如果可用,则可以在 vnode.dom 访问 vnode 的 DOM 元素。
重绘行为的更改
Mithril.js 的渲染引擎仍然基于半自动的全局重绘运行,但某些 API 和行为有所不同:
不再有重绘锁
在 v0.2.x 中,Mithril.js 允许“重绘锁”,该锁暂时阻止了阻塞绘制逻辑:默认情况下,m.request 会在执行时锁定绘制循环,并在所有挂起的请求都已解决时解锁 - 可以使用 m.startComputation() 和 m.endComputation() 手动调用相同的行为。后者 API 和相关的行为已在 v2.x 中删除,没有替代项。重绘锁定可能导致 UI 错误:应用程序的一部分不应阻止其他部分的视图更新。
从事件处理程序取消重绘
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 中,可以通过传递真值给 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,
}有关 v2.x vnode 结构及其规范化的详细信息,请参见 vnode 文档。
为了简洁起见,此处省略了大多数 v2.x vnode 属性。
键
在 v0.2.x 中,可以自由混合键控和非键控 vnode。
在 v2.x 中,片段和元素的子节点列表必须全部键控或全部非键控。为了进行此项检查,空位也被认为是非键控的 - 现在不会再忽略它们。
如需解决此问题,请使用包含单个 vnode 的片段。
view() 参数
在 v0.2.x 中,视图函数传递 controller 实例引用及可选的组件选项。在 v2.x 中,仅传递 vnode,与 controller 函数完全相同。
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
在 v0.2.x 中,可以通过将字符串 "pathname"、"hash" 或 "search" 分配给 m.route.mode 来设置路由模式。在 v.1.x 中,已被 m.route.prefix = prefix 替换,其中 prefix 可以是任何前缀。如果前缀以 # 开头,则使用"hash"模式,? 以“search”模式工作,任何其他字符(或空字符串)以“pathname”模式工作。它还支持上述组合,例如 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() 和锚标记
现在处理可路由链接需要使用特殊的内置组件,而不是属性。在 <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 中,有三种路径模板语法,尽管相似,但设计和实现各有不同。它的定义方式相当随意,并且参数通常没有进行转义。现在,若为 :key,则所有内容编码;若为 :key...,则为原始。如果出现意外的编码情况,请使用 :path...。就这么简单。
具体来说,以下是它如何影响每种方法:
m.request URL
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...。
v2.x 不会对内联查询字符串执行插值,例如 /api/search?q=:query。请通过 params 传递这些,并使用适当的键名,而无需在查询字符串中指定它。
需要注意的是,这一变化同样适用于 m.jsonp。从 m.request + dataType: "jsonp" 迁移到 m.jsonp 时,您还需要注意这一点。
m.route(route, params, shouldReplaceHistoryEntry) 路径
这些路径现在支持插值,其工作方式与 m.request 相同。
m.route 路由模式
形式为 :key... 的路径键在 v1.x 中返回其 URL 解码,但在 v2.x 中返回原始 URL。
以前,像 :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 中仍可用,且路由参数作为 vnode 的 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} 被 m.buildQueryString 和相关方法(如 m.request)序列化为 key=undefined。在 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;
}阻止卸载
无法再通过 onunload 的 e.preventDefault() 阻止卸载。应在满足条件时显式调用 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 返回的 Promise 不再是 m.prop 的 getter/setter。此外,不再支持 initialValue、unwrapSuccess 和 unwrapError 选项。
此外,请求不再具有 m.startComputation/m.endComputation 的语义。相反,当请求 Promise 链完成时,总会触发重绘(除非设置了 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); // 注意:不是 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); // 注意:不是 getter-setter
}, 1000);
m.request('https://api.github.com/', {
method: 'POST',
body: someJson,
});此外,如果将 extract 选项传递给 m.request,则提供的函数的返回值将直接用于解析请求 Promise,并且 deserialize 回调将被忽略。
m.request 标头
在 v0.2.x 中,Mithril.js 默认情况下未在请求上设置任何标头。现在,它最多设置 2 个标头:
- 对于具有
!= null的 JSON 正文的请求,Content-Type: application/json; charset=utf-8 - 对于期望 JSON 响应的请求,
Accept: application/json, text/*
由于指定的内容类型,这两个标头中的第一个标头 Content-Type 将触发 CORS 预检,因为它不是 CORS 安全列出的请求标头,这可能会根据服务器上 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 改为使用 Promise,并在不支持的环境中提供了 polyfill。在原本需要使用 m.deferred 的场景下,您应该改为使用 Promise。
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);
}); //1 秒后记录“hello world”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);
}); //1 秒后记录“hello world”m.sync 已移除
由于 v2.x 使用符合标准的 Promise,因此 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
m(
'svg',
// `href` 属性会自动命名空间
m("image[href='image.gif']")
);v2.x
m(
'svg',
// 用户在 `href` 属性上指定的命名空间
m("image[xlink:href='image.gif']")
);视图中的嵌套数组
数组现在代表 片段,这在 v2.x 虚拟 DOM 中具有结构意义。在 v0.2.x 中,嵌套数组会被扁平化成一个连续的虚拟节点列表用于比对,而 v2.x 则保留了数组结构 - 任何给定数组的子节点都不会被认为是相邻数组的同级节点。
vnode 相等性检查
如果一个 vnode 与它在上次绘制中占据相同位置的 vnode 严格相等,那么 v2.x 将跳过树的该部分,而不会检查突变或触发子树中的任何生命周期方法。组件文档包含 有关此问题的更多详细信息。