注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

近几年很火的「浏览器指纹」是怎么回事?

前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 不知道大家在浏览一些网站时,有没有注意到这么一件事情,就是你在某一个页面浏览了一些你喜欢的东西,但是你并没有登录,等你换一个标签页打开这个网站的时候,他照...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


不知道大家在浏览一些网站时,有没有注意到这么一件事情,就是你在某一个页面浏览了一些你喜欢的东西,但是你并没有登录,等你换一个标签页打开这个网站的时候,他照样能推送一些你比较感兴趣的内容供你阅读


就比如一些新闻网站、资讯网站、购物网站。我们并没有登录,他是怎么知道我们的喜好的呢?或者说他们是怎么记得我们的呢?



什么?浏览器也有指纹?


这里的指纹不是指的手机上的那种指纹解锁,你可以认为:浏览器指纹就是浏览器的标记


有了这个标记之后,每次请求接口的时候,浏览器都会带着这个标记去发送请求,这样后端那边就会缓存起来你这个标记,并且等下次遇到你个标记的时候,就给你推送对应的你感兴趣的内容



其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:



  • 新闻、资讯网站: 要精确推送一些你感兴趣的内容供你阅读

  • 购物网站: 要精确推送一些你近期浏览量比较多的商品展示给你看

  • 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?

  • 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为


浏览器指纹怎么算出来的呢?


刚刚说了,浏览器指纹就是浏览器的标记


你可以理解就是一段标识字符串,比如这样:



指纹算法


其实每个网站都有自己的一套计算浏览器指纹的算法,每个网站可能都不一样


但是其实市面上已经提供了很多浏览器指纹计算的算法了,大家可以到这个网站:browserleaks.com/,这个网站上展示了一些…



就比如使用 canvas 去计算浏览器指纹,通过介绍可以粗略知道,这是一种使用 canvas 画布去进行计算的指纹算法



我们可以点进去看看,在这里我们可以清楚看到目前我们这个浏览器的指纹长什么样



我们甚至可以看看这个算法,到底是什么原理,看介绍,大概就是分为几步:



  • 用 canvas 画出一个图像

  • 不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的

  • 接着把 canvas图像 转成字符串,这样就得到了一个趋近唯一的浏览器指纹



为了防止可能是浏览器缓存影响到浏览器指纹的计算,我们可以打开一个无痕浏览器,发现浏览器指纹是一致的,那就说明这个计算的算法跟浏览器缓存是无关的~



真的唯一吗?


其实浏览器指纹只能是趋近于唯一,毕竟他是通过你的电脑信息计算出来的一个标识,在你没登录的情况下,这已经是一个比较稳妥的计算方式了~


可以看到,canvas 算法也只能做到99.99%的唯一性,所以只能是趋近唯一,所以你有没有发现,很多网站或者APP都不断在某些时机提醒用户进行登录,那是为了能更精准地投用户之所好,提高用户的黏度~





作者:Sunshine_Lin
来源:juejin.cn/post/7364409181053763610
收起阅读 »

在工作中,大家会重新优化自己写过的代码嘛?

前言 今天在忙完工作的时候,发现还有很多时间,于是......我利用这些时间来优化自己之前写的代码。 项目:Vue2 + Element ui 😺 为什么要优化? 因为看见一个页面代码篇幅太“长”(950行+),这里的950行,并不是说所有900行的单页面...
继续阅读 »

前言



  • 今天在忙完工作的时候,发现还有很多时间,于是......我利用这些时间来优化自己之前写的代码。

  • 项目:Vue2 + Element ui


😺 为什么要优化?


因为看见一个页面代码篇幅太“长”(950行+),这里的950行,并不是说所有900行的单页面就是长代码了,我前面提到的这个 “长”针对我这个页面的功能的。


我认为我这个页面的功能,可以把这部分代码写得更少,封装得更好,可读性也能继续提高,所以选择重构。虽然这个页面功能较多这个无可厚非,但是这个不是 长篇幅 的理由哈哈哈😂。


image.png


虽然里面已经有封装了组件,但是不够完善,data属性过多methods方法多拆分不完善不全面,后期想快速 定位问题 可能比较 困难,所以选择 重构


😺 重构的前提


我们 不要代码行数 来作为 代码好坏的标准。我认为这是较为狭义的,不严谨的一种说法。
Vue单页面因为 templatescriptstyle 都在一个文件里,很容易写出行数较长代码,尤其是样式,如果说需要响应式,行数根本 hold 不住,所以一般我们会把 <style> 放到文件的底部。


而影响 Vue单页面 可读性可维护性 的,主要在脚本那部分。这部分写得烂的话,不需要堆成山就足够让人崩溃了,这也和行数无关。


对于独立且比较重的业务 来讲,写在 一个文件 里也是 没太大问题 的,那些把代码拆解到300~500行一个文件的同学,有一部分是为了拆而拆。不复用的话,我们拆了可能意义不大,所以我们要具体分析是否有拆分的意义所在。


😺 拆分优化


第一:分析页面结构,拆分出可独立维护的子模块


我们来看看掘金的首页,我们可以大致分成这几块,具体内容在具体分析(这里简单给大家演示一下 页面结构 基础拆分,详细的小伙伴可以自己继续深入研究😀)


image.png


第二:明确子模块对应的代码,确定可以拆分的子组件


其中像我今天优化的这个页面中存在比较多的Tabs 标签页每个tab 里面又包含了基础的表单查询+表格+分页,很明显这部分如果写在同一个页面将产生很多基础组件方法(函数)等,这部分可以优先拆分。还有就是弹窗,抽屉 一类的也可进行拆分,即便在同一个 Vue 文件中编写,这类组件也是比较独立的部分,拆分起来相对容易。


第三:针对子组件负责的渲染及业务逻辑,明确其所需的属性及事件


细分每个子组件负责的事情,比如还是用我们掘金的首页做分析,我们要把 header 部分拆分出来,首先需要明细 header 需要渲染的内容,像 logo ,导航菜单啊,创作者中心,用户头像等等;其次确定哪些是 header 内部维护的数据,哪些需要父组件传入;另外确定暴露的事件(分发事件),比如搜索,传递出去的参数,要告诉父组件触发了搜索事件,父组件接收到才会去更新内容部分。


🐱 写到最后



  • 今天的分享就到此为止啦!😉

  • 如果大家还有不懂的地方可以在评论区留言或者大家一起讨论哦!

  • 如果这篇文章有帮到您的的话不妨 关注 点赞 收藏 评论 转发支持一下,大家的支持就是我更新最大的动力啦~😆

  • 如果想跟我一起讨论和学习的话,可以私信留言,或者评论区留言,拉你进我的前端学习群哦!

  • 感谢大家支持🤩


作者:up_up_up
来源:juejin.cn/post/7371312966364184586
收起阅读 »

好烦啊,我真的不想写增删改查了!

大家好,我是程序员鱼皮。 很想吐槽:我真的不想写增删改查这种重复代码了! 大学刚做项目的时候,就在写增删改查,万万没想到 7 年后,还在和增删改查打交道。因为增删改查是任何项目的基础功能,每次带朋友们做新项目时,为了照顾更多同学,我都会带大家把增删改查的代码再...
继续阅读 »

大家好,我是程序员鱼皮。


很想吐槽:我真的不想写增删改查这种重复代码了!


大学刚做项目的时候,就在写增删改查,万万没想到 7 年后,还在和增删改查打交道。因为增删改查是任何项目的基础功能,每次带朋友们做新项目时,为了照顾更多同学,我都会带大家把增删改查的代码再编写并讲解一遍。


不开玩笑地说,我绝对有资格在简历上写 “自己精通增删改查的编写” 了!



相信很多已经在工作中的小伙伴,80% 甚至更多的时间也在天天写增删改查这种重复代码,也会因此感到烦恼。那大家有没有思考过:如何提高写增删改查的效率?让自己有更多时间进步(愉快摸鱼)呢?


其实有很多种方法,鱼皮分享下自己的提效小操作,看看朋友们有没有实践过~


如何提高增删改查的编写效率?


方法 1、复制粘贴


复制粘贴前人的代码是一种最简单直接的方法,估计大多数开发者在实际工作中都是这么干的。



但这种方式存在的问题也很明显,如果对复制的代码本身不够理解,很有可能出现细节错误。而且不同数据表的字段和校验规则是不同的,往往复制后的代码还要经过大量的人工修改。


还有很多 “小迷糊”,经常复制完代码后忘了修改一些变量名称和注释,出现类似下面代码的名场面:


// 帖子接口
class UserController {
}

方法 2、使用模板


一般新项目都是要基于模板开发的,而不是每次都重复编写一大堆的通用代码。比如我之前给编程导航同学编写的 Spring Boot 后端万用模板,内置了用户注册、账号密码登录、公众号登录等通用能力。基于这种模板二次开发,能够大大提高开发效率,也有助于开发同学遵循一致的规范。


模板支持的功能


然而,使用模板也存在一些风险,如果模板本身有功能存在漏洞,那么所有基于这个模板开发的项目可能都会存在风险。而且别人的模板也不是万能的,建议还是根据自己的开发经验,自己沉淀和维护一套模板。对团队来说,沉淀模板是必须要做的事。


方法 3、AI 工具


利用 AI 工具来生成增删改查的代码是一种新兴的方法。只需要甩给 AI 要生成代码的表结构,然后精准地编写要生成的代码要求,就可以让 AI 快速生成了。



这种方式的优点是非常灵活,能帮开发者提供一些灵感;缺点就是对编写 prompt(提示词)的要求会比较高,而且生成后的代码还是得仔细检查一遍的。


方法 4、超级抽象


这是一种更高级别的代码复用方法。通过设计 通用的 数据模型和操作接口,实现用一套代码满足多种不同业务场景下的增删改查操作。


举个例子,如果有帖子、评论、回答等多个资源需要支持用户收藏功能,系统规模不大的情况下,不需要编写 3 张不同的收藏表、并分别编写增删改查代码。而是可以设计 1 张通用的收藏表,通过 type 字段来区分不同类型的资源,从而实现统一的收藏操作。


像点赞、消息通知、日志、数据收集等业务场景,都可以采用这种方式,通过极致的复用完成快速开发。


但也要注意,千万不要把区别较大的功能强行合并到一起,反而会增加开发者的理解成本;而且如果系统数据量较大,分开维护表更有利于系统的性能和稳定性。


方法 5、代码生成器


这也是非常典型的一种提高增删改查效率的方法。后端可以使用 MyBatis X 插件来生成数据模型和数据访问层的 Mapper 代码,前端可以用 OpenAPI 工具生成请求函数和 TS 类型代码等。


不过用别人的生成器难免会出现无法满足需求的情况,生成后的代码一般还是要自己再修改一下的。


所以,我建议可以使用模板引擎技术,自己开发一套更灵活、更适合自己业务的代码生成器。


比如鱼皮给后端万用模板补充了代码生成器功能,使用 FreeMarker 模板引擎技术实现,定制了 Controller、Service、数据包装类的代码模板。用户只需要指定几个参数,就可以在指定位置生成代码了~ 昨天 AI 答题应用平台的开发中,就是用了这个代码生成器,几分钟写好一套功能。



可以在代码小抄阅读生成器的核心实现代码:http://www.codecopy.cn/post/edkpo4 。之前我从 0 到 1 直播带大家开发过一个代码生成器共享平台,感兴趣的同学也可以学习下,保证能把代码生成玩得很熟练~


方法 6、云服务


这种方式也比较新颖了,利用某些云服务提供的现成的数据库和操作数据库的接口,都不需要自己去编写增删改查了!


比如我之前用过的腾讯云开发 Cloudbase,开通服务后,只要在平台上建号数据表,就能自动得到数据管理页面,可以直接通过 HTTP 请求或 SDK 实现增删改查,尤其适合前端同学使用。


但这种方式的缺点也很明显,灵活性相对差了一些,而且会产生一些额外的费用。


所以还是那句话,没有最好的技术,只有最适合自身需求和业务场景的技术。




作者:程序员鱼皮
来源:juejin.cn/post/7369094945154711578
收起阅读 »

一个失败的独立开发的300多天的苦果

历史是成功者书写的,所以我们能看到的成功的独立开发者,正所谓一将功成万骨枯,其实失败的才是大多数。从2023年7月14到现在2024年5月22,10个多月,一个313天总共的收入只有652元(😭😭😭) appStore的收入($72.14=¥522) 微软商...
继续阅读 »

历史是成功者书写的,所以我们能看到的成功的独立开发者,正所谓一将功成万骨枯,其实失败的才是大多数。从2023年7月14到现在2024年5月22,10个多月,一个313天总共的收入只有652元(😭😭😭)


appStore的收入($72.14=¥522)


image.png


微软商店的收入($17.97=¥130)


image.png


总结一下失败原因



  1. 做了一堆垃圾,没有聚焦的做好一款产品

  2. 没有扬长避短,其实前端开发最适合的产品方向应该是web和微信小程序,在electron上架appStore上花费了大量的时间(15天真实的时间)

  3. 归根结底还是在做产品这方面的储备不够,做产品没有定力,心静不下来,如果其他的都不做把全部的精力都拿来做aweb浏览器(包括研发和宣传),结果也不至于这么差。


分享一下失败的经验吧



  1. 全职独立开发初期很难沉下来打磨产品,还是建议边工作边搞,沉不下来就会原来越乱

  2. 如果感觉效率低,还是不要在家里办公了,咖啡馆、图书管、公创空间(武汉这边500一个公位)都是不错的选择

  3. 有单还是接吧,不然真的是太难了

作者:九段刀客
来源:juejin.cn/post/7371638121279848499
收起阅读 »

😰我被恐吓了,对方扬言要压测我的网站

大家好我是聪,昨天真是水逆,在技术群里交流问题,竟然被人身攻击了!骂的话太难听具体就不加讨论了,人身攻击我可以接受,我接受不了他竟然说要刷我接口!!!!这下激发我的灵感来写一篇如何抵御黑子的压测攻击,还真得要谢谢他。 🔥本次的自动加入黑名单拦截代码已经上传到...
继续阅读 »

大家好我是聪,昨天真是水逆,在技术群里交流问题,竟然被人身攻击了!骂的话太难听具体就不加讨论了,人身攻击我可以接受,我接受不了他竟然说要刷我接口!!!!这下激发我的灵感来写一篇如何抵御黑子的压测攻击,还真得要谢谢他。


image-20240523081706355.png


🔥本次的自动加入黑名单拦截代码已经上传到短链狗,想学习如何生成一个短链可以去我的 Github 上面查看哦,项目地址:github.com/lhccong/sho…


思维发散


如果有人要攻击我的网站,我应该从哪些方面开始预防呢,我想到了以下几点,如何还有其他的思路欢迎大家补充:



  1. 从前端开始预防!


    聪 A🧑:确实是一种办法,给前端 ➕ 验证码、短信验证,或者加上谷歌认证(用户说:我谢谢你哈,消防栓)。


    聪 B🧑:再次思考下还是算了,这次不想动我的前端加上如何短信验证还消耗我的💴,本来就是一个练手项目,打住❌。


  2. 人工干预!


    聪 A🧑:哇!人工干预很累的欸,拜托。


    聪 B🧑:那如果是定时人工检查进行干预处理,辅助其他检测手段呢,是不是感觉还行!


  3. 使用网关给他预防!


    聪 A🧑:网关!好像听起来不错。


    聪 B🧑:不行!我项目都没有网关,单单为了黑子增加一个网关,否决❌。


  4. 日志监控!


    聪 A🧑:日志监控好像还不错欸,可以让系统日志的输出到时候统一监控,然后发短信告诉我们。


    聪 B🧑:日志监控确实可以,发短信还是算了,拒绝一切花销哈❌。


  5. 我想到了!后端 AOP 拦截访问限流,通过自动检测将 IP + 用户ID 加入黑名单,让黑子无所遁形。


    聪 A🧑:我觉得可以我们来试试?


    聪 B🧑:还等什么!来试试吧!



功能实现


设置 AOP 注解


1)获取拦截对象的标识,这个标识可以是用户 ID 或者是其他。


2)限制频率。举个例子:如果每秒超过 10 次就直接给他禁止访问 1 分钟或者 5 分钟。


3)加入黑名单。举个例子:当他多次触发禁止访问机制,就证明他还不死心还在刷,直接给他加入黑名单,可以是永久黑名单或者 1 天就又给他放出来。


4)获取后面回调的方法,会用反射来实现接口的调用。


有了以上几点属性,那么注解设置如下:


/**
* 黑名单拦截器
*
*
@author cong
*
@date 2024/05/23
*/

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface BlacklistInterceptor {

   /**
    * 拦截字段的标识符
    *
    *
@return {@link String }
    */

   String key() default "default";;

   /**
    * 频率限制 每秒请求次数
    *
    *
@return double
    */

   double rageLimit() default 10;

   /**
    * 保护限制 命中频率次数后触发保护,默认触发限制就保护进入黑名单
    *
    *
@return int
    */

   int protectLimit() default 1;

   /**
    * 回调方法
    *
    *
@return {@link String }
    */

   String fallbackMethod();
}

设置切面具体实现


@Aspect
@Component
@Slf4j
public class RageLimitInterceptor {
   private final Redisson redisson;

   private RMapCache blacklist;
   // 用来存储用户ID与对应的RateLimiter对象
   private final Cache userRateLimiters = CacheBuilder.newBuilder()
          .expireAfterWrite(1, TimeUnit.MINUTES)
          .build();

   public RageLimitInterceptor(Redisson redisson) {
       this.redisson = redisson;
       if (redisson != null) {
           log.info("Redisson object is not null, using Redisson...");
           // 使用 Redisson 对象执行相关操作
           // 个人限频黑名单24h
           blacklist = redisson.getMapCache("blacklist");
           blacklist.expire(24, TimeUnit.HOURS);// 设置过期时间
      } else {
           log.error("Redisson object is null!");
      }
  }


   @Pointcut("@annotation(com.cong.shortlink.annotation.BlacklistInterceptor)")
   public void aopPoint() {
  }

   @Around("aopPoint() && @annotation(blacklistInterceptor)")
   public Object doRouter(ProceedingJoinPoint jp, BlacklistInterceptor blacklistInterceptor) throws Throwable {
       String key = blacklistInterceptor.key();

       // 获取请求路径
       RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
       HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
       //获取 IP
       String remoteHost = httpServletRequest.getRemoteHost();
       if (StringUtils.isBlank(key)) {
           throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "拦截的 key 不能为空");
      }
       // 获取拦截字段
       String keyAttr;
       if (key.equals("default")) {
           keyAttr = "SystemUid" + StpUtil.getLoginId().toString();
      } else {
           keyAttr = getAttrValue(key, jp.getArgs());
      }

       log.info("aop attr {}", keyAttr);

       // 黑名单拦截
       if (blacklistInterceptor.protectLimit() != 0 && null != blacklist.getOrDefault(keyAttr, null) && (blacklist.getOrDefault(keyAttr, 0L) > blacklistInterceptor.protectLimit()
               ||blacklist.getOrDefault(remoteHost, 0L) > blacklistInterceptor.protectLimit())) {
           log.info("有小黑子被我抓住了!给他 24 小时封禁套餐吧:{}", keyAttr);
           return fallbackMethodResult(jp, blacklistInterceptor.fallbackMethod());
      }

       // 获取限流
       RRateLimiter rateLimiter;
       if (!userRateLimiters.asMap().containsKey(keyAttr)) {
           rateLimiter = redisson.getRateLimiter(keyAttr);
           // 设置RateLimiter的速率,每秒发放10个令牌
           rateLimiter.trySetRate(RateType.OVERALL, blacklistInterceptor.rageLimit(), 1, RateIntervalUnit.SECONDS);
           userRateLimiters.put(keyAttr, rateLimiter);
      } else {
           rateLimiter = userRateLimiters.getIfPresent(keyAttr);
      }

       // 限流拦截
       if (rateLimiter != null && !rateLimiter.tryAcquire()) {
           if (blacklistInterceptor.protectLimit() != 0) {
               //封标识
               blacklist.put(keyAttr, blacklist.getOrDefault(keyAttr, 0L) + 1L);
               //封 IP
               blacklist.put(remoteHost, blacklist.getOrDefault(remoteHost, 0L) + 1L);
          }
           log.info("你刷这么快干嘛黑子:{}", keyAttr);
           return fallbackMethodResult(jp, blacklistInterceptor.fallbackMethod());
      }

       // 返回结果
       return jp.proceed();
  }

   private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
       Signature sig = jp.getSignature();
       MethodSignature methodSignature = (MethodSignature) sig;
       Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
       return method.invoke(jp.getThis(), jp.getArgs());
  }

   /**
    * 实际根据自身业务调整,主要是为了获取通过某个值做拦截
    */

   public String getAttrValue(String attr, Object[] args) {
       if (args[0] instanceof String) {
           return args[0].toString();
      }
       String filedValue = null;
       for (Object arg : args) {
           try {
               if (StringUtils.isNotBlank(filedValue)) {
                   break;
              }
               filedValue = String.valueOf(this.getValueByName(arg, attr));
          } catch (Exception e) {
               log.error("获取路由属性值失败 attr:{}", attr, e);
          }
      }
       return filedValue;
  }

   /**
    * 获取对象的特定属性值
    *
    *
@param item 对象
    *
@param name 属性名
    *
@return 属性值
    *
@author tang
    */

   private Object getValueByName(Object item, String name) {
       try {
           Field field = getFieldByName(item, name);
           if (field == null) {
               return null;
          }
           field.setAccessible(true);
           Object o = field.get(item);
           field.setAccessible(false);
           return o;
      } catch (IllegalAccessException e) {
           return null;
      }
  }

   /**
    * 根据名称获取方法,该方法同时兼顾继承类获取父类的属性
    *
    *
@param item 对象
    *
@param name 属性名
    *
@return 该属性对应方法
    *
@author tang
    */

   private Field getFieldByName(Object item, String name) {
       try {
           Field field;
           try {
               field = item.getClass().getDeclaredField(name);
          } catch (NoSuchFieldException e) {
               field = item.getClass().getSuperclass().getDeclaredField(name);
          }
           return field;
      } catch (NoSuchFieldException e) {
           return null;
      }
  }


}

这段代码主要实现了几个方面:



  • 获取限流对象的唯一标识。如用户 Id 或者其他。

  • 将标识来获取是否触发限流 + 黑名单 如果是这两种的一种,直接触发预先设置的回调(入参要跟原本接口一致喔)。

  • 通过反射来获取回调的属性以及方法名称,触发方法调用。

  • 封禁 标识 、IP 。


代码测试


@BlacklistInterceptor(key = "title", fallbackMethod = "loginErr", rageLimit = 1L, protectLimit = 10)
   @PostMapping("/login")
   public String login(@RequestBody UrlRelateAddRequest urlRelateAddRequest) {
       log.info("模拟登录 title:{}", urlRelateAddRequest.getTitle());
       return "模拟登录:登录成功 " + urlRelateAddRequest.getTitle();
  }

   public String loginErr(UrlRelateAddRequest urlRelateAddRequest) {
       return "小黑子!你没有权限访问该接口!";
  }


  • key:需要拦截的标识,用来判断请求对象。

  • fallbackMethod:回调的方法名称(这里需要注意的是入参要跟原本接口保持一致)。

  • rageLimit:每秒限制的访问次数。

  • protectLimit:超过每秒访问次数+1,当请求超过 protectLimit 值时,进入黑名单封禁 24 小时。


以下是具体操作截图:


Snipaste_2024-05-23_11-28-41.png


到这里这个黑名单的拦截基本就实现啦,大家还有什么具体的补充点都可以提出来,一起学习一下,经过这次”恐吓风波“,让我知道互联网上的人戾气还是很重的,只要坚持好做自己,管他别人什么看法!!


作者:cong_
来源:juejin.cn/post/7371761447696121866
收起阅读 »

环信 X 星野| 共创沉浸式 AI 互动体验

大模型技术的发展使虚拟人更加智能和情感丰富,推动人与 AI 智能体互动体验进入新时代。星野App 是一款沉浸式 AI 内容社区,短短几个月日活过百万。虽然市面上的社交产品很多,但社交关系更多的是停留在表面,无法满足深层次情感交流需求。星野通过 AI 扮演驱动社...
继续阅读 »

大模型技术的发展使虚拟人更加智能和情感丰富,推动人与 AI 智能体互动体验进入新时代。星野App 是一款沉浸式 AI 内容社区,短短几个月日活过百万。虽然市面上的社交产品很多,但社交关系更多的是停留在表面,无法满足深层次情感交流需求。星野通过 AI 扮演驱动社交方式实现深层情感交流,短短几个月迅速占领社交市场。这其中环信IM通过提供高效、稳定、安全的即时通讯服务,助力星野提高用户粘性和转化率,推动人与AI智能互动体验共同发展。

在这里插入图片描述

当前社交产品的局限性

当前社交产品和用户需求之间存在显著的不匹配,尤其在深层次社交关系和真实情感交流方面。社交媒体如 Facebook 和微信,虽然用户众多,但这些平台上的关系多停留在表面,缺乏深度和真实性。很多“好友”实际上是完全不认识的人,导致人际关系的虚拟化和表层化,无法有效缓解孤独感。社交产品开发者需要更多关注如何促进真实的人际互动和情感支持,提供更人性化的产品设计,满足用户的深层次情感需求。

