帶你走近Vue服務器端渲染(VUE SSR)

上篇文章(《服務器端渲染與Nuxt.js》)介紹了服務器端渲染和一些Nuxt.js的概念,如今咱們就Vue SSR方面,從基礎開始,分低、中、高三個層面,來手寫實現下傳說中的服務端渲染。css

前言

在正式搭建項目以前,咱們仍是要回顧下vue服務器端渲染的一些特性。
服務器端渲染的 Vue.js 應用程序,是使vue應用既能夠在客戶端(瀏覽器)執行,也能夠在服務器端執行,咱們稱之爲「同構」或「通用」。html

Vue.js is a framework for building client-side applications. By default, Vue components produce and manipulate DOM in the browser as output. However, it is also possible to render the same components into HTML strings on the server, send them directly to the browser, and finally "hydrate" the static markup into a fully interactive app on the client.vue

之因此可以實現同構,是由於在客戶端和服務端都建立了vue應用程序,並都用webpack進行打包,生成了server bundle和client bundle。server bundle用於服務器渲染,client bundle是一個客戶端的靜態標記,服務器渲染好html頁面片斷後,會發送給客戶端,而後混合客戶端靜態標記,這樣應用就具備vue應用的特性。
須要注意是:

  • 服務器端渲染過程當中,只會調用beforeCreatecreated兩個鉤子函數,其它的只會在客戶端執行。那麼之前spa應用中,在created中建立一個setInterval,而後在destroyed中將其銷燬的相似操做就不能出現了,服務器渲染期間不會調用銷燬鉤子函數,因此這個定時器會永遠保留下來,服務器很容易就崩了。
  • 因爲服務器可客戶端是兩種不一樣的執行平臺環境,那麼一些特定平臺的API就不能用了,好比windowdocument,在node.js(好比created鉤子函數)中執行就會報錯。而且,咱們使用的第三方API中,須要確保能在node和瀏覽器都能正常運行,好比axios,它向服務器和客戶端都暴露相同的 API(瀏覽器的源生XHR就不行)。

第一層:服務器渲染從0到1

咱們先不考慮同構、不考慮各類配置,先實現一個基礎的服務器端渲染demo。node

準備

npm install vue vue-server-renderer express --save
複製代碼

vue-server-renderervue服務器端渲染的核心模塊,它須要匹配你的vue版本。安裝express是由於咱們等會會使用它來起個服務看到咱們的頁面效果。webpack

三步渲染一個Vue實例

// 第 1 步:建立一個 Vue 實例
const Vue = require('vue')
const app = new Vue({
  template: `<div>Hello Vue SSR</div>`
})

// 第 2 步:建立一個 renderer
const renderer = require('vue-server-renderer').createRenderer()

// 第 3 步:將 Vue 實例渲染爲 HTML
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html)   // <div data-server-rendered="true">Hello Vue SSR</div>
})
複製代碼

使用模板

上面只是生產了一個html代碼片斷,通常來講,須要將html片斷插入一個模板文件裏。OK,那咱們就來寫一個模板文件index.htmlios

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head><title>Hello</title></head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>
複製代碼

在渲染的時候,html片斷會被插入到<!--vue-ssr-outlet-->這個註釋標記這裏。git

const Vue = require('vue')
const app = new Vue({
  template: `<div>Hello Vue SSR</div>`
})
const renderer = require('vue-server-renderer').createRenderer({
  template: require('fs').readFileSync('./index.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html) // html 將是注入應用程序內容的完整頁面
})
複製代碼

咱們用fs模塊將文件讀取進來丟入render的template中,再重複上述步驟將html片斷插入到咱們的標記位。github

在node.js服務器中使用

如今,咱們將使用express來啓動一個node服務,驗證一下頁面效果。web

const Vue = require('vue')
// 第一步: 建立一個 express 應用
const server = require('express')()

// 第二步: 建立一個 Vue 實例
const app = new Vue({
  data: {
    msg: 'Hello Vue SSR'
  },
  template: `<div>{{msg}}</div>`
})

// 第三步: 建立一個 renderer
const renderer = require('vue-server-renderer').createRenderer({
  template: require('fs').readFileSync('./index.html', 'utf-8')
})

// 第四步: 設置路由,"*" 表示任意路由均可以訪問它
server.get('*', (req, res) => {
  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    res.end(html)
  })
})

