Angular庫與腳手架開發實戰

企業應用中常定製一些通用的組件,提供統一的用戶界面、數據呈現方式等,以便在不一樣的應用中重複使用。能夠將通用組件構建成Angular庫,這些庫能夠在Workspace本地使用,也能夠把它們發佈成 npm 包,共享給其它項目或其餘Angular開發者。css

Angular過期了麼?
Angular 與 angular.js 不是同一種前端框架,angular.js在2010年10月發佈,而 Angular 誕生於2016 年 9 月,比 React 和 Vue都要晚。整體而言,Angular、Vue 與 React 三種框架的運行速度沒有太大差別,不會是項目運行快慢的決定因素。html

國內華爲、阿里、中興等大廠都開發了Angular企業級組件庫,分別爲DevUING-ZORROJigsaw。另外,Angular Material是官方Angular組件庫。前端

開源組件庫是咱們學習Angular庫開發的最好教材,也能夠在他們的基礎上定製咱們的組件。node

Schematic,Angular中文版譯做原理圖,我更喜歡稱之爲腳手架。Schematic是一個基於模板的支持複雜邏輯的代碼生成器,能夠建立、修改和維護軟件項目。Schematic是Angular生態系統的一部分,咱們經常使用的Angular CLI命令ng generate、ng add和ng update,爲咱們添加/更新庫、建立構件提供了便利的工具。webpack

Angular CLI默認調用Schematics集合@schematics/angular,下面兩個命令功能是相同的:git

ng g component hello-world
ng g @schematics/angular:component hello-world

在庫開發中,一般要建立本身的schematics。es6

本文GitHub源碼:https://github.com/sunjc/ng-itrunnergithub

開發Angular庫

建立庫

用如下命令生成一個新庫的骨架:web

ng new ng-itrunner --new-project-root --create-application=false
cd ng-itrunner
ng generate library ng-itrunner --prefix ni

這會在工做區中建立 ng-itrunner 文件夾,裏面包含 NgModule、一個組件和一個服務。工做區的配置文件 angular.json 中添加了一個 'library' 類型的項目:shell