Z世代的影响力

Z 世代对科技和数字产品有天然亲和力和高度依赖,利用社交媒体表达观点,影响更多的网络群体。Z 世代重视品牌社会责任和道德标准,倾向支持反映其价值观的品牌,推动企业在营销和公关策略上的转变。他们对个性化需求极高,寻求能表达个人身份的定制化选项,这种需求体现在消费品和社交互动上。随着 Z 世代成为经济和文化主导力量,市场将继续向这些年轻消费者靠拢,开发符合他们需求的产品和服务。企业必须理解和适应 Z 世代的消费心理和行为模式,才能在市场竞争中占据优势。

解决情绪需求的商业潜力

解决情绪价值预示着一个潜力巨大的市场。在数字化和人工智能技术迅速发展的背景下,深层次情感交流和个性化关怀成为技术应用新前沿。AI 技术在情感识别和情感交互方面的突破,已见证初步商业化应用。AI 驱动的虚拟助手和聊天机器人在用户中受欢迎,提供陪伴和支持,在单身人群、老年人和需要情感支持的用户中尤其受欢迎。情感驱动的电商和内容平台改变传统购物和内容消费模式,通过情感分析优化的推荐系统,根据用户心情和偏好推送产品和内容,提高转化率和用户粘性。未来,将看到更多利用 AI 实现深层情感交流的创新应用,在教育、健康护理、客户服务等领域发挥重要作用。

星野App Z世代的一味社交解药

在星野App中,用户与基于 AIGC 技术创造的“智能体”之间可以实现实时沟通、互动并建立情感连接。目前,除了1对1虚拟社交,星野也在着力打造多 NPC 模式的虚拟社交,给与玩家更丰富、有趣的互动体验。

在这里插入图片描述

星野搭载的AIGC技术,可以让用户根据自己的喜好来创建智能体的人设、形象和声音,智能体的性格特质只需要通过一段简短的描述实现,并能在后续对话中不断调整强化,调教成用户所希望的性格和样子。用户可以与心目中幻想的、虚构的、想却无法触及的对象进行对话,演绎或是表达情感。在多种多样的虚拟社交场景,满足不同用户的社交需求。真正的实现千人千面,打造比肩真实世界的互动体验。

图片

人与智能体对话方案,打造千人千面专属对话

在星野App中,终端用户和智能体可以通过文字、语音、视频等多种载体来对话沟通,这种沉浸式的虚拟互动体验,背后是基于环信的 Chat AI 技术方案来实现的。通过环信提供的高效、稳定、安全的即时通讯服务,终端用户可以获得更加自然、便捷和高效的人机交互模式,从而满足用户的需求。同时,即时通讯服务还可以为应用提供数据传输、消息推送等基础服务,实现应用功能的完整性和可扩展性。

在环信方案中,通过建立一一对应的环信ID关系,形成相应的单聊会话或多人沟通场景。其中,为了进一步提升用户体验,⻓链接通道是非常重要的一个措施,可以有效地缓解移动端弱网环境下的网络延迟和不稳定性问题。

人与智能体对话方案

用户与AI智能体互动的场景非常依赖回调功能。环信丰富的消息回调和 RESTful API 相关能力,可以将星野App AI返回给环信的消息发送给终端用户,从而实现用户消息即时送达。在这一过程中,稍有延迟直接会在前端产生不好体验,这就要求环信提供更加稳定和高效的回调服务,来满足数据存储和业务运营的需求。

同时,环信即时通讯云还提供内容合规审核功能,能够对消息进行过滤,避免出现涉⻩、涉政等违规内容。该功能基于先进的算法和AI技术,在保证高效性和准确性的同时,能够精准判断和拦截不良信息,保障用户的信息安全和隐私。

智能体离线唤醒方案,有力提升用户粘性

对星野App来说,提升用户留存率和老用户激活是非常重要的运营指标。在这方面,环信的智能体离线唤醒方案提供了强有力的支持。通过该方案,即使终端用户离线或应用被关闭,也可以确保他们能够及时地收到重要的消息和提醒,从而增加使用频次和粘性,并提高用户满意度。

图片
智能体离线唤醒方案

同时,离线推送还可以帮助应用开发者实现更加智能、个性化的推送策略,根据用户行为和偏好进行定制化推送,提高推送效果和转化率。另外,环信即时通讯云的离线推送功能还具备灵活、可扩展的特点,支持多种推送方式和协议,以适应不同的业务需求和技术要求。

携手共创,打造极致即时沟通体验

在人工智能社交领域,环信IM作为领先的PaaS供应商,为星野APP的开发提供了关键支持。凭借卓越的技术实力和快速响应能力,环信IM与星野团队紧密合作,提供高效的即时通讯解决方案,大幅提升了用户体验,并加速了星野APP的市场推广。这种合作模式不仅提高了星野的运营效率,也增强了其市场竞争力。环信IM的技术专业性和创新能力在与星野APP的合作中得到了充分展现,成功打造了极致的沟通体验,赢得了用户的广泛认可。

丰富消息和会话能力:场景搭建的基石

在星野APP中,人与智能体的沟通是核心应用场景。环信IM提供了文本、音频、图片、视频等多种消息类型,极大丰富了人机沟通体验;消息编辑、删除功能满足了业务对生成内容处理的需求;离线消息和推送消息保障了移动应用场景下的用户体验;会话管理能力为人与多个智能体的沟通提供了场所和管理支持。

用户关系系统:社交关系裂变

作为社交应用,星野APP包含大量的社交关系:人与智能体的关系,人与频繁交流的智能体的关系,以及一组人与一组智能体之间的关系。环信IM的好友管理和群组管理功能,为星野APP提供了坚实的关系管理基础,增强了用户粘性。

全面的监控体系:为业务增长保驾护航

作为备受关注的AI社交应用,星野面临着业务超高速增长的挑战。环信IM的水晶球服务提供了全面的用量和质量监控告警服务,确保在用户规模快速增长的情况下,系统依然能平稳运行,为业务成功保驾护航。

高保障的回调服务:服务端消息事件同步

环信IM为星野APP提供了低延时高保障的回调服务,确保每天海量消息和事件的同步。在业务高峰时段,回调服务提供了强有力的支持;在应用服务器因异常无法接受回调时,环信IM提供高保障的回调内容存储和补发服务,确保数据的可靠性。

高质量的全球网络:丝滑的全球用户体验

面对全球复杂的网络环境,环信IM通过全球多地数据中心、公网直连、AWS-GA、SD-RTN三路智能网络路由切换,部署了近千个边缘接入节点,保障了终端用户在网络基础设施较差区域的低延时登录和消息收发,提供了极佳的用户体验。

消息流式输出:支持更多AI应用场景

AI服务正逐渐成为终端用户接入网络的主要界面,信息传输需求日益增加。环信IM结合大模型内容生成的特点,推出了消息流式传输方案,为星野APP等AI创新应用提供了多样化的解决方案,支持更多AI应用场景。

环信 IM 结合 AI 技术方案优势

海量并发

支持单日数十亿级别的消息传 输和处理,能够满足不同场景和业务需求,确保系统稳定。

精简流程海量并发

提供易用、可靠、高效的平台和会话API,让开发者专注于业务逻辑实现,减少实现难度和成本。

专线回调

提供安全、稳定、快速的回调,回调容灾机制,确保消息不丢和用户体验,带来更好交互效果。

低延时
全球平均时延小于100ms,使得用户交互过程流畅自然,并提升应用竞争力和用户满意度 。

高可用性
提供多备份、灾备恢复等技术手段,SLA 99.99%,确保系统24小时不停机、持续稳定运行。

监控保障

提供实时监测、故障排除等技术支持,确保数据安全和服务稳定性,维护系统的可靠运行。

相关文档:

收起阅读 »

程序员兼职那些事儿

最近周边发生一起程序员兼职引起的纠纷事件,作为一名资深程序员的我也做过兼职,所以不禁思考作为程序员做兼职时的一些套路,以及应该遵循的原则。 1、兼职引起的纠纷 最近笔者发现周边有些程序员常年利用上班时间做兼职工作,还拉拢一些在职同事一起参与,而且做兼职的过程...
继续阅读 »

最近周边发生一起程序员兼职引起的纠纷事件,作为一名资深程序员的我也做过兼职,所以不禁思考作为程序员做兼职时的一些套路,以及应该遵循的原则。



1、兼职引起的纠纷


最近笔者发现周边有些程序员常年利用上班时间做兼职工作,还拉拢一些在职同事一起参与,而且做兼职的过程中无意间泄露了所在公司的软件代码。后来被给所在公司发现,所在公司为了维护利益,进行了报警处理,经过一些争执后,最终双方和解,好聚好散。


2、个人很认可兼职


笔者个人是非常支持程序员朋友做兼职的,而且要尽早开启兼职事业,毕竟大部分程序员都是很普通的家庭出生,唯有通过自己更多的劳作才能创造更多的收入。(之前我经常开玩笑说,谁有钱还去做程序员啊!)


程序员做兼职还可以提升自己的技能,结实更多的朋友,开拓更多的可能性。同时这也是轻创业的一种方式,一个人一台电脑就可以开始自己的事业,说句扎心的话,确实比较适合没钱没背景的程序员朋友。


3、程序员兼职的种类



程序员兼职的种类较多,每个人根据自身情况决定。总之,只要能给别人带来价值,都可以去尝试。常见的程序员兼职种类如下:




  • 接项目:比如几个程序员朋友组队,接一些不大的项目,项目规模一般几千 ~ 几万 ~ 几十万不等。或者承接一些小工具或者小爬虫之类的项目。一般这种情况会承接熟人的项目,有些可能靠自己在闲鱼或者淘宝上的推广。

  • 做技术顾问:有些朋友技术很棒,有深度有广度,对业务也精通,可以长期给别的公司做技术顾问。不过这种类型需要个人在业内或者圈内有不错的口碑和知名度。

  • 知识付费:有朋友开玩笑说,程序员的尽头是卖课,哈哈!!!知识付费确实也是资深程序员在做的事情,比如在某些领域比较深入的朋友,会在一些付费平台上卖课。或者做私域的内容付费,比如知识星球、小报童等等。

  • 自媒体:有程序员通过开通博客或者自媒体,讲讲技术领域相关的内容,赚取广告费,后期也可能引流到私域,做一些增值付费的内容。

  • 打造小型产品:这种类型一般是做一些工具型的产品,或者某个行业的小型软件,或者维护一些开源的产品。产品的呈现形态可能是网站、APP、小程序等,然后在搭配上适当的营销推广,完成商业闭环。


4、程序员兼职的优先级


在选择兼职时,应该优先选择那些能够提升自身能力沉淀资源的事情。


这些事情不仅可以让我们学习到更多的知识(技术知识、商业知识、营销知识等等),还可以沉淀资源和拓展人脉,为未来的发展打下坚实的基础。


5、程序员兼职的自我保护



每个人的情况不同,选择的兼职种类也会有所不同。但无论选择什么样的兼职,都应该注意以下几点




  • 确保兼职时间在下班后,避免影响本职工作。本职工作一定要保质保量的完成,毕竟拿着公司的这份薪水。切莫因为公司给你安排了本职工作,让你感觉耽误了你的兼职工作,从而产生抵抗情绪。如果有紧急兼职事情非要在上班时间处理,那就不要留下痕迹。

  • 不要在兼职过程中泄露公司的任何机密信息,包括软件代码、项目计划等等。兼职期间写的代码,不要从所在公司的项目里拷贝,这样很容易引发泄密事件。不要给所在公司的竞对去做兼职。这样会让你不自觉的陷入到泄密陷阱里。

  • 不要在公司的电脑上进行兼职工作,以免留下证据或引起不必要的误会。

  • 尽量不要与兼职公司签订固定合同,以免因为兼职工作而影响自己的全职工作稳定性。如果非要签合同,建议使用家人的身份信息。


6、各方要遵守的底线


6.1、程序员的底线


程序员做兼职,大部分场景其实是处在与所在公司的对立面的,所以首要职责是完成好自己的本职工作,保证公司的正常运转和项目的顺利进行。如果因为兼职而耽误了本职工作,不仅会影响自己的职业发展,也会给公司带来损失。


一旦觉得兼职或者副业,可以全身心的投入了,笔者建议主动转向副业,副业变主业,对于自己和所在公司都是一种好的选择。有可能你的副业将来还能给所在公司带来更大的价值。笔者确实见过些好的案例:员工通过副业成功创业,然后和所在公司相互成就。


6.2、老板的胸怀


让我们换个角度思考一下。如果我们是公司的老板,我们应该如何看待员工的兼职行为呢?


作为老板,还是要保有一些胸怀和格局,允许员工自由发展。虽然老板希望员工能够全心全意地投入到工作中去,为公司创造更多的价值。但同时,老板也要理解员工需要追求个人成长兴趣满足收入创新等需求。


因此,在允许员工兼职的同时,公司也需要制定一些规范和原则,以确保公司的利益不受损害,以确保兼职行为不会给公司带来负面影响。


6.3、共赢


总之,程序员做兼职需要谨慎处理,既要追求个人成长、兴趣满足、收入创新等,也要遵守公司的规定和原则。


只有在良好的平衡和取舍中,个体的兼职公司的利益才能稳步前行,才能实现个人与公司的共赢。只有这样,我们才能在兼职和副业的道路上走得更远、更稳。


7、总结


本文主要聊了程序员做兼职时的一些套路,以及应该遵循的原则。主要内容如下:



  • 程序员做兼职的种类较多,优先做对个人能力提升较大或者能够沉淀资源的事情。

  • 一旦兼职或者副业发展起来,就可以辞去主业,全力投入副业。

  • 尽量避免站在公司的对立面,做兼职也可以正大光明的做,尽量打造个人和公司双赢的局面。

  • 如果实在无法正大光明的做,那就尽量避开一些坑点。




作者:程序员半支烟
来源:mp.weixin.qq.com/s/_bF0AspoPGdiZ-XahlS_FQ
收起阅读 »

打工人回家过年:只想休息,讨厌拜年、走亲戚、被催婚

大家好,我是杨成功。昨天楼下吃饭,听到一个女孩在打电话,声音很大,听起来很生气。原因是父母让她过年回去的时候给亲戚带礼物,女孩不愿意,和父母吵起来了。女孩说:“今年本来就没攒下钱,回家来回的车票就花了一大笔,给你们带礼物也花了不少,为啥非得给亲戚带礼物?你们别...
继续阅读 »

大家好,我是杨成功。

昨天楼下吃饭,听到一个女孩在打电话,声音很大,听起来很生气。

原因是父母让她过年回去的时候给亲戚带礼物,女孩不愿意,和父母吵起来了。

女孩说:“今年本来就没攒下钱,回家来回的车票就花了一大笔,给你们带礼物也花了不少,为啥非得给亲戚带礼物?你们别光考虑你们的面子,能不能考虑一下我,年后还要交房租...”

听到这里,我心里一痛。

作为一个资深北漂,我被戳中了。

很多人以为呆在北上广的人光鲜亮丽,实际上也只是两点一线的打工人;看起来钱赚的不少,实际上开销大到离谱,一年到头剩不下多少。

今年互联网裁员潮,一片一片地裁,搞的大家人心惶惶。好几个朋友上午还在开心地写代码,下午就被请到会议室喝茶。

有些拿不到赔偿的伙伴年底还在跑仲裁,真的很不容易。

如果连父母都不能理解的话,我实在不敢想象,这个女孩回家过年的压力有多大。

前几天有一条热搜:为什么年轻人不愿意回家过年了?

年轻人不愿意回家过年,很多父母的第一反应是不孝顺,白眼狼,在外面呆野了。

哎,谁不想回家过年啊,不回去肯定是不开心,而且不是一点点不开心,是压力重重。

可能父母认为,孩子回家过年就图个热闹,到七大姑八大姨家串门拜年,见一见亲戚朋友兄弟姐妹,喝酒吃肉聊天,好不开心。

其实不是的,真不是。就拿我来说,我回家只想睡觉嗑瓜子看电视,不洗脸不洗头谁都不见,同学聚会我都不想去。除非是几个关系极好的发小,其他任何社交局都是负担。

除了社交压力,还有经济压力。

像开头说的那个女孩一样,回一趟家要花车票钱、礼物钱、亲戚孩子压岁钱、给老人钱。赚钱了还好,如果一年没赚钱,这些人情开销就是一笔负担。

累了一整年,只想回家休息,好好过个年,结果还要看钱包。

当然还有催婚压力。

像我这个年纪,马上奔三的人,过年回家见个人就是“找对象了没”。我家人比较开明,最多开玩笑问一句,亲戚朋友问就是“明年”。

但我知道很多朋友、尤其女性朋友,过年催婚会把人逼疯。

有些父母的催婚极其致命:“快三十了还不结婚,过了三十谁要你?你不成家我都没脸出门;人家谁谁都二胎了,你到底想咋样?你对得起...”。

现在是 2024 年啊,找对象的难度不比打工挣钱低。如果再和父母吵上一架,这个年过的还有啥意思。

这一层层的压力,早把年轻人回家过年的热情打散了,过个年比上班还累。

现在能理解为啥年轻人不回家过年了吗?

对父母来说,如果孩子愿意回家过年,就别要求那么多了,人回来图个开心就好。

如果孩子在读大学,回家后就是想享受一下。你就让他睡到自然醒,让他每天蓬头垢面打游戏看电视,反正呆不了几天。

如果愿意出去走亲戚,那就带上,不愿意也别勉强。更不要动不动就要求上酒桌,给长辈敬个酒,还得提一个,真的很尴尬。

如果孩子在上班,一年已经很累了,她回家可能只想休息。父母们管好自己的嘴,少催婚,少安排相亲,少要求这要求那。

更不要说谁谁家孩子赚了多少钱,谁谁家都抱孙子了。这样大家都不舒服,开开心心过个年不好吗?

可能会有父母认为:我不催她都不上心。

想想上学的时候,天天盯着学习,不能上网,不能找对象,不能玩这玩那,结果考上985了吗?

结婚这事催不得,终身大事,你不能随便拉一个就领证吧,现在又不是70年代。

如果逼的太急,很可能孩子明年就不回来过年了,骂也没有用。

社会压力大,年轻人不比上一代轻松。多一点体贴关照,少一点要求,开心过年。

车上没网,有感而发,到此为止。


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

前仇旧怨一笔勾销!周鸿祎与傅盛和解背后的底层逻辑

今天我们来聊聊两位科技和AI圈都颇有名气的两位人物——周鸿祎和傅盛。这两位各自在网络安全和人工智能领域都有着显著的成就,但他们之间的恩怨纠葛也是众所周知的。不过最近,他们竟然和解坐到了一起,引发了不少人的猜测和讨论。 那么,到底是什么原因让这两位昔日的对手能...
继续阅读 »

今天我们来聊聊两位科技和AI圈都颇有名气的两位人物——周鸿祎和傅盛。这两位各自在网络安全和人工智能领域都有着显著的成就,但他们之间的恩怨纠葛也是众所周知的。不过最近,他们竟然和解坐到了一起,引发了不少人的猜测和讨论。



那么,到底是什么原因让这两位昔日的对手能够放下过往,重新坐到一起呢?接下来,我就从他们的恩怨历史和当前的商业逻辑来给大家分析分析。


首先,让我们来回顾一下两人的恩怨历史。


360安全卫士大家可能都用过吧,其首创的免费模式把一众杀毒软件公司都干死了,以前可以说是电脑的必装应用,当然也因为难以卸载,一直背负流氓软件的称号。


5g3ogX0Ap9FbTZYBuTWYMLmnOgFtWnu0z4qHyTIFoem4N1575941237193compressflag.jpg


周鸿祎,360公司的创始人,曾经是中国互联网安全界的领军人物,因免费杀毒和大战QQ而一战成名。而傅盛,曾经是周鸿祎的爱将,早年在360公司担任高管,负责360安全卫士产品,对360的成长有着不可磨灭的贡献。然而好景不长,由于理念和发展方向上的分歧,傅盛最终离开了360,创办了猎豹移动,开启了自己的创业之路。此后,两人在商业上多有摩擦,甚至公开在媒体上互相指责,成为了科技圈内的一个热门话题。



但是,科技圈的水很深,商业的世界里没有永远的敌人,只有永远的利益。现在,我们看到周鸿祎和傅盛重新坐到了一起,背后一定有着他们共同关注的商业逻辑。


现在的互联网世界,AI成了新的风口。周鸿祎的360,虽然在安全领域深耕多年,但随着互联网环境的变化,传统的安全产品和服务正面临着增长的瓶颈。而傅盛的猎豹移动,虽然在移动应用方面有所建树,但在AI领域的竞争也是异常激烈。AI和大数据的结合,正成为推动各行各业发展的新引擎,这无疑是两人和解坐到一起的一个重要原因。


周鸿祎拥有庞大的用户基础和数据积累,而傅盛在AI领域的探索也有着自己的独到见解。两人的合作,可以说是强强联合,360可以利用AI技术为用户提供更智能的安全解决方案,而猎豹移动也可以借助360的用户基础,扩大自己在AI领域的应用场景。这样的合作,对双方来说都是一次难得的发展机遇。



而且还有一个很重要的问题,现在是网红经济时代,谁吸引到了流量,谁就能挣到钱。两个人的和解也必定会收获很多的流量,让更多的人了解到他们,关注他们。在这一点上,两个人都不傻,都知道流量的价值。


当然,和解坐到一起并不意味着以前的恩怨就此烟消云散,科技圈的合作与竞争往往是同步进行的。这次的和解,也许只是双方为了共同的商业利益所做出的战略选择。未来他们是否能够真正放下过往,携手共进,还需要时间来证明。


最后,这次周鸿祎和傅盛的和解,不仅仅是两个人的事,也是整个科技圈发展趋势的一个缩影。在AI成为新的竞争焦点的今天,许多科技企业都在寻求转型和突破,合作成为了一种新的生存策略。那么,这是否意味着我们将会看到更多科技领袖之间的联合与和解呢?又或者,这只是一场精心策划的商业秀,背后隐藏着更深层次的商业计算呢?这些问题,都值得我们继续关注和思考。


好了,今天的内容就到这里。对于周鸿祎和傅盛的和解,你有什么看法呢?欢迎在评论区留言讨论,我们下次再见!


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

一条SQL差点引发离职

文章首发于微信公众号:云舒编程 关注公众号获取: 1、大厂项目分享 2、各种技术原理分享 3、部门内推 背景        最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。 本来是一个很简单的逻辑,就是根据唯一的id去...
继续阅读 »

文章首发于微信公众号:云舒编程

关注公众号获取:
1、大厂项目分享
2、各种技术原理分享
3、部门内推



背景


       最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。

本来是一个很简单的逻辑,就是根据唯一的id去更新对应的MySQL数据,代码简化后如下:


var updates []*model.Goods
for id, newGoods := range update {
 if err := model.GetDB().Model(&model.Goods{}).Where("id = ?", id).Updates(map[string]interface{}{
  "selling_price":  newGoods.SellingPrice,
  "sell_type":      newGoods.SellType,
  "status":         newGoods.Status,
  "category_id":    newGoods.CategoryID,
 }).Error; err != nil {
  return nil, err
 }
}

很明显,updates[]model.Goods\color{red}{updates []*model.Goods}本来应该是想声明为 map[string]model.Goods\color{red}{map[string]*model.Goods}类型的,然后key是唯一id。这样下面的更新逻辑才是对的,否则拿到的id其实是数组的下标。

但是code review由于跟着一堆代码一起评审了,并且这段更新很简单,同时测试的时候也测试过了(能测试通过也是“机缘巧合”),所以没有发现这段异常。

发到线上后,进行了灰度集群的测试,这个时候发现只要调用了这个接口,灰度集群的数据全部都变成了一样,回滚后正常。


分析


       回滚后在本地进行复现,由于本地环境是开启了SQL打印的,于是看到了这么一条SQL:很明显是拿数组的下标去比较了


update db_name set selling_price = xx,sell_type = xx where id = 0;

       由于我们的id是全部是通过uuid生成的,所以下意识的认为这条sql应该啥也不会更新才对,但是本地的确只执行了这条sql,没有别的sql,并且db中的数据全部都被修改了。

这个时候想起福尔摩斯的名言“排除一切不可能的,剩下的即使再不可能,那也是真相”\color{blue}{“排除一切不可能的,剩下的即使再不可能,那也是真相”} ,于是抱着试一试的心态直接拿这条sql去db控制台执行了一遍,发现果然所有的数据又都被修改了。

也就是 whereid=0\color{red}{where id = 0}  这个条件对于所有的记录都是恒为true,就会导致所有记录都被更新。在这个时候,想起曾经看到过MySQL对于不同类型的比较会有 【隐式转换】\color{red}{【隐式转换】},难道是这个原因导致的?


隐式转换规则


在MySQL官网找到了不同类型比较的规则:



最后一段的意思是:对于其他情况,将按照浮点(双精度)数进行比较。例如,字符串和数字的比较就按照浮点数规则进行比较。

也就是id会首先被转换成浮点数,然后再跟0进行比较。


MySQL字符转为浮点数时会按照如下规则进行:


1.如果字符串的第一个字符就是非数字的字符,那么转换结果就是0;

