在上一篇中介绍了在开发公司的组件库以前须要的准备工做,主要包括:需求评审、调研、组件设计和提出 issue。 本篇主要介绍在具体的开发环节须要注意哪些事项。html
组件开发的时候须要不断测试当前已开发的功能,这里建议单独配置一个私人 demo 项目用于本地开发。建立组件对应的初始库(详见Angular 的建立库),执行ng build [库名称]
生成本地的组件库。配置完成后,使用npm link
连接刚才打包好的组件库和 demo 项目,而后在 demo 项目的angular.json
中添加preserveSymlinks: true
(以下),完成建立开发环境。这是为了防止本地库的引用依赖错误。node
{
"$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,
...
复制代码
组件开发分为自行开发组件和基于第三方组件的二次封装,这两种在开发时略有不一样。前者一般是从自身的业务抽离而来好比标注组件,后者一般会有比较成熟的开源方案可供使用好比表格、图谱等。一般自行开发组件的成本会比较高,在业务相关性低,交互逻辑复杂,有比较成熟的开源方案的状况下,最好基于第三方组件作二次封装。react
二次封装一般有两种方式,能够用设计模式来表示:装饰者模式与中介者模式。git
装饰者模式是指在不改变原类及不使用继承的状况下,动态的扩展一个对象的功能。一般来讲就是用装饰器来实现一些原第三方组件作不到的功能。这种模式并不经常使用,这里很少介绍。github
咱们一般使用的是中介者模式。中介者模式是指用一个中介对象来封装一系列的对象交互,中介者使各对象不须要显式地相互引用,从而使其耦合松散,并且能够独立地改变它们之间的交互。 好比下面的例子中,TableComponent
充当了一个中介者,将输入的data
属性转化为第三方组件st
能够接受的数据类型,所以能够不加修改的直接引用原来的组件。typescript
// 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 提供的类型与构造函数。可是由于不能自定义组件的元参数,致使这种使用方式受限不少。npm
在 TypeScript 里,当多个装饰器应用在一个声明上时会进行以下步骤的操做:json
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被看成函数,由下至上依次调用。
// 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 官方也强调避免使用组件继承。)固然也不是彻底不能使用,当组件遵循第三方组件的大多数交互逻辑只是在视图层有所区别时,才建议考虑使用继承的方式实现。segmentfault
想了解更多关于组件继承,这篇文章可能会有帮助。设计模式
若是你肯定引入第三方组件,此时很容易忽略的一个问题就是第三方组件的 npm 包依赖问题。请选择第三方组件一个合适版本引入,确保兼容公司组件库的核心依赖库。
另外值得说明的是,在开发库的过程当中,请在组件库根目录的 package.json 中的 devDependencies
中安装全部的同级依赖。而在目标库的 package.json 中使用 peerDependencies
说明同级依赖。关于为什么这样作的缘由能够参考 Angular 文档开发 Angualr 库与 ng-packagr 的文档dependencies。
在一切准备就绪以后,先不要马上投入组件具体功能的实现中。我建议第一步实现一个最小的 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 中所列的功能了,在这期间可能会遇到不少意想不到的困难致使开发须要延期,也可能会须要调整不少次 API 以及增删一些功能。咱们是组件的设计者也是开发者同时也是使用者,咱们应该对它有本身的想法,开发时不断和你们沟通,能够据理力争也能够虚怀若谷,对它负责到底。这会是一个比较漫长的过程。咱们会像西西弗斯同样,不断踩坑爬坑,踩坑爬坑,踩坑爬坑。由于造轮子会比用轮子复杂许多,可能常常会碰到咱们知识的边界,会比较痛苦。可是当你最痛苦的时候,就是你进步最快的时候(也是头发掉的最快的时候)。
在开发 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/*"]
}
}
}
复制代码
经历一段时间辛苦的开发以后,组件终于能够投入使用了。在组件发布以前,请检查如下工做是否完成。
.gitignore
使其再也不忽略相应的编译文件,提交编译文件。接下来只须要按照流程进行发布便可。
咱们的项目通常采用 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 的形式管理工做流。
至此全部的工做就都结束了,停下来歇一歇,总结一下开发过程当中的收获,和同事们分享你的成长吧!
相关连接: 如何开发公司组件库(上)