JavaScript下载文件(简单模式、跨域问题、文件压缩)

简介

相信各位开发朋友都遇到过下载的文件的需求,有的非常简单,基本链接的形式就可以。

有的就比较复杂,涉及跨域和压缩文件,例如,文件在OSS中,有的oss不支持压缩文件,要下10个文件就得弹10个下载出来。

业务老师多半是没有办法接受这种情况,怎么处理呢?

这就涉及到跨域获取文件并压缩文件了。

本文会介绍一下简单下载和下载OSS文件并压缩。

简单文件下载

首先我们看一些2种简单下载方式

通过模拟form表单提交

function downloadRemoteFile(url,materialId) {
    var body = document.getElementsByTagName('body')[0];
    var form = document.createElement('form');
    form.method = 'POST';
    form.action = url;
    var param = document.createElement('input');
    param.type = "hidden";
    param.name = "materialId";
    param.value = materialId;
    form.appendChild(param);
    body.appendChild(form);
    form.submit();
    body.removeChild(form);
}

上面这种方式:

  1. 优点是简单
  2. 缺点是错误不友好,出错了,没有提示信息

如果希望错误信息友好一点,可以通过XMLHttpRequest方式。

通过XMLHttpRequest方式

function downloadRemoteFileXMLHttpRequest(url,materialId) {
    console.log("${downloadUrl}" + " " + materialId);
    var xhr = new XMLHttpRequest();
    xhr.open("POST", url, true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
    xhr.onload = function () {
        if (xhr.status === 200) {
            if (xhr.response == null || xhr.response == "" || xhr.response == undefined) {
                alert("下载文件出错,请检查文件是否存在:" + materialId);
                return;
            }
            // 获取判断Content-Type
            // var contentType = xhr.getResponseHeader("Content-Type");
            var blob = new Blob([xhr.response], { type: "application/octet-stream" });
            var fileName = getFileNameFromResponseHeader(xhr);
            var link = document.createElement("a");
            link.href = window.URL.createObjectURL(blob);
            link.download = fileName;
            link.click();
            link.remove();
            window.URL.revokeObjectURL(link.href);
        } else {
            alert("下载响应异常");
            console.log(xhr);
        }
    };
    xhr.send("materialId=" + materialId);
}

function getFileNameFromResponseHeader(xhr) {
    var contentDisposition = xhr.getResponseHeader("content-disposition")
    var matchResult = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
    if (matchResult != null && matchResult[1]) {
        return decodeURIComponent(matchResult[1].replace(/['"]/g, ""));
    }
    return "default-name";
}

通过XMLHttpRequest方式就灵活很多,虽然还是模拟了a链接,但是能先处理响应。

例如,下载后端可以能有一些预检查,如果预检查都没有通过,那么可能返回的就不是Blob文件,而是一个json。

通过XMLHttpRequest方式就可以通过判断ContentType内容,来获取文件流、或是json的内容,从而把错误信息比较友好的展示给用户。

对于后端下载接口,可能更好的方式是把信息写到header中,这样对于前端来说就能更好统一处理逻辑。

response.addHeader("success", "false");
response.addHeader("message", URLEncoder.encode("该数据您无权下载", StandardCharsets.UTF_8));

后端不同处理方式(仅仅演示,实际操作最后统一逻辑):

@RequestMapping("/download")
public void download(HttpServletResponse response, @RequestParam("fid") Long fid) throws IOException {
    if (fid < 0) {
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();
        Result<Void> result = ResultHelper.getFailResult("文件不存在");
        writer.write(JSON.toJSONString(result));
        return;
    }
    if (fid < 100) {
        response.addHeader("success", "false");
        String message = "该数据您无权下载";
        response.addHeader("message", URLEncoder.encode(message, StandardCharsets.UTF_8));
        return;
    }
    try (
            FileInputStream fis = new FileInputStream("F:\\tmp\\季报统计表.xlsx");
            OutputStream os = response.getOutputStream()
    ) {
        String fileName = URLEncoder.encode("季报统计表.xlsx",StandardCharsets.UTF_8);
        response.reset();
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
        response.addHeader("success", "true");
        IOUtils.copy(fis, os);
    }
}

跨域(oss)下载并压缩文件

下载并压缩文件主要有2个不好处理的问题:

  1. 跨域
  2. 文件流压缩

解决方案:

  1. 跨域问题主要是浏览器限制,可以通过后端添加header Access-Control-Allow-Origin或者直接设置浏览器处理
  2. 文件压缩可以使用jszip

完整示例

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>跨域下载</title>
	</head>
	<body>
		<button onclick="download()">下载</button>
	</body>
</html>
<script type="text/javascript" src="js/jszip.js"></script>
<script>
	function fetchFile(url, zip) {
		window.fetch(url, {
			method: "GET",
			// mode: 'no-cors',
			mode: 'cors' // 允许跨越
		}).then(response => {
			// no-cors 这里不执行
			return response.blob();
		}).catch(error => {
			console.log(error);
		});
	}
	// 下载文件
	function downloadFile(url, fileName) {
		const a = document.createElement('a')
		a.style.display = 'none'
		a.href = url
		a.download = fileName
		document.body.appendChild(a)
		a.click()
		document.body.removeChild(a)
	}

	function download() {
		const zip = new JSZip();

		// 从远程服务器获取文件
		const urls = [
			'http://127.0.0.1:8087/file/download2'
		];

		let map = new Map();
		let promises = [];
		var index = 0;
		urls.forEach(function(value, index, array) {
			// cors 表示可以跨越,服务端必须设置Access-Control-Allow-Origin
			//no-cors fetch不会执行then,拿不到文件
			var result = fetch(value, {
				mode: 'cors'
			}).then(response => response.blob());
			promises.push(result);
			map.set(index++, value);
		});

		Promise.all(promises)
			.then(results => {
				var flag = true;
				if (results.every(result => result !== undefined)) {
					flag = true;
				} else {
					flag = flase;
				}

				if(flag){
					index = 0;
					results.forEach(blob => {
					// promise 结果是按顺序返回的
						var url = map.get(index++);
						var filename = getFileName(url);
						zip.file(filename, blob, {
							binary: true
						});
					});
					zip.generateAsync({
						type: "blob"
					}).then(function(content) {
						const url = window.URL.createObjectURL(content);
						downloadFile(url, "result.zip");
					});
				}else{
					alert("有文件下载失败请重试!");
				}
			}).catch(error => console.error('Error:', error));
	}

	function getFileName(url) {
		var url = url.substring(url.lastIndexOf("/") + 1);
		var idx = url.lastIndexOf("?");
		if (idx > 0) {
			url = url.substring(0, idx);
		}
		return decodeURI(url);
	}
</script>

文件压缩

首先下载jszip的最新版本,注意使用新版本(3.10.x)和老版本的api差别较大。

jszip下载

function zip() {
    const zip = new JSZip();

    // 添加一个文本文件
    zip.file("Hello.txt", "Hello World\n");

    // 添加一个json文件
    let jsonData = {};
    const blob = new Blob([JSON.stringify(jsonData, null, 4)], {
        type: 'application/json'
    });
    zip.file('notes.json', blob);

    // 添加文件夹
    const folder = zip.folder("subdir");
    // 在文本文件中添加一个文本文件
    folder.file("Hi.txt", "Hi\n");

    // 除了blob类型,还可以设置为base64
    zip.generateAsync({
        type: "blob"
    }).then(function(content) {
        const url = window.URL.createObjectURL(content);
        downloadFile(url, "result.zip");
    });
}

// 下载文件
function downloadFile(url, fileName) {
    const a = document.createElement('a')
    a.style.display = 'none'
    a.href = url
    a.download = fileName
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
}

跨域设置

如果你看到一个类似于下面的错误,那是因为浏览器限制,不允许跨域。

跨域错误

Access to fetch at ‘http://127.0.0.1:8087/file/download2’ from origin ‘http://127.0.0.1:8848’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

解决这个问题的方法有2个:

  1. 服务端返回response的header Access-Control-Allow-Origin值类似于*
  2. 设置浏览器

设置服务器的方式很多,以springboot为例:

可以通过设置Filter:

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
//        res.setHeader("Access-Control-Allow-Headers", "Authorization");
        res.setHeader("Access-Control-Allow-Headers", "*");
        chain.doFilter(request, response);
    }
}

可以设置WebMvcConfigurer:

package vip.meet.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true)
                .allowedHeaders("*");
    }
}

可以直接在单个请求中设置:response.setHeader(“Access-Control-Allow-Origin”, “*”);

OSS中设置:

oss跨域设置

设置这些的目的只有一个就是然response中有Access-Control-Allow-Origin

response Access-Control-Allow-Origin

除了设置服务端,还可以设置浏览器,以Chrome为例:

浏览器跨域设置

设置浏览器的启动参数:

–disable-web-security --user-data-dir=D:\ChromeDevData

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值