前端花园

frontend garden


  • 首页

  • 归档

  • 标签
  全屏阅读

Angular 组件库 primeng 剖析

发表于 2017-08-05 | 分类于 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.json 和 tsconfig-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-header、p-column、p-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 相关属性 display,position,padding,margin等
  • 表现层样式:定义组件展现的外观属性,例如 border,color,background,transition,outline,box-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 文件夹有几个集成测试。

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

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

用 webpack 构建 AMD 项目

发表于 2017-08-01 | 分类于 webpack

问题缘起

最近接手了一个历史遗留项目,找不到构建代码了,作为一个前端的基本素养,不能直接将源码丢在线上吧,还是得完善项目的构建,先需要好好分析下项目代码。

项目分析

项目是一个 AMD 项目,采用了百度的 esl(类似 require.js)作为加载器,使用了 jquery、underscore 等基本库,另外还用了 echarts 和 raphael 等库来做一些视觉渲染,样式直接使用了 css。

其中 esl 加载脚本的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
require.config({
urlArgs: 'ver=' + (new Date()).getTime(),
baseUrl: './src',
paths: {
text : '../lib/require.text',
jquery: '../lib/jquery',
underscore: '../lib/underscore',
echarts: '../lib/echarts',
raphael: '../lib/raphael',
z: '../lib/z'
}
});

看起来并不是特别的复杂,另外 webpack 可以处理各种类型的模块(AMD、commonjs、es6 modules),对于一切皆是模块的 webpack 来说,构建也应该不会太难。

webpack 配置

首先安装 webpack 相关库。

1
2
3
4
5
6
7
8
9
10
11
# webpack 相关
$ npm i -D webpack webpack-merge
# babel 相关
$ npm i -D babel-core babel-loader babel-plugin-transform-runtime babel-preset-es2015 babel-preset-stage-2
# loader 相关
$ npm i -D css-loader style-loader raw-loader file-loader url-loader
# plugin 相关
$ npm i -D copy-webpack-plugin html-webpack-plugin html-withimg-loader

新建 webpack 配置文件 webpack.config.js。

输入输出

首先配置输入和输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path');
const resolve = loc => {
return path.join(__dirname, '.', loc);
};
module.exports = {
entry: {
show: [resolve('src/show.js')]
},
output: {
path: resolve('dist'),
filename: '[name].[hash:6].js'
}
}

配置别名

由于库文件没有使用 node_modules,而是位于 lib 目录,因此需要配置别名来定位这些库文件。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
// ...
resolve: {
extensions: ['.js', '.json'],
alias: {
jquery: resolve('lib/jquery.js'),
underscore: resolve('lib/underscore.js'),
echarts: resolve('lib/echarts.js'),
z: resolve('lib/z.js')
}
}
}

对比之前的 esl 配置,可以看到用于加载文本(此项目用于加载 html 模板)的 amd 插件 require.text 没有配置别名,后续会通过 loader 进行处理。

配置 loaders

配置 loader 之前需要分析项目中所有的 require(),找出所有的可能文件后缀,只有这样才知道用什么样的 loader 来处理这类型的文件。

另外需要将 require() 的路径改成相对路径(例如从 require("lib/abc") 需要改成 require("./lib/abc")),html 文件以及 css 文件中相关资源的路径也需要保证引用正确。

此项目比较特殊的是,需要用 html-withimg-loader 来处理 html 模板文件,该 loader 会处理 html 中的图片引用(查阅了下,html-loader 也可以)。

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
const appPath = resolve('src');
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [appPath]
},
{
test: /\.(htm|html|tpl)$/i,
loader: 'html-withimg-loader'
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader?limit=10000'
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader?limit=10000'
}
]
}
}

配置 plugins

我们需要将 lib 目录下的代码打包成一个文件,需要使用 CommonsChunkPlugin 插件。

同时需要使用 HtmlWebpackPlugin 插件来动态生成 html 文件。

