Vue进阶之 服务端渲染(ssr)
Vue.js 服务器端渲染
项目构建目录
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
构建配置
需要安装的项目依赖 生产依赖
npm i vue vue-server-renderer express cross-env
包 | 说明 |
---|---|
vue | Vue.js 核心库 |
vue-server-renderer | Vue 服务端渲染工具 |
express | 基于 Node 的 Web 服务框架 |
cross-env | 通过 npm scripts 设置跨平台环境变量 |
安装开发依赖
npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url- loader file-loader rimraf vue-loader vue-template-compiler friendly-errors- webpack-plugin
包 | 说明 |
---|---|
webpack | webpack 核心包 |
webpack-cli | webpack 的命令行工具 |
webpack-merge | webpack 配置信息合并工具 |
webpack-node-externals | 排除 webpack 中的 Node 模块 |
rimraf | 基于 Node 封装的一个跨平台 rm -rf 工具 |
friendly-errors-webpack-plugin | 友好的 webpack 错误提示 |
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader | Babel 相关工具 |
vue-loader vue-template-compiler | 处理 .vue 资源 |
file-loader | 处理字体资源 |
css-loader | 处理 CSS 资源 |
url-loader | 处理图片资源 |
配置文件目录
build
├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件
webpack.base.config.js
/**
* 公共配置
*/
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
mode: isProd ? 'production' : 'development',
output: {
path: resolve('../dist/'),
publicPath: '/dist/',
filename: '[name].[chunkhash].js'
},
resolve: {
alias: {
// 路径别名,@ 指向 src
'@': resolve('../src/')
},
// 可以省略的扩展名
// 当省略扩展名的时候,按照从前往后的顺序依次解析
extensions: ['.js', '.vue', '.json']
},
devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
module: {
rules: [
// 处理图片资源
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
// 处理字体资源
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader',
],
},
// 处理 .vue 资源
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 处理 CSS 资源
// 它会应用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `<style>` 块
{
test: /\.css$/,
use: [
'vue-style-loader',
'css-loader'
]
},
// CSS 预处理器,参考:https://vue-loader.vuejs.org/zh/guide/pre-processors.html
// 例如处理 Less 资源
// {
// test: /\.less$/,
// use: [
// 'vue-style-loader',
// 'css-loader',
// 'less-loader'
// ]
// },
]
},
plugins: [
new VueLoaderPlugin(),
new FriendlyErrorsWebpackPlugin()
]
}
webpack.client.config.js
/**
* 客户端打包配置
*/
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
entry: {
app: './src/entry-client.js'
},
module: {
rules: [
// ES6 转 ES5
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true,
plugins: ['@babel/plugin-transform-runtime']
}
}
},
]
},
// 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
// 以便可以在之后正确注入异步 chunk。
optimization: {
splitChunks: {
name: "manifest",
minChunks: Infinity
}
},
plugins: [
// 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
webpack.server.config.js
/**
* 服务端打包配置
*/
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
// 将 entry 指向应用程序的 server entry 文件
entry: './src/entry-server.js',
// 这允许 webpack 以 Node 适用方式处理模块加载
// 并且还会在编译 Vue 组件时,
// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: 'node',
output: {
filename: 'server-bundle.js',
// 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
libraryTarget: 'commonjs2'
},
// 不打包 node_modules 第三方包,而是保留 require 方式直接加载
externals: [nodeExternals({
// 白名单中的资源依然正常打包
allowlist: [/\.css$/]
})],
plugins: [
// 这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
new VueSSRServerPlugin()
]
})
打包命令
"scripts": {
// 打包儿
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
"build": "rimraf dist && npm run build:client && npm run build:server",
// 生成启动
"start": "cross-env NODE_ENV=production node server.js",
// 开发启动
"dev": "node server.js"
}
开发模式编写;注意点
1、监听文件变化,推荐使用第三方包 chokidar
2、读写文件如果都在硬盘中会比较慢,而且好性能,所有一些中间文件就写入内存中比较好,利用webpack插件 webpack-dev-middleware 用法可查考官网;
3、热更新工具 webpack webpack-hot-middleware
项目路由配置
可参照 官方文档 VUE-SSR 路由配置
页面 head 管理建议使用 插件 vue-meta
状态管理
vuex
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export const createStore = () => {
return new Vuex.Store({
state: () => ({
posts: []
}),
mutations: {
setPosts (state, data) {
state.posts = data
}
},
actions: {
// 在服务端渲染期间务必让 action 返回一个 Promise
async getPosts ({ commit }) {
// return new Promise()
const { data } = await axios.get('/sss/sss/sss/sss/')
commit('setPosts', data.data)
}
}
})
}
页面调用时需要注意再 serverPrefetch 函数中调用,服务端专门提供的钩子函数
// 服务端预取函数,只会在服务端执行
// Vue SSR 特殊为服务端渲染提供的一个生命周期钩子函数
serverPrefetch () {
// 发起 action,返回 Promise
// this.$store.dispatch('getPosts')
return this.getPosts();
}
entry-server.js
...
await new Promise(router.onReady.bind(router))
// 服务端渲染完毕被调用,可以拿到完整数据
context.rendered = () => {
// Renderer 会把 context.state 数据对象内联到页面模板中
// 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
// 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
context.state = store.state;
}
...
entry-client.js
...
const { app, router, store } = createApp()
if(window.__INITIAL_STATE__) store.replaceState(window.__INITIAL_STATE__);
...