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 特性。