如何使用Angular实现企业基础产品的插件式客户定制开发
Angular 实现类NestJS插件化定制能力的自定义页面加载方案
整体逻辑和之前NestJS端做的Plugin目录自动加载逻辑完全对齐,支持同仓库迭代通用基础模块、独立仓库维护客户定制代码两种模式,同时打通动态侧边栏配置,核心解决自定义页面的TS/HTML/SCSS三类资源加载问题,落地分两种场景:
方案一:同仓库插件目录模式(和后端Plugin机制完全匹配)
这个方案适合定制插件和基础代码放在同一代码库维护的场景,没有运行时额外开销,开发体验和写普通Angular页面完全一致。
- 统一目录约定
在项目根目录创建和后端对齐的plugins/文件夹,每个客户对应一个独立子目录,内部按Angular开发规范放页面、组件、路由定义,结构示例:src/ # 所有客户通用的基础版代码 plugins/ customer-a/ pages/ custom-order/ custom-order.component.ts custom-order.component.html custom-order.component.scss customer-a.module.ts routes.ts # 定义该客户的定制页面路由 customer-b/ # 其他客户的定制内容 - 自动扫描注册
借助@angular-builders/custom-webpack扩展Angular构建流程,加一个简单的扫描脚本:本地启动、生产构建时自动遍历plugins/目录下所有符合约定的routes.ts文件,把路由配置合并到主应用的根路由表中,不需要在基础版代码里手动引入任何定制模块。
构建时通过环境变量传入当前部署的客户标识,只会把对应客户的定制代码打进生产包,不会混入其他客户的逻辑。 - 侧边栏对接
首次加载拉取到的侧边栏JSON配置,只需要保证菜单项的path字段和插件里定义的路由path一致,点击菜单就能正常跳转,所有TS/HTML/SCSS资源都会在构建阶段和主应用一起编译,不需要运行时做额外处理。
方案二:独立仓库插件模式(定制代码单独维护)
这个方案对应后端把定制插件放在独立仓库维护的场景,基础版主包不需要提前感知任何定制逻辑,客户部署时只需要把对应插件资源放到指定目录,主应用启动时自动加载。
- 插件打包约定
独立仓库的定制代码优先用ng-packagr打包成符合Angular包规范的UMD格式,@angular/core、@angular/common这类公共依赖不要打进插件包,运行时直接复用主应用的依赖即可,包体积最小。
如果要直接放未编译的TS/HTML/SCSS源文件,需要在主应用里引入@angular/compiler运行时编译器,生产构建时调整angular.json配置避免编译器被摇树优化掉,注意这种模式会增加首屏包体积,非必要不选。 - 运行时加载逻辑
主应用拿到侧边栏JSON配置后,先处理所有标记为「自定义插件」的菜单项,完成路由注册后再渲染侧边栏,避免点击菜单时路由未就绪:- 根据菜单项里配置的插件资源地址,并行拉取对应的JS(编译后的TS逻辑)、HTML模板、CSS(提前编译好的SCSS产物)
- 如果是预编译好的Angular模块,直接调用动态
import()加载模块,提取模块内的路由配置合并到主路由表 - 如果是未编译的源文件,用运行时编译器动态创建组件、模块,编译完成后注册路由
核心逻辑参考代码:
import { Router } from '@angular/router'; import { Compiler, Component, NgModule, ViewEncapsulation } from '@angular/core'; async function registerCustomRoute(menuConf, router: Router, compiler: Compiler) { // 并行拉取三类资源 const [template, styles, compModule] = await Promise.all([ fetch(menuConf.resource.html).then(res => res.text()), fetch(menuConf.resource.css).then(res => res.text()), import(/* webpackIgnore: true */ menuConf.resource.js) ]); // 动态生成组件定义 const DynamicComp = Component({ template, styles: [styles], encapsulation: ViewEncapsulation.ShadowDom // 开启样式隔离,避免污染全局 })(compModule.default); // 动态生成临时模块 const DynamicModule = NgModule({ declarations: [DynamicComp] })(class {}); // 编译模块后追加到路由表 await compiler.compileModuleAsync(DynamicModule); const newRoutes = [...router.config, { path: menuConf.path, component: DynamicComp }]; router.resetConfig(newRoutes); } - 样式处理
自定义页面的SCSS建议在插件侧提前编译成CSS,不要在浏览器端运行SCSS编译,会拖慢加载速度;同时开启Angular的ShadowDom视图封装,避免定制样式污染基础版全局样式。
选型建议
优先选第一种同仓库插件目录的方案,和现有NestJS后端的插件维护逻辑完全对齐,团队学习成本最低,没有运行时编译的性能开销,后续迭代通用功能、定制功能的流程和现有开发流程几乎没有差异。只有在定制代码必须和主仓库完全物理隔离、独立发版的场景下,再选第二种独立仓库运行时加载的方案。
内容的提问来源于stack exchange,提问作者Elle96
相关产品推荐
相关产品推荐