"projects": {
  "ng-itrunner": {
    "projectType": "library",
    "root": "ng-itrunner",
    "sourceRoot": "ng-itrunner/src",
    "prefix": "ni",
    "architect": {
      "build": {
        "builder": "@angular-devkit/build-ng-packagr:build",
        "options": {
          "tsConfig": "ng-itrunner/tsconfig.lib.json",
          "project": "ng-itrunner/ng-package.json"
        },
        "configurations": {
          "production": {
            "tsConfig": "ng-itrunner/tsconfig.lib.prod.json"
          }
        }
      }
            ...

庫項目文件

源文件 用途
src/lib 包含庫項目的邏輯和數據。像應用項目同樣,庫項目也能夠包含組件、服務、模塊、指令和管道
src/test.ts 單元測試主入口點,含一些庫專屬的配置
src/public-api.ts 指定從庫中導出的全部文件
karma.conf.js Karma 配置
ng-package.json 構建庫時,ng-packagr 用到的配置文件
package.json 配置庫所需的 npm 包依賴
tsconfig.lib.json 庫專屬的 TypeScript 配置,包括 TypeScript 和 Angular 模板編譯器選項
tsconfig.spec.json 測試庫時用到的 TypeScript 配置
tslint.json 庫專屬的 TSLint 配置

要讓庫代碼能夠複用,必須定義一個公共的 API public-api.ts。當庫被導入應用時,從該文件導出的全部內容都會公開。

構建、測試和lint

運行以下命令:

ng build ng-itrunner
ng test ng-itrunner
ng lint ng-itrunner

說明,庫與應用的構建器不一樣:

  • 應用程序的構建體系(@angular-devkit/build-angular)基於 webpack,並被包含在全部新的 Angular CLI 項目中。
  • 庫的構建體系(@angular-devkit/build-ng-packagr)基於 ng-packagr,只有在使用 ng generate library添加庫時,纔會添加到依賴項中。

庫編譯後,默認會生成esm五、esm201五、fesm五、fesm201五、es2015幾種格式。

增量構建
增量構建功能能夠改善庫的開發體驗,每當文件發生變化時,都會執行局部構建:

ng build ng-itrunner --watch

發佈庫

前面編譯時能夠看到下面的輸出:

******************************************************************************
It is not recommended to publish Ivy libraries to NPM repositories.
Read more here: https://v9.angular.io/guide/ivy#maintaining-library-compatibility
******************************************************************************

Angular 9使用Ivy編譯,不建議把 Ivy 格式的庫發佈到 NPM 倉庫。在tsconfig.lib.prod.json文件的配置中禁用了Ivy,會使用老的View Engine編譯器和運行時:

{
  "extends": "./tsconfig.lib.json",
  "angularCompilerOptions": {
    "enableIvy": false
  }
}

生產編譯時使用--prod選項,而後再發布:

ng build ng-itrunner --prod
cd dist/ng-itrunner
npm publish

默認發佈到公共NPM registry https://registry.npmjs.org 。也能夠發佈到私有Registry,好比Nexus,配置.npmrc以下:

registry=http://localhost:8081/repository/itrunner/
email=sjc-925@163.com
always-auth=true
_auth=YWRtaW46YWRtaW4xMjM=

其中 _auth 項爲用戶名:密碼的Base64編碼,生成命令以下:

echo -n 'admin:admin123' | openssl base64

也能夠發佈到指定registry:

npm publish --registry=http://localhost:8081/repository/itrunner/

連接庫
在開發要發佈的庫時,可使用 npm link 把庫連接到全局 node_modules 文件夾中,避免每次構建時都從新安裝庫。

cd dist/ng-itrunner
npm link

or

npm link dist/ng-itrunner

使用本身的庫

沒必要把庫發佈到 npm 包管理器,也能夠在本身的應用中使用它。

Angular 庫是一個 Angular 項目,它與應用的不一樣之處在於它自己是不能運行的。咱們先建立一個應用:

ng g application demo

在AppModule中導入NgItrunnerModule:

import {NgItrunnerModule} from 'ng-itrunner';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    NgItrunnerModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

說明:
當在 Angular 應用中從某個庫導入東西時,Angular會尋找庫名和磁盤某個位置之間的映射關係。當用 npm 包安裝庫時,映射到 node_modules 目錄下。當本身構建庫時,就會在 tsconfig 路徑中查找這個映射。
用 Angular CLI 生成庫時,會自動把它的路徑添加到 tsconfig 文件中。 Angular CLI 使用 tsconfig 路徑告訴構建系統在哪裏尋找這個庫。

修改app.component.html,添加咱們庫中的組件:

<ni-ng-itrunner></ni-ng-itrunner>

<!-- Resources -->
<h2>Resources</h2>

啓動Demo,查看效果:

ng serve demo

Angular庫與腳手架開發實戰

擴展NG-ZORRO

首先,學習NG-ZORRO的目錄結構和命名習慣,調整一下咱們的庫配置。

重構

  • 將庫文件夾重命名爲components
  • 將angular.json中的路徑ng-itrunner/替換爲components/,將ng-itrunner的root改成components
  • 調整目錄結構、重命名組件,以下:

Angular庫與腳手架開發實戰

  • 爲支持單獨導入某一模塊,優化體積,配置次級入口( secondary entry point)。要建立次級入口,僅需添加package.json,內容以下:
{
  "ngPackage": {
    "lib": {
      "entryFile": "public-api.ts"
    }
  }
}
  • 建立index.ts,內容以下:
export * from './public-api';

index.ts文件能夠減小import語句,如:

import {NiHelloLibComponent, NiHelloLibService} from '../hello-lib';
  • 在components根目錄建立主入口文件index.ts,導出空內容便可,以下:
export default {};
  • 修改ng-package.json主入口使用"index.ts":
{
  "$schema": "../node_modules/ng-packagr/ng-package.schema.json",
  "dest": "../dist/ng-itrunner",
  "deleteDestPath": true,
  "lib": {
    "entryFile": "index.ts"
  }
}
  • 咱們將建立多個lib組件,修改workspace根目錄的tsconfig.json的paths配置:
"paths": {
  "ng-itrunner/*": [
    "dist/ng-itrunner/*"
  ]
}
  • 從新編譯、測試:
    ng build ng-itrunner
    ng test ng-itrunner

添加NG-ZORRO

在Workspace執行如下命令,安裝ng-zorro-antd:

ng add ng-zorro-antd
Installing packages for tooling via npm.
Installed packages for tooling via npm.
? Enable icon dynamic loading [ Detail: https://ng.ant.design/components/icon/en ] Yes
? Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ] Yes
? Choose your locale code: en_US
? Choose template to create project: blank
UPDATE package.json (1311 bytes)
UPDATE demo/src/app/app.component.html (276 bytes)
√ Packages installed successfully.
CREATE demo/src/theme.less (28746 bytes)
UPDATE demo/src/app/app.module.ts (895 bytes)
UPDATE angular.json (3867 bytes)

編輯components/package.json,添加ng-zorro-antd:

"peerDependencies": {
  "@angular/common": "^9.0.0",
  "@angular/core": "^9.0.0",
  "ng-zorro-antd": "^9.0.0",
  "tslib": "^1.10.0"
}

添加@angular/cdk

Angular CDK,組件開發工具包,實現了通用交互模式和核心功能,是庫與腳手架開發的必備工具。
在Workspace執行如下命令,安裝@angular/cdk:

ng add @angular/cdk

編輯components/package.json,添加@angular/cdk:

"peerDependencies": {
  "@angular/cdk": "^9.0.0",
  "@angular/common": "^9.0.0",
  "@angular/core": "^9.0.0",
  "ng-zorro-antd": "^9.0.0",
  "tslib": "^1.10.0"
}

擴展NG-ZORRO

咱們簡單地封裝NG-ZORRO內聯登陸欄爲新組件,演示以NG-ZORRO組件爲基礎定製本身的組件。

在components目錄下建立inline-login-form文件夾,在其下建立如下文件:

index.ts

export * from './public-api';

inline-login-form.component.ts

import {Component, EventEmitter, OnInit, Output} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';

@Component({
  selector: 'ni-inline-login-form',
  template: `
    <form nz-form [nzLayout]="'inline'" [formGroup]="loginForm" (ngSubmit)="submitForm()">
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your username!">
          <nz-input-group nzPrefixIcon="user">
            <input formControlName="username" nz-input placeholder="Username"/>
          </nz-input-group>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control nzErrorTip="Please input your Password!">
          <nz-input-group nzPrefixIcon="lock">
            <input formControlName="password" nz-input type="password" placeholder="Password"/>
          </nz-input-group>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control>
          <button nz-button nzType="primary" [disabled]="!loginForm.valid">Log in</button>
        </nz-form-control>
      </nz-form-item>
    </form>
  `
})
export class NiInlineLoginFormComponent implements OnInit {
  @Output()
  login: EventEmitter<any> = new EventEmitter();

  loginForm: FormGroup;

  submitForm(): void {
    this.login.emit(this.loginForm.value);
  }

  constructor(private fb: FormBuilder) {
  }

  ngOnInit(): void {
    this.loginForm = this.fb.group({
      username: [null, [Validators.required]],
      password: [null, [Validators.required]]
    });
  }
}

inline-login-form.module.ts

import {NgModule} from '@angular/core';
import {NiInlineLoginFormComponent} from './inline-login-form.component';
import {NzButtonModule, NzFormModule, NzInputModule} from 'ng-zorro-antd';
import {ReactiveFormsModule} from '@angular/forms';

@NgModule({
  declarations: [NiInlineLoginFormComponent],
  imports: [
    ReactiveFormsModule,
    NzButtonModule,
    NzFormModule,
    NzInputModule
  ],
  exports: [NiInlineLoginFormComponent]
})
export class NiInlineLoginFormModule {
}

package.json

{
  "ngPackage": {
    "lib": {
      "entryFile": "public-api.ts",
      "umdModuleIds": {
        "ng-zorro-antd": "ng-zorro-antd"
      }
    }
  }
}

咱們引入了外部模塊ng-zorro-antd,若未配置UMD 標識符映射,編譯時則會輸出如下信息:
WARNING: No name was provided for external module 'ng-zorro-antd' in output.globals – guessing 'ngZorroAntd'

public-api.ts

export * from './inline-login-form.module';
export * from './inline-login-form.component';

測試組件

從新編譯庫後,在AppModule中引入NiHelloLibModule、NiInlineLoginFormModule:

...
import {NiInlineLoginFormModule} from 'ng-itrunner/inline-login-form';
...

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...
    NiHelloLibModule,
    NiInlineLoginFormModule,
  ],
  providers: [{provide: NZ_I18N, useValue: en_US}],
  bootstrap: [AppComponent]
})
export class AppModule {
}

