伺服器端渲染與Nuxt.js

小牧_QAQ發表於2018-08-02

從前端發展史來看伺服器端渲染

前段時間在知乎上看到一篇提問,說的是為什麼現在又開始流行伺服器端渲染html了。整理了網上一些評論,結合自己的想法,整理出了一段前端發展史。

早在1989年,HTML的誕生是一個物理學家為了方便學術文件的分享而創造,這個也是前端起始的時間。後來,CSS和Javascript加入前端行列,用來渲染頁面樣式和處理頁面動效邏輯,前端三劍客成立。剛開始的前端程式設計師,其實就是做切圖寫樣式(CSS)和做頁面特效(JS)等一切基礎的工作,處於程式設計師鄙視鏈的底層。
隨著網際網路發展與技術進步,靜態頁面已經遠不能滿足產品需求,頁面上要根據邏輯產生動態的資料,這時,便迎來PHP,JSP等為代表的web1.0時代。此時的伺服器渲染,是以“文件”為核心思想。伺服器端的邏輯是把HTML,CSS和JS當做一個靜態檔案,對“文件”而言不存在“指令”和“資料”的區別,一切都是資料。所以我們可以看到伺服器渲染,GET就是請求一個檔案,而web 1.0時代的諸多服務端框架最基礎的元件之一就是文件模版,比如asp, JSP之類,核心設計理念就是HTML檔案裡放佔位符然後由服務端邏輯替換成實際資料後一股腦返回。很多中小型專案,不分前端後端,大家都是web開發工程師,按現在的說法叫全棧工程師。而在現在來看,這樣的模式是存在很多問題的,拿jsp舉例,動態資源和靜態資源完全耦合,伺服器壓力大,而且一旦出現狀況,前後臺一起玩完,使用者體驗極差;jsp必須要在支援java的web伺服器裡執行,效能提不上來;如果jsp中內容很多,頁面響應會很慢……
1998年,IE5.0引入XMLHttpRequest技術,實現了非同步呼叫伺服器的功能,2005年,Google在它著名的互動應用程式中使用了ajax非同步通訊,web前端引來2.0革命。之後W3C釋出XMLHttpRequest標準,為之後的ajax爆發提供技術基礎。
2006年,JQuery工具庫釋出,一經出世憑藉其簡單易容的特性和解決瀏覽器相容性的能力風靡全球。
2010年,Backbone誕生,RequireJS第一個版本釋出,前端的模組化開發時代正式來臨了。而後,隨著前端MVC的興起,SPA(Single Page Application 單頁面應用)開始變成一種專案開發的潮流,前後端分工非常清晰。前端工作在瀏覽器端,後端工作在服務端。清晰的分工,可以讓開發並行,測試資料的模擬不難,前端可以本地開發。此時前後端分離的運動在各大公司間興起,前端自立門戶,獨立發展。前端程式設計師們翻身的機會來了。
然而此時,很多本不該被做成SPA的也被做成了SPA。但是,SPA應用存在種種問題,比如SEO,比如首屏載入速度,這讓前端開發人員優化愁白了頭。
隨著Node.js的興起,Javascript開始有能力執行在伺服器端,這意味這有一種新的研發模式:Front-end UI layer 處理瀏覽器層的展現邏輯,Back-end UI layer 處理路由、模板、資料獲取、cookie 等。通過 Node,Web Server 層也是 JavaScript 程式碼,這意味著部分程式碼可前後複用,需要 SEO 的場景可以在服務端同步渲染,由於非同步請求太多導致的效能問題也可以通過服務端來緩解。前一種模式的不足,通過這種模式幾乎都能完美解決掉。
Web 2.0時代最大的思想革命本質不是前後端分離,而是把網頁當作獨立的應用程式(app)。前後端分離只是實現這一新架構的必然結果。對程式而言指令和資料是分離的。HTTP GET拿到的不是渲染後的網頁,而是一個由html和Javascript組成的app, 這個app以瀏覽器為虛擬機器。裝載和顯示資料是app啟動之後的執行邏輯。傳統上app叫什麼?叫Client,也就是前端。於是前後端就這麼分離了,瀏覽器變成了app的執行環境,後端蛻化成了單純的業務邏輯和資料介面。寫Javascript不再是給網頁添特效的小伎倆,而是正經的和寫桌面應用程式一樣的工程。於是我們看到了前端工程化,編譯(轉譯),各種MVC/MVVM框架,依賴工具,等等。

