基于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应用中更是给予非常重要的地位。而且应用范围可以非常广,比如类、类方法和属性。

结论

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

Angular2 路由

Angular组件路由使一个视图至另一个视图的导航任务组件。

一、概要

浏览器是一个最熟悉的导航模型应用了,输入一个URL地址后浏览器导航相应URL页面,点击网页上的链接导航至另一个新的页面,浏览器的“后退”或“前近”可以导航至我们浏览器的历史页面。

Angular组件路由(也叫路由)就是借用这个模型,可以理解为通过浏览器的网址来生成客户端视图,并通过一些可选参数指定当前显示哪些视图。当然也可以把路由绑定至一个视图组件里,当点击链接时,导航至适合的视图。并且记录在浏览器历史列表,以便浏览器的前进或后退正常工作。

二、基础知识

先从组件路由基本概念开始,慢慢深入。

1、<base href>

大部分带有路由应用程序都会在 index.html<head> 加上 <base> 元素,用来设置基准URL(也就是应用程序的根路径)。

<base href="/">

2、导入 Router

Angular组件路由是个可选组件,所以并不包括在 core 里,需要我们单独引用 @angular/router 模块。

import { ROUTER_PROVIDERS } from '@angular/router';

3、配置

当浏览器URL改变时,路由会查找相应的 RouteDefinition 来确定组件的显示。

一般路由配置都在宿主组件内,(在Angular1.x经常会看到一些项目所有路由都在一个js文件当中)而且非常简单,通过 @Routers 装饰注册路由。

@Component({ ... })
@Routes([
  {path: '/crisis-center', component: CrisisListComponent},
  {path: '/heroes',        component: HeroListComponent},
  {path: '/hero/:id',      component: HeroDetailComponent}
])
export class AppComponent  implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.navigate(['/crisis-center']);
  }
}

有好几种 RouteDefinition 路由定义方式,大部分都是用URL地址来映射组件。

其中第三条路由 :id 是一个路由参数,比如URL地址 /hero/42 中 “42” 值就是 id 参数值。

4、Router Outlet

现在我们已经知道如何配置路由,当浏览器请求URL地址 /heroes 时,路由会查找 RouteDefinition 匹配到 HeroListComponent,那么匹配到的组件放在哪呢?我们就需要在当前组件模板里加入 RouterOutlet

<router-outlet></router-outlet>

我把它叫成点位符更贴切一点。

5、路由链接

知道如何配置和显示,那接下来就是如何导航,正常我们是通过浏览器的地址栏输入网站,可大部分是用户去触发某个链接进行导航的。

可以在一个锚标签添加一个 RouterLink 指令,模板表达式绑定的是一个字符串数组类型的网址。第一眼一定会很奇怪为什么是数组?这是因为还可能需要URL可选参数,虽然还是很奇怪。

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

6、总结一下

先通过 @RouterConfig 配置组件 AppComponent 的路由,再利用 RouterOutlet 来指定命中路由在哪显示,最后使用 RouterLinks 来处理用户点击链接的导航。

现在 AppComoponent 已经变成了一个路由组件。

路由 意思
Router 类定义了当前活动URL地址环境,可以用它在组件类里进行导航管理。
@RouteConfig 路由装饰器,接收一个 RouteDefinitions 路由定义对象数组。
RouteDefinition 路由定义对象。
RouterOutlet 匹配到组件的显示位置。
RouterLink 用于模板的链接导航。
Link Parameters Array 链接参数数组,可以用在模板的 RouterLink 或 Router.navigate 方法。
Routing Component 配置路由的组件。

三、一个示例

官网用了蛮长的来说明这个示例是怎么回事,大概是这样的:有两个列表分别 “Crisis Center”(记A) 和 “Heroes”(记B),点击列表的项时会进入编辑状态。这在之前二者都是一样的,不同在于编辑页面。

对于A有保存和取消按钮,不管点哪个按钮最后都会回到A列表,而且如果你改动 Name 后点击浏览器后退时会有一个对话框告诉你是不是要放弃编辑;反之B编辑时就很简单只有一个返回按钮,因为你每一次变动都会自动保存。

一个很长的动画,有耐心的可以先看一看效果。

router-anim

那么好,接下来会按里程碑式一步步说说是怎么实现的。

四、里程碑一:开始Router入门

很简单,通过路由来控制两个视图显示。

router-1-anim

1、设置 <base href>

