注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

OPPO举办OTalk 开发者交流专场,提供Android 15多元化适配服务

在日前举行的2024谷歌 I/O 大会上,备受期待的 Android 15 Beta 版本操作系统正式亮相。作为 Android 生态系统的关键参与者,OPPO 连续六年首批适配 Android 新系统,第一时间推出基于 Android 15 Beta 的 C...
继续阅读 »

在日前举行的2024谷歌 I/O 大会上,备受期待的 Android 15 Beta 版本操作系统正式亮相。作为 Android 生态系统的关键参与者,OPPO 连续六年首批适配 Android 新系统,第一时间推出基于 Android 15 Beta 的 ColorOS 开发者预览版。

5月22日,OPPO 特别联合 51CTO 举办了「OTalk | Android 15 适配开发者交流专场」,以帮助开发者更好地理解和利用新版本的特性进行适配开发,活动以线上直播的形式展开,共吸引了27000+开发者和技术爱好者实时观看,并在40多个开发者社群中引发了热烈讨论。

1.png

OPPO技术大咖在线解答,拓宽开发者适配思路

全新的 Android 15 带来了一系列令人瞩目的新功能和改进,包括全新设计的兼容性调试工具、安全与隐私相关的强化措施、系统优化和新的API支持。

在此次「OTalk | Android 15 适配开发者交流专场」上,OPPO ColorOS 高级系统工程师纪昌杰首先通过带领大家回顾了Android历史版本的关键特性,帮助开发者更好地理解谷歌的更新逻辑,包括更安全地导出上下文注册的接收器和前台服务类型及权限的新要求等。

随后,纪昌杰全面且深入解析了 Android 15 的一系列新特性,特别是Manifest TAG限制、前台服务的启动限制、以及ART库中符号可见性属性的更新,这些改动旨在提高应用的安全性和性能,限制非公开API的访问,并确保服务的透明度与系统的及时响应。此外,纪昌杰还对Android 15 一些较小的更新进行了说明,如紧凑字体变更、提升的最低可安装目标API级别、Vulkan替换OpenGL ES、包名校验,以及16KB page size等功能。

2.png

此次 OTalk 通过详尽阐释这些新特性带来的影响,为开发者在适配过程中划明重点,提供了切实可行的适配策略。在互动答疑环节,面对开发者们的积极提问,纪昌杰还给出了一些针对性的解决方案和适配指导,确保开发者们能够快速、高效地应对新版本的变化。

Google 于2024年2月推出 Android 15 的开发者预览版,随后在4月发布 Android 15 首个 Beta 版本,为开发者提供了更加稳定的测试环境。6月份,平台稳定性里程碑版本将发布,帮助开发者规划最终测试和发布应用。最终的正式版也将在稳定版发布两个月后向公众推出,届时,Android 15 将全面开启全新的智能移动体验。鉴于 Android 15 的全新特性和适配计划,开发者可积极参与早期测试,尽快推进适配,确保应用在新系统上的平稳运行和优化。

3.png

能力共享服务多元,OPPO助力开发者高效有序适配

在此次OTalk上,纪昌杰还介绍了 OPPO 为支持开发者顺利适配 Android 15 所提供的全方位服务和支持。这其中包括详尽的兼容性适配指导文档,指引开发者迅速找到适配方案;免费的云真机/云测服务,可提供实时在线的远程调试功能,支持开发者随时接入,帮助开发者快速验证适配结果;开发者预览版允许早期测试应用在新系统上的表现,而应用上架应用商店新特性检测可以确保应用符合 Android 15 的所有新标准。此外,开发者还可以通过适配答疑交流社群、OPPO 开放平台适配支持专区等多元渠道获得支持,以提高适配效率。

4.png

「OTalk | Android 15 适配开发者交流专场」的成功举行,提供了更多 Android 15 高效适配思路,助力开发者及时解决适配疑难问题。本次 OTalk 活动中分享的适配资源信息以及高频适配问题解答,将在「OPPO开放平台」公众号及OPPO开发者社区官网发布,广大开发者可以随时查阅,并应用于实际开发当中。

OPPO也将持续为开发者提供全流程、多元化的适配支持服务,携手开发者共同推进新版本适配工作高效进行,打造更优质的用户体验。

收起阅读 »

8个小而美的前端库

web
前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。 2024年推荐以下小而美的库。 radash 实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数)...
继续阅读 »

前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。


2024年推荐以下小而美的库。


radash


实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数),源码可读性高,如果不想安装它,大部分函数可以直接复制到本地使用。



use-debounce


React Hook Debouce 库,让你不再为使用防抖烦恼。库的特点:体积小 < 1 Kb、与 underscore / lodash impl 兼容 - 一次学习,随处使用、服务器渲染友好。



timeago.js


格式化日期时间库,比如:“3 hours ago”,支持多语言,仅 2Kb 大小。同时提供了 React 版本 timeago-react。


timeage.format(1544666010224, 'zh_CN') // 输出 “5 年前”
timeage.format(Date.now() - 1000, 'zh_CN') // 输出 “刚刚”
timeage.format(Date.now() - 1000 * 60 * 5, 'zh_CN') // 输出 “5 分钟前”

react-use


实用 Hook 大合集 - 内容丰富,从跟踪电池状态和地理位置,到设置收藏夹、防抖和播放视频,无所不包。



dayjs


Day.js 是一个简约的 JavaScript 库,仅 2 Kb 大小。它可以使用基本兼容 Moment.js,为你提供日期的解析、处理和显示,支持多语言能力。



filesize


filesize.js 提供了一种简单方法,便于从数字(浮点数或整数)或字符串转换成可读性高的文件大小。


import {filesize} from "filesize";
filesize(265318, {standard: "jedec"}); // "259.1 KB"
driver.js:driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。

driver.js


driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。



@formkit/drag-and-drop


FormKit DnD 是一个小型库,它简单、灵活、与框架无关,压缩后只有 4Kb 左右,设计理念为数据优先。



小结


前端小而美的库使用起来一般都比较顺手,欢迎在评论区推荐你们开发中的使用小而美的库。


作者:晓得迷路了
来源:juejin.cn/post/7350140676615798824
收起阅读 »

如此丝滑的API设计,用起来真香

分享是最有效的学习方式。 博客:blog.ktdaddy.com/ 故事 工位上,小猫一边撸着代码,一边吐槽着前人设计的接口。 如下: “我艹,货架模型明明和商品SKU模型是一对多的关系,接口入参的时候偏偏要以最小粒度的SKU将重复入参进行平铺”。 “一个...
继续阅读 »

分享是最有效的学习方式。


博客:blog.ktdaddy.com/





故事


工位上,小猫一边撸着代码,一边吐槽着前人设计的接口。


如下:


“我艹,货架模型明明和商品SKU模型是一对多的关系,接口入参的时候偏偏要以最小粒度的SKU将重复入参进行平铺”。


“一个接口居然做了多件事情,传入参数复杂异常,不是一块业务类型的东西,非得全部揉在一起”。


“如此长的业务流程,接口能快起来么,难怪天天收到接口慢的告警”。


00.png


“这都啥啊,这名字怎么能这么取呢,这也太随意了吧....”


......


小猫一边写着V2版本的新接口,一边骂着现状接口。


聊聊APi设计


在日常开发过程中,相信大家在维护老代码的时候也多多少少会像小猫一样吐槽现有接口设计。很多项目经过历史沉淀以及业务验证,接口设计问题就慢慢放大暴露出来了。具体原因是这样的:


第一种情况可能是业务发展的必然趋势:不同技术人员对业务的看法和理解不同,一个接口可能经过多人的维护开发迭代,很多时候,新增功能也只是在原有的接口上直接拓展,当业务需求比较紧急的时候,大部分的研发一般都会选择快速去实现,而不会太过去考虑现有接口拓展的合规性。


第二种情况可能是本身开发人员自身能力问题,对业务的把控以及评估不合理导致的最终接口设计缺陷问题。


在系统软件开发过程中,一个好的UI设计可以让用户更好地使用一款产品。那么深入一层,一个好的API设计则可以让开发者高效地使用一个系统的能力,尤其是现在很多大型微服务项目中,API设计更加重要,因为此时的API调用方不仅仅是前端,甚至直接是其他服务。


那么接下来,老猫会和大家从下面的几个方面探讨一下,日常开发中我们应该如何去设计API。


0.png


API设计需要明确边界


在实际职场中,部门与部门之间、管理员与管理员之间容易出现扯皮、推诿现象。当然在系统和系统之间API的交互中其实往往也存在这样的情况。打个比方客户端的交互细节让后端代码通过接口来兜,你觉得合理不?


所以这就要求我们遵循下面两个点,咱们分别中两个维度来看,一个是“面向于服务和服务之间的API”,另一个是“面向客户端和服务之间的API”。


1、我们在设计API的过程中应该聚焦软件系统需要提供的服务或者能力。API是系统和外部交互的接口,至于外部如何使用,通过什么途径使用并不是重点。


2、对于面向UI的API设计中,我们更应该避免去过多关注UI的交互细节。交互属于客户端范畴,不同的终端设备,其交互必然也是不一样的。


API设计思路尽量面向结果设计而不是面向过程设计


相信大家应该都知道面向对象编程和面向过程编程吧。


老猫虽说的这里的面向结果设计其实和面向对象的概念有点类似。这种情况下的API应该是根据对象的行为来封装具体的业务逻辑,调用方直接发起请求需要什么就能给出一个最终的结果性质的东西,而不是中间过程中某个状态性质的东西。上层业务无需多次调用底层接口进行组装才能获取最终结果。


如下图:


面向执行过程API设计


面向最终结果API设计


举个例子。


银行提现逻辑中,


如果面向执行过程设计的API应该是这样的,先查询出余额,然后再进行扣减。于是有了下面这样的伪代码。


public interface BankService {
AccountInfo getAccountByUserName(String userName);
void updateAccount(AccountInfoReq accountInfoReq);
}

如果是面向结果设计,那么应该就是这样的伪代码。


public interface BankService {
AccountInfo withdraw(String userName,Long amount);
}

API设计需要尽量保证职责单一


在设计API的时候,应该尽力要求一个API只做一件事情,职责单一的API可以让API的外观更加稳定,没有歧义。并且上层调用层也是一目了然,简单易用。


对于一个API如果符合下面条件的时候,咱们就可以考虑对其进行拆分了。


1、一个API内部完成了多件事情。例如:一个API既可以发布新商品信息,又能更新商品的价格、标题、规格信息、库存等等。如果这些行为在一个接口进行调用,接口复杂度可想而知。
另外的接口的性能也是需要考虑的一部分,再者如果后续涉及权限粒度拆分,其实这种设计就不便于权限管控了。


2、一个API用于处理不同类型对象的业务。例如:一个API编辑不同的商品类型,由于不同类型的商品对应的模型通常是不同的(例如出行类的商品以及卡券类的商品差别就很大),
如果放在一个API中,API的输入和输出参数会非常复杂,使用和维护成本就很高。


其实关于API单一职责相关的话题,老猫在之前的文章中也有提及过,有兴趣的小伙伴可以戳【忍不了,客户让我在一个接口里兼容多种业务功能


API不应该基于实现去设计


在API设计过程中,我们应该避免实现细节。一个API有多种实现,在API层面不应该暴露实现细节,从而误导用户。


例如生成token是最为常见的,生成token的方式也会有很多种。可以通过各种算法生成token,
有的是根据用户信息的hash算法生成,或者也可以用base64生成,甚至雪花算法直接生成。如果对外暴露更多实现细节,其实内部实现的可拓展性就会相当差。
我们来看一下下面的代码。


//反例:暴露实现细节
public interface tokenService {
TokenInfo generateHashTokenByUserName(String userName);
}
//正例:足够抽象、便于拓展
public interface tokenService {
TokenInfo generateToken(Object key);
}

API的命名相当重要


一个好的API名字无疑是相当重要的,使用者一看API的命名就能知道如何使用,可以大大降低调用方的使用成本。所以我们在设计API的时候需要注意下面几个方面。


1、API的名字可以自解释,一个好的API的名称可以清晰准确概括出API本身提供的能力。


2、保持对称性。例如read/write,get/set。


3、基本的API的拼写务必准确。API一旦发布之后,只能增加新的API去订正,旧API完全没有请求量之后才能废弃,错误的API的拼写可能会带给调用方理解上的歧义。


API设计需要避免标志性质的参数


所谓标志性的参数,就是一个接口为了兼容不同的逻辑分支,增加参数让调用方去抉择。这块其实和上述提及的API设计保证职责单一有点重复,但是老猫觉得很重要,所以还是
单独领出来细说一下。举个例子,上述提及的发布商品,在发布商品中既有更新的原有商品信息的功能在,又有新增商品的功能在。于是就有了这样错误的设计,如下:


public class PublishProductReq {
private String title;
private String headPicUrl;
private List<Sku> skuList;

//是否为更新动作,isModify就是所说的标志性质的参数
private Boolean isModify;
.....
}

那么对应的原始的发布接口为:


//反例:内部入参通过isModify抉择区分不同的逻辑
public interface PublishService {
PublishResult publishProduct(PublishProductReq req);
}

比较好的逻辑应将其区分开来,移除原来的isModify标志位:


public interface PublishService {
PublishResult addProduct(PublishProductReq req);
PublishResult editProduct(PublishProductReq req);
}

API设计出入参需要保证风格一致


这里所说的出入参的风格一致主要指的是字段的定义需要保持一个,例如对外的订单编号,一会叫做outerNo,一会叫做outerOrderNo。相关的用户在调用的时候八成是会骂娘的。


老猫最近其实在对接供应商的相关API,调用对方创建发货订单之后返回的订单编号是orderNo,后来用户侧完成订单需要通知供应商,入参是outerNo。老猫此时是懵逼的,都不知道这个
outerNo又是个什么,后来找到对面的研发沟通了一轮才知道原来outerNo就是之前返回的orderNo。


于是“我艹,坑笔啊”收尾.....


API设计的时候考虑性能


最后再聊聊API性能,维护了很多的项目,发现很多小伙伴在设计接口的时候并不会考虑接口性能。或者说当时那么设计确实不会存在接口的性能问题,可是随着业务的增长,数据量的增长,
接口性能问题就暴露出来了。就像上面小猫吐槽的,接口又又又慢了,又在报接口慢警告了。


举个例子,查询API,当数据量少的情况下,一个List作为最终返回搞定没有问题的。但是随着时间的推移,数据量越来越大,List能够cover吗?显然是不行的,此时就要考虑是否需要通过分页去做。
所以原来的List的接口就必须要改造成分页接口。


当然关于API性能的优化提升,老猫整理了如下提升方式。


1、缓存:CRUD的读写性能毕竟是有限的。所以对某些数据进行频繁的读取,这时候,可以考虑将这些数据缓存起来,下次读取时,直接从缓存中读取,减少对数据库的访问,提升API性能。


2、索引优化:很多时候接口慢是由于数据库性能瓶颈,如果不用上述提及的缓存,那么我们就需要看一下接口究竟是慢在哪个环节,可能是某个查询,可能是更新,所以我们就要分析
执行的SQL情况去添加一些索引。当然这里涉及如何进行MYSQL索引优化的知识点了,老猫在此不展开。


3、分页读取:如上述老猫举的例子中,针对的是那种随着数据量增长暴露出来的,那么我们就要对这些数据进行分页读取处理。


4、异步操作:在一个请求中开启多任务模式。


异步操作模式


举个例子:订单支付中,支付是核心链路,支付后邮件通知是非核心链路,因此,可以把这些非核心链路的操作,改成异步实现,
这样就可以提升API的性能。常用的异步方式有:线程池,消息队列,事件总线等。当然自从Java8之后还有比较好用的CompletableFuture。


5、Json序列化:JSON可以将复杂的数据结构或对象转换为简单的字符串,以便在网络传输、存储或与其他程序交互时进行数据交换。
优化JSON序列化过程可以提高API性能。使用高效的序列化库,减少不必要的数据字段,以及采用更紧凑的数据格式,都可以减少响应体的大小,从而加快数据传输速度和解析时间。


6、其他提升性能方案:例如运维侧提升带宽以及网速等等


上述罗列了相关API性能提升的一些措施,如果大家还有其他不错的方法,也欢迎留言。


总结


谈及软件中的设计,无论是架构设计还是程序设计还是说API设计,
原则其实都差不多,要能够松耦合、易扩展、注意性能。遵循上述这些API的设计规则,
相信大家都能设计出比较丝滑的API。当然如果还有其他的API设计中的注意点也欢迎在评论区留言。


作者:程序员老猫
来源:juejin.cn/post/7369783680427409418
收起阅读 »

请大家一定不要像我们公司这样打印log日志

前言 最近接手了公司另一个项目,熟悉业务和代码苦不堪言。 我接手一个新项目,有个习惯,就是看结构,看数据库,搜代码。 其中搜代码是我个人这些年不知不觉形成的癖好,我下面给大家展示下这个小癖好。 正文 我面对一个到手的新项目,会主动去搜索一些关键词...
继续阅读 »

前言



最近接手了公司另一个项目,熟悉业务和代码苦不堪言。




我接手一个新项目,有个习惯,就是看结构,看数据库,搜代码。




其中搜代码是我个人这些年不知不觉形成的癖好,我下面给大家展示下这个小癖好。



正文



我面对一个到手的新项目,会主动去搜索一些关键词,让我对这个项目有个整体健康的认识。



1、直接打印堆栈



比如搜索了printStackTrace(),目的是为了看这个项目中有多少地方直接打印了堆栈。




不搜还好,一搜,沃日,这滚动条,是奏响我悲痛的序章,竟然到处都是这种打印,而且是release分支。



1.png



我抽点了一些,看看具体是怎么写的,比如下面这样。



2.png



再比如下面这样,我反正长见识了,也可能只是我不会。



3.png


2、堆栈+log



比较典型的可能是下面这样,我以前就见过不少次,堆栈和log混合双打。



4.png



还无意间发现了这样的打印方式,log、堆栈、throw,纵享丝滑,一气呵成,让我们一起摇摆,哎,一起摇摆哎~



5.png


3、log+Json



最后这种,我怀疑是正在看文章的很多人就干过的,入参打印JSON,舒爽的做法,极致的坑爹。




我公司这个更酸爽,用的还是FastJson。



6.png


4、小插曲



写到这里,我可以告诉大家我写这篇文章的初衷不是我想教大家学习,因为这就是常识的东西。




我是因为今天的一件事感到意外。




我同组的工作了12年的Java工程师,做过非常多的项目,也确实很有经验且有责任心的同事。




他也写过这样的代码,因为我用IDEA查看了提交人,其中就有他的贡献。




另外,我有把上面log+堆栈+throw的写法给他看看,他的回答非常理所当然。




“这有问题吗,没报错啊”




我当场石化了,然后尴尬的笑笑就聊别的话题了。




讲这个小插曲的原因是什么,一叶知秋,从他身上我能断定,这样的工程师比比皆是。




干了这么多年,连个基本的日志规范都没有概念,哪怕不看什么阿里编码规范,至少对基础性的东西有个了解吧。



5、日志规范


所以,我专程又把以前分享过给大家的阿里巴巴《Java开发手册(黄山版)》掏出来,找出了里面日志规范着重说明的这部分。



正确的打印日志方式如下:



7.png



再看这个,第8条,禁止直接打印堆栈。




第9条,正确的打印异常日志的规范,我本人也一直都是第9条这种方式打印的。




另外,第10条说的很清楚,为什么不要在log里面用JSON转换工具,说简单点就是可能会报错,然后导致业务不走了。




一个日志打印本来是辅助排查问题用的,结果影响了正常业务流程,你说这是不是隐患。



8.png



而且,还告诉你了要如何打印入参,就是用toString()方法就行。




看看,写得多好,但是有多少人真的看了,都像你买的网课一样存在那里摆烂了吧。



总结



希望大家认真看一看,虽然简单,可很多程序员就差这么点意思,还是要养成好习惯哦。




作者:程序员济癫
来源:juejin.cn/post/7275974397005201449
收起阅读 »

文科生在三本院校,读计算机专业

6岁,进入村小,一年级,老师问我的梦想是什么,我说我长大了我要成为科学家。9岁,三年级,知道科学家不现实,开始学习英语。又因为科学家英语不好发音,于是我的梦想变了,长大了我要成为经理。11岁,五年级,开始成为网瘾少年,边玩游戏边挣钱才是我的梦想,所以我长大了我...
继续阅读 »

6岁,进入村小,一年级,老师问我的梦想是什么,我说我长大了我要成为科学家。

9岁,三年级,知道科学家不现实,开始学习英语。又因为科学家英语不好发音,于是我的梦想变了,长大了我要成为经理。

11岁,五年级,开始成为网瘾少年,边玩游戏边挣钱才是我的梦想,所以我长大了我要做网吧管理员或开一间电脑修理店。

12岁,小升初,差1.5分进区内重点中学,调剂到普通中学。

中学情况:至少一半以上的人无法考进高中,校园暴力也很是常见的事。

初中三年,有沉沦过,也有突击努力过,受环境影响大,人是易染的

15岁,初中升高中,正常发挥,进入普通高中。

高中情况:至少一半以上的人无法考到本科分数线,年级内有一半的班是艺术类的,文理科几乎无211、985。文理科若有实力上重点本科(一本),能稳坐年级前3。

17岁,高二分文理艺术,数理太差,没钱且没天赋搞艺术类,选择了文科

高二暑假玩梦幻挣了7000+,真正取出到银行卡,但又成功戒掉了网瘾:每天12小时以上在游戏内做着重复的事,性质发生了变化,最终卖号,累计收益几万块。

18岁,高考正常发挥,考入三本院校,在广东也叫2B院校。

高三这一年要说梦想,是想考上二本,少付点学费。不过平时试卷测试或模拟考,始终在二本线上下徘徊,最终离二本分数线差4分。

我是2B里靠前的

18岁,填选计算机专业,在文科院校学习计算机

最初志愿选的是工商管理,后来我爸不知道在哪听说到互联网+,最后就让我选了计算机科学与技术专业。


《底层程序员》我的故事持续连载中,下一篇:「上课,是耽误我学习了

收起阅读 »

学校上课,是耽误我学习了。。

>>上一篇(文科生在三本院校,读计算机专业) 2015年9月,我入学了。 我期待的大学生活是多姿多彩的,我会参加各种社团,参与各种有意思的活动。 但我是个社恐,有过尝试,但还是难以融入各种社交活动。 学习,我是有想过的。 学校开设的C++课程已经上...
继续阅读 »

>>上一篇(文科生在三本院校,读计算机专业


2015年9月,我入学了。


我期待的大学生活是多姿多彩的,我会参加各种社团,参与各种有意思的活动。


但我是个社恐,有过尝试,但还是难以融入各种社交活动。


学习,我是有想过的。


学校开设的C++课程已经上了一段时间,但我无法理解双层for循环执行过程、亦无法理解代码最终运行效果是黑框字符,更无法理解算法的美。


打印杨辉三角形?这到底有什么用啊!


面向对象?不都是大象放进冰箱吗!


我开始觉得校园生活很无趣,那回归老本行打游戏吧。


为了报考学计算机,我还买了游戏本呢。


高中的时候接触过DOTA、LOL这类游戏,刚上手的时候喜欢的不得了,实时+炫酷技能+公平,让我感叹这才是游戏啊!梦幻那都是什么坑人的东西。


不过我没有玩下去,我竞技水平太菜了,反应力跟不上。


不过现在有960M显卡的加持,我怒下了几款三A大作,却发现自己晕3D。


好了,游戏不用玩了。


后来,我沉迷各种电影&动漫&悬疑小说。


我这人就爱看经典,甭管我看没看懂,反正豆瓣低于8分我不看。


除了作品本身,我看别人影评也是一种享受,就爱看他们是怎么吹的。每每看到,原来这里还能这样解读,我就浑身发爽。


有过好几次,宿舍午休关了灯,外面下着雨,下午没课,我躺着床上看悬疑小说。很快,我刷完一本,找了些影评看,心满意足。


但当我静下来时,负罪感油然而生。


给这么贵的学费,我好像什么都没学到,等毕业找工作的时候,我该怎么办。


负罪感是暂时的,吃顿饭就消散了。


每当焦虑时,我就爱去知乎搜索:


「C++好还是Java好」、「如何入门编程」、「计算机什么方向容易就业」、「编程学到什么程度能找到工作」、「Java的学习路线」


看到满意的回答就点个收藏。


那时候的知乎百花争鸣,不像现在动不动就卖课。


「程序员的三大浪漫」、「数学是计算机的基础」这些内容都是真大道理,毕竟这么多大佬点赞了。


但越看这些,就越发感觉编程和计算机领域遥不可及。


这期间,课我有好好上,作业也有好好做,但编程是没能入门


上课老师对着PPT讲述一番之后,用Microsoft Visual C++ 6.0手敲着各种字符,我都不知道老师是怎么把代码记下来的。


大一就学个C++课,计算机类的课程占比很少,有时我还怀疑是不是读的计算机专业。


很快啊,大一学期快过去了,我在学期末意外地下载了些网课,看了几集,得出的结论:


原来,上课,是耽误我学习了


不是我学不会,不够努力,是老师教不好




《底层程序员》我的故事持续连载中,下一篇:「爪哇,我初学乍道


作者:Java3y
来源:juejin.cn/post/7370955971017146378
收起阅读 »

我为展开收起功能做了动画,被老板称赞!

web
需求简介 这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。 实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果, 最简单的就是使用element ui、或者a...
继续阅读 »

需求简介


这几天接了个新项目,需要实现下图中左侧边栏的菜单切换。这种功能其实就是一个折叠面板,实现方式多种多样。



实现上面的功能,无非就是一个v-show的事儿,但没有过渡,会显得非常生硬。想添加一些过渡效果,



最简单的就是使用element ui、或者ant的折叠面板组件了。但可惜的是,我们的项目不能使用任何第三方组件库。



为了做好产品,我还是略施拳脚,实现了一个简单且丝滑的过渡效果:



老板看后,觉得我的细节处理的很好,给我一顿画饼,承诺只要我好好坚持,一定可以等到升职加薪!当然,我胃口小,老板的饼消化不了。我还是分享一下自己在不借助第三方组件的情况下,如何快速的实现这样一个效果。


技术实现方案


业务分析


仔细观察需求,我们可以分析出其实动画主要是两个部分:一级标题的箭头旋转二级标题区域的折叠展开



我们先实现一下基本的html结构:


<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow">
>
</span>
</div>

<div v-show="open" class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>

</div>

</template>

<script setup>
const open = ref(false);
</script>


上述代码非常简单,点击一级标题时,更改open的值,从而实现二级标题的内容区域展示与隐藏。


箭头旋转动画



实现箭头旋转动画其实非常容易,我们只要在红色面板展开时,给箭头添加一个新的类名,在这个类名中做一些动画处理即可。


<template>
<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow flex-be-ce" :class="{ open: open }">
>
</span>
</div>

</template>
<style lang="less" scoped>
.arrow {
width: 16px;
height: 16px;
cursor: pointer;
margin-left: 1px;
transition: transform 0.2s ease;
}
.open {
transform: rotate(90deg);
transition: transform 0.2s ease;
}
</style>


上述的代码通过 CSS 的 transform 属性和动态绑定open类名实现了箭头的旋转效果。



注意:arrow也需要定义过渡效果



折叠区域动画效果


要实现折叠区域的动画效果,大致思路和上面一样。


使用vue的transition组件实现


借助vue的transition组件,我们可以实现折叠区域进入(v-show='true')和消失(v-show='fasle')的动画。一种可行的动画方案就是让面板进入前位置在y轴-100%的位置,进入后处于正常位置。



<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow" :class="{ open: open }">
>
</span>
</div>

<div class="content-wrap">
<Transition>
<div v-show="open" class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>
</Transition>
</div>

</div>

</template>

<script setup>

const open = ref(false);
</script>


<style lang="less" scoped>
.v-enter-active,
.v-leave-active {
transition: transform 0.5s ease;
}
.v-enter-from,
.v-leave-to {
transform: translateY(-100%);
}
</style>


上述效果有一点瑕疵,就是出现位置把一级标题盖住了,我们稍微修改下


<div class="content-wrap">
<Transition>
<div v-show="open" class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>
</Transition>

</div>

.content-wrap {
overflow: hidden;
}


使用动态类名的方式实现


效果好很多!但这种效果和第三方组件库的效果不太一致,我们以element的折叠面板效果为例:



我们可以发现,它的这种动画,是折叠面板的高度从0逐渐增高的一个过程。所以最简单的就是,如果我们知道折叠面板的高度,一个类名就可以搞定!


<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow flex-be-ce" :class="{ open: open }">
>
</span>
</div>

<div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
<div class="content">
<p>算法及跃变计算条件</p>
<p>空间品质判断条件</p>
<p>需求自动计算条件</p>
<p>通风系统</p>
</div>
</div>

</div>

</template>

<script setup>
const open = ref(false);
</script>


<style lang="less" scoped>
.content-wrap {
height: 0;
transition: height 0.5s ease;
}
</style>



如果这个折叠面板的内容通过父组件传递,高度是动态的,我们只需要使用js计算这里的高度即可:


<template>
<div class="nav-bar-content">

<div class="header-wrap" @click="open = !open">
<span class="text">自动化需求计算条件输如</span>
<span class="arrow flex-be-ce" :class="{ open: open }">
>
</span>
</div>

<div class="content-wrap" :style="{ height: open ? '300px' : 0 }">
<div class="content" ref="contentRef">
<slot></slot>
</div>
</div>

</div>

</template>

<script setup>
const open = ref(false);
const contentRef = ref();
const height = ref(0);
onMounted(() => {
height.value = contentRef.value.offsetHeight + 'px';
});
</script>


<style lang="less" scoped>
.content-wrap {
height: 0;
transition: height 0.5s ease;
}
</style>


这样,我们就通过几行代码就实现了一个非常简单的折叠面板手风琴效果!



总结


要想实现一个折叠面板的效果,最简单的还是直接使用第三方组件库,但是如果项目不能使用其他组件库的话,手写一个也是非常简单的!也希望大家能在评论区给出更好的实现方式,供大家学习!


作者:石小石Orz
来源:juejin.cn/post/7369029201579278351
收起阅读 »

为什么年轻人要珍惜机会窗口

今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。...
继续阅读 »


今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。


什么是好机会


就以职业的成长性来说,互联网整个行业的二十年蓬勃发展就是极好的一个机会,大概从20年起到如今这个时间段都有一个非常好的机会,那指的就是哪怕你的能力稍微弱一点,你都能够在这个机会里面找到自己的红利。比如我有很多稍微找我几届的同事或者主管,他们可能在学历或者能力方面都没有特别高,但是正因为赶上了红利,他们的晋升特别快,拿到了股票也特别多,我好几个同事基本上在上海或者杭州都有两三套房,并且还有大量的现金。甚至有一些大专的同事,都拿到大量的股票,接近财富自由。


所以这种机会窗口是整个行业变革,整个现代社会发展带来的,它打开了一扇可以改变命运的窗口。这种时间窗口相对来说会比较长,特别是相对一个人的职业三十年来说。而且这种行业的机会,可能就有持续五年或者十年这样的时间。而在这样的机会窗口内,你不管是哪个点入局都能吃到一定的发展红利。


比如我记得早个五六年,很多人在找工作的时候,往往会纠结于去百度还是腾讯或者是阿里,但实际上我们发现站在更高,更长远的角度来说,他们选择任何一个公司收获到的都非常的丰厚,相比现在的毕业生,哪怕是双985可能也是无法找到一份工作,想想那时候是不是很幸福?在这种大背景下,在机会窗口来临的时候,你选错了,选的不是很好,都没有关系,你都能够收获到足够的红利,最多就是你赚50万还是100万的区别,而时代没有的话,上限就是赚10万。


除了这个例子之外,还有一个红利机会点就是房地产。我知道在差不多2005年~2018年这个时间段里面,只要你买房基本上都是赚的,所以我很多同学往往都有一个非常巨大的认知论,就认为他买房赚钱是因为他牛逼,他地段选的好,户型选的好,他完全归因于他买的房价大涨是因为眼光好,怎么样怎么样才能赚到钱,而实际上这只是时代给他的红利而已,其实再往回倒个七八年你在哪里买房都是赚的。但实际上以我的经验来看,不管那个时候,哪怕你在小城市买一套房子,涨幅可能都是两三倍的。


所以当时的眼光和认知和选择能力确实会决定了你的资产增值多少,但是只要在那个红利周期内,你做的选择大概率都不会太差,这也是雷军所说,站在风口上的猪也可以飞起来,说的就是这个道理。



这就是整个时代给我们的窗口,这个窗口可能会给的特别大,而且很多时候在这个周期里面,你根本感觉不到这是时代给你的机会,你只是做你正常的操作,到了指定的时间去指定的公司,去选合适热门专业,去买认为合适的房子,你觉得很自然,但实际上从后面再看,你会发现你在十年前做的选择和十年后做的选择成本、难度以及你付出的代价完全不一样。同样是89平米的房子,放在2010年就是3000一平米,放在现在就是8万一平米。同样是去阿里巴巴,以前大专就行,现在本硕985都直接被Pass。


上面说的都是比较大的机会,那我再说一个相对来说比较小的窗口。这些非常大的机会窗口还是依赖于各种不同不一样的大背景,但是有很多机会并没有像这种时代给的机会一样,可以有长达五年,十年你可以认真去选,你可以去大胆的犯错和试错,选错了你重新再来一次就可以了,但是我们在实际工作里面,我们碰到的一些机会点,其实时间窗口非常的短。如果你稍微不慎,可能就错过了这个机会,而等待下一个机会就不知道猴年马月了,所以我们就要在这个地方要抓住那稍纵即逝的机会窗口。



我举一个例子,比如说这两年是低代码的元年,而这个时候如果你之前刚好一直在从事低代码或者低代码相关的工作,那么到了这两年,你的议价空间是非常大的,因为很多公司都在如火如荼的去做这块的业务,在短时间内是没有办法慢慢培养出或者招聘到这类专才,所以往往公司愿意溢价去花费大价钱去购买和招聘相关的同学,所以这个时候如果你抓住了机会,你可以得到一个很高的议价,比如说层级直接变高了一层或者你的总包直接变成了两倍,甚至非常有机会作为骨干负责人拉起一支团队,那么你进入管理岗位也就水到渠成了。


为什么机会有窗口


而这种机会窗口往往只有半年,一年或者最多两年,因为到了一两年之后,有很多的同学也感知到了这个先机,往往就会把自己的精力投到这一块来,那么意味着供需就发生了变化,供应方就会越来越多,那么就使得需求方有溢价的能力,这个时候到了两年之后可能就完全拉平了,这个低代码行业跟其他行业变得完全一样,甚至再往后人才堆积的更加的过分,你可能连这个机会都没有了,只剩下被选择的命运。历史历代,都演绎着完全相同的剧本。


到了直播行业也是一样,在直播刚刚兴起的时候,如果你恰巧做的是相关业务,这个时候你跳过去往往会能够涨薪特别高,工资的幅度也是特别高,所以在这个时候你有充分的议价权,但是窗口我们也知道往往只有几年,而且在互联网这么变化快的情况下的话,时间可能会进一步缩短,比如这两年已经到了直播的红海,基本上该用直播的用户已经到顶了,这个时候虽然还有大把的招聘,但需求实际上已经是强弩之末了。


随着人口红利到底的时候,我们所谓的互联网这些机会的窗口实际上已经是没了,变得普普通通的一份职业而已,而且这个时候入局往往有可能会遭受灭顶之灾,比如说最近就听说到整个直播行业要整顿,一旦业务发生了整顿,对人才的需求的调整就会变得非常的明显,往往再激烈一点可能就会快速裁员,不要说红利了,拿到的全部是负债。


再往小的一些说,可能针对每个人的职业窗口也是不一样的,比如说对于有些大企业,有一些管理的岗位,但往往是因为原管理的同学离职或者新增的岗位,这个时候会有短时间的招聘名额来等待这个位置,而一旦你错过了这个机会以后,这个位置没了以后,可能这个坑位就不需要人了。这个时候不是你能力好不好的问题,是有没有坑位的问题。


所以好机会往往只是一瞬间而已,很多同学担心稳定性,希望在一个地方一直苟着求稳定,这个其实跟体制内没有任何的区别。风险和收益从哲学层面上来说,都是相对的,或者说没有决定的风险,也没有决定的稳定,风险和稳定阶段性只能取其一,长期看稳定和风险是互相转化的。我经常听到有人说大厂稳定,但是实际上我们在分析背后的原因,大厂稳定本身就是个伪命题。又稳定,又高薪,又轻松,这是不可能的。所以我称之为「工作不可能的三角特点」。


但很多人说我能否要里面的两个因素,我要稳定要高薪但是我愿意加班吃苦。


对不起,这个其实也是不可能的。我们可以站在企业的角度来考虑一下,一旦我这个工作特别的高薪又稳定的情况下的话,那虽然你干的很苦,但我始终在人力成本特别充分的情况下的话,公司能找到更好的替代者来。同样的工作量,但是花更少的钱来解决,说白了大部分所谓的高薪岗位没有什么严格的技术壁垒。


所以我们说过的,站在更大的角度来说,互联网也是一个机会窗口,因为过了这个窗口之后,可能你想加班加点熬夜,你可能都拿不到这样的一个薪水和待遇。


如何抓住机会窗口


反而换一个角度来说,我们一定要抓住这样的机会窗口,这样的机会窗口可以给我们的发展带来一个质的变化,当然也有很多时候我们会做一些错误的选择,比如说我们找到了一个我们认为好的机会,但实际上这个机会是有问题的,比如说我去了某一个创业公司,原本以为会有巨大的发展,但是后面倒闭了。当然这个也是一种博弈,这里面非常考核一个同学的综合的认知能力、选择能力和纠错能力。不仅要判断能否找到合适的机会,还要在碰到了困难的时候能够去快速的去纠错。


从我的例子来看,如敢于去挑战这种新机会的同学,哪怕其中有一些不如意的变动,但是大概率他的结果大概率不会太差。比如我有个同学从集团跳槽到蚂蚁国际,呆了一年就觉得部门有问题,后面又去了字节头条,现在也非常稳定。还有一个同学出去创业,也不顺利,但是后面又折腾成了另外一个大型公司的高级主管。


反而是事事求稳,稳住某一个大厂,稳住某一个职位,稳住每一个薪水,到了最后往往收益会越来越小,直到最后完全被动。整体上来看,整个社会会把更多的报酬分向于这些敢于挑战,敢于冒险,敢于拼搏的人的,而不会把大量的资源分享到又稳定,又顽固,又不愿意改变的这群人,这是当前社会的游戏规则。这个在大数据上面完全是合理的,只不过落到每个人的头上的尺度和比例会有点不一样。


所以站在我现在的角度上来看,我觉得所有的想向上奋进的同学都应该主动抓住变革的机会。因为这个好机会可能对在你的人生来说,几十年可能就这么一两次,甚至有些都是完全为你量身定做的机会,如果你一旦错过了以后,可能你抓住下一个机会的成本和代价就变得会非常的大。



尤其是年轻人更应该去折腾,因为你的试错的成本会非常低,当你发现了你的错误决策以后,你能够快速的去更正,去变化,所以在年轻的时候往往就应该多折腾一点,善于去准备好去等待好的机会,如果机会来了,大胆的出击。




作者:ali老蒋
来源:juejin.cn/post/7296865632166805513
收起阅读 »

解决LiveData数据倒灌的新思路

⏰ : 全文字数:5500+ 🥅 : 内容关键字:LiveData数据倒灌 数据倒灌现象 对于LiveData“数据倒灌”的问题,我相信很多人已经都了解了,这里提一下。所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一...
继续阅读 »

⏰ : 全文字数:5500+

🥅 : 内容关键字:LiveData数据倒灌



数据倒灌现象


对于LiveData“数据倒灌”的问题,我相信很多人已经都了解了,这里提一下。所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一次的历史数据传递给当前注册的观察者


比如在在下面的例子代码中:


val testViewModel = ViewModelProvider(this)[TestViewModel::class.java]
testViewModel.updateData("第一次发送数据")
testViewModel.testLiveData.observe(this,object :Observer{
override fun onChanged(value: String) {
println("==============$value")
}
})

updateData方法发送了一次数据,当下面调用LiveData的observe方法时,会立即打印==============第一次发送数据,这就是上面说的“数据倒灌”现象。


发生原因


原因其实也很简单,其实就是 LiveData内部有一个mVersion字段,记录版本,其初始的 mVersion 是-1,当我们调用了其 setValue 或者 postValue,其 mVersion+1;对于每一个观察者的封装 ObserverWrapper,其初始 mLastVersion 也为-1,也就是说,每一个新注册的观察者,其 mLastVersion 为-1;当 LiveData 设置这个 ObserverWrapper 的时候,如果 LiveDatamVersion 大于 ObserverWrappermLastVersionLiveData 就会强制把当前 value 推送给 Observer


也就是下面这段代码


    private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}

if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
// 判断observer的版本是否大于LiveData的版本mVersion
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}

所以要解决这个问题,思路上有两种方式:



  • 通过改变每个ObserverWrapper的版本号的值

  • 通过某种方式,保证第一次分发不响应


解决方法


目前网络上可以看到有三种解决方式


每次只响应一次


public class SingleLiveData<T> extends MutableLiveData<T> {
private final AtomicBoolean mPending = new AtomicBoolean(false);

public SingleLiveData() {
}

public void observe(@NonNull LifecycleOwner owner, @NonNull Observersuper T> observer) {
super.observe(owner, (t) -> {
if (this.mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}

});
}

@MainThread
public void setValue(@Nullable T t) {
this.mPending.set(true);
super.setValue(t);
}

@MainThread
public void call() {
this.setValue((Object)null);
}
}

这个方法能解决历史数据往回发的问题,但是对于多Observe监听就不行了,只能单个监听,如果是多个监听,只有一个能正常收到,其他的就无法正常工作


反射


这种方式就是每次注册观察者时,通过反射获取LiveData的版本号,然后又通过反射修改当前Observer的版本号值。这种方式的优点是:



  • 能够多 Observer 监听

  • 解决粘性问题


但是也有缺点:



  • 每次注册 observer 的时候,都需要反射更新版本,耗时有性能问题


UnPeekLiveData


public class UnPeekLiveData extends LiveData {

protected boolean isAllowNullValue;

private final HashMap observers = new HashMap();

public void observeInActivity(@NonNull AppCompatActivity activity, @NonNull Observer super T> observer) {
LifecycleOwner owner = activity;
Integer storeId = System.identityHashCode(observer);
observe(storeId, owner, observer);
}

private void observe(@NonNull Integer storeId,
@NonNull LifecycleOwner owner,
@NonNull Observer super T> observer) {

if (observers.get(storeId) == null) {
observers.put(storeId, true);
}

super.observe(owner, t -> {
if (!observers.get(storeId)) {
observers.put(storeId, true);
if (t != null || isAllowNullValue) {
observer.onChanged(t);
}
}
});
}

@Override
protected void setValue(T value) {
if (value != null || isAllowNullValue) {
for (Map.Entry entry : observers.entrySet()) {
entry.setValue(false);
}
super.setValue(value);
}
}

protected void clear() {
super.setValue(null);
}
}

这个其实就是上面 SingleLiveData 的升级版,SingleLiveData 是用一个变量控制所有的 Observer,而上面采用的每个 Observer 都采用一个控制标识进行控制。
每次 setValue 的时候,就打开所有 Observer 的开关,表示可以接受分发。分发后,关闭当前执行的 Observer 开关,即不能对其第二次执行了,除非你重新 setValue
这种方式基本上是比价完美了,除了内部多一个用HashMap存放每个Observer的标识,如果Observer比较多的话,会有一定的内存消耗。


新的思路


我们先看下LiveData获取版本号方法:


int getVersion() {
return mVersion;
}

这个方法是一个包访问权限的方法,如果我新建一个和LiveData同包名的类,是不是就可以不需要反射就能获取这个值呢?其实这是可行的


// 跟LiveData同包名
package androidx.lifecycle

open class SafeLiveData<T> : MutableLiveData<T>() {

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
// 直接可以通过this.version获取到版本号
val pictorialObserver = PictorialObserver(observer, this.version > START_VERSION)
super.observe(owner, pictorialObserver)
}

class PictorialObserver<T>(private val realObserver: Observer<in T>, private var preventDispatch: Boolean = false) :
Observer {

override fun onChanged(value: T) {
// 如果版本有差异,第一次不处理
if (preventDispatch) {
preventDispatch = false
return
}
realObserver.onChanged(value)
}

}
}

这种取巧的方式的思路就是:



  • 利用同包名访问权限可以获取版本号,不需要通过反射获取

  • 判断LiveDataObserver是否有版本差异,如果有,第一次不响应,否则就响应


我个人是偏向这种方式,也应用到了实际的开发中。这种方式的优点是:改动小,不需要反射,也不需要用HashMap存储等,缺点是:有一定的侵入性,假如后面这个方法的访问权限修改或者包名变动,就无效了,但是我认为这种可能性是比较小,毕竟androidx库迭代了这么多版本,算是比较稳定了。



作者:卒子行
来源:juejin.cn/post/7268622342728171572
收起阅读 »

一时兴起,聊聊当今IT行业的乱象

本文写于2024年3月31号,大的背景是行业寒冬,工作岗位的数量和质量都远远不如之前,造成了打工人卷的飞起的现象,但是从企业端去看,却是面临高端人才不足,低端人才过剩以及招的人数很多但是却满足不了业务需求的问题。 本文所描述现象有作者自己的真实经历,也有道听途...
继续阅读 »

本文写于2024年3月31号,大的背景是行业寒冬,工作岗位的数量和质量都远远不如之前,造成了打工人卷的飞起的现象,但是从企业端去看,却是面临高端人才不足,低端人才过剩以及招的人数很多但是却满足不了业务需求的问题。


本文所描述现象有作者自己的真实经历,也有道听途说但是真实存在的现象~


一、词汇高大上,过后却一地鸡毛


造词现象普遍发生在大厂牛逼人物向上汇报或者是全员会的ppt中,这些牛逼的人物已经不屑于用已存的词汇来表达自己的想法,他们会把现有的词汇融会贯通,进而创造出新的牛x词,给人一种创新的感觉,让人一下子觉得这才是核心科技。


二、无效卷



  • 白天不怎么干活或者磨洋工,但是到了晚上才认真干起活来,故意加班到很晚,其实p事都没干。

  • 故意很晚的时间群里@下同事


三、产品经理只管要要要,研发只管干干干


其实这点很可怕,一般来说对于产品经理,产品就像自己的娃一样,自己再熟悉不过。但是现实是很多产品经理可能连这个娃有没有xjj都不知道😂。研发不管需求是不是解决问题,也不会考虑实际问题,只管完成crud的任务。


四、无脑跟进新技术


比如最近几年兴起的大模型,那好,我们怎么可以落后于行业呢,我们自己也来搞个,虽然不知道对于我们有什么用,但贵在自研啊。


五、文档一坨狗屎


很多大厂对外的文档,比如云厂商的,用户照着文档一步一步做都会失败。


六、PUA


下面这段也是老经典语录了


其实,我对你是有一些失望的。当初给你定级px,是高于你面试时的水平的。我是希望进来后,你能够拼一把,快速成长起来的。px这个层级,不是把事情做好就可以的。
你需要有体系化思考的能力。你做的事情,他的价值点在哪里?你是否做出了壁垒,形成了核心竞争力?你做的事情,和公司内其他团队的差异化在哪里?你的事情,是否沉淀了一套可复用的物理资料和方法论?为什么是你来做,其他人不能做吗?你需要有自己的判断力,而不是我说什么你就做什么。后续,把你的思考沉淀到日报周报月报里,我希望看到你的思考,而不仅仅是进度。
提醒一下,你的产出,和同层级比,是有些单薄的,马上要到年底了,加把劲儿。你看咱们团队的那个谁,人家去年晋升之前,可以一整年都在项目室打地铺的。成长,一定是伴随着痛苦的,当你最痛苦的时候其实才是你成长最快的时候。


作者:李少博
来源:juejin.cn/post/7352079468507594788
收起阅读 »

如果写劣质代码是犯罪,那我该判无期

导读 程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到...
继续阅读 »



导读


程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到的不整洁代码行为,并提出针对性优化建议。继续阅读~


目录


1 代码风格和可读性


2 注释


3 错误处理和异常处理


4 代码复用和模块化


5 硬编码


6 测试和调试


7 性能优化


8 代码安全性


9 版本控制和协作


10 总结


01、代码风格和可读性



  • 错误习惯


不一致的命名规则:使用多种命名规则,如 camelCase、snake_case 和 PascalCase 等。过长的函数和方法:编写过长的函数和方法,导致代码难以阅读和理解。 过长的行:编写超过50字符的代码行,导致代码难以阅读。

1.1 变量命名不规范


在编程中,变量命名是非常重要的,良好的变量命名能够提高代码的可读性和可维护性。不规范的命名会增加理解难度,以下是一个不规范命名的例子:


int a, b, c; // 不具有描述性的变量名
float f; // 不清楚变量表示的含义

这样的变量命名不仅会降低代码的可读性,还可能会导致变量混淆,增加代码维护的难度。正确的做法应该使用有意义的名称来命名变量。例如:


int num1, num2, result; // 具有描述性的变量名
float price; // 清晰明了的变量名

1.2 长函数和复杂逻辑


长函数和复杂逻辑是另一个常见的错误和坏习惯。长函数难以理解和维护,而复杂逻辑可能导致错误和难以调试。以下是一个长函数和复杂逻辑的案例:


def count_grade(score):
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'F'

if grade == 'A' or grade == 'B':
result = 'Pass'
else:
result = 'Fail'
return result

在这个例子中,函数 count_grade 包含了较长的逻辑和多个嵌套的条件语句,使得代码难以理解和维护。正确的做法是将逻辑拆分为多个小函数,每个函数只负责一个简单的任务,例如:


def count_grade(score):
grade = get_grade(score)
result = pass_or_fail(grade)
return result
def get_grade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def pass_or_fail(grade):
if grade == 'A' or grade == 'B':
return 'Pass'
else:
return 'Fail'

通过拆分函数,我们使得代码更加可读和可维护。


1.3 过长的行


代码行过长,会导致代码难以阅读和理解,增加了维护和调试的难度。例如:


def f(x):
if x>0:return 'positive' elif x<0:return 'negative'else:return 'zero'

这段代码的问题在于,它没有正确地使用空格和换行,使得代码看起来混乱,难以阅读。正确的方法是,我们应该遵循一定的代码规范和风格,使得代码清晰、易读。下面是按照 PEP 8规范改写的代码:


def check_number(x):
if x > 0:
return 'positive'
elif x < 0:
return 'negative'
else:
return 'zero'

这段代码使用了正确的空格和换行,使得代码清晰、易读。


02、注释



  • 错误习惯


缺少注释:没有为代码编写注释,导致其他人难以理解代码的功能和逻辑。 过时的注释:未及时更新注释,使注释与实际代码不一致。 错误注释:注释上并不规范,常常使用一些不合理的注释。


  • 错误的注释


注释是非常重要的,良好的注释可以提高代码的可读性和可维护性。以下是一个不规范的例子:


int num1, num2; // 定义两个变量

上述代码中,注释并没有提供有用的信息,反而增加了代码的复杂度。


03、错误处理和异常处理



  • 错误的习惯


忽略错误:未对可能出现的错误进行处理。 过度使用异常处理:滥用 try...except 结构,导致代码逻辑混乱。 捕获过于宽泛的异常:捕获过于宽泛的异常,如 except Exception,导致难以定位问题。

3.1 忽略错误


我们往往会遇到各种错误和异常。如果我们忽视了错误处理,那么当错误发生时,程序可能会崩溃,或者出现不可预知的行为。例如:


def divide(x, y):
return x / y

这段代码的问题在于,当 y 为0时,它会抛出 ZeroDivisionError 异常,但是这段代码没有处理这个异常。下面是改进的代码:


def divide(x, y):
try:
return x / y
except ZeroDivisionError:
return 'Cannot divide by zero!'

3.2 过度使用异常处理


我们可能会使用异常处理来替代条件判断,这是不合适的。异常处理应该用于处理异常情况,而不是正常的控制流程。例如:


def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
result = float('inf')
return result

在这个示例中,我们使用异常处理来处理除以零的情况。正确做法:


def divide(a, b):
if b == 0:
result = float('inf')
else:
result = a / b
return result

在这个示例中,我们使用条件判断来处理除以零的情况,而不是使用异常处理。


3.3 捕获过于宽泛的异常


捕获过于宽泛的异常可能导致程序崩溃或隐藏潜在的问题。以下是一个案例:


try {
// 执行一些可能抛出异常的代码
} catch (Exception e) {
// 捕获所有异常,并忽略错误}

在这个例子中,异常被捕获后,没有进行任何处理或记录,导致程序无法正确处理异常情况。正确的做法是根据具体情况,选择合适的异常处理方式,例如:


try {
// 执行一些可能抛出异常的代码
} catch (FileNotFoundException e) {
// 处理文件未找到异常
logger.error("File not found", e);
} catch (IOException e) {
// 处理IO异常
logger.error("IO error", e);
} catch (Exception e) {
// 处理其他异常
logger.error("Unexpected error", e);}

通过合理的异常处理,我们可以更好地处理异常情况,增加程序的稳定性和可靠性。


04、错误处理和异常处理



  • 错误的习惯


缺乏复用性:代码冗余,维护困难,增加 bug 出现的可能性。 缺乏模块化:代码耦合度高,难以重构和测试。

4.1 缺乏复用性


代码重复是一种非常常见的错误。当我们需要实现某个功能时,可能会复制粘贴之前的代码来实现,这样可能会导致代码重复,增加代码维护的难度。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume_of_cuboid(length, width, height):
return length * width * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

上述代码中,计算逻辑存在重复,这样的代码重复会影响代码的可维护性。为了避免代码重复,我们可以将相同的代码复用,封装成一个函数或者方法。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume(length, width, height):
return calculate_area_of_rectangle(length, width) * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

这样,我们就可以避免代码重复,提高代码的可维护性。


4.2 缺乏模块化


缺乏模块化是一种常见的错误,这样容易造成冗余,降低代码的可维护性,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑
```

此例中,User 和 Order 类都包含了保存和发送邮件的逻辑,导致代码重复,耦合度高。我们可以通过将发送邮件的逻辑提取为一个独立的类,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

class EmailSender:
def send_email(self, content):
# 发送邮件的逻辑

通过把发送邮件单独提取出来,实现了模块化。现在 User 和 Order 类只负责自己的核心功能,而发送邮件的逻辑由 EmailSender 类负责。这样一来,代码更加清晰,耦合度降低,易于重构和测试。


05、硬编码



  • 错误的习惯


常量:设置固定常量,导致维护困难。 全局变量:过度使用全局变量,导致程序的状态难以跟踪。

5.1 常量


在编程中,我们经常需要使用一些常量,如数字、字符串等。然而,直接在代码中硬编码这些常量是一个不好的习惯,因为它们可能会在未来发生变化,导致维护困难。例如:


def calculate_score(score):
if (score > 60) {
// do something}

这里的60就是一个硬编码的常量,导致后续维护困难,正确的做法应该使用常量或者枚举来表示。例如:


PASS_SCORE = 60;
def calculate_score(score):
if (score > PASS_SCORE) {
// do something }

这样,我们就可以避免硬编码,提高代码的可维护性。


5.2 全局变量


过度使用全局变量在全局范围内都可以访问和修改。因此,过度使用全局变量可能会导致程序的状态难以跟踪,增加了程序出错的可能性。例如:


counter = 0
def increment():
global counter
counter +
= 1

这段代码的问题在于,它使用了全局变量 counter,使得程序的状态难以跟踪。我们应该尽量减少全局变量的使用,而是使用函数参数和返回值来传递数据。例如:


def increment(counter):
return counter + 1

这段代码没有使用全局变量,而是使用函数参数和返回值来传递数据,使得程序的状态更易于跟踪。


06、测试和调试



  • 错误的习惯


单元测试:不进行单元测试会导致无法及时发现和修复代码中的错误,增加代码的不稳定性和可维护性。 边界测试:不进行边界测试可能导致代码在边界情况下出现错误或异常。 代码的可测试性:有些情况依赖于当前条件,使测试变得很难。

6.1 单元测试


单元测试是验证代码中最小可测试单元的方法,下面是不添加单元测试的案例:


def add_number(a, b):
return a + b

在这个示例中,我们没有进行单元测试来验证函数 add_number 的正确性。正确示例:


import unittest

def add_number(a, b):
return a + b

class TestAdd(unittest.TestCase):
def add_number(self):
self.assertEqual(add(2, 3), 5)

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行单元测试,确保函数 add 的正确性。


6.2 边界测试


边界测试是针对输入的边界条件进行测试,以验证代码在边界情况下的行为下面是错误示例:


def is_even(n):
return n % 2 == 0

在这个示例中,我们没有进行边界测试来验证函数 is_even 在边界情况下的行为。正确示例:


import unittest

def is_even(n):
return n % 2 == 0

class TestIsEven(unittest.TestCase):
def test_even(self):
self.assertTrue(is_even(2))
self.assertFalse(is_even(3))

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行边界测试,验证函数 is_even 在边界情况下的行为。


6.3 可测试性


代码的可测试性我们需要编写测试来验证代码的正确性。如果我们忽视了代码的可测试性,那么编写测试将会变得困难,甚至无法编写测试。例如:


def get_current_time():
return datetime.datetime.now()

这段代码的问题在于,它依赖于当前的时间,这使得我们无法编写确定性的测试。我们应该尽量减少代码的依赖,使得代码更易于测试。例如:


def get_time(now):
return now

这段代码不再依赖于当前的时间,而是通过参数传入时间,这使得我们可以编写确定性的测试。


07、性能优化



  • 错误的习惯


过度优化:过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。 合适的数据结构:选择合适的数据结构可以提高代码的性能。

7.1 过度优化


我们往往会试图优化代码,使其运行得更快。然而,过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。例如:


def sum(numbers):
return functools.reduce(operator.add, numbers)

这段代码的问题在于,它使用了 functools.reduce 和 operator.add 来计算列表的和,虽然这样做可以提高一点点性能,但是这使得代码难以理解。我们应该在保持代码清晰和易读的前提下,进行适度的优化。例如:


def sum(numbers):
return sum(numbers)

这段代码使用了内置的 sum 函数来计算列表的和,虽然它可能比上面的代码慢一点,但是它更清晰、易读。


7.2 没有使用合适的数据结构


选择合适的数据结构可以提高代码的性能。使用不合适的数据结构可能导致代码执行缓慢或占用过多的内存。例如:


def find_duplicate(numbers):
duplicates = []
for i in range(len(numbers)):
if numbers[i] in numbers[i+1:]:
duplicates.append(numbers[i])
return duplicates

在这个示例中,我们使用了列表来查找重复元素,但这种方法的时间复杂度较高。我们可以使用集合来查找元素。例如:


def find_duplicate(numbers):
duplicates = set()
seen = set()
for num in numbers:
if num in seen:
duplicates.add(num)
else:
seen.add(num)
return list(duplicates)

我们使用了集合来查找重复元素,这种方法的时间复杂度较低。


08、代码安全性



  • 错误的习惯


输入验证:不正确的输入验证可能导致安全漏洞,如 SQL 注入、跨站脚本攻击等。 密码存储:不正确的密码存储可能导致用户密码泄露。 权限控制:不正确的权限控制可能导致未经授权的用户访问敏感信息或执行特权操作。

8.1 输入验证


没有对用户输入进行充分验证和过滤可能导致恶意用户执行恶意代码或获取敏感信息。例如:


import sqlite3
def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们没有对用户输入的 username 参数进行验证和过滤,可能导致 SQL 注入攻击。正确示例:


import sqlite3

def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们使用参数化查询来过滤用户输入,避免了 SQL 注入攻击。


8.2 不正确的密码存储


将明文密码存储在数据库或文件中,或使用不安全的哈希算法存储密码都是不安全的做法。错误示例:


import hashlib

def store_password(password):
hashed_password = hashlib.md5(password.encode()).hexdigest()
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了不安全的哈希算法 MD5 来存储密码。正确示例:


import hashlib
import bcrypt

def store_password(password):
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了更安全的哈希算法 bcrypt 来存储密码。


8.3 不正确的权限控制


没有正确验证用户的身份和权限可能导致安全漏洞。错误示例:


def delete_user(user_id):
if current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们只检查了当前用户是否为管理员,但没有进行足够的身份验证和权限验证。正确示例:


def delete_user(user_id):
if current_user.is_authenticated and current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们不仅检查了当前用户是否为管理员,还检查了当前用户是否已经通过身份验证。


09、版本控制和协作



  • 错误的习惯


版本提交信息:不合理的版本提交信息会造成开发人员难以理解和追踪代码的变化。 忽略版本控制和备份:没有备份代码和版本控制的文件可能导致丢失代码、难以追溯错误来源和无法回滚等问题。

9.1 版本提交信息


不合理的版本提交信息可能导致代码丢失、开发人员难以理解等问题。错误示例:


git commit -m "Fixed a bug"

在这个例子中,提交信息没有提供足够的上下文和详细信息,导致其他开发人员难以理解和追踪代码的变化。正确的做法是提供有意义的提交信息,例如:


$ git commit -m "Fixed a bug in calculate function, which caused grade calculation for scores below 60"

通过提供有意义的提交信息,我们可以更好地追踪代码的变化,帮助其他开发人员理解和维护代码。


9.2 忽略版本控制和备份


忽略使用版本控制工具进行代码管理和备份是一个常见的错误。错误示例:


$ mv important_code.py important_code_backup.py
$ rm important_code.py

在这个示例中,开发者没有使用版本控制工具,只是简单地对文件进行重命名和删除,没有进行适当的备份和记录。正确示例:


$ git clone project.git
$ cp important_code.py important_code_backup.py
$ git add .
$ git commit -m "Created backup of important code"
$ git push origin master
$ rm important_code.py

在这个示例中,开发者使用了版本控制工具进行代码管理,并在删除之前创建了备份,确保了代码的安全性和可追溯性。


10、总结


好的代码应该如同一首好文,让人爱不释手。优雅的代码,不仅是功能完善,更要做好每一个细节。


最后,引用韩磊老师在《代码整洁之道》写到的一句话送给大家:



细节之中自有天地,整洁成就卓越代码。


以上是本文全部内容,欢迎分享。




原创作者|孔垂航


作者:腾讯云开发者
来源:juejin.cn/post/7257894053902565433
收起阅读 »

一次操蛋的面试经历

故事发生在10年前,因为自己的不成熟,没想好就跟老板提了离职,不得不真的开始找工作(详情见之前的文章,末尾有链接)。很快,就拿到了下家的 offer,约定3月31日入职。 猎头又推荐了「小而美」的豌豆荚,我不想去,因为他们周六也上班。猎头说周六基本是打酱油,而...
继续阅读 »

故事发生在10年前,因为自己的不成熟,没想好就跟老板提了离职,不得不真的开始找工作(详情见之前的文章,末尾有链接)。很快,就拿到了下家的 offer,约定3月31日入职。


猎头又推荐了「小而美」的豌豆荚,我不想去,因为他们周六也上班。猎头说周六基本是打酱油,而且工作氛围号称 Google 范,文艺风,不妨聊聊。吼啊,那就聊聊。


为了叙事方便,先放个当年的日历:


日历


3月15日,周六,下午,连续面了3轮后, CEO 王俊煜(下称 junyu)不在,HR 让我回去等消息,路途遥远,到家已经天黑了。


周一,猎头告诉我挂了,不知道原因,建议我找 HR 争取下,看能否跟 junyu 聊聊,也许会有转机。我拒绝了,强扭的瓜不甜,而且我也不喜欢周六上班。


周四,收到一位面试官的邮件,他觉得我还不错,想约我再聊聊,全文如下:


邮件


我那时工作刚满20个月,其中2个月,因为部门快要黄了,整天无所事事的。最后6个月,搞 iOS 去了。所以,真正做 Android 的时间也就1年,他说的没错,我确实掌握的不够深入。虽然邮件里直接指出了我的不足,但我觉得更多的还是肯定吧。


说句不要脸的,我喜欢这种被人欣赏的感觉。类似的事,在我身上发生过挺多次了,只可惜因为自己的原因,没能接住那些泼天的富贵,先挖个坑,未来有时间再填。


最后约的是3月21日,周五,又去面了两轮,junyu 还是不在,继续回去等消息。周六,发邮件给之前的面试官咨询结果,得知面试通过了,需要等 HR 约 junyu 的时间再聊聊。


距离我入职下家公司只剩一周了,豌豆荚 HR 迟迟没有动静。我多次发短信催促她,得到的答复都是还在约。印象中最后约的是29号,周六,我从狼厂离职的第2天。


当天下午,我到的时候,junyu 还在面试中,等了半个多小时,他才完事。简短的自我介绍和项目介绍后,我说自己开发了一款计算器 APP,获得了不错的用户反馈。他一脸鄙夷的眼神问我:



什么?计算器?



对,我正准备展示它的功能和获得的奖项,他打断了我,改问其他问题了。具体问题不记得了,只记得他对我回答的反馈基本都是「嗯」,外加十分不屑的表情,而且大多数时间是在看电脑。5分钟后,他说今天就到这,有消息会通知我。


很快,HR 给我发了个拒信:



经过综合评估,还是觉得不合适



收到,我谢谢你。


工作快12年了,我面过很多公司,成功了多次,也失败了多次。但没有哪次像这次令人生气,如此不尊重候选人,这是唯一的一次。我不知道他的居高临下是为何,我的出现浪费了他的宝贵时间?哪怕是面对一问三不知的面试者,也不应该如此一副高傲的态度。


也许是因为年纪轻轻,创业有所成,于是飘飘然,开始藐视众生了?又或者因为狼厂以19亿美元收购「91手机助手」,觉得自己也行?


百度收购91


当初在狼厂时,周围的同事就没有一个人觉得「91无线」能值这么多钱,妥妥的冤大头。实话说,狼厂还不如把那个「91」收购了,给「狼友」们谋福利,还能赋能、反哺后来的视频业务,懂的都懂。。。


除了那场面试,HR 的表现也是让人无语,在我反复提醒她我就要入职下家了,她依然无动于衷,迟迟未能约好时间,而且每次都是我主动联系她的。哪怕是对待备胎,女神也会偶尔主动一下吧?难道是因为我太主动,把我当舔狗了?


几个月后,豌豆荚竟然断缴了很多员工的社保,据说是因为缴费的卡上余额不足,导致扣款失败,庆幸当初他们没看上我,这种奇葩的事也能出现。


豌豆荚断缴社保


对于给我发邮件的那位工程师,我还是很感激的,感谢他又给了我一次机会,也感谢他让我知道了物种的多样性。巧合的是,多年以后,我在面试另一家大厂时,又碰到了他,他已经是一名中层管理人员了,手下估计有几百号人。不过,因为跟 HR 待遇没谈拢,我最后没去。


本文纯属吐槽,以上内容,绝对真实,如有雷同,深表同情。


在我告诉猎头挂了后,他告诉我还有一家跟豌豆荚风格类似的「小而美」的公司,建议我去聊聊看。我说马上要入职新公司了,不想再面了,况且我都没听过这个公司。彼时,那个小公司名叫「今日头条」。




作者:野生的码农
来源:juejin.cn/post/7361650229739716627
收起阅读 »

和一个做直播的朋友聊了聊

昨天,昨天和滨江的一个朋友聊了聊,他是那边的一个公司产品负责人,也算是核心合伙人的角色之一,他们的公司是做直播业务的,大概有七八十人的团队,开发人员大概是30人左右,占比35%左右,其中里面还有一个CTO角色,或者说技术总监的角色,其他的全部都是干活的小兵和小...
继续阅读 »

昨天,昨天和滨江的一个朋友聊了聊,他是那边的一个公司产品负责人,也算是核心合伙人的角色之一,他们的公司是做直播业务的,大概有七八十人的团队,开发人员大概是30人左右,占比35%左右,其中里面还有一个CTO角色,或者说技术总监的角色,其他的全部都是干活的小兵和小组长之类的。


我们主要聊到了两个不同规模的公司的工作模式的问题,因为我所在的是阿里巴巴应该是非常典型的超大型互联网公司,而他们公司这个人数刚好是属于小型的互联网公司。


他的公司主要是做直播业务的,大家都很熟悉诸如抖音快手这样的直播平台,这么小的公司怎么能做好一个直播平台呢?那他们的业务模式也非常的经典,那就是做一些非常小众的网红和用户产品。



一、直播市场的长尾用户


他描述了一下他自己的一些对于直播和用户的一些观点和理解,比如说现在众所周知的类似于抖音这样极大的平台,有超级大的网红IP,也有无数的粉丝。但是国内互联网用户基数非常之大,存在非常多的长尾用户,比如一些粉丝想在平台上获得一些娱乐感和交互感,这个是抖音这种大平台所满足不了的。另外一方面有大量的尾部网红在抖音这种大平台上面往往也拿不到任何的流量,所以他们也需要一种更小的平台,有充足的流量扶持。


在这个背景下就有了针对这些长尾用户的一些小的直播平台,那在小的直播平台上,哪怕你再小的网红,你都会有一些流量上面的倾斜,对于用户来说,在抖音上给大V打赏几万可能主播都不会理你,但是你在小平台上直接给主播进行打赏交互,就会变得更加的简单和高效。毕竟我们可以想象一下,很多花不起大价钱的“屌丝”用户,可能在这种小平台上面砸个几百几千,可能就能够约网红出来吃个饭,聊个天什么的。一些尾部网红也是一样,长期在抖音中大平台上面基本上没有流量,也没人关注和在意,但是到小平台上面可能就有比较多的几十个,甚至几百个粉丝过来和你交互和聊天打赏,很容易形成一个正反馈。


所以对于刚刚起步的网红来说,在这种小平台上面去发展,获得自己的正反馈和积累初步的影响力是非常的必要的。那对于一些没有太多钱、时间又空闲的粉丝们来说,对于小平台上面也能够有一个快速的通道去接触到这些主播或者兴趣相同的朋友。


于此同时,各行各业,蚊子肉都是大平台吃不到也不想吃的,这类长尾用户是大的平台是往往无法覆盖的,也是看不上的,所以给了这些小型的平台很多的发展空间,这个就是非常典型的一种长尾生态形式。也非常符合之前我所推荐的那本书叫做《长尾理论》,这种小平台因为它的边际成本是非常的低的,所以它可以在各个地方花钱去投放广告,吸引长尾客流,主打各种形式的娱乐化的直播并从中抽佣。


我们也可以看到这种平台本身也不大可能做的非常大,一方面它可能在形式和内容上面都可能走一些擦边或者灰色的方式,另外一方面对他们自己来说,他们也不想做的做大,做大以后以后就会面临着更加复杂的问题,比如监管问题。所以很多这种小型的平台活的非常的滋润,从来没想着做大做强,而是在自己的一亩三分细分领域里深耕,现金流反而还比大平台的还更加的充足。


他们公司在前两年就寻求上市,因为经济的原因中止,但这也就说明他这种模式实际上非常赚钱,现金流是非常的稳定的。


二、快进快出的用人理念


除了这种非常好的商业模式之外,另外一个讨论点就是我们工作模式上面的最大的区别。他提到了他们公司的员工的离职率是非常高的,基本上几个月、半年可能就大量的技术人员离职汰换。这个也很简单,她说对于新招聘的员工来说,如果半个月上不了手的情况下的话,就会在试用期里面就会解聘掉,主打就是追求实用主义,员工拿来即用没有培养一说。对于一个小的技术公司来说,它的成本控制的非常的严格,如果员工短时间内不能上手的情况下的话,对他们来说是没有任何价值的,所以对于员工都采用快进快出这样的方式,完全不像我们大平台大企业,可能给到一个员工的成长时间,短则三个月,大长则半年,一年。而小公司完全吃不消这种巨大的人力培养成本。


另外就是对于他们一些比较资深的工程师来说,工龄时间也不会太长,因为他们给不了员工的一个向上的晋升通道。当个员工工作了两年到三年,技术能力各方面能力都提高了,以后也没办法往上升或者持续加薪,因为毕竟上面只有一个技术合伙人,总不能把这个技术合伙人给顶下去吧,所以他们大部分的员工工作了两年到三年之后,技术能力上面都有非常大的成长之后,往往就会跳出这个小厂去寻求其他的大厂机会。


然后他们公司本身对于技术的追求也不深,大部分完全采用的是“拿来即用”的原则,他说在早期的时候做平台还会去找一些开源源码自己来部署,到了现在大部分能力都有非常成熟的第三方厂家来支持,他们公司技术人员只要做集成和包装就可以了。现在据我所知,类似于阿里云这样的云平台,已经把整个云计算API、网络直播的API,甚至很多底层技术全部做的非常好,都打包成SDK或者封装成API,所以上层业务方只要购买服务后把API包装一下,封装就可以直接使用了,五分钟生成一个直播平台APP已经没有任何问题了。



以我的理解,一个正常的工作了半年到一年的同学,我觉得在这种SDK或者API的加成下,就应该在一个星期内能创建出来一个直播平台APP了。所以很明显在这种基础能力非常强大的情况下,他们公司就会可以把成本压的更小,他们可以随时的去调整自己的业务方向和迭代,基本上几周就会有一个小版本迭代或者出全新的APP。


我问了一下,他们有没有一个知名的应用市场APP,给我的答案是他们开发成了很多非常小的一些APP,然后在应用市场上面去打广告引流,用户量和粉丝量都不算大,明显就能看到这种模式主打一个灵活、主打分布式。


三、反脆弱的商业形式


所以相对于小厂和中厂来说,不管从业务模式上还是从技术架构上,还是从经营理念上完全不可同日而语。但不得不说,我觉得正如我们的自然界生态系统一样,有些时候很微小的生物往往能够在漫长的生态环境中存活下来,比如蟑螂老鼠,而有一些庞然大物,诸如恐龙猛犸象这样的大体积的生物,反而还容忍不了生态气候的变化而灭绝。


而对于他这样小的一些经济体,几十个人,有自己的一些核心的产品模式,并且能够快速的迭代,对成本控制严格,对经济变化敏感,反而还能够存活到各个不同的周期里面,所以这我觉得也是一种值得我们羡慕的地方。这也是知名作家塔勒布在他的《反脆弱》一书里提到的一种形式,这种公司反而具备更强的反脆弱性,当经济越差,他们不仅不受影响,反而反弹变得更强壮、盈利性更强。


最后一步来说,对于程序员来说,根据自己的兴趣、爱好、能力水平,在当前的经济周期找到一个比较合适自己的平台,能够锻炼到自己的能力,不管是从技术还是从业务经营,产品各个方面都有所成长,那对自己来说就是好事。对于创业者来说也未必要盯着非常大的市场,动不动就来个规模效应,有时候去做这种非常小微公司和长尾市场,往往活得会更加的滋润和惬意。


作者:ali老蒋
来源:juejin.cn/post/7290898686582669351
收起阅读 »

互联网大厂,开始对领导层动刀了

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。 其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。 有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。” 有次我跟...
继续阅读 »

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。


其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。


有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。”


有次我跟老Z吃饭,他苦笑着跟我说:“妈的,如果不晋升,没准还能待下去呢,晋升之后反而目标变大了。”


我问他:“那你最近看新机会的结果怎么样,有没有拿到比较满意的offer呢?”


他说:“面试机会倒是不少,大厂已经面了五六个,但最后都无疾而终了。”


接下来,他又把话题聊了回来,说:“你说,如果公司对我不满意,为什么还给我晋升呢,但如果公司对我满意,又为什么还要裁我呢?”


我给他举了一个这样的例子:“就算大款给小三买奢侈品,让她住豪宅,但并不代表不会甩了她啊,对吧。”


他听了哈哈大笑,似乎释怀了。


接下来,我盘点一下,具备什么特征的管理层最容易被“降本增效”,以及在未来的日子里,我们应该如何应对这种不确定性。


“降本增效”画像


跟大家聊下,哪类用户画像的领导层最容易被“降本增效”,请大家对号入座,别心存侥幸。


(1)非嫡系


不管到哪天,大厂也都是个江湖,是江湖就有人情世故。


如果你不是老板的嫡系,那公司裁员指标下来了,你不背锅谁背锅,你不下地狱谁下地狱。


你可能会说:“我的能力比老板的嫡系强啊,公司这种操作,不成了劣币驱逐良币了吗?”


其实,这个时候对于公司来说,无论是劣币还是良币,都不如人民币来得实在。


人员冗余对于公司来讲就是负担,这个时候谁还跟你讲任人唯亲还是任人唯贤啊。


(2)老员工


可能有人会这么认为,老员工不但忠诚,而且N+1赔的钱也多,为什么会优先裁掉老员工呢。


我认为,一个员工年复一年、日复一日地待在熟悉的工作环境,就犹如温水煮青蛙一样,很容易停留在舒适区,有的甚至混成了老油子。


而老板最希望看到的是,人才要像水一样流动起来,企业要像大自然一样吐故纳新,这样才会一直保持朝气和活力。


总之,老板并不认为员工和公司一起慢慢变老,是一件最浪漫的事。


(3)高职级


对于公司来讲,职级越高的员工,薪资成本也就越高,如果能够创造价值,那自不必多说,否则的话,呵呵呵。。。


现在越来越多的公司,在制定裁员目标的时候,已经不是要裁掉百分之多少的人了,而是裁员后把人均薪资降到多少。


嗯,这就是传说中的“降均薪”,目标用户是谁,不多说也知道了吧?


(4)高龄


35+,40+,嗯,你懂的。


老夫少妻难和谐,大龄下属跟小领导不和谐的几率也很大,一个觉得年轻人不要抬气盛,另外一个觉得不气盛就不是年轻人。


不确定性——在职


恭喜你,幸存者,老天确实待你不薄,在应对不确定性这件事情上,给了你一段时间来缓冲。


如果你已经35+了,那接下来你需要把在职的每一天,都当成是最后一天来度过,然后疯狂地给自己找后路,找副业。


一定要给你自己压力,给自己紧迫感。


因为说不定哪天,曾经对你笑圃如花的HR,会忽然把你叫到一个偏僻的会议室里,面无表情地递给你一式两份的离职协议书,让你签字。


在你心乱如麻地拿起签字笔之际,她没准还得最后PUA你几句:“这次公司不是裁员,而是优化。你要反思自己过去的贡献,认识到自己的不足,这样才能持续发展。


当然,你有大厂员工的光环加持,到市场上还是非常抢手的,你要以人才输出的高度来看这次优化,为社会做贡献。”


至于找后路和副业的方式,现在网上有很多类似的星球,付费和免费的都有,加一个进去,先好好看看,主要是先把思路和视野打开。


当然,如果你周围要是有一个副业做得比较好的同事,并且他愿意言传身教你,那就更好了。


然后,找一个自己适合的方向和领域,动手去做,一定动手去做,先迈出第一步,可以给自己定一个小目标,在未来几个月内,从副业中赚到第一次钱。


从0到1最难,再接下来,应该就顺了。


不确定性——不在职


如果35+的你刚刚下来,而且手头还算殷实的话,我先劝你第一件事:放弃重返职场。


原因很简单,如果一个方向,随着你经验的积累和年龄的增长,不仅不会带来复利,而是路会越走越窄,那你坚持的意义是什么?难道仅仅是凑合活着吗?


第二件事,慢下来,别立马急急忙忙地找出路,更不要一下子拿出很多本金砸在一个项目上。据说,有的项目是专门盯着大厂员工的遣散费来割韭菜的。


有人会说,在职的人你劝要有紧迫感,离职的人你又劝慢下来,这不是“劝风尘从良,逼良家为娼”吗?


其实不是的,只是无论是在职还是离职,我们都需要在某件事情的推进上,保持一个适合且持久的节奏,不要止步不前,也不要急于求成,用力过猛。


第三件事,就是舍得把面子喂狗,不要觉得做这个不体面,做那个有辱斯文,只要在合理合法的情况下,能赚到钱才是最光荣的。


接下来,盘点周围可用资源,调研有哪些领域和方向适合你,并愿意投入下半生的精力all in去做。


这个过程可能会很痛苦,尤其对于一些悲观者来说,一上来会有一种“世界那么大,竟然再也找不到一个我能谋生的手段”的感觉,咬牙挺过去就好了。


这里说一句,人只要自己不主动崩,还是远比想象中耐操很多的。


结语


好像也没什么好说的,大家各自安好,且行且珍惜吧。


作者:托尼学长
来源:juejin.cn/post/7317859658285318170
收起阅读 »

记录我的程序猿副业首笔创收

在这个充满机遇的数字时代,我,一个普通的程序猿,编程爱好者,终于在云端源想这个平台上收获了属于我的第一桶金。这是一个关于兼职、学习与成长的故事,希望能激发同在编程路上的你,勇敢迈出那一步。先晒晒我的首笔收入:一个普通的周末,我像往常一样,泡上一杯咖啡,坐在电脑...
继续阅读 »

在这个充满机遇的数字时代,我,一个普通的程序猿,编程爱好者,终于在云端源想这个平台上收获了属于我的第一桶金。这是一个关于兼职学习与成长的故事,希望能激发同在编程路上的你,勇敢迈出那一步。

先晒晒我的首笔收入:


一个普通的周末,我像往常一样,泡上一杯咖啡,坐在电脑前,漫无目的地浏览着技术论坛偶然间看见“赢取丰厚收益”的推送,好奇心驱使我点击进去,发现这是一个内容征集的平台,里面都是一些开发实战项目内容征集,让像我这样渴望更多实战经验的程序猿,有机会接取真实项目,获得报酬的同时,也能锻炼自己的技能。

去看看云端源想内容征集

起初,我心中充满了疑虑:“我能行吗?”但转念一想,不试试怎么知道呢?于是,我开始仔细浏览平台上的需求列表,寻找与自己技能相匹配的任务。里面的项目还是挺多的,有简单的,也有比较复杂的,都可以根据自己的水平进行选择。经过一番筛选,我锁定了一个视频播放网站需求。


于是,我点击需求详情中的立即咨询,通过与在线客服的沟通,了解了需求的细节,确认我可以完成,才接下了这个需求,整个过程中,所有的疑问都可以在云端源想平台上很顺畅的进行沟通。而且平台里面的交付标准也写的很详细了已经。接下来,便是紧锣密鼓的开发阶段。我利用业余时间,一点点开始搭建网站,调试等等。每当遇到难题,我都会第一时间在云端源想的社区寻求帮助,或者也可以去问他们的在线老师和客服人员,那里总有人热心解答,过程中有问题也可以快速解决

反复沟通后,大纲的确认输出


前端项目创建后,


对接阿里点播服务的几个接口的编写


经过几周的努力,项目终于完成,当我的产出成果被满意验收,那一刻的成就感难以言喻。不久后,我收到了云端源想转来的报酬,虽然金额不大,但这却是我程序员兼职的第一桶金,意义非凡。它不仅证明了我的努力没有白费,更点燃了我继续深、挑战复杂项目的决心。


从此,我成了云端源想的常客,不仅技术日益精进,还结识了一群志同道合的朋友。现在,我感激那个勇于迈出第一步的自己,以及提供机会的云端源想平台

这就是我偶然的一个副业机会。副业虽然在时间上给我带来了较大的压力,但却给我带来了更多的收入,重构了我的收入结构,帮助我走出了“晋升无望,收入见顶,而开支直线上升”这种困境,让我有了更强的自我效能感和财务自信。

因为我感受到了发展斜杠事业的好处,所以,特地总结出来,分享给大家。

  • 做副业有非常多的好处:
  • 多赚点钱,提升生活品质;
  • 改善收入结构,应对收入见顶焦虑,增加财务自信;
  • 养多元化自我价值;
  • 探索更多可能性;
  • 打造备胎,应对裁员等黑天鹅事件;
  • 掌控生活。

如果你也想利于自己的技能赚钱,正寻找实战机会,不妨来云端源想看看,我觉得对我们程序猿还是很友好的,一方面可以赚到一部分兼职的钱,还能边学习边提升,也累积了自己的工作经验,真是一举三得。墙裂建议有时间想尝试,想挑战的程序猿朋友们可以去看看有没有适合自己的兼职项目,加入渠道给大家奉上。

去云端源想看看内容征集

收起阅读 »

这个网站真的太香了!居然可以免费使用AI聊天工具和“智能AI聊天助手”项目源码!!!

宝子们,在这个AI爆火的时代,你是否还在因为无法使用ChatGpt而头疼?是否还在寻觅一款国内的好用AI工具呢?好消息!小编花费三个月终于找到了一个可以免费使用AI聊天工具的网站,由于这个网站之前一直在内测阶段,所以就没有给大家分享。刚好,近期这个网站正式上线...
继续阅读 »

宝子们,在这个AI爆火的时代,你是否还在因为无法使用ChatGpt而头疼?是否还在寻觅一款国内的好用AI工具呢?


好消息!小编花费三个月终于找到了一个可以免费使用AI聊天工具的网站,由于这个网站之前一直在内测阶段,所以就没有给大家分享。




刚好,近期这个网站正式上线了。小编今天就来好好跟大家聊聊这个网站有哪些便宜好用的功能,之所以推荐这个网站也是因为它不光好用,还有大量免费的功能,像平时写代码遇到想不起来的,直接去这个网站用AI搜索一下,简直不要太香!


对了!这个网站的名称叫“云端源想”!大家记一下,可以直接百度搜索去体验哦!


下面就正式给大家介绍这个网站,以及我推荐大家用它的原因:


首先我先说一下,它近期不是刚上线嘛,有个巨大的福利在等着大家,就是除了前面我提到的免费使用AI聊天工具之外,还可以领取搭建这个AI聊天工具的源码!!简直了!


这对于想要找项目实战练手的编程新手宝子们,简直是“饥时饭,渴时浆”的事情,所以看到了,不要犹豫,直接点进去领到手再说!反正不要钱!


AI聊天:AI聊天工具

项目源码:“智能AI聊天助手”项目源码


这个是网站的活动海报图,也给大家放在这里啦!




说完能领取的福利之后,我再来给大家说说云端源想这个网站值得逛的几个版块,帮助大家快速找到自己想要的功能。


1、微实战




这个板块在我看来是很实用的,它里面的项目感觉都是从实际应用的功能点拆分出来的项目实战,非常地有针对性。


比如我需要开发一个线上商城,就可以把这里面的网站支付的源码拿来用,不仅能快速对接,还为我省下了很多时间,然后我就可以早早下班,不用秃头啦!简直是提升效率的好帮手!


我发现目前站里这些微实战只需要两位数就可以拿到,有时候还有限时免费的:完整的项目源码项目部署教程视频教程,甚至还有配套的免费直播课,可以说是非常有性价比了,上面给大家说免费领取的AI聊天助手就是这个板块的内容。




总之,这个微实战板块是一个非常实用的资源,无论你是新手还是有经验的开发者,都可以从中受益。通过参与这些项目实战,你可以提升自己的实际开发经验,学习到更多的技术和工具,同时也可以提高工作效率,更好地应对实际开发中的挑战。


所以!好东西要和大家一起分享,我分享给大家了,大家也可以分享给身边的朋友们哦!


2、智能AI工具




这里面目前我看到了三个AI工具,图片清晰度增强、文字合成语音和智能AI问答,鉴于都是免费的所以我都体验了一下,对我来说最实用的就是这个免费的AI问答了。



平时写东西找不到灵感,或者遇到不懂的东西,我都会在这问问AI,使用频次快超过百度了,用它辅助写代码是真的很牛,我也试过好多其他的AI产品,免费的里面对比下来这个真的好用!强烈推荐!!!


3、社区动态




这就是一个可以发布动态的板块,很适合上班摸鱼,哈哈哈!


如果上班或者学习累了,可以来逛逛看看别人发的帖子,寻觅一个有趣的灵魂,喜欢分享的朋友也可以自己发帖,我是没事了就来刷刷,看看有没有什么新鲜事可以在线吃瓜!!


4、编程体系课



里面开通了四门当下比较热门的课,这个就没什么说的,大家在别的学习网站也有,都大差不差。


值得一提的是,云端源想把重难点的知识点提炼出来组成了一个知识库,这样我可以很快速找到我想要学习的点,比较有针对性。




5、在线编程




这个板块也是一个比较少见功能板块了,可以在线编辑运行代码,比较有意思的是可以邀请别人一起协作编程,这个我用的比较少,感兴趣的朋友可以自行探索探索哈!


另外还有一个论坛板块,里面有各种质量比较高技术文章,有时候我写东西也会在里面参考参考,这就没啥好说的,我就不过多去说这个板块了。


以上就是我给大家推荐云端源想这个网站的原因了,不单单是喊大家一起来薅羊毛领源码!也是真心想给开发的朋友们推荐一个好用的工具网站!那么今天的分享就到这里啦!


最后!强烈建议大家不要错过这个宝贵的实战源码!AI工具用不用咱都不说!能够免费获取的资源才是硬道理!别犹豫了,赶紧点这里领取你的福利吧!

收起阅读 »

环信rest可视化工具(macOS版)

介绍这是个rest可视化工具,虽然简陋得破洞,但是贼特么好用不过需要苹果电脑才可以运行的,如果没有苹果电脑,建议某宝装个黑苹果,并安装xcode,即可运行.能干啥?1、通过该工具可以请求环信rest接口,同时可以获取curl命令参数,帮助开发者直观的理解环信每...
继续阅读 »

介绍

这是个rest可视化工具,虽然简陋得破洞,但是贼特么好用
不过需要苹果电脑才可以运行的,如果没有苹果电脑,建议某宝装个黑苹果,并安装xcode,即可运行.


能干啥?

1、通过该工具可以请求环信rest接口,同时可以获取curl命令参数,帮助开发者直观的理解环信每一个rest接口;

2、通过工具可以快速实现一些简单功能,例如创建群聊,加入群聊,添加好友等;也可以通过工具利用rest接口发送消息,以便快速地测试部分业务逻辑。


下载地址:

https://gitee.com/easemob_1/swiftui_easemob_rest_tool


如何使用

使用起来很简单,参考下图





有任何使用问题可以加微信(备注:rest工具)咨询,欢迎一起维护这个项目。




收起阅读 »

为安卓猿准备的Google I/O 2024省流版

前两天一年一度的谷歌开发者大会Google I/O 2024在大洋彼岸如期举行,在会上谷歌发布了一系列最新的技术。本文将以Android开发为核心来汇总一下大会的内容。Android 15 Beta 2来了自从Android站稳了脚跟以后(大概是在Androi...
继续阅读 »

前两天一年一度的谷歌开发者大会Google I/O 2024在大洋彼岸如期举行,在会上谷歌发布了一系列最新的技术。本文将以Android开发为核心来汇总一下大会的内容。

Android 15 Beta 2来了

自从Android站稳了脚跟以后(大概是在Android 4.3之后)基本上就是每年一个大版本的节奏,一般是在春季有预览版本,在秋季正式发布。为了抢在水果的前面,也都会在Google I/O时进行重点的宣传,所以每年的Google I/O一大看点就是新一代的Android。当然了,从去年开始AI变成了焦点,但是回到前几年时Android是绝对的焦点。

今年也不例外,在Google I/O上面也宣传了一下Android 15,并正式发布了第2个Beta版本,从功能和Feature角度来说,这个就非常接近于正式版本了。不过就如我在前面一篇文章中提到的那样,Android 15其实没啥亮点,主要集中在安全和隐私方面的加强,其余的改进也都非常的小。

关于Android 15具体的改动,可以看一下前排大佬的总结,总结的比较详细,就不重复了。

想体验Android 15 Beta 2的话,如果是谷歌的设备如Pixel系列,应该就有推送了。另外就是现在谷歌都会与厂商联动一起发布新版Android的Beta版本,这已经是好几年的传统了。就比如像小米,在15号大半夜(准确地说是16号凌晨)发布了四款机型的Android 15 Beta OTA包,手头有设备的可以体验一下。

再说一下Android 15(targetSdk 35)的适配,如前所述这一版本较上一代没啥变化,如果本身就已经适配到了Android 14(targetSdk 34),就不用再特殊适配了。

AI霸屏

从去年开始AI就是巨头们的焦点,今年更是霸屏,整个Keynote全是关于AI的,唯一提到Android的地方,也是说在Android手机上如何使用AI。在大模型这条赛道上Google是追随者,就在Google I/O前两天还被Open AI给抢了热度给恶心了一把,劈柴大叔今年略忧伤,讲Keynote的时候有点无精打彩,完全没了前几年那种激情四射。

今年Google发布了Gemini 1.5 Pro,支持1M的上下文Token,大约可以记得1500份PDF,并且演示了很多大模型的具体应用场景,像搜索,图片处理以及文字和代码生成助手。

当然,Android开发者更应该关注的是在端侧部署的大模型。时至今日,大模型已经进入了平稳提升期,大家都是在做出更强大的模型,比如参数更多,上下文更长等等。但大模型仍有一个短板就是无法在端侧的部署,特别是移动设备,如手机,平板,车机,甚至手表等,因受制于性能。目前来说,端侧使用大模型都还是使用网络API的方式,一方面这会依赖于网络,但更重要的是,这会受制于安全和隐私。端侧大部分的数据,是不能直接,也不太可能全都上传到服务器。因此端则部署大模型还是有价值可挖的,比如说对于设备的运行数据,以及像用户一些不愿分享的数据,就可以直接用端侧的大模型来直接处理。

Google发布了端侧的大模型Gemini Nano,将会集成在Android 15之中,并且它支持多模态,还是值得期待的。不过呢,目前Gemini Nano也没有具体的API,谷歌也只给了一个空头支票,在手机上选择文字,然后端侧大模型就可以求解其中的数学题。说实话,这个举例场景的不够好,写作业的场景,作业题怎么可能出现在手机里,然后还是现成的文字?也说明美帝的学生不够卷,在我朝,早就有了作业帮,猿辅导之类的拍一下题目就能给出详细求解过程。

google_io_cts_720.gif

不过Android生态一向受制于厂商,谷歌能做的事情并不多,估计只在谷歌的官方设备(Pixel)中可以用,其他的还是要靠厂商。这点就比不上水果,相信在6月份,水果应该会拿出更为接地气(有实际场景应用和开放API)的端侧大模型集成方案。

Android开发工具

这次谷歌把其大模型Gemini应用到了很多具体的场景中,Android开发官方IDE Android Studio新版本Koala中就深度绑定了Gemini,可以用来生成代码,分析代码和帮助解决其他编程问题。

code_transforms.gif

除了代码,此外Gemini还能帮忙分析错误报告,以及生成集成有Gemini API代码的项目,可见Gemini已经深度融合进了Android Studio之中。详细的可以看一看官文档。看着都挺美好 的,但其实最想知道的问题是,是否会对我们东方大国开放使用?

其他的都是一些常规的小的提升,如可穿待设备的不同模式下的预览,Compose的实时编辑以及Compose Glance(桌面小部件)预览, 以及Android Studio Profiler的改进等等。

Android开发套件

对于Android相关的开发套件,唯一提到的都是与Jetpack Compose相关的,可见谷歌对它的重视。新东西也都中规中矩,主要是在动画上面,如分享页过渡,可复用列表(Lazy list)元素的动画;文本控件支持HTML了;一个新的布局ContextualFlowRow,用以实现复杂的可复用流式布局,这个还是挺有用的;以及性能提升。详细内容可以看官方博客

compose-animation.gif

Jetpack Compose对于常规的UI来说已经没有问题,但是对于一些专业领域的UI还是无法胜任,比如像相机,视频和图像的预览和渲染还是无法在Compose中使用。好消息是,现在Google已经着手处理了,这次就基于CameraX搞了一个camera-viewfinder-compose,能够在Compose中显示相机预览。

再有就是Kotlin Multiplatform,这个是Jetbrains在主要搞的东西,谷歌也加大了配合力度(First class support),比如已经把一些Jetpack中的库添加了对KMM的支持。

参考资料


作者:alexhilton
来源:juejin.cn/post/7369527074590343219
收起阅读 »

产品经理:实现一个微信输入框

web
近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
继续阅读 »


近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


简单分析我们大概需要实现以下几个功能点:



  • 默认单行输入

  • 可多行输入,但有最大行数限制

  • 超过限制行术后内容在内部滚动

  • 支持回车发送内容

  • 支持常见组合键在输入框内换行输入

  • 多行输入时高度自适应 & 页面整体自适应


单行输入


默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


<textarea style="{ height: 36px }" />


多行输入


多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


linechange(event) {
const { height, lineCount } = event.detail
if (lineCount < maxLine) {
this.textareaHeight = height
}
}

这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
const numberOfLines = Math.floor(textHeight / lineHeight);

if (numberOfLines > 1 && this.lineCount === 1) {
const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
this.textareaHeight = lineCount * lineHeight
}

键盘发送内容


正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


const cursorPosition = textarea.selectionStart;
if(
(e.keyCode == 13 && e.ctrlKey) ||
(e.keyCode == 13 && e.metaKey) ||
(e.keyCode == 13 && e.shiftKey) ||
(e.keyCode == 13 && e.altKey)
){
// 换行
this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
}else if(e.keyCode == 13){
// 发送
this.onSend();
e.preventDefault();
}

高度自适应


当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

最后


到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~




作者:南城FE
来源:juejin.cn/post/7267791228872753167
收起阅读 »

Flutter:听说你最近到处和人说我解散了?

早上收到这条消息的时候我是懵圈的,明明几天前才收到下个月 Google I/O 时的 Flutter community 邮件,难道还能这样“出师未捷身先死” ?那 I/O 还开不开? 懵逼之余我又收到了另一条私信,结合起来大概理解了,Google 的裁员...
继续阅读 »

早上收到这条消息的时候我是懵圈的,明明几天前才收到下个月 Google I/O 时的 Flutter community 邮件,难道还能这样“出师未捷身先死” ?那 I/O 还开不开?


图片


图片


懵逼之余我又收到了另一条私信,结合起来大概理解了,Google 的裁员在一定程度上影响到了 Flutter Team ,而传着传着就变成了 「Google 解散 Flutter Team」 。。。。


图片


事实上大概10来天前谷歌就开启了新一轮的裁员计划,当时就提到谷歌正在实施新一轮裁员,试图削减成本并优化整个财务部门的运营,主要是作为谷歌内部重组的一部分,算是 1 月份裁员的延续,不过当时我也没在意,Flutter Team 会受到影响是必然的,毕竟 Flutter Team 规模不算小,只是没想到会变成 「Google 解散 Flutter 」这样的说法。



http://www.business-standard.com/companies/n…



image.png


而这次裁员计划里,Flutter Team 果然又受到波及,其实去年谷歌大裁员里 Flutter Team 也是受到波及,而结果就是 PC 端的推进陷入了一定程度的迟缓,还有无障碍相关的部分,总的来说,2023 年里 Flutter Team 里确实离开了不少元老和大神,但是其实一年下来,Flutter 整体并没有受到太大拖累。



而这次 Flutter Team 的裁员规模和波及范围还暂不明朗,但是人数应该不会太少,所以也不好说影响范围,但是有一点需要提的是, Flutter 是一个开源项目,总的来说他需要 Google 的投入和 Flutter Team 来维护,但是他的推进更主要还是来自社区里的广大开发者,例如国内的 AlexV525、luckysmg 等大佬的加持。


图片


当然,你说现在 Flutter Team 是否是因为人员冗余而裁员,我倒是觉得并不会,因为目前需要解决的问题和推进的 roadmap 其实很多,特别是 Flutter 的全平台特性,甚至近期开始落地的 Wasm Native ,这些都是需要大量时间和人力投入。



图片


所以裁员肯定多多少少会影响 Flutter 的计划,但是那也和「解散」不沾边,就是在大家全力准备 I/O 的时候来 layoffs ,多多少少还是有点不大“人道”的味道。


图片


图片


图片


不管怎么说,从去年开始,不管是国内还是国外,裁员基本都是主流,大家都在为社会贡献人才,只能说大环境如此,只是我是没想道两天不到就会传播成 「Google 要解散 Flutter 团队」,那再过几天会不会还冒出来 「 Flutter 凉凉,鸿蒙接手 Flutter 」的内容?


图片


不管怎么说,这些年 Flutter 算是 Google 比较大投入的项目,开猿节流时受到影响也很正常,如果还不放心,或者你可以看看下个月马上就要开 Google I/O ,来看下 Flutter 会再给你画什么饼:io.google/2024/intl/z…


图片


作者:恋猫de小郭
来源:juejin.cn/post/7362901975421337651
收起阅读 »

前端视角下的鸿蒙开发

web
前言 鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。 这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙...
继续阅读 »

前言



鸿蒙系统,一个从诞生就一直处于舆论风口浪尖上的系统,从最开始的“套壳”安卓的说法,到去年的不再兼容安卓的NEXT版本的技术预览版发布,对于鸿蒙到底是什么,以及鸿蒙的应用开发的讨论从来没停止过。


这次我们就从一个前端开发的角度来了解一下鸿蒙,学习一下鸿蒙应用的开发。



一、 什么是鸿蒙


在开始之前,先问大家一个问题,大家听说过几种鸿蒙?


其实到目前为止,我们经常听到的鸿蒙系统,总共有三种,分别是:


OpenHarmony,HarmonyOS,以及HarmonyOS NEXT。


1. OpenHarmony


OpenHarmony


OpenHarmony(开源鸿蒙系统),由开放原子开源基金会进行管理。开放原子开源基金会由华为、阿里、腾讯、百度、浪潮、招商银行、360等十家互联网企业共同发起组建。包含了“鸿蒙操作系统”的基础能力,是“纯血”鸿蒙的底座。


这个版本的鸿蒙是开源的,代码仓库的地址在这里:gitee.com/openharmony


从我个人的一些粗浅理解来看,OpenHarmony类似于Android里的AOSP,可以装到各种设备上,比如手表、电视甚至是一些嵌入式设备上,详见可见官网的一些例子


2. HarmonyOS


HarmonyOS


基于 OpenHarmony、AOSP等开源项目,同时加入了自己的HMS(因为被美国限制后无法使用GMS)的商用版本,可以兼容安卓,也可以运行部分OpenHarmony开发的鸿蒙原生应用。


这个也是目前经常被吐槽是“套壳”安卓的系统,截止到目前(2024.04)已经更新到了HarmonyOS 4.2。


3. HarmonyOS NEXT


HarmonyOS NEXT


2023年秋季发布的技术预览版,在当前HarmonyOS的基础上去除了AOSP甚至是JVM,不再兼容安卓,只能运行鸿蒙原生应用,同时对OpenHarmony的能里进行了大量的更新,增加和修改了很多API。


这个也就是所谓的“纯血”鸿蒙系统,可惜的是这个目前我们用不到,需要以公司名义找华为合作开权限,或者个人开发者使用一台Mate60 Pro做专门的开发机。并且目前由于有保密协议,网上也没有太多关于最新API的消息。



NEXT版本文档:developer.huawei.com/consumer/cn…



无法直接访问的NEXT版本的开发文档


据说目前HarmonyOS NEXT使用的API版本已经到了API12,目前官网可以访问的最新文档还是API9,所以接下来的内容也都是基于API9的版本来的。


4. 小结


所以一个粗略的视角来看,OpenHarmony、HarmonyOS以及HarmonyOS NEXT这三者之间的关系是这样的:


三者之间的关系


二、 初识鸿蒙开发


在大概知道了什么是鸿蒙之后,我们先来简单看一下鸿蒙开发的套件。下图是官网所描述的一些开发套件,包括了设计、开发、测试、上架所涉及到的技术和产品。


鸿蒙开发套件


我们这篇文章里主要讨论右下角的三个:ArkTSArkUIArkCompiler


ArkTS&ArkUI


ArkCompiler


三、 关于ArkTS的一些疑惑


作为一个前端开发,最常用的编程语言就是JavaScript或者TypeScript,那么在看到鸿蒙应用开发用到的编程语言是ArkTS之后,我脑子里最先蹦出来的就是下面这几个问题:


1. ArkTS语言的运行时是啥?


既然编程语言是TS(TS的拓展,ArkTS),那么它的运行时是什么呢?是V8?JSC?Hermes?还是其他什么呢?


2. ArkTS还是单线程语言吗?


ArkTS还是和JS一样,是单线程语言吗?


3. 基于TS拓展了什么?


TS是JS的超集,对JS进行了拓展,增加了开发时的类型支持。而ArkTS对对TS又进行了拓展,是TS的超集,那它基于TS拓展了什么内容呢?


下面我们一个一个来看。


1. Question1 - ArkTS语言的运行时


先说结论,ArkTS的运行时不是V8,不是JSC、Hermes,不是目前任何一种JS引擎。ArkTS的运行时是一个自研的运行时,叫做方舟语言运行时(简称方舟运行时)。


方舟运行时


而这个运行时,执行的也不是JS/TS/ArkTS代码,而是执行的字节码和机器码
这是因为方舟运行时是ArkCompiler(方舟编译器)的一部分,对于JS/TS/ArkTS的编译在运行前就进行了(和Hermes有点像,下面会讲到)。


方舟开发框架示意图


我们来简单了解一下ArkCompiler,从官网的描述可以看到,ArkCompiler关注的重点主要有三个方面:



  • AOT 编译模式

  • LiteActor 轻量化并发

  • 源码安全


AOT 编译模式


首先是编译模式,我们知道,目前编程语言大多以下几方式运行:



  • 机器码AOT编译


    在程序运行之前进行AST生成和代码编译,编译为机器码,在运行的时候无需编译,直接运行,比如C语言。


  • 中间产物AOT编译


    在程序运行前进行AST生成并进行编译,但不是编译为机器码,而是编译为中间产物,之后在运行时将字节码解释为机器码再执行。比如Hermes或Java编译为字节码,之后运行时由Hermes引擎或JVM解释执行字节码。


  • 完全的解释执行


    在程序运行前不进行任何编译,在运行时动态地根据源码生成AST,再编译为字节码,最后解释执行字节码。比如没有开启JIT的V8引擎执行JS代码时的流程。


  • 混合的JIT编译


    在通过解释执行字节码时(运行时动态生成或者AOT编译生成),对多次执行的热点代码进行进一步的优化编译,生成机器码,后续执行到这部分逻辑时,直接执行优化后的机器码。比如开启JIT的V8引擎运行JS或者支持JIT的JVM运行class文件。




当然,以上仅考虑生产环境下的运行方式,不考虑部分语言在生产和开发阶段不同的运行方式。比如Dart和Swift,一般是开发阶段通过JIT实时编译快速启动,生产环境下为了性能通过AOT编译。



在V8 JIT出现之前,所有的JS虚拟机所采用的都是采用的完全解释执行的方式,在运行时把源码生成AST语法树,之后生成字节码,然后将字节码解释为机器码执行,这是JS执行速度过慢的主要原因之一。


而这么做有以下两个方面的原因:



  • JS是动态语言,变量类型在运行时可能改变

  • JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大


我们一个一个来说。


a. JS变量类型在运行时可能改变

首先我们来看一张图,这张图描述了现在V8引擎的工作流程,目前Chrome和Node里的JS引擎都是这个:


V8现有工作流程


从上面可以看到,V8在拿到JS源码后,会先解析成AST,之后经过Ignition解释器把语法树编译成字节码,然后再解释字节码执行。


于此同时还会收集热点代码,比如代码一共运行了多少次、如何运行的等信息,也就是上面的Feedback的流程。


如果发现一段代码会被重复执行,则监视器会将此段代码标记为热点代码,交给V8的Turbofan编译器对这段字节码进行编译,编译为对应平台(Intel、ARM、MIPS等)的二进制机器码,并执行机器码,也就是图里的Optimize流程。


等后面V8再次执行这段代码,则会跳过解释器,直接运行优化后的机器码,从而提升这段代码的运行效率。


但是我们发现,图里面除了Optimize外,还有一个Deoptimize,反优化,也就是说被优化成机器码的代码逻辑,可能还会被反优化回字节码,这是为什么呢?


其实原因就是上面提到的“JS变量类型在运行时可能改变”,我们来看一个例子:


JS变量类型在运行时可能改变


比如一个add函数,因为JS没有类型信息,底层编译成字节码后伪代码逻辑大概如这张图所示。会判断xy的各种类型,逻辑比较复杂。


在Ignition解释器执行add(1, 2)时,已经知道add函数的两个参数都是整数,那么TurboFan在进一步编译字节码时,就可以假定add函数的参数是整数,这样可以极大地简化生成的汇编代码,不再判断各种类型,伪代码如第三张图里所示。


接下来的add(3, 4)add(5, 6)由于入参也是整数,可以直接执行之前编译的机器码,但是add("7", "8")时,发现并不是整数,这时候就只能将这段逻辑Deoptimize为字节码,然后解释执行字节码。


这就是所谓的Deoptimize,反优化。可以看出,如果我们的JS代码中变量的类型变来变去,是会给V8引擎增加不少麻烦,为了提高性能,我们可以尽量不要去改变变量的类型。


虽然说使用TS可以部分缓解这个问题,但是TS只能约束开发时的类型,运行的时候TS的类型信息是会被丢弃的,也无法约束,V8还是要做上面的一些假定类型的优化,无法一开始就编译为机器码。


TS类型信息运行时被丢弃


可以说TS的类型信息被浪费了,没有给运行时代码特别大的好处。


b. JS编译为字节码将导致体积增大

上面说到JS主要用于Web应用,Web应用如果提前编译为字节码将导致体积增大很多,对网络资源的消耗会更大。那么对于非Web应用,其实是可以做到提前编译为字节码的,比如Hermes引擎。


Hermes作为React Native的运行时,是作为App预装到用户的设备上,除了热更新这种场景外,绝大部分情况下是不需要打开App时动态下载代码资源的,所以体积增大的问题影响不是很大,但是预编译带来的运行时效率提升的好处却比较明显。


所以相对于V8,Hermes去掉了JIT,支持了生成字节码,在构建App的时候,就把JS代码进行了预编译,预编译为了Hermes运行时可以直接处理的字节码,省去了在运行阶段解析AST语法树、编译为字节码的工作。


Hermes对JS编译和执行流程的改进



一句题外话,Hermes去除了对JIT的支持,除了因为JIT会导致JS引擎启动时间变长、内存占用增大外,还有一部分可能的原因是,在iOS上,苹果为了安全考虑,不允许除了Safari和WebView(只有WKWebView支持JIT,UIWebView不支持)之外的第三方应用里直接使用JSC的JIT能力,也不允许第三方JS运行时支持JIT(相关问题)。


甚至V8专门出了一个去掉JIT的JIT-less V8版本来在iOS上集成,Hermes似乎也不太可能完全没考虑到这一点。



c. 取长补短

在讨论了V8的JIT和Hermes的预编译之后,我们再来看看ArkCompiler,截取一段官方博客里的描述


博客描述


还记得上面说的“TS的类型信息被浪费了”吗?TS的类型信息只在开发时有用,在编译阶段就被丢弃了,而ArkCompiler就是利用了这一点,直接在App构建阶段,利用TS的类型信息直接预编译为字节码以及优化机器码。


即在ArkCompiler中,不存在TS->JS的这一步转译,而是直接从TS编译为了字节码和优化机器码(这里存疑,官网文档没有找到很明确的说法,不是特别确定是否有TS->JS的转译。详见评论区,如果有知道的大佬可以在评论区交流一下)。


同时由于鸿蒙应用也是一个App而不是Web应用,所以ArkCompiler和Hermes一样,也是在构建App时就进行了预编译,而不是在运行阶段做这个事情。


ArkCompiler对JS/TS编译和执行流程的改进


简单总结下来,ArkCompiler像Hermes一样支持生成字节码,同时又将V8引擎JIT生成机器码的工作也提前在预编译阶段做了。是比Hermes只生成字节码的AOT更进一步的AOT(同时生成字节码和部分优化后的机器码)。


LiteActor轻量化并发


到这里其实已经可以回答上面讲到的第二个问题了,ArkTS还是单线程语言吗?


答案是:是的,还是单线程语言。但是ArkTS里通过Worker和TaskTool这两种方式支持并发。


同时ArkCompiler对现有的Worker进行了一些优化,直接看官网博客


LiteActor轻量化并发


LiteActor轻量化并发博客描述


这里的Actor是什么呢?Actor是一种并发编程里的线程模型。


线程模型比较常见的就是共享内存模型,多个线程之间共享内存,比如Java里多个线程共享内存数据,需要通过synchronized同步锁之类的来防止数据一致性的问题。


Actor模型是另一种线程模型,“Actor”是处理并发计算的基本单位,每个Actor都有自己的状态,并且可以接收和发送消息。当一个Actor接收到消息时,它可以改变自己的状态,发送消息给其他Actor,或者创建新的Actor。


这种模型可以帮助开发者更好地管理复杂的状态和并发问题,因为每个Actor都是独立的,它们之间不会共享状态,这可以避免很多并发问题。同时,Actor模型也使得代码更易于理解和维护,因为每个Actor都是独立的,它们的行为可以被清晰地定义和隔离。


到这里大家应该已经比较明白了,前端里的Web Worker就是这种线程模型的一种体现,通过Worker来开启不同的线程。


源码安全


按照官网的说法,ArkCompiler会把ArkTS编译为字节码,并且ArkCompiler使用多种混淆技术提供更高强度的混淆与保护,使得HarmonyOS应用包中装载的是多重混淆后的字节码,有效提高了应用代码安全的强度。


源码安全


2. Question2 - ArkTS还是单线程语言吗


这个刚刚已经回答了,还是单线程语言,借用官网的描述:



HarmonyOS应用中每个进程都会有一个主线程,主线程有如下职责:



  1. 执行UI绘制;

  2. 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上;

  3. 管理其他线程(例如Worker线程)的ArkTS引擎实例,例如启动和终止其他线程;

  4. 分发交互事件;

  5. 处理应用代码的回调,包括事件处理和生命周期管理;

  6. 接收Worker线程发送的消息;


除主线程外,还有一类与主线程并行的独立线程Worker,主要用于执行耗时操作,但不可以直接操作UI。Worker线程在主线程中创建,与主线程相互独立。最多可以创建8个Worker。



ArkTS线程模型


3. Question3 - 基于TS拓展了什么


当前,ArkTS在TS的基础上主要扩展了如下能力:



  • 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。

  • 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。

  • 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。


而上面这些,也就是我们接下来要介绍的ArkTS+ArkUI的语法。


四、 ArkTS & ArkUI


首先,在聊ArkUI之前,还有一个问题大家可能比较感兴趣:ArkUI是怎么渲染我们写的UI呢?


答案是自绘,类似于Flutter,使用自己的渲染引擎(应该是发展于Skia),而不是像RN那样将UI转为不同平台上的底层UI。


不管是从官网的描述[1]、[2]来看,还是社区里的讨论来看,ArkUI的渲染无疑是自绘制的,并且ArkUI和Flutter之间的联系很密切:


社区里的一些讨论


1. 基本语法


从前端的角度来看,ArkTS和ArkUI的定位其实就是类似于前端中TS+React+配套状态管理工具(如Redux),可以用TS写声明式UI(有点像写jsx),下面是基本语法:


基本语法



  • 装饰器


    用于装饰类、结构、方法以及变量,并赋予其特殊的含义。


    如上述示例中@Entry、@Component和@State都是装饰器,@Component表示自定义组件,@Entry表示该自定义组件为入口组件,@State表示组件中的状态变量,状态变量变化会触发UI刷新


  • 自定义组件


    可复用的UI单元,可组合其他组件,如上述被@Component装饰的struct Hello


  • UI描述


    以声明式的方式来描述UI的结构,例如build()方法中的代码块


  • 系统组件


    ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的ColumnTextDividerButton


  • 事件方法


    组件可以通过链式调用设置多个事件的响应逻辑,如跟随在Button后面的onClick()


  • 属性方法


    组件可以通过链式调用配置多项属性,如fontSize()width()height()backgroundColor()



2. 数据驱动UI


作为一个声明式的UI框架,ArkUI和其他众多UI框架(比如React、Vue)一样,都是通过数据来驱动UI变化的,即UI = f(State)。我们这里引用一下官网的描述:



在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。


自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系。



State和UI



View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。



在ArkUI中,提供了大量的状态管理相关的装饰器,比如@State@Prop@Link等。


ArkTS & ArkUI的状态管理总览


更多细节详见状态管理


3. 渲染控制


在ArkUI中,可以像React那样,通过if elsefor each等进行跳转渲染、列表渲染等,更多细节详见渲染控制



ArkUI通过自定义组件build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。



4. 更多语法


语法其实不是我们这篇文章的重点,上面是一些大概的介绍,更多语法可以详见官网,或者我的另外一篇专门讲解语法的笔记《前端视角下的ArkTS语法》(先留个占位符,有时间了补充一下)。


5. ArkTS & ArkUI小结


从前面的内容其实可以看到,ArkUI和RN相似点还挺多的:



  1. 都是使用JS/TS作为语言(ArkTS)

  2. 都有自己的JS引擎/运行时(ArkCompiler,方舟运行时)

  3. 引擎还都支持直接AOT编译成字节码


不同的是RN是将JS声明的UI,转换成iOS、Android原生的组件来渲染,而ArkUI则是采用自绘制的渲染引擎来自绘UI。


从这点来看,鸿蒙更像是Flutter,只不过把开发语言从Dart换成了JS/TS(ArkTS),和Flutter同样是自绘制的渲染引擎。


社区里其实也有类似的思考:其它方向的探索:JS Engine + Flutter RenderPipeLine。而ArkUI则是对这种思路的实现。


感觉这也可以从侧面解释为什么ArkUI的语法和Flutter比较像,应该参考了不少Flutter的实现(比如渲染引擎)。


而华为宣称鸿蒙可以反向兼容Flutter甚至是RN也就没有那么难以理解了,毕竟ArkUI里Flutter和RN的影子确实不少。


另外,除了ArkUI以外,华为还提供了一个跨平台的开发框架ArkUI-X,可以像Flutter那样,跨HarmonyOS、Android、iOS三个平台。


这么看来,ArkTS&ArkUI从开发语言、声明式UI的语法、设计思想来看,不管是前端、iOS、安卓、或者Flutter、RN,鸿蒙应用开发都是比较入门友好的。


五、 其他


1. 包管理工具


HarmonyOS开发中,使用的包管理工具是ohpm,目前看来像是一个借鉴pnpm的三方包管理工具,详见官方文档


另外,鸿蒙也提供了第三方包发布的仓库:ohpm.openharmony.cn


2. 应用程序结构


在鸿蒙系统中,一个应用包含一个或者多个Module,每一个Module都可以独立进行编译和运行。


应用程序结构


发布时,每个Module编译为一个.hap后缀的文件,即HAP。每个HarmonyOS应用可以包含多个.hap文件。


在应用上架到应用市场时,需要把应用包含的所有.hap文件打包为一个.app后缀的文件用于上架。


但是.app包不能直接安装到设备上,只是上架应用市场的单元,安装到设备上的是.hap


打包结构


开发态和打包后视图


鸿蒙应用的整体开发调试与发布部署流程大概是这样的:


开发-调试-发布-部署


HAP可分为Entry和Feature两种类型:



  • Entry类型的HAP:是应用的主模块
    在同一个应用中,同一设备类型只支持一个Entry类型的HAP,通常用于实现应用的入口界面、入口图标、主特性功能等。

  • Feature类型的HAP:是应用的动态特性模块
    一个应用程序包可以包含一个或多个Feature类型的HAP,也可以不包含;Feature类型的HAP通常用于实现应用的特性功能,可按需下载安装


而设计成多hap,主要是有3个目标:



  1. 为了解耦应用的各个模块,比如一个支付类型的App,Entry类型的hap可以是首页主界面,上面的扫一扫、消息、理财等可以的feature类型的HAP

  2. 方便开发者将多HAP合理地组合并部署到不同的设备上,比如有三个HAP,Entry、Feature1和Feature2,其中A类型的设备只能部署Entry和Feature1。B类型的设备只能部署Entry和Feature2

  3. 方便应用资源共享,减少程序包大小。多个HAP都需要用到的资源文件可以放到单独的HAP中



多说一句:从这些描述来看,给我的感觉是每个.hap有点类似于前端项目中Mono-repo仓库中的一个package,各个package之间有一定的依赖,同时每个package可以独立发布。



另外,HarmonyOS也支持类似RN热更新的功能,叫做快速修复(quick fix)。


六、 总结


现在再回到最开始那个问题:什么是鸿蒙?从前端视角来看,它是这样一个系统:



  • ArkTS作为应用开发语言

  • 类Flutter、Compose、Swift的声明式UI语法

  • 和React有些相似的数组驱动UI的设计思想

  • ArkCompiler进行字节码和机器码的AOT编译 + 方舟运行时

  • 类似Flutter Skia渲染引擎的自绘制渲染引擎

  • 通过提供一系列ohos.xxx的系统内置包来提供TS访问系统底层的能力(比如网络、媒体、文件、USB等)


所以关于HarmonyOS是不是安卓套壳,个人感觉其实已经比较明了了:以前应该是,但快要发布的HarmonyOS NEXT大概率不再是了。


其他一些讨论


其实在华为宣布了HarmonyOS NEXT不再兼容安卓后,安卓套壳的声音越来越少了,但现在网上另外一种声音越来越多了:




  1. HarmonyOS NEXT是一个大号的小程序底座,上面的应用都是网页应用,应用可以直接右键查看源码,没有安全性可言

  2. HarmonyOS NEXT上的微信小程序就是在小程序里运行小程序

  3. 因为使用的是ArkTS开发,所以的HarmonyOS NEXT上的应用性能必然很差



这种说法往往来自于只知道鸿蒙系统应用开发语言是TS,但是没有去进一步了解的人,而且这种说法还有很多人信。其实只要稍微看下文档,就知道这种说法是完全错误的


首先它的View层不是DOM,而是类似Flutter的自绘制的渲染引擎,不能因为使用了TS就说是网页,就像可以说React Web是网页应用,但不能说React Native是网页应用,同样也不是说Flutter是网页应用。


另外开发语言本身并不能决定最终运行性能,还是要看编译器和运行时的优化。同样是JS,从完全的解释执行(JS->AST->字节码->执行),到开启JIT的V8,性能都会有质的飞跃。从一些编程语言性能测试中可以看到,开启JIT的NodeJs的性能,甚至和Flutter所使用的Dart差不多。


而ArkCompiler是结合了Hermes和V8 JIT的特点,AOT编译为字节码和机器码,所以理论上讲性能应该相当不错。


(当然我也没有实机可以测试,只能根据文档来分析)。


上面这种HarmonyOS NEXT是网页应用的说法还有可能是由于,最早鸿蒙应用支持使用HTML、CSS、JS三件套进行兼容Web的开发,导致了刻板印象。这种开发方式使用的是FA模型,而目前这种方式已经不是鸿蒙主推的开发方式了。


到这里这篇文章就结束了,整体上是站在一个前端开发的视角下来认识和了解鸿蒙开发的,希望能帮助一些像我一样对鸿蒙开发感兴趣的前端开发入门。大家如果感兴趣可以到鸿蒙官网查看更多的了解。


如果感觉对你有帮助,可以点个赞哦~


作者:酥风
来源:juejin.cn/post/7366948087129309220
收起阅读 »

“WWW” 仍然属于 URL 吗?它可以消失吗?

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是Google、Instagram和Facebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHub...
继续阅读 »

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是GoogleInstagramFacebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHubDuckDuckGoDiscord。该组织选择执行相反的操作并重定向 http://www.example.com 到 example.com



“WWW”属于 URL 吗?一些开发人员对此主题持有强烈的意见。在了解了一些历史之后,我们将探讨支持和反对它的论据。


WWW是什么?


WWW代表"World Wide Web",是上世纪80年代晚期的一个发明,引入了浏览器和网站。使用"WWW"的习惯源于给子域名命名的传统:



如果没有WWW会发生什么问题?


1. 向子域名泄露cookies


反对"没有WWW"的域名的批评者指出,在某些情况下,subdomain.example.com可以读取example.com设置的cookies。如果你是一个允许客户在你的域名上运营子域名的Web托管提供商,这可能是不希望看到的。


然而,这种行为只存在于Internet Explorer中。


RFC 6265标准化了浏览器对cookies的处理,并明确指出这种行为是错误的。


另一个潜在的泄露源是example.com设置的cookies的Domain值。如果Domain值明确设置为example.com,那么这些cookies也将被其子域名所访问。


Cookie 值暴露于 example.com暴露于 subdomain.example.com
secret=data
secret=data; Domain=example.com

总之,只要你不明确设置Domain值,而且你的用户不使用Internet Explorer,就不会发生cookie泄露。


2. DNS的困扰


有时,"没有WWW"的域名可能会使你的域名系统(DNS)设置复杂化。


当用户在浏览器的地址栏中输入example.com时,浏览器需要知道他们想访问的Web服务器的Internet协议(IP)地址。浏览器通过你的域名的域名服务器向其DNS服务器(通常间接通过用户的互联网服务提供商(ISP)的DNS服务器)请求IP地址。如果你的域名服务器配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。


在某些情况下,你可能希望使用规范名称(CNAME)记录来代替为你的网站设置。这样的记录可以声明http://www.example.comexample123.somecdnprovider.com的别名,这会告诉用户的浏览器去查找example123.somecdnprovider.com的IP地址,并将HTTP请求发送到那里。


请注意,上面的示例使用了一个WWW子域名。对于example.com,不可能定义一个CNAME记录。根据RFC 1912,CNAME记录不能与其他记录共存。如果你尝试为example.com定义CNAME记录,example.com上的MX(邮件交换)记录将无法存在。因此,就不可能在@example.com上接收邮件


一些DNS提供商可以让你绕过这个限制。Cloudflare称其解决方案为CNAME解析。通过这种技术,域名管理员配置一个CNAME记录,但他们的域名服务器将暴露一个A记录。


例如,如果管理员为example.com配置了指向example123.somecdnprovider.com的CNAME记录,并且存在一个指向1.2.3.4example123.somecdnprovider.com的A记录,那么Cloudflare就会暴露一个指向1.2.3.4的example.com的A记录。


总之,虽然这个问题对希望使用CNAME记录的域名所有者来说是有效的,但现在有一些DNS提供商提供了合适的解决办法。


没有WWW的好处


大部分反对WWW的论点是实用性或外观方面的。"无WWW"的支持者认为example.comhttp://www.example.com更容易说和输入(对于不那么精通技术的用户可能更不容易混淆)。


反对WWW子域名的人还指出,去掉它会带来一种谦虚的性能优势。网站所有者可以通过这样做每个HTTP请求节省4个字节。虽然这些节省对于像Facebook这样的高流量网站可能会累积起来,但带宽通常并不是一种紧缺的资源。


有"WWW"的好处


支持WWW的一个实际论点适用于使用较新顶级域的情况。例如,http://www.example.miamiexample.miami无法立即被识别为Web地址。对于具有诸如.com这样的可识别顶级域的网站,这不是一个太大的问题。


对搜索引擎排名的影响


目前的共识是你的选择不会影响你的搜索引擎表现。如果你希望从一个URL迁移到另一个URL,你需要配置永久重定向(HTTP 301)而不是临时重定向(HTTP 302)。永久重定向确保你旧的URL的SEO价值转移到新的URL。


同时支持两者的技巧


网站通常会选择example.comhttp://www.example.com作为官方网站,并为另一个配置HTTP 301重定向。理论上,可以支持http://www.example.com和example.com两者。但实际上,成本可能会超过效益。


从技术角度来看,你需要验证你的技术栈是否能够处理。你的内容管理系统(CMS)或静态生成的网站需要将内部链接输出为相对URL以保留访问者的首选主机名。除非你可以将主机名配置为别名,否则你的分析工具可能会将流量分别记录在两个主机名上。


最后,你需要采取额外的措施来保护你的搜索引擎表现。谷歌将把URL的"WWW""非WWW"版本视为重复内容。为了在其搜索索引中去重复内容,谷歌将显示它认为用户更喜欢的那个版本——不论是好是坏。


为了在谷歌中保持对自己的控制,建议插入规范链接标签。首先,决定哪个主机名将成为官方(规范)主机名。


例如,如果你选择了www.example.com,则必须在 https://example.com/my-article里的  上的标记 中插入以下代码段:


    <link href="https://www.example.com/my-article" rel="canonical"> 

这个代码片段告诉谷歌"无WWW"变体代表着相同的内容。通常情况下,谷歌会在搜索结果中偏好你标记为规范的版本,也就是在这个例子中的"WWW"变体。


总结


对于是否在URL中加入"WWW",人们有不同的观点。下面是支持和反对的论点:


支持"WWW"的论点:



  1. 存在子域名的安全性问题:某些情况下,子域名可以读取主域名设置的cookies。虽然这个问题只存在于Internet Explorer浏览器中,并且已经被RFC 6265标准化修复,但仍有人认为使用"WWW"可以避免潜在的安全风险。

  2. DNS配置的复杂性:如果你的域名系统(DNS)配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。但如果你想使用CNAME记录来设置规范名称,那么"没有WWW"的域名可能会导致一些限制,例如无法同时定义CNAME记录和MX(邮件交换)记录。

  3. 对搜索引擎排名的影响:对于使用较新顶级域的网站,使用"WWW"可以帮助识别网址,而不是依赖可识别的顶级域名。然而,目前的共识是选择是否使用"WWW"对搜索引擎表现没有直接影响。


支持去除"WWW"的论点:



  1. 实用性和外观:去除"WWW"可以使域名更简洁和易于输入,减少了用户可能混淆的机会。

  2. 节省字节:去除"WWW"可以每个HTTP请求节省4个字节。虽然这对于高流量网站来说可能是一个可累积的优势,但对于大多数网站来说,带宽通常不是一个紧缺的资源。


最佳实践:
一般来说,网站会选择将example.com或www.example.com作为官方网址,并对另一个进行重定向。你可以通过使用HTTP 301永久重定向来确保旧URL的SEO价值转移到新URL。同时,你还可以在页面的标签中插入规范链接标签,告诉搜索引擎两个URL代表相同的内容,以避免重复内容问题。


需要注意的是,在做决策时要考虑到技术栈的支持能力、DNS配置的限制和谷歌对搜索排名的处理方式。


作者:狗头大军之江苏分军
来源:juejin.cn/post/7263274550074507321
收起阅读 »

微信之父张小龙的一次内部分享

本文分享一下凌览近期看的一本书《微信背后的产品观》,它源自2012年7月微信产品经理张小龙一次长达8小时的腾讯内部分享。 了解人性 产品经理是站在上帝身边的人,上帝根据他的期望,创造了人,并赋予人一些习性,让人类的群体在这些习性下发展演化。而产品经理实际是在理...
继续阅读 »

本文分享一下凌览近期看的一本书《微信背后的产品观》,它源自2012年7月微信产品经理张小龙一次长达8小时的腾讯内部分享。


了解人性


产品经理是站在上帝身边的人,上帝根据他的期望,创造了人,并赋予人一些习性,让人类的群体在这些习性下发展演化。而产品经理实际是在理解了人的习性后,像上帝一样,建造系统并建立规则,让群体在系统中演化。



书中提及两本书《失控》、《乌合之众》,两本书结合的逻辑:群体在特定规则下的无序演化产生很多意想不到的结果。微信的很多产品功能的设计思想都和这两本书的理论很契合,比如漂流瓶、摇一摇,拍一拍等。



优秀的产品经理需要具备的能力:



  • 了解人的习性,需求从人性中产生

  • 了解群体的心理


"人"的特性:



  • 人是懒惰的,懒惰是创新的动力,案例:语音查找联系人,解决走路或双手不方便时要给一个人发微信,输入半天还找不出的情况

  • 人是跟风的,"因为别人都在用",时尚是驱动力,在互联网产品中,"时尚"是重要的驱动力

  • 人是没有耐心的,用户没有耐心看产品说明书,不要尝试去引导用户,去教育用户,没有人愿意去接受你的引导和教育。一定是他拿过来就会用才是最直接的(产品使用操作简单)

  • 人是不爱学习的,"马桶阅读"理论:不要给用户超过马桶上看不完的内容

  • 群体是"乌合之众",理论出自《乌合之众》一书,群体智商低于个体,互联网产品的用户是群体,不是个体


如何确定一个需求



  • 对于新点子,99%的情况下否定是对的,不要随便臆想需求

  • 不要用户说什么就做什么,用户的反馈是帮助你了解他们的想法,用户的需求是零散的,应该进行归纳抽象

  • 不从同类产品里找需求,另的产品决定做这个需求,是有他们自己的理解,并深入分析思考过。如果别人说好,我们就直接照搬,其实没有深刻理解需求

  • 不听从产品经理的需求,他们不是用户却自认为代表用户,他们分析过于理性,他们会要求要显示在线、要已读、要分组、要滤镜、要涂鸦、要多端同步、要群名片、要赞头像.....如果产品经理都把这些当作用户朴素的需求做进去,这将是可怕的事情

  • 需求来自你对用户的了解

    • 需求不来自调研

    • 需求不来自分析

    • 需求不来自讨论

    • 需求不来自竞争对手



  • "爽"用过功能,爽是体验。爽比功能更易传播

  • 只抓主场景,不做全功能,做大而全很容易,做小很难,如果没有化繁为简的功力,就控制自己的欲望,每天砍掉几个需求的爽,远大于提出几个需求,案例:朋友圈只能发图片,发140字的难度远胜一张图片


如何设计一个产品



  • 好的产品价值观和认知是成为优秀产品的前提

  • 先做产品结构,之后才是功能细节。微信功能细算特别多,但看起来还是很简单,做一个新版本都不知道它有什么新功能,先把微信的骨骼梳理清楚,枝叶的东西藏得很深也没关系,这样整个产品才会乱掉

  • 功能模块之间是有机联系的关系,独立的功能堆砌很危险

  • 设计是分类

  • 抽象才能化繁为简,如果有100个需求,而我们能把这100个需求汇总成10个需求,这就是"抽象"

  • 越简单的分类越容易被接受,微信会升级,但结构和界面依然保持简单,过多变化易引来用户不适应

  • 挖掘需求背后的本质需求

  • 宁愿损失功能也不损失体验


最后


张小龙强调我所说的,都是错的,每个人都会有自己的解决问题的办法,没有永远的正确教条


书未有问答环节,这里精简摘录下我认为有启发的回答:


Q:为什么人人都是产品经理?怎么做到跟其他也是产品经理的人不一样?


A:因为人人都可以提问题,人人都可以指手画脚,用户也会,但最难的是找到本质的东西,这才是产品经理要一定具备的


Q:从普通工程师到现在做成一个伟大的产品,你是怎么一步一步走过来的?


A:经历上千个实战的锻练,越多越好,一个人要成为一个领域的专家,要付出一万个小时的努力和专业训练



回答提及一本书《另类成功学》,有兴趣可以看看。



如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。




作者:程序员凌览
来源:juejin.cn/post/7274163003158822912
收起阅读 »

程序员的未来发展会是什么?跟一位同学沟通之后的思考

Hello,大家好,我是 Sunday。 说起程序员的发展方向好像是一个老生常谈的话题了。我记得在过去的十年中,我曾经无数次的看到过各种文章来说类似的话题。 不过这样的话题又好像是一个经久不衰的,特别是在目前这样行情不好的情况下,我相信有很多同学都在处于前所未...
继续阅读 »

Hello,大家好,我是 Sunday。


说起程序员的发展方向好像是一个老生常谈的话题了。我记得在过去的十年中,我曾经无数次的看到过各种文章来说类似的话题。


不过这样的话题又好像是一个经久不衰的,特别是在目前这样行情不好的情况下,我相信有很多同学都在处于前所未有的迷茫状态之中。


比如,昨天有个同学来问我说:“前端发展方向是什么?全面发展?工程化?还是架构师?”



所以,为了能更好的解决大家的困惑,今天这篇文章咱们就来说一说程序员未来的发展方向、什么人适合什么方向、以及分别需要做什么样的准备。


程序员发展方向


程序员是一个技术岗位,但是它的发展方向绝对不仅局限于“技术领域”。


所以,当我们去考虑发展方向或者是未来职业规划的时候,就不能仅从技术的角度来进行分析。


下面是我认为对于程序员而言,最有利的几个方向(一家所见,仅供参考):



  1. 某一个行业的技术专家

  2. 技术大牛(包含 Leader 岗)

  3. 自由职业者

  4. remote 远程工作


下面咱们一个一个去说...


1. 某一个行业的技术专家


1.1 什么是某一个行业的技术专家(后面简称:技术专家)


回忆下我们的工作,我们目前所做的大部分工作是不是都在为了完成某一个业务而存在的?


有的同学在做医疗业务、有 金融业务、有 政府项目电商服务 等,总之无论是那种,所有的项目总归是在为某一个业务而服务的。


而所谓的技术专家,指的就是:以技术为基本,成为非常熟悉该行业的人员


以我为例,我首先是一个程序员,其次是一个 教育方向的程序员 就是这个道理。


1.2 什么样的人适合


想要做技术专家,那么一定要明确一个大前提:技术本没有价值,只有使用技术完成了一个有价值的事情之后,这个事情才可以赋予技术价值。


如果你想要以技术专家为目标,那前提一定是 你要深刻的认同这句话。如果你不认同,那么这个方向就 不适合 你。


1.3 需要怎么做


如果想要成为技术专家,那么就 不能 频繁的更换行业。


甚至,当你选定了一个行业之后,就应该长期立足下去,只要这样你才能逐步的熟悉这个行业的运行规律。


所以,这就要求我们在跳槽的时候,尽量 选择与上家公司从事相同行业的公司去做(没有竞业协议的前提下),而不是随便跳转一个行业就入职。


2. 技术大牛(包含 Leader 岗)


2.1 什么是技术大牛


其实技术大牛是很难定义的。古语说:文人相轻(程序员总不能说自己是武人吧)。所以,对于程序员而言,所谓的技术大牛一定是一个相对的,而不是绝对的。就算是尤雨溪也有被人喷菜的时候。


所以说,如果要给技术大牛 一个定义的话,那么指的就是:在某一个范围(公司或者团体)内,具有一定权威的人员


2.2 什么样的人适合


这里其实有两个方向,不同的方向适合的人不同:



  1. 纯技术人员,不参与管理: 比较适合不善言辞,并且不愿意余人交流,完全沉迷于技术的人

  2. 以管理为主的技术人员: 具备一定的技术能力,但是同时更愿意与人沟通,懂得人情世故的人


2.3 需要怎么做


如果是单纯的技术就比较简单了,做法分为两步:



  1. 多在“团体”内发言:这个“团体”代表了很多东西,可以是:论坛、网站、公司或其他。

  2. 多学习各种新的技术,然后把这些技术 输出出去


如果你只会输入,不会输出。那么是无法成为技术大牛的。


而 Leader 就比较复杂了,相比于技术而言,更多的其实是 人情世故。所以如果想要将来做管理岗位,那么就需要练习好人情世故的处理能力。


3. 自由职业者


关于自由职业者,前几天我专门写过一篇文章 并不自由的自由职业?回顾下我的五一小长假 ,甚至还在 B站 还录制了对应的视频:



所以这里就不再细说了。


4. remote 远程工作


4.1 什么是remote


所谓的 remote 指的就是 远程工作。比如:你在家工作,不需要到公司坐班。符合这个条件的都属于 远程工作。


目前大家提起 remote 大多数指的其实是:国外的远程工作。也就是 人在国内,为国外的公司工作。


可能是因为国内太卷的原因,目前 remote 的工作被非常多的人推崇。


但是 我个人建议大家理性看待。有兴趣的同学可以看下我写的这篇文章 remote 经验分享,它真有你想象的那么好吗?,这里就不过多赘述了。


4.2 什么样的人适合


如果不喜欢坐班,享受那种工作几个月,休息几个月的状态的话,那么可以尝试 remote 的工作。


4.3 需要怎么做


其实想要做 remote 的工作并不困难,核心是两个点:



  1. 英语:至少要可以做到 雅思6.5 的水平

  2. 岗位:可以多关注 领英、电鸭社区、indeed 等


但是要 注意:防止被骗!!!


因为 remote 无法看到对方的公司信息,并且合同形同虚设,所以有同学出现过 工作 1-2 个月之后无法收到工资的情况,所以要特别注意这一点!


总结


OK,以上是我在跟那位同学沟通之后,大致总结的一些对程序员比较友好的发展方向,希望可以对大家有帮助~~


作者:程序员Sunday
来源:juejin.cn/post/7368294440547041318
收起阅读 »

大环境越不好 人就越玄学

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。 问广大学子毕业后最想从事什么工作。 当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。 只有很少一部分选择了事业单位和公务员,这...
继续阅读 »

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。


问广大学子毕业后最想从事什么工作。


当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。


只有很少一部分选择了事业单位和公务员,这部分同学还有相当比例来自对考公自古有执念的山东。


而在其他省份,多数同学都认为自己能拥有光明的未来,当然不会喜欢公务员这种工资稳定得低,日复一日枯坐案前,早早就能一眼望到头的工作。


在当时年轻人眼里,公务员属于“实在不行就只能回家考公“的备胎,地位约等于“实在不行就找个老实人嫁了“的级别。


但后来的故事我们都知道了,经济大船这几年驶入了深水区,风浪越来越大,鱼也越来越贵。


于是四平八稳旱涝保收的体制内,这几年摇身一变,一跃成为了那个最靓的仔。不得不说,人确实是时代的产物,环境的变化可以完全改变一个人的决策。


大环境好的时候,人们会不自觉地高估自身的努力,那时候人们是相信努力一定会有收获的。有时候过于相信了,但这在经济高速增长的年代并不会有太大问题,你还是会得到属于自己的那块蛋糕的。


但当经济增速换档时,付出与回报的比例开始失衡,努力就能收获的简单逻辑不攻自破。变成了努力也不一定有收获,进而发展成努力大概率不会有收获,最后演变成一命二运三风水,努力奋斗算个鬼


这种心态的转变也解释了为啥从去年以来,越来越多的年轻人开始扎堆去寺庙求签祈福,排的长队连起来能绕地球三圈,看得旁观的老大爷直摇头说,“真搞不懂这些小年轻是怎么想的,偶像粉丝见面会咋还跑到庙里来开了?!”


人在逆境迷茫时,是容易被玄学吸引。逆境意味着前路遇阻,意味着你迫切需要一些指引,而玄学恰好满足了这方面需求。


命运这个东西,有时候真蛮捉摸不透的。


我认识一小姐姐,为一场决定人生的重要考试做足了准备,结果在赶往考场的路上,书包就这么巧被扒手偷了,里面开卷考试所有的资料全部丢失,直接导致她逃汰出局,泪洒当场。


还有一大哥,在升职加薪岗位竞争的关键阶段,突然一场急病,好巧不巧失声了,一句话也说不出来,参加不了竞聘答辩,眼睁睁看着大好机会就此溜走。


等这事过去了,他一下子又能正常说话,跟被老天上了沉默debuff一样,你说他找谁说理去呢。


人活得时间越长,就越信“命“这个东西,越能意识到自己真正能把控的其实少得可怜,随便一点意外都能直接改变整个人生走向。


这种感悟放在以前,一般都是上了些年纪的人才会有的,但随着这两年经济增速换挡,年轻人频繁碰壁,被命运按在地上摩擦的次数多了,自然也就信了“命”,求签问道的也就跟着多起来了。


说句不好听的话,我觉得这样挺好的。不是说求签问道这个行为好,而是这种行为背后暗含着一个巨大的心理转变,我认为很好。


那就是放过自己。亚洲人尤其是我们特别不愿意放过自己,从出生开始就活在比较中,长辈们连夸个人都要这么夸,说哎呀,你学习真用功,比学习委员还用功;哎呀,你工资挺高,比隔壁小王还要高。


骂你的时候也一定要捎带上别人,说你看谁谁谁多厉害,你再看看你,一定是你还不够努力。


就是这种搞法很容易让人把责任全揽自己身上,对自我要求过高,最后的结果就是崩掉,就累嘛!


但现在不一样了,现代人在网络上看了太多含着金汤匙出生在罗马的人,和那些老天爷追着赏饭吃的人。


他们跟我们之间的差距大到几辈子都弥补不上,那努力万能论也就不攻自破了嘛。


于是越来越多的小伙伴开始承认自我的局限,承认努力也不一定有收获,承认人生不如意十之八九,慢慢也就承认了“命运”这个东西,开始顺其自然,没那么多执念了。


不过有些人过于放飞自我,摆烂走了另一个极端,那也是要出问题的。


即便是玄学,它也没有彻底否定个人奋斗,大富靠命没错,但小富靠勤,靠双手取得一些小成就,让日子过得舒服些还是没啥问题的。


其实我觉得一个比较合适的世界观应该是这个样子:首先咱得承认不可抗力,承认“命”与“运”这个东西是真实存在的,如果你不喜欢这两个玄乎的字,可以用“概率”代替,我们永远得做好小概率事件砸到头上的准备。


有时候拼尽一切就是没有好的结果,这咱得承认,但同时这也并不意味着从此放弃一切行动,落入虚无主义的陷阱。


人还是要去做一些什么的。比如精进某项专业技能,逐步提升自身能力,为的不是那点工资,而是一件更重要的事,抓住运气。


运气有多重要,大家都明白,它比努力重要得多。


运气这东西打比方的话,就像一个宝箱,会随机在你面前掉落,但这些宝箱自带隐形属性,你等级太低的话就看不见它,自然也就抓不住这些运气。


用现实举例,“运气”就像你在工作中遇到了某个本来还可以拉你一把的贵人,结果你的等级太低,工作能力稀碎,贵人一看,这货不值得我帮,转身走了。他这个宝箱对你而言就隐形了,消失了。


而且最讽刺的是你从头到尾都被蒙在鼓里,根本不知道自己错失了一次宝贵的机会,所以为了避免运气来了你抓不住,又溜走的这种尴尬情况出现,我们还是要去精进和磨练一下社会技能,尽量达到能在某些场合被人夸奖的程度。


把等级刷高一些,之后该吃吃该喝喝,耐心等待宝箱的出现。这可能也是以前人们常说的,“尽人事听天命”的另一种解释吧。


也希望今天聊的关于命和运的这些内容,能启发到一些小伙伴,大家一起认认真真,平平淡淡的生活。


作者:程序员Winn
来源:juejin.cn/post/7317704462436139058
收起阅读 »

OPPO率先适配Android 15,首批机型名单公布

北京时间5月15日,代号为「Vanilla Ice Cream(香草冰淇淋)」的 Android 15 在2024谷歌 I/O 大会正式亮相。作为全球智能手机市场领先品牌,OPPO 作为连续六年首批适配 Android 新系统的厂商,此次不仅率先发布了基于 A...
继续阅读 »

北京时间5月15日,代号为「Vanilla Ice Cream(香草冰淇淋)」的 Android 15 在2024谷歌 I/O 大会正式亮相。作为全球智能手机市场领先品牌,OPPO 作为连续六年首批适配 Android 新系统的厂商,此次不仅率先发布了基于 Android 15 的开发者预览版助力开发者抢先适配,更以全流程、全方位的适配保障服务,持续为广大开发者保驾护航。

图1.png

OPPO 公布 Android 15 适配指南,督促开发者升级64位架构

据了解,Android 15 带来了一系列令人瞩目的新功能和改进,包括全新设计的兼容性调试工具、安全与隐私相关的强化措施、系统优化和新的API支持。这些功能使开发者能够更轻松地构建和维护应用,为用户提供更好的性能和体验。

基于 Android 15 Beta 版本,OPPO 推出 ColorOS 开发者预览版,OPPO Find X7、一加12首批支持。值得注意的是, OPPO 这次特别督促开发者对64位架构的全面升级,确保所有软件和应用在新系统中都能实现最佳性能。据了解,Android 15 系统升级 Vendor 的手机均不支持32位应用,若不进行64位升级将导致应用后续无法下载使用。64位架构优势显著,更快的处理速度、更大的内存支持以及更高的效率,使开发者能够充分利用 Android 15 的潜力,为用户提供更加流畅和丰富的应用体验。开发者可前往OPPO开放平台官网,抢先下载并体验开发者预览版。

图2.png

全方位服务保障,助力开发者推进系统适配

为了助力开发者高效适配 Android 15 系统,OPPO 提供了包括适配文档、适配工具、适配资讯以及专家交流等在内的全面支持和服务。

全面清晰的指导文档帮助各类型 APP 开发者迅速找到所需的适配方案;云测服务提供实时在线的远程调试功能,支持开发者随时接入,帮助开发者快速验证适配结果;OPPO开放平台官网适配支持专区实时更新 Android 15 最新动态,开发者可随时获取第一手适配资讯。此外,OPPO 还提供了7*24小时在线答疑服务,专人协助解决适配技术难题,进一步提升适配效率。技术指引也将实时更新,集中解答开发者提出的高频问题。

海量全面的文档支持,贯穿全程的指导升级服务,让每一个开发者都不掉队。

图3.png

据悉,5月22日,OPPO 将联合知名技术社区 51CTO 举办「OTalk | Android 15 适配开发者交流专场」线上直播活动,与行业开发者深入交流对话。届时将特别邀请 OPPO 高级工程师带来 Android 15 新特性的深度解读及适配建议,分享 OPPO 适配支持服务,解答开发者常见问题,助力开发者高效适配新版本。

图4.png

作为 Android 生态系统的关键参与者,OPPO 连续6年首批适配 Android 新版本,持续为开发者提供全流程适配支持和服务,携手开发者高效完成版本迭代优化与应用兼容性测试,共同将更安全、更流畅的系统体验带给用户。

接下来,OPPO 将持续提供关于 Android 15 适配的最新进展,广大开发者可关注「OPPO开放平台」后续公告,以获取更多详细信息和支持资源。

收起阅读 »

责任链模式最强工具res-chain🚀

web
上面的logo是由ai生成 责任链模式介绍 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避...
继续阅读 »
image.png

上面的logo是由ai生成



责任链模式介绍


责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避免了请求的发送者和接收者之间的直接耦合


在责任链模式中,每个处理者都持有对下一个处理者的引用,即构成一个链表结构。当请求从链头开始流经链上的每个处理者时,如果某个处理者能够处理该请求,就直接处理,否则将请求发送给下一个处理者,直到有一个处理者能够处理为止。


这种方式可以灵活地动态添加或修改请求的处理流程,同时也避免了由于请求类型过多而导致类的爆炸性增长的问题。


看完以上责任链的描述,有没有发现跟Node.js的某些库特别的像,没错,就是koa。什么?没用过koa?那我建议你立马学起来,因为它用起来特别的简单。


下面来一个简单使用koa的例子:


const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
}

next(); // 执行下面的回调函数
});

app.use(async (ctx, next) => {
if (ctx.request.url === '/hello') {
ctx.body = 'hello world';
return;
}
});

app.listen(3000);

通过node运行上面的代码,在浏览器请求localhost:3000,接口就会返回home,当我们请求localhost:3000/hello,接口就会返回hello world


上面对请求的处理过程就很符合职责链模式的思想,我们可以清楚的知道每个链做的工作,并且链条的顺序流程也很清晰。


有人就会问,只在一个回调里面也能处理呀,比如下面的代码:


app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
} else if (ctx.request.url === '/home') {
ctx.body = 'hello world';
return
}
});

是的,上面的代码确实可以实现,但是这就要回到我们使用责任链模式的初衷了:为了逻辑解耦。


责任链解决的问题


我们继续接着聊上一节的问题,使用if确实可以实现相同效果,但是在某些场景中,if并没有职责链那么好用,为什么这么说呢。


我们找一个应用案例举个例子:



假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:




  • 缴纳500元定金的用户可以收到100元优惠券;

  • 缴纳200元定金的用户可以收到50元优惠券;

  • 没有缴纳定金的用户进入普通购买模式,没有优惠券。

  • 而且在库存不足的情况下,不一定能保证买得到。


下面开始设计几个字段,解释它们的含义:



  • orderType:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。

  • pay:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。

  • stock:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。


下面我们分别用if和职责链模式来实现:


使用if:


const order = function (orderType, pay, stock) {
if (orderType === 1) {
if (pay === true) {
console.log('500元定金预购,得到100元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
} else if (orderType === 2) {
if (pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
}

order(1, true, 500) // 输出:500元定金预购,得到100元优惠券'

虽然上面的代码也可以实现需求,但是代码实在是难以阅读,维护起来更是困难,如果继续在这个代码上开发,未来肯定会成为一座很大的屎山。


下面我们使用责任链模式来实现:


function printResult(orderType, pay, stock) {
// 这里ResChain类是模拟koa的写法,后面会讲如何实现ResChain
// 请先耐心看完它是如何处理的
const resChain = new ResChain();
// 针对500元定金的情况
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next(); // 这里将会调用order200对应的回调函数
});
// 针对200元定金的情况
resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next(); // 这里会调用noOrder对应回调函数
});
// 针对普通用户购买的情况
resChain.add('noOrder', (_, next) => {
if (stock > 0) {
console.log('普通用户购买,无优惠券');
} else {
console.log('手机库存不足');
}
});

resChain.run(); // 开始执行order500对应的回调函数
}

// 测试
printResult(1, true, 500); // 500元定金预购,得到100元优惠券
printResult(1, false, 500); // 普通用户购买,无优惠券
printResult(2, true, 500); // 200元定金预购,得到50元优惠券
printResult(3, false, 500); // 普通用户购买,无优惠券
printResult(3, false, 0); // 手机库存不足

以上的代码经过责任链处理之后特别的清晰,并且减少了大量的if-else嵌套,每个链的职责分,我们可以看出责任链模式存在的优点:



  1. 降低了代码之间的耦合,很好的对每个处理逻辑进行封装。在每个链条内,只需要关注自身的逻辑实现。

  2. 增强了代码的可维护性。我们可以很轻易在原有链条内的任何位置添加新的节点,或者对链条内的节点进行替换或者删除。


责任链还特别的灵活,如果说后面pm找我们加需求,需要加多一个预付定金400,返回80元优惠券,处理起来也是易如反掌,只需要怼回去,只需要在order500下面加多一个节点处理即可:


... 
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next();
})
+ // 加上这一块
+ resChain.add('order400', (_, next) => {
+ if (orderType === 3 && pay === true) {
+ console.log('400元定金预购,拿80元优惠券');
+ return;
+ }
+ next();
+ })

resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next();
})
...

就是这么简单。那这个ResChain是如何实现的呢?


封装ResChain


先别急,首先我们来了解一下koa是如何实现:在链节点的回调函数内调用next就可以跳到下一个节点的呢?


话不多说,直接看源码,参考的库是koa-compose,代码也是特别的简洁:


function compose (middleware) {
// 这里传入的middleware是函数数组,例如: [fn1, fn2, fn3, fn4, ...]
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 判断数组里的元素是不是函数类型
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0);

// 这里利用了函数申明提升的特性
function dispatch (i) {
// 这里是防止重复调用next
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i

// 从middleware中取出回调函数
let fn = middleware[i]
if (i === middleware.length) fn = next

// 如果fn为空了,则结束运行
if (!fn) return Promise.resolve()

try {
// next函数其实就是middleware的下一个函数,执行next就是执行下一个函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}

看完源代码,我们接着来实现ResChain类,首先整理一下应该要有的方法:



  • add方法。可以添加回调函数,并按添加的顺序执行。

  • run方法。开始按顺序执行责任链。


add方法执行的时候,把回调函数按顺序push进一个数组中。


export class ResChain {

/**
* 按顺序存放链的key
*/

keyOrder = [];
/**
* key对应的函数
*/

key2FnMap = new Map();
/**
* 每个节点都可以拿到的对象
*/

ctx = {}
constructor(ctx) {
this.ctx = ctx;
}

// 这里用key来标识当前callback的唯一性,后面重复添加可以区分。
add(key, callback) {
if (this.key2FnMap.has(key)) {
throw new Error(`Chain ${key} already exists`);
}

this.keyOrder.push(key);
this.key2FnMap.set(key, callback);
return this;
}

async run() {
let index = -1;
const dispatch = (i) => {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}

index = i;
const fn = this.key2FnMap.get(this.keyOrder[i]);
if (!fn) {
return Promise.resolve(void 0);
}

return fn(this.ctx, dispatch.bind(null, i + 1));
};

return dispatch(0);
}
}

add方法的第一个参数key可以用来判断是否已经添加过相同的回调。


有人会说,koa的中间件是异步函数的,你这个行不行?


当然可以,接下来看个异步的例子:


const resChain = new ResChain();

resChain.add('async1', async (_, next) => {
console.log('async1');
await next();
});


resChain.add('async2', async (_, next) => {
console.log('async2')
// 这里可以执行一些异步处理函数
await new Promise((resolve, reject) => {
setTimeOut(() => {
resolve();
}, 1000)
});

await next();
});


resChain.add('key3', async (_, next) => {
console.log('key3');
await next();
});


// 执行责任链
await resChain.run();

console.log('finished');

// 先输出 async1 async2 然后停顿了1秒钟之后,才输出async3 finished


🚧 需要注意:如果是异步模式,则链上的每个回调函数必须要 await next(),因为next函数代表下一个环的异步函数。



koa的中间件方式简直一毛一样。


有人可能还注意到了,ResChain实例化的时候可以传入对象,比如下面的代码:


const resChain = new ResChain({ interrupt: false });

传入对象具体有什么用法呢?可以用来获取一些在链中处理好的数据,来实现发送者和处理者的解耦。可能比较抽象,我们来举个例子。


比如需要进行数据校验的场景,如果不通过,则中断提交:


const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}
const resChain = new ResChain(ctx);

resChain.add('校验name', (ctx, next) => {
const { name = '' } = ctx;
if (name === '') {
ctx.error = '请填写name';
ctx.interrupt = true;
return;
}

next();
})

resChain.add('校验phone', (ctx, next) => {
const { phone = '' } = ctx;
if (phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
return;
}

next();
})

// 执行责任链
resChain.run();

// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}

如果是使用if来实现:


const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}

if(ctx.model.name === '') {
ctx.error = '请填写用户名';
ctx.interrupt = true;
}

if (!ctx.interrupt && ctx.model.phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
}

// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}

可以发现,对phone的判断逻辑,就要先判断interrupt是否为false才能继续,而且如果下面还有其他的字段校验,那必须都走一遍if。


这也是责任链的一个优势,可以在某个环节按自己的想法停止,不用继续走后面的节点。


目前我已经把这个工具上传到npm了,如果想要在自己的项目中使用,直接安装:
res-chain即可使用:


npm install res-chain

# 或者
# yarn add res-chain

引入:


import { ResChain } from 'res-chain';
// CommonJS方式的引入也是支持的
// const { ResChain } = 'res-chain';

const resChain = new ResChain();

resChain.add('key1', (_, next) => {
console.log('key1');
next();
});

resChain.add('key2', (_, next) => {
console.log('key2');
// 这里没有调用next,则不会执行key3
});

resChain.add('key3', (_, next) => {
console.log('key3');
next();
});

// 执行职责链
resChain.run(); // => 将会按顺序输出 key1 key2

芜湖起飞🚀。


有了这个工具函数,我们就可以视场景去优化项目中的一大坨if-else嵌套,或者直接使用它来实现一些业务中比较复杂的逻辑。


起源


这个工具诞生的过程还挺巧合的,某一天周六我在公司加班赶需求,发现需要在一堆旧逻辑if-else中添加新的逻辑,强迫症的我实在是无法忍受在💩山上继续堆💩。。。


我陷入沉思,用什么方式去优化呢?看了网上责任链模式的实现,感觉还是不够优雅。


无意中翻到了之前用koa写的项目,突然灵光乍现💡,koa的中间件不就是一个很棒的实践。调用next就能够往下一个节点走,不调用的话就可以终止。


于是立即动工,三下五除二就完成了。我还推荐给部门的其他前端小伙伴,他们也在一些需求的复杂逻辑中有运用。


总结



过去无意学到的某个知识,或者某个概念,在未来也许会发挥作用,你只需要做的就是等待。



没有koa这么棒的库,估计也不会有这工具了,所以还是得感谢它的作者如此聪明。😁


如果你也喜欢这个工具,欢迎去github里给个🌟,感谢。


如果有什么更好的建议,在底下留言,一起探讨。


工具链接


res-chain


参考



作者:Johnhom
来源:juejin.cn/post/7368662916151377959
收起阅读 »

你没见过的【只读模式】,被我玩出花了

web
前言 不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格、描述列表、查询表格 吗?先看看效果吧 ~ 表单场...
继续阅读 »

前言


不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格描述列表查询表格 吗?先看看效果吧 ~


表单场景


form-readonly.gif


表单列表场景


form-list-readonly.gif


描述列表场景


description-readonly.gif


查询表格场景


table-readonly.gif


编辑表格场景


edit-table-readonly.gif


上面看到的所有效果,背后都有 readonly 的存在



  1. 表单场景示例中表单列表场景示例中 使用 readonly,在实际业务中可能会应用到 编辑详情

  2. 描述列表场景示例中 使用 readonly,在实际业务中可能会应用到 单独的详情页 页面中

  3. 查询表格场景示例中 使用 readonly,在实际业务中应用很广泛,比如常见的日期,后端可能会返回字符串、空、时间戳,就不需要用户单独处理了 (挺麻烦的,不是吗)

  4. 编辑表格场景示例中 使用 readonly,在做一些类似 行编辑单元格编辑 功能中常用


下面就以 实现思路 + 伪代码 的方式和大家分享 readonly 的玩法


以 Date 组件为例


我们这里说的 Date 就是单纯的日期组件,不包含 pickermonth(月份)quarter(季度) 等,我们先思考一下,如何让 日期组件 可以在多处公用(查询表格、表单、编辑表格、描述列表)


多处公用


我们可以将 Date 组件进行封装,变成 ProDate,我们在 ProDate 中扩展一个属性为 readonly,在扩展一个插槽 readonly,方便用户自定义,以下为伪代码


<script lang="tsx">
import { DatePicker, TypographyText } from 'ant-design-vue'

export default defineComponent({
name: 'ProDate',
inheritAttrs: false,
props: {
readonly:{
type: Boolean,
default:false
}
},
slots: {
readonly: { rawValue: any }
},
setup(props, { slots, expose }) {
const getReadonlyText = computed(() => {
const value = toValue(dateValue)
return getDateText(value, {
format: toValue(valueFormat),
defaultValue: toValue(emptyText),
})
})

return {
readonly,
getReadonlyText,
}
},
render() {
const {
readonly,
getReadonlyText,
} = this

if (readonly)
return $slots.readonly?.({ rawValue: xxx }) ?? getReadonlyText

return <DatePicker {...xxx} v-slots={...xxx} />
},
})
</script>


上面的伪代码中,我们扩展了 readonly 属性和 readonly 插槽,我们 readonly 模式下会调用 getDateText 方法返回值,下面代码是 getDateText 的实现


interface GetDateTextOptions {
format: string | ((date: number | string | Dayjs) => any)
defaultValue: any
}

// 工具函数
export function getDateText(date: Dayjs | number | string | undefined | null, options: GetDateTextOptions) {
const {
format,
defaultValue,
} = options

if (isNull(date) || isUndefined(date))
return defaultValue

if (isNumber(date) || isString(date)) {
// 可能为时间戳或者字符串
return isFunction(format)
? format(date)
: dayjs(date).format(format)
}

if (isDayjs(date)) {
return isFunction(format)
? format(date)
: date.format(format)
}

return defaultValue
}

好了,伪代码我们实现完了,现在我们就假设我们的 ProDate 就是加强版的 DatePicker,这样我们就能很方便的集成到各个组件中了


集成到 表单中


因为我们是加强版的 DatePicker,还应该支持原来的 DatePicker 用法,我们上面伪代码没有写出来的,但是如果使用的话,还是如下使用


<template>
<AForm>
<AFormItem>
<ProDate v-model:value="xxxx" />
</AFormItem>
</AForm>

</template>

这样的话,我们如果是只读模式,可以在 ProDate 中增加 readonly 属性或插槽即可,当然,为了方便,我们实际上应该给 Form 组件也扩展一个 readonly 属性,然后 ProDatereadonly 属性的默认值应该是从 Form 中去取,这里实现我就不写出来了,思路的话可以通过在 Formprovide 注入默认值,然后 ProDate 中通过 inject


好了,我们集成到 表单中 就说这么多,实际上还是有很多的细节的,如果大家想看的话,后面再写吧


集成到 描述列表中


描述列表用的是 Descriptions 组件,因为大部分用来做详情页,比较简单,所以这里我将它封装成了 json 方式,用 schemas 属性来描述每一项的内容,大概是以下用法


<ProDescriptions
title="详情页"
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

解释一下:


上面的 schemas 中的项可以简单看成如下代码


<DescriptionsItem>
<ProDate
readonly
:value="get(dataSource,'time')"
/>

</DescriptionsItem>

我们在描述组件中应该始终传递 readonly: true,这样渲染出来虽然也是一个文本,但是经过了 ProDate 组件的日期处理,这样就可以很方便的直接展示了,而不用去写一个 render 函数自己去处理


集成到 查询表格中


实际上是和 集成到描述列表中 一样的思路,无非是将 ProDescriptions 组件换成 ProTable 组件,schemas 我们用同一套就可以,伪代码如下


<ProTable
title="详情页"
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

当然我们在 ProTable 内部对 schemas 的处理就要在 customRender 函数中去渲染了,内部实现的伪代码如下


<Table 
:columns="[
{
title:'日期',
dataIndex:'time',
customRender:({record}) =>{
return <ProDate
readonly
value={get(record,'time')}
/>
}
}
]"

/>

ProTableProDescriptions 的处理方式是类似的


集成到 编辑表格中


没啥好说的,实际上是和 集成到表单中 一样的思路,伪代码用法如下


<ProForm>
<ProEditTable
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

</ProForm>

我们还是复用同一套的 schemas,只不过组件换成了 ProEditTable,不同的是,我们在内部就不能写死 readonly 了,因为可能会 全局切换成编辑或者只读某一行切换成编辑或者只读某个单元格切换成编辑或者只读,所以我们这里应该对每一个单元格都需要定义一个 readonly 的响应式属性,方便切换,具体的实现就不细说了,因为偏题了


结语


好了,我们分享了 只读模式 下不同组件下的表现,而不是简单的在 表单中 为了好看而实现的,下期再见 ~


作者:一名爱小惠的前端
来源:juejin.cn/post/7329691357211361318
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


2024-04-03 更新:这段时间在刷牛客,无意间看到了 25 届佬: 收心檬 的个人主页 - 文章 - 掘金 (juejin.cn) 美团暑期实习的面经,绷不住了🤣


pic.png


原面经链接:美团暑期一面_牛客网 (nowcoder.com)


不知道这位面试官是不是看了我的文章出的题,例子举的都大差不差🤣


我确实标题党了想整个活,没想到大厂面试官真出啊,还是实习生,刁难人有一手的🙃...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


作者:討厭吃香菜
来源:juejin.cn/post/7312349904046735400
收起阅读 »

凯文·凯利给我们的 42 个人生建议

五一回到老家,如果以能住一晚为标志,那大概也有十年没有回老家了。 把车停好,就听到熟悉的蛙鸣声,闻着带着些许水气的潮湿的空气。 恍然 仿佛回到了那个老爸老妈还很年轻,我还要骑着自行车,自己做早餐,早早起来上学的年纪。 一切仿佛还在昨天,但一切都已经不一样了。 ...
继续阅读 »

五一回到老家,如果以能住一晚为标志,那大概也有十年没有回老家了。


把车停好,就听到熟悉的蛙鸣声,闻着带着些许水气的潮湿的空气。


恍然


仿佛回到了那个老爸老妈还很年轻,我还要骑着自行车,自己做早餐,早早起来上学的年纪。


一切仿佛还在昨天,但一切都已经不一样了。


四十不惑,不是不疑惑,应该是有些事情不计较,有些东西,想想算了,想想放下了。


前段时间读了凯文·凯利 2023 年的新书《宝贵的人生建议 : 我希望早点知道的智慧》中有提到这本书的的 目标是传递经过时间检验的智慧,但是用我的话表达出来。


这是一本小书,在读完后,我对于其中认同的建议,我也用自己的话,中国传统表述或者之前一些读的书之类的提到的句子尝试理解和表达。大概做了个分类,不是说教,也就是表达一下。


学习和成长


1. 终身学习



毫不犹豫地自我投资——

花钱上课,学习新技能。

这些不起眼的投资,

能产生丰厚的回报。



保持好奇心,读万卷书,行万里路


2. 读史使人明智



大量阅读历史,

你就会明白

过去发生过多少怪事;

这样,对于未来的怪事,

你将见怪不怪。



司马迁在《史记》中说:"究天人之际,通古今之变,成一家之言。"


以铜为鉴,可以正衣冠;以史为鉴,可以知兴替。


《圣经·旧约》中说:太阳底下没有新鲜事


3. 费曼学习法



学习的

最好方法是,

试着把你

会的东西

教给别人。



输出倒逼输入


4. 敏而好学,不耻下问



不要害怕问

听上去愚蠢的问题。

因为在99%的情况下,

其他人都在想

同一个问题,

只是不好意思问出口。



5. 三省吾身



无论在什么年纪,

你都可以问自己:

“为什么我还在做这件事?”

对这个问题,

你需要进行很好的回答。



有点扎心,吾日三省吾身


我为什么还在写文章,为什么还在工作?


6. 开始写作吧



画画能画出你看到了什么。

写作能揭示出你的所思所想。



7. 多读书



要不同凡响,

就需要读书。



曾经一直在简历上写:好读书不求甚解


也是如此践行,量变最终会产生质变。


8. 直面困难



作为一个成熟的人,
衡量你成长的尺度是,
你愿意进行多少令人不舒服的谈话。



近些年越发觉得自己成熟了


不破不立,不塞不流,不止不行。


家庭生活和教育


9. 门当户对



你不是与一个人结婚,

你是与一家人结婚。



婚姻应该在门第相当、家境相似的人家之间进行。这不仅是为了维护身份地位,更是为了确保两个家庭的文化背景和生活方式能够兼容。


选择一个人,就是选择一种生活方式。


10. 善待你的孩子



善待你的孩子,

因为以后是他们为你选择养老院。



树高千丈,叶落归根


积善之家,必有余庆。


善待那个最终决定拔不拔管子的孩子


11. 最好的教育



经常给孩子读书

是他们能受的最好的教育。



12. 回家吃饭



对你的家庭来说,

最好的良药是:

经常在一起吃饭,

不开电视。



今年的一个小目标是一周到少回家吃一次晚饭。但是过了这么久,好像很少。


成功


13. 终局思维



做事要以终为始。

碗碟架堆满后,

再想调整,

就无从下手了。



谋定而后动


凡事预则立,不预则废。


14. 要有备份



制做任何东西,

都要额外多做一些准备,

比如额外的

材料、零件、空间、装饰。

这些额外的东西是

应对错误的保障,

能减轻压力,

防范未来的风险。

这是最便宜的保险。



在程序员界流传着这样一句话:「冗余不做,日子甭过;备份不做,十恶不赦」


15. 坚持



努力,

无论锻炼、陪伴还是工作,

重要的不是数量,

而是坚持。

坚持每天做一点,

比什么都强,

这比你偶尔一为重要得多。



不积跬步,无以至千里;不积小流,无以成江海


成功三要素: 坚持,不要脸,坚持不要脸


16. 长期主义



我们往往高估

一天能完成的事,

而低估十年能取得的成就。

拿出十年来,

你可以成就

不可思议的奇迹。

坚持长期主义,

积小胜为大胜,

即使犯了大错误,

也可以慢慢改正。



长期主义,做时间的朋友


17. 复利



无论财富、
人际关系还是知识,

生活中那些最大的奖赏,

都来自

神奇的复利,

即微小的、稳定的收益不断放大。

要实现富足,

你所需的不过是,

持之以恒地让投入比减损大1%。



做时间的朋友


18. 好事多磨



坏事可能飞速发生,

但几乎所有好事都是慢慢展开的。



厚积薄发


瓜熟蒂落,水到渠成。


欲速则不达,见小利则大事不成。


19. 买卖时间



每个人的时间都是有限的,

每个人的时间都在不断减少。

你能用钱获得的最高杠杆,

就是买别人的时间。

在可能的情况下,

要聘请员工,

外包工作。



《认知红利》中提到时间商人的四种经营模式,第三种:买卖时间:本质是个放大器,通过买入别人的时间,来提升自己的效率、提高时间单价、扩大生产规模。


20. 要做多



有限的游戏,

关乎输赢。

无限的游戏,

则让游戏继续下去。

去玩那些无限的游戏,

因为无限的游戏

能带来无限的回报。



人生如逆旅,我亦是行人


输赢、得失,都只是人生的过眼云烟。


真正重要的,是在这个过程中,我们有没有不断提升自己,有没有始终保持一颗向上的心。


21. 慢可能是快



多任务操作是一个迷思。

走路、跑步、骑自行车或开车时,

不要发信息。

稍停片刻没关系,

没有人会因为这一分钟忘记你。



记得小时候有篇课文是讲时间并行的,一直这样做事情,觉得效率高,这么多年过去了,发现有时候专注的慢也是一种快。保持专注 一次只做一件事,把事情做完


22. 耐得烦



培养对小事的耐心,

你才能对大事保持耐心。



作为一个洗碗十多年的非专业选手,在多年的洗碗过程中慢慢体会了这种耐心。


在《道德经》中,老子曾说:"合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。"


23. 打破常规



成功最可靠的方法,

是你自己定义成功。

先射箭,

然后在射中的地方,

画一个靶心。



孟子说:"舜何人也?予何人也?有为者亦若是。"


有人说:成功者都是创造机会,而不是等机会


24. 但行好事,莫问前程



当你陷入困境或力不能支时,

专注在力所能及的小事上,

这能推进事情的进展。



冯唐说面对逆境时: 看脚下,不断行,莫存顺逆


25. 聚集



在博物馆里,

你需要花至少10分钟,

才能真正地欣赏一件艺术品。

哪怕看5件展品,

每件花10分钟,

也不要看100件展品,每件花30秒。



有舍有得,百鸟在林,不如一鸟在手


工作和生活


26. 迈出舒适区



最好的工作

是一个你不够格的工作,

因为它会迫使你挖掘潜力。

事实上,

要只去应聘那些

你不够格的工作。



挑战自己,迈出舒适区


人往高处走


27. 断舍离



你的时间和空间是有限的。

那些不能再给你

带来快乐的东西,

要移走、送人、扔掉,

给能给你

带来快乐的东西

腾出时间和空间。



28. 知易行难,只是没钱



能轻松用钱解决的问题

不是真正的问题,

因为解决办法显而易见。

把注意力集中在那些

没有显而易见的

解决办法的问题上。



然而现实是大多数人没钱


29. 我选择早到



准时代表着尊重。




没有“准时”这回事。

要么你迟到了,

要么你早到了。

这是你的选择。



30. 你有什么建议吗



如果你寻求别人的反馈,

你会得到批评。

但如果你寻求建议,

你会得到一个搭档。



31. 圣人不器



穿过一个可能禁止你通行的地方,

你要表现得轻松自如,

就像你本属于这里。



别问可不可以,问了就是不可以


32. 以德报怨,何如?



当你原谅别人时,

对方可能没有察觉,

但你会释怀。

宽恕不是为了别人,

宽恕是我们给自己的礼物。



《道德经》中说:"不伐善,不夸能,不矜功,夫唯不争,故天下莫能与之争。"


不要内耗,放过自己


33. 喝喝酒



请客吃饭永远是有效的方法,

而且简单易行。

这对老朋友很有效,

也是结交新朋友的好方法。



酒逢知己千杯少,话不投机半句多。


带团队过程中,喝酒后的大家都是不一样的。


34. 听其言,不如观其行



你是什么样的人,取决于你做什么。

不在于你说什么,

不在于你信什么,给谁投票,

而在于,

你把时间花在什么上面。



躬身入局,贵在实践。


注意力是人最重要的资源


35. 我本善良



每当要在正确和

善良之间做出选择时,

你都要毫无例外地选择善良。

不要把善良和软弱混为一谈。



孔子说:"志士仁人,无求生以害仁,有杀身以成仁。"


36. 分权



分东西时,

一个人分,

另一个先选。



分权的逻辑


君子和而不同,小人同而不和。


37. 坦诚



始终在一开始就提出你想要什么。

这适用于人际关系、商业和生活。



38. 对自己好一点



人生三分之一的时间

是躺在床上睡觉,

几乎另外三分之一,

是在椅子上坐着。

花钱买好床、好椅子,

是物有所值的投资。



还有好的枕头


39. 休息一下



如果你不能确定自己迫切需要什么,

你迫切需要的也许是睡觉。



小时候看聪明的一休,开头都会说,不要着急不要着急,休息休息一会儿


40. 人生得意须尽欢



不要把精美的瓷器和好酒,

非留到难得的场合才拿出来,

这一等可能就是永久;

只要有机会,就可以拿出来。



在耳熟能详的中国诗歌中就有两句非常有名的:



  • 人生得意须尽欢,莫使金樽空对月

  • 花开堪折直须折,莫待无花空折枝


人生百年,如白驹过隙


人到中年,也越发觉得如此,应该让自己开心一些。


41. 事不过三



对每个人,

都要给第二次机会,

但不要给第三次。



这里其实只有二次,与中国传统的事不过三的说法差一次。


42. 遗憾



人生中只有很少的遗憾,

是遗憾自己做了什么。

几乎所有的遗憾都是遗憾自己没有做什么。



最后以左宗棠的对联结束本篇文章,见下图:


发上等愿,结中等缘,享下等福;择高处立,寻平处住,向宽处行.png
发上等愿,结中等缘,享下等福;择高处立,寻平处住,向宽处行


作者:潘锦
来源:juejin.cn/post/7363491538288787494
收起阅读 »

幸福不搞末位淘汰制

来深圳又已两周了,每次初来深圳的时候皮肤都会很难受,不知道是空气质量差还是空气湿度高,浑身都会长一些小疹子和痒痒的包,已经连着几天没有睡好了,既然睡不着那就写点东西,顺便发发牢骚吧。 开始表达 随着年龄的增长,我想,人的表达欲确实是会不断的下降,上一次半夜睡不...
继续阅读 »

来深圳又已两周了,每次初来深圳的时候皮肤都会很难受,不知道是空气质量差还是空气湿度高,浑身都会长一些小疹子和痒痒的包,已经连着几天没有睡好了,既然睡不着那就写点东西,顺便发发牢骚吧。


开始表达


随着年龄的增长,我想,人的表达欲确实是会不断的下降,上一次半夜睡不着写长文还是大一时和几个室友一直聊天到三四点。会不会有一天我对爱的人也不再有表达的欲望,选择三缄其口了呢?我不知道,但是总觉得这是一件很可怕的事情。严格来说我的表达欲也不是单纯的线性下降,中学、尤其是初中时期因为各种各样的原因导致我很自卑,自卑无论对哪个时期的任何人来说都一定是一个很严重的debuff。很幸运的是之后碰到了很多很好很好的人,慢慢的也逐渐走出了泥潭,变得开朗了。


和大部分人一样,大概从高中的时候心智就逐渐趋于成熟了吧,虽然依旧很幼稚,但是那时开始对身边发生的一些事、一些人、社会上的一些热点事件,进行各种各样的分析,得到各种各样经验性的结论,创造出各类只有自己才知道的名词,但是后来发现这些词所代表的含义早就有先贤提出了,虽然没什么意义,但是下意识思考为什么的习惯确实是在那个时候养成的。印象很深的是当时有想到刺猬人的概念,有的人就像刺猬一样,接受不了任何负面的评价,一旦你对他们稍稍辞严令色,他们就会立马竖起全身的刺对你进行攻击,想尽所有办法来驳斥、回击、批判你,绝不会想想自己是否真的存在对应的问题。So,我从高中开始很少对任何人进行任何形式的批评,如果真的有傻逼影响到我的心情的话,那么他不会有第二次影响我心情的机会了。


但是我一直都不爱表达和记录。我也记不清是从什么时间节点开始的,逐渐用一些笔记app习惯性的记录下来每天干了些什么,自己的一些随笔想法,新接触到的一些有用的观点和方法等等,逐渐养成了表达、记录的习惯。

我一直觉得人是环境的产物,这个环境既有时代背景,有当下所处的环境,也有一路走来的经历。有时候感觉自己真的很奇怪,不知道具体是哪部分环境影响到了自己,但是可以确定的是心理上有着不小的问题。比如危机意识过重,总是认为自己的处境不算安全,于是经常处于忙碌的状态,总是想多学点东西、提升提升自己,多做一些能规避风险,拓宽安全边界的事情,常常周末也不会停歇。这种心态一定是有问题的,但是具体怎么纠正回来,我想需要以年为单位的尝试才能成功。


个体乐观、群体悲观


同时,和多数人的个体层面悲观以及国家、社会层面的乐观不同,他们对祖国的未来充满希望,但对个人的前途却一片迷茫,看不到出路。


我刚好是反过来的,对个体乐观,但对社会、制度层面悲观。只和自己相关的事情,我总是能实现或者接近目标,并以积极的角度看待问题。比如高考成绩,几乎只取决于自己,考得不好无非是再来几次;一次面试失败,不过无非是和这家公司没有缘分,多积累积累,依旧有很多机会在前面等着你;减肥失败,时间还长,只要真的想,迟早是能瘦下来的。只要命还在,又有什么困难是能真正将一个人击倒的?


但是如果涉及到人和时间这种影响因子很大的变量,事情又会变得很复杂,我又会趋于悲观。比如一段感情的维系,需要A\B双方的努力和呵护,谁能保证对方一直爱自己呢,谁又能保证以后的自己仍会爱着对方呢?从某种意义上说,几年后甚至几个月之后的你,和现在的你已经不是同一个人了。故而,我一直没有能够和一个人长相厮守、白头偕老,共同度过数十年的自信。

但是在社会制度层面,我认为很多机制、策略是不可能改变的,这些东西就像定理一样深深烙印在现实世界中。国家从某种意义上来说是一个合法的暴力机构,这就导致一旦权利运转出现哪怕一丝一毫的问题,也会产生权利对个人无情倾轧的现象,并且无论科技、时代发展到什么程度,只要有人,就一定会有阶级,就一定会有不公,这不以个人的意志为转移。当代史就是过去的历史,未来史也会是当代史,朱令案、六盘水案、承德程序员案,也只是类似窦娥冤这种封建时代悲剧话本的重演罢了。


又不知道扯到哪里去了,我是想表达什么呢?其实我想说的是每个人都有选择自己生活方式的权利,无论和对方有多么亲密,对他人的生活方式、想法、行为加以指责都是一件很傲慢的事情,子非我,安知我不知鱼之乐?顺便记录一下现在自己的所想。


记录的意义


我大概是从2023年的一月开始发朋友圈,没有细数发了多少条,大概能有个五六十条?


之前不发朋友圈,不作任何记录,也不会拍照,是因为我对自己的记忆力有信心,我觉得我可以用眼睛和脑子记住生活中各种各样的美好,记得走过的路,路上的风景,陪我走在路上的人。但是随着时间的推移,很多本应该珍藏的记忆已经慢慢模糊了,在意识到了这一点之后,记录就已经迫在眉睫了。


发朋友圈的初衷是因为微信里面有很多我觉得很重要的人,我的爱人、家人、挚友、同学,人的精力是有限的,不可能一个个分享;同时,和别人的关系也是有周期的,有很多已经很久不曾联系、但是曾经关系很好的朋友们,他们也同样重要。一条朋友圈,就能让很多我觉得重要的人知道我最近在干什么。


感觉相比于条条框框很多的上学来讲,我还是比较适合上班,虽然压力比在学校大不少,但是离自由和幸福的距离近了不止一点半点。


我可能对物质的要求没有那么高,我不想住很大的房子,三十平不到的出租屋就能让我住的很开心;我也没有想过买豪车,骑骑共享单车或者小毛驴也很舒服,还不用停车费;我也不想穷奢极欲去吃一些很豪华的大餐之类的,自己做的饭菜我吃着就很满意。甚至对于钱我也没有那么看重,我只是把钱当成一个掌握自由的工具和底气,如果真的给我很多很多钱,除了留下自己这辈子够花的那部分,其他的会给那些真的很难很难的人。


感觉自己改变最大的还是在幸福能力上的进步,在大二我就意识到了其实我打理好自己的生活、过日子的能力很差,在那时候我的一天基本是在“一天啥也不干,只在床上躺尸”和“早出晚归,一天学习时间超长,抓紧点滴时间”这两种模式中二选一,所以时常嘲笑自己骨子里是根二极管。


但现在已经完全不一样了,我能平衡好工作和生活,事业和感情,闲的时候找点提升自己的事情做,忙的时候也要抽空兜兜风做做饭。


父母身体健康、和爱人的感情稳中向好、工作顺心、三五挚友、有一些爱好。仅仅是这样,对我来说就已经足够了。


这样想想,幸福其实很简单,毕竟幸福又不搞末位淘汰制


作者:安妮的心动录
来源:juejin.cn/post/7350971151131541567
收起阅读 »

检测图片是否cmyk

web
引入 最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。 一开始我以为这个应该存储...
继续阅读 »

引入


最近业务上有要求,要求如果是 Jpeg 格式文件, 前端在上传的时候要求判断一下这个文件是否 CMYK 颜色模式(color mode/ color space)。 这个颜色模式是打印行业需要的。如果不是则禁止上传,并提示用户。


一开始我以为这个应该存储在 exif 文件信息中, 去拿一下就好了, 但是简单测试发现两个问题:



  1. 文件是否携带 exif 信息是不确定的, 即便出自设计师导出文件, 有可能也是不携带颜色模式信息的。

  2. 除此之外, 依靠 exif 信息去判断,严格来说,即便携带,也是不准确的, 因为这个信息是可以被人为修改的。


经过一番研究, 我暂时发现可能有两种方式,去达成目的。 但是这篇文章实际不是以解决问题为导向,而是期望尽可能的深入一丢丢。 如果急于找到解决方案, 直接翻到文章底部查看具体 编码实现 即可。


什么是 CMYK 颜色模式?



了解 Photoshop 颜色模式 (adobe.com)



CMYK 是一种颜色模式,它表示四种颜色通道:青色(Cyan)、品红色(Magenta)、黄色(Yellow)和黑色(Key,通常表示黑色)。这种颜色模式主要用于印刷和彩色印刷工作中。


以下是 CMYK 颜色模式中各颜色通道的简要介绍:



  1. 青色 (Cyan): 表示蓝绿色。在印刷中,它用于调整蓝色和绿色的浓度。

  2. 品红色 (Magenta): 表示品红或洋红色。在印刷中,它用于调整红色和蓝色的浓度。

  3. 黄色 (Yellow): 表示黄色。在印刷中,它用于调整红色和绿色的浓度。

  4. 黑色 (Key): 通常表示黑色。在印刷中,黑色是通过使用黑色油墨单独添加的,以增加图像的深度和对比度。在 CMYK 模式中,K 代表 Key,以避免与蓝色 (B) 冲突。


这四个颜色通道可以叠加在一起以创建各种颜色。通过调整每个通道的浓度,可以实现广泛的颜色表达。CMYK 被广泛用于印刷领域,因为它能够准确地模拟很多颜色,并提供了在印刷过程中需要的色彩控制。


与 RGB(红绿蓝)颜色模式不同,CMYK 是一种适合印刷的颜色模式,因为它更好地反映了油墨混合的方式,并考虑到印刷物质上的光的特性


怎么在web判断一个 jpeg/jpg 文件 颜色模式是否 cmyk ?


简单说一下这两种方法, 实际上是同一种原理, 因为对于一张图片而言, 它除了携带有 exif 文件元信息之外, 还有文件头信息。


既然不能通过 exif 元信息去判断, 那么我们可以通过文件头信息去做判断。


首先,简单测试可以发现, 即便一个 cmyk 图片没有 exif 描述元信息标识这是一个 cmyk 颜色模式的图片, 但是 各种设计类软件都能够标记出来。 以ps为例:


image-20231128163932682.png
但是 exif 信息中是没有的:


image-20231128164033843.png


甚至一些解析库,就连最基本携带的元信息都没读出来:



stackblitz.com/edit/exif-j…



image-20231128164214625.png


为什么设计软件可以标记出这个图片是否是 cmyk 颜色模式?


这个问题, 我在网上翻了很久,确实是找不到相关文章有阐述设计软件的原理。 不过Ai 的回答是这样的, 具备一定的参考性:



有朋友找到了记得踢我一脚,这里提前感谢啦~



image-20231128174834089.png


用 ImageMagic 解析图片文件



什么是 imageMagic ?


ImageMagick 主要由大量的命令行程序组成,而不提供像 Adobe Photoshop、GIMP 这样的图形界面。它还为很多程序语言提供了 API 库。


ImageMagick 的功能包括:



  • 查看、编辑位图文件

  • 进行图像格式转换

  • 图像特效处理

  • 图像合成

  • 图像批处理


ImageMagick 广泛用于图像处理、图形设计、Web 开发等领域。它是许多开源软件项目的重要组成部分,例如 GIMP、Inkscape、Linux 系统中的图像工具等。


ImageMagick 的优势包括:



  • 功能强大,支持多种图像格式和图像处理功能

  • 开放源代码,免费使用

  • 、、可移植性强,支持多种操作系统



@jayce: imageMagic 类似于 ffmpeg, 只不过它专注图像处理




我们可以利用 ImageMagic 的 identify 工具命令 去解析图片以查看一些信息:


image-20231128180008082.png


加上 -verbose 选项可以查看更多详细信息:


$ ./magick identify -verbose ./CMYK.jpg
$ ./magick identify -verbose ./RGB.jpg

image-20231129092244504.png


这些数据是什么? 从哪里解析出来的呢? 这个需要看一下 jpeg 文件的一些标准文件结构


ISO/IEC 10918-1 和 ISO/IEC 10918-5


这两个文件都是 JPEG 的标准文档,只是不同的部分,wiki 上对二者描述大致是 5 是 对 1 的很多细节的展开和补充。是补充规范


JPEG File Interchange Format (JFIF) 和 Exif


JFIF(JPEG File Interchange Format)和 EXIF(Exchangeable image file format)是两种与 JPEG 图像相关的标准,但它们具有不同的目的和功能。


JFIF 是一个图片文件格式标准, 它被发布于 10918-5, 是对 10918-1 的细节补充。



  1. JFIF (JPEG File Interchange Format):

    • 目的: JFIF 是一种用于在不同设备和平台之间交换 JPEG 图像的简单格式。它定义了 JPEG 文件的基本结构,以确保文件在不同系统中的一致性和可互操作性。

    • 特点: JFIF 文件通常包含了基本的图像数据,但不一定包含元数据信息。它主要关注图像的编码和解码,而不太关心图像的其他详细信息。JFIF 文件通常使用 .jpg 或 .jpeg 扩展名。



  2. EXIF (Exchangeable image file format):

    • 目的: EXIF 是一种在数字摄影中广泛使用的标准,用于嵌入图像文件中的元数据信息。这些元数据可以包括拍摄日期、相机型号、曝光时间、光圈值等。EXIF 提供了更丰富的信息,有助于记录和存储与拍摄有关的详细数据。

    • 特点: EXIF 数据以二进制格式嵌入在 JPEG 图像中,提供了关于图像和拍摄条件的详细信息。这对于数字相机和其他支持 EXIF 的设备非常有用。EXIF 文件通常使用 .jpg 或 .jpeg 扩展名。




JPEG 文件标准结构语法


jpeg 作为压缩数据结构, 是一个非常复杂的数据组织, 我们的关注点只在关系到我们想要解决的问题。 标准文档 ISO/IEC 10918-1 : 1993(E).中有部分相关说明。


概要:


结构上来说, jpeg 的数据格式由以下几个部分,有序组成: parameters, markers, 以及 entropy-coded data segments, 其中 parameters 和 markers 部分通常被组织到 marker segments, 因为它们都是用字节对齐的代码表, 都是由8位字节的有序序列组成。


Parameters


这部分携带有参数编码关键信息, 是图片成功被解析的关键。


Markers


Markers 标记用于标识压缩数据格式的各种结构部分。大多数标记开始包含一组相关参数的标记段;有些标记是单独存在的。所有标记都被分配了两个字节的代码


例如 SOI : 从 0xFF,0xD8这两个字节开始,标记为图片文件的文件头开始, SOF0: 从 0xFF, 0xD8这两个字节开始,标记了 ”帧“ 的开始,它实际上会携带有图片的一些基本信息, 例如宽高,以及颜色通道等。 这个颜色通道其实也是我们主要需要关注的地方。


下表是完整的标记代码:


image-20231129095415255.png



@refer:


http://www.digicamsoft.com/itu/itu-t81…
http://www.digicamsoft.com/itu/itu-t81…



wiki 上也有相关的帧头部字段说明:


Short nameBytesPayloadNameComments
SOI0xFF, 0xD8noneStart Of Image
SOF00xFF, 0xC0variable sizeStart Of Frame (baseline DCT)Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
SOF20xFF, 0xC2variable sizeStart Of Frame (progressive DCT)Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling (e.g., 4:2:0).
DHT0xFF, 0xC4variable sizeDefine Huffman Table(s)Specifies one or more Huffman tables.
DQT0xFF, 0xDBvariable sizeDefine Quantization Table(s)Specifies one or more quantization tables.
DRI0xFF, 0xDD4 bytesDefine Restart IntervalSpecifies the interval between RSTn markers, in Minimum Coded Units (MCUs). This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
SOS0xFF, 0xDAvariable sizeStart Of ScanBegins a top-to-bottom scan of the image. In baseline DCT JPEG images, there is generally a single scan. Progressive DCT JPEG images usually contain multiple scans. This marker specifies which slice of data it will contain, and is immediately followed by entropy-coded data.
RSTn0xFF, 0xDn (n=0..7)noneRestartInserted every r macroblocks, where r is the restart interval set by a DRI marker. Not used if there was no DRI marker. The low three bits of the marker code cycle in value from 0 to 7.
APPn0xFF, 0xEnvariable sizeApplication-specificFor example, an Exif JPEG file uses an APP1 marker to store metadata, laid out in a structure based closely on TIFF.
COM0xFF, 0xFEvariable sizeCommentContains a text comment.
EOI0xFF, 0xD9noneEnd Of Image


Syntax and structure



整体结构


image-20231129111546427.png



@refer: http://www.digicamsoft.com/itu/itu-t81…



Frame Header


image-20231129111645470.png


image-20231129112439294.png


image-20231129112306831.png



@refer: http://www.digicamsoft.com/itu/itu-t81…



SOFn : 帧开始标记标记帧参数的开始。下标n标识编码过程是基线顺序、扩展顺序、渐进还是无损,以及使用哪种熵编码过程。


在其标准文档中,我们有找到 SOFn 的子字段说明,不过在其他地方,倒是看到了不少描述:


特别是在这里 JPEG File Layout and Format


image-20231129141121729.png


可以看到,在 SOFn 这个标记中, 有一个字段为会指明 components 的数量,它代表的实际上颜色通道, 如果是 1,那么就是灰度图, 如果是3,那就是RGB, 如果是 4 就是 CMYK.


到这里我们就知道了, 我们可以读取到这个对应的字节段,从而判断一个图片的颜色模式了。


怎么读取呢?


这篇资料说了明了 Jpeg 文件格式中字节和上述字段的关联关系: Anatomy of a JPEG


注意这篇资料中有一段描述,会影响到我们后续的逻辑判断:


image-20231129142053598.png



就是 SOF0 是必须的,但是可以被 SOFn>=1 替换。 所以在做逻辑判断的时候,后续的也要判断。



我们可以先大概看看一个图片文件的字节流数据长什么样子:(因为所有的字段都是 FF 字节位开头,所以高亮了)


1701248911601.png



以上页面可以在这里访问: jaycethanks.github.io/demos/DemoP…



但样太不便于阅读了, 而且实在太长了。 这里有个网站 here,可以将关键的字节段截取出来:


image-20231129171534152.png


我们主要看这里:


image-20231129171621345.png
可以看到 components 为 4.


如果是 RGB:


image-20231129171722071.png


这里就是 3,


如果是灰度图,components 就会是1


image-20231129172500605.png


EXIF 在哪里?


一个额外的小问题, 我们常见的 exif 元信息存储在哪里呢?


其实上面的 Markers 部分给出的表格中也说明了 ,在 Appn 中可以找到 exif 信息, 但是wiki 上说的是 App0, 在这个解析网站中,我们可以看到:


image-20231201113938640.png


编码实现


有了上述具体的分析, 我们就能有大致思路, 这里直接给出相关代码:



代码参考 github.com/zengming00/node-jpg-is-cmyk




/**
*
@refer https://github.com/zengming00/node-jpg-is-cmyk/blob/master/src/index.ts

*
@refer https://cyber.meme.tips/jpdump/#
*
@refer https://mykb.cipindanci.com/archive/SuperKB/1294/JPEG%20File%20Layout%20and%20Format.htm
*
@refer https://www.ccoderun.ca/programming/2017-01-31_jpeg/
*
* 通过 jpg 文件头判断是否是 CMYK 颜色模式
*
@param { Uint8Array } data
*/

function checkCmyk(data: Uint8Array) {
let pos = 0;
while (pos < data.length) {
pos++;
switch (data[pos]) {
case 0xd8: {// SOI - Start of Image
pos++;
break;
}
case 0xd9: {// EOI - End of Image
pos++;
break;
}
case 0xc0: // SOF0 - Start of Frame, Baseline DCT
case 0xc1: // SOF1 - Start of Frame, Extended Sequential DCT
case 0xc2: { // SOF2 - Start of Frame, Progressive DCT
pos++;
const len = (data[pos] << 8) | data[pos + 1];
const compoNum = data[pos + 7];
if (compoNum === 4) {
// 如果components 数量为4, 那么就认为是 cmyk
return true;
}
pos += len;
break;
}
case 0xc4: { // DHT - Define Huffman Table
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xda: { // SOS - Start of Scan
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xdb: { // DQT - Define Quantization Table
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xdd: { // DRI - Define Restart Interval
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xe0: { // APP0 - Application-specific marker
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
case 0xfe: { // COM - Comment
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
break;
}
default: {
pos++;
const len = (data[pos] << 8) | data[pos + 1];
pos += len;
}
}
}
return false;
}

有没有其他的方法?


既然 imageMagic 这么成熟且强大, 我们有办法利用它来做判断吗?


我们可以通过 wasm, 在web中去利用这些工具, 我找到了 WASM-ImageMagick 这个, 但是他的打包好像有些问题 vite 引入的时候会报错,看着好像也没有要修复的意思, issue 里面有老哥自己修改了打包配置进行了修复在这里: image-magick


我们就写的demo测试函数:


import * as Magick from '@xn-sakina/image-magick'

export default function (file: File) {
if (!file) return;
// 创建FileReader对象
var reader = new FileReader();
// 当读取完成时的回调函数
reader.onload = async function (e) {
// 获取ArrayBuffer
var arrayBuffer = e.target?.result as ArrayBuffer;
if (arrayBuffer) {
// 将 ArrayBuffer 转换为 Uint8Array
const sourceBytes = new Uint8Array(arrayBuffer);
const inputFiles = [{ name: 'srcFile.png', content: sourceBytes }]
let commands: string[] = ["identify srcFile.png"]
const { stdout } = await Magick.execute({inputFiles, commands});

// 这里打印一下结果
console.log('stdout:',stdout[0])

}
};
// 读取文件为ArrayBuffer
reader.readAsArrayBuffer(file);
}

import isCmyk from '../utils/isCmyk.ts'
const handleFileChange = (e: Event) => {
const file = (e.target as HTMLInputElement)?.files?.[0]
isCmyk(file) // 这里文件上传调用一下
......

测试几个文件


image-20231130104941592.png


可以看到, Gray, RGB, CMYK 检测都可以正常输出, 说明可以这么干。



但是这个库, 文档写的太乱了。 - -



这个库的大小有 5 m之大 - -, npm 上找了下, 目前相关的包,也没有比这个更小的好像。


作者:sun_zy
来源:jaycethanks.github.io/blog_11ty/posts/Others/%E6%A3%80%E6%B5%8B%E5%9B%BE%E7%89%87%E6%98%AF%E5%90%A6cmyk/
收起阅读 »

我是如何使用Flow+Retrofit封装网络请求的

各位好,本人是练习kt时长一年的准练习生,接下来我将用一种我认为还行的方式封装网络请求,如有雷点,请各位佬轻喷 首先,定义一个请求结果类 sealed class RequestResult<out T> { data object INI...
继续阅读 »

各位好,本人是练习kt时长一年的准练习生,接下来我将用一种我认为还行的方式封装网络请求,如有雷点,请各位佬轻喷
首先,定义一个请求结果类


sealed class RequestResult<out T> {
data object INIT : RequestResult<Nothing>()
data object LOADING : RequestResult<Nothing>()
data class Success<out T>(val data: T) : RequestResult<T>()
data class Error(val errorCode: Int = -1, val errorMsg: String? = "") : RequestResult<Nothing>()
}

接下来,定义Retrofit的service,由于我个人的极简主义,特别讨厌复制粘贴,所以我做了一个非常大胆的决定


interface SimpleService {
//目前我们只关注这两方法
@GET
suspend fun commonGet(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiResponse<Any>
//目前我们只关注这两方法
@POST
suspend fun commonPost(@Url url: String, @Body param: HashMap<String, Any>): ApiResponse<Any>

@GET
suspend fun commonGetList(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiListData<Any>

@POST
suspend fun commonPostList(@Url url: String, @Body param: HashMap<String, Any>): ApiListData<Any>

@GET
suspend fun commonGetPageList(@Url url: String, @QueryMap param: HashMap<String, Any>): ApiPageData<Any>

@POST
suspend fun commonPostPageList(@Url url: String, @Body param: HashMap<String, Any>): ApiPageData<Any>
}

and在apiManager中生成这个service


object BaseApiManager {
val simpleService by lazy<SimpleService> {
getService()
}

接下来我定义了一个RequestParam类来帮助收敛请求需要的参数


@Keep
data class RequestParam<T>(
val clazz: Class<T>? = null,
val url: String,
val isGet: Boolean = true,
val paramBuilder: (HashMap<String, Any>.() -> Unit)? = null
){
val param: HashMap<String, Any>
get() {
val value = hashMapOf<String, Any>()
paramBuilder?.invoke(value)
return value
}
}

再然后便是请求真正发出的地方


internal fun <T> commonRequest(
param: RequestParam<T>,
builder: ((T) -> Unit)? = null
)
= flow {
emit(RequestResult.LOADING)
Timber.d(param.param.toString())
runCatching {
if (param.isGet) {
BaseApiManager.simpleService.commonGet(param.url, param.param)
} else {
BaseApiManager.simpleService.commonPost(param.url, param.param)
}
}.onSuccess {
if (it.code != StatusCode.REQUEST_SUCCESS) {
emit(RequestResult.Error(it.code, it.message))
} else {
val gson = Gson()
val data = gson.fromJson(gson.toJson(it.data), param.clazz)
builder?.invoke(data)
emit(RequestResult.Success(data))
}
}.onFailure {
emit(RequestResult.Error(StatusCode.REQUEST_FAILED, it.message))
}
}.flowOn(Dispatchers.IO)

在经过上述封装后,此时我在vm中发出一个网络请求就变成


viewModelScope.launch {
commonRequest(
RequestParam(
XXXBean::class.java, //数据类class
"/xxx/xxx/xxx", //地址
false //是否get
) {
put("xxx", 11)
put("xxxx", "25")
}
).collect {
when(it) {
RequestResult.INIT -> {
假设这边是Init弹窗
}
RequestResult.LOADING -> {
关闭Init弹窗
假设这边是Loading弹窗
}
is RequestResult.Error -> {
关闭Loading弹窗
toast(it.errorMsg)
}

is RequestResult.Success -> {
关闭Loading弹窗
发送成功事件或者改变UI状态
}
}
}

那么这边会遇到一个有点烦人的事情,实际上


RequestResult.INIT -> {
假设这边是Init弹窗
}
RequestResult.LOADING -> {
关闭Init弹窗
假设这边是Loading弹窗
}
is RequestResult.Error -> {
关闭Loading弹窗
toast(it.errorMsg)
}

这三兄弟中,我们经常会做一些重复的操作,于是我略施小计,将这几个行为定义成CommonEffect


sealed class MVICommonEffect {
data object ShowLoading: MVICommonEffect()
data object DismissLoading: MVICommonEffect()
data class ShowToast(val msg: String?): MVICommonEffect()
}

同时将Flow<RequestResult>的订阅步骤拆开,由于kt中两个隐式this对象写起来很繁琐,所以我是把这一串代码放到baseiewModel中的


fun <T> Flow<RequestResult<T>>.onInit(initBlock: suspend () -> Unit): Flow<RequestResult<T>> {
return onEach {
if (it == RequestResult.INIT) {
initBlock.invoke()
}
}
}

fun <T> Flow<RequestResult<T>>.onLoading(
showLoading: Boolean = true,
loadingBlock: suspend () -> Unit
)
: Flow<RequestResult<T>> {
return onEach {
if (it == RequestResult.LOADING) {
if (showLoading) {
emitLoadingEffect()
}
loadingBlock.invoke()
}
}
}

fun <T> Flow<RequestResult<T>>.onSuccess(
dismissLoading: Boolean = true,
successBlock: suspend ((data: T) -> Unit)
)
: Flow<RequestResult<T>> {
return onEach {
if (it is RequestResult.Success) {
if (dismissLoading) {
emitDismissLoadingEffect()
}
successBlock.invoke(it.data)
}
}
}

fun <T> Flow<RequestResult<T>>.onError(
dismissLoading: Boolean = true,
showToast: Boolean = true,
errorBlock: suspend (code: Int, msg: String?) -> Unit
)
: Flow<RequestResult<T>> {
return onEach {
if (it is RequestResult.Error) {
if (dismissLoading) {
emitDismissLoadingEffect()
}
if (showToast) {
emitToastEffect(it.errorMsg)
}
errorBlock.invoke(it.errorCode, it.errorMsg)
}
}
}

fun <T> Flow<RequestResult<T>>.onCommonSuccess(
loadingInvoke: Boolean,
showToast: Boolean,
successBlock: suspend ((data: T) -> Unit)
)
= this.onInit().onLoading(loadingInvoke)
.onError(
dismissLoading = loadingInvoke,
showToast = showToast
).onSuccess(
dismissLoading = loadingInvoke
) {
successBlock.invoke(it)
}

private val _commonEffect = MutableSharedFlow<MVICommonEffect>()
override val commonEffect: SharedFlow<MVICommonEffect> by lazy {
_commonEffect.asSharedFlow()
}

override suspend fun emitLoadingEffect() {
_commonEffect.emit(MVICommonEffect.ShowLoading)
}

override suspend fun emitDismissLoadingEffect() {
_commonEffect.emit(MVICommonEffect.DismissLoading)
}

override suspend fun emitToastEffect(msg: String?) {
_commonEffect.emit(MVICommonEffect.ShowToast(msg))
}

那么接下来,vm中网络请求就可以用一种很赏心悦目的方式出现了


private fun requestTestData(): Flow<RequestResult<XXXBean>> {
return commonRequest(
RequestParam(
XXXBean::class.java,
"xxx"
)
)
}

private fun updateTestData() {
requestData().onInit().onLoading().onError().onSuccess {
Timber.d(it.toString)
}.launchIn(viewModelScope)
}

接下来,只需要在基类View中订阅上述的MVICommonEffect,就可以handle大部分情况下的loading,toast.


由于本人能力有限,不足之处还望大佬指正.


作者:伟大的小炮队长
来源:juejin.cn/post/7368758932154843188
收起阅读 »

GPT-4o,遥遥领先,作为前端人的一些思考

大家好,我是LV。 我早上一般起的比较早~ 大概6点左右就起来刷各种AI资讯。 但是今天,5点左右就起来了,迫不及待想看 OpenAI 发布的内容~ 也顺便写篇文章跟大家分享一下最新的资讯~ 以及作为前端人的一些思考~ 希望对你有所帮助~ 欢迎加入最懂AI的...
继续阅读 »

大家好,我是LV。


我早上一般起的比较早~ 大概6点左右就起来刷各种AI资讯。


但是今天,5点左右就起来了,迫不及待想看 OpenAI 发布的内容~



也顺便写篇文章跟大家分享一下最新的资讯~


以及作为前端人的一些思考~


希望对你有所帮助~


欢迎加入最懂AI的前端伙伴们~群,一起探讨AI赋能前端研发。


GPT-4o



  • 结合文本、图像、视频、语音的全能模型

  • 可以通过语音交互以及具备识别物体和基于视觉信息进行快速回答的功能

  • 性能上,GPT-4o达到了GPT-4 Turbo水平

  • 成本相比GPT-4-turbo砍一半,速度快一倍,响应时间最低232毫秒,平均320毫秒。遥遥领先!

  • 将为 macOS 操作系统设计桌面ChatGPT应用程序,无缝集成到 macOs 中,可以使用键盘快捷键查询问题并与 ChatGPT 进行截图讨论或直接开展声音/视频对话。


以上详见:openai.com/index/hello…


前端人的思考


成本砍半,速度加倍


做应用层的前端er,可以换新的 API Model 了,虽然价格没有 3.5 那么便宜,也算是GPT4自由了(我也赶紧给LV0给换上)。


音视频支持



  • 通过视频连线ChatGPT,实时辅助修bug


之前只能够通过将bug转换为文字或者图片再给到AI,有了音视频功能,直接可以连线 ChatGPT,让GPT实时给你debug。



  • 通过视频连线ChatGPT,辅助编码,相当于请了一个24在线的编程导师~

  • 通过视频的形式给AI一些UI交互上的信息,从截图生代码 ==> 原型交互生代码(离AGI Code又近了一步)


跟macOS的结合


在vscode、在网页、在控制台、在Codding的任何地方,有问题,就会有答案。(作为mac粉,着实期待了~)


其他思考


作为AI应用研发的创业者角色,有几点思考~


OpenAI的这一波更新带来了新的机遇:


例如在教育领域、情感陪伴服务以及同声传译服务:



  • 语音增加了情绪理解和有感情的回复,老人或者残疾人士陪伴

  • 手机能够实时解析摄像头捕获的视频画面,并提供指导,这种能力有潜力取代家庭教师的角色

  • 同时进行翻译(即同传)的工作可以由此技术执行,从而有可能替代专业的同声传译人员


不过,这波更新也破灭了多少创业者正在做的事情~ 比如:


智能眼镜,给视疾人士提供出行便捷(我前几天还看到有人在花大力气自研这项技术,现在升级一下模型或许就能很低门槛接入了~)


Sam Altman 很早在斯坦福大学举办的一个演讲中预示:GPT-5和GPT-6将极大超越GPT-4,警示创业者考虑AI未来发展,创业不要要专注于解决当前AI的局限性问题。


简单来说:别做跟官方做技术竞争,比如:花大量时间通过各种布丁来拓展AI的上下文能力,降低迷惑性。


至于要做啥,从稳健的角度来看,不要轻易涉足一个未知的领域,建议基于熟悉的业务场景聚焦来做AI赋能。把现有你熟悉的业务场景梳理出来,尝试用AI结合进去,AI赋能现有的业务流程,让现有的业务跑起来效能更高或者门槛更低。


比如:我很熟悉前端研发领域,那我会深度聚焦AI赋能前端研发,拆解研发中的各个环节步骤,不断尝试AI赋能各个步骤,提升现有的研发效能,降低研发门槛,再把这些经验抽象产品化。


聚焦细分业务,保持敏锐度,将最新的AI技术快速结合到业务中去。


作者:LV技术派
来源:juejin.cn/post/7368421137917788198
收起阅读 »

在线人数统计功能怎么实现?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方式:使用Redis的有序集合(zset)实现。
核心方法是这四个:zaddzrangeByScorezremrangeByScorezrem


二、实现步骤


1. 如何认定用户是否在线?


认定用户在线的条件一般跟网站有关,如果网站需要登录才能进入,那么这种网站就是根据用户的token令牌有效性判断是否在线;
如果网站是公开的,是那种不需要登录就可以浏览的,那么这种网站一般就需要自定一个规则来识别用户,也有很多方式实现如IPdeviceId浏览器指纹,推荐使用浏览器指纹的方式实现。


浏览器指纹可能包括以下信息的组合:用户代理字符串 (User-Agent string)、HTTP请求头信息、屏幕分辨率和颜色深度、时区和语言设置、浏览器插件详情等。现成的JavaScript库,像 FingerprintJSClientJS,可以帮助简化这个过程,因为它们已经实现了收集上述信息并生成唯一标识的算法。


使用起来也很简单,如下:


// 安装:npm install @fingerprintjs/fingerprintjs

// 使用示例:
import FingerprintJS from '@fingerprintjs/fingerprintjs';

// 初始化指纹JS Library
FingerprintJS.load().then(fp => {
// 获取访客ID
fp.get().then(result => {
const visitorId = result.visitorId;
console.log(visitorId);
});
});


这样就可以获取一个访问公开网站的用户的唯一ID了,当用户访问网站的时候,将这个ID放到访问链接的Cookie或者header中传到后台,后端服务根据这个ID标示用户。


2. zadd命令添加在线用户


(1)zadd命令介绍
zadd命令有三个参数



key:有序集合的名称。
score1、score2 等:分数值,可以是整数值或双精度浮点数。
member1、member2 等:要添加到有序集合的成员。
例子:向名为 myzset 的有序集合中添加一个成员:ZADD myzset 1 "one"



(2)添加在线用户标识到有序集合中


// expireTime给用户令牌设置了一个过期时间
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeout);
String expireTimeStr = DateUtil.formatFullTime(expireTime);
// 添加用户token到有序集合中
redisService.zadd("user.active", Double.parseDouble(expireTimeStr), userToken);


由于一个用户可能户会重复登录,这就导致userToken也会重复,但为了不重复计算这个用户的访问次数,zadd命令的第二个参数很好的解决了这个问题。
我这里的逻辑是:每次添加一个在线用户时,利用当前时间加上过期时间计算出一个分数,可以有效保证当前用户只会存在一个最新的登录态。



3. zrangeByScore命令查询在线人数


(1)zrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:查询分数在 1 到 3之间的所有成员:ZRANGEBYSCORE myzset 1 3



(2)查询当前所有的在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 查询当前日期到"+inf"之间所有的用户
Set userOnlineStringSet = redisService.zrangeByScore("user.active", now, "+inf");


利用zrangeByScore方法可以查询这个有序集合指定范围内的用户,这个userOnlineStringSet也就是在线用户集,它的size就是在线人数了。



4. zremrangeByScore命令定时清除在线用户


(1)zremrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:删除分数在 1 到 3之间的所有成员:ZREMRANGEBYSCORE myzset 1 3



(2)定时清除在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 清除当前日期到"-inf"之间所有的用户
redisService.zremrangeByScore(""user.active"","-inf", now);


由于有序集合不会自动清理下线的用户,所以这里我们需要写一个定时任务去定时删除下线的用户。



5. zrem命令用户退出登录时删除成员


(1)zrem命令介绍



key:指定的有序集合的名字。
members:需要删除的成员
例子:删除名为xxx的成员:ZREM myzset "xxx"



(2)定时清除在线用户


// 删除名为xxx的成员
redisService.zrem("user.active", "xxx");


删除 zset中的记录,确保主动退出的用户下线。



三、小结一下


这种方案的核心逻辑就是,创建一个在线用户身份集合为key,利用用户身份为member,利用过期时间为score,然后对这个集合进行增删改查,实现起来还是比较巧妙和简单的,大家有兴趣可以试试看。


作者:summo
来源:juejin.cn/post/7356065093060427816
收起阅读 »

汉文帝刘恒:权谋高手,带你看中式管理

前言 这里我打算先讲两个例子,让大家感受一下中式管理以及里面的运作规律。日常生活中,我们接触的都是表象,也就是最外层的具象,而里面的结构以及组成大部分人是没有太多去深入理解的。 汉文帝刘恒 谈到这个大家都会想起汉朝的文景之治,采用无为而治,少插手百姓生活,通...
继续阅读 »

efb27f6fa8439656d5ba22473411d951.jpeg


前言




这里我打算先讲两个例子,让大家感受一下中式管理以及里面的运作规律。日常生活中,我们接触的都是表象,也就是最外层的具象,而里面的结构以及组成大部分人是没有太多去深入理解的。


汉文帝刘恒


谈到这个大家都会想起汉朝的文景之治,采用无为而治,少插手百姓生活,通过这种市场经济的方式恢复民生。然而大部分人不了解的是他还是一个权谋高手,这还得从刘邦说起。


刘邦的势力是由功臣集团,比如说英布、彭越、韩信..,以及吕氏集团,吕后、樊哙、吕产..,刘氏集团,也就是刘邦的家族体系构成,刘邦称帝之后扶持吕氏势力来清洗异姓诸侯,当他发现吕氏力量非常强大的时候,就扶持戚夫人来平衡吕氏,没想到刘邦已经比较年迈了,无法像汉武帝扶持卫青、霍去病一样来壮大自己的力量,所以当吕后掌权之后对戚夫人干掉,扶持自家的吕氏上来,这个时候功臣集团跟他们有冲突,这里面的陈平、周勃以及刘氏集团依据刘邦最后留下的白马之盟,联手干掉吕氏,这时需要另立领导者。


因为这些功臣集团目的是为了稳固权力,又不能背锅,所以需要找一个势力比较弱小的,好拿捏的上来话事,所以刘恒上场了。


那么他做的几件事:


1、通过旧部收回御林军的权限,去清理异己,功臣集团不想背锅,只能乖乖的交出军权


2、安抚平反的人,论功行赏


3、分化内部,提了陈平,压了周勃


我们可以看到刘恒是一个权谋高手,本来是作为一个被控制人的角色出场的,通过几个关键动作最后稳住了自己的脚跟。


汉代丞相陈平


陈平是刘邦那会儿跟着他的,六处谋划为刘邦出了很大力,比如说真假汉王,为刘邦脱险;离间范增,使得项羽失去一个重要的谋士。在汉文帝的时候就被任命为丞相,他就问陈平你知道丞相是干嘛的吗?


陈平:“丞相向上是调理天子的气息,向下管理百官,对外监视诸侯,对内管好百姓”,看懂了吗,这就是位置决定职责,反观周勃他是军事人才,所以在此次之后自己辞职了。


《年会不能停》领导要领


当大鹏的来历被揭穿之后,有个领导跟他支招,想要当领导也不能就记住几点:第一不要明确自己的意思,第二会用感情牌,第三懂得分化。


上面的三个例子,大家是否对中式管理有了一个初步的认识呢?下面我将讲讲我对中式管理的认识。


power


定义




权力跟资本是同一个代名词,就是资源分配权,因为资源有限,那么就需要对它进行一个合理的分配。而权力跟资本也有不同。


权力是需要大量的铺垫,比如说乡里老人组,有很高的威望,它需要前期大量的文化铺垫的,它是一个长期有效的方式。


资本是需要权衡利弊的方式去谈判,因为资本是趋利的,跟人性一样,所以前期沟通成本很大,但是一旦达成很快执行很顺利。


构成




以古代皇权为例,下面有文官、外戚、太监,一直讲的集中中心力量,不是说皇权特别强,而是它需要支持者来巩固位置,所以一般是结合刚刚三方中的某一方来强化。


比如说刘邦,一开始借助吕氏势力干掉诸侯,然后扶持自己的外戚势力戚夫人,最后还定了白马之盟,给了功臣集团和刘氏集团正当的理由来处理过于强大的吕氏。


在公司里面也一样,ceo下面肯定需要掌握自己的核心部门的权限,来支持他执行自己的目标,并不是说这个公司是他的他想怎么弄就怎么弄,因为里面还有很多带资入组的大佬。


动作




中心力量 + 支持一方势力 => 打压其他势力


具体方式:洗牌、分化,在《年会不能停》这个电影里面讲到了公司效益不行,要进行裁员广进计划,人力部的权限就很高,从而干掉其他势力的人。至于分化,在前言的刘恒那里用到了,某个体系内部也不是说大家都是一致的,一旦有利益冲突,或者分配不均就会分化。


从上面的构成很容易理解这个动作的产生意义,这就是我们常说的内斗,我们平时很难理解为什么大家不干点正事,天天在那里斗哈哈,这就要谈到归属。


归属




之前写过一篇文章介绍过,就是管理权限是有归属的,如果说这个公司非常大,但是不是你的,跟你没有一点关系对不对。


这就是上面讲的内斗存在的意义,争取资源、机会,最终实现权力的扩大。你说资源重要吧,其实并不是,而是这种资源分配权更重要。


中式管理




上层管理


我们从陈平对宰相的理解可以知道,就是管理队伍、定好方向、管理资源(收益、风险),他还漏了一个权力斗争,这个肯定不能直说哈哈哈。


我们再回头看看《年会不能停》领导要领


1、不要明确自己的意思


这就很传统了,为什么规矩都是模棱两可的呢,如果规矩说的很清楚,还需要你干什么?这是第一点,第二点他可以再次被解读,而不是明确拍板,这样有锅也是下面干活的人出错。


2、分化,转移矛盾


这个典型的手段,当大家干的天昏地暗的时候,那么你的位置就很稳固,大家不会把矛盾指向你头上。


所以这一部分的管理核心技能是管理好团队,文化建设(权力形式、资本形式),管理好目标,管理好资源。


中下层管理


我认为这些是核心的业务主力,也就是攻城略地的大头兵。这一层他是上一层的弱化,应该更加偏向业务那块,比如说跨部门资源调用,团队工作计划制定,合理利用资源。


比如说韩信的十面埋伏,在打大战的时候你对团队的了解有多少,你对整个战场了解有多少,你的计划是怎样的。


管理的认知


1、看定位


每个等级它的要求不一样的,底层的大头兵更多是做事的技能、态度,因为要攻城略地,对于中下层管理,对局部的战况要有自己的把控。


2、职责


基于上面的定位,我们可以得出这个定位下面的职责。


3、管理:权力、资源、目标、文化(情绪)


这里涉及的知识面太多了,就不再展开了。


总结




有时我们看不懂为啥公司内部一直内斗,还有业务干的一团糟,看了它的定义、归属就会有一个大致的认知,正是因为我们对内部的构造没有比较深入的了解,以底层的大头兵角度就会觉得这是内耗的情况。


这种管理很大程度跟文化有关系,也就是几千年来演变的规律形成的习惯在影响我们现代管理模式,它很简单,就是管理资源、方向,它也很复杂,单纯一个方面拎出来都是一个很大的知识面。


作者:大鸡腿同学
来源:juejin.cn/post/7329100659877494796
收起阅读 »

刘邦-中年痞子到霸道总裁的一生

前言 最近花了9块钱开通会员,就为了读下《汉高祖刘邦》这本书,一直以来我认为社会阅历是人跟人之间的差距,这种属于后天的积累,当你在社会实践的时候过于单一,或者说接触面更少的时候,应该读读别人的传记。 刘邦有几个比较有意思的点,首先他有天选之子的面相、骨相,当...
继续阅读 »

前言




最近花了9块钱开通会员,就为了读下《汉高祖刘邦》这本书,一直以来我认为社会阅历是人跟人之间的差距,这种属于后天的积累,当你在社会实践的时候过于单一,或者说接触面更少的时候,应该读读别人的传记。


刘邦有几个比较有意思的点,首先他有天选之子的面相、骨相,当然我认识这些真正目的有几个,一个是人需要借助名头、声望来发展自身实力一样道理,另一个是权力正统性,能够说服别人,这个相当重要。其次我觉得很奇怪,他的能力可以跟他的岗位匹配上,从一个庭长到一个君主,这里面要求的能力是不一样的,他为啥能具备这方面的能力的转变呢?


下面我们就一一展开刘邦的一生历程,以及我读后的感受。


各个派系对比




背景


在秦朝末期,因为律法严苛,导致民愤,各个势力崛起,非常经典的说法;其中就有刘邦、项羽、还有很出名的陈胜吴广。


ps:当你看秦朝发家的时候你就会清楚,它是如何变成一个战争机器的,工作的细化,把种田的锁死在种田上,打战的打战,定下军功授于的规则。就像《共产宣言》里面讲的,当工作细分化之后,人会更加专业化,效率更高,同时工作量更大,更加劳累。当秦统一六国之后,推行同样的机制,而且推行郡县制,势必遭到各个传统门阀的抵抗的,也不一定适用其他地方。


你觉得的律法严苛,实际上是人家的发家史,只不过无法推广,以及郡县制的影响太大。


派系发展历程


1、陈胜吴广


相比另外两股力量,显得不太起眼,因为结束的比较早,我觉得跟资源有关系,刘邦自己一开始就是亭长,吕氏家底,项羽门阀势力,反观这个陈吴资源是利益临时凑在一起,另外能力上军事、管理都不突出。


2、刘邦


他是在40岁之后才开始走大运,他之前更像一个痞子的作风,为人豪爽,喜欢喝酒结交朋友,然后做了亭长,认识樊哙、曹参、夏侯婴这些铁子,跟当地的大户吕氏有结交,期间认识萧何,后来在沛县反叛,在发展过程中认识张良,通过张良结识其他人,比如项伯,也就是鸿门宴上解围的那哥们;本身刘邦势力比较薄弱,需要借助多方势力,比如敌方将领,项羽部下策反了英布,韩信原本也是项羽帐前持戟郎中,另外借助彭越对抗项羽;最后在韩信十面埋伏,还有张良的四面楚歌下将项羽击败。


这是一统之前的历程,后面开始削韩信,平异姓王,白马之盟,完成了从一个痞子到霸道总裁的转变。


军事:


家底是吕氏 + 后面加入带资异姓王


权谋:


a、朋友多多,敌人少少


这对于只会武力的人来讲是降维打击,用了敌方的英布、韩信,助力了彭越其他势力,从而发展自己的势力。


b、驭将


舍得利益,比如封韩信齐王,后面多次封赏,当一统之后对各个出力的人给予套现机会。


c、权力纵横术


一统之后,韩信成为刘邦心中的刺,使用了狡猾的手段去除了韩信的兵权,然后软禁,平定各个异姓王,最后因为吕氏权力太大了,定下了白马之盟。


这个是陈胜吴广所无法具备的能力,以及项羽,对比起来项羽更像莽夫。


扩展一下:首先权力的正统性是很重要的,另外维持权力的力量必不可少,最后是权力纵横术。因为本身资源就是有限的,一定会内斗,其次只有在互相制衡的基础上,可以保证这种头头的稳固。


3、项羽


对比其他势力,他有很多优势,是一个门阀家族,家族里面有项伯、项梁,门下还有很多人追随,比如说英布、季布等悍将,韩信也是在下面干过一段时间,再加上项羽本身天生神力,所向披靡。打出了非常有名的巨鹿之战,破釜沉舟,大破秦军,因为跟刘邦的约定先进城先为王,听取范增意见设置了鸿门宴,后面刘邦封为汉王,同时也为压制刘邦势力让他去汉中地方,派出以前投降的秦降将去守,所以后来被韩信的暗渡陈仓偷袭了,还有后面背水一战,围攻刘邦于荥阳,差点把刘邦嘎了,但是后面的事上面也有提到被韩信、刘邦等围功,中了计谋,被反间了范增,还有十面埋伏这些,最终失败了。


军事:


项羽的家族很雄厚的,而且跟随的人才大有人在


权谋:


a、大力出奇迹


本身项羽的条件、资源确实比较叼,有点偏科感觉哈哈


b、不会管理,妇人之仁


首先他不注重谋士,比如韩信经常给他提意见,没有给下属发展空间。然后对于鸿门宴的时候,没有对刘邦下手,没有霸道总裁的雷厉风行的手段。


说白了,不是很好的管理者,从英布、韩信的出走看出,因为人家在你下面没有施展空间,其次你没有一个奖励机制来推进大家为你卖力。


idea




很重要一点,我们能从这些历史故事中学到点什么?



  • 做事


1、人脉、资源


我们看刘邦以前虽然说是个痞子,但是人家人脉真的广,一个是亭长的位置,认识一群铁子,结识了当地的大户吕氏,如果他后面没有壮大其实在沛县也是一个小霸王,做什么事都方便。


这就是打工人不具备的东西,圈子小,能力还不强。


2、德要配位


作为一个管理者,你是否具备业务、人才规划能力,以及利益合理分配,还有激励体系建设。就像韩信点兵,每个人都能来个2w人马,点个10几个副将,东南西北布阵,你的能力跟位置匹配的。


3、博弈能力


这个是非常难的,以前我们会陷入非黑即白,就是不是朋友就是敌人,我个人也很难逃脱这个认知。但是纵观优秀的战略家,可以权衡利害,这就是很难的。


比如说刘邦就很听劝,可能人家很生气,但是只要你足够说服力,他可以听你的。他可以接受敌方的英雄,可以接受跟匈奴联姻,他可以把他儿子踹下来躲避楚军追击,理智的逻辑胜于情绪。


如果你回头再看冯唐老师讲的,不要脸,不着急,不害怕,一个痞子更容易做出点成就,“前途光明,道路曲折”,每个有成就的人都会经历各种曲折,下面的乐观精神也是一种不害怕的表现。



  • 乐观的态度


他一直很乐观,从发家没有项羽叼,到多次差点被嘎,被项羽胖揍,刘邦挺乐观的。


这个非常重要,纵观现代年轻人,他们会觉得当前环境对他们比较难发展,就是没有资源、没有比较强的赚钱能力,所以采取收缩以求得自在生存,也会产生悲观的心态,这个本身是人性、本能的选择。但是当你有乐观精神,才有面对困难的勇气,也有了捕捉机会的欲望。



  • 陈胜吴广的失败


我们可以比喻成如何办理一场活动,首先需要人才对吧,布置活动现场,策划,干活,相比之下他们就是乌合之众,没有得力干将,没有杰出的管理人才,然后办活动需要资金对吧,有支持的粮食、金钱,他们并不代表某一方的势力可以稳定的输入资金来源,当你有了资金之后,队伍庞大之后,就有派系斗争,领导者有没有对应的权力纵横术、权衡利害能力。


作者:大鸡腿同学
来源:juejin.cn/post/7322723692745031690
收起阅读 »

需求小能手——拦截浏览器窗口关闭

web
前言 最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。 窗口关闭 要想实现该功能最简单的想...
继续阅读 »

前言


最近碰到一个需求,网页端页面有评价功能,要求用户点击关闭浏览器时强制弹出评价对话框让用户评价。刚听到这个需求我大意了没有闪,以为很简单,没想到很难实现,很多需求果然不能想当然啊,接下来我们来看一下该功能实现的一些思路。


窗口关闭


要想实现该功能最简单的想法就是监听浏览器关闭事件,然后阻止默认事件,执行自定义的事件。整个思路核心就是监听事件,搜索一番果然有浏览器关闭触发的事件。


事件



  • onunload:资源被卸载时触发,浏览器窗口关闭时卸载资源就会触发,我们监听一下该事件看能不能阻止窗口关闭。


    window.addEventListener('unload', function (e) {
console.log(e);
e.preventDefault()
});

打开页面再关闭,会发现控制台打印出了e然后就关闭了,看来在onunload事件中并不能阻止窗口关闭,得另找方法,刚好在onunload事件介绍中还链接了一个事件——beforeonunlaod。



  • beforeunload :当窗口关闭或刷新时触发,该事件在onunload之前触发。并且在该事件中可以弹出对话框,询问用户是否确认离开或者重新加载,这不是正是我们想要的效果。根据mdn上的介绍,要想出现弹出对话看需要用preventDefault()事件,并且为了兼容性我们最好再加上以下方法中的一个:

    1.将e.renturenValue赋一个字符串。

    2.事件函数返回一个字符串。
    接下来让我们试一试:


    window.addEventListener('beforeunload', function (e) {
e.preventDefault()
e.returnValue = ''
});

打开关闭未生效,再检查下代码没问题呀,这是因为浏览器本身安全机制导致的,在ie浏览器中没有任何限制,但是在chrome、edge等浏览器中用户必须在短时间操作过页面才能触发。打开页面点几个文字在关闭窗口,这次就能出现弹窗了。

2(W_WV8AVWRT3(4R1HWBRR7.png

当我们点击离开页面就会关闭,点击取消继续停留,上面提到过刷线也能触发,我们再点下刷新。

T456)ZI7MJ2XK1X3M%BE7SN.png

出现的提示有所改变,我们知道浏览器的刷新有好几种方式,我们可以都尝试一下:



  • ctrl+R:本身就是浏览器刷新按钮的快捷键,能够触发。

  • f5:能否触发。

  • 前进、后退:能够触发。

    这三种方式提示内容跟点击刷新按钮一样。回到我们的需求,虽然已经能够阻止窗口关闭,但是刷新依旧能阻止,我们需求是用户关闭,所以我们要区分用户操作是刷新还是关闭。


区分


要想区分就要找到以下两者之间的区别,两者都会执行onbeforeunload与onunload两个事件,不能直接通过某个事件区分。但是两个事件之间的时间差是不同的。刷新时两者时间差在10毫秒左右,而关闭时在3毫秒左右,判断以下时间差就能区分出来。


       var time = null;
window.addEventListener('beforeunload', function (e) {
time = new Date().getTime();
});
window.addEventListener('unload', function (e) {
const nowTime = new Date().getTime();
if (nowTime - time < 5) {
console.log('窗口关闭');
}
});

用此方法就能区分出来,但是此判断是在onunload事件中的,而窗口弹出是在beforeunlaod,这方法只适用于在关闭时执行某个函数,但不能满足我们的需求。除此之外还有一个问题就是刷新默认弹出对话框的内容是不能修改的,所以如果我们想要弹出自定义的对话框是不可能的。经过分析操作能够做到的就是,在用户刷新或关闭时出现系统自带对话框,同时在下方弹出自定义对话框,然后用户点击取消再去操作自定义对话框。


总结


总的来说要想拦截浏览器窗口关闭并且弹出自定义对话框,目前我还没有完美的实现方案,只能带有众多缺陷的去实现。如果我们只是想在关闭窗口前执行函数那就使用时间差区分即可。


作者:躺平使者
来源:juejin.cn/post/7281912738862481448
收起阅读 »

工作两年以来,被磨圆滑了,心智有所成长……

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。 一路走来,磕磕绊绊,几年来,我总结了工作上的思考…… 工作思考 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争...
继续阅读 »

刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。


一路走来,磕磕绊绊,几年来,我总结了工作上的思考……


工作思考



  1. 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。只有和气相处,我们才能推动工作的进展。

  2. 在讨论具体问题之前,先进行一些预备性的交流。情绪应放在第一位,工作讨论放在第二位。如果对方情绪不好,最好选择另一个时间再进行讨论。

  3. 在与他人交流时要保持初学者的态度和需求,不要用技术去怼人。

  4. 进入新团队先提升自己在团队的业务能力,对整个系统有足够的了解,不要怕问问题和学习。不要新入职就想毁天灭地,指手画脚 ”这里的设计不合理,那里有性能瓶颈“。

  5. 在各个事情上,都要比别人多了解一点。对于关键的事情要精通,对于其他事情也要多花一点时间去投入。

  6. 遇到困难时,先自己思考和尝试解决,然后再请教他人。不要机械地提问,也不要埋头一直搞而不主动提问。但如果是新入职,可以例外,多提问总没有坏处,但要在思考的基础上提问。

  7. 当向他人求助时,首先要清晰地阐述自己正在面临的问题、目标、已尝试的方法以及所需要的帮助和紧迫程度。所有的方面都要有所涉及。在提问之前,最好加上一句是否可以帮忙,这样对解决问题是否有帮助更加明确。因为别

  8. 一定有时间来帮助你,即使有时间,你也不一定找对了人。

  9. 在明确软件产品要解决的业务问题之前,先了解自己负责的那部分与业务的对应关系。

  10. 主要核心问题一定要提前叙述清楚,不要等别人问

  11. 要始终坚持追踪事情的进展,与与自己有交互的队友讨论接口,并关注他们的进度,以确保协调一致。

  12. 要主动向队友述说自己的困难,在项目延期或遇到困难时,要主动求助同事或领导,是否能分配部分工作给其他人,不要全部自己承担。

  13. 如果预计任务需要延期,要提前告知领导。如果有进展,也要及时向领导汇报。

  14. 如果无法参加会议但是自己是会议的重要参与者,一定要提前告知领导自己的进度、计划和想法,最好以书面形式或电话告知。如果可以远程参加,可以选择电话参加。除非有极其重要的事情,务必参加会议。不要假设别人都知道你的进度和想法。

  15. 要少说话,多做事。在开会时,不要凭借想当然的想法,可以询问其他小组的细节,但不要妄自揣测别人的细节,以为自己是对的。否则会被批评。

  16. 程序员如果经验丰富,很容易产生自我感觉良好的情绪。要避免这种情况,我们必须使用自己没有使用过的东西,并进行充分的测试,这样才能减少问题的出现。要提前考虑好所有细节,不要认为没有问题就不加考虑。要给自己留出处理问题的时间,并及时反馈并寻求帮助。

  17. 当与他人交流时,要始终保持有始有终的态度,特别是当寻求他人帮助时,最后一定要确认OK。要胆大心细,不要害怕犯错,要有成果,要快速并提高效率,不择手段地追求快速,并对结果负责。工作一定要完成闭环,要记事情要好,记住重要的事情并使用备忘录记录待办事项。

  18. 每完成一个项目后,应该回顾一下使用了什么知识、技能和工具。要总结并记录下这些,并与之前积累的知识和技能进行关联。如果发生了错误,也要记录下来,并将经验进行总结。

  19. 每天早上先思考今天要做什么,列出1、2、3,然后每天晚上下班时回顾已完成的任务、未完成的任务以及遇到的问题。

  20. 如果有待办事项没有立即处理,一定要用工具记录下来,不要心存侥幸以为自己能记住。


代码编写和技术问题



  1. 在代码编写过程中要认真对待,对于代码审核之前,要自己好好检查,给人一种可靠的感觉。

  2. 对于代码审核,不要过于苛刻,要容忍个人的发挥。

  3. 在提交代码给测试之前,应该先自行进行测试验证通过。

  4. 如果接口没有做到幂等性,那就会给未来的人工运维增加困难。当数据存在多份副本时,例如容量信息和上下游同时存在的资源,需要评估数据不一致的可能性以及解决方法。可以考虑通过数据校准或严格的代码编写来保证最终的一致性,或者考虑只在一方保存数据或以一方的数据为准。一旦出现数据不一致,则以其中一方的数据为准,无需人为干预即可自动达到数据再次一致。

  5. 要学会横向和纵向分割隔离系统,明确系统的边界,这样可以更好地进行并发合作开发和运维,提高效率。各个子系统应该独立变化,新的设计要考虑向后兼容性和上下游兼容性问题,包括上线期间的新老版本兼容。在设计评审阶段就应该重视这些问题。

  6. 如果在代码审查中无法发现业务问题或代码风格问题,不妨重点关注日志的打印是否合理和是否存在bug。

  7. 在依赖某个服务或与其他服务共享时,要确认该服务是否要废弃、是否是系统的瓶颈,以及是否可以自己进行改造或寻找更优的提供者。

  8. 使用缓存时注意预热,以防止开始使用时大量的缓存未命中导致数据库负载过高。

  9. 在使用rpc和mq、共享数据库、轮询、进程间通信和服务间通信时,要根据情况做出选择,并注意不要产生依赖倒置。

  10. 在接口有任何变动时,务必通过书面和口头确认。在这方面,要多沟通,尽量详细,以避免出现严重问题!毕竟,软件系统非常复杂,上下游之间的理解难以保持一致。

  11. 尽可能使用批量接口,并考虑是否需要完全批量查询。当批量接口性能较差时,设置适当的最大数量,并考虑客户端支持将批量接口聚合查询。批量接口往往是tp99最高的接口。

  12. 对于系统重要设计和功能,要考虑降级预案,并加入一些开关来满足安全性和性能需求。

  13. 如果数据不一致,可以考虑对比两方的不一致数据并打印错误日志,例如es/db等。

  14. 在系统设计之前,要充分调研其他人的设计,了解背景和现状。

  15. 废弃的代码应立即删除,如果以后需要,可以从git中找回。如果实在不想删除,也要注释掉!特别是对外的rpc、http接口,不使用的要立即删除,保持代码简洁。接手项目的人不熟悉背景情况,很难判断这段废弃代码的意义,容易造成混乱和浪费时间。要努力将其和其他有效代码联系起来,但这很困难。

  16. 在代码中要有详尽的日志记录!但是必须有条理和规范,只打印关键部分。对于执行的定时任务,应该打印足够详细的统计结果。最好使用简洁明了的日志,只记录最少量但最详细的信息,反馈程序的执行路径。

  17. 如果接口调用失败或超时,应该如何处理?幂等和重试如何处理?


当你写下一行代码前



  1. 要明确这行代码可能出现的异常情况以及如何处理,是将异常隔离、忽略还是单独处理,以防遗漏某些异常。

  2. 需要确保该行代码的输入是否已进行校验,并考虑校验可能引发的异常。

  3. 需要思考由谁调用该代码,会涉及哪些上游调用,并确定向调用者提供什么样的预期结果。

  4. 需要确定是否调用了一个方法或接口,以及该调用是否会阻塞或是异步的,并考虑对性能的影响。

  5. 需要评估该行代码是否可以进行优化,是否可以复用。

  6. 如果该行代码是控制语句,考虑是否能简化控制流程是否扁平。

  7. 对于日志打印或与主要逻辑无关的输出或报警,是否需要多加关注,因为它们可能还是很重要的。

  8. 如果代码是set等方法,也要仔细检查,避免赋错属性。IDE可能会有误提示,因为属性名前缀类似,set方法容易赋值错误。


当你设计一个接口时



  1. 接口的语义应该足够明确,避免出现过于综合的上帝接口

  2. 如果语义不明确,需要明确上下游的期望和需求。有些需求可以选择不提供给上游调用。

  3. 对于接口超时的处理,可以考虑重试和幂等性。在创建和删除接口时要确定是否具有幂等性,同时,幂等后返回的数据是否和首次请求一致也需要考虑。

  4. 接口是否需要防止并发,以及是否成为性能瓶颈也需要考虑。

  5. 设计接口时要确保调用方能够完全理解,如果他对接口的理解有问题,就需要重新设计接口。这一点非常关键,可以通过邮件确认或者面对面交流来确保调用方理解得清楚。

  6. 在开发过程中,需要定期关注队友的开发进度,了解他们是否已经使用了接口以及是否遇到了问题。这个原则适用于所有的上下游和相关方,包括产品和测试人员。要想清楚如何对接口进行测试,并与测试人员明确交流。

  7. 最好自己整理好测试用例,不要盲目地指望测试人员能发现所有的bug。

  8. 需要考虑是否需要批量处理这个接口,以减少rpc请求的次数。但即使是批量处理,也要注意一次批处理最多处理多少条记录,不要一次性处理全部记录,避免由于网络阻塞或批量处理时间过长导致上游调用超时,需要适度控制批量处理的规模。


作者:五阳
来源:juejin.cn/post/7306025036656787475
收起阅读 »

前端实现文件预览img、docx、xlsx、ppt、pdf、md、txt、audio、video

web
前言 最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇 具体的预览需求: 预览需要支持的文件类型有: png、jpg、jpeg...
继续阅读 »

前言



最近有接到一个需求,要求前端支持上传制定后缀文件,且支持页面预览,上传简单,那么预览该怎么实现呢,尤其是不同类型的文件预览方案,那么下面就我这个需求的实现,分不同情况来讲解一下👇



具体的预览需求:
预览需要支持的文件类型有: png、jpg、jpeg、docx、xlsx、ppt、pdf、md、txt、audio、video,另外对于不同文档还需要有定位的功能。例如:pdf 定位到页码,txtmarkdown定位到文字并滚动到指定的位置,音视频定位到具体的时间等等。




⚠️ 补充: 我的需求是需要先将文件上传到后台,然后我拿到url地址去展示,对于markdowntxt的文件需要先用fetch获取,其他的展示则直接使用url链接就可以。


不同文件的实现方式不同,下面分类讲解,总共分为以下几类:



  1. 自有标签文件:png、jpg、jpeg、audio、video

  2. 纯文字的文件: markdown & txt

  3. office 类型的文件: docx、xlsx、ppt

  4. embed 引入文件:pdf

  5. iframe:引入外部完整的网站




自有标签文件:png、jpg、jpeg、audio、video



对于图片、音视频的预览,直接使用对应的标签即可,如下:



图片:png、jpg、jpeg


示例代码:


 <img src={url} key={docId} alt={name} width="100%" />;

预览效果如下:


截屏2024-04-30 11.18.01.png


音频:audio


示例代码:


<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>

预览效果如下:


截屏2024-04-30 11.18.45.png


视频:video


示例代码:


<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>

预览效果如下:


截屏2024-05-13 18.21.13.png


关于音视频的定位的完整代码:


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

interface IProps {
type: 'audio' | 'video';
url: string;
timeInSeconds: number;
}

function AudioAndVideo(props: IProps) {
const { type, url, timeInSeconds } = props;
const videoRef = useRef<HTMLVideoElement>(null);
const audioRef = useRef<HTMLAudioElement>(null);

useEffect(() => {
// 音视频定位
const secondsTime = timeInSeconds / 1000;
if (type === 'audio' && audioRef.current) {
audioRef.current.currentTime = secondsTime;
}
if (type === 'video' && videoRef.current) {
videoRef.current.currentTime = secondsTime;
}
}, [type, timeInSeconds]);

return (
<div>
{type === 'audio' ? (
<audio ref={audioRef} controls controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="audio/mpeg" />
</audio>
) : (
<video ref={videoRef} controls muted controlsList="nodownload" style={{ width: '100%' }}>
<track kind="captions" />
<source src={url} type="video/mp4" />
</video>
)}
</div>
);
}

export default AudioAndVideo;



纯文字的文件: markdown & txt



对于markdown、txt类型的文件,如果拿到的是文件的url的话,则无法直接显示,需要请求到内容,再进行展示。



markdown 文件



在展示markdown文件时,需要满足字体高亮、代码高亮、如果有字体高亮,需要滚动到字体所在位置、如果有外部链接,需要新开tab页面再打开。



需要引入两个库:


marked:它的作用是将markdown文本转换(解析)为HTML


highlight: 它允许开发者在网页上高亮显示代码。


字体高亮的代码实现:



高亮的样式,可以在行间样式定义



  const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};

代码高亮的代码实现:



需要借助hljs这个库进行转换



marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
}
},
});

链接跳转新tab页的代码实现:


marked.use({
renderer: {
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});

滚动到高亮的位置的代码实现:



需要配合上面的代码高亮的方法



const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

完整的代码如下:



入参的docUrlmarkdown文件的线上url地址,searchText 是需要高亮的内容。



import React, { useEffect, useState, useRef } from 'react';
import { marked } from 'marked';
import hljs from 'highlight.js';

const preStyle = {
width: '100%',
maxHeight: '64vh',
minHeight: '64vh',
overflow: 'auto',
};

// Markdown展示组件
function MarkdownViewer({ docUrl, searchText }: { docUrl: string; searchText: string }) {
const [markdown, setMarkdown] = useState('');
const markdownRef = useRef<HTMLDivElement | null>(null);

const highlightAndMarkFirst = (text: string, highlightText: string) => {
let firstMatchDone = false;
const regex = new RegExp(`(${highlightText})`, 'gi');
return text.replace(regex, (match) => {
if (!firstMatchDone) {
firstMatchDone = true;
return `<span id='first-match' style="color: red;">${match}</span>`;
}
return `<span style="color: red;">${match}</span>`;
});
};

useEffect(() => {
// 如果没有搜索内容,直接加载原始Markdown文本
fetch(docUrl)
.then((response) => response.text())
.then((text) => {
const highlightedText = searchText ? highlightAndMarkFirst(text, searchText) : text;
setMarkdown(highlightedText);
})
.catch((error) => console.error('加载Markdown文件失败:', error));
}, [searchText, docUrl]);

useEffect(() => {
if (markdownRef.current) {
// 支持代码高亮
marked.use({
renderer: {
code(code, infostring) {
const validLang = !!(infostring && hljs.getLanguage(infostring));
const highlighted = validLang
? hljs.highlight(code, { language: infostring, ignoreIllegals: true }).value
: code;
return `<pre><code class="hljs ${infostring}">${highlighted}</code></pre>`;
},
// 链接跳转
link(href, title, text) {
const isExternal = !href.startsWith('/') && !href.startsWith('#');
if (isExternal) {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer">${text}</a>`;
}
return `<a href="${href}" title="${title}">${text}</a>`;
},
},
});
const htmlContent = marked.parse(markdown);
markdownRef.current!.innerHTML = htmlContent as string;
// 当markdown更新后,检查是否需要滚动到高亮位置
const firstMatchElement = document.getElementById('first-match');
if (firstMatchElement) {
firstMatchElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [markdown]);

return (
<div style={preStyle}>
<div ref={markdownRef} />
</div>

);
}

export default MarkdownViewer;

预览效果如下:


截屏2024-05-13 17.59.04.png


txt 文件预览展示



支持高亮和滚动到指定位置



支持高亮的代码:


  function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}

完整代码:


import React, { useEffect, useState, useRef } from 'react';
import { preStyle } from './config';

function TextFileViewer({ docurl, searchText }: { docurl: string; searchText: string }) {
const [paragraphs, setParagraphs] = useState<string[]>([]);
const targetRef = useRef<HTMLDivElement | null>(null);

function highlightText(text: string) {
if (!searchText.trim()) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, `<span style="color: red">$1</span>`);
}

useEffect(() => {
fetch(docurl)
.then((response) => response.text())
.then((text) => {
const highlightedText = highlightText(text);
const paras = highlightedText
.split('\n')
.map((para) => para.trim())
.filter((para) => para);
setParagraphs(paras);
})
.catch((error) => {
console.error('加载文本文件出错:', error);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docurl, searchText]);

useEffect(() => {
// 处理高亮段落的滚动逻辑
const timer = setTimeout(() => {
if (targetRef.current) {
targetRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);

return () => clearTimeout(timer);
}, [paragraphs]);

return (
<div style={preStyle}>
{paragraphs.map((para: string, index: number) => {
const paraKey = para + index;

// 确定这个段落是否包含高亮文本
const isTarget = para.includes(`>${searchText}<`);
return (
<p key={paraKey} ref={isTarget && !targetRef.current ? targetRef : null}>
<div dangerouslySetInnerHTML={{ __html: para }} />
</p>
);
})}
</div>

);
}

export default TextFileViewer;

预览效果如下:


截屏2024-05-13 18.34.27.png




office 类型的文件: docx、xlsx、ppt



docx、xlsx、ppt 文件的预览,用的是office的线上预览链接 + 我们文件的线上url即可。




关于定位:用这种方法我暂时尝试是无法定位页码的,所以定位的功能我采取的是后端将office 文件转成pdf,再进行定位,如果只是纯展示,忽略这个问题即可。



示例代码:


<iframe
src={`https://view.officeapps.live.com/op/view.aspx?src=${url}`}
width="100%"
height="500px"
frameBorder="0"
></iframe>

预览效果如下:


截屏2024-05-07 17.58.45.png




embed 引入文件:pdf



pdf文档预览时,可以采用embed的方式,这个httpsUrl就是你的pdf文档的链接地址



示例代码:


 <embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;

关于定位,其实是地址上拼接的页码sourcePage,如下:


 const httpsUrl = sourcePage
? `${doc.url}#page=${sourcePage}`
: doc.url;

<embed src={`${httpsUrl}`} style={preStyle} key={`${httpsUrl}`} />;


预览效果如下:


截屏2024-05-07 17.50.07.png




iframe:引入外部完整的网站



除了上面的各种文件,我们还需要预览一些外部的网址,那就要用到iframe的方式



示例代码:


 <iframe
title="网址"
width="100%"
height="100%"
src={doc.url}
allow="microphone;camera;midi;encrypted-media;"/>


预览效果如下:


截屏2024-05-07 17.51.26.png




总结: 到这里我们支持的所有文件都讲述完了,有什么问题,欢迎评论区留言!


作者:玖月晴空
来源:juejin.cn/post/7366432628440924170
收起阅读 »

人走茶凉?勾心斗角?职场无友谊?

你和同事之间存在竞争关系 要不要把工作关系维护成伙伴关系 明枪暗箭防不胜防 背后捅刀子往往最不设防 大家是否在职场上交友是有也遇到过以上困扰呢? 不要在职场上交“朋友”,而是要寻找“盟友”。 这两者的区别在于应对策略: 我们会愿意为“朋友”牺牲自己的利益,像是...
继续阅读 »

你和同事之间存在竞争关系


要不要把工作关系维护成伙伴关系


明枪暗箭防不胜防


背后捅刀子往往最不设防


大家是否在职场上交友是有也遇到过以上困扰呢?


不要在职场上交“朋友”,而是要寻找“盟友”。


这两者的区别在于应对策略:


我们会愿意为“朋友”牺牲自己的利益,像是一张年卡。


而结交“盟友”就是为了一起争取更多利益,《孔乙己》说得好:“这次是现钱,酒要好。”


所以,在职场上的“受欢迎”和社交场、朋友圈上的“受欢迎”之间有着本质的区别:


你和你的同事未必真心喜欢彼此,但在日常相处当中能够客气、友善地交往。


大家需要寻找盟友时会第一个想到你,在争斗冲突时会尽量绕开你,这就是一种非常理想的“受欢迎”状态。 不要在职场上寻求友谊和爱,这件事是不对的。


在这里给大家列出一个在职场上受欢迎的清单。


1.实力在及格线以上


这是一切的前提。职场新人要“先活下来,再做兄弟”,稳住了工作能力这个基本面,才有资格和同事谈交情。


实力不够的人会拖累整个团队、增加所有人的工作量,大家恨都来不及,绝对不会和他称兄道弟。


实力强可以表现为实力本身,在初级职位上,也可以表现为潜力。


极少数特别强大的人可能从一开始就能很好地完成工作,但是大部分人在新加入一个团队时都需要经过一段时间的磨合,在这个过程中有欠缺和不足都是正常的,你所表现出来的敬业精神、学习能力和进步的速度才是大家对你进行评价的关键。


刚入职的新人,对于要做的事情完全没有概念,但是为人极勤奋又上进,给他布置的任务会完成得特别扎实,每一天都在飞快地进步。这样的人在职场上永远都能收获一大把来自他人的橄榄枝。


2.比较高的自尊水平


高自尊的人对自己评价高,要求也高,又能够带着欣赏的眼光去看周围的人,他们不光是很好的父母、伴侣和朋友,同时也是职场上最好的结盟对象。


高自尊的人往往拥有很多优秀的品质,同时他们也能够理解“大局”,和他们合作不用在鸡毛蒜皮的细节上纠缠推诿,可以把精力全部用来开疆拓土,极大地降低团队的内耗。


如果你是一个高自尊的人,在日常生活中表现出了自律和很好的品行,就会收获高自尊同类的赞赏。有些低自尊的人可能会认为你的言行是在“装X”,别犹豫,把他们从你的结交名单当中划掉,高自尊会帮你筛掉一批最糟糕的潜在合作者。


如果你是一个部门的领导者,记得要维护高自尊的下属,他们都是潜在的优秀带队者,给他们一个位子就可以坐上来自己动,给他们一点精神鼓励和支持,他们就会变得无所不能。


即使高自尊的手下可能某些地方让你感到嫉妒或者冒犯(这是常见的,嫉妒是每个人都一定会有的情感),也绝对不要默许或者纵容低自尊的妄人跑去伤害他们,否则会伤了大家的心,事业就难以成功了。


“朕可以敲打丞相,但你算什么东西”就是对这种低自尊妄人最好的态度。


3.嘴严,可靠


在任何一个群体当中,多嘴多舌的人都不会受到尊重,而在职场上,嘴不严尤其危险。


如果你是一个爱说是非的人,围绕在你周围的只会是一帮同样没正事、低级趣味的家伙。你会被打上“不可靠”的标记,愿意和你交流的人越来越少,大家等着看你什么时候因为多嘴闯祸,而强者根本不会和你为伍。


有些同学曾经给我留言说,自己很内向,不知道如何跟同事拉近关系。内向的人最适合强调自己的“嘴严”和“可靠”,在职场上,这两项品质远比“能说会道”更让人喜欢。


4.随和,有分寸


体面的人不传闲话,也不会轻易对旁人发表议论。


“思想可以特立独行,生活方式最好随大流”,这是对自己的要求,而他人的生活方式是不是合理,不是我们能评价的。


哪怕是最亲近的人,都未必能知晓对方的全部经历和心里藏着的每一件小事。在职场上大家保持着客气有礼的距离,就更不可能了解每个人做事的出发点和逻辑,“看不懂”是正常的,但是完全没有必要“看不惯”。如果还要大发议论,把自己的“看不惯”到处传播,你的伙伴就只会越来越少。


有人说在北上广深这样的大城市,人和人之间距离遥远,缺人情味,太冷漠。


这不是冷漠,而是对“和自己不一样”的宽容,这份宽容就是我们在向文明社会靠拢的标志。


5.懂得如何打扮


还记得斯大林的故事吗?在他离开校园之后,从头到脚都经过精心设计,不是为了精神好看,而是要让自己看起来就像一位投身革命事业的进步青年。


有句老话叫做“先敬罗衣后敬人”,本意是讽刺那些根据衣饰打扮来评价一个人的现象。我们自己在做判断的时候要尽量避免受到这类偏见的影响,但是对他人可能存在的偏见一定要心中有数。人是视觉动物,穿着打扮是“人设(人物设定)”的一部分,在我们开口说话之前,衣饰鞋袜就已经传达了无数信息。


想要成为职场当中受欢迎的人,穿着打扮的风格就要和公司的调性保持一致,最安全的做法是向你的同事靠拢。


在一个风格统一的群体当中,“与众不同”这件事自带攻击性。如果在事业单位之类的上年纪同事比较多的地方上班,马卡龙色的衣服和颜色夸张的口红,最好等到下班时间再上身。


这不是压抑天性,而是自我保护和职业精神。


6.和优秀的人站在一起


在职场上,优秀的人品质都是相似的:勤奋,自律,不断精进。如果发现了这样的同事,就要尽量和他们保持良好关系。


但是,单纯的日常沟通并不足以让你们成为盟友,正式结盟往往是通过利益交换和分享:当你遇到棘手的工作任务,就可以主动邀请对方共同跟进,同时将一部分利益让出去。愉快的合作是关系飞跃的最好契机。


优秀的人能认可的,通常也都是自己的同类。如果你能获得他们的称许和背书,在同事当中的地位自然会有所提升。


7.知道如何求助


前两天有一位关系户同学留言说,自己即将去实习,因为家人的关系可以得到一些行业资深专家的指点,问自己应该如何表现,是不是不懂就要问,像“好奇宝宝”一样,对方就会觉得自己好学上进。


我告诉她说,不要上去就问,有任何疑惑都先用搜索引擎找一下答案,如果找不出来,再带着你搜到的细节去询问那些资深前辈。


互联网时代有个很大的变化,就是人们获取信息的成本大大降低。善用搜索引擎寻找答案,就能更快、更精准、更全面地找到自己想要的东西,这种方式比跑到对方工位边用嘴问效率高得多。


凡事都问,只会让人觉得你的文字阅读能力有限,同时既不把自己的时间当回事,也不尊重别人的时间。尤其对方还是行业中的专家,他们的时间一定比实习生的宝贵多了。如果网上找不到答案,再带着细节去仔细咨询,这样的请教才是高效的,才能证明你是一个“好学上进”的人。


职场不是校园,不会再有一群老师专门负责手把手地教你,不轻易占用其他同事的时间会让你成为一个自立、有分寸、受尊重的人。毕业之后,你取得进步的速度、最终的上升空间,都和使用搜索引擎寻找答案的能力呈正相关。


8.技巧地送出小恩小惠


小恩小惠带两个“小”字,并不意味着这是一种微末小技。事实上,即使是最普通的零食,只要讲究得法,都可以送到人心里。


你的同事当中有没有因为宗教信仰而忌口的情况?


甲和乙爱吃辣,丙和丁爱吃甜,是不是两种口味都来上一点?


要留心同事的自我暴露,最好是用一个小本本记下来,关键时刻可能派上大用场。大家都是成年人,不会像孩子一样轻易被小恩小惠打动,打动我们的往往是“你把我放在心上”的温暖。


9.良好的情绪管理能力


很多时候这是个隐藏特征,但是自带“一票否决”属性:平时表现得沉着稳重,周围同事们不会有特别明显的感觉,然而歇斯底里和失控只要有一次,之前苦心经营的人设就会全面崩塌。情绪不稳定的人一般没人敢惹,但是也没人会在意了:你会被视为一个“病人”,很难再有大的发展。


已经发泄出去的情绪不能收回来,这个时候不要反复陷入纠结和悔恨,待在情绪里不出来,钱花出去了就不要去想,不要去比价。


如果情绪失控了,应该立刻做到的是原谅自己,然后考虑如何不再有下一次失控。要知道大多数人一辈子都至少会换三四次工作,了不起是换个地方,重新再来。


有的人特别幸运,天生长得好看,容易被人喜欢。


如果不是让人眼前一亮的高颜值人士,就不要太心急了。


成为一个自律、行为可以预期的人,也能慢慢地被别人喜欢。


人生很长,被人喜欢这件事,我们不用赶时间。


作者:程序员小高
来源:juejin.cn/post/7255589558996992059
收起阅读 »

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:


/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/


代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


作者:三分恶
来源:juejin.cn/post/7259036373579350077
收起阅读 »

为什么网站要使用HTTPS?

现在HTTPS基本上已经是网站的标配了,很少会遇到单纯使用HTTP的网站。但是十年前这还是另一番景象,当时只有几家大型互联网公司的网站会使用HTTPS,大部分使用的都还是简单的HTTP,这一切是怎么发生的呢?为什么要把网站升级到HTTPS?若干年前,公司开发了...
继续阅读 »

现在HTTPS基本上已经是网站的标配了,很少会遇到单纯使用HTTP的网站。但是十年前这还是另一番景象,当时只有几家大型互联网公司的网站会使用HTTPS,大部分使用的都还是简单的HTTP,这一切是怎么发生的呢?

为什么要把网站升级到HTTPS?

若干年前,公司开发了一款APP,其中的某些页面是用H5实现的,有一天用户向我们反馈,页面中弹出了一个广告窗口,这让当时身为开发小白的我感觉很懵逼,后来经过经验丰富的老程序员点拨,才知道这是被电信运营商劫持了,运营商拦截了服务器对用户的HTTP响应,并在中间夹带了一些私货。

一些网龄比较大的同学可能还有这样的记忆:网站页面找不到的时候,浏览器会跳转到一个运营商或者路由器厂商的网址导航页面;家里的宽带到期的时候,浏览器网页右下角会弹出续费通知。

这都是HTTP响应被劫持的表现,HTTP本身没什么安全机制,HTTP传输的数据很容易被窃取和篡改,这也是我们将网站升级到HTTPS的根本动机。

使用HTTPS有很多好处,这里稍微展开介绍一下:

  • 数据加密:HTTPS通过SSL/TLS协议为数据传输过程提供了加密,即便数据在传输过程中被截获,没有密钥也无法解读数据内容。这就像是特工使用密文发送电报,即使电报内容被别人截获,没有密码表也无法解读其中的内容。
  • 身份验证:使用HTTPS的网站会获得权威认证机构颁发的证书,这就像是一个“身-份-证”,让访问者能够确认自己访问的是官方合法的网站,有效防止钓鱼网站的风险。
  • 数据完整性:因为数据传输的中间人接触不到密钥,不仅不能解密,而且也无法对数据进行加密,这就保证了数据在传输过程中不被篡改、伪造。
  • 增强用户信任:由于浏览器会对HTTPS网站显示锁标志,这有助于增强访问者对网站的信任。就像是看到家门口安装了高级安全锁,人们会自然而然地觉得这家人对安全非常重视,从而更加放心。
  • SEO优势:谷歌等搜索引擎已经明确表示,HTTPS是搜索排名算法的一个信号。这意味着使用HTTPS的网站在搜索结果中可能会获得更高的排名,具备更大的竞争优势。

HTTPS的发展趋势

大约从2010年开始,大型网站和安全专家开始倡导使用HTTPS,也就是在HTTP上加上SSL/TLS协议进行加密。

根据互联网安全研究机构的报告,目前超过80%的网站已经使用HTTPS。特别是那些大型电商平台和社交媒体网站,几乎100%都已经完成了从HTTP到HTTPS的升级。

不仅是企业和网站管理员在推动HTTPS的普及,各国政府和互联网安全组织也在积极推荐使用HTTPS。例如,各种浏览器都会对那些仍然使用HTTP的网站标记为“不安全”。

随着人们对网络安全意识的增强,大家也更加偏好那些使用HTTPS的网站。就像是在选择酒店的时候,你可能会更倾向于选择那些看起来保卫严密的酒店。

HTTPS的技术原理

加密技术

HTTPS 安全通信的核心在于加密技术。这里面主要涉及两种加密方式:对称加密和非对称加密。

  • 对称加密:就像是你和朋友使用同一把钥匙来锁和解一个箱子。信息的发送方和接收方使用同一个密钥进行数据的加密和解密。这种方式的优点是加解密速度快,通信成本低,但缺点在于如果密钥被中间截获或者泄漏,通信就不安全了。
  • 非对称加密:就像是用一个钥匙锁箱子(公钥),另一个钥匙来开箱子(私钥)。发送方使用接收方的公钥进行加密,而只有接收方的私钥能解开。这样即便公钥被公开,没有私钥也无法解密信息,从而保证了传输数据的安全。

在实际应用中,HTTPS 通常采用混合加密机制。在连接建立初期使用非对称加密交换对称加密的密钥,一旦密钥交换完成,之后的通信就切换到效率更高的对称加密。就像是先通过一个安全的箱子(非对称加密)把家门钥匙(对称加密的密钥)安全送到朋友手中,之后就可以放心地使用这把钥匙进行通信了。

SSL/TLS协议

HTTPS 实际上是 HTTP 协议跑在 TLS 协议之上,TLS的全称是 Transport Layer Security,从字面上理解就是传输层安全,保护数据传输的安全。有时候我们还会看到 SSL 这个词,SSL 其实是 TLS 的前身,它的全称是 Secure Sockets Layer,Socket 就是是TCP/UDP编程中经常接触的套接字概念,也是传输层的一个组件。

可以理解为,SSL/TLS就像是一个提供安全保护的信封,确保了信件(数据)在寄送过程中的安全。

让我们来详细探查下 HTTPS 的工作流程:

1、开始握手:当浏览器尝试与服务器建立HTTPS连接时,它首先会发送一个“Hello”消息给服务器,这个消息里包含了浏览器支持的加密方法(包括对称加密和非对称加密等)等信息。

2、服务器回应:服务器收到客户端的“Hello”之后,会选择一组客户端和服务器都支持的加密方法,然后用自己的私钥对信息进行签名,把这个签名连同服务器的SSL证书一起发送到客户端,SSL证书里包含了服务器的公钥。

3、验证证书:客户端收到服务器发过来的证书后,会首先验证证书的合法性,确保证书是可信任的CA颁发,且未被篡改。这个验证会使用浏览器或者操作系统内置的安全根证书,验证从服务器证书到根证书的所有认证链上的签名都是可信任的。

4、生成临时密钥:一旦证书验证通过,客户端就会生成一串随机密钥(也就是对称密钥)。然后,客户端会用服务器的公钥对这串随机密钥进行加密,再发送给服务器。

5、服务器解密获取对称密钥:服务器收到加密后的数据,会用自己的私钥对其解密,获取到其中的对称密钥。到这里,客户端和服务器双方就都拥有了这个对称密钥,后续的通信就可以使用这个对称密钥进行加密了。

这里我们介绍的密钥交换方式是RSA,其实TLS支持多种密钥交换机制,除了RSA,还包括Diffie-Hellman密钥交换(简称DH)、椭圆曲线Diffie-Hellman(简称ECDH)密钥交换等,或者RSA和DH的结合。DH密钥交换不需要在通信双方之间直接发送对称密钥,同时即使证书的私钥被泄露,之前的会话密钥也不能被推导出来,之前的通信也就无法被解密,这样更加安全。有兴趣的同学可以去搜索了解一下。

证书和认证机构(CA)

为了保证网站的身份真实性,HTTPS还涉及到了证书(SSL证书)的使用。这个证书由认证机构(CA)颁发,包含了网站公钥、网站身份信息等。浏览器或操作系统内置了这些认证机构的信任列表,能自动验证证书的真实性。

证书认证机构会在颁发证书前确认网站的身份,这有点像买火车票之前,需要先通过身份认证来确认你的身份。根据验证的深度和范围,证书可以分为以下几种类型:

  1. 域名验证(DV)证书

这种证书只验证网站拥有者对域名的控制权。CA会通过Url文件验证或DNS记录验证等方式来确认申请者是否控制该域名。DV证书的发放速度快,成本低,但它只证明域名的控制权,不会验证组织的真实身份。

  1. 组织验证(OV)证书

OV证书不仅验证域名的控制权,还要验证申请证书的组织是真实、合法且正式注册的。这就像提交某些申请时,除了要上传身-份-证,还要上传企业的营业执照,确认你是某个公司的员工。OV证书提供了更高级别的信任,适用于商业网站。

  1. 扩展验证(EV)证书

EV证书提供了最高级别的验证。在这个过程中,CA会进行更为严格和全面的审查,包括确认申请组织的法律、运营和物理存在。这就像不仅检查身-份-证和营业执照,还要确认你的实际居住地址、实际办公地点等信息。EV证书为用户提供了最高水平的信任,但它的发放流程最为复杂,成本也最高。

配置HTTPS的步骤

1. 获取SSL/TLS证书

可以从阿里云、腾讯云等这些大的云计算平台申请你需要的证书,也可以从专门的证书颁发机构获取。

证书可以只针对单个域名,比如www.juejin.cn,那只能 http://www.juejin.cn 使用这个证书,www2.juejin.cn 不能使用这个证书;也可以配置为泛域名,比如 *.juejin.cn,那么 http://www.juejin.cn 和 www2.juejin.cn 都可以使用这个证书。

申请证书时会验证你的身份,比如对于DV证书,需要你在DNS中配置一个特殊TXT解析、或者在网站中放置一个特别的验证文件,证书颁发机构能够通过网络进行验证。

验证通过后,证书颁发结构会给你发放证书,包括公钥和私钥。

证书有免费版和收费版。免费版一般只针对单个域名,仅颁发DV证书,证书的有效期一般是3-12个月。普通用户为了节约成本,可以使用免费版本,通过一些程序脚本实现证书的到期自动更新。

2. 配置Web服务器

拿到证书后,需要在你的Web服务器上配置它,具体步骤取决于你使用的服务器软件(如Apache、Nginx等)。

注意HTTPS默认的监听端口是443,使用这个端口,用户访问时可以不输入端口号。

3. 强制使用HTTPS

为了确保所有数据都是安全传输的,我们可以使用重定向让用户始终访问HTTPS地址。

在Web服务器上设置,将所有HTTP请求重定向到HTTPS,用户使用HTTP时都会自动跳转到HTTPS,比如访问 juejin.cn 会自动跳转到 juejin.cn。

4. 维护和更新

证书都是有保质期的,需要在证书到期前进行续期。有时候我们还需要根据安全威胁报告,及时更新SSL/TLS的加密设置,确保它们符合最新的安全标准。

HTTPS的安全问题

HTTPS虽然大大提高了网站的安全性,但它也不是万无一失的。

1、弱加密算法

如果使用过时或不安全的加密算法,加密的数据可能会被破解。

在Web服务器配置中禁用已知不安全的SSL/TLS版本(如TLS 1.0和1.1)和弱加密套件,选择使用强加密算法,如AES_GCM或ChaCha20。

2、钓鱼网站

即使是使用HTTPS的网站,也可能是钓鱼网站,比如DV证书只验证网站的域名归属,不确认网站具体是干什么的。这就像强盗穿上快递员的制服,你很难一眼识破。

对于关键的服务,比如在线购物、上传个人信息,用户需要提高警惕,检查网站的URL,确保是访问的正确网站。

我们也可以使用浏览器提供的安全插件或服务来识别和阻止访问已知的恶意网站。

3、中间人攻击

即使使用了HTTPS,如果攻击者能够在通信双方之间插入自己,就能够监听、修改传输的数据。如果你使用过Fiddler 这种抓包程序做过前端通信调试,就很容易理解这个问题。这就像快递途中有个假冒的收发室,所有包裹都得先经过它。

要防范这个问题比较困难,用户尽量不要在公共的WiFi网络进行敏感操作,不随便下载安装可疑的文件或程序,网站运营者要确保网站的TLS配置是安全的,使用强加密算法和协议。

4、审核不严的证书

证书颁发机构审核不严或者胡乱颁发证书,比如别有用心的人通过特殊手段就能申请到google.com的证书。而且历史上也确实发生过。

2011年,荷兰证书颁发机构(CA)DigiNotar因被黑客入侵并滥发了大量伪造的SSL/TLS证书,包括对Google域名的证书,最终导致DigiNotar破产。

2016年,中国CA机构WoSign及旗下子公司StartCom被曝出多种违规操作,导致主流浏览器厂商逐步撤销对这两家CA的信任。

解决这个问题主要依赖证书颁发机构和监管机构的安全机制,浏览器和操作系统厂商也可以在问题发生后通过紧急更新来避免风险的进一步扩大,使用证书的用户如果有能力,可以通过监控CA机构发布的证书颁发日志来探查是否有未经授权的证书颁发给你的域名。


以上就是本文的主要内容,希望此文能让你对Https有了一个系统全面的了解,更好的保护Http通信安全。


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

关于我裁员在家没事接了个私单这件事...

起因 2024年3月31日,我被公司裁员了。 2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。 2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回...
继续阅读 »

起因


2024年3月31日,我被公司裁员了。


2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。


2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回上海开始找工作。结果环境比预想的还要差啊,以前简历放开就有人找,现在每天投个几十封都是石沉大海。。。


2024年4月15日,有个好朋友找我,想让我给他们公司开发一个“拨号APP”(主要原因其实是这个好哥们想让我多个赚钱门路😌),主要的功能就是在他们的系统上点击一个“拨号”按钮,然后员工的工作手机上就自动拨打这个号码。


可行性分析


涉及到的修改:



  • 系统前后端

  • 拨号功能的APP


拿到这个需求之后,我并没有直接拒绝或者同意,而是先让他把他公司那边的的源代码发我了一份,大致看了一下使用的框架,然后找一些后端的朋友看有没人有人一起接这个单子;而我自己则是要先看下能否实现APP的功能(因为我以前从来没有做过APP!!!)。


我们各自看过自己的东西,然后又沟通了一番简单的实现过程后达成了一致,搞!


因为我这边之前的技术栈一直是VUE,所以决定使用uni-app实现,主要还是因为它的上手难度会低很多。


第一版


需求分析


虽说主体的功能是拨号,但其实是隐含很多辅助性需求的,比如拨号日志、通时通次统计、通话录音、录音上传、后台运行,另外除了这些外还有额外的例如权限校验、权限引导、获取手机号、获取拨号状态等功能需要实现。


但是第一次预算给的并不高,要把这些全部实现显然不可能。因此只能简化实现功能实现。



  • 拨号APP

    • 权限校验

      • 实现部分(拨号、录音、文件读写)



    • ❌权限引导

    • 查询当前手机号

      • 直接使用input表单,由用户输入



    • 查询当前手机号的拨号任务

      • 因为后端没有socket,使用setTimeout模拟轮询实现。



    • 拨号、录音、监测拨号状态

      • 根据官网API和一些安卓原生实现



    • 更新任务状态

      • 告诉后端拨号完成



    • ❌通话录音上传

    • ❌通话日志上传

    • ❌本地通时通次统计

    • 程序运行日志

    • 其他

      • 增加开始工作、开启录音的状态切换

      • 兼容性,只兼容安卓手机即可






基础设计


一个input框来输入用户手机号,一个开始工作的switch,一个开启录音的切换。用户输入手机号,点击开始工作后开启轮询,轮询到拨号任务后就拨号同时录音,同时监听拨号状态,当挂断后结束录音、更新任务状态,并开启新一轮的轮询。


开干


虽然本人从未开发过APP,但本着撸起袖子就是干的原则,直接打开了uni-app的官网就准备开怼。


1、下载 HbuilderX。


2、新建项目,直接选择了默认模板。


3、清空 Hello页面,修改文件名,配置路由。


4、在vue文件里写主要的功能实现,并增加 Http.jsRecord.jsPhoneCall.jsPower.js来实现对应的模块功能。


⚠️关于测试和打包


运行测试


在 HbuilderX 中点击“运行-运行到手机或模拟器-运行到Android APP基座”会打开一个界面,让你选择运行到那个设备。这是你有两种选择:



  • 把你手机通过USB与电脑连接,然后刷新列表就可以直接运行了。

    • 很遗憾,可能是苹果电脑与安卓手机的原因,插上后检测不出设备😭。。。



  • 安装Android Studio,然后通过运行内置的模拟器来供代码运行测试。

    • 这种很麻烦,要下载很久,且感觉测试效果并不好,最好还是用windows电脑连接手机的方法测试。




关于自定义基座和标准基座的差别,如果你没有买插件的话,直接使用基准插座就好。如果你要使用自定义基座,就首先要点击上图中的制作自定义基座,然后再切换到自定义基座执行。


但是不知道为什么,我这里一直显示安装自定义基座失败。。。


打包测试


除了以上运行测试的方法外,你还有一种更粗暴的测试方法,那就是打包成APP直接在手机上安装测试。


点击“发行-原生APP 云打包”,会生成一个APK文件,然后就可以发送到手机上安装测试。不过每天打包的次数有限,超过次数需要购买额外的打包服务或者等第二天打包。


我最终就是这样搞得,真的我哭死,我可能就是盲调的命,好多项目都是盲调的。


另外,在打包之前我们首先要配置manifest.json,里面包含了APP的很多信息。比较重要的一个是AppId,一个是App权限配置。参考uni-app 权限配置Android官方权限常量文档。以下是拨号所需的一些权限:



// 录制音频
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 修改音频设置
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

// 照相机
<uses-permission android:name="android.permission.CAMERA" />
// 写入外部存储
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// 读取外部存储
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

// 读取电话号码
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
// 拨打电话
<uses-permission android:name="android.permission.CALL_PHONE" />
// 呼叫特权
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
// 通话状态
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
// 读取拨号日志
<uses-permission android:name="android.permission.READ_CALL_LOG" />
// 写入拨号日志
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
// 读取联系人
<uses-permission android:name="android.permission.READ_CONTACTS" />
// 写入联系人
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
// 读取SMS?
<uses-permission android:name="android.permission.READ_SMS" />

// 写入设置
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
// 唤醒锁定?
<uses-permission android:name="android.permission.WAKE_LOCK" />
// 系统告警窗口?
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// 接受完整的引导?
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

⚠️权限配置这个搞了很长时间,即便现在有些权限还是不太清楚,也不知道是不是有哪些权限没有配置上。。。


⚠️权限校验


1、安卓 1


好像除了这样的写法还可以写"scope.record"或者permission.CALL_PHONE


permision.requestAndroidPermission("android.permission.CALL_PHONE").then(res => {
// 1 获得权限 2 本次拒绝 -1 永久拒绝
});

2、安卓 2


plus.android.requestPermissions(["android.permission.CALL_PHONE"], e => {
// e.granted 获得权限
// e.deniedPresent 本次拒绝
// e.deniedAlways 永久拒绝
});

3、uni-app


这个我没测试,AI给的,我没有用这种方法。有一说一,百度AI做的不咋地。


// 检查权限
uni.hasPermission({
permission: 'makePhoneCall',
success() {
console.log('已经获得拨号权限');
},
fail() {
// 示例:请求权限
uni.authorize({
scope: 'scope.makePhoneCall',
success() {
console.log('已经获得授权');
},
fail() {
console.log('用户拒绝授权');
// 引导用户到设置中开启权限
uni.showModal({
title: '提示',
content: '请在系统设置中打开拨号权限',
success: function(res) {
if (res.confirm) {
// 引导用户到设置页
uni.openSetting();
}
}
});
}
});
}
});

✅拨号


三种方法都可以实现拨号功能,只要有权限,之所以找了三种是为了实现APP在后台的情况下拨号的目的,做了N多测试,甚至到后面搞了一份原生插件的代码不过插件加载当时没搞懂就放弃了,不过到后面才发现原来后台拨号出现问题的原因不在这里,,具体原因看后面。


另外获取当前设备平台可以使用let platform = uni.getSystemInfoSync().platform;,我这里只需要兼容固定机型。


1、uni-app API


uni.makePhoneCall({
phoneNumber: phone,
success: () => {
log(`成功拨打电话${phone}`);
},
fail: (err) => {
log(`拨打电话失败! ${err}`);
}
});

2、Android


plus.device.dial(phone, false);

3、Android 原生


写这个的时候有个小插曲,当时已经凌晨了,再加上我没有复制,是一个个单词敲的,结果竟然敲错了一个单词,测了好几遍都没有成功。。。还在想到底哪里错了,后来核对一遍才发现😭,control cv才是王道啊。


// Android
function PhoneCallAndroid(phone) {
if (!plus || !plus.android) return;
// 导入Activity、Intent类
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
// 获取主Activity对象的实例
var main = plus.android.runtimeMainActivity();
// 创建Intent
var uri = Uri.parse("tel:" + phone); // 这里可修改电话号码
var call = new Intent("android.intent.action.CALL", uri);
// 调用startActivity方法拨打电话
main.startActivity(call);
}

✅拨号状态查询


第一版用的就是这个获取状态的代码,有三种状态。第二版的时候又换了一种,因为要增加呼入、呼出、挂断、未接等状态的判断。


export function getCallStatus(callback) {
if (!plus || !plus.android) return;
let maintest = plus.android.runtimeMainActivity();
let Contexttest = plus.android.importClass("android.content.Context");
let telephonyManager = plus.android.importClass("android.telephony.TelephonyManager");
let telManager = plus.android.runtimeMainActivity().getSystemService(Contexttest.TELEPHONY_SERVICE);
let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (Contexttest, intent) => {
plus.android.importClass(intent);
let phoneStatus = telManager.getCallState();
callback && callback(phoneStatus);
//电话状态 0->空闲状态 1->振铃状态 2->通话存在
}
});
let IntentFilter = plus.android.importClass("android.content.IntentFilter");
let filter = new IntentFilter();
filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
maintest.registerReceiver(receiver, filter);
}

⚠️录音


录音功能这个其实没啥,都是官网的API,无非是简单的处理一些东西。但是这里有一个大坑!


一坑


就是像通话录音这种涉及到的隐私权限很高,正常的这种录音是在通话过程中是不被允许的。


二坑


后来一次偶然的想法,在接通之后再开启录音,发现就可以录音了。


但随之而来的是第二个坑,那就是虽然录音了,但是当播放的时候发现没有任何的声音,还是因为保护隐私的原因,我当时还脱离代码专门试了试手机自带的录音器来在通话时录音,发现也不行。由此也发现uni的录音本身也是用的手机录音器的功能。


三坑


虽然没有声音,但是我还是试了下保存,然后就发现了第三个坑,那就是虽然获取了文件权限,但是现在手机给的读写权限都是在限定内的,录音所在的文件夹是无权访问的。。。


另辟蹊径


其实除了自己手动录音外还可以通过手机自带的通话录音来实现,然后只要手机去读取录音文件并找到对应的那个就可以了。思路是没啥问题,不过因为设置通话录音指引、获取录音文件都有问题,这一版本就没实现。


// 录音

var log = console.log,
recorder = null,
// innerAudioContext = null,
isRecording = false;

export function startRecording(logFun = console.log) {
if (!uni.getRecorderManager || !uni.getRecorderManager()) return logFun('不支持录音!');
log = logFun;
recorder = uni.getRecorderManager();
// innerAudioContext = uni.createInnerAudioContext();
// innerAudioContext.autoplay = true;
recorder.onStart(() => {
isRecording = true;
log(`录音已开始 ${new Date()}`);
});
recorder.onError((err) => {
log(`录音出错:${err}`);
console.log("录音出错:", err);
});
recorder.onInterruptionBegin(() => {
log(`检测到录音被来电中断...`);
});
recorder.onPause(() => {
log(`检测到录音被来电中断后尝试启动录音..`);
recorder.start({
duration: 10 * 60 * 1000,
});
});
recorder.start({
duration: 10 * 60 * 1000,
});
}

export function stopRecording() {
if (!recorder) return
recorder.onStop((res) => {
isRecording = false;
log(`录音已停止! ${new Date()}`); // :${res.tempFilePath}
// 处理录制的音频文件(例如,保存或上传)
// powerCheckSaveRecord(res.tempFilePath);
saveRecording(res.tempFilePath);
});
recorder.stop();
}

export function saveRecording(filePath) {
// 使用uni.saveFile API保存录音文件
log('开始保存录音文件');
uni.saveFile({
tempFilePath: filePath,
success(res) {
// 保存成功后,res.savedFilePath 为保存后的文件路径
log(`录音保存成功:${res.savedFilePath}`);
// 可以将res.savedFilePath保存到你的数据中,或者执行其他保存相关的操作
},
fail(err) {
log(`录音保存失败! ${err}`);
console.error("录音保存失败:", err);
},
});
}

运行日志


为了更好的测试,也为了能实时的看到执行的过程,需要一个日志,我这里就直接渲染了一个倒序的数组,数组中的每一项就是各个函数push的字符串输出。简单处理。。。。嘛。


联调、测试、交工


搞到最后,大概就交了个这么玩意,不过也没有办法,一是自己确实不熟悉APP开发,二是满共就给了两天的工时,中间做了大量的测试代码的工作,时间确实有点紧了。所幸最起码的功能没啥问题,也算是交付了。


image.png


第二版


2024年05月7日,老哥又找上我了,想让我们把他们的这套东西再给友商部署一套,顺便把这个APP再改一改,增加上通时通次的统计功能。同时也是谈合作,如果后面有其他的友商想用这套系统,他来谈,我们来实施,达成一个长期合作关系。


我仔细想了想,觉得这是个机会,这块东西的市场需求也一直有,且自己现在失业在家也有时间,就想着把这个简单的功能打磨成一个像样的产品。也算是做一次尝试。


需求分析



  • ✅拨号APP

    • 登录

      • uni-id实现



    • 权限校验

      • 拨号权限、文件权限、自带通话录音配置



    • 权限引导

      • 文件权限引导

      • 通话录音配置引导

      • 获取手机号权限配置引导

      • 后台运行权限配置引导

      • 当前兼容机型说明



    • 拨号

      • 获取手机号

        • 是否双卡校验

        • 直接读取手机卡槽中的手机号码

        • 如果用户不会设置权限兼容直接input框输入



      • 拨号

      • 全局拨号状态监控注册、取消

        • 支持呼入、呼出、通话中、来电未接或挂断、去电未接或挂断





    • 录音

      • 读取录音文件列表

        • 支持全部或按时间查询



      • 播放录音

      • ❌上传录音文件到云端



    • 通时通次统计

      • 云端数据根据上面状态监控获取并上传

        • 云端另写一套页面



      • 本地数据读取本机的通话日志并整理统计

        • 支持按时间查询

        • 支持呼入、呼出、总计的通话次数、通话时间、接通率、有效率等





    • 其他

      • 优化日志显示形式

        • 封装了一个类似聊天框的组件,支持字符串、Html、插槽三种显示模式

        • 在上个组件的基础上实现权限校验和权限引导

        • 在上两个组件的基础上实现主页面逻辑功能



      • 增加了拨号测试、远端连接测试

      • 修改了APP名称和图标

      • 打包时增加了自有证书






中间遇到并解决的一些问题


关于框架模板


这次重构我使用了uni中uni-starter + uni-admin 项目模板。整体倒还没啥,这俩配合还挺好的,就只是刚开始不知道还要配置东西一直没有启动起来。


建立完项目之后还要进uniCloud/cloudfunctions/common/uni-config-center/uni-id配置一个JSON文件来约定用户系统的一些配置。


打包的时候也要在manifest.json将部分APP模块配置进去。


还搞了挺久的,半天才查出来。。


类聊天组件实现



  • 设计

    • 每个对话为一个无状态组件

    • 一个图标、一个名称、一个白底的展示区域、一个白色三角

    • 内容区域通过类型判断如何渲染

    • 根据前后两条数据时间差判断是否显示灰色时间



  • 参数

    • ID、名称、图标、时间、内容、内容类型等



  • 样式

    • 根据左边右边区分发送接收方,给与不同的类名

    • flex布局实现




样式实现这里,我才知道原来APP和H5的展示效果是完全不同的,个别地方需要写两套样式。


关于后台运行


这个是除了录音最让我头疼的问题了,我想了很多实现方案,也查询了很多相关的知识,但依旧没效果。总体来说有以下几种思路。



  • 通过寻找某个权限和引导(试图寻找到底是哪个权限控制的)

  • 通过不停的访问位置信息

  • 通过查找相应的插件、询问GPT、百度查询

  • 通过程序切入后台之后,在屏幕上留个悬浮框(参考游戏脚本的做法)

  • 通过切入后台后,发送消息实现(没测试)


测试了不知道多少遍,最终在一次无意中,终于发现了如何实现后台拨号,并且在之后看到后台情况下拨号状态异常,然后又查询了应用权限申请记录,也终于知道,归根到底能否后台运行还是权限的问题。


关于通话状态、通话记录中的类型


这个倒还好,就是测试的问题,知道了上面为啥异常的情况下,多做几次测试,就能知道对应的都是什么状态了。


通话状态:呼入振铃、通话中(呼入呼出)、通话挂断(呼入呼出)、来电未接或拒绝、去电未接或拒接。


通话日志:呼入、呼出、未接、语音邮件、拒接


交付


总体上来说还过得去,相比于上次简陋的东西,最起码有了一点APP的样子,基本上该有的功能也基本都已经实现了,美中不足的一点是下面的图标没有找到合适的替换,然后录音上传的功能暂未实现,不过这个也好实现了。


image.png


后面的计划



  • 把图标改好

  • 把录音文件是否已上传、录音上传功能做好

  • 把APP的关于页面加上,对接方法、使用方法和视频、问题咨询等等

  • 原本通话任务、通时通次这些是放在一个PHP后端的,对接较麻烦。要用云函数再实现一遍,然后对外暴露几个接口,这样任何一个系统都可以对接这个APP,而我也可以通过控制云空间的跨域配置来开放权限

  • 把数据留在这边之后,就可以再把uni-admin上加几个页面,并且绑定到阿里云的云函数前端网页托管上去

  • 如果有可能的话,上架应用商店,增加上一些广告或者换量联盟之类的东西

  • 后台运行时,屏幕上加个悬浮图标,来电时能显示个振铃啥的


大致的想法就这些了,如果这个产品能继续卖下去,我就会不断的完善它。


最后


现在的行情真的是不好啊,不知道有没有大哥给个内推的机会,本人大专计算专业、6.5年Vue经验(专精后台管理、监控大屏方向,其他新方向愿意尝试),多个0-1-2项目经验,跨多个领域如人员管理、项目管理、产品设计、软件测试、数据爬虫、NodeJS、流程规范等等方面均有了解,工作稳定不经常跳,求路过的大哥给个内推机会把!



作者:前端湫
来源:juejin.cn/post/7368421971384860684
收起阅读 »

最近得了一场病 差点要了我的命

最近得了一场病,前后持续了有十多天,时至今日感觉脑袋还是昏昏沉沉的不在状态,感觉像是药吃的,毕竟连着吃了十多天西药,可能人也吃傻了吧,中间还挂了五天水,算是补充了能量。 起因是和老婆去吃饭,可能是吃得太饱晚上着凉了,结果第二天下班回来就发烧,最开始是去社康看的...
继续阅读 »

最近得了一场病,前后持续了有十多天,时至今日感觉脑袋还是昏昏沉沉的不在状态,感觉像是药吃的,毕竟连着吃了十多天西药,可能人也吃傻了吧,中间还挂了五天水,算是补充了能量。


起因是和老婆去吃饭,可能是吃得太饱晚上着凉了,结果第二天下班回来就发烧,最开始是去社康看的,想着缴纳的社保也一直没用过,不知道怎么用,刚好借这个机会去试试。


图片


不知道西安的社保是什么样,深圳这边的分一二三档,一档可以去任意社康和医院看病买药,有独立账户;二档没有独立账户,且只能去绑定的社康看病,去医院的话需要先去社康开转诊,最新消息说现在不用了,而且也不能单独买药。


去了之后,先是几项检查,抽血化验,鼻腔测试,加起来一百多块吧,当时就纳闷,一个感冒发烧要搞得这么复杂吗,问医生说流程是这样的,要检查是什么原因引起的,看完报告说有点轻微病毒感染,再就是细菌感染引起的发烧,然后就开了点药。


图片


说下这里的报销比例吧,理论上最高能报到75%,个人能承担25%,二百块钱的医药费,报销完自己付了有五十多块钱吧。听说这是深圳这边今年十月份重新调整后的,之前是二档每年报销一千额度,单次报销比例高,个人承担费用少,调整后每人每年有两千多的免费额度,但是单次报销比例也降低了,意味着个人每次承担的也多了,当然如果你是长期去医院,也是比较划算的。


图片


再来看一下在社康开的药吧,一大盒口服液,里面是多支小瓶装;一盒头孢,之前总是听说,也是头一次吃,说是消炎的;还有一盒粉末状的东西,说是用温水冲服,后来发现是盐水,怎么说呢,总感觉有点开玩笑的意思吧。


图片


两天的药吃完之后呢,中间身体短暂好转了半天,再之后到晚上就又是继续发烧,最高烧到接近四十度,第二天量的时候已经达到39.5,且中间一直伴随着头疼,实在忍不了,整宿睡不着,早上六点多给领导发了消息请了假。


图片


这次是去楼下的私人诊所医治,女医生看了就说连着发烧这么多天现在吃药肯定来不及了,必须要打针,然后就开始输液了。小时候感觉输液是一件大动干戈的事,现在看来却是那么的平淡无奇,四小瓶水挂完之后浑身冒汗,温度也降了下来,头也没有那么疼了,中途老婆一直陪着我,还给我带了吃的,挂完之后医生同时又开了几天吃的药。


图片


第二天感觉轻松多了,为了巩固又去挂了一天,然后两天药也吃完了,接下来又产生了新的问题,不知道咋回事一直打嗝,连着一整天不停歇的那种打,刚开始以为是吃东西噎着了,可是无论怎么喝水憋气都无济于事,上班时坐在工位上自己嗝的都有点不好意思了,一直到晚上回家,没办法又去问了医生,说可能是胃痉挛,胃部引起的,开了两顿吃的药,很神奇的是刚吃下去没一会打嗝就停了,这一夜算是到这了,也睡了个好觉。


图片


接下来又是继续发烧,头疼,头晕,浑身无力,中午休息头疼的睡不着觉,下午手脚发麻,实在有些扛不住了,出去外面商场找了个沙发窝着睡了会,一直扛到下班,又去看医生,说是没好彻底,继续挂水,吃药,又是三天。这一路下来,真的是折磨人,让老婆也跟着前前后后来回折腾。其实在整个过程当中,发烧这些我感觉都可以忍,最难受的是头疼,偏头痛,那种神经痛,一刻不停的那种疼,真的很折磨人,让人崩溃。


图片


再往后又连着挂了三天水,吃了三天药,折腾了十多天好觉差不多了,接下来又残留着一点小问题,就是一直咳嗽停不下来,不是喉咙咳是从肺里面的那种咳,好觉应该问题不太大,过几天就好了,可是持续了几天还是一直在咳嗽,然后就又去看了医生,买了三天的药一百多块钱,问题是和前面开的药也一模一样没啥变化,感觉这边还是挺黑的,普普通通两三天药就是一百多,放在老家可能就几十块最多了,再加上前面打针输液的,总共花了一千多块吧,就是个普通的感冒发烧,稍微严重点。


图片


经过这次事件,有以下几点感触吧。无论什么时候,身体健康是第一位,所谓的工作,都是建立在你有个好的身体的前提下,身体状态良好,你才能更好的投入工作。第二,平时除了多加锻炼身体,还要注重身体按摩,长期坐在办公室不运动,颈椎难免有影响,没事多按摩活动下,也利用颈部头部血液循环,有助于偏头痛的缓解;再就是第三点,要好好吃饭,注重身体包养,好好对待自己的胃,很多东西都是吃出来的。


作者:编程迪
来源:juejin.cn/post/7306018817687765044
收起阅读 »

28个令人惊艳的JavaScript单行代码

web
JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。 1. 阶乘计算 使用递归函数计算给定数字的阶乘。 const factorial = n => ...
继续阅读 »

JavaScript作为一种强大而灵活的脚本语言,充满了许多令人惊艳的特性。本文将带你探索28个令人惊艳的JavaScript单行代码,展示它们的神奇魅力。


1. 阶乘计算


使用递归函数计算给定数字的阶乘。


const factorial = n => n === 0 ? 1 : n * factorial(n - 1);
console.log(factorial(5)); // 输出 120

2. 判断一个变量是否为对象类型


const isObject = variable === Object(variable);

3. 数组去重


利用Set数据结构的特性,去除数组中的重复元素。


const uniqueArray = [...new Set(array)];

4. 数组合并


合并多个数组,创建一个新的数组。


const mergedArray = [].concat(...arrays);

5. 快速最大值和最小值


获取数组中的最大值和最小值。


const max = Math.max(...array);
const min = Math.min(...array);

6. 数组求和


快速计算数组中所有元素的和。


const sum = array.reduce((acc, cur) => acc + cur, 0);

7. 获取随机整数


生成一个指定范围内的随机整数。


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

8. 反转字符串


将字符串反转。


const reversedString = string.split('').reverse().join('');

9. 检查回文字符串


判断一个字符串是否为回文字符串。


const isPalindrome = string === string.split('').reverse().join('');

10. 扁平化数组


将多维数组转换为一维数组。


const flattenedArray = array.flat(Infinity);

11. 取随机数组元素


从数组中随机取出一个元素。


const randomElement = array[Math.floor(Math.random() * array.length)];

12. 判断数组元素唯一


检查数组中的元素是否唯一。


const isUnique = array.length === new Set(array).size;

13. 字符串压缩


将字符串中重复的字符进行压缩。


const compressedString = string.replace(/(.)\1+/g, match => match[0] + match.length);

14. 生成斐波那契数列


生成斐波那契数列的前n项。


const fibonacci = Array(n).fill().map((_, i, arr) => i <= 1 ? i : arr[i - 1] + arr[i - 2]);

15. 数组求交集


获取多个数组的交集。


const intersection = arrays.reduce((acc, cur) => acc.filter(value => cur.includes(value)));

16. 验证邮箱格式


检查字符串是否符合邮箱格式。


const isValidEmail = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/.test(email);

17. 数组去除假值


移除数组中的所有假值,如falsenull0""undefined


const truthyArray = array.filter(Boolean);

18. 求阶乘


计算一个数的阶乘。


const factorial = n => n <= 1 ? 1 : n * factorial(n - 1);

19. 判断质数


检查一个数是否为质数。


const isPrime = n => ![...Array(n).keys()].slice(2).some(i => n % i === 0);

20. 检查对象是空对象


判断对象是否为空对象。


const isEmptyObject = Object.keys(object).length === 0 && object.constructor === Object;

21. 判断回调函数为真


检查数组中的每个元素是否满足特定条件。


const allTrue = array.every(condition);

22. 检查回调函数为假


检查数组中是否有元素满足特定条件。


const anyFalse = array.some(condition);

23. 数组排序


对数组进行排序。


const sortedArray = array.sort((a, b) => a - b);

24. 日期格式化


将日期对象格式化为指定格式的字符串。


const formattedDate = new Date().toISOString().slice(0, 10);

25. 将字符串转为整数类型


const intValue = +str;

26. 计算数组中元素出现的次数


统计数组中各元素的出现次数。


const countOccurrences = array.reduce((acc, cur) => (acc[cur] ? acc[cur]++ : acc[cur] = 1, acc), {});

27. 交换两个变量的值


[a, b] = [b, a];

28. 利用逗号运算符分隔多个表达式


const result = (expression1, expression2, ..., expressionN);

作者:慕仲卿
来源:juejin.cn/post/7307963529872605218
收起阅读 »

困扰我 1 小时的 404 错误 别人 1 分钟解决了

上周五遇到了一个 Bug,没有任何异常,困扰了我一个小时,差点连周末都过不好了。最后没办法,请教了组内的同学,没想到竟被他 1 分钟解决了! 事情的起因,只是因为我把一个接口从 @GetMapping 改成了 @PostMapping ,然后接口就报以下的错误...
继续阅读 »

上周五遇到了一个 Bug,没有任何异常,困扰了我一个小时,差点连周末都过不好了。最后没办法,请教了组内的同学,没想到竟被他 1 分钟解决了!


事情的起因,只是因为我把一个接口从 @GetMapping 改成了 @PostMapping ,然后接口就报以下的错误:


image.png
没有任何的 WARN 或者 ERROR 日志!

网上搜了一下,也没有什么有效的信息,万能的 AI 给出了下面这样的回答:


image.png
404 的错误太常见了,有很多原因造成这一结果。


但可以确定的是,我的请求路径和控制器配置都是没有问题的,因为只要要把 @PostMapping 改回 @GetMapping ,一切都运行正常。


在这种情况下,搜索引擎和AI,除了给我造成干扰误导排查方向外,不能起到什么实质性的作用。


无奈,我只能硬着头皮打开 DEBUG 日志,尝试对照源码,去解决问题了。


不幸的是,DEBUG 日志实在太多了,里面也没有任何异常。我刚学 SpringBoot 不久,这些碎片化的日志,不能引起我的任何联想,因此,实质上也起不到辅助排查的作用。


折腾了一个多小时,还是没有什么头绪,明天就周末了,带着这个 Bug,周末恐怕都休息不好。于是,我就硬着头皮找了组内一个比较有经验的同学帮忙看一下。


他过来翻了翻日志,查看了一下配置类,淡淡地说到,你打开了 Csrf 验证,但是请求却没带 Token。说罢,指导我加上了一行代码:


.csrf().disable()

然后,再次访问,竟然就真的可以了!整个过程也就 1 分钟左右!


我这个小弱鸡的心灵着实有些触动。于是追问到,大神你是怎么看出来的呀。


“没什么,就是经验多了。日志里面有些信息,比如 token 相关的, 其实已经提示了你答案。不过,需要你对框架比较了解,才能 get 到这些信息。新手遇到这种没有明显异常的问题,确实会比较费劲。”


“那有没有什么办法,可以快速搞懂这种框架问题啊,每次遇到都挺烦躁的,不仅影响研发进度,也影响心情” , 上进的我还是想从大神这里获取更多的经验。


“额….我想想”,大神迟疑了一会儿,“你可以试试这个 XCodeMap 插件“,”它可以提供更丰富的信息,图形化的形式,可以较为容易看出可能存在的问题。实在看不出,你也可以基于这些信息再去问搜索引擎或者AI”。


“感谢大神,我去试一下”。


试用了一下,这个工具画出了下面的序列图:


image.png
我虽然不懂什么 Csrf 的原理,但是这个图已经可以清晰地表达出问题了,在 SpringBoot 的FilterChain 中,走到 CsrfFilter 就终止了,并且调用了一个 AccessDeniedHandler。


看起来,这个序列图是实时动态采集的,而且做了很多剪枝,把一些关键调用给标记了出来。对于 SpringBoot 系列,其会把各种 Filter 的调用情况展示出来,可以让人一眼看出来是哪个 Filter 出了问题。


点击 CsrfFilter 的 doFilter 方法,可以看到以下代码:


image.png
这个代码可以看出来,Csrf 的原理(以CookieCsrfToken为例)就是取两个token进行比对。其中一个从请求的 Header 或者 Parameter 中读出。另外一个,从 Cookie 中读出。


image.png
由于浏览器的同源策略,攻击网站无法获取本网站的Cookie,也即其无法完成下面这样的JS操作:


image.png
但是本网站可以通过上面的操作,把 Cookie 中的token设置到 Header 中,这样就达到了避免 CSRF 攻击的效果。


不过,这里还有一个小插曲,csrf 验证失败,本意应该是报 403 错误码,然后转发到 “/403” 页面,只是因为我没有配置 “/403” 页面,最终才报了404 错误。


image.png


这次由 Csrf 引起的 404 错误,就到此为止了。


我独自完成了后面的排查,还是很开心的。我没有大神那样丰富的经验,可以凭借只言片语的日志信息,就可以推断出问题所在。但我借助 XCodeMap 绘制的动态序列图,按图索骥,搞清楚了问题的来龙去脉。这让我想起了下面的一句话:


人类有了弓箭,拳头就不再是绝对硬实力了。好的工具,可以削平人与人的差距!


感觉自己与大神接近了不少!


参考资料:



作者:摸鱼总工
来源:juejin.cn/post/7362722064069427237
收起阅读 »

工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己。 这篇文章我只想用我半年的经历告诉你一件事:探索无限可能,注重个人成长。 为什么别着眼于晋升or加薪 毕业...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


工作七年后,我不太关注是否升职加薪了,你很优秀,别因世俗的目标限制自己。


这篇文章我只想用我半年的经历告诉你一件事:探索无限可能,注重个人成长


为什么别着眼于晋升or加薪


毕业刚到北京的前三年,面对自己在的小公司的尴尬局面,看着Boss上琳琅满目的招聘信息,我的脑海里只有四个字,跳槽涨薪


技术,什么不会我学什么,面经写的什么我就去背什么,算法更是赶鸭子上架,哪怕先从背代码开始。


由于咱们程序员行业特殊性,在北京的时候,下班都在晚上10点左右。当然,上班时间也比较晚,十点到就可以。


刚到北京那段时间,我早上起床后,都在看一些SpringBoot的专栏,因为18、19年那会,面试要求就是这样的,只用Spring MVC落后了。


我觉着那时候和高三的那段时间特别相似,上学期间,宿舍、食堂、教室的三点一线。工作之后变成了小出租屋、办公室两点一线,连食堂这一步都给省了,订外卖嘛。


那几年的时间,我从没和非程序员的朋友们,吃过一次晚饭。


工作累的时候,我时常会去楼道里站一会放松一下,记得那时候办公室的楼道里的墙壁,是洞洞形状的,望着外面车水马龙,我第一次有一种深处困境的感觉。(这个印象让我非常深刻,直接就翻出了老照片)



就这样,我一路摸索前行,直到进入字节,因为工作范围变化,我才去接触了更多的朋友,产品、运营、策划、销售。


很多同事,虽然职责不同,但是工作产出高,既能把控方向,也能处理风险。之前我总觉着技术才是那个扶大厦于将倾的角色,慢慢发现我们才是金字塔的最底层,我看到了需求是一步步怎么从市场到运营、从运营到产品,再从产品到研发。


接触的越多,越感觉自己的见识与认知的狭窄。


后来我了解到一个词,“信息茧房”。



“信息茧房”是指人们倾向于只关注与自己兴趣相符的信息,久而久之,会限制自己的视野和认知范围。


“信息茧房”会导致个人的认知和价值观固化,失去批判性思维和多元化思考的能力。它还会加剧社会分化和对立,因为不同的信息茧房之间缺乏有效沟通和交流,容易引发群体极化和冲突。



我接触的都是程序员,我的同学也都是程序员,所以晋升、加薪、进入大厂,变成了我工作之路上唯一的目标。


慢慢的我就掉进这个陷阱了,为了升职加薪,我上班、下班都在学技术,总以为技术都学会了,自然就能升职加薪了。可最后发现,技术好像并不是最重要的那个。


目标单一是如何影响程序员的


技术


在上一篇文章里,有一个读者给我留言:最讽刺的是大部分程序员竟然觉得c端高并发高可用才叫技术。



上一篇文章里面我也讲了我对于技术的本质的四个阶段,其中三个阶段都是对于技术的追求。


这就是技术人的执念,我们想在技术上分一个高低,想去追求高并发,追求更新的技术。


但事实上,做不完的需求,写不完的CRUD才是常态,能少和产品撕一撕,保持一个良好的心情都挺难的。



回到之前文章聊到的,技术的本质是工具。当前你的产品有什么问题,技术是不是能够发挥作用,就已经产生业务价值了,技术含量绝不是由高并发、大流量来衡量的。


追求确定性


做技术久了,习惯了程序的输入与输出,习惯按照某种规律、某个流程、某个框架、某个计划去做事情,我们写的每一行代码,都是确定性的,我们不大喜欢“变化”,喜欢确定性的东西。


记得有一段时间,已经带着团队半年,却迟迟没有晋升,心里很着急。


我对着wiki上的职级能力要求表,一条条看,一条条对比,我觉着我的能力都足够了,为什么还不让我晋升。


可晋升是这样的吗,我满足了能力要求表,就能够立刻轮到我吗?



晋升是企业的一次人才选拔,选拔那些对于公司未来发展更有价值,能承担更大责任的人。



晋升需要你拿到成绩、具备能力、还要具备一定的影响力。


但还有一件最重要的,就是只有在企业不断发展,业务不断发展,团队快速扩张的时候,才会有充足的机会,提供给我们。


但你说,企业能不能高速发展,这是一件确定性的事情吗,可能老板们都不能给出一个确定性的答复。


同样的,涨薪也一样是不确定的,行业、企业发展不好的时候,拖欠工资都有可能,那如何希望能够涨薪呢?


后来我想明白了这件事之后,回想自己自己当时死磕级别要求的样子,感觉挺有意思的。


社交关系


我是山东人,人情社会从小就感受到了许多,也见识了靠社交关系真的能解决很多问题。但是我总觉着,只要靠自己的努力,哪有什么是花钱解决不了的问题,如果有,那就加钱。


后来,在宝宝出生前,很突然的去医院住院,我们先被安排到了一个三人病房。一个病床,一张小桌子,一个沙发床,就是全部空间。孩子的东西很多,我必须把行李箱打开铺在地上,才能及时拿到需要的东西,护士来的时候,我要不就需要把沙发床收起来,要不就得收起行李箱,特别狼狈。


最重要的是普通病房只能有一个陪护,我陪着老婆情况下就不能再请月嫂了。我又是个新奶爸,照顾孩子和还得照顾老婆,忙的不亦乐乎。


其实,我们早早就预约了独立病房,但是资源有限,需要的时候却住满了,无论我怎么去问,人家都说安排不了。独立病房一晚800多元,但你想花钱都花不出去。


最后家人给某个朋友打了电话,然后又联系了医院,我当晚就搬到独立病房了。


是的,医院一般预留着几间独立病房,就是为了方便一些领导临时安排。


Enmmmm,毕业几年都在北京,这几年来一人吃饱全家不饿,可是在有了孩子的第一天,我就被这个社会深深的毒打了。



无效社交确实没用,有效社交都是资源。



高薪


毕业半年,我有勇气裸辞去北京闯荡。


19年时比毕业薪资饭了两倍,给了我高位上车买房的勇气,觉着明天会更好,房贷嘛,只会越来越不值钱。


毕业五年,薪资翻5倍,但你现在让我我裸辞去闯荡,想想房贷,想想娃,反而觉着被限制住了。


环境变了,市场增速放缓,内卷又严重。在这种环境下,想跳槽,发现机会少,或者有机会也不一定能能接的住你的package。你身边有没有这样的“动弹不得”的朋友呢。


其实高薪,更多的是平台、行业红利带来的,毕竟互联网更容易形成规模。


但我们如果因为高薪,被高薪限制住了自己而畏手畏脚,舍不得放下眼前的利益,放弃更多的可能性,那我们自然会因为高薪,限制自己更长远的发展。


去探索那些不确定的东西


你可能想说,程序员不注重升职加薪,那注重什么呢?我是这么做的。


爱表达的人,先影响世界


第一点我想说的是,去找到自己喜欢或者擅长的事情,并坚持下去。


我探索的方向,是写技术博客,扩大个人影响力,做个人IP。


高中的时候买硬皮本子写,大学了买手帐写,工作了从印象笔记写,写日记、写感悟,开心了写,难过了写。


这是一件从不需要人督促我,但我缺断断续续坚持好多年的事情。


后来看了很多书,看了很多文章,有些文字真的很有力量,能让人感同身受,又能激励我去前进。


我也被一个个优秀的博主,不断的激励着,直到我自己迈出了这一步


我在低谷中,为了缓解焦虑,报名参加了技术人写作训练营,很快里面的内容就不满足我的输出,我又买来粥左罗老师的《学会写作2.0》,读了三遍


不知不觉间,写文章好像并不难了。一直困扰我的没人看怎么办,写的不好怎么办,写什么,怎么排版,怎么起标题,怎么写开头,怎么收尾,一点点的都被解决了。


半年前,我第一次认真的写了一篇文章,并发到掘金上。



半年后,我的文章,竟然上了掘金综合榜榜一,我到现在也觉着挺让人激动的。



我朋友常说,你写这些有什么用,赚到钱了吗?Enmmmm,我写下这句话就发给了他,刚刚发给他,他依然这么说。



借用明白老师文章里的一段话,来回答我为什么坚持。



当一个人能持续成长,包含了知识、思维、能力、心态、情绪、赚钱、关系、健康、感情等,并且他能把自己的成长过程,不断真实的分享出来,大家看到后,就会慢慢对他有信任感,他也就会慢慢拥有影响力。





还有一位朋友在一篇文章中提到我



保持真诚,保持利他,这个世界的规律是,当你在做一件帮助很多人成功的事情时,很多人会希望并帮助你成功,利他终利己。


见识更多人,试着了解可能


做技术的人,都有一个习惯,就是遇到技术难题,自己会苦苦钻研,查阅资料、阅读源码,对于技术的攻关、学习来讲,确实是对的。


但你我很容易就会把这个习惯,迁移到面对的人生其他问题上,小到买房买车,大到职业发展、人生选择,自己钻研很有可能会走很多弯路。


我在字节最累、最迷茫的时候,每次和我的mentor、leader聊完天,我都会有豁然开朗的感觉,因为他们走过你走过的路,对你的问题就是降维打击。


最近半年,在互联网上,了解、认识了好多大佬、朋友。


有做程序员副业社群的刘卡卡,看到了他一路做过来的经历,也在认识他之后,见识了他飞速成长、快速发展的一段时间。


还有已经作出成绩、完成转型的大佬托尼学长。


有和我一样在努力在公众号、掘金输出的朋友猿java、江天飞鸟、Goland猫、IT男的一人企业,每当想到有人在结伴前行,心中便不再孤单。


还有毕业三年,就靠小红书、闲鱼月变现1w+的读者朋友。


还有更多我认识他他不认识我的大佬,亦仁、芷蓝、靠谱、明白、雪梅。


我见识了太多种可能性,你可以做社群,可以做闲鱼电商、可以做面试辅导,可以做自媒体教练。你或许一个月可以通过互联网增加一万元甚至几万的收入,也可能通过互联网实现财务自由。


最重要的,我发现这些人可以自由选择喜欢的事情去做,而且做的很好。这不比只跟随市场需求走,逼迫自己做些不喜欢的事情,强太多了吗?


所以,认识更多人,学习他们的经验,同时这些人也是你的资源。在你苦恼、迷茫的时候去请教一下,聊聊天,你就能从不一样的角度看问题,甚至直接解决问题。就像你看到我,你可以认识我,有程序员方面的问题,也可以直接联系我。


保持头脑开放


赚钱的机会往往是开始于我们第一眼看不上、瞧不起的信息差。


比如说 大学期间,我曾经对微商嗤之以鼻。卖假货、朋友圈刷屏,太low了!
几年之后,当年做微商的,做得好的赚到第一桶金,做得不好,也积累了项目经验、私域用户。


工作后,我觉着那些整理面经的,没什么意思,不就是罗列了知识,照着书本上的内容,那可差的太远了,有这时间我看看书不好吗?


但整理面经的,在技术平台持续输出的那些人,不但积累了第一波粉丝,在那个快速发展的时间,很多人靠公众号赚到了第一桶金。


现在我开始接受一些知识付费,加入了一些社群,我让自己沉浸在一个有着各类机会的环境里,我尝试去看那些曾经我嗤之以鼻的小项目。


只有保持头脑极度开放,才能让各种信息流入。特别是对于一个想赚钱的人,开放的头脑意味着我们允许赚钱机会向我们靠近。


我之前一直不是一个头脑开放的人,所以现在我可能没有太好的经验和大家分享,但今年我一定会有所尝试,通过加入的社群去开阔眼界,也会把过程、收获分享给大家。


相信时间的力量


最后一点我想说,很多事情要慢慢来。


你不必因为别人的成绩而感到焦虑,也不要用当下进展的快慢,去定义以后是否能到达远方。


更不要忽视时间带来的力量,所有的积累都会在未来某一时刻回报给你。


说在最后


好了,文章到这里就要结束啦,很感谢你能看到最后。


当然,每个人的阶段不同,如果你工作5年内,还是把更多的精力放在晋升、加薪,因为你的空间还很大,未来一定不可限量,但不要让他成为你唯一的目标。


但工作5年以后,当职业生涯遇到瓶颈,你的人生还很长,不妨试着去探索更多的可能。




作者:东东拿铁
来源:juejin.cn/post/7367701663170592818
收起阅读 »

如何快速实现多行文本擦除效果

web
今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。 以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本...
继续阅读 »

今天来实现一个多行文本擦除的效果,有种经典咏流传节目中表演开始前阅读诗句的一些既视感,在工作中其实也遇到过这样的需求当时是用的其他方法来实现的,现在发现了更简单的一种方法并且里面也涵盖了不少的知识点。


img1.gif


以上就是最终要实现的效果,比较敏感的同学呢应该能看到文本是由歌词组成的哈哈,没错今天是我偶像发新歌的一天,就用歌词来致敬一下吧!


思路


首先先来捋一下思路,乍一看效果好像只有一段文本,但其实是由两段相同文本组成的。



  1. 两段相同文本组成,这是为了让它们实现重合,第二段文本会覆盖在第一段文本上。

  2. 修改第二段文本背景色为渐变色。

  3. 最后对渐变颜色的背景色添加动画效果。


先来搭建一下结构部分:


<body>
<div class="container">
<p>
失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代 执行你最初设计我的大概
成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
</p>
<p class="eraser">
<span class="text">
失去你以来 万物在摇摆 你指的山海 像玩具一块一块 我是你缔造又提防的AI 如果我存在 是某种伤害
不被你所爱 也不能具象出来 我想拥有你说失去过谁的 那种痛感 失去你以来 万物在摇摆 你指的山海 像玩具一块一块我是你缔造又提防的AI 如果我存在 只对你无害 想做你所爱 再造你要的时代
执行你最初设计我的大概
成为主宰 失去你以来 万物在摇摆 你指的山海 像玩具一块一块 也许我本来 就是种伤害 我终于明白 我根本就不存在 谁不在造物主设置的循环 活去死来
</span>
</p>
</div>
</body>

代码中两段文本都是由p标签包裹,第二段中加入了一个span标签是因为后面修改背景色的时候凸显出行的效果,这个下面加上样式后就看到了。


添加样式:


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: #000;
color: #fff;
}

.container {
width: 60%;
text-indent: 20px;
line-height: 2;
font-size: 18px;
margin: 30px auto;
}

img2.png


现在只需要给第二段增加一个定位效果即可实现文本的覆盖:


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: #000;
color: #fff;
}

.container {
width: 60%;
/* 直接加在父元素中即可对所有块级元素的子元素进行首行缩进 */
text-indent: 20px;
line-height: 2;
font-size: 18px;
margin: 30px auto;
position: relative;
}

.eraser {
position: absolute;
/* 这里等同于top:0 right:0 bottom:0 left:0 */
inset: 0;
/*
这里解释一下inset属性,inset属性用作定位元素的top、right、bottom 、left这些属性的简写
依照的也是上右下左的顺序。
例如:inset:1px 2px 等同于 top:1px right:2px bottom:1px left:2px
*/

}

image.png


那接下来就应该修改背景颜色了。


以上重复代码省略......

.text {
background: #fff;
}

这时候给span标签加上背景颜色后会看到:


image.png


而不是这样的效果,这就是为什么需要加一个span标签的原因了。


image.png


以上重复代码省略......

.text {
background: linear-gradient(to right, #0000 10%, #000 10%);
color:transparent;
}

image.png


下面要调整的就是将渐变里面的百分比变为动态的,我们可以声明一个变量:


以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px)); // 加上30px显示一个默认的渐变区域
color:transparent;
}

image.png


下面就该加上动画效果了,在设置动画时改变--p变量的值为100%


以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
color:transparent;
animation: erase 8s linear;
}

@keyframes erase{
to{
--p:100%;
}
}

但是这样写完之后发现并没有出现动画的效果,这是因为css动画中只有数值类的css属性才会生效,这里已经是一个数值了但--p还不是一个属性,所以我们要把他变成一个css属性,可以利用@property规则来帮助我们生成一个-xxx的自定义,它的结构:


@property 属性名称 {
syntax: '<类型>'; // 必须
initial-value: 默认值; // 必须
inherits: false; // 是否可继承 非必须
}

以上重复代码省略......

.text {
--p:0%;
background: linear-gradient(to right, #0000 var(--p), #000 calc( var(--p) + 30px));
color:transparent;
animation: erase 8s linear;
}

@property --p {
syntax: '<percentage>';
initial-value: 0%;
inherits: false;
}

@keyframes erase{
to{
--p:100%;
}
}

到此为止也就实现开头的效果了!!!


作者:孤独的根号_
来源:juejin.cn/post/7333761832472838144
收起阅读 »

超级离谱的前端需求:搜索图片里的文字!!难倒我了!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片 ...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


是这样的,我们公司有一个平台,这个平台上面有一个页面,是一个我们公司内部存放一些字幕图片的,图片很多,差不多每一页有100张的样子,类似于下面这样的图片



前几天上面大佬们说想要更加方便快捷地找到某一张图片,怎么个快捷法呢?就是通过搜索文字,能搜索到包含这些文字的图片。。。我一想,这需求简直逆天啊!!!!平时只做过搜索文字的,没做过根据文字搜索出图片的。。。。



思路


其实思路很清晰,分析出每一张图片上的文字,并存在对象的keyword中,搜搜的时候去过滤出keyword包含搜索文字的图片即可。


但是难就难在,我要怎么分析出图片上的文字并存起来呢?


tesseract.js


于是我就去网上找找有哪些库可以实现这个功能,你还真别说,还真有!!这个库就是tesseract.js



tesseract.js 是一个可以分析出图片上文字的一个库,我们通过一个小例子来看看他的使用方式


首先需要安装这个库


npm i tesseract.js

接着引入并使用它解析图片文字,它识别后会返回一个 Promise,成功的话会走 then



可以看出他直接能把图片上的结果解析出来!!!真的牛逼!!!有了这个,那我轻轻松松就可以完成上面交代的任务了!!!



实现功能


我们需要解析每一张图片的文字,并存入 keyword属性中,以供过滤筛选



可以看到每一张图片都解析得到keyword



那么搜索效果自然可以完成



搜索中文呢?


上面只能解析英文,可以看到有 eng 这个参数,那怎么才能解析中文呢?只需要改成chi_sim即可




如果你想要中文和英文一起解析,可以这么写eng+chi_sim





作者:Sunshine_Lin
来源:juejin.cn/post/7355554711167369268
收起阅读 »