Angular(Angular 2+ )是一套现代的 WEB 开发框架,它采用模块化开发,提供一套完整的开发支持,使开发者能更专一于业务逻辑,提升生产效率。
CMS(内容管理系统),提供对内容的增、删、改、查等功能。
本文介绍如何用 Angular 搭建一个 CMS 系统,文章重点关注流程,更多技术细节请参考 官方文档。
实现简易用户管理功能,查看在线例子。html
确保设备已安装 node , 且知足 node 8.x 和 npm 5.x 以上的版本。前端
安装 Angular CLI 。它包含一套命令行指令,能够帮助开发者快速建立项目、添加文件、以及执行项目运行、打包等任务。node
npm install -g @angular/cli
使用 Angular CLI 提供的ng new
命令建立一个新项目。Angular CLI 会在当前目录建立一个指定命名的新项目,建立过程当中会自动安装项目所需依赖,若是在公司内网这一步须要配合代理进行。运行下列命令建立并启动一个 CMS 项目。react
ng new cms cd cms ng serve --open
使用--open
,在编译完成后会自动打开浏览器并访问 http://localhost:4200/,能够看到一个 Angular 项目启动了。其余比较经常使用的是参数有,git
--port 指定端口号 --proxy-config 代理配置文件 --host fe.cms.webdev.com /*在须要读取cookie的状况下会有用*/
模块与组件
Angular 采用模块化的开发方式。
模块是一组功能的集合。模块把若干组件、服务等聚合在一块儿,它们共享同一个编译上下文环境。页面的每个小部分均可以看做是一个组件。
组件包含组件类和组件模版。模版负责组件的展现,可使用 Angular 的模版语法对 html 进行修改。组件类实现组件的逻辑部分,能够经过注入服务去实现一些数据交互逻辑。github
Angular CLI 初始化项目中有惟一的一个模块—— AppModule 模块。它是一个根模块,页面从这里启动,它下面能够包含子模块和组件。为了演示方便,在项目中再也不新建模块,只经过组件去实现不一样页面的展现。web
新建两个组件:list 负责数据管理,edit 负责表单编辑。除此以外,还须要一个 nav-side 组件做为页面导航,负责 list、edit 的切换。用 ng g 命令建立这三个组件。下面几个命令是等价的。npm
ng generate component nav-side ng g component edit ng g c list
试试将它们添加到页面中,在模版中建立它们。json
<!-- app.component.html --> <app-nav-side></app-nav-side> <app-edit></app-edit> <app-list></app-list>
在页面上能够看到,这三个组件都被建立了。但咱们须要在不一样状况下分别展现 list 和 edit 组件,能够经过引入路由模块来实现。api
路由
Angular 的
Router
模块提供了对路由对支持。在 Angular 中使用路由至少要作以下两个配置:
一、定义路由。Angular 路由(Route)是一个包含 path 和 component 属性对对象数组。path 用来匹配URL路径,component 则告诉 Router 在当前路径下应该建立哪一个组件。
二、添加路由出口。在页面上添加<router-outlet>
元素,当路由到的某个组件时,会在当前位置展现组件的视图。
定义页面须要的路由。Edit 路由上定义了一个id参数,经过它能够把用户ID传给组件。
<!-- app.module.ts --> 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
的位置被建立。
<!-- app.component.ts --> <div class="app-doc"> <nav class="app-navbar"> <app-nav-side></app-nav-side> </nav> <div class="app-wrapper"> <router-outlet></router-outlet> </div> </div>
在 nav-side 中使用路由跳转。绑定routerLink
属性,下面使用两种方式,后一种方式支持传入更多参数。此外还绑定了routerLinkActive
属性,它支持传入CSS类,当当前路由被激活时CSS类就会被添加。
<!-- nav-side.component.html --> <ul class="app-menu"> <li routerLinkActive="open"> <a routerLink="/list">列表页</a> </li> <li routerLinkActive="open"> <a [routerLink]=["/edit"]>编辑页</a> </li> </ul>
如今咱们会看到页面效果如图。点击侧边栏,可在列表页和编辑页之间来回切换。
至此,页面骨架搭建完成。
简单梳理列表页须要实现的内容。
在开始页面实现以前,须要作一些准备工做,首先须要设计列表页的数据。
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提供了In-memory-web-api进行数据模拟。
咱们能够建立项目中须要的一组数据,而后经过 REST API 请求获取数据。咱们能够按照真实接口的样式去实现请求方法,在真正的接口准备好以后,只须要移除in-memory-data
,就能够实现真实与模拟请求的无缝替换。
下面咱们定义须要的数据。
<!-- in-memory-data.service.ts --> 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
操做符对数据进行处理,获取须要的对应分页下的数据。
<!-- data.service.ts --> 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<IList> { let currPage = params.page, totalPage: number, limit = 6; return this.http.get<IList>(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<IUser> { return this.http.get<IUser>(`${this.url}/${id}`) } deleteUser(id: number): Observable<IUser> { return this.http.delete<IUser>(`${this.url}/${id}`) } addUser(data: IUser): Observable<IUser> { return this.http.post<IUser>(this.url, data) } updateUser(data: IUser): Observable<IUser> { return this.http.put<IUser>(this.url, data) } }
在AppModule中引入发送数据请求须要的HttpClientModule
和本地数据获取须要的HttpClientInMemoryWebApiModule
。
<!-- app.module.ts --> 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 操做符,相关知识点参考 Angular Observable, RXJS
因为 DataService 返回一个包含列表数组及分页信息的 Observable 类型的数据,咱们须要将这两部分数据分离并展现。下面代码中,经过一系列流的操做,咱们把分页数据提取给了 pager 对象,列表数组使用一个 Observable 类型的对象表示—— listData$。
将 listData$ 绑定到模版上,经过async
[pipe](https://angular.io/guide/pipes)能够实现 Observable 的订阅。Observable 在被订阅后,每次更新 Observer 都会受到新数据,即页面上的数据都会刷新。因为 updateList$ 是BehaviorSubject
类型,只须要调用next
方法便可实现数据的刷新。
<!-- list.component.ts --> export class ListComponent implements OnInit { pager: IPager = { currPage: 1, totalPage: 1 } as IPager; listData$: Observable<IUser[]>; updateList$: BehaviorSubject<number> = new BehaviorSubject<number>(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); }) } }
<!-- list.component.html --> <table class="ngx-table"> <thead> <tr> <th>ID</th> <th>昵称</th> <th>性别</th> <th>操做</th> </tr> </thead> <tbody> <tr *ngFor="let data of listData$|async; let idx = index"> <td>{{data.id}}</td> <td>{{data.nick}}</td> <td>{{data.sex === 'male'? '男': '女'}}</td> <td class="action-group"> <a [routerLink]="['/edit', data.id]">编辑</a> <a (click)="deleteUser(data.id)">删除</a> </td> </tr> </tbody> </table>
实现一个简单的分页组件,展现当前页码和总页数,并提供一个输入框能够填写须要跳转到的页面。
新建一个 pagination 组件。组件接收 IPager 类型的参数,并展现 pager 内容。当跳转按钮被点击时,向外发出 pageChange 事件,并把须要跳转到的页码给出。父组件( ListComponent )须要在模版中给 pagination 组件传入 pager 属性的值,并监听 pageChange 事件。这里使用了 Angular 的@Input
、@Output
定义了组件的输入输出属性。
对于回车跳转的方式,能够直接监听 Input 上的 keyup 事件,也能够经过 RXJS 的fromEvent
监听 keyup 事件,当监听到回车时调用页面跳转方法。
<!-- pagination.component.ts --> export class PaginationComponent implements OnInit { targetPage: number; @Input() pager: IPager; @Output() pageChange: EventEmitter<number> = new EventEmitter<number>(); 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; } }
<!-- pagination.component.html --> <div class="page-wrapper"> <input id="input" type="text" [(ngModel)]="targetPage"> <a (click)="onPageChange()">跳转</a> <span class="summary">{{pager.currPage}} / {{pager.totalPage}}</span> </div>
<!--list.component.html --> <app-pagination [pager]="pager" (pageChange)="onPageChange($event)"></app-pagination>
<!--list.component.ts --> onPageChange(page: number) { this.updateList$.next(page); }
对于搜索组件,它须要将搜索表单内容与列表页共享,这里经过@ViewChild
的方式共享数据,它提供了父组件获取子组件实例的方法,经过组件实例能够获取到组件内的属性。
新建 searh-form 组件,使用 Reactive-Form 的模式构建一个搜索表单。
<!-- search-form.component.ts --> 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<void> = new EventEmitter<void>(); constructor(private fb: FormBuilder) { } ngOnInit() { this.form = this.fb.group({keyword: ['']}); } onSubmit() { this.search.emit(); } }
<!-- search-form.component.html --> <form class="search-form" [formGroup]="form" (submit)="onSubmit()"> <input type="text" formControlName="keyword" placeholder="请输入关键词"> <button type="submit">搜索</button> </form>
<!--list.component.html --> <app-search-form (search)="onSearchDataChange($event)"></app-search-form> <!--list.component.ts --> @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会及时更新。
<!-- edit.component.ts --> userId: string; construct( ... private route: ActiveRoute ) { this.route.paramMap.subscribe((params: ParamMap) => { this.userId = +params.get('id') || null; }) }
<!-- edit.component.html --> <h2>{{!userId? '新建用户': ('编辑用户 - ')}}{{userId}}</h2>
一样的,咱们引入 Reactive-Form 模块,经过数据模型来渲染表单。这里咱们加入了表单校验配置,设置 nick 和 sex 都必填,校验结果能够经过invalid
方法获取。而且在校验失败时,将提交按钮置灰。
表单数据的提交就是请求 DataService 的 addUser 方法,能够在提交成功后经过路由方法跳转到列表页。
<!-- edit.component.ts--> 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']); }) }
<!-- edit.component.html--> <form [formGroup]="userForm" (submit)="onSubmit()"> <div class="form-group"> <label class="form-label" for="nick">昵称:</label> <input class="form-control" type="text" formControlName="nick"> </div> <div class="form-group form-radio-group"> <label class="form-label" >性别:</label> <label class="center-block"><input type="radio" formControlName="sex" value="male">男</label> <label class="center-block"><input type="radio" formControlName="sex" value="female">女</label> </div> <div class="form-group form-group-btn"> <button type="submit" [disabled]="userForm.invalid">提交</button> </div> </form>
在用户ID存在时,须要获取用户信息进行展现。DataService 已经实现了数据获取方法,在拿到用户信息后,能够经过patchValue
对 userForm 的数据进行修改。
最后咱们修改一下 submit 方法,让它能兼容新建和保存两种模式。
<!-- edit.component.ts --> 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
参数以选择 AOT 的方式打包。打包后的文件会被保存在angular.json
中配置的outputPath
路径下。
文件的引用路径能够查看打包后的 index.html,而且能够在 angular.json 中修改配置路径。
整套流程下来,咱们构建了一个简单可是完整的 CMS 系统,涉及了 Angular 中大部分基础知识点。后续可参考官方文档,加强系统功能,运用更多 Angular 特性。