1.tofixedԴ??
2.electron应用版本更新添加releaseNotes(更新日志)的N种方法
3.为什么JS中数值类型已经可以使用Number方法,还要引入Number对象概念?
4.在Vant的基础上实现添加表单验证框架的方法示例
tofixedԴ??
文件处理一直都是前端人的心头病,如何控制好文件大小,文件太大上传不了,文件下载时间太长,tcp直接给断开了?多个闹钟源码等效果为了方便大家有意义的学习,这里就先放效果图,如果不满足直接返回就行,不浪费大家的时间。
文件上传文件上传实现,分片上传,暂停上传,恢复上传,文件合并等
文件下载为了方便测试,我上传了1个1g的大文件拿来下载,前端用的是流的方式来保存文件的,具体的防卫兵sentinel源码可以看这个apiTransformStream
正文本项目的地址是:/post/
requestIdleCallback有不明白的可以看这里:/post/
接下来咋们来计算文件的hash,计算文件的hash需要使用spark-md5这个库,
全量计算文件hashexportasyncfunctioncalcHashSync(file:File){ //对文件进行分片,每一块文件都是分为2MB,这里可以自己来控制constsize=2**;letchunks:any[]=[];letcur=0;while(cur<file.size){ chunks.push({ file:file.slice(cur,cur+size)});cur+=size;}//可以拿到当前计算到第几块文件的进度lethashProgress=0returnnewPromise(resolve=>{ constspark=newSparkMD5.ArrayBuffer();letcount=0;constloadNext=(index:number)=>{ constreader=newFileReader();reader.readAsArrayBuffer(chunks[index].file);reader.onload=e=>{ //累加器不能依赖index,count++;//增量计算md5spark.append(e.target?.resultasArrayBuffer);if(count===chunks.length){ //通知主线程,计算结束hashProgress=;resolve({ hashValue:spark.end(),progress:hashProgress});}else{ //每个区块计算结束,通知进度即可hashProgress+=/chunks.length//计算下一个loadNext(count);}};};//启动loadNext(0);});}全量计算文件hash,在文件小的时候计算是很快的,但是在文件大的情况下,计算文件的hash就会非常慢,并且影响主进程哦?
抽样计算文件hash抽样就是取文件的一部分来继续,原理如下:
/***抽样计算hash值大概是1G文件花费1S的时间**采用抽样hash的方式来计算hash*我们在计算hash的时候,将超大文件以2M进行分割获得到另一个chunks数组,*第一个元素(chunks[0])和最后一个元素(chunks[-1])我们全要了*其他的元素(chunks[1,2,3,4....])我们再次进行一个分割,这个时候的分割是一个超小的大小比如2kb,我们取*每一个元素的头部,尾部,xfce源码安装经验中间的2kb。*最终将它们组成一个新的文件,我们全量计算这个新的文件的hash值。*@paramfile{ File}*@returns*/exportasyncfunctioncalcHashSample(file:File){ returnnewPromise(resolve=>{ constspark=newSparkMD5.ArrayBuffer();constreader=newFileReader();//文件大小constsize=file.size;letoffset=2**;letchunks=[file.slice(0,offset)];//前面2mb的数据letcur=offset;while(cur<size){ //最后一块全部加进来if(cur+offset>=size){ chunks.push(file.slice(cur,cur+offset));}else{ //中间的前中后去两个字节constmid=cur+offset/2;constend=cur+offset;chunks.push(file.slice(cur,cur+2));chunks.push(file.slice(mid,mid+2));chunks.push(file.slice(end-2,end));}//前取两个字节cur+=offset;}//拼接reader.readAsArrayBuffer(newBlob(chunks));//最后Kreader.onload=e=>{ spark.append(e.target?.resultasArrayBuffer);resolve({ hashValue:spark.end(),progress:});};});}这个设计是不是发现挺灵活的,真是个人才哇
在这两个的基础上,咋们还可以分别使用web-worker和requestIdleCallback来实现,源代码在hereヾ(≧▽≦*)o
这里把我电脑配置说一下,公司给我分的电脑配置比较lower,8g内存的老机器。计算(3.3g文件的)hash的结果如下:
结果很显然,全量无论怎么弄,都是比抽样的更慢。
文件分片的方式这里可能大家会说,文件分片方式不就是等分吗,其实还可以根据网速上传的速度来实时调整分片的大小哦!
consthandleUpload1=async(file:File)=>{ if(!file)return;constfileSize=file.sizeletoffset=2**letcur=0letcount=0//每一刻的大小需要保存起来,方便后台合并constchunksSize=[0,网页互动小人源码2**]constobj=awaitcalcHashSample(file)as{ hashValue:string};fileHash.value=obj.hashValue;//todo判断文件是否存在存在则不需要上传,也就是秒传while(cur<fileSize){ constchunk=file.slice(cur,cur+offset)cur+=offsetconstchunkName=fileHash.value+"-"+count;constform=newFormData();form.append("chunk",chunk);form.append("hash",chunkName);form.append("filename",file.name);form.append("fileHash",fileHash.value);form.append("size",chunk.size.toString());letstart=newDate().getTime()//todo上传单个碎片constnow=newDate().getTime()consttime=((now-start)/).toFixed(4)letrate=Number(time)///速率有最大和最小可以考虑更平滑的过滤比如1/tanif(rate<0.5)rate=0.5if(rate>2)rate=2offset=parseInt((offset/rate).toString())chunksSize.push(offset)count++}//todo可以发送合并操作了} ATTENTION!!!?如果是这样上传的文件碎片,如果中途断开是无法续传的(每一刻的网速都是不一样的),除非每一次上传都把chunksSize(分片的数组)保存起来哦控制/post/electron应用版本更新添加releaseNotes(更新日志)的N种方法
前言
目前electron应用一般是使用electron-builder进行打包,使用electron-updater进行版本更新,客户端检测到新版本后一般会弹窗提示用户有新版本+展示更新日志,这就需要我们打包的时候将版本更新日志(releaseNotes)添加到latest.yml或latest-mac.yml文件中,然后客户端检测到新版本后就能够获取到该更新日志并展示给用户。通过分析electron-builder源码,总结出几种添加releaseNotes的方法。
version:?1.0.0files:?-?url:?electron-start_setup_1.0.0.exesha:?+yJuqcWDdhWGLvuLiJFjFKM+uQfihiQ8FHE7RoyyFjOiFZeGugE7UPlceDHfm9qyQOYmUvuEzjq/u3zw==size:?path:?electron-start_setup_1.0.0.exesha:?+yJuqcWDdhWGLvuLiJFjFKM+uQfihiQ8FHE7RoyyFjOiFZeGugE7UPlceDHfm9qyQOYmUvuEzjq/u3zw==releaseNotes:?"修复断网时离开会议失败的问题\r\n修复会议中受开关麦影响听不到的问题\r\n新增动态转发功能\r\n修复Bug,优化UI"releaseDate:?'--T::.Z'适用范围项目使用Electron-builder打包
provider为generic
//package.json{ ?"build":?{ "publish":?[?{ "provider":?"generic","url":?"xxxxxx"?}]?}}方法一:在package.json文件build节点下添加releaseNotes信息示例:
//package.json{ ?"build":?{ "releaseInfo":{ ?"releaseNotes":"修复断网时离开会议失败的问题\r\n修复会议中受开关麦影响听不到的问题\r\n新增动态转发功能\r\n修复Bug,优化UI"}?}}方法二:在package.json文件build节点下添加releaseNotesFiles信息(推荐使用)示例:
//package.json{ ?"build":?{ "releaseInfo":{ ?"releaseNotesFile":"release-1.0.0.md"}?}}releaseNotesFile字段指定更新日志文件为release-1.0.0.md,其内容如下:
修复断网时离开会议失败的问题修复会议中受开关麦影响听不到的问题新增动态转发功能修复Bug,优化UI使用这种方法添加releaseNotes,可自定义更新日志文件名称,如每发布一个版本就添加一个更新日志文件(release-1.0.0.md,sel4源码release-1.0.1.md...),便于以后查看每一个版本更新日志。
方法三:在打包输出目录下创建文件release-notes.md如指定了输出目录为build,则在build目录下创建名称为release-notes.md的文件。
可取的文件名为:
release-notes.md
release-notes-(mac|windows|linux).md
.....
方法四:直接修改打包生成的latest.yml或latest-mac.yml,添加releaseNotes字段(不建议使用)客户端实现使用electron-updater检查更新获取更新日志,弹窗提示用户有新版本
const?{ ?autoUpdater?}?=?require('electron-updater')//检测到新版本时触发autoUpdater.on('update-available',?function?(info)?{ //获取更新日志var?releaseNotes=info.releaseNotes//弹窗提示用户const?dialogOpts?=?{ type:?'info',buttons:?['立即下载','稍后'],title:?'版本更新',textWidth:?,message:?'发现新版本'+info.version+"("+(info.files[0].size//).toFixed(2)+"MB)"+"\r\n\r\n"+releaseNotes,cancelId:?1?}dialog.showMessageBox(dialogOpts).then((returnValue)?=>?{ if?(returnValue.response?===0)?{ autoUpdater.downloadUpdate();}})});实现效果如下:
macOS系统Windows系统相关源码分析electron-builder打包时获取releaseNotes对应的代码文件为packages/app-builder-lib/src/publish/updateInfoBuilder.ts,具体代码如下:
async?function?getReleaseInfo(packager:?PlatformPackager<any>)?{ ?const?releaseInfo:?ReleaseInfo?=?{ ?...(packager.platformSpecificBuildOptions.releaseInfo?||?packager.config.releaseInfo)?}?if?(releaseInfo.releaseNotes?==?null)?{ const?releaseNotesFile?=?await?packager.getResource(?releaseInfo.releaseNotesFile,?`release-notes-${ packager.platform.buildConfigurationKey}.md`,?`release-notes-${ packager.platform.name}.md`,?`release-notes-${ packager.platform.nodeName}.md`,?"release-notes.md")const?releaseNotes?=?releaseNotesFile?==?nullnull?:?await?readFile(releaseNotesFile,?"utf-8")//?to?avoid?undefined?in?the?file,?check?for?nullif?(releaseNotes?!=?null)?{ ?releaseInfo.releaseNotes?=?releaseNotes}?}?delete?releaseInfo.releaseNotesFile?return?releaseInfo}优先从releaseInfo.releaseNotes字段中取值(方法一)
如果releaseInfo.releaseNotes未定义,则从releaseInfo.releaseNotesFile取值(方法二)
如果releaseInfo.releaseNotesFile未定义,则从资源目录下的指定文件(如release-notes.md)中取值(方法三)
参考资源/post/
为什么JS中数值类型已经可以使用Number方法,还要引入Number对象概念?
JavaScript中,数值类型有两种表示方式:直接量和Number对象。这两种表示方式在大多数情况下是等价的,但是它们有一些重要的区别。
直接量(也称为原始值)表示一个数值常量,它直接写在代码中,例如或者3.。直接量是不可变的,它们是原始类型的值,不能调用方法,只能进行基本的运算。
Number对象表示一个数值,但是它是一个对象,而不是原始类型的值。Number对象可以调用方法来执行更高级的操作,比如转换为字符串、比较大小等。但是,由于Number对象是可变的,所以它的性能要比直接量差一些。
所以,通常来说,我们应该优先使用直接量来表示数值,只有在需要使用Number对象的方法时才使用Number对象。
对于上面的例子,n1和n2两者都可以调用toString()方法来将数值转换为字符串,但是n2是一个Number对象,所以它可以调用更多的方法。例如,使用Number.isInteger()方法检查一个数值是否为整数时,n1是直接量,所以它可以直接传入`Number
另外,由于Number.isInteger()方法是Number对象的一个静态方法,而不是实例方法,所以它无法直接在Number对象实例上调用。例如,在上面的例子中,Number.isInteger(n1)返回true,因为n1是一个直接量,它可以直接传入Number.isInteger()方法;而Number.isInteger(n2)返回false,因为n2是一个Number对象实例,而不是直接量,它不能直接传入Number.isInteger()方法。
总之,直接量和Number对象两者都可以表示数值,但是它们有一些重要的区别,应该根据实际情况来选择使用哪种方式。通常来说,我们应该优先使用直接量来表示数值,只有在需要使用Number对象的方法时才使用Number对象。
在Vant的基础上实现添加表单验证框架的方法示例
Vant 一套基于Vue的移动端UI框架,有赞出品。
因为UI设计的够漂亮,源码结构也比较清晰,插件定位也比较明确,重要是实战过程中的使用体验不错。在最近的项目当中就使用 Vant 作为移动端的基础UI框架,但在实践过程中发现该框架和其他框架有不一样的地方。例如它不内置表单验证,接下来,我把自己实现验证框架的思路分享出来。
分析需求
我们找的插件主要能解决以下问题
支持中文 适应UI框架 分组验证 动态验证(数据动态,规则动态)
去网络上搜索了一些框架,推荐两款(在官方也有推.vuejs.org/v2/cookbook/form-validation.html )
vuelidate vee-validate
我的项目里使用的是 vee-validate
解决问题
安装及支持中文
npm install vee-validate --saveimport VeeValidate, { Validator } from 'vee-validate'import zh_CN from 'vee-validate/dist/locale/zh_CN';Validator.localize('zh_CN', zh_CN)Vue.use(VeeValidate)
中文问题可以解决,但是遇到个很恶心的问题,这样的错误提示会变成 title不能为空 这样的提示,实际展示效果是不好的。
所以这个需要重构下,自己来实现错误提示的内容
const formatFileSize = function (size) { let units = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; let threshold = ; size = Number(size) * threshold; let i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(threshold)); return (((size / Math.pow(threshold, i)).toFixed(2) * 1) + " " + (units[i]));}Validator.localize('zh_CN', { name: 'zh_CN', attributes: { } messages: { _default: () => `${ fieldName}无效`, after: (field, [target]) => `${ fieldName}必须在${ target}之后`, alpha_dash: () => `${ fieldName}能够包含字母数字字符、破折号和下划线`, alpha_num: () => `${ fieldName}只能包含字母数字字符`, alpha_spaces: () => `${ fieldName}只能包含字母字符和空格`, alpha: () => `${ fieldName}只能包含字母字符`, before: (field, [target]) => `${ fieldName}必须在${ target}之前`, between: (field, [min, max]) => `${ fieldName}必须在${ min}与${ max}之间`, confirmed: (field, [confirmedField]) => `${ fieldName}不能和${ confirmedField}匹配`, credit_card: () => `${ fieldName}格式错误`, date_between: (field, [min, max]) => `${ fieldName}必须在${ min}和${ max}之间`, date_format: (field, [format]) => `${ fieldName}必须符合${ format}格式`, decimal: (field, [decimals = '*'] = []) => `${ fieldName}必须是数字,且能够保留${ decimals === '*' ? '' : decimals}位小数`, digits: (field, [length]) => `${ fieldName}必须是数字,且精确到${ length}位数`, dimensions: (field, [width, height]) => `${ fieldName}必须在${ width}像素与${ height}像素之间`, email: () => `${ fieldName}不是一个有效的邮箱`, ext: () => `${ fieldName}不是一个有效的文件`, image: () => `${ fieldName}不是一张有效的`, included: () => `${ fieldName}不是一个有效值`, integer: () => `${ fieldName}必须是整数`, ip: () => `${ fieldName}不是一个有效的地址`, length: (field, [length, max]) => { if (max) { return `${ fieldName}长度必须在${ length}到${ max}之间` } return `${ fieldName}长度必须为${ length}` }, max: (field, [length]) => `${ fieldName}不能超过${ length}个字符`, max_value: (field, [max]) => `${ fieldName}必须小于或等于${ max}`, mimes: () => `${ fieldName}不是一个有效的文件类型`, min: (field, [length]) => `${ fieldName}必须至少有${ length}个字符`, min_value: (field, [min]) => `${ fieldName}必须大于或等于${ min}`, excluded: () => `${ fieldName}不是一个有效值`, numeric: () => `${ fieldName}只能包含数字字符`, regex: () => `${ fieldName}格式无效`, required: () => `${ fieldName}不能为空`, size: (field, [size]) => `${ fieldName}必须小于${ formatFileSize(size)}`, url: () => `${ fieldName}不是一个有效的url` }})Vue.use(VeeValidate)
适应UI框架
虽然Vant没有内置验证框架,但提供了错误的样式。
<van-field :error="." :error-message="."/>
用 vee-validate 可以这样解决
<van-field . name="title" v-validate="'required|max:'" :error="errors.has('title')" :error-message="errors.first('title')"/>this.$validator.validateAll().then((result) => { if(result){ // . }})
分组验证
<van-field name="title" data-vv-scope="group-1" v-validate="'required|max:'" :error="errors.has('group-1.title')" :error-message="errors.first('group-1.title')"/>this.$validator.validateAll('group-1').then((result) => { if(result){ // . }})
如此,基于 Vant 的验证框架问题就得以解决了,可以愉快的玩耍表单验证了。