2.如果字符串以数字开头:

(1)如果字符串都是数字,转换结果就是整个字符串对应的数字;

(2)如果字符串中存在非数字,转换结果就是开头的那些数字对应的值;

举例说明:

"test" -> 0

"1test" -> 1

"12test12" -> 12

由于我们生成的uuid没有数字开头的字符串,于是都会转变成0。那么这条SQL就变成了:


update db_name set selling_price = xx,sell_type = xx where 0 = 0;

就恒为true了。

修复就很简单了,把取id的逻辑改成正确的就行。


为什么测试环境没有发现


       前面有提到这段代码在测试环境是测试通过了的,这是因为开发和测试同学的环境里都只有一条记录,每次更新他发现都能正常更新就认为是正常的了。同时由于逻辑太简单了,所以都没有重视这块的回归测试。

幸好在灰度集群就发现了这个问题,及时进行了回滚,如果发到了线上影响了用户数据,可能就一年白干了。


最后


代码无小事,事事需谨慎啊。一般致命问题往往是一行小小的修改导致的。


作者:云舒编程
来源:juejin.cn/post/7275550679790960640
收起阅读 »

Android 沉浸式状态栏,透明状态栏 采用系统api,超简单近乎完美的实现

前言 沉浸式的适配有多麻烦,相信大家既然来搜索这个,就说明都在为此苦恼,那么看看这篇文章吧,也许对你有所帮助(最下面有源码链接) 有写的不对的地方,欢迎指出 从adnroid 6.0开始,官方逐渐完善了这方面的api,直到android 11... ... 让...
继续阅读 »

前言


沉浸式的适配有多麻烦,相信大家既然来搜索这个,就说明都在为此苦恼,那么看看这篇文章吧,也许对你有所帮助(最下面有源码链接)


有写的不对的地方,欢迎指出


从adnroid 6.0开始,官方逐渐完善了这方面的api,直到android 11...


... 让我们直接开始吧


导入核心包


老项目非androidx的请自行研究下,这里使用的是androidx,并且用的kotlin语言
本次实现方式跟windowInsets息息相关,这可真是个好东西
首先是需要导入核心包
androidx.core:core

kotlin可选择导入这个:
androidx.core:core-ktx
我用的版本是
androidx.core:core-ktx:1.12.0

开启 “沉浸式” 支持


沉浸式原本的意思似乎是指全屏吧。。。算了,不管那么多,喊习惯了 沉浸式状态栏,就这么称呼吧。

在activity 的oncreate里调用
//将decorView的fitSystemWindows属性设置为false
WindowCompat.setDecorFitsSystemWindows(window, false)
//设置状态栏颜色为透明
window.statusBarColor = Color.TRANSPARENT
//是否需要改变状态栏上的 图标、字体 的颜色
//获取InsetsController
val insetsController = WindowCompat.getInsetsController(window, window.decorView)
//mask:遮罩 默认是false
//mask = true 状态栏字体颜色为黑色,一般在状态栏下面的背景色为浅色时使用
//mask = false 状态栏字体颜色为白色,一般在状态栏下面的背景色为深色时使用
var mask = true
insetsController.isAppearanceLightStatusBars = mask
//底部导航栏是否需要修改
//android Q+ 去掉虚拟导航键 的灰色半透明遮罩
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
//设置虚拟导航键的 背景色为透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//8.0+ 虚拟导航键图标颜色可以修改,所以背景可以用透明
window.navigationBarColor = Color.TRANSPARENT
} else {
//低版本因为导航键图标颜色无法修改,建议用黑色,不要透明
window.navigationBarColor = Color.BLACK
}
//是否需要修改导航键的颜色,mask 同上面状态栏的一样
insetsController.isAppearanceLightNavigationBars = mask

修改 状态栏、虚拟导航键 的图标颜色,可以在任意需要的时候设置,防止图标和字体颜色和背景色一致导致看不清

补充一下:
状态栏和虚拟导航栏的背景色要注意以下问题:
1.在低于6.0的手机上,状态栏上的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为它们有自己的api能实现修改颜色
2.在低于8.0的手机上,虚拟导航栏的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为他们有自己的api能实现修改颜色
解决方案:
低于指定版本的系统上,对应的颜色就不要用透明,除非你的APP页面是深色背景,否则,建议采用半透明的灰色

在带有刘海或者挖孔屏上,横屏时刘海或者挖孔的那条边会有黑边,解决方法是:
给APP的主题v27加上
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
参考图:

image.png


监听OnApplyWindowInsetsListener


//准备一个boolean变量 作为是否在跑动画的标记
var flagProgress = false

//这里可以使用decorView或者是任意view
val view = window.decorView

//监听windowInsets变化
ViewCompat.setOnApplyWindowInsetsListener(view) { view: View, insetsCompat: WindowInsetsCompat ->
//如果要配合下面的setWindowInsetsAnimationCallback一起用的话,一定要记得,onProgress的时候,这里做个拦截,直接返回 insets
if (flagProgress) return@setOnApplyWindowInsetsListener insetsCompat
//在这里开始给需要的控件分发windowInsets

//最后,选择不消费这个insets,也可以选择消费掉,不在往子控件分发
insetsCompat
}
//带平滑过渡的windowInsets变化,ViewCompat中的这个,官方提供了 api 21-api 29的支持,本来这个只支持 api 30+的,相当不错!
//启用setWindowInsetsAnimationCallback的同时,也必须要启用上面的setOnApplyWindowInsetsListener,否则在某些情况下,windowInsets改变了,但是因为不会触发setWindowInsetsAnimationCallback导致padding没有更新到UI上
//DISPATCH_MODE_CONTINUE_ON_SUBTREE这个代表动画事件继续分发下去给子View
ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
override fun onProgress(insetsCompat: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
//每一帧的windowInsets
//可以在这里分发给需要的View。例如给一个聊天窗口包含editText的布局设置这个padding,可以实现键盘弹起时,在底部的editText跟着键盘一起滑上去,参考微信聊天界面,这个比微信还丝滑(android 11+最完美)。
//最后,直接原样return,不消费
return insetsCompat
}

override fun onEnd(animation: WindowInsetsAnimationCompat) {
super.onEnd(animation)
//动画结束,将标记置否
flagProgress = false
}

override fun onPrepare(animation: WindowInsetsAnimationCompat) {
super.onPrepare(animation)
//动画准备开始,在这里可以记录一些UI状态信息,这里将标记设置为true
flagProgress = true
}
})

读取高度值


通过上面的监听,我们能拿到WindowInsetsCompat对象,现在,我们从这里面取到我们需要的高度值


先定义几个变量,我们需要拿的包含:
1. 刘海,挖空区域所占据的宽度或者是高度
2. 被系统栏遮挡的区域
3. 被输入法遮挡的区域

//cutoutPadding 刘海,挖孔区域的padding
var cutoutPaddingLeft = 0
var cutoutPaddingTop = 0
var cutoutPaddingRight = 0
var cutoutPaddingBottom = 0

//获取刘海,挖孔的高度,因为这个不是所有手机都有,所以,需要判空
insetsCompat.displayCutout?.let { displayCutout ->
cutoutPaddingTop = displayCutout.safeInsetTop
cutoutPaddingLeft = displayCutout.safeInsetLeft
cutoutPaddingRight = displayCutout.safeInsetRight
cutoutPaddingBottom = displayCutout.safeInsetBottom
}


//systemBarPadding 系统栏区域的padding
var systemBarPaddingLeft = 0
var systemBarPaddingTop = 0
var systemBarPaddingRight = 0
var systemBarPaddingBottom = 0

//获取系统栏区域的padding
//系统栏 + 输入法
val systemBars = insetsCompat.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars())
//左右两侧的padding通常直接赋值即可,如果横屏状态下,虚拟导航栏在侧边,那么systemBars.left或者systemBars.right的值就是它的宽度,竖屏情况下,一般都是0
systemWindowInsetLeft = systemBars.left
systemWindowInsetRight = systemBars.right
//这里判断下输入法 和 虚拟导航栏是否存在,如果存在才设置paddingBottom
if (insetsCompat.isVisible(WindowInsetsCompat.Type.ime()) || insetsCompat.isVisible(WindowInsetsCompat.Type.navigationBars())) {
systemWindowInsetBottom = systemBars.bottom
}
//同样判断下状态栏
if (insetsCompat.isVisible(WindowInsetsCompat.Type.statusBars())) {
systemWindowInsetTop = systemBars.top
}

到这里,我们需要的信息已经全部获取到了,接下来就是根据需求,设置padding属性了

补充一下:
我发现在低于android 11的手机上,insets.isVisible(Type)返回始终为true
并且,即使系统栏被隐藏,systemBars.top, systemBars.bottom也始终会有高度
所以这里


保留原本的Padding属性


上述获取的值,直接去设置padding的话,会导致原本的padding属性失效,所以我们需要在首次设置监听,先保存一份原本的padding属性,在最后设置padding的时候,把这份原本的padding值加上即可,就不贴代码了。


第一次写文章,写的粗糙了点

可能我写的不太好,没看懂也没关系,直接去看完整代码吧


我专门写了个小工具,可以去看看:
沉浸式系统栏 小工具


如果有更好的优化方案,欢迎在github上提出,我们一起互相学习!


作者:Matchasxiaobin
来源:juejin.cn/post/7275943802938130472
收起阅读 »

系统干崩了,只认代码不认人

各位朋友听我一句劝,写代码提供方法给别人调用时,不管是内部系统调用,还是外部系统调用,还是被动触发调用(比如MQ消费、回调执行等),一定要加上必要的条件校验。千万别信某些同事说的这个条件肯定会传、肯定有值、肯定不为空等等。这不,临过年了我就被坑了一波,弄了个生...
继续阅读 »

各位朋友听我一句劝,写代码提供方法给别人调用时,不管是内部系统调用,还是外部系统调用,还是被动触发调用(比如MQ消费、回调执行等),一定要加上必要的条件校验。千万别信某些同事说的这个条件肯定会传、肯定有值、肯定不为空等等。这不,临过年了我就被坑了一波,弄了个生产事故,年终奖基本是凉了半截。


为了保障系统的高可用和稳定,我发誓以后只认代码不认人。文末总结了几个小教训,希望对你有帮助。


一、事发经过


我的业务场景是:业务A有改动时,发送MQ,然后应用自身接受到MQ后,再组合一些数据写入到Elasticsearch。以下是事发经过:



  1. 收到一个业务A的异常告警,当时的告警如下:



  2. 咋一看觉得有点奇怪,怎么会是Redis异常呢?然后自己连了下Redis没有问题,又看了下Redis集群,一切正常。所以就放过了,以为是偶然出现的网络问题。

  3. 然后技术问题群里 客服 反馈有部分用户使用异常,我警觉性的感觉到是系统出问题了。赶紧打开了系统,确实有偶发性的问题。

  4. 于是我习惯性的看了几个核心部件:



    1. 网关情况、核心业务Pod的负载情况、用户中心Pod的负载情况。

    2. Mysql的情况:内存、CPU、慢SQL、死锁、连接数等。



  5. 果然发现了慢SQL和元数据锁时间过长的情况。找到了一张大表的全表查询,数据太大,执行太慢,从而导致元数据锁持续时间太长,最终数据库连接数快被耗尽。


SELECT xxx,xxx,xxx,xxx FROM 一张大表


  1. 立马Kill掉几个慢会话之后,发现系统仍然没有完全恢复,为啥呢?现在数据库已经正常了,怎么还没完全恢复呢?又继续看了应用监控,发现用户中心的10个Pod里有2个Pod异常了,CPU和内存都爆了。难怪使用时出现偶发性的异常呢。于是赶紧重启Pod,先把应用恢复。

  2. 问题找到了,接下来就继续排查为什么用户中心的Pod挂掉了。从以下几个怀疑点开始分析:



    1. 同步数据到Elasticsearch的代码是不是有问题,怎么会出现连不上Redis的情况呢?

    2. 会不会是异常过多,导致发送异常告警消息的线程池队列满了,然后就OOM?

    3. 哪里会对那张业务A的大表做不带条件的全表查询呢?



  3. 继续排查怀疑点a,刚开始以为:是拿不到Redis链接,导致异常进到了线程池队列,然后队列撑爆,导致OOM了。按照这个设想,修改了代码,升级,继续观察,依旧出现同样的慢SQL 和 用户中心被干爆的情况。因为没有异常了,所以怀疑点b也可以被排除了。

  4. 此时基本可以肯定是怀疑点c了,是哪里调用了业务A的大表的全表查询,然后导致用户中心的内存过大,JVM来不及回收,然后直接干爆了CPU。同时也是因为全表数据太大,导致查询时的元数据锁时间过长造成了连接不能够及时释放,最终几乎被耗尽。

  5. 于是修改了查询业务A的大表必要校验条件,重新部署上线观察。最终定位出了问题。


二、问题的原因


因为在变更业务B表时,需要发送MQ消息( 同步业务A表的数据到ES),接受到MQ消息后,查询业务A表相关连的数据,然后同步数据到Elasticsearch。


但是变更业务B表时,没有传业务A表需要的必要条件,同时我也没有校验必要条件,从而导致了对业务A的大表的全表扫描。因为:


某些同事说,“这个条件肯定会传、肯定有值、肯定不为空...”,结果我真信了他!!!

由于业务B表当时变更频繁,发出和消费的MQ消息较多,触发了更多的业务A的大表全表扫描,进而导致了更多的Mysql元数据锁时间过长,最终连接数消耗过多。


同时每次都是把业务A的大表查询的结果返回到用户中心的内存中,从而触发了JVM垃圾回收,但是又回收不了,最终内存和CPU都被干爆了。


至于Redis拿不到连接的异常也只是个烟雾弹,因为发送和消费的MQ事件太多,瞬时间有少部分线程确实拿不到Redis连接。


最终我在消费MQ事件处的代码里增加了条件校验,同时也在查询业务A表处也增加了的必要条件校验,重新部署上线,问题解决。


三、总结教训


经过此事,我也总结了一些教训,与君共勉:



  1. 时刻警惕线上问题,一旦出现问题,千万不能放过,赶紧排查。不要再去怀疑网络抖动问题,大部分的问题,都跟网络无关。

  2. 业务大表自身要做好保护意识,查询处一定要增加必须条件校验。

  3. 消费MQ消息时,一定要做必要条件校验,不要相信任何信息来源。

  4. 千万别信某些同事说,“这个条件肯定会传、肯定有值、肯定不为空”等等。为了保障系统的高可用和稳定,咱们只认代码不认人

  5. 一般出现问题时的排查顺序:



    1. 数据库的CPU、死锁、慢SQL。

    2. 应用的网关和核心部件的CPU、内存、日志。



  6. 业务的可观测性和告警必不可少,而且必须要全面,这样才能更快的发现问题和解决问题。




作者:程序员半支烟
来源:mp.weixin.qq.com/s/TvIpTZq0XO8v9ccYSsM37Q
收起阅读 »

那些年走岔的路,一个人总要为自己的认知买单!

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。 回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结! 一 大四下学期我们就离开学校了...
继续阅读 »

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。


回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结!



大四下学期我们就离开学校了,加上寒假的两个月,实际上我们的实习期有半年多,但是找工作应该是大四上学期就开始了。


那时候彪哥整天都在面试,积累了不少面试经验,也学习了不少知识,而那时候我鬼迷心窍,去做项目去了。


因为一些巧合,我加入了一个SAAS软件开发的小团队,做的是酒店方面的业务,我是远程办公,那段时间一边做毕设,一边做项目,但是做毕设的时间很少,因为论文就花了五天时间去写,更多是在做酒店项目。


现在我有一部分读者都是从我的区块链毕设过来的,我想对你们说一声,感谢你们的付费,但是也想对你们说一声对不起,如果当时我专心去做毕设,或许呈现在你们眼前的作品会更好,但是时间不能重来!


但是后来我仔细思考,我既不应该花时间去做毕设,也不应该为了点钱去做项目!


纵使我的毕设得了优秀毕设,算是我们那一届最优秀的毕设,但是并没有什么卵用,你的简历并不会因为一个优秀毕设而变得多么耀眼。


为了一点钱去做项目也不理智,因为一个人的时间是有限的,当把时间碎片化后,就很难集中去做一件事了,当时虽然说给我6k一个月,但是因为很多东西不熟悉,所以现去学,像uniapp都去学了,所以功能完成度和质量不高,一个月只给我结了3000不到!


干了两个月我们就毕业了,我收拾行李就回家了。



回到家里后,他们说直接给我一个单独项目做,也是一个SAAS的系统,说开发周期2个月,5万块钱,我当时心里想,一个月两万多,我直接不去实习了,安心干,干完我还可以玩几个月,这他妈多好啊。


于是我就接下来了,就开始进入coding状态,白天干,晚上干,后面在家里呆烦了,又跑回学校去。


在学校呆了半个多月,我做了50%,于是迫于经济压力,又回家了,回家最起码不愁饭吃。


图片


那时候,我把自己定义为一个自由职业者,我也挺享受这样的生活,coding累了,就出去走走,回来后又继续coding,说实话,还挺享受!


那时候基本上大多同学都出去实习了,有些去了很不错的互联网公司,听他们说公司又是用什么牛逼的技术了,心里就突然有点羡慕。


但是想到项目做完马上能拿到钱了,就没有去羡慕了。


两个月时间很快到了,老板准时来验收了,不过一验bug足足提了几百个,还有很多变更,老板说尽快改完!


当时我有点懵,不应该先给我点钱吗?


我就说先付40%给我,但是人家说,你这玩意用起来到处是问题,无法用啊,怎么给钱?


我无话可说,拿不到钱,心里更加焦虑了,想不干了,那么就前功尽弃,如果继续干,问题越来越多,变更越来越多,思来想去,最后还是硬着头皮干了!


陆陆续续又干了半个多月,这时候二验又开始了,老板说这次稍微好了一点,但是也无法用啊,于是叫我把代码上传到他们仓库,然后给我付3000块钱,开发完后再一起结,我自然不愿意。


我想,代码给你了,你不理我了怎么办,所以我还是想等开发完以后拿到钱再交代码。


这时候我干了快三个月了,心里虽然看到一点希望,但是更多的是焦虑,因为再有几个月了就要毕业了,而我还没有去实习!


父母也开始念叨,心里的压力就更大了,我想,再干半个月,还拿不了钱,我真的就不干了。


我又继续做,为了快速做完,很多东西我都是没有考虑的,所以问题自然也多,特别还有硬件对接,还有一些复杂的操作。


说实话,这东西暂时肯定是用不了的,但是为了能拿到钱,我也带有一点骗的成分在里面,偷工减料,以为人家看不出来,实际上别人比你精多!


很多项目二验不通过,那基本就烂尾了,但是老板说,来个三验,果然还是用不了,问题很多,所以依然没拿到钱。


心里更加烦躁了,后面我直接说要么给钱,要么不做了,心里彻底崩溃了,心里后悔,为啥要去接这个项目,为啥浪费这么多时间,为啥不去实习。


后面老板说,如果你不想开发了也可以,把代码交出来,给你5000块钱,后面你和别人一起协同开发,不用全职开发。


我心里是抗拒的,干了这么久才几千块钱,心有不甘,不过过了几天,因为经济压力,所以还是选择交出代码了,谈成了6000块钱。


因为我知道他们会一直加需求,一直在变更,是一个无底洞!


三个多月,就得了6000块钱,心里别提多难受,不过好在暂时有点钱用。


于是直接就不干了,在家里呆了几天就开始投简历了,只有三个月不到就毕业了,所以自然去不了外面了,于是只能在省会城市找实习了。


还好那时候面试机会还挺多,一个星期不到就入职了,6000块钱的实习,就去干了,说实话,一个三线城市,也只能开这么多了!


不过现在这种就业环境,如果学历背景没有占优势,三线城市找6000以上的实习,还是比较难的,这两年市场真的比较低迷了!


“自由职业者“的那段时间,大概是我这么多年来最煎熬的时光,因为总是在希望和失望中来回穿梭。


后来我在书中看到一段话,“如果命运给你一次机会,哪怕是一根稻草,你也要牢牢抓住”,显然那个时候我的认知比较低,认为那就是命运的稻草,但是实际上那不是,那是荆棘!


当你的认知和能力都不够的时候,就算钱摆在你面前你都拿不了。



落笔到这里,心里不禁泛起一阵酸楚!


一个人总要为自己的认知买单的,因为在很黄金的时间阶段,我去做了不太正确的选择,虽然不曾后悔,但是我知道那是不理智的选择。


这段回忆虽然会成为我人生的阅历,甚至可以说是一种财富,但是他终归是一个教训,不值得提倡!



在大四上学期,应该快速把毕设做完,然后进入复习,投简历,即使找不到工作,也能锻炼面试能力,对自己的知识体系进行查缺补漏!


优秀毕设,论文,这些在本科阶段实际上没什么卵用,不过是教育的一个考核而已。


在校期间,那些社团活动,学生会并不能为你将来的职业发展发挥多大的作用,切勿过于沉迷!


眼前的小钱是陷阱,在未来很快就能赚回来!


在学校期间,兼职是完全没有必要的,因为赚不了几个钱,但是却花费了大量的时间,学生时期正是学习知识的时候,浪费了就没有了。


因为把只是学扎实,这点钱等毕业后一个月就能全部赚回来,但是如果浪费了,将要用很多时间去弥补,这时候你已经落后于别人很多了!


虽然我去做项目也能锻炼自己的能力,但是时机不对,如果大三去做那么没问题,但是在临近毕业之际去做,这就是不理智的。



学生时代,对于项目我们是没有风险把控能力的,也不清楚项目的流程,所以能赚到钱的几率不大!


我浪费了三四个月的时间去做一个项目这是不理智的,首先单干很有局限性,因为独木不成舟,你很多东西考虑不到位,所以会有很多漏洞。


还有你不能学习优秀的人的逻辑,实际上你是处于一个封闭的状态。


我觉得正确的做法是应该找一个不错的公司进去学习,融入团队,这样才能真的学到东西。


天真的是,我当时还想将其打造成一个产品,然后进行创业!


后来想想,自己如果真的投入时间去做了,那么不仅赚不到钱,可能还会饿肚子。


不用说什么不去试试怎么知道。


当你的认知跟不上的时候,你所想的,所做的,基本上都不会成功,不要想着幸运之神降临在你的身上。



那年,我傻逼地把自己定义为自由职业者。


实际上我连边都沾不上,因为没有赚到钱,还谈什么自由,叫“烂账职业者”还差不多。


今天,我们总是去羡慕那些自由职业者每天不用上班也能赚钱,实际上和你看到的不一样。


自由职业者赚到钱的人只有少数,但是都是经历过很多尝试,认知得到飞跃地提升后才成的。


不过可以肯定的是,未来自由职业者会越来越多,个人IP也将在未来大爆发。


布局是我们该做的事。


种一棵树最好的时间是十年前,其次是现在。



以上也就是对于过去的一些反思,我从来不去抱怨过去,只是去思考自己。


因为每一条路都没有对错,只能说很多时候选择大于努力。


路走岔了的时候要及时止损,不要一头黑走到底,这样对自己不好。


对于未来,还是得比较理性去看待,虽然充满各种不确定性,但是很多确定性的东西我们是能看到的。


行文至此,已经凌晨2点!


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

MQ消息积压,把我整吐血了

大家好,我是苏三,又跟大家见面了。 前言 我之前在一家餐饮公司待过两年,每天中午和晚上用餐高峰期,系统的并发量不容小觑。为了保险起见,公司规定各部门都要在吃饭的时间轮流值班,防止出现线上问题时能够及时处理。 我当时在后厨显示系统团队,该系统属于订单的下游业务...
继续阅读 »

大家好,我是苏三,又跟大家见面了。



前言


我之前在一家餐饮公司待过两年,每天中午和晚上用餐高峰期,系统的并发量不容小觑。为了保险起见,公司规定各部门都要在吃饭的时间轮流值班,防止出现线上问题时能够及时处理。


我当时在后厨显示系统团队,该系统属于订单的下游业务。


用户点完菜下单后,订单系统会通过发kafka消息给我们系统,系统读取消息后,做业务逻辑处理,持久化订单和菜品数据,然后展示到划菜客户端。


这样厨师就知道哪个订单要做哪些菜,有些菜做好了,就可以通过该系统出菜。系统自动通知服务员上菜,如果服务员上完菜,修改菜品上菜状态,用户就知道哪些菜已经上了,哪些还没有上。这个系统可以大大提高后厨到用户的效率。图片


这一切的关键是消息中间件:kafka,如果它出现问题,将会直接影响到后厨显示系统的用户功能使用。


这篇文章跟大家一起聊聊,我们当时出现过的消息积压问题,希望对你会有所帮助。


1 第一次消息积压


刚开始我们的用户量比较少,上线一段时间,mq的消息通信都没啥问题。


随着用户量逐步增多,每个商家每天都会产生大量的订单数据,每个订单都有多个菜品,这样导致我们划菜系统的划菜表的数据越来越多。


在某一天中午,收到商家投诉说用户下单之后,在平板上出现的菜品列表有延迟。


厨房几分钟之后才能看到菜品。


我们马上开始查原因。


出现这种菜品延迟的问题,必定跟kafka有关,因此,我们先查看kafka。


