当前位置:网站首页>剖析Mooncake的代理原理,实现快速提效
剖析Mooncake的代理原理,实现快速提效
2022-07-29 17:46:00 【得物技术】
原创 migor 得物技术
前言
Mooncake 平台得物统一的针对UI侧域服务侧联调Mock的一款工具产品,如下所示,在平台可以快速的创建各个项目产品的Mock多场景数据。前端可以通过对应的接口,改造不同的请求场景和请求数据,大大提高了前端的开发效率,缩短了联调时间。
为什么要做Mooncake?
在Mooncake 平台之前,公司也有相应的Mock方案,基于Yapi 或者自研的Mock, 但是由于数据配置复杂,或者接入项目要侵入业务代码等一些原因,导致现有的Mock 在前端开发中的使用率不高,因此,基于这些痛点,我们进行了Mooncake 平台的开发,做到了Mooncake 平台接入方便不侵入业务代码,数据配置简单,增强了数据的可维护性。
1. Mooncake方案探讨
在最初的方案制定时,坚持两个原则:不做业务代码侵入,接入方便;数据可维护性,进行方案的探讨。
1.1本地Node服务器
基于本地JSON文件或者本地数据库为数据存储,启动一个本地的node服务,通过读写本地配置的接口URL匹配的JSON数据,给前端提供mock数据的服务和能力,大致的思路如图所示:
该方案类似于市场中的 json-server。
放弃原因:
数据维护为用户个人的行为,数据的复用性较差,项目之间配置重复性工作较多,无法满足数据的可维护性,不利于数据复用带来的提效
1.2 Yapi 方案
在Mooncake上线前,前后端用的mock数据都统一维护在Yapi平台上,对于前后端的开发确实起到了提效功能;但是Yapi本身的使用率并不高,大致的问题:
a. 过于依赖服务端
b. 过于集中管理
c. 上手成本高,配置能力太多,交互过于复杂
1.3 Mooncake代理方案
由于 Xhr 和 fetch 在浏览器中挂载到 window上的,最初的 Mooncake 定位是为前端开发提供 mock能力的,基于这两点,我们采用劫持请求代理的方案。
那么到此,我们基本确定了基本的方案,上面我们说过制定方案的两个原则:
1. 不侵入业务代码;
2. 数据可维护性,制定了第一个方案。
- 通过 html plugin 方式和代理劫持,通过配置 plugin 的方式,不侵入业务代码;
- node 服务会在项目目录生成一个数据维护文件夹,维护mock数据JSON文件,通过Gitlab代码仓库,保证数据的复用性;
- 通过UI配置平台,前端配置接口和接口数据。
上线之后,接入项目相对比较正常,反馈也比较好,但是无法在测试环境mock数据,无法给测试提供mock 和 UI 验收的能力,于是进行了方案的改进。
优点:
- 能够满足前端开发维护开发的Mock 数据;
- 测试回归测试场景、UI产品进行验收。
但是在使用了一段时间之后发现了一些问题:
- 项目的mock 数据维护在Gitlab 项目中,导致项目数据所占空间较大;
- 接口数据经常需要解决冲突问题;
- 开发需要维护两份mock 数据。
于是有了现在的最终代理方案。
开发和测试环境,统一维护在Mooncake的线上服务,保证了数据的统一和复用性;同时通过HTML Plugin保证了Mooncake的无侵入性的接入;后续同时提供了Chrome插件,提供了项目的无侵入性的接入。
Mooncake产品整体,包括了 线上配置平台、代理层,代理注入 三个板块,这篇文章主要给大家介绍一下 代理层 的实现。在这之前,我们先看一下整体的代理逻辑思路。
今天我们主要介绍一下 XHR 和 Fetch 的代理思路。
2. XHR
2.1 简介
XMLHttpRequest 一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿,也提供了这个接口,再后来 W3C 对他进行了标准化,提出了 XMLHttpRequest 标准。
通过查看 Can I Use【1】 可以查看各大浏览器对 XMLHttpRequest 的支持,入下图所示:
从图中可以看到:
- IE10/IE11部分支持,不支持xhr.responseType 为 json ;
- Opera Mini 不支持 xhr ;
- 部分浏览器版本不支持 timeout 超时请求和 responseType为 blob。
2.2 如何使用
function sendRequest() { // 请求数据 const formData = new FormData(); formData.append('name', 'migor'); formData.append('role', 'member'); // 创建请求xhr const xhr = new XMLHttpRequest(); // 设置超时时间 xhr.timeout = 3000; // 设置返回数据类型 xhr.responseType = 'json'; // 打开一个请求 xhr.open('POST', url, true); // 处理回调 xhr.onload = function(e) { if(this.status === 200 || this.status === xxx) { alert(this.responseText) } } xhr.ontimeout = function(e){...}; xhr.onerror = function(e) {...} //发送数据 xhr.send(formData)}
2.3 实现劫持
最初的实现思路如下图所示,在用户发送请求之后,先从开发服务器拿数据,在返回数据之前,请求mooncake服务器,如果有数据,则返回mooncake服务器数据,如果没有数据,则返回开发服务器数据。
通过查看MDN , 我们可以看到对于 XMLHttpRequest.response、XMLHttpRequest.responseText 这些为只读属性,所以我们不能直接修改 response 和 responseText 的值,只能通过Object.defineProperty 来实现对 response 的修改,实现代码如下:
let origin = XMLHttpRequest.prototype.openXMLHttpRequest.prototype.open = function(...args) { // 插入open拦截 this.onResponse = function(res) { return res; } return origin.apply(this, args);}var accessor = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "response")Object.defineProperty(XMLHttpRequest.prototype, "response", { get: function() { let response = accessor.get.call(this) // 在onResponse里修改你的response response = this.onResponse(response); return response; }, set: function(str) { return accessor.set.call(this, str); }, configurable: true})
思路很清晰,那么我们来测试一下:
const request = new XMLHttpRequest()request.onResponse = function(res) { return res + '添加的内容hello world';}request.open(method, url, true)request.onreadystatechange = function() { if(request.readyState === XMLHttpRequest.DONE && request.status === 200) { console.log(request.response) }}request.send();
可以看到返回结果已经变为处理后的结果,如下图所示:
现在我们已经完成了返回内容 response 的修改,回到我们最初的方案要求,需要在拿到内容之后进行请求 mooncake服务器 , 查看是否返回内容即可,那么实现get 的异步即可。到这时,但是后面去实现 getter 异步之后,发现在返回处返回的对象是 Promise , 思考之后,发现是因为 readyState 状态时间已经改变,但是异步任务还没结束,如果要等待结果,只能去手动触发 readyState 事件,那么这样就会多次触发 console ,同时我们发现在 getter/setter 中不支持 async 语法的,到此,这个方案出现了问题。
改进方案,如下图所示:
只需要实现XMLHttpRequest 的异步钩子即可,这边使用了Github 上成熟的方案,Ajax-hook ,具体的理解如下:
主要实现思路如下:
ob.hookAjax = function (funs) { //保存真正的XMLHttpRequest对象 window._ahrealxhr = window._ahrealxhr || XMLHttpRequest //1.覆盖全局XMLHttpRequest,代理对象 XMLHttpRequest = function () { //创建真正的XMLHttpRequest实例 this.xhr = new window._ahrealxhr; for (var attr in this.xhr) { var type = ""; try { type = typeof this.xhr[attr] } catch (e) {} if (type === "function") { //2.代理方法 this[attr] = hookfun(attr); } else { //3.代理属性 Object.defineProperty(this, attr, { get: getFactory(attr), set: setFactory(attr) }) } } }
一开始先保存了真正的XMLHttpRequest对象到一个全局对象,然后在注释1处,Ajax-hook覆盖了全局的XMLHttpRequest对象,这就是代理对象的具体实现。在代理对象内部,首先创建真正的XMLHttpRequest实例,记为xhr,然后遍历xhr所有属性和方法,在2处hookfun为xhr的每一个方法生成一个代理方法,在3处,通过defineProperty为每一个属性生成一个代理属性。下面我们重点看一看代理方法和代理属性的实现。
代理方法
function hookfun(fun) { return function () { var args = [].slice.call(arguments) //1.如果fun拦截函数存在,则先调用拦截函数 if (funs[fun] && funs[fun].call(this, args, this.xhr)) { return; } //2.调用真正的xhr方法 this.xhr[fun].apply(this.xhr, args); }}
属性修改
通过getFactory 和 setFactory 生成 setter 、getter 方法。
function getterFactory(attr) { return function () { var v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : this.xhr[attr] var attrGetterHook = (proxy[attr] || {})["getter"] return attrGetterHook && attrGetterHook(v, this) || v }}// Generate setter for attributes of xhr; by this we have an opportunity// to hookAjax event callbacks (eg: `onload`) of xhr;function setterFactory(attr) { return function (v) { var xhr = this.xhr var that = this var hook = proxy[attr] // hookAjax event callbacks such as `onload`、`onreadystatechange`... if (attr.substring(0, 2) === 'on') { that[attr + "_"] = v xhr[attr] = function (e) { e = configEvent(e, that) var ret = proxy[attr] && proxy[attr].call(that, xhr, e) ret || v.call(that, e) } } else { //If the attribute isn't writable, generate proxy attribute var attrSetterHook = (hook || {})["setter"] v = attrSetterHook && attrSetterHook(v, that) || v this[attr + "_"] = v try { // Not all attributes of xhr are writable(setter may undefined). xhr[attr] = v } catch (e) { } } }}
具体使用
import {proxy, unProxy} from "ajax-hook";proxy({ //请求发起前进入 onRequest: async (config, handler) => { const mockRes = await new Promise((resolve, reject) => { // 建立mock请求 const mockXhr = new XMLHttpRequest(); mockXhr.open(method, url) mockXhr.onload = function cb(e) { if(!mockXhr.response) { resolve({code: -1}) } else { const mockResponse = JSON.parse(mockXhr.response) resolve(mockResponse) } } mockXhr.send() }) // 如果没有数据转开发服务器数据 if(mockRes.code === -1) { handler.next(config) } else { const responseMock = setResponseData(res) handler.resolve({ config: config, response: responseMock, status: 200, statusText: 'OK' }) } handler.next(config); }, onError: (err, handler) => { console.log(err.type) // handler.next(err) // 异常重新走开发服务请求 handler.resolve(err) }, //请求成功后进入 onResponse: (response, handler) => { console.log(response.response) handler.next(response) }})
大致的思路就是这样的,服务的转发逻辑,例如开发环境、测试环境等的相关转发处理逻辑,以及数据的解析逻辑这里不在赘述。
3. Fetch
3.1 简介
Fetch可以理解为 XMLHttpRequest 的升级版,用于访问和操纵 HTTP 管道的一些具体部分,例如请求和响应。它还提供了一个全局 fetch()fetch() 方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
3.2 用法
从服务器获取JSON数据
const data = { username: 'example' };fetch('https://example.com/profile', { method: 'POST', // or 'PUT' headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data),}).then(response => response.json()).then(data => { console.log('Success:', data);}).catch((error) => { console.error('Error:', error);});
其中 response 内容的获取方法:
*response.text() // 获取文本字符串
*response.json() // 获取json对象
*response.blob() // 获取二进制Blob对象
*response.formData() // 获取FormData表单对象
*response.arrayBuffer() // 获取二进制 arrayBuffer 对象
3.3 实现劫持
由于fetch支持异步原因,fetch 的劫持相对来说比较简单,这里就不分析思路了,直接上代码:
// 劫持全局对象的fetchconst originFetch = window.fetch;window.fetch = await mooncakeFetchProxy(originFetch);async function mooncakeFetchProxy(originFetch: (input: RequestInfo, init?: RequestInit) => Promise<Response>) { return async (input, init = {}) => { // 处理请求参数和服务器中转逻辑 let mockInput = getOnlineMockUrl() // 发送mock请求 let response = null; const mockFecth = await fetch(mockInput).then(res => { // 需要使用clone方法才能将返回内容拷贝出来 response = res.clone() return res.json() }).catch(() => { console.log('服务出错了') }) if(!!mockFecth && mockFecth.code !== -1 && response.status === 200) { // 请求成功才能进来 const { type, headers, url, statusText } = response const mockRes = parseRes(mockFetch) const data = parseResponseData(mockRes) const blob = new Blob([JSON.stringify(data)]) const newInit = { type, headers, url, statusText } // 返回mock数据 const res = new Response(blob, newInit); return res; } // mock请求失败或者没有mock接口 const origin = await originFetch(input, init).then(res => { return res; }) // 返回原始请求数据 return origin; }}
到这里,我们已经基本完成了Mooncake 代理的实现,对于公司内各个项目的接入情况,数据维护状态,以及不同组织人员的使用情况,我们需要进行数据统计,因此我们在前期做了数据的埋点,来支撑Mooncake 平台的数据统计分析,同时在后期的抓包核心功能实现中,我们也进行了埋点方案的改进,下面我们讲一下Mooncake 平台的埋点方案。
4. 埋点方案探索
4.1 gif
由于最初统计接入Mooncake的项目和使用的接口的情况是在代理层进行的数据统计,只是简单的数据上报,所以埋点方案是用的gif方式,通过创建一个Image标签元素,在响应的时候给src赋值,做一次请求。
优点:
- 性能上比较好(简单请求,不用多一步预检请求的时间消耗)
- 跨域比较友好(天然支持跨域)
- 不需要响应
4.2 sendBeacon
在做抓包方案的时候,需要上传更复杂的数据,此时Gif 方案以及不适合了,因此想到了个Navigator.sendBeacon() ,查看一下支持的浏览器。
发现在Chrome 浏览器中基本支持 sendBeacon,查看一下MDN 的介绍:
- Image 的优点兼具
- 写法更简单
- 在浏览器空闲的时候发请求,更彻底的异步,不阻塞页面的刷新/跳转等。
- Beacon是非阻塞请求,不需要响应
在上线之后,数据上传陆续接收到不同的问题反馈,主要是集中在两点,一个是数据长度过长,导致请求失败;另外一个是在低版本的Chrome 浏览器中无法发送请求。针对第一个问题,查询发现因为 sendBeacon 支持的数据长度最多是64kb,第二个问题是在低版本浏览器中无法发送请求。
4.3 XMLHttpRequest
由于我们抓包发送的数据可能存在数据量较大,因此最后我们改用了XMLHttpRequest,由于Mooncake 平台在抓包过程中功能单一,所以页面不存在跳转之类的操作,完全能满足要求。
5. 总结
市场上的Mock方案相对较多,得物前端Mooncake平台作为UI测的联调提效工具,通过线上配置平台、代理层,代理注入三层的实现,实现了数据可视化配置和数据转发,提升了前端的联调效率。核心代理层基于Proxy对 XHR 和 Fetch进行请求转发,实现了Mock 接口数据的获取,完成了Mock数据和服务端数据的自由切换。
目前Mooncake 通过Mock能力积累了大量的前端和客户端用户,后续我们计划为服务端提供接口文档的管理能力,方便后端维护,以及同步前端接口文档,并基于现有场景组为测试通过自动化用例生成和回归测试的能力,从而打通前后端以及测试的整个链路环节,提升开发效率。
参考链接:
https://caniuse.com/?search=XMLHttpRequest
*文/migor
关注得物技术,每周一三五晚18:30更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
边栏推荐
- 实现get/post请求调用第三方接口
- 阿里最新发布的《Alibaba分布式系统速成笔记》PDF版,供下载
- 要卖课、要带货,知识付费系统帮你一步搞定!
- Mathematical Analysis_Proof_Two Important Limits (Tongji Version)
- 「记录」MMDetection入门篇
- Learn to arthas, 3 years experience with 5 years work force you!
- [Code Hoof Set Novice Village 600 Questions] Find the square root of the given data
- P5514 [MtOI2019]永夜的报应(位运算)
- 国产钡铼分布式IO模块如何与西门子PLC Profinet通讯
- 【码蹄集新手村600题】求所给数据的平方根
猜你喜欢
传统渲染农场和云渲染农场选择哪个好?
"Hardcore" labelme shows the label in the picture
Virtink:更轻量的 Kubernetes 原生虚拟化管理引擎
NFTScan and PANews jointly release multi-chain NFT data analysis report
The difference between firmware, driver and software
Dialogue with Academician Yu Fei of the Canadian Academy of Engineering: Looking for "Shannon's Theorem" in the field of AI
go的堆内存结构分析
解决 @RefreshScope 导致定时任务注解 @Scheduled 失效
NFTScan 与 PANews 联合发布多链 NFT 数据分析报告
华中农大团队提出:一种基于异构网络的方法,可自动提取元路径,预测药物-靶标相互作用
随机推荐
发力5G平板市场,品网科技首发展锐5G平板解决方案
华东师范大学副校长周傲英:数据赋能,从数据库到数据中台
[Code Hoof Set Novice Village 600 Questions] Find the distance between two points in the space rectangular coordinate system
Which is better, traditional render farm or cloud render farm?
Dialogue with Academician Yu Fei of the Canadian Academy of Engineering: Looking for "Shannon's Theorem" in the field of AI
Virtink:更轻量的 Kubernetes 原生虚拟化管理引擎
【运维】ssh tunneling 依靠ssh的22端口实现访问远程服务器的接口服务
mysql存储过程 实现全量同步数据并自动创建表
MySQL数据库的七种约束语法格式和使用详解&约束的总结
直播实录 | 37 手游如何用 StarRocks 实现用户画像分析
生产计划体系完整解决方案(1) - 复杂大规模问题的分阶段规划
Zigbee组网控制流程
白宫将举办半导体产业链CEO峰会,台积电、三星、格芯、英特尔等将参与
[Deep Learning] Use yolov5 to pre-label data
国产钡铼分布式IO模块如何与西门子PLC Profinet通讯
阿里最新发布的《Alibaba分布式系统速成笔记》PDF版,供下载
软考高级软件架构风格定义以及分类
Xatlas source code parsing (7)
redis cluster 集群,终极方案?
go的堆内存结构分析