如何二次封装 Axios 下载文件和上传文件?

点击上方 前端Q,关注公众号

回复加群,加入前端Q技术交流群

一 下载文件

正常情况下,我们用axios下载下来的文件是一个二进制的东西,需要处理才能成一个真真的文件。比如:我用express写一个接口,就是当页面访问的时候,下载这个美女图片。

服务端

c4289a79f97a12422d3dcecede4f2368.jpeg


客户端

4c7462cf9d51833d79ff3af269e80ff1.jpeg


浏览器console

02442d61f3eb4c1faddb446f3bc52cba.jpeg


上图里面,对象data里面放了很多二进制的东西,就是我们的文件内容,我们需要用Blob对象,把他转成Blob,然后用 window.URL.createObjectURL(blob) 给出电脑内存里面的url地址,然后创建一个a标签,执行他的click事件,浏览器就完成了自动下载。所以说:文件的自动下载,是前端自己封装的,不是后端做的。

说明:当设置responseType: 'blob'的时候,请求返回值里面的data本来就是Blob类型了,我们就不用再用 const blob = new Blob([response.data]);转化了。直接拿内存里面的url即可。

354be3b4c1844cf0d3ea52bf75145df8.jpeg


设置responseType: 'blob'的结果是

cb81cfdbed925d029a2b2f80696d193a.jpeg


一般为什么我们看不到它,就是因为有人在axios的二次封装里面,已经帮我们做好了。

下载文件的基础代码如下:

// axios发起下载文档请求
export const downloadDoc = (id: string) => {
  return request.get(`/downloadDoc?id=${id}`, {
   // 参考官方文档https://www.axios-http.cn/docs/req_config,responseType: 'blob'是浏览器专属
    responseType: 'blob'
  }) as Promise<Blob>
}
// 下载文件通用函数
export function download(filename: string, data: Blob): void {
  let downloadUrl = window.URL.createObjectURL(data) //创建下载的链接
  let anchor = document.createElement('a') // 通过a标签来下载
  anchor.href = downloadUrl
  anchor.download = filename // download属性决定下载文件名
  anchor.click() // //点击下载
  window.URL.revokeObjectURL(downloadUrl) // 下载后释放Blob对象
}

封装axios,这里面包含了大文件的下载,还有loading,当然你也可以把防抖,节流全部做进去,不过过度封装也会带来很多不必要的麻烦。

import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { Message } from "element-ui";
import { jumpLogin, downloadFile } from "@/utils";
import { Loading } from "element-ui";
import { ElLoadingComponent } from "element-ui/types/loading";
// import vm from "@/main";

let loadingInstance: ElLoadingComponent | null = null;
let requestNum = 0;

const addLoading = () => {
  // 增加loading 如果pending请求数量等于1,弹出loading, 防止重复弹出
  requestNum++;
  if (requestNum == 1) {
    loadingInstance = Loading.service({
      text: "正在努力加载中....",
      background: "rgba(0, 0, 0, 0)",
    });
  }
};

const cancelLoading = () => {
  // 取消loading 如果pending请求数量等于0,关闭loading
  requestNum--;
  if (requestNum === 0) loadingInstance?.close();
};

export const createAxiosByinterceptors = (
  config?: AxiosRequestConfig
): AxiosInstance => {
  const instance = axios.create({
    timeout: 1000,    //超时配置
    withCredentials: true,  //跨域携带cookie
    ...config,   // 自定义配置覆盖基本配置
  });

  // 添加请求拦截器
  instance.interceptors.request.use(
    function (config: any) {
      // 在发送请求之前做些什么
      const { loading = true } = config;
      console.log("config:", config);
      // config.headers.Authorization = vm.$Cookies.get("vue_admin_token");
      if (loading) addLoading();
      return config;
    },
    function (error) {
      // 对请求错误做些什么
      return Promise.reject(error);
    }
  );

  // 添加响应拦截器
  instance.interceptors.response.use(
    function (response) {
      // 对响应数据做点什么
      console.log("response:", response);
      const { loading = true } = response.config;
      if (loading) cancelLoading();
      const { code, data, message } = response.data;
      // config设置responseType为blob 处理文件下载
      if (response.data instanceof Blob) {
        return downloadFile(response);
      } else {
        if (code === 200) return data;
        else if (code === 401) {
          jumpLogin();
        } else {
          Message.error(message);
          return Promise.reject(response.data);
        }
      }
    },
    function (error) {
      // 对响应错误做点什么
      console.log("error-response:", error.response);
      console.log("error-config:", error.config);
      console.log("error-request:", error.request);
      const { loading = true } = error.config;
      if (loading) cancelLoading();
      if (error.response) {
        if (error.response.status === 401) {
          jumpLogin();
        }
      }
      Message.error(error?.response?.data?.message || "服务端异常");
      return Promise.reject(error);
    }
  );
  return instance;
};

