因工做繁忙,差很少有三个月没有写过技术文章了,自八月份第一次编写 schematics 以来,我一直打算分享关于 schematics 的编写技巧,无奈仍是拖到了年末。css
Angular Schematics 是很是强大的一个功能,能够快速初始化项目,也能够自定义组件模板。在去年 schematics 发布以来,已经有部分开发者在项目中尝试使用,可是学习资料仍是比较匮乏。目前官网已经有了 schematics 的简易教程,但在实际开发中仅靠官方教程仍是会遇到不少问题。在开发 Ng-Matero 的过程当中,编写 schematics 就像闯关同样,从 ng add
到 ng generate
再到 ng update
,每一个部分都耗费了博主大量的精力,翻阅了无数源码才得以实现。html
在这个系列文章中,我将以 Ng-Matero 为例讲解 schematics 开发过程当中遇到的难点,梳理开发流程,帮助你们开发自定义的 schematics 生成器。node
该系列文章的三部分将分别介绍 Add、Generation 以及 Update,即便分了三部分来说解 schematics,但我相信依然没法介绍的面面俱到。那遇到问题应该怎么办呢?没错,你须要看源码,这听起来可能让人心生畏惧,可是不用紧张,阅读源码并无你想象的那么困难。顺便说一下,不管编写组件库仍是 schematics,Angular Material
的源码都是最好的教材。git
在继续阅读文章以前,请务必将官网的 Schematics 教程撸一遍,有关方法的说明能够参考 Schematics 的 README 。github
在我目前见过的项目中,ng add
主要有两个用途:shell
初始化组件库相对简单一点,有些库的 ng add
甚至等同于 npm install
。npm
相比之下,初始化项目模板要复杂不少,不只要对项目进行配置,还要对项目中的文件进行增删改等操做。json
本文将以初始化项目模板为例介绍 ng add
的执行过程。gulp
假设你的根目录有一个 schematics 的文件夹。bootstrap
在官网的教程中,已经列出了 schematics 目录的两种风格:
一、你能够在 schematics 文件夹中单独安装 node_modules
,这样你在 package.json
中定义 scripts 的时候逻辑会比较清晰,可是整个项目会有两套 node_modules
,而大部分依赖都和根目录重复;
{
"scripts": {
"build": "tsc -p tsconfig.json"
},
}
复制代码
二、另外也能够复用根目录的 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。
由于 schemaics 目录也是一个项目目录,因此你能够在 schematics 的 package.json
中定义拷贝命令,和官网教程是同样的,可是更恰当的方式应该是将复制命令写在根目录的 package.json
中。
{
"scripts": {
"build:schematics": "npm run copy:schematics && cd schematics && npm run build && cd .. && npm run build:starter",
"build:starter": "gulp --gulpfile gulpfile.js",
"copy:schematics": "npm run clean:schematics && cpr schematics dist/schematics",
"clean:schematics": "rimraf dist/schematics",
}
}
复制代码
如今咱们能够开始 ng add 的编写了,简单梳理一下,若是要使用 schematics 添加项目文件,咱们须要作什么?
如下是 @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 自主群。