在上一篇中介绍了在开发公司的组件库之前需要的准备工作,主要包括:需求评审、调研、组件设计和提出 issue。 本篇主要介绍在具体的开发环节需要注意哪些事项。
创建开发环境
组件开发的时候需要不断测试当前已开发的功能,这里建议单独配置一个私人 demo 项目用于本地开发。创建组件对应的初始库(详见Angular 的创建库),执行ng build [库名称]生成本地的组件库。配置完成后,使用npm link链接刚才打包好的组件库和 demo 项目,然后在 demo 项目的angular.json中添加preserveSymlinks: true(如下),完成创建开发环境。这是为了防止本地库的引用依赖错误。
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"my-project": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "target",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/showcase/polyfills.ts",
"preserveSymlinks": true,
...
确定组件的开发方式
组件开发分为自行开发组件和基于第三方组件的二次封装,这两种在开发时略有不同。前者通常是从自身的业务抽离而来比如标注组件,后者通常会有比较成熟的开源方案可供使用比如表格、图谱等。通常自行开发组件的成本会比较高,在业务相关性低,交互逻辑复杂,有比较成熟的开源方案的情况下,最好基于第三方组件做二次封装。
中介者模式
二次封装通常有两种方式,可以用设计模式来表示:装饰者模式与中介者模式。
装饰者模式是指在不改变原类及不使用继承的情况下,动态的扩展一个对象的功能。通常来说就是用装饰器来实现一些原第三方组件做不到的功能。这种模式并不常用,这里不多介绍。
我们通常使用的是中介者模式。中介者模式是指用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
比如下面的例子中,TableComponent充当了一个中介者,将输入的data属性转化为第三方组件st可以接受的数据类型,因此可以不加修改的直接引用原来的组件。
// table.component.ts
export class TableComponent implements OnChanges {
@Input()
data: Data[];
_data: STData[];
get _data(): STData[] {
return this.data.map((item, index) => {
if (this.selectedRows && this.selectedRows.includes(index)) {
return {
checked: true,
...item
};
}
return item;
});
}
}
<st [data]="_data" ...></st>
是否应该使用组件继承
基于第三方组件的开发除了上述两种模式,其实还可以用继承来实现。Angular 的组件是使用@Component装饰器来实现的,因此组件的继承等同于 TypeScript 中带有类装饰器的类的继承。以下是一个组件继承的简单例子。像DerivedComponent一样,派生类如果拥有自己的@Component,其实相当于被两个@Component装饰器修饰过构造函数。根据 TypeScript 的装饰器组合原则,最外层的@Component被调用后对构造函数的修改会覆盖里层的@Component,最终生效的只是派生类的@Component中传入的参数。因此,如果需要改变@Component中的参数,则需要传入组件所需的全部参数。特别是providers中组件级的依赖,也是需要传入的,但第三方组件往往不会导出它的组件级依赖,就会给开发造成困难。而如果直接继承基类,像DirectDerivedComponent,该组件会继承@Component中所有的元参数和 BaseComponent 提供的类型与构造函数。但是因为不能自定义组件的元参数,导致这种使用方式受限很多。
在 TypeScript 里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。
// BaseComponent 基类组件
@Component({
selector: 'app-base',
templateUrl: './base.component.html',
styleUrls: ['./base.component.less'],
providers: [BaseService]
})
export class BaseComponent {
construct(baseService: BaseService) {}
}
// DerivedComponent 派生类组件
@Component({
selector: 'app-derived',
templateUrl: './derived.component.html',
styleUrls: ['./derived.component.less'],
providers: [BaseService] // 装饰器中的元参数会覆盖基类组件,因此需要重新提供 providers
})
export class DerivedComponent extends BaseComponent {}
// DirectDerivedComponent 直接继承的派生类组件
export class DirectDerivedComponent extends BaseComponent {}
以上分析说明,在 Angular 中使用继承的方式开发组件是一件受限很多且复杂的事情。而且 Angular 官方也没有提供关于组件继承相关的文档。只能从源码分析猜测组件继承的机制。因此,所有这些其实只是想说明一件事:尽量避免使用组件继承。如果你足够细心,会发现在 Angular 在DI 实战一节有此说明。(不仅 Angular, React 官方也强调避免使用组件继承。)当然也不是完全不能使用,当组件遵循第三方组件的大多数交互逻辑只是在视图层有所区别时,才建议考虑使用继承的方式实现。
想了解更多关于组件继承,这篇文章可能会有帮助。
引入第三方组件
如果你确定引入第三方组件,此时很容易忽略的一个问题就是第三方组件的 npm 包依赖问题。请选择第三方组件一个合适版本引入,确保兼容公司组件库的核心依赖库。
另外值得说明的是,在开发库的过程中,请在组件库根目录的 package.json 中的 devDependencies 中安装所有的同级依赖。而在目标库的 package.json 中使用 peerDependencies 说明同级依赖。关于为何这样做的原因可以参考 Angular 文档开发 Angualr 库与 ng-packagr 的文档dependencies。
实现一个最小 demo
在一切准备就绪之后,先不要立刻投入组件具体功能的实现中。我建议第一步实现一个最小的 demo,实现最基本最重要的功能,确保我们的思路是可行的,相当于 POC。比如我在开发 table 组件的时候,第一版的设计是这样的。table 列是通过 jsx 语法来实现自定义的,列的 API 设计也依赖于此。如果你之前用 React 的话会觉得这看起来是非常自然的设计。
@Component({
selector: 'table-demo',
template: `
<bx-table
[data]="data"
[columns]="columns"
></bx-table>
`
})
export class TableDemo {
// 表格行配置
columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name'
},
{
title: 'Action',
key: 'action',
template: (value, record, index) =>
`<a>Action 一 {{ record.name }}</a><nz-divider nzType="vertical"></nz-divider><a>Delete</a>`
}
];
}
然而 Angular 是不支持这种语法的,这是 React 的特性。导致我只能另寻他法。最终选择仿照 delon 实现自定义模板,然后重新改写 API。所以这个环节可以防止我们犯那些很严重的错误。往往我们第一次设计的 API 有诸多不合理之处,这是一个很好的机会进行调整,同时成功的 demo 也让我们有信心继续开发下去。
继续开发,逐步实现 issue 中的功能
接下来就需要我们慢慢逐步地实现 issue 中所列的功能了,在这期间可能会遇到很多意想不到的困难导致开发需要延期,也可能会需要调整很多次 API 以及增删一些功能。我们是组件的设计者也是开发者同时也是使用者,我们应该对它有自己的想法,开发时不断和大家沟通,可以力排众议也可以虚怀若谷,对它负责到底。这会是一个比较漫长的过程。我们会像西西弗斯一样,不断踩坑爬坑,踩坑爬坑,踩坑爬坑。因为造轮子会比用轮子复杂许多,可能经常会碰到我们知识的边界,会比较痛苦。但是当你最痛苦的时候,就是你进步最快的时候(也是头发掉的最快的时候)。
tsconfig.json 中的路径映射
在开发 angular 库的过程中,必然要进行打包(ng build)。由于 angular 默认的打包工具(@angular-devkit/build-ng-packagr)来自于ng-packagr,这里把我在使用ng-packagr时遇到的一个比较常见的坑和大家分享一下。
我们经常需要用别名指代某个位置的文件,比如需要用@component/*指代node_modules/component/dist/*。像这样的路径映射 typescript 是通过 tsconfig.json 的compilerOptions中baseUrl与paths来支持的。在 Angular 项目中会使用 angular.json 中指定的 tsconfig 文件,而 VS Code 编辑器中会使用根目录下的 tsconfig.json 文件。因此有时会出现编辑器不能正确解析路径映射的情况,但是 angular 是可以正常构建的。(来源见这里)
在开发库的时候,一般来说路径映射应该在主目录下的 tsconfig.json中配置,但有时候需要在库级别进行路径映射,也就是在 tsconfig.lib.json进行配置。这里值得注意的是,paths指向的路径必须是已编译的文件所在路径,而且除了目标本身,也需要对目标的子文件进行映射。例如同处于projects目录下的lib-a和lib-b,lib-b引入了lib-a,此时假如我们必须要在lib-b里进行路径映射,那么lib-b/tsconfig.lib.json需要配置大致如下。(关于此问题的讨论详见这里)
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"paths": {
"lib-a": ["../../dist/lib-a"],
"lib-a/*": ["../../dist/lib-a/*"]
}
}
}
开发终于结束了
经历一段时间辛苦的开发之后,组件终于可以投入使用了。在组件发布之前,请检查以下工作是否完成。
- 对比 issue 中的设计,查看是否完成所有的功能以及 API 是否有变动。如果有和 issue 设计不一致的地方,向大家说明原因。当然,最好是在开发的时候就积极沟通,和同事达成一致。
- 添加组件的 README,进行 API 说明。如果有必要的话,尽量补充使用范例。
- 编辑
.gitignore使其不再忽略相应的编译文件,提交编译文件。
接下来只需要按照流程进行发布即可。
- 发起 MR(如果在开发前已经发起了 WIP 状态的 MR,现在需要移除 WIP 标志)
- Code Review
- 合并入主分支
- 决定是否发布新版本
- 切出 release 分支,更新版本号
- 使用 cheers 发布 beta 版本(默认会打上 tag)
- 使用 demo 库测试 beta 版本,测试无误后合并入 master 分支,移除 release 分支
工作流介绍
我们的项目一般采用 gitflow 作为 git 工作流。它拥有两个长期分支 develop 与 master 和一些临时分支 feature分支、release 分支、hotfix 分支等。日常的开发在临时分支上进行,开发完毕后合并入 develop 分支,更新版本的时候通过 release 分支分别合并入 develop 分支和 master 分支。下图是 gitflow 的工作流图。(了解更多移步这里。)
不过组件库项目开发一般采用github flow。github flow 与 gitflow 不同的是移除了 develop 分支,只需维护一个长期分支 master(实际上它更类似于 github flow,见下图)。因此组件功能的更新不需要经过合入 develop 分支与切换 release 分支等操作,直接合入主分支即可。等到变动积累到一定量,会在主分支切出 release 分支,修改版本号然后打上 tag 进行发布。如此设计原因在于 gitflow 的工作流使用 develop (或 release 分支)来进行测试环境的更新,master 来进行生产环境的更新,适合于业务开发。但是组件库不依赖于测试环境,因此也就无需 develop 分支。常见的开源项目一般都采用 github flow 的形式管理工作流。
至此所有的工作就都结束了,停下来歇一歇,总结一下开发过程中的收获,和同事们分享你的成长吧!
相关链接: 如何开发公司组件库(上)