dojo dragon main logo

中间件基础

Dojo 提供了渲染中间件的概念,以帮助弥合响应式、功能性小部件与其底层命令式 DOM 结构之间的差距。

某些 Web 应用程序需求在小部件能够访问有关 DOM 的信息时才能最好地实现。常见示例包括:

  • 响应式 UI,它不绑定到特定设备类型,而是根据可用页面空间自适应不同元素的大小。
  • 仅在需要时延迟加载数据,例如,当某些元素在用户的视窗中可见时 - 例如无限滚动列表。
  • 引导元素焦点并响应用户焦点变化

但是,中间件不需要绑定到 DOM;该概念也可以用于围绕小部件渲染生命周期的更通用问题。此类需求的常见示例包括:

  • 在渲染之间缓存数据,当数据检索成本很高时。
  • 根据某些条件暂停和恢复小部件渲染;当所需信息不可用时,避免不必要的渲染。
  • 将功能性小部件标记为无效,以便 Dojo 可以重新渲染它。

一个中间件组件通常会公开与小部件渲染的 DOM 元素(通常是小部件的根节点)中的一个或多个相关联的某些功能。中间件系统为小部件提供了对其在浏览器中的表示和交互的更高级控制,并且还允许小部件以一致的方式使用多个新兴 Web 标准。

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

创建中间件

中间件是使用 @dojo/framework/core/vdom 模块中的 create() 工厂方法定义的。此过程类似于创建功能性小部件,但是,中间件工厂不是返回 VDOM 节点,而是返回一个具有适当 API 的对象,该 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();