// 第五步: 啓動服務並監遵從8080端口進入的全部鏈接請求
server.listen(8080)
複製代碼

這樣,咱們的一個簡單的頁面渲染就完成了,看下頁面效果和Response數據。vue-router

第二層:改造 —— 從SPA到SSR

知道了怎麼在服務器端渲染出一個頁面,下一步就是實現同構啦。爲了跳過各類項目配置,咱們就從熟悉的vue-cli模板下手。
官方提供了vue-cli的項目快速構建工具,能夠用它也進行SPA項目的快速搭建,咱們如今就把這個模板,改形成一個可以集成SSR的模板。

第二部份內容參考讓vue-cli初始化後的項目集成支持SSR,侵刪。

準備

安裝vue-cli (至少v2.x版本)後,使用基礎模板搭建個項目

vue init webpack spa_ssr
cd spa_ssr
複製代碼

跑一下確保項目可以正常運行,而後記得安裝vue-server-renderer模塊

npm install vue-server-renderer --save-dev
複製代碼

安裝完成,咱們就開始進入下一步。

改造src下的文件

咱們須要在src目錄下建立兩個js。

src
├── router
│   └── index.js
├── components
│   └── HelloSsr.vue
├── App.vue
├── main.js
├── entry-client.js # 僅運行於瀏覽器
└── entry-server.js # 僅運行於服務器
複製代碼

這兩個entry以後會進行配置,先來改造main.js
在改造main.js以前,須要說明一下,因單線程的機制,在服務器端渲染時,過程當中有相似於單例的操做,那麼全部的請求都會共享這個單例的操做,因此應該使用工廠函數來確保每一個請求之間的獨立性。好比在main.js中,咱們原先直接建立一個Vue實例,並直接掛載到DOM。如今的main.js做爲通用entry文件,它應該改形成一個能夠重複執行的工廠函數,爲每一個請求建立新的應用程序實例。掛載的工做,是由以後的客戶端entry來完成。

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

export function createApp () {
  const router = new CreateRouter()
  const app = new Vue({
    router,
    render: h => h(App)
  })
  return { app, router }
}
複製代碼

/router/index.js中,咱們一樣須要使用工廠函數來建立路由實例。而後將路由配置改成history模式(由於哈希不支持)

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

Vue.use(Router)

export function CreateRouter () {
  return new Router({
    mode: 'history',
    routes: [{
      path: '/ssr',
      name: 'HelloSsr',
      component: HelloSsr
    }]
  })
}
複製代碼

接下來咱們來寫客戶端的entry和服務器端的entry。客戶端的entry要作的很簡單,就是將vue實例掛載到DOM上,只不過,考慮到可能存在異步組件,須要等到路由將異步組件加載完畢,才進行此操做。

// entry-client.js
import { createApp } from './main'
const { app, router } = createApp()

router.onReady(() => {
  app.$mount('#app')
})
複製代碼

服務器entry要作的有兩步:1.解析服務器端路由;2.返回一個vue實例用於渲染。

// 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)
  })
}
複製代碼

webpack配置

vue相關代碼已處理完畢,接下來就須要對webpack打包配置進行修改了。 官方推薦了下面配置:

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

咱們的項目中的配置文件是basedevprod,如今咱們仍然保留這三個配置文件,只須要增長webpack.server.conf.js便可。

webpack.base.conf.js修改

咱們首先修改webpack.base.conf.jsentry入口配置爲:./src/entry-client.js,來生成客戶端的構建清單client manifest。服務器端的配置因爲引用base配置,entry會經過merge覆蓋,來指向server-entry.js

