资讯动态

Angular 库与 Schematics 集成实战:为你的 Library 提供 ng add、ng generate 与 ng update 的 CLI 支持

发布时间:2026/9/8 21:35:48 来源:尧图企业网站定制
Angular 库与 Schematics 集成实战为你的 Library 提供 ng add、ng generate 与 ng update 的 CLI 支持【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular本篇指南讲解如何在 Angular 项目中为自研 Library 配套 Schematics 集合collection让第三方使用者能够通过 Angular CLI 的三个核心命令完成与库的集成ng add一键安装并初始化库、ng generate生成库内定义的业务构件如带依赖注入的服务、ng update在库升级时自动迁移破坏性变更。文章以当前仓库中的官方示例schematics-for-libraries为骨架逐文件还原集合配置、工厂函数、模板系统与构建产物组织方式读完你便能从零搭建一套可打包、可发布、可被 CLI 自动发现的 schematics 工程。为什么 Library 需要 Schematics 集合当你在 Angular 工作区中创建了一个库Library时库的使用者并不会天然获得任何 CLI 集成能力——他们只能手动安装包、手动复制代码、手动改造AppModule。Angular 提供的解法是允许把 schematics 与库一起打包发布用 schematics 描述“如何把库接入使用者的项目”。借助 schematics你可以为使用者提供三类自动化能力它们都可以注册进同一个 collection 并随库一同发布CLI 命令触发场景典型职责ng add使用者首次安装库安装最新版本包、把库的模块注册进应用根、按需写入dependenciesng generate使用者创建库内定义的构件生成已预置好依赖注入与初始化逻辑的服务、组件等文件ng update库发布含破坏性变更的新版本自动改写使用者项目中的 API 调用平滑迁移到新版本ng update类 schematics常称为 migration schematics同样登记在本篇介绍的 collection 中下方章节会以ng add与ng generate两种类型为例完整演示集合的搭建与实现。仓库中的官方配套示例位于 adev/src/content/examples/schematics-for-libraries其中projects/my-lib就是一个被 schematics 化的库工程本文所有代码片段均取自该示例。创建 Schematics 集合一个 collection 本质上是“多个命名 schematic 的注册表”。创建流程分四步这一阶段不会修改任何使用者项目文件在库根目录下创建schematics文件夹在schematics/内为第一个 schematicng-add创建ng-add子文件夹在schematics根级创建collection.json文件编辑collection.json定义集合的初始结构。示例中的初始集合文件 collection.1.json 内容如下{ $schema: ../../../node_modules/angular-devkit/schematics/collection-schema.json, schematics: { ng-add: { description: Add my library to the project., factory: ./ng-add/index#ngAdd, schema: ./ng-add/schema.json } } }逐字段解读这份注册表$schema指向 Angular Devkit 提供的 collection 结构约束文件用于在编辑器中获得补全与校验路径相对projects/my-lib所在位置解析到仓库node_modules中schematics对象描述这个集合中包含的所有命名 schematic第一条目即名为ng-add的 schematicdescription说明用途factory指向该 schematic 被执行时调用的工厂函数——格式为./路径#导出的函数名例如./ng-add/index#ngAdd表示调用ng-add/index.ts中导出的ngAdd函数schema指向声明命令行选项的 JSON Schema 文件。接着在库工程的 package.json 中加入schematics字段指向集合描述文件{ name: my-lib, version: 0.0.1, schematics: ./schematics/collection.json }Angular CLI 正是通过该字段在已安装的包内定位命名的 schematics。从angular-devkit/schematics的集合解析机制看CLI 会把包入口中schematics字段解析为 collection 文件路径再依据ng command collection:name的语法找到schematics.name下的factory与schema。注册表先建好、package.json先声明后续实现的每个 schematic 才能被 CLI 发现。提供安装支持编写 ng-add schematicng addschematic 用于增强使用者的首次安装体验。Angular CLI 会自动安装库的最新版本而你的 schematic 负责在安装后完成初始化改造。仍以官方示例的三个文件为准。1. schema.json声明命令行选项在schematics/ng-add/下创建 schema.json{ $schema: https://json-schema.org/schema, $id: SchematicsMyLibNgAdd, title: MyLib ng add Schema, type: object, properties: { project: { type: string, description: Name of the project., $default: { $source: projectName } } } }关键点在于project选项的$default声明$source设为projectName意味着当使用者在命令行没有显式传--project时CLI 会自动把“当前/默认项目名”作为该选项的默认值注入。这类$source注入机制是 Angular CLI 选项解析约定的一部分无需工厂函数自行兜底。2. schema.ts类型化接口创建 schema.ts为schema.json中的选项提供 TypeScript 类型export interface Schema { // Name of the project. project: string; }3. index.ts工厂函数创建核心文件 index.tsimport {Rule} from angular-devkit/schematics; import {addRootImport} from schematics/angular/utility; import {Schema} from ./schema; export function ngAdd(options: Schema): Rule { // Add an import MyLibModule from my-lib to the root of the users project. return addRootImport( options.project, ({code, external}) code${external(MyLibModule, my-lib)}, ); }该工厂演示了一个非常有价值的模式——借助schematics/angular/utility提供的addRootImport把模块注册写进“应用根部”addRootImport接收项目名与一个回调回调需返回一段“代码块”回调参数code是一个带标签的模板字符串函数你在其内部书写任意希望插入的代码代码中出现的外部符号必须用external函数包裹例如external(MyLibModule, my-lib)。这样框架才会为你自动生成对应的import语句这里会生成import { MyLibModule } from my-lib;并把MyLibModule挂到根模块或应用引导配置上而不会把 import 与使用位置硬编码耦合。对使用者来说ng add my-lib完成后项目不仅装上了包根模块中也被正确地接入了库声明的模块。定义依赖保存类型save 选项CLI 执行ng add时默认把包写入dependencies但库作者可以通过package.json里的ng-add字段自定义行为。示例 package.json 中配置为ng-add: { save: devDependencies }save的可选值决定了库应写入使用者的dependencies、devDependencies还是不写入package.json值行为false不把包加入package.jsontrue加入dependenciesdependencies加入dependenciesdevDependencies加入devDependencies对于仅在构建期使用的辅助库例如需要与你的主库分离发布的工具包devDependencies是常见选择而运行时依赖的主库则通常省略该字段或设为true。构建 Schematics 并打进库产物schematics 源码默认不会被ng-packagr编入库分发目录因此需要先构建库、再独立编译 schematics最后把它们一起放进dist。官方示例对应的做法需要两个前提为 schematics 单独提供一份 TypeScript 配置说明如何编译、输出到哪在库的package.json中补充构建脚本把编译产物复制进库的分发包。tsconfig.schematics.json在tsconfig.lib.json负责库构建旁新增 tsconfig.schematics.json{ compilerOptions: { baseUrl: ., lib: [es2018, dom], declaration: true, module: commonjs, moduleResolution: node, noEmitOnError: true, noFallthroughCasesInSwitch: true, noImplicitAny: true, noImplicitThis: true, noUnusedParameters: true, noUnusedLocals: true, rootDir: schematics, outDir: ../../dist/my-lib/schematics, skipDefaultLibCheck: true, skipLibCheck: true, sourceMap: true, target: es6, types: [jasmine, node] }, include: [schematics/**/*], exclude: [schematics/*/files/**/*] }其中两个选项是整份配置的灵魂选项说明rootDir声明schematics文件夹为待编译的输入根目录保证输出目录结构以schematics为顶层outDir输出到库的分发目录默认即工作区根下的dist/my-lib此处进一步落到其schematics子目录注意exclude把schematics/*/files/**/*排除在编译之外files下的模板文件带有.template后缀与 EJS 风格占位语法不是合法的 TypeScript必须原样拷贝而非编译见下文postbuild脚本。package.json 构建脚本在库工程projects/my-lib的 package.json 中补充scripts: { build: tsc -p tsconfig.schematics.json, postbuild: copyfiles schematics/*/schema.json schematics/*/files/** schematics/collection.json ../../dist/my-lib/ }build用自定义tsconfig.schematics.json把 schematics 的 TypeScript 源码工厂函数、schema 接口等编译为 CommonJS 模块并写入../../dist/my-lib/schematicspostbuildbuild成功后用copyfiles把不能/不必编译的 JSON 与模板文件按原路径复制进dist/my-lib/——包括各 schematic 的schema.json、my-service/files/**模板目录以及集合入口collection.json。脚本依赖两个 npm 包copyfiles与typescript。示例将二者以本地路径形式写入devDependencies如copyfiles: file:../../node_modules/copyfiles如果你是在独立环境下编写脚本可改为普通版本号依赖然后进入devDependencies所在的工程目录执行npm install安装后即可运行脚本。完成这一步后dist/my-lib中将同时包含库主体与可被 CLI 解析的 schematics 集合。提供生成支持编写 ng generate schematic第二种 schematic 让使用者通过ng generate直接生成库定义好的构件。官方示例假设库定义了一个需要预置配置的服务my-service期望使用者执行ng generate my-lib:my-service命令语法my-lib:my-service中冒号左侧是 collection 名即包名右侧是在集合中注册的 schematic 名。配置新的 schematic更新 collection.json编辑 collection.json为新 schematic 登记条目并指向其 schema 文件{ $schema: ../../../node_modules/angular-devkit/schematics/collection-schema.json, schematics: { ng-add: { description: Add my library to the project., factory: ./ng-add/index#ngAdd, schema: ./ng-add/schema.json }, my-service: { description: Generate a service in the project., factory: ./my-service/index#myService, schema: ./my-service/schema.json } } }schema.json在schematics/my-service/下创建 schema.json{ $schema: https://json-schema.org/schema, $id: SchematicsMyService, title: My Service Schema, type: object, properties: { name: { description: The name of the service., type: string }, path: { type: string, format: path, description: The path to create the service., visible: false, $default: { $source: workingDirectory } }, project: { type: string, description: The name of the project., $default: { $source: projectName } } }, required: [name] }顶层字段含义如下$id该 schema 在集合中的唯一 IDtitle对人类可读的 schema 描述type描述properties提供值的类型此处为对象properties定义 schematic 的全部可选选项。properties中每个选项都把“键”关联到type期望值的形状、description当使用者通过--help查询该 schematic 用法时展示的帮助文本以及可选的 alias。若需查阅更丰富的选项定制能力如$default的其它$source取值、enum、x-prompt等可参考 Angular CLI 工作区自身的 schema 约定。注意本文件顶部的required声明了name必填而未声明path/project必填——它们都有$default回退。schema.ts创建配套的 schema.tsexport interface Schema { // The name of the service. name: string; // The path to create the service. path?: string; // The name of the project. project?: string; }三个选项在工厂中的职责选项说明name希望创建的服务的名称必填模板将据此生成类名与文件名path覆盖 schematic 的目标路径未提供时默认取当前工作目录workingDirectoryproject指定要在哪个项目上运行该 schematic未提供时在 schematic 内部可以依据默认规则推导添加模板文件要让 schematic 在项目中产出真实文件需要准备自己的模板。Schematics 模板支持在文件路径与文件内容两处执行占位符替换与代码拼接。在schematics/my-service/内创建files/文件夹创建名为__namedasherize__.service.ts.template的文件——注意.template后缀会在最终产物中被剥除而文件名本身由“name 选项的 dasherize 形式”决定。示例模板 __namedasherize__.service.ts.template 会生成一个已经把HttpClient注入到http属性的服务import { Injectable } from angular/core; import { HttpClient } from angular/common/http; Injectable({ providedIn: root }) export class % classify(name) %Service { private http inject(HttpClient); }模板语法解析% classify(name) %在内容中执行表达式并输出结果——classify(name)会把 name 转为标题式类名。若传入my-data这里渲染为MyData拼上Service后类名即MyDataService文件名中的__namedasherize__路径模板占位符dasherize是路径用字符串转换器把 name 转为短横线小写形式。若 name 为my-data或MyData文件最终名为my-data.service.ts。classify、dasherize是 schematics 框架提供的字符串工具函数angular-devkit/core的strings命名空间中同名导出而name则由工厂函数作为模板数据属性注入——它正是你在 schema 中定义、由命令行传入的同一个name。添加工厂函数空规则起步生成类 schematic 的核心是工厂函数。Schematics 框架提供了一套文件模板系统同时支持路径模板与内容模板系统作用于输入Tree中加载的文件/路径内的占位符并用传入Rule的值完成填充。官方示例从空工厂起步见 index.1.tsimport {Rule, Tree} from angular-devkit/schematics; import {Schema as MyServiceSchema} from ./schema; export function myService(options: MyServiceSchema): Rule { return (tree: Tree) tree; }这个工厂直接原样返回Tree不做任何修改options正是从ng generate命令透传进来的选项值。接下来要做的是把占位工厂替换为真正改写用户项目的逻辑。定义生成规则解析项目并渲染模板使用者安装库的 Angular 工作区通常包含多个项目应用与库并存。使用者可以在命令行指定--project也可以缺省无论哪种情形工厂内部都必须解析出“当前 schematic 作用于哪个项目”才能从项目配置中取回信息。这依赖传入工厂函数的Tree对象——Tree的方法暴露了工作区完整文件树允许 schematic 执行期间读写任意文件。解析工作区配置要确定目标项目使用workspaces.readWorkspace读取工作区配置文件angular.json而它需要一个从Tree构建的workspaces.WorkspaceHost。示例 index.ts 先实现了一个薄封装 hostimport { Rule, Tree, SchematicsException, apply, url, applyTemplates, move, chain, mergeWith, } from angular-devkit/schematics; import {strings, normalize, virtualFs, workspaces} from angular-devkit/core; import {Schema as MyServiceSchema} from ./schema; function createHost(tree: Tree): workspaces.WorkspaceHost { return { async readFile(path: string): Promisestring { const data tree.read(path); if (!data) { throw new SchematicsException(File not found.); } return virtualFs.fileBufferToString(data); }, async writeFile(path: string, data: string): Promisevoid { return tree.overwrite(path, data); }, async isDirectory(path: string): Promiseboolean { return !tree.exists(path) tree.getDir(path).subfiles.length 0; }, async isFile(path: string): Promiseboolean { return tree.exists(path); }, }; }WorkspaceHost把 Tree 的文件访问能力读/写/判目录/判文件适配为workspaces模块可用的接口。随后在工厂内解析工作区并核对项目名export function myService(options: MyServiceSchema): Rule { return async (tree: Tree) { const host createHost(tree); const {workspace} await workspaces.readWorkspace(/, host); const project options.project ! null ? workspace.projects.get(options.project) : null; if (!project) { throw new SchematicsException(Invalid project name: ${options.project}); } const projectType project.extensions.projectType application ? app : lib; if (options.path undefined) { options.path ${project.sourceRoot}/${projectType}; } // ...模板规则见下一节 }; }几个关键判断workspace.projects持有所有项目粒度的配置信息务必校验项目存在project为 null 时抛出SchematicsException携带“无效项目名”信息避免后续在空值上取属性project.extensions.projectType区分application与library代码据此把目标目录后缀定为app或liboptions.path决定模板文件最终被移动到哪。schema 中path的$default是当前工作目录但实际工作区里更稳妥的是未显式提供path时取项目配置里的sourceRoot拼上projectType如src/app或projects/xxx/src/lib这正是示例所做的回退逻辑。用 apply/url/applyTemplates/move 渲染模板一条Rule可以读取外部模板文件、做变换再返回携带变换结果的新Rule。示例中把“读取模板 → 注入模板数据 → 移动到目标目录”三步用apply串起来const templateSource apply(url(./files), [ applyTemplates({ classify: strings.classify, dasherize: strings.dasherize, name: options.name, }), move(normalize(options.path as string)), ]); return chain([mergeWith(templateSource)]);各工具函数的作用与机制函数说明url()从文件系统读取源文件路径相对当前 schematic此处读取同目录的./filesapply()接收两个参数一个 source、一组 rules把多条规则依次应用到 source 上并返回变换后的 sourceapplyTemplates()接收“希望暴露给模板与模板文件名使用的方法/属性”对象返回一条Rule。正是在这里注入classify()、dasherize()与name属性classify()把值转为标题式Title Case如my service→MyServicedasherize()把值转为小写短横线形式如MyService→my-servicemove()在 schematic 应用时把 source 文件移动到目标位置chain()把多条规则合并为一条规则使单个 schematic 内可顺序执行多个操作mergeWith()把变换好的 source 合并进当前Tree真正把生成的文件落盘上述流程的职责划分清晰url(./files)负责把模板载入内存 sourceapplyTemplates({classify, dasherize, name})用真实数据替换文件名与内容中的占位符这就是文档所说的 path template 与 content template 两类模板系统的统一入口move(...)把渲染结果定向到目标目录最后chain把模板合并规则与其它待执行逻辑收拢成最终规则返回给 CLI。注意模板文件位于files/目录但使用了move()指向options.path因此生成的my-data.service.ts会落在解析出的src/app之类的源码目录而不会连同files前缀一起出现。完整工厂函数一览将上述片段拼合即得到完整实现可直接对照仓库文件 index.ts 阅读export function myService(options: MyServiceSchema): Rule { return async (tree: Tree) { const host createHost(tree); const {workspace} await workspaces.readWorkspace(/, host); const project options.project ! null ? workspace.projects.get(options.project) : null; if (!project) { throw new SchematicsException(Invalid project name: ${options.project}); } const projectType project.extensions.projectType application ? app : lib; if (options.path undefined) { options.path ${project.sourceRoot}/${projectType}; } const templateSource apply(url(./files), [ applyTemplates({ classify: strings.classify, dasherize: strings.dasherize, name: options.name, }), move(normalize(options.path as string)), ]); return chain([mergeWith(templateSource)]); }; }运行你的库 schematic构建与验证的完整链路如下。构建库与 schematics在作为“已安装该库”的使用方工作区根目录先构建库本体ng build my-lib随后进入库目录运行刚才定义的 schematics 构建脚本cd projects/my-lib npm run buildnpm run build触发tsc -p tsconfig.schematics.json继而自动执行postbuild的copyfiles把集合与模板送入分发目录。执行顺序有讲究schematics 必须晚于库构建才能被放进dist/my-lib的正确位置。链接库到 node_modules库与 schematics 打好包后位于工作区根的dist/my-lib。为了让 CLI 能按包名解析到它需要在当前使用的工程里把它链接进node_modules。在工程根执行npm link dist/my-lib若你在另一个独立消费工程中验证也可在该工程内执行npm link my-lib要点是让 node 解析到带schematics字段的、已打包好的包目录。链接完成后CLI 即可通过包入口发现collection.json。运行 schematic 并核对产物现在用注册名运行刚才实现的生成类 schematicng generate my-lib:my-service --name my-dataCLI 输出中会出现文件创建记录例如CREATE src/app/my-data.service.ts (208 bytes)核对生成文件 my-data.service.ts 的预期内容my-data经classify得MyData类名为MyDataService文件按 dasherize 规则落为my-data.service.ts类中已含HttpClient的注入语句——无需使用者手写任何样板代码。如果输出中出现Nothing to be done或找不到 collection 的错误请依次检查package.json的schematics字段是否指向存在的collection.json、dist中是否真的复制了 schema 与模板文件、ng-add/my-service的factory路径与导出函数名是否与源码一致。小结为 Library 编写 schematics 集合本质上是在做三件事注册collection.json package.json 声明、实现schema 描述选项 工厂函数改写 Tree、打包分发tsconfig.schematics.json build/postbuild 脚本。本文以仓库官方示例schematics-for-libraries完整演示了ng add用addRootImport把模块挂到应用根与ng generate用模板系统产出带依赖注入的服务两条链路同一集合中再登记带package.json/migrations等约定的 migration schematics即可让ng update在版本升级时自动执行破坏性变更迁移。把三者做齐你的库就能像 Angular 官方与主流生态库一样被使用者在三条 CLI 命令内完成安装、使用与升级。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价