Angular7框架搭建及项目开发【已升级Angular8】

一、介绍

通过本文学习,能够掌握搭建Angular开发环境及快速项目构建。

本文主要使用Angular7.x + Ng-zorro-ant(v7.5.0)组件库进行搭建。Angular框架基础核心原理并不在文中进行太多介绍,主要文档参考请链接至官方中文文档

代码开发编辑器:Visual Studio Code

所需掌握技术点:ES6、Typescript、html、Less、Rxjs

二、Angular开发环境搭建

安装Angular-CLI

注:在安装Angular-CLI之前,请确保你的设备安装了Node.js v10.9.0以上版本。

  1. 使用npm命令安装angular/cli
npm install -g @angular/cli@7.3.6
  1. 安装成功后,运行以下命令即可查看是否安装成功
ng version / ng v 查看脚手架版本信息
  1. 出现以下画面表示已安装成
     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 7.3.6
Node: 11.12.0
OS: darwin x64
Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.13.6
@angular-devkit/core         7.3.6
@angular-devkit/schematics   7.3.6
@schematics/angular          7.3.6
@schematics/update           0.13.6
rxjs                         6.3.3
typescript                   3.2.4

三、创建项目并运行

  1. 运行CLI命令创建,如下所示
ng add ngExample
  1. 接下来,ng new命令会提示你添加初始项目所要包含的特性,默认选择后需等待CLI安装Angular包及其他依赖包。

  2. 项目创建成功后,进入项目根目录运行以下指令,向项目中添加ant组件库

cd ngExample
ng add ng-zorro-antd

输入命令后,选择ng-zorro安装特性,等待安装成功。

  1. 使用VSCode编辑工具,打开项目文件夹,在VSCode终端中运行以下CLI命令
ng serve

在终端中看到Compiled successfully,表示已经成功启动Angular项目,具体参考以下图片中内容:

vscode.png
  1. 在浏览器中访问localhost:4200,目前默认端口为4200,后续有需要可根据实际情况在CLI启动命令中变更端口。

  2. 到此为止,在浏览器中看到以下页面时,整个环境搭建以及简单项目构建已经完成,即将进入本文案例开发。

browser.png

四、项目框架技术点

4.1 目录结构

由于初始化项目目录结构并不能很好的满足开发规则,因此对目录结构进行了优化,如下所示

directory.png
目录 说明
src/app/core 核心组件目录,例如HTTP拦截器、国际化服务等
src/app/layout 层级组件目录,例如主框架页面、页面头部组件等
src/app/routes 业务组件目录,包含主体路由以及各业务模块路由
src/app/shared 共享文件目录,项目中可多次且通用文件可在此目录中声明
src/assets 资源目录,用于存放icon字体、国际化翻译文件、图片等
src/environments 环境配置文件
src/styles 自定义样式文件

除了上述优化后的目录介绍,其余文件介绍请参考Angular中文文档 - 项目文件结构

在tsconfig.json中配置模块引入时转为相对路径

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "src",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "module": "es2015",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2018",
      "dom"
    ],
    "paths": {
      "@shared": [
        "app/shared"
      ],
      "@shared/*": [
        "app/shared/*"
      ],
      "@core": [
        "app/core"
      ],
      "@core/*": [
          "app/core/*"
      ]
    }
  }
}

例子:

import { SharedModule } from '@shared/shared.module';

app目录

app目录是我们要编写的代码目录,我们写的代码都是放在这个目录。
一个Angular程序至少需要一个模块和一个组件,在我们新建项目的时候命令行已经默认生成出来了。

app
    app.component.ts  // 组件
    app.module.ts    // 模块

4.2 页面主体结构

4.2.1 主体结构开发

layout这个目录在Angular中属于一个模块,所以需要有layout.module.ts文件做承载,且要把该文件引入到根模块app.module.ts中。

  • 在src/app/layout中创建default.component.html模板
<div class="container">
    <!-- 头部 -->
    <div class="header"></div>

    <!-- 中间内容 -->
    <div class="content">
        <router-outlet></router-outlet>
    </div>

    <!--尾部-->
    <div class="footer"></div>
</div>
  • 在src/app/layout中创建default.component.ts文件
import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'layout-default',
    templateUrl: './default.component.html',
})
export class LayoutDefaultComponent {
}

该页面为上中下布局,中间层的<router-outlet></router-outlet>为路由加载位置。

4.2.2 优化页面结构

如何优化页面结构?

