iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 30
1
Modern Web

從 0 開始建一個 Static Site Generator系列 第 30

Day 30: 更多的 Vue SSR

這篇程式碼在 https://github.com/DanSnow/ironman-2020/tree/master/static-site-generator/packages/vue-ssr

接續上篇的內容,我們來把 Vuex 跟 vue-router 都加入我們的 SSR 中

Vuex

我們簡單的建一個 Vuex store :

import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex)

export default new Vuex.Store({
  state: () => ({
    message: '',
  }),
  mutations: {
    SET_MESSAGE(state, message) {
      state.message = message
    },
  },
})

然後在 App.vue 的 serverPrefetch 加上抓 API 的程式碼:

import ky from 'ky-universal'

export default {
  async serverPrefetch() {
    // 這個 API 寫在 server.js 裡
    const data = await ky.get('http://localhost:3000/api/foo').json()
    this.$store.commit('SET_MESSAGE', data.message)
  },

  computed: {
    message() {
      return this.$store.state.message
    },
  },
}

再來修改 app.js 讓它也回傳 store ,之後也要像這樣把 router 傳出來:

import Vue from 'vue'
import App from './App.vue'
import store from './store'

export function createApp() {
  const app = new Vue({
    store,
    render: (h) => h(App),
  })
  return { app, store }
}

再來就是重點了,在 entry-server.js 中,我們要把 store 的狀態存進 contextstate 中:

import { createApp } from './app'

export default (context) => {
  const { app, store } = createApp()
  // 這個是 vue-server-renderer 的一個 hook ,會在 render 完時呼叫
  context.rendered = () => {
    // 把 state 存進 context 中
    context.state = store.state
  }
  return app
}

接下來就是 Vue 的魔法了,你可以啟動 server ,看一下產出來的網頁原始碼,你看到了什麼呢?

<script>window.__INITIAL_STATE__={"message":"Hello world"}</script>

vue-server-renderer 已經幫我們把 state 加到產出來的 html 中了,但是如果你打看網頁,應該會發現你看不到這段訊息,因為我們沒有在 Client 端把初始的狀態加回去 Vuex 中,我們在 entry-client.js 中加上:

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

就這樣, Server 的狀態就可以用 Vuex 傳到 Client 端,再來是 vue-router

vue-router

為了使用 vue-router ,這邊我自己先很簡單的拆了兩個頁面出來,我們直接看 app.js ,這邊把 router 加進:

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

export function createApp() {
  const app = new Vue({
    store,
    router,
    render: (h) => h(App),
  })
  return { app, store, router }
}

再來是 entry-server.js

import { createApp } from './app'

export default (path, context) => {
  // 因為要能等待 router 做完,所以這邊改用 Promise 了
  return new Promise((resolve) => {
    const { app, store, router } = createApp()
    // 把路徑給 router ,讓它去 render 第一個頁面
    router.push(path)
    // 用 onReady 等待 router 通知完成
    router.onReady(() => {
      context.rendered = () => {
        context.state = store.state
      }
      // 用 Promise 回傳 app
      resolve(app)
    })
  })
}

server.js 則只是因應 entry-server.js 的改變,把 createApp 前加上 await 跟把路徑 / 改成用 /* 而已,就這樣,把 Vue 兩個很重要的部份也加了上去

Bundle Renderer

如果你真的有操作過一次的話,你中間應該有重啟 server 不少次吧,每次要重啟伺服器真的很麻煩, vue-server-renderer 提供了 Bundle Renderer 就是為了解決這個問題,它能夠自動重新載入打包好的 server 端的程式碼,不過因為這個功能似乎還沒支援 webpack 5 ,就大概講一下就好了,用這個要改兩個部份,一個是把 vue-server-renderer/server-plugin 的 webpack plugin 加入 webpack 的設定中,這樣產生出來的就不會是一個 js 檔,而是一個 vue-ssr-server-bundle.json

再來是 createRenderer 改成 createBundleRenderer

const renderer = createBundleRenderer('path/to/vue-ssr-server-bundle.json', {template})

而使用時則不用再傳入 app 當參數:

const html = await renderer.renderToString(context)

再來只要搭配 webpack 的 watch mode 就可以自動重新載入了

其實我有嘗試把 vue-server-renderer 的 plugin 修好,不過似乎還是有問題

這是這系列的技術文章部份的最後一篇了


上一篇
Day 29: 實作 Vue 的 Server Side Render
系列文
從 0 開始建一個 Static Site Generator30

尚未有邦友留言

立即登入留言