最后,还需要拷贝一些资源目录到构建目录,因此需要借助 CopyWebpackPlugin 插件。

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
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: resolve('show.html'),
inject: 'body',
hash: false
}),
new CommonsChunkPlugin({
name: 'vendor',
chunks: ['show'],
minChunks: module => /lib/.test(module.resource)
}),
new CopyWebpackPlugin([{
from: resolve('./show/reports'),
to: resolve('./dist/images')
}]),
new webpack.optimize.UglifyJsPlugin({
compress: {
screw_ie8: true
}
})
]
}

总结

webpack 是一个很强大的 bundle 工具,尤其是 一切皆模块 的思想可以很方便的用来处理各种资源复杂的依赖关系。

附录完整的配置文件如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const webpack = require('webpack');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const resolve = loc => {
return path.join(__dirname, '.', loc);
};
const appPath = resolve('src');
module.exports = {
entry: {
show: [resolve('src/show.js')]
},
output: {
path: resolve('dist'),
filename: '[name].[hash:6].js'
},
resolve: {
extensions: ['.js', '.json'],
alias: {
jquery: resolve('lib/jquery.js'),
underscore: resolve('lib/underscore.js'),
echarts: resolve('lib/echarts.js'),
z: resolve('lib/z.js')
}
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [appPath]
},
{
test: /\.(htm|html|tpl)$/i,
loader: 'html-withimg-loader'
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader?limit=10000'
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader?limit=10000'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: resolve('show.html'),
inject: 'body',
hash: false
}),
new CommonsChunkPlugin({
name: 'vendor',
chunks: ['show'],
minChunks: module => /lib/.test(module.resource)
}),
new CopyWebpackPlugin([{
from: resolve('./show/reports'),
to: resolve('./dist/images')
}]),
new webpack.optimize.UglifyJsPlugin({
compress: {
screw_ie8: true
}
})
]
}

angular-cli 指南

发表于 2017-07-31 | 分类于 angular

angular-cli 是一个能帮助你快速创建 Angular 项目的脚手架工具。并提供了项目创建、开发、托管、构建、Lint、测试等一套完整的工作流。

本篇博客将介绍一些 angular-cli 的使用注意事项,希望这篇博客能够节省你一些时间。

Node 和 NPM 版本要求

1
2
$ node -v
$ npm -v

其中 Node 版本需要 6.9.0+,NPM 需要 3.0.0+。

安装

如果之前安装了,升级到最新版本:

1
2
3
$ npm uninstall -g @angular/cli
$ npm cache clean
$ npm install -g @angular/cli@latest

创建应用

新建项目前,可以设置新项目下载 NPM 包的 registry 来提高下载速度,这里设置成 cnpm。

1
$ ng set --global packageManager=cnpm

创建新项目使用 ng new 命令,可以根据项目要求按需提供一些参数。

1
$ ng new lego --skip-tests=true --style=less --prefix=lego
  • --skip-tests=true 属性忽略生成组件测试文件(e2e及测试相关配置文件需要手动删除)
  • --style=less 属性指定样式文件名后缀为 less(默认为 css),默认样式也可以通过设置命令来修改:ng set defaults.styleExt less
  • --prefix=lego 属性指定有所有组件的标签选择器前缀为 lego-(默认为 app-)

最后,这些属性可以在 .angular-cli.json 配置文件中去修改。

生成的项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
.
├── README.md ----- 说明
├── e2e ----- 端到端测试
├── karma.conf.js ----- 测试配置文件
├── node_modules ----- 库
├── package.json ----- 包描述文件
├── protractor.conf.js ----- 端到端配置文件
├── src ----- 源目录
├── tsconfig.json ----- TS编译配置文件
└── tslint.json ----- 代码风格检查文件

启动应用

按照约定俗称的习惯,可以先使用 npm start 命令启动和托管应用。

1
$ npm start

按照提示打开 http://localhost:4200,不出意外,会看到应用程序已经成功启动了。

一般来说,我们希望启动应用后直接打开浏览器,刚才的 npm start 命令其实只是调用了 ng serve 命令,这个命令可以使用参数 --open 直接打开浏览器。

1
$ ng serve --open

