【问题标题】:vue 3 Server Side Rendering with Vuex and Routervue 3 使用 Vuex 和路由器的服务器端渲染
【发布时间】:2023-12-08 16:01:02
【问题描述】:

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

注意:我在 Vue3 https://blog.logrocket.com/using-vuex-4-with-vue-3/987654321@

要求现在我想更改我的 Vue3 应用程序以支持服务器端渲染(即 SSR)。

我观看了关于使用 Vue3 创建 SSR 应用程序的精彩视频:https://www.youtube.com/watch?v=XJfaAkvLXyU,我可以创建和运行视频中的简单应用程序。但是,当我尝试将它应用到我的主 Vue3 应用程序时,我被卡住了。

我目前的症结是如何在服务器代码上指定路由器和vuex。

我的代码

客户端入口文件(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;

并且在它当前拥有的快速服务器文件 (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。

问题

在快速服务器文件中,我无法像在客户端条目文件中那样导入路由器和 vuex,因为它由于在模块外部导入而失败,因此在快速服务器中我无法执行以下操作

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

我尝试将服务器入口文件(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);

当您的应用使用 Vuex 和 Router 时,有谁知道如何在 Vue 3 中进行 SSR。

我是如何在 Vue 2 中做到这一点的,以及我想要转换到 Vue 3 的方式

我的这个应用程序的 Vue2 版本有以下代码

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

客户端入口文件 (src/client/main.js) 从 app.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),从 app.js 获取应用程序,获取匹配的路由,该路由将在每个组件上调用“serverPrefetch”以将其数据填充到 Vuex 存储中,然后返回解决承诺

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.js) 使用捆绑渲染器将应用程序渲染为一个字符串以放入 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}`);
});

【问题讨论】:

    标签: vue.js vuex router vuejs3


    【解决方案1】:

    借助以下资源,我设法找到了解决方案:

    客户端入口文件 (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);
      });
    

    服务器入口文件 (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

    用于指定webpack构建的东西

    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 示例代码

    我发现我需要逐步构建代码,只做 SSR、Router、Vuex,然后将它们放在一起。

    我的测试应用在 github

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

    • “master”分支:只是一个带有路由器的 vue 3 应用
    • "add-ssr" 分支:采用"master" 分支并添加ssr 代码
    • "add-just-vuex" 分支:采用"master" 分支并添加 vuex 代码
    • “add-vuex-to-ssr”分支:带有路由器、vuex 和 ssr 的应用程序。

    【讨论】:

    • 为了使其正常工作,还需要使用 createSSRApp 创建客户端应用程序。否则,您现有的标记将不会被水合,而只会被新呈现的标记替换。
    • 服务器应该从app.jscreateSSRApp() 而不是App.vue。看起来createSSRApp(App) 正在App.vue 上运行
    • 第一卷 = 我在 app.js 中调用“createSSRApp()”。我的 App.vue 代码只是用 标签指定了我的布局。
    • @UliKrause 你有这方面的更多信息吗?我现在是如何拥有它的,当我运行我的应用程序并查看源代码时,我会看到所有标记。但是我确实看到了对我的数据服务器的调用,这不应该发生在客户端上,因为数据应该是在服务器上获取的。
    • @volumeone 要构建服务器代码,您将使用以下vue-cli-service build --dest dist/server 这将构建代码放在dist\server 并构建客户端我使用vue-cli-service build --dest dist/client,这将构建代码放在@ 987654343@。当您运行服务器应用程序node server.js 时,将使用dist\server 中的服务器代码并使用它。从服务器返回给客户端的 HTML 将包含对 dist\client 中客户端代码的引用,这是客户端获取客户端包的方式。
    【解决方案2】:

    您还可以使用具有原生 SSR 支持的 Vite,并且与 Webpack 不同,它无需配置即可开箱即用。

    如果你使用vite-plugin-ssr,那就更简单了。

    以下重点突出vite-plugin-ssr's Vuex example的主要部分

    <template>
      <h1>To-do List</h1>
      <ul>
        <li v-for="item in todoList" :key="item.id">{{item.text}}</li>
      </ul>
    </template>
    
    <script>
    export default {
      serverPrefetch() {
        return this.$store.dispatch('fetchTodoList');
      },
      computed: {
        todoList () {
          return this.$store.state.todoList
        }
      },
    }
    </script>
    
    import Vuex from 'vuex'
    
    export { createStore }
    
    function createStore() {
      const store = Vuex.createStore({
        state() {
          return {
            todoList: []
          }
        },
    
        actions: {
          fetchTodoList({ commit }) {
            const todoList = [
              {
                id: 0,
                text: 'Buy milk'
              },
              {
                id: 1,
                text: 'Buy chocolate'
              }
            ]
            return commit('setTodoList', todoList)
          }
        },
    
        mutations: {
          setTodoList(state, todoList) {
            state.todoList = todoList
          }
        }
      })
    
      return store
    }
    
    import { createSSRApp, h } from 'vue'
    import { createStore } from './store'
    
    export { createApp }
    
    function createApp({ Page }) {
      const app = createSSRApp({
        render: () => h(Page)
      })
      const store = createStore()
      app.use(store)
      return { app, store }
    }
    
    import { renderToString } from '@vue/server-renderer'
    import { html } from 'vite-plugin-ssr'
    import { createApp } from './app'
    
    export { render }
    export { addContextProps }
    export { setPageProps }
    
    async function render({ contextProps }) {
      const { appHtml } = contextProps
      return html`<!DOCTYPE html>
        <html>
          <body>
            <div id="app">${html.dangerouslySetHtml(appHtml)}</div>
          </body>
        </html>`
    }
    
    async function addContextProps({ Page }) {
      const { app, store } = createApp({ Page })
    
      const appHtml = await renderToString(app)
    
      const INITIAL_STATE = store.state
    
      return {
        INITIAL_STATE,
        appHtml
      }
    }
    
    function setPageProps({ contextProps }) {
      const { INITIAL_STATE } = contextProps
      return { INITIAL_STATE }
    }
    
    import { getPage } from 'vite-plugin-ssr/client'
    import { createApp } from './app'
    
    hydrate()
    
    async function hydrate() {
      const { Page, pageProps } = await getPage()
      const { app, store } = createApp({ Page })
      store.replaceState(pageProps.INITIAL_STATE)
      app.mount('#app')
    }
    

    【讨论】:

      最近更新 更多