在app.component.html中引入組件:

<nz-divider></nz-divider>
<!-- NI-iTRunner -->
<div nz-row nzJustify="center">
  <ni-hello></ni-hello>
</div>
<div nz-row nzJustify="center">
  <ni-inline-login-form (login)="login($event)"></ni-inline-login-form>
</div>

在app.component.ts中添加login()方法:

login(user: { username: string, password: string }) {
    ...
  }

啓動demo,效果以下:
Angular庫與腳手架開發實戰

Schematics CLI

Schematics有本身的命令行工具schematics cli,運行如下命令安裝:

npm install -g @angular-devkit/schematics-cli

Schematics最多見用途是將 Angular 庫與 Angular CLI 集成在一塊兒。能夠直接在 Angular 工做空間的庫項目中建立Schematics文件,而無需使用 Schematics CLI。

下面咱們使用 CLI 建立一個Schematics集合,僅爲介紹文件和目錄結構,以及一些基本概念。

建立Schematics集合
執行以下命令在同名的新項目文件夾中建立一個名爲 hello-world 的Schematic:

schematics blank --name=hello-world

生成項目的src/ 文件夾包含hello-world子文件夾,以及一個模式文件(collection.json)。

Schematic文件結構
每一個schematic通常都有如下主要部分:

文件 說明
index.ts 定義schematic中轉換邏輯的代碼
schema.json schematic變量定義
schema.ts schematic變量
files/ 要複製的可選組件/模板文件

src/hello-world中的主文件 index.ts 定義實現Schematic邏輯的規則:

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function helloWorld(_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    return tree;
  };
}

入口函數helloWorld是一個規則工廠,能夠經過調用外部工具和實現邏輯來修改項目。規則能夠利用 @schematics/angular 包提供的實用工具來處理模塊、依賴、TypeScript、AST、JSON、Angular CLI 工做空間和項目等等:

import {
  JsonAstObject,
  JsonObject,
  JsonValue,
  Path,
  normalize,
  parseJsonAst,
  strings,
} from '@angular-devkit/core';

collection.json
collection.json是集合中各個schematic的模式定義。每一個schematic都是用名稱、描述和工廠函數建立的:

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "hello-world": {
      "description": "A blank schematic.",
      "factory": "./hello-world/index#helloWorld"
    }
  }
}

