口水蛋仔 發表於 2020-1-7 21:36:00

Angular Schematics 三部曲之 Add

<p><img src="https://img2018.cnblogs.com/blog/999445/202001/999445-20200107202421169-318997450.jpg" alt="" loading="lazy"></p>
<h2 id="前言">前言</h2>
<p>因工作繁忙,差不多有三个月没有写过技术文章了,自八月份第一次编写 schematics 以来,我一直打算分享关于 schematics 的编写技巧,无奈还是拖到了年底。</p>
<p>Angular Schematics 是非常强大的一个功能,可以快速初始化项目,也可以自定义组件模板。在去年 schematics 发布以来,已经有部分开发者在项目中尝试使用,但是学习资料还是比较匮乏。目前官网已经有了 schematics 的简易教程,但在实际开发中仅靠官方教程还是会遇到很多问题。在开发Ng-Matero 的过程中,编写 schematics 就像闯关一样,从 <code>ng add</code> 到 <code>ng generate</code> 再到 <code>ng update</code>,每个部分都耗费了博主大量的精力,翻阅了无数源码才得以实现。</p>
<p>在这个系列文章中,我将以 Ng-Matero 为例讲解 schematics 开发过程中遇到的难点,梳理开发流程,帮助大家开发自定义的 schematics 生成器。</p>
<p>该系列文章的三部分将分别介绍 Add、Generation 以及 Update,即使分了三部分来讲解 schematics,但我相信依然无法介绍的面面俱到。那遇到问题应该怎么办呢?没错,你需要看源码,这听起来可能让人心生畏惧,但是不用紧张,阅读源码并没有你想象的那么困难。顺便说一下,无论编写组件库还是 schematics,<code>Angular Material</code> 的源码都是最好的教材。</p>
<blockquote>
<p>在继续阅读文章之前,请务必将官网的 Schematics 教程撸一遍,有关方法的说明可以参考 Schematics 的 README 。</p>
</blockquote>
<h2 id="add-的用途">Add 的用途</h2>
<p>在我目前见过的项目中,<code>ng add</code> 主要有两个用途:</p>
<ul>
<li>初始化组件库(比如 angular material,ng-zorro,ngx-bootstrap)</li>
<li>初始化项目模板(比如 ng-matero,ng-alain)。</li>
</ul>
<p>初始化组件库相对简单一点,有些库的 <code>ng add</code> 甚至等同于 <code>npm install</code>。</p>
<p>相比之下,初始化项目模板要复杂很多,不仅要对项目进行配置,还要对项目中的文件进行增删改等操作。</p>
<p>本文将以初始化项目模板为例介绍 <code>ng add</code> 的执行过程。</p>
<h2 id="schematics-目录">Schematics 目录</h2>
<p>假设你的根目录有一个 schematics 的文件夹。</p>
<p><img src="https://img2018.cnblogs.com/blog/999445/202001/999445-20200107213016914-1839748171.jpg" alt="" loading="lazy"></p>
<p>在官网的教程中,已经列出了 schematics 目录的两种风格:</p>
<p>1、你可以在 schematics 文件夹中单独安装 <code>node_modules</code>,这样你在 <code>package.json</code> 中定义 scripts 的时候逻辑会比较清晰,但是整个项目会有两套 <code>node_modules</code>,而大部分依赖都和根目录重复;</p>
<pre><code class="language-json">{
"scripts": {
    "build": "tsc -p tsconfig.json"
},
}
</code></pre>
<p>2、另外也可以复用根目录的 <code>node_modules</code>,这样的话就会减少不必要的安装了</p>
<pre><code class="language-json">{
"scripts": {
    "build": "../node_modules/.bin/tsc -p tsconfig.json"
},
}
</code></pre>
<p>使用 Angular CLI 来创建项目的话一般来说就是第一种情况,比如创建一个库或者创建一个 schematics,核心文件都会放在 src 目录。</p>
<p>注意:使用 Angular CLI 的默认目录对于 Generation 命令比较友好,Angular CLI 添加的默认路径为 <code>src/app</code> 或者 <code>src/lib</code> 等,如果我们修改了默认目录,则在使用 <code>ng generate</code> 命令时需要显式的设置 <code>--path</code> 参数。</p>
<h2 id="发布-schematics">发布 Schematics</h2>
<p>因为 schematics 就是一套执行脚本,所以在项目发布之前需要将 schematics 的编译文件复制到项目目录,否则也无法使用 schematics。</p>
<ul>
<li>如果你开发的是一套组件库,那么你需要将 schematics 编译的文件拷贝到组件库中一起发布;</li>
<li>如果你开发的是一个项目模板,那么只需要发布 schematics 就可以了。</li>
</ul>
<p>因为 schematics 目录也是一个项目目录,所以你可以在 schematics 的 <code>package.json</code> 中定义拷贝命令,和官网教程是一样的,但是更恰当的方式应该是将复制命令写在根目录的 <code>package.json</code> 中。</p>
<pre><code class="language-json">{
"scripts": {
    "build:schematics": "npm run copy:schematics &amp;&amp; cd schematics &amp;&amp; npm run build &amp;&amp; cd .. &amp;&amp; npm run build:starter",
    "build:starter": "gulp --gulpfile gulpfile.js",
    "copy:schematics": "npm run clean:schematics &amp;&amp; cpr schematics dist/schematics",
    "clean:schematics": "rimraf dist/schematics",
}
}
</code></pre>
<h2 id="添加-ng-add">添加 ng add</h2>
<p>现在我们可以开始 ng add 的编写了,简单梳理一下,如果要使用 schematics 添加项目文件,我们需要做什么?</p>
<ul>
<li>初始化项目代码(提供模板配置项等)</li>
<li>删除 ng new 生成的重复文件(因为 schematic 无法自动替换文件)</li>
<li>把原始项目模板文件拷贝到项目目录</li>
<li>调整一下 package.json 和 angular.json</li>
<li>添加一些额外的 module</li>
<li>执行 npm install 安装 package</li>
</ul>
<p>以下是 <code>@angular/material</code> 的 <code>ng add</code> 逻辑,<code>ng-matero</code> 与此类似。</p>
<p><img src="https://img2018.cnblogs.com/blog/999445/202001/999445-20200105225344282-1672289524.jpg" alt="" loading="lazy"></p>
<h3 id="初始化安装">初始化安装</h3>
<p>在 schematics 中,我们可以通过 <code>NodePackageInstallTask</code> 方法安装 package</p>
<pre><code class="language-ts">export default function(options: any): Rule {
return (host: Tree, context: SchematicContext) =&gt; {
    // 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), );
    return host;
};
}
</code></pre>
<p>初始化的过程是先将依赖包添加到 package.json 中,然后执行 <code>npm install</code>,以上代码实际执行了两次 <code>npm install</code>,在执行 Add 主逻辑之前,首先安装了 cdk,parse5 等依赖包。</p>
<p>除了在代码中安装依赖以外,也可以在 schematics 的 package.json 中定义 cdk、parse5,只要保证在执行 Add 主逻辑的时候已经安装了上述包即可,但是这种方式过于死板,在 package.json 中更新依赖包的版本号有些繁琐。</p>
<h3 id="更新文件">更新文件</h3>
<p>在执行 <code>ng add</code> 拷贝项目模板的时候,会有一些需要更新的文件,但是 schematics 没有办法直接替换这些文件,所以必须先删除再拷贝,如果没有提前删除重复的文件,则会报错终止。</p>
<p>以下是安装 Ng-Matero 时对 <code>ng new</code> 生成的项目文件进行删除的方法。</p>
<pre><code class="language-ts">/** delete exsiting files to be overwrite */
function deleteExsitingFiles() {
return (host: Tree) =&gt; {
    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 =&gt; host.exists(p))
      .forEach(p =&gt; host.delete(p));
};
}
</code></pre>
<p>注意:在删除文件时先要遍历文件确定目录中有该文件再删除,否则同样会报错终止。</p>
<h3 id="拷贝文件">拷贝文件</h3>
<p>在执行完一系列规则之后,最终需要将 <code>files</code> 文件夹中的文件复制到项目目录,直接拷贝整个文件夹就可以,方法如下:</p>
<pre><code class="language-ts">/** Add starter files to root */
function addStarterFiles(options: Schema) {
return chain([
    mergeWith(
      apply(url('./files'), [
      template({
          ...strings,
          ...options,
      }),
      ])
    ),
]);
}
</code></pre>
<p>在拷贝完成之后,命令行会列出文件的创建、更新等信息。</p>
<p>关于 <code>chain</code> <code>mergeWith</code> <code>apply</code> <code>template</code> 等方法的使用详见 Schematics 的 README ,不过 Schematics 的 README 上面的方法并不全,很多方法还是需要参考 <code>@angular/material</code> 以及其它库的使用方式。</p>
<p>简单说一下 <code>template</code> 和 <code>applyTemplates</code> 的不同之处:</p>
<ul>
<li><code>template</code> 作用于原始文件</li>
<li><code>applyTemplates</code> 作用于后缀名为 <code>.template</code> 的文件。</li>
</ul>
<p>添加 <code>.template</code> 后缀的文件可以避免 VS Code 报错。</p>
<p>schematics 中的 <code>files</code> 模板文件是从 Ng-Matero 项目中拷贝的,拷贝方式有多种,可以通过 shell 命令,也可以通过 gulp,这取决于你的喜好。</p>
<h3 id="修改文件">修改文件</h3>
<p>JSON 文件的修改非常简单,比如在 <code>angular.json</code> 中添加 hmr 的设置。</p>
<pre><code class="language-ts">/** Add hmr to angular.json */
function addHmrToAngularJson() {
return (host: Tree) =&gt; {
    const workspace = getWorkspace(host);
    const ngJson = Object.assign(workspace);
    const project = ngJson.projects;

    // 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));
};
}
</code></pre>
<p>对于 JSON 文件的修改主要用到的就是 <code>overwrite</code> 方法。而对于非 JSON 文件的修改,相对麻烦一点,比如添加 hammer.js 的声明:</p>
<pre><code class="language-ts">/** Adds HammerJS to the main file of the specified Angular CLI project. */
export function addHammerJsToMain(options: Schema): Rule {
return (host: Tree) =&gt; {
    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);
};
}
</code></pre>
<p>关于 <code>host.beginUpdate</code>、<code>recorder.insertRight</code>、<code>host.commitUpdate</code> 这几个方法,可以看一下 angular cli 的源码。</p>
<p>除了上述提到的方法之外,在修改文件的时候,还可能用到 <code>AST</code>,需要更精细的操作代码文件,我会在 Generation 部分重点讲解。</p>
<h2 id="调试">调试</h2>
<p>在编写 schematics 的时候,调试很重要,简单说一下关于调试的问题以及技巧。</p>
<p>编写完 schematics 之后,我们需要通过 npm link 进行测试。假设我们已经在项目的根目录创建了一个测试项目。npm link 其实就是将打包目录的快捷方式拷贝到 <code>node_modules</code> 中。</p>
<p><code>ng add</code> 的测试比较麻烦,如果将模板安装到项目之后,再次测试需要重新初始化一个 ng 项目。另外,切记在 npm link 之后,执行 <code>ng add</code> 之前,先删除 <code>package-lock.json</code> 文件,否则 npm link 的项目会被更新删除。</p>
<p>有时为了更方便的测试,可能需要直接更改 <code>node_modules</code> 中的源代码,其实编译后的代码并非难以辨认,和原始文件差别并不是很大。这些问题也会在 Generation 部分重点讲解。</p>
<h2 id="总结">总结</h2>
<p>在最开始写 Ng-Matero 这个项目的时候,我一直觉得 schematics 是最关键的组成部分。为了让 Ng-Matero 不仅仅只是一个模板项目,我耗费了大量精力实现了一套比较简单的 schematics,这让我多少感到欣慰,也希望大家在使用 Schematics 时候可以提出更多宝贵意见。</p>
<p>本文拖沓了很久,但是依然比较表浅,如果大家有什么问题,欢迎留言评论,或者加入 Ng-Matero 自主群。</p>
<p><img src="https://img2018.cnblogs.com/blog/999445/201909/999445-20190919105319134-685276492.jpg" alt="" loading="lazy"></p>


