我已经使用Vue CLI创建了一个Vue3应用程序,用Vuex和路由创建了我的应用程序.应用程序运行良好.

Note: I followed this useful doc for the Vuex with Vue3 100

Requirement现在,我想将我的Vue3应用程序更改为具有服务器端渲染支持(即SSR).

我观看了这段关于使用Vue3:https://www.youtube.com/watch?v=XJfaAkvLXyU创建SSR应用程序的精彩视频,我可以创建并运行视频中的简单应用程序.然而,当我try 将其应用到我的主Vue3应用程序时,我被卡住了.

我目前的难点是如何在服务器代码上指定路由和vuex.

My Code

客户端条目文件(src/main.js)有以下内容

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

createApp(App).use(store).use(router).mount('#app');

服务器条目文件(src/main.server.js)当前包含以下内容

import App from './App.vue';
export default App;

在express服务器文件(src/server.js)中,它当前有

const path = require('path');
const express = require('express');
const { createSSRApp } = require('vue');
const { renderToString } = require('@vue/server-renderer');

...
...

server.get('*', async (req, res) => {
  const app = createSSRApp(App);
  const appContent = await renderToString(app);

我需要更改此代码,以便服务器端的应用程序像在客户端一样使用路由和vuex.

Issues

在express server文件中,我无法在客户端条目文件中导入路由和vuex,因为它由于在模块外部导入而失败,因此在express server中,我无法执行以下操作:

const app = createSSRApp(App).use(store).use(router);

我try 将服务器条目文件(src/main.server.js)更改为以下内容,但这也不起作用.

import App from './App.vue';
import router from './router';
import store from './store';

const { createSSRApp } = require('vue');

export default createSSRApp(App).use(store).use(router);

Does anyone know how to do SSR in Vue 3 when your app is using Vuex and Router.

How i did this in Vue 2 is below and what i am trying to change over to Vue 3

此应用程序的Vue2版本包含以下代码

src/app.js使用指定的路由和存储创建Vue组件

客户端条目文件(src/Client/main.js)从应用程序获取应用程序.js,用html中序列化的数据预填充Vuex存储,在路由准备就绪时装载应用程序

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

export default 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 };
}

服务器条目文件(src/Server/main.js)从应用程序获取应用程序.js,获取匹配的路由,该路由将对每个组件调用"serverPrefetch",以在Vuex存储中填充其数据,然后返回解析promise

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);
  }, reject);
});

Express server(/server.js)使用bundle呈现程序将应用程序呈现为字符串,以放入html中

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

dotenv.config();

const bundleRenderer = createBundleRenderer(
  require('./dist/vue-ssr-server-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,
  clientBundle: `client-bundle.js`,
  };

  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);
  }
  });
});

const port = process.env.PORT || 3000
server.listen(port, () => {
  console.log(`Listening on port ${port}`);
});

推荐答案

由于以下资源,我成功地找到了解决方案:

client entry file (src/main.js)

import buildApp from './app';

const { app, router, store } = buildApp();

const storeInitialState = window.INITIAL_DATA;
if (storeInitialState) {
  store.replaceState(storeInitialState);
}

router.isReady()
  .then(() => {
    app.mount('#app', true);
  });

server entry file (src/main-server.js)

import buildApp from './app';

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

  // set server-side router's location
  router.push(url);

  router.isReady()
    .then(() => {
      const matchedComponents = router.currentRoute.value.matched;
      // no matched routes, reject with 404
      if (!matchedComponents.length) {
        return reject(new Error('404'));
      }

      // the Promise should resolve to the app instance so it can be rendered
      return resolve({ app, router, store });
    }).catch(() => reject);
});

src/app.js

import { createSSRApp, createApp } from 'vue';
import App from './App.vue';

import router from './router';
import store from './store';

const isSSR = typeof window === 'undefined';

export default function buildApp() {
  const app = (isSSR ? createSSRApp(App) : createApp(App));

  app.use(router);
  app.use(store);

  return { app, router, store };
}

server.js

const serialize = require('serialize-javascript');
const path = require('path');
const express = require('express');
const fs = require('fs');
const { renderToString } = require('@vue/server-renderer');
const manifest = require('./dist/server/ssr-manifest.json');

// Create the express app.
const server = express();

// we do not know the name of app.js as when its built it has a hash name
// the manifest file contains the mapping of "app.js" to the hash file which was created
// therefore get the value from the manifest file thats located in the "dist" directory
// and use it to get the Vue App
const appPath = path.join(__dirname, './dist', 'server', manifest['app.js']);
const createApp = require(appPath).default;

const clientDistPath = './dist/client';
server.use('/img', express.static(path.join(__dirname, clientDistPath, 'img')));
server.use('/js', express.static(path.join(__dirname, clientDistPath, 'js')));
server.use('/css', express.static(path.join(__dirname, clientDistPath, 'css')));
server.use('/favicon.ico', express.static(path.join(__dirname, clientDistPath, 'favicon.ico')));

