你一定会喜欢的mock开发插件~

413 阅读5分钟

proxy-pre-mock

💡 proxy-pre-mock顾名思义,在正向代理之前返回mock数据

为什么会起这个名字

在开发这个插件之前一直用的是apite这个工具来mock开发,但是用着用着发现几个问题:

  • 为什么mock服务要和proxy服务关联起来,这明明是两个独立的功能啊
  • 如果在开发一个新项目时,接口都是支持跨域的,不需要配置proxy进行开发呢?
  • 而且vite、webpack等构建工具中proxy服务都是devserver自带的,不需要重新再写一个proxy服务

所以就想着proxy能力用框架自带的,mock插件独立编写,然后在devserver中间件、插件等形式注入构建工具中(置于proxy服务之前)也是能实现的,这也就诞生了proxy-pre-mock这个想法。

使用方法

💡 具体使用和文档请查看https://github.com/Himangguo/proxy-pre-mock

proxy-pre-mock

  1. 引入库提供的中间件或插件

    // webpack
    const {webpack5Middleware} = require('proxy-pre-mock')
    module.exports = defineConfig({
        devServer: {
            setupMiddlewares: (middlewares) => {
                middlewares.unshift(webpack5Middleware('mock'));
                return middlewares;
            }
        }
    })
    
    // vite
    import { vite5Plugin } from 'proxy-pre-mock'
    export default defineConfig({
        plugins: [
            vite5Plugin('mock')
        ]
    })
    
  2. 定义mock请求响应

    const { addMock } = require('proxy-pre-mock')
    
    addMock('/api/check', 'get',(req)=>{
        return {
            code: 200,
            data: {
                name: 'check',
                age: 14
            }
        }
    })
    

2024/6/22更新

  • 支持addMock.getaddMock.post等这种调用方式
  • 增加mock开关,可以不用注释代码来关闭mock
  • 支持动态pathname参数,例如:/api/list/:id

开发中遇到的问题(包含解决方案)

1. 如何实现热更新

初期使用了动态require的方案:监听文件变动,重新require模块。但是模块会有缓存,所以单纯再次require不能生效,需要删除上一次的缓存

if (require.cache[filePath]) {
      delete require.cache[filePath]
}

这里并没有考虑删除缓存出现的内存泄漏问题,因为毕竟是本地环境使用的插件。

但是在vite项目中,一用就报错啦:由于是esmodule的项目,不能使用动态require Untitled.png 那么将package.json中的type=”module”改成commonjs不就行了吗,可以是可以,但是未来vite将要不再支持commonjs这种形式,所以这种方案并不能一劳永逸。

ok,支持esmodule的正常的思路是直接将动态require换成动态import。但是这时问题出现了:import不能像require一样传入模块的绝对路径 Untitled (1).png but,很快找到了解决方案:pathToFileURL

import { pathToFileURL } from "url"
// require(pathname)
import(pathToFileURL(pathname).href)

这也完美解决了这个报错,但是问题又来了,动态import的模块同样有缓存,但是它没有像commonjs模块中类似的获取缓存值和删掉缓存的方法。

寻找解决方案中发现说构建工具可以配置不使用模块缓存,但是这必然会大大降低编译的效率,得不偿失。

再后来发现了了一个知名的模块导入库bundle-require,它会自动识别你要导入的模块是commonjs还是esmodule,然后选择require还是import。并且为了避免缓存,他在每次导入前,使用esbuild在相同目录下构建好原模块的临时拷贝版本,并且文件名hash随机生成过。然后导入这个copy版本,这样就能避免命中模块缓存,再成功导入模块后,删除掉这个临时文件。amazing!

2. 删除文件或者注释掉mock函数没有生效

由于修改文件后会触发热更新,那么针对已有mock的修改或者新增mock都能很好的生效(在内部使用了一个hashMap来记录key→handle),key使用pathname+method作为唯一值,handle是响应体函数。

但是注释某一个addMock函数后,重新require的时候,并不知道哪个addMock被注释了,所以这个mock的接口还在生效。删除也是一个道理。所以需要有一个hashMap去记录文件对应的mock列表。但是问题是我怎么知道当前导入的是哪个模块?

我的思路是在全局定义一个对象,用于记录当前require的模块路径还有缓存解析模块时生成的mock。

const CUR_FILE = {
    path: null,
    cache: []
}

在解析完模块后,通过模块路径查找它对应的旧mock列表,然后通过CUR_FILE.cache(新mock列表)来diff,查找需要删除的mock接口。

function checkNeedDelMock() {
    const oldKeys = pathMockMap.get(CUR_FILE.path) || []
    const newKeys = CUR_FILE.cache
    const delKeys = oldKeys.filter(key => !newKeys.includes(key))
    delKeys.forEach(key => {
        handleMap.delete(key)
    })
    pathMockMap.set(CUR_FILE.path, CUR_FILE.cache)
}

3. bundle-require是异步导入带来的问题

由于热更新是从监听到文件变动后再执行模块导入,之前动态require没有问题是因为,动态require是同步代码,require的顺序是按照监听到变动的文件顺序执行的,所以模块的导入是同步的,这没有问题。但是改为异步后,每个模块导入开始和导入成功的时间点不是按顺序的,这样CUR_FILE记录的模块和当前正在导入的模块可能会不一样。这样逻辑就有问题了,所以需要写一个异步文件加载队列来实现某一时刻只能导入一个模块。

这是我实现这一思路的代码:

watcher
    .on('add', path => {
        addLoadTask(path)
        runLoadTask()
    })
    .on('change', path => {
        addLoadTask(path)
        runLoadTask()
})

const loadTask = {
    queue: [],
    finished: true
}

function addLoadTask(filePath) {
    if (!filePath || path.extname(filePath) !== '.js') return
    loadTask.queue.push(filePath)
}

async function runLoadTask() {
    if (!loadTask.finished) return
    loadTask.finished = false
    while (loadTask.queue.length) {
        const filePath = loadTask.queue.shift()
        console.log('模块加载开始:', filePath)
        try {
            await loadMockRoute(filePath)
            console.log('模块加载完成:', filePath)
        } catch (error) {
            console.log('模块加载失败:', filePath)
            console.log(error)
        }
    }
    loadTask.finished = true
}