[关闭]
@adonia 2016-10-09T16:40:57.000000Z 字数 13461 阅读 190

angular2(3): 构建简单的Reddit应用

angular2


在上一章节,介绍了开发angular2应用的基本流程---搭建环境、管理依赖、创建组件、编译运行等。

下面,将一步步构建一个稍复杂的应用,效果如下:

image_1at62r0elcjjmdv1lvil8q1q4k9.png-75.3kB

首先,将基于上一章节的代码为基础,一步步地介绍如何开发。

增加bootstrap样式

修改index.html内容如下:

  1. <!DOCTYPE html>
  2. <html lang="zh-cn">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <title>Reddit</title>
  8. <script src="node_modules/es6-shim/es6-sham.js"></script>
  9. <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
  10. <script src="node_modules/systemjs/dist/system.src.js"></script>
  11. <script src="node_modules/rxjs/bundles/Rx.js"></script>
  12. <script src="node_modules/angular2/bundles/angular2.dev.js"></script>
  13. <script src="node_modules/jquery/dist/jquery.min.js"></script>
  14. <script src="node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
  15. <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css"></link>
  16. </head>
  17. <body>
  18. <script>
  19. System.config({
  20. packages: {
  21. app: {
  22. format: 'register',
  23. defaultExtension: 'js'
  24. }
  25. }
  26. });
  27. System.import('app.js').then(null, console.error.bind(console));
  28. </script>
  29. <div class="container">
  30. <reddit></reddit>
  31. </div>
  32. </body>
  33. </html>

Tips:

  • 第6,7行是为了适应IE浏览器和设备尺寸;

  • 第16,17行为bootstrap的javascript依赖,需要注意的是,jquery的依赖,必须放置在bootstrap的上方;

  • 第19行为bootstrap的样式文件

  • 第35行中的container是bootstrap的样式风格

运行 npm run go,访问localhost:8080查看页面样式是否正确。

开发Form表单

让我们修改app.ts中的内容,使用bootstrap来修饰上图效果中的form表单。代码如下:

  1. import {bootstrap} from "angular2/platform/browser";
  2. import {Component} from "angular2/core";
  3. @Component({
  4. selector: 'reddit',
  5. template: `
  6. <form role="form">
  7. <div class="form-group">
  8. <h3>Add a link</h3>
  9. </div>
  10. <div class="form-group">
  11. <label for="titleInput">Title</label>
  12. <input type="text" class="form-control" id="titleInput" placeholder="title of article">
  13. </div>
  14. <div class="form-group">
  15. <label for="linkInput">Link</label>
  16. <input type="text" class="form-control" id="linkInput" placeholder="link of article">
  17. </div>
  18. <button class="btn btn-info">Submit</button>
  19. </form>
  20. `
  21. })
  22. class RedditApp {}
  23. bootstrap(RedditApp);

这里,我们创建了个RedditApp组件,其中包括了两个input,分别用来输入 ‘文章标题’ 和 ‘文章链接’;以及一个按钮,用于提交请求。保持npm run go命令处于运行状态,会自动检测到app.ts的变化,这时重新刷新页面,得到的结果如下图:

image_1at6qih3k1mnk8vd1c4javn1vs69.png-16.5kB

下面,为Submit按钮增加个事件---在点击按钮时,获取titlelink两个input输入的内容,并做后续的处理。修改后的app.ts如下:

  1. @Component({
  2. selector: 'reddit',
  3. template: `
  4. <form role="form">
  5. <div class="form-group">
  6. <h3>Add a link</h3>
  7. </div>
  8. <div class="form-group">
  9. <label for="title">Title</label>
  10. <input type="text" class="form-control" name="title" placeholder="title of article" #newTitle>
  11. </div>
  12. <div class="form-group">
  13. <label for="link">Link</label>
  14. <input type="text" class="form-control" name="link" placeholder="link of article" #newLink>
  15. </div>
  16. <button class="btn btn-info" (click)="addArticle(newTitle, newLink)">Submit</button>
  17. </form>
  18. `
  19. })
  20. class RedditApp {
  21. addArticle(title: HTMLInputElement, link: HTMLInputElement) {
  22. console.log(`adding article, title: ${title.value}, link: ${link.value}`);
  23. }
  24. }

Notes:

  • 在第10行,title输入框中增加个属性#newTitle

  • 在第14行,link输入框中增加个属性#newLink

  • 在第16行,Submit按钮增加个事件---(click)="addArticle(newTitle, newLink)"

