14

手把手教你搭建自己的Angular组件库

 3 years ago
source link: https://segmentfault.com/a/1190000022637243
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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,默认生成的目录结构如下

z2iYNjY.png!web

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 ,我们对目录结构进行自定义修改如下:

ENRvYfy.png!web

修改说明:

sourceRoot

修改如下:

// 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": "de

库构建关键配置

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组件主要是根据用户传入的类型呈现不同的颜色和图标,用于向用户显示不同的警告信息。视觉显示如下

AzyIbm2.png!web

组件结构分解

首先,我们看一下alert组件目录包含哪些文件

eMZfeeJ.png!web

目录结构说明:

  • 组件是一个完整的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>

打开页面,就可以看到当前开发的效果,这时候我们就可以根据页面表现来调整样式和交互逻辑,此处就不继续展示了

YbyE7fj.png!web

编写单元测试

前面提到我们有一个单元测试文件,组件开发为了保证代码的质量和后续重构组件的稳定性,在开发组件的时候,有条件的建议加上单元测试。

由于我们调整了目录结构,我们先修改一下相关配置

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

7VjqYff.png!web

构建成功后,就可以着手发布组件库了,这里以发布到npm官方仓库为例

  1. 如果还没有npm账号,请到 官网网站 注册一个账号,选用public类型的免费账号就可以
  2. 已有账号,先确认配置的registry是否指向npm官方registry https://registry.npmjs.org/
  3. 在终端中执行 npm login 登录已注册的用户

准备工作都完成后,进入构建目录,这里是publish目录,然后执行 npm publish --access public 就可以发布了,注意我们的库名需要是在npm上没有被占用的,名字的修改在my-lib目录下的package.json中修改。

7rmQbmB.png!web

npm发布参考: https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry

如果是内部私有库,按照私有库的要求配置registry就可以了,发布命令都是一样的。

加入我们

我们是 DevUI团队 ,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:[email protected]

文/DevUI June


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK