基于 MSW 的前端 Mock 方案原理与项目集成

309 阅读4分钟

一、引言

前面已经搭建了项目的基本机构,由于没有后端服务,暂且使用mock方案来完成动态路由的生成

现在前后端分离已成为主流架构模式。前端开发常常面临后端接口未完成或不稳定的情况,Mock 技术应运而生。Mock Service Worker(MSW)作为新一代的 Mock 方案,凭借其基于 Service Worker 的拦截能力,实现了对网络请求的无侵入式模拟,极大提升了前端开发与测试的效率。

二、MSW 原理与优势

Mock Service Worker是一种基于Service Worker API的网络请求拦截工具,它能够在不修改应用代码的前提下,拦截并模拟网络请求。MSW的工作原理可以概括为以下几个步骤:

  1. 注册Service Worker :MSW在应用启动时注册一个Service Worker,该Worker负责拦截网络请求。
  2. 请求拦截 :当应用发起网络请求时,已注册的Service Worker会拦截这些请求。
  3. 请求处理 :根据预定义的处理程序(handlers),Service Worker决定如何响应这些请求。
  4. 返回模拟数据 :Service Worker生成模拟响应并返回给应用

三、项目中的 MSW 集成

  1. 依赖安装于基础配置

    npm insall msw axios
    
  2. msw初始化 快速将 ./mockServiceWorker.js 工作脚本复制到应用程序的公共目录中。该脚本负责拦截与分发请求,必须生成

    //npx msw init <PUBLIC_DIR> --save
    npx msw init public --save
    
  3. axios简单封装

    /**
     * axios请求封装
     * 支持泛型返回、统一错误处理、自定义headers、loading控制等功能
     */
    import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'
    import axios from 'axios'
    import { ElMessage, ElLoading } from 'element-plus'
    import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading'
    
    /**
     * 接口响应通用格式
     * @template T 响应数据类型
     */
    export interface ApiResponse<T = unknown> {
      /** 响应数据 */
      data: T
      /** 错误码,0表示成功,其他表示失败 */
      errorCode: number
      /** 错误信息 */
      errorMsg: string
    }
    
    /**
     * 扩展的请求配置
     */
    export interface RequestOptions extends AxiosRequestConfig {
      /** 是否显示loading,默认false */
      showLoading?: boolean
      /** 是否直接返回data,默认true */
      returnData?: boolean
      /** 自定义headers */
      customHeaders?: Record<string, string>
    }
    
    /**
     * 默认配置
     */
    const defaultConfig: RequestOptions = {
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
      showLoading: false,
      returnData: true,
    }
    
    /**
     * 获取系统版本号
     */
    const getSystemVersion = (): string => {
      return import.meta.env.VITE_APP_VERSION || '1.0.0'
    }
    
    /**
     * 请求类
     */
    class Request {
      private instance: AxiosInstance
      private loadingInstance: LoadingInstance | null = null
    
      constructor(config: RequestOptions) {
        // 创建axios实例
        this.instance = axios.create(config)
    
        // 请求拦截器
        this.instance.interceptors.request.use(
          (config) => {
            const requestOptions = config as RequestOptions
    
            // 添加系统版本号到header
            config.headers['X-System-Version'] = getSystemVersion()
    
            // 添加自定义headers
            if (requestOptions.customHeaders) {
              Object.keys(requestOptions.customHeaders).forEach((key) => {
                config.headers[key] = requestOptions.customHeaders![key]
              })
            }
    
            // 显示loading
            if (requestOptions.showLoading) {
              this.loadingInstance = ElLoading.service({
                lock: true,
                text: '加载中...',
                background: 'rgba(0, 0, 0, 0.7)',
              })
            }
    
            return config
          },
          (error: unknown) => {
            return Promise.reject(error)
          },
        )
    
        // 响应拦截器
        this.instance.interceptors.response.use(
          (response) => {
            // 关闭loading
            if (this.loadingInstance) {
              this.loadingInstance.close()
            }
    
            const { data } = response
            const requestOptions = response.config as RequestOptions
    
            // 判断是否成功
            if (data.errorCode !== 0) {
              // 显示错误信息
              ElMessage.error(data.errorMsg || '请求失败')
              return Promise.reject(data)
            }
    
            // 根据配置返回data或完整响应
            return requestOptions.returnData ? data.data : data
          },
          (error: unknown) => {
            // 关闭loading
            if (this.loadingInstance) {
              this.loadingInstance.close()
            }
    
            // 处理错误
            let message = '网络请求失败'
            const axiosError = error as AxiosError
            if (axiosError.response) {
              switch (axiosError.response.status) {
                case 401:
                  message = '未授权,请重新登录'
                  break
                case 403:
                  message = '拒绝访问'
                  break
                case 404:
                  message = '请求地址错误'
                  break
                case 500:
                  message = '服务器内部错误'
                  break
                default:
                  message = `请求失败(${axiosError.response.status})`
              }
            } else if (axiosError.message && axiosError.message.includes('timeout')) {
              message = '请求超时'
            }
    
            ElMessage.error(message)
            return Promise.reject(error)
          },
        )
      }
    
      /**
       * 发送请求
       * @param config 请求配置
       * @returns Promise
       */
      public request<T = unknown, R = ApiResponse<T>>(config: RequestOptions): Promise<R> {
        return this.instance.request(config)
      }
    
    
      /**
       * GET请求
       * @param url 请求地址
       * @param params 请求参数
       * @param options 请求配置
       * @returns Promise
       */
      public get<T, R = Record<string, unknown>>(
        url: string,
        params?: R,
        options?: RequestOptions,
      ): Promise<T> {
        return this.instance.get(url, { params, ...options })
      }
    
      /**
       * POST请求
       * @param url 请求地址
       * @param data 请求数据
       * @param params 请求参数
       * @param options 请求配置
       * @returns Promise
       */
      public post<T, R = Record<string, unknown>>(
        url: string,
        data?: R,
        params?: R,
        options?: RequestOptions,
      ): Promise<T> {
        return this.instance.post(url, data, { params, ...options })
      }
    
      /**
       * PUT请求
       * @param url 请求地址
       * @param data 请求数据
       * @param options 请求配置
       * @returns Promise
       */
      public put<T>(url: string, data?: Record<string, unknown>, options?: RequestOptions): Promise<T> {
        return this.instance.put(url, data, options)
      }
    }
    
    // 导出请求实例
    export const request = new Request(defaultConfig)
    
    // 导出默认实例
    export default request
    
  4. 定义api接口

    如果后台服务还没搭建好,但是有相关接口规范文档,可以先定义接口,使用msw的mock方案

    import request from '@/utils/request'
    
    export function testApi(){
      return request.get('/api/test')
    }
    
    
  5. MSW配置

    • 基础配置

      // .env.development
      VITE_ENABLE_MOCK=true
      

      在开发环境中,可以通过修改 VITE_ENABLE_MOCK 变量来启用或禁用API模拟服务。当该值为 true 时,应用将使用MSW提供的模拟数据;当该值为 false 时,应用将使用真实的API服务。

    • Mock 规则编写

    import { http, HttpResponse } from 'msw';
    // Mock处理程序
    const handlers = [
      // 获取用户信息
      http.get('/api/test', () => {
        return HttpResponse.json(
          createResponse<Record<string,string|number>>({
            id: 1,
            username: 'admin',
            email: 'admin@example.com',
            role: 'admin',
          }),
        )
      }),
    ]
    
    • msw启动

      // 创建MSW worker
      const worker = setupWorker(...handlers)
      
      /**
       * 启动MSW
       */
      export function setupMSW() {
      // 检查是否启用mock服务
        const enableMock = import.meta.env.VITE_ENABLE_MOCK === 'true'
        if (enableMock) {
          worker
            .start({
              onUnhandledRequest: 'bypass', // 对未处理的请求直接放行
            })
            .catch(console.error)
      
          console.log('[MSW] Mock Service Worker 已启动')
        }
      }
      
      export default setupMSW
      
      //main.ts
      // 导入并启动MSW
      import { setupMSW } from './mocks'
      setupMSW()
      

至此msw继承完毕,在没有后端服务的情况下,可以使用这种方案,等后端服务搭建好后,可以你不用改动任何代码