当前位置: 首页 > news >正文

建个网站怎放上一张图成都建设银行分行招聘网站

建个网站怎放上一张图,成都建设银行分行招聘网站,做投票网站教程,网页模版图片前言这段时间面试官都挺忙的#xff0c;频频出现在博客文章标题#xff0c;虽然我不是特别想蹭热度#xff0c;但是实在想不到好的标题了-。-#xff0c;蹭蹭就蹭蹭 :)事实上我在面试的时候确实被问到了这个问题#xff0c;而且是一道在线 coding 的编程题#xff0c;当时…前言这段时间面试官都挺忙的频频出现在博客文章标题虽然我不是特别想蹭热度但是实在想不到好的标题了-。-蹭蹭就蹭蹭 :)事实上我在面试的时候确实被问到了这个问题而且是一道在线 coding 的编程题当时虽然思路正确可惜最终也并不算完全答对结束后花了一段时间整理了下思路那么究竟该如何实现一个大文件上传以及在上传中如何实现断点续传的功能呢本文将从零搭建前端和服务端实现一个大文件上传和断点续传的 demo前端vue element-ui服务端nodejs文章有误解的地方欢迎指出将在第一时间改正有更好的实现方式希望留下你的评论大文件上传整体思路前端前端大文件上传网上的大部分文章已经给出了解决方案核心是利用 Blob.prototype.slice 方法和数组的 slice 方法相似调用的 slice 方法可以返回原文件的某个切片这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片然后借助 http 的可并发性同时上传多个切片这样从原本传一个大文件变成了同时传多个小的文件切片可以大大减少上传时间另外由于是并发传输到服务端的顺序可能会发生变化所以我们还需要给每个切片记录顺序服务端服务端需要负责接受这些切片并在接收到所有切片后合并切片这里又引伸出两个问题何时合并切片即切片什么时候传输完成如何合并切片第一个问题需要前端进行配合前端在每个切片中都携带切片最大数量的信息当服务端接受到这个数量的切片时自动合并也可以额外发一个请求主动通知服务端进行切片的合并第二个问题具体如何合并切片呢这里可以使用 nodejs 的 api fs.appendFileSync它可以同步地将数据追加到指定文件也就是说当服务端接受到所有切片后先创建一个最终的文件然后将所有切片逐步合并到这个文件中talk is cheap,show me the code接着我们用代码实现上面的思路前端部分前端使用 Vue 作为开发框架对界面没有太大要求原生也可以考虑到美观使用 element-ui 作为 UI 框架上传控件首先创建选择文件的控件监听 change 事件以及上传按钮上传export default {data: () ({container: {file: null}}),methods: {async handleFileChange(e) {const [file] e.target.files;if (!file) return;Object.assign(this.$data, this.$options.data());this.container.file file;},async handleUpload() {}}};请求逻辑考虑到通用性这里没有用第三方的请求库而是用原生 XMLHttpRequest 做一层简单的封装来发请求request({url,method post,data,headers {},requestList}) {return new Promise(resolve {const xhr new XMLHttpRequest();xhr.open(method, url);Object.keys(headers).forEach(key xhr.setRequestHeader(key, headers[key]));xhr.send(data);xhr.onload e {resolve({data: e.target.response});};});}复制代码上传切片接着实现比较重要的上传功能上传需要做两件事对文件进行切片将切片传输给服务端上传 const LENGTH 10; // 切片数量export default {data: () ({container: {file: null, data: []}}),methods: {request() {},async handleFileChange() {}, // 生成文件切片 createFileChunk(file, length LENGTH) { const fileChunkList []; const chunkSize Math.ceil(file.size / length); let cur 0; while (cur file.size) { fileChunkList.push({ file: file.slice(cur, cur chunkSize) }); cur chunkSize; } return fileChunkList; }, // 上传切片 async uploadChunks() { const requestList this.data .map(({ chunk }) { const formData new FormData(); formData.append(chunk, chunk); formData.append(hash, hash); formData.append(filename, this.container.file.name); return { formData }; }) .map(async ({ formData }) this.request({ url: http://localhost:3000, data: formData }) ); await Promise.all(requestList); // 并发切片 }, async handleUpload() { if (!this.container.file) return; const fileChunkList this.createFileChunk(this.container.file); this.data fileChunkList.map(({ file }index) ({ chunk: file, hash: this.container.file.name - index // 文件名 数组下标 })); await this.uploadChunks(); }}};当点击上传按钮时调用 createFileChunk 将文件切片切片数量通过一个常量 Length 控制这里设置为 10即将文件分成 10 个切片上传createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回在生成文件切片时需要给每个切片一个标识作为 hash这里暂时使用文件名 下标这样后端可以知道当前切片是第几个切片由于之后的合并切片随后调用 uploadChunks 上传所有的文件切片将文件切片切片 hash以及文件名放入 FormData 中再调用上一步的 request 函数返回一个 proimise最后调用 Promise.all 并发上传所有的切片发送合并请求这里使用整体思路中提到的第二种合并切片的方式即前端主动通知服务端进行合并所以前端还需要额外发请求服务端接受到这个请求时主动合并切片上传export default {data: () ({container: {file: null},data: []}),methods: {request() {},async handleFileChange() {},createFileChunk() {},// 上传切片同时过滤已上传的切片async uploadChunks() {const requestList this.data.map(({ chunk }) {const formData new FormData();formData.append(chunk, chunk);formData.append(hash, hash);formData.append(filename, this.container.file.name);return { formData };}).map(async ({ formData }) this.request({url: http://localhost:3000,data: formData}));await Promise.all(requestList); // 合并切片 await this.mergeRequest();}, async mergeRequest() { await this.request({ url: http://localhost:3000/merge, headers: { content-type: application/json }, data: JSON.stringify({ filename: this.container.file.name }) }); },async handleUpload() {}}};服务端部分简单使用 http 模块搭建服务端const http require(http);const server http.createServer();server.on(request, async (req, res) {res.setHeader(Access-Control-Allow-Origin, *);res.setHeader(Access-Control-Allow-Headers, *);if (req.method OPTIONS) {res.status 200;res.end();return;}});server.listen(3000, () console.log(正在监听 3000 端口));接受切片使用 multiparty 包处理前端传来的 FormData在 multiparty.parse 的回调中files 参数保存了 FormData 中文件fields 参数保存了 FormData 中非文件的字段const http require(http);const path require(path);const fse require(fs-extra);const multiparty require(multiparty);const server http.createServer(); const UPLOAD_DIR path.resolve(__dirname, .., target); // 大文件存储目录server.on(request, async (req, res) {res.setHeader(Access-Control-Allow-Origin, *);res.setHeader(Access-Control-Allow-Headers, *);if (req.method OPTIONS) {res.status 200;res.end();return;} const multipart new multiparty.Form(); multipart.parse(req, async (err, fields, files) { if (err) { return; } const [chunk] files.chunk; const [hash] fields.hash; const [filename] fields.filename; const chunkDir ${UPLOAD_DIR}/${filename}; // 切片目录不存在创建切片目录 if (!fse.existsSync(chunkDir)) { await fse.mkdirs(chunkDir); } // 重命名文件 await fse.rename(chunk.path, ${chunkDir}/${hash}); res.end(received file chunk); });});server.listen(3000, () console.log(正在监听 3000 端口));查看 multiparty 处理后的 chunk 对象path 是存储临时文件的路径size 是临时文件大小在 multiparty 文档中提到可以使用 fs.rename 重命名的方式移动临时文件也就是文件切片在接受文件切片时需要先创建存储切片的文件夹由于前端在发送每个切片时额外携带了唯一值 hash所以以 hash 作为文件名将切片从临时路径移动切片文件夹中最后的结果如下合并切片在接收到前端发送的合并请求后服务端将文件夹下的所有切片进行合并const http require(http);const path require(path);const fse require(fs-extra);const server http.createServer();const UPLOAD_DIR path.resolve(__dirname, .., target); // 大文件存储目录 const resolvePost req new Promise(resolve { let chunk ; req.on(data, data { chunk data; }); req.on(end, () { resolve(JSON.parse(chunk)); }); }); // 合并切片 const mergeFileChunk async (filePath, filename) { const chunkDir ${UPLOAD_DIR}/${filename}; const chunkPaths await fse.readdir(chunkDir); await fse.writeFile(filePath, ); chunkPaths.forEach(chunkPath { fse.appendFileSync(filePath, fse.readFileSync(${chunkDir}/${chunkPath})); fse.unlinkSync(${chunkDir}/${chunkPath}); }); fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录 };server.on(request, async (req, res) {res.setHeader(Access-Control-Allow-Origin, *);res.setHeader(Access-Control-Allow-Headers, *);if (req.method OPTIONS) {res.status 200;res.end();return;} if (req.url /merge) { const data await resolvePost(req); const { filename } data; const filePath ${UPLOAD_DIR}/${filename}; await mergeFileChunk(filePath, filename); res.end( JSON.stringify({ code: 0, message: file merged success }) ); }});server.listen(3000, () console.log(正在监听 3000 端口));由于前端在发送合并请求时会携带文件名服务端根据文件名可以找到上一步创建的切片文件夹接着使用 fs.writeFileSync 先创建一个空文件这个空文件的文件名就是切片文件夹名 后缀名组合而成随后通过 fs.appendFileSync 从切片文件夹中不断将切片合并到空文件中每次合并完成后删除这个切片等所有切片都合并完毕后最后删除切片文件夹至此一个简单的大文件上传就完成了接下来我们再此基础上扩展一些额外的功能显示上传进度条上传进度分两种一个是每个切片的上传进度另一个是整个文件的上传进度而整个文件的上传进度是基于每个切片上传进度计算而来所以我们先实现切片的上传进度切片进度条XMLHttpRequest 原生支持上传进度的监听只需要监听 upload.onprogress 即可我们在原来的 request 基础上传入 onProgress 参数给 XMLHttpRequest 注册监听事件// xhrrequest({url,method post,data,headers {}, onProgress e e,requestList}) {return new Promise(resolve {const xhr new XMLHttpRequest(); xhr.upload.onprogress onProgress;xhr.open(method, url);Object.keys(headers).forEach(key xhr.setRequestHeader(key, headers[key]));xhr.send(data);xhr.onload e {resolve({data: e.target.response});};});}由于每个切片都需要触发独立的监听事件所以还需要一个工厂函数根据传入的切片返回不同的监听函数在原先的前端上传逻辑中新增监听函数部分// 上传切片同时过滤已上传的切片async uploadChunks(uploadedList []) {const requestList this.data.map(({ chunk }) {const formData new FormData();formData.append(chunk, chunk);formData.append(filename, this.container.file.name);return { formData };}).map(async ({ formData }) this.request({url: http://localhost:3000,data: formData onProgress: this.createProgressHandler(this.data[index]),}));await Promise.all(requestList);// 合并切片await this.mergeRequest();},async handleUpload() {if (!this.container.file) return;const fileChunkList this.createFileChunk(this.container.file);this.data fileChunkList.map(({ file }index) ({chunk: file, index,hash: this.container.file.name - index percentage:0}));await this.uploadChunks();} createProgressHandler(item) { return e { item.percentage parseInt(String((e.loaded / e.total) * 100)); }; }每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性之后把将 data 数组放到视图中展示即可文件进度条将每个切片已上传的部分累加除以整个文件的大小就能得出当前文件的上传进度所以这里使用 Vue 计算属性computed: {uploadPercentage() {if (!this.container.file || !this.data.length) return 0;const loaded this.data.map(item item.size * item.percentage).reduce((acc, cur) acc cur);return parseInt((loaded / this.container.file.size).toFixed(2));}}最终视图如下断点续传断点续传的原理在于前端/服务端需要记住已上传的切片这样下次上传就可以跳过之前已上传的部分有两种方案实现记忆的功能前端使用 localStorage 记录已上传的切片 hash服务端保存已上传的切片 hash前端每次上传前向服务端获取已上传的切片第一种是前端的解决方案第二种是服务端而前端方案有一个缺陷如果换了个浏览器就失去了记忆的效果所以这里选取后者生成 hash无论是前端还是服务端都必须要生成文件和切片的 hash之前我们使用文件名 切片下标作为切片 hash这样做文件名一旦修改就失去了效果而事实上只要文件内容不变hash 就不应该变化所以正确的做法是根据文件内容生成 hash所以我们修改一下 hash 的生成规则这里用到另一个库 spark-md5它可以根据文件内容计算出文件的 hash 值另外考虑到如果上传一个超大文件读取文件内容计算 hash 是非常耗费时间的并且会引起 UI 的阻塞导致页面假死状态所以我们使用 web-worker 在 worker 线程计算 hash这样用户仍可以在主界面正常的交互由于实例化 web-worker 时参数是一个 js 文件路径且不能跨域所以我们单独创建一个 hash.js 文件放在 public 目录下另外在 worker 中也是不允许访问 dom 的但它提供了importScripts 函数用于导入外部脚本通过它导入 spark-md5// /public/hash.jsself.importScripts(/spark-md5.min.js); // 导入脚本// 生成文件 hashself.onmessage e {const { fileChunkList } e.data;const spark new self.SparkMD5.ArrayBuffer();let percentage 0;let count 0;const loadNext index {const reader new FileReader();reader.readAsArrayBuffer(fileChunkList[index].file);reader.onload e {count;spark.append(e.target.result);if (count fileChunkList.length) {self.postMessage({percentage: 100,hash: spark.end()});self.close();} else {percentage 100 / fileChunkList.length;self.postMessage({percentage});// 递归计算下一个切片loadNext(count);}};};loadNext(0);};在 worker 线程中接受文件切片 fileChunkList利用 FileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中每计算完一个切片通过 postMessage 向主线程发送一个进度事件全部完成后将最终的 hash 发送给主线程spark-md5 需要根据所有切片才能算出一个 hash 值不能直接将整个文件放入计算否则即使不同文件也会有相同的 hash具体可以看官方文档spark-md5(https://www.npmjs.com/package/spark-md5)接着编写主线程与 worker 线程通讯的逻辑 // 生成文件 hash(web-worker) calculateHash(fileChunkList) { return new Promise(resolve { // 添加 worker 属性 this.container.worker new Worker(/hash.js); this.container.worker.postMessage({ fileChunkList }); this.container.worker.onmessage e { const { percentage, hash } e.data; this.hashPercentage percentage; if (hash) { resolve(hash); } }; });},async handleUpload() {if (!this.container.file) return;const fileChunkList this.createFileChunk(this.container.file); this.container.hash await this.calculateHash(fileChunkList);this.data fileChunkList.map(({ file }index) ({ fileHash: this.container.hash,chunk: file,hash: this.container.file.name - index, // 文件名 数组下标percentage:0}));await this.uploadChunks();}主线程使用 postMessage 给 worker 线程传入所有切片 fileChunkList并监听 worker 线程发出的 postMessage 事件拿到文件 hash加上显示计算 hash 的进度条看起来像这样至此前端需要将之前用文件名作为 hash 的地方改写为 workder 返回的这个 hash服务端则使用 hash 作为切片文件夹名hash 下标作为切片名hash 扩展名作为文件名没有新增的逻辑文件秒传在实现断点续传前先简单介绍一下文件秒传所谓的文件秒传即在服务端已经存在了上传的资源所以当用户再次上传时会直接提示上传成功文件秒传需要依赖上一步生成的 hash即在上传前先计算出文件 hash并把 hash 发送给服务端进行验证由于 hash 的唯一性所以一旦服务端能找到 hash 相同的文件则直接返回上传成功的信息即可 async verifyUpload(filename, fileHash) { const { data } await this.request({ url: http://localhost:3000/verify, headers: { content-type: application/json }, data: JSON.stringify({ filename, fileHash }) }); return JSON.parse(data); },async handleUpload() {if (!this.container.file) return;const fileChunkList this.createFileChunk(this.container.file);this.container.hash await this.calculateHash(fileChunkList); const { shouldUpload } await this.verifyUpload( this.container.file.name, this.container.hash ); if (!shouldUpload) { this.$message.success(秒传上传成功); return; }this.data fileChunkList.map(({ file }, index) ({fileHash: this.container.hash,index,hash: this.container.hash - index,chunk: file,percentage: 0}));await this.uploadChunks();}复制代码秒传其实就是给用户看的障眼法实质上根本没有上传服务端的逻辑非常简单新增一个验证接口验证文件是否存在即可 const extractExt filename filename.slice(filename.lastIndexOf(.), filename.length); // 提取后缀名const UPLOAD_DIR path.resolve(__dirname, .., target); // 大文件存储目录const resolvePost req new Promise(resolve {let chunk ;req.on(data, data {chunk data;});req.on(end, () {resolve(JSON.parse(chunk));});});server.on(request, async (req, res) {if (req.url /verify) { const data await resolvePost(req); const { fileHash, filename } data; const ext extractExt(filename); const filePath ${UPLOAD_DIR}/${fileHash}${ext}; if (fse.existsSync(filePath)) { res.end( JSON.stringify({ shouldUpload: false }) ); } else { res.end( JSON.stringify({ shouldUpload: true }) ); }}});server.listen(3000, () console.log(正在监听 3000 端口));暂停上传讲完了生成 hash 和文件秒传回到断点续传断点续传顾名思义即断点 续传所以我们第一步先实现“断点”也就是暂停上传原理是使用 XMLHttpRequest 的 abort 方法可以取消一个 xhr 请求的发送为此我们需要将上传每个切片的 xhr 对象保存起来我们再改造一下 request 方法request({url,method post,data,headers {},onProgress e e, requestList}) {return new Promise(resolve {const xhr new XMLHttpRequest();xhr.upload.onprogress onProgress;xhr.open(method, url);Object.keys(headers).forEach(key xhr.setRequestHeader(key, headers[key]));xhr.send(data);xhr.onload e { // 将请求成功的 xhr 从列表中删除 if (requestList) { const xhrIndex requestList.findIndex(item item xhr); requestList.splice(xhrIndex, 1); }resolve({data: e.target.response});}; // 暴露当前 xhr 给外部 requestList?.push(xhr);});},这样在上传切片时传入 requestList 数组作为参数request 方法就会将所有的 xhr 保存在数组中了每当一个切片上传成功时将对应的 xhr 从 requestList 中删除所以 requestList 中只保存正在上传切片的 xhr之后新建一个暂停按钮当点击按钮时调用保存在 requestList 中 xhr 的 abort 方法即取消并清空所有正在上传的切片handlePause() {this.requestList.forEach(xhr xhr?.abort());this.requestList [];}点击暂停按钮可以看到 xhr 都被取消了恢复上传之前在介绍断点续传的时提到使用第二种服务端存储的方式实现续传由于当文件切片上传后服务端会建立一个文件夹存储所有上传的切片所以每次前端上传前可以调用一个接口服务端将已上传的切片的切片名返回前端再跳过这些已经上传切片这样就实现了“续传”的效果而这个接口可以和之前秒传的验证接口合并前端每次上传前发送一个验证的请求返回两种结果服务端已存在该文件不需要再次上传服务端不存在该文件或者已上传部分文件切片通知前端进行上传并把已上传的文件切片返回给前端所以我们改造一下之前文件秒传的服务端验证接口const extractExt filename filename.slice(filename.lastIndexOf(.), filename.length); // 提取后缀名const UPLOAD_DIR path.resolve(__dirname, .., target); // 大文件存储目录const resolvePost req new Promise(resolve {let chunk ;req.on(data, data {chunk data;});req.on(end, () {resolve(JSON.parse(chunk));});}); // 返回已经上传切片名列表 const createUploadedList async fileHash fse.existsSync(${UPLOAD_DIR}/${fileHash}) ? await fse.readdir(${UPLOAD_DIR}/${fileHash}) : [];server.on(request, async (req, res) {if (req.url /verify) {const data await resolvePost(req);const { fileHash, filename } data;const ext extractExt(filename);const filePath ${UPLOAD_DIR}/${fileHash}${ext};if (fse.existsSync(filePath)) {res.end(JSON.stringify({shouldUpload: false}));} else {res.end(JSON.stringify({shouldUpload: true uploadedList: await createUploadedList(fileHash)}));}}});server.listen(3000, () console.log(正在监听 3000 端口));接着回到前端前端有两个地方需要调用验证的接口点击上传时检查是否需要上传和已上传的切片点击暂停后的恢复上传返回已上传的切片新增恢复按钮并改造原来上传切片的逻辑typefilechangehandleFileChange/上传暂停 恢复//... async handleResume() { const { uploadedList } await this.verifyUpload( this.container.file.name, this.container.hash ); await this.uploadChunks(uploadedList);},async handleUpload() {if (!this.container.file) return;const fileChunkList this.createFileChunk(this.container.file);this.container.hash await this.calculateHash(fileChunkList); const { shouldUpload, uploadedList } await this.verifyUpload(this.container.file.name,this.container.hash);if (!shouldUpload) {this.$message.success(秒传上传成功);return;}this.data fileChunkList.map(({ file }, index) ({fileHash: this.container.hash,index,hash: this.container.hash - index,chunk: filepercentage: 0})); await this.uploadChunks(uploadedList);},// 上传切片同时过滤已上传的切片 async uploadChunks(uploadedList []) {const requestList this.data .filter(({ hash }) !uploadedList.includes(hash)).map(({ chunk, hash, index }) {const formData new FormData();formData.append(chunk, chunk);formData.append(hash, hash);formData.append(filename, this.container.file.name);formData.append(fileHash, this.container.hash);return { formData, index };}).map(async ({ formData, index }) this.request({url: http://localhost:3000,data: formData,onProgress: this.createProgressHandler(this.data[index]),requestList: this.requestList}));await Promise.all(requestList);// 之前上传的切片数量 本次上传的切片数量 所有切片数量时// 合并切片 if (uploadedList.length requestList.length this.data.length) {await this.mergeRequest(); }}这里给原来上传切片的函数新增 uploadedList 参数即上图中服务端返回的切片名列表通过 filter 过滤掉已上传的切片并且由于新增了已上传的部分所以之前合并接口的触发条件做了一些改动到这里断点续传的功能基本完成了进度条改进虽然实现了断点续传但还需要修改一下进度条的显示规则否则在暂停上传/接收到已上传切片时的进度条会出现偏差切片进度条由于在点击上传/恢复上传时会调用验证接口返回已上传的切片所以需要将已上传切片的进度变成 100%async handleUpload() {if (!this.container.file) return;const fileChunkList this.createFileChunk(this.container.file);this.container.hash await this.calculateHash(fileChunkList);const { shouldUpload, uploadedList } await this.verifyUpload(this.container.file.name,this.container.hash);if (!shouldUpload) {this.$message.success(秒传上传成功);return;}this.data fileChunkList.map(({ file }, index) ({fileHash: this.container.hash,index,hash: this.container.hash - index,chunk: file, percentage: uploadedList.includes(index) ? 100 : 0}));await this.uploadChunks(uploadedList);},uploadedList 会返回已上传的切片在遍历所有切片时判断当前切片是否在已上传列表里即可文件进度条之前说到文件进度条是一个计算属性根据所有切片的上传进度计算而来这就遇到了一个问题点击暂停会取消并清空切片的 xhr 请求此时如果已经上传了一部分就会发现文件进度条有倒退的现象当点击恢复时由于重新创建了 xhr 导致切片进度清零所以总进度条就会倒退解决方案是创建一个“假”的进度条这个假进度条基于文件进度条但只会停止和增加然后给用户展示这个假的进度条这里我们使用 Vue 的监听属性data: () ({ fakeUploadPercentage: 0}),computed: {uploadPercentage() {if (!this.container.file || !this.data.length) return 0;const loaded this.data.map(item item.size * item.percentage).reduce((acc, cur) acc cur);return parseInt((loaded / this.container.file.size).toFixed(2));}},watch: { uploadPercentage(now) { if (now this.fakeUploadPercentage) { this.fakeUploadPercentage now; }}},当 uploadPercentage 即真的文件进度条增加时fakeUploadPercentage 也增加一旦文件进度条后退假的进度条只需停止即可至此一个大文件上传 断点续传的解决方案就完成了总结大文件上传前端上传大文件时使用 Blob.prototype.slice 将文件切片并发上传多个切片最后发送一个合并的请求通知服务端合并切片服务端接收切片并存储收到合并请求后使用 fs.appendFileSync 对多个切片进行合并原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度断点续传使用 spart-md5 根据文件内容算出文件 hash通过 hash 可以判断服务端是否已经上传该文件从而直接提示用户上传成功(秒传)通过 XMLHttpRequest 的 abort 方法暂停切片的上传上传前服务端返回已经上传的切片名前端跳过这些切片的上传源代码源代码增加了一些按钮的状态交互更加友好文章表达比较晦涩的地方可以跳转到源代码查看file-upload(https://github.com/yeyan1996/file-upload)控制网络速度.文章来源:
http://www.pierceye.com/news/734731/

