我正在尝试创建一个 @vue/cli3 SSR 项目,但似乎无法生成服务器捆绑文件。客户端构建良好。
package.json 脚本
"scripts": {
"serve": "npm run build && node scripts/serve",
"build": "npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json",
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"
}
运行出错yarn serve
$ yarn serve
yarn run v1.9.2
$ npm run build && node scripts/serve
> project-name@0.1.0 build /path/to/file/project-name
> npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json
> project-name@0.1.0 build:server /path/to/file/project-name
> cross-env WEBPACK_TARGET=node vue-cli-service build
⠙ Building for production...(node:24514) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead
⠏ Building for production...(node:24514) UnhandledPromiseRejectionWarning: Error: Server-side bundle should have one single entry file. Avoid using CommonsChunkPlugin in the server config.
at /path/to/file/project-name/node_modules/vue-server-renderer/server-plugin.js:58:13
at _err2 (eval at create (/path/to/file/project-name/node_modules/tapable/lib/HookCodeFactory.js:24:12), <anonymous>:22:1)
at callback (/path/to/file/project-name/node_modules/copy-webpack-plugin/dist/index.js:77:17)
at /path/to/file/project-name/node_modules/copy-webpack-plugin/dist/index.js:118:24
at <anonymous>
(node:24514) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:24514) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
vue.config.js
const path = require('path')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const resolve = (file) => path.resolve(__dirname, file)
const target = TARGET_NODE
? 'server'
: 'client'
module.exports = {
configureWebpack: () => ({
entry: {
app: `./src/entry-${target}`
},
target: TARGET_NODE ? 'node' : 'web',
node: TARGET_NODE ? undefined : false,
plugins: [
TARGET_NODE
? new VueSSRServerPlugin()
: new VueSSRClientPlugin()
],
externals: TARGET_NODE ? nodeExternals({
whitelist: /\.css$/
}) : undefined,
output: {
libraryTarget: TARGET_NODE
? 'commonjs2'
: undefined
},
optimization: {
splitChunks: undefined
},
resolve:{
alias: {
'@': resolve('src'),
'public': resolve('public')
}
}
}),
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options =>
merge(options, {
optimizeSSR: false
})
)
}
}
最后,server.js 文件:
/* eslint-disable no-console */
const fs = require('fs')
const path = require('path')
const express = require('express')
const compression = require('compression')
const favicon = require('serve-favicon')
const microcache = require('route-cache')
const Ouch = require('ouch')
var proxy = require('http-proxy-middleware')
const { createBundleRenderer } = require('vue-server-renderer')
const resolve = file => path.resolve(__dirname, file)
const devServerBaseURL = process.env.DEV_SERVER_BASE_URL || 'http://localhost'
const devServerPort = process.env.DEV_SERVER_PORT || 8080
const isProd = process.env.NODE_ENV === 'production'
const useMicroCache = process.env.MICRO_CACHE !== 'false'
const serverInfo =
`express/${require('express/package.json').version} ` +
`vue-server-renderer/${require('vue-server-renderer/package.json').version}`
const app = express()
function createRenderer (bundle, options) {
return createBundleRenderer(bundle, Object.assign(options, {
runInNewContext: false
}))
}
let renderer
const templatePath = path.resolve(__dirname, './src/index.template.html')
const bundle = require('./dist/vue-ssr-server-bundle.json')
const template = fs.readFileSync(templatePath, 'utf-8')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
renderer = createRenderer(bundle, {
template,
clientManifest
})
if (process.env.NODE_ENV !== 'production') {
app.use('/js/main*', proxy({
target: `${devServerBaseURL}/${devServerPort}`,
changeOrigin: true,
pathRewrite: function (path) {
return path.includes('main')
? '/main.js'
: path
},
prependPath: false
}))
app.use('/*hot-update*', proxy({
target: `${devServerBaseURL}/${devServerPort}`,
changeOrigin: true
}))
app.use('/sockjs-node', proxy({
target: `${devServerBaseURL}/${devServerPort}`,
changeOrigin: true,
ws: true
}))
}
const serve = (path, cache) => express.static(resolve(path), {
maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
})
app.use('/js', express.static(path.resolve(__dirname, './dist/js')))
app.use('/css', express.static(path.resolve(__dirname, './dist/css')))
app.use(express.json())
app.use(compression({ threshold: 0 }))
app.use(favicon('./public/favicon.ico'))
app.use('/public/manifest.json', serve('./manifest.json', true))
app.use('/public', serve('./public', true))
app.use('/public/robots.txt', serve('./robots.txt'))
app.get('/sitemap.xml', (req, res) => {
res.setHeader('Content-Type', 'text/xml')
res.sendFile(resolve('./public/sitemap.xml'))
})
// since this app has no user-specific content, every page is micro-cacheable.
// if your app involves user-specific content, you need to implement custom
// logic to determine whether a request is cacheable based on its url and
// headers.
// 10-minute microcache.
// https://www.nginx.com/blog/benefits-of-microcaching-nginx/
const cacheMiddleware = microcache.cacheSeconds(10 * 60, req => useMicroCache && req.originalUrl)
const ouchInstance = (new Ouch()).pushHandler(new Ouch.handlers.PrettyPageHandler('orange', null, 'sublime'))
function render (req, res) {
const start = Date.now()
res.setHeader('Content-Type', 'text/html')
res.setHeader('Server', serverInfo)
const context = {
url: req.url,
res
}
const handleError = err => {
if (err.url) {
res.redirect(err.url)
} else if (err.code === 404) {
res.status(404).send('404 | Page Not Found')
} else {
ouchInstance.handleException(err, req, res, output => {
console.log(JSON.stringify(err))
console.log('Error handled!')
})
}
}
renderer.renderToString(context, (err, html) => {
if (err) return handleError(err)
res.end(html)
!isProd && console.log(`whole request: ${Date.now() - start}ms`)
})
}
app.get('*', render, cacheMiddleware)
const port = process.env.PORT || 8095
const host = process.env.HOST || '0.0.0.0'
app.listen(port, host, () => {
console.log(`server started at ${host}:${port}`)
})
似乎此错误来自 vue-server-renderer,并且与https://github.com/vuejs/vue/issues/5553有关,但它不需要提供任何关于如何解决此问题的线索。所有的 Promise 都有 catch 块,所以不应该处理任何拒绝。这仅在WEBPACK_TARGET === 'node'
使用 VueSSRServerPlugin 时才会发生。