使用Angular輕鬆搭建CMS頁面

Angular(Angular 2+ )是一套現代的 WEB 開發框架,它採用模塊化開發,提供一套完整的開發支持,使開發者能更專一於業務邏輯,提升生產效率。
CMS(內容管理系統),提供對內容的增、刪、改、查等功能。
本文介紹如何用 Angular 搭建一個 CMS 系統,文章重點關注流程,更多技術細節請參考 官方文檔

目標

實現簡易用戶管理功能,查看在線例子html

  • 編輯頁:支持新建用戶,支持修改用戶信息
  • 列表頁:展現用戶數據,支持分頁查詢,支持刪除用戶

搭建環境

確保設備已安裝 node , 且知足 node 8.x 和 npm 5.x 以上的版本。前端

安裝 Angular CLI 。它包含一套命令行指令,能夠幫助開發者快速建立項目、添加文件、以及執行項目運行、打包等任務。node

npm install -g @angular/cli

建立Angular項目

使用 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 ObservableRXJS

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

相關文章
相關標籤/搜索