Vue SSR 将 express 服务器与 vue 应用程序捆绑在一起,因此应用程序可以 运行 从 build dist 文件夹复制到主机服务器

Vue SSR bundling the express server with the vue app so the app can be run from the build dist folder copied to a host server

我已经创建了一个 Vue SSR 应用程序和所有 instructions/wikis/blogs 我只读过告诉你如何 运行 在开发环境中应用程序。他们不会告诉您如何在生产环境中 运行 应用程序。

我以前在 React SSR 应用程序中编写过相同的应用程序。在那个应用程序中,构建生成一个包含包“server_bundle.js”的“dist”文件夹。这个包包含 Express 服务器 (server.js) 和 React 代码。我可以使用

从“dist”文件夹中 运行 应用程序
node dist/server_bundle.js

在 Vue SSR 应用程序中,构建还会生成一个“dist”文件夹。但是,它包含一个“vue-ssr-bundle.json”文件,该文件不包含 express 服务器(server.js)。对于 运行 开发中的应用程序,我必须使用位于项目根目录中的 Express 服务器文件,而不是 运行 从“dist”目录

中获取所有内容
node ./server.js

这在开发中没问题,因为我在我的项目中工作,但在生产中这将不起作用,因为我必须 运行“dist”文件夹中的所有内容。

建筑和运行

该应用程序是使用以下命令构建的(在开发中我添加了“--watch”参数)

webpack --config webpack.server.config.js
webpack --config webpack.client.config.js

这些构建脚本创建并填充 dist(服务器)和 public(客户端)文件夹

project_root      
  - dist
      - vue-ssr-bundle.json
  - public
      - 0_client_bundle.js
      - client_bundle.js
      - any static images, such as "myImage.jpg"      

到 运行 应用程序我 运行 Express 服务器使用

node ./server.js"

看到我在项目的根目录中使用“server.js”。它不在 dist 文件夹中。

问题 那么我如何 运行 生产中的应用程序没有我的项目代码,它只有我的构建生成的“dist”和“public”文件夹?

我的代码

项目结构

project_root
  - src
      - assets
          - any static images, such as "myImage.jpg"
      - client
          - client_main.js
      - components
          - lots of files
      - pages
          - components which are top level pages
      - server 
          - server_main.js
      - vuex 
          - folders and files containing code pertaining to vuex
      - app.js
      - App.vue
      - router.js
  - index.html (the template html file to have content inserted into)
  - server.js (the Express Server file)
  - webpack.base.config.js          
  - webpack.client.config.js
  - webpack.server.config.js

src/client/client_main.js

import { createApp } from '../app'

const { app, router, store } = createApp()
... 
router.onReady(() => {
  app.$mount('#app')
})

src/server/server_main.js

import { createApp } from '../app'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()

    router.push(context.url)

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      resolve(app)
    }, reject)

  })
}

src/app.js

import Vue from 'vue'
import App from './App.vue'
import { createStore } from './vuex/store'
import { createRouter } from './router'
import { sync } from 'vuex-router-sync'

export function createApp () {
  const store = createStore()
  const router = createRouter()

  sync(store, router)

  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })

  return { app, router, store }
}

server.js

const fs = require('fs');
const express = require('express');
const { createBundleRenderer } = require('vue-server-renderer');

const bundleRenderer = createBundleRenderer(
  require('./dist/vue-ssr-bundle.json'),
  {
    template: fs.readFileSync('./index.html', 'utf-8')
  }
);

const server = express();

server.use(express.static('public'));

server.get('*', (req, res) => {
    const context = { url: req.url }  

    bundleRenderer.renderToString(context, (err, html) => {
      if (err) {
        if (err.code === 404) {
          res.status(404).end('Page not found')
        } else {
          res.status(500).end('Internal Server Error')
        }
      } else {
        res.end(html)
      }
    })
});

server.listen(8080);

webpack.base.config.js

const webpack = require('webpack')

