注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

将一个图片地址转成文件流(File)再上传

web
写在开头 最近,小编在业务中遇到一个图片转存的场景。 领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。 我😃:Em....
继续阅读 »

写在开头


最近,小编在业务中遇到一个图片转存的场景。


领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。


14DBA96E.gif


我😃:Em...很合理的需求。


(但是,和有什么关系?我只是一个前端小菜鸡呀,不祥的预感.......)


我😃:(卑微提问)这个过程不是放后端做比较合理一点?


后端大哥😡:前端不能做?


我😣:可以可以,只是...这个好像会跨域?


后端大哥😠:已经配置了请求头('Access-Control-Allow-Origin': '*')。


我😖:哦,好的,我去弄一下。(*******此处省略几万字心理活动内容)


14F03F73.jpg

第一种(推荐)


那么,迫于......不,我自愿的,我们来看看前端要如何完成这个转成过程,代码比较简单,直接贴上来瞧瞧:


async function imageToStorage(path) {
// 获取文件名
const startIndex = path.lastIndexOf('/');
const endIndex = path.indexOf('?');
const imgName = path.substring(startIndex + 1, endIndex);
// 获取图片的文件流对象
const file = await getImgToFile(path, imgName);
// TODO: 将File对象上传到其他接口中
}

/**
* @name 通过fetch请求文件,将文件转成文件流对象
* @param { string } path 文件路径全路径
* @param { string } fileName 文件名
* @returns { File | undefined }
*/

function getImgToFile(path, fileName) {
const response = await fetch(path);
if (response) {
const blob = await response.blob();
const file = new File([blob], fileName, { type: blob.type });
return file;
}
}

上述方式,在后端配置了允许跨域后,正常是没有什么问题的,也是比较好的一种方式了。😃


但是,在小编实际第一次编码测试后,却还是遇上了跨域。😓


image.png


一猜应该就是后端实际还没配置好,问了一下。


后端大哥😑:还没部署,一会再自己试试。


我😤:嗯嗯。


第二种


等待的过程,小编又在网上找了找了,找到了第二种方式,各位看官可以瞧瞧:


