概述
前端工程化项目中,在项目的研发前期,难免要处理一些接口请求相关的东西,比如模拟接口数据,定义假数据,在还没有前后端联调的时候,此时是没有后端参与的,为了提高开发效率,在前端工程化设计中,能够拦截网络请求,然后模拟接口返回是必不可少的,这样我们就可以在没有后端参与的情况下,独立完成闭环逻辑,大大提升开发效率。
流程
msw
MSW(Mock Service Worker)是一个用于前端和 Node.js 的 API 模拟工具,它通过拦截实际网络请求并返回模拟响应来帮助开发者轻松构建测试和开发环境。以下是它的核心特性和用途:
1. 核心原理
- Service Worker 拦截请求:MSW 利用浏览器原生的 Service Worker API 拦截发出的 HTTP 请求(如
fetch、XMLHttpRequest),并返回预先定义的模拟响应。 - 无服务器依赖:无需启动后端服务,直接在浏览器或 Node.js 中模拟 API。
2. 核心功能
-
无缝模拟 REST/GraphQL API:
// 示例:模拟一个GET请求 import { setupWorker, rest } from 'msw'; const worker = setupWorker( rest.get('/api/user', (req, res, ctx) => { return res( ctx.json({ name: 'John Doe' }) ); }) ); worker.start(); -
支持多种请求类型:
GET、POST、PUT、DELETE等。 -
动态响应:根据请求参数返回不同数据。
-
模拟错误场景:如
404、500等状态码。
3. 主要用途
-
开发环境:在后端未完成时,前端独立开发。
-
测试(单元/集成/E2E) :
- 与 Jest、Vitest、Cypress 等工具结合,避免依赖真实接口。
- 测试边缘情况(如网络错误、空数据)。
// 测试示例 test('loads user data', async () => { render(<UserComponent />); await screen.findByText('John Doe'); });
4. 优势
- 真实网络行为:比传统的
jest.mock更贴近浏览器实际请求流程。 - 一次定义,多处使用:同一套 mock 可用于开发、测试。
- 调试友好:在浏览器开发者工具中能看到被拦截的请求记录。
5. 安装与配置
npm install msw --save-dev
# 或
yarn add msw --dev
- 浏览器环境:需生成 Service Worker 文件(通过
npx msw init public/)。 - Node 环境:使用
setupServer。
使用上的局限
如果在vite环境下使用,就需要通过上述一系列的安装操作,并且生成mockServiceWorker.js文件,这个文件是必须的,不然msw无法运行,并且要默认服务启动能直接访问该文件,意味着要把该文件放置到public文件下,因此,如果直接项目里面安装使用,会产生很多副作用:
- mockServiceWorker.js对开发者来说来说应该无感,不应该直接暴露开发环境下的东西到项目目录下
- 对于上述配置安装,应该内置集成,通过插件方式自动生成注入,开发者只关心配置接口相关的文件
插件编写
由于是基于vite搭建的项目,如果要做到在vite启动打包的过程中加入我们自定义的一些io操作,就需要通过插件来实现,这样才能做到使用者不用关心内部实现,只关心如何使用,对于如何编写一个cite插件,可以直接靠官网插件章节。
插件目录
插件目录如下:
- lib:最终项目打包生成的文件目录
- src: 项目根目录
- README.MD:项目描述文件
- vite.config.js: vite配置文件
- tsconfig.json: ts相关配置文件
插件要做的事情
- 自动生成mock相关的配置文件和初始化配置
- 自动注入mockServiceWorker.js到根服务(开发者无感,因此只在内存中生成)
- 在vite启动的时候,根据环境区分是否需要生成mock相关文件和逻辑
- 通过IO操作,生成项目mock相关配置文件
package.json
{
"name": "vite-plugin-msw",
"version": "1.0.3",
"description": "a vite plugin for vite-msw",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"typings": "./lib/index.d.ts",
"type": "module",
"exports": {
".": {
"require": "./lib/index.js",
"import": "./lib/index.js"
}
},
"scripts": {
"build": "vite build"
},
"keywords": [],
"files": [
"lib",
"README.md"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.15.29",
"clean-webpack-plugin": "^4.0.0",
"ts-loader": "^9.5.2",
"typescript": "^5.8.3",
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"fs-extra": "^11.3.0",
"fsevents": "^2.3.2",
"msw": "^2.10.2",
"vite": "^6.3.5"
}
}
src/index.ts
主要逻辑函数
export const moduleInfoFileContent =`
import { http, HttpResponse } from "msw";
export const handlers = [
http.post('/api/lcdp/v1/entries/delete', () => {
return HttpResponse.json({
code: '0',
data: null,
})
}),
http.post('/api/lcdp/v1/entries/add', () => {
return HttpResponse.json({
code: '0',
data: null,
})
}),
];
`
export const indexInfoFileContent =`
import { setupWorker } from "msw/browser";
// 批量导入某个文件夹下的所有文件
const modules = import.meta.glob('./module/**/*.mock.{js,ts}')
// 异步获取所有 handlers 并合并
async function getAllHandlers() {
const handlers = []
for (const path in modules) {
const module = await modules[path]()
if (module.handlers && Array.isArray(module.handlers)) {
handlers.push(...module.handlers)
}
}
return handlers
}
getAllHandlers().then(handlers => {
const worker = setupWorker(...handlers);
worker.start({
onUnhandledRequest: 'bypass',
});
})
mockServiceWorker.js
这个文件是msw必须要的文件,默认服务启动可以访问,因此要动态在内存中生成,首先通过脚本生成这个文件
npx msw init ./src
template.ts
生成默认的mock文件夹模板
export const moduleInfoFileContent =`
import { http, HttpResponse } from "msw";
export const handlers = [
http.post('/api/lcdp/v1/entries/delete', () => {
return HttpResponse.json({
code: '0',
data: null,
})
}),
http.post('/api/lcdp/v1/entries/add', () => {
return HttpResponse.json({
code: '0',
data: null,
})
}),
];
`
export const indexInfoFileContent =`
import { setupWorker } from "msw/browser";
// 批量导入某个文件夹下的所有文件
const modules = import.meta.glob('./module/**/*.mock.{js,ts}')
// 异步获取所有 handlers 并合并
async function getAllHandlers() {
const handlers = []
for (const path in modules) {
const module = await modules[path]()
if (module.handlers && Array.isArray(module.handlers)) {
handlers.push(...module.handlers)
}
}
return handlers
}
getAllHandlers().then(handlers => {
const worker = setupWorker(...handlers);
worker.start({
onUnhandledRequest: 'bypass',
});
})
vite打包配置
vite.config.js
import { defineConfig } from 'vite';
import path from 'path';
import { fileURLToPath } from "url";
import dts from 'vite-plugin-dts'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig({
build: {
outDir: "lib",
rollupOptions: {
external: [
/^node:/,
'path',
'fs',
'vite',
'fsevents',
'rollup',
'esbuild'
],
},
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
fileName: 'index',
formats: ['es']
}
},
plugins: [
dts({
insertTypesEntry: true, // 生成类型声明入口
include: ['src'], // 包含的ts文件
exclude: ['**/__tests__/**'], // 排除测试文件
beforeWriteFile: (filePath, content) => {
// 可选:自定义类型文件写入逻辑
return { filePath, content }
}
})
]
});
发布到远程
npm pubish
使用
安装
在需要使用的项目文件下安装我们写的插件
pnpm i vite-plugin-msw
配置
- vite.congfig.js
2. 生成.env.local文件
启动项目
pnpm run dev
可以看到自动生成了mock相关的配置文件
实际配置
开发者只需要关注这个配置文件即可,编写相关的接口拦截数据返回
效果
启动项目并且配置了开启接口模拟就可以看到如下效果说明插件生效了,如果不想模拟接口,直接关闭配置变量即可
总结
上面的插件是项目里面真实的工程化插件,可以根据自己项目实际情况,在插件当中扩展更多跟自己业务相关的逻辑。