• [技术干货] Vue实现大文件分片上传,包括断点续传以及上传进度条-转载
     首先解释一下什么是分片上传         分片上传就是把一个大的文件分成若干块,一块一块的传输。这样做的好处可以减少重新上传的开销。比如:如果我们上传的文件是一个很大的文件,那么上传的时间应该会比较久,再加上网络不稳定各种因素的影响,很容易导致传输中断,用户除了重新上传文件外没有其他的办法,但是我们可以使用分片上传来解决这个问题。通过分片上传技术,如果网络传输中断,我们重新选择文件只需要传剩余的分片。而不需要重传整个文件,大大减少了重传的开销。  但是我们要如何选择一个合适的分片呢?因此我们要考虑如下几个事情:  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 
  • [技术干货] Vant Uploader 上传图片功能-转载
     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 
  • [技术干货] v-viewer插件使用
    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)框架,得到了阿里巴巴集团内众多项目的使用和支持,具有较大的社区和生态系统,有更多的插件和工具可供选择。适用场景和定位:京东微前端:京东微前端定位于构建复杂应用系统的微前端架构,适用于需要将多个独立的前端应用集成在一起的场景。阿里微前端:阿里微前端定位于构建企业级中后台应用的微前端架构,适用于大型系统和组织内部的微服务架构。需要注意的是,以上对比仅是针对一些关键点进行的简要对比,具体选型还需要根据实际需求和具体情况进行综合考虑。对于特定的项目和团队,这些微前端框架都可能是合适的选择。建议在选择之前详细了解它们的特点、文档和使用案例,并根据实际需求进行评估和测试。
  • [技术干货] 【vue3】使用canvas-转载
     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 
  • [技术干货] Vue中实现自定义excel下载-转载
     最近在工作中遇到一个需求,就是需要在前端实现一个错误模板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-day01 使用cdn引入使用-转载
     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的生命周期钩子、数据绑定和计算属性等特性来完成这个功能。准备工作首先,我们需要创建一个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实现轮播图组件有所帮助!如果你有任何问题或疑问,欢迎提出。
  • [技术干货] vue如何使用print-nb库来实现打印功能
    步骤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.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
     为了在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压缩设置,您可以根据自己的需求进行修改。 
  • [技术干货] vue之el-menu-item如何更改导航菜单栏选中的背景颜色【转】
    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文件无法上传
    我们现在需要上传一个apk文件,但是我在论坛搜到的是cube平台好像不支持此类文件的上传,有什么解决办法吗
  • [技术干货] Vue.js 状态管理:Pinia 与 Vuex-转载
    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对象增加响应式属性”吧!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
总条数:277 到第 页
上滑加载中