返回

Vue Nuxt 2 中间件请求冲突解决:隔离用户数据

vue.js

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 攻击。
  • 确保服务器端返回数据的合法性. 不要完全信任客户端数据.

总而言之,遇到多用户请求冲突的问题,别慌。找到状态共享的“罪魁祸首”,把它隔离出来就搞定了。 从需求出发, 选择适合自己项目的方案.