# VUE-SSR
Vue 官方配置SSR的指南:
Vue.js 服务器端渲染指南 (opens new window)
Vue SSR官方Demo (opens new window)
# SSR 配置总结
通过 webpack 分别将 server bundle 和 client bundle 打包成特殊的 JSON 文件
vue-ssr-server-bundle.json
和vue-ssr-server-bundle.json
使用
vue-server-renderer
提供一个名为createBundleRenderer
的 API,处理上一步中提到的两个 JSON 文件生成 renderer最后通过
renderer.renderToString
方法生成 HTML
# 热更新
要实现热更新的条件:
1. 在服务中使用 webpack 打包我们的项目
以客户端为例:
const webpack = require('webpack')
const clientCompiler = webpack(clientConfig) // clientConfig 为客户端 webpack 配置
2. 监听文件变化后自动打包,并将 webpack 打包后的文件发送给服务
这一步需要用到 webpack-dev-middleware
,webpack-dev-middleware
执行结果返回一个 express
中间件函数,它能将 webpack 编译后的文件存储到内存中,然后在访问 express 服务时,将内存中对应的资源输出返回
const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
publicPath: clientConfig.output.publicPath,
})
app.use(devMiddleware)
3. 模块热更新
实现模块热更新需要使用到 webpack-hot-middleware
, 在 webpack 使用时需要添加以下配置:
在 plugins 中增加
HotModuleReplacementPlugin()
在 entry 中新增
webpack-hot-middleware/client
,将插件注入到程序中在 express 中加入中间件 webpack-hot-middleware
在入口文件添加
// 当前可忽略 if (module.hot) { module.hot.accept(); }
按照上面的步骤添加配置:
// 在 entry 中新增 `webpack-hot-middleware/client`,将插件注入到程序中
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
// 增加 `HotModuleReplacementPlugin()` 插件
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
)
// 加入中间件webpack-hot-middleware
app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))
4. 打包完成后,重新生成 render
重新打包也就重新生成了 vue-ssr-server-bundle.json
文件,所以我们需要监听打包完成事件,生成新的 renderer
clientCompiler.plugin('done', stats => {
stats = stats.toJson()
if (stats.errors.length) return
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
'vue-ssr-client-manifest.json'
))
update() // 生成新的render
})
TIP
上面的配置在每次更新都会重新生成新的 render ,但是除了第一次渲染需要用到以外,之后的热更新应该用不到 render , 在例子中尝试将后面的都更新都不重新生成 render 好像也没啥影响
至此客户端热更新的步骤就差不多了,接下就是服务端的,在服务端的更新就比较简单,只需要听到文件变化重新生成新的 vue-ssr-server-bundle.json
文件就可以了,直接使用 webpack 中的 watch 配置
// watch and update server renderer
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
if (stats.errors.length) return
// read bundle generated by vue-ssr-webpack-plugin
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
})
完整的配置参考,也可以看 官方Demo (opens new window)
// server.js
const path = require('path')
const fs = require('fs')
const express = require('express')
const LRU = require('lru-cache')
const app = express()
const { createBundleRenderer } = require('vue-server-renderer')
const isProd = process.env.NODE_ENV === 'production'
function createRenderer (bundle, options) {
return createBundleRenderer(bundle, Object.assign(options, {
// for component caching
cache: LRU({
max: 1000,
maxAge: 1000 * 60 * 15
}),
// this is only needed when vue-server-renderer is npm-linked todo
basedir: path.resolve('./dist'),
// recommended for performance
runInNewContext: false
}))
}
app.use(express.static('./dist'))
const templatePath = path.resolve(__dirname, './server/index.template.html')
let renderer
let readyPromise
if (isProd) {
// In production: create server renderer using template and built server bundle.
// The server bundle is generated by vue-ssr-webpack-plugin.
const template = fs.readFileSync(templatePath, 'utf-8')
const serverBundle = require('../../../dist/vue-ssr-server-bundle.json')
// The client manifests are optional, but it allows the renderer
// to automatically infer preload/prefetch links and directly add <script>
// tags for any async chunks used during render, avoiding waterfall requests.
const clientManifest = require('../../../dist/vue-ssr-client-manifest.json')
renderer = createRenderer(serverBundle, {
template,
clientManifest
})
} else {
// In development: setup the dev server with watch and hot-reload,
// and create a new renderer on bundle / index template update.
readyPromise = require('./server/setup-dev-server')(
app,
templatePath,
(bundle, options) => {
renderer = createRenderer(bundle, options)
}
)
}
function render(req, res){
const s = Date.now()
const handleError = err => {
if (err.url) {
res.redirect(err.url)
} else if(err.code === 404) {
res.status(404).send('404 | Page Not Found')
} else {
// Render Error Page or Redirect
res.status(500).send('500 | Internal Server Error')
console.error(`error during render : ${req.url}`)
console.error(err.stack)
}
}
const context = {
title: 'hellohello',
meta: '<meta charset="utf-8">',
url: req.url
}
renderer.renderToString(context, (err, html) => {
if (err) {
return handleError(err)
}
res.end(html)
if (!isProd) {
console.log(`whole request: ${Date.now() - s}ms`)
}
})
}
app.get('*', isProd ? render : (req, res) => {
readyPromise.then(() => {
return render(req, res)
})
})
const port =process.env.PORT || 8088
app.listen(port, ()=> {
console.log('启动啦啦啦啦啦啦啦', port)
})
setup-dev-server.js
const fs = require('fs')
const path = require('path')
const MFS = require('memory-fs')
const webpack = require('webpack')
const chokidar = require('chokidar')
const clientConfig = require('../../../webpack.config/ssr-vue-client')
const serverConfig = require('../../../webpack.config/ssr-vue-server')
const readFile = (fs, file) => {
try {
let fsData = fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
return fsData
} catch (e) {
console.log('readFile-e', e)
}
}
module.exports = function setupDevServer (app, templatePath, cb) {
let bundle
let template
let clientManifest
let ready
const readyPromise = new Promise(r => { ready = r })
const update = () => {
if (bundle && clientManifest) {
cb(bundle, {
template,
clientManifest
})
ready()
}
}
// read template from disk and watch
template = fs.readFileSync(templatePath, 'utf-8')
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8')
console.log('index.html template updated.')
update()
})
// modify client config to work with hot middleware
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
clientConfig.output.filename = '[name].js'
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
// new webpack.NoEmitOnErrorsPlugin() // 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段。这样可以确保输出资源不会包含错误
)
// dev middleware
const clientCompiler = webpack(clientConfig)
const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
publicPath: clientConfig.output.publicPath,
})
app.use(devMiddleware)
clientCompiler.plugin('done', stats => {
stats = stats.toJson()
stats.errors.forEach(err => console.error('stats.errors', err))
stats.warnings.forEach(err => console.warn('stats.warnings',err))
if (stats.errors.length) return
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
'vue-ssr-client-manifest.json'
))
update()
})
// hot middleware
app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))
// watch and update server renderer
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
if (stats.errors.length) return
// read bundle generated by vue-ssr-webpack-plugin
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
})
return readyPromise
}
# Q&R
在配置过程中可能会遇到的问题
运行后会提示Cannot find element: #app
在纯客户端应用程序中我们在模板中将预设好这个 div#app
元素,或者在入口函数中动态创建div#app
,但在 SSR 程序中, 需要将 div#app
元素将移到 App.vue 中
<template>
<div id="app">
<p>Hello Vue SSR</p>
</div>
</template>
(node:12612) DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks
instead
如果参考了官方的配置例子在 setup-dev-server.js
有使用 clientCompiler.plugin('done)
的形式,该提示就是这种钩子监听方式应该使用新的 Api hooks
代替,但直接使用 hooks
,又会出现内存系统访问不了目录的报错,还没找到解决文案