基于WeUI的Angular2开发

钟爱Angular

WeUI在微信上的表现真的很惊艳,极大简化微信UI的纠结。如果你和我一样一时找不到合适的UI,WeUI未尝不是一种选择。

一直以来大厂对React比较钟爱,官网也提供 React-Weui 版本;而对于像我这类钟爱Angular的人而言,或者说团队里面对Angular比较熟悉的情况下,如果能有一套比较标准的UI,在微信端开发应用也会极爽。

当然啦,Angular版本的WeUI在Github上也有好几套,但要么是还处于2.x时代、要么就是缺少维护。因此,不得以重复造了一套轮子,其名:ngx-weui

如何使用

下面会有点啰嗦,在这之前可以看一眼示例

样式

ngx-weui 除了官网不提供的UI组件样式外,但有些组件又很常用撸了点非官网的样式外,不带任何样式。因此,在使用前务必先引用weui.css,或者直接在 index.html 中引用官网CDN版本。

<link href="//res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css" rel="stylesheet">

安装与注册

npm install ngx-weui --save

ngx-weui 默认情况下可以直接使用:

import { WeUiModule } from 'ngx-weui';

@NgModule({
    imports: [ WeUiModule.forRoot() ]
})
export class AppModule { }

注册所有的模块。

当然,如果你明确知道只使用其中几个模块的话,可以针对模块进行导入,这样也可以简化包大小,比如导入一个弹出式菜单模块:

import { ActionSheetModule } from 'ngx-weui/actionsheet';

@NgModule({
    imports: [ ActionSheetModule.forRoot() ]
})
export class AppModule { }

使用

ngx-weui 本身并没有像 react-weui 版本那样,对样式也进行组件化。关于这一点好坏,和语言环境有很大关系。

用一句比较简单的话来讲,react在运行时决定组件,而angular在编译时决定组件。

正因此如此,所以我决定不对样式也组件化,因为这样完全没有意义。所以 ngx-weui 只对功能性进行组件化。

怎么区分这一点呢?比如说 weui-cell 表示一个单元格,这样样式本身只是为组织我们DOM结构,并不附加任何功能性质,因此并不会像react一样有一个相对应的 Cell 组件。

当然,像Toast本身是需要触发产生的,则 ngx-weui 会有一个对应的组件叫 weui-toast

<weui-button (click)="successToast.onShow()">Toast From Component</weui-button>
<weui-toast #success></weui-toast>
@ViewChild('success') successToast: ToastComponent;

默认Toast组件创建后是被隐藏的,所以这里需要定义一模板变量用于存储这个组件实例,才能调用该组件的 onShow() 方法。

恩,上面的写有点奇怪,因为不是很优雅,我的意思是说需要先在HTML模板中放置一个 weui-toast 组件,然后再定义一个模板变量,最后需要的时候调用显示方法。

所以,提供另一种 Service 形式写法,为什么呢?很简单,很多时候我们希望在 Class 里直接一个 Toast 显示告知用户你有问题,这个时候还要去HTML模板创建再显示,显得编码太过于笨拙。

constructor(public srv: ToastService) {
    // success
    srv.success();
    // loading
    srv.loading();
}

怎样,注入 Service 类,一行代码简洁、优雅!

全局默认配置项说明

有一些模块(比如:actionsheet、button等),虽然已经有一些默认的配置,但你可以通过全局注册来改变它。

比如,默认按钮的类型是 primary(成功样式)。

<weui-button>成功样式按钮</weui-button>
<weui-button weui-type="warn">警告样式按钮</weui-button>

可以在NgModule改变默认配置项,默认所有按钮为警告样式。

import { NgModule } from '@angular/core';
import { WeUiModule, ButtonConfig } from 'ngx-weui';

@NgModule({
    imports: [
        WeUiModule.forRoot()
    ],
    providers: [
        // 重置默认按钮样式为:warn
        { provide: ButtonConfig, useFactory: ()=> { return Object.assign(new ButtonConfig(), { type: 'warn' }); } }
    ]
})

这种方式,可以简化通用操作。

是否允许全局配置的模块,可以在API文档中见【可用于[全局配置]】字样的类,都属性可用于全局配置类。

一些细节但又有用的点

命名说明

HTML模板中组件名、属性名的命名有的是以 weui- 开头,而有的并没有。其实,很容易理解这些区别。

  1. 所有组件、指令都需要 weui- 开头。
  2. 指令所需要的属性都需要 weui- 开头。
  3. 组件只允许标签(指:<weui-actionsheet></weui-actionsheet>)写法都不需要 weui- 开头。

最后

ngx-weui 会保持更新及组件的开发,如果您有兴趣可以至 Github 了解更多。

如何利用angular-cli组织项目结构

导语

Angular2(已经统一更名为Angular,而Angular1表示1.x版本,以下统称Angular都是2.x版本以上)的目标是一套框架多个平台,这是所有前端工作的理想目标。

angular-cli它是angular框架官方的一个构建工具,当你使用 ng new xxx 创建一个项目时,所自动生成的项目结构是很有良心的。

我会从它开始,以我们目前生产项目中的一些经验,分享一些很基础的东西,希望有助于你了解整个Angular。

注:angular-cli的项目更新很频繁,但现在已经是rc0版本,所以以下不再探讨任何bate版本的内容。

一、安装注意项目

angular-cli的核心是webpack,以及npm做为依赖包。但往往在安装过程中会遇到很多奇怪问题,我把这一切都追根于网络问题

