Angular Schematics 三部曲之 Add
前言
因工作繁忙,差不多有三个月没有写过技术文章了,自八月份第一次编写 schematics 以来,我一直打算分享关于 schematics 的编写技巧,无奈还是拖到了年底。
Angular Schematics 是非常强大的一个功能,可以快速初始化项目,也可以自定义组件模板。在去年 schematics 发布以来,已经有部分开发者在项目中尝试使用,但是学习资料还是比较匮乏。目前官网已经有了 schematics 的简易教程,但在实际开发中仅靠官方教程还是会遇到很多问题。在开发 Ng-Matero 的过程中,编写 schematics 就像闯关一样,从
ng add到
ng generate再到
ng update,每个部分都耗费了博主大量的精力,翻阅了无数源码才得以实现。
在这个系列文章中,我将以 Ng-Matero 为例讲解 schematics 开发过程中遇到的难点,梳理开发流程,帮助大家开发自定义的 schematics 生成器。
该系列文章的三部分将分别介绍 Add、Generation 以及 Update,即使分了三部分来讲解 schematics,但我相信依然无法介绍的面面俱到。那遇到问题应该怎么办呢?没错,你需要看源码,这听起来可能让人心生畏惧,但是不用紧张,阅读源码并没有你想象的那么困难。顺便说一下,无论编写组件库还是 schematics,
Angular Material的源码都是最好的教材。
在继续阅读文章之前,请务必将官网的 Schematics 教程撸一遍,有关方法的说明可以参考 Schematics 的 README。
Add 的用途
在我目前见过的项目中,
ng add主要有两个用途:
- 初始化组件库(比如 angular material,ng-zorro,ngx-bootstrap)
- 初始化项目模板(比如 ng-alain,ng-matero)。
初始化组件库相对简单一点,有些库的
ng add甚至等同于
npm install。
相比之下,初始化项目模板要复杂很多,不仅要对项目进行配置,还要对项目中的文件进行增删改等操作。
本文将以初始化项目模板为例介绍
ng add的执行过程。
Schematics 目录
假设你的根目录有一个 schematics 的文件夹。
在官网的教程中,已经列出了 schematics 目录的两种风格:
1、你可以在 schematics 文件夹中单独安装
node_modules,这样你在
package.json中定义 scripts 的时候逻辑会比较清晰,但是整个项目会有两套
node_modules,而大部分依赖都和根目录重复;
{ "scripts": { "build": "tsc -p tsconfig.json" }, }
2、另外也可以复用根目录的
node_modules,这样的话就会减少不必要的安装了
{ "scripts": { "build": "../node_modules/.bin/tsc -p tsconfig.json" }, }
使用 Angular CLI 来创建项目的话一般来说就是第一种情况,比如创建一个库或者创建一个 schematics,核心文件都会放在 src 目录。
注意:使用 Angular CLI 的默认目录对于 Generation 命令比较友好,Angular CLI 添加的默认路径为
src/app或者
src/lib等,如果我们修改了默认目录,则在使用
ng generate命令时需要显式的设置
--path参数。
发布 Schematics
因为 schematics 就是一套执行脚本,所以在项目发布之前需要将 schematics 的编译文件复制到项目目录,否则也无法使用 schematics。
- 如果你开发的是一套组件库,那么你需要将 schematics 编译的文件拷贝到组件库中一起发布;
- 如果你开发的是一个项目模板,那么只需要发布 schematics 就可以了。
因为 schemaics 目录也是一个项目目录,所以你可以在 schematics 的
package.json中定义拷贝命令,和官网教程是一样的,但是更恰当的方式应该是将复制命令写在根目录的
package.json中。
{ "scripts": { "build:starter": "gulp --gulpfile gulpfile.js", "build:schematics": "npm run copy:schematics && cd schematics && npm run build && cd .. && npm run build:starter", "copy:schematics": "npm run clean:schematics && cpr schematics dist/schematics", "clean:schematics": "rimraf dist/schematics", } }
添加 ng add
现在我们可以开始 ng add 的编写了,简单梳理一下,如果要使用 schematics 添加项目文件,我们需要做什么?
- 初始化项目的原始模板文件
- 删除 ng new 生成的重复文件(因为 schematic 无法自动替换文件)
- 把原始项目模板文件拷贝到项目目录
- 调整一下 package.json 和 angular.json
- 添加一些额外的 module
- 执行 npm install 安装 package
以下是
@angular/material的
ng add逻辑,
ng-matero与此类似。
初始化安装
在 schematics 中,我们可以通过
NodePackageInstallTask方法安装 package
export default function(options: any): Rule { return (host: Tree, context: SchematicContext) => { // Add CDK first! addKeyPkgsToPackageJson(host); // Since the Angular Material schematics depend on the schematic utility functions from the // CDK, we need to install the CDK before loading the schematic files that import from the CDK. const installTaskId = context.addTask(new NodePackageInstallTask()); context.addTask(new RunSchematicTask('ng-add-setup-project', options), [installTaskId]); return host; }; }
初始化的过程是先将依赖包添加到 package.json 中,然后执行
npm install,以上代码实际执行了两次
npm install,在执行 Add 主逻辑之前,首先安装了 cdk,parse5 等依赖包。
除了在代码中安装依赖以外,也可以在 schematics 的 package.json 中定义 cdk、parse5,只要保证在执行 Add 主逻辑的时候已经安装了上述包即可,但是这种方式过于死板,在 package.json 中更新依赖包的版本号有些繁琐。
更新文件
在执行
ng add拷贝项目模板的时候,会有一些需要更新的文件,但是 schematics 没有办法直接替换这些文件,所以必须先删除再拷贝,如果没有提前删除重复的文件,则会报错终止。
以下是安装 Ng-Matero 时对
ng new生成的项目文件进行删除的方法。
/** delete exsiting files to be overwrite */ function deleteExsitingFiles() { return (host: Tree) => { const workspace = getWorkspace(host); const project = getProjectFromWorkspace(workspace); [ `${project.root}/tsconfig.app.json`, `${project.root}/tsconfig.json`, `${project.root}/tslint.json`, `${project.sourceRoot}/app/app-routing.module.ts`, `${project.sourceRoot}/app/app.module.ts`, `${project.sourceRoot}/app/app.component.spec.ts`, `${project.sourceRoot}/app/app.component.ts`, `${project.sourceRoot}/app/app.component.html`, `${project.sourceRoot}/app/app.component.scss`, `${project.sourceRoot}/environments/environment.prod.ts`, `${project.sourceRoot}/environments/environment.ts`, `${project.sourceRoot}/main.ts`, `${project.sourceRoot}/styles.scss`, ] .filter(p => host.exists(p)) .forEach(p => host.delete(p)); }; }
注意:在删除文件时先要遍历文件确定目录中有该文件再删除,否则同样会报错终止。
拷贝文件
在执行完一系列规则之后,最终需要将
files文件夹中的文件复制到项目目录,直接拷贝整个文件夹就可以,方法如下:
/** Add starter files to root */ function addStarterFiles(options: Schema) { return chain([ mergeWith( apply(url('./files'), [ template({ ...strings, ...options, }), ]) ), ]); }
在拷贝完成之后,命令行会列出文件的创建、更新等信息。
关于
chain
mergeWith
apply
template等方法的使用详见 Schematics 的 README,不过 Schematics 的 README 上面的方法并不全,很多方法还是需要参考
@angular/material以及其它库的使用方式。
简单说一下
template和
applyTemplates的不同之处:
template
作用于原始文件applyTemplates
作用于后缀名为.template
的文件。
添加
.template后缀的文件可以避免 VS Code 报错。
schematics 中的
files模板文件是从 Ng-Matero 项目中拷贝的,拷贝方式有多种,可以通过 shell 命令,也可以通过 gulp,这取决于你的喜好。
文件修改
JSON 文件的修改非常简单,比如在
angular.json中添加 hmr 的设置。
/** Add hmr to angular.json */ function addHmrToAngularJson() { return (host: Tree) => { const workspace = getWorkspace(host); const ngJson = Object.assign(workspace); const project = ngJson.projects[ngJson.defaultProject]; // build project.architect.build.configurations.hmr = { fileReplacements: [ { replace: `${project.sourceRoot}/environments/environment.ts`, with: `${project.sourceRoot}/environments/environment.hmr.ts`, }, ], }; // serve project.architect.serve.configurations.hmr = { hmr: true, browserTarget: `${workspace.defaultProject}:build:hmr`, }; host.overwrite('angular.json', JSON.stringify(ngJson, null, 2)); }; }
对于 JSON 文件的修改主要用到的就是
overwrite方法。而对于非 JSON 文件的修改,相对麻烦一点,比如添加 hammer.js 的声明:
/** Adds HammerJS to the main file of the specified Angular CLI project. */ export function addHammerJsToMain(options: Schema): Rule { return (host: Tree) => { const workspace = getWorkspace(host); const project = getProjectFromWorkspace(workspace, options.project); const mainFile = getProjectMainFile(project); const recorder = host.beginUpdate(mainFile); const buffer = host.read(mainFile); if (!buffer) { return console.error( `Could not read the project main file (${mainFile}). Please manually ` + `import HammerJS in your main TypeScript file.` ); } const fileContent = buffer.toString('utf8'); if (fileContent.includes(hammerjsImportStatement)) { return console.log(`HammerJS is already imported in the project main file (${mainFile}).`); } recorder.insertRight(0, `${hammerjsImportStatement}\n`); host.commitUpdate(recorder); }; }
关于
host.beginUpdate、
recorder.insertRight、
host.commitUpdate这几个方法,可以看一下 angular cli 的源码。
除了上述提到的方法之外,在修改文件的时候,还可能用到
AST,需要更精细的操作代码文件,我会在 Generation 部分重点讲解。
调试
在编写 schematics 的时候,调试很重要,简单说一下关于调试的问题以及技巧。
编写完 schematics 之后,我们需要通过 npm link 进行测试。假设我们已经在项目的根目录创建了一个测试项目。npm link 其实就是将打包目录的快捷方式拷贝到
node_modules中。
ng add的测试比较麻烦,如果将模板安装到项目之后,再次测试需要重新初始化一个 ng 项目。另外,切记在 npm link 之后,执行
ng add之前,先删除
package-lock.json文件,否则 npm link 的项目会被更新删除。
有时为了更方便的测试,可能需要直接更改
node_modules中的源代码,其实编译后的代码并非难以辨认,和原始文件差别并不是很大。这些问题也会在 Generation 部分重点讲解。
总结
在最开始写 Ng-Matero 这个项目的时候,我一直觉得 schematics 是最关键的组成部分。为了让 Ng-Matero 不仅仅只是一个模板项目,我耗费了大量精力实现了一套比较简单的 schematics,这让我多少感到欣慰,也希望大家在使用 Schematics 时候可以提出更多宝贵意见。
本文拖沓了很久,但是依然比较表浅,如果大家有什么问题,欢迎留言评论,或者加入 Ng-Matero 自主群。
- Attempting to add QLayout "" to MainWindow "", which already has a layout 给QWidget或者QDialog设置布局的时候方
- Unable to add window -- token null is not valid; is your activity running?错误及其修改方法
- XXX Params.addRule(); 该方法不能用的问题
- Add to List 542. 01 Matrix
- ModelState.AddModelError使用
- Calendar类中add/set/roll方法的区别
- 在 sys.servers 中找不到服务器,请执行存储过程 sp_addlinkedserver 以将服务器添
- phonegap platform add ios 出错的问题
- 内存泄露从入门到精通三部曲之基础知识篇
- leetcode练习 Add Two Numbers
- Leetcode: Add Two Numbers
- DateAdd 时间差函数
- Runtime.addShutdownHook用法
- listView.addHeaderView(viewpage)与listview有冲突的解决办法
- SVN中取消add
- 【Linux】多线程无锁编程--原子计数操作:__sync_fetch_and_add等12个操作
- SVN中正确的add操作和delete操作
- leecode_241 Different Ways to Add Parentheses
- Add-AppxProvisionedPackage
- DateTime.Now.Date.AddDays(int a)