New Site Nest 和 Vue 插件平台源码与架构学习教程
这套方案要解决的业务问题是:一个已交付的系统,怎样在不反复修改宿主业务代码的情况下,安装订单、CRM、待办等应用,并同时获得后端能力、前端页面和可控的启停行为。
它的核心做法是:宿主提供稳定的运行协议,插件交付业务实现,管理器把经过校验的业务实现接入平台。 Nest 负责创建后端对象,Vue 负责呈现页面,安装、注册、授权和生命周期则由我们自己的插件平台负责。
面向前端开发者,建议第一次先读第 1、2、4 节和第 7 节第 2、4 章;第二次再带着断点读安装、请求和停用链。完整实战路线放在第 6、9、10 节。
0. 分析范围与可信度说明
本文分析的是本地 E:/demo/new-site,重点是宿主、插件 SDK 与已经改造为 Vue 3、Vite、Pinia 的 plugin-template。CRM 和订单插件证明平台可以接入多种业务,详细代码示例采用当前待办模板,避免在多套业务字段之间来回切换。
资料包含实际源码、package.json、模板 README、历史验收报告,以及上一版完整交互架构图。New Site 当前不是一个具有独立提交历史的 Git 仓库;架构证据使用的本地源码快照提交为 95425fee313202400957b139b5d1c0219a2ce7b1,它是文档证据快照,不代表已发布产品版本。
Halo 对照范围是本地 E:/demo/research/halo 的 main 分支,提交 f6194fbd51ef995ce065dc603c3adf2ae39ff669。本文只比较已读到的插件核心与 UI 装载机制,不推断私有应用市场的服务端实现。
本文使用以下标记:
- 【已验证】:当前源码、配置或本轮执行结果直接支持。例如
enable()的调用顺序、固定 HTTP 分发入口、Vue 适配器创建独立应用。 - 【合理推断】:根据现有结构评估的影响,例如同页插件可以干扰 DOM、失控异步 disposer 可能阻塞 UI 同步。
- 【待确认】:没有在本轮运行中复测,或属于尚未实现的生产能力,例如多发布者密钥治理、数据库事务和跨进程隔离。
本轮实际执行了文件与符号检索、源码读取,以及模板目录中的 npm run check,后端 TypeScript 与前端 vue-tsc 均通过。没有在本轮重新安装依赖、运行宿主、重新打包或执行会改写源码的浏览器验收。历史 8 项自动检查、18 项开发浏览器验收、9 项真实宿主验收属于已有记录,不能等同于本轮复测,见 VERIFICATION.md。
有三处阅读时必须校正的旧信息:旧架构图中的数据库目前是 JSON 文件;宿主路由使用 hash 和 Map,不是 Vue Router;模板仍有 NotesService、NotesRuntime、notes-* 操作和少量“便签”文案。它们目前服务于待办页面,不存在一个已经实现的 TodoController。种子数据中提到旧 src/ui 目录的文字也已过时,现在应读 src/App.vue。这些命名没有改变实际插件协议。
1. 项目全景概览
30 秒解释版本
New Site 是一个可安装业务应用的 Nest 宿主。一个插件包同时带着后端代码、Vue 页面和元信息。管理员上传后,平台先校验和保存;启用时再创建插件的独立 Nest 容器,注册业务操作,再让浏览器加载插件页面。停用时先撤销入口,再排空请求,最后清理前后端资源。
5 分钟解释版本
假设你向三个客户交付同一套系统。客户甲需要 CRM,客户乙需要订单,客户丙只需要待办。如果直接把所有业务写进宿主,每次新增业务都要改模块导入、HTTP 接口、前端路由、菜单、权限、构建与发布,客户之间的业务组合也会逐渐变成分支判断。
插件平台把变化分成两层。宿主负责长期稳定的事情:用户身份、应用安装、协议版本、资源归属、统一 API 入口、页面装载、日志与存储能力。插件负责变化频繁的事情:订单状态机、线索字段、待办表单、业务校验和展示。
开发者在独立 Vue 工程中写 App.vue 和 Pinia,在 src/server 中写 Nest 业务服务。完成后执行打包命令,把构建产物连同 Manifest 与签名封成 .plugin.json。客户无需把插件仓库复制进宿主源码,只需上传符合协议且受宿主信任的包。
这个设计适合按客户选择业务模块的私有化部署、内部受信应用广场,以及由统一团队或经过审查的合作方开发的扩展。它也适合把稳定平台与快速变化的业务交付节奏分开。
它的边界同样明确:当前后端插件共享 Node 进程,前端插件共享页面。独立 Nest 容器、Vue app 和 Pinia 解决对象与生命周期组织问题,不能阻止恶意代码访问进程或页面。对陌生开发者上传即运行、严格租户隔离、资源配额、任意版本无重启切换等需求,需要额外架构。
借鉴 Halo 的地方与实际差异
| 维度 | Halo 已读源码 | 当前 New Site |
|---|---|---|
| 管理器 | PF4J DefaultPluginManager 的扩展 |
自建 PluginPlatform,承担 PluginManager 职责 |
| 后端加载 | JAR、插件 ClassLoader | CJS 与 Node require() |
| 插件容器 | 带 ClassLoader、以共享容器为 parent 的 Spring Context | 独立根 Nest ApplicationContext,显式注入 SDK |
| 后端接入 | 注册 Controller 映射及 RouterFunction | 固定 Controller 查询动态 operations Map |
| 前端装载 | 发现 provider、CSS 与模块装载、向真实 Vue Router 注册路由 | provider、CSS、ESM 与 hash 路由 Map;适配器创建独立 Vue app |
| 停用 | 注销路由、关闭上下文,另有 PF4J unload 流程 | 撤销入口、排空、清理、关闭 DI 容器;不承诺卸载 Node 模块缓存 |
Halo 依据为固定提交中的 HaloPluginManager、Context 工厂 与 UI 注册流程。New Site 依据为 PluginPlatform.enable、固定操作入口 和 UI 注册。
因此,这是一套借鉴 Halo 组织思想的 Node 实现。当前成熟度是可实践、已有验收记录的教学 Demo,不是已具备审核、结算、许可证和强隔离能力的商业市场产品。
2. 开始学习前需要掌握的概念
必须先掌握
| 概念 | 定义与项目中的作用 | 对应例子与源码 | 前端类比及局限 |
|---|---|---|---|
| 宿主与插件协议 | 宿主是运行环境,协议规定业务怎样被接入 | HostSdk、PluginEntrypoint、PluginManifest:SDK |
类似组件 props 和插件接口;协议还必须处理权限、版本、资源回收 |
| 依赖注入 DI | 由容器创建对象并把依赖交给构造函数 | NotesRuntime 注入 SDK 与 NotesService:后端入口 |
类似 provide/inject;后端容器不管理 DOM,也不等于全局变量 |
| DynamicModule | 描述模块类、Provider 与导出的定义对象 | createPluginModule():工厂 |
类似应用配置;返回定义不等于应用已经运行 |
| ApplicationContext | Nest 按模块定义创建出的 DI 对象图 | NestFactory.createApplicationContext():启用 |
类似创建出的应用实例;它在本例不监听端口,不提供进程隔离 |
| Runtime 与生命周期 | 插件供宿主调用的启动和清理对象 | PluginRuntime.start/dispose:接口 |
类似 mount/unmount 契约;后台连接、timer 必须明确归属 |
| Operation | 由 ID、权限、输入 Schema 和 handler 组成的业务操作 | notes-create:注册 |
类似按 action 名分发;它是在后端执行并再次鉴权的业务入口 |
对三个核心概念,分别回答八个问题就不会混淆:
- PluginManager 是什么? 自建协调器;为什么需要?Nest 不负责下载、验签、安装与运行状态;在哪里?真实类
PluginPlatform;例子?启用 todo;类比?应用生命周期管理员;局限?不是 JS 模块系统;误区?把它当 Nest 原生类;依赖?它调用工厂和 Nest,而不是被 DynamicModule 自动创建。 - DynamicModule 是什么? 模块元数据;为什么需要?让插件显式定义自己的 Provider;在哪里?工厂返回值;例子?
HOST_SDK的 value provider 与PLUGIN_RUNTIME的 class provider;类比?组件树配置;局限?不是对象实例;误区?动态模块天然支持热卸载;依赖?需要 ApplicationContext 才会形成可用对象。 - ApplicationContext 是什么? DI 容器实例;为什么需要?让每次激活拥有一套可关闭的插件对象;在哪里?
activation.context;例子?同一个发布停用再启用产生新 Runtime;类比?新的 Vue app;局限?仍然共享进程,且不会自动继承宿主 Provider;误区?独立容器等于安全沙箱;依赖?根据模块定义创建,再通过 Token 取得 Runtime。
可以边读边学
| 概念 | 当前作用与例子 | 源码与常见误区 |
|---|---|---|
| CJS、ESM、import map | 后端 require CJS;浏览器 import ESM;依赖名映射到共享 Vue | Vite 配置、import map。前后端不是同一个模块运行环境 |
| Manifest、digest、activationId | Manifest 描述身份和入口;digest 标识发布内容;activationId 标识某次运行 | 数据类型。相同版本重新启用,activationId 可以变化而 digest 不变 |
| JSON Schema 与 Ajv | 把不可信请求变成业务函数可接受的数据 | prepareOperations。TypeScript 类型不会在网络输入上自动执行 |
| HMR | 替换前端模块并尽量保留开发状态 | Pinia HMR。HMR 不是生产升级,也不保证任意修改都保留状态 |
| drain 与协作取消 | 撤入口后等待已接收的受管工作结束,通过 AbortSignal 通知取消 | drain。signal 不能强制打断忽略它的代码 |
| 数字签名 | 用受信公钥验证包来源与完整性 | 包校验。签名不是恶意行为检测 |
进阶阶段再学习
可以在独立完成一个插件后,再学习发布者信任与吊销、可恢复安装任务、数据库迁移、可靠事件 Outbox、跨进程 RPC、资源限额和宿主 SDK 兼容策略。它们解决生产治理问题;当前文件存储、内存事件和 DI 容器并没有隐含提供这些能力。
概念依赖顺序是:业务模块化需求 → 插件协议 → DI 与模块定义 → 容器与 Runtime → 操作注册和 UI provider → 生命周期与资源所有权 → 发布、兼容与安全治理。
3. 技术栈与工程系统
| 技术 | 用途 | 核心程度 | 配置或入口 | 学习优先级与掌握程度 |
|---|---|---|---|---|
| Node 与 TypeScript | 进程、构建、类型与后端执行 | 核心 | 宿主 package.json | 高,理解模块缓存与异步任务 |
| Nest 11.1.6 | 宿主 HTTP、插件 DI 容器 | 核心 | host.ts | 高,掌握 Provider、Token、Context |
| Vue 3.5.22 | 宿主与插件页面 | 核心 | plugin.ts | 高,掌握应用实例与卸载 |
| Vite 7.1.12 | SFC HMR 与生产 ESM 构建 | 核心开发工具 | vite.config.ts | 高,掌握库模式、external、相对资源 |
| Pinia 3.0.3 | 插件页面状态与 Store HMR | 插件前端核心 | todos.ts | 高,掌握每 app 创建与 dispose |
| esbuild | 后端 TypeScript 打为 CJS | 构建核心 | build.mjs | 中,理解入口和外置依赖 |
| Ajv | 操作输入运行时校验 | 后端核心 | prepareOperations | 中,会写与调试 Schema |
| Node crypto | SHA-256 与 Ed25519 | 包协议核心 | pack.mjs | 中,理解摘要、签名和受信公钥 |
| node:test 与 Playwright | HTTP、生命周期、真实浏览器验收 | 质量工具 | 测试脚本 | 中,会选择有意义的验收 |
宿主与模板各有自己的 package.json 和锁文件。模板通过 sdk/*.tgz 相对文件依赖引用 SDK、UI 协议、Vue adapter 和开发宿主快照,因此可复制到独立目录开发,不需要相对导入宿主源码。依赖事实见 模板 package.json。当前是一个包含宿主、SDK、示例插件和独立模板的多包目录组织,不应把它描述成已经采用 npm workspaces 的发布型 Monorepo。
开发路径和生产路径分开:开发时前端来自 Vite 模块图,后端来自可重启的 API 子进程;生产时执行类型检查与打包,输出签名 .plugin.json,客户上传后由原宿主装载。宿主自己的 npm run dev 只构建再启动,模板的 npm run dev 才有这套 Vite 开发监督流程。
配置入口主要是插件 plugin.json、模板工具设置与本地签名配置。模板 Node 要求为 ^20.19.0 || >=22.12.0;宿主声明 Node >=20,两者不能混为一个最低要求。发布流程不把开发密钥、业务数据、node_modules 或私钥打进插件包。
4. 整体架构
4.1 架构风格
【已验证】这是同进程插件架构。宿主维护稳定平台内核,插件有独立 Nest DI 对象图,通过显式 SDK 接入共享能力。判断依据是 require(entry) 与 createApplicationContext() 都在同一 enable() 方法内执行,业务请求最终直接调用 JavaScript handler,没有插件 RPC 或容器网络。
它有模块化单体的运行特征,也有独立制品的交付特征。独立交付不要求每个业务都成为微服务;同进程执行也不要求把业务源码编译进宿主入口。
4.2 模块划分
| 模块 | 职责与对外接口 | 上游 → 下游 | 层级与源码 |
|---|---|---|---|
| HTTP 宿主 | 固定管理、操作与 provider API | 浏览器 → Platform | 核心:host.ts |
| PluginPlatform | 安装、启用、发布、排空、清理与持久状态 | Controller → 插件/Nest/文件 | 核心:平台 |
| 后端 SDK | Token、Runtime、Operation、Manifest 与能力契约 | 平台/插件 → 稳定类型协议 | 核心:SDK |
| 插件后端 | Provider、业务校验、操作与生命周期 | SDK/容器 → 业务 JSON | 核心:模板后端 |
| 活动注册表 | operations Map 与 stage 门禁,属于平台内部状态 | Dispatcher → handler | 核心:注册校验 |
| UI provider | 只公布活动、对当前用户可见的插件描述 | 平台 → 浏览器装载器 | 核心:providers |
| 宿主前端 | 页面壳、管理台、菜单、hash 路由、import 与撤销 | provider/用户 → 插件 UI/API | 核心:app.js |
| Vue adapter | 将普通 Vue app 转为宿主 UI 协议,挂载与清理 | 宿主路由组件 → App.vue | 核心:adapter |
| 插件前端 | SFC、Pinia、业务请求、样式 | HostContext → ctx.invoke | 业务:App.vue |
| 开发监督器 | Vite 长驻、构建成功后重启后端、API 代理 | 文件保存 → 开发子进程 | 重要:dev.mjs |
| 构建与签名 | CJS/ESM/CSS/chunks 收集、摘要与签名包 | 工程源码 → 插件包 | 重要:build、pack |
| JSON 本地存储 | 安装意图与插件命名空间业务文档 | 平台 → 文件系统 | 重要:存储实现 |
PackageLoader、Registry 与事件能力在图中是职责划分;当前不是三个已经独立实现和部署的服务类。
4.3 架构图

图按开发、打包、上传、启用、业务访问和停用清理展开。放大原图可查看各节点及连线;下面给出便于先建立整体模型的简化版本。
完整图还包含本地 HMR、资源 API、SDK 事件、停用失败分支与源码节点依据:完整交互架构图。它是本地文件;转为对外文章时应上传图的导出文件或替换成读者可访问的地址。
4.4 核心数据流
以“新增一条待办”为例,页面先把表单交给 Pinia action,action 调用 ctx.invoke('notes-create', input)。宿主 Context 将操作名转为固定 API URL,附上会话、CSRF 与发布 digest。Controller 进入平台,平台确认这次激活可接流量、用户具备权限且输入符合 Schema,然后调用插件 handler。
handler 进入 NotesService.create(),通过 SDK 的串行更新保存文档,随后发布插件自己的事件;订阅者尝试发送通知,业务结果返回给浏览器。事件通知失败会记录日志,不把已经成功保存的业务伪装成失败。它没有可靠事件重放保证。
4.5 启动流程
真正入口 main.ts 解析根路径并调用 createHost(),读取 PORT,最后监听本地 4310。createHost() 创建 PluginPlatform 并先执行 init(),再创建包含固定 Controller 的宿主 Nest HTTP app,提供静态页面。见 createHost。
平台初始化从安装状态 JSON 恢复安装记录,对 desiredEnabled=true 的插件尝试调用 enable()。单个启用失败被记录,但安装状态文件本身非法仍可能阻止初始化。最后浏览器加载宿主壳、建立演示会话、获取 UI providers,才装载对应业务页面。恢复逻辑见 init。
4.6 关键调用链
启用链:PlatformController.enable → PluginPlatform.enable → verifyInstalledFiles → require(entry) → createPluginModule(createSdk(activation)) → NestFactory.createApplicationContext → context.get(PLUGIN_RUNTIME) → runtime.start → prepareOperations → 切换 ENABLED → 持久化。异常进入 withdraw、drain、cleanup,并根据能否确认清理选择 FAILED 或 RESTART_REQUIRED。依据:Controller、完整启用。
业务链:useTodosStore() 返回的 save() → UiContext.invoke → PlatformController.invoke → PluginPlatform.invoke → Operation.handler → NotesService.create → sdk.storage.update → JSON 文件 → 返回对象。异常由平台记录,再交给 Nest 返回 HTTP 错误。依据:Store.save、Context、分发、业务、存储。
5. 源码地图与推荐阅读顺序
| 顺序 | 文件与关键符号 | 阅读目的与前置概念 | 读完能回答什么 |
|---|---|---|---|
| 1 | plugin.json | 先看制品契约,只需 JSON | 一个应用向宿主声明什么 |
| 2 | SDK:HostSdk、Runtime、Manifest | 理解 TypeScript 接口与 Token | 插件能调用哪些宿主能力 |
| 3 | 插件工厂 | DI、Provider、DynamicModule | SDK 和 Runtime 怎样进入容器 |
| 4 | enable | 模块缓存与异步函数 | 谁创建容器,何时公开操作 |
| 5 | 固定 Controller、invoke | HTTP 与 Schema | 不改宿主怎样增加业务操作 |
| 6 | plugin.ts、adapter | Vue createApp、provide、Pinia | 普通 SFC 怎样成为插件页面 |
| 7 | synchronizeUi | ESM、CSS、Map | 页面和菜单为什么会出现或消失 |
| 8 | disableInternal、cleanup | 在途任务、资源所有权 | 为什么停用要分多个阶段 |
| 9 | dev.mjs、build.mjs、pack.mjs | Node 子进程与构建 | 开发体验为何不等于生产升级 |
| 10 | 模板测试、浏览器验收 | 测试隔离 | 如何证明扩展没有破坏生命周期 |
调试重点是 enable/invoke/withdraw/drain/cleanup、前端 removeUi/synchronizeUi 和业务 service。二次开发先改模板业务文件;理解核心之前暂时跳过 node_modules、dist、运行时 .data/.local、旧截图和生成的签名制品。SDK tarball 是交付物,理解协议先读对应源码。
6. 完整学习路线
第一轮 7 天建立可解释的整体模型;再用约 8 天熟悉源码与调试;若要独立负责平台演进,按一个月安排兼容、安全与持久化专题。
| 阶段 | 建议时间 | 阅读与源码 | 实践 | 验收 |
|---|---|---|---|---|
| 全景与概念 | 第 1 天 | 第 1—4 节,Manifest、SDK、工厂 | 手画宿主、后端插件、浏览器三者关系 | 能解释需求与三个核心对象 |
| 运行与观察 | 第 2 天 | 第 9 节,入口与 dev 工具 | 在独立模板副本运行预览,观察 PID/provider | 说清 Vite 与 Nest 子进程各负责什么 |
| 主链路 | 第 3—4 天 | enable、Controller、invoke、Service、storage | 对一次新增操作逐步调试 | 定位权限、Schema、handler 与写入 |
| Vue 接入 | 第 5 天 | plugin.ts、adapter、synchronizeUi | 修改 SFC 与 store,观察 HMR | 能解释共享 runtime、独立 app 与 Pinia |
| 生命周期 | 第 6—7 天 | withdraw、drain、cleanup、removeUi | 启停、读只读角色结果、观察资源数 | 撤入口、保留业务数据、无新增后台任务 |
| 故障与修改 | 第 8—11 天 | 测试、签名、操作注册、错误分支 | 修改已有功能,模拟操作名错误并修复 | 根据证据定位故障,而非盲目重启 |
| 小型扩展 | 第 12—15 天 | 第 10、11 节 | 独立新增一个报销插件并打包安装 | 宿主源码无业务性修改,完成完整启停 |
| 平台能力 | 第 16—30 天 | 第 8、12、16 节 | 设计一次 SDK v2/数据库/发布审核改造 | 说明兼容迁移、信任边界和失败恢复 |
7. 详细课程章节
第 1 章:把一个业务工程变成可安装制品
1. 本章定位
先理解宿主接收什么,才能判断哪些事情属于插件。一个 Vue dist 只说明怎样在浏览器执行,并没有描述后端入口、权限、宿主兼容版本和可信来源。
2. 学习目标
能辨认 Manifest、文件列表、digest 与签名;能指出安装与启用的边界;能解释为什么上传源码或任意 ZIP 不符合当前协议。
3. 前置知识
Base64 是二进制转文本的编码,不能证明安全;摘要标识内容;签名让宿主用预先信任的公钥核对“谁签过哪些内容”。这些步骤都先于执行第三方 JavaScript。
4. 对应源码
读 PluginPackage、pack、install、validatePackage。这四处分别定义契约、生产制品、消费制品与校验条件。
5. 工作原理
问题是客户系统怎样在没有插件源码工程的情况下安装业务。设计目标是制品自描述、完整、可核对、可重复安装。核心抽象是 manifest + files + signature;实现按文件记录路径、Base64 内容与 SHA-256,并对规范化的 Manifest 和排序后的路径/摘要载荷签名。
6. 执行流程
npm run pack → 类型检查 → 后端 CJS 与前端 Vite 构建 → 收集 CSS/chunks → 文件摘要 → 签名 → 输出 .plugin.json → 上传 → 宿主校验 → 暂存与重命名 → 记录 INSTALLED。这里最后一步仍没有 require 插件。
7. 关键源码讲解
包的实际结构可以缩写为:
interface PluginPackage {
manifest: PluginManifest;
files: Array<{ path: string; contentBase64: string; sha256: string }>;
signature: { keyId: string; value: string };
}
输入是构建完成的字节,输出是可上传 JSON。校验拒绝越界路径、重复文件、缺失入口、超限文件、错误摘要或无效签名。同 ID/版本不能被不同内容静默覆盖。文件上限 200、单文件 4 MiB、解码总量 20 MiB;模板还把最终 JSON 控制在 10 MiB,Base64 膨胀后可能更早遇到这个限制。当前只接受本机演示信任键 demo-local,不是多发布者市场。
8. 调试与观察方法
断点放在 validatePackage 与 install 写安装记录前;观察 Manifest、digest、文件名和 phase,不输出私钥。正常安装后应为 INSTALLED 且没有新增业务操作。安装失败先看入口、签名公钥、版本与路径;不要通过关闭验签“解决”。npm run pack 在本文本轮未执行,历史结果见第 0 节。
9. 本章实践任务
在模板副本打包后,只读取并列出包内文件与入口,确认包含后端 CJS、前端 ESM、CSS 和帮助组件 chunk,不含私钥、source map 与 node_modules。验收为 Manifest 每个入口都在 files 中。只观察不需要撤销;修改实验字段后用副本原始文件恢复。
10. 常见误区
安装不是执行;签名不是加密;相同 keyId 不代表同一把密钥;开发密钥不自动获得客户宿主信任。第三方发布应由审核后的受信流程签名,不能把宿主私钥发给所有开发者。
11. 自测题
- Base64、摘要、签名分别解决什么?
- 为什么安装阶段不能公开业务入口?
- 哪里定义包格式与文件限制?
- 摘要正确但签名失败应排查什么?
- 支持多个发布者需要怎样扩展信任模型?
第 2 章:PluginManager 如何使用 DynamicModule 和 ApplicationContext
1. 本章定位
这是整套机制的中心:从磁盘上的代码文件,到一组真正运行的 Nest 业务对象,中间需要加载、描述、实例化、启动和发布五个动作。
2. 学习目标
能按真实顺序解释 enable;能分别指出三个核心对象的输入、输出和所有者;能说明插件如何取得 SDK 而不是直接依赖宿主内部 Service。
3. 前置知识
Provider 是容器管理的对象定义,Token 是查找依赖的标识。useValue 注入已有对象,useClass 让容器创建类实例。一个独立 Context 对应一张依赖对象图,但这些对象仍在同一个进程和事件循环中运行。
4. 对应源码
重点是 enable、createPluginModule、Runtime.start 和 SDK Token。
5. 工作原理
问题是插件未编译进宿主 imports,宿主怎样创建它。设计目标是通过稳定工厂协议取得模块定义,并让每次激活持有自己的容器与资源。Manager 负责何时加载与何时公开,DynamicModule 负责怎样组装,ApplicationContext 负责创建并持有实例。
6. 执行流程
7. 关键源码讲解
插件真实返回的定义为:
export function createPluginModule(sdk: HostSdk): DynamicModule {
return {
module: NotesPluginModule,
providers: [
{ provide: HOST_SDK, useValue: sdk },
NotesService,
{ provide: PLUGIN_RUNTIME, useClass: NotesRuntime },
],
exports: [PLUGIN_RUNTIME],
};
}
输入是平台为本次激活创建的 SDK;输出就是 DynamicModule 配置对象。运行时 Token 使用 SDK 约定的 PLUGIN_RUNTIME 常量,没有另一个 runtimeToken 返回字段。Manager 随后调用 Nest 并通过 Token 取出 Runtime。这里没有 HTTP server,也没有父容器自动继承。SDK 是显式的 value provider,业务插件不需要知道宿主内部 Service 的路径与构造方式。
Runtime.start 注册的操作先被暂存;Manager 编译输入 Schema、检查重复 ID 与权限声明,全部通过后才将 stage 改为 ENABLED。这个可见性切换发生在单个事件循环同步步骤内,不能理解为同时回滚文件写入的数据库事务:start 已写入的初始化数据不会自动撤销。
启用失败会撤销入口并清理。若已经执行代码却没有得到可控 Context,或者回滚清理失败,会进入 RESTART_REQUIRED。abortOnError:false 允许错误返回管理流程,不代表初始化失败时一定没有残留。
8. 调试与观察方法
依次断在平台 251、253、256、257、260、263 行;观察 definition.providers、activation.context、activation.runtime、activation.operations、activation.stage。启用前没有活动实例,start 中仍为 STAGING,发布后才 ENABLED。找不到 PLUGIN_RUNTIME 应检查 Token 和 providers,而不是改宿主 HTTP 路由。
9. 本章实践任务
在调试副本给 NotesRuntime 的构造、start、dispose 各加一条日志,启用、停用、再启用同一制品。验收为两次不同 activationId、不同 Runtime 实例,业务数据保持。恢复时删除实验日志并重新打包;不要据此承诺新的 digest 可在同进程切换。
10. 常见误区
DynamicModule 的“动态”不包含应用市场;Context 不会自动共享所有宿主对象;关闭 Context 不清除 require 缓存。PluginManager 在这里是架构职责名称,源码类名是 PluginPlatform。
11. 自测题
- 模块定义与容器实例有什么不同?
- 谁决定调用 start 和发布操作?
- 哪里把 SDK 绑定到 HOST_SDK?
- start 中注册成功但页面没出现,应观察哪个状态?
- 为什么直接把宿主 ModuleRef 交给所有插件会增加耦合?
第 3 章:固定 HTTP 入口如何承载动态业务
1. 本章定位
理解容器之后,需要解释用户请求怎样到达插件。这个 Demo 的关键选择是保持 Nest Controller 固定,让业务操作注册表动态变化。
2. 学习目标
能从新增待办按钮走到写文件;能区分前端隐藏按钮与后端权限;能指出 Schema、release 校验、在途计数与实际 handler 的位置。
3. 前置知识
客户端 TypeScript 类型不约束伪造的 HTTP 请求。后端必须在业务函数前检查身份、权限和输入。CSRF 保护基于浏览器自动携带的会话凭据,不能由前端 UI 是否显示某按钮替代。
4. 对应源码
读 Context.invoke、Controller.invoke、平台 invoke、Service.create、SDK.storage。
5. 工作原理
问题是插件操作不断增加,宿主不能逐个 import 业务 Controller。设计目标是让通用入口统一治理流量。核心抽象是 PluginOperation:ID、title、permission、inputSchema、handler。新增业务改变注册表,不改变固定网关。
6. 执行流程
7. 关键源码讲解
真实注册片段的关键字段是:
this.sdk.registerOperation({
id: 'notes-create',
title: '创建便签',
permission: `${this.sdk.pluginId}.note.create`,
inputSchema: {
type: 'object', additionalProperties: false, required: ['title'],
properties: {
title: { type: 'string', minLength: 1, maxLength: 100 },
content: { type: 'string', maxLength: 2000 },
},
},
handler: (input, context) => this.notes.create(input, context),
});
运行时从当前激活的 Map 查 handler,先检查 digest、ENABLED、用户权限和 Schema,再登记本次任务的 Promise。handler 的 context 提供 principal、requestId 与 signal。这样停用时可以拒绝新任务,又知道旧任务还有多少没有结束。
存储 update 把同一个插件、同一个 key 的“读取、修改、写入”放在串行队列内,避免两个并发请求都读取旧数组再互相覆盖。实际文件在 .data/business/<id>/<key>.json,以临时文件写入后 rename 替换。它适用于当前单进程示例,不具备多实例数据库事务语义。
8. 调试与观察方法
浏览器 Network 查看操作 ID、状态码与 x-plugin-release,不要复制认证头到公开日志。后端断点放在 invoke 的状态/权限/Schema 分支与业务 update 回调。旧发布可能返回 409,未启用或操作不存在可能返回 404,权限不足返回 403;具体错误正文与记录一起判断。
9. 本章实践任务
在独立副本中用只读角色尝试创建,再以管理员创建。验收是只读请求后端拒绝且数据不变,管理员成功后数据、统计和通知一致。删除自己创建的测试记录即可恢复;不要删除整个业务目录。
10. 常见误区
动态操作注册不等于热插 Nest Controller;前端校验不等于后端校验;文件 update 的串行性不代表跨文档事务;当前事件名必须位于本插件命名空间,不是任意跨插件共享业务总线。
11. 自测题
- 为什么网络输入仍需要 JSON Schema?
- Controller、Map 与 Service 分别承担什么?
- 在途 Promise 在哪里登记?
- 按钮消失仍能伪造写请求,应该在哪修复?
- 想支持订单与库存的跨业务事务,需要增加什么能力?
第 4 章:普通 Vue 应用如何接入宿主并保持开发体验
1. 本章定位
后端已经公开能力,浏览器还需要知道“有哪些业务页面、从哪里加载、怎样挂载和撤销”。这章把熟悉的 SFC 工程与插件运行协议连接起来。
2. 学习目标
能解释 plugin.ts、adapter、App.vue、Pinia 各自的职责;能追踪 provider 到菜单;能区分普通前端 HMR、后端重启和生产发布。
3. 前置知识
共享 Vue runtime 是共享框架实现;独立 Vue app 是不同的根应用;独立 Pinia 是不同的内存 Store 容器。三者同时存在并不矛盾。ESM external 表示运行时从宿主解析依赖,不是把依赖代码丢弃后希望浏览器自行找到它。
4. 对应源码
读 plugin.ts、adapter、synchronizeUi、Vite 配置、dev 监督器。
5. 工作原理
问题是业务工程应该像普通 Vue 项目一样写,同时又符合宿主的 UI 插件接口。适配器负责桥接:对宿主导出 id、routes、activate,并由 activate 返回清理函数;对业务创建 Vue app、注入 HostContext,并调用业务 setup 安装 Pinia。
6. 执行流程
GET UI providers → 校验 digest/activationId → 加载 CSS 与动态 import → definition.activate(ctx) → 注册合法单一路径到 Map → 生成菜单 → 宿主渲染包装组件 → 包装组件 createApp/provide → 安装 Pinia → mount App.vue → Store 调 ctx.invoke。
7. 关键源码讲解
业务入口是真实代码:
export default defineVueAppPlugin({
manifest,
root: App,
setup(app) {
const pinia = createPinia();
app.use(pinia);
return () => disposePinia(pinia);
},
});
adapter 在包装组件 mounted 时 createApp(root),app.provide(CONTEXT, props.ctx),调用 setup 后 mount。每次页面挂载创建自己的 Pinia;普通切页或停用时 unmount 和 dispose,因此持久化业务数据与页面内存草稿必须分开考虑。
生产 Vite library mode 生成 ESM、一个 CSS 文件和相对路径 chunk。external 只有 Vue 与宿主 UI SDK;Pinia 和适配器被打进插件。宿主 import map 提供共享 Vue,避免宿主渲染出的包装组件引用另一套不一致的框架身份。
开发时 Vite 长驻。SFC、CSS、Store 保存交给 HMR;Pinia 使用 acceptHMRUpdate。src/server/** 或 plugin.json 变化才触发后端开发包重建,先构建成功再停旧 API。后端使用随机端口,由 Vite 代理统一入口,前端开发者无需管理两套 API 地址。重启 API 后 activationId 改变可能重新挂载 Pinia,不能承诺未提交草稿跨后端重启保留。
8. 调试与观察方法
前端断点放在 synchronizeUi、adapter mounted 与 Store action。Network 检查 provider 描述、主 ESM、CSS、HelpPanel chunk 与操作请求。普通 SFC HMR 时观察 DOM 更新、页面 timeOrigin、后端 PID 与 activationId 不变;后端重启时 PID 与 activationId 改变,而 Vite 页面通常不整页刷新。
9. 本章实践任务
模板副本中先填写未提交草稿,再改 App.vue 的文字、CSS 与 Store action,观察 HMR;随后修改后端日志观察只重启 API。验收区分这两种行为,恢复修改后再次确认页面正常。不要在手动 dev 运行时同时执行会写同目录产物的 pack 或浏览器改写测试。
10. 常见误区
宿主当前没有 Vue Router;单个声明根路径不代表任意深链已支持;scoped CSS 不隔离所有全局样式;卸载 Vue app 不卸载浏览器 ESM;前端 HMR 不提供客户环境的生产热升级。
11. 自测题
- 共享 Vue 与独立 Pinia 为什么能同时成立?
- adapter 为什么夹在宿主路由组件与 App.vue 中间?
- Pinia HMR 与后端重启分别由哪些文件处理?
- 主页面正常但帮助组件 404 应沿哪条资源链检查?
- 多页面深链需求应该扩展宿主协议还是逐业务改宿主路由?
第 5 章:启停、失败与资源回收
1. 本章定位
能够“装起来”只完成了一半。业务插件还会注册任务、监听器和异步操作,平台必须能撤销入口并给正在运行的工作一个明确结局。
2. 学习目标
能解释 withdraw、drain、dispose、close 的顺序;能区分 DISABLED 与 RESTART_REQUIRED;能说明前后端撤销的时间差以及业务数据为何保留。
3. 前置知识
资源所有权意味着知道谁创建、谁持有句柄、何时释放。drain 是等待受管 Promise 完成,不是阻塞所有 JS;AbortSignal 是协作通知。超时不会自动终止 Promise 内部代码。
4. 对应源码
读 disableInternal、withdraw/drain、cleanup、removeUi 与 adapter cleanup。
5. 工作原理
问题是直接关闭容器可能使仍在运行的请求访问已经回收的资源。设计目标是先阻止新增工作,再等待已接受的工作结束,最后回收受管资源。SDK 包装的操作、事件、任务与存储提供可追踪的所有权边界。
6. 执行流程
7. 关键源码讲解
withdraw 同步把 stage 设为 DRAINING,清受管 timer、订阅并 abort,随后新 invoke、provider 与资源入口都过不了 ENABLED 门禁。drain 不断观察 pending,包含已接收工作继续创建的受管子任务。
如果 drain 超时,平台不继续关闭仍可能被请求使用的资源,直接要求重启。排空成功才进入 cleanup;某个 Runtime 或 SDK disposer 抛错不会阻止尝试其余清理和 context.close,最后聚合异常。清理总超时或失败同样进入 RESTART_REQUIRED。
浏览器不是与后端共享内存。宿主每 2.5 秒轮询 provider,也会在本页管理动作后主动刷新。removeUi 先离开当前业务页,删除 Map 路由,等待 UI disposer,再移除 CSS。adapter 随之 unmount app、运行页面卸载钩子并 dispose Pinia。ESM 模块缓存仍可能存在。
停用只关闭本次运行,保留安装文件与业务 JSON。再次启用同 digest 可以创建新 Context。当前进程如果已经加载过同 ID 的另一 digest,平台明确要求重启,不能称为任意版本热替换。
8. 调试与观察方法
观察 stage、pending.size、subscriptions、timers、context 和 phase;前端观察 uiRecords、pluginRoutes 和 CSS link。断点设在 withdraw、drain 循环、每个清理调用与 removeUi。停止新入口后,资源数仍非零应检查在途工作和未托管资源;不要只根据菜单消失判断后端已停用。
9. 本章实践任务
在隔离测试中让一个受管 handler 等待可控 Promise,发起请求后停用,再释放 Promise。验收先拒绝新请求,再完成旧请求并关闭;另测不释放触发 RESTART_REQUIRED。恢复通过结束测试专有进程完成,不在真实客户宿主上注入永不结束的任务。
10. 常见误区
停用不等于删除数据;close 不等于卸载代码;Promise 超时不等于底层工作被终止;SDK 无法追踪插件绕过它创建的所有资源。浏览器前端 disposer 当前没有超时治理,合作代码不返回时可能阻塞同步流程。
11. 自测题
- 为什么先撤入口再排空?
- 前后端撤销为何不是同一瞬间?
- 哪个函数决定调用 context.close?
- disposer 失败后是否仍尝试 close?drain 超时呢?
- 如何为第三方 UI disposer 增加超时与故障状态?
8. 核心模块深度分析
PluginPlatform 的状态与所有权
最重要的数据不是某个业务 Service,而是 installation 与 activation。installation 是持久的安装记录,包含 Manifest、digest、期望启用状态与 phase;activation 是当前进程中的运行实例,持有 Context、Runtime、stage、operations、待完成 Promise 和受管资源。类型入口见 PluginInstallation,内部所有者见 平台字段。
| 状态对象 | 生命周期 | 修改入口 | 容易破坏的约束 |
|---|---|---|---|
| installation | 跨宿主重启保留 | install/enable/disable/persist | 运行状态与磁盘期望状态不一定在失败时同时成功 |
| activation | 每次启用新建 | createActivation/enable/cleanup | 所有资源必须归属于这次 activationId |
| operations Map | start 暂存、校验后可见 | registerOperation/prepareOperations | STAGING 不接流量;重复 ID、未声明权限必须拒绝 |
| loadedReleases | 当前进程保留 | require 前记录 | 停用不能把它当作代码已卸载而随意清除 |
| pending | 每个受管异步工作期间 | track/drain | 必须包含子任务,完成后及时移除 |
同一个插件的安装与启停通过 exclusive() 串行,避免两次管理动作交错;同一个业务文档的 read/update 通过文档队列串行。两者保护不同对象,不是一个全局“锁住 Node”的机制。依据:SDK 存储、队列。
当前设计的优点是调用直接、部署简单、资源归属清楚,且宿主不必知道具体业务。代价是平台类承担较多职责,同进程异常与阻塞会影响所有插件,启动中的业务副作用不能整体事务回滚,模块缓存也限制升级方式。
可替代方案包括:在启动期把受信业务统一编译为普通 Nest 模块;维持当前独立制品,但把业务放到 worker/子进程或容器,通过 RPC 访问受控能力;或者只对前端扩展提供页面插件。选择取决于独立交付、隔离、调试和部署复杂度,不能仅看“动态加载”四个字。
SDK 的能力与资源模型
SDK 既缩小插件对宿主内部代码的依赖,也为 timer、订阅、存储和清理建立归属。当前 capability 只有 storage、events、notifications、timers;注册操作和订阅有阶段限制;事件名限制在 ${pluginId}. 命名空间;timer 有 busy 标记,避免自身重叠执行。见 createSdk。
SDK 被冻结可以防止插件改写这个接口对象,但不能阻止插件直接 import Node 内置模块。能力声明约束的是遵守 SDK 的调用,不构成操作系统权限控制。
文件存储的两层约束也需要分开:命名空间避免正常插件拿错其他业务 key;每 key 队列避免单进程丢更新。它不提供多个宿主进程之间的协调,也不提供跨 key 原子事务。通知与日志当前在内存,不能代替持久消息队列。
前端同步的一致性与代价
UI provider 把后端的有效性转换成浏览器能理解的资源描述。digest 绑定制品,activationId 绑定运行代次,前端用二者判断旧 UI 是否应撤销后再装载。refresh 串行化避免旧快照晚返回而覆盖新撤销结果,见 refresh。
当前协议简单,但约束也集中:一个根路径;Vue/SDK 版本兼容;CSS 为同页样式;前端 disposer 需要合作;provider 轮询存在可见延迟。扩展时应针对这些协议升级,而不是让业务直接写宿主内部 Map。
测试策略与性能观察
平台应测试门禁和失败恢复,业务应测试规则与写入,浏览器应测试实际资源装载、权限和撤销。比“某方法调用次数正确”更有意义的断言是:安装后操作不存在;启用校验失败不公开半套操作;停用后新调用拒绝;已接收工作被排空;业务数据保留;错误清理不假报成功。
【合理推断】性能敏感区包括同事件循环的 CPU 任务、JSON 文档全量读写、启用时批量 Schema 编译、过多后台任务和长时间运行的 handler。Schema 已在注册时编译,业务请求复用验证器;真正容量和资源限额仍需基准与生产负载确认。
9. 运行、测试和调试指南
环境与最小启动
使用模板需要符合其 Node engines 与 npm。宿主默认仅监听 127.0.0.1:4310,Vite 默认 5173,后端开发 HTTP 使用随机端口,Inspector 默认 9230。先确认同目录没有已有 dev 实例;不需要为预览再手动启动第二个 API 服务。
【已执行并验证】
本轮在 E:/demo/new-site/plugin-template 执行:
npm run check
结果:tsc -p tsconfig.server.json && vue-tsc -p tsconfig.web.json 成功,退出码 0。这只验证类型,不代表本轮重新跑过完整安装和启停。
本轮还执行了 rg --files、按范围源码读取与 Halo 本地 Git 元数据核对。未执行删除数据、覆盖已发布包、发布博客或部署变更。
【仓库声明但本轮未执行】
宿主首次准备和启动:
cd E:/demo/new-site
npm ci --ignore-scripts
npm run dev
模板开发:
cd E:/demo/new-site/plugin-template
npm ci
npm run dev
模板构建、测试与打包应在停止同目录 dev 后运行:
npm run build
npm test
npm run test:e2e
npm run pack
pack 需要已配置的受信 Ed25519 签名密钥。示例的私钥路径属于本机演示,不应变成第三方获取宿主私钥的生产流程。真实宿主验收会安装并启停测试插件,运行前了解目标环境与测试数据范围:
npm run test:host -- --host http://127.0.0.1:4310
【根据项目结构推断,需在目标环境确认】
仅运行某一组模板测试可使用 node:test 的筛选方式,例如:
node --test --test-name-pattern "你的测试名称" tests/template.test.mjs
测试名称以当前文件为准;该命令本轮未执行。后端调试使用仓库声明的 npm run dev:debug,配套 .vscode/launch.json。Inspector 和 source map 有历史记录;本轮未操作 IDE 断点。
最小排查路径
- 启动失败:先读 terminal,检查 Node 版本、锁文件、Vite 端口和项目锁。端口占用先找进程归属,或在副本配置不同 DEV_PORT,不盲目结束其他服务。
- 包安装失败:检查 Manifest 版本、入口、文件大小、摘要、公钥、签名与同版本不可覆盖约束。
- 启用失败:按 require → 工厂 → Context → Runtime → start → prepareOperations 顺序找第一处失败。
- 页面不见:检查 ENABLED、当前用户业务权限、provider、ESM 导出、根路径冲突和 CSS/chunk。
- API 失败:检查会话、Origin/CSRF、digest、操作 ID、权限和 Schema,再进入 Service。
- 停用异常:先看 pending,再看 disposer/close 错误;RESTART_REQUIRED 不能靠手工改状态字段消除。
宿主日志在平台内存记录与终端,不存在本文凭空假定的永久日志目录;测试日志与截图在对应 test-results 中。开发业务数据通常位于 .local/runtime/business,生产 Demo 在 .data/business。环境与签名设置见 模板说明。
10. 渐进式实战项目
实践一:运行并观察一条新增待办链
背景是验证自己理解的是运行机制。目标是用 Network 和断点找到前后端连接。涉及 Preview、Pinia、Context、Controller、Platform 与 Service。
实施时在独立副本执行仓库开发命令,创建一条带唯一标题的记录,逐步跟进 notes-create。记录 provider 的 digest/activationId、请求的 operation、后端 permission 与存储 key。测试读取与只读角色拒绝行为;验收是能够把一条响应解释到写入位置。注意开发代理窗口可能短暂 503。恢复时只删除自己创建的记录。进一步挑战是说明通知失败为何不应让成功写入变成失败响应。
实践二:增加待办标题长度提示
背景是前端已有表单与后端最大 100 字符的约束。目标是修改低风险 UI,不改变协议。修改 App.vue,增加输入长度提示或与 Schema 一致的 maxlength;保持 Store 操作名不变。
先阅读后端 Schema,再修改页面与样式,运行 check,开发中验证 HMR 与草稿保留;同时确认超限伪造请求仍由后端拒绝。验收是正常请求行为不变,提示与实际校验一致。撤销 UI diff 即可恢复。进一步挑战是减少前后端约束重复,但不要假定共享 TypeScript 类型能替代运行时校验。
实践三:定位一个操作名不匹配故障
背景是前端调用 notes-toggle、后端按 ID 注册处理器。目标是从 404 定位协议不匹配。在实验分支只把前端操作名改成一个不存在的值,不改宿主。
观察 Network,检查插件是否 ENABLED,再核对前端 operation、后端 Map 与 Runtime 注册,最后恢复正确操作名。运行 check 并验证切换状态后持久化正确。验收是能指出准确错配位置,不通过新增一个宿主专用 Controller 规避问题。恢复前端原始字节。进一步挑战是在测试中加入“未知操作被拒绝且数据不变”的断言。
实践四:独立实现一个报销管理插件
背景是证明平台能接入新业务。目标是在宿主源码不变的前提下实现一套新的后端操作与 Vue 页面。先复制模板到独立目录,初始化 ID 为 expense、名称为“报销管理”,配置 Manifest 权限与根路径 /expense。
数据模型采用 id/title/amount/status/createdAt,金额建议使用整数分避免浮点累加误差,状态先限定 draft/submitted。后端实现 expenses-list、expenses-create、expenses-submit 三个操作,在 storage.update 回调内检查“只有草稿可提交”,禁止重复提交。把新的权限写入 Manifest,并让 Runtime 注册对应操作。事件使用 expense. 命名空间;暂不设计跨插件财务事务。
前端用 App.vue 和 Pinia 展示列表、新建表单与提交按钮,通过 ctx.invoke 调用这三个操作;不要给宿主增加 expense import、菜单分支或专用接口。测试覆盖金额输入、状态转换、重复提交、只读角色和停用后的拒绝。
停止同目录 dev 后执行类型检查、测试与 pack。签名由受信发布流程完成,然后在测试宿主上传、安装、启用、创建并提交记录,停用、再启用,确认数据保留和前端撤销。验收同时核对宿主源码 diff 为零、操作与资源归属为 expense、另一插件继续可用。可能踩坑包括残留 notes-* 操作、Manifest 权限不一致、根路径冲突、开发密钥不受信与同版本覆盖。恢复使用测试宿主正常停用该插件,保留业务数据;不要为恢复实验删除客户数据。进一步挑战是增加撤回状态和兼容旧文档的迁移方案。
本实践是扩展示例设计,尚未在仓库实现。它使用已经存在的扩展契约,没有把建议功能写成已完成事实。
11. 二次开发指南
新增普通业务优先放在独立模板:Manifest 定义身份、入口、权限与能力;后端 service 管规则,Runtime 负责注册;前端 SFC/Store 管页面与请求。可以先保留示例操作名理解链路,真正发布新业务时应统一业务命名。
| 需求 | 插件自己能完成吗 | 宿主需要什么变化 |
|---|---|---|
| 新增 CRM、订单、报销 CRUD | 在现有 SDK/包大小/页面约束内可以 | 不需要登记业务名称与菜单 |
| 新表单、列表、图表、内部组件 | 可以,依赖随插件构建 | 共享依赖需遵守当前版本契约 |
| 使用现有 storage/notifications/timers | 可以,声明 capability | 无需新增具体业务接口 |
| 插件内多个视图 | 可用组件状态或 memory router | 真实 URL 深链、参数路由需要通用协议升级 |
| 真正纯前端插件 | 当前不支持完整协议 | 放宽后端入口/至少一个 operation 与独立 UI 授权规则 |
| 文件上传、数据库事务、新公共能力 | 当前 SDK 未提供的不能假装已有 | 增加版本化 SDK 能力、权限、实现和生命周期测试 |
| 陌生第三方安全执行 | 同进程机制无法提供强隔离 | 进程/容器隔离、受控 RPC 与运行治理 |
“后续只上传包”的成立条件是业务使用已经稳定的通用能力。平台能力不足时可以做一次通用升级;不应每来一个业务就给宿主增加特殊分支。
保持兼容的做法是版本化 Host API 与 SDK,优先新增可选字段和能力,保留旧操作约定,明确 Manifest 最低版本与共享依赖范围。不要让插件依赖 PluginPlatform 内部字段、宿主的 Map、业务文件绝对路径或未公开的 Nest Provider。
提交前检查:Manifest 与操作权限一致;Schema 覆盖不可信输入;资源走受管 API;前端清理监听;旧异步结果失效;产物路径和 chunk 正确;私钥与环境文件不入包;启停不改其他插件;升级与数据迁移有明确策略。业务规则测试补在插件测试,协议和清理门禁测试补在平台,实际资源与页面接入补浏览器验收。
12. 风险区域与设计取舍
| 风险 | 证据等级与依据 | 影响与取舍 |
|---|---|---|
| 同进程第三方执行 | 【已验证】require;影响为【合理推断】 | 可绕过 SDK 访问 Node 能力;CPU 阻塞、进程退出、内存泄漏会影响宿主 |
| 同页 UI 与样式 | 【已验证】import 与 CSS | 独立 app/Pinia 不隔离 DOM、同源能力或全局 CSS |
| 平台职责集中 | 【已验证】平台类 | 修改加载、授权和清理容易互相影响;后续可按职责拆内部模块但保留所有权 |
| 注册的隐式约束 | 【已验证】prepareOperations | 注册阶段、操作唯一、权限声明、至少一个操作都影响可用性 |
| 模块缓存与全局状态 | 【已验证】loadedReleases | context.close 不保证模块字节、静态引用与全部资源释放 |
| 开始执行后的回滚 | 【已验证】enable catch | 入口可撤销,已经完成的业务写入不能自动事务回滚 |
| 持久化与并发 | 【已验证】串行存储与原子替换 | 适合当前单进程文档,不能直接扩为多实例或跨文档事务 |
| 事件与通知可靠性 | 【已验证】业务发布 | 内存订阅与通知无宕机重放,可靠交付需 Outbox 等设计 |
| 授权仍为演示 | 【已验证】SessionStore | viewer 按 read 后缀派生权限;生产需独立 RBAC 存储,声明权限不等于用户获批 |
| 静态资源授权差异 | 【已验证】资源 Controller | provider 元数据按用户过滤,资源接口没有同等 SessionGuard,不能误称所有 JS 按业务权限鉴权 |
| UI disposer 长时间等待 | 【已验证】await disposer,阻塞影响为【合理推断】 | 当前没有该处超时,可使 UI 同步链停滞,需要限时和故障恢复设计 |
| 跨平台与容量 | 【待确认】本轮未做兼容/负载试验 | 不把 Windows 历史验收扩大成所有平台与任意业务规模保证 |
新手不宜首先修改验签规范化、入口路径检查、stage 门禁、pending 跟踪和清理顺序。这些位置决定第三方字节是否执行、请求是否越权以及平台是否误判停止成功。
13. 最终源码阅读清单
- Manifest:难度低;能说明入口、权限、能力与版本。
- SDK 类型与 Token:难度低;能画出插件与宿主契约。
- 工厂与 Provider:难度中;能解释 useValue/useClass。
- enable:难度中;能从文件说到 ENABLED。
- prepareOperations:难度中;能列出全部注册门禁。
- invoke:难度中;能按状态、权限、Schema、handler 定位错误。
- createSdk/storage:难度高;能解释资源归属与每 key 串行。
- plugin.ts 与 adapter:难度中;能解释共享 runtime 和独立状态。
- synchronizeUi/removeUi:难度高;能解释版本变化与页面撤销。
- dev/build/pack:难度中;能解释开发与交付两条路径。
- drain/cleanup:难度高;能说明失败为何要求重启。
- 验收与限制:难度中;能区分源码事实、历史结果与未验证内容。
14. 掌握程度检查
初级:能够使用
独立启动模板预览;找到 Manifest、Vue 入口和后端工厂;成功使用一个已有操作;解释上传格式;能区分安装与启用。可观察产出是一张自己画的模块图和一次带 Network 记录的操作说明。
中级:能够解释
不看本文,沿着真实符号讲完一次 enable、新增记录与 disable;解释三个核心对象的输入输出、固定路由与操作 Map、共享 Vue 与独立 Pinia;根据一个 403/404/409 错误定位分支。可观察产出是带文件行号的调用链与一次故障复盘。
高级:能够修改和扩展
独立完成报销或同等复杂度插件,包含状态校验、权限、前端页面、受管资源、签名包与安装启停验收;提交宿主业务源码 diff 为零的证据,说明何时必须升级平台协议。还应能解释 RESTART_REQUIRED、业务数据兼容及陌生第三方安全执行所需的额外边界。
15. 自测题参考答案
| 章节 | 概念题 | 架构题 | 定位题 | 调试题 | 设计题 |
|---|---|---|---|---|---|
| 第 1 章 | 编码传字节、摘要识别内容、签名核对受信来源与完整性 | 安装未创建 Context,不能接收业务流量 | SDK 包类型、pack 与 validatePackage | 核对公钥是否相同、签名载荷、keyId 与包修改 | 发布者身份、公钥注册/吊销、审核签名、权限和审计 |
| 第 2 章 | 定义是元数据,Context 是对象图实例 | PluginPlatform 调工厂/Nest/start 并控制发布 | 工厂 HOST_SDK useValue provider | 看 STAGING/ENABLED、prepareOperations、provider 权限与 UI import | 限制稳定 SDK 表面,避免插件耦合内部容器及越权能力 |
| 第 3 章 | 网络请求不受 TS 编译类型保护 | Controller 接 HTTP,Map 找操作,Service 执行业务 | track 与 invoke 的 pending lease | 修后端权限/输入边界,前端隐藏只是展示 | 定义事务所有权与跨业务协议,可能需要数据库/可靠事件 |
| 第 4 章 | 同一框架实现可创建不同应用和 Store 容器 | 包装组件把宿主贡献协议转换成普通 Vue 生命周期 | todos.ts 的 HMR 与 dev.mjs 的后端 watcher | 检查 Vite 相对 chunk、包清单、资源 URL 与启用状态 | 升级通用路由协议并做兼容,不逐业务改宿主 |
| 第 5 章 | 阻止新工作后才可能收敛在途集合 | 浏览器独立执行,需通过 provider 同步 | drain 成功后的 cleanup | disposer 失败仍尝试 close;drain 超时不进入关闭 | 设置限时、故障状态与恢复路径,同时承认超时不终止原代码 |
评分重点是能否给出源码位置、前置条件与失败后果,而不是记住术语。
16. 未确认事项与后续调查清单
- 本轮运行覆盖:只重新执行了类型检查;如需要确认当前环境端到端状态,应在隔离副本重新执行 build/test/e2e/pack 和真实宿主验收,保存新报告。
- 第三方治理:当前没有多发布者信任库、密钥撤销、审核任务、许可证和商业结算。需要单独明确产品流程与安全责任。
- 强隔离执行:若要求陌生第三方上传即可安全运行,应验证子进程或容器、最小权限、配额、网络与文件访问治理及 RPC 取消语义。
- 数据与升级:没有通用 schema 迁移、跨文档事务和自动业务回滚;生产数据库与新旧插件兼容需要专门设计。
- 路由与 UI:多根路由、参数深链、纯前端插件、全局组件槽位、严格样式隔离与前端清理超时都需要平台协议演进。
- 性能与可观测性:没有本轮负载数据;需要测 JSON 文档规模、长操作、后台任务压力与多插件争用,并设计持久日志和监控指标。
- 跨平台:历史主要在 Windows 与本机 Chrome 验收;Linux/macOS、容器部署和反向代理 Origin/CSRF 行为应独立验证。
- Halo 的设计演进:本文只对固定提交源码做机制对照;若要解释某次设计选择的历史原因,应进一步查对应 Git 历史、Issue 与 PR,不从文件名推断作者意图。
理解整套方案后,最有价值的第一次二次开发是:在不改变宿主业务源码的条件下,完成一个带明确状态转换、权限与清理契约的新插件,并能解释它从文件变成对象、从对象变成接口、再变成页面的每一个步骤。