module.exports = {

  module: {
    rules: I wont put them all here to reduce noise, but i am using 'vue-loader', 'babel-loader''file-loader'       
  },

  resolve: {
    alias: {'vue$': 'vue/dist/vue.esm.js'},
    extensions: ['*', '.js', '.vue', '.json']
  },
  
  
  performance: {
    hints: false
  },
}

webpack.client.js

var path = require('path')
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.config.js');

const config = {
  entry: './src/client/main.js',      
  output: {
    filename: 'client-bundle.js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: I wont put them all here to reduce noise, but i am using 'vue-style-loader', 'css-loader'        
  },
  devServer: {
    historyApiFallback: true,
    noInfo: true,
    overlay: true
  },
  devtool: '#eval-source-map'
}

module.exports = merge(baseConfig, config);

webpack.server.js

const path = require('path')
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.config.js');
const webpackNodeExternals = require('webpack-node-externals');
const VueSSRPlugin = require('vue-ssr-webpack-plugin')

const config = {
  target: 'node',
  entry: './src/server/main.js',
  output: { 
    filename: 'server-bundle.js',
    path: path.resolve(__dirname, './dist'),
    libraryTarget: 'commonjs2'
  },
  externals: [webpackNodeExternals()],
  devtool: '#source-map',
  plugins: [
    new VueSSRPlugin(),
  ]
}

module.exports = merge(baseConfig, config);

我试过的

我预计服务器构建会将 Express 服务器与 Vue 应用程序代码捆绑在一起,就像 React 一样,因此我只需要 运行 从 dist 文件夹中进行捆绑。我认为这是一个更好更清洁的解决方案。

我尝试将 Vue 应用程序更改为构建和 运行 像 React 应用程序,但没有成功。

移除 VueSSRPlugin 我删除了 webpack.server.config.js 中的 VueSSRPlugin 引用,并在“dist”文件中看到我现在有包和图像,就像 React 一样。然而,这个包中仍然没有 Express 服务器。我仍然不知道如何在 dist 文件夹中获取 express 服务器

将快递文件移至 src/server 文件夹 我考虑将 express 文件 (server.js) 移动到我的项目源中,希望它能被添加到包中。但是我不知道如何更改 express 文件,因为它引用了 express 服务器文件最终所在的 JSON 文件。

我已经通过将以下内容复制到我的服务器暂时解决了这个问题

  • index.html
  • server.js
  • “dist”文件夹(包含服务器包)
  • “public”文件夹(包含客户端包、样式表和其他文件)

然后在服务器上我可以运行

node server.js

因为 server.js 和 index.html 已被复制到服务器上。

我仍然更喜欢 React 的构建方式,以便 express server.js 文件和 index.html 在服务器包中,但至少这是有效的。

我使用我的 React SSR 弄清楚如何将我的所有 Vue SSR 应用程序(包括 express 服务器)捆绑到服务器包中。

这个解决方案的缺点是我不能再使用“index.html”模板和“bundleRenderer”。

这个解决方案的好处是,express 服务器现在可以使用 WebPack 配置中设置的变量。

项目结构

我的项目结构如下。我将 Express 服务器文件移动到 src/server 文件夹并删除了“index.html”文件。

project_root
 - src
      - assets
          - any static images, such as "myImage.jpg"
      - client
          - client_main.js
      - components
          - lots of files
      - server 
          - server_main.js
          - server.js (the Express Server file)
      - vuex 
          - folders and files containing code pertaining to vuex
      - app.js
      - App.vue
      - router.js
 - webpack.base.config.js          
 - webpack.client.config.js
 - webpack.server.config.js

WebPack 配置更改

"webpack.base.config.js"不再需要

devtool: '#source-map',

"webpack.server.config.js" 不再需要 "VueSSRPlugin" 因此可以删除以下内容

const VueSSRPlugin = require('vue-ssr-webpack-plugin');

