1. 程式人生 > >Vue SSR之服務端渲染

Vue SSR之服務端渲染

目錄

準備工作

開始折騰

1. 首先安裝 ssr 支援

2. 增加路由test與頁面

3. 在src目錄下建立兩個js:

4. 修改router配置。

5. 改造main.js

6. entry-client.js加入以下內容:

7. entry-server.js

8. webpack配置

9. webpack 客戶端的配置

10. webpack 伺服器端的配置

11. 配置package.json增加打包伺服器端構建命令並修改原打包命令

12. 修改index.html

13. 執行構建命令

14. 構建伺服器端(官方例子使用的express,所以此 demo 將採用koa2來作為伺服器端,當然,無論是 koa 與 express 都不重要…)

15. 編寫服務端程式碼

16. 大功告成

優缺點


  • 準備工作

使用 vue-cli再次初始化一個專案:

vue init webpack vue-ssr-demo

然後,

cd vue-ssr-demo
npm install
npm run dev

確保初始化的專案可正常執行,接下來開始慢慢折騰吧~~

  • 開始折騰

  • 1. 首先安裝 ssr 支援

 npm i -D vue-server-renderer

重要的是 vue-server-renderer 與 vue 版本必須一致匹配

  • 2. 增加路由test與頁面

隨便寫了個計數器,以驗證服務端渲染時,vue 的機制會正常工作。

  <template>
    <div>
      Just a test page.
      <div>
        <router-link to="/">Home</router-link>
      </div>
      <div><h2>{{mode}}</h2></div>
      <div><span>{{count}}</span></div>
      <div><button @click="count++">+1</button></div>
    </div>
  </template>
  <script>
    export default {
      data () {
        return {
        mode: process.env.VUE_ENV === 'server' ? 'server' : 'client',
          count: 2
        }
      }
    }
  </script>
  • 3. 在src目錄下建立兩個js:

   src
   ├── entry-client.js # 僅運行於瀏覽器
   └── entry-server.js # 僅運行於伺服器
  • 4. 修改router配置。

無論什麼系統路由總是最重要的,伺服器端渲染自然也要公用一套路由系統,並且為了避免產生單例的影響,這裡主要只為每一個請求都匯出一個新的router例項:

   import Vue from 'vue'
   import Router from 'vue-router'
   import HelloWorld from '@/components/HelloWorld'

   Vue.use(Router)

   export function createRouter () {
     return new Router({
       mode: 'history', // 注意這裡也是為history模式
       routes: [
         {
           path: '/',
           name: 'Hello',
           component: HelloWorld
         }, {
           path: '/test',
           name: 'Test',
           component: () => import('@/components/Test') // 非同步元件
         }
       ]
     })
   }
  • 5. 改造main.js

main.js初始化的只適合在瀏覽器的執行,所以要改造兩端都可以使用的檔案,同樣為了避免產生單例的影響,這裡將匯出一個createApp的工廠函式:

   import Vue from 'vue'
   import App from './App'
   import { createRouter } from './router'

   export function createApp () {
     // 建立 router 例項
     const router = new createRouter()
     const app = new Vue({
       // 注入 router 到根 Vue 例項
       router,
       render: h => h(App)
     })
     // 返回 app 和 router
     return { app, router }
   }
  • 6. entry-client.js加入以下內容:

   import { createApp } from './main'
   const { app, router } = createApp()
   // 因為可能存在非同步元件,所以等待router將所有非同步元件載入完畢,伺服器端配置也需要此操作
   router.onReady(() => {
     app.$mount('#app')
   })
  • 7. entry-server.js

   // entry-server.js
   import { createApp } from './main'
   export default context => {
     // 因為有可能會是非同步路由鉤子函式或元件,所以我們將返回一個 Promise,
     // 以便伺服器能夠等待所有的內容在渲染前,
     // 就已經準備就緒。
     return new Promise((resolve, reject) => {
       const { app, router } = createApp()
       // 設定伺服器端 router 的位置
       router.push(context.url)
       // 等到 router 將可能的非同步元件和鉤子函式解析完
       router.onReady(() => {
         const matchedComponents = router.getMatchedComponents()
         // 匹配不到的路由,執行 reject 函式,並返回 404
         if (!matchedComponents.length) {
           // eslint-disable-next-line
           return reject({ code: 404 })
         }
         // Promise 應該 resolve 應用程式例項,以便它可以渲染
         resolve(app)
       }, reject)
     })
   }
  • 8. webpack配置

