文章目录
一、问题描述
在工作中,想自己根据后台模拟的数据,保存到本地,在网络不通的情况下,同样的写ajax请求逻辑代码,不需要改代码模拟ajax请求,获取本地的模拟的数据,能够正常测试和开发
,如果有开发环境,可以一键切换到开发环境中,而不需要切换代码
二、文章参考
- Mock.mock(rurl, rtype, template) 函数
- mockjs Doc
- Mock.mock(url,rtype, template)劫持ajax的原理(个人理解)
- Mock.mock() 方法介绍
- Mockjs如何拦截带参数的GET请求
三、快速开始
3.1 安装Mockjs
npm install mockjs -D
3.2 使用案例
import Mock from "mockjs";
//拦截请求,返回假数据
Mock.mock("http://10.91.7.159:9080/taxloan/productStat?customerId=sdf", {
message: "调用成功",
data: [
{ name: "XXXX1", code: "P00003" },
{ name: "XXXX2", code: "P00028" },
{ name: "XXXX3", code: "P00002" },
{ name: "XXXX4", code: "P00003" }
],
rtn: "0"
});
- 定义的URL 请求,直接放到浏览器中运行,实际上是没有响应的,估计是在发送ajax请求的时候,mockjs做了一个拦截处理,将响应的数据变为模拟的数据了
- 例子中,可以使用GET 或者 POST 请求
3.3 前端发送ajax请求
export default {
methods: {
// 获取地理位置信息
getLocationData: function () {
const that = this
http.get('http://10.91.7.159:9080/taxloan/productStat?customerId=sdf').then(function (resp) {
console.log(resp)
debugger
})
},
}
}
说明:
请求实际上没有发送到后台,而是被本地拦截了,因此 chrome开发工具查询不到请求的内容
- 代码逻辑实际上是是走的ajax请求,保证代码不用做任何修改
四、根据配置文件,异步加载mockjs 配置文件
main.js 启动文件中根据配置是否引用mock数据
import Vue from 'vue'
import App from './App.vue'
// 根据Vue的环境变量模式中获取配置信息,异步加载配置文件
// process.env.VUE_APP_MOCK 获取到的不是boolean,而是字符串
if (process.env.NODE_ENV === 'development' && process.env.VUE_APP_MOCK !== 'false') {
import('./data/mockeData').then(_ => {
console.log('引入mock数据成功')
})
}
new Vue({
render: h => h(App),
}).$mount('#app')
定义.env.development 的配置信息
VUE_APP_MOCK = false
Vue如何区分开发模式?
五、动态生成mock 请求拦截
如果需要灵活配置哪些接口是需要使用mock数据的,哪些是需要继续从后台获取的,那么就需要后台返回对应的接口和数据,
vue 动态创建mock数据
- 准备测试数据 test.json
{
"/hnhsav/ui/v1/myAlarm/getAlarmColor": {
"code": "0",
"msg": "success",
"data": {
"red": 1000,
"yellow": 8,
"green": 6,
"blue": 4
}
},
"/hnhsav/ui/v1/myAlarm/regionTop10": {
"code": "0",
"msg": "success",
"data": [
{
"name": "测试区域",
"count": 34419,
"sortNum": 1
}
]
},
}
- 使用vue关联返回的数据
<template>
<div class="index">
<Panel v-if="isShowContent"/>
</div>
</template>
<script>
import Panel from './panel'
import axios from 'axios'
import Mock from 'mockjs'
export default {
components: {
Panel
},
data () {
return {
isShowContent: false
}
},
mounted () {
this.getMockConfig()
},
methods: {
async getMockConfig () {
const { data } = await axios.get('/hnhsav/static/data/urlData.json')
// console.log(data)
// console.log(typeof data)
// console.log(JSON.parse(data))
// // debugger
Object.keys(data).forEach(key => {
Mock.mock(key, data[key])
})
this.isShowContent = true
}
}
}
</script>
六、 Mock 关键API介绍说明
6.1 Mock.mock( rurl, rtype, template ) 函数介绍
- Mock.mock( rurl, function( options ) )
记录用于生成响应数据的函数。当拦截到匹配 rurl 的 Ajax 请求时,函数 function(options) 将被执行,并把执行结果作为响应数据返回。
- Mock.mock( rurl, rtype, function( options ) )
记录用于生成响应数据的函数。当拦截到匹配 rurl 和 rtype 的 Ajax 请求时,函数 function(options) 将被执行,并把执行结果作为响应数据返回。
-
rurl 可选。
表示需要拦截的 URL,可以是 URL 字符串或 URL 正则。例如 //domain/list.json/、‘/domian/list.json’。 -
rtype 可选。
表示需要拦截的 Ajax 请求类型。例如 GET、POST、PUT、DELETE 等。 -
template 可选。
表示数据模板,可以是对象或字符串。例如 { ‘data|1-10’:[{}] }、‘@EMAIL’。
6.2 怎么区分不同的参数呢?
在工作中想使用POST请求方式获取数据字典的数据,由于不同的数据字典需要传递不同的参数,但是接口是同一个,这样就尴尬了,因为URL是固定的,那么怎么区分不同的参数呢?
6.3 POST 请求,相同的URL不同的参数
import Mock from 'mockjs'
// 证件类型
Mock.mock(`/${ctx}/api/v1/dict/getDictItem`, function (options) {
console.log(options)
if (options.body === '["aaa"]') {
return {
code: '0',
msg: 'success',
data: {
msg: '我是A请求'
}
}
} else if (options.body === '["bbb"]') {
return {
code: '0',
msg: 'success',
data: {
msg: '我是B请求'
}
}
}
})
6.4 GET 请求,相同的URL不同的参数
参照 上面POST的请求,URL地址相同,但是参数不同的解决办法
Mock.mock(RegExp(url + ".*"), "get", mockData);
import Mock from 'mockjs'
Mock.mock(RegExp('/hsdavs/hydrology/v1/getStationData\\?stcd\\=' + '.*'), {
'code': '0',
'msg': 'success',
'data': {
'wlTime': '2024-04-17T11:20:00.000+08:00',
'wlData': '34.25',
'flowTime': '2024-04-15T08:00:00.000+08:00',
'flowData': '161.0',
'jstime': '2024-04-17T00:00:00.000+08:00',
'jsdata': 'null'
}
})
Mock.mock(RegExp('/hsdavs/hydrology/v1/platform' + '.*'), {
'code': '0',
'msg': 'success',
'data': {
'platformIp': '10.43.216.17',
'platformProtocol': 'https',
'platformPort': '10443',
'multiRouteId': 0,
'platformLanguage': 'zh_CN',
'token': 'SElLIFhPTWdtL1RIa2g2cXNRRnc6R0xNQ1VBeUlpVnhJOTVLQU1mdmlZbnhTWTR4Nk5iU3djdFNrM2grNXdJbz06MTcxMzMyNDcxMjkxNQ=='
}
})
Mock.mock(RegExp('/label/v1/startTestFlow' + '.*'), function (options) {
return {
'code': '0',
'msg': 'SUCCESS',
'data': null
}
})
注意: 如果正则表达式中有 ? 则需要转义字符,例如
七、Mock.mock(url, template)的原理
查看了mockjs的源码,实际上是内部是采用ajax劫持 —— 将本地的
if (XHR) window.XMLHttpRequest = XHR // 拦截 XHR
- 根据
url+type
作为关键字,缓存 template,即根据url+type
查询数据 - 如果查询条件匹配得到,则使用缓存的数据,如果查询不到,则使用原生的ajax根据URL地址请求服务器
错误案例解析
api相对路径请求,mockjs查询不到数据
- 定义模拟的数据
import Mock from 'mockjs'
Mock.mock('https://11.11.11.11/api/aaa/bbb', { 'code': '0', 'msg': 'success', 'data': 'dlsajfkdas' })
- 调用的API
import axios from 'axios'
export const hungupService = () => {
return axios({
method: 'post',
url: 'api/aaa/bbb'
})
}
-
结论
浏览器还是向服务器发送了请求 -
错误理解
调用 ‘api/aaa/bbb’ 这个uri,浏览器默认会添加上下文转换为’https://11.11.11.11/api/aaa/bbb’这个请求,因此mock模拟数据的时候直接填写全路径 -
问题解析
因为mockjs判断是否拦截ajax请求,是根据 uri+type这两个因素来决定的,因为下面调用的是’api/aaa/bbb’,因此mockjs会根据这个’api/aaa/bbb’去查找,而不是根据浏览器转换之后的’https://11.11.11.11/api/aaa/bbb’全路径匹配
解决办法
a. 改变axios的uri请求
Mock.mock('https://11.11.11.11/api/aaa/bbb', { 'code': '0', 'msg': 'success', 'data': 'dlsajfkdas' })
匹配
import axios from 'axios'
export const hungupService = () => {
return axios({
method: 'post',
url: 'https://11.11.11.11/api/aaa/bbb'
})
}
b. 改变mockjs监听的数据
Mock.mock('api/aaa/bbb', { 'code': '0', 'msg': 'success', 'data': 'dlsajfkdas' })
匹配
import axios from 'axios'
export const hungupService = () => {
return axios({
method: 'post',
url: 'api/aaa/bbb'
})
}
axios request请求拦截?mock地址到底几何?
问题描述
由于公司的请求已经被封装,默认会统一管理上下文,即业务只需要管理 api 地址,通过拦截器统一添加上下文,如果根据上面的案例来定义mock的URL,则拦截不到请求
- 定义拦截器
// 请求拦截器
http.interceptors.request.use(function (config) {
config.baseURL = 'myproject'
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
- 定义的mock URL 地址
Mock.mock('api/aaa/bbb', { 'code': '0', 'msg': 'success', 'data': 'dlsajfkdas' })
- 发送请求的地址
import axios from 'axios'
export const hungupService = () => {
return axios({
method: 'post',
url: 'api/aaa/bbb'
})
}
-
结论现象
mock无法拦截到定义的URL 地址 -
问题分析
- 因为axios在发送请求的时候,首先被定义的拦截函数拦截,会自动的在请求前面添加“上下文”(工程名),导致URL地址改变
- 由于URL在前面添加了上下文,导致mockjs认为我们没有定义mockURL, 因此就放过了这个请求,从而向服务器获取
-
解决办法 —— 定义的mockURL统一添加上下文
import Mock from 'mockjs'
const ctx = process.env.VUE_APP_CONTEXT
// 获取组织接口
Mock.mock(`/${ctx}/api/aaa/bbb', { 'code': '0', 'msg': 'success', 'data': 'dlsajfkdas' })
八、源码探究
8.1 Mock.mock 函数的多样性(自由选择请求方式)
// 避免循环依赖
if (XHR) XHR.Mock = Mock
console.log(XHR)
debugger
/*
* Mock.mock( template )
* Mock.mock( function() )
* Mock.mock( rurl, template )
* Mock.mock( rurl, function(options) )
* Mock.mock( rurl, rtype, template )
* Mock.mock( rurl, rtype, function(options) )
根据数据模板生成模拟数据。
*/
Mock.mock = function(rurl, rtype, template) {
// Mock.mock(template)
if (arguments.length === 1) {
return Handler.gen(rurl)
}
// Mock.mock(rurl, template)
if (arguments.length === 2) {
template = rtype
rtype = undefined
}
// 拦截 XHR
if (XHR) window.XMLHttpRequest = XHR
console.log(rurl)
debugger
Mock._mocked[rurl + (rtype || '')] = {
rurl: rurl,
rtype: rtype,
template: template
}
return Mock
}
劫持 XMLHttpRequest
原生 XMLHttpRequest 发送ajax请求的步骤
- 创建 XMLHttpRequest 对象
- 调用 open 方法,确定请求的方式 和 URL 地址
- 调用 send 方法发送请求
// 原生ajax实现,非常的简单
function ajax() {
var xhr = new XMLHttpRequest();
xhr.open('get', 'http:localhost:8080/readNum');
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
console.log('success', xhr.responseText);
} else {
console.log('error', xhr.responseText);
}
}
}
Mock.xhr 劫持的过程
按照原生ajax 发送请求的过程,mock也做了相同的步骤,根据步骤我找出关键代码
- 创建 XMLHttpRequest 对象
function MockXMLHttpRequest() {
// 初始化 custom 对象,用于存储自定义属性
this.custom = {
events: {},
requestHeaders: {},
responseHeaders: {}
}
console.log("我是第一步")
}
- 调用 open 方法,确定请求的方式 和 URL 地址
// https://xhr.spec.whatwg.org/#the-open()-method
// Sets the request method, request URL, and synchronous flag.
open: function(method, url, async, username, password) {
var that = this
console.log("我是第二步")
debugger
Util.extend(this.custom, {
method: method,
url: url,
async: typeof async === 'boolean' ? async : true,
username: username,
password: password,
options: {
url: url,
type: method
}
})
this.custom.timeout = function(timeout) {
if (typeof timeout === 'number') return timeout
if (typeof timeout === 'string' && !~timeout.indexOf('-')) return parseInt(timeout, 10)
if (typeof timeout === 'string' && ~timeout.indexOf('-')) {
var tmp = timeout.split('-')
var min = parseInt(tmp[0], 10)
var max = parseInt(tmp[1], 10)
return Math.round(Math.random() * (max - min)) + min
}
}(MockXMLHttpRequest._settings.timeout)
// 查找与请求参数匹配的数据模板 —— 查找是否有定义的mock数据
var item = find(this.custom.options)
function handle(event) {
// 同步属性 NativeXMLHttpRequest => MockXMLHttpRequest
for (var i = 0; i < XHR_RESPONSE_PROPERTIES.length; i++) {
try {
that[XHR_RESPONSE_PROPERTIES[i]] = xhr[XHR_RESPONSE_PROPERTIES[i]]
} catch (e) {}
}
// 触发 MockXMLHttpRequest 上的同名事件
that.dispatchEvent(new Event(event.type /*, false, false, that*/ ))
}
// 如果未找到匹配的数据模板,则采用原生 XHR 发送请求。
if (!item) {
// 创建原生 XHR 对象,调用原生 open(),监听所有原生事件
var xhr = createNativeXMLHttpRequest()
this.custom.xhr = xhr
// 初始化所有事件,用于监听原生 XHR 对象的事件
for (var i = 0; i < XHR_EVENTS.length; i++) {
xhr.addEventListener(XHR_EVENTS[i], handle)
}
// xhr.open()
if (username) xhr.open(method, url, async, username, password)
else xhr.open(method, url, async)
// 同步属性 MockXMLHttpRequest => NativeXMLHttpRequest
for (var j = 0; j < XHR_REQUEST_PROPERTIES.length; j++) {
try {
xhr[XHR_REQUEST_PROPERTIES[j]] = that[XHR_REQUEST_PROPERTIES[j]]
} catch (e) {}
}
return
}
// 找到了匹配的数据模板,开始拦截 XHR 请求
this.match = true
this.custom.template = item
this.readyState = MockXMLHttpRequest.OPENED
this.dispatchEvent(new Event('readystatechange' /*, false, false, this*/ ))
},
根据find方法判断是否用户自定义了mock数据的URL地址
// 查找与请求参数匹配的数据模板:URL,Type
function find(options) {
console.log("我是第三步")
for (var sUrlType in MockXMLHttpRequest.Mock._mocked) {
var item = MockXMLHttpRequest.Mock._mocked[sUrlType]
if (
(!item.rurl || match(item.rurl, options.url)) &&
(!item.rtype || match(item.rtype, options.type.toLowerCase()))
) {
// console.log('[mock]', options.url, '>', item.rurl)
return item
}
}
function match(expected, actual) {
if (Util.type(expected) === 'string') {
return expected === actual
}
if (Util.type(expected) === 'regexp') {
return expected.test(actual)
}
}
}
- 调用 send 方法发送请求
// https://xhr.spec.whatwg.org/#the-send()-method
// Initiates the request.
send: function send(data) {
var that = this
this.custom.options.body = data
// 原生 XHR
if (!this.match) {
this.custom.xhr.send(data)
return
}
// 拦截 XHR
// X-Requested-With header
this.setRequestHeader('X-Requested-With', 'MockXMLHttpRequest')
// loadstart The fetch initiates.
this.dispatchEvent(new Event('loadstart' /*, false, false, this*/ ))
if (this.custom.async) setTimeout(done, this.custom.timeout) // 异步
else done() // 同步
function done() {
that.readyState = MockXMLHttpRequest.HEADERS_RECEIVED
that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
that.readyState = MockXMLHttpRequest.LOADING
that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
that.status = 200
that.statusText = HTTP_STATUS_CODES[200]
// fix #92 #93 by @qddegtya
that.response = that.responseText = JSON.stringify(
convert(that.custom.template, that.custom.options),
null, 4
)
that.readyState = MockXMLHttpRequest.DONE
that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
that.dispatchEvent(new Event('load' /*, false, false, that*/ ));
that.dispatchEvent(new Event('loadend' /*, false, false, that*/ ));
}
},