隨着前端框架的誕生,也會隨之出現一些組件庫,方便日常業務開發。今天就聊聊 angular4 組件庫開發流程。
下圖是 button 組件的基礎文件。
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
至此,項目打包結束。