IT이야기

Vuex 및 라우터를 사용한vue 3 서버 측 렌더링

cyworld 2022. 7. 12. 21:47
반응형

Vuex 및 라우터를 사용한vue 3 서버 측 렌더링

Vue CLI를 사용하여 Vue3 어플리케이션을 만들고 Vuex 및 라우터를 사용하여 어플리케이션을 만듭니다.응용 프로그램이 잘 실행된다.

주의: Vue3 탑재 Vuex https://blog.logrocket.com/using-vuex-4-with-vue-3/에서 이 유용한 문서를 참조했습니다.

요건 Vue3 어플리케이션을 서버 사이드 렌더링(SSR) 지원으로 변경하고 싶습니다.

저는 Vue3 : https://www.youtube.com/watch?v=XJfaAkvLXyU을 사용하여 SSR 어플리케이션을 만드는 이 멋진 비디오를 보았습니다.동영상처럼 간단한 어플리케이션을 만들고 실행할 수 있습니다.하지만 메인 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;

또한 Express Server 파일(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를 사용하도록 이 코드를 변경해야 합니다.

문제들

express 서버 파일에서는 모듈 외부 Import로 인해 장애가 발생하여 클라이언트엔트리 파일과 같이 라우터 및 vuex를 Import할 수 없습니다.따라서 express 서버에서는 다음 작업을 수행할 수 없습니다.

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에서 앱을 가져와 Vuex 스토어에 html에서 일련화된 데이터를 미리 채우고 라우터가 준비되면 앱을 마운트합니다.

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 3 비디오를 사용한 서버 사이드 렌더링: https://www.youtube.com/watch?v=XJfaAkvLXyU&feature=youtu.be 및 git 저장소: https://github.com/moduslabs/vue3-example-ssr

  • SSR + Vuex + Router 앱 : https://github.com/shenron/vue3-example-ssr

  • Vue 2에서 Vue 3으로의 이행 https://v3.vuejs.org/guide/migration/introduction.html

  • VueRouter 3에서 VueRouter 4로의 이행 https://next.router.vuejs.org/guide/migration/

  • Vuex 3에서 Vuex 4로의 이행 https://next.vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html

클라이언트 엔트리 파일(src/main.filename)

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.html)

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.disples

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.displaces

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.module

웹 팩 빌드 항목을 지정하는 데 사용됩니다.

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/srec/index.displacy

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.disples

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 앱만
  • "added-paramed" 브랜치: "master" 브랜치를 가져와서 sr 코드를 추가했습니다.
  • "add-just-vuex" 브랜치 : "master" 브랜치를 가져와 vuex 코드를 추가했습니다.
  • "added-vuex-to-param" 브랜치: 라우터, vuex 및 sr가 있는 앱.

또한 기본 SSR 지원이 있고 웹 팩과 달리 구성 없이 즉시 작동하는 Vite를 사용할 수 있습니다.

그리고 사용하면 훨씬 더 쉬워집니다.

다음은 의 Vuex 예제의 주요 부분을 보여 줍니다.

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

언급URL : https://stackoverflow.com/questions/64899690/vue-3-server-side-rendering-with-vuex-and-router

반응형