使用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 即是我們發請求獲取數據的地方。
適配現有組件
除了頁面入口之外,其他的組件都可以在瀏覽器和服務端復用。需要改造的主要有兩處:
- 組件內使用到瀏覽器相關api的部分需要判斷環境,比如初始化iscroll,測量寬度等
- 原來組件內直接從url 上獲取query 參數的地方現在要改為由父級傳入
這裡遇到比較多的是zepto 相關,一開始逐個判斷
var$;if(process.browser){$=require('zepto')}
後來嫌麻煩,就做了個$.js,把這段邏輯放進去,使用的地方直接:
var$=require('$')
如果量比較大的話可以狸貓換太子,把原來的zepto 改為zepto-origin,再建一個zepto 做以上事情,業務代碼就可以不用動了。
配置路由
路由優化也是SEO 的一部分,雖然感覺如何優化基本上是玄學,不過好些策略又似乎確實有效果。
具體來講,我們要做的就是把http:// m.piaoniu.com/activity/ category-home.html?categoryId=2這樣的鏈接變成http:// m.piaoniu.com/sh-dramas這樣,這也是使用SSR 方案有更大的控制權才能做的優化。
對於簡單的需求,Nuxt 支持配置如下的文件結構來支持:
pages/ --| activity/ -----| _id.vue
會生成如下配置:
router: { routes: [ { name: 'activity-id', path: '/users/:id?', component: 'pages/activity/_id.vue' } ] }
如果滿足不了需要,也可以自行在nuxt.config.js 中擴展,比如前文提到的場景我們是這麼配置的:
router : { extendRoutes ( routes , resolve ) { // ... routes . push ({ name : 'category-home' , path : '/:city-:category/:filter?' , component : resolve ( __dirname , 'nuxt/pages/activity/category-home.html.vue' ) }) // ... } }
更新發布腳本
原先的發布腳本做的事情是把靜態資源構建好,然後把普通資源和html先後分別發到不同的nginx服務器上,完事兒。現在多了一部,發現目錄下存在nuxt.config.js,則執行
npm run nuxt-build
之後把項目整個目錄打個壓縮包,傳到服務器上解壓,並通過pm2 做平滑重啟。
nginx 上也要做相應的改動:先嘗試try_file,如果找不到,則轉發給node 服務進行服務端渲染,相應的錯誤頁面也由node 服務提供了。
這裡由於我們要兼容已有的頁面比如activity/detail.html,而之前發不過的文件是不刪的,這就需要在nginx 上額外對這些頁面進行配置。平穩運行一段時間之後,可以在項目和服務器上把這些文件刪除,以簡化配置。
添加監控
既然要負責服務端渲染了,那麼相應的服務質量監控也要接手起來。後端我們使用的是點評出品的CAT ,nodejs也有對應的客戶端: cat-client ,使用起來並不麻煩,倒是尋找Nuxt錯誤頁的切入點花了一些功夫。官方沒有給到推薦的做法,研究了下源碼自己對錯誤入口做了切入。
Renderer.prototype.errorMiddleware=(err,req,res,next)=>{Cat.logError(req.url,err)//給用戶展示錯誤頁面,自己則可以更方便的看到具體錯誤訊息if(req.cookies.ERROR_VISIBLE){res.end(err.stack)}else{res.status(500).render('error',{code:500,error:'出錯了'})}}
另外,利用CAT 提供的Transaction 功能,也可以方便的看到服務的性能表現如何,瓶頸在哪裡,看起來長這樣:

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