记一次el-upload&mockjs的Q&A

911 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

背景

  1. 使用el-upload组件的默认上传
    • 上传时附带进度条效果
    • 上传时触发on-progress,即文件上传时的钩子
  2. 需模拟某些数据,利用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数据,后点击上传文件操作

会发现:

image.png

发送上传请求的对象来自于mock.js

Initiator 标记请求是由哪个对象或进程发起的(请求源)

对比未引入mockjs,即未出现问题时:

image.png

可得,定位到问题原因所在:

由mock.js触发的请求,会导致el-upload上传进度相关的功能无效

一点建议

为更好地观察上传进度条效果,开发者工具Network中选择Slow 3G

微信截图_20210928234357.png

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的输出:

image.png

  • 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的输出:

image.png

关于方案的修改

编写文章时,关于上述方案的代码是想当然的(本地却跑其他的,哈哈哈,求原谅)。发现了按照其进行,是会报错的:

微信截图_20211012213242.png

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: {…}}

链接传送门

elementUI el-upload上传组件没有进度条

vue项目中主要文件的加载顺序

Last but not least

如有不妥,请多指教~

如有不妥,请多指教~

如有不妥,请多指教~

重要的事情说3次