angular模組庫開發例項

weixin_33763244發表於2017-10-19

angular模組庫開發例項

隨著前端框架的誕生,也會隨之出現一些元件庫,方便日常業務開發。今天就聊聊angular4元件庫開發流程。

下圖是button元件的基礎檔案。

image

nk-button.component.ts為該元件的核心檔案,看看程式碼:

import {Component, Renderer2, ElementRef, AfterContentInit, ViewEncapsulation, Input} from '@angular/core';

@Component({
  selector: '[nk-button]',
  templateUrl: './nk-button.component.html',
  encapsulation: ViewEncapsulation.None,
  styleUrls: ['./nk-button.component.scss']
})
export class NkButtonComponent implements AfterContentInit {
  _el: HTMLElement;
  _prefixCls = 'ky-btn';
  _type: string;
  _size: string;
  _shape: string;
  _classList: Array<string> = [];

  @Input()
  get nkType() {
    return this._type;
  }

  set nkType(value) {
    this._type = value;
    this._setClass();
  }

  @Input()
  get nkSize() {
    return this._size;
  }

  set nkSize(value: string) {
    this._size = value;
    this._setClass();
  }

  @Input()
  get nkShape() {
    return this._shape;
  }

  set nkShape(value: string) {
    this._shape = value;
    this._setClass();
  }

  constructor(private _elementRef: ElementRef, private _renderer: Renderer2) {
    this._el = this._elementRef.nativeElement;
    this._renderer.addClass(this._el, this._prefixCls);
  }

  ngAfterContentInit() {
  }

  /**
   *設定class屬性
   */
  _setClass(): void {
    this._classList = [
      this.nkType && `${this._prefixCls}-${this.nkType}`,
      this.nkSize && `${this._prefixCls}-${this.nkSize}`,
      this.nkShape && `${this._prefixCls}-${this.nkShape}`
    ].filter(item => {
      return item;
    });
    this._classList.forEach(_className => {
      this._renderer.addClass(this._el, _className);
    });
  }
}

針對核心概念ElementRef、Renderer2、ViewEncapsulation做簡要說明:

ElementRef

在應用層直接操作 DOM,就會造成應用層與渲染層之間強耦合,通過 ElementRef 我們就可以封裝不同平臺下檢視層中的 native 元素 (在瀏覽器環境中,native 元素通常是指 DOM 元素),最後藉助於 Angular 提供的強大的依賴注入特性,我們就可以輕鬆地訪問到 native 元素。

參考連結

Renderer2

渲染器是 Angular 為我們提供的一種內建服務,用於執行 UI 渲染操作。在瀏覽器中,渲染是將模型對映到檢視的過程。模型的值可以是 JavaScript 中的原始資料型別、物件、陣列或其它的資料物件。然而檢視可以是頁面中的段落、表單、按鈕等其他元素,這些頁面元素內部使用DOM來表示。

參考連結

ViewEncapsulation

ViewEncapsulation 允許設定三個可選的值:

  • ViewEncapsulation.Emulated - 無 Shadow DOM,但是通過 Angular 提供的樣式包裝機制來封裝元件,使得元件的樣式不受外部影響。這是 Angular 的預設設定。
  • ViewEncapsulation.Native - 使用原生的 Shadow DOM 特性
  • ViewEncapsulation.None - 無 Shadow DOM,並且也無樣式包裝

參考連結

button元件建立思路:

  • 針對button我們只需修改其樣式,因此在這裡建立屬性指令
  • 提供屬性介面
  • 根據其傳入的屬性值動態渲染DOM

至此,最簡單的button就開發結束。

模組打包流程

合併html、css到component檔案

let fs = require('fs');
let pathUtil = require('path');
let sass = require('node-sass');
let filePath = pathUtil.join(__dirname, 'src', 'temp_components');

let fileArray = [];

function fildFile(path) {
  if (fs.statSync(path).isFile()) {
    if (/\.component.ts/.test(path)) {
      fileArray[0] = path;
    }
    if (/\.html$/.test(path)) {
      fileArray[1] = readFile(path)
    }
    if (/\.component.scss$/.test(path)) {
      fileArray[2] = path;
    }
  } else if (fs.statSync(path).isDirectory()) {
    let paths = fs.readdirSync(path);

    if (fileArray.length === 3) {
      writeFile(fileArray);
      fileArray = [];
    }
    paths.forEach((p) => {
      fildFile(pathUtil.join(path, p));
    });
  }

}

function readFile(file) {
  return fs.readFileSync(file);
}

function writeFile(fileArray) {
  let file = fileArray[0];
  let content = fileArray[1];
  let scssPath = fileArray[2];
  mergeContent(file, content, scssPath)
    .then(result => {
      if (!result) return;
      fs.writeFile(file, result, function (err) {
        if (err) console.error(err);
        console.log('file merge success!');
      })
    });

}

/**
 * 轉換scss
 * @param path
 * @returns {Promise}
 */
function processScss(path) {
  return new Promise((resolve, reject) => {
    sass.render({
      file: path
    }, (err, result) => {
      if (!err) {
        resolve(result.css.toString())
      } else {
        reject(err);
      }
    })
  })
}

