Dojo 部件進階

部件的基本原理

部件是全部 Dojo 應用程序的基本構建要素。部件是主要的封裝單元,它能表示從用戶界面的單個元素,到更高級別的容器元素(如 Form 表單、段落、頁面甚至是完整的應用程序)等全部內容。html

前言: 下降複雜度

單個部件一般表示應用程序中的單個職責。細微的職責天然會轉化爲單獨的部件,而複雜的職責就須要拆分爲幾個相互依賴的部分。而後,每部分就能夠實現爲一個部件,其中一個或多個父容器部件會協調全部拆開部件的交互。在這種層級結構中,能夠看出根部件在總體上實現了更大的責任,但實際上它是經過組合不少簡單的部件實現的。node

對一個完整的應用程序的來說,它的全部需求集就是一個單一的、複雜的責任。使用 Dojo 實現這個完整的需求集,會產生具備層級結構的部件,一般從根節點的「Application」部件開始,而後根據每層功能分支出層層部件,最終到達表示 HTML 頁面中單個元素的葉節點。react

簡單的好處

讓部件儘量簡單的緣由有:對單個部件而言,下降複雜度意味着更大的職責隔離(縮小範圍);更容易作全面測試;減小出錯的機會;更有針對性的修復錯誤;以及更普遍的組件複用潛力。git

從整個應用程序的層面看,簡單的部件使得咱們更容易理解每一個組件,以及它們是如何組合在一塊兒的。github

這些好處會簡化平常維護,並最終下降了構建和運行應用程序的總開銷。web

基本的部件結構

部件的核心只是一個渲染函數,該函數返回虛擬 DOM 節點,正是經過虛擬 DOM 節點描述部件在網頁中的結構。可是,應用程序一般須要處理更多邏輯,不只僅是簡單的羅列 HTML 元素,所以有意義的部件一般不只僅由簡單的渲染函數組成。typescript

部件一般位於它們各自的、單獨命名的 TypeScript 模塊中,且每一個模塊默認導出定義的部件。json

表示部件最簡單的方法是基於普通函數,從渲染函數的工廠定義開始。Dojo 的 @dojo/framework/core/vdom 模塊中提供了一個 create() 函數,容許做者定義他們本身的部件渲染函數工廠。可優先使用命名的渲染函數,由於這樣有助於調試;但並不是必須如此;部件也可使用一個被導出的變量標識,該變量保存了部件的工廠定義。數組

對於更喜歡使用類的結構而不是函數的應用程序,Dojo 也提供了基於類的部件。此部件繼承 @dojo/framework/core/WidgetBase 模塊中提供的 WidgetBase,並必需要實現一個 render() 方法。緩存

如下示例展現了一個 Dojo 應用程序的部件,雖然沒有實際用途,但功能完整:

src/widgets/MyWidget.ts

基於函數的 Dojo 部件:

import { create } from '@dojo/framework/core/vdom';

const factory = create();

export default factory(function MyWidget() {
    return [];
});

基於類的 Dojo 部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';

export default class MyWidget extends WidgetBase {
    protected render() {
        return [];
    }
}

由於此部件的渲染函數返回的是空數組,因此在應用程序的輸出中沒有任何內容。部件一般返回一到多個虛擬 DOM 節點,以便在應用程序的 HTML 輸出中包含有意義的結構。

將虛擬 DOM 節點轉換爲網頁中的輸出是由 Dojo 的渲染系統處理的。

部件樣式

部件的 DOM 輸出的樣式是由 CSS 處理的,相關的樣式類存在 CSS 模塊文件中,它與部件的 TypeScript 模塊是對應的。基於函數的部件和基於類的部件使用相同的樣式。該主題會在樣式和主題參考指南中詳細介紹。

渲染部件

Dojo 是一個響應式框架,負責處理數據變動的傳播和相關的後臺更新渲染。Dojo 採用虛擬 DOM(VDOM) 的概念來描述輸出的元素,VDOM 中的節點是簡單的 JavaScript 對象,旨在提升開發人員效率,而不用與實際的 DOM 元素交互。

應用程序只須要關心,將它們的指望的輸出結構聲明爲有層級的虛擬 DOM 節點便可,一般是做爲部件的渲染函數的返回值來完成的。而後,框架的 Renderer 組件會將指望的輸出同步爲 DOM 中的具體元素。也能夠經過給虛擬 DOM 節點傳入屬性,從而配置部件和元素,以及爲部件和元素提供狀態。

Dojo 支持樹的部分子節點渲染,這意味着當狀態發生變化時,框架可以定位到受變化影響的 VDOM 節點的對應子集。而後,只更新 DOM 樹中受影響的子樹,從而響應變化、提升渲染性能並改善用戶的交互體驗。

