實現基於Nuxt.js的SSR應用

Surmon發表於2017-04-25

SEO

很重要,所以要普及。

SEO: 搜尋引擎優化(Search Engine Optimization),它是指通過站內優化,如:網站結構調整、網站內容建設、網站程式碼優化以及站外優化等方法,來進行搜尋引擎優化。

簡單說: 通過各種技術(手段)來確保,你的Web內容被搜素引擎最大化收錄,最大化提高權重,帶來更多流量。

**常見關鍵詞:**白帽、黑帽、SEM、Backlink、Linkbait、PageRank、Keyword Stuffing...,總之都圍繞著一個核心:SEO;流量是變現的快車道,SEO 是低成本獲取流量的最佳方法。

目前大部分的搜尋引擎僅能抓取URI直接輸出的資料資源,對於 Ajax 類的非同步請求的資料無法抓取;Google 除外,Google 有自己的Google’s Webmaster AJAX Crawling Guidelines.技術支援。

SPA

**SPA:**單頁 Web 應用(single page web application,SPA),就是隻有一張 Web 頁面的應用,是載入單個 HTML 頁面並在使用者與應用程式互動時動態更新該頁面的 Web 應用程式。

簡單說: Web 不再是一張張頁面,而是一個整體的應用,一個由路由系統、資料系統、頁面(元件)系統...組成的應用程式,其中路由系統是非必須的。

大部分的 Vue 專案,本質是 SPA 應用,Angular.js、Angular、Vue、React...還有最早的"Pjax"均如此。

SPA 時代,主要是在Web端使用了historyhash(主要是為了低版本瀏覽器的相容)API,在首次請求經服務端路由輸出整個應用程式後,接下來的路由都由前端掌控了,前端通過路由作為中心樞紐控制一系列頁面(元件)的渲染載入和資料互動。

而上面所述的各類框架則是將以:路由、資料、檢視為基本結構進行的規範化的封裝。

最早的 SPA 應用,由 Gmail、Google Docs、Twitter 等大廠產品實踐佈道,廣泛用於對SEO要求不高的場景中。

SSR

SSR: 服務端渲染(Server Side Render),即:網頁是通過服務端渲染生成後輸出給客戶端。

在 SPA 之前的時代,我們的Web架構大都是 SSR,如:Wordpress(PHP)、JSP技術、JavaWeb...或者 DEDECMS、Discuz! 等這些程式都是傳統典型的 SSR 架構, 即:服務端取出資料和模板組合生成 html 輸出給前端,前端發生請求時,重新向服務端請求 html 資源,路由也由服務端來控制。

其次,有個概念叫預渲染(Prerendering)。

如果你只是用服務端渲染來改善一個少數的營銷頁面(如 首頁,關於,聯絡 等等)的 SEO,那你可以用預渲染來實現。 預渲染不像伺服器渲染那樣即時編譯 HTML,它只在構建時為了特定的路由生成特定的幾個靜態頁面,等於我們可以通過 Webpack 外掛將一些特定頁面元件 build 時就編譯為 html 檔案,直接以靜態資源的形式輸出給搜尋引擎。

但實際的商業應用中,大部分時候我們需要的是即時渲染,這也是我們今天討論的主題。

Why

為什麼要SSR,為了體驗,還有SEO

首先,使用者可能在網路比較慢的情況下從遠處訪問網站 - 或者通過比較差的頻寬。 這些情況下,儘量減少頁面請求數量,來保證使用者儘快看到基本的內容。 可以用Webpack的程式碼拆分避免強制使用者下載整個單頁面應用,但是,這樣也遠沒有下載個單獨的預先渲染過的 HTML 檔案效能高。

對於世界上的一些地區人,可能只能用1998年產的電腦訪問網際網路的方式使用計算機。 而 Vue 只能執行在 IE9 以上的瀏覽器,你可能也想為那些老式瀏覽器提供基礎內容 - 或者是在命令列中使用 Lynx 的時髦的黑客。

