當 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) 進入
(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 文件,指向新的原理圖子文件夾,並附上一個指向模式文件的指針,該文件將會指定新原理圖的輸入。
進入
創建一個 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的原創文章,盡在:"汪子熙":