注意: 部件渲染函數中返回的虛擬節點,是惟一影響應用程序渲染的因素。嘗試使用任何其餘實踐, 在 Dojo 應用程序開發中是被視爲反模式的,應當避免。

支持 TSX

Dojo 支持使用 jsx 語法擴展,在 TypeScript 中被稱爲 tsx。此語法能更方便的描述 VDOM 的輸出,而且更接近於構建的應用程序中的 HTML。

容許使用 TSX 的應用程序

能夠經過 dojo create app --tsx CLI 命令 輕鬆搭建出容許使用 TSX 的項目。

對於不是經過這種方式搭建的 Dojo 項目,能夠經過在項目的 TypeScript 配置中添加如下內容來啓用 TSX:

./tsconfig.json
{
    "compilerOptions": {
        "jsx": "react",
        "jsxFactory": "tsx"
    },
    "include": ["./src/**/*.ts", "./src/**/*.tsx", "./tests/**/*.ts", "./tests/**/*.tsx"]
}

TSX 部件示例

具備 .tsx 文件擴展名的部件,要在渲染函數中輸出 TSX,只須要導入 @dojo/framework/core/vdom 模塊中的 tsx 函數:

src/widgets/MyTsxWidget.tsx

基於函數的部件:

import { create, tsx } from '@dojo/framework/core/vdom';

const factory = create();

export default factory(function MyTsxWidget() {
    return <div>Hello from a TSX widget!</div>;
});

基於類的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';

export default class MyTsxWidget extends WidgetBase {
    protected render() {
        return <div>Hello from a TSX widget!</div>;
    }
}

若部件須要返回多個頂級 TSX 節點,則能夠將它們包裹在 <virtual> 容器元素中。這比返回節點數組更清晰明瞭,由於這樣支持更天然的自動格式化 TSX 代碼塊。以下:

src/widgets/MyTsxWidget.tsx

基於函數的部件:

import { create, tsx } from '@dojo/framework/core/vdom';

const factory = create();

export default factory(function MyTsxWidget() {
    return (
        <virtual>
            <div>First top-level widget element</div>
            <div>Second top-level widget element</div>
        </virtual>
    );
});

使用 VDOM

VDOM 節點類型

Dojo 會在 VDOM 中識別出兩類節點:

  • VNode,或稱爲 _Virtual Nodes_,是具體 DOM 元素的虛擬表示,做爲全部 Dojo 應用程序最底層的渲染輸出。
  • WNode,或稱爲 _Widget Nodes_,將 Dojo 部件關聯到 VDOM 的層級結構上。

Dojo 的虛擬節點中,VNodeWNode 均可看做 DNode 的子類型,但應用程序一般不處理抽象層面的 DNode。推薦使用 TSX 語法,由於它能以統一的語法渲染兩類虛擬節點。

實例化 VDOM 節點

若是不想使用 TSX,在部件中能夠導入 @dojo/framework/core/vdom 模塊中的 v()w() 函數。它們分別建立 VNodeWNode,並可做爲部件渲染函數返回值的一部分。它們的簽名,抽象地說,以下:

  • v(tagName | VNode, properties?, children?):
  • w(Widget | constructor, properties, children?)
參數 可選 描述
`tagName VNode` 一般,會以字符串的形式傳入 tagName,該字符串對應 VNode 將要渲染的相應 DOM 元素的標籤名。若是傳入的是 VNode,新建立的 VNode 將是原始 VNode 的副本。若是傳入了 properties 參數,則會合並 properties 中重複的屬性,並應用到副本 VNode 中。若是傳入了 children 參數,將在新的副本中徹底覆蓋原始 VNode 中的全部子節點。
`Widget constructor` 一般,會傳入 Widget,它將導入部件看成泛型類型引用。還能夠傳入幾種類型的 constructor,它容許 Dojo 以各類不一樣的方式實例化部件。它們支持延遲加載等高級功能。
properties v: 是, w: 否 用於配置新建立的 VDOM 節點的屬性集。它們還容許框架檢測節點是否已更新,從而從新渲染。
children 一組節點,會渲染爲新建立節點的子節點。若是須要,還可使用字符串字面值表示任何文本節點。部件一般會封裝本身的子節點,所以此參數更可能會與 v() 一塊兒使用,而不是 w()

虛擬節點示例

如下示例部件包含一個更有表明性的渲染函數,它返回一個 VNode。它指望的結構描述爲,一個簡單的 div DOM 元素下包含一個文本節點:

src/widgets/MyWidget.ts

基於函數的部件:

import { create, v } from '@dojo/framework/core/vdom';

const factory = create();

export default factory(function MyWidget() {
    return v('div', ['Hello, Dojo!']);
});

