使用Nuxt.js改善現有項目

緣由

票牛的移動站一直使用的是多頁 web 應用的形式,分別使用 jade、less 和 es6+zepto 來分開書寫頁面、樣式及腳本,再用 gulp 進行打包發布。api 介面則是和移動端保持同步,並在nginx 上做了反向代理以避免跨域問題。2017 年早些的時候,隨著頁面邏輯複雜度的上升,開始啟用vue作為基礎框架,並對部分老頁面進行了重構。

這套方案工作得很不錯,不過純前端渲染有其天生的問題:搜索引擎爬蟲抓不到啊。在這個問題上,業界有一些嘗試,比如 Prerender.io,這些服務的思路基本就是發現爬蟲過來的時候就把請求丟過去,他們會嘗試用 PhantomJS 進行渲染,再把產生的 html 給回來。現代主流前端框架也都提供了服務端渲染 (Server Side Rendering) 的實現。

取捨上講,我個人的看法是,如果只是要滿足 SEO 的需求,Prerender 這樣的方案是值得嘗試的,然而實際上我們還有一些別的場景需要在頁面標籤里動刀子,比如想要配置 Open Graph 來使 iMessage 里的鏈接帶預覽圖,就得知道蘋果的爬蟲長啥樣,響應速度上也會欠佳一些。並且 SSR 也讓開發者有更多的控制權,既然項目都已經遷到 Vue 上了,不如就試試看 2017 年的新科技吧。

Nuxt.js 是由一對法國的兄弟基於 vue 2.0 提供的 ssr 能力開發的框架,基於恰到好處的約定與配置,可以顯著的降低開發者創建服務端渲染 web app 的門檻。如果你是從零開始,可以按照官方推薦的做法,使用 vue-cli 創建項目。我們因為是已有項目,所以做了這樣幾件事情:

初始化項目

Nuxt 默認的 srcDir 為 rootDir,這裡我們的根目錄已經有自己項目的內容了,於是建了個 nuxt 目錄以區分對待,在 nuxt.config.js 做聲明就好。middleware 下放一些中間件,用來解析當前頁的城市信息等,配合 store 下的 action 存取,使得頁面都可以訪問這塊信息。

pages 目錄下則按照現有的 url 創建對應的文件,比如演出詳情的 url 是 /activity/detail.html ,就創建 pages/activity/detail.html.vue,然後把之前的 jade、less、js 都合到這個文件中去。

改造網路請求

服務端渲染的話,一開始頁面的數據也都是由服務端發起的了。之前對網路請求做過一層封裝,使用自己的 fetch 模塊,現在要做的事情就是在 fetch 的實現中區分客戶還是服務端,切換實現就好。服務端我們使用了 axios 作為請求庫,客戶端就還是保留 zepto。可以通過 process.server / process.browser 來進行判斷。

Nuxt 對 vue 的配置做了自己的擴展,作為頁面入口的 vue 文件會多出 asyncData、head 等配置項,asyncData 即是我們發請求獲取數據的地方。

適配現有組件

除了頁面入口之外,其他的組件都可以在瀏覽器和服務端復用。需要改造的主要有兩處:

  1. 組件內使用到瀏覽器相關api的部分需要判斷環境,比如初始化 iscroll,測量寬度等
  2. 原來組件內直接從 url 上獲取 query 參數的地方現在要改為由父級傳入

這裡遇到比較多的是 zepto 相關,一開始逐個判斷

var $;nif (process.browser) {n $ = require(zepto)n}n

後來嫌麻煩,就做了個 $.js,把這段邏輯放進去,使用的地方直接:

var $ = require($)n

如果量比較大的話可以狸貓換太子,把原來的 zepto 改為 zepto-origin,再建一個 zepto 做以上事情,業務代碼就可以不用動了。

配置路由

路由優化也是 SEO 的一部分,雖然感覺如何優化基本上是玄學,不過好些策略又似乎確實有效果。

具體來講,我們要做的就是把 m.piaoniu.com/activity/ 這樣的鏈接 變成 m.piaoniu.com/sh-dramas 這樣,這也是使用 SSR 方案有更大的控制權才能做的優化。

對於簡單的需求,Nuxt 支持配置如下的文件結構來支持:

pages/n--| activity/n-----| _id.vuen

會生成如下配置:

router: {n routes: [n {n name: activity-id,n path: /users/:id?,n component: pages/activity/_id.vuen }n ]n}n

如果滿足不了需要,也可以自行在 nuxt.config.js 中擴展,比如前文提到的場景我們是這麼配置的:

router: {n extendRoutes (routes, resolve) {n // ...n routes.push({n name: category-home,n path: /:city-:category/:filter?,n component: resolve(__dirname, nuxt/pages/activity/category-home.html.vue)n })n // ...n }n}n

參考: Routing - Nuxt.js

更新發布腳本

原先的發布腳本做的事情是把靜態資源構建好,然後把普通資源和html先後分別發到不同的nginx伺服器上,完事兒。現在多了一部,發現目錄下存在nuxt.config.js,則執行

npm run nuxt-buildn

之後把項目整個目錄打個壓縮包,傳到伺服器上解壓,並通過 pm2 做平滑重啟。

nginx 上也要做相應的改動:先嘗試 try_file,如果找不到,則轉發給 node 服務進行服務端渲染,相應的錯誤頁面也由 node 服務提供了。

這裡由於我們要兼容已有的頁面比如 activity/detail.html,而之前發不過的文件是不刪的,這就需要在 nginx 上額外對這些頁面進行配置。平穩運行一段時間之後,可以在項目和伺服器上把這些文件刪除,以簡化配置。

添加監控

既然要負責服務端渲染了,那麼相應的服務質量監控也要接手起來。後端我們使用的是點評出品的 CAT,nodejs也有對應的客戶端:cat-client ,使用起來並不麻煩,倒是尋找 Nuxt 錯誤頁的切入點花了一些功夫。官方沒有給到推薦的做法,研究了下源碼自己對錯誤入口做了切入。

Renderer.prototype.errorMiddleware = (err, req, res, next) => {n Cat.logError(req.url, err)n // 給用戶展示錯誤頁面,自己則可以更方便的看到具體錯誤信息n if (req.cookies.ERROR_VISIBLE) {n res.end(err.stack)n } else {n res.status(500).render(error, {n code: 500,n error: 出錯了n })n }n}n

另外,利用 CAT 提供的 Transaction 功能,也可以方便的看到服務的性能表現如何,瓶頸在哪裡,看起來長這樣:

嗯,返回一個演出詳情耗時 160 毫秒,還不錯。


推薦閱讀:

【譯】如何只用CSS製作一個漂亮的載入動畫
APP圖標設計小技巧:在iOS上快速獲得APP圖標的真實預覽圖
探究Babel生態
重新設計 React 組件庫

TAG:前端开发 | 服务端渲染 |