普通vue-cli初始專案轉為服務端渲染SSR

登樓痕 發表於 2020-11-21
Vue

1、第一步沒啥好說的,利用vue-cli腳手架建立一個demo專案:

vue init webpack vue-ssr-demo
cd vue-ssr-demo
npm install

2、安裝SSR依賴的模組:

npm i -D vue-server-renderer

3、隨便加一個components/test.vue

<template>
  <div>
    Just a test page.
 </div>
</template>
<script>
 export default {
 data () {
 return {
   }
  }
 }
</script>

4、src目錄下建立倆js檔案:

src
├── entry-client.js # 僅執行於瀏覽器
└── entry-server.js # 僅執行於伺服器

5、修改router/index.js

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') // 非同步元件
      }
    ]
  })
}

6、修改main.js里路由的引入:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
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 }
}

7、在entry_client.js裡面寫上如下內容:

import { createApp } from './main';

const {app, store, router} = createApp();

router.onReady(() => {
  // 新增路由鉤子函式,用於處理 asyncData.
  // 在初始路由 resolve 後執行,
  // 以便我們不會二次預取(double-fetch)已有的資料。
  // 使用 `router.beforeResolve()`,以便確保所有非同步元件都 resolve。
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)

    // 我們只關心非預渲染的元件
    // 所以我們對比它們,找出兩個匹配列表的差異元件
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })

    if (!activated.length) {
      return next()
    }

    // 這裡如果有載入指示器 (loading indicator),就觸發

    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ store, route: to })
      }
    })).then(() => {

      // 停止載入指示器(loading indicator)

      next()
    }).catch(next)
  })

  app.$mount('#app')
})

8、在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)
  })
}

9、在build資料夾下新增一個webpack.server.conf.js,寫上:

const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.conf.js')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
  target: 'node',
  devtool: '#source-map',
  entry: './src/entry-server.js',
  output: {
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  },
  resolve: {
    alias: {
      'create-api': './create-api-server.js'
    }
  },
  // https://webpack.js.org/configuration/externals/#externals
  // https://github.com/liady/webpack-node-externals
  externals: nodeExternals({
    // do not externalize CSS files in case we need to import it from a dep
    allowlist: /\.css$/
  }),
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"'
    }),
    new VueSSRServerPlugin()
  ]
})

10、在webpack.prod.conf.js中加上:

const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') // 開頭引入這個

....

 new webpack.DefinePlugin({
      'process.env': env,
      'process.env.VUE_ENV': '"client"' // 這裡加一行process.env.VUE_ENV
}),

....

 new VueSSRClientPlugin()  // plugins陣列裡最後加一個這個

11、修改webpack.base.conf.js的entry:

entry: {
    app: './src/entry-client.js' // 改為這個
},

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

"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"

13、根目錄新建一個index.template.html模板頁面(或者直接修改原index.html,只是這樣要區分生產環境和開發環境,否則run dev的時候就不ok):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title></title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

14、根目錄下新建一個server.js(沒安裝express的先npm install express):

const express = require('express');
const fs = require('fs');
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');

const app = express();

const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const template = fs.readFileSync(path.resolve('./index.template.html'), 'utf-8'); // 這裡就看13步驟裡你是哪個檔案

const render = createBundleRenderer(serverBundle, {
  runInNewContext: false, // 推薦
  template, // (可選)頁面模板
  clientManifest
});

app.use(express.static('./dist',{index:false}))

app.get('*', (req, res) => {
  const context = { url: req.url }
  render.renderToString(context, (err, html) => {
    console.log(html)
    // 處理異常……
    res.end(html)
  })
})

const port = 3003;
app.listen(port, function() {
 console.log(`server started at localhost:${port}`);
});

15、準備就緒了兄弟們,因為是本地執行,偷偷把"build:server"裡的NODE_ENV先等於development,執行:

npm run build

之後可以看到生成了dist目錄:

普通vue-cli初始專案轉為服務端渲染SSR

普通vue-cli初始專案轉為服務端渲染SSR

16、然後,執行:

node server

成功啟動server started at localhost:3003

開啟localhost:3003,在控制檯NetWork裡看到如下,SSR服務端渲染成功。

普通vue-cli初始專案轉為服務端渲染SSR