如果默认的端口被占用,可以配置新的端口以及 host(有些情况下会出现 Invalid Host Name错误,此时需要显示配置 host)。

1
$ ng serve --host 0.0.0.0 --port 4201 --open

实时更新(Live Reload)

打开 app.component.html,修改其中的任意内容,保存后看网页是否实时刷新。

打开 app.component.css(或者 .less),添加内容如下:

1
2
3
h1 {
color: red;
}

不出意外,标题变红了!

打开 app.component.ts,修改为:

1
2
3
4
// ...
export class AppComponent {
title = "Lego"
}

页面标题也实时刷新。

不过细心的人会发现,这种实时刷新是整个页面刷新,并不是热加载局部刷新,下边的热加载更新会介绍如何来尽可能做到局部刷新。

风格检查(Lint)

可以运行 ng lint 来检查,这些检查的 rules 规则定义在 tslint.json 中,相关 rules 规则的解释可以查看 gist。

其中的规则结合了 tslint 的规则(针对 TS)和 codelyzer(针对 Angular ) 的一些规则。

1
2
3
4
5
6
7
8
9
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
// ...
}
}
1
$ ng lint

会提示如下:

1
2
3
ERROR: /Users/apps/lego/src/app/app.component.ts[9, 11]: " should be '
Lint errors found in the listed files.

其中,Command + 鼠标单击可以跳转到特定的代码位置,去修改相应代码。

其中 lint 还有一些可选参数,其中的 --fix 参数能够修复一些简单的错误,不过不要过分依赖这个自动修复。

1
$ ng lint --fix

不过,手动运行 lint 还是有点麻烦,如果想在修改完一个文件后实时 lint,这个需要怎么做,这个大家可以思考思考。

好了,我们来看下构建吧。

构建(Build)

1
$ ng build

构建会从 .angular-cli.json 中去读取相关配置。

上述命令默认为开发环境构建,等同于:

1
2
3
4
$ ng build --target=development --environment=dev
$ ng build --dev --e=dev
$ ng build --dev
$ ng build

其中用到了如下特性(关闭 aot 构建、生成 sourcemaps 等):

1
2
3
4
5
--aot=false
--envrionment=dev
--output-hashing=media
--sourcemaps=true
--extract-css=false

如果需要生成生成环境的构建,运行如下命令:

1
2
3
4
5
$ ng build --prod
// 等同于
$ ng build --target=production --environment=prod
$ ng build --prod --env=prod

默认开启的选项有(开启 aot 构建,关闭 sourcemaps 等):

1
2
3
4
5
--aot=true
--envrionment=prod
--output-hashing=all
--sourcemaps=false
--extract-css=true

我们可以看下默认构建的文件大小(生产环境)。

1
2
3
4
5
6
7
8
9
$ ls -alh dist/
-rw-r--r-- 1 xxx staff 5.3K 6 26 12:05 favicon.ico
-rw-r--r-- 1 xxx staff 698B 6 26 12:05 index.html
-rw-r--r-- 1 xxx staff 1.4K 6 26 12:05 inline.a5e3d2d7744618897ae8.bundle.js
-rw-r--r-- 1 xxx staff 5.2K 6 26 12:05 main.8427633bb8b01b93cb97.bundle.js
-rw-r--r-- 1 xxx staff 60K 6 26 12:05 polyfills.1afc07fd8ec7201977c6.bundle.js
-rw-r--r-- 1 xxx staff 0B 6 26 12:05 styles.d41d8cd98f00b204e980.bundle.css
-rw-r--r-- 1 xxx staff 223K 6 26 12:05 vendor.0d2bee3de7a805e4fab5.bundle.js

最大的为 vendor 文件,有 223K 大小,gzip 压缩后(gzip vendor.0d2bee3de7a805e4fab5.bundle.js)也有 55K 大小。

添加自己的环境变量

有时候我们需要添加自己的环境变量,例如添加一个参数 hmr 来控制是否开启热加载,可以分为三步:

  1. 创建一个 src/environments/enviroment.NAME.ts
  2. 添加 {"NAME": 'src/environments/enviroment.NAME.ts'} 到 .anguler-cli.json 的 app[0].enviroments 数组中
  3. 使用时添加 --env=NAME 参数