// handle all routes in our application
server.get('*', async (req, res) => {
  const { app, store } = await createApp(req);

  let appContent = await renderToString(app);

  const renderState = `
    <script>
      window.INITIAL_DATA = ${serialize(store.state)}
    </script>`;

  fs.readFile(path.join(__dirname, clientDistPath, 'index.html'), (err, html) => {
    if (err) {
      throw err;
    }

    appContent = `<div id="app">${appContent}</div>`;

    html = html.toString().replace('<div id="app"></div>', `${renderState}${appContent}`);
    res.setHeader('Content-Type', 'text/html');
    res.send(html);
  });
});

const port = process.env.PORT || 8080;
server.listen(port, () => {
  console.log(`You can navigate to http://localhost:${port}`);
});

vue.config.js

用于指定网页包构建内容

const ManifestPlugin = require('webpack-manifest-plugin');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  devServer: {
    overlay: {
      warnings: false,
      errors: false,
    },
  },
  chainWebpack: (webpackConfig) => {
    webpackConfig.module.rule('vue').uses.delete('cache-loader');
    webpackConfig.module.rule('js').uses.delete('cache-loader');
    webpackConfig.module.rule('ts').uses.delete('cache-loader');
    webpackConfig.module.rule('tsx').uses.delete('cache-loader');

    if (!process.env.SSR) {
      // This is required for repl.it to play nicely with the Dev Server
      webpackConfig.devServer.disableHostCheck(true);

      webpackConfig.entry('app').clear().add('./src/main.js');
      return;
    }

    webpackConfig.entry('app').clear().add('./src/main-server.js');

    webpackConfig.target('node');
    webpackConfig.output.libraryTarget('commonjs2');

    webpackConfig.plugin('manifest').use(new ManifestPlugin({ fileName: 'ssr-manifest.json' }));

    webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ }));

    webpackConfig.optimization.splitChunks(false).minimize(false);

    webpackConfig.plugins.delete('hmr');
    webpackConfig.plugins.delete('preload');
    webpackConfig.plugins.delete('prefetch');
    webpackConfig.plugins.delete('progress');
    webpackConfig.plugins.delete('friendly-errors');

    // console.log(webpackConfig.toConfig())
  },
};

src/router/index.js

import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

const isServer = typeof window === 'undefined';
const history = isServer ? createMemoryHistory() : createWebHistory();
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    component: About,
  },
];

const router = createRouter({
  history,
  routes,
});

export default router;

src/store/index.js

import Vuex from 'vuex';
import fetchAllBeers from '../data/data';

export default Vuex.createStore({
  state() {
    return {
      homePageData: [],
    };
  },

  actions: {
    fetchHomePageData({ commit }) {
      return fetchAllBeers()
        .then((data) => {
          commit('setHomePageData', data.beers);
        });
    },
  },

  mutations: {
    setHomePageData(state, data) {
      state.homePageData = data;
    },
  },

});

Github sample code

我发现我需要一步一步地构建代码,只做SSR,只做路由,只做Vuex,然后把它们放在一起.

我的测试应用程序在github中

https://github.com/se22as/vue-3-with-router-basic-sample

  • "主"分支:只是一个带有路由的vue 3应用程序
  • "添加ssr"分支:获取"主"分支并添加ssr代码
  • "只添加vuex"分支:获取"主"分支并添加vuex代码
  • "将vuex添加到ssr"分支:带有路由、vuex和ssr的应用程序.

Vue.js相关问答推荐

VueI18n 无法读取或更改组合 API 中的区域设置

当用户在嵌套路由中时激活链接

将媒体源/流绑定到视频元素 - Vue 3 组合 API

为什么我在 Vue npm run serve 的 localhost 中得到无法获取

拖入 vue2-google-map 后如何获取标记位置?

将props传递给设置时的VueJS 3组合API和TypeScript类型问题:类型上不存在属性 user用户

如何使用 v-on:change 将输入文本传递给我的 vue 方法?

动态插入字符串上的 Vue 事件处理程序不起作用

如何在 vue.js 构建中重命名 index.html?

Vue:如何将 store 与组件一起使用?

在按键 vuejs 中只允许数字和一个小数点后 2 位限制

在 vue.js 中获取当前时间和日期

单击复选框后,V-model 未更新

所有 vue props和数据都给出错误 Property属性在 type 上不存在,带有 typescript

使用 Vue 在单个文件组件中调用渲染方法

如何使用vue.js/nuxt.js获取一个目录下的所有图片文件

更改事件上的 Select2 在 Vuejs 中不起作用

仅在提交时验证 vuetify 文本字段

使用 CloudFront 部署在 S3 上的 VueJS 应用程序的指定的密钥不存在

使用 vuex-persistedstate 仅使一个模块持久化