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 功能。