小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
背景
- 使用
el-upload
组件的默认上传- 上传时附带进度条效果
- 上传时触发on-progress,即文件上传时的钩子
- 需模拟某些数据,利用
mockjs
(传送门),实现类似调用接口时,返回响应数据(自定义数据)的效果
现根据1、2点开发:
- main.js
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
import API from "@/api/index.js";
Vue.prototype.$api = API;
- api/index.js
import axios from "axios";
import "@/mock/index.js";
axios.defaults.headers.post["Content-Type"] = "application/json";
const request = (url, data, type) => {
//封装请求……
};
export default {
toGetData: data => {
return request("/demo/data", data, "post");
}
}
- mock/index.js
import Mock from "mockjs";
Mock.mock("/demo/data", options => {
let params = JSON.parse(options.body);
// ……
});
Question
同时使用el-upload & mockjs,可能会遇到一个问题:
el-upload上传时无进度条效果,on-progress不触发!!!
将问题场景简化为下列代码:
- demoList.vue
<template>
<div>
<el-button type="primary" @click="handleGetData">获取Mock数据</el-button>
</div>
</template>
<script>
export default {
name: "DemoList",
methods: {
async handleGetData() {
let data = await this.$api.toGetData();
console.log(data);
},
},
};
</script>
- demoUpload.vue
<template>
<div>
<el-upload
class="upload-demo"
:action="action"
:on-progress="handleProgress"
>
<el-button type="primary">点击上传</el-button>
</el-upload>
</div>
</template>
<script>
export default {
name: "DemoUpload",
data() {
return {
action: "http://xxx.xxx.xxx.xxx:xxx/xxx/fileUpload"
};
},
methods: {
handleProgress(e, file, fileList) {
console.log("上传中", e, file, fileList);
},
},
};
</script>
- App.vue
<template>
<div id="app">
<DemoList/>
<DemoUpload />
</div>
</template>
<script>
import DemoList from './pages/demoList.vue'
import DemoUpload from './pages/demoUpload.vue'
export default {
name: 'App',
components: { DemoList, DemoUpload }
}
</script>
如何复现?答:先获取Mock数据,后点击上传文件操作
会发现:
发送上传请求的对象来自于mock.js
Initiator 标记请求是由哪个对象或进程发起的(请求源)
对比未引入mockjs,即未出现问题时:
可得,定位到问题原因所在:
由mock.js触发的请求,会导致el-upload上传进度相关的功能无效
一点建议
为更好地观察上传进度条效果,开发者工具Network中选择Slow 3G
Answer
el-upload
分析 el-upload 的源码,对应上传进度功能利用 XMLHttpRequest 的 upload 属性
详见 upload 目录中 ajax.js 文件:
const xhr = new XMLHttpRequest();
const action = option.action;
// xhr.upload:返回一个XMLHttpRequestUpload对象,用来表示上传的进度
if (xhr.upload) {
// 可被绑定在upload对象上的事件监听器:onprogress,表示数据传输进行中
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100;
}
option.onProgress(e);
};
}
mock.js
观察mockjs的源码,可知:
-
重写 XMLHttpRequest,重新声明一个XHR对象,即
MockXMLHttpRequest
-
MockXMLHttpRequest的upload属性为空
// node_modules\mockjs\src\mock\xhr\xhr.js
function MockXMLHttpRequest() {
// 初始化 custom 对象,用于存储自定义属性
this.custom = {
events: {},
requestHeaders: {},
responseHeaders: {}
}
}
// ……
Util.extend(MockXMLHttpRequest.prototype, {
// ……
upload: {},
// ……
})
// node_modules\mockjs\src\mock.js
var XHR
if (typeof window !== 'undefined') XHR = require('./mock/xhr')
// ……
Mock.mock = function(rurl, rtype, template) {
// ……
// 拦截 XHR
if (XHR) window.XMLHttpRequest = XHR
// ……
return Mock
}
解决方案
- main.js:对于全局声明:删除
$api
,添加$xhr
// import API from "@/api/index.js";
// Vue.prototype.$api = API;
Vue.prototype.$xhr = window.XMLHttpRequest; // 输出:ƒ XMLHttpRequest() { [native code]}
- demoList.vue:引入api/index.js,不影响mock模拟数据
<script>
import API from "@/api/index.js";
export default {
name: "DemoList",
methods: {
async handleGetData() {
let data = await API.toGetData();
console.log(data);
},
},
mounted() {
let xhr = new XMLHttpRequest();
console.log(xhr);
}
};
</script>
关于xhr的输出:
- demoUpload.vue:利用全局声明
$xhr
,重新定义window.XMLHttpRequest,避免mockjs其重写的XMLHttpRequest对象生效,上传进度有效
<script>
import Vue from 'vue';
window.XMLHttpRequest = Vue.prototype.$xhr;
export default {
// ……
mounted() {
let xhr = new XMLHttpRequest();
console.log(xhr);
}
};
</script>
关于xhr的输出:
关于方案的修改
编写文章时,关于上述方案的代码是想当然的(本地却跑其他的,哈哈哈,求原谅)。发现了按照其进行,是会报错的:
XMLHttpRequest is not a constructor ??? 哪里出现了问题呢
经过一轮排查,发现是js的执行顺序
的原因
会报错的原方案执行顺序:
demoList.vue中export外的js代码 ———— demoUpload.vue中export外的js代码 ———— App.vue中export外的js代码 ———— main.js ———— App.vue的created ———— demoList.vue的created ————demoUpload.vue的created
所以,为了保证执行顺序,避免XMLHttpRequest的覆盖,又满足两个功能的使用,现改为:
- main.js:删除关于
$xhr
的全局声明
// import API from "@/api/index.js";
// Vue.prototype.$api = API;
// Vue.prototype.$xhr = window.XMLHttpRequest;
- App.vue:注意引用顺序,让demoUpload.vue中export外的js代码
先于
demoList.vue中export外的js代码执行
<script>
import DemoUpload from './pages/demoUpload.vue'
import DemoList from './pages/demoList.vue'
</script>
- demoUpload.vue:利用window.XMLHttpRequest全局声明
$xhr
,demoList.vue中export外的js代码尚未执行,此时mockjs其重写的XMLHttpRequest暂未生效
<script>
import Vue from "vue";
Vue.prototype.$xhr = window.XMLHttpRequest;
export default {
// ……
mounted() {
window.XMLHttpRequest = this.$xhr;
let xhr = new XMLHttpRequest();
console.log(xhr);
}
};
</script>
输出:XMLHttpRequest {onreadystatechange: null, readyState: 0, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
- demoList.vue:引入了api/index.js,利用mockjs重写的window.XMLHttpRequest全局声明
$mockXhr
,当调用mock请求时重新赋值window.XMLHttpRequest为$mockXhr
(避免覆盖)
<script>
import API from "@/api/index.js";
import Vue from "vue";
Vue.prototype.$mockXhr = window.XMLHttpRequest;
export default {
name: "DemoList",
methods: {
async handleGetData() {
window.XMLHttpRequest = this.$mockXhr;
let xhr = new XMLHttpRequest();
console.log(xhr);
let data = await API.toGetData();
},
}
};
</script>
输出:MockXMLHttpRequest {custom: {…}}
链接传送门
Last but not least
如有不妥,请多指教~
如有不妥,请多指教~
如有不妥,请多指教~
重要的事情说3次