根据ng-zorro-ant组件库为我们提供了一些常用的页面布局组件,我们只需要简单步骤就能够替换。

  • 替换default.component.html内容
<nz-layout class="layout">
    <nz-header>
        <ul nz-menu [nzTheme]="'dark'" [nzMode]="'horizontal'" style="line-height: 64px;">
            <li nz-menu-item>用户列表</li>
        </ul>
    </nz-header>
    <nz-content style="padding:0 50px;">
        <nz-breadcrumb style="margin:16px 0;">
            <nz-breadcrumb-item>Home</nz-breadcrumb-item>
        </nz-breadcrumb>
        <div style="background:#fff; padding: 24px; min-height: 280px;">
            <router-outlet></router-outlet>
        </div>
    </nz-content>
    <nz-footer style="text-align: center;"></nz-footer>
</nz-layout>

<nz-layout> <nz-header> <nz-content> <nz-footer>这几个是ng-zorro-ant组件库所提供的已封装组件。

4.3 自定义组件

4.3.1 组件相关的概念

  • 组件元数据装饰器(@Component)
    简称组件装饰器,用来告知Angular框架如何处理一个TypeScript类.
    Component装饰器包含多个属性,这些属性的值叫做元数据,Angular会根据这些元数据的值来渲染组件并执行组件的逻辑

  • 模板(Template)
    我们可以通过组件自带的模板来定义组件的外观,模板以html的形式存在,告诉Angular如何来渲染组件,一般来说,模板看起来很像html,但是我们可以在模板中使用Angular的数据绑定语法,来呈现控制器中的数据。

  • 控制器(controller)
    控制器就是一个普通的typescript类,他会被@Component来装饰,控制器会包含组件所有的属性和方法,绝大多数的业务逻辑都是写在控制器里的。控制器通过数据绑定与模板来通讯,模板展现控制器的数据,控制器处理模板上发生的事件。

/*这里是从Angular核心模块里面引入了component装饰器*/
import {Component} from '@angular/core';

/*用装饰器定义了一个组件以及组件的元数据  所有的组件都必须使用这个装饰器来注解*/
@Component({
  /*组件元数据  Angular会通过这里面的属性来渲染组件并执行逻辑 */

  // 这是css选择器,表示这个组件可以通过app-user来调用
  selector: 'app-user',
  // 组件的模板,定义了组件的布局和内容
  templateUrl: './user.component.html',
  // 该模板引用less样式
  styleUrls: ['./user.component.less']
})
// UserComponent本来就是一个普通的typescript类,但是上面的组件元数据装饰器告诉Angular,UserComponent是一个组件,需要把一些元数据附加到这个类上,Angular就会把UserComponent当组件来处理
// 这个类实际上就是该组件的控制器,我们的业务逻辑就是在这个类中编写
export class UserComponent implements OnInit {
  
  // 组件初始化时加载
  ngOnInit() {
    console.log('这是用户管理页面');
  }
}

4.3.2 组件绑定数据

  • 首先在user.component.ts中定义一个值title,随后在组件初始化时赋值
export class UserComponent implements OnInit {
  
  title: string = '';
  
  ngOnInit() {
    console.log('这是用户管理页面');
    this.title = ‘用户管理页’;
  }
}
  • 如何使用这个值?在user.component.html页面中使用双括号绑定
<h1>{{title}}</h1>

4.3.3 双向数据绑定

<input nz-input placeholder="用户查询" [(ngModel)]="value" />
export class UserComponent {
  value: string;
}

4.4 路由

4.4.1 路由定义

  • management.module.ts中添加
const routes: Routes = [
    {
        path: '', component: ManagementComponent, data: { title: '管理' },
        children: [
            { path: 'user', component: UserComponent, data: { title: '用户列表' } },
        ]
    }
];
imports: [
    RouterModule.forChild(routes)
],

4.4.2 路由模块引入

  • 把刚定义好的路由ManagementModule放到routes-routing.module.ts
const routes: Routes = [
    { path: '', component: LayoutDefaultComponent,
      children: [
        { path: 'management', loadChildren: 'app/routes/management/management.module#ManagementModule' },
      ],
    }
];

4.4.3 路由使用

  • routeLink
<li nz-menu-item [routerLink]="['/management/user']">用户菜单</li>

4.5 服务及创建观察者对象

什么是服务?定义公共的方法,使得方法在组件之间共享调用

Angular 把组件和服务区分开,以提高模块性和复用性。 通过把组件中和视图有关的功能与其他类型的处理分离开,你可以让组件类更加精简、高效。