在大部分的商業應用中,我們有 SEO 的需求,我們需要搜尋引擎更多地抓取到我們的內容,更詳細地認識到我們的網頁結構,而不是僅對首頁或特定靜態頁進行索引,這是 SSR 最重要的意義。

簡單說就是,我們需要搜素引擎看到這樣的程式碼:

實現基於Nuxt.js的SSR應用

而不是這樣的程式碼:

實現基於Nuxt.js的SSR應用

且,我們還需要在 SSR 的基礎上實現 SPA,即:首屏渲染

基本流程是:

在瀏覽器第一次訪問某個 URI 資源的時候(首屏),Web 伺服器根據路由拿到對應資料渲染並輸出,且輸出的資料中包含兩部分:

  • 路由頁對應的頁面及已渲染好的資料
  • 完整的SPA程式程式碼

在客戶端首屏渲染完成之後,此時我們看到的其實已經是一個和之前的 SPA 相差無幾的應用程式了,接下來我們進行的任何操作都只是客戶端的應用進行互動, 頁面/元件由Web端渲染,路由也由瀏覽器控制,使用者只需要和當前瀏覽器內的應用打交道就可以了。

之前在各大 SPA 框架還未正式官方支援 SSR 時,有一些第三方的解決方案,如:prerender.io, 它們做的事情就是建立HTTP一箇中間層,在判斷到訪問來源是蜘蛛時,輸出已快取好的html資料,此資料若不存在,則呼叫第三方服務對 html 進行快取,往復進行。

另一方法是自行構建蜘蛛渲染邏輯,當識別 UA 為搜尋引擎時,拿服務端已準備好的模板和資料進行渲染輸出 html 資料,反之,則輸出 SPA 應用程式碼;

我當時也考慮過此方法,但有很多弊端,如:

  • 需要針對蜘蛛編寫一套獨立的渲染模板,因為大部分情況下 SPA 的程式碼是沒法直接在服務端使用的
  • 搜尋引擎若檢測到蜘蛛抓取資料和真實訪問資料不一致,會做降權懲罰,也就意味著渲染模板還必須和SPA預期輸出一模一樣

所以,最好的方法是 SPA 能和服務端使用同一套模板,且使用同一個服務端邏輯分支,再簡單說:最好 Vue、Ng2... 能直接在服務端跑起來

於是,陸續誕生了基於 React 的Next.js、基於 Vue 的Nuxt.js、Ng2 誕生之日便支援。

沒錯,Nuxt.js 就是今天的主角。

Nuxt.js

官方是這麼介紹自己的:

Nuxt.js 是一個基於 Vue 的通用應用框架。

通過對客戶端/服務端基礎架構的抽象組織,Nuxt.js 主要關注的是應用的 UI渲染。

我們的目標是建立一個靈活的應用框架,你可以基於它初始化新專案的基礎結構程式碼,或者在已有 Node.js 專案中使用 Nuxt.js。

Nuxt.js 預設了利用 Vue 開發服務端渲染的應用所需要的各種配置。

除此之外,我們還提供了一種命令叫:nuxt generate,為基於 Vue 的應用提供生成對應的靜態站點的功能。

我們相信這個命令所提供的功能,是向開發整合各種微服務(miscroservices)的 Web 應用邁開的新一步。

作為框架,Nuxt.js 為 客戶端/服務端 這種典型的應用架構模式提供了許多有用的特性,例如非同步資料載入、中介軟體支援、佈局支援等。

太囉嗦了,用我的話說:

Nuxt.js是使用 Webpack 和 Node.js 進行封裝的基於Vue的SSR框架,使用它,你可以不需要自己搭建一套 SSR 程式,而是通過其約定好的檔案結構和API就可以實現一個首屏渲染的 Web 應用。

之所以叫 Nuxt.js 也是因為受到了 Next.js 的啟發。

作者是法國的兄弟倆,EvenYou 在微博多次提到,也在歐洲見過哥倆。