开发Article展示列表

开发Article展示列表(1)

app.ts中增加用于展示文件列表的组件(追加在bootstrap(RedditApp);的下方),代码如下:

  1. @Component({
  2. selector: 'reddit-article',
  3. host: {
  4. class: 'row'
  5. },
  6. template: `
  7. <div class="container">
  8. <div class="row">
  9. <div class="col-md-2 text-center" style="background-color: #eee;">
  10. <h2>{{votes}}</h2>
  11. <h4>POINTS</h4>
  12. </div>
  13. <div class="col-md-10">
  14. <div style="margin-left: 16px;">
  15. <a href="{{link}}"><h4>{{title}}</h4></a>
  16. </div>
  17. <ul class="nav navbar-nav pull-left">
  18. <li>
  19. <a href (click)="voteUp()">
  20. <li class="glyphicon glyphicon-arrow-up"></li>
  21. upvote
  22. </a>
  23. </li>
  24. <li>
  25. <a href (click)="voteDown()">
  26. <li class="glyphicon glyphicon-arrow-down"></li>
  27. downvote
  28. </a>
  29. </li>
  30. </ul>
  31. </div>
  32. </div>
  33. </div>
  34. `
  35. })
  36. class ArticleComponent {
  37. // 点赞数
  38. votes: number;
  39. title: string;
  40. link: string;
  41. constructor() {
  42. this.votes = 10;
  43. this.title = 'angular2';
  44. this.link = 'http://angular.io';
  45. }
  46. voteUp() {
  47. this.votes += 1;
  48. }
  49. voteDown() {
  50. if(this.votes > 0) {
  51. this.votes -= 1;
  52. }
  53. }
  54. }
  55. bootstrap(ArticleComponent);

这里呢,先做个简单的示例,定义了ArticleComponent类,在构造方法中对其进行了初始化。我们先用默认值来看下,如何去渲染预期效果图的Article中的一列。

首先,Article中包含了三个属性---votes(点赞数),title(文章标题),link(文章链接);两个操作---点赞和不喜欢。三个属性的对应关系与表单组件中的是一致的,这里就不提了。两个操作,分别对应的是类ArticleComponent中的voteUpvoteDown方法。

组件定义的选择器为reddit-article,将其增加至index.html中,如下:

  1. <div class="container">
  2. <reddit></reddit>
  3. <reddit-article></reddit-article>
  4. </div>

执行npm run go命令,运行应用,效果如下:

image_1at9cao32o7t61h1l6q1err1unc9.png-29.1kB

可以看出,跟预期的基本一致。点击upvotedownvote按钮发现,虽然点赞数跟着发生了变化,但是,紧跟着,整个页面自动刷新了,ArticleComponent组件被重新初始化,点赞数又回到了默认的10。这是为什么呢?

在Javascript中,点击事件的默认机制是会将事件处理向上转移。也就是说,在upvote按钮点击时,首先触发了voteUp方法,将点赞数加1;紧接着,点击事件被传递给父组件--- <a href></a>,页面跳转到空链接,整个页面重新刷新。

那么,为了解决这个问题,需要在两个事件处理方法中增加return false,如下:

  1. voteUp() {
  2. this.votes += 1;
  3. return false;
  4. }
  5. voteDown() {
  6. if(this.votes > 0) {
  7. this.votes -= 1;
  8. }
  9. return false;
  10. }

修改app.ts,重新刷新页面,再次测试下。

再回到ArticleComponent组件的定义中,在注解中,增加个选项,如下:

  1. host: {
  2. class: 'row'
  3. }

这里的作用呢,是相当于在组件中增加个样式,等同于<reddit-article class="row"><reddit-article>。其实,将该样式增加到template中的顶级div中,效果也是一样的。

可以将上述host配置移除,看看效果变化。

开发Article展示列表(2)

先来回顾下目前的实现,看下app.tsindex.html的代码结构:

--app.ts:

  1. @Component({
  2. selector: 'reddit',
  3. template: `...`
  4. })
  5. class RedditApp {
  6. }
  7. bootstrap(RedditApp);
  8. @Component({
  9. selector: 'reddit-article',
  10. template: `...`
  11. })
  12. class ArticleComponent {
  13. }
  14. bootstrap(ArticleComponent);

