一、介绍
通过本文学习,能够掌握搭建Angular开发环境及快速项目构建。
本文主要使用Angular7.x + Ng-zorro-ant(v7.5.0)组件库进行搭建。Angular框架基础核心原理并不在文中进行太多介绍,主要文档参考请链接至官方中文文档。
代码开发编辑器:Visual Studio Code
所需掌握技术点:ES6、Typescript、html、Less、Rxjs
二、Angular开发环境搭建
安装Angular-CLI
注:在安装Angular-CLI之前,请确保你的设备安装了Node.js v10.9.0以上版本。
- 使用npm命令安装angular/cli
npm install -g @angular/[email protected]
- 安装成功后,运行以下命令即可查看是否安装成功
ng version / ng v 查看脚手架版本信息
- 出现以下画面表示已安装成
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 7.3.6
Node: 11.12.0
OS: darwin x64
Angular:
...
Package Version
------------------------------------------------------
@angular-devkit/architect 0.13.6
@angular-devkit/core 7.3.6
@angular-devkit/schematics 7.3.6
@schematics/angular 7.3.6
@schematics/update 0.13.6
rxjs 6.3.3
typescript 3.2.4
三、创建项目并运行
- 运行CLI命令创建,如下所示
ng add ngExample
接下来,ng new命令会提示你添加初始项目所要包含的特性,默认选择后需等待CLI安装Angular包及其他依赖包。
项目创建成功后,进入项目根目录运行以下指令,向项目中添加ant组件库
cd ngExample
ng add ng-zorro-antd
输入命令后,选择ng-zorro安装特性,等待安装成功。
- 使用VSCode编辑工具,打开项目文件夹,在VSCode终端中运行以下CLI命令
ng serve
在终端中看到Compiled successfully,表示已经成功启动Angular项目,具体参考以下图片中内容:
在浏览器中访问localhost:4200,目前默认端口为4200,后续有需要可根据实际情况在CLI启动命令中变更端口。
到此为止,在浏览器中看到以下页面时,整个环境搭建以及简单项目构建已经完成,即将进入本文案例开发。
四、项目框架技术点
4.1 目录结构
由于初始化项目目录结构并不能很好的满足开发规则,因此对目录结构进行了优化,如下所示
目录 | 说明 |
---|---|
src/app/core | 核心组件目录,例如HTTP拦截器、国际化服务等 |
src/app/layout | 层级组件目录,例如主框架页面、页面头部组件等 |
src/app/routes | 业务组件目录,包含主体路由以及各业务模块路由 |
src/app/shared | 共享文件目录,项目中可多次且通用文件可在此目录中声明 |
src/assets | 资源目录,用于存放icon字体、国际化翻译文件、图片等 |
src/environments | 环境配置文件 |
src/styles | 自定义样式文件 |
除了上述优化后的目录介绍,其余文件介绍请参考Angular中文文档 - 项目文件结构
在tsconfig.json中配置模块引入时转为相对路径
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "src",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"module": "es2015",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
],
"paths": {
"@shared": [
"app/shared"
],
"@shared/*": [
"app/shared/*"
],
"@core": [
"app/core"
],
"@core/*": [
"app/core/*"
]
}
}
}
例子:
import { SharedModule } from '@shared/shared.module';
app目录
app目录是我们要编写的代码目录,我们写的代码都是放在这个目录。
一个Angular程序至少需要一个模块和一个组件,在我们新建项目的时候命令行已经默认生成出来了。
app
app.component.ts // 组件
app.module.ts // 模块
4.2 页面主体结构
4.2.1 主体结构开发
layout这个目录在Angular中属于一个模块,所以需要有layout.module.ts文件做承载,且要把该文件引入到根模块app.module.ts中。
- 在src/app/layout中创建default.component.html模板
- 在src/app/layout中创建default.component.ts文件
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'layout-default',
templateUrl: './default.component.html',
})
export class LayoutDefaultComponent {
}
该页面为上中下布局,中间层的
为路由加载位置。
4.2.2 优化页面结构
如何优化页面结构?
根据ng-zorro-ant组件库为我们提供了一些常用的页面布局组件,我们只需要简单步骤就能够替换。
- 替换default.component.html内容
- 用户列表
Home
这几个是ng-zorro-ant组件库所提供的已封装组件。
4.3 自定义组件
4.3.1 组件相关的概念
组件元数据装饰器(@Component)
简称组件装饰器,用来告知Angular框架如何处理一个TypeScript类.
Component装饰器包含多个属性,这些属性的值叫做元数据,Angular会根据这些元数据的值来渲染组件并执行组件的逻辑模板(Template)
我们可以通过组件自带的模板来定义组件的外观,模板以html的形式存在,告诉Angular如何来渲染组件,一般来说,模板看起来很像html,但是我们可以在模板中使用Angular的数据绑定语法,来呈现控制器中的数据。控制器(controller)
控制器就是一个普通的typescript类,他会被@Component来装饰,控制器会包含组件所有的属性和方法,绝大多数的业务逻辑都是写在控制器里的。控制器通过数据绑定与模板来通讯,模板展现控制器的数据,控制器处理模板上发生的事件。
/*这里是从Angular核心模块里面引入了component装饰器*/
import {Component} from '@angular/core';
/*用装饰器定义了一个组件以及组件的元数据 所有的组件都必须使用这个装饰器来注解*/
@Component({
/*组件元数据 Angular会通过这里面的属性来渲染组件并执行逻辑 */
// 这是css选择器,表示这个组件可以通过app-user来调用
selector: 'app-user',
// 组件的模板,定义了组件的布局和内容
templateUrl: './user.component.html',
// 该模板引用less样式
styleUrls: ['./user.component.less']
})
// UserComponent本来就是一个普通的typescript类,但是上面的组件元数据装饰器告诉Angular,UserComponent是一个组件,需要把一些元数据附加到这个类上,Angular就会把UserComponent当组件来处理
// 这个类实际上就是该组件的控制器,我们的业务逻辑就是在这个类中编写
export class UserComponent implements OnInit {
// 组件初始化时加载
ngOnInit() {
console.log('这是用户管理页面');
}
}
4.3.2 组件绑定数据
- 首先在
user.component.ts
中定义一个值title,随后在组件初始化时赋值
export class UserComponent implements OnInit {
title: string = '';
ngOnInit() {
console.log('这是用户管理页面');
this.title = ‘用户管理页’;
}
}
- 如何使用这个值?在
user.component.html
页面中使用双括号绑定
{{title}}
4.3.3 双向数据绑定
export class UserComponent {
value: string;
}
4.4 路由
4.4.1 路由定义
- 在
management.module.ts
中添加
const routes: Routes = [
{
path: '', component: ManagementComponent, data: { title: '管理' },
children: [
{ path: 'user', component: UserComponent, data: { title: '用户列表' } },
]
}
];
imports: [
RouterModule.forChild(routes)
],
4.4.2 路由模块引入
- 把刚定义好的路由
ManagementModule
放到routes-routing.module.ts
中
const routes: Routes = [
{ path: '', component: LayoutDefaultComponent,
children: [
{ path: 'management', loadChildren: 'app/routes/management/management.module#ManagementModule' },
],
}
];
4.4.3 路由使用
- routeLink
用户菜单
4.5 服务及创建观察者对象
什么是服务?定义公共的方法,使得方法在组件之间共享调用
Angular 把组件和服务区分开,以提高模块性和复用性。 通过把组件中和视图有关的功能与其他类型的处理分离开,你可以让组件类更加精简、高效。
4.5.1 创建服务并注册引用
- 在
management
目录中创建一个名为management.service.ts
文件,初始内容如下
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ManagementService {
constructor() {
}
}
- 服务注册
在management.module.ts
中注册
import { ManagementService } from './management.service';
@NgModule({
providers: [ManagementService]
})
- 服务引用
在user.component.ts
中引用
import { ManagementService } from '../management.service';
constructor(
private managementService: ManagementService
) { }
4.5.2 创建可观察对象
可观察对象支持在应用中的发布者和订阅者之间传递消息。 在需要进行事件处理、异步编程和处理多个值的时候,可观察对象相对其它技术有着显著的优点。
可观察对象是声明式的 —— 也就是说,虽然你定义了一个用于发布值的函数,但是在有消费者订阅它之前,这个函数并不会实际执行。 订阅之后,当这个函数执行完或取消订阅时,订阅者就会收到通知。
- Observable声明
export class ManagementService {
constructor(private http: HttpClient) {
}
getUsers(): Observable {
let u: User = new User();
u.name = 'zhangsan';
u.email = "[email protected]";
let users = [{ name: 'zhangsan', email: 'xxx' }];
users.push(u);
return of(users);
}
}
class User {
public name: string;
public email: string;
}
4.6 订阅
当创建了观察者对象后,需要通过subscribe
订阅的方式来实现功能。
list;
ngOnInit() {
this.managementService.getUsers().subscribe(data => {
this.list = data;
});
}
- 订阅成功后使用
ng-zorro-ant
表格组件现实,其中涉及到ng指令*ngIf
,指令具体使用方式请参考Angular中文文档。
Name
Email
{{ item.name }}
{{ item.email }}
4.7 表单
本文中使用了ng-zorro-ant
封装的表单组件,是具有数据收集、校验和提交功能的表单。
validateForm: FormGroup;
constructor(
private fb: FormBuilder
) { }
ngOnInit() {
//
this.validateForm = this.fb.group({
userName: [null, [Validators.required]],
email: [null, [Validators.required]]
});
}
在html中,
利用this.validateForm.controls[’userName‘].value
可获取到输入的值。
** FormGroup ** : 把每个子 FormControl 的值聚合进一个对象,它的 key 是每个控件的名字。 它通过归集其子控件的状态值来计算出自己的状态。
4.8 HTTP拦截器
其作用是可拦截HTTP请求返回响应状态码,针对状态码进行处理。
- 在core/net文件夹中创建以下拦截器
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpErrorResponse, HttpEvent, HttpResponseBase } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { mergeMap, catchError } from 'rxjs/operators';
import { NzMessageService, NzNotificationService } from 'ng-zorro-antd';
import { _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
const CODEMESSAGE = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
/**
* 默认HTTP拦截器,其注册细节见 `app.module.ts`
*/
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
constructor(private injector: Injector) { }
get msg(): NzMessageService {
return this.injector.get(NzMessageService);
}
private goTo(url: string) {
setTimeout(() => this.injector.get(Router).navigateByUrl(url));
}
private checkStatus(ev: HttpResponseBase) {
if (ev.status >= 200 && ev.status < 300) return;
const errortext = CODEMESSAGE[ev.status] || ev.statusText;
this.injector.get(NzNotificationService).error(
`请求错误 ${ev.status}: ${ev.url}`,
errortext
);
}
private handleData(ev: HttpResponseBase): Observable {
// 可能会因为 `throw` 导出无法执行 `_HttpClient` 的 `end()` 操作
if (ev.status > 0) {
this.injector.get(_HttpClient).end();
}
this.checkStatus(ev);
// 业务处理:一些通用操作
switch (ev.status) {
case 200:
break;
case 401: // 未登录状态码
// 请求错误 401: https://preview.pro.ant.design/api/401 用户没有权限(令牌、用户名、密码错误)。
(this.injector.get(DA_SERVICE_TOKEN) as ITokenService).clear();
this.goTo('/passport/login');
break;
case 403:
case 404:
case 500:
break;
default:
if (ev instanceof HttpErrorResponse) {
console.warn('未可知错误,大部分是由于后端不支持CORS或无效配置引起', ev);
return throwError(ev);
}
break;
}
return of(ev);
}
intercept(req: HttpRequest, next: HttpHandler): Observable> {
// 统一加上服务端前缀
let url = req.url;
if (!url.startsWith('https://') && !url.startsWith('http://')) {
url = environment.SERVER_URL + url;
}
const newReq = req.clone({ url });
return next.handle(newReq).pipe(
mergeMap((event: any) => {
// 允许统一对请求错误处理
if (event instanceof HttpResponseBase)
return this.handleData(event);
// 若一切都正常,则后续操作
return of(event);
}),
catchError((err: HttpErrorResponse) => this.handleData(err)),
);
}
}
- 拦截器使用
在Angular框架@angular/common/http
中,提供了HTTP_INTERCEPTORS
,我们可以通过自己定义的拦截器触发,做出对应业务处理。
放到app.module.ts
中
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true}
]
4.9 管道
什么是管道?Angular中的管道(pipe)是用来对输入的数据进行处理,如大小写转换、数值和日期格式化等。
4.9.1 日期格式化管道
管道以 | 符号右边表示。
{{today | date:'yyyy-MM-dd HH:mm:ss' }}
4.9.2 自定义管道
- 自定义管道的步骤:
使用 @Pipe 装饰器定义 Pipe 的 metadata 信息,如 Pipe 的名称 - 即 name 属性
实现 PipeTransform 接口中定义的 transform 方法
import { Pipe, PipeTransform } from '@angular/core';
[@Pipe](/user/Pipe)({ name: 'welcome' })
export class WelcomePipe implements PipeTransform {
transform(value: string): string {
if(!value) return value;
if(typeof value !== 'string') {
throw new Error('Invalid pipe argument for WelcomePipe');
}
return "Welcome to " + value;
}
}
- 自定义管道使用
{{ 'ngExample' | welcome }}
五、Angular性能优化
5.1 多模块懒加载
优化前我们工程就一个主模块文件(app.module.ts),路由跳转各页面其实都属于该模块一部分,假如路由对应各页面都是子组件的话,编译时都会被打包到同一个文件。
拆分模块,如下所示
const routes: Routes = [
{ path: '', component: LayoutDefaultComponent,
children: [
{ path: '', redirectTo: 'index/welcome', pathMatch: 'full' },
{ path: 'index', loadChildren: 'app/routes/index/index.module#IndexModule' },
{ path: 'management', loadChildren: 'app/routes/management/management.module#ManagementModule' },
],
}
];
可以看到写法明显不同,每个路由页面其实都是一个单独模块,然后在编译时每个模块都会单独编译成一个文件。而且路由到某个页面时,才会加载该模块js文件。
运行时可以从控制台发现以下信息,表示拆分成功
chunk {app-routes-index-index-module} app-routes-index-index-module.js, app-routes-index-index-module.js.map (app-routes-index-index-module) 6.02 kB [rendered]
chunk {app-routes-management-management-module} app-routes-management-management-module.js, app-routes-management-management-module.js.map (app-routes-management-management-module) 6.78 kB [rendered]
5.2 打包优化
在打包时添加命令
--prod –aot
优化编译方式。
六、心得体会
从最开始的技术选型以及到后续慢慢深入学习了解Angular框架之后,每一个疑问难点解决过程走过来,Angular都能为我带来惊喜。
Angular 以M(model数据)V(view视图/表现层)C(controller控制器/业务逻辑)为基础,降低前端重复工作的劳动量,扩展了HTML功能,组件复用性高。
其中以双向绑定数据为核心,Angular开发过程中基本上是在操作数据。
七、学习网站分享
Angular-CLI
ant组件库
Angualr中文文档
八、此文档项目源码
目前项目源码Angular框架已升级至Angular8
Github链接