組件
結構
組件是一種封裝視圖部分,以便更容易組織和/或重用程式碼的機制。
任何具有 view
方法的 JavaScript 物件都是 Mithril.js 組件。 組件可以通過 m()
實用工具使用:
// 定義你的組件
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// 使用你的組件
m(Example);
// 等效的 HTML
// <div>Hello</div>
生命周期方法
組件可以具有與虛擬 DOM 節點相同的 生命週期方法。 請注意,vnode
作為參數傳遞給每個生命週期方法,以及 view
(前一個的 vnode 另外傳遞給 onbeforeupdate
):
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('initialized');
},
oncreate: function (vnode) {
console.log('DOM created');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM updated');
},
onbeforeremove: function (vnode) {
console.log('exit animation can start');
return new Promise(function (resolve) {
// 動畫完成後調用
resolve();
});
},
onremove: function (vnode) {
console.log('removing DOM element');
},
view: function (vnode) {
return 'hello';
},
};
與其他類型的虛擬 DOM 節點一樣,組件在作為 vnode 類型使用時,可以定義額外的生命週期方法。
function initialize(vnode) {
console.log('initialized as vnode');
}
m(ComponentWithHooks, { oninit: initialize });
vnode 中的生命週期方法不會覆蓋組件方法,反之亦然。 組件生命週期方法始終在 vnode 的相應方法之後執行。
請注意不要在 vnode 中將生命週期方法名稱用於您自己的回調函數名稱。
要了解有關生命週期方法的更多資訊,請參閱生命週期方法頁面。
將資料傳遞給組件
可以通過將 attrs
物件作為 hyperscript 函數中的第二個參數傳遞,將資料傳遞給組件實例:
m(Example, { name: 'Floyd' });
此資料可以在組件的視圖或生命週期方法中通過 vnode.attrs
存取:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
注意:生命週期方法也可以在 attrs
物件中定義,因此您應該避免將它們的名稱用於您自己的回調,因為它們也會被 Mithril.js 本身調用。 僅當您特別希望將它們用作生命週期方法時,才在 attrs
中使用它們。
狀態
與所有虛擬 DOM 節點一樣,組件 vnode 可以具有狀態。 組件狀態對於支援物件導向架構、封裝和關注點分離非常有用。
請注意,與許多其他框架不同,變更組件狀態_不會_觸發 重繪 或 DOM 更新。 相反,當事件處理程式觸發、m.request 發出的 HTTP 請求完成或瀏覽器導航到不同的路由時,會執行重繪。 Mithril.js 的組件狀態機制僅作為應用程式的便利而存在。
若狀態變更並非由上述條件引起(例如 setTimeout
之後),則可使用 m.redraw()
手動觸發重繪。
閉包組件狀態
在上面的示例中,每個組件都定義為 POJO(Plain Old JavaScript Object,簡單的 JavaScript 物件),Mithril.js 在內部將其用作該組件實例的原型。 可以將組件狀態與 POJO 一起使用(我們將在下面討論),但這不是最簡潔或最簡單的方法。 為此,我們將使用**閉包組件**,它只是一個包裝函數,它_返回_一個 POJO 組件實例,該實例又攜帶自己的封閉作用域。
使用閉包組件,可以簡單地通過在外部函數中聲明的變數來維護狀態:
function ComponentWithState(initialVnode) {
// 組件狀態變數,每個實例都是唯一的
var count = 0;
// POJO 組件實例:任何具有返回 vnode 的 view 函數的物件
return {
oninit: function (vnode) {
console.log('init a closure component');
},
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Increment count'
)
);
},
};
}
在閉包中聲明的任何函數也可以存取其狀態變數。
function ComponentWithState(initialVnode) {
var count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
return {
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: increment,
},
'Increment'
),
m(
'button',
{
onclick: decrement,
},
'Decrement'
)
);
},
};
}
閉包組件的使用方式與 POJO 相同,例如 m(ComponentWithState, { passedData: ... })
。
閉包組件的一個很大的優點是,在附加事件處理程式回調時,我們無需擔心繫結 this
。 事實上,this
從未使用過,我們也從不必考慮 this
上下文的歧義。
POJO 組件狀態
通常建議您使用閉包 (closure) 來管理組件狀態。 但是,如果您有理由在 POJO 中管理狀態,則可以通過三種方式存取組件的狀態:作為初始化時的藍圖、通過 vnode.state
以及通過組件方法中的 this
關鍵字。
初始化時
對於 POJO 組件,組件物件是每個組件實例的原型,因此,組件物件上定義的任何屬性,皆可作為 vnode.state
的屬性存取。 這允許簡單的「藍圖」狀態初始化。
在下面的示例中,data
成為 ComponentWithInitialState
組件的 vnode.state
物件的屬性。
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// 等效的 HTML
// <div>Initial content</div>
通過 vnode.state
如您所見,也可以通過 vnode.state
屬性存取狀態,該屬性可用於所有生命週期方法以及組件的 view
方法。
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// 等效的 HTML
// <div>Hello</div>
通過 this 關鍵字
也可以通過 this
關鍵字存取狀態,該關鍵字可用於所有生命週期方法以及組件的 view
方法。
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// 等效的 HTML
// <div>Hello</div>
請注意,使用 ES5 函數時,嵌套匿名函數中 this
的值不是組件實例。 解決此 JavaScript 限制有兩種建議方法:使用箭頭函數,或在不支援箭頭函數時使用 vnode.state
。
類別
如果它適合您的需求(例如在物件導向專案中),也可以使用類別編寫組件:
class ClassComponent {
constructor(vnode) {
this.kind = 'class component';
}
view() {
return m('div', `Hello from a ${this.kind}`);
}
oncreate() {
console.log(`A ${this.kind} was created`);
}
}
類別組件必須定義一個 view()
方法,通過 .prototype.view
檢測,以獲取要呈現的樹。
它們可以以與常規組件相同的方式使用。
// 範例:通過 m.render
m.render(document.body, m(ClassComponent));
// 範例:通過 m.mount
m.mount(document.body, ClassComponent);
// 範例:通過 m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// 範例:組件組合
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
類別組件狀態
使用類別,可以通過類別實例屬性和方法來管理狀態,並通過 this
存取:
class ComponentWithState {
constructor(vnode) {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
view() {
return m(
'div',
m('p', 'Count: ', this.count),
m(
'button',
{
onclick: () => {
this.increment();
},
},
'Increment'
),
m(
'button',
{
onclick: () => {
this.decrement();
},
},
'Decrement'
)
);
}
}
請注意,我們必須使用箭頭函數作為事件處理程式回調,以便可以正確參考 this
上下文。
混合組件種類
組件可以自由混合。 類別組件可以將閉包或 POJO 組件作為子組件,等等...
特殊屬性
Mithril.js 在幾個屬性鍵上放置了特殊的語義,因此您通常應避免在常規組件屬性中使用它們。
- 生命週期方法:
oninit
、oncreate
、onbeforeupdate
、onupdate
、onbeforeremove
和onremove
key
,用於追蹤鍵控片段中的身份tag
,用於區分 vnode 與常規屬性物件和其他非 vnode 物件。
避免反模式
儘管 Mithril.js 具有靈活性,但某些程式碼模式是不鼓勵的:
避免胖組件
一般來說,「胖」組件是指具有自定義實例方法的組件。 換句話說,您應該避免將函數附加到 vnode.state
或 this
。 很少有邏輯上適合組件實例方法並且不能被其他組件重用的邏輯。 相對常見的是,不同的組件可能在以後需要該邏輯。
如果將該邏輯放置在資料層中,而不是與組件狀態相關聯,則更容易重構程式碼。
考慮這個胖組件:
// views/Login.js
// 避免
var Login = {
username: '',
password: '',
setUsername: function (value) {
this.username = value;
},
setPassword: function (value) {
this.password = value;
},
canSubmit: function () {
return this.username !== '' && this.password !== '';
},
login: function () {
/*...*/
},
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
this.setUsername(e.target.value);
},
value: this.username,
}),
m('input[type=password]', {
oninput: function (e) {
this.setPassword(e.target.value);
},
value: this.password,
}),
m(
'button',
{ disabled: !this.canSubmit(), onclick: this.login },
'Login'
),
]);
},
};
通常,在較大應用程式的上下文中,像上面這樣的登入組件與用於使用者註冊和密碼恢復的組件一起存在。 想像一下,我們希望能夠在從登入螢幕導航到註冊或密碼恢復螢幕時(或反之亦然)預先填寫電子郵件欄位,以便使用者無需重新輸入他們的電子郵件,如果他們碰巧填寫了錯誤的頁面(或者如果您想在找不到使用者名稱時將使用者轉到註冊表單)。
顯而易見地,將此組件中的 username
和 password
欄位共享到另一個組件很困難。 這是因為胖組件封裝了其狀態,根據定義,這使得從外部難以存取此狀態。
將此組件重構並將狀態程式碼從組件中拉出並放入應用程式的資料層中更有意義。 這可以像建立一個新模組一樣簡單:
// models/Auth.js
// 偏好
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
canSubmit: function () {
return Auth.username !== '' && Auth.password !== '';
},
login: function () {
/*...*/
},
};
module.exports = Auth;
然後,我們可以清理組件:
// views/Login.js
// 偏好
var Auth = require('../models/Auth');
var Login = {
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
Auth.setUsername(e.target.value);
},
value: Auth.username,
}),
m('input[type=password]', {
oninput: function (e) {
Auth.setPassword(e.target.value);
},
value: Auth.password,
}),
m(
'button',
{
disabled: !Auth.canSubmit(),
onclick: Auth.login,
},
'Login'
),
]);
},
};
這樣,Auth
模組現在是與身份驗證相關的狀態的真實來源,並且 Register
組件可以輕鬆存取此資料,甚至可以在需要時重用 canSubmit
等方法。 此外,如果需要驗證程式碼(例如,對於電子郵件欄位),您只需修改 setEmail
,並且該變更將對修改電子郵件欄位的任何組件執行電子郵件驗證。
作為一個額外的好處,請注意我們不再需要使用 .bind
來保持對組件事件處理程式狀態的參考。
不要將 vnode.attrs
本身轉送到其他 vnode
有時,為了保持介面靈活性並簡化實現,您可能會希望將屬性轉送到特定的子組件或元素,例如 Bootstrap 的模態。 可能很想這樣轉送 vnode 的屬性:
// 避免
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// 在此處轉送 `vnode.attrs` ^
// ...
]);
},
};
如果您像上面這樣做,則在使用它時可能會遇到問題:
var MyModal = {
view: function () {
return m(
Modal,
{
// 這會切換兩次,因此它不會顯示
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
相反,您應該將_單個_屬性轉送到 vnode 中:
// 偏好
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// 在此處轉送 `attrs:` ^
// ...
]);
},
};
// 範例
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// 這會切換一次
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
不要操作 children
如果組件對其應用屬性或子項的方式有自己的看法,則應切換為使用自定義屬性。
通常,需要定義多個子項集,例如,如果組件具有可配置的標題和正文。
避免為此目的解構 children
屬性。
// 避免
var Header = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.children[0]),
m('.tagline', vnode.children[1]),
]);
},
};
m(Header, [m('h1', 'My title'), m('h2', 'Lorem ipsum')]);
// 笨拙的使用案例
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
上面的組件打破了子項將以與接收時相同的連續格式輸出的前提。 如果不閱讀其實現,很難理解該組件。 相反,請使用屬性作為命名參數,並為統一的子項內容保留 children
:
// 偏好
var BetterHeader = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.attrs.title),
m('.tagline', vnode.attrs.tagline),
]);
},
};
m(BetterHeader, {
title: m('h1', 'My title'),
tagline: m('h2', 'Lorem ipsum'),
});
// 更清晰的使用案例
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
靜態定義組件,動態調用它們
避免在視圖中建立組件定義
如果您從 view
方法中建立組件(直接內聯或通過調用執行此操作的函數),則每次重繪都會有不同的組件克隆。 在比較組件 vnode 時,如果新 vnode 引用的組件與舊組件引用的組件不完全相等,則假定這兩個組件是不同的組件,即使它們最終執行等效的程式碼。 這意味著通過工廠模式動態建立的組件將始終從頭開始重新建立。
因此,您應該避免重新建立組件。 相反,請慣用地使用組件。
// 避免
var ComponentFactory = function (greeting) {
// 每次調用都會建立一個新組件
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// 第二次調用會從頭開始重新建立 div,而不是不執行任何操作
m.render(document.body, m(ComponentFactory('hello')));
// 偏好
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// 第二次調用不會修改 DOM
m.render(document.body, m(Component, { greeting: 'hello' }));
避免在視圖之外建立組件實例
相反,由於類似的原因,如果在視圖之外建立組件實例,則未來的重繪將對節點執行相等性檢查並跳過它。 因此,組件實例應始終在視圖中創建。
// 避免
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
var counter = m(Counter);
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), counter];
},
});
在上面的示例中,點擊計數器組件按鈕將增加其狀態計數,但不會觸發其視圖,因為表示該組件的 vnode 共享相同的參考,因此呈現過程不會比較它們。 您應該始終在視圖中調用組件以確保創建新的 vnode。
// 偏好
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), m(Counter)];
},
});