注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

代码中被植入了恶意删除操作,太狠了!

背景 在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。 事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。 对方...
继续阅读 »

背景


在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。


事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。


对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。


排查过程


由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。


原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。


在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。


于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。


可疑代码


在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。


于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。


但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。


紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:


删除操作


原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。


T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。


找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。


又起波折


本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。


于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:


删除脚本


而在具体的脚本中,有如下执行操作:


删除核心依赖包


这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。


为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。


小结


原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。


当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有

作者:程序新视界
来源:juejin.cn/post/7140066341469290532
的,这点不接受反驳。

收起阅读 »

为什么App独立开发最好别做日记、记账

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。 认清自己的定位 在独立开发刚开始做,羽翼未丰的时候,你对...
继续阅读 »

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。


认清自己的定位


在独立开发刚开始做,羽翼未丰的时候,你对自己的定位就应该是一个游击队队员。这也是独立开发的天然优势,因为小,所以灵活机动。游击战的核心奥义是流动性和速决性。因为你没有一个根据地需要防守,你可以找到一个薄弱的地方进攻。因为这个地方薄弱,所以你集中优势兵力可以很快的决胜。因为你小,所以赚到一笔是一笔。因为你是在边缘薄弱的地方建立的优势,这个小细分市场的利润不足以吸引大部队来,你就有了自己的根据地。你想想红军的根据地都是在什么地方,都是在两省交界的山里。可没听说根据地在上海、北京的。


举个例子,一个小业务,几个月工作量,可以有10万利润。对于一个互联网团队是看不上的。一个完整的互联网团队,一个CEO,一个产品,一个设计,两个研发(前端+后端),一个测试,加上行政,加上公司的运营费用。这一套组合拳摊下来,为了组织的长期利益,短期的一个20万利润的项目他们是看不上的。但是如果你是独立开发,两个人搞两个月,赚10万,你做不做?或者说你能做吗?你能做的,只是赚的不多。但是公司这个事情就没法做。这就是大象踩不死蚂蚁的道理。有人觉得就算两个人两个月赚10万,一个月人均25000也不是什么大钱。确实不是很多的钱,但这也是独立开发普遍心态上的一个问题:你想要的太多。平民独立开发早期最关键的是找到一个持续赚钱的方式,你有实力让自己活下来,后期就可以慢慢发展。而不是一上来就觉得一年要赚100万,然后以一个游击队的姿态去攻打大城市。你要赚大钱,就要有对应的能力。但是很多起步的独立开发者并没有这样的能力,那就是一个不匹配的目标了。


局部创新在笔记领域不是决定性的优势


很多人觉得我做笔记,我有一个想法,我有一个痛点,市面上的笔记没有。我进行了一个局部创新,可能是更好看一点点的设计,或者一个特别的笔记格式。我有一个局部优势,我可以做,我入场。


第一个问题:某些品类的局部优势没有决定性。换一句话说,也许你的确解决了一个其他笔记没有的痛点,但是这个不能转化成你的产品整体优势。不足以转化成足够的收入。


如果你解决了一个普遍痛点,一个高商业价值的痛点,有什么理由现有的成熟笔记app不做呢?请问你作为一个独立开发者开发这个功能要做多久?你有没可能一年里没有任何风声开发一个杀手功能,出来的时候瞬间占领用户心智,用户蜂拥而来?在一个成熟品类里,这样很难的。诺曼底登陆不是游击队能做到的。你要是有这个能力和信心,你就不应该做独立开发,你应该成立公司高举高打。真实的现状是如果你的一个功能受欢迎,成熟的笔记团队花一两个月也就做出来了。


而且笔记类还有一个特点:他有数据积累。用户价值=新体验-旧体验-替换成本。假设你的新体验确实有优势,大于现有产品的旧体验,但是笔记类的替换成本是很高的。我已经在这里积累了好几年的数据,一个新的、个人的、上线没多久的数据积累类,替换成本是很高的。我需要同时愿意抛弃我的旧数据,还需要对你建立信任。 所以除非领头的app犯错误,否则后来的小app是没有机会超过他的。因为他有先发优势,他可以在看到你之后,完善自己,提高自己的旧体验。你不能静态的认为头部的成功的app会停在那里。


成熟品类的高入局门槛


笔记类app自 AppStore 开始有以来,就有人做了。这意味着在你的 app 上线之前,高付费意愿的用户已经付费购买过了。这意味着,如果你要获得存量用户,你需要超越现有品类的成功 app,你需要有更好的整体体验,你还需要有足够全面的运营能力。让这些买了其他 app 的用户愿意选择你。你做笔记 app,一上线,真正的笔记用户心里已经有一个对标的门槛了。本来你在农村盖个楼,是个一层土楼也可以,是个木楼也行,大家都认可是个楼。你在市中心盖一个土楼,大家就会觉得你简陋,功能不全了。用户心智里已经把这个品类的成熟应用当做了一个基线。


再说新的用户。新的用户如果要用一个笔记,上 AppStore 搜,上社交平台搜,肯定看到的大量都是成熟 app 的推荐。他们本来就是历经时代活下来的,自然是得到了用户的认可。既然存在了这么久,自然知道他们的人就多。所以你在自然新增流量上也很劣势。


所以你做一个成熟品类,意味着你要成功,你就必须有一个极具说服力的产品优势,加足够好的产品质量,加足够高效的运营。


我有两个理财项目,一个项目利润年化10%,一个项目利润年后1%,你选哪个?你既然都是在做新产品,为什么要选一个更难的赛道?是你觉得自己命中注定要开发一个笔记app,还是你贫瘠的想象力只想到了做笔记?你确定是在100个产品评估出的最好的方向是笔记?


对比一个新市场,比如最近很火的套壳的 gpt 应用。这是一个全新品类,意味着用户心里是没有对标品的。当用户下载你的 app 时,他不会期待你应该有怎样的功能,他没有对比的对象。这个品类里因为没有头部应用,大家搜索的时候也就看到什么下什么。你会有自然新增流量。因为大家都是差不多时间起步的,其他app不会领先很多,意味着你也有可能建立产品优势,至少你没什么太大的劣势,更有希望建立用户口碑。


长线和短线


投资理财主要有两个流派:长线和短线。长线就关注这个企业未来长期的发展,关注企业的价值。如果这个企业是低估的,就可以持有,因为他们评估出未来这个企业会成长。短期的波动对他们并不构成干扰。大概就是大家口中说的价值投资吧。还有一类是短线,他们不关注未来的情况,买入一只股票只关心这只股票下个月会不会涨,明天会不会涨。因此这个股票的已经100倍PE对他们也没影响,后面有人接盘就可以了。量化交易大多是这样的短线逻辑,于是他们有着高换手率。


长线和短线都是合理的策略。最大的问题是,你不能用抱着长线的心态做短线。这大概就是接盘股民的心态吧,他们持有一只股票的时候觉得这个企业未来会成长。但是短期市场遇冷跌了20%,看到很多人抛了,他们就觉得受不了了,于是割肉离场。买的时候听的是做长线人的意见,卖的时候是跟着做短线的人卖的。


把这个逻辑放到 app 上也是这样的。笔记 app 是一个长线价值,越到后面越值钱,做的越久产品优势越大,用户粘性越高。但是你抱着短线的心态进来做,做了4 个月,收入用户都没起色,你怀疑自己,团队开始有意见,于是你就放弃了。这就是问题所在了,大多数独立开发者没有耐心在一个赛道持续亏损做两年,不具备做长线的心态和财力。


所以我建议独立开发起步的时候多关注短线价值。就是你投入三五个月,能有起色的方向。做三五个月,要不就要赚钱,要不就要有用户口碑。一鸟在手胜过二鸟在林。等你解决了起步的时候生存问题,再考虑农村包围城市的问题。


谈谈番茄钟


也有人觉得番茄钟是独立开发的重灾区。虽然番茄钟似乎是一个红海了,但是我却觉得番茄钟反而是一个可以做的赛道。不知道大家有没有留意一下这个有这个功能的app,似乎万物都可以番茄钟。谜底时钟有番茄钟,滴答清单有番茄钟。番茄钟在设计上可以极简,可以是我的番茄,可以是小鸡,可以是面条,可以是像素。


image.png
IMG\_5978.png
IMG\_5979.png
IMG\_5980.png

那么为什么番茄钟可以呢?


番茄钟替换成本低。因为是及时性的工具类,历史积累的番茄时间统计并不太重要。在功能操作上也很简单,核心交互就是点击一下开始计时,25分钟后提醒你要休息5分钟。很容易可以完成一个基础任何。


设计差异化可以成为付费点,市场容量大。 因为交互很简单,所以做出突出的设计成本可以接受。因为这个品类里,设计可以成为卖点,设计又是一个各有所好的事情,因此天然会有很多不同的需求。某个番茄钟可能做的很好用,很好看,但是他不可能吃掉全部用户。一件短袖设计的再好看,也不可能让所有人都买单。


由此我们可以得出一个结论:番茄钟有短线价值。并且番茄钟引入养成和成就体系以后,有可能变成一个长线产品。如果你非要卷,去卷番茄钟吧。


最后


地上有两张钱,一张100,一张10块,你先捡哪张?我想结果是不言而喻的。独立开发者的一个生态位优势就是可以在一个小领域建立极高的人效比。这个小领域有两个可能:一个是这个领域太小太细分,只能容得下小团队做(高人效);这个领域是新的,还没人知道这里行不行,于是小成本的独立开发先做了(高灵活)。建议各位独立开发者如果要做死亡加速三件套的产品的话三思而后行。


作者:没故事的卓同学
来源:juejin.cn/post/7265967971162898487
收起阅读 »

万能的异步处理方案

异步处理通用方案 前言 良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素...
继续阅读 »

异步处理通用方案


前言


良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素。 由此我设计了一套通用的异步处理SDK,可以很轻松的实现各种异步处理


目的


通过异步处理不仅能够保证方法能够得到有效的执行而且不影响主流程


更重要的是各种兜底方法保证数据不丢失,从而达到最终一致性\color{red}最终一致性


优点


无侵入设计,独立数据库,独立定时任务,独立消息队列,独立人工执行界面(统一登录认证)


使用spring事务事件机制,即使异步策略解析失败也不会影响业务


如果你的方法正在运行事务,会等事务提交后再处理事件


就算事务提交了,异步策略解析失败了,我们还有兜底方案执行(除非数据库有问题,消息队列有问题,方法有bug)


组件


kafka 消息队列


xxl job 定时任务


mysql 数据库


spring 切面


vue 界面


设计模式


策略


模板方法


动态代理


流程图


image.png


数据库脚本


CREATE TABLE `async_req` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`application_name` varchar(100) NOT NULL DEFAULT '' COMMENT '应用名称',
`sign` varchar(50) NOT NULL DEFAULT '' COMMENT '方法签名',
`class_name` varchar(200) NOT NULL DEFAULT '' COMMENT '全路径类名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名称',
`async_type` varchar(50) NOT NULL DEFAULT '' COMMENT '异步策略类型',
`exec_status` tinyint NOT NULL DEFAULT '0' COMMENT '执行状态 0:初始化 1:执行失败 2:执行成功',
`exec_count` int NOT NULL DEFAULT '0' COMMENT '执行次数',
`param_json` longtext COMMENT '请求参数',
`remark` varchar(200) NOT NULL DEFAULT '' COMMENT '业务描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_applocation_name` (`application_name`) USING BTREE,
KEY `idx_exec_status` (`exec_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理请求';

CREATE TABLE `async_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`async_id` bigint NOT NULL DEFAULT '0' COMMENT '异步请求ID',
`error_data` longtext COMMENT '执行错误信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_async_id` (`async_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理日志';

异步策略


image.png


安全级别


image.png


执行状态


image.png


流程图


image.png


image.png
image.png

apollo 配置


# 开关:默认关闭
scm.async.enabled=true

# 数据源 druid
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/fc_async?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
spring.datasource.username=user
spring.datasource.password=xxxx
spring.datasource.filters=config
spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=yyy
#静态地址
spring.resources.add-mappings=true
spring.resources.static-locations=classpath:/static/


# 以下配置都有默认值
# 核心线程数
async.executor.thread.corePoolSize=10
# 最大线程数
async.executor.thread.maxPoolSize=50
# 队列容量
async.executor.thread.queueCapacity=10000
# 活跃时间
async.executor.thread.keepAliveSeconds=600

# 执行成功是否删除记录:默认删除
scm.async.exec.deleted=true

# 自定义队列名称前缀:默认应用名称
scm.async.topic=应用名称

# 重试执行次数:默认5次
scm.async.exec.count=5

# 重试最大查询数量
scm.async.retry.limit=100

# 补偿最大查询数量
scm.async.comp.limit=100

用法


1,异步开关
scm.async.enabled=true

2,在需要异步执行的方法加注解 (必须是spring代理方法)
@AsyncExec(type = AsyncExecEnum.SAVE_ASYNC, remark = "数据字典")

3,人工处理地址
http://localhost:8004/async/index.html

注意


1,应用名称
spring.application.name

2,队列名称
${scm.async.topic:${spring.application.name}}_async_queue
自定义topic:scm.async.topic=xxx

3,自己业务要做幂等

4,一个应用公用一个队列
自产自消

5,定时任务
异步重试定时任务(2分钟重试一次,可配置重试次数)
异步补偿定时任务(一小时补偿一次,创建时间在一小时之前的)

效果展示


image.png


image.png


作者:三火哥
来源:juejin.cn/post/7266087843239084090
收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


就着这事,聊聊最近对职场上关于沟通的一些思考:


第一,忌固执己见


职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


第二,不必说服,尊重就好


站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


第三,不懂的领域多听少说


如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


第四,没事多夸夸别人


在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。

作者:程序新视界
来源:juejin.cn/post/7265978883123298363
很有意思,也很有用。

收起阅读 »

AI欣赏-街头少女🔥🔥🔥

描述 💯💯💯 你更喜欢哪一位选手呢? 自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言! 本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。Lo...
继续阅读 »

描述


💯💯💯 你更喜欢哪一位选手呢?


自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言!


本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。

  • Lora:无

  • Embeddings:ng_deepnegative_v1_75t [1a3e]


Prompt



a young woman, street, laughing, ponytails, (hdr:1.3), (muted colors:1.2), dramatic, complex background, cinematic, filmic, (rutkowski, artstation:0.8), soaking wet,



Negative Prompt



(nsfw:2),Multiple people,easynegative,(worst quality:2),(low quality:2),lowres,(monochrome:1.4),(grayscale:1.4),big head,severed legs,short legs,missing legs,acnes,skin blemishes,age spot,backlight,(ugly:1.4),(duplicate:1.4),(morbid:1.2),(mutilated:1.2),mutated hands,(poorly drawn hands:1.4),blurry, (bad anatomy:1.4),(bad proportions:1.4),(disfigured:1.4),(unclear eyes:1.4),bad hands, bad tooth,missing fingers,extra digit,bad body,NG_DeepNegative_V1_75T,glans,EasyNegative:0.5,gross proportions.short arm,(missing arms:1.4),missing thighs,missing calf,mutation,duplicate,morbid,mutilated,poorly drawn cloth,strange finger,bad finger,(mutated hands and fingers:1.4),(text:1.4), bad-artist, bad_prompt_version2, bad-hands-5, bad-image-v2-39000,



基础配置




生成图的效果展示


选手1




选手2




选手3




选手4




选手5




选手6




选手7




选手8




选手9




投票


🌺 请开始诸位的投票吧!评论区见!!


作者:襄垣
链接:https://juejin.cn/post/7223267912727298103
来源:稀土掘金
收起阅读 »

iPhone两秒出图,目前已知的最快移动端Stable Diffusion模型来了

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7...
继续阅读 »

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7GB,近 10 亿参数,端上推理时间往往要接近 2min。


为了解决推理速度问题,学术界与业界已经开始对 SD 加速的研究,主要集中于两条路线:(1)减少推理步数,这条路线又可以分为两条子路线,一是通过提出更好的 noise scheduler 来减少步数,代表作是 DDIM [1],PNDM [2],DPM [3] 等;二是通过渐进式蒸馏(Progressive Distillation)来减少步数,代表作是 Progressive Distillation [4] 和 w-conditioning [5] 等。(2)工程技巧优化,代表作是 Qualcomm 通过 int8 量化 + 全栈式优化实现 SD-v1.5 在安卓手机上 15s 出图 [6],Google 通过端上 GPU 优化将 SD-v1.4 在三星手机上加速到 12s [7]。


尽管这些工作取得了长足的进步,但仍然不够快。


近日,Snap 研究院推出最新高性能 Stable Diffusion 模型,通过对网络结构、训练流程、损失函数全方位进行优化,在 iPhone 14 Pro 上实现 2 秒出图(512x512),且比 SD-v1.5 取得更好的 CLIP score。这是目前已知最快的端上 Stable Diffusion 模型!



论文地址:arxiv.org/pdf/2306.00…
Webpage: snap-research.github.io/SnapFusion


核心方法


Stable Diffusion 模型分为三部分:VAE encoder/decoder, text encoder, UNet,其中 UNet 无论是参数量还是计算量,都占绝对的大头,因此 SnapFusion 主要是对 UNet 进行优化。具体分为两部分:(1)UNet 结构上的优化:通过分析原有 UNet 的速度瓶颈,本文提出一套 UNet 结构自动评估、进化流程,得到了更为高效的 UNet 结构(称为 Efficient UNet)。(2)推理步数上的优化:众所周知,扩散模型在推理时是一个迭代的去噪过程,迭代的步数越多,生成图片的质量越高,但时间代价也随着迭代步数线性增加。为了减少步数并维持图片质量,我们提出一种 CFG-aware 蒸馏损失函数,在训练过程中显式考虑 CFG (Classifier-Free Guidance)的作用,这一损失函数被证明是提升 CLIP score 的关键!


下表是 SD-v1.5 与 SnapFusion 模型的概况对比,可见速度提升来源于 UNet 和 VAE decoder 两个部分,UNet 部分是大头。UNet 部分的改进有两方面,一是单次 latency 下降(1700ms -> 230ms,7.4x 加速),这是通过提出的 Efficient UNet 结构得到的;二是 Inference steps 降低(50 -> 8,6.25x 加速),这是通过提出的 CFG-aware Distillation 得到的。VAE decoder 的加速是通过结构化剪枝实现。




下面着重介绍 Efficient UNet 的设计和 CFG-aware Distillation 损失函数的设计。


(1)Efficient UNet


我们通过分析 UNet 中的 Cross-Attention 和 ResNet 模块,定位速度瓶颈在于 Cross-Attention 模块(尤其是第一个 Downsample 阶段的 Cross-Attention),如下图所示。这个问题的根源是因为 attention 模块的复杂度跟特征图的 spatial size 成平方关系,在第一个 Downsample 阶段,特征图的 spatial size 仍然较大,导致计算复杂度高。




为了优化 UNet 结构,我们提出一套 UNet 结构自动评估、进化流程:先对 UNet 进行鲁棒性训练(Robust Training),在训练中随机 drop 一些模块,以此来测试出每个模块对性能的真实影响,从而构建一个 “对 CLIP score 的影响 vs. latency” 的查找表;然后根据该查找表,优先去除对 CLIP score 影响不大同时又很耗时的模块。这一套流程是在线自动进行,完成之后,我们就得到了一个全新的 UNet 结构,称为 Efficient UNet。相比原版 UNet,实现 7.4x 加速且性能不降。


(2)CFG-aware Step Distillation


CFG(Classifier-Free Guidance)是 SD 推理阶段的必备技巧,可以大幅提升图片质量,非常关键!尽管已有工作对扩散模型进行步数蒸馏(Step Distillation)来加速 [4],但是它们没有在蒸馏训练中把 CFG 纳入优化目标,也就是说,蒸馏损失函数并不知道后面会用到 CFG。这一点根据我们的观察,在步数少的时候会严重影响 CLIP score。


为了解决这个问题,我们提出在计算蒸馏损失函数之前,先让 teacher 和 student 模型都进行 CFG,这样损失函数是在经过 CFG 之后的特征上计算,从而显式地考虑了不同 CFG scale 的影响。实验中我们发现,完全使用 CFG-aware Distillation 尽管可以提高 CLIP score, 但 FID 也明显变差。我们进而提出了一个随机采样方案来混合原来的 Step Distillation 损失函数和 CFG-aware Distillation 损失函数,实现了二者的优势共存,既显著提高了 CLIP score,同时 FID 也没有变差。这一步骤,实现进一步推理阶段加速 6.25 倍,实现总加速约 46 倍。


除了以上两个主要贡献,文中还有对 VAE decoder 的剪枝加速以及蒸馏流程上的精心设计,具体内容请参考论文。


实验结果


SnapFusion 对标 SD-v1.5 text to image 功能,目标是实现推理时间大幅缩减并维持图像质量不降,最能说明这一点的是下图:




该图是在 MS COCO’14 验证集上随机选取 30K caption-image pairs 测算 CLIP score 和 FID。CLIP score 衡量图片与文本的语义吻合程度,越大越好;FID 衡量生成图片与真实图片之间的分布距离(一般被认为是生成图片多样性的度量),越小越好。图中不同的点是使用不同的 CFG scale 获得,每一个 CFG scale 对应一个数据点。从图中可见,我们的方法(红线)可以达到跟 SD-v1.5(蓝线)同样的最低 FID,同时,我们方法的 CLIP score 更好。值得注意的是,SD-v1.5 需要 1.4min 生成一张图片,而 SnapFusion 仅需要 1.84s,这也是目前我们已知最快的移动端 Stable Diffusion 模型!


下面是一些 SnapFusion 生成的样本:




更多样本请参考文章附录。


除了这些主要结果,文中也展示了众多烧蚀分析(Ablation Study)实验,希望能为高效 SD 模型的研发提供参考经验:


(1)之前 Step Distillation 的工作通常采用渐进式方案 [4, 5],但我们发现,在 SD 模型上渐进式蒸馏并没有比直接蒸馏更有优势,且过程繁琐,因此我们在文中采用的是直接蒸馏方案。




(2)CFG 虽然可以大幅提升图像质量,但代价是推理成本翻倍。今年 CVPR’23 Award Candidate 的 On Distillation 一文 [5] 提出 w-conditioning,将 CFG 参数作为 UNet 的输入进行蒸馏(得到的模型叫做 w-conditioned UNet),从而在推理时省却 CFG 这一步,实现推理成本减半。但是我们发现,这样做其实会造成图片质量下降,CLIP score 降低(如下图中,四条 w-conditioned 线 CLIP score 均未超过 0.30, 劣于 SD-v1.5)。而我们的方法则可以减少步数,同时将 CLIP score 提高,得益于所提出的 CFG-aware 蒸馏损失函数!尤其值得主要的是,下图中绿线(w-conditioned, 16 steps)与橙线(Ours,8 steps)的推理代价是一样的,但明显橙线更优,说明我们的技术路线比 w-conditioning [5] 在蒸馏 CFG guided SD 模型上更为有效。




(3)既有 Step Distillation 的工作 [4, 5] 没有将原有的损失函数和蒸馏损失函数加在一起,熟悉图像分类知识蒸馏的朋友应该知道,这种设计直觉上来说是欠优的。于是我们提出把原有的损失函数加入到训练中,如下图所示,确实有效(小幅降低 FID)。




总结与未来工作


本文提出 SnapFusion,一种移动端高性能 Stable Diffusion 模型。SnapFusion 有两点核心贡献:(1)通过对现有 UNet 的逐层分析,定位速度瓶颈,提出一种新的高效 UNet 结构(Efficient UNet),可以等效替换原 Stable Diffusion 中的 UNet,实现 7.4x 加速;(2)对推理阶段的迭代步数进行优化,提出一种全新的步数蒸馏方案(CFG-aware Step Distillation),减少步数的同时可显著提升 CLIP score,实现 6.25x 加速。总体来说,SnapFusion 在 iPhone 14 Pro 上实现 2 秒内出图,这是目前已知最快的移动端 Stable Diffusion 模型。


未来工作:


1.SD 模型在多种图像生成场景中都可以使用,本文囿于时间,目前只关注了 text to image 这个核心任务,后期将跟进其他任务(如 inpainting,ControlNet 等等)。




  1. 本文主要关注速度上的提升,并未对模型存储进行优化。我们相信所提出的 Efficient UNet 仍然具备压缩的空间,结合其他的高性能优化方法(如剪枝,量化),有望缩小存储,并将时间降低到 1 秒以内,离端上实时 SD 更进一步。




参考文献


[1] Denoising Diffusion Implicit Models, ICLR’21


[2] Pseudo Numerical Methods for Diffusion Models on Manifolds, ICLR’22


[3] DPM-Solver: A Fast ODE Solver for Diffusion Probabilistic Model Sampling in Around 10 Steps, NeurIPS’22


[4] Progressive Distillation for Fast Sampling of Diffusion Models, ICLR’22


[5] On Distillation of Guided Diffusion Models, CVPR’23


[6] http://www.qualcomm.com/news/onq/20…


[7] Speed Is All You Need: On-Device Acceleration of Large Diffusion Models via GPU-Aware Optimizations, CVPR’23 Workshop


作者:机器之心
链接:https://juejin.cn/post/7244452476191850557
来源:稀土掘金
收起阅读 »

使用脚本更新 macOS 壁纸,让你每天看到不同的美景🖼️

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。 步骤1:获取unsplash API密钥 首先,你需要注册一个unspla...
继续阅读 »

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。


步骤1:获取unsplash API密钥


首先,你需要注册一个unsplash账户,并申请一个API密钥。这个API密钥将允许你通过编程方式访问unsplash图片库。


步骤2:编写bash脚本


创建一个新的文本文件,然后在其中添加以下代码:

#!/bin/bash

# set the unsplash API access key
access_key="YOUR_UNSPLASH_API_ACCESS_KEY"

# define the query to search for wallpaper images
query="nature"

# search for a random wallpaper image
result=$(/usr/bin/curl -s -H "Authorization: Client-ID $access_key" "https://api.unsplash.com/photos/random?query=$query")

# extract the image URL from the JSON response
image_url=$(echo "$result" | /opt/homebrew/bin/jq -r '.urls.full')

# download the image
/usr/bin/curl -s "$image_url" > ~/Pictures/wallpaper.jpg

# set the image as the desktop wallpaper
osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"$HOME/Pictures/wallpaper.jpg\""

这段代码会使用unsplash API搜索与“nature”相关的随机图片,并将其下载到“~/Pictures/wallpaper.jpg”文件中。然后,它会使用AppleScript将下载的图片设置为桌面壁纸。


步骤3:运行bash脚本


将文件保存为“update-wallpaper.sh”,然后打开终端并导航到该文件所在的目录。运行以下命令以使脚本可执行:

chmod +x update-wallpaper.sh

现在,你可以通过在终端中输入以下命令来运行脚本:

./update-wallpaper.sh

步骤4:设置定时任务

脚本依赖:curl、jq、bash、unsplash,使用 which 获取路径,然后替换脚本里的curl和jq。

which curl
which jq

你可以将该脚本设置为定时任务,以便每天自动更新壁纸。打开“终端”并输入以下命令以编辑cron定时任务:

crontab -e

然后,添加以下行:

0 9 * * * /path/to/update-wallpaper.sh

这将在每天上午9点运行该脚本。

现在,你可以坐下来,放松一下,让你的macOS自动更新壁纸。享受吧!

作者:FreeCultureBoy
链接:https://juejin.cn/post/7226301946839089211
来源:稀土掘金

收起阅读 »

完全免费白嫖 GPT-4 的终极方案!

GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。 大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 ...
继续阅读 »


GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。


大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 25 条消息。。。


要么就去 OpenAI 官网申请 GPT-4 的 API,但是目前申请到 API 的小伙伴非常少,你以为申请到 API 就可以用了吗?GPT-4 的 API 价格超级无敌贵,是 GPT-3.5 价格的 30 倍,你敢用吗?😄


然而,但是,既然我写了这篇文章,肯定是要告诉那一个惊天大幂幂的!


现在完全免费白嫖 GPT-4 的机会来了,不仅可以白嫖,还可以直接作为 API 来调用!


不仅能够作为 API 调用,我还接入了公众号给大家白嫖,你说气人不气人?



下面言归正传,开始手把手教大家如何免费白嫖 GPT-4


gpt4free-ts 介绍


GPT4Free 大家应该都知道吧?它上线几周就在 GitHub 上揽收了接近 4w 的 Star。原因就在于其提供了对 GPT-4 及 GPT-3.5 免费且几乎无限制的访问。该项目通过对各种调用了 OpenAI API 网站的第三方 API 进行逆向工程,达到使任何人都可以免费访问该流行 AI 模型的目的。


这就相当于什么?假设地主家有一个粮仓,你往他家的粮仓偷偷插了一根管子,不停地向外抽米,他全然不知,所以你也不用交钱,一切费用由地主承担


现在接入 GPT-4 的第三方网站就相当于那个地主,懂了吧?


但是这个项目并没有封装 API,而且目前也不太能用了。


作为开发者,我们想要的肯定是 API 啊!这就要提到今天的主角了:gpt4free-ts


这个项目是用 TypeScript 写的,相当于 GPT4Free 的 TypeScript 版本,但是更方便部署,而且封装了 API,简直就是开发者的福音,就他了!


这个项目向多个地主家的粮仓插了管子,其中最强大的地主就是 forefront.ai,这个地主家的粮仓里就包含了 GPT-4 这个香饽饽,而且还有 Claude,就嫖他了!


除了 forefront 之外,它接的粮仓还挺多的。。



大批量注册临时邮箱


forefront 的 GPT-4 模型是有限制的,每个账号每 3 小时内只能发送 5 条消息


所以接下来需要用到一个非常神奇的服务叫 RapidAPI你可以通过这个 API 来获取无穷无尽的临时邮箱,然后再用这些无穷无尽的临时邮箱去注册无穷无尽的 forefront 账号。


这么一说,你是不是就悟了?哈哈哈


首先你需要在这里注册一个账号并登录:rapidapi.com/calvinlovel…


然后需要在 Pricing 页面开启订阅:



一般情况下订阅免费套餐即可,一天可以调用 100 次。


如果你有更高的需求,可以考虑订阅更高级的套餐(比如你的用户数量特别多)。


订阅完了之后,你就能看到 API Key 了。这个 Key 我们后面会用到。



Sealos 云操作系统介绍


单机操作系统大家应该都知道吧?Windows、macOS、Linux 这些都属于单机操作系统,为什么叫单机操作系统呢?因为他的内存啊,CPU 啊,都在一台机器上,你不可能用其他机器的内存和 CPU。


那么什么是云操作系统呢?就是把一群机器的 CPU 和内存看成一个整体,然后给用户提供一个交互界面,用户可以通过这个交互界面来操作所有的资源。


懂 K8s 的玩家可能要说了:这个我懂,K8s 就可以!


如果我们的目标愿景是一个云操作系统,K8s 充其量只能是这个云操作系统的内核,就像 Linux 内核一样。完整的云操作系统需要一个像 Windows 和 Ubuntu 操作系统那样的交互界面,也就是操作系统发行版


对于云操作系统来说,Sealos 就是那个发行版。



链接:cloud.sealos.io




有人可能会把云操作系统理解成“Web 界面”,但其实不是,Sealos 云操作系统完全是类似于 Windows 和 macOS 桌面的那种逻辑,并不是 Web 界面。我只需要点几下鼠标,一个应用就装好了,老夫并不知道什么容器什么 K8s。


数据库也一样,小鼠标一点,一个分布式数据库就装好了。


我知道,这时候云原生玩家要坐不住了,您别着急,看到桌面上的终端了没?



终端只是这个云操作系统中的一个 App 而已。同理,容器管理界面仍然可以作为云操作系统的 App,我管你是 Kubernetes Dashboard、Rancher、KubeSphere 还是 Kuboard,都可以作为 App 装在这个云操作系统中。这时候对于云原生专家而言,仍然可以命令行咔咔秀操作,也可以通过各种管理界面来管理容器。


云操作系统嘛,就是要什么人都能用才行,不管你是什么角色,都能在这个操作系统里找到你想要的 App 去完成你的使命


安装 gpt4free-ts


接下来才是这篇文章的重头戏。


我要教大家如何在 Sealos 中点几下鼠标就能安装一个 gpt4free-ts 集群


没错,就是 gpt4free-ts 集群。


什么叫集群?就是说我要运行一群 gpt4free-ts 实例,然后前面加一个负载均衡作为对外的 API 入口。


下面的步骤非常简单,楼下的老奶奶都会,是真的,当时我就在楼下看她操作


首先进入 Sealos 云操作系统的界面:**cloud.sealos.io**。


然后打开桌面上的应用管理 App:



点击「新建应用」:



在启动参数中,按照以下方式进行设置:

1.应用名称随便写,比如 gpt4free。
2.镜像名称是:xiangsx/gpt4free-ts:latest
CPU 和内存需要根据应用的实际情况来填写。这个应用运行之后默认会启动两个 Chrome 浏览器来模拟登录 forefront,每次对话会从里面取一个账号来使用,次数用完了会自动注册新账号(因为每个账号每 3 小时只能发送 5 条信息)。我们可以通过环境变量来修改启动的浏览器数量,所以需要根据你的浏览器数量来确定 CPU 和内存。 我自己把浏览器数量设置为 3,所以需要的内存和 CPU 比较多(后面会告诉你怎么设置环境变量)。
3.实例数根据自己的实际需求填写,我需要接入公众号,粉丝比较多,一个实例才 3 个账号(因为我一个实例跑了 3 个浏览器),根本不够用,所以我开了 3 个实例。
4.容器暴露端口指定为 3000。
5.打开外网访问。



继续往下,展开高级设置,点击「编辑环境变量」:



填入以下环境变量:

rapid_api_key=<rapid_api_key>
DEBUG=0
POOL_SIZE=3


⚠️注意:请将 <rapid_api_key> 替换为你自己的 key。



其中 POOL_SIZE 就是浏览器数量,每个浏览器会登录一个 forefront 账号。你可以根据自己的需要调整浏览器数量,并根据浏览器数量调整 CPU 和内存。如果你不知道怎么调整合适,建议无脑跟着本文操作。



继续,点击「新增存储卷」:



容量只需 1G,挂载路径设置为 /usr/src/app/run




这个存储的作用是为了保存已登录的账号。已经注册的账号 3 个小时以后还可以重新使用,不用再浪费邮箱去注册新账号。



最终点击右上角的「部署应用」,即可完成部署:



最终要等待所有的实例都处于 Running 状态,才算是启动成功了。



点击右边的复制按钮,便可复制 API 的外网地址:



我们来测一下这个 API:



完美!打完收工!


接入微信公众号


什么?你想将这个 API 接入自己的公众号?换个形式吧!直接来看直播吧,我们会通过直播活动手把手教你如何将 GPT-4 免费接入公众号、网页等各种前端。


活动链接:forum.laf.run/d/684


当然,直播过程不会直接教你如何接入公众号,而是“授你🫵以渔”,告诉你如何使用 Laf 来通过各种姿势调用这个 API,最终你也接入公众号也罢,网页前端也罢,那都不是事儿~

作者:米开朗基杨
链接:https://juejin.cn/post/7241790368949190693
来源:稀土掘金
收起阅读 »

雷军写的代码上热搜了!

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。 出于好奇,第一时间点进去围观了一波。 原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。 这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是...
继续阅读 »

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。




出于好奇,第一时间点进去围观了一波。



原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。



这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是!



看一下代码细节,都是类似MOVJMPPUSHLOOP等这些指令。


这不就是自己当年上学时学得瑟瑟发抖痛哭涕零的汇编语言嘛。。



这一瞬间就让我想起了当年微博上的这张图:



早在十几年前,微博上就曾流传过雷军早年所写的一段完整的汇编代码。



当时雷军也转发过,并表示这个程序的第一个版本是他1989年写的,怀念当初写程序的快乐时光。



在网上经常会看到关于雷军代码水平到底如何的讨论帖子。


可以说,看看雷军早年写的这段代码相信心里基本就有答案了。


在早年那个机器硬件水平和性能都十分受限的年代,为了满足某些需求,可以说对编码的程序员提出了非常苛刻的要求。


开发者往往只能使用非常底层的编程语言去实现某些程序,同时还需要把代码优化做到极致,这本就非常考验程序员的基本功和编程底子。


各方面信息都显示,作为一个程序员来说,雷军不仅仅是合格,说他是非常厉害的大神那也丝毫没有毛病。


就像这次热搜话题下稚晖君大佬的一篇动态所言,雷军作为老一代程序员的代表和创业楷模,确实值得敬佩。



文章的最后,我们也找到了当年雷军所写的这段汇编代码的完整版,这里也分享给大家。


咳咳,前方高能!!!


作者:CodeSheep
链接:https://juejin.cn/post/7265679390242537512
来源:稀土掘金
收起阅读 »

吐槽大会,来瞧瞧资深老前端写的垃圾代码

忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了...
继续阅读 »

忍无可忍,不吐不快。


本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


知道了什么是烂代码,才能写出好代码。


别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。

优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


---------------------------------------------更新------------------------------------------------


集中回答一下评论区的问题:


1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


文件命名千奇百怪


同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。




组件职责不清


还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?




条件渲染逻辑置于底层


这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。




滥用、乱用 TS


项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。




留下大量无用注释代码和报错代码


感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。






丑陋的、隐患的、无效的、混乱的 css


丑陋的:没有空格,没有换行,没有缩进


隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合




一个文件 6 个槽点


槽点1:代码的空行格式混乱,影响代码阅读


槽点2:空函数,写了函数名不写函数体,但是还调用了!


槽点3:函数参数过多不优化


槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名




变态的链式取值和赋值


都懒得说了,各位观众自己看吧。




代码拆分不合理或者不拆分导致代码行数超标


能写出这么多行数的代码的绝对是人才。


尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。




这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。




杂七杂八的无用 js、md、txt 文件


在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


实在受不了干脆建个文件夹放一块,看起来也要舒服多了。




less、scss 混用


这是最奇葩的。




特殊变量重命名


这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。

const G = window;
const doc = G.document;

混乱的 import


规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。




写在最后


就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


不过说这么多,成事在人。


不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

作者:北岛贰
链接:https://juejin.cn/post/7265505732158472249
来源:稀土掘金
收起阅读 »

接口测试神器:ApiKit

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具1.背景 作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确...
继续阅读 »

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具

1.背景


作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确实非常出色。


但是在整个软件开发过程中,接口调试只是其中的一部分,还有很多事情 Postman 是无法完成的,或者无法高效完成,比如:接口文档定义、Mock 数据、接口自动化测试等等。


今天给大家推荐的一款神器: ApiKit=API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作

1.1聊一聊接口管理的现状


对于接口管理的现状来说,目前行业大部分采取的解决方案有如下几种:

1.使用 **Swagger**管理接口文档。 

2. 使用 Postman 调试接口。

3.使用 **RAP或Easy Mock**来进行 **Mock**数据。 

4. 使用 JMeter 做接口自动化测试。


而上述的接口管理手段,咋一看,貌似没有什么问题,但仔细分析,不难发现,当中存在的问题还真不少,比如要维护不同工具,并且这些工具之间数据一致性非常困难、非常低效。这里不仅仅是工作量的问题,更大的问题是多个系统之间数据不一致,导致协作低效,频繁出问题,开发人员、测试人员工作起来也痛苦不堪。


设想一下这样的一个协作流程(官方示例):



1. 开发人员在Swagger定义好文档后,接口调试的时候还需要去 Postman 再定义一遍。 2. 前端开发Mock 数据的时候又要去RAPEasy Mock定义一遍,手动设置好 Mock 规则。 3. 测试人员需要去 JMeter定义一遍。 4. 前端根据 RAPEasy Mock定义 Mock 出来的数据开发完,后端根据 Swagger定义的接口文档开发完,各自测试测试通过了,本以为可以马上上线,结果一对接发现各种问题:原来开发过程中接口变更,只修改了 Swagger,但是没有及时同步修改 RAPEasy Mock。 5. 同样,测试在 JMeter 写好的测试用例,真正运行的时候也会发现各种不一致。 6. 时间久了,各种不一致会越来越严重。

ApiKit介绍

官方对ApiKit定位是,API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作 

结合 API 设计、文档管理、自动化测试、监控、研发管理和团队协作的一站式 API 生产平台,从个人开发者到跨国企业用户,Apikit 帮助全球超过50万开发者和10万家企业更快、更好且更安全地开发和使用 API 

概括来讲,ApiKit 常用功能分为五类: 

1.智能且强大的 Mock 前端团队可以在 API 还没开发完成的情况下,借助 Mock API 实现预对接,加速开发进程。测试团队可以通过 Mock API 解耦不必要的系统,完成集成测试 

2.快速生成和管理所有 API 文档 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

3. 零代码自动化测试 Apikit 提供了 API 测试功能,支持自动生成测试数据,能够通过Javascript 对请求报文、返回结果等进行加解密、签名等处理;提供强大、易用的企业级 API 自动化测试解决方案,5分钟快速上手,提高 95% 以上回归测试效率,人人皆可使用的“零代码”自动化测试平台; 

4. 领先的 API 团队协作功能 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

5.还有更多的 Devops 功能 API 异常监控,对接CI/CD、DevOps 平台,支持主流IM ,也可通过自由拓展。 

ApiKit 小试牛刀 

接下来,带着大家,简单体验一下ApiKit的使用。

Apikit 有三种客户端,你可以依据自己的情况选择。三种客户端的数据是共用的,因此你可以随时切换不同的客户端。

我们推荐使用新推出的 Apikit PC 客户端,PC端拥有线上产品所有的功能,并且针对本地测试、自动化测试以及使用体验等方面进行了强化,可以提供最佳的使用感受。 



我们建议对本地测试有需求的用户使用PC端,可满足更多本地测试需求。



发起 API 测试


进入 API 文档详情页,点击上方 测试 标签,进入 API 测试页,系统会根据API文档自动生成测试界面并且填充测试数据。




填写请求参数


首先填写好请求参数。



请求头部


您可以输入或导入请求头部。批量导入的数据格式为 key : value ,一行一条header信息,如:

Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json
Date: Mon, 30 Dec 2019 20:49:45 GMT

请****求体


请求体提供了五种类型:

  1. Form-data(表单)

  2. JSON

  3. XML

  4. Raw(自定义文本类型数据)

  5. Binary(字节流、文件参数)


产品中提供了的 JSON 和 XML 编辑器,当您已经在 API 文档中定义好 API 的请求数据结构时,只需要在测试界面填写各个字段的值,系统会自动转换为相应的 JSON 和 XML 结构的请求数据。


Query 参数


Query参数指的是地址栏中跟在问号?后面的参数,如以下地址中的 user_name 参数:

/user/login?user_name=jackliu

批量导入的数据格式为 ?key=value ,通过&分隔多个参数,如:

api.eolinker.com/user/login?user_name=jackliu&user_password=hello

REST参数

REST参数指的是地址栏被斜杠/分隔的参数,如以下地址中的user_name、user_password参数。

/user/login/{user_name}/{user_password}

注意,只需要在URL中使用 {} 将REST参数括起来,下方的请求参数名中不需要使用 {} 。


处理脚本


脚本分为 前置脚本后置脚本 两种,分别对应 API 请求前 和 返回数据后 的两个阶段。您可以通过编写 Javascript 代码,在 API 前置脚本中改变请求参数,或者是在 API 后置脚本中改变返回结果。


脚本常用于以下几种情况:

1.API 请求前对请求参数进行复制、加解密等操作,比如进行Body进行整体签名 

2. API 返回结果后对结果进行解密等


发起的API请求会依次经过以下流程。其中如果您没有编写相应的API脚本,则会略过API脚本处理阶段。



管****理 Cookie


当您测试需要 Cookie 的 API 时,可以先进行一次 API 登录或者在 Cookie 管理里添加所需的 Cookie 信息,系统会自动将 Cookie 储存起来,下次测试其他相同域名的 API 时会自动传递 Cookie 请求参数。




查看测试结果


填写好请求参数后,点击测试按钮即可得到测试报告,报告包括以下内容:

1.返回头部 

2. 返回内容 

3.实际请求头部 

4. 实际请求内容 

5.请求时间分析




快速生成mock


在高级mock页面,选择添加为mock,可快速生成mock。



将测试用例请求参数和返回参数自动带到mock的请求报文和响应报文中。



ApiKit 更多特性


新建 API 文档



团队协作,API分享



高级mock



创建自动化测试



API 异常警告



环境管理



前后置脚本



创建项目



APIHub




关于 ApiKit 的更多功能,值得你来尝试体验!


传送门:


http://www.eolink.com/?utm_source…


小结


虽然 ApiKit 目前有些功能还并不完善,但整的来说,ApiKit 还是不错的,也为接口开发调试测试提供了一种效率更佳的的解决方案。

作者:CV_coder
链接:https://juejin.cn/post/7237024604962766909
来源:稀土掘金
收起阅读 »

新时代,你需要了解一下苹果的 VisionOS 系统

iOS
这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。 沉浸的光谱。 Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在...
继续阅读 »

这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。


沉浸的光谱。


Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在与周围环境保持联系的同时与您的应用互动,或者完全沉浸在您创造的世界中。您的体验可以是流畅的:从一个窗口开始,引入 3D 内容,过渡到完全身临其境的场景,然后马上回来。


选择权在您手中,这一切都始于 visionOS 上的空间计算构建块。




窗口(Windows)


您可以在 visionOS 应用程序中创建一个或多个窗口。它们使用 SwiftUI 构建,包含传统视图和控件,您可以通过添加 3D 内容来增加体验的深度。


体积(Volumes)


使用 3D 体积为您的应用添加深度。 Volumes 是一种 SwiftUI 场景,可以使用 RealityKit 或 Unity 展示 3D 内容,从而创建可从共享空间或应用程序的完整空间中的任何角度观看的体验。


空间(Space)


默认情况下,应用程序启动到共享空间,在那里它们并排存在——很像 Mac 桌面上的多个应用程序。应用程序可以使用窗口和音量来显示内容,用户可以将这些元素重新放置在他们喜欢的任何位置。为了获得更身临其境的体验,应用程序可以打开一个专用的完整空间,其中只会显示该应用程序的内容。在完整空间内,应用程序可以使用窗口和体积、创建无限的 3D 内容、打开通往不同世界的门户,甚至可以让某人完全沉浸在某个环境中。




Apple 框架 - 扩展空间计算


SwiftUI


无论您是要创建窗口、体积还是空间体验,SwiftUI 都是构建新的 visionOS 应用程序或将现有 iPadOS 或 iOS 应用程序引入该平台的最佳方式。凭借全新的 3D 功能以及对深度、手势、效果和沉浸式场景类型的支持,SwiftUI 可以帮助您为 Vision Pro 构建精美且引人入胜的应用程序。 RealityKit 还与 SwiftUI 深度集成,以帮助您构建清晰、响应迅速且立体的界面。 SwiftUI 还可以与 UIKit 无缝协作,帮助您构建适用于 visionOS 的应用程序。


RealityKit


使用 Apple 的 3D 渲染引擎 RealityKit 在您的应用程序中呈现 3D 内容、动画和视觉效果。 RealityKit 可以自动调整物理光照条件并投射阴影、打开通往不同世界的门户、构建令人惊叹的视觉效果等等。为了创作您的材料,RealityKit 采用了 MaterialX,这是一种用于指定表面和几何着色器的开放标准,由领先的电影、视觉效果、娱乐和游戏公司使用。


ARKit


在 vision Pro 上,ARKit 可以完全了解一个人的周围环境,为您的应用提供与周围空间交互的新方式。默认情况下,ARKit 支持内核系统功能,您的应用程序在共享空间中时会自动受益于这些功能——但是当您的应用程序移动到完整空间并请求许可时,您可以利用强大的 ARKit API,例如平面估计、场景重建、图像锚点、世界轨道和骨骼手部轨道。所以在墙上泼水。从地板上弹起一个球。通过将现实世界与您的内容融合在一起,打造令人惊叹的体验。


Accessibility


visionOS 的设计考虑了可访问性,适用于希望完全通过眼睛、声音或两者的组合与设备交互的人。对于喜欢以不同方式导航内容的人,Pointer Control 允许他们选择食指、手腕或头部作为替代指针。您可以使用已在其他 Apple 平台上使用的相同技术和工具为 visionOS 创建易于访问的应用程序,并帮助使 vision Pro 成为每个人的绝佳体验。




您需要的所有工具。


Xcode


visionOS 的开发从 Xcode 开始,其中包括 visionOS SDK。将 visionOS 目标添加到您现有的项目或构建一个全新的应用程序。在 Xcode 预览中迭代您的应用程序。在全新的 visionOS Simulator 中与您的应用程序交互,探索各种房间布局和照明条件。创建测试和可视化以探索空间内容的碰撞、遮挡和场景理解。


reality composer Pro


探索全新的 reality composer Pro,旨在让您轻松预览和准备 visionOS 应用程序的 3D 内容。随 Xcode 一起提供的 reality composer Pro 可以帮助您导入和组织资产,例如 3D 模型、材料和声音。最重要的是,它与 Xcode 构建过程紧密集成以预览和优化您的 visionOS 资产。


Unity


现在,您可以使用 Unity 强大、熟悉的创作工具来创建新的应用程序和游戏,或者为 visionOS 重新构想现有的 Unity 创建的项目。除了熟悉的 Unity 功能(如 AR foundation)之外,您的应用程序还可以获得 visionOS 的所有优势,例如直通和动态注视点渲染。通过将 Unity 的创作和模拟功能与 RealityKit 管理的应用程序渲染相结合,使用 Unity 创建的内容在 visionOS 上看起来和感觉起来就像在家里一样。




您的 visionOS 之旅从这里开始。


visionOS SDK 本月晚些时候与 Xcode、visionOS 模拟器、reality composer Pro、文档、示例代码、设计指南等一起发布。


为 visionOS 做准备


无论您已经在 App Store 上拥有应用程序,还是这是您第一次为 Apple 平台开发应用程序,您现在都可以做很多事情来为 visionOS SDK 的到来做好准备。了解如何更新您的应用程序并探索现有框架,让您更轻松地开始使用 visionOS。


Prepare for visionOS


了解 visionOS


visionOS 拥有一流的框架和工具,是帮助您创造令人难以置信的空间体验的完美平台。无论您是在构想游戏、构建媒体体验、设计与 SharePlay 的连接和协作时刻、创建业务应用程序,还是更新您的网站以支持 visionOS,我们都有会议和信息来帮助您制定计划。为第 46 场 WWDC23 会议准备好 visionOS SDK,以帮助您了解平台开发、空间体验设计以及测试和工具。


Learn about visionOS


与苹果合作


在为 visionOS 开发应用程序和游戏时,获得 Apple 的直接支持。了解即将举行的活动、测试机会和其他计划,以支持您为此平台创造令人难以置信的体验。


Learn about working with Apple


#visionOS #苹果MR #苹果VR #苹果AR


翻译原文地址


作者:稻草人家
链接:https://juejin.cn/post/7241393511618347045
来源:稀土掘金
收起阅读 »

Mac效率神器Alfred Workflows

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率, 这里简单介绍下 Alfred 的工作流 背景&效果展示 作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行...
继续阅读 »

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率,
这里简单介绍下 Alfred 的工作流



背景&效果展示


作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行转换。
每次打开浏览器找到网址,然后复制内容进行转换都需要 5秒以上的时间。
有什么快捷的方式能帮助我们快速的进行这个操作吗,这里我想到的 Alfred 的工作流。
Alfred工作流可以直接脚本开发,下面是工作流开发完的效果。


1)首先唤起Alfred输入框,这里看自己设置的快捷键了
2)输入这里工作流对应的keyword (tm或tmt)然后空格输入需要转换的内容
3)回车键将转换之后的内容复制到剪切板


这个简单的工作流可以实现linux类型的时间戳转换成 yyyy-MM-dd HH:mm:ss 类型的时间字符串,
也可以将 yyyy-MM-dd HH:mm:ss 类型的时间字符串转换成时间。
转换完的内容会自动放到剪切板里面,可以直接使用 command + v 进行粘贴,也可以使用Alfred的历史剪切板进行复制。


工作流开发


1、创建空的工作流


为了简单快捷这里使用的是python开发的这个功能。
开发这个工作流首先需要再 Alfred 面板上创建一个空的工作流
Alfred -> Preferences -> Workflows -> 左下角的 + -> Blank Workflows
如下图: 



2、添加流程节点


这里使用的是 Script Filter类型的节点,可以支持keyword触发。
节点配置如下,keyword 为触发的命令关键字。下面是运行命令的配置 Script输入框中写需要运行的命令,
这里使用 {query} 方式将转换的内容传递给python脚本。 



复制到剪切板的节点如下,创建好节点用线连接就行。 



3、python脚本开发


脚本的位置放到当前工作流的根目录就行,这样不行写绝对路径,也方便工作流的导出。
1. 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 


2.Alfred官方提供了一个python类库方便开发工作流。在工作流根目录执行命令

pip install --target=. Alfred-Workflow

3.创建脚本 timestamp_2_time.py,然后开发对应的代码即可 。代码里面有注释大家可以看下

# -*- coding: utf-8 -*-
import sys
from datetime import datetime
from workflow import Workflow, ICON_CLOCK # 导包

def main(wf):
query = wf.args[0] # 获取传入的参数,这里能获取到需要转换的呢绒
if not query:
return
# 时间戳转时间字符串的方法
d = datetime.fromtimestamp(int(query) / 1000)
str1 = d.strftime("%Y-%m-%d %H:%M:%S")
'''
调用框架的方法添加运行的结果
可选参数是标题、副标题,arg是下一个节点的入参,icon是这个item展示的图标
如果有多个结果可以放多个,然后通过上下键选择
'''
wf.add_item(title=query, subtitle=str1, arg=str1, valid=True, icon=ICON_CLOCK)
# 展示结果内容list
wf.send_feedback()


if __name__ == '__main__':
'''构造 Workflow 对象,运行完退出
'''
wf = Workflow()
sys.exit(wf.run(main))

通过这几行简单的代码实现了时间戳转换成时间的小功能,
相比于以前的使用网页的形式,这个工作流可以将时间缩短到1秒,每次为你省下 4 秒钟的时间 😂😂😂


debug


开发的时候可能会遇到bug,可以通过下图方式打开运行日志查问题。 



开发好的工作流要使用直接导入就行


tmwf.tar
下载之后 tar -xvf tmwf.tar 解压导入就行。
这里需要注意下,我本地的python路径是 /usr/local/bin/python 大家需要换成自己的python路径。

作者:程序员大鹏
链接:https://juejin.cn/post/7252541723149238330
来源:稀土掘金
收起阅读 »

axios使用异步方式无感刷新token,简单,太简单了

🍉 废话在前 写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。 下面接着分析一下踩到的坑以及解决思路 🍗 接着踩坑 我...
继续阅读 »

🍉 废话在前


写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。




下面接着分析一下踩到的坑以及解决思路


🍗 接着踩坑


我按照之前传统的方式在返回拦截器里面进行token刷新,正常的数据可以返回,但是这个时候会有比较麻烦的地方,就是请求的数据可以在拦截器里面得到,但是不能渲染到界面上(看到这里的时候我是懵的)。


看一下代码

service.interceptors.response.use(
response => {
const res = response.data
//刷新token的时候,可以从这里拦截到新数据,但是没有显示在页面上
console.log('拦截数据:',res)
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
error => {
switch (error.response.status) {
case 401:
MessageBox.confirm('身份认证已过期,是否刷新本页继续浏览?', '提示', {
confirmButtonText: '继续浏览',
cancelButtonText: '退出登录',
type: 'warning'
}).then(() => {
axios.post('/api/token/refresh/', {
refresh: localStorage.getItem("retoken")
}).then(response => {
let res = response.data || {}
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = '';
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
window.location.reload();
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
}
}).catch(err => {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '信息认证失败,请重新登录',
duration: 0
});
})
}).catch(() => {
router.push({
path: "/login",

})
Message({
message: '已退出',
type: 'success',
})
localStorage.clear();
});
break;
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

懵归懵,好在已经发现这个问题了,剩下的怎么解决呢?


当时想的是和后端配合,让后端直接发一个token过期的时间戳给我,我直接把这个时间戳放到localStrage里面,通过这个localStrage,直接在前端进行判断token的过期时间进行请求拦截,如果当前请求的时间大于了这个localStrage里面的时间,就说明token过期了,我这边就需要重新请求token了,而不要后端去进行token的验证。


信心满满的弄了一下,发现行不通,token过期的时候,后端直接一个错误401,前端就又回到解放前了。而且用时间戳的方式很容易出现bug,且在前端进行token时长的验证,很容易出现问题。因此,我还是觉得再研究一下上面那一段代码。


当然踩到坑不只是这个,还有百度和chatGPT,真的,看了一下没有找到一个可行的,总结一下主要有以下几种

  1. 状态管理vuex

  2. 路由router

  3. 时间戳(和我刚刚那种方式差不多)

  4. 依赖注入inject

  5. 刷新界面(我最开始那种方式,但是刷新的时候会出现页面白屏,且用户如果在页面上有一些自己输入的数据也会被清空,用户体验感不好)


以上方式我先不管行不行,但是麻烦是肯定的,做前端讲究的就一个字:“懒”
不是,应该是 “高效快捷”
所以这些方式就pass掉了


只能想想为什么拦截器里面可以得到数据,为什么页面位置得不到数据了


🥩 解决思路


下面是我的解决思路,有不对的地方还请看到的大神指出来一下😁


当用户发起请求的时候,因为刷新token的http状态码是401,这个时候axios的响应拦截器就直接进行错误捕获了,到了这里,因为数据已经返回了,但是因为是错误数据,页面得到的这个数据不可用且当前请求已经结束了,当然这里对状态码401是进行处理了的(应该获取token了)。


采用普通的获取方式来获取token,因为异步的原因,我们获取token的同时页面也在做刷新,token获取的同时,界面也刷新完毕了(但是是没有数据的,不做错误捕获会报错),因此我们在获取token完毕,且用新的token去获取数据时,拦截器里面会有数据,但是界面已经休息了,就不会把拦截器里面的新数据刷新到页面了。


因此这个地方需要对获取token的过程进行一下请求阻塞,把获取token的请求变成同步的。到这里就差不多了。直接把响应拦截器里面的error函数变成同步不就行了吗,async + await可以出来了。


以上是自己当时的想法,简单说来就是 页面刷新需要慢我获取token一步 ,通过这个方式也确实做到了无感刷新🤣


希望以上的能帮助到你,有什么好的思路也欢迎评论区指出。


🍓完整代码


这个是用了2.13.2版本的element-ui以及nprogress的一个axios代码模块,包含了一个下载文件的模块。


如果需要的话可以根据自己的需求来进行修改

import axios from 'axios'
import {
Message,
Loading,
Notification,
} from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import router from '@/router/index'
const baseURL = '/api'
const service = axios.create({
baseURL,
timeout: 6000
})
NProgress.configure({
showSpinner: false
}) // NProgress Configuration
let loadingInstance = undefined;
service.interceptors.request.use(
config => {
NProgress.start()
loadingInstance = Loading.service({
lock: true,
text: '正在加载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return config;
},
error => {
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
const res = response.data

if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失效',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
async error => {
switch (error.response.status) {
case 401:
const err401Data = error.response.data || {}
if (err401Data.code !== "token_not_valid") {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份信息认证失败,请您重新登录',
duration: 0
});
return
}
try {
const res = await service.post('/token/refresh/', {
refresh: localStorage.getItem("retoken")
})
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = ''
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return service(error.response.config)
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '当前身份认证信息已失效,请您重新登录',
duration: 0
});
}
} catch (error) {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份认证失败,请您重新登录,失败原因:' + error.message,
duration: 0
});
}

break
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

// 文件下载通用方式
export const requestFile = axios.create({
baseURL,
timeout: 0, //关闭超时时间
});

requestFile.interceptors.request.use((config) => {
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token") //携带的请求头
config.responseType = 'blob';
loadingInstance = Loading.service({
lock: true,
text: '正在下载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
return config;
});

requestFile.interceptors.response.use(
(response) => {
let res = response.data;
if (loadingInstance) {
loadingInstance.close()
}
// const contentType = response.headers['content-type'];//获取返回的数据类型
let blob = new Blob([res], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" //文件下载以及上传类型为 .xlsx
});
let url = window.URL.createObjectURL(blob);
// 创建一个链接元素
let link = document.createElement('a');
link.href = url;
link.download = '产品列表.xlsx'; // 自定义文件名
link.click();


},
(err) => {
Message({
message: '操作失败,请联系管理员',
type: 'error',
})
if (loadingInstance) {

loadingInstance.close()
}
}
);


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

🎉前端开发书籍推荐🎉

文章首发公众号:萌萌哒草头将军,欢迎关注 🎉SolidJS响应式原理和简易实现🎉 做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。 💎《JavaScript高级程序设计》 推荐指数:⭐⭐⭐⭐⭐...
继续阅读 »

文章首发公众号:萌萌哒草头将军,欢迎关注



🎉SolidJS响应式原理和简易实现🎉


做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。


💎《JavaScript高级程序设计》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容扎实,且不深奥。


这本书在我大学的时候就已经买了,当时经济实力有限,买的是影印版,不过也被我研读了多遍,并且做了详细的记录。这本书见证了我的前端之路,一直被我珍藏至今。




现在已经出了第四版了。前端入门进阶宝典,所以也被广大开发者称为《红宝书》。


💎《Javascript权威指南》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容全面,讲解详细,配合红宝书效果更佳。


该书被称为《犀牛书》,书如其名,真的是权威指南,即使你买了红宝书,我也推荐你买一本,因为这两本对相同的知识讲解,侧重点不同。


比如对于闭包,红宝书很详细,从作用域,到作用域链,再到活动对象,讲解由浅到深,十分详细,而《犀牛书》中仅仅是给出闭包的概念,然后举例说明差异。


但是《犀牛书》对于类型转换toStringvalueof讲解则十分详细,《红宝书》则浅浅的带过。


《犀牛书》更像是一本字典,有不懂的问题,可以及时查漏补缺。


💎《图解HTTP》




推荐指数:⭐⭐⭐⭐


推荐理由:图文并茂,简洁明了


这本书作为第三本推荐,绝不是空穴来风,大量的图来解释枯燥的概念,形象生动,老少皆宜。


前端开发一大部分的时间都是在和后端的接口打交道,而Http无疑是沟通的桥梁。


读完这本书,你将会了解到网络分层模型Http协议和TCP/IP的关系、后端的数据怎么从服务端到达浏览器的、常用Http状态码的含义、请求头的各种含义、你输入url浏览器发生了什么等热门面试题的答案。


我买的是三件套《图解Http》《图解TCP/IP》《图解网络硬件》,《图解网络硬件》一点也不推荐,不懂的硬件直接百度吧,还是彩色图片。如果你是做运维相关的前端开发,《图解TCP/IP》同样值得一看。




💎《数据结构与算法JavaScript描述》




推荐指数:⭐⭐⭐⭐


推荐理由:进阶利器,闭眼入就对了。


大多数同学选择前端,主要还是因为数据结构和算法方面比较薄弱,但是这本书却使用了简洁的方法实现了各种数据结构和算法。


对于难懂的数据结构,有详细的结构图解释,是我读过最容易理解的版本了,不过这本书目前还是ES5语法版本实现,我在前面的文章中使用ES6语法实现过,并做了部分笔记。


👉【数据结构】我的学习笔记




💎《JavaScript设计模式》




推荐指数:⭐⭐⭐


推荐理由:常见的设计模式都有,讲解的比较简单易懂,但是实现比较简陋。


这本书是你入门中级后继续提升的有利法宝,不管什么框架,底层都逃不出两三个设计模式的,所以十分推荐你进阶的时候去读它。


目前有两本名为《JavaScript设计模式》的书,我买的是徐涛翻译版本影印版(和红宝书一起买的),但是最近查阅发现流行的是张容铭著作的版本,这里请自行斟酌买哪个版本。


我买的这个版本将设计模式分为创建型、行为型和结构型三种,前面部分分别讲解了十三种设计模式,后半部分讲解了老牌框架JQuery设计的各种设计模式,虽然从现在的情况看JQuery已经凉了,但是它的设计智慧,真的令人敬佩。


这本书的缺点也是语法版本较旧,不过我也写了最新语法的部分笔记。


👉超级简单的设计模式,看不懂你来打我


今天的内容就这些了,如果你有更好的书籍,可以告诉我!


现在,关注我的公众号会有送书福利,具体请在公众号回复:活动,即可查看详情


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

推荐 6 个 火火火火 的开源项目

本期推荐开源项目目录:1、ChatGPT 网页应用(AI)2、AI 换脸(AI)3、API 调用 Midjourney 进行 AI 画图(AI)4、如何使用 Open AI 的 API?(AI)5、中华古诗词数据库6、动画编程 01. ChatGPT 网页应用...
继续阅读 »

本期推荐开源项目目录:

1、ChatGPT 网页应用(AI)
2、AI 换脸(AI)
3、API 调用 Midjourney 进行 AI 画图(AI)
4、如何使用 Open AI 的 API?(AI)
5、中华古诗词数据库
6、动画编程


01. ChatGPT 网页应用


基于 ChatGPT-Next-Web 二次开发的 ChatGPT 网页付费系统,包含用户管理模块和后台看板。


ChatGPT-Admin-Web 付费系统包含七个模块,包括:内容接口、用户系统、支付、敏感词过滤、自由聊天、分销、收益


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/AprilNEA/Ch…



编辑


添加图片注释,不超过 140 字(可选)


02. AI 换脸


适用于视频聊天的 AI 换脸模型,你可以使用这个 AI 模型替换摄像头中的面部或视频中的面部。这是一些例子:


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/iperov/Deep…


03. API 调用 Midjourney 进行 AI 画图


通过代理 MidJourney 的 Discord 频道,实现 api 形式调用AI绘图。


前提是你要注册 Midjourney 账号、并在 Discord 创建在自己的频道和机器人,然后就可以根据这个项目的指引一步步去使用 Api 调用 Midjourney 了。


开源地址:github.com/novicezk/mi…


编辑


添加图片注释,不超过 140 字(可选)


04. 如何使用 Open AI 的 API?


Open AI-Cook Book 是一本 Open AI 的 API 使用指南,提供了一些通过 Open AI 的 API 搭建任务的示例代码。


开源地址:github.com/openai/open…


编辑


添加图片注释,不超过 140 字(可选)


05. 中华古诗词数据库


为了让古诗词这个人类瑰宝传承下去,中华古诗词数据库诞生了。


这个项目整理了中华大量的古诗词,支持 Json 格式。数据库包含唐宋两朝近一万四千古诗人的作品, 接近 5.5 万首唐诗、26 万宋诗. 两宋时期1564位词人,21050首词。


开源地址:github.com/chinese-poe…


编辑


添加图片注释,不超过 140 字(可选)



  1. 动画编程


Motion Canvas 是一个 TypeScript 库,可以通过编程的方式生成动画,并提供所述动画的实时预览的编辑器。


开源地址:github.com/motion-canv…




编辑


添加图片注释,不超过 140 字(可选)


历史盘点


逛逛 GitHub 每天推荐一个好玩有趣的开源项目。历史推荐的开源项目已经收录到 GitHub 项目,欢迎 Star:

地址:https://github.com/Wechat-ggGitHub/Awesome-GitHub-Repo


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

收起阅读 »

rpc比http好吗,缪论?

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

是什么,如何理解


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


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


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

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


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


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


RPC|HTTP只是大家的简称

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

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


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


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


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


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

一次请求的过程



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


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


HTTP VS RPC自定义协议


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


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


编解码(序列化)



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

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


HTTP/1.1 一般用json


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


kitex序列化协议


协议层


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


可参考

1、kitex:概览,传输协议

2、dubbo:triple 协议,概览


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


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


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

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

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

网络通信层

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


HTTP的长连接和TCP长连接不是一个东西,需要注意下,TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置(只能保证网络没问题,不能代表服务没问题)


其中 HTTP2 拥有多路复用、优先级控制、头部压缩等优势


可以参考


kitex:连接类型


RPC自定义协议 和 HTTP的使用场景


公司内部的微服务,对客户端提供的服务 适合用RPC,更好的性能


对外服务、单体服务、为前端提供的服务适合用HTTP


我的思考


rpc在编解码、协议层、网络通信 都比HTTP有更大的优势,那为啥不把HTTP换成RPC呢

1、人的认知,HTTP已经深入人心(或者说生态好,通用性强),几乎所有的机器、浏览器和语言默认都会支持。但是自定义RPC协议 可能很多人都没听过(比如kitex、dubbo等),还让别人支持,根本不可能。

  • 需要建设全局的DNS等等,HTTP链路中的组件都需要换成 自定义的那一套,成本极高。
  • 但是公司内部可以搞成一套,可以极大提高性能,何乐而不为。

  • 我见过的案例是很多时候并没有深入思考为什么用,而是大家都这么用,我也这么用。

2、浏览器只支持 http协议。而且浏览器不支持自定义编解码的解析
      为啥大家面向浏览器/前端 不用自定义编解码?
     http不仅可以传输json、还可以传输二进制、图片等。所以协议层可以用http,编解码用protobuf/thrift也是可行的。
  • 公司内部实际案例:服务端和客户端交互时,为了提高性能,采用protobuf编解码数据,使用http协议传输数据。

  • 但是每次请求/响应数据都是不可读的。服务端会把protobuf编码前的数据转为json,用于打印log/存储,方便排查问题。

3、RPC框架 可以自定义负载均衡,重试机制,高可用,流量控制等策略。这些是HTTP不能支持的
  • 我理解是协议层用的http,但是内部的运行机制还是自定义的。http只是定义了传输数据的格式。举个例子:http的流量控制其实用的是 tcp的滑动窗口,http协议本身不具备这些功能。但是rpc是可以自己加这些功能的。这些功能必然有数据传输,这个传输协议用的http。

作者:cli
链接:https://juejin.cn/post/7264454873588449336
来源:稀土掘金

收起阅读 »

SpringBoot获取不到用户真实IP怎么办

今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地...
继续阅读 »
今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地址,害,看看我是怎么解决的吧!

问题原因

客户端请求数据时走的是Nginx反向代理,默认情况下客户端的真实IP地址会被其过滤,使得SpringBoot程序无法直接获得真实的客户端IP地址,获取到的都是Nginx的IP地址。

解决方案:

通过更改Nginx配置文件将客户端真实的IP地址加到请求头中,这样就能正常获取到客户端的IP地址了,下面我一步步带你看看如何配置和获取。
修改Nginx配置文件

#这个参数设置了HTTP请求头的Host字段,host表示请求的Host头,也就是请求的域名。通过这个设置,Nginx会将请求的Host头信息传递给后端服务。
proxy_set_header Host $host;
#这个参数设置了HTTP请求头的X−Real−IP字段,remote_addr表示客户端的IP地址。通过这个设置,Nginx会将客户端的真实IP地址传递给后端服务
proxy_set_header X-Real-IP $remote_addr;
#这个参数设置了HTTP请求头的 X-Forwarded-For字段,"X-Forwarded-For"是一个标准的HTTP请求头,用于表示HTTP请求经过的代理服务器链路信息,proxy_add_x_forwarded_for表示添加额外的服务器链路信息。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

修改后我的nginx.conf中的server如下所示

server {
listen 443 ssl;
server_name xxx.com;

ssl_certificate "ssl证书pem文件";
ssl_certificate_key "ssl证书key文件";
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

location / {
root 前端html文件目录;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# 关键在下面这个配置,上面的配置自己根据情况而定就行
location /hello{
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

SpringBoot代码实现

第一种方式:在代码中直接通过X-Forwarded-For获取到真实IP地址

@Slf4j
public class CommonUtil {
/**
* <p> 获取当前请求客户端的IP地址 </p>
*
* @param request 请求信息
* @return ip地址
**/
public static String getIp(HttpServletRequest request) {
if (request == null) {
return null;
}
String unknown = "unknown";
// 使用X-Forwarded-For就能获取到客户端真实IP地址
String ip = request.getHeader("X-Forwarded-For");
log.info("X-Forwarded-For:" + ip);
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
log.info("HTTP_X_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED");
log.info("HTTP_X_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
log.info("HTTP_X_CLUSTER_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
log.info("HTTP_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED_FOR");
log.info("HTTP_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED");
log.info("HTTP_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_VIA");
log.info("HTTP_VIA:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
log.info("REMOTE_ADDR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("getRemoteAddr:" + ip);
}
return ip;
}

第二种方式:在application.yml文件中加以下配置,直接通过request.getRemoteAddr()并可以获取到真实IP

server:
port: 8090
tomcat:
#Nginx转发 获取客户端真实IP配置
remoteip:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto

作者:BivinCode
链接:https://juejin.cn/post/7266040474321027124
来源:稀土掘金

收起阅读 »

69.9K Star,最强开源内网穿透工具:frp

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可...
继续阅读 »

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可以随时随地的访问。


说到内网传统,TJ君第一个想到的是国内最早的一款知名软件:花生壳。但是今天不是要推荐它,而是要推荐一个更牛的开源项目:frp!该项目目前已经收获了69.9 K Star,在GitHub上获得了极大的认可!


下载安装

frp目前已经提供了大部分操作系统的支持版本,通过这个链接:github.com/fatedier/fr… 就可以下载到适合你使用的安装。


以Windows的包为例,解压后可以获得这些内容:


frps是服务端程序,frpc是客户端程序。ini文件就是对应的配置文件。


首发 blog.didispace.com/tj-opensour…,转载请注明出处


暴露内网服务


内网穿透的玩法有很多,这里列举一个比较常见的例子。


比如:我要暴露一个只有自己能访问到服务器。那么可以这样配置:


配置 frps.ini,并启动服务端 frps

[common]
bind_port = 7000

在需要暴露到外网的机器上部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh]
type = stcp
# 只有 sk 一致的用户才能访问到此服务
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22

在想要访问内网服务的机器上也部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh_visitor]
type = stcp
# stcp 的访问者
role = visitor
# 要访问的 stcp 代理的名字
server_name = secret_ssh
sk = abcdefg
# 绑定本地端口用于访问 SSH 服务
bind_addr = 127.0.0.1
bind_port = 6000

把frpc也都启动起来之后,通过 SSH 就可以访问内网机器了

ssh -oPort=6000 test@127.0.0.1

其他支持


除了上面的玩法之外,frp还有很多玩法,比如:

1、自定义域名访问内网的 Web 服务

2、转发 DNS 查询请求

3、转发 Unix 域套接

4、对外提供简单的文件访问服务

5、为本地 HTTP 服务启用 HTTPS

6、点对点内网穿透


篇幅有限,具体如何配置这里就不多说了,有需要的读者可以直接查看官方文档,均有详细的服务端客户端配置案例。


最后,奉上相关链接:


开源地址:github.com/fatedier/fr…
文档地址:gofrp.org/docs/

作者:程序猿DD
链接:https://juejin.cn/post/7263283712224395321
来源:稀土掘金
收起阅读 »

你的野心距离成功,就差一个机会

这几天,深圳的几个达友张罗着找时间线下聚聚。 这两天在看《狂飙》。 自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。 这么热门的片子,加一起才看 6 集。 里面的张颂文,演技的确赞。 不过东哥想的是,演技这么好,怎么出道20年才火...
继续阅读 »

这几天,深圳的几个达友张罗着找时间线下聚聚。





这两天在看《狂飙》。


自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。


这么热门的片子,加一起才看 6 集。


里面的张颂文,演技的确赞。


不过东哥想的是,演技这么好,怎么出道20年才火起来?


他缺的,是什么?







人怎么能成功?简单地说



成功 = 能力 × 机会



能力是修炼内功,让自己变得更专业,成为头部的专家,是自己应该搞定的事情。


机会这事儿,就朦胧多了。


酒香不怕巷子深,是物质稀缺时代的事情。


整个镇子只有一家酿酒,巷子再深都有酒鬼登门。


而当前世界已经进入到了产能过剩、需求不足的时代。


国民总时间恒定,大家在存量的池子里杀的头破血流。


没机会展现的才华,只能被风沙埋没。


机会,越来越重要。


你与成功之间,可能就差一个机会。


比如张颂文,没机会拿到好剧本好角色,就只能是个小演员。


46 岁,买不起房子,没存款,感觉自己好失败。


每天都被拒绝,甚至侮辱地拒绝,让他滚蛋。


所以我们不能只死磕自己苦练内功,坐等被赏识被发现。


傻傻的,感动自己。


要为自己的野心,创造机会。


怎么创造呢?




去机会更多的地方。 在水多的地方打井,选鱼多的地方捞鱼。


最关键的选择,一个是城市,一个是赛道。


做金融,就到上海和深圳,搞互联网,就去北京和深圳。


东哥做香港保险,一个很大的优势就是所在的城市。


从福田站到香港西九龙,高铁15分钟。


市中心到市中心。


赛道选择上,做和钱近的工作,做可积累的工作。


能和人打交道,就别和机器打交道;能做销售,就别做售后。


销售看到的都是机会和钱,售后看到的都是负面问题。




向上链接,寻找大节点,利用好高能级的关系


人和人能量密度不同,大节点就是个人崛起的发动机。


达叔曾谈到过,在他写公众号的过程中,曾被欧神、医业观察的星哥、凯叔药械升职记的凯叔推荐。


三次推荐,引来了大批流量,成就了达叔的崛起。


在职场,就是要发现身边的强者,做深度绑定,成为强者权力结构件的一部分。


多花心思,多花钱,努力走进领导的小圈子,成为他身边的人。


利用他的势能,实现职业和财富的崛起。


就像《人民的名义》里面,高育良提到汉大帮的时候说



主观上说,我从没想过把人民赋予的权力向任何一位学生私相授受,但客观上也许私相授受了。
做了这么多年的法学教授,教了这么多年书,学生少不了,对自己学生呢,谁都不可能没感情,用人时就难免有偏爱。






进入不了核心圈子,就注定是个边缘人物,最后沦为炮灰。


如果在食物、资源匮乏的时候,你不坐在桌子旁,大概率就得躺在桌子上。


需要注意的是,链接的能级差不能太大。


基层员工,就别总想着去链接总经理董事长。


除非你是王思聪,他是王健林。


县官不如现管,链接好那个直接自己升职加薪的人,最有价值。




多和人互动,无论是线上还是线下。


人和人之间的互动永远是这个世界的最核心的算法,剩下全是工具。


职场中,多和实权派互动,和业务部门互动,和给公司赚钱最多的部门互动。


最忌讳的,就是整天对着电脑研究计算模型。


上班一天,接触的人不过办公室里这三五个,和固定的几个窗口,加一起超不过10个人。


能深入沟通和交流的,只有个位数。


除非你能成为公司最顶级的技术大牛,非你不可的那种,否则随时都可能被干掉。


东哥在自己的在每一篇文章后,都会附上了自己的个人微信,也是希望能和大家链接。


短视频时代,能潜下心来阅读文字本身就很可贵。


尤其还是东哥这种枯燥乏味谈赚钱的文章。


但有趣的是,很多人加微信以后一声不吱,甚至我主动询问也不回应。


这样的链接很难有实际价值。


只有沟通彼此的链接才会有价值,无论这个价值是信息、资源还是项目。


没有互动,彼此就仅仅是微信系统里面的几个号码。




多尝试,多拓展。机会是干出来的,不是想出来的。


就像猫王和村上春树的故事。


猫王是二十世纪收入最高的歌星,在卖唱及作明星之前是一位货车司机,每月的收入只数百美元。


工余之暇,他去试唱,被唱片公司相中,然后一举成名。


每年的收入以千万美元计。


村上春树则是在看棒球比赛的时候,突然想要写本小说。


比赛一结束,他就立马赶到文具店买了笔和纸,开始创作他的第一本小说《且听风吟》。


人生有无数种可能。当前的状态,不代表永远。


状态之外,要不断向外拓展,比如猫王的试镜,村上春树的小说。


只有不断地试,才会发现身上其他可能。


否则就只能,守着安静的沙漠 等待花开,看着别人的快乐默默感慨。


尤其自媒体,轻资产,风险可控,是时代给予我们的拓展机会。


多想想,自己有什么东西,可以不断打磨放大?


写公众号这几个月,和各路大 V 沟通,大开眼界。


医生讲医疗,房 V 讲房产都是常规操作。


搞心理学的做星座内容,看桃花运、分析财运,居然也有不少客户。


擅长做饭的可以做美食博主,身材好的可以做服装博主。


你擅长的是什么,能打出什么机会?




用无限游戏的思维方式,多推销自己。


无论做什么,总有一双眼睛在背后默默看着你。


合作的过程就是展现自己的机会。


工作中做汇报做提案,与同事沟通方案,最高任务是什么?


有人会说是把事情搞定,所以应该对事不对人。


对项目推进有利的,就要据理力争。


实际上,这样的工作是低效的,有时候甚至会起反作用。


同样的事情,不同的人沟通,常会有完全相反的结果。


对事不对人,现实中不存在。


理念和现实冲突时,错的永远是理念,不会是现实


沟通的最高任务,不是推销自己对项目的主张,而是推销自己这个人


从这个角度想,我们就不会局限于一城一地之得失,一朝一夕之荣辱。


而会站在一个更高层面,整个职业生涯的高度,来看待当下的问题。


项目成败的因素有很多,不一定是我们可以控制的。


但项目可以失败,人不可以失败


每一次沟通,都要展现自己足够靠谱,足够专业,是更长远合作的好伙伴。


所以要利用好每一次沟通,甚至是每一次刁难。


都是在给自己种善因。




资源也好,技术也罢,多分享,多展现。


不要藏着掖着,害怕教会徒弟饿死师傅。


太阳下面没有新鲜事,你能找到,别人也能找到。


真正的强者,你打压不了。


与其彼此竞争拼个头破血流,不如投资强者。


在他发展壮大的必然道路上,助他一臂之力。


也助以后的自己一臂之力。


你能成就多少强者,就有多大能量。


就像春秋时期齐桓公,想拜鲍叔牙为相,同时还要宰了对手公子纠的谋士管仲。


谁知鲍叔牙对他说:“我的才能只能让齐国平安,如果您要称霸天下,一定得拜管仲为相。”


齐桓公从之,尽释前嫌,拜管仲为国相,终成王霸之业,鲍叔牙也成就了自己千古美名。




结合多动手和多分享的思路,打造自己的产品。


就像伟大的程序员 Linus 说的



Talk is cheap. Show me the code.
被扯没用的,给我看代码。



产品是创作能力的背书,是自控能力的表现,是你最大的筹码。


自己的作品,可以理解为一种社交货币。


打磨作品就是往里钱,在将来的某个时刻,可以兑换它。


陌生人介绍的时候,就可以直接把作品丢出来。


有了作品,就有了各种合作的可能。


东哥这么小一个号,都有人过来谈合作,有时会一些意外的机会。




最后一点,给生活增加一些随机性。


平时晚上在家读书写作充电。下班时,朋友邀请参加一个陌生的聚会,去不去?


马上出发。


这就是生活中的随机性,说不定在聚会过程中会有新的收获。


一个漂亮的妞做女朋友,或一个合作的机会赚点钱。


这就是《黑天鹅》里面提到的正面黑天鹅


努力把自己暴露在可能发生正向意外的环境里。


用时间上微小的损失,换正面黑天鹅出现的可能,进而突破路径依赖。


就像这几天,深圳的几个达友张罗着找时间线下聚聚。


这群人里面有做医疗的,做芯片贸易的,做生物的,还有东哥是卖保险的。


彼此之间可能交流什么,完全未知。


但也就是因为这种未知,才能让自己突破日常的局限,也就更值钱。




供给过剩的时代,无数人在存量的池子里杀得头破血流。


给自己多创造一些机会,杀出来的可能就大一些

  • 去鱼多的地方捞鱼,挤到机会多的地方;
  • 向上链接,发现强者,进入到强者的核心圈子;
  • 多和人互动,不要沉迷于技术;
  • 基于自身优势多对外拓展,寻找机会;
  • 无限游戏思维,办事儿的时候笑面挑战,推销自己;
  • 多分享,尤其挖掘并投资潜力股;
  • 打造自己的作品,积累社交货币;
  • 给生活增加一些随机性;


避免像张颂文,满腹才华,却走了漫长的二十年。


你距离成功,也许只缺一个机会。


那就造一个出来。



作者:jetorz
来源:mdnice.com/writing/39186f5978a84668b24442c684d01fa3
收起阅读 »

this指向哪?

web
谁调用就指向谁 this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。 var userName = '张三' function fn() { var userName = '李四' console.log(this.use...
继续阅读 »

谁调用就指向谁


this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。


var userName = '张三'
function fn() {
var userName = '李四'
console.log(this.userName) // 张三
}
fn()

函数fn相当于挂在window,调用fn()等同于window.fn(),所以fn可以看做是被window调用,此时this便指向window,所以输出结果是函数外的userName变量。


再看下面的示例:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.userName) // 李四
}
}

obj.fn()

函数fn被定义在对象obj里,通过obj.fn()调用,回到我们开头的那句话:谁调用就指向谁fnobj调用,所以this指向obj,输出结果是李四。


如果使用windowd.obj.fn()调用呢?结果还是一样的,因为最终是obj调用,所以this指向的还是obj


接着往下看下面这段代码:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.name) // 张三
}
}

var func = obj.fn
func()

输出结果为张三,因为代码中只是将obj.fn赋值给func,此时函数fn还没有被调用,真正调用是在func()func挂在window下,所以最终相当于是window调用了函数fn,所以this指向的是window


还是应了那句话:谁调用就指向谁


其他情况


1、作为一个函数被直接调用


当函数被直接调用,没有挂在任何对象上时,this指向window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
function a() {
console.log(this.userName) // 张三
}
a()
}
}
obj.fn()

这里在fn中定义了一个函数a并调用,注意这里函数a只是在函数fn内,它并没有挂在哪个对象上,也没有通过其他方式被调用,而是作为一个函数被直接调用,所以this指向window


2、箭头函数


箭头函数的特点是没有自己的this也不能通过new调用。箭头函数通过作用域向上查找,找到离包裹它最近的那一个普通函数的this作为自己的this,如果没有则指向全局对象window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
var func = () => {
console.log(this.userName) // 李四
}
func()
},
a: {
b: () => {
console.log(this.userName) // 张三
}
}
}
obj.fn()
obj.a.b()

func是一个箭头函数,箭头函数通过向上查找找到fnfnthis指向obj,所以箭头函数的this指向的也是obj,输出结果:李四。


obj.a.b()则指向window,输出结果:张三。


3、new 构造函数


当函数通过使用new调用时,new过程中会创建一个新的对象,而this便是指向这个新创建的对象。


function fn(name) {
this.userName = name
this.age = 18
console.log(this) // {userName: '张三', age: 18}
}
let data = new fn('张三')
// new的同时内部会默认把this做回返回值返回
console.log('data: ', data) // data: {userName: '张三', age: 18}

如何改变this指向


1、call、apply、bind


可以通过callapplybind的第一个参数传值作为this值,改变this指向。


obj.fn('hello') // hello,张三
obj.fn.call({ userName: '李四' }, 'hello') // hello,李四
obj.fn.apply({ userName: '王五' }, ['hello']) // hello,王五
obj.fn.bind({ userName: '老六' }, 'hello')() // hello,老六

2、call、apply、bind区别


从上面的使用可以看到,这三个API的参数形式除了第一个参数外是有一些区别的



  • callbind是多参数的形式:call(thisArg, arg1, arg2...)

  • apply是数组的形式:call(thisArg, [arg1, arg2...])


另外一个区别是:bind的返回值是一个函数,需要自己再手动调用一次,而callapply<

作者:狂砍2分4篮板
来源:juejin.cn/post/7265534390506635319
/code>则不用。

收起阅读 »

孩子是双眼皮还是单眼皮?来自贝叶斯算法的推测

问题描述 最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。 我家的情况是这样,宝爸宝妈、爷爷奶...
继续阅读 »

问题描述


最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。


我家的情况是这样,宝爸宝妈、爷爷奶奶、姥姥姥爷都是双眼皮。


查了一下资料,双眼皮是显性基因,因此除非宝爸宝妈都是杂合性基因且都贡献单眼皮片段,孩子才能是单眼皮。


这里做一下假设,全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。即



其中S表示双眼皮,D表示单眼皮,C表示纯合基因,Z表示杂合基因。


祖父辈人基因类型的后验概率


父母双方的情况是对等的,因此只挑选其中一方进行计算。以父亲为例,爷爷奶奶可能的基因类型组合有:CC,CZ和ZZ。先验概率为:



已知父亲是双眼皮,则爷爷奶奶是CC组合的后验概率为:



类似地可以算出



进行归一化后得到:



这里之所以要进行归一化,是因为在计算过程中对概念进行了替换,我们利用了爷爷奶奶都是双眼皮的信息。


父母基因类型的概率


仍然以父亲为对象进行计算,其是纯合基因的概率为:



是杂合基因的概率为:



剩下1/20的概率是表现为单眼皮的概率,需要排除掉。也就是说,在观察到爷爷奶奶父亲都是双眼皮的情况下,父亲是纯合基因的概率为






































,是杂合基因的概率为







































孩子双眼皮的概率


综合以上,孩子是双眼皮的概率为:



可见这个概率是很高的。


压力测试


在之前的计算里面,由于没有一般人群的统计数据,我们假设全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。这里我们换一组数据,假设全部人群中有1/4是双眼皮,双眼皮人群中纯合基因有1/4,看看这样会对结果造成多大影响。


这种情况下



父母任意一方是纯合基因的概率为







































,是杂合基因的概率为









































最终得到孩子是双眼皮的概率是



可见概率依然非常高。“六个钱包”都是双眼皮是一个非常可靠的信号。


作者:Kelly1024
来源:mdnice.com/writing/f71210a4999f4bd2a674f9bead00551c
收起阅读 »

原来,我们的代码就是这样被污染的...

最近 CR 了这样一段代码: 我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在...
继续阅读 »

最近 CR 了这样一段代码:


插图1.png


我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在想,为什么会这样?


先探究一下这个问题的表面原因,让我们从底层开始:



  • DB 表结构按照团队规范,字段名是下划线分隔的

  • node 服务定义 entity 的时候,直接复用 DB 表结构字段名

  • service 模块在查询 DB 之后,直接回吐结果,所以 service 类返回的数据结构的变量名也是下划线的

  • node api 的 controller 通过调用 service 模块的函数,获取数据处理之后,也直接回吐下划线的变量名的数据结构

  • 因为 node api 的接口协议是下划线的,所以 web 页面请求 http api 的出入参也是下划线的,如上图所示

  • 因为 http api 返回的结果是下划线的,然后页面逻辑直接使用,最后,页面逻辑也出现了大量下划线


这是一段挺长的链路,只要我们在后面的任意一个环节处理一下变量名的转换,都能避免这个问题,但是并没有。


插图2.png


为什么会这样?稍微探究一下这里的深层原因,也是挺有意思的。


首先,项目启动时没有严格把控代码质量。这里的原因有很多,比如项目工期紧、主要以完成功能为主、内部项目不需要要求这么严格、项目启动时是直接 copy 另外一个项目的,那个项目也是这样写的等等。但是,这些统统都是借口!我是要负主要责任的。


其次,没有开发同学想过要去优化这里的代码。参与这个项目的同学并不是不知道如何去解决这个问题,但就是没人想要去解决这个问题。大家更多的是选择“入乡随俗”,别人这么写,我也这么写吧。


最后,Code Review 没有严抓。问题都是越早处理成本越小,如果在早期我们就开始严抓 Code Review 的话,说不定就能及时改善这个问题了。


这个案例是一个非常真实的破窗效应案例,这里面有不少地方值得我们深思的。


首先,它会污染整个项目。如果我们在项目的一开始就没有把控好代码质量,那我们的项目代码很快就会被污染。最开始的开发同学可能知道历史原因,自然不会觉得不自然。后续加入的维护者看到项目代码是这样子的,就会误以为这个项目的代码规范就是这样子的,于是也“入乡随俗”,最终整个项目的代码就被污染,破烂不堪。


其次,它会污染依赖的项目。比如这里就是 node 项目先被污染,对外提供的 api 也受到污染,然后调用这些 api 的 web 项目也被污染了。很多时候,api 的接口协议都是由后端开发来定,如果碰到一些缺乏经验的后端开发,前端同学会收到一些很奇怪的接口协议,比如字段命名规范不统一,冗余字段,设计不合理等等,由于前端的话语权较弱的缘故,很可能会被动接受,如果处理不当,前端项目的代码就会受到污染了。


最后,它会污染整个开发团队!初创的开发同学就不说了;后续的维护者看到代码是这样子的,不是错误认知这个项目的代码规范,就是错误认知团队的代码规范;更为糟糕的是,阅读项目源码的其他同学也会受到污染:“哦,原来别人也是这样写代码的”。噩梦由此而生...


插图3.png


破窗就像病毒一样,快速并疯狂地污染整个项目代码,然后传染给开发团队,最后扩散到其他团队。这个病毒传染的范围很广,速度很快,一不留神,整个团队就会沦陷。那我们应该怎样医治呢? 主要有 3 个方向:


首先,增强个人免疫力。形成个人的编码风格,然后坚持它,这是治本的良方。良好的编码风格是不会损坏个人的编码效率的,反而会有助于提升个人的研发效率,主要体现在以下几个方面:



  • 减少低级错误

  • 提升代码的可阅读性,提升代码的可维护性,从而提升团队协作效率

  • 形成“肌肉记忆”,提升编码效率

  • 避免返工,主要体现在糟糕的设计问题和编码风格冲突问题


其次,做好防护,避免传播。比如我们这个简单的案例,在整个链路的任何一个环节,都能轻松处理这个问题,这样就不会传播到后面的环节了。很多人碰到一些历史破窗代码时,可能会觉得修复这些破窗成本太大了,那就不要去修复,只需要保证自己写的新代码是良好的,也是一个不错的方案。已经中毒的人我们没有能力医治的时候,起码,我们可以认真做好防护,避免病毒进一步传播!


最后,对症下药医治。只有千日做贼,那有千日防贼,我们总是需要想办法医治这个病,而治病的药方就是重构。这里就不深入讲了,重构也是个大学问,只需要知道,越早重构,成本越低,比如在 Code Review 的时候就要严抓这些问题,并跟踪修复情况。


不知道,你的“免疫力”修炼得怎么样了?



【讨论问题】


除了这里的案例,还有很多其他类型的破窗,你都碰到了哪些?


欢迎在评论区分享你的想法,一起讨论。


作者:潜龙在渊灬
来源:juejin.cn/post/7252888158828642360
收起阅读 »

面试官:“只会这一种懒加载实现思路?回去等通知吧”

web
思路一:监听滚动事件 监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。 关键 API getBo...
继续阅读 »

思路一:监听滚动事件



监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。



关键 API



  • getBoundingClientRect() 方法返回的对象包含以下属性:



  • top:元素上边缘相对于视窗的距离。

  • right:元素右边缘相对于视窗的距离。

  • bottom:元素下边缘相对于视窗的距离。

  • left:元素左边缘相对于视窗的距离。

  • width:元素的宽度(可选)。

  • height:元素的高度(可选)。


可以看下下面的图。
image.png


来看看代码示例


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Exampletitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;

return (
rect.
top >= 0 &&
rect.
left >= 0 &&
rect.
bottom <= windowHeight &&
rect.
right <= windowWidth
);
}

function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
for (const img of images) {
if (isInViewport(img)) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}
}
}

window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
window.addEventListener('DOMContentLoaded', lazyLoad);

script>
body>
html>

这里的思路就相对简单了,核心思路就是判断这个元素在不在我的视口里面。


同样的思路下面还有另一种实现方式,是用 offsetTopscrollTopinnerHeight做的,这个很常见,我们就不说了。


思路二:Intersection Observer API



这是一种现代浏览器提供的原生 API,用于监控元素与其祖先元素或顶级文档视窗(viewport)的交叉状态。当元素进入可视区域时,会自动触发回调函数,从而实现懒加载。相比于监听滚动事件,Intersection Observer API 更高效且易于使用。



关键 API



  • Intersection Observer API:创建一个回调函数,该函数将在元素进入或离开可视区域时被调用。回调函数接收两个参数:entries(一个包含所有被观察元素的交叉信息的数组)和 observer(观察者实例)。


其中 entries 值得一提,entries 是一个包含多个 IntersectionObserverEntry 对象的数组,每个对象代表一个观察的元素(target element)与根元素(root element)相交的信息。IntersectionObserverEntry 对象包含以下属性:



  1. intersectionRatio: 目标元素和根元素相交区域占目标元素总面积的比例,取值范围为 0 到 1。

  2. intersectionRect: 目标元素和根元素相交区域的边界信息,是一个 DOMRectReadOnly 对象。

  3. isIntersecting: 布尔值,表示目标元素是否正在与根元素相交。

  4. rootBounds: 根元素的边界信息,是一个 DOMRectReadOnly 对象。

  5. target: 被观察的目标元素,即当前 IntersectionObserverEntry 对象所对应的 DOM 元素。

  6. time: 观察到的相交时间,是一个高精度时间戳,单位为毫秒。


这个最近我们组里的姐姐封了一个懒加载组件,通过单例模式 + Intersection Observer API,然后在外部控制 v-if 就好了,非常 nice。


由于代码保密性,我这里只能提供一个常规实现方法qwq


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Example - Intersection Observertitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function loadImage(img) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}

function handleIntersection(entries, observer) {
entries.
forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.
unobserve(entry.target);
}
});
}

function lazyLoad()
script>
body>
html>


思路三:虚拟列表



对于长列表数据,可以使用虚拟列表技术来实现懒加载。虚拟列表只会渲染当前可视区域内的列表项,当用户滚动列表时,动态更新可视区域内的内容。这种方式可以大幅减少 DOM 节点的数量,提高页面性能。



这个我之前专门有文章聊过:b站面试官:如果后端给你 1w 条数据,你如何做展示?


就不赘述啦~


那么这就是本篇文章的全部内容啦~


作者:阳树阳树
来源:juejin.cn/post/7265329025899823159
收起阅读 »

年中总结:浑浑噩噩

学业 今年好像一直在虚假地忙碌,都是忙给别人看到,自己的进度并没有多少。 会议论文还在审稿,能不能过还是两说,除此之外好像没什么学业进度了。 每当有人问我:忙什么呢? 我就理直气壮地说,“赶论文呢”。 说得多了,自己也就信了,一旦自己都信了,就开始行色匆匆,...
继续阅读 »

学业


今年好像一直在虚假地忙碌,都是忙给别人看到,自己的进度并没有多少。


会议论文还在审稿,能不能过还是两说,除此之外好像没什么学业进度了。



每当有人问我:忙什么呢?


我就理直气壮地说,“赶论文呢”。


说得多了,自己也就信了,一旦自己都信了,就开始行色匆匆,一副“谁也别理我,忙着呢”的架势。……就是靠着这点虚张声势的忙碌,我活的了一种滥竽充数的成人感。




稀里糊涂掉进学术圈子里,好像一个瘸子掉到了一个舞池子里,偏偏又是一个要强的个性,没有科研天分,还要打肿脸充胖子,硬着头皮在这个舞池子里一瘸一拐地跳下去。



感情


又是单身的一年,近乎偏执的纯爱党。身边朋友嘲笑“你想和人从朋友做起,慢慢了解了再在一起,这种现在哪有啊!你只能遇到渣男!!!”


遇到喜欢的男孩子,我去追了,但是我太主动了,人家不喜欢主动的女孩子,其实主要是不喜欢我。最后不了了之了。但我依旧认为,



“女生太主动了,是不是不太好?”


“没什么好不好的,爱情本来就没有什么公式。”



找过很多借口,帮助自己走出来。但是说来说去还是真心喜欢过才会找那么多的借口。



“你说会不会是这样,我其实也并不喜欢他,喜欢的不过是自己的面子?


“也许吧。”但这又说明什么?你可以说“我喜欢的不是他,而是自己的面子”,或者“我喜欢的不是他,我只是在逃避孤独”,或者,“我喜欢的不是真正的他,而是一个想象出来的他” “我喜欢的不是他,而是被人疼爱的那种感觉”·····这样的句子可以无限造下去,但结果殊途同归,就是你在乎。



生活


因为身体和学业问题,一度陷入焦虑。每天过的很差劲,不断内耗。


不想给朋友们倾诉,在他们眼里我永远都是那个元气少女。身边人都说“谁抑郁了,你都不可能抑郁。”


但是那段时间我真的是很难过很难过。我之前也是这样,但是把所有的负面情绪都给了我喜欢的男孩子。可能让人家觉得我就是鱼鱼女孩,其实我并不是。我觉得是我的错,我不该一直跟他倾诉我压力有多大。



传递悲伤这件事本身,就是一种暴力。悲伤不该特地说给别人听。



以至于后来,我真的很舍不得,但是我什么都不敢和他说。我怕这样的想念没有诚意,我怕他觉得我把他当作垃圾桶。所以只能自己解决。



人生若有知己相伴固然妙不可言,但那可遇而不可求,真的,也许既不可遇又不可求,可求的只有你自己,你要俯下身去,朝着幽暗深处的自己伸出手去。



读书


最近摆烂中,最近三个月只看了七本书。是时候放下手机重拾阅读兴趣了。


我是个特别喜欢看爱情小说的人,喜欢看别人的爱情观。但是今年上半年看的微乎其微,主要都是健康、社会、个人成长相关的内容。


比如《中国营养科学全书》,比如《孤独社会》等等。至于小说,国内的作家的只看了刘震云的《一句顶一万句》,我果然是不太喜欢国内的小说,太过沉重了。比如:



一开始觉得没有话说是两人不爱说话,后来发现不爱说话和没话说是两回事。不爱说话是心里还有话,没话说是干脆什么都没有了。




将心腹话说给朋友,没想到朋友一掰,这些自己说过的话,都成了刀子,反过头扎向自己。



推荐一本八嘎国的小说《星期一,喝抹茶》。没什么营养内容,就是适合消磨时间的那种小短文。每一个小故事的配角都会成为下一个小故事的主角,透露着生活中的小确幸。



“不用配合她呀”——我想起了富贵子刚才说的话。她确实没有配合我,她一直坚守着自己认准的道路,并且尊重了我的选择。



展望


生活都是你自己的,自己经历,自己成长,后果也要自己承担。不要再这么浑浑噩噩地过下去了。先做应该做的,

作者:Roche
来源:juejin.cn/post/7265210393046810664
再做喜欢做的事情吧。

收起阅读 »

动态样式去哪儿了?

web
本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约...
继续阅读 »

本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约束提升缓存效率,从而达到性能优化的目的。不过我们并不止步于此。我们可以通过一些逻辑,直接跳过运行时生成样式的阶段。


动态样式去哪儿了?


如果你研究过 Ant Design 的官网,你会发现 Ant Design 的组件并没有动态插入 来控制样式,而是通过 CSS 文件来控制样式:

image.png


image.png


document.head 里有几个 css 文件引用:



  • umi.[hash].css

  • style-acss.[hash].css


前者为 dumi 生成的样式内容,例如 Demo 块、搜索框样式等等。而后者则是 SSR 生成的样式文件。在定制主题文档中,我们提过可以通过整体导出的方式将页面中用到的组件进行预先烘焙,从而生成 css 文件以供缓存命中从而提升下一次打开速度。这也是我们在官网中使用的方式。所以 Demo 中的组件,其实就是复用了这部分样式。 等等!CSS-in-JS 不是需要在运行时生成样式的 hash 然后通过 进行对齐的么?为什么 css 文件也可以对齐?不用着急,我们慢慢看。

CSS-in-JS 注水


应用级的 CSS-in-JS 方案会对生成的样式计算出 hash 值,并且将其存入 Cache 中。当下次渲染时,会先从 Cache 中查找是否存在对应的样式,如果存在则直接使用,否则再生成一次。这样就可以避免重复生成样式,从而提升性能。


image.png


每个动态插入到页面中的样式同样以为 hash 作为唯一标识符。如果页面中已经存在该 hash 的 ,则说明 SSR 中做过 inline style 注入。那么 就不用再次创建。 你可以发现,虽然 的节点创建可以省略,但是因为 hash 依赖于计算出的样式内容。所以即便页面中已经有可以复用的样式内容,它仍然免不了需要计算一次。实属不划算。

组件级 CSS-in-JS


组件级别的 CSS-in-JS 一文中,我们提过。Ant Design 的 Cache 机制并不需要计算出完整的样式。对于组件库而言,只要通过 Token 和 ComponentName 就可以确定生成样式一致性,所以我们可以提前计算出 hash 值: 也因此,我们发现可以复用这套机制,实现在在客户端侧感知组件样式是否已经注入过。


SSR HashMap


在 @ant-design/cssinjs 中,Cache 本身包含了每个元素对应的 style 和 hash 信息。过去的 extractStyle 方法只取 Cache 中 style 的内容进行封装:


// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE"":where(.bAMbOo).ant-btn { color: red }"],

  "bAMbOo|Spin": ["liGHt"":where(.bAMbOo).ant-spin { color: blue }"]

}

提取:


:where(.bAMbOo).ant-btn {

  color: red;

}

:where(.bAMbOo).ant-spin {

  color: blue;

}

为了复用样式,我们更进一步。将 path 和 hash 值也进行了抽取:


{

  "bAMbOo|Button": "LItTlE",

  "bAMbOo|Spin""liGHt"

}

并且也打成 css 样式:


// Just example. Not real world code

.cssinjs-cache-path {

  content'bAMbOo|Button:LItTlE;bAMbOo|Spin:liGHt';

}

这样 SSR 侧就将我们所需的信息全部留存了下来,接下去只需要在客户端进行提取即可。


CSR HashMap


在客户端则简单的多,我们通过 getComputedStyle 提取 HashMap 信息留存即可:


// Just example. Not real world code

const measure = document.createElement('div');

measure.className = 'cssinjs-cache-path';

document.body.appendChild(measure);

 

// Now let's parse the `content`

const { content } = getComputedStyle(measure);

在组件渲染阶段,useStyleRegister 在计算 CSS Object 之前,会先在 HashMap 中查找 path 是否存在。如果存在,则说明该数据已经通过服务端生成。我们只需要将样式从现有的 里提取出来即可:

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""READ_FROM_INLINE_STYLE"],

  "bAMbOo|Spin": ["liGHt""READ_FROM_INLINE_STYLE"]

}

而对于 CSS 文件提供的样式(比如官网的使用方式),它不像 会被移除,我们直接标记为来自于 CSS 文件即可。和 inline style 一样,它们会在 useInsertionEffect 阶段被跳过。

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""__FROM_CSS_FILE__"],

  "bAMbOo|Spin": ["liGHt""__FROM_CSS_FILE__"]

}

总结


CSS-in-JS 因为运行时的性能损耗而被人诟病。而在 Ant Design 中,如果你的应用使用了 SSR,那么在客户端侧就可以直接跳过运行时生成样式的阶段从而提升性能。当然,我们会继续跟进 CSS-in-JS 的发展,

作者:支付宝体验科技
来源:juejin.cn/post/7265249262329839672
为你带来更好的体验。

收起阅读 »

如何去除图片马赛克?

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究! 一、前言 对图像不了解的人时常妄想去除马赛克是可以实现的,严格意义来说这确实是无法实现的。而深度学习是出现,让去除马赛克成为可能。 为了理解去除马赛克有多难,我们需要...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!



一、前言


对图像不了解的人时常妄想去除马赛克是可以实现的,严格意义来说这确实是无法实现的。而深度学习是出现,让去除马赛克成为可能。


为了理解去除马赛克有多难,我们需要知道马赛克是什么。观感上,马赛克就是方块感。当我们观察图像像素时, 马赛克表现为下图的情况:



原图右下角有十字,而添加马赛克后右下角一片都变成了同一像素,如果我们没保留原图,那么我们无法还原,也不知道是否还原了原图。因为原图已经被破坏了,这也是为什么马赛克是不可修复的。


那神经网络又是如何让修复成为可能呢?其实无论什么方式的修复,都是一种估计,而不是真正的修复。神经网络去除马赛克的操作其实是生成马赛克那部分内容,然后替代马赛克,从而达到修复的效果。


这种修复并不是还原,而是想象。假如我们对一张人脸打了马赛克,神经网络可以去除马赛克,但是去除后的人脸不再是原来那个人了。


二、实现原理


2.1 自编码器


图像修复的方法有很多,比如自编码器。自编码器是一种自监督模型,结构简单,不需要人为打标,收敛迅速。其结构如图:



编码器部分就是用于下采样的卷积网络,编码器会把图片编码成一个向量,而解码器则利用转置卷积把编码向量上采样成和原图大小一致的图片,最后我们把原图和生成结果的MSE作为损失函数进行优化。当模型训练好后,就可以用编码器对图片进行编码。


2.2 自编码器去除马赛克


那自编码器和去除马赛克有什么联系呢?其实非常简单,就是原本我们是输入原图,期望解码器能输出原图。这是出于我们希望模型学习如何编码图片的原图。而现在我们想要模型去除马赛克,此时我们要做的就是把马赛克图片作为输入,而原图作为输出,这样来训练就可以达到去除马赛克的效果了:



关于关于这种实现可以参考:juejin.cn/post/721068…


2.3 自编码器的问题


自编码器有个很明显的问题,就是图片经过编码器后会损失信息,而解码器的结果自然也会存在一些问题。这样既达不到去除马赛克的功能,连还原的原图都有一些模糊。


这里可以利用FPN的思想来改进,当自编码器加入FPN后,就得到了UNet网络结构。


2.4 UNet网络


UNet结构和自编码器类似,是一个先下再上的结构。和自编码器不同的时,UNet会利用编码器的每个输出,将各个输出与解码器的输入进行concatenate,这样就能更好地保留原图信息。其结构如下图:



UNet原本是用于图像分割的网络,这里我们用它来去除马赛克。


在UNet中,有几个部分我们分别来看看。


2.4.1 ConvBlock


在UNet中,有大量连续卷积的操作,这里我们作为一个Block(蓝色箭头),它可以实现为一个层,用PyTorch实现如下:


class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, 1, 1),
nn.BatchNorm2d(out_channels),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, 3, 1, 1),
nn.BatchNorm2d(out_channels),
nn.ReLU()
)

def forward(self, inputs):
return self.model(inputs)

这里其实就是两次卷积操作,这里的目的是提取当前感受野的特征。


2.4.2 ConvDown


经过连续卷积后,会使用卷积网络对图片进行下采样,这里把stride设置为2即可让图片缩小为原来的1/2。我们同样可以实现为层:


class ConvDown(nn.Module):
def __init__(self, channels):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(channels, channels, 3, 2, 1),
nn.BatchNorm2d(channels),
nn.ReLU()
)

def forward(self, inputs):
return self.model(inputs)

这里只有一个卷积,而且stride被设置为了2。


2.4.3 ConvUp


接下来是解码器部分,这里多了一个上采用的操作,我们可以用转置卷积完成,代码如下:


class ConvUp(nn.Module):
def __init__(self, channels):
super().__init__()
self.model = nn.Sequential(
nn.ConvTranspose2d(channels, channels // 2, 2, 2),
nn.BatchNorm2d(channels // 2),
nn.ReLU()
)

def forward(self, inputs):
return self.model(inputs)

上面是层可以把图片尺寸扩大为2倍,同时把特征图数量缩小到1/2。这里缩小特征图的操作是为了concatenate操作,后面详细说。


三、完整实现


首先,导入需要用的模块:


import os
import random
import torch
from torch import nn
from torch import optim
from torch.utils import data
import matplotlib.pyplot as plt
from torchvision import transforms
from torchvision.transforms import ToTensor
from PIL import Image, ImageDraw, ImageFilter
from torchvision.utils import make_grid

下面开始具体实现。


3.1 创建Dataset


首先创建本次任务需要的数据集,分布大致相同的图片即可,代码如下:


class ReConstructionDataset(data.Dataset):
def __init__(self, data_dir=r"G:/datasets/lbxx", image_size=64):
self.image_size = image_size
# 图像预处理
self.trans = transforms.Compose([
transforms.Resize(image_size),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
# transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 保持所有图片的路径
self.image_paths = []
# 读取根目录,把所有图片路径放入image_paths
for root, dirs, files in os.walk(data_dir):
for file in files:
self.image_paths.append(os.path.join(root, file))

def __getitem__(self, item):
# 读取图片,并预处理
image = Image.open(self.image_paths[item])
return self.trans(self.create_blur(image)), self.trans(image)

def __len__(self):
return len(self.image_paths)


@staticmethod
def create_blur(image, return_mask=False, box_size=200):
mask = Image.new('L', image.size, 255)
draw = ImageDraw.Draw(mask)
upper_left_corner = (random.randint(0, image.size[0] - box_size), random.randint(0, image.size[1] - box_size))
lower_right_corner = (upper_left_corner[0] + box_size, upper_left_corner[1] + box_size)
draw.rectangle([lower_right_corner, upper_left_corner], fill=0)
masked_image = Image.composite(image, image.filter(ImageFilter.GaussianBlur(15)), mask)
if return_mask:
return masked_image, mask
else:
return masked_image

Dataset的实现与以往基本一致,实现init、getitem、len方法,这里我们还实现了一个create_blur方法,该方法用于生成矩形马赛克(实际上是高斯模糊)。下面是create_blur方法生成的图片:



3.2 网络构建


这里我们需要使用前面的几个子单元,先实现编码器,代码如下:


class UNetEncoder(nn.Module):
def __init__(self):
super().__init__()
self.blk0 = ConvBlock(3, 64)
self.down0 = ConvDown(64)
self.blk1 = ConvBlock(64, 128)
self.down1 = ConvDown(128)
self.blk2 = ConvBlock(128, 256)
self.down2 = ConvDown(256)
self.blk3 = ConvBlock(256, 512)
self.down3 = ConvDown(512)
self.blk4 = ConvBlock(512, 1024)

def forward(self, inputs):
f0 = self.blk0(inputs)
d0 = self.down0(f0)
f1 = self.blk1(d0)
d1 = self.down1(f1)
f2 = self.blk2(d1)
d2 = self.down2(f2)
f3 = self.blk3(d2)
d3 = self.down3(f3)
f4 = self.blk4(d3)
return f0, f1, f2, f3, f4

这里就是ConvBlok和ConvDown的n次组合,最终会得到一个1024×4×4的特征图。在forward中,我们返回了5个ConvBlok返回的结果,因为在解码器中我们需要全部使用。


接下来是解码器部分,这里与编码器相反,代码如下:


class UNetDecoder(nn.Module):
def __init__(self):
super().__init__()
self.up3 = ConvUp(1024)
self.blk3 = ConvBlock(1024, 512)
self.up2 = ConvUp(512)
self.blk2 = ConvBlock(512, 256)
self.up1 = ConvUp(256)
self.blk1 = ConvBlock(256, 128)
self.up0 = ConvUp(128)
self.blk0 = ConvBlock(128, 64)
self.last_conv = nn.Conv2d(64, 3, 3, 1, 1)

def forward(self, inputs):
f0, f1, f2, f3, f4 = inputs
u3 = self.up3(f4)
df2 = self.blk3(torch.concat((f3, u3), dim=1))
u2 = self.up2(df2)
df1 = self.blk2(torch.concat((f2, u2), dim=1))
u1 = self.up1(df1)
df0 = self.blk1(torch.concat((f1, u1), dim=1))
u0 = self.up0(df0)
f = self.blk0(torch.concat((f0, u0), dim=1))
return torch.tanh(self.last_conv(f))

解码器的inputs为编码器的5组特征图,在forward时需要与上采样结果concatenate。


最后,整个网络组合起来,代码如下:


class ReConstructionNetwork(nn.Module):

def __init__(self):
super().__init__()
self.encoder = UNetEncoder()
self.decoder = UNetDecoder()

def forward(self, inputs):
fs = self.encoder(inputs)
return self.decoder(fs)

3.3 网络训练


现在各个部分都完成了,可以开始训练网络:


device = "cuda" if torch.cuda.is_available() else "cpu"


def train(model, dataloader, optimizer, criterion, epochs):
model = model.to(device)
for epoch in range(epochs):
for iter, (masked_images, images) in enumerate(dataloader):
masked_images, images = masked_images.to(device), images.to(device)
outputs = model(masked_images)
loss = criterion(outputs, images)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (iter + 1) % 100 == 1:
print("epoch: %s, iter: %s, loss: %s" % (epoch + 1, iter + 1, loss.item()))
with torch.no_grad():
outputs = make_grid(outputs)
img = outputs.cpu().numpy().transpose(1, 2, 0)
plt.imshow(img)
plt.show()
torch.save(model.state_dict(), '../outputs/reconstruction.pth')


if __name__ == '__main__':
dataloader = data.DataLoader(ReConstructionDataset(r"G:\datasets\lbxx"), 64)
unet = ReConstructionNetwork()
optimizer = optim.Adam(auto_encoder.parameters(), lr=0.0002)
criterion = nn.MSELoss()
train(unet, dataloader, optimizer, criterion, 20)

训练完成后,就可以用来去除马赛克了,代码如下:



dataloader = data.DataLoader(ReConstructionDataset(r"G:\datasets\lbxx"), 64, shuffle=True)
unet = ReConstructionNetwork().to(device)
unet.load_state_dict(torch.load('../outputs/reconstruction.pth'))
for masked_images, images in dataloader:
masked_images, images = masked_images.to(device), images.to(device)
with torch.no_grad():
outputs = unet(masked_images)
outputs = torch.concatenate([images, masked_images, outputs], dim=-1)
outputs = make_grid(outputs)
img = outputs.cpu().numpy().transpose(1, 2, 0)
img = cv2.normalize(img, , 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
Image.fromarray(img).show()


下面是生成结果。左侧为原图,中间为添加马赛克后的图片,右侧则是去除马赛克后的结果:



整体来说效果比较不错。本文的方法不只可以用来去除马赛克,还可以完成图像重构。比如老化的图片、被墨汁污染的图片等,都可以用本文的方法完成重构。另外,本文的数据有限,实现效果并不通用,有需求的读者可以移步CodeFormer项目:github.com/sczhou/Code…


作者:ZackSock
来源:juejin.cn/post/7265310874488520765
收起阅读 »

iOS设置圆角后阴影不显示

iOS
问题 设计图中View有阴影和圆角,里面填充了四个按钮。同时设置View的圆角和阴影,阴影并不显示。试了很多次,找了很多办法,记录一下过程。 为了展示父视图的圆角,设置了masksToBounds=YES,将超出父视图的内容clip掉,这样圆角就OK了。不设置...
继续阅读 »

问题


设计图中View有阴影和圆角,里面填充了四个按钮。同时设置View的圆角和阴影,阴影并不显示。试了很多次,找了很多办法,记录一下过程。


为了展示父视图的圆角,设置了masksToBounds=YES,将超出父视图的内容clip掉,这样圆角就OK了。不设置时,Subview会超出父视图,看起来像圆角没有设置成功。


接着给父视图设置Shadow却没有成功。一开始以为是代码的问题,写了很多次,数值啊颜色啊大小啊都设置的很大,却还是不显示。百度了一下,提示阴影为超出父视图的部分,如果使用masksToBounds会把超出部分切掉,但去掉masksToBounds会导致圆角失效。


解决方案


给View外面套一层ShadowView,把阴影加到ShadowView上,不设置masksToBounds,再设置真正需要圆角的View,设置masksToBounds即可。

self.shadowView.layer.cornerRadius = 50;
self.shadowView.layer.shadowOffset = CGSizeMake(1, 5);
self.shadowView.layer.shadowOpacity = 0.8;
self.shadowView.layer.shadowColor = [UIColor lightGrayColor].CGColor;
self.actionView.layer.cornerRadius = 50;
self.actionView.layer.masksToBounds = YES;

设置没有SubView的View时并不需要设置masksToBounds属性就可以同时拥有圆角和阴影,但有SubView时为了不让它超出父视图内容(如果都为白色的话超出看着特别像设置没有成功的样子)就必须设置masksToBounds属性了,才会有上面的问题。


Demo


写了一个Demo,展示一下不同的设置效果,给大家参考。
github.com/Yadea-Web/R…

1.第一个View是没有Subview的情况,直接设置cornerRadius即可,不需要设置masksToBounds属性。

2.第二个View有一个子View,不设置masksToBounds属性显示阴影但圆角没生效,因为子视图超出了。

3.第三个View有一个子View,设置masksToBounds属性圆角生效了,但阴影消失了,因为把超出部分截掉了。

4.第四个View有一个子View,设置View的阴影,不设置masksToBounds。设置Subview的cornerRadius,再设置masksToBounds属性。Subview将超出部分截掉,外层展示它应该展示的阴影效果。

5.第五第六展示的是有多个子控件的情况,与三四类似,详见Demo。



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

使用 AndroidX 增强 WebView 的能力

在应用开发过程中,为了在多个平台上保持一致的用户体验和提高开发效率,许多应用程序选择使用 H5 技术。在 Android 平台上,通常使用 WebView 组件来承载 H5 内容以供展示。 WebView 存在的问题 自 Android Lollipop 起,...
继续阅读 »

在应用开发过程中,为了在多个平台上保持一致的用户体验和提高开发效率,许多应用程序选择使用 H5 技术。在 Android 平台上,通常使用 WebView 组件来承载 H5 内容以供展示。


WebView 存在的问题


自 Android Lollipop 起,WebView 组件的升级已经独立于 Android 平台。然而,控制 WebView 的 API(android.webkit) 仍然与平台升级相关。这意味着应用开发者只能使用当前平台所定义的接口,而无法充分利用 WebView 的全部能力。例如: WebView.startSafeBrowsing API 在 Android 8.1 上被添加,该 Feature 由 WebView 提供,即使我们在 Android 7.0 更新 WebView 拥有了该 Feature ,由于 Android 7.0 没有 WebView.startSafeBrowsing API ,我们也没办法使用该功能。


WebView 的实现基于 Chromium 开源项目,而 Android 则基于 AOSP 项目,这两个项目有着不同的发布周期,WebView 往往一个月就可以推出下一个版本,而 Android 则需要一年的时间,对于 WebView 新增的 Feature 我们最迟需要一年才能使用。


AndroidX Webkit 的出现


为了解决上面平台能力和 WebView 不匹配的问题,我们可以独立于平台之外定义一套 WebView API ,并让它随着 WebView 的 Feature 更新 API ,这样解决了现有的问题却导入了另一个问题——如何将新定义的 WebView API 和 WebView 进行衔接。


从应用开发的角度,系统 WebView 难以修改,自己编译定制一个 WebView 并随着 apk 提供是一个很好方案。这时候,我们可以轻松的解决衔接问题,并能够按照需求,任意增改 Feature 而不必等官方更新。同时解决了兼容问题和 WebView 内核碎片化的问题。腾讯 X5 ,UC U4 等都是这个方案。维护一份 WebView 并不是一件容易的事,需要投入更多的人力支持,因为将 WebView 打入包中,还伴随着包体积的急剧增加。


从 Android 官方的角度,可以推动 WebView 上游支持该 WebView API , 而这正是 AndroidX Webkit 的解决方案。Android 官方将定义的 WebView API 放置到 AndroidX Webkit 库,以支持频繁的更新,并在 WebView 上游增加“胶水层”与 AndroidX Webkit 进行衔接,这样在旧版的 Android 平台上,只要安装了拥有"胶水"层代码的 WebView ,也就拥有了新版平台的功能。



“胶水层” 是在某个版本之后才后才支持的,旧版本的 WebView 内核并不支持,这也是为什么在调用之前始终应该检查 isFeatureSupported 的原因。



AndroidX Webkit 的功能


初步了解了 AndroidX Webkit 的产生和实现原理,下面带领大家看一下它都提供了哪些新能力能够增强我们的 WebView 。


向下兼容


如上文分析,AndroidX Webkit 提供了向下的兼容,如下面代码所示,由 WebViewCompat 提供兼容的接口调用。



需要注意的是在调用之前对 WebViewFeature 的检查,对于每个 Feature ,AndroidX Webkit 会取平台和 WebView 所提供 Feature 的并集 ,在调用某个 API 之前必须进行检查,如果平台和 WebView 均不支持该API则将抛出 UnsupportedOperationException 异常。

// Old code:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
   WebView.startSafeBrowsing(appContext, callback);
}

// New code:
if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
   WebViewCompat.startSafeBrowsing(appContext, callback);
}

如果我们扒开 WebViewCompat 的外衣查看他的源码(如下所示),会发现如果在当前版本 Platform API 提供了接口,就会直接调用 Platform API 的接口,而对于低版本,则由 AndroidX Webkit 和 WebView 的"通道"提供服务。

// WebViewCompat#startSafeBrowsing
public static void startSafeBrowsing(@NonNull Context context,
@Nullable ValueCallback<Boolean> callback) {
ApiFeature.O_MR1 feature = WebViewFeatureInternal.START_SAFE_BROWSING;
if (feature.isSupportedByFramework()) {
ApiHelperForOMR1.startSafeBrowsing(context, callback);
} else if (feature.isSupportedByWebView()) {
getFactory().getStatics().initSafeBrowsing(context, callback);
} else {
throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}

对比上面的代码,使用平台 API(old code)时仅可以支持 90% 的用户,而使用 AndroidX Webkit(new code) 则可以覆盖大约 99% 的用户。


代理功能支持


一直以来WebView 的代理设置异常繁琐,当遇到复杂的代理规则就无能为力了。在 AndroidX Webkit 中增加了 ProxyController API 用于为 WebView 设置代理。ProxyConfig.Builder 类提供了设置代理以及配置代理的绕过方式等方法,通过组合可以满足复杂的代理场景。

if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
ProxyConfig proxyConfig = new ProxyConfig.Builder()
.addProxyRule("localhost:7890") //添加要用于所有 URL 的代理
.addProxyRule("localhost:1080") //优先级低于第一个代理,仅在上一个失败时应用
.addDirect() //当前面的代理失败时,不使用代理直连
.addBypassRule("www.baidu.com") //该网址不使用代理,直连服务
.addBypassRule("*.cn") //以.cn结尾的网址不使用代理
.build();
Executor executor = ...
Runnable listener = ...
ProxyController.getInstance().setProxyOverride(proxyConfig, executor, listener);

以上代码定义了一个复杂的代理场景,我们为 WebView 设置了两个代理服务器,localhost:1080 仅当 localhost:7890 失败的情况下启用,addDirect 声明了如果两个服务器都失败则直连服务器,addBypassRule 规定了 http://www.baidu.com 和以 .so 结尾的域名始终不应该使用代理。


白名单代理


如果仅有少量的 URL 需要配置代理,我们可以使用 setReverseBypassEnabled(true) 方法将addBypassRule 添加的 URL 转变为使用代理服务器,而其他的 URL 则直连服务。


安全的 WebView 和 Native 通信支持


建立 WebView 和 Native 的双向通信是使用 Hybrid 混合开发模式的基础,在之前 Android 已经提供了一些机制能够让完成基本的通信,但是已有的接口都存在一些安全和性能问题,在 AndroidX 中增加了一个功能强大的接口 addWebMessageListener 兼顾了安全和性能等问题。


代码示例中将 JavaSript 对象 replyObject 注入到匹配 allowedOriginRules的上下文中,这样只有在可信的网站中才能被使用此对象,也就防止了不明来源的网络攻击者对该对象的利用。

// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    // do something about view, message, sourceOrigin and isMainFrame.
    replyProxy.postMessage("Got it!");
  }
};

HashSet<String> allowedOriginRules = new HashSet<>(Arrays.asList("[https://example.com](https://example.com/)"));
// Add WebMessageListeners.

WebViewCompat.addWebMessageListener(webView, "replyObject", allowedOriginRules,myListener);
调用上述方法之后,在 JavaScript 上下文中我们就可以访问 myObject ,调用 postMessage 就可以回调 Native 端的 onPostMessage 方法并自动切换到主线程执行,当 Native 端需要发送消息给 WebView 时,可以通过 JavaScriptReplyProxy.postMessage 发送到 WebView ,并将消息传递给 onmessage 闭包。
// Web page (in JavaScript)
myObject.onmessage = function(event) {
  // prints "Got it!" when we receive the app's response.
  console.log(event.data);
}
myObject.postMessage("I'm ready!");

文件传递


在以往的通讯机制中,如果我们想传递一个图片只能将其转换为 base64 等进行传输,如果曾经使用过 shouldOverrideUrlLoading 拦截 url 大概率会遇见传输瓶颈,AndroidX Webkit 中很贴心的提供了字节流传递机制。


Native 传递文件给 WebView

// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    // Communication is setup, send file data to web.
    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
      // Suppose readFileData method is to read content from file.
      byte[] fileData = readFileData("myFile.dat");
      replyProxy.postMessage(fileData);
    }
  }
}
// Web page (in JavaScript)
myObject.onmessage = function(event) {
  if (event.data instanceof ArrayBuffer) {
    const data = event.data;  // Received file content from app.
    const dataView = new DataView(data);
    // Consume file content by using JavaScript DataView to access ArrayBuffer.
  }
}
myObject.postMessage("Setup!");

WebView 传递文件给 Native

// Web page (in JavaScript)
const response = await fetch('example.jpg');
if (response.ok) {
    const imageData = await response.arrayBuffer();
    myObject.postMessage(imageData);
}
// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
      byte[] imageData = message.getArrayBuffer();
      // do something like draw image on ImageView.
    }
  }
};

深色主题的支持


Android 10 提供了深色主题的支持,但是在 WebView 中显示的网页却不会自动显示深色主题, 这就表现出严重的割裂感,开发者只能通过修改 css 来达到目的,但这往往费时费力还存在兼容性问题,Android 官方为了改善这一用户体验,为 WebView 提供了深色主题的适配。


一个网页如何表现是和prefers-color-scheme and color-scheme 这两个 Web 标准互操作的。 Android官方提供了一张表阐述了他们之间的关系。


上面这张图比较复杂,简单来说如果你想让 WebView 的内容和应用的主题相匹配,你应该始终定义深色主题并实现 prefers-color-scheme ,而对于未定义 prefers-color-scheme 的页面,系统按照不同的策略选择算法生成或者显示默认页面。



以 Android 12 或更低版本为目标平台的应用 API 设计过于复杂,以 Android 13 或更高版本为目标平台的应用精简了 API ,具体变更请参考官方文档



JavaScript and WebAssembly 执行引擎支持


我们有时候我们会在程序中运行 JavaScript 而不显示任何 Web 内容,比如小程序的逻辑层,使用 WebView 本能够满足我们的要求但是浪费了过多的资源,我们都知道在 WebView 中真正负责执行 JavaScript 的引擎是 V8 ,但是我们又无法直接使用,所以我们的安装包中出现了各种各样的引擎:HermesJSCV8等。


Android 发现了这”群雄割据“的局面,推出了AndroidX JavascriptEngine,JavascriptEngine 直接使用了 WebView 的 V8 实现,由于不用分配其他 WebView 资源所以资源分配更低,并可以开启多个独立运行的环境,还针对传递大量数据做了优化。


代码展示了执行 JavaScript 和 WebAssembly 代码的使用:

if(!JavaScriptSandbox.isSupported()){
return;
}
//连接到引擎
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
//创建上下文 上下文间有简单的数据隔离
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
//执行函数 && 获取结果
final String code = "function sum(a, b) { let r = a + b; return r.toString(); }; sum(3, 4)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Futures.addCallback(resultFuture,
new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
text.append(result);
}
@Override
public void onFailure(Throwable t) {
text.append(t.getMessage());
}
},
mainThreadExecutor); //Wasm运行
final byte[] hello_world_wasm = {
0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x0a ,0x02 ,0x60 ,0x02 ,0x7f ,0x7f ,0x01,
0x7f ,0x60 ,0x00 ,0x00 ,0x03 ,0x03 ,0x02 ,0x00 ,0x01 ,0x04 ,0x04 ,0x01 ,0x70 ,0x00 ,0x01 ,0x05,
0x03 ,0x01 ,0x00 ,0x00 ,0x06 ,0x06 ,0x01 ,0x7f ,0x00 ,0x41 ,0x08 ,0x0b ,0x07 ,0x18 ,0x03 ,0x06,
0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,0x79 ,0x02 ,0x00 ,0x05 ,0x74 ,0x61 ,0x62 ,0x6c ,0x65 ,0x01 ,0x00,
0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x00 ,0x09 ,0x07 ,0x01 ,0x00 ,0x41 ,0x00 ,0x0b ,0x01 ,0x01 ,0x0a,
0x0c ,0x02 ,0x07 ,0x00 ,0x20 ,0x00 ,0x20 ,0x01 ,0x6a ,0x0b ,0x02 ,0x00 ,0x0b,
};
final String jsCode = "android.consumeNamedDataAsArrayBuffer('wasm-1').then(" +
"(value) => { return WebAssembly.compile(value).then(" +
"(module) => { return new WebAssembly.Instance(module).exports.add(20, 22).toString(); }" +
")})";
boolean success = js.provideNamedData("wasm-1", hello_world_wasm);
if (success) {
FluentFuture.from(js.evaluateJavaScriptAsync(jsCode))
.transform(this::println, mainThreadExecutor)
.catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
} else {
// the data chunk name has been used before, use a different name
}

更多支持


AndroidX Webkit 是一个功能强大的库,由于篇幅原因上文将开发者比较常用的功能进行了列举,AndroidX 还提供对 WebView 更精细化的控制,对 Cookie 的便捷访问、对 Web 资源的便捷访问,对 WebView 性能的收集,还有对大屏幕的支持等等强大的 API,大家可以查看发布页面查看最新的功能。


写在最后


本文从实际矛盾出发,带领大家思考 AndroidX Webkit 的产生原因和实现原理,对于AndroidX Webkit 的几个功能分别做了简单的介绍,希望大家能在这篇文章获得一点启发和帮助。


作者:简绘Android
链接:https://juejin.cn/post/7259762775365320741
来源:稀土掘金
收起阅读 »

我的又一个神奇的框架——Skins换肤框架

为什么会有换肤的需求 app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。 换肤是什么 换...
继续阅读 »

为什么会有换肤的需求


app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。


换肤是什么


换肤是将app的背景色、文字颜色以及资源图片,一键进行全部切换的过程。这里就包括了图片资源和颜色资源。


Skins怎么使用


Skins就是一个解决这样一种换肤需求的框架。

// 添加以下代码到项目根目录下的build.gradle
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
// 添加以下代码到app模块的build.gradle
dependencies {
// skins依赖了dora框架,所以你也要implementation dora
implementation("com.github.dora4:dora:1.1.12")
implementation 'com.github.dora4:dview-skins:1.4'
}

我以更换皮肤颜色为例,打开res/colors.xml。

<!-- 需要换肤的颜色 -->
<color name="skin_theme_color">@color/cyan</color>
<color name="skin_theme_color_red">#d23c3e</color>
<color name="skin_theme_color_orange">#ff8400</color>
<color name="skin_theme_color_black">#161616</color>
<color name="skin_theme_color_green">#009944</color>
<color name="skin_theme_color_blue">#0284e9</color>
<color name="skin_theme_color_cyan">@color/cyan</color>
<color name="skin_theme_color_purple">#8c00d6</color>
将所有需要换肤的颜色,添加skin_前缀和_skinname后缀,不加后缀的就是默认皮肤。
然后在启动页应用预设的皮肤类型。在布局layout文件中使用默认皮肤的资源名称,像这里就是R.color.skin_theme_color,框架会自动帮你替换。要想让框架自动帮你替换,你需要让所有要换肤的Activity继承BaseSkinActivity。
private fun applySkin() {
val manager = PreferencesManager(this)
when (manager.getSkinType()) {
0 -> {
}
1 -> {
SkinManager.changeSkin("cyan")
}
2 -> {
SkinManager.changeSkin("orange")
}
3 -> {
SkinManager.changeSkin("black")
}
4 -> {
SkinManager.changeSkin("green")
}
5 -> {
SkinManager.changeSkin("red")
}
6 -> {
SkinManager.changeSkin("blue")
}
7 -> {
SkinManager.changeSkin("purple")
}
}
}

另外还有一个情况是在代码中使用换肤,那么跟布局文件中定义是有一些区别的。

val skinThemeColor = SkinManager.getLoader().getColor("skin_theme_color")
这个skinThemeColor拿到的就是当前皮肤下的真正的skin_theme_color颜色,比如R.color.skin_theme_color_orange的颜色值“#ff8400”或R.id.skin_theme_color_blue的颜色值“#0284e9”。
SkinLoader还提供了更简洁设置View颜色的方法。
override fun setImageDrawable(imageView: ImageView, resName: String) {
val drawable = getDrawable(resName) ?: return
imageView.setImageDrawable(drawable)
}

override fun setBackgroundDrawable(view: View, resName: String) {
val drawable = getDrawable(resName) ?: return
view.background = drawable
}

override fun setBackgroundColor(view: View, resName: String) {
val color = getColor(resName)
view.setBackgroundColor(color)
}

框架原理解析

先看BaseSkinActivity的源码。

package dora.skin.base

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
import android.view.View
import androidx.collection.ArrayMap
import androidx.core.view.LayoutInflaterCompat
import androidx.core.view.LayoutInflaterFactory
import androidx.databinding.ViewDataBinding
import dora.BaseActivity
import dora.skin.SkinManager
import dora.skin.attr.SkinAttr
import dora.skin.attr.SkinAttrSupport
import dora.skin.attr.SkinView
import dora.skin.listener.ISkinChangeListener
import dora.util.LogUtils
import dora.util.ReflectionUtils
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*

abstract class BaseSkinActivity<T : ViewDataBinding> : BaseActivity<T>(),
ISkinChangeListener, LayoutInflaterFactory {

private val constructorArgs = arrayOfNulls<Any>(2)

override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
if (createViewMethod == null) {
val methodOnCreateView = ReflectionUtils.findMethod(delegate.javaClass, false,
"createView", *createViewSignature)
createViewMethod = methodOnCreateView
}
var view: View? = ReflectionUtils.invokeMethod(delegate, createViewMethod, parent, name,
context, attrs) as View?
if (view == null) {
view = createViewFromTag(context, name, attrs)
}
val skinAttrList = SkinAttrSupport.getSkinAttrs(attrs, context)
if (skinAttrList.isEmpty()) {
return view
}
injectSkin(view, skinAttrList)
return view
}

private fun injectSkin(view: View?, skinAttrList: MutableList<SkinAttr>) {
if (skinAttrList.isNotEmpty()) {
var skinViews = SkinManager.getSkinViews(this)
if (skinViews == null) {
skinViews = arrayListOf()
}
skinViews.add(SkinView(view, skinAttrList))
SkinManager.addSkinView(this, skinViews)
if (SkinManager.needChangeSkin()) {
SkinManager.apply(this)
}
}
}

private fun createViewFromTag(context: Context, viewName: String, attrs: AttributeSet): View? {
var name = viewName
if (name == "view") {
name = attrs.getAttributeValue(null, "class")
}
return try {
constructorArgs[0] = context
constructorArgs[1] = attrs
if (-1 == name.indexOf('.')) {
// try the android.widget prefix first...
createView(context, name, "android.widget.")
} else {
createView(context, name, null)
}
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
} finally {
// Don't retain references on context.
constructorArgs[0] = null
constructorArgs[1] = null
}
}

@Throws(InflateException::class)
private fun createView(context: Context, name: String, prefix: String?): View? {
var constructor = constructorMap[name]
return try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
val clazz = context.classLoader.loadClass(
if (prefix != null) prefix + name else name).asSubclass(View::class.java)
constructor = clazz.getConstructor(*constructorSignature)
constructorMap[name] = constructor
}
constructor!!.isAccessible = true
constructor.newInstance(*constructorArgs)
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
val layoutInflater = LayoutInflater.from(this)
LayoutInflaterCompat.setFactory(layoutInflater, this)
super.onCreate(savedInstanceState)
SkinManager.addListener(this)
}

override fun onDestroy() {
super.onDestroy()
SkinManager.removeListener(this)
}

override fun onSkinChanged(suffix: String) {
SkinManager.apply(this)
}

companion object {
val constructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
private val constructorMap: MutableMap<String, Constructor<out View>> = ArrayMap()
private var createViewMethod: Method? = null
val createViewSignature = arrayOf(View::class.java, String::class.java,
Context::class.java, AttributeSet::class.java)
}
}
我们可以看到BaseSkinActivity继承自dora.BaseActivity,所以dora框架是必须要依赖的。有人说,那我不用dora框架的功能,可不可以不依赖dora框架?我的回答是,不建议。Skins对Dora生命周期注入特性采用的是,依赖即配置。
package dora.lifecycle.application

import android.app.Application
import android.content.Context
import dora.skin.SkinManager

class SkinsAppLifecycle : ApplicationLifecycleCallbacks {

override fun attachBaseContext(base: Context) {
}

override fun onCreate(application: Application) {
SkinManager.init(application)
}

override fun onTerminate(application: Application) {
}
}
所以你无需手动配置<meta-data android:name="dora.lifecycle.config.SkinsGlobalConfig" android:value="GlobalConfig"/>,Skins已经自动帮你配置好了。那么我顺便问个问题,BaseSkinActivity中最关键的一行代码是哪行?LayoutInflaterCompat.setFactory(layoutInflater, this)这行代码是整个换肤流程最关键的一行代码。我们来干预一下所有Activity onCreateView时的布局加载过程。我们在SkinAttrSupport.getSkinAttrs中自己解析了AttributeSet。
/**
* 从xml的属性集合中获取皮肤相关的属性。
*/
fun getSkinAttrs(attrs: AttributeSet, context: Context): MutableList<SkinAttr> {
val skinAttrs: MutableList<SkinAttr> = ArrayList()
var skinAttr: SkinAttr
for (i in 0 until attrs.attributeCount) {
val attrName = attrs.getAttributeName(i)
val attrValue = attrs.getAttributeValue(i)
val attrType = getSupportAttrType(attrName) ?: continue
if (attrValue.startsWith("@")) {
val ref = attrValue.substring(1)
if (TextUtils.isEqualTo(ref, "null")) {
// 跳过@null
continue
}
val id = ref.toInt()
// 获取资源id的实体名称
val entryName = context.resources.getResourceEntryName(id)
if (entryName.startsWith(SkinConfig.ATTR_PREFIX)) {
skinAttr = SkinAttr(attrType, entryName)
skinAttrs.add(skinAttr)
}
}
}
return skinAttrs
}

我们只干预skin_开头的资源的加载过程,所以解析得到我们需要的属性,最后得到SkinAttr的列表返回。

package dora.skin.attr

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import dora.skin.SkinLoader
import dora.skin.SkinManager

enum class SkinAttrType(var attrType: String) {

/**
* 背景属性。
*/
BACKGROUND("background") {
override fun apply(view: View, resName: String) {
val drawable = loader.getDrawable(resName)
if (drawable != null) {
view.setBackgroundDrawable(drawable)
} else {
val color = loader.getColor(resName)
view.setBackgroundColor(color)
}
}
},

/**
* 字体颜色。
*/
TEXT_COLOR("textColor") {
override fun apply(view: View, resName: String) {
val colorStateList = loader.getColorStateList(resName) ?: return
(view as TextView).setTextColor(colorStateList)
}
},

/**
* 图片资源。
*/
SRC("src") {
override fun apply(view: View, resName: String) {
if (view is ImageView) {
val drawable = loader.getDrawable(resName) ?: return
view.setImageDrawable(drawable)
}
}
};

abstract fun apply(view: View, resName: String)

/**
* 获取资源管理器。
*/
val loader: SkinLoader
get() = SkinManager.getLoader()
}

当前skins框架只定义了几种主要的换肤属性,你理解原理后,也可以自己进行扩展,比如RadioButton的button属性等。


开源项目传送门


如果你要深入理解完整的换肤流程,请阅读skins的源代码,[github.com/dora4/dview…] 。

作者:dora
链接:https://juejin.cn/post/7258483700815609916
来源:稀土掘金
收起阅读 »

工信部又出新规!爬坑指南

一、背景 工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。 二、整改 2.1 个...
继续阅读 »

一、背景


工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。


二、整改


2.1 个人信息保护


2.1.1 基本模式(无权限、无个人信息获取模式)


这次整改涉及到最大的一个点就是基本模式,基本模式指的是在用户选择隐私协议弹窗时,不能点击“不同意”即退出应用,而是需要给用户提供一个除联网功能外,无任何权限,无任何个人信息获取的模式且用户能正常使用。


这个说法有点抽象,我们来看下友商已经做好的案例。


腾讯视频


从腾讯视频的策略来看,用户第一次使用app,依旧会弹出一个“用户隐私协议”弹窗供用户选择,但是和以往不同的是,“不同意”按钮替换为了“不同意并进入基本功能模式”,用户点击“不同意并进入基本功能模式”则进入到一个简洁版的页面,只提供一些基本功能,当用户点击“进入全功能模式”,则再次弹出隐私协议弹窗。当杀死进程后,再次进入则直接进入基本模式。


网易云音乐


网易云音乐和腾讯视频的产品策略略有不同,在用户在一级授权弹窗点击“不同意”,会跳转至二级授权弹窗,当用户在二级弹窗上点击“不同意,进入基本功能模式”,才会进入基本功能页面,在此页面上点击“进入完整功能模式”后就又回到了二级授权页。当用户杀死进程,重新进入app时,还是会回到一级授权页。


网易云音乐比腾讯视频多了一个弹窗,也只是为了提升用户进入完整模式的概率,并不涉及到新规。


另外,B站、酷狗音乐等都已经接入了基本模式,有兴趣的伙伴可以自行下载体验。


2.1.2 隐私政策内容


如果app存在读取并传送用户个人信息的行为,需要检查其是否具备用户个人信息收集、使用规则,并明确告知读取和传送个人信息的目的、方式和范围。


判断权限是否有读取、修改、传送行为,如果有,需要在隐私协议中明文告知。


举个例子,app有获取手机号码并且保存在服务器,需要在协议中明确声明:读取并传送用户手机号码。


2.2 app权限调用


2.2.1 应用内权限调用

  • 获取定位信息和生物特征识别信息

在获取定位信息以及生物特征识别信息时需要在调用权限前,单独向用户明示调用权限的目的,不能用系统权限弹窗替代。


如上图,申请位置权限,需要在申请之前,弹出弹窗供用户选择,用户同意调用后才可以申请位置权限。

其他权限

其他权限如上面隐私政策一样,需要在调用时,声明是读取、修改、还是传送行为,如下图


2.3 应用软件升级


2.3.1 更新


应用软件或插件更新应在用户授权的情况下进行,不能直接更新,另外要明确告知用户此行为包含下载和安装。


简单来说,就是在app进行更新操作时,需要用弹窗告知用户,是否更新应用,更新的确认权交给用户,并且弹窗上需要声明此次更新有下载和安装两个操作。如下图

2.4 应用签名

需要保证签名的真实有效性。

作者:付十一
链接:https://juejin.cn/post/7253610755126476857
来源:稀土掘金
收起阅读 »

为什么大部分人做不了架构师?

腾小云导读成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设...
继续阅读 »

腾小云导读

成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设计入门教程。接下来本文作者将提取该书籍之精髓,结合自身经验分享架构设计常见方法以及高可用、高性能、可扩散架构模式的实现思路,将架构设计思维“为我所用”、提升日常研效。希望对你有帮助~

目录

1 基本概念与设计方法

2 高性能架构模式

2.1 存储高性能

2.2 计算高性能

3 高可用架构模式

3.1 理论方法

3.2 存储高可用

3.3 计算高可用

4 可扩展架构模式

5 总结

之前本栏目《腾讯专家10年沉淀:后海量时代的架构设计》、《工作十年,在腾讯沉淀的高可用系统架构设计经验》两篇文章中,两位腾讯的开发者结合自身经历,分享了架构设计的实践经验。而本期,本栏目特邀腾讯云对《从0开始学架构》一书提取精髓,并结合亲身经验做分享。

01、基本概念与设计方法

在讲解架构思想之前,我们先统一介绍一下基本概念的含义,避免每个人对系统、框架、架构这些名词的理解不一致导致的误解。下面是《从0开始学架构》作者对每个名词的定义。其作用域仅限本文范畴,不用纠结其在其他上下文中的意义。

系统:系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。 子系统:子系统也是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。模块:从业务逻辑的角度来拆分系统后,得到的单元就是“模块”。划分模块的主要目的是职责分离。组件:从物理部署的角度来拆分系统后,得到的单元就是“组件”。划分组件的主要目的是单元复用。框架:是一整套开发规范,是提供基础功能的产品。架构:关注的是结构,是某一套开发规范下的具体落地方案,包括各个模块之间的组合关系以及它们协同起来完成功能的运作规则。
由以上定义可见,所谓架构,是为了解决软件系统的某个复杂度带来的具体问题,将模块和组件以某种方式有机组合,基于某个具体的框架实现后的一种落地方案。

而讨论架构时,往往只讨论到系统与子系统这个顶层的架构。

可见,要进行架构选型,首先应该知道自己要解决的业务和系统复杂点在哪里,是作为秒杀系统有瞬间高并发,还是作为金融科技系统有极高的数据一致性和可用性要求等。

一般来说,系统的复杂度来源有以下几个方面:

高性能

如果业务的访问频率或实时性要求较高,则会对系统提出高性能的要求。

如果是单机系统,需要利用多进程、多线程技术。

如果是集群系统,则还涉及任务拆分、分配与调度,多机器状态管理,机器间通信,当单机性能达到瓶颈后,即使继续加机器也无法继续提升性能,还是要针对单个子任务进行性能提升。

高可用

如果业务的可用性要求较高,也会带来高可用方面的复杂度。高可用又分为计算高可用和存储高可用。

针对计算高可用,可以采用主备(冷备、温备、热备)、多主的方式来冗余计算能力,但会增加成本、可维护性方面的复杂度。

针对存储高可用,同样是增加机器来冗余,但这也会带来多机器导致的数据不一致问题,如遇到延迟、中断、故障等情况。难点在于怎么减少数据不一致对业务的影响。

既然主要解决思路是增加机器来做冗余,那么就涉及到了状态决策的问题。即如果判断当前主机的状态是正常还是异常,以及异常了要如何采取行动(比如切换哪台做主机)。

对主机状态的判断,多采用机器信息采集或请求响应情况分析等手段,但又会产生采集信息这一条通信链路本身是否正常的问题,下文会具体展开讨论。事实上,状态决策本质上不可能做到完全正确。

而对于决策方式,有以下几种方式:

独裁式:存在一个独立的决策主体来收集信息并决定主机,这样的策略不会混乱,但这个主体本身存在单点问题。 协商式:两台备机通过事先指定的规则来协商决策出主机,规则虽然简单方便,但是如果两台备机之间的协商链路中断了,决策起来就会很困难,比如有网络延迟且机器未故障、网络中断且机器未故障、网络中断其机器已故障,多种情况需要处理。民主式:如果有多台备机,可以使用选举算法来投票出主机,比如 Paxos 就是一种选举算法,这种算法大多数都采取多数取胜的策略,算法本身较为复杂,且如果像协商式一样出现连接中断,就会脑裂,不同部分会各自决策出不同结果,需要规避。

可扩展性

众所周知在互联网行业只有变化才是永远不变的,而开发一个系统基本都不是一蹴而就的,那应该如何为系统的未来可能性进行设计来保持可扩展性呢?

这里首先要明确的一个观点就是,在做系统设计时,既不可能完全不考虑可扩展性,也不可能每个设计点都考虑可扩展性,前者很明显,后者则是为了避免舍本逐末,为了扩展而扩展,实际上可能会为不存在的预测花费过多的精力。

那么怎么考虑系统的未来可能性从而做出相应的可扩展性设计呢?这里作者给出了一个方法:只预测两年内可能的变化,不要试图预测五年乃至十年的变化。因为对于变化快的行业来说,预测两年已经足够远了,再多就可能计划赶不上变化。而对变化慢的行业,则预测的意义更是不大。

要应对变化,主要是将变与不变分隔开来。

这里可以针对业务,提炼变化层和稳定层,通过变化层将变化隔离。比如通过一个 DAO 服务来对接各种变化的存储载体,但是上层稳定的逻辑不用知晓当前采用何种存储,只需按照固定的接口访问 DAO 即可获取数据。

也可以将一些实现细节剥离开来,提炼出抽象层,仅在实现层去封装变化。比如面对运营上经常变化的业务规则,可以提炼出一个规则引擎来实现核心的抽象逻辑,而具体的规则实现则可以按需增加。

如果是面对一个旧系统的维护,接到了新的重复性需求,而旧系统并不支持较好的可扩展性,这时是否需要花费时间精力去重构呢?作者也提出了《重构》一书中提到的原则:事不过三,三则重构。

简而言之,不要一开始就考虑复杂的做法去满足可扩展性,而是等到第三次遇到类似的实现时再来重构,重构的时候采取上述说的隔离或者封装的方案。

这一原则对新系统开发也是适用的。总而言之就是,不要为难以预测的未来去过度设计,为明确的未来保留适量的可扩展性即可。

低成本

上面说的高性能、高可用都需要增加机器,带来的是成本的增加,而很多时候研发的预算是有限的。换句话说,低成本往往并不是架构设计的首要目标,而是设计架构时的约束限制。

那如何在有限的成本下满足复杂性要求呢?往往只有“创新”才能达到低成本的目标。举几个例子:

NoSQL 的出现是为解决关系型数据库应对高并发的问题。 全文搜索引擎的出现是为解决数据库 like 搜索效率的问题。Hadoop 的出现是为解决文件系统无法应对海量数据存储与计算的问题。Facebook 的 HipHop PHP 和 HHVM 的出现是为解决 PHP 运行低效问题。新浪微博引入 SSD Cache 做 L2 缓存是为解决 Redis 高成本、容量小、穿透 DB 的问题。Linkedin 引入 Kafka 是为解决海量事件问题。
上述案例都是为了在不显著增加成本的前提下,实现系统的目标。

这里还要说明的是,创造新技术的复杂度本身就是很高的,因此一般中小公司基本都是靠引入现有的成熟新技术来达到低成本的目标;而大公司才更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。

安全

安全是一个研发人员很熟悉的目标,从整体来说,安全包含两方面:功能安全和架构安全。

功能安全是为了“防小偷”,即避免系统因安全漏洞而被窃取数据,如 SQL 注入。常见的安全漏洞已经有很多框架支持,所以更建议利用现有框架的安全能力,来避免重复开发,也避免因自身考虑不够全面而遗漏。在此基础上,仍需持续攻防来完善自身的安全。

架构安全是为了“防强盗”,即避免系统被暴力攻击导致系统故障,比如 DDOS 攻击。这里一方面只能通过防火墙集运营商或云服务商的大带宽和流量清洗的能力进行防范,另一方面也需要做好攻击发现与干预、恢复的能力。

规模

架构师在宣讲时往往会先说自己任职和设计过的大型公司的架构,这是因为当系统的规模达到一定程度后,复杂度会发生质的变化,即所谓量变引起质变。

这个量,体现在访问量、功能数量及数据量上。

访问量映射到对高性能的要求。功能数量需要视具体业务会带来不同的复杂度。而数据量带来的收集、加工、存储、分析方面的挑战,现有的方案基本都是基于 Google 的三篇大数据论文的理论:

Google File System 是大数据文件存储的技术理论。Google Bigtable 是列式数据存储的技术理论。Google MapReduce 是大数据运算的技术理论。
经过上面的分析可以看到,复杂度来源很多,想要一一应对,似乎会得到一个复杂无比的架构,但对于架构设计来说,其实刚开始设计时越简单越好,只要能解决问题,就可以从简单开始再慢慢去演化,对应的是下面三条原则:

合适原则:不需要一开始就挑选业界领先的架构,它也许优秀,但可能不那么适合自己,比如有很多目前用不到的能力或者大大超出诉求从而增加很多成本。其实更需要考虑的是合理地将资源整合在一起发挥出最大功效,并能够快速落地。简单原则:有时候为了显示出自身的能力,往往会在一开始就将系统设计得非常复杂,复杂可能代表着先进,但更可能代表着“问题”,组件越多,就越可能出故障,越可能影响关联着的组件,定位问题也更加困难。其实只要能够解决诉求即可。演化原则:不要妄想一步到位,没有人可以准确预测未来所有发展,软件不像建筑,变化才是主题。架构的设计应该先满足业务需求,适当的预留扩展性,然后在未来的业务发展中再不断地迭代,保留有限的设计,修复缺陷,改正错误,去除无用部分。这也是重构、重写的价值所在。

即使是 QQ、淘宝这种如今已经非常复杂的系统,刚开始时也只是一个简单的系统,甚至淘宝都是直接买来的系统,随着业务发展也只是先加服务器、引入一些组件解决性能问题,直到达到瓶颈才去重构重写,重新在新的复杂度要求下设计新的架构。

明确了设计原则后,当面对一个具体的业务,我们可以按照如下步骤进行架构设计:

识别复杂度:无论是新设计一个系统还是接手一个混乱的系统,第一步都是先将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。复杂度的主要来源上文已经说过,可以按照经验或者排查法进行分析。方案对比:先看看业界是否有类似的业务,了解他们是怎么解决问题的,然后提出3~5个备选方案,不要只考虑做一个最优秀的方案,一个人的认知范围常常是有限的,逼自己多思考几个方案可以有效规避因为思维狭隘导致的局限性,当然也不要过多,不用给出非常详细的方案,太消耗精力。备选方案的差异要比较明显,才有扩宽思路和对比的价值。设计详细方案:当多个方案对比得出最终选择后,就可以对目标方案进行详细的设计,关键细节需要比较深入,如果方案本身很复杂,也可以采取分步骤、分阶段、分系统的实现方式来降低实现复杂度。当方案非常庞大的时候,可以汇集一个团队的智慧和经验来共同设计,防止因架构师的思维盲区导致问题。

02、高性能架构模式

2.1 存储高性能

互联网业务大多是数据密集型的业务,其对性能的压力也常常来自于海量用户对数据的高频读写压力上。因此解决高性能问题,首先要解决数据读写的存储高性能问题。

读写分离

在大多数业务中,用户查询和修改数据的频率是不同的,甚至是差别很大的,大部分情况下都是读多写少的,因此可以将对数据的读和写操作分开对待,对压力更大的读操作提供额外的机器分担压力,这就是读写分离。

读写分离的基本实现是搭建数据库的主从集群,根据需要提供一主一从或一主多从。

注意是主从不是主备,从和备的差别在于从机是要干活的。

通常在读多写少的情况下,主机负责读写操作,从机只负责读操作,负责帮主机分担读操作的压力。而数据会通过复制机制(如全同步、半同步、异步)同步到从机,每台服务器都有所有业务数据。

既然有数据的同步,就一定存在复制延迟导致的从机数据不一致问题,针对这个问题有几种常见的解法,如:

写操作后同一用户一段时间内的读操作都发给主机,避免数据还没同步到从机,但这个逻辑容易遗漏。读从机失败后再读一次主机,该方法只能解决新数据未同步的问题,无法解决旧数据修改的问题(不会读取失败),且二次读取主机会给主机带来负担,容易被针对性攻击。关键读写操作全部走主机,从机仅负责非关键链路的读,该方法是基于保障关键业务的思路。
除了数据同步的问题之外,只要涉及主从机同时支持业务访问的,就一定需要制定请求分配的机制。上面说的几个问题解法也涉及了一些分配机制的细节。具体到分配机制的实现来说,有两种思路:

程序代码封装:实现简单,可对业务定制化,但每个语言都要自己实现一次,且很难做到同步修改,因此适合小团队。中间件封装:独立出一套系统管理读写的分配,对业务透明,兼容 SQL 协议,业务服务器就无需做额外修改适配。需要支持多语言、完整的 SQL 语法,涉及很多细节,容易出 BUG,且本身是个单点,需要特别保障性能和可用性,因此适合大公司。
分库分表

除了高频访问的压力,当数据量大了以后,也会带来数据库存储方面的压力。此时就需要考虑分库分表的问题。分库分表既可以缓解访问的压力,也可以分散存储的压力。

先说分库,所谓分库,就是指业务按照功能、模块、领域等不同,将数据分散存储到不同的数据库实例中。

比如原本是一个 MySQL 数据库实例,在库中按照不同业务建了多张表,大体可以归类为 A、B 两个领域的数据。现在新建一个库,将原库中 A 领域的数据迁移到新的库中存储,还是按需建表,而 B 领域的数据继续留在原库中。

分库一方面可以缓解访问和存储的压力,另一方面也可以增加抗风险能力,当一个库出问题后,另一个库中的数据并不会受到影响,而且还能分开管理权限。

但分库也会带来一些问题:原本同一个库中的不同表可以方便地进行联表查询,分库后则会变得很复杂。由于数据在不同的库中,当要操作两个库中的数据时,无法使用事务操作,一致性也变得更难以保障。而且当增加备库来保障可用性的时候,成本是成倍增加的。

基于以上问题,初创的业务并不建议在一开始就做这种拆分,会增加很多开发时的成本和复杂度,拖慢业务的节奏。

再说分表。所谓分表,就是将原本存储在一张表里的数据,按照不同的维度,拆分成多张表来存储。

按照诉求与业务的特性不同,可以采用垂直分表或水平分表的方式。

垂直分表相当于垂直地给原表切了一刀,把不同的字段拆分到不同的子表中,这样拆分后,原本访问一张表可以获取的所有字段,现在则需要访问不同的表获取。

垂直分表适合将表中某些不常用又占了大量空间的列(字段)拆分出去,可以提升访问常用字段的性能。

但相应的,当真的需要的字段处于不同表中时,或者要新增记录存储所有字段数据时,要操作的表变多了。

水平分表相当于横着给原表切了一刀,那么原表中的记录会被分散存储到不同的子表中,但是每张子表的字段都是全部字段。

水平分表适合表的量级很大以至影响访问性能的场景,何时该拆分并没有绝对的指标,一般记录数超过千万时就需要警觉了。

不同于垂直分表依然能访问到所有记录,水平分表后无法再在一张表中访问所有数据了,因此很多查询操作会受到影响,比如 join 操作就需要多次查询后合并结果,count 操作也需要计算多表的结果后相加,如果经常用到 count 的总数,可以额外维护一个总数表去更新,但也会带来数据一致性的问题。

值得特别提出的是范围查询,原本的一张表可以通过范围查询到的数据,分表后也需要多次查询后合并数据,如果是业务经常用到的范围查询,那建议干脆就按照这种方式来分表,这也是分表的路由方式之一:范围路由。

所谓路由方式是指:分表后当新插入记录时,如何判断该往哪张表插入。常用的插入方式有以下三种:

范围路由:按照时间范围、ID 范围或者其他业务常用范围字段路由。这种方式在扩充新的表时比较方便,直接加表给新范围的数据插入即可,但是数量和冷热分布可能是不均匀的。 Hash 路由:根据 Hash 运算来路由新记录插入的表,这种方式需要提前就规划好分多少张表,才能决定 Hash 运算方式。但表数量其实很难预估,导致未来需要扩充新表时很麻烦,但数据在不同表中的分布是比较均匀的。配置路由:新增一个路由表来记录数据 id 和表 id 的映射,按照自定义的方式随时修改映射规则,设计简单,扩充新表也很方便。但每次操作表都需要额外操作一次路由表,其本身也成为了单点瓶颈

无论是垂直分表还是水平分表,单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,可以不拆分到多台数据库服务器,毕竟分库也会引入很多复杂性的问题;如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。

NoSQL 数据库

上面发分库分表讨论的都是关系型数据库的优化方案,但关系型数据库也有其无法规避的缺点,比如无法直接存储某种结构化的数据、扩展表结构时会锁表影响线上性能、大数据场景下 I/O 较高、全文搜索的功能比较弱等。

基于这些缺点,也有很多新的数据库框架被创造出来,解决其某方面的问题。

比如以 Redis 为代表的的 KV 存储,可以解决无法存储结构化数据的问题;以 MongoDB 为代表的的文档数据库可以解决扩展表结构被强 Schema 约束的问题;以 HBase 为代表的的列式数据库可以解决大数据场景下的 I/O 问题;以 ES 为代表的的全文搜索引擎可以解决全文检索效率的问题等。

这些数据库统称为 NoSQL 数据库,但 NoSQL 并不是全都不能写 SQL,而是 Not Only SQL 的意思。

NoSQL 数据库除了聚焦于解决某方面的问题以外也会有其自身的缺点,比如 Redis 没有支持完整的 ACID 事务、列式存储在更新一条记录的多字段时性能较差等。因此并不是说使用了 NoSQL 就能一劳永逸,更多的是按需取用,解决业务面临的问题。

关于 NoSQL 的更多了解,推荐大家可以看看《NoSQL 精粹》这本书。

缓存

如果 NoSQL 也解决不了业务的高性能诉求,那么或许你需要加点缓存

缓存最直接的概念就是把常用的数据存在内存中,当业务请求来查询的时候直接从内存中拿出来,不用重新去数据库中按条件查询,也就省去了大量的磁盘 IO 时间。

一般来说缓存都是通过 Key-Value 的方式存储在内存中,根据存储的位置,分为单机缓存和集中式缓存。单机缓存就是存在自身服务器所在的机器上,那么势必会有不同机器数据可能不一致,或者重复缓存的问题,要解决可以使用查询内容做路由来保障同一记录始终到同一台机器上查询缓存。集中式缓存则是所有服务器都去一个地方查缓存,会增加一些调用时间。

缓存可以提升性能是很好理解的,但缓存同样有着它的问题需要应对或规避。数据时效性是最容易想到的问题,但也可以靠同时更新缓存的策略来保障数据的时效性,除此之外还有其他几个常见的问题。

如果某条数据不存在,缓存中势必查不到对应的 KEY,从而就会请求数据库确认是否有新增加这条数据,如果始终没有这条数据,而客户端又反复频繁地查询这条数据,就会变相地对数据库造成很大的压力,换句话说,缓存失去了保护作用,请求穿透到了数据库,这称为缓存穿透。

应对缓存穿透,最好的手段就是把“空值”这一情况也缓存下来,当客户端下次再查询时,发现缓存中说明了该数据是空值,则不会再问询数据库。但也要注意如果真的有对应数据写入了数据库,应当能及时清除”空值“缓存。

为了保障缓存的数据及时更新,常常都会根据业务特性设置一个缓存过期时间,在缓存过期后,到再次生成期间,如果出现大量的查询,会导致请求都传递到数据库,而且会多次重复生成缓存,甚至可能拖垮整个系统,这就叫缓存雪崩,和缓存穿透的区别在于,穿透是面对空值的情况,而雪崩是由于缓存重新生成的间隔期大量请求产生的连锁效应。

既然是缓存更新时重复生成所导致的问题,那么一种解法就是在缓存重新生成前给这个 KEY 加锁,加锁期间出现的请求都等待或返回默认值,而不去都尝试重新生成缓存。

另一种方法是干脆不要由客户端请求来触发缓存更新,而是由后台脚本统一更新,同样可以规避重复请求导致的重复生成。但是这就失去了只缓存热点数据的能力,如果缓存因空间问题被清除了,也会因为后台没及时更新导致查不到缓存数据,这就会要求更复杂的后台更新策略,比如主动查询缓存有效性、缓存被删后通知后台主动更新等。

虽说在有限的内存空间内最好缓存热点数据,但如果数据过热,比如微博的超级热搜,也会导致缓存服务器压力过大而崩溃,称之为缓存热点问题。

可以复制多份缓存副本,来分散缓存服务器的单机压力,毕竟堆机器是最简单有效。此处也要注意,多个缓存副本不要设置相同的缓存过期时间,否则多处缓存同时过期,并同时更新,也容易引起缓存雪崩,应该设置一个时间范围内的随机值来更新缓存。

2.2 计算高性能

讲完存储高性能,再讲计算高性能,计算性能的优化可以先从单机性能优化开始,多进程、多线程、IO 多路复用、异步 IO 等都存在很多可以优化的地方,但基本系统或框架已经提供了基本的优化能力,只需使用即可。

负载均衡

如果单机的性能优化已经到了瓶颈,无法应对业务的增长,就会开始增加服务器,构建集群。对于计算来说,每一台服务器接到同样的输入,都应该返回同样的输出,当服务器从单台变成多台之后,就会面临请求来了要由哪一台服务器处理的问题,我们当然希望由当前比较空闲的服务器去处理新的请求,这里对请求任务的处理分配问题,就叫负载均衡。

负载均衡的策略,从分类上来说,可以分为三类:

DNS 负载均衡:通过 DNS 解析,来实现地理级别的均衡,其成本低,分配策略很简单,可以就近访问来提升访问速度,但 DNS 的缓存时间长,由于更新不及时所以无法快速调整,且控制权在各域名商下,且无法根据后端服务器的状态来决定分配策略。 硬件负载均衡:直接通过硬件设备来实现负载均衡,类似路由器路由,功能和性能都很强大,可以做到百万并发,也很稳定,支持安全防护能力,但是同样无法根据后端服务器状态进行策略调整,且价格昂贵。软件负载均衡:通过软件逻辑实现,比如 nginx,比较灵活,成本低,但是性能一般,功能也不如硬件强大。

一般来说,DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。

所以部署起来可以按照这三层去部署,第一层通过 DNS 将请求分发到北京、上海、深圳的机房;第二层通过硬件负载均衡将请求分发到当地三个集群中的一个;第三层通过软件策略将请求分发到具体的某台服务器去响应业务。

就负载均衡算法来说,多是我们很熟悉的算法,如轮询、加权轮询、负载最低优先、性能最优优先、Hash 分配等,各有特点,按需采用即可。

03、高可用架构模式

3.1 理论方式

CAP 与 BASE
在说高可用之前,先来说说 CAP 理论,即:

在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

大家可能都知道 CAP 定理是什么,但大家可能不知道,CAP 定理的作者(Seth Gilbert & Nancy Lynch)其实并没有详细解释 CAP 三个单词的具体含义,目前大家熟悉的解释其实是另一个人(Robert Greiner)给出的。而且他还给出了两版有所差异的解释。

书中第二版解释算是对第一版解释的加强,他要加强的点主要是:

CAP 描述的分布式系统,是互相连结并共享数据的节点的集合。因为其实并不是所有的分布式系统都会互连和共享数据。CAP 理论是在涉及读写操作的场景下的理论,而不是分布式系统的所有功能。一致性只需要保障客户端读操作能读到最新的写操作结果,并不要求时时刻刻分布式系统的数据都是一致的,这是不现实的,只要保障客户读到的一致即可。可用性要求非故障的节点在合理的时间内能返回合理的响应,所谓合理是指非错误、非超时,即使数据不是最新的数据,也是合理的“旧数据”,是符合可用性的。分区容错性要求网络分区后系统能继续履行职责,不仅仅要求系统不宕机,还要求能发挥作用,能处理业务逻辑。比如接口直接返回错误其实也代表系统在运行,但却没有履行职责。
在分布式系统下,P(分区容忍)是必须选择的,否则当分区后系统无法履行职责时,为了保障 C(一致性),就要拒绝写入数据,也就是不可用了

在此基础上,其实我们能选择的只有 C+P 或者 A+P,根据业务特性来选择要优先保障一致性还是可用性。

在选择保障策略时,有几个需要注意的点:

CAP 关注的其实是数据的粒度,而不是整个系统的粒度,因此对于系统内的不同数据(对应不同子业务),其实是可以按照业务特性采取不同的 CAP 策略的。CAP 实际忽略了网络延迟,也就是允许数据复制过程中的短时间不一致,如果某些业务比如金融业务无法容忍这一点,那就只能对单个对象做单点写入,其他节点备份,无法做多点写入。但对于不同的对象,其实可以分库来实现分布式。当没有发生分区现象时,也就是不用考虑 P 时,上述限制就不存在,此时应该考虑如何保障 CA。当发生分区后,牺牲 CAP 的其中一个并不代表什么都不用做,而是应该为分区后的恢复 CA 做准备,比如记录分区期间的日志以供恢复时使用。

随 CAP 的一个退而求其次,也更现实的追求,是 BASE 理论,即基本可用,保障核心业务的可用性;软状态,允许系统存在数据不一致的中间状态;最终一致性,一段时间后系统应该达到一致。

FMEA 分析法

要保障高可用,我们该怎么下手呢?俗话说知己知彼才能有的放矢,因此做高可用的前提是了解系统存在怎样的风险,并且还要识别出风险的优先级,先治理更可能发生的、影响更大的风险。说得简单,到底怎么做?业界其实已经提供了排查系统风险的基本方法论,即 FMEA(Failure mode and effects analysis)——故障模式与影响分析。

FMEA 的基本思路是,面对初始的架构设计图,考虑假设其中某个部件发生故障,对系统会造成什么影响,进而判断架构是否需要优化。

具体来说,需要画一张表,按照如下步骤逐个列出:

功能点:列出业务流程中的每个功能点。故障模式:量化描述该功能可能发生怎样的故障,比如 MySQL 响应时间超过3秒。故障影响:量化描述该每个故障可能导致的影响,但不用非常精确,比如20%用户无法登录。严重程度:设定标准,给每个影响的严重程度打分。故障原因:对于每个故障,考虑有哪些原因导致该故障。故障概率:对于每个原因,考虑其发生的概率,不用精确,分档打分即可。风险程度:=严重程度 * 故障概率,据此就可以算出风险的处理优先级了,肯定是程度分数越高的越应该优先解决。已有措施、解决措施、后续规划:用于梳理现状,思考未来的改进方案等。
基于上面这套方法论,我们可以有效地对系统的风险进行梳理,找出需要优先解决的风险点,从而提高系统的可用性。

除了 FMEA,其实还有一种应用更广泛的风险分析和治理的理论,即 BCP——业务连续性计划,它是一套基于业务规律的规章流程,保障业务或组织在面对突发状况时其关键业务功能可以持续不中断。

相比 FMEA,BCP 除了评估风险及重要程度,还要求详细地描述应对方案、残余风险、灾备恢复方案,并要求进行相应故障的培训和演习安排,尽最大努力保障业务连续性。

知道风险在哪、优先治理何种风险之后,就可以着手优化架构。和高性能架构模式一样,高可用架构也可以从存储和计算两个方面来分析。

3.2 存储高可用

存储高可用的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来提高可用性。

双机架构

让我们先从简单的增加一台机器开始,即双机架构。

当机器变成两台后,根据两台机器担任的角色不同,就会分成不同的策略,比如主备、主从、主主。

主备复制的架构是指一台机器作为客户端访问的主机,另一台机器纯粹作为冗余备份用,当主机没有故障时,备机不会被客户端访问到,仅仅需要从主机同步数据。这种策略很简单,可以应对主机故障情况下的业务可用性问题,但在平常无法分担主机的读写压力,有点浪费。

主从复制的架构和主备复制的差别在于,从机除了复制备份数据,还需要干活,即还需要承担一部分的客户端请求(一般是分担读操作)。当主机故障时,从机的读操作不会受到影响,但需要增加读操作的请求分发策略,且和主备不同,由于从机直接提供数据读,如果主从复制延迟大,数据不一致会对业务造成更明显的影响。

对于主备和主从两种策略,如果主机故障,都需要让另一台机器变成主机,才能继续完整地提供服务,如果全靠人工干预来切换,会比较滞后和易错,最好是能够自动完成切换,这就涉及双机切换的策略。

在考虑双机切换时,要考虑什么?首先是需要感知机器的状态,是两台机器直连传递互相的状态,还是都传递给第三方来仲裁?所谓状态要包含哪些内容才能定义一台主机是故障呢?是发现一次问题就切换还是多观察一会再切换?切换后如果主机恢复了是切换回来还是自动变备机呢?需不需要人工二次确认一下

这些问题可能都得根据业务的特性来得出答案,此处仅给出三种常见的双机切换模式:

互连式:两台机器直接连接传递信息,并根据传递的状态信息判断是否要切换主机,如果通道本身发生故障则无法判断是否要切换了,可以再增加一个通道构成双通道保障,不过也只是降低同时故障的概率。中介式:通过第三方中介来收集机器状态并执行策略,如果通道发生断连,中介可以直接切换其他机器作为主机,但这要求中介本身是高可用的,已经有比较成熟的开源解决方案如 zookeeper、keepalived。模拟式:备机模拟成客户端,向主机发送业务类似的读写请求,根据响应情况来判断主机的状态决定是否要切换主机,这样可以最真实地感受到客户端角度下的主机故障,但和互连式不同,能获取到的其他机器信息很少,容易出现判断偏差。
最后一种双机架构是主主复制,和前面两种只有一主的策略不同,这次两台都是主机,客户端的请求可以达到任何一台主机,不存在切换主机的问题。但这对数据的设计就有了严格的要求,如果存在唯一 ID、严格的库存数量等数据,就无法适用,这种策略适合那些偏临时性、可丢失、可覆盖的数据场景。

数据集群

采用双机架构的前提是一台主机能够存储所有的业务数据并处理所有的业务请求,但机器的存储和处理能力是有上限的,在大数据场景下就需要多台服务器来构成数据集群。

如果是因为处理能力达到瓶颈,此时可以增加从机帮主机分担压力,即一主多从,称为数据集中集群。这种集群方式需要任务分配算法将请求分散到不同机器上去,主要的问题在于数据需要复制到多台从机,数据的一致性保障会比一主一从更为复杂。且当主机故障时,多台从机协商新主机的策略也会变得复杂。这里有开源的 zookeeper ZAB 算法可以直接参考。

如果是因为存储量级达到瓶颈,此时可以将数据分散存储到不同服务器,每台服务器负责存储一部分数据,同时也备份一部分数据,称为数据分散集群。数据分散集群同样需要做负载均衡,在数据分区的分配上,hadoop 采用独立服务器负责数据分区的分配,ES 集群通过选举一台服务器来做数据分区的分配。除了负载均衡,还需要支持扩缩容,此外由于数据是分散存储的,当部分服务器故障时,要能够将故障服务器的数据在其他服务器上恢复,并把原本分配到故障服务器的数据分配到其他正常的服务器上,即分区容错性。

数据分区 

数据集群可以在单台乃至多台服务器故障时依然保持业务可用,但如果因为地理级灾难导致整个集群都故障了(断网、火灾等),那整个服务就不可用了。面对这种情况,就需要基于不同地理位置做数据分区。

做不同地理位置的数据分区,首先要根据业务特性制定分区规则,大多还是按照地理位置提供的服务去做数据分区,比如中国区主要存储中国用户的数据。

既然分区是为了防灾,那么一个分区肯定不止存储自身的数据,还需要做数据备份。从数据备份的策略来说,主要有三种模式:

集中式:存在一个总备份中心,所有的分区数据都往这个总中心备份,设计起来简单,各个分区间没有联系,不会互相影响,也很容易扩展新的分区。但总中心的成本较高,而且总中心如果出故障,就要全部重新备份。互备式:每个分区备份另一个分区的数据,可以形成一个备份环,或者按地理位置远近来搭对备份,这样可以直接利用已有的设备做数据备份。但设计较复杂,各个分区间需要联系,当扩展新分区时,需要修改原有的备份线路。独立式:每个分区配备自己的备份中心,一般设立在分区地理位置附近的城市,设计也简单,各个分区间不会影响,扩展新分区也容易。但是成本会很高,而且只能防范城市级的灾难。

3.3 计算高可用

从存储高可用的思路可以看出,高可用主要是通过增加机器冗余来实现备份,对计算高可用来说也是如此。通过增加机器,分担服务器的压力,并在单机发生故障的时候将请求分配到其他机器来保障业务可用性。

因此计算高可用的复杂性也主要是在多机器下任务分配的问题,比如当任务来临(比如客户端请求到来)时,如何选择执行任务的服务器?如果任务执行失败,如何重新分配呢?这里又可以回到前文说过的负载均衡相关的解法上。

计算服务器和存储服务器在多机器情况下的架构是类似的,也分为主备、主从和集群。

主备架构下,备机仅仅用作冗余,平常不会接收到客户端请求,当主机故障时,备机才会升级为主机提供服务。备机分为冷备和温备。冷备是指备机只准备好程序包和配置文件,但实际平常并不会启动系统。温备是指备机的系统是持续启动的,只是不对外提供服务,从而可以随时切换主机。

主从架构下,从机也要执行任务,由任务分配器按照预先定义的规则将任务分配给主机和从机。相比起主备,主从可以发挥一定的从机性能,避免成本空费,但任务的分配就变得复杂一些。

集群架构又分为对称集群和非对称集群。

对称集群也叫负载均衡集群,其中所有的服务器都是同等对待的,任务会均衡地分配到每台服务器。此时可以采用随机、轮询、Hash 等简单的分配机制,如果某台服务器故障,不再给它分配任务即可。

非对称集群下不同的服务器有不同的角色,比如分为 master 和 slave。此时任务分配器需要有一定的规则将任务分配给不同角色的服务器,还需要有选举策略来在 master 故障时选择新的 master。这个选举策略的复杂度就丰俭由人了。

异地多活

讲存储高可用已经说过数据分区,计算高可用也有类似的高可用保障思路,归纳来说,它们都可以根据需要做异地多活,来提高整体的处理能力,并防范地区级的灾难。异地多活中的”异地“,就是指集群部署到不同的地理位置,“活”则强调集群是随时能提供服务的,不同于“备”还需要一个切换过程。

按照规模,异地多活可以分为同城异区、跨城异地和跨国异地。显而易见,不同模式下能够应对的地区级故障是越来越高的,但同样的,距离越远,通信成本与延迟就越高,对通信通道可用性的挑战也越高。因此跨城异地已经不适合对数据一致性要求非常高的业务,而跨国异地往往是用来给不同国家的用户提供不同服务的。

由于异地多活需要花费很高的成本,极大地增加系统复杂度,因此在设计异地多活架构时,可以不用强求为所有业务都做异地多活,可以优先为核心业务实现异地多活。尽量保障绝大部分用户的异地多活,对于没能保障的用户,通过挂公告、事后补偿、完善失败提示等措施进行安抚、提升体验。毕竟要做到100%可用性是不可能的,只能在能接受的成本下尽量逼近,所以当可用性达到一定瓶颈后,补偿手段的成本或许更低。

在异地部署的情况下,数据一定会冗余存储,物理上就无法实现绝对的实时同步,且距离越远对数据一致性的挑战越大,虽然可以靠减少距离、搭建高速专用网络等方式来提高一致性,但也只是提高而已,因此大部分情况下, 只需考虑保障业务能接受范围下的最终一致性即可。

在同步数据的时候,可以采用多种方式,比如通过消息队列同步、利用数据库自带的同步机制同步、通过换机房重试来解决同步延迟问题、通过 session id 让同一数据的请求都到同一机房从而不用同步等。

可见,整个异地多活的设计步骤首先是对业务分级,挑选出核心业务做异地多活,然后对需要做异地多活的数据进行特征分析,考虑数据量、唯一性、实时性要求、可丢失性、可恢复性等,根据数据特性设计数据同步的方案。最后考虑各种异常情况下的处理手段,比如多通道同步、日志记录恢复、用户补偿等,此时可以借用前文所说的 FMEA 等方法进行分析。

接口级故障

前面讨论的都是较为宏观的服务器、分区级的故障发生时该怎么办,实际上在平常的开发中,还应该防微杜渐,从接口粒度的角度,来防范和应对接口级的故障。应对的核心思路依然是优先保障核心业务和绝大部分用户可用。

对于接口级故障,有几个常用的方法:限流、排队、降级、熔断。其中限流和排队属于事前防范的措施,而降级和熔断属于接口真的故障后的处理手段。

限流的目的在于控制接口的访问量,避免被高频访问冲垮。

从限流维度来说,可以基于请求限流,即限制某个指标下、某个时间段内的请求数量,阈值的定义需要基于压测和线上情况来逐步调优。还可以基于资源限流,比如根据连接数、文件句柄、线程数等,这种维度更适合特殊的业务。

实现限流常用的有时间窗算法和桶算法。

时间窗算法分为固定时间窗和滑动时间窗。

固定时间窗通过统计固定时间周期内的量级来决定限流,但存在一个临界点的问题,如果在两个时间窗的中间发生超大流量,而在两个时间窗内都各自没有超出限制,就会出现无法被限流拦截的接口故障。因此滑动时间窗采用了部分重叠的时间统计周期来解决临界点问题。

桶算法分为漏桶和令牌桶。

漏桶算法是将请求放入桶中,处理单元从桶里拿请求去进行处理,如果桶堆满了就丢弃掉新的请求,可以理解为桶下面有个漏斗将请求往处理单元流动,整个桶的容量是有限的。这种模式下流入的速率取决于请求的频率,当桶内有堆积的待处理请求时,流出速率是匀速的。漏桶算法适用于瞬时高并发的场景(如秒杀),处理可能慢一点,但可以缓存部分请求不丢弃。

令牌桶算法是在桶内放令牌,令牌数是有限的,新的请求需要先到桶里拿到令牌才能被处理,拿不到就会被丢弃。和漏桶匀速流出处理不同,令牌桶还能通过控制放令牌的速率来控制接收新请求的频率,对于突发流量,可靠累计的令牌来处理,但是相对的处理速度也会突增。令牌桶算法适用于控制第三方服务访问速度的场景,防止压垮下游。

除了限流,还有一种控制处理速度的方法就是排队。当新请求到来后先加入队列,出队端通过固定速度出队处理请求,避免处理单元压力过大。队列也有长度限制,其机制和漏桶算法差不多。

如果真的事前防范真的被突破了,接口很可能或已经发生了故障,还能做什么呢?

一种手段是熔断,即当处理量达到阈值,就主动停掉外部接口的访问能力,这其实也是一种防范措施,对外的表现虽然是接口访问故障,但系统内部得以被保护,不会引起更大的问题,待存量处理被消化完,或者外部请求减弱,或完成扩容后,再开放接口。熔断的设计主要是阈值,需要按照业务特点和统计数据制定。

当接口故障后(无论是被动还是主动断开),最好能提供降级策略。降级是丢车保帅,放弃一下非核心业务,保障核心业务可用,或者最低程度能提供故障公告,让用户不要反复尝试请求来加重问题了。比起手动降级,更好的做法也是自动降级,需要具备检测和发现降级时机的机制。

04、可扩展架构模式

再回顾一遍互联网行业的金科玉律:只有变化才是不变的。在设计架构时,一开始就要抱着业务随时可能变动导致架构也要跟着变动的思想准备去设计,差别只在于变化的快慢而已。因此在设计架构时一定是要考虑可扩展性的。

在思考怎样才是可扩展的时候,先想一想平常开发中什么情况下会觉得扩展性不好?大都是因为系统庞大、耦合严重、牵一发而动全身。因此对可扩展架构设计来说,基本的思想就是拆分。

拆分也有多种指导思想,如果面向业务流程来谈拆分,就是分层架构;如果面向系统服务来谈拆分,就是 SOA、微服务架构;如果面向系统功能来拆分,就是微内核架构。

分层架构

分层架构是我们最熟悉的,因为互联网业务下,已经很少有纯单机的服务,因此至少都是 C/S 架构、B/S 架构,也就是至少也分为了客户端/浏览器和后台服务器这两层。如果进一步拆分,就会将后台服务基于职责进行自顶向下的划分,比如分为接入层、应用层、逻辑层、领域层等。

分层的目的当然是为了让各个层次间的服务减少耦合,方便进行各自范畴下的优化,因此需要保证各层级间的差异是足够清晰、边界足够明显的,否则当要增加新功能的时候就会不知道该放到哪一层。各个层只处理本层逻辑,隔离关注点。

额外需注意的是一旦确定了分层,请求就必须层层传递,不能跳层,这是为了避免架构混乱,增加维护和扩展的复杂度,比如为了方便直接跨层从接入层调用领域层查询数据,当需要进行统一的逻辑处理时,就无法切面处理所有请求了。

SOA 架构

SOA 架构更多出现在传统企业中,其主要解决的问题是企业中 IT 建设重复且效率低下,各部门自行接入独立的 IT 系统,彼此之间架构、协议都不同,为了让各个系统的服务能够协调工作,SOA 架构应运而生。

其有三个关键概念:服务、ESB 和松耦合。

服务是指各个业务功能,比如原本各部门原本的系统提供的服务,可大可小。由于各服务之间无法直接通信,因此需要 ESB,即企业服务总线进行对接,它将不同服务连接在一起,屏蔽各个服务的不同接口标准,类似计算机中的总线。松耦合是指各个服务的依赖需要尽量少,否则某个服务升级后整个系统无法使用就麻烦了。

这里也可以看出,ESB 作为总线,为了对接各个服务,要处理各种不同的协议,其协议转换耗费了大量的性能,会成为整个系统的瓶颈。

微服务

微服务是近几年最耳熟能详的架构,其实它和 SOA 有一些相同之处,比如都是将各个服务拆分开来提供能力。但是和 SOA 也有一些本质的区别,微服务是没有 ESB 的,其通信协议是一致的,因此通信管道仅仅做消息的传递,不理解内容和格式,也就没有 ESB 的问题。而且为了快速交付、迭代,其服务的粒度会划分地更细,对自动化部署能力也就要求更高,否则部署成本太大,达不到轻量快速的目的。

当然微服务虽然很火,但也不是解决所有问题的银弹,它也会有一些问题存在。如果服务划分的太细,那么互相之间的依赖关系就会变得特别复杂,服务数量、接口量、部署量过多,团队的效率可能大降,如果没有自动化支撑,交付效率会很低。由于调用链太长(多个服务),因此性能也会下降,问题定位会更困难,如果没有服务治理的能力,管理起来会很混乱,不知道每个服务的情况如何。

因此如何拆分服务就成了每个使用微服务架构的团队的重要考量点。这里也提供一些拆分的思路:

三个火枪手原则:考虑每三个人负责一个服务,互相可以形成稳定的人员备份,讨论起来也更容易得出结论,在此基础上考虑能负责多大的一个服务。基于业务逻辑拆分:最直观的就是按逻辑拆分,如果职责不清,就参考三个火枪手原则确定服务大小。基于稳定性拆分:按照服务的稳定性分为稳定服务和变动服务,稳定服务粒度可以粗一些,变动服务粒度可以细一些,目的是减少变动服务之间的影响,但总体数量依然要控制。基于可靠性拆分:按照可靠性排序,要求高的可以拆细一些,由前文可知,服务越简单,高可用方案就会越简单,成本也会越低。优先保障核心服务的高可用。基于性能拆分:类似可靠性,性能要求越高的,拆出来单独做高性能优化,可有效降低成本。
微服务架构如果没有完善的基础设施保障服务治理,那么也会带来很多问题,降低效率,因此根据团队和业务的规模,可以按以下优先级进行基础设施的支持:

优先支持服务发现、服务路由、服务容错(重试、流控、隔离),这些是微服务的基础。接着支持接口框架(统一的协议格式与规范)、API 网关(接入鉴权、权限控制、传输加密、请求路由等),可以提高开发效率。然后支持自动化部署、自动化测试能力,并搭建配置中心,可以提升测试和运维的效率。最后支持服务监控、服务跟踪、服务安全(接入安全、数据安全、传输安全、配置化安全策略等)的能力,可以进一步提高运维效率。
微内核架构
最后说说微内核架构,也叫插件化架构,顾名思义,是面向功能拆分的,通常包含核心系统和插件模块。在微内核架构中,核心系统需要支持插件的管理和链接,即如何加载插件,何时加载插件,插件如何新增和操作,插件如何和核心引擎通信等。

举一个最常见的微内核架构的例子——规则引擎,在这个架构中,引擎是内核,负责解析规则,并将输入通过规则处理后得到输出。而各种规则则是插件,通常根据各种业务场景进行配置,存储到数据库中。

05、总结

人们通常把某项互联网业务的发展分为四个时期:初创期、发展期、竞争期和成熟期。

在初创期通常求快,系统能买就买,能用开源就用开源,能用的就是好的,先要活下来;到了发展期开始堆功能和优化,要求能快速实现需求,并有余力对一些系统的问题进行优化,当优化到顶的时候就需要从架构层面来拆分优化了;进入竞争期后,经过发展期的快速迭代,可能会存在很多重复造轮子和混乱的交互,此时就需要通过平台化、服务化来解决一些公共的问题;最后到达成熟期后,主要在于补齐短板,优化弱项,保障系统的稳定。

在整个发展的过程中,同一个功能的前后要求也是不同的,随着用户规模的增加,性能会越来越难保障,可用性问题的影响也会越来越大,因此复杂度就来了。

对于架构师来说,首要的任务是从当前系统的一大堆纷繁复杂的问题中识别出真正要通过架构重构来解决的问题,集中力量快速突破,但整体来说,要徐徐图之,不要想着用重构来一次性解决所有问题。

对项目中的问题做好分类,划分优先级,先易后难,才更容易通过较少的资源占用,较快地得到成果,提高士气。然后再循序渐进,每个阶段控制在 1~3 个月,稳步推进。

当然,在这个过程中,免不了和上下游团队沟通协作,需要注意的是自己的目标和其他团队的目标可能是不同的,需要对重构的价值进行换位思考,让双方都可以合作共赢,才能借力前进。

还是回到开头的那句话,架构设计的主要目的是为了解决软件系统复杂度带来的问题。首先找到主要矛盾在哪,做到有的放矢,然后再结合知识、经验进行设计,去解决面前的问题。

祝各位开发者都成为一名合格的架构师。以上便是本次分享的全部内容,如果觉得内容有用,欢迎转发分享。

-End-

原创作者|Cloudox


作者:腾讯云开发者
链接:https://juejin.cn/post/7251779626682023994
来源:稀土掘金

收起阅读 »

被约谈,两天走人,一些思考

前言 个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流 来龙去脉 上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎有什么事发生 微信上...
继续阅读 »

前言


个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流


来龙去脉


上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎有什么事发生


微信上问了一句:什么情况?


前端同事:裁员,做好准备


公司现状


从我去年入职公司后,就在喊降本增效了,周一晨会时不时也会提一下降本增效,毕竟大环境不好,公司的业务这两年也受到不小的影响


今年好几个项目组人手不够,两三月前还在疯狂面试前后端测试产品,我们这边的业务线前端都面试都超过五十个人了,最后招了一个前端一个后端一个测试


想着这种情况,公司薪资给的也不高,新员工不大量招就算降本了吧,再优化优化各方面流程等提提效率,没想到降本的大刀直接落下来首先砍掉的是技术开发人员


裁员情况


公司北京总部这边,目前我们部门是裁了两个前端一个后端,其他部门也有有裁员,人数岗位就不清楚了


从被裁掉的同事那边了解到的消息,上周三下班后下午找他们谈的,周四交接,周五下班后就走了,按照法律规定赔偿


上周只是一个开始,应该是边裁边看,什么时候结束最终裁员比例目前还不清楚,由其他来源得到的消息来源说是这次裁员力度很大


现在如果不是核心项目员工,如果不是和领导关系比较好的员工,每个人头上都悬着一把达摩克利斯之剑


个人思考


看待裁员


我认为首先是放平心态吧


国际经济形去全球化,贸易战,疫情,到现在的各种制裁,俄乌战争等,极端气候频发,真是多灾多难的年代


裁员这几年大家也见多了,该来的总会来


我认为裁员好说,正常赔偿就行,好聚好散,江湖再见


企业层面


裁员也是企业激发组织活力的一种方式,正常看待就行,关于企业组织活力介绍的,这里推荐一本前段时间刚读完的一本书 《熵减:华为活力之源》



熵是来源于物理科学热力学第二定律的概念,热力学第二定律又称熵增定律。熵增表现为功能减弱直到逐渐丧失,而熵减表现为功能增强...



个人层面


1.如果公司正常走法律流程,拿赔偿走人,继续找工作,找工作的过程也能发现自己的不错,更加了解市场,甚至倒逼自己成长


2.如果公司只想着降低成本,不做人事,有那种玩下三滥手段的公司,一定要留好证据,拍照,录音,截图,保存到自己的手机或者云盘里,不想给赔偿或恶意玩弄手段的,果断仲裁,我们员工相对企业来讲是弱势群体,这时候要学会用法律武器保护自己(可能也是唯一的武器)



这年头行情不好,老板损失的可能只是近期收益,有的员工失去的可能是全家活下去的希望



日常准备


做好记录


日常自己工作上的重大成果,最好定期梳理一下,或者定期更新简历,也可以不更新简历,找地方记录下来,例如项目上的某个重大模块的开发升级,或者做的技术上的性能优化等,我是有写笔记博客的习惯,技术相关的有时间一般会写成文章发到社区里


保持学习


日常保持学习的基本状态,这个可能我们每个人都会有这个想法,但是能定期沉下心来去学习提升,系统地去提升自己的时候,很少能坚持下来,万事开头难,开头了以后剩下的是坚持,我自己也是,有些事情经常三天打鱼,两天晒网,一起加油


关注公司


如果公司有查考勤,或者重点强调考勤了,一般都是有动作了,我们公司这次就是,年中会后的第二周吧,大部门通报考勤情况,里面迟到的还有排名,没多久就裁员了


保护自己


有的公司可能流程操作不规范,也有的可能不想赔偿或者少赔偿,可能会在考勤上做文章,例如迟到啥的,如果公司有效益不好的苗头,一定要注意自己这方面的考勤,以及自己的绩效等,做好加班考勤截图,领导HR与自己的谈话做好录音,录屏等,后面可能用的上,也可能会让自己多一点点谈判筹码


经营关系


虽然裁员明面上都是根据工作表现来的,好多时候大家表现都差不多,这个时候就看人缘了,和领导关系好的,一般都不是优先裁员对象,和领导团队成员打成一片真的很重要



以前我还有过那种想法:


我一个做技术的,我认真做好我自己的工作不就行了?专心研究技术,经过多年的工作发现,很多时候真的不行,我们不是做的那种科研类的,只有自己能搞,国内的大部分软件开发岗可能都是用的开源的技术做业务相关的,这种没什么技术难度,技术上来看基本没有什么替代性的难度


可能可替代性比较难的就是某个技术人长期负责的某个大模块,然后写了一堆屎山吧,毕竟拉屎容易,吃屎难


越是优秀的代码,可读性越强,简洁优雅,像诗一样



关于简历


如果是刚毕业的,可能简历上还好,大部分都优化都是已经是有一定的工作经验了,简历的更新就比较重要了,尤其工作了两三年了,如果简历看起来内容很少,不是那么丰富或者看起来很简陋,在简历筛选这一关会降低自己的面试几率,这时候一定要丰富一下,也有一些可能不知道自己简历是否丰富的,网上有那种简历模板可以搜搜看看,也可以找大佬帮忙看看,也有技术圈提供简历优化的有偿服务


再找工作


我个人的感觉是如果还是继续老本行继续打工,这年头行情不好,最好第一时间找工作,不能因为拿了赔偿就想着休一个月再说之类的,我周围有那种本来准备休半个月或一个月的,结果一下子休了一年以上的,我面试的时候如果遇到那种空窗期很长的,如果第一轮技术面能力都差不多的情况,到第二轮的领导面或者HR面,他们有优先考虑让空窗期短的人加入


关于空窗期


基本所有的公司都会关注离职空窗期,如果这个空窗期时间长了,那么求职的竞争力会越来越小,我在面试的时候我也会比较关注空窗期,因为我会有如下思考(举个例子,纯属乐子哈)


1.为什么这个人求职者三个月多了不找工作,家里有矿?家里有矿还上班,工作不会是找个地方打发时间的吧



我朋友的朋友就是这样,北京土著,家中独子,前几年拆迁了,家里好几套房,自己开俩车,人家上班就是找地方交个社保,顺便打发一下时间




2.能力不行吗?找工作这么久都没找到,是太菜了吗?还是太挑剔了?长时间不敲代码,手也生疏了,来我们团队行不行呀,我们这里赶项目压力这么大,招进来万一上手一段时间干不了怎么办,自己还被牵连了



几年前在某家公司做团队leader的时候,我们做的又是AI类项目,用的技术也比较前沿,当时AI的生态还不完善,国内做AI的大部分还处于摸索阶段,项目中用的相关技术栈也没个中文文档,由于公司创业公司,价格给的很低,高手招不进来,没办法只能画饼招综合感觉不错的那种,结果好几个人来了以后又是培训,又是有把手把手教的,结果干了没多久干不动走了,留下的烂摊子还得自己处理



关于社保


如果自己家里没矿,最好还是别让社保断了,拿北京举例,社保断了影响医疗报销,影响买车摇号等等


如果实在没找到工作,又马上要断缴社保了,可以找个第三方机构帮忙代缴,几千块钱,这时候的社保补缴相对来讲代价就比较高了



我遇到的情况是,社保断了一个月,后来找到工作了,第三方机构补缴都补不了,后来一通折腾总算弄补缴上了



关于入职


先拿offer,每一家公司的面试都认真对待,抱着一颗交流开放互相尊重的心


如果自己跳槽频繁,再找公司,可能需要考虑一下自己是否能够长待了,跳槽越频繁,后面找工作越困难,没有哪个公司希望招的人干一年就走了


所以面试结束后,最好根据需要面试情况,以及网上找到的资料,分析一下公司的业务模式了,分析这家公司的行业地位,加入的业务线或者部门是否赚钱,所在的团队在公司属于什么情况,分析团队是否是边缘部门,招聘的业务线是否核心业务线,如果不是核心业务线,可能过段时间效益不好还会被砍掉,有时候虽然看拿了对应的赔偿,但是再找工作,与其他同级选手对比的话,竞争力会越来越低


不论是技术面试官,还是负责面试的HR,大部分也都是公司的普通员工,他们可能不会为公司考虑,基本都会为自己考虑的,万一招了个瘟神到公司或者团队里,没多久自己团队也解散了怎么整



这里也许迷信了,基于我的一些经历来看有些人确实会有一些人是看风水,看人分析运势的


之前在创业公司的时候,有幸和一些投资人,上市公司的总裁,央企董事长等所谓的社会高层接触过,越是那些顶级圈里的人,有些人似乎很看中这个,他们有人研究周易,有人信仰佛教,有人招聘必须看人面相,有人师从南怀瑾等等



再次强调


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步

作者:草帽lufei
链接:https://juejin.cn/post/7264236820725366840
来源:稀土掘金
收起阅读 »

《有钱人和你想的不一样》阅读笔记

作者哈维·艾克,世界顶级商业教练。 作者从小家境窘困,十三岁就开始打工,曾经送报、卖冰淇淋、摆摊子卖货物,还到海滩上兜售防晒油。在约克大学读了一年之后,决定开始全心追求他致富的梦想。他辗转在美国西岸和东岸几个城市里做过许多工作,甚至自己当老板,创业十几次却都...
继续阅读 »

作者哈维·艾克,世界顶级商业教练。


作者从小家境窘困,十三岁就开始打工,曾经送报、卖冰淇淋、摆摊子卖货物,还到海滩上兜售防晒油。在约克大学读了一年之后,决定开始全心追求他致富的梦想。他辗转在美国西岸和东岸几个城市里做过许多工作,甚至自己当老板,创业十几次却都无法成功。


多年的挫败没有把他打垮。他又开了一家贩售体育用品的公司,在两年半里快速成长为连锁企业,开设了十家分店。然后,他把这项事业卖给一家名列《财星》五百大的企业。他赚了一笔大钱。可是,由于他的错误投资和大肆挥霍,不到两年就把钱莫名其妙用光了。他又回到了起点。


他痛定思痛,回顾自己的成长背景和金钱观,分析各种与致富有关的内在思惟,体会到许多与金钱、致富梦想和人生态度有关的道理。他发现,每一个人心里都有一张金钱蓝图,假如不修改这张蓝图,那么就算赚了大钱,这笔钱也不会留在身边太久。


带着这样的信念,他彻底修改了自己的金钱蓝图,最后成为了超级大富翁,并且越来越有钱。


成功之后,他致力于帮助更多人实现致富的梦想,超越现有生活,让人生变得不同。他的潜能开发公司红遍全美,以独特的平民气息吸引了来自世界各地的无数普罗大众,一堂课经常塞满了两千人。很多上过他课的人表示,这堂课大大改变了他们的人生,使他们开始累积财富,得到快乐与成功。


现实社会


大多数人的现状:





  • 对其财政状况担忧而夜不能寐,早上睁眼起来,就去上班



  • 怀抱希望,自己马上就能赚到可以付清账单的钱。而当他们收入提高时,开支又梦魇般地同步增多



  • 唯一解决方法就是努力工作。希望通过多赚钱,最终变得节俭。但是生活水平同收入平行增长。负债日益增加。


这部分人就必须越来越努力而长时间地工作才能保持生活水平,以偿还债务和账单。


像仓鼠的轮子:尽管他们尽全力,却没有进步。同理一些人不管多努力地工作,只要处于仓鼠的轮子中,他就是原地踏步,这种现象在《财务自由之路2》被称为仓鼠之轮。


财富档案


书里讲了17个有钱人的思维习惯,其实书里有钱人17个思维习惯和大多数成功人士书籍所讲大同小异。我摘录几种我认为重要的:





  1. 有钱人相信:"我创造我的人生"。穷人相信:"人生发生在我的身上"。必须相信,只有你能造成自己的成功,也只有你能造成你的平庸,只有你能造成自己为钱辛苦或是前途茫茫。不管出于自觉还是不自觉的原因,你的人生状态都是自己造成的。行动上远离负面能量的人,每天检视自己的行为



  2. 有钱人努力让自己有钱。穷人一直想着要变得有钱。如果你并没有达到你嘴巴上所说的很渴望达到的财富成就,很可能是1. 潜意识时并不真的想变得有钱;2. 并不想付出应有的代价去创造财富



  3. 有钱人专注于机会。穷人专注于障碍。有钱人冒的是经过计划的风险,他们会做研究,付出必要的努力,再根据具体的资讯和事实做出决定。穷人,他们期待失败。他们对于自己和自己的能力都没有信心。穷人相信,如果事情行不通,就像世界末日来临。而正因为他们总是看见阻碍,所以他们通常都不愿意冒险。



  4. 有钱人欣赏其他的有钱人和成功人士,穷人讨厌有钱人和成功人士。不论你现在什么状态,首先做到不讨厌其他人,积极从其他人身上学到优点或教训



  5. 有钱人乐意宣传自己和自己价值观,穷人把推销和宣传看成不好的事。敢于推销自己和自己的产品,学习推销能力,不会或不敢推销一定不会成功。



  6. 有钱人大于他们的问题,穷人小于他们的问题。遇到问题不要急于去抱怨和责怪,因为抱怨和责怪来自于恐惧,要明白如果遇到了一个很大的问题,说明自己很小,要么列出多条解决方案去解决问题,要么行动起来去将自己变大。不要专注于问题,而专注于目标。



  7. 有钱人选择根据结果拿酬劳。穷人选择根据时间拿酬劳。稳定的薪酬会阻碍你挣更多的钱。要么选择可以以自己的成果或者公司的运营来领薪的工作,要么自己当老板,可以从兼职做起。



  8. 有钱人专注于自己的净值。穷人专注于自己的工作收入。净值指你所拥有的所有东西,现金,不动产,股票等等可以转化成现金的东西。提高收入,不乱花钱增加存款,有了存款加强投资,简化不必要的花销。看理财类书籍



  9. 有钱人管理他们的钱,穷人很会搞丢他们的钱。不是等我拥有了很多钱后我再开始管理,而是我开始管理我的钱了以后,我才会拥有很多钱。所以会管理钱很重要,不要在乎他们的多少。



  10. 有钱人让钱帮他们工作。穷人幸苦工作挣钱。一开始你努力工作挣钱,之后就要让钱为你努力工作。被动收入很重要,一定要去培养和开发。被动收入大于你的支出时,你才算真正的财务自由



  11. 有钱人就算恐惧也会采取行动,穷人却会让恐惧阻止他们行动有些事一开始做起来是痛苦的,但是你坚持下去就可以穿过痛苦区到达舒适区,不断的扩大你的舒适区因为舒适区和财务区是成正比的,如果一直待在舒适区你是不会成长的



  12. 有钱人持续学习成长,穷人以为他们知道了一切。一件事只有你经历了才有资格说你知道,否则你只是听说过看到过或读到过。如果能学到一个知识或者有了一点进步就是值得的。


最后





  • 行动比不行动好,只会YY没卵用



  • 培养财商很重要,有点小钱绝对不是只有买!买!买!



  • 月光族没希望,劳碌命,学会储蓄,积累一笔初始资金



  • 巴菲特说的:世界上最好的投资,就是投资你自己


如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。

作者:凌览
来源:mdnice.com/writing/d714945d3c0f4a29b02c5e4488341632
收起阅读 »

《Thinking In Java》作者:不要使用并发!

前言 今天纯粹就是带你们来读读书的~ 最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很...
继续阅读 »

前言



今天纯粹就是带你们来读读书的~


最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很多Java工程师都读过这本书,可以说是Java编程思想的良心之作。


虽然布鲁斯是我的老朋友,但我不得不吐槽一下,大概通读了一遍《On Java》之后,我心里大体认为是不如《Thinking In Java》的,可能和写小说一样,读者的要求高了,而作者的年纪大了。


我认识布鲁斯很多年了,他是个比较幽默风趣的人,经常在书中直言不讳某编程语言的垃圾之处,同时又对该语言的未来做一点展望,算是一个很中肯且典型的直男程序猿。


最后说一点,我认识他,他不认识我。





正文



我着重看了自己比较感兴趣的并发编程这一块,想知道这位大佬对于目前Java并发编程是否有新的看法和意见,不出我所料,他没讲什么重要的东西,但是好像又讲了,带着吐槽批判式的口吻,陈列了他喜欢和讨厌Java并发编程的地方。


所以我把一些我觉得有意思的地方画出来,分享给大家,看看一个资深Java大佬对并发编程的理解。



1、大佬的并发定律


111.png



其实看到作者研究出的这4条定律时,我还是挺意外的,第一句就点题了,不要使用并发。


仔细想想好像也对……再琢磨一下咦有感觉……最后回忆一下这些年参与的项目……哇擦好有道理!


接下来3条基本算是总纲了,后面的内容都是对这几条的说明。





2、你已埋下的隐患


222.png



这里就是对2、3条的具体说明了,有些话我觉得略显啰嗦,我把对于程序员来讲比较重要的一句话画出来了。


你很容易写出一个看起来运行正常但实际上有问题的并发程序。


看到这句话的时候是不是已经开始默默打开自己的IDEA了,然后审视了一遍自己提交的代码?


别看了,你埋的炸弹还少么,能看出花来吗。


看清楚作者后面那句:你这个问题只有满足最罕见的条件时,才会将自己暴露出来。


我可以这么说,在座绝大部分同行去了下一家公司干活,可能上一家公司的新同事才会在你毫不知情的时候默默踩到你埋的地雷然后被炸个粉碎,而你在新公司也正在踩别人的雷,出来混都是要还的。





3、别否认你就是这种人


333.png



看到这里的时候,我忍不住亲了布鲁斯一口,他痛快的描述出了我一直以来在工作中说不清道不明的烦躁,因为你总会遇到这样的人,同时很难发现自己到底是不是这样的人。


我在工作前3年其实如履薄冰,感觉自己什么都学了,但去了公司发现什么都不会,怀揣着自我否定一点点完成别人布置的任务,直到工作5年以后才有一种醍醐灌顶的感觉,理解了自己做的是什么,接下来要学习哪个方向,以前学到那么多东西究竟是怎么串联起来的,这是一种打通任督二脉的满足感。


等到工作8年之后,才真正开始回头看Java语言,对以前烦厌欲呕的Java基础提起莫名的兴趣,同时喜欢看书,写案例,尝试阅读别人的源码等等,此时我才真正有自己一只腿迈进Java领域的意识。


同时,在工作中会对许多能力一般但沟通较为偏执的同事产生抵触情绪,我有时会认为这是一种大人看小孩耍脾气的感觉,这个只有在工作多年之后才会产生,作者很准确的阐述出了我描绘不出的这种解释。


同样的,我认为在这个成长的过程中,我一定也成为过别人心中眼高手低的人。


我在这里能分享给大家的经验就是,在工作中多学习少争论,多和厉害的人走近一点,虚心把对方的东西都学过来,长此以往你会进步神速,这不是你在网上学习能得到的,一定是在工作中。





4、高级Javaer都有过的想法


444.png



这里我为什么专门画出来,因为很多高级javaer一定有过类似的想法,就是发现了Java并不擅长做并发编程,是否可以用其他语言来完成,而Java只做他自己擅长的事。


至少我以前就想过,可现实层面我认为是异想天开的,尤其是工作中,基本都是团队开发,这种想法就已经几乎被pass掉了,同时为了某一个领域的实现专门引入一门编程语言甚至体系,得不偿失,毕竟Java不擅长但却成熟,光是网上卖课郎告诉你的就有N种诸如《Java千亿级高并发解决方案》、《Java万亿级电商实战》等等这样的受用终生的鬼东西。


而你辛辛苦苦跟着学完后,发现玛德用不上,就像你学了《九阴真经》后以为可以当武林盟主最终却进了铁匠铺,而铁匠铺老板还不想听你鬼扯只想让你每天加班多打几把武器。


图片中我还画了个圈,我想不少人应该知道这门语言,还蛮有名的,就是国内不太火,这有编程历史因素在里面,其实还有一门语言也蛮适合的,而且这几年也挺火,我想你也猜到了,我觉得5年+的Java工程师都应该关注甚至学习一下。





5、我和大佬不谋而合


555.png



这是接近尾声的部分了,也是这位作者熟悉的笔法,发泄完自己的情绪后又开始对Java的某新版本极尽赞美,典型的被PUA了。


但不得不说,Java8我也认为是革命性的版本,在这个版本发布以前,作为Java工程师你甚至不会想到它敢做到这个地步,就像布鲁斯书中讲的,这是史诗般的魔法。


你可以在Java8的版本里发现一些其他语言的影子,这没什么,天下语言一大抄,发展到一定程度,已经是避免不了的趋势了。


重要的是,这个版本给Java上油了,为后续的版本提供了活力,而Java17作为官方长久支持版本的其中一个非常重要的版本,你可以发现有其他框架给它背书,比如SpringBoot3只支持Java17,而Jenkins也宣布在新版本放弃Java8并且该团队更推荐Java17,IDEA后续新版本可能也会放弃Java8,这明显就是小圈子,有利益的勾连,但对Java本身发展不是坏事。


所以,Java8的核心技术点最应该学习,如果现在还一点不会,赶紧学吧,我认为这是后续版本的基础了,lambada表达式、stream流不必说了,是Java8版本的核心技术,CompletableFuture作为Java8并发编程中最大的改进要花时间好好学习,这也是本书作者所提到的,而且后面专门花了一个大章来讲CompletableFuture。


作者虽然一直强调不要使用并发,但却对Java8的并发编程工具花了较大篇幅,我个人认为他更多的是一种见猎心喜,可是我们面试经常会问到这个工具类相关的东西,看一下大佬对该工具的理解还是很有用的。





总结



《On Java》这本书说实话,我觉得没有作者的《Thinking In Java》写得好,可能有多种原因导致。


我说下我觉得不好的主要感受在哪里,一是有些地方翻译的不好,会给你带来困惑,二是作者给出的一些案例有自己的风格,而且例子我没觉得那么通俗易懂。


但总体上还是值得一看,尤其是他穿插了很多和其他如C/C++、GO等语言的比较,还包含了自己对Java的理解,尤其是一些编程思想很直接,最后给出了林林总总有接近70条的编程指南,我认为对于初学者树立未来工作中的编程思想是很有用的。


这位作者的文字中弥漫着一股浓烈的不推荐使用并发编程的味道,我觉得是他多年工作的心得,所以大家在往后的工作中不妨可以借鉴下大佬的思维。


好了,我今天也就是带你读了下书,读的还开心吗。







本人原创文章纯手打,觉得有一滴滴帮助的话就请点个赞和收藏吧~


本人长期分享工作中的感悟、经验及实用案例,喜欢的话也可以进入个人主页关注一下哦~


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

个人支付项目,已稳定收款 100+

对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。下面是项目运行首页下面是项目登录注册页下面是商品支付...
继续阅读 »

对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。

我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。

下面是项目运行首页


下面是项目登录注册页


下面是商品支付页面


虽然项目整体规模较小但也算是五脏俱全,有认证相关、有支付相关、也有分布式问题相关。对于没有做过个人项目特别是没做过支付项目的小伙伴来说,用来练手或者写在简历上都是未尝不可的。


那下面我来具体项目中几个重要的业务点。


1、网关认证


以前我们开发项目要进行认证基本都是通过在服务中写个拦截器,然后配置拦截器拦截所有的请求,最终通过拦截器的逻辑进行认证。这中方法不是不可以,但我觉得不好,如果我们项目中有三个微服务以上,那么这个拦截器的认证逻辑就会存在于每个微服务中,这是我认为的不好的点。


那我是怎么做的呢!


对,在网关服务里做认证动作。将认证动作迁移,因为我的个人项目是通过网关进行请求转发,所以,所有的请求都会先进入网关,再进入各个具体的业务服务,那问题就好办了。我直接通过实现网关的 GlobalFilter 接口拦截所有的请求,通过实现该接口进行认证逻辑处理,完成本平台的认证、续约、限流等功能。


下面来看看请求流程图


2、支付逻辑


支付功能可以说是本项目的重中之重,需要有非常强的健壮性。因为我是一位个人开发者,所以不能对接需要有营业执照的支付功能,最终我选择了支付宝的当面付这一个功能。


当面付的好处很多,第一它不需要营业执照,第二对接也非常简单而且有支付宝封装的SDK,所以本人再对接的过程中没有费多少力气就把接口打通了。


主要就是对接了当面付的三个接口:

  1. 获取支付二维码接口

  2. 支付成功的回调接口

  3. 订单状态回查接口


当然,这三个接口的代码量也是很大的,所以本人为了通用就又对他做了一层封装,使得项目调用支付功能就更加简单了。如下就可以完成一个支付功能的完整逻辑:


是不是很简单,如果需要代码的可以看文章末尾哦!


下面我来介绍一下本项目中付费内容的整个业务流程。


1、用户获取付费商品详情


2、点击查看内容,这里就有两种结果了

  • 第一种结果:商品已支付,直接显示内容给用户观看

  • 第二种结果:商品未支付,提示用户付款查看


3、当显示第二种结果时,如果用户点击付款,则进入后续流程


4、服务器请求支付宝第三方,获取对应金额的支付二维码,并将返回的二维码和用户绑定生成一个未支付的订单,最终将这个待支付二维码返回给页面


5、页面显示二维码之后,用户就需要进行扫码付款(打开支付宝APP扫码付款)


6、用户付款成功之后,支付宝第三方会自动回调第四步我给支付宝的回调地址。回调接口的逻辑就是将订单状态改为已支付并做一些后续的流程操作。


7、为了防止回调接口出问题,还写了一个定时任务,定时回查订单表中未支付订单的状态,循环请求支付宝询问支付支付成功并执行支付成功的相应回调逻辑。


支付业务流程图


3、手写分布式锁


相信分布式锁大家都不陌生,无非就是向中间件中放入一个标志量,存在即表示已锁,反之则未锁执行相关逻辑。


说都会说,但要真正自己手写而且做到高可用确是一个非常困难的问题。其中非常关键的一点就是如何解锁,如何做到业务执行完成百分之百解锁,那我再项目中是如何考虑的呢!


我先来简单的说一下思路:


1、定义一个分布式锁注解,用来标注那些方法需要分布式锁加持


2、定义一个切面,逻辑就是给加上了分布式注解的方法进行增强


3、增强的逻辑为:加锁、生成续约任务、执行业务逻辑、解锁


4、另起一个延迟线程池,每隔一定时间遍历一次续约任务集合,判断任务是否需要进行续约(这个逻辑判断很多如:续约次数过多、业务已执行完毕、是否需要续约等等)


具体业务流程如图(我画的比较多)


当然,为了方便你们理解,我还出了相关视频,地址:



http://www.bilibili.com/video/BV1jP…



以上,就是赞赏平台项目中三个比较大的亮点,不论是写在简历上还是当作个人项目都是一个非常不错的选择,那我也把这个项目搭建起来了,地址如下:



admire.j3code.cn



需要项目代码 + 视频 + 详细文档的,我都放在这个平台上了,自取即可。


我是J3code(三哥),咱们下回见

作者:J3code
链接:https://juejin.cn/post/7199820362954588197
来源:稀土掘金
收起阅读 »

为了摸鱼,我开发了一个工具网站

  大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期...
继续阅读 »


  大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期所带的傲气也是被一点一点的慢慢的打磨掉,正是因为这些,带给我的成长是巨大的。好了,闲言少叙,下面让我们进入今天的主题。


创作背景


       因为”懒“得走路,所以发明了汽车飞机,因为”懒“得干苦力活,所以发明了机器帮助我们做,很早之前看到过这个梗,而我这次同样也是因为”懒“,所以才开发出了这个工具。这里先卖个关子,先不说这个工具的作用,容我向大家吐槽一下这一段苦逼的经历。在我实习刚入职不久,就迎来了自己第一个任务,由于自己对所参与的项目的业务并不太了解,所以只能先做一些类似测试的工作,比如就像这次,组长给了我一份Json 文件,当我打开文件后看到数据都是一些地区名称,但当我随手的将滚动条往下一拉,瞬间发现不对劲,因为这个小小的文件行数竟然达到了1w+❗❗❗❗


不禁让我脊背发凉,但是这时我的担心还没达到最坏的地步,毕竟我还对具体的任务不了解。但当组长介绍任务内容,主要是让我将这些数据添加到数据库对应的表中,由于没有sql脚本,只有这个json 文件,需要手动去操作,而且给我定的任务周期是两天。听到这个时间时内心的慌张瞬间消失了,因为在之前我就了解过Navicat支持Json格式的文件直接导入数据,一个这么简单的任务给我两天时间,这不是非要让我带薪学习。


当我接下任务自信打开Navicat的导入功能时发现了一个重要问题,虽然它支持字段映射,但是给的Json数据是省市区地址名称,里面包含着各种嵌套,说实话到想到这里我已经慌了,而且也测试了一下字段只能单个的批量导入,而且不支持嵌套的类型,突然就明白为什么 给我两天的时间。


这时候心里只能默默祈祷已经有大神开发出了能处理这种数据的工具网站,但是经过一个小时的艰苦奋斗,但最终依旧是没有结果,网上有很多JsonSQl的工具网站,但是很多都支持简单支持一下生成创建表结构的语句,当场心如死灰,跑路的心都有了。但最终还是咬着牙 手动初始化数据,其过程中的“趣味” 实属无法用语言表达……

 上述就是这个工具的开发背景,也是怕以后再给我分配这么“有趣” 的任务。那么下面就给大家分享一下我自制的 Json转译SQL 工具,而且它也是一个完全免费的工具网站,同时这次也是将项目进行了 开源分享,大家也可以自己用现成的代码完成本地部署测试,感兴趣的同学可以自行拉取代码!



开源地址:github.com/pdxjie/sql-…



项目简介


Sql-Translation (简称ST)是一个 Json转译SQL 工具,在同类工具的基础上增强了功能,为节省时间、提高工作效率而生。并且遵循 “轻页面、重逻辑” 的原则,由极简页面来处理复杂任务,且它不仅仅是一个项目,而是以“降低时间成本、提高效率”为目标的执行工具。


技术选型

前端:

Vue
AntDesignUI组件库
MonacoEditor 编辑器
sql-formatter SQL格式化

后端:

SpringBoot
FastJson

项目特点

1.内置主键:JSON块如果包含id字段,在选择建表操作模式时内部会自动为id设置primary key
2.支持JSON数据生成建表语句:按照内置语法编写JSON,支持生成创建表的SQL语句
3.支持JSON数据生成更新语句:按照内置语法编写JSON,支持生成创更新的SQL语句,可配置单条件、多条件更新操作
4.支持JSON数据生成插入语句:按照内置语法编写JSON,支持生成创插入的SQL语句,如果JSON中包含 多层 (children)子嵌套,可按照相关语法指定作为父级id的字段
5.内置操作语法:该工具在选取不同的操作模式时,内置特定的使用语法规范
6.支持字段替换:需转译的JSON中字段与对应的SQL字段不一致时可以选择字段替换
7.界面友好:支持在线编辑JSON代码,支持代码高亮、语法校验、代码格式化、查找和替换、代码块折叠等,体验良好

解决痛点

下面就让我来给大家介绍一下Sql-Translation 可以解决哪些痛点问题:

需要将大量JSON中的数据导入到数据库中,但是JSON中包含大量父子嵌套关系 ——> 可以使用本站

在进行JSON数据导入数据库时,遇到JSON字段与数据库字段不一致需要替换字段时 ——> 可以使用本站

根据Apifox工具来实现更新或新增接口(前提是对接口已经完成了设计工作),提供了Body体数据,而且不想手动编写SQL时 ——> 可以使用本站

对上述三点进行进行举例说明(按照顺序):

第一种情况:

{
"id": "320500000",
"text": "苏州工业园区",
"value": "320500000",
"children": [
{
"id": "320505006",
"text": "斜塘街道",
"value": "320505006",
"children": []
},
{
"id": "320505007",
"text": "娄葑街道",
"value": "320505007",
"children": []
},
....
]
}

第二种情况:


第三种情况


以上内容就是该工具的简单介绍,由于该工具内置了部分语法功能,想要了解本工具全部工具以及想要动手操作的的同学请点击前往操作文档 ,该操作文档中包含了具体的语法介绍以及每种转换的具体示例数据 提供测试使用。


地址传送门



如果感兴趣的同学还希望可以到源码仓库给作者点个star⭐ 作为支持,非常感谢!


作者:派同学
链接:https://juejin.cn/post/7168285867160076295
来源:稀土掘金
收起阅读 »

使用RecyclerView实现三种阅读器翻页样式

一、整体逻辑 为何直接对RecyclerView进行扩展而不使用ViewPager/ViewPager2?原因如下: Scroll Model(垂直滑动)需要自定义自动滑动(对指定页进行吸附) Flip Mode(仿真翻页)需要获取各种情况下的方向信息,以...
继续阅读 »

一、整体逻辑


image.png


为何直接对RecyclerView进行扩展而不使用ViewPager/ViewPager2?原因如下:



  1. Scroll Model(垂直滑动)需要自定义自动滑动(对指定页进行吸附)

  2. Flip Mode(仿真翻页)需要获取各种情况下的方向信息,以实现更好的控制

  3. RecyclerView方便拓展,同时三种模式同时使用RecyclerView实现,便于复用


实现逻辑:三种滑动模式都在RecyclerView地基础上更改其滑动行为,横向滑动需要修改子View层级,仿真翻页需要再覆盖一层仿真动画


二、横向覆盖滑动(Slide Mode)


横向.gif


Slide Mode 最适合直接使用 ViewPager,不过我们还是以 RecyclerView 为基础来实现,让三种模式统一实现方式。实现思路:先实现跨页吸附,再实现覆盖翻页效果


1、跨页吸附


实现跨页吸附,需要在手指离开屏幕时对 RecyclerView 进行复位吸附操作,有两种情况:


(1)Scroll Idle


拖拽发生后,RecyclerView 滑动状态变为 SCROLL_STATE_IDLE 时,需要进行复位吸附操作


// OrientationHelper为系统提供的辅助类,LayoutManager的包装类
// 可以让我们方便的计算出RecyclerView相关的各种宽高,计算结果和LayoutManager方向相关
open fun snapToTargetExistingView(helper: OrientationHelper): Pair<Int, Int>? {
val lm = mRecyclerView.layoutManager ?: return null
val childCount = lm.childCount // 可见数量
if (childCount < 1) return null

var closestChild: View? = null
var absClosest = Int.MAX_VALUE
var scrollDistance = 0
// RecyclerView中心点,LayoutManager为竖向则是Y轴坐标,为横向则是X轴坐标
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2

// 从可见Item中找到距RecyclerView离中心最近的View
for (i in 0 until childCount) {
val child = lm.getChildAt(i) ?: continue
if (consumeSnap(i, child)) return null // consumeSnap 默认返回false,竖直滑动模式才使用
val childCenter = (helper.getDecoratedStart(child)
+ helper.getDecoratedMeasurement(child) / 2)
val absDistance = abs(childCenter - containerCenter)
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
scrollDistance = childCenter - containerCenter
}
}
closestChild ?: return null

// 滑动
when (orientation) {
VERTICAL -> mRecyclerView.smoothScrollBy(0, scrollDistance)
HORIZONTAL -> mRecyclerView.smoothScrollBy(scrollDistance, 0)
}
return Pair(scrollDistance, lm.getPosition(closestChild))
}


(2)Fling


可以通过 RecyclerView 提供的OnFlingListener消费掉Fling,将其转化为 SmoothScroll ,滑动到指定位置


①、找到吸附目标的位置(adapter position)


open fun findTargetSnapPosition(
lm: RecyclerView.LayoutManager,
velocity: Int,
helper: OrientationHelper
)
: Int {
val itemCount: Int = lm.itemCount
if (itemCount == 0) return RecyclerView.NO_POSITION

// 中心点以前距离最近的View
var closestChildBeforeCenter: View? = null
var distanceBefore = Int.MIN_VALUE // 中心点以前,距离为负数
// 中心点以后距离最近的View
var closestChildAfterCenter: View? = null
var distanceAfter = Int.MAX_VALUE // 中心点以后,距离为正数
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2

val childCount: Int = lm.childCount
for (i in 0 until childCount) {
val child = lm.getChildAt(i) ?: continue
if (consumeSnap(i, child)) return RecyclerView.NO_POSITION // consumeSnap 默认返回false,竖直滑动模式才使用

val childCenter = (helper.getDecoratedStart(child)
+ helper.getDecoratedMeasurement(child) / 2)
val distance = childCenter - containerCenter

// Fling需要考虑方向,先获取两个方向最近的View
if (distance in (distanceBefore + 1)..0) {
distanceBefore = distance
closestChildBeforeCenter = child
}
if (distance in 0 until distanceAfter) {
distanceAfter = distance
closestChildAfterCenter = child
}
}

// 根据方向选择Fling到哪个View
val forwardDirection = velocity > 0
if (forwardDirection && closestChildAfterCenter != null) {
return lm.getPosition(closestChildAfterCenter)
} else if (!forwardDirection && closestChildBeforeCenter != null) {
return lm.getPosition(closestChildBeforeCenter)
}

// 边界情况处理
val visibleView =
(if (forwardDirection) closestChildBeforeCenter else closestChildAfterCenter)
?: return RecyclerView.NO_POSITION
val visiblePosition: Int = lm.getPosition(visibleView)
val snapToPosition = (visiblePosition - 1)

return if (snapToPosition < 0 || snapToPosition >= itemCount) {
RecyclerView.NO_POSITION
} else snapToPosition
}

②、使用RecyclerView的「LinearSmoothScroller」完成吸附动画


private fun createScroller(
oh: OrientationHelper
)
: LinearSmoothScroller {
return object : LinearSmoothScroller(mRecyclerView.context) {
override fun onTargetFound(
targetView: View,
state: RecyclerView.State,
action: Action
)
{
val d = distanceToCenter(targetView, oh)
val time = calculateTimeForDeceleration(abs(d))
if (time > 0) {
when (orientation) {
VERTICAL -> action.update(0, d, time, mDecelerateInterpolator)
HORIZONTAL -> action.update(d, 0, time, mDecelerateInterpolator)
}
}
}

override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics) =
100f / displayMetrics.densityDpi

override fun calculateTimeForScrolling(dx: Int) =
100.coerceAtMost(super.calculateTimeForScrolling(dx))
}
}

protected fun distanceToCenter(targetView: View, helper: OrientationHelper): Int {
val childCenter = (helper.getDecoratedStart(targetView)
+ helper.getDecoratedMeasurement(targetView) / 2)
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2
return childCenter - containerCenter
}

完整操作:


protected fun snapFromFling(
lm: RecyclerView.LayoutManager,
velocity: Int,
helper: OrientationHelper
)
: Pair<Boolean, Int> {
val targetPosition = findTargetSnapPosition(lm, velocity, helper)
if (targetPosition == RecyclerView.NO_POSITION) return Pair(false, 0)
val smoothScroller = createScroller(helper)
smoothScroller.targetPosition = targetPosition
lm.startSmoothScroll(smoothScroller)
return Pair(true, targetPosition) // 消费fling
}

2、覆盖效果实现


(1)如果使用PageTransform实现


如果使用ViewPagerPageTransform,是可以实现覆盖动画的,实现思路:使可见View的第二个View跟随屏幕滑动


image.png


假设上图蓝色透明矩形为屏幕,其他为ItemView,图片上半部分正常滑动的状态,下半部分为 translate view 之后的状态。可以看到,在横向滑动过程中,最多可见2个View(蓝色透明方框最多覆盖2个View),此时将第二个View跟随屏幕,其他View保持跟随画布滑动,即可达到效果。在OnPageScroll回调中实现这个逻辑:


for (i in 0 until layoutManager.childCount) {
layoutManager.getChildAt(i)?.also { view ->
if (i == 1) {
// view.left是个负数,offsetPx(=-view.left)是个正数
view.translationX = offsetPx.toFloat() - view.width // 需要translate的距离(向前移需要负数)
} else {
// 恢复其余位置的translate
view.translationX = 0f
}
}
}

(2)扩展RecyclerView实现覆盖翻页


知道如何通过 PageTransfrom 实现后,我们来看看直接使用 RecyclerView 如何实现。观看ViewPager2源码可知PageTransfrom的实现方式


image.png


故我们直接copy代码,在OnScrollListener中自行实现onPageScrolled回调即可实现覆盖翻页效果。


但是此时还有一个问题,就是子View的层级问题,你会发现上面的滑动示意图中,绿色View会在黄色View之上,如何解决这个问题呢?我们需要控制View的绘制顺序,前面的View后绘制,保证前面地View在后面的View的绘制层级之上。


观看源码会发现,RecyclerView其实提供了一个回调ChildDrawingOrderCallback,可以很方便地实现这个效果:


override fun attach() {
super.attach()
mRecyclerView.setChildDrawingOrderCallback(this)
}

override fun onGetChildDrawingOrder(childCount: Int, i: Int) = childCount - i - 1 // 反向绘制

三、竖直滑动(Scroll Mode)


垂直.gif


竖直滑动需要滑动到跨章的位置时才吸附(自动回滚到指定位置),需要实现两个效果:跨章吸附、跨章Fling阻断。我们可以在横向覆盖滑动(Slide Mode)的基础上做一个减法,首先将LayoutManager改为竖向的,然后实现上述两个效果。


1、跨章吸附


实现跨章吸附,我们先在 RecyclerViewAdapter 中对每个View进行一个标记:


companion object {
const val TYPE_NONE = 100 // 其他
const val TYPE_FIRST_PAGE = 101 // 首页
const val TYPE_LAST_PAGE = 102 // 末页
}


fun bind() { // onBindViewHolder 时调用
itemView.tag = when {
textPage.isLastPage -> TYPE_LAST_PAGE
textPage.isFirstPage -> TYPE_FIRST_PAGE
else -> TYPE_NONE
}
......
}

其次我们实现横向覆盖滑动(Slide Mode)中的一段代码(做一个减法):


// 如果不是章节的最后一页,则消费Snap(不进行吸附操作)
override fun consumeSnap(index: Int, child: View) =
index == 0 && child.tag != ReadBookAdapter.TYPE_LAST_PAGE

这样就可以实现不是跨越章节的翻页不进行吸附,而跨越章节的滑动会自动吸附。


2、跨章Fling阻断


在滑动过程中,基于可见View只有两个的情况:



  • 如果向上滑动,判断第一个可见View是否「末页」,如果是,smoothScroll到第二个可见View

  • 如果向下滑动,判断第二个可见View是否「首页」,如果是,smoothScroll到第一个可见View


private var inFling = false     // 正在fling,在OnFlingListener中设置为true
private var inBlocking = false // 阻断fling


override val mScrollListener = object : RecyclerView.OnScrollListener() {
var mScrolled = false

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
inFling = false // 重置inFling
}
RecyclerView.SCROLL_STATE_IDLE -> {
inFling = false // 重置inFling
if (inBlocking) {
inBlocking = false // 忽略阻断造成的IDLE
} else if (mScrolled) {
mScrolled = false
snapToTargetExistingView(orientationHelper.value)
}
}
}
}

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy != 0) {
if (!mScrolled) {
this@VSnapHelper.mCallback.onScrollBegin()
mScrolled = true
}
val lm = mRecyclerView.layoutManager ?: return
// fling阻断
if (inFling && !inBlocking) {
val child: View?
val type: Int
if (dy > 0) { // 向上滑动
child = lm.getChildAt(0)
type = ReadBookAdapter.TYPE_LAST_PAGE
} else {
child = lm.getChildAt(lm.childCount - 1)
type = ReadBookAdapter.TYPE_FIRST_PAGE
}
child?.let {
if (it.tag == type) {
inBlocking = true
val d = distanceToCenter(it, orientationHelper.value)
mRecyclerView.smoothScrollBy(0, d)
}
}
}
}
}
}

四、仿真页(Flip Mode)


仿真.gif


仿真页在横向覆盖滑动(Slide Mode)基础之上实现,我们还需要实现:



  1. 确认手指滑动方向

  2. 所有可见View都跟随屏幕

  3. 绘制次序根据拖拽方向改变,保证目标页在当前页之上

  4. 绘制仿真页

  5. 手指抬起后的翻页动画(确认Fling、Scroll Idle产生的两种Snap的方向,因为手指会来回滑动导致方向判断错误)


1、确认手指滑动方向


滑动方向不能直接在 onTouchdispatchTouchEvent 这些方法中直接判断,
因为极微小的滑动都会决定方向,这样会造成轻微触碰就判定了方向,导致页面内容闪动、抖动等问题。
我们需要在滑动了一定距离后确定方向,最好的选择就是在 onPageScroll 中进行判断,系统为我们保证了ScrollState已变为DRAGGING,此时用户100%已经在滑动。可以看下源码真正触发「onPageScroll」的条件有哪些


image.png


我们实现的判断方向的代码:


// 在onScrolled中调用
// mCurrentItem:onPageSelected中赋值,代表当前Item
// position:第一个可见View的位置
// offsetPx:第一个可见View的left取负
// mForward:方向,true为画布向左滑动(向尾部滑动),false画布向右滑动(向头部滑动)
private fun dispatchScrolled(position: Int, offsetPx: Int) {
if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
mForward = mCurrentItem == position
}
mCallback.onPageScrolled(position, mCurrentItem, offsetPx, mForward)
}

image.png


不过这个规则在超快速滑动时会判断错误,即settling直接变dragging的时候,所以会对滑动做一点限制


override fun dispatchTouchEvent(e: MotionEvent): Boolean {
if (snapHelper.mScrollState == RecyclerView.SCROLL_STATE_SETTLING) {
return true // sellting过程中禁止滑动
}
delegate.onTouch(e)
return super.dispatchTouchEvent(e)
}

2、遮盖效果


所有可见View都跟随屏幕,横向覆盖滑动(Slide Mode)的增强版,因为给 RecyclerView设置了 offScreenLimit=1 的效果,所以 LayoutManagerchild 数量最多会有4个
(参照 ViewPager2 # LinearLayoutManagerImpl 实现,这里设置是为了滑动时可以第一时间生成目标页的截图)


// onPageScrolled中调用
private fun transform(offsetPx: Int, firstVisible: Int) {
val count = layoutManager.childCount
if (count == 2 || (count == 3 && offsetPx == 0)) {
// 可见View只有一个的时候,全部复位
for (i in 0 until count) {
layoutManager.getChildAt(i)?.also { view ->
view.translationX = 0f
}
}
} else {
var target = 1
if (count == 3 && firstVisible == 0) target-- // 首位适配,currentItem=0且存在滑动的时候
for (i in 0 until layoutManager.childCount) {
layoutManager.getChildAt(i)?.also { view ->
when (i) {
target -> view.translationX = offsetPx.toFloat()
target + 1 -> view.translationX = offsetPx.toFloat() - view.width
else -> view.translationX = 0f
}
}
}
}
}

3、绘制次序根据拖拽方向改变


保证目标页在当前页之上,防止绘制的仿真页消失时出现闪屏(瞬间显示了不正确的页)


// 画布左移则反向绘制,右移则正想绘制
override fun getDrawingOrder(childCount: Int, i: Int) =
if (snapHelper.mForward) childCount - i - 1 else i

4、绘制仿真页


我们在 RecyclerView 的父View上直接覆盖绘制一层仿真页Bitmap


(1)生成截图


如上面所说,实现了 offScreenLimit=1 的效果,我们在首次获取到方向时生成截图:


// 生成截图方法
fun View.screenshot(): Bitmap? {
return runCatching {
val screenshot = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val c = Canvas(screenshot)
c.translate(-scrollX.toFloat(), -scrollY.toFloat())
draw(c)
screenshot
}.getOrNull()
}
private var isBeginDrag = false

override fun onPageStateChange(state: Int) {
when (state) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
isBeginDrag = true
}
}
}

override fun onPageScrolled(firstVisible: Int, current: Int, offsetPx: Int, forward: Boolean) {
if (isBeginDrag) {
isBeginDrag = false
delegate.apply {
if (forward) {
nextBitmap?.recycle()
nextBitmap = layoutManager.findViewByPosition(current + 1)?.screenshot()
curBitmap?.recycle()
curBitmap = layoutManager.findViewByPosition(current)?.screenshot()
} else {
prevBitmap?.recycle()
prevBitmap = layoutManager.findViewByPosition(current - 1)?.screenshot()
curBitmap?.recycle()
curBitmap = layoutManager.findViewByPosition(current)?.screenshot()
}
setDirection(if (forward) AnimDirection.NEXT else AnimDirection.PREV)
}
invalidate()
}
}

(2)绘制仿真页


绘制仿真页参考 gedoor/legadoSimulationPageDelegate



  • 基础知识:三角函数、Android的矩阵、贝塞尔曲线、canvas.clipPath的 XOR & INTERSECT 模式

  • 绘制方法:Android仿真翻页:cnblogs.com

  • 计算方法:使用手指触摸点和触摸点对应的角位置(比如触摸点靠近右下角,角位置就是右下角),这两个点可以算出所有参数


确认方向后,我们只用通过修改手指触碰点的参数即可控制整个动画(根据点击位置实时计算即可)


5、动画控制


手指抬起后的翻页动画通过 Scroller+invalidate实现


override fun computeScroll() {
if (scroller.computeScrollOffset()) {
setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) {
stopScroll()
}
}

对于FlingScroll Idle产生的吸附效果,我们需要各自回调方向:


// 选中时开始动画,此时position改变
override fun onPageSelected(position: Int) {
val page = adapter.data[position]
ReadBook.onPageChange(page)
if (canDraw) {
delegate.onAnimStart(300, false)
}
}

// position未改变的情况
override fun onSnap(isFling: Boolean, forward: Boolean, changePosition: Boolean) {
if (!changePosition) {
delegate.onAnimStart(
300,
true,
// 未改变方向,向前则播放向后动画
if (forward) AnimDirection.PREV else AnimDirection.NEXT
)
}
}

Scroll Idle通过 SmoothScroll 所需要滑动的距离正负判断方向:


// Scroll
override fun snapToTargetExistingView(helper: OrientationHelper): Pair<Int, Int>? {
mSnapping = true
super.snapToTargetExistingView(helper)?.also {
// first为滑动距离,second为目标Item的position
mCallback.onSnap(false, it.first > 0, mCurrentItem != it.second)
return it
}
return null
}

// Fling
override val mFlingListener = object : RecyclerView.OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
val lm = mRecyclerView.layoutManager ?: return false
mRecyclerView.adapter ?: return false
val minFlingVelocity = mRecyclerView.minFlingVelocity
val result = snapFromFling(
lm,
velocityX,
orientationHelper.value
)
val consume = abs(velocityX) > minFlingVelocity && result.first
if (consume) {
mSnapping = true
// second为目标Item的position,这里直接通过速度正负来判断方向
mCallback.onSnap(true, velocityX > 0, result.second != mCurrentItem)
}
return consume
}
}

(以上为所有关键点,只截取了部分

作者:三尺丶
来源:juejin.cn/post/7244819106343829564
代码,提供一个思路)

收起阅读 »

为什么要招聘有经验的人?

周末带娃出去玩,回来的时候路过一家新开的水果店买东西,因为是新店,所以店里在搞促销活动。进去后发现一个很糟糕的问题。店员对收银系统不熟悉,排队的人要好几分钟才能结算一个,很多人等着不耐烦就走了。现在客户买东西,在哪里买都是买,体验不好,留不住客户的。 这让我想...
继续阅读 »

周末带娃出去玩,回来的时候路过一家新开的水果店买东西,因为是新店,所以店里在搞促销活动。进去后发现一个很糟糕的问题。店员对收银系统不熟悉,排队的人要好几分钟才能结算一个,很多人等着不耐烦就走了。现在客户买东西,在哪里买都是买,体验不好,留不住客户的。


这让我想到前年六月份,公司开第一家实体店的时候,也出现过类似情况。当初做的是让客户通过小程序线下扫码购买,优惠设计得很复杂,服务员虽然做过培训,但很多细节不清楚。当客户支付出现异常情况时,又来回沟通处理。这就让用户很不耐烦。最后虽然看起来店里人多热闹,但实际营收并不高。


这并不是特例,有经验的老板,在正式开店前会有一段时间的试营业,非常低调地开门。等员工都熟悉工作了,才会正式开业。


我们都知道招聘的时候,企业更喜欢招聘有经验的人。为啥?因为经验指的是你不仅知道一个东西,还做到过。你能解决某个问题,解决问题的方法才是经验。


如果你只是第一次做,就算培训过,那也不是经验,只能说你知道某件事。从知道到做到,中间还隔很远,越是复杂系统,越需要花更多时间在这个做到的过程上。就像我们做菜,你按照菜谱做,第一次也大概率做得也不会很好吃。


另外一个原因是,人往往容易高估自己的能力。我们在评估一个项目工作量时就很容易犯这个错误,你要是没经验,很容易把一个复杂需求看得很简单。导致工作量评估不足,这也是导致项目延期很重要的一个原因之一。


说到这里,相信你也知道招聘的背后是找一个能解决他们现有问题的人,这个解决问题的经验才是他们需要的。那如果我们想换工作,我想你应该也知道招聘的关键是”经验“,跟岗位相关的经验都是加分项。在进一步思考,人的时间都是一样的,把时间用哪里,把解决什么问题变成经验,这是我们可以思考的方向之一。比如,你想做一名项目经理。那项目管理经验就是你的加分项。十人的管理、百人的管理,管理的深度不一样,获得经验也是不一样的。


总结一下,今天主要想跟大家表达两个观点:


1、珍惜你的时间,用有限的时间去拥有更多跟工作相关的经验。


2、没有经验就不要太乐观地去做一件事情。反之,在做一件没有做过的事情前,应该找一个无利益关系且有经验的前辈咨询下。


推荐相关阅读: 初学者思维 - 找到解决

问题的新方法

收起阅读 »

Axios的封装思路与技巧

web
Axios的封装思路与技巧 提示:文中使用的为ts代码,对ts不熟悉的同学可以删除所有类型降级为js代码,不影响使用 前言 项目中或多或少会有一些需要接口发送请求的需求,与其复制粘贴别人在业务中对请求方法的使用,不如自己花点时间研究项目中请求方法的实现,这样在...
继续阅读 »

Axios的封装思路与技巧


提示:文中使用的为ts代码,对ts不熟悉的同学可以删除所有类型降级为js代码,不影响使用


前言


项目中或多或少会有一些需要接口发送请求的需求,与其复制粘贴别人在业务中对请求方法的使用,不如自己花点时间研究项目中请求方法的实现,这样在处理请求出现的问题时能够更好的定位问题原因。本文循序渐进的介绍了如何对axios进行封装实现自己项目中的请求方法,希望各位同学在阅读后能有一定的体会,如有问题还请大家在评论区指正。


1.创建axios实例


创建一个axios实例,在这里为实例进行一些配置(如超时时间)。但是要注意,不要在此处配置一些动态的属性,如headers中的token,具体的原因我们会在后面提起


import axios from 'axios'
const instance = axios.create({
 timeout: 1000 * 120, // 超时时间120s
})

为实例配置拦截器(请求拦截器,响应拦截器)


可能有些同学对axios不太熟悉,不了解请求拦截器和响应拦截器的作用,这里会简单介绍一下


请求拦截器:在请求发送前进行拦截,或者对请求错误进行拦截


instance.interceptors.request.use(
 config => {
   // config为AxiosRequestConfig的一个实例,它是包含请求配置参数的对象
   // 在这里可以在请求发送前做一些处理,如向config实例中添加属性,取消请求,设置loading等  
   return config
},
 // 这里是请求报错时的拦截方法,这里直接返回一个状态为reject的promise
 // 实际测试时,即使前端请求报错并且未到达后端,也没有触发这里的钩子函数
 error => Promise.reject(error),
)

响应拦截器:在响应被.then.catch处理前拦截


instance.interceptors.response.use(
 response => {
   // 响应成功的场景
   // 在这里可以关闭loading或者对响应的返参对象response进行处理
   return response
},
 error => {
   // 响应失败的场景
   // http状态码不为2xx时就会进入,根据项目要求处理接口401,404,500等情况
   // 返回的promise也可以根据项目要求进行修改
   return Promise.reject(error)
},
)

这样我们就创建了一个可用的axios实例,对于实例的一些其他配置可以参考axios官网


2.创建Abstract类进一步封装


在创建了一个axios实例之后,我们就可以使用它去发送请求了,但是出于减少重复代码的目的,我们不在业务代码中直接使用axios实例去发送各种请求,而是选择去做进一步的封装让整体的代码更加简洁


通常来说,我在项目中更喜欢用面向对象的方式去对axios做进一步的封装,使用这种方式的优点会在后面进行说明 创建一个类,起名可以按自己的喜好来,这里我写的是Abstract,因为它的主要作用是做为一个底层的类让其他类去继承,在这里我们提供一些属性的配置,以及一些基础的请求方法


import axios from './axios'
import type { AxiosRequest, CustomResponse } from './types/index'
class Abstract {
 // 配置接口的baseUrl,这里用的是vite环境变量,可以根据需求自行修改
 protected baseURL: string = import.meta.env.VITE_BASEURL
 // 配置接口的请求头,这里仅简单配置一下
 protected headers: object = {
   'Content-Type': 'application/json;charset=UTF-8',
}
 // 提供类的构造器,可以在这里修改一些基础参数如baseUrl
 constructor(baseURL?: string) {
   this.baseURL = baseURL ?? this.baseURL
}
 // 重点!发起请求的方法
 // 这里的T是ts中泛型的用法,主要用于控制接口返回的类型,不熟悉ts的同学可以略过
 private apiAxios<T = any>({
   baseURL = this.baseURL,
   headers = this.headers,
   method,
   url,
   data,
   params,
   responseType
}: AxiosRequest): Promise<CustomResponse<T>> {
 // 在这里加上请求头的好处在于,每次请求时都会动态读取存储的token值
 // 正如前面所说的,不要在创建axios实例时在header上配置token是因为,浏览器除非刷新,否则只会创建一次axios实例,它的header上的token的值不会发生变化,如果涉及到用户退出等清除token的操作,下次登录时获得的新token不会被使用
 Object.assign(headers, {
   // 根据情况使用localStorage或sessionStorage
   token: localStorage.getItem('token')        
})
 return new Promise((resolve, reject) => {
   axios({
     baseURL,
     headers,
     method,
     data,
     url,
     params,
     responseType,
  })
    .then(res => {
       // 在这里处理http2xx的接口,根据业务的需要进行一些处理,返回一个成功的promise
       // 这里仅为演示,直接返回了原始的res
       resolve(res)
    })
    .catch(err => {
       // 在这里处理http不成功的状态,并根据业务的需要进行一个处理,返回一个失败的promise
       reject(err)
    })
  })
}
 // 通常我还会在基础类上封装一些现成的请求方法,如Get Post等,可以根据自己的需要封装其他的请求方法
 protected getReq<T = any>({ baseURL, headers, url, data, params, responseType, messageType }: AxiosRequest) {
   return this.apiAxios<T>({
     baseURL,
     headers,
     method: 'GET',
     url,
     data,
     params,
     responseType,
     messageType,
  })
}
   
 protected postReq<T = any>({ baseURL, headers, url, data, params, responseType, messageType }: AxiosRequest) {
   return this.apiAxios<T>({
     baseURL,
     headers,
     method: 'POST',
     url,
     data,
     params,
     responseType,
     messageType,
  })
}

3.继承Abstract类实现业务相关的请求类


这样,我们就成功封装了一个Axios的基础类,接下来可以创建一个新的业务类去继承它并使用。这里我们创建一个User类,代表用户相关的请求


import Abstract from '@/api/abstract'

class User extends Abstract {
 constructor(baseUrl?: string) {
   super(baseUrl)
}

 // post请求
 login(data: unknown) {
   return this.postReq({
     data,
     url: 'back/v1/user/login',
  })
}
 
 // get请求
 getUser(param: { id: string })
   return this.getReq({
     param,
     url: 'back/v1/user/getUser'
  })
}
 
 // 需要修改请求头的Content-Type,如表单上传
 saveUser(data: any) {
   const formData = new FormData()
   Object.keys(data).forEach(key => {
     formData.append('file', data[key])
  })
   return this.postReq({
     data,
     headers: {
       'Content-Type': 'multipart/form-data',
    },
     url: 'back/v1/user/saveUser',
  })
}

export default User

文件创建好了之后我们就可以引用到具体项目中使用了


// 可以在这里传入baseUrl,这也是基于类封装的好处,我们可以实例化多个user并使用不同的baseUrl
const userInstance = new User()
const res = await userInstance.login({
 username: 'xxx',
 password: 'xxx'
})
作者:kamesan
来源:juejin.cn/post/7264749103125184527

收起阅读 »

我家等离子电视也能用的移动端适配方案

web
前几天我的领导“徐江”让我把一个移动端项目做一下适配,最好让他在家用等离子电视也能看看效果,做不出来就给我“埋了”,在这种情况下才诞生了这篇文章~ 什么是移动端适配 移动端适配是指在不同尺寸的移动端设备上,页面能相对达到合理的显示或者保持统一的等比缩放效果 ...
继续阅读 »

前几天我的领导“徐江”让我把一个移动端项目做一下适配,最好让他在家用等离子电视也能看看效果,做不出来就给我“埋了”,在这种情况下才诞生了这篇文章~



什么是移动端适配


移动端适配是指在不同尺寸的移动端设备上,页面能相对达到合理的显示或者保持统一的等比缩放效果


移动端适配的两个概念



  1. 自适应:根据不同的设备屏幕大小来自动调整尺寸、大小

  2. 响应式:会随着屏幕的实时变动而自动调整,是一种自适应


1.png

而在我们日常开发中自适应布局在pc端和移动端应用都极为普遍,一般是针对不同端分别做自适应布局,如果要想同时兼容移动端和pc端,尤其是等离子电视这样的大屏幕,那么最好还是使用响应式布局~


移动端适配-视口(viewport)


在一个浏览器中,我们可以看到的区域就是视口(viewport),我们在css中使用的fixed定位就是相对于视口进行定位的。

在pc端的页面中,我们不需要对视口进行区分,因为我们的布局视口和视觉视口都是同一个。而在移动端是不太一样的,因为我们的移动端的网页往往很小,有可能我们希望一个大的网页在移动端上也可以完整的显示,所以在默认情况下,布局视口是大于视觉视口的。


  <style>
.box {
width: 100px;
height: 100px;
background-color: orange;
}
</style>
<body>
<div class="box"></div>
</body>


pc端展示效果
3.png


移动端展示效果
4.png
从上图可以看出在移动端上同样是100px的盒子,但是却没有占到屏幕的1/3左右的宽度,这是因为在大部分浏览器上,移动端的布局视口宽度为980px,我们在把pc端的页面切换成移动端页面时,右上角也短暂的显示了一下我们的布局视口是980px x 1743px。

所以在移动端下,我们可以将视口划分为3种情况:



  1. 布局视口(layout viewport)

  2. 视觉视口(visual layout)

  3. 理想视口(ideal layout)


这些概念的提出也是来自于ppk,是一位世界级前端技术专家。
贴上大佬的文章链接quirksmode.org/mobile/view…


1.jpeg

所以我们相对于980px布局的这个视口,就称之为布局视口(layout viewport),而在手机端浏览器上,为了页面可以完整的显示出来,会对整个页面进行缩小,那么显示在可见区域的这个视口就是视觉视口(visual layout)。


3.png


4.png

但是我们希望设置的是100px就显示的是100px,而这就需要我们设置理想视口(ideal layout)。


// initial-scale:定义设备宽度与viewport大小之间的缩放比例
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

移动端适配方案



  1. 百分比设置

  2. rem单位+动态html的font-size

  3. flex的弹性布局

  4. vw单位


在我们移动端适配方案中百分比设置是极少使用的,因为相对的参照物可能是不同的,所以百分比往往很难统一,所以我们常常使用的都是后面3种方案。


rem单位+动态html的font-size


rem单位是相对于html元素的font-size来设置的,所以我们只需要考虑2个问题,第一是针对不同的屏幕,可以动态的设置html不同的font-size,第二是将原来的尺寸单位都转换为rem即可。



talk is cheap, show me the code



/*
* 方案1:媒体查询
* 缺点:需要针对不同的屏幕编写大量的媒体查询,且如果动态的修改尺寸,不会实时的进行更新
*/

<style>
@media screen and (min-width: 320px) {
html {
font-size: 20px;
}
}
@media screen and (min-width: 375px) {
html {
font-size: 24px;
}
}
@media screen and (min-width: 414px) {
html {
font-size: 28px;
}
}
.box {
width: 5rem;
height: 5rem;
background-color: orange;
}
</style>

<body>
<div class="box"></div>
</body>

/*
* 方案2:编写js动态设置font-size
*/

<script>
const htmlEl = document.documentElement;

function setRem() {
const htmlWidth = htmlEl.clientWidth;
const htmlFontSize = htmlWidth / 10;
htmlEl.style.fontSize = htmlFontSize + "px";
}
// 第一次不触发,需要主动调用
setRem();

window.addEventListener("resize", setRem);
</script>


<style>
.box {
width: 5rem;
height: 5rem;
background-color: orange;
}
</style>


<body>
<div class="box"></div>
</body>


但是写起来感觉还是好麻烦,如果可以的话我希望白嫖-0v0-


5.png

所以我选择 postcss-pxtorem,vscode中也可以下载到相关插件哦,一鱼多吃,😁


vw单位


/*
* 方案1:手动换算
*/

<style>
/** 设置给375的设计稿 */
/** 1vw = 3.75px */
.box {
width: 26.667vw;
height: 26.667vw;
background-color: orange;
}
</style>

/*
* 方案2:less/scss函数
*/

@vwUnit: 3.75;

.pxToVw(@px) {
result: (@px / @vwUnit) * 1vw;
}
.box {
width: .pxToVw(100) [result];
height: .pxToVw(100) [result];
background-color: orange;
}

当然白嫖党永不言输,我选择 postcss-px-to-viewport,贴一下我的配置文件~


module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px", //需要转换的单位,默认为"px"
viewportWidth: 750, // 视窗的宽度,对应设计稿的宽度
viewportUnit: "vw", // 指定需要转换成的视窗单位,建议使用 vw,兼容性现在已经比较好了
fontViewportUnit: "vw", // 字体使用的视口单位

// viewportWidth: 1599.96, // 视窗的宽度,对应设计稿的宽度
// viewportUnit: 'vw', // 指定需要转换成的视窗单位,建议使用 vw
// fontViewportUnit: 'vw', // 字体使用的视口单位

unitPrecision: 8, // 指定`px`转换为视窗单位值的小数后 x位数
// viewportHeight: 1334, //视窗的高度,正常不需要配置
propList: ["*"], // 能转化为 rem的属性列表
selectorBlackList: [], //指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 1, // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
mediaQuery: false, // 允许在媒体查询中转换
replace: true, //是否直接更换属性值,而不添加备用属性
// exclude: [], //忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
landscape: false, //是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
landscapeUnit: "rem", //横屏时使用的单位
landscapeWidth: 1134 //横屏时使用的视口宽度
}
}
};

rem单位和vw单位的区别


rem事实上是作为一个过渡方案,它利用的也是vw的思想,并且随着前端的发展,vw的兼容性已经越来越好了,可以说具备了rem的所有优势。

但是我们假想一个这样的场景,我们希望网页在达到800px的时候页面布局不需要继续扩大了,这个时候如果我们采用的是rem布局,我们可以使用媒体查询设置max-width,而vw则始终是以视口为单位,自然不容易处理这样的场景。

当然,vw相比于rem,存在以下优势:



  1. 不需要计算font-size大小

  2. 不存在font-size继承的问题

  3. 不存在因为某些原因篡改了font-size导致页面尺寸混乱的问题

  4. vw更加的语义化

  5. 具备rem的所有优点


所以在开发中也更推荐大家使用vw单位进行适配。


6.jpeg


作者:魔术师Grace
来源:juejin.cn/post/7197623702410248251
收起阅读 »

鹅厂七年半,写在晋升失败的不眠之夜

夜半惊醒 看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同...
继续阅读 »

夜半惊醒


看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同组小伙伴之后。技术人员,终究困于技术的窠臼。


工作经历


浑浑噩噩的四年


我毕业就进入鹅厂工作,15年,正是鹅厂大杀四方的一年,至今犹记得当时拿到鹅厂 offer 时的兴奋和同学眼中的艳羡。来了公司后,跟着项目组开始创新项目,一眼望到头的挣不来钱,浑浑噩噩的干了3年,和一起进公司的小伙伴们收入差距越来越大。


好在我的驱力不错,既然挣不到钱,那就好好积累技术能力,提升自己。那段时间,周围没有伙伴,没有对比,没有好的机会,只剩下我一个人慢慢悠悠的学习积累,我以为自己足够努力,没有荒废时间;后来才发现自己其实是井底之蛙,一起入职的小伙伴付出了百倍的努力和数不清的不眠之夜,1年多就已经能 cover 当时的业务了,没过2年就已经提干了。


2018年底,我满怀信心去答辩后台 3.1,没过。我当时还觉得评委装逼,挑刺,后来回头看写的都是s,一点含量都没有。


时来运转


2019年3月,答辩刚结束。部门业务调整,我们被整组划到了一个相对挣钱的业务上,leader 也调整成后台 leader。


新业务还是挣钱,凭着第一个项目积累的技术和项目经验,我得到了新领导的认可,第一年干的轻轻松松就挣到了远远超出之前的收入,和之前累死累活拿温饱线形成鲜明对比,可见大厂里跟对项目是多么的重要。


只是没想到第一年就是巅峰,后面随着贸易战、反垄断,鹅厂形势越来越差,我们业务收入也受到极大影响,人员也越来越冗余。挣的钱少了,分蛋糕的人还多了,可想而知,收入越来越差。


在这里,我成功从8级升到了10级工程师。说起来我们这一届实名惨,第一批毕业要等1年才能晋级,最后一批8升9需要公司范围内通道评审,第一批9升10走BG内评审,第一批10升11走部门内评审,全程试验品,一样没落下。


10升11


去年年底公司晋升改革,10升11评审下放部门,职级和待遇不再挂钩,不仅看重”武功“,还看中”战功“,同时传闻这次晋升名额极少。大领导找我谈话,要不就别去了?我想了想这两年的技术和项目积累,不甘心。


整个中心,就我和同组的小伙伴两个人一起去,我挑的是个一直在做的有技术挑战、持续优化的项目,小伙伴挑的是挣钱的项目。我准备了几个周末,小伙伴临时抱佛脚,结果小伙伴过了,我没过。评委说,我过度设计(优化过头)、没有实战演练容灾。


我和大领导说这个和我预期差太多了,我和小伙伴一起预答辩过,都知道讲的啥,咱是技术评审,我的项目技术含量、架构和挑战明明更好,为啥是我没过?大领导说你是认知偏差,人家讲的确实好。不忿,遂找评委 battle,咱要真按这个说法,能不能对参加评审的项目一致对待,不要双标?且不说我是不是过度设计,凭啥对我的项目就要求容灾演练,对别人的项目就视而不见?评委不语,你的项目对部门价值没产生什么收益和价值。


部门400+人,4个晋升11级名额,大概率是一个中心一个。一个技术评审,糅合了许多超过技术方面的考量,业务挣不挣钱和技术有啥关系?技术好的业务就能挣钱?业务挣钱多的是技术好的原因?


我以前也晋级失败过,但是我认输,因为相对而言,整个BG一起评审,你没过就是你技术差,其他因素影响没有这么大。这次明明是我更有技术深度、更投心思在优化上,却事与愿违。


反思与感悟


反思一下。千头万绪,毛主席说,那是没有抓住主要矛盾。


大的方面上讲:这两年大环境差,公司收入减少,需要降本增效。我要是管理层,也会对内部薪资成本、晋升等要求控制增速;政策制定上,也会将资源更倾斜于现金流业务,控制亏损业务的支出。在这个大的背景下,公司不愿意养“能力强,战功少”的技术骨干了;更愿意养“能力强,战功多”的人员。希望员工把精力都聚焦在业务挣钱上。


部门层面上讲:和公司政策一脉相承。晋升名额少,那紧着挣钱的中心、挣钱的小组、挣钱的个人给。这个意义上讲,职级体系已经不是衡量技术能力的标尺了。你技术能力强,没用。你得是核心,得是领导认可的人。


中心层面上讲:要争取名额,否则怎么团结手底下人干活。而且名额要给业务干活干的最出色的那个人,其他人就往后稍稍。我要是我领导,年前也会劝退我的。毕竟我技术能力虽然强,更多的是问题专家的角色,而不是业务核心。


且预测一下,中心之间肯定进行了各种资源置换。评审估计流于形式。慢说我被挑出来了问题,就是挑不出来问题也得给你薅下来。我就是和小伙伴互换项目去讲,估计还是小伙伴过,到时候评委就该说我“你做这东西有啥难度,我叫个毕业生都能来做”了。此处我想起了一副对联,不太合适,就是很搞笑。“说你行你就行不行也行,说你不行你就不行行也不行”,横批是“不服不行”。



从个人角度讲,付出和收获不成正比,难受吗?那肯定的。但谁让方向一开始就错了呢?这世界不公平的事情多了,不差这一个。



重新出发


综上来说,短期内,不改变发力方向到业务上,后面可以说晋升无望。以前老是觉得自己技术能力强,心高气傲,心思没在业务上,勤于术而懒于道,实在是太过幼稚。做了几年都对很多东西不甚了了。


今后,需要趁着还能拼的时候拼一下,潜心业务。编码和开发只是很小一部分,更要从:



  1. 业务大局出发,实时数据驱动,监控、统计一定要完善,要有看板,否则无法衡量。

  2. 主动探索业务,给产品经理出主意,一起合力把产品做上去做大,提升对业务的贡献,探索推荐、探索推广渠道等等。产品做不上去,个人也没机会发展。

  3. 多做向上管理,和领导、大领导多沟通。做好安排的每一件小事,同时主动汇报,争取重获信任。

  4. 主动承担,做一个领导眼里靠谱放心的人。

  5. 多思考总结,多拔高提升,不要做表面的勤奋工作,看似没有浪费时间,实则每分每秒都在浪费时间。

  6. 多社交,多沟通,多交流,打破技术人员的牢笼。


凡事都有两面性,福兮祸所依,祸兮福所倚。多受挫折、早受挫折不是坏事。


2023,就立个

作者:醉梦星河
来源:juejin.cn/post/7208907027840630840
flag 在这里吧。

收起阅读 »

逆流而上、拒绝躺平

前言 这是一篇非技术的文章,有感于近期和团队小伙伴们的交流、以及观察到的最近这一两年来大家对生活和工作态度的明显变化,一直在思考如何在这种逆风的局面下让自己成为受益者、而不是单纯的悲观主义者。算是一篇心灵鸡汤吧,写给自己、写给自己团队小伙伴们、也写给所有的同行...
继续阅读 »

前言


这是一篇非技术的文章,有感于近期和团队小伙伴们的交流、以及观察到的最近这一两年来大家对生活和工作态度的明显变化,一直在思考如何在这种逆风的局面下让自己成为受益者、而不是单纯的悲观主义者。算是一篇心灵鸡汤吧,写给自己、写给自己团队小伙伴们、也写给所有的同行(特别是几年才入行、或者即将入行的)。


现实总是那么艰难


一种整体的社会现象


“躺平”是一种来自中国的社会现象,源于一些年轻人对当前社会竞争压力和生活压力的反思。这个词汇在近年来逐渐流行起来,它代表了一种消极的、放弃抵抗的生活态度。躺平者认为,面对高房价、高消费、高竞争压力等现实问题,他们选择不再过度努力,而是保持低消费、低欲望的生活方式,以求在现实压力下保持心理平衡。躺平主义者通常会减少工作时间,避免加班,降低生活消费水平,不再过分追求物质和金钱。这种现象在一定程度上反映了年轻人对现实生活的无奈和对未来的担忧。


一个无可逃避的经济周期


【经济不可能永远高速增长】


经过改革开放这40多年以来的高速发展,经济的绝对值已经很大,这注定了速度会越来越慢;而且经济是有周期,不可能一直线性增长。


经济发展就像人的一生一样,总是在不断的波折中向前发展,只是当前我们正好赶上了前进道路上的逆境。


【社会的长期稳定性和阶层之间的流动性成反比】


如果一个社会长期稳定,那么阶层固化就会越来越严重,这是历史发展的必然,不是我们任何一个个体可以抗衡的。


一种心存美好但又无比失望的纠结心态


【心存美好是因为现实确实很美好】


能看到这篇文章的读者,一定是这个时代的受益者,我们一步一步见证了生活在不断地变好;尤其是我们这个行业的从业者,正享受着比大多数行业都要优厚的福利。


【失望是因为我们一直心存希望】


之所以现在大家感受到失望、甚至绝望,是因为还有希望,只是今非昔比,希望比较渺茫而已。


幸与不幸,完全来自于内心


面对当前的逆风局,无外乎以下几种选择:


1、消极应对:就地躺下,放弃抵抗,不做任何努力


能不干的就不干,降低欲望,放弃自我提升,放弃一切竞争


2、积极应对:保持拼搏和竞争意识,不放弃任何机会


保持顺风局一样的心态,不断学习进度、调整自我心态,努力拼搏,抓住任何能抓住的机会


更多的失望来源于应对方式和期待收获之间的错位


> 消极应对、降低甚至放弃期待
> 积极应对、保持或降低期待
> 消极应对、保持期待

从现实中的观察来看,第一类和第二类人所占的比例并不高,更多的是属于第三类,这类人一边吐槽和抱怨现实的不公、一边用实际行动来表明自己是在躺平,这两种行为螺旋交织在一起让这类人充满了负能量,随着时间的推移不断带来负向的反馈,结果就是内心的不平衡不断加剧,最终影响到生活以及身边亲近的人。


心态好,眼里才会有光


逆风局是弯道超车的机会


在当前逆风的情况下,大部分人都放弃了竞争和自我进步,这意味着相比之前我们可以用更少的投入,就能在竞争格局中保持一个相对有利的位置。


如果你能继续保持之前的投入的话,那么你将与其他竞争者拉开更大的差距。


现在是在为未来做投资


选择长期利益还是短期利益,这会决定你的心态和实际行动:


选择短期利益:这个更符合人性,因为让人会更加的舒适,顺应内心的想法,过好眼前的每一天


选择长期利益:就像投资一样,这是反人性的,毕竟短期看投入和收益是不成正比的;要想收获更多的长远利益,就得不断地调整自己的心态,不断对抗人性,这对大多人来说是非常痛苦的。


这两种选择都没有对错之分,只是一种生活态度罢了,只要你能接受因你的选择而带来的结果就行,不必刻意纠结


我的人生态度


用最悲观的眼光看待现实,用最积极的态度应对未来


现实不是我们任何一个个体能够干预的,我们唯一能做的就是时刻保持一个好的心态,降低短期的收获期待,时刻保持竞争意识,这样才能在逆风局中用相对较小的代价超越更多的竞争者,让自己保持在一个相对有利的位置。


结束语


以上是我最近一年的一些感想以及对未来的应对

作者:先秦剑仙
来源:juejin.cn/post/7264921418810966031
态度,与君共勉!!!

收起阅读 »

又干倒一家公司,我悟了

目前这家公司由于是政府项目,好多项目周期过长,未能及时回款,导致资金链断裂,摇摇欲坠,开始裁员。虽然由于项目资源稀缺,需要我,不至于裁到我头上,但是生存压力迫使我看向了外边,很快决定了入职新公司,同事请领导们吃散伙饭,有被裁的大佬,有留下的,有主动辞职的,各自...
继续阅读 »

目前这家公司由于是政府项目,好多项目周期过长,未能及时回款,导致资金链断裂,摇摇欲坠,开始裁员。虽然由于项目资源稀缺,需要我,不至于裁到我头上,但是生存压力迫使我看向了外边,很快决定了入职新公司,同事请领导们吃散伙饭,有被裁的大佬,有留下的,有主动辞职的,各自怀着复杂的心情,聊了几个小时,现总结一下,作为教训:


1. 技术方面

  • 谨慎对待新技术,技术路线要有延续性,做大版本更新,而不是每个项目都尝试新的路线方案。
  • 小项目,以高复用和快速实现为首要目的,不频繁更换框架。


2. 需求方面

  • 以产品或项目的需求导向而不是以技术为纲。

  • 需求方也许不懂要什么,产品去引导,简单到复杂,别把自己带进技术化和复杂化的迷宫。

  • 有些急的项目前期可以快速简单实现,先验证,而不是等原型、设计,错失良机。


3. 人员方面

  • 主要技术领导别追求完美,要控主方向和整体,而不是较真细节。

  • 十个项目一个成,架构师们歇歇吧,杀鸡不兴用牛刀,尽可能简单实现,别整太复杂的框架和路线。

  • 团队内聚,可以将产品、测试长期配置到各开发团队,减少成本矛盾和沟通问题。

  • boss需要及时频繁的与主要领导沟通,定方向,放大权,用人不疑。


4. 企业管理方面

  • 切忌盲目自信,疯狂扩张团队,利润和成本须同步,不能因短期项目招兵买马。

  • 适时而果断拿起手术刀,坚决断臂求生,及时且合规地删减人员,要有人情但不能妇人之仁、拖泥带水,对个体和公司都好。

最后,警惕以上问题,期待自己尽快找到更明确的方向,去奋斗!


作者:Adam289
链接:https://juejin.cn/post/7208534700223184951
来源:稀土掘金
收起阅读 »

Xcode快捷Behavior

前言在Xcode开发环境中,有一些可以自定义的快捷Behavior,可以大大提高开发效率。如何配置Behavior以下是在Xcode中配置Behavior的通用步骤:1.打开Xcode的偏好设置。2.点击“Behaviors”选项卡。3.点击左下角的"+"号创...
继续阅读 »

前言

在Xcode开发环境中,有一些可以自定义的快捷Behavior,可以大大提高开发效率。


如何配置Behavior

以下是在Xcode中配置Behavior的通用步骤:

1.打开Xcode的偏好设置。

2.点击“Behaviors”选项卡。

3.点击左下角的"+"号创建一个新的Behavior。

4.为Behavior命名,例如你希望调用的脚本名。

5.在“Run”下选择“Script”,然后粘贴你的脚本。

6.按需配置快捷键,并保存。

现在,每当你使用配置的快捷键时,它就会运行你的脚本。

Behavior1:打开终端并cd到当前工作目录
创建open_terminal.sh,写入以下内容

#!/bin/bash
open -a terminal "`pwd`"

并添加执行权限

sudo chmod +x open_terminal.sh

添加Behavior并在Run处选中该脚本路径,配置好快捷键。 当你使用配置的快捷键时,就会打开终端并cd到当前工作目录。

Behavior2:打开项目文件夹
创建open_project_folder.sh,写入以下内容

#!/bin/bash

# Path to your project
project_path="$1"

# Open the project folder in Finder
open "$project_path"

并添加执行权限

sudo chmod +x open_project_folder.sh

添加Behavior并在Run处选中该脚本路径,配置好快捷键。
当你使用配置的快捷键时,就会在Finder中打开你的项目文件夹。

总结

通过配置Behavior,我们可以更快速地访问项目文件夹和命令行等,从而提高开发效率。自定义Behavior是Xcode强大功能的一个体现,它允许我们根据自己的需求调整开发环境。

作者:冰淇淋真好吃
链接:https://juejin.cn/post/7262634764301844536
来源:稀土掘金

收起阅读 »

60分前端之canvas

web
前言 最近在等前端对接,没事干就折腾一下前端。写了这么多年服务端,一直都是写api接口,还没好好学过前端呢。前端框架那么多,什么Vue、React,还分移动端、PC端,还有什么响应式、自适应,各种花里胡哨的东西,也没那么多精力去折腾,直接上手小程序吧。 小程序...
继续阅读 »

前言


最近在等前端对接,没事干就折腾一下前端。写了这么多年服务端,一直都是写api接口,还没好好学过前端呢。前端框架那么多,什么Vue、React,还分移动端、PC端,还有什么响应式、自适应,各种花里胡哨的东西,也没那么多精力去折腾,直接上手小程序吧。


小程序开发虽然简单,但基本的CSS样式还是得学一下吧:

1. MDN官方CSS教程
2. 谷歌出的CSS教程
3. 谷歌HTML教程


在国内,微信小程序开发,应该是每个前端程序员必会的技术了吧。


话不多说,直接开干。


写了这么多年的hello world了,学一门“新”的技术还是很简单的。而且咱要求也不高,毕竟不是专门写前端的,所以对于前端的技术,只要做到60分就够了。


所以,下面很多东西,我们都是能忽略就忽略,主打一个囫囵吞枣、不求甚解,以解决问题为主。


微信小程序类目的坑


微信小程序文档


学习任何技术,官方文档都是最好的入门方式之一。


查看微信小程序官方文档,照着文档一步一步往下走,有个两年开发经验的人,基本都能搞定,没什么难度。


但是问题来了,在配置小程序时,需要设置小程序类目。这里提前说一下,因为是练手的项目,所以我写的这个小程序是个大杂烩,既有数独小游戏,又有背单词。由于微信小程序类目可以选择多个,所以我先选择了小游戏......


问题来了!


微信小程序类目,选择了游戏之后,就不能选择其他的了!


而且还改不了!!


改不了就算了,更坑的是,小游戏的代码结构和小程序还不一样,没办法通用。选择小游戏,就必须用小游戏的代码结构,没办法和小程序混用。


练手的几个小游戏,我都是用小程序实现的,没办法在小游戏模式下运行。


网上搜了一下,发现很多人都遇到过这样的问题,也都向官方反映过了,但官方都没有给回复。顺带一提,好像社区里很多问题,官方基本都不回复的。


没办法,只能重新注册一个号了。或者注销之前的号,然后重新注册也行。嫌麻烦,干脆重新注册一个号吧。


数独81宫格实现过程

数独游戏估计很多人都只听过,但没实际玩过,这里就不详细介绍数独游戏了,我们只需要知道一点,数独游戏需要有一个81宫格。


可以看到,数独的81宫格最外层边框加粗,里面每三列的右边框、每三行的下边框也加粗,相当于是9个9宫格合并在一起。

作为前端的初学者,想要实现一个样式,第一反应肯定是用CSS来实现。

CSS选择器

先来把基本的框给实现了。

.sudoku {
box-sizing: border-box;
width: 700rpx;
min-height: 730rpx;
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(9, 1fr);
gap: 0;
border: 2px solid #000000;
}
.cell {
background-color: #FFFFFF;
border: 1px solid #000000;
border-right-width: 0;
border-bottom-width: 0;
}

实现效果是这样的:


不知道大家有没有注意到这段样式min-height: 730rpx;,设置最小高度。


一开始设置height: 700rpx;,宽高都是700rpx,在iPhone12/13 Pro Max机型下没问题,可是在iPhone12/13 Pro机型下,最下面一行会少一截,高度不够了!


前端的同学都知道CSS的一个概念,叫盒模型。这里高度不够是因为每个小格子的边框也有个宽度,实际总高度应该是每个格子的高度加上格子边框的宽度之和。宽度也是一样的。


这里有一个问题,就算宽高要加上每个小格子边框的宽度,最终的宽高也应该一样的才对啊?!为什么高度反而比宽度还大了呢?!


不理解!算了,毕竟是写服务端的,前端只要做到60分就够了。解决问题就行。高度不够的话,给它加高不就行了。/doge


加多少呢?试呗。😁


好了,在iPhone12/13 Pro机型下问题是解决了,但其他机型下呢?


这个好办,既然知道高度可能不够,继续加高的话,怕影响其他机型下的效果,那干脆给一个最低高度,超过这个高度就让它自己加高,简单粗暴。


它已经是个成熟的高度了,已经学会了自己长高。


还有一种更直接的办法,就是不设置高度,让它自己撑开。但是这样有一个不好的点,高度在撑开的过程中,会很明显看到页面抖动。设置一个最低高度的话,抖动就不明显了。


只要我看不到它抖动,它就不存在。


基本的形状已经有了,下面开始对内部指定的边框进行加粗。先来对每三列的右边框加粗,看过上面的CSS教程的话,稍微学点CSS知识的人都知道,CSS有个伪类选择器,我们用选择器nth-child来实现:

.cell:nth-child(3n+1) {
border-left-width: 2px;
}

:nth-child(n) 选择器匹配父元素中的第 n 个子元素,元素类型没有限制。


n 可以是一个数字,一个关键字,或者一个公式。



这里,我们用的是公式的方式。



使用公式(an+ b)a代表一个循环的大小,n是一个计数器(从0开始),以及b是偏移量。ab都必须是整数,an 必须写在 b 的前面,不能写成 b+an 的形式。


第一列左边框看起来太粗了,虽然样式上看起来边框的宽度是2px,但视觉上却像是4px也不知道是什么原因?


不管它什么原因了,毕竟我们不是专业的前端,就像上面说的那样,我们只需要做到60分就够了。


既然视觉上看起来像是4px,那就让它在视觉上看起来像2px

.cell:nth-child(9n+1) {
border-left-width: 1px;
}


现在看起来顺眼了很多。


宽度明明设置的是1px,为什么视觉上看起来像2px,不知道什么原因?老规矩,不管!😁


每三列的左边框加粗,这个已经实现了,下面开始实现每三行的下边框加粗


可是问题来了,使用:nth-child选择器貌似不能选中连续的子元素,网上搜了一堆,都没有解决方案,后来问了一下ChatGPT,给的回复是这样的:

/* 设置第3行和第6行的下边框宽度为2px */
.cell:nth-child(3n+1):nth-child(n+7),
.cell:nth-child(3n+2):nth-child(n+8),
.cell:nth-child(3n+3):nth-child(n+9) {
border-bottom-width: 2px;
}

还亲切地给了注释。可惜根本用不了。


除了第一行前6个的下边框没加粗外,其他元素的下边框全加粗了!


题外话:ChatGPT对CSS的问题,一本正经地胡说八道,给的答案基本都用不了。


折腾了好久,都没折腾出怎么用CSS选择器实现每三行下边框加粗这个效果,算了,换种方式吧。


对象模型


数独游戏不但要有这个宫格,相应的宫格里还得要有数字的。最终的效果应该像这样:


既然没办法自动计算出每三列和每三行的元素,那我们就用笨办法,手动给它们全标记出来。

定义对象模型:

interface SudokuItem {
numerical: number, // 数值
tower: boolean, // 左边框是否需要加粗
floor: boolean // 下边框是否需要加粗
}

初始化对象模型:

Page({
data: {
sudoku: [
{numerical: 0, tower: false, floor: false},
{numerical: 0, tower: false, floor: false},
{numerical: 0, tower: true, floor: false},
...
]
}
})

实际初始化的时候,通过JS代码初始化sudoku,并计算哪个元素的tower为true,哪个floor为true。

页面渲染的时候:


class="cell-item {{item.tower ? 'tower' : ''}} {{item.floor ? 'floor' : ''}}"
bindtap="focusTheCell"
data-index="{{idx}}"
>
{{item.numerical == 0 ? undefined : item.numerical}}

.sudoku .floor {
border-top-width: 2px;
}

.sudoku .tower {
border-right-width: 2px;
}

好了,基本的样式是实现了,虽然过程很简单粗暴,但咱毕竟是写服务端的,对前端的要求也不高,能做到60分就够了。


继续折腾,上面是用CSS样式实现的81宫格,还有其他办法吗?


必须有啊,前端的Canvas技术也很火啊,这个必须得折腾一下啊。


而且貌似还有很多前端不会Canvas的哦,等我们学会了,就去找前端嘚瑟一下。


Canvas


MDN官方教程


先看Canvas官方教程,嗯......,东西挺多的,好像有点复杂,几十个API,看得人眼花。没关系,毕竟我们是带着问题来学习的。这里,我们只需要几个API就行了。


什么问题?使用Canvas画出一个数独的81宫格。


我在学习一个新技术的时候,都是先上手再深入,不懂的地方暂时先跳过去,前面说过的,主打一个囫囵吞枣、不求甚解。先把问题解决了再说。


我们先暂时抛开小程序,只专注于Canvas。


直接开干。


首页,创建一个Canvas画布。

const canvas = document.getElementById("sudoku");
const ctx = canvas.getContext("2d");

好了,一个画布就创建好了。是不是很简单。而且是固定套路,可以不理解,记住就行了。下次用的时候,直接复制粘贴,再简单改改就行了。


id:和HTML标签的id属性一样。


widthheight:画布的宽高,可以通过CSS属性调整。这里的宽高类似于图片的原始宽高,CSS调整后的宽高就和修改页面图片的宽高一样,实际是对图片进行缩放。


canvas.getContext("2d"):接受一个参数,即上下文的类型。可以传入2dwebglwebgl2bitmaprenderer,用于创建不同类型的上下文。


这里我们传入2d,创建一个二维渲染上下文。


画布创建好了,下面该画画了。


想想看,如果让你在现实中用笔画一个81宫格,你应该怎么做?


首先,得有张白纸,然后提笔,之后开始画。


在Canvas中也一样,白纸我们已经有了,上面创建的画布就是白纸。


下面,该考虑怎么画了。初中数学老师告诉我们,两点确定一条直线,从A点到B点,画一条直线。


Canvas通过路径和填充的方式来绘制图画的,填充我们暂时不管,路径可以简单理解为线段,直线、曲线这些。


每条线段就是一条路径。



生成路径的第一步叫做 beginPath()。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。



不理解?记住就行了,都是固定套路。先记住怎么用,等bug写多了,自然就理解了。别管代码是不是写的垃圾,先敲出来再说,别只光在脑子里想。


ctx.beginPath():清空子路径列表开始一个新路径。可以简单理解成“提笔”。


ctx.moveTo(x, y):将一个新的子路径的起始点移动到 (x,y) 坐标。可以理解成上面的“从A点......”


ctx.lineTo(x, y):使用直线连接子路径的终点到(x,y)坐标。可以理解成上面的“到B点,画一条直线”


上面的(x, y)为坐标,页面坐标这个东西,相信很多人应该都已经知道了。


这里的坐标为画布中的坐标,坐标原点(0, 0)为画布的左上角。

const canvas = document.getElementById("sudoku");
// 创建二维上下文
const ctx = canvas.getContext("2d");
// 从A点
ctx.moveTo(100, 100);
// 到B点
ctx.lineTo(200, 100);

刷新一下页面,什么都没有!


别急,这就和我们现实中画画一样,先要在脑中构思,构思完了,才开始落笔绘画。


同样的道理,上面的那些相当于是在构思,构思完了后,我们得要落笔绘制。


ctx.stroke():根据当前的画线样式,绘制当前或已经存在的路径。


所以,完整的代码应该是这样的:

const canvas = document.getElementById("sudoku");
// 创建二维上下文
const ctx = canvas.getContext("2d");
ctx.beginPath();
// 从A点
ctx.moveTo(100, 100);
// 到B点
ctx.lineTo(200, 100);
// 画线
ctx.stroke();


具体可以看一下MDN的示例,使用canvas来绘制图形


好了,一条直线就绘制出来了。


可以了,能画出一条直线,我们可以开始画81宫格了。81宫格就是10条横线、10条竖线组成的。只要计算好每个点的坐标,然后画线就行了。


计算点的坐标,这是数学的范畴,就不过多赘述了,而且点的坐标计算也不难。

const canvas = document.getElementById("sudoku");
const ctx = canvas.getContext("2d");
// 单元格的宽高
const cellSize = 50;

for (let i = 0; i < 10; i++) {
// 画横线,留5px的间距
ctx.beginPath();
ctx.moveTo(5, i * cellSize + 5);
ctx.lineTo(455, i * cellSize + 5);
ctx.stroke();

// 画竖线
ctx.beginPath();
ctx.moveTo(i * cellSize + 5, 5);
ctx.lineTo(i * cellSize + 5, 455);
ctx.stroke();
}


81宫格是画出来了,可还没法用,因为数独中的81宫格,要求最外围边框加粗,里面每三列左边框加粗、每三行下边框加粗。


ctx.lineWidth:设置线段的宽度


还记得上面我们通过CSS选择器给边框加粗时遇到的痛苦吗,现在就简单多了,只需要简单的数学计算就可以了。

// 设置画笔宽度
if (i % 3 == 0) {
ctx.lineWidth = 3;
} else {
ctx.lineWidth = 1;
}

设置完后我们,看一下效果


下面是完整的代码:






<span class="hljs-string">Canvas</span> <span class="hljs-string">练习</span>





之后只要根据小程序的规则,将代码移植到小程序里就行了。至于怎么往数独里填充数字,这个之后再说吧,我们一点一点的来。这里我们只介绍这么画直线。


学一门新技术,一定要多练。数独的81宫格画完了,还可以画点其他的啊。比如画一个象棋的棋盘,或者画一个围棋的棋盘。


多练手,遇到的问题多了,自然就会了。


最后


虽然是一个练手的项目,但是既然已经做出来了,干脆就上线算了。

作者:W_懒虫
链接:https://juejin.cn/post/7260820017758093349
来源:稀土掘金
收起阅读 »

历时一个月,终于找到自己满意的工作了

由于公司经营遇到了巨大问题,出现严重亏损。 不得不忍痛告诉全体员工团队解散一事 衷心感谢全体小伙伴们在公司付出努力与汗水 目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。 再次感谢各位伙伴们 希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦...
继续阅读 »

由于公司经营遇到了巨大问题,出现严重亏损。
不得不忍痛告诉全体员工团队解散一事
衷心感谢全体小伙伴们在公司付出努力与汗水
目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。
再次感谢各位伙伴们
希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦



2023年6月16日我正在开开心心的写着代码,突然来了这么一条消息;我直接原地呆住了,沉思良久我才发现办公室内寂静的可怕,没有了键盘的敲击声;有的只有同事们的呼吸声,大家都依靠在椅子上,像是在思考什么。很快大家也都接受了这个不愿看到的实事。过了一两天就在开始办理离职一类的事情了。


投递简历


在离职以后,我们同事之间约好一起打了2个小时的球,吃了一个饭;在我们经常唱歌的地方,唱了几个小时歌,我们玩的很开心、吃的很开心,就像从来没有经历过这件事情一样。


在过了一段时间以后,我开始修改简历投递简历了。沟通的第一天就给我狠狠的打了一次脸,我在沟通了20/30个公司的时候,恢复我消息的不足5个,已读的大约有一半左右;后面我就开始加大了沟通的力度;大约在沟通了140家以后,收到了第一个面试机会。


然后第二个面试机会是在人才市场去找的。
第三个面试机会是在沟通了60多家以后得到的机会也是我满意的一家。


第一家


该公司是一家外包公司在成都的分公司,我进去面试的时候没有笔试题,直接是主管来面试的,主要的问题还是围绕业务层面以及上家的一些工作经历,然后就是一些关于vue的一些原理以及简单的算法问题。当时在面试完以后我自己感觉很好;觉得肯定能面试上,结果真的面试上了;下午的时候这家公司就给我打电话了,给出的工资是11k;但是我觉得外包不是很喜欢,而且是单双休,后面就给拒绝了,没想到周一的时候人事又给我打电话了,说主管这边商量了以后决定给你涨一千;请问你愿意来吗?说实话,当时是真的心动了;我考虑了一天以后还是拒绝了,因为我实在是不喜欢外包。


第二家


该公司属于半外包性质的,经历了两面,第一面是技术面(超级简单),第二面是主管过来的(主管是后端)面试的,第二面主要就是业务方面的,当时说的是智慧数字一类的产品,我当时确实被该概念吸引了,后面再谈了以后,发现并没有给我offer,我就发了一个消息过去问,然后人事告诉我说是:工资要的太高了,公司给不了,然后我说可以调薪;在多次沟通后给到了9.5k;然后半年有一次涨薪的机会、年底双薪以及试用期交社保一类的。我进去待了几天发现他们并不是什么智慧数字,而且技术用的不是很好的,所以我就放弃了,我个人觉得对我的技术提升没有太多的帮助


第三家


该公司是一家完全自研的公司,并且产品已经上线,用户量达到了千万,日活也有10多20万的样子;让我觉得很不错,所以在知晓了之后就对自己说一定要好好面一定要进去,哈哈哈


在该公司经历了三次面试吧!


第一次是技术面,问的问题也是一些业务问题;然后会涉及到一些js的基础原理以及vue中的一些实现原理等等问题(主要是大部分都忘了,哈哈哈);


然后第二次就是人事小姐姐问了一些问题,问了问题以后,人事小姐姐叫来了一个领导然后跟我谈,主要谈的话就是一些收获啊、自豪感啊、研究等等问题。


在面试完以后都过去了4个小时了,又遇到了下大雨;我骑上我的小电驴穿梭在城市的街头,却充满了一点小小的期待。


过了几个小时以后,通知我说面试通过了,试用期也是有社保、有年终、也有涨薪制度等等


补充:在这家公司中可能工资不高,但是这家公司的技术能力比较强而且用的很多技术我都不会,并且我也很想去学习此类的技术,刚好有这个机会,所以我是很开心的。


总结


以上是我这段时间面试的一些经历;但是工作确实并不好找。


主要原因还是面试机会少,很多公司都要求本科以及本专业等等,其实面试的话都还好基本上跟原来差别不大,还有就是对刚出来的这些小伙伴可能不是很友好。所以希望大家如果有工作的话,就先好好上班吧!目前大环境都是这样的,加油哦!


作者:雾恋
链接:https://juejin.cn/post/7263274550074769465
来源:稀土掘金
收起阅读 »

作为一个老程序员,想对新人说什么?

前言最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。这篇文章根据我多年的工作经验,给新人总结了26条建议,希望对你会有所帮...
继续阅读 »

前言

最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。


在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。

这篇文章根据我多年的工作经验,给新人总结了26条建议,希望对你会有所帮助。

1.写好注释


很多小伙伴不愿意给代码写注释,主要有以下两个原因:

1. 开发时间太短了,没时间写注释。
2.《重构》那本书说代码即注释。


我在开发的前面几年也不喜欢写注释,觉得这是一件很酷的事情。


但后来发现,有些两年之前的代码,业务逻辑都忘了,有些代码自己都看不懂。特别是有部分非常复杂的逻辑和算法,需要重新花很多时间才能看明白,可以说自己把自己坑了。


没有注释的代码,不便于维护。


因此强烈建议大家给代码写注释。


但注释也不是越多越好,注释多了增加了代码的复杂度,增加了维护成本,给自己增加工作量。


我们要写好注释,但不能太啰嗦,要给关键或者核心的代码增加注释。我们可以写某个方法是做什么的,主要步骤是什么,给算法写个demo示例等。


这样以后过了很长时间,再去看这段代码的时候,也会比较容易上手。


2.多写单元测试


我看过身边很多大佬写代码有个好习惯,比如新写了某个Util工具类,他们会同时在test目录下,给该工具类编写一些单元测试代码。


很多小伙伴觉得写单元测试是浪费时间,没有这个必要。


假如你想重构某个工具类,但由于这个工具类有很多逻辑,要把这些逻辑重新测试一遍,要花费不少时间。


于是,你产生了放弃重构的想法。


但如果你之前给该工具类编写了完整的单元测试,重构完成之后,重新执行一下之前的单元测试,就知道重构的结果是否满足预期,这样能够减少很多的测试时间。


多写单元测试对开发来说,是一个非常好的习惯,有助于提升代码质量。


即使因为当初开发时间比较紧,没时间写单元测试,也建议在后面空闲的时间内,把单元测试补上。


3.主动重构自己的烂代码


好的代码不是一下子就能写成的,需要不断地重构,修复发现的bug。


不知道你有没有这种体会,看自己1年之前写的代码,简直不忍直视。


这说明你对业务或者技术的理解,比之前更深入了,认知水平有一定的提升。


如果有机会,建议你主动重构一下自己的烂代码。把重复的代码,抽取成公共方法。有些参数名称,或者方法名称当时没有取好的,可以及时修改一下。对于逻辑不清晰的代码,重新梳理一下业务逻辑。看看代码中能不能引入一些设计模式,让代码变得更优雅等等。


通过代码重构的过程,以自我为驱动,能够不断提升我们编写代码的水平。


4.代码review很重要


有些公司在系统上线之前,会组织一次代码评审,一起review一下这个迭代要上线的一些代码。


通过相互的代码review,可以发现一些代码的漏洞,不好的写法,发现自己写代码的坏毛病,让自己能够快速提升。


当然如果你们公司没有建立代码的相互review机制,也没关系。


可以后面可以多自己review自己的代码。


5.多用explain查看执行计划


我们在写完查询SQL语句之后,有个好习惯是用explain关键字查看一下该SQL语句有没有走索引。


对于数据量比较大的表,走了索引和没有走索引,SQL语句的执行时间可能会相差上百倍。


我之前亲身经历过这种差距。


因此建议大家多用explain查看SQL语句的执行计划。

6.多看看优秀的工具


太空电梯、MOSS、ChatGPT等,都预兆着2023年注定不会是平凡的一年。任何新的技术都值得推敲,我们应要有这种敏感性。


这几年隐约碰过低代码,目前比较热门,很多大厂都相继加入。



低代码平台概念:通过自动代码生成和可视化编程,只需要少量代码,即可快速搭建各种应用。



到底啥是低代码,在我看来就是拖拉拽,呼呼呼,一通操作,搞出一套能跑的系统,前端,后端,数据库,一把完成。当然这可能是最终目标。


链接:http://www.jnpfsoft.com/?juejin,如果你感兴趣,也体验一下。


JNPF的优势就在于它能生成前后台代码,提供了极大的灵活性,能够创建更复杂、定制化的应用。它的架构设计也让开发者无需担心底层技术细节,能够专注于应用逻辑和用户体验的开发。


7.上线前整理checklist


在系统上线之前,一定要整理上线的清单,即我们说的:checklist。


系统上线有可能是一件很复杂的事情,涉及的东西可能会比较多。


假如服务A依赖服务B,服务B又依赖服务C。这样的话,服务发版的顺序是:CBA,如果顺序不对,可能会出现问题。


有时候新功能上线时,需要提前执行sql脚本初始化数据,否则新功能有问题。


要先配置定时任务。


上线之前,要在apollo中增加一些配置。


上线完成之后,需要增加相应的菜单,给指定用户或者角色分配权限。


等等。


系统上线,整个过程中,可能会涉及多方面的事情,我们需要将这些事情记录到checklist当中,避免踩坑。


8.写好接口文档


接口文档对接口提供者,和接口调用者来说,都非常重要。


如果你没有接口文档,别人咋知道你接口的地址是什么,接口参数是什么,请求方式时什么,接口多个参数分别代码什么含义,返回值有哪些字段等等。


他们不知道,必定会多次问你,无形当中,增加了很多沟通的成本。


如果你的接口文档写的不好,写得别人看不懂,接口文档有很多错误,比如:输入参数的枚举值,跟实际情况不一样。


这样不光把自己坑了,也会把别人坑惨。


因此,写接口文档一定要写好,尽量不要马马虎虎应付差事。


如果对写接口文档比较感兴趣,可以看看我的另一篇文章《瞧瞧别人家的API接口,那叫一个优雅》,里面有详细的介绍。


9.接口要提前评估请求量


我们在设计接口的时候,要跟业务方或者产品经理确认一下请求量。


假如你的接口只能承受100qps,但实际上产生了1000qps。


这样你的接口,很有可能会承受不住这么大的压力,而直接挂掉。


我们需要对接口做压力测试,预估接口的请求量,需要部署多少个服务器节点。


压力测试的话,可以用jmeter、loadRunner等工具。


此外,还需要对接口做限流,防止别人恶意调用你的接口,导致服务器压力过大。


限流的话,可以基于用户id、ip地址、接口地址等多个维度同时做限制。


可以在nginx层,或者网关层做限流。


10.接口要做幂等性设计


我们在设计接口时,一定要考虑并发调用的情况。


比如:用户在前端页面,非常快的点击了两次保存按钮,这样就会在极短的时间内调用你两次接口。


如果不做幂等性设计,在数据库中可能会产生两条重复的数据。


还有一种情况时,业务方调用你这边的接口,该接口发生了超时,它有自动重试机制,也可能会让你这边产生重复的数据。


因此,在做接口设计时,要做幂等设计。


当然幂等设计的方案有很多,感兴趣的小伙伴可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》。


如果接口并发量不太大,推荐大家使用在表中加唯一索引的方案,更加简单。


11.接口参数有调整一定要慎重


有时候我们提供的接口,需要调整参数。


比如:新增加了一个参数,或者参数类型从int改成String,或者参数名称有status改成auditStatus,参数由单个id改成批量的idList等等。


建议涉及到接口参数修改一定要慎重。


修改接口参数之前,一定要先评估调用端和影响范围,不要自己偷偷修改。如果出问题了,调用方后面肯定要骂娘。


我们在做接口参数调整时,要做一些兼容性的考虑。


其实删除参数和修改参数名称是一个问题,都会导致那个参数接收不到数据。


因此,尽量避免删除参数和修改参数名。


对于修改参数名称的情况,我们可以增加一个新参数,来接收数据,老的数据还是保留,代码中做兼容处理。


12.调用第三方接口要加失败重试


我们在调用第三方接口时,由于存在远程调用,可能会出现接口超时的问题。


如果接口超时了,你不知道是执行成功,还是执行失败了。


这时你可以增加自动重试机制。


接口超时会抛一个connection_timeout或者read_timeout的异常,你可以捕获这个异常,用一个while循环自动重试3次。


这样就能尽可能减少调用第三方接口失败的情况。


当然调用第三方接口还有很多其他的坑,感兴趣的小伙伴可以看看我的另一篇文章《我调用第三方接口遇到的13大坑》,里面有详细的介绍。


13.处理线上数据前,要先备份数据


有时候,线上数据出现了问题,我们需要修复数据,但涉及的数据有点多。


这时建议在处理线上数据前,一定要先备份数据。


备份数据非常简单,可以执行以下sql:

create table order_2022121819 like `order`;
insert into order_2022121819 select * from `order`;

数据备份之后,万一后面哪天数据处理错了,我们可以直接从备份表中还原数据,防止悲剧的产生。


14.不要轻易删除线上字段


不要轻易删除线上字段,至少我们公司是这样规定的。


如果你删除了某个线上字段,但是该字段引用的代码没有删除干净,可能会导致代码出现异常。


假设开发人员已经把程序改成不使用删除字段了,接下来如何部署呢?


如果先把程序部署好了,还没来得及删除数据库相关表字段。


当有insert请求时,由于数据库中该字段是必填的,会报必填字段不能为空的异常。


如果先把数据库中相关表字段删了,程序还没来得及发。这时所有涉及该删除字段的增删改查,都会报字段不存在的异常。


所以,线上环境字段不要轻易删除。


15.要合理设置字段类型和长度


我们在设计表的时候,要给相关字段设置合理的字段类型和长度。


如果字段类型和长度不够,有些数据可能会保存失败。


如果字段类型和长度太大了,又会浪费存储空间。


我们在工作中,要根据实际情况而定。


以下原则可以参考一下:

1.尽可能选择占用存储空间小的字段类型,在满足正常业务需求的情况下,从小到大,往上选。

2.如果字符串长度固定,或者差别不大,可以选择char类型。如果字符串长度差别较大,可以选择varchar类型。

3.是否字段,可以选择bit类型。

4.枚举字段,可以选择tinyint类型。

5.主键字段,可以选择bigint类型。

6.金额字段,可以选择decimal类型。

7.时间字段,可以选择timestamp或datetime类型。


16.避免一次性查询太多数据


我们在设计接口,或者调用别人接口的时候,都要避免一次性查询太多数据。


一次性查询太多的数据,可能会导致查询耗时很长,更加严重的情况会导致系统出现OOM的问题。


我们之前调用第三方,查询一天的指标数据,该接口经常出现超时问题。


在做excel导出时,如果一次性查询出所有的数据,导出到excel文件中,可能会导致系统出现OOM问题。


因此我们的接口要做分页设计。


如果是调用第三方的接口批量查询接口,尽量分批调用,不要一次性根据id集合查询所有数据。


如果调用第三方批量查询接口,对性能有一定的要求,我们可以分批之后,用多线程调用接口,最后汇总返回数据。


17.多线程不一定比单线程快


很多小伙伴有一个误解,认为使用了多线程一定比使用单线程快。


其实要看使用场景。


如果你的业务逻辑是一个耗时的操作,比如:远程调用接口,或者磁盘IO操作,这种使用多线程比单线程要快一些。


但如果你的业务逻辑非常简单,在一个循环中打印数据,这时候,使用单线程可能会更快一些。


因为使用多线程,会引入额外的消耗,比如:创建新线程的耗时,抢占CPU资源时线程上下文需要不断切换,这个切换过程是有一定的时间损耗的。


因此,多线程不一定比单线程快。我们要根据实际业务场景,决定是使用单线程,还是使用多线程。


18.注意事务问题


很多时候,我们的代码为了保证数据库多张表保存数据的完整性和一致性,需要使用@Transactional注解的声明式事务,或者使用TransactionTemplate的编程式事务。


加入事务之后,如果A,B,C三张表同时保存数据,要么一起成功,要么一起失败。


不会出现数据保存一半的情况,比如:表A保存成功了,但表B和C保存失败了。


这种情况数据会直接回滚,A,B,C三张表的数据都会同时保存失败。


如果使用@Transactional注解的声明式事务,可能会出现事务失效的问题,感兴趣的小伙伴可以看看我的另一篇文章《聊聊spring事务失效的12种场景,太坑了》。


建议优先使用TransactionTemplate的编程式事务的方式创建事务。


此外,引入事务还会带来大事务问题,可能会导致接口超时,或者出现数据库死锁的问题。


因此,我们需要优化代码,尽量避免大事务的问题,因为它有许多危害。关于大事务问题,感兴趣的小伙伴,可以看看我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,里面有详情介绍。


19.小数容易丢失精度


不知道你在使用小数时,有没有踩过坑,一些运算导致小数丢失了精度。


如果你在项目中使用了float或者double类型的数据,用他们参与计算,极可能会出现精度丢失问题。


使用Double时可能会有这种场景:

double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);

正常情况下预计amount2 - amount1应该等于0.01

但是执行结果,却为:

0.009999999999999998

实际结果小于预计结果。


Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。


因此,在做小数运算时,更推荐大家使用BigDecimal,避免精度的丢失。


但如果在使用BigDecimal时,使用不当,也会丢失精度。

BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));

这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。

结果:

0.0099999999999999984734433411404097569175064563751220703125

使用BigDecimal的构造函数创建BigDecimal,也会导致精度丢失。

如果如何避免精度丢失呢?

BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));

使用BigDecimal.valueOf方法初始化BigDecimal类型参数,能保证精度不丢失。


20.优先使用批量操作


有些小伙伴可能写过这样的代码,在一个for循环中,一个个调用远程接口,或者执行数据库的update操作。


其实,这样是比较消耗性能的。


我们尽可能将在一个循环中多次的单个操作,改成一次的批量操作,这样会将代码的性能提升不少。

例如:

for(User user : userList) {
userMapper.update(user);
}

改成:

userMapper.updateForBatch(userList);

21.synchronized其实用的不多


我们在面试中当中,经常会被面试官问到synchronized加锁的考题。


说实话,synchronized的锁升级过程,还是有点复杂的。


但在实际工作中,使用synchronized加锁的机会不多。


synchronized更适合于单机环境,可以保证一个服务器节点上,多个线程访问公共资源时,只有一个线程能够拿到那把锁,其他的线程都需要等待。


但实际上我们的系统,大部分是处于分布式环境当中的。


为了保证服务的稳定性,我们一般会把系统部署到两个以上的服务器节点上。


后面哪一天有个服务器节点挂了,系统也能在另外一个服务器节点上正常运行。


当然也能会出现,一个服务器节点扛不住用户请求压力,也挂掉的情况。


这种情况,应该提前部署3个服务节点。


此外,即使只有一个服务器节点,但如果你有api和job两个服务,都会修改某张表的数据。


这时使用synchronized加锁也会有问题。


因此,在工作中更多的是使用分布式锁。


目前比较主流的分布式锁有:

1.数据库悲观锁。

2.基于时间戳或者版本号的乐观锁。

3.使用redis的分布式锁。

4.使用zookeeper的分布式锁。


其实这些方案都有一些使用场景。


目前使用更多的是redis分布式锁。


当然使用redis分布式锁也很容易踩坑,感兴趣的小伙伴可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》,里面有详细介绍。


22.异步思想很重要


不知道你有没有做过接口的性能优化,其中有一个非常重要的优化手段是:异步。


如果我们的某个保存数据的API接口中的业务逻辑非常复杂,经常出现超时问题。


现在让你优化该怎么优化呢?


先从索引,sql语句优化。


这些优化之后,效果不太明显。


这时该怎么办呢?


这就可以使用异步思想来优化了。


如果该接口的实时性要求不高,我们可以用一张表保存用户数据,然后使用job或者mq,这种异步的方式,读取该表的数据,做业务逻辑处理。


如果该接口对实效性要求有点高,我们可以梳理一下接口的业务逻辑,看看哪些是核心逻辑,哪些是非核心逻辑。


对于核心逻辑,可以在接口中同步执行。


对于非核心逻辑,可以使用job或者mq这种异步的方式处理。


23.Git提交代码要有好习惯


有些小伙伴,不太习惯在Git上提交代码。


非常勤劳的使用idea,写了一天的代码,最后下班前,准备提交代码的时候,电脑突然死机了。


会让你欲哭无泪。


用Git提交代码有个好习惯是:多次提交。


避免一次性提交太多代码的情况。


这样可以减少代码丢失的风险。


更重要的是,如果多个人协同开发,别人能够尽早获取你最新的代码,可以尽可能减少代码的冲突。


假如你开发一天的代码准备去提交的时候,发现你的部分代码,别人也改过了,产生了大量的冲突。


解决冲突这个过程是很痛苦的。


如果你能够多次提交代码,可能会及时获取别人最新的代码,减少代码冲突的发生。因为每次push代码之前,Git会先检查一下,代码有没有更新,如果有更新,需要你先pull一下最新的代码。


此外,使用Git提交代码的时候,一定要写好注释,提交的代码实现了什么功能,或者修复了什么bug。


如果有条件的话,每次提交时在注释中可以带上jira任务的id,这样后面方便统计工作量。


24.善用开源的工具类


我们一定要多熟悉一下开源的工具类,真的可以帮我们提升开发效率,避免在工作中重复造轮子。


目前业界使用比较多的工具包有:apache的common,google的guava和国内几个大佬些hutool。


比如将一个大集合的数据,按每500条数据,分成多个小集合。


这个需求如果要你自己实现,需要巴拉巴拉写一堆代码。


但如果使用google的guava包,可以非常轻松的使用:

List list = Lists.newArrayList(1, 2, 3, 4, 5);
List> partitionList = Lists.partition(list, 2);
System.out.println(partitionList);

如果你对更多的第三方工具类比较感兴趣,可以看看我的另一篇文章《吐血推荐17个提升开发效率的“轮子”》。


25.培养写技术博客的好习惯


我们在学习新知识点的时候,学完了之后,非常容易忘记。


往往学到后面,把前面的忘记了。


回头温习前面的,又把后面的忘记了。


因此,建议大家培养做笔记的习惯。


我们可以通过写技术博客的方式,来记笔记,不仅可以给学到的知识点加深印象,还能锻炼自己的表达能力。


此外,工作中遇到的一些问题,以及解决方案,都可以沉淀到技术博客中。


一方面是为了避免下次犯相同的错误。


另一方面也可以帮助别人少走弯路。


而且,在面试中如果你的简历中写了技术博客地址,是有一定的加分的。


因此建议大家培养些技术博客的习惯。


26.多阅读优秀源码


建议大家利用空闲时间,多阅读JDK、Spring、Mybatis的源码。


通过阅读源码,可以真正的了解某个技术的底层原理是什么,这些开源项目有哪些好的设计思想,有哪些巧妙的编码技巧,使用了哪些优秀的设计模式,可能会出现什么问题等等。


当然阅读源码是一个很枯燥的过程。


有时候我们会发现,有些源码代码量很多,继承关系很复杂,使用了很多设计模式,一眼根本看不明白。


对于这类不太容易读懂的源码,我们不要一口吃一个胖子。


要先找一个切入点,不断深入,由点及面的阅读。


我们可以通过debug的方式阅读源码。


在阅读的过程中,可以通过idea工具,自动生成类的继承关系,辅助我们更好的理解代码逻辑。


我们可以一边读源码,一边画流程图,可以更好的加深印象。

作者:高端章鱼哥
链接:https://juejin.cn/post/7257735219435765820
来源:稀土掘金
收起阅读 »

iOS热修复,看这里就够了(手把手教你玩热修)

背景 对于app store的审核周期不确定性,可长到2星期,短到1天。假如线上的应用出现了一些bug,甚至是致命的崩溃,这时候假如按照苹果的套路乖乖重新发布一个版本,然后静静等待看似漫无期限的审核周期,最终结果就是:用户大量流失。因此,对于一些线上...
继续阅读 »

背景


对于app store的审核周期不确定性,可长到2星期,短到1天。假如线上的应用出现了一些bug,甚至是致命的崩溃,这时候假如按照苹果的套路乖乖重新发布一个版本,然后静静等待看似漫无期限的审核周期,最终结果就是:用户大量流失。因此,对于一些线上的bug,需要有及时修复的能力,这就是所谓的热修复(hotfix)。


随着迭代频繁或者次数的增多,应用出现功能异常或不可用的情况也会随之增多。这时候又有什么方法可以快速解决线上的问题呢?第一、在一开始功能设计的时候就设计降级方案,但随之开发成本和测试成本都会双倍增加;第二、每个功能加上开关配置,这样治标不治本,当开关关掉的时候,就意味着用户无法使用该功能。这时候热修复就是解决这种问题的最佳之选,既能修复问题,又能让用户无感知,两全其美。iOS热修复技术从最初的webView到最近的SOT,技术的发展越来越快,新技术已经到来:


一. 首先是原理篇MangoFix:(知道原理才能更好的干活)


热修复的核心原理:


1.  拦截目标方法调用,让其调用转发到预先埋好的特定方法中

1.  获取目标方法的调用参数


只要完成了上面两步,你就可以随心所欲了。在肆意发挥前,你需要掌握一些 Runtime 的基础理论,

Runtime 可以在运行时去动态的创建类和方法,因此你可以通过字符串反射的方式去动态调用OC方法、动态的替换方法、动态新增方法等等。下面简单介绍下热修复所需要用到的 Runtime 知识点。


OC消息转发机制



由上图消息转发流程图可以看出,系统给了3次机会让我们来拯救。


第一步,在resolveInstanceMethod方法里通过class_addMethod方法来动态添加未实现的方法;


第二步,在forwardingTargetForSelector方法里返回备用的接受者,通过备用接受者里的实现方法来完成调用;


第三步,系统会将方法信息打包进行最终的处理,在methodSignatureForSelector方法里可以对自己实现的方法进行方法签名,通过获取的方法签名来创建转发的NSInvocation对象,然后再到forwardInvocation方法里进行转发。


方法替换就利用第三步的转发进行替换。


当然现在有现成的,初级及以上iOS开发工程师很快就可以理解的语法分析,大概了解一下mangofix是可以转化oc和swift代码的:具体详情请看

http://www.jianshu.com/p/7ae91a2da…


那么为什么它可以执行转化呢,转化逻辑是什么?

MangoFix项目主页上中已经讲到,MangoFix既是一个iOS热修复SDK,但同时也是一门DSL(领域专用语言),即iOS热修复领域专用语言。既然是一门语言,那肯定要有相应的编译器或者解析器。相对于编译器,使用解析器实现语言的执行,虽然效率低了点,但显然更加简单和灵活,所以MangoFix选择了后者。下面我们先用一张简单流程图,看一下MangoFix的运行原理,然后逐一解释。




1、MangoFix脚本


首先热修复之前,我们先要准备好热修复脚本文件,以确定我们的修复目标和执行逻辑,这个热修复脚本文件便是我们这里要介绍的MangoFix脚本,正常是放在我们的服务端,然后由App在启动时或者适当的运行期间进行下载,利用MangoFix提供的MFContext对象进行解析执行。对于MangoFix脚本的语法规则,这点可以参考MangoFix Quick Start,和OC的语法非常类似,你如果有OC开发经验,相信你花10分钟便可以学会。当然,在后续的文章中我可能也会介绍这一块。


2、词法分析器


几乎所有的语言都有词法分析器,主要是将我们的输入文件内容分割成一个个token,MangoFix也不例外,MangoFix词法分析器使用Lex所编写,如果你想了解MangoFix词法分析器的代码,可以点击这里


3、语法分析器


和词法分析器类似,几乎所有语言也都有自己的语法分析器,其主要目的是将词法分析器输出的一个个token构建成一棵抽象语法树,而且这颗抽象语法树是符合我们预先设计好的上下文无关文法规则的,如果你想了解MangoFix语法分析器的代码,可以点击这里


4、语义检查


由于语法分析器输出的抽象语法树,只是符合上下文无关文法规则,没有上下文语义关联,所以MangoFix还会进一步做语义检查。比如我们看下面代码:

less  
复制代码
@interface MyViewController : UIViewController

@end
angelscript  
复制代码
class MyViewController : BaseViewController{

- (void)viewDidLoad{
    //TODO
}

}

上面部分是OC代码,下面部分是MangoFix代码,从文法角度MangoFix这个代码是没有问题的,但是在逻辑上却有问题, MyViewController在原来OC中和MangoFix中继承的父类不一致,这是OC runtime所不允许的。


5、创建内置对象


MangoFix脚本中很多功能都是通过预先创建内置对象的方式支持的,比如常用结构体的声明、变量、宏、C函数和GCD相关的操作等,如果想详细了解MangoFix中有哪些内置对象,可以点击这里。当然MangoFix也开放了相关接口,你也可以向MangoFix执行上下文中注入你需要的对象。


6、执行顶层语句


在做完上面的操作后,MangoFix解析器就开 始真正执行MangoFix脚本了,比如顶层语句的执行、结构体的声明、类的定义等。


7、利用runtime热修复


现在就到了最关键一步了,就是利用runtime替换掉原来Method的IMP指针,MangoFix利用libffi库动态创建C函数,在创建的C函数中调用MangoFix脚本中方法,然后用刚刚创建的C函数替换原来Method的IMP指针,当然MangoFix也会保留原有的IMP指针,只不过这时候该IMP指针对应的selector要在原有的基础上在前面拼接上ORG,这一点和JSPatch一致。当然,MangoFix也支持对属性的添加。


8、MangoFix方法执行


最后当被修复的OC方法在被调用的时候,程序会走到我们动态创建的C函数中,在该函数中我们通过查找一个全局的方法替换表,找到对应的MangoFix方法实现,然后利用MangoFix解析器执行该MangoFix的方法。


二. 具体执行(OC修复OC)。


1.后台分发补丁平台:


补丁平台:patchhub.top/mangofix/lo…


github地址:github.com/yanshuimu/M…

1.首先你要明白:必须得有个后台去上传,分发bug的文件,安全起见,脚本已经通过AES128加密,终端收到加密的脚本再去解密,防止被劫持和篡改,造成代码出现问题。
登录这个补丁平台,可以快速创建appid。
github地址下载并配合使用:
以下是MangoFixUtil的说明:
MangoFixUtil是对MangoFix进行了简单的封装,该库在OC项目中实战已经近2年多,经过多次迭代,比较成熟。但需要搭配补丁管理后台一起使用,后台由作者开发维护,目前有50+个已上架AppStore的应用在使用,欢迎小伙伴们使用。

2.举个实战中的例子:

我们快速迭代中遇到的一些问题:



有一次我们解析到后台数据从中间截取字符串,然而忘了做判空操作,后台数据一旦不给返回,那么项目立马崩溃,所以做了热修复demo.mg文件放到Patch管理平台,具体具体代码如OC基本一致:

class JRMineLoginHeaderView:JRTableViewHeaderView {  

- (NSString *)getNetStringNnm:(NSString *)str{
    NSError *error = nil;
    if(str.length<=0) {
        return @"";
    }
    
    NSRegularExpression *regex = NSRegularExpression.regularExpressionWithPattern:options:error:(@"\d+",0,&error);

    if (error) {
        return @"";
    } else {
    
    if (str.length == 0) {
        return @"";
    }
        
        NSArray *matches = regex.matchesInString:options:range:(str,0,NSMakeRange(0, str.length));
        for (NSTextCheckingResult *match in matches) {
            NSString *matchString = str.substringWithRange:(match.range);
            return matchString;
        }
    }
    return @"";
}

}

以上代码中,新增了对象长度判空操作: if(str.length<=0) {
return @"";
}
完美的解决了崩溃的问题。


2.oc转换成DSL语言。


一切准备就绪,oc转换成DSL语言浪费人力,而且准确率又低怎么办?怎么可以快速的用oc转换成mangofix语言呢?

这是macOS系统上的可视化辅助工具,将OC语言转成mangofix脚本。


做iOS热修复时,大量时间浪费在OC代码翻译成脚本上,提供这个辅助工具,希望能给iOSer提供便利,
本人写了一个mac应用,完美的解决了不同语法障碍,转换问题。

mac版本最低(macos10.11)支持内容:


(1)OC代码 一键 批量转换成脚本


(2)支持复制.m内容粘贴,转换


(3)支持单个OC API转换,自动补全


(4)报错提示:根据行号定位到OC代码行



自动转化文件QQ群获取。



3.打不开“OC2PatchTool.app”,因为它来自身份不明的开发者


方案1.系统偏好设置>>安全与隐私>>允许安装未知来源


方案2.打开 Terminal 终端后 ,在命令提示后输入

sudo spctl --master-disable

OC转换成 脚本 支持两种方式

方式1.拷贝.m文件所有内容,粘贴到OC输入框内。 示例代码:AFHTTPSessionManager.m


方式2. 拷贝某个方法粘贴到OC输入框内,转换时会自动补全



三.App 审核分析


其实能不能成功上线是热修复的首要前提,我们辛辛苦苦开的框架如果上不了线,那一切都是徒劳无功。下面就来分析下其审核风险。


-   首先这个是通过大量C语言混编转换的,所以苹果审核无法通过静态代码识别,这一点是没有问题的。

-   其次系统库内部也大量使用了消息转发机制。这一点可以通过符号断点验证_objc_msgForwardforwardInvocation:。所以不存在风险。此外,你还可以通过一些字符串拼接和base64编码方式进行混淆,这样就更加安全了。

-   除非苹果采用动态检验消息转发,非系统调用都不能使用,但这个成本太大了,几乎不可能。

-   Mangofix 库目前线上有大量使用,为此不用担心。就算 Mangofix 被禁用,参考 Mangofix 自己开发也不难。


综上所述:超低审核风险。


热修复框架只是为了更好的控制线上bug影响范围和给用户更好的体验。

建议:

Hotfix会在应用程序运行时动态地加载代码,因此需要进行充分的测试,以确保修复的bug或添加的新功能不会导致应用程序崩溃或出现其他问题。


有兴趣的一起来研究,QQ群:770600683


作者:洞窝技术
链接:https://juejin.cn/post/7257333598469783610
来源:稀土掘金
收起阅读 »

如何使用 Xcode 15 新组件 TipKit

iOS
TipKit 介绍今年的 WWDC 发布了一个新的 UI 组件库 TipKit,使用 TipKit 可以很方便的在 iOS/macOS/watchOS 等平台的 App 上展示一个提示框,并且内置了 UI 布局,并且支持配置展示频率、规则等功能。今天 Xcod...
继续阅读 »

TipKit 介绍

今年的 WWDC 发布了一个新的 UI 组件库 TipKit,使用 TipKit 可以很方便的在 iOS/macOS/watchOS 等平台的 App 上展示一个提示框,并且内置了 UI 布局,并且支持配置展示频率、规则等功能。

今天 Xcode 15 Beta 5 发布了,TipKit 也终于带了进来,我大概尝试了一下这套 API,和一个月前 WWDC 的视频教程上有些不一样的地方,今天就来讲讲怎么使用。

今天的代码使用 SwiftUI 来演示。

启动配置

想要正常展示 Tip 组件,需要在 App 启动入口加载和配置应用程序中所有 Tip 的统一状态:

import SwiftUI
import TipKit

@main
struct TipKitDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
try? await Tips.configure()
}
}
}
}

这里的 Tips.configure() 函数支持设置一系列用于自定义 Tip 的选项,我这里没有传参数,它会自动帮我配置默认值。

自定义 Tip

首先导入 TipKit 框架:

然后声明一个 struct 继承 Tip:

struct MyTip: Tip {
var title: Text {
Text("Tip Title")
}
}

Tip 是一个协议,title 是必须实现的,其他值都可选。

展示 Tip

Tip 有两种展示方式,popover 和 Inline,popover 需要在指定的元素上使用 popoverTip 方法挂载这个 Tip,Tip 展示出来后会有个箭头指向这个元素,比如我在收藏按钮下展示这个 Tip:

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "star")
.imageScale(.large)
.foregroundStyle(.tint)
.popoverTip(MyTip(), arrowEdge: Edge.top) { action in
print(action)
}
}
.padding()
}
}

看下效果:


Inline 的方式是作为一个独立的 View 展示在视图上的,需要用到 TipView 组件:


Tip 的 UI 配置

刚刚提到 Tip 是一个协议,可以配置一些其他 UI,比如,在标题下方增加一行描述 (下边的效果截图均以 popover 的方式展示):

struct MyTip: Tip {
var title: Text {
Text("Save as a Favorite")
}

var message: Text? {
Text("Your favorite backyards always appear at the top of the list.")
}
}


添加图标:

struct MyTip: Tip {
// 其他代码...
var asset: Image? {
Image(systemName: "star")
}
}


添加按钮

struct MyTip: Tip {
// 其他代码...
var actions: [Action] {
[
Action(id: "1", title: "Learn More", perform: {
print("点击了第一个按钮")
}),
Action(id: "2", title: "OK", perform: {
print("点击了第二个按钮")
})
]
}
}


配置规则

除此之外,还可以配置一系列显示的规则,比如我定义一个 Bool 来控制这个 Tip 的展示与隐藏:

struct MyTip: Tip {
@Parameter
static var isShowing: Bool = false

// ...其他代码...

var rules: [Rule] {
[
#Rule(MyTip.$isShowing) { $0 == true }
]
}
}

然后我们再稍微改一下 ContentView 的代码,每次点击按钮的时候反转 isShowing 这个参数,来控制 Tip 的出现和消失:

struct ContentView: View {
var body: some View {
VStack {
Button(action: {
// 控制隐藏和出现
MyTip.isShowing.toggle()
}, label: {
Image(systemName: "star.fill")
})
.popoverTip(MyTip(), arrowEdge: Edge.top) { action in
print(action)
}
}
.padding()
}
}

这样我们就可以通过点击按钮来展示和隐藏这个提示框了:



这里需要注意,目前 Xcode Beta 5 有个已知的问题是不能正常访问 @Parameter 这个宏,解决办法是在项目的 Build Settings 的 Other Swift Flags 中手动添加 -external-plugin-path (SYSTEM\_DEVELOPER\_DIR)/Platforms/iPhoneOS.platform/Developer/usr/lib/swift/host/plugins#(SYSTEM_DEVELOPER_DIR)/Platforms/iPhoneOS.platform/Developer/usr/bin/swift-plugin-server,否则无法编译通过


配置显示选项

通过 TipOption 可以配置一些额外的展示选项,比如我这里配置这个 Tip 最大显示 5 次:

struct MyTip: Tip {

// ...其他代码...

var options: [TipOption] {
[ Tips.MaxDisplayCount(5) ]
}
}

更多的配置大家可以自行探索。


作者:杂雾无尘
链接:https://juejin.cn/post/7262162940971139109
来源:稀土掘金

收起阅读 »