全屏阅读

Angular 组件库 primeng 剖析

primeng 是目前市面上一个比较优秀的 Angular 组件库,组件种类多且全,可定制化程度高,文档也写得通俗易懂。

本文将对 primeng 组件库进行简单分析,抛砖引玉,希望能对编写你自己的组件库有所帮助。

项目结构

进入项目目录后,运行 tree -L 1 .(如果没有安装 tree 命令,使用 brew install tree 安装),查看目录结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ tree -L 1 .
.
├── .angular-cli.json --- angular-cli 配置文件
├── LICENSE.md --- 使用 MIT LICENSE
├── README.md --- README
├── e2e --- 基于 Protractor 的集成(端到端)测试,包括几个基础组件
├── gulpfile.js --- 使用 gulp 来处理一些 css 和 image 资源
├── karma.conf.js --- karma 测试配置文件
├── package.json --- package.json
├── primeng.d.ts --- 使用 es2015 模块语法导出所有组件中定义的对象
├── primeng.js --- 使用 commonjs 模块语法导出所有组件中定义的对象
├── protractor.conf.js --- e2e 集成测试配置文件,会运行 e2e 中定义的测试 case
├── src --- 源目录
├── tsconfig-aot.json --- 编译配置文件,使用 aot 构建,暂时没有看到使用的地方
├── tsconfig-release.json --- 编译配置文件,暂时没有看到使用的地方
├── tsconfig.json --- 编译基本配置,会被 src 和 e2e 目录下的配置文件所继承(extends)
└── tslint.json --- lint 配置

可以看出,primeng 项目结构也采用了 angular-cli 搭建,相比 angular-cli 初始化的项目,多了如下四个文件:

1
2
3
4
primeng.d.ts
primeng.js
tsconfig-aot.json
tsconfig-release.json

不过暂时没有看到 tsconfig-aot.jsontsconfig-release.json 两个编译配置文件用在何处。

再来看下 src 目录下的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ tree -L 2 .
.
├── app
│ ├── components --- 组件库,每个组件一个模块
│ └── showcase --- demo库
├── assets --- 全局资源,包括图片(images)和主题样式(themes)
│ ├── components --- 组件库用到的资源
│ └── showcase --- demo库用到的资源
├── environments --- 环境变量配置
│ ├── environment.prod.ts --- 生成环境变量配置
│ └── environment.ts --- 开发环境变量配置
├── favicon.png
├── index.html --- 主页
├── main.ts --- 入口文件
├── polyfills.ts --- polyfills,包括 core-js、zone.js、intl、web-animations-js 等
├── styles.css --- 全局样式
├── test.ts --- 测试入口文件
├── tsconfig.app.json --- 主项目编译文件
├── tsconfig.spec.json --- 测试编译文件
├── typings.d.ts --- 自定义类型
└── upload.php --- 上传处理页

组件设计

组件的代码位于 src/app/components 目录中,组件的一些设计思路如下:

  • 一个组件一个模块

这样可以很方便的按需引入组件,也利于组件的开发和维护。

另外,为了方便,在一个文件中定义了多个组件(一般是该组件的一些依赖组件),以及该包含该组件的模块。

  • 组件样式全局引入

组件样式不在组件中单独引入,那样会给所有组件样式添加默认的 scope 限定(encapsulation = ViewEncapsulation.Emulated),如果外层需要覆盖内层组件样式,会很不方便。因此组件的样式全部在 src/styles.css 中引入。当然也可以将 encapsulation 设置成 ViewEncapsulation.None 来避免 scope 限定,这两种方式二选一即可。

styles.css 内容如下:

1
2
3
@import 'assets/showcase/css/primeng.css';
@import 'assets/showcase/css/code.css';
@import 'assets/showcase/css/site.css';

assets/showcase/css/primeng.css 文件引入了所有 components 的样式。

1
2
3
4
@import '../../../app/components/common/common.css';
@import '../../../app/components/autocomplete/autocomplete.css';
@import '../../../app/components/accordion/accordion.css';
/* ... */
  • 全部内联模板

primeng 全部将模板写成了内联模板,优点是文件内容紧凑,不会分散注意力;缺点就是缺乏语法高亮,编写HTML模板无法使用 emmet 等自动语法提示。

在组件库中,有两个文件夹值得特别注意。

common 文件夹

common.css 中定义了通用样式,例如 .ui-widget.ui-helper-reset等,这些通用样式对于更换主题意义重大。

此外,其余大部分文件定义了组件库使用的一些全局性类型接口,例如广泛使用的 menuitem 结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// menuitem.ts
export interface MenuItem {
label?: string;
icon?: string;
command?: (event?: any) => void;
url?: string;
routerLink?: any;
items?: MenuItem[];
expanded?: boolean;
disabled?: boolean;
visible?: boolean;
target?: string;
routerLinkActiveOptions?: any;
separator?: boolean;
badge?: string;
badgeStyleClass?: string;
style?:any;
styleClass?:string;
}

另外,在 shared.ts 中,定义了一些通用的模板组件,例如 p-headerp-columnp-templateLoader 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// shared.ts
@Component({
selector: 'p-header',
template: '<ng-content></ng-content>'
})
export class Header {}
@Component({
selector: 'p-footer',
template: '<ng-content></ng-content>'
})
export class Footer {}
// ...