還有兩個可選屬性:schema和aliases。

  • schema指定一個 JSON 模式文件,定義schematic可用的命令行參數
  • aliases 指定一個或多個可用來調用此schematic的別名。

例如:

...
"ng-add": {
  "description": "Adds Angular Material to the application without affecting any templates",
  "factory": "./ng-add/index",
  "schema": "./ng-add/schema.json",
  "aliases": ["material-shell", "install"]
}
...

schema.ts
定義schematic變量,例如:

export interface Schema {
  /** Name of the project. */
  project: string;

  /** Whether Angular browser animations should be set up. */
  animations: boolean;

  /** Name of pre-built theme to install. */
  theme: 'indigo-pink' | 'deeppurple-amber' | 'pink-bluegrey' | 'purple-green' | 'custom';

  /** Whether to set up global typography styles. */
  typography: boolean;
}

schema.json
定義輸入選項及其容許的值和默認值,例如:

{
  "$schema": "http://json-schema.org/schema",
  "id": "angular-material-ng-add",
  "title": "Angular Material ng-add schematic",
  "type": "object",
  "properties": {
    "project": {
      "type": "string",
      "description": "Name of the project.",
      "$default": {
        "$source": "projectName"
      }
    },
    "theme": {
      "description": "The theme to apply",
      "type": "string",
      "default": "indigo-pink",
      "x-prompt": {
        "message": "Choose a prebuilt theme name, or \"custom\" for a custom theme:",
        "type": "list",
        "items": [
          { "value": "indigo-pink",       "label": "Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]" },
          { "value": "deeppurple-amber",  "label": "Deep Purple/Amber  [ Preview: https://material.angular.io?theme=deeppurple-amber ]" },
          { "value": "pink-bluegrey",     "label": "Pink/Blue Grey     [ Preview: https://material.angular.io?theme=pink-bluegrey ]" },
          { "value": "purple-green",      "label": "Purple/Green       [ Preview: https://material.angular.io?theme=purple-green ]" },
          { "value": "custom",            "label": "Custom" }
        ]
      }
    },
    "typography": {
      "type": "boolean",
      "default": false,
      "description": "Whether to set up global typography styles.",
      "x-prompt": "Set up global Angular Material typography styles?"
    },
    "animations": {
      "type": "boolean",
      "default": true,
      "description": "Whether Angular browser animations should be set up.",
      "x-prompt": "Set up browser animations for Angular Material?"
    }
  },
  "required": []
}

schema.json語法請查看官方文檔

安裝依賴、編譯Schematic

cd hello-world
npm install
npm run build

運行Schematic
按如下格式提供項目路徑、Schematic名稱和全部必選項,使用 schematics 命令運行Schematic:

schematics <path-to-schematics-project>:<schematics-name> --<required-option>=<value>

路徑能夠是絕對路徑,也能夠是執行該命令的當前工做目錄的相對路徑:

schematics .:hello-world

庫的Schematics

接下來回到ng-itrunner workspace,建立庫的Schematics。做爲一名庫開發人員,一般要開發add schematic、generation schematic、update schematic,以便把庫與 Angular CLI 集成在一塊兒,能夠運行ng add來安裝庫,運行ng generate來修改項目、添加構件等,運行ng update更新庫依賴、調整變動等。

Add Schematic

建立Add Schematic

  1. 在components文件夾中建立schematics/ 文件夾
  2. 在schematics/文件夾中建立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 {angularCdkVersion, zorroVersion} from '../utils/version-names';
import {Schema} from './schema';

/**
 * Schematic factory entry-point for the `ng-add` schematic. The ng-add schematic will be
 * automatically executed if developers run `ng add ng-itrunner`.
 *
 * Since the NG-iTRunner 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.
 */
export default function(options: Schema): Rule {
  return (host: Tree, context: SchematicContext) => {
    addPackageToPackageJson(host, '@angular/cdk', angularCdkVersion);
    addPackageToPackageJson(host, 'ng-zorro-antd', zorroVersion);

    const installTaskId = context.addTask(new NodePackageInstallTask());

    context.addTask(new RunSchematicTask('ng-add-setup-project', options), [installTaskId]);
  };
}

在運行ng-add schematic前,CLI會自動添加ng-itrunner到宿主項目的package.json中。咱們的schematic會用到CDK工具函數,咱們的庫依賴ng-zorro-antd,所以首先須要將二者添加到package.json中。接下來,SchematicContext觸發安裝任務NodePackageInstallTask,將依賴安裝到項目的 node_modules 目錄下。最後調用另外一Schematic任務ng-add-setup-project配置項目。

說明:代碼中涉及的utils方法,請查看GitHub源碼。

  • setup-project.ts(ng-add-setup-project)

在庫開發中,通常會定義主題、依賴某些module等,能夠在此配置這些項目。前面咱們安裝了Angular CDK,如今可使用@angular/cdk的工具函數了。

