使用Angular轻松搭建CMS页面

Angular(Angular 2+ )是一套现代的 WEB 开发框架,它采用模块化开发,提供一套完整的开发支持,使开发者能更专注于业务逻辑,提高生产效率。
CMS(内容管理系统),提供对内容的增、删、改、查等功能。
本文介绍如何用 Angular 搭建一个 CMS 系统,文章重点关注流程,更多技术细节请参考 官方文档

目标

实现简易用户管理功能,查看在线例子

  • 编辑页:支持新建用户,支持修改用户信息
  • 列表页:展示用户数据,支持分页查询,支持删除用户

搭建环境

确保设备已安装 node , 且满足 node 8.x 和 npm 5.x 以上的版本。

安装 Angular CLI 。它包含一套命令行指令,可以帮助开发者快速创建项目、添加文件、以及执行项目运行、打包等任务。

npm install -g @angular/cli

创建Angular项目

使用 Angular CLI 提供的ng new命令创建一个新项目。Angular CLI 会在当前目录创建一个指定命名的新项目,创建过程中会自动安装项目所需依赖,如果在公司内网这一步需要配合代理进行。运行下列命令创建并启动一个 CMS 项目。

ng new cms
cd cms
ng serve --open

使用--open,在编译完成后会自动打开浏览器并访问 http://localhost:4200/,可以看到一个 Angular 项目启动了。其他比较常用的是参数有,

--port 指定端口号
--proxy-config 代理配置文件
--host fe.cms.webdev.com /*在需要读取cookie的情况下会有用*/

搭建页面骨架

模块与组件

Angular 采用模块化的开发方式。
模块是一组功能的集合。模块把若干组件、服务等聚合在一起,它们共享同一个编译上下文环境。页面的每一个小部分都可以看作是一个组件。
组件包含组件类和组件模版。模版负责组件的展示,可以使用 Angular 的模版语法对 html 进行修改。组件类实现组件的逻辑部分,可以通过注入服务去实现一些数据交互逻辑。

Angular CLI 初始化项目中有唯一的一个模块—— AppModule 模块。它是一个根模块,页面从这里启动,它下面可以包含子模块和组件。为了演示方便,在项目中不再新建模块,只通过组件去实现不同页面的展示。

新建两个组件:list 负责数据管理,edit 负责表单编辑。除此之外,还需要一个 nav-side 组件作为页面导航,负责 list、edit 的切换。用 ng g 命令创建这三个组件。下面几个命令是等价的。

ng generate component nav-side
ng g component edit
ng g c list

试试将它们添加到页面中,在模版中创建它们。




在页面上可以看到,这三个组件都被创建了。但我们需要在不同情况下分别展示 list 和 edit 组件,可以通过引入路由模块来实现。

路由

Angular 的Router模块提供了对路由对支持。在 Angular 中使用路由至少要做如下两个配置:
1、定义路由。Angular 路由(Route)是一个包含 path 和 component 属性对对象数组。path 用来匹配URL路径,component 则告诉 Router 在当前路径下应该创建哪个组件。
2、添加路由出口。在页面上添加元素,当路由到的某个组件时,会在当前位置展示组件的视图。

定义页面需要的路由。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轻松搭建CMS页面_第1张图片

至此,页面骨架搭建完成。

列表页实现

简单梳理列表页需要实现的内容。

  • 功能拆分:数据展示、查询、删除
  • 页面划分:表格、分页、搜索框

数据定义

在开始页面实现之前,需要做一些准备工作,首先需要设计列表页的数据。

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,就可以实现真实与模拟请求的无缝替换。

下面我们定义需要的数据。

 
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 操作符,相关知识点参考 Angular ObservableRXJS

由于 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 组件,使用 Reactive-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);
}

至此,我们实现了用户的展示、查询、删除操作,列表页完成。

使用Angular轻松搭建CMS页面_第2张图片

编辑页实现

简单梳理编辑页需要实现的内容。

  • 功能拆分:数据新增、修改
  • 页面划分:标题、表单

标题

在编辑页需要根据用户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']);
  })
}

使用Angular轻松搭建CMS页面_第3张图片

修改

在用户ID存在时,需要获取用户信息进行展示。DataService 已经实现了数据获取方法,在拿到用户信息后,可以通过patchValue对 userForm 的数据进行修改。
最后我们修改一下 submit 方法,让它能兼容新建和保存两种模式。



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']);
  })
}

使用Angular轻松搭建CMS页面_第4张图片

项目打包及部署

如果需要把项目打包并部署到服务器上,只需要运行ng build命令即可完成打包,可以配置--prod参数以选择 AOT 的方式打包。打包后的文件会被保存在angular.json中配置的outputPath路径下。
文件的引用路径可以查看打包后的 index.html,并且可以在 angular.json 中修改配置路径。

最后

整套流程下来,我们构建了一个简单但是完整的 CMS 系统,涉及了 Angular 中大部分基础知识点。后续可参考官方文档,增强系统功能,运用更多 Angular 特性。

你可能感兴趣的:(angular6,angular,单页应用,前端框架,前端)