// webpack.base.conf.js
module.exports = {
  entry: {
    // app: './src/main.js'
    app: './src/entry-client.js'   // <-修改入口文件改成
  },
  // ...
}
複製代碼

webpack.prod.conf.js修改

在客戶端的配置prod中,咱們須要引入一個服務器端渲染的插件client-plugin,用來生成vue-ssr-client-manifest.json(用做靜態資源注入),同時,咱們須要把HtmlWebpackPlugin給去掉,在SPA應用中,咱們用它來生成index.html文件,可是這裏咱們有vue-ssr-client-manifest.json以後,服務器端會幫咱們作好這個工做。

const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
// ...
  plugins: [
    new webpack.DefinePlugin({
      'process.env': env,
      'process.env.VUE_ENV': '"client"' // 增長process.env.VUE_ENV
    }),
    // ...
    // 如下內容註釋(或去除)
    // 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()
  ]
// ...
複製代碼

webpack.server.conf.js配置

server配置基本參考官方的配置,這裏仍是說明下:

  1. 咱們須要去掉baseConfig中的打包css的配置;
  2. 這裏使用了webpack-node-externals來加快構建速度和減少打包體積,因此咱們要先安裝一下它:npm install webpack-node-externals --save-dev
  3. prod配置同樣,這裏須要引入並使用server-plugin插件來生成vue-ssr-server-bundle.json。這東西是用來等會作服務器端渲染的。
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()
  ]
})
複製代碼

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。(cross-env用來防止使用NODE_ENV =production 來設置環境變量時,Windows命令提示會報錯)

npm install --save-dev cross-env
複製代碼

修改index.html

如第一層說的,咱們須要在這個index.html外層模板文件中,插入一個<!--vue-ssr-outlet-->註釋標記,用來標識服務器渲染的html代碼片斷插入的地方,同時刪掉原先的<div id="app">
服務器端會在這個標記的位置自動生成一個<div id="app" data-server-rendered="true">,客戶端會經過app.$mount('#app')掛載到服務端生成的元素上,並變爲響應式的。

  • ps:這裏單純將模板改成服務器端渲染適用的模板,可是在dev模式下,會由於找不到#app而報錯,這裏就不作dev下的處理,若是須要,能夠爲dev模式單獨創建一個html模板。

打包構建

npm run build
複製代碼

在dist目錄下會生成兩個json文件:vue-ssr-server-bundle.jsonvue-ssr-client-manifest.json,用於服務端端渲染和靜態資源注入。

構建服務器端

這裏仍是採用express來做爲服務器端,先進行安裝:

npm install express --save
複製代碼

以後在根目錄下建立server.js,代碼主要分爲3步:

  1. 採用createBundleRenderer來建立renderer,咱們引入以前生成好的json文件,並讀取index.html做爲外層模板;
  2. 設置路由,當請求指定路由的時候,設置請求頭,調用渲染函數,將渲染好的html返回給客戶端;
  3. 監聽3001端口。
const express = require('express')
const app = express()

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'), {
  // 模板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(express.static('./dist'))

app.use(async(req, res, next) => {
  try {
    const context = {
      title: '服務端渲染測試', // {{title}}
      url: req.url
    }
    // 設置請求頭
    res.set('Content-Type', 'text/html')
    const render = await renderToString(context)
    // 將服務器端渲染好的html返回給客戶端
    res.end(render)
  } catch (e) {
    console.log(e)
    // 若是沒找到,放過請求,繼續運行後面的中間件
    next()
  }
})

app.listen(3000)
複製代碼

完過後啓動服務命令:

node server.js
複製代碼

訪問localhost:3000/ssr,就能獲取咱們以前定義好的頁面。

第三層:Nuxt.js源碼初探

Nuxt.js是什麼

Nuxt.js是Vue官方推薦的一個項目,它是一個基於 Vue.js 的通用應用框架。預設了服務器端渲染所需的各類配置,如異步數據,中間件,路由,只要遵循其中的規則就能輕鬆實現SSR。開箱即用,體驗友好。經過對客戶端/服務端基礎架構的抽象組織,Nuxt.js 主要關注的是應用的 UI渲染。

