1. 介绍
构建过程
如上图所示,使用webpack利用我们配置不同的入口生成服务端和客户端的bundle,服务端的bundle是用来生成html字符串,客户端bundle是用来注入到服务端生成的html字符串中的,由于服务端返回的是字符串,一系列的事件需要依赖客户端打包的js代码(客户端的js + 服务端渲染的字符串)由浏览器渲染这样就完成了一个ssr的构建
优点
1.利于seo优化
在浏览器渲染的时候当我们查看源代码只能看到一个<div id='app'></div> 内容是由js生成,这样不利于爬虫所爬取到,服务端渲染是将解析过程放到了服务端来做,服务端将解析好的字符串传给前端,当查看源代码时就会显示解析后的元素,爬虫更容易被检索
2.解决首页白屏的效果
如果数据量比较大那么浏览器会卡顿处于白屏状态,使用服务端渲染直接将解析好的HTML字符串传递给浏览器,大大加快了首屏加载时间
缺点
1.占用内存
所有的渲染逻辑都在服务端进行的,那么会占用更多的CPU和内存资源,当请求过多时不停的解析页面返回给客户端,会导致卡顿效果
2.浏览器Api不能使用
由于页面在服务端渲染那么服务端是不能调用浏览器的api的
3.生命周期
由于服务器端不知道什么时候挂载完成,在vue中只支持beforeCreated和created两个生命周期
2. 开发前配置
1.安装依赖包
cnpm i webpack webpack-cli webpack-dev-server koa koa-router koa-static vue vue-router vuex vue-server-renderer vue-loader vue-style-loader css-loader html-webpack-plugin @babel/core @babel/preset-env babel-loader vue-template-compiler webpack-merge url-loader
2.认识目录
3.基础代码
App.vue
<template>
<!-- id="app" 客户端激活,服务端解析成字符串返回给客户端,使其变为由 Vue 管理的动态 DOM 的过程 -->
<div id="app">
<Bar></Bar>
<Foo></Foo>
</div>
</template>
<script>
import Bar from "./components/Bar";
import Foo from "./components/Foo";
export default {
components: {
Bar,
Foo,
},
};
</script>
Bar.vue
<template>
<div id="bar">
Bar
</div>
</template>
<style scoped>
#bar {
background: red;
}
</style>
Foo.vue
<template>
<div>
Foo
<button @click="clickMe">点击</button>
</div>
</template>
<script>
export default {
methods: {
clickMe() {
alert("点我");
},
},
};
</script>
public/server.html
<!DOCTYPE html>
<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>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
main.js
Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。所以我们需要保证每次访问都会产生一个新的Vue实例,暴露一个函数每次调用都保证是新的根实例
import Vue from 'vue'
import App from './App'
export default () => {
let app = new Vue({
el: '#app',
render: h => h(App)
})
return { app }
}
client-entry.js
客户端正常挂载
import createApp from './main'
let { app } = createApp()
app.$mount('#app')
server-entry.js
import createApp from './main'
export default () => {
let { app } = createApp();
return app
}
集成路由
增加router.js文件
import Vue from 'vue'
import VueRouter from 'vue-router'
import Foo from './components/Foo.vue'
Vue.use(VueRouter)
export default () => {
const router = new VueRouter({
mode: "history",
routes: [
{ path: "/", component: Foo },
{ path: "/bar", component: () => import("./components/Bar.vue") }
]
});
return router;
}
main.js
import Vue from 'vue'
import App from './App'
import createRouter from './router'
export default () => {
let router = createRouter()
let app = new Vue({
el: '#app',
router,
render: h => h(App)
})
return { app, router }
}
App.vue
<template>
<div id="app">
<router-link to="/">foo</router-link>
<router-link to="/bar">bar</router-link>
<router-view></router-view>
</div>
</template>
server-entry.js
import createApp from './main'
// 服务端需要调用当前这个文件 产生一个vue的实例
export default (context) => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
//以便服务器能够等待所有的内容在渲染前,就已经准备就绪。
return new Promise((resolve, reject) => {
let { app, router } = createApp();
//返回的实例应该跳转到/ 或者/bar context.url是服务端跳转的默认路径
router.push(context.url)
// 涉及到异步组件的问题
router.onReady(() => {
//获取当前跳转到的匹配组件
let matchs = router.getMatchedComponents()
//matchs匹配到的所有的组件,整个都在服务端执行
if (matchs.length == 0) {
reject({ code: 404 })
}
resolve(app)
}, reject)
})
}
集成vuex
增加store文件
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default () => {
let store = new Vuex.Store({
state: {
name: ''
},
mutations: {
changeName(state) {
state.name = 'myh'
}
},
actions: {
changeName({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('changeName');
resolve()
}, 1000)
})
}
}
})
// 如果浏览器执行时 我需要将服务器设置的最新状态替换成客户端的状态,设置到window上的操作是server-entry.js下的操作
if (typeof window !== 'undefined' && window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
return store
}
main.js
import Vue from 'vue'
import App from './App'
import createRouter from './router'
import createStore from './store'
export default () => {
let router = createRouter()
let store = createStore()
let app = new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
return { app, router, store }
}
Foo.vue
<template>
<div>
Foo
<button @click="clickMe">点击</button>
{{ $store.state.name }}
</div>
</template>
<script>
export default {
asyncData(store) {
//asyncData只在服务端执行 只在页面组件中执行
return store.dispatch("changeName");
},
methods: {
clickMe() {
alert("点我");
},
},
};
</script>
server-entry.js
import createApp from './main'
export default (context) => {
return new Promise((resolve, reject) => {
let { app, router, store } = createApp();
router.push(context.url)
router.onReady(() => {
//获取到匹配到的所有路径
let matchs = router.getMatchedComponents()
if (matchs.length == 0) {
reject({ code: 404 })
}
//如果匹配到的组件中有asyncData 默认执行
Promise.all(matchs.map(v => {
if (v.asyncData) {
// asyncData是在服务端调用的
return v.asyncData(store)
}
})).then(() => {
// 以上all中的方法会改变store中的state
context.state = store.state;//把vuex的状态挂载到上下文中 会将状态挂载window上
resolve(app)
})
}, reject)
})
}
服务端代码 server.js
let Koa = require('koa')
let Router = require('koa-router')
let Static = require('koa-static')
let fs = require('fs')
let path = require('path');
let app = new Koa()
let router = new Router()
let VueServerRender = require('vue-server-renderer')
let ServerBundle = require('./dist/vue-ssr-server-bundle.json')
// 渲染打包后的结果
let template = fs.readFileSync('./dist/server.html', 'utf8')
let clientManifest = require('./dist/vue-ssr-client-manifest.json')
//createBundleRenderer 找到webpack打包后的函数 内部会调用这个函数获取到vue的实例
let render = VueServerRender.createBundleRenderer(ServerBundle, {
template,
clientManifest
})
router.get('/(.*)', async ctx => {
try {
ctx.body = await new Promise((resolve, reject) => {
//renderToString=>根据实例生成一个字符串返回给浏览器
render.renderToString({ url: ctx.url }, (err, data) => {
if (err) reject(err)
resolve(data);
});
});
} catch (e) {
ctx.body = '404'
}
})
app.use(Static(path.resolve(__dirname, 'dist')))
app.use(router.routes())
app.listen(3002)
webpack配置
webpack.base.js
let path = require('path')
let VueLoader = require('vue-loader/lib/plugin')
let resolve = dir => {
return path.resolve(__dirname, dir)
}
module.exports = {
output: {
filename: '[name].bundle.js',
path: resolve('../dist')
},
resolve: {
extensions: ['.js', '.vue']
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
},
//vue-style-loader基于style-loader实现的 支持服务端渲染
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
loader: 'url-loader'
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url-loader'
},
]
},
plugins: [
new VueLoader(),
]
}
webpack.client.js
let {merge} = require('webpack-merge')
let base = require('./webpack.base')
let path = require('path')
let ClientRenderPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(base, {
entry: {
client: path.resolve(__dirname,'../src/client-entry.js')
},
output:{
//不设置这个的话 打包出来的vue-ssr-client-manifest.json中的publicPath为'auto',默认请求静态资源http://localhost:3002/auto/client.bundle.js
publicPath:'/',
},
plugins: [
// 此插件在输出目录中,生成 `vue-ssr-client-manifest.json`。
new ClientRenderPlugin()
]
})
webpack.server.js
let { merge } = require('webpack-merge')
let base = require('./webpack.base')
let path = require('path')
let ServerRenderPlugin = require('vue-server-renderer/server-plugin')
let HtmlWebpackPlugin = require('html-webpack-plugin')
let resolve = dir => {
return path.resolve(__dirname, dir)
}
module.exports = merge(base, {
entry: {
server: resolve('../src/server-entry.js')
},
target: 'node',//要给node来使用
output: {
libraryTarget: 'commonjs2'
},
devtool: 'source-map',
plugins: [
// 这是将服务器的整个输出 构建为单个 JSON 文件的插件。 默认文件名为 `vue-ssr-server-bundle.json`
new ServerRenderPlugin(),
new HtmlWebpackPlugin({
filename: 'server.html',
template: resolve('../public/server.html'),
minify: false,//不压缩
excludeChunks: ['server']
}),
]
})
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。