相关文章:

  • 怎样增加网站会员量微信商城怎么进入
  • 网站建设目的功能行业门户网站源码
  • 建设网站收费标准100m做电影网站
  • 怎么样自己做最简单的网站wordpress酷黑主题
  • 长沙市建设网站网站修改域名
  • 邢台企业做网站价格如何做网络推广运营
  • 番禺网站建设服务百度广告推广价格
  • 什么系统做网站最安全网络科技网站设计
  • 通州网站建设是什么查看网站开发商
  • 建设网站公司浩森宇特怎么推广公司网站
  • 来宾住房和城乡建设网站变装第三性wordpress
  • 自己开发网站怎么开发站长工具网址是多少
  • 农业交易平台网站建设成都微信网站开发
  • 十大网站app软件网站建设企业的市场分析
  • 建设网站教程视频下载企业所得税税率2019
  • 网站图片上传不了是什么原因建筑施工图设计
  • 做网站必须要公网ip个人主页网页设计教程
  • 智能家居网站开发成都家装设计公司排名榜
  • 中国建设银行门户网站企业wordpress锁
  • 购物网站建设所需软件科技部网站公布首批创新型县(市)建设名单
  • 网站访客qq抓取淘宝官网首页入口
  • 网站界面设计实训总结cp wordpress
  • 网站建设综合案例济宁北湖建设局网站
  • 青岛网站建设运营网络程序
  • 哈铁工程建设公司网站可做推广的网站
  • 建湖做网站哪家最好计算机网络中小型企业网络设计方案
  • 如何用 python 做网站网站后台seo优化如何做
  • 网站搭建徐州百度网络c2c平台是洗钱吗
  • 二级域名站群微信上浏览自己做的网站吗
  • 十堰微网站建设费用ps做网站尺寸