注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

async/await 你可能正在将异步写成同步

web
前言 你是否察觉到自己随手写的异步函数,实际却是“同步”的效果! 正文 以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。 第一版 思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。 import path fr...
继续阅读 »

前言


你是否察觉到自己随手写的异步函数,实际却是“同步”的效果!


正文


以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。


第一版


思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。


import path from 'node:path'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'

async function findFiles(root) {
if (!existsSync(root)) return

const rootStat = await fs.stat(root)
if (rootStat.isFile()) return [root]

const result = []
const find = async (dir) => {
const files = await fs.readdir(dir)
for (let file of files) {
file = path.resolve(dir, file)
const stat = await fs.stat(file)
if (stat.isFile()) {
result.push(file)
} else if (stat.isDirectory()) {
await find(file)
}
}
}
await find(root)
return result
}

机智的你是否已经发现了问题?


我们递归查询子目录的过程是不需要等待上一个结果的,但是第 20 行代码,只有查询完一个子目录之后才会查询下一个,显然让并发的异步,变成了顺序的“同步”执行。


那我们去掉 20 行的 await 是不是就可以了,当然不行,这样的话 await find(root) 在没有完全遍历目录之前就会立刻返回,我们无法拿到正确的结果。


思考一下,怎么修改它呢?......让我们看第二版代码。


第二版


import path from 'node:path'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'

async function findFiles(root) {
if (!existsSync(root)) return

const rootStat = await fs.stat(root)
if (rootStat.isFile()) return [root]

const result = []
const find = async (dir) => {
const task = (await fs.readdir(dir)).map(async (file) => {
file = path.resolve(dir, file)
const stat = await fs.stat(file)
if (stat.isFile()) {
result.push(file)
} else if (stat.isDirectory()) {
await find(file)
}
})
return Promise.all(task)
}
await find(root)
return result
}

我们把每个子目录内容的查询作为独立的任务,扔给 Promise.all 执行,就是这个简单的改动,性能得到了质的提升,让我们看看测试,究竟能差多少。


对比测试


console.time('v1')
const files1 = await findFiles1('D:\\Videos')
console.timeEnd('v1')

console.time('v2')
const files2 = await findFiles2('D:\\Videos')
console.timeEnd('v2')

console.log(files1?.length, files2?.length)

result


版本二快了三倍不止,如果是并发的接口请求被不小心搞成了顺序执行,差距比这还要夸张。


作者:justorez
来源:juejin.cn/post/7332031293877485578
收起阅读 »

为什么大家都不想回家过年了

大家好,我是林家少爷,是一位专注于前端技术,立志成为前端技术专家的热血青年。 2023年,看到身边的朋友多多少少都经历了裁员,降薪,甚至被迫自离。 我还好,2023年唯一值得庆幸的事情就是我工资涨薪2k,不过年终奖依旧无望。 今年年会都取消了,过年礼品也没见影...
继续阅读 »

大家好,我是林家少爷,是一位专注于前端技术,立志成为前端技术专家的热血青年。


2023年,看到身边的朋友多多少少都经历了裁员,降薪,甚至被迫自离。


我还好,2023年唯一值得庆幸的事情就是我工资涨薪2k,不过年终奖依旧无望。
今年年会都取消了,过年礼品也没见影。


坦白讲,自从放开疫情之后,行情不仅没有好转,反而更差了,预计2024年要比之前更难,所以,2024年到2025年,这两年还是多攒钱,多搞钱,然后把钱存下来,尽量多存钱,比什么都强。


好不容易熬到了过年,本来想回到温暖的港湾,把2023年的事情好的不好的都说出来跟家里人倾诉一下。


没想到面对的,是家里人的催婚,在一线城市工资那么高,钱都到哪里去了,人家谁谁才比你大三个月都已经二胎了......


甚至过年还要走亲戚,甚至拉着亲戚说介绍对象。


这些我都理解,因为我正在经历这一切。


我的观点是,2023年赚钱是真不容易,打工的面对工作量增加,平时无偿加班,领导PUA(可能领导也被更高级的领导PUA),还有被迫降薪,薪资被压年底年终奖(然后这中间可操作空间很大),而且大城市本来消费就高,有好些同事被裁员还拿不到裁员费,大过年还要跳楼拉横幅争取,以上总总,我都亲眼看过,赚钱太难了。


我从2020年开始,一直都在做副业,中间经历了一次失败的创业,2021年回归职场,也是行情最差的时候,没想到后面两年越来越差,也明显感觉2023年副业赚钱越来越卷,任何一点赚钱的项目也会被互联网给公开,摊平了信息差,然后大家一窝蜂地涌进来,自己流量也被抢占了,很快这个项目又得放弃了,我还没统计2023年我的副业赚了多少,但肯定不超过10万,离我定的目标(超过主业工资持续3个月以上)还差很远,而且总是被加班,老板周末问话给打断,有时候自己也被气炸了,好几次都想不干了,但是看着自己还没有做起来的副业盘子,贸贸然走人收入立马断了,也是要重新找工作的,就忍了下来。


大家看到这里就知道我当时内心有多矛盾,但是我都坚持了下来,我相信总有一天我可以真正把副业做起来,真正拥有属于自己的事业,手上有许多现钱,不需要看任何人眼色,我能活成我自己。


回到过年这个话题,很多老一辈就觉得,大过年的就应该走走亲戚,见见七八姑八大姨,互相聊聊家常,好不热闹。


但是我身边很多同事,不包括程序员,其实都是偏内向的人(包括我也是),就想着在家里跟家里人倾诉一下,哪怕不倾诉,也是关在家里面,把房间打扫的干净整洁,安静的看书,或者玩玩游戏,或者搞钱,就是不想去见一些八竿子打不着的亲戚,这是一种精神内耗。


说实话,他们真的只想回家好好休息,啥事都不做,饭店有家人做好的满满一桌饭菜,上班那点屁事就不管他了~


催婚,算了吧,想当年我们的目标都是考清华北大,985,211,为了高考付出了多少个不眠之夜,但是人人都考得上吗?


尤其是奔三的女孩子,我能体会她们被家里人各种催婚的痛苦,真的会把人逼疯。


婚姻大事,岂非儿戏,还是要找到同频人,先谈恋爱,再结婚,感情基础决定上层建筑,只有这样才能长长久久。


搭伙过日子,那只适合70年代,不适合我们,随意搭伙,闪婚,没有前期磨合阶段,大概率会闪离,现在离婚率高不是没有原因的,所以还需慎重。


至于面对家里走亲戚,出到社会都知道不过是演戏而已,跟着演,演到底即可。


不是个好演员做不了一个好销售,不是好销售做不了一个会赚钱的程序员。


这句话我自己总结出来的,会自我营销就是要会演戏,把自己都骗过了才能骗别人。


前期会比较累,慢慢就习惯了。


实在忍受不了,下一年大不了不回家了,图个清静。


点到为止,祝大家新年快乐。


作者:林家少爷
来源:juejin.cn/post/7332593229337100303
收起阅读 »

小镇做题家必须要跨过的三道坎

其实我们大多数人都还称不上小镇做题家,因为我们大多都是来自乡村,只不过在乡镇做了上了几年的学而已。 大多数人的人生轨迹基本上都是从乡村到乡镇再到县城,最终去到了市级以上的城市读大学,没有资源,没人指路,毕业后也是一脸茫然,最终回到原点! 所以大多数小镇做题家的...
继续阅读 »

其实我们大多数人都还称不上小镇做题家,因为我们大多都是来自乡村,只不过在乡镇做了上了几年的学而已。


大多数人的人生轨迹基本上都是从乡村到乡镇再到县城,最终去到了市级以上的城市读大学,没有资源,没人指路,毕业后也是一脸茫然,最终回到原点!


所以大多数小镇做题家的生活永远都是那么艰难,很难成为出题家,是因为多数人身上都出现了致命的弱点,而这些弱点多数是由原生家庭带来的。


一.自卑


自卑可以说是原生家庭带来的第一原罪,也是很难突破的一关,而导致自卑的导火索一定是贫穷。


因为我们多数人从小都被父母灌输“我们家穷,所以你不应该这样做”。


所以在进入大学后,多数人的心中都有一个魔咒,我不配拥有,不配出去玩,不配谈恋爱,不配穿好看的衣服,即使是自己打工赚的钱,多花一分都觉得有负罪感。


因为心里会想着,父母那么辛苦,我就不应该去花钱,而是要节省,当然节省并没有不好。


但是从逻辑上就已经搞错了,首先如果自己舍不得钱去学习,去投资自己,一个月挣3000能够省下2000,那么这个省钱毫无意义,因为省下的这点钱根本做不了什么,但是会把自己置入一个更深的漩涡,收入无法提升,眼界无法开阔,而是一直盯着一个月省2000这个感人的骗局。


除了用钱有负罪感,还有不敢向上社交,觉得自己是小山旮旯出来的,不配和优秀的人结伴,因为父母从小就说: 我们家条件没人家好,人家有钱,人家会瞧不起你的。所以很多人内心就已经打了退堂鼓,从而错过了和优秀的人链接的机会。


但是事实上真正优秀的人,人家从来不会有瞧不起人的心态,只要你身上有优点,你们之间能够进行价值交换,别人比你还谦卑。


我这几年从互联网行业链接到的人有1000个以上,优秀的人不在少数,我发现越优秀的人越谦虚。


自卑导致的问题还很多,比如做事扭扭咧咧,不敢上台,不敢发表意见,总是躲在角落里。


但是这个社会没有人喜欢扭扭咧咧的人,都喜欢落落大方的人,即使你的知识不专业,你普通话不好,这些都不重要,但是只要你表现出自信,大方的态度,那么大家都会欣赏你,因为这就是勇敢。


二.面子


有一个事实,越脆弱的人越爱面子,越无能的人越爱面子。


这句话虽然说起来有点不好听,但是这是事实,我们这种小镇做题家最要先放下面子。


比如自己不懂很多东西,但是不愿意去问别人,怕被别人说,在学校的时候,有问题不敢问,于是花费很多时间去弄,到最后还是搞不懂。


进入工作后,不懂的也不好意思问同事和领导,怕被别人说菜,怀疑自己。


其实真的没几个人有那个闲心去关注你的过往,你的家庭,相反越是伪装,越容易暴露。


我很喜欢的一段话: 我穷,但是我正在拼命改变、我菜,但是我可以拼命学!


面子的背后是自负,是错失,是沦陷。


三.认知


认知是一个人的天花板,它把人划分了层级。


有一段话说得很好:你永远无法赚到认知之外的钱,你也无法了解认知之外的世界。


我们多数小镇做题家的思维永远停留在老师和父母的教导:好好读书,将来就能考一个好工作,就能找一个铁饭碗。

然后在职场中听老板的:好好工作,任劳任怨,以后给你升职加薪。


然后多数人就深信不疑,就觉得人生的目标就是铁饭碗,其他的都不行,工作努力就一定能得到升职加薪,实际上这一切是在PUA自己。


当被这个社会毒打后,才发现自己是那么无知,那么天真。


而这一切的罪魁祸首就是自己认知不够,总觉得按部就班就一定能顺利过好一生。


————


自卑,面子,认知这三点是我们多数小镇做题家难以跨过的三道坎。


而这三道坎基本上都是原生家庭和教育造成的。


跨过这三道坎的方法就是逃离和向上链接。


施耐庵在几百年前就总结出的一句话:母弱出商贾,父强做侍郎,族望留原籍,家贫走他乡。


显然我们大多数都是属于第四类,所以远走他乡是唯一的选择,只有脱离原生的环境,才能打开视野。


事实也是如此,我们观察身边没有背景,没有经济条件的人,他们的谷底反弹一定是逃离。


绝非留恋原地!


作者:苏格拉的底牌
来源:juejin.cn/post/7330295661784875043
收起阅读 »

压缩炸弹,Java怎么防止

一、什么是压缩炸弹,会有什么危害 1.1 什么是压缩炸弹 压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不...
继续阅读 »

一、什么是压缩炸弹,会有什么危害


1.1 什么是压缩炸弹


压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。


以下是安全测试几种经典的压缩炸弹


graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)

A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。


压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。


压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。



1.2 压缩炸弹会有什么危害


graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px

压缩炸弹可能对计算机系统造成以下具体的破坏:



  1. 资源耗尽:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。

  2. 磁盘空间耗尽:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。

  3. 系统崩溃:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。

  4. 拒绝服务攻击:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。

  5. 数据丢失:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。



重要提示:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。



二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹


2.1 个人有没有方法可以检测压缩炸弹?


有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:


graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)

A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


  1. 安全软件和防病毒工具(推荐):使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。

  2. 文件大小限制:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。

  3. 文件类型过滤:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。


2.2 Java怎么防止压缩炸弹


在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:


graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px


  1. 解压缩算法的限制:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。

  2. 设置解压缩操作的资源限制:使用Java的java.util.zipjava.util.jar等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。

  3. 使用安全的解压缩库:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。

  4. 文件类型验证和过滤:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。

  5. 异步解压缩操作:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。

  6. 安全策略和权限控制:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。


2.2.1 使用解压算法的限制来实现防止压缩炸弹


在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制来实现防止压缩炸弹。


先来看看我们实现的思路


graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L

style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

实现流程说明如下:



  1. 首先,通过给定的 file 参数创建一个 ZipFile 对象,用于打开要解压缩的 ZIP 文件。

  2. zipFileSize 变量用于计算解压缩后的文件总大小。

  3. 使用 zipFile.entries() 方法获取 ZIP 文件中的所有条目,并通过 while 循环逐个处理每个条目。

  4. 对于每个条目,使用 entry.getSize() 获取条目的未压缩大小,并将其累加到 zipFileSize 变量中。

  5. 如果 zipFileSize 超过了给定的 size 参数,说明解压后的文件大小超过了限制,此时会调用 deleteDir() 方法删除已解压的文件夹,并抛出 IllegalArgumentException 异常,以防止压缩炸弹攻击。

  6. 创建一个 File 对象 unzipped,表示解压后的文件或目录在输出文件夹中的路径。

  7. 如果当前条目是一个目录,且 unzipped 不存在,则创建该目录。

  8. 如果当前条目不是一个目录,确保 unzipped 的父文件夹存在。

  9. 创建一个 FileOutputStream 对象 fos,用于将解压后的数据写入到 unzipped 文件中。

  10. 通过 zipFile.getInputStream(entry) 获取当前条目的输入流。

  11. 创建一个缓冲区 buffer,并使用循环从输入流中读取数据,并将其写入到 fos 中,直到读取完整个条目的数据。

  12. 最后,在 finally 块中关闭 fos 和 zipFile 对象,确保资源的释放。


实现代码工具类


import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class FileBombUtil {

/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/

public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;

/**
* 文件超限提示
*/

public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";

/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/

public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumerationextends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}

fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);

byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}

}

/**
* 递归删除目录文件
*
* @param dir 目录
*/

private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}

}

测试类


import java.io.File;

/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class Test {

public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}

}

三、总结


文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。


文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。


总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。


在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:




  1. 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。

  2. 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。

  3. 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。



作者:独爱竹子的功夫熊猫
来源:juejin.cn/post/7289667869557178404
收起阅读 »

突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?

web
前言在前端领域,Vue 和 React 是两个备受欢迎的 JavaScript 框架,它们都为开发者提供了灵活而强大的工具。React 引入了 Hooks 的概念,使得函数组件可以拥有状态和其他 React 特性,而不再需要使用类组件。那么,对于那些突发奇想、...
继续阅读 »

前言

在前端领域,Vue 和 React 是两个备受欢迎的 JavaScript 框架,它们都为开发者提供了灵活而强大的工具。React 引入了 Hooks 的概念,使得函数组件可以拥有状态和其他 React 特性,而不再需要使用类组件。那么,对于那些突发奇想、吃饱了撑的时候,我们是否可以在 Vue 3 中实现类似 React Hooks 的功能呢?

首先,我们需要了解 React Hooks 的核心思想。React Hooks 允许函数组件拥有状态,副作用和其他 React 特性,而无需使用类组件。这是通过使用 useStateuseEffect 等特殊的函数来实现的。

Vue 3 也引入了 Composition API,它在一定程度上类似于 React Hooks。Composition API 允许我们在函数组件中组织和重用逻辑。虽然它不是完全相同的实现,但能够达到类似的效果。

useState

React 中的 useState:

useState 是 React 中的一个 Hook,用于在函数组件中引入状态。通过 useState,我们可以在函数组件中保存和更新状态,而不必使用类组件。

基本语法如下:

import React, { useState } from 'react';

function ExampleComponent() {
// 使用 useState 定义状态变量 count 和更新函数 setCount,并初始化为 0
const [count, setCount] = useState(0);

return (
<div>
<p>Count: {count}p>
<button onClick={() => setCount(count + 1)}>
加一
button>
div>
);
}

下面是一个使用 vue3实现类似于 useState 的例子:

import { ref, UnwrapRef } from "vue";

type UpdateFunction = (nextState: UnwrapRef) => UnwrapRef;
function isUpdateFc(
nextState: UnwrapRef | UpdateFunction
): nextState is UpdateFunction {
return typeof nextState === "function";
}

export default function useState(initialState: T) {
const state = ref(initialState);
const useState = (nextState: UnwrapRef | UpdateFunction) => {
// 检测传入的是不是函数,如果是函数就把state传给函数,把函数执行返回值赋给重新state
if (isUpdateFc(nextState)) {
state.value = nextState(state.value);
} else {
state.value = nextState;
}
};
return [state, useState] as const;
}

<template>
<div>
<button>{{ count }}button>
<button @click="() => handerCount()">+button>
div>
template>

useEffect

React 中的 useEffect:

useEffect 是 React 中的一个重要 Hook,用于处理副作用操作,比如数据获取、订阅、手动操作 DOM 等。它在函数组件渲染完成后执行,可以用于管理组件的生命周期。

基本语法如下:

import React, { useState, useEffect } from 'react';

function ExampleComponent() {
const [data, setData] = useState(null);

useEffect(() => {
// 在组件渲染完成后执行的副作用操作
fetchData(); // 例如,发起数据请求
}, []); // 第二个参数是依赖数组,为空数组表示只在组件挂载和卸载时执行

return (
<div>
{/* 组件渲染的内容 */}
div>
);
}

在 Vue 3 中使用 onMounted, onUpdated, onUnmounted,watch 实现类似功能:

在 Vue 3 中,可以使用一系列的生命周期钩子和 watch 函数来实现与 useEffect 类似的效果。

  1. onMounted: 在组件挂载后执行。
  2. onUpdated: 在组件更新后执行。
  3. onUnmounted: 在组件卸载前执行。
  4. watch: 监听特定数据的变化。

下面是一个使用 vue3实现类似于 useEffect 的例子:

import { ref, onMounted, watch, onUnmounted, onUpdated } from "vue";

type EffectCleanup = void | (() => void);
export default function useEffect(
setup: () => EffectCleanup,
dependencies?: readonly unknown[]
): void {
const cleanupRef = ref<EffectCleanup | null>(null);
const runEffect = () => {
// 判断下一次执行副作用前还有没有清理函数没有执行
if (cleanupRef.value) {
cleanupRef.value();
}
// 执行副作用,并赋值清理函数
cleanupRef.value = setup();
};
// 组件挂载的时候执行一次副作用
onMounted(runEffect);
// 判断有没有传依赖项,有的话就watch监听
if (dependencies && dependencies.length > 0) {
watch(dependencies, runEffect);
} else if(dependencies === undefined) {
// 没有传依赖项就组件每次渲染都要执行副作用
onUpdated(runEffect)
}
// 组件销毁的使用如果有清理函数就执行清理函数
onUnmounted(() => {
if (cleanupRef.value) {
cleanupRef.value();
}
});
}

useReducer

React 中的 useReducer:

useReducer 是 React 中的另一个 Hook,用于处理具有复杂状态逻辑的组件。它接受一个包含当前状态和触发状态更新的函数的 reducer,以及初始状态。通过 useReducer,我们可以更好地管理和处理复杂的状态变更逻辑。

基本语法如下:

import React, { useReducer } from 'react';

// 定义 reducer 函数
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
};

function ExampleComponent() {
// 使用 useReducer,传入 reducer 函数和初始状态
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<div>
<p>Count: {state.count}p>
<button onClick={() => dispatch({ type: 'increment' })}>加一button>
<button onClick={() => dispatch({ type: 'decrement' })}>减一button>
div>
);
}

通过刚刚实现的 useState 来实现类似 useReducer 的功能:

import { UnwrapRef } from "vue";
import useState from "./useState";

type ReducerType = (state: T, action: A) => any;
export default function useReducer(
reducer: ReducerType<UnwrapRef, A>,
initialArg: T,
init?:
(value: T) => T
) {
// 根据传没传init函数来初始化state
const [state, setState] = useState(init ? init(initialArg) : initialArg);
const dispatch = (action: A) => {
// 通过reducer函数的返回结果来修改state的值
setState((state) => reducer(state, action));
};
return [state, dispatch] as const;
}

<template>
<div>
<div>
<p>Count: {{ state.count }}p>
<button @click="() => dispatch({ type: 'increment' })">
加一
button>
<button @click="() => dispatch({ type: 'decrement' })">
减一
button>
div>
div>
template>

useCallback

React 中的 useCallback:

useCallback 是 React 中的一个 Hook,用于返回一个 memoized 版本的回调函数,避免在每次渲染时都创建新的回调函数。这在防止不必要的渲染和优化性能方面非常有用。

基本语法如下:

import React, { useState, useCallback } from 'react';

function ExampleComponent() {
const [count, setCount] = useState(0);

// 使用 useCallback 返回 memoized 版本的回调函数
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // 依赖数组中的值发生变化时,重新创建回调函数

return (
<div>
<p>Count: {count}p>
<button onClick={handleClick}>加一button>
div>
);
}

下面是一个使用 useState 和 vue3 中的 watch 模拟实现类似 useCallback 的例子:

import { watch } from "vue";
import useState from "./useState";

type FnType = (...args: T[]) => any;
export default function useCallback(fn: FnType, dependencies: D[]) {
const [callback, setCallback] = useState(fn);
// 如果依赖项有变更就把fn重新赋值没有就直接返回callback
watch(
dependencies,
() => {
setCallback((cb: FnType) => cb = fn);
},
{ immediate: false }
);
return callback;
}

<template>
<div>
<button>{{ count }}button>
<button @click="() => handerCount()">+button>
div>
template>

useMemo

React 中的 useMemo:

useMemo 是 React 中的一个 Hook,用于记忆(memoize)计算结果,避免在每次渲染时都重新计算。它对于在渲染期间执行昂贵的计算并确保只在依赖项更改时重新计算结果非常有用。

基本语法如下:

import React, { useState, useMemo } from 'react';

function ExampleComponent() {
const [count, setCount] = useState(0);

// 使用 useMemo 记忆计算结果
const expensiveCalculation = useMemo(() => {
console.log('计算了一次...');
return count * 2;
}, [count]); // 依赖数组中的值发生变化时,重新计算结果

return (
<div>
<p>Count1: {count}p>
<p>Count2: {expensiveCalculation}p>
<button onClick={() => setCount(count + 1)}>加一button>
div>
);
}

下面是一个使用 useStateuseEffect 和 vue3 的 computed 模拟实现类似 useMemo 的例子:

import { UnwrapRef, computed } from "vue";
import useEffect from "./useEffect";
import useState from "./useState";

export default function useMemo(
calculateValue: () => R,
dependencies: T[]
) {
const [cache, setCache] = useStatenull>(null);
// 判断依赖项有没有变更,没有就直接返回缓存,有的话就重新计算
useEffect(() => {
setCache((cache) => {
return (cache = computed(calculateValue) as UnwrapRef);
});
}, dependencies);
return cache as UnwrapRef;
}

<template>
<div>
<div>平方: {{ squareSum }}div>
<div>平方: {{ squareSum }}div>
<button @click="handelNumbers">更改numbersbutton>
div>
template>

useRef

React 中的 useRef:

useRef 是 React 中的一个 Hook,主要用于在函数组件中创建一个可变的对象,该对象的 current 属性被初始化为传入的参数。通常用于获取或存储组件中的引用(reference),并且不会触发组件重新渲染。

基本语法如下:

import React, { useRef, useEffect } from 'react';

function ExampleComponent() {
const myRef = useRef(null);

useEffect(() => {
// 使用 myRef.current 访问引用的 DOM 元素
console.log(myRef.current);
}, []);

return <div ref={myRef}>获取DOMdiv>;
}

下面是一个使用 Vue 3 的 ref 模拟实现 useRef 的例子:

import { ref, Ref } from "vue";

function isHTMLElement(obj: unknown): obj is HTMLElement {
return obj instanceof HTMLElement;
}

function useRefextends HTMLElement>(initialValue: T | null): Refnull>;
function useRefextends unknown>(
initialValue: T extends HTMLElement ? never : T
): { current: T };

function useRef(
initialValue: unknown
): Ref<HTMLElement | null> | { current: unknown } {
// 判断传入的是不是一个HTML节点
// 这里可能有点问题就是,或者传入null也会被判定为HTML节点,我没想到怎么解决这个问题
if (isHTMLElement(initialValue) || initialValue === null) {
return ref(initialValue);
} else {
// 不是就返回一个普通对象
return {
current: initialValue,
};
}
}

export default useRef;

<template>
<div>
<input ref="myInputRef" type="text" />
<p>Counter: {{ counterRef.current }}p>
<button @click="incrementCounter">加一button>
div>
template>

补充

对于react中的createContext,useContext和vue3中的provide,inject很像。

React 中的 createContext 和 useContext:

  1. createContext: 用于创建一个上下文对象,它包含一个 Provider 组件和一个 Consumer 组件。createContext 接受一个默认值,这个默认值在组件树中找不到对应的 Provider 时被使用。
const MyContext = React.createContext(defaultValue);
  1. useContext: 用于在函数组件中订阅上下文的变化,获取当前 Provider 提供的值。
const contextValue = useContext(MyContext);

Vue3 中的 provide 和 inject:

  1. provide: 用于在父组件中提供数据,被提供的数据可以被子组件通过 inject 访问到。provide 接受一个对象,对象的属性即为提供的数据。

  1. inject: 用于在子组件中注入父组件提供的数据。可以是一个数组,也可以是一个对象,对象的属性为子组件中的变量名,值为从父组件中注入的数据。

相似之处:

  • 目的相同: 无论是 React 中的上下文和钩子,还是 Vue 3 中的 provide 和 inject,它们都旨在实现组件之间的状态共享,提供一种在组件树中传递数据的方式。
  • 使用方式: 在使用上,它们都在父组件中提供数据,并在子组件中获取数据。
  • 避免了 props 层层传递: 这些机制都避免了将数据通过 props 层层传递的麻烦,特别在深层嵌套的组件树中,可以更方便地进行状态管理。

总体而言,虽然具体的实现和语法有所不同,但这些机制在概念上非常相似,都是为了解决在组件树中共享数据的问题。

总结

本文源于作者的一时灵感,尝试探讨在 Vue 3 中是否能实现类似 React Hooks 的功能。虽然这个想法是出于好奇和娱乐,但在实际的开发中或许并没有太多实际用途。

通过对比 React Hooks 和 Vue 3 Composition API,我们发现两者在语法和实现上存在一些差异,但本质上都为开发者提供了在函数组件中组织和重用逻辑的方式。这种灵活性是前端技术不断演进的体现,而每一种方式都有其适用的场景。

在实际项目中,选择使用 React Hooks 还是 Vue3 Composition API 取决于团队和个人的偏好,以及项目的具体需求。技术的发展是不断前行的过程,而我们在其中的探索和实践都是宝贵的经验。

愿读者在技术的海洋中,既能保持对新鲜事物的好奇心,又能在实际项目中选择合适的工具,取得更好的开发效果。无论是整活还是严肃的技术探讨,都让我们在编码的世界里保持一份热爱和乐趣。


作者:辛克莱
来源:juejin.cn/post/7328229830134972425
收起阅读 »

换个角度学TS,也许你能熟悉它

web
前言 TS绝非为了炫技的存在, 一切为了类型安全, 牢记这句话, 牢记这句话, 牢记这句话!!!感谢林不渡和光神大佬, 本文基本上都是从大佬们那里学来的。 一道开胃菜 function memoize

前言


TS绝非为了炫技的存在, 一切为了类型安全, 牢记这句话, 牢记这句话, 牢记这句话!!!感谢林不渡和光神大佬, 本文基本上都是从大佬们那里学来的。


一道开胃菜


function memoizeextends (...args: any[]) => any>(fn: T) {
const cache = new Map()
return (...args: Parameters<typeof fn>) => {
const key = JSON.stringify(fn)
if (cache.has(key)) {
return cache.get(key)
}
const result = fn(...args)
cache.set(key, result)
return result
}
}

const add = (a: number, b: number) => a + b
const memoAdd = memoize(add)
console.log(memoAdd(1, 2)) // 3
console.log(memoAdd(1, 2)) // 3

上面这个缓存函数的有个泛型T, 泛型T被约束为是一个可以是任意类型参数和返回任意类型的函数, 函数被调用时才明确知道T的类型, 比如下面的add函数, 这个时候TS类型就自动推导T是一个a、b参数类型为number返回一个number的函数, 我们看缓存函数内部返回一个函数接收的参数类型应该和T的参数类型一致, 这个时候可以使用TS内置的工具类型Parameters, 它可以返回一个函数类型的参数数组类型, 所以我们typeof fn, 就可以拿到fn的类型, 或者直接使用Parameters


我们来看看Parameters是怎么实现的:


type Parametersextends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;

Parameters工具类型接收一个泛型T被约束为可以是任意的函数类型, 后面用了infer类型推导, 可以在类型被使用时推导出具体的类型, T如果是(...args: infer P) => any的子类型, 就返回P, 否则返回never(表示永不可达)。


不熟悉这种语法没关系, 我们把TS的内置类型都实现一遍,熟能生巧。


TS内置类型工具


Awaited


// 基础用法
type promise = Promise<string>
type p = Awaited // string

// 定义一个返回 Promise 的函数
function fetchData(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve('成功啦啦啦');
}, 1000);
});
}
// 使用 Awaited 获取 Promise 结果的类型
type ResultType = Awaited<ReturnType<typeof fetchData>>;

const result: ResultType = 'Awaited'; // 此处类型会被推断为 string

async function useResult() {
const data = await fetchData();
console.log(data); // 此处 data 的类型已经被推断为 string
}
useResult();

这里的ReturnType和Parameters是配对的, 一个是获取函数类型的参数类型, 一个是获取函数类型的返回值类型, 我们看看ReturnType是怎么实现的


type ReturnTypeextends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;

我们发现和上面Parameters的实现如出一辙, 只是把infer推断从参数的位置移动到了函数返回值的位置。不过ReturnType如果不满足条件返回的是any而不是never了, 这并没有什么影响。所以ReturnType拿到的类型是定义promise函数的返回类型Promise, 而我们的Awaited就是要拿到Promise里面的类型string


这里有个思路


type MyAwait = T extends Promise ? P : never
type p = MyAwait<Promise<string>> // string

利用infer推断Promise里面的类型, 好的, 恭喜你, 你已经了解TS了, 可是这并不全面, 如果Promise返回的还是一个Promise呢


type MyAwait = T extends Promise ? P : never
type p = MyAwait<Promise<Promise<string>>> // Promise

递归?如果P还是一个Promise就递归调用MyAwait直到拿到最里面的类型, 没错就是你想的这样, 非常完美


type MyAwait = T extends Promise // T如果是Promise的子类型
? P extends Promise<unknown> // 如果推断出来的P还是一个Promise
? MyAwait

// 递归MyAwait


: P // 不是Promise就直接返回P
: T; // 如果泛型传的都不是一个promise直接返回T
type p = MyAwait<Promise<Promise<string>>>; // string


我们来看看TS内部是如何实现的


type Awaited = T extends null | undefined
? T // 对于 `null | undefined` 这种特殊情况,当未启用 `--strictNullChecks` 时,直接返回 T
: T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
// 仅当类型 T 是一个对象并且具有可调用 `then` 方法时,才会进行下一步处理
? F extends (value: infer V, ...args: infer _) => any
// 如果 `then` 方法的参数是可调用的,提取其第一个参数的类型
? Awaited // 递归地解开该值的嵌套异步类型
: never // `then` 方法的参数不可调用
: T; // 非对象或不具有 `then` 方法的类型

Partial


// 基础用法
type obj = {
a: 1,
b: 2
}
type obj2 = Partial
/**
* type obj2 = {
a?: 1 | undefined;
b?: 2 | undefined;
}
*/


我们来实现实现,上面我们讲的是infer提取类型如果有深层的话就是extends配合三元递归,这个Partial就可以用映射成新类型?:表示的就是可选, 可选后除了原来的类型还联合了undefined类型, 这是为了类型安全考虑可选后就可能没有值。


// 基础用法
type obj = {
a: 1,
b: 2
}
type MyPartial = {
[K in keyof T]?: T[K]
}
type obj2 = MyPartial
/**
* type obj2 = {
a?: 1 | undefined;
b?: 2 | undefined;
}
*/


原来的obj类型被构造成了一个新的类型obj2,还是一个对象, 对象里面的每一个键(K)是(in)对象里面所有的键(keyof T)?:(可选) T[K](T对象里面的K键的值),反复想想是不是这回事。


如果有多个对象嵌套,就递归


type obj = {
a: 1,
b: {
c: 2
}
}
type DeepPartial = {
[K in keyof T]?: T[K] extends object ? DeepPartial : T[K]
}
type obj2 = DeepPartial
/**
* type obj2 = {
a?: 1 | undefined;
b?: DeepPartial<{
c: 2;
}> | undefined;
}
*/


Required


// 基础用法
type obj = {
a: 1,
b: {
c: 2
}
}
type MyPartial = {
[K in keyof T]?: T[K]
}
type obj2 = Required<MyPartial>
/**
*type obj2 = {
a: 1;
b: {
c: 2;
};
}
*/


Required就是把可选的变成必传的,非常简单,只需要把?去掉


// 基础用法
type obj = {
a: 1,
b: {
c: 2
}
}
type MyPartial = {
[K in keyof T]?: T[K]
}
type MyRequired = {
[K in keyof T]-?: T[K]
}
type obj2 = MyRequired<MyPartial>
/**
*type obj2 = {
a: 1;
b: {
c: 2;
};
}
*/


直接-?就可以了,神奇吧,那如果对象嵌套了,我想你会举一反三了吧,没错还是它递归


type DeepRequired = {
[K in keyof T]-?: T[K] extends object ? DeepRequired : T[K]
}

Readonly


type obj = {
a: 1,
b: {
c: 2
}
}
type obj2 = Readonly
/**
* type obj2 = {
readonly a: 1;
readonly b: {
c: 2;
};
}
*/


Readonly就是把对象里面的值变成只读的,看这个obj2的类型,我想你知道怎么做了吧


type MyReadonly = {
readonly [K in keyof T]: T[K]
}

type DeepReadonly = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]
}

Record


type obj = Record<string, any>
/**
* type obj = {
[x: string]: any;
}
*/


其实根据上面学的,你已经会实现它了


type MyRecordextends keyof any, T> = {
[P in K]: T
}

type obj = MyRecord<string, any>
/**
* type obj = {
[x: string]: any;
}
*/


K extends keyof any: 这里使用了泛型约束,确保 K 是任何类型的键集合的子集。keyof any 表示任何类型的所有键的联合。[P in K]: T: 这是一个映射类型。它表示对于 K 中的每个键 P,都创建一个属性,其值类型是 T


Pick


type MyPickextends object, K extends keyof T> = {
[P in K]: T[K]
}

type obj = MyPick<{a: 1, b: 2}, 'a'>
/***
* type obj = {
a: 1;
}
*/


Omit


type MyOmitextends object, K extends keyof T> =
PickExclude>

type obj = MyOmit<{ a: 1, b: 2 }, 'a'>
/***
* type obj = {
b: 2;
}
*/



  • Exclude: 使用 keyof T 获取对象 T 的所有键,然后使用 Exclude 排除其中与 K 相同的键。这样得到的是 T 中除去 K 对应键的键集合。

  • Pick: 使用 Pick 从对象 T 中选择具有特定键集合的属性。在这里,我们选择了 T 中除去 K 对应键的其他所有属性。


我们来看看Exclude的实现


Exclude


type MyExclude = T extends U ? never : T
type T0 = MyExclude<"a" | "b" | "c", "a">;
// type T0 = "b" | "c"

如果T中存在U就剔除(never)否则保留


Extract


很明显就是Exclude的反向操作


type MyExtract = T extends U ? T : never
type T0 = MyExtract<"a" | "b" | "c", "a">;
// type T0 = "a"

NonNullable


type T0 = NonNullable<string | number | undefined>;
type T1 = NonNullable<string[] | null | undefined>;
type NonNullable = T & {};

T0 的类型是 string | number。这是因为 NonNullable 类型别名被定义为 T & {},这意味着它接受一个类型 T,并返回一个新的类型,该新类型是 T 和空对象类型的交叉类型。由于 TypeScript 中的交叉类型(T & {})会过滤掉 nullundefined,因此 T0 的类型就是排除了 undefinedstring | number


也可以这样实现


