-
首先解释一下什么是分片上传 分片上传就是把一个大的文件分成若干块,一块一块的传输。这样做的好处可以减少重新上传的开销。比如:如果我们上传的文件是一个很大的文件,那么上传的时间应该会比较久,再加上网络不稳定各种因素的影响,很容易导致传输中断,用户除了重新上传文件外没有其他的办法,但是我们可以使用分片上传来解决这个问题。通过分片上传技术,如果网络传输中断,我们重新选择文件只需要传剩余的分片。而不需要重传整个文件,大大减少了重传的开销。 但是我们要如何选择一个合适的分片呢?因此我们要考虑如下几个事情: 1.分片越小,那么请求肯定越多,开销就越大。因此不能设置太小。 2.分片越大,灵活度就少了。 3.服务器端都会有个固定大小的接收Buffer。分片的大小最好是这个值的整数倍。 分片上传的步骤 1.先对文件进行md5加密。使用md5加密的优点是:可以对文件进行唯一标识,同样可以为后台进行文件完整性校验进行比对。 2.拿到md5值以后,服务器端查询下该文件是否已经上传过,如果已经上传过的话,就不用重新再上传。 3.对大文件进行分片。比如一个100M的文件,我们一个分片是5M的话,那么这个文件可以分20次上传。 4.向后台请求接口,接口里的数据就是我们已经上传过的文件块。(注意:为什么要发这个请求?就是为了能断点续传,比如我们使用百度网盘对吧,网盘里面有续传功能,当一个文件传到一半的时候,突然想下班不想上传了,那么服务器就应该记住我之前上传过的文件块,当我打开电脑重新上传的时候,那么它应该跳过我之前已经上传的文件块。再上传后续的块)。 5.开始对未上传过的文件块进行上传。(这个是第二个请求,会把所有的分片合并,然后上传请求)。 6.上传成功后,服务器会进行文件合并。最后完成。 话不多说,直接开始干代码 <template> <div> <!-- on-preview 点击文件列表中已上传的文件时的钩子 --> <!-- http-request 覆盖默认的上传行为,可以自定义上传的实现 --> <!-- limit 最大允许上传个数 --> <!-- before-upload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 --> <!-- accept 接受上传的文件类型(thumbnail-mode 模式下此参数无效) --> <!-- multiple 是否支持多选文件 --> <!-- on-change 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 --> <!-- on-remove 文件列表移除文件时的钩子 --> <!-- file-list 上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] --> <!-- on-exceed 文件超出个数限制时的钩子 --> <!-- auto-upload 是否在选取文件后立即进行上传 --> <!-- action 必选参数,上传的地址 例如 action="https://jsonplaceholder.typicode.com/posts/"--> <el-upload drag multiple :auto-upload="true" :http-request="checkedFile" :before-remove="removeFile" :limit="10" action="/tools/upload_test/" > <i class="el-icon-upload"></i> <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> </el-upload> <el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress> </div> </template> 文件上传时,会走http-request方法,如果定义了这个方法,组件的submit方法就会被拦截掉(注意别在这个方法里面调用组件的submit 方法,会造成死循环),在这个方法里面我就可以搞我想搞的事情了。 http-request 这个传入的回调函数应该返回一个Promise,所以我自己定义了一个无用的Promise进去 const prom = new Promise((resolve, reject) => {}) prom.abort = () => {} return prom 如果要实现断点续传,需要和后端确定好,如何配合。 我这里的方案是,在我把所有的分片全部上传一遍后,会请求一个查询接口,后端在这个接口里面返回给我哪些分片没有上传成功(会给我序号),我这个时候,再去重新上传那些没有被上传成功的分片 直接贴完整代码,注释都在里面,看不懂的可以直接联系我,博客上有联系方式(依赖element-ui、axios、spark-md5) <template> <div> <!-- on-preview 点击文件列表中已上传的文件时的钩子 --> <!-- http-request 覆盖默认的上传行为,可以自定义上传的实现 --> <!-- limit 最大允许上传个数 --> <!-- before-upload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 --> <!-- accept 接受上传的文件类型(thumbnail-mode 模式下此参数无效) --> <!-- multiple 是否支持多选文件 --> <!-- on-change 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 --> <!-- on-remove 文件列表移除文件时的钩子 --> <!-- file-list 上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] --> <!-- on-exceed 文件超出个数限制时的钩子 --> <!-- auto-upload 是否在选取文件后立即进行上传 --> <!-- action 必选参数,上传的地址 例如 action="https://jsonplaceholder.typicode.com/posts/"--> <el-upload drag multiple :auto-upload="true" :http-request="checkedFile" :before-remove="removeFile" :limit="10" action="/tools/upload_test/" > <i class="el-icon-upload"></i> <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> </el-upload> <el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress> </div> </template> <script> import axios from "axios"; import SparkMD5 from "spark-md5"; export default { data() { return { maxSize: 5 * 1024 * 1024 * 1024, // 上传最大文件限制 最小单位是b multiUploadSize: 100 * 1024 * 1024, // 大于这个大小的文件使用分块上传(后端可以支持断点续传) 100mb eachSize: 100 * 1024 * 1024, // 每块文件大小 100mb requestCancelQueue: [], // 请求方法队列(调用取消上传 url: "/tools/upload_test/", //上传进度 progress: 0, showProgress: false, // 每上传一块的进度 eachProgress: 0, // 总共有多少块。断点续传使用 chunksKeep:0, // 切割后的文件数组 fileChunksKeep:[], // 这个文件,断点续传 fileKeep:null }; }, mounted() { }, methods: { async checkedFile(options) { console.log(options); const { maxSize, multiUploadSize, getSize, splitUpload, singleUpload } = this; // 解构赋值 const { file, onProgress, onSuccess, onError } = options; // 解构赋值 if (file.size > maxSize) { return this.$message({ message: `您选择的文件大于${getSize(maxSize)}`, type: "error" }); } this.fileKeep = file const uploadFunc = file.size > multiUploadSize ? splitUpload : singleUpload; // 选择上传方式 try { await uploadFunc(file, onProgress); this.$message({ message: "上传成功", type: "success" }); this.showProgress = false; this.progress = 0; onSuccess(); } catch (e) { console.error(e); this.$message({ message: e.message, type: "error" }); this.showProgress = false; this.progress = 0; onError(); } const prom = new Promise((resolve, reject) => {}); // 上传后返回一个promise prom.abort = () => {}; return prom; }, // 格式化文件大小显示文字 getSize(size) { return size > 1024 ? size / 1024 > 1024 ? size / (1024 * 1024) > 1024 ? (size / (1024 * 1024 * 1024)).toFixed(2) + "GB" : (size / (1024 * 1024)).toFixed(2) + "MB" : (size / 1024).toFixed(2) + "KB" : size.toFixed(2) + "B"; }, // 单文件直接上传 async singleUpload(file, onProgress) { await this.postFile( { file, uid: file.uid, fileName: file.fileName ,chunk:0}, onProgress ); var spark = new SparkMD5.ArrayBuffer(); spark.append(file); var md5 = spark.end(); console.log(md5); const isValidate = await this.validateFile({ fileName: file.name, uid: file.uid, md5:md5, chunks:1 }); }, // 大文件分块上传 splitUpload(file, onProgress) { console.log('file11') console.log(file) return new Promise(async (resolve, reject) => { try { const { eachSize } = this; const chunks = Math.ceil(file.size / eachSize); this.chunksKeep = chunks const fileChunks = await this.splitFile(file, eachSize, chunks); this.fileChunksKeep = fileChunks console.log('fileChunks,文件数组切割后') console.log(fileChunks) //判断每上传一个文件,进度条涨多少,保留两位小数 this.eachProgress = parseInt(Math.floor(100 / chunks * 100) / 100); this.showProgress = true; let currentChunk = 0; for (let i = 0; i < fileChunks.length; i++) { // 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传 console.log(currentChunk, i); // 此时需要判断进度条 if (Number(currentChunk) === i) { // 每块上传完后则返回需要提交的下一块的index await this.postFile( { chunked: true, chunk: i, chunks, eachSize, fileName: file.name, fullSize: file.size, uid: file.uid, file: fileChunks[i] }, onProgress ); currentChunk++ // 上传完一块后,进度条增加 this.progress += this.eachProgress; // 不能超过100 this.progress = this.progress > 100 ? 100 : this.progress; } } var spark = new SparkMD5.ArrayBuffer(); spark.append(file); var md5 = spark.end(); console.log(md5); const isValidate = await this.validateFile({ chunks: fileChunks.length, // chunk: fileChunks.length, fileName: file.name, uid: file.uid, md5:md5, // task_id:file.uid }); // if (!isValidate) { // throw new Error("文件校验异常"); // } resolve(); } catch (e) { reject(e); } }); }, againSplitUpload(file, array) { console.log('file,array') console.log(file) console.log(array) return new Promise(async (resolve, reject) => { try { const { eachSize , fileKeep } = this; const chunks = this.chunksKeep const fileChunks = this.fileChunksKeep this.showProgress = true; // let currentChunk = 0; for (let i = 0; i < array.length; i++) { // 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传 // console.log(currentChunk, i); // 此时需要判断进度条 // 每块上传完后则返回需要提交的下一块的index await this.postFile( { chunked: true, chunk: array[i], chunks, fileName: file.fileName, fullSize: fileKeep.size, uid: file.uid, file: fileChunks[array[i]] }, ); // currentChunk++ // 上传完一块后,进度条增加 // this.progress += this.eachProgress; // 不能超过100 this.progress = this.progress > 100 ? 100 : this.progress; } var spark = new SparkMD5.ArrayBuffer(); spark.append(fileKeep); var md5 = spark.end(); console.log(md5); const isValidate = await this.validateFile({ chunks: fileChunks.length, // chunk: fileChunks.length, fileName: file.fileName, uid: file.uid, md5:md5, // task_id:file.uid }); // if (!isValidate) { // throw new Error("文件校验异常"); // } resolve(); } catch (e) { reject(e); } }); }, // 文件分块,利用Array.prototype.slice方法 splitFile(file, eachSize, chunks) { return new Promise((resolve, reject) => { try { setTimeout(() => { const fileChunk = []; for (let chunk = 0; chunks > 0; chunks--) { fileChunk.push(file.slice(chunk, chunk + eachSize)); chunk += eachSize; } resolve(fileChunk); }, 0); } catch (e) { console.error(e); reject(new Error("文件切块发生错误")); } }); }, removeFile(file) { this.requestCancelQueue[file.uid](); delete this.requestCancelQueue[file.uid]; return true; }, // 提交文件方法,将参数转换为FormData, 然后通过axios发起请求 postFile(param, onProgress) { console.log(param); const formData = new FormData(); // for (let p in param) { // formData.append(p, param[p]); // } formData.append('file', param.file) // 改了 formData.append('uid',param.uid) formData.append('chunk',param.chunk) const { requestCancelQueue } = this; const config = { cancelToken: new axios.CancelToken(function executor(cancel) { if (requestCancelQueue[param.uid]) { requestCancelQueue[param.uid](); delete requestCancelQueue[param.uid]; } requestCancelQueue[param.uid] = cancel; }), onUploadProgress: e => { if (param.chunked) { e.percent = Number( ( ((param.chunk * (param.eachSize - 1) + e.loaded) / param.fullSize) * 100 ).toFixed(2) ); } else { e.percent = Number(((e.loaded / e.total) * 100).toFixed(2)); } onProgress(e); } }; // return axios.post('/api/v1/tools/upload_test/', formData, config).then(rs => rs.data) return this.$http({ url: "/tools/upload_test/", method: "POST", data: formData // config }).then(rs => rs.data); }, // 文件校验方法 validateFile(file) { // return axios.post('/api/v1/tools/upload_test/', file).then(rs => rs.data) console.log(2) console.log(file) return this.$http({ url: "/tools/upload_test/upload_success/", method: "POST", data: file }).then(res => { if(res && res.status == 1){ this.againSplitUpload(file,res.data.error_file) return true } }); } } }; </script> <style scoped> .progress{ /* 在当前页面居中 */ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); /* 宽度 */ } </style> 更新代码,上面的代码使用md5加密后,与后端加密的MD5值不一样,下面的加密过后是一样的 <template> <div :class="showProgress == true ? 'loading' : ''"> <!-- on-preview 点击文件列表中已上传的文件时的钩子 --> <!-- http-request 覆盖默认的上传行为,可以自定义上传的实现 --> <!-- limit 最大允许上传个数 --> <!-- before-upload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 --> <!-- accept 接受上传的文件类型(thumbnail-mode 模式下此参数无效) --> <!-- multiple 是否支持多选文件 --> <!-- on-change 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 --> <!-- on-remove 文件列表移除文件时的钩子 --> <!-- file-list 上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] --> <!-- on-exceed 文件超出个数限制时的钩子 --> <!-- auto-upload 是否在选取文件后立即进行上传 --> <!-- action 必选参数,上传的地址 例如 action="https://jsonplaceholder.typicode.com/posts/"--> <el-upload drag multiple :auto-upload="true" :http-request="checkedFile" :before-remove="removeFile" :limit="10" action="/tools/upload_chunk/" :disabled="showProgress"> <i class="el-icon-upload"></i> <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> </el-upload> <!-- 正在上传的弹窗 --> <el-dialog title="正在上传" :visible.sync="showProgress" width="50%"> <el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress> </el-dialog> <!-- <el-progress type="circle" :percentage="progress" class="progress" v-if="showProgress"></el-progress> --> </div> </template> <script> import axios from "axios"; import SparkMD5 from "spark-md5"; export default { data() { return { maxSize: 5 * 1024 * 1024 * 1024, // 上传最大文件限制 最小单位是b multiUploadSize: 100 * 1024 * 1024, // 大于这个大小的文件使用分块上传(后端可以支持断点续传) 100mb eachSize: 100 * 1024 * 1024, // 每块文件大小 100mb requestCancelQueue: [], // 请求方法队列(调用取消上传 url: "/tools/upload_chunk/", //上传进度 progress: 0, showProgress: false, // 每上传一块的进度 eachProgress: 0, // 总共有多少块。断点续传使用 chunksKeep: 0, // 切割后的文件数组 fileChunksKeep: [], // 这个文件,断点续传 fileKeep: null, // 断点续传,文件md5 fileMd5Keep: "" }; }, mounted() { }, methods: { async checkedFile(options) { // console.log(options); const { maxSize, multiUploadSize, getSize, splitUpload, singleUpload } = this; // 解构赋值 const { file, onProgress, onSuccess, onError } = options; // 解构赋值 if (file.size > maxSize) { return this.$message({ message: `您选择的文件大于${getSize(maxSize)}`, type: "error" }); } this.fileKeep = file; const uploadFunc = file.size > multiUploadSize ? splitUpload : singleUpload; // 选择上传方式 try { await uploadFunc(file, onProgress); onSuccess(); } catch (e) { console.error(e); this.$message({ message: e.message, type: "error" }); this.showProgress = false; this.progress = 0; onError(); } const prom = new Promise((resolve, reject) => { }); // 上传后返回一个promise prom.abort = () => { }; return prom; }, // 格式化文件大小显示文字 getSize(size) { return size > 1024 ? size / 1024 > 1024 ? size / (1024 * 1024) > 1024 ? (size / (1024 * 1024 * 1024)).toFixed(2) + "GB" : (size / (1024 * 1024)).toFixed(2) + "MB" : (size / 1024).toFixed(2) + "KB" : size.toFixed(2) + "B"; }, // 单文件直接上传 async singleUpload(file, onProgress) { await this.postFile( { file, uid: file.uid, fileName: file.fileName, chunk: 0 }, onProgress ); // var spark = new SparkMD5.ArrayBuffer(); // spark.append(file); // var md5 = spark.end(); // console.log(md5); const reader = new FileReader(); reader.readAsArrayBuffer(file); let hashMd5 = ""; console.log(hashMd5); const that = this; function getHash(cb) { console.log("进入单个上传的getHash"); reader.onload = function (e) { console.log("进入单个上传的getHash的函数2"); console.log(hashMd5); console.log(this); // console.log(e) const hash = SparkMD5.ArrayBuffer.hash(e.target.result); // const hash = SparkMD5.ArrayBuffer.hash(file); console.log(hash); that.hashMd5 = hash; console.log(that.hashMd5); that.fileMd5Keep = hash; cb(hash); }; } await getHash(function (hash) { console.log(hash); console.log(that); // 请求接口 that.validateFile({ name: file.name, uid: file.uid, md5: hash, chunks: 1, filter_type: "user_data_file" }); }); }, // getMd5(file, chunkCount) { // const spark = new SparkMD5.ArrayBuffer(); // let currentChunk = 0; // const reader = new FileReader(); // reader.onload = function(e) { // spark.append(e.target.result); // currentChunk++; // if (currentChunk < chunkCount) { // console.log(currentChunk); // loadNext(); // } else { // console.log(spark.end()); // // 在这里请求接口 // return spark.end(); // } // }; // function loadNext() { // const start = currentChunk * chunkSize; // const end = // start + chunkSize >= file.size ? file.size : start + chunkSize; // reader.readAsArrayBuffer(file.slice(start, end)); // } // loadNext(); // }, // 大文件分块上传 splitUpload(file, onProgress) { return new Promise(async (resolve, reject) => { try { const { eachSize } = this; const chunks = Math.ceil(file.size / eachSize); this.chunksKeep = chunks; const fileChunks = await this.splitFile(file, eachSize, chunks); this.fileChunksKeep = fileChunks; console.log("fileChunks,文件数组切割后"); console.log(fileChunks); //判断每上传一个文件,进度条涨多少,保留两位小数 this.eachProgress = parseInt(Math.floor((100 / chunks) * 100) / 100); this.showProgress = true; let currentChunk = 0; for (let i = 0; i < fileChunks.length; i++) { // 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传 console.log(currentChunk, i); // 此时需要判断进度条 if (Number(currentChunk) === i) { // 每块上传完后则返回需要提交的下一块的index await this.postFile( { chunked: true, chunk: i, chunks, eachSize, fileName: file.name, fullSize: file.size, uid: file.uid, file: fileChunks[i] }, onProgress ); currentChunk++; // 上传完一块后,进度条增加 this.progress += this.eachProgress; // 不能超过100 this.progress = this.progress > 100 ? 100 : this.progress; } } // this.getMd5(file, chunks); // var spark = new SparkMD5.ArrayBuffer(); // spark.append(file); // var md5 = spark.end(); // console.log(md5); const spark = new SparkMD5.ArrayBuffer(); let currentChunkMd5 = 0; const that = this; const reader = new FileReader(); reader.onload = async function (e) { spark.append(e.target.result); currentChunkMd5++; if (currentChunkMd5 < chunks) { loadNext(); } else { // console.log(spark.end()); var hashMd5111 = spark.end(); that.fileMd5Keep = hashMd5111; console.log(that); console.log(hashMd5111); // 在这里请求接口 await that.validateFile({ name: file.name, uid: file.uid, md5: hashMd5111, chunks: fileChunks.length, filter_type: "git_secret_file" // chunk: fileChunks.length, }); } }; async function loadNext() { const start = currentChunkMd5 * eachSize; const end = start + eachSize >= file.size ? file.size : start + eachSize; await reader.readAsArrayBuffer(file.slice(start, end)); } this.$message({ message: "正在进行文件加密校验", type: "info" }); await loadNext(); // let hashMd5 = ""; // // console.log(hashMd5) // const that = this; // console.log("进入分片上传的getHash"); // function getHash(cb) { // reader.onload = function(e) { // console.log("进入分片上传的getHash的函数"); // const hash = SparkMD5.ArrayBuffer.hash(e.target.result); // // const hash = SparkMD5.ArrayBuffer.hash(file); // console.log(hash); // that.hashMd5 = hash; // console.log(that.hashMd5); // that.fileMd5Keep = hash; // cb(hash); // }; // reader.readAsArrayBuffer(file); // } // await getHash(function() { // console.log(that); // that.validateFile({ // name: file.name, // uid: file.uid, // md5: that.hashMd5, // chunks: fileChunks.length // // chunk: fileChunks.length, // }); // }); // 请求接口 // console.log('fileChunks.length') // 请求接口 // this.validateFile({ // fileName: file.name, // uid: file.uid, // md5:md5, // chunks:1 // }); resolve(); } catch (error) { reject(error); } }); }, // 断点续传 againSplitUpload(file, array) { console.log("file,array"); console.log(file); console.log(array); return new Promise(async (resolve, reject) => { try { const { eachSize, fileKeep } = this; const chunks = this.chunksKeep; const fileChunks = this.fileChunksKeep; this.showProgress = true; // let currentChunk = 0; for (let i = 0; i < array.length; i++) { // 服务端检测已经上传到第currentChunk块了,那就直接跳到第currentChunk块,实现断点续传 // console.log(currentChunk, i); // 此时需要判断进度条 // 每块上传完后则返回需要提交的下一块的index await this.postFile({ chunked: true, chunk: array[i], chunks, name: file.name, fullSize: fileKeep.size, uid: file.uid, file: fileChunks[array[i]] }); // currentChunk++ // 上传完一块后,进度条增加 // this.progress += this.eachProgress; // 不能超过100 this.progress = this.progress > 100 ? 100 : this.progress; } // var spark = new SparkMD5.ArrayBuffer(); // spark.append(fileKeep); // var md5 = spark.end(); // console.log(md5); var fileMd5KeepTwo = this.fileMd5Keep; const isValidate = await this.validateFile({ chunks: fileChunks.length, // chunk: fileChunks.length, name: file.name, uid: file.uid, md5: fileMd5KeepTwo, filter_type: "git_secret_file" // task_id:file.uid }); // if (!isValidate) { // throw new Error("文件校验异常"); // } // 关闭进度条 this.showProgress = false; // 重置进度条 this.progress = 0; resolve(); } catch (e) { reject(e); } }); }, // 文件分块,利用Array.prototype.slice方法 splitFile(file, eachSize, chunks) { return new Promise((resolve, reject) => { try { setTimeout(() => { const fileChunk = []; for (let chunk = 0; chunks > 0; chunks--) { fileChunk.push(file.slice(chunk, chunk + eachSize)); chunk += eachSize; } resolve(fileChunk); }, 0); } catch (e) { console.error(e); reject(new Error("文件切块发生错误")); } }); }, removeFile(file) { this.requestCancelQueue[file.uid](); delete this.requestCancelQueue[file.uid]; return true; }, // 提交文件方法,将参数转换为FormData, 然后通过axios发起请求 postFile(param, onProgress) { // console.log(param); const formData = new FormData(); // for (let p in param) { // formData.append(p, param[p]); // } formData.append("file", param.file); // 改了 formData.append("uid", param.uid); formData.append("chunk", param.chunk); formData.append("filter_type", "git_secret_file"); const { requestCancelQueue } = this; const config = { cancelToken: new axios.CancelToken(function executor(cancel) { if (requestCancelQueue[param.uid]) { requestCancelQueue[param.uid](); delete requestCancelQueue[param.uid]; } requestCancelQueue[param.uid] = cancel; }), onUploadProgress: e => { if (param.chunked) { e.percent = Number( ( ((param.chunk * (param.eachSize - 1) + e.loaded) / param.fullSize) * 100 ).toFixed(2) ); } else { e.percent = Number(((e.loaded / e.total) * 100).toFixed(2)); } onProgress(e); } }; // return axios.post('/api/v1/tools/upload_chunk/', formData, config).then(rs => rs.data) return this.$http({ url: "/tools/upload_chunk/", method: "POST", data: formData // config }).then(rs => rs.data); }, // 文件校验方法 validateFile(file) { // return axios.post('/api/v1/tools/upload_chunk/', file).then(rs => rs.data) return this.$http({ url: "/tools/upload_chunk/upload_success/", method: "POST", data: file }).then(res => { if (res && res.status == 1) { this.againSplitUpload(file, res.data.error_file); this.$message({ message: "有文件上传失败,正在重新上传", type: "warning" }); } else if (res && res.status == 0) { this.$message({ message: "上传成功", type: "success" }); this.showProgress = false; this.progress = 0; } else if (res && res.status == 40008) { this.$message.error(res.message); this.showProgress = false; this.progress = 0; } }); } } }; </script> <style scoped> .loading { /* 整体页面置灰 */ /* background: rgba(0, 0, 0, 0.5); */ } .progress { /* 在当前页面居中 */ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); margin-top: 40px; /* 宽度 */ } /deep/ .el-dialog { position: relative; height: 500px; } </style> ———————————————— 版权声明:本文为CSDN博主「yjxkq99」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/yjxkq99/article/details/128942133
-
van-uploader参考文档 限制上传数量 通过 max-count 属性可以限制上传文件的数量,上传数量达到限制后,会自动隐藏上传区域。 限制上传大小 通过 max-size 属性可以限制上传文件的大小,超过大小的文件会被自动过滤,文件信息通过 oversize 事件获取。 文件上传前进行校验和处理 通过before-read 传入函数,可以在上传前进行校验和处理,支持返回 Promise 对 file 对象进行自定义处理。例如压缩图片:使用compressorjs压缩图片,优化功能,压缩所有格式的图片。 文件上传完毕后获取数据 文件上传后会触发 after-read 回调函数,获取到对应的 file 对象。 更多细节请参考vant官方文档 <template> <div> <van-uploader v-model="fileList" :max-count="1" :max-size="2048 * 1024" :after-read="OnAfterRead" :before-read="onBeforeRead" @oversize="onOversize" @delete="onDelete" /> </div> </template> <script> import Compressor from "compressorjs"; export default { data() { return { fileList: [], IMG_LIST: "", //图片路径 }; }, methods: { OnAfterRead(result) { let newImage = new Image(); // 这里将src传入需要获取信息的图片地址或base64 newImage.src = result.content; newImage.onload = () => { // 输出图片信息 比如可以获取图片宽高 console.log("图片宽", newImage.width); console.log("图片高", newImage.height); this.fileList[0].url = result.content; // 点击上传图片的结果(base64的图片字符串)IMG_LIST this.IMG_LIST = this.fileList[0].url; // 控制台打印 console.log(" this.IMG_LIST ", this.IMG_LIST); }; }, //上传图片压缩,需要安装依赖: npm i compressorjs,并引入(import) 图片0.6倍压缩 onBeforeRead(file) { return new Promise((resolve) => { new Compressor(file, { quality: 0.6, success: resolve, error(err) { console.log('图片压缩失败---->>>>>',err.message); }, }); }); }, //图片大小超过2M提示 onOversize(file) { this.$toast("图片大小不能超过 2M"); }, //点击移除图片 onDelete() { this.fileList = []; }, }, }; </script> <style> </style> ———————————————— 版权声明:本文为CSDN博主「前端小程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/CSSAJBQ_/article/details/128012890
-
Vue.js 是一个流行的 JavaScript 框架,可以用于构建交互式的 Web 应用程序。要在 Vue.js 中使用 v-viewer 插件,您可以按照以下步骤进行操作:安装 v-viewer:您可以使用 npm 或 yarn 安装 v-viewer 插件。使用 npm 进行安装:npm install v-viewer --save或使用 yarn 进行安装:yarn add v-viewer导入和使用 v-viewer:在您的 Vue 组件中,导入 v-viewer 并使用它。// 在您的组件文件中 import Viewer from 'v-viewer'; import 'viewerjs/dist/viewer.css'; export default { // 组件的其他选项 // 在组件中注册 v-viewer mounted() { this.$nextTick(() => { this.initViewer(); }); }, methods: { initViewer() { this.$viewer = new Viewer(this.$refs.viewerWrapper); }, }, };您需要引入 viewer.css 样式文件,确保 v-viewer 的样式正确加载。在模板中使用 v-viewer:在需要展示图片的模板中,使用 v-for 循环来渲染图片,并使用 ref 添加引用以便在组件中使用。<template> <div> <div ref="viewerWrapper"> <img v-for="(image, index) in images" :src="image" :key="index" @click="$viewer.show()"> </div> </div> </template> <script> export default { // 组件的其他选项 data() { return { images: ['image1.jpg', 'image2.jpg', 'image3.jpg'], }; }, }; </script>在上述示例中,images 数组包含了要展示的图片地址。当点击图片时,使用 $viewer.show() 方法来显示图片。以上是在 Vue.js 中使用 v-viewer 的简单示例。您可以根据自己的需求进行进一步的配置和定制。请确保按照 v-viewer 的文档了解更多可用的选项。
-
京东微前端和阿里微前端都是当前比较流行的微前端框架,它们有各自的优点和特点。下面是对两者进行简要对比的一些关键点:总体架构:京东微前端:京东微前端采用了基于 Web Components 的技术栈,使用自研的微前端框架来进行组织和管理微前端应用。阿里微前端:阿里微前端采用了基于微前端架构飞冰(Ice)的技术栈,使用自研的插件化中台框架来实现微前端的组织和运行。技术栈和生态系统:京东微前端:京东微前端使用了 Web Components 技术,不依赖特定的前端框架,可以与任意前端技术栈集成。它提供了一套完整的解决方案,包括构建、打包、路由、通信等。阿里微前端:阿里微前端基于飞冰(Ice)框架,使用 React 和 Vue 作为主要的前端技术栈。它也提供了一套完整的解决方案,包括构建、打包、路由、通信等,并且有丰富的生态系统和插件可用。生态和社区支持:京东微前端:虽然京东微前端是京东自研的框架,但它在社区中的影响力逐渐增强,受到一些开发者的关注和支持。但它的生态系统相对较小。阿里微前端:阿里微前端基于飞冰(Ice)框架,得到了阿里巴巴集团内众多项目的使用和支持,具有较大的社区和生态系统,有更多的插件和工具可供选择。适用场景和定位:京东微前端:京东微前端定位于构建复杂应用系统的微前端架构,适用于需要将多个独立的前端应用集成在一起的场景。阿里微前端:阿里微前端定位于构建企业级中后台应用的微前端架构,适用于大型系统和组织内部的微服务架构。需要注意的是,以上对比仅是针对一些关键点进行的简要对比,具体选型还需要根据实际需求和具体情况进行综合考虑。对于特定的项目和团队,这些微前端框架都可能是合适的选择。建议在选择之前详细了解它们的特点、文档和使用案例,并根据实际需求进行评估和测试。
-
canvas是什么? 一个html5支持的新标签,见名知意,canvas就是画板的意思,可以在canvas上画画。css画三角形很简单,但是要画五角星呢,不妨试试canvas。 在html中使用canvas 1、canvas是html5中的一个标签。 新建一个html。并在body中加入canvas标签。 <body> <canvas height="600" width="600"></canvas> </body> 此时canvas已经显示在画板中,只不过因为和body的颜色一样,所以看不出来。 在head中加入css样式。 <style> canvas { border:1px solid; } </style> 这时我们就可以看到canvas了。 2、获取CanvasRenderingContext2D对象用于绘图。 先给canvas一个id属性, <canvas id='canvas' height="600" width="600"></canvas> 1 获取。 <script> const ctx=document.querySelector('#canvas').getContext('2d'); </script> 注意,script标签应该在body标签后(至少在canvas标签后),只有在canvas渲染后才能通过JavaScript代码获取到canvas中的CanvasRenderingContext2D对象。 <body> <canvas height="600" width="600"></canvas> </body> <script> const ctx=document.querySelector('.canvas').getContext('2d'); </script> 3、使用JavaScript代码进行绘画。 <script> const ctx=document.querySelector('#canvas').getContext('2d'); ctx.moveTo(100,100); ctx.lineTo(100,400); ctx.stroke(); </script> 该代码绘制了一条直线。 关于CanvasRenderingContext2D对象更多的绘制方法请参考官方文档。至少现在我们知道canvas是如何使用的了。(一定要注意要在渲染完成后才能通过JavaScript代码获取CanvasRenderingContext2D对象) 在vue3中使用canvas 1、创建vite+vue3项目并运行。 npm init vue@latest 1 2、创建我们的canvas。 这是我们的App.vue文件 <script setup> </script> <template> </template> <style scoped> </style> 创建我们的canvas <script setup> </script> <template> <canvas height="600" width="600"></canvas> </template> <style scoped> canvas { border: 1px solid; } </style> 3、获取CanvasRenderingContext2D对象并绘制直线。 给canvas标签添加一个ref属性 <canvas ref='canvas' height="600" width="600"></canvas> 1 获取canvas对象 <script setup> import {ref} from 'vue' const canvas = ref(); </script> 渲染完成后获取CanvasRenderingContext2D对象 <script setup> import { onMounted, ref } from 'vue' const canvas = ref(); onMounted(() => { const ctx = canvas.value.getContext('2d'); }) </script> 画一条直线 <script setup> import { onMounted, ref } from 'vue' const canvas = ref(); onMounted(() => { const ctx = canvas.value.getContext('2d'); ctx.moveTo(100, 100); ctx.lineTo(100, 400); ctx.stroke(); }) </script> 4、模板 <script setup> import { onMounted, ref } from 'vue' const canvas = ref(); let ctx = ref(); const drawLine = () => { ctx.moveTo(100, 100); ctx.lineTo(100, 400); ctx.stroke(); } const initContext = () => { ctx = canvas.value.getContext('2d'); } onMounted(() => { initContext(); drawLine(); }) </script> <template> <canvas ref='canvas' height="600" width="600"></canvas> </template> <style scoped> canvas { border: 1px solid; } </style> canvas快速入门 绘制折线 一个moveTo配合多个lineTo。可以通过lineWidth设置线宽,还可以设置两个端点和转折处的形状(使用lineCap和lineJoin) // 使用moveTo,lineTo,lineWidth,lineCap,lineJoin const drawCurvedLine = () => { ctx.moveTo(100, 100); ctx.lineTo(400, 100); ctx.lineTo(100, 400); ctx.lineTo(400, 400); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.stroke(); } 绘制矩形 rect方法以及strokeRect和fillRect。效果等效:strokeRect=rect+stroke,fillRect=rect+stroke。 绘制方式:绘制边框,使用stroke,绘制填充,使用fill。strokeStyle可以设置边框颜色,fillStyle可以设置填充颜色。 // 使用rect,srokeStyle,stroke,fillStyle,fill const drawStrokeRect = () => { ctx.rect(100, 100, 100, 100); ctx.strokeStyle = 'green'; ctx.stroke(); } const drawFillRect = () => { ctx.rect(300, 100, 100, 100); ctx.fillStyle = 'blue'; ctx.fill(); } 将绘制一个绿色边框的矩形和蓝色的矩形。然而,当一同调用时,会发现变成了两个一模一样的矩形(绿色边框或者蓝色填充)。 属性作用域:解决上述问题,使用beginPath方法即可。beginPath将后面对于属性的设置隔离开来,以避免覆盖前面的属性。 // 这里还尝试了使用strokeRect和fillRect替代了rect、stroke、fill const drawStrokeRect = () => { ctx.beginPath(); ctx.strokeStyle='green'; ctx.strokeRect(100,100,100,100); } const drawFillRect = () => { ctx.beginPath(); ctx.fillStyle = 'blue'; ctx.fillRect(300, 100, 100, 100); } 绘制弧线 圆圈 ctx.beginPath(); ctx.arc(100,75,50,0,2*Math.PI); ctx.stroke(); 圆弧 ctx.beginPath(); ctx.arc(100,75,50,90/180*Math.PI,2*Math.PI); ctx.stroke(); 扇形 ctx.beginPath(); ctx.moveTo(100,75); ctx.arc(100,75,50,90/180*Math.PI,2*Math.PI); ctx.closePath(); ctx.fill(); 圆环 const RINGWIDTH = 10; ctx.beginPath(); ctx.arc(100, 75, 90, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); ctx.arc(100, 75, 90-2*RINGWIDTH, 0, 2 * Math.PI); ctx.fillStyle = 'white'; ctx.fill(); 补充: 如你所见,绘制扇形时使用了closePath,意思是将所有端点连接起来(一般是将终点和起点连接起来,形成一个闭合图形。只有图形闭合时,fill才能生效)。 所有函数的参数不需要单位。(设置字体时,ctx.font=‘40px’,需要带单位,但确实不是函数的参数) 需要角度作为参数时,都是以弧度的形式提供。计算公式,弧度=角度*Math.PI/180。90度,记为90*Math.PI/180。 更多关于画布的使用,可以查看HTML Canvas 参考手册 (w3school.com.cn) ———————————————— 版权声明:本文为CSDN博主「46590928」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_46590928/article/details/127358493
-
最近在工作中遇到一个需求,就是需要在前端实现一个错误模板Excel的下载功能。 实现下载有两种方式,一种是后端生成一个excel,放在服务器指定目录下,然后前端直接去后端拿。第二种是后端传给前端一个json的list,前端用后端传过来的json的list直接在前端合成一个excel。 第一种:后端生成excel java后端生成excel代码 生成excel工具方法 /** * @author: wu linchun * @creat: 2021-07-29 9:49 * @desc: 将list集合转成Excel文件 * list: 内容list * path: 上传的excel路径 * fileName: 上传的excel名称 **/public static String createExcel(List<? extends Object> list, String path, String fileName) {String result = "";if (list.size() == 0 || list == null) {result = "没有对象信息";} else {Object o = list.get(0);Class<? extends Object> clazz = o.getClass();String className = clazz.getSimpleName();//这里通过反射获取字段数组Field[] fields = clazz.getDeclaredFields();File folder = new File(path);if (!folder.exists()) {folder.mkdirs();}String name = fileName.concat(".xls");WritableWorkbook book = null;File file = null;try {file = new File(path.concat(File.separator).concat(name));//创建xls文件book = jxl.Workbook.createWorkbook(file);WritableSheet sheet = book.createSheet(className, 0);//列int i = 0;//行int j = 0;for (Field f : fields) {j = 0;//这里把字段名称写入excel第一行中Label label = new Label(i, j, f.getName());sheet.addCell(label);j = 1;for (Object obj : list) {Object temp = getFieldValueByName(f.getName(), obj);String strTemp = "";if (temp != null) {strTemp = temp.toString();}//把每个对象此字段的属性写入这一列excel中sheet.addCell(new Label(i, j, strTemp));j++;}i++;}book.write();result = file.getPath();} catch (Exception e) {// TODO Auto-generated catch blockresult = "SystemException";e.printStackTrace();} finally {fileName = null;name = null;folder = null;file = null;if (book != null) {try {book.close();} catch (WriteException e) {// TODO Auto-generated catch blockresult = "WriteException";e.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blockresult = "IOException";e.printStackTrace();}}}}//最后输出文件路径return result;} 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 下载文件 /** * @author: wu linchun * @creat: 2021-07-29 13:28 * @desc: 下载错误模板 **/@Overridepublic ResponseEntity<Object> downloadErrorModel() throws FileNotFoundException {// 这里的fileName是指路径+文件名String fileName="move_cms/src/main/resources/static/errorList.xls";File file = new File(fileName);InputStreamResource resource = new InputStreamResource(new FileInputStream((file)));org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();headers.add("Content-Disposition", String.format("attachment;filename="%s"", file.getName()));headers.add("Cache-Control", "no-cache,no-store,must-revalidate");headers.add("Pragma", "no-cache");headers.add("Expires", "0");ResponseEntity<Object> responseEntity = ResponseEntity.ok().headers(headers).contentLength(file.length()).contentType(MediaType.parseMediaType("application/text")).body(resource);return responseEntity;} 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 下载excel的接口 /** * @author: wu linchun * @creat: 2021-07-29 11:41 * @desc: 下载错误模板 **/@ApiOperation(value = "下载错误模板")@GetMapping(value = "/downloadErrorModel")@PassTokenpublic ResponseEntity<Object> downloadErrorModel() throws FileNotFoundException {System.out.println("下载错误模板");return welfareGrantService.downloadErrorModel();} 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 在掉/downloadErrorModel这个接口是要注意,只能用超链接 “<a:href />调用,不能使用axios调这个接口。这是由于axios获取的是json 而这个接口返回的是file类型,file会以byte流的形式在http上面传输,因此调/downloadErrorModel这个接口将会接收到byte流,axios默认是接收不了byte的,需要进行一些特殊的设置。可以参考一下这篇:vue使用axios接收流文件_weixin_43869439的博客-CSDN博客 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 如果使用超链接的方式访问接口进行文件下载的话,则需要获取到后端服务器的ip+port (实际中不建议这么做,因为get请求会暴露ip地址和端口,可通过浏览器f12看到ip地址和端口,不安全) ip地址可通过 InetAddress.getLocalHost()获取到,端口号直接用@Value从配置文件中拿到。 第二种:前端合成excel 在vue工程中引入两个依赖:file-saver和xlsx <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 使用“npm install”,安装package.json中新增的依赖 npm install 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 新增 ExportExcelUtil.js,用来根据list生成excel /* eslint-disable */ import { saveAs } from 'file-saver' import XLSX from 'xlsx' function generateArray(table) {var out = [];var rows = table.querySelectorAll('tr');var ranges = [];for (var R = 0; R < rows.length; ++R) {var outRow = [];var row = rows[R];var columns = row.querySelectorAll('td');for (var C = 0; C < columns.length; ++C) {var cell = columns[C];var colspan = cell.getAttribute('colspan');var rowspan = cell.getAttribute('rowspan');var cellValue = cell.innerText;if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;//Skip rangesranges.forEach(function (range) {if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);}});//Handle Row Spanif (rowspan || colspan) {rowspan = rowspan || 1;colspan = colspan || 1;ranges.push({s: {r: R,c: outRow.length},e: {r: R + rowspan - 1,c: outRow.length + colspan - 1}});};//Handle ValueoutRow.push(cellValue !== "" ? cellValue : null);//Handle Colspanif (colspan)for (var k = 0; k < colspan - 1; ++k) outRow.push(null);}out.push(outRow);}return [out, ranges]; }; function datenum(v, date1904) {if (date1904) v += 1462;var epoch = Date.parse(v);return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); } function sheet_from_array_of_arrays(data, opts) {var ws = {};var range = {s: {c: 10000000,r: 10000000},e: {c: 0,r: 0}};for (var R = 0; R != data.length; ++R) {for (var C = 0; C != data[R].length; ++C) {if (range.s.r > R) range.s.r = R;if (range.s.c > C) range.s.c = C;if (range.e.r < R) range.e.r = R;if (range.e.c < C) range.e.c = C;var cell = {v: data[R][C]};if (cell.v == null) continue;var cell_ref = XLSX.utils.encode_cell({c: C,r: R});if (typeof cell.v === 'number') cell.t = 'n';else if (typeof cell.v === 'boolean') cell.t = 'b';else if (cell.v instanceof Date) {cell.t = 'n';cell.z = XLSX.SSF._table[14];cell.v = datenum(cell.v);} else cell.t = 's';ws[cell_ref] = cell;}}if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);return ws; } function Workbook() {if (!(this instanceof Workbook)) return new Workbook();this.SheetNames = [];this.Sheets = {}; } function s2ab(s) {var buf = new ArrayBuffer(s.length);var view = new Uint8Array(buf);for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;return buf; } export function export_table_to_excel(id) {var theTable = document.getElementById(id);var oo = generateArray(theTable);var ranges = oo[1];/* original data */var data = oo[0];var ws_name = "SheetJS";var wb = new Workbook(),ws = sheet_from_array_of_arrays(data);/* add ranges to worksheet */// ws['!cols'] = ['apple', 'banan'];ws['!merges'] = ranges;/* add worksheet to workbook */wb.SheetNames.push(ws_name);wb.Sheets[ws_name] = ws;var wbout = XLSX.write(wb, {bookType: 'xlsx',bookSST: false,type: 'binary'});saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), "test.xlsx") } export function export_json_to_excel({multiHeader = [],header,data,filename,merges = [],autoWidth = true,bookType = 'xlsx' } = {}) {/* original data */filename = filename || 'excel-list'data = [...data]data.unshift(header);for (let i = multiHeader.length - 1; i > -1; i--) {data.unshift(multiHeader[i])}var ws_name = "SheetJS";var wb = new Workbook(),ws = sheet_from_array_of_arrays(data);if (merges.length > 0) {if (!ws['!merges']) ws['!merges'] = [];merges.forEach(item => {ws['!merges'].push(XLSX.utils.decode_range(item))})}if (autoWidth) {/*设置worksheet每列的最大宽度*/const colWidth = data.map(row => row.map(val => {/*先判断是否为null/undefined*/if (val == null) {return {'wch': 10};}/*再判断是否为中文*/else if (val.toString().charCodeAt(0) > 255) {return {'wch': val.toString().length * 2};} else {return {'wch': val.toString().length};}}))/*以第一行为初始值*/let result = colWidth[0];for (let i = 1; i < colWidth.length; i++) {for (let j = 0; j < colWidth[i].length; j++) {if (result[j]['wch'] < colWidth[i][j]['wch']) {result[j]['wch'] = colWidth[i][j]['wch'];}}}ws['!cols'] = result;}/* add worksheet to workbook */wb.SheetNames.push(ws_name);wb.Sheets[ws_name] = ws;var wbout = XLSX.write(wb, {bookType: bookType,bookSST: false,type: 'binary'});saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), `${filename}.${bookType}`); } <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 在.vue中添加下载excel方法 // 下载ExceldownloadExcel () {import('@/utils/ExportExcelUtil').then(excel => {const filterVal = ['item', 'error']const tHeader = ['item', 'error']const data = this.formatModelJson(filterVal, this.errList)console.log(data)console.info(data)excel.export_json_to_excel({header: tHeader,data,filename: 'errorList'})})},// 格式化JSONformatModelJson (filterVal, list) {return list.map(v => filterVal.map(j => {return v[j]}))}, 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> errorList是从后端传过来的 exportExcel () {getErrorListExcel().then(response => {this.errList = response.data})} 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 使用created方法,在加载.vue时,就调用/getErrorListExcel接口,获取errorList的值。 created () {this.exportExcel()}, 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 调用后端接口的api import axios from 'axios' export function getErrorListExcel () {return axios.get('http://127.0.0.1:8082/login/getErrorListExcel', {}).then(response => response).catch(error => error) } <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 后端接口设置errorList中的值 @GetMapping("/getErrorListExcel")public List<ErrorItemBO> getErrorListExcel() {List<ErrorItemBO> list = new ArrayList<>();for (int i = 0; i < 10; i++) {ErrorItemBO errorItemBO = new ErrorItemBO();errorItemBO.setItem("item" + i);errorItemBO.setError("error" + i);list.add(errorItemBO);}return list;} 1 <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> <img src=“https://juejin.cn/ “点击并拖拽以移动”” style=“margin: auto” /> 总结 如果涉及到文件下载,尽量后端传一个list,然后都在前端合成相应的文件以到达减轻服务器负担的作用。 最后 最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。 ———————————————— 版权声明:本文为CSDN博主「梅花十三儿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Android062005/article/details/127207166
-
vue? Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。 vue简明例子 首先使用cdn引入vue.js文件,<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>。 vue.js?声明式渲染,条件循环指令,处理用户表单的输入,以及自定义组件,模板语法… <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue-01</title> </head> <body> <div id="app"> {{message}} </div> <div id="app2" style="border: 1px solid red;"> <span v-bind:title="message"> 鼠标悬停几秒查看信息 </span> </div> <div id="app3"> <p v-if="seen">现在你看到我了</p> </div> <div id="app4"> <ol> <li v-for="todo in todos"> {{todo.text}} </li> </ol> </div> <div id="app5"> <p>{{message}}</p> <button v-on:click="reverseStr">点击反转</button> </div> <!-- 开发环境版本,包含了有帮助的命令行警告 --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> /*简单数据绑定,使用mustache语法将数据与app对象绑定*/ let app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }); /*使用v-bind指令,响应式的更新html属性*/ let app2 = new Vue({ el:'#app2', data: { message:"哈哈哈,我成功了!vue开始" } }); /*使用v-if指令,根据值来判断对应元素是否显示*/ let app3 = new Vue({ el: '#app3', data:{ seen:true } }); /*使用v-for指令,将todos中的内容遍历到html上*/ let app4 = new Vue({ el:'#app4', data:{ todos:[ {text:"学习"}, {text:"真的"}, {text:"需要坚持"} ] } }); /*利用v-on:click来监听点击事件,然后对原本的字符串进行反转*/ let app5 = new Vue({ el:"#app5", data:{ message:"反转!" }, methods:{ reverseStr:function () { this.message=this.message.split('').reverse().join('') } } }); </script> </body> </html>
-
在本篇博客中,我们将使用Vue来实现一个简单的轮播图组件。我们将利用Vue的生命周期钩子、数据绑定和计算属性等特性来完成这个功能。准备工作首先,我们需要创建一个Vue组件,用于展示轮播图。在这个例子中,我们将创建一个名为Carousel的组件。模板我们首先来定义组件的模板,即HTML结构。我们将使用一个ul元素来包含轮播图的图片,以及一个指示器来显示当前显示的图片。<template> <div class="carousel"> <ul class="slides"> <li v-for="(slide, index) in slides" :key="index" :class="{ active: currentIndex === index }"> <img :src="slide.image" alt="Slide Image"> </li> </ul> <div class="indicators"> <span v-for="(slide, index) in slides" :key="index" :class="{ active: currentIndex === index }" @click="goToSlide(index)" ></span> </div> </div> </template>在上面的代码中,我们使用了Vue的指令v-for来循环渲染轮播图的每个图片。我们还根据当前索引currentIndex来设置每个轮播图项的active类,以及根据点击事件切换到对应的轮播图。数据和计算属性接下来,我们需要在组件的data中定义轮播图的数据和当前索引。我们使用一个数组slides来存储每个轮播图项的信息,以及一个整数currentIndex来表示当前显示的轮播图索引。<script> export default { data() { return { slides: [ { image: 'slide1.jpg' }, { image: 'slide2.jpg' }, { image: 'slide3.jpg' } ], currentIndex: 0 }; }, // ... }; </script>生命周期钩子我们需要在组件挂载后启动轮播功能。为此,我们可以使用Vue提供的mounted生命周期钩子。<script> export default { // ... mounted() { this.startCarousel(); }, // ... }; </script>在mounted钩子中,我们调用startCarousel方法来启动自动轮播功能。方法接下来,我们需要定义一些方法来实现轮播图的切换。<script> export default { // ... methods: { startCarousel() { setInterval(this.nextSlide, 3000); }, nextSlide() { this.currentIndex = (this.currentIndex + 1) % this.slides.length; }, goToSlide(index) { this.currentIndex = index; } } }; </script>在上面的代码中,我们使用setInterval函数来定时调用nextSlide方法,以实现自动切换轮播图。nextSlide方法将当前索引递增,并使用取余运算符确保索引在轮播图数组范围内循环。goToSlide方法用于切换到指定的轮播图。样式最后,我们需要为组件添加一些样式,以使其呈现为一个轮播图。<style scoped> .carousel { position: relative; width: 100%; height: 300px; } .slides { list-style: none; position: relative; width: 100%; height: 100%; } .slides li { position: absolute; /* 设置轮播图项的样式 */ } .indicators { /* 设置指示器的样式 */ } </style>在上述代码中,我们为组件容器.carousel设置了宽度和高度,并使用绝对定位来布局轮播图项。你可以根据需要自定义轮播图项和指示器的样式。运行代码现在,我们已经完成了轮播图组件的实现。你可以在Vue应用中使用<carousel></carousel>标签来引入这个组件。<template> <div> <!-- 其他内容 --> <carousel></carousel> <!-- 其他内容 --> </div> </template> <script> import Carousel from './Carousel.vue'; export default { components: { Carousel }, // ... }; </script>运行代码后,你将看到轮播图按照指定的时间间隔自动切换,并且你也可以点击指示器切换到对应的轮播图。总结通过使用Vue的特性,我们可以很方便地实现一个轮播图组件。我们利用了生命周期钩子、数据绑定和计算属性等功能,使得轮播图的切换和渲染变得简单而灵活。希望本篇博客能对你理解Vue实现轮播图组件有所帮助!如果你有任何问题或疑问,欢迎提出。
-
步骤1:安装print-nb首先,我们需要安装print-nb库。我们可以使用npm包管理器执行以下命令来安装:npm install print-nb --save步骤2:引入print-nb接下来,我们需要在Vue组件中引入print-nb库。以下是一种方法:import Print from 'print-nb';步骤3:创建打印功能现在,我们将创建一个Vue方法来处理打印功能。以下是示例代码:print() { let options = { // 打印样式表 styles: [ 'path/to/styles.css' ], // 要打印的元素的id id: 'printable' }; Print(options);}在这里,我们首先定义打印选项。我们指定要在打印中使用的样式表和要打印的元素的id。然后,我们使用Print()函数来处理打印。Print()函数将弹出一个打印对话框,并启动打印功能。步骤4:调用打印功能最后,我们需要在Vue组件中调用打印功能。以下是示例代码:<template> <div> <!-- 要打印的内容 --> <div id="printable"> ... </div> <!-- 触发打印功能 --> <button @click="print">打印</button> </div></template><script>import Print from 'print-nb';export default { methods: { print() { let options = { // 打印样式表 styles: [ 'path/to/styles.css' ], // 要打印的元素的id id: 'printable' }; Print(options); } }}</script>在这里,我们指定要打印的内容,并定义一个按钮来触发打印功能。当用户点击该按钮时,将调用print()方法并执行打印功能。结论到此为止,我们已经学会了如何使用print-nb库在Vue.js应用程序中实现打印功能。print-nb库提供了一个简单而强大的方式来实现打印功能,并可以轻松集成到Vue.js应用程序中。
-
在Vue应用程序中,我们可能需要实现打印功能。幸运的是,在现代Web浏览器中,打印功能已经内置了。在本文中,我们将学习如何使用Vue.js来实现打印功能,并将其与打印机集成。步骤1:安装打印机驱动程序首先,我们需要确保计算机上安装了打印机驱动程序。如果您已经安装了打印机驱动程序,则可以跳过此步骤。否则,请从打印机制造商的网站下载并安装适当的驱动程序。步骤2:引入打印样式表接下来,我们需要引入一个打印样式表,以确保在打印时页面布局正确。以下是一个示例样式表:@media print { body * { visibility: hidden; } #printable, #printable * { visibility: visible; } #printable { position: absolute; left: 0; top: 0; }}在这里,我们使用@media print媒体查询来为打印定义样式。我们首先将所有元素的可见性设置为隐藏,然后将要打印的部分(具有id“printable”)和其所有子元素的可见性设置为可见。最后,我们将打印部分定位为绝对位置,以确保它在打印时正确显示。步骤3:创建打印功能现在,我们将创建一个Vue方法来处理打印功能。以下是示例代码:print() { const printable = document.getElementById('printable'); const originalBody = document.body.innerHTML; document.body.innerHTML = printable.innerHTML; window.print(); document.body.innerHTML = originalBody;}在这里,我们首先获取具有id“printable”的元素。然后,我们将当前文档的HTML内容替换为要打印的部分的HTML内容。接下来,我们使用window.print()方法来打印页面。最后,我们将文档的HTML内容恢复为原始值。步骤4:集成打印机最后,我们需要将打印与打印机集成。在现代Web浏览器中,打印对话框已经内置了打印机选择器。因此,只需调用打印方法即可使用用户选择的打印机。以下是修改后的打印方法示例代码:print() { const printable = document.getElementById('printable'); const originalBody = document.body.innerHTML; document.body.innerHTML = printable.innerHTML; window.print(); document.body.innerHTML = originalBody;}在这里,我们删除了打印机选择器,并在调用window.print()方法时使用默认打印机。结论到此为止,我们已经学会了在Vue.js应用程序中使用打印功能,并与打印机集成。Vue.js提供了一个简单而强大的方法来实现打印功能,而无需编写复杂的代码。
-
为了在Vue-CLI 3.0中配置production gzip,您需要按照以下步骤进行: 安装compression-webpack-plugin: npm install compression-webpack-plugin --save-dev 在vue.config.js文件中添加下面的代码: javascript const CompressionWebpackPlugin = require('compression-webpack-plugin') module.exports = { configureWebpack: config => { if (process.env.NODE_ENV === 'production') { return { plugins: [ new CompressionWebpackPlugin({ filename: '[path].gz[query]', algorithm: 'gzip', test: /\.(js|css)$/, threshold: 10240, minRatio: 0.8 }) ] } } } } 其中,CompressionWebpackPlugin插件可以帮助我们生成gzip文件。 配置完成后,运行npm run build会自动生成压缩文件。 最后,在服务器上设置正确的响应头,才能让浏览器正确解压缩文件。例如,使用Nginx时,可以通过添加以下指令来启用gzip压缩: gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_min_length 10240; gzip_comp_level 6; gzip_vary on; gzip_disable "msie6"; 这些是一些常见的gzip压缩设置,您可以根据自己的需求进行修改。
-
el-menu-item更改导航菜单栏选中的背景颜色修改element导航栏选中时的背景色,可以通过设置 .el-menu-item.is-active 选择器的样式属性 【background-color】使用 !important 使得优先级最大。如下:1234567.el-menu-item.is-active { background-color: #3370ff !important; color: #fff; span { color: #fff !important; } }element的导航栏修改其选中时颜色的问题修改element导航栏选中时的背景色,通过浏览器选中导航栏查看可知修改样式.el-menu-item.is-active即可。于是……123.el-menu-item.is-active { background: #247bb4 !important;}以为完事了吗?不不不正常使用时效果确实达到了但是长时间闲置页面,再回头看这个页面时会出现哪里来的灰色???鼠标点击界面任意位置,灰色立马消失……我……疯狂百度,没人有这种情况但是看到别人的代码如下:123.el-menu-item.is-active { background-color: #247bb4 !important;}于是尝试着用background-color替换background好像还真的OK了再也没出现过如上情况了然后过了好几个月,发现这个现象还是存在一次偶然的机会,被我捕获到了问题哈哈哈莫名其妙这个时候会给当前元素加上background-color: rgb(204, 204, 204);然后只要点击屏幕,瞬间这个值会变成background-color: rgba(0, 0, 0, 0.1);所以,我只要给它默认设一下背景色就好了啊!!!修改代码如下:123.el-menu-item { background-color: rgba(0, 0, 0, 0.1) !important;}这次是真的OK了!!!心塞啊心塞,这个还是没解决因为直接强制修改el-menu-item的背景色,会导致想要它变色的时候变不了了啊啊啊,脑壳痛。直接看最终样式修改因为需要菜单透明,于是乎这么设置以后很久没有给我蒙上一层灰了。123456789<el-menu router class="el-menu-vertical-demo menu-Bar" background-color="rgba(36,123,180,0.51)" text-color="#b4dbf4" active-text-color="#7BE4FF"> <navMenu :navMenus="menuData"></navMenu></el-menu>.el-menu-item { background-color: rgba(0, 0, 0, 0.1);}.el-menu-item.is-active { background-color: #247bb4 !important;}总结以上为个人经验,希望能给大家一个参考
-
我们现在需要上传一个apk文件,但是我在论坛搜到的是cube平台好像不支持此类文件的上传,有什么解决办法吗
-
uex 和 Pinia 是用于管理 Vue.js 应用程序状态的标准 Vue.js 库。让我们比较一下他们的代码、语言、功能和社区支持。 如果没有合适的库,开发人员管理应用程序的状态可能会很困难。Vuex 和 Pinia 是标准的 Vue.js 库,用于处理应用程序中的条件。这两个库都非常适合状态管理,但是由于它们出色的特性和功能,选择哪个库用于你的项目需要时间并且令人沮丧。好吧,我们将在本文中看看为什么一个是最好的。 在本文中,我们将通过实际代码示例查看代码比较、它们的变体、功能,以及推荐使用哪一个来管理你的状态应用程序,以便更好地理解。我们还将考虑每个产品的语言、功能和社区支持。 Pinia 和 Vuex 简介 我将简要总结 Vuex 和 Pinia。如果你想要更详尽的解释,我建议阅读Vuex 文档和Pinia 文档。 什么是Pinia? Pinia是一个新的状态管理库,可帮助你在 Vue.js 应用程序中跨组件管理和存储响应数据和状态。Pinia 由 Vue 核心团队成员之一 Eduardo San Martin Morote 创建。 Pinia 使用新的反应系统来构建一个直观且完全类型化的状态管理系统。 库中引入的新特性是促成 Pinia 推荐的因素之一。总体而言,Pinia 显得轻巧、简单且易于掌握。它拥有可以使 Vue 3 和 Vue 2 中的编程变得通用的一切。因此,这是试用 Pinia 的理想机会。 什么是 Vuex? Vuex是一种状态管理模式和库,构建为集中式存储,可帮助你维护 Vue 应用程序中存在的所有组件的状态。Vuex 遵循确保你的状态突变为预测标准的规则。 使 Vuex 更强大的一个因素是组件从 Vuex store 中获取它们的状态,并且可以快速有效地响应 store 状态的变化。 Vuex虽然是维护你store的状态管理库,但建议你熟悉或搭建过大型SPA。如果你没有经验,Vuex 可能会冗长且令人生畏。 如果你的应用程序很广泛,你需要管理复杂的数据流,并且你有嵌套的组件,你可以使用 Vuex。查看官方文档以获取有关何时使用 Vuex 的更多信息。 Pinia 的特点 Pinia 和 Vuex 之间的区别之一是 Pinia 是“模块化设计”,换句话说,它被构建为拥有多个商店,而 Vuex 只有一个商店。在这些商店中,您可以拥有子模块。除此之外,Pinia 允许将这些模块中的每一个从他们的商店直接导入到需要的组件中。 模块化设计 如果您是一名 Vue 开发人员并且曾使用 Vuex 管理应用程序的状态,您会注意到 Vuex 只有一个商店。在该商店中,您可以在其中包含多个模块。使用 Pinia,您可以将这些模块中的每一个都存储在一个地方,并在需要时将它们直接导入到组件中。 此方法允许捆绑器自动对它们进行代码拆分,并提供更好的 TypeScript 推理。 完整的开发工具支持 Pinia 提供开发工具支持,以帮助您使用该库构建和轻松调试。当我们安装 Pinia 时,它会自动挂接到我们的 Vue.js 开发工具,并让我们跟踪对我们的商店所做的更改,这让我们在 Vue.js 版本(Vue 2 和 Vue3)中获得流畅的开发人员体验。 Pinia 很直观 Pinia 提供了一个简单的 API,使您的商店的编写变得简单且井井有条,就像创建组件一样。这意味着与 Vuex 解决方案相比,需要掌握的样板文件和概念更少。 Pinia 是可扩展的 Pinia 还提供了一个完整的插件系统,具有交易和本地存储同步等功能,适用于 Pinia 默认行为不足的情况。 TypeScript 支持 Pinia 提供比 Vuex 更好的 TypeScript 支持,具有 Javascript 自动完成功能,这使得开发过程变得简单。 Pinia 轻量的 Pinia 的重量只有 1 KB,因此很容易融入您的项目。这可能会提高您的应用程序性能。 Vuex的特点 模块 当您的应用程序扩展时,遍历变得很困难。但是,使用 Vuex 模块,您可以根据领域功能将您的商店拆分为多个文件,并从该特定命名空间中的模块访问状态循环。 开发工具支持 Vue devtools 中的 Vuex 选项卡允许我们查看状态并跟踪我们的变化。这使我们能够快速评估我们的程序如何执行和调试错误。 热重载 Vuex支持热重载功能,它使用 webpack 的热模块替换 API,可以在您开发时快速重载您的 mutations、modules、action 和 getters。 TypeScript 支持 如果你想编写一个 TypeScript 存储定义,Vuex 可以提供它的类型并且更容易实现。它有一个默认的 TypeScript 配置,不需要额外的设置。 Pinia和Vuex的代码对比 Pinia 是一个轻量级库,可帮助您管理整个应用程序的反应状态。与 Vuex 相比,Pinia API 易于学习,使您的代码更易于阅读。 让我们看一下使用 Pinia 和 Vuex 管理我们的状态的代码比较: Vuex 在此示例中,我们将查看一个简单的 Vuex 存储,它跟踪待办事项列表项的状态: import { createStore } from 'vuex' const TodoListstore = createStore({ state() { return { todoListItems: [] } }, actions: { addNewList({ commit }, item) { { commit('createNewItem', item) } }, mutations: { createNewItem(state, item) { state.todoListItems.push(item) } } }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 如果你看上面的代码,你可以看到 Vuex 存储中的三个动作:状态、 动作和突变。状态返回当前的todoListItems,动作提交一个突变来创建一个新项目,最后,突变创建新项目并将其添加到列表中。当你构建一个更大的应用程序时,你可能会意识到动作和突变相对相似,导致冗余代码,因为每个状态变化都需要一个样板。 Pinia 使用 Pinia 简单 API,您可以消除突变和冗余代码。让我们查看一个代码示例,了解当您使用 Pinia 实现之前的代码时的样子: import { defineStore } from 'pinia' Export const useListStore = defineStore('list', { state() => ({ return { todoListItems: [] } }), actions: { addNewList() { this.todoListItems.push(item) } }}) 1 2 3 4 5 6 7 8 9 10 11 12 13 上面的示例是 Pinia API 在管理应用程序状态时如何工作的简单代码。使用 Pinia,我们删除了突变并将其直接更新到我们的动作中。 注意:在上面的代码示例中,当我们将项目直接提交给我们的操作时,我们不需要跟踪我们的项目。 Pinia 和 Vuex 的优缺点 Pinia 和 Vuex 是控制应用程序状态的优秀工具,但其中一个必须具有另一个所没有的某些功能。让我们来看看它们是什么。 Pinia 的优点 Pinia 允许你在不重新加载页面的情况下修改你的商店。 它在 JavaScript 中提供 TypeScript 支持和良好的自动完成功能。 Pinia 提供 devtool 支持,这有助于增强开发人员使用该工具的体验。 Pinia 只有状态、吸气剂和动作。不需要突变。 使用 Pinia,你可以将状态存储在一个地方,并在请求时将其传递给它们的组件。 它是一个重量为 1 KB 的轻量级库。 它提供服务器端渲染支持和自动类型模块,无需额外工作。 Pinia 兼容 Vue 2 和 Vue 3。 Pinia 的缺点 与 Vuex 相比,它没有庞大的社区支持和解决方案。 Pinia 不支持调试功能,如时间旅行和编辑。 Vuex 的优点 Vuex 有 mutations、getters 和 actions。 与 Pinia 相比,Vuex 的社区支持很大。 Vuex 支持调试功能,如时间旅行和编辑。 Vuex 的缺点 它对 TypeScript 不友好。 你只能将其用于大型 SPA。 我应该使用哪个:Pinia 还是 Vuex? 好吧,这就是它变得更具挑战性的地方,因为仍然有一些项目需要使用 Vuex 来处理状态应用程序,即使 Pinia 是新推荐的状态管理库。它有几个 Vuex 没有的有价值的功能。 Vuex 仍然是构建大型 SPA 的理想解决方案,因为对于构建中小型应用程序的人来说,它相当冗长。 Pinia 也一样。尽管如此,如果你需要所有列出的功能,例如 devtool 支持、TypeScript 支持和状态应用程序的轻松管理,那么 Pinia 是最好的解决方案——它为你提供流畅的开发体验。 如果你正在构建一个不太复杂的应用程序,无论是中等到广泛的应用程序,你都可以使用 Pinia,因为它的重量约为 1 KB。 结论 由于功能多样,在管理应用程序状态时,在 Vuex 和 Pinia 之间进行选择可能会造成混淆。不过,这两个框架都非常适合管理状态应用程序。本文简要比较了它们的特性、功能和对代码的影响。读完这篇文章后,也许你将能够找到适合你的库。 ———————————————— 版权声明:本文为CSDN博主「海拥✘」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_44273429/article/details/128932617
-
这篇文章主要讲解了“怎么给Vue对象增加响应式属性”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么给Vue对象增加响应式属性”吧!Vue.$set给对象新增属性Vue.$set()Object.assign()$set()方法不生效时,改用Object.assign()试试vue代码中,只要在data对象里定义的对象,赋值后,任意一个属性值发生变化,视图都会实时变化比如下面在data定义了obj对象,mounted里赋值后,(也可以在其他地方赋值)只要obj.a或者obj.b的值改变了,视图会跟着变化。data() { return { obj: {} } },mounted: { this.obj = {a:1, b: 2} // 改变this.obj.a this.obj.c的值视图会更新 this.obj.c = 3 // 改变this.obj.c的值 视图不会更新 Object.assign(this.obj, {d: 4}) // 改变this.obj.c的值 视图不会更新 this.$set(this.obj, 'e', 5) // 改百年this.obj.e时 视图会更新 console.log('obj' + this.obj) }但是我们在obj对象上新增的属性变化时,值会变化,但是视图不会实时变化比如obj.c或者obj.d变化时,值虽然会变,但是视图不会跟着变Vue.$set()使用Vue.$set()方法,既可以新增属性,又可以触发视图更新,比如obj.e改变时,视图会相应改变打印出的obj,可以看出,新增的属性只有通过this.set()的obj.e属性有get和set方法的,而新增的obj.c和obj.d没有根据官方文档定义:如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。当我们把一个JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty把这些属性全部转为 getter/setter。Object.assign()给data定义的对象新增属性,同时又要视图实时更新,除了用Vue.$set()方法,也可以通过Object.assign()实现data() { return { obj: {} } },mounted: { this.obj = { a: 1, b: 2 } this.obj.c = 3 Object.assign(this.obj, { d: 4 }) // this.$set(this.obj, 'e', 5) // 下面这一行代码才触发了视图更新 this.obj = Object.assign({}, this.obj, {e: 5}) console.log("obj", this.obj) }以上的代码等同于data() { return { obj: {} } },mounted: { this.obj = { a: 1, b: 2, c: 3, d: 4, e: 5 } }$set()方法不生效时,改用Object.assign()试试使用this.$set()去改变一个传入组件的对象(没有直接定义在data对象中,通过mixins选项做相关操作,能够通过this.去获取),没有触发视图更新而改用Object.assign()去给对象重新赋值时,会触发视图更新通过重新给对象赋值,来使视图更新;使用Object.assign()新增,或者改变原有对象,// 起作用的是给对象重新赋值this.obj = Object.assign({}, this.obj, {e: 5})以上就是“怎么给Vue对象增加响应式属性”的内容了,经过本文的学习后,相信大家对怎么给Vue对象增加响应式属性这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。原文链接:https://www.yisu.com/zixun/734104.html
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签