相信很多利用npm解决依赖包的人都知道淘宝有良心产品 cnpm,但这一次cnpm在安装angular依赖包时可能会行不通。那么一个正确的安装依赖包的姿势应该是:

1、Windows下必须是【管理员模式】下运行CMD;再使用 ng 命令。
2、当 ng new xx 创建项目时会自动执行 npm install 下载依赖包。
3、如果你网络没有问题的情况下,此时 ng serve 就可以正常运行。

然,很多时候,你可能会收到像:

图片描述

懵逼了吧,无从下手了吧。其实是因为所依赖的.d.ts声明文件是存在rawgit里,靠腰啊,大部分网络环境是被抢!!所以类似这种问题,建议解决你的网络问题,那就是VPN。这也是前面我说cnpm也帮不了你的原因,无意黑cnpm!

UPDATE 2017-04-11 有一次我尝试以下办法完成:

npm install -g nrm
nrm use taobao
npm install

当然如若不行,可以尝试以下:

-- windows下使用管理员模式CMD
-- 1、先安装全局包
npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest
-- 2、创建项目
ng new ng-article
cd ng-article
ng serve
-- 3、如果ng serve运行不起来,尝试:
  + 删除node_modules
  + npm install
--4、依然错误
  + 尝试VPN,再循环第3步

升级老项目也比较简单:

-- windows下使用管理员模式CMD
1、全局版本
npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest
2、项目版本,先删除node_modules
npm install --save-dev @angular/cli@latest
npm install

3、最麻烦就是可能会一些配置的变更,这个只能看CHANGELOG.md

二、IDE

“工欲善其事必先利器”,别着急去看生成后的文件。因为我发现很多人使用webstorm来做开发angular,这样要强烈抗议,vs code与Typescript才是最配的好吗?

vs code默认对ts支持非常激进的,必须这两样都是M$的东西嘛。而且,还能再加点扩展,让开发更高效。

1、Angular 2 TypeScript Snippets
一个Agnualr代码片断。

2、Path Intellisense
路径感知,这让我们在写 import 路径时更高效。

3、Auto Import
看图不解释。
图片描述

4、Angular Files
创建Angular文件,就是 ng 命令转化成操作,减少cmd的打开次数;看图不解释。
图片描述

VS CODE执行ng serve

