Angular2 简介

jopen 9年前

Angular2 简介

声明:Angular2 现在还处于开发预览版,所以会有很多功能缺失,非兼容性升级,并且也会存在各种变化。

那么准备好了么?Yeah!

在这个实例中,我们需要用 german words 编写一个应用。如果用户通过认证的话就将这个单词解码。

第一步,从这里把后端服务KOA抓下来,然后执行:

$ npm install  $ node server.js

注意:你需要 node 0.11+ 版本或者 io.js 来运行 koa。如果选择 node 的话,需要用 --harmony 来启动服务:

$ node --harmony server.js

我们边启动服务边把这个库 clone 下来,切到 before 目录下,使用命令:

$ npm install  $ npm start

我从我的朋友Pawel那里搞到了这个代码模板然后修改了一下,同时也感谢mgontogdi2290

让我来简单解释一下:

由于 Angular 2 还不是最终版本,所以它需要配置一大堆模板代码,而这个框架就是做这个的。

一方面,我们用经典的 javascript 和 css 任务流管理工具 gulpfile 来处理angular 和它的路由规则。另一方面,我们用 index.html 来加载所有我们需要的Angular 2 的库。如我之前所说,在 Angular 2 发布最终版之前, 我们需要一大堆库来运行 Angular 2, 但不用担心,最终版是不需要这么麻烦的。

另一个有趣的东西是 System 模块。System 是ES6的模块加载器,也是我们的应用启动模块。耶,再也不需要一大堆script标签了,耶!

当然,也不会再有 ng-app 了 : )

那么,System 模块应该加载 index 来启动我们的应用程序,对吧?那么该怎么做呢?想要启动 Angular 2 应用,我们需要将入口组件传入 bootstrap(译者注:bootstrap)方法。那入口组件是什么玩意?我们马上就会知道了 :)

bootstrap(OurMainComponent);

在我们的应用程序中,需要创建一个App组件来引导启动我们的应用程序:

index.js    import { bootstrap } from 'angular2/angular2';  import { App } from './app/app';    bootstrap(App);

我们只需要导入我们的 App 组件和 bootstrap 方法,然后就可以使用 bootstrap 方法启动应用了。我们的应用还需要引入路由(译者注:router)组件,由于路由组件是 angular 2 的外部组件,所以我们也需要把它作为依赖加载进来。直到今天(2015-05-04),路由模块也还不能导出必要的注入使其更加易用,所以我们需要构造一个新的路由实例然后将它注入进去:

index.js    import { bootstrap } from 'angular2/angular2';  import { RootRouter } from 'angular2/src/router/router';  import { Pipeline } from 'angular2/src/router/pipeline';  import { bind } from 'angular2/di';  import { Router } from 'angular2/router';    import { App } from './app/app';    bootstrap(App, [    bind(Router).toValue(new RootRouter(new Pipeline()))  ]);

我们加载了所有构造路由需要的依赖,并且用 bind(译者注:bind)服务创建了一个路由的绑定。希望能尽早修复这个问题。

好了,现在让我们正式开始写 App 组件的代码吧。不过,组件到底是什么?组件只是一个类,可以用来表示一个 home 页面,或者一个 login 模块,users 信息模块……或者可以用来创建一个类似于 datepicker,tabs 等的指令(译者注:指令)。

app/app.js    export class App {    }

如我所说,组件只是一个类(我们把它导出以便于可以在其他文件中导入使用,就像我们在 index 中导入其他组件一样)。到目前为止好像我们还是没都没做,让我们动起来!

在 Angular 2 中我们可以使用注解(译者注:[annotations][10])。想象一下通过注解的方式给我们的类添加元数据。我们一步一步来。第一步,导入我们需要的两个注解:

app/app.js    import {View, Component} from 'angular2/angular2';

然后我们就可以使用了,Component 注解用来给组件自身添加元数据,包括组件的选择器,以及需要注入的服务。View注解用来添加 HTML 模板,我们可以给组件指定想要使用的模板,以及指令等。我们可以加入多个 View 注解(mobile 视图,desktop 视图等)。

让我们用起来:

app/app.js    @Component({    selector: 'words-app'  })  @View({    template: `<h1>Hello angular 2</h1>`  })  export class App {    }

我们给 Component 指定了组件的选择器 words-app(唉呀妈呀,终于不用再纠结像Angular 1 里面是驼峰命名还是下划线命名的问题了),这意味着如果我们想在任何一个地方用这个组件,只需要写上<words- app></words-app>就可以了。

这次我们先给 View 创建一个简单的模板【注意引号(译者注:这里也可以用单引号,双引号)】。

注意:不要在注解后面加分号,那会把Angular 2搞哭的 :)

你说过可以把这个选择器放在任何地方?耶,我们来修改一下 index.html:

index.html    <body>      <div class="container">          <words-app>              Loading...          </words-app>      </div>  </body>

好了,现在我们来运行下我们的组件,在浏览器中访问 localhost:3000

到目前为止,一切都还好,对吧?现在我们需要配置一下从 bootstrap 函数中注入的路由。

app/app.js    export class App {    constructor(router: Router) {      }  }

构造函数会接收一个 Router 类型的参数 router:

app/app.js    export class App {    constructor(router: Router) {      router        .config('/home', Home)        .then(() => router.navigate('/home'));    }  }

router 有一个 config 方法,我们可以通过传递 path 为组件配置路由规则,然后通过链式调用 promise 的方式将路由导航到 /home。

不久以后,我们就可以用注解的方式为每个组件配置路由了。不过现在我们还是得用这种方式配置路由。

当我们通过一个新的路由加载组件时,组件生成的 View 放在哪里呢?我们需要为这个新的组件添加一个叫做 router-outlet的 ng-view。

我们把 View 的注解稍作修改:

app/app.js    @View({    template: `<router-outlet></router-outlet>`,    directives: [RouterOutlet]  })

这一点非常重要:当模板用到指令/组件的时候,我们需要把 RouterOutlet 导入进来。注意,我们把导入的 RouterOutlet对象添加到了 directives 数组中。说到导入…… 我们还需要导入几个组件。导入这几个组件后,我们的代码如下:

app/app.js    import {View, Component} from 'angular2/angular2';  import {Router, RouterOutlet} from 'angular2/router';  import {Home} from '../home/home';

我们加载了 Router, RouterOutlet 和 Home 组件。

现在我们的应用会直接跳转到 Home 组件。我们来写这个组件:

home/home.js    import {View, Component} from 'angular2/angular2';    @Component({    selector: 'home'  })  @View({    templateUrl: 'home/home.html'  })  export class Home {    }

这次我们用引用外部文件的方式代替直接在注解中嵌入模板的方式:

home/home.html    <h1>This is home</h1>

在启动我们的应用之前,我们把 bootstrap 引入到我们的 index.css 中:

index.css    @import 'bootstrap';

现在我们的应用看起来应该是这样的:

现在我们来做点儿有用的东西?在这个 home 组件中,我们希望每次点击按钮的时候都可以获取一个新的单词。我们需要创建一个 Words 的服务来处理请求和数据:

services/words.js    export class Words {    getWord() {      var jwt = localStorage.getItem('jwt');        return fetch('http://localhost:3001/api/random-word', {        method: 'GET',        headers: {          'Accept': 'application/json',          'Content-Type': 'application/json',          'Authorization': 'bearer ' + jwt        }      })      .then((response) => {        return response.text();      })      .then((text) => {        return JSON.parse(text);      })    }  }

服务在 Angular 2 中只是一个类,对于这个 Words 服务而言,我们只需要一个获取单词的方法而已。到今天为止,Angular 2 还没有一个类似于 $http 的库,所以我们需要使用一个名为 fetch 的库。

我们使用 fetch 向服务端发起一个请求,如果有 JWT 令牌的话解析之,我们从服务器端响应中提取返回的文本并解析成JSON 对象,然后再下次调用 .then 方法的时候,我们就能拿到这个 JSON 对象。

我们在 Home 中导入这个服务:

home/home.js    import {Words} from '../services/words';

现在我们需要在组件中注入这个服务:

home/home.js    @Component({    selector: 'home',    injectables: [Words]  })

