Angular Schematics 三部曲之 Add


前言

因工作繁忙,差不多有三個月沒有寫過技術文章了,自八月份第一次編寫 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-matero,ng-alain)。

初始化組件庫相對簡單一點,有些庫的 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 自主群。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM