上一篇文章介绍了vue ssr
的实现思路和打包流程,实现了一个简单版的 ssr。这一篇将实现客户端和服务端分离打包,根据不同端的配置打包出来不同的端文件。服务端返回打包出来 html 的内容,然后配合客户端打包出来的 js 逻辑,来实现服务端渲染。
具体的实现可以参照官方文档,本文也是照搬照抄。。。
Vue SSR 指南
https://ssr.vuejs.org/zh/
新建工程
新建src
和public
目录,public 目录有两个 html 模板文件,index.ssr.html 不同的是需要引入 ssr 的标记,表示服务端渲染,目录结果如下:
.
├── public
│ └── index.html // 客户端模板
│ └── index.ssr.html // 服务端模板
├── build // webpack打包配置
│ └── webpack.base.js // 公用配置
│ └── webpack.client.js // 客户端配置
│ └── webpack.server.js // 服务端配置
├── src
│ ├── app.js // app 入口文件
│ ├── App.vue // page入口
│ ├── client-entry.js // 客户端打包入口文件
│ ├── server-entry.js // 服务端打包入口文件
│ ├── components
│ │ ├── Foo.vue
│ │ ├── Bar.vue
├── server.js
客户端配置
这一步先实现将客户端应用运行起来。webpack
的配置需要使用如下的包:
- webpack webpack-cli webpack-dev-server(webpack相关包)
- html-webpack-plugin(html模板插件,将打出来的文件直接插入模板中)
- 解析css文件: vue-style-loader(支持服务端渲染) css-loader vue-template-compiler
- 解析js文件:@babel/core @babel/preset-env babel-loader
- 解析vue文件:vue-loader
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin vue-loader vue-style-loader css-loader vue-template-compiler @babel/core @babel/preset-env babel-loader -D
配置webpack文件
配置webpack.base.js
// webpack.base.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const resolve = dir => {
return path.resolve(__dirname, dir)
}
module.exports = {
mode: 'production',
output: {
filename: '[name].bundle.js', // 入口文件
path: resolve('../dist'), // 出口目录
},
resolve: {
extensions: ['.js', '.vue', '.css', '.jsx'] // 引入文件时省略后缀
},
module: {
rules: [
{
test: /\.vue$/, // 解析vue文件
use: 'vue-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader'] // loader的执行顺序是从上到下,从右到左
},
{
test: /\.js$/, // 解析es6以上文件
use: {
options: { // babel 配置
presets: ['@babel/preset-env'] // 将es6转化为es5
},
loader: 'babel-loader' // babel-loader 会默认调babel-core
},
exclude: /node_modules/
}
]
},
plugins: [
new VueLoaderPlugin(),
]
}
配置webpack.client.js
// webpack.client.js
const base = require('./webpack.base')
const { merge } = require('webpack-merge') // 合并webpack配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') // 客户端映射插件
const path = require('path')
const resolve = dir => {
return path.resolve(__dirname, dir)
}
module.exports = merge(base, {
entry: {
client: resolve('../src/client-entry.js') // 客户端打包入口文件
},
plugins: [
new VueSSRClientPlugin(), // 打包出来的是一个映射json文件,不需要写死引入client.bundle.js,因为打包出来的名字可能不是固定的,带有hash值的
// 客户端打包其实不需要html,因为用的是服务端打包出来的index.ssr.html
// 因为我们需要先预览保证客户端能跑通,所以先留着
new HtmlWebpackPlugin({
template: resolve('../public/index.html')
}),
]
})
配置webpack.server.js
const base = require('./webpack.base')
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const path = require('path')
const { node } = require('./webpack.client')
const resolve = dir => {
return path.resolve(__dirname, dir)
}
module.exports = merge(base, {
entry: {
server: resolve('../src/server-entry.js'), // server端入口文件
},
output: {
libraryTarget: 'commonjs2', // 打包出来按照module.exports方式
},
target: 'node', // 服务端打出来的文件是要给node服务用的
plugins: [
new VueSSRServerPlugin(), // 打包出来的是服务端的映射
new HtmlWebpackPlugin({
filename: 'index.ssr.html',
template: resolve('../public/index.ssr.html'),
minify: false, // 不压缩,这样打包的时候就不会把ssr的注释标记给删掉。默认打出来的文件index.html
excludeChunks: ['server'], // 排除引入文件,因为服务端引入的是客户端打包出来的文件
}),
]
})
public 模板文件
index.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>
index.ssr.html
// 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>ssr html</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
components组件
这里面暂时写了两个组件,一个写了样式,一个写了事件绑定,主要是为了验证服务端渲染是不是可以用。
// Bar.vue
<template>
<div>
bar
</div>
</template>
<style scoped>
div{
background: #f00;
}
</style>
// Foo.vue
<template>
<div @click="counter += 1">
foo{{ counter }}
</div>
</template>
<script>
export default {
data() {
return {
counter: 0
}
},
method: {
}
}
</script>
App.vue文件
// App.vue
<template>
<div id="app">
<Bar />
<Foo />
</div>
</template>
<script>
import Bar from './components/Bar.vue'
import Foo from './components/Foo.vue'
export default {
components: {
Bar,
Foo
}
}
</script>
app.js文件
这里的写法需要注意的是,返回一个函数,函数里返回app实例
,这样写是为了客户端访问服务器的时候 可以产生多个实例,这样每个实例都是独立的。之前客户端渲染不需要这样写,是因为本身每个浏览器访问都会产生不同的实例。
import Vue from 'vue'
import App from './App.vue'
// const vm = new Vue({
// el: '#app',
// render: h => h(App)
// })
// 1. 客户端渲染的时候每打开一个浏览器都会产生一个vue的实例,
// 而服务器如果按照这样的写法,会在所有人访问时都产生同样的实例,
// 所有app.js一定要导出一个函数,每次访问都产生新的实例
export default () => {
const app = new Vue({ render: h => h(App)
})
return {// 返回一个对象,后续会加入router等
app
}
}
client入口 client-entry.js
// src/client-entry.js
import createApp from './app'
const { app } = createApp()
app.$mount('#app')
server入口 server-entry.js
// src/server-entry.js
import createApp from './app'
// 服务端入口导出函数,每次请求进来返回的都是全新
export default () => {
const { app }= createApp()
return app
}
server.js 启动文件
// server.js
const Koa = require('koa')
const Router = require('@koa/router')
const { createBundleRenderer } = require('vue-server-renderer')
const fs = require('fs')
const path = require('path')
const static = require('koa-static')
const app = new Koa()
const router = new Router()
// 换一种方式:json
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const template = fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf8')
const render = createBundleRenderer(serverBundle, {
template,
clientManifest // 通过后端注入前端的js脚本
})
router.get('/', async (ctx) => {
// 在渲染页面的时候,需要让服务器根据当前路径渲染对应的路由
ctx.body = await render.renderToString()
})
app.use(router.routes())
app.use(static(path.resolve(__dirname, 'dist'))) // 静态文件查找路径
app.listen(3006)
配置打包命令 package.json
// package.json
"start": "nodemon server.js",
"client:dev": "webpack-dev-server --config ./build/webpack.client.js",
"client:build": "webpack --config ./build/webpack.client.js --watch",
"server:build": "webpack --config ./build/webpack.server.js --watch",
"build:all": "concurrently \"npm run client:build\" \"npm run server:build\""
因为服务端要引入客户端打包的文件,所以需要同时打包,可以使用concurrently
包,这个包可以同时启动多个命令,安装npm install concurrently -D
,如下命令同时启动客户端和服务端打包:
"build:all": "concurrently \"npm run client:build\" \"npm run server:build\""
启动
以上就是全部配置和代码实现,现在我们先启动npm run client:dev
,访问http://localhost:8080/
看看客户端跑通之后的效果:
可以看到客户端已经跑通了。
接下来再看看服务端,同时打包客户端和服务端代码,运行
npm run build:all
打包出来的文件如下:
现在运行server.js
看一下ssr
的效果:
npm start
,访问http://localhost:3006/
可以看到服务端也正常启动了,事件交互也没问题。
这一步基本的 ssr 实现就算通过了。
但是到这里,还是有问题的,因为服务端如果直接刷新非根路由,页面是会报 404 的,因为我们在 server.js 中只处理了/
路由,下一篇我们会加上 vue-router 和 store 来完善应用。
github:https://github.com/mxcz213/vue-ssr-demo/tree/part-two