1. 程式人生 > 其它 >Angular 原理圖 Schematics 學習 - 動手開發一個實際的例子

Angular 原理圖 Schematics 學習 - 動手開發一個實際的例子

當 ng add 命令向專案中新增某個庫時,就會執行原理圖。ng generate 命令則會執行原理圖,來建立應用、庫和 Angular 程式碼塊。

一些術語:

規則

在原理圖 中,是指一個在檔案樹上執行的函式,用於以指定方式建立、刪除或修改檔案,並返回一個新的 Tree 物件。

檔案樹

在 schematics 中,一個用 Tree 類表示的虛擬檔案系統。 Schematic 規則以一個 tree 物件作為輸入,對它們進行操作,並且返回一個新的 tree 物件。

開發人員可以建立下列三種原理圖:

  • 安裝原理圖,以便 ng add 可以把你的庫新增到專案中。
  • 生成原理圖,以便 ng generate 可以為專案中的已定義工件(元件,服務,測試等)提供支援。
  • 更新原理圖,以便 ng update 可以更新你的庫的依賴,並提供一些遷移來破壞新版本中的更改。

下面我們動手做一個例子。

在庫的根資料夾中,建立一個 schematics/ 資料夾。

在 schematics/ 資料夾中,為你的第一個原理圖建立一個 ng-add/ 資料夾。

在 schematics/ 資料夾的根級,建立一個 collection.json 檔案。

編輯 collection.json 檔案來定義你的集合的初始模式定義。

如下圖所示:

collection.json 檔案內容如下:

{
  "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "ng-add": {
      "description": "Add my library to the project.",
      "factory": "./ng-add/index#ngAdd"
    },
    "my-service": {
      "description": "Generate a service in the project.",
      "factory": "./my-service/index#myService",
      "schema": "./my-service/schema.json"
    }
  }
}

下圖高亮行的意思是:執行 ng add 時,呼叫資料夾 ng-add 下面的 index.ts 檔案。

即這個檔案:

我們需要在 my-lib 庫的根目錄下的 package.json 裡,申明對上圖 collection.json 檔案的引用:

ng add 命令的原理圖可以增強使用者的初始安裝過程。可以按如下步驟定義這種原理圖。

(1) 進入 /schematics/ng-add/ 目錄。
(2) 建立主檔案 index.ts。
(3) 開啟 index.ts 並新增原理圖工廠函式的原始碼:

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';

// Just return the tree
export function ngAdd(options: any): Rule {
  return (tree: Tree, context: SchematicContext) => {
    context.addTask(new NodePackageInstallTask());
    return tree;
  };
}

提供初始 ng add 支援所需的唯一步驟是使用 SchematicContext 來觸發安裝任務。該任務會藉助使用者首選的包管理器將該庫新增到宿主專案的 package.json 配置檔案中,並將其安裝到該專案的 node_modules 目錄下。

在這個例子中,該函式會接收當前的 Tree 並返回它而不作任何修改。如果需要,你也可以在安裝軟體包時進行額外的設定,例如生成檔案、更新配置、或者庫所需的任何其它初始設定。

定義依賴型別

如果該庫應該新增到 dependencies 中、devDepedencies 中,或者不用儲存到專案的 package.json 配置檔案中,請使用 ng-add 的 save 選項進行配置

"ng-add": {
    "save": "devDependencies"
  }

可能的值有:

  • false - 不把此包新增到 package.json
  • true - 把此包新增到 dependencies
  • "dependencies" - 把此包新增到 dependencies
  • "devDependencies" - 把此包新增到 devDependencies

構建你的原理圖

必須首先構建庫本身,然後再構建 Schematics.

你的庫需要一個自定義的 Typescript 配置檔案,裡面帶有如何把原理圖編譯進庫的釋出版的一些指令。

要把這些原理圖新增到庫的釋出包中,就要把這些指令碼新增到該庫的 package.json 檔案中。

假設你在 Angular 工作區中有一個庫專案 my-lib。要想告訴庫如何構建原理圖,就要在生成的 tsconfig.lib.json 庫配置檔案旁新增一個 tsconfig.schematics.json 檔案。

新建一個 tsconfig.schematics.json 檔案,維護如下的原始碼:

{
  "compilerOptions": {
    "baseUrl": ".",
    "lib": [
      "es2018",
      "dom"
    ],
    "declaration": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "rootDir": "schematics",
    "outDir": "../../dist/my-lib/schematics",
    "skipDefaultLibCheck": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strictNullChecks": true,
    "target": "es6",
    "types": [
      "jasmine",
      "node"
    ]
  },
  "include": [
    "schematics/**/*"
  ],
  "exclude": [
    "schematics/*/files/**/*"
  ]
}

rootDir 指出在你的 schematics/ 資料夾中包含要編譯的輸入檔案,即下圖高亮的檔案:

