因工做繁忙,差很少有三個月沒有寫過技術文章了,自八月份第一次編寫 schematics 以來,我一直打算分享關於 schematics 的編寫技巧,無奈仍是拖到了年末。css
Angular Schematics 是很是強大的一個功能,能夠快速初始化項目,也能夠自定義組件模板。在去年 schematics 發佈以來,已經有部分開發者在項目中嘗試使用,可是學習資料仍是比較匱乏。目前官網已經有了 schematics 的簡易教程,但在實際開發中僅靠官方教程仍是會遇到不少問題。在開發 Ng-Matero 的過程當中,編寫 schematics 就像闖關同樣,從 ng add
到 ng generate
再到 ng update
,每一個部分都耗費了博主大量的精力,翻閱了無數源碼才得以實現。html
在這個系列文章中,我將以 Ng-Matero 爲例講解 schematics 開發過程當中遇到的難點,梳理開發流程,幫助你們開發自定義的 schematics 生成器。node
該系列文章的三部分將分別介紹 Add、Generation 以及 Update,即便分了三部分來說解 schematics,但我相信依然沒法介紹的面面俱到。那遇到問題應該怎麼辦呢?沒錯,你須要看源碼,這聽起來可能讓人心生畏懼,可是不用緊張,閱讀源碼並無你想象的那麼困難。順便說一下,不管編寫組件庫仍是 schematics,Angular Material
的源碼都是最好的教材。git
在繼續閱讀文章以前,請務必將官網的 Schematics 教程擼一遍,有關方法的說明能夠參考 Schematics 的 README。github
在我目前見過的項目中,ng add
主要有兩個用途:shell
初始化組件庫相對簡單一點,有些庫的 ng add
甚至等同於 npm install
。npm
相比之下,初始化項目模板要複雜不少,不只要對項目進行配置,還要對項目中的文件進行增刪改等操做。json
本文將以初始化項目模板爲例介紹 ng add
的執行過程。gulp
假設你的根目錄有一個 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。
由於 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 的編寫了,簡單梳理一下,若是要使用 schematics 添加項目文件,咱們須要作什麼?
如下是 @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 自主羣。