參考文章:
簡(jiǎn)書:http://m.itdecent.cn/p/c6a07755b08d
掘金:https://github.com/tiodot/vnews
文章中敘述的代碼地址:https://github.com/ShuangWW/vue-server-renderer
項(xiàng)目一
1.項(xiàng)目搭建
mkdir ssr1 // 新建文件夾ssr1
cd ssr1 // 進(jìn)入文件夾ssr1
cnpm init // 初始化
cnpm i vue vue-server-renderer --save // 安裝插件vue、vue-server-renderer
cnpm i express --save // 安裝插件express
2.在根目錄下創(chuàng)建server.js文件
const Vue = require('vue')
const express = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
const app = new Vue({
template:'<div>hello world</div>'
})
express.get('/',(req,res)=>{
renderer.renderToString(app,(err,html)=>{
if(err){
return res.state(500).end('運(yùn)行時(shí)錯(cuò)誤')
}
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue2.0 SSR渲染頁面</title>
</head>
<body>
${html}
</body>
</html>
`)
})
})
express.listen(8080,()=>{
console.log('服務(wù)器已經(jīng)啟動(dòng)!')
})
3.輸入node server.js啟動(dòng)服務(wù),在瀏覽器輸入http://localhost:8080訪問



項(xiàng)目二
1.項(xiàng)目搭建,并創(chuàng)建如下目錄結(jié)構(gòu)
mkdir ssr2 // 新建文件夾ssr2
cd ssr2 // 進(jìn)入文件夾ssr2
cnpm init // 初始化
cnpm i vue vue-server-renderer --save // 安裝插件vue、vue-server-renderer
cnpm i express --save // 安裝插件express

2.將package.json文件修改如下配置,然后cnpm i安裝相關(guān)需要的依賴包
{
"name": "ssr2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"server": "webpack --config ./webpack/webpack.server.js",
"client": "webpack --config ./webpack/webpack.client.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.16.0",
"babel": "^6.23.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"body-parser": "^1.18.3",
"compression": "^1.7.2",
"express": "^4.15.4",
"express-http-proxy": "^1.2.0",
"gulp": "^3.9.1",
"gulp-shell": "^0.6.5",
"http-proxy-middleware": "^0.18.0",
"less": "^3.0.4",
"less-loader": "^4.1.0",
"shell": "^0.5.0",
"superagent": "^3.8.3",
"vue": "^2.2.2",
"vue-meta": "^1.5.0",
"vue-router": "^2.2.0",
"vue-server-renderer": "^2.2.2",
"vue-ssr-webpack-plugin": "^3.0.0",
"vuex": "^2.2.1",
"vuex-router-sync": "^4.2.0"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^6.4.1",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^0.28.4",
"style-loader": "^0.18.2",
"vue-loader": "^11.1.4",
"vue-template-compiler": "^2.2.4",
"webpack": "^2.7.0"
}
}
3.src文件夾
// App.vue
<template>
<div>
<h2>歡迎來到SSR渲染頁面</h2>
<router-view></router-view>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
// route.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
export default function createRouter() {
let router = new VueRouter({
// 要記得增加mode屬性,因?yàn)?后面的內(nèi)容不會(huì)發(fā)送至服務(wù)器,服務(wù)器不知道請(qǐng)求的是哪一個(gè)路由
mode: 'history',
routes: [
{
alias: '/',
path: '/home',
component: require('./views/home.vue')
},
{
path: '/animal',
component: require('./views/animal.vue')
},
{
path: '/people',
component: require('./views/people.vue')
}
]
})
return router
}
// main.js
import Vue from 'vue'
import createRouter from './route.js'
import App from './App.vue'
// 導(dǎo)出一個(gè)工廠函數(shù),用于創(chuàng)建新的vue實(shí)例(可以隔離開各個(gè)客戶端的請(qǐng)求,每次客戶端的請(qǐng)求,都會(huì)創(chuàng)建一個(gè)新的vue實(shí)例)
export function createApp() {
const router = createRouter()
const app = new Vue({
router,
render: h => h(App)
})
return app
}
// home.vue
<template>
<div>
home
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
// animal.vue
<template>
<div>
animal
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
// people.vue
<template>
<div>
people
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
4..babelrc文件夾用于配置babel
{
"presets": [
"babel-preset-env"
],
"plugins": [
"transform-runtime"
]
}
5.server.js文件
/* server.js */
const express = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']
// 響應(yīng)路由請(qǐng)求
express.get('*', (req, res) => {
const context = { url: req.url }
// 創(chuàng)建vue實(shí)例,傳入請(qǐng)求路由信息
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('運(yùn)行時(shí)錯(cuò)誤') }
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue2.0 SSR渲染頁面</title>
</head>
<body>
${html}
</body>
</html>
`)
})
}, err => {
if(err.code === 404) { res.status(404).end('所請(qǐng)求的頁面不存在') }
})
})
// 服務(wù)器監(jiān)聽地址
express.listen(8080, () => {
console.log('服務(wù)器已啟動(dòng)!')
})
6.entry-server.js
import { createApp } from '../src/main'
export default context => {
return new Promise((resolve, reject) => {
const app = createApp()
// 更改路由
app.$router.push(context.url)
// 獲取相應(yīng)路由下的組件
const matchedComponents = app.$router.getMatchedComponents()
// 如果沒有組件,說明該路由不存在,報(bào)錯(cuò)404
if (!matchedComponents.length) { return reject({ code: 404 }) }
resolve(app)
})
}
7.webpack.server.js
const path = require('path');
const projectRoot = path.resolve(__dirname, '..');
module.exports = {
// 此處告知 server bundle 使用 Node 風(fēng)格導(dǎo)出模塊(Node-style exports)
target: 'node',
entry: ['babel-polyfill', path.join(projectRoot, 'entry/entry-server.js')],
output: {
libraryTarget: 'commonjs2',
path: path.join(projectRoot, 'dist'),
filename: 'bundle.server.js',
},
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.js$/,
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/,
options: {
presets: ['es2015']
}
},
{
test: /\.less$/,
loader: "style-loader!css-loader!less-loader"
}
]
},
plugins: [],
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js'
}
}
}
8.打包文件并開啟服務(wù)器(如下命令),瀏覽器輸入localhost:8080
cnpm run server
node server


