zoukankan      html  css  js  c++  java
  • 用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小水管的博客应用来说,有点接受不了。但如果你开发一个稍微大型点的应用,相信这个缺陷应该不是问题了。

  • 相关阅读:
    PAT 1097. Deduplication on a Linked List (链表)
    PAT 1096. Consecutive Factors
    PAT 1095. Cars on Campus
    PAT 1094. The Largest Generation (层级遍历)
    PAT 1093. Count PAT's
    PAT 1092. To Buy or Not to Buy
    PAT 1091. Acute Stroke (bfs)
    CSS:word-wrap/overflow/transition
    node-webkit中的requirejs报错问题:path must be a string error in Require.js
    script加载之defer和async
  • 原文地址:https://www.cnblogs.com/dongkuo/p/6582953.html
Copyright © 2011-2022 走看看