從 v0.2.x 遷移
v1.x 和 v2.x 在很大程度上與 v0.2.x 的 API 相容,但仍存在一些重大變更。遷移到 v2.x 幾乎完全相同,因此以下說明主要適用於兩者。
如果您正在遷移,請考慮使用 mithril-codemods 工具來協助自動化最直接的遷移。
移除 m.prop
在 v2.x 中,m.prop()
已經轉換為一個更強大的 stream 微函式庫,但它不再是核心的一部分。您可以在文件中閱讀關於如何使用可選的 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 沒有出現類似廣泛且慣用的 stream 用法,因此 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,此 Promise 會在
// 準備好從 DOM 移除節點時解析
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()
來強制 Mithril.js 立即重繪。在 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, {
// 預設情況下,這會繫結到 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 屬性。
Key
在 v0.2.x 中,您可以自由混合帶有 key 和不帶 key 的 vnode。
在 v2.x 中,片段和元素的子節點清單必須全部帶 key 或全部不帶 key。出於此檢查的目的,Hole 也被認為是不帶 key 的 - 它不再忽略它們。
如果您需要解決這個問題,請使用包含單一 vnode 的片段的慣用法,例如 [m("div", {key: whatever})]
。
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
來設定。在 v1.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 中,有三種獨立的路徑模板語法,雖然它們很相似,但實際上是 2 種獨立設計的語法,並有 3 種不同的實現。它是以相當臨時的方式定義的,並且參數通常沒有經過轉義。現在,所有內容都會在 :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,請改用 :key...
。
在 v2.x 中,不會執行內嵌查詢字串中的插值,例如 /api/search?q=:query
。請改為透過具有適當 key 名稱的 params
傳遞這些參數,而無需在查詢字串中指定它。
請注意,這也適用於 m.jsonp
。從 m.request
+ dataType: "jsonp"
遷移到 m.jsonp
時,您也需要注意這一點。
m.route(route, params, shouldReplaceHistoryEntry)
路徑
現在這些允許插值,且其工作方式與 m.request
相同。
m.route
路由模式
形式為 :key...
的路徑 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 中,key 被省略,並且這延續到 m.request
。如果您之前依賴此行為,請變更您的程式碼以完全從物件中省略 key。如果不容易做到這一點,且您需要保留 v0.2.x 的行為,則可能值得使用一個簡單的工具函式,從物件中移除所有值為 undefined
的 key。
// 每當您需要從物件中省略 `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 body 的請求,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 中,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 就會跳過樹狀結構的該部分,而不檢查變更或觸發子樹中的任何生命週期方法。元件文件中包含關於此問題的更多詳細資訊。