测试(Test)

如果是库项目或者一些公有底层项目,一个项目需要配套的测试文件,可以使用 ng test 来进行单元测试,使用 ng e2e 来进行集成测试。

不过如果是变化频繁的业务型项目,可以看情况是否启用测试。

如果不需要测试,可以将相关测试文件全部删掉即可,另外用 ng new 和 ng g 生成项目和组件时,可以忽略生成测试文件。

1
2
$ ng new lego --skip-tests=true
$ ng g c header --spec=false

脚手架(Scaffold)

可以使用命令快速生成项目代码:

1
2
3
4
5
6
7
8
9
10
Component ng g component my-new-component
Component ng g component my-new-component
Directive ng g directive my-new-directive
Pipe ng g pipe my-new-pipe
Service ng g service my-new-service
Class ng g class my-new-class
Guard ng g guard my-new-guard
Interface ng g interface my-new-interface
Enum ng g enum my-new-enum
Module ng g module my-module

自定义脚手架

如果有自己的代码规范,那么使用脚手架生成的代码很可能就不符合代码规范,每一次需要 format(希望你不要人肉去改),还是挺麻烦的。

另外 ng g c 等命令的配置较少,没有针对一些样式格式的配置(例如默认缩进空格)。

我们来生成一个组件 header,不需要单测文件,使用了 --spec=false。

1
$ ng g c header --spec=false

生成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'lego-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.less']
})
export class HeaderComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

我们希望:空格默认为 4 个,并且 import 语句的大括号旁边没有空格,有两种方案解决这个问题:

  • 继续使用 ng g 等命令

如果希望继续使用 ng g 等命令,需要修改 angular cli 的源代码,找到 angular/cli/blueprints/component/files/__path__目录,列出其中文件如下:

1
2
3
4
5
.
├── __name__.component.__styleext__
├── __name__.component.html
├── __name__.component.spec.ts
└── __name__.component.ts

修改这些对应的模板文件即可。

此种方式的缺点是,直接修改了 node_modules!,很明显,此种方式是不利于团队的。

  • 不使用 ng g 命令

另外一种方式是,不用 ng g 来生成组件,可以借助编辑器的 snippet 功能,自定义 snippet,此种方式也是非常不错的一种方式,缺点是需要新建多个文件(ng g则一次性生成了组件的多个文件)。

举个例子,用 vscode 自定义了一个组件的 snippet,在 ts 文件中,输入 ngC 就能看到该提示,按 tab 键就能自动填充了。

关于如何自定义 vscode 的 snippet,可以参考 官网文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"Angular Component": {
"prefix": "ngComponent",
"body": [
"import {",
" Component, Input, Output, EventEmitter, ",
" OnInit, ViewEncapsulation, ChangeDetectionStrategy",
"} from '@angular/core';\n",
"@Component({",
" selector: '${1:tag-name}',",
" templateUrl: './${3:list.component.tpl.html}',",
" styleUrls: ['./${4:list.component.less}'],",
" encapsulation: ViewEncapsulation.Emulated,",
" changeDetection: ChangeDetectionStrategy.Default",
"})",
"export class ${2:ComponentName}Component implements OnInit {",
" name = '${2:ComponentName}';\n",
" constructor() {\n",
" }\n",
" ngOnInit() {\n",
" }",
"}\n"
],
"description": "Define an Angular Component"
}

热加载(HMR)

由于 angular 的模块和组件设计机制,修改一个组件,最终的变动会冒泡到根组件和根模块,从而导致整个组件树刷新。

如果你仔细看了 angular-cli 的相关文档,会发现 ng serve 有一个 hmr 参数,我们来试一下。

为了测试 HMR 是否生效,可以做个实验,在 index.html 添加一个 input 输入框。

1
2
3
4
<body>
<input type="text">
<lego-root></lego-root>
</body>