在此之前,國內有一些對 Vue SSR 的整合嘗試,但都沒有成功,主要在於 Webpack 和 Node 的結合上沒有實踐出最佳方案, 當我看到 Nuxt.js 以約束資料夾和配置檔案nuxt.config.js的方式來管理多個程式元件之間的關係時,就覺得,很酷!

接下來,我不會提供具體更多的學習資料,因為官方文件已經非常全面和成熟,已經 0.10.5 了(現在是 RC-11),只講下其架構和原理,和一些生產環境會遇到的問題。

首先,Nuxt.js 是一個 Node 程式,就像上面說的,我們是要把 Vue 跑在服務端,所以必須使用 Node 環境。

我們對 Nuxt.js 應用的訪問,實際上是在訪問這個 Node.js 程式的路由,程式輸出首屏渲染內容 + 用以重新渲染的 SPA 的指令碼程式碼,而路由是由 Nuxt.js 約定好的 pages 資料夾生成的。

所以,整體上,Nuxt.js 通過各個資料夾和配置檔案的約束來管理我們的程式,而又不失擴充套件性,其有自己的外掛機制


按照目前的版本,Nuxt.js 的程式的檔案結構大概分為以下部分:

  • pages:各頁面元件,用於生成對應路由,支援巢狀,支援動態路由
  • components:各元件,用於你自己管理公共元件或非公共元件
  • layouts:宿主佈局頁面模板元件,用於你可以把不同的頁面指定使用不同的佈局
  • assets:用於 Webpack 編譯的各類資源,通常是一些小的資源,如代替雪碧圖之類的圖片等東西
  • middleware:中介軟體,首屏渲染和路由跳轉前均執行對應中介軟體,可以返回promise或直接next(很實用!)
  • plugins:外掛,SPA中用的各類第三方元件和一些node模組都可以在這引入,甚至可以引入自己編寫的第三方庫
  • store:內建了vuex,可以直接返回資料模組或返回一個自建vuex根物件,具體要翻文件
  • 其他:你可以自定義資料夾和別名對映,文件都有提及,這裡有配置程式碼

nuxt.config.js對程式的擴充套件管理可大概分為以下類:

  • build:主要對應 Webpack 中的各配置項,可以對預設的 Webpack 配置進行擴充套件,如這裡程式碼
  • cache:主要對應內建的元件快取模組lru-cache的配置物件,有預設值,可選關閉
  • css:對應我們在SPA隨處引用樣式檔案的require語句
  • dev:用於自定義配置環境變數,對應之前webpack.config.js相關檔案中的變數語句
  • env:同上息息相關
  • generate:對generate命令執行時的行為做一些定製
  • head:對應vue-meta外掛的全域性配置,vue-meta用於VUE/SSR程式的文件元資訊的管理
  • loading:用於定製化Nuxt.js內建的進度條元件
  • performance:用於配置Node.js伺服器效能上的配置
  • plugins:用於管理和應用對應plugins資料夾中的外掛
  • rootdir:用於設定 Nuxt.js 應用的根目錄(這倆api有很大合併的意義)
  • srcdir:用於設定 Nuxt.js 應用的原始碼目錄(這倆api有很大合併的意義)
  • router:用於對vue-router的擴充套件和定製,其中還包括了中介軟體的配置,但並不完美(後面說)
  • transition:用於定製Nuxt.js內建的頁面切換過渡效果的預設屬性值
  • watchers:用於定製Nuxt.js內建的檔案監聽模組chokidar和 Webpack 的相關配置項

generate

同時,Nuxt.js 支援以generate命令將程式直接構建為靜態 html ,就像上面說的,可以作為靜態資源直接輸出。

生產環境實踐

特殊的非同步需求

這是生產環境最常見的問題,沒有之一。

我的部落格右側 Sidebar 為例,在元件結構中,其屬於宿主 layout 下的子元件,不屬於頁面元件,無法使用頁面元件中的fetch方法, 官方的解釋是子元件無法使用阻塞非同步請求,即:子元件得到的非同步資料無法用於服務端渲染,這對於程式是合理的,避免異常阻塞,簡化業務模型;