Nuxt.js的一小小小部分源碼解讀

Nuxt.js源碼涉及的內容比較多,咱們不一一細說(好吧,是我功力不夠,吃不透 = =||)。咱們就來看看,Nuxt.js做爲中間件的時候,整個流程都幹了些什麼。
Nuxt.js官方提供的examples裏有一個custom-serverNuxt會做爲中間件傳入express中,咱們來看下代碼:

import express from 'express'
import { Nuxt, Builder } from 'nuxt'

const app = express()

const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000

// Import and set Nuxt.js options
let config = require('./nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')

const nuxt = new Nuxt(config)

// Start build process in dev mode
if (config.dev) {
  const builder = new Builder(nuxt)
  builder.build()
}

// Give nuxt middleware to express
app.use(nuxt.render)

// Start express server
app.listen(port, host)
複製代碼

這段代碼有部分地方和咱們以前寫的類似,都用express起了一個服務,這裏涉及Nuxt.js的代碼有2處:

  1. 引入nuxt.config.js配置文件,並做爲參數,建立一個Nuxt實例;
  2. 實例化一個Builder,構建路由。

Nuxt是根據pages文件夾下的目錄結構來生成對應的路由的,這是Nuxt的特色之一,這部份內容咱們這裏不展開細說,之後再詳細討論。咱們主要來講說new Nuxt()裏的事。
上面咱們看的,咱們是將Nuxt的實例化對象的render屬性值傳做爲中間件傳給了express,咱們在源碼中全局搜索找到Nuxt構造函數:


咱們看到,這裏又建立了一個Renderer實例,那咱們繼續找到這個 Renderer構造函數。代碼太長我就不全貼了。這麼多內容感受仍是無從下手,不慌,既然以前說了, vue-server-renderer是SSR服務器端渲染的核心模塊,那咱們嘗試在這裏搜索 vue-server-renderer,果真,搜到以下內容:

在createRenderer函數中,咱們經過註釋能夠看到,爲服務器端渲染建立了一個 bundle renderer,這不就是咱們以前本身實現的服務器渲染函數麼,它的第一個參數 this.resources.serverBundle,咱們在文件中能夠搜到:

咱們的nuxt項目會在運行的時候自動構建,生成一個.nuxt的文件夾,裏面就包含了:

這一串代碼聯合起來,咱們能夠看到, Nuxt.js一樣是使用預編譯的應用程序包 createBundleRenderer來建立了渲染器,其中所需的 server-bundle.jsonclient-mainfest.json會由nuxt在運行的時候自動構建生成。
下面是調用裏面的renderToString方法,和咱們上面寫的demo同樣,來生成html片斷。咱們在 renderRoute方法中找到了它:

這段代碼向咱們展現了一段比較完整的渲染流程,調用渲染函數生成HTML片斷,拼接HTML片斷,拼接HEAD片斷,丟進預置的模板中渲染出完整的html。由此能夠看出, Nuxt.js的核心服務器端渲染原理和咱們以前寫的demo基本相同。

總結

服務器端渲染的優劣都很是明顯,若是僅僅是爲了優化網頁的SEO,咱們還能夠嘗試Vue官方給咱們推薦的預渲染(Prerendering),這裏就很少贅述。 以上Demo僅僅是做爲服務器端渲染的一種實現demo,若是須要正式用到項目中,還須要更加複雜的配置。若是隻是使用服務器端渲染來開發簡單的項目,咱們能夠直接用Nuxt.js便可。 以上有些知識點在概念上有誤差,歡迎指正。

參考文獻

  1. Vue SSR官方指南:ssr.vuejs.org/zh/
  2. 讓vue-cli初始化後的項目集成支持SSR: blog.csdn.net/ligang25851…
  3. vue-server-renderer: www.jianshu.com/p/8e7099aed…
相關文章
相關標籤/搜索