template/docs/advanced/i/plugin-system.md
unanmed aee8627e95 docs(10-33): 高级 I 移入 i/ 并重建总导览与侧边栏
- 九个高级 I 页面 git mv 至 docs/advanced/i/
- docs/advanced/index.md 重建为高级教程总导览(III 暂不提供链接)
- 侧边栏新增总导览入口,I 组九项改指 /advanced/i/
- 修正 I 页与高级 II 入链的跨层相对路径
2026-10-26 20:36:00 +08:00

525 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 插件系统
如果你已经用过中级教程里的[数据插件](../../intermediate/data-plugins.md),你会知道插件能把一组功能打包、挂到 `state` 上,并按固定时机初始化。那一篇停在「会用」;这一篇讲怎么自己写一个插件、让插件彼此协作,以及怎么在插件之外调用它。
插件分数据端与渲染端两套,写法、注册方式与生命周期一一对应:数据端插件负责逻辑与状态,渲染端插件负责画面与交互。渲染端插件可以依赖数据端插件,反过来不行。下面两边并列讲,遇到不同的地方单独说明。
插件是高级 I 的核心:后面的可存档内容、录像一致性、钩子深入,实际都挂在某个插件上。
阅读前需要会基础语法:`class`、`new`、`import`、`async` / `await`。不需要懂引擎内部实现;涉及内部机制的地方,会先说清「你能拿它做什么」。
## 插件解决什么问题
假设你要加一个任务系统:它有自己的状态、自己的管理器,还要在加载完地图后读一遍任务配置。不做成插件的话,这些代码会散在几个地方——对象在 `src/data/initialize.ts` 里 `new`,存档注册也写在那里,加载后的初始化再塞进某个钩子。功能一多,`initialize.ts` 会变成所有人的公共草稿纸。
插件把这一组东西收进一个独立单元:
- 自己的对象在自己的目录里创建;
- 自己的存档内容在自己的 `awake` 里注册;
- 自己的加载后逻辑在自己的 `afterLoad` 里写;
- 启动时只需要在对应端的插件列表里 `new` 一次。
这和[声明式接口](./declarative-vs-imperative.md)是同一个思路:你不去改引擎的初始化流程,只声明「在哪个时机做什么」,时机由引擎统一调用。
## 注册与目录
数据端插件放在 `src/data/plugin/`,渲染端插件放在 `src/client/plugin/`,各自在自己的目录里:
| 位置 | 放什么 |
| ----------------------------- | ---------- |
| `src/data/plugin/<插件名>/` | 数据端插件 |
| `src/client/plugin/<插件名>/` | 渲染端插件 |
插件目录内部按需要拆文件,常见形状两端一致:
| 文件 | 放什么 |
| --------------------------------------- | ------------------------------------------------------------- |
| `plugin.ts` | 插件类本身,实现 `IDataPluginDefine` 或 `IClientPluginDefine` |
| `types.ts` | 插件对外接口、存档数据结构 |
| `state.ts` / `manager.ts` / `system.ts` | 插件自己的对象,按需创建 |
| `index.ts` | 统一导出 |
写好后,把插件实例 `new` 进对应端的列表:
| 端 | 列表文件 | 数组 |
| ------ | --------------------------- | --------------- |
| 数据端 | `src/data/plugin/list.ts` | `dataPlugins` |
| 渲染端 | `src/client/plugin/list.ts` | `clientPlugins` |
```ts
// src/data/plugin/list.ts
import { IDataPluginDefine } from '@user/data-state';
import { DataShopPlugin } from './shop';
// 新建插件文件时,必须补上你自己的 import
import { MyPlugin } from './my-plugin';
/** 数据端插件列表 */
export const dataPlugins: IDataPluginDefine[] = [
new DataShopPlugin(),
// 追加到末尾,不要替换上面的已有插件
new MyPlugin()
];
```
```ts
// src/client/plugin/list.ts
import { IClientPluginDefine } from '@user/client-modules';
import { ClientShopPlugin } from './shop';
import { MyClientPlugin } from './my-client';
/** 渲染端插件列表 */
export const clientPlugins: IClientPluginDefine[] = [
new ClientShopPlugin(),
// 追加到末尾,不要替换上面的已有插件
new MyClientPlugin()
];
```
新建插件时,`list.ts` 里不会有你的类名,必须自己补一行 `import`,路径指向你的插件目录。只把数组写成 `[new MyPlugin()]` 会把已有插件挤掉。
两个入口文件分别把主插件放在数组前面,再展开各自的列表:`src/data/entry.ts` 中是 `[new MainDataPlugin(HERO_FACE), ...dataPlugins]`,`src/client/entry.ts` 中是 `[new MainClientPlugin(), ...clientPlugins]`。日常只需要改两个 `list.ts`,不必动入口。
同一端的注册表里,`id` 重复时后一个会被忽略,控制台报一条错误(错误码 `78`),所以请保证 `id` 在各自的注册表里唯一。
## 插件接口
数据端插件实现 `IDataPluginDefine`(来自 `@user/data-state`),渲染端插件实现 `IClientPluginDefine`(来自 `@user/client-modules`)。两者都由 `id`、依赖声明与三个时机方法组成;渲染端多一份 `clientDeps`,生命周期方法也多一个依赖参数。
如果你想新增一个插件,可以参考下面两个示例。数据端与渲染端各一份,结构相同。
### 数据端插件示例
```ts
// src/data/plugin/my-plugin/types.ts
import { IDataPluginDefine } from '@user/data-state';
/** 插件对外暴露的能力;依赖它的插件按这个接口调用 */
export interface IMyPlugin extends IDataPluginDefine {
/** 插件公开给其他插件调用的方法 */
doSomething(): void;
}
```
```ts
// src/data/plugin/my-plugin/plugin.ts
import { ICoreState, IDataPlugin } from '@user/data-state';
import { IMyPlugin } from './types';
// 数据端插件类;注册表按 id 创建它
export class MyPlugin implements IMyPlugin {
// 插件标识符:在数据端注册表里唯一
readonly id = 'my-plugin';
// 依赖的其他数据端插件 id;没有依赖就留空
readonly dataDeps: string[] = [];
// 同步初始化:创建插件自己的对象
awake(state: ICoreState): void {
// 在这里装配插件自己的对象
// 需要进存档的对象,在这里调用 state.addSaveableContent(...)
}
doSomething(): void {
// 对外方法:其他插件、钩子等通过插件对象调用
}
// 异步初始化:加载开始前执行;不需要就留空
async initialize(
state: ICoreState,
dependencies: Readonly<IDataPlugin>[]
): Promise<void> {}
// 加载完毕后执行;需要读加载内容时再填
async afterLoad(
state: ICoreState,
dependencies: Readonly<IDataPlugin>[]
): Promise<void> {}
}
```
### 渲染端插件示例
```ts
import {
IClientCore,
IClientPlugin,
IClientPluginDefine
} from '@user/client-modules';
import { IDataPlugin } from '@user/data-state';
// 渲染端插件类,文件放在 src/client/plugin/my-client/plugin.ts
export class MyClientPlugin implements IClientPluginDefine {
// 插件标识符:在渲染端注册表里唯一
readonly id = 'my-client-plugin';
// 依赖的数据端插件 id;没有依赖就留空
readonly dataDeps: string[] = [];
// 依赖的渲染端插件 id;没有依赖就留空
readonly clientDeps: string[] = [];
// 同步初始化:三个时机都会收到已解析的依赖
awake(
state: IClientCore,
dataDeps: IDataPlugin[],
clientDeps: IClientPlugin[]
): void {
// 依赖已经解析好,直接取用即可
}
// 异步初始化:加载开始前执行
async initialize(
state: IClientCore,
dataDeps: IDataPlugin[],
clientDeps: IClientPlugin[]
): Promise<void> {}
// 加载完毕后执行
async afterLoad(
state: IClientCore,
dataDeps: IDataPlugin[],
clientDeps: IClientPlugin[]
): Promise<void> {}
}
```
两个示例都声明了一个实现插件接口的类:数据端的 `MyPlugin` 实现 `IMyPlugin extends IDataPluginDefine`,渲染端的 `MyClientPlugin` 实现 `IClientPluginDefine`。它们都没有构造器,插件需要的对象要在 `awake` 里自己创建。三个时机方法各管一段:`awake` 里装配对象、`initialize` 里做加载前的异步准备、`afterLoad` 里做依赖加载内容的初始化。数据端示例把对外接口放在 `types.ts`,是为了让依赖它的插件能按接口调用;渲染端示例暂时没有单独拆文件。
### 逐项说明
**`id`**:两端的插件标识符,在各自的注册表里唯一。其他插件按这个字符串声明依赖,`getDataPlugin` / `getClientPlugin` 也按它取回插件。
**`dataDeps`**:依赖的数据端插件 id 数组。数据端插件只能填数据端插件;渲染端插件可以填任何数据端插件。没有依赖就留空数组。
**`clientDeps`**:只有渲染端插件有,写依赖的渲染端插件 id。没有依赖同样留空。
依赖方向是单向的:<strong>渲染端可以依赖数据端,数据端不能依赖渲染端</strong>。数据端不引用任何渲染端内容,才能在没有渲染端的环境里单独加载、存档与回放。
**依赖参数**:三个时机方法收到的依赖是解析后的插件包装对象,不是 id 字符串,形如 `{ status, plugin }`。数据端插件的参数统一叫 `dependencies`,类型是 `Readonly<IDataPlugin>[]`;渲染端插件分成 `dataDeps` 与 `clientDeps` 两个参数,类型分别是 `IDataPlugin[]` 与 `IClientPlugin[]`,顺序与声明一致。不需要时可以把依赖参数整个省略。
依赖在插件执行前已经解析完毕。如果声明在 `dataDeps` / `clientDeps` 里的依赖缺失或成环,插件本身不会被加载,所以拿到依赖后直接取 `.plugin` 用即可,<strong>不要判空</strong>。
**`awake`**:同步初始化,在主对象构造期调用。`awake` 返回 `void`,引擎不会等待它的返回值;需要 `await` 的东西放到 `initialize`。数据端此时地图与配置都还没加载,渲染端此时刚拿到渲染端主对象。
**`initialize`**:异步初始化,在加载开始前调用,会被 `await`。它不兑现,后面的插件与加载都不会继续。
**`afterLoad`**:加载完毕后调用,同样会被 `await`。适合读取加载内容之后再装配的对象。
三个时机的具体位置见下一节。
## 生命周期
三个时机在启动流程里的位置是固定的。两端一一对应,下面按数据端的执行顺序说明:
```text
CoreState 构造
└─ awake 同步执行,全部可初始化插件依次完成
load() 开始
├─ initialize 异步,加载开始前
├─ loader.start() 真正加载数据、产出进度
└─ afterLoad 异步,加载完毕后
```
### awake:主对象构造期,同步执行
`awake` 在主对象构造过程中被调用,此时地图、配置都还没加载。它返回 `void`,<strong>引擎不会等待它的返回值</strong>——即使你把它写成 `async`,引擎也会立刻走到下一个插件。
所以 `awake` 适合做「不需要等外部数据」的装配:
- `new` 出插件自己的系统、管理器;
- 把它们挂到插件对象上,供后续调用;
- 调用 `state.addSaveableContent(id, content)` 注册需要进存档的对象。
需要在 `awake` 里等的东西(读文件、等网络、等加载器),都不属于这里,放到 `initialize`。渲染端插件同样在渲染端主对象构造期执行 `awake`,只是没有存档注册这一类数据端操作。
### initialize:加载开始前
`initialize` 在 `load()` 的最前面被调用,此时数据还没开始加载。它返回 `Promise`,引擎会 `await` 它。
只要这个 Promise 不兑现,后面的插件(包括依赖它的)都不会继续,`loader.start()` 也不会开始。所以这里可以放心做「加载前必须完成」的异步准备,但不要在里面等一个永远不会结束的任务。
### afterLoad:加载完毕后
`afterLoad` 在整个加载结束之后被调用,同样返回 `Promise` 并被等待。它适合「必须读到加载内容才能做」的初始化,例如读核心配置、读地图、根据数据创建对象。
`load()` 的最后一步就是 `await afterLoad()`:它不兑现,加载流程就不算结束。
### 状态与顺序
每个插件在注册表里都带着一个 `PluginStatus`,随阶段推进:
| 状态 | 含义 |
| -------------------------- | ----------------------------- |
| `PluginStatus.NotAwakened` | 还没执行 `awake` |
| `PluginStatus.Awakened` | 已执行 `awake`,未异步初始化 |
| `PluginStatus.Initialized` | 已执行 `initialize`,未加载完 |
| `PluginStatus.Loaded` | 三个时机都已完成 |
三个阶段都按同一套依赖关系执行:<strong>被依赖的插件先完成同一阶段,依赖它的一方才会开始</strong>。也就是说,插件 B 的 `awake` 开始时,它声明的依赖插件 A 一定已经 `awake` 完毕。不构成依赖关系的插件之间,不要假定谁先谁后;需要顺序就显式声明依赖。
依赖关系在启动前定好,之后不会重算。启动后修改某个插件的 `dataDeps` 数组不会改变执行顺序,也没有接口在运行时追加插件。
依赖写错时,整条链路会被阻断:
- <strong>循环依赖</strong>:环上的插件全部被排除,控制台报一条错误(错误码 `76`),列出环上的 id;
- <strong>缺失依赖</strong>:`dataDeps` 里写了一个没有注册的 id,该插件被排除,控制台按「插件 id + 缺失的依赖 id」逐条报错(错误码 `77`);
- <strong>下游阻断</strong>:依赖上述插件的插件也会一起被排除,三个时机一个都不执行。
排查时先看控制台里的错误码与 id,再顺链往上找。依赖尽量少:每多一条依赖,就多一个「对方改名或失败会拖住自己」的机会。渲染端插件之间的顺序按 `clientDeps` 同样处理;`dataDeps` 指向的数据端插件在渲染端启动前已经就绪。
## 调用插件
插件的能力不只在插件之间使用。其他插件、钩子、事件、命令式脚本、渲染端 UI,只要拿得到对应的主对象,都可以调用插件的公开成员。
### 在插件里用依赖参数
声明了依赖,最直接的方式就是用时机方法参数里的插件对象。注意它是 `{ status, plugin }` 包装对象,你要取 `.plugin`:
```ts
import { ICoreState, IDataPlugin, IDataPluginDefine } from '@user/data-state';
import { IShopPlugin } from '../shop';
// 依赖商店插件,在初始化时调用它的公开成员
export class ShopConsumerPlugin implements IDataPluginDefine {
// 本插件的唯一标识
readonly id = 'shop-consumer-plugin';
// 依赖顺序与 dataDeps 数组一致
readonly dataDeps = ['data-shop-plugin'];
awake(state: ICoreState, dependencies: Readonly<IDataPlugin>[]): void {
// 依赖已经解析好,直接取 .plugin,不需要判空
const [{ plugin }] = dependencies;
// 断言成商店插件的对外接口,才能调用它公开的方法
const dataShop = plugin as IShopPlugin;
const shop = dataShop.createShopState('my-shop');
shop.enable();
}
// 本示例不需要异步初始化,留空即可
async initialize(
state: ICoreState,
dependencies: Readonly<IDataPlugin>[]
): Promise<void> {}
// 本示例不需要加载后处理
async afterLoad(
state: ICoreState,
dependencies: Readonly<IDataPlugin>[]
): Promise<void> {}
}
```
依赖数组的顺序与 `dataDeps` 一致。元素是注册表里同一个包装对象,它的 `status` 会随阶段推进更新:你在自己 `awake` 里看到的依赖状态是 `Awakened`,在 `afterLoad` 里看到的通常已经是 `Loaded`。
要调用插件自己的成员,需要把 `.plugin` 断言成它的对外接口(例如 `IShopPlugin`)。所以写插件时,如果希望别人调用你的方法,就在 `types.ts` 里定一个 `IMyPlugin extends IDataPluginDefine`,把公开成员写进去。渲染端插件的 `dataDeps` / `clientDeps` 参数同理,分别断言成数据端接口与渲染端接口。
### 用 getDataPlugin / getClientPlugin 取回
没有声明依赖、或者在启动之后才按需访问时,数据端用 `state.getDataPlugin(id)`,渲染端用 `state.getClientPlugin(id)`。两端返回的都是只读的插件包装对象,取不到时为 `null`:
```ts
import { ICoreState, PluginStatus } from '@user/data-state';
import { IShopPlugin } from './types';
// 数据端:按 id 取回插件,确认状态后再调用
export function enableShop(state: ICoreState): void {
const holder = state.getDataPlugin<IShopPlugin>('data-shop-plugin');
if (!holder || holder.status !== PluginStatus.Loaded) {
// 没注册、id 拼错,或还没执行到 afterLoad
return;
}
holder.plugin.createShopState('my-shop').enable();
}
```
```ts
import { IClientCore } from '@user/client-modules';
import { PluginStatus } from '@user/data-state';
import { IClientShopPlugin } from './types';
// 渲染端:接口名与取回方法不同,判空与状态检查完全一致
export function useShopUI(state: IClientCore): void {
const holder =
state.getClientPlugin<IClientShopPlugin>('client-shop-plugin');
if (!holder || holder.status !== PluginStatus.Loaded) {
// 渲染端插件同样可能还没到达 Loaded
return;
}
// 取回的是插件实例,公开成员可以直接使用
console.log(holder.plugin.ShopUI);
}
```
`getDataPlugin` 返回 `Readonly<IDataPlugin<T>> | null`,`getClientPlugin` 返回 `Readonly<IClientPlugin<T>> | null`。这里和依赖参数相反,两个检查都不能省:
- 返回 `null` 表示没有这个 `id`,可能是没注册,也可能是拼写错了;
- `status` 没到你要的阶段,说明它还没执行到那一步,此时调用它的方法可能读到还没装配好的字段。
如果在启动阶段就要用某个插件,优先声明依赖,让引擎保证顺序;这两个按 id 取回的方法更适合可选的、运行中才发生的调用。
### 插件之外也能调用
调用方不必是插件。钩子、事件脚本、命令式函数,只要拿得到 `state`,都能按 id 取回插件并调用它的公开成员:
```ts
import { ICoreState, PluginStatus } from '@user/data-state';
import { IHeroAttr } from '@user/data-common';
import { IHeroAttributeHooks } from '@user/data-base';
import { IMyPlugin } from './types';
// 钩子里调用插件:钩子只负责在合适的时机触发
export class MyAttributeHooks implements IHeroAttributeHooks<IHeroAttr> {
constructor(private readonly state: ICoreState) {}
onUpdateAttribute<K extends keyof IHeroAttr>(
name: K,
value: IHeroAttr[K]
): void {
const holder = this.state.getDataPlugin<IMyPlugin>('my-plugin');
if (holder?.status === PluginStatus.Loaded) {
holder.plugin.doSomething();
}
}
}
```
这里同样要判空与检查状态,因为 `getDataPlugin` 只是查注册表,不保证插件已经加载完成。渲染端插件用 `getClientPlugin`,写法一致。
## 示例:读一个现有插件
### DataShopPlugin:awake 里装配并注册存档
商店插件是 `dataPlugins` 里当前唯一的独立插件,适合逐段对照。它的代码在 `src/data/plugin/shop/plugin.ts`:
```ts
// 商店插件:把系统、管理器与存档注册收进一个单元
export class DataShopPlugin implements IShopPlugin {
// 数据端注册表里的唯一标识
id: string = 'data-shop-plugin';
// 不依赖其他插件,因此会在第一批 awake
dataDeps: string[] = [];
// 对外暴露的系统与管理器,其他插件可以直接调用
shop!: IShopSystem;
manager!: IShopManager;
/** 数据端主对象 */
// awake 时保存,供创建商店状态使用
private state!: ICoreState;
// 其他插件通过这个方法创建一份商店状态
createShopState(id: string): IShopState {
return new ShopState(id, this.state);
}
awake(state: ICoreState): void {
this.state = state;
// 这两个对象都需要主对象,只能在这里 new
this.shop = new ShopSystem(state);
this.manager = new ShopManager(state);
// 注册的是管理器,不是插件本身
state.addSaveableContent('@plugin/shop', this.manager);
}
// 商店不依赖加载内容,两个异步时机留空
async initialize(): Promise<void> {}
async afterLoad(): Promise<void> {}
}
```
逐段看:
- `id` 是 `data-shop-plugin`,没有依赖,所以它会在 `awake` 的第一批里执行;
- `awake` 先保存 `state`,再创建系统与管理器。这两个对象都需要主对象,所以只能在这里 `new`;
- `state.addSaveableContent('@plugin/shop', this.manager)` 把管理器注册为存档内容。<strong>注册的是管理器,不是插件本身</strong>;
- `initialize` 与 `afterLoad` 都是空的:商店不依赖加载内容,装配在 `awake` 就完成了。
`IShopPlugin` 在 `IDataPluginDefine` 之上加了 `shop`、`manager`、`createShopState`,这就是「插件对外暴露能力」的写法。别的插件依赖它时,就能直接调用这些成员。
渲染端也有形状对应的 `ClientShopPlugin`(`src/client/plugin/shop/plugin.ts`):它的三个时机都是空的,只把 `Shop` 与 `ShopUI` 作为公开成员暴露出来,供其他渲染端插件使用。两端插件的结构一致,差别只在依赖与生命周期参数。
### MainDataPlugin:afterLoad 读取加载后的配置
主插件 `src/data/initialize.ts` 的 `MainDataPlugin` 是 `afterLoad` 的典型用法。它的 `awake` 负责装配朝向、寻路、加载钩子与各系统;创建勇士则放到 `afterLoad`:
```ts
import { ICoreState, ICoreStateCoreConfig } from '@user/data-state';
import { FaceDirection, FaceGroup, IFacedTileLocator } from '@user/data-common';
import { logger } from '@motajs/common';
import { IDataMainPlugin } from './types';
// 主插件节选:只保留 afterLoad,其余成员用注释省略
export class MainDataPlugin implements IDataMainPlugin {
readonly id = 'main-data-plugin';
readonly dataDeps: string[] = [];
// 朝向组由入口在构造时传入
constructor(readonly heroFace: FaceGroup) {}
// ... awake / initialize 等成员省略
async afterLoad(state: ICoreState): Promise<void> {
// 核心配置只有在加载完成后才拿得到
const coreConfig = state.loader.getConfig<ICoreStateCoreConfig>('core');
if (!coreConfig) {
logger.error(79, 'Data');
return;
}
// 朝向组在 awake 里已经由入口传入,这里可以直接用
const dir = state.faceManager.get(this.heroFace);
if (!dir) {
logger.error(80);
return;
}
// 属性与位置都要靠加载后的配置才能创建
state.hero.createAttribute(coreConfig.init.hero.attribute);
const location: IFacedTileLocator = {
x: coreConfig.init.hero.x,
y: coreConfig.init.hero.y,
direction: FaceDirection.Down
};
state.hero.createLocation(location, dir);
}
}
```
这段为什么不能放进 `awake`:`coreConfig` 来自 `core.jsonc`,要等加载器读完才有。`awake` 阶段 `getConfig` 拿不到内容,创建勇士就会失败。`afterLoad` 正好是「加载已完成、但游戏还没正式开始」的时机。它同时说明了另一个点:`afterLoad` 拿到的 `state` 与 `awake` 是同一个主对象,`awake` 里存下的引用(如 `this.heroFace`)此时仍然可用。
## 验证
启动完成后,用 `state.getDataPlugin` 取回插件,确认它已经进了注册表:
```ts
import { ICoreState, PluginStatus } from '@user/data-state';
import { IMyPlugin } from './types';
// 启动完成后取回插件并检查状态
export function checkMyPlugin(state: ICoreState): void {
const holder = state.getDataPlugin<IMyPlugin>('my-plugin');
console.log(holder?.status === PluginStatus.Loaded, holder?.plugin);
}
```
`PluginStatus` 与 `ICoreState` 从 `@user/data-state` 引入,`IMyPlugin` 是你写在插件 `types.ts` 里的对外接口。`status` 应为 `Loaded`,`plugin` 应是你 `new` 的那个实例。如果是 `null`,先检查 `list.ts` 有没有真的 `new` 并放进数组。
想确认依赖阻断,可以把 `dataDeps` 里的某个 id 改成一个不存在的名字再启动:控制台应报缺失依赖错误(`77`,带插件 id 与缺失的依赖 id),并且你的插件没有任何时机输出。插件注册的存档内容如何验证,见[可存档内容](./saveable-content.md#验证)的「验证」一节。
## 边界与注意
- <strong>在 awake 里 await</strong>。`awake` 的返回值会被忽略,写 `async awake` 也不会被等待。异步准备放到 `initialize`。
- <strong>把 dataDeps 写成对象</strong>。它必须是 id 字符串数组;插件对象是三个时机方法收到的参数,不是声明时写的东西。
- <strong>对依赖参数判空</strong>。依赖在插件执行前已经解析好,写 `dataDeps[0]?.plugin` 只会掩盖声明错误;直接取 `dataDeps[0].plugin`。
- <strong>依赖成环</strong>。环上的插件全部被排除,依赖它们的下游也不执行。依赖关系要单向。
- <strong>依赖 id 拼错</strong>。插件不会报「找不到类」,而是被当作缺失依赖排除;控制台会给出插件 id 与缺失的 id。
- <strong>用 getDataPlugin 不判空</strong>。它查的是注册表,可能返回 `null`,也可能还没到你需要的阶段。
- <strong>直接改其他系统的内部字段</strong>。插件之间应通过公开方法与接口协作;直接改私有字段会绕开对方维护的一致性逻辑。
## 小结
- 插件把一组对象与它们的生命周期收进一个独立单元,在对应端的 `list.ts` 里 `new` 一次。
- 数据端与渲染端的写法一一对应;`dataDeps` 声明数据端依赖,渲染端另有 `clientDeps`;渲染端可以依赖数据端,数据端不能依赖渲染端。
- `awake` 在构造期同步执行,不能等异步;`initialize` 在加载前、`afterLoad` 在加载后,二者都会阻塞。
- 三个阶段都按依赖顺序走;循环依赖、缺失依赖会连同下游一起被阻断。
- 插件可以被其他插件调用,也可以在钩子、事件或任何拿得到主对象的地方按 id 取回调用。
返回[高级 I 导览](./index.md)。