Angular腳手架開發

簡介

寫一份自定義的angular腳手架吧
寫以前咱們先解析一下antd的腳手架html

前提

先把 Angular Schematic這篇文章讀一遍,確保瞭解了collection等基礎node

antd腳手架

克隆項目

git clone https://github.com/NG-ZORRO/ng-zorro-antd.gitgit

開始

打開項目github

clipboard.png

在schematics下的collection.json爲入口,查看內容json

clipboard.png

一共定了了4個schematic,每一個schema分別指向了各文件夾的子schema.json,factory指向了函數入口,index.tsbootstrap

ng-add/schema.json

{
  // 指定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": []
}

schema.ts

當你進入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;
}

ng-add/index.ts

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));
    }
  };
}

addPackageToPackageJson

// 看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]);

RunSchematicTask('ng-add-setup-project')

任務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"]
    }
  }
}

ng-add/setup-project

// 剛纔的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()
  ]);
}

addRequiredModules

// 模塊字典
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

registerLocale

不怕多,一點一點看,這裏主要作的工做就是i18n本地化啥的
先上一張圖片,記得腦子裏哦
clipboard.png

clipboard.png

接下來的函數都是爲了作上面這個工做

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...

未完待續

相關文章
相關標籤/搜索