{T}

项目结构

推荐结构

基础结构

适用于中小型项目:

code
src/
├── store/
│   ├── index.js              # store 入口文件
│   ├── state.js              # 根状态
│   ├── mutations.js          # 根 mutations
│   ├── actions.js            # 根 actions
│   ├── getters.js            # 根 getters
│   └── modules/              # 模块目录
│       ├── user.js           # 用户模块
│       ├── cart.js           # 购物车模块
│       └── products.js       # 商品模块
├── App.vue
└── main.js

标准结构

适用于中大型项目:

code
src/
├── store/
│   ├── index.js                    # store 入口文件,组装模块并导出 store
│   ├── mutation-types.js           # Mutation 类型常量
│   ├── state.js                    # 根状态
│   ├── mutations.js                # 根 mutations
│   ├── actions.js                  # 根 actions
│   ├── getters.js                  # 根 getters
│   ├── modules/                    # 模块目录
│   │   ├── index.js               # 模块统一导出
│   │   ├── user/                  # 用户模块
│   │   │   ├── index.js           # 模块入口
│   │   │   ├── state.js           # 模块状态
│   │   │   ├── mutations.js       # 模块 mutations
│   │   │   ├── actions.js         # 模块 actions
│   │   │   └── getters.js         # 模块 getters
│   │   ├── cart/                  # 购物车模块
│   │   │   └── ...
│   │   └── products/              # 商品模块
│   │       └── ...
│   └── plugins/                    # 插件目录
│       ├── persist.js             # 持久化插件
│       └── logger.js              # 日志插件
├── api/                            # API 接口
│   ├── user.js
│   ├── cart.js
│   └── products.js
├── utils/                          # 工具函数
├── App.vue
└── main.js

大型项目结构

适用于企业级大型项目:

code
src/
├── store/
│   ├── index.js                    # store 入口
│   ├── mutation-types.js           # Mutation 类型常量
│   ├── rootState.js                # 根状态
│   ├── rootMutations.js            # 根 mutations
│   ├── rootActions.js              # 根 actions
│   ├── rootGetters.js              # 根 getters
│   │
│   ├── modules/                    # 业务模块
│   │   ├── index.js               # 模块导出
│   │   │
│   │   ├── user/                  # 用户模块
│   │   │   ├── index.js           # 模块入口
│   │   │   ├── state.js
│   │   │   ├── mutations.js
│   │   │   ├── actions.js
│   │   │   ├── getters.js
│   │   │   └── types.js           # 模块私有类型
│   │   │
│   │   ├── cart/                  # 购物车模块
│   │   │   └── ...
│   │   │
│   │   ├── products/              # 商品模块
│   │   │   └── ...
│   │   │
│   │   └── common/                # 公共模块
│   │       ├── app.js             # 应用状态
│   │       ├── error.js           # 错误状态
│   │       └── loading.js         # 加载状态
│   │
│   ├── plugins/                    # 插件
│   │   ├── index.js               # 插件导出
│   │   ├── persist.js             # 持久化
│   │   ├── logger.js              # 日志
│   │   └── error.js               # 错误处理
│   │
│   └── utils/                      # store 工具函数
│       ├── storage.js             # 存储工具
│       └── helpers.js             # 辅助函数
│
├── api/                            # API 接口
│   ├── index.js                   # API 导出
│   ├── request.js                 # 请求封装
│   ├── user.js
│   ├── cart.js
│   └── products.js
│
├── types/                          # TypeScript 类型定义
│   ├── user.d.ts
│   ├── cart.d.ts
│   └── products.d.ts
│
├── App.vue
└── main.js

文件组织

入口文件 (index.js)

javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import state from './state'
import mutations from './mutations'
import modules from './modules'
import plugins from './plugins'

Vue.use(Vuex)

const store = new Vuex.Store({
  state,
  mutations,
  actions,
  getters,
  modules,
  plugins,
  strict: process.env.NODE_ENV !== 'production'
})

export default store

// 热重载
if (module.hot) {
  module.hot.accept(['./state', './mutations', './actions', './getters', './modules'], () => {
    const newState = require('./state').default
    const newMutations = require('./mutations').default
    const newActions = require('./actions').default
    const newGetters = require('./getters').default
    const newModules = require('./modules').default
    
    store.hotUpdate({
      state: newState,
      mutations: newMutations,
      actions: newActions,
      getters: newGetters,
      modules: newModules
    })
  })
}

Mutation 类型常量 (mutation-types.js)

javascript
// store/mutation-types.js

// 用户相关
export const SET_USER = 'SET_USER'
export const SET_TOKEN = 'SET_TOKEN'
export const CLEAR_USER = 'CLEAR_USER'

// 购物车相关
export const ADD_TO_CART = 'ADD_TO_CART'
export const REMOVE_FROM_CART = 'REMOVE_FROM_CART'
export const UPDATE_CART_QUANTITY = 'UPDATE_CART_QUANTITY'
export const CLEAR_CART = 'CLEAR_CART'

