摘要:DevUI 是一款面向企業中后台產品的開源前端解決方案,它倡導沉浸
、靈活
、至簡
的設計價值觀,提倡設計者為真實的需求服務,為多數人的設計,拒絕嘩眾取寵、取悅眼球的設計。如果你正在開發 ToB
的工具類產品
,DevUI 將是一個很不錯的選擇!
DevUI是一支兼具設計視角和工程視角的團隊,服務於華為雲 DevCloud平台和華為內部數個中后台系統,服務於設計師和前端工程師。
官方網站: devui.design
Ng組件庫: ng-devui(歡迎Star)
引言
作為前端開發者,隨着公司業務的不斷發展和增長,業務對組件功能、交互的訴求會越來越多,不同產品或者團隊之間公用的組件也會越來越多,這時候就需要有一套用於支撐內部使用的組件庫,也可以是基於已有組件擴展或者封裝一些原生三方庫。本文會手把手教你搭建自己的Angular組件庫。
創建組件庫
我們首先創建一個Angular項目,用來管理組件的展示和發布,用以下命令生成一個新的項目
ng new <my-project>
項目初始化完成后,進入到項目下運行以下cli命令初始化lib目錄和配置, 生成一個組件庫骨架
ng generate library <my-lib> --prefix <my-prefix>
my-lib
為自己指定的library名稱,比如devui,my-prefix
為組件和指令前綴,比如d-xxx,默認生成的目錄結構如下
angular.json配置文件中也可以看到projects下面多出了一段項目類型為library的配置
"my-lib": { "projectType": "library", "root": "projects/my-lib", "sourceRoot": "projects/my-lib/src", "prefix": "dev", "architect": { "build": { "builder": "@angular-devkit/build-ng-packagr:build", "options": { "tsConfig": "projects/my-lib/tsconfig.lib.json", "project": "projects/my-lib/ng-package.json" }, "configurations": { "production": { "tsConfig": "projects/my-lib/tsconfig.lib.prod.json" } } }, ...
關鍵配置修改
目錄布局調整
從目錄結構可以看出默認生成的目錄結構比較深,參考material design
,我們對目錄結構進行自定義修改如下:
修改說明:
- 刪除了my-lib目錄下的src目錄,把src目錄下的test.ts拷貝出來,組件庫測試文件入口
- 把組件平鋪到my-lib目錄下,並在my-lib目錄下新增my-lib.module.ts(用於管理組件的導入、導出)和index.ts(導出my-lib.module.ts,簡化導入)
- 修改angular.json中my-lib下面的
sourceRoot
路徑,指向my-lib即可
修改如下:
// my-lib.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AlertModule } from 'my-lib/alert'; // 此處按照按需引入方式導入,my-lib對應我們的發布庫名 @NgModule({ imports: [ CommonModule ], exports: [AlertModule], providers: [], }) export class MyLibModule {} // index.ts export * from './my-lib.module'; //angular.json "projectType": "library", "root": "projects/my-lib", "sourceRoot": "projects/my-lib", // 這里路徑指向我們新的目錄 "prefix": "devui"
庫構建關鍵配置
ng-package.json
配置文件,angular library構建時依賴的配置文件
{ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", "dest": "../../publish", "lib": { "entryFile": "./index.ts" }, "whitelistedNonPeerDependencies": ["lodash-es"] }
關鍵配置說明:
- dest,lib構建輸出路徑,這里我們修改為publish目錄,和項目構建dist目錄區分開
- lib/entryFile,指定庫構建入口文件,此處指向我們上文的index.ts
whitelistedNonPeerDependencies(可選),如果組件庫依賴了第三方庫,比如lodash,需要在此處配置白名單,因為ng-packagr
構建時為了避免第三方依賴庫可能存在多版本沖突的風險,會檢查package.json的dependencies
依賴配置,如果不配置白名單,存在dependencies
配置時就會構建失敗。
package.json
配置,建議盡量使用peerDependcies,如果業務也配置了相關依賴項的話
{ "name": "my-lib", "version": "0.0.1", "peerDependencies": { "@angular/common": "^9.1.6", "@angular/core": "^9.1.6", "tslib": "^1.10.0" } }
詳細完整的配置,可以參考angular官方文檔 https://github.com/ng-packagr/ng-packagr/blob/master/docs/DESIGN.md
開發一個Alert組件
組件功能介紹
我們參考DevUI組件庫的alert組件開發一個組件,用來測試我們的組件庫,alert組件主要是根據用戶傳入的類型呈現不同的顏色和圖標,用於向用戶顯示不同的警告信息。視覺顯示如下
組件結構分解
首先,我們看一下alert組件目錄包含哪些文件
目錄結構說明:
- 組件是一個完整的module(和普通業務模塊一樣),並包含了一個單元測試文件
- 組件目錄下有一個package.json,用於支持二級入口(單個組件支持按需引入)
- public-api.ts用於導出module、組件、service等,是對外暴露的入口,index.ts會導出public-api,方便其它模塊
關鍵內容如下:
// package.json { "ngPackage": { "lib": { "entryFile": "public-api.ts" } } } //public-api.ts /* * Public API Surface of Alert */ export * from './alert.component'; export * from './alert.module';
定義輸入輸出
接下來我們就開始實現組件,首先我們定義一下組件的輸入輸出,alert內容我們采用投影的方式傳入,Input參數支持指定alert類型、是否顯示圖標、alert是否可關閉,Output返回關閉回調,用於使用者處理關閉后的邏輯
import { Component, Input } from '@angular/core'; // 定義alert有哪些可選類型 export type AlertType = 'success' | 'danger' | 'warning' | 'info'; @Component({ selector: 'dev-alert', templateUrl: './alert.component.html', styleUrls: ['./alert.component.scss'], }) export class AlertComponent { // Alert 類型 @Input() type: AlertType = 'info'; // 是否顯示圖標,用於支持用戶自定義圖標 @Input() showIcon = true; // 是否可關閉 @Input() closeable = false; // 關閉回調 @Output() closeEvent: EventEmitter<boolean> = new EventEmitter<boolean>(); hide = false; constructor() {} close(){ this.closeEvent.emit(true); this.hide = true; } }
定義布局
根據api定義和視覺顯示我們來實現頁面布局結構,布局包含一個關閉按鈕、圖標占位和內容投影 ,組件關閉時,我們采用清空dom的方式處理。
<div class="dev-alert {{ type }} " *ngIf="!hide"> <button type="button" class="dev-close" (click)="close()" *ngIf="closeable"></button> <span class="dev-alert-icon icon-{{ type }}" *ngIf="showIcon"></span> <ng-content></ng-content> </div>
到這里,我們組件的頁面布局和組件邏輯已經封裝完成,根據視覺顯示再加上對應的樣式處理就開發完成了。
測試Alert組件
開發態引用組件
組件開發過程中,我們需要能夠實時調試邏輯和調整UI展示,打開根目錄下的tsconfig.json,修改一下paths路徑映射,方便我們在開發態就可以本地調試我們的組件,這里直接把my-lib指向了組件源碼,當然也可以通過ng build my-lib --watch
來使用默認的配置, 指向構建好的預發布文件,此時這里就要配置成我們修改過的目錄public/my-lib/*
"paths": { "my-lib": [ "projects/my-lib/index.ts" ], "my-lib/*": [ "projects/my-lib/*" ], }
配置完成后,就可以在應用中按照npm的方式使用我們正在開發的庫了,我們在app.module.ts中先導入我們的正在開發的組件,這里可以從my-lib.module導入全部組件,或者直接導入我們的AlertModule(前面已經配置支持二級入口)
import { AlertModule } from 'my-lib/alert'; // import { MyLibModule } from 'my-lib'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, // MyLibModule AlertModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
此時在app.component.html頁面中就可以直接使用我們正在開發的alert組件了
<section> <dev-alert>我是一個默認類型的alert</dev-alert> </section>
打開頁面,就可以看到當前開發的效果,這時候我們就可以根據頁面表現來調整樣式和交互邏輯,此處就不繼續展示了
編寫單元測試
前面提到我們有一個單元測試文件,組件開發為了保證代碼的質量和后續重構組件的穩定性,在開發組件的時候,有條件的建議加上單元測試。
由於我們調整了目錄結構,我們先修改一下相關配置
// angular.json "my-lib": { ... "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "projects/my-lib/test.ts", // 這里指向調整后的文件路徑 "tsConfig": "projects/my-lib/tsconfig.spec.json", "karmaConfig": "projects/my-lib/karma.conf.js" } }, } //my-lib 目錄下的tsconfig.spec.json "files": [ "test.ts" // 指向當前目錄下的測試入口文件 ]
下面是一個簡單的測試參考,只簡單測試了type
類型是否正確,直接測試文件中定義了要測試的組件,場景較多的時候建議提供demo,直接使用demo進行不同場景的測試。
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { AlertModule } from './alert.module'; import { AlertComponent } from './alert.component'; import { By } from '@angular/platform-browser'; @Component({ template: ` <dev-alert [type]="type" [showIcon]= "showIcon"[closeable]="closeable" (closeEvent)="handleClose($event)"> <span>我是一個Alert組件</span> </dev-alert> ` }) class TestAlertComponent { type = 'info'; showIcon = false; closeable = false; clickCount = 0; handleClose(value) { this.clickCount++; } } describe('AlertComponent', () => { let component: TestAlertComponent; let fixture: ComponentFixture<TestAlertComponent>; let alertElement: HTMLElement; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [AlertModule], declarations: [ TestAlertComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(TestAlertComponent); component = fixture.componentInstance; alertElement = fixture.debugElement.query(By.directive(AlertComponent)).nativeElement; fixture.detectChanges(); }); describe('alert instance test', () => { it('should create', () => { expect(component).toBeTruthy(); }); }); describe('alert type test', () => { it('Alert should has info type', () => { expect(alertElement.querySelector('.info')).not.toBe(null); }); it('Alert should has success type', () => { // 修改type,判斷類型改變是否正確 component.type = 'success'; fixture.detectChanges(); expect(alertElement.querySelector('.success')).not.toBe(null); }); }
通過執行 ng test my-lib
就可以執行單元測試了,默認會打開一個窗口展示我們的測試結果
到這一步,組件開發態引用、測試就完成了,功能和交互沒有問題的話,就可以准備發布到npm了。
更多測試內容參考官方介紹:https://angular.cn/guide/testing
發布組件
組件開發完成后,單元測試也滿足我們定義的門禁指標,就可以准備發布到npm提供給其他同學使用了。
首先我們構建組件庫,由於ng9之后默認使用ivy引擎。官方並不建議把 Ivy 格式的庫發布到 NPM 倉庫。因此在發布到 NPM 之前,我們使用 --prod
標志構建它,此標志會使用老的編譯器和運行時,也就是視圖引擎(View Engine),以代替 Ivy。
ng build my-lib --prod
構建成功后,就可以着手發布組件庫了,這里以發布到npm官方倉庫為例
- 如果還沒有npm賬號,請到官網網站注冊一個賬號,選用public類型的免費賬號就可以
- 已有賬號,先確認配置的registry是否指向npm官方registry https://registry.npmjs.org/
- 在終端中執行
npm login
登錄已注冊的用戶
准備工作都完成后,進入構建目錄,這里是publish目錄,然后執行 npm publish --access public
就可以發布了,注意我們的庫名需要是在npm上沒有被占用的,名字的修改在my-lib目錄下的package.json中修改。
npm發布參考: https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry
如果是內部私有庫,按照私有庫的要求配置registry就可以了,發布命令都是一樣的。
加入我們
我們是DevUI團隊,歡迎來這里和我們一起打造優雅高效的人機設計/研發體系。招聘郵箱:muyang2@huawei.com。