vue-ssr解决什么问题?
-
spa
全部靠的是js来渲染,默认首页显示内容是一个空的div
标签,不利于SEO
搜索引擎搜索 -
服务端渲染是可以被爬虫抓取到的,客户端异步渲染是很难被爬虫抓取到的
-
spa
应用会有首页白屏时间过长的问题,服务器渲染的好处是将访问好的数据拼接好给前端,首页白屏时间缩短
服务端渲染的缺点?
需要占用服务器的CPU和内存,前端中的一些生命周期函数无法使用
vue-ssr使用
使用的包
vue
vue-server-renderer
koa
koa-router
示例
template.html: 这里的 <!--vue-ssr-outlet-->
是固定字段
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
const Koa = require('koa');
const Router = require('koa-router');
const Vue = require('vue');
const VueServerRenderer = require('vue-server-renderer');
const app = new Koa();
const router = new Router();
const path = require('path');
const fs = require('fs');
const vm = new Vue({
data: {
name: '张三'
},
template: '<div>hello {{name}}</div>'
})
const template = fs.readFileSync(path.resolve(__dirname, 'template.html'), 'utf8');
router.get('/', async (ctx) => {
ctx.body = await VueServerRenderer.createRenderer(template).renderToString(vm);
})
//使用路由
app.use(router.routes());
app.listen(3000, () => {
console.log('start server success')
})
通过webpack实现编译vue项目
依赖包
npm i webpack webpack-cli webpack-merge -D
npm i @babel/core @babel/preset-env babel-loader -D
npm i vue-loader vue-style-loader vue-template-compiler -D
npm i css-loader html-webpack-plugin concurrently -D
项目结构
ssr
├── dist
│ ├── client.bundle.js
│ ├── client.html
│ ├── server.bundle.js
│ └── server.html
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── index.ssr.html
├── server.js
├── src
│ ├── App.vue
│ ├── client-entry.js
│ ├── components
│ │ ├── Bar.vue
│ │ └── Foo.vue
│ ├── main.js
│ └── server-entry.js
├── vue-ssr.md
├── webpack.base.js
├── webpack.client.js
└── webpack.server.js
公共webpack配置
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development',
output: '[name].bundle.js',
module: {
rules: [{
test: /\.css$/,
use: ['vue-style-loader', {
loader: 'css-loader',
options: {
esModule: false, //注意配合vue-style-loader使用时需要加上这个属性
}
}]
},
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin() //
]
}
客户端webpack配置
const path = require('path');
cosnt HtmlWebpackPlugin = require('html-webpack-plugin');
const {
merge
} = reqire('webpack-merge');
const base = require('./webpack.base.js');
module.exports = (base, {
entry: {
client: path.resolve(__dirname, './src/client-entry.js')
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.html'),
filename: 'client.html'
})
]
})
服务端webpack配置
const path = require('path');
cosnt HtmlWebpackPlugin = require('html-webpack-plugin');
const {
merge
} = reqire('webpack-merge');
const base = require('./webpack.base.js');
module.exports = merge(base, {
target: 'node', //目标是给node使用
entry: {
server: path.resolve(__dirname, './src/server-entry.js')
},
output: {
libraryTarget: 'commonjs2' //让打包后的server.bundle.js用module.exports导出
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.ssr.html'),
filename: 'server.html',
excludeChunks: ['server'], //打包后server.html中不引入server.bundle.js
mimyfy: false, //不压缩,防止<!--vue-ssr-outlet-->被覆盖
})
]
})
index.ssr.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
<script src="./client.bundle.js"></script>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
main.js
const Vue from 'vue';
const App from './App.vue';
export default () => {
const app = new Vue({
render: h => h(App)
})
return {
app
}
}
client-entry.js
const createApp from './main.js';
const {
app
} = createApp();
app.$mount('#app');
server-entry.js
const createApp from './main.js';
export default () => {
const {
app
} = createApp();
return app;
}
server.js
const Koa = require('koa');
const Router = require('koa-router');
const static = require('koa-static');
const VueServerRenderer = require('vue-server-renderer');
const fs = require('fs');
const path = require('path');
const app = new Koa();
cont router = new Router();
const serverBundle = fs.readFileSync(path.resolve(__dirname, './dist/server.bundle.js'));
const template = fs.readFileSync(path.resolve(__dirname, './dist/server.html'));
const render = VueServerRenderer.createBundleRenderer(serverBundle, {
template
})
router.get('/', async (ctx) => {
ctx.body = new Promise((resolve, reject) => {
render.renderToString((err, html) => {
if (err) reject(err);
resolve(html);
})
})
})
app.use(route.routes())
app.use(static(path.resolve(__dirname, './dist')));
app.listen(3000, () => {
console.log('start server success');
})
App.vue
这里需要注意的是,在最外层需要添加一个
id='app'
, ssr官网上有说客户端激活
, data-server-rendered 特殊属性,让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载。注意,这里并没有添加 id=“app”,而是添加 data-server-rendered 属性:你需要自行添加 ID 或其他能够选取到应用程序根元素的选择器,否则应用程序将无法正常激活,
<template>
<div id='app'>
<Foo></Foo>
<Bar></Bar>
</div>
</template>
<script>
import Bar from "./components/Bar.vue";
import Foo from "./components/Foo.vue";
export default {
components: {
Foo,
Bar,
},
};
</script>
Bar.vue
<template>
<div>bar</div>
</template>
<style scoped="true">
div{
background:red
}
</style>
Foo.vue
<template>
<div @click="show">foo</div>
</template>
<script>
export default {
methods:{
show(){
alert(1)
}
}
}
</script>