</div>
<div id="MySignature" role="contentinfo">
    <div class="signature-main">
<p>感谢您的阅读,如果您对我的文章感兴趣,可以关注我的博客,我是叙帝利,下篇文章再见!</p>
<hr>
<p>高颜值的渐变编辑器组件,支持所有 CSS 渐变语法 https://github.com/acrodata/gradient-picker</p>
<p>一款小而美的颜色选择器组件 https://github.com/acrodata/color-picker</p>
<p>低代码平台必备轻量级 GUI 库 https://github.com/acrodata/gui</p>
<p>适用于 Angular 的 CodeMirror 6 组件 https://github.com/acrodata/code-editor</p>
<p>适用于 Angular 的水印组件(防删除,盲水印) https://github.com/acrodata/watermark</p>
<p>支持拖拽和缩放的弹窗组件 https://github.com/acrodata/rnd-dialog</p>
<p>开发低代码平台的必备拖拽库 https://github.com/ng-dnd/ng-dnd</p>
<p>基于 Angular Material 的中后台管理框架 https://github.com/ng-matero/ng-matero</p>
<p>Angular Material Extensions 扩展组件库 https://github.com/ng-matero/extensions</p>
<p>Unslider 轮播图插件纯 JS 实现 https://github.com/nzbin/unsliderjs</p>
<p>仿 Windows 照片查看器插件 https://github.com/nzbin/photoviewer</p>
<p>仿 Windows 照片查看器插件 jQuery 版 https://github.com/nzbin/magnify</p>
<p>完美替代 jQuery 的模块化 DOM 库 https://github.com/nzbin/domq</p>
<p>简化类名的轻量级 CSS 框架 https://github.com/nzbin/snack</p>
<p>与任意 UI 框架搭配使用的通用辅助类 https://github.com/nzbin/snack-helper</p>
<p>单元素纯 CSS 加载动画 https://github.com/nzbin/three-dots</p>
<p>有趣的 jQuery 卡片抽奖插件 https://github.com/nzbin/CardShow</p>
<p>悬疑科幻电影推荐 https://github.com/nzbin/movie-gallery</p>
<p>锻炼记忆力的小程序 https://github.com/nzbin/memory-stake</p>
</div><br><br>
来源:https://www.cnblogs.com/nzbin/p/12078629.html
頁: [1]
查看完整版本: Angular Schematics 三部曲之 Add