outDir 對映到了庫的輸出目錄下。預設情況下,這是工作區根目錄下的 dist/my-lib 資料夾,即下圖這些檔案:

要確保你的原理圖原始檔會被編譯進庫包中,請把下列指令碼新增到庫專案的根資料夾(projects/my-lib)下的 package.json 檔案中。

{
  "name": "my-lib",
  "version": "0.0.1",
  "scripts": {
    "build": "../../node_modules/.bin/tsc -p tsconfig.schematics.json",
    "copy:schemas": "cp --parents schematics/*/schema.json ../../dist/my-lib/",
    "copy:files": "cp --parents -p schematics/*/files/** ../../dist/my-lib/",
    "copy:collection": "cp schematics/collection.json ../../dist/my-lib/schematics/collection.json",
    "postbuild": "npm run copy:schemas && npm run copy:files && npm run copy:collection"
  },
  "peerDependencies": {
    "@angular/common": "^7.2.0",
    "@angular/core": "^7.2.0"
  },
  "schematics": "./schematics/collection.json",
  "ng-add": {
    "save": "devDependencies"
  }
}

build 指令碼使用自定義的 tsconfig.schematics.json 檔案來編譯你的原理圖。

copy:* 語句將已編譯的原理圖檔案複製到庫的輸出目錄下的正確位置,以保持目錄的結構。

postbuild 指令碼會在 build 指令碼完成後複製原理圖檔案。

提供生成器支援

你可以把一個命名原理圖新增到集合中,讓你的使用者可以使用 ng generate 命令來建立你在庫中定義的工件。

我們假設你的庫定義了一項需要進行某些設定的服務 my-service。你希望使用者能夠用下面的 CLI 命令來生成它。

ng generate my-lib:my-service

首先,在 schematics 資料夾中新建一個子資料夾 my-service.

編輯一下 schematics/collection.json 檔案,指向新的原理圖子資料夾,並附上一個指向模式檔案的指標,該檔案將會指定新原理圖的輸入。

進入 /schematics/my-service/ 目錄。

建立一個 schema.json 檔案並定義該原理圖的可用選項。

每個選項都會把 key 與型別、描述和一個可選的別名關聯起來。該型別定義了你所期望的值的形態,並在使用者請求你的原理圖給出用法幫助時顯示這份描述。

建立一個 schema.ts 檔案,並定義一個介面,用於存放 schema.json 檔案中定義的各個選項的值。

export interface Schema {
  // The name of the service.
  name: string;

  // The path to create the service.
  path?: string;

  // The name of the project.
  project?: string;
}

name:你要為建立的這個服務指定的名稱。

path:覆蓋為原理圖提供的路徑。預設情況下,路徑是基於當前工作目錄的。

project:提供一個具體專案來執行原理圖。在原理圖中,如果使用者沒有給出該選項,你可以提供一個預設值。

要把工件新增到專案中,你的原理圖就需要自己的模板檔案。原理圖模板支援特殊的語法來執行程式碼和變數替換。

在 schematics/my-service/ 目錄下建立一個 files/ 資料夾。

建立一個名叫 name@dasherize.service.ts.template 的檔案,它定義了一個可以用來生成檔案的模板。這裡的模板會生成一個已把 Angular 的 HttpClient 注入到其建構函式中的服務。

檔案內容如下:

// #docregion template
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class <%= classify(name) %>Service {
  constructor(private http: HttpClient) { }
}

classify 和 dasherize 方法是實用函式,你的原理圖會用它們來轉換你的模板原始碼和檔名。

name 是工廠函式提供的一個屬性。它與你在模式中定義的 name 是一樣的。

添加工廠函式

現在,你已經有了基礎設施,可以開始定義一個 main 函式來執行要對使用者專案做的各種修改了。

Schematics 框架提供了一個檔案模板系統,它支援路徑和內容模板。系統會操作在這個輸入檔案樹(Tree)中載入的檔案內或路徑中定義的佔位符,用傳給 Rule 的值來填充它們。

關於這些資料結構和語法的詳細資訊,請參閱 Schematics 的 README。

建立主檔案 index.ts 併為你的原理圖工廠函式新增原始碼。

首先,匯入你需要的原理圖定義。Schematics 框架提供了許多實用函式來建立規則或在執行原理圖時和使用規則。

程式碼如下:

import {
  Rule, Tree, SchematicsException,
  apply, url, applyTemplates, move,
  chain, mergeWith
} from '@angular-devkit/schematics';

import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';

匯入已定義的模式介面,使用別名重定義為 MyServiceSchema,它會為你的原理圖選項提供型別資訊。

要想構建 "生成器原理圖",我們從一個空白的規則工廠開始。

index.js 檔案裡:

export function myService(options: MyServiceSchema): Rule {
  return (tree: Tree) => {
    return tree;
  };
}

這個規則工廠返回樹而不做任何修改。這些選項都是從 ng generate 命令傳過來的選項值。