// 商品相关
export const SET_PRODUCTS = 'SET_PRODUCTS'
export const SET_CURRENT_PRODUCT = 'SET_CURRENT_PRODUCT'

// 应用状态
export const SET_LOADING = 'SET_LOADING'
export const SET_ERROR = 'SET_ERROR'

State 文件

javascript
// store/state.js
export default {
  // 应用全局状态
  loading: false,
  error: null,
  
  // 用户信息
  user: null,
  token: null,
  
  // 其他根状态
  sidebarCollapsed: false,
  theme: 'light'
}

Mutations 文件

javascript
// store/mutations.js
import * as types from './mutation-types'

export default {
  [types.SET_LOADING](state, loading) {
    state.loading = loading
  },
  
  [types.SET_ERROR](state, error) {
    state.error = error
  },
  
  [types.SET_USER](state, user) {
    state.user = user
  },
  
  [types.SET_TOKEN](state, token) {
    state.token = token
  },
  
  [types.CLEAR_USER](state) {
    state.user = null
    state.token = null
  }
}

Actions 文件

javascript
// store/actions.js
import * as types from './mutation-types'
import api from '@/api'

export default {
  // 全局加载状态
  setLoading({ commit }, loading) {
    commit(types.SET_LOADING, loading)
  },
  
  // 全局错误处理
  setError({ commit }, error) {
    commit(types.SET_ERROR, error)
  },
  
  // 用户登录
  async login({ commit, dispatch }, credentials) {
    dispatch('setLoading', true)
    try {
      const response = await api.user.login(credentials)
      commit(types.SET_TOKEN, response.token)
      commit(types.SET_USER, response.user)
      return response
    } catch (error) {
      dispatch('setError', error.message)
      throw error
    } finally {
      dispatch('setLoading', false)
    }
  },
  
  // 用户登出
  async logout({ commit }) {
    await api.user.logout()
    commit(types.CLEAR_USER)
  }
}

Getters 文件

javascript
// store/getters.js
export default {
  // 用户相关
  isLoggedIn: state => !!state.token,
  userName: state => state.user?.name || '未登录',
  userAvatar: state => state.user?.avatar || '/default-avatar.png',
  
  // 应用状态
  isLoading: state => state.loading,
  hasError: state => !!state.error,
  errorMessage: state => state.error?.message || '',
  
  // 主题
  isDarkTheme: state => state.theme === 'dark'
}

模块导出文件

javascript
// store/modules/index.js
import user from './user'
import cart from './cart'
import products from './products'
import common from './common'

export default {
  user,
  cart,
  products,
  common
}

单个模块文件

javascript
// store/modules/user/index.js
import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}
javascript
// store/modules/user/state.js
export default {
  userInfo: null,
  token: localStorage.getItem('token') || null,
  preferences: {
    theme: 'light',
    language: 'zh-CN'
  }
}
javascript
// store/modules/user/mutations.js
import { SET_USER, SET_TOKEN, CLEAR_USER } from '@/store/mutation-types'

export default {
  [SET_USER](state, user) {
    state.userInfo = user
  },
  
  [SET_TOKEN](state, token) {
    state.token = token
    if (token) {
      localStorage.setItem('token', token)
    } else {
      localStorage.removeItem('token')
    }
  },
  
  [CLEAR_USER](state) {
    state.userInfo = null
    state.token = null
    localStorage.removeItem('token')
  }
}

插件组织

持久化插件

javascript
// store/plugins/persist.js
const STORAGE_KEY = 'vuex-store'

export default function createPersistPlugin(options = {}) {
  const { key = STORAGE_KEY, paths = [] } = options
  
  return store => {
    // 初始化时从存储中恢复状态
    const savedState = localStorage.getItem(key)
    if (savedState) {
      try {
        const parsed = JSON.parse(savedState)
        store.replaceState({
          ...store.state,
          ...parsed
        })
      } catch (e) {
        console.error('Failed to parse persisted state:', e)
      }
    }
    
    // 订阅状态变化,保存到存储
    store.subscribe((mutation, state) => {
      try {
        let stateToPersist = state
        
        // 如果指定了路径,只保存指定路径的状态
        if (paths.length > 0) {
          stateToPersist = paths.reduce((acc, path) => {
            const keys = path.split('.')
            let value = state
            for (const key of keys) {
              value = value[key]
            }
            acc[path] = value
            return acc
          }, {})
        }
        
        localStorage.setItem(key, JSON.stringify(stateToPersist))
      } catch (e) {
        console.error('Failed to persist state:', e)
      }
    })
  }
}

日志插件

javascript
// store/plugins/logger.js
export default function createLoggerPlugin(options = {}) {
  const { collapsed = true, filter = () => true } = options
  
  return store => {
    store.subscribe((mutation, state) => {
      if (!filter(mutation, state)) return
      
      const groupMethod = collapsed ? console.groupCollapsed : console.group
      
      groupMethod(`[Vuex] ${mutation.type}`)
      console.log('Payload:', mutation.payload)
      console.log('State:', state)
      console.groupEnd()
    })
  }
}

