参考文章:
简书:https://www.jianshu.com/p/c6a07755b08d
掘金:https://github.com/tiodot/vnews
文章中叙述的代码地址:https://github.com/ShuangWW/vue-server-renderer
项目一
1.项目搭建
mkdir ssr1 // 新建文件夹ssr1
cd ssr1 // 进入文件夹ssr1
cnpm init // 初始化
cnpm i vue vue-server-renderer --save // 安装插件vue、vue-server-renderer
cnpm i express --save // 安装插件express
2.在根目录下创建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('运行时错误')
}
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('服务器已经启动!')
})
页面.png3.输入node server.js启动服务,在浏览器输入http://localhost:8080访问
控制台.png
项目目录结构.png
项目二
1.项目搭建,并创建如下目录结构
mkdir ssr2 // 新建文件夹ssr2
cd ssr2 // 进入文件夹ssr2
cnpm init // 初始化
cnpm i vue vue-server-renderer --save // 安装插件vue、vue-server-renderer
cnpm i express --save // 安装插件express
项目目录.png
2.将package.json文件修改如下配置,然后cnpm i安装相关需要的依赖包
{
"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属性,因为#后面的内容不会发送至服务器,服务器不知道请求的是哪一个路由
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'
// 导出一个工厂函数,用于创建新的vue实例(可以隔离开各个客户端的请求,每次客户端的请求,都会创建一个新的vue实例)
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']
// 响应路由请求
express.get('*', (req, res) => {
const context = { url: req.url }
// 创建vue实例,传入请求路由信息
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('运行时错误') }
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('所请求的页面不存在') }
})
})
// 服务器监听地址
express.listen(8080, () => {
console.log('服务器已启动!')
})
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)
// 获取相应路由下的组件
const matchedComponents = app.$router.getMatchedComponents()
// 如果没有组件,说明该路由不存在,报错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 风格导出模块(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.打包文件并开启服务器(如下命令),浏览器输入localhost:8080
cnpm run server
node server
image.png
image.png
在此基础上,增加客户端渲染部分
新建 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']
// 设置静态文件目录
express.use('/', exp.static(__dirname + '/dist'))
const clientBundleFileUrl = '/bundle.client.js'
// 响应路由请求
express.get('*', (req, res) => {
const context = { url: req.url }
// 创建vue实例,传入请求路由信息(head里面添加脚本,用于引入单页面应用)
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('运行时错误') }
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('所请求的页面不存在') }
})
})
// 服务器监听地址
express.listen(8080, () => {
console.log('服务器已启动!')
})
更改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>
打包开启服务,并在浏览器输入地址localhost:8080
cnpm run server
cnpm run client
node server
image.png
当点击切换路由时,浏览器并未再次请求服务器,一切都在本地完成
项目三
基于项目二的基础上进行添加动态从服务器获取数据
1.修改entry.server.js文件。
增加一个Promise.all函数,遍历请求下的组件,看是否含有serverRequest函数来判断是否需要服务端请求数据,若需要则执行此函数并传入一个store参数
// entry.server.js
import { createApp } from '../src/main'
export default context => {
return new Promise((resolve, reject) => {
const app = createApp()
// 更改路由
app.$router.push(context.url)
// 获取相应路由下的组件
const matchedComponents = app.$router.getMatchedComponents()
// 如果没有组件,说明该路由不存在,报错404
if (!matchedComponents.length) { return reject({ code: 404 }) }
// 遍历路由下所以的组件,如果有需要服务端渲染的请求,则进行请求
Promise.all(matchedComponents.map(component => {
if (component.serverRequest) {
return component.serverRequest(app.$store)
}
})).then(() => {
resolve(app)
}).catch(reject)
})
}
2.创建store.js文件,通过axios来发起请求
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实例
import Vue from 'vue'
import createRouter from './route.js'
import App from './App.vue'
import createStore from './store'
// 导出一个工厂函数,用于创建新的vue实例
export function createApp() {
const router = createRouter()
const store = createStore()
const app = new Vue({
router,
store,
render: h => h(App)
})
return app
}
4.修改server.js,增加请求接口
const exp = require('express')
const express = exp()
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']
// 设置静态文件目录
express.use('/', exp.static(__dirname + '/dist'))
// 客户端打包地址
const clientBundleFileUrl = '/bundle.client.js'
// getHomeInfo请求
express.get('/api/getHomeInfo', (req, res) => {
res.send('SSR发送请求')
})
// 响应路由请求
express.get('*', (req, res) => {
const context = { url: req.url }
// 创建vue实例,传入请求路由信息
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('运行时错误') }
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('所请求的页面不存在') }
})
})
// 服务器监听地址
express.listen(8080, () => {
console.log('服务器已启动!')
})
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.编译并运行(localhost:8080)
cnpm run server
cnpm run client
node server
运行结果.png
请求结果.png
服务端和客户端是两个vue实例各自进行自己的渲染,然后拼接在一起的。
通过serverRequest发出的请求只有在服务端的vue实例可以拿到这个store数据
客户端的vue实例是拿不到的
请求返回的html文件中,是有'SSR发送请求的'字样的,但是页面为什么不显示?
这是因为客户端的vue实例脚本加载成功后,homeInfo被客户端的homeInfo属性覆盖,而客户端的homeInfo是没有值的,是个空属性,因此不显示
如何解决上述问题呢?
1.修改server.js
新加一个script标签,创建一个全局对象,值是state的值,将服务器端请求得出的结果传给客户端
<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']
// 设置静态文件目录
express.use('/', exp.static(__dirname + '/dist'))
const clientBundleFileUrl = '/bundle.client.js'
// getHomeInfo请求
express.get('/api/getHomeInfo',(req,res)=>{
res.send('SSR发送请求')
})
// 响应路由请求
express.get('*', (req, res) => {
const context = { url: req.url }
/* 创建vue实例,传入请求路由信息
新加一个script标签,创建一个全局对象,值是state的值,将服务器端请求得出的结果传给客户端
head里面添加脚本,用于引入单页面应用,<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('运行时错误') }
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('所请求的页面不存在') }
})
})
// 服务器监听地址
express.listen(8080, () => {
console.log('服务器已启动!')
})
2.修改entry-client.js文件
/* entry-client.js */
import { createApp } from '../src/main'
const app = createApp()
// 同步服务端信息
if(window.__INITIAL_STATE__){
app.$store.replaceState(window.__INITIAL_STATE__)
}
// 绑定app根元素
window.onload = function() {
app.$mount('#app')
}
image.png3.编译并运行
综上,项目三已经实现了完整的服务端渲染项目
项目四
类似于官方提供的demo,只不过内容源换成了掘金
网友评论