function mergeContent(file, content, scssPath) {
  let componentContent = readFile(file);
  let htmlRegex = /(templateUrl *:\s*[\"|\'])(.*[\"|\']\,?)/g;
  let scssRegex = /(styleUrls *:\s*)(\[.*\]\,?)/g;

  let newContent = '';
  if (htmlRegex.test(componentContent) && scssRegex.test(componentContent)) {
    let contentArray = componentContent.toString().split(htmlRegex);
    contentArray[1] = 'template:`';
    contentArray[2] = content + '`,';
    contentArray.forEach(con => {
      newContent += con;
    })
    contentArray = newContent.toString().split(scssRegex);

    return new Promise((resolve, reject) => {
      processScss(scssPath)
        .then(result => {
          newContent = '';
          contentArray[1] = 'styles:[`';
          contentArray[2] = result + '`],';
          contentArray.forEach(con => {
            newContent += con;
          })
          resolve(newContent)
        }, err => {
          reject(err);
        })
    });
  }
}

fildFile(filePath);

ts編譯(tsconfig-aot.json)

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./publish/src",
    "baseUrl": "./",
    "declaration": true,
    "importHelpers": true,
    "module": "es2015",
    "sourceMap": false,
    "target": "es2015",
    "types": [
      "node"
    ]
  },
  "files": [
    "./src/temp_components/ng-kylin.module.ts"
  ],
  "angularCompilerOptions": {
    "annotateForClosureCompiler": true,
    "strictMetadataEmit": true,
    "flatModuleOutFile": "index.js",
    "flatModuleId": "ng-kylin",
    "skipTemplateCodegen": true
  }
}

rollup打包 (rollup-config.js)

import resolve from 'rollup-plugin-node-resolve'
import replace from 'rollup-plugin-replace'

const format = process.env.ROLLUP_FORMAT || 'es'

let globals = {
  '@angular/animations': 'ng.animations',
  '@angular/cdk': 'ng.cdk',
  '@angular/core': 'ng.core',
  '@angular/common': 'ng.common',
  '@angular/compiler': 'ng.compiler',
  '@angular/forms': 'ng.forms',
  '@angular/platform-browser': 'ng.platformBrowser',
  'moment': 'moment',
  'moment/locale/zh-cn': null,
  'rxjs/BehaviorSubject': 'Rx',
  'rxjs/Observable': 'Rx',
  'rxjs/Subject': 'Rx',
  'rxjs/Subscription': 'Rx',
  'rxjs/observable/fromPromise': 'Rx.Observable',
  'rxjs/observable/forkJoin': 'Rx.Observable',
  'rxjs/observable/fromEvent': 'Rx.Observable',
  'rxjs/observable/merge': 'Rx.Observable',
  'rxjs/observable/of': 'Rx.Observable',
  'rxjs/operator/auditTime': 'Rx.Observable.prototype',
  'rxjs/operator/catch': 'Rx.Observable.prototype',
  'rxjs/operator/debounceTime': 'Rx.Observable.prototype',
  'rxjs/operator/distinctUntilChanged': 'Rx.Observable.prototype',
  'rxjs/operator/do': 'Rx.Observable.prototype',
  'rxjs/operator/filter': 'Rx.Observable.prototype',
  'rxjs/operator/finally': 'Rx.Observable.prototype',
  'rxjs/operator/first': 'Rx.Observable.prototype',
  'rxjs/operator/map': 'Rx.Observable.prototype',
  'rxjs/operator/pluck': 'Rx.Observable.prototype',
  'rxjs/operator/startWith': 'Rx.Observable.prototype',
  'rxjs/operator/switchMap': 'Rx.Observable.prototype',
  'rxjs/operator/takeUntil': 'Rx.Observable.prototype',
  'rxjs/operator/throttleTime': 'Rx.Observable.prototype',
}

if (format === 'es') {
  globals = Object.assign(globals, {
    'tslib': 'tslib',
  })
}

let input
let file

switch (format) {
  case 'es':
    input = './publish/src/index.js'
    file = './publish/esm15/index.js'
    break
  case 'umd':
    input = './publish/esm5/index.js'
    file = './publish/bundles/ng-kylin.umd.js'
    break
  default:
    throw new Error(`format ${format} is not supported`)
}

export default {
  input,
  output: {
    file,
    format,
  },
  exports: 'named',
  name: 'ngKylin',
  plugins: [replace({ "import * as moment": "import moment" }), resolve()],
  external: Object.keys(globals),
  globals,
}

shell指令碼定義執行流程(build.sh)

#!/usr/bin/env bash

rm -rf ./publish

cp -r src/app/components src/temp_components

node ./html.merge.js

echo 'Generating entry file using Angular compiler'
$(npm bin)/ngc -p tsconfig-aot.json
rm -rf src/temp_components

echo 'Bundling to es module'
export ROLLUP_FORMAT=es
$(npm bin)/rollup -c rollup-config.js
rm -rf publish/src/*.js
rm -rf publish/src/**/*.js
sed -e "s/from '.\//from '.\/src\//g" publish/src/index.d.ts > publish/index.d.ts
sed -e "s/\":\".\//\":\".\/src\//g" publish/src/index.metadata.json > publish/index.metadata.json
rm publish/src/index.d.ts publish/src/index.metadata.json

echo 'Transpiling es module to es5'
$(npm bin)/tsc --allowJs --importHelpers --target es5 --module es2015 --outDir publish/esm5 publish/esm15/index.js

echo 'Bundling to umd module'
export ROLLUP_FORMAT=umd
$(npm bin)/rollup -c rollup-config.js

echo 'Minifying umd module'
$(npm bin)/uglifyjs publish/bundles/ng-kylin.umd.js --output publish/bundles/ng-kylin.umd.min.js

echo 'Copying package.json'
cp package.json publish/package.json

至此,專案打包結束。

原始碼

相關文章