但實際需求中,我需要這些非同步資料增強站內內鏈 SEO;於是,我們可以巧妙地使用內建 vuex 中的nuxtServerInit這個 API,這個 API 是在 Nuxt.js 程式例項化之後第一次執行的方法, 其內部返回一個promise,我們可以在這裡完成我們站內的所有子元件非同步請求,隨後將資料對映至對應子元件即可,這裡有實踐程式碼

記憶體問題

在阿里雲低配機上出現記憶體膨脹的問題,一個 Blog 程式 Run 起的記憶體高達 100M+,當然也由於 Node.js 的特殊單執行緒非同步機制,暫不關心。

但在經過一段時間的訪問之後,特別是瞬間高併發訪問,會導致記憶體膨脹爆表當機,經分析,是由於元件快取引起的,將元件快取減少至10,問題有所改觀,但不明顯;

更深原因是,每次使用者訪問,程式均會重新渲染元件輸出,元件資料即在一段時間內駐存在記憶體中,直到 V8GC 回收。

最終的解決方案是:

使用官方推薦的"使用編碼中的 Nuxt.js "方法,自定義Node.js程式的入口,對程式進行一些優化; 如果你對業務和程式都需要有深度掌控的話,我很推薦此方法,它可以使你以管理 Node.js 程式的方式管理應用。

具體的優化方法是使用了一個叫idle-gc的垃圾回收模組來優化記憶體管理,

idle-gc是在node早期版本中被廢除的功能,主要負責空閒時的堆記憶體回收,然後早期被認為有 BUG,經常會導致 CPU 滿載,於是從 Node.js 中移除了,此專案作者修復了這個 BUG,併發布了模組。

另外,如果機器配置足夠,建議開啟快取,即cache選項,且適當往大的配置,cache 的意義在於使用記憶體常駐來減輕 CPU 的計算壓力,這對於單執行緒的 Node.js 是很好的業務實踐。

最新更新:已不再使用此模組,最終靠 [ 優化業務邏輯 ] + [ 優化頁面結構和抽象粒度 ] + [ 升級硬體 ] 來解決了問題。

這是 PM2 監控程式的日常資料之一:

實現基於Nuxt.js的SSR應用

移動版本適配問題

幾乎所有的搜尋引擎對於 PC 和移動端業務都是分開的,所以我們可以巧妙地使用layouts佈局模組來實現我們移動端和 PC 端業務的分離; 在我的部落格專案裡,由於業務邏輯和頁面均不夠複雜,故使用了 CSS3 媒體查詢 + 元件內判斷的形式實現了移動端的適配。

Route自定義meta問題

目前 API 中對 router 的支援不夠全面,如自定義的配置都還無法實現,不過可以通過宿主元件對應週期的hook來實現對例項化後的 router 物件進行修改和管理,儘管這不夠優雅。

Window問題

由於 Vue 的底層使用 Virtual DOM,所以 Nuxt.js 在 Node.js 環境中的編譯實際上是物件計算為字串的過程,並沒有依賴 Window/Dom,或者說任何基於 Vue 的 SSR 程式均如此。

我們在實際生產時可能用到一些需要依賴 DOM 的外掛/擴充套件,正確的方法是根據官方文件 - 只在瀏覽器裡使用的外掛推薦的方法,通過變數判斷外掛/擴充套件的應用環境, 這裡有實踐程式碼,或者使用SSR版本的元件,如:vue-awesome-swiper, 或自行封裝directive型別的外掛,而非component, 切記不要使用jsdom等類似 Node.js 中的 DOM 庫,這類庫本身是為爬蟲或測試誕生的,且本身會佔據大量的記憶體,這不是真正的解決方案!

有關更多常用使用問題,可以參考官方解答

最後:這是我的部落格,也是一個完整的 Nuxt.js 程式,原始碼在這裡

若有差池,期待指正

相關文章