Vue Nuxt 2 中间件请求冲突解决:隔离用户数据
2025-03-23 11:56:38
Vue Nuxt 2 Middleware: 解决多用户请求冲突问题
碰到了一个麻烦事:在我的Vue Nuxt 2(同时使用了vue storefront)项目中,用中间件解析URL,结果发现不同用户的请求会相互干扰。
举个例子,用户A打开产品"abc"的页面,同时用户B打开产品"xyz"的页面,有时候用户A看到的却是"xyz"的内容。 这可不行啊!
下面先贴上我出问题的中间件代码 (url-resolver.ts
):
import { Middleware } from '@nuxt/types';
import { usePageStore } from '~/stores/page';
import { Logger } from '~/helpers/logger';
import { RoutableInterface } from '~/modules/GraphQL/types';
const urlResolverMiddleware: Middleware = async (context) => {
const pageStore = usePageStore();
const { path } = context.route;
const clearUrl = path.replace(/[a-z]+\/[cp|]\//gi, '').replace(`/${context.i18n.locale}`, '');
Logger.debug('middleware/url-resolver', clearUrl);
const { data, errors } = await context.app.$vsf.$magento.api.route(clearUrl);
Logger.debug('middleware/url-resolver/result', { data, errors });
const results: RoutableInterface | null = data?.route ?? null;
if (!results || errors?.length) context.error({ statusCode: 404 });
pageStore.$patch((state) => {
state.routeData = results;
});
};
export default urlResolverMiddleware;
以及 stores > pages.ts
:
import { defineStore } from 'pinia';
interface PageState {
routeData: any;
}
export const usePageStore = defineStore('page', {
state: (): PageState => ({
routeData: null,
}),
});
问题根源:共享状态惹的祸
仔细瞧瞧,问题出在 Pinia store (usePageStore
) 的 routeData
上。多个用户同时访问时,都在修改同一个共享状态。最后访问的那个用户,就把之前用户的数据给覆盖了。 这就导致了请求冲突。
更具体地说, 每次触发中间件,pageStore.routeData
都会被新的请求数据覆盖。 因为 pageStore
是一个单例,所有用户共享。
解决方案:隔离用户数据
要解决这个问题,核心思想就是把用户数据隔离开,别再搅和在一起。下面几种方法都能达到这个目的:
1. 利用 Nuxt Context
Nuxt 的 context
对象是每个请求独立的。我们可以直接把数据存储在 context
里,而不是全局的 store。
-
原理:
context
对象是请求级别的,每个用户请求都有自己独立的context
,互不干扰。 -
实现:
修改
url-resolver.ts
:import { Middleware } from '@nuxt/types'; // import { usePageStore } from '~/stores/page'; // 不再需要引入 import { Logger } from '~/helpers/logger'; import { RoutableInterface } from '~/modules/GraphQL/types'; const urlResolverMiddleware: Middleware = async (context) => { const { path } = context.route; const clearUrl = path.replace(/[a-z]+\/[cp|]\//gi, '').replace(`/${context.i18n.locale}`, ''); Logger.debug('middleware/url-resolver', clearUrl); const { data, errors } = await context.app.$vsf.$magento.api.route(clearUrl); Logger.debug('middleware/url-resolver/result', { data, errors }); const results: RoutableInterface | null = data?.route ?? null; if (!results || errors?.length) context.error({ statusCode: 404 }); // 直接把数据存储在 context 中 context.routeData = results; }; export default urlResolverMiddleware;
-
页面组件获取数据 : 在页面组件中, 通过
context.routeData
获取对应的数据, 替代原来的从store
中获取:
<script>
export default {
async asyncData(context) {
// 通过context获取中间件存储的数据
const routeData = context.routeData;
return {
routeData
}
},
}
</script>
- 优势: 简单, 改动最小。
- 劣势: 如果你需要在多个中间件, 或者layout, plugin中共享这份数据,
context
传递起来会相对繁琐.
2. 使用请求特定的 Store ID
Pinia 允许我们在创建 store 时传入一个 ID。我们可以用请求相关的唯一标识符(比如 route 的 path)作为 ID,这样每个请求都会有自己的 store 实例。
-
原理: 通过为每个请求创建独立的 Pinia store 实例,实现状态隔离。
-
实现:
修改
url-resolver.ts
:import { Middleware } from '@nuxt/types'; import { usePageStore } from '~/stores/page'; import { Logger } from '~/helpers/logger'; import { RoutableInterface } from '~/modules/GraphQL/types'; const urlResolverMiddleware: Middleware = async (context) => { const { path } = context.route; // 使用 path 作为 store 的 ID const pageStore = usePageStore(path); const clearUrl = path.replace(/[a-z]+\/[cp|]\//gi, '').replace(`/${context.i18n.locale}`, ''); Logger.debug('middleware/url-resolver', clearUrl); const { data, errors } = await context.app.$vsf.$magento.api.route(clearUrl); Logger.debug('middleware/url-resolver/result', { data, errors }); const results: RoutableInterface | null = data?.route ?? null; if (!results || errors?.length) context.error({ statusCode: 404 }); pageStore.$patch((state) => { state.routeData = results; }); }; export default urlResolverMiddleware;
页面获取: 仍然使用和
url-resolver.ts
中间件一样的ID, 来获取指定的store
实例。<script> import { usePageStore } from '~/stores/page'; export default { computed:{ routeData(){ const pageStore = usePageStore(this.$route.path); return pageStore.routeData; } } } </script>
-
进阶: 如果担心 path 太长或者有特殊字符影响 store 的创建,可以对 path 进行哈希处理,生成一个更短、更安全的 ID。 可以用
crypto
模块, 或者第三方的hash库, 比如hash.js
.import hash from 'hash.js'; // ... const storeId = hash.sha256().update(path).digest('hex'); const pageStore = usePageStore(storeId); // ...
组件内部也做相同处理:
import hash from 'hash.js'; //... computed:{ routeData(){ const storeId = hash.sha256().update(this.$route.path).digest('hex'); const pageStore = usePageStore(storeId); return pageStore.routeData; } }
-
优势: 可以在应用的其他地方(layout, component)共享这份请求相关的数据。
-
劣势: 要小心内存泄漏, 在组件销毁时手动调用
pageStore.$dispose()
, 释放不再需要的store。 你也可以通过维护一个 store ID 的列表,在适当的时机(比如路由变化后)批量清理过期的 store。
3. 使用 Vuex (如果你还在用)
如果你仍然在使用 Vuex,那么这个问题通常不会出现,因为 Vuex 的 module 默认是 namespaced,状态通常是隔离的。但如果你的 Vuex store 配置不当,也可能出现类似问题。确保你的 modules 启用了 namespaced: true
,并且 state 是一个函数,返回一个对象。
- 原理: 利用 Vuex 的 namespaced modules 特性,确保每个模块的状态是独立的。
- 确认
namespaced
已开启.
// store/myModule.js
export default {
namespaced: true, // 确保已开启
state: () => ({
routeData: null,
}),
// ... mutations, actions, getters
};
4. 调整数据获取逻辑 (终极方案, 更推荐)
前面的方案主要着眼于状态管理, 其实还可以从数据获取逻辑上入手. 将 context.app.$vsf.$magento.api.route(clearUrl)
移出中间件, 直接放在页面的 asyncData
或者 fetch
钩子中。
-
原理: 中间件主要用于处理一些通用的逻辑, 比如鉴权, 重定向等. 而页面特定数据的获取,更适合放在页面组件内部。
-
实现:
移除
url-resolver.ts
(或将其简化,仅处理通用逻辑)。在你的页面组件 (
.vue
文件) 中:<script> import { Logger } from '~/helpers/logger'; // 如果需要 import { RoutableInterface } from '~/modules/GraphQL/types'; export default { async asyncData({ app, route, error }) { // 使用 asyncData 或 fetch const { path } = route; const clearUrl = path.replace(/[a-z]+\/[cp|]\//gi, '').replace(`/${route.params.locale}`, ''); Logger.debug('page/asyncData', clearUrl); const { data, errors } = await app.$vsf.$magento.api.route(clearUrl); Logger.debug('page/asyncData/result', { data, errors } ); const results: RoutableInterface | null = data?.route ?? null; if (!results || errors?.length) error({ statusCode: 404 }); return { routeData: results, }; }, }; </script>
-
优势: 数据获取和组件强相关, 代码更清晰, 不会污染全局状态. 请求错误也可以直接在当前页面处理, 逻辑更集中。
-
劣势: 如果你需要在多个页面重复这段数据获取的逻辑, 会有重复代码. 这时, 可以考虑把请求相关的逻辑封装成一个可复用的 composable function (Vue 3) 或者 mixin (Vue 2).
安全提示(通用)
不管你用哪种方案,都要记得对用户的输入(比如 URL 中的参数)进行校验和清理,防止恶意代码注入。 例如:
- 使用成熟的库(如
validator
)来验证 URL 或其他参数的格式。 - 对用户输入进行转义,防止 XSS 攻击。
- 确保服务器端返回数据的合法性. 不要完全信任客户端数据.
总而言之,遇到多用户请求冲突的问题,别慌。找到状态共享的“罪魁祸首”,把它隔离出来就搞定了。 从需求出发, 选择适合自己项目的方案.