Flutter 为什么没有一款好用的UI框架?
哈喽,我是老刘
前两天,系统给我推送了一个问题。
我理解提问者真正想问的是:有没有一个不用学习那么多UI组件和渲染知识,可以简单快速搭建UI的东西。
Flutter 包括原生开发,为什么需要考虑那么多细节,不能做的简单一些?
首先,我们需要明白Flutter的定位。
Flutter不是一个简单的甜品,而是一个能支撑大型系统开发的工程级框架。
这种定位和原生框架的定位是相当的。
因此,它要求整个框架有足够的灵活性,能适用于尽可能多的场景。
那么,如何提供足够的灵活性呢?
答案是让整个框架尽可能多的细节是可控的。
这就需要把整个框架的功能拆分的更细,提供的配置项足够多。
然而,这样的缺点就是开发起来会比较麻烦,需要控制很多细节。
因此,我们可以看到Flutter的组件拆分的很细,甚至有类似Padding这样专门负责缩进的组件,而且每个组件都有很多的配置参数。
Flutter配合Material组件库本身本就非常优秀的UI框架
虽然Flutter的灵活性带来了开发上的复杂性,但Flutter配合Material组件库本身就是一个非常优秀的UI框架。
Material组件库提供了丰富的预设组件,这些组件遵循Material Design指南,可以帮助开发者快速搭建出既美观又符合设计规范的UI界面。
使用Material组件库,开发者可以不必从头开始设计每一个UI元素,而是可以直接使用现成的组件,如按钮、对话框、卡片等,这些组件都有良好的交互和动画效果。
此外,Material组件库还提供了主题支持,开发者可以通过简单的配置,快速应用统一的风格到整个应用中。
因此,虽然Flutter的灵活性可能让初学者感到有些复杂,但配合Material组件库,Flutter实际上提供了一个非常高效和优秀的UI开发体验。
大型项目的正确打开方式
即便是Material组件库,它的设计是需要考虑应对各种不同类型app开发的,但是针对一个具体的项目,我们大多数时候不需要这样高的灵活性。
所以,这种情况下直接用Flutter提供的组件效率会比较低。
解放方法就是针对特定的项目做组件封装。
以我目前维护的项目为例,我们项目中所有的对话框都是相同的偏绿色调,圆角半径20,按钮大小固定,标题、详情的字体、字号也固定。
简单来说,就是所有的UI细节都是固定的,只是不同的dialog需要填充的文字不同。
这时候,我们就会定义一个自己的Dialog组件,只需要使用者传入标题和内容,以及设置按钮的回调即可。
UI的其他地方也是如此,比如页面框架、在多个页面都能用到的用户卡片、商品卡片等等。
当你的整个App大部分都是基于这些自定义组件进行搭积木式的开发,那开发效率是不是比找一些通用的UI框架更高呢?
总结
总而言之,Flutter因为它的工程级框架定位需要提供高度的灵活性,而这往往会导致开发细节的复杂性。
但是,通过针对具体项目的组件封装,我们可以大大提高开发效率,同时保持UI的一致性和项目的特定需求。
所以,与其寻找一个通用的UI框架,不如根据项目的具体需求进行自定义组件的开发。
如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》
来源:juejin.cn/post/7387001928209170447
劝互联网的牛马请善待自己
掘友们,大家好,我是一名95后全栈程序媛,一直以来在努力追求WLB,28岁前完成了畅游中国,既努力生活也认真工作。很多人可能还不知道WLB这个词,WLB就是work life balance
,一开始我看到这个词都是从猎头那里传过来的,岗位招聘一般都是:xxx神仙外企WLB,一周只上3天班,每周两天可以居家办公,每年年假几十天,要求英语口语流利,有x年工作经验,base范围在xx个w....
众所周知,外企的年包肯定不如那些一线爆肝厂,当然工作时间跟收入都是成正比的,965的外企跟11116的互联网的年包肯定是不一样的,付出的工作时间都不一样。假如拼夕夕给你年薪百万,神仙外企给你年薪50+w,你会怎么选?
最近出来几条消息刺痛了牛马的心情!这个世界变幻莫测~
四十多岁的程序员在公司工作11年被裁员
徐峥出新电影了《逆行人生》讲述的就是一个四十多岁的程序员被裁员后找不到工作只能去送外卖的心酸故事,当现实照进电影,卑微的打工人在时代的潮流下只是一个渺小颗粒的缩影。
得物“35岁被暴力裁员”、“80余万元期权直接打水漂”。
一年前,面临裁员的得物员工徐凯多次与公司沟通取得期权再离职未果后,他到上海市仲裁委员会处申请恢复与得物的劳动关系,后被予以支持。7月,因不服上海市仲裁委员会裁定的结果,得物继续上诉,再度将前员工诉于法庭之上。
去哪儿宣布每周可居家办公两天
这则消息意味着互联网公司开启新的里程碑,向神仙外企的福利看齐了,对于老弱病残的打工人简直不要太友好了。
这种待遇,以前在互联网几乎不存在的。一周连休四天的日子,体验过就不再想去卷996的牛马岗了。
不管我们有多努力,我们都只是老板眼里赚钱的工具人
在互联网,35岁已经是一道坎,人在互联网漂,哪有不挨刀,不管有多努力,到了大龄的年纪,工资比年轻人高产出比年轻人低的时候,面临着公司随时都可能会说:分手吧,没有分手费,你自己知难而退吧,大家好聚好散!像极了一个渣男遇到了更年轻漂亮的白富美抛弃糟糠之妻,完了还pua你说都是你的错我才选择了别人。同样,公司会pua说,都是你的没能力,我才选择了别的员工,渣男有道德的谴责?公司有吗?公司跟你只有劳动关系,只要合法,随时跟你说你被毕业了,给个n+1的分手费都要被牛马说这渣渣企真良心!
对于老板来说,赚钱的时候大家都是兄弟,不赚钱了不认兄弟,说好聚好散!你把公司当家,公司把你当牛马。这点,我们真的要向00后学习,提前认清职场,打工就是为了赚钱,为了更好的生活,并不是为了努力加班毁了我们的生活,那我们辛辛苦苦打工有什么意义呢?
是否真的对自己的选择满意
知乎上有一个热度很高的话题:阿里p7和副处级哪个更厉害?
总说纷云,有人选择p7:
有人选择为人民服务;
也有人两者都想要:
在稳定和高薪面前,大家都想要稳定高薪的工作,最后变成稳定焦虑的牛马,这就好像围城,体制内的羡慕体制外的高薪,高薪的牛马羡慕体制内的稳定。即使义无反顾选择了卷互联网,几年挣够了人家一辈子的钱,但是买了二居想换三居,买了三居想换别墅,收入的增长带来消费的提升,物欲的无限放大,依然很多年入百万的人并不觉得真正的快乐而满足现状!即使选择了稳定的体制内,工作体面生活稳定,但是在权力面前,一直追名逐利,在很多诱惑下,最后的选择身不由己!
所以,欲望面前,你有好好认真的生活吗?认真对待自己的身体健康吗?是为了碎银几两熬夜加班把身体搞垮还是为了三餐有汤就行选择WLB呢?希望每一个焦虑的互联网牛马都好好善待自己,平衡好自己身体的健康和对金钱物欲的追逐。
我最羡慕内心富裕,内核稳定的人,这种人一般要比同龄人状态更年轻。不容易被外界所干扰,明确知道自己该要什么,不该要什么,选择适合自己的生活,幸福满意度极高。
来源:juejin.cn/post/7390457313163067431
买房后,害怕失业,更不敢裸辞,心情不好就提前还房贷,缓解焦虑
自从买房后,心态有很大变化。虽然住自己的房子,心情和体验都很好,但是一把掏空钱包,很焦虑。买房后现金流一直吃紧,再加上每年16万的房贷,我很焦虑会失业。之前我喜欢裸辞,现在不敢想裸辞这个话题。尤其是在行业下行期,找工作很艰难,背着房贷裸辞,简直是头孢就酒,嫌命太久。
焦虑的根源是背负房贷,金额巨大,而且担心40岁以后失业,还不上房贷。
一次偶然的沟通
"你的带款利率调整了吗",同事问我。
同事比我早两年在北京买房,他在顺义买的,我在昌平买的,我俩一直有沟通房贷的问题。但我没听说利率有调整,银行好像也没通知我,于是我问道:”我不知道啊,你调整了?调到多少了?“。
”调的挺多的,已经降到了 4.3%“。同事兴高采烈的回复我。
”这么牛逼,之前我记得一直是4.85%,我去看看我的利率“,我听说房贷利率下降那么多,很是兴奋。
然而我的房贷利率没有调整,我尝试给银行打电话,沟通的过程很坎坷。工商银行客服说了很多,大概意思是:利率会自动调整,无需申请,但是要等到利率调整日才会调整”。我开始很不理解,很生气,利率都调整了,别人也都调整了,凭什么不给我调整呢?
我想到同事有尝试提前还贷,生气的时候,我就萌发了提前还贷的想法。
开始尝试提前还贷,真香
我在22年初带款买房,其中商业带款 174 万,带款25年,等额本息,每个月要还 1 万的房贷。公积金带款每个月大概需要还 2500。每个月一万二的房贷还是很有压力的,尤其是刚买房的这一两年,兜里比脸都干净,没存款,不敢失业,更不敢裸辞。
即便兜里存款不多,也要提前还贷,因为实在太香了。
我在工行App上,申请 提前还贷,选择缩短 18个月的房贷,只需要 6万2,而我每个月房贷才1万,相当于是用 6 万 顶 18 万的房贷。还有比这更划算的事情吗?
预约提前还款后,银行会安排一个时间,在这个时间前,把钱存进去。到时候银行就会扣除,如果扣除金额不足,那么提前还款计划则自动终止,需要重新预约!
工行的预约还款时间大概是1个月以后,我是10-15号申请提前还款,银行给的预约日期是 11-14号,大概是1个月。
提前还款,比理财强多了
这次还贷以后,我又申请了提前还款, 提前还 24 期,只需要 9 万,也就是 9 万顶 24 万;提前还 60 期,只需要 24 万,相当于 24 万顶 60 万。
还有比提前还贷收益更高,风险更低的理财方式吗?没有! 除了存款外,任何理财都是有风险的。债券和基金收益和风险挂钩,想找到收益5%的债券基金,要承担亏本风险。你惦记人家的利息,人家惦记你的本金!
股票的风险更不必说,我买白酒股票已经被套的死死,只能躺平装死。(劝大家不要入 A 股)
提前还贷划算吗?
我目前的带款利息是 4.85%,而存到银行的利息不会超过 3% ,很多货币基金只有 2%了。两者利息差高达 3%,肯定是提前还带款更加合适。
要明白,一年两年短期的利息差还好,但是房贷可是高达 25 年。25年 170 万带款 3% 的利息差,这个金额太大了。提前还了,省下来的钱还是很多的。例如刚才截图里展示的 提前还 24 万顶了 60 万的房贷。
网上很多砖家说,“要考虑通货膨胀因素,4.85% 的带款利率和实际通货膨胀比起来不高,提前还款不划算。”
砖家说话都是昧良心的。提前还带款是否划算,只需要和存款利率比就行了,不需要和通货膨胀比。因为把钱存在银行也会因为通货膨胀贬值。只有把钱 全都消费,全部花光才不会受通货膨胀的困扰,建议砖家,多消费,把家底败光,这样最划算!
砖家们一定是害怕太多人提前还贷,影响了银行的放贷生意。今年上半年,提前还贷已经成潮流,有些银行坐不住,甚至关闭了提前还贷的入口…… 所以要抓紧,没准哪天就提高了还贷门槛,或者直接禁止。
程序员群体收入高,手里闲钱多,可以考虑提前还带款,比存银行划算多了,别再给银行打工了!
来源:juejin.cn/post/7301530293378727971
刚入职因为粗心大意,把事情办砸了,十分后悔
刚入职,就踩大坑,相信有很多朋友有我类似的经历。
5年前,我入职一家在线教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。
在熟悉环境之后,我趁着上厕所的时候,顺便去旁边的零食摊挑了点零食。接下来的一天里,我专注地配置开发环境、阅读新人文档,当然我也不忘兼顾手边的零食。
初出茅庐,功败垂成
"好景不长",第三天上午,刚到公司,屁股还没坐热。新组长立刻给我安排了任务。他决定让我将配置端的课程搜索,从使用现有的Lucene搜索切换到ElasticSearch搜索。这个任务并不算复杂,然而我却办砸了。
先说为什么不复杂?
- ElasticSearch的搜索功能 基于Lucene工具库实现的,两者在搜索请求构造方式上几乎一致,在客户端使用上差异很小。
- 切换方案无需顾虑太多稳定性问题。由于是配置端课程搜索,并非是用户端搜索,所以平稳切换的压力较小、性能压力也比较小。
总的来说,领导认为这个事情并不紧急,重要性也不算高,而且业务逻辑相对简单,难度能够把握,因此安排我去探索一下。可是,我却犯了两个错误,把入职的第一件事办砸了。现在回过头来看,十分遗憾!
难以解决的bug让我陷入困境
将搜索方式从Lucene切换为ElasticSearch后,如何评估切换后搜索结果的准确度呢?
除了通过不断地回归测试,还有一个更好的方案。
我的方案是,在调用搜索时同时并发调用Lucene搜索和ElasticSearch搜索。在汇总搜索结果时,比对两者的搜索结果是否完全一致。如果在切换搜索引擎的过程中,两个方案的搜索结果不一致,就打印异常搜索条件和搜索结果,并进行人工排查原因。
在实际切换过程中,我经常遇到搜索数据不一致的情况,这让我感到十分苦恼。我花了一周的时间编写代码,然后又用了两周多的时间来排查问题,这超出了预估的时间。在这个过程中,我感到非常焦虑和沮丧。作为一个新来的员工,我希望能够表现出色,给领导留下好印象。然而事与愿违,难以解决的bug让我陷入困境。
经过无数次的怀疑和尝试,我终于找到了问题的根源。原来,我忘记了添加排序方式。
因为存在很多课程数据,所以配置端搜索需要分页搜索。在之前的Lucene搜索方式中,我们使用课程Id来进行排序。然而在切换到新的ElasticSearch方案中时,我忘记了添加排序方式。这个错误的后果是,虽然整体上结果是一致的,但由于新方案没有排序方式,每一页的搜索结果是随机的,无法预测,所以与原方案的结果不一致。
新方案加上课程Id排序方式以后,搜索结果和原方案一致。
为此,我总结了分页查询的设计要点!希望大家不要重复踩坑!# 四选一,如何选择适合你的分页方案?
千万不要粗心大意
实际上,在解决以上分页搜索没有添加排序方式的问题之后,还存在着许多小问题。而这些小问题都反映了我的另一个不足:粗心大意。
正是这些小问题,导致线上环境总会出现个别搜索结果不一致的情况,导致这项工作被拖延很久。
课程模型是在线教育公司非常核心的数据模型,业务逻辑非常复杂,当然字段也非常多。在我入职时,该模型已经有120 个字段,并且有近 50 个字段可以进行检索。
在切换搜索方式时,我需要重新定义各种DO、DTO、Request等类型,还需新增多个类,并重新定义这些字段。在这个过程中,我必须确保不遗漏任何字段,也不能多加字段。当字段数量在20个以内时,这项工作出错的可能性非常低。然而,班课模型却有多达120个字段,因此出错的风险极大。当时我需要大量搬运这些字段,然而我只把这项工作看作是枯燥乏味的任务,未能深刻意识到出错的可能性极大,所以工作起来散漫随意,没有特别仔细校验重构前后代码的准确性。
墨菲定律:一件事可能出错时就一定会出错
墨菲定律是一种普遍被接受的观念,指出如果某件事情可能出错,那么它将以最不利的方式出错。这个定律起源于美国航天局的项目工程师爱德华·墨菲,在1950年代发现了这一规律。
墨菲定律还强调了人类的倾向,即将事情弄糟或让事情朝着最坏的方向发展。它提醒人们在计划和决策时要考虑可能出错的因素,并准备应对不利的情况。
墨菲定律实在是太准了,当你感觉某个事情可能会出错的时候,那它真的就会出错。而且我犯错不止一次,因为有120个字段,很多字段的命名非常相似,最终我遗漏了2个字段,拼写错误了一个字段,总共有三个字段出了问题。
不巧的是,这三个字段也参与检索。当用户在课程搜索页面选择这三个字段来进行检索时,因为字段的拼写错误和遗漏,这三个字段没有被包含在检索条件中,导致搜索结果出错……
导致这个问题的原因有很多,其中包括字段数量太多,我的工作不够细致,做事粗心大意,而且没有进行充分的测试……
为什么没有测试
小公司的测试人员相对较少,尤其是在面对课程管理后台的技术重构需求时,更加无法获取所需的测试资源!
组长对我说:“ 要人没有,要测试更没有!”
事情办砸了,十分遗憾
首先,从各个方面来看,切换搜索引擎这件事的复杂度和难度是可控的,而且目标也非常明确。作为入职后第一项任务,我应该准确快速地完成它,以留下一个良好印象。当然,领导也期望我能够做到这一点,然而事实与期望相去甚远。
虽然在线上环境没有出现问题,但在上线后,问题排查的时间却远远超出了预期,让领导对结果不太满意。
总的来说,从这件事中,我获得的最重要教训就是:对于可能出错的事情,要保持警惕。时刻用墨菲定律提醒自己,要仔细关注那些可能发生小概率错误的细节问题。
对于一些具有挑战性的工作,我们通常都非常重视,且在工作中也非常认真谨慎,往往不会出错。
然而,像大量搬运代码、搬运大量字段等这类乏味又枯燥的工作确实容易使人麻痹大意,因此我们必须提高警惕。要么我们远离这些乏味的工作,要么就要认真仔细地对待它们。
否则,如果对这些乏味工作粗心大意,墨菲定律一定会找上你,让你在线上翻车!
来源:juejin.cn/post/7295576148364787751
记一种不错的缓存设计思路
之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。
场景
假设有个以下格式的接口:
GET /api?keys={key1,key2,key3,...}&types={1,2,3,...}
其中 keys 是业务主键列表,types 是想要取到的信息的类型。
请求该接口需要返回业务主键列表对应的业务对象列表,对象里需要包含指定类型的信息。
业务主键可能的取值较多,千万量级,type 取值范围为 1-10,可以任意组合,每种 type 对应到数据库是 1-N 张表,示意:
现在设想这个接口遇到了性能瓶颈,打算添加 Redis 缓存来改善响应速度,应该如何设计?
设计思路
方案一:
最简单粗暴的方法是直接使用请求的所有参数作为缓存 key,请求的返回内容为 value。
方案二:
如果稍做一下思考,可能就会想到文首我提到的觉得不错的思路了:
- 使用
业务主键:表名
作为缓存 key,表名里对应的该业务主键的记录作为 value; - 查询时,先根据查询参数 keys,以及 types 对应的表,得到所有
key1:tb_1_1
、key1:tb_1_2
这样的组合,使用 Redis 的 mget 命令,批量取到所有缓存中存在的信息,剩下没有命中的,批量到数据库里查询到结果,并放入缓存; - 在某个表的数据有更新时,只需刷新
涉及业务主键:该表名
的缓存,或令其失效即可。
小结
在以上两种方案之间做评估和选择,考虑几个方面:
- 缓存命中率;
- 缓存数量、占用空间大小;
- 刷新缓存是否方便;
稍作思考和计算,就会发现此场景下方案二的优势。
另外,就是需要根据实际业务场景,如业务对象复杂度、读写次数比等,来评估合适的缓存数据的粒度和层次,是对应到某一级组合后的业务对象(缓存值对应存储 + 部分逻辑),还是最基本的数据库表/字段(存储的归存储,逻辑的归逻辑)。
来源:juejin.cn/post/7271597656118394899
有哪些事情,是当了程序员之后才知道的?
1、平庸的程序员占比很大。 还没参加工作时,觉得程序员是个改变世界的高科技职业,后来才发现,其实这个群体里有很多CRUD Boy和SQL Boy,Ctrl C+Ctrl V是我们使用最多的电脑操作,没有之一。
而且,大多数同事上班摸鱼偷懒,遇到问题躲着走,下班也从来不主动学习充电。每当我问他们,他们都说这样混着也挺好,不想太累。
2、数量堆死质量。 如果你觉得没有写代码的天赋,那么请你先写10万行代码再说。
如果你在刷leetcode的时候非常痛苦,甚至有时候看答案都看不懂。那你就先把代码背下来,然后一遍一遍默写。每当你默写五遍以上,就开始慢慢理解了,刷十遍以上,再遇到类似的题,就有条件发射,能举一反三了。
这种方法运用到看底层源码,看一些晦涩难懂的技术类书籍上,也同样适用。
后来,我在网上看了硅谷王川的一段话:所有的我们以为的质量问题,大多本质是数量问题。数量是最重要的质量。
而欧成效则说得更加直接:数量堆死质量!
3、尽量选择研发出身的老板的公司。 他们会知道程序员不是故意写bug的,也没有任何系统能做到100%的可用性。
而销售出身的老板,却永远把自家公司的程序员看做产出并不令人满意的高成本项。而且还时不时地要求程序员跟销售一样喊几句令人其鸡皮疙瘩的鸡血口号。
4、大厂和小厂的程序员,技术上差距并不大。 他们的差距也许是在学历上,也许是在人脉上,也许是在沟通和向上管理上。
5、对测试同学客气一点, 他们是你写的代码的最后一道防线。再有就是,如果线上出了故障或者严重bug,很多产研以外的人都关注是哪个程序员造成了事故,而不是哪个测试同学没测出来。
6、产品经理是SB,甲方是SB的N次方。 最令人蛋疼的是,任何一家公司都是这样,所以你根本避无可避,只能长期共存。
7、程序员涨薪,最好的方式是跳槽, 而不是兢兢业业地加班工作。如果就靠公司每年涨的那些钱,估计得用7,8年才能实现薪资翻番。但如果靠跳槽,估计3年就能实现薪资翻番。
8、能不去外包公司就尽量不去,那种寄人篱下的无归属感才最让人心累。你会发现,公司的正式员工吃饭和娱乐都是不愿意带你玩儿的,平时跟你说话的表情也是鼻孔朝天。
9、面试造火箭,工作拧螺丝是正常的。 你要做的就是提升造火箭吹牛逼的能力,毕竟这才是你定级谈薪的资本。不要抱怨,要适应。
10、35岁的危机真的存在。 那些认为技术牛逼就可以平稳度过中年危机的人,很多都SB了。人老不以筋骨和技术为能,顺势而为,尽早找后路才是王道。
11、尽量去工程师占比超过30%的公司,因为它的期权可能在未来十年内变得很有价值。因为工程师占比越高,边际成本就越低。
12、离开公司这个平台,也许你什么都不是。 很多大厂的高P前辈,甚至是总监、 VP,也可能在某一个时间点,突然被淘汰!我身边就有一个BAT的总监,真的就突然被优化了,真的就找不到哪怕一半的薪资了。突然之间!
拔剑四顾心茫然.... 所以,永远要分清楚哪些是平台资源,哪些是你的能力。时刻对自身能力保持清醒且准确的认知,千万不要陷入盲目自负的境地。实在太过乐观的大厂朋友,可以周期性出来面试,哪怕不跳槽,认知自己的真实价值。
13、技术面试官问期望薪资,记得往低了说。 因为他们往往并不负责最终的定薪,但如果你的期望薪资高于他,会让他产生强烈的不平衡,从而把你Pass掉。
14、身体才是一切的本钱。 前些天左耳朵耗子前辈的忽然离世,再次验证了这一点,如果身体健康是0,那么其他的所有一切都是0。
15、脱发和格子衫的说法,并不普遍。 我认识的程序员里,80%是不穿格子衫的,而且35岁+的程序员,80%也是不脱发的。
但是有一种东西是很普遍的,那就是装着电脑的双肩包。
16、PPT架构师、周报合并师、无损复读师真的存在,而且越是在大厂,这种人就会越多。
PPT架构师在PPT中讲的架构各种高端大气上档次,其实就是大家很常用的部署流程;周报合并师每周的任务就是每周将团队中每个人的周报进行汇总,再报告给上级;无损复读师要求可能会高一些,对老板提出的问题或者质疑,要原原本本的向下传达给项目组对应的同学,不能有一丝偏差。
或许他们最开始不是这样的,但是慢慢地,他们活成了最舒服的,也是曾经最讨厌的样子。
17、大多数程序员是不会修电脑的。 很多行业以外的人,他们会觉得很多事情程序员都可以做,从盗QQ,Photoshop,硬盘文件恢复,到装系统,处理系统故障和软件问题,安装各种盗版软件,各种手机的越狱Root装盗版应用。
并且,另外这些事情往往不涉及实物,给人的感觉是只是在键盘上打打字,又不需要买新硬件之类的,所以往往会被认为是举手之劳,理应帮忙。
18、杀死程序员不用枪,改三次需求就可以了。 很多程序员并不反感别人说他无趣,也不反感别人说他们的穿着土鳖,也不反感别人说他们长相平庸。
也就是说,除了反复改需求,别的他们都能忍受。
先说这么多吧,总结得也算是比较全了,后续有新的,我再补充。
来源:juejin.cn/post/7292960995437166601
uniapp下各端调用三方地图导航
技术栈
- 开发框架: uniapp
- vue 版本: 2.x
- 开发框架: uniapp
- vue 版本: 2.x
需求
使用uniapp
在app端(Android,IOS)
中显示宿主机已有的三方导航应用,由用户自主选择使用哪家地图软件进行导航,选择后,自动将目标地址设为终点在导航页面。 使用uniapp
在微信小程序
中调用微信内置地图导航。
使用uniapp
在app端(Android,IOS)
中显示宿主机已有的三方导航应用,由用户自主选择使用哪家地图软件进行导航,选择后,自动将目标地址设为终点在导航页面。 使用uniapp
在微信小程序
中调用微信内置地图导航。
实现
微信小程序调用微信内置地图导航
使用uni.openLocation()
方法可直接调用,微信比较简单
传值字段
名称 说明 是否必传 latitude 纬度,范围为-90~90,负数表示南纬,使用 gcj02 国测局坐标系 是 longitude 经度,范围为-180~180,负数表示西经,使用 gcj02 国测局坐标系 是 name 位置名称 非必传,但不传不显示目标地址名称 address 地址的详细说明 非必传,但不传不显示目标地址名称详情
具体代码
经纬度需转为float数据类型
uni.openLocation({
latitude: parseFloat('地址纬度'),
longitude: parseFloat('地址经度'),
name: ‘地址名称,
address: '地址详情',
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
使用uni.openLocation()
方法可直接调用,微信比较简单
传值字段
名称 | 说明 | 是否必传 |
---|---|---|
latitude | 纬度,范围为-90~90,负数表示南纬,使用 gcj02 国测局坐标系 | 是 |
longitude | 经度,范围为-180~180,负数表示西经,使用 gcj02 国测局坐标系 | 是 |
name | 位置名称 | 非必传,但不传不显示目标地址名称 |
address | 地址的详细说明 | 非必传,但不传不显示目标地址名称详情 |
具体代码
经纬度需转为float数据类型
uni.openLocation({
latitude: parseFloat('地址纬度'),
longitude: parseFloat('地址经度'),
name: ‘地址名称,
address: '地址详情',
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
app端调用宿主机三方地图导航
步骤:
- 获取宿主机已安装的三方地图应用并显示,没有安装提示宿主机。
- 根据宿主机选择的三方地图,打开对应的三方地图进行导航。
使用plus
调用原生API知识点:
- 获取宿主机系统环境
uniapp文档:uniapp.dcloud.net.cn/api/system/…
使用uniapp
的uni.getSystemInfoSync().platform
方法获取宿主机系统环境,结果为android
、ios
。
- 获取宿主机是否安装某个应用
步骤:
- 获取宿主机已安装的三方地图应用并显示,没有安装提示宿主机。
- 根据宿主机选择的三方地图,打开对应的三方地图进行导航。
使用plus
调用原生API知识点:
- 获取宿主机系统环境
uniapp文档:uniapp.dcloud.net.cn/api/system/…
使用uniapp
的uni.getSystemInfoSync().platform
方法获取宿主机系统环境,结果为android
、ios
。
- 获取宿主机是否安装某个应用
使用H5产业联盟中的 plus.runtime.isApplicationExist
来判断宿主机是否安装指定应用,已安装返回True
,
Android平台需要通过设置appInf的pname属性(包名)进行查询。 iOS平台需要通过设置appInf的action属性(Scheme)进行查询,在iOS9以后需要添加白名单才可查询,在manifest.json文件plus->distribute->apple->urlschemewhitelist节点下添加(如urlschemewhitelist:["weixin"])。
调用示例
// Android
plus.runtime.isApplicationExist({pname: 'com.autonavi.minimap'})
// iOS
plus.runtime.isApplicationExist({action: 'iosamap://'})
- 调用系统级选择菜单显示已安装地图列表
调用示例
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: [
{title: '1'},
{title: '2'}
]
}, function (e) {
console.log("您点击的是第几个:"+e.index)
})
- 打开三方某个应用
调用示例
// Android
plus.runtime.openURL('三方应用地址', function(res){
// todo...
}, 'com.xxx.xxxapp');
// ios
plus.runtime.openURL('三方应用地址', function(res){
// todo...
});
具体代码:
<template>
<view @click.stop="handleNavigation">导航view>
template>
<script>
...
data() {
return {
// 目标纬度
latitude: '',
// 目标经度
longitude: '',
// 目标地址名称
name: '',
// 目标地址详细信息
address: '',
// 我自己的位置经纬度(百度地图需要传入自己的经纬度进行导航)
selfLocation: {
latitude: '',
longitude: ''
}
}
},
methods: {
handleNavigation() {
const _this = this
if (!this.latitude || !this.longitude || !this.name) return
// 微信
// #ifdef MP-WEIXIN
let _obj = {
latitude: parseFloat(this.latitude),
longitude: parseFloat(this.longitude),
name: this.name,
}
if (this.address) {
_obj['address'] = this.address
}
uni.openLocation({
..._obj,
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
// #endif
// #ifdef APP-PLUS
// 判断系统安装的地图应用有哪些, 并生成菜单按钮
let _mapName = [
{title: '高德地图', name: 'amap', androidName: 'com.autonavi.minimap', iosName: 'iosamap://'},
{title: '百度地图', name: 'baidumap', androidName: 'com.baidu.BaiduMap', iosName: 'baidumap://'},
{title: '腾讯地图', name: 'qqmap', androidName: 'com.tencent.map', iosName: 'qqmap://'},
]
// 根据真机有的地图软件 生成的 操作菜单
let buttons = []
let platform = uni.getSystemInfoSync().platform
platform === 'android' && _mapName.forEach(item => {
if (plus.runtime.isApplicationExist({pname: item.androidName})) {
buttons.push(item)
}
})
platform === 'ios' && _mapName.forEach(item => {
console.log(item.iosName)
if (plus.runtime.isApplicationExist({action: item.iosName})) {
buttons.push(item)
}
})
if (buttons.length) {
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: buttons
}, function (e) {
let _map = buttons[e.index - 1]
_this.openURL(_map, platform)
})
} else {
uni.showToast({
title: '请安装地图软件',
icon: 'none'
})
return
}
// #endif
},
// 打开第三方程序实际应用
openURL(map, platform) {
let _defaultUrl = {
android: {
"amap": `amapuri://route/plan/?sid=&did=&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&coord_type=wgs84&mode=driving&src=andr.baidu.openAPIdemo"`
},
ios: {
"amap": `iosamap://path?sourceApplication=fuxishan_uni_client&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&mode=driving&src=ios.baidu.openAPIdemo`
}
}
let newurl = encodeURI(_defaultUrl[platform][map.name]);
console.log(newurl)
plus.runtime.openURL( newurl, function(res){
console.log(res)
uni.showModal({
content: res.message
})
}, map.androidName ? map.androidName : '');
}
}
script>
<template>
<view @click.stop="handleNavigation">导航view>
template>
<script>
...
data() {
return {
// 目标纬度
latitude: '',
// 目标经度
longitude: '',
// 目标地址名称
name: '',
// 目标地址详细信息
address: '',
// 我自己的位置经纬度(百度地图需要传入自己的经纬度进行导航)
selfLocation: {
latitude: '',
longitude: ''
}
}
},
methods: {
handleNavigation() {
const _this = this
if (!this.latitude || !this.longitude || !this.name) return
// 微信
// #ifdef MP-WEIXIN
let _obj = {
latitude: parseFloat(this.latitude),
longitude: parseFloat(this.longitude),
name: this.name,
}
if (this.address) {
_obj['address'] = this.address
}
uni.openLocation({
..._obj,
success: function (res) {
console.log('打开系统位置地图成功')
},
fail: function (error) {
console.log(error)
}
})
// #endif
// #ifdef APP-PLUS
// 判断系统安装的地图应用有哪些, 并生成菜单按钮
let _mapName = [
{title: '高德地图', name: 'amap', androidName: 'com.autonavi.minimap', iosName: 'iosamap://'},
{title: '百度地图', name: 'baidumap', androidName: 'com.baidu.BaiduMap', iosName: 'baidumap://'},
{title: '腾讯地图', name: 'qqmap', androidName: 'com.tencent.map', iosName: 'qqmap://'},
]
// 根据真机有的地图软件 生成的 操作菜单
let buttons = []
let platform = uni.getSystemInfoSync().platform
platform === 'android' && _mapName.forEach(item => {
if (plus.runtime.isApplicationExist({pname: item.androidName})) {
buttons.push(item)
}
})
platform === 'ios' && _mapName.forEach(item => {
console.log(item.iosName)
if (plus.runtime.isApplicationExist({action: item.iosName})) {
buttons.push(item)
}
})
if (buttons.length) {
plus.nativeUI.actionSheet({ //选择菜单
title: "选择地图应用",
cancel: "取消",
buttons: buttons
}, function (e) {
let _map = buttons[e.index - 1]
_this.openURL(_map, platform)
})
} else {
uni.showToast({
title: '请安装地图软件',
icon: 'none'
})
return
}
// #endif
},
// 打开第三方程序实际应用
openURL(map, platform) {
let _defaultUrl = {
android: {
"amap": `amapuri://route/plan/?sid=&did=&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&coord_type=wgs84&mode=driving&src=andr.baidu.openAPIdemo"`
},
ios: {
"amap": `iosamap://path?sourceApplication=fuxishan_uni_client&dlat=${this.latitude}&dlon=${this.longitude}&dname=${this.name}&dev=0&t=0`,
'qqmap': `qqmap://map/routeplan?type=drive&to=${this.name}&tocoord=${this.latitude},${this.longitude}&referer=fuxishan_uni_client`,
'baidumap': `baidumap://map/direction?origin=${this.selfLocation.latitude},${this.selfLocation.longitude}&destination=name:${this.name}|latlng:${this.latitude},${this.longitude}&mode=driving&src=ios.baidu.openAPIdemo`
}
}
let newurl = encodeURI(_defaultUrl[platform][map.name]);
console.log(newurl)
plus.runtime.openURL( newurl, function(res){
console.log(res)
uni.showModal({
content: res.message
})
}, map.androidName ? map.androidName : '');
}
}
script>
最终效果图
- 微信
- 微信
- app端
最后
参考链接: H5产业联盟:http://www.html5plus.org/doc/h5p.htm… uniapp: uniapp.dcloud.net.cn/api/ 百度、高德、腾讯地图,三方APP调用其的文档。
本文初发于:blog.zhanghaoran.ren/article/htm…
来源:juejin.cn/post/7262941534528700453
这可能是开源界最好用的行为验证码工具
- 💂 个人网站: IT知识小屋
- 🤟 版权: 本文由【IT学习日记】原创、需要转载请联系博主
- 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦
写在前面
大家好,这里是IT学习日记。今日推荐项目:tianai-captcha行为验证码工具。
1000+优质开源项目推荐进度:6/1000。如需更多类型优质项目推荐,请在文章后留言。
工具简介
tianai-captcha行为验证码工具:分为 Go 和 Java 两个版本。支持多种验证方式,包括随机验证、曲线匹配、滑块验证、增强版滑块验证、旋转验证、滑动还原、角度验证、刮刮乐、文字点选、图标点选及语序点选等。
该系统能够快速集成到个人项目或系统中,显著提高开发效率。
功能展示
- 随机型验证码
- 曲线匹配验证码
- 滑动验证增强版验证码
- 滑块验证码
- 旋转验证码
- 滑动还原验证码
- 角度验验证码
- 刮刮乐验验证码
- 文字点选验证码
- 图标验证码
架构设计
tianai-captcha 验证码整体分为 生成器(ImageCaptchaGenerator)、校验器(ImageCaptchaValidator)、资源管理器(ImageCaptchaResourceManager) 其中生成器、校验器、资源管理器等都是基于接口模式实现可插拔的,可以替换为自定义实现,灵活度高
- 生成器 (ImageCaptchaGenerator)
主要负责生成行为验证码所需的图片。 - 校验器 (ImageCaptchaValidator)
主要负责校验用户滑动的行为轨迹是否合规。 - 资源管理器 (ImageCaptchaResourceManager)
主要负责读取验证码背景图片和模板图片等。
- 资源存储 (ResourceStore)
负责存储背景图和模板图。 - 资源提供者 (ResourceProvider)
负责将资源存储器中对应的资源转换为文件流。一般资源存储器中存储的是图片的 URL 地址或 ID,资源提供者则负责将 URL 或其他 ID 转换为真正的图片文件。
- 资源存储 (ResourceStore)
- 图片转换器 (ImageTransform)
主要负责将图片文件流转换成字符串类型,可以是 Base64 格式、URL 或其他加密格式,默认实现为 Base64 格式。
工具集成
引入依赖
<!-- maven 导入 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha</artifactId>
<version>1.4.1</version>
</dependency>
- 使用 ImageCaptchaGenerator生成器生成验证码
public class Test {
public static void main(String[] args) throws InterruptedException {
ImageCaptchaResourceManager imageCaptchaResourceManager = new DefaultImageCaptchaResourceManager();
ImageTransform imageTransform = new Base64ImageTransform();
ImageCaptchaGenerator imageCaptchaGenerator = new MultiImageCaptchaGenerator(imageCaptchaResourceManager,imageTransform).init(true);
/*
生成滑块验证码图片, 可选项
SLIDER (滑块验证码)
ROTATE (旋转验证码)
CONCAT (滑动还原验证码)
WORD_IMAGE_CLICK (文字点选验证码)
更多验证码支持 详见 cloud.tianai.captcha.common.constant.CaptchaTypeConstant
*/
ImageCaptchaInfo imageCaptchaInfo = imageCaptchaGenerator.generateCaptchaImage(CaptchaTypeConstant.SLIDER);
System.out.println(imageCaptchaInfo);
// 负责计算一些数据存到缓存中,用于校验使用
// ImageCaptchaValidator负责校验用户滑动滑块是否正确和生成滑块的一些校验数据; 比如滑块到凹槽的百分比值
ImageCaptchaValidator imageCaptchaValidator = new BasicCaptchaTrackValidator();
// 这个map数据应该存到缓存中,校验的时候需要用到该数据
Map<String, Object> map = imageCaptchaValidator.generateImageCaptchaValidData(imageCaptchaInfo);
}
}
- 使用ImageCaptchaValidator校验器 验证
public class Test2 {
public static void main(String[] args) {
BasicCaptchaTrackValidator sliderCaptchaValidator = new BasicCaptchaTrackValidator();
ImageCaptchaTrack imageCaptchaTrack = null;
Map<String, Object> map = null;
Float percentage = null;
// 用户传来的行为轨迹和进行校验
// - imageCaptchaTrack为前端传来的滑动轨迹数据
// - map 为生成验证码时缓存的map数据
boolean check = sliderCaptchaValidator.valid(imageCaptchaTrack, map).isSuccess();
// // 如果只想校验用户是否滑到指定凹槽即可,也可以使用
// // - 参数1 用户传来的百分比数据
// // - 参数2 生成滑块是真实的百分比数据
check = sliderCaptchaValidator.checkPercentage(0.2f, percentage);
}
}
工具获取
如果这篇文章对您有帮助,请**“彦祖们”**一定帮我点个 “关注” 和 “点赞”,这对我非常重要。我将会继续推荐更多优质项目和新闻。
来源:juejin.cn/post/7391351326153965568
这样做产品,死是早晚的事!
昨天和在北京的朋友聊天,他了解到之前我做过餐饮的SAAS系统,于是问我这一块是否还能分到一杯羹!
说实话,我觉得没机会,特别是对于一家小公司来说,基本上没机会,甚至连入场券都拿不到!
这不禁让我想起几年前认识的一个小公司,给他们兼职开发的两款SAAS产品,一款是连锁酒店系统,一款则是餐饮系统。
他们的酒店系统,现在在我看来依然是很牛逼的,我也去看过一些市面上的解决方案,但是依然没有他们的牛逼。
不过残酷的是,最近半年来,他们好像一套也没有卖出去,如果我没猜错的话,这几年下来,他们应该没有卖出多少套。
其实几年前我和他们协同开发,听了他们的一些想法,我就预见他们很难打出去。
因为我发现他去做了一些看似很完美,但是不是必须的功能,而且还花了大量时间去做,当时我觉得这完全就是鸡肋,现在看来是鸡骨头。
说白了,就是定位不明确,想做一个大而全方案,但是这对于一个小公司初创团队来说,这是很致命的,特别是资金不充足的情况下去干这事!
下面从几个方面去看问题。
1.定位不明确
理想一定是会被现实啪啪打脸的,当想去做一个产品的时候,不要觉得自己做得很全很大就能赢得市场,这简直是痴人说梦。
特别是在行业竞争如此之大的情况下,大公司早都入局了,人家的解决方案比你强大,价格比你便宜,售后比你全,你拿什么去拼?
当时我问他,为啥要做餐饮解决方案,你觉得你从技术,价格,服务这些方面,你有哪里比得上客如云,微盟,美团这些巨头,他说别管那么多,东西做出来自然有办法!
现在里面过去了,基本上没有任何推进。
这肯定是定位出问题了啊,不要觉得你手上有产品就能赚钱,如果是这样,那还需要销售干嘛。
对于小公司来说,大家都是技术出身,没有营销经验,就算做出产品来,也只能摆着看,如果要请销售团队,公司又支撑不起,显然矛盾了!
所以就尽量别去做这类似的产品,应该去做一些能解决别人痛点的小而美的解决方案。
就像微信公众号刚兴起的那几年,因为公众号自带的编辑器很难用,有一个人就做了一个小编辑器出来,赚得盆满钵满。
看似冷门,但是垂直!
2.陷入大而全的误区
接着上面的说。
后面有人看到看到了这个红利,就进军去做,他们希望做出更强大,功能更全的编辑器,结果花了大量时间去做,最后产品出来了,但是市场已经被别人抢了先机,最终不得不死。
这就是迷恋大而全的后果!
其实开源就是一个很好避免大而全的方案。
在开源领域,先做出一个小而美的产品,把影响力传播开,然后根据用户的需求不断迭代,这时候不是人去驱动产品了,而是需求去驱动产品。
这样做出来的产品不仅能避免出现很多无用的功能,还能节约很多的成本!
一定要让用户的需求来驱动产品的发展,而不是靠自己的臆想去决定做什么产品!
老罗当年在做锤子科技的时候,我觉得他就陷入了想去做一个大而全的产品,还陷入自己以为的漩涡,所以耗费了很多资金去研发TNT,所以导致失败。
如果那时候致力于去做好坚果系列,那么结局可能大不一样!
3.没有尝到甜头,你怎敢去做!
在我们贵州本土,有一个技术大佬,他一开始做了一个门户系统的解决方案,后续就有人来找他,说要购买他的系统,他从里面尝到了甜头!
于是就在这个领域持续深耕,最终形成了一套强大的解决方案。现在他的解决方案已经遍布全国。
他们公司基本上就是靠门户系统的解决方案来维持的。
所以,做一个产品,只有自己尝到甜头了,再去深耕,形成一套解决方案,那么成功率就会变得越高。
特别对于小公司来说,这是很重要的!
4.总结
做产品一定要忌讳大而全,也不要陷入只要我做出来了,无论如何都能分一杯羹,这是不现实的。
市场上到处是饿狼潜伏,你不过是一只小羊羔,怎么生存?
用最少的成本开发出一个小而美的解决方案,然后拿出去碰一碰,闻到味道了,再不断进击,这样成功率就高一点,即使失败了代价也不高。
今天的分享就到这里!
来源:juejin.cn/post/7313887095415324672
小小扫码枪bug引发的思考
最近新公司发生了一件bug引发思考的事
产品需求
大致如上图,一个输入框,我们制作了自定义数字键盘,input框可以回显键盘的输入,并且,可以支持扫码枪输入回显
bug描述
在win 系统没有问题,但在安卓系统:
- 每次自定义键盘输入时,还会吊起系统软键盘,且通过系统软键盘输入,input是无法回显的!
- 不支持 扫码枪输入了
最讨厌研究 系统兼容性问题了,但问题出了,就得研究
我们先看一下,自定义数字键盘是怎么实现的?
在了解自定义键盘之前,我先问问大家,键盘输入会触发哪些事件?
对,就是这三个 keydown,keypress, keyup
如何控制Input框只回显数字呢?答案就是在keyDown事件里,通过捕获 event.key来获取用户按下的物理按键的值,非数字的值直接return就能做到了
那么言归正传,自定义键盘怎么实现呢?
其实到这边我们不难想到一个解决方案的思路就是,当按下自定义键盘时,我们模拟一个 keydown事件,并向获得焦点的input 派发这个keydown事件,那么就能模拟键盘输入了
上代码:
const input = document.activeElement
const event = document.createEvent('UIEvents')
event.initUIEvent('keydown', true, true, window, 0)
event.key = key
event.keyCode = -1
input.dispatchEvent(event)
扫码枪又是个啥?
就是这个东东:
去过超市的都看过吧
用扫码枪或者其他设备扫描图形码(条形码或其他码)后将其为文本输入,
input需要识别到扫码枪输入结束,并回显input区,
其实扫码枪输入和用户键盘输入一样都可以触发keydown事件,派发给聚焦的input
那么问题来了?
怎样识别 扫码枪输入结束呢?
答案是onEnter事件
我们再来看看 安卓端出现的bug
1,为啥每次我们在自定义键盘上输入,会同时弹出系统软键盘呢??
问了下安卓侧RD,原来只要input获得焦点,系统键盘就会弹出
但是不聚焦,自定义键盘/扫码枪也没办法回显了呀?
难道真的无解了吗?这时候第n个知识点来了!用readOnly!
readonly,对,就是它,
什么?readonly不是只读吗?有了它,相当于 用户无法输入,因此无法触发系统键盘,这个可以理解,但是,加上它之后,还有焦点吗?
这里有个问题要问大家,你知道readonly和disabled的区别吗?
答案就是在交互上,readonly 仍是可以聚焦的!disabled 就不能了
并且readOnly 是禁止用户输入,所以在允许聚焦的同时,又阻止了软键盘的弹出,这时我不禁感叹: 完美!
2,安卓为啥不支持扫码枪扫码了?
我们通过调试发现,在安卓上,keyDown事件 捕获到的event.key 是 Unidentified, 被我们判定为非数字,直接return了
那解法呢?我们神奇的发现,当我们解了bug1,加上readonly后,bug2也好了!
至于为啥它也好了,具体原因我还不清楚,以下是我的猜测:
前文我们提到,只要input聚焦,软键盘就会弹出,而扫码枪其实也可以看成一个特殊的键盘,可能两个键盘冲突导致 event.key 无法识别,加上readonly禁掉 软键盘后,冲突解除,自然event.key 也可以正常识别了
清楚原因的同学可以留言给我哈!我好想知道!!
反思来了
这件问题的最终解决方案只有一行代码,一个单词: readOnly
简单到令人发指,而且这个问题是一个刚来两天的新同学搞定的
我在想这一连串的故事,太神奇了
为啥这个困扰前辈同学包括我很久的问题,一个萌新一下子就解决了呢?虽然我也是萌新
readOnly可以 解决禁止软键盘弹出,网上的答案是有的,但是我pass了这��方案,
为什么呢?
- input相关基础差,我错误的认为readOnly是只读嘛,肯定会不带焦点啊,虽然禁用了软键盘,但是 扫码枪输入也不能回显了啊
- 当我看到 event.key 是 Unidentified 时,研究重点跑偏了
- 我觉得这可能某种程度上是一种 beginer’s luck, 因为当时新同学的任务是研究如何禁用软键盘,并没有提到其他扫码枪问题,可能这种心无旁骛反而成了事
- 工作中,尤其遇到一些诡异的兼容性问题,真的需要多尝试,不要被自己的想当然绑手绑脚
- 对于兼容性问题,因为要不断尝试,最好找到一种简单方便的调试方法,会大大加快调研进度
最后还是感谢一切的发生,收获了知识,也让我有冲动分享给大家我的一点小思考,感恩感恩!
来源:juejin.cn/post/7388459061758017571
软件工程师,为什么不喜欢关电脑
💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。
概述
你是否注意到,软件工程师们似乎从不关电脑,也不喜欢关电脑?别以为他们是电脑“上瘾”,或是沉迷于电脑,这一现象背后蕴含着多种实际原因。
1、代码保存与恢复。
在编写代码过程中,遇到问题时可能会暂时离开去查阅资料或者休息,而不想打断当前的思路和工作进度。如果电脑不关机,他们可以迅速回到上次中断的地方,继续解决问题,避免了重新加载项目和找回思考线索的过程。
2、远程访问与协作。
很多软件工程师采用分布式团队协作模式,需要通过SSH等远程访问手段进行代码部署、调试或监控线上服务。下班后保持电脑开机,有利于他们在家或其他地点远程处理紧急任务。
3、持续集成/持续部署。
对于实施CI/CD流程的项目,电脑上的开发环境可能作为构建服务器的一部分,用于自动编译、测试和部署代码。在这种情况下,电脑全天候运行是必需的。
4、虚拟机与容器运行。
软件工程师使用的电脑上可能运行着虚拟机或容器,用于支持多套开发环境或者运行测试实例。这些虚拟资源,通常要求宿主机保持运行状态。
5、挂起与休眠模式。
虽然没有完全关机,但许多软件工程师会选择将电脑设置为休眠或挂起模式,这样既能节省能源,又能在短时间内快速恢复到工作状态。
实际上,以上5点归根到底,都是为了保持一个持续开发环境。那么,何为持续开发环境?
持续开发环境
持续开发环境是指软件工程师为了进行软件开发而搭建的、包含所有必要工具和服务的一套完整生态系统。它涵盖了集成开发环境(IDE)、版本控制系统(比如:Git)、本地服务器、数据库服务、构建工具以及各种编程框架和库等元素。这个环境是软件工程师日常工作的核心载体,也是他们实现高效编程、调试和测试的基础。
首先,持续开发环境通过自动化流程,极大地减少了开发过程中的人工干预。每当软件工程师提交代码到版本控制系统时,持续开发环境会自动触发构建、测试和部署流程。这意味着:软件工程师无需手动编译代码、运行测试用例或手动部署应用程序。这些繁琐的任务由持续开发环境自动完成,从而释放了软件工程师的时间和精力,让他们更专注于编写高质量的代码。
其次,持续开发环境有助于及时发现和修复问题。在持续集成的过程中,每次代码提交都会触发一次完整的构建和测试流程。这意味着:任何潜在的错误或问题都会在早期阶段被及时发现。此外,持续开发环境通常与持续监控和警报系统相结合,当出现问题时,系统会立即向团队成员发送警报,从而确保问题能够得到及时解决。
此外,持续开发环境还促进了团队协作和沟通。通过版本控制系统和自动化测试工具,团队成员可以轻松地查看彼此的代码、理解彼此的工作进度,并在出现问题时及时沟通。这种透明的工作方式有助于建立信任、减少误解,从而提高团队的整体效能。
最后,持续开发环境为创新提供了有力的支持。在快速迭代和不断试错的过程中,软件工程师可以迅速验证他们的想法和假设。如果某个功能或改进在实际应用中效果不佳,他们可以迅速调整方向,尝试新的方法。这种灵活性和敏捷性使得软件工程师能够不断尝试新的技术和方法,从而推动软件行业的创新和发展。
在这个日益复杂和快速变化的数字世界中,持续开发环境已经成为软件工程师们不可或缺的利器。但持续开发环境的搭建和启动可能耗时较长,因此为了保持工作连续性,软件工程师往往倾向于让电脑保持开机状态,以便随时可以继续编程或调试。
案例一
假设小张是一位正在开发一款大型Web应用的后端软件工程师,他的工作台的配置如下。
操作系统:Windows 10。
集成开发环境:IntelliJ IDEA,用于编写Java代码。
版本控制系统:Git,用于代码版本管理及团队协作。
本地服务器:Apache Tomcat,用于运行和测试Java Web应用。
数据库服务:MySQL,存储应用程序的数据。
构建工具:Maven,负责项目的自动化构建与依赖管理。
虚拟机环境:Docker容器,模拟生产环境以进行更真实的测试。
在每天的工作中,小张需要不断地编译代码、调试程序、提交更新到Git仓库,并在本地Tomcat服务器上验证功能是否正常。同时,他还可能需要在Docker容器内模拟不同的操作系统环境,以对软件进行兼容性测试。
如果小张下班时关闭了电脑,第二天重新启动所有服务和工具将会耗费至少半小时以上的时间。而在这段时间里,他无法立即开始编程或解决问题,影响了工作效率。
此外,小张所在的团队采用了CI/CD流程,利用Jenkins等工具自动执行代码编译、单元测试以及部署至测试服务器的任务。这就要求他的电脑作为Jenkins客户端始终在线,以便触发并完成这些自动化任务。
因此,为了确保高效流畅的开发流程,减少不必要的环境配置时间,及时响应线上问题以及支持远程协同,小张和其他许多软件工程师都会选择让自己的电脑始终保持开机状态,维持一个稳定的持续开发环境。
案例二
假设小李是一名全栈开发者,他正在参与一个大型的微服务项目,他的开发环境配置如下。
操作系统:Ubuntu 20.04 LTS。
集成开发环境:Visual Studio Code,用于编写前后端代码。
版本控制系统:Git,协同团队进行代码管理。
本地开发工具链:Node.js、NPM/Yarn用于前端开发,Python及pip用于后端开发,同时使用Kubernetes集群模拟生产环境部署。
数据库与缓存服务:MySQL作为主数据库,Redis作为缓存服务。
消息队列服务:RabbitMQ用于微服务间的异步通信。
CI/CD工具:GitHub Actions和Docker Compose结合,实现自动化构建、测试和部署。
在项目开发过程中,小李需要频繁地编译、打包、运行并测试各个微服务。一旦他关闭电脑,第二天重新启动所有服务将耗费大量时间。比如:搭建完整的Kubernetes集群可能需要数分钟到数十分钟不等,而每次重启服务都可能导致微服务间的依赖关系错乱,影响开发进度。
此外,由于团队采用了敏捷开发模式,每天都有多次代码提交和合并。为了能及时响应代码变动,小李设置了自己的电脑作为GitHub Actions的一部分,当有新的Pull Request时,可以立即触发自动化构建和测试流程,确保新代码的质量。
更进一步,在下班后或周末期间,如果线上服务出现紧急问题,小李可以通过SSH远程登录自己始终保持在线的电脑,快速定位问题所在,并在本地环境中复现和修复,然后推送到测试或生产环境,大大提高了响应速度和解决问题的效率。
综上所述,对于像小李这样的全栈开发者而言,维持一个持续稳定的开发环境是其高效工作的重要保障,也是应对复杂软件工程挑战的关键策略之一。
案例三
假设小王是一名独立游戏开发者,他正在使用Unity引擎制作一款3D角色扮演游戏,他的开发环境配置如下。
操作系统:macOS Big Sur。
集成开发环境:Unity Editor,集成了脚本编写、场景设计、动画编辑等多种功能。
版本控制系统:Perforce,用于大型项目文件的版本管理和团队协作。
资产构建工具:TexturePacker用于图片资源打包,FMOD Studio用于音频处理和混音。
本地测试环境:在电脑上运行Unity的内置播放器进行实时预览和调试。
云服务与部署平台:阿里云服务器作为远程测试和分发平台。
在游戏开发过程中,小王需要频繁地编辑代码、调整场景布局、优化美术资源并即时查看效果。由于Unity项目的加载和编译过程可能较长,尤其在处理大量纹理和模型时,如果每次关闭电脑后都要重新启动项目,无疑会大大降低工作效率。
此外,小王经常需要利用晚上或周末时间对游戏进行迭代更新,并将新版本上传到云端服务器进行远程测试。为了能在任何时刻快速响应工作需求,他的电脑始终保持开机状态,并且已连接至Perforce服务器,确保能及时获取最新的代码变更,同时也能立即上传自己的工作成果以供团队其他成员审阅和测试。
因此,对于小王这样的游戏开发者来说,保持持续开发环境不仅能有效提高日常工作效率,还能确保在非工作时段可以灵活应对突发任务,从而更好地满足项目进度要求。
总结
持续开发环境为程序员提供了一个高效、稳定且富有创新的工作环境。它通过自动化流程、及时发现问题、促进团队协作和支持创新,为软件开发带来了巨大的变革。
保持持续开发环境对于软件开发者而言至关重要,它能够显著提高工作效率,并确保项目开发的连贯性。通过维持开发环境始终在线,我们可以在任何时间方便地进行代码编辑、资源优化、实时预览和调试,并能灵活应对团队协作需求,实现快速迭代更新,从而满足项目进度要求。
来源:juejin.cn/post/7376837003520245772
种种迹象表明:前端岗位即将消失
最近,腾讯混元大模型的HR约我面试,为了确定是否真招人,我打开了腾讯内推的小程序,确实有这个岗位,但整个深圳也只有这一个。
于是,我突然意识到:在大模型时代,前端工程师这个岗位应该会是最先消失的岗位。
AI程序员的诞生
24年年初,英伟达CEO黄仁勋表示,自己相信就在不久的将来,人类再也不需要学习如何编码了,孩子们应该停止编程课。
然后24年3月,一家叫Cognition美国初创公司,发布了首个AI软件工程师Devin。它掌握全栈技能,云端部署、底层代码、改bug、训练和微调AI模型都不在话下。
只需一句指令,Devin就可端到端处理整个开发项目,这再度引发“码农是否将被淘汰”的大讨论。在SWE-bench上,它的表现远远超过Claude 2、Llama、GPT-4等选手,取得了13.86%的惊人成绩!
也就是说,它已经能通过AI公司的面试了。
接着4月,阿里发布消息称,其迎来了首位 AI 程序员——通义灵码。并在阿里云上海AI峰会上,阿里云宣布推出首个AI程序员,具备架构师、开发工程师、测试工程师等多种岗位的技能,能一站式自主完成任务分解、代码编写、测试、问题修复、代码提交整个过程,最快分钟级即可完成应用开发,大幅提升研发效率。
此次发布的AI程序员,是基于通义大模型构建的多智能体,每个智能体分别负责具体的软件开发任务并互相协作,可端到端实现一个产品功能的研发,这极大地简化了软件开发的流程。
由此带来的影响
一方面, AI技术的迅速发展和普及势必给程序员的工作带来冲击:传统的编码方式将显著改变,水平一般的程序员被取代的趋势或不可避免。
另一方面,尽管AI可以辅助程序员快速生成代码、提高开发效率,但并不能完全取代程序员的角色,尤其是技术理解深厚、能力强大的高水平程序员。
对于未来的程序员而言,掌握AI技术并应用于自己的工作流程中,与AI协同工作从而提高自己的工作效率和编码质量,是与时俱进、适应市场的必然需求。
由此,未来一名好的程序员不应仅仅是一名技术人员,还需要具备广泛的知识和技能。他们是整个人、机、环境系统框架中的创造者,要持续创新、创造价值。
具体而言,为了编写高质量代码,他们可能要精通多种编程语言;为了能按需选用合适的技术方案,他们要能迅速适应新的技术和工具。
为了面对复杂问题时能抓住原因并及时分析解决,他们必须保持与团队及客户的高效沟通协作,并不断积累知识、经验,同步跟进行业技术前沿,针对具体问题设计出创新的解决方案,保障程序的稳定性和可靠性。
所以,去年我在 从美团的开发通道合并谈谈开发的职业规划 就提出:LLM在软件工程的采用,将在众多工程领域产生突破,甚至于颠覆,由此也敦促我们必须认真审视专业能力的变迁和专业角色的定义。
为何最先消失的是前端岗
在我去年写前端学哪些技能饭碗越铁收入还高时,我还没有前端岗位可能即将消失的观点,但过去半年和很多猎头聊了一下前端岗的机会,以及看了很多后端培训课程中都包含前端的知识技能。
再结合22年我在美团内部,给几百个后端同学培训如何快速上手前端开发,我觉得前端这个岗位很有可能以后在招聘中就看不到这一细分岗位了。
其实15年前,全球应该都没有前端工程师这个岗位,当时的多数前端工作都比较简单,一部分是后端自己做,一个部分则是设计出生的切图仔完成~
后来随着移动互联网的兴起,前端开发语言发布了全新的规范ES6,整个前端开发生态逐步繁荣了起来,因为发展很快,网页的多端兼容和多版本工作比较繁杂,所以前端工作才由一个全新的岗位为负责。
原本很多前端同学在整个系统开发中就处于辅助角色,经常是多个团队的后端争抢一个专业的前端工程师,但如今,随着前端技术已经非常成熟和完善和大模型技术的加持,后端完成前端工作越来越容易。
所以,各公司自然就会减少很多前端岗位的招聘,只有少量技术比较新或业务比较复杂的项目才需要少量专职的前端工程师。
从各公司合并开发通道来看,消失的不仅是前端,还有后端和系统开发,对外招聘岗位都是软件工程师,工作内容根据需要动态调整。
总结
知识本身并不是力量,能有效将知识应用于实践才是真正的力量。同样,大量的编程知识可能是有价值的,但若不会运用、不知变通,无法解决实际问题,它就很难产生任何实质性影响。
能够有效使用程序,意味着智能体正具备将知识与学习应用转化的能力。这就需要程序员具备一些编程规则之外的能力,如分析、判断、解决问题的能力等。
程序员之所以能够不被取代,底气正在于其能将所学与实际情况相结合,并作出正确决策,而不是像AI程序员那样的编程工具,为了编程而编程。
未来,AI负责基础重复性劳动、人类程序员负责顶层设计的模式已经初露端倪,而认为人类程序员将被AI取代、沦为提要求的“边缘人”,为时尚早。
来源:juejin.cn/post/7392852233999892495
谁说forEach不支持异步代码,只是你拿不到异步结果而已
在前面探讨 forEach 中异步请求后端接口时,很多人都知道 forEach 中 async/await 实际是无效的,很多文章也说:forEach 不支持异步,forEach 只能同步运行代码,forEach 会忽略 await 直接进行下一次循环...
当时我的理解也是这样的,后面一细想好像不对,直接上我前面一篇文章用到的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
}
上面 $getListData、$getExtraInfo 都是 promise 异步方法,按照上面说的 forEach 会直接忽略掉 await,那么循环体内部拿到的 res 就应该是 undefined,后面的 res.extraInfo 应该报错才对,但是实际上代码并没有报错,说明 await 是有效的,内部的异步代码也是可以正常运行的,所以 forEach 肯定是支持异步代码的。
手写版 forEach
先从自己实现的简版 forEach 看起:
Array.prototype.customForEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}
里面会为数组的每个元素执行一下回调函数,实际拿几组数组测试和正宗的 forEach 方法效果也一样。可能很多人还是会有疑问你自己实现这到底靠不靠谱,不瞒你说我也有这样的疑问。
MDN 上关于 forEach 的说明
先去 MDN 上搜一下 forEach,里面的大部分内容只是使用层面的文档,不过里面有提到:“forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。在使用 Promise(或异步函数)作为 forEach 回调时,请确保你意识到这一点可能带来的影响”。
ECMAScript 中 forEach 规范
继续去往 javascript 底层探究,我们都知道执行 js 代码是需要依靠 js 引擎,去将我们写的代码解释翻译成计算机能理解的机器码才能执行的,所有 js 引擎都需要参照 ECMAScript 规范来具体实现,所以这里我们先去看下 ECMAScript 上关于 forEach 的标准规范:
添加图片注释,不超过 140 字(可选)
谷歌 V8 的 forEach 实现
常见的 js 引擎有:谷歌的 V8、火狐 FireFox 的 SpiderMonkey、苹果 Safari 的 JavaScriptCore、微软 Edge 的 ChakraCore...后台都很硬,这里我们就选其中最厉害的谷歌浏览器和 nodejs 依赖的 V8 引擎,V8 中对于 forEach 实现的主要源码:
transitioning macro FastArrayForEach(implicit context: Context)(
o: JSReceiver, len: Number, callbackfn: Callable, thisArg: JSAny): JSAny
labels Bailout(Smi) {
let k: Smi = 0;
const smiLen = Cast<Smi>(len) otherwise goto Bailout(k);
const fastO = Cast<FastJSArray>(o) otherwise goto Bailout(k);
let fastOW = NewFastJSArrayWitness(fastO);
// Build a fast loop over the smi array.
for (; k < smiLen; k++) {
fastOW.Recheck() otherwise goto Bailout(k);
// Ensure that we haven't walked beyond a possibly updated length.
if (k >= fastOW.Get().length) goto Bailout(k);
const value: JSAny = fastOW.LoadElementNoHole(k)
otherwise continue;
Call(context, callbackfn, thisArg, value, k, fastOW.Get());
}
return Undefined;
}
源码是 .tq 文件,这是 V8 团队开发的一个叫 Torque 的语言,语法类似 TypeScript,所以对于前端程序员上面的代码大概也能看懂,想要了解详细的 Torque 语法,可以直接去 V8 的官网上查看。
从上面的源码可以看到 forEach 实际还是依赖的 for 循环,没有返回值所以最后 return 的一个 Undefined。看完源码是不是发现咱上面的手写版也大差不差,只不过 V8 里实现了更多细节的处理。
结论:forEach 支持异步代码
最后的结论就是:forEach 其实是支持异步的,循环时并不是会直接忽略掉 await,但是因为 forEach 没有返回值,所以我们在外部没有办法拿到每次回调执行过后的异步 promise,也就没有办法在后续的代码中去处理或者获取异步结果了,改造一下最初的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
setTimeout(() => {
console.log(list)
}, 1000 * 10)
}
你会发现 10 秒后定时器中是可以按照预期打印出我们想要的结果的,所以异步代码是生效了的,只不过在同步代码中我们没有办法获取到循环体内部的异步状态。
如果还是不能理解,我们对比下 map 方法,map 和 forEach 很类似,但是 map 是有返回值的,每次遍历结束之后我们是可以直接 return 一个值,后续我们就可以接收到这个返回值。这也是为什么很多文章中改写 forEach 异步操作时,使用 map 然后借助 Promise.all 来等待所有异步操作完成后,再进行下面的逻辑来实现同步的效果。
参考文档
- MDN forEach 文档:developer.mozilla.org/zh-CN/docs/…
- ECMAScript 中 forEach 规范:tc39.es/ecma262/#se…
- 谷歌 V8 中 forEach 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 中 map 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 官网:v8.dev
- 谷歌 V8 源码:github.com/v8/v8
来源:juejin.cn/post/7389912354749087755
js如何实现当文本内容过长时,中间显示省略号...,两端正常展示
前一阵做需求时,有个小功能实现起来废了点脑细胞,觉得可以记录一下。
产品的具体诉求是:用户点击按钮进入详情页面,详情页内的卡片标题内容过长时,标题的前后两端正常展示,中间用省略号...表示,并且鼠标悬浮后,展示全部内容。
关于鼠标悬浮展示全部内容的代码就不放在这里了,本文主要写关于实现中间省略号...的代码。
实现思路
- 获取标题盒子的真实宽度, 我这里用的是clientWidth;
- 获取文本内容所占的实际宽度;
- 根据文字的大小计算出每个文字所占的宽度;
- 判断文本内容的实际宽度是否超出了标题盒子的宽度;
- 通过文字所占的宽度累加之和与标题盒子的宽度做对比,计算出要截取位置的索引;
- 同理,文本尾部的内容需要翻转一下,然后计算索引,截取完之后再翻转回来;
代码
html代码
<div class="title" id="test">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</div>
css代码: 设置文本不换行,同时设置overflow:hidden
让文本溢出盒子隐藏
.title {
width: 640px;
height: 40px;
line-height: 40px;
font-size: 14px;
color: #00b388;
border: 1px solid #ddd;
overflow: hidden;
/* text-overflow: ellipsis; */
white-space: nowrap;
/* box-sizing: border-box; */
padding: 0 10px;
}
javascript代码:
获取标题盒子的宽度时要注意,如果在css样式代码中设置了padding, 就需要获取标题盒子的左右padding值。 通过getComputedStyle
属性获取到所有的css样式属性对应的值, 由于获取的padding值都是带具体像素单位的,比如: px
,可以用parseInt特殊处理一下。
获取盒子的宽度的代码,我当时开发时是用canvas计算的,但计算的效果不太理想,后来逛社区,发现了嘉琪coder
大佬分享的文章,我这里就直接把代码搬过来用吧, 想了解的掘友可以直接滑到文章末尾查看。
判断文本内容是否超出标题盒子
// 标题盒子dom
const dom = document.getElementById('test');
// 获取dom元素的padding值
function getPadding(el) {
const domCss = window.getComputedStyle(el, null);
const pl = Number.parseInt(domCss.paddingLeft, 10) || 0;
const pr = Number.parseInt(domCss.paddingRight, 10) || 0;
console.log('padding-left:', pl, 'padding-right:', pr);
return {
left: pl,
right: pr
}
}
// 检测dom元素的宽度,
function checkLength(dom) {
// 创建一个 Range 对象
const range = document.createRange();
// 设置选中文本的起始和结束位置
range.setStart(dom, 0),
range.setEnd(dom, dom.childNodes.length);
// 获取元素在文档中的位置和大小信息,这里直接获取的元素的宽度
let rangeWidth = range.getBoundingClientRect().width;
// 获取的宽度一般都会有多位小数点,判断如果小于0.001的就直接舍掉
const offsetWidth = rangeWidth - Math.floor(rangeWidth);
if (offsetWidth < 0.001) {
rangeWidth = Math.floor(rangeWidth);
}
// 获取元素padding值
const { left, right } = getPadding(dom);
const paddingWidth = left + right;
// status:文本内容是否超出标题盒子;
// width: 标题盒子真实能够容纳文本内容的宽度
return {
status: paddingWidth + rangeWidth > dom.clientWidth,
width: dom.clientWidth - paddingWidth
};
}
通过charCodeAt返回指定位置的字符的Unicode
编码, 返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
截取和计算文本长度
// 计算文本长度,当长度之和大于等于dom元素的宽度后,返回当前文字所在的索引,截取时会用到。
function calcTextLength(text, width) {
let realLength = 0;
let index = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2 * 14; // 14是字体大小
}
// 判断长度,为true时终止循环,记录索引并返回
if (realLength >= width) {
index = i;
break;
}
}
return index;
}
// 设置文本内容
function setTextContent(text) {
const { status, width } = checkLength(dom);
let str = '';
if (status) {
// 翻转文本
let reverseStr = text.split('').reverse().join('');
// 计算左右两边文本要截取的字符索引
const leftTextIndex = calcTextLength(text, width);
const rightTextIndex = calcTextLength(reverseStr, width);
// 将右侧字符先截取,后翻转
reverseStr = reverseStr.substring(0, rightTextIndex);
reverseStr = reverseStr.split('').reverse().join('');
// 字符拼接
str = `${text.substring(0, leftTextIndex)}...${reverseStr}`;
} else {
str = text;
}
dom.innerHTML = str;
}
最终实现的效果如下:
上面就是此功能的所有代码了,如果想要在本地试验的话,可以在本地新建一个html文件,复制上面代码就可以了。
下面记录下从社区内学到的相关知识:
- js判断文字被溢出隐藏的几种方法;
- JS获取字符串长度的几种常用方法,汉字算两个字节;
1、 js判断文字被溢出隐藏的几种方法
1. Element-plus这个UI框架中的表格组件实现的方案。
通过document.createRange
和document.getBoundingClientRect()
这两个方法实现的。也就是我上面代码中实现的checkLength
方法。
2. 创建一个隐藏的div模拟实际宽度
通过创建一个不会在页面显示出来的dom元素,然后把文本内容设置进去,真实的文本长度与标题盒子比较宽度,判断是否被溢出隐藏了。
function getDomDivWidth(dom) {
const elementWidth = dom.clientWidth;
const tempElement = document.createElement('div');
const style = window.getComputedStyle(dom, null)
const { left, right } = getPadding(dom); // 这里我写的有点重复了,可以优化
tempElement.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
white-space: nowrap;
padding-left:${style.paddingLeft};
padding-right:${style.paddingRight};
font-size: ${style.fontSize};
font-family: ${style.fontFamily};
font-weight: ${style.fontWeight};
letter-spacing: ${style.letterSpacing};
`;
tempElement.textContent = dom.textContent;
document.body.appendChild(tempElement);
const obj = {
status: tempElement.clientWidth + right + left > elementWidth,
width: elementWidth - left - right
}
document.body.removeChild(tempElement);
return obj;
}
3. 创建一个block元素来包裹inline元素
这种方法是在UI框架acro design vue
中实现的。外层套一个块级(block)元素,内部是一个行内(inline)元素。给外层元素设置溢出隐藏的样式属性,不对内层元素做处理,这样内层元素的宽度是不变的。因此,通过获取内层元素的宽度和外层元素的宽度作比较,就可以判断出文本是否被溢出隐藏了。
// html代码
<div class="title" id="test">
<span class="content">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</span>
</div>
// 创建一个block元素来包裹inline元素
const content = document.querySelector('.content');
function getBlockDomWidth(dom) {
const { left, right } = getPadding(dom);
console.log(dom.clientWidth, content.clientWidth)
const obj = {
status: dom.clientWidth < content.clientWidth + left + right,
width: dom.clientWidth - left - right
}
return obj;
}
4. 使用canvas中的measureText方法和TextMetrics对象来获取元素的宽度
通过Canvas 2D渲染上下文(context)可以调用measureText方法,此方法会返回TextMetrics对象,该对象的width
属性值就是字符占据的宽度,由此也能获取到文本的真实宽度,此方法有弊端,比如说兼容性,精确度等等。
// 获取文本长度
function getTextWidth(text, font = 14) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")
context.font = font
const metrics = context.measureText(text);
return metrics.width
}
5. 使用css实现
这种方式来自评论区的掘友@S_mosar
提供的思路。
先来看下效果:
代码如下:
css部分
.con {
font-size: 14px;
color: #666;
width: 600px;
margin: 50px auto;
border-radius: 8px;
padding: 15px;
overflow: hidden;
resize: horizontal;
box-shadow: 20px 20px 60px #bebebe, -20px -20px 60px #ffffff;
}
.wrap {
position: relative;
line-height: 2;
height: 2em;
padding: 0 10px;
overflow: hidden;
background: #fff;
margin: 5px 0;
}
.wrap:nth-child(odd) {
background: #f5f5f5;
}
.title {
display: block;
position: relative;
background: inherit;
text-align: justify;
height: 2em;
overflow: hidden;
top: -4em;
}
.txt {
display: block;
max-height: 4em;
}
.title::before{
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
html部分
<ul class="con">
<li class="wrap">
<span class="txt">CSS 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 实现优惠券的技巧 - 2021-03-26">CSS 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26">CSS
测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 拖拽?</span>
<span class="title" title="CSS 拖拽?">CSS 拖拽?</span>
</li>
<li class="wrap">
<span class="txt">CSS 文本超出自动显示title</span>
<span class="title" title="CSS 文本超出自动显示title">CSS 文本超出自动显示title</span>
</li>
</ul>
思路解析:
- 文字内容的父级标签li设置
line-height: 2;
、overflow: hidden;
、height: 2em;
,因此 li 标签的高度是当前元素字体大小的2倍,行高也是当前字体大小的2倍,同时内容若溢出则隐藏。 - li 标签内部有两个 span 标签,二者的作用分别是:类名为
.txt
的标签用来展示不需要省略号时的文本,类名为.title
用来展示需要省略号时的文本,具体是如何实现的请看第五步。 - 给
.title
设置伪类before
,将伪类宽度设置为50%,搭配浮动float: right;
,使得伪类文本内容靠右,这样设置后,.title
和伪类就会各占父级宽度的一半了。 .title
标签设置text-align: justify;
,用来将文本内容和伪类的内容两端对齐。- 给伪类
before
设置文字对齐方式direction: rtl;
,将伪类内的文本从右向左流动,即right to left
,再设置溢出省略的css样式就可以了。 .title
标签设置了top: -4em
,.txt
标签设置max-height: 4em;
这样保证.title
永远都在.txt
上面,当内容足够长,.txt
文本内容会换行,导致高度从默认2em变为4em,而.title
位置是-4em
,此时正好将.txt
覆盖掉,此时显示的就是.title
标签的内容了。
知识点:text-align: justify;
- 文本的两端(左边和右边)都会与容器的边缘对齐。
- 为了实现这种对齐,浏览器会在单词之间添加额外的空间。这通常意味着某些单词之间的间距会比其他单词之间的间距稍大一些。
- 如果最后一行只有一个单词或少数几个单词,那么这些单词通常不会展开以填充整行,而是保持左对齐。
需要注意的是,
text-align: justify;
主要用于多行文本。对于单行文本,这个值的效果与text-align: left;
相同,因为单行文本无法两端对齐。
2、JS获取字符串长度的几种常用方法
1. 通过charCodeAt判断字符编码
通过charCodeAt获取指定位置字符的Unicode
编码,返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
function calcTextLength(text) {
let realLength = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2;
}
}
return realLength;
}
2. 采取将双字节字符替换成"aa"的做法,取长度
function getTextWidth(text) {
return text.replace(/[^\x00-\xff]/g,"aa").length;
};
参考文章
4. canvas绘制字体偏上不居中问题、文字垂直居中后偏上问题、measureText方法和TextMetrics对象
来源:juejin.cn/post/7329967013923962895
一文让你彻底悟透柯里化
什么是柯里化?
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术
前端为什么需要使用柯里化?
前端使用柯里化的用途主要就是简化代码结构,提高系统的维护性,一个方法只有一个参数,强制了功能的单一性,很自然就能做到功能内聚,降低耦合
一句话就是:降低代码重复率,提高代码适应性
普通函数
实现一个普通的累加函数,调用时需要传入三个参数,如果少 传则输出NaN,多传则后面的参数都无效
function add(a,b,c){
return a + b + c
}
add(1,2,3) //6
普通柯里化函数
实现一个普通的柯里化函数(含有柯里化性质),通过调用传参的方式将函数传入,并传入参数进行运算,返回一个新的函数的参数进行累计(取决于传入函数时传入参数的个数以及执行函数的传入参数进行累计)
function add(a, b, c) {
return a + b + c;
}
function fixedCurryAdd(fn) {
const arg = [].slice.call(arguments, 1);
return function () {
const newArg = arg.concat([].slice.call(arguments, 0));
return fn.apply(this, newArg);
};
}
const curryAdd = new fixedCurryAdd(add, 1);
console.log(curryAdd(2,11)); //14
柯里化函数
通过上面的含有柯里化性质的函数可以看出 ,要实现柯里化函数可以有多种传参方式,例如:
newAdd(1,2,3,4)
newAdd(1)(2,3,4)
newAdd(1)(2,3)(4)
newAdd(1)(2)(3)(4)
含有多种传参方式 ,无论哪种方式,最后都会把所需参数传入,但是柯里化函数只是期望你执行这次函数传入所需参数个数,并不强求你传入所需参数(4个,可传1个后续补上即可,最后一次凑齐4个即可)
function add(a, b, c) {
return a + b + c;
}
function CurryAdd(fn){
let arg = [].slice.call(arguments,1);
return function(){
let newArg = arg.concat([].slice.call(arguments,0));
return fn.apply(this,newArg);
}
}
function Curry(fn,length){
let len = length|| fn.length; //获取传入函数所需参数的个数
return function(){
if(arguments.length <len){
const callback = [fn].concat([].slice.call(arguments,0));
return Curry(CurryAdd.apply(this,callback),len-arguments.length);
}else{
return fn.apply(this,arguments);
}
}
}
let adds=new Curry(add)
let a = adds(1)(2)
console.log(a(1)); //4
以上完善柯里化函数的整个书写。下面来捋一下这个书写过程的思路
- 首先柯里化函数期待传入一个函数,并且返回一个函数(add)
- 通过fn.length获取当前以传入的参数个数
- 在返回函数中判断当前参数是否已传入完毕
- 如果已传入fn.length个 则直接调用传入函数
- 如果未传入fn.length个 则通过callback将fn放到第一位在进行合并arguments作为下一次进入此函数的参数,通过CurryAdd 函数对参数再进行一遍"过滤",通过递归调用自己来判断参数是否已经达到fn.length个从而实现柯里化
柯里化应用
说了这么多,那么柯里化到底能做哪些应用呢?
在前端页面中,向后端进行数据请求时,大部分都用到ajax进行请求
function ajax(method,url,data){
...ajax请求体(不作书写)
}
ajax("post","/api/getNameList",params)
ajax("post","/api/getAgeList",params)
ajax("post","/api/getSexList",params)
如果有这么多请求且每次都需要写请求方式("post"),页面多了请求多了自然成为冗余代码,那么优化一下
const newAjax = Curry(ajax);
const postAjax = newAjax("post")
...
如果url还有类似的那么就可以重复以上的代码,这样能减少相同代码重复出现
来源:juejin.cn/post/7389049604632166427
背调,程序员入职的紧箍咒
首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。
今天跟大家分享出来,给大家在求职路上避避坑。
上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴,赶紧恭喜了他,现在这年头,大厂的offer没这么好拿的。
又过了两周,小张沮丧地跟我说,这家公司是先发offer后背调,结果背调之后,offer GG了,公司HR没有告知他具体原因,只是委婉地说有缘自会再相见。(手动狗头)
我听了,惋惜之余有些惊讶,问了他具体情况。
原来,小李并没有在学历上作假,也没有做合并或隐藏工作经历的事。
他犯错的点是,由于在上家公司,他跟他老板的关系不好,所以他在背调让填写上级领导信息的时候,只写了上级领导的名字,电话留的是他一个同事的。
我听后惋惜地一拍脑门儿,说:“你这么做之前,怎么也不问我一下啊?第三方背调公司进行手机号和姓名核实,都是走系统的,秒出结果。而且,这种手机号的机主姓名造假,背调结果是亮红灯的,必挂。”
小李听后,也是悔得肠子都青了,没办法,只能重新来过了。
我以前招人的时候,遇到过一次这样的情况,当时有个候选人面试通过,发起背调流程。一周后,公司HR给了我一份该候选人背调结果的pdf,上面写着:
“候选人背调信息上提供,原公司上级为郭xx,但经查手机号主为王xx,且候选人原公司并无此人。”
背调结果,红灯,不通过。
基本面
学历信息肯定不能造假,这个大家应该都清楚,学信网不是吃素的,秒出结果。
最近两份工作的入离职时间不要出问题,这个但凡是第三方背调,近两份工作是必查项,而且无论是明察还是暗访,都会查得非常仔细,很难钻空子的。
再有就是刚才说的,手机号和人名要对上,而且这个人确实是在这家公司任职的。
大家耳熟能详的大厂最好查,背调公司都有人才数据库的,而且圈子里的人也好找。再有就是,随便找个内部员工,大厂的组织结构在内部通讯软件里都能看到的。
小厂难度大一些,如果人才数据库没有的话,背调员会从网上找公司电话,然后打给前台,让前台帮忙找人。但有的前台听了会直接挂断电话。
薪资方面,不要瞒报,一般背调公司会让你打印最近半年或一年的流水,以及纳税信息。
直接上级
这应该也是大家最关心的问题之一。
马云曾经说过:离职无非两种原因,钱没给够,心委屈了。而心委屈了,绝大多数都跟自己的直接上级有关。
如果在背调的时候,担心由于自己跟直接上级关系不好,从而导致背调结果不利的话,可以尝试以下三种方式。
第一,如果你在公司里历经了好几任领导的话,可以留关系最好的那任领导的联系方式,这个是在规则允许范围内的。
第二,如果你的直接上级只是一个小组长,而你跟大领导(类似于部门负责人)关系还可以的话,可以跟大领导沟通一下,然后背调留他的信息。像这个,一般背调公司不会深究的。
就像我的那个表哥,背调公司的老板所说的:“如果一个腾讯员工,马化腾都出来给他做背调了,那我们还能说什么呢?”
第三,如果前两点走不通的话,还可以坦诚地跟HR沟通一次,说明跟上级之间确实存在一些问题,原因是什么什么。
比如:我朋友遇到了这种情况,公司由于经营不善而裁员,老板竟然无耻地威胁我朋友,如果要N+1赔偿的话,背调就不会配合。
如果你确实不是责任方的话,一般HR也能理解。毕竟都是打工人,何苦相互为难呢。
你还可以这么加上一句:“我之前工作过的公司,您背调哪家都可以,我的口碑都很好的,唯独这家有些问题。”
btw:还有一些朋友,背调的时候留平级同事的真实电话和姓名,用来冒充领导,这个是有风险的。但是遇到背调不仔细的公司,也能通过。通过概率的话,一半一半吧。
就像我那个朋友所说:“现在人力成本不便宜,如果公司想盈利的话,我的背调员一天得完成5个背调,平均不到两个小时一个。你总不能希望他们个个都是名侦探柯南吧。”
信用与诉讼
一般来讲,背调的标准套餐还包括如下内容:金融违规、商业利益冲突、个人信用风险和有限民事诉讼。其中后两个大家尽量规避。
个人信用风险包括:网贷/逾期风险、反欺诈名单和欠税报告。
网贷这块,当时我有一个同事,2021年的时候,拿了4个offer,结果不明不白地都挂在了背调上,弄得他很懵逼。
当他问这三家公司HR原因的时候,HR都告诉他不便透露。
最后,他动用身边人脉,才联系上一家公司的HR出来吃饭,HR跟他说:“以后网贷不要逾期,尤其是不同来源的网贷多次逾期。”
同事听了,这才恍然大悟。
欠税这个,就更别说了,呵呵,大家都懂,千万别心存侥幸。
再说说劳动仲裁和民事诉讼。
现在有些朋友确实法律意识比较强,受到不公正待遇了,第一想法就是“我要去仲裁”,仲裁不满意了,就去打官司。
首先我要说的是,劳动仲裁是查不到的,所以尽量在这一步谈拢解决。
但民事诉讼在网上都是公开的,而且第三方背调公司也是走系统的,一查一个准儿。如果非必要的话,尽量不要跟公司闹到这一步。
如果真遇到垃圾公司或公司里的垃圾人,第一个想法应该是远离,不要让他们往你身上倒垃圾。
尤其是你主动跟公司打官司这种,索要个加班费、年终奖什么的,难免会让新公司产生顾虑,会不会我offer的这名候选人,以后也会有对簿公堂的一天。
结语
现在这大市场行情,求职不易,遇到入职前背调更是如履薄冰,希望大家都能妥善处理好,一定要避免节外生枝的情况发生,不要在距离成功一米的距离倒下。
最后,祝大家工作顺利,纵情向前,人人都能拿到自己满意的offer,开开心心地入职。
来源:juejin.cn/post/7295160228879204378
Python: 深入了解调试利器 Pdb
Python是一种广泛使用的编程语言,以其简洁和可读性著称。在开发和调试过程中,遇到错误和问题是不可避免的。Python为此提供了一个强大的调试工具——Pdb(Python Debugger)。Pdb是Python标准库中自带的调试器,可以帮助开发者跟踪代码执行、查看变量值、设置断点等功能。本文将详细介绍Pdb的使用方法,并结合实例展示其强大的调试能力。
1. Pdb简介
Pdb是Python内置的调试器,支持命令行操作,可以在Python解释器中直接调用。Pdb提供了一系列命令来控制程序的执行,查看和修改变量值,甚至可以在运行时修改代码逻辑。
2. 如何启动Pdb
在Python代码中启动Pdb有多种方式,以下是几种常见的方法:
2.1 在代码中插入断点
在代码中插入import pdb; pdb.set_trace()
可以在运行到该行时启动Pdb:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
import pdb; pdb.set_trace()
print(factorial(5))
2.2 通过命令行启动
可以通过命令行启动Python脚本,并在需要调试的地方使用pdb
模块:
python -m pdb myscript.py
3. Pdb的基本命令
Pdb提供了许多命令来控制调试过程,以下是一些常用命令:
b
(break
): 设置断点c
(continue
): 继续执行程序直到下一个断点s
(step
): 进入函数内部逐行执行n
(next
): 执行下一行,不进入函数内部p
(print
): 打印变量的值q
(quit
): 退出调试器
4. 实战示例
让我们通过一个具体的例子来演示Pdb的使用。假设我们有一个简单的Python脚本,用于计算列表中元素的平均值:
def average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
numbers = [1, 2, 3, 4, 5]
print(average(numbers))
4.1 设置断点并启动调试
我们希望在计算平均值之前检查total
和count
的值:
import pdb; pdb.set_trace()
def average(numbers):
total = sum(numbers)
count = len(numbers)
return total / count
numbers = [1, 2, 3, 4, 5]
print(average(numbers))
运行上述代码,当程序执行到pdb.set_trace()
时,将进入调试模式:
PS C:\src\uml\2024\07> python -m pdb myscript.py
> c:\src\uml\2024\07\myscript.py(1)<module>()
-> import pdb; pdb.set_trace()
(Pdb) n
> c:\src\uml\2024\07\myscript.py(3)<module>()
-> def average(numbers):
(Pdb) m
*** NameError: name 'm' is not defined
(Pdb) n
> c:\src\uml\2024\07\myscript.py(8)<module>()
-> numbers = [1, 2, 3, 4, 5]
(Pdb) n
> c:\src\uml\2024\07\myscript.py(9)<module>()
-> print(average(numbers))
(Pdb) n
3.0
--Return--
> c:\src\uml\2024\07\myscript.py(9)<module>()->
-> print(average(numbers))
4.2 查看变量值
在调试模式下,可以使用p
命令查看变量值:
(Pdb) p numbers
[1, 2, 3, 4, 5]
(Pdb)
通过这种方式,可以一步步检查变量的值和程序的执行流程。
5. 高级功能
除了基本命令,Pdb还提供了许多高级功能,如条件断点、调用栈查看等。
5.1 查看调用栈
使用where
命令可以查看当前的调用栈:
(Pdb) where
<frozen runpy>(198)_run_module_as_main()
<frozen runpy>(88)_run_code()
c:\users\heish\miniconda3\lib\pdb.py(1952)<module>()->
-> pdb.main()
c:\users\heish\miniconda3\lib\pdb.py(1925)main()
-> pdb._run(target)
c:\users\heish\miniconda3\lib\pdb.py(1719)_run()
-> self.run(target.code)
c:\users\heish\miniconda3\lib\bdb.py(600)run()
-> exec(cmd, globals, locals)
<string>(1)<module>()->
> c:\src\uml\2024\07\myscript.py(9)<module>()->
-> print(average(numbers))
6. 总结
Pdb是Python提供的一个功能强大的调试工具,掌握它可以大大提高代码调试的效率。在开发过程中,遇到问题时不妨多利用Pdb进行调试,找出问题的根源。通过本文的介绍,希望大家能够更好地理解和使用Pdb,为Python编程之路增添一份助力。
来源:juejin.cn/post/7392439754678321192
安卓开发转鸿蒙开发到底有多简单?
前言
相信各位搞安卓的同学多多少少都了解过鸿蒙了,有些一知半解而有些已经开始学习起来。那这个鸿蒙到底好不好搞?要不要搞?
安卓反正目前工作感觉不好找,即便是上海这样的大城市也难搞,人员挺饱和的。最近临近年关裁员的也很多。想想还是搞鸿蒙吧现在刚刚要起步说不定有机会!
首先可以肯定的一点,对于做安卓的来说鸿蒙很好搞,究竟有多好搞我来给大家说说。最近开始学鸿蒙,对其开发过程有了一定了解。刚好可以进行一些对比。
好不好搞?
开发环境
要我说,好搞的很。首先开发环境一样,不是说长得像,而是就一模一样。
你看这个DevEco-Studio和Android Studio什么关系,就是双胞胎。同样基于Intellj IDEA开发, 刚装上的时候我都惊呆了,熟悉的感觉油然而生。
再来仔细看看:
- 项目文件管理栏,同样可以切换Project和Packages视图
- 底部工具栏,文件管理,日志输出,终端,Profiler等
- SDK Manager, 和安卓一样也内建了SDK管理器,可以下载管理不同版本的SDK
- 模拟器管理器
可以看出鸿蒙开发的IDE是功能完备并且安卓开发人员可以无学习成本进行转换。
开发工具
安卓开发中需要安装Java语言支持,由于开发过程需要进行调试,adb也是必不可少的。
在鸿蒙中,安装EcoDev-Studio后,可以在IDE中选择安装Node.js即可。由于鸿蒙开发使用的语言是基于TS改进增强而来,也就是熟悉JS语言就可以上手。而会JAVA的话很容易可以上手JS
- 语言支持
- 鸿蒙上的类似adb的工具名叫hdc
hdc(HarmonyOS Device Connector)是HarmonyOS为开发人员提供的用于调试的命令行工具,通过该工具可以在windows/linux/mac系统上与真实设备或者模拟器进行交互。
- hdc list targets
- hdc file send local remote
- hdc install package File
这里列举的几个命令是不是很熟悉?一看名字就知道和安卓中的adb是对应关系。不需要去记忆,在需要使用到的时候去官网查一下就行: hdc使用指导
配置文件
安卓中最主要的配置文件是AndroidManifest.xml。 其中定义了版本号,申明了页面路径,注册了广播和服务。并且申明了App使用的权限。
而鸿蒙中也对应有配置文件,但与安卓稍有不同的是鸿蒙分为多个文件。
- build-profile.json5
Sdk Version配置在这里, 代码的模块区分也在这里
{
"app": {
"signingConfigs": [],
"compileSdkVersion": 9,
"compatibleSdkVersion": 9,
"products": [
{
"name": "default",
"signingConfig": "default",
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}
- app.json5
包名,VersionCode,VersionName等信息
{
"app": {
"bundleName": "com.example.firstDemo",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
- module.json5
模块的详细配置,页面名和模块使用到的权限在这里申明
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions":[
{
"name" : "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"inuse"
}
}
]
}
}
官方指导
安卓开发的各种技术文档在网上可以很方便的搜索到,各种demo也有基数庞大的安卓开发者在技术网站上分享。虽然鸿蒙目前处于刚起步的阶段,但是官方的技术文档目前也已经非常完善,并且可以感受到鸿蒙的官方维护团队肯定在高强度加班中,他们的文档更新的太快了。经常能看到文档的编辑日期在迅速迭代。
从日期可以看到非常新。而且文档都是中文的,学习和查找起来都特别方便。
并且不仅仅是api文档,鸿蒙官方还提供了各种用以学习的demo, 甚至还有官方的视频教程和开发论坛。
遇到问题有各种方法可以解决,查文档,看视频课程,抄官方demo, 论坛发帖提问,简直是保姆级的官方支持!
其他
- 鸿蒙的UI开发模式是一种响应式开发,与安卓的compose UI很像。组件的名字可能不同,但是概念上是一致的,并且鸿蒙的原生组件种类丰富也比较全。熟悉以后使用起来很方便。
build() {
Column() {
Text(this.accessText)
.fontSize(20)
.fontWeight(FontWeight.Bold)
if (!this.hasAccess) {
Button('点击申请').margin({top: 12})
.onClick(() => {
this.reqPermissionsFromUser(this.permissions);
})
} else {
Text('设备模糊位置信息:' + '\n' + this.locationText)
.fontSize(20)
.margin({top: 12})
.width('100%')
}
}
.height('100%')
.width('100%')
.padding(12)
}
- 对应安卓的权限管理
鸿蒙有ATM,ATM (AccessTokenManager) 是HarmonyOS上基于AccessToken构建的统一的应用权限管理能力。
- 对应安卓的SharedPreferences能力,鸿蒙有首选项能力。
这里就不一一列举了
我们只需要知道在安卓上有的概念,就可以在鸿蒙官方文档中去找一下对应的文档。
原理都是相通的。所以有过安卓开发经验的同学相对于前端FE来说有对客户端开发理解的优势。
要不要搞?
先看看目前的情况, 各家大厂正在积极布局鸿蒙客户端开发。
虽说移动端操作系统领域对安卓和iOS进行挑战的先例也有且还没有成功的先例。但是当前从国内互联网厂商的支持态度,从国际形势的情况,从华为对鸿蒙生态的投入来看。 我觉得很有搞头!
明年鸿蒙即将剔除对安卓的支持,届时头部互联网公司的大流量App也将完成鸿蒙原生纯血版的开发。
更有消息称鸿蒙PC版本也在路上了,了解信创的朋友应该能感受到这将意味着国产移动端和PC端操作系统会占有更大比例的市场。不仅仅是企业的市场行为,也是国产操作系统快速提升市占率的大好时机。
话说回来,作为安卓开发者,学习鸿蒙的成本并不高!
而对我们来说这是个机遇,毕竟技多不压身,企业在选取人才的时候往往也会偏好掌握更多技术的候选人。
如果鸿蒙起飞,你要不要考虑乘上这股东风呢?
我是张保罗,一个老安卓。最近在学鸿蒙
来源:juejin.cn/post/7308001278420320275
只会Vue的我,一入职就让用React,用了这个工具库,我依然高效
由于公司最近项目周期紧张,还有一个项目因为人手不够排不开,时间非常紧张,所以决定招一个人来。这不,经过一段时间紧张的招聘,终于招到了一个前端妹子。妹子也坦白过,自己干了3年,都是使用的Vue开发,自己挺高效的。但如果入职想用React的话,会稍微费点劲儿。我说,没事,来就是了,我们都可以教你的。
但入职后发现,这个妹子人家一点也不拖拉,干活很高效。单独分给她的项目,她比我们几个干的还快,每天下班准时就走了,任务按时完成。终于到了分享会了,组长让妹子准备准备,分享一下高效开发的秘诀。
1 初始化React项目
没想到妹子做事还挺认真,分享并没有准备个PPT什么的,而是直接拿着电脑,要给我们手动演示她的高效秘诀。而且是从初始化React项目开发的,这让我们很欣慰。
首先是初始化React项目的命令,这个相信大家都很熟悉了:
第一步:启动终端
第二步:npm install -g create-react-app
第三步:create-react-app js-tool-big-box-website
(注意:js-tool-big-box-website是我们要创建的那个项目名称)
第四步:cd js-tool-big-box-website
(注意:将目录切换到js-tool-big-box-website项目下)
第五步:npm start
然后启动成功后,可以看到这样的界面:
2 开始分享秘诀
妹子说,自己不管使用Vue,还是React,高效开发的秘诀就是 js-tool-big-box 这个前端JS库
首先需要安装一下: npm install js-tool-big-box
2.1 注册 - 邮箱和手机号验证
注册的时候,需要验证邮箱或者手机号,妹子问我们,大家平时怎么验证?我们说:不是有公共的正则验证呢,就是验证一下手机号和邮箱的格式呗,你应该在utils里加了公共方法了吧?或者是加到了表单验证里?
妹子摇摇头,说,用了js-tool-big-box工具库后,会省事很多,可以这样:
import logo from './logo.svg';
import './App.css';
import { matchBox } from 'js-tool-big-box';
function App() {
const email1 = '232322@qq.com';
const email2 = '232322qq.ff';
const emailResult1 = matchBox.email(email1);
const emailResult2 = matchBox.email(email2);
console.log('emailResult1验证结果:', emailResult1); // true
console.log('emailResult2验证结果:', emailResult2); // false
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
js-tool-big-box,使React开发更加高效
</header>
</div>
);
}
export default App;
2.2 验证密码强度值
验证密码强度值的时候呢,妹子问我们,大家平时怎么验证?我们说:不就是写个公共方法,判断必须大于几位,里面是否包含数字,字母,大写字母,特殊符号这样子吗?
妹子摇摇头,说,不是,我们可以这样来验证:
const pwd1 = '12345';
const pwd1Strength = matchBox.checkPasswordStrength(pwd1);
console.log('12345的密码强度值为:', pwd1Strength); // 0
const pwd2 = '123456';
const pwd2Strength = matchBox.checkPasswordStrength(pwd2);
console.log('123456的密码强度值为:', pwd2Strength); // 1
const pwd3 = '123456qwe';
const pwd3Strength = matchBox.checkPasswordStrength(pwd3);
console.log('123456qwe的密码强度值为:', pwd3Strength); // 2
const pwd4 = '123456qweABC';
const pwd4Strength = matchBox.checkPasswordStrength(pwd4);
console.log('123456qweABC的密码强度值为:', pwd4Strength); // 3
const pwd5 = '123@456qwe=ABC';
const pwd5Strength = matchBox.checkPasswordStrength(pwd5);
console.log('123@456qwe=ABC的密码强度值为:', pwd5Strength); // 4
2.3 登录后存localStorage
登录后,需要将一些用户名存到localStorage里,妹子问,我们平时怎么存?我们说:就是直接拿到服务端数据后,存呗。妹子问:你们加过期时间不?我们说:有时候需要加。写个公共方法,传入key值,传入value值,传个过期时间,大家不都是这样?
妹子摇摇头,说,不是,我们可以这样来存:
import { storeBox } from 'js-tool-big-box';
storeBox.setLocalstorage('today', '星期一', 1000*6);
2.4 需要判断是否手机端浏览器
我们市场需要判断浏览器是否是手机端H5浏览器的时候,大家都怎么做?我们说:就是用一些内核判断一下呗,写好方法,然后在展示之处判断一下,展示哪些组件?不是这样子吗?
妹子又问:我这个需求,老板比较重视微信内置的浏览器,这样大家写的方法是不是就比较多了?我们说,那再写方法,针对微信内置浏览器的内核做一下判断呗。
妹子摇摇头,说,那样得写多少方法啊,可以用这个方法,很全面的:
如果你单纯的只是想判断一下是否是手机端浏览器,可以这样:
import { browserBox } from 'js-tool-big-box';
const checkBrowser = browserBox.isMobileBrowser();
console.log('当前是手机端浏览器吗?', checkBrowser);
如果你需要更详细的,根据内核做一些判断,可以这样:
const info = browserBox.getBrowserInfo();
console.log('=-=-=', info);
这个getBrowserInfo方法,可以获取更详细的ua,浏览器名字,以及浏览器版本号
2.5 日期转换
妹子问,大家日常日期转换怎么做?如果服务端给的是一个时间戳的话?我们说:不就是引入一个js库,然后就开始使用呗?
妹子问:这次产品的要求是,年月日中间不是横岗,也不是冒号,竟然要求我显示这个符号 “~” ,也不是咋想的?然后我们问:你是不是获取了年月日,然后把年月日中间拼接上了这个符号呢?
妹子摇摇头,说,你可以这样:
import { timeBox } from 'js-tool-big-box';
const dateTime2 = timeBox.getFullDateTime(1719220131000, 'YYYY-MM-DD', '~');
console.log(dateTime2); // 2024~06~24
2.6 获取数据的详细类型
妹子问,大家日常获取数据的类型怎么获取?我们说,typeof呀,instanceof呀,或者是
Object.prototype.toString.call 一下呗,
妹子摇摇头,说,你可以这样:
import { dataBox } from 'js-tool-big-box';
const numValue = 42;
console.log('42的具体数据类型:', dataBox.getDataType(numValue)); // [object Number]
const strValue = 'hello';
console.log('hello的具体数据类型:', dataBox.getDataType(strValue)); // [object String]
const booleanValue = true;
console.log('true的具体数据类型:', dataBox.getDataType(booleanValue)); // [object Boolean]
const undefinedValue = undefined;
console.log('undefined的具体数据类型:', dataBox.getDataType(undefinedValue)); // [object Undefined]
const nullValue = null;
console.log('null的具体数据类型:', dataBox.getDataType(nullValue)); // [object Null]
const objValue = {};
console.log('{}的具体数据类型:', dataBox.getDataType(objValue)); // [object Object]
const arrayValue = [];
console.log('[]的具体数据类型:', dataBox.getDataType(arrayValue)); // [object Array]
const functionValue = function(){};
console.log('function的具体数据类型:', dataBox.getDataType(functionValue)); // [object Function]
const dateValue = new Date();
console.log('date的具体数据类型:', dataBox.getDataType(dateValue)); // [object Date]
const regExpValue = /regex/;
console.log('regex的具体数据类型:', dataBox.getDataType(regExpValue)); // [object RegExp]
2.8 更多
估计妹子也是摇头摇的有点累了,后来演示的就快起来了,我后来也没听得太仔细,大概有,
比如我们做懒加载的时候,判断某个元素是否在可视范围内;
比如判断浏览器向上滚动还是向下滚动,距离底部和顶部的距离;
比如某个页面,需要根据列表下载一个excel文件啦;
比如生成一个UUID啦;
比如后面还有将小写数字转为大写中文啦,等等等等
3 最后
分享完了第二天,妹子就没来,我们还准备请教她具体js-tool-big-box的使用心得呢。据说是第一天分享的时候,摇头摇得把脖子扭到了,希望妹子能早日康复,早点来上班。
最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)
js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)
来源:juejin.cn/post/7383650248265465867
绑定大量的的v-model,导致页面卡顿的解决方案
绑定大量的的v-model,导致页面卡顿的解决方案
设计图如下:
页面布局看着很简单使用element组件,那就完蛋了,因为是大量的数据双向绑定,所以使用组件,延迟非常高,高到什么程度,请求100条数据到渲染到页面上,要10-12s,特别是下拉选择的时候,延迟都在2-3s,人麻了老铁!!!
卡顿的原因很长一段时间都是在绑定v-model,为什么绑定v-model会很卡呢,请求到的每一条数据有14个数据需要绑定v-model,每次一请求就是100个打底,那就是1400个数据需要绑定v-model;而且组件本身也有延迟,所以这个方案不能采用,那怎么做呢?
我尝试采用原生去写,写着写着,哎解决了!!!惊呆了
做完后100条数据页面渲染不超过2s,毕竟还是需要绑定v-model,能在2s内,我还是能接受的吧;选择和输入延迟基本没有
下面就来展示一下我的代码,写的不好看着玩儿就好了:
请求到的数据:
methods这两个事件做的什么事儿呢,就是手动将数据绑定到数据上去也就是row上如图:
当然还有很多解决方案
来源:juejin.cn/post/7392248233222881316
那些年,我在职场中做过的蠢事
大家好,我是程序员马晓博,目前从事前端行业已有5年有余,而近期由于被裁员有一段时间,也开始回顾自己的过往,发现自己以前在职场中,做过不少傻事,这里就写篇文章来记录下。曾经的犯傻已不可避免,索性公之于众,坦然面对。
ps: 像一直戴工牌,故意露个工牌带子在外面就不说了,谁还没年轻过呢?还真谁也别笑话谁哈哈。
自诩正义强出头
这是我在腾讯实习的时候遇到的,故事很简单,有个同事加了一个全局错误捕获的逻辑,导致原本有报错但是能够正常运行的程序,出现了线上 bug。
此时团队间要追究责任,认为是加了全局捕获错误的同事的责任。从现在的视角看,加了全局错误捕获同事自然是有问题的,但是当时的我,非常正义的认为,写 bug 的人才应该承担责任,而为了让代码更健壮写了全局错误捕获的同事是没有错的。
现在回想起当时自己义正严辞的发言,真是太年轻啦。
更何况这事我连当事人都算不上,只能安慰自己:谁还没年轻过呢?
平易近领导
这个事,也是发生在腾讯实习期间。
在来实习之前的我,深受互联网扁平化管理,工位不做区别,这些非常先进的思想影响,到了腾讯之后,听领导说,自己平时喜欢游泳,我一想我也喜欢啊,就直接跑领导工位上问,你平时在哪里游泳?
吓的我的直属上级,直接跑出来拉住我,我给你推荐,我带你游泳。
还有一次是,午休一起打王者荣耀,人多了把我空出来了,我就指导领导玩亚瑟,上来就说你这出装太肉了,没一点伤害,要怎么怎么玩。
我现在还能想起来他当时的眼神,你,是在教我做事?
对于领导,毫无距离感。这应该是很多年轻人都会有的心态,还会认为这就是年轻人的本色,是互联网的特色,而且会对奉承领导的人嗤之以鼻。
当然,互联网公司文化本身也都提倡这种,没有上下级,大家都能上。也就是倡导所谓扁平化管理,工位也是领导普通员工没区别。
曾经的我认为这一切都是问题不大的,就是扁平化,互联网就是不一样,但是透过一些其他行业的人,我虽不介意工位情况,但是难免对其底层所宣扬的扁平化产生一定的怀疑。
ps: 这里放一个其他行业的人的对互联网工位的一些看法,我妹妹(中国移动打过一段时间工)来参观了我的工位之后说了一句话:这是工位?牛马间吧这是,我不以为然。直到我看到了她的工位,差别还真不是一般的大,好歹有隔间。这里应该放一张图,但是我没有,大家自行脑补吧哈哈。
自诩性格真诚直率
可谓是初生牛犊不怕虎吧,看到认为拉垮的代码,就会找同事当面聊,应该怎么怎么样,不应该怎么怎么写。
但是实际上,代码写成你认为的不合理的样子,往往是很多因素导致的,或工期,或对方当时也是初学者,或团队风格,或当时环境,或仅仅是对方对新方案的尝试。
上下文不了解,就开始吐槽。但是实际上人家的代码线上运行毕竟没有问题,没有故障的代码,本身就是一份合格的代码以及对方能力的认证。
而我的当面不友好交流,真是一点礼貌没有。还美名其曰,性格比较直率真诚。
好在当时的同事比较友好,并未计较,还在我后来选房子的时候还提供了很大的帮助,可叹没有仔细聆听对方的教诲终究还是没有十全十美。
夜郎自大而不自知
这是在我工作两年左右时候产生的一种感觉,觉得自己完成业务没有任何压力,而且还承担了一些比较重要的工作,从而有一种觉得自己很行的错觉。
但是当时面试很快就泼了一盆冷水,一般来讲,这个阶段做业务的同学,应对业务开发其实基本都没有什么问题。
但是国内对程序员的面试根本不限于业务,深挖一些知识点,理解其原理才是及格线。
而当时的我就是一直停留在使用阶段,用好本身没有问题,但是奈何不足以应对面试。
当然,心态还是最重要的。半瓶水晃荡而不自知才是最可怕的。
开弓来个回头箭
这个事,说实话有点羞于启齿。
是我工作大概第四个年头发生的,那时我在网易工作有一年多一些。
由于自己做了一个还算比较有技术难度的项目,想要寻求晋升,结果当时的晋升期答辩都结束了自己还不知晓。
心里有闷气,就开始面试找工作,也顺利拿到了几个涨幅非常不错的 offer。
开始跟上级提离职,哈哈,对方聊了下,也答应了。
结果我自己晚上就是睡不着,始终觉得自己这个时候走,是逃避,是逃兵。而且这个时候走,之前的积累就全部白费,新公司还得从零做起。
网上都说开弓没有回头箭。但是我就还是厚着脸皮来个回头箭。
不得不说,这个决定并不算蠢事,我在整件事里最蠢的是没有想好就和上级提了离职,虽然拿了 offer,但是没有想清楚就离职,是非常不成熟的表现。
好在我的上级,也主打一个真诚,也明确说明,想清楚了就行。
接下来一年的合作非常愉快,既有可视化埋点平台这样的业务技术都有挑战的项目,也有团队状态管理方案的产出,顺利在第二年迎来了自己的晋升。
这一次,愚蠢更多是在于自己没有想清楚就开弓,而真诚待人在我看来是双向必杀技,但真诚也为我后来吃亏埋下了种子。
整体而言,在网易的几年,领导,同事,大家都比较真诚,不屑于暗地里去做一些掉份的事情,也让我在职场上,形成了真诚而缺少防范的一个问题,这在我的下一步职业生涯中,给我带来了比较大的打击和跟头。
和同事交往讲真诚
这是我在离开网易后,选择的一家规模比较小的公司。
这时候,我工作已经整整 5年了,但是我过往的经历终究让我缺少了一些对同事的防范,大公司还好,大家相互之间,利益冲突不大,更多的是合作关系,同时由于大家或多或少都有自己的一点点的"骄傲",所以其实并没有遇到一些因为利益冲突而导致的暗箭。
而过往的经历也在告诉我,真诚,并不会带来什么问题。
真诚无错,但是说者无意听者有心。
到新公司之后,也到了该带人的职级,此时,我还是主打真诚,很快就和团队融为一片。
几个关系近的同事和下属,知道我家里买了几套房,知道我平时看的书,知道我平时都在干啥,知道我对生活和工作的态度,知道我在工作上的安排。
这些事情,平时没有什么问题,但是当和有心的同事出现利益冲突的时候,这些事情就成为一把利剑,间接导致我失去了这份工作。
而这些利剑,是我亲手递给了对方。
对职场恶意的容忍
如果说真诚是给别人递了一把利剑,那么自己的容忍和锋芒的隐藏,是我自己收起了盾牌。
我在周围的同事身上,总能看到自己的影子,所以对于他们的恶意,往往有一定程度的容忍,我觉得,年轻人嘛,有点锋芒,很正常。
比如,当他们吹嘘自己写了一篇文章,获得了几个赞的时候,我往往是进行倾听并表示赞赏,虽然几个赞的文章其实真的很简单。又或者公开场合提出质疑,虽然我会讲道理,理可以辩明,但是对于这其中的恶意,我一般会选择包容。
但是就是这一步,自身锋芒的隐藏,在对方眼里却是得寸进尺的机会。
个人觉得,作为级别比对方高的,还是需要适时的漏出自身的锋芒,而不是仅仅倾听加赞赏,同时由于私下交往的密集,更导致对方的肆无忌惮。
从而亲手递给对方利剑,又自己收起盾牌。
只能说,在这条路上,我还是太稚嫩。
最后
以上,就是我个人认为在职场中,做过的一些蠢事。虽然已经工作了五年之久,但是这条路上,还是觉得太过稚嫩,谨以此文,纪念哪些蠢事!
ps: 不知道看完这篇的你,有没有回忆起一些类似的事情呢?欢迎交流哈。
// 还是那句话,都年轻过,谁也别笑话谁~
来源:juejin.cn/post/7357994849386102836
从20k到50k再到2k,聊聊我在互联网干前端的这几年
大家好,我是程序员晓博,目前从事前端行业已经有将近 6年。这六年,从最初的互联网鼎盛时期,到今年是未来十年内最好的一年,再到疫情时期的回暖,再到如今年年都喊寒冬的寒冬。从最初的 20k,到最近的一份 50k 的工作,再到如今的 "政府补贴" 2k,可谓是感悟颇多。
刚好最近 gap 一段时间,有所空闲,就整理下这几年的经历以及我所看到的行业的兴衰。
学生见闻
我是 2011 年读的大学,当时是电子科学与技术这个专业,并非计算机科班出身,更偏向于硬件编程,单片机,嵌入式,FPGA 这些会更多一些。
所以当时对于互联网行业的前端后端,并没有特别明确的概念,也对于 c++, java 这些语言的地位和适用性其实也没有明确的认知。
记得是 2012, 2013 年的时候,学校里经常有 java 培训班的宣传,说实话,那会还看不上 java,虽然自己也不会,但是学校里教的都是 c, c++, java 那会在我看来,更多的是一个 c++ 简化后的语言,所以对于我这个非科班的并没有提起兴趣。
现在回想起来,那时可真是入行的好时期,当然也是风云变换的几年。那会学校流传着一个段子: 你只要会安装 eclipse,就能找到一份美团的工作。而之后的一年,你得开发过自己的 app,才能找到安卓开发工作。
不过当时更多的观念告诉我至少得读个研究生出来,所以我选择了读研而非直接工作。可以说是错过了互联网飞速发展的黄金时期,直接毕业就来到了今年是未来十年里最好的一年。
也就是在研究生阶段,我才慢慢了解到,外界的互联网大厂,其实已经分化出了移动端,前端,后端这样的岗位,当时的前端圈最为活跃,而移动端,后端,似乎都已经定型。而前端圈的新框架此起彼伏,从 react, vue, webpack,还有很多已经消失在历史中的框架。
在当时的就业情况下,前端的工资似乎是最高的,在加上当时的前端圈确实很活跃,而学习起来也比较简单。作为非科班的学生,自学前端上手最快,所以我选择了前端作为自己的就业方向。专业对口的硬件开发就不说了,工资实在是大相径庭。
但是话说回来,硬件开发如今的热门程度,并不亚于当时的软件开发。硬件开发培训,挑战 30w 年薪这样的培训班,在 2022 年左右也出现了,一如当时 2015 年左右 java 开发包就业那样的火热。
不过这个专业,其实还给我带来了一份现在看起来可以称为副业的东西:代写课程设计和毕业设计,因为是比互联网前后端更细分的赛道,所以竞争并不激烈,我还是接到了不少的单子,但是由于自己本身也是学生,所以定价很低,基本按照 100/h 的费用在收,也有做代码复用的整合,但是在硬件这一块,它的售后并不像软件这样,往往需要花费时间帮用户在板子上走通,这一部分是比较花费时间的。也在研究生阶段尝试过转项目的方式来获取收益,但是由于定价过低以及单子并不多的问题,而没有继续。不过如今想来,借助 chatgpt 等 AI 工具,定价确实还能更低(尤其是包含论文的单子)。
第一份实习
出于提升自身竞争力的考虑,我在研究生阶段就开始了边自学,边找实习。好在自学的时间比较早,准备的也比较充分,顺利拿到了腾讯等几家公司的实习。
当时虽说还处于互联网发展的时期,但是竞争其实就已经比较激烈了,没有实习进大厂基本就是 hard 模式,我也是面试了 n 家公司,才拿到的 offer。
不过腾讯这个部门虽然在面试的时候,会问一些比较现代技术的问题,但是实际进去后,是写的 php 和 jquery。我的收获其实并不多,但是简历上好看一点,后来也顺利拿到了转正的 offer。
当时给的薪资应该是 16k 左右,还会加上城市的补贴大概 2k。不过最终因为房价的原因,并没有考虑留在深圳。
ps:我在实习的时候专门考察了深圳腾讯总部附近的房价,好像是 6w,确认过眼神,是掏光 6 个钱包也买不起的房子。不过据说一度涨到了 10w,现在没有再关注了,可能有所下跌吧。
说起来当时还有一个事让我印象比较深刻,也因此对阿里有了一些抵触。就是 2017:众多应届生被阿里毁了 offer。而对于这种事情,阿里给的解释是:拥抱变化。
可能马爸爸从那时就嗅到了危机,但这却是我第一次听说毁应届生 offer,非常败好感。ps: 现在这种毁应届生 offer 的事是非常常见啦。
似乎那时警钟已经敲响,但是我并没有未雨绸缪。
第一份工作
我是 2018 年毕业的,那会北京,上海,都有落户的限制,甚至还有一些积分制等似乎不欢迎应届生去上班的感觉。
那会刚毕业,可谓是心比天高,落个户都这么麻烦,我还不想去呢!还不如人深圳的口号,来了就是深圳人。而当时阿里的总部,就在杭州,而且杭州只要是大学生,立马就能落户,立马能摇号买房。而当时房价也比较亲民 (确认过眼神,是掏光钱包可以买得起的价格)。
所以基本上只找杭州的工作。最终入职了当时比较热门的 p2p 领域的独角兽,51 信用卡。
当时的 51信用卡,可以说是 p2p 领域的一只牛逼独角兽,甚至这家公司的缩写就是 51NB。
不过以我当时的认知,入职 51信用卡,纯粹是因为 20k 的薪资,以及全额报销来回路费。要知道当时 BAT 虽然有报销,但是实际上都有各种限制和上限。
ps: 以我当时的认知,几乎没有任何犹豫,我就关闭了我的副业通道,因为我觉得,精进前端技术,带来的收益更大,毕竟一个月 20k 的收入,更别提还有 4个月加的年终收入了。而这份副业,一方面对主业没有提升,同时还要消耗比较大的精力(主要集中在给学生讲解代码以及售后上),收入也就几千块而且时间比较集中,很难兼顾。
现在回过头来看,真的是误打误撞赶上了 p2p 行业的末班车。起始薪资确实不错,但是很快就来到了国家严控 p2p 行业的开端。
最终,入职当年,就遇到了一波一波的裁员,从开始的 n + 3,到 n + 2 再到 n + 1,可谓是一波一波的裁员。也包括了应届生。一如现在,应届生也还是裁员重灾区。
也因为 51 当时是杭州互联网第一波开启的裁员,还裁了应届生。口碑急转直下,但是很快就迎来了反转,隔壁滴滴,微店等也迅速开启裁员模式,仅仅只有 n + 1。
ps: 在 51 的第一年,是有年会的。第二年,年会倒是有,但是主题就是一句话,今年将是接下来十年内,最好的一年。也是这一年 2019,p2p 彻底宣告结束,51 也出现了警车上门的事件。最终借贷业务转型为依赖于银行的借贷业务。也结束了接近 10% 的储蓄利率时代。
短暂的阿里之旅
2020 年年初,p2p 行业宣告结束叠加疫情之初,悲观情绪四处蔓延。我在 51 的旅程也渐渐走到了尾声。
当时面试了字节和阿里。彼时的字节跳动,在杭州名气和规模还没有如今这么大。权衡之下选择了名气更盛,当时口碑更好的阿里。但是我对于字节的判断,实在是偏差的离谱,看着如今蒸蒸日上的字节,真是后悔莫及。
但是进了阿里,说实话是真有些不适应。
一方面生活上,不提供纸巾,让我颇为诧异,而时不时的团队聚餐竟然是 AA 也让我非常不适应。
当然,对方看我也很奇怪,说了一句话,感觉你是外企来的 (ps: 如今的 51信用卡还是有点小而美的感觉,各项业务依托也还在继续,也有露营等新业务的开拓,老板自由后也还在继续折腾着)。
因为疫情的原因,我并没有经历百阿培训。但是有一本小册子,写着价值观。让我印象深刻的是,"此时此刻,非我莫属" 和 "不难,要你做什么"。
这两句话听起来都没有什么问题,鼓励人奋进并没有任何问题,但是以前的奋斗,伴随着可能的巨大的回报,而我当时的付出与回报,显然已经是大打折扣了。
那时还没有 pua 的说法,但是确实有一些话让我觉得不舒服,比如目标要跳起来才是 3.5,蹦起来才是 3.75,以及业务好和你一点关系都没有,你把业务做好了,也只能给你 3.25。必须得出一些技术项目,才能拿到好绩效。
而那种没有人会点明但是大家都在执行的道理:和我 kpi 有关的就是天,无关的就是已读不回,而已读不回,就是拒绝。更是让当时还非常稚嫩的我想要逃之夭夭。
这些也不能说错,但是这确实和我在 5信用卡当时围着业务转的风格大相径庭。
说回技术,阿里整个集团的基建可以说非常好,反而我所在的这个小前端团队的基建,赶不上 51前端团队的基建。
不论是脚手架,发布系统(比较让我震惊的是我当时团队的发布是自己丢文件到服务器上,测试正式环境的区分还是靠手动维护的一份文件),开发流程,完全赶不上 51当时的丝滑程度。
可以说是对压力的逃避,也可以说是对这种环境的不适应,也可以说是对涨幅的不满,我很快就开启了下一段旅程。
现在回过头来看,当时离职,还是冲动占了大部分。一方面,随着后来业务接触的多了,能够理解当时那个小团队的基建差的原因:主要是业务形态,当时的小团队是 toB 的,要维护的仅仅是一个项目,自然在发布流程上的投入不会太多,而且收益也是远远不及 51 这种移动端几十上百个工程的发布工作来的实在。
而另一方面,所谓的深夜开会,不明说但是心里都清楚的加班氛围,以及唯 kpi 导向的风气,其实也不过是一种生存规则而已。强行说服自己接受也很容易。毕竟人生如戏,适应规则,利用规则,掌握规则,但凡能够想通这一点,当时坚持下来也非常容易。
长达三年的网易之旅
当处于阿里的水深火热之中时,一个在周末就完成了全部流程的网易团队,向我抛出了橄榄枝。
经过短暂的调整,我也就入职了这个团队,不曾想一待就是三年。此时的薪资来到了 30k 左右,但是由于当时这个团队独特的奖金制,月收入会比 base 高出不少。
团队的业务主要是直播,所以 toB 和 toC 的业务都有。基建上也比较完善,发布系统,组件库,脚手架,微前端,等等,相对更为繁荣。
这个团队并没有明确的技术项目的考核,还是以业务为主,大多数人,完成业务开发目标,就能够顺利拿到 3.5 的绩效,同时由于当时直播行业的繁荣,基本都会有一笔不菲的奖金。而技术项目属于锦上添花,确确实实能在最终的绩效上有所体现但是并不多。
但是恰恰是在这样的环境下,组内的同学在相对宽松的氛围下,更热衷于鼓捣技术项目,反而平时对技术的研究及讨论会更多一些。
这三年,也算是见证了业务的兴衰,从开始的营收暴涨开始出海,到最终营收暴跌收缩,到裁员。也不过短短三年。
现在回头来看,在这三年里,对于业务的了解更多的还是停留在表层,虽然当时觉得自己理解业务方的需求了,但是其实内部的很多玩法还是远非仅仅理解需求就能接触到的,什么大 R 运营,"军火商" 等等秀场直播的黑话,我是没有学到一点。
由于组内业务还算比较综合,c端页面的开发,b端后台都有所接触。同时业务之余还会有很多时间去做一些技术项目,比如我负责的 CloudIDE, WebIDE, 可视化埋点项目, 基于 zustand 的状态管理库, 均是这一时期的产物。
整体来讲,这三年不论是工作节奏,还是技术产出,都还算可以。
但是如今回过头来看,似乎这三年,对外界的关注,基本上有了一定的钝感,不像之前,对互联网的各个信息都会去了解看一下。反而这几年,说内敛沉稳也好,说闭门造车也好,说停留在自己的舒适圈内也好,除了技术层面的精进,对于整个行业的发展,都太过闭塞,仿佛只是重复一种舒适的生活过了三年:每天和老婆一起轮流开车上下班,顺便再健个身,住着自己的房子,还着公积金就能覆盖还有结余的带款。
如今回想起来,也正是这三年的经历,让我在技术上有所精进,但是对互联网行业的关注,反而有所下降。同时由于同事间的关系比较简单,也让我在人际交往上变得更加朴素真诚。
半年的小公司之旅
怎么说呢,好像人总是在不稳定的时候追求稳定,在稳定的时候追求不稳定。
所以在结束了网易的三年相对稳定的工作之后,我内心反而变得很躁动,想要去小公司,谋一番事业。
出来看机会之后,才发现外界的环境其实并没有平时了解的那么糟糕,确实不像之前机会那么多,但是确实也还有一些岗位。
在这之中,我选择了在发展业务第二曲线同时又有第一业务支持的说稳定又不稳定的公司 ---- 爱普拉维。
这家公司业务主要集中在海外,所以整体业务情况也还是非常客观。给出的薪资也比较客观,我的薪资也在这一时期,达到了 50k 左右。
不过入职之初,就经历了一些人事变动,如今想来,可以说是警醒,但是我应该是选择性的进行了忽视。心思沉浸在技术和一点点的管理上。
这个团队前端同学并不多,但是业务上除了常规的 h5 和少量的后台项目之外,还会存在一些 chrome 扩展逆向,爬虫项目的存在,而我被招进来的主要任务,也就是 chrome 扩展的逆向和爬虫项目。
在这一期间,我一度沉浸在了技术上的钻研中,从 webpack 的解码逆向,到 puppeteer 爬虫的实现,从 项目秒开的优化,到 svelte 的重构,都是对我之前技术经验的一个补充。也顺利在技术角度上在公司站稳了脚跟。度过了这个公司网上传言的不好过的试用期。
不过终归还是在人际交往上有所欠缺,叠加上公司的业务方向调整,导致了最终今年 1月份的离职。而这,也为我的职场画下了短暂的暂停键。
离职快小半年了
不知不觉离离职已经快小半年了,也顺利领到了失业金,也就是题目中提到的 2k。
这段时间从刚开始的玩乐,到中途的读书写文章,再到一些副业(对于无业人员来讲应该是主业)的探索。焦虑在所难免,未来也还比较迷茫,而其他主业的探索,说实话也没探索出来什么结果。
反倒是这段读书的时间给了我一些收获,一方面是 《穷爸爸富爸爸》中对于资产负债表的解释,我自己也做了一份,还参加了财富流沙盘游戏,对自己的财务状况有了更好的认知。另一方面便是 《认知觉醒》中关于焦虑的说法,一定程度上命中了当下的自己很多。
最后,就用《认知觉醒》中关于焦虑的根源来结束这篇文章吧:想同时做很多事,又想立即看到效果。自己的欲望大于能力,又极度缺乏耐心。人的天性就是避难驱易和急于求成。
ps: 避难驱易,这几个字实在太戳我了,也正是因为避难驱易,所以其实很多之前就想写的文章都拖拖拖,直到认识到是内心的避难驱易之后才开始控制自己开始输出,而也正是输出才让我注意到了自己之前没有注意到的点,才有了这篇文章以及 那些年,我在职场中做过的蠢事。
最后的最后,愿我们都有美好的未来!
来源:juejin.cn/post/7366567675315126281
uni-app 集成推送
研究了几天,终于是打通了uni-app的推送,本文主要针对的是App端的推送开发过程,分为在线推送和离线推送。我们使用uni-app官方推荐的uni-push2.0。官方文档
准备工作:开通uni-push功能
- 勾选uniPush2.0
- 点击"配置"
- 填写表单
关联服务空间说明:
uni-push2.0需要开发者开通uniCloud。不管您的业务服务器是否使用uniCloud,但实现推送,就要使用uniCloud服务器。
- 如果您的后台业务使用uniCloud开发,那理解比较简单。
- 如果您的后台业务没有使用uniCloud,那么也需要在uni-app项目中创建uniCloud环境。在uniCloud中写推送逻辑,暴露一个接口,再由业务后端调用这个推送接口。
在线推送
以上操作配置好了以后,回到HBuilderX。
因为上面修改了manifest.json配置,一定要重新进行一次云打包(打自定义调试基座和打正式包都可以)后才会生效。
客户端代码
我这边后端使用的是传统服务器,未使用云开发。要实现推送,首先需要拿到一个客户端的唯一标识,使用uni.getPushClientId API
链接地址
onLaunch() {
uni.getPushClientId({
success: (res) => {
let push_clientid = res.cid
console.log('客户端推送标识:', push_clientid)
// 保存在全局,可以在进入app登录账号后调用一次接口将设备id传给后端
this.$options.globalData.pushClientId = push_clientid
// 一进来就掉一次接口把push_clientid传给后端
this.$setPushClientId(push_clientid).then(res => {
console.log('[ set pushClientId res ] >', res)
})
},
fail(err) {
console.log(err)
}
})
}
客户端监听推送消息
监听推送消息的代码,需要在收到推送消息之前被执行。所以应当写在应用一启动就会触发的应用生命周期
onLaunch
中。
//文件路径:项目根目录/App.vue
export default {
onLaunch: function() {
console.log('App Launch')
uni.onPushMessage((res) => {
console.log("收到推送消息:",res) //监听推送消息
})
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
服务端代码
- 鼠标右击项目根目录,依次执行
- 然后右击uniCloud目录,选择刚开始创建的云服务空间
- 在cloudfunctions目录右击,新建云函数/云对象,命名为uni-push,会创建一个uni-push目录
- 右击uni-push目录,点击 管理公共模块或扩展库依赖,选择uni-cloud-push
- 右击database目录,新建DB Schema,创建这三张表:
opendb-tempdata
,opendb-device
,uni-id-device
,也就是json文件,直接输入并选择相应的模板。
- 修改index.js
'use strict';
const uniPush = uniCloud.getPushManager({appId:"__UNI__XXXX"}) //注意这里需要传入你的应用appId
exports.main = async (event, context) => {
console.log('event ===> ', event)
console.log('context ===> ', context)
// 所有要传的参数,都在业务服务器调用此接口时传入
const data = JSON.parse(event.body || '{}')
console.log('params ===> ', data)
return await uniPush.sendMessage(data)
};
- package.json
{
"name": "uni-push",
"dependencies": {},
"main": "index.js",
"extensions": {
"uni-cloud-push": {}
}
}
- 右击uni-push目录,点击上传部署
- 云函数url化
登录云函数控制台,进入云函数详情
8. postman测试一下接口
没问题的话,客户端将会打印“console.log("收到推送消息:", xxx)”,这一步最好是使用真机,运行到App基座,使用自定义调试基座运行,会在HBuilderX控制台打印。
离线推送
APP离线时,客户端收到通知会自动在通知栏创建消息,实现离线推送需要配置厂商参数。
苹果需要专用的推送证书,创建证书参考链接
安卓需要在各厂商开发者后台获取参数,参考链接
参数配置好了以后,再次在postman测试
注意 安卓需要退出app后,在任务管理器彻底清除进程,才会走离线推送
解决离线推送没有声音
这个是因为各安卓厂商为了避免开发者滥用推送进行的限制,因此需要设置离线推送渠道,查看文档
调接口时需要传一个channel参数
实现离线推送自定义铃声
这个功能只有华为和小米支持
也需要设置channel参数,并使用原生插件,插件地址
注意 使用了原生插件,一定要重新进行一次云打包
- 华为,申请了自分类权益即可
- 小米,在申请渠道时,选择系统铃声,url为
android.resource://安卓包名/raw/铃声文件名(不要带后缀)
来源:juejin.cn/post/7267417057451573304
无框架,跨框架!时隔两年,哈啰Quark Design迎来重大特性升级!
引言
历经1年多迭代,Quarkd 2.0 版本正式发布,这是自 Quarkd 开源以来第二个重大版本。本次升级主要实现了组件外部可以穿透影子Dom,修改组件内部元素的任何样式。
- (迁移后)最新官网:quark-ecosystem.github.io/quarkd-docs
- Github 地址:github.com/hellof2e/qu…
Quark Design 介绍
Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架/无框架中。
前端各类框架技术发展多年,很多公司存量前端项目中必定存在各类技术栈。为了解决各类不同技术栈下UI交互统一,我们开发了这套UI组件库。
之前技术瓶颈
熟悉 quarkd 的开发者都知道其底层基因是 Web Components,从而实现了跨技术栈使用。但Web Components 中的 shadow dom 特性决定了其“孤岛”的特性,组件内部是个独立于外部的小世界,外部无法修改组件内部样式,若要修改内部样式,我们在 quarkd 1.x 版本中采用了 CSS 变量的方式来支援这种做法。
但这种做法依旧局限性非常大,你只能修改预设css变量的指定样式,比如你要修改 Dialog 内容中的字体大小/颜色:
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
// 内部css源码
:host .quark-dialog-content {
font-size: var(--dialog-content-font-size, 14px);
color: var(--dialog-content-color, "#5A6066");
// ... 其它样式
}
这时候,你需要在组件外部书写:
.dialog {
--dialog-content-font-size: 36px;
--dialog-content-color: red;
}
这种做法会带来一些问题,比如当源码中没有指定的css变量,就意味着你无法通过css变量从外面渗透进入组件内部去修改,比如 dialog conent 内的 font-style
。
升级后
得益于 ::part
CSS 伪元素的特性, 我们将 Quarkd 主要 dom 节点进行改造,升级后,你可以通过如下方式来自定义任何组件样式。
custom-element::part(foo) {
/* 样式作用于 `foo` 部分 */
}
::part
可以用来表示在阴影树中任何匹配 part
属性的元素。
该特性已兼容主流浏览器,详情见:mozilla.org # ::part()
用法示例:
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
.dialog::part(body) {
font-size: 24px;
color: #666;
}
.dialog::part(footer) {
font-size: 14px;
color: #333;
}
其它DEMO地址:stackblitz.com/edit/quarkd…
关于升级
Quarkd 2.x 向下兼容所有 1.x 功能及特性,之前的css变量也被保留,所以使用者可以从1.x直接升级到2.x!
One more thing
假如你也想利用 quarkd 底层能力构建属于自己的跨技术栈组件,欢迎使用:
github.com/hellof2e/qu…
最后
感谢在Quarkd迭代期间作出贡献的朋友们,感谢所有使用quarkd的开发者!
来源:juejin.cn/post/7391753478123864091
zero-privacy——uniapp小程序隐私协议弹窗组件
一. 引言
为规范开发者的用户个人信息处理行为,保障用户的合法权益,自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。
公告地址:关于小程序隐私保护指引设置的公告
developers.weixin.qq.com/miniprogram…
接下来我们将打造一个保姆级的隐私协议弹窗组件
二. 开发调试基础
划重点,看文档,别说为什么没有效果,没有弹窗
1. 更新用户隐私保护指引
小程序管理员或开发者可以根据具体小程序涉及到的隐私相关接口来更新微信小程序后台的用户隐私保护指引,更新并审核通过后就可以进行相关的开发调试工作。仅有在指引中声明所处理的用户信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将直接禁用。
- ���知道怎么填写隐私协议,看看文档:用户隐私保护指引设置developers.weixin.qq.com/miniprogram…
- 哪些api需要用户点击同意隐私协议才可以使用的看这里:小程序用户隐私保护指引内容介绍developers.weixin.qq.com/miniprogram…
审核时间有人说十几分钟,我自己的给大家参考一下。
审核通过!审核通过!审核通过后才可以开发调试。
2.配置调试字段 "__usePrivacyCheck__": true
- 在 2023 年 9 月 15 号之前,在 app.json 中配置
"__usePrivacyCheck__": true
后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。 - 在 2023 年 9 月 15 号之后,不论 app.json 中是否有配置 usePrivacyCheck,隐私相关功能都会启用。
- 所以在基于uni-app开发时,我们在 2023 年 9 月 15 号之前进行相关开发调试则需要在manifest.json文件mp-weixin中添加
"__usePrivacyCheck__": true
- manifest.json文件源码视图
"mp-weixin" : {
"__usePrivacyCheck__": true
},
3. 配置微信开发工具基础库
将调试基础库改为3.0.0以上。具体路径为:
微信开发者工具->详情->本地设置->调试基础库
以上配置完成后,即可看看效果,我在小程序后台设置了剪切板的隐私接口,果然,已经提示没有隐私授权不能使用了。
三. zero-privacy组件介绍
组件下载地址:ext.dcloud.net.cn/plugin?name…
组件的功能和特点
- 支持 居中弹出,底部弹出
- 不依赖第三方弹窗组件,内置轻量动画效果
- 支持自定义触发条件
- 支持自定义主题色
- 组件中最重要的4个api(只需用到前3个):
- wx.getPrivacySetting 查询隐私授权情况 官方链接
- wx.onNeedPrivacyAuthorization 监听隐私接口需要用户授权事件。 官方链接
- wx.openPrivacyContract 跳转至隐私协议页面 官方链接
- wx.requirePrivacyAuthorize 模拟隐私接口调用,并触发隐私弹窗逻辑 官方链接
四. zero-privacy组件使用方法
在uniapp插件市场直接下载导入 uni_modules
后使用即可
- 最直接看到弹窗效果的测试方法
<template>
<view class="container">
<zero-privacy :onNeed='false'></zero-privacy>
</view>
</template>
注意以上是测试方案,不建议实际开发中按上面的方法使用,推荐以下两种方法
- 在小程序首页等tabbar页面直接处理隐私弹窗逻辑
<template>
<view class="container">
<zero-privacy :onNeed='false' :hideTabBar='true'></zero-privacy>
</view>
</template>
- 在页面点击某些需要用到隐私协议后处理隐私弹窗逻辑
<template>
<view class="container">
<view class="btn" @click="handleCopy">
复制
</view>
<zero-privacy></zero-privacy>
</view>
</template>
- 自定义内容使用
<template>
<view class="container">
<zero-privacy title="测试自定义标题" predesc="协议前内容" privacy-contract-name-custom="<自定义名称及括号>" subdesc="协议后内容协议后内容协议后内容. 主动换行"></zero-privacy>
</view>
</template>
五. zero-privacy组件参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
position | String | center | 可选 bottom ,从底部弹出 |
color | String | #0396FF | 主颜色: 协议名和同意按钮的背景色 |
bgcolor | String | #ffffff | 弹窗背景色 |
onNeed | Boolean | true | 使用到隐私相关api时触发弹窗,设置为false时初始化弹窗将判断是否需要隐私授权,需要则直接弹出 |
hideTabBar | Boolean | false | 是否需要隐藏tabbar,在首页等tabbar页面使用改弹窗时建议改为true |
title | String | #ffffff | 用户隐私保护提示 |
predesc | String | 使用前请仔细阅读 Ï | 协议名称前的内容 |
subdesc | String | 当您点击同意后,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用该服务。 | 协议名称后的内容 |
privacyContractNameCustom | String | '' | 自定义协议名称,不传则由小程序自动获取 |
predesc
和 subdesc
的自定义内容,需要主动换行时在内容中添加实体字符
即可
六. zero-privacy组件运行效果
来源:juejin.cn/post/7273803674790150183
java就能写爬虫还要python干嘛?
爬虫学得好,牢饭吃得饱!!!切记!!!
相信大家多少都会接触过爬虫相关的需求吧,爬虫在绝大多数场景下,能够帮助客户自动的完成部分工作,极大的减少人工操作。目前更多的实现方案可能都是以python为实现基础,但是作为java程序员,咱们需要知道的是,以java 的方式,仍然可以很方便、快捷的实现爬虫。下面将会给大家介绍两种以java为基础的爬虫方案,同时提供案例供大家参考。
一、两种方案
传统的java实现爬虫方案,都是通过jsoup的方式,本文将采用一款封装好的框架【webmagic】进行实现。同时针对一些特殊的爬虫需求,将会采用【selenium-java】的进行实现,下面针对两种实现方案进行简单介绍和演示配置方式。
1.1 webmagic
官方文档:webmagic.io/
1.1.1 简介
使用webmagic开发爬虫,能够非常快速的实现简单且逻辑清晰的爬虫程序。
四大组件
- Downloader:下载页面
- PageProcessor:解析页面
- Scheduler:负责管理待抓取的URL,以及一些去重的工作。通常不需要自己定制。
- Pipeline:获取页面解析结果,数持久化。
Spider
- 启动爬虫,整合四大组件
1.1.2 整合springboot
webmagic分为核心包和扩展包两个部分,所以我们需要引入如下两个依赖:
<properties>
<webmagic.version>0.7.5</webmagic.version>
</properties>
<!--WebMagic-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>${webmagic.version}</version>
</dependency>
到此为止,我们就成功的将webmagic引入进来了,具体使用,将在后面的案例中详细介绍。
1.2 selenium-java
1.2.1 简介
selenium是一款浏览器自动化工具,它能够模拟用户操作浏览器的交互。但前提是,我们需要在使用他的机器(windows/linux等)安装上它需要的配置。相比于webmigc的安装,它要繁琐的多了,但使用它的原因,就是为了解决一些webmagic做不到的事情。
支持多种语言:java、python、ruby、javascript等。其使用代码非常简单,以java为例如下:
package dev.selenium.hello;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class HelloSelenium {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
driver.get("https://selenium.dev");
driver.quit();
}
}
1.2.2 安装
无论是在windows还是linux上使用selenium,都需要两个必要的组件:
- 浏览器(chrome)
- 浏览器驱动 (chromeDriver)
需要注意的是,要确保上述两者的版本保持一致。
下载地址
chromeDriver:chromedriver.storage.googleapis.com/index.html
windows
windows的安装相对简单一些,将chromeDriver.exe下载至电脑,chrome浏览器直接官网下载相应安装包即可。严格保证两者版本一致,否则会报错。
在后面的演示程序当中,只需要通过代码指定chromeDriver的路径即可。
linux
linux安装才是我们真正的使用场景,java程序通常是要部署在linux环境的。所以我们需要linux的环境下安装chrome和chromeDriver才能实现想要的功能。
首先要做的是判断我们的linux环境属于哪种系统,是ubuntu
、centos
还是其他的种类,相应的shell脚本都是不同的。
我们采用云原生的环境,所有的服务均以容器的方式部署,所以要在每一个服务端容器内部安装chrome和chromeDiver。我们使用的是Alpine Linux
,一个轻量级linux发行版,非常适合用来做Docker镜像。
我们可以通过apk --help
去查看相应的命令,我直接给出安装命令:
# Install Chrome for Selenium
RUN apk add gconf
RUN apk add chromium
RUN apk add chromium-chromedriver
上面的内容,可以放在DockerFile文件中,在部署的时候,会直接将相应组件安装在容器当中。
需要注意的是,在Alpine Linux中自带的浏览器是chromium
和chromium-chromedriver
,且版本相应较低,但是足够我们的需求所使用了。
/ # apk search chromium
chromium-68.0.3440.75-r0
chromium-chromedriver-68.0.3440.75-r0
1.2.3 整合springboot
我们只需要在爬虫模块引入依赖就好了:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
二、三个案例
下面通过三个简单的案例,给大家实际展示使用效果。
2.1 爬取省份街道
使用webmagic进行省份到街道的数据爬取。注意,本文只提供思路,不提供具体爬取网站信息,请同学们自己根据使用选择。
接下来搭建webmagic的架子,其中有几个关键点:
- 创建页面解析类,实现PageProcessor。
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
/**
* 页面解析
*
* @author wjbgn
* @date 2023/8/15 17:25
**/
public class TestPageProcessor implements PageProcessor {
@Override
public void process(Page page) {
}
@Override
public Site getSite() {
return site;
}
/**
* 初始化Site配置
*/
private Site site = Site.me()
// 重试次数
.setRetryTimes(3)
//编码
.setCharset(StandardCharsets.UTF_8.name())
// 超时时间
.setTimeOut(10000)
// 休眠时间
.setSleepTime(1000);
}
- 实现PageProcessor后,要重写其方法process(Page page),此方法是我们实现爬取的核心(页面解析)。通常省市区代码分为6级,所以常见的网站均是按照层级区分,我们是从省份开始爬取,即从第三层开始爬取。
- 初始化变量
@Override
public void process(Page page) {
// 市级别
Integer type = 3;
// 初始化结果明细
RegionCodeDTO regionCodeDTO = new RegionCodeDTO();
// 带有父子关系的结果集合
List<Map<String, Object>> list = new ArrayList();
// 页面所有元素集合
List<String> all = new ArrayList<>();
// 页面中子页面的链接地址
List<String> urlList = new ArrayList<>();
}
- 根据不同级别,获取相应页面不同的元素
if (CollectionUtil.isEmpty(all)) {
// 爬取所有的市,编号,名称
all = page.getHtml().css("table.citytable").css("tr").css("a", "text").all();
// 爬取所有的城市下级地址
urlList = page.getHtml().css("table.citytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 区县级别
type = 4;
all = page.getHtml().css("table.countytable").css("tr.countytr").css("td", "text").all();
// 获取区
all.addAll(page.getHtml().css("table.countytable").css("tr.countytr").css("a", "text").all());
urlList = page.getHtml().css("table.countytable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 街道级别
type = 5;
all = page.getHtml().css("table.towntable").css("tr").css("a", "text").all();
urlList = page.getHtml().css("table.towntable").css("tr").css("a", "href").all()
.stream().distinct().collect(Collectors.toList());
if (CollectionUtil.isEmpty(all)) {
// 村,委员会
type = 6;
List<String> village = new ArrayList<>();
all = page.getHtml().css("table").css("tr.villagetr").css("td", "text").all();
for (int i = 0; i < all.size(); i++) {
if (i % 3 != 1) {
village.add(all.get(i));
}
}
all = village;
}
}
}
}
- 定义一个实体类RegionCodeDTO,用来存放临时获取的code,url以及父子关系等内容:
public class RegionCodeDTO {
private String code;
private String parentCode;
private String name;
private Integer type;
private String url;
private List<RegionCodeDTO> regionCodeDTOS;
}
- 接下来对页面获取的内容(code、name、type)进行组装和临时存储,添加到children中:
// 初始化子集
List<RegionCodeDTO> children = new ArrayList<>();
// 初始化临时节点数据
RegionCodeDTO region = new RegionCodeDTO();
// 解析页面结果集all当中的数据,组装到region 和 children当中
for (int i = 0; i < all.size(); i++) {
if (i % 2 == 0) {
region.setCode(all.get(i));
} else {
region.setName(all.get(i));
}
if (StringUtils.isNotEmpty(region.getCode()) && StringUtils.isNotEmpty(region.getName())) {
region.setType(type);
// 添加子集到集合当中
children.add(region);
// 重新初始化
region = new RegionCodeDTO();
}
}
- 组装页面链接,并将页面链接组装到children当中。
// 循环遍历页面元素获取的子页面链接
for (int i = 0; i < urlList.size(); i++) {
String url = null;
if (StringUtils.isEmpty(urlList.get(0))) {
continue;
}
// 拼接链接,页面的子链接是相对路径,需要手动拼接
if (urlList.get(i).contains(provinceEnum.getCode() + "/")) {
url = provinceEnum.getUrlPrefixNoCode();
} else {
url = provinceEnum.getUrlPrefix();
}
// 将链接放到临时数据子集对象中
if (urlList.get(i).substring(urlList.get(i).lastIndexOf("/") + 1, urlList.get(i).indexOf(".html")).length() == 9) {
children.get(i).setUrl(url + page.getUrl().toString().substring(page.getUrl().toString().indexOf(provinceEnum.getCode() + "/") + 3
, page.getUrl().toString().lastIndexOf("/")) + "/" + urlList.get(i));
} else {
children.get(i).setUrl(url + urlList.get(i));
}
}
- 将children添加到结果对象当中
// 将子集放到集合当中
regionCodeDTO.setRegionCodeDTOS(children);
- 在下面的代码当中将进行两件事儿:
- 处理下一页,通过page的addTargetRequests方法,可以进行下一页的跳转,此方法参数可以是listString和String,即支持多个页面跳转和单个页面的跳转。
- 将数据传递到Pipeline,用于数据的存储,Pipeline的实现将在后面具体说明。
// 定义下一页集合
List<String> nextPage = new ArrayList<>();
// 遍历上面的结果子集内容
regionCodeDTO.getRegionCodeDTOS().forEach(regionCodeDTO1 -> {
// 组装下一页集合
nextPage.add(regionCodeDTO1.getUrl());
// 定义并组装结果数据
Map<String, Object> map = new HashMap<>();
map.put("regionCode", regionCodeDTO1.getCode());
map.put("regionName", regionCodeDTO1.getName());
map.put("regionType", regionCodeDTO1.getType());
map.put("regionFullName", regionCodeDTO1.getName());
map.put("regionLevel", regionCodeDTO1.getType());
list.add(map);
// 推送数据到pipeline
page.putField("list", list);
});
// 添加下一页集合到page
page.addTargetRequests(nextPage);
- 当本次process方法执行完后,将会根据传递过来的链接地址,再次执行process方法,根据前面定义的读取页面元素流程的代码,将不符合type=3的内容,所以将会进入到下一级4的爬取过程,5、6级别原理相同。
- 创建Pipeline,用于编写数据持久化过程。经过上面的逻辑,已经将所需内容全部获取到,接下来将通过pipline进行数据存储。首先定义pipeline,并实现其process方法,获取结果内容,具体存储数据的代码就不展示了,需要注意的是,此处pipeline没有通过spring容器托管,需要调用业务service需要使用SpringUtils进行获取:
public class RegionDataPipeline implements Pipeline{
@Override
public void process(ResultItems resultItems, Task task) {
// 获取service
IXXXXXXXXXService service = SpringUtils.getBean(IXXXXXXXXXService.class);
// 获取内容
List<Map<String, String>> list = (List<Map<String, String>>) resultItems.getAll().get("list");
// 解析数据,转换为对应实体类
// service.saveBatch
}
- 启动爬虫
//启动爬虫
Spider.create(new RegionCodePageProcessor(provinceEnum))
.addUrl(provinceEnum.getUrl())
.addPipeline(new RegionDataPipeline())
//此处不能小于2
.thread(2).start()
2.2 爬取网站静态图片
爬取图片是最常见的需求,我们通常爬取的网站都是静态的网站,即爬取的内容都在网页上面渲染完成的,我们可以直接通过获取页面元素进行抓取。
可以参考下面的文章,直接拉取网站上的图片:juejin.cn/post/705138…
针对获取到的图片网络地址,直接使用如下方式进行下载即可:
url = new URL(imageUrl);
//打开连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//设置请求方式为"GET"
conn.setRequestMethod("GET");
//超时响应时间为10秒
conn.setConnectTimeout(10 * 1000);
//通过输入流获取图片数据
InputStream is = conn.getInputStream();
2.3 爬取网站动态图片
在2.2中我们可以很快地爬取到对应的图片,但是在另外两种场景下,我们获取图片将会不适用上面的方式:
- 需要拼图,且多层的gis相关图片,此种图片将会在后期进行复杂的图片处理(按位置拼接瓦片,多层png图层叠加),才能获取到我们想要的效果。
- 动态js加载的图片,直接无法通过css、xpath获取。
所以在这种情况下我们可以使用开篇介绍的selenium-java来解决,本文使用的仅仅是截图的功能,来达到我们需要的效果。具体街区全屏代码如下所示:
public File getItems() {
// 获取当前操作系统
String os = System.getProperty("os.name");
String path;
if (os.toLowerCase().startsWith("win")) {
//windows系统
path = "driver/chromedriver.exe";
} else {
//linux系统
path = "/usr/bin/chromedriver";
}
WebDriver driver = null;
// 通过判断 title 内容等待搜索页面加载完毕,间隔秒
try {
System.setProperty("webdriver.chrome.driver", path);
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-gpu");
chromeOptions.addArguments("--window-size=940,820");
driver = new ChromeDriver(chromeOptions);
// 截图网站地址
driver.get(UsaRiverConstant.OBSERVATION_POINT_URL);
// 休眠用于网站加载
Thread.sleep(15000);
// 截取全屏
File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
return screenshotAs;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
driver.quit();
}
}
如上所示,我们获取的是整个页面的图片,还需要对截取的图片进行相应的剪裁,保留我们需要的区域,如下所示:
public static void cutImg(InputStream inputStream, int x, int y, int width, int height, OutputStream outputStream) {//图片路径,截取位置坐标,输出新突破路径
InputStream fis = inputStream;
try {
BufferedImage image = ImageIO.read(fis);
//切割图片
BufferedImage subImage = image.getSubimage(x, y, width, height);
Graphics2D graphics2D = subImage.createGraphics();
graphics2D.drawImage(subImage, 0, 0, null);
graphics2D.dispose();
//输出图片
ImageIO.write(subImage, "png", outputStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
三、小结
通过如上两个组件的简单介绍,足够应付在java领域的大多数爬取场景。从页面数据、到静态网站图片,在到动态网站的图片截取。本文以提供思路为主,原理请参考相应的官方文档。
爬虫学得好,牢饭吃得饱!!!切记!!!
来源:juejin.cn/post/7267532912617177129
领导让前端实习生在网页上添加一个长时间不操作锁定电脑的功能
前情提要
大约一个月前,公司的医疗管理系统终于完工上线。后面一个周一,领导叫大家开会,说后面没有项目进来了,用不了这么多开发人员,原地宣布裁员。再后一周后,花 2000 招了个实习生,工作内容为系统维护。
工作内容
领导:由于我们工作内容很简单,事情轻松,基本就在页面上加加按钮就行,所以工资相对较少一些,是否接受?
实习生小李:能开实习证明吗?
领导:能的。
实习生小李:好的,谢谢老板。
领导:什么时候能入职?
实习生小李:现在。
工作来源
医疗系统是一个比较数据敏感的系统,现在医院那边需要添加一个十分钟时间没有在系统进行操作,则锁定电脑的功能,使用者再次使用时,必须输入密码。客户那边在系统对接群里发出需求时,并没有人回复(PS:人都裁完了),然后老板回复到:好的。
工作安排
领导:小李,我们有个医疗系统,需要添加锁屏功能,你处理一下,两天时间应该没问题吧?
实习生小李:(思索片刻)好的,有代码吗
(4小时之后)
领导:有的,我找下
(第二天10点)
实习生小李:王总,代码找到了没有
(第二天12点)
领导:没代码改不了吗?
实习生小李:(瑟瑟发抖)我试试
(第二天14点)
实习生小李:王总,是那种长时间不操作就锁定系统的功能吗
领导:是的
实习生小李:多久不操作才锁
领导:十分钟,锁了需要输入密码才能使用
实习生小李:但是我们医疗系统没有密码功能
领导:客户电脑有密码啊
实习生小李:是锁电脑系统吗
领导:对
实习生小李:(若有所思)我试试
实现过程
实习生小李:魔镜魔镜,我们有个医疗系统,需要做一个十分钟不操作电脑,就锁定用户电脑系统的功能,在没有源代码的情况下如何实现?
魔镜:好的,在没有源代码的情况下为医疗系统添加十分钟不操作电脑就锁定用户电脑系统的功能,可以使用 sys-shim 实现。
第一步,创建一个目录例如 medical-system
,目录里有以下两个文件:
package.json
文件用来配置 sys-shimpreload.js
用来向医疗系统添加功能
第二步
在 package.json 中编写内容如下
{
"browserArguments": "--disable-web-security --allow-running-insecure-content ",
"form": {
"right": "1300",
"bottom": "800"
},
"page": "https://www.baidu.com/"
}
- browserArguments 用来指定浏览器参数,这里配置为允许跨域以方便注入代码
- form 用来控制窗口如何显示,这里表示窗口大小
- page 表示医疗系统的页面
在 preload.js 中编写内容如下
new Promise(async function () {
window.main = await new window.Sys({ log: true })
// 设置倒计时时间,为了测试方便,这里改为 30 秒
const TIMEOUT = 0.5 * 60 * 1000;
// 声明一个变量来存储 setTimeout 的引用
let timeoutId = null;
// 定义一个函数来重置倒计时并在2分钟后打印日志
function startInactivityCheck() {
// 清除之前的倒计时(如果有的话)
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// 设置一个新的倒计时
timeoutId = setTimeout(function() {
// 锁定系统
window.main.native.sys.lock()
}, TIMEOUT);
}
// 为 body 元素添加点击事件监听器
document.body.addEventListener('click', function() {
console.log("检测到点击事件,重新开始计时。");
// 重置倒计时
startInactivityCheck();
});
// 初始化倒计时
startInactivityCheck();
})
sys.lock()
方法用于锁定操作系统。
第三步,生成应用程序
npx sys-shim pack --input medical-system
运行该命令后,会在当前目录生成一个名为 medical-system.exe 的可执行文件。它封装了医疗系统这个 web 程序,并在里面添加了锁屏功能。
pack
指定表示打包应用--input
参数表示要打包的目录
--input 参数也可以是线上的网页,比如:
npx sys-shim pack --input https://www.baidu.com/
即可获取一个可以调用操作系统 api 的 web 应用。
交付反馈
用户:以前我们还需要进入浏览器输入网址才能进入系统,现在直接在桌面上就能进入,并且还有安全锁屏功能,非常好!
领导:小李干得不错,但没有在规定的时间内完成,但由于客户反馈不错,就不扣你的考核分了。
实习生小李:(不得其解)谢谢老板。
后记
不知不觉,又到了周五,这是公司技术分享会的时候。当前公司技术人员只有实习生小李,由小李负责技术分享。
宽旷的会议室里,秘书、领导、小李三人面面相觑,小李强忍住尴尬,开始了自己的第一次技术分享:
实习生小李:感谢领导给我的工作机会,在这份工作里,我发现了 sys-shim 这个工具,它可以方便的在已有的 web 页面中添加系统 api,获取调用操作系统层面功能的能力,比如关机、锁屏。
领导:(好奇)那他可以读取电脑上的文件吗?
实习生小李:可以的,它可以直接读取电脑上的文件,例如电脑里面的文档、照片、视频等。
突然领导脸色一黑,看了一眼秘书,并关闭了正在访问的医疗系统,然后在技术分享考核表上写下潦潦草草的几个字:考核分-5
。
续集:托领导大福!前端实习生用 vue 随手写了个系统修复工具,日赚 300
提示
大家可以直接运行这个命令生成 app 体验:
npx sys-shim pack --input https://www.baidu.com/
生成后的 app 可以右键解压,看到内部结构。如果遇到问题,可以在这里提交,方便追溯,我会及时解答的。
参考
来源:juejin.cn/post/7373831659470880806
领导被我的花式console.log吸引了!直接写入公司公共库!
文章的效果,大家可以直接只用云vscode实验一下:juejin.cn/post/738875…
背景简介
这几天代码评审,领导无意中看到了我本地代码的控制台,被我花里胡哨的console打印
内容吸引了!
老板看见后,说我这东西有意思,花里胡哨的,他喜欢!
但是随即又问我,这么花里胡哨的东西,上生产会影响性能吧?我自信的说:不会,代码内有判断的,只有开发环境会打印
!
老板很满意,于是让我给其他前端同事分享一下,讲解下实现思路!最终,这个方法还被写入公司的公用utils库里,供大家使用!
console简介
console 是一个用于调试和记录信息的内置对象, 提供了多种方法,可以帮助开发者输出各种信息,进行调试和分析。
console.log()
用于输出一般信息,大家应该在熟悉不过了。
console.info() :
输出信息,与 console.log 类似,但在某些浏览器中可能有不同的样式。
console.warn() :
输出警告信息,通常会以黄色背景或带有警告图标的样式显示。
console.error() :
输出错误信息,通常会以红色背景或带有错误图标的样式显示。
console.table() :
以表格形式输出数据,适用于数组和对象。
例如:
const users = [
{ name: '石小石', age: 18 },
{ name: '刘亦菲', age: 18 }
];
console.table(users);
通过上述介绍,我们可以看出,原生的文本信息、警告信息、错误信息、数组信息打印出来的效果都很普通,辨识度不高!现在我们通过console.log来实现一些花里花哨的样式!
技术方案
console.log()
console.log() 可以接受任何类型的参数,包括字符串、数字、布尔值、对象、数组、函数等。最厉害的是,它支持占位符!
常用的占位符:
- %s - 字符串
- %d or %i - 整数
- %f - 浮点数
- %o - 对象
- %c - CSS 样式
格式化字符串
console.log() 支持类似于 C 语言 printf 函数的格式化字符串。我们可以使用占位符来插入变量值。
const name = 'Alice';
const age = 30;
console.log('Name: %s, Age: %d', name, age); // Name: Alice, Age: 30
添加样式
可以使用 %c 占位符添加 CSS 样式,使输出内容更加美观。
console.log('%c This is a styled message', 'color: red; font-size: 20px;');
自定义样式的实现,其实主要是靠%c 占位符添加 CSS 样式实现的!
实现美化的信息打印
基础信息打印
我们创建一个prettyLog方法,用于逻辑编写
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
// 基础信息打印
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
return {
info
};
};
上述代码定义了一个 prettyLog 函数,用于美化打印信息到控制台。通过自定义样式,输出信息以更易读和美观的格式呈现。
我们使用一下看看效果
// 创建打印对象
const log = prettyLog();
// 不带标题
log.info('这是基础信息!');
//带标题
log.info('注意看', '这是个男人叫小帅!');
info 方法用于输出信息级别的日志。它接受两个参数:textOrTitle 和 content。如果只提供一个参数,则视为内容并设置默认标题为 Info;如果提供两个参数,则第一个参数为标题,第二个参数为内容。最后调用 prettyPrint 方法进行输出。
错误信息打印
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
// ...
};
const info = (textOrTitle: string, content = '') => {
// ...
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
// retu;
return {
info,
error,
};
};
// 创建打印对象
const log = prettyLog();
log.error('奥德彪', '出来的时候穷 生活总是让我穷 所以现在还是穷。');
log.error('前方的路看似很危险,实际一点也不安全。');
成功信息与警告信息打印
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
// retu;
return {
info,
error,
warning,
success
};
};
// 创建打印对象
const log = prettyLog();
log.warning('奥德彪', '我并非无路可走 我还有死路一条! ');
log.success('奥德彪', '钱没了可以再赚,良心没了便可以赚的更多。 ');
实现图片打印
// 美化打印实现方法
const prettyLog = () => {
// ....
const picture = (url: string, scale = 1) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
return {
info,
error,
warning,
success,
picture
};
}
// 创建打印对象
const log = prettyLog();
log.picture('https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2024%2F0514%2Fd0ea93ebj00sdgx56001xd200u000gtg00hz00a2.jpg&thumbnail=660x2147483647&quality=80&type=jpg');
上述代码参考了其他文章:Just a moment...
url可以传支持 base64,如果是url链接,图片链接则必须开启了跨域访问才能打印
实现美化的数组打印
打印对象或者数组,其实用原生的console.table比较好
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.table(data);
当然,我们也可以伪实现
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
但是,我们无法控制表格的宽度,因此,这个方法不太好用,不如原生。
仅在开发环境使用
// 美化打印实现方法
const prettyLog = () => {
//判断是否生产环境
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
// ...
};
// ...
const picture = (url: string, scale = 1) => {
if (isProduction) return;
// ...
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
我们可以通过import.meta.env.MODE 判断当前环境是否为生产环境,在生产环境,我们可以禁用信息打印!
完整代码
// 美化打印实现方法
const prettyLog = () => {
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
const picture = (url: string, scale = 1) => {
if (isProduction) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
// 创建打印对象
const log = prettyLog();
来源:juejin.cn/post/7371716384847364147
听说去哪儿混合办公了? 聊聊程序员如何找到远程工作和好处
哈喽大家好,这两天看到去哪儿开始混合办公了,作为远程工作的支持者我表示很开心,终于有大厂全面开始支持远程员工,去哪儿的邮件截图是这么说的
去年开始现在一些团队做小范围的尝试,好评如潮,有的同学利用通勤的时间减肥,有的同学可以回家陪家人,家人对工作的支持度变高,而且工作效率一点都没下降,所以去哪儿在7月开始,每周三周五可以居家办公,无需申请
远程的好处非常明显,尤其是在一线城市,通勤的一个半小时就省下了,生活幸福度直线提高,你出去旅游周四就可以出发,如果五天都可以居家就是完全远程,你可以回老家省会还有北京一线的收入,老板节省下了组办公室的费用
因为远程无论是对员工满意度,还是老板的成本控制都很友好,混合或者远程办公在海外已经比较流行了,基本海外所有的招聘网站都有是否远程的选项,你可以过滤只看支持远程的,你搜工作就有一个选项是坐班,混合还是远程的,不像国内boss直聘,默认都是要通勤的,哪怕跟我说支持远程,也是只是面试可以远程
比如英国这边混合办公室基本操作,好一些的就会全员远程,比如这边大厂,Meta是每周去两天还是三天,我给忘了,我觉得国内以后支持远程的会越来越多,越来越多的小老板会抛弃自己奴隶主的思想,必须得盯着你干活,反而会考虑更真实的办公室成本
对远程最大的批评就是会降低工作效率,所谓的见面沟通效率才是最高的,确实有一些场景面对面效率最高,但是扪心自问,你现在开会的效率真的高吗,大公司动辄就一小时的会议,而且你首先就在通勤上浪费了一小时,你一天的效率可能很高吗 ,腾讯会议的AI总结功能比以前人工写的会议纪要不知道好多少倍
而一个人能有更多的时间照顾家庭后,闲暇时间才会产生创意,尝试新的工具和沟通方式等等,可能会让效率有非常大的提高
远程我就可以工作的同事带家人出游,远程久了你换工作就会只考虑远程的,再也不想挤地铁了,虽然现在通勤还是主流,尤其是还有马斯克这种非常反对远程的人,但是我最近聊的一些创业公司和小公司,基本都支持混合办公的
说了这么多远程办公的好处,喜欢工作和生活平衡的你可能已经蠢蠢欲动了,那远 程工作需要哪些能力呢,以及如何找到一个远程工作呢
其实远程工作有很多最佳实践,比较典型的有37signals这家公司,这家公司坚持小而美,写出来ruby on rails这种框架,有一套书叫重来,有三本,第二本就叫remote,比较系统的介绍远程工作文化的方方面面
包括远程工作的好处,可以逃离大城市的房价,更好的work life balance,更自在的生活,还反驳了一些对远程文化的批评,比如觉得坐在一起才能效率高,家里干扰大等等
更重要的介绍了如何更好的远程协作,这是需要学习的技能,比如如何可视化你的工作进度,高效的沟通,怎么管理远程员工的效率,还有远程人如何更好的生活,非常推荐
还有就是有一个公司叫gitlab,这个更厉害,这是一家美国上市公司,应该是第一个招股书里没有办公地址的,从老板到实习生,全员都散落在世界各地办公,关于大型公司如何实践远程文化,他们有一个专门的文档,质量非常高,主要是关于如何管理远程团队,还有很重要的远程开会技巧
地址和上面重来的电子书,评论区好像没法发链接,要不加我吧,我研究下怎么发给大家
那最后如何找到一个远程工作呢,其实之前我也分享过,这里简单总结下
首先程序员是非常适合远程的,所有的代码任务都可以在线完成,通过git管理代码,腾讯会议或者zoom开会,飞书钉钉slack等工作聊天等等
就像前面所述国内远程机会比较少,而且很多国内的老板哪怕远程也依然是监控心态,比如要求你开摄像头或者响应速度也挺难受,所以我觉得比如你想明年找个远程,那现在就优先学英语,程序员怎么学英语大家感兴趣以后可以专门聊,大概方法就是不要学英语,而是用英语学习编程就可以了
希望大家都能尝试和探索混合或者远程工作的新体验,能够拥有一个更加自在的职场,在努力工作的同时,可以有时间陪家人探索世界
来源:juejin.cn/post/7392116075674927131
同为情怀程序员,给博客园提供几个救园思路
博客园是老牌的技术社区,近来因为各种原因,导致社区运营岌岌可危。
笔者也是一个情怀型程序员,但是在这几年的创业生涯中,也摸爬滚打,养成了一些商业思路。
这个世界,就是一个肉弱强食,尔虞我诈的,不要做一个纯粹的情怀主义者,也不要做一个老实人。
那么下面呢,基于笔者的认知和思路,给博客园提供一些救园思路。
降本增效
个人认为,这是非常核心,非常重要的问题。
收入骤减,那么我们的开销成本,也要跟着降低,才能维持更久的运转。
比如关停不必要的网站功能,减少服务器的运行开支。
作为一个技术社区,主要的金钱投入,除了人员开支,办公场地之外,应该就是服务器的运行成本了吧。
那么相应的,人员开支,办公费用,场地,该减少的减少,该换场地的就换场地。
这是降低成本,控制支出最直观的方式了。
那么接下来呢,就是如何提高收入了。
外包接单
这是博客园正在做的,当时我看到官方提成只拿5%(原文我没找到了,如有错漏,请指正),这个提成,太低了。
我自己也做接单撮合,运营2-3个月,撮合成功30-40单,平台提成是15%。
那么这个提成,市场行情是多少呢?20%-30%。
5%,说实话,太少了。
接单撮合,有一个问题非常重要:接单,客户,技术,三方都要能赚到钱
。
你拿5%,唉,还是程序员思维,还是在做情怀。
不要这样,商业就是商业,程序员作为最终实施的一方,确实出力最多,最苦,最累。
平台少拿点,给技术多一点,非常好,但是,我的个人建议是不能低于15%,为什么呢?
如果有额外的介绍人,可以给介绍人5%或一半(7.5%),这样才有余地。
如果平台赚不到钱,那么如何运营好接单撮合这个项目呢?
广告接入
这个问题,在博客园7月15日的文章中已经说明了。
我的感觉是,令人肃然起敬,但是违背社会规律。
可以这么说,为了博客园的存亡问题,至少90%以上的用户,是不反感广告的。
但是,目前来看,似乎只有网站的运营者反对广告。。。
只要不做成某C开头的社区,看个电视剧,广告比剧集还长。
所以,我突然发现,这一整个事件的根本原因是什么?居然是人的执念。。可怕。
凭博客园的流量,接入广告,分分钟救园,大家又能愉快地玩耍。
不要考验人性
笔者曾经说过一句话,在网上广为流传。
博客园是一个程序员社区。
通过卖社区周边产品,还是通过情怀变现,下策,下策啊!
既然要商业化,那就不要过多考虑情怀,要从商业的角度去思考问题。
所谓的商业,并不是变成一个尔虞我诈的商人,有太多的既盈利,又给用户带来有价值,有意义的参考,大家可以想想有哪些。
而且,不管是会员,还是周边,这根本不是一个“我尽心尽力服务多年,当我落难的时候,大家挺身而出
”的问题。
大家受益于博客园,博客园也因为所有用户而收益,这是一个相互作用。
绝大多数用户,仅仅只是看客,真正写文章,在博客园出人头地的,少之又少。
社会和技术的发展,日新月异,技术内容,也远不及当年那么必需。
周边也不是必需,会员也是一次性的,根本不具备持续性。
救了这次,下次呢?所以还得从长计议。
产品推广分成
现在,有各种“严选”,“甄选”,“优品”。
博客园也可以做严选,甄选产品。
通过合作的模式,降低产品价格,获得产品提成,也不失为一个好地方方式。
比如,某某软件,某某产品,以低于官网价格的拿货价,平台推广卖给光大社区用户。
这样,用户购买价格更低,产品方销量更好,博客园平台也赚到了钱,也是一个三方共赢的方案。
比卖会员,卖周边好。
这种方式,选品品类更多,更接近用户刚需。
甚至可以发起投票,让用户选择拼团购买什么产品,软件,工具。
既可以积累社区凝聚力,也能让用户买到真正的,需要的东西,平台也赚到了钱。
舆论维护
要倾听用户的声音,目前技术圈,对博客园的这段时间运营情况,可以说非常不满。
鱼皮的这篇文章我觉得就说得不错,这是具备真正生意头脑的创业者思路。
如果一意孤行,还是程序员的传统思路,最终很可能连口碑也坏了,三思。
网站出售
最后呢,当然,是最坏的情况,万不得已,也可以卖掉博客园,获得一笔不菲的收入。
当然,这是下下之策。
希望博客园可以挺过这次难关,不论如何,这么有情怀的社区,真的不多见了。
如果它真的消失了,那将是技术圈的重大损失。
来源:juejin.cn/post/7392071328520994816
fabric.js实战
一、业务需求
- 给定一个图片作为参考
- 可配置画笔颜色、粗细
- 可通过手势生成实际轨迹
- 可通过手势来圈选轨迹
- 可撤销、删除轨迹
- 可操作轨迹
- 可缩放、拖动
- 背景网格,且背景网格可缩放和拖动
- 参考图片可缩放、偏移
- 手势处理系统,单指绘制、双指缩放、三指拖动
- 需要一个禁止绘制区域,方便用户在平板上有手掌的支撑区域
二、技术选型
因为涉及到轨迹
、操作轨迹
两点,
svg无法满足大量且复杂的轨迹,canvas没法操作轨迹,所以从 fabricjs/konva 中选型,
因 fabricjs 使用人数更多,所以采取了其作为技术选型。
三、fabric 原理
- 通过其内置的几何对象来创建图形
- 维护一个对象树
- 将对象树通过 canvas-api 绘制在实际的 canvas 上
因此,fabric 能做非常多的优化手段
- 已渲染的节点可通过
子canvas
做缓存 - 对比新旧对象树能做差值更新
- 虚拟画布,类似于虚拟滚动,只渲染可视化的区域
四、模块拆分
- header:与业务相关
- toolBar:工具栏
- sidebar:与业务相关
- canvas:绘制画板
五、架构设计
六、问题收集
6.1 性能问题
- 圈选是实时的,即判断一个多边形是否相交于或包含于一条复杂轨迹,因为使用了射线法,当遇到大量轨迹的时候,可能会卡顿。目前做了多重优化手段,比如函数节流、先稀疏复杂轨迹的点、然后判断图形的占位区域是否相交、然后判断图形的线段之间是否相交、然后判断是否包含关系。
- 轨迹的实时生成,在一长串touchmove事件中,使用一个初始化的polyline,后续更改其点集,这样只需要实例化一个对象,性能高。
- touchmove回调里执行复杂的逻辑,这会阻塞touchmove的触发频率,我们将touchmove里的回调通过settimeout放到异步队列中,这样就剥离了touchmove
事件层
和 回调函数处理层
,这样touchmove的触发频率就不会被影响
6.2 禁止绘制区域
该需求无法实现,因为当我们手掌放在平板上时,会触发系统级别的误触识别算法,阻止所有的触摸事件,所以我们没办法在页面上实现该功能。
touch事件
一个屏幕上可以有多个touch触摸点,这些触摸点绑定的target是可以多个的
touchEvent对象有如下重要属性
- targetTouches:只在当前target(比如某div)上触发的touch触摸点
- touches:在屏幕上触发的所有touch触目点
特别注意点:
- 没有类似鼠标的mouseout事件,所以你一开始是点在div上的,然后移出div的范围后,依旧触发touchmove事件
平板调试手段-chrome浏览器
- 平板安装chrome移动版
- 电脑安装chrome + chrome插件:inspect devices
- 平板开启开发者选项,然后允许usb调试
- usb链接平板和chrome
- 平板和电脑都打开chrome
- 电脑启动插件,然后就能控制平板的chrome,并且对该chrome访问的网页进行调试
- 很好用哇
来源:juejin.cn/post/7278931998650744869
实现小红书响应式瀑布流
前言
瀑布流布局,不管是在pc端还是手机端都很常见,但是我们通常都是列固定。今天来实现一下小红书的响应式瀑布流。后面有仓库地址。
正文
还是先来看看效果
原理:
对每一个item都使用绝对定位,left和top都是0,最后根据容器大小、item的height通过计算来确定item的transform值
接下来从易到难来解析一下实现
初始化数据
列表怎么可以没有数据,先来初始化一下数据
确定列数及列大小
由于是响应式,我们要去监听列表容器的大小变化并记录容器宽度,这样才能做出相应的处理
根据监听得到的容器大小信息,我们可以确定每行个数
和每一个item的宽度
确定列表中item位置
确定item的位置,那么我们只需要确定transform
值就可以了,这也是整个实现的核心。我们还需要解决几个问题
- 对还不知道item的高度,怎么确定
- 我们希望把新的item放置在最低高度的旧item下方,这样全部渲染完每一列的高度才不会相差很多。
item放置的原理图,放置在当前最低高度的下面
更新item高度
当我们第一次运行的时候,每一个item的高度一定都是随机生成的,现在我们要确定item的实际高度。在这里我们还可优化一下,使用懒加载和底部加载,提升性能
。这两个在这里就不讲了,不懂的可以去搜一下。
下面代码一共两个作用
- 记录容器滚动值,传递给每一个
item
,用于判断是否加载图片。 - 判断是否请求添加数据
根据滚动值判断是否加载图片,加载图片后触发父亲更新高度函数
父亲接受到新的高度并更新高度,然后去重新计算transform
值和item高度
完整代码
结语
感兴趣的可以去试试
来源:juejin.cn/post/7270160291411886132
明明 3 行代码即可轻松实现,Promise 为何非要加塞新方法?
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 观前须知
地球人都知道,JS 中的异步编程是 单线程 的,和其他多线程语言的三观一龙一猪。因此,虽然其他语言的异步模式彼此互通有无,但对 JS 并不友好,比如 Actor 模型等。
这并不是说 JS 被异步社区孤立了,只是因为 JS 天生和多线程八字不合。你知道的,要求 JS 使用多线程,就像要求香菜恐惧症患者吃香菜一样离谱。本质上而言,这是刻在 JS 单线程 DNA 里的先天基因,直接决定了 JS 的“异步性状”。有趣的是,如今 JS 也变异出若干多线程的使用场景,只是比较非主流。
ES6 之后,JS 的异步编程主要基于 Promise
设计,比如人气爆棚的 fetch
API 等。因此,最新的 ES2024 功能里,又双叒叕往 Promise
加塞了新型静态方法 Promise.withResolvers()
,也就见怪不怪了。
问题在于,我发现这个新方法居然只要 3 行代码就能实现!奥卡姆剃刀原则告诉我们, 若无必要,勿增实体。那么这个鸡肋的新方法是否违背了奥卡姆剃刀原则呢?我决定先质疑、再质疑。
当然,作为应试教育的漏网之鱼,我很擅长批判性思考,不会被第一印象 PUA。经过三天三夜的刻意练习,机智如我发现新方法果然深藏不露。所以,本期我们就一起来深度学习 Promise
新方法的技术细节。
01. 静态工厂方法
Promise.withResolvers()
源自 tc39/proposal-promise-with-resolvers
提案,是 Promise
类新增的一个 静态工厂方法。
静态的意思是,该方法通过 Promise
类调用,而不是通过实例对象调用。工厂的意思是,我们可以使用该方法生成一个 Promise
实例,而无须求助于传统的构造函数 + new
实例化。
可以看到,这类似于 Promise.resolve()
等语法糖。区别在于,传统构造函数实例化的对象状态可能不太直观,而这里的 promise
显然处于待定状态,此外还“买一送二”,额外附赠一对用于改变 promise
状态的“变态函数” —— resolve()
和 reject()
。
ES2024 之后,该方法可以作为一道简单的异步笔试题 —— 请你在一杯泡面的时间里,实现一下 Promise.withResolvers()
。
如果你是我的粉丝,根本不慌,因为新方法的基本原理并不复杂,参考我下面的实现,简单给面试官表演一下就欧了。
可以看到,这个静态工厂方法的实现难点在于,如何巧妙地将变态函数暴露到外部作用域,其实核心逻辑压缩后有且仅有 3 行代码。
这就引发了本文开头的质疑:新方法是否多此一举?难道负责 JS 标准化的 tc39 委员会也有绩效考核,还是确实存在某些不为人知的极端情况?
02. 技术细节
通过对新方法进行苏格拉底式的“灵魂拷问”和三天三夜的深度学习,我可以很有把握地说,没人比我更懂它。
首先,与传统的构造函数实例化不同,新方法支持无参构造,我们不需要在调用时传递任何参数。
可以看到,构造函数实例化要求传递一个执行器回调,偷懒不传则直接报错,无法顺利实例化。
其次,变态函数的设计更加自由。
可以看到,传统的构造函数中,变态函数能且仅能作为局部变量使用,无法在构造函数外部调用。而新方法同时返回实例及其变态函数,这意味着实例和变态函数处于同一级别的作用域。
那么,这个设计上的小细节有何黑科技呢?
假设我们想要一个 Promise
实例,但尚未知晓异步任务的所有细节,我们期望先将变态函数抽离出来,再根据业务逻辑灵活调用,请问阁下如何应对?
ES2024 之前,我们可以通过 作用域提升 来“曲线救国”,举个栗子:
可以看到,这种方案的优势在于,诉诸作用域提升,我们不必把所有猫猫放在一个薛定谔的容器里,在构造函数中封装一大坨“代码屎山”;其次,变态函数不被限制在构造函数内部,随时随地任你调用。
该方案的缺陷则在于,某些社区规范鼓励“const
优先”的代码风格,即 const
声明优先,再按需修改为 let
声明。
这里的变态函数被迫使用 let
声明,这意味着存在被愣头青意外重写的隐患,但为了缓存赋值,我们一开始就不能使用 const
声明。从防御式编程的角度,这可能不太鲁棒。
因此,Promise.withResolvers()
应运而生,该静态工厂方法允许我们:
- 无参构造
const
优先- 自由变态
03. 设计动机
在某些需要封装 Promise
风格的场景中,新方法还能减少回调函数的嵌套,我把这种代码风格上的优化称为“去回调化”。
举个栗子,我们可以把 Node 中回调风格的 API 转换为 Promise
风格,以 fs
模块为例:
可以看到,由于使用了传统的构造函数实例化,在封装 readFile()
的时候,我们被迫将其嵌套在构造函数内部。
现在,我们可以使用新方法来“去回调化”。
可以看到,传统构造函数嵌套的一层回调函数就无了,整体实现更加扁平,减肥成功!
粉丝请注意,很多 Node API 现在也内置了 Promise
版本,现实开发中不需要我们手动封装,开箱即用就欧了。但是这种封装技巧是通用的。
举个栗子,瞄一眼 MDN 电子书搬运过来的一个更复杂的用例,将 Node 可读流转换为异步可迭代对象。
可以看到,井然有序的代码中透露着一丝无法形容的优雅。我脑补了一下如何使用传统构造函数来实现上述功能,现在还没缓过来......
04. 高潮总结
从历史来看,Promise.withResolvers()
并非首创,bluebird 的 Promise.defer()
或 jQuery 的 $.defer()
等库就提供了同款功能,ES2024 只是换了个名字“新瓶装旧酒”,将其标准化为内置功能。
但是,Promise.withResolvers()
的标准化势在必行,比如 Vite 源码中就自己手动封装了同款功能。
无独有偶,Axios、Vue、TS、React 等也都在源码内部“反复造轮子”,像这种回头率超高的代码片段我们称之为 boilerplate code(样板代码)。
重复乃编程之大忌,既然大家都要写,不如大家都别写,让 JS 自己写,牺牲小我,成全大家。编程里的 DRY 原则就是让我们不要重复,因为很多 bug 就是重复导致的,而且不好统一管理和维护,《ES6 标准入门教程》科普的 魔术字符串 就是其中一种反模式。
兼容性方面,我也做过临床测试了,主流浏览器广泛支持。
总之,Promise.withResolvers()
通过将样板代码标准化,达到了消除重复的目的,原生实现除了性能更好,是一个性价比较高的静态工厂方法。
参考文献
- GitHub:github.com/tc39/propos…
- MDN:developer.mozilla.org/en-US/docs/…
- bluebird:bluebirdjs.com/docs/deprec…
粉丝互动
本期话题是:你觉得新方法好评几颗星,为什么?你可以在本文下方自由言论,文明科普。
欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。
坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~
来源:juejin.cn/post/7391745629876469760
显示器分辨率的小知识
数字化时代,显示器是我们日常生活和工作中不可或缺的一部分。
无论是在电脑、手机、平板还是电视上,我们都依赖显示器来呈现图像和文字。然而,对于许多人来说,显示器分辨率这一概念可能并不十分清晰。
分辨率是影响显示器性能和视觉效果的关键因素之一。它决定了图像的清晰度、细节和整体观感。
因此,了解显示器分辨率的知识对于我们选择和使用显示设备至关重要。
本文将向大家介绍关于显示器分辨率的一些小知识,希望能够帮助大家更好地选择合适的显示器,并提升在使用显示设备时的体验。
1. 常用的分辨率
代号 | 分辨率 | 备注 |
---|---|---|
720p | 1280 x 720 | 也被称为 HD,高清 |
1080p | 1920 x 1080 | 也被称为 FULL HD,全高清 |
1440p | 2560 x 1440 | 也被称为 QHD,Quad HD |
2160p | 3840 x 2160 | 也被称为 4K |
4320p | 7680 x 4320 | 也被称为 8K |
2. 一些术语
关于显示器,最常见的三个术语就是:
2.1. 刷新率
刷新率(Refresh rate
)是指屏幕硬件每秒刷新以显示图像的速率,通常以赫兹(Hz
)为单位。
刷新率是指显示器的能力,简单理解就是每秒屏幕能切换多少个图像。
刷新率越高的显示器,显示的视频越流畅。
不过,由于人眼有视觉暂留的能力,一般60Hz左右的液晶屏已经很流畅了。
2.2. 帧速率
帧速率(Frame rate
)是指视频或游戏每秒传输的图像帧数,通常以FPS
(每秒帧数)为单位。
帧速率一般取决于视频或者游戏本身,与显示器关系不大。
帧速率越高,视频和游戏的清晰度和流畅度越高,当然,占用的硬盘空间也越大,对显卡要求也越高。
在实际使用中,如果帧速率高于刷新率,可能会出现屏幕撕裂等现象,因为显示器无法完全跟上图像的更新速度。
而如果刷新率高于帧速率的话,对显示影响不大,但是对显示器来说,有点大材小用。
因此,刷新率和帧速率的匹配和协调对于获得最佳的视觉体验,以及购买显示器时考虑性价比至关重要。
2.3. 纵横比
纵横比(Aspect ratio
)概念比较简单,是指水平像素数与垂直像素数的比率。
对于视频和游戏,一般可以调节输出的纵横比;对于显示器,也可以通过调节像素来显示不同的纵横比。
视频或游戏与显示器的纵横比匹配的时候,显示效果最佳,图像不会变形。
这也是为什么很多视频在手机上竖屏看的时候,只会集中在中间显示,上下很多部分都是黑屏,
就是因为视频的纵横比在竖屏上的纵横比不匹配,只能缩小在中间那部分显示。
换成横屏观看,视频才能完全展开。
3. 容易混淆的概念
关于纵横比和刷新,有2个概念可能我们平时容易混淆。
3.1. 4:3 和 16:9
这两种纵横比常常被误会成差不多,甚至是一样的,但是细算起来,它们的差距还挺大。
对于4:3 的纵横比意味着图像中每 4 个宽度单位就有 3 个高度单位,
最终显示出来,屏幕宽度比长度增加了 33%。
而16:9 的纵横比意味着图像中每 16 个宽度单位就有 9 个高度单位,
最终显示出来,屏幕宽度比长度增加了 78%。
3.2. 1080i 和 1080p
i 代表隔行扫描,通过照亮屏幕上的奇数像素和偶数像素,然后将它们的结果拼接在一起以获得最终图像,容易闪烁。
p 代表逐行扫描,对图像以逐行平滑的方式拼接,有效防止屏幕闪烁。
1080p比1080i的显示效果更加的清晰和细腻,因为1080p
是后来改进的技术,现在的显示器用 i 的方式已经不多了。
来源:juejin.cn/post/7302268383315148827
关于我在HarmonyOS中越陷越深这件事...
前言
上次发文已是2023年,在上一篇 前端的春天!拥抱HarmonyOS4.0🤗 - 掘金 (juejin.cn)一文中我介绍了一些鸿蒙OS知识,此文一出大家的看法也层出不穷,笔者持开放的态度对待大家对于新生态的看法。在2024年的今天,我想来说说这几个月我有哪些思考和行动。
在短短几个月的时间里,HarmonyOS已经来到了Next版本,迎来属于鸿蒙的春天。俗话说光说不练假把式,实践是试金石,我深知在做开发的这一行只有不断试错,反复的验证,才能创造新的轮子,创造力一个人无法被替代的根本。
我写这篇文章的目的不在于极力推荐大家去学习这项技术,更多的是以一个求学者的角度去阐述自己对新技术学习的心路历程。
为什么学习鸿蒙?
迷茫
笔者是25届的学生,对于学生来说最多的是时间和学习热情,自己也曾经经历过一段时间的专业方向选择困惑期,或许当人越迷茫的时候越容易听信别人的话吧,好与坏是相对的,分人也分时间,在合适的时间选择做了合适的事情这就没有什么问题了,至少学习鸿蒙这件事情对我来说,无论将来何时都会让我记忆犹新。
渴望
在学校里老师会告诉你成熟的解决方案,会告诉你应该这样做,不应该那样做,你仿佛一个机器人,进行一些机械系的学习,时间太急,急到我们只能应付相对的课程考试与学习,内容太多,多到我们最后仅靠老师给出的精简知识点去实际开发项目。这显然不是我想要的学习方式和结果...
动力源泉
有人说:这不就是Vue、React、Flutter、就是个缝合怪....
面对互联网高速发展的今天,各家博采众长,相互吸收优秀的开发思想已不是一件新鲜的事情了。
我自己学习的方向是大前端,加上之前开发的项目都是web的与小程序相关的,自己一直想尝试结合之前开发的项目开发一个基于HarmonyOS的App,听到“一次开发多端部署”这句话让我眼前一亮(很可惜这里的多端部署在4.0的开放版本是不支持的)。在接触鸿蒙的第一天,犹如我第一次接触前端开发,那种所见即所得的开发体验让我从内心里竟有了一丝“自信”,但也恰恰是这种“自信”也逐渐将我推入了深渊
坎坷与前行
在我真正尝试开发一个鸿蒙App的时候是在2023年底,我希望通过我所学的东西去做一个完整的东西并参加 2024年的计算机设计大赛。
在十二月份的那几周,我不断的使用Figma进行原型的绘制,与指导老师探讨功能、确定交互逻辑,期间我也参考了大量的App类设计准则,最后发现鸿蒙的ArkUI是具有工业审美的(至少是符合我的想法),这使得我不必耗费太多的精力在从0到1的去做一些组件,仅需适配设计规范上所涉及到的即可,将更多的精力放在逻辑的完整性。
在开发过程中遇到了各种形形色色的问题,例如:http请求封装upload组件无法拿到回调、地图功能无法使用的解决方案、websocket连接不上、创建时间、地理位置编码......
所幸所有问题都有解决办法,只是过程真的很痛苦,反复尝试、不断验证,我很喜欢在夜晚写代码,天空越黑星星越亮,当空气都变得安静时,我的内心反而会激发一种向上的力量来支撑我,可能是因为自己太想进步了吧(hhhh),在开发的App的日子里,每天都很崩溃,但是我的老师、朋友也都在鼓励我,我又不太想都付出这么多了又轻易放弃......
在2024年的4月,我去看了武汉的樱花,距离比截止还有七天不到,因为我实在撑不住了,在这个时间点,与其逼自己一把,不如放自己一马,于是和朋友相约武汉一起赏樱......
在五月,我得知自己的鸿蒙原生应用拿到了省一等奖,内心是非常激动的,但同时有一些失落的是,我无缘继续参与今年七月的国赛,因为赛制名额原因,我无法被上推。
比赛结束后,我开始准备投递简历实习,但最终都石沉大海,行业现状让我十分焦虑,我时常觉得自己能力不足......
星河璀璨,紧接着HarmonyOS Next 正式面世,我内心不断在问自己,难道我就所有的努力都要止步于此了吗?
....
学习现状
六月我的一位学习伙伴邀请我和他们一起开发研究院的一款基于ArkUI-X的软件,这将对我来说是一个非常宝贵的机会,几个月的时间,兜兜转转回到了我梦开始的地方......
Harmony Next 正式beta发布已过去半个多月了,这期间我了解到了很多之前没有学习到的新东西,鸿蒙提供了3w+的api,这些api有什么用?我打个比方,你要做满汉全席首先得要食材,其次需要烹饪技巧。而鸿蒙他会为你提供所需的所有食材,但是,你要做松鼠桂鱼还是佛跳墙,完全取决于你自己!至于烹饪技巧,鸿蒙开设了相关的做菜视频,你可以从中学习。
笔者也看到了许多鸿蒙原生开发者,一起交流关于鸿蒙的技术问题,在这里我附上一个宝藏鸿蒙优秀案例仓库
HarmonyOS NEXT应用开发案例集
感悟
真正的强大不是对抗,而是允许和接受,接纳挫折,接纳无常,接纳情绪,接纳不同,当你允许一切发生之后,就会不再那么尖锐,会渐渐变得柔和。
Per aspera ad astra. 没有人能熄灭满天星光,鸿蒙让我见证了从星光微微到星河璀璨,它教会我的不是一项技术,更多的是教会我如何去解决问题,去思考问题,当问题没有解决方案的时候,是否自己能够结合现有资源去提出自己的想法,并不断进行验证与总结。
路上会有风
会有浪漫
会有悲伤
会有孤独
也会有无尽的星辰与希望
来源:juejin.cn/post/7390956576180109312
原来Optional用起来这么清爽!
前言
大家好,我是捡田螺的小男孩。
最近在项目中,看到一段很优雅的代码,用Optional 来判空的。我贴出来给大家看看:
//遍历打印 userInfoList
for (UserInfo userInfo : Optional.ofNullable(userInfoList)
.orElse(new ArrayList<>())) {
//print userInfo
}
这段代码因为Optional的存在,优雅了很多,因为userInfoList
可能为null,我们通常的做法,是先判断不为空,再遍历:
if (!CollectionUtils.isEmpty(userInfoList)) {
for (UserInfo userInfo:userInfoList) {
//print userInfo
}
}
显然,Optional让我们的判空更加优雅啦、
- 关注公众号:捡田螺的小男孩(很多后端干货文章)
1. 没有Optional,传统的判空?
如果只有上面这一个例子的话,大家会不会觉得有点意犹未尽呀。那行,田螺哥再来一个。
假设有一个订单信息类,它有个地址属性。
要获取订单地址的城市,会有这样的代码:
String city = orderInfo.getAddress().getCity();
这块代码会有啥问题呢?是的,可能报空指针问题!为了解决空指针问题,一般我们可以这样处理:
if (orderInfo != null) {
Address address = orderInfo.getAddress();
if (address != null) {
String city = address.getCity();
}
}
这种写法显然有点丑陋。为了更加优雅一点,我们可以使用Optional
String city = Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)
.orElseThrow(() ->
new IllegalStateException("OrderInfo or Address is null"));
这样是不是优雅一点,好了这例子也介绍完了。你们知道,田螺哥很细的。当然,是指写文章很细哈
有些伙伴,可能第一眼看那个Optional
优化后的代码有点生疏。因此,接下来,给介绍Optional
相关API
。
2. Optional API简介
2.1 ofNullable(T value)、empty()、of(T value)
因为我们上面的例子,使用到了 Optional.ofNullable(T value)
,第一个函数就讲它啦。源码如下:
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
如果value
为null,就返回 empty()
,否则返回 of(value)
函数。接下来,我们看Optional的empty()
和 of(value)
函数
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
显然, empty()
函数的作用就是返回EMPTY
对象。
而of(value)
函数会返回Optional的构造函数
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
对于 Optional的构造函数:
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
- 当value值为空时,会报
NullPointerException
。 - 当value值不为空时,能正常构造
Optional
对象。
2.2 orElseThrow(Supplier<? extends X> exceptionSupplier)、orElse(T other) 、orElseGet(Supplier<? extends T> other)
上面的例子,我们用到了orElseThrow
.orElseThrow(() -> new IllegalStateException("OrderInfo or Address is null"));
那我们先来介绍一下它吧:
public final class Optional<T> {
private final T value;
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
很简单就是,如果value
不为null
,就返回value
,否则,抛出函数式exceptionSupplier
的异常。
一般情况,跟orElseThrow
函数功能相似的还有orElse(T other)
和 orElseGet(Supplier<? extends T> other)
public T orElse(T other) {
return value != null ? value : other;
}
对于orElse
,如果value
不为null
,就返回value
,否则返回 other
。
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
对于orElseGet
,如果value
不为null
,就返回value
,否则返回执行函数式other
后的结果。
2.3 map 和 flatMap
我们上面的例子,使用到了map(Function<? super T, ? extends U> mapper)
Optional.ofNullable(orderInfo)
.map(Order::getAddress)
.map(Address::getCity)
我们先来介绍一下它的:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
public boolean isPresent() {
return value != null;
}
其实这段源码很简答,先是做个空值检查,接着就是value的存在性检查,最后就是应用函数并返回新的
Optional```
跟.map
相似的,还有个flatMap
,如下:
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
可以发现,它两差别并不是很大,主要就是体现在入参所接受类型不一样。
2.4 isPresent 和ifPresent
我们在使用Optional的过程中呢,有些时候,会使用到isPresent
和ifPresent
,他们有点像,一个就是判断value值是否为空,另外一个就是判断value值是否为空,再去做一些操作。比如:
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
即判断value值是否为空,然后做一下函数式的操作。
举个例子,这段代码:
if(userInfo!=null){
doBiz(userInfo);
}
用了isPresent
可以优化为:
Optional.ofNullable(userInfo)
.ifPresent(u->{
doBiz(u);
});
优雅永不过时,嘻嘻~
来源:juejin.cn/post/7391065678546386953
我毕业俩月,就被安排设计了公司第一个负载均衡方案,真头大
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。
今天我想和大家聊一段我自己毕业初期的一段经历,当领导给你安排了无从下手的任务,难以解决的技术问题时,该怎么办?
本文会从我的一段经历出发,分析在初入职场、刚刚成为一个程序的时候,遇到自己无解决的问题,和自己的一些解决问题的思考。
如果你工作不满三年,一定要往下看,相信一定会对你有帮助。但如果你已经工作3年以上,那么你应该不会遇到下面的问题啦,但也欢迎你往下看,也许会有不同的收获。
先讲故事
每个人都有自己的职场新人期,这个阶段你会敏感、迷茫。
记得在毕业的2个月后,我的组长给我布置了一个任务,实现服务的热部署。
热部署是什么?
热部署是一种技术,它允许在应用程序运行时不中断地更新或替换软件组件,如代码或配置。这种技术主要应用于Web应用程序和分布式系统中,旨在减少停机时间,提高开发效率,并增强应用程序的可用性。
那时候我们的服务器是单实例,每次升级,一定要先停掉服务,替换war包,然后重启tomcat。想升级,必须要在半夜客户下班的时候升级。
作为一个职场小白,技术小白,第一次接到这种任务的时候,我整个人是一脸懵逼的。
那时候报错的堆栈信息我都看不懂,还得请教组里的同事,CRUD还没整明白呢。就要去解决这个架构问题,可想而知我那时候有多崩溃。
其实选择让我一个毕业2个月的应届生来做,能发现两个基本的背景
- 公司没有标准化的解决方案,没有人知道你的解决方案是否对错。
- 包括我的组长、组内老同事在内,也没有人了解如何在生产环境实现热部署,没人可以提供帮助。
因此在这件事的起步阶段,非常的困难。最重要的是,我还不会调研行业的标准方案是什么,刚毕业的我只能埋头苦干,不断试错。
持续的没有进展,也让我内心非常焦虑。多次周会上被问及这件事情,也完全不知道如何汇报进展,因为我自己也不知道,到底什么时候能解决。
记得那时候上厕所、接水的时候,我都是低头不语,装作看不见,生怕他问我一句:“那件事情的进度怎么样了?”
现在想起自己那时候的无助和不安,我都会微微一笑。
很庆幸是在计算机行业,即使是用百度,也能够搜到大把的信息,那段时间也了解到了像Nginx、Redis这样的中间件,最终也是用Nginx,实现了最基本的目标,可以在保证客户使用的情况下,正常的升级我们的系统了。
是的,毕业后的第一家公司的负载均衡和不停机服务发布,是由一个小小毕业生来做的,前期内心有多煎熬,上线的那一刻就有多自豪。
故事讲完了,我相信很多职场人,都会面临这样的场景吧,或许是一个功能不知道怎么去实现,又或者一个方案不知道如何去设计,不敢面对,不敢说出来。
为什么会这样
在职场中,我们大概可以用四个阶段概括
- 新人期
- 发展期
- 成熟期
- 衰退期
上面我自己的一小段经历,遇到了新人期中最容易面临的问题,技能上的迷茫,和职场上的迷茫。
技能上的迷茫
我们可能发现自己有好多东西不会,什么都要学,却不知道自己该如何入手。
在大厂里,周围人看起来都是大牛,可以看着他们侃侃而谈,虽然自己只能茫然四顾,但起码有着标杆和榜样。
而在普通传统行业,身边连个会的人都没有,自己的摸索更像是盲人摸象。
职场上的迷茫
不知道事情该办成什么样,领导会不会不满意,对我的考核会不会有影响。
又或者主动说了有困难,领导会不会觉得我能力不足?
面对多重困难,我该如何汇报自己的进度?没有进度怎么办。
思考、建立正确的认知
界定问题,比问题本身要重要
技术日新月异,新技术是学不完的,解决问题的方式更是多种多样的。所有成熟的方案,都不是一蹴而就的,而是通过发现问题、解决问题一步步优化完善来的。
所以我们有一个清晰的认知,我们很难得到最优的解决方案,而是把注意力放在问题本身,看看我们到底要解决什么问题。
例如我一开始不知道热部署的含义,但我知道的是,公司想解决服务的不停机升级问题。那么我最终引入了Nginx,通过反向代理,部署多个服务就好了。提供的解决方案,完成了预期,我认为对于一个新人来讲就是合格的。
PS:事实上我在使用了Nginx完成了负载均衡后,我的组长曾和我说,你这个方案感觉不太行,不是领导想要的效果。但点到为止了,也没有什么指导,或者改进措施,最终也用这个方案在生产去部署。
注重当下
职场规则是明确的,可以在员工手册、职级要求中看到。但职场中的潜规则是不明确的,你无法摸清领导最真实的想法。
为什么这么说呢?
- 领导可能就是想历练你,给你有挑战的事情,想看看你做到什么程度,你的能力边界在哪里
- 领导对你的预期就是能够解决,也不需要他的指导
不同的想法,对你的要求截然不同。
历练你的时候,即使事情没有结果,领导也不会多说什么,或许这就是一个当下受限于公司运维、资源而难以做到的事情。
但领导相信你能够解决问题的时候,你必须要尽量完成,不然确实会影响到对你的一些看法。
对于职场新手期,你短时间内是无法建立一个良好的向上沟通渠道的(如何建立向上沟通渠道,我们后面再说)。所以对你而言,与其揣摩想法, 不如界定好问题,然后注重当下,解决好遇到的每一个困难。
两个方法
目标拆分
有一个关于程序员的经典段子:这个工作已经做完了80%,剩下的**20%**还要用和前面的一样时间。
遇到无从下手,不知如何去解决的问题,是怎么给出一个可执行的分解。
重点来了,拆分的,一定要可执行。每个人对于可执行的区分,是有很大不同的。不同的地方在于可执行的定义,你是否能清楚地知道这个问题该如何解决。
比如文章开头的故事,如果时间回到那一刻,我会这么列出计划
- 了解什么是热部署,方案调研
- 了解Nginx是什么,学会使用基本的命令
- 多个服务之间如何同步信息,比如上一秒在A服务,下一秒在B服务
- 进行验证,线上部署
学会借力
手里有把锤子,看见什么都是钉子。
我们更倾向于用我们手里的某种工具,去解决所有问题。
在遇到问题时,我们的大脑会根据以往经验做出预判,从而形成思维定势,使得我们倾向于使用熟悉的工具或方法来解决问题。然而,并不是所有问题都是钉子,一旦碰到超出我们经验边界的事情,我们可能会束手无策。
还是分享一段经历:
在字节的时候,我遇到过一个问题场景,就是如何保证集群服务器的本地缓存,保持一致。
直白来讲,我要开一个500人的会议,我准备了500份资料,那么在资料可能会修改的情况下,如何保证大家手里资料,都是最新的?
加载、更新,我能想到的就是用消息队列广播,系统收到消息的时候,每一台服务器都走一遍加载逻辑。
但有一个问题我解决不了,几百台服务器同时请求,如何解决突发的压力问题呢?如果500个人同时去找会议组织人打印,排队不说了,打印机得忙冒烟。
我给身边的一个技术大拿说了我的问题,他说,你可以用公司的一个中间件啊,数据只需要一次加载,然后服务器去下载就好了。是哈,用一个超级打印机打印出500份,分发下去就好了,不用每个人亲自来取呀。
就是几句点拨,同步给我了这一个信息,我了解了一下这个中间件,完美解决了我的问题。
因此,学会借力。找到公司大佬,找到网上的大佬,买杯咖啡、发个红包,直接了当的说出你的问题,咨询解决方案,从更高层次,降维打击你的问题。
当然,一定要在自己思考完、没有结果的情况下,再去请教,能够自己研究明白的,比如使用相关的,不要去麻烦别人。
说在最后
文章到这里就要结束啦,很感谢你能看到最后。
职场初期遇到无从下手的任务时,我们应该建立正确的认知,界定问题,并注重当下。
也分享给你了两个行之有效的方法,目标拆分和学会借力,当然,这一切都离不开你的行动和坚持,这不是方法,而是一个技术人最基本的素质,所以就不多说啦。
不知道你在职场中初期遇到无从下手的问题时,你是怎么处理的呢,欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,让我做你的垫脚石,帮你解决你遇到的问题呀,欢迎一起交流~
来源:juejin.cn/post/7362905254725648438
失业的七个月,失去了很多很多,一个普通的不能再普通的人的年中总结
开篇
这不是一篇技术的文章。
第一阶段 裸辞后的两个半月
介绍下自己的情况,坐标上海,双非院校前端打工人,目前是有三年的工作经验,在23年的年底裸辞了,有一个女朋友,本来异地,她在22年10月来到了上海,选择相信我。
在刚刚裸辞的时候,我信心满满的在各个平台投递着简历,给自己做了一个规划:“先投外包的,投小公司来练手,最后一鸣惊人进入大厂,走向人生巅峰!”当时已经可以在各大论坛上看到,前端已死啊之类的标题,但从实际感受来说,好像并没有那么夸张,程序员还是比较高薪的职业。已读不回是挺常见的,但实际面试还是比较多的,并不像大家在网上说的感觉前端开发都要找不到工作了。在这三个月里,我BOSS上沟通了400个人左右,面试的有11家,其中有5个外包。这样的情况在现在的我看来真有点属于暴殄天物了,只沟通了400多个公司就约了11家面试,后来的我才知道这就是我找工作最顺利的阶段,也是我最浪费机会的阶段。由于没有一个正确的认知,以及一个具体的规划和行动,我面试没有尽力去准备,再加上父亲住院,经常往医院跑,一来一回基本一天就没了,11家面试只有一家进入了二轮复面,甚至没有一家进入到谈薪阶段。可当时的我始终是抱着一个比较乐观的态度,还会天天和我的女朋友吐槽一些公司,从来没有认真的找过自己的问题,天真的认为只要我好好准备一下,就一定可以拿下offer,还和她保证我过年前一定能找到工作!她也十分信任我,可后续的发展就愈发的不可控了。
第二阶段 字节面试一轮结束 失业的第四个月
因为之前的面试都没有一个很好的反馈,渐渐的我有点开始着急,也已经渐渐处于摆烂的状态投递简历,就是海投,不看公司介绍,只要是个公司我就打个招呼,有回复我就发个简历,以至于在投递简历的过程中,都不知道何时自己投了字节的一个岗位,然后还约到了面试,我一时间来了信心,约了两周后的字节一轮面试。这两周我没有打开招聘软件,处于一个孤注一掷的状态,实际上就是抱着侥幸心理想碰碰运气,这期间我看了一周的技术课程,浅浅的背了一些八股文,但并没有对之前所有面试的经历进行一个总结,找找自己的问题。之后时间很快就到了面试的当天,这天我突然感觉自己差的东西有很多,但还是硬着头皮去面了,结果可想而知,一轮没有通过。我当时非常沮丧,给女朋友打了一个电话说自己没有通过,她还是非常鼓励我,不给我压力。她工作也很忙,基本就是早上8点到晚上11点的上班强度,但还是一直很有耐心的和我说没事,没有工作也没事,等你想找工作的时候再找也来得及。实际这个时候我就应该醒悟了,自己并不是什么技术大牛,只是一个顶着一个前端工程师光环的cv工程师,一个凭借一些错误的自我认知,就觉得自己未来一定会更好的愚蠢、不可理喻的愚蠢的人,况且我已经不算年轻,是一个快要奔三的大龄青年了,还抱着这种天真,不成熟的想法,但这也是后话了,当时的我还是挫败了几天后继续着躺平的生活,还大言不惭的和我的女朋友说着我三四月份一定可以找到一份合适的工作,做出承诺,但却没有匹配承诺的态度和行为,这时候的女朋友已经对我有了一些意见,但她没有明确的和我说,怕给我压力,只是感觉她的工作状态逐渐不对了,疲惫和内耗占据了她大部分空闲时间,她第一次跟我说了,要不试试别的岗位吧,前端的工作这么不好找,不如换个方向,但这句话我没有放在心上。因为报了健身班,我的健身教练已经开始为我着急了,他说要不来干健身教练吧,我带你入门,我也只当是玩笑话了。
第三阶段 裸辞后的六月半
不出意外的,在金三银四过去之后,我仍然没有找到工作,面试机会也几乎没有了,一个月可能有一两个外包的面试,但自己因为不想考虑外包,在某一家外包面完挫败感的驱使下,屏蔽了BOSS上大多数的外包公司,自此我的BOSS上再也没有一点水花。后来在我认为是缘分的加持下,我的父母和她的父母在上海碰到了,然后顺利的吃了饭,见了面,我爸妈给了我的女朋友见面礼一万零一的红包,寓意万里挑一。我的父母是很普通的农民,一辈子都跟种地打交道,不善言辞,一整个饭局,基本就是我的哥哥和她的爸妈和她在聊天,我在饭桌上也一言不发,可能是怕她的父母问及我工作的问题,也可能是因为哥哥是个很优秀的人,他对于我的期望很高,每次他在场的时候我都会避免开口,以免被抓到会被教育的点。总之饭局也很快就结束了,她的父母对我也没有什么过分的要求,也没有着急让我找工作,说着安慰我的话,现在市场环境不好,找不到工作很正常,利用这段时间学习,以后还有很多机会,你还年轻呢,不要害怕。现在的我几乎是流着眼泪打出当时阿姨跟我说的话,她们一家都是很讲道理,也会为别人考虑的人,但我还是辜负了他们的信任,让他们失望了。这期间面试机会少之又少,我这时候已经渐渐由摆烂,变得慌张的不知所措了,现在发生的情况我之前完全没有考虑到,随着存款一点一点的被社保、房租、吃喝消耗,女朋友的状态也愈发变得消极,我内心陷入了很痛苦的境地。但这并没有让我去总结面试的问题,只是一味的投着简历,看着八股文,等待面试。在六月初,女朋友第二次和我商量,如果前端的工作不好找就换个别的干干吧,这时我已经有点想逃避社会了,想要避免外出,除了每天去健身房,剩下时间几乎都呆在小小的出租屋房间。但我还是答应了她,在6月15号之后我就找其他岗位的工作。接下来几天,有了两个面试,还有原来公司的人找我想让我帮忙做个项目,因为甲方报价还没有,所以这个事情就相当于没有后续,但实际我还是心存幻想,想走更平坦的路。接着时间很快就到了6月13号,她问我计划还实行吗,我说当然实行啊,不过想等一下这个项目的事情,她第一次发泄了自己的情绪,说自己要等到什么时候啊!6月14号,她说她好累,不想等了,想分手。晚上沟通中我没有任何的话可以讲出口,一直沉默,气氛一直很凝重,6月15日,我整理了一些我的问题,想和她聊聊,她上班回来之后很平静的和我说了分手,说了本身对我的期望也没有那么高,说只需要有一个工作,或者我努力的心就可以。我说了我的很多问题,保证我自己会改,但为时已晚,因为她已经给我了足够的时间,但我没有珍惜。她把礼金退给了我,我搬了出去,故事在这就画了句号。
现在
痛苦总是后知后觉,并且悄悄击碎你的防线。父亲身体愈发不好,在知道我分手之后,去医院检查身体的时候晕倒了,进入急诊的那一刻,看着急诊中的病人,看着意识模糊的父亲,我的世界一下就崩塌了。好在父亲没有大碍,但精神状态不是很好。我没有一个最坏的预期,并且时刻都在回避问题,不敢直面自己,承认自己的弱小以及愚蠢,还渐渐看不到身边人做出的牺牲和努力。我浑浑噩噩的过着这几天,这几天是我第一次正面的和哥哥进行了我认为的平等的沟通,我第一次很认真的听他讲话,不再排斥,也第一次觉得他说的其实都对,他的话第一次在我眼里不是说教,而是关心和爱护。我发现了自己的很多问题,他还教会了我一个人生态度:凡事抱最大的希望,尽最大的努力,做最坏的打算。我的世界开始慢慢重建起来,我发现了自己之前的工作内容很简单,基于vue2、antdv、Echarts的后台管理系统,图表,小程序。我发现我没有规划,总是走一步看一步,并且还带着莫名的自信。我发现我辜负了很多人的期待,让自己也很失望。我发现了很多很多,我不知道这次裸辞还会持续的给我带来什么影响,但生活总是要继续,一手好牌最终打的稀烂,但已经没有时间让我继续消沉下去,2024年有可能是我这一生都不会忘记的。
结语
这七个月,让我失去了很多很多,失去了工作,失去了信任,失去了挚爱,失去了···这篇文章主要目的不是为了卖惨,或者贩卖焦虑,只是一个普通的不能再普通的人的一段时间的总结,是对可能已经逝去爱情的怀念。未来还会有很多很多意外的事情发生,我需要做的是积极的去面对,能做的是适应环境,找自己的不足,及时补救,不要一切都来不及的时候才后悔,才反思。我想要和林克一样,忍受孤独,努力变强,去找到她。
来源:juejin.cn/post/7382892371224461352
教你做事,uniapp ios App 打包全流程
背景
使用uniapp 开发App端,开发完成后,ios端我们需要上架到App Store,在此之前,我们需要将App先进行打包。
在HubilderX中,打包ios App我们需要四个东西,分别是:
- Bundle ID
- 证书私钥密码
- 证书私钥文件
- 证书profile文件
下面,我将一步步讲解,如何获取以上文件。
加入苹果开发者
- 使用iPhone或iPad 在App Store 下载 Apple Developer
- 进入App
- 点击底部【账户】
- 点击立即注册
- 填写资料(填写的信息要与你的苹果账号对应,因为这个App需要双重认证)
- 填完信息和资料后点击订阅
- 付费(需要给你的手机添加付款方式)
- 付费成功
- 成功加入苹果开发者计划
生成p12证书和证书私钥密码
步骤:CSR文件 ➡️ cer文件 ➡️ p12文件
- 进入Apple Developer官网,登录成功后,点击顶部导航栏的【账户】,在【账户】页面点击【证书】
- 进入到【Certificates, Identifiers & Profiles】页面,点击+号,开始注册证书
- 选择【iOS Distribution (App Store and Ad Hoc)】再点击【Continue】
- 上传证书签名(CSR文件) 下面会教大家如何生成CSR文件:
- 打开Mac上的【钥匙串访问】App
- 依次选择App顶上菜单栏的【钥匙串访问】➡️【证书助理】➡️【从证书颁发机构请求证书…】
- 打开弹窗,填写两个邮件、常用名称,选择存储到磁盘,点击【继续】
- 存储到桌面,得到【CSR文件】
- 回到网页,选择并上传刚刚生成的【CSR文件】,点击【Continue】
- 到这里【cer文件】就生成好了,点击【Download】下载到桌面
- 得到【cer文件】
接下来我们要根据这个【cer文件】导出生成为【p12文件】
- 双击打开【cer文件】,Mac会自动打开【钥匙串访问】,选中左侧登录 ➡️ 我的证书 ➡️ 证书文件,找到这个【cer证书】
- 此时证书是未受信任,双击该证书,在弹窗中展开【信任】,选择【始终信任】,然后关闭输入密码保存,证书就改成受信任了
- 右键选中该证书,在菜单中选择【导出】
- 输入密码,即【证书私钥密码】(该密码就是HbuilderX发行打包App时,填写的【证书私钥密码】),之后再输入电脑密码
- 最终得到【p12证书】
生成Bundle ID
- 回到页面(Certificates, Identifiers & Profiles),选择【Identifiers】,点击+号
- 选择【App IDs】,点击【Continue】
- 选择【App】,点击【Continue】
- 填写描述和Bundle ID,ID格式如:com.domainname.appname
- 下面的功能如果有需要的话,需要勾选上
- 比如你的App需要Apple登录的话,则需要勾选【Sign In with Apple】
- 设置完成后,点击右上角的【Continue】,【Bundle ID】就生成好了
生成profile文件
- 回到页面(Certificates, Identifiers & Profiles),选择【Profiles】,点击+号
- 选择【App Store】,点击【Continue】
- 选择上一步生成的【身份标识】,点击【Continue】
- 选择第一步生成的【Certificates证书】,点击【Continue】
- 设置【配置文件名称】,点击【Generate】生成
- 点击【Download】下载【profile文件】
- 得到【profile文件】
到这里,【Bundle ID】、【p12文件】【证书私钥密码】、【profile文件】就生成好了,可以去HbuilderX打包ios App了
HbuilderX 打包ios App
- 填入配置和文件
- 点击【打包】,即可生成App
到这一步,iOS App就生成好了。
来源:juejin.cn/post/7264939254290579495
程序员的这10个坏习惯,你中了几个?超过一半要小心了
前言
一些持续关注过我的朋友大部分都来源于我的一些资源分享和一篇万字泣血斩副业的劝诫文,但今年年后开始我有将近4个月没有再更新过。
有加过我好友的朋友私聊我问过,有些回复了有些没回复。
想通过这篇文章顺便说明一下个人的情况,主要是给大家的一些中肯的建议。
我的身体
今年年前公司福利发放的每人一次免费体检,我查出了高密度脂蛋白偏低,因为其他项大体正常,当时也没有太在意。
但过完年后的第一个月,我有一次下午上班忽然眩晕,然后犯恶心,浑身发软冒冷汗,持续了好一阵才消停。
当时我第一感觉就是颈椎出问题了?毕竟这是程序员常见的职业病。
然后在妻子陪伴下去医院的神经内科检查了,结果一切正常。
然后又去拍了片子看颈椎什么问题,显示第三节和第四节有轻微的增生,医生说其实没什么,不少从事电脑工作的人都有,不算是颈椎有大问题。
我人傻了,那我这症状是什么意思。
医生又建议我去查下血,查完后诊断出是血脂偏高,医生说要赶紧开始调理身体了,否则会引发更多如冠心病、动脉粥样硬化、心脑血管疾病等等。
我听的心惊胆战,没想到我才34岁就会得上老年病。
接下来我开始调理自己的作息和生活,放弃一些不该强求的,也包括工作之余更新博客,分享代码样例等等。
4个月的时间,我在没有刻意减肥的情况下体重从原先152减到了140,整个人也清爽了许多,精力恢复了不少。
所以最近又开始主动更新了,本来是总结了程序员的10个工作中的不良习惯。
但想到自己的情况,决定缩减成5个,另外5个改为程序员生活中的不良习惯,希望能对大家有警示的作用。
不良习惯
1、工作
1)、拖延症
不到最后一天交差,我没有压力,绝不提前完成任务,从上学时完成作业就是这样,现在上班了,还是这样,我就是我,改不了了。
2)、忽视代码可读性
别跟我谈代码注释,多写一个字我认你做die,别跟我谈命名规范,就用汉语拼音,怎样?其他人读不懂,关我什么事?
3)、忽视测试
我写一个单元测试就给我以后涨100退休金,那我就写,否则免谈。接口有问题你前端跟我说就行了发什么脾气,前后端联调不就这样吗?!
4)、孤立自己
团队合作不存在的,我就是不合群的那个,那年我双手插兜,全公司没有一个对手。
5)、盲目追求技术新潮
晚上下班了,吃完饭打开了某某网,看着课程列表中十几个没学完的课程陷入了沉默,但是首页又出现了一门新课,看起来好流行好厉害耶,嗯,先买下来,徐徐图之。
2、生活
1)、缺乏锻炼和运动
工作了一天,还加班,好累,但还是得锻炼,先吃完饭吧,嗯,看看综艺节目吧,嗯,再看看动漫吧,嗯,还得学习一下新技术吧,嗯,是手是得洗澡了,嗯,还要洗衣服,咦,好像忘记了什么重要的事情?算了,躺床上看看《我家娘子不对劲》慢慢入睡。
2)、加班依赖症
看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像十一点半了,快中午了,待会儿吃什么呢?
午睡醒了,继续干吧,看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像5点半了,快下班了,任务没完成。
算了,加加班,争取8点之前搞定。
呼~搞定了,走人,咦,10点了。
3)、忽视饮食健康
早上外卖,中午外卖,晚上外卖,哇好丰富耶,美团在手,简直就是舌尖上的中国,晚上再来个韩式炸鸡?嗯,来个韩式甜辣酱+奶香芝士酱,今晚战个痛快!
4)、缺乏社交活动
好烦啊,又要参加公司聚会,聚什么餐,还不是高级外卖,说不定帮厨今天被大厨叼了心情不好吐了不少唾沫在里面,还用上完厕所摸了那里没洗的手索性搅了一遍,最后在角落里默默看着你们吃。
吃完饭还要去KTV?继续喝,喝不死你们,另外你们唱得很好听吗?还不是看谁嗷的厉害!
谁都别跟我说话,尤其是领导,离我越远越好,唉,好想回去,这个点B站该更新了吧,真想早点看up主们嘲讽EDG。
5)、没有女朋友
张三:我不是不想谈恋爱,是没人看得上我啊,我也不好意思硬追,我也要点脸啊,现在的女孩都肿么了?一点暗示都不给了?成天猜猜猜,我猜你MLGB的。
李四:家里又打电话了,问在外面有女朋友了没,我好烦啊,我怎么有啊,我SpringCloudAlibaba都没学会,我怎么有?现在刚毕业的都会k8s了,我不学习了?不学习怎么跳槽,不跳槽工资怎么翻倍,不翻倍怎么买房,不买房怎么找媳妇?
王五:亲朋好友介绍好多个了,都能凑两桌麻将了,我还是没谈好,眼看着要30了,我能咋整啊,我瞅她啊。破罐破摔吧,大不了一个人过呗,多攒点钱以后养老,年轻玩个痛快,老了早点死也不亏,又不用买房买车结婚受气还得养娃,多好啊,以后两脚一蹬我还管谁是谁?
总结
5个工作坏习惯,5个生活坏习惯,送给我亲爱的程序员们,如果你占了一半,真得注意点了,别给自己找借口,你不会对不起别人,只是对不起自己。
喜欢的小伙伴们,麻烦点个赞,点个关注,也可以收藏下,以后没事儿翻出来看看哈。
来源:juejin.cn/post/7269375465319415867
你的 Flutter 项目异常太多是因为代码没有这样写
以前在团队里 Review 过无数代码,也算是阅码无数的人了,解决过的线上异常也数不胜数,故对如何写出健状性的代码有一些微小的见解。刚好最近闲来得空(其实是拖延症晚期)把之前躺在备忘录里的一些小记整理了一下,希望能对你有一些启发。
Uri 对象的使用
在 Dart 语言中 Uri 类用于表示 URIs(网络地址、文件地址或者路由),它内部能自动处理地址的模式与的百分号编解码。在实际开发过程我们经常直接使用字符串进行拼接 URIs,然而这种方式会给地址的高级处理带来不便甚至隐式异常。
/// 假设当前代码为一个内部系统,[outsideInput] 变量是外部系统传入内部的字符串变量
/// 你无法限定 [outsideInput] 的内容,如果变量包含非法字符(如中文),整个地址非法
final someAddress = 'https://www.special.com/?a=${outsideInput}';
/// 为了保持 URI 的完整性你可能会这样做
final someAddress = 'https://www.special.com/?a=${Uri.decodeFull(outsideInput)}';
/// 如有多个外部输入变量你又需要这样做
final someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}&b=${Uri.encodeFull(outsideInput1)}&c=${Uri.encodeFull(outsideInput2)}';
直接使用字符串来拼接 URI 地址会带来非常多的限制,你需要关注地址中拼接的每个部分的合法性,并且在处理复杂逻辑时需要更冗长为的处理。
/// 如果我们需要在另一个系统中对 [someAddress] 地址的参数按条件进行添加
if (conditionA) {
someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}';
} else if (conditionB) {
someAddress = 'https://www.special.com/?a=${Uri.encodeFull(outsideInput)}&b=${otherVariable}';
} else {
someAddress = 'https://www.special.com';
}
如果使用 Uri 可以简化绝大多数对 URIs 的处理,同时限定类型对外部有更明确的确定性,因此针对 URIs 需要做如下约定:
在任何系统中都不应直接拼接 URIs 字符串,应当构造 URI 对象作为参数或返回值。
/// 生成 Uri 对象
final someAddress = Uri(path: 'some/sys/path/loc', queryParameters: {
'a': '${outsideInput}', // 非法参数将自动百分号编码
'b': '${outsideInput1}', // 不用对每个参数单独进行编码
if (conditionA) 'c': '${outsideInput2}', // 条件参数更为简洁
});
类型转换
Dart 中可以使用 is
进行类型判断,as
进行类型转换。 同时,使用 is
进行类型判断成功后会进行隐性的类型转换。示例如下:
class Animal {
void eat(String food) {
print('eat $food');
}
}
class Bird extends Animal {
void fly() {
print('flying');
}
}
void main() {
Object animal = Bird();
if (animal is Bird) {
animal.fly(); // 隐式类型转换
}
(animal as Animal).eat('meat'); // 强制类型转换一旦失败就会抛异常
}
由于隐式的类型转换存在,is
可以充当 as
的功能,同时 as
进行类型失败会抛出异常。
所以日常开发中建议使用 is
而不是 as
来进行类型转换。 is
运算符允许更安全地进行类型检查,如果转换失败,也不会抛出异常。
void main() {
dynamic animal = Bird();
if (animal is Bird) {
animal.fly();
} else {
print('转换失败');
}
}
List 使用
collection package 的使用
List 作为 Dart 中的基础对象使用广范,由于其本身的特殊性,如使用不当极易导致异常,从而影响业务逻辑。典型示例如下:
List<int> list = [];
// 当 List 为空时访问其 first 会抛异常
list.first
// 同理访问 last 也会抛异常
list.last
// 查找对象时没有提供 orElse 也会抛异常
list.firstWhere((t) => t > 0);
// List 对象其它会抛异常的访问还有
list.single
list.lastWhere((t) => t > 0)
list.singleWhere((t) => t > 0)
所以如果没有前置判断条件,所有对 List 的访问均需替换为 collection
里对应的方法。
import 'package:collection/collection.dart';
List<int> list = [];
list.firstOrNull;
list.lastOrNull;
list.firstWhereOrNull((t) => t > 0);
list.singleOrNull;
list.lastWhereOrNull((t) => t > 0);
list.singleWhereOrNull((t) => t > 0);
取元素越界
在 Dart 开发时,碰到数组越界或者访问数组中不存在的元素情况时,会导致运行时错误,如:
List<int> numbers = [0, 1, 2];
print(numbers[3]); // RangeError (index): Index out of range: index should be less than 3: 3
你可以使用使用 try-catch
来捕获异常,但由于数组取值这样的基础操作往往遍布在项目的各个角落,try-catch
在这样的情况下使用起来会比较繁琐,而且并不是所有的取值都会导致异常,所以往往越界的问题只有真正出现了才发现。
好在,我们可以封装一个 extension
来简化数组越界的问题:
extension SafeGetList<T> on List<T> {
T? tryGet(int index) =>
index < 0 || index >= this.length ? null : this[index];
}
使用时:
final list = <int>[];
final single = list.tryGet(0) ?? 0;
由于 tryGet
返回值类型为可空(T?
) ,外部接收时需要进行空判断或者赋默认值,这相当于强迫开发者去思考值不存在的情况,如此减少了异常发生的可能,同时在业务上也更加严谨。
当然还有另一种方案,可以继承一个 ListMixin
的自定义类:SafeList
,其代码如下:
class SafeList<T> extends ListMixin<T> {
final List<T?> _rawList;
final T defaultValue;
final T absentValue;
SafeList({
required this.defaultValue,
required this.absentValue,
List<T>? initList,
}) : _rawList = List.from(initList ?? []);
@override
T operator [](int index) => index < _rawList.length ? _rawList[index] ?? defaultValue : absentValue;
@override
void operator []=(int index, T value) {
if (_rawList.length == index) {
_rawList.add(value);
} else {
_rawList[index] = value;
}
}
@override
int get length => _rawList.length;
@override
T get first => _rawList.isNotEmpty ? _rawList.first ?? defaultValue : absentValue;
@override
T get last => _rawList.isNotEmpty ? _rawList.last ?? defaultValue : absentValue;
@override
set length(int newValue) {
_rawList.length = newValue;
}
}
使用:
final list = SafeList(defaultValue: 0, absentValue: 100, initList: [1,2,3]);
print(list[0]); // 正常输出: 1
print(list[3]); // 越界,输出缺省值: 100
list.length = 101;
print(list[100]); // 改变数组长度了,输出默认值: 0
以上两种方案均可以解决越界的问题,第一个方案更简洁,第二个方案略复杂且侵略性也更强但好处是可以统一默认值、缺省值,具体使用哪种取决于你的场景。
ChangeNotifier 使用
ChangeNotifier 的属性访问或方法调用
ChangeNotifier
及其子类在 dispose
之后将不可使用,dispose
后访问其属性(hasListener
)或方法(notifyListeners
)时均不合法,在 Debug 模式下会触发断言异常;
// ChangeNotifier 源码
bool get hasListeners {
// 访问属性时会进行断言检查
assert(ChangeNotifier.debugAssertNotDisposed(this));
return _count > 0;
}
void dispose() {
assert(ChangeNotifier.debugAssertNotDisposed(this));
assert(() {
// dispose 后会设置此标志位
_debugDisposed = true;
return true;
}());
_listeners = _emptyListeners;
_count = 0;
}
static bool debugAssertNotDisposed(ChangeNotifier notifier) {
assert(() {
if (notifier._debugDisposed) { // 断言检查是否 dispose
throw FlutterError(
'A ${notifier.runtimeType} was used after being disposed.\n'
'Once you have called dispose() on a ${notifier.runtimeType}, it '
'can no longer be used.',
);
}
return true;
}());
return true;
}
在 dispose
后访问属性或调用方法通常出现在异步调用的场景下,由其是在网络请求之后刷新界面。典型场景如下:
class PageNotifier extends ChangeNotifier {
dynamic pageData;
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// 接口返回之后此实例可能被 dispose,从而导致异常
notifyListeners();
}
}
为使代码逻辑更加严谨,增强整个代码的健状性:
ChangeNotifier
在有异步的场景情况下,所有对 ChangeNotifier
属性及方法的访问都需要进行是否 dispose
的判断。
你可能会想到加一个 hasListeners
判断:
class PageNotifier extends ChangeNotifier {
dynamic pageData;
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// Debug 模式下 hasListeners 依然可能会抛异常
if (hasListeners) notifyListeners();
}
}
如上所述 hasListeners
内部仍然会进行是否 dispose
的断言判断,所以 hasListeners
仍然不安全。
因此正确的做法是:
// 统一定义如下 mixin
mixin Disposed on ChangeNotifier {
bool _disposed = false;
bool get hasListeners {
if (_disposed) return false;
return super.hasListeners;
}
@override
void notifyListeners() {
if (_disposed) return;
super.notifyListeners();
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}
// 在必要的 ChangeNotifier 子类混入 Disposed
class PageNotifier extends ChangeNotifier with Disposed {
Future<voud> beginRefresh() async {
final response = await API.getPageContent();
if (!response.success) return;
pageData = response.data;
// 异步调用不会异常
notifyListeners();
}
}
ChangeNotifier 禁止实例复用
ChangeNotifier
在各种状态管理模式中一般都用于承载业务逻辑,初入 Flutter 的开发者会受原生开发的思维模式影响可能会将 ChangeNotifier
实例进行跨组件复用。典型的使用场景是购物车,购物车有加/减商品、数量管理、折扣管理、优惠计算等复杂逻辑,将 ChangeNotifier
单个实例复用甚至单例化能提高编码效率。
但单个 ChangeNotifier
实例在多个独立的组件或页面中使用会造成潜在的问题:复用的实例一旦在某个组件中被意外 dispose
之后就无法使用,从而影响其它组件展示逻辑并且这种影响是全局的。
@override
void initState() {
super.initState();
// 添加监听
ShoppingCart.instance.addListener(_update);
}
@override
void dispose() {
// 正确移除监听
ShoppingCart.instance.removeListener(_update);
// 如果哪个实习生不小心在组件中这样移除监听,将产生致命影响
// ShoppingCart.instance.dispose();
super.dispose();
}
因此在 Flutter 开发中应禁止 ChangeNotifier
实例对外跨组件直接复用,如需跨组件复用应借助provider
、get_it
等框架将 ChangeNotifer
子类实例对象置于顶层;
void main() {
runApp(
MultiProvider(
providers: [
Provider<Something>.value(ShoppingCart.instance),
],
child: const MyApp(),
)
);
}
如果你非得要 「单例化」 自定义 ChangeNotifier
子类实例,记得一定要重新 dispose
函数。
Controller 使用
在 Flutter 中大多数 Controller
都直接或间接继承自 ChangeNotifier
。为使代码逻辑更加严谨,增强整个代码的健状性,建议:
所有 Controller
需要显式调用 dispose
方法,所有自定义 Controller
需要重写或者添加 dispose
方法。
// ScrollController 源码
class ScrollController extends ChangeNotifier {
//...
}
// 自定义 Controller 需要添加 dispose 方法
class MyScrollController {
ScrollController scroll = ScrollController();
// 添加 dispose 方法
void dispose() {
scroll.dispose();
}
}
ChangeNotifierProvider 使用
ChangeNotifierProvider
有两个构造方法:
ChangeNotifierProvider.value({value:})
ChangeNotifierProvider({builder:})
使用 value
构造方法时需要注意:value
传入的是一个已构造好的 ChangeNotifier
子类实例,此实例不由 Provider
内构建,Provider
不负责此实例的 dispose
。
虽然这个差异在 Provider 文档中有重点说明,但仍然有不少开发人员在写代码的过程中混用,故在此再次强调
因此开发人员在使用 ChangeNotifierProvider.value
时为使代码逻辑更加严谨,增强整个代码的健状性,培养良好的开发习惯开发人员需践行以下规范:
使用 ChangeNotifierProvider.value
构造方法时传入的实例一定是一个已构建好的实例,你有义务自行处理此实例的 dispose
。使用 ChangeNotifierProvider(builder:)
构造方法时你不应该传入一个已构建好的实例,这会导致生命周期混乱,从而导致异常。
你需要这样做
MyChangeNotifier variable;
void initState() {
super.initState();
variable = MyChangeNotifier(); // 提前构建实例
}
void build(BuildContext context) {
return ChangeNotifierProvider.value(
value: variable, // 已构建好的实例
child: ...
);
}
void dispose() {
super.dispose();
variable.dispose(); // 主动 dispose
}
你不能这样做
MyChangeNotifier variable;
void initState() {
super.initState();
variable = MyChangeNotifier();
}
void build(BuildContext context) {
// create 对象的生命周期只存在于 Provider 树下,此处应不直接使用此实例
return ChangeNotifierProvider(
create: (_) => variable,
child: ...
);
}
避免资源释放遗忘
在 Flutter 中有很多需要主动进行资源释放的类型,包含但不限于:Timer
、StreamSubscription
、ScrollController
、TextEditingController
等,另外很多第三方库存在需要进行资源释放的类型。
如此多的资源释放类型管理起来是非常麻烦的,一旦忘记某个类型的释放很会造成整个页面的内存泄漏。而资源的创建一般都位于 initState
内,资源释放都位于 dispose
内。
为了减小忘记资源释放的可能性,dispose
应为 State
内的第一个函数并尽可能的将 initsate
紧跟在 dispose
后
这样在代码 Review 时可以从视觉上一眼看出来资源释放是否被遗忘。
Bad
final _controller = TextEditingController();
late Timer _timer;
void initState() {
super.initState();
_timer = Timer(...);
}
Widget build(BuildContext context) {
return SizedBox(
child: // 假设此处为简单的登录界面,也将是一串很长的构建代码
);
}
void didChangeDependencies() {
super.didChangeDependencies();
// 又是若干行
}
// dispose 函数在 State 末尾,与 initState 大概率会超过一屏的距离
// 致使 dispose 需要释放的资源与创建的资源脱节
// 无法直观看出是否漏写释放函数
void dispose() {
_timer.cancell();
super.dispose();
}
Good
final _controller = TextEditingController();
late Timer _timer;
// 属性后第一个函数应为 dispose
void dispose() {
_controller.dispose();
_timer.cancell();
super.dispose();
}
// 中间不要插入其它函数,紧跟着写 initState
void initState() {
super.initState();
_timer = Timer(...);
}
上面推荐的写法也可以用在自定义的 ChangeNotifer
子类中,将 dispose
函数紧在构造函数后,有利于释放遗漏检查。
由于创建资源与释放资源在不同的函数内,因此存在一种情况:为了释放资源不得不在 State
内加一个变量以便于在 dipose
函数中引用并释放,即便此资源仅在局部使用。
典型场景如下:
late CancelToken _token;
Future<void> _refreshPage() async {
// _token 只在页面刷新的函数中使用,却不得不加一个变量来引用它
_token = CancelToken();
Dio dio = Dio();
Response response = await dio.get(url, cancelToken: _token);
int code = response.statusCode;
// ...
}
void dispose() {
super.dispose();
_token.cancel();
}
这样的场景在一个页面内可能有多处,相同的处理方式使用起来就略显麻烦了,也容易导致遗忘。因此推荐如下写法:
// 创建下面的 Mixin
mixin AutomaticDisposeMixin<T extends StatefulWidget> on State<T> {
Set<VoidCallback> _disposeSet = Set<VoidCallback>();
void autoDispose(VoidCallback callabck) {
_disposeSet.add(callabck);
}
void dispose() {
_disposeSet.forEach((f) => f());
_disposeSet.removeAll();
super.dispose();
}
}
class _PageState extends State<Page> with AutomaticDisposeMixin {
Future<void> _refreshPage() async {
final token = CancelToken();
// 添加到自动释放队列
autoDispose(() => token.cancel());
Dio dio = Dio();
Response response = await dio.get(url, cancelToken: token);
int code = response.statusCode;
// ...
}
}
当然也这种用法不限于局部变量,同样也可以在 initState
内进行资源声明的同时进行资源释放,这种写法相对来讲更加直观,更不易遗漏资源释放。
final _controller = TextEditingController();
void initState() {
super.initState();
_timer = Timer(...);
autoDispose(() => _timer.cancel());
autoDispose(() => _controller.dispose());
}
StatefulWidget 使用
State 中存在异步刷新
在开发过程中简单的页面或组件通常直接使用 StatefulWidget
进行构建,并在 State
中实现状态逻辑。因此 State
不可避免可能会存在异步刷新的场景。但异步结束时当前 Widget 可能已经从当前渲染树移除,直接刷新当前 Widget 可能导致异常。典型示例如下:
class SomPageState extends State<SomePageWidget> {
PageData _data;
Future<void> _refreshPage() async {
// 异步可能是延时、接口、文件读取、平台状态获取等
final response = await API.getPageDetaile();
if (!response.success) return;
// 直接界面刷新页面可能会导致异常,当前 Widget 可能已从渲染树移除
setState((){
_data = response.data;
});
}
}
为使代码逻辑更加严谨,增强整个代码的健壮性,培养良好的开发习惯,建议:
在 State
里异步刷新 UI 时需要进行 mounted
判断,确认当前 Widget
在渲染树中时才需要进行界面刷新否则应忽略。
Future<void> _refreshPage() async {
// 异步可能是接口、文件读取、状态获取等
final response = await API.getPageDetaile();
if (!response.success) return;
// 当前 Widget 存在于渲染树中才刷新
if (!mounted) return;
setState((){
_data = response.data;
});
}
上面的 mounted
判断可能会存在于所有 State
中又或者一个 State
里有多个异步 setState
调用,每个调用都去判断过于繁锁,因此更推荐如下写法:
// 统一定义如下 mixin
mixin Stateable<T extends StatefulWidget> on State<T> {
@override
void setState(VoidCallback fn) {
if (!mounted) return;
super.setState(fn);
}
}
// 在存在异步刷新的 State 中 with 如上 mixin
class SomPageState extends State<SomePageWidget> with Stateable {
//...
}
来源:juejin.cn/post/7375882178012577802
为什么常说,完成比完美更重要
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
最近学习了12个生财思维,受益匪浅,但是纸上得来终觉浅,绝知此事要躬行,没有亲身实践,怎么能更好的理解呢?
单纯的学习,尤其是思维工具类的学习,只看但不实践,是不会有太好的效果的。
课程中的案例虽然真实,但是每个人的眼界、能力不同,所以案例对自己只能开开眼,但自己对于思维模式的理解却不会有太多的帮助。
为了更好的理解每一个生财思维,我决定根据每一个生财思维去复盘过去十年间遇到的机遇,看看自己错过了什么,有抓住了什么,然后把学习过程中的思考重新整理出来。
今天想和大家分享的是迭代思维,希望对你有所帮助。
迭代思维
什么是迭代思维
迭代是什么意思,一种重复反馈过程的活动,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
迭代对于一个程序员来讲并不陌生,甚至很多公司把版本发布,都成为“迭代”。
如何真正的用好迭代思维?
主要有三步
- 确定目标,比如在软件开发中,我们首先要知道,我们目标是什么,是收入、用户数,还是流量。
- 找到迭代方法,这个依然很常见,利用上一篇文章讲的对标思维,参考领域成功的高手,看看他们用了什么工具、方法。
- 持续改进,每次哪怕只改进一小点,让每一步走的更踏实。
先完成,再完美
一周上线的新系统
软件开发是离不开迭代的过程的,就算是你设计了完美的框架,也会在各类需求的轰炸中不得不进行迭代满足用户需求。
在上家公司,有一次领导安排一个新业务启动,于是要单独启动一个项目。
我不知道大家平常对于一个新项目的开发需要多久,那时候我的能力较弱,开发语言也不熟悉,以往的经验是,从技术设计、框架搭建、代码开发、测试这一套流程下来,我觉着这个项目起码也要1个月左右才能完成吧。
但是经过领导们评估,最终决定这个系统的开发时间是5天,留两天时间自测,说实话我是持着怀疑的态度,硬着头皮接了下来。
最终项目用了一周多的时间就上线了,虽然时长报警,缺乏监控,代码性能不够高,但快速验证了业务的可行性。
我承认刚上线的系统并不是一个完美的系统,甚至有问题时发下连日志都没打印。
但是,发现问题排查困难,所以先不停的完善日志打印。
接着觉着发现问题太慢了,补充了监控和报警,异常情况第一时间就能感知。
性能不够,响应时间长,花了3天时间优化性能瓶颈点。
业务不断扩充,代码扩展性不好,优化了1周的时间,进行了一部分重构。
就是随着一段段时间的投入,一次次的发版上线,最终服务趋于稳定,也成了业务的一部分收入来源。
不断练习的写作
如果还能想到一个例子,那这件事就是写作。
去年年底决定开始写作时,发布的那篇文章,写完就直接发布了,很明显,数据非常不好。
内容很少分段,也没有配图,更不用说加黑、二级标题这些了。
先从内容开始,对比很多流量好的文章,都有一个共性,就是没有一大段话,而是都进行了分段。为了让大家阅读更加轻松,把一大段话,拆成几个小段,看来更加清爽。
添加了分段之后,又发现如果正片内容都是文字,一样让大家阅读压力很大,所以在其中搭配上配图,效果会更好。
后面,改进了文字排版,开始带有一级标题、二级标题。
再后来,学习如何取标题、如何选题,还建立了自己的写作模版,开头和结尾,还添加上了引导关注的文案。
就用这种方式,在掘金和公众号上,也写出了一些数据比较好的内容,掘金的创作登记,从lv3,也升级到了lv5。
而迭代思维,也会在写作上,持续应用下去。
不去开始,必定失败
迭代思维,主要适用于哪类场景呢?
当你有一件事情、一个项目时,因为内心没有完美的方案,迟迟没有行动时,就需要用到迭代思维了。
记得我发布的第一篇文章,可以是2022年了,然而下一次再次发布,已经隔了1年多的时间。
相隔时间这么久的原因,就是因为我在那个时候,我发现我写出的文章和别人差距太大了,别人的文章洋洋洒洒一两千字,标题吸引人,然而我自己的文章即没有深度,又没有自己的感触。
我在和一篇优秀的文章做比较时,我完全不知道应该如何做,才能写的像别人一样好。
于是,遇到问题不做了,睡大觉。这一睡,就白白荒废了1年的时间。
迭代思维,首先要避免完美思维,先开始,然后最重要的是小步快跑。
记得在学习写作的时候,听到老师的一句话,让我记忆犹新,也再次分享给大家。
想都是问题,做才有答案。
说在最后
好了,文章到这里就要结束了,总结一下。
迭代思维从概念上看其实不难,只需要三步即可,确定目标,找到迭代方法,持续改进。
来源:juejin.cn/post/7390134326871080972
美团多场景建模的探索与实践
本文介绍了美团到家/站外投放团队在多场景建模技术方向上的探索与实践。基于外部投放的业务背景,本文提出了一种自适应的场景知识迁移和场景聚合技术,解决了在投放中面临外部海量流量带来的场景数量丰富、场景间差异大的问题,取得了明显的效果提升。希望能给大家带来一些启发或帮助。
1 引言
美团到家Demand-Side Platform(下文简称DSP)平台,主要负责在美团外部媒体上进行商品或者物料的推荐和投放,并不断优化转化效果。随着业务的不断发展与扩大,DSP对接的外部渠道越来越丰富、展示形式越来越多样,物料展示场景的差异性愈发明显(如开屏、插屏、信息流、弹窗等)。
例如,用户在午餐时间更容易点击【某推荐渠道下】【某App】【开屏展示位】的快餐类商家的物料而不是【信息流展示位】的啤酒烧烤类商家物料。场景间差异的背后本质上是用户意图和需求的差异,因此模型需要对越来越多的场景进行定制化建设,以适配不同场景下用户的个性化需求。
业界经典的Mixture-of-Experts架构(MoE,如MMoE、PLE、STAR[1]等)能一定程度上适配不同场景下用户的个性化需求。这种架构将多个Experts的输出结果通过一个门控网络进行权重分配和组合,以得到最终的预测结果。早期,我们基于MoE架构提出了使用物料推荐渠道进行场景划分的多场景建模方案。然而,随着业务的不断壮大,场景间的差异越来越大、场景数量也越来越丰富,这版模型难以适应业务发展,不能很好地解决DSP背景下存在的以下两个问题:
- 负迁移现象:以推荐渠道为例,由于不同推荐渠道的流量在用户分布、行为习惯、物料展示形式等方面存在差异,其曝光数、点击率也不在同一个数量级(如下图1所示,不同渠道间点击率相差十分显著),数据呈现典型的“长尾”现象。如果使用推荐渠道进行多场景建模的依据,一方面模型会更倾向于学习到头部渠道的信息,对于尾部渠道会存在学习不充分的问题,另一方面尾部渠道的数据也会给头部渠道的学习带来“噪声”,导致出现负迁移。
- 数据稀疏难以收敛:DSP会在外部不同媒体上进行物料展示,而用户在访问外部媒体时,其所处的时空背景、上下文信息、不同App以及物料展示位等信息共同构成了当前的场景,这样的场景在十万的量级,每个场景的数据又十分稀疏,导致模型难以在每个场景上得到充分的训练。
在面对此类建模任务时,业界现有的方法是在不同场景间进行知识迁移。例如,SAML[2]模型采用辅助网络来学习场景的共享知识并迁移至各场景的独有网络;ADIN[3]和SASS[4]模型使用门控单元以一种细粒度的方式来选择和融合全局信息到单场景信息中。然而,在DSP背景中复杂多变的流量背景下,场景差异性导致了场景数量的急剧增长,现有方法无法在巨量稀疏场景下有效。
因此,在本文中我们提出了DSP背景下的自适应场景建模方案(AdaScene, Adaptive Scenario Model),同时从知识迁移和场景聚合两个角度进行建模。AdaScene通过控制知识迁移的程度来最大化不同场景共性信息的利用,并使用稀疏专家聚合的方式利用门控网络自动选择专家组成场景表征,缓解了负迁移现象;同时,我们利用损失函数梯度指导场景聚合,将巨大的推荐场景空间约束到有限范围内,缓解了数据稀疏问题,并实现了自适应场景建模方案。
2 自适应场景建模
在本节开始前,我们先介绍多场景模型的建模方式。多场景模型采用输入层 Embedding + 混合专家(Mixture-of-Experts, MoE)的建模范式,其中输入信息包括了用户侧、商家侧以及场景上下文特征。多场景模型的损失由各场景的损失聚合而成,其损失函数形式如下:
其中,为场景数量,为各场景的损失权重值。
我们提出的AdaScene自适应场景模型主要包含以下2个部分:场景知识迁移(Knowledge Transfer)模块以及场景聚合(Scene Aggregation)模块,其模型结构如下图2所示。场景知识迁移模块自适应地控制不同场景间的知识共享程度,并通过稀疏专家网络自动选择 K 个专家构成自适应场景表征。场景聚合模块通过离线预先自动化衡量所有场景间损失函数梯度的相似度,继而通过最大化场景相似度来指导场景的聚合。
该模型结构的整体损失函数如以下公式所示:
其中,为每个场景组的损失函数所对应的系数,为第个场景组下的的场景数量,为某种场景组的划分方式。
下面,我们分别介绍自适应场景知识迁移和场景聚合的建模方案。
2.1 自适应场景知识迁移
在多场景建模中,场景定义方式决定了场景专家的学习样本,很大程度上影响着模型对场景的拟合能力,但无论采用哪种场景定义方式,不同场景间用户分布都存在重叠,用户行为模式也会有相似性。
为提升不同场景间共性的捕捉能力,我们从场景特征和场景专家两个维度探索场景知识迁移的方法,在以物料推荐渠道×App×展示形态作为多场景建模Base模型的基础上,构建了如下图3所示的自适应场景知识迁移模型(Adaptive Knowledge Transfer Network, AKTN)。该模型建立了场景共享参数与私有参数的知识迁移桥梁,能够自适应地控制知识迁移的程度、缓解负迁移现象。
- 场景特征适配:通过Squeeze-and-Excitation Network[5]构建场景适应层(Scene Adaption Layer),其结构可表示为,其中表示全连接层,为激活函数。由于不同场景对原始特征的关注程度存在较大差异,该层能够根据不同场景的信息生成原始特征的权重,并利用这些权重对输入特征进行相应的变换,实现场景特定的个性化输入表征,提高模型的场景信息捕捉能力。
- 场景知识迁移:使用GRU门控单元构建场景知识迁移层(Scene Transfer Layer)。GRU门控单元通过场景上下文信息对来自全局场景专家和当前场景专家的信息流动进行控制,筛选出符合当前场景的有用信息;并且,该结构能以层级方式进行堆叠,不断对场景输出进行修正。
场景特征适配在输入层根据场景信息对不同特征进行权重适配,筛选出当前场景下模型最关注的特征;场景知识迁移在隐层专家网络中进行知识迁移,控制共享专家中共性信息向场景独有信息的流动,使得场景共性信息得以传递。
这两种知识迁移方式互为补充、相辅相成,共同提升多场景模型的预估能力。我们对比了不同模块的实验效果,具体结果如下表1所示。可以看出,引入场景知识迁移和特征权重优化在头部、尾部渠道都能带来一定提升,其中尾部小流量场景上(见下表1子场景2、3)有更为明显的提升,可见场景知识迁移缓解了场景之间的负迁移现象。
相关研究和实践表明[6][7][8],稀疏专家网络对于提高计算效率和增强模型效果非常有用。因此,我们在AKTN模型的基础上,在专家层进一步优化多场景模型。具体的,我们将场景知识迁移层替换为自动化稀疏专家选择方法,通过门控网络从大规模专家中选取与当前场景最相关的个构成自适应场景表征,其选择过程如下图4所示:
在实践中,我们通过使用可微门控网络对专家进行有效组合,以避免不相关任务之间的负迁移现象。同时大规模专家网络的引入扩大了多场景模型的选择空间,更好地支持了门控网络的选择。考虑到多场景下的海量流量和复杂场景特征,在业界调研的基础上对稀疏专家门控网络进行了探索。
具体而言,我们对以下稀疏门控方法进行了实践:
- 方法一:通过散度衡量子场景与各专家之间的相似度,以此选择与当前场景最匹配的个专家。在实现方式上,使用场景*专家的二维矩阵计算相似性,并通过散度选择出最适合的个专家。
- 方法二:每个子场景配备一个专家选择门控网络,个场景则有个门控网络。对于每个场景的门控网络,配备个单专家选择器[9],每个单专家选择器负责从个专家中选择一个作为当前场景的专家(为Experts个数)。在实践中,为提高训练效率,我们对单专家选择器中权重较小的值进行截断,保证每个单专家选择器仅选择一个专家。
在离线实验中,我们以物料推荐渠道 * 展示形态作为场景定义,对上述稀疏门控方法进行了尝试,离线效果如下表2所示:
可以看出,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。相较于常见的以截断方式为主的门控网络,使用二进制编码的方式使得其在不损失其他专家网络信息的同时,能够更好地收敛到目标专家数量,同时其可微性使得其在以梯度为基础的优化算法中训练更加稳定。
同时,为了验证稀疏门控网络能否有效区分不同场景并捕捉到场景间差异性,我们使用=16个专家中选择=7个的例子,对验证集中不同场景下各专家的利用率、选择专家的平均权重进行了可视化分析(如图5-图7所示),实验结果表明该方法能够有效地选择出不同的专家对场景进行表达。
例如,图6中KP_1更多地选择第5个专家,而KP_2更倾向于选择第15个专家。并且,不同场景对各专家的使用率以及选择专家的平均权重也有着明显的差异性,表明该方法能够捕捉到细分场景下流量的差异性并进行差异化的表达。
实验证明,在通过大规模专家网络对每个场景进行建模的同时,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。 同时,为了进一步探索Experts个数对模型性能的影响,我们在方法二的基础上通过调整专家个数和topK比例设计了多组对比实验,实验结果如下表3所示:
从实验数据可以看出,大规模的Experts结构会带来正向的离线收益;并且随着选取专家个数比例的增加(表3横轴),模型整体的表现效果也有上升的趋势。
2.2 自适应场景聚合
理想情况下,一条请求(流量)可以看作一个独立的场景。但如引言所述,随着DSP业务持续发展,不同的物料展示渠道、形式、位置等持续增加,每个场景的数据十分稀疏,我们无法对每个细分场景进行有效训练。因此,我们需要对各个推荐场景进行聚类、合并。我们使用场景聚合的方法对此问题进行求解,通过衡量所有场景间的相似度,并最大化该相似度来指导场景的聚合,解决了数据稀疏导致难以收敛的问题。具体的,我们将该问题表示为:
其中表示某种分组方式,为场景在分组内与其他场景的总体相似度。在将个场景聚合成个场景组的过程中,我们需要找到使得场景间整体相似度最大的分组方式。
因此,我们在2.1节场景知识迁移模型的基础上,增加了场景聚合部分,提出了基于Two-Stage策略进行训练的场景聚合模型:
- Stage 1:基于相似度衡量方法对各场景的相似度进行归纳,并以最大化分组场景的相似度为目标找到各场景的最优聚合方式(如Scene1与Scene 4可聚合为场景组合Scene Gr0up SGA);
- Stage 2:基于Stage 1得到的场景聚合方式,以交叉熵损失为目标函数最小化各场景下的交叉熵损失。
其中,Stage 2与2.1节中所述一致,本节主要针对Stage 1进行阐述。我们认为,一个有效的场景聚合方法应该能自适应地应对流量变化的趋势,能够发现场景之间的内在联系并依据当前流量特点自动适配聚合方法。我们首先想到的是从规则出发,将人工先验知识作为场景聚合的依据,按照推荐渠道、展示形式以及两者叉乘的方式进行了相应迭代。然而这类场景聚合方式需要可靠的人工经验来支撑,且在应对海量流量时不能迅速捕捉到其中的变化。
因此,我们对场景之间关系的建模方法进行了相关的探索。首先,我们通过离线训练时场景之间的表征迁移和组合训练来评估场景之间的影响,但这种方式存在组合空间巨大、训练耗时较长的问题,效率较低。
在多任务的相关研究中[10][11][12][13],使用梯度信息对任务之间的关系进行建模是一种有效的方法。类似的在多场景模型中,能够根据各场景损失函数的梯度信息对场景间的相似度进行建模,因此我们采用多专家网络并基于梯度信息自动化地对场景之间的相似度进行求解,模型示意如下图8所示:
基于上述思路,我们对场景之间的关系建模方法进行了以下尝试:
1. Gradient Regulation
基于梯度信息能够对场景信息进行潜在表示这一认知,我们在损失函数中加入各场景损失函数关于专家层梯度距离的正则项,整体的损失函数如下所示,该正则项的系数表示场景之间的相似度,为常见的评估梯度之间距离的方法,比如,距离。
2. Lookahead Strategy
3. Meta Weights
Lookahead Strategy该方法对场景间的关系进行了显式建模,但是这种根据损失函数的变化计算场景相关系数的策略存在着训练不稳定、波动较大的现象,无法像Gradient Regulation这一方法对场景相似度进行求解。
因此,我们引入了场景间的相关性系数矩阵(meta weights),结合前两种方法对该问题进行如下建模,通过场景的数据对其与其他场景的相关性系数进行更新,同时基于该参数对全局的参数模型进行优化。针对这种典型的两层优化问题,我们基于MAML[14]方法进行求解,并将meta weights作为场景间的相似度。
我们以推荐渠道和展示形式(是否开屏)的多场景模型作为Base,对上述3种方法做了探索。为了提高训练效率,我们在设计 Stage 1 模型时做了以下优化:
我们对每个方法的GAUC进行了比较,实验效果如下表4所示。相较于人工规则,基于梯度的场景聚合方法都能带来效果的明显提升,表明损失函数梯度能在一定程度上表示场景之间的相似性,并指导多场景进行聚合。
为了更全面的展现场景聚合对于模型预估效果的影响,我们选取Meta Weights进行分组数量的调优实验,具体的实验结果如下表5所示。可以发现:随着分组数的增大,GAUC提升也越大,此时各场景间的负迁移效应减弱;但分组超过一定数量时,场景间总体的相似度减小,GAUC呈下降趋势。
此外,我们对Meta Weigts方法中部分场景间的关系进行了可视化分析,分析结果如下图9所示。以场景作为坐标轴,图中的每个方格表示各场景间的相似度,颜色的深浅表示渠道间的相似程度大小。
从图中可以发现,以渠道和展示形式为粒度的细分场景下,该方法能够学习到不同场景间的相关性,例如A渠道下的信息流(s16)与其他场景的相关性较低,会将其作为独立的场景进行预估,而B渠道下的开屏展示(s9)与C渠道开屏展示(s8)相关性较高,会将其聚合为一个场景进行预估,同时该相似度矩阵不是对称的,这也说明各场景间相互的影响存在着差异。
3 总结与展望
通过多场景学习的探索和实践,我们深入挖掘了推荐模型在不同场景下的建模能力,并分别从场景知识迁移、场景聚合方向进行了尝试和优化,这些尝试提供了更好的理解和解释推荐模型对不同类型流量和场景的应对能力。然而,这只是多场景学习研究的开始,后续我们会探索并迭代以下方向:
- 更好的场景划分方式:当前多场景的划分主要还是依据渠道(渠道*展示形态)作为流量的划分方式,未来会在媒体、展示位、媒体*时间等维度上进行更详细地探索;
- 端到端的流量聚合方式:在进行流量聚合时,使用了Two-Stage的策略进行聚合。然而,这种方式不能充分地利用流量数据中相关的信息。因此,需要探索端到端的流量场景聚合方案将更直接和有效地提高推荐模型的能力。
结合多场景学习,在未来的研究中将不断探索新的方法和技术,以提高推荐模型对不同场景和流量类型的建模能力,创造更好的用户体验以及商业价值。
4 作者简介
王驰、森杰、树立、文帅、尹华、肖雄等,均来自美团到家事业群/到家研发平台。
5 参考文献
- [1] STAR:Sheng, Xiang-Rong, et al. "One model to serve all: Star topology adaptive recommender for multi-domain ctr prediction." Proceedings of the 30th ACM International Conference on Information & Knowledge Management. 2021.
- [2] SAML:Chen, Yuting, et al. "Scenario-aware and Mutual-based approach for Multi-scenario Recommendation in E-Commerce." 2020 International Conference on Data Mining Workshops (ICDMW). IEEE, 2020.
- [3] ADIN:Jiang, Yuchen, et al. "Adaptive Domain Interest Network for Multi-domain Recommendation." Proceedings of the 31st ACM International Conference on Information & Knowledge Management. 2022.
- [4]SASS:Zhang, Yuanliang, et al. "Scenario-Adaptive and Self-Supervised Model for Multi-Scenario Personalized Recommendation." Proceedings of the 31st ACM International Conference on Information & Knowledge Management. 2022.
- [5] Squeeze-and-Excitation:Hu, Jie, Li Shen, and Gang Sun. "Squeeze-and-excitation networks." Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.
- [6] 美团外卖推荐情境化智能流量分发的实践与探索
- [7] PaLM:ai.googleblog.com/2022/04/pat…
- [8] GLaM:proceedings.mlr.press/v162/du22c.…
- [9] 单专家选择器:arxiv.org/abs/2106.03…
- [10] HOA:proceedings.mlr.press/v119/standl…
- [11] Gradient Affinity:proceedings.neurips.cc/paper/2021/…
- [12] SRDML:dl.acm.org/doi/abs/10.…
- [13] Auto-Lambda:arxiv.org/abs/2202.03…
- [14] MAML:arxiv.org/abs/1703.03…
来源:juejin.cn/post/7278597227785551883
终于找到一个比较好用的前端国际化方案了
在开发Vue/React应用,一直对现有的多语言方案不是很满足,现在终于出了一个比较满意好用的了。
本节以标准的Nodejs
应用程序为例,简要介绍VoerkaI18n
国际化框架的基本使用。
vue
或react
应用的使用流程也基本相同,可以参考Vue集成和React集成。
myapp
|--package.json
|--index.js
在本项目的所有支持的源码文件中均可以使用t
函数对要翻译的文本进行包装,简单而粗暴。
// index.js
console.log(t("中华人民共和国万岁"))
console.log(t("中华人民共和国成立于{}",1949))
t
翻译函数是从myapp/languages/index.js
文件导出的翻译函数,但是现在myapp/languages
还不存在,后续会使用工具自动生成。voerkai18n
后续会使用正则表达式对提取要翻译的文本。
第一步:安装命令行工具
安装@voerkai18n/cli
到全局。
> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
> pnpm add -g @voerkai18/cli
第二步:初始化工程
在工程目录中运行voerkai18n init
命令进行初始化。
> voerkai18n init
上述命令会在当前工程目录下创建languages/settings.json
文件。如果您的源代码在src
子文件夹中,则会创建在src/languages/settings.json
settings.json
内容如下:
{
"languages": [
{
"name": "zh",
"title": "zh"
},
{
"name": "en",
"title": "en"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}
上述命令代表了:
- 本项目拟支持
中文
和英文
两种语言。 - 默认语言是
中文
(即在源代码中直接使用中文) - 激活语言是
中文
(代表当前生效的语言)
注意:
- 可以修改该文件来配置支持的语言、默认语言、激活语言等。可支持的语言可参阅语言代码列表。
voerkai18n init
是可选的,voerkai18n extract
也可以实现相同的功能。- 一般情况下,您可以手工修改
settings.json
,如定义名称空间。 voerkai18n init
仅仅是创建languages
文件,并且生成settings.json
,因此您也可以自己手工创建。- 针对
js/typescript
或react/vue
等不同的应用,voerkai18n init
可以通过不同的参数来配置生成ts
文件或js
文件。 - 更多的
voerkai18n init
命令的使用请查阅这里
第三步:标识翻译内容
接下来在源码文件中,将所有需要翻译的内容使用t
翻译函数进行包装,例如下:
import { t } from "./languages"
// 不含插值变量
t("中华人民共和国")
// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")
t
翻译函数只是一个普通函数,您需要为之提供执行环境,关于t
翻译函数的更多用法见这里
第四步:提取文本
接下来我们使用voerkai18n extract
命令来自动扫描工程源码文件中的需要的翻译的文本信息。 voerkai18n extract
命令会使用正则表达式来提取t("提取文本")
包装的文本。
myapp>voerkai18n extract
执行voerkai18n extract
命令后,就会在myapp/languages
通过生成translates/default.json
、settings.json
等相关文件。
- translates/default.json : 该文件就是从当前工程扫描提取出来的需要进行翻译的文本信息。所有需要翻译的文本内容均会收集到该文件中。
- settings.json: 语言环境的基本配置信息,包含支持的语言、默认语言、激活语言等信息。
最后文件结构如下:
myapp
|-- languages
|-- settings.json // 语言配置文件
|-- translates // 此文件夹是所有需要翻译的内容
|-- default.json // 默认名称空间内容
|-- package.json
|-- index.js
如果略过第一步中的voerkai18n init
,也可以使用以下命令来为创建和更新settings.json
myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
以上命令代表:
- 扫描当前文件夹下所有源码文件,默认是
js
、jsx
、html
、vue
文件类型。 - 支持
zh
、en
、de
、jp
四种语言 - 默认语言是中文。(指在源码文件中我们直接使用中文即可)
- 激活语言是中文(即默认切换到中文)
-D
代表显示扫描调试信息,可以显示从哪些文件提供哪些文本
第五步:人工翻译
接下来就可以分别对language/translates
文件夹下的所有JSON
文件进行翻译了。每个JSON
文件大概如下:
{
"中华人民共和国万岁":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"] // 记录了该信息是从哪几个文件中提取的
},
"中华人民共和国成立于{}":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"]
}
}
我们只需要修改该文件翻译对应的语言即可。
重点:如果翻译期间对源文件进行了修改,则只需要重新执行一下voerkai18n extract
命令,该命令会进行以下操作:
- 如果文本内容在源代码中已经删除了,则会自动从翻译清单中删除。
- 如果文本内容在源代码中已修改了,则会视为新增加的内容。
- 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。
总之,反复执行voerkai18n extract
命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
第六步:自动翻译
voerkai18n
支持通过voerkai18n translate
命令来实现调用在线翻译服务进行自动翻译。
>voerkai18n translate --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>
在项目文件夹下执行上面的语句,将会自动调用百度的在线翻译API
进行翻译,以现在的翻译水平而言,您只需要进行少量的微调即可。关于voerkai18n translate
命令的使用请查阅后续介绍。
第七步:编译语言包
当我们完成myapp/languages/translates
下的所有JSON语言文件
的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续名称空间
介绍),接下来需要对翻译后的文件进行编译。
myapp> voerkai18n compile
compile
命令根据myapp/languages/translates/*.json
和myapp/languages/settings.json
文件编译生成以下文件:
|-- languages
|-- settings.json // 语言配置文件
|-- idMap.js // 文本信息id映射表
|-- index.js // 包含该应用作用域下的翻译函数等
|-- storage.js
|-- zh.js // 语言包
|-- en.js
|-- jp.js
|-- de.js
|-- formatters // 自定义扩展格式化器
|-- zh.js
|-- en.js
|-- jp.js
|-- de.js
|-- translates // 此文件夹包含了所有需要翻译的内容
|-- default.json
|-- package.json
|-- index.js
第八步:导入翻译函数
第一步中我们在源文件中直接使用了t
翻译函数包装要翻译的文本信息,该t
翻译函数就是在编译环节自动生成并声明在myapp/languages/index.js
中的。
import { t } from "./languages"
因此,我们需要在需要进行翻译时导入该函数即可。
但是如果源码文件很多,重次重复导入t
函数也是比较麻烦的,所以我们也提供了一个babel/vite
等插件来自动导入t
函数,可以根据使用场景进行选择。
第九步:切换语言
当需要切换语言时,可以通过调用change
方法来切换语言。
import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// 或者VoerkaI18n是一个全局单例,可以直接访问
await VoerkaI18n.change("en")
i18nScope.change
与VoerkaI18n.change
两者是等价的。
一般可能也需要在语言切换后进行界面更新渲染,可以订阅事件来响应语言切换。
import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
//
VoerkaI18n.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
@voerkai18n/vue和@voerkai18n/react提供了相对应的插件和库来简化重新界面更新渲染。
第十步:语言包补丁
一般情况下,多语言的工程化过程就结束了,voerkai18n
在多语言实践考虑得更加人性化。有没有经常发现这样的情况,当项目上线后,才发现:
- 翻译有误
- 客户对某些用语有个人喜好,要求你更改。
- 临时要增加支持一种语言
一般碰到这种情况,只好重新打包构建工程,重新发布,整个过程繁琐而麻烦。 现在voerkai18n
针对此问题提供了完美的解决方案,可以通过服务器来为应用打语言包补丁
和动态增加语言
支持,而不需要重新打包应用和修改应用。
方法如下:
- 注册一个默认的语言包加载器函数,用来从服务器加载语言包文件。
import { i18nScope } from "./languages"
i18nScope.registerDefaultLoader(async (language,scope)=>{
return await (await fetch(`/languages/${scope.id}/${language}.json`)).json()
})
- 将语言包补丁文件保存在Web服务器上指定的位置
/languages/<应用名称>/<语言名称>.json
即可。 - 当应用启动后会自动从服务器上加载语言补丁包合并,从而实现动为语言包打补丁的功能。
- 利用该特性也可以实现动态增加临时支持一种语言的功能
来源:juejin.cn/post/7275944565885485116
前段时间面试了一些人,有这些槽点跟大家说说
大家好,我是拭心。
前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。
简历书写和自我介绍
- 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备
- 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了
- 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求
- 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色
- 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多
- 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!
- 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)
- 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点
- 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说
- 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到
- 实在不知道怎么介绍,翻上去看第 4 点和第 5 点
- 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略
- 你可以这样审视自己的简历和自我介绍:
a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点
b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考
c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点
面试问题
- 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质
- 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任
- 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质
项目经历
项目经历就是我们过往做过的项目。
项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。
有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?
大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。
在项目经历上,面试者常见的问题有这些:
- 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)
- 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)
- 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)
出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。
在看面试者的项目经历时,面试官主要关注这三点:
1. 之前做的项目有没有难度
2. 项目经验和当前岗位需要的是否匹配
3. 经过这些项目,这个人的能力有哪些成长
因此,我们在日常工作和准备面试时,可以这样做:
- 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处
- 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强
- 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点
- 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长
- 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案
- 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分
- 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验
技能知识点
技能知识点就是我们掌握的编程语言、技术框架和工具。
相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。
在技能知识点方面,面试者常见的问题有这些:
- 不胜任岗位:基础不扎实,不熟悉常用库的原理
- 技术不对口:没有岗位需要的领域技术
- 技术过剩:能力远远超出岗位要求
第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。
第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。
第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。
在我面试的人里,通过面试的都有这些特点:
- 技术扎实:不仅仅基础好,还有深度
- 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事
有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?
软素质
这里的「软素质」指面试时考察的、技术以外的点。
程序员的日常工作里,除了写代码还需要做这些事:
- 理解业务的重点和不同需求的核心点,和其他同事协作完成
- 从技术角度,对需求提出自己的思考和建议,反馈给其他人
- 负责某个具体的业务/方向,成为这个方面所有问题的处理者
因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:
- 理解能力和沟通表达能力
- 业务能力
- 稳定性
第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。
第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。
业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。
遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。
第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。
针对以上这三点,我们可以这样做:
- 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张
- 回答问题时有逻辑条理,可以采用类似总分总的策略
- 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法
- 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标
总结
好了,这就是我前段时间面试的感悟和吐槽。
总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。
来源:juejin.cn/post/7261604248319918136
使用Tauri快速搭建桌面项目
什么是 Tauri
Tauri 是一个跨平台的 GUI
框架,与 Electron
的理念相似。Tauri 的前端部分同样基于 Web 技术,但它的后端则采用了 Rust
语言。Tauri 可以创建体积更小、运行更快且更加安全的跨平台桌面应用。
与 Electron
不同,Tauri 并没有内置 Chromium
,因此打包后的应用体积要比 Electron
小很多,启动速度更快,内存和 CPU 占用率也更低。
然而,由于没有内置 Chromium
,Tauri 使用系统原生的 WebView 来渲染网页,这可能导致不同系统之间的页面表现存在差异。同时,Tauri 的后端需要使用 Rust
进行开发,这对前端开发人员来说可能会有一定的上手成本。
好在 Tauri 已经为我们封装了大部分 API,即使不懂 Rust
,也可以开发出一款简单的应用。
预先准备
我们以 macOS 为例:
1. 首先安装 Xcode 命令行工具
在终端中执行以下命令:
xcode-select --install
如果已经安装过 Xcode 命令行工具,则可以直接进行下一步。
2. 安装 Rust
在 macOS 上安装 Rust,请打开终端并输入以下命令:
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
安装成功后,终端将显示以下内容:
Rust is installed now. Great!
请确保重新启动终端以使更改生效。
快速开始
创建项目
Tauri 官方提供了多种项目模板,可以快速搭建项目:
# pnpm
pnpm create tauri-app
# npm
npm create tauri-app
# yarn
yarn create tauri-app
按照提示选择自己喜欢的模板。
这里我们选择 react
开发前端页面。
一路回车后,打开项目文件夹。执行安装依赖命令:pnpm i
。依赖安装完成后,执行 pnpm tauri dev
命令启动项目。这时便会启动一个应用,如下图所示:
开发
Tauri 的开发非常容易上手,我们先来看下项目文件结构:
是不是和 Vite 的目录结构一样?
没错,这就是一个常规的 Vite 目录结构,唯一的区别是增加了一个 src-tauri
文件夹,这里面是 Rust
部分的代码,也就是后端代码。
打包
首先,我们需要修改默认的包标识符,位置在 src-tauri > tauri.conf.json > tauri > bundle > identifier
。
这里我们随便填写一个标识符 com.example.app
,保存,然后执行命令:pnpm tauri build
就可以正常打包了。
tauri.conf.json
文件是我们的应用配置文件,包含了应用的基本信息。
打包完成后,就可以在 tauri-app/src-tauri/target/release/bundle
目录下找到我们的应用。
现在我们只构建了 macOS 下的应用。
打开之后就可以看到我们的应用了。
参考文档
来源:juejin.cn/post/7388842078798823433
DDD项目落地之充血模型实践 | 京东云技术团队
背景:
充血模型是DDD分层架构中实体设计的一种方案,可以使关注点聚焦于业务实现,可有效提升开发效率、提升可维护性;
1、DDD项目落地整体调用关系
调用关系图中的Entity为实体,从进入领域服务(Domin)时开始使用,直到最后返回。
2、实体设计
充血模型是实体设计的一种方法,简单来说,就是一种带有具体行为方法和聚合关联关系的特殊实体;
关于实体设计,需要明白的关键词为:领域服务->聚合->聚合根->实体->贫血模型->充血模型
聚合与聚合根:
聚合是一种关联关系,而聚合根就是这个关系成立的基础,没有聚合根,这个聚合关系就无法成立;
举个例子,存在3个实体:用户、用户组、用户组关联关系,这3个实体形成的关联关系就是聚合,而用户实体就是这个聚合中的聚合根;
实体:
定义在领域层,是领域层的重要元素,从领域划分到工程实践落地,都应该围绕实体进行,DDD中的实体和数据库表不只是1对1关系,可能是1对多或者仅为内存中的对象;
贫血模型:
实体不带有任何行为方法,也不带有聚合关联关系,作用基本相当于值对象(ValueObject),仅作为值传递的对象,和传统三层项目架构中的实体具有相同作用,不建议使用。补充说明:一般我们使用的DTO就可以被当做是值对象
充血模型:
实体中带有具有行为方法和聚合关联关系,行为方法是说create、save、delete等封装了一类可以指代行为的方法,比如在用户实体对象中具有用户组实体的引用,这样当我们需要操作用户组时,只通过用户实体进行操作就可以。
工程实践中,建议采用充血模型,好处是隐藏胶水代码,提升代码可读性,使关注点聚焦于业务实现。
充血模型在实践中的问题:
行为代码量过多,导致实体内部臃肿膨胀,难以阅读,难以维护,对于这种问题,我们需要根据实体行为的代码量多少来采取不同的解决方案。
解决方案:
场景1:行为不会导致实体臃肿的情况下,在实体中完成行为定义
public CooperateServicePackageConfig save() {
// 直接调用基础设施层进行保存
cooperateServicePackageConfigRepository.save(this);
return this;
}
场景2:行为导致实体臃肿的情况下,采用外部定义行为的方式,核心思想是借助其他类实现行为代码定义,将臃肿代码外移,保留干净的实体行为:
1)创建工具类,将某个实体中的行为定义其中,实体负责调用该工具类
public CooperateServicePackageConfig save() {
// 将处理过程放在工具类中
ServicePackageSaveUtils.save(this);
return this;
}
2)创建新实体,将该实体的使用场景明确至某个细分行为,比如一个聚合根(ExampleEntity)的保存可能涉及到5个实体的保存,那么我们定义一个ExampleSaveEntity实体,专门用来处理该聚合下的保存行为
实践经验:
1、关于spring bean注入:充血模型在实体中使用静态注入方法实现。例:
private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);
2、充血模型的实体序列化,排除非必要属性,在一些redis对象缓存时可能会用到。例:
// 使用注解排除序列化属性
@Getter(AccessLevel.NONE)
private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);
// 使用注解排除序列化属性
@JSONField(serialize = false)
private ServicePackageConfig servicePackageConfig;
// 使用注解排除序列化 get 方法
@Transient
@JSONField(serialize = false)
public static CooperateServicePackageRepositoryQuery getAllCodeQuery(Long contractId) {
CooperateServicePackageRepositoryQuery repositoryQuery = new CooperateServicePackageRepositoryQuery();
repositoryQuery.setContractIds(com.google.common.collect.Lists.newArrayList(contractId));
repositoryQuery.setCode(RightsPlatformConstants.CODE_ALL);
return repositoryQuery;
}
3、利用Set方法建立聚合绑定关系。例:
public void setServiceSkuInfos(List<ServiceSkuInfo> serviceSkuInfos) {
if (CollectionUtils.isEmpty(serviceSkuInfos))
{
return;
}
this.serviceSkuInfos = serviceSkuInfos;
List<String> allSkuNoSet = serviceSkuInfos
.stream()
.map(one -> one.getSkuNo())
.collect(Collectors.toList());
String skuJoinStr = Joiner.on(GlobalConstant.SPLIT_CHAR).join(allSkuNoSet);
this.setSkuNoSet(skuJoinStr);}
作者:京东健康 张君毅
来源:京东云开发者社区
来源:juejin.cn/post/7264235181778190373
学会Grid之后,我觉得再也没有我搞不定的布局了
说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局
、双飞翼布局
等非常耳熟的名词;
为了实现这些布局我们有很多种实现方案,例如:table布局
、float布局
、定位布局
等,当然现在比较流行的肯定是flex布局
;
flex布局
属于弹性布局,所谓弹性也可以理解为响应式布局
,而同为响应式布局的还有Grid布局
;
Grid布局
是一种二维布局,可以理解为flex布局
的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;
本篇不会过多介绍
grid
的基础内容,更多的是一些布局的实现方案和一些小技巧;
常见布局
所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局
、双飞翼布局
这种名词我个人觉得不用太过于去在意;
因为这类布局最后的解释都会变成几行几列
,内容在哪一行哪一列,而这些就非常直观的对标了grid
的特性;
接下来我们来一起看看一些非常常见的布局,并且用grid
来实现;
1. 顶部 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.header,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
</body>
</html>
2. 顶部 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr 60px;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.footer {
background-color: #039BE5;
}
.header,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这里示例和上面的示例唯一的区别就是多了一个
footer
,但是我们可以看到代码并没有多少变化,这就是grid
的强大之处;
可以看
码上掘金
的效果,这里的内容区域是单独滚动的,从而实现了header
和footer
固定,内容区域滚动的效果;
实现这个效果也非常简单,只需要在
content
上加上overflow: auto
即可;
3. 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.left {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例效果其实和第一个是类似的,只不过是把
grid-template-rows
换成了grid-template-columns
,这里就不提供码上掘金
的示例了;
4. 顶部 + 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-column: 1 / 3;
background-color: #039BE5;
}
.left {
background-color: #4FC3F7;
}
.content {
background-color: #99CCFF;
}
.header,
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例不同点在于
header
占据了两列,这里我们可以使用grid-column
来实现,grid-column
的值是start / end
,例如:1 / 3
表示从第一列到第三列;
如果确定这一列是占满整行的,那么我们可以使用
1 / -1
来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧
的布局,那么header
就不需要修改了;
5. 顶部 + 左侧 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header"
"left content"
"left footer";
grid-template-rows: 60px 1fr 60px;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.left {
grid-area: left;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.footer {
grid-area: footer;
background-color: #6699CC;
}
.header,
.left,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这个示例的小技巧是使用了
grid-template-areas
,使用这个属性可以让我们通过代码来直观的看到布局的样式;
这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:
"header header"
表示第一行的两列都是header
,这里的header
是我们自己定义的,可以是任意值;
定义好了之后就可以在对应的元素上使用
grid-area
来指定对应的区域,这里的值就是我们在grid-template-areas
中定义的值;
在
码上掘金
中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto
即可;
响应式布局
响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;
这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;
1. 基础布局实现
移动端布局
以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是
header
、navigation
、content
;
注:这里不是要
100%
还原掘金的页面,只是为了演示grid
布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header"
"navigation"
"content";
grid-template-rows: 60px 48px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
</body>
</html>
iPad布局
这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下
grid-template-rows
和grid-template-columns
的值即可;
由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的
css
代码,只保留需要修改的代码;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header"
"navigation navigation"
"content right";
grid-template-columns: 1fr 260px;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
PC端布局
和上面处理方式相同,由于
Navigation
移动到了左侧,所以还要额外的修改一下grid-template-areas
的值;
这里就可以体现
grid
的强大之处了,我们可以简单的修改grid-template-areas
就可以实现一个完全不同的布局,而且代码量非常少;
为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用
.
来实现,这里的.
表示一个空白区域;
由于内容的宽度基本上是固定的,所以留白区域简单的使用
1fr
进行占位即可,这样就可以平均的分配剩余的空间;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 1fr;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
完善一些细节
最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用
column-gap
和一个空的区域进行占位来实现的;
这里的
column-gap
表示列与列之间的间距,值可以是px
、em
、rem
等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr
;
空区域进行占位留间距其实我并不推荐,这里只是演示
grid
布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin
来实现;
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header header"
"navigation navigation navigation"
". . ."
". content .";
grid-template-columns: 1fr minmax(0, 720px) 1fr;
grid-template-rows: 60px 48px 10px 1fr;
column-gap: 10px;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header header header"
"navigation navigation navigation navigation"
". . . ."
". content right .";
grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". . . . ."
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 10px 1fr;
}
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
简单复刻版
以
码上掘金
上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;
异型布局
异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid
是如何实现的;
1. 照片墙
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #f2f3f5;
overflow: auto;
}
body {
display: grid;
grid-template-columns: repeat(12, 100px);
grid-auto-rows: 100px;
place-content: center;
gap: 6px;
height: 100vh;
}
.photo-item {
width: 200px;
height: 200px;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
</style>
</head>
<body>
</body>
<script>
function randomColor() {
return '#' + Math.random().toString(16).substr(-6);
}
let row = 1;
let col = 1;
for (let i = 0; i < 28; i++) {
const div = document.createElement('div');
div.className = 'photo-item';
div.style.backgroundColor = randomColor();
div.style.gridRow = `${row} / ${row + 2}`;
div.style.gridColumn = `${col} / ${col + 2}`;
document.body.appendChild(div);
col += 2;
if (col > 11) {
row += 1;
col = row % 2 === 0 ? 2 : 1;
}
}
</script>
</html>
这是一个非常简单的照片墙效果,如果不使用
grid
的话,我们大概率是会使用定位去实现这个效果,但是换成grid
的话就非常简单了;
而且代码量是非常少的,这里就不提供
码上掘金
的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;
2. 漫画效果
在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用
grid
的话就非常简单了;
可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用
z-index
来实现,这里的z-index
值越大,元素就越靠前;
而且气泡文字效果也是通过
grid
来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;
3. 画报效果
在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;
在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用
grid
的话就会简单很多;
我这里将页面划分为
12 * 12
区域的网格,然后依次对不同的元素进行单独排列和样式的设置;
流式布局
流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;
但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;
通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))
这种;
直接看效果:
这里有两个关键字,一个是
auto-fit
,还有一个是auto-fill
,在行为上它们是相同的,不同的是它们在网格创建的不同,
就像上面图中看到的一样,使用
auto-fit
会将空的网格进行折叠,可以看到他们的结束colum
的数字都是6
;
像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位
fr
,只有使用固定单位才会出现这个现象;
感兴趣的同学可以将
minmax(200px, 1fr)
换成200px
尝试;
对比 Flex 布局
在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid
进行的布局基本上都是大框架;
当然上面也有一些布局使用flex
也是可以实现的,但是我们再换个思路,除了flex
可以做到上面的一些布局,float
布局、table
布局、定位布局其实也都能实现;
不同的是float
布局、table
布局、定位布局基本上都是一些hack
的方案,就拿table
布局来说,table
本身就是一个html
标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;
而web布局
发展到现在的我们有了正儿八经可以布局的方案flex
,为什么又要出一个grid
呢?
grid
的出现绝对不是用来替代flex
的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex
;
我个人理解的是使用grid
进行主体的大框架的搭建,flex
作为一些小组件的布局控制,两者搭配使用;
flex
能实现一些grid
不好实现的布局,同样grid
也可以实现flex
实现困难的布局;
本身它们的定位就不痛,flex
作为一维布局的首选,grid
定位就是比flex
高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;
总结
上面介绍的这么多基于grid
布局实现的布局方案,足以看出grid
布局的强大;
grid
布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid
布局去实现这些布局,来体会grid
带来的便利;
可能需要完全理解我上面的全部示例需要对grid
有一定的了解才可以,但是都看到这里了,不妨去深挖一下;
grid
布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid
还有很多小技巧来实现非常多的布局场景;
碍于我的见识和文笔的限制,我这次介绍grid
肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;
来源:juejin.cn/post/7310423470546354239
域名还能绑定动态IP?真是又涨见识了,再也不用购买固定IP了!赶快收藏
大家好,我是冰河~~
一般家庭网络的公网IP都是不固定的,而我又想通过域名来访问自己服务器上的应用,也就是说:需要通过将域名绑定到动态IP上来实现这个需求。于是乎,我开始探索实现的技术方案。
通过在网上查阅一系列的资料后,发现阿里云可以做到实现动态域名解析DDNS。于是乎,一顿操作下来,我实现了域名绑定动态IP。这里,我们以Python为例实现。
小伙伴们注意啦:Java版源码已提交到:github.com/binghe001/m…
好了,说干就干,我们开始吧,走起~~
阿里云DDNS前置条件
- 域名是在阿里云购买的
- 地址必须是公网地址,不然加了解析也没有用
通过阿里云提供的SDK,然后自己编写程序新增或者修改域名的解析,达到动态解析域名的目的;主要应用于pppoe拨号的环境,比如家里设置了服务器,但是外网地址经常变化的场景;再比如公司的pppoe网关,需要建立vpn的场景。
安装阿里云SDK
需要安装两个SDK库,一个是阿里云核心SDK库,一个是阿里云域名SDK库;
阿里云核心SDK库
pip install aliyun-python-sdk-core
阿里云域名SDK库
pip install aliyun-python-sdk-domain
阿里云DNSSDK库
pip install aliyun-python-sdk-alidns
设计思路
- 获取阿里云的accessKeyId和accessSecret
- 获取外网ip
- 判断外网ip是否与之前一致
- 外网ip不一致时,新增或者更新域名解析记录
实现方案
这里,我直接给出完整的Python代码,小伙伴们自行替换AccessKey和AccessSecret。
#!/usr/bin/env python
#coding=utf-8
# 加载核心SDK
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
# 加载获取 、 新增、 更新、 删除接口
from aliyunsdkalidns.request.v20150109 import DescribeSubDomainRecordsRequest, AddDomainRecordRequest, UpdateDomainRecordRequest, DeleteDomainRecordRequest
# 加载内置模块
import json,urllib
# AccessKey 和 Secret 建议使用 RAM 子账户的 KEY 和 SECRET 增加安全性
ID = 'xxxxxxx'
SECRET = 'xxxxxx'
# 地区节点 可选地区取决于你的阿里云帐号等级,普通用户只有四个,分别是杭州、上海、深圳、河北,具体参考官网API
regionId = 'cn-hangzhou'
# 配置认证信息
client = AcsClient(ID, SECRET, regionId)
# 设置主域名
DomainName = 'binghe.com'
# 子域名列表 列表参数可根据实际需求增加或减少值
SubDomainList = ['a', 'b', 'c']
# 获取外网IP 三个地址返回的ip地址格式各不相同,3322 的是最纯净的格式, 备选1为 json格式 备选2 为curl方式获取 两个备选地址都需要对获取值作进一步处理才能使用
def getIp():
# 备选地址:1, http://pv.sohu.com/cityjson?ie=utf-8 2,curl -L tool.lu/ip
with urllib.request.urlopen('http://www.3322.org/dyndns/getip') as response:
html = response.read()
ip = str(html, encoding='utf-8').replace("\n", "")
return ip
# 查询记录
def getDomainInfo(SubDomain):
request = DescribeSubDomainRecordsRequest.DescribeSubDomainRecordsRequest()
request.set_accept_format('json')
# 设置要查询的记录类型为 A记录 官网支持A / CNAME / MX / AAAA / TXT / NS / SRV / CAA / URL隐性(显性)转发 如果有需要可将该值配置为参数传入
request.set_Type("A")
# 指定查记的域名 格式为 'test.binghe.com'
request.set_SubDomain(SubDomain)
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
# 将获取到的记录转换成json对象并返回
return json.loads(response)
# 新增记录 (默认都设置为A记录,通过配置set_Type可设置为其他记录)
def addDomainRecord(client,value,rr,domainname):
request = AddDomainRecordRequest.AddDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1') # MX 记录时的必选参数
request.set_TTL('600') # 可选值的范围取决于你的阿里云账户等级,免费版为 600 - 86400 单位为秒
request.set_Value(value) # 新增的 ip 地址
request.set_Type('A') # 记录类型
request.set_RR(rr) # 子域名名称
request.set_DomainName(domainname) #主域名
# 获取记录信息,返回信息中包含 TotalCount 字段,表示获取到的记录条数 0 表示没有记录, 其他数字为多少表示有多少条相同记录,正常有记录的值应该为1,如果值大于1则应该检查是不是重复添加了相同的记录
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
relsult = json.loads(response)
return relsult
# 更新记录
def updateDomainRecord(client,value,rr,record_id):
request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1')
request.set_TTL('600')
request.set_Value(value) # 新的ip地址
request.set_Type('A')
request.set_RR(rr)
request.set_RecordId(record_id) # 更新记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
return response
# 删除记录
def delDomainRecord(client,subdomain):
info = getDomainInfo(subdomain)
if info['TotalCount'] == 0:
print('没有相关的记录信息,删除失败!')
elif info["TotalCount"] == 1:
print('准备删除记录')
request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
request.set_accept_format('json')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
request.set_RecordId(record_id) # 删除记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
result = client.do_action_with_exception(request)
print('删除成功,返回信息:')
print(result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查后再操作!")
# 有记录则更新,没有记录则新增
def setDomainRecord(client,value,rr,domainname):
info = getDomainInfo(rr + '.' + domainname)
if info['TotalCount'] == 0:
print('准备添加新记录')
add_result = addDomainRecord(client,value,rr,domainname)
print(add_result)
elif info["TotalCount"] == 1:
print('准备更新已有记录')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
cur_ip = getIp()
old_ip = info["DomainRecords"]["Record"][0]["Value"]
if cur_ip == old_ip:
print ("新ip与原ip相同,不更新!")
else:
update_result = updateDomainRecord(client,value,rr,record_id)
print('更新成功,返回信息:')
print(update_result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查删除后再操作!")
IP = getIp()
# 循环子域名列表进行批量操作
for x in SubDomainList:
setDomainRecord(client,IP,x,DomainName)
# 删除记录测试
# delDomainRecord(client,'b.jsoner.com')
# 新增或更新记录测试
# setDomainRecord(client,'192.168.3.222','a',DomainName)
# 获取记录测试
# print (getDomainInfo(DomainName, 'y'))
# 批量获取记录测试
# for x in SubDomainList:
# print (getDomainInfo(DomainName, x))
# 获取外网ip地址测试
# print ('(' + getIp() + ')')
Python脚本的功能如下:
- 获取外网ip地址。
- 获取域名解析记录。
- 新增域名解析记录。
- 更新域名解析记录。
- 删除域名解析记录 (并不建议将该功能添加在实际脚本中)。
- 批量操作,如果记录不存在则添加记录,存在则更新记录。
另外,有几点需要特别说明:
- 建议不要将删除记录添加进实际使用的脚本当中。
- 相同记录是同一个子域名的多条记录,比如 test.binghe.com。
- 脚本并没有验证记录类型,所以同一子域名下的不同类型的记录也会认为是相同记录,比如:有两条记录分别是 test.binghe.com 的 A 记录 和 test.binghe.com 的 AAAA 记录,会被认为是两条相同的 test.binghe.com 记录.如果需要判定为不同的记录,小伙伴们可以根据上述Python脚本自行实现。
- 可以通过判断获取记录返回的 record_id 来实现精确匹配记录。
最后,可以将以上脚本保存为文件之后,通过定时任务,来实现定期自动更新ip地址。
来源:juejin.cn/post/7385106262009004095
我为什么选择Next.js+Supabase做全栈开发
作为一名前端工程师,选择合适的技术栈对项目的成功至关重要,我最近一个星期尝试了下这两个技术栈的组合,大概在一个星期就写了一个小 SAAS,总共 10 多个页面。在本文中,我将分享为什么我选择Next.js 14和Supabase作为全栈开发的首选组合,并通过最新的代码示例和比较数据,直观地展示这个选择带来的诸多优势。
Next.js 14: 现代React应用的革新框架
默认服务器组件的优势
Next.js 14默认使用服务器组件,这对于提升性能和开发体验至关重要。
例如,一个简单的服务器组件如下:
// app/page.js
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Home() {
const data = await getData()
return <div>Welcome to {data.name}div>
}
在这个例子中,Home
组件是一个异步的服务器组件,它可以直接进行数据获取,而无需使用useEffect或getServerSideProps。
App Router: 更强大的路由系统
Next.js 14采用了新的App Router,提供了更灵活和直观的路由方式:
app/
page.js // 对应路由 /
about/
page.js // 对应路由 /about
posts/
[id]/
page.js // 对应路由 /posts/1, /posts/2, 等
Server Actions: 无需API路由的表单处理
Next.js 14引入了Server Actions,允许我们直接在服务器上处理表单提交,无需单独的API路由:
// app/form.js
export default function Form() {
async function handleSubmit(formData) {
'use server'
// 在服务器上处理表单数据
const name = formData.get('name')
// ...处理逻辑
}
return (
<form action={handleSubmit}>
<input type="text" name="name" />
<button type="submit">Submitbutton>
form>
)
}
这个能力好用到哭,不用再写API路由了,直接在页面上处理表单提交。代码简单了不止一点点。
Supabase: 开源Firebase替代品的崛起
数据库即服务的便利性
Supabase提供了PostgreSQL数据库即服务,使用起来非常简单:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
// 插入数据
const { data, error } = await supabase
.from('users')
.insert({ name: 'John', email: 'john@example.com' })
实时功能的强大支持
Supabase的实时订阅功能让实现实时更新变得轻而易举:
import { useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
function RealtimeData() {
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, payload => {
console.log('New user:', payload.new)
})
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])
return <div>Listening for new users...div>
}
身份认证和授权的简化
Supabase内置的身份认证系统大大简化了用户管理:
const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})
Next.js 14 + Supabase: 完美的全栈开发组合
开发效率的显著提升
结合Next.js 14和Supabase,我们可以快速构建全功能的Web应用。以下是一个简单的例子,展示了如何在Next.js 14的服务器组件中使用Supabase:
// app/posts/page.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
export default async function Posts() {
const { data: posts } = await supabase.from('posts').select('*')
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}div>
))}
div>
)
}
这个例子展示了Next.js 14服务器组件如何与Supabase无缝集成,直接在服务器端获取数据并渲染。
与其他技术栈的对比
为了更直观地展示Next.js 14+Supabase的优势,我们来看一个更新后的比较表格:
特性 | Next.js 14+Supabase | MERN Stack | Firebase | Django |
---|---|---|---|---|
默认服务器组件 | ✅ | ❌ | ❌ | N/A |
App Router | ✅ | ❌ | ❌ | ❌ |
Server Actions | ✅ | ❌ | ❌ | ✅ |
实时数据库 | ✅ | 需配置 | ✅ | 需配置 |
SQL支持 | ✅ (PostgreSQL) | ❌ (默认NoSQL) | ❌ (NoSQL) | ✅ |
身份认证 | ✅ | 需配置 | ✅ | ✅ |
学习曲线 | 中 | 中 | 低 | 高 |
全栈JavaScript | ✅ | ✅ | ✅ | ❌ |
开源 | ✅ | ✅ | ❌ | ✅ |
选型优势的直观感受
- 开发速度:使用Next.js 14+Supabase,你可以在几小时内搭建起一个包含用户认证、数据库操作和实时更新的全栈应用。
- 代码量减少:得益于Next.js 14的服务器组件和Supabase的简洁API,代码量可以减少40%-60%。
- 性能提升:通过Next.js 14的默认服务器组件和自动代码分割,页面加载速度可以提升40%-70%。
- 学习成本:虽然新概念(如服务器组件)需要一定学习时间,但整体学习曲线比传统全栈开发更平缓,2-3周即可上手。
- 维护简化:单一语言(TypeScript)贯穿全栈,加上Next.js的文件约定和Supabase的声明式API,大大减少了维护的复杂度。
- 可扩展性:Supabase基于PostgreSQL,为未来的扩展提供了更多可能性,而Next.js的渐进式框架特性也允许逐步采用高级功能。
一些想法
Next.js 14和Supabase是现代全栈开发的最佳选择,它们的结合提供了前所未有的开发体验和性能优势。如果你正在寻找一个全栈开发的新方向,不妨试试Next.js 14和Supabase,相信你会爱上这个组合。而且 supabase 学了也很划算,即便你想做 react native,Flutter,他都可以作为你坚实的后端。
来源:juejin.cn/post/7389925676520226825