基於類的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v } from '@dojo/framework/core/vdom';

export default class MyWidget extends WidgetBase {
    protected render() {
        return v('div', ['Hello, Dojo!']);
    }
}

組合部件的示例

相似地,也可使用 w() 方法組合部件,還能夠混合使用兩種類型的節點來輸出多個節點,以造成更復雜的層級結構:

src/widgets/MyComposingWidget.ts

基於函數的部件:

import { create, v, w } from '@dojo/framework/core/vdom';

const factory = create();

import MyWidget from './MyWidget';

export default factory(function MyComposingWidget() {
    return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
});

基於類的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v, w } from '@dojo/framework/core/vdom';

import MyWidget from './MyWidget';

export default class MyComposingWidget extends WidgetBase {
    protected render() {
        return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
    }
}

渲染到 DOM 中

Dojo 爲應用程序提供了一個渲染工廠函數 renderer()@dojo/framework/core/vdom 模塊默認導出該函數。提供的工廠函數定義了應用程序的根節點,會在此處插入 VDOM 結構的輸出結果。

應用程序一般在主入口點 (main.tsx/main.ts) 調用 renderer() 函數,而後將返回的 Renderer 對象掛載到應用程序的 HTML 頁面中指定的 DOM 元素上。若是掛載應用程序時沒有指定元素,則默認掛載到 document.body 下。

例如:

src/main.tsx
import renderer, { tsx } from '@dojo/framework/core/vdom';

import MyComposingWidget from './widgets/MyComposingWidget';

const r = renderer(() => <MyComposingWidget />);
r.mount();

MountOptions 屬性

Renderer.mount() 方法接收一個可選參數 MountOptions,該參數用於配置如何執行掛載操做。

屬性 類型 可選 描述
sync boolean 默認爲: false。 若是爲 true,則渲染生命週期中相關的回調(特別是 afterdeferred 渲染回調函數)是同步運行的。 若是爲 false,則在 window.requestAnimationFrame() 下一次重繪以前,回調函數被安排爲異步運行。在極少數狀況下,當特定節點須要存在於 DOM 中時,同步運行渲染回調函數可能頗有用,但對於大多數應用程序,不建議使用此模式。
domNode HTMLElement 指定 DOM 元素,VDOM 的渲染結果會插入到該 DOM 節點中。若是沒有指定,則默認爲 document.body
registry Registry 一個可選的 Registry 實例,可在掛載的 VDOM 間使用。

例如,將一個 Dojo 應用程序掛載到一個指定的 DOM 元素,而不是 document.body 下:

src/index.html
<!DOCTYPE html>
<html lang="en-us">
    <body>
        <div>This div is outside the mounted Dojo application.</div>
        <div id="my-dojo-app">This div contains the mounted Dojo application.</div>
    </body>
</html>
src/main.tsx
import renderer, { tsx } from '@dojo/framework/core/vdom';

import MyComposingWidget from './widgets/MyComposingWidget';

const dojoAppRootElement = document.getElementById('my-dojo-app') || undefined;
const r = renderer(() => <MyComposingWidget />);
r.mount({ domNode: dojoAppRootElement });

向 VDOM 中加入外部的 DOM 節點

Dojo 能夠包裝外部的 DOM 元素,有效地將它們引入到應用程序的 VDOM 中,用做渲染輸出的一部分。這是經過 @dojo/framework/core/vdom 模塊中的 dom() 工具方法完成的。它的工做原理與 v() 相似,但它的主參數使用的是現有的 DOM 節點而不是元素標記字符串。在返回 VNode 時,它會引用傳遞給它的 DOM 節點,而不是使用 v() 新建立的元素。

一旦 dom() 返回的 VNode 添加到應用程序的 VDOM 中,Dojo 應用程序就實際得到了被包裝 DOM 節點的全部權。請注意,此過程僅適用於 Dojo 應用程序的外部節點,如掛載應用程序元素的兄弟節點,或與主網頁的 DOM 斷開鏈接的新建立的節點。若是包裝的節點是掛載了應用程序的元素的祖先或子孫節點,將無效。

dom() API

  • dom({ node, attrs = {}, props = {}, on = {}, diffType = 'none', onAttach })
參數 可選 描述
node 添加到 Dojo VDOM 中的外部 DOM 節點
attrs 應用到外部 DOM 節點上的 HTML 屬性(attributes)
props 附加到 DOM 節點上的屬性(properties)
on 應用到外部 DOM 節點上的事件集合
diffType 默認爲: none更改檢測策略,肯定 Dojo 應用程序是否須要更新外部的 DOM 節點
onAttach 一個可選的回調函數,在節點追加到 DOM 後執行

檢測外部 DOM 節點的變化

