打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
如何利用angular

导语

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 nrmnrm use taobaonpm install

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

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

升级老项目也比较简单:

-- windows下使用管理员模式CMD1、全局版本npm uninstall -g @angular/clinpm cache cleannpm install -g @angular/cli@latest2、项目版本,先删除node_modulesnpm install --save-dev @angular/cli@latestnpm 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/209export 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-12export 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>-- jsthis.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>`-- jspublic 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(),这简直就是多此一举好吗?

-- promisethis.http.get(``).toPromise().then();-- Observatethis.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。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
angular2实例
Angular 应用里的 public_api.ts 文件的作用
.Net Core应用框架Util介绍(四)
Angular前端来源框架
前端开发框架之Angular自定义组件学习分享
Angular 10材质的模态弹出示例和教程
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服