plugins: [
  new VueSSRPlugin(),
],

将服务器条目更改为快速服务器文件

entry: './src/server/server.js',

服务器主文件:src/server/server_main.js

Return VUEX 商店和 Vue 应用程序。这是因为我们必须手动提取商店数据,将其序列化并将其添加到 HTML returned 到客户端。

import createApp from '../app';

export default (context) => new Promise((resolve, reject) => {
  const { app, router, store } = createApp();

  router.push(context.url);

  router.onReady(() => {
    const matchedComponents = router.getMatchedComponents();
    if (!matchedComponents.length) {
      return reject(new Error('404'));
    }

    context.rendered = () => {
      context.state = store.state;
    };

    return resolve({ app, store });
  }, reject);
});

创建一个renderer.js文件负责生成HTML到return给客户端(即替换index.html

注意:您不必为此创建一个单独的文件,如果您愿意,可以将此代码包含在您的快速服务器文件中。

import serialize from 'serialize-javascript';

export default (store, html) => {

  return `
    <html>
      <head>
        <link rel="icon" href="/favicon.png" type="image/png">
      </head>
      <body>
        <script>
          window.__INITIAL_STATE__ = ${serialize(store.state)}
        </script>         
        <div id="root">${html}</div>
        <script src="/client-bundle.js"></script>   
      </body>
    </html>
  `;
};

Express 服务器文件更改 (src/server/server.js) - 不再使用 bundleRenderer

更改 express 服务器文件以使用 vue 服务器渲染器而不是 bundle 渲染器。 调用“generateApp”(即 server_main.js 中的函数未更改),当该承诺已解决时,所有 HTML 已生成并且 VUEX 存储包含所有数据。在传入已创建的 Vue 应用程序的 vue 渲染器上调用 renderToString。在 renderToString 的回调函数中调用渲染器传递生成的 HTML 和 VUEX 存储。渲染器将​​创建 HTML 到 return 包括序列化存储。然后 HTML 被 return 发送给客户端。

import generateApp from './server_main.js';
import renderer from './renderer';

const express = require('express');
const vueServerRenderer = require('vue-server-renderer').createRenderer();

const server = express();

server.use(express.static('public'));

server.get('*', (req, res) => {
  const context = { url: req.url };

  generateApp(context).then((createdAppObj) => {
    const { app, store } = createdAppObj;

    vueServerRenderer.renderToString(
      app,
      (err, html) => {
        if (err) {
          if (err.code === 404) {
            res.status(404).end('Page not found');
          } else {
            res.status(500).end('Internal Server Error');
          }
        } else {
          // BUILD UP THE HTML TO RETURN
          const content = renderer(store, html);
          res.send(content);
        }
      },
    );
  });
});

server.listen(8080);

package.json变

现在,当所有内容构建完成后,“dist”目录将包含以下内容

- server-bundle.js

这包含 express 服务器文件作为入口文件,因此要 运行 您执行的应用程序

node ./server.js

此解决方案的一个优势:Express Server 可以使用 WebPack 配置中设置的变量

Vue 支持使用模板插值将变量添加到“index.html”模板中,请参阅https://ssr.vuejs.org/guide/#using-a-page-template

但是在我的例子中,我需要在我的 WebPack 配置中使用一个变量设置来放入“index.html”。

我的 WebPack 配置设置来自“.env”文件的变量,然后在我的 Vue 应用程序中使用。
我需要在我的“index.html”中的这个“.env”文件中使用一个变量。由于 Express Server 未被 WebPack 解析和捆绑,因此不会设置变量。我将不得不阅读快速服务器文件中的“.env”文件。这意味着我还必须在 express 服务器文件所在的生产环境中拥有“.env”文件,以便从中读取。

通过此评论中描述的更改,WebPack 现在捆绑了 express 服务器文件,因此 express 服务器文件可以访问 WebPack 中设置的所有变量,因此它将可以访问从“.env”设置的变量