在前后端分离的开发模式中,为了不影响任何一端的开发进度,通常会制定好接口的格式,然后前端工程师通过mock数据的方式,暂时在本地实现接口相关数据传输功能。目前有很多同类产品或代码库实现了这个功能,比如
Apifox
、Yapi
等。包括本篇文章介绍的msw
。
msw介绍
源代码版本:2.2.2msw官网: https://mswjs.io/docs/
npm上地址:https://mswjs.io/docs/
github地址:https://github.com/mswjs/msw
msw(Mock Service Worker)
是一个可以在node环境和浏览器环境进行网络请求数据模拟的功能库。截止发文时可以进行模拟数据的api有两种,一种是常见的 REST API
,另外一种是 GraphQL API
。
这篇文章主要记录
REST API
使用和源码阅读
使用步骤也非常简单明了,只需要三个步骤就可以进行接口数据模拟了。按照官网介绍的步骤如下:
- 第一步,下载安装msw
- 第二步,描述网络行为
- 第三步,集成到软件环境中
显而易见三个步骤中,描述网络行为是整个库的核心技术所在区域。用非常直白的话来说,这部分做的事情就是对请求的拦截和处理。
作者在官网又将描述网络行为分为了两个部分:handler
、resolver
。
- handler:一个描述需要被劫持请求的函数。
- resolver:一个描述了在获取到被劫持的请求后,如何进行处理的函数。
虽然handler
、resolver
作为单独的定义在官方文档给出了说明,但是个人觉得其实也就是对一个请求的不同时间段的处理。
handler
更倾向于在请求还没有发出,我们先设定的“关卡”,用来拦截目标请求。
resovler
更注重的是对拦截后的请求进行处理,也就是用来进行数据mock或者一些特殊的数据处理逻辑的地方。
// file: handlers.js
import { http, HttpResponse } from 'msw'
const handlers = [
// 这部分对应的是handler
http.get('/hello', () => {
// 这个对应的是resovler
return new HttpResponse('hello')
})
]
Service Woker API
Service Workder API 介绍
在 msw
库中对请求拦截,并且对其做出定制化响应内容的主要技术就是 Service Worker API
。它相当于在客户端和服务器端中间做了一个代理服务器的角色。
在mdn上对它的使用场景做了如下的列举:
- 后台数据同步
- 响应来自其他源的资源请求
- 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
- 在客户端进行 CoffeeScript、LESS、CJS/AMD 等模块编译和依赖管理(用于开发目的)
- 后台服务钩子
- 自定义模板用于特定 URL 模式
- 性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片
除了上述的功能外,Service Worker API
被设计出来最直接的目的,就是解决客户端(特别指浏览器端)上的应用在没有网络的时候,能和移动端app一样,有内容返回(这里的内容是指在真实接口缓存的数据),从而提升离线Web应用程序的体验。
注意点:
- Service Worker API 是单独的线程,并且是完全异步的,所以不可以在它的上下文中操作DOM元素、同步XHR、Web Storage等功能。
- Service Worker API 只能由HTTPS进行承载,当然为了便捷开发,对本地服务localhost也采用了默认认可的的状态,而之所以这样做就是为了防止
中间人攻击
Service Worker API的初步使用
Service Worker API
的使用可以简单概括为两个重要的步骤:1. 注册 、2. 下载|安装|激活。
1. 注册
Service Worker Api
通过 navigator.serviceWorker.register()
函数进行注册。
函数支持两个参数:
- scriptUrl:String
- info:Object
-
- scope: String
-
- type: ‘classic’ | ‘module’
-
- updateViaCache: ‘all’ | ‘imports’ | ‘none’
service worker脚本的URL。这里要补充说明下,这个脚本其实就是我们把 Service Worker
相关的代码进行了单独文件的封装。而这个文件所在的位置决定了我们去进行监听处理的脚本可以的最大范围值。
这个最大范围值,会影响到第参数的 info.scope
,info.scope
的作用是限制一个可用路径的最大范围中的工作范围。
root
--src
main.js
--html
--head
--index.html
--foot
--index.html
sw.js(注册Service Worker的文件)
// 如果一个项目是上面的结构。在进行注册 Service Worker的时候.
// 只有当访问localhost:9527/html/foot/下面的资源时候才有效果
const registration = await navigator.serviceWorker.register('/sw.js', {scope: '/html/foot/'})
info.type
是用来标识注册 service worker 脚本的引入方式,默认是标准的script引用,使用 ‘module’ 变成了 Es Module 模块
info.updateViaCache
表示在升级的时候如何进行内容缓存。
info.type
和info.updateViaCache
只有mdn英文版中有详细说明。
2. 下载|安装|激活
在通过 navigator.serviceWorker.register()
进行注册后,浏览器会自动对其进行安装并且激活。在使用的时候首先要对其进行兼容性处理。
如下一个完整的注册流程:
// sw.js
const registerServiceWorker = async () => {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {scope: './'})
if (registration.installing) {
console.log('正在安装 Service worker')
} else if (registration.waiting) {
console.log('已安装 Service worker installed')
} else if (registration.active) {
console.log('已激活 Service worker active')
}
} catch(err) {
console.log('注册失败', err)
}
}
}
3. 相关事件
在准备步骤完成后,还有两点需要牢记:
-
Service Worker的版本
Service Worker是有更新逻辑的,当新的版本安装后,并不会立刻在页面中激活并生效,而是会等到老的版本不再被使用后才会进行更替。可以通过skipWaiting()
更快的进行激活。 -
激活后
Service Worker会控制已经在register成功后打开的页面,已经打开的页面需要进行重新加载,可以使用clients.claim()方法解决
在经过注册、下载、安装之后,需要使用监听事件的方式进行使用。常用的事件有 install(安装)
、active(生效)
、fetch(拦截到请求)
上文提到过的,如果想实现WebApp的离线体验,可以从 install
阶段,初始化cache
、indexDb
等缓存策略。以便在 fetch
阶段进行请求的拦截,并返回缓存的内容。
/**
* self是当前的worker上下文
**/
// 下载后立刻进行安装激活,不等待之前版本的失活
self.addEventListener('install', () => {
self.skipWaiting()
})
// 立刻接手并控制给定路径下的所有页面(包括已经打开的页面)
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})
self.addEventListener('fetch', (event) => {
// 拦截 POST 请求
if (event.request.method === 'POST') {
event.respondWith(new Response("hello",{
status: 200,
headers: {"Content-type": "application/text"}
}))
resolve();
}
})
event.waitUntil(promise)
接受一个Promise作文参数, 会告知事件分发器当前事件正在进行,来保证在异步事件完成前,终止当前线程的服务
msw的使用
// 使用msw简单的例子
import { http, HttpResponse } from 'msw';
// 制定handlers(要拦截的请求)
const handlers = [
http.get(
// 路径可以传入正则类型
/\/pets/,
(info) => {
// 拦截到请求后,使用HttpResponse返回mock的数据
return HttpResponse.json(['Tom', 'Jerry'])
}
)
]
async function beforeStart() {
const { setupWorker } = await import('msw/browser');
const worker = setupWorker(...handlers)
// worker.start()返回的是一个Promise
return worker.start()
}
beforeStart().then(() => {
// 模拟浏览器请求
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:9527/pets');
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send()
})
上面代码的运行达到了拦截符合正则 /pets
的 GET
请求,并且返回了 ['Tom', 'Jerry']
。