前言

因工作繁忙,差不多有三个月没有写过技术文章了,自八月份第一次编写 schematics 以来,我一直打算分享关于 schematics 的编写技巧,无奈还是拖到了年底。

Angular Schematics 是非常强大的一个功能,可以快速初始化项目,也可以自定义组件模板。在去年 schematics 发布以来,已经有部分开发者在项目中尝试使用,但是学习资料还是比较匮乏。目前官网已经有了 schematics 的简易教程,但在实际开发中仅靠官方教程还是会遇到很多问题。在开发 Ng-Matero 的过程中,编写 schematics 就像闯关一样,从 ng addng 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 就可以了。

因为 schematics 目录也是一个项目目录,所以你可以在 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

现在我们可以开始 ng add 的编写了,简单梳理一下,如果要使用 schematics 添加项目文件,我们需要做什么?

  • 初始化项目代码(提供模板配置项等)
  • 删除 ng new 生成的重复文件(因为 schematic 无法自动替换文件)
  • 把原始项目模板文件拷贝到项目目录
  • 调整一下 package.json 和 angular.json
  • 添加一些额外的 module
  • 执行 npm install 安装 package

以下是 @angular/materialng 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 以及其它库的使用方式。

简单说一下 templateapplyTemplates 的不同之处:

  • 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.beginUpdaterecorder.insertRighthost.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 自主群。

Angular Schematics 三部曲之 Add的更多相关文章

  1. 创建自定义的 Angular Schematics

    本文对 Angular Schematics 进行了介绍,并创建了一个用于创建自定义 Component 的 Schematics ,然后在 Angular 项目中以它为模板演练了通过 Schemat ...

  2. 在库中使用schematics——ng add与ng update

    起步 创建一个angular库 ng new demo --create-application=false ng g library my-lib 可见如下目录结构 ├── node_modules ...

  3. Angular v6 正式发布

    Angular 6 正式发布 Angular 6 已经正式发布了!这个主要版本并不关注于底层的框架,更多地关注于工具链,以及使 Angular 在未来更容易快速推进. 作为发布的一部分,我们同步了主要 ...

  4. [Angular2 Router] Load Data Based on Angular 2 Route Params

    You can load resource based on the url using the a combination of ActivatedRouteand Angular 2’s Http ...

  5. Angular 2.0 从0到1 (五)

    第一节:Angular 2.0 从0到1 (一)第二节:Angular 2.0 从0到1 (二)第三节:Angular 2.0 从0到1 (三)第四节:Angular 2.0 从0到1 (四)第五节: ...

  6. angular自动化测试--protractor

    前戏 面向模型编程: 测试驱动开发: 先保障交互逻辑,再调整细节.---by 雪狼. 为什么要自动化测试? 1,提高产出质量. 2,减少重构时的痛.反正我最近重构多了,痛苦经历多了. 3,便于新人接手 ...

  7. angular 4 实现的tab栏切换

    管理系统 tab 切换页,是一种常见的需求,大概如下: 点击左边菜单,右边显示相应的选项卡,然后不同的选项卡面可以同时编辑,切换时信息不掉失! 用php或.net,java的开发技术,大概是切换显示, ...

  8. 使用angular4和asp.net core 2 web api做个练习项目(二), 这部分都是angular

    上一篇: http://www.cnblogs.com/cgzl/p/7755801.html 完成client.service.ts: import { Injectable } from '@an ...

  9. Angular 4+ Http

    HTTP: 使应用能够对远端服务器发起相应的Http调用: 你要知道: HttpModule并不是Angular的核心模块,它是Angualr用来进行Web访问的一种可选方式,并位于一个名叫@angu ...

随机推荐

  1. Bert系列(二)——源码解读之模型主体

    本篇文章主要是解读模型主体代码modeling.py.在阅读这篇文章之前希望读者们对bert的相关理论有一定的了解,尤其是transformer的结构原理,网上的资料很多,本文内容对原理部分就不做过多 ...

  2. python中'0b111'中的b 是什么意思

    https://zhidao.baidu.com/question/987330764742072579.html binary,二进制的意思

  3. Android Studio(十二):打包多个发布渠道的apk文件

    Android Studio相关博客: Android Studio(一):介绍.安装.配置 Android Studio(二):快捷键设置.插件安装 Android Studio(三):设置Andr ...

  4. Porject Euler Problem 6-Sum square difference

    我的做法就是暴力,1+...+n 用前n项和公式就行 1^2+2^2+....+n^2就暴力了 做完后在讨论版发现两个有趣的东西. 一个是 (1+2+3+...+n)^2=(1^3)+(2^3)+(3 ...

  5. python深浅copy和赋值

    Python直接赋值,浅copy和深copy的比较 基于引用和对象(python引用和对象分离) 总结: 直接赋值:a = b -->a,b两个引用指向相同的对象 浅copy:a为b的copy ...

  6. SELinux: Could not downgrade policy file

    在配置nfs服务器,设定selinux时,碰到了SELinux: Could not downgrade policy file的错误提示,下文是其解决方案. 一.故障现象 [root@system1 ...

  7. H3C NAT组网和常用术语

  8. 关于Spring JavaWeb工程中的ContextRefreshedEvent事件

    在应用启动时,通常想在此时预加载一些资源,全局使用. Spring会在操作应用上下文时,使用ApplicationEventPublisher触发相关ApplicationContextEvent,我 ...

  9. vue-element Tree树形控件通过id默认选中

    一.设置 1.给树形控件设置 ref="tree" node-key="id" 2.在获取数据的位置加上 this.$nextTick(() => { t ...

  10. 2019-8-31-C#-获取-PC-序列号

    title author date CreateTime categories C# 获取 PC 序列号 lindexi 2019-08-31 16:55:58 +0800 2018-7-30 10: ...