在此基礎(chǔ)上,增加客戶端渲染部分
新建 entry>entry-client.js(客戶端入口文件)
新建 webpack>webpack.client.js(客戶端配置文件)
/* entry-client.js */
import { createApp } from '../src/main'
const app = createApp()
// 綁定app根元素
window.onload = function() {
app.$mount('#app')
}
/* webpack.client.js */
const path = require('path');
const projectRoot = path.resolve(__dirname, '..');
module.exports = {
entry: ['babel-polyfill', path.join(projectRoot, 'entry/entry-client.js')],
output: {
path: path.join(projectRoot, 'dist'),
filename: 'bundle.client.js',
},
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/,
options: {
presets: ['es2015']
}
}
]
},
plugins: [],
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js'
}
}
};
更改server.js文件
const exp = require('express')
const express = exp()
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']
// 設(shè)置靜態(tài)文件目錄
express.use('/', exp.static(__dirname + '/dist'))
const clientBundleFileUrl = '/bundle.client.js'
// 響應(yīng)路由請(qǐng)求
express.get('*', (req, res) => {
const context = { url: req.url }
// 創(chuàng)建vue實(shí)例,傳入請(qǐng)求路由信息(head里面添加腳本,用于引入單頁面應(yīng)用)
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('運(yùn)行時(shí)錯(cuò)誤') }
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue2.0 SSR渲染頁面</title>
<script src="${clientBundleFileUrl}"></script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
}, err => {
if(err.code === 404) { res.status(404).end('所請(qǐng)求的頁面不存在') }
})
})
// 服務(wù)器監(jiān)聽地址
express.listen(8080, () => {
console.log('服務(wù)器已啟動(dòng)!')
})
更改app.vue,添加路由
<template>
<div>
<h2>歡迎來到SSR渲染頁面</h2>
<router-link to="/home">home</router-link>
<router-link to="/animal">animal</router-link>
<router-link to="/people">people</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
打包開啟服務(wù),并在瀏覽器輸入地址localhost:8080
cnpm run server
cnpm run client
node server