為什麼要用伺服器端渲染?

使用伺服器端渲染,最主要的問題,其實就是為了解決SEO的問題。如果SPA應用也有良好的SEO,就不用伺服器端渲染這麼麻煩了。當然伺服器端渲染能解決的首屏載入速度的問題也是原因之一。那麼,SEO是什麼呢?
SEO(Search Engine Optimization),搜尋引擎優化。比如谷歌、百度需要抓取你所釋出的網站資訊來進行自然排序,是通過爬蟲進行的。
來看兩段程式碼:

伺服器端渲染與Nuxt.js

伺服器端渲染與Nuxt.js

上面是之前很早前寫過的兩段掘金文章的爬蟲程式碼(寫的有點low),大概思路就是使用superagent傳送http請求,把整個頁面(文件物件)爬下來,包括head, body等,然後用cheerio進行解析,然後抓取頁面節點元素以及關鍵資訊。可能你覺得,這個簡單,我頁面上資訊都是通過ajax請求到然後插入到dom元素中的。注意,爬蟲爬到的頁面並沒有傳送ajax請求,就是一個初始化的純靜態頁面,如果你用的是spa應用,那可能body中除了一個id為app的節點,什麼都沒有。所以,我們需要讓頁面在伺服器端就已經被渲染完成,傳給客戶端的時候已經是一個具有資料資訊的靜態html文件。當然,現在也有針對SPA應用進行的SEO優化方案,這個不在本文討論範圍之內。
以下總的列舉伺服器渲染的一些優缺點:

優點

  • 有利於SEO。
  • 首屏載入速度快。因為SPA引用需要在首屏獲取所有資源,而伺服器端渲染直接拿了成品展示出來就行了。
  • 無需佔用客戶端資源。解析模板工作交給伺服器完成,對於客戶端資源佔用更少,尤其是移動端,也可以更省電。

缺點

  • 佔用伺服器資源。伺服器端完成html模板解析,如果請求較多,會對伺服器造成一定的訪問壓力。而如果是前端渲染,就是把這些壓力分攤給了前端。
  • 不利於前後端分離。

VUE SSR

通用(也稱同構)的JavaScript已經成為JavaScript社群很常用的一個術語。通用的JavaScript用來形容可以在客戶端執行,也可在服務端執行的Javascript程式碼。 在VUE的官方文件上是這麼描述的:

Vue.js 是構建客戶端應用程式的框架。預設情況下,可以在瀏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以將同一個元件渲染為伺服器端的 HTML 字串,將它們直接傳送到瀏覽器,最後將靜態標記"混合"為客戶端上完全互動的應用程式。

前半句好理解,就是說你可以在伺服器(後端)環境中,使用vue.js來構建元件和頁面,然後將渲染好的靜態html字串傳給客戶端展示。後半句感覺句子不太通順,英文版是這樣的:

“Finally "hydrate" the static markup into a fully interactive app on the client.”

大概意思就是說,把應用傳給客戶端以後,由於一些靜態標記,客戶端也會具備同樣的互動(就是MVVM雙向資料繫結)。

伺服器端渲染與Nuxt.js
官網給出的這張構建步驟圖也可以看出,對於客戶端應用程式和伺服器應用程式,我們都要使用 webpack 打包 - 伺服器需要「伺服器 bundle」然後用於伺服器端渲染(SSR),而「客戶端 bundle」會傳送給瀏覽器,用於混合靜態標記。 本文不過多深入Vue SSR源生的內容,Vue官方推薦了一個優秀的社群專案Nuxt.js,它為Vue的服務端渲染提供了非常良好的開發體驗,我們將主要來討論一下它。

Nuxt.js

Nuxt.js 是什麼?