在Windows下不需要再开启一个CMD命令窗口,只需要打开 TERMINAL(ctrl+`) 就可以直接在IDE里面使用 ng 命令。

三、初始化目录结构解读

图片描述

1、.angular-cli.json

stylesscripts

当需要引入用于全局作用域的类库,就需要添加相应类库的脚本和样式,比如在使用 jQuerybootstrap 时:

"styles": [
  "../node_modules/bootstrap/dist/css/bootstrap.css",
  "styles.css"
],
"scripts": [
  "../node_modules/jquery/dist/jquery.js",
  "../node_modules/bootstrap/dist/js/bootstrap.js"
]

其实不光一些全局作用域类库,有一些第三方(例如jQuery插件)插件,因为这类插件并不能被 TypeScript 识别,依然在npm安装完相应插件包后,也需要引相应的js和css加入到这里面。

defaults 键

生成方式的相关配置,比如默认初始化的项目都是采用 css,前端如果不使用CSS预处理语言,就不要好意思说你懂前端。我就是Sass的重度依赖者,所以初始化项目的时候会把css换成scss。只需要简单一步:

"defaults": {
  "styleExt": "scss"
}

因为angular-cli默认就支持sass/scss、less、stylus,你唯一要做的,就是把文件后缀由css变为scss即可。

支持JSON Schema

值得说明的是angular-cli.json配置文件支持JSON Schema,每一个键值都会智能提醒,以及完整的含义解释(虽然是英文的)。

2、tsconfig.json

TypeScript的配置基类,为什么说基类,这是因为ts配置文件是允许被继承的,有没有发现 src/tsconfig.app.jsonsrc/tsconfig.spce.json 这两个分别针对APP和测试环境的TS配置文件。那么angular-cli在执行tsc时会把 tsconfig.json + src/tsconfig.app.json 作为真正的配置文件。

有关更多细节点tsconfig.json

3、src/polyfills.ts

用于解决浏览器兼容问题的,比如像为了支持IE11以下可能你还可以导入一些ES6相应的polyfill。

如果你需要让一些pipe支持i18n的话,需要额外的安装相应intl。

Zone.js

之所以特意在这提一下zone.js,是因为TA对于angular来说非常重要,应该说像 (click) 这些操作和zone.js息息相关,这是angular团队专用angular开发用来解决异步任务间在上下文间传递数据的解决方案。有关这个话题另文在探讨。

四、NgModule与路由

Angular引导启动时是从根模块开始;而模块(NgModule)定义着组件、指令、服务、管道等等的访问权限,这样会使得每一个模块更内聚,这也是软件设计工程里面一直提倡且所追求的“高内聚、低耦合”。

@NgModule({
  // 声明组件和指令
  declarations: [
    AppComponent
  ],
  // 导入其他模块,这样本模块可以使用暴露出来的组件、指令、管道等
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  // 服务依赖注入
  providers: [],
  // 暴露本模块的组件、指令、管道等
  exports: [],
  entryComponents: [],
  // APP启动的根组件
  bootstrap: [AppComponent]
})

在代码中已经在大概的描述,更详细见参考

1、entryComponents

描述 entryComponents 时,我们需要先谈angular-cli的摇树优化,什么意思呢?当编译生产环境代码时 ng build --prod,angular-cli会自动对那一些完全没有被用到模板里的组件、管道等等自动排除掉,那怕是你在 declarations 声明过,这样才可以很大幅度减少文件大小。

所以有一些组件的确不会出现在模板中,但又会用到,比如某个组件是放在模态框里面,而模态框则是通过动态加载的方式来初始化组件,这个时候这些组件如果不在 entryComponents 中定义的话就会被排除掉。

2、模块在项目结构中的应用

前面说过模块可以让代码工程更内聚,利在“模块”,而器在“人”;因此,每个人如何去组织代码结构都会不一样,那我是怎么做的呢?

假设应用我们都会有一个布局,比如上左右结构,而正常上用户登录信息,左为菜单,右为内容。而唯一的特点是上左是通用的,右是根据路由来确定内容。

那么基于此,我的模块分布会是这样:

src/app
│  app.component.html
│  app.component.scss
│  app.component.spec.ts
│  app.component.ts
│  app.module.ts
├─layout // 通用布局组件
│      layout.module.ts
└─routes
    │  routes.ts // 路由配置文件
    │  routes.module.ts
    ├─trade // 订单
    │  │  trade.module.ts
    │  ├─list // 订单列表组件目录
    │  └─view // 订单明细组件目录
    └─user // 会员
        │  user.module.ts
        ├─list
        └─view

layout模块里面包含我上左的组件信息,这个模块与trade/user完全无关的;而对于trade的模块会有相应的list/view两个组件。而对于 routes.module.ts 是会导入 trade/user 两个模块一些通用的模块。

路由写在模块里

整个结构中,只出现一个 routes.ts 文件来管理路由,但它并不是用来管理所有应用的路由,只是路由一些根级路由的配置,比如登录、未找到路由时处理方式。

export const routes = [
    {
        path: '',
        component: LayoutComponent, // 这个组件会在每个路由中优先加载 
        children: [
        ]
    },
    { path: 'login', component: LoginComponent },
    // Not found
    { path: '**', redirectTo: 'dashboard' }
]

路由就是一个带有层次结构的,这点和URI地址一样,用/来表示区隔。

等等,那我们后面的订单、用户的怎么办?怎么关联?

模块懒加载

模块间的导入与导出,其实从代码的角度来讲还是很依赖的,但是我们有一种办法可以让这种依赖变得更模糊。比如说让路由来帮忙加载,而不是通过模块与模块间的编码方式。

因此,只需要在 routes.tschildren 配置路径。

children: [
  { path: 'trade', loadChildren: './trade/trade.module#TradeModule' },
  { path: 'user', loadChildren: './user/user.module#UserModule' }
]

图片描述

3、最佳实践

@NgModule 的信息量就几个属性而已,本没有什么特殊之处,而官网也提供了一些最佳实践的方法供借鉴。

共享模块

所谓共享是指在每个模块中可能都需要用到的,比如表单模块、Http模块、路由模块等等,这样的模块你想用必须手动导入。

因此,创建一个 app/shared/shared.module.ts 模块来管理你共享的模块。

import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule  } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { HttpModule, Http } from '@angular/http';
import { BootstrapModalModule } from 'ng2-bootstrap-modal';

@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        BootstrapModalModule.forRoot({container:document.body})
    ],
    exports: [
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        HttpModule,
        RouterModule
    ]
})

// https://github.com/ocombe/ng2-translate/issues/209
export class SharedModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: SharedModule
        };
    }
}

Service服务不应该放在共享模块中,这是因为Service是依靠DI来实现,只有DI才能保证Service是单一实例。

核心模块

如果你希望有些东西只是在Angular启动时初始化,然后在任何地方都可以用到,那么把这些东西放在这最适宜的。

import { NgModule, Optional, SkipSelf } from '@angular/core';

import { throwIfAlreadyLoaded } from './module-import-guard';

@NgModule({
    imports: [
    ],
    providers: [
    ],
    declarations: [
    ],
    exports: [
    ]
})
export class CoreModule {
    constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
        throwIfAlreadyLoaded(parentModule, 'CoreModule');
    }
}

既然是允许根模块才需要的核心模块,就不允许在其他地方被导入。所以还需要一个防止不小心的人。

throwIfAlreadyLoaded.ts

// https://angular.io/styleguide#!#04-12
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
  if (parentModule) {
    throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
  }
}

五、HTTP

@angular/http 已经提供非常丰富的HTTP请求方法,但实际实践中发现很麻烦,特别是在REST请求时,总感觉在写很多很委屈的代码。

1、REST

只谈REST,不会谈别的,因为这样才最配,没有之一。正常我们需要这么来写:

return this.http.get(this.url)
            .map(this.extractData)
            .subscribe(res => {
                this.list = res;
            })
            .catch(this.handleError);

这是一个很标准的请求写法,走四步:请求>提取数据>订阅结果>异常。然而问题来了,Token?统一处理异常消息?401时跳转登录?这几个问题我们当然可以对上面代码加工后得以满足,但不可能每一次请求,都要在做写同样的,哪怕是多一行代码,也无法忍受。

我找到了一个捷径,ng2-resource-rest,TA和大部分REST客户端没有太多的区别(可以查阅TA的源码,没有几行,很简单),只不过做了很不错的封装,但又能解决我上面提出的几个问题。

REST特征

一个REST URI包含了最简单的CRUD操作,只需要简单是几行可以编写一个CRUD Service。

@Injectable()
@ResourceParams({
  url: 'https://domain.net/api/users'
})
export class NewRes extends ResourceCRUD<IQueryInput, INewsShort, INews> {}

-- 使用
-- this._res.query / get / save / update / remove。

自定义基类

可以自定义一个 Resource 来解决我们上面提中的几个问题。

export class AuthResource extends Resource {
  getHeaders(methodOptions: any): any { 
    // 在这里重写header,加入token
  }

  responseInterceptor(observable: Observable<any>, request: Request, methodOptions: ResourceActionBase): Observable<any> {
    // 对结果统一处理 401、API中错误消息、Http Status等
  }
}

更多方法,可以参考github,作者写了很多直观的DEMO。

Service文件位置

如前面说过在 Core Module 中,把需要通过的 Service 放在里面。但,对于一些并特别针对某个组件,最好放在和 .module.ts 同等的位置,当然这取决于你对粒度的一种控制。

比如,我们项目大部分会在这样放置REST Service。

│  user.memory.service.ts
│  user.module.ts
│  user.service.ts
├─list
│      list.component.ts
└─view
        view.component.ts

list & view 虽然是两个不同的组件,但对于他们来说都使用着相同的Service服务,但也不能把粒度做得太细,比如 list 和 view 分别有一个 service。这看起来像是在男人的房间。

2、Observable

RxJS是Reactive编程库,提供了强大的数据流组合与控制能力,而Observable就是其中之一;RxJS在Angular里非常有地位,网上很多人把他拿 Promise 相比,个人认为是不合理的,压根就没法比。RxJS有丰富的组合和控制能力,而Promise只能告诉你是与不是。

数据控制

如果单纯认为Observable和Promise有实际中的运用没有什么区别,那说明你out了。来看一个我们真实的示例(适当做了简化):

-- template
<li *ngFor="let item of list | async" >{{item.time}}</li>
-- js
this.list = this.form.get('name')
    .valueChanges
    .debounceTime(400)
    .distinctUntilChanged()
    .do(val => {
        console.log('新值', val)
    })
    .map(val => {
        // HTTP请求查询
        return new Array(5).fill(0).map(i => { return { time: val + Math.random() }; });
    });

这是一个很简单的文本框过滤列表的功能,但区区几行代码,带着很不简单的功能。有400ms的抖动、去重、新值的监控、HTTP请求。怎么样,这是Promise无法做到的吧。

这样的功能在我们项目里面,大部分列表页都有。

Async Pipe

在用法上面是否采用Observable或Promise没有太多区别,很多人依然还是很依赖Promise,可能因为学习成本低一点。而Observable更可以通过一些组合和控制,达到更好的编码体验。看一个隔2秒生成一数据的示例:

--template
`<li *ngFor="let num of numbers | async">{{num}}</li>`
-- js
public numbers: Observable<Array<any>>;
ngOnInit() {
    this.numbers = Observable.interval(1000 * 2).map( i => {
        return new Array(5).fill(0).map(i =>  { return Math.random(); });
    });
}

示例中并没有编写任何 subscribe 来订阅结果,而只是在模块中添加了 async Pipe。这样的好处是代码量减少了点、值变更时自动检查组件的变化、当组件被销毁时自动取消订阅避免内存泄露

toPromise()

很多人在通过Http请求一个数据时,会使用 toPromise(),这简直就是多此一举好吗?

-- promise
this.http.get(``).toPromise().then();
-- Observate
this.http.get(``).subscribe();

使用 Promise 的好处是多写几个字母,翻阅 toPromise 源码这检查就是脱裤子放屁。

3、代理请求API

这里代理是指angular-cli在开发过程中,原因是解决跨域请求问题。非常简单的,根目录创建 proxy.conf.json 文件,内容:

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

原先 ng serve 改为 ng serve --proxy-config proxy.conf.json

不过,建议还是使用CROS来解决跨域问题,只需要简单的后端配置,安全、靠谱、方便!

4、跨域

前面说利用CROS来解决Angular跨域,然而发现好多人都理不清一些细节问题,这里以在QQ群中经常遇到的几个点写下来,共勉之!

  • 后端未开启 OPTIONS 预检,如果你接收到一个 405 异常时,那么大多数是这个问题。
  • Angular的 Httpcontent-type 默认是 javascript/json,所以不同后端语言在接收时要注意。
  • 如果不设置【Access-Control-Allow-Credentials:true】的话默认是不会发送COOKIE的,这样如果有些请求需要用到COOKIE,比如登录校验,那就需要注意了。

该节会持续更新。

六、表单

Angular提供模板和模型不同的驱动方式来构建复杂表单,二者编写方式完全不同。表单我最关心就是校验问题,目的尽可能让后端接受到的是一个合理的数据,了解他们的风格才能更好的掌握表单。

1、模板驱动

模板表单最核心的是 ngModel 指令,依赖双向绑定让表单与数据同步,一个简单的示例:

<form #f="ngForm" (ngSubmit)="onTemplateSave()">
    <p>Name:<input type="text" [(ngModel)]="user.name" name="name" required maxlength="20" /></p>
    <p>Pwd:<input type="password" [(ngModel)]="user.pwd" name="pwd" required /></p>
    <p><button type="submit" [disabled]="!f.valid">Submit</button></p>
</form>

最核心是 ngForm,使得表单具备一些HTML5表单属性的检验,比如 required 必填项,并以不同CSS样式来表达状态,所有跟校验有关全都在模板中完成。

很明显非常简单,但无法完成复杂的检验,比如:用户名是否重复;而且无法写单元测试。

2、模型驱动

把上面示例改成模型驱动。

<form [formGroup]="form" (ngSubmit)="onModelSave()">
    <p>Name:<input type="text" formControlName="name" /></p>
    <p>Pwd:<input type="password" formControlName="pwd" /></p>
    <p><button type="submit" [disabled]="!form.valid">Submit</button></p>
</form>
nameCheck(ctrl: FormControl) {
    return new Observable((obs: Observer<any>) => {
        ctrl
            .valueChanges
            .debounceTime(500)
            .distinctUntilChanged()
            .map(value => {
                if (value != 'admin') throw new Error('无效用户');
                return value;
            })
            .subscribe(
                res => {
                    obs.next(null);
                    obs.complete();
                },
                err => {
                    obs.next({asyncInvalid: true});
                    obs.complete();
                }
            );
    });
}

constructor(private fb: FormBuilder) {
    this.form = fb.group({
        'name': new FormControl('', [Validators.required, Validators.maxLength(20)], [ this.nameCheck ]),
        'pwd': ['', Validators.required]
    });
}

相同的功能,虽代码量上升了,但模型驱动的可塑造性非常强。示例中使用了内置检验对象 Validators(其实这些模型和模板驱动所采用的模型完全一置),以及自定义了一个异步检查用户名是否有效的检验。

细心,你会发现模板中连 ngModel 也不见了,因为 this.form 已经自带完整的数据模型,虽然你依然可以写上来支持双向绑定,但这看起来会非常奇怪不建议这样子做

3、如何选择?

很明显二者在可塑造性有很大的区别,当然二者不一定非要二选一,你完全可以混合着用。

但我建议整个项目最好只采用其中一种形式。特别是基于模型驱动创建的表单,不光可塑造性非常强,而且还能够写单元测试

七、关于模态框

模态在应用的地位还是很高的,但目前并没有发现让我用得很爽的,所有难于复用的模态组件都是假的。特别是像我们项目中的订单详情,会在订单列表中、结算列表中、支付列表中等,需要一个能别复用的模态实在太重要了。

这里有一个ng2-bootstrap-modal比较不错的,至少满足两个:

  • 可监控。
  • 模态组件可复用。

一个简单的示例:

@Component({
    selector: 'app-list',
    template: `<div class="modal-dialog">
                <div class="modal-content">
                   <div class="modal-header">
                     <button type="button" class="close" (click)="close()" >×</button>
                     <h4 class="modal-title">Confirm</h4>
                   </div>
                   <div class="modal-body">
                     <p>Are you sure?</p>
                   </div>
                   <div class="modal-footer">
                     <button type="button" class="btn btn-primary" (click)="confirm()">OK</button>
                     <button type="button" class="btn btn-default" (click)="close()" >Cancel</button>
                   </div>
                 </div>
              </div>`
})
export class CancelComponent extends DialogComponent<any, boolean> {
    constructor(dialogService: DialogService) {
        super(dialogService);
    }
    confirm() {
        this.result = true;
        this.close();
    }
}
this.dialogService.addDialog(CancelComponent, {}).subscribe((isConfirmed) => {
    console.log(isConfirmed)
});

虽说无法设置窗体大小、没有遮罩层,但至少可以复用

八、测试

TDD在其他前端框架中很应该不那么容易,但在Angular中是一件非常简单的事情。这一节以 TDD 编程来了解 Angular 在可测试性方面有多么牛B。

angular-cli在初始化项目时,就安装Karma测试任务管理工具、Jasmine单元测试框架、Protractor端对端模拟用户交互工具。

使用 ng test 可以启用Karma控制台,以下是我对前面示例中表单的测试代码:

/* tslint:disable:no-unused-variable */

import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';

import { ViewComponent } from './view.component';
import { SharedModule } from "../../../shared/shared.module";

describe('Component: View', () => {

    let comp: ViewComponent;
    let fixture: ComponentFixture<ViewComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ViewComponent],
            imports: [SharedModule],
            schemas: [NO_ERRORS_SCHEMA]
        })
            .compileComponents()
            .then(() => {
                fixture = TestBed.createComponent(ViewComponent);
                comp = fixture.componentInstance;
            });
    }))

    it('初始化组件', () => {
        expect(comp).toBeTruthy();
    });

    it('检查:表单值变更后是否有更新', () => {
        comp.form.controls['name'].setValue('admin');
        comp.form.controls['pwd'].setValue('admin');
        expect(comp.form.value).toEqual({ name: 'admin', pwd: 'admin' });
    });

    it('检查:用户名为[admin]时,表单应该是有效', (done) => {
        comp.form.controls['name'].setValue('admin');
        comp.form.controls['pwd'].setValue('admin');
        setTimeout(() => {
            expect(comp.form.controls['name'].valid).toEqual(true);
            done();
        }, 1000);
    });

    it('检查:用户名为[admin1]时,表单应该是无效', (done) => {
        comp.form.controls['name'].setValue('admin1');
        comp.form.controls['pwd'].setValue('admin');
        setTimeout(() => {
            expect(comp.form.controls['name'].invalid).toEqual(true);
            done();
        }, 1000);
    });
});

上面分别是表单的三个相对比较变态的测试用例,对表单的测试在很多前端框架是很难做到的,但你看在Angular中很轻松。

不必在意,我这里用了很猥琐的 setTimeout 来解决异步请求等待问题;但我真的找不到怎么测试这种带有异步检验的方法 ~_~。

Angular内部还提供 @angular/core/testing 一些测试的辅助类,这样更有利于写异步方面的测试代码。

覆盖率

当创建一个新组件时 ng g component xx 会自动生成一个 *.spec.ts 的测试文件,这简直就是逼着我们100%测试覆盖率。

检测覆盖率可以使用 ng test --code-coverage,会在根目录下生成一个 /coverage 文件夹。

E2E

E2E是一种模拟用户操作UI流程的测试方法。把上面单元测试用例,改成E2E的测试写法:模拟用户点击用户列表-》点击某个用户详情》在用户编辑页里某个输入用户名》检查用户输入的值是否正确。

it('导航》用户列表页》用户详情》输入【asdf】》结果表单无法提交', () => {
    browser.get('/');
    element(by.linkText('user list')).click();
    element(by.linkText('to view')).click();
    element(by.id('name')).sendKeys('asdf');
    element(by.id('pwd')).sendKeys('admin');
    browser.sleep(1000);
    let submitEl = element(by.id('submit'));
    expect(submitEl.getAttribute('disabled')).toBe('true');
});

it('导航》用户列表页》用户详情》输入【admin】》结果表单无法提交', () => {
    browser.get('/');
    element(by.linkText('user list')).click();
    element(by.linkText('to view')).click();
    element(by.id('name')).sendKeys('admin');
    element(by.id('pwd')).sendKeys('admin');
    browser.sleep(1000);
    let submitEl = element(by.id('submit'));
    expect(submitEl.getAttribute('disabled')).toBe(null);
});

Protractor是专为Angular打造的端对端测试框架,用法和WebDriver差不多,不过Protractor增加一些针对 Angular 的方法,比如根据ngModel获取某个元素 by.model('ngModel Name')、从列表中选择某一行 by.repeater('book in library').row(0) 等等一些很贴心的设计。

结论

其实使用angular-cli创建的项目已经足够清晰,无非就是分而治之。而大部分时难于驾驭Angular,我认为最核心的问题是没有对Angular的全面性了解。

  • Angular默认采用TypeScript为编码语言,“奇怪”语法让大部分难于入手,建议在学习Angular前,先学习ts语言,这样会事半功倍。

  • npm在国内有很多限制,虽然 cnpm 良心淘宝有一个镜像,但某些包还是需要从 gitraw 下载一些依赖,这倒置很多人失去信心。

  • Angular是数据驱动DOM,这句话很重要。

另外文章大部分代码都是直接从项目中截取,为了方便我在github的一份完整的示例源码。

希望大家都尽快驾驭Angular。

** 引用 **
+ ANGULAR DOCS
+ JSON Schema
+ tsconfig.json
+ Angular 2 Series – Part 5: Forms and Custom Validation
+ Introduction to Angular 2 Forms – Template Driven vs Model Driven or Reactive Forms

Angular2如何使用jQuery

Angular2是以TypeScript语言作为默认编码语言,所以你看到的全部都是.ts结尾的文件。

那什么是TypeScript

首先,它是一个编译型语言;既然是编译型,那么你像重构、导航、智能提醒这种工具属性就可以发挥出来,所以你会发现使用VS CODE来写Angular简直就是绝配。

同时,TypeScript还带来一些ES6/7才有特性,比如let、const、async等,你无须关心ES几。

最霸气,TypeScript还是一个强类型、泛型、多态等一些面向对象编程的东西。

那,为何我们不选它呢?

当然,说了这么多,这跟主题看似无关,但如果你了解这些,才能看到问题的本质。

首先,第一关键点编译型,既然是编译型,那么你在代码中出现的任何变量、类、函数都是必须要存在,否则TA就会在编译时报错。

但,问题来了,现在的JavaScript世界中已经有那么多现成的第三方库,难道说都不能用了?非也!

TypeScript一开始就照顾这些了,所以就会有一个叫.d.ts的声明文件。MS当然不会让你去编写这一个文件,所以就有一个叫http://definitelytyped.org/ 网站,TA汇集了很多现成类库的第三方.d.ts的声明文件提供我们下载。

方法一

当然是使用最正规军了,用命令安装jQuery的声明文件。

npm install -D @types/jquery

以及使用

import * as $ from 'jquery';

$('body').addClass('');

完美的智能提示,如果你在VS CODE下的话。

方法二

对于一些并未提供 .d.ts 声明文件的类库,我们怎么办?那当然只能自己写了。

什么?自己写?很困难吧!很复杂吧!

没那么一回事,声明文件其实是对一些类库接口的描述,以下是我截取一段jQuery声明文件的部分代码

interface JQueryStatic {
    /**
     * 去掉字符串首尾空格
     *
     * @param str 字符串
     * @see {@link https://api.jquery.com/jQuery.trim/}
     */
    trim(str: string): string;
}

declare var $: JQueryStatic;

我还特意译成中文,这里的含量很少,最关键的就是 declare 它就是把一个变量 $ 定义成类型 JQueryStatic (还是个接口)。

这样,TS编译器在遇到 $ 时会去找该类型,并且你的代码里面不能出现 $.time1() 之类的,因为你的接口,只有一个 $.trim()

等等,jQuery几十个接口,我都要这么写吗?

NO!!!当然不是,除非你想写一个又漂亮、又好看、又是中文、又是完美智能提示的声明文件的话。

否则,你那就拿 any 类型吧,TA就是万能货。你不需要写一个很复杂的声明文件,只需要:

declare var $: any;

简单粗暴有效!

结论

哎~其实是因群里每天都可以看到一句【怎么使用jQuery】;虽然最简单的结果只需要一句话 declare var $: any;,但我还是啰里吧嗦将了一大堆,可不把前因后果将清楚,我烦~。

另,此解只是抛砖引玉,在很多类库中都是通用的办法。但我建议还是找一些Angular2类库来使用,因为如何更有效的管理JavaScript运行,是一门学问。

希望此篇对你的帮助,快乐编程!

JavaScript修饰器-让代码更干净

一般在JavaScript中为了让部分代码延迟执行,一想起的自然是 setTimeout,比如:

setTimeout(() => {
  // doing
}, 0);

这种代码或许你不知道写过多少遍,但,我们在 setTimeout 中多数情况下会去调用另一个方法:

setTimeout(() => {
  this.fn();
}, 0);

你会发现,我们一直都在重复写着 setTimeout,再套用一个匿名函数,最后才真正去编写我们需要执行的方法。我越来越讨厌这种写法,老是写着一些无关系要多余的代码。

使用Angular的同学对 @Component 不陌生,里面大量的使用这种ES7才会有的“修饰器”。

修饰器是一个函数,用于修改类行为。

那, 应该怎么编写一个更干净的 setTimeout,比如,我希望这样来编写我的timeout:

@timeout(1000)
fn() {
  // doing
}

this.fn();

对应的 timeout 修饰器代码:

// timeout.ts
export function timeout(milliseconds: number = 0) {
  return function(target, key, descriptor) {
    // value 值相当于上面示例中 `change` 方法。
    var orgMethod = descriptor.value;
    descriptor.value = function(...args) {
      setTimeout(() => {
        orgMethod.apply(this, args);
      }, milliseconds);
    };
    return descriptor;
  }
}

target:实例对象,即 IndexComponent 实例化对象。
key:方法名称,即 fn
descriptor:对象描述,同Object.getOwnPropertyDescriptor() 。

怎么样,这样子写的代码是不是更酷?

修饰器目前只能在ES7才会有,但一些在Typescript、Babel等转码器已经被支持,特别是Angular2应用中更是给予非常重要的地位。而且应用范围可以非常广,比如类、类方法和属性。

结论

以上只是一个很简单的修饰器示例,你可以根据需要生产一些有意思的修饰器,让编写的代码更优雅、更干净。完整示例

angular-cli如何启用HMR

webpack有一个非常好玩的插件叫:模块换替换(HMR),即添加或删除模块时应用持续运行,不需要页面刷新。虽然angular cli至问世以来就以webpack为核心,可直到【1.0.0-beta.22】以上版本才开始支持。

那么,接下来我们来玩一玩Angular的热替换。

先决条件

  • angular cli 至少 1.0.0-beta.22 以上版本

创建项目

使用Angular CLI创建一个空项目。

ng new ng-hmr
cd ng-hmr

添加HMR环境配置文件

HMR需要一定代码嵌入,而这些模块我们希望只在开发过程中使用,所以这里创建一个环境配置文件,用于区分我们什么时候需要引入 hmr 模块,而不会影响我们生产环境。

首先,创建 src/environments/environment.hmr.ts 文件,内容为:

export const environment = {
  production: false,
  hmr: true
};

这里相比较默认两个环境,增加了 hmr: true 变量。那么,另外两个,也一并设置相应 hmr 变量,来确保什么情况下才允许开启 HMR。

environment.prod.ts 生产环境肯定不会开启它:

export const environment = {
  production: true,
  hmr: false
};

environment.ts 默认环境,如果有必要也可以设置为 true,为了后面更好的演示,这里我设置 false

export const environment = {
  production: false,
  hmr: false
};

新环境配置还需要更新 angular-cli.jsonenvironments 节点,以确保angular cli 能认识:

      "environments": {
        "source": "environments/environment.ts",
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts",
        // 新增的节点
        "hmr": "environments/environment.hmr.ts"
      }

到此,与配置相关已经完成,你可以尝试使用 ng serve --hmr -e=hmr 来启用HMR。而且和之前相比会有这样一段提醒:

NOTICE Hot Module Replacement (HMR) is enabled for the dev server.
The project will still live reload when HMR is enabled,
but to take advantage of HMR additional application code is required
(not included in an Angular CLI project by default).
See https://webpack.github.io/docs/hot-module-replacement.html
for information on working with HMR for Webpack.

添加angular-hmr模块依赖与配置

正如提醒信息中说的,Angular CLI默认并不提供HMR所需要的额外应用代码。但Angular已经有了一个现成HMR模块即【Angular 2 Hot Module Replacement】,安装它:

npm install --save-dev @angularclass/hmr

接下来,创建 src/hmr.ts 文件,配置模块内容:

import { NgModuleRef, ApplicationRef } from '@angular/core';
import { createNewHosts } from '@angularclass/hmr';

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
    let ngModule: NgModuleRef<any>;
    module.hot.accept();
    bootstrap().then(mod => {
        ngModule = mod;
    });
    module.hot.dispose(() => {
        let appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);
        let elements = appRef.components.map(c => c.location.nativeElement);
        let makeVisible = createNewHosts(elements);
        ngModule.destroy();
        makeVisible();
    });
};

最后,修改 src/main.ts 内容:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode, ApplicationRef } from '@angular/core';
import { environment } from './environments/environment';
import { AppModule } from './app/app.module';

import { hmrBootstrap } from './hmr';

if (environment.production) {
  enableProdMode();
}

const bootstrap = () => {
  return platformBrowserDynamic().bootstrapModule(AppModule);
}

if (environment.hmr) {
  if (module['hot']) {
    hmrBootstrap(module, bootstrap);
  } else {
    // 未加上 --hmr 时,控制台会有错误提醒
    console.error('HMR没有启用,确保 ng server 命令加上 --hmr 标记');
  }
} else {
  bootstrap();
}

运行

现在,我们可以使用以下命令来启动HMR:

ng serve --hmr -e=hmr

尝试修改一个 app.component.tstitle 属性值,保存后,无须刷新页面,立即生效:

效果图

快点,享受更快乐的编码方式吧!

Angular2 动态创建组件

在ng1.x时我写过Angular 动态创建 directive;而在Angular2应用是由一个庞大的组件树来构建业务,一般而言我们是通过 template 模板来动态加载组件。

一、原因

我依然以1.x所遇到的项目实现为原型。

我们所认知的编辑器是为创建一个富HTML,而为了让用户更好的体验(总不能让客户写HTML语言吧)所产生的按表单填写数据最终生成有效的HTML。那么自然我们不可能只放一种像百度编辑器就完事,你让用创建一个轮播广告几乎是不可能;所以需要有很多组件,比如:商品列表、富文本、图片广告、轮播广告等等,而每一种组件又不可能是统一界面。

二、创建组件

首先需要先创建了两个组件 richconfig

src/components/rich.ts

import { Component } from '@angular/core';

@Component({
  selector: 'rich',
  template: `
    <p>Rich Content</p>
  `
})

export class RichComponent { }

src/components/config.ts

import { Component } from '@angular/core';

@Component({
  selector: 'config',
  template: `
    <p>Config Content</p>
  `
})

export class ConfigComponent { }

三、利用模板动态加载组件

template 模板里面加载组件是最常用加载方式,而且非常简单,只需要存放相应的 selector 模板引擎会自己查找并编译。

//our root app component
import {Component} from '@angular/core'

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

@Component({
  selector: 'template-load',
  providers: [],
  template: `
      <h1>方式一:通过 template 动态加载</h1>
      <config></config>
      <rich></rich>
  `,
  directives: [ ConfigComponent, RichComponent ]
})
export class TemplateLoadComponent {
  constructor() {

  }
}

这样的方式很明显的一点是,template的结构将决定组件在组件树当中所处的位置。假如我们要把 configrich 组件对换位置,不得不对 template 重新编写,这样更别说让用户来拖拽来调整组件显示位置这么高级的办法了。

四、利用 DynamicComponentLoader 实现动态加载

DynamicComponentLoader 支持两种不同的加载方式来动态创建组件;但在这之前我们需要先做点什么。

首先,我示例里是根据按钮点击事件创建相应的组件,所以我们需要先创建一个组件映射表。

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

export class DynamicLoadComponent {
  compMaps: {[key: string]: any} = {},
  constructor() {
    this.compMaps.config = ConfigComponent;
    this.compMaps.rich = RichComponent;
  }
}

这样我们可以模板里面定义添加事件,并传递相应的key值,下面是分别定义两个按钮用来创建 configrich 组件,以及 container1 元素用于放置组件位置。

<button type="button" (click)="add('config')">Add Config</button>
<button type="button" (click)="add('rich')">Add Rich</button>
<div id="container1"></div>

准备工作完成后,我们分别来看一下二种方式的差异:

1、loadAsRoot

loadAsRoot(type: Type, overrideSelectorOrNode: string|any, injector: Injector, onDispose?: () => void, projectableNodes?: any[][]) : Promise<ComponentRef<any>>

loadAsRoot 方法会创建一个 type 组件实例,然后放到 overrideSelectorOrNode 放处位置的第一个元素下面,不管你动态加载几次永远都只有一份组件实例

constructor(public dcl:DynamicComponentLoader, public _injector:Injector) {
}

add(comp) {
  this.dcl.loadAsRoot(this.compMaps[comp], "#container1", this._injector);
}

#container1 位置可以在整个文档的任意位置,只要该ID是存在。

最终的DOM结构像这样:

<div id="container1">
   <p>Config Content</p>
</div>

loadAsRoot 方法很好理解,而且很霸道,始终只会创建一个组件实例。

2、loadNextToLocation

loadNextToLocation(type: Type, location: ViewContainerRef, providers?: ResolvedReflectiveProvider[], projectableNodes?: any[][]) : Promise<ComponentRef<any>>

同样也是创建一个 type 组件实例,并插入到指定 location 位置的后面,ViewContainerRef 是一个视图容器,所以通过 loadNextToLocation 方法加载的组件,都会放入容器里面。

首先需要在模板中定义一个本地模板变量 container2,并在组件类中获取该变量。

@Component({
   template: `
    <div #container2></div>
   `
})

export class DynamicLoadComponent {
  @ViewChild('container2', {read: ViewContainerRef}) target;
}

add 方法也非常简单:

  add(type) {
    this.dcl.loadNextToLocation(this.compMaps[comp], this.target)
  }

最终的DOM结构像这样:

<config>
  <p>Config Content</p>
</config>
<rich>
  <p>Rich Content</p>
</rich>

3、传递数据

两种方法返回的结果都是一个 Promise 类型,并传递一个 ComponentRef 的新创建组件对象。


this.dcl.loadNextToLocation(this.compMaps[comp], this.target).then(function(cmpRef) { // doing })

可以通过 cmpRef 参数进行数据传递,比如:

cmpRef.instance.someProperty = someData;

五、总结

文章中只是做个抛砖引玉,实际项目应该做更深一层的抽象,Angular2是以组件为出发点,不能有太多了依赖。但是,文章至少已经解决最核心的问题了。文章并没有给出完整代码,如您感兴趣见Plunker

DynamicComponentLoader 从变更纪录来看非常频繁,在前两个BETA版本中都对其做大量的变动;但不管怎么变,是越来越精简。

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top