Angular(Angular 2+ )是一套现代的 WEB 开发框架,它采用模块化开发,提供一套完整的开发支持,使开发者能更专注于业务逻辑,提高生产效率。 CMS(内容管理系统),提供对内容的增、删、改、查等功能。 本文介绍如何用 Angular 搭建一个 CMS 系统,文章重点关注流程,更多技术细节请参考 。
目标
实现简易用户管理功能,查看。
- 编辑页:支持新建用户,支持修改用户信息
- 列表页:展示用户数据,支持分页查询,支持删除用户
搭建环境
确保设备已安装 , 且满足 node 8.x 和 npm 5.x 以上的版本。
安装 。它包含一套命令行指令,可以帮助开发者快速创建项目、添加文件、以及执行项目运行、打包等任务。
npm install -g @angular/cli
创建Angular项目
使用 Angular CLI 提供的ng new
命令创建一个新项目。Angular CLI 会在当前目录创建一个指定命名的新项目,创建过程中会自动安装项目所需依赖,如果在公司内网这一步需要配合代理进行。运行下列命令创建并启动一个 CMS 项目。
ng new cmscd cmsng serve --open
使用--open
,在编译完成后会自动打开浏览器并访问 :4200/,可以看到一个 Angular 项目启动了。其他比较常用的是参数有,
--port 指定端口号--proxy-config 代理配置文件--host fe.cms.webdev.com /*在需要读取cookie的情况下会有用*/
搭建页面骨架
模块与组件
Angular 采用模块化的开发方式。
模块是一组功能的集合。模块把若干组件、服务等聚合在一起,它们共享同一个编译上下文环境。页面的每一个小部分都可以看作是一个组件。组件包含组件类和组件模版。模版负责组件的展示,可以使用 Angular 的模版语法对 html 进行修改。组件类实现组件的逻辑部分,可以通过注入服务去实现一些数据交互逻辑。
Angular CLI 初始化项目中有唯一的一个模块—— AppModule 模块。它是一个根模块,页面从这里启动,它下面可以包含子模块和组件。为了演示方便,在项目中不再新建模块,只通过组件去实现不同页面的展示。
新建两个组件:list 负责数据管理,edit 负责表单编辑。除此之外,还需要一个 nav-side 组件作为页面导航,负责 list、edit 的切换。用 命令创建这三个组件。下面几个命令是等价的。
ng generate component nav-sideng g component editng g c list
试试将它们添加到页面中,在模版中创建它们。
在页面上可以看到,这三个组件都被创建了。但我们需要在不同情况下分别展示 list 和 edit 组件,可以通过引入路由模块来实现。
路由
Angular 的
1、定义路由。Angular 路由(Route)是一个包含 path 和 component 属性对对象数组。path 用来匹配URL路径,component 则告诉 Router 在当前路径下应该创建哪个组件。2、添加路由出口。在页面上添加Router
模块提供了对路由对支持。在 Angular 中使用路由至少要做如下两个配置:<router-outlet>
元素,当路由到的某个组件时,会在当前位置展示组件的视图。
定义页面需要的路由。Edit 路由上定义了一个id参数,通过它可以把用户ID传给组件。
import { RouterModule, Routes } from '@angular/router';const appRoutes: Routes = [ { path: 'list', component: ListComponent }, { path: 'edit/:id', component: EditComponent }, { path: 'edit', redirectTo: 'edit/create', pathMatch: 'full'}, { path: '', redirectTo: '/list', pathMatch: 'full'} // 默认定向到list];@NgModule({ imports: [ RouterModule.forRoot(appRoutes), // other imports here ], ...})export class AppModule { }
在模版中定义路由出口,之前的 edit 和 list 模块被路由出口代替。当路由匹配 edit 或 list 时,它们会在router-outlet
的位置被创建。
在 nav-side 中使用路由跳转。绑定routerLink
属性,下面使用两种方式,后一种方式支持传入更多参数。此外还绑定了routerLinkActive
属性,它支持传入CSS类,当当前路由被激活时CSS类就会被添加。
现在我们会看到页面效果如图。点击侧边栏,可在列表页和编辑页之间来回切换。
至此,页面骨架搭建完成。
列表页实现
简单梳理列表页需要实现的内容。
- 功能拆分:数据展示、查询、删除
- 页面划分:表格、分页、搜索框
数据定义
在开始页面实现之前,需要做一些准备工作,首先需要设计列表页的数据。
Angular项目中默认使用TypeScript开发,在TS中我们可以通过Interface实现数据类型的定义。 定义Interface的好处在于可以规范数据类型,编辑器及代码编译阶段都会对数据类型做检查,可以减少由于类型而导致的问题的产生,明确的类型定义也便于后期维护。
新建一个data.interface.ts
文件,并定义用户、列表、分页、列表搜索参数的数据格式。
export interface IUser { id?: number; nick: string; sex: 'male'|'female';}export interface IList { data: IUser[]; pager: IPager}export interface IPager { currPage: number; totalPage: number;}export interface ISearchParams { page?: number; keyword?: string;}
数据模拟
在一些场景下,为了模拟数据请求,前端需要实现mock接口的功能。Angular提供了进行数据模拟。
我们可以创建项目中需要的一组数据,然后通过 REST API 请求获取数据。我们可以按照真实接口的样式去实现请求方法,在真正的接口准备好之后,只需要移除in-memory-data
,就可以实现真实与模拟请求的无缝替换。 下面我们定义需要的数据。
import { InMemoryDbService } from 'angular-in-memory-web-api';export class InMemoryDataService implements InMemoryDbService { createDb() { const users = [ { id: 12, nick: 'Narco', sex: 'male' }, { id: 13, nick: 'Bombasto', sex: 'male' } ... ]; return {users}; }}
数据请求
HttpClient
Angular中实现HTTP请求需要引入
HttpClientModule
。HttpClient
提供了一组 API 用来实现 HTTP 请求,并返回一个 Observable 类型的对象,可以对返回数据做流式处理,如错误拦截、数据转化等。
新建data.service.ts
,用来实现数据请求。
在获取数据列表的请求中,我们使用map
操作符对数据进行处理,获取需要的对应分页下的数据。
import { Injectable } from '@angular/core';import { HttpClient, HttpParams } from '@angular/common/http';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';import { IList, IUser, ISearchParams } from './data.interface';@Injectable({ providedIn: 'root',})export class DataService { private url = 'api/users'; constructor(private http: HttpClient) {} getList(params: ISearchParams): Observable{ let currPage = params.page, totalPage: number, limit = 6; return this.http.get (this.url, { params: new HttpParams().set('nick', params.keyword) }).pipe( map((data: IUser[]) => { return { // 模拟分页 data: data.slice((currPage-1)*limit, (currPage)*limit), pager: { currPage: currPage, totalPage: Math.ceil(data.length / limit) } } })) } getUser(id: number): Observable { return this.http.get (`${this.url}/${id}`) } deleteUser(id: number): Observable { return this.http.delete (`${this.url}/${id}`) } addUser(data: IUser): Observable { return this.http.post (this.url, data) } updateUser(data: IUser): Observable { return this.http.put (this.url, data) }}
在AppModule中引入发送数据请求需要的HttpClientModule
和本地数据获取需要的HttpClientInMemoryWebApiModule
。
import { HttpClientModule } from '@angular/common/http';import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';import { InMemoryDataService } from './in-memory-data.service';@NgModule({ imports: [ HttpClientModule, HttpClientInMemoryWebApiModule.forRoot( InMemoryDataService, { dataEncapsulation: false } ) // other imports here ], ...})export class AppModule { }
组件实现
下一步,需要在 list 组件内调用 DataService 获取列表数据并展示。这里使用到了 Angular ——ngOnInit
,在组件 Init 之后执行页面逻辑。
接下来会使用到 Observale 和 RXJS 操作符,相关知识点参考 ,
由于 DataService 返回一个包含列表数组及分页信息的 Observable 类型的数据,我们需要将这两部分数据分离并展示。下面代码中,通过一系列流的操作,我们把分页数据提取给了 pager 对象,列表数组使用一个 Observable 类型的对象表示—— listData$。
将 listData$ 绑定到模版上,通过async
[pipe](https://angular.io/guide/pipes)可以实现 Observable 的订阅。Observable 在被订阅后,每次更新 Observer 都会受到新数据,即页面上的数据都会刷新。由于 updateList$ 是BehaviorSubject
类型,只需要调用next
方法即可实现数据的刷新。 export class ListComponent implements OnInit { pager: IPager = { currPage: 1, totalPage: 1 } as IPager; listData$: Observable; updateList$: BehaviorSubject = new BehaviorSubject (1); constructor(private service: DataService) { } ngOnInit() { this.listData$ = this.updateList$ .pipe( switchMap((page: number) => { // 获取列表数据 return this.service.getList(Object.assign({ page: page }, this.searchForm.form.getRawValue())).pipe( catchError(() => of({ data: [], pager: { currPage: 1, totalPage: 1 } }))) }), tap((list: IList) => { this.pager = list.pager }), map((list: IList) => list.data) ) } //删除用户 deleteUser(id: number) { this.service.deleteUser(id).subscribe(() => { //刷新列表 this.updateList$.next(this.pager.currPage); }) }}
ID | 昵称 | 性别 | 操作 |
---|---|---|---|
{ {data.id}} | { {data.nick}} | { {data.sex === 'male'? '男': '女'}} | 编辑 删除 |
组件间数据交互
分页组件
实现一个简单的分页组件,展示当前页码和总页数,并提供一个输入框可以填写需要跳转到的页面。
新建一个 pagination 组件。组件接收 IPager 类型的参数,并展示 pager 内容。当跳转按钮被点击时,向外发出 pageChange 事件,并把需要跳转到的页码给出。父组件( ListComponent )需要在模版中给 pagination 组件传入 pager 属性的值,并监听 pageChange 事件。这里使用了 Angular 的@Input
、@Output
定义了组件的输入输出属性。
对于回车跳转的方式,可以直接监听 Input 上的 keyup 事件,也可以通过 RXJS 的fromEvent
监听 keyup 事件,当监听到回车时调用页面跳转方法。
export class PaginationComponent implements OnInit { targetPage: number; @Input() pager: IPager; @Output() pageChange: EventEmitter= new EventEmitter (); ngOnInit() { fromEvent(document.getElementById('input'), 'keyup') .pipe(filter((event: KeyboardEvent) => event.key === 'Enter')) .subscribe(() => { this.onPageChange(); }) } onPageChange() { this.pageChange.emit(+this.targetPage); this.targetPage = null; }}
跳转 { {pager.currPage}} / { {pager.totalPage}}
onPageChange(page: number) { this.updateList$.next(page);}
搜索组件
对于搜索组件,它需要将搜索表单内容与列表页共享,这里通过@ViewChild
的方式共享数据,它提供了父组件获取子组件实例的方法,通过组件实例可以获取到组件内的属性。
新建 searh-form 组件,使用 的模式构建一个搜索表单。
import { Component, OnInit, Output, EventEmitter } from '@angular/core';import { FormGroup, FormBuilder, Validators } from '@angular/forms';...export class SearchFormComponent implements OnInit { form: FormGroup; @Output() search: EventEmitter= new EventEmitter (); constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({keyword: ['']}); } onSubmit() { this.search.emit(); }}
@ViewChild(SearchFormComponent) searchForm: SearchFormComponent;ngOnInit() {this.listData$ = this.updateList$ .pipe( switchMap((page: number) => { return this.service.getList(Object.assign({ page: page }, this.searchForm.form.getRawValue())).pipe( catchError(() => of({ data: [], pager: { currPage: 1, totalPage: 1 } }))) }), tap((list: IList) => { this.pager = list.pager }), map((list: IList) => list.data) )}onSearchDataChange() { this.updateList$.next(1);}
至此,我们实现了用户的展示、查询、删除操作,列表页完成。
编辑页实现
简单梳理编辑页需要实现的内容。
- 功能拆分:数据新增、修改
- 页面划分:标题、表单
标题
在编辑页需要根据用户ID区分是否新建用户。在路由配置中我们已经配置了编辑页最后一个参数为ID,并设置对于新建用户(没有用户ID)的情况下路由统一跳转到 create。因此我们需要在页面中获取路由ID参数,根据是否 create 判断是否为新建用户,并保存用户ID。
这里采用了监听路由参数的方式来获取路由参数,在页面URL发生改变时,用户ID会及时更新。userId: string;construct( ... private route: ActiveRoute) { this.route.paramMap.subscribe((params: ParamMap) => { this.userId = +params.get('id') || null; })}
{ {!userId? '新建用户': ('编辑用户 - ')}}{ {userId}}
表单
新建
同样的,我们引入 Reactive-Form 模块,通过数据模型来渲染表单。这里我们加入了表单校验配置,设置 nick 和 sex 都必填,校验结果可以通过invalid
方法获取。并且在校验失败时,将提交按钮置灰。
表单数据的提交就是请求 DataService 的 addUser 方法,可以在提交成功后通过路由方法跳转到列表页。
ngOnInit() { this.userForm = this.fb.group({ nick: [null, Validators.required], sex: [null, Validators.required] })}onSubmit() { this.dataservice.addUser(this.userForm.getRawValue()).subscribe(() => { this.router.navigate(['/list']); })}
修改
在用户ID存在时,需要获取用户信息进行展示。DataService 已经实现了数据获取方法,在拿到用户信息后,可以通过patchValue
对 userForm 的数据进行修改。
construct( ... private route: ActiveRoute) { this.route.paramMap.subscribe((params: ParamMap) => { this.userId = +params.get('id') || null; this.userId && this.getFormData(); })}private getFormData() { this.dataservice.getUser(this.userId).subscribe((data) => { this.userForm.patchValue({nick: data.nick, sex: data.sex}); }) }onSubmit() { let submitType = this.userId? 'updateUser': 'addUser'; let formData = this.userForm.getRawValue(); this.userId && (formData.id = this.userId); this.dataservice[submitType](formData).subscribe(() => { this.router.navigate(['/list']); })}
项目打包及部署
如果需要把项目打包并部署到服务器上,只需要运行ng build
命令即可完成打包,可以配置--prod
参数以选择 的方式打包。打包后的文件会被保存在angular.json
中配置的outputPath
路径下。
最后
整套流程下来,我们构建了一个简单但是完整的 CMS 系统,涉及了 Angular 中大部分基础知识点。后续可参考官方文档,增强系统功能,运用更多 Angular 特性。