用Angular2+Express快速搭建博客

1. 写在前面

昨天花了1天的时间把自己的博客从以前的Express换成了Angular2+Express,遂记录于此。博客Demo在这里,你也可以点击这里查看完整代码。

第一次使用Angular2,还是遇到了不少问题,比如

  1. ng-cli(1.0.0-rc.1)自动生成的项目直接跑起来报错;
  2. 采用前端路由,刷新页面出现404;
  3. 用webpack打包后端项目要注意什么;
  4. 使用Angular2时,如何为某个组件加script标签;
  5. ...

如果你也遇到了这些问题,或者你想了解一下Angular2开发的大体流程,可以接着往下看。

2. 前后端分离与SPA

先来谈谈传统的Web开发流程。在传统开发里,前端的工作可能是用HTML、CSS将页面“绘制”出来,然后用JS去处理页面里的逻辑。但由于页面中需要展示一些动态的来自数据库中的数据,所以“绘制”的内容不能在当时确实下来,于是用一些“变量”填充在HTML里,等有数据时,才用数据去替换对应的变量,得到最终的完整的页面。以上用“变量”填充HTML的过程,有可能也是由前端完成,但更多的时候其实是后端来完成的;用数据去替换变量的过程,即所谓的页面渲染一般也是在后端完成的,即所谓的后端渲染。还忘了说的一点是路由。传统意义下,页面的路由是由后端控制的,即我们每点击一个链接,跳转到哪个页面或者说接收到什么页面完全是由后端控制的。

以上是传统Web前后端搭配干活的方式,存在着一些问题。比如上面所说的用变量填充HTML的操作若交给后端去做,那么他必须先读懂前端的HTML逻辑,然后才能下手;就算把填充变量的活交给前端去做,但由于这些变量都来自后端,前端测试起来将非常困难;比如,由于填充HTML的操作是交给后端去做的,那么前端在做页面时可能是用一些写死的数据做的测试,最终将真实数据套用过来时,页面显示可能会有出入;再比如如果前端已经将页面交给后端去添加变量,若他再修改了页面,他必须告诉后端哪里做了修改,否则后端需要在修改后的页面里重新再添加一遍变量,这样之前的工作都白费了。

于是,有人提出增大前端的职责范围,把页面渲染交给前端去做,但还是在服务端完成,后端只负责提供数据API接口,完全不管页面的渲染,包括路由。而此意义下的前端,即需要编写页面的结构样式,还需要负责将数据套在里面渲染出最终的页面,需要数据时,通过HTTP或者其他手段调用后端提供的接口即可。这样分工下来,前后端的工作几乎没有重叠之处,他们唯一的交接点在于提供数据的API接口,而这个API接口可以保证是稳定的。这确实能够解决之前的开发效率问题,但增加了一层接口的调用,并对前端的要求会更高。而对前端人员而言,最熟悉的编程语言莫过于JS,于是多出的,调用后端接口,渲染页面的这一层很自然的就会采用Node.js来做。于是有了下面这图(盗用自淘宝UED博客,现在好像搜索不到了:-?):

前后端分离

再说说Angular的工作模式。Angular跟上图的工作方式很像,但只是说在前后端分工上是相似的。Angular把页面渲染的工作放在了浏览器端,(当然Angular也支持后端渲染,参见Angular Universal),因此没有Node这一层,如下图:

这种方式其实更像是C/S架构的软件:除了数据需要向后台获取,其余的工作,像是页面路由,页面渲染等,都是在”客户端“完成的,只不过这里的”客户端“运行在浏览器里。这即是所谓的SPA(单页面应用)。

3. Angular

前面说了一些题外话,下面正式介绍用Angular开发我们的博客前端,需要把Node.js和npm安装好,npm仓库最好使用国内的镜像。可以安装一个叫做nrm的库来非常方便的更改我们的npm源。

首先是工程骨架的搭建,直接采用Angular的构建工具@angular/cli,先安装:

npm install -g @angular/cli

安装完成后就可以使用ng命令去生成我们的项目了:

ng new NiceBlog

生成的同时它会自动安装依赖包,完毕后,我们就可以进入NiceBlog目录,运行初始构建的项目了:

cd NiceBlog
npm start

注意:这里有坑!如果你使用的angular-cli版本是1.0.0-rc.1,生成的项目很可能跑不起来,至少我这里是这样。你需要将Angular的版本化由2.4.0换成2.4.9,然后重新安装依赖。

之后你便可以开发了。开发时,只要你修改代码,浏览器会自动刷新。

博客打算做成这个样子:

业务逻辑非常简单,就不再做解释了。按照Angular的开发思想,我们需要将一个应用切分为多(一)个模块,每个模块切分为多(一)个组件,组件依赖于服务,管道等。简单解释一下这些概念,模块是一系列组件,服务,管道等元素的集合,它通常按照业务功能进行划分;组件可以看成是一个页面里的小部件,比如一个导航条,一个菜单栏,一个Top10列表等;服务和后端开发里面的Service层相似,它为组件提供服务,比如一个ArticleService暴露出getArticles方法,为组件提供获取文章的服务,这样组件在需要文章数据时,依赖该服务即可,而不必考虑如何得到的这些数据;管道通常用来处理数据的输出格式。