import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {RunSchematicTask} from '@angular-devkit/schematics/tasks';
import {getProjectFromWorkspace} from '@angular/cdk/schematics';
import {getWorkspace} from '@schematics/angular/utility/config';
import {getProjectStyle} from '../utils/project-style';
import {Schema} from './schema';

/**
 * Scaffolds the basics of a NG-iTRunner application, this includes:
 *  - Add Template
 */
export default function(options: Schema): Rule {
  return chain([
    addTemplate(options)
  ]);
}

function addTemplate(options: Schema) {
  return (host: Tree, context: SchematicContext) => {
    if (options.template) {
      const workspace = getWorkspace(host);
      const project = getProjectFromWorkspace(workspace, options.project);
      const style = getProjectStyle(project);
      context.addTask(new RunSchematicTask(options.template, {...options, style: style}));
    }
    return host;
  };
}

chain() 方法容許把多個規則組合到一個規則中,這樣就能夠在一個Schematic中執行多個操做。這裏僅爲示例,只添加了一個建立模板組件方法。模板組件Schematic將在下一節介紹,爲成功運行ng-add能夠先暫時註釋此部分代碼。

  • schema.ts
export enum ProjectTemplate {
  Blank = 'blank',
  Login = 'login'
}

export interface Schema {
  /** Name of the project to target. */
  project?: string;
  template?: ProjectTemplate;
}
  • schema.json
{
  "$schema": "http://json-schema.org/schema",
  "id": "ni-ng-add",
  "title": "NG-iTRunner ng-add schematic",
  "type": "object",
  "properties": {
    "project": {
      "type": "string",
      "description": "Name of the project.",
      "$default": {
        "$source": "projectName"
      }
    },
    "template": {
      "type": "string",
      "default": "blank",
      "description": "Create an Angular project with using preset template.",
      "x-prompt": {
        "message": "Choose template to create project:",
        "type": "list",
        "items": [
          "blank",
          "login"
        ]
      }
    }
  },
  "required": []
}
  1. 在schematics/文件夾中建立 collection.json 文件,內容以下:
{
  "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "ng-add": {
      "description": "Add NG-iTRunner",
      "factory": "./ng-add/index",
      "schema": "./ng-add/schema.json",
      "hidden": true
    },
    "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",
      "schema": "./ng-add/schema.json"
    },
    "blank": {
      "description": "Set up boot page",
      "private": true,
      "factory": "./ng-generate/blank/index",
      "schema": "./ng-generate/blank/schema.json"
    },
    "login": {
      "description": "Create a login component",
      "factory": "./ng-generate/login/index",
      "schema": "./ng-generate/login/schema.json"
    }
  }
}
  1. 在庫項目的 package.json 文件中,添加 「schematics」 的條目。當 Angular CLI 運行命令時,會據此在集合中查找指定名字的schematic。
...
"schematics": "./schematics/collection.json"
...

構建Schematic
要把Schematic和庫打包到一塊兒,必須把庫配置成單獨構建Schematic,而後再把它們添加到發佈包中。所以必須先構建庫再構建Schematic,才能把它們放到正確的目錄下。

  1. 在庫根目錄下建立自定義的 Typescript 配置文件tsconfig.schematics.json,內容以下:
{
  "compilerOptions": {
    "baseUrl": ".",
    "lib": [
      "es2018",
      "dom"
    ],
    "declaration": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": false,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "rootDir": "schematics",
    "outDir": "../dist/ng-itrunner/schematics",
    "skipDefaultLibCheck": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strictNullChecks": true,
    "target": "es6",
    "types": [
      "jasmine",
      "node"
    ]
  },
  "include": [
    "schematics/**/*"
  ],
  "exclude": [
    "schematics/*/files/**/*"
  ]
}
  1. 在庫的package.json 文件中添加build scripts,運行build時將Schematic源文件編譯進庫包:
"scripts": {
    "build": "../node_modules/.bin/tsc -p tsconfig.schematics.json",
    "copy:schemas": "cp --parents schematics/*/schema.json schematics/*/*/schema.json ../dist/ng-itrunner/",
    "copy:files": "cp --parents -r schematics/*/*/files/** ../dist/ng-itrunner/",
    "copy:collection": "cp schematics/collection.json ../dist/ng-itrunner/schematics/collection.json",
    "copy:migration": "cp schematics/migration.json ../dist/ng-itrunner/schematics/migration.json",
    "postbuild": "npm run copy:schemas && npm run copy:files && npm run copy:collection && npm run copy:migration"
  }

說明:上面是本示例中完整的build腳本,須要根據實際狀況調整路徑、postbuild。後面再也不說明。

  1. 構建庫和Schematic
ng build ng-itrunner --prod
cd components
npm run build

運行Schematic

  1. 發佈、連接庫
cd dist/ng-itrunner
npm publish
npm link
  1. 運行
ng add ng-itrunner

運行ng add ng-itrunner將自動執行ng-add schematic。

Generation Schematic

運行ng generate --help能夠查看@schematics/angular提供的默認Schematic:

Available Schematics:
  Collection "@schematics/angular" (default):
    appShell
    application
    class
    component
    directive
    enum
    guard
    interceptor
    interface
    library
    module
    pipe
    service
    serviceWorker
    webWorker

