Angular2 简介

4 年前

声明: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 还不是最终版本,所以它需要配置一大堆模板代码,而这个框架就是做这个的。

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

另一个有趣的东西是 System 模块。SystemES6的模块加载器,也是我们的应用启动模块。耶,再也不需要一大堆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 信息模块……或者可以用来创建一个类似于 datepickertabs 等的指令(译者注:指令)。

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

Hello angular 2

到目前为止,一切都还好,对吧?现在我们需要配置一下从 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-outletng-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';

我们加载了 RouterRouterOutletHome 组件。

现在我们的应用会直接跳转到 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';

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

2

现在我们来做点儿有用的东西?在这个 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;
  });
}

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

Alt text

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

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

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 方法来做登录和登出。没什么特别的东西。localStoragejwt_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);
    });
  }
}

就像其他组件,我们需要 ComponentView 等注解,还需要注入一个叫做 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]
})

然后我们需要 loginlogout 方法,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 页面:

Alt text

然后我们点击 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'));
  }
}

现在它好了:

Alt text

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

Alt text

爽!

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

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>

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

Alt text

噢 稍等,我忘了登录:

Alt text

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

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

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

Jesus Rodriguez 发表于 2015年5月4日

原文:http://angular-tips.com/blog/2015/05/an-introduction-to-angular-2/

外刊君推荐阅读:

Alt text

关注微博:前端外刊评论

0
推荐阅读