文章目录
- 关于SSR
- 什么是SSR
- 为什么要用SSR
- SSR原理
- 通用代码约束:
- 构建SSR应用程序
- 创建vue项目
- 修改router/index.js
- 修改store/index.js
- 修改main.js
- 服务端数据预取
- 客户端数据预取
- 构建配置
- webpack进行打包操作
- 创建服务
关于SSR
什么是SSR
- 可以将同一个组件渲染为服务器端的
HTML
字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序
- 简单点说:就是将页面在服务端渲染完成后在客户端直接显示。无需等待所有的
JavaScript
都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。
为什么要用SSR
- 更好的
SEO
,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
- 更快的内容到达时间 (
time-to-content
),特别是对于缓慢的网络情况或运行缓慢的设备。
SSR原理
- 1)所有的文件都有一个公共的入口文件
app.js
- 2)进入
ServerEntry
(服务端入口)与clientEntry
(客户端入口)
- 3)经过webpack打包生成
ServerBundle
(供服务端SSR
使用,一个json
文件)与ClientBundle
(给浏览器用,和纯Vue
前端项目Bundle
类似)
- 4)当请求页面的时候,
node
中ServerBundle
会生成html
界面,通过ClientBundle
混合到html
页面中
通用代码约束:
- 实际的渲染过程需要确定性,所以我们也将在服务器上“预取”数据 ("
pre-fetching" data
) - 这意味着在我们开始渲染时,我们的应用程序就已经解析完成其状态。
- 避免在
beforeCreate
和 created
生命周期时产生全局副作用的代码,例如在其中使用 setInterval
设置 timer
- 通用代码不可接受特定平台的
API
,因此如果你的代码中,直接使用了像 window
或 document
,这种仅浏览器可用的全局变量,则会在 Node.js
中执行时抛出错误,反之也是如此
构建SSR应用程序
创建vue项目
vue create vue-ssr-demo
- 根据提示启动项目
修改router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
export function createRouter(){
return new VueRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: () => import( '../views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import( '../views/About.vue')
}
]
})
}
修改store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore(){
return new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
}
修改main.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
Vue.config.productionTip = false
export function createApp(){
const store = createStore();
const router = createRouter();
const app = new Vue({
store,
router,
render: h => h(App)
})
return {app, store, router}
}
服务端数据预取
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 })
}
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
客户端数据预取
import { createApp } from './main';
const {app, store, router} = createApp();
router.onReady(() => {
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 => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
next()
}).catch(next)
})
app.$mount('#app')
})
构建配置
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? 'server' : 'client'
module.exports = {
css: {
extract: false
},
configureWebpack: () => ({
entry: `./src/entry-${target}.js`,
devtool: 'source-map',
target: TARGET_NODE ? 'node' : 'web',
node: TARGET_NODE ? undefined : false,
output: {
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
},
externals: TARGET_NODE
? nodeExternals({
allowlist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: TARGET_NODE ? false : undefined
},
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return merge(options, {
optimizeSSR: false
})
})
}
}
webpack进行打包操作
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build",
"build:win": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json"
- 执行
build:win
, 生成的dist目录如下:
创建服务
const express = require('express');
const fs = require('fs');
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');
const app = express();
const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const template = fs.readFileSync(path.resolve('./src/index.template.html'), 'utf-8');
const render = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest
});
app.use(express.static('./dist',{index:false}))
app.get('*', (req, res) => {
const context = { url: req.url }
render.renderToString(context, (err, html) => {
console.log(html)
res.end(html)
})
})
const port = 3003;
app.listen(port, function() {
console.log(`server started at localhost:${port}`);
});
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>title>
head>
<body>
body>
html>
- 运行
node server.js