1. 程式人生 > 程式設計 >vue ssr+koa2構建服務端渲染的示例程式碼

vue ssr+koa2構建服務端渲染的示例程式碼

之前做了活動投放頁面在百度、360等渠道投放,採用 koa2 + 模版引擎的方式。發現幾個問題

  • 相較於框架開發頁面效率較低,維護性差
  • 相容性問題,在頁面中新增埋點後發現有些使用者的資料拿不到,排查後發現通過各個渠道過來的使用者的裝置中仍然包含大量低版本的瀏覽器。

服務端渲染

服務端渲染和單頁面渲染區別

檢視下面兩張圖,可以看到如果是服務端渲染,那麼在瀏覽器中拿到的直接是完整的 html 結構。而單頁面是一些 script 標籤引入的js檔案,最終將虛擬dom去掛在到 #app 容器上。

vue ssr+koa2構建服務端渲染的示例程式碼

vue ssr+koa2構建服務端渲染的示例程式碼

@vue/cli 4 來構建專案結構

下面程式碼使用最精簡的例項完整程式碼會放到 github 上

step1 安裝最新的腳手架初始化專案

yarn global add @vue/cli

step2 新增服務端檔案

啟動一個 web 服務下方程式碼中 http://localhost:9000 就是我們最終要訪問到地址

const Koa = require('koa')
const path = require('path')

const resolve = file => path.resolve(__dirname,file)
const app = new Koa()
const router = require('./router')
const port = 9000
app.listen(port,() => {
 console.log(`server started at localhost:${port}`)
})
module.exports = app

這裡只是啟動了服務,我們需要在去讀取服務端和客戶端到檔案,下面程式碼就是服務端渲染的關鍵步驟

const fs = require('fs')
const path = require('path')
const send = require('koa-send')
const Router = require('koa-router')
const router = new Router()
// 獲取當前檔案的絕對路徑
const resolve = file => path.resolve(__dirname,file)
const { createBundleRenderer } = require('vue-server-renderer')
const bundle = require('../dist/vue-ssr-server-bundle.json')
const clientManifest = require('../dist/vue-ssr-client-manifest.json')
// 建立一個 BunleRender 例項用於 renderer.renderToString 將 bundle 渲染為字串
const renderer = createBundleRenderer(bundle,{
 runInNewContext: false,template: fs.readFileSync(resolve('../src/index.temp.html'),'utf-8'),clientManifest: clientManifest
})

const handleRequest = async ctx => {
 ctx.res.setHeader('Content-Type','text/html')
 // 在 2.5.0+ 版本中,此 callback 回撥函式是可選項。在不傳遞 callback 時,此方法返回一個 Promise 物件,在其 resolve 後返回最終渲染的 HTML。
 ctx.body = await renderer.renderToString(Object.assign({},ctx.state.deliver,{ url }))
}
router.get('/home',handleRequest)
module.exports = router

vue-server-render 提供一個名為 createBundleRenderer 的 API 使用方法如下

const { createBundleRenderer } = require('vue-server-renderer')

const renderer = createBundleRenderer(serverBundle,// 推薦
 template,// (可選)頁面模板
 clientManifest // (可選)客戶端構建 manifest
})

通過上面的 createBundleRenderer 方法生產 render 物件最終將 bunlde 渲染為字串,將最終的 html 返回給客戶端。

bundleRenderer.renderToString([context,callback]): ?Promise<string>

step3 新增 entry-client.js,entry-server.js 入口檔案

在 src 中除了這兩個入口檔案,其他的檔案都是在客戶端和服務端公用的。來看下這兩個入口檔案中分別幹了什麼。

大體的流程就是:服務端建立 vue 例項,將頁面中的非同步請求的資料拿到儲存在容器中 --> 客戶端接收到服務端傳送的 html 以啟用模式進行掛載,自動給根元素 #app 上新增 data-server-rendered="true" 特殊屬性

main.js

import Vue from 'vue'
import App from './App.vue'
...
export function createApp() {
 // ...
 const app = new Vue({
 router,store,render: h => h(App)
 })
 return { app,router,store }
}

entry-server.js

import { createApp } from './main.js'
export default context => {
 // 因為有可能會是非同步路由鉤子函式或元件,所以我們將返回一個 Promise,
 // 以便伺服器能夠等待所有的內容在渲染前,
 // 就已經準備就緒。
 return new Promise((resolve,reject) => {
 const { app,store } = createApp()
 // 設定伺服器端 router 的位置
 router.push(context.url)

 // 等到 router 將可能的非同步元件和鉤子函式解析完
 router.onReady(() => {
  const matchedComponents = router.getMatchedComponents()

  // 匹配不到的路由,執行 reject 函式,並返回 404
  if (!matchedComponents.length) {
  return reject({ code: 404 })
  }
  Promise.all(
  matchedComponents.map(component => {
   if (component.asyncData) {
   return component.asyncData({
    store,context,route: router.currentRoute
   })
   }
  })
  ).then(() => {
  // 在所有預取鉤子(preFetch hook) resolve 後,
  // 我們的 store 現在已經填充入渲染應用程式所需的狀態。
  // 當我們將狀態附加到上下文,
  // 並且 `template` 選項用於 renderer 時,
  // 狀態將自動序列化為 `window.__INITIAL_STATE__`,並注入 HTML。
  // 否則會導致客戶端和服務端資料不統一造成渲染錯誤
  context.state = store.state
  resolve(app)
  }).catch(reject)
 },reject)
 })
}

entry-client.js

import { createApp } from './main'
const { app,store } = createApp()

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

router.onReady(() => {
 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()
 }

 Promise.all(
  activated.map(component => {
  if (component.asyncData) {
   component.asyncData({
   store,route: to
   })
  }
  })
 )
  .then(() => {
  next()
  })
  .catch(next)
 })
 app.$mount('#app')
})

最後

完整程式碼參考 github地址

順便貼上這張圖

vue ssr+koa2構建服務端渲染的示例程式碼

到此這篇關於vue ssr+koa2構建服務端渲染的示例程式碼的文章就介紹到這了,更多相關vue ssr koa2 服務端渲染內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!