--index.html:

  1. <div class="container">
  2. <reddit></reddit>
  3. <reddit-article></reddit-article>
  4. </div>

app.ts中定义了两个组件---redditreddit-article,分别用来渲染 表单 和 文章列表项。两个组件都通过bootstrap引导。相应地,在index.html中,也使用两个标签。

但这样的使用方式并不好,表单和文章列表项对外应该是一体的,也就是说,在index.html中,应该只需要引用reddit标签就可以了。下面就看下如何将reddit-article组件合并至reddit

首先,将reddit-article标签从index.html中移出,放置到RedditApptemplate中的form标签下方,其余保持不变。此时,app.ts中的内容如下:

  1. @Component({
  2. selector: 'reddit',
  3. template: `
  4. <form role="form">
  5. <div class="form-group">
  6. <h3>Add a link</h3>
  7. </div>
  8. <div class="form-group">
  9. <label for="title">Title</label>
  10. <input type="text" class="form-control" name="title" placeholder="title of article" #newTitle>
  11. </div>
  12. <div class="form-group">
  13. <label for="link">Link</label>
  14. <input type="text" class="form-control" name="link" placeholder="link of article" #newLink>
  15. </div>
  16. <button class="btn btn-info" (click)="addArticle(newTitle, newLink)">Submit</button>
  17. </form>
  18. <reddit-article></reddit-article>
  19. `
  20. })
  21. class RedditApp {
  22. addArticle(title: HTMLInputElement, link: HTMLInputElement) {
  23. console.log(`adding article, title: ${title.value}, link: ${link.value}`);
  24. }
  25. }
  26. bootstrap(RedditApp);
  27. @Component({
  28. selector: 'reddit-article',
  29. host: {
  30. class: 'row'
  31. },
  32. template: `
  33. <div class="container">
  34. <div class="row">
  35. <div class="col-md-2 text-center" style="background-color: #eee;">
  36. <h2>{{votes}}</h2>
  37. <h4>POINTS</h4>
  38. </div>
  39. <div class="col-md-10">
  40. <div style="margin-left: 16px;">
  41. <a href="{{link}}"><h4>{{title}}</h4></a>
  42. </div>
  43. <ul class="nav navbar-nav pull-left">
  44. <li>
  45. <a href (click)="voteUp()">
  46. <li class="glyphicon glyphicon-arrow-up"></li>
  47. upvote
  48. </a>
  49. </li>
  50. <li>
  51. <a href (click)="voteDown()">
  52. <li class="glyphicon glyphicon-arrow-down"></li>
  53. downvote
  54. </a>
  55. </li>
  56. </ul>
  57. </div>
  58. </div>
  59. </div>
  60. `
  61. })
  62. class ArticleComponent {
  63. // 点赞数
  64. votes: number;
  65. title: string;
  66. link: string;
  67. constructor() {
  68. this.votes = 10;
  69. this.title = 'angular2';
  70. this.link = 'http://angular.io';
  71. }
  72. voteUp() {
  73. this.votes += 1;
  74. return false;
  75. }
  76. voteDown() {
  77. if(this.votes > 0) {
  78. this.votes -= 1;
  79. }
  80. return false;
  81. }
  82. }
  83. bootstrap(ArticleComponent);

index.html中的内容如下:

  1. <div class="container">
  2. <reddit></reddit>
  3. </div>

在工程根目录下执行npm run go,访问http://localhost:8080查看是不是预期的效果。

按照上面的修改,将组件reddit-article放置在组件reddit中,但是,依然有显式引导ArticleComponent类。如果注释bootstrap(ArticleComponent);,待重新编译app.ts后,再次访问http://localhost:8080,会发现文章列表模块不见了。在浏览器中按下F12,查看页面元素,会发现reddit-article组件并没有被解析:

image_1atnm282u1314t5v12l0s6tmc79.png-33.2kB