组件路由使用浏览器 history.pushState 来导航,这样的URL地址看起来就像跟直接从服务端URL一样,例如:localhost:3000/crisis-center

现代浏览器都支持HTML5 pushState,为了让 pushState 更好工作,还需要在 index.html 添加一个 <base href="/"> 元素,这样同样可以确保所有请求的资源前缀。

HTML5模式导航是组件路由默认风格,我们也可以用老式的哈希(#)风格,后面会谈到。

2、导入组件路由模块

前面就说过路由模块是独立模块,需要从 angular/router 模块中获取。

import { Routes, Router, ROUTER_DIRECTIVES } from '@angular/router';

3、引导程序中注册路由服务

需要在 main.ts 文件里添加启动时注册路由服务。

import { bootstrap }        from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router';

import { AppComponent }     from './app.component';
bootstrap(AppComponent, [
  ROUTER_PROVIDERS
]);

从路由模块中导入 ROUTER_PROVIDERS,它实现一个默认配置的项,并注入到 provider 中。

4、AppComponent组件

和往常一样 AppComponent 组件是我们根组件,模板里包括两个导航链接和一个Router Outlet,通过点击导航链接显示不同的组件。

shell-and-outlet

对应的模板:

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

RouterOutlet

组件模板中 <router-outlet> 标签就是点位符,告诉命中路由的组件放在哪。

RouterLink属性绑定

RouterLink 属性指令,模板表达式的右边是一个数组对象来表示路由器导航所需要的数据:

  • 下标0是固定的URL部分。
  • 下标1以后放置的是可选参数数据。
<a [routerLink]="['/heroes']">Heroes</a>

除此之外,我们还可以在组件类里面使用 Route 导航管理器进行跳转。

this.router.navigate(['/heroes']);

总结

对于组件而言是一个带有层级结构的,那么当一个组件被放置时就已经被确立其URL地址结构了;所以组件自身理应包括路由的定义。如何更好在代码里面编写路由注册代码,应该在构建项目时就要加以考虑。

路由这一课题Angular2.x一直在变动,但整体的使用上并没有太多不同。

Angular2 结构指令

一个单页应用程序(SPA)最大特征就是对DOM树的操作,是根据程序状态对整段DOM的显示和消失。

一、什么是结构指令

Angular有三种指令类型:

  1. 组件
  2. 属性指令
  3. 结构指令

组件实际是一个带有模板的指令,这是最常见的三种指令,我们的应用程序也是基于指令来构建的。

属性指令改变的是元素的外观或行为,例如内置 NgStyle 指令,可以根据绑定组件的属性值同时改变多个元素的样式。

而结构指令是对DOM的添加或移除,这会改变DOM的布局。在模板语法章节就介绍过 ngIfngSwitchngFor

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>
<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

二、NgIf 案例研究

ngIf 指令通过一个布尔类型变量来改变DOM的显示或消失。

<p *ngIf="condition">
  condition is true and ngIf is true.
</p>
<p *ngIf="!condition">
  condition is false and ngIf is false.
</p>

ngIf 指令并不是隐藏元素,使用浏览器开发工具我们可以看到,当条件为 true 时显示第一段DOM,反之显示第二段。

为什么要移除而不是隐藏?

我们可以通过设置CSS display 样式为 none 来隐藏元素,元素保留在DOM中只是看不见而已。相反,就用 ngIf

当我们隐藏一个元素,组件的行为还是继续着,像事件监听、变化检测、子组件的事件监听等依然还会在执手,哪怕我们压根看不见,很明显隐藏这一点来说最大的可能是性能问题;当然他也有好处,那就是当元素想要再一次被显示时,会非常快。

ngIf 就不同,设置为 false 时,会释放组件及其子组件的所有资源,并从DOM里移除元素;当然坏处就是,当你很快又需要该元素时,就要重新构建了。

虽然两种方法都有利弊,如果我们确认不使用的元素在短时间内不会再用那么 ngIf 最合理,反之。

接下来,我们用一个示例来说明二者的区别,通过设置css样式和 ngIf 对一个组件 heavy-loader (假设组件非常复杂)的显示或隐藏。

<div><!-- Visibility -->
  <button (click)="isVisible = !isVisible">show | hide</button>
  <heavy-loader [style.display]="isVisible ? 'inline' : 'none'" [logs]="logs"></heavy-loader>
</div>
<div><!-- NgIf -->
  <button (click)="condition = !condition">if | !if</button>
  <heavy-loader *ngIf="condition" [logs]="logs"></heavy-loader>
</div>
<h4>heavy-loader log:</h4>
<div *ngFor="let message of logs">{{message}}</div>

同时还记录组件的创建和销毁日志,这是利用内置 ngOnInitngOnDestroy 生命周期钩子来实现,效果如下:

heavy-loader-toggle

一开始在DOM都显示这两个组件,首先我们反复切换可见性,利用CSS样式由于每一次的可见性都是同一个实例,所以日志没有任何改变。

相反,当我们反复切换第二 ngIf 的可见性时,每一次都会出现创建新实例和销毁日志。

三、<template> 标签

结构指令像 ngIf,使用HTML template标签。对于非Angular应用而言 <template> 标签 display 样式值为 none,内容是不可见的文档片断;而Angular应用中,会移除所有 <template> 标签,并存储起来。

比如以下代码片断,中间的 Hip! 是用一个 <template> 包裹着。

<p>
  Hip!
</p>
<template>
  <p>
    Hip!
  </p>
</template>
<p>
  Hooray!
</p>

左边是非Angular应用,右边是Angular应用,二者的DOM区别。

template-in-out-of-a2

显然,默认情况下Angular会把 <template> 标签替换成一个空内容的 <script> 标签(虽然我本地的测试里并不会有这个空的script标签),不过呢,他可以做更多的事,比如 ngSwitch 指令在 <template> 标签中的应用:

<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

当其中一个 ngSwitch 条件为 true 时,Angular会把标签里模板内容插入DOM当中。

四、关于指令中的 * 号

看看下面代码有什么特别的?

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>

上面指令名称有前缀星号(*),星号是一个语法糖,简化了 ngIfngFor 二者的读写,在模板引擎里最后还是会转换成 <template> 模式。

以下是 ngIf 语法糖与 <template> 的写法:

<!-- Examples (A) and (B) are the same -->
<!-- (A) *ngIf paragraph -->
<p *ngIf="condition">
  Our heroes are true!
</p>

<!-- (B) [ngIf] with template -->
<template [ngIf]="condition">
  <p>
    Our heroes are true!
  </p>
</template>

只要不傻肯定用第一种写法。值得注意的是从A扩展到B,先将段落及其内容移到 <template> 标签里面,属性绑定部分作为 <template> 标签的指令。

接下来看看 *ngFor 是如何转换:

<!-- Examples (A) and (B) are the same -->

<!-- (A) *ngFor div -->
<div *ngFor="let hero of heroes">{{ hero }}</div>

<!-- (B) ngFor with template -->
<template ngFor let-hero [ngForOf]="heroes">
  <div>{{ hero }}</div>
</template>

基本上和 ngIf 如出一辙;额外的细微是多了 ngForOf 属性绑定和 hero 本地模板变量。

五、创建结构指令

让我们创建一个 Unless 指令,类似 ngIf,只不过条件是反着的,当为 false 时才会显示内容。创建指令类似于创建组件:

  • 导入 Directive 装饰器。
  • 添加指令的CSS属性选择器标识符。
  • 指定一个公共的 Input 属性属性用于绑定(通常是指令的名字)。
  • 指令类实现。

先从以下代码开始:

import { Directive, Input } from '@angular/core';

@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
}

指令的选择器用的 [myUnless],了解CSS选择器的就很清楚,用中括号来表示某个HTML元素属性。同时命名也尽可能避免 ng 开头,这一点说过很多次,无非就是害怕冲突。

现在还需要分别定义一个 TemplateRef 用来访问模板内容,以及 ViewContainerRef 用来访问视图容器。

constructor(
  private templateRef: TemplateRef<any>,
  private viewContainer: ViewContainerRef
  ) { }

重点来了,给指令定义一个输入属性 myUnless (名称和 selector 一样),并且接收一个布尔类型,用于条件判断添加或移除模板内容。

@Input() set myUnless(condition: boolean) {
  if (!condition) {
    this.viewContainer.createEmbeddedView(this.templateRef);
  } else {
    this.viewContainer.clear();
  }
}

整体的逻辑非常简单,指令所有代码如下:

import { Directive, Input } from '@angular/core';
import { TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
    ) { }
  @Input() set myUnless(condition: boolean) {
    if (!condition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

现在来试试怎么运用这个指令,同 *ngIf 一样,只不过改成 *myUnless

<p *myUnless="condition">
  condition is false and myUnless is true.
</p>

<p *myUnless="!condition">
  condition is true and myUnless is false.
</p>

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

Theme by cipchk

to top