vue相關程式碼已處理完畢,接下來就需要對webpack打包配置進行修改了。

官網推薦下面這種配置:

 build
  ├── webpack.base.conf.js  # 基礎通用配置
  ├── webpack.client.conf.js  # 客戶端打包配置
  └── webpack.server.conf.js  # 伺服器端打包配置

vue-cli初始化的配置檔案也有三個:base、dev、prod ,我們依然保留這三個配置檔案,只需要增加webpack.server.conf.js即可。

  • 9. webpack 客戶端的配置

修改webpack.base.conf.jsentry入口配置為: ./src/entry-client.js。這樣原 dev 配置與 prod 配置都不會受到影響。

伺服器端的配置也會引用base配置,但會將entry通過merge覆蓋為 server-entry.js。

生成客戶端構建清單client manifest

好處:

  1. 在生成的檔名中有雜湊時,可以取代 html-webpack-plugin 來注入正確的資源 URL。
  2. 在通過 webpack 的按需程式碼分割特性渲染 bundle 時,我們可以確保對 chunk 進行最優化的資源預載入/資料預取,並且還可以將所需的非同步 chunk 智慧地注入為 <script> 標籤,以避免客戶端的瀑布式請求(waterfall request),以及改善可互動時間(TTI - time-to-interactive)。

其實很簡單,在prod配置中引入一個外掛,並配置到plugin中即可:

 const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    // ...
    // ...
    plugins: [
       new webpack.DefinePlugin({
         'process.env': env,
         'process.env.VUE_ENV': '"client"' // 增加process.env.VUE_ENV
       }),
       //...
       // 另外需要將 prod 的HtmlWebpackPlugin 去除,因為我們有了vue-ssr-client-manifest.json之後,伺服器端會幫我們做好這個工作。
       // new HtmlWebpackPlugin({
       //   filename: config.build.index,
       //   template: 'index.html',
       //   inject: true,
       //   minify: {
       //     removeComments: true,
       //     collapseWhitespace: true,
       //     removeAttributeQuotes: true
       //     // more options:
       //     // https://github.com/kangax/html-minifier#options-quick-reference
       //   },
       //   // necessary to consistently work with multiple chunks via CommonsChunkPlugin
       //   chunksSortMode: 'dependency'
       // }),

       // 此外掛在輸出目錄中
       // 生成 `vue-ssr-client-manifest.json`。
       new VueSSRClientPlugin()
    ]
 // ...
  • 10. webpack 伺服器端的配置

server的配置有用到新外掛執行安裝: npm i -D webpack-node-externals

webpack.server.conf.js配置如下:

  const webpack = require('webpack')
  const merge = require('webpack-merge')
  const nodeExternals = require('webpack-node-externals')
  const baseConfig = require('./webpack.base.conf.js')
  const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
  // 去除打包css的配置
  baseConfig.module.rules[1].options = ''

  module.exports = merge(baseConfig, {
    // 將 entry 指向應用程式的 server entry 檔案
    entry: './src/entry-server.js',
    // 這允許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動態匯入(dynamic import),
    // 並且還會在編譯 Vue 元件時,
    // 告知 `vue-loader` 輸送面向伺服器程式碼(server-oriented code)。
    target: 'node',
    // 對 bundle renderer 提供 source map 支援
    devtool: 'source-map',
    // 此處告知 server bundle 使用 Node 風格匯出模組(Node-style exports)
    output: {
      libraryTarget: 'commonjs2'
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外接化應用程式依賴模組。可以使伺服器構建速度更快,
    // 並生成較小的 bundle 檔案。
    externals: nodeExternals({
      // 不要外接化 webpack 需要處理的依賴模組。
      // 你可以在這裡新增更多的檔案型別。例如,未處理 *.vue 原始檔案,
      // 你還應該將修改 `global`(例如 polyfill)的依賴模組列入白名單
      whitelist: /\.css$/
    }),
    plugins: [
      new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
        'process.env.VUE_ENV': '"server"'
      }),
      // 這是將伺服器的整個輸出
      // 構建為單個 JSON 檔案的外掛。
      // 預設檔名為 `vue-ssr-server-bundle.json`
      new VueSSRServerPlugin()
    ]
  })