由于这个应用够简单,我们不需要多余的模块,一个App模块作为启动模块,一个路由模块即可。然后App模块再按页面结构分为app、header、footer、summary、archive、detail、about组件。这些模块后可以用ng命名自动生成,以生成header组件为例:

ng g component header

我们的工作中心围绕组件展开,其余的一切都是为组件服务的。一个基本组件由三个方面(文件)组成:

  1. 一个是组件的文档结构和各种事件的响应方法的指定,这个由HTML文件来控制,该文件通常起名为:[组件名].component.html
  2. 再一个是组件的样式,这个由css文件来控制,该文件通常起名为:[组件名].component.css
  3. 最后一个是组件的数据结构定义和对数据结构进行操作的方法,并且还需要在其中指定以上的两点,该文件官方推荐用TypeScript编写,通常起名为:[组件名].component.ts

下面介绍各个组件的编写。

app组件

app组件是我们App模块的bootstrap组件(启动引导组件),这个ng在创建项目时就已经帮我们生成了。我们需要做的是在app组件里面布置好页面结构即可,这需要在该组件对应的HTML页面app.component.html里写:

<blog-header></blog-header>
<main>
  <router-outlet>
  </router-outlet>
</main>
<blog-footer></blog-footer>

相信很容易看懂它的意思:顶部和底部是header和footer组件,它们是固定的,会出现在每个页面;夹在中间的main便签里面router-outlet表示的是路由组件,到时候在路由模块里指定的是哪一个组件,它就会被那个组件代替。然后,你可以为main便签设置点样式,比如让它居中,这个在app组件对应的css文件app.component.css设置即可。这样app组件就搞定了。

header组件

header组件即页面的导航栏,没有啥逻辑,因此也只需要编写其html和css即可:

header.component.html

<nav>
  <div class="wrapper">
    <img class="logo" src="../../../assets/img/logo.jpg"/>
    <div class="items">
      <a class="item" routerLink="/home" routerLinkActive="active">首·页</a>
      <a class="item" routerLink="/archives" routerLinkActive="active">归·档</a>
      <a class="item" routerLink="/about" routerLinkActive="active">关·于</a>
      <a class="item" href="https://github.com/derker94" target="_blank">Github</a>
    </div>
  </div>
</nav>

代码也很简单,但要注意里面的a标签的链接地址是写在routerLink属性里的,而不是在传统的href里。这个属性和routerLinkActive是Angular定义的,照做就是。这样我们点击链接时,不会发出http请求,页面的路由是Angular完成的。

Route模块

下面定义route模块。可以使用ng g module app-routing命令帮我们自动生成。在生成的模块定义文件app-routing.module.ts里,需要交代路由链接与相应模板的关系,之前我们在app组件一节中就说过,这样<router-outlet></router-outlet>,就会被相应的组件替换。具体代码如下:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {SummaryComponent} from './components/summary/summary.component'
import {ArchiveComponent} from './components/archive/archive.component'
import {AboutComponent} from './components/about/about.component'
import {DetailComponent} from './components/detail/detail.component'

const routes: Routes = [
  {path: 'home', component: SummaryComponent},
  {path: 'archives', component: ArchiveComponent},
  {path: 'about', component: AboutComponent},
  {path: '', redirectTo: '/home', pathMatch: 'full'},
  {path: 'articles/:id', component: DetailComponent},
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  providers: []
})
export class AppRoutingModule { }

路由编写好后,你就可以点击页面上的链接了,看看路由是不是生效了呢。

footer组件与about组件

这两个组件没什么好介绍的,都是些写死的数据。

summary组件与archive组件

根据以上路由规则,这个组件是我们在访问/home时用到的组件。它是一个文章摘要的列表,就像下图一样:

Summary组件

看到列表,自然想到对应的数据结构,数组;而列表的每一项正对应文章(Article)数据结构。于是先定义Article数据结构:

article.ts

export class Article {
  _id: string;
  title: string;
  word: number; // 字数
  view: number; // 阅读数
  comment: number; // 评论
  comments: string[]; // 评论
  labels: string[]; //标签
  summary: string; // 摘要
  html: string; // html 格式内容
  date: Date;
}

然后,在Summary组件中,当然有一个文章数组的成员变量:

summary.component.ts

export class SummaryComponent implements OnInit {

  articles: Article[];

  constructor() {
  }
}

于是在html中我们就可以”显示“该文章数组了:

summary.component.html

