Vite怎么状态管理?前端工程师必备
▌ 技术引导 Vite内置状态管理并不够用,你得自己加。项目一上来就用pinia,别搞reactive和ref,我见过很多人用这些反而把自己绕进去。直接用pinia,它轻量、响应式、支持TS,还能和vue3无缝对接。别想着用globalThis或者window对象,那玩意儿容易出事,特别是在多页面或SSR场景下。模板引擎那边,直接用vite-plugin-vue-jsx或者vite-plugin-vue3-transform,这玩意儿能帮你把pinia的模块结构串起来。还有个点很关键,别在setup里直接修改state,用actions封装,不然会触发无限循环。你要是用nuxt3,那state管理就交给nuxt的stores,不用额外配置。要是用vite+react,就用useContext之类的钩子,别用react的context,会冲突。这些是我踩过坑得出来的结论,别浪费时间在无用的方案上。 ▌ 技术参考 一 使用pinia作为状态管理工具 在vite项目中引入pinia,创建store时用defineStore函数,配置文件放在src/stores目录。每个store文件导出一个默认对象,里面包含state、actions、getters等。例如,创建一个useCounter.js: import { defineStore } from 'pinia' export default defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ }, decrement() { this.count-- } } }) 在main.js中注册pinia实例,然后在组件中用useCounter()引入。pinia支持模块化,每个store可以独立管理,结构清晰。但别在setup函数里直接操作state,容易被reactive函数的响应式规则干扰,导致不必要的渲染。 二 vitesse与vite集成方式 vitesse是vite官方的项目模板,它内置了pinia和vue3,或者react+react-query。在创建项目时选择vue3模板,然后在src/stores目录下创建store文件。默认会自动加载,不用额外配置。如果项目中需要动态加载store,可以用import.meta.glob自动引入。比如: const modules = import.meta.glob('./stores/.js') for (const path in modules) { const module = await modules[path]() app.use(module.default) } 这种方式适合中大型项目,模块按需加载,减少初始打包体积。不过要注意,某些store可能依赖其他模块,需要按依赖顺序加载,否则会出现undefined。 三 使用reactive和ref的注意事项 在vue3中,如果你不使用pinia而手动用reactive和ref管理状态,得注意不能在setup函数里直接修改ref对象的值。比如: const count = ref(0) count.value = 100 // 这是正确的写法 count = 100 // 这是错误的,会破坏响应式 如果在setup中用reactive包裹对象,修改属性会触发更新,但如果是直接给对象赋值,不会。所以在组件内操作状态时,要保持一致的写法,避免类型变化引起的问题。这种写法适合小型组件,但在大型项目中容易混乱,建议统一用pinia。 四 踩坑场景:状态未被正确注入 在使用pinia时,如果store没有被正确注入到app实例中,组件调用useStore时会报错。常见错误是忘记调用app.use(pinia)。比如,在main.js中: import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const app = createApp(App) const pinia = createPinia() app.use(pinia) // 忘记这行,组件用useStore会出问题 app.mount('#app') 如果你没有这行,直接使用pinia会报错。另外,如果store文件名不是以use开头,vite不会自动加载,得手动导入。 五 踩坑场景:store模块未被正确分组 有些项目会把多个store分组,比如用户模块和商品模块,这时候需要在main.js里手动注册模块。比如: import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import userStore from './stores/user' import productStore from './stores/product' const app = createApp(App) const pinia = createPinia() app.use(pinia) pinia.use(userStore) pinia.use(productStore) app.mount('#app') 如果模块没有被正确注册,useStore的时候会提示模块不存在。此外,如果使用了模块化,记得在store文件中导出默认对象,否则会出错。 六 踩坑场景:状态未被正确响应式绑定 在使用pinia时,如果在组件中调用了useStore,但状态没有变化时,组件不会重新渲染。这时候需要检查是否在actions中正确修改了state。比如,如果在action中直接给state赋值,而没有使用this.state.xxx = value,那么响应式不会触发。 正确的做法是: actions: { updateCount(value) { this.count = value } } 错误的做法是: this.count = value // 这行没问题 this.count.value = value // 这行会有问题,因为state已经是一个响应式对象了 所以在action里,不要用.value去修改state,直接赋值即可。 七 踩坑场景:store未被正确类型化 在ts项目中,如果store没有正确类型化,会导致类型报错。比如,定义一个store时,state中的属性没有类型说明,ts会报错。解决方法是使用defineStore时传入类型参数: import { defineStore } from 'pinia' import { ref } from 'vue' interface UserState { name: string age: number } export default defineStore('user', { state: (): UserState => ({ name: '', age: 0 }), actions: { setName(name: string) { this.name = name } } }) 如果没有类型说明,ts会自动推断,但类型不准确时会导致后续使用报错。所以在大型项目中,类型化非常重要,能减少很多调试时间。 八 使用vite-plugin-vue3-transform优化store加载 对于vue3项目,如果store文件很多,手动导入会很麻烦。可以用vite-plugin-vue3-transform插件,它会自动引入所有store文件。安装插件:npm install vite-plugin-vue3-transform -D 然后在vite.config.js中配置: import vue3Transform from 'vite-plugin-vue3-transform' export default defineConfig({ plugins: [ vue3Transform(), ] }) 这样,所有src/stores目录下的store文件都会被自动引入,不需要手动管理。但要注意,插件不支持动态加载,所以大型项目还是推荐手动管理,或者用其他工具如import.meta.glob。 九 使用import.meta.glob动态加载store模块 在需要按需加载store的情况下,可以用import.meta.glob来动态引入。比如: const stores = import.meta.glob('./stores/.js') for (const path in stores) { const module = await stores[path]() app.use(module.default) } 这种方式适合多页面应用,或者需要根据路由加载不同store的场景。但要注意,某些store可能依赖其他模块,比如用户模块可能需要访问商品模块,这时候要确保依赖顺序正确。否则,会遇到undefined的问题。 十 使用vite-plugin-pinia自动化配置 如果你不想手动处理store的注册,可以用vite-plugin-pinia插件,它会自动注册所有store。安装插件:npm install vite-plugin-pinia -D 然后在vite.config.js中配置: import pinia from 'vite-plugin-pinia' export default defineConfig({ plugins: [ pinia(), ] }) 这样,所有src/stores目录下的store文件都会被自动加载,组件中直接使用useStore()即可。但插件不支持ts项目,如果项目是ts,需要额外配置ts的类型支持。否则,类型会缺失,导致开发体验变差。 十一 pinia在SSR场景下的表现 如果项目需要支持服务端渲染,比如使用nuxt3,pinia的状态在服务端和客户端之间可能不一致。这时候需要使用pinia的persist插件,将状态持久化到客户端。安装插件:npm install pinia-plugin-persist -D 然后在main.js中配置: import { createPinia } from 'pinia' import piniaPersist from 'pinia-plugin-persist' const pinia = createPinia() pinia.use(piniaPersist()) app.use(pinia) 这样,状态会在服务端和客户端之间保持一致。但要注意,persist插件可能会增加首屏加载时间,特别是在状态较多的情况下,需要权衡是否使用。 十二 使用react-query进行状态管理 对于react项目,如果不想用pinia,可以用react-query。安装react-query:npm install @tanstack/react-query -D 然后配置vite的react插件,开启ssr支持: import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ react({ ssr: true }), ] }) 接着在组件中使用useQuery,或者useMutation。比如: import { useQuery } from 'react-query' const { data } = useQuery('key', async () => { return await fetch('/api/data') }) react-query会自动管理状态,支持缓存和更新,适合数据较多的场景。不过它不支持模块化,每个组件都得自己管理查询,可能不够灵活。 十三 在vite+vue3项目中使用pinia的响应式特性 pinia的状态本身是响应式的,所以直接修改state中的值会触发组件更新。比如: const store = useStore() store.count = 100 这时候组件会自动更新。但如果在store外直接操作state,比如: const count = store.count count = 100 // 这样不会触发更新 必须通过store.count = value来修改,这样才是响应式的。这和vue3的reactive机制一致,所以不要混淆。 十四 状态持久化与localStorage的配合使用 如果需要在页面刷新后保留状态,可以使用localStorage。在pinia中创建一个store来管理存储: import { defineStore } from 'pinia' import { ref } from 'vue' export default defineStore('persist', { state: () => ({ stateData: ref({}) }), actions: { saveState(key, value) { localStorage.setItem(key, JSON.stringify(value)) }, loadState(key) { const data = localStorage.getItem(key) if (data) { return JSON.parse(data) } return null } } }) 然后在应用初始化时加载数据,在状态修改时保存。不过要注意,频繁操作localStorage会影响性能,所以只在必要时使用,比如登录状态或用户偏好设置。 十五 使用vite-plugin-ssr进行状态管理 如果项目需要强SSR支持,可以用vite-plugin-ssr插件。它会帮助你处理状态在服务端和客户端之间的传递。安装插件:npm install vite-plugin-ssr -D 然后在vite.config.js中配置: import ssr from 'vite-plugin-ssr' export default defineConfig({ plugins: [ ssr(), ] }) 使用插件后,vite会自动处理state的序列化和反序列化,但需要配合pinia使用。在组件中,通过useStore获取状态,在服务端渲染时需要确保状态数据正确传入。这会增加一些复杂度,但能提升性能和用户体验。 十六 区分单文件组件和全局store的使用 在某些场景下,组件内部可能需要临时状态,这时候用reactive或ref更合适。比如在组件内: const tempData = ref({}) 在组件外,用pinia管理全局状态。这种分工能减少不必要的状态传递,提升代码可维护性。如果在组件内用store,会增加依赖,导致组件之间耦合过强。所以要根据场景选择合适的工具。 十七 使用vite-plugin-vue3-transform进行store结构转换 当store结构复杂时,可以用vite-plugin-vue3-transform插件,自动将store结构转换成更易管理的格式。安装插件:npm install vite-plugin-vue3-transform -D 然后在vite.config.js中配置: import vue3Transform from 'vite-plugin-vue3-transform' export default defineConfig({ plugins: [ vue3Transform(), ] }) 这样,所有store文件会被自动处理,结构清晰。但插件不支持动态加载,所以大型项目还是得手动注册模块。 十八 使用vite插件进行环境区分状态管理 在开发环境和生产环境使用不同的状态管理方式。比如在开发时用pinia,在生产时用localStorage保存状态。可以通过环境变量判断: if (process.env.NODE_ENV === 'production') { // 使用localStorage } else { // 使用pinia } 但这样会增加代码复杂度,所以更推荐在store中处理环境判断,比如: actions: { saveState(key, value) { if (process.env.NODE_ENV === 'production') { localStorage.setItem(key, JSON.stringify(value)) } else { this.stateData[key] = value } } } 这样不管在哪种环境都能正常工作,减少出错概率。 十九 使用vite-plugin-ssr进行数据预加载 对于需要大量数据的组件,可以用vite-plugin-ssr预加载数据。安装插件:npm install vite-plugin-ssr -D 然后在组件中使用useDataPrefetch来预加载数据: useDataPrefetch('key', 'value') 这样,数据会在组件加载前准备好,避免白屏。但要注意,预加载的数据必须是静态的,不能动态生成。否则会引发性能问题。 二十 使用vite-plugin-ssr进行状态同步 在vite-plugin-ssr中,服务端渲染的state需要和客户端保持同步。可以通过useStore获取state,然后在服务端渲染时传递给组件。比如: const state = useStore() renderToString() 这样,客户端可以接收到服务端的数据,避免重复请求。但要注意,state中的数据必须是可序列化的,否则会出错。所以状态管理时要避免使用函数或对象引用。





