用Angular2+Express快速搭建博客

1. 寫在前面

昨天花了1天的時間把本身的博客從之前的Express換成了Angular2+Express,遂記錄於此。博客Demo在這裏,你也能夠點擊這裏查看完整代碼。css

第一次使用Angular2,仍是遇到了很多問題,好比html

  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的過程,有可能也是由前端完成,但更多的時候實際上是後端來完成的;用數據去替換變量的過程,即所謂的頁面渲染通常也是在後端完成的,即所謂的後端渲染。還忘了說的一點是路由。傳統意義下,頁面的路由是由後端控制的,即咱們每點擊一個連接,跳轉到哪一個頁面或者說接收到什麼頁面徹底是由後端控制的。webpack

以上是傳統Web先後端搭配幹活的方式,存在着一些問題。好比上面所說的用變量填充HTML的操做若交給後端去作,那麼他必須先讀懂前端的HTML邏輯,而後才能下手;就算把填充變量的活交給前端去作,但因爲這些變量都來自後端,前端測試起來將很是困難;好比,因爲填充HTML的操做是交給後端去作的,那麼前端在作頁面時多是用一些寫死的數據作的測試,最終將真實數據套用過來時,頁面顯示可能會有出入;再好比若是前端已經將頁面交給後端去添加變量,若他再修改了頁面,他必須告訴後端哪裏作了修改,不然後端須要在修改後的頁面裏從新再添加一遍變量,這樣以前的工做都白費了。git

因而,有人提出增大前端的職責範圍,把頁面渲染交給前端去作,但仍是在服務端完成,後端只負責提供數據API接口,徹底無論頁面的渲染,包括路由。而此意義下的前端,即須要編寫頁面的結構樣式,還須要負責將數據套在裏面渲染出最終的頁面,須要數據時,經過HTTP或者其餘手段調用後端提供的接口便可。這樣分工下來,先後端的工做幾乎沒有重疊之處,他們惟一的交接點在於提供數據的API接口,而這個API接口能夠保證是穩定的。這確實可以解決以前的開發效率問題,但增長了一層接口的調用,並對前端的要求會更高。而對前端人員而言,最熟悉的編程語言莫過於JS,因而多出的,調用後端接口,渲染頁面的這一層很天然的就會採用Node.js來作。因而有了下面這圖(盜用自淘寶UED博客,如今好像搜索不到了:-?):github

先後端分離

再說說Angular的工做模式。Angular跟上圖的工做方式很像,但只是說在先後端分工上是類似的。Angular把頁面渲染的工做放在了瀏覽器端,(固然Angular也支持後端渲染,參見Angular Universal),所以沒有Node這一層,以下圖:web

這種方式其實更像是C/S架構的軟件:除了數據須要向後臺獲取,其他的工做,像是頁面路由,頁面渲染等,都是在」客戶端「完成的,只不過這裏的」客戶端「運行在瀏覽器裏。這便是所謂的SPA(單頁面應用)。數據庫

3. Angular

前面說了一些題外話,下面正式介紹用Angular開發咱們的博客前端,須要把Node.js和npm安裝好,npm倉庫最好使用國內的鏡像。能夠安裝一個叫作nrm的庫來很是方便的更改咱們的npm源。express

首先是工程骨架的搭建,直接採用Angular的構建工具@angular/cli,先安裝:npm

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小水管的博客應用來講,有點接受不了。但若是你開發一個稍微大型點的應用,相信這個缺陷應該不是問題了。

相關文章
相關標籤/搜索