果然出现了消息积压


通常情况下,出现消息积压的原因有:



  1. mq消费者挂了。

  2. mq生产者生产消息的速度,大于mq消费者消费消息的速度。


我查了一下监控,发现我们的mq消费者,服务在正常运行,没有异常。


剩下的原因可能是:mq消费者消费消息的速度变慢了。


接下来,我查了一下划菜表,目前不太多只有几十万的数据。


看来需要优化mq消费者的处理逻辑了。


我在代码中增加了一些日志,把mq消息者中各个关键节点的耗时都打印出来了。


发现有两个地方耗时比较长:



  1. 有个代码是一个for循环中,一个个查询数据库处理数据的。

  2. 有个多条件查询数据的代码。


于是,我做了有针对性的优化。


将在for循环中一个个查询数据库的代码,改成通过参数集合,批量查询数据。


有时候,我们需要从指定的用户集合中,查询出有哪些是在数据库中已经存在的。


实现代码可以这样写:


public List<User> queryUser(List<User> searchList) {
    if (CollectionUtils.isEmpty(searchList)) {
        return Collections.emptyList();
    }

    List<User> result = Lists.newArrayList();
    searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
    return result;
}

这里如果有50个用户,则需要循环50次,去查询数据库。我们都知道,每查询一次数据库,就是一次远程调用。


如果查询50次数据库,就有50次远程调用,这是非常耗时的操作。


那么,我们如何优化呢?


具体代码如下:


public List<User> queryUser(List<User> searchList) {
    if (CollectionUtils.isEmpty(searchList)) {
        return Collections.emptyList();
    }
    List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
    return userMapper.getUserByIds(ids);
}

提供一个根据用户id集合批量查询用户的接口,只远程调用一次,就能查询出所有的数据。


多条件查询数据的地方,增加了一个联合索引,解决了问题。


这样优化之后, mq消费者处理消息的速度提升了很多,消息积压问题被解决了。


2 第二次消息积压


没想到,过了几个月之后,又开始出现消息积压的问题了。


但这次是偶尔会积压,大部分情况不会。


这几天消息的积压时间不长,对用户影响比较小,没有引起商家的投诉。


我查了一下划菜表的数据只有几百万。


但通过一些监控,和DBA每天发的慢查询邮件,自己发现了异常。


我发现有些sql语句,执行的where条件是一模一样的,只有条件后面的参数值不一样,导致该sql语句走的索引不一样。


比如:order_id=123走了索引a,而order_id=124走了索引b。


有张表查询的场景有很多,当时为了满足不同业务场景,加了多个联合索引。


MySQL会根据下面几个因素选择索引:



  1. 通过采样数据来估算需要扫描的行数,如果扫描的行数多那可能io次数会更多,对cpu的消耗也更大。

  2. 是否会使用临时表,如果使用临时表也会影响查询速度;

  3. 是否需要排序,如果需要排序则也会影响查询速度。


综合1、2、3以及其它的一些因素,MySql优化器会选出它自己认为最合适的索引。


MySQL优化器是通过采样来预估要扫描的行数的,所谓采样就是选择一些数据页来进行统计预估,这个会有一定的误差。


由于MVCC会有多个版本的数据页,比如删除一些数据,但是这些数据由于还在其它的事务中可能会被看到,索引不是真正的删除,这种情况也会导致统计不准确,从而影响优化器的判断。


上面这两个原因导致MySQL在执行SQL语句时,会选错索引


明明使用索引a的时候,执行效率更高,但实际情况却使用了索引b。


为了解决MySQL选错索引的问题,我们使用了关键字force index,来强制查询sql走索引a。


这样优化之后,这次小范围的消息积压问题被解决了。


3 第三次消息积压


过了半年之后,在某个晚上6点多钟。


有几个商家投诉过来,说划菜系统有延迟,下单之后,几分钟才能看到菜品。


我查看了一下监控,发现kafka消息又出现了积压的情况。


查了一下MySQL的索引,该走的索引都走了,但数据查询还是有些慢。


此时,我再次查了一下划菜表,惊奇的发现,短短半年表中有3千万的数据了。


通常情况下,单表的数据太多,无论是查询,还是写入的性能,都会下降。


这次出现查询慢的原因是数据太多了。


为了解决这个问题,我们必须:



  1. 做分库分表

  2. 将历史数据备份


由于现阶段做分库分表的代价太大了,我们的商户数量还没有走到这一步。


因此,我们当时果断选择了将历史数据做备份的方案。


当时我跟产品和DBA讨论了一下,划菜表只保留最近30天的数据,超过几天的数据写入到历史表中。


这样优化之后,划菜表30天只会产生几百万的数据,对性能影响不大。


消息积压的问题被解决了。


最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。


你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


进群方式


添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。


4 第四次消息积压


通过上面这几次优化之后,很长一段时间,系统都没有出现消息积压的问题。


但在一年之后的某一天下午,又有一些商家投诉过来了。


此时,我查看公司邮箱,发现kafka消息积压的监控报警邮件一大堆。


但由于刚刚一直在开会,没有看到。


这次的时间点就有些特殊。


一般情况下,并发量大的时候,是中午或者晚上的用餐高峰期,而这次出现消息积压问题的时间是下午


这就有点奇怪了。


刚开始查询这个问题一点头绪都没有。


我问了一下订单组的同事,下午有没有发版,或者执行什么功能?


因为我们的划菜系统,是他们的下游系统,跟他们有直接的关系。


某位同事说,他们半小时之前,执行了一个批量修改订单状态的job,一次性修改了几万个订单的状态。


而修改了订单状态,会自动发送mq消息。


这样导致,他们的程序在极短的时间内,产生了大量的mq消息。


而我们的mq消费者根本无法处理这些消息,所以才会产生消息积压的问题。


我们当时一起查了kafka消息的积压情况,发现当时积压了几十万条消息。


要想快速提升mq消费者的处理速度,我们当时想到了两个方案:



  1. 增加partion数量。

  2. 使用线程池处理消息。


但考虑到,当时消息已经积压到几个已有的partion中了,再新增partion意义不大。


于是,我们只能改造代码,使用线程池处理消息了。


为了开始消费积压的消息,我们将线程池的核心线程最大线程数量调大到了50。


这两个参数是可以动态配置的。


这样调整之后,积压了几十万的mq消息,在20分钟左右被消费完了。


这次突然产生的消息积压问题被解决了。


解决完这次的问题之后,我们还是保留的线程池消费消息的逻辑,将核心线程数调到8,最大线程数调到10


当后面出现消息积压问题,可以及时通过调整线程数量,先临时解决问题,而不会对用户造成太大的影响。



注意:使用线程池消费mq消息不是万能的。该方案也有一些弊端,它有消息顺序的问题,也可能会导致服务器的CPU使用率飙升。此外,如果在多线程中调用了第三方接口,可能会导致该第三方接口的压力太大,而直接挂掉。



总之,MQ的消息积压问题,不是一个简单的问题。


虽说产生的根本原因是:MQ生产者生产消息的速度,大于MQ消费者消费消息的速度,但产生的具体原因有多种。


我们在实际工作中,需要针对不同的业务场景,做不同的优化。


我们需要对MQ队列中的消息积压情况,进行监控和预警,至少能够及时发现问题。


没有最好的方案,只有最合适当前业务场景的方案。


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。


求一键三连:点赞、转发、在看。


关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。


作者:苏三说技术
来源:juejin.cn/post/7368308963128000512
收起阅读 »

工作7年了,才明白技术的本质不过是工具而已,那么未来的方向在哪里?

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 五一过去了,不知道大家有没有好好的放松自己呢?愉快的假期总是这么短暂,打工人重新回到自己的岗位。 我目前工作7年了,这几年来埋头苦干,学习了很多技术,做了不少系统,也解决过不少线...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


五一过去了,不知道大家有没有好好的放松自己呢?愉快的假期总是这么短暂,打工人重新回到自己的岗位。


我目前工作7年了,这几年来埋头苦干,学习了很多技术,做了不少系统,也解决过不少线上问题。自己虽然在探寻个人IP与副业,自己花了很多时间去思考技术之外的路该怎么走。但转念一想,我宁愿花这么多时间去探索技术之外的路线,但是却从没好好静下来想一下技术本身。


技术到底是什么,你我所处的技术行业为什么会存在,未来的机会在哪里。


因此,我结合自己的工作经历,希望和大家一起聊聊,技术的本质与未来的方向,到底在哪里,才疏学浅,如果内容有误还希望你在评论区指正。


背景


行业现状


互联网行业发展放缓,进入调整阶段,具体表现为市场需求、用户规模、营收利润、创新活力等方面的放缓或下降。


一些曾经风光无限的互联网公司也遭遇了业绩下滑、股价暴跌、裁员潮等困境,你是不是也曾听过互联网的寒冬已至的言论?


其实互联网本身,并没有衰败或消亡,而是因为互联网高速发展的时代过去了。



  1. 中国经济增速放缓、消费升级趋势减弱、人口红利消失等因素的影响,中国互联网市场的需求增长趋于饱和或下降。

  2. 用户规模停滞,智能手机普及率饱和,互联网用户规模增长趋于停滞,由增量市场变为存量市场,互联网获客成本越来越高。

  3. 监管政策收紧,互联网行业规范和监管愈加严格,更加注重合规,因此互联网行业也会收到影响。


供需环境


供需环境变化,应届生要求越来越高,更加注重学历。


社招更是看中学历的同时,开始限制年龄。招聘更看重项目经验,业务经验。五年前,你只要做过一些项目,哪怕不是实际使用的,也很容易拿到offer。而现在企业在看中技术能力的同时,还会关注候选人对与行业的理解,以及以往的工作经验。


技术的本质


先说结论,技术的本质是工具。 我把过去几年的认知变化分成了四个阶段,给大家展示一下我对于技术的认知成长过程。


第一阶段


技术就是应用各类前沿的框架、中间件。


刚毕业时,我就职于一家传统信息企业。谈不上所谓的架构,只需要Spring、Mysql就构建起了我们的所有技术栈。当然,微服务框架更不可能,Redis、MQ在系统中都没使用到。


此时互联网企业已经开始快速发展,抖音诞生区区不过一年。


一线城市的互联网公司,都已经开始使用上了SpringBoot、微服务,还有各类我没有听说过的中间件。


工作环境的闭塞,让我对各类技术有着无限憧憬,因为很多当下难以解决的问题,应用一些新技术、新架构,就能立刻对很多难题降维打击。


举个例子,如果你使用本地缓存,那么集群部署时,你一定要考虑集群的缓存一致性问题,可这个问题如果用上分布式缓存Redis,那么一致性问题迎刃而解。


所以那个时候的我认为,技术就是应用各类中间件,只要用上这些中间件、框架,我就已经走在了技术的前沿。


第二阶段


技术对我而言就是互联网。
半年后,我摆脱传统行业,来到了一个小型互联网公司,用上了不少在我眼中的新技术。


但任何新技术,如果只停留在表面,那么对于使用者来说,就是几个API,几行代码,你很快就会感到厌倦,发现问题也会焦虑,因为不清楚原理,问题就无从排查。


很快,所谓的“新技术”,就不能给我带来成就感了。我开始羡慕那些互联网行业APP,无时无刻都在畅想着,如果我做的产品能够被大家看到并应用,那该是多么有意思的一件事情。


于是我又认为,技术就是做那些被人看见、被人应用的网站、APP。


第三阶段


技术就是高并发、大流量、大数据。
当自己真正负责了某一个APP的后端研发后,很多技术都有机会应用,也能够在AppStore下载自己的APP了,没事刷一刷,看到某一个信息是通过我自己写的代码展示出去,又满足了第二阶段的目标了。


那么我接下来追求的变成了,让更多的人使用我做的产品,起码让我的亲人、朋友也能看到我做的东西。


当然,随之而来的就是日益增长的数据规模和大流量,这些无时无刻都在挑战系统的性能,如何去解决这些问题,成为了我很长一段时间的工作主线。


应对高并发、大流量,我们需要对系统作出各种极致性能的优化。


为了性能优化,还需要了解更多的底层原理,才能在遇到问题时有一个合理的解决方案。


所以,我认为技术就是高并发、大数据,做好这些,才算做好了技术。


第四阶段


经过了传统企业,到互联网公司,再到互联网大厂的一番经历,让我发现技术的本质就是工具,在不同阶段,去解决不同的问题。


在第一阶段,技术解决了各类行业的数据信息化问题,借助各类中间件、架构把具体的需求落地。


在第二阶段、第三阶段,技术解决了业务的规模化问题,因为在互联网,流量迅猛增长,我需要去用技术解决规模化带来的各类的问题,做出性能优化。


当然,技术在其他领域也发挥着作用,比如AI&算法,给予了互联网工具“智能化”的可能,还有比如我们很难接触到的底层框架研发,也就是技术的“技术”,这些底层能力,帮助我们更好的发挥我们的技术能力。


未来机会


大厂仍是最好的选择


即使是在互联网增速放缓、内卷持续严重的今天,即使我选择从大厂离职,但我依然认为大厂是最好的选择。


为什么这么说,几个理由



  • 大厂有着更前沿的技术能力,你可以随意选择最适合的工具去解决问题

  • 大厂有着更大的数据、流量规模,你所做的工作,天然的就具备规模化的能力

  • 大厂有先进的管理方法,你所接触的做事方法、目标管理可能让你疲倦,但工作方法大概率是行业内经过验证的,你不会走弯路,能让你有更快的进步速度


数字化转型


如果你在互联网行业,可能没有听说过这个词,因为在高速发展的互联网行业,本身就是数字驱动的,比如重视数据指标、AB实验等。但在二线、三线城市的计算机行业或者一些传统行业,数字化转型是很大的发展机会。


过去十年,传统行业做的普遍是信息化转型,也就是把线下,需要用纸、笔来完成工作的,转移到系统中。


那什么是数字化转型?



我用我自己的理解说一下,数字化转型就是业务流程精细化管理,数据驱动,实现降本增效。



我目前所在的公司的推进大方向之一,就是数字化转型。因为许多行业的数字化程度非常低,本质而言,就是把数字驱动的能力,带给传统企业,让传统企业也能感受到数字化带来的发展可能。


举个例子,比如一个餐饮系统数字化转型后,一方面可以把用户下单、餐厅接单、开始制作、出餐、上餐线上化,还可以和原材料供应系统打通,当有订单来时,自动检测餐饮的库存信息,库存不足及时提供预警,甚至可以作出订单预测,比如什么时间点,哪类餐品的点单量最高。


当然,数字化转型与互联网有着极大的不同,在互联网行业,你只需要坐在工位,等着产品提出需求就可以了。但是传统行业,你需要深入客户现场,实地查看业务流程,与用户交谈,才能真正的理解客户需求。


或许这样的工作并不炫酷,还需要出差,但在互联网行业饱和的今天,用技术去解决真实世界的问题,也不失为一个很好的选择。


AI&智能化


随着AI快速发展,各类智能化功能已经遍布了我们使用的各类APP,极客时间有了AI自动总结,懂车帝有了智能选车度搜索问题,有时候第一个也会是AI来给我们解答。



任何行业遇上AI都可以再做一遍。



抛开底层算法、模型不谈,但从使用者角度来说,最重要的是如何与行业、场景结合相使用。但是想要做好应用,需要你在行业有着比较深的沉淀,有较深的行业认知。


当然,智能化也不仅限于AI,像上面餐饮系统的例子,如果能够实现订单预测、自动库存管理,其实也是智能化的体现。


终身学习


技术能力


持续精进专业技术能力,相信大家对此都没有疑问。


对于日常使用到的技术,我们需要熟练掌握技术原理,积累使用经验,尤其是线上环境的问题处理经验。


第一个是基础。比如对集合类,并发包,IO/NIO,JVM,内存模型,泛型,异常,反射,等有深入了解,最好是看过源码了解底层的设计。


第二你需要有全面的互联网技术相关知识。从底层说起,你起码得深入了解mysql,redis,nginx,tomcat,rpc,jms等方面的知识。


第三就是编程能力,编程思想,算法能力,架构能力。


在这个过程中,打造自己的技能树,构建自己的技术体系。


对于不断冒出的新技术,我们一方面要了解清楚技术原理,也要了解新技术是为了解决什么问题,诞生于什么背景。


业务能力


前面说到技术是一种工具,解决的是现实世界的问题,如果我们希望更好的发挥技术的作用,那么就需要我们先掌握好业务领域。


互联网领域
如果你想要快速地入门互联网领域的业务,你可以使用AARRR漏斗模型来分析。


AARRR这5个字母分别代表 Acquisition、Activation、Retention、Revenue 和 Refer
五个英文单词,它们分别对应用户生命周期中的 5 个重要环节:获取(Acquisition)、激活(Activation)、留存(Retention)、收益(Revenue)和推荐(Refer)。


AARRR 模型的核心就是以用户为中心,以完整的用户生命周期为指导思想,分析用户在各个环节的行为和数据,以此来发现用户需求以及产品需要改进的地方。


举一个简单的例子,我们以一个互联网手游 LOL来举例:
获取就是用户通过广告、push等形式,了解到了游戏并注册或者登陆。
激活就是用户真正的开始游戏,比如开始了一场匹配。
留存就是用户在7天、30天内,登陆了几次,打了几把比赛,几天登陆一次,每日游戏时常又是多少。
收益,用户购买皮肤了,产生了收益。
推荐,用户邀请朋友,发送到微信群中,邀请了朋友一起开黑。


如果你所在的行业是C端产品,那么这个模型基本可以概括用户的生命周期全流程。


传统行业
传统行业没有比较通用的业务模型,如果想要入手,需要我们从以下三个角度去入手



  1. 这个行业的商业模式是什么,也就是靠什么赚钱的?比如售卖系统收费,收取服务费等

  2. 行业的规模如何?头部玩家有哪些?它们的模式有哪些特色?

  3. 这个行业的客户是谁、用户是谁?有哪些经典的作业场景?业务操作流程是什么样的?


如何获取到这些信息呢?有几种常见的形式



  1. 权威的行业研究报告,这个比较常见

  2. 直接关注头部玩家的官网、公众号、官媒

  3. 深入用户现场


我们以汽车行业来举例
商业模式:整车销售、二手车、汽车租赁等,细分一点,又有传统动力和新能源两种分类。
规模:如下图


头部车企:传统的四大车企一汽、东风、上汽、长安,新势力 特斯拉、蔚小理


经典场景:直接去4S店体验一下汽车销售模式、流程


说在最后


好了,文章到这里就要结束啦,我用我自己工作几年的不同阶段,给你介绍了我对于技术的本质是工具的思考过程,也浅浅的探寻了一下,未来的发展机会在哪里,以及我们应该如何提升自己,很感谢你能看到最后,希望对你有所帮助。




作者:东东拿铁
来源:juejin.cn/post/7365679089812553769
收起阅读 »

面试官:“你知道什么情况下 HTTPS 不安全么”

面试官:“HTTPS的加密过程你知道么?”我:“那肯定知道啊。”面试官:“那你知道什么情况下 HTTPS 不安全么”我:“这....”越面觉得自己越菜,继续努力学习!!!什麽是中间人攻击?中间人攻击(MITM)在密码学和计算机安全领域中是指攻击者与通讯的两端分...
继续阅读 »

面试官:“HTTPS的加密过程你知道么?”

我:“那肯定知道啊。”

面试官:“那你知道什么情况下 HTTPS 不安全么”

我:“这....”

越面觉得自己越菜,继续努力学习!!!


什麽是中间人攻击?

中间人攻击MITM)在密码学计算机安全领域中是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制[1]。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。在许多情况下这是很简单的(例如,在一个未加密的Wi-Fi 无线接入点的接受范围内的中间人攻击者,可以将自己作为一个中间人插入这个网络)。

一个中间人攻击能成功的前提条件是攻击者能将自己伪装成每一个参与会话的终端,并且不被其他终端识破。中间人攻击是一个(缺乏)相互认证的攻击。大多数的加密协议都专门加入了一些特殊的认证方法以阻止中间人攻击。例如,SSL协议可以验证参与通讯的一方或双方使用的证书是否是由权威的受信任的数字证书认证机构颁发,并且能执行双向身份认证。

以上定义来自维基百科,我们来举一个通俗的例子来理解中间人攻击:

image.png

  1. A发送给B一条消息,却被C截获:

A: “嗨,B,我是A。给我你的公钥”

  1. C将这条截获的消息转送给B;此时B并无法分辨这条消息是否从真的A那里发来的:

C: “嗨,B,我是A。给我你的公钥”

  1. B回应A的消息,并附上了他的公钥:

B -> B 的公钥 -> C

  1. C用自己的密钥替换了消息中B的密钥,并将消息转发给A,声称这是B的公钥:

C -> C 的公钥 -> A

  1. A 用它以为是 B的公钥,加密了以为只有 B 能看到的消息

A -> xxx -> C

  1. C 用 B 的密钥进行修改

C -> zzz -> B

这就是整个中间人攻击的流程。

中间人攻击怎么作用到 HTTPS 中?

首先让我来回顾一下 HTTPS 的整个流程:

回顾 HTTPS 过程

image.png

这是 HTTPS 原本的流程,但是当我们有了 中间人服务器之后,整个流程就变成了下面这个样子。

这个流程建议动手画个图,便于理解

  1. 客户端向服务器发送 HTTPS 建立连接请求,被中间人服务器截获。
  2. 中间人服务器向服务器发送 HTTPS 建立连接请求
  3. 服务器向客户端发送公钥证书,被中间人服务器截获
  4. 中间人服务器验证证书的合法性,从证书拿到公钥
  5. 中间人服务器向客户端发送自己的公钥证书

注意!在这个时候 HTTPS 就可能出现问题了。客户端会询问你:“此网站的证书存在问题,你确定要信任这个证书么。”所以从这个角度来说,其实 HTTPS 的整个流程还是没有什么问题,主要问题还是客户端不够安全。

  1. 客户端验证证书的合法性,从证书拿到公钥
  2. 客户端生成一个随机数,用公钥加密,发送给服务器,中间人服务器截获
  3. 中间人服务器用私钥加密后,得到随机数,然后用随机数根据算法,生成堆成加密密钥,客户端和中间人服务器根据对称加密密钥进行加密。
  4. 中间人服务器用服务端给的证书公钥加密,在发送给服务器时
  5. 服务器得到信息,进行解密,然后用随机数根据算法,生成对称加密算法

如何预防?

刚才我们说到这里的问题主要在于客户端选择信任了,所以主要是使用者要放亮眼睛,保持警惕

参考文章:


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

Shadcn UI 现代 UI 组件库

web
前言 不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules...
继续阅读 »

image.png


前言


不知道大家是否使用过 Shadcn UI,它在Github 上拥有了 35k star,它与大多数 UI 组件库(如 Ant desgin 和 Chakra UI)不同,一般组件库都是通过 npm 的方式给项目使用,代码都是存在 node_modules 中,而 Shadcn UI 可以将单个 UI 组件的源代码下载到项目源代码中(src 目录下),开发者可以自由的修改和使用想要的 UI 组件,它已经被一些知名的网站(vercel.combestofjs.org)等使用。那么它到底有什么优势呢? 一起来来探讨下。


Shadcn UI 介绍


Shadcn UI 实际上并不是组件库或 UI 框架。相反,它是可以根据文档“让我们复制并粘贴到应用程序中的可复用组件的集合”。它是由 vercel 的工程师Shadcn创建的,他还创建了一些知名的开源项目,如 TaxonomyNext.js for DrupalReflexjs


Radix UI - 是一个无头 UI 库。也就是说,它有组件 API,但没有样式。Shadcn UI 建立在 Tailwind CSS 和 Radix UI 之上,目前支持 Next.js、Gatsby、Remix、Astro、Laravel 和 Vite,并且拥有与其他项目快速集成的能力——安装指南


Shadcn UI 功能特点


多主题和主题编辑器



在 Shadcn UI 的官网上有一个主题编辑器,我们可以点击 Customize 按钮实时切换风格和主题颜色,设计完成后,我们只需要拷贝 css 主要变量到我们的程序中即可。 下图是需要拷贝的 css 颜色变量。



颜色使用 hls 表示,主题变量分为背景色(background) 和 前景色(foreground),Shadcn UI 约定 css 变量省略 background,比如 --card 就是表示的是 card 组件的背景颜色。


深色模式


可以看到复制的 css 变量支持生成深色模式,如果你使用 react, 可以使用 next-themes,这个包来实现主题切换,当然也可以通过 js 在 html 上切换 dark 这个样式来实现。 除了 react 版,社区还自发实现了 vuesvelte 版本


CLI


除了手动从文档中复制组件代码到项目中,还可以使用 cli 来自动生成代码



  • 初始化配置


npx shadcn-ui@latest init



  • 添加组件


npx shadcn-ui@latest add


按空格选择想要的组件,按回车就会下载选中的 UI 组件代码



下载的源码在 components/ui 目录下,并且自动安装 Radix UI 对应的组件。


丰富的组件库


Shadcn UI 拥有丰富的组件,包括 常见的 Form、 Table、 Tab 等 40+ 组件。





使用 Shadcn UI 创建登录表单


接下来我们一起实战下,使用 Shadcn UI 创建登录表单, 由于 Shadcn UI 是一个纯 UI 组件,对于复杂的表单,我们还需要使用 react-hook-form 和 zod。