4.5.1 创建服务并注册引用

  • management目录中创建一个名为management.service.ts文件,初始内容如下
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class ManagementService {
    constructor() {

    }
}
  • 服务注册

management.module.ts中注册

import { ManagementService } from './management.service';

@NgModule({
    providers: [ManagementService]
})

  • 服务引用

user.component.ts中引用

import { ManagementService } from '../management.service';

constructor(
    private managementService: ManagementService
) { }

4.5.2 创建可观察对象

可观察对象支持在应用中的发布者和订阅者之间传递消息。 在需要进行事件处理、异步编程和处理多个值的时候,可观察对象相对其它技术有着显著的优点。

可观察对象是声明式的 —— 也就是说,虽然你定义了一个用于发布值的函数,但是在有消费者订阅它之前,这个函数并不会实际执行。 订阅之后,当这个函数执行完或取消订阅时,订阅者就会收到通知。

  • Observable声明
export class ManagementService {
    constructor(private http: HttpClient) {

    }
    getUsers(): Observable<any> {
        let u: User = new User();
        u.name = 'zhangsan';
        u.email = "zhangsan@163.com";
        let users = [{ name: 'zhangsan', email: 'xxx' }];
        users.push(u);
        return of(users);
    }

}
class User {
    public name: string;
    public email: string;
}

4.6 订阅

当创建了观察者对象后,需要通过subscribe订阅的方式来实现功能。

list;

ngOnInit() {
    this.managementService.getUsers().subscribe(data => {
        this.list = data;
    });
}
  • 订阅成功后使用ng-zorro-ant表格组件现实,其中涉及到ng指令*ngIf,指令具体使用方式请参考Angular中文文档。
<nz-table #basicTable [nzData]="list">
    <thead>
      <tr>
        <th>Name</th>
        <th>Email</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let item of list">
        <td>{{ item.name }}</td>
        <td>{{ item.email }}</td>
      </tr>
    </tbody>
</nz-table>
table1.png

4.7 表单

本文中使用了ng-zorro-ant封装的表单组件,是具有数据收集、校验和提交功能的表单。

validateForm: FormGroup;

constructor(
    private fb: FormBuilder
) { }

ngOnInit() {
    
    // 
    this.validateForm = this.fb.group({
        userName: [null, [Validators.required]],
        email: [null, [Validators.required]]
      });

}

在html中,

<input formControlName="userName" nz-input />

<input formControlName="email" nz-input />

利用this.validateForm.controls[’userName‘].value可获取到输入的值。

** FormGroup ** : 把每个子 FormControl 的值聚合进一个对象,它的 key 是每个控件的名字。 它通过归集其子控件的状态值来计算出自己的状态。

4.8 HTTP拦截器

其作用是可拦截HTTP请求返回响应状态码,针对状态码进行处理。

  • 在core/net文件夹中创建以下拦截器
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpErrorResponse, HttpEvent, HttpResponseBase } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { mergeMap, catchError } from 'rxjs/operators';
import { NzMessageService, NzNotificationService } from 'ng-zorro-antd';
import { _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';

const CODEMESSAGE = {
  200: '服务器成功返回请求的数据。',
  201: '新建或修改数据成功。',
  202: '一个请求已经进入后台排队(异步任务)。',
  204: '删除数据成功。',
  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
  401: '用户没有权限(令牌、用户名、密码错误)。',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  406: '请求的格式不可得。',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器。',
  502: '网关错误。',
  503: '服务不可用,服务器暂时过载或维护。',
  504: '网关超时。',
};

/**
 * 默认HTTP拦截器,其注册细节见 `app.module.ts`
 */
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
  constructor(private injector: Injector) { }

  get msg(): NzMessageService {
    return this.injector.get(NzMessageService);
  }

  private goTo(url: string) {
    setTimeout(() => this.injector.get(Router).navigateByUrl(url));
  }

  private checkStatus(ev: HttpResponseBase) {
    if (ev.status >= 200 && ev.status < 300) return;

    const errortext = CODEMESSAGE[ev.status] || ev.statusText;
    this.injector.get(NzNotificationService).error(
      `请求错误 ${ev.status}: ${ev.url}`,
      errortext
    );
  }

  private handleData(ev: HttpResponseBase): Observable<any> {
    // 可能会因为 `throw` 导出无法执行 `_HttpClient` 的 `end()` 操作
    if (ev.status > 0) {
      this.injector.get(_HttpClient).end();
    }
    this.checkStatus(ev);
    // 业务处理:一些通用操作
    switch (ev.status) {
      case 200:
        break;
      case 401: // 未登录状态码
        // 请求错误 401: https://preview.pro.ant.design/api/401 用户没有权限(令牌、用户名、密码错误)。
        (this.injector.get(DA_SERVICE_TOKEN) as ITokenService).clear();
        this.goTo('/passport/login');
        break;
      case 403:
      case 404:
      case 500:
        break;
      default:
        if (ev instanceof HttpErrorResponse) {
          console.warn('未可知错误,大部分是由于后端不支持CORS或无效配置引起', ev);
          return throwError(ev);
        }
        break;
    }
    return of(ev);
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // 统一加上服务端前缀
    let url = req.url;
    if (!url.startsWith('https://') && !url.startsWith('http://')) {
      url = environment.SERVER_URL + url;
    }

    const newReq = req.clone({ url });
    return next.handle(newReq).pipe(
      mergeMap((event: any) => {
        // 允许统一对请求错误处理
        if (event instanceof HttpResponseBase)
          return this.handleData(event);
        // 若一切都正常,则后续操作
        return of(event);
      }),
      catchError((err: HttpErrorResponse) => this.handleData(err)),
    );
  }
}

  • 拦截器使用