構建服務端渲染的JavaScript程式多少有些無趣,在開始編碼之前,需要大量的基礎配置。因此,解決vue.js服務端渲染問題的Nuxt.js產生了。 Nuxt.js 是一個基於 Vue.js 的通用應用框架。預設了伺服器端渲染所需的各種配置,如非同步資料,中介軟體,路由,只要遵循其中的規則就能輕鬆實現SSR。。它好比是 Angular Universal 之於 Angular, Next.js 之於 React。 通過對客戶端/服務端基礎架構的抽象組織,Nuxt.js 主要關注的是應用的 UI渲染。

Nuxt.js 能做什麼

  • 無需再為了路由劃分而煩惱,只需要按照對應的資料夾層級建立 .vue 檔案就行
  • 無需考慮資料傳輸問題,nuxt 會在模板輸出之前非同步請求資料(需要引入 axios 庫),而且對 vuex 有進一步的封裝
  • 內建了 webpack,省去了配置 webpack 的步驟,nuxt 會根據配置打包對應的檔案

Nuxt.js 的安裝與執行

在安裝vue-cli的情況下,快速生成一個nuxt專案的命令如下:

$ vue init nuxt-community/starter-template <project-name>
複製程式碼

進入專案目錄後

$ npm install
複製程式碼

然後啟動專案

$ npm run dev
複製程式碼

這樣專案就能正常執行在http://localhost:3000

Nuxt.js 的實踐介紹

這裡就不詳細介紹nuxt.js的一些用法和API了,可以直接看官網的教程:zh.nuxtjs.org/guide
我自己做了一個nuxt.js的簡單demo(極其簡單),Github地址,這裡就我自己的的一些體驗,對比spa應用,來聊聊這個框架。

1. Nuxt.js的目錄結構

nuxt是採用vue-cli來建立的模板,相比常規的vue模板,他們具備非常重要的一點:方便。nuxt.js同樣已經將各種專案所需的webpack配置替我們打理好了,開箱即用,基本不需要作什麼改動。而且即使需要自定義一些配置,修改起來也非常簡單。我們來把它的目錄結構和SPA應用作一個對比。

伺服器端渲染與Nuxt.js
從上圖可以看出,nuxt似乎把src資料夾中的很多內容給放到了外邊,少了一些檔案,多了幾個沒見過的資料夾。這些資料夾各有各的用處。

  • layouts用於放置頁面佈局。
  • middleware用於放置一些中介軟體,我們在頁面元件中可以引用這些中介軟體,在執行頁面邏輯的時候會先執行其中的邏輯。
  • pages就是放置我們的所有頁面元件啦,但是,與spa應用不同的是,nuxt裡的page會根據檔案和資料夾結構生成對應的路由,打個比方,我page資料夾下的目錄結構如下
pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue
複製程式碼

Nuxt.js 生成對應的路由配置表為:

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'users-id',
      path: '/users/:id?',
      component: 'pages/users/_id.vue'
    },
    {
      name: 'slug',
      path: '/:slug',
      component: 'pages/_slug/index.vue'
    },
    {
      name: 'slug-comments',
      path: '/:slug/comments',
      component: 'pages/_slug/comments.vue'
    }
  ]
}
複製程式碼
  • plugins集中放置一些外掛,比如axios等。
  • store是集中定義狀態樹。nuxt.js已經整合了vuex,這裡只需要定義一個index.js,然後對外暴露一個Vuex.Store例項即可。但是,經過踩坑,這裡的狀態樹和SPA裡有些不一樣,這個等會說。

2. Nuxt.js的頁面元件