之前通过bootstrap(ArticleComponent);方式来引导组件,这样在RedditApp能够识别组件ArticleComponent。通过bootstrap的方式引导的组件,是全局的;就如同在angular1中定义的指令一般。但是,在angular2中,如果要引用组件,就需要显式声明引用。那么,如果想要在RedditApp中使用ArticleComponent组件,需要在RedditApp中增加directives属性,如下:

  1. @Component({
  2. selector: 'reddit',
  3. directives: [ArticleComponent],
  4. template: `
  5. ...

重新编译app.ts后,再次访问http://localhost:8080,发现页面空白,打开F12,发现Console中有报错:

image_1atnmqjuraav1ru31aco1ttj1lg013.png-110kB

错误提示,RedditApp中的directive的值是undefined,说明directives: [ArticleComponent],中指明的组件ArticleComponent,在解析RedditApp的时候,并没有被初始化呢。现在要做的就是将ArticleComponent组件的定义移至RedditApp的上方。

现在的代码结构如下:

  1. @Component({
  2. selector: 'reddit-article',
  3. host: {
  4. class: 'row'
  5. },
  6. template: `...`
  7. })
  8. class ArticleComponent {
  9. ...
  10. }
  11. @Component({
  12. selector: 'reddit',
  13. directives: [ArticleComponent],
  14. template: `...`
  15. })
  16. class RedditApp {
  17. ...
  18. }
  19. bootstrap(RedditApp);

这次,页面的渲染结果就是预期的了。 具体参考完整代码

开发Article展示列表(3)

现在考虑如何在列表中展示多篇文章。

首先,Article包括三个属性---titlelinkvotes,就是现在ArticleComponent中的属性定义。那现在如果要在ArticleComponent中渲染多篇文章的话,就需要将Article抽象出来。在ArticleComponent的上方新建Article类:

  1. class Article {
  2. title: string;
  3. link: string;
  4. votes: number;
  5. constructor(title: string, link: string, votes: number) {
  6. this.title = title;
  7. this.link = link;
  8. this.votes = votes || 0;
  9. }
  10. }

定义了Article类之后,那在ArticleComponent中,就可以直接引用Article对象了,如下:

  1. class ArticleComponent {
  2. article: Article;
  3. constructor() {
  4. this.article = new Article('angular2', 'http://angular2.io', 10);
  5. }
  6. voteUp() {
  7. this.article.votes += 1;
  8. return false;
  9. }
  10. voteDown() {
  11. if(this.article.votes > 0) {
  12. this.article.votes -= 1;
  13. }
  14. return false;
  15. }
  16. }

这里定义了属性---article,类型是Article;定义的方式跟之前的title: string;相同,只不过这次,指定的类型是复杂对象。

注意,此时ArticleComponent中的属性变成了article,那么在template中的引用---titlelinkvotes,这些就无法直接解析了。需要分别以article.titlearticle.linkarticle.votes这种对象属性的方式引用了。如下:

  1. <div class="container">
  2. <div class="row">
  3. <div class="col-md-2 text-center" style="background-color: #eee;">
  4. <h2>{{article.votes}}</h2>
  5. <h4>POINTS</h4>
  6. </div>
  7. <div class="col-md-10">
  8. <div style="margin-left: 16px;">
  9. <a href="{{article.link}}"><h4>{{article.title}}</h4></a>
  10. </div>
  11. <ul class="nav navbar-nav pull-left">
  12. <li>
  13. <a href (click)="voteUp()">
  14. <li class="glyphicon glyphicon-arrow-up"></li>
  15. upvote
  16. </a>
  17. </li>
  18. <li>
  19. <a href (click)="voteDown()">
  20. <li class="glyphicon glyphicon-arrow-down"></li>
  21. downvote
  22. </a>
  23. </li>
  24. </ul>
  25. </div>
  26. </div>
  27. </div>

app.ts重新编译后,访问页面,应该是可以正常显示的。

再来审视下ArticleComponent类,其中的voteUp()voteDown()方法是直接操作的Article对象的属性,这不符合面向对象的思想,最好的方式是在Article中定义相应的方法,在ArticleComponent中,通过调用Article的方法的方式来修改其属性。

Article类中增加如下方法:

  1. voteUp() {
  2. this.votes += 1;
  3. }
  4. voteDown() {
  5. if(this.votes > 0) {
  6. this.votes -= 1;
  7. }
  8. }

然后,将ArticleComponent类中的方法修改为:

  1. voteUp() {
  2. this.article.voteUp();
  3. return false;
  4. }
  5. voteDown() {
  6. this.article.voteDown();
  7. return false;
  8. }

注意,此处方法的返回值不能丢 。

到现在为止,对照MVC框架中的概念,Article对应着ModelArticleComponent对应着Controller

具体参考完整代码

开发Article展示列表(4)

下面看下怎样去存储文章列表。

RedditApp中定义一个属性---articles,用来存储提交的文章列表,如下:

  1. class RedditApp {
  2. articles: Article[];
  3. constructor() {
  4. this.articles = [
  5. new Article('angualr2', 'http://angular2.io', 3),
  6. new Article('Fullstack', 'http://fullstack.io', 2),
  7. new Article('angular1', 'http://angular1.io', 1)
  8. ]
  9. }
  10. ...
  11. }

RedditApp中定义的articles的类型是Article[],指的是元素类型为Article的数组。也可以定义为Array[Article]

那如何将RedditApp中存储的Article集合传给ArticleComponent组件去渲染呢?
看下之前ArticleComponent的实现,接收的是单个Article对象。那是不是可以在<reddit-article>标签中增加个参数,然后通过标签的参数,将其传递给ArticleComponenet组件呢?例如:

  1. <reddit-article [article]='article1'></reddit-article>
  2. <reddit-article [article]='article2'></reddit-article>
  3. ...

angular2中提供了inputs属性,来支持上述特性。在ArticleComponent中增加inputs属性,如下:

  1. @Component({
  2. selector: 'reddit-article',
  3. inputs: ['article'],
  4. host: {
  5. class: 'row'
  6. },
  7. template: `
  8. ...

那么现在,就可以使用<reddit-article [article]='myArticle'></reddit-article>的形式,将myArticle传递给ArticleComponent类了。

N.B. : [article]中的article需要跟inputs中的保持一致。myArticle为传递的变量名,这里任意,只要跟后续使用到的保持一致即可。

通过[article]='myArticle'的方式,将外部Article对象作为参数的方式传给ArticleComponent类的话,之前的构造方法中的默认值就不需要了。需要将ArticleComponent类中的构造方法移除:

  1. class ArticleComponent {
  2. article: Article;
  3. voteUp() {
  4. this.article.voteUp();
  5. return false;
  6. }
  7. voteDown() {
  8. this.article.voteDown();
  9. return false;
  10. }
  11. }

现在,就来考虑下将RedditApp中的Article数组中的元素,一个个地,通过[article]参数的方式传递给ArticleComponent组件。遍历数组,就需要用到之前提到过的NgFor组件了。

首先,在app.ts中引入NgFor,如下:

  1. import {NgFor} from "angular2/common";

接着,将RedditApp中的template下的<reddit-article></reddit-article>标签替换成如下内容:

  1. <reddit-article *ngFor="#myArticle of articles" [article]="myArticle"></reddit-article>

Note:

  • *ngFor表示NgFor组件,跟之前介绍的一样;

  • [article]="myArticle"中,article需要跟inputs中的对应起来;myArticle即为NgFor中的变量名。也就是说,上述语句写成<reddit-article *ngFor="#foo of articles" [article]="foo"></reddit-article>也是可以的。但是不能写成<reddit-article *ngFor="#myArticle of articles" [foo]="myArticle"></reddit-article>

按照预期,可以在Form表单中输入titlelink信息,然后,点击submit按钮增加文章。对应的事件处理在RedditApp中的addArticle方法。

其实,addArticle方法的逻辑很简单,只需要将titlelink中的值取出,然后构造一个Article对象,放到articles数组中就可以了。如下:

  1. addArticle(title: HTMLInputElement, link: HTMLInputElement) {
  2. console.log(`adding article, title: ${title.value}, link: ${link.value}`);
  3. this.articles.push(new Article(title.value, link.value, 0));
  4. title.value = '';
  5. link.value = '';
  6. }

app.ts重新编译后,再次访问http://localhost:8080,验证下,应该可以正常处理的。

现在呢,想将点赞数较多的文章放在靠前的区域,类似一个排行榜的感觉。也就意味着,在每次点赞数发生变化时,按照点赞数从多到少进行排序,从而来展示文章列表。

首先,实现排序的功能,在RedditApp类中增加如下方法:

  1. sortedArticles(): Article[] {
  2. return this.articles.sort((a: Article, b: Article) => b.votes - a.votes);
  3. }

之前是对articles进行遍历展示---<reddit-article *ngFor="#myArticle of articles" [article]="myArticle"></reddit-article>。那现在要遍历的对象应该是排序后的结果,也就是sortedArticles()的返回值。那么,template中的遍历代码片段应该更改为:

  1. <reddit-article *ngFor="#myArticle of sortedArticles()" [article]="myArticle"></reddit-article>

N.B.: 此处是方法调用,不要忘记()

可参考完整代码

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注