七的博客

Angular4+Mock.js导致的文件上传错误

JavaScript

Angular4+Mock.js导致的文件上传错误

1. 背景

环境: Angular 4 + PrimeNG + Mock.js

问题: 在项目一个功能的文件上传表单中 , 使用的是 p-fileUpload 组件 ,点击文件上传后一直报错。报错的提示是 xhr_1.upload.addEventListener is not a function

涉及到的代码大致如下(已对关键字以及业务逻辑做过处理以及简化):

    <p-fileUpload ([ngModel])="files" customUpload="true" (uploadHandler)="onUpload($event)">
    </p-fileUpload>
import {
    Http,
    Request,
    Headers,
    RequestOptions,
    RequestOptionsArgs,
    RequestMethod,
    Response,
    HttpModule
}
from '@angular/http';

@Injectable()
export class UploadFileComponent {

    constructor(private http: Http) {}

    onUpload(event) {
        const formData = new FormData();
        formData.append('excel', event.files[0]);

        // 发送请求上传文件
        this.post(`http: //xxx.xxx.xxx.xxx:10001/upload/`, formData)
        .map((res) = >{
            return res.json();
        },
        (err) = >{
            console.error(err);
        });
    }

    public post(url: string, body: any, options ? :RequestOptionsArgs) : Observable < Response > {
        let options = new RequestOptions({
            body: body,
            method: RequestMethod.Post,
            url: url
        });
        if (additionalOptions) {
            options = options.merge(additionalOptions);
        }
        this.request(new Request(this.mergeOptions(options, this.defOpts)));
    }

}

逻辑还是比较简单,就是使用了 p-fileUpload 组件,然后上传绑定的是 onUpload() 方法。 onUpload() 方法就是调用 Angular 内置的 http 组件进行请求发送。

而且这个代码以前运行的也是正常的。

2. 初步排查

看了下控制台的报错, 提示的是 xhr_1.upload.addEventListener is not a function 。 堆栈信息如下:

FileUpload.html:8 ERROR TypeError: xhr_1.upload.addEventListener is not a function
    at FileUpload.upload (fileupload.js:186)
    at Object.eval [as handleEvent] (FileUpload.html:8)
    at handleEvent (core.js:13532)
    at callWithDebugContext (core.js:15041)
    at Object.debugHandleEvent [as handleEvent] (core.js:14628)
    at dispatchEvent (core.js:9944)
    at eval (core.js:10569)
    at HTMLButtonElement.eval (platform-browser.js:2628)
    at ZoneDelegate.invokeTask (zone.js:421)
    at Object.onInvokeTask (core.js:4724)

一眼看过去,这个错误的提示信息还是挺明显的。调用 xhr_1.upload.addEventListener 的时候提示 addEventListener 不是一个函数。

看名字的话,应该是对 xhr 的一个封装对象。 xhr 全称是 XMLHttpRequest ,这是一种用于浏览器与服务器进行异步通信的 JavaScript 对象。 这是一个 JavaScript 内置的对象,可以在这里查看到 https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/upload

The XMLHttpRequest upload property returns an XMLHttpRequestUpload object that can be observed to monitor an upload’s progress.

XMLHttpRequest 上传属性返回一个 XMLHttpRequestUpload 对象,可以观察该对象来监视上传的进度。

3. 断点调试

通过常识去猜测错误看起来不太管用,这个时候只能选择去断点调试。

一路 Debug 进去,总算是找到了点线索。在 fileupload.js 里面,我注意到 xhr 这个对象已经变成了 MockXMLHttpRequest , 不是预期的 **XMLHttpRequest ** 。

断点查看

查看下 upload 属性的内容:

upload属性

突然想起来项目里使用了 Mock.js ,Mock.js 是一个用于前端开发中生成随机数据和拦截 Ajax 请求的 JavaScript 库。这个库在我们项目中主要作用是帮助模拟服务器端的行为和返回数据,允许前端同事进行独立测试。

那么知道了是 Mock.js 导致的,有个暴力的方式就是直接屏蔽,屏蔽后确实是好使的。

这个问题上我也直接去 Mock.js 的 Github 上提了个 issue,但是作者很久都没回复问题了。 项目从 2016年开始就没怎么更新迭代,同时 issue 列表很多都是没有回复跟关闭的。

4. 具体问题分析

既然 Mock.js 是拦截了 XMLHttpRequest ,那自然是需要重写里面的方法来模拟行为的。

我猜测是没有完整模拟所有的 XHR 功能,特别是与文件上传相关的部分。直接去看看它的源码,看看是不是缺了什么。

Github 地址: https://github.com/nuysoft/Mock

查找 xhr 相关的文件名或者是文件夹,最终在 src/mock/xhr/xhr.js 找到了相关的覆盖逻辑:

/*
    ## MockXMLHttpRequest

    期望的功能:
    1. 完整地覆盖原生 XHR 的行为
    2. 完整地模拟原生 XHR 的行为
    3. 在发起请求时,自动检测是否需要拦截
    4. 如果不必拦截,则执行原生 XHR 的行为
    5. 如果需要拦截,则执行虚拟 XHR 的行为
    6. 兼容 XMLHttpRequest 和 ActiveXObject
        new window.XMLHttpRequest()
        new window.ActiveXObject("Microsoft.XMLHTTP")
*/

// .... 省略

// 初始化 Request 相关的属性和方法
Util.extend(MockXMLHttpRequest.prototype, {
    // .... 省略
    
    upload: {},
    // 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
        }
}        
    

可以看到,upload 属性是空的,这样调用 xhr.upload.addEventListener() 肯定是会出现错误的。

回到 Mock.js 的设计理念, Mock.js 的核心功能是 拦截网络请求并提供模拟响应,它对 XMLHttpRequest 的一些功能的支持不是完全的,就比如现在遇到的这个文件上传。

5. 解决方式

上面说了,我们项目中是采取临时屏蔽 Mock.js 来解决这个问题的。

因为是在开发环境,所以大家直接本地修改下即可,成本最低。

但是如果我们想从根源上解决这个问题,也可以有以下办法:

  • 修改 node_modeule 下 mock.js 的逻辑,给 upload 属性指向原生的 xhr.upload 属性即可。
  MockXMLHttpRequest.prototype.upload = 原生xhr.upload;
  • 直接全局修改
  var backupXhr = new window._XMLHttpRequest();
  window.XMLHttpRequest.prototype.upload = backupXhr.upload;

参考链接