vue-server-renderer 无法构建捆绑文件

Posted

技术标签:

【中文标题】vue-server-renderer 无法构建捆绑文件【英文标题】:vue-server-renderer not able to build bundle file 【发布时间】:2019-01-12 07:28:29 【问题描述】:

我正在尝试创建一个@vue/cli3 s-s-r 项目,但似乎无法生成服务器捆绑文件。客户端构建良好。

package.json 脚本

"scripts": 
    "serve": "npm run build && node scripts/serve",
    "build": "npm run build:server && mv dist/vue-s-s-r-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-s-s-r-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-s-s-r-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-s-s-r-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 Vues-s-rServerPlugin = require('vue-server-renderer/server-plugin')
const Vues-s-rClientPlugin = 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 Vues-s-rServerPlugin()
        : new Vues-s-rClientPlugin()
    ],
    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, 
          optimizes-s-r: 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-s-s-r-server-bundle.json')
const template = fs.readFileSync(templatePath, 'utf-8')
const clientManifest = require('./dist/vue-s-s-r-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() - startms`)
  )


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' 并因此使用 Vues-s-rServerPlugin 时。

【问题讨论】:

【参考方案1】:

我是这样解决的:

const base = require("@vue/cli-service/webpack.config");
delete base.optimization;

module.exports = merge(base, 
   ... config related to s-s-r ...
);

【讨论】:

以上是关于vue-server-renderer 无法构建捆绑文件的主要内容,如果未能解决你的问题,请参考以下文章

「WC2016」论战捆竹竿

uoj #172. WC2016论战捆竹竿

同捆不同路线

Celery 收下这捆芹菜!

全体注意!一捆来自Microsoft的红包请你查收!

luogu P4156 [WC2016]论战捆竹竿