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를 사용할 수 있습니다.
그리고 사용하면 훨씬 더 쉬워집니다.
<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
'IT이야기' 카테고리의 다른 글
운영체제는 어떤 종류의 C로 기술되어 있습니까? (0) | 2022.07.19 |
---|---|
Java Array 처음에 요소를 추가하는 방법 목록 (0) | 2022.07.12 |
플레인 VueJS 프로젝트에서 Typescript 함수 사용 (0) | 2022.07.12 |
vuex: 알 수 없는 getter: 기사 (0) | 2022.07.12 |
Java List.contains(필드 값이 x와 동일한 개체) (0) | 2022.07.12 |