當(dāng)點(diǎn)擊切換路由時(shí),瀏覽器并未再次請(qǐng)求服務(wù)器,一切都在本地完成
項(xiàng)目三
基于項(xiàng)目二的基礎(chǔ)上進(jìn)行添加動(dòng)態(tài)從服務(wù)器獲取數(shù)據(jù)
1.修改entry.server.js文件。
增加一個(gè)Promise.all函數(shù),遍歷請(qǐng)求下的組件,看是否含有serverRequest函數(shù)來判斷是否需要服務(wù)端請(qǐng)求數(shù)據(jù),若需要?jiǎng)t執(zhí)行此函數(shù)并傳入一個(gè)store參數(shù)
// entry.server.js
import { createApp } from '../src/main'
export default context => {
return new Promise((resolve, reject) => {
const app = createApp()
// 更改路由
app.$router.push(context.url)
// 獲取相應(yīng)路由下的組件
const matchedComponents = app.$router.getMatchedComponents()
// 如果沒有組件,說明該路由不存在,報(bào)錯(cuò)404
if (!matchedComponents.length) { return reject({ code: 404 }) }
// 遍歷路由下所以的組件,如果有需要服務(wù)端渲染的請(qǐng)求,則進(jìn)行請(qǐng)求
Promise.all(matchedComponents.map(component => {
if (component.serverRequest) {
return component.serverRequest(app.$store)
}
})).then(() => {
resolve(app)
}).catch(reject)
})
}
2.創(chuàng)建store.js文件,通過axios來發(fā)起請(qǐng)求
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default function createStore() {
let store = new Vuex.Store({
state: {
homeInfo: ''
},
actions: {
getHomeInfo({ commit }) {
return axios.get('http://localhost:8080/api/getHomeInfo').then((res) => {
commit('setHomeInfo', res.data)
})
}
},
mutations: {
setHomeInfo(state, res) {
state.homeInfo = res
}
}
})
return store
}
3.修改main.js文件,將store引入vue實(shí)例
import Vue from 'vue'
import createRouter from './route.js'
import App from './App.vue'
import createStore from './store'
// 導(dǎo)出一個(gè)工廠函數(shù),用于創(chuàng)建新的vue實(shí)例
export function createApp() {
const router = createRouter()
const store = createStore()
const app = new Vue({
router,
store,
render: h => h(App)
})
return app
}
4.修改server.js,增加請(qǐng)求接口
const exp = require('express')
const express = exp()
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']
// 設(shè)置靜態(tài)文件目錄
express.use('/', exp.static(__dirname + '/dist'))
// 客戶端打包地址
const clientBundleFileUrl = '/bundle.client.js'
// getHomeInfo請(qǐng)求
express.get('/api/getHomeInfo', (req, res) => {
res.send('SSR發(fā)送請(qǐng)求')
})
// 響應(yīng)路由請(qǐng)求
express.get('*', (req, res) => {
const context = { url: req.url }
// 創(chuàng)建vue實(shí)例,傳入請(qǐng)求路由信息
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('運(yùn)行時(shí)錯(cuò)誤') }
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue2.0 SSR渲染頁面</title>
<script src="${clientBundleFileUrl}"></script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
}, err => {
if(err.code === 404) { res.status(404).end('所請(qǐng)求的頁面不存在') }
})
})
// 服務(wù)器監(jiān)聽地址
express.listen(8080, () => {
console.log('服務(wù)器已啟動(dòng)!')
})
5.修改home.vue文件
<!-- home.vue -->
<template>
<div>
home
<div>{{ homeInfo }}</div>
</div>
</template>
<script>
export default {
serverRequest(store) {
return store.dispatch('getHomeInfo')
},
computed: {
homeInfo() {
return this.$store.state.homeInfo
}
}
}
</script>
<style scoped>
</style>
6.編譯并運(yùn)行(localhost:8080)
cnpm run server
cnpm run client
node server


服務(wù)端和客戶端是兩個(gè)vue實(shí)例各自進(jìn)行自己的渲染,然后拼接在一起的。
通過serverRequest發(fā)出的請(qǐng)求只有在服務(wù)端的vue實(shí)例可以拿到這個(gè)store數(shù)據(jù)
客戶端的vue實(shí)例是拿不到的
請(qǐng)求返回的html文件中,是有'SSR發(fā)送請(qǐng)求的'字樣的,但是頁面為什么不顯示?
這是因?yàn)榭蛻舳说膙ue實(shí)例腳本加載成功后,homeInfo被客戶端的homeInfo屬性覆蓋,而客戶端的homeInfo是沒有值的,是個(gè)空屬性,因此不顯示
如何解決上述問題呢?
1.修改server.js
新加一個(gè)script標(biāo)簽,創(chuàng)建一個(gè)全局對(duì)象,值是state的值,將服務(wù)器端請(qǐng)求得出的結(jié)果傳給客戶端
<script>window.INITIAL_STATE = ${state}</script>
// server.js
const exp = require('express')
const express = exp()
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']
// 設(shè)置靜態(tài)文件目錄
express.use('/', exp.static(__dirname + '/dist'))
const clientBundleFileUrl = '/bundle.client.js'
// getHomeInfo請(qǐng)求
express.get('/api/getHomeInfo',(req,res)=>{
res.send('SSR發(fā)送請(qǐng)求')
})
// 響應(yīng)路由請(qǐng)求
express.get('*', (req, res) => {
const context = { url: req.url }
/* 創(chuàng)建vue實(shí)例,傳入請(qǐng)求路由信息
新加一個(gè)script標(biāo)簽,創(chuàng)建一個(gè)全局對(duì)象,值是state的值,將服務(wù)器端請(qǐng)求得出的結(jié)果傳給客戶端
head里面添加腳本,用于引入單頁面應(yīng)用,<script src="${clientBundleFileUrl}"></script>
*/
createApp(context).then(app => {
let state = JSON.stringify(context.state)
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('運(yùn)行時(shí)錯(cuò)誤') }
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue2.0 SSR渲染頁面</title>
<script>window.__INITIAL_STATE__ = ${state}</script>
<script src="${clientBundleFileUrl}"></script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
}, err => {
if(err.code === 404) { res.status(404).end('所請(qǐng)求的頁面不存在') }
})
})
// 服務(wù)器監(jiān)聽地址
express.listen(8080, () => {
console.log('服務(wù)器已啟動(dòng)!')
})
2.修改entry-client.js文件
/* entry-client.js */
import { createApp } from '../src/main'
const app = createApp()
// 同步服務(wù)端信息
if(window.__INITIAL_STATE__){
app.$store.replaceState(window.__INITIAL_STATE__)
}
// 綁定app根元素
window.onload = function() {
app.$mount('#app')
}
3.編譯并運(yùn)行

綜上,項(xiàng)目三已經(jīng)實(shí)現(xiàn)了完整的服務(wù)端渲染項(xiàng)目
項(xiàng)目四
類似于官方提供的demo,只不過內(nèi)容源換成了掘金