前言
因工作繁忙,差不多有三個月沒有寫過技術文章了,自八月份第一次編寫 schematics 以來,我一直打算分享關於 schematics 的編寫技巧,無奈還是拖到了年底。
Angular Schematics 是非常強大的一個功能,可以快速初始化項目,也可以自定義組件模板。在去年 schematics 發布以來,已經有部分開發者在項目中嘗試使用,但是學習資料還是比較匱乏。目前官網已經有了 schematics 的簡易教程,但在實際開發中僅靠官方教程還是會遇到很多問題。在開發 Ng-Matero 的過程中,編寫 schematics 就像闖關一樣,從 ng add
到 ng 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/material
的 ng 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
以及其它庫的使用方式。
簡單說一下 template
和 applyTemplates
的不同之處:
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.beginUpdate
、recorder.insertRight
、host.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 自主群。