从 v1.x 迁移
v2.x 的 API 几乎完全兼容 v1.x,但仍存在一些不兼容的变更。
赋值给 vnode.state
在 v1.x 中,你可以随意操作 vnode.state
并赋值任何内容。但在 v2.x 中,尝试修改它会抛出错误。迁移方法因情况而异,但在大多数情况下,只需将对 vnode.state
的引用更改为 vnode.state.foo
,并为 foo
选择一个合适的名称(例如,如果它表示计数器的当前值,可以命名为 count
)。
v1.x
var Counter = {
oninit: function (vnode) {
vnode.state = 0;
},
view: function (vnode) {
return m('.counter', [
m(
'button',
{
onclick: function () {
vnode.state--;
},
},
'-'
),
vnode.state,
m(
'button',
{
onclick: function () {
vnode.state++;
},
},
'+'
),
]);
},
};
v2.x
var Counter = {
oninit: function (vnode) {
vnode.state.count = 0;
},
view: function (vnode) {
return m('.counter', [
m(
'button',
{
onclick: function () {
vnode.state.count--;
},
},
'-'
),
vnode.state.count,
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'+'
),
]);
},
};
当 v1.0 首次发布时,类组件和闭包组件还不存在,因此 Mithril 只是从 vnode.tag
中提取所需的信息。这个实现细节允许你这样做,并且在文档的某些地方也暗示了这一点。现在,情况有所不同,从实现的角度来看,管理起来更容易一些,因为只有一个对状态的引用,而不是两个。
路由锚点的更改
在 v1.x 中,你使用 oncreate: m.route.link
,如果链接可能发生变化,则同时使用 onupdate: m.route.link
。它们都是 vnode 上的生命周期钩子,用于路由导航。在 v2.x 中,现在使用 m.route.Link
组件。如果使用 m("a", ...)
以外的元素,可以通过 selector:
属性指定选择器,通过 options:
指定选项,通过 disabled:
属性禁用它,并且可以内联指定其他属性,包括 href:
(必需)。selector:
本身可以包含任何作为 m
的第一个参数有效的选择器,并且属性 [href=...]
和 [disabled]
可以在选择器中以及普通选项中指定。
v1.x
m('a', {
href: '/path',
oncreate: m.route.link,
});
m('button', {
href: '/path',
oncreate: m.route.link,
});
m('button.btn[href=/path]', {
oncreate: m.route.link,
});
v2.x
m(m.route.Link, {
href: '/path',
});
m(m.route.Link, {
selector: 'button',
href: '/path',
});
m(m.route.Link, {
selector: 'button.btn[href=/path]',
});
m.request
错误的更改
在 v1.x 中,m.request
解析来自 JSON 调用的错误,并将解析后对象的属性赋值给响应。因此,如果收到一个状态码为 403 的响应,且响应体 (body) 为 {"code": "backoff", "timeout": 1000}
,那么错误对象将包含两个额外的属性:err.code = "backoff"
和 err.timeout = 1000
。
在 v2.x 中,响应被赋值给结果的 response
属性,而 code
属性包含 HTTP 状态码。因此,如果你收到一个状态码为 403 的响应,且响应体 (body) 为 {"code": "backoff", "timeout": 1000}
,则该错误将被赋值两个属性:err.response = {code: "backoff", timeout: 1000}
和 err.code = 403
。
m.withAttr
已移除
在 v1.x 中,事件监听器可以使用 oninput: m.withAttr("value", func)
这样的方式。在 v2.x 中,只需直接从事件目标读取它们。虽然它与流(stream)配合得很好,但由于 m.withAttr("value", stream)
的使用频率远低于 m.withAttr("value", prop)
,m.withAttr
的实用性大大降低,因此被移除。
v1.x
var value = '';
// In your view
m('input[type=text]', {
value: value(),
oninput: m.withAttr('value', function (v) {
value = v;
}),
});
// OR
var value = m.stream('');
// In your view
m('input[type=text]', {
value: value(),
oninput: m.withAttr('value', value),
});
v2.x
var value = '';
// In your view
m('input[type=text]', {
value: value,
oninput: function (ev) {
value = ev.target.value;
},
});
// OR
var value = m.stream('');
// In your view
m('input[type=text]', {
value: value(),
oninput: function (ev) {
value(ev.target.value);
},
});
m.route.prefix
在 v1.x 中,m.route.prefix
是一个通过 m.route.prefix(prefix)
调用的函数。现在它是一个你可以通过 m.route.prefix = prefix
设置的属性。
v1.x
m.route.prefix('/root');
v2.x
m.route.prefix = '/root';
m.request
/m.jsonp
的 params 和 body
data
和 useBody
被重构为 params
(用于将查询参数插入到 URL 并附加到请求) 和 body
(用于在底层 XHR 中发送请求体)。这让你更好地控制实际发送的请求,并允许你既可以使用 POST
请求将参数插入到查询参数中,也可以创建带有 body 的 GET
请求。
m.jsonp
没有有意义的 "body",只使用 params
,因此将 data
重命名为 params
对于该方法来说就足够了。
v1.x
m.request('https://example.com/api/user/:id', {
method: 'GET',
data: { id: user.id },
});
m.request('https://example.com/api/user/create', {
method: 'POST',
data: userData,
});
v2.x
m.request('https://example.com/api/user/:id', {
method: 'GET',
params: { id: user.id },
});
m.request('https://example.com/api/user/create', {
method: 'POST',
body: userData,
});
路径模板
在 v1.x 中,有三种独立的路径模板语法,虽然它们很相似,但有两种单独设计的语法和三种不同的实现。它的定义方式相当随意,并且参数通常不会进行转义。现在,对于 :key
,所有内容都会被编码;对于 :key...
,则保持原始状态。如果遇到意外的编码,请使用 :path...
。就这么简单。
具体来说,以下是它如何影响每个方法:
m.request
和 m.jsonp
的 URL,m.route.set
的路径
在 v2.x 中,路径组件在进行插值时会自动进行转义。假设你调用 m.route.set("/user/:name/photos/:id", {name: user.name, id: user.id})
。以前,如果 user
是 {name: "a/b", id: "c/d"}
,这将把路由设置为 /user/a%2Fb/photos/c/d
,但现在它将把它设置为 /user/a%2Fb/photos/c%2Fd
。如果你确实需要插入一个未经转义的键,请使用 :key...
。
v2.x 中的键不能包含任何 .
或 -
的实例。在 v1.x 中,它们可以包含除 /
之外的任何内容。
在 v2.x 中,不会执行内联查询字符串中的插值,例如 /api/search?q=:query
。请通过带有适当键名的 params
传递这些参数,而无需在查询字符串中指定它。
m.route
路由模式
对于 :key...
形式的路径键,v1.x 返回的是 URL 解码后的值,而 v2.x 返回的是原始 URL。
以前,像 :key.md
这样的写法会被错误地接受,并将生成的参数的值设置为 keymd: "..."
。现在情况并非如此 - .md
现在是模式的一部分,而不是名称。
生命周期调用顺序
在 v1.x 中,组件 vnode 属性上的生命周期钩子函数总是在组件自身的生命周期钩子函数之前被调用。在 v2.x 中,只有 onbeforeupdate
钩子函数是这种情况。因此,你可能需要相应地调整代码。
v1.x
var Comp = {
oncreate: function () {
console.log('Component oncreate');
},
view: function () {
return m('div');
},
};
m.mount(document.body, {
view: function () {
return m(Comp, {
oncreate: function () {
console.log('Attrs oncreate');
},
});
},
});
// Logs:
// Attrs oncreate
// Component oncreate
v2.x
var Comp = {
oncreate: function () {
console.log('Component oncreate');
},
view: function () {
return m('div');
},
};
m.mount(document.body, {
view: function () {
return m(Comp, {
oncreate: function () {
console.log('Attrs oncreate');
},
});
},
});
// Logs:
// Component oncreate
// Attrs oncreate
m.redraw
同步性
v2.x 中的 m.redraw()
始终是异步的。你可以通过 m.redraw.sync()
显式请求同步重绘,前提是当前没有正在进行的重绘。
选择器属性优先级
在 v1.x 中,选择器属性优先于属性对象中指定的属性。例如,m("[a=b]", {a: "c"}).attrs
返回 {a: "b"}
。
在 v2.x 中,属性对象中指定的属性优先于选择器属性。例如,m("[a=b]", {a: "c"}).attrs
返回 {a: "c"}
。
请注意,这实际上是恢复到了 v0.2.x 版本的行为。
子节点规范化
在 v1.x 中,组件 vnode 子节点像其他 vnode 一样被规范化。在 v2.x 中,情况已经改变,你需要据此进行调整。这不会影响在渲染时完成的规范化。
m.request
标头
在 v1.x 中,Mithril.js 在所有非 GET
请求上设置了这两个标头,但仅当 useBody
设置为 true
(默认值)并且满足列出的其他条件时:
- 对于包含 JSON 请求体的请求,
Content-Type: application/json; charset=utf-8
- 对于预期返回 JSON 响应的请求,
Accept: application/json, text/*
在 v2.x 中,对于所有包含非空 ( != null
) JSON 请求体的请求,Mithril.js 会设置第一个标头。否则,默认情况下会省略该标头。此行为与所选的 HTTP 方法无关,包括 GET
请求。
这两个标头中的其中第一个标头,Content-Type
,将触发 CORS 预检,因为它不是 CORS 安全列表请求标头,因为指定的内容类型,并且这可能会引入新的错误,具体取决于 CORS 在你的服务器上的配置方式。如果遇到此类问题,你可能需要通过传递 headers: {"Content-Type": "text/plain"}
来覆盖该标头。(Accept
标头不会触发任何内容,因此你无需覆盖它。)
Fetch 规范允许避免 CORS 预检检查的唯一内容类型是 application/x-www-form-urlencoded
、multipart/form-data
和 text/plain
。它不允许任何其他内容,并且它有意不允许 JSON。
路由中哈希字符串中的查询参数
在 v1.x 中,你可以在查询字符串和哈希字符串中为路由指定查询参数,因此 m.route.set("/route?foo=1&bar=2")
、m.route.set("/route?foo=1#bar=2")
和 m.route.set("/route#foo=1&bar=2")
都是等效的,并且从中提取的属性将是 {foo: "1", bar: "2"}
。
在 v2.x 中,哈希字符串的内容将被忽略但保留。因此,从以下每个 URL 中提取的属性将是:
m.route.set("/route?foo=1&bar=2")
→{foo: "1", bar: "2"}
m.route.set("/route?foo=1#bar=2")
→{foo: "1"}
m.route.set("/route#foo=1&bar=2")
→{}
这样做的原因是像 https://example.com/#!/route#key
这样的 URL 在技术上根据 URL 规范 是无效的,甚至根据 之前的 RFC 也是无效的,并且只是 HTML 规范的一个特殊处理允许它们。
或者简而言之,停止使用无效的 URL!
键
在 v1.x 中,你可以自由地混合键控和非键控 vnode。如果第一个节点是键控的,则执行键控差异,假设每个元素都有一个键,并且只是忽略它前进时的空洞。否则,将执行迭代差异。如果一个节点包含键,则在检查标签等属性的同时,还会检查该键是否发生了更改。
在 v2.x 中,片段和元素的子节点列表必须全部键控或全部非键控。为了进行此检查,空洞也被认为是非键控的 - 它不再忽略它们。
如果需要规避此限制,可以使用包含单个 vnode 的片段,例如 [m("div", {key: whatever})]
。
m.version
已移除
它通常用处不大,你可以根据需要自行添加。你应该更喜欢使用特性检测来了解哪些特性可用,并且 v2.x API 旨在更好地支持这一点。