經過 dom() 添加的外部節點是從常規的虛擬 DOM 節點中移除的,由於它們可能會在 Dojo 應用程序以外被處理。這意味着 Dojo 不能主要使用 VNode 的屬性設置元素的狀態,而是必須依賴 DOM 節點自己的 JavaScript 屬性(properties)和 HTML 屬性(attributes)。

dom() 接收 diffType 屬性,容許用戶爲包裝的節點指定屬性變動檢測策略。一個指定的策略,會指明如何使用包裝的節點,以幫助 Dojo 來肯定 JavaScript 屬性和 HTML 屬性是否已變化,而後將變化應用到包裝的 DOM 節點上。默認的策略是 none,意味着 Dojo 只需在每一個渲染週期將包裝好的 DOM 元素添加到應用程序輸出中。

注意: 全部的策略都使用前一次 VNode 中的事件,以確保它們會被正確的刪除並應用到每一個渲染中。

可用的 dom() 變化檢測策略:

diffType 描述
none 此模式會爲包裝的 VNode 的前一次 attributesproperties 傳入空對象,意味着在每一個渲染週期,都會將傳給 dom()propsattrs 從新應用於包裝的節點。
dom 此模式基於 DOM 節點中的 attributesproperties 與傳入 dom()propsattrs 進行比較計算,肯定是否存在差別,而後應用這些差別。
vdom 此模式與前一次的 VNODE 作比較,這其實是 Dojo 默認的 VDOM 差別對比策略。在變動檢測和更新渲染時會忽略直接對包裝的節點所作的任何修改。

經過屬性配置部件

傳遞給 VDOM 中節點的屬性(properties)概念是 Dojo 的核心支柱。節點屬性充當在應用程序中傳播狀態的主要管道,可將其從父部件傳給子部件,也能夠經過事件處理器逐層回傳。它們也能夠做爲使用者與部件交互的重要 API,爲父部件傳入屬性來配置其 DOM 結構(返回 VNode),也能夠傳給其管理的子部件(返回 WNode)。

VNode 接收 VNodeProperties 類型的屬性,WNode 最低接收 WidgetProperties。部件的做者一般會定義本身的屬性接口,而後須要調用者傳入該接口。

VDOM 節點的 key

Widgetproperties 很是簡單,只包含一個可選屬性 key,該屬性也存在於 VNodeProperties 中。

當部件開始輸出的多個元素,處在 VDOM 的同一個層級,而且類型相同,就必須指定 key。例如,一個列表部件管理了多個列表項,就須要爲列表中的每一項指定一個 key

當從新渲染 VDOM 中受影響部分時,Dojo 使用虛擬節點的 key 來惟一標識特定實例。若是沒有使用 key 在 VDOM 中區分開同一層級中的相同類型的多個節點,則 Dojo 就沒法準確地肯定哪些子節點受到了失效更改(invalidating change)的影響。

注意: 虛擬節點的 key 應在屢次渲染函數的調用中保持一致。在每一次的渲染調用中,爲相同的輸出節點生成不一樣的 key, 在 Dojo 應用程序開發中被認爲是反模式的,應當避免。

配置 VNode

VNodeProperties 包含不少字段,是與 DOM 中的元素交互的重要 API。其中不少屬性鏡像了 HTMLElement 中的可用屬性,包括指定各類 oneventname 的事件處理器。

應用程序的這些屬性是單向的,由於 Dojo 將給定的屬性集應用到具體的 DOM 元素上,但不會將相應的 DOM 屬性後續的任何更改同步到 VNodeProperties。任何此類更改都應該經過事件處理器回傳給 Dojo 應用程序。當調用事件處理程序時,應用程序能夠處理事件所需的任何狀態更改,在輸出 VDOM 結構進行渲染時,更新對應的 VNodeProperties 視圖,而後 Dojo 的 Renderer 會同步全部相關的 DOM 更新

修改屬性和差別檢測

Dojo 使用虛擬節點的屬性來肯定給定節點是否已更新,從而是否須要從新渲染。具體來講,它使用差別檢測策略來比較前一次和當前渲染幀的屬性集。若是在節點接收的最新屬性集中檢測到差別,則該節點將失效,並在下一個繪製週期中從新渲染。

注意: 屬性更改檢測是由框架內部管理的,依賴於在部件的渲染函數中聲明的 VDOM 輸出結構。試圖保留屬性的引用,並在正常的部件渲染週期以外對其進行修改, 在 Dojo 應用程序開發中被視爲反模式的,應當避免。

支持交互

事件監聽器

在實例化節點時,爲虛擬節點指定事件監聽器的方法與指定任何其餘屬性的方法相同。當輸出 VNode 時,VNodeProperties 上事件監聽器的名字會鏡像到 HTMLElement 的等價事件上。雖然自定義部件的做者能夠根據本身的選擇命名事件,但一般也遵循相似的 onEventName 的命名約定。