最后,我们给构造函数传入这个服务的实例对象:

home/home.js    export class Home {    constructor(words: Words) {      this.words = words;    }  }

就像 router 对象,我们得到一个 Words 类型的实例 words。

服务代码做好之后,现在我们来编写模板代码:

home/home.html    <div class="jumbotron centered">    <h1>German words demo!</h1>    <p>Click the button below to get a random German word with its translation:</p>    <p><a class="btn btn-primary" role="button" (click)="getRandomWord()">Give me a word!</a></p>    <div *if="word">      <pre>Word: {{word.german}}</pre>    </div>  </div>

这里有几个 Angular 2 的新语法糖。我们用(click)代替了 angular 1 中的 ng-click。圆括号表示这是一个事件(点击事件)。另一个语法糖,*if 就是我们经典的 ng-if。符号*表示这是一个模板,一般用来做一些简写,相当于:

<template [if]="word">

那 if 是一个指令(译者注:指令)么?是的。如果我们需要在模板中使用指令的话,之前怎么说来着?嗯,我们需要导入这些玩意:

home/home.js    import {View, Component, If} from 'angular2/angular2';

然后:

home/home.js    @View({    templateUrl: 'home/home.html',    directives: [If]  })

下一步,我们需要在点击 button 的时候显示服务器端返回的单词:

home/home.js    getRandomWord() {    this.words.getWord().then((response) => {      this.word = response;    });  }

再试着运行一下我们的应用,我们会看到:

嗯,终于像那么回事儿了!

现在我们来做个身份认证。你说是一个服务?没错!

export class Auth {    constructor() {      this.token = localStorage.getItem('jwt');      this.user = this.token && jwt_decode(this.token);    }      isAuth() {      return !!this.token;    }      getUser() {      return this.user;    }      login(username, password) {      return fetch('http://localhost:3001/sessions/create', {        method: 'POST',        headers: {          'Accept': 'application/json',          'Content-Type': 'application/json'        },        body: JSON.stringify({          username, password        })      })      .then((response) => {        return response.text();      })      .then((text) => {        this.token = JSON.parse(text).id_token;        localStorage.setItem('jwt', this.token);      });    }      logout() {      localStorage.removeItem('jwt');      this.token = null;      this.user = null;    }  }

我们存储两个字段,一个是令牌,一个是解码后的令牌,然后我们需要一个 login 方法和logout 方法来做登录和登出。没什么特别的东西。localStorage 和 jwt_decode 都是全局的,不需要导入。我们再次使用 fetch 函数在服务器端处理请求。

另外,既然我们说到了登录,那么我们就需要一个登录的途径,对吧?我们来创建一个登录组件:

login/login.js    import {Component, View} from 'angular2/angular2';  import {Router} from 'angular2/router';  import {Auth} from '../services/auth';    @Component({    selector: 'login',    injectables: [Auth]  })  @View({    templateUrl: 'login/login.html'  })  export class Login {    constructor(router: Router, auth: Auth) {      this.router = router;      this.auth = auth;    }      login(event, username, password) {      event.preventDefault();      this.auth.login(username, password).then(() => {        this.router.parent.navigate('/home');      })      .catch((error) => {        alert(error);      });    }  }

就像其他组件,我们需要 Component 和 View 等注解,还需要注入一个叫做 Auth 的新服务。

这个组件只有一个方法:用 Auth 服务处理登录并且把 jwt 令牌塞到 localStorage 里面。如果成功的话,我们就跳转到/home 页面。

编写我们的模板代码:

login/login.html    <div class="login jumbotron center-block">    <h1>Login</h1>    <form role="form" (submit)="login($event, username.value, password.value)">      <div class="form-group">        <label for="username">Username</label>        <input type="text" #username class="form-control" id="username" placeholder="Username">      </div>      <div class="form-group">        <label for="password">Password</label>        <input type="password" #password class="form-control" id="password" placeholder="Password">      </div>      <button type="submit" class="btn btn-default">Submit</button>    </form>  </div>

注意我们用 #username 代替了 ng-model="username" 。这是 Angular 2 中的绑定方式。

最后一个步,我们简单修改一下 css,因为表单稍微有点宽:

login/login.css    .login {    width: 40%;  }

同样需要导入这些 .css 文件:

index.css    @import 'bootstrap';  @import './src/login/login.css';

为了展示 login 组件,我们需要与 Home 组件建立连接:

home/home.html    <div class="jumbotron centered">    <h1>German words demo!</h1>    <p>Click the button below to get a random German word with its translation:</p>    <p><a class="btn btn-primary" role="button" (click)="getRandomWord()">Give me a word!</a></p>    <div *if="word">      <pre>Word: {{word.german}}</pre>    </div>  </div>  <div *if="isAuth">    <p>Welcome back {{user.username}}</p>    <a href="#" (click)="logout($event)">Logout</a>  </div>  <div *if="!isAuth">    <a href="#" (click)="login($event)">Login</a>  </div>

我们有了一个名为 isAuth 的标识,可以让我们在两个 div 之间进行切换。

我们也需要将 Auth 服务导入进来:

home/home.js    import {Auth} from '../services/auth';    @Component({    selector: 'home',    injectables: [Words, Auth]  })

然后我们需要 login 和 logout 方法,isAuth 标志值,并将其关联到我们的用户。现在我们的组件是这样的:

home/home.js    export class Home {    constructor(router: Router, words: Words, auth: Auth) {      this.router = router;      this.auth = auth;      this.words = words;        this.isAuth = auth.isAuth();        if (this.isAuth) {        this.user = this.auth.getUser();      }    }      getRandomWord() {      this.words.getWord().then((response) => {        this.word = response;      });    }      login(event) {      event.preventDefault();      this.router.parent.navigate('/login');    }      logout(event) {      event.preventDefault();      this.auth.logout();      this.isAuth = false;      this.user = null;    }  }

在不久的将来,就像基础组件 ui-sref 一样,RouteLink 组件可以让我们避免 login 这种做法。

好的,现在让我们链接到 login 页面:

然后我们点击 login,我们就会……哦 稍等,它没反应了。 啊,我们忘了把 login 的路由加到 app.js 里:

app/app.js    import {Login} from '../login/login';    export class App {    constructor(router: Router) {      router        .config('/home', Home)        .then(() => router.config('/login', Login))        .then(() => router.navigate('/home'));    }  }

现在它好了:

然后我们用示例账户(demo / 12345)登录,我们会看到:

爽!

最后一步,当我们登录进去之后我们需要把单词解码,这个比较简单。我们已经从服务器取到了单词,并且现在服务器认为我们处于登录状态,所以它也会把解码后的单词返回回来。这意味着我们只需要更新我们的模板就可以了:

home/home.html    <div class="jumbotron centered">    <h1>German words demo!</h1>    <p>Click the button below to get a random German word with its translation:</p>    <p><a class="btn btn-primary" role="button" (click)="getRandomWord()">Give me a word!</a></p>    <div *if="word">      <pre>Word: {{word.german}}</pre>      <pre *if="isAuth">Translation: {{word.english}}</pre>      <p *if="!isAuth">Please login below to see the translation</p>    </div>  </div>  <div *if="isAuth">    <p>Welcome back {{user.username}}</p>    <a href="#" (click)="logout($event)">Logout</a>  </div>  <div *if="!isAuth">    <a href="#" (click)="login($event)">Login</a>  </div>

现在我们终于完成了应用:

噢 稍等,我忘了登录:

我希望你学习 Angular 2 能比学图片中的单词还要快 :)

在结束这篇文章之前,作为一个有趣的探索,你可以把 Login 组件导入到 Home 中,然后把 Login 作为一个指令加入到View 的指令数组中,接着把 <login></login> 添加到模板的底部。这样做之后,你就可以在的 Home 页面使用 login 组件完整的表单和功能了,耶!

我想感谢Matias GontovnikasPatrickJS的demo angular2-authentication-sample和他们的解释,这给我理解整个流程提供了巨大的帮助。

Jesus Rodriguez 发表于 2015年5月4日

原文地址:An introduction to Angular 2

外刊君推荐阅读:

来自:http://zhuanlan.zhihu.com/FrontendMagazine/20058966