注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

何时使用Kafka而不是RabbitMQ

Kafka 和 RabbitMQ 都是流行的开源消息系统,它们可以在分布式系统中实现数据的可靠传输和处理。Kafka 和 RabbitMQ 有各自的优势和特点,它们适用于不同的场景和需求。本文将比较 Kafka 和 RabbitMQ 的主要区别,并分析何时使用...
继续阅读 »

Kafka 和 RabbitMQ 都是流行的开源消息系统,它们可以在分布式系统中实现数据的可靠传输和处理。Kafka 和 RabbitMQ 有各自的优势和特点,它们适用于不同的场景和需求。本文将比较 Kafka 和 RabbitMQ 的主要区别,并分析何时使用 Kafka 而不是 RabbitMQ。

推荐博主开源的H5商城项目waynboot-mall,这是一套全部开源的微商城项目,包含一个运营后台、h5商城和后台接口。 实现了一个商城所需的首页展示、商品分类、商品详情、sku详情、商品搜索、加入购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于最新得Springboot3.0、jdk17,整合了Redis、RabbitMQ、ElasticSearch等常用中间件, 贴近生产环境实际经验开发而来不断完善、优化、改进中。

github地址:github.com/wayn111/way…

影响因素

  1. 可扩展性:Kafka 旨在处理大容量、高吞吐量和实时数据流。它每秒能够处理数百万个事件,并且可以处理大量数据。另一方面,RabbitMQ 的设计更加灵活,可以处理广泛的用例,但可能不太适合大容量、实时数据流。
  2. 耐用性:Kafka 通过将所有数据写入磁盘来提供高度的耐用性,这对于任务关键型应用程序非常重要。 RabbitMQ 还提供基于磁盘的持久性,但这可能不如 Kafka 提供的那么强大。
  3. 延迟:RabbitMQ 设计为低延迟,这对于实时数据处理和分析非常重要。Kafka 延迟相比 RabbitMQ 会高一点。
  4. 数据流:Kafka 使用无界的数据流,即数据持续地流入到指定的主题(topic)中,不会被删除或过期,除非达到了预设的保留期限或容量限制。RabbitMQ 使用有界的数据流,即数据被生产者(producer)创建并发送到消费者(consumer),一旦被消费或者达到了过期时间,就会从队列(queue)中删除。
  5. 数据使用:Kafka 支持多个消费者同时订阅同一个主题,并且可以根据自己的进度来消费数据,不会影响其他消费者。这意味着 Kafka 可以支持多种用途和场景,比如实时分析、日志聚合、事件驱动等。RabbitMQ 的消费者从一个队列中消费数据,一旦被消费,就不会再被该队列其他消费者看到。这意味着 RabbitMQ 更适合一对一的通信或任务分发。
  6. 数据顺序:Kafka 保证了同一个分区(partition)内的数据是有序的,即按照生产者发送的顺序来存储和消费。但是不同分区之间的数据是无序的,即不能保证跨分区的数据按照全局顺序来处理。 RabbitMQ 保证了同一个队列内的数据是有序的,即按照先进先出(FIFO)的原则来存储和消费。但是不同队列之间的数据是无序的,即不能保证跨队列的数据按照全局顺序来处理。
  7. 数据可靠性:Kafka 通过副本(replica)机制来保证数据的可靠性,即每个主题可以有多个副本分布在不同的节点(broker)上,如果某个节点发生故障,可以自动切换到其他节点继续提供服务。 RabbitMQ 通过镜像(mirror)机制来保证数据的可靠性,即每个队列可以有多个镜像分布在不同的节点上,如果某个节点发生故障,可以自动切换到其他节点继续提供服务。
  8. 数据持久性:Kafka 将数据持久化到磁盘中,并且支持数据压缩和批量传输,以提高性能和节省空间。Kafka 可以支持TB级别甚至PB级别的数据存储,并且可以快速地重放历史数据。RabbitMQ 将数据缓存在内存中,并且支持消息确认和事务机制,以提高可靠性和一致性。RabbitMQ 也可以将数据持久化到磁盘中,但是会降低性能和吞吐量。RabbitMQ 更适合处理小规模且实时性较高的数据。
  9. 数据扩展性:Kafka 通过分区机制来实现水平扩展,即每个主题可以划分为多个分区,并且可以动态地增加或减少分区数量
  10. 复杂性:与 RabbitMQ 相比,Apache Kafka 具有更复杂的架构,并且可能需要更多的设置和配置,因此它的复杂性也允许更高级的功能和定制。另一方面,RabbitMQ 更容易设置和使用。

应用场景

Kafka 适用场景和需求

  • 跟踪高吞吐量的活动,如网站点击、应用日志、传感器数据等。
  • 事件溯源,Kafka 保存着所有历史消息,可以用于事件回溯和审计。
  • 流式处理,如实时分析、实时推荐、实时报警等。
  • 日志聚合,如收集不同来源的日志并统一存储和分析。

RabbitMQ 适用场景和需求

  • 中小项目,项目消息量小、吞吐量不高、对延时敏感。
  • 遗留应用,如需要与旧系统或第三方系统进行集成或通信。
  • 复杂路由,如需要根据不同的规则或条件来分发或过滤消息。
  • 任务分发,如需要将任务均匀地分配给多个工作进程或消费者。

总结

在公司项目中,一般消息量都不大的情况下,博主推荐大家可以使用 RabbitMQ。消息量起来了可以考虑切换到 Kafka,但是也要根据公司内部对两种 MQ 的熟悉程度来进行选择,避免 MQ 出现问题时无法及时处理。


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

实战:工作中对并发问题的处理

大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm&nb...
继续阅读 »

大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm 中,欢迎Star和获取原文。

1. 问题背景

问题发生在快递分拣的流程中,我尽可能将业务背景简化,让大家只关注并发问题本身。

分拣业务针对每个快递包裹都会生成一个任务,我们称它为 task。task 中有两个字段需要关注,一个是分拣中发生的 异常(exp_type),另一个是分拣任务的 状态(status)。另外,需要关注 分拣状态上报接口,通过它来记录分拣过程中的异常和状态变更。

一般情况下,分拣机在分拣异常发生时会及时调用接口上报,在分拣完成时调用接口来标记为完成状态,两次接口调用的时间间隔较长,不会发生并发问题。

但是有一种特殊的分拣机,它不会在异常发生时及时上报,而是在分拣完成时将分拣过程中发生的异常和分拣结果一起上报,那么此时分拣状态上报接口在同一时间内就会有两次调用,这时便发生了预期外的并发问题。

我们先看下分拣状态上报接口的执行流程:

  1. 先查询到该分拣任务 task,默认情况下 exp_type 和 status 均为默认值0

  2. 分拣异常修改 task 中的 exp_type,分拣完成修改 status 字段信息

  3. 修改完成将 task 写入

数据库初始值为 1, 0, 0,分拣异常和分拣完成几乎同时上报,它们都读取到该值。分拣异常动作将 exp_type 修改为9,写入数据库,此时数据库值为 1, 9, 0;分拣完成动作将 status 修改为1,写入数据库,使得数据库最终值为 1, 0, 1,它将异常字段的值覆盖掉了。正常情况下,最终值应该为 1, 9, 1,分拣完成动作应该读取到分拣异常完成后的值 1, 9, 0 后再进行修改才对。

2. 解决方案

发生这个问题的原因很容易就能发现:两个事务同时执行 读取-修改-写入 序列,其中一个写操作在没有合并另一个写操作变更的情况下,直接覆盖了另一个写操作的结果,所以导致了数据的丢失。

这种问题是比较典型的 丢失更新 问题,可以通过对数据库读操作加锁或者改变数据库的隔离级别为可串行化使事务串行执行的方式进行避免。下面我会将大家在讨论避免丢失更新问题时提出的方案进行介绍,并尽可能的用代码来表现它们。

2.1 数据库读操作加锁和可串行化隔离级别

我们可以考虑:如果对每条Task数据修改的事务都是在当前事务完成之后才允许后续事务进行修改,使事务串行执行,那么我们就能够避免这种情况。比较直接的实现是通过显式加锁来实现,如下

select exp_type, status
from task
where id = 1
for update;

先查询该行数据的事务会获取到该行数据的 排他锁,后续针对该数据的所有读写请求都会被阻塞,直到先前事务执行完将锁释放。

这样通过加锁的方式实现了事务的串行执行。但是,在为SQL添加加锁语句时,需要确定是不是为该行数据加锁而不是锁住了整个表,如果是后者,那么可能会造成系统性能严重下降,而且还需要关注有哪些业务场景使用到了该SQL,是否存在长时间执行的只读事务使用,如果存在的话可能会出现因加锁导致延迟和系统性能下降,所以需要谨慎的评估。

此外,可串行化的数据库隔离级别也能保证事务的串行执行,不过它针对的是所有事务。一般情况下为了保证性能,我们不会采用这种方案(默认使用MySQL可重复读隔离级别)。

MySQL的InnoDB引擎实现可串行化隔离级别采用的是2PL机制:在第一阶段事务执行时获取锁,第二阶段事务执行完成释放锁。

2.2 针对业务只修改必要字段

如果异常状态请求仅修改 exp_type 字段,分拣完成仅修改 status 字段的话,那么我们可以梳理一下业务逻辑,仅将必要修改的字段写入数据库,这样就不会发生丢失更新的异常,如下代码所示:

// 处理异常状态请求,封装修改数据的对象
Task task = new Task();
tast.setId(id);
task.setExpType(expType);

// 更改数据
taskService.updateById(task);

在执行修改数据前,创建一个新的修改对象,并只为其必要修改字段赋值。但是还需要考虑的是:如果这个业务流程处理已经很复杂了,很可能不清楚该为哪些字段赋值而导致再发生新的异常,所以采用这种方法需要对业务足够熟悉,并且在修改完后进行充分的测试。

2.3 分布式锁

分布式锁的方法与方法一类似,都是通过加锁的方式来保证同时只有一个事务执行,区别是方法一的锁加在了数据库层,而分布式锁是借助Redis来实现。

这种实现方式的好处是锁的粒度小,发生锁争抢仅限于单个包裹,无需像数据库加锁一样去考虑锁的粒度和对相关业务的影响。伪代码如下所示:

// 分布式锁KEY
String distributedKey = String.format(DISTRIBUTED_KEY_PREFIX, packageNo);
try {
// 分布式锁阻塞同一包裹号的修改
lock(distributedKey);
// 处理业务逻辑
handler();
} finally {
// 执行完解锁
redissonDistributedLocker.unlock(distributedKey);
}

需要注意,lock() 加锁方法要保证加锁失败或发生其他异常情况不影响业务逻辑的执行,并设定好锁持有时间和等待锁的阻塞时间,此外解锁方法务必添加到 finally 代码块中保证锁的释放。

2.4 CAS

CAS是乐观的解决方案,它一般通过在数据库中增加时间戳列来记录上次数据更改的时间,当新的事务执行时,需要比对读取时该行数据的时间戳和数据库中保存的时间戳是否一致,以此来判断事务执行期间是否有其他事务修改过该行数据,只有在没有发生改变的情况下才允许更新,否则需要重试这个事务。样例SQL如下所示:

update task 
set exp_type = #{expType}, status = #{status}, ts = #{currentTs}
where id = #{id} and ts = #{readTs}

它的原理不难理解,但是实现起来可能会存在困难,因为需要考虑在执行失败后该如何重试,重试的方式和重试的次数需要根据业务去判断。


作者:方圆想当图灵
链接:https://juejin.cn/post/7261600077915357243
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术?首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?

首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。

那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?

先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。

为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。

对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。

对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。

那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?

所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?

也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。

出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。

那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。

其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。

第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。

我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。

换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。

所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。

那怎么样能够保证你自己有一个技术细节的敏感度?

我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。

当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。

自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。

总结

所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。

当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


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

前端发展:走进行业迷茫的迷雾中

引言2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。第一部分:前...
继续阅读 »

引言

2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。

第一部分:前端的价值

前端开发作为网页和移动应用程序开发的重要组成部分,扮演着连接用户与产品的桥梁。前端技术的发展不仅推动了用户体验的提升,也对整个互联网行业产生了深远的影响。随着移动互联网的普及和技术的进步,前端在用户与产品之间的交互变得越来越重要。

对于企业而言,拥有优秀的前端开发团队意味着能够提供更好的用户体验、增强品牌形象、吸引更多用户和扩大市场份额。因此,前端开发的技能依然是企业争相追求的核心能力之一。

第二部分:行业不景气的背后

然而,正如每个行业都经历高低起伏一样,前端开发也面临着行业不景气带来的挑战。2023年,全球经济增长乏力、市场竞争激烈以及萧条的就业市场等因素,使得许多公司紧缩预算、停止招聘,并导致了失业率的上升和裁员的潮水。

在这种情况下,前端开发者需要重新审视自己的技能和市场需求。他们需要具备综合能力,包括对最新前端技术的深入了解、与其他团队成员的良好沟通合作能力以及持续学习和适应变化的能力。

第三部分:自我调整与进阶

面对市场变化和就业压力,前端开发者需要主动调整自己的发展路径。以下是一些建议:

  1. 多元化技能:学习并精通多种前端框架和库,如React、Vue.js和Angular等。同时,了解后端开发和数据库知识,拥有全栈开发的能力,将会让你在就业市场上更具竞争力。
  2. 学习与实践并重:不仅仅是学习新知识,还要将所学应用于实际项目中。积累项目经验,并在GitHub等平台分享你的作品,以展示自己的能力和潜力。同时,参加行业内的比赛、活动和社区,与他人交流并学习他们的经验。
  3. 持续学习:前端技术发展日新月异,不断学习是必需的。关注行业的最新趋势和技术,参加培训、研讨会或在线课程,保持对新知识的敏感度和学习能力。

第四部分:面对就业市场的挑战

在面对行业不景气和裁员的情况下,重新进入就业市场变得更加具有挑战性。以下是一些建议:

  1. 提升个人竞争力:通过获得认证、实习或自主开发项目等方式,提升自己在简历中的竞争力。扩展自己的专业网络,与其他开发者和雇主建立联系。
  2. 寻找新兴领域:探索新兴的技术领域,如大数据、人工智能和物联网等,这些领域对前端开发者的需求逐渐增加,可能为你提供新的机会。
  3. 转型或深耕细分领域:如果市场需求不断减少,可以考虑转型到与前端相关的领域,如UI设计、交互设计或用户体验设计等。或者在前端领域深耕细分领域,在特定行业或特定技术方向上寻找就业机会。

结论

 虽然当前的行业环境确实严峻,但前端开发作为连接用户与产品的重要纽带,在未来依然有着广阔的发展空间。关键在于前端开发者要不断自我调整与进阶,持续学习并适应市场需求。通过多元化技能、学习实践、提升个人竞争力以及面对市场挑战,前端开发者依然可以在这个变革时代中谋得一席之地。


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

一代枭雄曹操也需要借力,何况我们

前言1、人情世故如果做得好就会说是情商高,做不好会说是世故,这是冯仑老师一段话,然后怎么做不世故呢,也很难评判。借着这个聊聊人情世故,在我看来它也是做事规则的一部分,我们发展很长一段历史,从不同的立场、不同的利益分出了派别,又从血缘关系分出了宗族,这些都是为了...
继续阅读 »

前言


1、人情世故

如果做得好就会说是情商高,做不好会说是世故,这是冯仑老师一段话,然后怎么做不世故呢,也很难评判。

借着这个聊聊人情世故,在我看来它也是做事规则的一部分,我们发展很长一段历史,从不同的立场、不同的利益分出了派别,又从血缘关系分出了宗族,这些都是为了利益最大化的一个产物。

反观博主本人,典型理工男,执着技术研究,所以这块一直是弱项,不太会讲话,但是我人缘一直比较好的。当然有利也有弊,弊端的话比较明显的,当一个人说话很厉害的时候,会给人自信,给人觉得靠谱,当一个人说话不咋样的时候,其实也有好处,就是藏锋,你不说出来个人想法大家是不知道你心里的小九九的,所以保全了你自身。(当一个人份量足的时候,说话会引发很大的影响,所以你可以发现如果一个人在公开场合大发演讲,要么是初出茅庐要么就是有靠山)

2、人生的发展需要平台

王立群老师:人生发展往往需要平台,秦国李斯这么一个故事,他发现仓鼠跟厕鼠待遇很不一样,同样是一个物种,但是一个光明正大的吃着粮食,一个过街老鼠人人喊打,所以他悟到了一个道理,人生好的发展需要借助平台的。

我们今天讲的人物:曹操,我们还是从几个学习角度去看,一个是做事的方法,另一个我们从他的事迹里面看出成事的借力的这么一回事。

曹操


出身

他祖父是一个大太监,伺候皇后还有皇上,古代有三股力量,两股都是因为比较亲近产生的,一个是外戚,另一个太监,还有一股力量是文官,这个是人数最多的。那么他祖父权利很大的,然后收了一个义子也就是曹操的父亲,然后他本身属于夏侯家族,所以他带的资源是曹家还有夏侯家非常有实力。

他并没有说直接躺平,而是想着有所作为,接下来我们再看看他的做事方面

做事手段

1、许劭风评

古代有个一个规则,靠着这些有能力、有品德的人来进行推荐人才,曹操想出来做事,他找到许劭,一开始是不肯的,因为前面讲过三股力量,文官是很鄙视太监的,后面曹操使了点手段最终让许劭给他做了风评,然后他听完大笑而去。

idea:从这件事看做什么事都是有个窍门,这个方式是别人建议曹操这么干,所以做事要恰到好处。另外里面提到曹操使了点手段,哈哈透出了一个狠,有点东西。

2、傍大腿

曹操曾经在袁绍下面干活,然后好几次都把自己的精锐干没了,袁绍作为盟主,慷慨的给予兵马才得以恢复元气。

idea:我们看曹操的出身,这么牛逼的背景,他也需要大腿的支持,更何况普普通通的我们。

3、挟天子以令诸侯

这个是非常著名的历史典故,也是因为这个跟袁绍闹掰了,当汉献帝去了洛阳的时候,他马上去迎接,然后用这个发号施令讨伐别人。

idea:曹操的眼光十分毒辣,他看出潜在的价值,不愧是曹老板。

4、善用人才

像官渡之战,像迎接汉献帝,都是底下这批谋士给的主意,曹操手下文官是人才济济的,另外这个老板是善于听从这些好的计谋,这是非常重要的。

官渡之战,袁绍没有听从谋士的重兵把守粮草,导致给了曹操抓住了机会,乌巢一把火烧光了粮草。

个人看法

a、平台是重要的,借力也是需要的

从曹操的发迹来看,他站在一个大平台上面,不像刘备四处投奔。人并不是说能力很强就能表现出来,需要有平台,有这么伯乐去发现你,然后有这么一股力量在你困难的时候拉你一把,这是重要的。

b、曹操做事狠

这里的狠,不是残暴,而是毒辣,眼光毒辣、做事方式到位,我们从善用人才,许劭风评,挟天子以令诸侯,这些做的都很到位。举个例子,比如说我们要煮开一壶水,需要火柴、木头、可能需要鼓风工具,这都是关键那些点。

这个我们前面也提到了,做事一定要有所研究,事情的关键点是什么,当然有这么一群得力助手也很重要,发现关键突破点。所以古代对英雄标准是:腹有良策,有大气概。

c、驾驭人

司马家起来是在曹操去世后几代的事情,可以说在曹操在的时候,这些有心机的人没有动作的,侧面看出曹操的厉害之处,懂人心。在资治通鉴里面也有一个例子,就是桓温,他也是古代一个权臣,后面几代就不行了压不住这批人。

学历史,学读懂人心


历史里面基本都是那个朝代的精英,他们的事迹,做事方法,当然我们看到很多东西,包括抱负、无奈、遗憾;我们学的不仅仅是做事方法,避开权谋的陷阱,还有就是学习读懂人心、人性。当我们谈到这个,大家第一印象就是坏的人性,其实它是一种自然的表现,就像饿了就要吃饭。

《百家讲坛》里面讲了这么一个故事,曹操的下邳之战生擒了吕布,原本曹操很爱惜人才的,后面刘备的一句话:吕布对以往老板不好,而曹操生性多疑,最终嘎了吕布。王立群老师:人们往往看重结果,以结果说话,而不是问你这么做的原因。

是啊,我们在故事背后,看到整件事情人心的博弈,刘备被人称为仁义之君,但是他在那会落进下石了,因为他之前跟吕布有些矛盾的,吕布把他从原来的根据地赶走了,当然他说的也是事实。所以我们除了学习历史,还需要去洞察人心,往往这些能决定事情的走向。


作者:大鸡腿同学
链接:https://juejin.cn/post/7261231205353242682
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

独立开发前100天真正重要的事

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功...
继续阅读 »

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功(没有走的很远),所以只能分享独立开发前100天的经验。

先说一下我认为独立开发起步阶段面临的主要困难:

第一:没有公司的孤独感。如果是一个人全职开发就更寂寞了。即使有一两个合作伙伴,但是大概率也是异地,因此也算是网友性质的社交。人说到底是群居的,所以需要找到一种社交平衡。我想可能这也是很多独立开发白天要在外面地方待着的原因,也许一个人一直在家待着有点闷。

第二:无法建立产品的健康开发节奏。以前在公司的时候自己是流程里的一环,只关心自己分工的完成情况。做了独立开发以后,所有事情都需要自己决策。太多自由的结果就是没了方向。什么都想做,好像什么都可以做,又感觉什么都不好做。

第三:没有收入。产品从开始建造到有足够健康收入中间有一段过程。这还有一个前提要是一个真正有用户价值的产品。如果你起步的时候自己没有一个优势产品方向,又没有个人社区号召力,就算你的产品是好的,也需要一段时间(可长可短)才能获得有效收入。一上线就火的概率太小了。高强度投入一件事情,如果长期没有收入,家人会有很多质疑,可能最后自己也很怀疑自己。

我把这三点结合起来,编一个故事大家可能比较有画面了:

一个人做独立开发已经半年多了,产品设计都是自己做,也没有什么人可以讨论,不知道下一步该做什么。目前每天只有零星的新增。做了这么久,总共只有两三千的收入。看来做产品还得会营销**,打算最近开始学习一下运营**。最近也打算做一下AI产品,感觉这个赛道很火。老婆说如果不行就早点回去上班好了,总不能一直这样。家人们,你们说我应该坚持吗。

家人们会说你的产品很棒,会说你做的比他们强,会说下次一定,会说你未来会成功。但是家人们不会为你掏一分钱。

也许我们不知道如何成功,但是我们可以知道什么是失败。你知道的失败方式越多,你成功的概率就越大。总的来说,产品的成功就两个要点:有用户价值,能赚钱。注意,这两点是或的关系,不是且的关系。一个产品可以能赚钱,但是没有用。一个产品也可以有用,但是不赚钱。失败就是你做的产品:既没用,又不赚钱

基于前面提到的三个困难,我得出的前100天最重要的事是:找到一个可行的产品迭代方向。和团队经过磨合,互相能有有效、信任的协作。找到一百个种子用户。你越早解决这三个困难,你越快走上轨道

确认产品方向

如果你真的做过产品,你就知道最终正确的产品路径不是通过脑中的某刻灵光乍现得到的。所以不是那种大脑飞速运算解题的方式。这里有两件事情需要确认:大的产品方向,产品的路径。

比如阿里巴巴,马云不是一开始就做的淘宝网。他只是觉得互联网普及以后,电子商务会有需求。最开始做的是黄页,并不是淘宝网。但是他没有在第一个项目失败以后,去做门户网站。产品路径的例子是特斯拉。特斯拉很早就确定了先出高性能的跑车,高性能轿车(model s),有了前面的技术积累以后,最后通过推出平价的轿车赢得市场(model 3)。特斯拉在 model 3 大规模量产前都是亏损的。

所以最重要的是确认产品方向。这个方向要结合自身的情况进行设定,就是我在前面帖子里提到的要是你想做的,能做的。也许想达到的产品方向有很多工作量,这个时候就要有同步的产品路径。比如小米手机的创业,他们一开始就想造手机。但是直接启动手机的制造市场、技术都有很大的困难。于是他们先通过做 MIUI 入局。

这里面首先要有个大的方向判断,对于独立开发来说,我觉得张宁在《创作者》里提到的两个维度的方向挺有意思:大众、小众;高频、低频。这里面两两结合各有什么特点我这里就不展开了,大家可以自行体会。

但是可以明确的是,独立开发者做不了又大众又高频的应用。大众又高频,就不可能小而美。大众又高频,最后赢家除了产品能力,要有运营优势,要有资源优势。独立开发者通常没有运营优势和资源优势。另外一点,如果是小众低频,就一定要高忠诚,高付费转化。可以往大众低频或者小众高频的方向多想想

产品方向选择还有一个建议就是要有秘密。成功的业务后面一定有秘密。秘密也回答了一个问题:如果这个需求真的存在,为什么用户选择了你的产品。

最初级的秘密就是信息差,你知道别人不知道,所以你可以,更早做,可以更低的成本,更高效,有更高的获客率。

更高级的秘密就是大家都能看到,但是大家知道了,但是大家不信(脑中想到了拼多多的砍一刀)。

最高级的秘密就是所有人都知道,但是他们做不到。

总结起来,你应该找到一个你有优势的细分方向。信息优势,洞察优势也是优势。

没有一发即中的银弹,最平凡的方式想很多方向,用最低成本进行最快速的验证。在反馈中渐渐明晰产品路径。如果你三个月不管反馈闷头做,只做出了一个产品方向。你失败的概率是很大的。所以我看到很多产品1.0 的时候就做会员,做社区,做跨平台我是很不理解的。其实这些功能在早期性价比很低。

我的方式是脑海中有10个想法,挑出3个想法做初步设计,选出一个或者两个想法做产品验证。可能是原型,数据是模拟的,没有设计,如果产品真的解决了痛点的话,用户会愿意用,然后他会给你反馈他想要更好的体验,他愿意付钱得到这些改进。这里的效率优势是,你能在更短的时间验证产品方向是不是对的。总比走了3个月才发现是一条死胡同要好。

开发者很容易因为想到一个想法很兴奋,觉得这个很有用,就闷头做了一个月。有可能的问题是,这个想法虽然是个痛点,但是这个痛点频次很低,场景很少,所以虽然有用,但是没人会愿意买单。所以尽量跳出自己的思维,从用户的角度来进行验证是很必要的。

团队协作

独立开发的开发方式和传统公司不同。需要建立一个全新的工作流程。在初期大家都是空白,所以需要通过产品迭代中,形成高效的开发默契。大家松散做东西,工作习惯,工作职责都需要有共识才行。

比如我合作的设计师早期喜欢一次做一大板块的整体设计,大概一周的工作量。初期我觉得我们对产品有激情,大家都应该有自由的发挥空间。但是做了一周的设计图和产品脑海中的产品行进方向不一致怎么办。在工作时间上,我合作的设计师因为目前还是兼职,他只能在下班后设计。然而我全职只在6点前工作。这又是一个要协调的地方。

如果你是一个产品,需要协调研发和设计,三个人协调就又更复杂了。要找到一个大家都舒服,高效的协作方式。

100个种子用户

独立开发最核心的一环就是找到一个健康的商业模式。产品方向和团队协作的目标都是为了未来可以达成一个健康的商业模式。我觉得太多独立开发者上来就把目标(野心)定的太高。一口吃不成胖子。独立开发早期的商业目标只有一个:尽快达成团队最低维持标准。一鸟在手胜过二鸟在林。不要在团队只有几个人的时候用几十个人的方式管理。

初期就要估算出产品(团队)能够持续运转的最低收入。这个成本越低,团队就越容易跑起来。当收入足够覆盖团队的成本后,你的心态就会得到极大的自由,可以尝试很多奇奇怪怪有趣的想法。所以早期不要想有多高的天花板,如何建立壁垒,就关心如何达成产品的及格生命线。谁会想做一个注定失败的产品呢。

早期在没有运营优势的情况下,最重要的指标就是用户满意度了。用户满意度,就暗示了这个产品有没有解决切实的用户问题,用户愿不愿意为你宣传。其实很多人都搞错了重点,在产品没有让100个种子用户满意前,新增的流量是没有意义的。因为再多的用户都会流失。竹篮打水一场空。如果你把产品的用户目标定在100个种子用户,你也就没了运营压力,可以关注在如何打造正确的产品上。在产品基本盘没有问题后,再思考后面的才有意义。

总结

总结起来三点就是:做什么(产品方向),怎么做(团队协作),为谁做(验证用户)。以上就是我全职独立开发3个多月以来肤浅的经验分享,希望对你有帮助。


作者:没故事的卓同学
链接:https://juejin.cn/post/7259210748801663031
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

《程序员职场工具库》从行动开始 —— MORS 法则

你是否曾经有过类似的疑惑?我尝试过好几次健身,但都坚持不下来,我是不是一个没有耐心,没有毅力的人?领导反馈说我需要提升沟通能力(开发效率、主动性),可是我要怎么提升呢?我知道该怎么做,但就是做不到呀。在我们的个人成长中,出现这些疑惑是很正常的,它们可以归纳为以...
继续阅读 »

你是否曾经有过类似的疑惑?

  • 我尝试过好几次健身,但都坚持不下来,我是不是一个没有耐心,没有毅力的人?
  • 领导反馈说我需要提升沟通能力(开发效率、主动性),可是我要怎么提升呢?
  • 我知道该怎么做,但就是做不到呀。

在我们的个人成长中,出现这些疑惑是很正常的,它们可以归纳为以下两个原因:

  • 不知道方法
  • 知道方法但不知道该怎样坚持

如果你经常被这两个问题困扰,长此以往,就会陷入自我怀疑的状态。这时,MORS 法则可以帮到你。

MORS 法则,又叫做具体性原则。MORS 法则认为,我们的行为只有符合Measured(可测评)、Observable(可观察)、Reliable(可信赖)、Specific(明确化) 这 4 个要素,才是一个具体的、可执行的行为。

针对上面说到的问题,我们不要光喊口号,而需要给出具体的、可执行的方案,然后行动起来!

也就是说,当你碰到一个很复杂的目标时,要尽量拆解目标,制定一系列符合 MORS 法则的行动计划,然后开始行动,这样就能达成目标

让我用一个栗子来解释 MORS 法则,假想一下,你今年给自己立了一个 flag:“今年要跑半马(21公里)”。要是只有这样一个目标,你能做到吗?大概率是不能的,即使你知道要每天坚持跑步,也很难坚持下来。你有可能会被临时的一些事情耽搁了几天就放弃了;你有可能在坚持了一段时间之后发现自己“根本做不到”就放弃了。

想要达成这个大目标,你可以尝试先拆解目标。假设第一次开始跑步,你可以跑 1 公里,希望 10 个月后能跑 21 公里,那就是每个月要多跑 2 公里;然后再拆到每周要多跑 0.5 公里;然后再拆解到每 2 天多跑 0.07 公里左右。这样分拆之后,就可以罗列出每天的跑步行动计划,比如:xx月xx日,跑 1.14 公里;xx月xx+1日,跑 1.21 公里... 这些行动是符合 MORS 法则的:

  • Measured:这些行动是可以被测量和评估的,你可以明确知道今天计划跑多少公里,最终有没有完成这个行动也可以很清楚地衡量和评估。
  • Observable:这些行动是可见的,你要跑起来,可能需要一些跑步装备和资源(比如准备水),这些都是实实在在的行动,不是 YY。
  • Reliable:这些行动是可以完成的,它不会太难,每天多跑 0.07 公里远远比多跑 20 公里要简单太多了,肯定能做到吧?
  • Specific:这些行动是非常明确的,就是跑步,不会有什么歧义。

当然,肯定有更好的、更加符合运动理论的跑步计划,比如是不是每天跑,还是一周休息 2 天、是应该每天突破,还是隔一天突破等等。这里只是举例,就简单一点了。不过不管是什么跑步计划,原理是不变的。

当你制定了符合 MORS 法则的行动计划之后,就能够更加容易地达成目标,也更加容易坚持下来了。因为:

  • 行动计划是具体的,明确的,可行的。相比原来的假大空的目标,它更容易做到!而且,按照这个行动计划来行动,就能达成目标。
  • 能够不断地感受到成果,这会帮助你更容易坚持下来。在这个栗子中,就是每天都多跑了一些,感受到自己每天都在进步,这是强大的正反馈,会给予你力量。

注意不要跟 SMART 原则搞混了哈。SMART 原则是制定目标时使用的,MORS 法则是制定行动计划的时候使用的。它们有一些相同的点,比如 Measured 和 Specific,那是因为每一个行动本身也是带有目的性的,所以 MORS 法则和 SMART 原则会有重叠的地方。

好了,MORS 法则的介绍就到这里了,它的内容非常简单,但是 MORS 法则对于我们的个人成长来说,作用是非常大的,希望你们可以多加实践啦。

除了在个人成长上面有帮助之外,MORS 法则对于管理者指导下属也非常有用,后面有机会我再介绍吧。

加油,让我们从行动开始!


作者:潜龙在渊灬
链接:https://juejin.cn/post/7259567288143020090
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我的日常开发收获

passive event listenerspassive event listeners 是一种新兴的web标准,Chrome 51中提供的新功能为滚动性能提供了巨大的潜在提升。Chrome Release Notes.背景:所有的现代浏览器都有一个滚动功...
继续阅读 »
  1. passive event listeners

passive event listeners 是一种新兴的web标准,Chrome 51中提供的新功能为滚动性能提供了巨大的潜在提升。Chrome Release Notes.

背景:所有的现代浏览器都有一个滚动功能的线程,可以保证即使在运行耗时的js代码时滚动也能够平滑进行,但这种优化部分因需要等待任何touchstart 和 touchmove处理程序的结果而失败,因为这些交互可能会通过调用preventDefault() 事件来完全阻止滚动。

于是有了 {passive: true}

通过将touchwheel事件监听标记为 passive,开发人员承诺处理程序不会调用 preventDefault来禁用滚动。这使浏览器可以立即响应滚动,而无需等待js的执行,从而确保为用户提供可靠流畅的滚动体验。

  1. 关于系统设计

    • 关于分层

分层一般是基于模块功能来分层,有时候分层不清晰可能是有哪些模块,各模块间的功能,整个功能流程不是很清楚。

有时候两个模块间的交互复杂度增加,可以考虑构建一个中间层。 这样可以保持两个模块不会杂糅相关度不是很高的处理逻辑,功能逻辑更纯粹,保持边界清晰,降低模块本身的复杂度。

  1. 关于编程思维

仔细想来,虽然从事开发工作很久了,但是编程上还是很没有章法,架构设计能力较弱,多年来都是凭借以前热血和几分小聪明存活。

今日份反思:

拿到一个需求,分析该需求需要支持那些场景,为了支持这些场景它需要具备哪些功能,思考怎样实现这些功能,根据功能做模块划分,对这些模块进行分析,做逻辑抽象(也就是分层),然后整个需求实现的大致框架就心中有数了,开始产出技术方案。 技术方案产出后,按照技术方案的设想去实施,实施过程中可能会遇到没考虑到的场景,或者发现之前的设计不能很好的cover,调整设计,增加分层或者调整已有的分层,然后修改技术方案。 不断的经历上述过程,会慢慢的沉淀出一些业务通用的设计思路,这样下次再做技术方案的时候就不会很迷茫。脑子理清楚,而后出设计,实践后总结。

  1. ShadowRealm API

一个进入 statge3 的新的 JavaScript 提案,用于创建一个独立的JavaScript运行环境,里面有独立的变量作用域。

数据结构:

declare class ShadowRealm {
  constructor();
// 同步执行字符串,类似eval()
  evaluate(sourceText: string): PrimitiveValueOrCallable;
// 返回一个Promise对象,异步执行代码字符串
  importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}

使用场景:

  • 在Web IDE 或 Web绘图应用程序中运行插件等第三方代码; 这种方式比iframe的实现更简单、灵活度更高,占用内存更少、代码的安全性更高。
  • 用 ShadowRealms 中创建编程环境,运行用户代码,如codepen,codesandbox;
  • 服务器可以在 ShadowRealms 运行第三方代码,防止第三方代码出错打挂主环境;
  • 网页抓取和网页应用测试可以在ShadowRealms中运行;

补充:Node.jsvm模块与ShadowRealm API类似,但具有更多功能,缓存Javascript 引擎,拦截import() 等等。

  1. 关于状态管理

做状态管理的核心就是监听数据的变化,监听数据的变化有两种方式:

  • 提供api来修改,内部做联动处理(React的setState)
  • 对对象做一层代理,set的时候做联动处理,同时get时收集所有依赖。(vue,mobx的响应式数据)
  1. 需求/调研

关于需求调研,我还是很急躁,急急忙忙开始技术方案评审、开发、排期的话,就会导致整个开发过程很被动。

比较好的方式,前期对于自己做的需求,以及需求的各个功能依赖有比较充分的了解(不过这在很多功能依赖方都没有文档的情况下很难做到),然后写一个符合需求的简版的demo,对可能出现的阻塞点和解决方案心里有一个预期,把图纸画好,照着图纸开发,从而更好的掌控整个开发进度。

  1. 功能/需求拆解

对功能需求的有效拆解,功能模块详细具体,粒度合适,不会太过细化,也不会忽略一些关键点,才能够更好的把控整个开发进度,评估可能出现的风险点。

  1. 工具方法收敛思路

收敛-内部集中分发给各个具体的util处理,保证对外暴露的接口的统一,降低该方法使用的心智负担。

  1. 关于「自顶向下」和「自底向上」

自顶向下:

程序设计时,先考虑整体,后考虑细节。先考虑全局目标,后考虑局部目标。不要一开始就过多追求总多的细节,先从最上层总目标开始设计,逐步使问题具体化。

模块化设计:一个复杂的问题,肯定是有若干稍简单的问题构成。模块化是把程序要解决的总目标拆解为子目标,再进一步细化分解为具体的小目标,把每一个小目标称为一个模块。

自底向上:

自底向上的设计简单来说就是先完成细节功能,每个细节功能抽象成一个运算符,然后将这些完成的细节功能组装到整体的架构中。

自动化的设计是不是就应该采用自底向上的设计思路,把每个需要的细节功能做抽象,使得配置规则的人可以任意组装,对于不支持的功能制造新的抽象?

  1. eval 和 new Function的区别

eval 和 new Function都可以解析执行一段传入的字符串。但有以下不同的地方:

  • eval中的代码是当前作用域,它可以访问当前函数中的局部变量和全局变量。new Function中的代码执行的作用域是全局作用域,不论它在哪个地方被调用,可访问的都是全局变量;
  • eval接收函数作为字符串时需要“(”和“)”作为前缀和后缀,new Function不需要,new Function可以接收N个参数,最后一个参数作为函数体;
  • eval不容易调试,用chromeDev等调试工具无法打断点调试;
  • 性能问题,eval通常比其他替代方法更慢,因为他必须调用js解释器,而其他结构则可被现代js引擎优化;
  • eval存在安全问题,因为可访问局部作用域的变量,其内部逻辑不可预测性很强,可能导致XSS攻击;
  1. 乐观更新与保守更新
  • 乐观更新(Optimistic Update): 乐观更新:如果有编辑等改动,先更新前端页面,再像服务端发送请求,如果请求成功则结束操作,无需额外处理,若请求失败,则页面回滚到先前状态; 这样更新方式的优点是响应及时,缺点就是低概率的请求失败回滚的体验不太好。
  • 保守更新(Perssimistic Update): 保守更新:如果有编辑等改动,向服务端发送请求,等收到回复请求后再响应用户操作,在此之前用户都需要处于等待状态。 这样做的缺点是会使页面有比较大的延时感,优点是最终呈现的结果是可信赖、稳定可靠的。
  1. 正交的概念

编程上的正交,从数学上引进这个词,用于表示相互独立,相互间不可替代,并且可以组合起来实现其他功能。比如if和for语言是正交的,但for和while与句的功能是有重叠的。逻辑运算not、and也是正交的,其他复杂的逻辑运算都可以用这三种基本运算叠加起来。 编程语言经常定义一组正交语法特性,相互间不可替代,组合起来可以实现其他功能。为了更方便使用,在基础特性之上,再添加一些额外特性。这些非基本的额外特性,成为语法糖。语法糖对语言的功能没有太大影响,只是有了,代码写起来更方便些。

  1. 引入外部字体,因等待字体文件的加载而产生文字不可见问题的一些解决方案
  • 临时显示系统字体:添加font-display: swap;到自定义字体的style中,在自定义字体加载好之前显示系统字体;
  • 预加载网页字体:用<link rel="preload" as="font" >更早的获取字体文件。
  1. 总结写技术文章的几个步骤,引导自己学习并践行:
  • 学习优秀的人写的东西,看看他们的理解;
  • 带着自己的疑问,和目前接收到的理解去读源码;
  • 读完之后,按照自己最终的理解绘制相关逻辑的流程图;
  • 针对关键功能模块做拆解和源码解读;
  • 总结相关功能的实现机制,以及给我带来的启发和思考;
  1. 关于团队中被高频讨论的去底座;

去底座不是完全失去对底座(数据)的访问能力,而是设计一个标准化的API来支持按需访问底座(数据)的能力。


作者:前端古力士
链接:https://juejin.cn/post/7260411184179675193
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用小明的故事随便谈谈kotlin中的apply等函数

前言本文仅简单描述一下kotlin中常用到的scope function,如apply,let,run,with,also等函数的常用方法和选取。即使很多情况下选择不同函数,也同样都能达到最终效果,具体选择哪个函数我们不会严格约束,但如果你是对代码规范要求比较...
继续阅读 »

前言

本文仅简单描述一下kotlin中常用到的scope function,如apply,let,run,with,also等函数的常用方法和选取。即使很多情况下选择不同函数,也同样都能达到最终效果,具体选择哪个函数我们不会严格约束,但如果你是对代码规范要求比较高的,最好建立良好的代码习惯。

一般对比

函数一般使用场景函数定义上下文对象可用作返回值
apply在需要对对象进行初始化或配置的时候使用public inline fun <T> T.apply(block: T.() -> Unit): T接收器this返回值是对象本身
also在需要对对象执行额外操作并返回原对象的时候使用public inline fun <T> T.also(block: (T) -> Unit): T变量it返回值是对象本身
let在需要对对象进行非空判断并执行特定操作的时候使用public inline fun <T, R> T.let(block: (T) -> R): R变量it返回值是 lambda 结果
run在需要对对象进行多个操作,并返回一个结果的时候使用,通常是一个新的对象或其他public inline fun <T, R> T.run(block: () -> R): R 接收器this返回值是 lambda 结果
with在不拥有对象的上下文的时候使用public inline fun <T, R> with(receiver: T, block: T.() -> R): R接收器this返回值是 lambda 结果
  1. apply 函数接收一个 lambda 表达式作为参数,并返回被调用对象本身。通过 apply,可以在对象创建后立即对其进行链式操作,设置属性值、调用方法等。适合用于链式初始化或配置一些属性。
val person = Person().apply { 
name = "John"
age = 30
}
  1. also 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 it 引用指向调用 also 的对象。通过 also,可以对对象进行额外的操作,而原对象仍然是函数调用的结果。适合用于在对象操作过程中执行额外的副作用操作。
val modifiedObject = myObject.also {
// 额外操作 it
}
  1. let 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 it 引用指向调用 let 的对象。如果对象不为空,则执行 lambda 表达式内的操作,并返回 lambda 表达式的结果。适合用于安全地操作对象,避免空指针异常。
val result = nullableValue?.let {
// 操作非空对象 it
}
  1. run 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 this 引用指向调用 run 的对象。通过 run,可以便捷地对对象进行多次操作,并返回最后一个表达式的结果。适合用于执行一系列操作并返回最终结果。
val result = myObject.run {
// 对象操作1
// 对象操作2
// ...
// 返回结果
}

  1. with 函数接收一个对象和一个 lambda 表达式作为参数,lambda 表达式中的 this 引用指向传入的对象。通过 with,可以在没有对象接收者的情况下操作对象,并返回最后一个表达式的结果。适合用于对对象进行一系列操作,而无需在乎返回值。
val result = with(myObject) {
// 对象操作1
// 对象操作2
// ...
// 返回结果
}

小明的故事

故事是这样的

  1. 小明今年上一年级
  2. 但是家长跟学校说,小明是个天才,现在可以直接跳级到二年级
  3. 学校给二年级分配的老师是王老师,是个女教师
  4. 半学期后,王老师怀孕了需要休息,于是学校给王老师放假
  5. 学校给二年级分配了新的李老师,小明有了新老师

下面是故事的代码:


data class Student(var name: String = "", var grade: String = "", var teacher: Teacher? = null) {
//插班跳级
fun needSkippingGrade(insertGrade: String) {
this.grade = insertGrade
}
}

data class Teacher(var name: String = "") {
fun relax() {
println("$name 休假了!")
}
}

fun main() {

//1. **小明**今年上一年级
val xiaoming = Student()
.apply {
name = "小明"
grade = "一年级"
println("小明开始前: $this")
}
.also {
//2.现在可以直接跳级到二年级
it.needSkippingGrade("二年级")
println("小明插班后: $it")
}

//3. 学校给二年级分配的老师是**王老师**,是个女教师
val ownTeacher = xiaoming.teacher?.let {
println("小明当前的老师不为NULL,是${it}")
} ?: Teacher("王老师").also {
xiaoming.teacher = it
println("小明有了老师: $xiaoming")
}

fun changeStudentCurrentTeacher(student: Student): Teacher? {
return student.run {
teacher?.relax()
Teacher("李老师")
}
}

//4. 半学期后,王老师怀孕了需要休息,于是学校给王老师放假
//5. 学校给二年级分配了新的**李老师**,小明有了新老师
with(xiaoming) {
println("开学了!")
println("半学期后,王老师怀孕了!...")
val newTeacher = changeStudentCurrentTeacher(this)
println("新老师是$newTeacher")
teacher = newTeacher
println("小明有了新老师$this")
}

}

输出结果:

小明开始前: Student(name=小明, grade=一年级, teacher=null)
小明插班后: Student(name=小明, grade=二年级, teacher=null)
小明有了老师: Student(name=小明, grade=二年级, teacher=Teacher(name=王老师))
开学了!
半学期后,王老师怀孕了!...
王老师 休假了!
新老师是Teacher(name=李老师)
小明有了新老师Student(name=小明, grade=二年级, teacher=Teacher(name=李老师))

最后

实际过程中,需要根据具体的场景和需求来选择适合的函数。前面这些函数在 Kotlin 中提供了更简洁、可读性更高的方式来处理对象,根据不同的使用场景,你可以选择最适合和更易读的函数来操作对象


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

当面试官问你BroadcastReciver的静态注册与动态注册的区别,你又该作何应对?

什么是广播,简单点广播就是安卓系统本身发出的声音,我们可以通过安卓提供给我们的一系列内容来接收和发出广播,以此来简单快捷地实现一些功能。在实际开发中也常常用到,而是否熟悉使用,这成为面试官最常问的问题。当面试官问你:1.请问BroadcastReciver的静...
继续阅读 »

什么是广播,简单点广播就是安卓系统本身发出的声音,我们可以通过安卓提供给我们的一系列内容来接收和发出广播,以此来简单快捷地实现一些功能。

在实际开发中也常常用到,而是否熟悉使用,这成为面试官最常问的问题。

当面试官问你:

1.请问BroadcastReciver的静态注册与动态注册的区别?,你在开发中用过吗?

答:

其实广播分为两种基本类型:

在一个程序中,可以发送广播供当前程序的广播接收器收到。首先我们来看下两种方式的发送广播。 在Android系统中,主要有两种基本的广播类型: - 标准广播(Normal Broadcasts) - 有序广播(Ordered Broadcasts)

标准广播:

是一种完全异步执行的广播,在广播发出之后,所有的广播接收器会在同一时刻接收到这条广播,广播无法被中断。

发送广播的方式十分容易的,只需要实例化一个Intent对象,然后调用context的** sendBroadcast() **方法。这样就完成了广播的发送。

        //intent中的参数为action
       Intent intent=new Intent("com.example.dimple.BROADCAST_TEST");
       sendBroadcast(intent);

有序广播:

是一种同步执行的广播,在广播发出之后,优先级高的广播接收器会先接收到这条广播,并可以在优先级较低的广播接收器之前终止发送这条广播。

        //intent中的参数为action
       Intent intent=new Intent("com.example.dimple.BROADCAST_TEST");
       sendOrderBroadcast(intent,null);//第二个参数是与权限相关的字符串。

到此时,如果你的程序中只有一个广播接收器的话,是体现不出有序广播的特点的, 右击包名——New——Other——BroadcastReceiver多创建几个广播接收器。

此时你还是会发现,所有的广播接收器是同时接收到广播消息的。注意上面介绍的时候说到优先级,这个时候我们需要设置优先级,在AndroidManifest文件中的Receiver标签中设置广播接收器的优先级。

        <receiver
           android:name=".MyReceiver"
           android:enabled="true"
           android:exported="true">
           <!--注意此时有一个Priority属性-->
           <intent-filter android:priority="100">
               <action android:name="android.intent.action.BROADCAST_TEST"></action>
           </intent-filter>
       </receiver>

优先级越高的广播接收器优先收到广播,也可以在收到广播的时候调用abortBroadcast() 方法截断广播。优先级低的广播接收器就无法接收到广播了。

面试官,假设我有一个接收者如下:

在Android的广播接收机制中,如果接收到广播,就需要创建广播接收器。而创建广播接收器的方法就是新建一个类(可以是单独新建类,也可以是内部类(public)) 继承自BroadcastReceiver

   class myBroadcastReceiver extends BroadcastReceiver{

       @Override
       public void onReceive(Context context, Intent intent) {
           //接收到广播的处理,注意不能有耗时操作,当此方法长时间未结束,会报错。
           //同时,广播接收器中不能开线程。
      }
  }

面试官,接下来就是面临注册的问题了,有两种注册方式,一种是动态注册,一种是静态注册

所谓动态注册是指在代码中注册。步骤如下 :

  • 实例化自定义的广播接收器。
  • 创建IntentFilter实例。
  • 调用IntentFilter实例的addAction()方法添加监听的广播类型。
  • 最后调用Context的registerReceiver(BroadcastReceiver,IntentFilter)动态的注册广播。

这个时候,已经为我们自定义的广播接收器关联了广播,当收到和绑定的广播一直的广播的时候,就会调用广播接收器中的onReceiver方法。

        MyBroadcastReceiver myBroadcastReceiver=new MyBroadcastReceiver();
       IntentFilter intentFilter=new IntentFilter();
       intentFilter.addAction("com.example.dimple.MY_BROADCAST");
       registerReceiver(myBroadcastReceiver,intentFilter);

这里需要注意的是,如果需要接收系统的广播(比如电量变化,网络变化等等),别忘记在AndroidManifest配置文件中加上权限。另外,动态注册的广播在活动结束的时候需要取消注册:

    @Override
   protected void onDestroy() {
       super.onDestroy();
       unregisterReceiver(myBroadcastReceiver);
  }  

静态注册:

在创建好的广播接收器中添加一个Toast提示。代码如下:

public class MyReceiver extends BroadcastReceiver {
   @Override
   public void onReceive(Context context, Intent intent) {
       Toast.makeText(context,"开机启动!",Toast.LENGTH_LONG).show();
  }  
}

然后在AndroidManifest文件中添加:

  • 权限 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"></uses-permission>

  • Intent-filter

            <receiver
               android:name=".MyReceiver"
               android:enabled="true"
               android:exported="true">
               <!--添加以下3行-->
               <intent-filter>
                   <action android:name="android.intent.action.BOOT_COMPLETED"></action>
               </intent-filter>
           </receiver>

    此时重启Android系统就可以收到开机提示了。

总结:

动态注册静态注册的不同:

动态注册的广播接收器可以自由的实现注册和取消,有很大的灵活性。但是只有在程序启动之后才能收到广播,此外,不知道你注意到了没,广播接收器的注销是在onDestroy()方法中的。所以广播接收器的生命周期是和当前Activity的生命周期一样。

静态注册的广播不受程序是否启动的约束,当应用程序关闭之后,还是可以接收到广播。

标准广播和有序广播的接收和发送都是全局性的,这样会使得其他程序有几率接收到广播,会造成一定的安全问题。为了解决这个问题,Android系统中有一套本地广播的机制。这个机制是让所有的广播事件(接收与发送)都在程序内部完成。主要是采用的一个localBroadcastReceiver对广播进行管理。


作者:派大星不吃蟹
链接:https://juejin.cn/post/7255213112881266749
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

终于搞明白了什么是同步屏障

背景今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler 里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏...
继续阅读 »

背景

今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler 里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏障。

同步屏障机制

1. 直奔主题,同步屏障机制这几个字听起来很牛逼,能浅显的解释一下,先让大家明白它的作用是啥不?

同步屏障实际上就是字面意思,可以理解为建立一道屏障,隔离同步消息,优先处理消息队列中的异步消息进行处理,所以才叫同步屏障。

2. 第二个问题,同步消息又是啥呢?异步消息和同步消息有啥不一样呢?

要回答这个问题,我们就得了解一下 MessageMessage 的消息种类分为三种:

  • 普通消息(同步消息)
  • 异步消息
  • 同步屏障消息

我们平时使用 Handler 发送的消息基本都是普通消息,中规中矩的排到消息队列中,轮到它了再乖乖地出来执行。

考虑一个场景,我现在往 UI 线程发送了一个消息,想要绘制一个关键的 View,但是现在 UI 线程的消息队列里面消息已经爆满了,我的这条消息迟迟都没有办法得到处理,导致这个关键 View 绘制不出来,用户使用的时候很恼怒,一气之下给出差评这是什么垃圾 app,卡的要死。

此时,同步屏障就派上用场了。如果消息队列里面存在了同步屏障消息,那么它就会优先寻找我们想要先处理的消息,把它从队列里面取出来,可以理解为加急处理。那同步屏障机制怎么知道我们想优先处理的是哪条消息呢?如果一条消息如果是异步消息,那同步屏障机制就会优先对它处理。

3.那要如何设置异步消息呢?怎样的消息才算一条异步消息呢?

Message 已经提供了现成的标记位 isAsynchronous 用来标志这条消息是不是异步消息。

4.能看看源码了解下官方到底怎么实现的吗?

看看怎么往消息队列 MessageQueue 中插入同步屏障消息吧。

private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;

Message prev = null;
// 当前消息队列
Message p = mMessages;
if (when != 0) {
// 根据when找到同步屏障消息插入的位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// 插入同步屏障消息
if (prev != null) {
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
// 前面没有消息的话,同步屏障消息变成队首了
mMessages = msg;
}
return token;
}
}

在代码关键位置我都做了注释,简单来说呢,其实就像是遍历一个链表,根据 when 来找到同步屏障消息应该插入的位置。

5.同步屏障消息好像只设置了when,没有target呢?

这个问题发现了华点,熟悉 Handler 的朋友都知道,插入消息到消息队列的时候,系统会判断当前的消息有没有 targettarget 的作用就是标记了这个消息最终要由哪个 Handler 进行处理,没有 target 会抛异常。

boolean enqueueMessage(Message msg, long when) {
// target不能为空
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
...
}

问题 4 的源码分析中,同步屏障消息没有设置过 target,所以它肯定不是通过 enqueueMessage() 添加到消息队列里面的啦。很明显就是通过 postSyncBarrier() 方法,把一个没有 target 的消息插入到消息队列里面的。

6.上面我都明白了,下面该说说同步屏障到底是怎么优先处理异步消息的吧?

OK,插入了同步屏障消息之后,消息队列也还是正常出队的,显然在队列获取下一个消息的时候,可能对同步屏障消息有什么特殊的判断逻辑。看看 MessageQueue 的 next 方法:

Message next() {
...
// msg.target == null,很明显是一个同步屏障消息
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
...
}

方法代码很长,看源码最主要还是看关键逻辑,也没必要一行一行的啃源码。这个方法中相信你一眼就发现了msg.target == null,前面刚说过同步屏障消息的 target 就是空的,很显然这里就是对同步屏障消息的特殊处理逻辑。用了一个 do...while 循环,消息如果不是异步的,就遍历下一个消息,直到找到异步消息,也就是 msg.isAsynchronous() == true

7.原来如此,那如果消息队列中没有异步消息咋办?

如果队列中没有异步消息,就会休眠等待被唤醒。所以 postSyncBarrier() 和 removeSyncBarrier() 必须成对出现,否则会导致消息队列中的同步消息不会被执行,出现假死情况。

8.系统的 postSyncBarrier() 貌似也没提供给外部访问啊?这我们要怎么使用?

确实我们没办法直接访问 postSyncBarrier() 方法创建同步屏障消息。你可能会想到不让访问我就反射调用呗,也不是不可以。

但我们也可以另辟蹊径,虽然没办法创建同步屏障消息,但是我们可以创建异步消息啊!只要系统创建了同步屏障消息,不就能找到我们自己创建的异步消息啦。

系统提供了两个方法创建异步 Handler

public static Handler createAsync(@NonNull Looper looper) {
if (looper == null) throw new NullPointerException("looper must not be null");
// 这个true就是代表是异步的
return new Handler(looper, null, true);
}

public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
if (looper == null) throw new NullPointerException("looper must not be null");
if (callback == null) throw new NullPointerException("callback must not be null");
return new Handler(looper, callback, true);
}

异步 Handler 发送的就是异步消息。

9.那系统什么时候会去添加同步屏障呢?

有对 View 的工作流程比较了解的朋友想必已经知道了,在 ViewRootImpl 的 requestLayout 方法中,系统就会添加一个同步屏障。

不了解也没关系,这里我简单说一下。

(1)创建 DecorView

当我们启动了 Activity 后,系统最终会执行到 ActivityThread 的 handleLaunchActivity 方法中:

final Activity a = performLaunchActivity(r, customIntent);

这里我们只截取了重要的一行代码,在 performLaunchActivity 中执行的就是 Activity 的创建逻辑,因此也会进行 DecorView 的创建,此时的 DecorView 只是进行了初始化,添加了布局文件,对用户来说,依然是不可见的。

(2)加载 DecorView 到 Window

onCreate 结束后,我们来看下 onResume 对应的 handleResumeActivity 方法:

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
// 1.performResumeActivity 回调用 Activity 的 onResume
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
...
final Activity a = r.activity;
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
// 2.获取 decorview
View decor = r.window.getDecorView();
// 3.decor 现在还不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 4.decor 添加到 WindowManger中
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
...
}

注释 4 处,DecorView 会通过 WindowManager 执行了 addView() 方法后加载到 Window 中,而该方法实际上是会最终调用到 WindowManagerGlobal 的 addView() 中。

(3)创建 ViewRootImpl 对象,调用 setView() 方法

// WindowManagerGlobal.ddView()
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

WindowManagerGlobal 的 addView() 会先创建一个 ViewRootImpl 实例,然后将 DecorView 作为参数传给 ViewRootImpl,通过 setView() 方法进行 View 的处理。setView() 的内部主要就是通过 requestLayout 方法来请求开始测量、布局和绘制流程

(4)requestLayout() 和 scheduleTraversals()

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 主要方法
scheduleTraversals();
}
}

void scheduleTraversals() {
if (!mTraversalScheduled) {
// 1.将mTraversalScheduled标记为true,表示View的测量、布局和绘制过程已经被请求。
mTraversalScheduled = true;
// 2.往主线程发送一个同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 3.注册回调,当监听到VSYNC信号到达时,执行该异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

看到了吧,注释 2 的代码熟悉的很,系统调用了 postSyncBarrier() 来创建同步屏障了。那注释 3 是啥意思呢?mChoreographer 是一个 Choreographer 对象。

要理解 Choreographer 的话,还要明白 VSYNC

我们的手机屏幕刷新频率是 1s 内屏幕刷新的次数,比如 60Hz、120Hz 等。60Hz表示屏幕在一秒内刷新 60 次,也就是每隔 16.6ms 刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算,每收到 VSYNC,CPU 就开始处理各帧数据。这时 Choreographer 就上场啦,当有 VSYNC 信号到来时,会唤醒 Choreographer,触发指定的工作。它提供了一个回调功能,让业务知道 VSYNC 信号来了,可以进行下一帧的绘制了,也就是注释 3 使用的 postCallback 方法。

当监听到 VSYNC 信号后,会回调来执行 mTraversalRunnable 这个 Runnable 对象。

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// View的绘制入口方法
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

在这个 Runnable 里面,会移除同步屏障。然后调用 performTraversals 这个View 的工作流程的入口方法完成对 View 的绘制。

这回明白了吧,系统会在调用 requestLayout() 的时候创建同步屏障,等到下一个 VSYNC 信号到来时才会执行相应的绘制任务并移除同步屏障。所以在等待 VSYNC 信号到来的期间,就可以执行我们自己的异步消息了。


作者:搬砖的代码民工
链接:https://juejin.cn/post/7258850748150104120
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

该怎么放弃你,我的内卷

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也...
继续阅读 »

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也都清楚,最后给我打了个 B-。呵呵,扣工资 20%,变成所谓的绩效工资,下次考核看情况发放。

很多兄弟看到这可能会替我打抱不平,狗资本家,快去发起劳动仲裁。可是他们马上又下上另一剂猛药,那就是不停的 PUA 你,告诉你现在有家庭,要多努力,要一心扑在工作上,放心,下次一定给你打回来。

我承认,他们这些话术我都看过,基本相当于明牌。可依然我还是被影响到了,情绪十分低落,也没心思去劳动局跟他们 PK。对未来的预期瞬间变得很悲观,人要是一悲观了,真的干什么也提不起兴趣。我一门心思都扑在以后能干什么上,其它的啥也不想管,疯狂的在国内外门户上刷信息,希望能找到一条“赚钱之路”。我研究了 Web3、AI 绘画、搞自媒体、网赚攻略(什么视频搬运、抄书、小说转漫画等等),基本上信息流推给我的,我都研究了一遍。这些玩意越研究越让人焦虑,因为那些标题都起的特别的有煽动性,动不动就日入几万,而我发的那些,浏览量都破不了百。于是我就想研究更多的路子去赚钱,老实说,东南亚那边的情况,我也了解过一些。

后面我对家人的态度也越来越坏,经常不耐烦,看着我小孩我经常叹气,我想这他妈可能就是中年危机提前爆发了,总之那段时间人会越来越焦虑。

后来还是我一兄弟,邀请我一家人去平潭自驾游,我们其实也没玩几天,属于特种兵式旅游,两天两晚(晚上熬夜开车去)。回来之后心情就好多了,也没那么焦虑了。其实本来也没什么,君子不立于危墙之下,这里不行那就走。找不到就先干自己的项目(我有开源项目)。我其实对干这行还是蛮有兴趣的,应该持续坚持的干下去,半途而废干别的是下策。

回想下我那时候焦虑的经历,我以前根本看都不看那种赚钱文章的,因为我知道这些大部分是在卖课,可为什么那时候我着了魔一样呢?其实很大部分与网络有关系,你着急干什么,你就愿意看点什么,你看点什么,网络就给你推什么。这种消极循环人一旦深陷其中,光凭自己是很难走出来的。其实这种时候应该主动去接收一些积极乐观的情绪,有助于自己调整心态,网络给不了你,只有身边人能给你。

更深一步的想,所谓内卷是不是也是通过网络在传播着,深刻的影响到每一个人。所谓的“智能推荐算法”,真的智能吗?大家想看的一定就是适合每个人的吗?你不停的点击去看的信息,真的能帮助到你吗?网络是我们的工具,还是我们是网络的工具?

我想我们真的应该停下来,想想我们到底在多大程度上需要抖音、需要 BiliBili、需要知乎,也许它们真的没这么重要。

人生在世,我们到底应该追逐什么?或者说,追逐什么其实不重要,重要的是我们去追逐的过程。在这个过程中,没有内卷,没有与别人的竞争,只有对自我的审视和成长。

换句话说,我有多久没有好好了解自己了,那些独属于自己的东西,永远不会背叛的资源。我们常说的:能力、人脉、技术、视野。其实除此之外还有很多很多,我刷视频看到的有趣视频点的赞,我 Chrome 里收藏的网页,我百度网盘里躺着的分享资料等等等等,还有最重要的一项,就是我的身体和组成我身体的每一个部分:大脑、心脏、肺...... 有多久没有关注和了解它们了?在这个内卷的时代,每个人都在比拼都在竞争,都怕落于人后,都想快点挣更多的钱,这些,时常让我们忽视了对我们最重要的东西。

有趣的是,每个平台都在疯狂的更新自己的算法,期望能更精准的描述一个人,给人打上各种各样的标签。但在这场竞赛中,没有平台能竞争的过你自己,在这个世界上,只有自己更了解自己。所以我真的感觉它们在做无用功,浪费资源,最好的平台,不是给打各种标签,而是引导每个人发现自己的标签是什么。

这里我想分享给各位几个我思考的点,以供探讨。

原则一:相比与到处去找信息差,更重要的是建立自己的“资源池”

我那时候不停的刷信息,不停的找信息,本质上,我是在幻想着找到一个信息差,从而获利。这也是网上铺天盖地的文章所推崇的,所谓在风口上猪都能飞。但它们总是在掩盖一个逻辑错误,那就是找到信息差和获利之间的因果关系。实际上,找到信息差只是获利的条件之一,你有多大的能力利用这个信息差,这个信息差的时效性,方方面面的因素都会互相交织和影响。

更进一步的想,信息差就像风一样,它存在于冷热空气的交换之时,它存在于各行各业、每时每刻。让我们去追逐风,这现实吗?

我们更应该静下来,好好数数自己手头的东西,整理自己的大脑。找到自己“资源池”有哪些资源,哪些可以为我们所用,哪些可以继续扩充。思路可以打开一点,任何在当前时刻属于你的东西,都是你“资源池”的一部分。

原则二:出卖自己时间和体力的不做

这个不做,不是指不去做,而是指不长期的做。一般入门一个行业或者技术,肯定要付出时间和体力的。但你要说十年如一日的付出相同的东西,那所谓“35 岁”危机就只能找到你了。这点其实各行各业都一样,只是互联网行业处在发声的前沿罢了。

包括所谓网赚、搬运都是一个道理,毫无技术含量的事做几年就好。要时常审视自己现在在干什么,手头有哪些资源,未来的目标是什么。这跟程序运行是一个道理,运行了一段时间,停下来让自己 GC 一下。不然很容易 StackOverflow。

原则三:自己抓住的资源,千万不要轻易放手

如果不经常审视自己的“资源池”,给所有资源估估价值,就很容易被人带坑里。

原先我就做过一个项目,这是个跨部门项目,我那个领导一直告诉我说这个项目没前途、没卵用,绩效也给我打的不好,问我还要不要继续做。我说那就算了吧,做的我都不想做了。

我一放弃,马上就有新人接手,连交接也不用做,代码直接拿走,吃相可见一斑。

也就是从这里我才理解到,我其实没有了解自己,没了解过我手里的项目,被人潜移默化的影响了。影响一个人的思想真的不难,不停的重复就好了。所以还是那句话,多把自己手里的“资源池”拿出来晒一晒,整理一下。

其实 996 也是一样,拿出了你最重要的资源---身体,到底换来了什么,值得好好评估一下。

原则四:做自己喜欢的赛道,更要积累自己的资源

这几个月的经历给我的最大感觉是,这世界上真的有太多太多的行业,也有很多人赚到了钱(至少网络上宣传他们赚了钱)。网络能让这些信息病毒式的传播,导致很多人错觉的以为自己照着做也能挣到钱。但他们忽视的是,网络能把世界各地的人汇聚起来,让信息流通。其实也提供了一个更大的平台,在这个平台里,只有更卷的人才能挣到钱。

有时候真的应该抛开网络。比如,你会写代码,这是你“资源池”里的一项技能,你把这个技能公开到网络售卖。只有两种情况,要么你非常的卷,打拼出一番事业;要么你根本竞争不过别人,这是普遍情况,这世界那么大,比你优秀的人有太多太多了。

但是抛开网络,回到你身边的小小社交圈子,你的技能可能就没那么普遍了。可能你会说,那我做程序员,我身边朋友认识的大部分也是做程序员啊。那么可以这么想,假如你会做菜,你身边的程序员朋友都会做菜吗?假如你会画画,你身边的程序员朋友都会画画吗?人和人总有差异点,你觉得找不到优势,那是因为你尚未建立自己的“资源库”。

先认识自己,再让身边的人认识自己,当他们会给你打标签时,他们就成了你“资源库”中的一员,这就是人脉。这才是是独属于你自己的标签,而不是抖音、B 站为你打的冷冰冷的标签。

总结

以上我感悟的四个原则,我称之为“资源池思维”,一个比较程序员化的名词。

这篇文章发完后,我后续可能就继续更新一下具体的技术文章了,继续深耕技术。

最后,推荐看到最后的各位看一部冷门电影:《神迹》,讲述的是医生维维安托马斯的故事。看完可以来一起交流交流感悟。


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

摸鱼时间打造一款产品

辞职我辞职了拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较闲,大把...
继续阅读 »

辞职

我辞职了

拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方

辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开

公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较闲,大把摸鱼时间,逛逛各种论坛,掘金、知乎、github不亦乐乎,现在看来公司倒闭和我不无关系

久而久之,不免有些无聊。论坛里充斥着灌水文章,看多了属实是食之无味。于是为了打发时间,只能写一写自己的项目。一想到老板在为我打工,敲打键盘的双手便愈发轻盈了

未曾设想的道路

大半年前为了记录学习一项技能到底要花多少时间,我开了个新坑,做一款计时软件,记录某个时间段发生的事情

和以往一样,最初只是打算随便写写,写个基础功能完事。但在使用的过程中,越来越多的需求在脑海中构建。编程最有趣的便是创造感,你能感受到自己在创建一个新的世界,在创造的过程中时间飞速流逝,一转眼便度过了无聊的一天

激情是短暂的,生活是漫长的。和往常一样,功能在逐步完成,但我的兴趣在逐渐减少。没有添加柴薪,火便只能渐渐势弱

此时两个选择摆在面前,一个便是不再更新下去,毕竟要做的已经完成了,再去寻找下一个打发时间的事情就好了。另一个则是保持兴趣继续做下去

兴趣?利益?

做一个开源软件,如果能收获社区的掌声想必是件自豪的事情。但如果只有掌声,久而久之开源作者可能会陷入自己到底为了什么才做这件事的思维泥潭。有的人失去了兴趣便离开了,有的人发出了声音希望得到一些回馈

兴趣可以支撑人前行,但又有多少人能不求回报去做一件事?不可否认,曾经幻想过做出爆红的软件,然后不用打工,财富自由这样的白日梦。虽然不能一步登天,但我想借助它向前一步

审视一下目前的状况,如果要供用户使用,一个简单的计时功能加上记录,未免太过单薄。这么简单的功能实在谈不上什么竞争力,实现成本过低,而且我相信人们更愿意使用移动app,而不是在pc上去使用这个功能。我需要一个特定于pc且有实在价值的功能,很快我便找到了,它既满足前面的要求,又契合软件的主题

广告恐怕是最理想的获利方式,不会影响用户使用,也不用去考虑升级版之类的的东西。虽然不知道具体能有多少收入,但希望起码能够抵消掉域名的费用

有了继续前进的目标,这艘小船便能扬帆远航

但眼下的问题很严重,我在技术选型上摔了个大跟头

重头再来

好的开始是成功的一半,但没有人能预料到未来会发生什么

使用vue3为前端,我直接选择了webview方向的跨端框架

在以go为后端的wails和rust为后端tauri中,我选择了go。之前学习过一段时间的rust,深知学习的难度。而且在最初的预想中,我只是打算做个简单的计时软件,使用go也只是做一下数据库操作。不久后就完成了最初的一版,但在后续的尝试中,发现wails的生态还是太小了,很多基础的功能都需要自己实现。这时再看看tauri就显得很香了,各种插件和前端的绑定,再加上go并没有用得多么称手,于是只能长痛不如短痛了

ui框架的选择上我也犯了同样的问题。开始是偏向于material design这种风格,选择了vuetify,这个框架当时我看了很久,做的时候已经要到v3正式版本了。本来以为没问题,但后续使用时过于难受,此时文档基本没怎么更新,issue也被各种bug塞满了。只能快刀斩乱麻,换了习惯的ant-design-vue,风格区别很大,但改改样式也能用。quasar同样在我的考虑范围内,但更加小众,目前是不打算换了,在tauri v2移动端正式版后,再做尝试

为什么最开始没有选择做移动端?功能契合,使用起来也更方便。一方面是我的主要技能栈是js,另一方面重新学移动端过于不切实际,为一个八字没一撇的项目去学实在没有必要。flutter我之前也学过,试着写了一点,但还是不如js来得舒服

回过头来,发现走了很多弯路,但不去尝试只站在远处观望,永远也不会有结果。颠颠撞撞重头再来

编程之外

我一直把时间花在了代码之上,但想要做一款产品还远远不够,它迫使我不得不将视角转向那些我不曾关注的角落

UI可谓是产品的脸面,用户的第一印象便停留在了logo和界面上,虽然使用了风格统一的组件库,但将他们组合在一起的时候未必能将它们严丝合缝。目前只能说是勉强能看,日后再做修改

说明文档带领用户快速理解程序的运作,由于用户没有设计者的前提条件,很多理所当然也就需要一一记录

想要完善功能,bug和feature的反馈也要做指引,方便接收用户意见,确定前进路线

说明文档

参考vite的官网,使用vitepress,写markdown就可以了,还可以配上vue组件,还算方便

部署上选择了netlify,可以换自己的域名,还可以自动更新ssl证书

本来以为部署很麻烦的,结果一个小时左右就全部搞定,包括在namesilo上买域名,然后在netlify部署、配置

拥抱AI

在完成这些工作的过程中,有不少地方借助了AI,可以说很大程度加快了进程

编码上,由于我完全不懂windows编程和勉强会点rust语法,想要完成监听系统上的应用状态这项功能,根本就无从谈起。要花大量时间去学习的话,反而和我利用碎片时间进行编程相冲突了。况且在new bing的帮助下,我完成一个简单的函数就要花费数个小时的尝试。new bing根据我的需求返回了相关的api参考,但很多时候返回的代码并不能直接运行,有着这样那样的问题,需要去修正。很难想象仅凭我一人去翻找资料何时才能完成这冰山一角

在这个过程中,new bing最大的帮助就是提供了关键词。很多时候,你知道一个事物,想用自己的语言需要一长串词语去描述,但过去的搜索引擎并不能理解这些,而且就算把描述输入进去,也会因为过多的关键字导致答案被淹没在茫茫的网页之中。这就造成了一个困境,我不知道它叫什么,所以我要去搜索,但搜索的时候要知道它叫什么

在netlify配置域名,我输入了如何去配置,new bing给出了关键的name servers,省去了花时间去到处去找教程

为应用绘制一个logo,很显然我并没有这个能力,使用Bing Image Creator,一段描述就能生成

这些都是一些无关紧要,琐碎的事情,我只想获取结果,把精力留在我擅长的事情上。试想一下,我一个人去实现要花掉多少时间?最终能实现吗?部分功能交给其他人,又要用什么去换取?

计划

讲到这里如果有兴趣了解一下的话,可以移步仓库地址,但目前的功能我只能说很少,而且还可能出现问题,我提前声明一下。说明文档见此处,需要看清警告提示

为什么这个时候来写这篇文章来介绍呢,主要是辞职了也没事干,已经做了大半年就整理了一下。原本是半年后再辞职的,但计划赶不上变化,只能提前放出来看看情况

还有一项没能赶上的便是广告,使用的是google adsense,但提交申请后便石沉大海。尽管提前做了申请,但已经过去了几个星期。可能是开始建站时随意申请被驳回的缘故,久久没有反应

最后

辞职其实还有一个原因,就是累了,光是待在公司什么都不干,也能感觉到劳累。工作小憩之余,在过道眺望远方时,我一直想问自己究竟在干些什么。我想做出改变,想去尝试新的东西,体验另外一种生活

想过很多次,以后也许不会从事编程的工作,但又有什么选择呢。我希望是创作,而不是枯燥的重复劳作,但我很清楚这不是换一个职业就能改变的问题,终究是实力的问题。编程很有趣,但在公司并不是如此

现在我已经度过了一周的悠闲时光了,白天在家看看书,傍晚下楼走走,看着面向我驶过的匆忙下班的人流,感叹这也是自己前不久的模样。我背朝着喧闹,走上凉爽的林荫道,晚风吹过,天边挂着一轮淡淡的月牙


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

工作三年后的胡思乱想

一眨眼工作已经三年了,前两年的总结 工作第一年、工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的...
继续阅读 »

一眨眼工作已经三年了,前两年的总结 工作第一年工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。

经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的各种链接都成为了可能,在互联网诞生之前,人与人之间的交流就是现实生活中的圈子,而现在本来这一辈子都不会在现实中产生交集的人在互联网却会相遇。

各种写书的大佬、开源的大佬,以往可能只是从文字、代码中了解他们,但现在通过社交媒体、微信竟然就产生了互动。当然不好一面就是也会遇到和自己不相投的人,也许会影响自己的心情。

通过互联网极大的扩宽了我们的视野,看到了别人在怎么生活,也放大了自己的焦虑和欲望。我们需要认清自己的边界,知道自己想要什么,自己能做什么,不需要对本来不可能发生在自己身上的事情而焦虑。

当迷茫焦虑时,看看宇宙的纪录片,从宇宙的视角去看自己,无论从空间大小还是时间维度,其实自己什么都不是,想那么多干啥。

再想想其他动物,吃饭睡觉喵喵叫,也挺好的。

前端已死

互联网已经结束了快速扩张的时期,这是个客观事实,因此招聘的人数相对于之前减少了很多,但远没到一个已死的状态,相对于其他行业,选择互联网依旧是一个不错的选择。

前端会不会死不知道,互联网肯定会一直存在下去,现在整个社会都是基于互联网,已经变成了像电、水一样的基础设施,没有人可以离开它。因此互联网的相关的岗位一定会一直一直存在。

至于互联网中具体的职业划分,前端、后端、算法、数据库等,它们各自使用的语言、技术一定会发生变化的,当选择互联网技术行业的时候,就应该抱有持续学习的态度。

塞班操作系统被安卓、iOS 取代、.Net 岗位的减少、客户端大量岗位转前端,这些也就发生在近十几二十年。当某一个岗位减少的时候,一定又会出现新的岗位,保持开放的心态去学就可以,变化再多肯定也有不变的东西。当掌握一门技术再学习另一门技术的时候,肯定会比小白学习一门新技术快很多很多,很多经验也会迁移过去。

去年 12 月出来的 chatGPT 为代表的大模型,到现在也就半年多的时间,很多以前完全不敢想的事情就这样发生了。可以预见的是一部分岗位数量肯定也会减少,目前影响最大的应该是 UI 岗,其次一定程度上可以提高程序员的开发以及学习效率,但还没有到取代的程度,但未来会再怎么发展就不得而知了。

相对于其他行业,虽然互联网相关技术迭代确实很快,但如果是因为热爱而选择这个行业,我觉得去做一辈子是没问题的。

技术

底层技术服务于上层技术,上层技术服务于应用,真正赚钱的是应用,它可能提升了用户的效率、也可能提升了用户的生活体验,这样用户才愿意付费。上层技术的人收到了钱,进一步也愿意为底层技术的人付费。

但对于一个应用,技术并不是最重要的,更多需要的是产品和运营,一个应用在 chatGPT 和各种框架、云服务的加持下做出来变得太简单了,更多的是我们需要思考如何设计产品和如何推广运营产品,和用户产生更亲密的连接,用户才愿意付费。

极端一点,即使现在所有的应用都停止更新了,其实也并不会产生多大的影响。

在公司中亦是如此,对于技术开发,没有谁是不可取代的,公司更期望的是那些可以发现问题、分析问题、定义问题的人,至于怎么解决,问题定义清楚以后,解决方案自然可以出来,谁去解决并不重要了。

但也不用太过悲观,虽然技术不是最重要的,但一定是不可或缺的,在解决问题的过程中也会区分出能力强和能力差的:方案的设定、代码编写的好坏、线上的 bug 数、代码的扩展性等。

赚钱

赚钱很大程度又是需要运气的,比如同一个人十年前进入互联网和现在进入互联网差别就会很大,再比如开发一个应用突然爆火,例如「羊了个羊」,这些我们是很难控制的,我们只能「尽人事,听天命」。

最近几年,除了在公司工作,对于有技术的同学赚钱有下边的方式:

  • 付费课程、出书

    最近几年越来越多的人在极客时间、掘金小册写课程或者直接出书。

    对于写课的人赚到了钱,对于买课的人只要跟着看完了,多多少少都会有很多收获。付费课程会比较系统, 如果没有这些课程,去学东西肯定也是可以学的,但需要花很多时间去网上搜一些零碎的资料,由于没有经验甚至可能走很多弯路。

  • 付费社群

    市面上也会有一些付费训练的社群或者知识星球

    对于组织付费社群的人会花费很大的精力,需要持续运营并且照顾到每一个人,不然就等着挨骂吧。因此这类收益也会很高,一些人会辞去工作专职来搞。

  • 开源

    大部分开源基本上是用爱发电,更多是收获一些朋友、流量、提升技术。

    比如 core-js 作者的经历,一个 22.6k star 的项目,几乎各个网站都在用的一个项目,作者却因为钱的问题被很多人谩骂。因此如果是个人专职开源一个项目靠 GitHub Sponsor 会很难很难。

    当然,开源也是能赚到钱的,比如 Vue 开源就赚到了很多钱,但毕竟是很少很少数了。

    依赖纯开源项目赚到钱,还是需要背靠公司。比如阿里云谦的 Umi、通过开源加入 NuxtLab 的 Anthony Fu、在 AFFiNE 的雪碧等等。

  • 应用

    身为一个程序员,尤其是前端程序员,当然可以自己维护一个应用来赚钱。

    做得很成功的比如 Livid 的 V2ex 社区,Abner Lee 的 Typora(后来知道作者竟然是国内开发者)。

    也有一些没有那么出名的,比如大鹏的 mdnice,秋风的 木及简历

    当然如果要做一个很大的项目,背靠公司也是一个很好的选择,比如之前阿里玉伯的语雀、之前极客邦池建强的极客时间。

    还有一些小的创业公司会做的,冯大辉的「抽奖助手」、吴鲁加的「知识星球」等。

    做出这些应用不需要很多时间,需要我们善于发现生活中的痛点以及强大的执行力,当然想成功的话需要再加一点运气,在成功前需要不断尝试不同的东西。

  • 流量变现

    有流量就会赚钱,不管是接广告、还是带货。互联网上也会有部分人专注于怎么搞流量,知乎怎么获得更多曝光、视频号怎么获得更多流量、怎么批量注册号,各个平台规则可能是什么,怎么对抗规则,这类有技术加持也会更加顺利,很多人也在专职做。

赚钱的方式有很多,对于我来说,我会尽量选择复利的事情,这样才能产生更大的价值。比如一对一咨询,一份时间换一份收入。但如果把东西写成课程,只需要花一份的时间就能获得 N 份的收入。

另外就是需要保持分享,分享除了能帮助其他人,对自己也会有很大的帮助,写文章的过程中也会不断的有新的认知得到。虽然当下可能没有金钱方面的收入,但时间放宽到几十年,相信一定会有很大的回报。

人的欲望是无穷的,也不能陷入赚钱的极端,目标应该是关注此刻,体验生活,享受生活,而不是不停的赚钱。之前听播客,有一个恰当的比喻,钱就好比汽油,不停的赚钱相当于不停的加油,但如果汽车停着一直不动,再多的汽油也是无意义的。

健康

最近几年总是爆出程序员突然离世的新闻,前段时间耗子叔突然离世的消息听到之后真的很震惊。twitter 经常刷到耗子叔的动态,然后突然一天竟然就戛然而止了,毫无征兆。

意外是无法避免的,只能尽可能的从饮食、作息、锻炼三方面降低生病的风险。

饮食

我是工作第一年体检的时候检查出了中度脂肪肝、尿酸高,当时因为是刚毕业,体重是我的巅峰,140 多斤,脂肪都堆在了肚子上。那段时间就开始跑步加吃沙拉,少吃米饭、面条。降的也快,几个月就回到了 130 斤以下,甚至到 120 多点。

第二年体检的时候,脂肪肝基本没有了,尿酸也降了许多。

image-20230702141922024

后来就保持少吃米饭,多吃蛋白质、蔬菜的饮食了。

作息

有一次得了带状疱疹,那种非常痛的类似于痘痘的东西,后来了解了一下是因为免疫力低导致病毒入侵的。猜测因为晚上坐在电脑前,气温降低了没注意,从而导致了生病。

病好之后就决心养成早睡早起的习惯。

之前作息基本上是 1 点到 2 点睡觉,9 点前后起床。现在基本上保持在 11 点前后睡觉,6 点到 7 点间起床了。

早起的好处就是早上会有大把的时间,而且这段时间是专属于自己的,并且因为大脑刚苏醒,效率也会很高。但如果是工作一天,晚上回家再做自己的事情,此时大脑已经很疲惫了,效率会比较低。

运动

最开始是跑步,但确实很难坚持下去,跑步需要换衣服、出门,还依赖于外边的天气,成本很高。后来陆续尝试过 keep、一些付费课程,都做了但没有完全养成习惯。

后来知道了 switch 的健身环大冒险,然后就一路坚持到了现在,前段时间已经通关了。

目前也一直在坚持,基本上一周会运动三到四次,一次大概花费 50 分钟左右。

投资

大学的时候开始接触到理财,知道了基金的概念,看了银行螺丝钉的「指数基金定投指南」,也看了「穷爸爸富爸爸」、「小狗钱钱」这类理财入门的书。当时赚到的一些钱,就跟着银行螺丝钉投了,主要是一些宽基和中概、医疗。

一直到工作的第一年,基金收入确实不错,甚至赚了百分之四五十。当时想着原来股市这么简单,这咋还能亏钱了。

接着疫情不断发展,还有外部经济的变化,中概、医疗都大跌,当时发了年终奖还不停的补仓中概,到现在亏损也有百分之三四十了。

但我心态是可以的,一切都是浮亏和浮盈,只要不卖一切都是浮云。

经历了大起大落后吸取了一些教训,那就是一定要严格执行计划,现金流多不一定要立刻全部投入,而是按计划定投,因为没人知道会跌多久,只有有充足的现金流,才能够把亏损逐步拉平。

现在国家规定互联网基金这些必须走「投顾」,也就是主理人帮我们买入、卖出,我们只需要交一定的投顾费即可。目前我都是在雪球上投,跟投的有孟岩的「长钱账户」、alex 的「全球精选」、螺丝钉的指数增强和主动优选。

能设置自动跟投的就自动跟投了,我相信专业的事交给专业的人肯定是没问题的。

投资肯定是财富自由不了的,但一定比把钱放余额宝强一些,只要耐心持有,尤其是目前这样的熊市投入,相信到下一个牛市会有不错的回报。

(以上仅个人看法,股市有风险,入市需谨慎)

保险

如果开始接触理财,除了投资,一个绕不过去的点就是保险。

对于保险是什么的比喻,之前听薛兆丰的课时候印象深刻。

我现在还年轻力壮,将来年纪大了可能会生病,为了防止以后生病要花一大笔医药费,今天就开始存钱,每个月拿出 10% 的收入存起来,未雨绸缪。这是一种做法。

另外一种做法,是我每个月也拿出 10% 的收入去买保险。

这两种做法有什么区别呢?

区别在于,如果我是用储蓄来未雨绸缪,那么未来可能就会发生两种不同的情形。

如果我将来年纪大了也没生病,我存的钱就还是我的钱,我不需要花出去,这时候我还是很幸运的,能够保有我原来的收入,这份储蓄没有被花掉,我赚了。

但是如果我运气不好,生病了,这份储蓄就会被用掉,甚至需要借很多钱去治病,生活会发生巨大的变化。

所以通过储蓄来未雨绸缪,它的特点是未来的结局是可变的,是变动的、是带有风险的。要么高、要么低,要么能够保有原来的这份储蓄,要么这份储蓄就被用掉了甚至借更多的钱。

而对于保险来说,如果你没病,那你的生活该怎么样还是怎么样。如果你病了,那会有保险公司给你支付一大笔钱,你也不用和别人借钱,病好后继续该干啥干啥。

因此存钱去防止生病就有赌的成分了,如果没病就白赚了很多钱,如果病了生活质量可能会发生很大的变化。

而保险就可以降低风险,未来即使生病了,由于看病不需要花钱了,病好后生活质量也尽可能的维持在原来轨道 。

我期望未来肯定是尽量稳定的,所以在不影响当前生活质量的条件下我愿意拿出一部分钱来买保险。原计划我可能会 30 岁以后开始买重疾险,之前女朋友的朋友有推荐保险的,然后就跟女朋友一起配置了重疾险。

选保险一定要慎重,一些看起来很划算的保险, 到理赔的时候可能会推三阻四,甚至理赔前公司破产了,尽量要选择大公司。

当然生活没有标准答案,每个人看到世界也都是不同的,我也一直在成长,一直在认识新的东西,上边的所想的也不能保证说未来不会再变。

未来能做的就是多看看书,不限制自己,看看经济学的、哲学的、心理学的、人文的,多出去走走看看,尽可能多的增加人生体验,去认识世界,认识自己,做自己想做的事,爱自己所爱的人,走下去就好了。


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

谈软件项目中的外行管理内行

掘友的一个评论,让我又想跟大家唠会儿嗑了。我之前写过一篇文章叫《该写好代码吗?我也迷茫了》。于是,这位掘友评论说,他在上家公司的时候,他们组代码质量要求高,前期设计考虑全面,所以上线后问题少。但是在领导眼里就像没事做一样。其他组经常出事故,反而很忙,像是一直在...
继续阅读 »

掘友的一个评论,让我又想跟大家唠会儿嗑了。

我之前写过一篇文章叫《该写好代码吗?我也迷茫了》。于是,这位掘友评论说,他在上家公司的时候,他们组代码质量要求高,前期设计考虑全面,所以上线后问题少。但是在领导眼里就像没事做一样。其他组经常出事故,反而很忙,像是一直在攻克难题。

我回复说:那可能是领导不够专业,没有深入到项目里

回复完了,我仍然意犹未尽。于是,才有了这篇文章。

正常情况下,对于一个软件项目或者一次版本迭代,负责人应该是心中有数的。这个项目的难度如何?手里的人员能力如何?大约做多长时间?最终会达到什么效果?这些应该是负责人的基本功。我不自夸,反正我带项目时,我们几个负责人都是能做到的。

据此,你才能更好地把握一个项目的始终。有时,项目中间出现难题,要干预。有时,大家超额完成,要嘉奖。有时,很简单的功能,他开发出了很多bug,要扣奖金。甚至有时候,一个月的工期,老板硬要半个月完成,这时对结果不能要求太高,你不能又要做完又要做好,那是找打的节奏,最终会两败俱伤。

但是,存在一种叫“外行管理内行”的情况。这类领导呢,他不知道你干了啥,也不知道对你来说这活好不好干,甚至你干没干完他也分辨不出,全靠读你的周报和计划。但他却是你的分管领导

我原本以为,在技术领域不会存在这类情况。后来发现,不但有,居然还很普遍。而且在国内,这还是一道别致的景观。

听过一个报道,说国内有一位教授,他从国外买来高端芯片,先把标志磨掉,再印上自己的Logo。然后,他对外宣传是自主研发的。出人意料的是,他还开发布会,还拿到了上亿的科研资金。随后几年,他甚至还推出了第二代、第三代、第四代,都是通过自己印标签的手段完成的。

整个过程,但凡有内行审一下图纸,看一下生产车间,都会很容易识破骗局

当然,咱们普通人一般到不了研究芯片,那么高精尖的层次。但是,我觉得作为一个普通企业的小领导,参与到项目中,这总是可以的吧。

从实际情况来看,其实很少有领导能躬身入局。哪怕这个领导只管六、七个人,他也会再分成三四个组。遇到一项任务,领导会安排给员工,让它们去协商完成。而他,要去规划团队未来的发展。

我有种很明显的感觉,但现在还找不到一个合适的名词来描述它。只能从一些例子上去体会其中的差异。

我有一个任领导,开周会是他做总结。他说这一周,后端完成了什么,前端没有完成什么。前端还反驳他,说自己完成了。领导说,别糊弄我,你只是把接口地址敲上了,根本没调用过,你跑跑试试,数据结构对不上。他是最了解整个项目开发进度的人

还有些领导与他形成鲜明的对比。他们了解事情靠开会听汇报,就算只有三个下属,也要他们说说这周干了啥。他做一下汇总,然后再上报给他的领导。功能交付后,项目出现了问题,他会说是下属欺骗他,居然谎报工期,没测说测了,没做说做了。

至于形成这种区别的原因,可能跟文化和制度有关。不能说哪种绝对好或者不好。

前面那种“严管型”领导所在的公司,企业文化中,负责人制,不讲理由。出了问题不要推脱是张三、李四没做好,他们担不起这个责任,就是你负责人的问题。所以,这才导致了领导会落实每一项流程。

后者那类“汇报型”的企业,从上到下都注重文书的格式、措辞,讲究高瞻远瞩,着眼未来,允许试错,探索创新。因此,他们便将更多的精力投入到了汇报上。

作为一名老程序员,我感觉,软件开发是一种很工程化的工作,一层层拆分好,安排到基层人员手里,告诉他们如何执行就可以了。但是新一代领导说,管理的最高境界是让团队具备自驱力,硅谷就是这样的。自驱力就是你不用安排,他们自己就能主动克服困难去完成。即便领导不在,团队也能照常运转,这才是健康的团队。

抱歉,有点儿跑题了。

本文的初衷是“外行管理内行”与“躬身入局”。

我觉得“外行管理内行”在国内是必然现象,尤其在技术领域

在国内的中小企业,作为团队的领导,“行政类”的事情要比“专业类”多。你看看你的领导,他经常开会和写材料。这不是它自己要干的,也不是老板要求的,是受行业和环境的影响。你想要申报科技企业,申请政策扶持,参加高新评级,你就得去准备。

除此之外,团队的日常管理,年计划、月计划,人员流动、评优、优化等等,都会耗费日常的精力。而这些活,专业的程序员并不想干。

另外,就算一个内行被推上管理岗位,程序员工种复杂,知识更新也快,那他干上几年,也会慢慢被磨成“外行”。

因此,内行外行只是相对的,并不重要。我则更强调“躬身入局”

躬身入局,就是参与到项目中。就算不写代码,起码也了解下大家遇到的问题。在这个过程中,作为管理者,可以制定一些规则和标准,来保证良性运作。好的流程制度,是可以实现员工自驱动的。

举一个身边的例子。现在夏天了,天气很热,办公室都会开一天的空调,温度调到23度。

这天下过大暴雨,空气清爽,气温很低。这时,23度的空调就很冷了。正对着空调口的瘦同事,就想去关掉空调。结果被一个静止都会喘粗气的大胖子给制止了。

胖子问:为啥关空调?

瘦子说:外面下雨,不热了!我冷。

胖子说:你冷你穿衣服。你关了我热。

瘦子说:你热,你打开小风扇。

说完,瘦子关了空调。 随后,胖子又打开了空调。

这类事情就是公说公有理,婆说婆有理。搞不好还需要领导出面调停,或者搞一个群体性的投票。如果没人理你,那就看谁的底线更低,或者说谁更狠。

但是,如果说团队制定这么一个小规则,就是室温超过26度可以开空调制冷,制冷底线是23度。那么,即便没有领导在场,大家也能很好处理此类事情。甚至,这类情况根本都不会发生。

这只是为了体现规则重要性的例子。到软件开发中,可能就是比如这类情形:一个接口如果多个端都调用,那么就由后端组织特定的数据格式。

如果你躬身入局,你就会经历类似的事情。否则的话,你可能会说,吵什么吵,大家要以大局为重(只是要求停止争吵,没有决定开不开空调)。

可能说这么多,有人朝我笑了:你以为我傻?我这么做,老板给我涨工资,多拿钱!

抱歉,抱歉!我只想着如何提高项目质量了。关于挣钱方面,我外行了!

唉,大家一定要重视环境的重要性。


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

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。一步跨进电梯间,我擦...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。

脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。


阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。

一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”

老板:“早,你还在呢?又来带薪划水了?”

我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”

老板:“没有哈哈,我开玩笑。”

我:“我也是,哈哈哈。”

今天的电梯似乎比往常慢了很多。

我:“老板最近在忙什么?”

老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”

我:“卧槽,真能装。没有,哈哈。”

老板:“哈哈哈”。

电梯到了,我俩都步履匆匆地进了公司。

小组内每天早上都有一个晨会,汇报工作进度和计划。

开了一会,转着椅子,划着朋友圈的我停了下来——到我了。

我:“昨天主要……今天计划……”

Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”

我:“影响你合周报了是吗?不是哈哈。”

Leader、小组同事:“哈哈哈“。

Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。

我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”

同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”

Leader、同事、我:“哈哈哈“。

晨会开完,开始工作,产品经理拉我和和前端对需求。

产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”

我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”

产品经理、前端、我:“哈哈哈”。

产品经理:“那我们就对到这了,你们接着聊技术实现。”

前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”

我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”

前端、我:“哈哈哈”。

经过一番拉扯之后,我终于开始写代码了。

看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:

/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/

代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。

又在背着我们偷偷写烂代码了,建议改行。没有哈哈。

同事、我:“哈哈哈”。

终于下班了,路过门口,HR小姐姐还在加班。

我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”

HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”

HR小姐姐、我:“哈哈哈”。

我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——

“既分高下,也决生死”。


写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。

后来,他结婚了。

看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


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

人性的弱点之如何让别人信服你

前言 早些年花时间拜读了《人性的弱点》一书,不可否认其中的一些观点和方法确实很有用,在后来的工作与生活中我有意无意的使用其中的一些语言上的技巧,确实化解了一些无意义的争端,在生活中与人相处也顺利、平和了许多。 当然了,里面的一些观点与方法并不适合所有人,也...
继续阅读 »

前言


早些年花时间拜读了《人性的弱点》一书,不可否认其中的一些观点和方法确实很有用,在后来的工作与生活中我有意无意的使用其中的一些语言上的技巧,确实化解了一些无意义的争端,在生活中与人相处也顺利、平和了许多。


当然了,里面的一些观点与方法并不适合所有人,也许有人会觉得这样很累,也会有人觉得大可不必,因人而异吧, 如果有些朋友明显感觉到在生活、工作中因为说话频频损失了一些机会,那么我建议可以阅读一下这本书。


本文是我对其中一部分章节的提炼与总结,也是后来时长翻阅警醒自己的摘要。


一、避免陷入争论

  1. 丢掉先入为主的观念
  2. 控制好自己的情绪
  3. 耐心听对方说完
  4. 努力找认同点
  5. (语言上)能让步就让步
  6. 真心感谢别人对你的重视

二、永远别说:“你错了。”


永远不要对别人说:“让我来告诉你,你哪里错了!”


这是一种挑衅,会造成对方的反感,会让聆听者想要和你争论,即使你是好心提醒,并没有想要引起争端。


如果你直接了当的告诉他:你错了。人们的第一反应是会反击你,但不会让他们改变主意。


因为你的直接否定了对方的判断力、智商、打击了他们的自尊和骄傲。


三、学会认错

  • 靠争夺,你永远都不能让自己满足;
  • 懂得谦让,你才会收获出乎意料的受益。

四、温柔友爱比狂躁和武力更强大

  1. 一滴蜂蜜比一加仑胆汁更吸引苍蝇
  2. 如果你是充满物理倾向的来谈论问题,别人也会气势汹汹的和你争论
  3. 你用温和友好的态度,别人也会不由自主的认同你的观点
  4. 善良友好的交谈方式更容易让别人接受

五、让对方说“是”

  1. 再开始与别人交流的时候,先认可对方,在刚开始得到“是”也多,越容易被接受
  2. 不要着急把自己的见解说出来,先强调你认同对方的那些观点
  3. 如果一开始就让对方说“是”,那么就会忘记争议,听从我的建议
  4. 温和提出问题,一个可以让对方说“是”的问题
  5. 轻履者行远

六、对待抱怨的安全方式

  1. 人们通常会不停的谈论自己的观点,借此获得他人的信服
  2. 不要打断别人的讲话,即便是你不认同的观点
  3. 耐心听完对方的话,一定要诚恳,还要鼓励他们说完想法和意见
  4. 让对方成为你们交谈的主要人物
  5. 如果想交友,就让你的朋友胜过你

七、让对方觉得自己聪明

  1. 和别人的叙述相比,人们更愿意相信自己努力的出来的结论
  2. 让他人觉得这是依照他的想法所为
  3. 我们不喜欢别人强行把东西卖给我们,也不喜欢别人逼迫我们做某事
  4. 我们希望可以购买自己喜欢的东西,可以做自己喜欢的事
  5. 我们希望别人关心我们的想法和愿望

八、学会换位思考

  1. 一定要站在对方的立场想问题
  2. 一个人即使真的错了,也不会认为自己有错
  3. 人们的一切想法和行为都是有依据的
  4. 想要他人和你倾心交谈,请像重视你自己的感受一向重视他人
  5. 出于自愿的改过,不会有不满的情绪
  6. 想让对方认可你钱,先问问自己:“他为什么想做这种事?”

九、与他人充分共情

  1. 你的观点我一点都不否认,如果换做是我,我也会和你有相同的感受
  2. 你所遇到的人中,百分之九十的人都渴望得到怜悯
  3. 对于那些不愉快的情绪,同情能起到关键性的调节作用
  4. 所有人都希望得到同情

十、激发他人高尚的情操

  1. 人们做一件事的原因有两个,一个是高尚的借口,另一个才是真正的理由
  2. 人们总是把自己理想化,更愿意去相信那些高尚的借口
  3. 当你想让他们是发生改变的时候,就需要把他的高尚动机激发出法
  4. 大部分人都是诚实的, 他们一旦认为自己行为是正确的,就会非常想维护这种正确性。
  5. 我相信即是一个村新欺骗的人,当你嘉定他是真诚正直的时候,他也不想辜负你的信任。

十一、戏剧化表达你的想法

  1. 单凭陈述事实不能解决问题,需要以生动有趣的方式展现出来
  2. 在任何常场所,都可以把你想的想法戏剧化的展现出来
  3. 戏剧化表达方式就是利用某件东西,环境来衬托下面要说的事

十二、发起挑战的激励性

  1. 竞争可以产生效率,所以要激发他的竞争意识
  2. 所有人的心中都有恐惧,只有勇士会忘记恐惧,勇往直前。他们可能一败涂地,但通常都会取得胜利
  3. 战胜恐惧是人世间最大的挑战
  4. 每个成功人士都热爱竞争
  5. 激发人对胜利的渴望,对“被重视的感觉”的渴望


作者:子洋
来源:mdnice.com/writing/2b351a9007a249e19c9d3757876c19e5
收起阅读 »

图解项目管理抓手,如何轻松管理项目?

项目管理抓手.png 一、项目进度控制 1.  制定周报、月报、季报等进度报告,实时监控项目进度,及时跟进。 2. 设置里程碑,定期审核各项任务的完成情况和项目整体进度。 3. 使用项目管理软件或工具,制作项目进度计划,动态分配资源和任...
继续阅读 »
项目管理抓手.png

项目管理抓手.png


一、项目进度控制


1.  制定周报、月报、季报等进度报告,实时监控项目进度,及时跟进。


2. 设置里程碑,定期审核各项任务的完成情况和项目整体进度。


3. 使用项目管理软件或工具,制作项目进度计划,动态分配资源和任务。


4. 加强对关键任务的跟踪和监督,确保按时完成。


5. 积极处理进度偏差,采取调整计划、增加资源等措施弥补延误。


二、项目成本控制


1. 制定具体的项目预算,并下达到每个任务和人员。


2. 实施成本监控,分析阶段成本与实际情况间的差异。


3. 评估项目影响要素,制定成本风险预案。


4. 优化资源配置,最大程度提高投入产出比。


5. 严格审核支出,防范不必要的支出。


三、项目变更管理


1. 根据明确的变更标准和流程管理项目变更事项。


2. 通过项目变更委员会或会议,审议和批准变更。


3. 评估变更对其他事项的影响,并做出相应调整。


4. 详细记录变更内容及其历史,方便查询和参考。


5. 定期分析项目变更趋势,降低未来变更的可能性。


四、质量控制


1. 对项目产出物在功能、性能、兼容性和可靠性等方面进行测试和复核。


2. 设置质量标准和控制措施,并对团队成员进行培训和监督。


3. 针对测试或使用过程中发现的问题进行跟踪处理和改进。


4. 详细记录质量数据和信息,分析质量事件的影响因素。


5. 不断完善质量管理流程和标准 ,以提高整体质量水平。


六、风险管理


1. 开展项目风险识别,穷尽可能出现的风险因素。


2. 对风险因素进行定量分析,评估影响程度和可能性。


3. 制定风险应对计划并统筹资源,积极化解风险。


4. 持续监测项目进展中的不确定性,及时更新风险分析与应对。


5. 分析风险事件的教训与启示。为今后提供借鉴。


七、项目团队管理


1.  明确团队各成员的工作任务和职责。


2. 建立沟通机制和交流渠道。


3. 通过奖励机制来激励团队工作积极性。


4. 建立团队协同工作能力,提高效率和效果。


5. 不断完善团队组建和管理流程与机制。


四、其他方面


1. 统筹资源配置,有效考核绩效,激励执行。


2. 保存项目相关文件,创建项目知识库。


3. 分析项目经验教训,为今后提供借鉴。


4. 加强项目状态信息和数据的共享。


5. 不断优化项目管理流程,以提高效率。


作者:极客技术之路
来源:mdnice.com/writing/a3b44581c0f5475a9def01b2c762b3fa
收起阅读 »

8个方面快速提高项目交付速度

项目完成和项目成功地完成是两个不同的概念。作为一个专业的项目管理人员,需要确保项目尽可能地输出好的成果,这个成果要满足以下两个基本要求,才能算是真正成功的项目交付,其中 达到内外部客户满意的要求 是最重要的。 达到基本的质量要求 ...
继续阅读 »

项目完成和项目成功地完成是两个不同的概念。作为一个专业的项目管理人员,需要确保项目尽可能地输出好的成果,这个成果要满足以下两个基本要求,才能算是真正成功的项目交付,其中 达到内外部客户满意的要求 是最重要的。





  • 达到基本的质量要求



  • 达到内外部客户满意的要求


以下是整理的8个方面提高项目交付速度,供大家参考。





  • 提前开始



  • 停止多任务



  • 设定预期



  • 并行增加工作量



  • 减少需求



  • 优化依赖关系



  • 尝试增加风险



  • 优先排序


一、提前开始



如果在项目还没有正式开始之前,项目经理就已经知道项目的时间限制,那么可以提前做好项目的准备工作。特别是对于项目需要交付的内容做好提前规划,对于确定性的工作形成合适的规划,对于不确定的部分提前做好变化应对。一旦不确定性的工作确定下来,可以节省项目前期规划的时间,加快整个项目的交付进度。






  1. 提前组建项目团队,尽早进入工作状态





  2. 与客户进行需求交流,尽早确定需求范围和优先级





  3. 对确定的需求提前进行设计和规划





  4. 将确定的设计转换为项目任务和工作包,形成初步的项目计划。





  5. 对不确定需求,提前制定假设和方案,待确定后快速推进





  6. 提前准备技术方案,评估不同方案的可行性。





  7. 预先确定供应商,签订合同,准备材料





  8. 制定项目进度表、资源计划,确定项目范围。





  9. 提前测试相关工具、环境,确保可用性。





  10. 制定风险管理策略,识别可能的风险因素。




二、停止多任务



许多报告和研究表明,在项目中不断从一个任务转向另一个任务是一种不切实际的工作方式,而且这种方式反而会让工作效率变得更低。项目经理需要确保项目团队成员每天留出专用的时间专注于某一个任务,这样他们才能更好地完成工作,更少地分心,更有可能在截止期限前完成任务。






  1. 制定项目工作规范,建立不间断工作时间段,减少随机任务切换





  2. 对团队成员的工作时间进行合理规划,留出至少2-3小时的专注工作区间。





  3. 在专注区间内,关闭通讯工具,减少外部干扰。





  4. 制定个人工作计划表,按优先级专注于一项任务,避免跳跃作业。





  5. 优化工作环境,减少外界干扰源。





  6. 建立时间管理意识,记录每个工作区间的产出。





  7. 跟踪工作进度,衡量多专注工作的效果提升。





  8. 培养团队自律习惯,鼓励单项任务深度完成。





  9. 合理安排休息,保证工作效率。





  10. 不断优化多任务问题,提升团队效率。




三、设定预期


> 不要总是以为项目的发起人对项目的进度、成本和质量都关注,他们可能只会关注其中的某一个方面。项目经理要管理项目关键利益相关者的期望,一定要找出对方最为关注的点。这是非常重要的一件事,这样一旦项目中出现冲突的时候,项目经理可以做出一定的取舍。如果重点关注质量,并想要提前交付完成,那就可以在保证质量的前提下要求增加资源。





  1. 与项目发起人和关键利益相关方进行充分沟通,理解各方的需求和期望。





  2. 制定项目三角管理框架,评估客户对成本、进度、质量、范围各方面的关注点。





  3. 对客户进行需求调研和访谈,确认关键的期望指标。





  4. 构建项目目标体系,确定客户的主要关注点,如质量或交付时间。





  5. 根据主要关注点设定项目进度和质量目标,形成项目章程。





  6. 进行风险分析,识别可能影响关键关注点的风险因素。





  7. 制定风险缓解策略,保证达成客户关键预期。





  8. 与客户持续沟通,明确范围变更对预期的影响。





  9. 在项目执行中优先保证客户关键关注点,做出必要权衡和取舍。





  10. 不断确认客户的关注点,及时调整项目目标。




四、并行增加工作量



项目经理可以对项目中的各项任务进行审核和分析,看看哪些工作可以早点启动,哪些工作需要和其他任务一并执行。项目经理需要同时注意资源管理,因为你无法安排同一个人同时执行两项任务,如果确实增加了工作量,那就需要引进更多的人来帮助你进行任务管理。






  1. 分析任务依赖关系,识别可以并行执行的任务





  2. 对独立任务进行资源评估,确定可以增加的并行工作量。





  3. 根据关键路径,优先使关键任务尽早开始





  4. 对冗余资源较充裕的任务适当提前启动。





  5. 重新制定资源计划,扩充项目团队,提供并行任务所需资源。





  6. 制定多团队协作机制,加强沟通和协调。





  7. 监控各并行任务的进展,必要时进行资源调配。





  8. 合理安排任务优先级,平衡资源使用。





  9. 加强项目整体计划和风险管理。





  10. 避免过度并行增加项目管理复杂度。




五、减少需求



项目经理可以与项目发起人一起商量这个项目究竟能砍掉些什么,哪些可选的要求能够取消,将范围缩小到实际可以实现的交付工作量。通过这种方式可以去掉不能为实现项目主要目标增值的任务,也就是意味着缩小项目的范围,从而减少交付工时,加快项目完成进度。






  1. 与客户沟通确定核心需求和次要需求。





  2. 对次要需求进行价值分析,评估其实施的优先级。





  3. 制定不同的交付方案,包含次要需求的全量和简化版本。





  4. 估算各方案的交付进度,与客户讨论不同方案的业务影响。





  5. 根据客户的意见,确定移除或简化的次要需求。





  6. 更新需求文档,移除或标记出简化处理的需求。





  7. 重新评估工作量和交付时间表,更新项目计划。





  8. 调整资源分配,将精力集中在核心需求的交付上。





  9. 加强对范围变更的管理,避免需求不断膨胀





  10. 提高客户沟通频率,及时获取客户反馈




六、优化依赖关系



项目中各项任务之间的依赖关系在调度中非常重要,而且往往是项目计划中的关键组成部分,也意味着项目任务必须按照顺序进行。但是,实际的项目交付并非一成不变地按照项目经理制订的计划来进行。这个时候,项目经理要检查所有的依赖关系,看看哪些是必需的,哪些是需要去除的。大多数情况下都能发现一些可以改变和优化的地方。






  1. 清理任务之间不必要的依赖关系,识别可以同时进行的任务。





  2. 对依赖关系进行质疑和验证,确定必须依赖的关键任务链。





  3. 采用方法设计、敏捷开发等,使任务模块更加独立。





  4. 明确依赖关系中的关键路径任务,缩短其时长。





  5. 对冗余依赖任务进行重构、优化或外包。





  6. 增加资源投入,使依赖任务可以并行推进。





  7. 引入新的技术或方法,简化任务流程,减少依赖。





  8. 加强沟通协作,使信息流通,及时消除依赖。





  9. 监控依赖关系变化,及时更新项目计划。





  10. 继续寻找简化依赖的新思路。




七、尝试增加风险


很多时候,过于保守地执行计划会导致项目进度缓慢。如果出现这种情况,为了更快地交付,项目经理可以尝试做出一些改变而不是墨守成规。项目本身的存在即是一种风险,项目经理不应该简单地避免风险,而是要学会管理风险,需要查看项目规划阶段时的假设,看看能否通过一些假设来推动项目进度。





  1. 识别项目进展缓慢的领域,寻找可采取的风险。





  2. 对增加进行成本效益分析,评估收益和代价。





  3. 优先选择对项目进度影响显著、风险较可控的方案。





  4. 制定风险缓解和应急策略,做好风险管理。





  5. 留出时间和资源增加风险缓冲。





  6. 与团队和利益相关方沟通,获取支持。





  7. 监控风险状况,根据需要及时调整策略。





  8. 加强数据统计分析,衡量风险的成效。





  9. 记录风险管理过程,总结经验教训。





  10. 不断提高风险意识和管理能力。




八、优先排序


项目经理可以对工作量和要求进行优先级排序,将低优先级的工作或项目延后,提前完成项目中的高优先级部分。





  1. 明确项目关键路径和里程碑节点





  2. 根据客户需求与项目目标,确定各项任务的优先级。





  3. 制定优先级评估矩阵,评分确定任务顺序。





  4. 优先安排关键路径和高优先级任务所需资源。





  5. 优化资源计划,将非关键任务适当推迟。





  6. 对低优先任务简化要求,减少工作量。





  7. 提前完成高优先级任务,为后续任务创造缓冲。





  8. 与客户沟通,获取对优先级的确认。





  9. 监控进展情况,必要时调整任务优先级。





  10. 持续总结和改进优先排序的决策机制。




作者:极客技术之路
来源:mdnice.com/writing/42e1d4e8e00843e7a5dcdfa6d5db9781
收起阅读 »

看完这篇,SpringBoot再也不用写try/catch了

前言 使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。 ...
继续阅读 »

前言


使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。


本篇主要讲述在SpringBoot 中,如何用全局异常处理优雅的处理异常。


为什么要优雅的处理异常


如果我们不统一的处理异常,开发人员经常会在代码中东一块的西一块的写上 try catch代码块,长久以往容易堆积成屎山。


@Slf4j
@Api(value = "User Interfaces", tags = "User Interfaces")
@RestController
@RequestMapping("/user")
public class UserController {
    /**
     * @param userParam user param
     * @return user
     */

    @ApiOperation("Add User")
    @ApiImplicitParam(name = "userParam", type = "body", dataTypeClass = UserParam.classrequired true)
    @PostMapping("add")
    public ResponseEntity add(@Valid @RequestBody UserParam userParam) {
        // 每个接口都需要手动try catch
        try {
            // do something
        } catch(Exception e) {
            return ResponseEntity.fail("error");
        }
        return ResponseEntity.ok("success");
    }
}

那我们应该如何实现统一的异常处理呢?


使用 @ControllerAdvice + @ExceptionHandler注解



@ControllerAdvice 定义该类为全局异常处理类


@ExceptionHandler 定义该方法为异常处理方法。value 的值为需要处理的异常类的 class 文件。



首先自定义异常类 BusinessException :


/**
 * 业务异常类
 * @author rango
 */

@Data
public class BusinessException extends RuntimeException {
    private String code;
    private String msg;
 
    public BusinessException(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

然后编写全局异常类,用 @ControllerAdvice 注解:


/**
 * 全局异常处理器
 * @author rango
 */

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 处理 Exception 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public ResponseEntity exceptionHandler(HttpServletRequest httpServletRequestException e
{
        logger.error("服务错误:", e);
        return new ResponseEntity("******""服务出错");
    }
 
    /**
     * 处理 BusinessException 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = BusinessException.class)
    public ResponseEntity businessExceptionHandler(HttpServletRequest httpServletRequestBusinessException e
{
        logger.info("业务异常报错!code:" + e.getCode() + "msg:" + e.getMsg());
        return new ResponseEntity(e.getCode(), e.getMsg());
    }
}


定义了全局异常处理器,项目就可以对不同的异常进行统一处理了。通常,为了使 controller 中不再使用任何 try/catch,会在 GlobalExceptionHandler 中对 Exception 做统一的拦截处理。这样其他没有用 @ExceptionHandler 配置的异常就都会统一被处理。


遇到异常时主动抛出异常


在业务中,遇到业务异常的地方,我们直接 throw 抛出对应的业务异常即可。如下所示


throw new BusinessException(ERROR_CODE, "用户账号/密码有误");

在 Controller 中的写法


Controller 中,不需要再写 try/catch,除非特殊场景。


@RequestMapping(value = "/test")
public ResponseEntity test() {
    ResponseEntity re = new ResponseEntity();
    // 业务处理
    return re;
}

结果展示


异常抛出后,返回如下结果。


{
    "code""E0014",
    "msg""用户账号/密码有误",
    "data"null
}

注意!!!



  • 抛出的异常如果被代码内的 try/catch 捕获了,就不会被 GlobalExceptionHandler 处理



  • 异步方法中的异常不会被全局异常处理(多线程)



  • 不是 controller 层抛出的异常才能被 GlobalExceptionHandler 处理,只要异常最后是从 contoller 层抛出去的都可以被捕获并处理

总结


本文介绍了使用 SpringBoot 时,如何通过配置全局异常处理器统一处理项目中的一些通用的异常,避免程序员不断的写try/catch导致的代码冗余,有利于代码的维护。


作者:程序员典籍
来源:mdnice.com/writing/103055f00ba04cf4b06f0195f839a449
收起阅读 »

pnpm 是凭什么对 npm 和 yarn 降维打击的

web
大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。 那具体好在哪里呢? 我们一起来看一下。 我们按照包管理工具的发展历史,从 npm2 开始讲起: npm2 用 node 版本管理工具把...
继续阅读 »

大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。
那具体好在哪里呢? 我们一起来看一下。



我们按照包管理工具的发展历史,从 npm2 开始讲起:


npm2


用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。


768C1B00093D82D19D2CC333F3221670.jpg


然后找个目录,执行下 npm init -y,快速创建个 package.json。


然后执行 npm install express,那么 express 包和它的依赖都会被下载下来:


FB54F396F6A73093CE052A6881AF7C50.jpg
展开 express,它也有 node_modules:


E652FB00C06BA36FA8861E3B785981BF.jpg
再展开几层,每个依赖都有自己的 node_modules:


75AC9B15A99383C9EA9E5E4EF8302588.jpg
也就是说 npm2 的 node_modules 是嵌套的。


这很正常呀?有什么不对么?


这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。


这个还不是最大的问题,致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。


当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:


yarn


yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?


铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。


我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:


这时候 node_modules 就是这样了:


7AF387F155588612B92C329F91D30BFA.jpg


全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:


BBBA3B5F68AB691541FD51569E5B1316.jpg


当然也有的包还是有 node_modules 的,比如这样:


B5E0DBF3C7E6FBEEDC2CC90D350A278C.jpg
为什么还有嵌套呢?


因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。


npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:


67B0E1280BAD542944AB08A35CCE88C3.jpg
当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。


yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?


并不是,扁平化的方案也有相应的问题。


最主要的一个问题是幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。


这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。


但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。


这就是幽灵依赖的问题。


而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。


那社区有没有解决这俩问题的思路呢?


当然有,这不是 pnpm 就出来了嘛。


那 pnpm 是怎么解决这俩问题的呢?


pnpm


回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?


那如果不复制呢,比如通过 link。


首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。


如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?


这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。


没错,pnpm 就是通过这种思路来实现的。


再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。


你会发现它打印了这样一句话:


FA450CB6BE37F7AEDADDD7AF8CB5EBF9.jpg


包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。


我们打开 node_modules 看一下:


DD21BA4ABF8516795C6BC205C18793E3.jpg
确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。


展开 .pnpm 看一下:


25BF2AA593655F0A20232371A43AB81A.jpg
所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。


比如 .pnpm 下的 expresss,这些都是软链接:


6F84C353D1CFE72E2F820B62C9A3B96E.jpg
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。


官方给了一张原理图,配合着看一下就明白了:


0E694CA43CC1E52ED6AF8BCD50882004.jpg
这就是 pnpm 的实现原理。


那么回过头来看一下,pnpm 为什么优秀呢?


首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。


其次就是快,因为通过链接的方式而不是复制,自然会快。


这也是它所标榜的优点:


image.png


相比 npm2 的优点就是不会进行同样依赖的多次复制。


相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。


这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。


总结


pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因:


npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。


npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。


pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。


这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。


pnpm 就是凭借这个对 npm 和 yarn 降维打击的。


作者:JEECG官方
来源:juejin.cn/post/7260283292754919484
收起阅读 »

前端发展:走进行业迷茫的迷雾中

web
引言 2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。 第一部...
继续阅读 »

引言


image.png
2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。


第一部分:前端的价值


image.png
前端开发作为网页和移动应用程序开发的重要组成部分,扮演着连接用户与产品的桥梁。前端技术的发展不仅推动了用户体验的提升,也对整个互联网行业产生了深远的影响。随着移动互联网的普及和技术的进步,前端在用户与产品之间的交互变得越来越重要。


对于企业而言,拥有优秀的前端开发团队意味着能够提供更好的用户体验、增强品牌形象、吸引更多用户和扩大市场份额。因此,前端开发的技能依然是企业争相追求的核心能力之一。


第二部分:行业不景气的背后


image.png
然而,正如每个行业都经历高低起伏一样,前端开发也面临着行业不景气带来的挑战。2023年,全球经济增长乏力、市场竞争激烈以及萧条的就业市场等因素,使得许多公司紧缩预算、停止招聘,并导致了失业率的上升和裁员的潮水。


在这种情况下,前端开发者需要重新审视自己的技能和市场需求。他们需要具备综合能力,包括对最新前端技术的深入了解、与其他团队成员的良好沟通合作能力以及持续学习和适应变化的能力。


第三部分:自我调整与进阶


image.png
面对市场变化和就业压力,前端开发者需要主动调整自己的发展路径。以下是一些建议:



  1. 多元化技能:学习并精通多种前端框架和库,如React、Vue.js和Angular等。同时,了解后端开发和数据库知识,拥有全栈开发的能力,将会让你在就业市场上更具竞争力。

  2. 学习与实践并重:不仅仅是学习新知识,还要将所学应用于实际项目中。积累项目经验,并在GitHub等平台分享你的作品,以展示自己的能力和潜力。同时,参加行业内的比赛、活动和社区,与他人交流并学习他们的经验。

  3. 持续学习:前端技术发展日新月异,不断学习是必需的。关注行业的最新趋势和技术,参加培训、研讨会或在线课程,保持对新知识的敏感度和学习能力。


第四部分:面对就业市场的挑战


image.png
在面对行业不景气和裁员的情况下,重新进入就业市场变得更加具有挑战性。以下是一些建议:



  1. 提升个人竞争力:通过获得认证、实习或自主开发项目等方式,提升自己在简历中的竞争力。扩展自己的专业网络,与其他开发者和雇主建立联系。

  2. 寻找新兴领域:探索新兴的技术领域,如大数据、人工智能和物联网等,这些领域对前端开发者的需求逐渐增加,可能为你提供新的机会。

  3. 转型或深耕细分领域:如果市场需求不断减少,可以考虑转型到与前端相关的领域,如UI设计、交互设计或用户体验设计等。或者在前端领域深耕细分领域,在特定行业或特定技术方向上寻找就业机会。


结论


image.png
虽然当前的行业环境确实严峻,但前端开发作为连接用户与产品的重要纽带,在未来依然有着广阔的发展空间。关键在于前端开发者要不断自我调整与进阶,持续学习并适应市场需求。通过多元化技能、学习实践、提升个人竞争力以及面对市场挑战,前端开发者依然可以在这个变革

作者:Jony_men
来源:juejin.cn/post/7260330862289371173
时代中谋得一席之地。

收起阅读 »

树结构的数据扁平化

web
function flattenTree(data) { data = JSON.parse(JSON.stringify(data)); var res = []; while(data.length) { var n...
继续阅读 »

function flattenTree(data) {
data = JSON.parse(JSON.stringify(data));
var res = [];
while(data.length) {
var node = data.shift();
if (node.children && node.children.length) {
data = data.concat(node.children);
}
delete node.children;
res.push(node);
}
return res;
}


我们用一个数据来测试:



var tree = [{
id: 1,
name: '1',
children: [{
id: 2,
name: '2',
children: [{
id: 3,
name: '3',
children: [{
id: 4,
name: '4'
}]
}, {
id: 6,
name: '6'
}]
}]
}, {
id: 5,
name: '5'
}]


使用:



console.log(flattenTree(tree));


打印结果:


image.png


作者:tntxia
来源:juejin.cn/post/7260500913848090661
收起阅读 »

千万级高可用分布式对账系统设计实践

背景         目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理...
继续阅读 »

背景


        目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理。面对千万级的订单量,人工对账肯定是不可行的,所以,实现一套对账系统成为了必然的事,不仅为资金安全提供依据,也节省公司运维人力,数据更加可视化。目前这套系统已覆盖聚合渠道网关与外部渠道100%的对账业务,完成春晚期间支付宝亿级订单量对账,完成日常AC项目千万级订单量对账,对账准确率实现6个9,为公司节省2~3个人力。


介绍


        对账模块是支付系统的核心功能之一,不同业务设计的对账模型不同,但是都会遇到以下几个问题:



  • 海量的数据,就目前聚合支付的订单量来看,设计的对账系统需要应对千万级的数据量;

  • 面对日切、多账、少账等异常差异订单应该如何处理;

  • 账单格式、下载账单时间、下载方式等不一致问题。


        针对以上问题,并结合财经聚合支付系统的特点,本文将设计一套可以应对千万级数据量、分布式和高可用的对账系统,利用消息队列Kafka的解耦性解决对账系统各模块之间的强依赖性。文章从三个方面介绍对账系统,第一方面,总体介绍对账系统的设计,依次介绍各个模块的实现及其过程中使用到的设计模式;第二方面,介绍对账系统版本迭代的过程,为什么需要进行版本迭代,以及版本迭代过程中踩过的“坑”;第三方面,总结现有版本的特点并提出下一步的优化思路。


系统设计


系统结构图


        图1为对账系统总结构图,分为六个模块,分别是文件下载模块、文件解析并推送模块、平台数据获取并推送模块、执行对账模块、对账结果统计模块和中间态模块,每个模块负责自己的职能。
对账系统总结构图


图1 对账系统总结构图


        图2为对账系统利用Kafka实现的状态转换图。每个模块独立存在,彼此之间通过消息中间件Kafka实现系统状态转换,通过中间态UpdateReconStatus类实现状态更新和message发送。这种设计不仅实现流水线对账,也利用消息中间件的特点,实现重试和模块之间的解耦。

对账系统状态转换图.png


图2 对账系统状态转换图


        为了更好的了解每个模块的实现过程,下面将依次对各个模块进行说明。

文件下载模块


设计

        文件下载模块主要完成各个外部渠道账单的下载功能。众所周知,聚合支付是聚众家三方机构能力为一体的支付方式,其中三方机构包括支付宝、微信等支付界的领头羊,多样性的支付渠道导致账单下载存在多样性,如何实现多模式、可拔插的文件下载能力成为该模块设计的重点。分析Java设计模式的特点,本模块采用接口模式,符合面向对象的设计理念,可实现快速接入。具体实现类图如图3所示(只展示部分类图)。


图3 文件下载实现类图


        下面就以支付宝对账文件下载方式为例,具体阐述一下实现过程。


实现

        分析支付宝接口文档,目前采用的下载方式为HTTPS,文件格式为.csv的压缩包。根据这些条件,本系统的实现方式如下(只摘取了部分代码)。由于消息中间件Kafka和中间态模块的机制,已经从系统层面考虑了重试的能力,因此不需要考虑重试机制,后续模块也如此。


public interface BillFetcher {
// ReconTaskMessage 为kafka消息,
// FetcherConsumer为自定义账单下载后的处理方式
String[] fetch(ReconTaskMessage message,FetcherConsumer consumer) throws IOException;
}

@Component
public class AlipayFetcher implements BillFetcher {

public AlipayFetcher(@Autowired BillDownloadService billDownloadService) {
Security.addProvider(new BouncyCastleProvider());
billDownloadService.register(BillFetchWay.ALIPAY, this);
}
...
@Override
public String[] fetch(ReconTaskMessage message, FetcherConsumer consumer) throws IOException {
String appId = map.getString("appId");
String privateKey = getConvertedPrivateKey(map.getString("privateKey"));
String alipayPublicKey = getPublicKey(map.getString("publicKey"), appId);
String signType = map.getString("signType");
String url = "https://openapi.alipay.com/gateway.do";
String format = "json";
String charset = "utf-8";
String billDate = DateFormatUtils.format(message.getBillDate(), DateTimeConstants.BILL_DATE_PATTERN);
String notExists = "isp.bill_not_exist";
String fileContentType = "application/oct-stream";
String contentTypeAttr = "Content-Type";
//实例化客户端
AlipayClient alipayClient = new DefaultAlipayClient(url, appId, privateKey, format, charset, alipayPublicKey, signType);
//实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
// trade指商户基于支付宝交易收单的业务账单
// signcustomer是指基于商户支付宝余额收入及支出等资金变动的帐务账单
request.setBizContent("{" +
""bill_type":"trade"," +
""bill_date":"" + billDate + """ +
" }");
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
//do 根据下载地址获取对账文件,通过流式方式将文件放到指定的目录下
...
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}
}
}

具体步骤:



  1. 重写构造方法,将实现类注入到一个map中,根据对应的下载方式获取实现类;

  2. 实现fetch接口,包括构造请求参数、请求支付宝、解析响应结果、采用流式将文件放入对应的目录下,以及这个过程中的异常处理。


文件解析并推送模块


设计

        前面提到,聚合支付是面对不同的外部渠道,对账文件的多样性不言而喻。比如微信是采用txt格式,支付宝采用csv格式等等,而且各个渠道的账单内容也是不一致的。如何解决渠道之间账单的差异性成为该模板需要重点考虑的问题。通过调研和现有对账系统的分析,本系统采用接口模式+RDF(结构化文本文件)的实现方式,其中接口模式解决账单多模式的问题,同时也实现可拔插的机制,RDF工具组件实现账单的快速标准化,操作简单易会。具体实现类图如图4所示(只展示部分类图)。


图4 文件标准化实现类图


        下面就以支付宝对账文件解析为例,具体阐述一下实现过程。
实现

        根据支付宝的账单格式,提前定义RDF标准模板,后续账单解析将根据模板将每一行对账文件解析为对应的一个实体类,其中需要注意标准模板的字段必须要和账单数据一一对应,实体类的字段可以多于账单字段,但必须包括所有的账单字段。接口定义如下:


public interface BillConverter<T> {
//账单是否可以使用匹配器
boolean match(String channelType, String name);
//转换原始对账文件到Hive
void convertBill(InputStream sourceFile, ConverterConsumer<T> consumer) throws IOException;
//转换原始对账文件到Hive
void convertBill(String localPath, ConverterConsumer<T> consumer) throws IOException;
}

具体实现步骤如图5所示:


流程图.png


图5 文件解析流程图



  1. 定义RDF标准模板,如下为支付宝业务流水明细模板,其中body结构内字段名必须和实体类名保持一致。


{
"head": [
"title|支付宝业务明细查询|Required",
"merchantId|账号|Required",
"billDate|起始日期|Required",
"line|业务明细列表|Required",
"header|header|Required"
],
"body": [
"channelNo|支付宝交易号",
"merchantNo|商户订单号",
"businessType|业务类型",
"production|商品名称",
"createTime|创建时间|Date:yyyy-MM-dd HH:mm:ss",
"finishTime|完成时间|Date:yyyy-MM-dd HH:mm:ss",
"storeNo|门店编号",
"storeName|门店名称",
"operator|操作员",
"terminalNo|终端号",
"account|对方账户",
"orderAmount|订单金额|BigDecimal",
"actualReceipt|商家实收|BigDecimal",
"alipayRedPacket|支付宝红包|BigDecimal",
"jiFenBao|集分宝|BigDecimal",
"alipayPreferential|支付宝优惠|BigDecimal",
"merchantPreferential|商家优惠|BigDecimal",
"cancelAfterVerificationAmount|券核销金额|BigDecimal",
"ticketName|券名称",
"merchantRedPacket|商家红包消费金额|BigDecimal",
"cardAmount|卡消费金额|BigDecimal",
"refundOrRequestNo|退款批次号/请求号",
"fee|服务费|BigDecimal",
"feeSplitting|分润|BigDecimal",
"remark|备注",
"merchantIdNo|商户识别号"
],
"tail": [
"line|业务明细列表结束|Required",
"tradeSummary|交易合计|Required",
"refundSummary|退款合计|Required",
"exportTime|导出时间|Required"
],
"protocol": "alib",
"columnSplit":","
}


  1. 实现接口的getChannelType、match方法,这两个方法用于匹配具体使用哪一个Convert类。如匹配支付宝账单,实现方式为:


@Override
public String getChannelType() {
return ChannelType.ALI.name();
}
@Override
public boolean match(String channelType, String name) {
return name.endsWith(".csv.zip");
}


  1. 实现接口的convertBill方法,完成账单标准化;


@Override
public void convertBill(String path, ConverterConsumer<ChannelBillPojo> consumer) throws IOException
{
FileConfig config = new FileConfig(path, "rdf/alipay-business.json", new StorageConfig("nas"));
config.setFileEncoding("UTF-8");
FileReader fileReader = FileFactory.createReader(config);
AlipayBusinessConverter.AlipayBusinessPojo row;
try {
while (null != (row = fileReader.readRow(AlipayBusinessConverter.AlipayBusinessPojo.class))) {
convert(row, consumer);
}
...
}


  1. 将标准化账单推送至Hive


平台数据获取并推送模块


        平台数据获取一般都是从数据库中获取,数据量小的时候,查询时数据库的压力不会很大,但是数据量很大时,如电商交易,每天成交量在100万以上,通过数据库查询是不可取的,不仅效率低,而且容易导致数据库崩溃,影响线上交易,这点会在后续的版本迭代中体现。因此,平台数据的抽取是从Hive上获取,只需要提前将交易数据同步到Hive表中即可,这样做不仅效率高,而且更加安全。考虑到抽取的Hive表不同、数据的表结构,数据收集器Collector类也采用了接口模式。Collector接口定义如下:


public interface DataCollector {
void collect(OutputStream os) throws IOException;
}

        根据目前平台数据收集器实现情况,可以得到类图如图6所示。


图6 平台数据收集器实现类图


执行对账模块


        该模块主要完成Hive命令的执行,在平台账单和渠道账单已全部推送至Hive的前提下,利用Hive处理大数据效率高的特点,执行全连接sql,并将结果存入指定的Hive表中,用于对账结果统计。执行对账sql可以根据业务需求而定,如需要了解本系统的全连接sql,欢迎与我交流。


对账结果统计模块


        对账任务执行成功之后,需要统计全连接后的数据,重点统计金额不一致、状态不一致、日切、少账(平台无账,渠道有账)和多账(平台有账,渠道无账)等差异。针对不同的情况,本系统分别采用如下的解决方案:



  1. 金额不一致:前端页面展示差异原因,人工进行核对;

  2. 状态不一致:针对退款订单,查询平台退款表,存在且金额一致认为已对平,不展示差异,其他情况,需要在前端页面展示差异原因,人工进行核对;

  3. 日切:当平台订单为成功,渠道无单时,根据平台订单创建时间判断是否可能存在日切,如果判断是日切订单,会将这笔订单存入buffer文件中,待统计结束后,将buffer文件上传至Hive日切表中,等第二天重新加载这部分数据实现跨日对账。对于平台无订单,渠道有单的情况,通过查询平台数据库判断是否存在差异,如果存在差异,需要在前端页面展示差异,人工进行核对。

  4. 少账:目前主要通过查询平台数据库判断是否存在差异,确认确实存在差异时,需要在前端页面展示差异,人工进行核对。

  5. 多账:目前这种有可能是日切,会先考虑日切,如果不在日切范围内,需要在前端页面展示差异,人工进行核对。


中间态模块


        中间态模块是用于各模块之间状态转换的模块,利用Kafka和状态是否更新的机制,实现消息的重发和对账状态的更新。从一个状态到下一个状态,必须满足当前状态为成功,对账流程才会往下一步执行。中间态的设计不仅解决了重试问题,而且将数据库的操作进行了收敛,更符合模块化的设计,各个模块各司其职。重试次数也不是无限的,目前设置的重试次数为3次,如果3次重试后依然没有成功,会发lark通知,人工介入解决。


        总之,对账工作,既复杂也不复杂,需要我们细心,对业务要有深入的了解,并选择合适的处理方式,针对不同的业务,不断迭代优化系统。


版本迭代


        系统的设计很大程度受业务规模的影响,对于财经聚合支付而言,订单量发生了几个数量级的变化,这个过程中不断暴露出对账系统存在的问题,优化改进对账系统是必然的事。从系统设计到目前大致可以分为三个阶段:初始阶段、过渡阶段和当前阶段。


初始版(v1.0)

        初始版上线后实现了聚合渠道对账的自动化,尤其在2018年的春节活动中,资金安全提供了重要的保障,实现了聚合和老合众、支付宝、微信等渠道的对账。随着财经业务的发展,抖音电商的快速崛起,对账系统逐渐暴露出不足,比如对账任务失败增多,尤其是数据量大的对账、非正常差异结果展示、对账效率低等问题。通过不断分析,发现存在以下几个问题:



  1. 系统的文件都是放在临时目录tmp下的,TCE平台会对这个目录下的文件定时清理,导致推送文件到Hive时会报找不到文件的情况,尤其是大数据量的对账任务;

  2. Kafka消息积累多,导致对账流程中断,主要是新增渠道,对账任务增加,同时Hive执行队列是共享队列,大部分的对账流程因为没有资源而卡住;

  3. 非正常差异结果展示,主要是查单没有增加重试机制,当查询过程中出现超时等异常,会出现非正常差异结果,还有部分原因是日切跨度小而导致的非正常差异结果。


过渡版(v2.0)

        考虑到初始版对账系统存在的不足和对账功能的急迫性,对初始版进行过渡性的优化,初步实现大数据量的对账功能,同时也提高了差异结果的准确率。相比初始版,该版本主要进行了以下几点优化:



  1. 文件存放目录由临时目前改为服务下的某一个目录,防止大文件被回收,文件上传到Hive后删除文件;

  2. 重新申请独占的执行队列,解决资源不足导致对账流程卡住的问题;

  3. 查单新增重试机制,日切跨度增大,解决非正常差异结果展示,提供差异结果的准确率。


        过渡版集中解决初始版明显存在的问题,对于一些潜在的问题并没有彻底解决,如代码容错率低、对账任务异常后人工响应慢、对账效率低、数据库安全性低等问题。


当前版(v3.0)

        当前版优化的宗旨是实现对账系统的"三高",分别为高效率、高准确率(6个9)和高稳定性。


        对于高效率,主要体现在平台数据获取慢,而且存在数据库安全问题,针对这块逻辑进行了优化,改变数据获取途径,由原来的数据库获取改为从高效率的Hive中获取,只需要提前将数据同步到Hive表中即可。


        对于高准确率,主要优化对账差异处理逻辑,进一步细化差异处理方式,新增差异结果报警,细化前端页面差异原因。


        对于高稳定性,主要优化RDF处理对账文件发生异常时新增兜底逻辑,提高系统的容错性;对账任务失败或超过指定重试阈值时增加报警,加快人工响应速率;对查单等操作数据库逻辑增加限流,防止数据库崩溃。


        版本迭代过程可以总结如下,希望读者别重复入坑,尤其是大文件处理方面。


业务情况优点存在的问题目标
初始版(v1.0)财经部门初期,订单量少,业务结构简单实现少量交易量对账;支持分布式效率低;对账任务容易卡住;非异常case普遍;大数据基本不能完成对账保障资金安全问题,实现聚合渠道网关与外部渠道的对账功能
过渡版(v2.0)电商业务崛起,订单量增加,业务种类增多实现海量数据对账;查单新增重试机制;降低非异常case数量影响数据库安全性;代码容错率低;对账效率低;对账任务异常时人工响应慢支持千万级订单量对账
当前版(v3.0)优化过渡版遗漏问题,改变数据获取路径效率大大提升;实现千万级数据量对账;实现高稳定性,高准确率,高效率全连接效率低;不支持订单状态推进实现对账系统的高效率,准确率实现6个9;功能全面

总结


        对账系统模型与业务息息相关,业务不同,对账系统模型也会不同,但是大部分对账系统的整体架构变化不大,主要区别是各个模块的实现方式不同。希望本文介绍的对账系统能为各位读者提供设计思路,避免重复入坑。对对账系统感兴趣的同学可以找财经支付团队同学详聊,一起深入探讨,提出优化建议,比如优化全连接策略,也欢迎各种简历推荐。


参考文章


信息流对账与平台化实现-曾佳


混合编程在财经对账中的应用-王亚宁


内推链接


image.png

收起阅读 »

【镜·映】《烂》:没有反转的生活

增村保造的这部《烂》(Tadare,1962,tt0310199),改编自日本自然主义大家德田秋声的同名小说。 益子(Masuko,若尾文子)在东京与汽车销售员浅井(Asai,田宫二郎)同居一段时间之后,才发现这个男人原有妻子。益子不情愿破坏浅井的婚姻,提议...
继续阅读 »


增村保造的这部《烂》(Tadare,1962,tt0310199),改编自日本自然主义大家德田秋声的同名小说。


益子(Masuko,若尾文子)在东京与汽车销售员浅井(Asai,田宫二郎)同居一段时间之后,才发现这个男人原有妻子。益子不情愿破坏浅井的婚姻,提议分手,但浅井早已经不能忍受神经质的妻子了,只是因为早年接受过妻家的资助,一直未能下决心离婚。被益子发现后,浅井终于离了婚,而前妻也因承受不住打击而精神崩溃。此时益子的侄女英子(Eiko,水谷八重子)不愿与家里安排的对象相亲,从乡下跑到东京,寄宿在姑姑家中。英子向往大城市的生活,却又看不起给人做情妇的姑姑,结果却是自己与浅井勾搭在一起,被益子捉奸在床,怒不可遏的益子将英子逐出家门。而后则强行安排英子与她原本的相亲对象——一个西装革履的农民结了婚。


小三上位,然后又被自己的侄女绿了,好在她“奋起反击”,终于捍卫了所谓“爱情”——看起来,这是一段有些夸张却俗套的故事,然而,故事的结尾,却拍得令人震撼。


一切依传统进行。姑姑拉扯着身着新娘盛装的侄女来到一众亲友面前,“看,她是个好新娘!”





浅井站起来,走到英子面前,面对着这个前两天还和自己享受着最后疯狂的性爱的女子,他挤出一些不多的笑容:“你真美,你太棒了!”,然后转头离开。





在众人面带微笑的审视中,一脸漠然的新人被送上花车,她的姑姑益子就坐在她身边,紧盯着她,像极了押送犯人走向牢笼。





镜头切换,开往名古屋的列车就要出发了。





已经换了便装的英子坐在车厢中,带着幽怨凝视着窗外的浅井——他和益子站在窗外,眉头微蹙,益子则面无表情。





英子的丈夫在模糊的前景中微笑着和他人告别。车站的广播,正一遍遍播放着:“请站在白线以外,请站在白线以外......”





列车徐徐开动,浅井缓缓抬起右手,犹犹豫豫地做了一个告别的手势,似乎还未完成,就缓缓放下了。





益子抬眼凝视着他,轻叹一口气,默然转头,独自离开,送行人群纷纷挥动手臂,背景渐渐模糊,益子面色苍白疲惫,仿佛仍旧难以释怀,而明明一切已然结束,除了当事人,没有人知道姑侄两人曾是情敌,为了争夺一个男人,到了以死相拼的地步。





本来,电影到这里其实可以结束了。然而,接下来的3分多钟,才是见证一位大导演真实功力的时刻。


送走英子,益子和浅井 “像往常一样” 回到“家”,开始了 “像往常一样”“日常时刻”


益子问:“要吃点东西吗?”“要我给你准备洗澡水吗?”“要睡觉了吗?”“我给你泡点热茶吧?”浅井一概说不。





然而他回到卧室,看着曾经和两个女人翻滚过的床,却又觉得空虚。





回到客厅,益子已经泡好了茶。两人开始 “像往常一样”“日常闲谈”





益子说,“她会是一位好妻子”,浅井说,“也许吧”;益子说,“举办婚礼真好”“我们要不要也举办一场婚礼?”,浅井笑笑说,“那也挺好,我们准备一下吧”。然后,就独自回房了。





益子漠然坐着,低下头将茶杯顶在额头。自己的提议并未遭到拒绝,然而,浅井那种怎么都无所谓的回答,却比拒绝还令人难受。





尽管如此,当听到浅井那一句“你怎么还不来?”,她还是反射般地应道 “哈依” 。不想表达内心的苦楚,因为表达了也无意义。她开始一件一件脱去衣服,像往常一样搭在椅子背上,只留下半透的薄纱内衣。





然后,益子慢慢踱向内室,带上了门,屏幕转暗,左下角显示出一个 “终” 字。





还记得《毕业生》(The Graduate,1967)最后的反转吗?曾被女友母亲诱惑的男主最后的时刻鼓起勇气冲入婚礼现场,劫走了即将成为别人新娘的女友,宣告一切错误终结与新生活的开始。


然而,《烂》的结尾,没有反转


一切如常。


一切都过去了,一切都被无形的力量压制在生活的 “日常和谐” 之下,再无声息,只有三个当事人吞咽下那些无法言说的苦楚,不出意外的话,他们将会把这些意难平带进坟墓。而那些不明缘由的关系人与看客,只会知道这是一场完美的婚礼,并为此或真情或假意地抚掌相庆。错误已被终结,但新的生活并未开始。


村上春树说:“我不想找一个搭伙过日子的人,我要找一个一见我就笑,我一见就笑,喝了酒满眼光给我讲浪漫和爱的人。”也许,这是个讽刺。


也许,浅井和益子也曾期待过那样的生活。但经过这小小的插曲,他们的生活似乎又回到了常态,并且很可能那就是他们可能期待的、唯一的生活。


益子看似是这场“宫斗”戏中的胜利者,她挤走了浅井的原配,又逼退了自家的侄女,然而,除了肉体,她不知道还有什么可以留住这个男人——现在连这一点,她都不那么确定了。更为悲哀的是,她似乎只能接受这种生活的“安排”,她可以战胜情敌,却无力摆脱一个社会系统将她锁定的位置。某种程度上,她甚至不如英子这个“失败者”,至少英子曾经痛快地享受过Stolen Pleasure(这是影片的另一个名字)。


如果仅从女性主义的角度解读这部电影,就会忽略一个事实:浅井也不是胜利者。这个男人因为接受过前妻家的资助,娶了他并不爱的女人,觉得自己处处受制于神经质的妻子,好不容易摆脱了,却发现益子的善妒与疯狂,比前妻更甚。在这场荒唐的情爱纠缠中,的确女性受到的损害更甚,但浅井也没办法为所欲为,当益子和英子两位女性疯狂地撕打在一起时,他的惶恐无措说明了一切。他依靠益子摆脱了前妻,现在他必须接受这个可以为了保住自己的位置而试图掐死自己侄女的疯狂的女人。这里没有胜利者,也没有自由人。


并非是男人操纵女人,或者女人操纵男人那么简单,所有人都在被一只看不见的手操弄着。


人一般很难超越对于自己身处其中的社会的既定秩序的理解。 一般人所能做的,就是无意识地压抑,然后再无意识地合理化这种压抑:事实如此,历来如此,所有人都如此。


故事中所有的当事人,都不是脸谱化的坏人,浅井可以为情妇的兄弟慷慨解囊(当时英子还没出现);益子曾经不愿拆散浅井的婚姻,在愤怒地将英子逐出家门之后,又不忍她流落街头。


普通人的普通,也许正在于此,无法摆脱甚至完全意识不到自己就生活在社会话语的规训中,不能知行合一地依照本心行事,却又无法让良知彻底沉默。


这当然不是说人就应当违背公序良俗、像野蛮人一样生活,而是说每个人都应该意识到这些话语权力与生存困境,这种问题意识的觉醒也许会带来痛苦,但却是通向自由意志选择的必经之路。像《烂》中的男男女女一样,本能的情欲化反抗,彼此扯着头发的撕打,保卫虚假爱情的算计,始终都不可能在没有反转的生活里掀起一点点波澜。


作者:wingsay
来源:mdnice.com/writing/df16952233da49c1816ddf3746d1fa84
收起阅读 »

构建写作世界的元素:概念库、词汇库与风格感觉的塑造

对于那些渴望在文字的广袤世界中留下个人思想和观点的人来说,写作无疑是一项必备的技能。噢,老板曾言我写的文字仿若小学生。或许,老板高估了我,我自觉可能还不及个别小孩儿的水平。 为了能够写出简单、清晰、真实的文章,我构想了一个写作工具箱,从下到上约4层内容,从基...
继续阅读 »


对于那些渴望在文字的广袤世界中留下个人思想和观点的人来说,写作无疑是一项必备的技能。噢,老板曾言我写的文字仿若小学生。或许,老板高估了我,我自觉可能还不及个别小孩儿的水平。


为了能够写出简单、清晰、真实的文章,我构想了一个写作工具箱,从下到上约4层内容,从基本素材到风格感觉,从写作方法到写作SOP(Standard Operating Procedure),力求全方位地提升我的写作能力。目前只完成了第1层基本素材,其他层次怎在探索中,哈哈。



第1层 基本素材

第1层 基本素材


第1层,基本素材,正如Stephen King所说,包括概念、词汇和语法,它们是文章的基石,是构建文字世界的灵感火花。这些元素将成为写作中常常使用的工具。


1、概念库


什么是概念呢?概念是对一个事物的清晰定义。我的理解就是概念在说清楚什么是什么。概念之所以重要,就好比你修房子时所用的砖块,虽然微小不起眼,却是构成了我们构建知识和认知大厦的基本单位。


为什么要掌握清晰的、准确的、必要的概念?它们为我们的思考提供了基本框架。明辨式思维,理性决策,都依赖于对概念的准确掌握。


最近学到了一个有趣的例子,让我费曼一下,哈哈。你是否知道「给予」和「付出」之间的区别?


在亲密关系中,「给予」意味着我愿意给你这些东西,做这件事情本身让我很快乐。例如你有喜欢的东西,而我正好能送你,我会因为你的喜欢而高兴;而「付出」则不同,它带有一种期望回报的心态。就是说我给你这个东西,是希望你下一次也要回报给我,并不是因为这个行为本身让我快乐。例如我请你吃顿饭,下次你得回请我才行。


这两个概念反应的是爱的动力机制问题。弗洛姆说:爱是给予,这种给予是抛开计算的,不存在我送你一份东西,你就欠我们一份人情之说。


掌握清晰而准确的概念,可以帮助我们在行动决策时,做出符合自己价值观的事情,认清什么重要,什么更重要。


概念库的构建并不是独自进行的,就像人是社会性动物一样,我们需要与他人互动、交流,形成社区、小团体,与同频的小伙伴一起搭伴前行,才能走的更好一样。


当建立起概念库之后,这些概念与概念之间,有多少清晰的、必要的联系?是接下里要做的事情。不要让它们孤立存在,而是相互联系、相互补充,形成一个有机的整体。


2.词汇库


什么是词汇?词汇≠单词,它是由单词与短语组成的丰富集合体。在构建词汇库时,我们可以放入各种素材,不仅仅局限于单个词汇,也可以是短语、句子甚至一段话。


词汇库是文章的基石,它决定了文章的架构与内涵。虽然名言金句能为文章增色不少,但并不能形成文章的骨架和血肉。因此,在词汇库中,应该注重收集更多事实性知识、概念性知识、作者的重要理论、对话、案例以及那些支持批判性思考的素材。这些资料能够确保你的论证有据可依,避免将个人轶事或经历当作普遍规律,从而在写作时避免陷入无病呻吟和猛灌鸡汤的误区。


如何扩大词汇库?仅仅拿个本子摘抄是不够的。还记得小时候死命摘抄的「金句本」吗?我现在还有一大摞,抄的时候激情澎湃,都是名言佳句;用的时候抓耳挠腮,为啥?不是书到用时方恨少,而是书到用时找不到!这就是纸质版的缺点,无法实现快速检索和有效提取内容,翻半天都找不到想用的那句话写在哪里,气哭自己系列。


因此,扩大词汇库时,不仅要考虑放在在哪里,还要要考虑怎么提取方便。怎么放?放的目的是为了将来能够快速的提取。要实现这个目标,需要做到2点。


首先,将工具电子化,如Obsidian、 iA writer,它们的优势在于能够实现关键词检索、概念互链、工具之间互相转换(例如Obsidian->Anki)。将收集的素材打上标签或拟定标题,这样一键搜索即可轻松找到需要的内容。同时,电子工具的容量也较大,方便随时收藏和携带,让词汇库始终伴随你左右。


其次,建立一个素材收集体系并定期整理。虽然关键词检索能够实现快速查找,但随着时间推移,收集的词汇会不断增多,简单地收集已经无法满足我们的需求。因此,根据主题或兴趣进行分类整理,有助于进行更有系统性的提取。



词汇库-写作主题

词汇库-写作主题


3. 语法库


写作是一门既关乎内容又关乎表达的艺术。语法作为语言的基本规则,通过简洁精炼的表达方式,赋予文字更强大的力量和自信。虽然英文和中文的语法略有不同,但本质上有许多共通之处。


真正的写作应当摒弃造作和恐惧。只有放下恐惧和造作才能写出好东西。要使表达简洁精炼,通常使用「名词+动词」形式,减少不必要的副词修饰和被动语态,这是掌握语言节奏好的方法。过多的副词会使文章显得啰嗦,而被动语态则会让表达显得不够自信。


我写文章老板常说像个小学生写的。有段时间写着写着就不想写了,后来一琢磨吧。嗨,有啥不敢写的呀,遇到问题了就琢磨琢磨。我的问题在于,输入的太少、概念太少、词汇太少,嗯嗯啊啊咿咿呀呀,来来回回就是那几句,而且就那几句还写的非常稚嫩、生涩。怎么办呢?那就继续写吧,不写怎么积累,怎么改进呀,哈哈。


4. 好作品库


史蒂芬·平克在《风格感觉》中说过,成为一个好作者的起点是成为一个好读者。好作者都酷爱阅读,就像Savage一样,他们家没有电视,而满眼都是书,客厅有一排长长的书架、卧室也有一排长长的书架,书很多大概有几千本。王小波也是喜欢读书,文学、哲学、历史、科学什么都有。一个好的读者能够阅读中能够发现、欣赏,并逆向解构好作品,从而获得自己写作所需的技巧。通过阅读好作品,他们逐渐掌握了丰富的概念、词汇和语法,培养出自己独特的风格感觉。分辨和欣赏优秀作品有时比单纯遵循写作技巧更加有效,能从根本上提升写作能力。


什么样的作品是好作品?大师经典。举个例子,如果你想了解关于霸道总裁小说的套路和框架,直接阅读《呼啸山庄》就可以,因为它是该类型小说的原型和经典之作。通过阅读这本书,你可以获取第一手的信息和资料,不需要去翻阅其他杂乱的书籍。这种方式可以让你更加深入地理解和把握这一类型的小说。


有时候,一说到阅读经典,脑子里反射性的就是晦涩、难懂、冗长......我小时候也被“经典”二字吓到了,觉得玩意儿是我能读的,不想读!后来,我的写作水平一直停留在,嗯,小学。翻开几页,多翻几页,会发现阅读⼤师经典能让人更清楚地认识到什么是好,感受到什么是好。以后遇到好的作品,尝试流连不去,沉思它好在哪⾥吧。


如何阅读好作品?对于我们这些基础知识相对欠缺的人来说(没有读过1000本书很难有太多基础知识,参考我老板,读完1000本之后,无论是阅读速度还是阅读质量远远超过我们),如果自己不懂如何阅读经典,最好的方式就是找个老师带着读。你不懂如何提问,那就看好老师如何提问;你不懂如何欣赏,那就看好老师如何解读好作品;你不懂如何提炼技巧,那就看好老师如何拆解作品。跟随老师的步伐,模仿老师,学习老师的方法,你会不断进步,完善自己的技艺是终⽣的事情,将错误看作是进步过程中的一部分,把它们当作游戏的一部分。




小狐狸专区





“本质的东西眼睛是看不见的”,需要用心去感受和理解。小王子喜欢玫瑰,我爱狐狸。


你相信永恒吗?我相信它是存在的,它需要两件法宝才能得以存在:信任和时间。不是一时的,也不是长期如此,而是终生如是。

  • 标题:《小狐狸在讲话》
  • 作者:陈小羊
  • AI copilot:Midjourney
  • Prompt:A cute white fox giving a speech, sunrise, wide view, full body, side view, wearing a blue dressin,the style of riso printing --style raw --ar 137:58
  • 话说:有人说Midjourney生成的图没有灵魂,我咂摸了一下,Prompt是为图注入灵魂的关键呐。


作者:happy_logos
来源:mdnice.com/writing/31230914cb804a91897c3ce3b8bf98cb
收起阅读 »

Flutter-数字切换动画

效果 需求 数字切换时新数字从上往下进入,上个数字从上往下出 新数字进入时下落到位置并带有回弹效果 上个数字及新输入切换时带有透明度和缩放动画 实现 主要采用Animat...
继续阅读 »

效果





需求





  • 数字切换时新数字从上往下进入,上个数字从上往下出



  • 新数字进入时下落到位置并带有回弹效果



  • 上个数字及新输入切换时带有透明度和缩放动画


实现


主要采用AnimatedSwitcher实现需求,代码比较简单,直接撸


import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_xy/widgets/xy_app_bar.dart';

class NumAnimPage extends StatefulWidget {
  const NumAnimPage({super.key});

  @override
  State<NumAnimPage> createState() => _NumAnimPageState();
}

class _NumAnimPageState extends State<NumAnimPage> {
  int _currentNum = 0;

  // 数字文本随机颜色
  Color get _numColor {
    Random random = Random();
    int red = random.nextInt(256);
    int green = random.nextInt(256);
    int blue = random.nextInt(256);
    return Color.fromARGB(255, red, green, blue);
  }

  // 数字累加
  void _addNumber() {
    setState(() {
      _currentNum++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: XYAppBar(
        title: "数字动画",
      ),
      body: Center(
        child: _bodyWidget(),
      ),
    );
  }

  Widget _bodyWidget() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 500),
          transitionBuilder: (Widget child, Animation<double> animation) {
            Offset startOffset = animation.status == AnimationStatus.completed
                ? const Offset(0.0, 1.0)
                : const Offset(0.0, -1.0);
            Offset endOffset = const Offset(0.0, 0.0);
            return SlideTransition(
              position: Tween(begin: startOffset, end: endOffset).animate(
                CurvedAnimation(parent: animation, curve: Curves.bounceOut),
              ),
              child: FadeTransition(
                opacity: Tween(begin: 0.0, end: 1.0).animate(
                  CurvedAnimation(parent: animation, curve: Curves.linear),
                ),
                child: ScaleTransition(
                  scale: Tween(begin: 0.5, end: 1.0).animate(
                    CurvedAnimation(parent: animation, curve: Curves.linear),
                  ),
                  child: child,
                ),
              ),
            );
          },
          child: Text(
            '$_currentNum',
            key: ValueKey<int>(_currentNum),
            style: TextStyle(fontSize: 100, color: _numColor),
          ),
        ),
        const SizedBox(height: 80),
        ElevatedButton(
          onPressed: _addNumber,
          child: const Text(
            '数字动画',
            style: TextStyle(fontSize: 25, color: Colors.white),
          ),
        ),
      ],
    );
  }
}


具体见github:https://github.com/yixiaolunhui/flutter_xy


作者:移动小样
来源:mdnice.com/writing/9645b22a9a54493f9f2e3f74e60d17c7
收起阅读 »

第三方认证中心跳转

一、业务需求 由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。 二、 业务流程 模拟第三方应用 CUSTOM-USERTOKEN 是第三方的 tok...
继续阅读 »

一、业务需求


由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。


二、 业务流程





模拟第三方应用





  • CUSTOM-USERTOKEN 是第三方的 token



  • proxy_pass 是我们的前端地址


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • backend 是后端服务地址



  • 80 是前端代理端口


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

三、处理方式


由于放在 header 中的内容,前端只有从 XHR 请求中才能拿到,所以直接打开页面时,肯定是无法拿到 header 中的 token 的,又因为这个 token 只有从第三方系统中跳转才能携带,所以也无法通过请求当前页面去获取 header 中的内容。


一、通过后端重定向


在 nginx 代理中,第三方请求从原本跳转访问前端的地址==改为==后端地址, 因为后端是可以从请求总直接拿到 header,所以这时由后端去处理 token ,在重定向到前端。





  • 后端可以设置 cookie,前端从 cookie 中获取



  • 后端可以拼接 URL, 前端从 url 中获取



  • 后端可以通过缓存 cookie, 重定向到前端后发请求获取 token


模拟第三方应用





  • 第三方应用由跳转前端改为跳转后端接口


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://backend/token;
}
}

前端静态代理





  • 前端代理不需要做任何处理


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

二、通过 nginx 重定向 URL


在 nginx 代理中,新增一个 /token 的代理地址,用于转发地址,第三方请求从原本跳转访问前端的地址,改为 /token 代理地址 因为 nginx 中是可以获取 header 中的内容的,所以这时由 /token 处理拼接好 url ,在重定向到前端。





模拟第三方应用



  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • 新增 /token 代理,进行拼接 URL 后跳转


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
location /token {
# 将 $http_custom_usertoken 拼接在 URL 中,同时重定向到前端
# 前端通过 location.search 处理 token
rewrite (.+) http://127.0.0.1?token=$http_custom_usertoken;
}
error_page 405 =200 $uri;
}

三、通过 nginx 设置 Cookie


由于通过响应头中设置 Set-Cookie 可以直接存储到浏览器中,所以我们也可以通过直接设置 cookie 的方式处理。





模拟第三方应用





  • 此时第三方应用直接访问前端即可


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • token 设置在 cookie


  server {
listen 80;
server_name localhost;

location / {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

四、nginx 代理转发设置 Cookie


方法 三、通过 nginx 设置 Cookie 中,存在一个问题,由于此时在前端静态代理上添加 cookie,这就会导致所有静态资源都会携带 cookie, 这就会造成 cookie 中因为 path 不同而重复添加, 所以我们还可以通过造一层代理的方式处理这个问题





模拟第三方应用





  • 代理地址再次修改为 token


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • token 设置在 /token 代理地址的 cookie



  • /token 重定向到前端地址


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}

location /token {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
rewrite (.+) http://127.0.0.1;
}
error_page 405 =200 $uri;
}

作者:子洋
来源:mdnice.com/writing/d92f346cc96a43b49fc36c9894add729
收起阅读 »

用Vue.js构建一个Web3应用像,像开发 Web2 一样熟悉

web
作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。 但不用担心!有一种解决方案可以无缝衔接 Web2...
继续阅读 »

作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。


但不用担心!有一种解决方案可以无缝衔接 Web2 和 Web3,它就是 Juno



网址:https://juno.build/



在本篇博文中,我们将探讨如何利用 Vue 和 Juno 的强大功能来开发去中心化应用程序(dApps)。加入我们的旅程,揭开 Juno 的神秘面纱,让您轻松创建非凡的去中心化体验!





导言


在我之前的博文中,我讨论了 React[1]Angular[2] 这两个流行的 JavaScript 前端框架的类似解决方案。如果这两个框架中的任何一个是您的首选,我建议您浏览这些具体的文章,以获得量身定制的见解。


Juno如何工作


如果你还不了解 Juno,它是一个功能强大的开源区块链即服务平台,旨在让去中心化应用程序开发变得更加容易。可以把它想象成一个无服务器平台,类似于谷歌Firebase或AWS Amplify等流行服务,但增加了区块链技术的优势。Juno 完全在区块链上运行您的应用程序,确保完全去中心化和安全的基础设施。


通过利用Internet Computer[3]区块链网络和基础设施,Juno 为您创建的每个应用程序引入了一个名为 “Satellites” 的独特概念。这些 Satellites 作为强大的智能合约,封装了您的整个应用程序,包括 JavaScript、HTML 和图像文件等网络资产,以及简单的数据库、文件存储和身份验证机制。通过 Juno,您可以完全控制应用程序的功能和数据。


构建一个 Dapp


让我们开始构建我们的第一个去中心化应用程序,简称“dapp”。在这个例子中,我们将创建一个笔记应用程序,允许用户存储和检索数据条目,以及上传文件。


本教程和代码示例使用了 Vue Composition API。


初始化


在将 Juno 集成到应用程序之前,需要创建一个 satellite。该过程在文档[4]中有详细的解释。


此外,还需要安装SDK。


npm i @junobuild/core

完成这两个步骤后,您可以在 Vue 应用程序的根目录(例如 App.vue)中使用 satellite ID 初始化 Juno。这将配置库与您的智能合约进行通信。


<script setup lang="ts">
import { onMounted } from 'vue'
import { initJuno } from '@junobuild/core'

onMounted(
  async () =>
    await initJuno({
      satelliteId'pycrs-xiaaa-aaaal-ab6la-cai'
    })
)
</script>

<template>
<h1>Hello World</h1>
</template>

配置完成!现在,您的应用程序已经可以用于 Web3 了!😎


身份验证


为了确保用户身份的安全性和匿名性,需要对用户进行登录和注销。要做到这一点,可以将相关函数绑定到应用程序中任何位置的 call-to-action 按钮。


<script setup lang="ts">
import { signIn, signOut} from '@junobuild/core'
</script>

<button @click="signIn">Sign-in</button>
<button @click="signOut">Sign-out</button>

为了与其他服务建立无缝集成,库和 satellite 组件在用户成功登录后自动在您的智能合约中生成新条目。此功能使库能够在任何数据交换期间有效地验证权限。


为了监视并深入了解该条目,从而访问有关用户状态的信息,Juno提供了一个名为authSubscribe() 的可观察函数。您可以根据需要灵活地多次使用此函数。然而,你也可以创建一个在整个应用中有效传播用户信息的 store。


import { ref, type Ref } from 'vue'
import { defineStore } from 'pinia'
import { authSubscribe, type User } from '@junobuild/core'

export const useAuthStore = defineStore('auth', () => {
  const user: Ref<User | null | undefined> = ref(undefined)

  const unsubscribe = authSubscribe((u) => (user.value = u))

  return { user, unsubscribe }
})

这样,在应用程序的顶层订阅它就变得非常方便。


<script setup lang="ts">
import { useAuthStore } from '../stores/auth.store'
import { storeToRefs } from 'pinia'

const store = useAuthStore()
const { user } = storeToRefs(store)
</script>

<template>
  <template v-if="user !== undefined && user !== null">
    <slot /
>
  </template>

  <template v-else>
    <p>Not signed in.</
p>
  </template>
</
template>

存储文档


Juno提供了一个名为“Datastore”的功能,旨在将数据直接存储在区块链上。Datastore 由一组集合组成,其中每个集合保存文档,这些文档由您选择的键唯一标识。


在本教程中,我们的目标是存储笔记,因此必须按照文档中提供的说明创建一个集合。为集合选择合适的名称,例如“notes”。


一旦设置好应用程序并创建了必要的集合,就可以利用库的 setDoc 函数将数据持久化到区块链上。此功能使您能够安全且不变地存储笔记。


import { setDoc } from "@junobuild/core";

// TypeScript example from the documentation
await setDoc<Example>({
  collection"my_collection_key",
  doc: {
    key"my_document_key",
    data: myExample,
  },
});

由于集合中的文档是通过唯一的密钥来标识的,因此我们使用 nanoid[5] 来创建密钥--这是一种用于 JavaScript 的微型字符串 ID 生成器。


<script lang="ts" setup>
import { ref } from 'vue'
import { setDoc } from '@junobuild/core'
import { nanoid } from 'nanoid'

const inputText = ref('')

const add = async () => {
  const key = nanoid()

  await setDoc({
    collection'notes',
    doc: {
      key,
      data: {
        text: inputText.value
      }
    }
  })
}
</script>

<template>
  <textarea rows="5" placeholder="Your diary entry" 
            v-model="inputText">
</textarea>

  <button @click="add">Add</button>
</template>


请注意,为简单起见,本教程提供的代码片段不包括适当的错误处理,也不包括复杂的表单处理。



检索文档列表


为了检索存储在区块链上的文档集合,我们可以使用库的 listDocs 函数。这个多功能函数允许加入各种参数,以方便数据过滤、排序或分页。


出于本教程的目的,我们将保持示例的简约性。我们的目标是在挂载组件时简单地列出所有用户数据。


<script lang="ts" setup>
import { listDocs } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const items = ref([])

const list = async () => {
  const { items: data } = await listDocs({
    collection'notes'
  })

  items.value = data
}

onMounted(async () => await list())
</script>

<template>
  <p v-for="(item, index) in items">
    <span>
      {{ index + 1 }}
    </span>
    <span>{{ item.data.text }}</span>
  </p>
</template>

文件上传


在去中心化网络上存储数据是一项复杂的任务。然而,Juno 的设计旨在为需要轻松存储和检索用户生成内容(如照片或文件)的应用程序开发人员简化这一过程。


在处理文档时,第一步是按照文档[6]中提供的说明创建一个集合。在本教程中,我们将重点实施图片上传,因此该集合可以恰当地命名为 “images”。


为确保存储数据的唯一性和正确识别,每个文件都有唯一的文件名和路径。这一点非常重要,因为数据是在网络上提供的,每条数据都应该有一个独特的 URL。


要实现这一点,我们可以使用用户唯一ID的文本表示形式和每个上传文件的时间戳的组合来创建一个键。通过访问我们之前在存储中声明的属性,我们可以检索相应的用户键。


<script lang="ts" setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth.store'
import { storeToRefs } from 'pinia'
import { uploadFile } from '@junobuild/core'

const file = ref(undefined)

const store = useAuthStore()
const { user } = storeToRefs(store)

const setFile = (f) => (file.value = f)

const upload = async () => {
  // Demo purpose therefore edge case not properly handled
  if ([nullundefined].includes(user.value)) {
    return
  }

  const filename = `${user.value.key}-${file.value.name}`

  const { downloadUrl } = await uploadFile({
    collection'images',
    data: file.value,
    filename
  })

  console.log('Uploaded', downloadUrl)
}
</script>

<template>
  <input type="file" @change="(event) => setFile(event.target.files?.[0])" />

  <button @click="upload">Upload</button>
</template>

一旦一个资源被上传,一个 downloadUrl 返回,它提供了一个直接的 HTTPS 链接,可以在web上访问上传的资源。


列出资源


为了检索存储在区块链上的资产集合,我们可以利用库提供的 listAssets 函数。这个函数在参数方面提供了灵活性,允许我们根据需要对文件进行过滤、排序或分页。


与前面的文档示例类似,我们将保持这个示例的简约性。


<script lang="ts" setup>
import { listAssets } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const assets = ref([])

const list = async () => {
  const { assets: images } = await listAssets({
    collection'images'
  })

  assets.value = images
}

onMounted(async () => await list())
</script>

<template>
  <img loading="lazy" :src="asset.downloadUrl" v-for="asset in assets" />
</template>

部署 🚀


在开发和构建应用程序之后,下一步是将其部署到区块链上。为此,您需要在终端中执行以下命令来安装 Juno 命令行接口(CLI):


npm i -g @junobuild/cli

安装过程完成后,您可以按照文档[7]中的说明并从终端登录来访问您的 satellite。这将使你的机器能够控制你的 satellite。


juno login

最后,您可以使用以下命令部署项目:


juno deploy

恭喜你!您的 Vue dapp 现在已经上线,并完全由区块链提供支持。🎉


资源





原文:https://betterprogramming.pub/build-a-web3-app-with-vuejs-db1503ca20d2


是哒是哒说


参考资料


[1]

React: https://betterprogramming.pub/build-a-web3-app-with-react-js-6353825baf9a

[2]

Angular: https://levelup.gitconnected.com/develop-an-angular-app-on-blockchain-9cde44ae00b7

[3]

Internet Computer: https://internetcomputer.org/

[4]

文档: https://juno.build/docs/add-juno-to-an-app/create-a-satellite

[5]

nanoid: https://github.com/ai/nanoid

[6]

文档: https://juno.build/docs/build/storage#collections-and-rules

[7]

文档: https://juno.build/docs/miscellaneous/cli#login

[8]

https://juno.build/docs/intro: https://juno.build/docs/intro

[9]

GitHub 代码库: https://github.com/buildwithjuno/examples/tree/main/vue/diary



作者:程序员张张
来源:mdnice.com/writing/26615feb73924bb4821f543e0f041fa4
收起阅读 »

前端开发如何给自己定位?初级?中级?高级!

web
引言 在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前...
继续阅读 »

引言


在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端面试指南,P6/P6+/P7的能力标准。


目录



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

1.熟练掌握JavaScript。

2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

4.熟练掌握react生态常用工具,redux/react-router等。

5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。


初级:



  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。

  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。

  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。

  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。

  • 知道raf和其他达到60fps的方法。


中级:



  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。

  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。

  • 知道怎样在移动端处理加载问题,渲染性能问题。

  • 知道如何结合native能力优化性能。

  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。


高级:



  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。

  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。

  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。


1.熟练掌握JavaScript。


初级:



  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:

  • 知道组合寄生继承,知道class继承。

  • 知道怎么创建类function + class。

  • 知道闭包在实际场景中怎么用,常见的坑。

  • 知道模块是什么,怎么用。

  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。

  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。

  • 知道ES6数组相关方法,比如forEach,map,reduce。


中级:



  • 知道class继承与组合寄生继承的差别,并能举例说明。

  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。

  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。

  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。

  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。


2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。


初级:



  • 知道webpack,rollup以及他们适用的场景。

  • 知道webpack v4和v3的区别。

  • 脱口而出webpack基础配置。

  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。

  • 知道amd,cmd,commonjs,es module分别是什么。

  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。


中级:



  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。

  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。

  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。

  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。


高级:



  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:

  • 项目脚手架搭建,及如何以工具形态共享。

  • 团队eslint规范如何设计,及如何统一更新。

  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。

  • 客户端缓存及预加载方案。


3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。


初级:



  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。

  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。

  • 知道diff算法大致实现思路。

  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。

  • 以上几点react替换为vue或angular同样适用。


中级:



  • 能说明白为什么要实现fiber,以及可能带来的坑。

  • 能说明白为什么要实现hook。

  • 能说明白为什么要用immutable,以及用或者不用的考虑。

  • 知道react不常用的特性,比如context,portal。

  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。


高级:



  • 能设计出框架无关的技术架构。包括但不限于:

  • 说明如何解决可能存在的冲突问题,需要结合实际案例。

  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。


4.熟练掌握react生态常用工具,redux/react-router等。


初级:



  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。

  • 知道vue和angular对应全家桶分别有哪些。

  • 知道浏览器react相关插件有什么,怎么用。

  • 知道react-router v3/v4的差异。

  • 知道antd组件化设计思路。

  • 知道thunk干嘛用的,怎么实现的。


中级:



  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。

  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。

  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。


高级:



  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决


5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。


初级:



  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。

  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。

  • 知道axios或同级别网络请求库,知道axios的核心功能。

  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;

  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;


中级:



  • HTML方面能够结合各个浏览器api描述常用类库的实现。

  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。

  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。

  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。

  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。

  • 知道oauth2.0轻量与完整实现原理。

  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。


高级:



  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。

  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。

  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。

  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。


6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。


初级:



  • 知道eslint,以及如何与工程配合使用。

  • 了解近3年前端较重要的更新事件。

  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。

  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。


高级:



  • 在团队内推行eslint,并给出工程化解决方案。

  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。


7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



  • 根据了解的深度分初/中/高级。

  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。

  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。

  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。


5. 结论与进一步学习


本文为前端开发人员提供了一个能力定位指南,帮助他们了解自己在前端领域的定位,并提供了具体的代码示例来巩固学习成果。通过不断学习和实践,前端开发人员可以逐步提升自己的能力,从初级到中级再到高级。但请注意,在实际工作中,不同公司和项目对于各个级别的要求可能会有所不同。


为了进一步提高自己的水平,前端开发人员可以考虑以下学习路径和资源:



  • 阅读官方文档和教程,如MDN、React官方文档等;

  • 参与开源项目,并与其他开发人员进行交流和合作;

  • 关注前端开发的博客和社区,如Medium、Stack Overflow等;

  • 参加在线或线下的前端开发培训课程;

  • 阅读经典的前端开发书籍,如《JavaScript高级程序设计》、《CSS权威指南》等。


通过持续学习和实践,相信每个前端开发人员都可以不断成长,并在前端领域中取得更好的成就。祝愿大家在前端开

作者:Jony_men
来源:juejin.cn/post/7259961208794628151
发的道路上越走越远!

收起阅读 »

该写好代码吗?我也迷茫了

我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。 他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。 其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。 程序员内部,曾经流传着这样几句圣经: 代码写的好,写得快,会像...
继续阅读 »

2023-04-21_172756.png


我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。


他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。


其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。


程序员内部,曾经流传着这样几句圣经:



代码写的好,写得快,会像个闲人。代码有注释,逻辑清晰,任何人都能轻松取代你。


代码写的烂,只有自己能看懂,一次次救火,你反而会成为团队不可缺少的人才。



那么,问题来了:到底该把事情干好呢,还是不要干好呢?


这是一个问题吗?当然是往多快好省了做呀!


我以前的想法就是这样。


我做底层员工时,代码写的清晰简洁,高效严谨。有时候我会因为计算循环次数而费心设计。如果循环层数太多,我会先把关键数据放到Map里,后续可以直接取用。我也会关注代码的可读性,尽量少套几层循环,命名兼顾字符长度和表意指向,如果代码太多就抽离成一个方法函数,并且要在代码里注释清楚。而这些操作,在形成习惯之后,是不会影响开发效率的。反而在某些情况下,还会提高效率。因为不管逻辑多复杂,不管过去多久,一看就能懂,很容易排查问题和他人接手。


我做中层管理时,除了培养团队内每个员工都能做到上述标准外,让我投入很大精力的事情就是“去我化”。也就是通过手段、流程、文化做到团队自治。我在团队时,大家能很高效地完成工作。当我短时间内离开时,大家依然能依靠惯性维持高效的状态。


我的想法很单纯:不管我是一线开发,还是中层管理,我修炼的都是自己。当你具备一定的职场能力时,你就是值钱的。作为员工你能把手头的活干得又快又好,作为管理你能把团队管理得积极健康。这就是亮点。不要在意别人的看法,你只需要修炼自己。当你具备了这个能力,这里不适合你,好多地方都会求此类贤人若渴。


其实,后面慢慢发现,这种想法可能还值得商榷。


因为创业和打工的区别还是挺大的。


创业是给自己干,想干好是肯定的,谁都不愿意面对一团糟。


我看明朝的历史,建文帝朱允炆刚登基时,就想削弱其他藩王的势力,加强自己的权力。当建文帝打算办燕王朱棣时,朱棣就起兵造反,自己做了皇帝。我感觉朱棣其实是自卫。后来,感情朱棣当上皇帝的第一件事,也是继续削弱藩王的势力。其实,大家都一样。


打工就不一样了,干得好不好,不是你说了算,是你的上级领导说了算,周围同事说了算,规章制度说了算。


因此,扁鹊三兄弟的现象就出现了。


扁鹊大哥,医术最高,能预防病人生病。扁鹊二哥,医术很高,能消灭病症在萌芽阶段。到扁鹊这里,只能到人快死了,开刀扎针,救人于生死之间。但是,世人都称扁鹊为神医。


如果你的领导是一个技术型的,同时他还能对你的工作质量做一些审查,那么他对你的评价,可能还具有些客观性。


但是,如果你碰到的是一个行政型的领导,他不是很懂医术,那他就只能像看医生治病一样,觉得救治好了快要死的人,才是高人。而对于预防重症这类防患于未然的事情,他会觉得你在愚弄他。


事实上,不少中小企业的领导,多是行政型领导。他们常常以忠心于老板而被提拔。


因此,为了自己获得一些利益。有些人常常是先把大病整出来,然后再治好,以此来体现自己的价值。


老维修工建设管道,水管里流的是燃气,燃气管的作用是排废水。出了问题,来一批一批的新师傅,都解决不了,越弄越乱。结果老维修工一出手,就把问题解决了。老板觉得,哎呀,看,还得是我的老师傅管用。


相反,如果你把事情打理的井井有条,没有一丝风浪,就像扁鹊大哥一样,我都不得病,还养你干啥,随便换谁都可以。这样的话,往往你的结局就多是被领导忽视。


我有好几个大领导都在大会上说过:你请假一周,你的部门连给你打一个电话的都没有,这说明你平时疏于管理,对于团队没有一丝作用!


从业这么多年,见过各种现实,很讽刺,就像是笑话。有一个同事,给程序加了一个30秒后延时执行。后来,领导让他优化速度,他分4次,将30秒调到5秒。最后领导大喜,速度提高6倍,他被授予“超级工匠”的荣誉称号。


一个是领导的评价。还有一个是同事的评价。


我有一次,在自己的项目组里搞了个考核。考核的核心就是,干好了可以奖,干差了便会罚。我觉得这样挺好,避免伤了好人心,杜绝隧了闲人的意。结果因为其他项目组没有搞,所以我成了众矢之的。他为什么要搞?人家项目组都没有,就他多事,咱们不在他这里干了!


我想,国内的管理可能不是一种客观的结果制。而是另一种客观的”平衡制“。


就像是古代的科举,按照才学,按照成绩来说,状元每届多是江南的。但是,皇帝需要平衡,山西好久没有出个状元了,点一个吧。河南今年学子闹事,罢考,为了稳一稳人心,给一个吧。江南都这么多了,少一个没什么关系的。


他更多是要让各方都满意。一个”平衡“贯穿了整个古今现代的价值观。


有人遵循自己的内心做事,也有人遵循别人的内心做事。不管遵循哪一方,坚持就好,不要去轻易比较,各自有各自的付出和收获。


当路上都在逆行时,你会发

作者:TF男孩
来源:juejin.cn/post/7224764099187966010
现,其实是你在逆行。

收起阅读 »

给同学解决问题有感——天下前端是一家!

web
   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~    最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时...
继续阅读 »

   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~


   最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时学到的前端思想,还是成功的解决了这些问题,有感而发,记录下来~



  •    第一位出场的是正在自学python的学习委员,也是一位准研究生。他遇到的问题是,在a项目里定义了一个复杂界面,在b项目里定义了一个简单页面。他找到我的时候说,启动了b项目,但打开的却是a项目定义的页面。报错如下:


d60d9f2414f5a2867091a5a14db6e54.png
    看了他的页面,这路由和我学的不长得一毛一样!


8bb4fc7bab1580d1baa6692d0f2b801.png
  打开他的浏览器页面,看看页面的网络请求,404,我第一反应会不会是他路由的问题,导致找不到这个页面,显示了之前项目的界面。。。


dc111956dfdb5bc954fd74c3bfdea11.png
  但我转念一想,404 应该不会显示另一个项目的界面呀,除非请求的是之前项目的服务器。再注意到warning中的,use a server instead,这不就是换一个服务器,那一定是之前的端口被占用了,所以相当于请求之前的服务器。于是,搬出来我只会一个cd的小黑窗:


1684658842987.png
   解决占用之后,重启项目,完整的展示了新项目中的页面~
这个问题准确的说属于计网,但前后端的思想还是在里面。果然还是基础的东西~



  • 第二位出场的是一位在做安卓应用(毕设项目)的女同学,躺在床上收到她的连环问,导航栏隐藏?我直接惊坐起:


1684659076469.png
   仔细听了她的描述之后,在有了导航栏之后,页面某些按钮的位置发生了偏差,如下图:


4637c8740c325359481bc6fb139af42.png
  原本卡其色圆圈应该和下面蓝色圆圈重合,通关之后,显示下面的颜色。
因为下面一层的按钮是嵌套的背景图片里,所以不知道固定位置,不能用绝对定位控制两个按钮的位置完全重合(她是用可视化拖动的方法做的页面)。
让导航栏透明肯定是有办法的,但我想如果只透明 但仍然占据文档流,那还是没用呀!


1684659743090.png
  找到网友的方法,尝试之后,模拟机显示确实ok,隐藏了导航栏,位置也消除了偏差,在鼠标接触到导航栏位置时,导航栏显现,很人性化!但她在手机上通过apk安装包查看,还是有一定的偏差,我想是因为屏幕尺寸的问题吗?看了他的代码用的都是相对单位dp,应该可以自适应的呀,这个就不懂了,毕竟适配所有机型的问题,我在实习的时候也很头疼!




PS:
在此应该鸣谢一下我的老师和队友,坚定让我自己做了前后端分离的一个项目,自己建数据库,自己写接口,对前后端请求的
作者:MindedMier
来源:juejin.cn/post/7235458133505491005
发送接收还是有更细致的了解!
收起阅读 »

Kotlin 密封接口sealed interface

什么是密封接口?密封接口(sealed interface)是kotlin 1.5引入的一个新特性,它可以让我们定义一个限制性的类层次结构,也就是说,我们可以在编译时就知道一个密封接口有哪些可能的子类型。这样,我们就可以更好地控制继承关系,避免出现意外的子类型...
继续阅读 »

什么是密封接口?

密封接口(sealed interface)是kotlin 1.5引入的一个新特性,它可以让我们定义一个限制性的类层次结构,也就是说,我们可以在编译时就知道一个密封接口有哪些可能的子类型。这样,我们就可以更好地控制继承关系,避免出现意外的子类型。

密封接口与密封类(sealed class)类似,都可以用来表示一组有限的可能性。但是,密封类只能有一个实例,而密封接口的子类型可以有多个实例。此外,密封类只能被类继承,而密封接口可以被类和枚举类(enum class)实现。

要声明一个密封接口,我们需要在interface关键字前加上sealed修饰符:

sealed interface Error // 密封接口

一个密封接口可以有抽象或默认实现的方法,也可以有属性:

sealed interface Shape { // 密封接口
val area: Double // 属性
fun draw() // 抽象方法
fun printArea() { // 默认实现方法
println("The area is $area")
}
}

密封接口的优点

使用密封接口有以下几个优点:

  • 类型安全:由于密封接口的子类型是固定的,我们可以在编译时就检查是否覆盖了所有可能的情况。这样,我们就不会遗漏某些分支或者处理错误的类型。
  • 可读性:使用密封接口可以让我们清楚地看到一个类型有哪些变种。这样,我们就可以更容易地理解和维护代码。
  • 灵活性:使用密封接口可以让我们定义更多样化的子类型。我们可以使用数据类(data class),对象(object),普通类(class),或者另一个密封类(sealed class)作为子类型。我们还可以在不同的文件或模块中定义子类型。
  • 表达力:使用密封接口可以让我们利用多态(polymorphism)和继承(inheritance)来实现更复杂和优雅的设计模式。

密封接口在设计模式中的应用

设计模式是一些经过验证和总结的解决特定问题的代码结构和技巧。使用设计模式可以让我们编写出更高效,更可复用,更易扩展的代码。

下面,我们将介绍几种常见的设计模式,并展示如何使用密封接口来实现它们。

策略模式

策略模式(Strategy Pattern)是一种行为型设计模式,它可以让我们在运行时根据不同的情况选择不同的算法或策略。这样,我们就可以将算法的定义和使用分离,提高代码的灵活性和可维护性。

要实现策略模式,我们可以使用密封接口来定义一个策略的抽象,然后使用不同的子类型来实现具体的策略。例如,我们可以定义一个排序策略的密封接口,然后使用不同的排序算法作为子类型:

sealed interface SortStrategy { // 密封接口
fun sort(list: List<Int>): List<Int> // 抽象方法
}

object BubbleSort : SortStrategy { // 对象
override fun sort(list: List<Int>): List<Int> {
// 实现冒泡排序
}
}

object QuickSort : SortStrategy { // 对象
override fun sort(list: List<Int>): List<Int> {
// 实现快速排序
}
}

object MergeSort : SortStrategy { // 对象
override fun sort(list: List<Int>): List<Int> {
// 实现归并排序
}
}

然后,我们可以定义一个上下文类(Context Class),它可以持有一个策略的引用,并根据需要切换不同的策略:

class Sorter(var strategy: SortStrategy) { // 上下文类
fun sort(list: List<Int>): List<Int> {
return strategy.sort(list) // 调用策略的方法
}
}

最后,我们可以在客户端代码中使用上下文类来执行不同的策略:

fun main() {
val list = listOf(5, 3, 7, 1, 9)
val sorter = Sorter(BubbleSort) // 创建上下文类,并指定初始策略
println(sorter.sort(list)) // 使用冒泡排序
sorter.strategy = QuickSort // 切换策略
println(sorter.sort(list)) // 使用快速排序
sorter.strategy = MergeSort // 切换策略
println(sorter.sort(list)) // 使用归并排序
}

使用密封接口实现策略模式的优点是:

  • 我们可以在编译时就知道有哪些可用的策略,避免出现无效或未知的策略。
  • 我们可以使用数据类,对象,普通类或密封类作为子类型,根据不同的策略需要定义不同的属性和方法。
  • 我们可以在不同的文件或模块中定义子类型,提高代码的模块化和可读性。

如果使用java实现策略模式,我们可能需要定义一个接口来表示策略,然后使用不同的类来实现接口:

interface SortStrategy { // 接口
List<Integer> sort(List<Integer> list); // 抽象方法
}

class BubbleSort implements SortStrategy { // 类
@Override
public List<Integer> sort(List<Integer> list) {
// 实现冒泡排序
}
}

class QuickSort implements SortStrategy { // 类
@Override
public List<Integer> sort(List<Integer> list) {
// 实现快速排序
}
}

class MergeSort implements SortStrategy { // 类
@Override
public List<Integer> sort(List<Integer> list) {
// 实现归并排序
}
}

然后,我们也需要定义一个上下文类来持有和切换策略:

class Sorter { // 上下文类
private SortStrategy strategy; // 策略引用

public Sorter(SortStrategy strategy) { // 构造函数
this.strategy = strategy;
}

public void setStrategy(SortStrategy strategy) { // 设置策略方法
this.strategy = strategy;
}

public List<Integer> sort(List<Integer> list) {
return strategy.sort(list); // 调用策略的方法
}
}

最后,我们也可以在客户端代码中使用上下文类来执行不同的策略:

public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 3, 7, 1, 9);
Sorter sorter = new Sorter(new BubbleSort()); // 创建上下文类,并指定初始策略
System.out.println(sorter.sort(list)); // 使用冒泡排序
sorter.setStrategy(new QuickSort()); // 切换策略
System.out.println(sorter.sort(list)); // 使用快速排序
sorter.setStrategy(new MergeSort()); // 切换策略
System.out.println(sorter.sort(list)); // 使用归并排序
}

使用java实现策略模式的缺点是:

  • 我们不能在编译时就知道有哪些可用的策略,因为任何类都可以实现接口。
  • 我们只能使用类作为子类型,不能使用数据类或对象。
  • 我们必须在同一个包中定义子类型,不能在不同的文件或模块中。

访问者模式

访问者模式(Visitor Pattern)是一种行为型设计模式,它可以让我们在不修改原有类结构的情况下,为类添加新的操作或功能。这样,我们就可以将数据结构和操作分离,提高代码的扩展性和复用性。

要实现访问者模式,我们可以使用密封接口来定义一个元素(Element)的抽象,然后使用不同的子类型来实现具体的元素。例如,我们可以定义一个表达式(Expression)的密封接口,然后使用不同的子类型来表示不同的表达式:

sealed interface Expression { // 密封接口
fun accept(visitor: Visitor): Any // 抽象方法,接受访问者
}

data class Number(val value: Int) : Expression { // 数据类
override fun accept(visitor: Visitor): Any {
return visitor.visitNumber(this) // 调用访问者的方法
}
}

data class Sum(val left: Expression, val right: Expression) : Expression { // 数据类
override fun accept(visitor: Visitor): Any {
return visitor.visitSum(this) // 调用访问者的方法
}
}

data class Product(val left: Expression, val right: Expression) : Expression { // 数据类
override fun accept(visitor: Visitor): Any {
return visitor.visitProduct(this) // 调用访问者的方法
}
}

然后,我们可以定义一个访问者(Visitor)的接口,它可以为每种元素提供一个访问方法:

interface Visitor { // 访问者接口
fun visitNumber(number: Number): Any // 访问数字表达式
fun visitSum(sum: Sum): Any // 访问加法表达式
fun visitProduct(product: Product): Any // 访问乘法表达式
}

最后,我们可以定义不同的访问者实现类,它们可以为元素提供不同的操作或功能。例如,我们可以定义一个求值(Evaluate)访问者,它可以计算表达式的值:

class Evaluate : Visitor { // 求值访问者
override fun visitNumber(number: Number): Any {
return number.value // 返回数字本身
}

override fun visitSum(sum: Sum): Any {
return (sum.left.accept(this) as Int) + (sum.right.accept(this) as Int) // 返回左右子表达式之和
}

override fun visitProduct(product: Product): Any {
return (product.left.accept(this) as Int) * (product.right.accept(this) as Int) // 返回左右子表达式之积
}
}

我们还可以定义一个打印(Print)访问者,它可以打印表达式的字符串表示:

class Print : Visitor { // 打印访问者
override fun visitNumber(number: Number): Any {
return number.value.toString() // 返回数字的字符串
}

override fun visitSum(sum: Sum): Any {
return "(${sum.left.accept(this)}) + (${sum.right.accept(this)})" // 返回加法的字符串
}

override fun visitProduct(product: Product): Any {
return "(${product.left.accept(this)}) * (${product.right.accept(this)})" // 返回乘法的字符串
}
}

使用密封接口实现访问者模式的优点是:

  • 我们可以在编译时就知道有哪些可用的元素,避免出现无效或未知的元素。
  • 我们可以使用数据类,对象,普通类或密封类作为子类型,根据不同的元素需要定义不同的属性和方法。
  • 我们可以在不同的文件或模块中定义子类型,提高代码的模块化和可读性。
  • 我们可以在不修改元素类的情况下,为它们添加新的访问者和操作。

如果使用java实现访问者模式,我们可能需要定义一个抽象类来表示元素,然后使用不同的子类来继承元素:

abstract class Expression { // 抽象类
public abstract Object accept(Visitor visitor); // 抽象方法,接受访问者
}

class Number extends Expression { // 子类
private int value; // 属性

public Number(int value) { // 构造函数
this.value = value;
}

public int getValue() { // 获取属性值方法
return value;
}

@Override
public Object accept(Visitor visitor) {
return visitor.visitNumber(this); // 调用访问者的方法
}
}

class Sum extends Expression { // 子类
private Expression left; // 属性
private Expression right; // 属性

public Sum(Expression left, Expression right) { // 构造函数
this.left = left;
this.right = right;
}

public Expression getLeft() { // 获取属性值方法
return left;
}

public Expression getRight() { // 获取属性值方法
return right;
}

@Override
public Object accept(Visitor visitor) {
return visitor.visitSum(this); // 调用访问者的方法
}
}

class Product extends Expression { // 子类
private Expression left; // 属性
private Expression right; // 属性

public Product(Expression left, Expression right) { // 构造函数
this.left = left;
this.right = right;
}

public Expression getLeft() { // 获取属性值方法
return left;
}

public Expression getRight() { // 获取属性值方法
return right;
}

@Override
public Object accept(Visitor visitor) {
return visitor.visitProduct(this); // 调用访问者的方法
}
}

作者:淘淘养乐多
链接:https://juejin.cn/post/7259964169846538297
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Java Map 所有的值转为String类型

可以使用 Java 8 中的 Map.replaceAll() 方法将所有的值转为 String 类型:Map<String, Object> map = new HashMap<>(); // 添加一些键值对 ma...
继续阅读 »

可以使用 Java 8 中的 Map.replaceAll() 方法将所有的值转为 String 类型:

Map<String, Object> map = new HashMap<>();
// 添加一些键值对
map.put("key1", 123);
map.put("key2", true);
map.put("key3", new Date());

// 将所有的值转为 String 类型
map.replaceAll((k, v) -> String.valueOf(v));

上面的代码会将 map 中所有的值都转为 String 类型。


HashMap 是 Java 中使用最广泛的集合类之一,它是一种非常快速的键值对存储方式,可以用于存储和访问大量的数据。下面介绍一些 HashMap 的常用方法:

  1. put(key, value) :向 HashMap 中添加一个键值对。
HashMap<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
  1. get(key) :根据键取出对应的值。
Integer value = map.get("apple");
  1. containsKey(key) :判断 HashMap 中是否包含指定键。
if (map.containsKey("apple")) {
// ...
}
  1. containsValue(value) :判断 HashMap 中是否包含指定值。
if (map.containsValue(1)) {
// ...
}
  1. remove(key) :根据键删除 HashMap 中的一个键值对。
map.remove("apple");
  1. keySet() :返回 HashMap 中所有键的集合。
Set<String> keys = map.keySet();
  1. values() :返回 HashMap 中所有值的集合。
Collection<Integer> values = map.values();
  1. entrySet() :返回 HashMap 中所有键值对的集合。
Set<Map.Entry<String, Integer>> entries = map.entrySet();

以上是常用的 HashMap 方法,还有其他一些方法可以查阅相关文档获得更多信息。


HashMap 的存储原理主要是基于 Hash 算法和数组实现的。 在 HashMap 中,每个键值对对应一个数组中的一个元素,这个元素叫做“桶(bucket)”或“槽(slot)”。

数组的索引值就是通过 Hash 算法计算出来的,每个桶中存放的是一个链表,存储了 key-value 对。如果不同的键值对计算出来的索引值相同,则这些键值对会被放到同一个桶中,以链表的形式存储在该桶中,这就是 HashMap 的解决冲突的方法。

HashMap 的存储过程如下:

  1. 当使用 put 方法将一个键值对添加到 HashMap 中时,首先会根据键的 hashCode 值计算出数组索引位置。具体方法是,将 hashCode 值进行一些运算,得到一个数组索引值。这个索引值是键值对在数组中的位置。
  2. 如果数组中该位置为空,那么就可以直接将键值对存储在该位置,完成添加操作。
  3. 如果该位置已经有了键值对,那么就需要通过比较键的 equals 方法,来判断是更新该键值对的值,还是添加一个新的键值对。
  4. 如果表示键值对的链表长度较长,就会影响到 HashMap 的性能,因为在查找时可能需要遍历整个链表。

为此,Java 8 引入了“红黑树”(Red-Black Tree) 的数据结构,可以将链表转换为树,以提高性能。 需要注意的是,HashMap 是非线程安全的,如果在多线程环境下使用,可能会发生一些异常情况。如果需要在多线程环境中使用 HashMap,可以使用 ConcurrentHashMap 或 Collections.synchronizedMap 方法来实现线程安全。


作者:早起的年轻人
链接:https://juejin.cn/post/7228473633508048951
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用户被盗号?你肯定缺少这些设计

前言在之前的文章【你的登录接口真的安全吗?】中,我们在用户登录安全方面做了很多设计,就是保护用户的账号安全,但是!!我相信做过用户体系的开发或产品都知道,用户的密码泄漏是一个不可避免的事件,总会有用户因为各种奇奇怪怪的原因而导致账号被盗,进而导致用户信息泄漏、...
继续阅读 »

前言

在之前的文章【你的登录接口真的安全吗?】中,我们在用户登录安全方面做了很多设计,就是保护用户的账号安全,但是!!我相信做过用户体系的开发或产品都知道,用户的密码泄漏是一个不可避免的事件,总会有用户因为各种奇奇怪怪的原因而导致账号被盗,进而导致用户信息泄漏、虚拟数据丢失、经济损失等各种后果。那针对这种场景,我们可以通过什么手段来尽量预防呢?

几种实现方式来判断用户登录环境

一般这种情况,业界最简单的处理方式就是识别用户登录环境是是否正常:比如是否是常登录IP、或者是常登录设备等,如果不是,那么则进行限制、二次验证、用户告警等操作。

异地登录

首先是用户异地登录,一般场景下,用户的使用环境大部分时间都是不怎么变化的,比如公司、家里、宿舍或学校等。那么我们可以基于用户的使用IP,来做风险管理。

伪代码实现

   def login(username, password, ip):
# 登录失败,简化其它流程
if not do_login(username, password):
return Result(100, '登录失败')

# 检查用户登录环境异常
if !check_login_env(username, ip):
# 发送短信或邮件给用户,告知用户账号在非常用地登录
send_notice(username, ip)
# 前端收到这个状态码后跳转到二次验证页面
return Result(101, '非常登录地登录')

# 登录成功,异步记录当前ip
async_log_ip(username, ip)
return Result(0, '登录成功')

流程很简单,用户登录成功后,进行一次环境校验,判断用户当前登录ip是否为常登录地,如果不是,那么先给用户发送邮件或短信通知,然后返回对应状态码给前端,跳转到二次校验的页面。

二次校验可以通过APP扫码、手机验证码等方式登录。

*上面的代码中还缺少很重要的一步操作,怎么判断用户的IP是否是常登录IP?我们可以基于IP来实现,但是我们现在家用网络的IP基本上都不是固定IP,所以实际场景下可能更多的是使用地区来判断,比如城市。

伪代码实现

   def check_login_env(username, ip):
cur_city = get_city_from_ip(ip)
# 此城市是否在近半年的常登录城市中
city = get_last_half_year_cities(username)
return cur_city == city


def log_env(username, ip):
city = get_city_from_ip(ip)
# 记录当前登录城市
insert_login_ip(username, city, ip, datetime.now())
# 统计常登录城市
# 查询的近半年登录次数最多的城市
city = select_max_login_city_last_half_year()
# 设置常登录城市
set_last_half_year_cities(username, city)

上面只是简单的实现,整个判断过程比较粗糙,准确性也不够高。实际产生中,我们可以根据ip位置,结合算法来计算常登录地;或者结合下面的其它方式共同判断。

非常用设备登录

除了通过ip来判断用户使用环境外,我们一般还会结合用户设备来判断,特别是移动端应用,用户设备大部分时候是固定不变的。
设备信息一般可以使用设备指纹的方式,通过采集设备的各种信息,生成一个唯一标识,用于标识此设备。如果设备指纹不存在,那么证明用户在新设备上登录,则进行二次验证。

伪代码实现

   def login(username, password, ip, device_info):
# 登录失败,简化其它流程
if not do_login(username, password):
return Result(100, '登录失败')

# 检查用户登录环境异常
if !check_login_env(username, ip):
# 发送短信或邮件给用户,告知用户账号在非常用地登录
send_notice(username, ip)
# 前端收到这个状态码后跳转到二次验证页面
return Result(101, '非常登录地登录')

if !check_device_env(username, device_info):
send_notice(username, device_info)
# 前端收到这个状态码后跳转到二次验证页面
return Result(102, '正在使用新设备登录')

# 登录成功,异步记录当前ip
async_log_ip(username, ip)
# 记录设备信息
async_log_device(username, device_info)
return Result(0, '登录成功')

用户设备信息采集需要征得用户同意,那万一无法采集信息怎么办? 我们也可以想办法在用户第一次登录时,在用户设备中生成记录一个唯一ID并存储在设备中,用于标记这个设备。

异常IP登录

这个和异地登录不同的是,我们可以维护一个IP黑名单,只要是用户登录的IP在黑名单内,则一定要求用户做二次验证。

黑名单的来源主要是通过购买、自行采集的方式获取到的黑产IP

用户风控

上面的各种方式,都不是孤立的,更多的是结合起来,包括其它更多的判断逻辑,来最终决定用户的登录环境是否存在风险,而这部分功能,我们一般会把它抽离出来,单独做为一个风控服务实现。 我们输入用户登录相关的数据,比如用户ID、登录IP、设备信息等,风控服务结合历史数据、用户行为数据等,通过大数据分析以及我们配置的风控规则,最终输出给我们一个风险级别,然后再根据风险级别决定是否需要做后续的措施。

总结

今天主要讲了几种预防用户账号被盗的手段以及简单的实现,相信大家在使用各种产品的时候也有碰到过对应的功能,我们在做用户体系设计的时候,前期可能用户量比较少,但是也可以尽量的考虑安全相关的设计,体量小有小的做法,大有大的做法,但是做了总比没做好。希望读完这篇文章大家有所收获~


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

服务器被爬虫恶意攻击怎么办?

在有预算的情况可以采购第三方服务防火墙,没钱就使用开源的WAF进行防护。WAF防火墙的基本防护原理WAF(Web 应用防火墙)可以使用多种技术来防止恶意爬虫攻击,例如:黑名单:WAF 可以使用黑名单技术来过滤恶意爬虫的请求。黑名单中包含一些已知的爬虫用户代理(...
继续阅读 »

在有预算的情况可以采购第三方服务防火墙,没钱就使用开源的WAF进行防护。

WAF防火墙的基本防护原理

WAF(Web 应用防火墙)可以使用多种技术来防止恶意爬虫攻击,例如:

  1. 黑名单:WAF 可以使用黑名单技术来过滤恶意爬虫的请求。黑名单中包含一些已知的爬虫用户代理(User-Agent),WAF 可以检查每个请求的用户代理,并拒绝那些与黑名单匹配的请求。

  2. 限制访问频率:WAF 可以使用限制访问频率的技术来防止恶意爬虫攻击。例如,可以设置每个 IP 地址在一定时间内只能访问网站的某个页面一定次数。如果超过了访问次数限制,则 WAF 会拒绝该 IP 地址的请求。

  3. JavaScript 检测:WAF 可以使用 JavaScript 检测技术来检测爬虫。例如,可以在页面中嵌入一些 JavaScript 代码,这些代码会检测浏览器的一些属性(如是否支持 JavaScript、是否支持 Cookie 等),如果检测到浏览器属性与正常用户不同,则 WAF 可以认为该请求来自恶意爬虫,从而拒绝该请求。

  4. 隐藏字段:WAF 可以在页面中添加一些隐藏的字段,这些字段只有正常用户才会填写,而恶意爬虫往往无法正确填写这些字段。例如,可以在登录表单中添加一个隐藏字段(如 CSRF Token),如果该字段的值不正确,则 WAF 可以认为该请求来自恶意爬虫,从而拒绝该请求。

  5. 图片验证码:WAF 可以使用图片验证码技术来防止恶意爬虫攻击。例如,可以在某些敏感操作(如注册、登录、发表评论等)前,要求用户输入验证码。如果 WAF 发现多次输入错误验证码的请求,则可以认为该请求来自恶意爬虫,从而拒绝该请求。

使用注意事项

关于 WAF 的具体使用方法,常见的开源 WAF 包括 ModSecurity、Naxsi、WebKnight 等。这些 WAF 都可以通过配置文件来设置规则,过滤恶意请求。一般来说,使用 WAF 的步骤如下:

  1. 安装 WAF:根据 WAF 的安装说明,安装 WAF 并将其集成到 Web 服务器中。

  2. 配置规则:编辑 WAF 的配置文件,设置需要过滤的请求规则,例如黑名单、访问频率限制等。

  3. 测试 WAF:启动 Web 服务器,并针对一些已知的恶意请求进行测试,验证 WAF 是否能够正确过滤这些请求。

  4. 持续维护:WAF 的规则需要根据实际情况不断更新和维护,以保证其能够有效地防止恶意攻击。

开源WAF的优缺点

ModSecurity、Naxsi、WebKnight 都是常见的开源 WAF,它们各有优缺点。

  1. ModSecurity

优点:

  • 可以通过自定义规则来检测和防止各种攻击,包括 SQL 注入、XSS 攻击、命令注入、文件包含等。
  • 支持正则表达式,可以灵活地匹配和过滤请求。
  • 支持 HTTP/2 和 WebSocket 协议。
  • 有一个活跃的社区,提供了丰富的文档和示例代码。
  • 可以与 Apache、Nginx、IIS 等常见的 Web 服务器集成。

缺点:

  • 学习曲线较陡峭,需要一定的安全知识和经验。
  • 配置复杂,需要仔细调整规则以避免误报和漏报。
  • 对于高并发的 Web 应用,可能会对性能产生一定的影响。
  1. Naxsi

优点:

  • 专门针对 Web 应用安全的防火墙,易于使用和配置。
  • 通过学习模式(Learning Mode)和白名单模式(Whitelist Mode)来防止误报。
  • 支持自定义规则,可以根据实际需求进行扩展。
  • 对于高并发的 Web 应用,性能表现较好。

缺点:

  • 仅支持 Nginx Web 服务器。
  • 防护能力相对较弱,只能检测和防止一些常见的攻击,如 SQL 注入、XSS 攻击等。
  • 社区活跃度不高,文档相对较少。
  1. WebKnight

优点:

  • 支持多种 Web 服务器,包括 IIS、Apache、Tomcat 等。
  • 可以通过自定义规则来检测和防止各种攻击,包括 SQL 注入、XSS 攻击、命令注入等。
  • 支持正则表达式,可以灵活地匹配和过滤请求。
  • 有一个活跃的社区,提供了较为详细的文档和示例代码。

缺点:

  • 学习曲线较陡峭,需要一定的安全知识和经验。
  • 配置较为复杂,需要仔细调整规则以避免误报和漏报。
  • 对于高并发的 Web 应用,可能会对性能产生一定的影响。

总的来说,选择哪种 WAF 主要取决于实际需求和应用场景。如果需要防范多种攻击,并且具备一定的安全知识和经验,可以选择 ModSecurity;如果需要一个易于使用和配置的 WAF,并且仅需要防范一些常见的攻击,可以选择 Naxsi;如果需要一个支持多种 Web 服务器的 WAF,并且对性能要求较高,可以选择 WebKnight。

需要注意的是,WAF 并不能完全防止恶意爬虫攻击,因为恶意攻击者可以使用各种技术来规避 WAF 的过滤。因此,在使用 WAF 的同时,还需要采取其他措施来增强网站的安全性,例如使用 SSL/TLS 加密技术、限制敏感操作的访问、使用验证码等。


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

做个清醒的程序员之努力工作为哪般

阅读时长约10分钟,共计2268个字如果要问自己这样一个问题:“我们工作的意义到底是什么?”会得到怎样的答案?是为了安身立命?是为了满足别人的期待?是为了得到社会的认同?抑或是索性认为工作是无意义的?如果我说:工作的意义在于自我实现,你会同意吗?你会觉得这样的...
继续阅读 »

阅读时长约10分钟,共计2268个字

如果要问自己这样一个问题:“我们工作的意义到底是什么?”会得到怎样的答案?

是为了安身立命?是为了满足别人的期待?是为了得到社会的认同?抑或是索性认为工作是无意义的?

如果我说:工作的意义在于自我实现,你会同意吗?你会觉得这样的观点很片面吗?你会觉得这很理想化吗?

其实,依我的工作经验来看,上面列举出的常见答案其实都有道理。人在不同的阶段,不同状态,工作的意义便会发生变化。

坦率地讲,我从开始工作之后,就下定决心不再啃老。简单地说,就是不再向父母伸手要钱。于是,如何保障自己的温饱就是我工作的最首要和最基本目的。也就是说,我刚开始的时候,工作就是为了挣钱。

刚起步的时候我一个月有多少工资呢?很少,2500块。即便是毕业后转正,拿到手也只有三四千块。

但是,这些钱已经可以很好地满足我的温饱需要。即使我出去租房,不在家吃饭,这些钱其实也是够的,只不过很可能剩不下分文。很感谢我的父母,在我事业刚刚开始的时候,照顾了我的起居生活。

一开始,我眼中工作的意义就是为了能养活我自己,就是为了那三两碎银。所以,为了养活自己而选择工作,挣钱,然后达到目的,我至今也不觉得有什么丢人的。

后来呢?因为我一直在家吃饭,午饭的话公司也有食堂,所以基本没什么开销。唯一生活上的花销就是衣服和鞋,可偏偏我穿衣穿鞋算是比较省的,一件衣服基本上少说穿个四五年,鞋的话就更久了,我现在还经常穿七八年前买回来的经典款。我认为穿衣方面,买经典款总是不会错,而且很难因为流行趋势而过时。

话说回来,随着我的小金库慢慢积累变多,我就不愁“安身立命”的目标了。因为我是家族后代中唯一的男性,所以心中总会有一种使命感,虽然没有人给我这方面的压力。我感受到的最大的责任感其实是想让家人生活得更美好的目标,虽然我父母在这方面依然没有表现出很大的期待。

于是凭着这个我自认为的“责任感”,一直努力工作了很多年。其实我的想法很简单,就是想让爱自己和自己爱的人过得好一点。我觉得凭本事挣更多的钱,然后达到这个目标,更是无可厚非的事情,也没什么错。

后来呢?我其实很早就有写博客的习惯,随着读者对我的文章产生认同,更重要的是有出版社编辑的认同,我就产生了要获得社会认同感的目标。虽说是“社会认同感”,其实它所包括的内容很广泛。比如读者的、家人的、老同学的、(前)同事的等等。这种“社会认同感”还会顺便带来他人的尊重甚至是敬重。至少我这次的工作和上次的工作,在面试的时候基本上技术方面是被认可的,免去了“八股文”的考验。不过老实讲,如果考验我面试“八股文”,大概率我还真得吃败仗。

再到现在,金钱对我的诱惑依然存在,但已经大幅降低了。更多的是考虑如何实现自己的价值,真正地释放自己的潜力,对这个社会,对这个世界发挥光和热。也就是我在一开始说的“工作的意义在于自我实现”。

好了,这就是我的故事。一开始为了满足温饱,我去工作,去挣钱;后来,为了得到别人的认可,获得社会认同感而努力工作,顺便把钱给挣了,引一句读者的评论:“挣钱是重要的事情中最不重要的”;再到现在,自我实现对我是最重要的。

所以,在不同阶段,不同状态,对工作意义产生不同的观点,我觉得都是正常的,也都是正确的。

但是,你知道吗?在我刚刚工作的时候,我就立下目标,要在这个世界上留下点什么,留下自己活过的印记,不想虚度此生。但当时无论如何也想不到,自己会成为作者,通过文字和图片把枯燥的编程知识教授给需要的人。

不知道你有没有听说过“马斯洛需求层次”,莫名其妙地,从一开始我就攀登这个需求金字塔,直到现在,已过去十余年。

有读者说我是“长期主义者”,以现在的认知,我愿意做一个“长期主义者”。但当初的我,哪懂什么“长期主义”。我更偏向于用“轴”、“固执”或是“不见棺材不落泪,不撞南墙不死心”这类的修饰词来形容自己。所幸的是,在我“固执”的一路上,受到了很多人的帮助与支持,还有上天的眷顾。“运气”、“机遇”占了很大的比重。

回到最初的问题:“我们工作的意义到底是什么”?如果我们对一个疲于奔命的人说:“你要实现自我”。很遗憾,“实现自我”在这个人眼中,也许就只是保障温饱,他甚至会转过头来和别人说我们是神经病。因为阶段不同,状态不同,追求的东西自然也会不同。也许这很无情,很冷血。但它很真实,也很正常。

但即便我们处在最开始的温饱阶段,着眼于生计,但目光一定要看到那个“未来”,坚定不移地坚持走“长期主义”的道路。

另一方面,在学校里,往往努力就会有结果,这是具有极大的确定性的。踏入社会之后,这种确定性消失了,我们往往努力了,却并没有得到想要的结果。很多人在鼓吹“选择大于努力”,选择固然重要。选错了,越努力,走得越偏。但这种话听多了,就要当心它是否成为了不努力的借口。努力是为了在机会到来的时候,我们有能力抓住他。这就好像不慎跌落坑里的人,有人扔绳子救他,他得努力抓住绳子,才有被救的可能。如果一味求救,却不选一种力所能及的方法去做,无论有多少次生还的机会,也会错过。

所以,任何时候也不要轻视努力与坚持的重要性。这看上去很笨很傻,但它却是每一个平凡人实现“自我价值”的可行之路。就像歌中唱的那样:“老天爱笨小孩”。在这个充满不确定性的时代,更应如此。不确定性就像基因突变,可能会变糟,也可能会变好。当有好事到来时,便是好运来临之际。和“选择大于努力”相比,我更倾向于相信“机会总是留给有准备的人”。这句话出自法国著名的微生物学家、化学家路易斯·巴斯德,告诫人们:机遇往往不易察觉,可遇不可求,容易稍纵即逝。作为普通人,若要抓住机遇,就要把功夫用在平时,甚至是有点傻的、偏执的努力。


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

思考:如何做一名合格的面试官?

背景关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。因此,简单做一些总...
继续阅读 »

背景

关于招聘,在最近一段时间有幸又参与到了面试工作。这次面试也包含一些相对高级一点的岗位,如何招到合适的人成为了最近一段时间一直在思考的一个点。

整体上感觉自己还是没有章法,没有清晰的思路(做不到因人而异),这种情况对自己对他人都是不负责任的。

因此,简单做一些总结思考,边面边想边改进吧。

招聘者的目标

首先,作为招聘者,都希望能找到一些厉害的人,成本不应该是他要考虑的问题。但现实总是相反。

面试官:这是我这次招聘的需求,要这个...... 那个...... 总之,能力越强越好。

公司:这次招聘成本范围已发给你了,注意查收。

面试官:......

所以,在成本有限的情况下,面试官要做的就是找到那些会发光的人。对面试官来说,招到一个即战力(不亏),招到一个高潜力(赚翻了)。

因此,招聘者的目标都是希望能够招到 能力 > 成本 的人。

梳理招聘需求

招聘的需求是需要面试官提前梳理好的,面试官作为团队的组建者,一定要提前规划好招聘需求。比如:

  1. 我要找技术强的。(我不懂的他来)
  2. 我要找态度好的。(我说啥都听)
  3. 我要找有责任心的。(不用我说就把活干得很漂亮)
  4. 我要找学习能力强的。(自我提升,啥需求都能接的住)
  5. 最好还能带团队。(这样我就轻松了)
  6. 最后一定要稳定。(这样我就一直轻松了)

哈哈,先开个玩笑。虽然我真的想......

现实就像上边提到的,招聘者希望的求职者模样。虽然知道不可能,但还是忍不住想要(我控制不住我几己呀!),所以在有限的面试时间内,问了很多方方面面的问题......

面试结束后:我问了那么多才答上来那么几个问题,不行,下一个......

面了几天后:人怎么这么难招呢?

所以真正的招聘需求应该是下边这样的:

  1. 我要招一个领导者还是执行者:这个一定要想清楚,两者考察维度完全不一样。
  2. 我要技术强的:想好哪方面技术强,不要妄图面面俱到。
  3. 我要找有责任心的,学习能力强的,稳定的:想好怎么提问,如何判断。

如果能够做到上边的三点,相信招进来的人应该都是OK得。PS:先做到不亏。

领导者Or执行者

为什么把这个作为第一点,上边也提到过,两者考察维度完全不一样。一场面试,时间就那么点,所以要有针对性。

先说领导者

如果招聘领导者。试想一下领导者有哪些特点,什么样的人你愿意让他成为领导者。

  1. 业务理解程度深,不仅限于产品规划的业务需求,还要有自己的理解和看法。
  2. 技术能力强,通常一个技术方案便能提现出来,方案好不好,考虑全面不全面。
  3. 抗压能力强,能够承担工作压力,这里不是指加班(当然加班也算),更多的是来自于技术,业务的困难和挑战。

以上三点并不代表全部,仅做参考。那么如何在面试中确认呢?

  1. 业务理解程度主要通过追问细节的方式来确认。在你不了解的情况下,依然能够给你讲明白,这个业务是做什么的,关键核心点是什么,核心点有什么难度和挑战,最后是怎么解决的,解决的是否完美,不完美的原因。如果能够回答的不错那就基本合格了。最后可以再多问一下: 有没有哪些产品提出的需求,你认为不合理或者不适合当前产品现状的?这个问题只要回答的有一定高度,那就完美了。

    ps: 如果面试者认为没有什么难度和挑战,只能证明他自己没有深度参与或主导该业务。再简单的系统,也不可能一点问题都没有,如果真的没有,那么完全没有必要安排团队去专门负责。没有简单的系统,只有简单的思考。

    举个栗子,用户管理(用户CRUD)系统我们一听可能都觉得很简单,早期,用户注册要填一堆的东西,现在都是各种登录渠道,非常的方便。站在现在的角度,对于早期的用户管理来说,如何提升用户注册效率,增加用户量就是一个有难度有挑战的事情。

  2. 技术能力,我简单分为有效技术能力和无效技术能力。无效技术能力代指无用且无聊的八股,当然也不是所有的八股都无用。有效技术能力我理解就是解决问题的能力,而解决问题不在于使用的技术手段与否高明,是否先进,只要贴合业务场景,我都会认为有技术能力。反而那些八股回答的头头是道,解决实际项目问题无一用到的会严重减分。

  3. 抗压能力,项目经验是能反应出来一些信息的:有难度,有挑战的事情你不会交给一个不合适的人来做的,所以如果简历的项目经验中有类似的经验那么就证明别人已经帮你筛选过了。PS:别忘了鉴别一下。

再说执行者

还是试想一下,好的执行者有哪些特质:

  1. 注重细节,考虑问题全面。
  2. 责任心强,不会随便应付了事。
  3. 技术OK,至少基础没有问题。

同样,上述三点仅做参考。

  1. 注重细节,直接体现其实跟方案的完善程度有关,所以问问技术方案的异常情况是如何考虑的。另外,直接体现其实就是BUG比较少,当然这个一般人肯定不会说自己BUG多,所以可以问问,对于如何减少BUG量,有没有心得。(这个问题目前来看基本没啥用,哈哈)

  2. 责任心强。责任心如何体现的我也说不太清楚,思考以后认为加班算一方面,有加薪、晋升算一方面,面试中很难直接体现,只能凭感觉。和第一条注重细节一样,我面试全凭聊完之后的直觉,这种大家都会有,而且一般来说准确率也不错。

    PS: 其实在面试沟通过程中,一问一答,很多事情靠直觉、面向也能猜个七七八八,玄学的东西这里不多说。说一点有科学依据的,人的性格简单分为外向、内向,两者都有其各自的特质,通常来说,内向者的特质更多时候适合于执行者。感兴趣的可以去了解一下两种性格特质,有益于团队管理。

  3. 技术OK。这个不做多说了,一定要多问实际使用的,不用的就不要问了,可能用的适当问一下,实际使用的也可以拔高一下往深了问问。比如:mysql都在用,mysql的八股可以多问几个。JVM这种开发基本不用的,简单问一下得了(我一般是不问的)。


这一篇先到这里把,关于技术强、责任心其实也简单提了一下。关于这两点,后续结合实际情况再更新吧。


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

项目提交按钮没防抖,差点影响了验收

前言一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交...
继续阅读 »

前言

一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误个别单据流程给弄不正常了,一些报表的数据统计也不对了,客户相关人员很不满意,马上该交付了,出这问题可还了得,项目款不按时给了,这责任谁都担不起🤣

领导紧急组织相关技术人员开会分析原因

初步分析原因

发生这个情况前端选手应该会很清楚这是怎么回事,明显是项目里的按钮没加防抖导致的,按钮点击触发接口,接口响应慢,用户多点了几次,可能查询接口还没什么问题,如果业务复杂的地方,部分按钮的操作涉及到一些数据计算和后端多次交互更新数据的情况,就会出现错误。

看下项目情况

用到的框架和技术

项目使用 angular8 ts devextreme 组合。对!这就是之前文章提到的那个屎山项目(试用期改祖传屎山是一种怎么样的体验

项目规模

业务单据页面大约几百个,项目里面的按钮几千个,项目里面的按钮由于场景复杂,分别用了如下几种写法:

  • dx-button
  • div
  • dx-icon
  • input type=button
  • svg

由于面临交付,领导希望越快越好,最好一两天之内解决问题

还好我们领导没有说这问题当天就要解决 😁

解决方案

1. 添加防抖函数

按钮点击添加防抖函数,设置合理的时间

function debounce(func, wait) {
let timeout;
return function () {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(func, wait)
}
}

优点

封装一个公共函数,往每个按钮的点击事件里加就行了

缺点

这种情况有个问题就是在业务复杂的场景下,时间设置会比较棘手,如果时间设置短了,接口请求慢,用户多次点击还会出现问题,如果时间设置长了,体验变差了

2. 设置按钮禁用

设置按钮的 disabled 相关属性,按钮点击后设置禁用效果,业务代码执行结束后取消禁用

this.disabled = true
this.disabled = false

优点

原生按钮和使用的UI库的按钮设置简单

缺点

diviconsvg 这种自定义的按钮的需要单独处理效果,比较麻烦

3. 请求拦截器中添加loading

在请求拦截器中根据请求类型显示 loading,请求结束后隐藏

优点

直接在一个地方设置就行了,不用去业务代码里一个个加

缺点

由于我们的技术栈使用的 angular8 内置的请求,无法实现类似 axios 拦截器那种效果,还有就是项目中的接口涉及多个部门的接口,不同部门的规范命名不一样,没有统一的标准,在实际的业务场景中,一个按钮的行为可能触发了多个请求,因此这个方案不适合当前的项目

4. 添加 loading 组件(项目中使用此方案)

新增一个 loading 组件,绑定到全局变量中,按钮点击触发显示 loading,业务执行结束后隐藏。

loading 组件核心代码

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private isLoading$ = new BehaviorSubject<boolean>(false);
  private message$ = new BehaviorSubject<string>('正在加载中...');
  constructor() {}
  show(): void {
    this.isLoading$.next(true);
  }
  hide(): void {
    this.isLoading$.next(false);
  }
}

主要是 show() 和 hide() 函数,将 loading 组件绑定到 app.components.ts 中,绑定组件到window 对象上,

window['loading'] = this.loadingService

在按钮点击时触发 show() 函数,业务代码执行结束后触发 hide() 函数

window['loading'].show();
window['loading'].hide();

优点

这种方式很好的解决了问题,由于 loading 有遮罩层还避免了用户点击某提交按钮后,接口响应慢,这时候去点击了别的操作按钮的情况。

缺点

需要在业务单据的按钮提交的地方一个个加

问题来了,一两天解决所有问题了吗?

这么大的项目一两天不管哪种方案,把所有按钮都处理好是不现实的,经过分析讨论,最终选择了折中处理,先把客户提出来的几个业务单据页面,以及相关的业务单据页面添加上提交 loading 处理,然后再一边改 bug 一边完善剩余的地方,优先保证客户正常使用


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

Android View绘制原理 - RenderNode

这一篇文章我们继续分析另外一个重要的类RenderNode, 这个在前面绘制流程里有也有提到,这里我将更加深入的介绍这个类1 简介RenderNode是一个绘制节点,一个大的界面是由很多小的绘制单元组成,这个正如View的层级结构,整个界面由很多控件组成,这样...
继续阅读 »

这一篇文章我们继续分析另外一个重要的类RenderNode, 这个在前面绘制流程里有也有提到,这里我将更加深入的介绍这个类

1 简介

RenderNode是一个绘制节点,一个大的界面是由很多小的绘制单元组成,这个正如View的层级结构,整个界面由很多控件组成,这样带来的好处就是需要整体绘制界面的时候,只有那些变化的单元重新绘制,然后在重新组装界面即可。这让我联想到了活字印刷术,当我们要印刷一页内容的时候,如果将所有的字都刻在一块板上,当要修改的时候,就需要整体重新来刻,效率很低成本很高,但是如果是将每一个字作为一个组件,页面只是这些字拼接出来的,修改或者重用的话就相对容易很多,RenderNode就相当于是一个个的字。

尽管在应用层我们很少使用这个类,但是实际上的每个View都持有 一个RenderNode,我们可以这样去理解,View作为一个组件,会由很多业务,比如事件,布局,测量和绘制等,而绘制业务正是委托给RenderNode去完成,绘制需要Canvas也是由这个RenderNode提供的。RenderNode除了为View提供绘制能力外,还为其他可绘制的API提供绘制能力,最常见的就是Drawable,我们也可以封装自己的绘制组件,基于RenderNode的绘制是利用了硬件加速的绘制。

在应用层,View会形成树型的层级结构,因此RenderNode也会相应的构造一个出绘制节点的树形结构。但是RenderNode的树形结构和View的树形结构可能是不一样的,因为一个View可能会对应着几个RenderNode,比如View的背景也会转换成一个RenderNode,因此一个View节点可能会产生多个RenderNode对象,通常一个View的背景和View的的Children是平级的。

2 属性

2.1 Java层

RenderNode 的功能主要是在C层实现的。在java层,它持有一个mCurrentRecordingCanvas,表示当前正在使用的那个Canvas
frameworks/base/graphics/java/android/graphics/RenderNode.java

private RecordingCanvas mCurrentRecordingCanvas;

这个Canvas的类型是RecordingCanvas, 它由RenderNode的beginRecording方法创建的

public @NonNull RecordingCanvas beginRecording(int width, int height) {
if (mCurrentRecordingCanvas != null) {
throw new IllegalStateException(
"Recording currently in progress - missing #endRecording() call?");
}
mCurrentRecordingCanvas = RecordingCanvas.obtain(this, width, height);
return mCurrentRecordingCanvas;
}

这里可以看到beginRecording方法不能连续调用,需要在调用endRecording之后才能再次调用。这个canvas是通过RecordingCanvas获得的一个canvas,obtain方法往往代表是从缓存池中获取的,这里我们不深入介绍,我们知道这个Canvas 是再从这里获得的,它的类型是RecordingCanvas. 它是Canvas的子类。

RenderNode 也由很多其他的属性,但是在C层定义的,所以我们继续分析一下在C层的RenderNode

2.2 C层

在JNI 和C层这里,主要有这个几个文件
frameworks/base/libs/hwui/jni/android_graphics_RenderNode.cpp

frameworks/base/libs/hwui/RenderNode.h
frameworks/base/libs/hwui/RenderNode.cpp

以及专门用于存储属性的RenderProperties类

frameworks/base/libs/hwui/RenderProperties.h
frameworks/base/libs/hwui/RenderProperties.cpp

2.2.1 mStagingProperties

mStagingProperties记录的是修改过的属性,在没有提交前,所有的修改都临时存在mStagingProperties。

RenderProperties mStagingProperties;

对属性的修改,是通过一个宏定义来实现的,来分析一个简单属性的top的修改流程

frameworks/base/graphics/java/android/graphics/RenderNode.java

public boolean setTop(int top) {
return nSetTop(mNativeRenderNode, top);
}

frameworks/base/libs/hwui/jni/android_graphics_RenderNode.cpp

static jboolean android_view_RenderNode_setTop(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr, int top) {
return SET_AND_DIRTY(setTop, top, RenderNode::Y);
}


通过SET_AND_DIRTY这个宏定义来调用mutateStagingProperties上的方法

#define SET_AND_DIRTY(prop, val, dirtyFlag) \
(reinterpret_cast<RenderNode*>(renderNodePtr)->mutateStagingProperties().prop(val) \
? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true) \
: false)

扩展开就相当于是

reinterpret_cast<RenderNode*>(renderNodePtr)->mutateStagingProperties().setTop(top) 
? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true)
: false

renderNode->mutateStagingProperties() 返回的就是 mStagingProperties
frameworks/base/libs/hwui/RenderNode.h

RenderProperties& mutateStagingProperties() { return mStagingProperties; }

因此,会执行RenderProperties的setTop方法。如果setTop返回true,则会调用setPropertyFieldsDirty,记录发生变化的属性,这里传入的是RenderNode::Y这个枚举值,定义如下:

 enum DirtyPropertyMask {
GENERIC = 1 << 1,
TRANSLATION_X = 1 << 2,
TRANSLATION_Y = 1 << 3,
TRANSLATION_Z = 1 << 4,
SCALE_X = 1 << 5,
SCALE_Y = 1 << 6,
ROTATION = 1 << 7,
ROTATION_X = 1 << 8,
ROTATION_Y = 1 << 9,
X = 1 << 10,
Y = 1 << 11,
Z = 1 << 12,
ALPHA = 1 << 13,
DISPLAY_LIST = 1 << 14,
};

frameworks/base/libs/hwui/RenderProperties.h

bool setTop(int top) {
if (RP_SET(mPrimitiveFields.mTop, top)) {
mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop;
if (!mPrimitiveFields.mPivotExplicitlySet) {
mPrimitiveFields.mMatrixOrPivotDirty = true;
}
return true;
}
return false;
}

RP_SET是一个宏定义

#define RP_SET(a, b, ...) ((a) != (b) ? ((a) = (b), ##__VA_ARGS__, true) : false)

也就是如果mPrimitiveFields.mTop与top不相同,则将top赋值给mPrimitiveFields.mTop, 并且返回true,否则直接返回false。
如果top变化了,同步修改高度。
这就是一个简单属性的修改流程。 那么RenderNode有那些属性呢?来看一看RenderProperties的定义

 struct PrimitiveFields {
int mLeft = 0, mTop = 0, mRight = 0, mBottom = 0;
int mWidth = 0, mHeight = 0;
int mClippingFlags = CLIP_TO_BOUNDS;
SkColor mSpotShadowColor = SK_ColorBLACK;
SkColor mAmbientShadowColor = SK_ColorBLACK;
float mAlpha = 1;
float mTranslationX = 0, mTranslationY = 0, mTranslationZ = 0;
float mElevation = 0;
float mRotation = 0, mRotationX = 0, mRotationY = 0;
float mScaleX = 1, mScaleY = 1;
float mPivotX = 0, mPivotY = 0;
bool mHasOverlappingRendering = false;
bool mPivotExplicitlySet = false;
bool mMatrixOrPivotDirty = false;
bool mProjectBackwards = false;
bool mProjectionReceiver = false;
bool mAllowForceDark = true;
bool mClipMayBeComplex = false;
Rect mClipBounds;
Outline mOutline;
RevealClip mRevealClip;
} mPrimitiveFields;

我们可以看到这里的属性和我们在JAVA层View的几何属性是非常相似的,基本上View的几何属性都会类似setTop的方式反映到RenderProperties。大部分的简单属性比如top,bottom,translate,rotate,elevation,scale,pivot这些就不介绍了,我们分析一下两个比较特殊的属性mProjectBackwards 和 mProjectionReceiver。这两个属性会更改RenderNode绘制顺序。设置成mProjectionReceiver的RenderNode会成为一个锚点,被标记成mProjectBackwards的RenderNode不会被绘制在它的父节点,而是绘制到它最近的父节点中的标记成mProjectionReceiver的子节点中。例如P节点包含一个子节点C,以及P的背景PB,C包含一个背景CB. 一般的顺序应该是CB绘制到C中,然后C和PB绘制到P中。 但是如果PB被设置成mProjectionReceiver ,且CB被标记成mProjectBackwards,绘制的顺序将变成,C绘制到P中,CB绘制到PB 中,然后PB绘制到P中。也就是说将CB投影到PB中去。这种做法将使得CB的变化不会导致C重新绘制,从而提升效率,比如作为背景动画的RenderNode,它不会导致View自身的RenderNode的重新绘制。

2.2.1 mProperties

mStagingProperties暂存的修改将会与mProperties同步,从而正式成为影响绘制的参数。同步的方法很简单,直接赋值,在绘制帧之前会完成这些参数的同步。

void RenderNode::syncProperties() {
mProperties = mStagingProperties;
}

个人感觉好像第一同步之后,mProperties就和mStagingProperties指向同一个对象,只有似乎以后没有同步的必要了。

3 总结

RenderNode主要保存了一系列的属性,大部分的View属性都会反映到RenderNode,RenderNode使用RenderProperties来保存这些属性,在绘制帧的时候,这些属性会影响最终的绘制。RenderNode也会形成一颗树形的层级结构,但是它与View的层级结构并不是一一对应的,在同一级中的RenderNode不仅包含View的兄弟节点的RenderNode,也包含父View的背景等可绘制内容。除了属性之外,RenderNode的另外一个重要属性是DisplayList,它存放的是这个RenderNode的绘制指令,这个将在下一篇中继续分析。

以上内容是对RenderNode的分析,基于个人的理解,如有疏漏和错误, 👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀


作者:淘淘养乐多
链接:https://juejin.cn/post/7259650149076172860
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

前端跨域的几种方式

前端跨域的几种方式一、 什么是跨域跨域(Cross-Origin)是指在浏览器你执行脚本时,通过XMLHttpRequest、Fetch等方式请求不同源(协议、域名、端口)的资源。同源策略是浏览器的一种安全机制,它限制了网页中的脚本只能与同源(相同协议、域名、...
继续阅读 »

前端跨域的几种方式

一、 什么是跨域

跨域(Cross-Origin)是指在浏览器你执行脚本时,通过XMLHttpRequest、Fetch等方式请求不同源(协议、域名、端口)的资源。同源策略是浏览器的一种安全机制,它限制了网页中的脚本只能与同源(相同协议、域名、端口)的资源进行交互,防止恶意网站获取用户的敏感信息或进行攻击。

在同源策略下。浏览器允许脚本访问同源的资源,但不允许访问不同域的资源。跨域请求会触发浏览器的安全机制,导致请求被拒绝。例如,如果网页在域名A下加载了一个脚本,而在这个脚本尝试访问域名B下的资源,浏览器阻止这个跨域请求。

对于前端开发来说,跨域请求是一个常见的问题,因为现代应用通常需要不同的服务器或域名上获取数据。为了实现跨域访问,开发者可以采用常用的一些常见的方式,如 JSONP、CORS、代理服务器或 WebSocket等。这些允许前端页面与其他源的服务器进行安全的通信。

二、 前端跨域的几种方式

1、JSONP

JSONP(JSON with Padding)是一种利用<script>标签跨域获取数据的方法,可以绕过浏览器的同源策略限制。

JSONP的原理如下:

  • 通过请求参数作为回调函数的参数传递给服务器,服务器在响应中返回这个回调函数的调用,前端页面通过动态插入<script>标签来加载数据。
  • 由于<script>标签不受同源策略的限制,因此可以跨域加载并执行返回的脚本。

以下是JSONP的使用示例:

<script>
function callback(data) {
// 处理数据
}
</script>

<script src="http://example.com/api?callback=callback"></script>

上面的示例中,我们定义了一个名为callback的函数,在之后的脚本中使用这个函数来处理返回的数据。通过将callback函数的名称作为请求参数传递给服务器(例如: example.com/api?callbac… ),服务器在返回的响应中将调用该函数并传递数据。前端页面通过动态插入<script>标签来加载这个跨域的脚本,并在脚本执行时用callback函数来处理数据。

JSONP的应用场景是在需要获取跨域数据时,由于同源策略的限制我们无法直接使用XMLHttpRequest 或 Fetch方法时。比如说:我们需要从另一个域名的API获取数据,而该API支持JSONP,我们可以使用JSONP来实现跨域获取数据并在前端页面中进行处理。

JSONP需要服务器直接返回可执行的脚本代码。此外,JSONP只支持GET请求,不支持POST请求等其他类型的脚本。

2、CORS

CORS(跨域资源共享)是一种通过在服务器配置响应头来实现跨域请求的机制。它允许在浏览器中进行安全的跨域通信,突破同源策略的限制。

CORS的原理如下:

  • 前端页面发送跨域请求给服务器。
  • 服务器在响应头中添加Access-Control-Allow-Origin字段,指定允许跨域的源。例如,可以将其设置为Access-Control-Allow-Origin: http://example.com
  • 浏览器收到带有这个响应头的请求后,会判断该请求是否在允许的跨域列表中,如果是则将响应返回给前端页面,否则会被浏览器拦截。
  • 前端页面收到响应后,跨域像处理同源请求一样处理响应数据。

以下是CORS的使用示例:

//服务器端响应头配置
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type


//前端页面请求
fetch('http://example.com/api',{
method: 'GET',
mode: 'cors'
})
.then(response => response.json())
.then(data => {
//处理数据
});

在上面的示例中,服务器在响应头中添加了Access-Control-Allow-Origin字段,指定允许跨域请求源为http://example.com。前端页面在发送具有mode: 'cors'的跨域请求时,浏览器会允许请求通过,并将响应返回给前端页面,使得前端跨域像处理同源请求一样处理跨域请求的响应数据。

CORS的运用非常广泛,特别是在现代的Web应用中。通过使用CORS,前端跨域与其他域的服务器进行安全的跨域通信,实现数据的获取与交互。开发者跨域在服务器端配置CORS响应头,来实现不同应用之间的跨域请求,提供更好的用户体验以及功能拓展。

3、前端代理服务器

前端代理服务器作为中间层。通过其中转,跨域绕过浏览器的同源限制,实现跨域请求。这种方法的优点是简单、灵活、适用于各种场景。

前端代理服务器的原理如下:

  • 前端代理服务器位于浏览器和后端服务器之间,充当转发请求和响应的角色。
  • 当浏览器发起跨域请求时,请求会先发送到前端代理服务器。
  • 前端代理服务器收到请求后,根据根据配置的规则判断是否属于跨域请求。
  • 如果属于跨域请求,前端代理服务器将发送新的请求到后端服务器,获取数据。
  • 前端代理服务器收到后端服务器的响应后,将响应内容返回给浏览器。

以下是如何使用Node.js创建一个前端代理服务器:

const http = require('http');
const request = require('request');

const proxy = http.createServer((req,res) => {
//处理跨域请求
res.setHeader('Access-Control-Allow-Origin','*');
res.setHeader('Access-Control-Allow-Methods','GET,POST,PUT,DELETE');

//转发请求到后端服务器
const url = 'http://example.com' + req.url;
req.pipe(request(url)).pipe(res);
});

const port = 8080;
proxy.listen(port, () => {
console.log('Proxy server is running on port ${port}');
});

使用前端代理服务器的好处是可以方便地在开发环境中进行前后端分离,同时避免一些跨域请求带来地麻烦。但在生产环境中,建议采用更成熟地反向代理服务器,如Nginx来处理跨域请求。

4、WebSocket

前端WebSocket实现跨域的原理是基于浏览器的同源策略的限制,通过WebSocket协议进行通信。由于WebSocket是基于TCP协议的全双工通信协议,WebSocket对象不受同源策略的约束,因此跨域实现跨域通信。

WebSocket通信的实现原理:

  • 在服务器端配置允许跨域请求:服务器端需要设置响应头,允许特定的域名访问该服务器资源。可以通过Access-Control-Allow-Origin进行跨域资源共享。
  • 在前端使用WebSocket对象与服务器建立连接:在前端代码中,可以使用WebSocket对象建立与目标服务器的连接。使用WebSocket构造函数提供服务器的URL,例如:let socket = new WebSocket('ws://example.com/socket').
  • 进行WebSocket通信:一旦与服务器建立了WebSocket连接,前端就可以通过WebSocket对象发送和接收数据。可以使用WebSocket对象的send()方法发送数据,使用onmessage时间监听接收到的信息,使用onopen事件监听连接建立成功,使用onclose事件建立连接关闭。

WebSocketHTML5的新技术,不是所有的浏览器都支持。在使用WebSocket实现跨域通信时,需要检查浏览器的兼容性并提供备选方案,确保在不支持WebSocket的情况下仍能正常工作。


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

前端开发如何给自己定位?初级?中级?高级!

引言在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端...
继续阅读 »

引言

在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端面试指南,P6/P6+/P7的能力标准。

目录

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。
1.熟练掌握JavaScript。
2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。
3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。
4.熟练掌握react生态常用工具,redux/react-router等。
5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。
6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。
7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。

0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

初级:

  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。
  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。
  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。
  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。
  • 知道raf和其他达到60fps的方法。

中级:

  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。
  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。
  • 知道怎样在移动端处理加载问题,渲染性能问题。
  • 知道如何结合native能力优化性能。
  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。

高级:

  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。
  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。
  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。

1.熟练掌握JavaScript。

初级:

  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:
  • 知道组合寄生继承,知道class继承。
  • 知道怎么创建类function + class。
  • 知道闭包在实际场景中怎么用,常见的坑。
  • 知道模块是什么,怎么用。
  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。
  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。
  • 知道ES6数组相关方法,比如forEach,map,reduce。

中级:

  • 知道class继承与组合寄生继承的差别,并能举例说明。
  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。
  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。
  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。
  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。

2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

初级:

  • 知道webpack,rollup以及他们适用的场景。
  • 知道webpack v4和v3的区别。
  • 脱口而出webpack基础配置。
  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。
  • 知道amd,cmd,commonjs,es module分别是什么。
  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。

中级:

  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。
  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。
  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。
  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。

高级:

  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:
  • 项目脚手架搭建,及如何以工具形态共享。
  • 团队eslint规范如何设计,及如何统一更新。
  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。
  • 客户端缓存及预加载方案。

3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

初级:

  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。
  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。
  • 知道diff算法大致实现思路。
  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。
  • 以上几点react替换为vue或angular同样适用。

中级:

  • 能说明白为什么要实现fiber,以及可能带来的坑。
  • 能说明白为什么要实现hook。
  • 能说明白为什么要用immutable,以及用或者不用的考虑。
  • 知道react不常用的特性,比如context,portal。
  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。

高级:

  • 能设计出框架无关的技术架构。包括但不限于:
  • 说明如何解决可能存在的冲突问题,需要结合实际案例。
  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。

4.熟练掌握react生态常用工具,redux/react-router等。

初级:

  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。
  • 知道vue和angular对应全家桶分别有哪些。
  • 知道浏览器react相关插件有什么,怎么用。
  • 知道react-router v3/v4的差异。
  • 知道antd组件化设计思路。
  • 知道thunk干嘛用的,怎么实现的。

中级:

  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。
  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。
  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。

高级:

  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决

5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

初级:

  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。
  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。
  • 知道axios或同级别网络请求库,知道axios的核心功能。
  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;
  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;

中级:

  • HTML方面能够结合各个浏览器api描述常用类库的实现。
  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。
  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。
  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。
  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。
  • 知道oauth2.0轻量与完整实现原理。
  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。

高级:

  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。
  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。
  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。
  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。

6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

初级:

  • 知道eslint,以及如何与工程配合使用。
  • 了解近3年前端较重要的更新事件。
  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。
  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。

高级:

  • 在团队内推行eslint,并给出工程化解决方案。
  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。

7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。

  • 根据了解的深度分初/中/高级。
  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。
  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。
  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。

5. 结论与进一步学习

本文为前端开发人员提供了一个能力定位指南,帮助他们了解自己在前端领域的定位,并提供了具体的代码示例来巩固学习成果。通过不断学习和实践,前端开发人员可以逐步提升自己的能力,从初级到中级再到高级。但请注意,在实际工作中,不同公司和项目对于各个级别的要求可能会有所不同。

为了进一步提高自己的水平,前端开发人员可以考虑以下学习路径和资源:

  • 阅读官方文档和教程,如MDN、React官方文档等;
  • 参与开源项目,并与其他开发人员进行交流和合作;
  • 关注前端开发的博客和社区,如Medium、Stack Overflow等;
  • 参加在线或线下的前端开发培训课程;
  • 阅读经典的前端开发书籍,如《JavaScript高级程序设计》、《CSS权威指南》等。

通过持续学习和实践,相信每个前端开发人员都可以不断成长,并在前端领域中取得更好的成就。祝愿大家在前端开发的道路上越走越远!


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

程序员接外包的三个原则以及有意思的讨论

文章来源网络 原则一:乙方来做决策 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接 因为在这种甲方眼里,你只是...
继续阅读 »

文章来源网络


原则一:乙方来做决策



  • 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了

  • 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接

  • 因为在这种甲方眼里,你只是“施工方”,他们即不需要你的经验价值,更不会为你的经验买单。所以这种甲方你会做得很累,当他们觉得“你的工作强度不足以匹配付给你的费用时(他们总这样觉得)”,他们就会不停地向你提出新的开发需求

  • 所以,你要尽量找那种尊重你经验价值,总是向你请教,请你帮他做决策的甲方


原则二:为甲方护航



  • 甲方未来会遇到什么问题,你们双方其实都不知道,所以你需要一边开发一边解决甲方实际遇到的问题。

  • 因此,不要为了完成合同上的工作内容而工作,要为甲方遇到的实际问题,为了甲方的利益而开发,要提前做好变通的准备。

  • 永远不要觉得你把合同上的功能列表做完,你就能休息了。你要解决的真正问题是,为甲方护航,直至甲方可以自己航行。


原则三:不做没有用户的项目



  • 如果甲方的项目没有太多用户使用,这种项目就不要接。

  • 除了代码的累计经验,还有一种经验也很重要,那就是“了解用户的市场经验”

  • 只有真正面对有实际用户的项目,你才能有“解决市场提出的问题”的经验,而不是停留在“解决甲方提出的问题”

  • 拥有市场经验,你就会有更高的附加价值,再配上尊重你经验价值的甲方,你就会有更高的收入

  • 永远记住:真正愿意在你身上花钱的甲方,他的目的一定是为了让你帮他赚钱!


以上只是我根据自己经验的一家之言,可能对我有用,不一定对别人也有用。肯定还有很多有价值的原则,希望大家根据自己的经验一起来分享。


下面是一些有意思的讨论


原则 2 、3 都是虚的,就不讨论了。

只说原则一:


一般而言,甲方跟你对接的,一定不是老板。

所以他的核心目的一定是项目实施成功。

但项目是不是真的能给企业带来效益,其实是优先级特别低的一个选项。


拿日常生活举个例子。夫妻、情侣之间,你媳妇儿托你办个事,比如让你买个西瓜。

你明知道冬天的西瓜又贵又不好吃,你会怎么办?


A ,买西瓜,回去一边吃西瓜一起骂水果摊老板没良心。

B ,给你媳妇儿上农业课。然后媳妇儿让你跪搓衣板。

C ,水果摊老板训你一顿,以冬天吃白菜豆腐好为由,非卖你一颗大白菜。你家都不敢回。


这里面,你就是那个甲方对接人。你怎么选?


所以乙方一定不能做决策。乙方做决策的结果,就是甲方对接人被利益集团踹开或者得罪甲方对接人,最终导致项目失败




我也来说三个原则

1.要签合同,合同越细越好;

2.要给订金,订金越多越好;

3.尾款不结不给全部源码。




原则一:外包大部分就是苦力活,核心有价值的部分有自己公司的人干轮不到外包,你不干有的是人干,不会有人尊重你经验价值,甲方说怎么干就怎么干,写到合同里,按合同来,没甲方懂自家业务,别替甲方做决策,万一瞎建议导致项目出现大问题,黄了,外包钱都拿不回来


原则二:给多少钱办多少事,如果甲方给钱痛快,事少,可以看自己良心对甲方多上点心,否则别给自己加戏,不然很可能把自己感动了,甲方却想着好不容易碰上这么个人,白嫖


原则三:不做没有用户的项目,太片面,不是所有外包项目都是面对海量用户,但是做所有外包项目都是为了赚钱,假如有个富二代两三万找你做个毕设,简单钱多不用后续维护,这种接不接?假如某工厂几十万定制内部系统,可能只有几个人用,这种接不接


总之外包就是赚个辛苦钱,别指望这个来提升自己技术和自我价值,外包行业水太深,你这几个原则都太理想化




某富豪要盖一栋私人别墅,招建筑工人,现在缺一名搅拌水泥的工人,

找到了张三,张三说我去过很多工地,啥活儿都干过,经验极其丰富,我可以指导一切事物,我再给你兼职当个总设计师吧,一切事物听我的决策没错。

我每天做完我的本职工作搅拌水泥砂浆,我还能熬夜给建筑设计布局,风水,房间规划,材料采购等等,我啥都会,直接干到建筑完工

富豪很感兴趣,说那你来吧,我盖的是自己住的别墅,张三一听连连摆手:你是盖私人别墅啊?不行不行,我不去了,我以前盖的都是高楼大厦,住户多对我技术水平有严峻的考验,做成了对我有很大提高,私人别墅才几个人用,对我职业生涯一点帮助都没




永远不要接外包

这才是正确的答案

做私活的时间

不如自己休息休息,陪陪家人




屁事真多,有钱就行了,管他项目有没有人,人家产品低能你还得兜底,接外包考虑的是能不能满足需求。啥条件啊还能挑三拣四,给多少钱干多少活。 招投标接的 30 万以上的项目才有可能考虑你说的这些东西。





呵呵 我的意见是:



  1. 给钱就做(前提是合规,不是合理),先给定金,拿到定金开工。

  2. 遇到扯皮,就停止开发。

  3. 要有空闲的时间,偶尔做做(上面说的对:永远不要做外包)。


展开来说,做外包的长期收益很低。就当临时玩一下,所以给钱就做,不管你的需求合理不合理,比如甲方想给智障人士开发一款数独小游戏,好,给钱,签合同,支付定金,开工。


开工了3天,甲方突然说,那个我想加个魔方游戏。不好意思,不行,立即停止开发,开始和甲方掰扯,如果掰扯不明白,就终止合同,如果掰扯明白就继续。


不说了,我要和甲甲甲甲甲方掰扯去了。

作者:Data_Adventure
来源:juejin.cn/post/7256590619412676663

收起阅读 »

这一篇浏览器事件循环,可能会颠覆部分人的对宏任务和微任务的理解🤪🤪🤪

web
在这两天里看到一篇文章,发现好像很多人都把事件循环给搞混了,到底是宏任务先执行还是微任务先执行。在写这篇文章之前,我也随机挑选了几位幸运观众来问这个问题,好像大多都是说微任务先执行。 那么从这篇文章里,我们就来探讨一下到底是哪个先执行。 什么是进程 进程是计算...
继续阅读 »

在这两天里看到一篇文章,发现好像很多人都把事件循环给搞混了,到底是宏任务先执行还是微任务先执行。在写这篇文章之前,我也随机挑选了几位幸运观众来问这个问题,好像大多都是说微任务先执行。


那么从这篇文章里,我们就来探讨一下到底是哪个先执行。


什么是进程


进程是计算机系统中正在运行的程序的实例。它是操作系统对一个正在运行的程序的抽象表示,负责管理程序的执行和资源分配。


通常它包括堆栈,例如临时数据,如函数参数、返回地址和局部变量和数据段,其中数据段包括全局变量。


进程还可能包括堆,这是在进程运行时动态分配的内存。在 JavaScript 中,堆和栈的内存分配是通过不同方式进行的:




  1. 堆内存分配:



    • JavaScript 中的对象、数组和函数等复杂数据类型都存储在堆内存中;

    • 使用 new 关键字或对象字面量语法创建对象时,会在堆内存中动态分配相应的内存空间;

    • 堆内存的释放由垃圾回收机制自动处理,当一个对象不再被引用时,垃圾回收机制会自动回收其占用的堆内存,释放资源;




  2. 栈内存分配:



    • JavaScript 中的基本数据类型,如数字、布尔值和字符串以及函数的局部变量保存在栈内存中;

    • 栈内存的分配是静态的,编译器在编译阶段就确定了变量的内存空间大小;

    • 当函数被调用时,会在栈内存中创建一个称为栈帧 stack frame 的数据结构,用于存储函数的参数、局部变量、返回地址等信息;

    • 当函数执行完毕或从函数中返回时,对应的栈帧会被销毁,栈内存中的数据也随之释放;




在操作系统中,每个进程都有自己的地址空间、状态和控制信息。进程可以独立运行,与其他进程离开来,互不干扰。它们可以同时进行,并通过进程间通信机制进行交互。


进程在执行时会改变状态。进程状态,部分取决于进程的当前活动。每个进程可能处于以下状态:



  • 新的: 进程正在创建;

  • 运行: 指令正在运行;

  • 等待: 进程等待发生某个时间,如 I/O 完成或收到信号;

  • 就绪: 进程等待分配处理器;

  • 终止: 进程已经完成执行;


下图完整地显示了一个状态图:
20230725131814


什么是线程


线程是进程中的一个执行路径,是进程的组成部分。在同一个进程中的多个线程共享进程的资源,如内存空间和文件句柄等。不同线程之间可以并发执行,各自堵路地完成特定的任务。


进程和线程之间的关系如下:



  • 一个进程可以创建多个线程,这些线程共享同一个地址空间和资源,能够并发地执行任务;

  • 线程是在进程内部创建和销毁的,它们与进程共享进程的上下文,包括打开的文件、全局变量和堆内存等;

  • 每个进程至少包含一个主线程,主线程用于执行进程的主要业务逻辑。其他线程可以作为辅助线程来完成特定的任务;


主线程是一个程序中的特殊线程,它是程序的入口点和主要执行线程。


在许多编程语言和操作系统中,主线程是程序启动后自动创建的线程,负责执行程序的主要业务逻辑。主线程会按照顺序执行代码,从程序的入口点开始,直到程序结束或主线程显式终止。


你可以理解成一个篮球场,整个篮球场就是一个进程,就是一块能供线程使用的内存。


而线程就是篮球场上的每一个队员,每个人都有不同的职责。


Chrome: 多进程架构浏览器


许多网站包含活动内容,如 JavaScriptFlushHTML5 等,以便提供丰富的、动态的 Web 浏览体验。遗憾的是,这些 Web 应用程序也可能包含软件缺陷,从而导致响应迟滞,有的甚至网络浏览器崩溃。如果一个 Web 浏览器只对一个网站进行浏览,那么这不是一个大问题。


但是,现代 Web 浏览器提供标签是浏览,它运行 Web 浏览器的一个实例,同时打开多个网站,而每个标签代表一个网站。要在不同网站之间切换,用户只需点击响应的标签。这种安排如下图所示:


20230725154126


这种方法的一个问题是: 如果任何标签的 Web 应用程序崩溃,那么整个进程,包括所有其他标签所显示的网站也会崩溃。


GoogleChrome Web 浏览器通过多进程架构的设计解决这以问题。Chrome 具有多种不同类型的进程,这里我们主要讲讲其中三个:



  • 浏览器进程: 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能;

  • 网络进程: 网络进程负责处理浏览器内发起的所有网络请求,例如加载网页、资源文件,如图片、CSS JavaScriptXMLHttpRequest 和 Fetch API 等请求;

  • 渲染进程: 主要负责渲染网页的逻辑。主要处理 HTML、Javascript、图像等等。一般情况下,对应于新标签的每个网站都会创建一个新的渲染进程。因此,可能会有多个渲染进程同时活跃;


如下图所示:
20230725155622


例如我只打开了两个标签也,浏览器就会开辟两个两个不同的进程,两者之间相互独立,一个崩掉了不会另外一个。



这个目前是这样子的,但是后续可能会改,根据 chrome 文档说明,后期可能会修改成每个站点开启一个进程,例如,你访问 b 站,而再 b 站里面的所有页面都不会开启新的渲染进程了,详情请看 chrome 官方文档



渲染主线程是如何工作的


渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:



  • 解析 HTML;

  • 解析 CSS;

  • 计算样式;

  • 布局;

  • 处理图层;

  • 每秒把⻚面画 60 次;

  • 执行全局 JavaScript 代码;

  • 执行事件处理函数;

  • 执行计时器的回调函数;


等等事情,当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在事件循环的作用下,渲染主线程取出消息队列中的渲染任务,并开启渲染流程。整个过程分为多个阶段,分别是: 解析 HTML、样式计算、布局、分层、绘制、分块、光栅化、画,每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。


首先解析的是 HTML 文档,这里我们忽略 CSS 的文件,如果主线程解析到 script 位置,会停止解析 HTML,转而等待 JavaScript 文件下载好,主线程将 JavaScript 代码解析执行完成后,才能继续解析 HTML。这是因为 JavaScript 代码的执行过程中可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JavaScript 会阻塞 HTML 解析的根本原因。
20230726082307



前面的这句话中有两个关键字很关键字,画好重点,它们分别是 下载解析



要处理这么多的事情,浏览器给渲染进程采用了多线程,它主要包含了以下线程:



  • 主线程: 主线程负责解析 HTMLCSSJavaScript,构建 DOM 树、CSSOM 树和渲染树,并进行页面布局和绘制。它还处理用户交互,执行 JavaScript 代码以及其他页面渲染相关的任务;

  • 合成线程: 合成线程负责将渲染树转换为图层,并执行图层的合成操作;

  • 网络线程: 网络线程负责处理网络请求和数据传输。当浏览器需要加载网页、图片或其他资源时,网络线程负责发送请求并接收响应数据;

  • 定时器线程: 定时器线程负责管理定时器事件,包括 setTimeoutsetInterval 等。它用于在指定的时间间隔内触发预定的任务;

  • 事件处理线程: 当用户进行交互操作,如点击按钮、滚动页面、输入文本等,需要触发相应的事件处理函数;


由于主线程和合成线程是并行执行的,这就可能导致这两个线程之间存在数据交互的问题。例如,当主线程和合成线程都需要访问相同的共享资源时,就需要进行同步,以避免竞态条件等问题。


这里就设计到消息队列的作用: 主线程和合成线程之间通过消息队列进行通信。主线程将渲染任务和图层数据等信息封装成消息,并将消息放入消息队列中。合成线程从消息队列中获取消息,并执行相应的图层合成操作。


浏览器中出现消息队列是为了处理异步任务和事件。在浏览器当中,有许多人任务是在后台执行或者将来某个事件触发时才执行的,例如:



  • 异步操作: 比如通过 AJAX 请求从服务器获取数据、读取本地文件等,这些操作需要等待网络请求或者文件读取完成后再处理响应的数据;

  • 定时器: 通过 setTimeoutsetInterval 设置的定时器任务,需要在指定的时间间隔后执行;

  • 事件处理: 当用户进行交互操作,如点击按钮、滚动页面、输入文本等,需要触发相应的事件处理函数;


20230726091257


如上图所示,当渲染主线程正在执行一个 JavaScript 函数,执行到一半的时候用户点击了按钮或者碰到了一个定时器,也就是 setTimeout。因为在我们的渲染进程里面是有定时器线程的,定时器线程监听到有这个定时器操作。那么该线程会将 setTimeout 里面的事件处理函数 (setTimeout 的第一个回调函数) 作为一个任务拿去排队。


因为消息队列采用的是队列的数据结构,当渲染主线程将所有任务情况之后,然后从消息队列中拿去最旧的那个任务,假设消息队列之前没有任务的情况下,就拿出 setTimeout 这个事件处理函数。如果在该函数当中又遇到了类似的事件处理函数或者定时器,按照前面的步骤。依此循环,直到所有任务执行完成。


整个过程,就被称之为事件循环。


什么是异步


JavaScript 是一门单线程的编程语言.意味着在一个特定的时间点,只能有一个代码块在执行。当执行一个同步任务时,如果任务需要很长时间才能完成,如网络请求、文件读取等,整个程序会被阻塞,导致用户界面无响应,甚至造成卡顿的问题。这种情况在 Web 应用中尤其常见,因为 JavaScript 经常与网络请求、DOM 操作等耗时任务打交道。


常见的异步操作包括:



  • 网络请求: 发送 HTTP 请求并等待服务器响应时,通常使用异步方式,以允许程序继续执行其他操作;

  • 定时器: 设置定时器,在一段时间后执行某个任务,也是异步操作的一种;

  • 事件处理: 为 DOM 元素注册事件监听器是一种常见的异步任务。当特定事件触发时,相应的事件处理函数将被异步调用,例如 addEventListener


渲染主线程负责处理网页的构建、布局、绘制和用户交互等任务,而异步编程使得我们可以在主线程执行同步代码的同时,处理耗时的异步操作,例如网络请求、文件读写等,以提高程序的性能和用户体验。在 JavaScript 中,通过事件循环机制,异步编程实现了一种非阻塞的执行方式,使得浏览器能够高效地处理各种任务,同时保持用户界面的响应性。


例如你要执行一个 setTimeout:


setTimeout(() => {
console.log(111);
}, 3000);

console.log(222);

在这段代码当中,如果让渲染主线程去等待这个定时器任务执行完再去执行下一个任务,就会导致主线程长期处于阻塞的状态,从而导致浏览器页面长期见不到效果,可能要砸电脑了。


20230726095635



整个流程你可以理解为这样,但也可能不完全正确。



等到整个计时结束,再执行 console.log(222) 的代码,这种模式就叫作同步。整个时候消息队列还有很多任务在等待,可能还存在一些渲染页面的任务,有可能直接导致整个页面卡死。


所以为了这个问题,浏览器采用了异步的方式来解决这个问题,因为渲染主线程承担着及其重要的工作,无论如何都不能阻塞。


20230726100527


当计时开始之后,我就不管你了,就例如你是服务,餐厅里面来了客人,客人点完了菜,你把菜单交给后厨,你就可以先不管了,继续服务下一个客人,也就是从消息队列中拿下一个任务。当计时结束之后,会把该回调函数放入到消息队列末尾.等到剩下的任务完成之后,你就可以给客人端菜了。


任务优先级


任务没有优先级,在消息队列中先进先出,但消息队列是有优先级的。


事件循环在过去的说法中,任务分为两个队列,一个是宏任务,一个是微任务。但是现在已经没有了宏任务的说法了。



因为我在年初的时候就写过相关事件循环的文章,且有在 mdn 上搜索过宏任务的相关概念,但现在在 mdn 已经完全搜索不到了。



根据 W3C 的最新解释:



  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环当中,浏览器可以根据实际情况从不同的队列中取出任务执行;

  • 浏览器必须准备好一个微任务队列,微队列中的任务优先所有其他任务执行;


相关 W3C 连接


Chrome 的实现中,至少包含了下面的队列:



  • 延时队列: 用于存放计时器到达后的回调任务,优先级 ;

  • 交互队列: 用于存放用户操作后产生的事件任务,优先级 ;

  • 微队列: 用户存放需要最快执行的任务,优先级最高;


虽然浏览器最新规范是这样,但是你用之前的宏任务和微任务去答题也完全没有问题的,但是输出的顺序是完全没有变的,况且这篇文章主要内容也不是讲这个,那么在之后的代码中我们就继续以宏任务和微任务的来讲。


宏任务和微任务 重点来啦!!!


宏任务是一组异步任务,这些任务通常由浏览器的事件触发器发起,并在主线程中按照顺序执行。常见的宏任务包括:



  • setTimeoutsetInterval;

  • I/O 操作,例如读取文件、网络请求;

  • DOM 事件,例如点击事件、输入事件;

  • requestAnimationFrame;

  • script 标签;


微任务是一个细微的异步任务,它的执行时机在宏任务之后、渲染之前。微任务通常在一个宏任务执行完毕后立即执行,而不需要等待其他宏任务。这使得微任务的执行优先级比宏任务高。常见的微任务包括:



  • Promiseresolvereject 回调;

  • async/await 中的异步函数;

  • MutationObserver;


很重要的一点来了,为什么说 script 标签是宏任务呢?


如果忘记了,你再看看我们前面中说到的 下载解析 两个关键字。


script 标签包含的 JavaScript 代码在浏览器中执行时,被认为是宏任务。这是因为执行 script 标签内的代码需要进行一系列的操作,包括解析、编译和执行。主要有以下几个理由:



  • 解析和编译: 当浏览器遇到 script 标签时,它会停止当前的文档解析过程,并开始解析 script 内的 JavaScript 代码。解析器将逐行读取代码,并将其转换为可执行的内部表示形式。这个解析和编译的过程是一个比较耗时的操作,需要占用大量的 CPU 资源;

  • 阻塞页面渲染: 由于脚本的执行通常会修改当前页面的结构和样式,浏览器必须等待脚本执行完毕后再进行页面的渲染。也就是说,当浏览器执行 script 标签时,它会阻塞页面的渲染,直到脚本执行完毕才会继续渲染;

  • 可能引起网络请求:在 script 标签中,可以使用外部的 JavaScript 文件引用,例如 <script src="example.js"></script>。当浏览器遇到这样的情况时,它会发起一个网络请求去下载该文件,并等待文件下载完成后再执行。网络请求通常是一个比较耗时的操作,因此将其作为宏任务可以确保脚本的执行按照正确的顺序进行;


总结起来,script 标签被认为是宏任务是因为它需要解析、编译和执行 JavaScript 代码,并且会阻塞页面的渲染。此外,如果使用了外部 JavaScript 文件,还可能引起网络请求,进一步增加了执行时间。这些特性使得 script 标签的执行与其他微任务(如 Promise)不同,被归类为宏任务。


如果你依然觉得理由不够充分的话,请看以下代码:


<!-- 脚本 1 -->
<script>
// 同步
console.log("start1");
// 异步宏
setTimeout(() => console.log("timer1"), 0);
new Promise((resolve, reject) => {
// 同步
console.log("p1");
resolve();
}).then(() => {
// 异步微
console.log("then1");
});
// 同步
console.log("end1");
</script>

<!-- 脚本 2 -->
<script>
// 同步
console.log("start2");
// 异步宏
setTimeout(() => console.log("timer2"), 0);
new Promise((resolve, reject) => {
// 同步
console.log("p2");
resolve();
}).then(() => {
// 异步微
console.log("then2");
});
// 同步
console.log("end2");
</script>

该代码的输出结果如下所示:
20230726105526


如果 script 标签不是宏任务,普通任务的话,是不是应该先执行 start2end2 再执行 then1


所以根据此结论,整个浏览器循环应该是 先执行 宏任务 -> 同步代码 -> 微任务,直到当前宏任务中的微任务清理完毕,继续执行下一个宏任务,以此类推。


最后我再抛出一个问题,没有你这个 script 这个宏任务的出现,你哪来的微任务?


一个事件循环过程模型如下,当调用栈为空时,执行以下步骤:



  1. 选择任务队列中最旧的任务(队列是一个先进先出的队列,最旧的那个就是最先进的,这里是 任务 A);

  2. 如果任务 A为空(意味着任务队列为空),跳转到第6步;

  3. 将当前运行的任务设置为任务 A;

  4. 运行任务 A,意味着运行回调函数;

  5. 运行结束,将当前的任务设置为空,删除任务 A;

  6. 执行微任务队列:

    1. 选择微任务队列中最早的任务 X;

    2. 如果任务 X,代表这微任务为空,跳转到步骤6;

    3. 将当前运行的任务设置为任务 X,并运行该任务;

    4. 运行结束,将当前正在运行的任务设置为空,删除任务 X;

    5. 选择微任务队列中下一个最旧的任务,可以理解为第n+1个入队的,跳转到步骤2;

    6. 完成微任务队列;



  7. 跳转到第1步;


这个事件循环过程模型如下图所示:


image.png


值得注意的是,当一个任务在宏任务队列中正在运行时,可能会注册新事件,因此可能会创建新任务,下面是两个新创建的任务:



  • Promise.then(...) 是一个回调任务:当 promisefulfilled/rejected:任务将被推入当前轮事件循环中的微任务队列;当promisepending:任务将在下一轮事件循环中被推入微任务队列(可能是下一轮);


案例


接下来我们通过一些案例来加深对事件循环的理解。


案例一


setTimeout(() => {
console.log("time1");

new Promise((resolve) => {
resolve();
}).then(() => {
new Promise((resolve) => {
resolve();
}).then(() => {
console.log("then4");
});

console.log("then2");
});
});

new Promise((resolve) => {
console.log("p1");
resolve();
}).then(() => {
console.log("then1");
});

最后的输出结果为 p1 then1 time1 then2 then4,下面就来分析一下这个结果的由来:



  1. 代码首先遇到settimeout,是一个宏任务,里面的代码不会被执行;

  2. 接着代码往下执行,遇到 new Promise(...)中的回调函数是一个同步任务,直接执行;

  3. 直接输出 "p1",调用 resolve(),Promise 的状态变为 fuifilled,当 promise 状态变为 fulfilled/rejected时,任务将被推入当前轮事件循环中的微任务队列,所以后面的 then(...) 会被加入到微任务队列里面;

  4. 主线程中的同步代码执行完,从微任务中取出最旧的那个任务,也就是 then(...),输出 then1,此时微任务队列为空;

  5. 继续执行宏任务,也就是这个 settimeout,代码从上往下执行,首先输出 time1;

  6. 在下面的代码中又遇到了 new Promise(...),并且调用了 resolve(),then(...)被加入到微任务队列中,此时的同步任务已经执行完毕,直接执行这个 then(...);

  7. 又是遇到 new Promise(....),又是调用的 resolve(),所以 then() 方法会被添加到微任务队列中,代码往下执行,输出 "then2",此时微任务then(...)中的代码全部执行完毕;

  8. 此时同步任务执行完毕,继续执行微任务中的 then(...),输出 "then4";

  9. 所有代码运行完毕,程序结束;


案例二


<script>
console.log(1);

setTimeout(() => {
console.log(5);
});

new Promise((resolve) => {
resolve();
}).then(() => {
console.log(3);
});
console.log(2);
</script>

<script>
console.log(4);
</script>

这段代码的最后的输出结果是: 1 2 3 4 5,具体代码执行过程有以下步骤:
首先提醒一点,script 标签本身是一个宏任务,当页面出现多个 script 标签的时候,浏览器会把script 标签作为宏任务来解析。当前实例中两个 script 标签,它们会一次加入到宏任务队列中。



  1. console.log(...) 是同步代码,1首先会被输出,代码往下执行;

  2. 遇到 settimeout(),会被加入到宏任务队列中;

  3. then(...) 会被加入到微任务队列中,代码继续往下执行;

  4. console.log(...) 为同步认为输出 2;

  5. 此时同步任务执行完毕,转而执行微任务 then(...),输出 3;

  6. 当前宏任务执行完毕,此时同步任务和微任务都为空,取出最旧的宏任务,也就是第二个 script 标签;

  7. 输出 4,此时同步代码和微任务队列都为空,继续执行下一个宏任务,也就是 settimeout;

  8. 输出 5;


案例三


async function foo() {
console.log("start");
await bar();
console.log("end");
}

async function bar() {
console.log("bar");
}

console.log(1);

setTimeout(() => {
console.log("time");
});

foo();

new Promise((resolve) => {
console.log("p1");
resolve();
}).then(() => {
console.log("p2");
});

console.log(2);

这段代码的最后的输出结果是: 1 start bar p1 2 end p2 time,下面就来分析一下这段代码的执行过程:



  1. 前面两个是函数定义,不执行,遇到 console.log(),输出 1;

  2. 代码继续往下执行,遇到 settimeout(),代码加入到宏任务队列之中,代码往下执行;

  3. 调用 foo,输出 start;

  4. await 等待 bar() 调用的返回结果;

  5. 执行 bar() 函数,输出 bar;

  6. await 相当于 Promise.then(...),代码被加入到微任务队列中,所以 end 还不执行;

  7. 代码往下执行,遇到 new Promise(...),p1 直接输出,then() 又继续被加入到微任务队列中;

  8. 代码继续往下执行,遇到 console.log(2),输出 2;

  9. 此时主线程代码快为空,执行微任务队列中最旧的那个任务,继续执行 await 后续代码,输出 end;

  10. 执行 then() ,输出 p2;

  11. 最后执行 settimeout,输出 time;


案例四


Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
})
.then((res) => {
console.log(res);
});

Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});

这个案例中,因为每一个 then() 都是一个微任务,所以首先执行的是0,代码继续往下执行,输出同级的 then(),也就是输出 1


如果 Promise 内返回的对象具有可调用的 then() 方法,则会在微任务队列中再插入一个任务,这就慢了一拍,如果这个 then() 方法是来源于 Promise 的,则因为是异步又慢了一拍,所以一共是慢了拍,所以 Promise.resolve(4) 的结果等到 23 输出完成,console.log(res) 的结果才会被输出;


所以该案例的最终结果输出的是 0 1 2 3 4 5 6


参考资料



总结


整个浏览器循环应该是 先执行 宏任务 -> 同步代码 -> 微任务,直到当前宏任务中的微任务清理完毕,继续执行下一个宏任务,以此类推。


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰


作者:Moment
来源:juejin.cn/post/7259927532249710653
收起阅读 »

今日算法09-青蛙跳台阶问题

web
一、题目描述 题目链接:leetcode.cn/problems/qi… 难易程度:简单 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。 答案需要取模 1e9+7(1000000007),如计算初始结果为...
继续阅读 »

一、题目描述



题目链接:leetcode.cn/problems/qi…


难易程度:简单



一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。


答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。


示例1
输入:n = 2
输出:2

示例2
输入:n = 7
输出:21

示例3
输入:n = 0
输出:1




二、解题思路


动态规划


当 n 为 1 时,只有一种覆盖方法:





当 n = 2 时,有两种跳法:





跳 n 阶台阶,可以先跳 1 阶台阶,再跳 n-1 阶台阶;或者先跳 2 阶台阶,再跳 n-2 阶台阶。而 n-1 和 n-2 阶台阶的跳法可以看成子问题,该问题的递推公式为:





也就变成了斐波那契数列问题,参考:今日算法07-斐波那契数列


复杂度分析


时间复杂度 O(N) :计算 f(n) 需循环 n 次,每轮循环内计算操作使用 O(1) 。


空间复杂度 O(1) : 几个标志变量使用常数大小的额外空间。


三、代码实现


public int JumpFloor(int n) {
   if (n <= 2)
       return n;
   int pre2 = 1, pre1 = 2;
   int result = 0;
   for (int i = 2; i < n; i++) {
       result = pre2 + pre1;
       pre2 = pre1;
       pre1 = result;
  }
   return result;
}


推荐阅读



封面




今日算法系列,题解更新地址:studeyang.tech/2023/0725.h…



作者:杨同学technotes
来源:juejin.cn/post/7259543257708658747
收起阅读 »

媒体查询,响应式设计?帮帮我!

web
什么是媒体查询?媒体查询是一种 CSS 语言特性,它允许作者根据设备或窗口的特性有条件地应用 CSS 规则来查看应用程序。最常见的情况是根据视口宽度应用 CSS 规则,这样 CSS 作者就能根据窗口或设备的大小创建相应的组件和布局。但这也可能延伸到用户是否偏好...
继续阅读 »

什么是媒体查询?
媒体查询是一种 CSS 语言特性,它允许作者根据设备或窗口的特性有条件地应用 CSS 规则来查看应用程序。最常见的情况是根据视口宽度应用 CSS 规则,这样 CSS 作者就能根据窗口或设备的大小创建相应的组件和布局。但这也可能延伸到用户是否偏好浅色或深色模式,甚至是用户的可访问性偏好,以及更多属性。



什么是响应式设计?


随着各种设备类型和屏幕尺寸的增多,网络应用程序为用户提供更加量身定制的可视化展示,并针对用户首选交互方式的屏幕尺寸进行优化,已变得越来越重要。


响应式设计可以通过多种技术组合来实现,包括有条件地应用 CSS 规则的媒体查询、容器查询,以及根据它们所包含的内容(例如 flexbox 或 grid)选择灵活的布局。在本文中,我们将重点关注媒体查询和响应式布局,但随着浏览器支持程度的增加,容器查询也需要记住。在撰写本文时,它们还没有准备好进入普及阶段,但可以用于渐进式增强


什么是移动优先设计?


移动优先设计是在设计和构建响应式 web 应用时可以采用的原则。理想情况下,这种方法应该在流程的所有阶段--从开始到结束--都作为指导原则。对于设计来说,这意味着原型或 UI 设计的第一次迭代应该专注于移动的体验,然后再转向更宽的视口尺寸。


虽然你可以从另一个方向(宽优先)来处理 Web 应用程序,但随着屏幕空间的增加,在视觉上重新组织组件要比试图将组件塞进更小的屏幕空间容易得多。


类似的规则也适用于开发过程。一般来说,您应该为基本情况(最窄的屏幕)编写标记和样式,并在必要时逐步为更宽的屏幕应用条件样式。


虽然你可以从另一个方向来处理这个问题,或者使用窄优先和宽优先的混合方法,但这会使你的样式更难以理解,并增加了其他人在审查或维护时的精神负担。当然,也有一些例外情况,编写少量的宽优先规则会更简单,所以请谨慎行事。


CSS 像素对比设备像素


当苹果在2011年推出 iPhone 4时,它是第一款采用高密度显示屏的主流智能手机。早期的 iPhone 的显示分辨率为320x480px,当 iPhone 推出所谓的 “视网膜显示屏” 时 -- 在相同的物理显示宽度下,分辨率提高了一倍,达到640x960px -- 这带来了挑战。不希望用户面临他们不断问自己的情况,“这是什么,蚂蚁的网站?”,一个巧妙的解决方案被设计了出来,iPhone 4将遵循 CSS 规则,就好像它仍然是一个320 x480 px 的设备,并简单地以两倍的密度渲染。这使得现有的网站可以按预期运行,而不需要任何代码更改 - 当为 Web 引入新技术时,您会发现这是一个常见的主题。


由此,创建了术语 CSS 像素和设备像素。


W3C CSS 规范将设备像素定义为:



设备像素是设备输出上能够显示其全部颜色范围的最小面积单位。



CSS 像素(也称为逻辑像素或参考像素)由 W3C CSS 规范定义为:



参考像素是设备像素密度为96 dpi 并且与读取器的距离为一臂长的设备上的一个像素的视角。因此,对于28英寸的标称臂长,视角约为0.0213度。因此,对于在臂长处的阅读,lpx 对应于约0.26mm(1/96英寸)。



参考像素的96 DPI 规则并不总是严格遵守,并且可以根据设备类型和典型的观看距离而变化。
设备像素比率(或 dppx)是每个 CSS 像素使用多少设备像素的一维因子。设备像素比通常是整数(例如,整数)。1、2、3),因为这使得缩放更简单,但并不总是(例如,1.25、1.5等)。


如何使我的网站响应?


默认情况下,移动的浏览器将假定网站的设计不适合这种设备的较窄视口。为了向后兼容,这些浏览器可能会呈现一个网站,就好像屏幕更大,然后缩小到适合更小的屏幕。这不是一个理想的体验,用户经常需要缩放和平移页面,但允许网站的功能主要是因为它最初创建的。


要告诉浏览器某个站点正在为所有视口大小提供优化的体验,您可以在文档中包含以下 Meta 标记:


<meta name="viewport" content="width=device-width, initial-scale=1" />

非矩形显示器


如今,一些设备具有圆角或显示遮挡(诸如显示凹口、相机孔穿孔或软件覆盖),这意味着整个矩形对于用于内容来说是不“安全”的,因为它可能被部分地或完全地遮挡。


默认情况下,此类设备上的浏览器将在“安全区”内接矩形和与文档背景匹配的垂直或水平条内显示内容。


有一些方法可以允许内容扩展到这个区域,避免黑边邮筒的丑陋,但这是一个更高级的功能,不是必需的。


要退出默认的黑边和邮筒,并声明您的应用程序可以适当地处理屏幕的安全和不安全区域,您可以包含以下 Meta 标记:


<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

文本大小


移动的浏览器还可能人为地增大字体大小,以使文本更具可读性。如果您的网站已经提供了适当大小的文本,您可以包含以下 CSS 来禁用此行为:


html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}

虽然在无障碍标准中没有规定文本的最小大小,但在大多数情况下 16px 是一个很好的最低限度。


对于输入字段,如果字体大小小于 16px,浏览器可能会在聚焦时放大。有一些方法可以禁用这种行为,例如在 Meta viewport 中设置 maximum-scale=1.0,但强烈建议不要这样做,因为这可能会干扰依赖缩放的用户。更好的解决方案是确保 font-size 大小至少为 16px


什么是断点?


样式中的断点是指条件样式规则停止或开始应用于页面以响应视口大小的点。最常见的是指 min-widthmax-width,但也可以应用于 height


在媒体查询中,这些断点(768px479px)将像这样使用:


@media (min-width: 768px) {
// 宽度 > 768px的条件样式
}

@media (max-width: 479px) {
// 宽度 <= 479px的条件样式
}

当遵循移动优先设计原则时,大多数时候应该使用媒体查询。


还需要注意的是,min-*max-* 查询适用于包含范围,因此在定义断点两侧的样式时,不应使用相同的像素值。


同样重要的是要注意,当放大页面时,以 CSS 像素为单位的视口大小可能会沿着明显的设备像素比率而变化。这可能会导致视口实际上表现得好像其长度是小数值的情况。


@media (max-width: 479px) {
// 宽度 < 479px的条件样式
}

@media (min-width: 480px) {
// 宽度 >= 480px的条件样式
}

在上面的示例中,如果视口(作为缩放的结果)报告为 479.5px,则两个条件规则块都不适用。相反,例如额外的小数值 0.98px 通常应用于查询 max-width


为什么要这么说? 0.02px 是早期版本的 Safari 支持的 CSS 像素的最小分割。参见 WebKit bug #178261


CSS 在 Media Queries Level 4规范中引入了范围查询的概念,其中 <><=, 和 >= 可用于表达性更强的条件,包括包含和排除范围。在撰写本文时,所有主流浏览器都支持这些功能,然而,在 iOS 等平台上的支持还不够。


@media (width < 480px) {
// 宽度 < 480px的条件样式
}

@media (width >= 480px) {
// 宽度 >= 480px的条件样式
}

我应该选择哪些断点?


这是一个经常被问到的问题,但坦率地说,这并不重要,只要一个 Web 应用程序在你选择的断点之间的所有屏幕尺寸上都能正常工作。你也不想选择太多或太少。


iPhone 在2007年首次推出时,屏幕分辨率为320x480px。按照惯例,所有智能手机的视口宽度至少为320 CSS 像素。当建立一个响应式网站时,你至少应该满足这个宽度的设备。


最近,网络变得更容易被一类设备访问,这些设备适合更经典的外形,称为功能手机,以及可穿戴技术。这些设备通常具有小于320px 的视口宽度。


一些设备,如 Apple Watch,将表现得好像它们有一个320px 的视口,以允许与未专门针对极小视口进行优化的网站兼容。如果要声明您确实处理 Apple watch 的较窄视口,请在文档中包含以下 Meta 标记:


<meta name="disable-adaptations" content="watch" />

如果您使用的是设计系统或组件库(例如 Material UI、Bootstrap 等)它为您提供了自己的默认断点,您可能会发现坚持使用这些断点是有益的。


如果你选择自己的断点,有一些历史上相关的断点可以作为灵感:



  • 320px - 智能手机视口的最小宽度

  • 480px - 智能手机和平板电脑之间的近似边界

  • 768px - 最初的 iPad 分辨率为768 x1024 px

  • 1024px - 同上

  • 1280px - 16:9 720p(HD)显示器的标准宽度


通常,给断点命名是个好主意。但是,不要试图称它们为“移动”,“平板电脑”和“桌面”之类的名称。虽然在平板电脑的早期,移动,平板电脑和桌面之间的界限更加清晰,但现在有如此广泛的设备和视口尺寸,以至于这些设备之间的界限变得模糊。如今,我们有屏幕尺寸比一些平板电脑更大的可折叠手机,以及让台式机和笔记本电脑屏幕相形见绌的平板电脑屏幕。


将特定范围称为“平板电脑”或“台式机”可能会让您陷入为单一类型的设备(例如平板电脑)进行构建和设计的陷阱。假设“移动的”或“平板”视口将总是使用触摸屏)。相反,您应该专注于构建在各种设备上工作的体验。


响应式布局技术


有两种 CSS 布局算法特别适合响应式设计:



  • Flexbox

  • Grid


FLEXBOX


Flexbox 是一种 CSS 布局算法,它允许我们指定子元素在页面上的排列方式。此控件应用于特定方向(称为 flex 轴)。


虽然 flexbox 可以用于呈现多行(带换行),但一行中的内容元素不会改变其他行中元素的排列方式。这意味着除非明确设置 flex 项的宽度,否则它们的排列方式可能不一致。如果需要,CSS Grid 可能更合适。


Flex Wrap


使用 Flexbox 时可以不使用媒体查询,而是依靠 flex-wrap 属性,使内容可以根据内容大小多次跨轴。设置 flex-wrap: wrap 将意味着内容在下方( flex-direction: row)或右侧( flex-direction: column)换行。您还可以设置 flex-wrap: wrap-reverse 使内容在上方或左侧换行。


Flex Direction


通常情况下,对于水平空间有限的窄视口,设计可能要求垂直排列内容,但对于屏幕空间较大的宽视口,则可能改为水平排列内容。


.className {
display: flex;
flex-direction: column;
}

@media (min-width: 768px) {
.className {
flex-direction: row;
}
}

长期以来,媒体查询需要在顶层定义,但当相关规则没有在大型样式表中共存时,这会增加维护负担。在撰写本文时,浏览器尚未广泛支持这种做法,但许多工具和预处理器都允许这样做。


.className {
display: flex;
flex-direction: column;

@media (min-width: 768px) {
flex-direction: row;
}
}

GRID


Grid 是一种 CSS 布局算法,它允许我们指定子元素在页面上的排列方式。Grid 允许开发人员指定元素在行和列之间的排列方式。


就可实现的布局类型而言,它与 flexbox 有重叠之处,但也有显著区别。使用 Grid 布局时,网格项会根据横轴和纵轴上的网格轨道进行约束和对齐。


以下是与响应式设计搭配使用的常见布局技术的几个示例。


Columns


设计师通常会使用 12 栏网格(或窄视口的 4 栏网格)。您可以使用 grid-template-columns 在 CSS 中复制这种模式。结合断点,您就可以轻松分配类,使元素跨越特定的列数。


Google的Una Kravets在One Line Layouts开发站点上分享了一些交互式示例。


RAM(重复、自动、最小最大)


另一种网格布局技术通常称为RAM(重复,自动,最小最大)。我鼓励你去看看One Line Layouts开发站点上的交互式示例


当你事先不知道网格需要多少列,而是希望在一些预设的范围内让内容的大小来决定列数时,RAM 是最有用的。auto-fitauto-fill 的工作方式类似,但当项目数量少于填充一行的数量时会发生什么情况除外。


// 网格项目将始终至少150像素宽,
// 并将伸展以填满所有可用空间
.auto-fit {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

// 网格项目将始终至少150像素宽,
// 并且会伸展直到有足够的空间
// (如果有)添加匹配的空网格轨道
.auto-fill {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}

Grid Template Areas


网格模板区域是用于响应式布局的最强大工具之一,可让您以直观的方式将元素排列到网格上。


举个例子,我们可能会有一个包含页眉、主要部分、侧边栏和页脚的布局,它们都在狭窄的视口上垂直排列。不过,将 grid-template-areas 区域与 grid-template-columnsgrid-template-rows 结合使用,我们可以用相同的标记将这些元素重新排列成网格模式。


代码示例:


.layout {
display: grid;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
grid-template-rows: auto 1fr auto auto;
}

@media (min-width: 768px) {
.layout {
grid-template-areas:
"header header"
"main sidebar"
"footer footer";
grid-template-columns: 1fr 200px;
grid-template-rows: auto 1fr auto;
}
}

.header {
grid-area: header;
}

.main {
grid-area: main;
}

.sidebar {
grid-area: sidebar;
}

.footer {
grid-area: footer;
}

<div class="layout">
<header class="header">Headerheader>
<main class="main">Mainmain>
<aside class="sidebar">Sidebaraside>
<footer class="footer">Footerfooter>
div>

响应图像


在高密度显示屏上,根据 CSS 像素而不是设备像素来调整图片大小,可能会导致图片质量低于用户的预期,尤其是在显示清晰的文本或矢量资源时,会显得格外刺眼。因此,为用户提供更高密度的图片是有意义的。


如果可能,请使用基于矢量的图像 (SVG)。矢量不是指定像素的光栅,而是描述在屏幕上绘制某些内容的过程,这一过程可以放大/缩小到任何屏幕尺寸,但始终保持清晰。矢量图像通常适用于简单的插图、图标或徽标。它们不适用于照片。



请注意,SVG 可以嵌入光栅图像。如果是这种情况,又无法获得真正的矢量图像,最好直接使用光栅图像。这是因为光栅图像在 SVG 中使用 base64 编码,与普通二进制文件相比,文件大小会增大。


对于光栅图像,有几种为高密度显示指定多个图像源的方法,允许浏览器选择最适合特定设备的图像源。


对于静态大小的图像,你可以使用 x 描述符(指定最佳设备像素比)指定。例如,如果您有一个图标或徽标,显示宽度为44px,你可以创建该图像的多个不同版本,并指定如下内容:


<img
srcset="
/path/
to/img-44w.png 1x,
/path/
to/img-66w.png 1.5x,
/path/
to/img-88w.png 2x,
/path/
to/img-132w.png 3x
"

/>


重要的是,这些 x 描述符只是一种提示,设备仍可能出于各种原因(如用户选择了节省带宽的规定)选择较低分辨率的版本。


在 CSS 中使用 image-set()(注意浏览器的支持并不完善)对background-image也可以采用类似的技术:


.selector {
height: 44px;
width: 44px;
/* 对于不支持 image-set() 的浏览器使用 2x 回退 */
background-image: url(/path/to/img-88w.png);
/* Safari 只支持 -webkit-image-set() */
background-image: -webkit-image-set(
url(/path/to/img-44w.png) 1x,
url(/path/to/img-66w.png) 1.5x,
url(/path/to/img-88w.png) 2x,
url(/path/to/img-132w.png) 3x
);
/* 标准语法 */
background-image: image-set(
url(/path/to/img-44w.png) 1x,
url(/path/to/img-66w.png) 1.5x,
url(/path/to/img-88w.png) 2x,
url(/path/to/img-132w.png) 3x
);
}

对于随着页面大小调整而改变大小的图像,可以组合使用 srcsetsizes 属性。例如。


<img
srcset="
/path/to/img-320w.jpg 320w,
/path/to/img-480w.jpg 480w,
/path/to/img-640w.jpg 640w,
/path/to/img-960w.jpg 960w,
/path/to/img-1280w.jpg 1280w
"

sizes="(min-width: 768px) 480px, 100vw"
/>


在上面的例子中,我们在 srcset 中以不同实际宽度(320px、480px、640px、960px、1280px)的多个不同图像渲染。在 sizes 属性中,我们告诉浏览器这些图像将默认以视口宽度的 100% 显示,然后对于768px 和更宽的视口,图像将以固定的480 CSS 像素宽度显示。然后,浏览器将根据设备像素比为设备选择最佳图像渲染(尽管这只是一个提示,浏览器可以选择使用更高或更低的分辨率选项)。


使用 WebP 和 AVIF 等现代图像格式压缩技术,当图像以 2 倍密度显示时,文件大小通常只比 1 倍版本略有增加。而且,当设备像素比大于 2 时,收益会越来越小。因此,您可以只包括优化的2x 图像。Google Chrome 团队的开发者倡导者 Jake Archibald 写了一篇博客文章讨论了这一问题,并强调了一个事实:你的大多数用户可能都在使用高密度显示器浏览网页。

作者:chansee97
来源:juejin.cn/post/7259605860603576375

收起阅读 »

微服务的各种边界在架构演进中的作用

演进式架构 在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此! Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式...
继续阅读 »

演进式架构


在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此!


Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式架构就是以支持增量的、非破坏的变更作为第一原则,同时支持在应用程序结构层面的多维度变化。


那如何判断微服务设计是否合理呢?其实很简单,只需要看它是否满足这样的情形就可以了:随着业务的发展或需求的变更,在不断重新拆分或者组合成新的微服务的过程中,不会大幅增加软件开发和维护的成本,并且这个架构演进的过程是非常轻松、简单的。


这也是微服务设计的重点,就是看微服务设计是否能够支持架构长期、轻松的演进。


那用DDD方法设计的微服务,不仅可以通过限界上下文和聚合实现微服务内外的解耦,同时也可以很容易地实现业务功能积木式模块的重组和更新,从而实现架构演进。


微服务还是小单体?


有些项目团队在将集中式单体应用拆分为微服务时,首先进行的往往不是建立领域模型,而只是按照业务功能将原来单体应用的一个软件包拆分成多个所谓的“微服务”软件包,而这些“微服务”内的代码仍然是集中式三层架构的模式,“微服务”内的代码高度耦合,逻辑边界不清晰,这里我们暂且称它为“小单体微服务”。


下面这张图也很好地展示了这个过程。





而随着新需求的提出和业务的发展,这些小单体微服务会慢慢膨胀起来。当有一天你发现这些膨胀了的微服务,有一部分业务功能需要拆分出去,或者部分功能需要与其它微服务进行重组时,你会发现原来这些看似清晰的微服务,不知不觉已经摇身一变,变成了臃肿油腻的大单体了,而这个大单体内的代码依然是高度耦合且边界不清的。


“辛辛苦苦好多年,一夜回到解放前啊!”这个时候你就需要一遍又一遍地重复着从大单体向单体微服务重构的过程。想想,这个代价是不是有点高了呢?


其实这个问题已经很明显了,那就是边界。


这种单体式微服务只定义了一个维度的边界,也就是微服务之间的物理边界,本质上还是单体架构模式。微服务设计时要考虑的不仅仅只有这一个边界,别忘了还要定义好微服务内的逻辑边界和代码边界,这样才能得到你想要的结果。


那现在你知道了,我们一定要避免将微服务设计为小单体微服务,那具体该如何避免呢?清晰的边界人人想要,可该如何保证呢?DDD已然给出了答案。


微服务边界的作用


你应该还记得DDD设计方法里的限界上下文和聚合吧?它们就是用来定义领域模型和微服务边界的。


我们再来回顾一下DDD的设计过程。


在事件风暴中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象。根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。


为了方便理解,我们将这些边界分为: 逻辑边界、物理边界和代码边界


逻辑边界 主要定义同一业务领域或应用内紧密关联的对象所组成的不同聚类的组合之间的边界。事件风暴对不同实体对象进行关联和聚类分析后,会产生多个聚合和限界上下文,它们一起组成这个领域的领域模型。微服务内聚合之间的边界就是逻辑边界。一般来说微服务会有一个以上的聚合,在开发过程中不同聚合的代码隔离在不同的聚合代码目录中。


逻辑边界在微服务设计和架构演进中具有非常重要的意义!


微服务的架构演进并不是随心所欲的,需要遵循一定的规则,这个规则就是逻辑边界。微服务架构演进时,在业务端以聚合为单位进行业务能力的重组,在微服务端以聚合的代码目录为单位进行微服务代码的重组。由于按照DDD方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间代码松耦合,因此在领域模型和微服务代码重构时,我们就不需要花费太多的时间和精力了。


现在我们来看一个微服务实例,在下面这张图中,我们可以看到微服务里包含了两个聚合的业务逻辑,两个聚合分别内聚了各自不同的业务能力,聚合内的代码分别归到了不同的聚合目录下。


那随着业务的快速发展,如果某一个微服务遇到了高性能挑战,需要将部分业务能力独立出去,我们就可以以聚合为单位,将聚合代码拆分独立为一个新的微服务,这样就可以很容易地实现微服务的拆分。





另外,我们也可以对多个微服务内有相似功能的聚合进行功能和代码重组,组合为新的聚合和微服务,独立为通用微服务。现在你是不是有点做中台的感觉呢?


物理边界 主要从部署和运行的视角来定义微服务之间的边界。不同微服务部署位置和运行环境是相互物理隔离的,分别运行在不同的进程中。这种边界就是微服务之间的物理边界。


代码边界 主要用于微服务内的不同职能代码之间的隔离。微服务开发过程中会根据代码模型建立相应的代码目录,实现不同功能代码的隔离。由于领域模型与代码模型的映射关系,代码边界直接体现出业务边界。代码边界可以控制代码重组的影响范围,避免业务和服务之间的相互影响。微服务如果需要进行功能重组,只需要以聚合代码为单位进行重组就可以了。


正确理解微服务的边界


从上述内容中,我们知道了,按照DDD设计出来的逻辑边界和代码边界,让微服务架构演进变得不那么费劲了。


微服务的拆分可以参考领域模型,也可以参考聚合,因为聚合是可以拆分为微服务的最小单位的。但实施过程是否一定要做到逻辑边界与物理边界一致性呢?也就是说聚合是否也一定要设计成微服务呢?答案是不一定的,这里就涉及到微服务过度拆分的问题了。


微服务的过度拆分会使软件维护成本上升,比如:集成成本、发布成本、运维成本以及监控和定位问题的成本等。在项目建设初期,如果你不具备较强的微服务管理能力,那就不宜将微服务拆分过细。当我们具备一定的能力以后,且微服务内部的逻辑和代码边界也很清晰,你就可以随时根据需要,拆分出新的微服务,实现微服务的架构演进了。


当然,还要记住一点,微服务内聚合之间的服务调用和数据依赖需要符合高内聚松耦合的设计原则和开发规范,否则你也不能很快完成微服务的架构演进。


总结


我们主要讨论了微服务架构设计中的各种边界在架构演进中的作用。


逻辑边界: 微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。


物理边界: 微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。


代码边界: 不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。


通过以上边界,我们可以让业务能力高内聚、代码松耦合,且清晰的边界,可以快速实现微服务代码的拆分和组合,轻松实现微服务架构演进。但有一点一定要格外注意,边界清晰的微服务,不是大单体向小单体的演进。


作者:架构狂人
来源:mdnice.com/writing/2e64f8fdf9cb4213894a57d4e7a8a904
收起阅读 »

分布式架构关键设计10问

一、选择什么样的分布式数据库? 分布式架构下的数据应用场景远比集中式架构复杂,会产生很多数据相关的问题。谈到数据,首先就是要选择合适的分布式数据库。 分布式数据库大多采用数据多副本的方式,实现数据访问的高性能、多活和容灾。目前主要有三种不同的分布式数据库...
继续阅读 »

一、选择什么样的分布式数据库?


分布式架构下的数据应用场景远比集中式架构复杂,会产生很多数据相关的问题。谈到数据,首先就是要选择合适的分布式数据库。


分布式数据库大多采用数据多副本的方式,实现数据访问的高性能、多活和容灾。目前主要有三种不同的分布式数据库解决方案。它们的主要差异是数据多副本的处理方式和数据库中间件。


1. 一体化分布式数据库方案


它支持数据多副本、高可用。多采用Paxos协议,一次写入多数据副本,多数副本写入成功即算成功。代表产品是OceanBase和高斯数据库。


2. 集中式数据库+数据库中间件方案


它是集中式数据库与数据库中间件结合的方案,通过数据库中间件实现数据路由和全局数据管理。数据库中间件和数据库独立部署,采用数据库自身的同步机制实现主副本数据的一致性。集中式数据库主要有MySQL和PostgreSQL数据库,基于这两种数据库衍生出了很多的解决方案,比如开源数据库中间件MyCat+MySQL方案,TBase(基于PostgreSQL,但做了比较大的封装和改动)等方案。


3. 集中式数据库+分库类库方案


它是一种轻量级的数据库中间件方案,分库类库实际上是一个基础JAR包,与应用软件部署在一起,实现数据路由和数据归集。它适合比较简单的读写交易场景,在强一致性和聚合分析查询方面相对较弱。典型分库基础组件有ShardingSphere。


小结: 这三种方案实施成本不一样,业务支持能力差异也比较大。一体化分布式数据库主要由互联网大厂开发,具有超强的数据处理能力,大多需要云计算底座,实施成本和技术能力要求比较高。集中式数据库+数据库中间件方案,实施成本和技术能力要求适中,可满足中大型企业业务要求。第三种分库类库的方案可处理简单的业务场景,成本和技能要求相对较低。在选择数据库的时候,我们要考虑自身能力、成本以及业务需要,从而选择合适的方案。


二、如何设计数据库分库主键?


选择了分布式数据库,第二步就要考虑数据分库,这时分库主键的设计就很关键了。


与客户接触的关键业务,我建议你以客户ID作为分库主键。这样可以确保同一个客户的数据分布在同一个数据单元内,避免出现跨数据单元的频繁数据访问。跨数据中心的频繁服务调用或跨数据单元的查询,会对系统性能造成致命的影响。


将客户的所有数据放在同一个数据单元,对客户来说也更容易提供客户一致性服务。而对企业来说,“以客户为中心”的业务能力,首先就要做到数据上的“以客户为中心”。


当然,你也可以根据业务需要用其它的业务属性作为分库主键,比如机构、用户等。


三、数据库的数据同步和复制


在微服务架构中,数据被进一步分割。为了实现数据的整合,数据库之间批量数据同步与复制是必不可少的。数据同步与复制主要用于数据库之间的数据同步,实现业务数据迁移、数据备份、不同渠道核心业务数据向数据平台或数据中台的数据复制、以及不同主题数据的整合等。


传统的数据传输方式有ETL工具和定时提数程序,但数据在时效性方面存在短板。分布式架构一般采用基于数据库逻辑日志增量数据捕获(CDC)技术,它可以实现准实时的数据复制和传输,实现数据处理与应用逻辑解耦,使用起来更加简单便捷。


现在主流的PostgreSQL和MySQL数据库外围,有很多数据库日志捕获技术组件。CDC也可以用在领域事件驱动设计中,作为领域事件增量数据的获取技术。


四、跨库关联查询如何处理?


跨库关联查询是分布式数据库的一个短板,会影响查询性能。在领域建模时,很多实体会分散到不同的微服务中,但很多时候会因为业务需求,它们之间需要关联查询。


关联查询的业务场景包括两类:第一类是基于某一维度或某一主题域的数据查询,比如基于客户全业务视图的数据查询,这种查询会跨多个业务线的微服务;第二类是表与表之间的关联查询,比如机构表与业务表的联表查询,但机构表和业务表分散在不同的微服务。


如何解决这两类关联查询呢?


对于第一类场景,由于数据分散在不同微服务里,我们无法跨多个微服务来统计这些数据。你可以建立面向主题的分布式数据库,它的数据来源于不同业务的微服务。采用数据库日志捕获技术,从各业务端微服务将数据准实时汇集到主题数据库。在数据汇集时,提前做好数据关联(如将多表数据合并为一个宽表)或者建立数据模型。面向主题数据库建设查询微服务。这样一次查询你就可以获取客户所有维度的业务数据了。你还可以根据主题或场景设计合适的分库主键,提高查询效率。


对于第二类场景,对于不在同一个数据库的表与表之间的关联查询场景,你可以采用小表广播,在业务库中增加一张冗余的代码副表。当主表数据发生变化时,你可以通过消息发布和订阅的领域事件驱动模式,异步刷新所有副表数据。这样既可以解决表与表的关联查询,还可以提高数据的查询效率。


五、如何处理高频热点数据?


对于高频热点数据,比如商品、机构等代码类数据,它们同时面向多个应用,要有很高的并发响应能力。它们会给数据库带来巨大的访问压力,影响系统的性能。


常见的做法是将这些高频热点数据,从数据库加载到如Redis等缓存中,通过缓存提供数据访问服务。这样既可以降低数据库的压力,还可以提高数据的访问性能。


另外,对需要模糊查询的高频数据,你也可以选用ElasticSearch等搜索引擎。


缓存就像调味料一样,投入小、见效快,用户体验提升快。


六、前后序业务数据的处理


在微服务设计时你会经常发现,某些数据需要关联前序微服务的数据。比如:在保险业务中,投保微服务生成投保单后,保单会关联前序投保单数据等。在电商业务中,货物运输单会关联前序订单数据。由于关联的数据分散在业务的前序微服务中,你无法通过不同微服务的数据库来给它们建立数据关联。


如何解决这种前后序的实体关联呢?


一般来说,前后序的数据都跟领域事件有关。你可以通过领域事件处理机制,按需将前序数据通过领域事件实体,传输并冗余到当前的微服务数据库中。


你可以将前序数据设计为实体或者值对象,并被当前实体引用。在设计时你需要关注以下内容:如果前序数据在当前微服务只可整体修改,并且不会对它做查询和统计分析,你可以将它设计为值对象;当前序数据是多条,并且需要做查询和统计分析,你可以将它设计为实体。


这样,你可以在货物运输微服务,一次获取前序订单的清单数据和货物运输单数据,将所有数据一次反馈给前端应用,降低跨微服务的调用。如果前序数据被设计为实体,你还可以将前序数据作为查询条件,在本地微服务完成多维度的综合数据查询。只有必要时才从前序微服务,获取前序实体的明细数据。这样,既可以保证数据的完整性,还可以降低微服务的依赖,减少跨微服务调用,提升系统性能。


七、数据中台与企业级数据集成


分布式微服务架构虽然提升了应用弹性和高可用能力,但原来集中的数据会随着微服务拆分而形成很多数据孤岛,增加数据集成和企业级数据使用的难度。你可以通过数据中台来实现数据融合,解决分布式架构下的数据应用和集成问题。


你可以分三步来建设数据中台。


第一,按照统一数据标准,完成不同微服务和渠道业务数据的汇集和存储,解决数据孤岛和初级数据共享的问题。


第二,建立主题数据模型,按照不同主题和场景对数据进行加工处理,建立面向不同主题的数据视图,比如客户统一视图、代理人视图和渠道视图等。


第三,建立业务需求驱动的数据体系,支持业务和商业模式创新。


数据中台不仅限于分析场景,也适用于交易型场景。你可以建立在数据仓库和数据平台上,将数据平台化之后提供给前台业务使用,为交易场景提供支持。


八、BFF与企业级业务编排和协同


企业级业务流程往往是多个微服务一起协作完成的,每个单一职责的微服务就像积木块,它们只完成自己特定的功能。那如何组织这些微服务,完成企业级业务编排和协同呢?


你可以在微服务和前端应用之间,增加一层BFF微服务(Backend for Frontends)。 BFF主要职责是处理微服务之间的服务组合和编排,微服务内的应用服务也是处理服务的组合和编排,那这二者有什么差异呢?


BFF位于中台微服务之上,主要职责是微服务之间的服务协调; 应用服务主要处理微服务内的服务组合和编排。 在设计时我们应尽可能地将可复用的服务能力往下层沉淀,在实现能力复用的同时,还可以避免跨中心的服务调用。


BFF像齿轮一样,来适配前端应用与微服务之间的步调。它通过Façade服务适配不同的前端,通过服务组合和编排,组织和协调微服务。BFF微服务可根据需求和流程变化,与前端应用版本协同发布,避免中台微服务为适配前端需求的变化,而频繁地修改和发布版本,从而保证微服务核心领域逻辑的稳定。


如果你的BFF做得足够强大,它就是一个集成了不同中台微服务能力、面向多渠道应用的业务能力平台。


九、分布式事务还是事件驱动机制?


分布式架构下,原来单体的内部调用,会变成分布式调用。如果一个操作涉及多个微服务的数据修改,就会产生数据一致性的问题。数据一致性有强一致性和最终一致性两种,它们实现方案不一样,实施代价也不一样。


对于实时性要求高的强一致性业务场景,你可以采用分布式事务,但分布式事务有性能代价,在设计时我们需平衡考虑业务拆分、数据一致性、性能和实现的复杂度,尽量避免分布式事务的产生。


领域事件驱动的异步方式是分布式架构常用的设计方法,它可以解决非实时场景的数据最终一致性问题。基于消息中间件的领域事件发布和订阅,可以很好地解耦微服务。通过削峰填谷,可以减轻数据库实时访问压力,提高业务吞吐量和处理能力。你还可以通过事件驱动实现读写分离,提高数据库访问性能。对最终一致性的场景,我建议你采用领域事件驱动的设计方法。


十、多中心多活的设计


分布式架构的高可用主要通过多活设计来实现,多中心多活是一个非常复杂的工程,下面我主要列出以下几个关键的设计。


1.选择合适的分布式数据库。数据库应该支持多数据中心部署,满足数据多副本以及数据底层复制和同步技术要求,以及数据恢复的时效性要求。


2.单元化架构设计。将若干个应用组成的业务单元作为部署的基本单位,实现同城和异地多活部署,以及跨中心弹性扩容。各单元业务功能自包含,所有业务流程都可在本单元完成;任意单元的数据在多个数据中心有副本,不会因故障而造成数据丢失;任何单元故障不影响其它同类单元的正常运行。单元化设计时我们要尽量避免跨数据中心和单元的调用。


3.访问路由。访问路由包括接入层、应用层和数据层的路由,确保前端访问能够按照路由准确到达数据中心和业务单元,准确写入或获取业务数据所在的数据库。


4.全局配置数据管理。实现各数据中心全局配置数据的统一管理,每个数据中心全局配置数据实时同步,保证数据的一致性。


总结


企业级分布式架构的实施是一个非常复杂的系统工程,涉及到非常多的技术体系和方法。今天我列的10个关键的设计领域,每个领域其实都非常复杂,需要很多的投入和研究。在实施的时候,你和你的公司要结合自身情况来选择合适的技术组件和实施方案。


作者:架构狂人
来源:mdnice.com/writing/efcac6bf632b4172903c8a14c2e1f0f4
收起阅读 »

纯C文件推理Llama 2

web
这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的...
继续阅读 »

这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的6层6头模型(约15M个参数)推断速度约为每秒100个令牌;在M1 MacBook Air上推断速度也差不多。作者有些惊喜地发现,采用这种简单方法,可以以高度交互的速度运行相当大的模型(几千万个参数)。




参考文献:
https://github.com/karpathy/llama2.c

作者:阿升
来源:mdnice.com/writing/6f98f171b14e4050bf627afe59ccb82a

收起阅读 »

也许跟大家不太一样,我是这么用TypeScript来写前端的

web
一、当前一些写前端的骚操作 先罗列一下见到过的一些写法吧:) 1. interface(或Type)一把梭 掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样: type User = { ...
继续阅读 »

一、当前一些写前端的骚操作


先罗列一下见到过的一些写法吧:)


1. interface(或Type)一把梭


掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样:


type User = {
nickname: string
avatar?: string
age: number
}

interface User {
nickname: string
avatar?: string
age: number
}

然后其他方法限制下入参类型,搞定,我掌握了 TypeScript 了,工资不得给我涨3000???



这里说明一下, 我司 不允许 直接使用 interface type 来定义非装饰器参数和配置性参数之外其他 任何数据类型



2. 类型体操整花活


要么把属性整成只读了,要么猪狗类型联合了,要么猪尾巴搞丢了,要么牛真的会吹牛逼了。


类型体操确实玩出了很多花活。 昨天说过了:TypeScript最好玩的就是类型体操, 也恰好是最不应该出现的东西


3. hook 的无限神话


不知道什么时候开始,hook 越来越流行。 听说不会写 hook 的前端程序员,已经算不上高阶程序员了, 不 use 点啥都展示不出牛逼的水平。


4. axios 拦截大法好


随便搜索一下 axios 的文章, 没有 拦截器 这个关键词的文章都算不上 axios 的高端用法了。


二、 我们一些不太一样的前端骚操作


昨天的文章有提到一些关于在前端使用 装饰器 来实现一些基于配置的需求实现, 今天其实想重点聊一聊如何在前端优雅的面向对象。


写过 JavaSpringBootJPA 等代码的后端程序员应该非常熟悉的一些概念:



  • 抽象: 万物都可抽象成相关的类和对象

  • 面向对象: 继承、封装、多态等特性的面向对象设计思维

  • 切面: 没有什么是切一刀解决不了的,如果一刀不行, 那就多来几刀。

  • 注解: 没有什么常量是不能使用注解来配置的, 也没有什么注解是切面想切还能躲得掉的

  • 反射: 没有什么是暴力拿取会失败的, 即使失败也没有异常是丢不出来的

  • 实体: 没有什么是不能抽象到实体上的, 万物皆唯一。

  • 很多: 还有很多,以上描述比较主观和随意。


于是我们开始把后端思维往前端来一个个的转移:)


1. 抽象和面向对象


与后端的交互数据对象、 请求的API接口都给抽象到具体的类上去,于是有了:



  • Service API请求类


abstract class AbstractService{
// 实现一个抽象属性 让子类们实现
abstract baseUrl!: string

// 再实现一些通用的 如增删改查之类的网络请求
// save()

// getDetail()

// deleteById()

// select()

// page()

// disabled()

// ......
}


  • Entity 数据实体基类


abstract class AbstractBaseEntityextends AbstractService> {
abstract service!: AbstractService

// 任何数据都是唯一的 ID
id!: number

// 再来实现一些数据实体的更新和删除方法
save(){
await service.save(this.toJson())
Notify.success("新增成功")
}

delete(){
service.deleteById(this.id)
Notify.success("删除成功")
}

async validate(scene: EntityScene):Promise<void>{
return new Promise((resolve,reject)=>{
// 多场景的话 可以Switch
if(...){
Notify.error("XXX校验失败")
reject();
}
resove();
})
}
// ......
}



  • 子类的实现:)


class UserEntity extends AbstractUserEntity<UserService>{
service = new UserService()

nickname!: string
age!: number
avatar?: string

// 用户是否成年人
isAdult(): boolean{
return this.age >= 18
}

async validate(scene: EntityScene): Promise<void> {
return new Promise((resove,reject)=>{
if(!this.isAdult()){
Notify.error("用户未成年, 请确认年龄")
reject();
}
await super.validate(scene)
})
}

}


  • View 视图调用


<template>
<el-input v-model="user.nickname"/>
<el-button @click="onUserSave()">创建用户el-button>
template>
<script setup lang="ts">
const user = ref(new UserEntity())
async function onUserSave(){
await user.validate(EntityScene.SAVE);
await user.save()
}
script>

2. 装饰器/切面/反射


装饰器部分的话,昨天的文章有提到一些了,今天主要所说反射和切面部分。


TypeScript 中, 其实装饰器本身就可以理解为一个切面了, 这里与 Java 中还是有很多不同的, 但概念和思维上是基本一致的。


反射 ReflectTypeScript 中比较坑的一个存在, 目前主要是依赖 reflect-metadata 这个第三方库来实现, 将一些元数据存储到 metadata 中, 在需要使用的时候通过反射的方式来获取。 可以参考这篇文章:TypeScript 中的元数据以及 reflect-metadata 实现原理分析


在实际使用中, 我们早前用的是 class-transformer 这个库, 之前我对这个库的评价应该是非常高的: “如果没有 class-transformer 这个库, TypeScript 狗都不写。”


确实很棒的一个库,但是在后来,我们写了个通用的内部框架, 为了适配 微信小程序端 以及 uniapp 端, 再加上有一些特殊的业务功能以及 class-transfromer 的写法和命名方式我个人不太喜欢的种种原因, 我们放弃了这个库, 但我们仿照了它的思想重新实现了一个内部使用的库,做了一些功能的阉割和新特性的添加。


核心功能的一些说明




  • 通过反射进行数据转换



    如将后端API返回的数据按照前端的数据结构强制进行转换, 当后端数据返回乱七八糟的时候,保证前端数据在使用中不会出现任何问题, 如下 demo



    class UserEntity {
    @Type(String) phone!: string;
    @Type(RoleEntity) roleInfo!: RoleEntity:
    @Type(DeptEntity) @List @Default([]) deptInfoList!: DeptEntity[]
    @Type(Boolean) @Default(false) isDisabled!: boolean
    }



  • 通过反射进行配置的存储和读取



    这个在昨天的文章中有讲到一部分, 比如配置表单、表格、搜索框、权限 等





3. 再次强调面向对象


为了整个前端项目的工程化、结构化、高度抽象化,这里不得不再次强调面向对象的设计:)




  • 这是个拼爹的社会



    一些通用的功能,一旦有复用的可能, 都可以考虑和尝试让其父类进行实现, 如需要子类传入一些特性参数时, 可以使用抽象方法或抽象属性(这可是Java中没有的)来传入父类实现过程中需要的特性参数。





  • 合理的抽象分层



    将一些特性按照不同的抽象概念进行组合与抽离,实现每个类的功能都是尽可能不耦合,实现类的单一职责。如存在多继承, 在考虑实现类的实现成本前提下,可考虑抽象到接口 interface 中。





  • 还有很多,有空再一一列举




4. 严格但又有趣的 tsdoc


我们先来看一些注释的截图吧:)








一些详细的注释、弃用的方法、选填的参数、传入参数后可能影响或依赖的其他参数,在注释里写好玩的 emoji或者图片,甚至是 直接在注释里写调用 demo, 让调用方可以很轻松愉快的对接调用, 玩归玩, 确实对整体项目的质量有很大的帮助。


三、 写在最后


中午跟同事吃饭聊了聊现在国内大前端的一个状态, 当时聊到一个关键词 舒适区, 还有前端整个技术栈过于灵活的一些优缺点, 几个大老爷们都发出了一些感慨, 如果前端能够更标准化一些, 像 Java 一样, 说不定前端还能上升几个高度。


我们基于今天文章里的一些设计写了一些DEMO,但目前不太方便直接开源,如果有兴趣,可以私聊我获取代码链接。


是的, 我还是那个 Java 仔, 是, 也不仅仅是。

作者:Hamm
来源:juejin.cn/post/7259562014417813564

收起阅读 »