You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Vite-Vue SSR项目中正确使用Vue Router与Vuex?

在Vite官方Vue 3 SSR模板中集成Vuex与Vue Router的正确方式

我正在学习Vue SSR,基于Vite文档推荐的官方Vue 3 SSR启动模板重写项目,现在想引入Vuex,单独创建store.js管理状态,但模板里没有相关文件。我找了一些指南和示例,但和当前模板不匹配,刚接触SSR,对语法和插件差异感到困惑,想请教如何正确同时使用Vuex和Vue Router。

我已经修改了main.js、entry-server.js、entry-client.js并新增了store.js,但不确定是否正确,希望帮忙修正这些文件,让两者能正常工作。


解决方案步骤

1. 安装Vuex依赖

首先确保安装适配Vue 3的Vuex 4.x版本:

npm install vuex@next
# 或
yarn add vuex@next

2. 修正store.js

调整导入和导出方式,保持与router.js一致的简洁风格:

import { createStore } from 'vuex'

export function createStore() {
  return createStore({
    state() {
      return {
        todoList: []
      }
    },
    actions: {
      fetchTodoList({ commit }) {
        // 实际项目中替换为异步请求后端数据
        const todoList = [
          { id: 0, text: 'Buy milk' },
          { id: 1, text: 'Buy chocolate' }
        ]
        commit('setTodoList', todoList)
      }
    },
    mutations: {
      setTodoList(state, todoList) {
        state.todoList = todoList
      }
    }
  })
}

3. 完善main.js

你的代码已经正确创建并挂载store,无需修改,保留即可:

import { createSSRApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

export function createApp() {
  const app = createSSRApp(App)
  const router = createRouter()
  const store = createStore()
  
  app.use(router)
  app.use(store)
  
  return { app, router, store }
}

4. 修改entry-server.js(核心:服务端数据预取)

SSR关键是在服务端预取数据并将状态传递给客户端,避免 hydration 不匹配:

import { basename } from 'node:path'
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'

export async function render(url, manifest) {
  const { app, router, store } = createApp()

  await router.push(url)
  await router.isReady()

  // 预取当前路由组件的数据
  const matchedComponents = router.currentRoute.value.matched.flatMap(record => Object.values(record.components))
  await Promise.all(
    matchedComponents.map(component => {
      if (component.asyncData) {
        return component.asyncData({ store, route: router.currentRoute.value })
      }
    })
  )

  const ctx = {
    state: store.state // 将store状态存入上下文
  }
  const html = await renderToString(app, ctx)

  const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
  return { html, preloadLinks, state: store.state }
}

// 以下两个函数保持不变
function renderPreloadLinks(modules, manifest) {
  let links = ''
  const seen = new Set()
  modules.forEach((id) => {
    const files = manifest[id]
    if (files) {
      files.forEach((file) => {
        if (!seen.has(file)) {
          seen.add(file)
          const filename = basename(file)
          if (manifest[filename]) {
            for (const depFile of manifest[filename]) {
              links += renderPreloadLink(depFile)
              seen.add(depFile)
            }
          }
          links += renderPreloadLink(file)
        }
      })
    }
  })
  return links
}

function renderPreloadLink(file) {
  if (file.endsWith('.js')) {
    return `<link rel="modulepreload" crossorigin href="${file}">`
  } else if (file.endsWith('.css')) {
    return `<link rel="stylesheet" href="${file}">`
  } else if (file.endsWith('.woff')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
  } else if (file.endsWith('.woff2')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
  } else if (file.endsWith('.gif')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/gif">`
  } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">`
  } else if (file.endsWith('.png')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/png">`
  } else {
    return ''
  }
}

5. 修改entry-client.js(恢复服务端状态)

客户端需要接收服务端注入的状态,初始化store时同步:

import { createApp } from './main'

const { app, router, store } = createApp()

// 同步服务端传递的初始状态
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

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

6. 修改服务端入口(如server.js)注入状态

在服务端生成的HTML中注入store状态,供客户端使用:

// 以Express服务器为例
app.get('*', async (req, res) => {
  try {
    const url = req.originalUrl
    const { html, preloadLinks, state } = await render(url, manifest)

    res.send(`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>Vue SSR App</title>
        ${preloadLinks}
        <!-- 注入初始状态,避免XSS攻击 -->
        <script>window.__INITIAL_STATE__ = ${JSON.stringify(state).replace(/</g, '\\u003c')}</script>
      </head>
      <body>
        <div id="app">${html}</div>
        <script type="module" src="/src/entry-client.js"></script>
      </body>
    </html>
    `)
  } catch (e) {
    console.error(e)
    res.status(500).send('Internal Server Error')
  }
})

7. 组件中使用示例

在页面组件中定义asyncData方法实现服务端预取:

<template>
  <div>
    <h1>Todo List</h1>
    <ul>
      <li v-for="todo in todoList" :key="todo.id">{{ todo.text }}</li>
    </ul>
  </div>
</template>

<script>
import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  // 服务端预取数据的钩子
  asyncData({ store }) {
    return store.dispatch('fetchTodoList')
  },
  setup() {
    const store = useStore()
    const todoList = computed(() => store.state.todoList)
    return { todoList }
  }
}
</script>

内容的提问来源于stack exchange,提问作者hamid-davodi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 09:15:30