在Angular框架@angular/common/http中,提供了HTTP_INTERCEPTORS,我们可以通过自己定义的拦截器触发,做出对应业务处理。

放到app.module.ts

providers: [
    { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true}
]

4.9 管道

什么是管道?Angular中的管道(pipe)是用来对输入的数据进行处理,如大小写转换、数值和日期格式化等。

4.9.1 日期格式化管道

管道以 | 符号右边表示。

<p>{{today | date:'yyyy-MM-dd HH:mm:ss' }}</p>

4.9.2 自定义管道

  • 自定义管道的步骤:

使用 @Pipe 装饰器定义 Pipe 的 metadata 信息,如 Pipe 的名称 - 即 name 属性
实现 PipeTransform 接口中定义的 transform 方法

import { Pipe, PipeTransform } from '@angular/core';

[@Pipe](/user/Pipe)({ name: 'welcome' })

export class WelcomePipe implements PipeTransform {
  transform(value: string): string {
    if(!value) return value;
    if(typeof value !== 'string') {
      throw new Error('Invalid pipe argument for WelcomePipe');
    }
    return "Welcome to " + value;
  }
} 
  • 自定义管道使用
<div>
   <p>{{ 'ngExample' | welcome }}</p> <!-- Output: Welcome to ngExample -->
</div>

五、Angular性能优化

5.1 多模块懒加载

优化前我们工程就一个主模块文件(app.module.ts),路由跳转各页面其实都属于该模块一部分,假如路由对应各页面都是子组件的话,编译时都会被打包到同一个文件。

拆分模块,如下所示

const routes: Routes = [
    { path: '', component: LayoutDefaultComponent,
      children: [
        { path: '', redirectTo: 'index/welcome', pathMatch: 'full' },
        { path: 'index', loadChildren: 'app/routes/index/index.module#IndexModule' },
        { path: 'management', loadChildren: 'app/routes/management/management.module#ManagementModule' },
      ],
    }
];

可以看到写法明显不同,每个路由页面其实都是一个单独模块,然后在编译时每个模块都会单独编译成一个文件。而且路由到某个页面时,才会加载该模块js文件。

运行时可以从控制台发现以下信息,表示拆分成功

chunk {app-routes-index-index-module} app-routes-index-index-module.js, app-routes-index-index-module.js.map (app-routes-index-index-module) 6.02 kB  [rendered]
chunk {app-routes-management-management-module} app-routes-management-management-module.js, app-routes-management-management-module.js.map (app-routes-management-management-module) 6.78 kB  [rendered]

5.2 打包优化

在打包时添加命令
--prod –aot 优化编译方式。

六、心得体会

从最开始的技术选型以及到后续慢慢深入学习了解Angular框架之后,每一个疑问难点解决过程走过来,Angular都能为我带来惊喜。

Angular 以M(model数据)V(view视图/表现层)C(controller控制器/业务逻辑)为基础,降低前端重复工作的劳动量,扩展了HTML功能,组件复用性高。

其中以双向绑定数据为核心,Angular开发过程中基本上是在操作数据。

七、学习网站分享

Angular-CLI

ant组件库

Angualr中文文档

八、此文档项目源码

目前项目源码Angular框架已升级至Angular8

Github链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容