# 代码地址
https://gitee.com/huwanlong/Demo-SSR.git (opens new window)
# 为什么使用服务端渲染
更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
# 预渲染
如果你调研服务器端渲染 (SSR) 只是用来改善少数营销页面(例如 /
, /about
, /contact
等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。
如果你使用 webpack,你可以使用 prerender-spa-plugin (opens new window) 轻松地添加预渲染。
vue init webpack prerender-spa
npm install prerender-spa-plugin -D
npx cross-env PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors/ npm install prerender-spa-plugin -D
// build/webpack.prod.conf.js
const PrerenderSpaPlugin = require('prerender-spa-plugin')
//调用渲染器
const Renderer = PrerenderSpaPlugin.PuppeteerRenderer
plugins: [
...
// 预渲染插件配置
new PrerenderSpaPlugin({
staticDir:path.join(__dirname,'../dist'),
routes:['/about','/test'],
// 这个配置很重要,如果没有这个配置,也不会进行预编译
renderer:new Renderer({
inject:{
foo:"far"
},
headless:false,
// 在项目的入口中使用 document.dispatchEvent(new Event('render-event'))
renderAfterDocumentEvent:'render-event'
})
}),
...
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main.js
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
mounted() {
document.dispatchEvent(new Event('render-event'))
},
})
2
3
4
5
6
7
8
9
10
npm run build
hs -o -p 9999
# 服务端渲染
Vue.js 是构建客户端应用程序的框架,默认情况下,可以在浏览器中输出 Vue 组件,进而生成 DOM 和操作 DOM,然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记“激活”为客户端上完全可交互的应用程序。
npm init -y
npm i vue express vue-server-renderer -S
// server.js
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
const app = new Vue({
template: '<div>hello</div>',
})
// 服务器端渲染的核心就在于
// 通过vue-server-renderer插件的renderToString()方法,将vue实例转换为字符串插入到html文件中
server.get('/', (req, res) => {
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
实际上我们第一次在浏览器输入 URL,并且得到返回页之后,所有的操作仍然是单页面应用在控制。我们所做的服务端渲染,只是在平时返回的单页面应用 HTML 上增加了对应路由的页面信息,好让爬虫获取到而已
明白了这一点,就可以将项目一分为二,也就是分为服务端渲染和客户端渲染。服务端渲染就是上面所做的,根据 VUE 实例获取对应路由的 seo 信息,然后添加到返回的单页面 HTML 上;
# 优点
1、更好的 SEO,搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
2、更快的内容到达时间,用户将会更快速地看到完整渲染的页面,提升用户体验
# 缺点
1、开发条件有限,要使用通用代码
2、构建设置和部署更麻烦
3、更多的服务端负载
npm install vue-server-renderer -S
# 服务端渲染代码
// build/webpack.server.conf.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.conf')
module.exports = merge(base, {
target: 'node',
entry: './src/entry-server.js',
output: {
filename: 'bundle.server.js',
libraryTarget: 'commonjs2',
},
plugins: [],
})
2
3
4
5
6
7
8
9
10
11
12
13
14
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import About from '@/components/About'
import Test from '@/components/Test'
Vue.use(Router)
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: About,
},
{
path: '/test',
name: 'Test',
component: Test,
},
],
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import { createRouter } from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
// 单例
// new Vue({
// el: '#app',
// router,
// components: { App },
// template: '<App/>'
// })
// 工厂
export function createApp() {
const router = createRouter()
const app = new Vue({
router,
components: { App },
template: '<App/>',
})
return { app }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// entry-server.js
import { createApp } from './main'
export default context => {
return new Promise((resolve, reject) => {
const { app } = createApp()
const router = app.$router
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
// 更改路由
router.push(url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
resolve(app)
}, reject)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// package.json
"server": "webpack --config build/webpack.server.conf.js"
2
3
# 客户端渲染代码
// src/entry-client.js
import { createApp } from '../src/main'
const { app } = createApp()
const router = app.$router
// 绑定app根元素
window.onload = function() {
app.$mount('#app')
}
2
3
4
5
6
7
8
9
10
// build/webpack.client.conf.js
const webpack = require('webpack')
const path = require('path')
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: './src/entry-client.js',
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist',
filename: 'bundle.client.js',
},
plugins: [],
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
vue$: 'vue/dist/vue.esm.js',
'@': resolve('src'),
},
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false,
},
},
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')],
},
],
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//server.js
const Vue = require('vue')
const exp = require('express')
const server = exp()
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']
// 设置静态资源目录
server.use('/', exp.static(__dirname + '/dist'))
const clientBundleFileUrl = '/bundle.client.js'
// const app = new Vue({
// template:'<div>hello</div>'
// })
// 服务器端渲染的核心就在于
// 通过vue-server-renderer插件的renderToString()方法,将vue实例转换为字符串插入到html文件中
server.get('*', (req, res) => {
const context = { url: req.url }
createApp(context).then(
app => {
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>SSR</title></head>
<script src="${clientBundleFileUrl}"></script>
<body>${html}</body>
</html>
`)
})
},
err => {
if (err.code === 404) {
res.status(404).end('404')
}
},
)
})
server.listen(9999, () => {
console.log('服务器已启动!')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
npm run server
npm run client
nodemon server.js
2
3
# 如何处理请求接口
如果页面有数据是请求的接口的呢?
把接口数据放到 store 里面,然后在 HTML 页面加一个INITIAL_STATE,在 entry-client.js 里处理,把数据放到客户端实例 store 里
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export function createStore() {
let store = new Vuex.Store({
state: {
homeInfo: '',
},
mutations: {
setHomeInfo(state, res) {
state.homeInfo = res
},
},
actions: {
getHomeInfo({ commit }) {
return axios.get('http://localhost:9999/api/getHomeInfo').then(res => {
commit('setHomeInfo', res.data)
})
},
},
})
return store
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Home.vue
<template>
<div>
我是首页
<router-link to="/test">测试</router-link>
<div>{{ homeInfo }}</div>
</div>
</template>
<script>
export default {
serverRequest(store) {
return store.dispatch('getHomeInfo')
},
computed: {
homeInfo() {
return this.$store.state.homeInfo
},
},
}
</script>
<style></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.js
import Vue from 'vue'
import App from './App'
import { createRouter } from './router'
import { createStore } from './store'
Vue.config.productionTip = false
export function createApp() {
const router = createRouter()
const store = createStore()
const app = new Vue({
router,
store,
components: { App },
template: '<App/>',
})
return { app }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// build/entry-server.js
import { createApp } from './main'
export default context => {
return new Promise((resolve, reject) => {
const { app } = createApp()
const router = app.$router
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
// 更改路由
router.push(url)
router.onReady(() => {
// 获取相应路由下的组件
const matchedComponents = router.getMatchedComponents()
// 没有路由匹配 返回404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// resolve(app)
// 遍历路由下所有的组件,如果有需要服务端渲染的请求,则进行请求
Promise.all(
matchedComponents.map(component => {
if (component.serverRequest) {
// 组件中如果有serverRequest对象 判断是否需要服务端请求数屈打成招,并传入一个store参数
return component.serverRequest(app.$store)
}
}),
)
.then(() => {
context.state = app.$store.state
resolve(app)
})
.catch(reject)
}, reject)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// build/entry-client.js
import { createApp } from '../src/main'
const { app } = createApp()
const router = app.$router
if (window.__INITIAL_STATE__) {
app.$store.replaceState(window.__INITIAL_STATE__)
}
// 绑定app根元素
window.onload = function() {
app.$mount('#app')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
// server.js
const Vue = require('vue')
const exp = require('express')
const server = exp()
const renderer = require('vue-server-renderer').createRenderer()
const createApp = require('./dist/bundle.server.js')['default']
// 设置静态资源目录
server.use('/', exp.static(__dirname + '/dist'))
const clientBundleFileUrl = '/bundle.client.js'
server.get('/api/getHomeInfo', (req, res) => {
res.send('SSR发送请求了')
})
// const app = new Vue({
// template:'<div>hello</div>'
// })
// 服务器端渲染的核心就在于
// 通过vue-server-renderer插件的renderToString()方法,将vue实例转换为字符串插入到html文件中
server.get('*', (req, res) => {
const context = { url: req.url }
createApp(context).then(
app => {
let state = JSON.stringify(context.state)
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' })
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>SSR</title></head>
<script>window.__INITIAL_STATE__=${state}</script>
<script src="${clientBundleFileUrl}"></script>
<body>${html}</body>
</html>
`)
})
},
err => {
if (err.code === 404) {
res.status(404).end('404')
}
},
)
})
server.listen(9999, () => {
console.log('服务器已启动!')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54