函數屬性(如事件處理程序)會自動綁定到實例化此虛擬節點的部件的 this 上下文。可是,若是將已綁定的函數傳給屬性值,將不會重複綁定給 this

處理 focus

輸出 VNode 時,部件可使用 VNodePropertiesfocus 屬性來控制生成的 DOM 元素在渲染時是否獲取焦點。這是一個特殊屬性,它可接收一個 boolean 類型的對象或者是返回一個 boolean 類型的函數。

當直接傳入 true 時,只有上一次的值不是 true 時,元素纔會獲取焦點(相似於常規屬性變動檢測)。而傳入函數時,只要函數返回 true,元素就會獲取焦點,而無論上一次返回值。

例如:

根據元素的順序,下面的 「firstFocus」 輸入框只會在初始化渲染時獲取焦點,而 「subsequentFocus」 輸入框在每次渲染時都會獲取焦點,由於 focus 屬性的值是函數。

src/widgets/FocusExample.tsx

基於函數的部件:

import { create, tsx, invalidator } from '@dojo/framework/core/vdom';

const factory = create({ invalidator });

export default factory(function FocusExample({ middleware: { invalidator } }) {
    return (
        <div>
            <input key="subsequentFocus" type="text" focus={() => true} />
            <input key="firstFocus" type="text" focus={true} />
            <button onclick={() => invalidator()}>Re-render</button>
        </div>
    );
});

基於類的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';

export default class FocusExample extends WidgetBase {
    protected render() {
        return (
            <div>
                <input key="subsequentFocus" type="text" focus={() => true} />
                <input key="firstFocus" type="text" focus={true} />
                <button onclick={() => this.invalidate()}>Re-render</button>
            </div>
        );
    }
}

委託 focus

基於函數的部件可以使用 focus 中間件爲其子部件設置焦點,或者接受來自父部件的焦點。基於類的部件可以使用 FocusMixin(來自 @dojo/framework/core/mixins/Focus)以相同的方式委託 focus。

FocusMixin 會給部件的類中添加一個 this.shouldFocus() 方法,而基於函數的部件使用 focus.shouldFocus() 中間件方法實現相同的目的。此方法會檢查部件是否處於執行了獲取焦點的狀態(譯註:即調用了 this.focus()),而且僅對單個調用返回 true,直到再次調用部件的 this.focus() 方法(基於函數的部件使用等價的 focus.focus())。

FocusMixin 或者 focus 中間件也會爲部件的 API 添加一個 focus 函數屬性。框架使用此屬性的布爾結果來肯定渲染時,部件(或其一個子部件)是否應得到焦點。一般,部件經過其 focus 屬性將 shouldFocus 方法傳遞給特定的子部件或輸出的節點上,從而容許父部件將焦點委託給其子部件。

基於函數的部件的示例,請參閱 Dojo 中間件參考指南中的 focus 中間件委派示例

下面基於類的部件示例,顯示了在部件層次結構內和輸出的 VNode 之間委託和控制焦點:

src/widgets/FocusableWidget.tsx
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
import Focus from '@dojo/framework/core/mixins/Focus';

interface FocusInputChildProperties {
    onFocus: () => void;
}

class FocusInputChild extends Focus(WidgetBase)<FocusInputChildProperties> {
    protected render() {
        /*
            The child widget's `this.shouldFocus()` method is assigned directly to the
            input node's `focus` property, allowing focus to be delegated from a higher
            level containing parent widget.

            The input's `onfocus()` event handler is also assigned to a method passed
            in from a parent widget, allowing user-driven focus changes to propagate back
            into the application.
        */
        return <input onfocus={this.properties.onFocus} focus={this.shouldFocus} />;
    }
}

export default class FocusableWidget extends Focus(WidgetBase) {
    private currentlyFocusedKey = 0;
    private childCount = 5;

    private onFocus(key: number) {
        this.currentlyFocusedKey = key;
        this.invalidate();
    }

    /*
        Calling `this.focus()` resets the widget so that `this.shouldFocus()` will return true when it is next invoked.
    */
    private focusPreviousChild() {
        --this.currentlyFocusedKey;
        if (this.currentlyFocusedKey < 0) {
            this.currentlyFocusedKey = this.childCount - 1;
        }
        this.focus();
    }

    private focusNextChild() {
        ++this.currentlyFocusedKey;
        if (this.currentlyFocusedKey === this.childCount) {
            this.currentlyFocusedKey = 0;
        }
        this.focus();
    }

