文章來自
帶你五步學會Vue-SSR
Vue服務器端渲染
構建一個SSR應用程序
SSR熱更新
github項目
Vue-SSR優缺點
- 請求到的首屏頁面是服務器渲染好的了,SEO很好
- 但是對服務器的壓力很大


安裝插件
# 安裝 vue-server-renderer
# 安裝 lodash.merge
# 安裝 webpack-node-externals
# 安裝 cross-env
npm install vue-server-renderer lodash.merge webpack-node-externals cross-env --save-dev
# 安裝 koa
# 安裝 koa-static
npm install koa koa-static --save
改造vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore() {
return new Vuex.Store({
state: {
test: ''
},
mutations: {
SET_TEST(state, data) {
state.test = data;
}
},
actions: {
test({ commit },opt) {
// 異步查詢
commit('SET_TEST', data);
}
}
});
}
改造vue-route
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export function createRouter() {
return new Router({
mode: 'history', // 注意這里要使用history模式,因為hash不會發送到服務端
routes: []
});
}
改造main.js
import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store';
import './assets/css/style.scss';
import './assets/iconfont/iconfont.css';
Vue.config.productionTip = false;
export function createApp() {
const router = createRouter();
const store = createStore();
const app = new Vue({
router,
store,
render: h => h(App)
});
return { app, router, store };
}
改造所有的vue文件
<template>
<div>{{ test }}</div>
</template>
<script>
export default {
// 這個方法需要主動調用,等於是自定義生命周期函數,而且必須是這個名字
asyncData ({ store, route }) {
return store.dispatch('test', route.params.id)
},
computed: {
// 當asyncData被執行,vuex數據改變,導致computed發生改變
test() {
return this.$store.state.test
}
}
}
</script>
創建entry-client.js
import { createApp } from './main';
// 客戶端特定引導邏輯……
const { app, router, store } = createApp();
// 如果有__INITIAL_STATE__變量,則將store的狀態用它替換
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
// 添加路由鈎子函數,用於處理 asyncData方法
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(c => {
// 把所有的vue里的asyncData方法一起執行了
if (c.asyncData) {
return c.asyncData({ store, route: to });
}
})
).then(() => {
next();
}).catch(next);
});
// 將Vue實例掛載到dom中,完成瀏覽器端應用啟動
app.$mount('#app');
});
創建entry-server.js
import { createApp } from './main';
export default context => {
// 因為有可能會是異步路由鈎子函數或組件,所以我們將返回一個 Promise
// 以便服務器能夠等待所有的內容在渲染前,就已經准備就緒。
return new Promise((resolve, reject) => {
const { app, store, router } = 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(c => {
if (c.asyncData) {
return c.asyncData({ store, route: router.currentRoute});
}
})
).then(() => {
// 當使用 template 時,context.state 將作為 window.__INITIAL_STATE__ 狀態,自動嵌入到最終的 HTML 中
context.state = store.state;
// 返回根組件
resolve(app);
})
.catch(reject);
}, reject);
});
};
修改vue.config.js
有些教程是把這個分成三個配置文件,效果也一樣
const path = require('path');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const nodeExternals = require('webpack-node-externals'); // 忽略node_modules文件夾中的所有模塊
// const merge = require('lodash.merge');
// const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const target = TARGET_NODE ? 'server' : 'client'; //根據環境變量來指向入口
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
//基本路徑
publicPath: process.env.NODE_ENV !== 'production' ? 'http://127.0.0.1:8080' : './',
// 如果你不需要生產環境的 source map,可以將其設置為 false 以加速生產環境構建。
// productionSourceMap: false,
// 輸出文件目錄
outputDir: 'dist',
css: {
extract: process.env.NODE_ENV === 'production',
sourceMap: true
//向 CSS 相關的 loader 傳遞選項(支持 css-loader postcss-loader sass-loader less-loader stylus-loader)
},
configureWebpack: () => ({
// 將 entry 指向應用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 需要開啟source-map文件映射,因為服務器端在渲染時,
// 會通過Bundle中的map文件映射關系進行文件的查詢
devtool: 'source-map',
// 服務器端在Node環境中運行,需要打包為類Node.js環境可用包(使用Node.js require加載chunk)
// 客戶端在瀏覽器中運行,需要打包為類瀏覽器環境里可用包
target: TARGET_NODE ? 'node' : 'web',
// 關閉對node變量、模塊的polyfill
node: TARGET_NODE ? undefined : false,
output: {
// 配置模塊的暴露方式,服務器端采用module.exports的方式,客戶端采用默認的var變量方式
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
},
// 外置化應用程序依賴模塊。可以使服務器構建速度更快
externals: TARGET_NODE
? nodeExternals({
// 不要外置化 webpack 需要處理的依賴模塊。
// 你可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 你還應該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
whitelist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: TARGET_NODE ? false : undefined
},
// 根據之前配置的環境變量判斷打包為客戶端/服務器端Bundle
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
// 關閉vue-loader中默認的服務器端渲染函數
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
// merge(options, {
// optimizeSSR: false
// });
options.optimizeSSR = false;
return options;
});
config.resolve.alias
.set('@src', resolve('src'))
.set('@api', resolve('src/api'))
.set('@assets', resolve('src/assets'))
.set('@comp', resolve('src/components'))
.set('@views', resolve('src/views'));
},
devServer: {
historyApiFallback: true,
headers: { 'Access-Control-Allow-Origin': '*' }
// port: 8088
// proxy: { ... }
// }
},
lintOnSave: false
};
創建index.temp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ title }}</title>
</head>
<body>
<div id="app">
<!--vue-ssr-outlet-->
</div>
</body>
</html>
修改package.json
// 在原本的dev和build基礎上加上
"build:server": "cross-env NODE_ENV=production WEBPACK_TARGET=node vue-cli-service build",
運行npm run build:server會生成兩個json
創建service.js
const fs = require("fs");
const Koa = require("koa");
const path = require("path");
const koaStatic = require('koa-static')
const app = new Koa();
const resolve = file => path.resolve(__dirname, file);
// 開放dist目錄
app.use(koaStatic(resolve('./dist')))
// 第 2 步:獲得一個createBundleRenderer
const template = fs.readFileSync(resolve("./public/index.temp.html"), "utf-8");
const { createBundleRenderer } = require("vue-server-renderer");
const bundle = require("./dist/vue-ssr-server-bundle.json");
const clientManifest = require("./dist/vue-ssr-client-manifest.json");
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: template,
clientManifest: clientManifest
});
function renderToString(context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
}
// 第 3 步:添加一個中間件來處理所有請求
app.use(async (ctx, next) => {
const context = {
title: "ssr-test",
};
// 將 context 數據渲染為 HTML
const html = await renderToString(context);
ctx.body = html;
});
const port = 3000;
app.listen(port, function() {
console.log(`server started at localhost:${port}`);
});
修改package.json
// 再加上
"dev:serve": "node server.js",
全部配置完成,先執行打包再啟動服務
其他配置
- 服務器路由緩存,加快編譯