首先下载 UI


npx shadcn-ui@latest add form

安装 react-hook-form 以及 zod 验证相关的包


yarn add add react-hook-form zod @hookform/resolvers

zod 用于格式验证


下面代码是最基本的 Form 结构


import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>

)}
/>


  • FormField 用于生成受控的表单字段

  • FormMessage 显示表单错误信息


登录表单代码


"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"

import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
email: z.string().email({message:'邮箱格式不正确'}),
password: z.string({required_error:'不能为空'}).min(6, {
message: "密码必须大于6位",
}),
})

export default function ProfileForm() {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
},
})

// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 w-80 mx-auto mt-10">
<FormField
control={form.control}
name="email"
render={({ field }) =>
(
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input placeholder="请输入邮箱" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) =>
(
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input placeholder="请输入密码" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">登录</Button>
</form>
</Form>

)
}


展示效果



小结


与其他组件库相比,Shadcn UI 提供了几个好处。



  • 易用性:使用复制和粘贴或 CLI 安装方法可以轻松访问其组件.

  • 可访问性:Shadcn UI 的组件是完全可访问的,并符合 Web 内容可访问性指南 (WCAG) 标准,它支持屏幕阅读器、键盘导航和其他辅助设备。

  • 灵活和可扩展性:Shadcn UI 只会下载需要使用的组件在源码中,并且开发者可以灵活定制和修改。


当然需要手动拷贝安装每一个组件可能是一件麻烦的事情,这也会导致源码量的增加,因此是否使用 Shadcn UI 还得开发者自行决定,总的来说 Shadcn UI,我还是非常看好,我将配合 next.js 在一些新项目中使用。


作者:狂奔滴小马
来源:juejin.cn/post/7301573649328668687
收起阅读 »

产品经理:为什么你做的地图比以前丝滑了许多?

web
从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。照常理来说,地图相...
继续阅读 »

从业多年第一次接触地图相关的需求,开发过程中产生了一些思考,遂记录下来,欢迎讨论

Vue3 + 高德地图 JS API 2.0 + 高德地图 AMapUI组件库

近两年前端大家是真的不好混,在职的人呢被极限压榨,待业的人呢投简历都是【未读不回】。

1.gif

照常理来说,地图相关的需求都是由组内的地图大佬负责的,但眼瞅着公司里前端同学越来越少,这“泼天的富贵”终于有一天也落到了我头上。

需求的内容倒是很简单:要在地图上绘制一些轨迹点和一条轨迹线,以及一个目标点KeyPoint,让使用者来审查轨迹线是否经过KeyPoint,以及系统中记录KeyPoint的信息是否正确。当轨迹未经过 或 KeyPoint信息不正确时,会再提供一些辅助点SubPoint供用户选择,替换掉KeyPoint。(轨迹点也属于一种SubPoint

本着能CV就不手写的原则,我打开了项目代码(Vue2)寻找之前类似的地图需求,看看能不能套用一下然后快速下班,结果我看到了若干个大几千行的文件,以及这样的渲染效果(轨迹点上的箭头表示当前移动的方向):

1.png

2.png

大哥喂,咱就是说,方向盘打不正的话要抓紧去修,上路是要出事故的 3.jpg

得,言归正传,且不说那加起来几万行的代码我能不能捋顺喽,就是这个效果,干脆我还是用Vue3重新实现一下吧。

别忘了,前端的老本行是什么


业务的关注点

开始之前,我们先思考一个问题:业务的关注点是什么?

想明白了这个问题,我们在设计地图样式以及一些交互细节时,才能有更好的针对性。

(让我看看有多少人是默认样式+内置组件一把梭的)

image.png

ok那既然涉及到了地图,归根结底我们的关注点无非是这三方面:

  • 线
  • 区域

如果按照关注点的归属粗略的分为两类:外部添加的地图自身的

当业务更关注外部添加的元素时(如maker、轨迹),随着地图缩放、地形改变、POI显隐,我们添加的元素是否始终有一个比较醒目的显示效果?

当业务更关注地图自身的元素时(如兴趣点),对于POI的 pick 动作,是否贴合业务流程?是否足够智能与便捷?(可参考高德自己的效果)

这里针对第一类推荐两个初始化地图的可选配置项:

  1. features:地图显示要素(查看效果
  2. mapStyle:地图主题(查看效果

相信很多人可能都没关注过这两个配置项,而这两个东西组合起来使用,不仅能使你添加的外部元素始终处于一个高醒目的level,也可以与你项目本身的风格主题更搭,如何抉择,诸君自行思量。

(浅浅吐槽一下,高德提供的功能和配置项非常丰富,但文档真的是一言难尽。。。一样的功能在不同的地方都有文档,有些内容还不一致)

4.png

选择画点的方法

当你大概明白自己要做什么样的地图之后,让我们稍微进入一点正题:怎么选择合理的画点方法?

高德提供了哪些画点的方法呢?

  • JS API
    1. 默认点标记Marker
    2. 圆形标记CircleMarker
    3. 灵活点标记ElasticMarker
    4. 海量标注LabelMarker:需要维护图层、维护避让等级、自定义样式实现起来比较麻烦
    5. 海量点标记MassMarker:无法显示文字label
    6. 点聚合
      • 按距离聚合MarkerCluster:需要维护权重
      • 按索引聚合IndexCluster:需要维护索引规则
  • JS API UI组件库
    1. 简单标注SimpleMarker
    2. 字体图标标注AwesomeMarker
    3. 矢量标注SvgMarker
    4. 海量点PointSimplifier:可使用Canvas

至于像文本标记、折线、多边形等等一些通过某些黑科技实现类似点标记的方法(GPT说的),和Native端的画点方法,不在本文的讨论范围中。


美国五星上将麦克阿瑟曾说过,一切抛开实际背景去讨论问题的行为都是耍流氓。

画点的方法找到了很多,那我们要画什么样的点呢?

1. 从数量上看

动辄上万

为什么我要先看数量呢,因为可自定义样式的画点方法很多,但是要支持大数量级渲染且性能良好,就把上边一多半方法给pass掉了。

还剩下这些可供选择:海量标注LabelMarker海量点标记MassMarker按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier

2. 从样式上看

需要自定义。从上边的截图中可以看出,点的形状为圆形,黑边黄底,中心有个箭头,且整体随着当前运动方向有一个rotate deg

上述画点方法至少都支持(图片 或 HTML String 或 CSS Style)中的一种方式,而这三种方式理论上也都能实现我们想要的效果,所以下一个。

3. 特性

test.gif

虽然我们需要关注轨迹点,但并不是所有状态下都需要。比如在地图的缩放等级很小时(看到的是省、国家级别),并不需要把每一个轨迹点都展示出来。所以可以看到,之前的实现效果中,放大缩小都会重新适应尺寸,并且临近的点有自动合并的效果。

海量标注LabelMarker海量点标记MassMarker退出了游戏,他俩是全量绘制并且没有外部接入的话点是始终展示的。

至此,只有按距离聚合MarkerCluster按索引聚合IndexCluster海量点PointSimplifier三者进入了决赛圈。

现在来综合对比一下这三种方法:

方法1w+点渲染性能自定义样式合并逻辑
按距离聚合MarkerCluster渲染迅速,操作不卡顿HTML String或图片距离+权重就近合并
按索引聚合IndexCluster渲染迅速,操作不卡顿HTML String或图片距离+索引分组合并
海量点PointSimplifier渲染迅速,操作不卡顿Canvas或图片TopN

渲染方面在1w+点的竞赛中大家表现得都不错,官网示例中心可以看到,这里不再赘述。

自定义样式则有三种途径,图片、HTML字符串和新出现的Canvas。图片和Canvas比较简单,我们先讲一讲这个HTML字符串,也就是原生的HTML。

假如你用了某个现代化的前端框架在开发你的系统,用到了高德地图,并且想画一些漂漂亮亮的点在你需要标注的地方。在翻阅了文档之后,发现似乎直接传入HTML字符串这种方法是最快的,于是你开开心心的输入了一个

My Marker
试试水,接着,保存、等待hot reload,并把期待的目光投向了屏幕...

353ad3a27c3ac95c86c30e66f1b4f15.png

好家伙,这小Marker不仔细看,还真有点找不到呢...你决定继续添加一些样式

2M2we.gif

之后代码可能逐渐变成了这样...

微信图片_20240514153858.png

你:

WzgGg.png

把手指从ctrl C V三个键拿下来之后,你陷入了沉思:我能不能用XXX UI组件库来自定义Marker?

答案当然是肯定的~下面请允许我用Vue@3.3.4 + ant-design-vue@3.2.20来做个示范~~

首先,他接收的是HTML字符串,所以直接传进去一个vue组件肯定是不行的

import MyComponent from './MyComponent.vue'

// 以普通点标记举例
new Marker({
content: MyComponent,
// ...other configs
})

// not work

所以我们要做的就是把MyComponent给转成原生HTML,最简单的办法当然就是Vue实例的mount()API啦:

MyComponent作为根组件创建一个新的Vue实例

import {createApp} from 'vue'
import MyComponent from './MyComponent.vue'

const app = craeteApp(MyComponent)

将实例挂载到一个div上,得到原生的HTML

const div = document.createElement("div")
app.mount(div)

console.log('div: ', div)

打印一下:

223.png

使用:

// 以普通点标记举例
const marker = new Marker({
content: div,
// ...other configs
});

效果图就不放啦,有几个注意的点要提一下:

  1. 最重要的放在最前边:如果你的点在整个页面的生命周期内仅会绘制一次,那你可以跳过这一条。否则一定要记得app.unmount()。一种比较好的实践是,把点数据画点的方法移除点的方法写进一个hook里。
// example
import { createGlobalState } from "@vueuse/core";
import { ref, createApp } from "vue";
import Map from './Map.js' // 地图实例
import MyComponent from "./MyComponent.vue";

export const useCustomPoints = createGlobalState(() => {
const pointData = ref([]);
const removePointsCb = [];

const setPoint = () => {
const data = pointData.value.map(point => {
const div = document.createElement("div");
const app = createApp(MyComponent);
app.mount(div);

removePointsCb.push(() => app.unmount()) // 清除图标实例的回调

// 以普通点标记举例
const marker = new Map.Constructors.Marker({
map: Map,
content: div,
// ...other config
});

return marker
})

Map.add(data); // 将点添加到地图上

removePointsCb.push(() => Map.remove(data)); // 移除点的回调
}

const removePoints = () => {
while (removePointsCb.length) {
removePointsCb.pop()();
}
};

return {
pointData,
setPoint,
removePoint
}
})
  1. 可以通过createApp的第二个参数传递props进去,这些props是响应式的
  2. 新创建的Vue实例与你项目自身的实例不共享全局的配置,比如路由组件Store等,需要单独配置
  3. Vue2以及其他的一些框架,实现思路类似

好了,言归正传。

看起来似乎三种方法都可以实现需求,但是仔细翻看点聚合方法的文档,发现使用图片自定义点时没有提供旋转的配置,也就是说我们可能需要准备n张图片(取决于你想实现角度渲染的精确度),不,这太不优雅了。而如果使用原生HTML去自定义,要么接受丑炸的效果(纯手工css),要么面临着卡顿的风险(大量的app实例)

没办法,只好被(xin)迫(ran)接受用海量点PointSimplifier的Canvas去做了~毕竟能用Canvas画就约等于能画一切嘛~~

抱着视死如归的心情去翻了一下JS API UI组件库的海量点PointSimplifiercanvas绘制function文档,发现了一句了不得的话:

微信图片_20240514173923.png

划重点:通常只是描绘路径尽量不要fill或者stroke引擎自己一次性

翻译:该函数通常只是描绘路径,但是也能描绘形状。尽量不要fill或者stroke,除非你能搞明白我们的描绘机制。所有点的路径描绘完成后,引擎自己会在尾部调用fill以及stroke,一次性绘出所有路径,所以你要注意尾部的这次操作,避免冲突。

三个字总结海量点PointSimplifier的描绘机制就是:连笔画

不是每个点都创建一个新的Canvas画布,绘制完成后立即渲染;而是所有的点都共用一个Canvas画布,以当次你能绘制的区域坐标作为参数,重复绘制n(点的数量)次,最后一把全渲染出来。

微信图片_20240515171523.png

微信图片_20240516093003.png

明白了这个,我们在书写function的逻辑的时候,只要注意保证每次绘制开始、结束时笔触的落点和绘制上下文状态即可。绘制一个有旋转角度的、中间有箭头的圆形(圆形的背景色是通过海量点PointSimplifier的lineStyle配置的),示例代码如下:

由于叠加了变换,处理状态时偷懒使用了save()、restore()

renderOptions: {
// 这里使用了样式分组引擎:https://lbs.amap.com/demo/amap-ui/demos/amap-ui-pointsimplifier/group-style-render
// 以点的旋转角作为组id入参,方便操作
// 无需分组时,renderOptions.pointStyle.content = renderOptions.groupStyleOptions.pointStyle.content 逻辑一致
groupStyleOptions: function (gid) {
return {
pointStyle: {
content: function (ctx, x, y, width, height) {
// 存了一个坐标,画箭头的时候用
const startX = x + width / 2;
const startY = y + height / 4;

// 移动到画布的最右侧、中间位置
ctx.moveTo(x + width, y + height / 2);

// 画圆
ctx.arc(
x + width / 2,
y + height / 2,
width / 2,
0,
Math.PI * 2,
true
);

// 变换前保存一下状态
ctx.save();

// 以圆心为旋转的中心点
ctx.translate(x + width / 2, y + height / 2);
// 按照轨迹方向旋转
ctx.rotate((Math.PI / 180) * gid);
// 重置中心点
ctx.translate(-(x + width / 2), -(y + height / 2));

// 画箭头
ctx.moveTo(startX, startY);
ctx.lineTo(x + width / 4, y + height / 2);
ctx.moveTo(startX, startY);
ctx.lineTo(startX, y + (height * 3) / 4);
ctx.moveTo(startX, startY);
ctx.lineTo(x + (width * 3) / 4, y + height / 2);

// 由于箭头需要在旋转的状态下绘制,所以在箭头绘制完成后再恢复状态
ctx.restore();
},
},
};
},
}

来一个无旋转时的笔触顺序动图,我尽力了6j4l.png

test.gif

画完之后,看一下对比效果:




OK,点画出来了。

上边特性中有提到:当我们距离很远时,就不需要再关注某个具体的轨迹点。所以可以再进一步优化,当地图的缩放等级zoom小于某个阈值时,清空point:

import {computed, watch, ref} from 'vue'
import {Map, PointSimplifierIns} from 'Map.js' // 地图实例、海量点实例

const zoom = ref(null);

const showPoint = computed(() => zoom.value > 10);

const pointData = ref([ /* ...赋值逻辑省略 */]);

Map.on("zoomchange",
debounce(() => {
zoom.value = Map.getZoom();
}, 200)
);

watch(showPoint, (show) => {
PointSimplifierIns.setData(show ? pointData.value : []);
})

效果如下:

test.gif

控制显示隐藏没有用自带的show()hide()方法,而是选择直接重设数据源,是因为:海量点PointSimplifiershow状态下时对地图进行缩放,会自动重绘适应尺寸;hide状态下则不会。从show变为hide时,会保存当前zoom下点的尺寸,供下次hideshow时用。如果地图缩放的太快,当前的zoom与上次保存尺寸时的zoom跨度太大,可能会导致点位不匹配现象。


选择画轨迹的方法

画线的选择过程就简单了很多,之前需求中是用折线Polyline实现的,画出来的效果总感觉差点意思,所以就去翻了翻高德的文档,共找到常规画线方法3种:

  • JS API
    1. 折线Polyline
    2. 贝塞尔曲线BesizerCurve
  • JS API UI组件库
    1. 轨迹展示PathSimplifier

基本上毫无疑问了嘛~我们本身就是要画轨迹,还有什么好选的~~ 必须用轨迹展示

不过这里还是分享一些对三种方法实际体验之后的感受:

  1. 折线Polyline:无法识别线上的点。如果轨迹数据没有经过噪点清除,画出来之后在细节处会有比较严重的锯齿。不过整体上感觉,倒也不是不能用~
  2. 贝塞尔曲线BesizerCurve:无法识别线上的点。但理论上是唯一可以绘制出完全符合真实运动轨迹的、贴合地图路线的方法了,代价也是相当的大——至少要在原本轨迹点的基础上额外维护n-1个控制点,放弃~~
  3. 轨迹展示PathSimplifier:性能好,相同数据量下的显示效果要比折线画出来的平滑许多。以及来自官网的优点罗列:
    • 使用Simplify.js进行点的简化处理,提高性能
    • 支持识别线上的点做信息展示
    • 内置巡航器
    • 样式配置更加丰富

实现过程比较简单,照着文档撸就行,可以对比下折线和轨迹展示两种方式,在拐角细节处的差异:

折线


轨迹展示


小tips: 适当增加线宽lineWidth可以有效的缓解锯齿现象


Loading的区域与时机

当我第一次打开上文提到的老版本地图页面时,除了渲染效果不够理想外,最大的一个感受就是:Loading太长

不是想像中那样常规的:打开页面,给一个满屏Spin等待加载各种数据、等待绘制点、线的动作,所有准备工作完成后,取消Spin允许用户开始操作。

咱就是说,像这样的交互逻辑,其实也没啥问题。毕竟谁还没个业务繁忙的时候,最简单最原始最暴力的满屏Spin虽然在体验上不尽如人意,但我觉得是符合上线标准的。

但您猜我看到了什么?

微信图片_20240520171349.png

Form、Map、Action Bar三个区域各自一个小Spin,整体有个大Spin,可以透过大Spin的透明遮罩层看到下面的小spin们反复交替进行,以及大Spin自己也时不时的闪现一下...

7D91.gif

Spin为什么会闪现?回到需求当中来:

地图上点、线的绘制依赖了多个数据源

  • 轨迹点、轨迹线数据源
  • KeyPoint数据源
  • SubPoint数据源
    • Type 1
    • Type 2

这些接口一部分是并发请求,但也有个别的接口请求参数依赖于其他接口的返回值

以及,使用高德地图提供的API绘制点、线时,也共用了接口请求时的Spin。

还有诸多类似这样的代码:

setTimeout(() => {
loading = false
}, 2000)

对渲染流程管理混乱、对数据流向不了解、对自己代码不自信,故意延迟loading的结束时机,防止用户过早操作导致报错


const interval = setInterval(() => {
if(conditon) {
clearInterval(interval);
loading = false
}
}, 1000)

依赖第三方的内容加载,或将多个小loading合并为一个大loading


最终的结果就是让人一整个loading住...

pj7dW.png

而我做了哪些改变

首先,将单个loading覆盖的区域尽可能的缩小

举个例子,上边提到的Action Bar,假设里边既有展示KeyPoint信息的列表,又有展示所有SubPoint信息的列表。在之前的处理方案中,Action Bar区域只有一个整体的Spin,所以整个区域loading的流程大概是:

%%{init: { 'theme': 'base', 'themeVariables': {
'cScale0': '#996666', 'cScaleLabel0': '#ffffff',
'cScale1': '#996633','cScaleLabel1': '#ffffff',
'cScale2': '#999999', 'cScaleLabel2': '#ffffff'
}}}%%

timeline
title Loading 状态

section 阶段一
show : request for KeyPoint data
hide : request success

section 阶段二
show : request for SubPoint type1 data
hide : request success

section 阶段三
show : request for SubPoint type2 data
hide : request success

而我则是把每个数据源对应的列表都单独分配了一个loading组件

聪明的看官老爷可能会问,同时存在多个Spin,不也很奇怪吗?

test.gif

所以我选择了骨架屏Skeleton作为loading组件:

test.gif

受gif图的帧率影响,实际效果还是很丝滑的。(但使用骨架屏时也有一个注意的点:骨架屏的占位高度需要配置段落占位图行数来调整,避免loading结束时真实的渲染内容与骨架屏高度相差太大产生视差)

然后,在Map区域用其他形式的提示代替传统的loading

与上边类似,Map区域不仅同时用到了KeyPointSubPoint数据源,而且在绘制点、线时也有loading。并且也是一个整体的Spin,你应该能想象出每次数据初始化时,Map上闪来闪去的Spin。。。

地图本身,是高德提供出来可以开箱即用的组件,我们所添加的点、线只是附加属性,并不应该使用整体的Spin遮罩阻止用户使用地图的其他功能。在某些附加属性成功添加之前,用户只需要知道与之相关的功能是不可用状态即可。

我的方案是:图例化提供一个loading-box,里边展示了每个数据源的加载状态

test.gif

为了不遮挡地图,loading-box不是始终展示的,基础显示逻辑是:

  1. watch监听loadings 数组
  2. 只要有一个数据源loading中,则显示。
  3. 全部数据源都不在loading中,则debounce n秒后隐藏。

显示动作是实时的,只要有一个数据源在loading中,就应该立刻让用户感知到。

而隐藏动作如果也是实时的,loading-box的显隐切换会比较频繁,显得很突兀。

  • 如果使用setTimeout做延时,期望是发出hide指令n秒后执行,但无法保证n秒后没有新的loading正在进行,导致显隐切换逻辑紊乱。
  • 如果使用throttle做延时,导致的问题与setTimeout相同,只是发生概率会小一些。
  • 相比之下debounce最适合做这个场景的解决方案。

再结合上边提到的hook写法,把loading状态也放进去,方便loading-box使用:

// example
import { createGlobalState } from "@vueuse/core";
import { ref, createApp } from "vue";

export const useCustomPoints = createGlobalState(() => {
const pointData = ref([]);
const pointLoading = ref(false);
const removePointsCb = [];

const getPoint = async () => {
pointLoading.value = true;
const data = await requestPointData();
pointLoading.value = false;

pointData.value = data;
}

const setPoint = () => {
// ...
}

const removePoint = () => {
// ...
};

return {
pointData,
pointLoading,
getPoint,
setPoint,
removePoint
}
})
// loading-box.vue

<script setup>
import { watch } from 'vue';
import { useCustomPoints } from 'useCustomPoints.js';

const { pointLoading } = useCustomPoints();

watch([pointLoading, /* and other loadings */], () => {
// do loading-box show/hide logic
})
script>

应用了上述loading相关的优化后,虽然跟核心业务逻辑相关的代码改动几乎为0,但用户的体验却有相当大的提升,究其原因:

在老版本的实现中,因为全屏Spin的存在,任何一项页面准备工作完成前,页面都无可交互区域;拆分loading后,把一大部分无可交互区域的时间变成了局部可交互区域的时间,甚至在Map模块替换了loading的形式,完全避免了Spin遮罩层这种阻隔用户的效果。加上Spin动画本身的耗时、显示/隐藏Spin的耗时,积少成多,产生质变。

image.png

可以看到,在局部loading耗时完全一样的情况下,老版本中:

无可交互区域时间 = 全屏Spin时间 = 局部loading的最大时间

而在新版本中:

无可交互区域时间 = 几个all loading片段的时间之和

而这,也是一些复杂应用做体验优化的思路之一。


结语

okok,先写到这,毕竟马上就要下班了

1b006c53e15945a09253df289b6192cc~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.awebp

没什么高大上理论也没什么八股文,只是一个从业多年一事无成小前端在重构需求时的一些感想~~

还是那句话,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


彩蛋

文章标题来自需求上线后,产品经理的真实评价

image.png


作者:Elecat
来源:juejin.cn/post/7371633297153687606
收起阅读 »

2024 前端趋势:全栈也许已经是必选项

web
过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。 React 与 Vue 生态对比 首先,我们来看看 React 与 Vue 生态的星趋势对比: 上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多...
继续阅读 »

过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。


React 与 Vue 生态对比


首先,我们来看看 React 与 Vue 生态的星趋势对比:


截屏2024-02-29 10.05.39转存失败,建议直接上传图片文件


上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多。我们将其做一个分类:


排名ReactVue
1UI全栈
2白板演示文稿
3全栈后台管理系统
4状态管理hook
5后台管理系统UI
6文档文档
7全栈框架集成UI
8全栈框架UI框架
9后台管理系统UI
10无服务栈状态管理

可以看到 React 这边的生态链基本成熟,几乎每一个分类都有一个上榜的库,不再像 Vue 那样还在卷 UI 框架。


在全栈方面,Vue 的首位就是全栈 Nuxt。


React 的 Next.js 虽然不在首位,但是服务端/全栈相关的内容就占了 4 个,其中包含第 10 名的无服务栈。另外值得注意的是,React 这边还有服务端组件的概念。Shadcn/ui 能占到第一位,因为它基于无头 UI Radix 实现的,在服务端组件也能运用。所以,服务端/全栈在 React 中占的比重相当大的。


这样看来,前端往服务端进发已经成为一个必然趋势。


htmx 框架的倒退


再看看框架这边,htmx 在星趋势里,排行第二位,2023增长的星星数为 15.6K,与第一位的 React 颇为相近。


而 htmx 也是今年讨论度最高的。


在我经历过前后端不分离的阶段中,使用 jsp 生成前端页面,js 更多是页面炫技的工具。然后在 jQuery + Ajax 得到广泛应用之后,才真正有前后端分离的项目。


htmx 的出现,不了解的人,可能觉得是倒退到 Java + jQuery + Ajax 的前后端分离状态。但是,写过例子之后,我发现,它其实是倒退到了前后端不分离的阶段。


用 java 也好,世界上最好的 php 也好,或者用现在的 nodejs 服务,都能接入 htmx。你只要在服务端返回 html 即可。


/** nodejs fastity 写的一个例子 **/
import fastify from 'fastify'
import fastifyHtml from 'fastify-html'
import formbody from '@fastify/formbody';

const app = fastify()
await app.register(fastifyHtml)
await app.register(formbody);
// 省略首页引入 htmx

// 首页的模板,提供一个按钮,点击后请求 html,然后将请求返回的内容渲染到 parent-div 中
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
return reply.html`

Hello ${name}


`
, reply
})

// 请求返回 html
app.post('/clicked', (req, reply) => {
reply.html`

Clicked!

`
;
})

await app.listen({ port: 3000 })

也许大家会觉得离谱,但是很显然,事情已经开始发生了变化,后端也来抢前端饭碗了。


截屏2024-02-29 10.32.24.png


htmx 在 github 上已经有不少跟随者,能搜出前端代码已有不少,前三就有基于 Python 语言的 Django 服务端框架。


jQuery 见势头不错,今年也更新了 4.0 的 beta 版本,对现代浏览器提供了更好的支持。这一切似乎为旧架构重回大众视野做好了准备。


企业角度


站在企业角度来看,一个人把前后端都干了不是更好吗?


的确如此。前后端一把撸更符合企业的利益。国外的小公司更以全栈作为首选项。


也许有人觉得国情不同,但是在我接触的前端群里,这两年都有人在群里说他们公司前后端分离的情况。


还有的人还喜欢大厂那一套,注意分工合作,但是其实大厂里遗留项目也不少,有的甚至是 php;还有新的实验项目,如果能投入最少人力,快速试错,这种全栈的框架自然也是最优选择。


我并不是说,前后端分离不值得。但是目前已经进入 AI 赛道,企业对后台系统的开发,并不愿意投入更多了。能用就行已经成为当前企业的目标,自然我们也应该跟着变化。


全栈破局


再说说前端已死的论调。我恰恰觉得这是最好做改变的时机。


在浏览器对新技术支持稳定,UI 框架趋同,UI 组件库稳定之后,前端不再需要为浏览器不兼容素手无策了,不再需要苦哈哈地为1个像素争辩不停了,也不再需要为产品莫名其妙的交互焦头烂额了。


这并不意味着前端已死,反而可能我们某个阶段的任务完成了,后面有更重要的任务交给我们。也许,全栈就是一个破局。


在云服务/云原生如此普遍的情况下,语言不再是企业开发考虑的主要因素,这也为 nodejs 全栈铺平了道路。


前端一直拣最苦最脏的话来做,从 UI 中拿到了切图的工作,然后接手了浏览器兼容的活,后来又从后端拿到了渲染页面的工作。


那我们为何不再进一步,主动把 API 开发的工作也拿过来?


作者:陈佬昔没带相机
来源:juejin.cn/post/7340603873604599843
收起阅读 »

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

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

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

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

1.png

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

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

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

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

2.png

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

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

3.png

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

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

4.png

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

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

收起阅读 »

8个小而美的前端库

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

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


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


radash


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



use-debounce


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



timeago.js


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


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

react-use


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



dayjs


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



filesize


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


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

driver.js


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



@formkit/drag-and-drop


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



小结


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


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

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

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

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


博客:blog.ktdaddy.com/





故事


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


如下:


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


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


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


00.png


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


......


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


聊聊APi设计


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


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


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


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


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


0.png


API设计需要明确边界


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


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


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


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


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


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


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


如下图:


面向执行过程API设计


面向最终结果API设计


举个例子。


银行提现逻辑中,


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


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

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


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

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


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


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


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


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


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


API不应该基于实现去设计


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


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


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

API的命名相当重要


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


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


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


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


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


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


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

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

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


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

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


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

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


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


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


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


API设计的时候考虑性能


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


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


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


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


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


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


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


异步操作模式


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


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


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


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


总结


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


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

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

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

前言



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




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




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



正文



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



1、直接打印堆栈



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




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



1.png



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



2.png



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



3.png


2、堆栈+log



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



4.png



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



5.png


3、log+Json



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




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



6.png


4、小插曲



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




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




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




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




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




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




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




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




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



5、日志规范


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



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



7.png



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




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




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




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



8.png



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




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



总结



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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

我是2B里靠前的

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

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


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

收起阅读 »

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

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

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


2015年9月,我入学了。


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


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


学习,我是有想过的。


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


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


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


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


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


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


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


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


好了,游戏不用玩了。


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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




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


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

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

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

需求简介


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



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



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



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



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


技术实现方案


业务分析


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



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


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

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

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

</div>

</template>

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


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


箭头旋转动画



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


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

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


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



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



折叠区域动画效果


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


使用vue的transition组件实现


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



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

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

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

</div>

</template>

<script setup>

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


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


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


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

</div>

.content-wrap {
overflow: hidden;
}


使用动态类名的方式实现


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



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


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

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

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

</div>

</template>

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


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



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


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

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

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

</div>

</template>

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


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


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



总结


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


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

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

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


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


什么是好机会


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


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


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


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


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



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


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



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


为什么机会有窗口


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


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


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


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


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


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


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


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


如何抓住机会窗口


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


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


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


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



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




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

解决LiveData数据倒灌的新思路

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

⏰ : 全文字数:5500+

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



数据倒灌现象


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


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


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

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


发生原因


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


也就是下面这段代码


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

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

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



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

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


解决方法


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


每次只响应一次


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

public SingleLiveData() {
}

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

});
}

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

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

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