<div class="wrapper" infinite-scroll (scrolled)="onScroll()">
  <section *ngFor="let article of articles">
    <h2>
      <a class="primary-link" [routerLink]="['/articles', article._id]">{{article.title}}</a>
      <time class="float-right">{{article.date | smartDate}}</time>
    </h2>
    <p class="hint">字数 {{article.word}} 阅读 {{article.view}} 评论 {{article.comment || 0}} </p>
    <p>{{article.summary}}...</p>
    <p>
      <span class="label" *ngFor="let label of article.labels">{{label}}</span>
    </p>
  </section>
</div>

其中用到了用来循环操作的ngFor指令,具体语法请参考Angular2官方文档吧。

再回到summary.component.ts中,我们考虑如何获得这个文章数组呢,之前就说过通过服务来拿,我们注入一个ArticleService(目前还没创建,先写着吧):

export class SummaryComponent implements OnInit {

  articles: Article[];
  constructor(private articleService: ArticleService) {// <====== 
  }
}

然后再生命周期方法里调用该服务:

ngOnInit() {
    this.articleService.getSummaries(0, this.limit).subscribe(res => {
        this.articles = res.data;
        this.total = res.total;
    });
}

archive组件也是类似的,这里就不再介绍了。

ArticleService

下面编写Article服务类,好像也没啥好说的,就不贴代码了。Angular2在Http里面用到了RxJs,很值得学习。需要说明的一点是,在我们的代码里,是直接通过后端接口来获取数据的,要想前后端同步工作,必须先把http接口定义好。还需要说明的一点是,若前端在完成Service后想进行测试,而后端接口开发还没完成,或者前端在开发阶段时服务器是跑在本地的,这样调用接口存在跨域问题。解决上面问题的方法是使用Angular提供的in-memory-web-api模块。

其他问题

以上是用Angular编写前端的大致过程,相信你已经清楚了。还有一个我遇到的问题是:如何在一个组件中使用第三方的脚本呢?比如我要用Mathjax去处理我页面里的Tex公式,以前的做法是直接在html里面用script便签引入Mathjax库即可,但现在好像没地方可以让我们这么去做,在xxx.component.html中去写吗?我试过,不行。最后Google到Stack Overflow里的一个答案,写一个服务来帮助我们加载,具体可以看我Github上的代码。

最后,代码写完后,我们可以使用npm run build去build我们的代码,最后我们的代码会被打包成很少的几个文件。你会发现,这样打包出来的代码,有些文件会很大,有1M左右。可以开启aot进行优化,具体是把package.json中的build对应的命令加上如下参数:

ng build --aot -prod

4. Express

后端采用Express开发,数据库使用的是MongoDB,采用这两者主要是开发的快。当然你也可以常用各种其他的语言技术,比如用JavaWeb来开发,或者用GO,Python,Ruby来开发等等。接口采用Restful风格,以json作为输出格式,相信这个很容易就能搞定,这里不多说。

想提一下的是,原本我准备把开发好的后端代码也用webpack打包一下,这样不仅能装x,最重要的是这么多文件被打包成一个文件,体积上小了不少,而且发布的时候特方便。但无奈装x归装x,在刚开始还能打包,但随着安装的库的增多,便开始报错,解决又需要花大力气,遂放弃。

最后说一下,前后端开发好后怎么结合在一起呢?这个具体实现要看你的后端选择的技术了。但是要保证:

  1. 前端build出的一堆文件的相对位置不要改变;
  2. 前端build出的index.html是首页面,在访问根url/时,需要后端把这个index.html响应给浏览器。
  3. 后端在收到无效的链接请求时,不要响应404,而是将请求转发到根url/上,或者还是将index.html响应给浏览器,注意是请求转发,而不是重定向

第3点是解决一些页面从首页点进来是ok的,但是刷新就报404的问题的关键。为什么这样能够解决呢?这是因为我们使用Angular后,点击链接时并不是像传统的那样发出一个http请求(还记得在header组件中,我们并没有为a标签指定href属性吗),而是由Angular处理了点击操作(前端路由),更新了页面(DOM),并更新了浏览器地址栏中的地址。我们刷新浏览器,相当于发出一个http请求去请求该页面,而后端压根就没有编写处理该请求的逻辑,自然会报404。解决的方法就是既然我们把路由交给了Angular去做,那么对于后端无法处理的请求同样转发到前端去,让前端去完成。

5. 小结

以上过程记录的并不详细,原因是如果你已经学过Angular了,那么你会觉得太啰嗦了;如果你还没学过Angular,建议你还是到官网去学习,那你已经讲解的非常详细了。以上只是记录整体结构和遇到的问题,希望能够为你带来帮助。

最后谈一谈使用Angular的感觉,一个字,太棒了!最大的感受是,它让不会组织代码的人都能把代码管理的井井有条。至于缺点嘛,尽管使用了aot,但build出来的文件还是感觉太大(500K左右),对于一个跑在1M小水管的博客应用来说,有点接受不了。但如果你开发一个稍微大型点的应用,相信这个缺陷应该不是问题了。

posted @ 2017-03-19 23:12 学数学的程序猿 阅读(...) 评论(...) 编辑 收藏