    protected render() {
        /*
            The parent widget's `this.shouldFocus()` method is passed to the relevant child element
            that requires focus, based on the simple previous/next widget selection logic.

            This allows focus to be delegated to a specific child node based on higher-level logic in
            a container/parent widget.
        */
        return (
            <div>
                <button onclick={this.focusPreviousChild}>Previous</button>
                <button onclick={this.focusNextChild}>Next</button>
                <FocusInputChild
                    key={0}
                    focus={this.currentlyFocusedKey === 0 ? this.shouldFocus : undefined}
                    onFocus={() => this.onFocus(0)}
                />
                <FocusInputChild
                    key={1}
                    focus={this.currentlyFocusedKey === 1 ? this.shouldFocus : undefined}
                    onFocus={() => this.onFocus(1)}
                />
                <FocusInputChild
                    key={2}
                    focus={this.currentlyFocusedKey === 2 ? this.shouldFocus : undefined}
                    onFocus={() => this.onFocus(2)}
                />
                <FocusInputChild
                    key={3}
                    focus={this.currentlyFocusedKey === 3 ? this.shouldFocus : undefined}
                    onFocus={() => this.onFocus(3)}
                />
                <FocusInputChild
                    key={4}
                    focus={this.currentlyFocusedKey === 4 ? this.shouldFocus : undefined}
                    onFocus={() => this.onFocus(4)}
                />
            </div>
        );
    }
}

狀態管理

在數據不須要在多個組件之間流動的簡單應用程序中,狀態管理是很是簡單的。可將部件須要的數據封裝在部件內,這是 Dojo 應用程序中狀態管理的最基本形式

隨着應用程序變得愈來愈複雜,而且開始要求在多個部件之間共享和傳輸數據,就須要一種更健壯的狀態管理形式。在這裏,Dojo 開始展示出其響應式框架的價值,容許應用程序定義數據如何在組件之間流動,而後由框架管理變動檢測和從新渲染。這是經過在部件的渲染函數中聲明 VDOM 輸出時將部件和屬性鏈接在一塊兒而作到的。

對於大型應用程序,狀態管理多是最具挑戰性的工做之一,須要開發人員在數據一致性、可用性和容錯性之間進行平衡。雖然這種複雜性大多超出了 web 應用程序層的範圍,但 Dojo 提供了更進一步的解決方案,以確保數據的一致性。Dojo Store 組件提供了一個集中式的狀態存儲,它提供一致的 API,用於訪問和管理應用程序中多個位置的數據。

基礎:自封裝的部件狀態

部件能夠經過多種方式維護其內部狀態。基於函數的部件可使用 cacheicache 中間件來存儲部件的本地狀態,而基於類的部件可使用內部的類字段。

內部狀態數據可能直接影響部件的渲染輸出,也可能做爲屬性傳遞給子部件,而它們繼而又直接影響了子部件的渲染輸出。部件還可能容許更改其內部狀態,例如響應用戶交互事件。

如下示例解釋了這些模式:

src/widgets/MyEncapsulatedStateWidget.tsx

基於函數的部件:

import { create, tsx } from '@dojo/framework/core/vdom';
import cache from '@dojo/framework/core/middleware/cache';

const factory = create({ cache });

export default factory(function MyEncapsulatedStateWidget({ middleware: { cache } }) {
    return (
        <div>
            Current widget state: {cache.get<string>('myState') || 'Hello from a stateful widget!'}
            <br />
            <button
                onclick={() => {
                    let counter = cache.get<number>('counter') || 0;
                    let myState = 'State change iteration #' + ++counter;
                    cache.set('myState', myState);
                    cache.set('counter', counter);
                }}
            >
                Change State
            </button>
        </div>
    );
});

基於類的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';

export default class MyEncapsulatedStateWidget extends WidgetBase {
    private myState = 'Hello from a stateful widget!';
    private counter = 0;

    protected render() {
        return (
            <div>
                Current widget state: {this.myState}
                <br />
                <button
                    onclick={() => {
                        this.myState = 'State change iteration #' + ++this.counter;
                    }}
                >
                    Change State
                </button>
            </div>
        );
    }
}

注意,這個示例是不完整的,在正在運行的應用程序中,單擊「Change State」按鈕不會對部件的渲染輸出產生任何影響。這是由於狀態徹底封裝在 MyEncapsulatedStateWidget 部件中,而 Dojo 無從得知對部件的任何更改。框架只處理了部件的初始渲染。

要通知 Dojo 從新渲染,則須要封裝渲染狀態的部件自行失效。

讓部件失效

基於函數的部件可使用 icache 中間件處理本地的狀態管理,當狀態更新時會自動失效部件。icache 組合了 cacheinvalidator 中間件,擁有 cache 的處理部件狀態管理的功能,和 invalidator 的當狀態變化時讓部件失效的功能。若是須要,基於函數的部件也能夠直接使用 invalidator