type MyNonNullable = T extends null | undefined ? never : T;

ConstructorParameters


type MyConstructorParametersextends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
class C {
constructor(a: number, b: string) {}
}
type T3 = MyConstructorParameters<typeof C>;
// type T3 = [a: number, b: string]

还是老套路infer推断,这个和我们上面那个获取函数参数类型差不多的,换了个形式。



  • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。

  • T extends abstract new (...args: infer P) => any ? P : never: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的参数类型 P,否则返回 never


InstanceType


class C {
x = 0;
y = 0;
}
type MyInstanceTypeextends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : never;
type T0 = MyInstanceType<typeof C>;
// type T0 = C

和我们上面那个实现的获取函数类型返回值类型如出一辙和获取类的构造器类型是配对的。



  • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。

  • T extends abstract new (...args: any) => infer R ? R : any: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的实例类型 R,否则返回 never


ThisParameterType


function toHex(this: Number) {
return this.toString(16);
}
function numberToString(n: ThisParameterType<typeof toHex>) {
return toHex.apply(n);
}

像这种提取类型的都用infer,先猜一猜,内部肯定是判断T是不是一个函数, 第一个参数this infer R是就返回R不是就返回never。
我们看看答案


type ThisParameterType =
T extends (this: infer U, ...args: never) => any
? U
: unknown;

和我们猜想的差不多,我想你现在应该可以类型编程了吧。


TS内部还有四个内置类型是通过JS来实现的,我们就不研究了


`Uppercase`
`Lowercase`
`Capitalize`
`Uncapitalize`

可以看看我的这篇文章vue里面对于TS的使用 # 突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?


祝大家TS的编程技术越来越好吧,本人非常菜,大佬轻喷。


作者:辛克莱
来源:juejin.cn/post/7332435905926070322

JS 不写分号踩了坑,但也可以不踩坑

web
前言 “所有直觉性的 “当然应该加分号” 都是保守的、未经深入思考的草率结论。” —— 尤雨溪。 重新认识分号在代码中的作用,为什么要写分号?又为什么可以没有分号? 踩的坑 写一个方法将秒数转为“xx天xx时xx分xx秒”的形式 const ONEDAYSEC...
继续阅读 »

前言


“所有直觉性的 “当然应该加分号” 都是保守的、未经深入思考的草率结论。” —— 尤雨溪。

重新认识分号在代码中的作用,为什么要写分号?又为什么可以没有分号?


踩的坑


写一个方法将秒数转为“xx天xx时xx分xx秒”的形式


const ONEDAYSECOND = 24 * 60 * 60
const ONEHOURSECOND = 60 * 60
const ONEMINUTESECOND = 60

function getQuotientandRemainder(dividend,divisor){
const remainder = dividend % divisor
const quotient = (dividend - remainder) / divisor
return [quotient,remainder]
}

function formatSeconds(time){
let restTime,day,hour,minute
restTime = time
[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)
[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)
[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)
return day + '天' + hour + '时' + minute + '分' + restTime + '秒'
}
console.log(formatSeconds(time)) // undefined天undefined时undefined分NaN,NaN秒

按照这段代码执行完后,day、hour、minute这些变量得到的都是 undefined,而 restTime 则好像得到一个数组。

问题就在于 13、14、15、16 行之间没有添加分号,导致解析时,没有将这三行解析成三条语句,而是解析成一条语句。最终的表达式就是这样的:


restTime = time[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)

那执行的过程相当于给 restTime 进行赋值,表达式从左往右执行,最终表达式的值为右值。最右边的值就是 getQuotientandRemainder(restTime,ONEMINUTESECOND),由于在计算过程中 restTime 还没有被赋值,一直是 undefined,所以经过 getQuotientandRemainder 计算后得到的数组对象每个成员都是 NaN,最终赋值给 restTime 就是这样一个数组。


分号什么时候会“自动”出现


有时候好像不写分号也不会出问题,比如这种情况:


let a,b,c
a = 1
b = 2
c = 3
console.log(a,b,c) // 1 2 3

这是因为,JS 进行代码解析的时候,能够识别出语句的结束位置并“自动添加分号“,从而能够解析出“正确”的抽象语法树,最终执行的结果也就是我们所期待的。

JS 有一个语法特性叫做 ASI (Automatic Semicolon Insertion),就是上面说到的”自动添加分号”的东西,它有一定的插入规则,在满足时会为代码自动添加分号进行断句,在我们不写分号的时候,需要了解这个规则,才能不踩坑。(当然这里说的加分号并不是真正的加分号,只是一种解析规则,用分号来代表语句间的界限)


ASI 规则


JS 只有在出现换行符的时候才会考虑是否添加分号,并且会尽量“少”添加分号,也就是尽量将多行语句合成一行,仅在必要时添加分号。


1. 行与行之间合并不符合语法时,插入分号


比如上面那个自动添加分号的例子,就是合并多行时会出现语法错误。

a = 1b = 2 这里 1b 是不合法的,因此会加入分号使其合法,变为 a = 1; b = 2


2. 在规定[no LineTerminator here]处,插入分号


这种情况很有针对性,针对一些特定的关键字,如 return continue break throw async yield,规定在这些关键字后不能有换行符,如果在这些关键字后有了换行符,JS 会自动在这些关键字后加上分号。
看下面这个例子🌰:


function a(){
return
123
}
console.log(a()) // undefined

function b(){
return 123
}
console.log(b()) // 123

在函数a中,return 后直接换行了,那么 return 和 123 就会被分成两条语句,所以其实 123 根本不会被执行到,而 return 也是啥也没返回。


3. ++、--这类运算符,若在一行开头,则在行首插入分号


++ 和 -- 既可以在变量前,也可以在变量后,如果它们在行首,当多行进行合并时,会产生歧义,到底是上一行变量的运算,还是下一行变量的运算,因此需要加入分号,处理为下一行变量的运算。


a
++
b
// 添加分号后
a
++b

如果你的预期是:


a++ 
b

那么就会踩坑了。


4. 在文件末尾发现语法无法构成合法语句时,会插入分号


这条和 1 有些类似


不写分号时需要注意⚠️


上面的 ASI 规则中,JS 都是为了正确运行代码,必须按照这些规则来分析代码。而它不会做多余的事,并且在遵循“尽量合并多行语句”的原则下,它会将没有语法问题的多行语句都合并起来。这可能违背了你的逻辑,你想让每行独立执行,而不是合成一句。开头贴出的例子,就是这样踩坑的,我并不想一次次连续的对数组进行取值🌚。

因此我们要写出明确的语句,可以被合并的语句,明确是多条语句时需要加上分号。


(如果你的项目中使用了某些规范,它不想让你用分号,别担心,它只是不想让你在行尾用分号,格式化时它会帮你把分号移到行首)像这样:


// before lint
restTime = time;
[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND);
[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND);
[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND);

// after lint
restTime = time
;[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND)
;[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND)
;[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND)

参考



作者:用户9787521254131
来源:juejin.cn/post/7269645636210458635
收起阅读 »

基于 localStorage 实现有过期时间的存储方式

web
我们知道 localStorage 中的数据是长期保存的,除非手动删除,否则他会一直存在。如果我们想实现一种数据有过期时间的存储方式,该怎么实现呢? 首先应该想到的是 cookie,cookie 本身就有有效期的配置,当某个 cookie 时,浏览器自动清理该...
继续阅读 »

我们知道 localStorage 中的数据是长期保存的,除非手动删除,否则他会一直存在。如果我们想实现一种数据有过期时间的存储方式,该怎么实现呢?


首先应该想到的是 cookie,cookie 本身就有有效期的配置,当某个 cookie 时,浏览器自动清理该 cookie。可是使用 cookie 存储数据,有个不好的地方,很多我们存储的数据,本就是我们前端自己用到的,后端根本用不到。可是存储到 cookie 中后,页面中所有的 cookie 都会随着请求发送给后端,造成传输的 cookie 比较长,而且没有必要。


低调低调


因此,我们可以基于 localStorage 来实现一套这样的有过期时间的存储方式。我们在之前的文章 如何重写 localStorage 中的方法 中,也了解了一些重写 localStorage 的方法。这里我们是自己在外层封装一层的方式,来调用 localStorage。


我这里封装的类名叫: LocalExpiredStorage,即有过期时间的 localStorage。


1. 实现与 localStorage 基本一致的 api


我们为了实现跟 localStorage 使用上的一致性体验,这里我们自己的 api 名称和实现方式跟 localStorage 基本一致。


interface SetItemOptions {
maxAge?: number; // 从当前时间往后多长时间过期
expired?: number; // 过期的准确时间点,优先级比maxAge高
}

class LocalExpiredStorage {
private prefix = "local-"; // 用于跟没有过期时间的key进行区分

constructor(prefix?: string) {
if (prefix) {
this.prefix = prefix;
}
}

setItem(key: string, value: any, options?: SetItemOptions) {}
getItem(key: string): any {}
removeItem(key: string) {}
clearAllExpired() {}
}
const localExpiredStorage = new LocalExpiredStorage();
export default localExpiredStorage;

可以看到我们实现的类里,有三个变化:



  1. setItem()方法新增了一个 options 参数,这里主要是为了配置过期时间,这里有两种配置方式,一种是可以设置多长时间后过期,比如 2 个小时后过期(开发者不用特殊计算 2 个小时后的时间节点);再一种是设置过期的时间节点,该值可以是格式化的时间,也可以是时间戳;

  2. 有一个 prefix 属性,在具体实现中,我们会将 prefix 属性与操作的 key 进行拼接,标识该 key 是具有过期时间特性的,方便我们自己的类进行处理;

  3. 新增了一个 clearAllExpired() 方法,这是为了清理所有已经过期的 key,避免占用缓存;该方法在应用的入口处就应当调用,便于及时清理;


上面是我们的大致框架,接下来我们来具体实现下这些方法。


干饭


2. 具体实现


接下来我们来一一实现这些方法。


2.1 setItem


这里我们新增了一个 options 参数,用来配置过期时间:



  • expired: 固定的过期时间点,比如点击关闭按钮,当天不再展示,那过期时间就是今天晚上的 23:59:59,可以使用该属性;

  • maxAge: 从当前时间起,设置多长时间后过期;比如点击某个提示,3 天内不再展示,使用该属性就比较方便;


假如两个属性都设置了,我这里约定 expired 属性的优先级更高一些。


class LocalExpiredStorage {
private prefix = "local-"; // 用于跟没有过期时间的key进行区分

constructor(prefix?: string) {
if (prefix) {
this.prefix = prefix;
}
}

setItem(key: string, value: any, options?: SetItemOptions) {
const now = Date.now();
let expired = now + 1000 * 60 * 60 * 3; // 默认过期时间为3个小时

// 这里我们限定了 expired 和 maxAge 都是 number 类型,
// 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
if (options?.expired) {
expired = options?.expired;
} else if (options?.maxAge) {
expired = now + options.maxAge;
}

// 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
// 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
localStorage.setItem(
`${this.prefix}${key}`,
JSON.stringify({
value,
start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
})
);
}
}

我们在过期时间的实现过程中,目前只支持了 number 类型,即需要传入一个时间戳,参与运算。您也可以扩展到 string 类型(比如'2024/11/23 14:45:34')或者其他格式{ d:2, h:3 } 这种格式。


设置好过期时间后,我们将 value,存储的起始时间和过期时间,转义成 json string 存储起来。我们这里用了 dayjs 对时间戳进行格式化,方便开发者可以快速地识别。若没有这个需要,也可以直接存储时间戳,减少第三方类库的依赖。


该方法并没有支持永久存储的设定,若您需要永久存储,可以直接使用 localStorage 来存储。


2.2 getItem


获取某 key 存储的值,主要是对过期时间的判断。


class LocalExpiredStorage {
private prefix = "local-"; // 用于跟没有过期时间的key进行区分

constructor(prefix?: string) {
if (prefix) {
this.prefix = prefix;
}
}

getItem(key: string): any {
const result = localStorage.getItem(`${this.prefix}${key}`);
if (!result) {
// 若key本就不存在,直接返回null
return result;
}
const { value, expired } = JSON.parse(result);
if (Date.now() <= dayjs(expired).valueOf()) {
// 还没过期,返回存储的值
return value;
}
// 已过期,删除该key,然后返回null
this.removeItem(key);
return null;
}
removeItem(key: string) {
localStorage.removeItem(`${this.prefix}${key}`);
}
}

在获取 key 时,主要经过 3 个过程:



  1. 若本身就没存储这个 key,直接返回 null;

  2. 已存储了该 key 的数据,解析出数据和过期时间,若还在有效期,则返回存储大数据;

  3. 若已过期,则删除该 key,然后返回 null;


这里我们在删除数据时,使用了this.removeItem(),即自己实现的删除方法。本来我们也是要实现这个方法的,那就直接使用了吧。


2.3 clearAllExpired


localStorage 中的数据并不会自动清理,我们需要一个方法用来手动批量清理已过期的数据。


class LocalExpiredStorage {
private prefix = "local-"; // 用于跟没有过期时间的key进行区分

clearAllExpired() {
let num = 0;

// 判断 key 是否过期,然后删除
const delExpiredKey = (key: string, value: string | null) => {
if (value) {
// 若value有值,则判断是否过期
const { expired } = JSON.parse(value);
if (Date.now() > dayjs(expired).valueOf()) {
// 已过期
localStorage.removeItem(key);
return 1;
}
} else {
// 若 value 无值,则直接删除
localStorage.removeItem(key);
return 1;
}
return 0;
};

const { length } = window.localStorage;
const now = Date.now();

for (let i = 0; i < length; i++) {
const key = window.localStorage.key(i);

if (key?.startsWith(this.prefix)) {
// 只处理我们自己的类创建的key
const value = window.localStorage.getItem(key);
num += delExpiredKey(key, value);
}
}
return num;
}
}

在项目的入口处添加上该方法,用户每次进入项目时,都会自动清理一次已过期的 key。


醒一醒


3. 完整的代码


上面我们是分步讲解的,这里我们放下完整的代码。同时,我也在 GitHub 上放了一份:wenzi0github/local-expired-storage


interface SetItemOptions {
maxAge?: number; // 从当前时间往后多长时间过期
expired?: number; // 过期的准确时间点,优先级比maxAge高
}

class LocalExpiredStorage {
private prefix = "local-"; // 用于跟没有过期时间的key进行区分

constructor(prefix?: string) {
if (prefix) {
this.prefix = prefix;
}
}

// 设置数据
setItem(key: string, value: any, options?: SetItemOptions) {
const now = Date.now();
let expired = now + 1000 * 60 * 60 * 3; // 默认过期时间为3个小时

// 这里我们限定了 expired 和 maxAge 都是 number 类型,
// 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
if (options?.expired) {
expired = options?.expired;
} else if (options?.maxAge) {
expired = now + options.maxAge;
}

// 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
// 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
localStorage.setItem(
`${this.prefix}${key}`,
JSON.stringify({
value,
start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
})
);
}

getItem(key: string): any {
const result = localStorage.getItem(`${this.prefix}${key}`);
if (!result) {
// 若key本就不存在,直接返回null
return result;
}
const { value, expired } = JSON.parse(result);
if (Date.now() <= dayjs(expired).valueOf()) {
// 还没过期,返回存储的值
return value;
}
// 已过期,删除该key,然后返回null
this.removeItem(key);
return null;
}

// 删除key
removeItem(key: string) {
localStorage.removeItem(`${this.prefix}${key}`);
}

// 清除所有过期的key
clearAllExpired() {
let num = 0;

// 判断 key 是否过期,然后删除
const delExpiredKey = (key: string, value: string | null) => {
if (value) {
// 若value有值,则判断是否过期
const { expired } = JSON.parse(value);
if (Date.now() > dayjs(expired).valueOf()) {
// 已过期
localStorage.removeItem(key);
return 1;
}
} else {
// 若 value 无值,则直接删除
localStorage.removeItem(key);
return 1;
}
return 0;
};

const { length } = window.localStorage;
const now = Date.now();

for (let i = 0; i < length; i++) {
const key = window.localStorage.key(i);

if (key?.startsWith(this.prefix)) {
// 只处理我们自己的类创建的key
const value = window.localStorage.getItem(key);
num += delExpiredKey(key, value);
}
}
return num;
}
}
const localExpiredStorage = new LocalExpiredStorage();
export default localExpiredStorage;

使用:


localExpiredStorage.setItem("key", "value", { maxAge: 5000 }); // 有效期为5000毫秒
localExpiredStorage.setItem("key", "value", {
expired: Date.now() + 1000 * 60 * 60 * 12,
}); // 有效期为 12 个小时,自己计算到期的时间戳

// 获取数据
localExpiredStorage.getItem("key");

// 删除数据
localExpiredStorage.removeItem("key");

// 清理所有过期的key
localExpiredStorage.clearAllExpired();

4. 总结


这个功能本身不难,也有很多开发者自己实现过。这里我也是总结下之前实现的过程。



作者:小蚊酱
来源:juejin.cn/post/7215775714417655867
收起阅读 »

url请求参数带有特殊字符“%、#、&”时,参数被截断怎么办?

web
是的,最近又踩坑了! 事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。 一排查,发现特殊字符“%%%”并未成功传给后端。 我们的这个全局搜索功能是需要跳转页面才能查看到搜索结...
继续阅读 »

是的,最近又踩坑了!


事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。


一排查,发现特殊字符“%%%”并未成功传给后端。


我们的这个全局搜索功能是需要跳转页面才能查看到搜索结果的。所以,搜索条件是作为参数拼接在页面url上的。


正常的传参:


image.png


当输入的是特殊字符“%、#、&”时,参数丢失


image.png


也就是说,当路由请求参数带有浏览器url中的特殊含义字符时,参数会被截断,无法正常获取参数。


那么怎么解决这个问题呢?


方案一:encodeURIComponent/decodeURIComponent


拼接参数时,利用encodeURIComponent()进行编码,接收参数时,利用decodeURIComponent()进行解码。


// 编码
this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(searchValue)}`});

// 解码
const text = decodeURIComponent(this.$route.query.text)

此方法对绝大多数特殊字符都适用,但是唯独输入“%”进行搜索时不行,报错如下。


image.png


所以在编码之前,还需进行一下如下转换:



this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(encodeSpecialChar(searchValue))}`});


/**
* @param {*} char 字符串
* @returns
*/

export const encodeSpecialChar = (char) => {
// #、&可以不用参与处理
const encodeArr = [{
code: '%',
encode: '%25'
},{
code: '#',
encode: '%23'
}, {
code: '&',
encode: '%26'
},]
return char.replace(/[%?#&=]/g, ($) => {
for (const k of encodeArr) {
if (k.code === $) {
return k.encode
}
}
})
}


方案二: qs.stringify()


默认情况下,qs.stringify()方法会使用encodeURIComponent方法对特殊字符进行编码,以保证URL的合法性。


const qs = require('qs');

const searchObj = {
type: selectValue,
text: searchValue
};
this.$router.push({path: `/crm/global-search/search-result?${qs.stringify(searchObj)}`});


使用了qs.stringify()方法,就无需使用encodeSpecialChar方法进行转换了。


作者:HED
来源:juejin.cn/post/7332048519156776979
收起阅读 »

别再只用axios了,试试这个更轻量的网络请求库!

web
嗨,我们又见面了!今天我要和大家分享一个前端工程师都会遇到的问题:如何优化移动端网络请求。相信很多人都用过 axios 或 fetch API,但你是否觉得处理分页逻辑、缓存、请求防抖等很麻烦呢?别担心,我找到了一个解决方案——Alova.js。 Alova...
继续阅读 »

嗨,我们又见面了!今天我要和大家分享一个前端工程师都会遇到的问题:如何优化移动端网络请求。相信很多人都用过 axios 或 fetch API,但你是否觉得处理分页逻辑、缓存、请求防抖等很麻烦呢?别担心,我找到了一个解决方案——Alova.js。



Alova.js 是一个轻量级的请求策略库,它可以帮助我们简化网络请求的编写,让我们更专注于业务逻辑。它提供了多种服务端数据缓存模式,比如内存模式和持久化模式,这些都能提升用户体验,同时降低服务端的压力。而且,Alova.js 只有 4kb+,体积是 axios 的 30%,非常适合移动端使用。


Alova.js 的基础请求功能非常简单,比如你可以这样请求数据:


const todoDetail = alova.Get('/todo', { params: { id: 1 } });
const { loading, data, error } = useRequest(todoDetail);

它还提供了分页请求、表单提交、验证码发送、文件上传等多种请求策略,大大减少了我们的工作量。比如,使用分页请求策略,你只需要这样:


const {
loading,
data,
isLastPage,
page,
pageSize,
pageCount,
total,
} = usePagination((page, pageSize) => queryStudents(page, pageSize));

怎么样,是不是很简单?Alova.js 还支持 Vue、React、React Native、Svelte 等多种前端框架,以及 Next、Nuxt、SvelteKit 等服务端渲染框架,非常适合现代前端开发。


感兴趣的话,可以去 Alova.js 的官网看看:Alova.js 官网。也可以在评论区分享你对 Alova.js 的看法哦!嘿嘿,今天就聊到这里,下次见!👋
有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。


作者:古韵
来源:juejin.cn/post/7332388389944819748
收起阅读 »

记录一次类似页面抽出经历

web
一、背景 刚入职了一家新公司,摸鱼式的把每个项目都打开看了下。不够五分钟便惊出了一身冷汗,午饭时分,便和领导聊了下这些项目中的通病。其中就有一个问题就是代码复用的问题,一个Vue文件6000多行代码,多个文件中还有很多重复的代码。领导听了休息了片刻便拍案而起,...
继续阅读 »

一、背景


刚入职了一家新公司,摸鱼式的把每个项目都打开看了下。不够五分钟便惊出了一身冷汗,午饭时分,便和领导聊了下这些项目中的通病。其中就有一个问题就是代码复用的问题,一个Vue文件6000多行代码,多个文件中还有很多重复的代码。领导听了休息了片刻便拍案而起,好的小陈你就把这几个类似的页面抽出来吧!我:。。。。默默扒饭。


二、问题和方案


类似登录页这种几百年不变的页面,多个项目不管是逻辑还是UI基本上都是一样的,多个项目要用。虽然CV也挺快,但是如果逻辑一改的话,yi那其实还是挺麻烦的。(领导视角)


方案一:Iframe嵌入主项目❌


一开始是想把要引入的页面打包然后通过Iframe引入,但是这样的话会存在域不同的问题,而无法随心所欲的操作本地存储之类的东西。虽然可以用postMessage的方法进行通信传输数据,但是要传输到主页面的信息一多的话,很难分清楚哪个数据是所需要的。在尝试了半天之后,PASS了这个方案。


方案二:将页面打包成组件,然后在主项目中注册且使用✔


通过采用lib库模式打包Vue页面为组件,然后在主项目中引入,便可以实现页面的复用。然后引入组件也可以自由的访问本地存储等东西。
打包命令:


vue-cli-service build --target lib --name main --dest lib src/components/index.js

详情可以参考官网的指南
构建目标 | Vue CLI (vuejs.org)


接下来便是痛苦且折磨的试错之路😖


初步实现



  1. 要引入页面的项目结构(components中的About和Main中的文件即为要打包的文件)
    image.png

  2. 配置库打包的文件

    简单说明下这两个文件的作用:

    Main文件夹下面的index.js作用:包含Main的Vue文件注册成全局组件的方法;

    components文件夹下index.js作用:暴露出一个方法可以批量注册components下的组件。

    image.png


接下来看下这两个文件的具体内容

Main下面的index.js
image.png


components下面的index.js
image.png


看了下这两个文件的内容,写过Vue插件的铁铁们应该都很熟悉,对其实就是把页面当成组件了。有的铁铁举手问,小陈那个initRouter是啥呀,小陈后面为铁铁们解答,我们一步一步慢慢实现。

Main页面如下
image.png


接下来便是通过命令行打包成组件的步骤了
image.png


现在打包的项目这边的任务就告一段落,后面我们看下主项目要如何引用这个被打包的组件。
image.png
只需要在主项目的main中注册我们打包的组件就可以使用了,然后结构出来的Main和About正是刚才我们在components下index.js暴露的两个组件,componentPage则是components下index.js暴露的默认的install方法用于注册。启动下主项目试试就发现我们引入的两个组件都加到主项目路由里面去了。心头一甜但是隐约觉得事情没这么简单。😱


image.png
image.png


三、遇到的问题及解决策略


问题、组件需要使用主项目的路由


以登录页为例子,在用户验证完身份之后,需要跳转到主项目中的其他页面。例如跳转home需要跳转到主项目的home页面,在主项目中点击会报错,因为组件路由根本没有这个路由配置,所以需要把主项目的路由引入到组件中,那要怎么做捏?容小陈慢慢解释。


image.png


解决:在注册的时候引入主项目的路由


通过initRouter在注册组件的时候,把主项目的路由引入到组件中,然后在需要使用主项目路由的时候,使用getCurRouter给组件路由赋值成主项目的路由即可。只不过在使用router的js文件都需要使用getCurRouter。Vue文件中则不需要做任何配置,因为this.router/this.route访问的均是主项目的路由。

组件的路由文件配置
image.png
组件下components的index.js配置
image.png
Main中跳转的方法
image.png
顺便一提:判断生产还是开发环境都是为了开发的时候,不用做额外的配置,只是方法比较笨。如果大佬们有更好的方法麻烦踢一下小陈。


总结


这是小陈第一次在掘金上写文章,可能这篇文章的作用不是很大,但也是记录小陈解决问题的载体。文章有啥不清楚的或者不合理的地方还麻烦铁铁们和小陈促膝长谈。但是此次的实践还是让小陈对Vue的一些知识这块有了新的理解。然后日后还请大佬们多多指教。

Demo的地址: only-for-test: 仅用来测试的仓库 (gitee.com)


作者:用户1863710796985
来源:juejin.cn/post/7250667613020291109
收起阅读 »

曹贼,莫要动‘我’网站 —— MutationObserver

web
前言 本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。 正文 话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子 这么好看的看的小乔,谁看谁不糊,更何...
继续阅读 »

前言


本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。


正文


话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子


image.png
这么好看的看的小乔,谁看谁不糊,更何况曹老板。这天,曹操在浏览网页的时候,无意间发现了周瑜的这个网站,看着美若天仙的小乔,曹操的眼泪止不住的从嘴角流了下来。赶紧将网站上的照片保存了下来。

这个消息最后传到了周瑜的耳朵里,他只是想展示小乔,可不是为了让别人下载的。于是在自己的网站上做了一些预防措施。

为了防止他人直接在网站上直接下载图片,周瑜将右键的默认事件给关闭了,并且为了防止有人打开控制台,并对图片保存,采取了以下方法:


禁用右键和F12键


//给整个document添加右击事件,并阻止默认行为
document.addEventListener("contextmenu", function (e) {
e.preventDefault();
return false;
});

//给整个页面禁用f12按键 keyCode即将被禁用 不再推荐使用 但仍可以使用
document.addEventListener("keydown", function (e) {
//当点了f3\f6\f10之后,即使禁用了f12键依旧可以打开控制台,所以一并禁用
if (
[115, 118, 121, 123].includes(e.keyCode) ||
["F3", "F6", "F10", "F12"].includes(e.key) ||
["F3", "F6", "F10", "F12"].includes(e.code) ||
//ctrl+f 效果和f3效果一样 点开搜索之后依旧可以点击f12 打开控制台 所以一并禁用
//缺点是此网站不再能够 **全局搜索**
(e.ctrlKey && (e.key == "f" || e.code == "KeyF" || e.keyCode == 70))||
//禁用专门用于打开控制台的组合键
(e.ctrlKey && e.shiftKey && (e.key == "i" || e.code == "KeyI" || e.keyCode == 73))
) {
e.preventDefault();
return false;
}
});

当曹操再次想保存小乔照片的时候,发现使用网页的另存了已经没用了。这能难倒曹老板吗,破解方法,在浏览器的右上角进行操作就可打开控制台,这个地方是浏览器自带的,没办法禁用


image.png
这番操作之后,曹操可以选择元素保存那个图片了。周瑜的得知了自己的禁用措施被破解后,赶忙连夜加班打补丁,于是又加了一些操作,禁止打开控制台后进行操作


禁用控制台


如何判定控制台被打开了,可以使用窗口大小来判定


function resize() {
var threshold = 100;
//窗口的外部减窗口内超过100就判定窗口被打开了
var widthThreshold = window.outerWidth - window.innerWidth > threshold;
var heightThreshold = window.outerHeight - window.innerHeight > threshold;
if (widthThreshold || heightThreshold) {
console.log("控制台打开了");
}
}
window.addEventListener("resize", resize);

但是也容易被破解,只要让控制台变成弹窗窗口就可以了


也可以使用定时器进行无限debugger,因为只有在控制台打开的时候debugger才会生效。关闭控制台的时候,并不会影响功能。当前网页内存占用比较大的时候,定时器的占用并不明显。在当前网页占用比较小的时候,一直开着定时器才会有较为明显的提升


  setInterval(() => {
(function () {})["constructor"]("debugger")();
}, 500);

破解方法一样有,在debugger的位置右键禁用调试就可以了。这样控制台就可以正常操作了


image.png
既然有方法破解,就还要做一层措施,既然是要保存图片,那就把img转成canvas,这样即使打开控制台也没办法进行对图片的保存


//获取dom
const img = document.querySelector(".img");
const canvas = document.querySelector("#canvas");
//img转成canvas
canvas.width = img.width;
canvas.height = img.height;
ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
document.body.removeChild(img);

经过一夜的努力,该加的措施都加上了。周瑜心想这下就没办法保存我的小乔了吧。

来到曹操这边,再次打开周瑜的小破站,还想故技重施时,发现已经有了各种显示,最后也没难倒曹操,那些阻碍也都被破解了。但是到保存图片的时候傻眼了,竟然已经不是图片格式了,那就没办法下载了呀。但是小乔真的很养神,曹操心有不甘,于是使用了最后一招,既然没办法下载那就截图,虽然有损画质,但是依旧能看。


得知如此情况的大都督周瑜不淡定了,从未见过如此厚颜无耻之人,竟然使用截图。


006APoFYly1g2qcclw1frg308w06ox2t.gif
话说魔高一尺,道高一丈,周瑜再次熬夜加班进行对网站的优化。于是使用了全屏水印+MutationObserver监听水印dom的方法。即使截图也让他看着不舒服。


MutationObserver


MutationObserver是一个构造函数,接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

它接收一个回调函数,每当监听的dom发生改变时,就会调用这个函数,函数传入一个参数,数组包对象的格式,里面记录着dom的变化以及dom的信息。


image.png
返回的实例是一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象。有三个方法observedisconnecttakeRecords



  • observe接收两个参数,第一个为要监听的dom元素,第二个则是一些配置对象,当调用 observe() 时,childListattributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。配置对象如下:

    • subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false

    • childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false

    • attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false

    • attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

    • attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false

    • characterDate:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

    • characterDateOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false



  • disconnect方法用来停止观察(当被观察dom节点被删除后,会自动停止对该dom的观察),不接受任何参数

  • takeRecords:方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。


该构造函数监听的dom即使在控制台中被更改属性或值,也会被监听到。


使用MutationObserver对水印dom进行监听,并限制更改。


<style>
//定义水印的样式
#watermark {
width: 100vw;
height: 100vh;
position: absolute;
left: 0;
top: 0;
font-size: 34px;
color: #32323238;
font-weight: 700;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-content: space-evenly;
z-index: 9999999;
}
#watermark span {
transform: rotate(45deg);
}
</style>

<script>
//获取水印dom
const watermark = document.querySelector("#watermark");
//克隆水印dom ,用作后备,永远不要改变
const _watermark = watermark.cloneNode(true);
//获取水印dom的父节点
const d = watermark.parentNode;
//获取水印dom的后一个节点
let referenceNode;
[...d.children].forEach((item, index) => {
if (item == watermark) referenceNode = d.children[index + 1];
});
//定义MutationObserver实例observe方法的配置对象
const prop = {
childList: true,//针对整个子树
attributes: true,//属性变化
characterData: true,//监听节点上字符变化
subtree: true,//监听以target为根节点的整个dom树
};
//定义MutationObserver
const observer = new MutationObserver(function (mutations) {
//在这里每次坚挺的dom发生改变时 都会运行,传入的参数为数组对象格式
mutations.forEach((item) => {
//这里可以只针对监听dom的样式来判断
if (item.attributeName === "style") {
//获取父节点的所有子节点,因为时伪数组,使用扩展运算符转以下
[...d.children].forEach((v) => {
//判断一下,是父节点里的那个节点被改变了,并且删除那个被改变的节点(也就是删除水印节点)
if (item.target.id && v == document.querySelector(`#${item.target.id}`)) {
v.remove();
}
});
//原水印节点被删除了,这里使用克隆的水印节点,再次克隆
const __watermark = _watermark.cloneNode(true);
//这里的this指向是MutationObserver的实例对象,所以同样可以使用observe监听dom
//监听第二次克隆的dom
this.observe(__watermark, prop);
//因为水印dom被删除了,再将克隆的水印dom添加到原来的位置 就是referenceNode节点的前面
d.insertBefore(__watermark, referenceNode);
}
});
});
在初始化的时候监听初始化的水印dom
observer.observe(watermark, prop);
</script>



这样,每当对水印dom进行更改样式的时候,就会删除该节点,并重新添加一个初始的水印dom,即使突破重重困难打开开控制台,用户也是无法对dom 进行操作。


视频转Gif_爱给网_aigei_com.gif


隔天曹操再次打开网页,发现网页上的水印,心里不足为惧,心想区区水印能难倒自己?操作到最后却发现,不论如何对水印dom进行操作,都无法改变样式。虽说只是为了保存图片,但是截图有着这样水印,任谁也不舒服呀。曹操大怒,刚吃了两口的饭啪的一下就盖在了桌子上......


20230508094549_33500.gif
然而曹操不知道的是,在控制台中,获取dom节点右键是可以只下载获取的那个节点的......


image.png


结尾


文章主要是以鬼畜恶搞的方式讲述了,如何禁止用户打开控制台(还有重写toSring,consloe.log等一些方法,但我并没有没有实现,所以这里并没有写上),并且如何使用MutationObserver构造函数来监听页面中的dom元素。其实大多情况下并没有这方面的项目需求,完全可以当扩展知识看了。


写的不好的地方可以提出意见,虚心请教!


作者:iceCode
来源:juejin.cn/post/7290862554657423396
收起阅读 »

前端实现 word 转 png

web
在此之前 word 转图片的需求都是在后端实现的,用的是 itext 库,但是 itext 收费的,商用需要付费。近期公司内部安全部门发出侵权警告,要求整改。 所以采用前端实现 word 文档转图片功能。 一、需求 用户在页面上上传 .docx 格式的文件...
继续阅读 »

在此之前 word 转图片的需求都是在后端实现的,用的是 itext 库,但是 itext 收费的,商用需要付费。近期公司内部安全部门发出侵权警告,要求整改。


所以采用前端实现 word 文档转图片功能。



一、需求



  1. 用户在页面上上传 .docx 格式的文件

  2. 前端拿到文件,解析并生成 .png 图片

  3. 上传该图片到文件服务器,并将图片地址作为缩略图字段


二、难点


目前来看,前端暂时无法直接实现将 .docx 文档转成图片格式的需求


三、解决方案


