侧边栏壁纸
  • 累计撰写 16 篇文章
  • 累计创建 52 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

前端使用原生请求下载文件,并响应接口返回的 Blob 和 JSON 两种格式数据

Stone
2022-06-29 / 0 评论 / 0 点赞 / 458 阅读 / 1,295 字

前言

很久没写前端代码,这次写了个简单的证书管理平台,后端用 Spring Boot,前端用 Thymeleaf,其中证书生成接口涉及到下载功能,下载的证书竟然在客户端无法使用,看了一下文件大小,与用 Postman 请求和浏览器 url 直接请求得到的大小不一致,那就只能面向搜索引擎一波了。

最后虽然解决了问题,但是为什么会有这种情况产生,由于学艺不精暂时不知道为什么,也没搜出个所以然来,这里先做个记录,有懂得小伙伴欢迎留言探讨,如果哪里写的不对的还请指出。

后端接口

Controller 层

@GetMapping("generate")
public void generateLicense(@RequestParam Integer id, HttpServletResponse response) throws Exception {
    LicenseParam licenseParam = licenseService.getLicenseInfo(id).getData();
    if (ObjectUtils.isEmpty(licenseParam)) {
        throw new Exception("证书生成失败! 未找到证书信息!");
    }
    long expiryTime = licenseParam.getExpiryTime().getTime();
    if (expiryTime < System.currentTimeMillis()) {
        throw new Exception("证书生成失败! 证书失效时间不应晚于当前时间!");
    }
    if ("user".equalsIgnoreCase(licenseParam.getConsumerType()) && 1 != licenseParam.getConsumerAmount()) {
        throw new Exception("证书生成失败! 使用者类型为“用户”时, 使用者数量只能为 1 !");
    }
    licenseService.generateLicense(id, response);
}

Service 层

/**
 * 生成证书
 *
 * @param id       编号
 * @param response response
 */
public void generateLicense(Integer id, HttpServletResponse response) {
    LicenseParam licenseParam = licenseParamMapper.selectLicenseParamById(id);
    LicenseCreatorParam creatorParam = ConvertUtils.sourceToTarget(licenseParam, LicenseCreatorParam.class);
    if (ObjectUtils.isNotEmpty(licenseParam.getLicenseModel())) {
        LicenseCheckModel checkModel = ConvertUtils.sourceToTarget(licenseParam.getLicenseModel(), LicenseCheckModel.class);
        creatorParam.setLicenseCheckModel(checkModel);
    }
    creatorParam.setPrivateAlias(privateAlias);
    creatorParam.setKeyPass(keyPass);
    creatorParam.setStorePass(storePass);
    creatorParam.setPrivateKeysStorePath("privateKeys.keystore");
    try (OutputStream outputStream = response.getOutputStream()
    ) {
        LicenseCreator licenseCreator = new LicenseCreator(creatorParam);
        byte[] licenseBytes = licenseCreator.createLicense();
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename=\"license.lic\"");
        response.setHeader("Accept-Ranges", "bytes");
        outputStream.write(licenseBytes);
        outputStream.flush();
    } catch (IOException e) {
        log.error("generate error : ", e);
    }
}

后端代码没有什么可说的,可用 POST 也可用 GET ,这里我用的是 GET,并且我需要返回一些校验的信息给前端。

前端代码

原有写法

这里使用了比较熟悉的 jQuery Ajax,并且标记 responseType: 'blob',同时在 complete 事件中使用 XMLHttpRequest.responseJSON.message 展示后端在 code 为 500 的时候返回的错误信息。

function generate(id) {
    $.ajax({
        type: "get",
        url: prefix + "/generate",
        responseType: 'blob',
        data: {
            "id": id
        },
        success: function(data) {
            if (!data) {
                $.modal.alertError("证书生成失败!");
                return;
            }
            let url = window.URL.createObjectURL(new Blob([data], {type:'application/octet-stream'}))
            let link = document.createElement('a')
            link.style.display = 'none'
            link.href = url
            link.setAttribute('download', 'license.lic')
            document.body.appendChild(link)
            link.click()
            // 释放URL对象所占资源
            window.URL.revokeObjectURL(url)
            // 用完即删
            document.body.removeChild(link)
        },
        complete: function(XMLHttpRequest, textStatus) {
            if (textStatus == 'timeout') {
                $.modal.alertWarning("服务器超时,请稍后再试!");
                $.modal.enable();
                $.modal.closeLoading();
            } else if (textStatus == "parsererror" || textStatus == "error") {
                $.modal.alertWarning(XMLHttpRequest.responseJSON.message != '' ? XMLHttpRequest.responseJSON.message : "服务器错误,请联系管理员!");
                $.modal.enable();
                $.modal.closeLoading();
            }
        }
    });
}

修正后写法

改为原生的 XMLHttpRequest,在 onreadystatechange 事件中处理结果,并展示后端在 code 为 500 的时候返回的错误信息。

function generate(id) {
    let url = prefix + "/generate?id=" + id;
    let xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.responseType = "blob";
    // 定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
    xhr.onload = function () {
        // 请求完成
        if (this.status === 200) {
            // 返回200
            let blob = this.response;
            let reader = new FileReader();
            reader.readAsDataURL(blob);
            reader.onload = function (e) {
                // 转换完成,创建一个a标签用于下载
                let link = document.createElement('a');
                link.download = 'license.lic';
                link.href = e.target.result;
                $("body").append(link);    // 修复firefox中无法触发click
                link.click();
                $(link).remove();
            }
        }
    };
    // 发送ajax请求
    xhr.send();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 500) {
            let reader = new FileReader()
            reader.onload = function () {
                let jsonData = JSON.parse(reader.result);
                let message = jsonData.message
                $.modal.alertWarning(_isBlank(message) ? "服务器错误,请联系管理员!" : message);
                $.modal.enable();
                $.modal.closeLoading();
            };
            reader.readAsText(this.response);
        }
    };
}

解决过程

比对结果

页面调试,先打印数据看看,这样更加直观

jQuery Ajax 请求代码

直接打印结果集,是一堆乱码,这里使用 new Blob 方法打印

jQuery Ajax 请求结果

可以看到大小为 1563,比接口直接请求得到的数据流大了快一倍,这样生成的证书肯定是不能使用的

XMLHttpRequest 请求代码

改用原生方式请求,并打印结果集

XMLHttpRequest 请求结果

可以看到大小为 864,与接口直接请求得到的数据流一致!

处理后端返回的错误信息

之前用 jQuery Ajax 时在 complete 事件中使用 XMLHttpRequest.responseJSON.message 就可以直接展示了,但是用原生请求的时候,XMLHttpRequest 只有 responseText 属性,并没有 responseJSON 之类的属性

onreadystatechange 事件请求代码

在控制台打印 response 属性,为啥打印这个,只是因为返回值写着 any

onreadystatechange 结果展示

可以看到 type 是 application/json ,不过这也写着是 Blob 类型,那就用上面的方法写一下,打印出来没啥问题

xhr.onreadystatechange = function () {
	if (xhr.readyState === 4 && xhr.status === 500) {
		let reader = new FileReader()
		reader.onload = function () {
			let jsonData = JSON.parse(reader.result);
			let message = jsonData.message
			$.modal.alertWarning(_isBlank(message) ? "服务器错误,请联系管理员!" : message);
			$.modal.enable();
			$.modal.closeLoading();
		};
		reader.readAsText(this.response);
	}
};
0

评论区