基於類的部件,則有兩種失效的方法:

  1. 在狀態被更改後的適當位置顯式調用 this.invalidate()

    • MyEncapsulatedStateWidget 示例中,可在「Change State」按鈕的 onclick 處理函數中完成。
  2. 使用 @watch() 裝飾器(來自 @dojo/framework/core/vdomercorators/watch 模塊)註釋任何相關字段。當修改了 @watch 註釋的字段後,將隱式調用 this.invalidate(),這對於狀態字段頗有用,這些字段在更新時老是須要從新渲染。

注意: 將一個部件標記爲無效,並不會馬上從新渲染該部件,而是通知 Dojo,部件已處於 dirty 狀態,應在下一個渲染週期中進行更新和從新渲染。這意味着在同一個渲染幀內屢次失效同一個部件並不會對應用程序的性能產生負面影響,但應避免過多重複的失效以確保最佳性能。

如下是修改過的 MyEncapsulatedStateWidget 示例,當狀態變化時會正確地更新輸出。

基於函數的部件:

import { create, tsx } from '@dojo/framework/core/vdom';
import icache from '@dojo/framework/core/middleware/icache';

const factory = create({ icache });

export default factory(function MyEncapsulatedStateWidget({ middleware: { icache } }) {
    return (
        <div>
            Current widget state: {icache.getOrSet<string>('myState', 'Hello from a stateful widget!')}
            <br />
            <button
                onclick={() => {
                    let counter = icache.get<number>('counter') || 0;
                    let myState = 'State change iteration #' + ++counter;
                    icache.set('myState', myState);
                    icache.set('counter', counter);
                }}
            >
                Change State
            </button>
        </div>
    );
});

基於類的部件:

此處,myStatecounter 都在應用程序邏輯操做的同一個地方進行了更新,所以可將 @watch() 添加到任一字段上或者同時添加到兩個字段上,這些配置的實際結果和性能情況徹底相同:

src/widgets/MyEncapsulatedStateWidget.tsx
import WidgetBase from '@dojo/framework/core/WidgetBase';
import watch from '@dojo/framework/core/decorators/watch';
import { tsx } from '@dojo/framework/core/vdom';

export default class MyEncapsulatedStateWidget extends WidgetBase {
    private myState: string = 'Hello from a stateful widget!';

    @watch() private counter: number = 0;

    protected render() {
        return (
            <div>
                Current widget state: {this.myState}
                <br />
                <button
                    onclick={() => {
                        this.myState = 'State change iteration #' + ++this.counter;
                    }}
                >
                    Change State
                </button>
            </div>
        );
    }
}

中級:傳入部件屬性

經過虛擬節點的 properties 將狀態傳入部件是 Dojo 應用程序中鏈接響應式數據流最有效的方法。

部件指定本身的屬性接口,該接口包含部件但願向使用者公開的任何字段,包括配置選項、表示注入狀態的字段以及任何事件處理函數。

基於函數的部件是將其屬性接口以泛型參數的形式傳給 create().properties<MyPropertiesInterface>() 的。而後,本調用鏈返回的工廠函數經過渲染函數定義中的 properties 函數參數,讓屬性值可用。

基於類的部件可將其屬性接口定義爲類定義中 WidgetBase 的泛型參數,而後經過 this.properties 對象訪問其屬性。

例如,一個支持狀態和事件處理器屬性的部件:

src/widgets/MyWidget.tsx

基於函數的部件:

import { create, tsx } from '@dojo/framework/core/vdom';
import icache from '@dojo/framework/core/middleware/icache';

const factory = create().properties<{
    name: string;
    onNameChange?(newName: string): void;
}>();

export default factory(function MyWidget({ middleware: { icache }, properties }) {
    const { name, onNameChange } = properties();
    let newName = icache.get<string>('new-name') || '';
    return (
        <div>
            <span>Hello, {name}! Not you? Set your name:</span>
            <input
                type="text"
                value={newName}
                oninput={(e: Event) => {
                    icache.set('new-name', (e.target as HTMLInputElement).value);
                }}
            />
            <button
                onclick={() => {
                    icache.set('new-name', undefined);
                    onNameChange && onNameChange(newName);
                }}
            >
                Set new name
            </button>
        </div>
    );
});

基於類的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';

export interface MyWidgetProperties {
    name: string;
    onNameChange?(newName: string): void;
}

