寫一份自定義的angular腳手架吧
寫以前咱們先解析一下antd的腳手架html
先把 Angular Schematic這篇文章讀一遍,確保瞭解了collection等基礎node
git clone https://github.com/NG-ZORRO/ng-zorro-antd.git
git
打開項目github
在schematics下的collection.json爲入口,查看內容json
一共定了了4個schematic,每一個schema分別指向了各文件夾的子schema.json,factory指向了函數入口,index.tsbootstrap
{ // 指定schema.json的驗證模式 "$schema": "http://json-schema.org/schema", "id": "nz-ng-add", "title": "Ant Design of Angular(NG-ZORRO) ng-add schematic", "type": "object", // 包含的屬性 "properties": { "project": { "type": "string", "description": "Name of the project.", "$default": { "$source": "projectName" } }, // 是否跳過package.json的安裝屬性 "skipPackageJson": { // 類型爲布爾 "type": "boolean", // 默認值爲false "default": false, // 這是個描述,能夠看到,若是在ng add ng-zorro-antd時不但願自動安裝能夠加入--skipPackageJson配置項 "description": "Do not add ng-zorro-antd dependencies to package.json (e.g., --skipPackageJson)" }, // 開始頁面 "bootPage": { // 布爾 "type": "boolean", // 默認爲true "default": true, // 不指定--bootPage=false的話,你的app.html將會被覆蓋成antd的圖標頁 "description": "Set up boot page." }, // 圖標配置 "dynamicIcon": { "type": "boolean", "default": false, "description": "Whether icon assets should be add.", "x-prompt": "Add icon assets [ Detail: https://ng.ant.design/components/icon/en ]" }, // 主題配置 "theme": { "type": "boolean", "default": false, "description": "Whether custom theme file should be set up.", "x-prompt": "Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ]" }, // i18n配置,當你ng add ng-antd-zorro 的時候有沒有讓你選擇這個選項呢? "i18n": { "type": "string", "default": "en_US", "enum": [ "ar_EG", "bg_BG", "ca_ES", "cs_CZ", "da_DK", "de_DE", "el_GR", "en_GB", "en_US", "es_ES", "et_EE", "fa_IR", "fi_FI", "fr_BE", "fr_FR", "is_IS", "it_IT", "ja_JP", "ko_KR", "nb_NO", "nl_BE", "nl_NL", "pl_PL", "pt_BR", "pt_PT", "sk_SK", "sr_RS", "sv_SE", "th_TH", "tr_TR", "ru_RU", "uk_UA", "vi_VN", "zh_CN", "zh_TW" ], "description": "add locale code to module (e.g., --locale=en_US)" }, "locale": { "type": "string", "description": "Add locale code to module (e.g., --locale=en_US)", "default": "en_US", "x-prompt": { "message": "Choose your locale code:", "type": "list", "items": [ "en_US", "zh_CN", "ar_EG", "bg_BG", "ca_ES", "cs_CZ", "de_DE", "el_GR", "en_GB", "es_ES", "et_EE", "fa_IR", "fi_FI", "fr_BE", "fr_FR", "is_IS", "it_IT", "ja_JP", "ko_KR", "nb_NO", "nl_BE", "nl_NL", "pl_PL", "pt_BR", "pt_PT", "sk_SK", "sr_RS", "sv_SE", "th_TH", "tr_TR", "ru_RU", "uk_UA", "vi_VN", "zh_TW" ] } }, "gestures": { "type": "boolean", "default": false, "description": "Whether gesture support should be set up." }, "animations": { "type": "boolean", "default": true, "description": "Whether Angular browser animations should be set up." } }, "required": [] }
當你進入index.ts時首先看到的是一個帶options:Schema的函數,
options指向的類型是Schema interface,而這個interface 剛好是schema.json中的properties,也就是cli的傳入參數類.
咱們能夠經過自定義傳入參數類來完成咱們須要的操做.數組
export type Locale = | 'ar_EG' | 'bg_BG' | 'ca_ES' | 'cs_CZ' | 'da_DK' | 'de_DE' | 'el_GR' | 'en_GB' | 'en_US' | 'es_ES' | 'et_EE' | 'fa_IR' | 'fi_FI' | 'fr_BE' | 'fr_FR' | 'is_IS' | 'it_IT' | 'ja_JP' | 'ko_KR' | 'nb_NO' | 'nl_BE' | 'nl_NL' | 'pl_PL' | 'pt_BR' | 'pt_PT' | 'sk_SK' | 'sr_RS' | 'sv_SE' | 'th_TH' | 'tr_TR' | 'ru_RU' | 'uk_UA' | 'vi_VN' | 'zh_CN' | 'zh_TW'; export interface Schema { bootPage?: boolean; /** Name of the project to target. */ project?: string; /** Whether to skip package.json install. */ skipPackageJson?: boolean; dynamicIcon?: boolean; theme?: boolean; gestures?: boolean; animations?: boolean; locale?: Locale; i18n?: Locale; }
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { NodePackageInstallTask, RunSchematicTask } from '@angular-devkit/schematics/tasks'; import { addPackageToPackageJson } from '../utils/package-config'; import { hammerjsVersion, zorroVersion } from '../utils/version-names'; import { Schema } from './schema'; // factory指向的index.ts必須實現這個函數,一行一行看代碼 // 咱們的函數是一個更高階的函數,這意味着它接受或返回一個函數引用。 // 在這種狀況下,咱們的函數返回一個接受Tree和SchematicContext對象的函數。 // options:Schema上面提到了 export default function(options: Schema): Rule { // tree:虛擬文件系統:用於更改的暫存區域,包含原始文件系統以及要應用於其的更改列表。 // rule:A Rule是一個將動做應用於Tree給定的函數SchematicContext。 return (host: Tree, context: SchematicContext) => { // 若是須要安裝包,也就是--skipPackageJson=false if (!options.skipPackageJson) { // 調用addPackageToPackageJson,傳入,tree文件樹,包名,包版本 addPackageToPackageJson(host, 'ng-zorro-antd', zorroVersion); // hmr模式包 if (options.gestures) { addPackageToPackageJson(host, 'hammerjs', hammerjsVersion); } } const installTaskId = context.addTask(new NodePackageInstallTask()); context.addTask(new RunSchematicTask('ng-add-setup-project', options), [installTaskId]); if (options.bootPage) { context.addTask(new RunSchematicTask('boot-page', options)); } }; }
// 看function名字就知道這是下載依賴的函數 // @host:Tree 文件樹 // @pkg:string 包名 // @vserion:string 包版本 // @return Tree 返回了一個修改完成後的文件樹 export function addPackageToPackageJson(host: Tree, pkg: string, version: string): Tree { // 若是文件樹裏包含package.json文件 if (host.exists('package.json')) { // 讀取package.json的內容用utf-8編碼 const sourceText = host.read('package.json').toString('utf-8'); // 而後把package.json轉化爲對象,轉爲對象,轉爲對象 const json = JSON.parse(sourceText); // 若是package.json對象裏沒有dependencies屬性 if (!json.dependencies) { // 給package對象加入dependencies屬性 json.dependencies = {}; } // 若是package對象中沒有 pkg(包名),也就是說:若是當前項目沒有安裝antd if (!json.dependencies[pkg]) { // 那麼package的dependencies屬性中加入 antd:version json.dependencies[pkg] = version; // 排個序 json.dependencies = sortObjectByKeys(json.dependencies); } // 重寫tree下的package.json內容爲(剛纔不是有package.json對象嗎,如今在轉回去) host.overwrite('package.json', JSON.stringify(json, null, 2)); } // 把操做好的tree返回給上一級函數 return host; }
如今在回過頭去看 ng-add/index.tsantd
// 給context對象增長一個安裝包的任務,而後拿到了任務id const installTaskId = context.addTask(new NodePackageInstallTask()); // context增長另外一個任務,而後傳入了一個RunSchematicTask對象,和一個id集合 context.addTask(new RunSchematicTask('ng-add-setup-project', options), [installTaskId]);
任務ng-add-setup-project
定義在了schematic最外層的collection.json裏,記住以下4個schematic,後文再也不說起app
{ "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "ng-add": { "description": "add NG-ZORRO", "factory": "./ng-add/index", "schema": "./ng-add/schema.json" }, // 在這裏 "ng-add-setup-project": { "description": "Sets up the specified project after the ng-add dependencies have been installed.", "private": true, // 這個任務的函數指向 "factory": "./ng-add/setup-project/index", // 任務配置項 "schema": "./ng-add/schema.json" }, "boot-page": { "description": "Set up boot page", "private": true, "factory": "./ng-generate/boot-page/index", "schema": "./ng-generate/boot-page/schema.json" }, "add-icon-assets": { "description": "Add icon assets into CLI config", "factory": "./ng-add/setup-project/add-icon-assets#addIconToAssets", "schema": "./ng-generate/boot-page/schema.json", "aliases": ["fix-icon"] } } }
// 剛纔的index同樣,實現了一個函數 export default function (options: Schema): Rule { // 這裏其實就是調用各類函數的一個集合.options是上面的index.ts中傳過來的,配置項在上文有說起 return chain([ addRequiredModules(options), addAnimationsModule(options), registerLocale(options), addThemeToAppStyles(options), options.dynamicIcon ? addIconToAssets(options) : noop(), options.gestures ? hammerjsImport(options) : noop() ]); }
// 模塊字典 const modulesMap = { NgZorroAntdModule: 'ng-zorro-antd', FormsModule : '@angular/forms', HttpClientModule : '@angular/common/http' }; // 加入必須依賴模塊 export function addRequiredModules(options: Schema): Rule { return (host: Tree) => { // 獲取tree下的工做目錄 const workspace = getWorkspace(host); // 獲取項目 const project = getProjectFromWorkspace(workspace, options.project); // 獲取app.module的路徑 const appModulePath = getAppModulePath(host, getProjectMainFile(project)); // 循環字典 for (const module in modulesMap) { // 調用下面的函數,意思就是:給appModule引一些模塊,好吧,傳入了tree,字典key(模塊名稱),字典value(模塊所在包),project對象,appModule的路徑,Schema配置項 addModuleImportToApptModule(host, module, modulesMap[ module ], project, appModulePath, options); } // 將構建好的tree返回給上層函數 return host; }; } function addModuleImportToApptModule(host: Tree, moduleName: string, src: string, project: WorkspaceProject, appModulePath: string, options: Schema): void { // 若是app.module引入了NgZorroAntdModule等字典中的模塊 if (hasNgModuleImport(host, appModulePath, moduleName)) { // 來個提示 console.log(chalk.yellow(`Could not set up "${chalk.blue(moduleName)}" ` + `because "${chalk.blue(moduleName)}" is already imported. Please manually ` + `check "${chalk.blue(appModulePath)}" file.`)); return; } //若是沒有引入過就直接引入 addModuleImportToRootModule(host, moduleName, src, project); }
addAnimationsModule 內容差很少,略過ide
不怕多,一點一點看,這裏主要作的工做就是i18n本地化啥的
先上一張圖片,記得腦子裏哦
接下來的函數都是爲了作上面這個工做
export function registerLocale(options: Schema): Rule { return (host: Tree) => { // 獲取路徑 const workspace = getWorkspace(host); const project = getProjectFromWorkspace(workspace, options.project); const appModulePath = getAppModulePath(host, getProjectMainFile(project)); const moduleSource = getSourceFile(host, appModulePath); // 獲取add 時選擇的zh_cn,en_us啥的就是一個字符串 const locale = getCompatibleLocal(options); // 拿到 zh en這種 const localePrefix = locale.split('_')[ 0 ]; // recorder能夠理解成?快照,一個目錄下多個文件組成的文件快照,re coder // 爲何要beginUpdate,實際上個人理解是拿appModulePath文件創建了快照 // 直到後文 host.commitUpdate(recorder);纔會把快照做出的修改提交到tree上面 // 也能夠理解成你的項目有git控制,在你commit以前你操做的是快照,理解理解 const recorder = host.beginUpdate(appModulePath); // 對快照的操做列表 // insertImport = import {xxx} from 'xxx'這種 // 結合代碼看一下app.module.ts上面的import內容(上面圖片) const changes = [ insertImport(moduleSource, appModulePath, 'NZ_I18N', 'ng-zorro-antd'), insertImport(moduleSource, appModulePath, locale, 'ng-zorro-antd'), insertImport(moduleSource, appModulePath, 'registerLocaleData', '@angular/common'), insertImport(moduleSource, appModulePath, localePrefix, `@angular/common/locales/${localePrefix}`, true), registerLocaleData(moduleSource, appModulePath, localePrefix), // 這個函數特殊,看下面 ...insertI18nTokenProvide(moduleSource, appModulePath, locale) ]; // 循環變動列表若是是insertChange(import)那麼引入 changes.forEach((change) => { if (change instanceof InsertChange) { recorder.insertLeft(change.pos, change.toAdd); } }); // 提交變動到tree host.commitUpdate(recorder); // 返回tree給上一級函數 return host; }; } //上面說了,就是那個zh_CN/en_Us function getCompatibleLocal(options: Schema): string { const defaultLocal = 'en_US'; if (options.locale === options.i18n) { return options.locale; } else if (options.locale === defaultLocal) { console.log(); console.log(`${chalk.bgYellow('WARN')} ${chalk.cyan('--i18n')} option will be deprecated, ` + `use ${chalk.cyan('--locale')} instead`); return options.i18n; } else { return options.locale || defaultLocal; } } // 這個函數主要是爲了生成調用angular本地化的代碼registerLocaleData(zh); function registerLocaleData(moduleSource: ts.SourceFile, modulePath: string, locale: string): Change { ... if (registerLocaleDataFun.length === 0) { // 最核心的要在app.module中加入registerLocaleData(zh);才能把本地化作到angular上面 return insertAfterLastOccurrence(allImports, `\n\nregisterLocaleData(${locale});`, modulePath, 0) as InsertChange; } ... } * 這個change在change列表略特殊 * @param moduleSource module文件 * @param modulePath module路徑 * @param locale zh */ function insertI18nTokenProvide(moduleSource: ts.SourceFile, modulePath: string, locale: string): Change[] { const metadataField = 'providers'; // 獲取app.module中NgModule註釋的內容 //{ // declarations: [ // AppComponent // ], // imports: [ // BrowserModule, // AppRoutingModule, // NgZorroAntdModule, // FormsModule, // HttpClientModule, // BrowserAnimationsModule // ], // providers: [{ provide: NZ_I18N, useValue: zh_CN }], // bootstrap: [AppComponent] // } const nodes = getDecoratorMetadata(moduleSource, 'NgModule', '@angular/core'); // 生成一個provide到app.module中的ngModule註釋中,生成到providers數組中 **的操做**(只是生成一個動做)還沒應用到文件上 const addProvide = addSymbolToNgModuleMetadata(moduleSource, modulePath, 'providers', `{ provide: NZ_I18N, useValue: ${locale} }`, null); let node: any = nodes[ 0 ]; // tslint:disable-line:no-any // 而後下面開始作了一堆校驗工做 if (!node) { return []; } const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties .filter(prop => prop.kind === ts.SyntaxKind.PropertyAssignment) .filter((prop: ts.PropertyAssignment) => { const name = prop.name; switch (name.kind) { case ts.SyntaxKind.Identifier: return (name as ts.Identifier).getText(moduleSource) === metadataField; case ts.SyntaxKind.StringLiteral: return (name as ts.StringLiteral).text === metadataField; } return false; }); if (!matchingProperties) { return []; } if (matchingProperties.length) { const assignment = matchingProperties[ 0 ] as ts.PropertyAssignment; if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { return []; } const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; if (arrLiteral.elements.length === 0) { return addProvide; } else { node = arrLiteral.elements.filter(e => e.getText && e.getText().includes('NZ_I18N')); if (node.length === 0) { return addProvide; } else { console.log(); console.log(chalk.yellow(`Could not provide the locale token to your app.module file (${chalk.blue(modulePath)}).` + `because there is already a locale token in provides.`)); console.log(chalk.yellow(`Please manually add the following code to your provides:`)); console.log(chalk.cyan(`{ provide: NZ_I18N, useValue: ${locale} }`)); return []; } } } else { // 若是都沒什麼大問題,則把增長Provide的動做返回到changes列表,等待commit而後做出更改動做 return addProvide; } }
AST:https://www.kevinschuchard.co...
Schematic:https://brianflove.com/2018/1...
Ng add:https://brianflove.com/2018/1...