什麼是伺服器端渲染 (SSR)
Vue.js 是建構用戶端應用程式的架構。預設情況下,可以在浏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以将同一個元件渲染為伺服器端的 HTML 字元串,将它們直接發送到浏覽器,最後将這些靜态标記"激活"為用戶端上完全可互動的應用程式。
伺服器渲染的 Vue.js 應用程式也可以被認為是"同構"或"通用",因為應用程式的大部分代碼都可以在伺服器和用戶端上運作。
為什麼使用伺服器端渲染
1.更好的 SEO,由于搜尋引擎爬蟲抓取工具可以直接檢視完全渲染的頁面
2.更快的内容到達時間 (time-to-content),特别是對于緩慢的網絡情況或運作緩慢的裝置
3.在對你的應用程式使用伺服器端渲染 (SSR) 之前,你應該問的第一個問題是,是否真的需要它。這主要取決于内容到達時間 (time-to-content) 對應用程式的重要程度。例如,如果你正在建構一個内部儀表盤,初始加載時的額外幾百毫秒并不重要,這種情況下去使用伺服器端渲染 (SSR) 将是一個小題大作之舉。然而,内容到達時間 (time-to-content) 要求是絕對關鍵的名額,在這種情況下,伺服器端渲染 (SSR) 可以幫助你實作最佳的初始加載性能。
準備工作
1.搭建一個vue項目
2.安裝的插件
cnpm install vue vue-server-renderer --save
cnpm install express --save
cnpm install cross-env -D
參考來源 https://ssr.vuejs.org/zh/guide/
參考文章連結https://blog.csdn.net/weixin_30732487/article/details/98167965
首先是main.js
我們為每個請求建立一個新的根 Vue 執行個體。這與每個使用者在自己的浏覽器中使用新應用程式的執行個體類似。如果我們在多個請求之間使用一個共享的執行個體,很容易導緻交叉請求狀态污染 (cross-request state pollution)
是以,我們不應該直接建立一個應用程式執行個體,而是應該暴露一個可以重複執行的工廠函數,為每個請求建立新的應用程式執行個體:
// main.js
import Vue from 'vue'
import App from './App.vue'
import {createRouter} from './router'
import {createStore} from './store'
import { sync } from 'vuex-router-sync'
Vue.config.productionTip = false
export default function createApp () {
// 建立 router 和 store 執行個體
const router = createRouter()
const store = createStore()
// 同步路由狀态(route state)到 store
sync(store, router)
const app = new Vue({
router,
store,
render: h => h(App)
})
return {app, router, store}
}
entry-client.js用戶端配置
用戶端 entry 隻需建立應用程式,并且将其挂載到 DOM 中:
// entry-client.js
import createApp from './main'
const {app, router, store} = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
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')
})
entry-server.js服務端配置
在 entry-server.js 中,我們可以通過路由獲得與 router.getMatchedComponents() 相比對的元件,如果元件暴露出 asyncData,我們就調用這個方法。然後我們需要将解析完成的狀态,附加到渲染上下文(render context)中。
// entry-server.js
import createApp from './main'
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 對所有比對的路由元件調用 `asyncData()`
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 在所有預取鈎子(preFetch hook) resolve 後,
// 我們的 store 現在已經填充入渲染應用程式所需的狀态。
// 當我們将狀态附加到上下文,
// 并且 `template` 選項用于 renderer 時,
// 狀态将自動序列化為 `window.__INITIAL_STATE__`,并注入 HTML。
context.state = store.state
resolve(app)
}).catch(err => {
throw err
})
}, reject)
})
}
router.js路由配置
你可能已經注意到,我們的伺服器代碼使用了一個 * 處理程式,它接受任意 URL。這允許我們将通路的 URL 傳遞到我們的 Vue 應用程式中,然後對用戶端和伺服器複用相同的路由配置!
為此,建議使用官方提供的 vue-router。我們首先建立一個檔案,在其中建立 router。注意,類似于 createApp,我們也需要給每個請求一個新的 router 執行個體,是以檔案導出一個 createRouter 函數:
// router.js
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue')
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue')
}
],
});
}
store.js
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 假定我們有一個可以傳回 Promise 的
// 通用 API(請忽略此 API 具體實作細節)
import { fetchItem } from '../api'
export function createStore () {
return new Vuex.Store({
state: {
items: {}
},
actions: {
fetchItem ({ commit }, id) {
// `store.dispatch()` 會傳回 Promise,
// 以便我們能夠知道資料在何時更新
return fetchItem(id).then(item => {
console.log(item)
commit('setItem', { id, item })
})
}
},
mutations: {
setItem (state, { id, item }) {
Vue.set(state.items, id, item)
}
}
})
}
vue.config
// vue.config.js
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const env = process.env
const isServer = env.RUN_ENV === 'server'
console.log(isServer, env.RUN_ENV)
module.exports = {
publicPath: './',
outputDir: `dist/${env.RUN_ENV}`,
configureWebpack: {
// 将 entry 指向應用程式的 server / client 檔案
entry: `./src/entry-${env.RUN_ENV}.js`,
devtool: 'eval',
// 這允許 webpack 以 Node 适用方式(Node-appropriate fashion)處理動态導入(dynamic import),
// 并且還會在編譯 Vue 元件時,
// 告知 `vue-loader` 輸送面向伺服器代碼(server-oriented code)。
target: isServer ? 'node' : 'web',
// 此處告知 server bundle 使用 Node 風格導出子產品(Node-style exports)
output: {
libraryTarget: isServer ? 'commonjs2' : undefined
},
optimization: {
splitChunks: isServer ? false : undefined
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應用程式依賴子產品。可以使伺服器建構速度更快,
// 并生成較小的 bundle 檔案。
externals: isServer ? nodeExternals({
// 不要外置化 webpack 需要處理的依賴子產品。
// 你可以在這裡添加更多的檔案類型。例如,未處理 *.vue 原始檔案,
// 你還應該将修改 `global`(例如 polyfill)的依賴子產品列入白名單
allowlist: /\.css$/
}) : undefined,
// 這是将伺服器的整個輸出
// 建構為單個 JSON 檔案的插件。
// 服務端預設檔案名為 `vue-ssr-server-bundle.json`
// 用戶端預設檔案名為 `vue-ssr-client-manifest.json`
plugins: [
isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
]
}
}
最後配置一個本地開發使用的server.js
// server.js
const http = require('http')
const path = require('path')
const fs = require('fs')
const express = require('express')
const {createBundleRenderer} = require('vue-server-renderer')
const serverBundle = require('./dist/server/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/client/vue-ssr-client-manifest.json')
const app = express()
app.set('port', 8001);
// 靜态檔案目錄指向dist檔案夾
app.use(express.static(path.join(__dirname, './dist/client')))
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template: fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8'),
clientManifest
})
app.get('*', (req, res) => {
const context = {
title: 'Hello SSR',
url: req.url
}
renderer.renderToString(context, (err, html) => {
if (err) {
if (err.code === 404) {
res.status(404).end('404 not found')
} else {
res.status(500).end(err.message)
}
} else {
res.end(html)
}
})
})
/*服務啟動*/
http.createServer(app).listen(app.get('port'), function () {
console.log('service start at ' + app.get('port'));
});
package.json添加下面指令
"start": "npm run build:server && npm run build:client && npm run service",
"build:client": "cross-env RUN_ENV=client vue-cli-service build",
"build:server": "cross-env RUN_ENV=server vue-cli-service build",
"service": "node server.js"
一個坑打包server時報錯Server-side bundle should have one single entry file. Avoid using CommonsChu
在config.js裡面添加下面幾行代碼
optimization: {
splitChunks: isServer ? false : undefined
}
伺服器端包應該有一個條目檔案。避免在伺服器配置中使用CommonsChunkPlugin。
猜測可能是因為vue-cli3配置了webpack4 裡面的分包功能,是以,再弄得環境下給他禁止掉就好了
自己記錄也可以給有興趣的同學參考,不喜勿噴