utils里面的函数

src/utils/index.ts

import { Message } from "element-ui";
import { AxiosResponse } from "axios";
import vm from "@/main";

/**
 *   跳转登录
 */
export const jumpLogin = () => {
  vm.$Cookies.remove("vue_admin_token");
  vm.$router.push(`/login?redirect=${encodeURIComponent(vm.$route.fullPath)}`);
};

/**
 * 下载文件
 * @param response
 * @returns
 */
export const downloadFile = (response: AxiosResponse) => {
  console.log("response.data.type:", response.data.type);
  return new Promise((resolve, reject) => {
    const fileReader = new FileReader();
    fileReader.onload = function () {
      try {
        console.log("result:", this.result);
        const jsonData = JSON.parse((this as any).result); // 成功 说明是普通对象数据
        if (jsonData?.code !== 200) {
          Message.error(jsonData?.message ?? "请求失败");
          reject(jsonData);
        }
      } catch (err) {
        // 解析成对象失败,说明是正常的文件流
        const blob = new Blob([response.data]);
        // 本地保存文件
        const url = window.URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        const filename = response?.headers?.["content-disposition"]
          ?.split("filename*=")?.[1]
          ?.substr(7);
        link.setAttribute("download", decodeURI(filename));
        document.body.appendChild(link);
        link.click();
        resolve(response.data);
      }
    };
    fileReader.readAsText(response.data);
  });
};

axios的封装就是创建一个axios的实例,然后在请求拦截器和响应拦截器里面写入你要拦截的东西,比如loading,就是在请求里面拦截,错误显示��是在响应里面拦截的。

84f2643f564d5ed0b67018a173e36e0a.jpeg


当然,你在处理错误的时候,可以把所有的错误都列出来

export const CODE_MESSAGE: { [key: number]: string } = {
    200: '服务器成功返回请求的数据',
    201: '新建或修改数据成功',
    202: '一个请求已经进入后台排队(异步任务)',
    204: '删除数据成功',
    400: '发出的请求有错误,服务器没有进行新建或修改数据的操作',
    401: '用户没有权限(令牌、用户名、密码错误)',
    403: '用户得到授权,但是访问是被禁止的',
    404: '发出的请求针对的是不存在的记录,服务器没有进行操作',
    406: '请求的格式不可得',
    410: '请求的资源被永久删除,且不会再得到的',
    422: '当创建一个对象时,发生一个验证错误',
    500: '服务器发生错误,请检查服务器',
    502: '网关错误',
    503: '服务不可用,服务器暂时过载或维护',
    504: '网关超时'
  }

然后在响应拦截里面这样做:

6b5f97457975099ad0dabf75cb0cdf93.jpeg


二 上传文件

最简单的上传文件:在html里面写一个input标签,如下:

<input class="upload" type="file" ref="upload" accept="image/jpeg,image/jpg,image/png" id="fileUpload">

在js文件里面写:

const fileUpload = document.getElementById("fileUpload")
fileUpload.onchange = (e)=>{
  console.log( e.target.files[0])
}

有些人离开框架就不会写代码了。这真的很危险,操作如下:

c77c697ec72be392ce1eefa0701720e6.jpeg


a0db353d7c75775cdf6ce161481ff5e7.jpeg


当你看到index.js的时候,你会发现和写vue,react一样了,是不是?

现在启动页面,看看结果如下:上传一个文件以后,在开发者工具里面你就能看到上传的文件信息了

6ae3f3109fcc3cd5215587b511f91921.jpeg


如果咱们的数据是以form的形式传给后端,那么我们就要转化为formdata才行

const fileUpload = document.getElementById("fileUpload")

fileUpload.onchange = (e)=>{
  console.log( e.target.files[0])
  let file = e.target.files[0]
  // 实例化
  let formdata = new FormData()
  formdata.append('file', file)
}

现在我们写一下后端存储文件的代码,后端是express搭建的,直接用node ./server/index,js 就能启动,代码如下:

const express = require('express');
const multer = require('multer');
const app = express();
const port = process.env.PORT || 3002;


// 引入需要的模块

let path = require("path");
let fs = require("fs");
//app.use('/uploads',express.static("./upload")); // 托管静态资源
//1. 设置文件存放位置
let upload = multer({dest:'uploads'})
app.post('/upload',upload.single('file'),(req,res)=>{  // file 与前端input 的name属性一致
    res.setHeader('Access-Control-Allow-Origin', '*'); 
  // 2. 上传的文件信息保存在req.file属性中
    console.log(req.file)  
    let oldName = req.file.path; // 上传后默认的文件名 : 15daede910f2695c1352dccbb5c3e897
    let newName = 'uploads/'+req.file.originalname  // 指定文件路径和文件名
    // 3. 将上传后的文件重命名
    fs.renameSync(oldName, newName);
    // 4. 文件上传成功,返回上传成功后的文件路径
    res.send({
        err: null,   
        url: "http://localhost:3002/" +newName // 复制URL链接直接浏览器可以访问
    });
})

app.listen(port, () => {
    console.log(`已经启动服务,端口号是:${port}`)
})

启动后在vscode上就能看到信息如下:

eff3a1563a1cf16f0dfed78dab126787.jpeg


用axios请求,我们需要用POST方法,再配置headers,需要这个浏览器才知道是表单 headers: { 'Content-Type': 'multipart/form-data;charset=UTF-8' }如果不指定是form-data,那么默认就是json的,不信你是试试看如下:

4e83e71b90f6499f1fb1593ed06a102c.jpeg


所以需要在请求的时候手动设置下才行,设置完以后,发送请求,后端才能识别出来,你发送的是form数据,然后用具体的方式对待你传过来的文件。

const fileUpload = document.getElementById("fileUpload")
fileUpload.onchange = (e)=>{
  console.log(222222222222)
    // 获取file
    let file = e.target.files[0]
    // 实例化
    let formdata = new FormData()
    formdata.append('file', file)
    axios.post('http://localhost:3002/upload', formdata, {
        headers: {
          'Content-Type': 'multipart/form-data;charset=UTF-8'
        }
      })
      .then(response => {
        console.log('File uploaded successfully');
        // 处理响应数据
      })
      .catch(error => {
        console.log('Error uploading file');
        // 处理错误情况
      });
}

测试如下:

a6bd123107ab901251d6c65441174413.jpeg


3570640a11e433304ec9538c12178045.jpeg 看文件是不是存到uploads里面去了

bb47aafe3d42cbcff00f110dfae02f4c.jpeg


如果咱们是form上传,你就把input包在form里面。

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" />
    <button>提交</button>
</form>

上传的时候,如果要对axios进行封装,我们只需要将请求头的'Content-Type': 'multipart/form-data;charset=UTF-8'纳入axios的请求拦截器里面就好了。

c53875ebe31d7367bf9ec9b45bd2f7b2.jpeg


封装代码如下:

f7488531e1d3579547f0241f57b9719b.jpeg


836db497193f90018247ced0c112f3e7.jpeg


打包以后,我们接入看看。(你不打包也可以测试,是为了封装axios才打包的。)

1178a64cfe66fb8761e9d97b6ffdaf0a.jpeg


执行代码:

f5ac9e551b94de064d63bb462c9bb819.jpeg


这样一个简单粗暴的上传和下载就搞定了,从这些简单的代码里面我们既能看到它最原始的面目,还能从根本上弄透彻这些基础知识。为什么做前端这么多年,这些基础的东西一直都模模糊糊?就是因为前后端分离以后,我们缺少了对知识的整体把握,还有过度的封装很多人弄不清到底原理是什么。造成现在的局面,就是前端的夹层,很多人工作5年,却只有2年的前端经验。

178eb2c99a070622b10602814df53270.png

往期推荐

写给懒人的Vue3快速查阅宝典

cef14e54de94bc4b9efe656d295da1a7.png

如何实现一套完整的CI/CD?

e31c44b75cd20735bef1957148751395.png

把黑神话悟空视频设置为vscode背景,真的太炫酷了。

4567f2f2b81cdd3a6287e2626309f116.png


最后

  • 欢迎加我微信,拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

f2aeee70c994cf06cbbc7319b556e8b3.jpeg

1ad8311c32950455eb068af9436bb9e4.png

点个在看支持我吧

0e8abe0e3e4c605b56973f587df63bce.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值