export default class MyWidget extends WidgetBase<MyWidgetProperties> {
    private newName = '';
    protected render() {
        const { name, onNameChange } = this.properties;
        return (
            <div>
                <span>Hello, {name}! Not you? Set your name:</span>
                <input
                    type="text"
                    value={this.newName}
                    oninput={(e: Event) => {
                        this.newName = (e.target as HTMLInputElement).value;
                        this.invalidate();
                    }}
                />
                <button
                    onclick={() => {
                        this.newName = '';
                        onNameChange && onNameChange(newName);
                    }}
                >
                    Set new name
                </button>
            </div>
        );
    }
}

此示例部件的使用者能夠經過傳入適當的屬性與之交互:

src/widgets/NameHandler.tsx

基於函數的部件:

import { create, tsx } from '@dojo/framework/core/vdom';
import icache from '@dojo/framework/core/middleware/icache';

import MyWidget from './MyWidget';

const factory = create({ icache });

export default factory(function NameHandler({ middleware: { icache } }) {
    let currentName = icache.get<string>('current-name') || 'Alice';
    return (
        <MyWidget
            name={currentName}
            onNameChange={(newName) => {
                icache.set('current-name', newName);
            }}
        />
    );
});

基於類的部件:

import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
import watch from '@dojo/framework/core/decorators/watch';
import MyWidget from './MyWidget';

export default class NameHandler extends WidgetBase {
    @watch() private currentName: string = 'Alice';

    protected render() {
        return (
            <MyWidget
                name={this.currentName}
                onNameChange={(newName) => {
                    this.currentName = newName;
                }}
            />
        );
    }
}

高級:提取和注入狀態

實現複雜功能時,在部件內遵循狀態封裝模式可能會致使組件膨脹、難以管理。在大型應用程序中也可能出現另外一個問題,數百個部件跨數十個層級組合在一塊兒。一般是葉部件使用狀態數據,並非 VDOM 層次結構中的中間容器。讓數據狀態穿透這樣一個層次結構複雜的部件須要增長脆弱、沒必要要的代碼。

Dojo 提供的 Store 組件 解決了這些問題,它將狀態管理提取到專用上下文中,而後將應用程序中的相關狀態注入到特定的部件中。

最佳開發實踐

使用 Dojo 部件時,應謹記一些重要原則,以免在應用程序代碼中引入反模式。試圖以不受支持的方式使用框架可能會致使意外的行爲,並在應用程序中引入難以發現的錯誤。

部件屬性

  • 部件應只能讀取傳入其中的屬性(properties)。

    • 若是修改了傳入部件中的屬性值,則不能回傳給框架,以免致使部件和框架之間出現差別。
  • Widgets should avoid deriving further render state from their properties, and instead rely on their complete render state being provided to them.

    • Deriving render state can cause similar divergences between the widget and the framework as modifying received properties; the framework is not aware of the derived state, so cannot properly determine when a widget has been updated and requires invalidation and re-rendering.
  • 若是須要,內部或私有狀態能夠徹底封裝在部件內。

    • 實現「純」部件是一個有效且一般是可取的模式,它不會產生反作用,並用屬性接收它們的全部狀態,但這不是開發 Dojo 部件的惟一模式。

使用基於類的部件

  • __render___, __setProperties__, and __setChildren___ 函數屬於框架內部實現細節,毫不容許在應用程序中調用或覆寫。
  • 應用程序不該直接實例化部件——Dojo 徹底接管部件實例的生命週期,包括實例化、緩存和銷燬。

虛擬 DOM

  • 虛擬節點的 key 應在屢次渲染調用中保持一致。

    • 若是在每次渲染調用中都指定一個不一樣的 key,則 Dojo 沒法有效地將前一次渲染和本次渲染中的相同節點關聯上。Dojo 會將上一次渲染中沒有看到的新 key 看成新元素,這會致使從 DOM 中刪除以前的節點並從新添加一套,即便屬性沒有發生變化,不須要從新更新 DOM。
    • 一個常見的反模式是在部件的渲染函數中爲節點的 key 分配一個隨機生成的 ID(如 GUID 或 UUID)。除非生成策略是等冪的,不然不該在渲染函數中生成節點的 key 值。
  • 應用程序不該存儲虛擬節點的引用,以便從部件的渲染函數返回它們後,進行後續操做;也不該嘗試經過使用單個實例跨多個渲染調用來優化內存分配。

    • 虛擬節點被設計成輕量級的,而且在每次部件渲染週期內實例化新版本的開銷很是低。
    • 框架依賴於在兩次部件渲染函數調用中有兩個單獨的虛擬節點實例來執行準確的更改檢測。若是未檢測到任何變化,則不會產生進一步的開銷、渲染等。

渲染到 DOM 中

  • 應用程序不該使用命令式的 DOM 操做調用。

    • 框架負責處理全部具體的渲染職責,而且爲部件做者提供了替代機制,以更簡單、類型安全和響應式的方式使用各類 DOM 功能。