愉快使用Angular-cli构建项目
jiayisheji opened this issue · 4 comments
前言
使用Angular CLI开发Angular应用程序是一个非常愉快的体验!Angular团队
为我们提供了惊人的CLI
工具,支持大部分开箱即用的构建项目所需要的功能。
标准化的项目结构,具有全面的测试功能(单元测试和e2e测试),代码脚手架,支持使用环境特定配置的生产级构建。这是一个强大功能的脚手架,为我们每个新项目节省了大量的时间。这里要感谢Angular团队
!
虽然Angular CLI
从一开始就非常好用,也有很多吐槽点(吐槽在最后),但是我们可以利用一些潜在的配置改进和最佳实践来使我们的项目更加完善!
我们需要了解什么?
- 如何规划我们的模块
- 如何使用应用程序和环境文件夹的别名来支持快捷导入
- 如何使用Sass和Angular Material(或者NG-ZORRO,或者自己打造UI组件库)
- 如何建立良好的生产构建
- 如何不使用Phantom JS,而使用Headless Chrome代替测试
- 如何通过自动生成的更新日志和正确的版本号来管理我们的项目
- 如何通过CLI配置代理API请求
安装和使用(需要科学上网,不然以下安装会有各种问题)
安装nodejs
官网下载即可,注意:下载v8.x版本
安装 Angular CLI
npm install -g @angular/cli
注意:Windows下面安装angular-cli有两个典型的坑,一个是node-sass被墙了,第二个就是node-gyp依赖于某些API,必备需要安装:python2.7(一定要2.7)、Visual Studio(包含VB,C++等,不过有点大10g左右)
如果安装不成功可以使用以下方式:
npm i -g cnpm
cnpm i -g @angular/cli
创建新项目
ng new 项目名称
cd 项目名称 (自动去npm install 安装angular/cli提供的依赖包)
ng serve
注意:ng new my-app 失败?npm-gyp没安装,环境不行- Environment setup and configuration
1. 规划模块--基于核心模块,共享模块和特性模块与懒加载的模块结构的最佳实践
我们使用Angular CLI生成了新的项目,那现在呢?我们应该继续生成我们的服务和组件到一些随机的文件夹。接下来该如何构建我们的项目?
一个好的设计方式是把我们的应用程序分成至少三个不同的模块 - 核心模块,共享模块和特性模块(尽管我们可能需要多个特性模块)【特性模块就是我们常说每个功能页面】,如果我们不想使用第三方UI组件库,那我们还需要一个UI模块来修饰。
CoreModule【核心模块】
在Angular官网模块部分也专门指出用核心模块,只在应用启动时导入它一次,而不会在其它地方导入它。
在所有每个应用程序(单例服务)上必须有且仅有一个实例的服务应该在这里实现。典型的例子可以是认证服务或用户服务。我们来看一个CoreModule实现的例子。
core.module.ts
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
/* 我们自己的定制全局服务 */
import { UserService } from './user/user.service';
@NgModule({
imports: [
HttpClientModule
],
providers: [
/* 我们自己的定制全局服务 */
SomeSingletonService
]
})
export class CoreModule {
/* 确保CoreModule只能在AppModule导入 */
constructor (
@Optional() @SkipSelf() parentModule: CoreModule
) {
if (parentModule) {
throw new Error('CoreModule is already loaded. Import only in AppModule');
}
}
}
SharedModule 【共享模块】
在Angular官网模块部分也专门指出用共享模块,只在特性模块里导入它一次,而不需要再去导入其他Angular核心模块和第三方模块,我们自定义组件,指令,管道。
所有的应用组件,指令和管道应该在这里管理。这些组件不会在其构造函数中从核心或其他功能导入和注入服务。他们应该通过使用它们的组件模板中的属性来接收所有的数据。这一切都归结于SharedModule对我们的应用程序的其余部分没有任何依赖性的事实。这也是导入和导出UI组件,业务通用组件的理想场所。
shared.module.ts
/* Angular核心模块 */
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
/* 第三方组件 */
import { MdButtonModule } from '@angular/material';
/* our own custom components */
import { SomeCustomComponent } from './some-custom/some-custom.component';
@NgModule({
imports: [
/* Angular核心模块 */
CommonModule,
FormsModule,
/* 第三方组件 */
MdButtonModule,
],
declarations: [
ListComponent
],
exports: [
/* Angular核心模块 */
CommonModule,
FormsModule,
/* 第三方组件 */
MdButtonModule,
/* 自定义组件 */
ListComponent
]
})
export class SharedModule { }
CoreModule和SharedModule区别
Module属性 | CoreModule | SharedModule |
---|---|---|
imports | 必须 | 必须 |
providers | 必须 | 禁止 |
declarations | 禁止 | 必须 |
exports | 禁止 | 必须 |
总结:CoreModule 只有导入没有导出,SharedModule有导入导出,却没有服务依赖注入管理,这个需要在 CoreModule 里面操作。
使用Angular CLI构建项目结构
我们可以在创建新项目后立即生成Core和Shared模块。这样,我们将准备从一开始就生成额外的组件和服务。
运行ng generate module core
可以生成模块核心。具体规则可以看这里ng generate
然后在core
文件夹中创建index.ts
文件,并重新导出CoreModule本身。
代码:export * from './core.module';
在进一步开发的过程中,我们会再出口更多的公共服务,这些服务应该在index.ts
提供。
core/index.ts
export * from './user/user.service';
export * from './core.module';
如何访问:
app.module.ts
import { CoreModule } from './core';
好处我不需要关心里面的CoreModule 所在文件位置,只需要关心我对于的导出依赖名称即可。
同样,我们可以为共享模块做同样的事情。
FeatureModule 【特性模块】
在Angular官网模块部分也专门指出用特性模块,特性模块是带有@NgModule装饰器及其元数据的类,就像根模块一样。 特性模块的元数据和根模块的元数据的属性是一样的。根模块和特性模块还共享着相同的执行环境。 它们共享着同一个依赖注入器,这意味着某个模块中定义的服务在所有模块中也都能用。
它们在技术上有两个显著的不同点:
- 我们引导根模块来启动应用,但导入特性模块来扩展应用。
- 特性模块可以对其它模块暴露或隐藏自己的实现。
特性模块用来提供了内聚的功能集合。 聚焦于应用的某个业务领域、用户工作流、某个基础设施(表单、HTTP、路由),或一组相关的工具集合。
将为我们的应用程序的每个独立功能创建多个功能模块。功能模块应该只从CoreModule导入服务。
如果功能模块A需要从功能模块B导入服务,则考虑将该服务移入CoreModule。
在某些情况下,需要仅由某些功能共享的服务,将它们转移到核心并不合理。在这种情况下,我们可以创建特殊的共享功能模块,不依赖于CoreModule提供的服务和SharedModule提供的组件的任何其他功能的功能。
这将保持我们的代码清洁,易于维护和扩展的新功能。这也减少了重构所需的工作量。如果正确执行,我们将确信更改一个功能不会影响或破坏我们的应用程序的其余部分。
LazyLoading 【懒加载模块】
懒加载需要配合路由完成. 可以看伪代码
app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'user',
loadChildren: 'app/user/user.module#UserModule'
},
{
path: '**',
redirectTo: 'user'
}
];
@NgModule({
// useHash支持带#号的url地址,
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule {}
我们应该尽可能延迟加载我们的功能模块。理论上,应用程序启动期间只能同步加载一个功能模块以显示初始内容。在用户触发导航之后,每个其他功能模块应该被延迟加载。
附上一张自己YY的应用架构图
2. 为应用程序和环境文件夹设置别名
别名我们的应用程序和环境文件夹将使我们能够实施干净的入口,这将在整个应用程序中保持一致。
考虑假设的,但通常的情况。我们正在研究一个功能A中位于三个文件夹深处的组件,并且我们要从位于两个文件夹深处的核心导入服务。这将导致导入语句看起来像import {SomeService} from '../../../core/user/user-settings/user-settings.service'这样。
是不是很不爽。。。
更不爽的是,任何时候我们想改变这两个文件中的任何一个的位置,我们的导入语句就会中断,需要重新去改引入url地址,有些编辑器可以帮我们重构这个问题,例如:Webstorm。
曾经看过vue-cli,在它里面构建里面路径是‘@/xxx’ 如果没有记错,(ps:因为快一年没有使用了)。应该是这样的,@代表src,这个是webpack里面设置的别名功能。
在Angular-cli去改Webpack配置是一个很麻烦的事情,我们可以修改tsconfig.json
配置。
为了能够使用别名,我们必须添加baseUrl
和paths
属性到我们的tsconfig.json文件中,像这样...
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@app/*": ["app/*"],
"@env/*": ["environments/*"]
}
}
}
注意:如果报错找不到@app/xxxx模块,需要把 "baseUrl": "src"
换成"baseUrl": "./src"
。
我们还添加了@env别名,以便能够使用
import { environment } from "@env/environment"
从我们的应用程序的任何位置轻松访问环境变量。它将适用于所有指定的环境,因为它会根据传递给ng build命令的--env标志自动解析正确的环境文件。
随着我们的路径,我们现在可以导入像这样的环境和服务...
user.module.ts
/* Angular核心模块 */
import { NgModule } from '@angular/core';
/* 共享模块 */
import { SharedModule } from '@app/shared';
import { environment } from '@env/environment';
@NgModule({
imports: [
/* 共享模块 */
SharedModule
],
providers: [
{
provide: 'USER_API', useValue: environment.production ? '/api/test':'/api'
}
]
})
export class UserModule { }
您可能已经注意到我们直接从@app/core
而不是@app/core/user/user .service
导入实体(如上例中的UserService)。这是可能的,这要归功于重新导出主index.ts
文件中的每个公共实体。我们创建一个index.ts文件每个包(文件夹),他们看起来像这样...
export * from './core.module';
export * from './user/user.service';
在大多数应用程序中,特定功能模块的组件和服务通常只需要访问来自CoreModule的服务和来自SharedModule的组件即可。有时这可能不足以解决特定的业务案例,我们还需要某种“共享功能模块”,为其他功能模块的有限子集提供功能。
在这种情况下,我们将最终得到来自import { FeatureService } from '@app/shared-feature';
因此与核心类似,也使用@app别名访问共享特征。
3. 使用Sass 和第三方UI组件
Sass是一个样式预处理器,它支持像变量这样的强大的东西(即使css也会很快得到变量),函数,mixins 等,你把它命名为...
Sass还需要有效地使用官方Angular Material Components库以及其广泛的主题功能。假设使用Sass是大多数项目的默认选择是安全的。
要使用Sass,我们必须用--style scss标志来使用Angular CLI生成我们的项目,或者在defaults和styleExt来设置。默认情况下,没有添加的是stylePreprocessorOptions和includePaths,我们可以使用强制性的根“./”和可选的“./themes”值来设置。
angular-cli.json
{
"apps": [
{
...
"stylePreprocessorOptions": {
"includePaths": ["./", "./themes"]
}
"defaults": {
"styleExt": "scss"
}
}
]
}
常用的UI组件:
- Angular Material:官方提供,基于Google Design
- NG-ZORRO:阿里提供,基于Ant Design
想要快速熟悉Angular,推荐自己撸一套UI组件库。
4. 如何建立良好的生产构建
Angular CLI生成的项目只带有一个非常简单的ng build脚本。要生成生产级的工件,我们必须自己做一些定制。
我们在package.json脚本中添加“build:prod”:“ng build --target production --build-optimizer --vendor-chunk”
。
发布生产
这是一个设置开关,它使代码缩小和默认情况下很多有用的构建标志。这相当于使用以下...
--environment prod
使用environment.prod.ts
文件的环境变量--aot
启用前期编译。这是目前版本的Angular CLI的默认设置。如果你使用低版本,必须手动启用它--extract-css true
将所有的CSS提取到独立的样式表文件中--sourcemaps false
禁用压缩文件对应map的生成--named-chunks false
禁用使用人类可读名称的块和使用数字
其他有用的设置
--build-optimizer
新功能,导致更小的捆绑,但更长的构建时间,所以谨慎使用!(也应该在未来默认启用)--vendor-chunk
将所有第三方依赖(库)代码提取到单独的块中
另外检查其他可用的配置标志官方文档,这可能在您的个人项目中有用。
5. 使用Headless Chrome代替Phantom JS
PhantomJS是一个非常知名的headless browser(ps: 无头浏览器 很恐怖),它实际上是用于在CI服务器和许多开发机器上运行前端测试的解决方案。
虽然还算不错,但对现代ECMAScript功能的支持还是比较滞后的。更为严重的是,这些非标准的行为在本地没有问题地通过测试的时候就曾多次引起头痛,但是仍然打破了CI的环境。
幸运的是,我们不必再处理它了!
正如官方文件所说...
Headless Chrome正在使用Chrome 59.这是在无头环境下运行Chrome浏览器的一种方式。本质上,在没有Chrome的情况下运行Chrome!它将Chromium和Blink渲染引擎提供的所有现代Web平台功能带到命令行。
那么我们如何在Angular CLI项目中使用它?
我们在项目的package.json
添加如下代码...
package.json
"scripts": {
"test": "npm run lint && ng test --single-run",
"watch": "ng test --browsers ChromeHeadless --reporters spec",
},
6. 使用标准的提交消息
可以看这里我的这篇文章GET新技能之Git commit message。
快速总结一下我们感兴趣的项目的新功能和缺陷修复是非常好的。
让我们为用户提供相同的便利!
手动编写更改CHANGELOG.md
将是极其繁琐的容易出错的任务,因此最好是自动执行该过程。
有很多可用的工具Conventional Commits specification可以完成这项工作,但我们只关注标准版本。
常规提交定义了强制类型,可选(范围):后跟提交消息。也可以添加可选的正文和页脚,两者都用空行分隔。通过查看angular-cli的完整提交消息的示例,我们来看看在实践中该如何实现。
feat(@angular/cli): move angular-cli to @angular/cli (#4328)
由于BREAKING CHANGE关键字在提交主体中的存在,标准版本将正确地冲击项目的MAJOR版本。
生成的CHANGELOG.md将会看起来像这样...
v1.6.0-beta.2
Bug Fixes
@ngtools/webpack: fix elide removing whole imports on single match (62f3454), closes #8518
v1.5.2
Bug Fixes
@ngtools/webpack: fix elide removing whole imports on single match (62f3454), closes #8518
看起来是不是很酷呀!那么我们怎样才能在我们的项目中使用这个?
我们首先安装npm install -D standard-version
将其保存在我们的devDependencies中,并将“release”:“standard-version”
添加到我们的package.json
scripts中。
package.json
"scripts": {
"release": "standard-version"
},
我们还可以添加git push
和npm publish
来自动完成整个过程。
在这种情况下,我们需要改进脚本为下面例子
package.json
"scripts": {
"release": "standard-version && git push --follow-tags origin master && npm publish"
},
注意:我们使用
&&
并将依赖于平台的命令链在基于Unix的系统上(因此也在Windows上使用Cygwin、Gitbash或Linux的新Win10子系统)。
7. 如何通过CLI配置代理API请求
目前开发都是前后分离式的开发,前端使用CLI启动服务端口一般都是4200,后台也有API端的,nodejs一般常用都是3000,如果直接去请求3000端口的数据就好出现跨域请求。
跨域请求(同源策略)
简单理解跨域:不同的协议(http|https),不同的IP,不同的端口
本地开发ip都一样,不一样的是端口号,这样跨域浏览器会有
这样的报错信息,那么我们需要用代理,代理方式有很多。这里不介绍其他就说CLI里面如何配置的。
CLI如何配置
- 在根目录下新建一个
proxy.conf.json
,这个文件名字不需要固定(我为了适应不同场景,还是做不同代理配置,proxy-dev.conf.json,proxy-test.conf.json,,因为后台很多,有时候需要直接去联调他们电脑服务) - 代理配置信息
假设:跨域请求地址是 http://localhost:8000/api/user/123
{
"/api": { // 这个是必须的,相当一个标识 target地址下一级文件夹目录 上面跨域请求是api 那么这里就是`/api`
"target": "http://127.0.0.1:8000", // 你需要代理的地址 注意:只需要ip和端口号就好了
"secure": false, // 安全,自己联调,可以关了
"changeOrigin": true, // 如果不是代理本机需要设为true,不然可以不设置
"logLevel": "debug" // 这是调试,如果代理成功,命令行会出现每次请求的地址
}
}
Angular里面使用 (默认服务是http://localhost:4200)
this.http.get('http://localhost:4200/api/user/123')
package.json
文件里的scripts
也需要配置
代理之前的:
"serve": "ng serve --open"
代理之后的:
"serve": "ng serve --proxy-config proxy.conf.json --open"
- 在启动
npm start
或者npm run serve
就可以运行代理了
注意:
/api
一定要有,不然就会报错,鉴于这样的情况我写了一个本地代理,来转发。遇到神一样队友,没有办法。
附上nodejs转发API源码:
新建一个dev.js
// 获取依赖包
const express = require('express');
const bodyParser = require('body-parser');
const superagent = require('superagent');
const path = require('path');
// 实例化express
const app = express();
// 解析json
app.use(bodyParser.urlencoded({
extended: false
}));
app.use(bodyParser.json());
/**
* 获取代理地址
*/
// 连接代理API地址 默认 测试代理地址
const baseUrl = `http://xxx.xxx.xxx.xxx:4000/`;
// 错误处理默认返回
const error = {
"code": "0003",
"data": null,
"field": null,
"msg": null
};
// 代理请求处理
app.post(`/api/*`, (req, res, next) => { // 我的神队友只有一个请求方式,请求方式参考`express` 路由
superagent
.post(baseUrl + req.params[0])
.send(req.body)
.set('Accept', 'application/json')
.end(function(err, results) {
// 如果出错就直接返回默认错误json
if (err) {
console.log('superagent error:------------------------------------')
console.log(JSON.stringify(err))
return res.json(error);
}
// 因为JSON.parse解析非法json会抛出异常,需要用try catch来捕获,如果出错了就直接跑错返回
try {
const data = JSON.parse(results.text);
return res.json(data);
} catch (error) {
console.log('results error:------------------------------------')
console.log(JSON.stringify(err))
return res.json(error);
}
});
});
app.use(function(err, req, res, next) {
console.error(err.stack);
res.send(500, 'Something broke!');
});
app.listen(8000, () => console.log('Express server listening on http://localhost:8000 proxy url ', baseUrl));
解释:
CLI默认带express
相关的包,自己去下载一个superagent
请求包,还有一个并行处理npm命令的包concurrently
这里可以修改一下脚本命令:
"start": "concurrently \"npm run serve:dev\" \"npm run serve\"",
"serve": "ng serve --proxy-config proxy.conf.json --open",
"serve:dev": "node dev.js dev",
就可以一个命令来控制。
其他相关技巧(待续,不断完善中...)
推荐vscode插件
- Visual Studio Code Commitizen Support
git commit message书写规范提示模板
- Angular5 Snippets - TypeScript, Html, Angular Material, ngRx, RxJS & Flex Layout
ng4/5非常不错简写提示插件
Prefix | Description |
---|---|
ng- | Angular Snippets |
fx- | Angular Flex Layout Snippets |
ngrx- | Angular NgRx Snippets |
m- | Angular Material Design Snippets |
md- | Angular Material Design Snippets for all versions before 2.0.0-beta.11 |
rx- | RxJS Snippets for both TypeScript and JavaScript |
关于模拟API请求数据
如果只想做简单演示,模拟API请求,熟悉HTTP请求,又不想起一个本地后台服务器或者模拟mack服务器,那怎么办?
- 我们需要修改angular-cli脚手架核心配置
.angular-cli.json
"assets": [
"api",
"assets",
"favicon.ico"
],
- 去
src
文件夹下创建一个api
文件夹,你需要本地数据都可以丢里面。
为什么是src,因为脚手架里面配置有一句
"root": "src"
,表示根路径。
- 在服务里面写HTTP请求
在api文件里创建了一个test.json
,写点假数据吧,那我们怎么去请求了?
constructor(private http: HttpClient) { }
getTest() {
this.http.get('/api/test.json').subscribe((data) => {
console.log(data);
});
}
注意:/api就是前面创建的src/api
文件夹,因为src
是根目录,所以我们只需要/api
即可
自动升版本和changelog很不错
@paddingme 是很不错,你可以看这里面的GET新技能之Git commit message 如果配置好了,提交人员不按写要求git commit message根本不能提交代码。是不是很凶残。哈哈
看起来好费力,太长了。建议将该文整理成目录式,仅提供相关推文的链接。
@courage007 你这建议不错