定義一個生成器規則

我們現在有了一個框架,可用來建立一些真正修改使用者程式的程式碼,以便對庫中定義的服務進行設定。

使用者安裝過此庫的 Angular 工作區中會包含多個專案(應用和庫)。使用者可以在命令列中指定一個專案,也可以使用它的預設值。在任何一種情況下,你的程式碼都需要知道應該在哪個專案上應用此原理圖,這樣才能從該專案的配置中檢索資訊。

你可以使用傳給工廠函式的 Tree 物件來做到這一點。通過 Tree 的一些方法,你可以訪問此工作區的完整檔案樹,以便在執行原理圖時讀寫檔案。

獲取專案配置

要確定目標專案,可以使用 workspaces.readWorkspace 方法在工作區的根目錄下讀取工作區配置檔案 angular.json 的內容。要想使用 workspaces.readWorkspace,你要先從這個 Tree 創建出一個 workspaces.WorkspaceHost。 將以下程式碼新增到工廠函式中。

function createHost(tree: Tree): workspaces.WorkspaceHost {
  return {
    async readFile(path: string): Promise<string> {
      const data = tree.read(path);
      if (!data) {
        throw new SchematicsException('File not found.');
      }
      return virtualFs.fileBufferToString(data);
    },
    async writeFile(path: string, data: string): Promise<void> {
      return tree.overwrite(path, data);
    },
    async isDirectory(path: string): Promise<boolean> {
      return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
    },
    async isFile(path: string): Promise<boolean> {
      return tree.exists(path);
    },
  };
}

export function myService(options: MyServiceSchema): Rule {
  return async (tree: Tree) => {
    const host = createHost(tree);
    const { workspace } = await workspaces.readWorkspace('/', host);

  };
}

workspaces 是從 @angular-devkit/core 匯出的,readWorkspace 是其標準方法。該方法需要的第二個輸入引數 host,是從另一個自定義函式 createHost 返回的。

下面這行 default 邏輯處理:

if (!options.project) {
  options.project = workspace.extensions.defaultProject;
}

此 workspace.extensions 屬性中包含一個 defaultProject 值,用來確定如果沒有提供該引數,要使用哪個專案。如果 ng generate 命令中沒有明確指定任何專案,我們就會把它作為後備值。

有了專案名稱之後,用它來檢索指定專案的配置資訊。

const project = workspace.projects.get(options.project);
if (!project) {
  throw new SchematicsException(`Invalid project name: ${options.project}`);
}

const projectType = project.extensions.projectType === 'application' ? 'app' : 'lib';

options.path 決定了應用原理圖之後,要把原理圖模板檔案移動到的位置。
原理圖模式中的 path 選項預設會替換為當前工作目錄。如果未定義 path,就使用專案配置中的 sourceRoot 和 projectType 來確定。

邏輯體現在下面的程式碼裡:

if (options.path === undefined) {
  options.path = `${project.sourceRoot}/${projectType}`;
}

sourceRoot 在 angular.json 裡定義:

定義規則

Rule 可以使用外部模板檔案,對它們進行轉換,並使用轉換後的模板返回另一個 Rule 物件。你可以使用模板來生成原理圖所需的任意自定義檔案。

將以下程式碼新增到工廠函式中。

const templateSource = apply(url('./files'), [
  applyTemplates({
    classify: strings.classify,
    dasherize: strings.dasherize,
    name: options.name
  }),
  move(normalize(options.path as string))
]);

apply() 方法會把多個規則應用到原始碼中,並返回轉換後的原始碼。它需要兩個引數,一個原始碼和一個規則陣列。

url() 方法會從檔案系統中相對於原理圖的路徑下讀取原始檔。

applyTemplates() 方法會接收一個引數,它的方法和屬性可用在原理圖模板和原理圖檔名上。它返回一條 Rule。你可以在這裡定義 classify() 和 dasherize() 方法,以及 name 屬性。

classify() 方法接受一個值,並返回標題格式(title case)的值。比如,如果提供的名字是 my service,它就會返回 MyService。Title case 和駝峰命名法類似,是一種變數拼寫規則。

dasherize() 方法接受一個值,並以中線分隔並小寫的形式返回值。比如,如果提供的名字是 MyService,它就會返回 “my-service” 的形式。

當應用了此原理圖之後,move 方法會把所提供的原始檔移動到目的地。所以,my service 被轉換為 MyService,進而為 my-service.

規則工廠必須返回一條規則。

return chain([
  mergeWith(templateSource)
]);

該 chain() 方法允許你把多個規則組合到一個規則中,這樣就可以在一個原理圖中執行多個操作。這裡你只是把模板規則和原理圖要執行的程式碼合併在一起。

至此這個 Angular 庫的 Schematics 就開發完畢了,請持續關注 Jerry 後續文章,我會介紹如何消費這個 Schematics.

更多Jerry的原創文章,盡在:"汪子熙":