dom 文件夹

尽管不推荐直接操作 dom,但是对于一个组件库来说,操作 dom 是无法避免的,因此 domhandler.ts 文件封装了大部分 dom 操作。

  • addClass
  • addMultipleClasses
  • removeClass
  • hasClass
  • siblings
  • find
  • findSingle
  • index
  • relativePosition
  • absolutePosition
  • getHiddenElementOuterHeight
  • getHiddenElementOuterWidth
  • getHiddenElementDimensions
  • scrollInView
  • fadeIn
  • fadeOut
  • getWindowScrollTop
  • getWindowScrollLeft
  • matches
  • getOuterWidth
  • getHorizontalPadding
  • getHorizontalMargin
  • innerWidth
  • width
  • getInnerHeight
  • getOuterHeight
  • getHeight
  • getWidth
  • getViewport
  • getOffset
  • getUserAgent
  • isIE
  • appendChild
  • removeChild
  • isElement
  • calculateScrollbarWidth
  • invokeElementMethod
  • clearSelection

看到这么一大串方法列表,有人会疑惑,这不就是提取了 jQuery 的一部分功能吗,为什么不直接引入 jQuery 呢?

这里就涉及到一个库的设计原则:组件库的设计应该纯粹,能够拿来开箱即用,而不用再去配置组件库的一些依赖

如果要引入 jQuery,如果使用 es2015 的模块按需引用,那么需要手动去配置 jQuery 依赖;如果采取全量引入,那就需要在构建时将 jQuery 代码打包到最终的构建输出中,这并不是一个很好的设计方案。

因此,这里自己实现了一个 jQuery 的 mini 集合来操作 dom。

组件例子

以组件 accordion 为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// accordion.ts
// import list
@Component({
selector: 'p-accordion',
template: `
<div [ngClass]="'ui-accordion ui-widget ui-helper-reset'" [ngStyle]="style" [class]="styleClass">
<ng-content></ng-content>
</div>
`,
})
export class Accordion implements BlockableUI {
// ...
}
@Component({
selector: 'p-accordionTab',
template: `
<div class="ui-accordion-header">
</div>
<div class="ui-accordion-content-wrapper">
</div>
`
// ...
})
export class AccordionTab implements OnDestroy {
// ...
}
@NgModule({
imports: [CommonModule],
exports: [Accordion,AccordionTab],
declarations: [Accordion,AccordionTab]
})
export class AccordionModule { }

文件中定义了一个模块 AccordionModule,容器组件 p-accordion,以及面板组件 p-accordionTab。当然也可以分为三个文件去定义组件和模块,这两种代码组织方式也是仁者见仁,智者见智的问题。

主题样式

primeng 的主题样式采取了组件结构层样式(css)和表现层样式(scss)分离的策略,也即:

  • 结构层样式:只定义组件的结构相关的属性,例如 box-model 相关属性 displaypositionpaddingmargin
  • 表现层样式:定义组件展现的外观属性,例如 bordercolorbackgroundtransitionoutlinebox-shadow

以 Button 为例,在 src/app/components/button/button.css 中,定义了按钮结构层相关样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Button */
.ui-button {
display: inline-block;
position: relative;
padding: 0;
margin-right: .1em;
text-decoration: none !important;
cursor: pointer;
text-align: center;
zoom: 1;
overflow: visible; /* the overflow property removes extra width in IE */
}
.ui-button-icon-only {
width: 2em;
}
/*button text element */
.ui-button .ui-button-text {
display: block;
line-height: normal;
}

src/assets/components/themes/_theme.scss 中,定义了组件的一些基础表现层样式,例如所有的组件都包含有 ui-widget 类,该类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.ui-widget {
font-family: $fontFamily;
font-size: $fontSize;
input, select, textarea, button {
font-family: $fontFamily;
font-size: $fontSize;
}
:active {
outline: none;
}
}
.ui-widget-content {
border: $contentBorderWidth solid $contentBorderColor;
background: $contentBgColor;
color: $contentTextColor;
a {
color: $contentTextColor;
}
}

这两个样式类定义了所有组件的通用表现样式,例如字体、边框、背景色、前景色、链接颜色等。

其中的变量,例如 $fontFamily$contentBorderWidth 则在主题文件中去定义,primeng 提供了相当多的主题,你也可以很轻松的自定义自己的主题。

例如,看下主题 cruze 的样式实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* src/assets/components/themes/cruze/theme.scss */
$fontFamily: Segoe UI, Arial, sans-serif;
$fontSize: 1em;
$borderRadius: 6px;
$disabledOpacity: 0.35;
//Content
$contentBorderWidth: 1px;
$contentBorderColor: #dddddd;
$contentBgColor: #ffffff;
$contentTextColor: #444444;
/* ... */
@import '../_theme';

你也可以写一个自己的主题配置文件来实现轻松换肤的要求。

文档

文档就是 src/app/showcase 模块,在该模块中引入了 src/app/components 中的组件,编写相应的帮助文档。

测试

primeng 暂时没有包含组件单元测试,只有在 e2e 文件夹有几个集成测试。

个人建议如果时间精力足够的话,每个组件还是需要写上单测。

后续有时间,会抽出几个有代表性的组件进行分析,敬请期待。

坚持原创技术分享,您的支持将让我在冬天有冰棍吃啦,^_^!