手写 mock 插件
大约 3 分钟
手写 mock 插件
信息
在实现 mock 插件之前我们需要了解一下 vite 插件的一些基本 api 的使用
Vite 插件形式
vite 插件扩展自 Rollup 插件接口,只是额外多了一些 vite 特有选项 vite 插件是一个拥有名称、创建构造或生成钩子的一个对象
如果需要配置插件,它的形式应该是一个接收插件选项,返回插件对象函数
插件钩子
通用钩子
开发时,Vite dev server
创建一个差价容器按照Rollup
调用创建钩子的规则其你去各个钩子函数,下面钩子会在服务器启动时调用一次
- options 替换或操作 rollup 选项
- buildStart 开始创建
下面钩子在请求时候会被调用 就是被 import 的时候会调用
- resolveId 创建自定义确认函数,用来定位模块
- load 创建自定义加载函数,可用于返回自定义内容
- transform 可用于转换已加载的模块内容
下面钩子会在关闭会在关闭一次
- buildEnd
- closeBundle
vite 特有钩子
config 修改 vite 配置 configResolved vite 配置确认 configureServer 用于配置 dev server handleHotUpdate 自定义 HMR 更新时间 transformIndexHtml 用于转换宿主页
调用顺序 如下
config configResolved options configureServer buildStart transformIndexHtml resolveId load transform
插件执行顺序
- Alias
- 带有 enforce: 'pre' 的用户插件
- Vite 核心插件
- 没有 enforce 值的用户插件
- Vite 构建用的插件
- 带有 enforce: 'post' 的用户插件
- Vite 后置构建插件(最小化,manifest,报告)
实现一个 mock 插件
实现思路就是给开发服务器(connect)配置一个中间件,该中间件可以存储用户配置接口的映射信息,并提前处理输入请求,如果请求的是 url 和路由表匹配则接管,按用户配置的 handler 返回结果
主要用到的 钩子是 configureServer
去监听路由
代码
import { ViteDevServer, Plugin, normalizePath } from 'vite'
import { resolve, isAbsolute, basename, relative } from 'path'
import fs from 'fs'
import { IncomingMessage, ServerResponse } from 'node:http'
import url from 'url'
import { parse, init } from 'es-module-lexer'
/**
* stps
* 1. 读取配置 是否开启前缀 prefix mock入口 enter
* 2. 创建一个中间件
* 3. 匹配路由映射
* 4. 请求的时候获取映射
* 5. 将 req 和 res 参数传到 handle 里面去
*
*/
type MyMockOptions = {
enter?: string
prefix?: boolean // 是否开启命名空间
}
type StaticRouter = {
route?: string
method: string
handle: (
res?: IncomingMessage & { query?: Object; body?: Object },
req?: ServerResponse & { send: Function }
) => void
}
const defaultEnter = normalizePath(resolve(process.cwd(), 'mock'))
/**
* http模块的get 请求 和post请求的处理 https://blog.csdn.net/weixin_43285360/article/details/121512719
*/
export default (options: MyMockOptions = {}): Plugin => {
let { enter = defaultEnter, prefix = true } = options
// 保存映射
const routerMap = new Map<string, StaticRouter>()
if (!isAbsolute(enter)) {
enter = normalizePath(resolve(process.cwd(), enter))
}
const sendHendle = function (
this: ServerResponse & { send: Function },
data: string | Object
) {
let chunk: Buffer | string = JSON.stringify(data)
if (chunk) {
chunk = Buffer.from(chunk, 'utf-8')
this.setHeader('Content-Length', chunk.length)
}
this.setHeader('Content-Type', 'application/json')
this.end(chunk, 'utf-8')
}
// 构建依赖图
const createRouter = async (invork: (file: string) => void) => {
await Promise.all(
fs.readdirSync(enter).map(async (dir) => {
await invork(dir)
})
)
}
const loadMockModle = async (
file: string,
rejest: (touter: StaticRouter) => void
): Promise<void> => {
const mockdir = normalizePath(relative(process.cwd(), resolve(enter, file)))
const a = (await import(`./${mockdir}`).then((m) => {
return m.default ?? []
})) as StaticRouter[]
for (const route of a) {
const r = prefix ? '/' + basename(file, '.js') + route.route : route.route
if (routerMap.has(r)) {
routerMap.delete(r)
}
const method = route.method.toUpperCase()
const handle = route.handle ?? (() => {})
await rejest({ route: r, handle, method })
}
}
const registRouter = async (server: ViteDevServer) => {
// server.middlewares.stack = []
for (const route of [...routerMap.keys()]) {
server.middlewares.stack.push({
route,
handle: (
req: IncomingMessage & { query?: Object; body?: Object },
res: ServerResponse & { send: Function }
) => {
res.send = sendHendle
const method = routerMap.get(route).method
if (req.method !== method) {
res.statusCode = 405
res.send({ message: '方法不支持' })
return
}
if (method === 'GET') {
const query = url.parse(req.url, true).query
req.query = query
routerMap.get(route).handle(req, res)
}
if (method === 'POST') {
// 1.定义变量保存传递过来的参数
let params = ''
req
.on('data', (chunk) => {
params += chunk
})
.on('end', function () {
let body = JSON.parse(params)
req.body = body
routerMap.get(route).handle(req, res)
})
}
},
})
}
}
return {
name: 'my-mock-plugin',
async configureServer(server) {
await createRouter(async (file: string) => {
await loadMockModle(file, async (router) => {
const { route, ...res } = router
routerMap.set(route, res)
})
})
await registRouter(server)
},
async handleHotUpdate({ file, server, read }) {
if (!file.startsWith(enter)) return null
const data = await read()
await loadMockModle(file, async (router) => {
const { route, ...res } = router
routerMap.set(route, res)
const st = server.middlewares.stack.find((st) => st.route === route)
st.handle = res.handle
})
},
}
}