既然直接转无法实现,那就采用迂回战术



  1. 先转成 html(用到库 docx-preview

  2. 再将 html 转成 canvas(用到库 html2canvas

  3. 最后将 canvas 转成 png


四、实现步骤




  1. .docx 文件先转成 html 格式,并插入到目标节点中


    安装 docx-preview 依赖: pnpm add docx-preview --save




jsx
复制代码
import { useEffect } from 'react';
import * as docx from 'docx-preview';

export default ({ file }) => {
useEffect(() => {
// file 为上传好的 docx 格式文件
docx2Html(file);
}, [file]);

/**
* @description: docx 文件转 html
* @param {*} file: docx 格式文件
* @return {*}
*/
const docx2Html = file => {
if (!file) {
return;
}
// 只处理 docx 文件
const suffix = file.name?.substr(file.name.lastIndexOf('.') + 1).toLowerCase();
if (suffix !== 'docx') {
return;
}
// 生成 html 后挂载的 dom 节点
const htmlContentDom = document.querySelector('#htmlContent');
const docxOptions = Object.assign(docx.defaultOptions, {
debug: true,
experimental: true,
});
docx.renderAsync(file, htmlContentDom, null, docxOptions).then(() => {
console.log('docx 转 html 完成');
});
};

return <div id='htmlContent' />;
};

此时,在 idhtmlContent 的节点下,就可以看到转换后的 html 内容了( htmlContent 节点的宽高等 css 样式自行添加)




  1. html 转成 canvas


    安装 html2canvas 依赖: pnpm add html2canvas --save




jsx
复制代码
import html2canvas from 'html2canvas';

/**
* @description: dom 元素转为图片
* @return {*}
*/
const handleDom2Img = async () => {
// 生成 html 后挂载的 dom 节点
const htmlContentDom = document.querySelector('#htmlContent');
// 获取刚刚生成的 dom 元素
const htmlContent = htmlContentDom.querySelectorAll('.docx-wrapper>section')[0];
// 创建 canvas 元素
const canvasDom = document.createElement('canvas');
// 获取 dom 宽高
const w = parseInt(window.getComputedStyle(htmlContent).width, 10);
// const h = parseInt(window.getComputedStyle(htmlContent).height, 10);

// 设定 canvas 元素属性宽高为 DOM 节点宽高 * 像素比
const scale = window.devicePixelRatio; // 缩放比例
canvasDom.width = w * scale; // 取文档宽度
canvasDom.height = w * scale; // 缩略图是正方形,所以高度跟宽度保持一致

// 按比例增加分辨率,将绘制内容放大对应比例
const canvas = await html2canvas(htmlContent, {
canvas: canvasDom,
scale,
useCORS: true,
});
return canvas;
};


  1. 将生成好的 canvas对象转成 .png 文件,并下载


jsx
复制代码
// 将 canvas 转为 base64 图片
const base64Str = canvas.toDataURL();

// 下载图片
const imgName = `图片_${new Date().valueOf()}`;
const aElement = document.createElement('a');
aElement.href = base64Str;
aElement.download = `${imgName}.png`;
document.body.appendChild(aElement);
aElement.click();
document.body.removeChild(aElement);
window.URL.revokeObjectURL(base64Str);

五、总结


前端无法直接实现将 .docx 文档转成图片格式,所以要先将 .docx 文档转换成 html 格式,并插入页面文档节点中,然后根据 html 内容生成canvas对象,最后将 canvas对象转成 .png 文件


有以下两个缺点:



  1. 只能转 .docx 格式的 word 文档,暂不支持 .doc 格式;

  2. 无法自动获取文档第一页来生成图片内容,需要先将 word 所有页面生成为 html,再通过 canvas 手动裁切,来确定图片宽高。

作者:Tim_
链接:https://juejin.cn/post/7331799381896151067
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

低成本创建数字孪生场景-开发篇

web
介绍 本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。 CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建...
继续阅读 »

介绍


本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。


CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建复杂的3D地形和城市模型。CesiumJS的功能强大,但入门难度比较高,需要提前了解很多概念和设计理念,为方便理解本案例仅仅使用其提供的一些基础功能。


Guanlianx_5.gif


需求说明


为了搭建1个简易的山区小乡镇场景,我们首先梳理下需求,把任务分解好。



  1. 在底图上叠加各种图层

    • 支持叠加地形图层、3DTiles图层、数据图层

    • 支持多种方式分发图层数据



  2. 鼠标与图层元素的交互

    • 鼠标移动时,使用屏幕事件处理器监听事件,获取当前屏幕坐标

    • 如果已经有高亮的元素,将其恢复为正常状态

    • 以当前屏幕坐标为起点发送射线,获取射线命中的元素,如果有命中的元素就高亮它

    • 鼠标点击时使用屏幕事件处理器获取命中元素,如果命中了,就判断元素是否描边状态,有则取消描边,没有则增加描边轮廓



  3. 加载Gltf等其他模型

    • 模型与其他图层元素一样,可以被光标拾取

    • 模型支持播放自带动画




准备工作


数据分发服务


当前案例涉及的图层数据以文件类为主,为方便多处使用,需要将图层的服务独立部署,这里有两个方案:



  1. 自行搭建静态文件服务器,网上搜一个常用的node.js静态服务脚本即可

  2. 把文件放到cesium ion上,如果你要使用cesium ion资产,需要注意配好defaultAccessToken,具体调用方式看下文的代码实现


安装依赖


以下为本案例的前端工程使用的核心框架版本


依赖版本
vue^3.2.37
vite^2.9.14
Cesium^1.112.0

代码实现



  1. 地图基本场景,本示例使用vite+vue3开发,html非常简单,只需要一个

    标签即可,在cesiumjs中以Viewer为起点调用其他控件,因此我们实例化一个Cesium.viewer, 这里面有非常多配置参数,详细请看开发文档


    import * as Cesium from 'cesium'
    import 'cesium/Build/Cesium/Widgets/widgets.css'

    Cesium.Ion.defaultAccessToken = '可以把一些GIS资产放到Cesium ION上托管,Tokenw为调用凭证'

    // 地图中心
    const center = [1150, 29]

    // cesium实例
    let viewer = null

    // 容器
    const cesiumContainer = ref(null)

    onMounted(async () => {
    await init()
    })

    async function init() {
    viewer = new Cesium.Viewer(cesiumContainer.value, {
    timeline: true, //显示时间轴
    animation: true, //开启动画
    sceneModePicker: true, //场景内容可点击
    baseLayerPicker: true, //图层可点击
    infoBox: false, // 自动信息弹窗
    shouldAnimate: true // 允许播放动画
    })
    // 初始化镜头视角
    restoreCameraView()

    // 开启地形深度检测
    viewer.scene.globe.depthTestAgainstTerrain = true
    // 开启全局光照
    viewer.scene.globe.enableLighting = true
    // 开启阴影
    viewer.shadows = true

    })

    // 设置初始镜头
    function restoreCameraView(){
    viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(center[0], center[1], 0),
    orientation: {
    heading: Cesium.Math.toRadians(0), // 相机的方向
    pitch: Cesium.Math.toRadians(-90), // 相机的俯仰角度
    roll: 0 // 相机的滚动角度
    }
    })
    }

    // 加载地形图层
    async function initTerrainLayer() {
    const tileset = await Cesium.CesiumTerrainProvider.fromUrl(
    'http://localhost:9003/terrain/c8Wcm59W/',
    {
    requestWaterMask: true,
    requestVertexNormals: false
    }
    )
    viewer.terrainProvider = tileset
    }


  2. 在地图上叠加地形图层,图层数据可以自行部署


    // 方法1: 加载本地地形图层
    async function initTerrainLayer() {
    const tileset = await Cesium.CesiumTerrainProvider.fromUrl(
    'http://localhost:9003/terrain/c8Wcm59W/',
    {
    requestWaterMask: true,
    requestVertexNormals: false
    }
    )
    viewer.terrainProvider = tileset
    }

    // 方法2: 加载Ion地形图层
    async function initTerrainLayer() {
    const tileset = await Cesium.CesiumTerrainProvider.fromIonAssetId(1,{
    requestVertexNormals: true
    }
    )
    viewer.terrainProvider = tileset
    }


  3. 加载3DTiles图层,与地形图层类似,换成了Cesium3DTileset类。需要注意使用url加载需要自行解决跨域问题


    const tileset = await Cesium.Cesium3DTileset.fromUrl(
    'http://localhost:9003/model/tHuVnsJXZ/tileset.json',
    {}
    )
    // 将图层加入到场景
    viewer.scene.primitives.add(tileset)

    // 适当调整图层位置
    const translation = getTransformMatrix(tileset, { x: 0, y: 0, z: 86 })
    tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation)

    // 获取变化矩阵
    function getTransformMatrix (tileset, { x, y, z }) {
    // 高度偏差,正数为向上偏,负数为向下偏,根据真实的模型位置不断进行调整
    const heightOffset = z
    // 计算tileset的绑定范围
    const boundingSphere = tileset.boundingSphere
    // 计算中心点位置
    const cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center)
    // 计算中心点位置坐标
    const surface = Cesium.Cartesian3.fromRadians(cartographic.longitude,
    cartographic.latitude, 0)
    // 偏移后的三维坐标
    const offset = Cesium.Cartesian3.fromRadians(cartographic.longitude + x,
    cartographic.latitude + y, heightOffset)

    return Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3())
    }


  4. 鼠标事件交互,鼠标悬浮,在改变选中元素的状态之前,需要将它的当前状态保存下来以便下次可以恢复。


    // 缓存高亮状态
    const highlighted = {
    feature: undefined,
    originalColor: new Cesium.Color()
    }

    // 鼠标与物体交互事件
    function initMouseInteract () {
    // 事件处理器
    const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

    // 鼠标悬浮选中
    handler.setInputAction((event) => {
    // 将原有高亮对象恢复
    if (Cesium.defined(highlighted.feature)) {
    highlighted.feature.color = highlighted.originalColor
    highlighted.feature = undefined
    }
    // 获取选中对象
    const pickedFeature = viewer.scene.pick(event.endPosition)

    if (Cesium.defined(pickedFeature)) {
    // 高亮选中对象
    if (pickedFeature !== moveSelected.feature) {
    highlighted.feature = pickedFeature
    Cesium.Color.clone(pickedFeature.color, highlighted.originalColor)
    pickedFeature.color = Cesium.Color.YELLOW
    }
    }
    }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)


  5. 鼠标事件,鼠标点击,描边轮廓使用了Cesium自带的后期效果处理器,不需要自行编写着色器等操作,因此实现起来很便捷。只需要将选中的元素放到 效果的selected对象数组内就行了。


    // 缓存后期效果
    let edgeEffect = null

    function initMouseInteract(){
    // 鼠标点击选中
    handler.setInputAction((event) => {

    // 获取选中对象
    const pickedFeature = viewer.scene.pick(event.position)

    if (!Cesium.defined(pickedFeature)) {
    return null
    } else {

    // 描边效果:兼容GLTF和3DTiles
    setEdgeEffect(pickedFeature.primitive || pickedFeature)

    // 如果拾取的要素包含属性信息,则打印出来
    if (Cesium.defined(pickedFeature.getPropertyIds)) {
    const propertyNames = pickedFeature.getPropertyIds()
    const props = propertyNames.map(key => {
    return {
    name: key,
    value: pickedFeature.getProperty(key)
    }
    })
    console.info(props)
    }
    }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
    }

    // 选中描边
    function setEdgeEffect (feature) {
    if (edgeEffect == null) {
    // 后期效果
    const postProcessStages = viewer.scene.postProcessStages

    // 增加轮廓线
    const stage = Cesium.PostProcessStageLibrary.createEdgeDetectionStage()
    stage.uniforms.color = Cesium.Color.LIME //描边颜色
    stage.uniforms.length = 0.05 // 产生描边的阀值
    stage.selected = [] // 用于放置对元素

    // 将描边效果放到场景后期效果中
    const silhouette = Cesium.PostProcessStageLibrary.createSilhouetteStage([stage])
    postProcessStages.add(silhouette)

    edgeEffect = stage
    }

    // 选多个元素进行描边
    const matchIndex = edgeEffect.selected.findIndex(v => v._batchId === feature._batchId)
    if (matchIndex > -1) {
    edgeEffect.selected.splice(matchIndex, 1)
    } else {
    edgeEffect.selected.push(feature)
    }

    }


  6. 加载gltf模型, gltf加载后需要进行一次矩阵变换modelMatrix, 加载后启动指定索引的动画进行播放。


    // 加载模型
    async function loadGLTF () {

    let animations = null

    let modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(
    Cesium.Cartesian3.fromDegrees(lng,lat,altitude)
    )

    const model = await Cesium.Model.fromGltfAsync({
    url: './static/gltf/windmill.glb',
    modelMatrix: modelMatrix,
    scale: 30,
    // minimumPixelSize: 128, // 设定模型最小显示尺寸
    gltfCallback: (gltf) => {
    animations = gltf.animations
    }
    })

    model.readyEvent.addEventListener(() => {
    const ani = model.activeAnimations.add({
    index: animations.length - 1, // 播放第几个动画
    loop: Cesium.ModelAnimationLoop.REPEAT, //循环播放
    multiplier: 1.0 //播放速度
    })
    ani.start.addEventListener(function (model, animation) {
    console.log(`动画开始: ${animation.name}`)
    })
    })

    viewer.scene.primitives.add(model)
    }



部署说明



  1. 场景演示包括前端工程、GIS数据分发服务、服务端接口几个部分

  2. 前端工程使用vue3开发,其中CesiumJs通过NPM依赖包引入

  3. 场景中相关图层均为静态文件,可放入主工程静态目录中,也可以独立部署(需解决跨域访问),或者使用cesiumlab3分发服务便于管理

  4. web端场景对终端设备和浏览器有一定要求,具体配置需要进一步测试


总结


在本文中并没有涉及到服务端数据的接入,数据接入进来后,我们可以利用Cesium在GIS开发领域强大功能,与three.js的webGL开发优势,两者相互融合创建更多数据可视化效果。那么关于Cesium和three.js的融合开发还在初步探索阶段,希望下一次有精彩内容分享给大家。


Hengjiang3.gif


相关链接


最新版cesium集成threejs


Cesium和Three.js结合的5个方案


Cesium实现更实用的3D描边效果


作者:gyratesky
来源:juejin.cn/post/7331626882552872986
收起阅读 »

前端将dom转换成图片

web
一、问题描述 在工作的过程中会遇到要将dom变成图片并下载的问题,刚开始使用的html2canvas进行转换的,但是我老大告诉我说,咱们做的是面向国外的网页,插件太大会导致页面加载慢的问题(国外部分地区网络没有国内这么发达),就让我用原生dom或一些比较小的插...
继续阅读 »

一、问题描述


在工作的过程中会遇到要将dom变成图片并下载的问题,刚开始使用的html2canvas进行转换的,但是我老大告诉我说,咱们做的是面向国外的网页,插件太大会导致页面加载慢的问题(国外部分地区网络没有国内这么发达),就让我用原生dom或一些比较小的插件,在原生dom下载的时候遇到了context.drawImage(element, 0, 0, width, height)这一方法传入参数要传类型HTMLCanvasElement的问题,所以要将一个HTMLElement转换成HTMLCanvasElement,但是经过一些信息的查找,我发现有个很好用且轻量化的插件,可以完美解决这一问题,所以这里给大家推荐一个轻量级的插件dom-to-image(23kb),这个插件可以不用进行类型转换,直接将dom元素转换成需要的文件格式。


二、dom-to-image的使用


2.1 dom-to-image的安装


在终端输入以下代码进行dom-to-image安装



npm install dom-to-image



2.2 dom-to-image引入


2.2.1 vue项目引入


在需要使用这个插件的页面使用以下代码进行局部引入


import domToImage from 'dom-to-image';

然后就可以通过以下代码进行图片的转换了


const palGradientGap = document.getElementById('element')
const canvas = document.createElement('canvas')
canvas.width = element.offsetWidth
canvas.height = element.offsetHeight
this.domtoimage.toPng(element).then(function (canvas) {
const link = document.createElement('a')
link.href = canvas
link.download = 'image.png' // 下载文件的名称
link.click()
})

当然也可以进行全局引入
创建一个domToImage.js文件写入以下代码


import Vue from 'vue'; 
import domToImage from 'dom-to-image';
const domToImagePlugin = {
install(Vue) {
Vue.prototype.$domToImage = domToImage;
}
};
Vue.use(domToImagePlugin);

然后再入口文件main.js写入以下代码全局引入插件


import Vue from 'vue'
import App from './App.vue'
import './domToImage.js'; // 引入全局插件
Vue.config.productionTip = false
new Vue({ render: h => h(App), }).$mount('#app')

三、dom-to-image相关方法



  1. toSvg(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 SVG 图片,并返回一个 Promise 对象。


    参数说明:



    • node:要转换为图片的 DOM 元素。

    • options:可选参数对象,用于配置转换选项。



  2. toPng(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 PNG 图片,并返回一个 Promise 对象。


    参数说明:



    • node:要转换为图片的 DOM 元素。

    • options:可选参数对象,用于配置转换选项。



  3. toJpeg(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 JPEG 图片,并返回一个 Promise 对象。


    参数说明:



    • node:要转换为图片的 DOM 元素。

    • options:可选参数对象,用于配置转换选项。



  4. toBlob(node: Node, options?: Options): Promise<Blob>:将 DOM 元素转换为 Blob 对象,并返回一个 Promise 对象。


    参数说明:



    • node:要转换为图片的 DOM 元素。

    • options:可选参数对象,用于配置转换选项。



  5. toPixelData(node: Node, options?: Options): Promise<Uint8ClampedArray>:将 DOM 元素转换为像素数据,并返回一个 Promise 对象。


    参数说明:



    • node:要转换为图片的 DOM 元素。

    • options:可选参数对象,用于配置转换选项。



  6. toCanvas(node: Node, options?: Options): Promise<HTMLCanvasElement>:将 DOM 元素转换为 Canvas 对象,并返回一个 Promise 对象。


    参数说明:



    • node:要转换为图片的 DOM 元素。

    • options:可选参数对象,用于配置转换选项。




其中,Options 参数是一个可选的配置对象,用于设置转换选项。以下是一些常用的选项:



  • width:输出图像的宽度,默认值为元素的实际宽度。

  • height:输出图像的高度,默认值为元素的实际高度。

  • style:要应用于元素的样式对象。

  • filter:要应用于元素的 CSS 滤镜。

  • bgcolor:输出图像的背景颜色,默认值为透明。

  • quality:输出图像的质量,仅适用于 JPEG 格式,默认值为 0.92。


作者:crazy三笠
来源:juejin.cn/post/7331626882553937946
收起阅读 »

JS 前端框架的新年预言

web
免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 2024 Predictions by JavaScript Frontend Framework Maintainers。 本期共享的是 —— 来自 React/Next...
继续阅读 »

免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 2024 Predictions by JavaScript Frontend Framework Maintainers



本期共享的是 —— 来自 React/Next.js/Angular/Solid 的维护者和创建者科普了它们计划在新年里框架改进的未来规划。


fe-2024.png


React:新年预览


Meta(前脸书)的 React 工程经理 E.W. 表示,React 团队预计在新的一年里会有更多的框架采用 RSC(React 服务服务端组件)。


“对于大多数人而言,RSC 已经对其所了解的 React 作用域产生了重大变化,从只是一个 UI 层,到对构建 App 的方式产生更大的影响,以享受最佳的用户体验和开发体验,尤其以前对于 SPA(单页应用程序)还不够好,”E.W. 如是说。


虽然它没有具体爆料来年的任何新进展,但 E.W. 确实表示它们会发布并共享某些去年开始可公开的进展。举个栗子,在 React Advanced 上,该团队向与会者展示了 React Forget,这是 React 的自动记忆编译器。E.W. 表示,React Forget 意味着,开发者不再需要使用 useMemo/useCallback


“在 React Native EU 上,我们表示,从 0.73 版本开始,我们会把 Web 开发者熟悉的 Chrome 开发工具移植到 React Native 中,”E.W. 补充道。“我们还共享了我们对 Static Hermes 的研究,它是我们的 JS 原生编译器,它不仅有可能加快 React Native App 的速度,还能从根本上改变 JS 的有效用途。”


Next.js:正在运行的新编译器


Next.js 推出了一款新的 App 服务器,旨在支持去年的 RSC(React 服务端组件)和 SA(服务端操作)。Vercel 的产品主管 L.R. 表示,它会继续支持旧版的 App 服务器,并且它们的路由系统是可互换的。这种互操作性意味着,开发者可以把时间花在添加新功能上。


“有些客户已经使用 Next.js 开发了五六年,它们采用这些新功能也需要多年时间,”L.R. 讲道。“我们希望让大家尽可能顺利地度过这段旅程。”


新的一年里,Next.js 想要解决一大坨问题,但其中一个优先事项可能是简化缓存。它说,就开发体验而言,这可能会更容易。


“通常情况下,生态系统中的一大坨开发者必须引入一大坨额外软件包,或学习如何使用其他工具来请求、缓存和重新验证,”L.R. 说。“Next.js 现在已经内置了一大坨十分给力的同款功能,但这也意味着,大家需要学习其他东西,目前用户初步的反馈是,‘这很棒棒哒;它十分给力,但如果能更简单一点的话,我们会不吝赞词。’”


Next.js 团队还会继续关注性能优化,它称之为“我们的持续投资”。


它补充道,这可能会在新年里以新编译器的形式出现,这将加快在开发者的机器上启动 Next.js 的速度。该编译器已经投入使用了大约一年,Vercel 一直在内部将其用于其产品和 App。它说,由 Rust 提供支持的编译器,在无缓存的情况下比以前有缓存的编译器更快。


L.R. 说:“我们推出该功能指日可待,大家都可以默认启动它,而且它比现存的 Webpack 编译解决方案更快。” “开发者希望它们的工具更快。它们永远不会抱怨它变得更快。因此,很有趣的是,可以看到工具作者,而不是工具的用户,而是实际的工具开发者转向 Rust 等较低阶的工具,帮助斩获咫尺之间的性能优势。”


目标三是继续为 Next.js 的未来 10 年奠定基础。


“你知道的,这个新的路由系统显然让我们十分鸡冻。我们相信这是未来的基础,”它说。“但这也需要时间。大家会尝试,用户会提出功能请求,它们会希望看到事情发生改变。我们认为这是未来五到十年的一项非常长期的投资。”


它补充说,“有一天”但可能不是今年的目标是,寻求一种更棒的方案来处理 Next.js 内部的内容。


“今天,它能奏效,我们仍然可以连接到想要的任何内容源,但存在某些方案可以简化开发体验,”它补充道。“与其说这是一项要求,不如说是一种美好的享受,这就是为什么私以为我们无法在新年实现此目标,但我想在未来用它搞点事情。”


Angular:可选的 Zone.js


谷歌 Angular DevRel 技术主管兼经理 M.G. 表示,在过去的一年里,Angular 的两大成就是:



  • 引入了 Signal(信号)的细粒度响应性

  • 引入了可延迟视图


它讲道,明年会在此基础上,进一步关注细粒度响应性,并使 Zone.js 成为可选选项。


在 Angular 中,Zone 是跨异步任务持续存在的执行上下文。Zone 的 GitHub 仓库对此进行了详细解释,但 Zone 有五大职责,包括但不限于拦截异步任务调度和包装错误处理的回调,以及跨异步操作的 Zone 追踪。Zone.js 可以创建跨异步操作持久存在的上下文,并为异步操作提供生命周期钩子。


“我们正在探索为现存项目启用可选的 Zone.js,开发者应该可以通过重构现存 App 来利用该功能,”M.G. 如是说。“诉诸可选的 Zone.js,我们期望优化加载时间,并提升初始渲染速度。研究细粒度响应性将其提升到另一个水平,使我们能够只检测组件模板的局部变化。”


它说,这些功能将带来更快的运行时间。


在另一个性能游戏中,Angular 正在考虑是否默认启用混合渲染。它补充说,可以选择退出混合渲染,因为它会增加托管要求和成本。


“我们瞄到了 SSG(静态站点生成)和 SSR(服务端渲染)的巨大价值,凭借我们在 v17 中奠定的坚硬基建,我们正在努力进行最后的润色,以便从一开始就实现这种体验,”M.G. 如是说。


它补充道,另一个优先事项是落实 Signal 的征求意见。


开发者还可能会见证 Angular 文档的改进。根据其开发者调查,开发者希望享受进阶的学习体验,其中一部分包括使 Angular.dev 成为 Angular 的全新官网主页。它补充道,开发者还优先考虑了初始加载时间(混合渲染、部分水合和可选的 Zone.js 部分应该解决此问题),以及组件创作(Angular 计划进一步简化组件创作)。


“我们致力于可持续迭代功能,并与时俱进地渐进增强它们,”M.G. 讲道。“开发者将能够从新年里的所有优化中受益,并将在接下来的几年中享受更好的开发体验和性能。”


Solid:聚焦原语


“Solid 之父”R.C. 表示,Solid 开发者可以关注新年的 SolidStart 1.0 和 Solid.js 2.0。SolidStart 是一个元框架,这意味着,它构建于 Solid.js 框架之上。它说,它相相当于 Svelte 的 SvelteKit。


SolidStart 的官网文档是这样解释的:


“Web App 通常包含一大坨组件:数据库、服务器、前端、打包器、数据请求/变更、缓存和基建。编排这些组件极具挑战性,并且通常需要跨 App 堆栈大量共享状态和冗余逻辑。进入 SolidStart:一种元框架,它提供了将所有这些组件万法归一的平台。”


由于 SolidStart 仍处于测试阶段,R.C. 基本上有机会使用生态系统中已有的内容来使其变得更好。


“其中一个重要的部分是,我们现在不再编写自己的部署适配器,而是使用 Nitro,它也为 Nuxt 框架提供支持,这让我们可以部署到所有不同的平台,”R.C. 讲道。


另一个例子是,任何 Solid 路由器都可以在 SolidStart 中奏效。


“这意味着,对路由器的底层部分大量更新,这样它们能够“梦幻联动”,但我非常满意的最终结果是,我们的志愿者小团队需要维护的代码更少了,而且它为开发者提供了很大的灵活性和控制力,”它说。“它们不会被迫采用单一的解决方案,这对我而言兹事体大,因为每个人都有自己的需求。正如我所言,如果您构建正确的基建,并弄清楚这些构建模块是什么,大家可以做更多的事情。”


它说,最终的结果是一个具有“可交换”部分的元框架,而且不太我行我素。在越来越多的元框架决定开发者技术方案的世界中,Solid 团队一直在思考正确的原语片段的影响。


“于我而言,它始终是关于构建基元块,这是一个非常工程化的焦点,我认为这是它与众不同的部分原因,”它说。“我一直喜欢提供选择,而且私以为如果我们有正确的原语、正确的片段,我们就可以构建正确的解决方案。”


它表示,Solid 2.0 应该会在新年中后期的某个时间点发布。它说,目前它们正在设计如何处理异步系统的原型。


“Solid 2.0 也将是重量级版本,因为我们正在重新审视响应式系统,并研究如何解决异步 Signal 或异步系统,”R.C. 讲道。


它补充道,Solid 试图平衡控制与性能。


“我们的社区中有一大坨热心人,它们非常有技术头脑,既关心性能,也关心控制,”它说。“我们确实吸引了一大坨自己真正想要控制构建的方方面面的用户。”


作者:人猫神话
来源:juejin.cn/post/7331925629082566707
收起阅读 »

浏览器沙盒你知多少😍

web
开题话: 😍随着业务环境的快速变化,安全性是开发人员和测试人员在现代 Web 开发周期中面临的最大挑战之一。构建和部署现代 Web 应用程序的复杂性会导致更多的安全漏洞。根据 IBM 和 Ponemon Institute 的数据泄露成本报告,2021 年,数...
继续阅读 »

开题话:


😍随着业务环境的快速变化,安全性是开发人员和测试人员在现代 Web 开发周期中面临的最大挑战之一。构建和部署现代 Web 应用程序的复杂性会导致更多的安全漏洞。根据 IBM 和 Ponemon Institute 的数据泄露成本报告,2021 年,数据泄露成本从 3 万美元(86 年的平均成本)上升到 2019 万美元,啧啧啧,这是该报告 4 年来的最高平均成本


因此,网络安全在软件开发生命周期中变得越来越重要,以确保用户数据安全和隐私。如果你可以开发和测试网站和 Web 应用程序而不必担心安全漏洞,那不是很好吗?👏沙盒是一种可以帮助你实现此目的的技术。沙盒是一种安全隔离应用程序、Web 浏览器和一段代码的方法。它可以防止恶意或有故障的应用程序攻击或监视你的 Web 资源和本地系统。


举个栗子➡, 在现实世界中,沙盒是被墙壁包围的儿童游乐区。它允许孩子们玩沙子,而草坪周围没有沙子。同样,沙盒浏览器创建了一个隔离的环境,用户可以在其中从第三方来源下载和安装应用程序,并在安全、隔离的环境中操作它们,即使他们行为可疑。因此,沙盒浏览器可以保护你的计算机免受额外的安全风险。


下面我们说说什么是浏览器沙盒吧!😘


本文将探讨什么是浏览器沙盒、不同类型的沙盒的优点和重要性,以及如何实现沙盒。


一、什么是浏览器沙盒?


为了防止系统或 Web 应用程序中出现安全漏洞,开发人员需要弄清楚如何处理它们。这是浏览器沙盒派上用场的时候。浏览器沙箱提供了一个安全的虚拟环境来测试有害代码或运行第三方软件,而不会损害系统的数据或本地文件。


例如,如果你在沙盒中下载恶意附件,它不会损坏系统的现有文件或资源。沙盒具有同源功能,它允许JavaScript在网页上添加或自定义元素,同时限制对外部JSON文件的访问。


今天,流行的网络浏览器,如Chrome,Firefox和Edge,都带有内置的沙箱。沙盒浏览器的最终目标是保护你的机器免受与浏览相关的风险。因此,如果用户从网站下载恶意软件,该软件将下载到浏览器的沙箱中。关闭沙箱时,其中的所有内容(包括有害代码)都会被清除。


浏览器沙盒使用两种隔离技术来保护用户的 Web 浏览活动和系统硬件、本地 PC 和网络:



  • 本地浏览器隔离

  • 远程浏览器隔离


本地浏览器隔离


本地浏览器隔离是一种传统的浏览器隔离技术,它在沙盒中运行虚拟浏览器或在用户的本地基础结构上运行虚拟机。它有助于将数据与外部安全威胁和不安全浏览隔离开来。例如,如果恶意元素潜入,影响将仅限于沙盒浏览器和虚拟机。


远程浏览器隔离


远程浏览器隔离涉及一种虚拟化技术,其中浏览器在基于云的服务器(公共云和私有云)上运行。在远程隔离中,用户的本地系统没有浏览活动,浏览器沙盒、过滤和风险评估在远程服务器上进行。


远程浏览器隔离涉及两种隔离用户本地基础结构和 Web 内容的方法:



  1. DOM 镜像:在这种技术中,浏览器并不完全与用户的本地系统隔离。但是,DOM 镜像技术会过滤恶意内容,并将其余内容呈现给用户。

  2. 可视化流式处理:此技术提供完全的远程浏览器隔离。可视化流式处理的工作方式类似于 VDI(虚拟桌面基础结构)系统,其中浏览器在基于云的服务器上运行,并将视觉输出显示到用户的本地计算机。


二、为什么浏览器沙盒很重要?


现代 Web 技术正在迅速扩展,从而使用户能够顺利开发和发布网站和 Web 应用程序。与此同时,对Web应用程序的需求也在以前所未有的速度增长。根据Imperva的一项调查,Web应用程序是50%数据泄露的来源。因此,拥有一个安全、受控的环境(如沙盒浏览器)至关重要,以便在不危及本地基础设施和系统资源的情况下执行操作。


例如,用户在沙盒中运行 Web 浏览器。如果恶意代码或文件利用 Web 浏览器漏洞,则沙盒中的影响受到限制。此外,引爆程序可以帮助发现新的漏洞并在 Web 浏览器中缓解它们。但是,如果禁用沙盒浏览器,恶意程序可以利用 Web 浏览器漏洞并损坏用户的本地系统和资源。


三、沙盒的好处


将沙盒合并到 Web 开发工作流中有很多优点。下面提到了一些优点😍😍:



  • 沙盒使设备和操作系统免于面临潜在威胁。

  • 与未经授权的一方或供应商合作时,最好使用沙盒环境。在部署内容之前,你可以使用沙盒来测试可疑代码或软件。

  • 沙盒可以帮助防止零日攻击。由于开发人员无法发现漏洞的即时补丁,因此零日攻击本质上是有害的。因此,沙盒通过向系统隐藏恶意软件来减轻损害。

  • 沙盒环境隔离威胁和病毒。这有助于网络专家研究和分析威胁趋势。它可以防止未来的入侵和识别网络漏洞。

  • 沙盒应用程序是一种混合解决方案,这意味着它们可以在本地和远程部署(基于云的服务器)。混合系统比传统解决方案更安全、更可靠、更具成本效益。

  • 沙盒和 RDP(远程桌面协议)设置可帮助企业确保安全的外部网络连接。

  • 沙盒可以与防病毒或其他安全工具和策略结合使用,以增强整个安全生态系统。


四、哪些应用正在沙盒化😎?


我们在日常工作流程中使用的大部分资产(如在线浏览器、网页、PDF、移动应用程序和 Windows 应用程序)都是沙盒化的。


下面列出了正在沙盒化的应用:



  • Web 浏览器:可能易受攻击的浏览器在沙盒环境中运行。

  • 浏览器插件:加载内容时,浏览器插件在沙盒中运行。沙盒浏览器插件(如 Java)更容易受到攻击。

  • 网页:浏览器以沙盒模式加载网页。由于网页是内置的 JavaScript,因此它无法访问本地计算机上的文件。

  • 移动应用:与 Android 和 iOS 一样,移动操作系统在沙盒模式下运行其应用。如果他们希望访问你的位置、联系人或其他信息,他们会弹出权限框。

  • Windows 软件和程序:在对系统文件进行更改之前,Windows 操作系统中的用户帐户控制 (UAC) 会请求你的许可。UAC 的功能类似于沙盒,但它不提供完整的保护。但是,不应禁用它。


五、不同类型的沙盒


在浏览器沙盒的这一部分中,我们将讨论不同类型的沙盒。沙盒分为三类:



  1. 应用程序沙盒

  2. 浏览器沙盒

  3. 安全沙盒


应用程序沙盒


使用应用程序沙箱,你可以在沙盒中运行不受信任的应用程序,以防止它们损坏本地系统或窃取数据。它有助于创建一个安全的环境,使应用程序可以在其中运行而不会损坏系统。通过将应用与用户的本地计算机隔离,应用程序沙盒增强了应用的完整性。


浏览器沙盒


可以在沙盒中执行基于浏览器的潜在恶意应用程序,以防止它们对你的本地基础架构造成损害。它导致建立一个安全的环境,在该环境中,Web 应用程序可以在不影响系统的情况下运行。引爆技术可以帮助发现 Web 浏览器中的新漏洞并缓解其。


安全沙盒


安全沙盒允许你探索和检测可疑代码。它扫描附件并识别潜在有害网站的列表,并确定是否下载或安装受感染的文件。


六、使用内置沙盒浏览器进行沙盒分析


沙盒预装在流行的浏览器中,如Chromium,Firefox和Edge,以保护你的系统免受浏览漏洞的影响。让我们看看沙盒在不同浏览器中的工作原理:


Chromium浏览器沙盒


Google Chrome和Microsoft Edge建立在Chromium浏览器上。代理和目标是构成 Chromium 浏览器沙箱的两个进程。目标进程是子进程,而浏览器进程是代理进程。目标进程的代码在沙盒环境中执行。代理进程在子进程和硬件资源之间操作,为子进程提供资源。


火狐浏览器沙盒


为了保护本地系统免受威胁,Firefox 在沙箱中执行不受信任的代码。Firefox 浏览器是使用父进程和子进程沙盒化的。浏览时,潜在的恶意程序会在沙盒中运行。在沙盒期间,父进程是子进程与其余系统资源之间的中介。


你可以更改 Firefox 浏览器中的沙盒程度,使其限制最少、中等或高度严格:



  • 级别 0:限制最少

  • 1级:中等

  • 第 2 级:高度限制


要检查 Firefox 沙盒浏览器的级别,请在地址栏中传递以下命令:


arduino
复制代码
about:config

在页面上,它将加载 Firefox 可配置变量。现在,在配置页面上点击“CTRL + F”,在搜索框中输入以下命令,然后按“Enter”。


image.png


image.png


Edge浏览器沙盒


当启动 Edge 沙盒浏览器 Windows 10 时,你将看到一个全新的桌面,该桌面仅具有“回收站”和 Edge 快捷方式。它显示“开始菜单”和其他图标,但它们在此沙盒环境中不起作用。你可以在标准Windows 10上访问它们,而不是沙盒Windows 10。


关闭 Edge 浏览器沙盒后,你的浏览器历史记录将不再可用。你的 ISP 可能会跟踪沙盒中的操作,但此数据不可审核。


七、禁用谷歌浏览器沙盒


在执行基于 Chrome 的沙盒测试时,你可能会遇到这样一种情况,即沙盒功能可能会导致 Chrome 浏览器闪烁以下错误:“应用程序初始化失败”。


在这种情况下,你可能需要停用 Chrome 浏览器沙盒。以下是以下步骤:



  1. 如果你没有 Google Chrome 沙盒快捷方式,请创建一个。

  2. 右键单击快捷方式,然后选择“属性”。选择属性

  3. 在目标中提供的应用路径中输入以下命令:



css
复制代码
--no-sandbox

属性 2



  1. 单击“应用”,然后单击“确定”。


八、浏览器沙盒:它是100%安全的吗💢?


大多数 Web 浏览器都使用沙盒。但是,互联网仍然是病毒和其他恶意软件的来源。沙盒的级别似乎有所不同。不同的 Web 浏览器以不同的方式实现沙盒,因此很难弄清楚它们是如何工作的。但是,这并不意味着所有网络浏览器都是不安全的。另一方面,浏览器沙箱可以使它们更安全。


但是,如果你问它是否提供100%的安全性,答案是否定的。如果某些浏览器组件使用 Flash 和 ActiveX,则它们可能会延伸到沙箱之外。


九、写在最后


企业受到高级持续性威胁 (APT) 的攻击,沙盒可以保护它们。通过查看前方的情况,你可以为未知攻击做好准备。你可以在隔离的环境中测试和开发应用程序,而不会因沙盒而损害本地系统资产。Sandboxie,BitBox和其他沙盒工具在市场上可用。但是,在沙盒中设置和安装不同的浏览器需要时间。


下回再说说前端——浏览器的安全性吧,本文就说到这里了😘


感谢jym浏览本文🤞,若有更好的建议,欢迎评论区讨论哈🌹。


如果jym觉得这篇干货很有用,加个关关🥰,点个赞赞🥰,后面我会继续卷,分享更多干货!感谢支持!😍


作者:珑墨
链接:https://juejin.cn/post/7241492186917847077
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

尾递归优化是一场谎言

web
TLDR 本文是对蹦床函数的应用案例。 蹦床函数跟尾递归优化都是为了解决递归次数过多导致调用栈溢出的问题。 蹦床函数的原理:令原函数返回一个可执行函数,由蹦床函数来控制执行时机,使执行函数与其父函数无执行调用关系。 处理递归栈溢出还有递归转迭代、异步执行等方...
继续阅读 »

TLDR



  • 本文是对蹦床函数的应用案例。

  • 蹦床函数跟尾递归优化都是为了解决递归次数过多导致调用栈溢出的问题。

  • 蹦床函数的原理:令原函数返回一个可执行函数,由蹦床函数来控制执行时机,使执行函数与其父函数无执行调用关系。

  • 处理递归栈溢出还有递归转迭代、异步执行等方案,但蹦床函数对代码的改动量很低,也不影响原代码的阅读和执行逻辑。


起因


接到一个需求,需要对 markdown 进行分句。大致如下:


// 原始 md
aaa。bbb[^1^]。ccc。

// 转换后 md
aaa。x:[bbb[^1^]]。ccc。

即把以[^*^]结尾的句子给包起来。


方案思路比较简单:



  • 写一个函数,入参为起始位置,大概这样:walk(startIndex)

  • 起始位置设为最后一个字符位置,往前找,如果找到[^*^]则往前找句子结束符(如。!?-),找到后就把整句话用x:[]包起来。

  • 继续调用这个函数,入参startIndex为刚刚那个句子的起始位置。

  • 直到入参为0(即找到头了),结束执行。


完美,补充完测试用例,开心光速下班 😃。





命运所有的馈赠早已暗中标好了价格。





报错


一天后:


image.png


image.png


栈溢出了。


同事的需求需要往这段 markdown 里填充上万字符,我的递归函数爆栈了 🤯🤯🤯。


一时间脑海里立刻出现 3 条解决方案:



  • 问 gpt

  • 问 gpt

  • 问 gpt


image.png
得到了6种解决方案:



  1. 尾递归优化(Tail Call Optimization)

  2. 循环替换递归( Loop) 将递归转换为等效的循环

  3. 使用堆栈来管理状态(Manual Stack Management) 手动使用数组作为堆栈来保存需要的状态,模拟递归的过程。

  4. 使用异步递归( Async Recursion) 通过把递归调用放在setTimeout、setImmediate或Promise中来异步执行,可以避免同步递归调用造成的堆栈溢出。

  5. 节流递归(Throttling Recursion) 通过定期将递归调用事件推迟到下一个事件循环迭代中,你可以避免堆栈溢出。

  6. Trampoline 函数(Trampoline Function) Trampoline 是一种编程技巧,允许你改写递归函数,使其成为迭代的,而不需要占用新的调用栈帧。


简单评估下这几个方案:



  • 方案2和3对代码的改动量较大,递归变迭代,实在懒得改也懒得验证,哒咩 ❌

  • 方案4和5把同步逻辑改成了异步,对代码逻辑的侵入性太强,哒咩 ❌

  • 方案1的尾递归优化我现在就在用,无效,哒咩 ❌


方案一让我感觉撞了鬼:为什么我写的尾递归优化没生效?


搜了下才发现尾递归优化可谓名存实亡,主流浏览器全都不支持尾递归优化:


compat-table.github.io/compat-tabl…


image.png


详见文章:尾递归的后续探究-腾讯云开发者社区-腾讯云


解决


5/6的方案都被否掉,看来看去只能使用 Trampoline 函数,即蹦床函数。


我们看下 gpt给的示例,以解释蹦床函数做了些什么:


function trampoline(fn) {
return function(...args) {
let result = fn(...args);
while (typeof result === 'function') {
result = result();
}
return result;
};
}

function sum(x, y) {
if (y > 0) {
return () => sum(x + 1, y - 1);
} else {
return x;
}
}

let safeSum = trampoline(sum);
safeSum(1, 100000);

这里对原函数的修改在第13行,正常的递归会直接执行sum函数,而这段优化里则改成返回了一个函数,我们姑且称之为 handler 函数。


而 trampoline 函数的作用则是执行 sum 函数并判断返回值,如果返回值是函数(即 handler 函数),则继续执行该函数,直到返回值是数字。整个判断&执行过程使用 while循环。


蹦床函数之所以能够摆脱递归调用栈限制,是因为 handler 函数是由蹦床函数执行的,handler 函数执行前,它的父函数 sum 函数已经执行完毕了,handler 的执行跟 sum 的执行没有堆栈关系。




完美,



  • 补充测试用例。

  • 加上try catch防止白屏。

  • 加上memo防止每次render都递归计算。


开心光速下班 😃。


作者:tumars
来源:juejin.cn/post/7330521390510440511
收起阅读 »

Vite 4.3 为何性能爆表?

web
免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 How we made Vite 4.3 faaaaster。 本期共享的是 —— Vite 4.3 性能大幅提升的幕后技术细节。 地球人都知道,Vite 4.3 相比 V...
继续阅读 »

免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 How we made Vite 4.3 faaaaster



本期共享的是 —— Vite 4.3 性能大幅提升的幕后技术细节。


地球人都知道,Vite 4.3 相比 Vite 4.2 取得了惊人的性能提升。


01-vite.png


fs.realpathSync 的问题


Nodejs 中有一个有趣的 realpathSync 问题,它指出 fs.realpathSyncfs.realpathSync.native70 倍。


但由于在 Windows 上的行为不同,Vite 4.2 只在非 Windows 系统上使用 fs.realpathSync.native。为了搞定此问题,Vite 4.3 在 Windows 上调用 fs.realpathSync.native 时添加了网络驱动验证。


Vite 从未放弃 Windows,它真的......我哭死。


JS 优化


不要错过编程语言优化。Vite 4.3 中若干有趣的 JS 优化案例:


*yield 重构为回调函数


Vite 使用 tsconfck 来查找和解析 tsconfig 文件。tsconfck 用于通过 *yield 遍历目标目录,生成器的短板之一在于,它需要更多的内存空间来存储其 Generator 对象,且生成器中存在一大坨生成器上下文切换运行。因此,自 v2.1.1 以来,该核心库用回调函数重构了 *yield


startsWith/endsWith 重构为 ===


Vite 4.2 使用 startsWith/endsWith 来检查热门 URL 中的前置和后置 '/'。我们测评了 str.startsWith('x') 和 str[0] === 'x' 的执行基准跑分,发现 ===startsWith 快约 20%。同时 endsWith=== 慢约 60%。


避免重新创建正则表达式


Vite 需要一大坨正则表达式来匹配字符串,其中大多数都是静态的,所以最好只使用它们的单例。Vite 4.3 优化了正则表达式,这样可以重复使用它们。


放弃生成自定义错误


为了更好的开发体验,Vite 4.2 中存在若干自定义错误。这些错误可能会导致额外的计算和垃圾收集,降低 Vite 的速度。在 Vite 4.3 中,我们不得不放弃生成某些热门的自定义错误(比如 package.json NOT_FOUND 错误),并直接抛出原始错误,获取更好的性能。


更机智的解析策略


Vite 会解析所有已接收的 URL 和路径,获取目标模块。


Vite 4.2 中存在一大坨冗余的解析逻辑和非必要的模块搜索。 Vite 4.3 使解析逻辑更精简、更严格、更准确,减少计算量和 fs 调用。


更简单的解析


Vite 4.2 重度依赖 resolve 模块来解析依赖的 package.json,当我们偷看 resolve 模块的源码时,发现在解析 package.json 时存在一大坨无用逻辑。Vite 4.3 弃用了 resolve 模块,遵循更精简的解析逻辑:直接检查嵌套父目录中是否存在 package.json


更严格的解析


Vite 需要调用 Nodejs 的 fs API 来查找模块。但 IO 成本昂贵。Vite 4.3 缩小了文件搜索范围,并跳过搜索某些特殊路径,尽量减少 fs 调用。举个栗子:



  1. 由于 # 符号不会出现在 URL 中,且用户可以控制源文件路径中不存在 # 符号,因此 Vite 4.3 不再检查用户源文件中带有 # 符号的路径,而只在 node_modules 中搜索它们。

  2. 在 Unix 系统中,Vite 4.2 首先检查根目录内的每个绝对路径,对于大多数路径而言问题不大,但如果绝对路径以 root 开头,那大概率会失败。为了在 /root/root 不存在时,跳过搜索 /root/root/path-to-file,Vite 4.3 会在开头判断 /root/root 是否作为目录存在,并预缓存结果。

  3. 当 Vite 服务器接收到 @fs/xxx@vite/xxx 时,无需再次解析这些 URL。Vite 4.3 直接返回之前缓存的结果,不再重新解析。


更准确的解析


当文件路径为目录时,Vite 4.2 会递归解析模块,这会导致不必要的重复计算。Vite 4.3 将递归解析扁平化,针对不同类型的路径对症下药。拍平后缓存某些 fs 调用也更容易。


package


Vite 4.3 打破了解析 node_modules 包数据的性能瓶颈。


Vite 4.2 使用绝对文件路径作为包数据缓存键。这还不够,因为 Vite 必须在 pkg/foo/barpkg/foo/baz 中遍历相同的目录。


Vite 4.3 不仅使用绝对路径(/root/node_modules/pkg/foo/bar.js/root/node_modules/pkg/foo/baz.js),还使用遍历的目录(/root/node_modules/pkg/foo/root/node_modules/pkg)作为 pkg 缓存的键。


另一种情况是,Vite 4.2 在单个函数内查找深度导入路径的 package.json,举个栗子,当 Vite 4.2 解析 a/b/c/d 这样的文件路径时,它首先检查根 a/package.json 是否存在,如果不存在,那就按 a/b/c/package.json -> a/b/package.json 的顺序查找最近的 package.json,但事实上,查找根 package.json 和最近的 package.json 应该分而治之,因为它们需要不同的解析上下文。Vite 4.3 将根 package.json 和最接近的 package.json 的解析分而治之,这样它们就不会混合。


非阻塞任务


作为一种按需服务,Vite 开发服务器无需备妥所有东东就能启动。


非阻塞 tsconfig 解析


Vite 服务器在预打包 ts/tsx 时需要 tsconfig 的数据。


Vite 4.2 在服务器启动前,会在 configResolved 插件钩子中等待解析 tsconfig 的数据。一旦服务器启动而尚未备妥 tsconfig 的数据,即使该请求稍后可能需要等待 tsconfig 解析,页面请求也可以访问服务器,


Vite 4.3 在服务器启动前初始化 tsconfig 解析,但服务器不会等待它。解析过程在后台运行。一旦 ts 相关的请求进来,它就必须等待 tsconfig 解析完成。


非阻塞文件处理


Vite 中存在一大坨 fs 调用,其中某些是同步的。这些同步 fs 调用可能会阻塞主线程。Vite 4.3 将其更改为异步。此外,异步函数的并行化也更容易。关于异步函数,我们关心的一件事是,解析后可能需要释放一大坨 Promise 对象。得益于更机智的解析策略,释放 fs - Promise 对象的成本要低得多。


HMR 防抖


请考虑两个简单的依赖链 C <- B <- AD <- B <- A,当编辑 A 时,HMR 会将两者从 A 传播到 CD。这导致 AB 在 Vite 4.2 中更新了两次。


Vite 4.3 会缓存这些遍历过的模块,避免多次探索它们。这可能会对那些具有组件集装导入的文件结构产生重大影响。这对于 git checkout 触发的 HMR 也有好处。


并行化


并行化始终是获取更好性能的不错选择。在 Vite 4.3 中,我们并行化了若干核心功能,包括但不限于导入分析、提取 deps 的导出、解析模块 url 和运行批量优化器。并行化之后确实有令人印象深刻的改进。


基准测试生态系统



  • vite-benchmark:Vite 使用此仓库来测评每个提交的跑分,如果您正在使用 Vite 开发大型项目,我们很乐意测试您的仓库,以获得更全面的性能。

  • vite-plugin-inspect:vite-plugin-inspect 从 v0.7.20 开始支持显示插件的钩子时间,并且将来会有更多的跑分图,请向我们反馈您的需求。

  • vite-plugin-warmup:预热您的 Vite 服务器,并提升页面加载速度!


《前端 9 点半》每日更新,持续关注,坚持阅读,每天一次,进步一点


谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7331361547011801115
收起阅读 »

百度输入法在候选词区域植入广告,网友:真nb!

web
V2EX 用户发帖称,百度输入法最新版本在候选词区域植入了广告。具体表现为,如果用户要打 “招商银行” 四个字,当输入 “招商” 之后,候选词的首位是 “★热门加盟店排行” 的链接,点击后会进入名为「加盟星榜单」的广告页面。https://www.v2ex.c...
继续阅读 »

V2EX 用户发帖称,百度输入法最新版本在候选词区域植入了广告。

具体表现为,如果用户要打 “招商银行” 四个字,当输入 “招商” 之后,候选词的首位是 “★热门加盟店排行” 的链接,点击后会进入名为「加盟星榜单」的广告页面。

https://www.v2ex.com/t/1011440

别的不说,想出这个功能的产品经理真是个人才,因此评论区有用户感叹道:


不说用户体验怎么样,不得不说这个键盘的候选词广告想法确实超前,不光超前,还实现了。
根据输入内容,直接用候选词的方式推送广告,从源头出发拿到用户的一手数据,直接甩掉了各种中间商。速度也更快,更精确的投送。
可以说是真 nb 呀


知名科技博主阑夕对此评论道:“你都打出招商两个字了,一定是想加盟店铺做生意吧?逻辑极其通顺智能,对不对?这真的是人类能够企及的创新吗,太牛逼了。


作者:架构师大咖
来源:mp.weixin.qq.com/s/0KR2F_a9q2_9JSS8nXtodQ
收起阅读 »

从uni-app中去掉编译后微信小程序的滚动条

web
首先如果你使用的是页面级滚动,即使uni-app中的pages.json中有相关配置,在编译到小程序中也是没有效果的,因为小程序原生不支持,如下: 那么我们去看微信的官方回复: 所以得出一个结论,要想隐藏滚动条,我们必须使用scroll-view视图组件...
继续阅读 »

首先如果你使用的是页面级滚动,即使uni-app中的pages.json中有相关配置,在编译到小程序中也是没有效果的,因为小程序原生不支持,如下:



那么我们去看微信的官方回复:




所以得出一个结论,要想隐藏滚动条,我们必须使用scroll-view视图组件


那么在uni-app页面滚动是不是scroll-view,答案是的,但是我们没办法在顶层设置,因为官方没有暴露相关api,那么要想去掉滚动条,我们就只能在自己的页面使用scroll-view视图组件,取代全局的滚动视图。


下面上简易代码


<template>
<scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
<view class="list" v-for="iten in 30">列表{{iten}}</view>
</scroll-view>
</template>


<style lang="scss" scoped>
.main{
height: 100vh;
}
.list{
border: 1xp solid black;
margin: 20rpx auto;
text-align: center;
line-height: 100rpx;
}
</style>

效果图:


初版.gif


如果你的组件不是占满全屏,比如有头部导航


这时候有两种做法:


1.将头部标签放到scroll-view内部,然后固定定位


<template>
<scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
<view class="nav">导航nav</view>
<view class="list-container">
<view class="list" v-for="iten in 30">列表{{iten}}</view>
</view>
</scroll-view>
</template>

<style lang="scss" scoped>
.main{
height: 100vh;
}
.list-container{
margin-top: 200rpx;
}
.list{
border: 1xp solid black;
margin: 20rpx auto;
text-align: center;
line-height: 100rpx;
}
.nav{
position: fixed;
top: 0;
line-height: 200rpx;
padding-top: 20rpx;
width: 100vw;
text-align: center;
border: 1px solid black;
background-color: #fff;
}
</style>

效果图:


230187154229138168229133168229177143.gif


2.将scroll-view的高度设置为视口余下高度


这里注意一下在移动端尽量较少的使用cale()计算高度


所以这里我们使用flex布局


<template>
<view class="content">
<view class="nav">导航nav</view>
<scroll-view scroll-y="true" :show-scrollbar="false" :enhanced="true" class="main">
<view class="list" v-for="iten in 30">列表{{iten}}</view>
</scroll-view>
</view>
</template>

<style lang="scss" scoped>
.content{
height: 100vh;
width: 100vw;
overflow: hidden;
display: flex;
flex-direction: column;
}
.main{
flex-grow: 1;
}
.list{
border: 1xp solid black;
margin: 20rpx auto;
text-align: center;
line-height: 100rpx;
}
.nav{
height: 200rpx;
line-height: 200rpx;
width: 100vw;
text-align: center;
border: 1px solid black;
background-color: #fff;
}
</style>

效果图:


230187154229138168229133168229177143.gif


如果有帮助到你的话,记得点个赞哦!


猫咪.gif


作者:aways
来源:juejin.cn/post/7330655456883654667
收起阅读 »

解锁 JSON.stringify() 7 个鲜为人知的坑

web
在本文中,我们将探讨与JSON.stringify()相关的各种坑。 1. 处理undefined、Function和Symbol值 在前端中 undefined、Function和Symbol值不是有效的JSON值。在转换过程中遇到它们时,它们会被省略(在对...
继续阅读 »

在本文中,我们将探讨与JSON.stringify()相关的各种坑。


1. 处理undefined、Function和Symbol值


在前端中 undefinedFunctionSymbol值不是有效的JSON值。在转换过程中遇到它们时,它们会被省略(在对象中),或者被更改为null(在数组中)。


例如:

const obj = { foo: function() {}, bar: undefined, baz: Symbol('example') };  
const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: '{}'

const obj2 = {arr: [function(){}]};
console.log(JSON.stringify(obj2)); // 输出: {"arr":[null]}

2. 布尔、数字和字符串对象


布尔、数字和字符串对象在字符串化过程中会被转换为它们对应的原始值。

const boolObj = new Boolean(true);  
const jsonString = JSON.stringify(boolObj);
console.log(jsonString); // 输出: 'true'

3. 忽略Symbol键的属性


Symbol键属性在字符串化过程中完全被忽略,即使使用替换函数也是如此。这意味着与Symbol键关联的任何数据都将在生成的JSON字符串中被排除。

const obj = { [Symbol('example')]: 'value' };  
const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: '{}'

const obj2 = {[Symbol('example')]: [function(){}]};
console.log(JSON.stringify(obj2)); // 输出 '{}'

4. 处理无穷大(Infinity)、NaN和Null值


Infinity、NaN 和 null 值在字符串化过程中都被视为 null。

const obj = { value: Infinity, error: NaN, nothing: null };  
const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: '{"value":null,"error":null,"nothing":null}'

5. Date对象被视为字符串


Date实例通过实现toJSON()函数来返回一个字符串(与date.toISOString()相同),因此在字符串化过程中被视为字符串。

const dateObj = new Date();
const jsonString = JSON.stringify(dateObj);
console.log(jsonString); // 输出:"2024-01-31T09:42:00.179Z"

6. 循环引用异常


如果 JSON.stringify() 遇到具有循环引用的对象,它会抛出一个错误。循环引用发生在一个对象在循环中引用自身的情况下。

const circularObj = { self: null };
circularObj.self = circularObj;
JSON.stringify(circularObj); // Uncaught TypeError: Converting circular structure to JSON

7. BigInt转换错误


使用JSON.stringify()转换BigInt类型的值时引发错误。

const bigIntValue = BigInt(42);  
JSON.stringify(bigIntValue); // Uncaught TypeError: Do not know how to serialize a BigInt

各位同学如果在开发中还遇到过不一样的坑,还请评论区补充互相讨论


作者:StriveToY
来源:juejin.cn/post/7330289404731047936
收起阅读 »

分享:一个超实用的文字截断技巧

web
文字截断是 Web 开发中非常普遍的一个需求,原因无他,很多时候我们无法确定展示文本的容器的宽度,如果不使用文字截断,要么文本被生硬的截断隐藏、要么文本破坏了预期中的布局。 Tailwind CSS 提供的文字截断的原子类: .truncate { over...
继续阅读 »

文字截断是 Web 开发中非常普遍的一个需求,原因无他,很多时候我们无法确定展示文本的容器的宽度,如果不使用文字截断,要么文本被生硬的截断隐藏、要么文本破坏了预期中的布局。


Tailwind CSS 提供的文字截断的原子类:


.truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

这 3 组 CSS 规则共同作用才可实现用 ... 截断文字的效果,缺一不可。



  • overflow: hidden 表示容器空间不足时内容应该隐藏,而非默认的 visible 完全展示

  • white-space: nowrap 表示文本容器宽度不足时,不能换行,其默认值为 normal,该行为不太好预测,但大部分情况下,它等同于 wrap 即文本尽可能的换行

  • text-overflow: ellipsis 指定文本省略使用 ... ,该属性默认值为 clip ,表示文本直接截断什么也不显示,这样展示容易对用户造成误解,因此使用 ... 更合适


接下来介绍一个在 PC Web 上很实用的交互效果:在需要截断的文本后面,紧跟一个鼠标 hover 上去才会展示的按钮, 执行一些和省略文本强相关、轻操作的动作。


Untitled.gif


如图所示,鼠标 hover 表示的按钮可以用来快速的编辑「标题」。下面介绍一下它的纯 CSS 实现。


首先,我们来实现一个基础版的。


Untitled 1.gif


代码:


<div class="container">
<p class="complex-line truncate">
<span class="truncate">海和天相连成为海岸线</span>
<span class="icon">❤️</span>
</p>
<p class="truncate">鱼和水相濡以沫的世界</p>
</div>

<style>
.truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.container {
max-width: 100px;
}
.complex-line {
display: flex;
}
.complex-line:hover .icon {
display: block;
}
.icon {
display: none;
}
</style>

一些重点:



  • 容器 .container 必须是宽度固定、或者最大宽度固定的,以此确保容器不会因为子元素无止境的扩充。比如你可以设置容器的 widthmax-width 。在某些情况下,即使设置了 max-width 但不生效,此时可以尝试添加 overflow: hidden

  • 含有按钮的行 .complex-line 和子元素中展示文字的标签( .complex-line 下的 span.truncate),都要添加文字截断类 .truncate

  • 按钮 .icon 默认不展示,hover 当前行的时候才展示,这个也很常规,主要通过设置其 display 属性实现。

  • 🌟 接下来一步很巧妙,就是为含有按钮的行 .complex-line ,设置其为 Flex 容器,这主要是借助 Flex Item 的 flex-shrink 属性默认值为 1 的特性,允许含有文字的 Flex Item span.truncate 在按钮 span.icon 需要展示的时候进一步缩放,腾出空间,用于完整展示按钮。


这样便实现了基础版的文字截断 + 可以自动显示隐藏按钮的交互。


接下来再看看文章开头提到的这个交互:


Untitled.gif


它和基础版的样式最大的不同就是它是多列的,这里可以使用 Grid 网格布局实现。


<div class="container">
<label>标题:</label>
<p class="complex-line truncate">
<span class="truncate">高质量人才生产总基地</span>
<span class="icon">✏️</span
</p>
<label>编号:</label>
<p class="truncate">No.9781498128959</p>
</div>

<style>
.container {
display: grid;
grid-template-columns: auto 1fr;
/** ... */
}
</style>

其他样式代码和基础版中的一致,不再赘述。


总结


为了实现本文介绍的这种交互,应该确保:



  • 容器的宽度或最大宽度应该是固定的

  • 按钮前面的那个展示文字的元素,和它们的父元素,都应该有 .truncate 文字截断效果

  • 按钮的父元素应该为 Flex 容器,使得按钮显示时,文字所在标签可以进一步缩放


作者:人间观察员
来源:juejin.cn/post/7330464865315094554
收起阅读 »

消息通知文字竖向无缝轮播组件实现历程

web
背景 最近有个需求需要做一个无缝轮播的消息通知,并且需要抽离成通用组件,记录下实现这个组件的历程。 先看效果 实现过程 思考(part-1) 因为刚开始给的设计稿是没有动画效果的,我刚开始想的效果是只有红色加粗的文字轮播,其他文字不变;然后想着看下有没...
继续阅读 »

背景



最近有个需求需要做一个无缝轮播的消息通知,并且需要抽离成通用组件,记录下实现这个组件的历程。



先看效果
noticeBar.gif


实现过程


思考(part-1)



因为刚开始给的设计稿是没有动画效果的,我刚开始想的效果是只有红色加粗的文字轮播,其他文字不变;然后想着看下有没有已经实现好的轮子,找到一个 js 库:react-text-loop;但是经过我的考虑,如果只是部分文字滚动,红色加粗的文字可能宽度不一样,会导致其他文字换位,所以还是想着整条文字滚动会比较好。



使用 antd-m Swiper 实现(part-2)



想到这个滚动效果在移动端应该很常见,antd-m 应该可能会有组件吧,去瞧瞧👀;noticeBar 组件只有横向滚动文字,没有竖直滚动的。既然没有,那就用其他方式实现吧,最简单的就是用 Swiper 组件设置成 竖向滚动,因为我负责的项目基本都用 antd-m,所以就用 antd-m 的 Swiper 来实现了。



实现过程遇到的问题


antd-m 的 Swiper 组件竖向滚动必须指定高度

在使用 antd-m 的 Swiper 组件竖向滚动的方式好像有问题,但是看文档的使用又是正常,结果发现竖向滚动需要指定高度,所以文档还是要仔细看清楚: Swiper 竖向文档


依赖冲突问题

在自己仓库使用很正常,一点问题都没有;然后打算抽离到我们项目的组件库中,然后在把项目中使用替换成组件库的包,过程很顺畅;过了段时间另一个项目要使用我们的组件,然后我就把包发给他,结果他说他项目里用不了,会报错。


然后 clone 他的项目试了下,果然是有问题的,因为他们项目里用的是 antd-m 2.x,2.x 没有 Swiper 组件,而我的组件库依赖的是 antd-m 5.x,看了下他们仓库用的是antd-m 2.x 和 5.x 共存的方式,可以看一下这个 antd-m 迁移指南,如果要两个版本共存且之前用的是组件按需导入,那么组件按需导入的配置也会有问题,因为两个版本的文件差异比较大,所以需要改一下按需导入的配置:


module.exports = {
"plugins": [
// 原配置
// [
// 'import',
// {
// libraryName: 'antd-mobile',
// libraryDirectory: 'lib',
// style: 'css',
// },
// 'antd-mobile',
// ]

// 修改为
[
'import',
{
libraryName: 'antd-mobile',
customName: (name, file) => {
const { filename } = file.opts;
if (filename.includes('/es/')) {
return `antd-mobile/es/components/${name}`;
}
return `antd-mobile/lib/${name}`;
},
style: (name, file) => {
const { filename } = file.opts;
if (filename.includes('/es/')) {
// 如果项目已经有 global 文件,return false 即可
// 如果没有,这样配置后就不需要手动引入 global 文件
return 'antd-mobile/es/global';
}
return `${name}/style/css`;
},
},
'antd-mobile',
]
]
}

想彻底解决这个依赖冲突问题


其实修改完配置之后使用就正常了,但是我考虑到如果之后想使用这个组件,如果 antd-m 版本不是 5.x,那么有一个项目就要改一个配置,很烦人;而且 antd-m Swiper 竖向需要指定高度,如果都需要指定高度了,那么我直接实现一个滚动动画应该也很简单吧,说干就干。



自己手写一个轮播组件(pard-3)



手动实现轮播还是比较简单的,只不过无缝轮播那里需要处理下,传统的方式都是通过在轮播 item 首尾复制 item 插入,当轮播到最后一个,用复制的 item 来承接,之后在回归原位正常滚动。



手写实现思路



  1. 传入轮播容器的高度,使用 transform: translate3d(0px, ${容器高度}, 0px); 每次移动列表 translateY 的距离为容器的高度。

  2. 处理无缝轮播,因为这个组件没有手动滑动轮播,自由自动向下轮播,所以不需要考虑反方向的轮播处理;当轮播到最后一个 item,那么就将第一个 item transform: translateY(${轮播列表高度}),这时候第一个就在最后一个下面,监听轮播列表 onTransitionEnd,判断当前是否轮播到第一个,是的话就将轮播列表的 translateY 的距离归 0。


最终实现代码



其实最后是封装成了一个 react 组件,但是掘金上为了大家看到更好的效果,用原生的方式简单写了下。如果需要封装成 react/vue 组件参考下方代码应该就够了。



容器未 hidden 效果



组件封装的设计



这里放一下我封装组件设计的 props



interface IProps {
/** 轮播通知 list */
list: any[];
/** noticebar 显隐控制 */
visible?: boolean;
/** 单个轮播 item 的高度, 传入 750 设计稿的数字,会转成 vw */
swiperHeight?: number;
/** 每条通知轮播切换的间隔 */
interval?: number;
/** 轮播动画的持续时间 */
animationDuration?: number;
/** 是否展示关闭按钮 */
closeable?: boolean;
/** 关闭按钮点击的回调 */
onClose?: () => void;
/** 自定义轮播 item 的内容 */
renderItem?: (item: any) => React.ReactNode;
/** notice 的标题 */
noticeTitle?: ReactNode;
/** notice 右边自定义 icon */
rightIcon?: ReactNode;
/** 是否展示 notice 左边 icon */
showLeftIcon?: boolean;
/** notice 左边自定义 icon */
leftIcon?: ReactNode;
/** 自定义类名 */
className?: string;
}

作者:wait
来源:juejin.cn/post/7330054489079169065
收起阅读 »

Vue 依赖注入:一种高效的数据共享方法

web
什么是vue依赖注入? Vue是一个用于构建用户界面的渐进式框架。 它提供了一种简单而灵活的方式来管理组件之间的数据流,即依赖注入(Dependency Injection,DI)。 依赖注入是一种设计模式,它允许一个组件从另一个组件获取它所依赖的数据...
继续阅读 »

什么是vue依赖注入?



Vue是一个用于构建用户界面的渐进式框架。




它提供了一种简单而灵活的方式来管理组件之间的数据流,即依赖注入(Dependency Injection,DI)。



依赖注入是一种设计模式,它允许一个组件从另一个组件获取它所依赖的数据或服务,而不需要自己创建或管理它们。这样可以降低组件之间的耦合度,提高代码的可维护性和可测试性。


依赖注入示意图


在这里插入图片描述

provide和inject



在Vue中,依赖注入的方式是通过provide和inject两个选项来实现的。




provide选项允许一个祖先组件向下提供数据或服务给它的所有后代组件。
inject选项允许一个后代组件接收来自祖先组件的数据或服务。
这两个选项都可以是一个对象或一个函数,对象的键是提供或接收的数据或服务的名称,值是对应的数据或服务。函数的返回值是一个对象,具有相同的格式。



下面是一个简单的例子,演示了如何使用依赖注入的方式共享数据:


父组件


<template>
<div>
<h1>我是祖先组件</h1>
<child-component></child-component>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
name: 'AncestorComponent',
components: {
ChildComponent
},
// 提供一个名为message的数据
provide: {
message: 'Hello from ancestor'
}
}
</script>

子组件


<template>
<div>
<h2>我是后代组件</h2>
<p>{{ message }}</p>
</div>
</template>


// 后代组件
<script>
export default {
name: 'ChildComponent',
// 接收一个名为message的数据
inject: ['message']
}
</script>

这样,后代组件就可以直接使用祖先组件提供的数据,而不需要通过props或事件来传递。


需要注意的是,依赖注入的数据是不可响应的,也就是说,如果祖先组件修改了提供的数据,后代组件不会自动更新。
如果需要实现响应性,可以使用一个响应式的对象或者一个返回响应式对象的函数作为provide的值。


实现响应式依赖注入的几种方式


一、提供响应式数据



方法是在提供者组件中使用ref或reactive创建响应式数据,然后通过provide提供给后代组件。后代组件通过inject接收后,就可以响应数据的变化。



提供者:


<template>
<div>
<h1>我是提供者组件</h1>
<button @click="count++">增加计数</button>
<child-component></child-component>
</div>

</template>

<script>
import ChildComponent from './ChildComponent.vue'
import { ref, provide } from 'vue'

export default {
name: 'ProviderComponent',
components: {
ChildComponent
},
setup() {
// 使用ref创建一个响应式的计数器
const count = ref(0)
// 提供给后代组件
provide('count', count)
return {
count
}
}
}
</script>


接收者:


<template>
<div>
<h2>我是接收者组件</h2>
<p>计数器的值是:{{ count }}</p>
</div>

</template>

<script>
import { inject } from 'vue'

export default {
name: 'ChildComponent',
setup() {
// 接收提供者组件提供的响应式对象
const count = inject('count')
return {
count
}
}
}
</script>


二、提供修改数据的方法



提供者组件可以提供修改数据的方法函数,接收者组件调用该方法来更改数据,而不是直接修改注入的数据。



提供者:


<template>
<div>
<h1>我是提供者组件</h1>
<p>消息是:{{ message }}</p>
<child-component></child-component>
</div>

</template>

<script>
import ChildComponent from './ChildComponent.vue'
import { ref, provide } from 'vue'

export default {
name: 'ProviderComponent',
components: {
ChildComponent
},
setup() {
// 使用ref创建一个响应式的消息
const message = ref('Hello')
// 定义一个更改消息的方法
function updateMessage() {
message.value = 'Bye'
}
// 提供给后代组件
provide('message', { message, updateMessage })
return {
message
}
}
}
</script>


接收者:


<template>
<div>
<h2>我是接收者组件</h2>
<p>消息是:{{ message }}</p>
<button @click="updateMessage">更改消息</button>
</div>

</template>

<script>
import { inject } from 'vue'

export default {
name: 'ChildComponent',
setup() {
// 接收提供者组件提供的响应式对象和方法
const { message, updateMessage } = inject('message')
return {
message,
updateMessage
}
}
}
</script>


三、使用readonly包装



通过readonly包装provide的数据,可以防止接收者组件修改数据,保证数据流的一致性。



提供者:


<template>
<div>
<h1>我是提供者组件</h1>
<p>姓名是:{{ name }}</p>
<child-component></child-component>
</div>

</template>

<script>
import ChildComponent from './ChildComponent.vue'
import { ref, provide, readonly } from 'vue'

export default {
name: 'ProviderComponent',
components: {
ChildComponent
},
setup() {
// 使用ref创建一个响应式的姓名
const name = ref('Alice')
// 使用readonly包装提供的值,使其不可修改
provide('name', readonly(name))
return {
name
}
}
}
</script>


接收者:


<template>
<div>
<h2>我是接收者组件</h2>
<p>姓名是:{{ name }}</p>
<button @click="name = 'Bob'">尝试修改姓名</button>
</div>

</template>

<script>
import { inject } from 'vue'

export default {
name: 'ChildComponent',
setup() {
// 接收提供者组件提供的只读对象
const name = inject('name')
return {
name
}
}
}
</script>


四、使用<script setup>



<script setup>组合式写法下,provide和inject默认就是响应式的,无需额外处理。



总结



依赖注入的方式共享数据在Vue中是一种高级特性,它主要用于开发插件或库,或者处理一些特殊的场景。



作者:Yoo前端
来源:juejin.cn/post/7329830481722294272
收起阅读 »

你还在使用websocket实现实时消息推送吗?

web
前言 在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。 本文主要介绍SSE的使用场景和如何使用SSE。 服务端向客户端推送数据的实现方案有哪几种? 我们常规实现这些需求...
继续阅读 »

前言


在日常的开发中,我们经常能碰见服务端需要主动推送给客户端数据的业务场景,比如数据大屏的实时数据,比如消息中心的未读消息,比如聊天功能等等。


本文主要介绍SSE的使用场景和如何使用SSE。


image.png


服务端向客户端推送数据的实现方案有哪几种?


我们常规实现这些需求的方案有以下三种



  1. 轮询

  2. websocket

  3. SSE


轮询简介


在很久很久以前,前端一般使用轮询来进行服务端向客户端进行消息的伪推送,为什么说轮询是伪推送?因为轮询本质上还是通过客户端向服务端发起一个单项传输的请求,服务端对这个请求做出响应而已。通过不断的请求来实现服务端向客户端推送数据的错觉。并不是服务端主动向客户端推送数据。显然,轮询一定是上述三个方法里最下策的决定。


轮询的缺点:



  1. 首先轮询需要不断的发起请求,每一个请求都需要经过http建立连接的流程(比如三次握手,四次挥手),是没有必要的消耗。

  2. 客户端需要从页面被打开的那一刻开始就一直处理请求。虽然每次轮询的消耗不大,但是一直处理请求对于客户端来说一定是不友好的。

  3. 浏览器请求并发是有限制的。比如Chrome 最大并发请求数目为 6,这个限制还有一个前提是针对同一域名的,超过这一限制的后续请求将会被阻塞。而轮询意味着会有一个请求长时间的占用并发名额

  4. 而如果轮询时间较长,可能又没有办法非常及时的获取数据


websocket简介


websocket是一个双向通讯的协议,他的优点是,可以同时支持客户端和服务端彼此相互进行通讯。功能上很强大。


缺点也很明显,websocket是一个新的协议,ws/wss。也就是说,支持http协议的浏览器不一定支持ws协议。


相较于SSE来说,websocket因为功能更强大。结构更复杂。所以相对比较


websocket对于各大浏览器的兼容性↓
image.png


SSE简介


sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。


长链接是一种HTTP/1.1的持久连接技术,它允许客户端和服务器在一次TCP连接上进行多个HTTP请求和响应,而不必为每个请求/响应建立和断开一个新的连接。长连接有助于减少服务器的负载和提高性能。

SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且SSE使用的是http协议(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。


注意:IE大魔王不支持SSE


SSE对于各大浏览器的兼容性↓
image.png


注意哦,上图是SSE对于浏览器的兼容不是对于服务端的兼容。


websocket和SSE有什么区别?


轮询


对于当前计算机的发展来说,几乎很少出现同时不支持websocket和sse的情况,所以轮询是在极端情况下浏览器实在是不支持websocket和see的下策。


Websocket和SSE


我们一般的服务端和客户端的通讯基本上使用这两个方案。首先声明:这两个方案没有绝对的好坏,只有在不同的业务场景下更好的选择。


SSE的官方对于SSE和Websocket的评价是



  1. WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。

  2. WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在HTTP协议之上的,现有的服务器软件都支持。

  3. SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。

  4. SSE默认支持断线重连,WebSocket则需要额外部署。

  5. SSE支持自定义发送的数据类型。


Websocket和SSE分别适用于什么业务场景?


对于SSE来说,它的优点就是轻,而且对于服务端的支持度要更好。换言之,可以使用SSE完成的功能需求,没有必要使用更重更复杂的websocket。


比如:数据大屏的实时数据,消息中心的消息推送等一系列只需要服务端单方面推送而不需要客户端同时进行反馈的需求,SSE就是不二之选。


对于Websocket来说,他的优点就是可以同时支持客户端和服务端的双向通讯。所适用的业务场景:最典型的就是聊天功能。这种服务端需要主动向客户端推送信息,并且客户端也有向服务端推送消息的需求时,Websocket就是更好的选择。


SSE有哪些主要的API?


建立一个SSE链接 :var source = new EventSource(url);

SSE连接状态


source.readyState



  • 0,相当于常量EventSource.CONNECTING,表示连接还未建立,或者连接断线。

  • 1,相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。

  • 2,相当于常量EventSource.CLOSED,表示连接已断,且不会重连。


SSE相关事件



  • open事件(连接一旦建立,就会触发open事件,可以定义相应的回调函数)

  • message事件(收到数据就会触发message事件)

  • error事件(如果发生通信错误(比如连接中断),就会触发error事件)


数据格式


Content-Type: text/event-stream //文本返回格式
Cache-Control: no-cache //不要缓存
Connection: keep-alive //长链接标识

image.png


SSE:相关文档,文档入口文档入口文档入口文档入口


显然,如果直接看api介绍不论是看这里还是看官网,大部分同学都是比较懵圈的状态,那么我们写个demo来看一下?


image.png


demo请看下方


我更建议您先把Demo跑起来,然后在看看上面这个w3cschool的SSE文档。两个配合一起看,会更方便理解些。


image.png


如何实操一个SSE链接?Demo↓


这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。
后端选用语言是node,框架是Express。


理论上,把这两段端代码复制过去跑起来就直接可以用了。



  1. 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件

  2. 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行


npm init          //初始化npm       
npm i express //下载node express框架
node index //启动服务

image.png


在这一层文件夹下执行命令。


完成以上操作就可以把项目跑起来了


前端代码Demo


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="ul">

</ul>
</body>
<script>

//生成li元素
function createLi(data){
let li = document.createElement("li");
li.innerHTML = String(data.message);
return li;
}

//判断当前浏览器是否支持SSE
let source = ''
if (!!window.EventSource) {
source = new EventSource('http://localhost:8088/sse/');
}else{
throw new Error("当前浏览器不支持SSE")
}

//对于建立链接的监听
source.onopen = function(event) {
console.log(source.readyState);
console.log("长连接打开");
};

//对服务端消息的监听
source.onmessage = function(event) {
console.log(JSON.parse(event.data));
console.log("收到长连接信息");
let li = createLi(JSON.parse(event.data));
document.getElementById("ul").appendChild(li)
};

//对断开链接的监听
source.onerror = function(event) {
console.log(source.readyState);
console.log("长连接中断");
};

</script>
</html>

后端代码Demo(node的express)


const express = require('express'); //引用框架
const app = express(); //创建服务
const port = 8088; //项目启动端口

//设置跨域访问
app.all("*", function(req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", '*');
//允许的header类型
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 可以带cookies
res.header("Access-Control-Allow-Credentials", true);
if (req.method == 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
})

app.get("/sse",(req,res) => {
res.set({
'Content-Type': 'text/event-stream', //设定数据类型
'Cache-Control': 'no-cache',// 长链接拒绝缓存
'Connection': 'keep-alive' //设置长链接
});

console.log("进入到长连接了")
//持续返回数据
setInterval(() => {
console.log("正在持续返回数据中ing")
const data = {
message: `Current time is ${new Date().toLocaleTimeString()}`
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
})

//创建项目
app.listen(port, () => {
console.log(`项目启动成功-http://localhost:${port}`)
})

效果


动画3.gif


总结



  1. SSE比websocket更轻

  2. SSE是基于http/https协议的

  3. websocket是一个新的协议,ws/wss协议

  4. 如果只需要服务端向客户端推送消息,推荐使用SSE

  5. 如果需要服务端和客户端双向推送,请选择websocket

  6. 不论是SSE还是websocket,对于浏览器的兼容性都不错

  7. 轮询是下策,很占用客户端资源,不建议使用。(不过偷懒的时候他确实方便)

  8. IE不支持SSE

  9. 小白同学demo如果跑不明白可以私信我


对了,小程序不支持SSE哦


image.png


最后


如果文章对您有帮助的话。


image.png


作者:工边页字
来源:juejin.cn/post/7325730345840066612
收起阅读 »

实现一个鼠标框选的功能,要怎么实现和设计 api?

web
前言 前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 rea...
继续阅读 »

285330798-9d463acf-c56b-48d8-b7d5-2dc02b4257e0.gif


前言


前两年在一家做电商的公司做了一个需求:鼠标框选商品卡片,开始拖拽的时候合成一个然后改变位置,页面上有几千个所以还要结合虚拟列表。当时不知道怎么做,就在 github 上到处找现成的库,最后找到了 react-selectable-fast,并结合 react-virtualizedreact-sortable-hoc 完成了需求。虽然该库已经很久不维护了,但大致上能满足我的需求了,尽管它是以 dom 的方式,很不 react,但秉承着能用就行的原则。意料之中,开发过程中遇到了 bug,最后只能 fork 一份修改源码后自己发了个 npm 包来使用。


项目介绍


前几个月在空闲时间突然来了兴致,自己找点事做,就想自己开发一个框选的库吧,万一也有人有这个需求不知道怎么办呢?写完后发到了 antd 的社区共建群里,有的人觉得不错也 star 了。先献上项目地址 react-selectable-box,文档完整,使用 dumi 编写。api 友好,支持自定义一些功能。


api 设计


一个组件在设计时,首先思考的应该是 api 如何去设计,最好符合大家平常的习惯,并具有一定的自定义和拓展能力。再加上了解 react-selectable-fast 这个库的缺点和痛点,对我的设计就更加有帮助了。大家在看下面的文章之前也可以思考一下,如果是你,你会怎么设计?这里只选取几个 api 来进行介绍。


主组件 Selectable


选中的值


defaultValuevalue,类型为 any[],每个方块一般都有一个唯一 id 来标识,2024-1-31 更新后 后支持任意类型,因为考虑到很多情况你可能需要一个对象或数组来标识,文章后面提供了 compareFn 来自定义比较值相等。


禁用


disabled,大部分有值的组件应该都会有此属性,能直接禁用框选功能。


模式


mode,类型为 "add" | "remove" | "reverse"。模式,表明当前框选是增加、减少还是取反。这个 api 感觉是设计的最好的,用户会框选来选择目标,肯定也会需要删除已经框选的目标,可能是按住 shift 来删除等等之类的操作。用户可以自己编写自定义逻辑来修改 mode 的值来控制不同的行为,反观 react-selectable-fast,则是提供了 deselectOnEscallowAltClickallowCtrlClickallowMetaClickallowShiftClick 等多个 api。


开始框选的条件


selectStartRange,类型 "all" | "inside" | "outside",鼠标从方块内部还是外部可以开始框选,或都可以。


可以进行框选的容器


dragContainer,类型 () => HTMLElement,例如你只希望某个卡片内才可以进行框选,不希望整个页面都可以进行框选,这个 api 就会起到作用了。


滚动的容器


scrollContainer,类型 () => HTMLElement,如果你的这些方块是在某个容器中并且可滚动,就需要传入这个属性,就可以在滚动的容器中进行框选操作了。


框选矩形的 style 与 className


boxStyleboxClassName,使用者可以自定义颜色等一些样式。


自定义 value 比较函数


compareFn,类型 (a: any, b: any) => boolean,默认使用 === 进行比较(因为 value 支持任意类型,比如你使用了对象或数组类型,所以你可能需要自定义比较)


框选开始事件


onStart,框选开始时,使用者可能需要做一些事情。


框选结束事件


onEnd,类型为 (selectingValue: (string | number)[], { added: (string | number)[], removed: (string | number)[] }) => voidselectingValue 为本次框选的值,added 为本次增加的值,removed 为本次删除的值。例如你想在每次框选后覆盖之前的操作,直接设置 selectingValue 成 value 即可。如果你想每次框选都是累加,加上 added 的值即可,这里就不再说明了。


方块可选 - useSelectable


怎么让方块可以被选择呢?并且一一绑定上对应的值?react-selectable-fast 则是提供 clickableClassName api,传入可以被选择的目标的 class,这种方式太不 react 了。此时我的脑海里想到了 dnd-kit,我认为是 react 最好用的拖拽库,它是怎么让每个方块可以被拖拽的呢?优秀的东西应该借鉴,于是就有了 useSelectable


const { 
setNodeRef, // 设置可框选元素
isSelected, // 是否已经选中
isAdding, // 当前是否正在添加
isRemoving, // 当前是否正在删除
isSelecting, // 当前是否被框选
isDragging // 是否正在进行框选操作
} = useSelectable({
value, // 每个元素的唯一值,支持任意类型
disabled, // 这个元素是否禁用
rule, // "collision" | "inclusion" | Function,碰撞规则,碰到就算选中还是全部包裹住才算选中,也可以自定义
});

如何使用?


const Item = ({ value }: { value: number }) => {
const { setNodeRef, isSelected, isAdding } = useSelectable({
value,
});

return (
<div
ref={setNodeRef}
style={{
width: 50,
height: 50,
borderRadius: 4,
border: isAdding ? '1px solid #1677ff' : undefined,
background: isSelected ? '#1677ff' : '#ccc',
}}
/>

);
};

实现


这里只简单讲一下思路,有兴趣的同学可以直接前往源码进行阅读。


主组件 Selectable 相当于一个 context,一些状态在这里进行保存,并掌管每个 useSelectable,将其需要的值通过 context 传递过去。


在设置的可被框选的容器内监听鼠标 mousedown 事件,记录其坐标,根据 mousemove 画出框选矩形,再根据 setNodeRef 收集的元素和框选矩形根据碰撞检测函数计算出是否被框选了,并将值更新到 Selectable 中去,最后在 mouseup 时触发 onEnd,将值处理完之后并丢出去。


演示


这里演示一下文章开头所说的框选拖拽功能,配合 dnd-kit 实现,代码在文档的 example 中。
录屏2024-01-23 19.27.43.gif


遇到的坑


这里分享一下遇到的坑的其中之一:框选的过程中会选中文字,很影响体验,怎么让这些文字不能被框选呢?


方案1: 用 user-select: none 来控制文本不可被选中,但是这是在用户侧来做,比较麻烦。并且发现在 chrome 下设置此属性后,拖拽框选到浏览器边缘或容器边缘后不会自动滚动,其它浏览器则正常


方案2: 在 mousedown 时设置 e.preventDefault(),这样选中时文字就不会被选中,但是拖拽框选到浏览器边缘或容器边缘后不会自动滚动,只能自己实现了滚动逻辑。后面又发现在移动端的 touchstart 设置时,会发现页面上的点击事件都失效了,查资料发现没法解决,只能另辟蹊径。


方案3: 在 mousemovetouchmove 时设置 e.preventDefault() 也是可以的,但也需要自己实现滚动逻辑。


最终也是采取了方案3。


后续目标


目前只能进行矩形的碰撞检测,不支持圆形(2024.1.26 更新支持自定义已经可以实现)及一些不规则图形(2024.1.26 更新提供自定义碰撞检测(dom 下太难,canvas 比较好做碰撞检测),剩下的就是使用者的事了!)。这是一个难点,如果有想法的可以在评论区提出或者 pr 也可。


2024-1-24 更新


添加 cancel 方法,试一试。可以调用 ref.current?.cancel() 方法取消操作。这样可以自定义按下某个键来取消当前操作。有想需不需要添加一个属性传入 keyCode 数组内置取消,但是感觉会使 api 太多而臃肿,也欢迎留下你的想法。


2024-1-26 更新一


添加 items api 以优化虚拟滚动滚动时框选范围增加或减小时,已经卸载的 Item 的判断框选。(可选)试一试


优化前:滚动到下面时,加大框选面积,上面已经被卸载的不会被选中


录屏2024-01-26 16.50.31.gif


优化后:滚动到下面时,加大框选面积,上面已经被卸载的会被选中


录屏2024-01-26 16.53.36.gif


2024-1-26 更新二


支持自定义碰撞规则检测,试一试自定义圆形碰撞检测
录屏2024-01-26 17.41.37.gif


2024-1-31 更新


value 支持任意类型 any,不再只是 string | number 类型,因为很多情况需要是一个对象或数组来当唯一标识,并提供了 compareFn 来支持自定义值的比较,默认使用 ===,如果你的 value 是对象或数组,需要此属性来比较值。


总结


开发一个较为复杂的组件,可以提交自己的 api 设计能力和解决问题的能力,可以将平常所学习、所了解、所使用的东西取其精华运用起来。最后希望这个组件能帮助到有需要的人,欢迎大家提出建议!有 issues 才能维护下去!如果觉得不错,帮忙点个 star 吧,地址 react-selectable-box


作者:马格纳斯
来源:juejin.cn/post/7326979670485123110
收起阅读 »

01CSS 实现多行文本“展开收起”

web
最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家 完成效果: 实现思路: 1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据...
继续阅读 »

最近在开发移动端的评论内容功能时,我遇到了一个需求,需要实现一个展开收起效果。主要目的是让用户能够方便地查看和隐藏评论内容,现在想将我的成果分享给大家



完成效果:


展开收起.gif


实现思路:


1.准备一个文本的外部容器( content),并将最大高度设置为65px(根据实际需求而定),超出内容设置不可见


image.png


2.文本容器的高度(text-content)不做样式设置,这个容器是为了获取内容实际高度


image.png


3.通过 js 获取文本容器的高度(text-content),判断文本高度是否超过外部容器(content)的最大高度,控制展开收起按钮是否显示


4.点击按钮时根据条件设置容器(content)的最大高度,css 对通过 transition 对 max-height 设置过渡效果


完整示例代码如下


HTML



<div class="container">
<div class="content">
<div class="text-content">
1月30日上午10时,中国贸促会将召开1月例行新闻发布会,介绍第二届链博会筹备进展情况;
2025大阪世博会中国馆筹备进展;2023年全国贸促系统商事认证数据;2023年贸法通运行情况;
2023年11月全球经贸摩擦指数;2023年12月全球知识产权保护指数月度观察报告;助力培育外贸新动能有关工作考虑等。
</div>
</div>
<button class="btn">展开</button>
</div>


CSS



.container {
width: 260px;
padding: 20px;
border: 1px solid #ccc;
margin: 50px auto;
}

.content {
max-height: 65px;
overflow: hidden;
transition: max-height 0.5s;
}


.btn {
display: flex;
width: 40px;
color: cornflowerblue;
outline: none;
border: none;
background-color: transparent;
}



JS


    const maxHeight=65
const btn = document.querySelector('.btn')
const content = document.querySelector('.content')
const textContent=document.querySelector('.text-content')
const textHeight=textContent.getBoundingClientRect().height // 文本高度
const contentHeight=content.getBoundingClientRect().height // 容器高度
let flag = false
if (textHeight < maxHeight) {
btn.style.display = 'none'
}
btn.addEventListener('click', () => {
if (!flag) {
content.style.maxHeight=textHeight+'px'
btn.innerHTML = '收起'
flag = true
} else {
content.style.maxHeight=contentHeight+'px'
btn.innerHTML = '展开'
flag = false
}
})



实现一个功能的方式往往有多种,你们是怎么解决的呢?


作者:前端小山
来源:juejin.cn/post/7329694104118919195
收起阅读 »

浏览器关闭实现登出(后端清token)

web
实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。 // 写在APP.vue mounted() { window.addEventLi...
继续阅读 »

实现浏览器关闭后,后端记录用户登出日志的最佳方式是通过前端发送请求来通知后端进行记录。以下是一种常见的实现方式,重点就是如何区分用户行为是页面刷新还是关闭浏览器。


// 写在APP.vue
mounted() {
window.addEventListener("beforeunload", () => this.beforeunloadHandler())
window.addEventListener("unload", () => this.unloadHandler())
},

destroyed() {
window.removeEventListener("beforeunload", () => this.beforeunloadHandler())
window.removeEventListener("unload", () => this.unloadHandler())
clearInterval(this.timer)
},

methods:{
beforeunloadHandler() {
this.beforeUnloadTime = new Date().getTime()
},
unloadHandler() {
this.gapTime = new Date().getTime() - this.beforeUnloadTime
if (this.gapTime <= 5) { //判断是窗口关闭还是刷新,小于5代表关闭,否则就是刷新。
// 这里是关闭浏览器
logout()
}
},
}


但是经测试,发现上面这种浏览器关闭事件并不是一种可靠的方式来捕捉用户的登出操作,后端并非百分百接收到logout请求,经查资料得知,在unload阶段发送的异步请求是不可靠的,有可能被cancel。后面又尝试了fetch,设置了keepalive(即使浏览器关闭,请求照样发送), 但是又发现gapTime<=5的判断条件也存在兼容性问题,不同浏览器的时间差存在差异。此外还存在一些特殊情况:用户可能直接关闭浏览器窗口、断开网络连接或发生其他异常情况,导致浏览器关闭事件无法被触发,因此pass掉上述方案。


后面也尝试了心跳机制(websocket),也存在局限性,pass。


最后想到了一种最简单,最朴实的方式:
开启定时器每秒往localStorage写入当前时间lastRecordTime(new Date().getTime()), 在请求拦截器中给每个接口请求头带上两个时间,最后一次写入时间lastRecordTime和当前时间nowTime, 后端只要把两个时间相减, 超过5s(自定义)就算登出,清掉redis里相应的token。


// 写在APP.vue
created (){
// 每秒写入一次时间
this.timer = setInterval(() => {
// 这个判断代表登录成功后才开始写入时间
if(localStorage.getItem('token')) {
localStorage.setItem('lastRecordTime', new Date().getTime())
}
}, 1000)
}

另外需要注意, 在登录成功的地方要立即写入一次时间, 不然有BUG。


  // 写在请求拦截器
const headers = config.headers;
/** 用于判断用户是否关闭过浏览器,如果关闭则跳转至登录页面,以及及时清理redis中的token */
if (localStorage.getItem('lastRecordTime')) {
headers.lastRecordTime = localStorage.getItem('lastRecordTime');
}
headers.nowTime = new Date().getTime();

总结一下,目前没发现哪种方式可以提供一种可靠的通信方式去通知后端清除token, 通过两个时间差的方式相对靠谱。


作者:起床搬砖啦
来源:juejin.cn/post/7328221562817478665
收起阅读 »

🌟前端使用Lottie实现炫酷的开关效果🌟

web
前言 在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。 比如说产品让我们实现这样的一个开关动...
继续阅读 »

前言


在平时的开发过程中,前端或多或少都会遇到实现动画效果的场景。手写动画是一件相当麻烦的事情,调来调去不仅费时费力,可能还会被产品/UI吐槽:这动画效果也不难呀,为什么就不能实现呢?/为什么就没有还原成我想要的样子呢。


image.png


比如说产品让我们实现这样的一个开关动效


Kapture 2024-01-20 at 21.53.34.gif


今天我们就用动画的实现方式——Lottie,来百分百还原设计师的动画效果,并且可以大大提高我们的工作效率(摸鱼时间)。


image.png


Lottie简介


首先我们先来看一下,平时我们实现动画都有哪些方式,它们分别有什么优缺点:


动画类型优点缺点
CSS 动画使用简便,通过@keyframestransition创建动画;浏览器原生支持,性能较好控制有限,不适用于复杂动画;复杂动画可能需要大量 CSS 代码,冗长
JavaScript 动画提供更高程度的控制和灵活性;适用于复杂和精细动画效果引入库增加页面负担,可能需要学习曲线;使用不当容器对页面性能造成影响,产生卡顿
GIF 动画制作和使用简单,无需额外代码;几乎所有浏览器原生支持有限颜色深度,不适用于所有场景;清晰度与文件尺寸成正比,无法适应所有分辨率
Lottie支持矢量动画,保持清晰度和流畅性 ;跨平台使用,适用于 iOS、Android 和 Web在一些较旧或性能较低的设备上,播放较大的 Lottie 动画可能会导致性能问题;对设计师要求较高

Lottie是由Airbnb开发的一个开源库,用于在移动端和Web上呈现矢量动画。它基于JSON格式的Bodymovin文件,可以将由设计师在AE中创建的动画导出为可在Lottie库中播放的文件。


相对于手写CSS/JS动画而言,它可以大大减少前端开发的工作量,相对于GIF文件来说,它可以在一个合理的文件体积内保证动画的清晰度以及流畅程度。下面我们就介绍一下如何播放一个Lottie动画,并实现一个炫酷的开关效果。


Hello Lottie


假设我们现在已经有一个Lottiejson文件,那么现在安装一些依赖


npm i react-lottie prop-types

安装完之后我们就可以这样子来播放一个Lottie动画:


import animationData from "../../assets/switch-lottie.json";

const LottieSwitch = () => {
const playing = useRef(false);
const options = {
loop: true,
autoplay: true,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};
return (
<Lottie
options={options}
height={20}
width={40}
/>

);
};


Kapture 2024-01-20 at 21.41.20.gif


来解释一下上面的options参数里面各个字段是什么意思:



  • loop:是否循环播放

  • autoplay:是否自动播放

  • animationDataLottie动画json资源

  • rendererSettings.preserveAspectRatio:指定如何在给定容器中渲染Lottie动画

    • xMidYMid: 表示在水平和垂直方向上都在中心对齐

    • 表示保持纵横比,但可能会裁剪超出容器的部分




正/反向播放


正常的把Lottie动画播放出来之后,我们就可以开始实现一个开关的功能。其实就是点击的时候更换Lottie的播放方向,这里对应的是direction字段,direction1时正向播放,direction-1时反向播放。


我们就要实现下面的功能:



  • 点击时切换方向

  • 播放过程中加锁,禁止切换方向

  • 监听播放结束事件,解锁

  • loop改为falseautoplay改为false


实现代码如下:


const LottieSwitch = () => {
const [direction, setDirection] = useState(null);
const playing = useRef(false);
const options = {
loop: false,
autoplay: false,
animationData: animationData,
rendererSettings: {
preserveAspectRatio: "xMidYMid slice",
},
};

const handleClick = () => {
if (playing.current) {
return;
}
playing.current = true;
setDirection((prevState) => (prevState === 1 ? -1 : 1));
};
return (
<div style={{ padding: 40 }}>
<div onClick={handleClick} className={styles.lottieWrapper}>
<Lottie
direction={direction}
options={options}
speed={2}
height={20}
width={40}
eventListeners={[
{
eventName: "complete",
callback: () =>
{
playing.current = false;
},
},
]}
/>
</div>
</div>

);
};

这样我们就是实现了一个开关的效果


Kapture 2024-01-20 at 21.53.34.gif


持续时长


Lottiejson中,有几个关键的字段跟动画的播放时长有关系:



  • fr:帧率,每一秒的帧数

  • ip:开始帧

  • op:结束帧


假如说有下面的一个描述:


{
"fr": 30,
"ip": 0,
"op": 60,
}

则表示帧率是30帧,从第0帧开始,60帧结束,那这个动画的持续时长是 (op-ip)/fr,为2s。那如果我们希望整个动画的播放时长是500ms,则只需要把Lottie的倍速调整为4。对应的就是speed字段:


<Lottie
direction={direction}
options={options}
speed={4}
height={20}
width={40}
eventListeners={[
{
eventName: "complete",
callback: () => {
playing.current = false;
},
},
]}
/>

Kapture 2024-01-20 at 22.06.53.gif


修改Lottie


Lottie json中,描述整个动画的过程以及效果其实对应的就是某个值。在实现的过程中,其实开发是可以去修改这些值的。比如说我们可以修改上面开关的边框颜色以及小球的颜色。


首先在页面中找到小球对应的颜色是rgb(99, 102, 241)


image.png


Lottie JSON文件中,颜色信息通常出现在表示图层样式的字段中。常见的字段是 "c"(color)
"c" 字段表示颜色,通常以RGBA格式(红绿蓝透明度)存储。例如:


"c": {"a":0,"k":[1,0,0,1]}

这表示红色,RGBA值为 [1, 0, 0, 1]


rgb(99, 102, 241)转成上面的写法那就是"c": {"a":0,"k":[99/255,102/255,241/255,1]}。以99/255为例,结果是0.38823529411764707,那么就拿这个结果去json文件中找到对应的节点。


image.png


对应有2个结果,就是小球的颜色以及边框的颜色。当我们找到这个值的时候,如果我们想修改这个值,就必须知道这个值的路径,在一个Lottie中,想肉眼找到这个值的路径是一件很难的事情。所以我们写一个辅助函数:


const updateJsonValue = (json, targetValue, newValue) => {
const find = (json, targetValue, currentPath = []) => {
for (const key in json) {
if (json[key] === targetValue) {
return [...currentPath, key];
} else if (typeof json[key] === "object" && json[key] !== null) {
const path = find(json[key], targetValue, [...currentPath, key]);
if (path) {
return path;
}
}
}
};
const res = JSON.parse(JSON.stringify(json));
const path = find(res, targetValue);
let current = res;

for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
current = current[key];
}

const lastKey = path[path.length - 1];
current[lastKey] = newValue;

return json;
};

上面的辅助函数就帮助我们找到这个值的路径,并修改目标值。比如说我们想把目前的颜色改成绿色(rgb(25, 195, 125)),就可以找到对应的路径,并修改。别忘了替换的时候把rgb对应的值除以255


let newAnimationData = updateJsonValue(animationData, 0.388235300779, 0.09803921568627451)
newAnimationData = updateJsonValue(newAnimationData, 0.388235300779, 0.09803921568627451)
newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
newAnimationData = updateJsonValue(newAnimationData, 0.40000000596, 0.7647058823529411)
newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)
newAnimationData = updateJsonValue(newAnimationData, 0.945098042488, 0.49019607843137253)

image.png


掌握了这种方式之后,我们就能修改Lottie里面的大部分内容,包括文案、资源图片、颜色等等。


最后


以上就是一些Lottie的使用以及修改的介绍,下次再遇到比较麻烦的动画需求。就可以跟产品说:可以做,让UI给我导出一个Lottie


image.png


如果你有一些别的想法,欢迎评论区交流~如果你觉得有意思的话,点点关注点点赞吧~


作者:可乐鸡翅kele
来源:juejin.cn/post/7325717778597773348
收起阅读 »

从‘相信前端能做一切’到‘连这个都做不了么’

web
帮助阅读 此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了 需求 h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比...
继续阅读 »

4711705568245_.pic.jpg


帮助阅读


此篇文章主要是为了实现仪表盘功能,前后过了4种方案,每篇方案从逻辑、代码、效果、问题四个方面出发。最后个人总结。同时非常非常希望有大佬能够提供一个方案,个人实在想不到实现方案了


需求


h5页面中,做一个环形仪表盘(如下图),需要一个从0%到实际百分比的增长过渡动效
未命名.png


前提


使用前端原生Html、css、js语言实现, 不打算借助第三方插件。


最初Scheme


将UI图片作为背景,上面放一个白色div作为遮罩,再利用css3将白色div旋转,从而达到过渡效果。


代码如下:


<style>
.light-strip {
width: 500px;
height:500px;
border: 1px solid #efefef;
background-image: url('Frame 29@3x.png');
float: right;
background-size: 100% 100%;
}
.light-strip-top {
margin-top: 0%;
width: 500px;
height: 250px;
background: #fff;
transform-origin: bottom center;
/* transform: rotate 5s ; */
rotate: 0deg;
transition: all 2s ease-in-out;
}
</style>
<body onload="load()">
<div class="light-strip">
<div class="light-strip-top">

</div>
</div>
</body>
<script>
function load() {
setTimeout(() => {
document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 180deg")
}, 1000)
}
</script>

效果如下:


屏幕录制2024-01-29 13.50.58.gif


出现问题:


由于仪表盘整体大于180度,所以白色div,在最开始遮挡全部仪表盘,那么旋转一定角度后一定会覆盖UI图。


进化Scheme


根据上面出现的问题,想到与UI沟通将仪表盘改成180度效果(解决不了问题,就把问题解决掉),该方案由于改变了原型之后会导致UI过于丑,就没有进行深度测试。


超进化Scheme


根据上面两个方案的结合,想到将方案1中的白色div换成一张指针图片,利用css3旋转追针,达到过渡效果,但此方案也是改了UI效果。


代码如下:


	<style>
.light-strip {
width: 500px;
height:500px;
border: 1px solid #efefef;
background-image: url('Frame 29@3x.png');
/* background-color: #fff; */
float: right;
background-size: 100% 100%;
}
.light-strip-top {
margin-top: 50%;
width: 49%;
height: 4px;
background: red;
transform-origin: center right;
/* transform: rotate 5s ; */
rotate: -35deg;
transition: all 2s ease-in-out;
}

</style>
<body onload="load()">
<div class="light-strip">
<div class="light-strip-top">

</div>
</div>
</body>
<script>
function load() {
setTimeout(() => {
document.querySelectorAll('.light-strip-top')[0].setAttribute('style', "rotate: 90deg")
}, 1000)
}
</script>

效果如下:


屏幕录制2024-01-29 15.44.31.gif


Now:


此时大脑宕机了,在我的前端知识基础上,想不到能够完美实现UI效果的方案了。于是和同事探讨了一下,了解到element-plus中的进度条有类似的效果,于是打算看一下源码,了解到它是svg实现的。发现新大陆又开始尝试svg实现。


究极进化Scheme


利用svg,做一个带白色的背景圆环A,再做一个带有渐变背景色的进度圆环B, 利用进度圆环的偏移值、显示长度、断口长度配合css3过渡实现过渡效果。


代码如下:


 <style>
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}

.dashboard {
position: relative;
width: 200px;
height: 200px;
background-size: 100% 100%;
}

.circle-background {
fill: none; /* 不填充 */
stroke: #fff; /* 圆环的颜色 */
stroke-width: 10; /* 圆环的宽度 */
stroke-dasharray: 200, 52; /* 圆环断开部分的长度,总长度为周长 */
stroke-dashoffset: 163;
stroke-linecap: round;
border-radius: 10;
transition: all 1s; /* 过渡效果时间 */
}

.circle-progress {
fill: none; /* 不填充 */
stroke: url(#gradient); /* 圆环的颜色 */
stroke-width: 10; /* 圆环的宽度 */
stroke-dasharray: 252, 0; /* 圆环断开部分的长度,总长度为周长 */
stroke-dashoffset: 163;
stroke-linecap: round; /* 圆滑断点 */
transition: all 1s; /* 过渡效果时间 */
}

.percentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: #3498db;
}
</style>
</head>
<body>

<svg class="dashboard" viewBox="0 0 100 100">
<!-- 定义渐变色 -->
<defs>
<linearGradient id="gradient" gradientUnits="userSpaceOnUse" x1="50" y1="0" x2="50" y2="100%">
<stop offset="0%" style="stop-color: rgba(111, 232, 191, 1)" />
<stop offset="33%" style="stop-color: rgba(255, 175, 19, 1)" />
<stop offset="70%" style="stop-color: rgba(222, 19, 80, 1)" />
<stop offset="100%" style="stop-color: rgba(133, 14, 205, 1)" />
</linearGradient>
</defs>

<!-- 背景圆环 -->
<circle class="circle-background" cx="50" cy="50" r="40"></circle>

<!-- 进度圆环 -->
<circle class="circle-progress" cx="50" cy="50" r="40"></circle>

</svg>

<!-- 进度百分比显示 -->
<div class="percentage" id="percentage">0%</div>

<script>
function setProgress(percentage) {
const circleProgress = document.querySelector('.circle-progress');
const circleBackground = document.querySelector('.circle-background');
const percentageText = document.getElementById('percentage');

const circumference = 2 * Math.PI * 40; // 圆的周长
const circumNewLength = (percentage / 100) * (circumference - 52);
const dashOffset = 163 - circumNewLength;


// 设置进度圆环的样式
circleBackground.style.strokeDashoffset = dashOffset;
circleBackground.style.strokeDasharray = `${200 - circumNewLength}, ${ 52 + circumNewLength }`
circleProgress.style.strokeDasharray = `${circumNewLength}, ${ circumference - circumNewLength }`

// 更新百分比文本
percentageText.textContent = `${percentage}%`;
}

// 设置初始进度为0%
setProgress(0);

// 模拟过渡效果,从0%到50%
setTimeout(() => {
setProgress(50);
}, 1000); // 过渡时间为1秒,你可以根据需要调整这个值
</script>


效果如下:


屏幕录制2024-01-29 15.46.35.gif


问题:


基本实现,但是还有一个问题是,渐变色是两点之间的线性渐变,无法做到圆环的顺时针渐变。


总结



  • 单纯前端不是万能的😂😂😂😂

  • 个人认为这个需求还是能够实现的

  • 希望有da lao能出个方案

  • 加油,继续搞


作者:Otway
来源:juejin.cn/post/7329310941106356275
收起阅读 »

伪指纹浏览器开发的那些事

web
什么是伪指纹浏览器开发 就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发 一、如何操作 本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心...
继续阅读 »

什么是伪指纹浏览器开发


就是通过开源的chromium浏览器进行二次简单的封装不涉及到重新编译chromium,配合puppeteer进行轻微的指纹修改开发


一、如何操作


本次操作客户端以前端擅长的electron来举例子,至于electron是什么,打开文心一言看看...


第一步下载chromium到本地客户端


登录官网,看到如下界面


image.png


可以发现箭头处指定是浏览器对应的版本buildId和系统,这里可以直接手动点击下载到本地,也可以通过@puppeteer/browsers这个库使用js代码去下载。这里说说如何使用它下载


const { app } = require('electron')
const browserApi = require('@puppeteer/browsers')
const axios = require('axios')

// browser缓存路径,避免和electron一起打包占用安装包体积和打包时间
const cacheDir = `${app.getPath('cache')}/myBrowser`

browserApi.install({
cacheDir, // 自己想要下载的路径,用来给puppeteer去调用
browser: browserApi.Browser.CHROMIUM,
// buildId: '1247373',
// baseUrl: 'https://commondatastorage.googleapis.com/chromium-browser-snapshots'
})

耐心的小伙伴肯定发现了这里buildId版本号和baseUrl下载url我打了注释,是因为@puppeteer/browsers默认下载的chromium版本比较旧,那么我们怎么获取这个最新版本buildId和baseUrl呢,还是官网那个界面打开控制台,可以看到如下请求链接


image.png
然后看到请求结果
image.png
这就是最新的buildId了,然后封装成函数调用


// 获取最新的chromium构建ID
function getLastBuildId(platform) {
return axios
.get(
`https://download-chromium.appspot.com/rev/${browserApi.BrowserPlatform.MAC}?type=snapshots`
)
.then((res) => res.data.content)
}

baseUrl可以在界面点击下载时候,看到控制台有一个请求,那就是baseUrl了


image.png
下载好后,可以去我们定义的下载保存地址,通过终端去打开就可以看到了


二、第二步启动chromium


使用puppeteer-core这个库,启动我们下好的chromium


const puppeteer = require('puppeteer-core')
const browserApi = require('@puppeteer/browsers')

// browser缓存路径
const cacheDir = `${app.getPath('cache')}/myBrowser`

// 获取安装的浏览器路径
function getBrowserPath() {
return browserApi
.getInstalledBrowsers({ cacheDir })
.then((list) => list[0]?.executablePath)
}

// 浏览器生成
const createBrowser = async (proxyServer, userAgent) => {
const browser = await puppeteer.launch({
args: [
`--proxy-server=${proxyServer}`,
`--user-agent="${userAgent}"`,
'--no-first-run',
'--no-zygote',
'--disable-extensions',
'--disable-infobars',
'--disable-automation',
'--no-default-browser-check',
'--disable-device-orientation',
'--disable-metrics-reporting',
'--disable-logging'
],
headless: false,
defaultViewport: null,
ignoreHTTPSErrors: true,
ignoreDefaultArgs: [
'--enable-infobars',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--enable-automation',
'about:blank'
],
executablePath: await getBrowserPath()
})

return browser
}

通过puppeteer.launch启动一个浏览器,至于启动参数这里我只说指纹相关的两个参数--proxy-server--user-agent,其他AI一下。


--proxy-server代理服务,浏览器访问的出口IP,即你用自己启动的浏览器访问google时候,那边服务端获取的ip就是你的代理ip,测试时候可以自己在另外一台机器上装个Squid测试。--user-agent即浏览器的window.navigator.userAgent,简单指纹一般都是依赖于它生成


三、开发过程中用到的功能点


看完puppeteer官网,我们知道操作chromium依赖于一套协议chromedevtools.github.io/devtools-pr…


3.1 更换dock图标


比如多开浏览器,我如何更换chromium的桌面dock图标,去标识这是我启动的第几个浏览器。我们可以使用Browser.setDockTile去操作浏览器更换dock图标


const pages = await browser.pages()
const page = pages[0]
const session = await pages[0].target().createCDPSession()
await session.send('Browser.setDockTile', {
image: new Buffer.from(fs.readFileSync(file)).toString('base64')
})

效果如下:


image.png


更多的协议操作需要自己摸索了,提示下,AI搜索chrome cdp协议


3.2 增加默认书签


这里我没找到协议,直接通过类似爬虫的方式,先进入标签管理页面,直接操作js新增,也算是一个技巧性的骚操作


await page.goto('chrome://bookmarks/') // 进入标签管理页面
await page.evaluate(async () => {
// 类似在控制台直接操作一样,下面的代码控制台一样可以达到效果
const defaultBookmarks = [
{
title: "文心一言",
url: "https://yiyan.baidu.com/",
},
{
title: "掘金",
url: "https://juejin.cn/",
},
];

defaultBookmarks.forEach((item) => {
chrome.bookmarks.create({
parentId: "1",
...item,
});
});
});
await page.goto('自己的本来要跳的首页')

3.3 如何使用已经打开的浏览器


const browserWSEndpoint = browser.wsEndpoint() // 获取本次打开的浏览器链接,留作下一次使用
// 保存下来, 比如直接存在一个变量map中,给它定义一个唯一的browserId,下一次好直接获取
browserMap.set(browserId, browserWSEndpoint)

...
// 再次打开新页面,要用到上一次打开的浏览器
const browser = puppeteer.connect({
...launchOptions, // 和自己首次打开浏览器的配置一样
browserWSEndpoint: browserMap.set(browserId)
})

这样就可以使用之前打开的浏览器打开网页了


3.4 如何把浏览器的信息显示在网页上


比如代理、userAgent、地区、浏览器名称等信息,先写个页面,然后轮询从localStorage直到获取信息为止。


// 浏览器代理信息页
await page.goto('浏览器信息页')
// 设置localStorage
await page.evaluate(
(values) => {
window.localStorage.setItem('browserInfo', values)
},
JSON.stringify(browserData)
)

page在打开页面后,并不会在页面中马上能获取到这里注入的browserInfo,可以通过轮询方式去扫描localStorage中是否存在我们注入的变量,这里举个react中的例子,在页面ready后去轮询处理


useEffect(() => {
let loopId = null
const clearLoop = () => {
loopId && clearTimeout(loopId)
}

// 轮询直到获取browserInfo
const loop = () => {
loopId = setTimeout(() => {
const localData = window.localStorage.getItem('browserInfo')
if (localData) {
Promise.resolve()
.then(() => {
setInfo(JSON.parse(localData))
})
.catch(() => {
message.error('获取浏览器信息失败')
})
} else {
loop()
}
}, 1500)
}

loop()

return () => {
clearLoop()
}
})

3.5 校验代理


一般的代理服务为了不让别人也能用都会加上账密校验,所以我们还需要在启动后,调用方法去校验


// 校验proxy
if (proxyData.proxyServer) {
await page.authenticate({
username: proxyData.proxyUser,
password: proxyData.proxyPwd
})
}

四、遇到了哪一些问题


4.1 mac下关闭浏览器关不掉


当我们点击左上角关闭浏览器按钮或者是关闭所有页面时候,底部的dock中依旧存在着,我们不希望像mac其他软件一样保留在dock中,不然下一次打开浏览器时候,会出现相同标识的浏览器,可以这么解决


// 每次页面关闭时候,查看浏览器是不是还有页面了,没有就关闭
browser.on('targetdestroyed', async () => {
const pages = await browser.pages()
if (!pages.length) {
await browser.close()
}
})

4.2 当我们之间关闭电脑屏幕时候,比如盖上电脑,再次打开时候,关闭不了浏览器


打上log,可以发现熄屏时候,会触发puppeteer定义的browser的disconnected事件,但是再次打开电脑时候浏览器是可以正常使用的,也就是说,puppeteer和我们打开的chromium断连了,所以我们需要在disconnected事件里再此尝试链接下chromium,如果不行才认为是浏览器被关闭了


browser.on('disconnected', () => {
const cacheData = browserMap.get(browserId)
puppeteer
.connect({
...launchOptions,
browserWSEndpoint: cacheData.browserWSEndpoint
})
.then((newBrowser) => {
browser = newBrowser
log.info(
'browser disconnected but browser is exist',
)
initEvent()
})
.catch((err) => {
log.info(
'browser disconnected success',
)
})
})

结语


puppeteer很强大,chromium也强大,就是那个官网文档啊,写的真是让人...,所以多问问AI吧


作者:柠檬阳光
来源:juejin.cn/post/7327642905245433891
收起阅读 »

【干货】一文掌握JavaScript检查对象空值的N种技巧!

在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:防止空引用错误:当我们尝试访问或使用一个空对象时,可能...
继续阅读 »

在开发 JavaScript 应用程序时,经常需要检查对象是否为空。这是因为在处理和操作对象数据时,我们需要确保对象包含有效的值或属性。以下是一些常见情况,我们需要检查 JavaScript 对象是否为空:

  1. 防止空引用错误:当我们尝试访问或使用一个空对象时,可能会导致空引用错误(如 TypeError: Cannot read property ‘x’ of null)。通过检查对象是否为空,我们可以避免这些错误的发生,并采取相应的处理措施。
  2. 数据验证和表单提交:在表单提交之前,通常需要验证用户输入的数据是否有效。如果对象为空,表示用户未提供必要的数据或未填写表单字段,我们可以显示错误消息或阻止表单提交。
  3. 条件逻辑和流程控制:根据对象是否为空,可以根据不同的条件逻辑执行不同的操作或采取不同的分支。例如,如果对象为空,可以执行备用的默认操作或返回默认值。
  4. 数据处理和转换:在处理对象数据之前,可能需要对其进行处理或转换。如果对象为空,可以提前终止或跳过数据处理逻辑,以避免不必要的计算或错误发生。
  5. 用户界面交互和显示:在用户界面中,可能需要根据对象的存在与否来显示或隐藏特定的界面元素、更改样式或呈现不同的内容。

通过检查 JavaScript 对象是否为空,可以增加应用程序的健壮性、提升用户体验,并避免潜在的错误和异常情况。因此,检查对象是否为空是编写高质量代码的重要部分。

在本文中,我们将讨论如何检查对象是否为空,其中包括 JavaScript 中检查对象是否为空的不同方法以及如何检查对象是否为空、未定义或为 null。

使用Object.keys()

使用Object.keys()方法可以检查对象是否为空。Object.keys(obj)返回一个包含给定对象所有可枚举属性的数组。
利用这个特性,我们可以通过检查返回的数组长度来确定对象是否为空。如果数组长度为0,则表示对象没有任何属性,即为空。
以下是一个示例代码:

javascriptCopy Codefunction isObjectEmpty(obj) {
return Object.keys(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.keys(obj)获取对象的所有可枚举属性,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。

使用Object.values()

使用Object.values()方法来检查对象是否为空,Object.values(obj)方法返回一个包含给定对象所有可枚举属性值的数组。如果返回的数组长度为0,则表示对象没有任何属性值,即为空。

以下是使用Object.values()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.values(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,我们定义了一个isObjectEmpty()函数,它接受一个对象作为参数。函数内部使用Object.values(obj)获取对象的所有可枚举属性值,并检查返回的数组长度是否为0。根据返回结果,判断对象是否为空。
请注意,Object.values()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

使用 for…in 循环

使用 for…in 循环方法是通过遍历对象的属性来判断对象是否为空。以下是一个示例代码:

javascriptCopy Codefunction isObjectEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false; // 只要有一个属性存在,就返回false表示不为空
}
}
return true; // 如果遍历完所有属性后仍然没有返回false,表示对象为空
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用 for…in 循环遍历对象的属性,如果发现任何属性,则返回false表示对象不为空;如果循环结束后仍然没有返回false,则表示对象为空,并返回true。
虽然使用 for…in 循环可以达到同样的目的,但相比起使用 Object.keys() 或 Object.values() 方法,它的实现稍显繁琐。因此,通常情况下,推荐使用 Object.keys() 或 Object.values() 方法来检查对象是否为空,因为它们提供了更简洁和直观的方式。

使用 Object.entries()

Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组。如果返回的数组长度为0,则表示对象没有任何属性,即为空。
以下是使用Object.entries()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.entries(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.entries(obj)获取对象的键值对数组,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
请注意,Object.entries()方法是ES2017(ES8)引入的新方法,因此在一些旧版本的JavaScript引擎中可能不被支持。在使用之前,请确保你的环境支持该方法或使用适当的polyfill来提供支持。

使用 JSON.stringify()

使用 JSON.stringify() 方法来检查对象是否为空的方法是将对象转换为 JSON 字符串,然后检查字符串的长度是否为 2。当对象为空时,转换后的字符串为 “{}”,长度为 2。如果对象不为空,则转换后的字符串长度会大于 2。
以下是使用 JSON.stringify() 方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return JSON.stringify(obj) === "{}";
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上述示例中,isObjectEmpty() 函数接受一个对象作为参数。函数内部使用 JSON.stringify(obj) 将对象转换为 JSON 字符串,然后将转换后的字符串与 “{}” 进行比较。如果相等,则表示对象为空。
需要注意的是,这种方式只适用于纯粹的对象,并且不包含任何非原始类型属性(如函数、undefined 等)。如果对象中包含了非原始类型的属性,那么转换后的 JSON 字符串可能不为空,即使对象实际上是空的。

E6使用Object.getOwnPropertyNames()

在ES6中,你可以使用Object.getOwnPropertyNames()方法来检查对象是否为空,但需要注意的是,该方法返回一个数组,其包含给定对象中所有自有属性(包括不可枚举属性,但不包括使用 symbol 值作为名称的属性)。
以下是使用Object.getOwnPropertyNames()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Object.getOwnPropertyNames(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertyNames(obj)获取对象的所有属性名,并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
请注意,Object.getOwnPropertyNames()方法返回的数组只包含对象自身的属性,不包括继承的属性。如果你需要检查继承的属性,请使用for…in循环或其他方法。同样,Object.getOwnPropertyNames()方法在ES5中引入,因此在一些旧版本的JavaScript引擎中可能不被支持。

ES6使用Object.getOwnPropertySymbols()方法

在ES6中,可以使用Object.getOwnPropertySymbols()方法来检查对象是否为空。该方法返回一个数组,其中包含了给定对象自身的所有符号属性。
以下是使用Object.getOwnPropertySymbols()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
const symbols = Object.getOwnPropertySymbols(obj);
const hasSymbols = symbols.length > 0;
return !hasSymbols;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const symbol = Symbol("key");
const obj2 = { [symbol]: "value" };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Object.getOwnPropertySymbols(obj)获取对象的所有符号属性,并将它们存储在symbols数组中。然后,通过检查symbols数组的长度是否大于0来判断对象是否具有符号属性。如果symbols数组的长度为0,则表示对象没有任何符号属性,即为空。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里即可免费学习!

注意,Object.getOwnPropertySymbols()方法只返回对象自身的符号属性,不包括其他类型的属性,例如字符串属性。如果你想同时检查对象的字符串属性和符号属性,可以结合使用Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()方法。

ES6使用Reflect.ownKeys()

在ES6中,你可以使用Reflect.ownKeys()方法来检查对象是否为空。Reflect.ownKeys()返回一个包含了指定对象自身所有属性(包括字符串键和符号键)的数组。
以下是使用Reflect.ownKeys()方法来检查对象是否为空的示例代码:

function isObjectEmpty(obj) {
return Reflect.ownKeys(obj).length === 0;
}

// 测试对象是否为空
const obj1 = {};
console.log(isObjectEmpty(obj1)); // true

const symbol = Symbol("key");
const obj2 = { [symbol]: "value" };
console.log(isObjectEmpty(obj2)); // false

在上面的示例中,isObjectEmpty()函数接受一个对象作为参数。函数内部使用Reflect.ownKeys(obj)获取对象的所有自身属性名(包括字符串键和符号键),并检查返回的数组长度是否为0。如果数组长度为0,则表示对象没有任何属性,即为空。
Reflect.ownKeys()方法提供了一种统一的方式来获取对象的所有键,包括字符串键和符号键。因此,使用Reflect.ownKeys()方法可以更全面地检查对象是否为空。

使用lodash库的isEmpty()函数

如果您使用了lodash库,可以使用其提供的isEmpty()函数来直接判断对象是否为空。
以下是使用 Lodash 的 isEmpty() 函数进行对象空检查的示例代码:

// 导入Lodash库
const _ = require('lodash');

// 检查对象是否为空
const obj1 = {};
console.log(_.isEmpty(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log(_.isEmpty(obj2)); // false

在上述示例中,_.isEmpty() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
使用 Lodash 的 isEmpty() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

使用jQuery库的$.isEmptyObject()函数

要使用 jQuery 库中的 $.isEmptyObject() 函数来检查 JavaScript 对象是否为空,首先确保已经安装了 jQuery 库,并将其导入到你的项目中。
以下是使用 jQuery 的 $.isEmptyObject() 函数进行对象空检查的示例代码:

// 导入jQuery库
const $ = require('jquery');

// 检查对象是否为空
const obj1 = {};
console.log($.isEmptyObject(obj1)); // true

const obj2 = { name: "John", age: 25 };
console.log($.isEmptyObject(obj2)); // false

在上述示例中,$.isEmptyObject() 函数接受一个对象作为参数,并返回一个布尔值表示对象是否为空。如果对象为空,则返回 true;否则返回 false。
使用 jQuery 的 $.isEmptyObject() 函数可以更方便地进行对象空检查,同时处理了各种情况,包括非原始类型的属性、数组、字符串等。

检查对象是否为空、未定义或为 null

要同时检查对象是否为空、未定义或为 null,你可以使用以下函数来进行判断:

function isObjectEmptyOrNull(obj) {
return obj === undefined || obj === null || Object.getOwnPropertyNames(obj).length === 0;
}

在上述代码中,isObjectEmptyOrNull函数接收一个对象作为参数。它首先检查对象是否为 undefined 或者 null,如果是,则直接返回 true 表示对象为空或者未定义。如果对象不是 undefined 或者 null,则使用 Object.getOwnPropertyNames() 方法获取对象的所有自身属性名,然后判断属性名数组的长度是否为 0。如果属性名数组长度为 0,则表示对象没有任何属性,即为空。
下面是一个示例用法:

const obj1 = {};
console.log(isObjectEmptyOrNull(obj1)); // true

const obj2 = null;
console.log(isObjectEmptyOrNull(obj2)); // true

const obj3 = { name: "John", age: 25 };
console.log(isObjectEmptyOrNull(obj3)); // false

const obj4 = undefined;
console.log(isObjectEmptyOrNull(obj4)); // true

总结和比较

在本文中,我们介绍了多种方法来检查 JavaScript 对象是否为空。下面是这些方法的优缺点总结:

  • 使用 Object.keys() 方法

优点:简单易用,不需要依赖第三方库。
缺点:无法处理非原始类型的属性,如函数、undefined 等。

  • Object.values()

优点:能够将对象的属性值组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:无法直接判断对象是否为空,只提供了属性值的数组。

  • 使用 for…in 循环遍历对象

优点:可以处理非原始类型的属性。
缺点:代码较为冗长,需要手动判断每个属性是否为对象自身属性。

  • 使用 JSON.stringify() 方法

优点:可以处理非原始类型的属性,并且转换后的字符串长度为 2 表示对象为空。
缺点:当对象包含循环引用时,将抛出异常。

  • Object.entries()

优点:能够将对象的键值对组成一个数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了键值对数组。

  • Object.getOwnPropertyNames()

优点:能够返回对象自身的所有属性名组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了属性名数组。

  • Object.getOwnPropertySymbols()

优点:能够返回对象自身的所有 Symbol 类型的属性组成的数组,可以通过判断该数组的长度来判断对象是否为空。
缺点:仅针对 Symbol 类型的属性,无法判断其他类型的属性是否为空。

  • Reflect.ownKeys()

优点:能够返回对象自身的所有属性(包括字符串键和 Symbol 键)组成的数组,包括可枚举和不可枚举的属性,可以通过判断该数组的长度来判断对象是否为空。
缺点:同样无法直接判断对象是否为空,只提供了所有键的数组。

  • 使用 Lodash 库的 isEmpty() 函数

优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
缺点:需要依赖第三方库。

  • 使用 jQuery 库的 $.isEmptyObject() 函数

优点:可以处理各种情况,包括非原始类型的属性、数组、字符串等。
缺点:需要依赖第三方库。

总体来说, 这些方法都提供了一种间接判断对象是否为空的方式,即通过获取对象的属性、属性值或键值对的数组,并判断该数组的长度。然而,它们并不能直接告诉我们对象是否为空,因为它们只提供了属性、属性值或键值对的信息。因此,在使用这些方法判断对象是否为空时,需要结合其他判断条件来综合考虑。

收起阅读 »

JS逐页转pdf文件为图片格式

web
背景 年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片 不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以...
继续阅读 »

背景


年前的时候,开发一个电子杂志项目,功能需求是通过上传pdf文件,将其转为图片格式,所以杂志的内容其实就是一张张图片


不过当时技术要求用后端实现,所以使用的是PHP实现该功能。项目完成后,寻思着在前端是否也能实现pdf转图片的功能。一番研究后,果真可行。以下就分享如何通过前端js将pdf文件转为图片格式,并且支持翻页预览、以及图片打包下载


效果预览


图片

所需工具



  1. pdf.js(负责API解析,可将pdf文件渲染成canvas实现预览)

  2. pdf.worker.js(负责核心解析)

  3. jszip.js(将图片打包成生成.zip文件)

  4. Filesaver.js(保存下载zip文件)


工具下载


一、pdf.js及pdf.worker.js下载地址:


mozilla.github.io/pdf.js/gett…


1.选择稳定版下载


图片


2.解压后将bulid中的pdf.js及pdf.worker.js拷贝到项目中


图片


二、jszip.js及Filesaver.js下载地址:

stuk.github.io/jszip/


1.点击download.JSZip


图片


2.解压后将dist文件夹下的jszip.js文件以及vendor文件夹下的FileSaver.js文件拷贝到项目中


图片


至此,所需工具已齐全。以下直接附上项目完整代码(代码可直接复制使用,查看效果。 对应的文件需自行下载引入)


源代码: 嫌麻烦的小伙伴可以直接在公众号后回复: pdf转图片


代码实现


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>PDF文件转图片</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="js/pdf.js"></script>
<script type="text/javascript" src="js/pdf.worker.js"></script>
<script type="text/javascript" src="js/jszip.js"></script>
<script type="text/javascript" src="js/FileSaver.js"></script>
<style type="text/css">

button {
width: 120px;
height: 30px;
background: none;
border: 1px solid #b1afaf;
border-radius: 5px;
font-size: 12px;
font-weight: 1000;
color: #384240;
cursor: pointer;
outline: none;
margin: 0 0.5%
}

button:hover {
background: #ccc;
}

#container {
width: 600px;
height: 780px;
margin-top: 1%;
border-radius: 2px;
border: 2px solid #a29b9b;
}

.pdfInfos {
margin: 0 2%;
}
</style>
</head>

<body>

<div style="margin-top:1%">
<button id="prevpage">上一页</button>
<button id="nextpage">下一页</button>
<button id="exportImg">导出图片</button>
<button onclick="choosePdf()">选择一个pdf文件</button>
<input style="display:none" id='chooseFile' type='file' accept="application/pdf">
</div>

<div style="margin-top:1%">
<span class="pdfInfos">页码:<span id="currentPages"></span><span id="totalPages"></span></span>
<span class="pdfInfos">文件名:<span id="fileName"></span></span>
<span class="pdfInfos">文件大小:<span id="fileSize"></span></span>
</div>

<div style="position: relative;">
<div id="container"></div>
<img id="imgloading" style="position: absolute;top: 20%;left: 2%;display:none" src="loading.gif">
</div>

</body>


<script>

var currentPages,totalPages //声明一个当前页码及总页数变量
var scale = 2; //设置缩放比例,越大生成图片越清晰

$('#chooseFile').change(function() {
var pdfFilePath = $('#chooseFile').val();
if(pdfFilePath) {

$("#imgloading").css('display','block');
$("#container").empty(); //清空上一PDF文件展示图

currentPages=1; //重置当前页数
totalPages=0; //重置总页数

var filesdata = $('#chooseFile')[0].files; //jquery获取到文件 返回属性的值
var fileSize = filesdata[0].size; //文件大小
var mb;

if(fileSize) {
mb = fileSize / 1048576;
if(mb > 10) {
alert("文件大小不能>10M");
return;
}
}

$("#fileName").text(filesdata[0].name);
$("#fileSize").text(mb.toFixed(2) + "Mb");

var reader = new FileReader();
reader.readAsDataURL(filesdata[0]); //将文件读取为 DataURL
reader.onload = function(e) { //文件读取成功完成时触发

pdfjsLib.getDocument(this.result).then(function(pdf) { //调用pdf.js获取文件
if(pdf) {
totalPages = pdf.numPages; //获取pdf文件总页数
$("#currentPages").text("1/");
$("#totalPages").text(totalPages);

//遍历动态创建canvas
for(var i = 1; i <= totalPages; i++) {
var canvas = document.createElement('canvas');
canvas.id = "pageNum" + i;
$("#container").append(canvas);
var context = canvas.getContext('2d');
renderImg(pdf,i,context);
}

}
});

};
}
});

//渲染生成图片
function renderImg(pdfFile,pageNumber,canvasContext) {
pdfFile.getPage(pageNumber).then(function(page) { //逐页解析PDF
var viewport = page.getViewport(scale); // 页面缩放比例
var newcanvas = canvasContext.canvas;

//设置canvas真实宽高
newcanvas.width = viewport.width;
newcanvas.height = viewport.height;

//设置canvas在浏览中宽高
newcanvas.style.width = "100%";
newcanvas.style.height = "100%";

//默认显示第一页,其他页隐藏
if (pageNumber!=1) {
newcanvas.style.display = "none";
}

var renderContext = {
canvasContext: canvasContext,
viewport: viewport
};

page.render(renderContext); //渲染生成
});

$("#imgloading").css('display','none');

return;
};

//上一页
$("#prevpage").click(function(){

if (!currentPages||currentPages <= 1) {
return;
}

nowpage=currentPages;
currentPages--;

$("#currentPages").text(currentPages+"/");

var prevcanvas = document.getElementById("pageNum"+currentPages);
var currentcanvas = document.getElementById("pageNum"+nowpage);
currentcanvas.style.display = "none";
prevcanvas.style.display = "block";

})

//下一页
$("#nextpage").click(function(){

if (!currentPages||currentPages>=totalPages) {
return;
}

nowpage=currentPages;
currentPages++;

$("#currentPages").text(currentPages+"/");

var nextcanvas = document.getElementById("pageNum"+currentPages);
var currentcanvas = document.getElementById("pageNum"+nowpage);
currentcanvas.style.display = "none";
nextcanvas.style.display = "block";

})

//导出图片
$("#exportImg").click(function() {

if (!$('#chooseFile').val()) {
alert('请先上传pdf文件')
return false;
}

$("#imgloading").css('display','block');

var zip = new JSZip(); //创建一个JSZip实例
var images = zip.folder("images"); //创建一个文件夹用来存放图片

//遍历canvas,将其生成图片放进文件夹images中
$("canvas").each(function(index, ele) {
var canvas = document.getElementById("pageNum" + (index + 1));

//将图片放进文件夹images中
//参数1为图片名称,参数2为图片数据(格式为base64,需去除base64前缀 data:image/png;base64)
images.file("" + (index + 1) + ".png", splitBase64(canvas.toDataURL("image/png", 1.0)), {
base64: true
});

})

//打包下载
zip.generateAsync({
type: "blob"
}).then(function(content) {
saveAs(content, "picture.zip"); //saveAs依赖的js文件是FileSaver.js
$("#imgloading").css('display','none');
});

});

//截取base64前缀
function splitBase64(dataurl) {
var arr = dataurl.split(',')
return arr[1]
}

function choosePdf(){
$("#chooseFile").click()
}
</script>
</html>

项目实现原理分析



  1. 首先利用pdf.js将上传的pdf文件转化成canvas

  2. 然后使用jszip.js将canvas打包图片生成.zip文件

  3. 最后使用Filesaver.js将zip文件保存下载


项目注意要点



  1. 由于pdf文件是通过上传的,因此需要通过js的FileReader()对象将其读取为DataURL,pdf.js文件才可读取渲染

  2. JSZip对象的.file()函数中第二个参数传入的是base64格式图片,但是要去掉base64前缀标识


作者:程序员Winn
来源:juejin.cn/post/7238442926334918711
收起阅读 »

JSON.parse记录一次线上bug排查

web
最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。 现状 首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮 跳...
继续阅读 »

最近项目中有一个匪夷所思的问题,业务在使用的时候,偶发性的会白屏,经常下班的时候骚扰我们,开发苦不堪言,经过长达一周的排查,仍然没有查到bug的存在,最终尝试通过添加埋点日志,记录关键信息。


现状


首先讲述一下现状,首先业务进入后,页面可以认为有两个按钮



  • 跳转共享链接

  • 打开表单弹窗按钮,点击后展示表单。


image-20240124132744181


操作顺序是,页面加载后,先点击跳转共享链接,看完链接后再返回点击表单弹窗。



里面有两个重要的时间节点,一个是跳转链接之前,一个是返回到当前页面。




  • 跳转链接之前



    • 需要存储接口数据,接口数据包含了表单的数据



  • 返回当前页面



    • 请求接口数据



      • 本地缓存无,直接使用接口数据

      • 本地缓存有,缓存和接口数据合并,接口数据优先






image-20240124132901079


返回页面的时候,点击表单弹窗


正常上来说弹窗能够正常显示,但是线上环境再点击 展示弹窗的按钮导致白屏了。整个流程如下


image-20240124133213958


初步判断是整合缓存和接口数据问题,于是需要给页面添加两个埋点



  • 页面报错异常时上报

  • 点击打开表单的时,上报缓存数据和聚合之后的数据。



    • 为什么不上报接口数据呢?因为当时修复bug比较紧急,观察代码发现接口直接返回的数据没有在公共变量中存储,如果需要存储改动较大,还有就是接口数据也可以从后端日志去排查




页面报错异常上报


异常上报的方法有很多,通常使用一个gif图片,地址为get的请求地址+上报信息,具体的可以自行百度,此处简单叙述下


使用图片是因为加载资源里面img优先级比较低,不会阻塞其他资源,而且图片请求不会跨域,用gif是因为对比图片类型他是比较小的


//utils/utils.js
/**
* 异常上报方法
* 希望抽离出来同步异常类和异步异常类
*/

function uploadError() {
 //上报处理参数
 const upload = errObj =>{
   const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
   //将obj拼接成url
   const queryStr = Object.entries(errObj)
      .map(([key, value]) => `${key}=${value}`)
      .join('&');
     const oImg = new Image();
     oImg.src = logUrl + '?' + encodeURIComponent(queryStr);
}
 //同步方法
 function handleError(e) {
   try {
     let baseInfo = localStorage.getItem('base_info'); // 域账户
     let masterName = baseInfo ? JSON.parse(baseInfo)?.master_name : ''; // 域账户
     let errObj = {
       masterName: masterName,//域账户
       url: window.location.href,//报错的路由,利于排查
       reason: JSON.stringify({
         message: e?.error?.message, //报错信息
         stack: e?.error?.stack,//调用栈
      }),
       message: e?.message, //报错信息
    };
     upload(errObj)
     console.log('error', errObj);
  } catch (err) {
     console.log('error', err);
  }
}
 window.addEventListener('error', handleError);//调用监听
}

//app.js
//异常上报方法 开发环境禁止上报
if(!['dev'].includes(process.env.BUILD_ENV)){
 uploadError()
}

点击弹窗的异常上报


//打开弹窗的操作  
const open = () => {
   setShow(!show);//控制表单的展示隐藏
   if(!show){
     const logUrl = 'https://xxx.xxx.com/log.gif'; // 上报接口
     const oImg = new Image();
     let initFormVal = localStorage.getItem('initFormVal' + query?.id);
     oImg.src = logUrl + '?' + encodeURIComponent(`initFormVal=${initFormVal}&integratedData=${JSON.stringify(integratedData)}`);
  }
};
//initFormVal为缓存中的数据 integratedData为整合后的数据

发现问题原因


通过添加以上异常上报,业务员进行操作时,又出现了白屏,此时根据业务员token与上报关键字与时间查到了相关日志,其中日志中记录的是


https://xxx.xxx.com/log.gif?initFormVal=&integratedData=null

integratedData是后端接口数据和缓存的融合呀!通过查日志发现当时后端确确实实返回正常的响应了,不可能为null,同时还有一个疑问浮出水面,为什么initFormVal没有值,而不是null


正常来说如果initFormVal从json中取值时,取不到应该默认就是null,此处为'',只说明一个问题,缓存的时候给他赋值了


那么问题大致可以定位到以下两个操作节点



  • 缓存时

  • 返回页面后,缓存和接口数据融合时


//缓存时操作  
const getFormValues = () => {
   let formVal = childRef?.current?.getFormVal() || '';
localStorage.setItem('initFormVal' + query.id, JSON.stringify(formVal));
};

缓存时,如果子节点获取不到,那么childRef?.current?.getFormVal()就为undefind,又由于使用了或运算符,那么此时存储的是'',那么取这个暂时看也没问题呀,然后也写入了缓存



更严格来讲,应该先判断formVal是否存在然后再去缓存,没有就不缓存。



再看一下返回页面,数据融合的代码


const getDataFn = url => {
   dispatch({
     type: url,
     payload: { id: query.id },
     callback: res => {
       if (res.ret === 1) {
         let initFormVal = localStorage.getItem('initFormVal' + query?.id);
         console.log('initFormVal', JSON.parse(initFormVal));
         let cacheFormVal = {};
         
         if (initFormVal) {
           //initFormVal赋值给cacheFormVal,此处省略
        }
         setPricingInfo({
           ...cacheFormVal,
           ...res.data
        });
      }  

发现有一个console.log(),JSON.parse('')会是什么?报错,果然,查异常上报日志的时候,也查到这个错误,真是一失足成千古恨,当时只是为了方便查看,打印了一下缓存数据,没想到是这个地方出现的问题 Uncaught SyntaxError: Unexpected end of JSON input


image-20240124142222982


JSON.parse


那问题来了 json.parse什么情况会报错呢?通过查阅MDN


image-20240124143007732


那么,什么是规范的JSON格式呢?我们此处再去查阅MDN


此处只列出了json的结构 很显然,传入null 是合法的,但是传入空字符是不合法的,


JSON = null
   or true or false
   or JSONNumber
   or JSONString
   or JSONObject
   or JSONArray

吐槽


可能有人要吐槽,直接写JSON存储的时候格式不对不就行了吗?干什么这那么多,又是异常上报,又是贴代码?又是贴MDN的。


我在这里回答一下之所以这么写一是为了记录出错的时候出现的问题,方便下次出现类似问题能够即时复盘。


二是希望贴出自己的排错方式,新手若有不明白的可以模仿这个方式得到一些启发和思考,高手也可指出我的问题,共同成长


同样我也希望大家遇到问题的时候要记得查文档,查文档再查文档,自己遇到的问题,先文档,是不是自己理解错了,如果还不行就去stackoverflow,如果再不济就去github issue看看是否有相同的问题是不是作者的bug,如果都没有,那么好了,这个问题几乎解决不了了,此时有两个选择,要么产品接受,要么 那我走???


作者:傲娇的腾小三
来源:juejin.cn/post/7327227246618476583
收起阅读 »

相见恨晚的前端开发利器-PageSpy

web
今天介绍一个非常有用的前端开发工具。 做前端开发的你,一定有过以下经历: 我这里是好的啊,你截个图给我看看 不会吧,你打开f12,控制台截个图给我看看 录个屏给我看看你是怎么操作的 ... 还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测...
继续阅读 »

今天介绍一个非常有用的前端开发工具。


做前端开发的你,一定有过以下经历:



  1. 我这里是好的啊,你截个图给我看看

  2. 不会吧,你打开f12,控制台截个图给我看看

  3. 录个屏给我看看你是怎么操作的

  4. ...


还有,我们在开发h5的时候,一般为了调试方便,可能会在开发环境和测试环境实例化一个vConsole,遇到问题看一下大概就能定位到错误。


可是如果测试小姐姐在远程呢?如果是线上环境呢?如果有这么一个工具,能让我坐在工位上就能调(窥)试(探)用户的操作,那岂不是美滋滋。


你可能会说,这不就是埋点吗,先别急,今天介绍的这个工具和埋点有着本质区别。


不啰嗦了,有请主角**「PageSpy」**登场。


PageSpy是什么?




PageSpy[1] 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。




有什么作用?



  • 一眼查看客户端信息 能识别客户端运行环境,支持Linux/Mac/Window/IOS/Android

  • 实时查看输出 可以实时输出客户端的Element,Console,Network,Storage

  • 网络请求监控 可以捕获和显示页面的网络请求

  • 远程控制台 支持远程调试客户机上的js代码


如何使用?


查看官方文档[2]



  1. 安装npm包


yarn global add @huolala-tech/page-spy-api

# 如果你使用 npm

npm install -@huolala-tech/page-spy-api


  1. 启动服务


直接在命令行执行page-spy-api,部署完成后浏览器访问:6752,页面顶部会出现接入SDK菜单,点击菜单查看如何在业务项目中配置并集成。图片命令行执行后出现这个界面表示服务启动成功了,然后访问我自己的ip+端口,再点击顶部接入SDK图片去创建一个测试项目,建一个最简单的index.html,按照文档接入SDK,然后在浏览器访问这个页面图片图片左下角出现Pagepy的logo说明引入成功了。


此时点击顶部菜单房间列表图片点击调试,就可以看到这个项目的一些实时调试信息,但是还没加什么代码。图片现在改一下我们的代码,加一些输出信息。图片Console控制台的信息图片直接输出用户端代码变量的实时的值图片加个定时器试试,也是实时输出的图片图片再来看看Storage信息图片图片Element信息图片调个接口试试图片图片图片


好了,今天的介绍就到这里,这么牛叉的工具,是不是有种相见恨晚的感觉,感兴趣的小伙伴快去试试吧!


Reference


[1] PageSpy:huolalatech.github.io/page-spy-we…


[2] 官方文档:github.com/HuolalaTech…


作者:丝绒拿铁有点甜
来源:juejin.cn/post/7327691403844665380
收起阅读 »

uniapp云开发--微信登录

web
前言 我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。 小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。 注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面...
继续阅读 »

前言


我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。


小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。


注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面



uniCloud


创建 uniapp + uniCloud 项目,创建云数据库 数据表 uniCloud传送门


开始


创建项目


39d23acf47b440e2880f5ccadc1417f9~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


关联云服务空间




创建云数据库 数据表


不使用模版,输入名称直接创建即可。



编辑表结构,想了解更多可以去看云数据库 DB Schema 数据结构文档 传送门


{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"nickName": {
"bsonType": "string",
"label": "昵称",
"description": "用户昵称,登录获取的"
},
"avatarUrl": {
"bsonType": "string",
"label": "头像",
"description": "用户头像图片的 URL,登录获取的"
},
"gender": {
"bsonType": "number",
"label": "性别",
"description": "用户性别,1: 男;2: 女"
},
"personalize": {
"bsonType": "string",
"label": "个性签名",
"description": "个性签名,编辑资料获取"
},
"background": {
"bsonType": "object",
"label": "个人中心背景图",
"description": "个人中心背景图,编辑资料获取"
},
"mp_wx_openid": {
"bsonType": "string",
"description": "微信小程序平台openid"
},
"register_date": {
"bsonType": "timestamp",
"description": "注册时间",
"forceDefaultValue": {
"$env": "now"
}
}
}
}

创建云函数




云函数代码


云函数 将 uni.login 取得的 code 获取到用户 session, 并对 数据库进行 增加、修改、查询 操作,第一次注册必须用户主动填写用户资料。


对云数据库的相关操作 传送门


'use strict';

//小程序的AppID 和 AppSecret
const mp_wx_data = {AppID: '************', AppSecret: '***********************'}

//event为客户端上传的参数
exports.main = async (event, context) => {

//使用云数据库
const db = uniCloud.database();
// 获取 `users` 集合的引用
const pro_user = db.collection('users');
// 通过 action 判断请求对象

let result = {};
switch (event.action) {
// 通过 code 获取用户 session
case 'code2Session':
const res_session = await uniCloud.httpclient.request('https://api.weixin.qq.com/sns/jscode2session', {
method: 'GET', data: {
appid: mp_wx_data.AppID,
secret: mp_wx_data.AppSecret,
js_code: event.js_code,
grant_type: 'authorization_code'
}, dataType: 'json'
}
)
const success = res_session.status === 200 && res_session.data && res_session.data.openid
if (!success) {
return {
status: -2, msg: '从微信获取登录信息失败'
}
}

//从数据库查找是否已注册过
const res_user = await pro_user.where({
mp_wx_openid: res_session.data.openid
}).get()
// 没有用户信息,进入注册
if (res_user.data && res_user.data.length === 0) {
//event.user_info 用户信息
if (event.user_info) {
//有信息则进入注册,向数据库写入数据
const register = await uniCloud.callFunction({
name: 'user',
data: {
action: 'register',
open_id: res_session.data.openid,
user_info: event.user_info
}
}).then(res => {
result = res
})
} else {
//没有信息返回{register: true}
result = {
result: {
result: {register: true}
}
}
}
} else {
result = {
result: {
result: res_user.data[0]
}
}
}
break;
//注册 向数据库写入数据
case 'register':
const res_reg = await pro_user.add({
nickName: event.user_info.nickName,
avatarUrl: event.user_info.avatarUrl,
gender: event.user_info.gender,
mp_wx_openid: event.open_id,
register_date: new Date().getTime()
})
if (res_reg.id) {
const res_reg_val = await uniCloud.callFunction({
name: 'user', data: {
action: 'getUser', open_id: event.open_id
}
}).then(res => {
result = res
})
} else {
result = {
status: -1, msg: '微信登录'
}
}
break;
case 'update':
if (event._id && event.info) {
const res_update = await pro_user.doc(event._id).update(event.info)
if (res_update.updated >= 0) {
result = {status: 200, msg: '修改成功'}
} else {
result = {status: -1, msg: '修改失败'}
}
} else {
result = {status: -1, msg: '修改失败'}
}
break;
case 'getUser':
const res_val = await pro_user.where({
mp_wx_openid: event.open_id
}).get()
return res_val.data[0]
break;
}
return result;
};

微信登录操作


如上面所说,用户需手动上传资料,对于用户头像我们需要上传至云储存。


上传用户头像


上传图片函数参数为微信本地图片路径,我们对路径用/进行分割,取最后的图片名称进行上传


/**
* 上传图片至云存储
*/

export async function uploadImage(url) {
const fileName = url.split('/')
return new Promise(resolve => {
uniCloud.uploadFile({
filePath: url,
cloudPath: fileName[fileName.length - 1],
success(res) {
resolve(res)
},
fail() {
uni.showToast({
title: '图片上传失败!',
icon: 'none'
})
resolve(false)
}
})
})
}

登录函数


如果用户第一次上传资料,我们需要先上传头像并取得图片链接,再将用户资料写入数据库。


async wxLogin() {
if (this.userInfo && this.userInfo.avatarUrl) {
uni.showLoading({
title: '正在上传图片...',
mask: true
});
//上传头像至云储存并返回图片链接
const imageUrl = await uploadImage(this.userInfo.avatarUrl)
if (!imageUrl) {
return
}
this.userInfo = {...this.userInfo, avatarUrl: imageUrl.fileID}
}
uni.showLoading({
title: '登陆中...',
mask: true
});
const _this = this
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code) {
//取得code并调用云函数
uniCloud.callFunction({
name: 'user',
data: {
action: 'code2Session',
js_code: res.code,
user_info: _this.userInfo
},
success: (res) => {
//如register为true,用户未填写资料
if (res.result.result.result.register) {
//_this.showUserInfo 显示填写资料组件
_this.showUserInfo = true
uni.hideLoading();
return
}
if (res.result.result.result._id) {
const data = {
_id: res.result.result.result._id,
mp_wx_openid: res.result.result.result.mp_wx_openid,
register_date: res.result.result.result.register_date
}
this.loginSuccess(data)
}
},
fail: () => {
this.loginFail()
}
})
}
}
})
},

登录成功与失败


在用户登录成功后将数据存入 Storage 中,添加登录过期时间,我这里设置的是七天的登录有效期。


loginSuccess(data) {
updateTokenStorage(data)
updateIsLoginStorage(true)
uni.showToast({
title: '登陆成功!',
icon: 'none'
});
uni.navigateBack()
},

将用户数据存入 Storage,并设置过期时间 expiresTime


export function updateTokenStorage(data = null) {
if (data) {
const expiresTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000
data = {...data, expiresTime: expiresTime}
}
uni.setStorageSync('user', data)
}

isLogin 用于判断是否是否登录


export function updateIsLoginStorage(data = null) {
uni.setStorageSync('isLogin', data)
}

登录失败


loginFail() {
updateTokenStorage()
updateIsLoginStorage()
uni.showToast({
title: '登陆失败!',
icon: 'none'
});
}

判断是否登录


除了判断 isLogin 还要判断 expiresTime 是否登录过期


//判断是否登陆
export function isLogin() {
try {
const user = uni.getStorageSync('user')
const isLogin = uni.getStorageSync('isLogin')
const nowTime = new Date().getTime()
return !!(isLogin && user && user._id && user.expiresTime > nowTime);
} catch (error) {

}
}

最后


至此就实现了微信登录并将用户信息存入数据库中,我们也可以通过云函数获取用户数据,做出用户个人主页。



以上是我做个人小程序时用的登录流程,整个小程序项目已上传至 GitHub。


GitHub地址


小程序码



作者:Biao
来源:juejin.cn/post/7264592481592705076
收起阅读 »

真的不考虑下grid布局?有时候真的很方便!

web
前言 flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。 宫格类的布局 比如...
继续阅读 »

前言


flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。


宫格类的布局


比如我要实现一个布局,最外层元素的宽度是1000px,高度自适应。子元素宽度为300px,一行展示3个,从左到右排列。其中最左边与最右边的元素需要紧挨父元素的左右边框。如下图所示:



使用flex实现


这个页面布局在日常开发中非常常见,考虑下使用flex布局如何实现,横向排列元素,固定宽度300,wrap设置换行显示,设置双端对齐。看起来很简单,来实现一下。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box{
width: 1000px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
}
.item{
background: pink;
width: 300px;
height: 150px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>


实现之后发现了问题,由于我们设置了双端对齐导致,当最后一行的个数不足三个时,页面展示的效果和我们预期的效果有出入。使用flex实现这个效果就要对这个问题进行额外的处理。


处理的方式有很多种,最常见的处理方式是在元素后面添加空元素,使其成为3的倍数即可。其实这里添加空元素的个数没有限制,因为空元素不会展示到页面上,即使添加100个空元素用户也是感知不到的。个人觉得这并不是一个好办法,在实际处理的时候可能还会遇到别的问题。个人觉得还是把flex下的子元素设置成百分比好一点。


使用grid实现


面对这种布局使用grid是非常方便的,设置3列,每列300px,剩下的元素让它自己往下排即可。几行代码轻松实现该效果,不需要flex那样额外的处理。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(3, 300px);
justify-content: space-between;
gap: 10px;
width: 1000px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>

</div>
</body>

</html>


实现后台管理布局



这种后台管理的布局,使用flex实现当然也没有问题。首先需要纵向排列红色的两个div,然后再横向的排列蓝色的两个div,最后再纵向的排列绿色的两个div实现布局。达到效果是没有问题的,但是实现起来较为繁琐,而且需要很多额外的标签嵌套。



由于grid是二维的,所以它不需要额外的标签嵌套。html里面结构清晰,如果需要改变页面结构,只需要改变container的样式就可以了,不需要对html进行修改。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.container {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 100px 1fr 100px;
grid-template-areas:
'header header'
'aside main'
'aside footer';
height: 100vh;
}

.header {
grid-area: header;
background: #b3c0d1;
}

.aside {
grid-area: aside;
background: #d3dce6;
}

.main {
grid-area: main;
background: #e9eef3;
}

.footer {
grid-area: footer;
background: #b3c0d1;
}
</style>
</head>

<body>
<div class="container">
<div class="header">Header</div>
<div class="aside">Aside</div>
<div class="main">Main</div>
<div class="footer">Footer</div>
</div>
</body>

</html>

实现响应式布局


借助grid的auto-fillminmax函数可以实现类似响应式布局的效果,可以应用在后台管理的表单布局等场景。



<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
justify-content: space-between;
gap: 10px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>

兼容性对比


flex的兼容性


image.png


grid的兼容性


image.png


可以看到grid在兼容性上还是不如flex,grid虽然强大,但是在使用前还是需要先考虑一下项目的用户群体。


结尾


除了上述场景外肯定还有许多场景适合使用grid来完成。gridflex都是强大的布局方式,它们并没有明显的优劣之分。关键在于掌握这两种方法,并在开发中根据实际情况选择最合适的方案。


希望大家能有所收获!


作者:欲买炸鸡同载可乐
来源:juejin.cn/post/7326816030042669110
收起阅读 »

一些不被人熟知,但又很好用的HTML属性

web
HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。 下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性 contenteditable: 这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的...
继续阅读 »

HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。
下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性


contenteditable:


这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的内容。


<div contenteditable="true">
这段内容可以被编辑。
</div>

使用场景:
-可以用来创建富文本编辑器,使用户能够在网页中创建、编辑和格式化文本,


spellcheck:


该属性用于启用或禁用元素的拼写检查功能。(如果用户输入的单词拼写有误,浏览器通常会标记出来并提供纠正建议)


<textarea spellcheck="true">
这个文本区域启用了拼写检查。
</textarea>

image.png


使用场景:



  • 可以在文章创作者的富文本编辑器中使用,辅助文章创作


代码演示:


draggable:


该属性使元素可拖动。通常与 JavaScript 结合使用,实现拖放功能。


<img src="image.jpg" draggable="true" alt="可拖动的图片">

使用场景:



  • 在电子商务网站中,用户可以拖动产品图像到购物车区域,以便快速添加商品到购物清单。

  • 在可视化数据分析工具中,用户可以通过拖拽图表或数据元素来定制自己的数据可视化图形。

  • 可以创建一个可拖放的低代码平台


代码演示:


sandbox:


与 元素一起使用,sandbox 属性限制了嵌入内容的行为,如阻止执行脚本或提交表单。

<iframe src="sandboxed-page.html" sandbox="allow-same-origin allow-scripts"></iframe>

使用场景:



  • 可以在电子邮件客户端中,通过使用 sandbox 属性限制电子邮件中嵌入内容的行为,以确保安全性并防止恶意代码执行。

  • 可以在需要嵌入第三方内容(如广告、外部应用程序等)但又需要限制其行为的情况下使用。这可以防止嵌入的内容执行恶意脚本或访问敏感信息。


download:


该属性与 <a>(锚点)元素一起使用,指定用户单击链接时应下载的目标。


<a href="document.pdf" download="my-document">下载 PDF</a>

使用场景:



  • 可用于提供下载链接,例如下载文档、图像或其他文件。这使得用户可以通过单击链接直接下载相关内容而无需离开页面。


hidden:


该属性用于隐藏页面上的元素。这是最初隐藏内容的简单方法,可以通过 CSS 或 JavaScript 在后来显示。


<p hidden>这个段落最初是隐藏的。</p>

使用场景:



  • 在网页中使用弹出式模态框或折叠式面板,可以利用 hidden 属性来最初隐藏它们,并在用户点击或触发特定事件时展现。

  • 在网页表单验证中,可以将错误消息初始隐藏,只有当用户提交表单出现错误时才显示出来。


defer:



<script defer src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


async:


类似于 defer,async 属性与

<script async src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


Accept 属性:


你可以将 accept 属性与 元素(仅适用于文件类型)一起使用,以指定服务器可以接受的文件类型。


<input type="file" accept=".jpg, .jpeg, .png">

使用场景:



  • 在上传图片的社交媒体平台中,限制用户只能上传特定格式(如 JPG、PNG)的图片文件,确保图片质量和页面加载速度。

  • 在在线应用程序中,限制用户只能上传特定类型的文件,例如在云存储服务中只允许上传文档文件。


Translate:


该属性用于指定在页面本地化时,元素的内容是否应该被翻译。


<p translate="no">这段内容不应被翻译。</p>

作者:zayyo
来源:juejin.cn/post/7303789262989443083
收起阅读 »

Celeris Web,一套女生都觉得好看的Vue3模板

web
Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板 一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的? 嗯,女生总是很喜欢漂亮的东...
继续阅读 »

Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板


一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的?


嗯,女生总是很喜欢漂亮的东西,对吧?于是我决定写一款前端开发模板,让开发出来的工具她们用起来不仅方便,还得有点美美哒。Vue 3、Unocss、NaiveUI、Monorepo,这些都是我的秘密武器。我取名它为Celeris Web


这个开发框架采用了最新的技术,包括Vue 3、Vite和 TypeScript。而且,这个项目的设计初衷就用了monorepo的方法使得依赖管理和多个项目的协作变得轻松。这可是一套为开发人员提供了构建现代Web应用程序的全面解决方案哦。


不管你是老手还是新手,Celeris Web都能给你提供一个简化的前端开发流程,利用最新的工具和技术。是不是觉得很吸引人?


Snipaste_2024-01-16_14-27-03.png


Celeris Web的特点



  • ⚡ 闪电般快速:使用Vue 3,Vite和pnpm构建 🔥

  • 💪 强类型:使用TypeScript 💻

  • 📂 单库存储:易于管理依赖项和协作多个项目 🤝

  • 🔥 最新语法:使用新的< script setup >语法 🆕

  • 📦 自动导入组件:自动导入组件 🚚

  • 📥 自动导入API:使用unplugin-auto-import直接导入Composition API和其他API 📨

  • 💡 官方路由器:使用Vue Router v4 🛣️

  • 🎉 加载反馈:使用NProgress提供页面加载进度反馈 🔄

  • 🍍 状态管理:使用Pinia进行状态管理 🗃️

  • 📜 中文字体预设:包含中文字体预设 🇨🇳

  • 🌍 国际化就绪:具备使用本地化的国际化功能 🌎

  • ☁️ Netlify准备就绪:在Netlify上零配置部署 ☁️


有了Celeris Web,你的前端开发之路将更加轻松愉快!🚀


中英文双语注释


在Celeris Web的设计中,我们注重代码的可读性和学习性,为此,我们为每个函数都配备了中英文双语注释,以确保无论您的母语是中文还是英文,都能轻松理解和学习代码。


为什么选择中英文双语注释?



  1. 全球协作: 在多语言团队中,中英文双语注释能够促进更好的沟通和协作,确保团队成员都能准确理解代码的功能和实现。

  2. 学习便捷: 对于新手来说,中英文双语注释提供了更友好的学习环境,帮助他们更快速地掌握代码的逻辑和结构。

  3. 开发者友好: 我们致力于构建一个开发者友好的开发环境,中英文双语注释是我们为实现这一目标而采取的一项关键措施。

  4. 示例:


    /**
    * 打开一个新的浏览器窗口
    * Open a new browser window
    *
    * @param {string} url - 要在新窗口中打开的 URL
    * The URL to open in the new window
    *
    * @param {object} options - 打开窗口的选项
    * Options for opening the window
    * @param {string} options.target - 新窗口的名称或特殊选项,默认为 "_blank"
    * @param {string} options.features - 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    */

    export function openWindow(url: string, { target = "_blank", features = "noopener=yes,noreferrer=yes" }: {
    target?: "_blank" | "_self" | "_parent" | "_top"; // 新窗口的名称或特殊选项,默认为 "_blank"
    features?: string; // 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    } = {}
    ) {
    window.open(url, target, features);
    }

    通过这样的中英文双语注释,我们希望为开发者提供更愉悦、更高效的编码体验,让Celeris Web成为一个真正容易上手和深入学习的前端模板。



Monorepo 设计的好处


1. 依赖管理更轻松: Monorepo 将所有项目的依赖项集中管理,避免了不同项目之间版本冲突的问题,使得整体的依赖管理更加清晰和简便。


2. 代码共享与重用: 不同项目之间可以方便地共享和重用代码,减少重复开发的工作量。这对于保持代码一致性和提高开发效率非常有利。


3. 统一的构建和部署: Monorepo 可以通过统一的构建和部署流程,简化整个开发过程,减少了配置和管理的复杂性,提高了开发团队的协作效率。


4. 统一的版本控制: 所有项目都在同一个版本控制仓库中,使得版本管理更加一致和可控。这有助于团队协同开发时更好地追踪和处理版本问题。 Monorepo设计让Celeris Web不仅是一款后台管理系统模板,同时也是一个快速开发C端产品的前端Web模板。有了Celeris Web,前端开发之路将更加轻松愉快!🚀


设计理念:突破Admin管理的局限性,关注C端用户体验


在市面上,大多数前端模板都着眼于满足B端用户的需求,为企业管理系统(Admin)提供了强大的功能和灵活的界面。然而,很少有模板将C端产品的特点纳入设计考虑,这正是我们Celeris Web的创新之处。


突破Admin管理的局限性:


传统的Admin管理系统更注重数据展示和业务管理,但C端产品更加侧重用户体验和视觉吸引力。我们深知C端用户对于界面美观、交互流畅的要求,因此Celeris Web不仅提供了强大的后台管理功能,更注重让前端界面在用户层面上达到更高水平。


关注C端用户体验:


Celeris Web不仅仅是一个后台管理系统的模板,更是一个注重C端用户体验的前端Web模板。我们致力于打破传统Admin系统的束缚,通过引入崭新的设计理念,使得C端产品在前端呈现上具备更为出色的用户体验。


特色亮点:



  • 时尚美观的UI设计: 我们注重界面的美感,采用现代化设计语言,使得Celeris Web的UI不仅仅是功能的堆砌,更是一种视觉盛宴,让C端用户爱不释手。

  • 用户友好的交互体验: 考虑到C端用户的习惯和需求,Celeris Web注重交互体验的设计,通过流畅的动画效果和直观的操作,使用户感受到前所未有的愉悦和便捷。

  • 个性化定制的主题支持: 我们理解C端产品的多样性,因此提供了丰富的主题定制选项,让每个C端项目都能拥有独一无二的外观,更好地满足产品个性化的需求。


通过这一独特的设计理念,Celeris Web致力于在前端开发领域探索全新的可能性,为C端产品注入更多活力和创意。我们相信,这样的创新将带来更广泛的用户认可和更高的产品价值。在Celeris Web的世界里,前端不再局限于Admin系统,而是融入了更多关于用户体验的精彩元素。


后期发展路线:瞄准AIGC,引领互联网产品变革


随着人工智能与图形计算(AIGC)技术的崛起,我们决定将Celeris Web的发展方向更加专注于推动AIGC相关产品的研发和落地。这一战略决策旨在顺应互联网产品的变革浪潮,为未来的科技创新开辟全新的可能性。


AIGC技术引领变革:


AIGC的兴起标志着互联网产业迎来了一场技术变革,为产品带来更加智能、交互性更强的体验。Celeris Web将积极响应这一变革,致力于为开发者提供更优秀的工具,助力他们在AIGC领域创造更具前瞻性的产品。


模板的研发重心:


在后期的发展中,Celeris Web将更加重视AIGC相关产品的研发需求。我们将推出更多针对人工智能的功能模块,使开发者能够更便捷、高效地构建出色的AIGC应用。


专注产品落地:


除了技术研发,我们将加强对AIGC产品落地的支持。通过提供详实的文档、示例和定制化服务,Celeris Web旨在帮助开发者更好地将AIGC技术融入他们的实际项目中,实现技术创新与商业应用的有机结合。


开放合作生态:


为了推动AIGC技术的更广泛应用,Celeris Web将积极构建开放合作生态。与行业内优秀的AIGC技术提供商、开发者社区保持密切合作,共同推动AIGC技术的发展,携手打造更加繁荣的互联网产品生态圈。


Celeris Web未来的发展将以AIGC为核心,我们期待在这个快速发展的技术领域中,与开发者们一同探索、创新,共同引领互联网产品的未来。通过持续的努力和创新,Celeris Web将成为AIGC领域的引领者,助力开发者创造更加智能、引人入胜的互联网产品。


源码


kirklin/celeris-web (github.com)


作者:KirkLin
来源:juejin.cn/post/7324334380373688371
收起阅读 »

揭秘 "mitt" 源码:为什么作者钟情于 `map` 而放弃 `forEach`

web
故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach, 而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. ...
继续阅读 »

故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach


而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. (使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


咦?为什么会这样呢?


"mitt" 简介


首先,让我们认识一下 "mitt",它是一只小巧灵活的事件发射器(event emitter)库,体积仅有 200 字节,但功能强大。这个小家伙在项目中充当了事件的传播者,有点像是一个小型的邮差,把消息传递给需要它的地方。


developit/mitt: 🥊 Tiny 200 byte functional event emitter / pubsub. (github.com)


作者的选择:map vs forEach


在源码中,我们发现作者选择使用了 Array.prototype.map(),这是一个处理数组每个元素并返回新数组的函数。然而,有趣的地方在于,作者并没有在 map 中返回任何值。这和我对 map 的期望有些出入,因为我们习惯于用它生成一个新的数组。


代码的细微变化


曾经,代码片段是这样的,作者想要用 map 来执行一些操作,但却不生成新数组。


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.map((handler) => {
handler(evt!);
});
}

我希望修改成这样:


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.forEach((handler) => {
handler(evt!);
});
}

所以我很快就交了个PR:将map改成了forEach,经过了几个月的等待,PR被拒了,作者的回应是:map() is used because it is 3 bytes smaller when gzipped.(使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


code.png


pr.png


小技巧背后的逻辑


虽然 map 通常用于生成新数组,但作者在这里使用它更像是在借助压缩的优势,让代码更轻量。


大小对比


通过实验验证,使用 map 的打包大小确实稍微小一些:



  • 使用 map 时,打包大小为:


  - 190 B: mitt.js.gz
- 162 B: mitt.js.br
- 189 B: mitt.mjs.gz
- 160 B: mitt.mjs.br
- 268 B: mitt.umd.js.gz
- 228 B: mitt.umd.js.br


  • 而使用 forEach 后,打包大小为:


  - 192 B: mitt.js.gz
- 164 B: mitt.js.br
- 191 B: mitt.mjs.gz
- 162 B: mitt.mjs.br
- 270 B: mitt.umd.js.gz
- 230 B: mitt.umd.js.br

进一步实验


为了深入了解选择的影响,我又进行了一个实验。有趣的是,当我将代码中的一处使用 map 改为 forEach,而另一处保持不变时,结果居然是打包体积更大了。


experiment_results.png


总结


这个故事让我不仅仅关注于代码表面,还开始注重微小选择可能带来的影响。学到了很多平时容易忽略的点,"mitt" 作者的选择展现了在开发中面对权衡时的智慧,通过选择不同的API,以轻松的方式达到减小代码体积的目标。在编写代码时,无处不充满着权衡的乐趣。


如果你对这个故事有更多的想法或者其他技术话题感兴趣,随时和我分享哦!


作者:KirkLin
来源:juejin.cn/post/7327424955037564965
收起阅读 »

使用pixi.js开发一个智慧路口(车辆轨迹追踪)项目

web
项目效果 项目功能: 位置更新、航向角计算。 debug模式。 位置角度线性补帧。 变道、转弯、碰撞检测。 mock轨迹数据 图片效果: 视频效果: 项目启动 项目地址 github:(github.com/huoguozhang…) 线上:todo...
继续阅读 »

项目效果


项目功能:



  • 位置更新、航向角计算。

  • debug模式。

  • 位置角度线性补帧。

  • 变道、转弯、碰撞检测。

  • mock轨迹数据


图片效果:


result.gif


视频效果:



项目启动


项目地址



(如果觉得项目对你有帮助的话, 可以给我一个star 和 赞,❤️)


启动demo项目



  1. cd car-tracking-2d/demos/react-demo

  2. yarn

  3. yarn start


界面使用


debug 模式


浏览器url ?后面(search部分)加入参数debug=1


例如:http://localhost:3000/home?tunnelNo=tunnel1&debug=1


将会展示调试信息:


image.png


如图:车旁边的白色文字信息为debug模式才会展示的内容(由上到下为:里程、车id、车道id、[x,y]、旋转角度)


实现:


技术栈:


ts+pixi.js+任意前端框架


(前端框架使用vuereact或者其他框架都可以。只需要在mounted阶段,实例化我们暴露出来class即可。然后在destroyed或者unmounted阶段destory示例即可,后面会提到。)


pixi.js


官网介绍:



Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.



pixi.js是一个2D的WebGL的渲染库。但是没有three.js知名度高。一个原因是,我们2D的需求技术路线很多,可以是dom、svg、canvas draw api等,包括本项目也可以使用其他技术方案实现,希望通过本文,大家在实现这种频繁更新元素位置的功能,可以考虑一下pixi.js。



API快速讲解

这里只讲我们项目使用到的


Application

import * as PIXI from 'pixi.js';

const app = new PIXI.Application({
view: canvasDom // canvas dom 对象
});


Container

容器,功能为一个组。
当我们设置容器的scale(缩放)、rotation(旋转)、x、y(位置)时。里面的元素都会收到影响。


(ps:app.stage也是一个Container


每个 Container可以通过addChild(增加子节点)、removeChild(删除子节点),也可以设置子元素的zIndex(和css的功能一致)。子原始的scale(缩放)、rotation(旋转)、x、y(位置)是相对于Container的。


Sprite

精灵,渲染图片对象。


carObj = Sprite.from('/car.svg')

Sprite.from(url),url相同的话,只会加载一次图片。纹理对象也只会创建一次。


anchor属性其他对象也有,设置定位点,类似于csstransform-origin


执行下面代码
carObj.anchor.set(0.5, 0.5)


如果x = 10 y =10,carObj的中心点的坐标就是(10,10),旋转原点也是(10,10),缩放也是如此。


Graphics

绘制几何图形,圆弧,曲线、直线等都可以。也支持fill和stroke,canvas draw api支持的,Graphics都支持。


Text

文本,比较简单。字体、颜色、大小,都支持。



  • 值得注意的是文本内容含有换行符时(\n \r),文本会换行。

  • pixi提供测量文本的width height的方法非常好用。


Tick

this.app.ticker.add(() => {})

类似于requestAnimationFrame


具体实现


分三步,vue/react都一样:


1 获取canvas dom通过ref的方式。


2 创建我们封装Stage Road


3 组件销毁时,执行 stage.destroy(注意stage是我封装的,不是pixi的。使用方不需要使用pixi.js的api)


线性插帧

当有一个对象由坐标 点a(0,0)变换到点b(1000,1000),1秒内完成。
中间的变化值为:
dx =1000 dy=1000
记录每帧的时间差t(当前帧距离第0帧的,单位毫秒)


所以第n帧位置信息为(0+dx / 1000 * t, 0+ dy /1000 *t)


角度变换也是这个道理。


位置坐标获取

如果直线长度为1000px,对应的实际里程为100米。


当跑了50米,当前就是直线的中点坐标。
弯道呢,通过弧度可以推算出坐标。
可以把 Road.ts line 70的注释取消。


 // 方便开发观察 绘制车道线 ---begin----
// this.mount(lane.centerLine.paint())

航向角

直线简单,通过Math.atan2可以求出来。
弯道需要通过解析几何,计算出圆弧切线,然后推测出航向角。


转弯

mark.png
可以查看我们标注的一些点


以1到7的弯道举例,相当于是从新创建一次车道,车道的点是车道1和车道7的组合。
我们通过 circle属性配置,在创建Road


{
uid: '1-2',
x: 1072,
y: 1605,
circle: {
// 编号形式 车道序号-第几个点

linkId: '7-3'
}
},

这条信息表示:车道1的第2个点(uid),有圆弧链接到车道7的第3个点(circle.linkId)


碰撞检测

我们这个项目的特点是,前端展示,实际后端返回什么数据,我们就展示什么数据。(一般不需要前端处理)。
这里我们mock的数据就简单处理一下。判断是否存在相交的线段(当前对象的位置和将要到达的点),如果线段相交,车辆暂停移动。


作者:火锅小王子
来源:juejin.cn/post/7327467832866095130
收起阅读 »

微信小程序开发大坑盘点

web
微信小程序开发大坑盘点 起因 前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app...
继续阅读 »

微信小程序开发大坑盘点


起因


前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app 做,但是这玩意太难用所以不了了之了。


于是这次打算正经的用微信自己的那套东西做,结果不出意外的是入了深坑......


大坑


微信小程序云函数外部调用异常


微信小程序提供 wx.request 发起 HTTP 请求,由于微信不是浏览器,没有跨域限制,这方便了很多事,但是由于 wx.request 函数只能对 HTTPS 协议的地址发起请求,而我们学校的教务系统又是清一色的 HTTP,因此我需要一个可以用来帮助我发起 HTTP 请求的转发接口。


对于这种简单需求,云函数显然是最好的解决方案,进而我发现微信小程序自带云函数的支持,于是便兴冲冲地写了一段 NodeJS 代码,放上去跑。


结果我发现不知道为什么,请求其他网站都没问题,唯独请求我们教务系统就会原地超时。经过了几个小时的调试,最后以失败告终,转而改用腾讯云的云函数。


代码也十分简单:


const url = require('url')

const express = require('express');
const app = express()
const port = 9000

const rp = require('request-promise')

app.use(express.json());

app.post('/', async (req, res) => {
const jar = rp.jar()

try {
const response = await rp({
...req.body,
resolveWithFullResponse: true,
simple: false,
jar: jar
})
res.json(response)
} catch (e) {
res.json(e)
console.error(e)
}
})

app.listen(port, () => {
console.log("Successfully loaded")
})

其中额外引入了 request-promise 库(express 是默认引入的,腾讯云函数这里做的不错,对 npm 支持很好)。


然后做了一个模仿 wx.request 调用风格的 request 函数,这样我就可以在 wx.request 和我自己的 request 函数中无缝切换(更进阶的是,我自己写的这个还额外支持了以 Promise 风格调用。


export async function request(data) {
try {
const res = await rp({
...data,
uri: data.url,
headers: data.header,
})
let result = {
...res,
data: res.body,
header: res.headers
}
if (result.statusCode != 200) {
throw {
err_msg: "内部错误"
}
}
if (data.dataType === 'json') {
result.body = JSON.parse(result.body)
}
data.success && data.success(result);
data.complete && data.complete({})
return result;
} catch (e) {
data.fail && data.fail(e)
data.complete && data.complete({})
throw e;
}
}

function rp(data) {
return new Promise((resolve, reject) => {
wx.request({
method: 'POST',
url: 'https://service-abcdefg-123456789.gz.apigw.tencentcs.com/release/',
data: data,
success: (res) => {
resolve(res.data)
},
fail: (err) => {
reject(err)
}
})
})
}

ES6 module 和变量作用域支持差


不知道为什么,微信小程序完全不支持 ES6 module,即使它是支持 ES6 语法的。也就是说,你只能使用这种传统的 CommonJS 方式引入:


const module = require('module.js')

而不是 ES6 的 import 语法:


import module from 'module.js'

最离谱的是,微信小程序这个基于 VSCode 的编译器会给你 warn 这段代码,告知你可以转换为使用 import 导入。



于是这又引出了另外一个奇怪的问题:当你在一个界面的逻辑层文件上声明变量时,IDE 会认为这个变量是一个全局变量,因此在其他界面声明同名变量会得到一个 error,即使不会导致任何编译错误。


这导致了,现在我的模块引入必须用一种很奇怪的写法...


const sessionModule = require('../../utils/session');
const tgcModule = require('../../utils/tgc')
const cryptoModule = require('../../miniprogram_npm/crypto-js/index.js')

奇葩的 NPM 支持


在以前,微信小程序是不支持包管理器的,这也就意味着,你得手动把那些库的 JS 复制到你的项目目录里再引用,非常麻烦。但是现在好了,微信可以自动帮你做这件事了。


没错,是自动帮你复制,而不是做了包管理器支持。


怎么说呢...你需要先在你的项目源代码目录中 init 一个 package.jsonadd 你需要的包然后 install,接下来点击 IDE 顶栏的 Tools - Build npm 选项,Weixin Devtools 就会帮你生成一个 miniprogram_npm文件夹,将每个项目各自 combine 到一个 index.js 然后塞到各自名字的文件夹里,然后,你就能通过上面那种方式手动引入使用了。


很奇葩但是... 勉强能用(而且不限制使用的包管理器,比如我用的就是 yarn)。


避免使用双向绑定


微信小程序的 WXML 存在一个有限的双向绑定支持,也是类似 Vue 的那种语法糖:


<input model:value="{{value}}" />

但是这个双向绑定不知道为什么,在某些情况下会认为你没有设置一个 bindinput 事件(但实际上应该是由双向绑定自动设置的),于是不断地在后台刷警告,因此还不如手动实现来的省心。


有限的标准组件支持


如果你觉得微信小程序的开发和前端开发差不多,那就大错特错了。因为微信小程序默认情况下根本不支持任何 HTML 元素,而是套了一层他们自己的元素,比如 view 实际上是 classblock 则和 Vue 的 template 差不多(微信小程序也有 template 元素,只不过那个是给组件用的),不分 h1, h2, span, strong,只有 text 元素等。当然好在 CSS 还是那套,基本都能用。


但是... 微信小程序提供的元素依然太少了,根本没办法满足实际开发需要(比如根本没有表格元素)。于是微信小程序提供了一个 rich-text 元素,可用于渲染 HTML 元素。


但是这个 rich-text 就显得十分鸡肋,他不是通过 slot 传入 HTML 元素,而是通过 string 或者 object。这凭空增加了开发难度,导致我不得不这么写:


<rich-text nodes="{{nodes}}"></rich-text>

this.setData({
nodes: licenses.map(it => {
return `
<div style="margin: 20px 10px;"><strong>${it.projectName}</strong>
is licensed under the <code>${it.licenseName}</code>:</div>
<pre style="overflow: auto; background-color:#F5F6FA;"><code>${it.fullLicense}</code></pre>
${it.sourceRepo?`<div style="margin: 20px 10px;"><span style="color:gray; font-size: 12px;">The source code can be found at: ${it.sourceRepo}</span></div>`:""}
<br/><br/>
`

}).join("")
})

甚至这么写:



完美的回答了知乎有人“为什么不用 JSON 表达页面而是用类似 XML 一样的 HTML”的问题。


最后


虽然吐槽了这么多,但是微信小程序还是有一些不错的点的。除了上面说的宽松的跨域策略以外,微信小程序的 TypeScript 支持很完善,IDE 工具链做的也不错(除了他那个特别容易崩溃的 Simulator),加之微信开放社区的活跃度也不低(问问题一天内就有人回复),也算是能用了。


作者:HikariLan贺兰星辰
来源:juejin.cn/post/7228563544022761509
收起阅读 »

一行代码快速实现全局模糊

web
github 仓库:github.com/astak16/blu…npm 仓库:http://www.npmjs.com/package/blu…页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理敏感数据过滤通常是由后端去做的,有时候...
继续阅读 »

github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232
收起阅读 »

同学,请实现一个扫码登录

web
马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦! 即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求...
继续阅读 »

马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦!


即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求,我一看有点来劲了。一来做了多年前端,类似的需求还没有接触过,平时做的多的页面需求和改改bug对自身能力显然是无法提升的。二来扫码登录的功能很多应用都有做过,常见的微信扫码登录,也挺好奇具体如何实现。我大概看了一遍需求文档,写的挺详细的,流程图也标明了各端的交互流程。由于内网开发,产品流程图也忘记截图了,此处在网上找到的一个大概的流程图:
image.png


主要涉及到的是pc端、手机端和后台服务端。由于听产品同事说手机端由原生端(安卓和IOS)来实现,因此我这边只需要开发pc端就行,工作量直接减半有没有。做过该功能的小伙伴肯定了解,pc端的实现还是比较简单的,主要就是开启轮询查询后台扫码状态,然后做对应的提示或登录成功后跳转首页。


扫码登录的需求在前端主要难点在轮询上


0. 什么叫轮询?


所谓的轮询就是,由后端维护某个状态,或是一种连续多篇的数据(如分页、分段),由前端决定按序访问的方式将所有片段依次查询,直到后端给出终止状态的响应(结束状态、分页的最后一页等)。


1. 轮询的方案?


一般有两种解决方案:一种是使用websocket,可以让后端主动推送数据到前端;还有一种是前端主动轮询(上网查了下细分为长轮询和短轮询),通过大家熟悉的定时器(setIntervalsetTimeout)实现。


由于项目暂未用到websocket,且长轮询需要后台配合,所以直接采用短轮询(定时器)开撸了。


遇到的问题:


1、由于看需求文档上交互流程比较清晰,最开始没去网上查找实现方案,自己直接整了一版setInterval的轮询实现。在跟后台联调的过程中发现定时器每1s请求一次接口,发现很多接口没等响应就开启下一次的请求,很多请求都还在pending中,这样是不对的,对性能是很大消耗。于是想了下,可以通过setTimeout来优化,具体就是用setTimeout递归调用方式模拟setInterval的效果,达到只有上一次请求成功后才开启下一次的请求。


// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},

2、在自测了过程中又发现了另外一个问题,stopPolling方法中clearTimeout似乎无法阻止setTimeout的执行,二维码失效后请求仍在不停发出,这就很奇怪了。上网搜索了一番,发现一篇文章(很遗憾,已经找不到是哪篇文章了!)记录了这个问题:大概意思是虽然clearTimeout已经清除了定时器,但此时有请求已经在进行中,导致再次进入了循环体,重新开启了定时器。解决办法就是,需要手动声明一个标识位isStop来阻止循环体的执行。


    stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},

试了下确实达到效果了,但其实这个问题产生的具体原因我还是有些模糊的,希望遇到过相关问题的大佬能指点一下,感激不尽!


3、解决了上面提到的问题,就在以为万事大吉,只待提测的时候。后台同事发现了一个问题(点赞后台同事的尽责之心):他在反复切换登录方式(扫码登录<->账号密码登录)的过程中,发现后台日志有一段时间打印的qrcId不是最新的。然后我这边试了下,确实在切换频率过高时,此时有未完成的请求仍在进行中,导致qrcId被重新赋值了。虽然已经在beforeDestroy里调用了stopPolling清除定时器,但此时请求是未停止的。聪明的小伙伴们肯定想到axioscancelToken可以取消未完成的请求,但我实际也并没有用过,而且项目里也没有可以表演Ctrl+CCtrl+V的地方。于是百度了一番,找到一篇掘友的文章,为了表示尊敬我原封不动的搬到我的代码里了,哈哈!


import axios from "axios";
const CancelToken = axios.CancelToken;

const cancelTokenMixin = {
data() {
return {
cancelToken: null, // cancelToken实例
cancel: null, // cancel方法
};
},
created() {
this.newCancelToken();
},
beforeDestroy() {
//离开页面前清空所有请求
this.cancel("取消请求");
},
methods: {
//创建新CancelToken
newCancelToken() {
this.cancelToken = new CancelToken((c) => {
this.cancel = c;
});
},
},
};
export default cancelTokenMixin;

掘友文章[:](在vue项目中取消axios请求(单个和全局) - 掘金 (juejin.cn))


在组件里引入mixin,另外在请求时传入cancelToken实例,确实达到效果了。此时再次切换登录方式,之前的未完成的请求已被取消,也就无法再篡改qrcId。写到此处,我发现问题2也是未完成的请求导致的,那么是否可以不用isStop标识,直接在stopPolling中调用this.cancel("取消请求");不是更好吗?


完整代码如下:


import sunev from 'sunev'; // 全局公共方法库
import cancelTokenMixin from "@/utils/cancelTokenMixin"; // axios取消请求

export default {
props: {
loginType: {
type: String,
default: 'code'
}
},
mixins: [cancelTokenMixin],
data() {
return {
qrcId: '', // 二维码标识
qrcBase64: '', // 二维码base64图片
macAddr: '', // mac地址
loading: false,
isStop: false,
codeStatus: '0',
qrStatusList: [
{
status: '-1',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码生成失败\n请刷新重试',
refresh: true
},
{ status: '0', icon: '', text: '', refresh: false },
{
status: '1',
icon: 'scan',
color: '#2986ff',
svgClass: 'icon-scan-small',
text: '扫描成功\n请在移动端确认',
refresh: false
},
{
status: '2',
icon: 'confirm',
color: '#2986ff',
svgClass: 'icon-confirm-small',
text: '移动端确认登录',
refresh: false
},
{
status: '3',
icon: 'cancel',
text: '移动端已取消',
refresh: false
},
{
status: '4',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码已失效\n请刷新重试',
refresh: true
},
{
status: '5',
icon: 'success',
color: '#2986ff',
svgClass: 'icon-success-small',
text: '登录成功',
refresh: false
},
{
status: '6',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '登录失败\n请刷新重试',
refresh: true
}
],
errMsg: ''
}
},
async created() {
try {
await this.getQrCode();
this.beginPolling();
} catch(err) {
console.log(err);
}
},
computed: {
// 当前状态
curQrStatus() {
const statusObj = this.qrStatusList.find(item => item.status === this.codeStatus);
if (this.errMsg) {
statusObj.text = this.errMsg;
}
return statusObj;
}
},
methods: {
// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},
// 暂停轮询
stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},
// 获取二维码base64
async getQrCode() {
this.reset();
this.loading = true;
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCGen',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
if (res.qrcId) {
this.qrcId = res.qrcId;
this.qrcBase64 = res.qrcBase64;
} else {
this.stopPolling();
}
} catch(err) {
this.errMsg = err.message;
this.stopPolling();
}
},
// 获取二维码状态
async getQrCodeStatus() {
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCQry',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
return res.status;
} catch(err) {
this.stopPolling();
}
},
// 刷新二维码
async refresh() {
await this.getQrCode();
this.beginPolling();
},
// 切换登录类型
toggle() {
this.$emit('toggleLoginType');
},
// 重置
reset() {
this.isStop = false;
this.codeStatus = '0';
this.errMsg = '';
},
beforeDestroy() {
this.stopPolling();
}
}
}

ps:


1、由于是老项目了,登录界面逻辑较多,避免臃肿,二维码登录拆分成单独组件实现


2、由于项目组在内网开发,以下代码都是一行行重新手打的,不是很重要的html和css部分就省略了


后记:


由于此需求并不着急上线,暂未提测,所以还不知测试同事会提出怎样的bug。另外掘友们如果发现问题,也欢迎批评指正,感激不尽!


作者:wing98
来源:juejin.cn/post/7326268998490865673
收起阅读 »

前端如何统一开发环境

web
统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。 nodejs 首先推荐使用 fnm 管理多版本 nodejs。 对比 nvm: 支持 brew 安装,...
继续阅读 »

统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。


nodejs


首先推荐使用 fnm 管理多版本 nodejs。


对比 nvm



  • 支持 brew 安装,更新方便

  • 跨平台,windows 也能用 winget 安装


使用 fnm 一定要记得开启根据当前 .nvmrc 自动切换对应的 nodejs 版本,也就是在在 .zshrc 中加入:


eval "$(fnm env --use-on-cd)"

包管理器


尽管 npm 一直在进步,甚至 7.x 已经原生支持了 workspace。但是我钟爱 pnpm,理由:



  • 安全,避免幽灵依赖,不会将依赖的依赖平铺到 node_modules 下

  • 快,基于软/硬链接,node_modules 下是软连接,硬链接到 .pnpm 文件夹下的硬链接

  • 省磁盘,公司配的 mac 只有 256G

  • pnpm 的可配置性很强,配置不够用还可以用 .pnpmfile.js 编写 hooks

  • yarn2 node_modules 都看不到,分析依赖太麻烦了

  • 公司用的 vue,而 vue3/vite 用 pnpm(政治正确)


推荐使用 Corepack 管理用户的包管理器,其实我一开始知道有 corepack 这个 nodejs 官方的东西的时候,我就在想:为啥不叫 npmm(node package manager manager) 呢?


corepack 目前官方觉得功能没稳定,所以默认没开启,需要用户通过 corepack enable 手动开启,相关的讨论:enable corepack by default


有了 corepack 我们就可以轻松的在 npm/yarn/pnpm 中切换,安装和更新不同的版本。还有一个非常方便的特性就是通过在 package.json 中声明 packageManager 字段例如 "pnpm@8.14.1",当我们开启了 corepack,cd 到该 package.json 所在的 package 的时候,运行 pnpmcorepack 会使用 8.14.1 版本的 pnpm


corepack 是怎样做到的呢?nodejs 安装文件夹有个的 bin 目录,这个目录会被添加到 path 环境变量,其中包含了 corepack 以及 corepack 支持的包管理器的可执行文件:


❯ tree ../../Library/Caches/fnm_multishells/17992_1705553706619/bin
../../Library/Caches/fnm_multishells/17992_1705553706619/bin
├── corepack -> ../lib/node_modules/corepack/dist/corepack.js
├── node
├── npm -> ../lib/node_modules/npm/bin/npm-cli.js
├── npx -> ../lib/node_modules/npm/bin/npx-cli.js
├── pnpm -> ../lib/node_modules/corepack/dist/pnpm.js
├── pnpx -> ../lib/node_modules/corepack/dist/pnpx.js
├── yarn -> ../lib/node_modules/corepack/dist/yarn.js
└── yarnpkg -> ../lib/node_modules/corepack/dist/yarnpkg.js

可以看到 pnpm 被链接到了 corepack 的一个 js 文件,查看 corepack/dist/pnpm.js 内容:


#!/usr/bin/env node
require('./lib/corepack.cjs').runMain(['pnpm', ...process.argv.slice(2)]);

可以看到其实 corepack 相当于劫持了 pnpmyarn 命令,然后根据 packageManager 字段配置自动切换到对应的包管理器,如果已经安装过就使用缓存,没有就下载。


怎样统一 nodejs 和 包管理器


问题


虽然我在项目中配置了 .nvmrc 文件,在 package.json 中声明了 packageManager 字段,但是用户可能没有安装 fnm 以及配置根据 .nvmrc 自动切换对应的 nodejs,还有可能没有开启 corepack,所以同事的环境还是有可能和要求的不一致。我一向认为,不应该依靠人的自觉去遵守规范,通过工具强制去约束才能提前发现问题和避免争论。


解决办法


最开始是看到项目中使用了 only-allow 用于限制同事开发时只能用 pnpm,由此我引发了我一个灵感,为什么我不干脆把事情做绝一点,把 nodejs 的版本也给统一了


于是我写了一个脚本用于检查用户本地的 nodejs 的版本,包管理器的版本必须和要求一致。最近封装成了一个 cli:check-fe-env。使用方式很简单,增加一个 preinstall script:


{
"scripts": {
"preinstall": "npx check-fe-env"
}
}

工作原理



  • 用户在运行 pnpm install 之后,install 依赖之前,包管理器会执行 preinstall 脚本

  • cli 会检测:

    • 用户当前环境的 nodejs 版本和 .nvmrc 中声明的是否一样

    • 用户当前使用的包管理器种类和版本是否和 package.jsonpackageManager 字段一样




获取当前环境的 nodejs 版本很简单,可以用 process.version。想要获取执行脚本时的包管理器可以通过环境变量:process.env.npm_config_user_agent,如果一个 npm script 是通过 pnpm 运行的,那么这个环境变量会被设置为例如 pnpm/8.14.1 npm/? node/v20.11.0 darwin arm64,由此我们可以获取当前使用的包管理器和版本。


为了加快安装速度,我特意把源码和相关依赖给一起打包了,整个 bundle 大小 8k 左右。


局限性


最新的 npmpnpm 目前貌似都有一个 bug,都是安装完依赖才执行 preinstall hooks,具体看这: Preinstall script runs after installing dependencies


这个方案对于 monorepo 项目或者说不需要发包的项目是没啥问题的,但是不适用于一个要发包的项目。原因是 preinstall script 除了会在本地 pnpm install 时执行,别人安装这个包,也会执行这个 preinstall script,就和 vue-demi 用的 postinstall script 一样。主要是确实没找到一个:只会在本地运行 pnpm install 后且在安装依赖前执行的 hook。


作者:余腾靖
来源:juejin.cn/post/7325069743143878697
收起阅读 »

一个指令实现左右拖动改变布局

web
一个指令实现左右拖动改变布局 一、前言 本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了: 实现思路 总结关键技术点 完整 demo 二、实现思路 2.1 外层div布局 首先设置4个div元素,一个作为父容器,一个...
继续阅读 »

一个指令实现左右拖动改变布局


一、前言


本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了:





    1. 实现思路





    1. 总结关键技术点





    1. 完整 demo




二、实现思路


2.1 外层div布局


首先设置4个div元素,一个作为父容器,一个作为左边的容器,一个在中间作为拖动指令承载的元素,最后一个在作为右边容器的元素。


js
复制代码
<div>
<div class="left"></div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right"></div>
</div>

2.2 获取指令元素的父元素和兄弟元素


首先,接收指令传递的各元素的宽,并进行初始赋值和利用 calc 计算右边元素宽度。


js
复制代码
let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

然后,接收指令传递下来的元素 el,并根据该元素 通过 Element.previousElementSibling 获取当前元素前一个兄弟元素,即是 所在的元素。 通过
Element.nextElementSibling 获取当前元素的后一个兄弟元素,即是 所在的元素。 通过 Element.parentElement 获取当前元素的父元素。


js
复制代码
bind: function (el, binding, vnode) {
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement
}

2.3 利用浮动定位,实现浮动布局


接着,给各个容器元素设置浮动定位 float = 'left'。当然,其实其他方式也可以的,只要能达到类似“行内块”的布局即可。


可以提一下的是,设置 float = 'left' 可以创建一个独立的 BFC 区域,具有“独立隔离性”, 即 BFC 区域内部元素的布局,不会“越界”影响外部元素的布局; 外部元素的布局也不会“穿透”,影响 BFC 区域的内部布局。


js
复制代码
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
left.style.float = 'left'
resize.style.float = 'left'
right.style.float = 'left'

2.4 实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标


通过 onpointerdown 监听,实现实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标,这个特定元素即 v-resize 指令所在的元素。


这样,就可以通过获取 v-resize 指令所在的元素的位置属性,来计算出左右的元素,在拖动时需要设置的宽和位置信息。


js
复制代码
resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.setPointerCapture(e.pointerId);
return false
}

2.5 实现鼠标移动时,改变左右的宽度


通过 onpointermove 监听,实现在鼠标指针移动时,获取鼠标事件的位置信息 clientX 等,并由此计算出合适的移动距离 moveLen, resize 的左边距离,left 元素的宽,以及 right
元素的宽。


由此,就实现了每移动一步,就重新计算出新的布局位置信息,并进行了赋值。


js
复制代码
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}

2.6 鼠标抬起时,将鼠标指针从先前捕获的元素中释放


通过监听 onpointerup,实现在鼠标指针抬起时,通过 releasePointerCapture 将鼠标指针从先前捕获的元素中释放,还给鼠标自由。并将 resize 元素的 onpointermove 事件设置为
null。这样,当鼠标被抬起后,再操作就不会携带此前的绑定操作了。


js
复制代码
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}

经过上诉步骤,我们就实现了,从鼠标按下,到移动计算改变布局,然后鼠标抬起释放绑定,操作完成,改变布局的目标达成。


三、总结关键技术点


实现本需求主要的关键技术点有:


3.1 setPointerCapture 和 releasePointerCapture


Element.setPointerCapture() 用于将特定元素指定为未来指针事件的捕获目标。 指针的后续事件将以捕获元素为目标,直到捕获被释放(通过 Element.releasePointerCapture())。


Element.releasePointerCapture() 则用来将鼠标从先前通过 Element.setPointerCapture() 绑定的元素身上释放出来,还给鼠标自由。


需要注意的是,类似的功能事件还有 setCapture() 和 releaseCapture,但它们已经被标记为弃用,且是非标准的,所以不建议使用。


3.2 onpointerdown,onpointermove 和 onpointerup


与上面配套的关键事件还有,onpointerdown,onpointermove 和 onpointerup。其中 onpointermove 是实现主要改变布局的逻辑的地方。


pointerdown:全局事件处理程序,当鼠标指针按下时触发。返回 pointerdown 事件触发对象的事件处理程序。


onpointermove:全局事件处理程序,当鼠标指针移动时触发。返回 targetElement 元素的 pointermove 事件处理函数。


onpointerup:全局事件处理程序,当鼠标指针抬起时触发。返回 targetElement 元素的pointerup事件处理函数。


3.3 注意事项


① Vue.nextTick 的使用。在 vue 指令定义的 bind 中使用了 Vue.nextTick,是为了解决初次运算时,有些 dom 元素未完成渲染,设置元素属性会报警告或错误。


js
复制代码
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
}
})

② position = 'relative' 的设置。给每个元素 left 和 right 元素设置 position = 'relative',是为了解决 z-index 可能会失效的问题,我们知道有时浮动元素会导致这种情形发生。
当然这并不影响本次需求的实现,是为了其他设计考虑才这样做的。


js
复制代码
left.style.position = 'relative'
resize.style.position = 'relative'
right.style.position = 'relative'

③ cursor = 'col-resize' 的设置。为了获得更友好的体验,使得用户一眼鉴别这个功能,我们使用了 cursor 的 col-resize 属性。


js
复制代码
resize.style.cursor = 'col-resize'

四、完整 demo


// 这是定义指令的完整代码:directive.js


js
复制代码

/**
* 自定义调整宽度指令:添加指令后,可以实现拖拽边线改变页面元素的宽度。
* 指令接收两个参数,left 左边元素的宽度,中间 resize 元素的宽度。数据类型均为 number
* 使用示例:
* <div>
* <div></div>
* <div v-resize="{left: 300, resize: 10}" />
* <div></div>
* </div>
*
* 注意:由于是使用 float 布局,所以需要保证有4个元素作为浮动元素的容器,即父容器 1 个,子容器 3 个。
*
*/

import Vue from 'vue'

const resizeDirective = {}
const handler = (el, binding, vnode) => {

let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

if (binding.value?.left && Object.prototype.toString.call(binding.value?.left) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}
if (binding.value?.resize && Object.prototype.toString.call(binding.value?.resize) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}

let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
box.style.height = '100%'
box.style.width = '100%'
box.style.overflow = 'hidden'

left.style.float = 'left'
left.style.width = leftWidth + 'px'
left.style.position = 'relative'

resize.style.float = 'left'
resize.style.cursor = 'col-resize'
resize.style.width = resizeWidth + 'px'
resize.style.height = box.offsetHeight + 'px'
resize.style.position = 'relative'

right.style.float = 'left'
right.style.width = rightWidth
right.style.position = 'relative'
right.style.zIndex = 99

resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}
resize.setPointerCapture(e.pointerId);
return false
}
}
resizeDirective.install = Vue => {
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
},
update: function (el, binding) {
handler(el, binding)
},
unbind: function (el, binding) {
el.instance && el.instance.$destroy()
}
})
}

export default resizeDirective



// 在 main.js 中使用


js
复制代码
import resizeDirective from './directive'

Vue.use(resizeDirective)

// 在具体页面中使用:ResizeWidth.vue


html
复制代码

<template>
<div>
<div class="left">left</div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right">right</div>
</div>
</template>

<script>
export default {
name: 'ResizeWidth'
}
</script>

<style scoped>
.left {
background: #42b983;
height: 50vh;
}

.resize {
background: #EEEEEE;
height: 50vh;
}

.right {
background: #1e87f0;
height: 50vh;
}
</style>


作者:灵扁扁
链接:https://juejin.cn/post/7245936314851377210
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

箭头函数太长了,缩短小窍门来了

web
前言 使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略: 参数括号 (param1, param2) return 关键字 甚至大括号 { }。 1. 基本语法 完整版本的箭头函数声明包括: 一对带有参数枚举的括号 (param...
继续阅读 »

前言


使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略:



  • 参数括号 (param1, param2)

  • return 关键字

  • 甚至大括号 { }


1. 基本语法


完整版本的箭头函数声明包括:



  • 一对带有参数枚举的括号 (param1, param2)

  • 后面跟随箭头 =>

  • 以函数体 {FunctionBody} 结尾


典型的箭头函数如下所示:


const sayMessage = (what, who) => {
  return `${what}${who}!`;
};

sayMessage('Hello''World'); // => 'Hello, World!'

这里有一点需要注意:你不能在参数 (param1, param2) 和箭头 => 之间放置换行符。


接下来我们看看如何缩短箭头函数,在处理回调时,使它更易于阅读。


2. 减少参数括号


以下函数 greet 只有一个参数:


const greet = (who) => {
  return `${who}, Welcome!`
};

greet('Aliens'); // => "Aliens, Welcome!"

greet 箭头函数只有一个参数 who 。该参数被包装在一对圆括号(who) 中。


当箭头函数只有一个参数时,可以省略参数括号。


可以利用这种性质来简化 greet


const greetNoParentheses = who => {
  return `${who}, Welcome!`
};

greetNoParentheses('Aliens'); // => "Aliens, Welcome!"

新版本的箭头函数 greetNoParentheses 在其单个参数 who 的两边没有括号。少两个字符:不过仍然是一个胜利。


尽管这种简化很容易掌握,但是在必须保留括号的情况下也有一些例外。让我们看看这些例外。


2.1 注意默认参数


如果箭头函数有一个带有默认值的参数,则必须保留括号。


const greetDefParam = (who = 'Martians') => {
  return `${who}, Welcome!`
};

greetDefParam(); // => "Martians, Welcome!"

参数 who 的默认值为 Martians。在这种情况下,必须将一对括号放在单个参数(who ='Martians')周围。


2.2 注意参数解构


你还必须将括号括在已解构的参数周围:


const greetDestruct = ({ who }) => {
  return `${who}, Welcome!`;
};

const race = {
  planet'Jupiter',
  who'Jupiterians'
};

greetDestruct(race); // => "Jupiterians, Welcome!"

该函数的唯一参数使用解构 {who} 来访问对象的属性 who。这时必须将解构式用括号括起来:({who {}})


2.3 无参数


当函数没有参数时,也需要括号:


const greetEveryone = () => {
  return 'Everyone, Welcome!';
}

greetEveryone(); // => "Everyone, Welcome!"

greetEveryone 没有任何参数。保留参数括号 ()


3. 减少花括号和 return


当箭头函数主体内仅包含一个表达式时,可以去掉花括号 {} 和 return 关键字。


不必担心会忽略 return,因为箭头函数会隐式返回表达式评估结果。这是我最喜欢的箭头函数语法的简化形式。


没有花括号 {} 和 return 的 greetConcise 函数:


const greetConcise = who => `${who}, Welcome!`;

greetConcise('Friends'); // => "Friends, Welcome!"

greetConcise 是箭头函数语法的最短版本。即使没有 return,也会隐式返回 $ {who},Welcome! 表达式。


3.1 注意对象文字


当使用最短的箭头函数语法并返回对象文字时,可能会遇到意外的结果。


让我们看看这时下会发生些什么事:


const greetObject = who => { message: `${who}, Welcome!` };

greetObject('Klingons'); // => undefined

期望 greetObject 返回一个对象,它实际上返回 undefined


问题在于 JavaScript 将大括号 {} 解释为函数体定界符,而不是对象文字。message: 被解释为标签标识符,而不是属性。


要使该函数返回一个对象,请将对象文字包装在一对括号中:


const greetObject = who => ({ message: `${who}, Welcome!` });

greetObject('Klingons'); // => { message: `Klingons, Welcome!` }

({ message: `${who}, Welcome!` })是一个表达式。现在 JavaScript 将其视为包含对象文字的表达式。


4.粗箭头方法


类字段提案(截至2019年8月,第3阶段)向类中引入了粗箭头方法语法。这种方法中的 this 总是绑定到类实例上。


让我们定义一个包含粗箭头方法的 Greet 类:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = (who) => {
    return `${who}${this.what}!`;
  }
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage 是 Greet 类中的一个方法,使用粗箭头语法定义。getMessage 方法中的 this 始终绑定到类实例。


你可以编写简洁的粗箭头方法吗?是的你可以!


让我们简化 getMessage 方法:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = who => `${who}${this.what}!`
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage = who => `${who}, ${this.what}! 是一个简洁的粗箭头方法定义。省略了其单个参数 who 周围的一对括号,以及大括号 {} 和 return关键字。


5. 简洁并不总是意味着可读性好


我喜欢简洁的箭头函数,可以立即展示该函数的功能。


const numbers = [145];
numbers.map(x => x * 2); // => [2, 8, 10]

x => x * 2 很容易暗示一个将数字乘以 2 的函数。


尽管需要尽可能的使用短语法,但是必须明智地使用它。否则你可能会遇到可读性问题,尤其是在多个嵌套的简洁箭头函数的情况下。


我更喜欢可读性而不是简洁,因此有时我会故意保留大括号和 return 关键字。


让我们定义一个简洁的工厂函数:


const multiplyFactory = m => x => x * m;

const double = multiplyFactory(2);
double(5); // => 10

虽然 multiplyFactory 很短,但是乍一看可能很难理解它的作用。


这时我会避免使用最短的语法,并使函数定义更长一些:


const multiplyFactory = m => { 
  return x => x * m;
};

const double = multiplyFactory(2);
double(5); // => 10

在较长的形式中,multiplyFactory 更易于理解,它返回箭头函数。


无论如何,你都可能会进行尝试。但我建议你将可读性放在简洁性之前。


6. 结论


箭头函数以提供简短定义的能力而闻名。


使用上面介绍的诀窍,可以通过删除参数括号、花括号或 return 关键字来缩短箭头函数。


你可以将这些诀窍与粗箭头方法放在一起使用。


简洁是好的,只要它能够增加可读性即可。如果你有许多嵌套的箭头函数,最好避免使用最短的形式。


作者:河马老师
来源:juejin.cn/post/7326758010523697192
收起阅读 »

爆肝手写 · 一镜到底特效· 龙年大吉 【CSS3】

web
前言 作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的...
继续阅读 »

前言


作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的家伙变成自己的热爱的事情。 龙年来临之际, 通宵写了一个全新的CSS3 一镜到底的特效案例,如下图, 希望能与大家分享这份创意与激情, 祝各位掘友们新年快乐, 龙年行大运!


Video_20240120111316[00_00_12--00_00_15].gif


上源码:



整体实现思路介绍



整个案例使用到CSS3 和 HTML技术, 案例的核心知识点 使用到了 CSS3 中的透视 、3D变换、 动画 、无缝滚动等技术要点, 下面我会逐一进行介绍




  • 知识点1: 一镜到底特效的 案例的整体布局、设计、及动画思路

  • 知识点2:CSS3中的3D坐标系

  • 知识点3:CSS3中的3D变换及案例应用

  • 知识点4:CSS3中的3D透视及案例应用

  • 知识点5:CSS3中的 透视及3d变换的异同点

  • 知识点6:CSS3中的 动画及案例应用


1、一镜到底特效 的整体布局、设计、及动画思路


如下图所示,一镜到底的案例特效 最核心的就是要 构成一套 在3D 空间中, 有多个平行的场景, 然后以摄像机的视角 从前往后 移动,在场景中穿梭, 依次穿过每一个场景的页面即可啦,自己闭上眼睛来体验一下吧;
无标题.png


对应到本案例中效果就是这样啦:


image.png


当然有朋友会说看上图,感觉不到明显的3D 立体效果, 那再来看看下面这个图吧;


消失点.png


上面这张图就是 基于人眼 看不同距离的物体呈现出的结果, 也就是透视效果, 透视效果最核心的特点就是近大远小;而影响看到透视物体大小的一个参数就是消失点距离, 比如消失点越近,最远处的物体会越小, 近大远小的效果越明显, 自己闭上眼睛来体验一下吧;


对应到本案例中效果就是这样啦:


image.png



  • 上述框架对应的HTML源码如下, 其中.sence-in 内部的子元素是素材,可以先忽略:


<div class="sence-box sence-box1">
<div class="sence-in">
<div class="text-left text-box">掘金多多</div>
<div class="text-right text-box">大展鸿图</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
</div>
</div>
<div class="sence-box sence-box2">
<div class="sence-in">
<div class="text-left text-box">步步高升</div>
<div class="text-right text-box">年年有鱼</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box3">
<div class="sence-in">
<div class="text-left text-box">心想事成</div>
<div class="text-right text-box">万事如意</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box4">
<div class="sence-in">
<div class="text-left text-box">蒸蒸日上</div>
<div class="text-right text-box">一帆风顺</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box5">
<div class="sence-in">
<div class="text-left text-box">自强不息</div>
<div class="text-right text-box">恭喜发财</div>
<div class="sence-block">龙年大吉</div>
<div class="denglong-box"></div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>

知识点一: CSS3中的坐标系


CSS3中的坐标系,是一切3D 效果的基石, 务必熟练掌握 , 如下图所示:



  • x轴坐标:左边负,右边正

  • y轴坐标:上边负,下边正

  • z轴坐标:里面负,外面正

  • 注意: 坐标系的原点在 浏览器的左上角


image.png


知识点二: 透视(perspective)


perspective属性定义了观察者和Z=0平面之间的距离,从而为3D转换元素创建透视效果。上面也说了, 透视的效果就是 近大远小, 上面的截图中也能看到 。这个属性是用来创建3D转换效果的必要属性,因为当我们进行旋转或其他3D转换时,如果透视效果设置得不正确,元素可能会显得很奇怪或不正常。 透视的语法如下:


在CSS中,我们可以通过在父元素上设置perspective属性来控制子元素的3D效果。例如:


	.container {  
perspective: 1000px;
}

在这个例子中,我们为.container元素设置了perspective属性,值为1000px。这意味着任何在这个元素内部的3D转换都会基于这个视距进行透视。


知识点三:3D 变换的核心属性: transform-style


transform-style属性决定了是否保留元素的三维空间布局。当设置为preserve-3d时,它会保留元素内部的三维空间,即使这个元素本身没有进行任何3D转换。这使得子元素可以相对于父元素进行旋转或其他3D转换,而不会影响其他元素。在我们的案例截图中 也能看出在父元素设置了 transform-style: preserve-3d;属性后, 各个场景在 Z轴方向上,已经有了前后距离上的差异了。 需要注意的点就是, transform-style属性一定要设置给发生3D变换元素的父元素


例如:


 /* 透视属性加给了 最外层的元素, 保证所有子元素的透视效果是一致的,协调的*/
.perspective-box {
transform-style: preserve-3d;
}

在这个例子中,我们为.perspective-box元素设置了transform-style属性为preserve-3d,这意味着任何在这个元素内部的3D转换都会保留其三维空间布局。



  • 小技巧:如果你希望自己做的3D场景,立体效果很真实的话, 可以尽量多的给不同的元素,设置在Z轴方向上 设置不同的偏移量, 这样的效果是 摄像机在穿梭的过程中,每一段距离都能看到不同的风景, 层次感会很强, 当然也不要太疯狂, 不然场景会变得混乱哦


知识点四、perspective和transform-style的差异和注意点(炒鸡重要!)



  • perspective属性定义了观察者和Z=0平面之间的距离,通俗的说 就是屏幕 到消失点的距离,从而影响3D元素的透视效果, 而transform-style属性决定了是否保留元素的三维空间布局

  • 当我们只使用perspective属性时,只有被明确设置为3D转换的元素才会显示透视效果。而当我们使用transform-style: preserve-3d时,即使元素本身没有进行任何3D转换,其子元素也可以进行3D转换并保留三维空间布局。


注意:perspective属性,只能带来近大远小的透视视觉效果,并不能构成真正的3D空间布局。真正的3D布局必须依赖于transform-style: preserve-3d属性来实现


知识点五、animation动画的定义和使用


CSS动画是一种使元素从一种样式逐渐改变为另一种样式的方法。这个过程是通过关键帧(keyframes)来定义的,关键帧定义了动画过程中的不同状态。 在一镜到底的案例中, 整个场景的前后移动,用的就是动画属性。


动画的使用分为两步, 具体使用方式如下:



  • 1.使用@keyframes 来定义动画

  • 2.使用animation属性来调用动画,



@keyframes rotate {
from { transform: rotateX(0deg); }
to { transform: rotateX(360deg); }
}

在这个例子中,我们定义了一个名为“rotate”的关键帧动画,使元素从X轴的0度旋转到360度。然后,我们可以通过将这个动画应用到HTML元素上来使用它:


	.perspective-content {  
animation: rotate 5s infinite linear;
}

在这个例子中,我们将“rotate”动画应用到.cube元素上,设置动画时间为5秒,无限循环,并且线性过渡;


在一镜到底的案例中, 我们定义的动画如下:



@keyframes perspective-content {

0% {
transform: translateZ(0px);
}

50% {
transform: translateZ(6000px);
}

50.1% {
transform: translateZ(-6000px);
}

100% {
transform: translateZ(0px);
}
}


上午动画 其实做了一个无线循环轮播的逻辑, 就是当 在Z轴方向上 从 0 移动到 6000距离以后, 在重置到-6000px, 这样就可以在从-6000继续向前移动, 移动到 0 ,达到一个循环, 再开始下一次的循环;



  • 小技巧: 你可以把动画 单独加给每个场景(可能有10多个子元素, 你的重复写10多遍,会很麻烦的),也可以把动画加给公共的父元素,父元素会带着里面的子元素一起动, 这样只用写一次就行哦;


结束语:


以上就是案例用到的所有知识点啦, 整个案例的代码,可以在顶部源码位置查看,我就不一一解释了, 如有疑问和建议,可以留言,一起探讨学习哦, 本人能力有限, 希望大家多多批评指导;


作者:IT大春哥
来源:juejin.cn/post/7325739662033879090
收起阅读 »

前端实现汉堡菜单

web
如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。 单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。 在...
继续阅读 »

如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。


单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。


在这篇文章中,我们将向您展示如何在 CSS 中创建不同的汉堡菜单动画。让我们开始吧!


创建汉堡菜单


要创建汉堡菜单,我们首先需要创建 HTML 。由一个按钮元素和三个嵌套的 div 元素组成,每个元素代表汉堡图标的一行。


<button class="hamburger">
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
</button>

接下来,我们将为元素添加一些基本样式。我们将从按钮元素中删除任何默认样式,包括背景和边框颜色。


.hamburger {
background: transparent;
border: transparent;
cursor: pointer;
padding: 0;
}

然后,对于每个线元素,我们将设置背景颜色、高度、宽度和每个直线之间的间距。


.hamburger__line {
background: rgb(203 213 225);
margin: 0.25rem 0;
height: 0.25rem;
width: 2rem;
}

X


是时候使用 CSS 创建一个很酷的汉堡菜单动画了。当用户将鼠标悬停在按钮上时,我们希望线条转换为“X”形。


为了实现这一点,我们将使用  :hover  伪类和  nth-child  选择器来访问每一行。我们将使用  translate() 和  rotate() 函数将线条转换为 X 形状。


第一条线将在 y 轴上向下移动并旋转 45 度以创建一条 X 形状的线。第二行将通过将其不透明度设置为零而消失。最后一条线将在 y 轴上向上移动,并逆时针方向旋转 45 度以完成 X 形状。我们将通过在  translate()rotate()  函数中使用负值,将其转换为与第一行相反的方向。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px) rotate(45deg);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-45deg);
}

若要应用转换,我们将使用该 transition 属性。动画将使用 ease-out 计时功能运行 300 毫秒 (0.3s)。该 all 值表示将对样式更改进行动画处理,包括 transformopacity 属性。


.hamburger__line {
transition: all 0.3s ease-out;
}

通过将鼠标悬停在按钮上来尝试一下。



形成减号


在这种方法中,当按钮悬停在按钮上时,我们会将其变成减号。我们将使用与上一种方法相同的转换,但我们不会旋转第一行和最后一行。


相反,我们将在 y 轴上向下移动第一行,直到它到达第二行。第三条线将向上移动,直到到达第一行。然后,第二行将关闭可见性,就像在前面的方法中一样。


第一行和最后一行的 `transform` 属性将与前面的方法相同,只是我们将不再使用该 `rotate()` 函数。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px);
}

看看它是什么样子的!



要将按钮变成减号,我们可以使用另一种效果,将第一行和最后一行水平移出按钮。我们将使用该 translateX() 函数来指示位置仅在 x 轴上发生了变化。使用 translateX(-100%) ,可以将目标从左向右移出容器,而使用translateX(100%) ,我们可以做相反的事情。


这两种转换都将 opacity 属性设置为零,使第一行和最后一行不可见。因此,动画完成后,只有第二行仍然可见。


.hamburger:hover .hamburger__line:nth-child(1) {
opacity: 0;
transform: translateX(-100%);
}

.hamburger:hover .hamburger__line:nth-child(3) {
opacity: 0;
transform: translateX(100%);
}

看看这如何重现减号。



形成加号


在本节中,我们将向您展示另一种类型的转换。当用户将鼠标悬停在按钮上时,它会变成一个加号。为了达到这种效果,我们将第一条线向下移动,直到它与第二条线相遇,从而形成一条水平线。


然后,我们移动 y 轴上的最后一条线并将其逆时针旋转 90 度,形成加号的垂直线。最后,我们调整 opacity  第二行,使其在动画后不可见。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-90deg);
}

查看下面的演示,了解这种方法的实际应用。



形成箭头


为了在按钮上创建箭头,我们使用简单的转换技术。第一条线旋转 45 度并沿 x 轴和 y 轴移动,直到它与第二条线的第一个点相交,形成箭头的顶线。然后,我们减小第一行的宽度,使其看起来更时尚。将相同的转换应用于最后一行,以创建箭头的底线。


如果需要调整箭头的位置,请随意调整传递给 translate() 函数的值。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(-2px, 4px) rotate(-45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(-2px, -4px) rotate(45deg);
width: 16px;
}

当您将鼠标悬停在按钮上时,箭头的样子如下:



要更改箭头的方向,请调整 translate() 函数的参数。这将确保第一行和最后一行到达第二行的末尾,并且箭头将沿相反方向旋转。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(17px, 4px) rotate(45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(17px, -4px) rotate(-45deg);
width: 16px;
}


原文:phuoc.ng/collection/…


作者:关山月
来源:juejin.cn/post/7325040809698656256
收起阅读 »