中间件基本原理

Dojo 提供了渲染中间件的概念,以帮助衔接响应式、函数部件与底层的命令式 DOM 结构。

如果部件能够访问 DOM 信息,某些 web 应用程序需求就更容易实现。常见的例子有:

  • 响应式 UI 不与特定的设备类型绑定,而是根据可用的页面区域改变元素的大小。
  • 仅当某些元素在用户可视区域可见时,才延迟加载需要的数据——例如无限滚动列表。
  • 引导元素获取焦点,并在用户变更焦点后进行响应。

但是,中间件并非必须与 DOM 绑定;这个概念还适合部件的渲染生命周期等更常用的情况。此类需求的常见示例如下:

  • 如果获取数据的开销大,则在渲染间缓存数据
  • 根据特定条件暂停和恢复部件的渲染;在所需信息不可用时,避免无用的渲染
  • 将函数部件标记为无效,以便 Dojo 可以重新渲染部件

一个中间件组件一般公开的某些功能与部件渲染的 DOM 元素有关;大多是部件的根节点。中间件系统为部件在浏览器中的展示和交互提供了更高级的控制,并且允许部件以一致的方式使用几个新兴的 Web 标准。

如果部件在其底层的 DOM 元素存在之前访问中间件的某些属性,则返回合理的默认值。还有一些中间件可以暂停部件的渲染,直到满足某些条件。使用这些中间件,部件能避免不必要的渲染,直到所需的信息可用为止,然后 Dojo 将在数据可用时获取中间件的正确属性值,自动重新渲染受影响的部件。

创建中间件

中间件是使用 @dojo/framework/core/vdom 中的 create() 工厂方法定义的。这与创建函数部件的过程类似,但是中间件工厂返回的并不是 VDOM 节点,而是允许访问中间件功能集的 API。简单的中间件只需要一个函数调用来实现它们的需求,也可以直接返回一个函数,而不需要将中间件包装在一个对象中。

下面介绍一个中间件组件,它有一个简单的 get()set() API:

src/middleware/myMiddleware.ts

  1. import { create } from '@dojo/framework/core/vdom';
  2. const factory = create();
  3. export const myMiddleware = factory(() => {
  4. return {
  5. get() {},
  6. set() {}
  7. };
  8. });
  9. export default myMiddleware;

使用中间件

中间件主要用在函数部件中,但也可以通过组合形成其他中间件,以实现更复杂的需求。这两种情况下,任何用到的中间件都会作为属性传给 create() 方法,然后通过部件或中间件工厂实现函数中的 middleware 参数使用这些中间件。

例如,在部件中使用上面的 myMiddleware 中间件:

src/widgets/MiddlewareConsumerWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import myMiddleware from '../middleware/myMiddleware';
  3. const render = create({ myMiddleware });
  4. export const MiddlewareConsumerWidget = render(({ middleware: { myMiddleware } }) => {
  5. myMiddleware.set();
  6. return <div>{`Middleware value: ${myMiddleware.get()}`}</div>;
  7. });
  8. export default MiddlewareConsumerWidget;

组合中间件

以下示例演示了用中间件组合出新的中间件,以实现更有用的需求:

  • 在本地缓存中取一个值
  • 如果缓存未命中,则从外部获取值
  • 在等待外部的值返回时,暂停使用该中间件的部件的进一步渲染
  • 一旦外部的值可以通过本地缓存访问,就恢复渲染并让使用的部件失效,以重新渲染这些部件

src/middleware/ValueCachingMiddleware.ts

  1. import { create, defer } from '@dojo/framework/core/vdom';
  2. import icache from '@dojo/framework/core/middleware/icache';
  3. const factory = create({ defer, icache });
  4. export const ValueCachingMiddleware = factory(({ middleware: { defer, icache }}) => {
  5. get(key: string) {
  6. const cachedValue = icache.get(key);
  7. if (cachedValue) {
  8. return cachedValue;
  9. }
  10. // Cache miss: fetch the value somehow through a promise
  11. const promise = fetchExternalValue(value);
  12. // Pause further widget rendering
  13. defer.pause();
  14. promise.then((result) => {
  15. // Cache the value for subsequent renderings
  16. icache.set(key, result);
  17. // Resume widget rendering once the value is available
  18. defer.resume();
  19. });
  20. return null;
  21. }
  22. });
  23. export default ValueCachingMiddleware;

传入中间件属性

由于中间件是通过 create() 工具函数定义的,因此为中间件指定属性接口的方式,与为函数部件指定属性接口的方式相同。主要的区别是中间件属性会被添加到所有消费者部件的属性接口中。这意味着属性值是在实例化部件时设置的,而不是在部件使用中间件时。在整个组合层次结构中,属性被看作是只读的,因此中间件不能修改属性值。

下面是具有属性接口的中间件示例:

src/middleware/middlewareWithProperties.tsx

  1. import { create } from '@dojo/framework/core/vdom';
  2. const factory = create().properties<{ conditional?: boolean }>();
  3. export const middlewareWithProperties = factory(({ properties }) => {
  4. return {
  5. getConditionalState() {
  6. return properties().conditional ? 'Conditional is true' : 'Conditional is false';
  7. }
  8. };
  9. });
  10. export default middlewareWithProperties;

在部件中使用中间件及其属性:

src/widgets/MiddlewarePropertiesWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import middlewareWithProperties from '../middleware/middlewareWithProperties';
  3. const render = create({ middlewareWithProperties });
  4. export const MiddlewarePropertiesWidget = render(({ properties, middleware: { middlewareWithProperties } }) => {
  5. return (
  6. <virtual>
  7. <div>{`Middleware property value: ${properties().conditional}`}</div>
  8. <div>{`Middleware property usage: ${middlewareWithProperties.getConditionalState()}`}</div>
  9. </virtual>
  10. );
  11. });
  12. export default MiddlewarePropertiesWidget;

然后,当创建 MiddlewarePropertiesWidget 实例时,指定中间件的 conditional 属性值,例如:

src/main.tsx

  1. import renderer, { tsx } from '@dojo/framework/core/vdom';
  2. import MiddlewarePropertiesWidget from './widgets/MiddlewarePropertiesWidget';
  3. const r = renderer(() => <MiddlewarePropertiesWidget conditional={true} />);
  4. r.mount();