在日常工作中,文件上传是一个很常见的功能。在项目开发过程中,我们通常都会使用一些成熟的上传组件来实现对应的功能。一般来说,成熟的上传组件不仅会提供漂亮 UI 或好的交互体验,而且还会提供多种不同的上传方式,以满足不同的场景需求。
一般在我们工作中,主要会涉及到 8 种文件上传的场景,每一种场景背后都使用不同的技术,其中也有很多细节需要我们额外注意。今天阿宝哥就来带大家总结一下这 8 种场景,让大家能更好地理解成熟上传组件所提供的功能。阅读本文后,你将会了解以下的内容:
input
元素的 accept
属性限制上传文件的类型、利用 JS 检测文件的类型及使用 Koa 实现单文件上传的功能;input
元素的 multiple
属性支持选择多文件及使用 Koa 实现多文件上传的功能;input
元素上的 webkitdirectory
属性支持目录上传的功能及使用 Koa 实现目录上传并按文件目录结构存放的功能;对于单文件上传的场景来说,最常见的是图片上传的场景,所以我们就以图片上传为例,先来介绍单文件上传的基本流程。
html
在以下代码中,我们通过 input
元素的 accept
属性限制了上传文件的类型。这里使用 image/*
限制只能选择图片文件,当然你也可以设置特定的类型,比如 image/png
或 image/png,image/jpeg
。
<input id="uploadFile" type="file" accept="image/*" />
<button id="submit" onclick="uploadFile()">上传文件</button>
需要注意的是,虽然我们把 input 元素的 accept
属性设置为 image/png
。但如果用户把 jpg/jpeg
格式的图片后缀名改为 .png
,就可以成功绕过这个限制。要解决这个问题,我们可以通过读取文件中的二进制数据来识别正确的文件类型。
要查看图片对应的二进制数据,我们可以借助一些现成的编辑器,比如 Windows 平台下的 WinHex 或 macOS 平台下的 Synalyze It! Pro 十六进制编辑器。这里我们使用 Synalyze It! Pro 这个编辑器,来查看阿宝哥头像对应的二进制数据。
那么在前端能否不借助工具,读取文件的二进制数据呢?答案是可以的,这里阿宝哥就不展开介绍了。感兴趣的话,你可以阅读 [JavaScript 如何检测文件的类型?] 这篇文章。另外,需要注意的是 input
元素 accept
属性有存在兼容性问题。比如 IE 9 以下不支持,具体如下图所示:
(图片来源 —— https://caniuse.com/input-file-accept)
js
const uploadFileEle = document.querySelector("#uploadFile");
const request = axios.create({
baseURL: "http://localhost:3000/upload",
timeout: 60000,
});
async function uploadFile() {
if (!uploadFileEle.files.length) return;
const file = uploadFileEle.files[0]; // 获取单个文件
// 省略文件的校验过程,比如文件类型、大小校验
upload({
url: "/single",
file,
});
}
function upload({ url, file, fieldName = "file" }) {
let formData = new FormData();
formData.set(fieldName, file);
request.post(url, formData, {
// 监听上传进度
onUploadProgress: function (progressEvent) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
在以上代码中,我们先把读取的 File 对象封装成 FormData 对象,然后利用 Axios 实例的 post
方法实现文件上传的功能。在上传前,通过设置请求配置对象的 onUploadProgress
属性,就可以获取文件的上传进度。
Koa 是一个简单易用的 Web 框架,它的特点是优雅、简洁、轻量、自由度高。所以我们选择它来搭建文件服务,并使用以下中间件来实现相应的功能:
multipart/form-data
的中间件;const path = require("path");
const Koa = require("koa");
const serve = require("koa-static");
const cors = require("@koa/cors");
const multer = require("@koa/multer");
const Router = require("@koa/router");
const app = new Koa();
const router = new Router();
const PORT = 3000;
// 上传后资源的URL地址
const RESOURCE_URL = `http://localhost:${PORT}`;
// 存储上传文件的目录
const UPLOAD_DIR = path.join(__dirname, "/public/upload");
const storage = multer.diskStorage({
destination: async function (req, file, cb) {
// 设置文件的存储目录
cb(null, UPLOAD_DIR);
},
filename: function (req, file, cb) {
// 设置文件名
cb(null, `${file.originalname}`);
},
});
const multerUpload = multer({ storage });
router.get("/", async (ctx) => {
ctx.body = "欢迎使用文件服务(by 阿宝哥)";
});
router.post(
"/upload/single",
async (ctx, next) => {
try {
await next();
ctx.body = {
code: 1,
msg: "文件上传成功",
url: `${RESOURCE_URL}/${ctx.file.originalname}`,
};
} catch (error) {
ctx.body = {
code: 0,
msg: "文件上传失败"
};
}
},
multerUpload.single("file")
);
// 注册中间件
app.use(cors());
app.use(serve(UPLOAD_DIR));
app.use(router.routes()).use(router.allowedMethods());
app.listen(PORT, () => {
console.log(`app starting at port ${PORT}`);
});
以上代码相对比较简单,我们就不展开介绍了。Koa 内核很简洁,扩展功能都是通过中间件来实现。比如示例中使用到的路由、CORS、静态资源处理等功能都是通过中间件实现。因此要想掌握 Koa 这个框架,核心是掌握它的中间件机制。如果你想深入了解的话,可以阅读 [如何更好地理解中间件和洋葱模型] 这篇文章。其实除了单文件上传外,在文件上传的场景中,我们也可以同时上传多个文件。
单文件上传示例:single-file-upload
https://github.com/semlinker/file-upload-demos/tree/master/single-file-upload
要上传多个文件,首先我们需要允许用户同时选择多个文件。要实现这个功能,我们可以利用 input
元素的 multiple
属性。跟前面介绍的 accept
属性一样,该属性也存在兼容性问题,具体如下图所示:
(图片来源 —— https://caniuse.com/mdn-api_htmlinputelement_multiple)
html
相比单文件上传的代码,多文件上传场景下的 input
元素多了一个 multiple
属性:
<input id="uploadFile" type="file" accept="image/*" multiple />
<button id="submit" onclick="uploadFile()">上传文件</button>
js
在单文件上传的代码中,我们通过 uploadFileEle.files[0]
获取单个文件,而对于多文件上传来说,我们需要获取已选择的文件列表,即通过 uploadFileEle.files
来获取,它返回的是一个 FileList 对象。
async function uploadFile() {
if (!uploadFileEle.files.length) return;
const files = Array.from(uploadFileEle.files);
upload({
url: "/multiple",
files,
});
}
因为要支持上传多个文件,所以我们需要同步更新一下 upload
函数。对应的处理逻辑就是遍历文件列表,然后使用 FormData 对象的 append
方法来添加多个文件,具体代码如下所示:
function upload({ url, files, fieldName = "file" }) {
let formData = new FormData();
files.forEach((file) => {
formData.append(fieldName, file);
});
request.post(url, formData, {
// 监听上传进度
onUploadProgress: function (progressEvent) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
在以下代码中,我们定义了一个新的路由 —— /upload/multiple
来处理多文件上传的功能。当所有文件都成功上传后,就会返回一个已上传文件的 url 地址列表:
router.post(
"/upload/multiple",
async (ctx, next) => {
try {
await next();
urls = ctx.files.file.map(file => `${RESOURCE_URL}/${file.originalname}`);
ctx.body = {
code: 1,
msg: "文件上传成功",
urls
};
} catch (error) {
ctx.body = {
code: 0,
msg: "文件上传失败",
};
}
},
multerUpload.fields([
{
name: "file", // 与FormData表单项的fieldName相对应
},
])
);
介绍完单文件和多文件上传的功能,接下来我们来介绍目录上传的功能。
多文件上传示例:multiple-file-upload
https://github.com/semlinker/file-upload-demos/tree/master/multiple-file-upload
可能你还不知道,input
元素上还有一个的 webkitdirectory
属性。当设置了 webkitdirectory
属性之后,我们就可以选择目录了。
<input id="uploadFile" type="file" accept="image/*" webkitdirectory />
当我们选择了指定目录之后,比如阿宝哥桌面上的 images
目录,就会显示以下确认框:
点击上传按钮之后,我们就可以获取文件列表。列表中的文件对象上含有一个 webkitRelativePath
属性,用于表示当前文件的相对路径。
虽然通过 webkitdirectory
属性可以很容易地实现选择目录的功能,但在实际项目中我们还需要考虑它的兼容性。比如在 IE 11 以下的版本就不支持该属性,其它浏览器的兼容性如下图所示:
(图片来源 —— https://caniuse.com/?search=webkitdirectory)
了解完 webkitdirectory
属性的兼容性,我们先来介绍前端的实现代码。
为了让服务端能按照实际的目录结构来存放对应的文件,在添加表单项时我们需要把当前文件的路径提交到服务端。此外,为了确保@koa/multer
能正确处理文件的路径,我们需要对路径进行特殊处理。即把 /
斜杠替换为 @
符号。对应的处理方式如下所示:
function upload({ url, files, fieldName = "file" }) {
let formData = new FormData();
files.forEach((file, i) => {
formData.append(
fieldName,
files[i],
files[i].webkitRelativePath.replace(/\//g, "@");
);
});
request.post(url, formData); // 省略上传进度处理
}
目录上传与多文件上传,服务端代码的主要区别就是 @koa/multer
中间件的配置对象不一样。在 destination
属性对应的函数中,我们需要把文件名中 @
还原成 /
,然后根据文件的实际路径来生成目录。
const fse = require("fs-extra");
const storage = multer.diskStorage({
destination: async function (req, file, cb) {
// images@image-1.jpeg => images/image-1.jpeg
let relativePath = file.originalname.replace(/@/g, path.sep);
let index = relativePath.lastIndexOf(path.sep);
let fileDir = path.join(UPLOAD_DIR, relativePath.substr(0, index));
// 确保文件目录存在,若不存在的话,会自动创建
await fse.ensureDir(fileDir);
cb(null, fileDir);
},
filename: function (req, file, cb) {
let parts = file.originalname.split("@");
cb(null, `${parts[parts.length - 1]}`);
},
});
现在我们已经实现了目录上传的功能,那么能否把目录下的文件压缩成一个压缩包后再上传呢?答案是可以的,接下来我们来介绍如何实现压缩目录上传的功能。
目录上传示例:directory-upload
https://github.com/semlinker/file-upload-demos/tree/master/directory-upload
在 [JavaScript 如何在线解压 ZIP 文件?] 这篇文章中,介绍了在浏览器端如何使用 JSZip 这个库实现在线解压 ZIP 文件的功能。JSZip 这个库除了可以解析 ZIP 文件之外,它还可以用来 创建和编辑 ZIP 文件。利用 JSZip 这个库提供的 API,我们就可以把目录下的所有文件压缩成 ZIP 文件,然后再把生成的 ZIP 文件上传到服务器。
JSZip 实例上的 file(name, data [,options])
方法,可以把文件添加到 ZIP 文件中。基于该方法我们可以封装了一个 generateZipFile
函数,用于把目录下的文件列表压缩成一个 ZIP 文件。以下是 generateZipFile
函数的具体实现:
function generateZipFile(
zipName, files,
options = { type: "blob", compression: "DEFLATE" }
) {
return new Promise((resolve, reject) => {
const zip = new JSZip();
for (let i = 0; i < files.length; i++) {
zip.file(files[i].webkitRelativePath, files[i]);
}
zip.generateAsync(options).then(function (blob) {
zipName = zipName || Date.now() + ".zip";
const zipFile = new File([blob], zipName, {
type: "application/zip",
});
resolve(zipFile);
});
});
}
在创建完 generateZipFile
函数之后,我们需要更新一下前面已经介绍过的 uploadFile
函数:
async function uploadFile() {
let fileList = uploadFileEle.files;
if (!fileList.length) return;
let webkitRelativePath = fileList[0].webkitRelativePath;
let zipFileName = webkitRelativePath.split("/")[0] + ".zip";
let zipFile = await generateZipFile(zipFileName, fileList);
upload({
url: "/single",
file: zipFile,
fileName: zipFileName
});
}
在以上的 uploadFile
函数中,我们会对返回的 FileList 对象进行处理,即调用 generateZipFile
函数来生成 ZIP 文件。此外,为了在服务端接收压缩文件时,能获取到文件名,我们为 upload
函数增加了一个 fileName
参数,该参数用于调用 formData.append
方法时,设置上传文件的文件名:
function upload({ url, file, fileName, fieldName = "file" }) {
if (!url || !file) return;
let formData = new FormData();
formData.append(
fieldName, file, fileName
);
request.post(url, formData); // 省略上传进度跟踪
}
以上就是压缩目录上传,前端部分的 JS 代码,服务端的代码可以参考前面单文件上传的相关代码。
压缩目录上传示例:directory-compress-upload
https://github.com/semlinker/file-upload-demos/tree/master/directory-compress-upload
要实现拖拽上传的功能,我们需要先了解与拖拽相关的事件。比如 drag
、dragend
、dragenter
、dragover
或 drop
事件等。这里我们只介绍接下来要用到的拖拽事件:
dragenter
:当拖拽元素或选中的文本到一个可释放目标时触发;dragover
:当元素或选中的文本被拖到一个可释放目标上时触发(每100毫秒触发一次);dragleave
:当拖拽元素或选中的文本离开一个可释放目标时触发;drop
:当元素或选中的文本在可释放目标上被释放时触发。基于上面的这些事件,我们就可以提高用户拖拽的体验。比如当用户拖拽的元素进入目标区域时,对目标区域进行高亮显示。当用户拖拽的元素离开目标区域时,移除高亮显示。很明显当 drop
事件触发后,拖拽的元素已经放入目标区域了,这时我们就需要获取对应的数据。
那么如何获取拖拽对应的数据呢?这时我们需要使用 DataTransfer 对象,该对象用于保存拖动并放下过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。若拖动操作涉及拖动文件,则我们可以通过 DataTransfer 对象的 files
属性来获取文件列表。
介绍完拖拽上传相关的知识后,我们来看一下具体如何实现拖拽上传的功能。
html
<div id="dropArea">
<p>拖拽上传文件</p>
<div id="imagePreview"></div>
</div>
css
#dropArea {
width: 300px;
height: 300px;
border: 1px dashed gray;
margin-bottom: 20px;
}
#dropArea p {
text-align: center;
color: #999;
}
#dropArea.highlighted {
background-color: #ddd;
}
#imagePreview {
max-height: 250px;
overflow-y: scroll;
}
#imagePreview img {
width: 100%;
display: block;
margin: auto;
}
js
为了让大家能够更好地阅读拖拽上传的相关代码,我们把代码拆成 4 部分来讲解:
1、阻止默认拖拽行为
const dropAreaEle = document.querySelector("#dropArea");
const imgPreviewEle = document.querySelector("#imagePreview");
const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
dropAreaEle.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
2、切换目标区域的高亮状态
["dragenter", "dragover"].forEach((eventName) => {
dropAreaEle.addEventListener(eventName, highlight, false);
});
["dragleave", "drop"].forEach((eventName) => {
dropAreaEle.addEventListener(eventName, unhighlight, false);
});
// 添加高亮样式
function highlight(e) {
dropAreaEle.classList.add("highlighted");
}
// 移除高亮样式
function unhighlight(e) {
dropAreaEle.classList.remove("highlighted");
}
3、处理图片预览
dropAreaEle.addEventListener("drop", handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = [...dt.files];
files.forEach((file) => {
previewImage(file, imgPreviewEle);
});
// 省略文件上传代码
}
function previewImage(file, container) {
if (IMAGE_MIME_REGEX.test(file.type)) {
const reader = new FileReader();
reader.onload = function (e) {
let img = document.createElement("img");
img.src = e.target.result;
container.append(img);
};
reader.readAsDataURL(file);
}
}
4、文件上传
function handleDrop(e) {
const dt = e.dataTransfer;
const files = [...dt.files];
// 省略图片预览代码
files.forEach((file) => {
upload({
url: "/single",
file,
});
});
}
const request = axios.create({
baseURL: "http://localhost:3000/upload",
timeout: 60000,
});
function upload({ url, file, fieldName = "file" }) {
let formData = new FormData();
formData.set(fieldName, file);
request.post(url, formData, {
// 监听上传进度
onUploadProgress: function (progressEvent) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(percentCompleted);
},
});
}
拖拽上传算是一个比较常见的场景,很多成熟的上传组件都支持该功能。其实除了拖拽上传外,还可以利用剪贴板实现复制上传的功能。
拖拽上传示例:drag-drop-upload
https://github.com/semlinker/file-upload-demos/tree/master/drag-drop-upload
在介绍如何实现剪贴板上传的功能前,我们需要了解一下 Clipboard API。Clipboard
接口实现了 Clipboard API,如果用户授予了相应的权限,就能提供系统剪贴板的读写访问。在 Web 应用程序中,Clipboard API 可用于实现剪切、复制和粘贴功能。该 API 用于取代通过 document.execCommand API 来实现剪贴板的操作。
在实际项目中,我们不需要手动创建 Clipboard
对象,而是通过 navigator.clipboard
来获取 Clipboard
对象:
在获取 Clipboard
对象之后,我们就可以利用该对象提供的 API 来访问剪贴板,比如:
navigator.clipboard.readText().then(
clipText => document.querySelector(".editor").innerText = clipText
);
以上代码将 HTML 中含有 .editor
类的第一个元素的内容替换为剪贴板的内容。如果剪贴板为空,或者不包含任何文本,则元素的内容将被清空。这是因为在剪贴板为空或者不包含文本时,readText
方法会返回一个空字符串。
利用 Clipboard API 我们可以很方便地操作剪贴板,但实际项目使用过程中也得考虑它的兼容性:
(图片来源 —— https://caniuse.com/async-clipboard)
要实现剪贴板上传的功能,可以分为以下 3 个步骤:
FormData
对象并上传。了解完上述步骤,接下来我们来分析一下具体实现的代码。
html
<div id="uploadArea">
<p>请先复制图片后再执行粘贴操作</p>
</div>
css
#uploadArea {
width: 400px;
height: 400px;
border: 1px dashed gray;
display: table-cell;
vertical-align: middle;
}
#uploadArea p {
text-align: center;
color: #999;
}
#uploadArea img {
max-width: 100%;
max-height: 100%;
display: block;
margin: auto;
}
js
在以下代码中,我们使用 addEventListener
方法为 uploadArea
容器添加 paste
事件。在对应的事件处理函数中,我们会优先判断当前浏览器是否支持异步 Clipboard API。如果支持的话,就会通过 navigator.clipboard.read
方法来读取剪贴板中的内容。在读取内容之后,我们会通过正则判断剪贴板项中是否包含图片资源,如果有的话会调用 previewImage
方法执行图片预览操作并把返回的 blob
对象保存起来,用于后续的上传操作。
const IMAGE_MIME_REGEX = /^image\/(jpe?g|gif|png)$/i;
const uploadAreaEle = document.querySelector("#uploadArea");
uploadAreaEle.addEventListener("paste", async (e) => {
e.preventDefault();
const files = [];
if (navigator.clipboard) {
let clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
if (IMAGE_MIME_REGEX.test(type)) {
const blob = await clipboardItem.getType(type);
insertImage(blob, uploadAreaEle);
files.push(blob);
}
}
}
} else {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (IMAGE_MIME_REGEX.test(items[i].type)) {
let file = items[i].getAsFile();
insertImage(file, uploadAreaEle);
files.push(file);
}
}
}
if (files.length > 0) {
confirm("剪贴板检测到图片文件,是否执行上传操作?")
&& upload({
url: "/multiple",
files,
});
}
});
若当前浏览器不支持异步 Clipboard API,则我们会尝试通过 e.clipboardData.items
来访问剪贴板中的内容。需要注意的是,在遍历剪贴板内容项的时候,我们是通过 getAsFile
方法来获取剪贴板的内容。当然该方法也存在兼容性问题,具体如下图所示:
(图片来源 —— https://caniuse.com/mdn-api_datatransferitem_getasfile)
前面已经提到,当从剪贴板解析到图片资源时,会让用户进行预览,该功能是基于 FileReader API 来实现的,对应的代码如下所示:
function previewImage(file, container) {
const reader = new FileReader();
reader.onload = function (e) {
let img = document.createElement("img");
img.src = e.target.result;
container.append(img);
};
reader.readAsDataURL(file);
}
当用户预览完成后,如果确认上传我们就会执行文件的上传操作。因为文件是从剪贴板中读取的,所以在上传前我们会根据文件的类型,自动为它生成一个文件名,具体是采用时间戳加文件后缀的形式:
function upload({ url, files, fieldName = "file" }) {
let formData = new FormData();
files.forEach((file) => {
let fileName = +new Date() + "." + IMAGE_MIME_REGEX.exec(file.type)[1];
formData.append(fieldName, file, fileName);
});
request.post(url, formData);
}
前面我们已经介绍了文件上传的多种不同场景,接下来我们来介绍一个 “特殊” 的场景 —— 大文件上传。
剪贴板上传示例:clipboard-upload
https://github.com/semlinker/file-upload-demos/tree/master/clipboard-upload
相信你可能已经了解大文件上传的解决方案,在上传大文件时,为了提高上传的效率,我们一般会使用 Blob.slice 方法对大文件按照指定的大小进行切割,然后通过多线程进行分块上传,等所有分块都成功上传后,再通知服务端进行分块合并。具体处理方案如下图所示:
因为在 [JavaScript 中如何实现大文件并发上传?] 这篇文章中,阿宝哥已经详细介绍了大文件并发上传的方案,所以这里就不展开介绍了。我们只回顾一下大文件并发上传的完整流程:
前面我们都是介绍客户端文件上传的场景,其实也有服务端文件上传的场景。比如在服务端动态生成海报后,上传到另外一台服务器或云厂商的 OSS(Object Storage Service)。下面我们就以 Node.js 为例来介绍在服务端如何上传文件。
大文件分块上传示例:big-file-upload
https://github.com/semlinker/file-upload-demos/tree/master/big-file-upload
服务器上传就是把文件从一台服务器上传到另外一台服务器。借助 Github 上 form-data 这个库提供的功能,我们可以很容易地实现服务器上传的功能。下面我们来简单介绍一下单文件和多文件上传的功能:
const fs = require("fs");
const path = require("path");
const FormData = require("form-data");
const form1 = new FormData();
form1.append("file", fs.createReadStream(path.join(__dirname, "images/image-1.jpeg")));
form1.submit("http://localhost:3000/upload/single", (error, response) => {
if(error) {
console.log("单图上传失败");
return;
}
console.log("单图上传成功");
});
const form2 = new FormData();
form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-2.jpeg")));
form2.append("file", fs.createReadStream(path.join(__dirname, "images/image-3.jpeg")));
form2.submit("http://localhost:3000/upload/multiple", (error, response) => {
if(error) {
console.log("多图上传失败");
return;
}
console.log("多图上传成功");
});
观察以上代码可知,创建完 FormData
对象之后,我们只需要通过 fs.createReadStream
API 创建可读流,然后调用 FormData
对象的 append
方法添加表单项,最后再调用 submit
方法执行提交操作即可。
其实除了 ReadableStream
之外,FormData
对象的 append
方法还支持以下类型:
const FormData = require('form-data');
const http = require('http');
const form = new FormData();
http.request('http://nodejs.org/images/logo.png', function(response) {
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_logo', response);
});
服务端文件上传的内容就介绍到这里,关于 form-data 这个库的其他用法,感兴趣的话,可以阅读对应的使用文档。其实除了以上介绍的八种场景外,在日常工作中,你也可能会使用一些同步工具,比如 Syncthing 文件同步工具实现文件传输。好的,本文的所有内容都已经介绍完了,最后我们来做一个总结。
服务端上传示例:server-upload
https://github.com/semlinker/file-upload-demos/tree/master/server-upload
本文阿宝哥详细介绍了文件上传的八种场景,希望阅读完本文后,你对八种场景背后使用的技术有一定的了解。由于篇幅有限,阿宝哥就没有展开介绍与 multipart/form-data
类型相关的内容,感兴趣的小伙伴可以自行了解一下。
此外,在实际项目中,你可以考虑直接使用成熟的第三方组件,比如 Github 上的 Star 数 11K+ 的 filepond。该组件采用插件化的架构,以插件的方式,提供了非常多的功能,比如 File encode、File rename、File poster、Image preview 和 Image crop 等。总之,它是一个很不错的组件,以后有机会的话,大家可以尝试一下。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/aRfDFVpDFftn1qFYeLg4Iw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。