collection.json中未設置"hidden"和"private"屬性爲true的schematics會顯示在Available Schematics列表中。

接下來咱們將新建blank和login兩個schematic,用於建立初始頁面和登陸組件,collection.json以下:

{
  "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "ng-add": {
      "description": "Add NG-iTRunner",
      "factory": "./ng-add/index",
      "schema": "./ng-add/schema.json",
      "hidden": true
    },
    "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",
      "schema": "./ng-add/schema.json"
    },
    "blank": {
      "description": "Set up boot page",
      "private": true,
      "factory": "./ng-generate/blank/index",
      "schema": "./ng-generate/blank/schema.json"
    },
    "login": {
      "description": "Create a login component",
      "factory": "./ng-generate/login/index",
      "schema": "./ng-generate/login/schema.json"
    }
  }
}

其中ng-add的"hidden"屬性爲true,ng-add-setup-project和blank的"private"屬性爲true。"hidden"屬性爲true則運行ng generate --help時不會顯示在Available Schematics列表中;"private"屬性爲true代表該Schematic僅供內部調用,同時暗示"hidden"屬性爲true。

當發佈咱們的庫後,因僅login可顯示,運行如下命令會直接顯示login的幫助:

ng g ng-itrunner: --help
Generates and/or modifies files based on a schematic.
usage: ng generate ng-itrunner:login <name> [options]

arguments:
  schematic
    The schematic or collection:schematic to generate.

options:
  --defaults
    When true, disables interactive input prompts for options with a default.
  --dry-run (-d)
    When true, runs through and reports activity without writing out results.
  --force (-f)
    When true, forces overwriting of existing files.
  --help
    Shows a help message for this command in the console.
  --interactive
    When false, disables interactive input prompts.

Help for schematic ng-itrunner:login

arguments:
  name
    The name of the component.

options:
  --prefix (-p)
    The prefix to apply to generated selectors.
  --project
    The name of the project.
  --skip-import
    Flag to skip the module import.
  --style
    The file extension to be used for style files.

不管是否設置"hidden"和"private"屬性,實際上不會影響運行ng generate,下面的命令能夠正常執行:

ng g ng-itrunner:blank
ng g ng-itrunner:ng-add

在schematics/文件夾中建立ng-generate文件夾。

blank schematic

blank schematic將覆蓋app.component.html,其內容僅包含一個圖片連接和一個ni-hello組件。
在ng-generate文件夾中建立blank文件夾,而後分別建立如下文件:

  • index.ts
import {Rule, Tree} from '@angular-devkit/schematics';
import {addModuleImportToRootModule, getProjectFromWorkspace} from '@angular/cdk/schematics';
import {getWorkspace} from '@schematics/angular/utility/config';
import {existsSync, statSync as fsStatSync} from 'fs';
import {Schema} from './schema';
import {itRunnerImage} from '../../utils/image';

const bootPageHTML = `<!-- NG-iTRunner -->
<a href="https://github.com/sunjc/ng-itrunner" target="_blank" style="display: flex;align-items: center;justify-content: center;width: 100%;">
  <img height="382" src="${itRunnerImage}" >
</a>
<div style="text-align: center">
  <ni-hello></ni-hello>
</div>`;

export default function(options: Schema): Rule {
  return (host: Tree) => {
    const workspace = getWorkspace(host);
    const project = getProjectFromWorkspace(workspace, options.project);
    const appHTMLFile = `${project.sourceRoot}/app/app.component.html`;
    const buffer = host.read(appHTMLFile);

    if (!buffer) {
      console.error(`Could not find the project ${appHTMLFile} file inside of the workspace config`);
      return;
    }

    if (existsSync(appHTMLFile)) {
      const stat = fsStatSync(appHTMLFile);
      if (stat.mtimeMs === stat.ctimeMs) {
        host.overwrite(appHTMLFile, bootPageHTML);
      }
    } else {
      host.overwrite(appHTMLFile, bootPageHTML);
    }

    // import NiHelloLibModule
    addModuleImportToRootModule(host, 'NiHelloLibModule', 'ng-itrunner/hello-lib', project);

    return host;
  };
}
  • schema.ts
export interface Schema {
  /** Name of the project to target. */
  project?: string;
}
  • schema.json
{
  "$schema": "http://json-schema.org/schema",
  "id": "ni-ng-generate-boot",
  "title": "NG-iTRunner boot page schematic",
  "type": "object",
  "properties": {
    "project": {
      "type": "string",
      "description": "Name of the project.",
      "$default": {
        "$source": "projectName"
      }
    }
  },
  "required": []
}

login schematic

login schematic利用模板文件封裝了ni-inline-login-form組件。

在ng-generate文件夾中建立login文件夾,而後建立如下文件:

  • 模板文件

在login文件夾中建立下面的目錄:

files\__path__\__name@dasherize@if-flat__

而後在其下建立四個component模板文件:

__name@dasherize__.component.html.template
__name@dasherize__.component.ts.template
__name@dasherize__.component.spec.ts.template
__name@dasherize__.component.__style__.template

內容分別爲:

  1. component.html.template
<ni-inline-login-form (login)="login($event)"></ni-inline-login-form>
  1. component.ts.template
import { Component } from '@angular/core';

@Component({
  selector: '<%= prefix %>-login',
  templateUrl: './<%= dasherize(name) %>.component.html',
  styleUrls: ['./<%= dasherize(name) %>.component.<%= style %>']
})
export class <%= classify(name) %>Component {

  login(user: { username: string, password: string }) {
      console.log(`{username: ${user.username}, password: ${user.password}}`);
  }

}
  1. component.spec.ts.template
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { NzButtonModule, NzFormModule, NzInputModule } from 'ng-zorro-antd';

import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';

describe('<%= classify(name) %>Component', () => {
  let component: <%= classify(name) %>Component;
  let fixture: ComponentFixture<<%= classify(name) %>Component>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [<%= classify(name) %>Component],
      imports: [
        ReactiveFormsModule,
        NzButtonModule,
        NzFormModule,
        NzInputModule
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(<%= classify(name) %>Component);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should compile', () => {
    expect(component).toBeTruthy();
  });
});
  1. component.style.template內容爲空。
  • index.ts
import {chain, noop, Rule, Tree} from '@angular-devkit/schematics';
import {addModuleImportToModule, buildComponent, findModuleFromOptions} from '@angular/cdk/schematics';
import {Schema} from './schema';

export default function(options: Schema): Rule {
  return chain([
    buildComponent({...options}, {
      template: './__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html.template',
      stylesheet:
        './__path__/__name@dasherize@if-flat__/__name@dasherize__.component.__style__.template',
    }),
    options.skipImport ? noop() : addRequiredModulesToModule(options)
  ]);
}

/**
 * Adds the required modules to the relative module.
 */
function addRequiredModulesToModule(options: Schema) {
  return (host: Tree) => {
    const modulePath = findModuleFromOptions(host, options)!;
    addModuleImportToModule(host, modulePath, 'NiInlineLoginFormModule', 'ng-itrunner/inline-login-form');
    return host;
  };
}

調用ng generate建立組件時,buildComponent方法將自動替換路徑、文件名稱、文件內容中的變量,addModuleImportToModule方法添加NiInlineLoginFormModule到指定的module。

  • schema.ts
import {Schema as ComponentSchema} from '@schematics/angular/component/schema';

export interface Schema extends ComponentSchema {
}
  • schema.json
{
  "$schema": "http://json-schema.org/schema",
  "id": "login",
  "title": "Login Component",
  "type": "object",
  "properties": {
    "path": {
      "type": "string",
      "format": "path",
      "description": "The path to create the component.",
      "visible": false
    },
    "project": {
      "type": "string",
      "description": "The name of the project.",
      "$default": {
        "$source": "projectName"
      }
    },
    "name": {
      "type": "string",
      "description": "The name of the component.",
      "$default": {
        "$source": "argv",
        "index": 0
      },
      "x-prompt": "What should be the name of the component?"
    },
    "prefix": {
      "type": "string",
      "format": "html-selector",
      "description": "The prefix to apply to generated selectors.",
      "default": "app",
      "alias": "p"
    },
    "style": {
      "description": "The file extension to be used for style files.",
      "type": "string"
    },
    "skipImport": {
      "type": "boolean",
      "description": "Flag to skip the module import.",
      "default": false
    },
    "module": {
      "type": "string",
      "description": "Allows specification of the declaring module.",
      "alias": "m"
    }
  },
  "required": ["name"]
}

運行Schematic
從新構建Schematic、發佈庫後便可完整的運行ng add ng-itrunner了:

ng add ng-itrunner
Installing packages for tooling via npm.
Installed packages for tooling via npm.
? Choose template to create project: blank
UPDATE package.json (1614 bytes)
√ Packages installed successfully.
UPDATE src/app/app.component.html (362933 bytes)

啓動應用,效果以下:
Angular庫與腳手架開發實戰
固然,您也能夠運行ng g ng-itrunner:login,建立login組件。

Update Schematic

Update Schematic能夠更新庫依賴,也能夠調整組件庫的變動。先看一下@angular/cdk/schematics提供的用於Update Schematic的一個重要函數createUpgradeRule():

/**
 * Creates a Angular schematic rule that runs the upgrade for the
 * specified target version.
 */
export function createUpgradeRule(
    targetVersion: TargetVersion, extraRules: NullableMigrationRule[], upgradeData: RuleUpgradeData,
    onMigrationCompleteFn?: PostMigrationFn): Rule {
        ...
}

其中包含四個參數:

  • targetVersion 運行ng update時可自動升級的Angular版本
  • extraRules 額外的規則,能夠自定義一些升級規則
  • upgradeData 類型爲RuleUpgradeData,定義AttributeSelector、ClassName、Constructor、CssSelector、ElementSelector、InputName、Method、OutputName、PropertyName變化時的更新規則。
