文章内容输出来源:拉勾教育大前端高薪训练营
官方文档: Vue.js 服务器端渲染指南
使用服务器端渲染(SSR)优势:
- 更好的SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
- 更快的内容到达时间,特别是对于缓慢的网络情况或运行缓慢的设备。
1.依赖安装
通过express创建整个项目的运行服务,及安装部分开发依赖
# vue-server-renderer版本须与vue保持一致
npm install --save vue vue-server-renderer express cross-env
npm install --save-dev webpack webpack-cli webpack-merge webpack-node-externals rimraf friendly-errors-webpack-plugin
npm install --save-dev @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader
npm install --save-dev vue-loader vue-template-compiler vue-style-loader
npm install --save-dev url-loader css-loader style-loader file-loader
npm install --save-dev chokidar
2.源码结构
.
├── app.server.js # 项目服务启动入口
├── src
│ ├── components
│ │ ├── Foo.vue
│ │ └── Bar.vue
│ ├── router
│ │ └── index.js
│ ├── store
│ │ └── index.js
│ ├── App.vue
│ ├── app.js # 通用 entry(universal entry)
│ ├── entry-client.js # 仅运行于浏览器
│ └── entry-server.js # 仅运行于服务器
3.项目配置
1. 配置结构
build
├── build.production.js # 将webpack.client.config.js、webpack.server.config.js组合到一起
├── setup-dev-server.js # 加载webpack配置读取资源
├── utils.js
├── webpack.base.config.js # 公共配置项
├── webpack.client.config.js # 客户端配置项
├── webpack.server.config.js # 服务端配置项
2. setup-dev-server.js
const fs = require('fs')
const chalk = require('chalk')
const webpack = require('webpack')
const chokidar = require('chokidar')
const ServerConfig = require('./webpack.server.config.js')
const ClientConfig = require('./webpack.client.config.js')
const utils = require('./utils.js')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')
/**
* 通过webpack-dev-middleware将资源写入内存中
*
* @param {Express} server
* @param {Function} callback
* @returns
*/
module.exports = (server, callback) => {
let serverBundle, template, clientManifest
const templatePath = utils.resolve('../index.template.html')
template = fs.readFileSync(templatePath, 'utf-8')
const notifyWrapper = (resolve, reject) => {
return (args) => {
try {
const isCompleted = Object.values(args).every(val => val)
if (isCompleted) {
resolve(callback(args))
}
} catch (error) {
reject(error)
}
}
}
return new Promise((resolve, reject) => {
const notify = notifyWrapper(resolve, reject)
// 检测模板文件变化
chokidar.watch(templatePath).on('change', (path, stats) => {
template = fs.readFileSync(templatePath, 'utf-8')
notify({ serverBundle, clientManifest, template })
})
const ServerCompiler = webpack(ServerConfig)
const ServerDevMiddleware = devMiddleware(ServerCompiler, {
logLevel: 'silent' // 关闭默认日志输出
})
ServerCompiler.hooks.done.tap('server', () => {
bundleString = ServerDevMiddleware
.fileSystem
.readFileSync(utils.resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
serverBundle = JSON.parse(bundleString)
console.log(chalk.cyan('ServerCompiler Build complete.'))
notify({ serverBundle, clientManifest, template })
})
ClientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
ClientConfig.entry.app.push('webpack-hot-middleware/client?quiet=true&reload=true')
const ClientCompiler = webpack(ClientConfig)
const ClientDevMiddleware = devMiddleware(ClientCompiler, {
logLevel: 'silent', // 关闭默认日志输出
publicPath: ClientConfig.output.publicPath
})
ClientCompiler.hooks.done.tap('client', () => {
bundleString = ClientDevMiddleware
.fileSystem
.readFileSync(utils.resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')
clientManifest = JSON.parse(bundleString)
console.log(chalk.cyan('ClientCompiler Build complete.'))
notify({ serverBundle, clientManifest, template })
})
server.use(ClientDevMiddleware)
server.use(hotMiddleware(ClientCompiler, {
log: false // 关闭默认日志输出
}))
})
}
4. 开发构建
1.依赖安装
# 其实例对象用法详见: memory-fs; 将输出的文件存在于内存中;
# webpack-dev-middleware 是一个容器(wrapper), 可以把 webpack 处理后的文件传递给一个服务器;
npm install --save-dev webpack-dev-middleware
# https://www.npmjs.com/package/webpack-hot-middleware
npm install --save-dev webpack-hot-middleware
2.开发构建
1. app.server.js
const renderer = require('vue-server-renderer').createBundleRenderer();
// 1.将Vue实例渲染为html字符串 - 渲染方式
renderer.renderToString(app, (err, html) => {});
// or
renderer.renderToString(app).then((html) => {}, (err) => {});
// 2.将Vue实例渲染为stream流 - 渲染方式
const renderStream = renderer.renderToStream(app);
// 通过订阅事件,在回调中进行操作
// event可取值'data'、'beforeStart'、'start'、'beforeEnd'、'end'、'error'等
renderStream.on(event, (res) => {});
// 接受所有请求
server.get('*', async (req, res) => {
try {
if (!isProd) await onReady
const html = await renderer.renderToString({
url: req.url,
title: 'Vue.js 服务器端',
cookies:req.cookies
})
res.set('Content-Type', 'text/html; charset=utf8;')
res.end(html)
} catch (error) {
res.sendStatus(error.code || 500)
}
})
2. entry-client.js
/**
* 文件功能描述: 客户端启动入口
*/
import { createApp } from '@/app.js'
const cookie = { token: 123 }
const { app, router, store } = createApp(cookie)
router.onReady(() => {
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中。
// 而在客户端,在挂载到应用程序之前,store 就应该获取到状态
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
app.$mount('#app')
})
3. entry-server.js
/**
* 文件功能描述: 服务端启动入口
* 使用default export导出函数,
* 并在每次渲染中重复调用此函数;
*/
import { createApp } from '@/app.js'
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp(context.cookies)
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
store.commit('initToken', context.cookies) // 将cookie信息注册到store里
context.rendered = () => {
// 我们的 store 现在已经填充入渲染应用程序所需的状态。
// 当我们将状态附加到上下文,
// 并且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML;
// 在entry-client.js中进行客户端渲染时使用;
context.state = store.state
}
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
}, reject)
})
}
5.项目开发
1.依赖安装
npm install --save vue-router # 采用vue-router的Vue的SSR渲染,必须使用history作为路由模式
npm install --save vuex
npm install --save vuex-router-sync # 通过动态注册模块将vuex与vue-router结合在一起,实现应用的路由状态管理
npm install --save cookie-parser
2.路由和代码分割
// entry-server.js entry-client.js app.server.js 依次修改
3.数据预取和状态
// vuex-router-sync Uasge -- 将vue-router的状态同步到vuex中
import { sync } from 'vuex-router-sync'
// 默认模块名: route
const unsync = sync(store, router, { moduleName: 'RouteModule' } )
// app.js 部分代码
export function createApp(cookies) {
const router = createRouter(cookies)
const store = createStore()
sync(store, router, { moduleName: 'route' } )
const app = new Vue({
router,
store,
render: h => h(App)
})
return { app, router, store }
}