注意此處對baseConfig刪除了一個屬性

baseConfig.module.rules[1].options = '' // 去除分離css打包的外掛
  • 11. 配置package.json增加打包伺服器端構建命令並修改原打包命令

"scripts": {
    //...
    "build:client": "node build/build.js",
    "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
    "build": "rimraf dist && npm run build:client && npm run build:server"
}   

如果出現cross-env找不到,請安裝npm i -D cross-env

  • 12. 修改index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vue-ssr-demo</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

原來的<div id="app">刪掉,只在 body 中保留一個標記即可:<!--vue-ssr-outlet-->。 伺服器端會在這個標記的位置自動生成一個<div id="app" data-server-rendered="true">,客戶端會通過app.$mount('#app')掛載到服務端生成的元素上,並變為響應式的。

注意一下,此處將模板 html 修改為服務端渲染適用的模板了,但專案中的 dev 模式也適用的這個模板,但會因為找不到#app到報錯,可以這樣處理一下:

  1. 最簡單的辦法,為dev模式單獨建立一個 html 模板。。。
  2. dev模式也整合服務端渲染模式,這樣無論生產環境與開發環境共同處於服務端渲染模式下也是相當靠譜的一件事。(官方例子是這樣操作的)
  • 13. 執行構建命令

npm run build

然後在dist目錄下可見生成的兩個 json 檔案: vue-ssr-server-bundle.jsonvue-ssr-client-manifest.json

這兩個檔案都會應用在 node 端,進行伺服器端渲染與注入靜態資原始檔。

  • 14. 構建伺服器端(官方例子使用的express,所以此 demo 將採用koa2來作為伺服器端,當然,無論是 koa 與 express 都不重要…)

npm i -S koa

在專案根目錄建立server.js,內容如下

const Koa = require('koa')
const app = new Koa()

// response
app.use(ctx => {
  ctx.body = 'Hello Koa'
})

app.listen(3001)

執行node server.js,訪問localhost:3001,確保瀏覽器得到了Hello Koa

  • 15. 編寫服務端程式碼

需要安裝koa靜態資源中介軟體: npm i -D koa-static

server.js程式碼如下:

const Koa = require('koa')
const app = new Koa()
const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')

const resolve = file => path.resolve(__dirname, file)

// 生成服務端渲染函式
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
  // 推薦
  runInNewContext: false,
  // 模板html檔案
  template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
  // client manifest
  clientManifest: require('./dist/vue-ssr-client-manifest.json')
})

function renderToString (context) {
  return new Promise((resolve, reject) => {
    renderer.renderToString(context, (err, html) => {
      err ? reject(err) : resolve(html)
    })
  })
}
app.use(require('koa-static')(resolve('./dist')))
// response
app.use(async (ctx, next) => {
  try {
    const context = {
      title: '服務端渲染測試', // {{title}}
      url: ctx.url
    }
    // 將伺服器端渲染好的html返回給客戶端
    ctx.body = await renderToString(context)

    // 設定請求頭
    ctx.set('Content-Type', 'text/html')
    ctx.set('Server', 'Koa2 server side render')
  } catch (e) {
    // 如果沒找到,放過請求,繼續執行後面的中介軟體
    next()
  }
})

app.listen(3001)

執行啟動服務命令:

node server.js
  • 16. 大功告成

瀏覽器訪問: localhost:3001/test,截圖為伺服器渲染成功的頁面 

test 頁面

test 頁面

test.vue中的 data 屬性便已證明伺服器端渲染工作是正常的(mode: process.env.VUE_ENV === 'server' ? 'server' : 'client',),但在客戶端資料混合的時候,mode 是等於 client 的。

  • 優缺點

可以做到真實資料實時渲染,完全可供SEO小蜘蛛盡情的爬來爬去

完全前後端同構,路由配置共享,不再影響伺服器404請求

依舊只支援h5 history的路由模式,(沒辦法,雜湊就是提交不到伺服器能咋辦呢。。。)

配置比較麻煩、處理流程比較複雜 (比對預渲染外掛,複雜太多)

約束較多,不能隨心所欲的亂放大招

對伺服器會造成較大的壓力,既然讓瀏覽器更快的渲染了,那就得以佔用伺服器的效能來買單了