使用插件

javascript
// store/plugins/index.js
import createPersistPlugin from './persist'
import createLoggerPlugin from './logger'

const plugins = []

// 持久化插件
plugins.push(createPersistPlugin({
  key: 'my-app-store',
  paths: ['user.token', 'user.preferences']
}))

// 日志插件(仅开发环境)
if (process.env.NODE_ENV === 'development') {
  plugins.push(createLoggerPlugin())
}

export default plugins

命名规范

文件命名

类型命名规范示例
目录小写,多个单词用连字符user-profile/
模块文件小写user.js
类型文件小写,连字符mutation-types.js
插件文件小写persist.js

Mutation 命名

javascript
// 使用大写蛇形命名
export const SET_USER = 'SET_USER'
export const ADD_TO_CART = 'ADD_TO_CART'
export const UPDATE_CART_QUANTITY = 'UPDATE_CART_QUANTITY'

// 动词 + 名词
SET_USER      // 设置用户
ADD_ITEM      // 添加项目
REMOVE_ITEM   // 移除项目
UPDATE_DATA   // 更新数据
RESET_STATE   // 重置状态

Action 命名

javascript
// 使用小驼峰命名
actions: {
  fetchUser() {},        // 获取用户
  fetchUserList() {},    // 获取用户列表
  createUser() {},       // 创建用户
  updateUser() {},       // 更新用户
  deleteUser() {},       // 删除用户
  login() {},            // 登录
  logout() {}            // 登出
}

Getter 命名

javascript
// 使用小驼峰命名,可以是属性形式
getters: {
  isLoggedIn() {},       // 是否已登录
  userName() {},         // 用户名
  cartTotal() {},        // 购物车总价
  itemCount() {},        // 项目数量
  filteredItems() {}     // 过滤后的项目
}

最佳实践

1. 按功能划分模块

javascript
// ✅ 推荐 - 按业务功能划分
modules/
├── user/          # 用户相关
├── cart/          # 购物车相关
├── products/      # 商品相关
└── orders/        # 订单相关

// ❌ 不推荐 - 按技术类型划分
modules/
├── states/
├── mutations/
├── actions/
└── getters/

2. 统一导出模块

javascript
// store/modules/index.js
const modules = {}

const moduleFiles = require.context('.', true, /index\.js$/)

moduleFiles.keys().forEach(path => {
  const moduleName = path.replace(/^\.\/(.*)\/index\.js$/, '$1')
  if (moduleName !== 'index') {
    modules[moduleName] = moduleFiles(path).default
  }
})

export default modules

3. 使用严格模式

javascript
// 仅在开发环境启用
const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

4. 合理使用根状态

javascript
// 根状态只存放全局共享的状态
state: {
  loading: false,      // 全局加载状态
  error: null,         // 全局错误
  theme: 'light'       // 全局主题
}

// 业务状态放在模块中
modules: {
  user: { ... },
  cart: { ... }
}

5. API 与 Action 分离

javascript
// api/user.js - 纯 API 调用
export default {
  login(credentials) {
    return request.post('/auth/login', credentials)
  },
  logout() {
    return request.post('/auth/logout')
  },
  getUserInfo() {
    return request.get('/user/info')
  }
}

// store/modules/user/actions.js - Action 处理业务逻辑
import api from '@/api/user'

export default {
  async login({ commit }, credentials) {
    const response = await api.login(credentials)
    commit('SET_TOKEN', response.token)
    commit('SET_USER', response.user)
    return response
  }
}

6. 使用 TypeScript 增强

typescript
// types/store.d.ts
import { Store } from 'vuex'

interface User {
  id: number
  name: string
  email: string
}

interface RootState {
  loading: boolean
  error: Error | null
}

interface UserState {
  userInfo: User | null
  token: string | null
}

declare module 'vue/types/vue' {
  interface Vue {
    $store: Store<RootState>
  }
}

常见问题

Q: 什么时候需要拆分模块?

当单个文件超过 200 行代码,或者多个组件共享同一类状态时,应该考虑拆分模块。

Q: 根状态和模块状态如何划分?

  • 根状态:全局共享的状态,如 loading、error、theme
  • 模块状态:特定业务领域的状态,如用户、购物车、商品

Q: 如何处理模块间的依赖?

javascript
// 通过 rootState 和 rootGetters 访问其他模块
actions: {
  async checkout({ state, rootState, rootGetters }) {
    const isLoggedIn = rootGetters['user/isLoggedIn']
    if (!isLoggedIn) {
      throw new Error('请先登录')
    }
    // ...
  }
}

Q: 如何实现状态的懒加载?

javascript
// 路由守卫中动态注册模块
router.beforeEach(async (to, from, next) => {
  if (to.meta.requiresModule && !store.hasModule(to.meta.module)) {
    const module = await import(`@/store/modules/${to.meta.module}`)
    store.registerModule(to.meta.module, module.default)
  }
  next()
})