反射


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



  • 能够多 Observer 监听

  • 解决粘性问题


但是也有缺点:



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


UnPeekLiveData


public class UnPeekLiveData extends LiveData {

protected boolean isAllowNullValue;

private final HashMap observers = new HashMap();

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

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

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

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

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

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

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


新的思路


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


int getVersion() {
return mVersion;
}

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


// 跟LiveData同包名
package androidx.lifecycle

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

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

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

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

}
}

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



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

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


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



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

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

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

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


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


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


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


二、无效卷



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

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


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


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


四、无脑跟进新技术


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


五、文档一坨狗屎


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


六、PUA


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


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


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

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

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



导读


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


目录


1 代码风格和可读性


2 注释


3 错误处理和异常处理


4 代码复用和模块化


5 硬编码


6 测试和调试


7 性能优化


8 代码安全性


9 版本控制和协作


10 总结


01、代码风格和可读性



  • 错误习惯


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

1.1 变量命名不规范


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


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

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


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

1.2 长函数和复杂逻辑


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


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

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

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


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

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


1.3 过长的行


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


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

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


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

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


02、注释



  • 错误习惯


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


  • 错误的注释


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


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

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


03、错误处理和异常处理



  • 错误的习惯


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

3.1 忽略错误


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


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

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


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

3.2 过度使用异常处理


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


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

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


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

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


3.3 捕获过于宽泛的异常


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


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

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


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

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


04、错误处理和异常处理



  • 错误的习惯


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

4.1 缺乏复用性


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


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

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

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

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

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


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

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

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

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

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


4.2 缺乏模块化


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


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

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

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

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

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

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

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


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

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

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

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

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

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


05、硬编码



  • 错误的习惯


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

5.1 常量


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


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

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


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

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


5.2 全局变量


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


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

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


def increment(counter):
return counter + 1

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


06、测试和调试



  • 错误的习惯


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

6.1 单元测试


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


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

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


import unittest

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

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

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

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


6.2 边界测试


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


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

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


import unittest

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

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

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

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


6.3 可测试性


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


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

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


def get_time(now):
return now

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


07、性能优化



  • 错误的习惯


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

7.1 过度优化


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


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

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


def sum(numbers):
return sum(numbers)

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


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


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


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

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


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

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


08、代码安全性



  • 错误的习惯


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

8.1 输入验证


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


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

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


import sqlite3

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

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


8.2 不正确的密码存储


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


import hashlib

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

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


import hashlib
import bcrypt

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

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


8.3 不正确的权限控制


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


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

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


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

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


09、版本控制和协作



  • 错误的习惯


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

9.1 版本提交信息


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


git commit -m "Fixed a bug"

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


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

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


9.2 忽略版本控制和备份


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


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

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


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

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


10、总结


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


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



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


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




原创作者|孔垂航


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

一次操蛋的面试经历

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

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


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


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


日历


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


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


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


邮件


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


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


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


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


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



什么?计算器?



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


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



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



收到,我谢谢你。


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


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


百度收购91


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


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


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


豌豆荚断缴社保


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


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


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




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

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

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

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


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


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



一、直播市场的长尾用户


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


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


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


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


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


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


二、快进快出的用人理念


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


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


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



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


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


三、反脆弱的商业形式


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


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


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


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

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

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

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


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


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


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


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


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


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


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


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


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


“降本增效”画像


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


(1)非嫡系


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


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


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


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


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


(2)老员工


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


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


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


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


(3)高职级


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


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


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


(4)高龄


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


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


不确定性——在职


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


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


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


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


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


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


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


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


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


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


不确定性——不在职


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


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


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


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


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


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


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


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


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


结语


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


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

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

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

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

先晒晒我的首笔收入:


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

去看看云端源想内容征集

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


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

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


前端项目创建后,


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


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


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

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

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

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

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

去云端源想看看内容征集

收起阅读 »

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

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

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


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




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


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


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


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


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


AI聊天:AI聊天工具

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


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




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


1、微实战




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


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


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




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


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


2、智能AI工具




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



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


3、社区动态




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


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


4、编程体系课



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


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




5、在线编程




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


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


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


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

收起阅读 »

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

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

介绍

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


能干啥?

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

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


下载地址:

https://gitee.com/easemob_1/swiftui_easemob_rest_tool


如何使用

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





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




收起阅读 »

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

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

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

Android 15 Beta 2来了

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

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

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

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

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

AI霸屏

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

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

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

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

google_io_cts_720.gif

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

Android开发工具

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

code_transforms.gif

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

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

Android开发套件

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

compose-animation.gif

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

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

参考资料


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

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

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


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


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


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



  • 默认单行输入

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

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

  • 支持回车发送内容

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

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


单行输入


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


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


多行输入


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


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


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


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

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


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

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

键盘发送内容


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


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


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

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


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

高度自适应


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



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


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

最后


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


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




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

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

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

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


图片


图片


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


图片


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



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



image.png


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



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


图片


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



图片


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


图片


图片


图片


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


图片


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


图片


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

前端视角下的鸿蒙开发

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

前言



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


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



一、 什么是鸿蒙


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


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


OpenHarmony,HarmonyOS,以及HarmonyOS NEXT。


1. OpenHarmony


OpenHarmony


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


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


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


2. HarmonyOS


HarmonyOS


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


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


3. HarmonyOS NEXT


HarmonyOS NEXT


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


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



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



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


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


4. 小结


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


三者之间的关系


二、 初识鸿蒙开发


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


鸿蒙开发套件


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


ArkTS&ArkUI


ArkCompiler


三、 关于ArkTS的一些疑惑


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


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


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


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


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


3. 基于TS拓展了什么?


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


下面我们一个一个来看。


1. Question1 - ArkTS语言的运行时


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


方舟运行时


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


方舟开发框架示意图


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



  • AOT 编译模式

  • LiteActor 轻量化并发

  • 源码安全


AOT 编译模式


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



  • 机器码AOT编译


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


  • 中间产物AOT编译


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


  • 完全的解释执行


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


  • 混合的JIT编译


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




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



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


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



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

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


我们一个一个来说。


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

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


V8现有工作流程


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


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


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


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


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


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


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


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


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


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


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


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


TS类型信息运行时被丢弃


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


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

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


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


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


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



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


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



c. 取长补短

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


博客描述


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


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


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


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


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


LiteActor轻量化并发


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


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


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


LiteActor轻量化并发


LiteActor轻量化并发博客描述


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


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


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


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


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


源码安全


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


源码安全


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


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



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



  1. 执行UI绘制;

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

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

  4. 分发交互事件;

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

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


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



ArkTS线程模型


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


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



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

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

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


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


四、 ArkTS & ArkUI


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


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


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


社区里的一些讨论


1. 基本语法


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


基本语法



  • 装饰器


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


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


  • 自定义组件


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


  • UI描述


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


  • 系统组件


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


  • 事件方法


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


  • 属性方法


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



2. 数据驱动UI


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



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


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



State和UI



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



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


ArkTS & ArkUI的状态管理总览


更多细节详见状态管理


3. 渲染控制


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



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



4. 更多语法


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


5. ArkTS & ArkUI小结


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



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

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

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


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


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


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


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


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


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


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


五、 其他


1. 包管理工具


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


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


2. 应用程序结构


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


应用程序结构


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


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


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


打包结构


开发态和打包后视图


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


开发-调试-发布-部署


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



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

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


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



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

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

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



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



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


六、 总结


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



  • ArkTS作为应用开发语言

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

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

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

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

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


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


其他一些讨论


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




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

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

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



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


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


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


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


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


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


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


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


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

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

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是Google、Instagram和Facebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHub...
继续阅读 »

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是GoogleInstagramFacebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHubDuckDuckGoDiscord。该组织选择执行相反的操作并重定向 http://www.example.com 到 example.com



“WWW”属于 URL 吗?一些开发人员对此主题持有强烈的意见。在了解了一些历史之后,我们将探讨支持和反对它的论据。


WWW是什么?


WWW代表"World Wide Web",是上世纪80年代晚期的一个发明,引入了浏览器和网站。使用"WWW"的习惯源于给子域名命名的传统:



如果没有WWW会发生什么问题?


1. 向子域名泄露cookies


反对"没有WWW"的域名的批评者指出,在某些情况下,subdomain.example.com可以读取example.com设置的cookies。如果你是一个允许客户在你的域名上运营子域名的Web托管提供商,这可能是不希望看到的。


然而,这种行为只存在于Internet Explorer中。


RFC 6265标准化了浏览器对cookies的处理,并明确指出这种行为是错误的。


另一个潜在的泄露源是example.com设置的cookies的Domain值。如果Domain值明确设置为example.com,那么这些cookies也将被其子域名所访问。


Cookie 值暴露于 example.com暴露于 subdomain.example.com
secret=data
secret=data; Domain=example.com

总之,只要你不明确设置Domain值,而且你的用户不使用Internet Explorer,就不会发生cookie泄露。


2. DNS的困扰


有时,"没有WWW"的域名可能会使你的域名系统(DNS)设置复杂化。


当用户在浏览器的地址栏中输入example.com时,浏览器需要知道他们想访问的Web服务器的Internet协议(IP)地址。浏览器通过你的域名的域名服务器向其DNS服务器(通常间接通过用户的互联网服务提供商(ISP)的DNS服务器)请求IP地址。如果你的域名服务器配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。


在某些情况下,你可能希望使用规范名称(CNAME)记录来代替为你的网站设置。这样的记录可以声明http://www.example.comexample123.somecdnprovider.com的别名,这会告诉用户的浏览器去查找example123.somecdnprovider.com的IP地址,并将HTTP请求发送到那里。


请注意,上面的示例使用了一个WWW子域名。对于example.com,不可能定义一个CNAME记录。根据RFC 1912,CNAME记录不能与其他记录共存。如果你尝试为example.com定义CNAME记录,example.com上的MX(邮件交换)记录将无法存在。因此,就不可能在@example.com上接收邮件


一些DNS提供商可以让你绕过这个限制。Cloudflare称其解决方案为CNAME解析。通过这种技术,域名管理员配置一个CNAME记录,但他们的域名服务器将暴露一个A记录。


例如,如果管理员为example.com配置了指向example123.somecdnprovider.com的CNAME记录,并且存在一个指向1.2.3.4example123.somecdnprovider.com的A记录,那么Cloudflare就会暴露一个指向1.2.3.4的example.com的A记录。


总之,虽然这个问题对希望使用CNAME记录的域名所有者来说是有效的,但现在有一些DNS提供商提供了合适的解决办法。


没有WWW的好处


大部分反对WWW的论点是实用性或外观方面的。"无WWW"的支持者认为example.comhttp://www.example.com更容易说和输入(对于不那么精通技术的用户可能更不容易混淆)。


反对WWW子域名的人还指出,去掉它会带来一种谦虚的性能优势。网站所有者可以通过这样做每个HTTP请求节省4个字节。虽然这些节省对于像Facebook这样的高流量网站可能会累积起来,但带宽通常并不是一种紧缺的资源。


有"WWW"的好处


支持WWW的一个实际论点适用于使用较新顶级域的情况。例如,http://www.example.miamiexample.miami无法立即被识别为Web地址。对于具有诸如.com这样的可识别顶级域的网站,这不是一个太大的问题。


对搜索引擎排名的影响


目前的共识是你的选择不会影响你的搜索引擎表现。如果你希望从一个URL迁移到另一个URL,你需要配置永久重定向(HTTP 301)而不是临时重定向(HTTP 302)。永久重定向确保你旧的URL的SEO价值转移到新的URL。


同时支持两者的技巧


网站通常会选择example.comhttp://www.example.com作为官方网站,并为另一个配置HTTP 301重定向。理论上,可以支持http://www.example.com和example.com两者。但实际上,成本可能会超过效益。


从技术角度来看,你需要验证你的技术栈是否能够处理。你的内容管理系统(CMS)或静态生成的网站需要将内部链接输出为相对URL以保留访问者的首选主机名。除非你可以将主机名配置为别名,否则你的分析工具可能会将流量分别记录在两个主机名上。


最后,你需要采取额外的措施来保护你的搜索引擎表现。谷歌将把URL的"WWW""非WWW"版本视为重复内容。为了在其搜索索引中去重复内容,谷歌将显示它认为用户更喜欢的那个版本——不论是好是坏。


为了在谷歌中保持对自己的控制,建议插入规范链接标签。首先,决定哪个主机名将成为官方(规范)主机名。


例如,如果你选择了www.example.com,则必须在 https://example.com/my-article里的  上的标记 中插入以下代码段:


    <link href="https://www.example.com/my-article" rel="canonical"> 

这个代码片段告诉谷歌"无WWW"变体代表着相同的内容。通常情况下,谷歌会在搜索结果中偏好你标记为规范的版本,也就是在这个例子中的"WWW"变体。


总结


对于是否在URL中加入"WWW",人们有不同的观点。下面是支持和反对的论点:


支持"WWW"的论点:



  1. 存在子域名的安全性问题:某些情况下,子域名可以读取主域名设置的cookies。虽然这个问题只存在于Internet Explorer浏览器中,并且已经被RFC 6265标准化修复,但仍有人认为使用"WWW"可以避免潜在的安全风险。

  2. DNS配置的复杂性:如果你的域名系统(DNS)配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。但如果你想使用CNAME记录来设置规范名称,那么"没有WWW"的域名可能会导致一些限制,例如无法同时定义CNAME记录和MX(邮件交换)记录。

  3. 对搜索引擎排名的影响:对于使用较新顶级域的网站,使用"WWW"可以帮助识别网址,而不是依赖可识别的顶级域名。然而,目前的共识是选择是否使用"WWW"对搜索引擎表现没有直接影响。


支持去除"WWW"的论点:



  1. 实用性和外观:去除"WWW"可以使域名更简洁和易于输入,减少了用户可能混淆的机会。

  2. 节省字节:去除"WWW"可以每个HTTP请求节省4个字节。虽然这对于高流量网站来说可能是一个可累积的优势,但对于大多数网站来说,带宽通常不是一个紧缺的资源。


最佳实践:
一般来说,网站会选择将example.com或www.example.com作为官方网址,并对另一个进行重定向。你可以通过使用HTTP 301永久重定向来确保旧URL的SEO价值转移到新URL。同时,你还可以在页面的标签中插入规范链接标签,告诉搜索引擎两个URL代表相同的内容,以避免重复内容问题。


需要注意的是,在做决策时要考虑到技术栈的支持能力、DNS配置的限制和谷歌对搜索排名的处理方式。


作者:狗头大军之江苏分军
来源:juejin.cn/post/7263274550074507321
收起阅读 »

微信之父张小龙的一次内部分享

本文分享一下凌览近期看的一本书《微信背后的产品观》,它源自2012年7月微信产品经理张小龙一次长达8小时的腾讯内部分享。 了解人性 产品经理是站在上帝身边的人,上帝根据他的期望,创造了人,并赋予人一些习性,让人类的群体在这些习性下发展演化。而产品经理实际是在理...
继续阅读 »

本文分享一下凌览近期看的一本书《微信背后的产品观》,它源自2012年7月微信产品经理张小龙一次长达8小时的腾讯内部分享。


了解人性


产品经理是站在上帝身边的人,上帝根据他的期望,创造了人,并赋予人一些习性,让人类的群体在这些习性下发展演化。而产品经理实际是在理解了人的习性后,像上帝一样,建造系统并建立规则,让群体在系统中演化。



书中提及两本书《失控》、《乌合之众》,两本书结合的逻辑:群体在特定规则下的无序演化产生很多意想不到的结果。微信的很多产品功能的设计思想都和这两本书的理论很契合,比如漂流瓶、摇一摇,拍一拍等。



优秀的产品经理需要具备的能力:



  • 了解人的习性,需求从人性中产生

  • 了解群体的心理


"人"的特性:



  • 人是懒惰的,懒惰是创新的动力,案例:语音查找联系人,解决走路或双手不方便时要给一个人发微信,输入半天还找不出的情况

  • 人是跟风的,"因为别人都在用",时尚是驱动力,在互联网产品中,"时尚"是重要的驱动力

  • 人是没有耐心的,用户没有耐心看产品说明书,不要尝试去引导用户,去教育用户,没有人愿意去接受你的引导和教育。一定是他拿过来就会用才是最直接的(产品使用操作简单)

  • 人是不爱学习的,"马桶阅读"理论:不要给用户超过马桶上看不完的内容

  • 群体是"乌合之众",理论出自《乌合之众》一书,群体智商低于个体,互联网产品的用户是群体,不是个体


如何确定一个需求



  • 对于新点子,99%的情况下否定是对的,不要随便臆想需求

  • 不要用户说什么就做什么,用户的反馈是帮助你了解他们的想法,用户的需求是零散的,应该进行归纳抽象

  • 不从同类产品里找需求,另的产品决定做这个需求,是有他们自己的理解,并深入分析思考过。如果别人说好,我们就直接照搬,其实没有深刻理解需求

  • 不听从产品经理的需求,他们不是用户却自认为代表用户,他们分析过于理性,他们会要求要显示在线、要已读、要分组、要滤镜、要涂鸦、要多端同步、要群名片、要赞头像.....如果产品经理都把这些当作用户朴素的需求做进去,这将是可怕的事情

  • 需求来自你对用户的了解

    • 需求不来自调研

    • 需求不来自分析

    • 需求不来自讨论

    • 需求不来自竞争对手



  • "爽"用过功能,爽是体验。爽比功能更易传播

  • 只抓主场景,不做全功能,做大而全很容易,做小很难,如果没有化繁为简的功力,就控制自己的欲望,每天砍掉几个需求的爽,远大于提出几个需求,案例:朋友圈只能发图片,发140字的难度远胜一张图片


如何设计一个产品



  • 好的产品价值观和认知是成为优秀产品的前提

  • 先做产品结构,之后才是功能细节。微信功能细算特别多,但看起来还是很简单,做一个新版本都不知道它有什么新功能,先把微信的骨骼梳理清楚,枝叶的东西藏得很深也没关系,这样整个产品才会乱掉

  • 功能模块之间是有机联系的关系,独立的功能堆砌很危险

  • 设计是分类

  • 抽象才能化繁为简,如果有100个需求,而我们能把这100个需求汇总成10个需求,这就是"抽象"

  • 越简单的分类越容易被接受,微信会升级,但结构和界面依然保持简单,过多变化易引来用户不适应

  • 挖掘需求背后的本质需求

  • 宁愿损失功能也不损失体验


最后


张小龙强调我所说的,都是错的,每个人都会有自己的解决问题的办法,没有永远的正确教条


书未有问答环节,这里精简摘录下我认为有启发的回答:


Q:为什么人人都是产品经理?怎么做到跟其他也是产品经理的人不一样?


A:因为人人都可以提问题,人人都可以指手画脚,用户也会,但最难的是找到本质的东西,这才是产品经理要一定具备的


Q:从普通工程师到现在做成一个伟大的产品,你是怎么一步一步走过来的?


A:经历上千个实战的锻练,越多越好,一个人要成为一个领域的专家,要付出一万个小时的努力和专业训练



回答提及一本书《另类成功学》,有兴趣可以看看。



如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。




作者:程序员凌览
来源:juejin.cn/post/7274163003158822912
收起阅读 »

程序员的未来发展会是什么?跟一位同学沟通之后的思考

Hello,大家好,我是 Sunday。 说起程序员的发展方向好像是一个老生常谈的话题了。我记得在过去的十年中,我曾经无数次的看到过各种文章来说类似的话题。 不过这样的话题又好像是一个经久不衰的,特别是在目前这样行情不好的情况下,我相信有很多同学都在处于前所未...
继续阅读 »

Hello,大家好,我是 Sunday。


