从 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 将跳过树的该部分,而不会检查突变或触发子树中的任何生命周期方法。组件文档包含 有关此问题的更多详细信息。