应用启动后,在输入框输入若干文字,然后修改 app 组件中的任意内容,看刚才输入的文字是否还存在,如果存在的话,说明 HMR 生效了,否则说明 HMR 不生效,fallback 到了 livereload 功能。

也就是说 HMR 只会更新 lego-root 组件的内容,而 livereload 就是全页面刷新了。

1
$ ng serve --hmr --open

实验之后,会发现 --hmr 并没有带来 HMR 的效果。

不过仔细看看命令启动时的文字提示,会看到如下的提示:

1
2
3
4
5
6
NOTICE Hot Module Replacement (HMR) is enabled for the dev server.
The project will still live reload when HMR is enabled,
but to take advantage of HMR additional application code is required
(not included in an Angular CLI project by default).
See https://webpack.github.io/docs/hot-module-replacement.html
for information on working with HMR for Webpack.

大致意思是说,要实现 HMR 功能,需要添加 HMR 相关的额外代码,这些代码 angular cli 项目本身不提供,详情可以参见 这个网址。

不过上面的网址只是官方 webpack 的 hmr 介绍,对 angular-cli 的 HMR 如何配置并没有任何帮助,还是来看看 这篇文章 吧。

另外最近发现 angular/cli 的官网也有相关 story 了。

配置完毕后,HMR 整体还是比 livereload 全页面要快不少的。

代理(Proxy)

例如我们一个站点 http://localhost:4200/api,想将所有 /api 开头的请求代理到 http://localhost:3000,可以使用 --proxy-config 参数:

1
$ ng serve --proxy-config proxy.conf.json

其中 proxy.conf.json 内容如下:

1
2
3
4
5
6
{
"/api": {
"target": "http://localhost:3000",
"secure": false
}
}

除了单个路径,还可以代理多个路径,下边请求以 /my,/many 等开头的全部代理到 http://localhost:3000。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const PROXY_CONFIG = [
{
context: [
"/my",
"/many",
"/endpoints",
"/i",
"/need",
"/to",
"/proxy"
],
target: "http://localhost:3000",
secure: false
}
]
module.exports = PROXY_CONFIG;

如果深入了解下,ng serve 底层使用了 webpack-dev-server,而 webpack-dev-server 又使用了 http-proxy-middleware。

启动多个APP

有时候需要启动多个 app,例如一个 app 提供 mock 数据服务,另一个 app 提供业务代码逻辑。

  • 第一步:修改 .angular-cli.json 的 apps 数组,添加一个新 app 配置对象后如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"name": "mock",
"root": "src",
// ...
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
...
},
{
"name": "main"
"root": "src",
// ...
"main": "main2.ts",
"polyfills": "polyfills2.ts",
"test": "test2.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app2",
...
}

其中,新 app 配置对象需要修改 main、pollyfills、test、prefix 等字段,同时两个配置文件都添加了 name 字段。

  • 第二步:修改启动命令,添加 -app 参数。
1
$ ng serve --app=mock && ng serve --app main

--app 参数同时能接受序号。

1
$ ng serve --app=0 && ng serve --app 1

总结

  • 设置自定义下载源:
    ng set --global packageManager=cnpm
  • 新建项目忽略测试文件,使用 less:
    ng new lego --skip-tests=true --style=less --prefix=lego
  • 生成组件时不生产测试文件:
    ng g c header --spec=false
  • 托管应用使用 --open 自动打开浏览器:
    ng serve --host 0.0.0.0 --port 4301 --open
  • 代码检查后 command + 鼠标单击 定位错误,同时使用 --fix 来修复简单错误
    ng lint --fix
  • 配置 HMR 来提供开发效率
  • 修改脚手架文件或者定义 snnipet 来统一代码规范
  • 使用代理来进行请求拦截,适用于 mock 数据和真实数据切换等场景
zxhfighter

zxhfighter

frontend, angular, angularjs, typescript, rxjs, spa, ionic, vue, vue.js, react, native react, nativescript

3 日志
3 分类
7 标签
© 2017 zxhfighter
由 Hexo 强力驱动
主题 - NexT.Pisces