说起程序员的发展方向好像是一个老生常谈的话题了。我记得在过去的十年中,我曾经无数次的看到过各种文章来说类似的话题。


不过这样的话题又好像是一个经久不衰的,特别是在目前这样行情不好的情况下,我相信有很多同学都在处于前所未有的迷茫状态之中。


比如,昨天有个同学来问我说:“前端发展方向是什么?全面发展?工程化?还是架构师?”



所以,为了能更好的解决大家的困惑,今天这篇文章咱们就来说一说程序员未来的发展方向、什么人适合什么方向、以及分别需要做什么样的准备。


程序员发展方向


程序员是一个技术岗位,但是它的发展方向绝对不仅局限于“技术领域”。


所以,当我们去考虑发展方向或者是未来职业规划的时候,就不能仅从技术的角度来进行分析。


下面是我认为对于程序员而言,最有利的几个方向(一家所见,仅供参考):



  1. 某一个行业的技术专家

  2. 技术大牛(包含 Leader 岗)

  3. 自由职业者

  4. remote 远程工作


下面咱们一个一个去说...


1. 某一个行业的技术专家


1.1 什么是某一个行业的技术专家(后面简称:技术专家)


回忆下我们的工作,我们目前所做的大部分工作是不是都在为了完成某一个业务而存在的?


有的同学在做医疗业务、有 金融业务、有 政府项目电商服务 等,总之无论是那种,所有的项目总归是在为某一个业务而服务的。


而所谓的技术专家,指的就是:以技术为基本,成为非常熟悉该行业的人员


以我为例,我首先是一个程序员,其次是一个 教育方向的程序员 就是这个道理。


1.2 什么样的人适合


想要做技术专家,那么一定要明确一个大前提:技术本没有价值,只有使用技术完成了一个有价值的事情之后,这个事情才可以赋予技术价值。


如果你想要以技术专家为目标,那前提一定是 你要深刻的认同这句话。如果你不认同,那么这个方向就 不适合 你。


1.3 需要怎么做


如果想要成为技术专家,那么就 不能 频繁的更换行业。


甚至,当你选定了一个行业之后,就应该长期立足下去,只要这样你才能逐步的熟悉这个行业的运行规律。


所以,这就要求我们在跳槽的时候,尽量 选择与上家公司从事相同行业的公司去做(没有竞业协议的前提下),而不是随便跳转一个行业就入职。


2. 技术大牛(包含 Leader 岗)


2.1 什么是技术大牛


其实技术大牛是很难定义的。古语说:文人相轻(程序员总不能说自己是武人吧)。所以,对于程序员而言,所谓的技术大牛一定是一个相对的,而不是绝对的。就算是尤雨溪也有被人喷菜的时候。


所以说,如果要给技术大牛 一个定义的话,那么指的就是:在某一个范围(公司或者团体)内,具有一定权威的人员


2.2 什么样的人适合


这里其实有两个方向,不同的方向适合的人不同:



  1. 纯技术人员,不参与管理: 比较适合不善言辞,并且不愿意余人交流,完全沉迷于技术的人

  2. 以管理为主的技术人员: 具备一定的技术能力,但是同时更愿意与人沟通,懂得人情世故的人


2.3 需要怎么做


如果是单纯的技术就比较简单了,做法分为两步:



  1. 多在“团体”内发言:这个“团体”代表了很多东西,可以是:论坛、网站、公司或其他。

  2. 多学习各种新的技术,然后把这些技术 输出出去


如果你只会输入,不会输出。那么是无法成为技术大牛的。


而 Leader 就比较复杂了,相比于技术而言,更多的其实是 人情世故。所以如果想要将来做管理岗位,那么就需要练习好人情世故的处理能力。


3. 自由职业者


关于自由职业者,前几天我专门写过一篇文章 并不自由的自由职业?回顾下我的五一小长假 ,甚至还在 B站 还录制了对应的视频:



所以这里就不再细说了。


4. remote 远程工作


4.1 什么是remote


所谓的 remote 指的就是 远程工作。比如:你在家工作,不需要到公司坐班。符合这个条件的都属于 远程工作。


目前大家提起 remote 大多数指的其实是:国外的远程工作。也就是 人在国内,为国外的公司工作。


可能是因为国内太卷的原因,目前 remote 的工作被非常多的人推崇。


但是 我个人建议大家理性看待。有兴趣的同学可以看下我写的这篇文章 remote 经验分享,它真有你想象的那么好吗?,这里就不过多赘述了。


4.2 什么样的人适合


如果不喜欢坐班,享受那种工作几个月,休息几个月的状态的话,那么可以尝试 remote 的工作。


4.3 需要怎么做


其实想要做 remote 的工作并不困难,核心是两个点:



  1. 英语:至少要可以做到 雅思6.5 的水平

  2. 岗位:可以多关注 领英、电鸭社区、indeed 等


但是要 注意:防止被骗!!!


因为 remote 无法看到对方的公司信息,并且合同形同虚设,所以有同学出现过 工作 1-2 个月之后无法收到工资的情况,所以要特别注意这一点!


总结


OK,以上是我在跟那位同学沟通之后,大致总结的一些对程序员比较友好的发展方向,希望可以对大家有帮助~~


作者:程序员Sunday
来源:juejin.cn/post/7368294440547041318
收起阅读 »

大环境越不好 人就越玄学

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。 问广大学子毕业后最想从事什么工作。 当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。 只有很少一部分选择了事业单位和公务员,这...
继续阅读 »

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。


问广大学子毕业后最想从事什么工作。


当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。


只有很少一部分选择了事业单位和公务员,这部分同学还有相当比例来自对考公自古有执念的山东。


而在其他省份,多数同学都认为自己能拥有光明的未来,当然不会喜欢公务员这种工资稳定得低,日复一日枯坐案前,早早就能一眼望到头的工作。


在当时年轻人眼里,公务员属于“实在不行就只能回家考公“的备胎,地位约等于“实在不行就找个老实人嫁了“的级别。


但后来的故事我们都知道了,经济大船这几年驶入了深水区,风浪越来越大,鱼也越来越贵。


于是四平八稳旱涝保收的体制内,这几年摇身一变,一跃成为了那个最靓的仔。不得不说,人确实是时代的产物,环境的变化可以完全改变一个人的决策。


大环境好的时候,人们会不自觉地高估自身的努力,那时候人们是相信努力一定会有收获的。有时候过于相信了,但这在经济高速增长的年代并不会有太大问题,你还是会得到属于自己的那块蛋糕的。


但当经济增速换档时,付出与回报的比例开始失衡,努力就能收获的简单逻辑不攻自破。变成了努力也不一定有收获,进而发展成努力大概率不会有收获,最后演变成一命二运三风水,努力奋斗算个鬼


这种心态的转变也解释了为啥从去年以来,越来越多的年轻人开始扎堆去寺庙求签祈福,排的长队连起来能绕地球三圈,看得旁观的老大爷直摇头说,“真搞不懂这些小年轻是怎么想的,偶像粉丝见面会咋还跑到庙里来开了?!”


人在逆境迷茫时,是容易被玄学吸引。逆境意味着前路遇阻,意味着你迫切需要一些指引,而玄学恰好满足了这方面需求。


命运这个东西,有时候真蛮捉摸不透的。


我认识一小姐姐,为一场决定人生的重要考试做足了准备,结果在赶往考场的路上,书包就这么巧被扒手偷了,里面开卷考试所有的资料全部丢失,直接导致她逃汰出局,泪洒当场。


还有一大哥,在升职加薪岗位竞争的关键阶段,突然一场急病,好巧不巧失声了,一句话也说不出来,参加不了竞聘答辩,眼睁睁看着大好机会就此溜走。


等这事过去了,他一下子又能正常说话,跟被老天上了沉默debuff一样,你说他找谁说理去呢。


人活得时间越长,就越信“命“这个东西,越能意识到自己真正能把控的其实少得可怜,随便一点意外都能直接改变整个人生走向。


这种感悟放在以前,一般都是上了些年纪的人才会有的,但随着这两年经济增速换挡,年轻人频繁碰壁,被命运按在地上摩擦的次数多了,自然也就信了“命”,求签问道的也就跟着多起来了。


说句不好听的话,我觉得这样挺好的。不是说求签问道这个行为好,而是这种行为背后暗含着一个巨大的心理转变,我认为很好。


那就是放过自己。亚洲人尤其是我们特别不愿意放过自己,从出生开始就活在比较中,长辈们连夸个人都要这么夸,说哎呀,你学习真用功,比学习委员还用功;哎呀,你工资挺高,比隔壁小王还要高。


骂你的时候也一定要捎带上别人,说你看谁谁谁多厉害,你再看看你,一定是你还不够努力。


就是这种搞法很容易让人把责任全揽自己身上,对自我要求过高,最后的结果就是崩掉,就累嘛!


但现在不一样了,现代人在网络上看了太多含着金汤匙出生在罗马的人,和那些老天爷追着赏饭吃的人。


他们跟我们之间的差距大到几辈子都弥补不上,那努力万能论也就不攻自破了嘛。


于是越来越多的小伙伴开始承认自我的局限,承认努力也不一定有收获,承认人生不如意十之八九,慢慢也就承认了“命运”这个东西,开始顺其自然,没那么多执念了。


不过有些人过于放飞自我,摆烂走了另一个极端,那也是要出问题的。


即便是玄学,它也没有彻底否定个人奋斗,大富靠命没错,但小富靠勤,靠双手取得一些小成就,让日子过得舒服些还是没啥问题的。


其实我觉得一个比较合适的世界观应该是这个样子:首先咱得承认不可抗力,承认“命”与“运”这个东西是真实存在的,如果你不喜欢这两个玄乎的字,可以用“概率”代替,我们永远得做好小概率事件砸到头上的准备。


有时候拼尽一切就是没有好的结果,这咱得承认,但同时这也并不意味着从此放弃一切行动,落入虚无主义的陷阱。


人还是要去做一些什么的。比如精进某项专业技能,逐步提升自身能力,为的不是那点工资,而是一件更重要的事,抓住运气。


运气有多重要,大家都明白,它比努力重要得多。


运气这东西打比方的话,就像一个宝箱,会随机在你面前掉落,但这些宝箱自带隐形属性,你等级太低的话就看不见它,自然也就抓不住这些运气。


用现实举例,“运气”就像你在工作中遇到了某个本来还可以拉你一把的贵人,结果你的等级太低,工作能力稀碎,贵人一看,这货不值得我帮,转身走了。他这个宝箱对你而言就隐形了,消失了。


而且最讽刺的是你从头到尾都被蒙在鼓里,根本不知道自己错失了一次宝贵的机会,所以为了避免运气来了你抓不住,又溜走的这种尴尬情况出现,我们还是要去精进和磨练一下社会技能,尽量达到能在某些场合被人夸奖的程度。


把等级刷高一些,之后该吃吃该喝喝,耐心等待宝箱的出现。这可能也是以前人们常说的,“尽人事听天命”的另一种解释吧。


也希望今天聊的关于命和运的这些内容,能启发到一些小伙伴,大家一起认认真真,平平淡淡的生活。


作者:程序员Winn
来源:juejin.cn/post/7317704462436139058
收起阅读 »

OPPO率先适配Android 15,首批机型名单公布

北京时间5月15日,代号为「Vanilla Ice Cream(香草冰淇淋)」的 Android 15 在2024谷歌 I/O 大会正式亮相。作为全球智能手机市场领先品牌,OPPO 作为连续六年首批适配 Android 新系统的厂商,此次不仅率先发布了基于 A...
继续阅读 »

北京时间5月15日,代号为「Vanilla Ice Cream(香草冰淇淋)」的 Android 15 在2024谷歌 I/O 大会正式亮相。作为全球智能手机市场领先品牌,OPPO 作为连续六年首批适配 Android 新系统的厂商,此次不仅率先发布了基于 Android 15 的开发者预览版助力开发者抢先适配,更以全流程、全方位的适配保障服务,持续为广大开发者保驾护航。

图1.png

OPPO 公布 Android 15 适配指南,督促开发者升级64位架构

据了解,Android 15 带来了一系列令人瞩目的新功能和改进,包括全新设计的兼容性调试工具、安全与隐私相关的强化措施、系统优化和新的API支持。这些功能使开发者能够更轻松地构建和维护应用,为用户提供更好的性能和体验。

基于 Android 15 Beta 版本,OPPO 推出 ColorOS 开发者预览版,OPPO Find X7、一加12首批支持。值得注意的是, OPPO 这次特别督促开发者对64位架构的全面升级,确保所有软件和应用在新系统中都能实现最佳性能。据了解,Android 15 系统升级 Vendor 的手机均不支持32位应用,若不进行64位升级将导致应用后续无法下载使用。64位架构优势显著,更快的处理速度、更大的内存支持以及更高的效率,使开发者能够充分利用 Android 15 的潜力,为用户提供更加流畅和丰富的应用体验。开发者可前往OPPO开放平台官网,抢先下载并体验开发者预览版。

图2.png

全方位服务保障,助力开发者推进系统适配

为了助力开发者高效适配 Android 15 系统,OPPO 提供了包括适配文档、适配工具、适配资讯以及专家交流等在内的全面支持和服务。

全面清晰的指导文档帮助各类型 APP 开发者迅速找到所需的适配方案;云测服务提供实时在线的远程调试功能,支持开发者随时接入,帮助开发者快速验证适配结果;OPPO开放平台官网适配支持专区实时更新 Android 15 最新动态,开发者可随时获取第一手适配资讯。此外,OPPO 还提供了7*24小时在线答疑服务,专人协助解决适配技术难题,进一步提升适配效率。技术指引也将实时更新,集中解答开发者提出的高频问题。

海量全面的文档支持,贯穿全程的指导升级服务,让每一个开发者都不掉队。

图3.png

据悉,5月22日,OPPO 将联合知名技术社区 51CTO 举办「OTalk | Android 15 适配开发者交流专场」线上直播活动,与行业开发者深入交流对话。届时将特别邀请 OPPO 高级工程师带来 Android 15 新特性的深度解读及适配建议,分享 OPPO 适配支持服务,解答开发者常见问题,助力开发者高效适配新版本。

图4.png

作为 Android 生态系统的关键参与者,OPPO 连续6年首批适配 Android 新版本,持续为开发者提供全流程适配支持和服务,携手开发者高效完成版本迭代优化与应用兼容性测试,共同将更安全、更流畅的系统体验带给用户。

接下来,OPPO 将持续提供关于 Android 15 适配的最新进展,广大开发者可关注「OPPO开放平台」后续公告,以获取更多详细信息和支持资源。

收起阅读 »

责任链模式最强工具res-chain🚀

web
上面的logo是由ai生成 责任链模式介绍 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避...
继续阅读 »
image.png

上面的logo是由ai生成



责任链模式介绍


责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避免了请求的发送者和接收者之间的直接耦合


在责任链模式中,每个处理者都持有对下一个处理者的引用,即构成一个链表结构。当请求从链头开始流经链上的每个处理者时,如果某个处理者能够处理该请求,就直接处理,否则将请求发送给下一个处理者,直到有一个处理者能够处理为止。


这种方式可以灵活地动态添加或修改请求的处理流程,同时也避免了由于请求类型过多而导致类的爆炸性增长的问题。


看完以上责任链的描述,有没有发现跟Node.js的某些库特别的像,没错,就是koa。什么?没用过koa?那我建议你立马学起来,因为它用起来特别的简单。


下面来一个简单使用koa的例子:


const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
}

next(); // 执行下面的回调函数
});

app.use(async (ctx, next) => {
if (ctx.request.url === '/hello') {
ctx.body = 'hello world';
return;
}
});

app.listen(3000);

通过node运行上面的代码,在浏览器请求localhost:3000,接口就会返回home,当我们请求localhost:3000/hello,接口就会返回hello world


上面对请求的处理过程就很符合职责链模式的思想,我们可以清楚的知道每个链做的工作,并且链条的顺序流程也很清晰。


有人就会问,只在一个回调里面也能处理呀,比如下面的代码:


app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
} else if (ctx.request.url === '/home') {
ctx.body = 'hello world';
return
}
});

是的,上面的代码确实可以实现,但是这就要回到我们使用责任链模式的初衷了:为了逻辑解耦。


责任链解决的问题


我们继续接着聊上一节的问题,使用if确实可以实现相同效果,但是在某些场景中,if并没有职责链那么好用,为什么这么说呢。


我们找一个应用案例举个例子:



假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:




  • 缴纳500元定金的用户可以收到100元优惠券;

  • 缴纳200元定金的用户可以收到50元优惠券;

  • 没有缴纳定金的用户进入普通购买模式,没有优惠券。

  • 而且在库存不足的情况下,不一定能保证买得到。


下面开始设计几个字段,解释它们的含义:



  • orderType:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。

  • pay:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。

  • stock:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。


下面我们分别用if和职责链模式来实现:


使用if:


const order = function (orderType, pay, stock) {
if (orderType === 1) {
if (pay === true) {
console.log('500元定金预购,得到100元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
} else if (orderType === 2) {
if (pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
}

order(1, true, 500) // 输出:500元定金预购,得到100元优惠券'

虽然上面的代码也可以实现需求,但是代码实在是难以阅读,维护起来更是困难,如果继续在这个代码上开发,未来肯定会成为一座很大的屎山。


下面我们使用责任链模式来实现:


function printResult(orderType, pay, stock) {
// 这里ResChain类是模拟koa的写法,后面会讲如何实现ResChain
// 请先耐心看完它是如何处理的
const resChain = new ResChain();
// 针对500元定金的情况
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next(); // 这里将会调用order200对应的回调函数
});
// 针对200元定金的情况
resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next(); // 这里会调用noOrder对应回调函数
});
// 针对普通用户购买的情况
resChain.add('noOrder', (_, next) => {
if (stock > 0) {
console.log('普通用户购买,无优惠券');
} else {
console.log('手机库存不足');
}
});

resChain.run(); // 开始执行order500对应的回调函数
}

// 测试
printResult(1, true, 500); // 500元定金预购,得到100元优惠券
printResult(1, false, 500); // 普通用户购买,无优惠券
printResult(2, true, 500); // 200元定金预购,得到50元优惠券
printResult(3, false, 500); // 普通用户购买,无优惠券
printResult(3, false, 0); // 手机库存不足

以上的代码经过责任链处理之后特别的清晰,并且减少了大量的if-else嵌套,每个链的职责分,我们可以看出责任链模式存在的优点:



  1. 降低了代码之间的耦合,很好的对每个处理逻辑进行封装。在每个链条内,只需要关注自身的逻辑实现。

  2. 增强了代码的可维护性。我们可以很轻易在原有链条内的任何位置添加新的节点,或者对链条内的节点进行替换或者删除。


责任链还特别的灵活,如果说后面pm找我们加需求,需要加多一个预付定金400,返回80元优惠券,处理起来也是易如反掌,只需要怼回去,只需要在order500下面加多一个节点处理即可:


... 
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next();
})
+ // 加上这一块
+ resChain.add('order400', (_, next) => {
+ if (orderType === 3 && pay === true) {
+ console.log('400元定金预购,拿80元优惠券');
+ return;
+ }
+ next();
+ })

resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next();
})
...

就是这么简单。那这个ResChain是如何实现的呢?


封装ResChain


先别急,首先我们来了解一下koa是如何实现:在链节点的回调函数内调用next就可以跳到下一个节点的呢?


话不多说,直接看源码,参考的库是koa-compose,代码也是特别的简洁:


function compose (middleware) {
// 这里传入的middleware是函数数组,例如: [fn1, fn2, fn3, fn4, ...]
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 判断数组里的元素是不是函数类型
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0);

// 这里利用了函数申明提升的特性
function dispatch (i) {
// 这里是防止重复调用next
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i

// 从middleware中取出回调函数
let fn = middleware[i]
if (i === middleware.length) fn = next

// 如果fn为空了,则结束运行
if (!fn) return Promise.resolve()

try {
// next函数其实就是middleware的下一个函数,执行next就是执行下一个函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}

看完源代码,我们接着来实现ResChain类,首先整理一下应该要有的方法:



  • add方法。可以添加回调函数,并按添加的顺序执行。

  • run方法。开始按顺序执行责任链。


add方法执行的时候,把回调函数按顺序push进一个数组中。


export class ResChain {

/**
* 按顺序存放链的key
*/

keyOrder = [];
/**
* key对应的函数
*/

key2FnMap = new Map();
/**
* 每个节点都可以拿到的对象
*/

ctx = {}
constructor(ctx) {
this.ctx = ctx;
}

// 这里用key来标识当前callback的唯一性,后面重复添加可以区分。
add(key, callback) {
if (this.key2FnMap.has(key)) {
throw new Error(`Chain ${key} already exists`);
}

this.keyOrder.push(key);
this.key2FnMap.set(key, callback);
return this;
}

async run() {
let index = -1;
const dispatch = (i) => {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}

index = i;
const fn = this.key2FnMap.get(this.keyOrder[i]);
if (!fn) {
return Promise.resolve(void 0);
}

return fn(this.ctx, dispatch.bind(null, i + 1));
};

return dispatch(0);
}
}

add方法的第一个参数key可以用来判断是否已经添加过相同的回调。


有人会说,koa的中间件是异步函数的,你这个行不行?


当然可以,接下来看个异步的例子:


const resChain = new ResChain();

resChain.add('async1', async (_, next) => {
console.log('async1');
await next();
});


resChain.add('async2', async (_, next) => {
console.log('async2')
// 这里可以执行一些异步处理函数
await new Promise((resolve, reject) => {
setTimeOut(() => {
resolve();
}, 1000)
});

await next();
});


resChain.add('key3', async (_, next) => {
console.log('key3');
await next();
});


// 执行责任链
await resChain.run();

console.log('finished');

// 先输出 async1 async2 然后停顿了1秒钟之后,才输出async3 finished


🚧 需要注意:如果是异步模式,则链上的每个回调函数必须要 await next(),因为next函数代表下一个环的异步函数。



koa的中间件方式简直一毛一样。


有人可能还注意到了,ResChain实例化的时候可以传入对象,比如下面的代码:


const resChain = new ResChain({ interrupt: false });

传入对象具体有什么用法呢?可以用来获取一些在链中处理好的数据,来实现发送者和处理者的解耦。可能比较抽象,我们来举个例子。


比如需要进行数据校验的场景,如果不通过,则中断提交:


const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}
const resChain = new ResChain(ctx);

resChain.add('校验name', (ctx, next) => {
const { name = '' } = ctx;
if (name === '') {
ctx.error = '请填写name';
ctx.interrupt = true;
return;
}

next();
})

resChain.add('校验phone', (ctx, next) => {
const { phone = '' } = ctx;
if (phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
return;
}

next();
})

// 执行责任链
resChain.run();

// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}

如果是使用if来实现:


const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}

if(ctx.model.name === '') {
ctx.error = '请填写用户名';
ctx.interrupt = true;
}

if (!ctx.interrupt && ctx.model.phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
}

// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}

可以发现,对phone的判断逻辑,就要先判断interrupt是否为false才能继续,而且如果下面还有其他的字段校验,那必须都走一遍if。


这也是责任链的一个优势,可以在某个环节按自己的想法停止,不用继续走后面的节点。


目前我已经把这个工具上传到npm了,如果想要在自己的项目中使用,直接安装:
res-chain即可使用:


npm install res-chain

# 或者
# yarn add res-chain

引入:


import { ResChain } from 'res-chain';
// CommonJS方式的引入也是支持的
// const { ResChain } = 'res-chain';

const resChain = new ResChain();

resChain.add('key1', (_, next) => {
console.log('key1');
next();
});

resChain.add('key2', (_, next) => {
console.log('key2');
// 这里没有调用next,则不会执行key3
});

resChain.add('key3', (_, next) => {
console.log('key3');
next();
});

// 执行职责链
resChain.run(); // => 将会按顺序输出 key1 key2

芜湖起飞🚀。


有了这个工具函数,我们就可以视场景去优化项目中的一大坨if-else嵌套,或者直接使用它来实现一些业务中比较复杂的逻辑。


起源


这个工具诞生的过程还挺巧合的,某一天周六我在公司加班赶需求,发现需要在一堆旧逻辑if-else中添加新的逻辑,强迫症的我实在是无法忍受在💩山上继续堆💩。。。


我陷入沉思,用什么方式去优化呢?看了网上责任链模式的实现,感觉还是不够优雅。


无意中翻到了之前用koa写的项目,突然灵光乍现💡,koa的中间件不就是一个很棒的实践。调用next就能够往下一个节点走,不调用的话就可以终止。


于是立即动工,三下五除二就完成了。我还推荐给部门的其他前端小伙伴,他们也在一些需求的复杂逻辑中有运用。


总结



过去无意学到的某个知识,或者某个概念,在未来也许会发挥作用,你只需要做的就是等待。



没有koa这么棒的库,估计也不会有这工具了,所以还是得感谢它的作者如此聪明。😁


如果你也喜欢这个工具,欢迎去github里给个🌟,感谢。


如果有什么更好的建议,在底下留言,一起探讨。


工具链接


res-chain


参考



作者:Johnhom
来源:juejin.cn/post/7368662916151377959
收起阅读 »

你没见过的【只读模式】,被我玩出花了