/** @name 将图片的网络链接转成base64 **/
function imageUrlToBase64(imageUrl: string, fileName: string): Promise<File> {
return new Promise(resolve => {
const image = new Image();
// 让Image元素启用cors来处理跨源请求
image.setAttribute('crossOrigin', 'anonymous');
image.src = imageUrl + '&v=' + Math.random();
image.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext('2d')!;
context.drawImage(image, 0, 0, image.width, image.height);
// canvas.toDataURL
const imageBase64 = canvas.toDataURL('image/jpeg', 1); // 第二个参数是压缩质量
// 将图片的base64转成文件流
const file = base64ToFile(imageBase64, fileName);
resolve(file);
};
});
}
/** @name 将图片的base64转成文件流 **/
function base64ToFile(base64: string, fileName: string) {
const baseArray = base64.split(',');
// 获取类型与后缀名
const mime = baseArray[0].match(/:(.*?);/)![1];
const suffix = mime.split('/')[1];
// 转换数据
const bstr = atob(baseArray[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
// 生成文件流
const file = new File([u8arr], `${fileName}.${suffix}`, {
type: mime,
});
return file;
}

这第二种方式由于要先把图片绘制到 canvas 再去转成 base64 再去转成文件流,小编用 console.time 稍微测了一下,每次转化过程都要几百毫秒,图片越大时间越长,挺影响性能的。


所以,小编还是推荐使用第一种方式,当然,最稳妥的方案是后端去搞最好了。😉



网上很多都说第二种方式可以直接绕过跨域,各种谈论。😪


主要就是这个 crossOrigin 属性。MDN解释


它原理是通过了 CORS


image.png


或者可以再看看这个解释:传送门










至此,本篇文章就写完啦,撒花撒花。


image.png


希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。


作者:橙某人
来源:juejin.cn/post/7336756027385872424
收起阅读 »

年终总结:工作三年,满腔热血已然消散

毕业到现在也已经有三年多的工作经验了,最开始坚信努力就会有结果,到被社会毒打后开始迷茫,一直在思考生活和工作之间的关系和意义。目前是彻底的技术躺平,如今供需关系变化了,单纯的提升技术也不见得能找到一份好的工作,所以有点学不动技术了,感觉学习技术的价值不如以前大...
继续阅读 »

毕业到现在也已经有三年多的工作经验了,最开始坚信努力就会有结果,到被社会毒打后开始迷茫,一直在思考生活和工作之间的关系和意义。目前是彻底的技术躺平,如今供需关系变化了,单纯的提升技术也不见得能找到一份好的工作,所以有点学不动技术了,感觉学习技术的价值不如以前大了,还不如学点软技能,或者看看书拓宽视野也好。好在人没有躺平,一直在尝试寻找一个破局的契机。既然改变不了环境,那就先改变自己吧。



说说以前


第一份工作


刚毕业的第一份工作工资很低而且996,花了很多钱去报名学习编程技术的课程,当时觉得只有技术上去了薪资才会随之上涨,果不其然,最开始先投资自己才能更快提高身价的方式很对,来年跳槽工资是第一份工作的2.X倍吧,一方面当时环境还没这么糟糕,另一方面HR问期望薪资的时候自己也比较敢开价,主要是当时学了不少技术课程,面试的时候也是一顿Vue源码输出,所以当时底气比较足敢报价,现在回想起来确实有点狮子大开口的感觉[笑哭]。


第二份工作


第二份工作虽然比第一份工作的薪资高了很多,不过好景不长,待遇上去了,伴随而来的是更多的挑战和更大的压力,我倒是不抵触压力和挑战,适当的压力会让人更快的成长,我抵触的是那家公司的“坐牢”文化,明面上写着5点半下班,暗地里大搞工时考核,工作日至少需要待够11.5个小时,工时不够的就会被谈话了,就算没啥事也得待在公司刷工时,后面11.5都没法满足了。


休养


人到底能有多坏?一直以为大家同是打工人,矛盾是一致对外的,直到碰到了上家公司领导无底线的压榨、转正临近人事威胁劝退的恶心事之后,才体验到真正的险恶和残酷。没了工作后,开始怀疑自我,变得迷茫,每天都要出去走走,不让自己闷在屋子里,或许是阅历不足、亦或是承受能力不够吧,能熬过去的都会有成长。


休息了近半年才重新开始找工作,这半年时间基本上是边做个低代码开源,边外出散心的状态,见见老友,吃饭叙旧,也尝试了很多新奇的体验。


新工作入职前去了趟南通,在大学室友那住几天,算起来我俩都是无业游民,他住的附近有一个很大的批发市场,海鲜还算便宜种类也多,每天都让他骑着小电驴载着我去,我俩连续吃了很多顿海鲜,吃饱后骑着小电驴去看山山水水、逛逛公园,晚上回来一起开黑。对比一线城市的物价,感觉那的东西还挺实惠,可能我们是工作日去逛的,车少人也少,没有大城市的拥挤和喧嚣,也没有匆忙的旅人,突然间感觉生活节奏变得很慢,原来很多事情都可以慢慢来。


不过小城市的工作收入确实很低很多,没有收入来源的话,这种生活不会持续太久,活在当下尽情享受当前的每分每秒。


回到现在


现在这份工作


当时休息完开始找工作的时候,知道环境不好,心里已经有一个预期了,不过这难找的程度还是超过了我的预期,找的时间越长人越焦虑,虽然焦虑并没有什么正向作用,反而让人睡不着吃不好,但很难控制得住不去焦虑,当时想着随便有个外包都去了,好在最终结果是好的,拿到了唯一一份offer,也就是现在这份工作,没有太多的考虑的余地就买机票去了。


这份工作虽然降薪了,但是双休不加班,工作上也没什么太大的压力,下班后晚上就去跑步,很喜欢跑完大汗淋漓的感觉。


不过最近公司好像是收益不太行了,少了很多人,收益不行的时候就开始抓考勤、算考核绩效,几个人的活压到一个人身上,见怪不怪了,一般公司开始走下坡路的时候就出现这种操作。总感觉码农这份工作抗风险能力还是很差的,要资源没资源,要人脉没人脉,多数劳动合同上还限制了不能干副业,风险来临前要早做打算才是。


谈谈未来


2023年完成的目标


1、看完5本书(趁着地铁通勤时看的,对扩宽视野很有帮助)


2、坚持跑步(每个工作周至少去跑一次)


3、打卡背单词(一直想提升英语能力,不过感觉每天只是花点时间背单词的话,还是提升太慢了)


4、附近城市旅游(珠海跨年、清远漂流、桂林竹筏、江门海岛沙滩等,基本都是周末去的,玩得也很开心,节假日的时候人多不想去挤)


5、交朋友(总感觉这一年交到的朋友太少了,想玩个桌游都凑不够人)


2024年我想要做什么


1、雅思,目标定在5.5以上吧(我的英语一直很差很差,以前没怎么学,考试基本靠蒙,想着年后报个班提升一下效率,在英语上欠下的债总归要还的)


2、看书(闲的时候多看几本书)


3、旅游(旅游是一个享受世界的过程,我喜欢慢慢悠悠的,总想着没工作的时候去一趟新疆或者内蒙,估摸着能住一个月吧,就想静静的在一望无际的草原上躺这么几天)


4、出去外面看看(护-照最近也办下来了,想体验更多的风土人情,见识更宽阔的世界)


5、运动(养生和跑步不能停,目标累计到一千公里以上吧,23年已累计到八百公里了,剩下两百公里应该洒洒水)


6、交朋友(交到更多的朋友,偶尔想去玩一些东西,总是没有合适的朋友一起)


寻破局之路


什么局


总觉得程序员这个职业稳定性差,没什么安全感,要么是三十五岁槛,要么是身体扛不住,不乏有些大咖支招转管理岗,可现实就是管理坑位就这么些,绝多大数人是坐不上的,再者拼命去卷竞争管理岗,到时身体垮了,ICU住一住,抵不住可能会人财两空。到时候年龄大了失业了,找不到工作,即没人脉和资源,也不会别的专业技能,这种可预见性的危机可以提前做些准备,避免走到这一步。


如何破


感觉自己总是走不出眼前的一亩三分地,不停的在这小圈子里兜兜转转,世界规则的条条例例已经把人框在一个范围里了,人生应该是旷野而不是轨道,如果自己再给自己设限,那么这个框将更小。很多时候出于对未知的恐惧,没勇气踏出第一步。我给自己找了两个破土而生的方向:1、换个赛道;2、换个环境,若当下环境找不到破局的希望,那就朝外面看看,试着换个环境,让变数更大。


两个方向的结果未定,好在变数够大,不至于一眼就看到头。


作者:Daw
来源:juejin.cn/post/7330521390510866495
收起阅读 »

热爱前端,也没能逃过七年之痒

大家好,我是杨成功。 从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。 以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工...
继续阅读 »

大家好,我是杨成功。


从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。


以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工程师,但是工作内容已经离前端越来越远了。


以前我觉得做一个骨灰级程序员、掌握各种牛逼的技术是毕生目标;现在我会想人生精彩多样,多尝试一些不一样的事情不也同样有趣?


1-3 年:热爱、探索


我参加工作很早,二十出头。那时候啥也不懂,但是精力旺盛啥也想学,经常写代码到凌晨 2 点也不觉得累。有一部分人选择前端是因为简单,我就是纯粹的喜欢前端。


前端中有很多好玩的东西,比如各种动画、特效,我都非常感兴趣。在工作中常常因为研究出一种“高级”的写法、实现了某个“牛逼”的功能而沾沾自喜。虽然现在看起来很小儿科,但想起来真让人怀念。


我的第一份工作工资很低(<3k),应该比 95% 的前端都低。当时没有经验,心里想着只要能学到东西就成。在那家公司干了一年多,公司用到的技术基本都学了一遍,进步飞快。“又穷又爱”的状态估计以后再也不会有了。


3-5 年:积累、挑战


工作三年多的时候,我换了家公司,带一个前端小团队,每天都扎在项目里。以前总是追求新技术,怎么花哨怎么来。可负责项目后才发现,解决问题和快速产出才是第一位。


当时的前端非常火热,全社会都是跳槽的机会,跳槽等于涨薪。于是面试变得千奇百怪,大家在卷各种原理、源码、八股文,不管面不面试刷题成了必修课。很多开发者们非常讨厌这些东西,但是又不得不去做。


当然也有好处,就是各种新技术层出不穷。虽然很多都是轮子,但确实有不少突破性的技术,帮助传统前端接触到更广的技术面,能做更多的事情。


我没有花大量时间刷面试题,新技术倒是跟了不少,而且很多都用在了项目中。像 JS 原理题、算法题、某些框架的源码之类,我基本没怎么看过;但是像 Node.js、Android、Linux、跨端开发这些,我花了很多的时间研究,因为确实可以解决项目中的问题。


我一直认为我属于“外卷”类型的:Title 一直是前端,但从不认为自己只是一个前端。什么技术都想试试。所以后来我承担过很多攻坚的角色,像服务器、原生 App、音视频等。我发现能让我上头的可能并不是前端,而是搞定一个难题的快感。


得益于这种心态吧,五年内我积累了很多,但我认为收获最大的是习惯了面对挑战。


5-7 年:瓶颈、迷茫


工作五年以上,年龄直逼 30 岁,好像一瞬间就老了,可我总觉得自己还是个孩子。这个时候总会问自己:我的工作有什么意义?我要一直这样下去吗?我想要什么样的生活?


我是在第 6 年的时候感受到了瓶颈。技术方面一直在进步,但对项目的帮助越来越小———项目进入了稳定期。稳定期意味着没有了涨薪的机会,工作重点逐渐从“怎么实现”变成了“怎么汇报”。以前写日报是“汇总成果”,现在变成了“显得有事可做”。


可能任何一家产品成熟的公司都是这样吧,我不习惯,我还在适应阶段。


从今年开始,我最大的迷茫是工作与生活如何平衡。我在北京这几年,大部分精力都扑在了工作上,家人离的很远,每年见个一两次,也没把谈女朋友当回事。想和家人朋友在一块,可工作又不能放弃。成年人说自己不做选择全都要,而我好像只能二选一。


以前一门心思地想靠技术跳槽、进大厂,今年突然觉得没意思。看到很多人被裁员、加班、互卷,我突然想也许现在挺好的呢?双休不加班、领导也 Nice、没有绩效考核、办公室关系也简单。是不是以前自己太浮躁了,没有好好享受当下呢?


所以,要不要继续写代码?还是回老家做别的事?工作上要不要再卷一点?努力攒钱还是趁年轻消费?要不要参加相亲考虑结婚?一连串的问题汹涌而来。


有些问题能想明白,有些问题还是不明白,但更多的是想明白了也做不到。人的成长流失最快的是勇气,可能某天一件意料之外的事情,会让你一下子做出决定。


写了一本书


工作五年之后,我常常会思考一个问题:如果有一天不做程序员了,我还能干什么?


程序员大概都不喜欢社交吧,或者不擅长社交。我特别羡慕大圣老师,他可以把自己的知识通过视频很生动的表达出来。但我就不行,我好像对镜头恐惧,尝试过好多次全身的不自在。


录视频有难度,不过写文章还行。正好积累了很多知识经验,一边总结一边练笔,于是开始写掘金。后来又碰到个机会写书,我就觉得这个更好,可以把这么多年的经验总结浓缩到一本书里。或许可以帮助一些前端朋友快速进阶,或许还能赚点稿费。


这本书名叫 《前端开发实战派》


之后怎么走


七年之前觉得我会写代码到 70 岁,直到写不动了为止。七年之后,我最喜欢的工作依然是程序员,但我不再执着于能不能干到 35 岁了。世界还有很多不一样的精彩,我不能把自己困在程序里。


与那些大厂大佬们相比,我赚的不多,心气也不高。没有想过一定要留在大城市,也不觉得以后有了小孩,就一定要奔着“好的教育”和“名校”去卷,太累了。其实只要没有大城市和名校的执念,生活压力也不会那么大。


这样来看,如果有一天我被裁了,其实也没什么可担心的。选择一个离家近的地方,没有大都市的物欲和诱惑,过一些简单轻松的生活,或许并不糟糕。只是身在大城市,面对万千繁华仿佛难以自拔,但你心里好像知道这不是你追求的,却又停不下来。


我有一个预感,可能 30 岁后不再做程序员了,至少不会只埋头钻研技术。做前端这几年让我在各方面成长迅速,不过做久了也有弊端,比如表达能力、社交能力退化,不擅长处理人际关系,不直接接触商业,而这些往往是人生下半场,决定幸福和事业的关键。


但我依然喜欢技术。无论做什么,技术都会是我自己的优势。


我们大老板是技术出身,孩子都上小学了,还经常熬夜帮我们处理技术难题。有次聚会我问他,公司那么多事情要忙,怎么还有精力写代码呢?他说写代码就是我最放松的时候。我不由得一阵佩服,或许这就是技术人的魅力吧。


但在 30 岁之前,我会继续站在技术一线,做一个什么都搞的前端人。


作者:杨成功
来源:juejin.cn/post/7295551745580793919
收起阅读 »

360周鸿祎为什么说大模型已成茶叶蛋?

大模型炒了一年,为什么没有特别火的应用? 最近几天360创始人周鸿祎称,去年感觉大模型是原子弹,今年感觉是茶叶蛋。 什么意思?我想大概就是说大模型谁都能玩了,现在国内的大模型没有一千,也有几百个了,大模型没什么稀奇的了。但是另一方面也反映了大家都是为了大模型而...
继续阅读 »

大模型炒了一年,为什么没有特别火的应用?


最近几天360创始人周鸿祎称,去年感觉大模型是原子弹,今年感觉是茶叶蛋。


什么意思?我想大概就是说大模型谁都能玩了,现在国内的大模型没有一千,也有几百个了,大模型没什么稀奇的了。但是另一方面也反映了大家都是为了大模型而大模型,但是大模型没能解决什么实际问题,或者说解决的问题太小,有点让人失望了。



邓宁-克鲁格效应


我认为这种感觉是很正常的,也符合事物的一般发展规律,一个新事物出现的时候,大家都抱着很大的期望,期待它去解决各种各样的问题,但是毕竟是新东西,和整个世界的磨合、整合还不够,还需要各种去适配,所以新鲜劲儿过去之后,很多问题还是没解决,大家就感觉失望了。然后这个新事物还要默默的发展一段时间,才有机会重回梦想之巅。


这种情况有一个名词:邓宁-克鲁格效应(Dunning-Kruger Effect),也简称达克效应(D-K Effect),可以用下边这条曲线来理解它。达克效应本来说的是人的认知过程,但也经常被用来表示事物的发展过程。



AI大模型的下一步


AI大模型下一步会怎么发展?我认为首先还是要紧盯OpenAI,作为大模型的引爆者和引领者,OpenAI的发展方向至关重要。


去年底OpenAI推出了GPTs,也就是大模型的应用商店,为什么干这件事?我认为是因为AGI发展遇阻,技术和资金都有点跟不上,这一点可以从最近OpenAI投资AI芯片、大规模融资,以及OpenAI CEO奥特曼让大家耐心等待AGI等等事件中略窥一二。为了提振信心,探寻更多机会,OpenAI不得不搞出这个应用商店,借助外部的更多力量来促进AI的发展。


另外预计OpenAI今年就会发布GPT-5,大模型的能力进一步增强。据预测,GPT-5将是一个原生的多模态大模型,不仅能处理文本和图像,还能处理音视频内容,GPT-5甚至将会具备自主的AI模型开发能力,这将使其能够生成各种多模态的AI模型,从而学习和完成新的任务,这将大大扩展GPT-5的应用能力,有力推动通用机器人的发展,给人很多的想象空间。


GPT-5是更好吃的茶叶蛋,还是更厉害的氢弹?让我们拭目以待!



大模型和世界的磨合


另外上边我提到大模型需要和世界进行磨合,怎么磨合?


我认为第一步就是将AI能力融入到企业的产品或者服务中去。我们现在可以看到很多工具都集成了AI大模型,比如钉钉魔法棒、WPS AI助手、Photoshop AI绘画功能等等,现在也有了一些AI商用产品,比如AI客服、AI培训、AI教育等等方面,还有很多看起来不起眼的AI写作、AI绘画、AI编程等等,他们都在慢慢的渗透到各行各业,这些已经在潜移默化的发生,慢慢的改变工作方式,提升效率。


虽然还没看到可以持续爆火的应用,也许只是磨合的不够,是黎明前的黑暗。


对于大家特别期待的AI原生应用,或许可以小小的期待下GPT-5。


不过我认为不管是AI+应用还是AI原生应用,最重要的是要解决确定性的问题,解决可能产生的错误或不准确的预测结果,否则大家只能把它当做一个玩具,或者只用在某些比较小的场景,无法做到各行各业遍地开花,也就无法推动整个世界的变革与发展。




以上就是本文的主要内容,欢迎留言一起讨论。


作者:萤火架构
来源:juejin.cn/post/7329782406540853286
收起阅读 »

NestJS 依赖注入DI与控制反转IOC

web
1. 前言 在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Cont...
继续阅读 »

1. 前言


在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Controller 中。


2. 概念


2.1 依赖注入、控制反转、容器


何为容器?容器是可以装一些资源的。举个例子,普通的容器,生活中的容器比如保温杯,只能用来存储东西,没有更多的功能。而程序中的容器则包括数组、集合Set、Map 等。


而复杂的容器,生活中比如政府,政府管理我们的一生,生老病死和政府息息相关。


AB7410FF-F52F-4C58-9171-DFB6303157DD.png


程序中复杂的容器比如NestJS 容器,它能够管理 Controller、Service 等组件,负责创建组件的对象、存储 组件的对象,还要负责 调用 组件的方法让其工作,并在一定的情况下 销毁 组件。


依赖注入(Dependency Injection)是实现控制反转的一种方式。控制反转又是什么呢?控制反转(Inversion of Control)是指从容器中获取资源的方式跟以往有所不同。


2.2 为什么需要控制反转


2.2.1 依赖关系复杂、依赖顺序约束


后端系统中有多个对象:



  • Controller 对象: 处理 HTTP 请求,调用 Service,返回响应。

  • Service 对象: 实现业务逻辑。

  • Repository 对象: 实现对数据库的增删改查。


此外,还包括数据库链接对象 DataSource、配置对象 Config 等等。这些对象之间存在复杂的关系:



  • Controller 依赖 Service 实现业务逻辑。

  • Service 依赖 Repository 进行数据库操作。

  • Repository 依赖 DataSource 建立连接,而 DataSource 则需要从 Config 对象获取用户名密码等信息。


这导致对象的创建变得复杂,需要理清它们之间的依赖关系,确保正确的创建顺序。例如:


const config = new Config({ username: 'xxx', password: 'xxx'});
const dataSource = new DataSource(config);
const repository = new Repository(dataSource);
const service = new Service(repository);
const controller = new Controller(service);

这些对象需要一系列初始化步骤后才能使用。此外,像 config、dataSource、repository、service、controller 这些对象不需要每次都新建一个,可以保持单例。在应用初始化时,需要明确依赖关系,创建对象组合,并确保单例模式,这是后端系统常见的挑战。


2.3.2 高层逻辑直接依赖低层逻辑,违反依赖倒置规范



依赖倒置: 什么是依赖倒置原则(Dependency Inversion Principle)高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。  抽象不应该依赖细节,细节(具体实现)应该依赖抽象。 



1.举一个工厂例子,初始化时有工人、车间、工厂。


2FE7E55F-42A4-48FF-9A6B-8E987E386A7F.png


1.工厂是容器,车间是消费者,依赖工人和工人的服务,工人是依赖,是生产者。


// 工人
class Worker {
  manualProduceScrew(){
    console.log('A screw is built')
  }
}

// 螺丝生产车间
class ScrewWorkshop {
  private worker: Worker = new Worker()
 
  produce(){
    this.worker.manualProduceScrew() // 调用工人的方法
  }
}

// 工厂
class Factory {
  start(){
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()



2.现在要把工人制造改为机器制造,需要直接在车间把工人制造改为机器制造,麻烦。


// 机器
class Machine {
  autoProduceScrew(){
    console.log('A screw is built')
  }
}

class ScrewWorkshop {
  // 改为一个机器实例
  private machine: Machine = new Machine()
 
  produce(){
    this.machine.autoProduceScrew() // 调用机器的方法
  }
}

class Factory {
  start(){
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()


3.此时考虑依赖倒置原则,通过实现生产者接口来处理(PS:这也是为什么像 Java 语言中,实现业务服务时需要定义接口类和实现类,遵循依赖倒置,方便切换不同的业务逻辑)


// 定义一个生产者接口
interface Producer {
  produceScrew: () => void
}

// 实现了接口的机器
class Machine implements Producer {
  autoProduceScrew(){
    console.log('A screw is built')
  }
 
  produceScrew(){
    this.autoProduceScrew()
  }
}

// 实现了接口的工人
class Worker implements Producer {
  manualProduceScrew(){
    console.log('A screw is built')
  }
 
  produceScrew(){
    this.manualProduceScrew()
  }
}

class ScrewWorkshop {
  // 依赖生产者接口,可以随意切换啦!!!
  // private producer: Producer = new Machine()
  private producer: Producer = new Worker()
 
  produce(){
    this.producer.produceScrew() // 工人和机器都提供了相同的接口
  }
}

class Factory {
  start(){
    const screwWorkshop = new ScrewWorkshop()
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()



4.工厂改造后,螺丝生产车间的改造变得更容易了,只需要改变其属性中所新建的遵循Producer接口的实例即可。然而,这并没有完全改善我们与车间主任之间的关系,每次厂里在改造生产机器时我们还是需要麻烦车间主任。这是因为我们还是没有完全遵守依赖倒置原则,ScrewWorkshop 仍然依赖了 Worker/Machine的实例,只不过这种依赖相较之前少了一点罢了。


要完全遵守依赖倒置原则,需要使用控制反转依赖注入


2.3 控制反转思想


2.3.1 获取资源的传统方式



  • 自己做饭:买菜、洗菜、择菜、切菜、炒菜,全过程参与,费时费力,必须了解资源创建整个过程中的全部细节并熟练掌握。

  • 在应用程序中,组件需要获取资源,传统的方式是主动从容器中获取所需资源,这样开发人员需要知道具体容器中特定资源的获取方式,增加了学习成本,也降低了开发效率。


2.3.2 获取资源的控制反转方式



  • 点外卖:下单、等待、吃外卖,省时省力,不必关心资源创建过程的全部细节。

  • 控制反转的思想改变了应用程序组件获取资源的方式,容器会主动将资源推送给需要的组件,开发人员只需要提供接收资源的方式即可,这样可以降低学习成本,提高开发效率。这种方式被称为查找的被动方式。


2.4 如何实现控制反转


起源:许多应用程序的业务逻辑实现需要两个或多个类之间的协作,这种协作使得每个对象都需要获取与其合作的对象(即其所依赖的对象的引用)。如果这种获取过程由对象自身实现,那么将导致代码高度耦合,难以维护和调试。


技术描述


在 Class A 中,我们使用了 Class B 的对象 b。通常情况下,我们需要在 A 的代码中显式地使用 new 来创建 B 的对象。但是,如果采用依赖注入技术,A 的代码只需要定义一个 private 的 B 对象,而不需要直接 new 来获取这个对象。相反,我们可以通过相关的容器控制程序来在外部创建 B 对象,并将其注入到 A 类中的引用中。具体获取的方法以及对象被获取时的状态由配置文件(如 XML)来指定。这种方法可以使代码更加清晰和正式。


loc 也可以理解为把流程的控制从应用程序转移到框架之中。以前,应用程序掌握整个处理流程;现在,控制权转移到了框架,框架利用一个引擎驱动整个流程的执行,框架会以相应的形式提供一系列的扩展点,应用程序则通过定义扩展的方式实现对流程某个环节的定制。“框架Call 应用”。基于 MVC 的 web 应用程序就是如此。


实现方法


实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类 A 的实例创建过程中即创建了依赖的 B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的事件也可以在代码中自由控制。


细说


1.依赖注入:



  • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象

  • 基于set方法。实现特定属性的publicSet方法,来让外部容器调用传入所依赖类型的对象

  • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象

  • 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。


2.依赖查找


依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。


2.4.1 工厂例子依赖注入改造


通过以上学习,现在把工厂例子代码进一步改造,将底层类的依赖,由从中间类直接引用,变为高层类在构造时的依赖注入


// ......Worker/Machine及其所遵循的接口Producer的实现与此前一致,此处省略

class ScrewWorkshop
  private producer: Producer
 
  // 通过构造函数注入
  constructor(producer: Producer){
    this.producer = producer
  }
 
  produce(){
    this.producer.produceScrew()
  }
}

class Factory {
  start(){
    // 在Factory类中控制producer的实现,控制反转啦!!!
    // const producer: Producer = new Worker()
    const producer: Producer = new Machine()
    // 通过构造函数注入
    const screwWorkshop = new ScrewWorkshop(producer)
    screwWorkshop.produce()
  }
}

const factory = new Factory()
// 工厂开工啦!!!
factory.start()



至此,回顾对这个车间的改造三步



  1. 依赖倒置: 解除ScrewWorkshop与Worker/Machine具体类之间的依赖关系,转为全部依赖Producer接口;

  2. 控制反转: 在Factory类中实例化ScrewWorkshop中需要使用的producer,ScrewWorkshop的对依赖项Worker/Machine的控制被反转了;

  3. 依赖注入: ScrewWorkshop中不关注具体producer实例的创建,而是通过构造函数constructor注入;


3. NestJS 依赖注入


在Nest的设计中遵守了控制反转的思想,使用依赖注入(包括构造函数注入、参数注入、Setter方法注入)解藕了Controller 与 Provider之间的依赖。


我们将Nest中的元素与我们自己编写的工厂进行一个类比:



  1. Provider & Worker/Machine:真正提供具体功能实现的低层类。

  2. Controller & ScrewWorkshop:调用低层类来为用户提供服务的高层类。

  3. Nest框架本身 & Factory:控制反转容器,对高层类和低层类统一管理,控制相关类的新建与注入,解藕了类之间的依赖。


IOC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。


Nest 实现了 IOC 容器,会从入口模块开始扫描,分析 Module 之间的引用关系,对象之间的依赖关系,自动把 provider 注入到目标对象。


Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。并且Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。


provider 一般都是用 @Injectable 修饰的 class:


2864AB0B-6A5F-4C29-AE1C-87AD610BA635.png


在 Module 的 providers 里声明:


27933B1A-306E-43E2-8203-598E1998B6B0.png


上面是一种简写,完整的写法是这样的


4CF33A34-F899-4AA0-9DA0-B93D177C9760.png


构造函数或者属性注入


E9561C4C-D02B-47DD-8B66-D0076666E2A8.png


异步的注入对象


24974844-A158-4FC1-8A14-33A1AF8033FB.png


通常情况下,提供者通过使用 @Injectable 声明,然后在 @Moduleproviders 数组中注册类来实现。默认的 token 是类本身,因此不需要使用 @Inject 来指定注入的 token。


但是,也可以使用字符串类型的 token,但在注入时需要单独指定 @Inject。除了可以使用 useClass 指定注入的类,还可以使用 useValue 直接指定注入的对象。如果想要动态生成对象,则可以使用 useFactory,它的参数也注入到 IOC 容器中的对象中,然后动态返回提供者的对象。如果想要为已有的 token 指定一个新的 token,可以使用 useExisting 来创建别名。通过灵活运用这些提供者类型,可以在 Nest 的 IOC 容器中注入任何对象。


4.实践


之前部门逻辑都是放在 controller 中的,现在可以把逻辑放 dept.service.ts  上来,添加 @Injectable 装饰器。


9102DFFB-B32C-4ECE-8A5E-4D34135A6391.png


在 DeptModule  模块中的 propviders 中引入 DeptService


1397E621-39A4-4EF7-829A-8500C7B7B0B6.png


最后在 dep.controller  使用部门服务,通过 @Inject() 装饰器注入。


C63CF5D6-F08C-440C-9255-5688F35ADA41.png


小结


本文我们学习了什么是容器、依赖注入与控制反转,掌握了 Nest 依赖注入使用,并通过部门服务例子进行演示。


参考资料



作者:jecyu
来源:juejin.cn/post/7336055070508843048
收起阅读 »

前端最全的5种换肤方案总结

web
最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。 方案一:硬编码 对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现...
继续阅读 »

最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。


方案一:硬编码


对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现的方法只能是全局样式替换,工作量比较大,需要更改form表单、按钮、表格、tab、容器等所有组件的各种状态,此外还需更换icon图标。


以下是我们的一个老项目实现主题色更换,全局样式替换接近500行,如下图所示:


image.png


image.png


image.png


总结: 对于这种老项目只能通过硬编码的方式去更改,工作量较大,好在老项目依赖同一个基础库和业务库,所以在一个项目上实现了也可以快速推广到其它项目。


方案二:sass变量配置


团队的基础组件库Link-ui是基于Eelement-ui二次开发,因此可以采取类似于Element-ui的方式进行主题更改,只需要设计师提供6个主题色即可完成主题色的更改,如下所示。


image.png



  • 配置基础色
    基础色一般需要设计师提供,也可以通过配置化的方式实现,


$--color-primary-bold: #1846D1 !default;
$--color-primary: #2664FD !default;
$--color-primary-light: #4D85FD !default;
$--color-primary-light-1: #9AC0FE !default;
$--color-primary-light-2: #C1DBFF !default;
$--color-primary-lighter: #E8F2FF !default;



  • 从基础库安装包引入基础色和库的样式源文件
    image.png


@import "./common/base_var.scss";

/* 改变 icon 字体路径变量,必需 */
$--font-path: '~link-ui-web/lib/theme-chalk/fonts';

@import "~link-ui-web/packages/theme-chalk/src/index";


  • 全局引入


import '@/styles/link-variables.scss';


  • 更换主题色
    只需要更改上面的6个变量即可实现主题色的更改,比如想改成红色,代码如下


$--color-primary-bold: #D11824 !default;
$--color-primary: #FD268E !default;
$--color-primary-light: #D44DFD !default;
// $--color-primary-light-1: #9AC0FE !default;
$--color-primary-light-2: #DCC1FF !default;
$--color-primary-lighter: #F1E8FF !default;

image.png


总结: 对于基础库和样式架构设计合理的项目更改主题色非常的简单,只要在配置文件更换变量的值即可。它的缺点是sass变量的更改每次都需要编译,很难实现配置化。


方案三、css变量+sass变量+data-theme


代码结构如下:


image.png



  • 设计三套主题分别定义不同的变量(包含颜色、图标和图片)


  // theme-default.scss
/* 默认主题色-合作蓝色 */
[data-theme=default] {
--color-primary: #516BD9;
--color-primary-bold: #3347B6;

--color-primary-light: #6C85E1;
--color-primary-light-1: #C7D6F7;
--color-primary-light-2: #c2d6ff;
--color-primary-lighter: #EFF4FE;

--main-background: linear-gradient(90deg,#4e68d7, #768ff3);

--user-info-content-background-image: url('../../assets/main/top-user-info-bg.png');
--msg-tip-content-background-image: url('../../assets/main/top-user-info-bg.png');
...
}


  // theme-orange.scss
// 阳光黄
[data-theme=orange] {
--color-primary: #FF7335;
--color-primary-bold: #fe9d2e;

--color-primary-light: #FECB5D;
--color-primary-light-1: #FFDE8B;
--color-primary-light-2: #fcdaba;
--color-primary-lighter: #FFF3E8;

--main-background: linear-gradient(90deg,#ff7335 2%, #ffa148 100%);


--user-info-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
--msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
...
}


  // theme-red.scss
/* 财富红 */
[data-theme=red] {
--color-primary: #DF291E;
--color-primary-bold: #F84323;

--color-primary-light: #FB8E71;
--color-primary-light-1: #FCB198;
--color-primary-light-2: #ffd1d1;
--color-primary-lighter: #FFEEEE;


--main-background: linear-gradient(90deg,#df291e 2%, #ff614c 100%);

--user-info-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
--msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
...
}



  • 把主题色的变量作为基础库的变量


$--color-primary-bold: var(--color-primary-bold) !default;
$--color-primary: var(--color-primary) !default;
$--color-primary-light: var(--color-primary-light) !default;
$--color-primary-light-1: var(--color-primary-light-1) !default;
$--color-primary-light-2: var(--color-primary-light-2) !default;
$--color-primary-lighter: var(--color-primary-lighter) !default;


  • App.vue指定默认主题色


window.document.documentElement.setAttribute('data-theme', 'default')

data-theme会注入到全局的变量上,所以我们可以在任何地方获取定义的css变量


image.png


实现效果如下:


image.png


image.png


image.png


总结: 该方案是最完美的方案,但是需对颜色、背景图、icon等做配置,需设计师设计多套方案,工作量相对较大,适合要求较高的项目或者标准产品上面,目前我们的标准产品选择的是该方案。


方案四:滤镜filter


filter CSS属性将模糊或颜色偏移等图形效果应用于元素。滤镜通常用于调整图像,背景和边框的渲染。


它有个属性hue-rotate() 用于改变图整体色调,设定图像会被调整的色环角度值。值为0deg展示原图,大于360deg相当于又绕一圈。
用法如下:


body {
filter: hue-rotate(45deg);
}


产品新建UI单元测试运行录制.gif


总结: 成本几乎为0,实现简单。缺点是对于某些图片或者不想改的颜色需要特殊处理。


方案五:特殊时期变灰



  • filter还有个属性 grayscale() 改变图像灰度,值在 0% 到 100% 之间,值为0%展示原图,值为100% 则完全转为灰度图像。


body {
filter: grayscale(1);
}

image.png


总结: 成本小,可以将该功能做成配置项,比如配置它的生效开始时间和生效结束时间,便于运营维护也不用频繁发布代码。


总结


以上就是实现换肤的全部方案,我们团队在实际项目都有使用,比较好推荐的方案是方案一、方案三、方案五,对于要求不高的切换主题推荐方案四,它的技术零成本,对于标准产品推荐方案三。如有更好的方案欢迎评论区交流。


作者:_无名_
来源:juejin.cn/post/7329573754987462693
收起阅读 »

接手了一个外包开发的项目,我感觉我的头快要裂开了~

嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,...
继续阅读 »

嗨,大家好,我是飘渺。


最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,在这段时间里,我受到了来自"外包"和"微服务"这双重debuff的折磨。


image-20231016162237399


今天,我想和大家分享一下我在这几天中遇到的问题。希望这几个问题能引起大家的共鸣,以便在未来的微服务开发中避免再次陷入相似的困境。


1、服务模块拆分不合理


绝大部分网上的微服务开源框架都是基于后台管理进行模块拆分的。然而在实际业务开发中,应该以领域建模为基础来划分子服务。


目前的服务拆分方式往往是按照团队或功能来拆分,这种不合理的拆分方式导致了服务调用的混乱,同时增加了分布式事务的风险。


2、微服务拆分后数据库并没拆分


所有服务都共用同一个数据库,这在物理层面无法对数据进行隔离,也导致一些团队为了赶进度,直接读取其他服务的数据表。


这里不禁要问:如果不拆分数据库,那拆分微服务还有何意义?


3、功能复制,不是双倍快乐


在项目中存在一个基础设施模块,其中包括文件上传、数据字典、日志等基础功能。然而,文件上传功能居然在其他模块中重复实现了一遍。就像这样:


image-20231017185809403


4、到处都是无用组件堆彻


在项目的基础模块中,自定义了许多公共的Starter,并且这些组件在各个微服务中被全都引入。比如第三方登录组件、微信支付组件、不明所以的流程引擎组件、验证码组件等等……


image.png


拜托,我们已经有自己的SSO登录,不需要微信支付,还有自己的流程引擎。那些根本用不到的东西,干嘛要引入呢?


5、明显的错误没人解决


这个问题是由上面的问题所导致的,由于引入了一个根本不需要的消息中间件,项目运行时不断出现如下所示的连接异常。


image-20231013223714103


项目开发了这么久,出错了这么久,居然没有一个人去解决,真的让人不得不佩服他们的忍受力。


6、配置文件一团乱麻


你看到服务中这一堆配置文件,是不是心里咯噔了一下?


image-20231017190214587


或许有人会说:"没什么问题呀,按照不同环境划分不同的配置文件”。可是在微服务架构下,已经有了配置中心,为什么还要这么做呢?这不是画蛇添足吗?


7、乱用配置中心


项目一开始就明确要使用Apollo配置中心,一个微服务对应一个appid,appid一般与application.name一致。


但实际上,多个服务却使用了相同的appid,多个服务的配置文件还塞在了同一个appid下。


更让人费解的是,有些微服务又不使用配置中心。


8、Nacos注册中心混乱


由于项目有众多参与的团队,为了联调代码,开发人员在启动服务时不得不修改配置文件中Nacos的spring.cloud.nacos.discovery.group属性,同时需要启动所有相关服务。


这导致了两个问题:一是某个用户提交了自己的配置文件,导致其他人的服务注册到了别的group,影响他人的联调;二是Nacos注册中心会存在一大堆不同的Gr0up,查找服务变得相当麻烦。


其实要解决这个问题只需要重写一下网关的负载均衡策略,让流量调度到指定的服务即可。据我所知,他们使用的开源框架应该支持这个功能,只是他们不知道怎么使用。


9、接口协议混乱


使用的开源脚手架支持Dubbo协议和OpenFeign调用,然而在我们的项目中并不会使用Dubbo协议,微服务之间只使用OpenFeign进行调用。然而,在对外提供接口时,却暴露了一堆支持Dubbo协议的接口。


10、部署方式混乱


项目部署到Kubernetes云环境,一般来说,服务部署到云上的内部服务应该使用ClusterIP的方式进行部署,只有网关服务需要对外访问,网关可以通过NodePort或Ingress进行访问。


这样做可以避免其他人或服务绕过网关直接访问后端微服务。


然而,他们的部署方式是所有服务都开启了NodePort访问,然后在云主机上还要部署一套Nginx来反向代理网关服务的NodePort端口。


image-20231016162150035


结语


网络上涌现着众多微服务开源脚手架,它们吸引用户的方式是将各种功能一股脑地集成进去。然而,它们往往只是告诉你“如何集成”却忽略了“为什么要集成”。


尽管这些开源项目能够在学习微服务方面事半功倍,但在实际微服务项目中,我们不能盲目照搬,而应该根据项目的实际情况来有选择地裁剪或扩展功能。这样,我们才能更好地应对项目的需求,避免陷入不必要的复杂性,从而更加成功地实施微服务架构。


最后,这个开源项目你们认识吗?


image-20231017190633190


作者:飘渺Jam
来源:juejin.cn/post/7291480666087964732
收起阅读 »

8年前端,那就聊聊被裁的感悟吧!!

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。 另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的...
继续阅读 »

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。
另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的际遇


我的经历


第一家公司


第一家公司说来也巧,本来是准备入职一家外包的,在杭州和同学吃个饭,接到了面试通知,一看地址就在楼上,上去一共就3轮面试,不到2个小时直接给了offer。有些东西真的就是命中注定


第二家公司


第二家公司我入职以后挖了上家公司诸多墙角,我一共挖了6个前端,2个后端。拯救朋友们于水深火热之中。





我本以为我能开启美好的新生活,结果第二年就传来我父亲重病的噩耗 肺癌晚期,我学习了大量的肺癌知识什么小细胞,非小细胞,基因检测呀等等。。。





可是最后还是没有挽留住他的生命,我记得我俩在最后一次去武汉的时候,睡在一起,他给我说了很多。


他说:治不好就算了,只是没能看到自己的孙子有些可惜罢了。

他说:我这一辈碌碌无为,没给你带来多么优越的条件,结婚、买房、工作都没给到任何帮助,唯一让我感到欣慰的是你那么努力,比我强多了,家里邻居很多都眼馋你呢。
他说:你小孩的名字想好了吗?你媳妇真是个孝顺的孩子,性格也好,心地善良,你要好好对待她。

他说了很多。。。我都快忘了他说了啥了,我不想忘来着,可是可是,想起来就又好难过。


这只是我人生历程的一部分,我把这些讲出来,是为了让大家明白,你现在所经历的困苦其实没有那么严重,人在逆境之中会放大自己的困难,以博得同情。所以现在很多人给我倒苦水的时候,我总有点不屑一顾的感觉,并不是我有多强,我只是觉得都能过去。


在灰暗的时候,工作总是心不在焉,情绪莫名冲动,我和领导吵过架,和ui妹妹撕破脸,导致人家天天投诉我。我leader说我态度极其嚣张,我说你再多说一句,我干死你所以不裁我裁谁


我的人生感悟


我时常以我爸的角度换位思考,我在得知这个消息后我该咋办?是积极面对,还是放弃治疗?可是所有的都是在假设的前提之下,一切不可为真。只有在其中的才最能明白其中的感受。
那一年我看着他积极想活着的毅力,也看到了他身体日渐消瘦的无奈,无奈之余还要应付各种亲戚的嘘寒问暖


我现在很能明白《天道》中那段,丁元英说的如果为了孝顺的名声,让父亲痛苦没有尊严地活着,还不如让父亲走了。 的意思了。在他昏迷不醒的时候,大小便失禁的时候,真不如有尊严的走了。


我其实已经预感到自己要被裁,我原本是挺担心的,可是后来想想父亲的话,我总结成一句话圆滑对事,诚以待人。 这句话看上去前后矛盾,无外乎俩个观点。


圆滑对事的意思是:就是要学会嘴甜,事嘛能少干就少干,能干几分是几分,累的是别人,爽的是自己,在规则中寻求最大的自我利益化。


诚以待人的意思是:圆滑归圆滑,不能对谁都圆滑,你得有把事情办的很好的能力,你需要给真正需要的人创造价值,而不是为了给压榨者提供以自我健康为代价的价值。



用现在最流行的词来说就是「佛系」。


什么叫活明白了,通常被理解为不争不抢,得之淡然、失之泰然、顺其自然的一种心理状态。


活明白的人一般知道自己要什么样的生活,他们不世故、不圆滑,坦荡的、磊落的做自己应该做的事儿。他们与社会上潜规则里的不良之风格格不入,却不相互抵触,甚至受到局中人的青睐与欣赏。


活明白的人看着更为洒脱,得不张扬,失不气馁,心态随和、随遇而安。


不过,还有一种活明白的人,不被多数人所接受。他们玩世不恭、好吃懒做,把所有一切交给命运去背锅。这种人极度自我,没有什么可以超越他自己的利益,无法想象这种活法,简直就是在浪费六道轮回的名额。


总之,有的人活明白了,是调整自己的心态,维护社会的稳定和安宁。有的人活明白了,是以自我为中心,一边依赖着社会救济,一边责备社会龌蹉。


所以,活明白的人也分善与恶,同样是一种积极向善,另一种是消极向恶,二者同出而异名。



我对生活的态度


离职的第一个月,便独自一人去了南京,杭州,长沙,武汉,孝感。我见了很多老朋友,听听他们发发牢骚,然后找一些小众的景点完成探险。


在南京看了看中医,在杭州露营看了看日落,在长沙夜爬了岳麓山,在武汉坐了超级大摆锤,在孝感去了无名矿坑并在一个奶奶家蹭了中午饭。


我的感受极其良好,我体验了前所未有生活态度,我热情待人,嘻嘻笑笑,我站在山顶敞怀吹风,在无尽的树林中悠然自得,治愈我不少的失落情绪。我将继续为生活的不易奔波,也将继续热爱生活,还会心怀感恩对待他人,也会圆滑处事 事事佛系。


背景1.png


图层 1.png


IMG_6214.JPG


IMG_6198.JPG


IMG_6279.JPG


可能能解决你的问题


要不要和家里人说


我屏蔽了家里人,把负面情绪隐藏,避免波及母亲本就脆弱的内心世界,我还骗她说公司今年不挣钱,提前让我们放假,只给基础工资。如果你家境殷实,家庭和睦,我建议大方的说,这样你和父母又多了一个可以聊的话题,不妨和他们多多交流,耐心一些。


裁员,真不是你的问题


请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术,你得让自己变得精彩,别虚度了这如花般的时光。可能你懒,可能也没什么规划,那就想到啥就做啥好了,可能前几次需要鼓足干劲,后面就会发现轻而易举。


如何度过很丧的阶段


沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,多和朋友吹吹牛逼,别把脸面看的那么重要,死皮赖脸反而是一种讨人喜欢的性格。



不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你放平心态,积极应对

如果你也在人生的至暗时刻,也请不要彷徨,时间总会治愈一切

不妨试试大胆一点,生活给的惊喜也同样不少

我在一个冬天的夜晚写着文字,希望能对你有些帮助


作者:顾昂_
来源:juejin.cn/post/7331657679012380722
收起阅读 »

vue3的宏到底是什么东西?

web
前言 从vue3开始vue引入了宏,比如defineProps、defineEmits等。我们每天写vue代码时都会使用到这些宏,但是你有没有思考过vue中的宏到底是什么?为什么这些宏不需要手动从vue中import?为什么只能在setup顶层中使用这些宏? ...
继续阅读 »

前言


vue3开始vue引入了宏,比如definePropsdefineEmits等。我们每天写vue代码时都会使用到这些宏,但是你有没有思考过vue中的宏到底是什么?为什么这些宏不需要手动从vueimport?为什么只能在setup顶层中使用这些宏?


vue 文件如何渲染到浏览器上


要回答上面的问题,我们先来了解一下从一个vue文件到渲染到浏览器这一过程经历了什么?


我们的vue代码一般都是写在后缀名为vue的文件上,显然浏览器是不认识vue文件的,浏览器只认识html、css、jss等文件。所以第一步就是通过webpack或者vite将一个vue文件编译为一个包含render函数的js文件。然后执行render函数生成虚拟DOM,再调用浏览器的DOM API根据虚拟DOM生成真实DOM挂载到浏览器上。


progress.png


vue3的宏是什么?


我们先来看看vue官方的解释:



宏是一种特殊的代码,由编译器处理并转换为其他东西。它们实际上是一种更巧妙的字符串替换形式。



宏是在哪个阶段运行?


通过前面我们知道了vue 文件渲染到浏览器上主要经历了两个阶段。


第一阶段是编译时,也就是从一个vue文件经过webpack或者vite编译变成包含render函数的js文件。此时的运行环境是nodejs环境,所以这个阶段可以调用nodejs相关的api,但是没有在浏览器环境内执行,所以不能调用浏览器的API


第二阶段是运行时,此时浏览器会执行js文件中的render函数,然后依次生成虚拟DOM和真实DOM。此时的运行环境是浏览器环境内,所以可以调用浏览器的API,但是在这一阶段中是不能调用nodejs相关的api


而宏就是作用于编译时,也就是从vue文件编译为js文件这一过程。


举个defineProps的例子:在编译时defineProps宏就会被转换为定义props相关的代码,当在浏览器运行时自然也就没有了defineProps宏相关的代码了。所以才说宏是在编译时执行的代码,而不是运行时执行的代码。


一个defineProps宏的例子


我们来看一个实际的例子,下面这个是我们的源代码:


<template>
<div>content is {{ content }}div>
<div>title is {{ title }}div>
template>

<script setup lang="ts">
import {ref} from "vue"
const props = defineProps({
content: String,
});
const title = ref("title")
script>

在这个例子中我们使用defineProps宏定义了一个类型为String,属性名为contentprops,并且在template中渲染content的内容。


我们接下来再看看编译成js文件后的代码,代码我已经进行过简化:


import { defineComponent as _defineComponent } from "vue";
import { ref } from "vue";

const __sfc__ = _defineComponent({
props: {
content: String,
},
setup(__props) {
const props = __props;
const title = ref("title");
const __returned__ = { props, title };
return __returned__;
},
});

import {
toDisplayString as _toDisplayString,
createElementVNode as _createElementVNode,
Fragment as _Fragment,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
} from "vue";

function render(_ctx, _cache, $props, $setup) {
return (
_openBlock(),
_createElementBlock(
_Fragment,
null,
[
_createElementVNode(
"div",
null,
"content is " + _toDisplayString($props.content),
1 /* TEXT */
),
_createElementVNode(
"div",
null,
"title is " + _toDisplayString($setup.title),
1 /* TEXT */
),
],
64 /* STABLE_FRAGMENT */
)
);
}
__sfc__.render = render;
export default __sfc__;

我们可以看到编译后的js文件主要由两部分组成,第一部分为执行defineComponent函数生成一个 __sfc__ 对象,第二部分为一个render函数。render函数不是我们这篇文章要讲的,我们主要来看看这个__sfc__对象。


看到defineComponent是不是觉得很眼熟,没错这个就是vue提供的API中的 definecomponent函数。这个函数在运行时没有任何操作,仅用于提供类型推导。这个函数接收的第一个参数就是组件选项对象,返回值就是该组件本身。所以这个__sfc__对象就是我们的vue文件中的script代码经过编译后生成的对象,后面再通过__sfc__.render = renderrender函数赋值到组件对象的render方法上面。


我们这里的组件选项对象经过编译后只有两个了,分别是props属性和setup方法。明显可以发现我们原本在setup里面使用的defineProps宏相关的代码不在了,并且多了一个props属性。没错这个props属性就是我们的defineProps宏生成的。


convert.png


我们再来看一个不在setup顶层调用defineProps的例子:


<script setup lang="ts">
import {ref} from "vue"
const title = ref("title")

if (title.value) {
const props = defineProps({
content: String,
});
}
script>

运行这个例子会报错:defineProps is not defined


我们来看看编译后的js代码:


import { defineComponent as _defineComponent } from "vue";
import { ref } from "vue";

const __sfc__ = _defineComponent({
setup(__props) {
const title = ref("title");
if (title.value) {
const props = defineProps({
content: String,
});
}
const __returned__ = { title };
return __returned__;
},
});

明显可以看到由于我们没有在setup的顶层调用defineProps宏,在编译时就不会将defineProps宏替换为定义props相关的代码,而是原封不动的输出回来。在运行时执行到这行代码后,由于我们没有任何地方定义了defineProps函数,所以就会报错defineProps is not defined


总结


现在我们能够回答前面提的三个问题了。



  • vue中的宏到底是什么?


    vue3的宏是一种特殊的代码,在编译时会将这些特殊的代码转换为浏览器能够直接运行的指定代码,根据宏的功能不同,转换后的代码也不同。


  • 为什么这些宏不需要手动从vueimport


    因为在编译时已经将这些宏替换为指定的浏览器能够直接运行的代码,在运行时已经不存在这些宏相关的代码,自然不需要从vueimport


  • 为什么只能在setup顶层中使用这些宏?


    因为在编译时只会去处理setup顶层的宏,其他地方的宏会原封不动的输出回来。在运行时由于我们没有在任何地方定义这些宏,当代码执行到宏的时候当然就会报错。



如果想要在vue中使用更多的宏,可以使用 vue macros。这个库是用于在vue中探索更多的宏和语法糖,作者是vue的团队成员 三咲智子


作者:欧阳码农
来源:juejin.cn/post/7335721246931189795
收起阅读 »

【如诗般写代码】你甚至连注释都没玩明白

web
引言要问我认为最难的事是什么,那只有维护前人的代码。我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行,一头扎进去,闷头写到天昏地暗的什么修复bug,提升性能之类的都不是事,会用调试工具和分时渲染就够了但是看屎山是真的难受注释篇利用注释在编辑器...
继续阅读 »

引言

要问我认为最难的事是什么,那只有维护前人的代码。
我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行,
一头扎进去,闷头写到天昏地暗的

什么修复bug,提升性能之类的都不是事,会用调试工具和分时渲染就够了
但是看屎山是真的难受


注释篇

  1. 利用注释在编辑器开启代码提示

    image.png

    看到区别了吗,左边用的是文档注释,鼠标悬浮能看到变量描述
    右边用的是行内注释,没有任何作用

    初步认识了文档注释的好处后,很多人可能还是不用,因为嫌麻烦。
    所以编辑器也为你着想了,以 VSCode为例,输入 /**,就会自动生成文档注释
    如果在函数上面,再按下回车,还能补齐函数参数文档,如下图所示

    comment.gif


  1. 利用文档注释描述函数功能

    当我鼠标悬浮在函数上时,就能看到他的各种描述,从其他文件导入也有效
    image.png


  1. 智能提示当前参数描述,以及类型等

    这里也能用快捷键呼出,详见我上篇文章:# 【最高效编码指南】也许你不会用VSCode | IDEA

    image.png


  1. 添加 JS 类型,实现类似 TS 的效果

    这里我呼出代码提示,但是他并没有给我补全任何方法,因为他不知道你的类型是什么

    image.png

    如果是强类型语言的话,那就会给你补全代码
    那么动态类型如何实现呢?以 JS 为例,使用文档注释即可,也就是前阵子沸沸扬扬的利用 JSDoc 代替 TS

    image.png

    不仅于此,连枚举都能实现,反正 TS 有的,他应该都有,我没有详细研究

    image.png

  2. 文档注释指令

    如下图所示,我想在文档注释里写上用法,但是他的格式十分丑陋,而且没有语法高亮

    image.png

    于是我使用 example 指令,告诉他这是一个示例,这时就有语法高亮了

    image.png

    指令还有很多,你们输入 @ 就会有提示了,比如 deprecated,标记已弃用
    这时你使用它就会有个提示,并且划上一根线

    image.png

  3. MarkDown 文档注释

    有时候,指令可能不够用,这时就可以使用 MarkDown 语法了

    image.png

  4. 结合 TS

    定义类型时,写上文档注释,当你鼠标悬浮时,就能查看对应注释

    image.png

    函数重载情况下,文档注释要写在类型上才行,下面这种无效

    image.png

    要写在类型定义的地方才行

    image.png

  5. 总结

    如果你用的是变量、函数或是 TS 定义类型,你要写注释,那就一定要写 文档注释,我跪下来求求你了 😭

减少条件分支语句

  1. 策略模式,写个映射表即可。这个有一点开发经验的应该都知道吧

    如果遇到复杂情况,映射表里也可以写函数,执行后返回逻辑

    image.png

  2. 提前返回

    这里第 2 种提前返回就减少了一层嵌套,实际开发中,能减少更多嵌套语句

    image.png

  3. 多个相等判断,使用数组代替

    image.png

代码七宗罪

让我来细数一下这坨代码的罪行,然后引出另一个主题,美化代码
下面这段,这简直是"甲级战犯",

  1. 一堆变量写了或者导入了不用,放那恶心谁呢
  2. 注释了的代码不删 (虽然可能有用,但是真丑)
  3. 都什么年代了,还在用var (坏处下面说)
  4. 用行内注释和没写区别不大,要写就写文档注释 (文档注释的优点上面解释了,不再赘述)
  5. 小学生流水账一般的代码,连个函数入口都没提供,想一句写一句
  6. 连个代码格式化都不会,多按几个回车,你的键盘不会烂掉;每个分段加个注释,你的速度慢不了多少
  7. 硬编码,所有类型用字符串直接区分,你万一要改怎么办?

image.png

语义化

我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行
一头扎进去,闷头写到天昏地暗的,比如下面这种

image.png

这玩意要我一行一行看?我是真的被恶心坏了
写代码要突出一个重点,看个大概,然后才能快速排查,看第三方库源码也是如此

我的习惯是写一个主入口,你叫 main | init | start 什么的都行,我只希望你能写上
然后主入口按照逻辑,给每个函数命名,这样一眼就能看出来你在干什么
如下图所示,这是我的偏好

image.png

我喜欢利用函数全局提升,把初始化函数放在文件顶部。这样每次打开一个文件,就能立刻看到大概逻辑
所以我很少用匿名函数,像上面那种全部写一坨,还都是匿名函数,我真的很难看出来谁是函数,谁是变量

这就引出一个新问题,函数的二义性

函数二义性

众所周知, JS 的类就是函数,里面有自己的 this,可以 new 一个函数

image.png

你要知道他是函数还是类,一般是通过首字母是否大写区分
但是这仅仅是弱规范,人家爱咋写咋写,所以后来出现了匿名函数(主要还是为了解决 this)

匿名函数没有自己的 this 指向,没有 arguments,如下图

image.png

而且用 const 定义,所以也就没了函数提升,严格来说,匿名函数才是真函数

不过我觉得直接写匿名函数有点丑,而且写起来似乎繁琐一点,虽然我都是用代码片段生成的
如果用了匿名函数,那么我就没了函数提升了

所以我仅仅在以下情况使用匿名函数

  1. 作为回调函数
  2. 不需要 this
  3. 函数重载

函数重载我来说说吧,应该挺多人不知道。
比如下图,针对每一种情况,写一遍类型,这样就能更加清楚描述函数的所有参数情况

image.png

不过这样好麻烦,而且好丑啊,于是可以用接口,这时你用 function 就实现不了了

image.png

var 的坏处

  1. var 会变量提升,你可能拿到 undefined

    image.png

  2. var 没有块级作用域,会导致变量共享

    按照常识,下面代码应该输出 0,1,2,3,4

    image.png

    但是你里面是异步打印,于是等你打印时,i 以及加了5次了,又没有块级作用域,所以你拿到的是同一个东西

    在古时候,是用立即执行函数解决的,如下图。因为函数会把变量存起来传给内部

    image.png

    现在用 let 就行了

    image.png

    所以我求求你别用 var 了

格式化

这里可能有争议性,仅仅是我个人喜欢,看着舒服

大多数写前端的,基本人手一个 Prettier 插件自动格式化,再来个 EsLint
然后也懒得看配置,默认就是 2 格缩进,回车多了会被删掉什么的

这样下来,整个文件就相当臃肿,密密麻麻的,我看着很难受

我的风格如下

  • 用 4 格缩进
  • 代码按照语义类型分块,写上块级文档注释
  • import 语句下面空两行,这样更加直观
  • 每一段,用独特醒目的文档注释划分
  • 定义变量优先使用 const,并且只写一个 const
  • 函数参数过长,则一行放一个参数
  • 写行内样式以及较长字符串时( 比如函数作为字符串 ),用特殊的宽松格式书写,保持类似代码的格式化
  • if 分支语句,要多空一行,看着清爽
  • 三目运算,用三行来写
  • 条件判断尽量提前 return,减少分支缩进

下面来用图演示一下,不然看着上面的描述抽象

代码按照语义类型分块,写上块级文档注释

每一段逻辑写完,用个醒目的、大块的文档注释分开。
全部执行的逻辑,放在一个 init 函数中

image.png

定义变量优先使用 const,并且只写一个 const

比如声明变量,我喜欢这么写
按照分类,类型不同则换行,并且写上注释,仅用一个 const

image.png

来看看大众写法,可以说 99.9878987%的人都这么写,这种我一看就难受

image.png

如果你用 let,并且用 4 格缩进,那么你就刚好对齐了,能少写一个回车
不过尽量使用 const

image.png

函数参数过长,则一行放一个参数

如果你这么写,那我看完会头晕眼花,属实是又臭又长的参数列表

image.png

如果你这么写,我会夸你代码和人一样好看

image.png

三目运算格式化

这俩,你说谁的可读性高,肯定是分三行写的好看啊

image.png

字符串以及对象格式化

这俩,你说谁看得舒服,那肯定是 2 啊
我看了身边的人和网上的很多代码,大多数都是 1 这种

image.png

不管你是用字符串,还是对象等方式表达,你都应该写 2 这种样式

image.png

分支语句

这俩哪种好看还用说吗,肯定是左边的好啊。但是你用 Prettier 格式化的话,应该就变成右边的了
同理 try catch 之类的也是一样

image.png

最后,多用换行,我跪下来求求你了 😭


作者:寅时码
来源:juejin.cn/post/7335277377621639219

收起阅读 »

前端项目如何准确预估个人工时

补充 看来很多小伙伴对这个问题感兴趣,大家不要忽视了压工时这个事。 领导为什么会压工时? 使他的KPI更好看 不清楚做这个东西实际要多长时间 因为第2点的原因,他也无法去争取合理时间 部分人看着下属加班,有种大权在握,言出法随的畅快感 码农为什么不要轻易答...
继续阅读 »

补充


看来很多小伙伴对这个问题感兴趣,大家不要忽视了压工时这个事。


领导为什么会压工时?



  1. 使他的KPI更好看

  2. 不清楚做这个东西实际要多长时间

  3. 因为第2点的原因,他也无法去争取合理时间

  4. 部分人看着下属加班,有种大权在握,言出法随的畅快感


码农为什么不要轻易答应压工时?



  • 无形中会打击你的自信心,当自信心全无的时候,要么是职业生涯结束,要么是变成人人都跑来拿捏一手的角色

  • 轻易妥协,会让你的说的话,可信度降低。毕竟,别人随便说一下,激一下,你就妥协了,那很容易就让人觉得,你就是随意乱说一个时间

  • 这会妨碍你对自己真实能力的认知和评估


被压工时了怎么办?



  • 偶尔有少量任务,被压了少量工时,个人认为是可以接受的,毕竟不可能一切都能按规划走

  • 大量工作被压工时,那就告知延期风险,你的工作速度应该保持不变,完不成,就让项目延期。如何解决延期问题?那是领导的事情,不是一个小码农应该操心的。


没怎么压工时,但把工作时间延长了?



  • 首先,工作该是什么速度,就是什么速度,不要替领导着急着赶进度

  • 其次,反馈这有延期风险,建议领导增派人手。(记得先和其他成员通个气)

  • 该提加班就提加班,调休或加班工资是你应得的,累了就调休,你是人,不是机器


为什么要给自己留缓冲时间?加缓冲时间是工作不饱和?



  • 加缓冲时间不是工作不饱和

  • 8小时工作日,你不可能每分每秒都在写代码,谁也做不到。

  • 你不可能熟悉每个API,总有要你查资料的时候,而查资料,可能你查了4-5个地方,才总结出正确用法,这需要额外的时间

  • 你的工作随时可能被人打断,比如:开会,喝水,同事问你问题等等,这都会耗费你的时间

  • 你拉取代码,提交代码,思考实现方式,和业务进一步确认需求细节,和UI沟通交互细节,自测,造mock数据,这都需要时间

  • 如果没有缓冲时间,一个任务延期,可能会导致,后续N个任务都延期。

  • 即使从项目角度分析,足够的缓冲时间,有利于降低项目延期风险


工作总是被人打断怎么办?



  • 比如:开会,比如插入了一个紧急工作任务,这种较长时间的打断,那就将这些被占用的时间,写进工作日志,即时向项目组反馈,要求原本的工作任务加工时或延迟开始

  • 被同事问问题。几句话能说清楚的,那不妨和他直说。几句话说不清楚的,那只能等你有空的时候,再给他解答。要优先考虑完成自己的工作任务。


大方的承认自己的不足,能力多大,就做多少事,明确自己的定位


可能有的小伙伴,可能被别人激一下,被人以质疑的语句问一下,后续就被人牵着鼻子走了。有很大因素是因为不敢承认某方面的工作能力暂有欠缺。其实大方的承认即可,有问题,那就暴露问题,如果项目组其他成员会,那就让他来教你,这也属于沟通协作。如果没人会,那说明这是一个需要集思广益的公共问题。


可能有同学觉得自己就是个小码农甚至因为自己是外包,不敢发表自己的想法和见解,其实大可不必,只要你就事论事,有理有据,完全可以大方说出来,你不说出来,你永远只能从自己的角度看这个问题,你无法确认自己是对的还是错的。错了咱改,对了继续保持。既不贬低别人,也不看轻自己,以平常心讨论即可。


明确自己的定位,就是个普通码农,普通干活的,项目延期了,天塌了也是领导想办法解决。自己不会的就反馈,别人不会自己会的,那就友好分享。不会的,不要羞于请教。干不过来了,及时告知领导,让其协调解决。坦坦荡荡,不卑不亢。


前提



  1. 此方法是在没有技术阻碍的前提条件下预估,如果有技术障碍,请先解决技术阻碍

  2. 此方法需要根据个人实际情况调整

  3. 这里以普通的以vue,element-plus,axios为基础技术的管理系统为例

  4. 这些都是个人见解,欢迎在评论区提出不同观点

  5. 请先以一个普通的CRUD界面,测算自己的基本编码速度


为啥评估会不准确


自我评估时





领导给你评估时

功能领导认为的领导忘记的领导认为的时间实际时间
加个字段加个显示字段而已,实际只要3分钟吧码农要找到对应代码,查看代码上下文,或许还涉及样式的修改,后端接口可能还没有这个字段, 还要自测20分钟2小时
做个纯列表页面前端只要把后端的字段显示出来就好了吧,肯定会很快可能没有直接可用的组件,即使有,前端可能需要查组件文档,看具体用法, 还得处理loading状态,空状态,然后还得查看后端接口文档,看哪些字段需要额外处理,最后还得自测,甚至可能在真正对接前,需要自己造mock接口2小时8小时
编辑/新增界面就写个表单,前端把数据提交给后端就完事了前端需要理解业务逻辑,需要做数据校验,对于类似下拉数据,图片上传,可能还要和后端沟通,数据从哪里取,分别表示什么意思,怎么上传图片,提交数据后,成功后要怎么做,以及失败的异常处理,用户填了一半数据之后,刷新了界面,应该如何处理,后端接口没出来前,需要自己mock接口,用来自测4小时3天
一个响应式界面就一个普通界面应该不至于有什么难度吧忽略了这是一个响应式界面,前端需要与UI设计师沟通,确认在不同情况,界面如何响应,以及思考如何实现,如果业务数据还会对界面的响应式产生影响,那还得进一步深入分析8小时3天
实现多语言功能多语言,不就是用编码代替原本的文字嘛,根本不需要额外的时间处理吧前端需要考虑多语言数据从哪里来,多语言切换之后对原本布局的影响,多语言切换之后,表单错误提示的处理方式不给额外时间3-4天
做个3/4环直接使用图表插件,调下API就出来了前端可能需要进行数据转换,需要查看图表插件的文档,图表插件可能没有现成的,需要通过搜索引擎找类似的示例,然后模仿实现,甚至图表插件根本无法实现这种效果,需要用其他技术实现3小时4天
前期一个连续的类似界面上一个界面和这个类似,把上个界面的代码复制过来,改改字段和接口,应该能很快完成很多界面看着一样,但实际业务逻辑差别很大,只是界面表现形式类似,有些字段是动态显示/隐藏的,有些可以固定写,表单字段的验证逻辑,可能也不一样。并且上一个界面的代码都还没写,还没测试,这里还有很多不确定因素,直接复制还可能导致,同一个错误,在多个界面同时出现2-3小时前一个界面花了多久,这个界面可能还是花了差不多的时间
仿照xx官网的效果,做个静态界面好多网站都是这个效果,这应该是烂大街的效果吧某个效果可能是知识盲区,需要查资料2天1周,甚至可能做不了
参考公司内部其他系统界面,实现类似界面现成的东西,这系统也上线好久了,应该把代码复制过来,稍微改改就OK了吧当前这个人从未接触过这个系统,对这个系统一点都不了解,了解需要时间,可能另外的项目有自己的框架,和当前系统的框架不同,无法直接使用, 另外一个项目无法直接给项目代码给你,只能让人给你讲解,但讲解人没时间或不是随时都有时间,或就是随意讲讲,另一个项目的这个界面,可能是经过多人集思广益,多轮讨论与重构才最终得到了这个效果5小时3-5天
用低代码平台实现个界面就是拖拖组件的事情,代码都不用写,应该很快组件可能没有,有组件可能某些业务逻辑在这个低代码平台难以实现,需要咨询低代码平台的提供方,但低代码提供方,几个人需要服务几十个人,无法优先给你解答,即使解答了,可能给出的方案不通用(或者他们还需要继续问他们内部团队的人),遇到下个类似的情况,原来的解决方案又无效了。难以调试或无法调试,前端原本的知识储备,在低代码平台,仅剩原始的js语法有效2天3周

总原则



  • 不要duang的一下,对整个界面/模块进行评估,应该对行列,表单项,逻辑点,进行评估,然后将总的时间加起来,就是这个界面的预估工时

  • 要至少多估20%的时间,一个是因为你很难持续性的投入(比如:有人突然问你问题,上厕所,喝水,或有事请假)

  • 请将一天的工作时间最多算6.5小时(因为你可能需要开会,可能被其他事情打断,可能有时不在状态,同时也算是给自己留点思考时间)

  • 尽量不要在过了一遍需求之后,立马评估工时(不要被项目经理或业务的节奏带偏),而是要自己再思考一遍需求,想想大概的实现逻辑,重难点等等,尽量不要当天给出工时评估

  • 如果是给别人评估工时,那尽可能给别人多评点工时

  • 工期紧的时候,加人有必要,但1个人干7天的活,7个人未必能1天干完

  • 有公共组件和没有公共组件完成同样的功能,所需要的时间可能天差地别, 因此,请确保先完成公共组件的开发

  • 请先将业务逻辑理顺,把工作进行拆分,直至自己能正确预估的范围内


前端有哪些地方需要耗费工时



  • 思考实现方式

  • 静态UI界面还原与响应式

  • 业务逻辑实现

  • 动态UI交互

  • 后端接口mock

  • 后端接口对接

  • 自测


前端项目应该分成几步实现



  1. 整体项目搭建以及规范与约束确认

  2. 整体页面结构,路由结构搭建

  3. 统一UI风格,以及公共组件实现

  4. 具体的界面实现


1,2点应该由项目组长完成
3点应该由项目组长以及技术较强的组员共同完成


常见的公共组件工时

组件工时
查询按钮60 分钟
提交按钮60 分钟
confirm按钮60 分钟
下拉按钮60 分钟
分页表格360 分钟
JSON配置分页表格240 分钟
动态表单360 分钟
JSON动态表单360 分钟
模态框90 分钟
抽屉组件90 分钟
select组件90 分钟
tree组件120 分钟
cascade组件90 分钟
日期选择组件60 分钟
日期范围选择组件120 分钟
axios封装360 分钟
卡片组件60 分钟
面包屑组件60 分钟

列表页拆分与编码工时预估




首先做总体拆分,分成3大部分



  1. 头部的搜索表单

每个表单项30分钟左右,每个功能按钮40分钟左右


因此这里是1个表单项(30分钟),2个功能按钮(80分钟),总计110分钟



  1. 中间的工具栏




P.S. 这里没算右侧工具条,只算了左侧功能按钮


因为是列表页,添加角色这个按钮,只考虑是个简单按钮加个点击事件,至于点击按钮之后的角色添加界面的工时不放在列表页评估,而是在添加角色界面单独评估,因此添加角色按钮算30分钟


批量操作按钮,应该使用公共组件的下拉按钮组件,以及与分页表格组件配合实现,因此算40-60分钟


因此这里整体应该总计在70分钟内



  1. 主体的分页表格




应该使用公共组件的分页表格组件实现



  • 普通列(直接显示字段值的列,和简单转换的列)每列算20分钟

  • 操作列按每个操作按钮另算

  • 复杂转换列按40-60分钟算

  • 排序列按40-60分钟算

  • 分页表格组件调用30分钟


从界面看,这里有6列,checkbox列和序号列,是分页表格组件实现的,无需再算工时,除操作列和创建时间外,其他都属于普通列算20分钟每列,创建时间列算40分钟,因此总共100分钟


操作列角色成员,角色权限和修改,都需要打开一个抽屉界面(抽屉界面里的东西另算,不算在列表页中),删除需要调后端接口以及确认,因此



  • 角色成员按钮: 20分钟

  • 角色权限按钮: 20分钟

  • 修改按钮: 20分钟

  • 删除按钮: 30分钟


总计: 100 + 20*3 + 30 = 190分钟


因此整个列表页工时


列表页需要mock 1个接口,列表接口,算20分钟


110 + 70 + 190 + 20 = 390 分钟 = 6.5小时


再在390分钟的基础上再多加20% = 390*1.2 = 468 分钟 = 7.8 小时


P.S.



  1. 添加角色/角色成员/角色权限这是独立界面,需要单独计算时间。计算方式也与上面的类似

  2. 没有单独计算自测时间,个人认为理想情况应该对1个界面,加2-3小时自测时间

  3. 没有计算联调时间,联调时间应该另算

  4. 没有计算UI还原时间,对于复杂UI界面或UI还原度要求高的界面,应该单独计算UI还原时间

  5. 对于复杂的业务逻辑,可以将业务逻辑拆解为一条条的业务逻辑项,每个业务逻辑项以40分钟左右每条作为参考实现时间

  6. 没有考虑思考时间,对于复杂的业务逻辑,或者没做过的界面形态,或者复杂的界面形态等,必须将思考时间计算进来,或者说,在已经基本想明白怎么去实现的基础上,再去评估工时


被误解的敏捷开发模式

错误的敏捷开发




  • 敏捷开发就是强调一个快字

  • 敏捷开发就是不断的压榨工时

  • 敏捷开发就是不停的加班


正确的敏捷开发



  • 测试在项目之初就介入,编写完测试用例之后,共享给开发,方便开发自测

  • 将一个完整的项目进行合理拆分,拆分为若干独立小迭代

  • 每个小迭代完成之后,进行提测以及收集用户试用反馈,尽早反馈,以及尽早发现问题

  • 在小迭代提测期间,应该让开发略作修整(改bug或修整)和总结(总结共性问题,避免下阶段,再重复出现这些共性问题),而非让开发立马进入下阶段开发,否则容易造成,开发一边赶下阶段需求,一边赶上阶段bug

  • 个人认为敏捷开发,重点在于敏捷,灵巧好掉头,分阶段交付,及早发现问题,拥抱需求变化。而非简单的抽着鞭子让程序员加班赶工996或007


相关文章


作者:悟空和大王
链接:https://juejin.cn/post/7330071686489636904
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

情人节即分手,FreeNginx 来了

时间线 2019 年 3 月 11 日,F5 Networks 宣布以 6.7 亿美元收购 Nginx。 2022.01.18, NGINX 创始人 Igor Sysoev 从 NGINX 和 F5 离职:“Igor Sysoev 选择离开 F5,以便将...
继续阅读 »

38d432781be446559f99d3d139ef6be7.png


时间线



2019 年 3 月 11 日,F5 Networks 宣布以 6.7 亿美元收购 Nginx。




2022.01.18, NGINX 创始人 Igor Sysoev 从 NGINX 和 F5 离职:“Igor Sysoev 选择离开 F5,以便将更多的时间留给陪伴家人和朋友,以及个人项目。感谢他所做的一切让全世界的网站变得更好。”




在 2024.2.14 情人节这天,作为 nginx 的长期核心开发者之一,马克西姆-杜宁(Maxim Dounin)宣布创建一个名为 Freenginx 的新分叉项目。



他在宣布 Freenginx 时说道


"你们可能知道,F5 于 2022 年关闭了莫斯科办事处,从那时起我就不再为 F5 工作了。 不过,我们已经达成协议,我将继续作为志愿者参与nginx开发。在近两年的时间里,我一直致力于改进nginx,免费为大家提供更好的nginx。
不幸的是,F5一些新的非技术管理人员最近决定干涉nginx多年来使用的安全策略,无视策略和开发者的立场。


这很好理解:他们拥有这个项目,可以对其做任何事情,包括以市场为导向的行为,无视开发者和社区的立场。 不过,这还是与我们的协议相矛盾。 更重要的是,我无法再控制F5内部对nginx的修改,也不再将nginx视为一个为公众利益开发和维护的自由开源项目。


因此,从今天起,我将不再参与F5运营的nginx开发。 取而代之的是,我将启动另一个项目,由开发者而非公司实体运行。


目标是使nginx开发不受公司任意行为的影响。 欢迎提供帮助和贡献。 希望大家都能从中受益。


freenginx.org上的简短声明



freenginx.org的目标是使nginx的开发不受任意公司行为的影响。



image.png


开源和商业


利益与目标不同决定了开源项目的不同发展方向,这不好评说好坏对错。



作为商业公司,F5毕竟真金白银花了那么多钱拥有了nginx,全职人员的成本付出,这肯定需要往商业化方向考量,希望能找到商业与开源的平衡。




Maxim Dounin 有着开发者的自由理想园,站在开发者和开源使用者的角度看开源项目的发展,nginx 能更开放更自由,方向由社区掌控。也希望 freenginx 能发展顺利。



oracle-jdk vs openjdk, mysql vs mariadb, 现在有了 nginx vs freenginx, 我们现在可以开始关注 Freenginx 的未来发展,看未来有多少其他开发者会专注于这个新的分叉。


Nginx 擦边广告,使用 HertzBeat 快速监控 Nginx



HertzBeat 是一款我们开源的实时监控系统,无需Agent,性能集群,兼容Prometheus,自定义监控和状态页构建能力。github.com/dromara/her…




它支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web服务器,云原生,网络,自定义等监控。下面广告演示下如果使用 HertzBeat 快速监控 Nginx 服务状态。



1. 部署 HertzBeat


docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat tancloud/hertzbeat


2. 部署 Nginx


本地部署启动 Nginx, 默认监控 Nginx 可用性,若监控更多指标,则需启用 Nginx 的 ngx_http_stub_status_modulengx_http_reqstat_module 监控模块


参考文档:hertzbeat.com/zh-cn/docs/…


3. 在 HertzBeat 添加 Nginx 监控


访问 HertzBeat 控制页面,在 应用服务监控 -> Nginx服务器 添加对端 Nginx 监控,配置对端IP端口等参数。



确认添加后就OK啦,后续我们就可以看到 Nginx 的相关指标数据状态,还可以设置告警阈值通知等,当 Nginx 挂了或者某个指标异常过高时,通过邮件钉钉微信等通知我们。





10分钟搞定,快来使用 HertzBeat 24小时自动观测你的 Nginx 状态


在 Github Star 我们!


github.com/dromara/her…


gitee.com/dromara/her…



部分内容来源于 http://www.msn.com/zh-cn/chann…



作者:Dromara开源社区
来源:juejin.cn/post/7335089578321854498
收起阅读 »

localhost和127.0.0.1的区别是什么?

今天在网上逛的时候看到一个问题,没想到大家讨论的很热烈,就是标题中这个: localhost和127.0.0.1的区别是什么? 前端同学本地调试的时候,应该没少和localhost打交道吧,只需要执行 npm run 就能在浏览器中打开你的页面窗口,地址栏显...
继续阅读 »

今天在网上逛的时候看到一个问题,没想到大家讨论的很热烈,就是标题中这个:


localhost和127.0.0.1的区别是什么?



前端同学本地调试的时候,应该没少和localhost打交道吧,只需要执行 npm run 就能在浏览器中打开你的页面窗口,地址栏显示的就是这个 http://localhost:xxx/index.html


可能大家只是用,也没有去想过这个问题。


联想到我之前合作过的一些开发同学对它们俩的区别也没什么概念,所以我觉得有必要普及下。


localhost是什么呢?


localhost是一个域名,和大家上网使用的域名没有什么本质区别,就是方便记忆。


只是这个localhost的有效范围只有本机,看名字也能知道:local就是本地的意思。


张三和李四都可以在各自的机器上使用localhost,但获取到的也是各自的页面内容,不会相互打架。


从域名到程序


要想真正的认清楚localhost,我们还得从用户是如何通过域名访问到程序说起。


以访问百度为例。


1、当我们在浏览器输入 baidu.com 之后,浏览器首先去DNS中查询 baidu.com 的IP地址。


为什么需要IP地址呢?打个比方,有个人要寄快递到你的公司,快递单上会填写:公司的通讯地址、公司名称、收件人等信息,实际运输时快递会根据通信地址进行层层转发,最终送到收件人的手中。网络通讯也是类似的,其中域名就像公司名称,IP地址就像通信地址,在网络的世界中只有通过IP地址才能找到对应的程序。


DNS就像一个公司黄页,其中记录着每个域名对应的IP地址,当然也有一些域名可能没做登记,就找不到对应的IP地址,还有一些域名可能会对应多个IP地址,DNS会按照规则自动返回一个。我们购买了域名之后,一般域名服务商会提供一个域名解析的功能,就是把域名和对应的IP地址登记到DNS中。


这里的IP地址从哪里获取呢?每台上网的电脑都会有1个IP地址,但是个人电脑的IP地址一般是不行的,个人电脑的IP地址只适合内网定位,就像你公司内部的第几栋第几层,公司内部人明白,但是直接发给别人,别人是找不到你的。如果你要对外部提供服务,比如百度这种,你就得有公网的IP地址,这个IP地址一般由网络服务运营商提供,比如你们公司使用联通上网,那就可以让联通给你分配一个公网IP地址,绑定到你们公司的网关服务器上,网关服务器就像电话总机,公司内部的所有网络通信都要通过它,然后再在网关上设置转发规则,将网络请求转发到提供网络服务的机器上。


2、有了IP地址之后,浏览器就会向这个IP地址发起请求,通过操作系统打包成IP请求包,然后发送到网络上。网络传输有一套完整的路由协议,它会根据你提供的IP地址,经过路由器的层层转发,最终抵达绑定该IP的计算机。


3、计算机上可能部署了多个网络应用程序,这个请求应该发给哪个程序呢?这里有一个端口的概念,每个网络应用程序启动的时候可以绑定一个或多个端口,不同的网络应用程序绑定的端口不能重复,再次绑定时会提示端口被占用。通过在请求中指定端口,就可以将消息发送到正确的网络处理程序。


但是我们访问百度的时候没有输入端口啊?这是因为默认不输入就使用80和443端口,http使用80,https使用443。我们在启动网络程序的时候一定要绑定一个端口的,当然有些框架会自动选择一个计算机上未使用的端口。



localhost和127.0.0.1的区别是什么?


有了上边的知识储备,我们就可以很轻松的搞懂这个问题了。


localhost是域名,上文已经说过了。


127.0.0.1 呢?是IP地址,当前机器的本地IP地址,且只能在本机使用,你的计算机不联网也可以用这个IP地址,就是为了方便开发测试网络程序的。我们调试时启动的程序就是绑定到这个IP地址的。


这里简单说下,我们经常看到的IP地址一般都是类似 X.X.X.X 的格式,用"."分成四段。其实它是一个32位的二进制数,分成四段后,每一段是8位,然后每一段再转换为10进制的数进行显示。


那localhost是怎么解析到127.0.0.1的呢?经过DNS了吗?没有。每台计算机都可以使用localhost和127.0.0.1,这没办法让DNS来做解析。


那就让每台计算机自己解决了。每台计算机上都有一个host文件,其中写死了一些DNS解析规则,就包括 localhost 到 127.0.0.1 的解析规则,这是一个约定俗成的规则。


如果你不想用localhost,那也可以,随便起个名字,比如 wodehost,也解析到 127.0.0.1 就行了。


甚至你想使用 baidu.com 也完全可以,只是只能自己自嗨,对别人完全没有影响。


域名的等级划分


localhost不太像我们平常使用的域名,比如 http://www.juejin.cn 、baidu.com、csdn.net, 这里边的 www、cn、com、net都是什么意思?localhost为什么不需要?


域名其实是分等级的,按照等级可以划分为顶级域名、二级域名和三级域名...


顶级域名(TLD):顶级域名是域名系统中最高级别的域名。它位于域名的最右边,通常由几个字母组成。顶级域名分为两种类型:通用顶级域名和国家顶级域名。常见的通用顶级域名包括表示工商企业的.com、表示网络提供商的.net、表示非盈利组织的.org等,而国家顶级域名则代表特定的国家或地区,如.cn代表中国、.uk代表英国等。


二级域名(SLD):二级域名是在顶级域名之下的一级域名。它是由注册人自行选择和注册的,可以是个性化的、易于记忆的名称。例如,juejin.cn 就是二级域名。我们平常能够申请到的也是这种。目前来说申请 xxx.com、xxx.net、xxx.cn等等域名,其实大家不太关心其顶级域名com\net\cn代表的含义,看着简短好记是主要诉求。


三级域名(3LD):三级域名是在二级域名之下的一级域名。它通常用于指向特定的服务器或子网。例如,在blog.example.com中,blog就是三级域名。www是最常见的三级域名,用于代表网站的主页或主站点,不过这只是某种流行习惯,目前很多网站都推荐直接使用二级域名访问了。


域名级别还可以进一步细分,大家可以看看企业微信开放平台这个域名:developer.work.weixin.qq.com,com代表商业,qq代表腾讯,weixin代表微信,work代表企业微信,developer代表开发者。这种逐层递进的方式有利于域名的分配管理。


按照上边的等级定义,我们可以说localhost是一个顶级域名,只不过它是保留的顶级域,其唯一目的是用于访问当前计算机。


多网站共用一个IP和端口


上边我们说不同的网络程序不能使用相同的端口,其实是有办法突破的。


以前个人博客比较火的时候,大家都喜欢买个虚拟主机,然后部署个开源的博客程序,抒发一下自己的感情。为了挣钱,虚拟主机的服务商会在一台计算机上分配N多个虚拟主机,大家使用各自的域名和默认的80端口进行访问,也都相安无事。这是怎么做到的呢?


如果你有使用Nginx、Apache或者IIS等Web服务器的相关经验,你可能会接触到主机头这个概念。主机头其实就是一个域名,通过设置主机头,我们的程序就可以共用1个网络端口。


首先在Nginx等Web程序中部署网站时,我们会进行一些配置,此时在主机头中写入网站要使用的域名。


然后Nginx等Web服务器启动的时候,会把80端口占为己有。


然后当某个网站的请求到达Nginx的80端口时,它会根据请求中携带的域名找到配置了对应主机头的网络程序。


然后再转发到这个网络程序,如果网络程序还没有启动,Nginx会把它拉起来。


私有IP地址


除了127.0.0.1,其实还有很多私有IP地址,比如常见的 192.168.x.x。这些私有IP地址大部分都是为了在局域网内使用而预留的,因为给每台计算机都分配一个独立的IP不太够用,所以只要局域网内不冲突,大家就可劲的用吧。你公司可以用 192.168.1.1,我公司也可以用192.168.1.1,但是如果你要访问我,就得通过公网IP进行转发。


大家常用的IPv4私有IP地址段分为三类:


A类:从10.0.0.0至10.255.255.255


B类:从172.16.0.0至172.31.255.255


C类:从192.168.0.0至192.168.255.255。


这些私有IP地址仅供局域网内部使用,不能在公网上使用。


--


除了上述三个私有的IPv4地址段外,还有一些保留的IPv4地址段:


用于本地回环测试的127.0.0.0至127.255.255.255地址段,其中就包括题目中的127.0.0.1,如果你喜欢也可以给自己分配一个127.0.0.2的IP地址,效果和127.0.0.1一样。


用于局域网内部的169.254.0.0至169.254.255.255地址段,这个很少接触到,如果你的电脑连局域网都上不去,可能会看到这个IP地址,它是临时分配的一个局域网地址。


这些地址段也都不能在公网上使用。


--


近年来,还有一个现象,就是你家里或者公司里上网时,光猫或者路由器对外的IPv4地址也不是公网IP了,这时候获得的可能是一个类似 100.64.x.x 的地址,这是因为随着宽带的普及,运营商手里的公网IP也不够了,所以运营商又加了一层局域网,而100.64.0.0 这个网段是专门分给运营商做局域网用的。如果你使用阿里云等公有云,一些云产品的IP地址也可能是这个,这是为了将客户的私有网段和公有云厂商的私有网段进行有效的区分。


--


其实还有一些不常见的专用IPv4地址段,完整的IP地址段定义可以看这里:http://www.iana.org/assignments…



IPv6


你可能也听说过IPv6,因为IPv4可分配的地址太少了,不够用,使用IPv6甚至可以为地球上的每一粒沙子分配一个IP。只是喊了很多年,大家还是喜欢用IPv4,这里边原因很多,这里就不多谈了。


IPv6地址类似:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX

它是128位的,用":"分成8段,每个X是一个16进制数(取值范围:0-F),IPv6地址空间相对于IPv4地址有了极大的扩充。比如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b 就是一个有效的IPv6地址。


关于IPv6这里就不多说了,有兴趣的可以再去研究下。


关注萤火架构,加速技术提升!


作者:萤火架构
来源:juejin.cn/post/7321049446443417638
收起阅读 »

前端实现excel_xlsx文件预览

web
使用的框架: React 要使用的库: exceljs、handsontable 1. 概述 接到一个任务,是要前端实现文件预览效果,百度了一圈,发现也没有什么好的方法可以完美的将表格渲染出来。在前端中有sheetjs和exceljs可以对xlsx文件进行解...
继续阅读 »

使用的框架: React


要使用的库: exceljs、handsontable



1. 概述


接到一个任务,是要前端实现文件预览效果,百度了一圈,发现也没有什么好的方法可以完美的将表格渲染出来。在前端中有sheetjsexceljs可以对xlsx文件进行解析,本来一开始我用的是sheetjs,但是在样式获取上遇到了麻烦,所以我改用了exceljs,不过很难受,在样式获取时同样遇到了不小的麻烦,但是我懒得换回sheetjs了,那就直接使用exceljs吧。


要实现xlsx文件预览效果,我的想法是使用一个库对xlsx文件进行解析,然后使用另一个库对解析出来的数据在页面上进行绘制,综上,我采用的方案是:exceljs+handsontable


2. 实现步骤


2.1 安装库


使用命令: npm i exceljs handsontable @handsontable/react


2.2 使用exceljs解析数据并使用handsontable进行渲染


直接贴代码了:


import Excel from 'exceljs'
import { useState } from 'react';

import { HotTable } from '@handsontable/react';
import { registerAllModules } from 'handsontable/registry';
import 'handsontable/dist/handsontable.full.min.css';
import { textRenderer, registerRenderer } from 'handsontable/renderers';

// 注册模块
registerAllModules();

export default function XLSXPreView() {
const [data, setData] = useState([]);

const handleFile = async (e) => {
const file = e.target.files[0];

const workbook = new Excel.Workbook();
await workbook.xlsx.load(file)

// 第一个工作表
const worksheet = workbook.getWorksheet(1);

// 遍历工作表中的所有行(包括空行)
const sheetData = [];
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
// console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
// 使用row.values获取每一行的值时总会多出一条空数据(第一条),这里我把它删除
const row_values = row.values.slice(1);
sheetData.push(row_values)
});
setData(sheetData);
}

return (
<>
<input type="file" onChange={handleFile}/>
<div id='table_view'>
<HotTable
data={data}
readOnly={true}
rowHeaders={true}
colHeaders={true}
width="100vw"
height="auto"
licenseKey='non-commercial-and-evaluation'// 一定得加这个handsontable是收费的加了这个才能免费用
/>


</div>
</>

)
}

到这里,已经实现了从xlsx文件中获取数据,并使用handsontable将表格中的数据渲染出来,示例结果如下,如果只需要将数据显示出来,并不需要将样式什么的一起复现了,那到这里就已经结束了!


image.png


但事实上,这并不是我要做到效果,我的xlsx里面还有样式什么的,也需要复现,头疼😔


3. 其它的杂七杂八


3.1 单元格样式


事实上,在exceljs解析xlsx文件时,它顺带一起把样式获取到了,通过worksheet.getCell(1, 1).style可以获取对应单元格的样式,如下,背景色存放在fill.fgColor中,字体颜色存放在font.color中,这样的话只需要将这些样式一一赋值给handsontable组件再添加样式就好了。


image.png


但是实际操作的时候却遇到了问题,先说excel中的颜色,在选择颜色时,应该都会打开下面这个选项框吧,如果你选择的是标准色,它获取到的颜色就是十六进制,但是如果你选择主题中的颜色,那就是另一种结果了,并且还会有不同的深暗程度tint,这就很难受了!


image.png


随后在控制台中打印了workbook,发现它把主题返回了,可以通过work._themes.theme1获取,不过获取到的是xml格式的字符串,由于xml我没学,我不会,所以我就把它转换成json来进行处理了。


第一步


安装xml转json的库: npm i fast-xml-parser


import {XMLParser} from 'fast-xml-parser'

// 将主题xml转换成json
const themeXml = workbook._themes.theme1;
const options = {
ignoreAttributes: false,
attributeNamePrefix: '_'
}
const parser = new XMLParser(options);
const json = parser.parse(themeXml)
setThemeJson(json);


其实它的theme好像是固定的,也可以在一些格式转换的网站中直接转换成json然后放到一个json文件中,读取就行,我这里就直接放到一个state中了!



第二步


接下来就是重头戏了!设置单元格样式...


首先安装一个处理颜色的库color,用来根据tint获得不同明暗程度的颜色: npm i color


下面是获取颜色的函数:


// 根据主题和明暗度获取颜色
const getThemeColor = (themeJson, themeId, tint) => {
let color = '';
const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
switch (themeId) {
case 0:
color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
break;
case 1:
color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
break;
case 2:
color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
break;
case 3:
color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
break;
default:
color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
break;
}
// 根据tint修改颜色深浅
color = '#' + color;
const colorObj = Color(color);
if(tint){
if(tint>0){// 淡色
color = colorObj.lighten(tint).hex();
}else{ // 深色
color = colorObj.darken(Math.abs(tint)).hex();
}
}
return color;
}
// 获取颜色
const getColor = (obj, themeJson) => {
if('argb' in obj){ // 标准色
// rgba格式去掉前两位: FFFF0000 -> FF0000
return '#' + obj.argb.substring(2);
}else if('theme' in obj){ // 主题颜色
if('tint' in obj){
return getThemeColor(themeJson, obj.theme, obj.tint);
}else{
return getThemeColor(themeJson, obj.theme, null);
}
}
}

然后设置handonsontable的单元格的一些样式:颜色、加粗、下划线、边框balabala...的


顺带把行高和列宽一起设置了,这个还比较简单,就一笔带过了...


3.2 合并单元格


从获取到的sheet中有一个_meages属性,该属性中存放了表格中所有的合并单元格区域,所以只需要将它们重新渲染在handsontable中就好。


image.png


然后就实现了表格的一些基本功能的预览,结果如下图:


image.png


3. 总结(附全代码)


其实这个的本质主要就是通过ecxeljs解析表格文件的数据,然后通过handsontable将它们重新绘制在页面上,个人觉得这种方法并不好,因为表格里的操作太多了要把它们一一绘制工作量实在是太大了,而且很麻烦,如有有更好的方案希望大佬们告诉我一下,我这里把表格的一些常用到的功能实现了预览,还有想表格里放图片什么的都没有实现,如果有需要,可以根据需求再进行进行写。



我写的其实还有一点bug,单元格的边框样式我只设置了solid和dashed,但事实上excel中单元格的边框有12种样式,而且还有对角线边框,设置起来好麻烦,我就不弄了,大家用的时候注意一下哈,有需要的话可以自己修改一下!



附上全部代码:


/**
* exceljs + handsontable
*/

import Excel from 'exceljs'
import { useState } from 'react';

import { HotTable } from '@handsontable/react';
import { registerAllModules } from 'handsontable/registry';
import 'handsontable/dist/handsontable.full.min.css';
import { textRenderer, registerRenderer } from 'handsontable/renderers';

import {XMLParser} from 'fast-xml-parser'
import Color from 'color';

// 注册模块
registerAllModules();

// 根据主题和明暗度获取颜色
const getThemeColor = (themeJson, themeId, tint) => {
let color = '';
const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
switch (themeId) {
case 0:
color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
break;
case 1:
color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
break;
case 2:
color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
break;
case 3:
color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
break;
default:
color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
break;
}
// 根据tint修改颜色深浅
color = '#' + color;
const colorObj = Color(color);
if(tint){
if(tint>0){// 淡色
color = colorObj.lighten(tint).hex();
}else{ // 深色
color = colorObj.darken(Math.abs(tint)).hex();
}
}
return color;
}
// 获取颜色
const getColor = (obj, themeJson) => {
if('argb' in obj){ // 标准色
// rgba格式去掉前两位: FFFF0000 -> FF0000
return '#' + obj.argb.substring(2);
}else if('theme' in obj){ // 主题颜色
if('tint' in obj){
return getThemeColor(themeJson, obj.theme, obj.tint);
}else{
return getThemeColor(themeJson, obj.theme, null);
}
}
}
// 设置边框
const setBorder = (style) =>{
let borderStyle = 'solid';
let borderWidth = '1px';
switch (style) {
case 'thin':
borderWidth = 'thin';
break;
case 'dotted':
borderStyle = 'dotted';
break;
case 'dashDot':
borderStyle = 'dashed';
break;
case 'hair':
borderStyle = 'solid';
break;
case 'dashDotDot':
borderStyle = 'dashed';
break;
case 'slantDashDot':
borderStyle = 'dashed';
break;
case 'medium':
borderWidth = '2px';
break;
case 'mediumDashed':
borderStyle = 'dashed';
borderWidth = '2px';
break;
case 'mediumDashDotDot':
borderStyle = 'dashed';
borderWidth = '2px';
break;
case 'mdeiumDashDot':
borderStyle = 'dashed';
borderWidth = '2px';
break;
case 'double':
borderStyle = 'double';
break;
case 'thick':
borderWidth = '3px';
break;
default:
break;
}
// console.log(borderStyle, borderWidth);
return [borderStyle, borderWidth];
}

export default function XLSXPreView() {
// 表格数据
const [data, setData] = useState([]);
// 表格
const [sheet, setSheet] = useState([]);
// 主题
const [themeJson, setThemeJson] = useState([]);
// 合并的单元格
const [mergeRanges, setMergeRanges] = useState([]);

registerRenderer('customStylesRenderer', (hotInstance, td, row, column, prop, value, cellProperties) => {
textRenderer(hotInstance, td, row, column, prop, value, cellProperties);
// console.log(cellProperties);
// 填充样式
if('fill' in cellProperties){
// 背景颜色
if('fgColor' in cellProperties.fill && cellProperties.fill.fgColor){
td.style.background = getColor(cellProperties.fill.fgColor, themeJson);
}
}
// 字体样式
if('font' in cellProperties){
// 加粗
if('bold' in cellProperties.font && cellProperties.font.bold){
td.style.fontWeight = '700';
}
// 字体颜色
if('color' in cellProperties.font && cellProperties.font.color){
td.style.color = getColor(cellProperties.font.color, themeJson);
}
// 字体大小
if('size' in cellProperties.font && cellProperties.font.size){
td.style.fontSize = cellProperties.font.size + 'px';
}
// 字体类型
if('name' in cellProperties.font && cellProperties.font.name){
td.style.fontFamily = cellProperties.font.name;
}
// 字体倾斜
if('italic' in cellProperties.font && cellProperties.font.italic){
td.style.fontStyle = 'italic';
}
// 下划线
if('underline' in cellProperties.font && cellProperties.font.underline){
// 其实还有双下划线,但是双下划綫css中没有提供直接的设置方式,需要使用额外的css设置,所以我也就先懒得弄了
td.style.textDecoration = 'underline';
// 删除线
if('strike' in cellProperties.font && cellProperties.font.strike){
td.style.textDecoration = 'underline line-through';
}
}else{
// 删除线
if('strike' in cellProperties.font && cellProperties.font.strike){
td.style.textDecoration = 'line-through';
}
}

}
// 对齐
if('alignment' in cellProperties){
if('horizontal' in cellProperties.alignment){ // 水平
// 这里我直接用handsontable内置类做了,设置成类似htLeft的样子。
//(handsontable)其实至支持htLeft, htCenter, htRight, htJustify四种,但是其是它还有centerContinuous、distributed、fill,遇到这几种就会没有效果,也可以自己设置,但是我还是懒的弄了,用到的时候再说吧
const name = cellProperties.alignment.horizontal.charAt(0).toUpperCase() + cellProperties.alignment.horizontal.slice(1);
td.classList.add(`ht${name}`);
}
if('vertical' in cellProperties.alignment){ // 垂直
// 这里我直接用handsontable内置类做了,设置成类似htTop的样子。
const name = cellProperties.alignment.vertical.charAt(0).toUpperCase() + cellProperties.alignment.vertical.slice(1);
td.classList.add(`ht${name}`);
}
}
// 边框
if('border' in cellProperties){
if('left' in cellProperties.border && cellProperties.border.left){// 左边框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.left.style);
let color = '';
// console.log(row, column, borderWidth, borderStyle);
if(cellProperties.border.left.color){
color = getColor(cellProperties.border.left.color, themeJson);
}
td.style.borderLeft = `${borderStyle} ${borderWidth} ${color}`;
}
if('right' in cellProperties.border && cellProperties.border.right){// 左边框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.right.style);
// console.log(row, column, borderWidth, borderStyle);
let color = '';
if(cellProperties.border.right.color){
color = getColor(cellProperties.border.right.color, themeJson);
}
td.style.borderRight = `${borderStyle} ${borderWidth} ${color}`;
}
if('top' in cellProperties.border && cellProperties.border.top){// 左边框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.top.style);
let color = '';
// console.log(row, column, borderWidth, borderStyle);
if(cellProperties.border.top.color){
color = getColor(cellProperties.border.top.color, themeJson);
}
td.style.borderTop = `${borderStyle} ${borderWidth} ${color}`;
}
if('bottom' in cellProperties.border && cellProperties.border.bottom){// 左边框
const [borderWidth, borderStyle] = setBorder(cellProperties.border.bottom.style);
let color = '';
// console.log(row, column, borderWidth, borderStyle);
if(cellProperties.border.bottom.color){
color = getColor(cellProperties.border.bottom.color, themeJson);
}
td.style.borderBottom = `${borderStyle} ${borderWidth} ${color}`;
}
}

});

const handleFile = async (e) => {
const file = e.target.files[0];

const workbook = new Excel.Workbook();
await workbook.xlsx.load(file)

const worksheet = workbook.getWorksheet(1);

// const sheetRows = worksheet.getRows(1, worksheet.rowCount);
setSheet(worksheet)

// console.log(worksheet.getCell(1, 1).style);

// 遍历工作表中的所有行(包括空行)
const sheetData = [];
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
// console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
// 使用row.values获取每一行的值时总会多出一条空数据(第一条),这里我把它删除
const row_values = row.values.slice(1);
sheetData.push(row_values)
});
setData(sheetData);

// 将主题xml转换成json
const themeXml = workbook._themes.theme1;
const options = {
ignoreAttributes: false,
attributeNamePrefix: '_'
}
const parser = new XMLParser(options);
const json = parser.parse(themeXml)
setThemeJson(json);

// 获取合并的单元格
const mergeCells = [];

for(let i in worksheet._merges){
const {top, left, bottom, right} = worksheet._merges[i].model;
mergeCells.push({ row: top-1, col: left-1, rowspan: bottom-top+1 , colspan: right-left+1})
}
setMergeRanges(mergeCells)
console.log(worksheet);
}

return (
<>
<input type="file" onChange={handleFile}/>
<div id='table_view'>
<HotTable
data={data}
readOnly={true}
rowHeaders={true}
colHeaders={true}
width="100vw"
height="auto"
licenseKey='non-commercial-and-evaluation'
rowHeights={function(index) {
if(sheet.getRow(index+1).height){
// exceljs获取的行高不是像素值事实上它是23px - 13.8 的一个映射所以需要将它转化为像素值
return sheet.getRow(index+1).height * (23 / 13.8);
}
return 23;// 默认
}}
colWidths={function(index){
if(sheet.getColumn(index+1).width){
// exceljs获取的列宽不是像素值事实上它是81px - 8.22 的一个映射所以需要将它转化为像素值
return sheet.getColumn(index+1).width * (81 / 8.22);
}
return 81;// 默认
}}
cells={(row, col, prop) =>
{
const cellProperties = {};
const cellStyle = sheet.getCell(row+1, col+1).style

if(JSON.stringify(cellStyle) !== '{}'){
// console.log(row+1, col+1, cellStyle);
for(let key in cellStyle){
cellProperties[key] = cellStyle[key];
}
}
return {...cellProperties, renderer: 'customStylesRenderer'};
}}
mergeCells={mergeRanges}
/>

</div>
</>

)
}

作者:汤圆要吃咸的
来源:juejin.cn/post/7264461721279774780
收起阅读 »

告别axios,这个库让你爱上前端分页!

web
嗨,我们又见面了! 今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了! 那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢...
继续阅读 »

嗨,我们又见面了!


今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了!


那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢?这时候,alovajs就派上用场了!


alovajs:轻量级请求策略库


alovajs是一个轻量级的请求策略库,它可以帮助我们轻松处理分页请求。它支持开发者使用声明式实现各种复杂的请求,比如请求共享、分页请求、表单提交、断点续传等等。使用alovajs,我们可以用很少的代码就实现高效、流畅的请求功能。比如,在Vue中,你可以这样使用alovajs进行分页请求:


const alovaInstance = createAlova({
// VueHook用于创建ref状态,包括请求状态loading、响应数据data、请求错误对象error等
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responded: response => response.json()
});

const { loading, data, error } = useRequest(
alovaInstance.Get('https://api.alovajs.org/profile', {
params: {
id: 1
}
})
);

看到了吗?只需要几行代码,alovajs就帮我们处理了分页请求的各种细节,我们再也不用手动维护那些繁琐的状态了!


对比axios,alovajs的优势


和axios相比,alovajs有哪些优势呢?首先,alovajs与React、Vue等现代前端框架深度融合,可以自动管理请求相关数据,大大提高了开发效率。其次,alovajs在性能方面做了很多优化,比如默认开启了内存缓存和请求共享,这些都能显著提高请求性能,提升用户体验的同时还能降低服务端的压力。最后,alovajs的体积更小,压缩后只有4kb+,相比之下,axios则有11+kb。


总之,如果你想在分页加载方面做得更轻松、更高效,alovajs绝对值得一试!



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

indexOf的第二个参数你用过嘛🤔

web
大家好,我是哈默。indexOf 是我们非常熟悉的一个方法,它可以用来获取某一个元素在一个数组里的位置,我们一般就会使用 array.indexOf(element) 的方法来进行使用。 但是,大家有没有使用过 indexOf 的第二个参数呢?第二个参数的使用...
继续阅读 »

大家好,我是哈默。indexOf 是我们非常熟悉的一个方法,它可以用来获取某一个元素在一个数组里的位置,我们一般就会使用 array.indexOf(element) 的方法来进行使用。


但是,大家有没有使用过 indexOf 的第二个参数呢?第二个参数的使用会经常出现在一些优秀的库的源码当中,用于依次分析(或者说扫描)某一个字符串。


比如命令行美化输出的 chalk 库中就有此应用,因为 chalk 库的原理就是对于我们输出在终端的内容进行处理,然后将处理后的字符串显示在终端上。


indexOf 基本用法


首先,我们还是先来回顾一下 indexOf 的最基本用法。


给定一个数组:[10, 20, 30],寻找这个数组中 30 的位置,是 2


const arr = [10, 20, 30];
const element = 30;
const index = arr.indexOf(element);

console.log(index); // 2

indexOf 的第二个参数


明确了 indexOf 的基本用法以后,它的第 2 个参数有什么用呢?


其实是起到了一个调整从哪里开始查找的作用。


我们来看一个例子:


const arr = [10, 20, 30];
const element = 10;
const index = arr.indexOf(element);

console.log(index); // 0

const arr2 = [10, 20, 30, 10];
const element2 = 10;
const index2 = arr2.indexOf(element2, 1);

console.log(index2); // 3

可以看到,同样是查找 [10, 20, 30, 10] 当中 10 的位置,但是因为第一次是从数组第 1 个元素开始查找的,所以得到的结果是 0。


而第二次是从数组的第 2 个元素开始查找的,所以得到的结果是 3。


优秀库源码里的使用


明确了 indexOf 第二个参数的使用之后,我们再来看一下在一些优秀的库的源码里面,它们是如何利用起这个第二个参数的作用的。



⚠️注意:我下面会以 String.prototype.indexOf 举例,而上面举的例子是以 Array.prototype.indexOf 为例,但是这两个 API 的第二个参数都是起到一个搜索位置的作用,所以在这里可以一起学习一下



这里,我们只会分析它的思想,具体的实现在具体的源码里会存在差异,但思想是相同的。


我们首先定义一个方法,addEmoji,接受三个参数:


/**
* 在一个 string 的 targetString 后面,加上一个 emoji
* @param string 原始 string
* @param targetString 加 emoji 的那个 string
* @param emoji 加入的 emoji
* @returns 处理后的最终结果
*/

function addEmoji(string, targetString, emoji) {
let result = "";

// 一系列处理
// ...

return result;
}

我们最终会这样调用,在 大家好,我是哈默,今天是一个好天气。 这个字的后面,加上 👍 的 emoji:


const res = addEmoji("大家好,我是哈默,今天是一个好天气。", "好", "👍");
console.log(res);

那么首先我们就可以使用 indexOf 方法来从输入的字符串里找到 的位置:


function addEmoji(string, targetString, emoji) {
// 找到 targetString 的位置
let index = string.indexOf(targetString);

let result = "";

// 记录当前扫描到的位置,现在是在参数 string 的开头位置
// 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
let currentScanIndex = 0;

return result;
}

如果我们找到了 targetString,即 index !== -1,那么我们就在 targetString 后,加上一个 emoji:


function addEmoji(string, targetString, emoji) {
// 找到 targetString 的位置
let index = string.indexOf(targetString);

let result = "";

// 记录当前扫描到的位置,现在是在参数 string 的开头位置
// 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
let currentScanIndex = 0;

// 如果找到了 targetString
if (index !== -1) {
// 在 targetString 后面增加 emoji
result += string.slice(currentScanIndex, index) + targetString + emoji;
// 将当前扫描位置,移动到 targetString 之后的那个位置上
currentScanIndex = index + targetString.length;
}

// 将 targetString 之后的内容追加到 result 里
result += string.slice(currentScanIndex);

return result;
}

此时,我们在第一个 字后面,加上了 👍,得到的结果:


res1.png


但是,我们这个字符串中,还有一个 好天气,也就是存在多个 targetString,所以我们这里不能是 if 只执行一次,而是要做一个循环。


我们可以使用一个 while 循环:


function addEmoji(string, targetString, emoji) {
// 找到 targetString 的位置
let index = string.indexOf(targetString);

let result = "";

// 记录当前扫描到的位置,现在是在参数 string 的开头位置
// 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
let currentScanIndex = 0;

// 如果找到了 targetString
while (index !== -1) {
// 在 targetString 后面增加 emoji
result += string.slice(currentScanIndex, index) + targetString + emoji;
// 将当前扫描位置,移动到 targetString 之后的那个位置上
currentScanIndex = index + targetString.length;
+ // 重点来了!!!我们要从当前扫描的位置开始,去寻找 targetString
+ index = string.indexOf(targetString, currentScanIndex);
}

// 将 targetString 之后的内容追加到 result 里
result += string.slice(currentScanIndex);

return result;
}

此时,我们便成功的给第二个 ,也加上了 emoji:


res2.png


这个地方我们就使用到了之前提到的 indexOf 的第二个参数:


// 重点来了!!!我们要从当前扫描的位置开始,去寻找 targetString
index = string.indexOf(targetString, currentScanIndex);

我们是从当前扫描到的位置 currentScanIndex 开始,查找 targetString 的,这样我们就可以找到下一个 targetString 了。


所以,这里的思想就是通过 indexOf 的第二个参数,帮助我们能够依次扫描一个字符串,依次找到我们想要找的那个元素的位置,然后做相应的处理。


总结


indexOf 的第二个参数,叫 fromIndex,看到这里,大家应该也能很好的理解这个 fromIndex 的作用了,就是从哪里开始找嘛!


作者:我是哈默
来源:juejin.cn/post/7332858431571230747
收起阅读 »

春晚刘谦魔术的模拟程序

web
昨晚春晚上刘谦的两个魔术表演都非常精彩,尤其是第二个魔术,他演绎了经典的约瑟夫环问题! 什么是约瑟夫环问题? 约瑟夫环(Josephus problem)是一个经典的数学问题,最早由古罗马历史学家弗拉维奥·约瑟夫斯提出,但它的名字是在19世纪由德国数学家约瑟夫...
继续阅读 »

昨晚春晚上刘谦的两个魔术表演都非常精彩,尤其是第二个魔术,他演绎了经典的约瑟夫环问题!


什么是约瑟夫环问题?


约瑟夫环(Josephus problem)是一个经典的数学问题,最早由古罗马历史学家弗拉维奥·约瑟夫斯提出,但它的名字是在19世纪由德国数学家约瑟夫·乔瑟夫斯(Josef Stein)命名的。


问题的描述是这样的:假设有n个人(编号从1到n)站成一个圆圈,从第一个人开始报数,报到某个数字(例如k)的人就被杀死,然后从下一个人开始重新报数并继续这个过程,直到只剩下一个人留下来。


问题的关键是找出存活下来的那个人的编号。


结合扑克牌解释约瑟夫环问题


1、考虑最简单的情况


假设有2张牌,编号分别是1和2。


首先将1放到后面,扔掉2。剩下的就是最开始放在最上边的那张1。


2、稍微复杂一点的情况,牌的张数是2的n次方


比如有8张牌,编号分别是1、2、3、4、5、6、7、8。


第一轮会把2、4、6、8扔掉,剩下1、3、5、7按顺序放在后面,又退化成了4张牌的情况。


第二轮会把3、7扔掉,剩下1、5按顺序放在后面,又退化成了2张牌的情况。


第三轮把5扔掉,剩下1,就是最初在最前面的那张。


结论:如果牌的张数是2^n,最后剩下的一定是最开始放在牌堆顶的那张。


3、考虑任意的情况,牌的张数是2^n+m


比如牌的张数是11,等于8+3。把1放到后面,把2扔掉,把3放到后面,把4扔掉,把5放到后面,把6扔掉,现在剩下的编号序列是7、8、9、10、11、1、3、5,这又是8张牌的情况!最后一定剩下的是现在牌堆顶的7!


因此,只要提前知道牌的张数,就一定能马上推导出最终是剩下哪一张牌。一切的魔法都是数学!!都是算法!!


见证奇迹的时刻!魔术的流程



  1. 4张牌对折后撕开,就是8张,叠放在一起就是ABCDABCD。注意,ABCD四个数字是完全等价的。

  2. 根据名字字数,把顶上的牌放到下面,但怎么放都不会改变循环序列的相对位置。譬如2次,最后变成CDABCDAB;譬如3次,最后换成DABCDABC。但无论怎么操作,第4张和第8张牌都是一样的。

  3. 把顶上3张插到中间任意位置。这一步非常重要!因为操作完之后必然出现第1张和第8张牌是一样的!以名字两个字为例,可以写成BxxxxxxB(这里的x是其他和B不同的牌)。

  4. 拿掉顶上的牌放到一边,记为B。剩下的序列是xxxxxxB,一共7张牌。

  5. 南方人/北方人/不确定,分别拿顶上的1/2/3张牌插到中间,但是不会改变剩下7张牌是xxxxxxB的结果。

  6. 男生拿掉1张,女生拿掉2张。也就是男生剩下6张,女生剩下5张。分别是xxxxxB和xxxxB。

  7. 循环7次,把最顶上的放到最底下,男生和女生分别会是xxxxBx和xxBxx。

  8. 最后执行约瑟夫环过程!操作到最后只剩下1张。当牌数为6时(男生),剩下的就是第5张牌;当牌数为5时(女生),剩下的就是第3张牌。Bingo!就是第4步拿掉的那张牌!


下面是完整的 JavaScript 代码实现:


// 定义一个函数,用于把牌堆顶n张牌移动到末尾
function moveCardBack(n, arr) {
// 循环n次,把队列第一张牌放到队列末尾
for (let i = 0; i < n; i++) {
const moveCard = arr.shift(); // 弹出队头元素,即第一张牌
arr.push(moveCard); // 把原队头元素插入到序列末尾
}
return arr;
}

// 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置
function moveCardMiddleRandom(n, arr) {
// 插入在arr中的的位置,随机生成一个idx
// 这个位置必须是在n+1到arr.length-1之间
const idx = Math.floor(Math.random() * (arr.length - n - 1)) + n + 1;
// 执行插入操作
const newArr = arr.slice(n, idx).concat(arr.slice(0, n)).concat(arr.slice(idx));
return newArr;
}

// 步骤1:初始化8张牌,假设为"ABCDABCD"
let arr = ["A", "B", "C", "D", "A", "B", "C", "D"];
console.log("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。");
console.log("此时序列为:" + arr.join('') + "\n---");

// 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)
const nameLen = Math.floor(Math.random() * 4) + 2;
// 把nameLen张牌移动到序列末尾
arr = moveCardBack(nameLen, arr);
console.log(`步骤2:随机选取名字长度为${nameLen},把第1张牌放到末尾,操作${nameLen}次。`);
console.log(`此时序列为:${arr.join('')}\n---`);

// 步骤3(关键步骤):把牌堆顶三张放到中间任意位置
arr = moveCardMiddleRandom(3, arr);
console.log(`步骤3:把牌堆顶3张放到中间的随机位置。`);
console.log(`此时序列为:${arr.join('')}\n---`);

// 步骤4(关键步骤):把最顶上的牌拿走
const restCard = arr.shift(); // 弹出队头元素
console.log(`步骤4:把最顶上的牌拿走,放在一边。`);
console.log(`拿走的牌为:${restCard}`);
console.log(`此时序列为:${arr.join('')}\n---`);

// 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置
// 随机选择1、2、3中的任意一个数字
const moveNum = Math.floor(Math.random() * 3) + 1;
arr = moveCardMiddleRandom(moveNum, arr);
console.log(`步骤5:我${moveNum === 1 ? '是南方人' : moveNum === 2 ? '是北方人' : '不确定自己是哪里人'},\
${moveNum}张牌插入到中间的随机位置。`
);
console.log(`此时序列为:${arr.join('')}\n---`);

// 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌
const maleNum = Math.floor(Math.random() * 2) + 1; // 随机选择1或2
for (let i = 0; i < maleNum; i++) { // 循环maleNum次,移除牌堆顶的牌
arr.shift();
}
console.log(`步骤6:我是${maleNum === 1 ? '男' : '女'}生,移除牌堆顶的${maleNum}张牌。`);
console.log(`此时序列为:${arr.join('')}\n---`);

// 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次
arr = moveCardBack(7, arr);
console.log(`步骤7:把顶部的牌移动到末尾,执行7次`);
console.log(`此时序列为:${arr.join('')}\n---`);

// 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。
console.log(`步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。`);
while (arr.length > 1) {
const luck = arr.shift(); // 好运留下来
arr.push(luck);
console.log(`好运留下来:${luck}\t\t此时序列为:${arr.join('')}`);
const sadness = arr.shift(); // 烦恼都丢掉
console.log(`烦恼都丢掉:${sadness}\t\t此时序列为:${arr.join('')}`);
}
console.log(`---\n最终结果:剩下的牌为${arr[0]},步骤4中留下来的牌也是${restCard}`);


这段代码实现了昨晚春晚上刘谦的第二个魔术表演的过程,并提供了详细的解释。享受魔术的魅力吧!


image-20240210161329783


image-20240210161339317


看到观看的人这么多,除了JavaScript,下面我补充了一些其他语言的实现


import random

# 定义一个函数,用于把牌堆顶n张牌移动到末尾
def move_card_back(n, arr):
   for i in range(n):
       move_card = arr.pop(0)
       arr.append(move_card)
   return arr

# 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置
def move_card_middle_random(n, arr):
   idx = random.randint(n + 1, len(arr) - 1)
   new_arr = arr[n:idx] + arr[0:n] + arr[idx:]
   return new_arr

# 步骤1:初始化8张牌,假设为"ABCDABCD"
arr = ["A", "B", "C", "D", "A", "B", "C", "D"]
print("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。")
print("此时序列为:" + ''.join(arr) + "\n---")

# 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)
name_len = random.randint(2, 5)
move_card_back(name_len, arr)
print("步骤2:随机选取名字长度为" + str(name_len) + ",把第1张牌放到末尾,操作" + str(name_len) + "次。")
print("此时序列为:" + ''.join(arr) + "\n---")

# 步骤3(关键步骤):把牌堆顶三张放到中间任意位置
arr = move_card_middle_random(3, arr)
print("步骤3:把牌堆顶3张放到中间的随机位置。")
print("此时序列为:" + ''.join(arr) + "\n---")

# 步骤4(关键步骤):把最顶上的牌拿走
rest_card = arr.pop(0)
print("步骤4:把最顶上的牌拿走,放在一边。")
print("拿走的牌为:" + rest_card)
print("此时序列为:" + ''.join(arr) + "\n---")

# 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置
# 随机选择1、2、3中的任意一个数字
move_num = random.randint(1, 3)
arr = move_card_middle_random(move_num, arr)
print("步骤5:我" + ("是南方人" if move_num == 1 else "是北方人" if move_num == 2 else "不确定自己是哪里人") + ",把" + str(move_num) + "张牌插入到中间的随机位置。")
print("此时序列为:" + ''.join(arr) + "\n---")

# 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌
male_num = random.randint(1, 2)
for i in range(male_num):
   arr.pop(0)
print("步骤6:我是" + ("男" if male_num == 1 else "女") + "生,移除牌堆顶的" + str(male_num) + "张牌。")
print("此时序列为:" + ''.join(arr) + "\n---")

# 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次
for i in range(7):
   move_card = arr.pop(0)
   arr.append(move_card)
print("步骤7:把顶部的牌移动到末尾,执行7次")
print("此时序列为:" + ''.join(arr) + "\n---")

# 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。
print("步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。")
while len(arr) > 1:
   luck = arr.pop(0)
   arr.append(luck)
   print("好运留下来:" + luck + "\t\t此时序列为:" + ''.join(arr))
   sadness = arr.pop(0)
   print("烦恼都丢掉:" + sadness + "\t\t此时序列为:" + ''.join(arr))
print("---\n最终结果:剩下的牌为" + arr[0] + ",步骤4中留下来的牌也是" + rest_card)


java


import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Main {
   public static void main(String[] args) {
       List<String> arr = new ArrayList<>();
       arr.add("A");
       arr.add("B");
       arr.add("C");
       arr.add("D");
       arr.add("A");
       arr.add("B");
       arr.add("C");
       arr.add("D");

       System.out.println("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。");
       System.out.println("此时序列为:" + String.join("", arr));
       System.out.println("---");

       Random rand = new Random();

       int nameLen = rand.nextInt(4) + 2;
       moveCardBack(nameLen, arr);
       System.out.println("步骤2:随机选取名字长度为" + nameLen + ",把第1张牌放到末尾,操作" + nameLen + "次。");
       System.out.println("此时序列为:" + String.join("", arr));
       System.out.println("---");

       moveCardMiddleRandom(3, arr);
       System.out.println("步骤3:把牌堆顶3张放到中间的随机位置。");
       System.out.println("此时序列为:" + String.join("", arr));
       System.out.println("---");

       String restCard = arr.remove(0);
       System.out.println("步骤4:把最顶上的牌拿走,放在一边。");
       System.out.println("拿走的牌为:" + restCard);
       System.out.println("此时序列为:" + String.join("", arr));
       System.out.println("---");

       int moveNum = rand.nextInt(3) + 1;
       moveCardMiddleRandom(moveNum, arr);
       System.out.println("步骤5:我" + (moveNum == 1 ? "是南方人" : moveNum == 2 ? "是北方人" : "不确定自己是哪里人") + ",把" + moveNum + "张牌插入到中间的随机位置。");
       System.out.println("此时序列为:" + String.join("", arr));
       System.out.println("---");

       int maleNum = rand.nextInt(2) + 1;
       for (int i = 0; i < maleNum; i++) {
           arr.remove(0);
      }
       System.out.println("步骤6:我是" + (maleNum == 1 ? "男" : "女") + "生,移除牌堆顶的" + maleNum + "张牌。");
       System.out.println("此时序列为:" + String.join("", arr));
       System.out.println("---");

       for (int i = 0; i < 7; i++) {
           String moveCard = arr.remove(0);
           arr.add(moveCard);
      }
       System.out.println("步骤7:把顶部的牌移动到末尾,执行7次");
       System.out.println("此时序列为:" + String.join("", arr));
       System.out.println("---");

       System.out.println("步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。");
       while (arr.size() > 1) {
           String luck = arr.remove(0);
           arr.add(luck);
           System.out.println("好运留下来:" + luck + "\t\t此时序列为:" + String.join("", arr));
           String sadness = arr.remove(0);
           System.out.println("烦恼都丢掉:" + sadness + "\t\t此时序列为:" + String.join("", arr));
      }
       System.out.println("---\n最终结果:剩下的牌为" + arr.get(0) + ",步骤4中留下来的牌也是" + restCard);
  }

   private static void moveCardBack(int n, List<String> arr) {
       for (int i = 0; i < n; i++) {
           String moveCard = arr.remove(0);
           arr.add(moveCard);
      }
  }

   private static void moveCardMiddleRandom(int n, List<String> arr) {
       Random rand = new Random();
       int idx = rand.nextInt(arr.size() - n - 1) + n + 1;
       List<String> newArr = new ArrayList<>(arr.subList(n, idx));
       newArr.addAll(arr.subList(0, n));
       newArr.addAll(arr.subList(idx, arr.size()));
       arr.clear();
       arr.addAll(newArr);
  }
}


以及c++代码


#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdlib>
#include <ctime>

void moveCardBack(int n, std::vector<std::string>& arr) {
   for (int i = 0; i < n; i++) {
       std::string moveCard = arr[0];
       arr.erase(arr.begin());
       arr.push_back(moveCard);
  }
}

void moveCardMiddleRandom(int n, std::vector<std::string>& arr) {
   int idx = rand() % (arr.size() - n - 1) + n + 1;
   std::vector<std::string> newArr;
   newArr.insert(newArr.end(), arr.begin() + n, arr.begin() + idx);
   newArr.insert(newArr.end(), arr.begin(), arr.begin() + n);
   newArr.insert(newArr.end(), arr.begin() + idx, arr.end());
   arr = newArr;
}

int main() {
   srand(time(0));

   std::vector<std::string> arr = {"A", "B", "C", "D", "A", "B", "C", "D"};
   std::cout << "步骤1:拿出4张牌,对折撕成8张,按顺序叠放。" << std::endl;
   std::cout << "此时序列为: ";
   for (const std::string& card : arr) {
       std::cout << card;
  }
   std::cout << std::endl;
   std::cout << "---" << std::endl;

   int nameLen = rand() % 4 + 2;
   moveCardBack(nameLen, arr);
   std::cout << "步骤2:随机选取名字长度为" << nameLen << ",把第1张牌放到末尾,操作" << nameLen << "次。" << std::endl;
   std::cout << "此时序列为: ";
   for (const std::string& card : arr) {
       std::cout << card;
  }
   std::cout << std::endl;
   std::cout << "---" << std::endl;

   moveCardMiddleRandom(3, arr);
   std::cout << "步骤3:把牌堆顶3张放到中间的随机位置。" << std::endl;
   std::cout << "此时序列为: ";
   for (const std::string& card : arr) {
       std::cout << card;
  }
   std::cout << std::endl;
   std::cout << "---" << std::endl;

   std::string restCard = arr[0];
   arr.erase(arr.begin());
   std::cout << "步骤4:把最顶上的牌拿走,放在一边。" << std::endl;
   std::cout << "拿走的牌为: " << restCard << std::endl;
   std::cout << "此时序列为: ";
   for (const std::string& card : arr) {
       std::cout << card;
  }
   std::cout << std::endl;
   std::cout << "---" << std::endl;

   moveCardMiddleRandom(rand() % 3 + 1, arr);
   std::cout << "步骤5:我" << (rand() % 2 == 0 ? "是南方人" : "是北方人") << ",把" << rand() % 3 + 1 << "张牌插入到中间的随机位置。" << std::endl;
   std::cout << "此时序列为: ";
   for (const std::string& card : arr) {
       std::cout << card;
  }
   std::cout << std::endl;
   std::cout << "---" << std::endl;

   int maleNum = rand() % 2 + 1;
   for (int i = 0; i < maleNum; i++) {
       arr.erase(arr.begin());
  }
   std::cout << "步骤6:我" << (maleNum == 1 ? "男" : "女") << "生,移除牌堆顶的" << maleNum << "张牌。" << std::endl;
   std::cout << "此时序列为: ";
   for (const std::string& card : arr) {
       std::cout << card;
  }
   std::cout << std::endl;
   std::cout << "---" << std::endl;

   for (int i = 0; i < 7; i++) {
       std::string moveCard = arr[0];
       arr.erase(arr.begin());
       arr.push_back(moveCard);
  }
   std::cout << "步骤7:把顶部的牌移动到末尾,执行7次" << std::endl;
   std::cout << "此时序列为: ";
   for (const std::string& card : arr) {
       std::cout << card;
  }
   std::cout << std::endl;
   std::cout << "---" << std::endl;

   std::cout << "步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。" << std::endl;
   while (arr.size() > 1) {
       std::string luck = arr[0];
       arr.erase(arr.begin());
       arr.push_back(luck);
       std::cout << "好运留下来: " << luck << "\t\t此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;

       std::string sadness = arr[0];
       arr.erase(arr.begin());
       std::cout << "烦恼都丢掉: " << sadness << "\t\t此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
  }
   std::cout << "---\n最终结果: " << arr[0] << ", 步骤4中留下来的牌也是" << restCard << std::endl;

   return 0;
}

作者:小u
来源:juejin.cn/post/7332865125640044556
收起阅读 »

rpc比http好吗,缪论?

是什么,如何理解 RPC(Remote Procedure Call) 直译就是远程过程调用 HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议 RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议...
继续阅读 »

是什么,如何理解


RPC(Remote Procedure Call) 直译就是远程过程调用


HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议


RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议、效率、应用范围、使用规则等不同,所以是不同的名字,本质都是为了分布式系统间的通信而生,是一种应用层通信(请求-响应)协议(从OSI网络模型来看)。



  • RPC是 Bruce Jay Nelson 在1981年创造的术语,HTTP是在1990年左右产生的(可以参看维基百科)


RPC协议 和 RPC,到底叫什么?RPC协议=RPC


HTTP协议、HTTP,到底叫什么?HTTP协议=HTTP


RPC|HTTP只是大家的简称



  • HTTP协议不仅仅只有协议,还有超文本,传输,以及很多功能(比如编解码、面试经常背的各种参数的作用)

  • RPC协议也不仅仅只有协议,还有 编解码,服务注册发现,负载均衡等


RPC协议本质上定义了一种通信的流程,而具体的实现技术是没有约束的,每一种RPC框架都有自己的实现方式,我认为HTTP也是RPC的一种实现方式


协议直白来讲是一种约定,rpc和http都是为了服务器间的通信而生,都需要制定一套标准协议来进行通信。不过HTTP比较火,是一个全世界的统一约定,使用比较广泛。但通用也意味着冗余,所以后来又产生了很多RPC框架(自定义协议,具备优秀的性能等)


我们可以自定义RPC请求/响应 包含的消息头和消息体结构,自定义编解码方式,自定义网络通信方式,只要clientserver消息的发送和解析能对应即可,这些问题确认下来,一个RPC框架就设计出来了


下面先从请求过程看一下RPC和HTTP都会经历哪些阶段,然后再分阶段去做对比


一次请求的过程



阶段阶段分层RPCHTTP
client: 业务逻辑xx业务逻辑层
client: 客户端构造请求,发起调用编解码thrift|json|protobuf等json|图片等
client: 根据传输协议构造数据流协议层thrift|gRPC|Kitex|dubbo等HTTP1 |HTTP1.1|HTTP2|QUIC等
client: 服务发现服务发现自定义内部服务发现组件DNS
client: 网络通信:传输数据流网络通信层接口层:netty|netpool,根据OS的API做了一些封装本质:TCP|UDP|HTTP系列接口层:HTTP内部自己实现,目前不清楚咋做的本质:TCP|UDP
server: 把数据流解析为协议结构协议层略,同上略,同上
server: 解析协议中的请求体编解码略,同上略,同上
server: 执行业务逻辑xx业务逻辑层略,同上略,同上

从请求链路可以看到,最核心的只有三层:编解码、协议、网络通信


下面会从这3个角度去对比HTTP和RPC


HTTP VS RPC自定义协议


HTTP和RPC 2个关键词不具备可比较性,因为RPC包含了HTTP。


但是RPC自定义协议(thrift, protobuf, dubbo, kitex-thrift等) 是RPC的具体实现,HTTP也是RPC的具体实现,它们是具备可比较性的


编解码(序列化)



  • 序列化: 指将程序运行过程中的动态内存数据(java的class、go的struct)转化为硬盘中静态二进制数据的过程,以方便网络传输。

  • 反序列化:指将硬盘中静态二进制数据转化为程序运行过程中的动态内存数据的过程,以方便程序计算。


HTTP/1.1 一般用json


自定义RPC协议 一般用 thrift、protobuf


kitex序列化协议


维度json(HTTP/1.1)protobuf(gRPC)
优点1. 可读性好、使用简单,学习成本低1. 序列化后的体积比json小 => 传输效率高
2. 序列化/反序列化速度快 => 性能损耗小
缺点1. JSON 进行序列化的额外空间开销比较大
2. JSON 没有类型,比如无法区分整数和浮点
像 Java 、Go这种强类型语言,不是很友好,解析速度比较慢(需要通过反射解决)
1. 不可读,都是二进制
适用场景适用于服务提供者与服务调用者之间传输的数据量要相对较小的情况,否则会严重影响性能追求高性能的场景

协议层


编码之后,数据转换成字节流,但是RPC通信时,每次请求发送的数据大小不是固定的,那么为了区分消息的边界,避免粘包、半包等现象,我们需要定义一种协议,来使得接收方能够正确地读出不定长的内容。简单点说,通信协议就是约定客户端和服务器端传输什么数据,以及如何解析数据。


维度HTTP/1.1kitex-TTHeader
优点1. 灵活,可以自定义很多字段
2. 几乎所有设备都可以支持HTTP协议
1. 灵活,通用,可以自定义
  • 自定义必要字段即可 => 减小报文体积,提高传输效率
    2. 性能优秀
  • 缺点1. 包含许多为了适应浏览器的冗余字段,这些是内部服务用不到的,性能差1. 部分设备存在不能支持,通用性欠佳

    可参考



    可以思考一下 序列化、传输协议、网络通信的关系,下面以kitex为例进行分析


    kitex codec 接口定义kitex thrift 序列化实现kitex ttheader协议,kitex 发送请求核心代码



    可以发现 Encode中,先根据message构造出header,写入out,然后再把data(实际的业务数据)写到out。


    encode函数完全遵守 ttheader协议去构造数据。


    最后再把out通过网络库发送出去



    网络通信层


    网络通信层主要提供一个易用的网络库,封装了操作系统提供的socket api。


    维度HTTP/1.1kitex框架
    实现方式一般采用短连接需要3次握手(可以配置长链接添加请求头Keep-Alive: timeout=20)- 长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包rpc框架维护一个tcp连接池,每次用完不断开连接,通过心跳检测断开连接(探测服务是否有问题)- 支持短连接、长连接池、连接多路复用以及连接池状态监控。
    优点1. 几乎所有设备都可以支持HTTP协议1. 不用每次请求都经历三次握手&四次挥手,减少延时
    缺点1. 每次请求都要新建连接,性能差1. 部分设备存在不能支持,通用性欠佳

    HTTP的长连接和TCP长连接不是一个东西,需要注意下,TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置(只能保证网络没问题,不能代表服务没问题)


    其中 HTTP2 拥有多路复用、优先级控制、头部压缩等优势


    可以参考


    kitex:连接类型


    RPC自定义协议 和 HTTP的使用场景


    公司内部的微服务,对客户端提供的服务 适合用RPC,更好的性能


    对外服务、单体服务、为前端提供的服务适合用HTTP


    我的思考


    rpc在编解码、协议层、网络通信 都比HTTP有更大的优势,那为啥不把HTTP换成RPC呢



    1. 人的认知,HTTP已经深入人心(或者说生态好,通用性强),几乎所有的机器、浏览器和语言默认都会支持。但是自定义RPC协议 可能很多人都没听过(比如kitex、dubbo等),还让别人支持,根本不可能。

      • 需要建设全局的DNS等等,HTTP链路中的组件都需要换成 自定义的那一套,成本极高。

      • 但是公司内部可以搞成一套,可以极大提高性能,何乐而不为。

      • 我见过的案例是很多时候并没有深入思考为什么用,而是大家都这么用,我也这么用。



    2. 浏览器只支持 http协议。而且浏览器不支持自定义编解码的解析

      • 为啥大家面向浏览器/前端 不用自定义编解码?

        • 举例:protobuf不支持前端语言,但是支持java

        • 就是自定义编解码框架支持语言有限,很多语言没有工具可以做,并且浏览器也不支持。对于问题排查比较困难。

        • github.com/protocolbuf…



      • http不仅可以传输json、还可以传输二进制、图片等。所以协议层可以用http,编解码用protobuf/thrift也是可行的。

        • 公司内部实际案例:服务端和客户端交互时,为了提高性能,采用protobuf编解码数据,使用http协议传输数据。

          • 但是每次请求/响应数据都是不可读的。服务端会把protobuf编码前的数据转为json,用于打印log/存储,方便排查问题。





      • 参考 丨隋堤倦客丨的评论





    • RPC框架 可以自定义负载均衡,重试机制,高可用,流量控制等策略。这些是HTTP不能支持的

      • 我理解是协议层用的http,但是内部的运行机制还是自定义的。http只是定义了传输数据的格式。举个例子:http的流量控制其实用的是 tcp的滑动窗口,http协议本身不具备这些功能。但是rpc是可以自己加这些功能的。这些功能必然有数据传输,这个传输协议用的http。

      • 参考 leewp同学的评论




    参考


    如何保活主流RPC框架长连接,Dubbo的心跳机制,值得学习_牛客博客


    3.8 既然有 HTTP 协议,为什么还要有 RPC?


    4.15 TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?


    RPC 漫谈: 连接问题


    聊一聊Go网络编程(一)--TCP连接通信 - 掘金


    Kitex前传:RPC框架那些你不得不知的故事


    kitex 传输协议


    dubbo RPC 协议


    作者:cli
    来源:juejin.cn/post/7264454873588449336
    收起阅读 »

    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
    收起阅读 »

    代码字体 ugly?试试这款高颜值代码字体

    Monaspace 是有 GitHub 开源的代码字体,包含 5 种变形字体的等宽代码字体家族,颜值 Up,很难不喜欢。 来看一下这 5 种字体分别是: 1️⃣ Radon 手写风格字体 2️⃣ Krypton 机械风格字体 3️⃣ Xenon 衬线风格字...
    继续阅读 »

    Monaspace 是有 GitHub 开源的代码字体,包含 5 种变形字体的等宽代码字体家族,颜值 Up,很难不喜欢。


    来看一下这 5 种字体分别是:


    1️⃣ Radon 手写风格字体



    2️⃣ Krypton 机械风格字体

    3️⃣ Xenon 衬线风格字体



    4️⃣ Argon 人文风格字体



    5️⃣ Neon 现代风格字体



    👉 项目地址:github.com/githubnext/…


    下载方式


    MacOS


    使用 brew 安装:


    brew tap homebrew/cask-fonts
    brew install font-monaspace

    Windows


    下载该文件:github.com/githubnext/…


    拖到 C:\Windows\Fonts 中,点击安装


    下载好后,如果是 VSCode 文件,可以在设置中找到 font-family,改为:'Monaspace Radon', monospace





    作者:吴楷鹏
    来源:juejin.cn/post/7332435905925562418
    收起阅读 »

    2024年,为啥我不建议应届生再去互联网?

    最近快过年了,和还留在成都的一些研究生同学吃了顿饭,其中博士姐姐因为今年刚刚毕业,所以在饭局里面还跟我们谈了一下今年的师弟师妹们的去向。 她说今年虽然就业挺难的,但是师弟师妹们的工作还都挺好的,有去成飞的,有去选调的还有去了一些大国企研究所的。然后我就问今年没...
    继续阅读 »

    最近快过年了,和还留在成都的一些研究生同学吃了顿饭,其中博士姐姐因为今年刚刚毕业,所以在饭局里面还跟我们谈了一下今年的师弟师妹们的去向。


    她说今年虽然就业挺难的,但是师弟师妹们的工作还都挺好的,有去成飞的,有去选调的还有去了一些大国企研究所的。然后我就问今年没有去互联网的吗?她说有哇,有学弟去了美团,钱还开得挺多的,有学弟去了个独角兽做算法,但是也就这两个人去了互联网相关的了。


    其实听到这个我还是蛮感慨的,在我毕业的时候互联网还是如日中天,大多数计算机毕业的孩子首选的就是去互联网狠狠的大赚一笔。短短3年间,去互联网的应届生就屈指可数了,一方面是这两年互联网大厂缩招严重,进互联网没有我们当年那么容易。另一方面是,在大环境不容乐观的今天,以及互联网增长见顶的背景下,互联网的工作其实已经不是应届生的首选工作了。


    实际上,即使在今年你能过千军万马杀出重围拿到互联网的offer,作为一个过来人我也不是很建议你再去趟互联网这趟浑水。因为,作为一个新人在一个注定下行的行业当中,你可能搭上的不是通往财富自由的快车道,很快你需要考虑的可能就是你还能不能保住你手头的这份工作的问题。


    说一个老生常谈的事情,互联网的增长确实见底了,阿里、腾讯、网易的股票最近狂跌,阿里都跌回2014年了,只有抖音还依然坚挺一些但是依然看不到未来成长的空间。从2014年到2024年,正好十年的时间,互联网员工们加班加点996,熬夜爆肝的奋斗,最终的结果尽然是回到了原点。


    其实,这个事情也并不奇怪,这些互联网大厂只是坐在电梯里面的人,他们都觉得自己能够取得成功是因为自己在电梯里面做俯卧撑。实际上,跟你在电梯上做啥没有关系,你之所以能够成功只是因为你恰好赶上了这班电梯而已,跟你在里面睡觉还是瞎折腾关系都不大。如今风停了,电梯开始往下走了,作为个体你非要去搭上这个末班车并且期待在踩在早就已经上电梯的这群人的头上的话,那么我只能跟你说,祝你好运了。


    其实,作为一名应届生的时候我对职场也没有清醒的认识,以为职场上的同事和学校的同学一样大家和和气气不争不抢的。但是,正是抱着这样的心态我入职了互联网之后的短短一年时间内,才深刻感受到了社会的毒打和职场真实的样貌。所以,我不知道在学校的应届生们有没有做好准备在互联网面对全方位的竞争,这种竞争不仅仅是技术,不仅仅是加班,更是向上管理和领导处理好关系。和国企、外企、体制内不一样,互联网的大多数公司是有强制末尾淘汰的,有些公司甚至连新人保护期都没有,那么你觉得你作为一个活蹦乱跳的应届生,这个名额是老油条扛呢还是你呢?


    另外,以前的人扎堆朝互联网冲是因为真的有财富自由的机会的,那时候啥app都没有,张小龙找几个应届生关小黑屋都能写出未来的国民级app微信。16年的字节也还是个小公司,那时候往互联网里面冲的话搞不好真的可以一年能够赚到别人一辈子赚不到的钱,所以去互联网真是一点儿问题都没有。你那时候不去互联网我都会拿着鞭子抽你,劝你上进一点儿!但是都2024年了,市场永远比个人知道一个方向的未来,还是那句话你想创业互联网都拉不到风投的年代,你还能奢望能够实现财富自由吗?


    The End

    其实作为一名程序员还是挺享受写有趣代码的过程的,也希望做一点儿东西能够被大家认可,所以我劝退互联网但是并不是劝退计算机。


    即使是Chatgpt大行其道的今天,我也不认为未来某一天机器能够真正意义上取代程序员,要取代也是从另外一个维度上取代,比如说根据需求直接生成机器码而不是生成代码的这种形式。虽然互联网是一片红海,但是像新的技术VR、物联网、工业软件、芯片和智能机器人等行业,在我们国家还是蕴含着无限机会的。但是,我并不认为去到我上面所说的这些行业工资收入上能够超过现在的互联网大厂给出的工资,我的意思真的有想法的人可以尝试在这些领域去找到自己的一席之地,尤其是在校学生。


    你去卷一个注定下山的行业无论它钱给多少都是毫无意义的,因为入职就可能就是你职业生涯的巅峰。相比起来,我觉得华子未来比这些靠着广告赚钱的公司都更有前景,因为是真的有一些核心技术在的。


    所以,选择一个还没有走过巅峰的行业,提前布局才是更有未来的职业选择。


    作者:浣熊say
    来源:juejin.cn/post/7327447632111419443
    收起阅读 »

    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
    收起阅读 »

    系统干崩了,只认代码不认人

    各位朋友听我一句劝,写代码提供方法给别人调用时,不管是内部系统调用,还是外部系统调用,还是被动触发调用(比如MQ消费、回调执行等),一定要加上必要的条件校验。千万别信某些同事说的这个条件肯定会传、肯定有值、肯定不为空等等。这不,临过年了我就被坑了一波,弄了个生...
    继续阅读 »

    各位朋友听我一句劝,写代码提供方法给别人调用时,不管是内部系统调用,还是外部系统调用,还是被动触发调用(比如MQ消费、回调执行等),一定要加上必要的条件校验。千万别信某些同事说的这个条件肯定会传、肯定有值、肯定不为空等等。这不,临过年了我就被坑了一波,弄了个生产事故,年终奖基本是凉了半截。


    为了保障系统的高可用和稳定,我发誓以后只认代码不认人。文末总结了几个小教训,希望对你有帮助。


    一、事发经过


    我的业务场景是:业务A有改动时,发送MQ,然后应用自身接受到MQ后,再组合一些数据写入到Elasticsearch。以下是事发经过:



    1. 收到一个业务A的异常告警,当时的告警如下:



    2. 咋一看觉得有点奇怪,怎么会是Redis异常呢?然后自己连了下Redis没有问题,又看了下Redis集群,一切正常。所以就放过了,以为是偶然出现的网络问题。

    3. 然后技术问题群里 客服 反馈有部分用户使用异常,我警觉性的感觉到是系统出问题了。赶紧打开了系统,确实有偶发性的问题。

    4. 于是我习惯性的看了几个核心部件:



      1. 网关情况、核心业务Pod的负载情况、用户中心Pod的负载情况。

      2. Mysql的情况:内存、CPU、慢SQL、死锁、连接数等。



    5. 果然发现了慢SQL和元数据锁时间过长的情况。找到了一张大表的全表查询,数据太大,执行太慢,从而导致元数据锁持续时间太长,最终数据库连接数快被耗尽。


    SELECT xxx,xxx,xxx,xxx FROM 一张大表


    1. 立马Kill掉几个慢会话之后,发现系统仍然没有完全恢复,为啥呢?现在数据库已经正常了,怎么还没完全恢复呢?又继续看了应用监控,发现用户中心的10个Pod里有2个Pod异常了,CPU和内存都爆了。难怪使用时出现偶发性的异常呢。于是赶紧重启Pod,先把应用恢复。

    2. 问题找到了,接下来就继续排查为什么用户中心的Pod挂掉了。从以下几个怀疑点开始分析:



      1. 同步数据到Elasticsearch的代码是不是有问题,怎么会出现连不上Redis的情况呢?

      2. 会不会是异常过多,导致发送异常告警消息的线程池队列满了,然后就OOM?

      3. 哪里会对那张业务A的大表做不带条件的全表查询呢?



    3. 继续排查怀疑点a,刚开始以为:是拿不到Redis链接,导致异常进到了线程池队列,然后队列撑爆,导致OOM了。按照这个设想,修改了代码,升级,继续观察,依旧出现同样的慢SQL 和 用户中心被干爆的情况。因为没有异常了,所以怀疑点b也可以被排除了。

    4. 此时基本可以肯定是怀疑点c了,是哪里调用了业务A的大表的全表查询,然后导致用户中心的内存过大,JVM来不及回收,然后直接干爆了CPU。同时也是因为全表数据太大,导致查询时的元数据锁时间过长造成了连接不能够及时释放,最终几乎被耗尽。

    5. 于是修改了查询业务A的大表必要校验条件,重新部署上线观察。最终定位出了问题。


    二、问题的原因


    因为在变更业务B表时,需要发送MQ消息( 同步业务A表的数据到ES),接受到MQ消息后,查询业务A表相关连的数据,然后同步数据到Elasticsearch。


    但是变更业务B表时,没有传业务A表需要的必要条件,同时我也没有校验必要条件,从而导致了对业务A的大表的全表扫描。因为:


    某些同事说,“这个条件肯定会传、肯定有值、肯定不为空...”,结果我真信了他!!!

    由于业务B表当时变更频繁,发出和消费的MQ消息较多,触发了更多的业务A的大表全表扫描,进而导致了更多的Mysql元数据锁时间过长,最终连接数消耗过多。


    同时每次都是把业务A的大表查询的结果返回到用户中心的内存中,从而触发了JVM垃圾回收,但是又回收不了,最终内存和CPU都被干爆了。


    至于Redis拿不到连接的异常也只是个烟雾弹,因为发送和消费的MQ事件太多,瞬时间有少部分线程确实拿不到Redis连接。


    最终我在消费MQ事件处的代码里增加了条件校验,同时也在查询业务A表处也增加了的必要条件校验,重新部署上线,问题解决。


    三、总结教训


    经过此事,我也总结了一些教训,与君共勉:



    1. 时刻警惕线上问题,一旦出现问题,千万不能放过,赶紧排查。不要再去怀疑网络抖动问题,大部分的问题,都跟网络无关。

    2. 业务大表自身要做好保护意识,查询处一定要增加必须条件校验。

    3. 消费MQ消息时,一定要做必要条件校验,不要相信任何信息来源。

    4. 千万别信某些同事说,“这个条件肯定会传、肯定有值、肯定不为空”等等。为了保障系统的高可用和稳定,咱们只认代码不认人

    5. 一般出现问题时的排查顺序:



      1. 数据库的CPU、死锁、慢SQL。

      2. 应用的网关和核心部件的CPU、内存、日志。



    6. 业务的可观测性和告警必不可少,而且必须要全面,这样才能更快的发现问题和解决问题。




    作者:不焦躁的程序员
    来源:juejin.cn/post/7331628641360248868
    收起阅读 »

    别再只用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
    收起阅读 »

    打工人回家过年:只想休息,讨厌拜年、走亲戚、被催婚

    本文来自公众号 成功同学 大家好,我是杨成功。 昨天楼下吃饭,听到一个女孩在打电话,声音很大,听起来很生气。 原因是父母让她过年回去的时候给亲戚带礼物,女孩不愿意,和父母吵起来了。 女孩说:“今年本来就没攒下钱,回家来回的车票就花了一大笔,给你们带礼物也花了...
    继续阅读 »

    本文来自公众号 成功同学



    大家好,我是杨成功。


    昨天楼下吃饭,听到一个女孩在打电话,声音很大,听起来很生气。


    原因是父母让她过年回去的时候给亲戚带礼物,女孩不愿意,和父母吵起来了。


    女孩说:“今年本来就没攒下钱,回家来回的车票就花了一大笔,给你们带礼物也花了不少,为啥非得给亲戚带礼物?你们别光考虑你们的面子,能不能考虑一下我,年后还要交房租...”


    听到这里,我心里一痛。


    作为一个资深北漂,我被戳中了。


    很多人以为呆在北上广的人光鲜亮丽,实际上也只是两点一线的打工人;看起来钱赚的不少,实际上开销大到离谱,一年到头剩不下多少。


    今年互联网裁员潮,一片一片地裁,搞的大家人心惶惶。好几个朋友上午还在开心地写代码,下午就被请到会议室喝茶。


    有些拿不到赔偿的伙伴年底还在跑仲裁,真的很不容易。


    如果连父母都不能理解的话,我实在不敢想象,这个女孩回家过年的压力有多大。


    前几天有一条热搜:为什么年轻人不愿意回家过年了?


    年轻人不愿意回家过年,很多父母的第一反应是不孝顺,白眼狼,在外面呆野了。


    哎,谁不想回家过年啊,不回去肯定是不开心,而且不是一点点不开心,是压力重重。


    可能父母认为,孩子回家过年就图个热闹,到七大姑八大姨家串门拜年,见一见亲戚朋友兄弟姐妹,喝酒吃肉聊天,好不开心。


    其实不是的,真不是。就拿我来说,我回家只想睡觉嗑瓜子看电视,不洗脸不洗头谁都不见,同学聚会我都不想去。除非是几个关系极好的发小,其他任何社交局都是负担。


    除了社交压力,还有经济压力。


    像开头说的那个女孩一样,回一趟家要花车票钱、礼物钱、亲戚孩子压岁钱、给老人钱。赚钱了还好,如果一年没赚钱,这些人情开销就是一笔负担。


    累了一整年,只想回家休息,好好过个年,结果还要看钱包。


    当然还有催婚压力。


    像我这个年纪,马上奔三的人,过年回家见个人就是“找对象了没”。我家人比较开明,最多开玩笑问一句,亲戚朋友问就是“明年”。


    但我知道很多朋友、尤其女性朋友,过年催婚会把人逼疯。


    有些父母的催婚极其致命:“快三十了还不结婚,过了三十谁要你?你不成家我都没脸出门;人家谁谁都二胎了,你到底想咋样?你对得起...”。


    现在是 2024 年啊,找对象的难度不比打工挣钱低。如果再和父母吵上一架,这个年过的还有啥意思。


    这一层层的压力,早把年轻人回家过年的热情打散了,过个年比上班还累。


    现在能理解为啥年轻人不回家过年了吗?


    对父母来说,如果孩子愿意回家过年,就别要求那么多了,人回来图个开心就好。


    如果孩子在读大学,回家后就是想享受一下。你就让他睡到自然醒,让他每天蓬头垢面打游戏看电视,反正呆不了几天。


    如果愿意出去走亲戚,那就带上,不愿意也别勉强。更不要动不动就要求上酒桌,给长辈敬个酒,还得提一个,真的很尴尬。


    如果孩子在上班,一年已经很累了,她回家可能只想休息。父母们管好自己的嘴,少催婚,少安排相亲,少要求这要求那。


    更不要说谁谁家孩子赚了多少钱,谁谁家都抱孙子了。这样大家都不舒服,开开心心过个年不好吗?


    可能会有父母认为:我不催她都不上心。


    想想上学的时候,天天盯着学习,不能上网,不能找对象,不能玩这玩那,结果考上985了吗?


    结婚这事催不得,终身大事,你不能随便拉一个就领证吧,现在又不是70年代。


    如果逼的太急,很可能孩子明年就不回来过年了,骂也没有用。


    社会压力大,年轻人不比上一代轻松。多一点体贴关照,少一点要求,开心过年。


    车上没网,有感而发,到此为止。


    祝各位假期快乐,新年快乐。


    作者:杨成功
    来源:juejin.cn/post/7332293353197748258
    收起阅读 »

    年会结束,立马辞职了!

    那是发生在多年前的一件事,当时我也是在那家公司做 Java 开发。公司很大,大到去了很长一段时间都感觉毫无存在感。 那年年会,作为技术部的我,依然被安排到一个比较边缘化的桌子,这么多年走来,早已经习惯了这样的安排。 可能只有我们做技术人的心里才会觉得“技术牛逼...
    继续阅读 »

    副本_最后一天__2024-02-06+18_22_58.jpeg


    那是发生在多年前的一件事,当时我也是在那家公司做 Java 开发。公司很大,大到去了很长一段时间都感觉毫无存在感。


    那年年会,作为技术部的我,依然被安排到一个比较边缘化的桌子,这么多年走来,早已经习惯了这样的安排。


    可能只有我们做技术人的心里才会觉得“技术牛逼,技术万岁!”,但在公司领导层看来,这技术研发部就是整个公司开销最大的一个部门,又不能直接产生效益,但开除了又不合适,还要靠他们干活呢,这真是一件即讽刺、又无奈的事儿啊。


    说回正题,那年公司所有人依旧是尴尬的、极不情愿的、又不得不碍于情面凑在一起,听完了所谓的又毫无意义的年终总结,然后又敷衍的敬完酒之后,才能装模作样的挥手告别亲爱的同事。


    我之所以,要等待年会的第二天才告诉我的顶头上司“我要离职”的主要原因是,年会的时候才给大家集中发年终奖。


    我也是领到钱之后就不装了,我摊牌了,第二天就找到了领导,告诉他,我要离职了。这个时候上司也知道你的心思,话已经收出来了,尤其是离职的事,大概率是劝不回来了,毕竟覆水难收。大家都是明白人,寒暄了几句之后,就签了离职的申请。


    工作就像谈对象,合不来也没必要勉强。那时候开发的行情还很好,出去面试 4 家公司,最少也能拿 3 个 Offer,所以跳槽基本都是裸跳,一副此地不留爷,自有留爷处的傲娇姿态。


    然而,年终奖是拿到手了,新工作也很快又着落了,薪资每次跳槽也能涨到自己满意的数,但干着干着发现,好像还是原来的配方,还是原来的味道,好像也不是理想中的工作嘛。


    于是,在周而复始的折腾中才发现,只要是给别人上班,永远不会有理想中的工作,因为上班的本质是你替别人办事,别人给你发薪水,工作从来都是简单的雇佣关系,那来的别人要为你的理想来买单嘛,这本来就不合理,只是想明白这点时,以是上班了十年之后(此处可见自己的笨拙)。


    理解了这点之后,我才发现,给任何公司上班的区别不会太大,无非是钱多钱少、活多活少、周围人好相处与否的细微差别,但碍于生计,又不得不苟延残喘的上下班,这可能是大部分打工人的真实感受和现状了。


    但即使这样,你依然会发现,你的岗位正在被新人所替代,你的选择也变的越来越少,你的挣钱能力也变的越来越弱,这可能就是所谓的“中年危机”吧。所以说“中年危机”这个词,不是那个行业的专属名称,而是所有行业共性,那要怎么解决呢?


    三个小小的建议:



    1. 尽量不要买房:不要和自己过不去,买房一时爽,还贷“火葬场”。我有一个朋友,一个月 2.1W 的房贷,生活中哪怕有一点点小小的变动,对于他来说都是不可承受之殇。“如履薄冰”也不过如此吧?

    2. 培养自己的第二职业:找到自己感兴趣点,并且它能帮你长久的带来经济收益最好,不求大富大贵,只要能够日常开支已经很不错了。任何时候有准备都比没准备要强很多。还有,在做之前,不要怕起步晚、进步慢,只要肯坚持,终会有收获。路虽远,行则将至;事虽难,做则必成。

    3. 提升自己主业的能力:任何时候,提升自己主业的能力,都是收益最大的投资,也是最明智的投资,当你看不清前进的道路时,当你感觉人生黯淡无光事,唯有干好自己目前本职的工作,才是最优的选择,这也能让你为以后的新计划积攒足够的能量。


    最后,愿新的一年里:奔赴热爱、享受自由,找到自己热爱的事,并为之努力。加油,XDM~


    作者:Java中文社群
    来源:juejin.cn/post/7332227724801753140
    收起阅读 »

    记录一次我们的PostgreSQL数据库被攻击了

    数据库所有表被删除了 这个程序员把我们的数据库表都删除了,然后新建了一个数据库redme_to_recover数据库 里面还有一张表,表里是让你支付,然后给你数据下载地址。 通过查看Docker里部署的PostgreSQL执行日志是没有操作记录的 根据数据...
    继续阅读 »

    数据库所有表被删除了


    微信图片_20240126160520.png


    这个程序员把我们的数据库表都删除了,然后新建了一个数据库redme_to_recover数据库


    里面还有一张表,表里是让你支付,然后给你数据下载地址。


    通过查看Docker里部署的PostgreSQL执行日志是没有操作记录的


    微信图片_20240126162925.png


    根据数据库的日志确定,1月24号13点数据库被重启了。


    25号的日志非常少,错误信息都是客户端连接失败,无法从客户端接收数据。(25号系统还是正常的)


    26号02时的日志就显示tdd表没了(这时候应该是所有表都没了)。


    中间没有删除表的操作日志,跟大佬请教了一下,确定应该是有人登录了我们的Linux系统。然后从Linux系统层面直接删除的表资源数据,没有通过PGSQL操作,没有删除操作记录。


    我对黑客攻击的数据库进行了修改密码,然后发现密码失效了,无论输入什么密码,都能正常登录数据库。


    我是怎么恢复的


    1、将原来的PG数据库镜像删除,重新修改了端口号和数据库密码然后启动数据库容器。


    docker ps -a 列出所有的Docker容器,包括正在运行和已经停止的容器。


    docker rm [容器id/容器名称] 删除PostgreSQL容器。


    docker run 启动一个新的容器。
    image.png


    2、将Linux账户登录密码修改。


    3、修改端口号和数据库配置密码后,重新打包我们的数据处理程序。


    4、修改Nacos里配置的接口服务程序的数据库连接配置。


    5、将表结构恢复,系统表和业务表结构,系统表包括账户角色等信息(幸亏我们同事有备份)


    6、丢失了历史业务数据


    image.png


    作者:悟空啊
    来源:juejin.cn/post/7328003589297291276
    收起阅读 »

    可视化 Java 项目

    有一定规模的 IT 公司,只要几年,必然存在大量的代码,比如腾讯,2019 年一年增加 12.9 亿行代码,现在只会更多。不管是对于公司,还是对于个人,怎么低成本的了解这些代码的对应业务,所提供的能力,都是非常有必要的! 今天,阿七就带大家破解这个难题,根据这...
    继续阅读 »

    有一定规模的 IT 公司,只要几年,必然存在大量的代码,比如腾讯,2019 年一年增加 12.9 亿行代码,现在只会更多。不管是对于公司,还是对于个人,怎么低成本的了解这些代码的对应业务,所提供的能力,都是非常有必要的!


    今天,阿七就带大家破解这个难题,根据这个文档,你能使用 AI 编程技术,根据包含 Java 完整代码的项目实现可视化下面三个方面的内容:



    • 模块和功能:应用内部的业务模块和功能,及相互间的关系,为用户提供应用的整体视图。

    • 类和接口:应用模块提供的业务能力以及对应的类和接口,以及接口对应业务流程语义化。

    • 方法实现语义化:方法实现逻辑的语义化和可视化;


    一、先秀一下成果


    一)Java 项目概览图


    根据一个 Java 项目,可以生成下面这样的项目整体概览图,对于不需要了解实现细节的产品、运营同学,直接看这个图,就能够了解这个 Java 项目在干什么、能提供什么能力。


    对于部分技术同学,不需要了解代码详情的,也可以直接看这个图即可。满足新入职同学对于接手不常变更项目的理解和全局业务的了解!


    PS:由于保密需要,所有的成果图仅为示例图。实际的图会更好看、更震撼,因为一个 Java 项目的功能模块可能很多,提供的能力可能很多。



    对于需要了解技术细节的同学,点击入口,能看到当前方法的流程图,快速了解当前方法提供的能力,具体的细节。还能迅速发现流程上可能存在的问题,快速纠正。


    二)具体方法流程图



    有了上面的两层可视化图表,不管是产品、技术、测试、运营以及小领导,都能快速的根据一个 Java 项目获取到他所需要的层级的信息,降低开发人员通过阅读代码梳理业务逻辑和代码逻辑的时间,尤其是新入职的同学。这个时间据统计,基本上在 25%-30%(百度、阿里等大公司调研数据更大,为 55%-60%),对于新同学,这个比例会更大!


    二、实现步骤


    一)整体概述图怎么生成?


    一个 Java 项目所有对外接口在做的事情,就是一个 Java 项目的核心业务。这个对外接口包括:HTTP 接口、Dubbo 接口、定时任务。


    1、获取一个 Java 项目所有对外接口


    1)通过 Trace 平台


    可以查询到一个 Java 项目所有对外的 HTTP 接口和 Dubbo 接口,通过注解可以查询一个 Java 项目所有定时任务。


    优点:



    • 数据准确,跑出来的数据,一定是还在用的接口;
      缺点:

    • 需要依赖 Trace 平台数据,部分公司可能没有 Trace 平台。


    2)通过 JavaParser 工具


    可以通过 JavaParser 工具,扫描整个 Java 项目代码。找到所有的对外入口。


    优点:



    • 不依赖 Trace 数据;
      缺点:

    • 可能不准确,因为有些接口已经不被使用了。


    2、获取对外接口的方法内容


    1)根据 HTTP 的接口 url 可以反解析出来这个 url 对应的方法的全路径。


    具体来说,在项目中获取 Spring 上下文,Spring 上下文中有一个 Bean 叫 RequestMappingHandlerMapping,这个 Bean 中提供了一个方法 getHandlerMethods,这个方法中保存了一个 Java 项目中所有的对外 HTTP 方法。


    这个方法返回一个 Map对象,key 是 HTTP 接口的 URL,value 就是这个 URL 对应方法的全路径名称。



    2)根据方法全路径,获取方法内容


    根据上面的全路径名,使用 Spoon 框架我们能拿到对应方法的方法体。



    fr.inria.gforge.spoon
    spoon-core


    我们让 ChatGPT 帮我们写代码,提示词:



    写一个 Java 方法,使用 Spoon 框架解析 Java 方法的完整内容
    其中入参是方法全路径名




    PS:这个代码一会还有用,我们往下递归的话,能拿到这个 Controller 方法调用的所有方法体。


    3、根据方法内容生成方法注释


    就和 GitHub Copilot 和百度 Comate 代码助手一样,GPT 可以根据代码生成方法注释,提示词:



    角色: 你是一个 Java 技术专家。

    任务: # 号开头的是一个 Java 方法。请你逐行阅读代码,然后为这个 Java 方法生成一句话注释。

    限制:不要超过 20 个字



    举个例子,我有个工具方法,使用 GPT 为他生成注释,如下:



    4、生成 Java 项目一句话描述



    角色: 你是一个 Java 技术专家。

    任务: --- 符号以上的是一个 Java 项目中所有对外方法的注释,请你逐行阅读这些注释,然后给这个 Java 项目生成一句话描述。

    限制: 结果不要超过两句话。



    这个利用的是 GPT 的总结概要的能力,GPT 能总结论文、总结文章,他也能总结一段描述 Java 项目的文字。这样就能获取对于一个 Java 项目的一句话描述,也就是项目概览图的第一层。


    5、总结:生成项目概览图


    我们要求 GPT 根据 Java 项目的一句话描述,和所有对完方法的方法注释,生成思维导图数据。为了项目概览图的层级更可读、更清晰,我们可以要求 GPT 根据方法注释的相似性进行分类,形成项目概览图的第二层。第三层就是所有项目中对外方法的注释。


    生成思维导图,可以让 GPT 根据结构内容生成 puml 格式的思维导图数据,我们把 puml 格式的数据存储为 puml 文件,然后使用 xmind 或者在线画图工具 processOn 打开就能看到完整的思维导图。


    参考提示词如下:



    应用代码:appCodeValue

    项目描述:appCodeDescValue

    项目描述:appCodeDescValue

    方法描述:methodDescListValue

    角色:你是一个有多年经验的 Java 技术专家,在集成 Java 项目方面有丰富的经验。

    任务:根据 Java 项目中所有公共接口的描述信息生成思维导图。

    要求:思维导图只有四个层级。

    详细要求:思维导图的中心主题是 appCodeValue,第一层分支是 appCodeDescValue;第二层分支是公共接口的分类;下层分支是每个分类下方法的描述信息。

    返回正确格式的 opml 思维导图 xml 数据,并且内容是中文。



    二)流程图怎么生成?


    1、获取递归代码


    直接问 GPT,让 GPT 改造上面的获取方法体的方法。


    prompt;



    {获取方法体的方法}

    上面的 Java 代码是使用 Spoon 框架解析 Java 方法的完整内容
    其中入参是方法全路径名

    任务:现在要求你改造这个方法,除了打印当前方法的完整内容,还要求递归打印所有调用方法的方法体内容,包含被调用方法调用的方法





    这样,我们能获取到一个 controller 方法所有递归调用的方法,每个方法生成自己的流程图,最后通过流程图嵌套的形式进行展示。


    比如这个例子,当前能看到的是当前方法的流程图,带 + 号的内容,是当前方法调用方法的流程图。这样方便我们按照自己需要的深度去了解当前方法的具体实现流程!


    2、无效代码剪枝


    按照上面生成的流程图可能分支很多,还有一些无效的信息,影响用户判断,我们可以通过删除一些业务无关代码的方法,精简流程图。


    比如,我们可以删除日志、监控等与业务逻辑无关的代码,删除没有调用的代码(现在市面上有些这种技术方案,可以检测当前项目中没有被实际调用的代码)。


    3、生成流程图


    先让 GPT 根据代码生成结构化的 Json 数据。



    给你一段 Java 代码,请你使用 spoon 输出结构化的 Json 数据。要求:请你直接输出结构的 json 结果数据,不需要过程代码



    然后,可以让 GPT 根据 Json 数据生成流程图数据,使用流程图工具打开即可。



    给你一段 Spoon 结构化 Java 代码的 Json 数据,整理对应 Java 代码的意思,生成一个流程图数据,流程图使用 PlantUML。现在请输出能直接绘制 PlantUML 图的数据




    三、改进方案


    我们可以从下面几个方面改进这个项目,从而实现真正落地,解决实际公司需求:



    1. 获取代码,修改为从 gitlab 等代码仓库直接拉取,这样使用的时候不需要将工具包导入到具体的 Java 项目中。

    2. 优化生图,提前生成全量图标,通过浏览器的形式进行访问。

    3. 增加图表内容手动校正功能,生成不准确的,支持开发人员手动调整。

    4. 增加检索功能,可以按照自然语言进行检索。

    5. 把项目中的方法和类信息存起来,生成更准确的图标。

    6. 根据完整项目代码,反向生成项目概要图,可能能得到更准确的概要图。

    7. 递归方法流程图,可以使用流程图嵌套,如下进行展示。



    四、总结


    AI 在编程领域,除了大厂都在卷的代码助手,结合自己公司还有很多可探索的地方,比如本文说的可视化 Java 项目,还可以通过分析日志,进行异常、故障的根因分析,做到快速定位问题,帮助快速解决问题,减少影响。


    如果故障根因分析这个工具做出来了,阿里云的 P0 故障,滴滴的 P0 故障,还有很多大中小厂的故障,是不是能更快恢复?减少声誉、金钱损失?


    就说,项目可视化这个需求,据我了解的内部消息,有些互联网中大厂已经在使用这个方式进行落地了。另外,我陪伴群里也有同学接触到了类似不少甲方的类似的强需求,如果想深入这块技术的同学,不管是进互联网大厂还是做自己的副业产品都是不错的方向!


    作者:伍六七AI编程
    来源:juejin.cn/post/7311652298227990563
    收起阅读 »

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

    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
    收起阅读 »

    整理下最近做的产品里 比较典型的代码规范问题

    前言 最近负责了一个产品的前端代码Code Review的工作,90%代码是从之前做过的一个项目merge过来的,由于当时开发周期紧张,没有做好足够的Code Review流程,导致代码质量很差,而产品的代码质量要求就很高。 前端开发30个左右,技术经验高的1...
    继续阅读 »

    前言


    最近负责了一个产品的前端代码Code Review的工作,90%代码是从之前做过的一个项目merge过来的,由于当时开发周期紧张,没有做好足够的Code Review流程,导致代码质量很差,而产品的代码质量要求就很高。


    前端开发30个左右,技术经验高的10年左右,低的2-3年,经过几轮的code review,整理几个比较常见,比较典型的例子,简单的总结下。


    ESLint


    首先,可以引入 ESLint 静态代码检测工具,它可以保证高质量的代码,尽量减少和提早发现一些错误。同时也支持IDE自动检查提示。


    具体可以参考之前的文章:
    ESLint配合VSCode 统一团队前端代码规范


    IDE Format


    当然也离不开Code Format格式化,需要配置一套固定的Format格式,保证团队内所有代码格式化统一。


    我不太喜欢 Prettier 的换行机制,弄得大片大片的换行,可读性也差。团队大多用的VSCode,所以就用VSCode内置的Format功能,再加下可配置项,这样只要用VSCode开发,就会默认使用统一的代码格式化。


    具体可以参考之前的文章:只用VSCode自带的Format功能,满足可配置的代码格式化需求


    代码规范及习惯


    下面介绍一些团队内经常遇到的代码规范、质量问题,还有一些很不好的开发习惯。


    大小写



    1. 常量名:都大写或首字母大写;

    2. 变量:首字母小写,驼峰;

    3. dom id:全小写

    4. class name:全小写

    5. React router path:全小写,路由跳转的url一样。

    6. React 组件名(类\函数):大写开头,驼峰,尽量与文件名一致(除了index.jsx)


    catch


    这里一般是指请求后台api的catch,而且common已经封装好了fetch方法,并处理了公共异常,比如根据status提示不一样的提示语弹框:



    1. 由于common有封装,大多情况下不需要业务加catch处理;

    2. 如果有catch,必须要求throw,原因:

      1. throw会让程序中断,就是说不会再继续执行后续代码;

      2. F12 console里会打浏览器默认的error log,非常必要。



    3. 如果加了catch或finally,一定要测下程序走到这里的case,并想想是否有必要加。


    减少非必要的可选链操作符 (?.)


    产品里经常用到的操作符,用的很无脑,经常遇到这种代码:


    // 1
    const a = obj?.a?.b?.c?.d;

    // 2
    <div>{this.state?.name}</div>

    // 3
    const arr = list.filter(item=> ...);
    if (arr?.length) { ... }

    // 4
    if(item.a) { ... }
    fn(item?.b);

    // 5
    if (item) {
    fn(item?.b);
    }


    1. 如果变量或属性不能是空,不要加问号;

    2. 假如一个后台返回的值,不能是空,空就是bug了,这个时候前台加了问号,如果真有bug,就不容易发现了(反之会直接console抛错,很容易发现)

    3. 理解其原理用法,想想如果真是空,对后续是否有影响?这个值是否可能是空?

    4. 不要盲目加,有个点儿就加问号。

    5. 另一个目的:增加代码可读性,维护性。


    common 控件属性


    在使用common或第三方控件时:



    1. 一定要理解每个属性的作用,以及默认值;

    2. 不必须要设置的属性,不要设置;

    3. 如果属性有默认值,而且你用到的也是默认值,有些情况是不要设置的;

    4. 目的:方便维护,增加可读性。


    sessionStorage 和 localStorage


    思考两个问题:



    1. 是否真的了解两者的区别以及作用?

    2. 你是否真的需要它们?


    async await


    这里对于新手,会有很多不正确的用法,但代码运行没问题,只是用法不规范:



    1. 使用之前,一定要弄懂async await是做啥用的,不要滥用、乱用。

    2. 很多地方是不需要用的。

    3. 下面举例几个错误用法:


    fn = async () => {
    // 整个方法内部都没有用到await
    }

    fn = async () => {
    return await request(); // 不需要加async await
    }

    fn = async () => {
    const result = await request();
    return Promise.resolve(result); // 可以直接return
    }

    深拷贝



    1. 例如:JSON.parse(JSON.stringify(obj\array))

    2. 有些开发会用的很频繁,很无脑,有很多情况下,浅拷贝就可以满足、或者根本不需要拷贝的情况下就使用了,造成了很多额外开销。

    3. 需要理解 引用类型、浅拷贝、深拷贝 三个概念。


    React Hooks


    这里指React官方提供的Hooks,比如 useEffect useCallback useMemo memo 这几个“常用”的。


    发现业务中使用的很频繁,这里简单说下我的理解:



    • useEffect:注意第二个参数 deps,有些情况下,不是所有用到的参数都加到 deps里,会导致bug。

    • useCallbackmemo:大多数地方都是不需要的使用的(90%以上)。

    • useMemo:复杂逻辑可以用,其它情况不需要。

    • 以上,如果用的不对,反而会导致 业务bug负优化,甚至 反向优化

    • 这里说的比较浅,总结一个大致结论,详细说明网上很多。


    如果提升代码经验和意识


    简单总结几点:



    1. 写代码时要多问、多想、多调,不要功能好事了就完事了。

    2. 多看别人写的代码,比如团队内级别高的开发、网上大佬写的、第三方源码。

    3. 多review自己写过的代码,并优化。


    总结


    本文写的比较杂、也都比较浅,因为涉及到的知识点、经验太多了,不是三言两语就能说明白,详细的重要的点,也会在后续文章中详细讲解。


    作者:Mark大熊
    来源:juejin.cn/post/7235109911780311101
    收起阅读 »

    文档都写不好,当个屁的架构师!

    大家好,我是冰河~~ 最近有很多小伙伴,也不乏身边的一些同事问我:哎,架构师为什么要写这么多文档啊?有啥用呢?不能跟开发一样多写写代码吗?天天写文档,又感觉自己的文档写不好,有什么写文档的技巧吗? 今天也正好看到一篇文章,就给大家统一回复下这个问题。 软件设计...
    继续阅读 »

    大家好,我是冰河~~


    最近有很多小伙伴,也不乏身边的一些同事问我:哎,架构师为什么要写这么多文档啊?有啥用呢?不能跟开发一样多写写代码吗?天天写文档,又感觉自己的文档写不好,有什么写文档的技巧吗?


    今天也正好看到一篇文章,就给大家统一回复下这个问题。


    软件设计文档就是架构师的主要工作成果,它需要阐释工作过程中的各种诉求,描绘软件的完整蓝图,而软件设计文档的主要组成部分就是软件模型。


    软件设计过程可以拆分成 需求分析、概要设计和详细设计 三个阶段。


    在需求分析阶段,主要是通过用例图来描述系统的功能与使用场景;对于关键的业务流程,可以通过活动图描述;如果在需求阶段就提出要和现有的某些子系统整合,那么可以通过时序图描述新系统和原来的子系统的调用关系;可以通过简化的类图进行领域模型抽象,并描述核心领域对象之间的关系;如果某些对象内部会有复杂的状态变化,比如用户、订单这些,可以用状态图进行描述。


    在概要设计阶段,通过部署图描述系统最终的物理蓝图;通过组件图以及组件时序图设计软件主要模块及其关系;还可以通过组件活动图描述组件间的流程逻辑。


    在详细设计阶段,主要输出的就是类图和类的时序图,指导最终的代码开发,如果某个类方法内部有比较复杂的逻辑,那么可以将这个方法的逻辑用活动图进行描述。


    我们在每个设计阶段使用几种UML模型对领域或者系统进行建模,然后将这些模型配上必要的文字说明写入到文档中,就可以构成一篇软件设计文档了。


    由于时间关系,今天就跟大家聊到这里,后续给大家分享系统写架构文档的方法论。


    好了,今天就到这儿吧,我是冰河,我们下期见~~


    作者:冰_河
    来源:juejin.cn/post/7330835892276838441
    收起阅读 »

    简单一招竟把nginx服务器性能提升50倍

    需求背景 接到重点业务需求要分轮次展示数据,预估最高承接 9w 的 QPS,作为后端工程师下意识的就是把接口写好,分级缓存、机器扩容、线程拉满等等一系列连招准备,再因为数据更新频次两只手都数得过来,我们采取了最稳妥的处理方式,直接生成静态文件拿 CDN 抗量 ...
    继续阅读 »

    需求背景


    接到重点业务需求要分轮次展示数据,预估最高承接 9w 的 QPS,作为后端工程师下意识的就是把接口写好,分级缓存、机器扩容、线程拉满等等一系列连招准备,再因为数据更新频次两只手都数得过来,我们采取了最稳妥的处理方式,直接生成静态文件拿 CDN 抗量


    架构流程大致如下所示:



    数据更新后会重新生成新一轮次的文件,刷新 CDN 的时候会触发大量回源请求,应用服务器极端情况得 hold 住这 9w 的 QPS


    第一次压测


    双机房一共 40 台 4C 的机器,25KB 数据文件,5w 的 QPS 直接把 CPU 打到 90%


    这明显不符合业务需求啊,咋办?先无脑加机器试试呗


    就在这时测试同学反馈压测的数据不对,最后一轮文件最大会有 125KB,雪上加霜


    于是乎文件替换,机器数量整体翻一倍扩到 80 台,服务端 CPU 依然是瓶颈,QPS 加不上去了



    到底是哪里在消耗 CPU 资源呢,整体架构已经简单到不能再简单了


    这时候我们注意到为了节省网络带宽 nginx 开启了 gzip 压缩,是不是这小子搞的鬼


    server
    {
    listen 80;

    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain application/css text/css application/xml text/javascript application/javascript application/x-javascript;

    ......
    }



    第二次压测


    为了验证这个猜想,我们把 nginx 中的 gzip 压缩率从 6 调成 2,以减少 CPU 的计算量



    gzip_comp_level 2;



    这轮压下来 CPU 还是很快被打满,但 QPS 勉强能达到 9w,坐实了确实是 gzip 在耗 CPU



    nginx 作为家喻户晓的 web 服务器,以高性能高并发著称,区区一个静态数据文件就把应用服务器压的这么高,一定是哪里不对


    第三次压测


    明确了 gzip 在耗 CPU 之后我们潜下心来查阅了相关资料,发现了一丝进展


    html/css/js 等静态文件通常包含大量空格、标签等重复字符,重复出现的部分使用「距离加长度」表达可以减少字符数,进而大幅降低带宽,这就是 gzip 无损压缩的基本原理


    作为一种端到端的压缩技术,gzip 约定文件在服务端压缩完成,传输中保持不变,直到抵达客户端。这不妥妥的理论依据嘛~


    nginx 中的 gzip 压缩分为动态压缩和静态压缩两种


    •动态压缩


    服务器给客户端返回响应时,消耗自身的资源进行实时压缩,保证客户端拿到 gzip 格式的文件


    这个模块是默认编译的,详情可以查看 nginx.org/en/docs/htt…


    •静态压缩


    直接将预先压缩过的 .gz 文件返回给客户端,不再实时压缩文件,如果找不到 .gz 文件,会使用对应的原始文件


    这个模块需要单独编译,详情可以查看 nginx.org/en/docs/htt…


    如果开启了 gzip_static always,而且客户端不支持 gzip,还可以在服务端加装 gunzip 来帮助客户端解压,这里我们就不需要了


    查了一下 jdos 自带的 nginx 已经编译了 ngx_http_gzip_static_module,省去了重新编译的麻烦事



    接下来通过 GZIPOutputStream 在本地额外生成一个 .gz 的文件,nginx 配置上静态压缩再来一次



    gzip_static on;




    面对 9w 的QPS,40 台机器只用了 7% 的 CPU 使用率完美扛下


    为了探底继续加压,应用服务器 CPU 增长缓慢,直到网络流出速率被拉到了 89MB/s,担心影响宿主机其他容器停止压力,此时 QPS 已经来到 27w


    qps 5w->27w 提升 5 倍,CPU 90%->7% 降低 10 倍,整体性能翻了 50 倍不止,这回舒服了~


    写在最后


    经过一连串的分析实践,似乎静态压缩存在“压倒性”优势,那什么场景适合动态压缩,什么场景适合静态压缩呢?一番探讨后得出以下结论



    纯静态不会变化的文件适合静态压缩,提前使用gzip压缩好避免CPU和带宽的浪费。动态压缩适合API接口返回给前端数据这种动态的场景,数据会发生变化,这时候就需要nginx根据返回内容动态压缩,以节省服务器带宽



    作为一名后端工程师,nginx 是我们的老相识了,抬头不见低头见。日常工作中配一配转发规则,查一查 header 设置,基本都是把 nginx 作为反向代理使用。这次是直接访问静态资源,调整过程的一系列优化加深了我们对 gzip 的动态压缩和静态压缩的基本认识,这在 NG 老炮儿眼里显得微不足道,但对于我们来说却是一次难得的技能拓展机会


    在之前的职业生涯里,我们一直聚焦于业务架构设计与开发,对性能的优化似乎已经形成思维惯性。面对大数据量长事务请求,减少循环变批量,增大并发,增加缓存,实在不行走异步任务解决,一般瓶颈都出现在 I/O 层面,毕竟磁盘慢嘛,减少与数据库的交互次数往往就有效果,其他大概率不是问题。这回有点儿不一样,CPU 被打起来的原因就是出现了大量数据计算,在高并发请求前,任何一个环节都可能产生性能问题


    作者:京东零售 闫创


    来源:京东云开发者社区 转载请注明来源


    作者:京东云开发者
    来源:juejin.cn/post/7328766815101206547
    收起阅读 »

    多租户架构设计思考

    共享数据库,共享表 描述 所有租户的数据都在同一个数据库表内,以租户字段:tenant_id来区分。 优点 成本低,实现方式简单,适合中小型项目的快速实现。 缺点 数据隔离性差,某一个租户的数据量大的时候,会影响其他租户数据的操作效率。 需要在表上增加租户字...
    继续阅读 »

    共享数据库,共享表


    描述


    所有租户的数据都在同一个数据库表内,以租户字段:tenant_id来区分。


    优点


    成本低,实现方式简单,适合中小型项目的快速实现。


    缺点



    • 数据隔离性差,某一个租户的数据量大的时候,会影响其他租户数据的操作效率。

    • 需要在表上增加租户字段,对系统有一定的侵入性。

    • 数据备份困难,因为所有租户的数据混合在一起,所以针对某个租户数据的备份、恢复会比较麻烦。


    实现方式


    **方式一:**编写Mybatis拦截器,拦截增删改查操作,动态的增加租户条件,如:


    SELECT * FROM sys_user;

    修改成:


    SELECTG * FROM sys_user WHERE tenant_id = 100;

    这种方案并不靠谱,因为动态修改SQL语句不是一个好的处理方式,如果SQL解析没有做好,或者出现复杂SQL,那么很容易产生bug。


    **方式二:**编写Mybatis拦截器,拦截增删改查操作,判断是否有租户条件,如:


    SELECT * FROM sys_user WHERE id=1;

    使用jsqlparser工具解析SQL,判断出该SQL语句没有tenant_id的条件,那么抛出异常,不允许执行。


    这种方案比较稳妥,因为只做判断不做修改。


    查询操作的优先级不高,如果不在乎数据敏感,可以不拦截。


    要注意的是修改操作,稍不注意容易被某一个租户影响其他租户的数据。


    共享数据库,独立一张表


    描述


    所有租户的数据都在同一个数据库中,但是各自有一个独立的表,如:


    # 1号租户的用户表
    sys_user_1

    # 2号租户的用户表
    sys_user_2

    ...

    优点


    成本低,数据隔离性比共享表稍好,并且不用新增租户字段,对系统没有侵入性。


    缺点



    • 数据隔离性虽然比共享表好了些,但是因为仍在同一数据库下,所以某一个租户影响其他租户的数据操作效率问题依然存在。

    • 数据备份困难的问题依然存在。


    实现方式


    **方式一:**编写Mybatis拦截器,拦截增删改查操作,动态的修改表名称,如:


    SELECT * FROM sys_user;

    修改成:


    SELECT * FROM sys_user_1;

    同样的,这种动态修改SQL语句的方式并不推荐,所以我们有另一种方式。


    **方式二:**将表名作为参数传入


    本来在Mapper.xml中,查询语句是这样的:


    SELECT * FROM sys_user WHERE id = #{userId};

    现在改成:


    SELECT * FROM #{tableName} WHERE id = #{userId};

    这样可以避免动态修改SQL语句操作。


    独立数据库


    描述


    每个租户都单独分配一个数据库,数据完全独立,如:


    database_1;
    database_2;
    ...

    优点



    • 数据隔离性最好,不需要添加租户id字段,租户之间不会被彼此影响。

    • 便于数据备份和恢复。

    • 便于扩展。


    缺点



    • 经费成本高,尤其在有多个租户的情况下。

    • 运维成本高。


    结论


    一般来说,当数据量不高的时候,选择共享数据库共享表的方式,表内加个租户id字段做区分,数据量或者用户量多起来,就可以直接升级到独立数据库的方式,因为独立表的方式处理起来是有些麻烦的,倒不如加个字段来的方便。


    作者:失败的面
    来源:juejin.cn/post/7282953307529953291
    收起阅读 »

    你要写过年,就不能只写万家灯火与团圆

    昨天晚上八点过,下了地铁,走到出租屋的楼下,原本热闹的小区,也变得冷冷清清的,来到我经常吃肉沫粉的小店门口,小雨缠绵,透过那层破旧的透明胶纸,看到老板和老板娘在收拾行李。 我轻轻撩开胶纸,问老板还有吃的吗,他笑着说:兄弟,刚好还有最后一份,你来得正巧,卖给你后...
    继续阅读 »

    昨天晚上八点过,下了地铁,走到出租屋的楼下,原本热闹的小区,也变得冷冷清清的,来到我经常吃肉沫粉的小店门口,小雨缠绵,透过那层破旧的透明胶纸,看到老板和老板娘在收拾行李。


    我轻轻撩开胶纸,问老板还有吃的吗,他笑着说:兄弟,刚好还有最后一份,你来得正巧,卖给你后我们就该回家了。


    可以看出他们心中是很开心的,两个孩子也在不停叨唠:回家了,回家了。


    老板做好粉给我端来,可能是最后一份,料加得特别足,我吃了一半就饱了,然后擦了擦嘴,给老板说了声新年快乐,老板和老板娘笑眯眯回了我一句:兄弟,新年快乐,明年见!


    于是我就上楼了,往日上楼都有不少人在等电梯,今日五个电梯门都停在一楼,大家都回去过年了吧,再过两天,这个城市可能会更加冷清。


    回到出租屋后,坐在桌子前,回想很多事情,我觉得可以动笔了!


    一.“不想回家过年”的人


    下班后,打了个滴滴去20公里以外的地方办点事,一上车和师傅就开始聊了起来,师傅问我还不回家过年吗?


    图片


    我对他说:还有好几天呢,除夕再回去。


    我反问他:只有几天过年了,为啥还不回去过年呢?


    他说道:平时跑车都没啥生意,过年生意好一点,多跑几天,和你一样也是除夕当天才回去。


    他说好几天没遇到我这么大的单了,60块钱,平时都是10块,8块的,一天也就能跑两百块钱左右,最近一天能跑500左右。


    我们在车上一直聊,聊他的年轻时进厂打工过年回家的时光,他14岁时就去浙江进厂,每年回家过年也就能带几千块钱过年,有一年从义乌坐了三天的大巴车回来,路上堵车,事故,经历“九九八十一难”才到家。


    回来过年打了几天的麻将,几千块钱全部输完后,给家里要了几百块钱后,又灰溜溜地出门进厂了。这样的日子反反复复了六七年,一分钱都没存到。


    后面觉得这样不行,于是家里给他说了个媳妇,还是卖了一块土地,才勉强把彩礼凑齐了。


    成家后有了孩子,压力大了,于是就在家乡的县城干工地,一干就是十几年,直到35岁的时候,存了十几万块钱,2019年在县城首付买了一套房子。


    没过多久,疫情就来了,他说没活干,收入彻底断了,但是房贷没有断,于是刷信用卡,借钱来还房贷,后面疫情稍微放开后,就想办法搞了一个二手车来跑,那会跑十几二十公里都很难拉到一个人,一天勉强能跑八九十块钱,勉强能够一家人吃饭,但是房贷还是要想其它的办法。


    他说为啥不敢提前回去过年,就是因为还要还房贷,所以不敢松一口气。


    聊了大概一个小时,一路堵车,我到站了,下车后他递了一支烟给我,说道:兄弟,很久没有和别人聊这么久了,新年快乐。


    我也对他说了一句新年快乐。


    我给了一个好评,并且打赏了十块钱。


    是啊,我何尝不是很久没有和别人聊这么久了呢,我们都在自己该走的路上马不停蹄奔跑,一切还不是为了生活!


    没有谁不想提前回家过年,没有谁不想回家看看父母,没有谁不想回家去感受热乎乎的饭菜!


    可是回到了家,生活又该怎么继续继续呢?


    二.想回家过年却回不了的人


    上个月从广州回来,广州南站已经是人山人海了,那会朋友说抢回广西的票已经很难抢了,都不知道还能不能回去过年。


    图片


    这两天和在广东打工的朋友聊了下,他说根本抢不到票,不知道还能不能回家。


    我打开了手机购票软件,全是暂无余票,建议抢票,抢到票的人是幸运的,但是抢不到票的人,此刻心中又是何种感受。


    因为只有火车,高铁,大巴是中国大部分人能消费得起的,大部分根本不舍得买一张机票。


    和滴滴师傅聊天时,他说他的哥哥和嫂子现在还在义乌进厂,由于抢不到火车票和高铁票,他们看了看机票,需要1400元,两个人就需要差不多3000多,这已经顶得上他们一个人一个月的工资了。


    所以想了想还是不回了,打了几千块钱给家里的老人和孩子,让他们自己过年了。


    可能下一次见到家中的老父母和孩子又是下一年了,不知道下次回家的时候,孩子看他们的眼神是不是会有一丝陌生,老人的眼神是不是又多了几分期待。


    还记得在我小时候,父母在外省打工,过年的时候,他们背着很大的牛仔背包,里面有被子,衣服,只要能带回来的东西都带回来了,那时候父母还算年轻,但是回到家的时候我却感觉有点陌生。


    因为长时间不见他们,当见到他们的时候,虽然心里很高兴,但是却一时表现不出来,反而会流下泪水。


    我在农村看了太多这样的场景,爸爸妈妈在外打工,过年回来过年,孩子在门前呆呆坐着,叫了他几声都没答应,最后大哭了起来。


    是啊,有谁能在几年时间里没见到自己的爸爸妈妈,当见到的时候能不大哭呢?


    不过这就是中国大部分农村的实际情况,父母因为要赚钱回来修房子,供孩子上学,所以很多父母过年不舍得花费太多路费回来。


    除了交通工具和回家路费的限制,还有很多因为工作不能回家过年的人,他们很想回来,但是却不能回来,他们有工人,有白领,有交警,有驻守边疆的战士......


    此刻,不管你过年在厂区里面加班,在写字楼工作,在路上指挥车辆,还是在祖国的边疆驻守。


    我都对你们表示尊敬,祝你们新年快乐!


    三.不敢回家过年的人


    总有人有家不敢回。


    图片


    可能网上的过年文案都是阖家欢乐,大团圆,但是在社会的深处,总有很多人不敢回家,或者不好意思回家。


    远在深圳的朋友,和我聊天说不敢回家过年了,钱是钱没赚到,女朋友是女朋友没找到,回去面对逐渐变老的父母,心中不忍。


    这几年赚钱是真的特别难,朋友在深圳搞销售,因为销售很不稳定,并且是个苦活,他一个月也就能赚几千块钱,除了花销,还要还债,就留不下几个钱。


    后面觉得送外卖可能能多赚一点,但是送了不久,和别人电车又撞了,还受了伤,于是只能放弃,直接去找了一个工厂进。


    我们大多数人总是看到大城市的繁华,以为都能赚到钱。但是大城市里面,大部分人都是拿着最微薄的工资,干着最累的活,最后还存不了几个钱。


    可能你觉得在几十层的写字楼里面工作的白领都是年薪几十几百万,但是实际情况是,大多数都是几千块,每天通勤都是按小时来计算,加班后回到出租屋已经累趴,一趟就睡。


    但是一年下来却赚不了几个钱,在亲人朋友的眼中以为你在大城市混得不错,但是苦只有自己知道。


    所以带着这种压力和心理负担,很多人不敢回家。


    还有一些怕回去被催婚,被相亲,被攀比,所以索性直接留在打工的地方过年,因为觉得自己不甘随便找个人结婚,不想去和谁比这比那,索性选择一个人留下来。


    也许大年三十你看到了漫天的烟花,饭桌上丰盛的菜肴,但是总有人在没人看到的地方吃着泡面,烟花爆开的一瞬间,他的眼泪刚好掉下。


    我经历过这样的日子,我曾看到别人团圆而自己孤身一人而落泪,也曾看到万家灯火而自己在黑暗中哭泣。


    四.无家可回的人


    总有人想过年,但是却没有家回的人。


    图片


    在我还是学生的时候,有一个朋友过年不知道去哪里过,他常年都在外面打工,过年的时候回来,我们在一起喝酒,一起聊天,但是到最后,每个人都回家了,他独自一个人去酒店了。


    他父母在他小的时候就离婚了,并且父母都对他不管不顾,在他十几岁的时候就独自出门打工了,他已经没啥亲人了,所以回到家乡只是来找一个曾经的感觉。


    还记得前两年,除夕的前一天我们在一起玩耍,我叫他和我去我家一起过年,他拒绝了,后面被另外的两个朋友硬拉着去他们家过年。


    当时他的眼睛里面充满泪花,我从他的眼神里面看到了别人没有的坚强。


    是呀,可能在我们的世界里,过年是个再寻常不过的日子了,但是在他的世界里,过年却是一件无法奢求的事情。


    像我朋友这样情况的人还是比较多的。


    不过我的朋友,请你相信,你失去的终究会翻倍给你偿还,你得到的会加倍给你馈赠。


    ---------------


    行笔到此,心中百感交集。


    过年是中国人独有的传统,在这个日子里面,是团圆,是喜庆,是期待……


    按理这个日子应该用华丽的辞藻和温馨的言语来写。


    但是在自己经历了很多事,看到了很多现实场景的时候,我无法动笔去写空洞的句子。


    最后给“不想回家过年”,想回家过年却回不了,不敢回家过年,无家可回,无年可过的朋友们说一句,也给我自己说一句。


    这个世界总有一盏灯会为你亮着,总有一个眼神,为你等待着。


    新年快乐!


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

    一种好用的KV存储封装方案

    一、 概述 众所周知,用kotlin委托属性去封装KV存储库,可以优化数据的访问。 封装方法有多种,各有优劣。 通过反复实践,笔者摸索出一套比较好用的方案,借此文做个简单的分享。 代码已上传Github: github.com/BillyWei01/… 项目...
    继续阅读 »

    一、 概述


    众所周知,用kotlin委托属性去封装KV存储库,可以优化数据的访问。

    封装方法有多种,各有优劣。

    通过反复实践,笔者摸索出一套比较好用的方案,借此文做个简单的分享。


    代码已上传Github: github.com/BillyWei01/…

    项目中是基于SharePreferences封装的,但这套方案也适用于其他类型的KV存储框架。


    二、 封装方法


    此方案封装了两类委托:



    1. 基础类型

      基础类型包括 [boolean, int, float, long, double, String, Set<String>, Object] 等类型。

      其中,Set<String> 本可以通过 Object 类型囊括,

      但因为Set<String>是 SharePreferences 内置支持的类型,这里我们就直接内置支持了。

    2. 扩展key的基础类型

      基础类型的委托,定义属性时需传入常量的key,通过委托所访问到的是key对应的value

      而开发中有时候需要【常量+变量】的key,基础类型的委托无法实现。

      为此,方案中实现了一个 CombineKV 类。

      CombineKV通过组合[key+extKey]实现通过两级key来访问value的效果。

      此外,方案基于CombineKV封装了各种基础类型的委托,用于简化API,以及约束所访问的value的类型。


    2.1 委托实现


    基础类型BasicDelegate.kt

    扩展key的基础类型: ExtDelegate.kt


    这里举例一下基础类型中的Boolean类型的委托实现:


    class BooleanProperty(private val key: String, private val defValue: Boolean) :
    ReadWriteProperty<KVData, Boolean> {
    override fun getValue(thisRef: KVData, property: KProperty<*>): Boolean {
    return thisRef.kv.getBoolean(key, defValue)
    }

    override fun setValue(thisRef: KVData, property: KProperty<*>, value: Boolean) {
    thisRef.kv.putBoolean(key, value)
    }
    }

    class NullableBooleanProperty(private val key: String) :
    ReadWriteProperty<KVData, Boolean?> {
    override fun getValue(thisRef: KVData, property: KProperty<*>): Boolean? {
    return thisRef.kv.getBoolean(key)
    }

    override fun setValue(thisRef: KVData, property: KProperty<*>, value: Boolean?) {
    thisRef.kv.putBoolean(key, value)
    }
    }

    经典的 ReadWriteProperty 实现:

    分别重写 getValue 和 setValue 方法,方法中调用KV存储的读写API。

    由于kotlin区分了可空类型和非空类型,方案中也分别封装了可空和非空两种委托。


    2.2 基类定义


    实现了委托之后,我们将各种委托API封装到一个基类中:KVData


    abstract class KVData {
    // 存储接口
    abstract val kv: KVStore

    // 基础类型
    protected fun boolean(key: String, defValue: Boolean = false) = BooleanProperty(key, defValue)
    protected fun int(key: String, defValue: Int = 0) = IntProperty(key, defValue)
    protected fun float(key: String, defValue: Float = 0f) = FloatProperty(key, defValue)
    protected fun long(key: String, defValue: Long = 0L) = LongProperty(key, defValue)
    protected fun double(key: String, defValue: Double = 0.0) = DoubleProperty(key, defValue)
    protected fun string(key: String, defValue: String = "") = StringProperty(key, defValue)
    protected fun stringSet(key: String, defValue: Set<String> = emptySet()) = StringSetProperty(key, defValue)
    protected fun <T> obj(key: String, encoder: ObjectEncoder<T>, defValue: T) = ObjectProperty(key, encoder, defValue)

    // 可空的基础类型
    protected fun nullableBoolean(key: String) = NullableBooleanProperty(key)
    protected fun nullableInt(key: String) = NullableIntProperty(key)
    protected fun nullableFloat(key: String) = NullableFloatProperty(key)
    protected fun nullableLong(key: String) = NullableLongProperty(key)
    protected fun nullableDouble(key: String) = NullableDoubleProperty(key)
    protected fun nullableString(key: String) = NullableStringProperty(key)
    protected fun nullableStringSet(key: String) = NullableStringSetProperty(key)
    protected fun <T> nullableObj(key: String, encoder: NullableObjectEncoder<T>) = NullableObjectProperty(key, encoder)

    // 扩展key的基础类型
    protected fun extBoolean(key: String, defValue: Boolean = false) = ExtBooleanProperty(key, defValue)
    protected fun extInt(key: String, defValue: Int = 0) = ExtIntProperty(key, defValue)
    protected fun extFloat(key: String, defValue: Float = 0f) = ExtFloatProperty(key, defValue)
    protected fun extLong(key: String, defValue: Long = 0L) = ExtLongProperty(key, defValue)
    protected fun extDouble(key: String, defValue: Double = 0.0) = ExtDoubleProperty(key, defValue)
    protected fun extString(key: String, defValue: String = "") = ExtStringProperty(key, defValue)
    protected fun extStringSet(key: String, defValue: Set<String> = emptySet()) = ExtStringSetProperty(key, defValue)
    protected fun <T> extObj(key: String, encoder: ObjectEncoder<T>, defValue: T) = ExtObjectProperty(key, encoder, defValue)

    // 扩展key的可空的基础类型
    protected fun extNullableBoolean(key: String) = ExtNullableBooleanProperty(key)
    protected fun extNullableInt(key: String) = ExtNullableIntProperty(key)
    protected fun extNullableFloat(key: String) = ExtNullableFloatProperty(key)
    protected fun extNullableLong(key: String) = ExtNullableLongProperty(key)
    protected fun extNullableDouble(key: String) = ExtNullableDoubleProperty(key)
    protected fun extNullableString(key: String) = ExtNullableStringProperty(key)
    protected fun extNullableStringSet(key: String) = ExtNullableStringSetProperty(key)
    protected fun <T> extNullableObj(key: String, encoder: NullableObjectEncoder<T>) = ExtNullableObjectProperty(key, encoder)

    // CombineKV
    protected fun combineKV(key: String) = CombineKVProperty(key)
    }

    使用时,继承KVData,然后实现kv, 返回一个KVStore的实现类即可。


    举例,如果用SharedPreferences实现KVStore,可如下实现:


    class SpKV(name: String): KVStore {
    private val sp: SharedPreferences =
    AppContext.context.getSharedPreferences(name, Context.MODE_PRIVATE)
    private val editor: SharedPreferences.Editor = sp.edit()

    override fun putBoolean(key: String, value: Boolean?) {
    if (value == null) {
    editor.remove(key).apply()
    } else {
    editor.putBoolean(key, value).apply()
    }
    }

    override fun getBoolean(key: String): Boolean? {
    return if (sp.contains(key)) sp.getBoolean(key, false) else null
    }

    // ...... 其他类型
    }


    更多实现可参考: SpKV


    三、 使用方法


    object LocalSetting : KVData("local_setting") {
    override val kv: KVStore by lazy {
    SpKV(name)
    }
    // 是否开启开发者入口
    var enableDeveloper by boolean("enable_developer")

    // 用户ID
    var userId by long("user_id")

    // id -> name 的映射。
    val idToName by extNullableString("id_to_name")

    // 收藏
    val favorites by extStringSet("favorites")

    var gender by obj("gender", Gender.CONVERTER, Gender.UNKNOWN)
    }


    定义委托属性的方法很简单:



    • 和定义变量类似,需要声明变量名类型

    • 和变量声明不同,需要传入key

    • 如果要定义自定义类型,需要传入转换器(实现字符串和对象类型的转换),以及默认值


    基本类型的读写,和变量的读写一样。

    例如:


    fun test1(){
    // 写入
    LocalSetting.userId = 10001L
    LocalSetting.gender = Gender.FEMALE

    // 读取
    val uid = LocalSetting.userId
    val gender = LocalSetting.gender
    }

    读写扩展key的基本类型,则和Map的语法类似:


    fun test2() {
    if (LocalSetting.idToName[1] == null || LocalSetting.idToName[2] == null) {
    Log.d("TAG", "Put values to idToName")
    LocalSetting.idToName[1] = "Jonn"
    LocalSetting.idToName[2] = "Mary"
    } else {
    Log.d("TAG", "There are values in idToName")
    }
    Log.d("TAG", "idToName values: " +
    "1 -> ${LocalSetting.idToName[1]}, " +
    "2 -> ${LocalSetting.idToName[2]}"
    )
    }

    扩展key的基本类型,extKey是Any类型,也就是说,以上代码的[],可以传入任意类型的参数。


    四、数据隔离


    4.1 用户隔离


    不同环境(开发环境/测试环境),不同用户,最好数据实例是分开的,相互不干扰。

    比方说有 uid='001' 和 uid='002' 两个用户的数据,如果需要隔离两者的数据,有多种方法,例如:



    1. 拼接uid到key中。


      如果是在原始的SharePreferences的基础上,是比较好实现的,直接put(key+uid, value)即可;

      但是如果用委托属性定义,可以用上面定义的扩展key的类型。


    2. 拼接uid到文件名中。


      但是不同用户的数据糅合到一个文件中,对性能多少有些影响:



      • 在多用户的情况下,实例的数据膨胀;

      • 每次访问value, 都需要拼接uid到key上。


      因此,可以将不同用户的数据保存到不同的实例中。

      具体的做法,就是拼接uid到路径或者文件名上。



    基于此分析,我们定义两种类型的基类:



    • GlobalKV: 全局数据,切换环境和用户,不影响GlobalKV所访问的数据实例。

    • UserKV: 用户数据,需要同时区分 “服务器环境“ 和 ”用户ID“。


    open class GlobalKV(name: String) : KVData() {
    override val kv: KVStore by lazy {
    SpKV(name)
    }
    }

    abstract class UserKV(
    private val name: String,
    private val userId: Long
    ) : KVData() {
    override val kv: SpKV by lazy {
    // 拼接UID作为文件名
    val fileName = "${name}_${userId}_${AppContext.env.tag}"
    if (AppContext.debug) {
    SpKV(fileName)
    } else {
    // 如果是release包,可以对文件名做个md5,以便匿藏uid等信息
    SpKV(Utils.getMD5(fileName.toByteArray()))
    }
    }
    }

    UserKV实例:


    /**
    * 用户信息
    */

    class UserInfo(uid: Long) : UserKV("user_info", uid) {
    companion object {
    private val map = ArrayMap<Long, UserInfo>()

    // 返回当前用户的实例
    fun get(): UserInfo {
    return get(AppContext.uid)
    }

    // 根据uid返回对应的实例
    @Synchronized
    fun get(uid: Long): UserInfo {
    return map.getOrPut(uid) {
    UserInfo(uid)
    }
    }
    }

    var gender by intEnum("gender", Gender.CONVERTER)
    var isVip by boolean("is_vip")

    // ... 其他变量
    }

    UserKV的实例不能是单例(不同的uid对应不同的实例)。

    因此,可以定义companion对象,用来缓存实例,以及提供获取实例的API。


    保存和读取方法如下:

    先调用get()方法获取,然后其他用法就和前面描述的用法一样了。


    UserInfo.get().gender = Gender.FEMALE

    val gender = UserInfo.get().gender

    4.2 环境隔离


    有一类数据,需要区分环境,但是和用户无关。

    这种情况,可以用UserKV, 然后uid传0(或者其他的uid用不到的数值)。


    /**
    * 远程设置
    */

    object RemoteSetting : UserKV("remote_setting", 0L) {
    // 某项功能的AB测试分组
    val fun1ABTestGr0up by int("fun1_ab_test_group")

    // 服务端下发的配置项
    val setting by combineKV("setting")
    }

    五、小结


    通过属性委托封装KV存储的API,可使原来“类名 + 操作 + key”的方式,变更为“类名 + 属性”的方式,从而简化KV存储的使用。
    另外,这套方案也提到了保存不同用户数据到不同实例的演示。


    方案内容不多,但其中包含一些比较实用的技巧,希望对各位读者有所帮助。


    作者:呼啸长风
    来源:juejin.cn/post/7323449163420303370
    收起阅读 »

    java 实现后缀表达式

    一、概述 后缀表达式(也称为逆波兰表达式)是一种数学表达式的表示方法,其中操作符位于操作数的后面。这种表示法消除了括号,并且在计算机科学和计算中非常有用,因为它更容易计算和解析。 与中缀表达式(通常我们使用的数学表达式,例如"a * (b + c)")不同,后...
    继续阅读 »

    一、概述


    后缀表达式(也称为逆波兰表达式)是一种数学表达式的表示方法,其中操作符位于操作数的后面。这种表示法消除了括号,并且在计算机科学和计算中非常有用,因为它更容易计算和解析。


    与中缀表达式(通常我们使用的数学表达式,例如"a * (b + c)")不同,后缀表达式的运算符放在操作数之后,例如:“a b c + *”。后缀表达式的计算方法是从左到右遍历表达式,遇到操作数时将其压入栈,遇到操作符时从栈中弹出所需数量的操作数进行计算,然后将结果重新压入栈。这个过程一直持续到整个表达式处理完毕,最终栈中只剩下一个结果,即表达式的计算结果。


    后缀表达式具有以下优点:



    1. 不需要括号,因此消除了歧义。

    2. 更容易计算,因为遵循一定的计算顺序。

    3. 适用于计算机的堆栈操作,因此在编译器和计算器中经常使用。


    转换中缀表达式为后缀表达式需要使用算法,通常是栈数据结构。


    二、后缀表达式的运算顺序


    后缀表达式的运算顺序是从左到右遍历表达式,遇到操作数时将其压入栈,遇到操作符时从栈中弹出所需数量的操作数进行计算,然后将计算结果重新压入栈。这个过程一直持续到整个表达式处理完毕,最终栈中只剩下一个结果,即表达式的计算结果。


    后缀表达式的运算顺序是非常直观的,它遵循从左到右的顺序。当计算后缀表达式时,按照以下规则:



    1. 从左到右扫描后缀表达式中的每个元素(操作数或操作符)。

    2. 如果遇到操作数,将其推入栈。

    3. 如果遇到操作符,从栈中弹出所需数量的操作数进行计算,然后将计算结果推回栈中。

    4. 重复这个过程,直到遍历完整个后缀表达式。


    三、常规表达式转化为后缀表达式



    • 创建两个栈,一个用于操作符(操作符栈),另一个用于输出后缀表达式(输出栈)。

    • 从左到右遍历中缀表达式的每个元素。

    • 如果是操作数,将其添加到输出栈。

    • 如果是操作符:

    • 如果操作符栈为空,直接将该操作符推入操作符栈。

      否则,比较该操作符与操作符栈栈顶的操作符的优先级。如果当前操作符的优先级较高,将其推入操作符栈。

      如果当前操作符的优先级较低或相等,从操作符栈中弹出并添加到输出栈,然后重复比较直到可以推入操作符栈。

      如果遇到左括号"(“,直接推入操作符栈。

      如果遇到右括号”)“,将操作符栈中的操作符弹出并添加到输出栈,直到遇到匹配的左括号”("。

      最后,将操作符栈中的剩余操作符全部弹出并添加到输出栈。

      完成遍历后,输出栈中的内容就是中缀表达式转化为后缀表达式的结果。


    四、代码实现


    /**
    * 定义操作符的优先级
    */

    private Map<String, Integer> opList =
    Map.of("(",3,")",3,"*",2,"/",2,"+",1,"-",1);

    public List<String> getPostExp(List<String> source) {

    // 数字栈
    Stack<String> dataStack = new Stack<>();
    // 操作数栈
    Stack<String> opStack = new Stack<>();
    // 操作数集合
    for (int i = 0; i < source.size(); i++) {
    String d = source.get(i).trim();
    // 操作符的操作
    if (opList.containsKey(d)) {
    operHandler(d,opStack,dataStack);
    } else {
    // 操作数直接入栈
    dataStack.push(d);
    }
    }
    // 操作数栈中的数据,到压入到栈中
    while (!opStack.isEmpty()) {
    dataStack.push(opStack.pop());
    }
    List<String> result = new ArrayList<>();
    while (!dataStack.isEmpty()) {
    String pop = dataStack.pop();
    result.add(pop);
    }
    // 对数组进行翻转
    return CollUtil.reverse(result);
    }

    /**
    * 对操作数栈的操作
    * @param d,当前操作符
    * @param opStack 操作数栈
    */

    private void operHandler(String d, Stack<String> opStack,Stack<String> dataStack) {
    // 操作数栈为空
    if (opStack.isEmpty()) {
    opStack.push(d);
    return;
    }
    // 如果遇到左括号"(“,直接推入操作符栈。
    if (d.equals("(")) {
    opStack.push(d);
    return;
    }
    // 如果遇到右括号”)“,将操作符栈中的操作符弹出并添加到输出栈,直到遇到匹配的左括号”("。
    if (d.equals(")")) {
    while (!opStack.isEmpty()) {
    String pop = opStack.pop();
    // 不是左括号
    if (!pop.equals("(")) {
    dataStack.push(pop);
    } else {
    return;
    }
    }
    }
    // 操作数栈不为空
    while (!opStack.isEmpty()) {
    // 获取栈顶元素和优先级
    String peek = opStack.peek();
    Integer v = opList.get(peek);
    // 获取当前元素优先级
    Integer c = opList.get(d);
    // 如果当前操作符的优先级较低或相等,且不为(),从操作符栈中弹出并添加到输出栈,然后重复比较直到可以推入操作符栈
    if (c < v && v != 3) {
    // 出栈
    opStack.pop();
    // 压入结果集栈
    dataStack.push(peek);
    } else {
    // 操作符与操作符栈栈顶的操作符的优先级。如果当前操作符的优先级较高,将其推入操作符栈。
    opStack.push(d);
    break;
    }
    }
    }

    测试代码如下:


    PostfixExpre postfixExpre = new PostfixExpre();

    List<String> postExp = postfixExpre.getPostExp(
    Arrays.asList("9", "+", "(" , "3", "-", "1", ")", "*", "3", "+", "10", "/", "2"));

    System.out.println(postExp);

    输出如下:


    [9, 3, 1, -, 3, *, 10, 2, /, +, +]


    五、求后缀表示值


    使用栈来实现


        /****
    * 计算后缀表达式的值
    * @param source
    * @return
    */

    public double calcPostfixExpe(List<String> source) {

    Stack<String> data = new Stack<>();
    for (int i = 0; i < source.size(); i++) {
    String s = source.get(i);
    // 如果是操作数
    if (opList.containsKey(s)) {
    String d2 = data.pop();
    String d1 = data.pop();
    Double i1 = Double.valueOf(d1);
    Double i2 = Double.valueOf(d2);
    Double result = null;
    switch (s) {
    case "+":
    result = i1 + i2;break;
    case "-":
    result = i1 - i2;break;
    case "*":
    result = i1 * i2;break;
    case "/":
    result = i1 / i2;break;
    }
    data.push(String.valueOf(result));
    } else {
    // 如果是操作数,进栈操作
    data.push(s);
    }
    }
    // 获取结果
    String pop = data.pop();
    return Double.valueOf(pop);
    }

    测试


    PostfixExpre postfixExpre = new PostfixExpre();

    List<String> postExp = postfixExpre.getPostExp(
    Arrays.asList("9", "+", "(" , "3", "-", "1", ")", "*", "3", "+", "10", "/", "2"));

    System.out.println(postExp);

    double v = postfixExpre.calcPostfixExpe(postExp);

    System.out.println(v);

    结果如下:


    [9, 3, 1, -, 3, *, 10, 2, /, +, +]
    20.0

    作者:小希爸爸
    来源:juejin.cn/post/7330583100059762697
    收起阅读 »

    我发现了 Android 指纹认证 Api 内存泄漏

    我发现了 Android 指纹认证 Api 内存泄漏 目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt 先说问题,使用Biome...
    继续阅读 »

    我发现了 Android 指纹认证 Api 内存泄漏


    目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt


    先说问题,使用BiometricPrompt 会造成内存泄漏,目前该问题试了 Android 11 到 13 都发生,而且没有什么好的办法。目前想到的最好的方法是漏的少一点。当然谁有好的办法欢迎留言。


    问题再现


    先看动画


    在这里插入图片描述


    动画中操作如下



    1. MainAcitivity 跳转到 SecondActivity

    2. SecondActivity 调用 BiometricPrompt 三次

    3. 从SecondActivity 返回到 MainAcitivity


    以下是使用 BiometricPrompt 的代码


    public fun showBiometricPromptDialog() {
    val keyguardManager = getSystemService(
    Context.KEYGUARD_SERVICE
    ) as KeyguardManager;

    if (keyguardManager.isKeyguardSecure) {
    var biometricPromptBuild = BiometricPrompt.Builder(this).apply {// this is SecondActivity
    setTitle("verify")
    setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
    }
    val biometricPromp = biometricPromptBuild.build()
    biometricPromp.authenticate(CancellationSignal(), mExecutor, object :
    BiometricPrompt.AuthenticationCallback() {

    })
    }
    else {
    Log.d("TAG", "showLockScreen: isKeyguardSecure is false");
    }
    }

    以上逻辑 biometricPromp 是局部变量,应该没有问题才对。


    内存泄漏如下


    在这里插入图片描述
    可以看到每启动一次生物认证,创建的 BiometricPrompt 都不会被回收。


    规避方案:


    修改方案也简单


    方案一:



    1. biometricPromp 改为全局变量。

    2. this 改为 applicationContext


    方案一存在的问题,SecondActivity 可能频繁创建,所以 biometricPromp 还会存在多个实例。


    方案二(目前想到的最优方案):



    1. biometricPromp 改为单例

    2. this 改为 applicationContext


    修改后,App memory 中只存在一个 biometricPromp ,且没有 Activity 被泄漏。


    想到这里,应该会觉得奇怪,biometricPromp 为什么不会被回收?提供的 API 都看过了,没有发现什么方法可以解决这个问题。直觉告诉我这个可能是系统问题,下来分析下BiometricPrompt 吧。


    BiometricPrompt 源码分析


    在这里插入图片描述


    App 相关信息通过 BiometricPrompt 传递到 System 进程,System 进程再通知 SystemUI 显示认证界面。


    App 信息传递到 System 进程,应该会使用 Binder。这个查找 BiometricPrompt 使用哪些 Binder。


    private final IBiometricServiceReceiver mBiometricServiceReceiver =
    new IBiometricServiceReceiver.Stub() {

    ......
    }

    源码中发现 IBiometricServiceReceiver 比较可疑,IBiometricServiceReceiver 是匿名内部类,内部是持有 BiometricPrompt 对象的引用。


    接下来看下 System Server 进程信息(注:系统是 UserDebug 的手机,才可以查看,买的手机版本是不支持的)


    在这里插入图片描述



    😂 App 使用优化后(方案二)App 只存在一个 IBiometricServiceReceiver ,而 system 进程中存在三个 IBiometricServiceReceiver 的 binder proxy。 每次启动 BiometricPrompt 都会创建一个。这个就不解释为什么会出现三个binder proxy,感兴趣可以看下面推荐的文章。GC root 是 AuthSession。

    再看下 AuthSession 的实例数


    在这里插入图片描述


    果然 AuthSession 也存在三个。


    在这里插入图片描述


    这里有个知识点,binder 也是有生命周期的,三个 Proxy 这篇文章也是解释了的。有兴趣的可以了看下。


    Binder | 对象的生命周期


    一开始,我以为 AuthSession 没有被置空,看下代码,发现 AOSP 的代码,还是比较严谨的,有置空的操作。


    细心的同学发现,上图中 AuthSession 没有被任何对象引用,AuthSession 就是 GC Root,哈哈哈。


    问题解密


    一个实例什么情况可以作为GC Root,有兴趣的同学,可以自行百度,这里就不卖关子了,直接说问题吧。


    Binder.linkToDeath()


    public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
    }

    需要传递 IBinder.DeathRecipient ,这个 DeathRecipient 会被作为 GC root。当调用 unlinkToDeath(@NonNull DeathRecipient recipient, int flags),GC root 才被收回。


    AuthSession 初始化的时候,会调用 IBiometricServiceReceiver .linkToDeath。


    public final class AuthSession implements IBinder.DeathRecipient {
    AuthSession(@NonNull Context context,
    ......
    @NonNull IBiometricServiceReceiver clientReceiver,
    ......
    ) {
    Slog.d(TAG, "Creating AuthSession with: " + preAuthInfo);
    ......
    try {
    mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);//this 变成 GC root
    } catch (RemoteException e) {
    Slog.w(TAG, "Unable to link to death");
    }

    setSensorsToStateUnknown();
    }
    }

    Jni 中 通过 env->NewGlobalRef(object),告诉虚拟机 AuthSession 是 GC Root。


    core/jni/android_util_Binder.cpp

    static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
    jobject recipient, jint flags)
    // throws RemoteException
    {
    if (recipient == NULL) {
    jniThrowNullPointerException(env, NULL);
    return;
    }

    BinderProxyNativeData *nd = getBPNativeData(env, obj);
    IBinder* target = nd->mObject.get();

    LOGDEATH("linkToDeath: binder=%p recipient=%p\n", target, recipient);

    if (!target->localBinder()) {
    DeathRecipientList* list = nd->mOrgue.get();
    sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient, list);//java 中 DeathRecipient 会被封装为 JavaDeathRecipient
    status_t err = target->linkToDeath(jdr, NULL, flags);
    if (err != NO_ERROR) {
    // Failure adding the death recipient, so clear its reference
    // now.
    jdr->clearReference();
    signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/);
    }
    }
    }

    JavaDeathRecipient(JNIEnv* env, jobject object, const sp<DeathRecipientList>& list)
    : mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)),// object -> DeathRecipient 变为 GC root
    mObjectWeak(NULL), mList(list)
    {
    // These objects manage their own lifetimes so are responsible for final bookkeeping.
    // The list holds a strong reference to this object.
    LOGDEATH("Adding JDR %p to DRL %p", this, list.get());
    list->add(this);

    gNumDeathRefsCreated.fetch_add(1, std::memory_order_relaxed);
    gcIfManyNewRefs(env);
    }

    unlinkToDeath 最终会在 Jni 中 通过 env->DeleteGlobalRef(mObject),告诉虚拟机 AuthSession 不是GC root。


    virtual ~JavaDeathRecipient()
    {
    //ALOGI("Removing death ref: recipient=%p\n", mObject);
    gNumDeathRefsDeleted.fetch_add(1, std::memory_order_relaxed);
    JNIEnv* env = javavm_to_jnienv(mVM);
    if (mObject != NULL) {
    env->DeleteGlobalRef(mObject);// object -> DeathRecipient GC root 被撤销
    } else {
    env->DeleteWeakGlobalRef(mObjectWeak);
    }
    }

    解决方式


    AuthSession 置空的时候调用 IBiometricServiceReceiver 的 unlinkToDeath 方法。


    总结


    以上梳理的其实就是 Binder 的造成的内存泄漏。


    问题严重性来看,也不算什么大问题,因为调用 BiometricPrompt 的进程被杀,system 进程相关实例也就回收释放了。一般 app 也不太可能出现,常驻进程,而且还频繁调用手机认证的。


    这里主要介绍了一种容易被忽略的内存泄漏,Binder.linktoDeath()。
    Google issuetracker


    参考资料


    Binder | 对象的生命周期


    作者:Jingle_zhang
    来源:juejin.cn/post/7202066794299129914
    收起阅读 »

    曹贼,莫要动‘我’网站 —— 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
    收起阅读 »

    什么是Spring Boot中的@Async

    异步方法 随着硬件和软件的高度发展,现代应用变得更加复杂和要求更高。由于 高需求,工程师总是试图寻找新的方法来提高应用程序性能和响应能力。慢节奏应用程序的一种解决方案是实施异步方法。异步处理是一种执行任务并发运行的进程或函数,无需等待一个任务完成后再开始另一个...
    继续阅读 »

    异步方法


    随着硬件和软件的高度发展,现代应用变得更加复杂和要求更高。由于

    高需求,工程师总是试图寻找新的方法来提高应用程序性能和响应能力。慢节奏应用程序的一种解决方案是实施异步方法。异步处理是一种执行任务并发运行的进程或函数,无需等待一个任务完成后再开始另一个任务。在本文中,我将尝试探索 Spring Boot 中的异步方法和 @Async 注解,试图解释多线程和并发之间的区别,以及何时使用或避免它。


    Spring中的@Async是什么?


    Spring 中的 @Async 注解支持方法调用的异步处理。它指示框架在单独的线程中执行该方法,允许调用者继续执行而无需等待该方法完成。这

    提高了应用程序的整体响应能力和吞吐量。


    要使用@Async,您必须首先通过将@EnableAsync注释添加到配置类来在应用程序中启用异步处理:


    @Configuration
    @EnableAsync
    public class AppConfig {
    }

    接下来,用@Async注解来注解你想要异步执行的方法:



    @Service
    public class AsyncService {
    @Async
    public void asyncMethod() {
    // Perform time-consuming task
    }
    }

    @Async 与多线程和并发有何不同?


    有时,区分多线程和并发与并行执行可能会让人感到困惑,但是,两者都与并行执行相关。他们每个人都有自己的用例和实现:



    • @Async 注解是 Spring 框架特定的抽象,它支持异步执行。它提供了轻松使用异步的能力,在后台处理所有艰苦的工作,例如线程创建、管理和执行。这使用户能够专注于业务逻辑而不是底层细节。

    • 多线程是一个通用概念,通常指操作系统或程序同时管理多个线程的能力。由于 @Async 帮助我们自动完成所有艰苦的工作,在这种情况下,我们可以手动处理所有这些工作并创建一个多线程环境。 Java 具有ThreadExecutorService等必要的类来创建和使用多线程。

    • 并发是一个更广泛的概念,它涵盖多线程和并行执行技术。它是

      系统在一个或多个处理器上同时执行多个任务的能力。


    综上所述,@Async是一种更高层次的抽象,它为开发人员简化了异步处理,而多线程和并发更多的是手动管理并行执行。


    何时使用 @Async 以及何时避免它。


    使用异步方法似乎非常直观,但是,必须考虑到这种方法也有注意事项。


    在以下情况下使用@Async:



    • 您拥有可以并发运行的独立且耗时的任务,而不会影响应用程序的响应能力。

    • 您需要一种简单而干净的方法来启用异步处理,而无需深入研究低级线程管理。


    在以下情况下避免使用 @Async:



    • 您想要异步执行的任务具有复杂的依赖性或需要大量的协调。在这种情况下,您可能需要使用更高级的并发 API,例如CompletableFuture或反应式编程库,例如 Project Reactor。

    • 您必须精确控制线程的管理方式,例如自定义线程池或高级同步机制。在这些情况下,请考虑使用 Java 的ExecutorService或其他并发实用程序。


    在 Spring Boot 应用程序中使用 @Async。


    在此示例中,我们将创建一个简单的 Spring Boot 应用程序来演示 @Async 的使用。

    让我们创建一个简单的订单管理服务。



    1. 创建一个具有最低依赖要求的新 Spring Boot 项目:


      org.springframework.boot:spring-boot-starter

      org.springframework.boot:spring-boot-starter-web

      Web 依赖用于 REST 端点演示目的。 @Async 带有引导启动程序。


    2. 将 @EnableAsync 注释添加到主类或应用程序配置类(如果我们使用它):


    @SpringBootApplication
    @EnableAsync
    public class AsyncDemoApplication {
    public static void main(String[] args) {
    SpringApplication.run(AsyncDemoApplication.class, args);
    }
    }

    @Configuration
    @EnableAsync
    public class ApplicationConfig {}


    1. 对于最佳解决方案,我们可以做的是,创建一个自定义 Executor bean 并根据我们的需要在同一个 Configuration 类中对其进行自定义:


       @Configuration
    @EnableAsync
    public class ApplicationConfig {

    @Bean
    public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(10);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("");
    executor.initialize();
    return executor;
    }
    }

    通过此配置,我们可以控制最大和默认线程池大小。以及其他有用的定制。



    1. 使用 @Async 方法创建 OrderService 类:


    @Service
    public class OrderService {

    @Async
    public void saveOrderDetails(Order order) throws InterruptedException {
    Thread.sleep(2000);
    System.out.println(order.name());
    }

    @Async
    public CompletableFuture<String> saveOrderDetailsFuture(Order order) throws InterruptedException {
    System.out.println("Execute method with return type + " + Thread.currentThread().getName());
    String result = "Hello From CompletableFuture. Order: ".concat(order.name());
    Thread.sleep(5000);
    return CompletableFuture.completedFuture(result);
    }

    @Async
    public CompletableFuture<String> compute(Order order) throws InterruptedException {
    String result = "Hello From CompletableFuture CHAIN. Order: ".concat(order.name());
    Thread.sleep(5000);
    return CompletableFuture.completedFuture(result);
    }
    }

    我们在这里所做的是创建 3 种不同的异步方法。第一个saveOrderDetails服务是一个简单的异步

    服务,它将开始异步计算。如果我们想使用现代异步Java功能,

    例如CompletableFuture,我们可以通过服务来实现saveOrderDetailsFuture。通过这个服务,我们可以调用一个线程来等待@Async的结果。应该注意的是,CompletableFuture.get()在结果可用之前会阻塞。如果我们想在结果可用时执行进一步的异步操作,我们可以使用thenApplythenAccept或 CompletableFuture 提供的其他方法。



    1. 创建一个 REST 控制器来触发异步方法:


    @RestController
    public class AsyncController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
    this.orderService = orderService;
    }

    @PostMapping("/process")
    public ResponseEntity<Void> process(@RequestBody Order order) throws InterruptedException {
    System.out.println("PROCESSING STARTED");
    orderService.saveOrderDetails(order);
    return ResponseEntity.ok(null);
    }

    @PostMapping("/process/future")
    public ResponseEntity<String> processFuture(@RequestBody Order order) throws InterruptedException, ExecutionException {
    System.out.println("PROCESSING STARTED");
    CompletableFuture<String> orderDetailsFuture = orderService.saveOrderDetailsFuture(order);
    return ResponseEntity.ok(orderDetailsFuture.get());
    }

    @PostMapping("/process/future/chain")
    public ResponseEntity<Void> processFutureChain(@RequestBody Order order) throws InterruptedException, ExecutionException {
    System.out.println("PROCESSING STARTED");
    CompletableFuture<String> computeResult = orderService.compute(order);
    computeResult.thenApply(result -> result).thenAccept(System.out::println);
    return ResponseEntity.ok(null);
    }
    }

    现在,当我们访问/process端点时,服务器将立即返回响应,同时

    继续saveOrderDetails()在后台执行。 2秒后,服务完成。第二个端点 -/process/future将使用我们的第二个选项,CompletableFuture在这种情况下,5 秒后,服务将完成,并将结果存储在CompletableFuture我们可以进一步使用future.get()来访问结果。在最后一个端点 - 中/process/future/chain,我们优化并使用了异步计算。控制器使用相同的服务方法CompletableFuture,但不久之后,我们将使用thenApply,thenAccept方法。服务器立即返回响应,我们不需要等待5秒,计算将在后台完成。在这种情况下,最重要的一点是对异步服务的调用,在我们的例子中compute()必须从同一类的外部完成。如果我们在一个方法上使用@Async并在同一个类中调用它,它将不起作用。这是因为Spring使用代理来添加异步行为,并且在内部调用方法会绕过代理。为了使其发挥作用,我们可以:



    • 将 @Async 方法移至单独的服务或组件。

    • 使用 ApplicationContext 获取代理并调用其上的方法。


    总结


    Spring 中的 @Async 注解是在应用程序中启用异步处理的强大工具。通过使用@Async,我们不需要陷入并发管理和多线程的复杂性来增强应用程序的响应能力和性能。但要决定何时使用 @Async 或使用替代并发

    使用程序,了解其局限性和用例非常重要。


    作者:it键盘侠
    来源:juejin.cn/post/7330227149176881161
    收起阅读 »

    前端实现 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
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Android-桌面小组件RemoteViews播放动画

    一、前言 前段时间什么比较火?当然是木鱼了,木鱼一敲,烦恼全消~在这个节奏越来越快的社会上,算是一个不错的解压利器! 我们也紧跟时事,推出了  我要敲木鱼(各大市场均可以下载哦~) 咳咳,扯远了,说回正题 我们在后台收到大量反馈,说是希望添加桌面组件敲木鱼功能...
    继续阅读 »

    一、前言


    前段时间什么比较火?当然是木鱼了,木鱼一敲,烦恼全消~在这个节奏越来越快的社会上,算是一个不错的解压利器!


    我们也紧跟时事,推出了  我要敲木鱼(各大市场均可以下载哦~)


    咳咳,扯远了,说回正题


    我们在后台收到大量反馈,说是希望添加桌面组件敲木鱼功能。好嘛,用户的话就是圣旨,那必须要安排上,正好我也练练手。


    老规矩,先来看下我实现的效果



    这个功能看着很简单对吧,却也花了我一天半的时间。主要用来实现敲击动画了!!


    二、代码实现


    1、新建小组件



     2、修改界面样式


    主要会生成3个关键文件(文件名根据你设置的来)

    ①、APPWidget  类,继承于 AppWidgetProvider,本质是一个 BroadCastReceiver


    ②、layout/widget.xml ,小组件布局文件


    ③、xml/widget_info.xml ,小组件信息说明文件


    同时会在 AndroidManifest中注册好


    类似如下代码:


         <receiver
    android:name=".receiver.MuyuAppWidgetBig"
    android:exported="false">
    <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    <action android:name="com.fyspring.bluetooth.receiver.action_appwidget_muyu_knock" />
    </intent-filter>

    <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/app_widget_info_big" />
    </receiver>

    3、添加敲木鱼逻辑代码


    通过 APPWidget 的模板代码我们知道,内部通过 RemoteViews 来进行更新View,而我们都知道 RemoteViews 是无法通过 findViewById 来转成对应的 view,更无法对其添加 Animator。那么我们该怎么办来给桌面木鱼组件添加一个 缩放动画呢?


    给你三秒时间考虑下,这里我可花了一天时间来研究....


    通过 layoutAnimation !!!


    layoutAnimation 是在 ViewGr0up 创建之后,显示时作用的,作用时间是:ViewGr0up 的首次创建显示,之后再有改变就不行了。


    虽然 RemoteViews 不能执行 findViewById,但它提供了两个关键方法: remoteViews.removeAllViews  和  remoteViews.addView 。如果我们在点击时,向组件布局中添加一个带有 layoutAnimation 的布局,不是就可以间接播放动画了么?


    关键代码:


    private fun doAnimation(context: Context?, remoteViews: RemoteViews?) {
    remoteViews?.removeAllViews(R.id.muyu_rl)
    val remoteViews2 = RemoteViews(context?.packageName, R.layout.anim_layout)
    remoteViews2.setImageViewResource(R.id.widget_muyu_iv, R.mipmap.ic_muyu)
    remoteViews?.addView(R.id.muyu_rl, remoteViews2)
    }

    小组件布局:


    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    style="@style/Widget.BlueToothDemo.AppWidget.Container"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/shape_round_bg"
    android:theme="@style/Theme.BlueToothDemo.AppWidgetContainer">

    <LinearLayout
    android:layout_width="140dp"
    android:layout_height="140dp"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <TextView
    android:id="@+id/appwidget_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:contentDescription="测试桌面木鱼"
    android:text="已敲0次"
    android:textColor="@color/white"
    android:textSize="18sp"
    android:textStyle="bold" />

    <RelativeLayout
    android:id="@+id/muyu_rl"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
    android:id="@+id/widget_muyu_iv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerHorizontal="true"
    android:layout_margin="15dp"
    android:src="@mipmap/ic_muyu" />

    </RelativeLayout>
    </LinearLayout>
    </RelativeLayout>

    添加替换的动画布局(anim_layout.xml),注意两边的木鱼ImgView 的 ID保持一致,因为要统一设置点击事件!!


    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layoutAnimation="@anim/muyu_anim">

    <ImageView
    android:id="@+id/widget_muyu_iv"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@mipmap/ic_muyu2" />
    </RelativeLayout>

    动画文件:(muyu_anim.xml)


    <?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/scale_anim"/>


    动画文件:(scale_anim.xml)


    <?xml version="1.0" encoding="utf-8"?>
    <scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="100"
    android:fromXScale="0.9"
    android:fromYScale="0.9"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toXScale="1"
    android:toYScale="1" />

    关键动画代码就是以上这些,如果有问题欢迎私信。希望大家在新的一年里,木鱼一敲,烦恼全消~


    欢迎体验下我做的木鱼,记得搜  我要敲木鱼  哦~~


    作者:今夜太冷不宜私奔丶
    来源:juejin.cn/post/7323025855154962459
    收起阅读 »

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

    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
    收起阅读 »