dojo dragon main logo

中间件基本原理

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

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

const factory = create();

export const myMiddleware = factory(() => {
	return {
		get() {},
		set() {}
	};
});

export default myMiddleware;

使用中间件

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

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

src/widgets/MiddlewareConsumerWidget.tsx

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

const render = create({ myMiddleware });
export const MiddlewareConsumerWidget = render(({ middleware: { myMiddleware } }) => {
	myMiddleware.set();
	return <div>{`Middleware value: ${myMiddleware.get()}`}</div>;
});

export default MiddlewareConsumerWidget;

组合中间件

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

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

src/middleware/ValueCachingMiddleware.ts

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

const factory = create({ defer, icache });

export const ValueCachingMiddleware = factory(({ middleware: { defer, icache }}) => {
	get(key: string) {
		const cachedValue = icache.get(key);
		if (cachedValue) {
			return cachedValue;
		}
		// Cache miss: fetch the value somehow through a promise
		const promise = fetchExternalValue(value);
		// Pause further widget rendering
		defer.pause();
		promise.then((result) => {
			// Cache the value for subsequent renderings
			icache.set(key, result);
			// Resume widget rendering once the value is available
			defer.resume();
		});
		return null;
	}
});

export default ValueCachingMiddleware;

传入中间件属性

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

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

src/middleware/middlewareWithProperties.tsx

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

const factory = create().properties<{ conditional?: boolean }>();

export const middlewareWithProperties = factory(({ properties }) => {
	return {
		getConditionalState() {
			return properties().conditional ? 'Conditional is true' : 'Conditional is false';
		}
	};
});

export default middlewareWithProperties;

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

src/widgets/MiddlewarePropertiesWidget.tsx

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

const render = create({ middlewareWithProperties });
export const MiddlewarePropertiesWidget = render(({ properties, middleware: { middlewareWithProperties } }) => {
	return (
		<virtual>
			<div>{`Middleware property value: ${properties().conditional}`}</div>
			<div>{`Middleware property usage: ${middlewareWithProperties.getConditionalState()}`}</div>
		</virtual>
	);
});

export default MiddlewarePropertiesWidget;

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

src/main.tsx

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

const r = renderer(() => <MiddlewarePropertiesWidget conditional={true} />);
r.mount();