web
前言 不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格、描述列表、查询表格 吗?先看看效果吧 ~ 表单场...
继续阅读 »

前言


不是标题党,不是标题党,不是标题党,重要的话说三遍!大家常见的【只读模式】,下面简称 readonly,可能最常用的是在 表单场景中,除了正常的表单场景,你还会想象到它可以应用在我们中后台场景的 编辑表格描述列表查询表格 吗?先看看效果吧 ~


表单场景


form-readonly.gif


表单列表场景


form-list-readonly.gif


描述列表场景


description-readonly.gif


查询表格场景


table-readonly.gif


编辑表格场景


edit-table-readonly.gif


上面看到的所有效果,背后都有 readonly 的存在



  1. 表单场景示例中表单列表场景示例中 使用 readonly,在实际业务中可能会应用到 编辑详情

  2. 描述列表场景示例中 使用 readonly,在实际业务中可能会应用到 单独的详情页 页面中

  3. 查询表格场景示例中 使用 readonly,在实际业务中应用很广泛,比如常见的日期,后端可能会返回字符串、空、时间戳,就不需要用户单独处理了 (挺麻烦的,不是吗)

  4. 编辑表格场景示例中 使用 readonly,在做一些类似 行编辑单元格编辑 功能中常用


下面就以 实现思路 + 伪代码 的方式和大家分享 readonly 的玩法


以 Date 组件为例


我们这里说的 Date 就是单纯的日期组件,不包含 pickermonth(月份)quarter(季度) 等,我们先思考一下,如何让 日期组件 可以在多处公用(查询表格、表单、编辑表格、描述列表)


多处公用


我们可以将 Date 组件进行封装,变成 ProDate,我们在 ProDate 中扩展一个属性为 readonly,在扩展一个插槽 readonly,方便用户自定义,以下为伪代码


<script lang="tsx">
import { DatePicker, TypographyText } from 'ant-design-vue'

export default defineComponent({
name: 'ProDate',
inheritAttrs: false,
props: {
readonly:{
type: Boolean,
default:false
}
},
slots: {
readonly: { rawValue: any }
},
setup(props, { slots, expose }) {
const getReadonlyText = computed(() => {
const value = toValue(dateValue)
return getDateText(value, {
format: toValue(valueFormat),
defaultValue: toValue(emptyText),
})
})

return {
readonly,
getReadonlyText,
}
},
render() {
const {
readonly,
getReadonlyText,
} = this

if (readonly)
return $slots.readonly?.({ rawValue: xxx }) ?? getReadonlyText

return <DatePicker {...xxx} v-slots={...xxx} />
},
})
</script>


上面的伪代码中,我们扩展了 readonly 属性和 readonly 插槽,我们 readonly 模式下会调用 getDateText 方法返回值,下面代码是 getDateText 的实现


interface GetDateTextOptions {
format: string | ((date: number | string | Dayjs) => any)
defaultValue: any
}

// 工具函数
export function getDateText(date: Dayjs | number | string | undefined | null, options: GetDateTextOptions) {
const {
format,
defaultValue,
} = options

if (isNull(date) || isUndefined(date))
return defaultValue

if (isNumber(date) || isString(date)) {
// 可能为时间戳或者字符串
return isFunction(format)
? format(date)
: dayjs(date).format(format)
}

if (isDayjs(date)) {
return isFunction(format)
? format(date)
: date.format(format)
}

return defaultValue
}

好了,伪代码我们实现完了,现在我们就假设我们的 ProDate 就是加强版的 DatePicker,这样我们就能很方便的集成到各个组件中了


集成到 表单中


因为我们是加强版的 DatePicker,还应该支持原来的 DatePicker 用法,我们上面伪代码没有写出来的,但是如果使用的话,还是如下使用


<template>
<AForm>
<AFormItem>
<ProDate v-model:value="xxxx" />
</AFormItem>
</AForm>

</template>

这样的话,我们如果是只读模式,可以在 ProDate 中增加 readonly 属性或插槽即可,当然,为了方便,我们实际上应该给 Form 组件也扩展一个 readonly 属性,然后 ProDatereadonly 属性的默认值应该是从 Form 中去取,这里实现我就不写出来了,思路的话可以通过在 Formprovide 注入默认值,然后 ProDate 中通过 inject


好了,我们集成到 表单中 就说这么多,实际上还是有很多的细节的,如果大家想看的话,后面再写吧


集成到 描述列表中


描述列表用的是 Descriptions 组件,因为大部分用来做详情页,比较简单,所以这里我将它封装成了 json 方式,用 schemas 属性来描述每一项的内容,大概是以下用法


<ProDescriptions
title="详情页"
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

解释一下:


上面的 schemas 中的项可以简单看成如下代码


<DescriptionsItem>
<ProDate
readonly
:value="get(dataSource,'time')"
/>

</DescriptionsItem>

我们在描述组件中应该始终传递 readonly: true,这样渲染出来虽然也是一个文本,但是经过了 ProDate 组件的日期处理,这样就可以很方便的直接展示了,而不用去写一个 render 函数自己去处理


集成到 查询表格中


实际上是和 集成到描述列表中 一样的思路,无非是将 ProDescriptions 组件换成 ProTable 组件,schemas 我们用同一套就可以,伪代码如下


<ProTable
title="详情页"
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

当然我们在 ProTable 内部对 schemas 的处理就要在 customRender 函数中去渲染了,内部实现的伪代码如下


<Table 
:columns="[
{
title:'日期',
dataIndex:'time',
customRender:({record}) =>{
return <ProDate
readonly
value={get(record,'time')}
/>
}
}
]"

/>

ProTableProDescriptions 的处理方式是类似的


集成到 编辑表格中


没啥好说的,实际上是和 集成到表单中 一样的思路,伪代码用法如下


<ProForm>
<ProEditTable
:data-source="{time:'2023-01-30'}"
:schemas="[
{
label:'日期',
name:'time',
component:'ProDate'
}
]"

/>

</ProForm>

我们还是复用同一套的 schemas,只不过组件换成了 ProEditTable,不同的是,我们在内部就不能写死 readonly 了,因为可能会 全局切换成编辑或者只读某一行切换成编辑或者只读某个单元格切换成编辑或者只读,所以我们这里应该对每一个单元格都需要定义一个 readonly 的响应式属性,方便切换,具体的实现就不细说了,因为偏题了


结语


好了,我们分享了 只读模式 下不同组件下的表现,而不是简单的在 表单中 为了好看而实现的,下期再见 ~


作者:一名爱小惠的前端
来源:juejin.cn/post/7329691357211361318
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


2024-04-03 更新:这段时间在刷牛客,无意间看到了 25 届佬: 收心檬 的个人主页 - 文章 - 掘金 (juejin.cn) 美团暑期实习的面经,绷不住了🤣


pic.png


原面经链接:美团暑期一面_牛客网 (nowcoder.com)


不知道这位面试官是不是看了我的文章出的题,例子举的都大差不差🤣


我确实标题党了想整个活,没想到大厂面试官真出啊,还是实习生,刁难人有一手的🙃...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


作者:討厭吃香菜
来源:juejin.cn/post/7312349904046735400
收起阅读 »

凯文·凯利给我们的 42 个人生建议

五一回到老家,如果以能住一晚为标志,那大概也有十年没有回老家了。 把车停好,就听到熟悉的蛙鸣声,闻着带着些许水气的潮湿的空气。 恍然 仿佛回到了那个老爸老妈还很年轻,我还要骑着自行车,自己做早餐,早早起来上学的年纪。 一切仿佛还在昨天,但一切都已经不一样了。 ...
继续阅读 »

五一回到老家,如果以能住一晚为标志,那大概也有十年没有回老家了。


把车停好,就听到熟悉的蛙鸣声,闻着带着些许水气的潮湿的空气。


恍然


仿佛回到了那个老爸老妈还很年轻,我还要骑着自行车,自己做早餐,早早起来上学的年纪。


一切仿佛还在昨天,但一切都已经不一样了。


四十不惑,不是不疑惑,应该是有些事情不计较,有些东西,想想算了,想想放下了。


前段时间读了凯文·凯利 2023 年的新书《宝贵的人生建议 : 我希望早点知道的智慧》中有提到这本书的的 目标是传递经过时间检验的智慧,但是用我的话表达出来。


这是一本小书,在读完后,我对于其中认同的建议,我也用自己的话,中国传统表述或者之前一些读的书之类的提到的句子尝试理解和表达。大概做了个分类,不是说教,也就是表达一下。


学习和成长


1. 终身学习



毫不犹豫地自我投资——

花钱上课,学习新技能。

这些不起眼的投资,

能产生丰厚的回报。



保持好奇心,读万卷书,行万里路


2. 读史使人明智



大量阅读历史,

你就会明白

过去发生过多少怪事;

这样,对于未来的怪事,

你将见怪不怪。



司马迁在《史记》中说:"究天人之际,通古今之变,成一家之言。"


以铜为鉴,可以正衣冠;以史为鉴,可以知兴替。


《圣经·旧约》中说:太阳底下没有新鲜事


3. 费曼学习法



学习的

最好方法是,

试着把你

会的东西

教给别人。



输出倒逼输入


4. 敏而好学,不耻下问



不要害怕问

听上去愚蠢的问题。

因为在99%的情况下,

其他人都在想

同一个问题,

只是不好意思问出口。



5. 三省吾身



无论在什么年纪,

你都可以问自己:

“为什么我还在做这件事?”

对这个问题,

你需要进行很好的回答。



有点扎心,吾日三省吾身


我为什么还在写文章,为什么还在工作?


6. 开始写作吧



画画能画出你看到了什么。

写作能揭示出你的所思所想。



7. 多读书



要不同凡响,

就需要读书。



曾经一直在简历上写:好读书不求甚解


也是如此践行,量变最终会产生质变。


8. 直面困难



作为一个成熟的人,
衡量你成长的尺度是,
你愿意进行多少令人不舒服的谈话。



近些年越发觉得自己成熟了


不破不立,不塞不流,不止不行。


家庭生活和教育


9. 门当户对



你不是与一个人结婚,

你是与一家人结婚。



婚姻应该在门第相当、家境相似的人家之间进行。这不仅是为了维护身份地位,更是为了确保两个家庭的文化背景和生活方式能够兼容。


选择一个人,就是选择一种生活方式。


10. 善待你的孩子



善待你的孩子,

因为以后是他们为你选择养老院。



树高千丈,叶落归根


积善之家,必有余庆。


善待那个最终决定拔不拔管子的孩子


11. 最好的教育



经常给孩子读书

是他们能受的最好的教育。



12. 回家吃饭



对你的家庭来说,

最好的良药是:

经常在一起吃饭,

不开电视。



今年的一个小目标是一周到少回家吃一次晚饭。但是过了这么久,好像很少。


成功


13. 终局思维



做事要以终为始。

碗碟架堆满后,

再想调整,

就无从下手了。



谋定而后动


凡事预则立,不预则废。


14. 要有备份



制做任何东西,

都要额外多做一些准备,

比如额外的

材料、零件、空间、装饰。

这些额外的东西是

应对错误的保障,

能减轻压力,

防范未来的风险。

这是最便宜的保险。



在程序员界流传着这样一句话:「冗余不做,日子甭过;备份不做,十恶不赦」


15. 坚持



努力,

无论锻炼、陪伴还是工作,

重要的不是数量,

而是坚持。

坚持每天做一点,

比什么都强,

这比你偶尔一为重要得多。



不积跬步,无以至千里;不积小流,无以成江海


成功三要素: 坚持,不要脸,坚持不要脸


16. 长期主义



我们往往高估

一天能完成的事,

而低估十年能取得的成就。

拿出十年来,

你可以成就

不可思议的奇迹。

坚持长期主义,

积小胜为大胜,

即使犯了大错误,

也可以慢慢改正。



长期主义,做时间的朋友


17. 复利



无论财富、
人际关系还是知识,

生活中那些最大的奖赏,

都来自

神奇的复利,

即微小的、稳定的收益不断放大。

要实现富足,

你所需的不过是,

持之以恒地让投入比减损大1%。



做时间的朋友


18. 好事多磨



坏事可能飞速发生,

但几乎所有好事都是慢慢展开的。



厚积薄发


瓜熟蒂落,水到渠成。


欲速则不达,见小利则大事不成。


19. 买卖时间



每个人的时间都是有限的,

每个人的时间都在不断减少。

你能用钱获得的最高杠杆,

就是买别人的时间。

在可能的情况下,

要聘请员工,

外包工作。



《认知红利》中提到时间商人的四种经营模式,第三种:买卖时间:本质是个放大器,通过买入别人的时间,来提升自己的效率、提高时间单价、扩大生产规模。


20. 要做多



有限的游戏,

关乎输赢。

无限的游戏,

则让游戏继续下去。

去玩那些无限的游戏,

因为无限的游戏

能带来无限的回报。



人生如逆旅,我亦是行人


输赢、得失,都只是人生的过眼云烟。


真正重要的,是在这个过程中,我们有没有不断提升自己,有没有始终保持一颗向上的心。


21. 慢可能是快



多任务操作是一个迷思。

走路、跑步、骑自行车或开车时,

不要发信息。

稍停片刻没关系,

没有人会因为这一分钟忘记你。



记得小时候有篇课文是讲时间并行的,一直这样做事情,觉得效率高,这么多年过去了,发现有时候专注的慢也是一种快。保持专注 一次只做一件事,把事情做完


22. 耐得烦



培养对小事的耐心,

你才能对大事保持耐心。



作为一个洗碗十多年的非专业选手,在多年的洗碗过程中慢慢体会了这种耐心。


在《道德经》中,老子曾说:"合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。"


23. 打破常规



成功最可靠的方法,

是你自己定义成功。

先射箭,

然后在射中的地方,

画一个靶心。



孟子说:"舜何人也?予何人也?有为者亦若是。"


有人说:成功者都是创造机会,而不是等机会


24. 但行好事,莫问前程



当你陷入困境或力不能支时,

专注在力所能及的小事上,

这能推进事情的进展。



冯唐说面对逆境时: 看脚下,不断行,莫存顺逆


25. 聚集



在博物馆里,

你需要花至少10分钟,

才能真正地欣赏一件艺术品。

哪怕看5件展品,

每件花10分钟,

也不要看100件展品,每件花30秒。



有舍有得,百鸟在林,不如一鸟在手


工作和生活


26. 迈出舒适区



最好的工作

是一个你不够格的工作,

因为它会迫使你挖掘潜力。

事实上,

要只去应聘那些

你不够格的工作。



挑战自己,迈出舒适区


人往高处走


27. 断舍离



你的时间和空间是有限的。

那些不能再给你

带来快乐的东西,

要移走、送人、扔掉,

给能给你

带来快乐的东西

腾出时间和空间。



28. 知易行难,只是没钱



能轻松用钱解决的问题

不是真正的问题,

因为解决办法显而易见。

把注意力集中在那些

没有显而易见的

解决办法的问题上。



然而现实是大多数人没钱


29. 我选择早到



准时代表着尊重。




没有“准时”这回事。

要么你迟到了,

要么你早到了。

这是你的选择。



30. 你有什么建议吗



如果你寻求别人的反馈,

你会得到批评。

但如果你寻求建议,

你会得到一个搭档。



31. 圣人不器



穿过一个可能禁止你通行的地方,

你要表现得轻松自如,

就像你本属于这里。



别问可不可以,问了就是不可以


32. 以德报怨,何如?



当你原谅别人时,

对方可能没有察觉,

但你会释怀。

宽恕不是为了别人,

宽恕是我们给自己的礼物。



《道德经》中说:"不伐善,不夸能,不矜功,夫唯不争,故天下莫能与之争。"


不要内耗,放过自己


33. 喝喝酒



请客吃饭永远是有效的方法,

而且简单易行。

这对老朋友很有效,

也是结交新朋友的好方法。



酒逢知己千杯少,话不投机半句多。


带团队过程中,喝酒后的大家都是不一样的。


34. 听其言,不如观其行



你是什么样的人,取决于你做什么。

不在于你说什么,

不在于你信什么,给谁投票,

而在于,

你把时间花在什么上面。



躬身入局,贵在实践。


注意力是人最重要的资源


35. 我本善良



每当要在正确和

善良之间做出选择时,

你都要毫无例外地选择善良。

不要把善良和软弱混为一谈。



孔子说:"志士仁人,无求生以害仁,有杀身以成仁。"


36. 分权



分东西时,

一个人分,

另一个先选。



分权的逻辑


君子和而不同,小人同而不和。


37. 坦诚



始终在一开始就提出你想要什么。

这适用于人际关系、商业和生活。



38. 对自己好一点



人生三分之一的时间

是躺在床上睡觉,

几乎另外三分之一,

是在椅子上坐着。

花钱买好床、好椅子,

是物有所值的投资。



还有好的枕头


39. 休息一下



如果你不能确定自己迫切需要什么,

你迫切需要的也许是睡觉。



小时候看聪明的一休,开头都会说,不要着急不要着急,休息休息一会儿


40. 人生得意须尽欢



不要把精美的瓷器和好酒,

非留到难得的场合才拿出来,

这一等可能就是永久;

只要有机会,就可以拿出来。



在耳熟能详的中国诗歌中就有两句非常有名的:



  • 人生得意须尽欢,莫使金樽空对月

  • 花开堪折直须折,莫待无花空折枝


人生百年,如白驹过隙


人到中年,也越发觉得如此,应该让自己开心一些。


41. 事不过三



对每个人,

都要给第二次机会,

但不要给第三次。



这里其实只有二次,与中国传统的事不过三的说法差一次。


42. 遗憾



人生中只有很少的遗憾,

是遗憾自己做了什么。

几乎所有的遗憾都是遗憾自己没有做什么。



最后以左宗棠的对联结束本篇文章,见下图:


发上等愿,结中等缘,享下等福;择高处立,寻平处住,向宽处行.png
发上等愿,结中等缘,享下等福;择高处立,寻平处住,向宽处行


作者:潘锦
来源:juejin.cn/post/7363491538288787494
收起阅读 »

幸福不搞末位淘汰制

来深圳又已两周了,每次初来深圳的时候皮肤都会很难受,不知道是空气质量差还是空气湿度高,浑身都会长一些小疹子和痒痒的包,已经连着几天没有睡好了,既然睡不着那就写点东西,顺便发发牢骚吧。 开始表达 随着年龄的增长,我想,人的表达欲确实是会不断的下降,上一次半夜睡不...
继续阅读 »

来深圳又已两周了,每次初来深圳的时候皮肤都会很难受,不知道是空气质量差还是空气湿度高,浑身都会长一些小疹子和痒痒的包,已经连着几天没有睡好了,既然睡不着那就写点东西,顺便发发牢骚吧。


开始表达


随着年龄的增长,我想,人的表达欲确实是会不断的下降,上一次半夜睡不着写长文还是大一时和几个室友一直聊天到三四点。会不会有一天我对爱的人也不再有表达的欲望,选择三缄其口了呢?我不知道,但是总觉得这是一件很可怕的事情。严格来说我的表达欲也不是单纯的线性下降,中学、尤其是初中时期因为各种各样的原因导致我很自卑,自卑无论对哪个时期的任何人来说都一定是一个很严重的debuff。很幸运的是之后碰到了很多很好很好的人,慢慢的也逐渐走出了泥潭,变得开朗了。


和大部分人一样,大概从高中的时候心智就逐渐趋于成熟了吧,虽然依旧很幼稚,但是那时开始对身边发生的一些事、一些人、社会上的一些热点事件,进行各种各样的分析,得到各种各样经验性的结论,创造出各类只有自己才知道的名词,但是后来发现这些词所代表的含义早就有先贤提出了,虽然没什么意义,但是下意识思考为什么的习惯确实是在那个时候养成的。印象很深的是当时有想到刺猬人的概念,有的人就像刺猬一样,接受不了任何负面的评价,一旦你对他们稍稍辞严令色,他们就会立马竖起全身的刺对你进行攻击,想尽所有办法来驳斥、回击、批判你,绝不会想想自己是否真的存在对应的问题。So,我从高中开始很少对任何人进行任何形式的批评,如果真的有傻逼影响到我的心情的话,那么他不会有第二次影响我心情的机会了。


但是我一直都不爱表达和记录。我也记不清是从什么时间节点开始的,逐渐用一些笔记app习惯性的记录下来每天干了些什么,自己的一些随笔想法,新接触到的一些有用的观点和方法等等,逐渐养成了表达、记录的习惯。

我一直觉得人是环境的产物,这个环境既有时代背景,有当下所处的环境,也有一路走来的经历。有时候感觉自己真的很奇怪,不知道具体是哪部分环境影响到了自己,但是可以确定的是心理上有着不小的问题。比如危机意识过重,总是认为自己的处境不算安全,于是经常处于忙碌的状态,总是想多学点东西、提升提升自己,多做一些能规避风险,拓宽安全边界的事情,常常周末也不会停歇。这种心态一定是有问题的,但是具体怎么纠正回来,我想需要以年为单位的尝试才能成功。


个体乐观、群体悲观


同时,和多数人的个体层面悲观以及国家、社会层面的乐观不同,他们对祖国的未来充满希望,但对个人的前途却一片迷茫,看不到出路。


我刚好是反过来的,对个体乐观,但对社会、制度层面悲观。只和自己相关的事情,我总是能实现或者接近目标,并以积极的角度看待问题。比如高考成绩,几乎只取决于自己,考得不好无非是再来几次;一次面试失败,不过无非是和这家公司没有缘分,多积累积累,依旧有很多机会在前面等着你;减肥失败,时间还长,只要真的想,迟早是能瘦下来的。只要命还在,又有什么困难是能真正将一个人击倒的?


但是如果涉及到人和时间这种影响因子很大的变量,事情又会变得很复杂,我又会趋于悲观。比如一段感情的维系,需要A\B双方的努力和呵护,谁能保证对方一直爱自己呢,谁又能保证以后的自己仍会爱着对方呢?从某种意义上说,几年后甚至几个月之后的你,和现在的你已经不是同一个人了。故而,我一直没有能够和一个人长相厮守、白头偕老,共同度过数十年的自信。

但是在社会制度层面,我认为很多机制、策略是不可能改变的,这些东西就像定理一样深深烙印在现实世界中。国家从某种意义上来说是一个合法的暴力机构,这就导致一旦权利运转出现哪怕一丝一毫的问题,也会产生权利对个人无情倾轧的现象,并且无论科技、时代发展到什么程度,只要有人,就一定会有阶级,就一定会有不公,这不以个人的意志为转移。当代史就是过去的历史,未来史也会是当代史,朱令案、六盘水案、承德程序员案,也只是类似窦娥冤这种封建时代悲剧话本的重演罢了。


又不知道扯到哪里去了,我是想表达什么呢?其实我想说的是每个人都有选择自己生活方式的权利,无论和对方有多么亲密,对他人的生活方式、想法、行为加以指责都是一件很傲慢的事情,子非我,安知我不知鱼之乐?顺便记录一下现在自己的所想。


记录的意义


我大概是从2023年的一月开始发朋友圈,没有细数发了多少条,大概能有个五六十条?


之前不发朋友圈,不作任何记录,也不会拍照,是因为我对自己的记忆力有信心,我觉得我可以用眼睛和脑子记住生活中各种各样的美好,记得走过的路,路上的风景,陪我走在路上的人。但是随着时间的推移,很多本应该珍藏的记忆已经慢慢模糊了,在意识到了这一点之后,记录就已经迫在眉睫了。


发朋友圈的初衷是因为微信里面有很多我觉得很重要的人,我的爱人、家人、挚友、同学,人的精力是有限的,不可能一个个分享;同时,和别人的关系也是有周期的,有很多已经很久不曾联系、但是曾经关系很好的朋友们,他们也同样重要。一条朋友圈,就能让很多我觉得重要的人知道我最近在干什么。


感觉相比于条条框框很多的上学来讲,我还是比较适合上班,虽然压力比在学校大不少,但是离自由和幸福的距离近了不止一点半点。


我可能对物质的要求没有那么高,我不想住很大的房子,三十平不到的出租屋就能让我住的很开心;我也没有想过买豪车,骑骑共享单车或者小毛驴也很舒服,还不用停车费;我也不想穷奢极欲去吃一些很豪华的大餐之类的,自己做的饭菜我吃着就很满意。甚至对于钱我也没有那么看重,我只是把钱当成一个掌握自由的工具和底气,如果真的给我很多很多钱,除了留下自己这辈子够花的那部分,其他的会给那些真的很难很难的人。


感觉自己改变最大的还是在幸福能力上的进步,在大二我就意识到了其实我打理好自己的生活、过日子的能力很差,在那时候我的一天基本是在“一天啥也不干,只在床上躺尸”和“早出晚归,一天学习时间超长,抓紧点滴时间”这两种模式中二选一,所以时常嘲笑自己骨子里是根二极管。


但现在已经完全不一样了,我能平衡好工作和生活,事业和感情,闲的时候找点提升自己的事情做,忙的时候也要抽空兜兜风做做饭。


父母身体健康、和爱人的感情稳中向好、工作顺心、三五挚友、有一些爱好。仅仅是这样,对我来说就已经足够了。


这样想想,幸福其实很简单,毕竟幸福又不搞末位淘汰制


作者:安妮的心动录
来源:juejin.cn/post/7350971151131541567
收起阅读 »