Angular Schematics 三部曲之 Add

前言

因工做繁忙,差很少有三個月沒有寫過技術文章了,自八月份第一次編寫 schematics 以來,我一直打算分享關於 schematics 的編寫技巧,無奈仍是拖到了年末。css

Angular Schematics 是很是強大的一個功能,能夠快速初始化項目,也能夠自定義組件模板。在去年 schematics 發佈以來,已經有部分開發者在項目中嘗試使用,可是學習資料仍是比較匱乏。目前官網已經有了 schematics 的簡易教程,但在實際開發中僅靠官方教程仍是會遇到不少問題。在開發 Ng-Matero 的過程當中,編寫 schematics 就像闖關同樣,從 ng addng generate 再到 ng update,每一個部分都耗費了博主大量的精力,翻閱了無數源碼才得以實現。html

在這個系列文章中,我將以 Ng-Matero 爲例講解 schematics 開發過程當中遇到的難點,梳理開發流程,幫助你們開發自定義的 schematics 生成器。node

該系列文章的三部分將分別介紹 Add、Generation 以及 Update,即便分了三部分來說解 schematics,但我相信依然沒法介紹的面面俱到。那遇到問題應該怎麼辦呢?沒錯,你須要看源碼,這聽起來可能讓人心生畏懼,可是不用緊張,閱讀源碼並無你想象的那麼困難。順便說一下,不管編寫組件庫仍是 schematics,Angular Material 的源碼都是最好的教材。git

在繼續閱讀文章以前,請務必將官網的 Schematics 教程擼一遍,有關方法的說明能夠參考 Schematics 的 READMEgithub

Add 的用途

在我目前見過的項目中,ng add 主要有兩個用途:shell

  • 初始化組件庫(好比 angular material,ng-zorro,ngx-bootstrap)
  • 初始化項目模板(好比 ng-alain,ng-matero)。

初始化組件庫相對簡單一點,有些庫的 ng add 甚至等同於 npm installnpm

相比之下,初始化項目模板要複雜不少,不只要對項目進行配置,還要對項目中的文件進行增刪改等操做。json

本文將以初始化項目模板爲例介紹 ng add 的執行過程。gulp

Schematics 目錄

假設你的根目錄有一個 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 的編譯文件複製到項目目錄,不然也沒法使用 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/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 自主羣。

相關文章
相關標籤/搜索