序言
昨天在写博客的个人动态页面,里面涉及到了图片上传。
之前我都是用的别人的插件和elementUI的upload组件。但是现在没法用了。
页面的效果差别有点大,如果改elementUI的样式,会很累。
这时候很愁人啊。(懒啊!)
这是页面开发的效果,很像qq空间的感觉。
一、选型
1、选择1:自己从新写系列。
我参考了(简称xhr)和Fetch
文档地址:
XMLHttpRequest:https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
fetch:https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
上面的xhr其实就是我们用的最多的http的核心函数,早年的jquery.ajax和如今的axios都是用的这个。
fetch,兼容性差点,但是是最新的标准函数。已经没有那么多乱七八糟的东西了。把xhr干成了ajax的样子。
总结:
xhr的情况下需要完整的自己写一遍ajax函数
fetch呢是新标准,缺点是ie全系不支持,并且edge也有部分不支持。
哎呀:就是懒啊。不想自己写。(而且:自己写封装要考虑好多东西)
2、选axios
这个吗,没什么多说的。肯定不会选jq。
而且我早先的时候自己封装过axios,形成了自己项目下的专属使用的ajax函数。所以这次的封装是在自己封装的ajax函数上进行复用。
这里放出下我现如今的整体的axios内部的情况。
主角是下半部分的axios.upload
"use strict"; import Vue from "vue"; import axios from "axios"; import VueAxios from "vue-axios"; import { Notification, Loading } from "element-ui"; import apiList from "./api-list"; //接口列表数据 const store = require("store"); import control from "@/common/control_center/index"; // axios全局导入设置 Vue.use(VueAxios, axios); Vue.prototype.$api = apiList; //将接口列表数据绑定到vue全局 //自定义消息提示函数信息 let customMsg = { //成功信息提示 sucIfno(info) { Notification({ title: "答对了!", type: "success", message: info }); }, //错误信息提示 errIfno(info) { Notification({ title: "答错了呢", type: "error", message: info }); } }; // 授权函数封装 const authorize = herders => { const user_info = store.get("user_info"); if (user_info && user_info.sign) { herders.Authorization = user_info.sign; return herders; } else { return herders; } }; // axios函数封装 const ajax = ({ url = "", loading = false, //加载拦截 baseURL = apiList.baseURL, data = {}, headers = { "Content-Type": "application/json;charset=UTF-8" }, //头部信息处理 method = "get", success = false, //成功信息提示 error = true, //错误信息提示 timeout = 1000 }) => { // 数据过滤,过滤字段中空数据等 const filter = record => { for (let key in record) { !record[key] && delete record[key]; } return record; }; //接口全局加载提示 let loadingInstance = ""; if (loading !== false) { loadingInstance = Loading.service({ lock: true, text: loading !== true ? loading : "努力加载中……", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.5)" }); } return new Promise((suc, err) => { // 预处理数据部分 method = method.toLocaleLowerCase(); //转化为小写 headers = authorize(headers); axios({ url: url, baseURL: baseURL, headers: headers, method: method, [method === "post" ? "data" : "params"]: filter(data), timeout: timeout }) .then(response => { loadingInstance && loadingInstance.close(); // 刷新口令以及接口判断是否退出登录 if (!control.refresh_sign_or_out(response)) { customMsg.errIfno("数据异常,退出登录"); err(response); } const res = response.data; //自定义成功失败处理,code值代表后端接口数据处理成功或者失败 // 后端返回格式 /*data = { code: 0, // 0成功,1失败 msg: "", // 错误信息 data: "" // 数据 };*/ if (res && res.code === 0) { success !== false && customMsg.sucIfno(success === true ? "信息处理成功" : success); suc(res); } else { error !== false && customMsg.errIfno(res.msg ? res.msg : "信息处理失败"); err(res); } }) .catch(e => { console.log(e); loadingInstance && loadingInstance.close(); error !== false ? customMsg.errIfno("接口异常") : false; //catch代表网络异常部分和后端返回结果无关 err(e); }); }); }; //暴露的ajax函数,进一步封装节流和防抖 let shakeTime = ""; axios.ajax = options => { //参数预处理 let shake = options.shake || false; //不等于false直接传true或者防抖时间 //防抖函数处理 if (shake === false) { //不进行防抖处理 return new Promise((suc, err) => { ajax(options) .then(e => { suc(e); }) .catch(e => { err(e); }); }); } else { //进行防抖处理 return new Promise((suc, err) => { shakeTime && clearTimeout(shakeTime); let callNow = !shakeTime; if (callNow) { ajax(options) .then(e => { suc(e); }) .catch(e => { err(e); }); } shakeTime = setTimeout( () => { shakeTime = null; }, //见注解 shake === true ? 700 : shake ); }); } }; axios.upload = options => { // 对uri地址进行数据拼接 const new_url = obj => { if (obj) { let fields = ""; for (let key in obj) { fields = fields + `${key}=${obj[key]}`; } return "?" + fields; } else { return ""; } }; options.fdata = options.fdata || ""; //文件上传的url拼接地址 options.success = options.success || "文件上传成功"; options.url = options.url + new_url(options.fdata); options.headers = options.headers || {}; let header = { "Content-Type": "multipart/form-data" }; for (let i in header) { options.headers[i] = header[i]; } options.method = "post"; options.multiple = options.multiple || false; //是否多文件,默认false //文件类型验证,注意传入数组,默认["image/jpeg", "image/png"] options.type = options.type || ["image/jpeg", "image/png"]; options.size = options.size || 0; //文件大小限制,默认0 options.max = options.max || 5; //最多上传几个文件 //文件验证处理 let input = document.createElement("input"); input.type = "file"; options.multiple ? (input.multiple = "multiple") : ""; input.click(); return new Promise((suc, err) => { let type = options.type; input.addEventListener("input", watchUpload, false); function watchUpload(event) { //移除监听 let remove = () => { input.removeEventListener("input", watchUpload, false); input = null; }; const file = event.path[0].files; const len = file.length; // 文件数量限制 if (len > options.max) { err(file); remove(); customMsg.errIfno("文件个数超过" + options.max); return false; } let formData = new FormData(); for (let i = 0; i < len; i++) { // 文件大小限制 if (options.size !== 0 && file[i].size / 1024 / 1024 > options.size) { err(file[i]); remove(); customMsg.errIfno(file[i].name + "文件超过指定大小"); return false; } // 文件类型限制 if (type.length > 0 && !type.includes(file[i].type)) { err(file); remove(); customMsg.errIfno(file[i].name + "文件类型为" + file[i].type); return false; } formData.append("dhtUpload", file[i], file[i].name); } options.data = formData; // 最终进行文件上传 options.baseURL = ""; ajax(options) .then(e => { suc(e); }) .catch(e => { err(e); }); } }); }; export default axios;
二、开始编写upload函数
注意,其实本身是直接用的axios效果没什么差别。不要被我自己封装的ajax迷糊了。
1、不在页面上展现input元素
这里的初衷就是我以函数的形式,而不在页面上存在input元素。我看网上很多教程都是预先定义好input节点的
这里我放出自己一个基于input的颜色选择器。文件的方式类似这个
//颜色选择器 colorSelect() { let input = document.createElement("input"); input.type = "color"; input.click(); input.addEventListener("input", watchColorPicker, false); function watchColorPicker(event) { //console.log(event.target.value); //移除监听 input.removeEventListener("input", watchColorPicker, false); input = ""; } }
2、先看下input监听取得的值是什么
我们要的是path里面的值。看看path里面
注意这里就一个input元素,我们取input中的files数组就是我们的文件了。
这里看看单个文件的情况
这里我们能看到每个文件的大小,类型的情况。这些在后面会使用到。比如我们限制文件上传个数,文件的大小和文件类型
3、了解FormData()
这里我直接放MDN文档了。
https://developer.mozilla.org/zh-CN/docs/Web/API/FormData/Using_FormData_Objects
其实我们只需要知道,是下面这样用的就行了。
let formData = new FormData(); formData.append("dhtUpload", file[i], file[i].name);
append参数:文件名称,文件,文件名称
这里其实我没太理解
放一下elementUI源码中的一个情况。
formData.append(option.filename, option.file, option.file.name);
具体没太懂。
4、了解下ajax函数的情况
const ajax = ({ url = "", loading = false, //加载拦截 baseURL = apiList.baseURL, data = {}, headers = { "Content-Type": "application/json;charset=UTF-8" }, //头部信息处理 method = "get", success = false, //成功信息提示 error = true, //错误信息提示 timeout = 1000 })
上面就是我的ajax函数的参数。
5、upload函数的参数预处理
// 对uri地址进行数据拼接 const new_url = obj => { if (obj) { let fields = ""; for (let key in obj) { fields = fields + `${key}=${obj[key]}`; } return "?" + fields; } else { return ""; } }; options.baseURL = ""; //个人处理,需要兼容之前的elementui等插件的上传 options.fdata = options.fdata || ""; //文件上传的url拼接地址 options.success = options.success || "文件上传成功"; options.url = options.url + new_url(options.fdata); options.loading = options.loading || true; options.headers = options.headers || {}; options.headers["Content-Type"] = "multipart/form-data"; options.method = "post"; options.multiple = options.multiple || false; //是否多文件,默认false //文件类型验证,注意传入数组,默认["image/jpeg", "image/png"] options.type = options.type || ["image/jpeg", "image/png"]; options.size = options.size || 0; //文件大小限制,默认0 options.max = options.max || 5; //最多上传几个文件
看着参数好多是吧。其实没多少。
解析一下:
fdata:因为文件上传地址肯定是多样的。但是我们又需要传递一些文件之外的信息。这时候其实可以在地址栏拼接参数。后端也是能获取到的。
new_url 和fdata是为了这个存在的。
success:单纯的的文件上传提示信息。这个是因为之前ajax封装将提示信息封装进去了,但是默认成功不提示。
headers:这个没什么多说的,但是为了保证headers可能有别的存在,那么就这样进行设置了。"Content-Type":"multipart/form-data"这个是必须的
loading:文件加载的时候全屏出现加载提示
method:必须是post
multiple:这个这里缺失了一点,就是为了控制文件是多文件还是单文件上传
type:文件类型的控制。这里传入数组。默认控制图片文件。传入什么默认限制这些类型
size:限制每个文件的大小,0不限制
max:限制文件个数,单文件没意义
6、创建input对象,并且做好参数预处理。
//文件验证处理 let input = document.createElement("input"); input.type = "file"; options.multiple ? (input.multiple = "multiple") : ""; input.click();
代码就这么简单,这里options.multiple就是刚才参数设置的意义所在了
7、创建input监听,获取文件对象信息,注意外层用promise封装下
这里我放整个控制部分,然后拆开解析
return new Promise((suc, err) => { let type = options.type; input.addEventListener("input", watchUpload, false); function watchUpload(event) { //console.log(event); //移除监听 let remove = () => { input.removeEventListener("input", watchUpload, false); input = null; }; const file = event.path[0].files; const len = file.length; // 文件数量限制 if (len > options.max) { remove(); customMsg.errIfno("文件个数超过" + options.max); err(file); return false; } let formData = new FormData(); for (let i = 0; i < len; i++) { // 文件大小限制 if (options.size !== 0 && file[i].size / 1024 / 1024 > options.size) { remove(); customMsg.errIfno(file[i].name + "文件超过指定大小"); err(file[i]); return false; } // 文件类型限制 if (type.length > 0 && !type.includes(file[i].type)) { remove(); customMsg.errIfno(file[i].name + "文件类型为" + file[i].type); err(file); return false; } formData.append("dhtUpload", file[i], file[i].name); } options.data = formData; // 最终进行文件上传 ajax(options) .then(e => { suc(e); }) .catch(e => { err(e); }); // 没有问题下,清空监听。 remove(); } });
8、获取文件数组信息,注意你没有选择文件是不会触发监听的
//console.log(event); //移除监听 let remove = () => { input.removeEventListener("input", watchUpload, false); input = null; }; const file = event.path[0].files; const len = file.length; // 文件数量限制 if (len > options.max) { remove(); customMsg.errIfno("文件个数超过" + options.max); err(file); return false; } let formData = new FormData();
这里看注释也知道我干了什么。
第一:我获取文件数组。并且预先将移除监听处理好,因为很多地方会用到。
第二:我判断文件数量限制。超过的话,promise(代码中的err(file))返回错误;并且注销监听
第三:先声明formdata对象
9、为每个元素进行限制,并且加入到formdata中
for (let i = 0; i < len; i++) { // 文件大小限制 if (options.size !== 0 && file[i].size / 1024 / 1024 > options.size) { remove(); customMsg.errIfno(file[i].name + "文件超过指定大小"); err(file[i]); return false; } // 文件类型限制 if (type.length > 0 && !type.includes(file[i].type)) { remove(); customMsg.errIfno(file[i].name + "文件类型为" + file[i].type); err(file); return false; } formData.append("dhtUpload", file[i], file[i].name); } options.data = formData;
这里应该很明显了。我遍历每个file文件,并且进行文件大小和文件类型的判断限制。
最后将通过的formdata数据赋值给ajax的data对象中
10、万事大吉,调用原本封装的ajax
// 最终进行文件上传 ajax(options) .then(e => { suc(e); }) .catch(e => { err(e); }); // 没有问题下,清空监听。 remove();
三、完整的使用。
this.axios .upload({ url: this.$api.static().upload_pictures }) .then(e => { console.log("成功", e); }) .catch(e => { console.log("错误", e); });