伺服器端渲染與Nuxt.js
從上圖的Nuxt.js和SPA的.vue單頁檔案對比,,除了一般的.vue單頁檔案中的常規例項屬性外(比如data, methods,props等),Nuxt.js還提供了很多方便又有意思的屬性,這也是Nuxt.js最大的特點之一。
伺服器端渲染與Nuxt.js

  1. 在nuxt內部的整個執行流程中,最先經過的是狀態管理中actions中的nuxtServerInit函式,這個我們等會再說。
  2. 然後會經過middleware裡的中介軟體函式,此時,還沒有進行資料獲取和頁面渲染,所以我們可以在中介軟體函式中執行一些進入路由前的邏輯,比如使用者許可權判斷。
  3. 之後開始獲取頁面資料,asyncData和data的結果基本相同,我們可以直接呼叫server的介面,比如理由axios傳送http請求獲取頁面所需的原始資料,然後以物件的形式return出去,此時,Vue物件還沒有例項化,所以asyncData裡無法呼叫到this
  4. fetch裡主要用作填充狀態樹(store)資料。
  5. 這些全部做完以後,開始例項化Vue物件,這裡的邏輯和單頁應用是一樣的,在組裝好整個頁面應用之後,nuxt.js會將這個應用返回至前端。注意,這裡返回的不是單純的頁面,而是應用。此時的頁面區域性spa應用的一些性質,比如資料監聽雙向繫結。
  6. 頁面來到前端後,開始執行mount的相關邏輯。
    除了應用的執行流程外,再看看頁面渲染的模組。
    伺服器端渲染與Nuxt.js
  • head部分可以自定義當前頁面的頭部資訊,比如title, meta之類的。當然,如果需要定義全域性head可以在nuxt.config.js中配置。
  • layout部分可以自定義頁面佈局,很多頁面公用的靜態頭、尾部分可以統一定義按需引用。
  • scrollToTop用於頁面跳轉時將頁面滾動置頂。
  • transition用於頁面間跳轉的過渡動畫。

3. Vuex狀態樹

整個demo做下來,目前讓我印象最深的就是狀態樹,它和SPA應用還是有一定區別的。
當時我需要完成的需求是,儲存使用者資訊,並在任何頁面可以使用它,如果非登入頁沒有獲取到使用者資訊,跳轉回登入頁
起初,我的設計思路是,在使用者登入成功後,呼叫後臺介面獲取該使用者所有資訊,並且存在store中。流程圖如下:

伺服器端渲染與Nuxt.js
按照SPA應用的情況,store裡的資料應該在頁面元件中都是共享的。但是,發現Nuxt中一旦頁面跳轉,整個Vuex狀態樹會重置,原來存下的使用者資訊也沒有了。由此可以推測,不同路由下的頁面是一個獨立的應用,它們並不會共享state中的資料。
這時我想到了本地會話儲存localStorage,只要把原流程中從store存取的邏輯改為從localStorage的邏輯即可。這種方式是可行的,但是這樣一來,Vuex感覺存在感就不強了(此事必有蹊蹺),並且就沒法用server層來控制會話的過期等邏輯。
後來發現,伺服器端渲染的vuex中的action中提供了一個方法:nuxtServerInit。Nuxt.js 呼叫它的時候會將頁面的上下文物件作為第2個引數傳給它,上下文物件可以拿到req請求物件,那麼就存在這麼一種邏輯。我可以將使用者資訊儲存在伺服器session中,然後通過req.session.user來訪問當前登入的使用者。將使用者登入資訊傳給客戶端的狀態樹,程式碼如下:

actions: {
  nuxtServerInit ({ commit }, { req }) {
    if (req.session.user) {
      commit('user', req.session.user)
    }
  }
}
複製程式碼

這樣在配合middleware中介軟體,就可以完成使用者資訊獲取和會話控制,流程如下:

伺服器端渲染與Nuxt.js

4. Nuxt.js的其它相關

其它還涉及的一些內容,其實看看官網教程,看看官網示例都能搞定,教程還是非常易懂的。

總結

使用Vue,React等伺服器渲染,並不是走以前模板式渲染的老路。它已經跨越歷史,朝著更優秀的方面發展。 而Nuxt.js,還是一個非常年輕的框架(現在官網才是0.10.7版本),目前也有很多待改進的問題,但它的出現為 Vue.js 開發者搭建服務端渲染專案提供了巨大的便利。聽說Nuxt.js 2.0 即將來臨,期待版本釋出後,能給我們帶來更多實用的新功能。

相關文章