export interface RuleUpgradeData {
  attributeSelectors: VersionChanges<AttributeSelectorUpgradeData>;
  classNames: VersionChanges<ClassNameUpgradeData>;
  constructorChecks: VersionChanges<ConstructorChecksUpgradeData>;
  cssSelectors: VersionChanges<CssSelectorUpgradeData>;
  elementSelectors: VersionChanges<ElementSelectorUpgradeData>;
  inputNames: VersionChanges<InputNameUpgradeData>;
  methodCallChecks: VersionChanges<MethodCallUpgradeData>;
  outputNames: VersionChanges<OutputNameUpgradeData>;
  propertyNames: VersionChanges<PropertyNameUpgradeData>;
}
  • onMigrationCompleteFn 升級完成後的回調函數

建立Update Schematic

  1. 在schematics/文件夾中建立ng-update文件夾
  2. 在ng-update文件夾中建立data文件夾,在其下建立如下文件用於定義RuleUpgradeData升級規則:
attribute-selectors.ts
class-names.ts
constructor-checks.ts
css-selectors.ts
element-selectors.ts
index.ts
input-names.ts
method-call-checks.ts
output-names.ts
property-names.ts

文件內容以下:

import { AttributeSelectorUpgradeData, VersionChanges } from '@angular/cdk/schematics';

export const attributeSelectors: VersionChanges<AttributeSelectorUpgradeData> = {};
import { ClassNameUpgradeData, TargetVersion, VersionChanges } from '@angular/cdk/schematics';

export const classNames: VersionChanges<ClassNameUpgradeData> = {
  [ TargetVersion.V9 ]: [ ]
};

這裏咱們定義了空的規則,升級時不會對項目作出更改。更詳細的規則定義方法能夠查看NG-ZORRO和Angular Material源碼。

  1. 在ng-update文件夾建立upgrade-data.ts文件,內容以下:
import { RuleUpgradeData } from '@angular/cdk/schematics';
import {
  attributeSelectors,
  classNames,
  constructorChecks,
  cssSelectors,
  elementSelectors,
  inputNames,
  methodCallChecks,
  outputNames,
  propertyNames
} from './data';

/** Upgrade data that will be used for the NG-iTRunner ng-update schematic. */
export const ruleUpgradeData: RuleUpgradeData = {
  attributeSelectors,
  classNames,
  constructorChecks,
  cssSelectors,
  elementSelectors,
  inputNames,
  methodCallChecks,
  outputNames,
  propertyNames
};
  1. 在ng-update文件夾建立主文件index.ts,內容以下:
import {Rule, SchematicContext} from '@angular-devkit/schematics';
import {createUpgradeRule, TargetVersion} from '@angular/cdk/schematics';
import {ruleUpgradeData} from './upgrade-data';

/** Entry point for the migration schematics with target of NG-iTRunner v9 */
export function updateToV9(): Rule {
  return createUpgradeRule(TargetVersion.V9, [], ruleUpgradeData, onMigrationComplete);
}

/** Function that will be called when the migration completed. */
function onMigrationComplete(context: SchematicContext, targetVersion: TargetVersion,
                             hasFailures: boolean) {
  context.logger.info('');
  context.logger.info(`  ✓  Updated NG-iTRunner to ${targetVersion}`);
  context.logger.info('');

  if (hasFailures) {
    context.logger.warn(
      '  ⚠  Some issues were detected but could not be fixed automatically. Please check the ' +
      'output above and fix these issues manually.');
  }
}
  1. 在schematics文件夾建立migration.json文件,內容以下:
{
  "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "migration-v9": {
      "version": "9.0.0",
      "description": "Updates NG-iTRunner to v9",
      "factory": "./ng-update/index#updateToV9"
    },
    "ng-post-update": {
      "description": "Prints out results after ng-update.",
      "factory": "./ng-update/index#postUpdate",
      "private": true
    }
  }
}
  1. 在package.json中增長如下配置:
"ng-update": {
  "migrations": "./schematics/migration.json",
  "packageGroup": [
    "ng-itrunner"
  ]
}

運行
將庫ng-itrunner的版本號改成9.0.0,與Angular主版本號保持一致。而後,從新構建庫、schematic,發佈庫。

運行ng update:

ng update ng-itrunner
Repository is not clean. Update changes will be mixed with pre-existing changes.
Using package manager: 'npm'
Collecting installed dependencies...
Found 34 dependencies.
Fetching dependency metadata from registry...
    Updating package.json with dependency ng-itrunner @ "9.0.0" (was "1.0.0")...
UPDATE package.json (1614 bytes)
√ Packages installed successfully.
** Executing migrations of package 'ng-itrunner' **

> Updates NG-iTRunner to v9

      ✓  Updated NG-iTRunner to version 9

  Migration completed.

總結: 本文主要參考了Angular官方文檔與NG-ZORRO和Angular Material源碼,介紹了庫與Schematic開發的基本過程。您要更深刻的學習,能夠查看NG-ZORRO、Angular Material源碼。

參考文檔

Angular 庫開發
Top 10 Angular Best Practices You Must Know

相關文章
相關標籤/搜索