注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Redis只用来做缓存?来认识一下它其他强大的能力吧。

当今互联网应用中,随着业务的发展,数据量越来越大,查询效率越来越高,对于时序数据的存储、查询和分析需求也越来越强烈,这时候 Redis 就成为了首选的方案之一。 Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,每种数据结构都具备不同的...
继续阅读 »

当今互联网应用中,随着业务的发展,数据量越来越大,查询效率越来越高,对于时序数据的存储、查询和分析需求也越来越强烈,这时候 Redis 就成为了首选的方案之一。


Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,每种数据结构都具备不同的特性,可以满足不同的业务需求。其中,有序集合的 score 可以存储时间戳,非常适合用于存储时序数据,例如监控指标、日志、统计数据、报表等。下面举几个时序数据场景例子:



  1. 监控指标:


假设我们有一个服务,名为 my_service,需要监控它的请求响应时间。我们可以使用 Redis 有序集合来存储数据,每个请求的响应时间作为 value,请求的时间戳作为 score。示例如下:


> ZADD requests:my_service 1613115560 350
(integer) 1
> ZADD requests:my_service 1613115570 450
(integer) 1
> ZADD requests:my_service 1613115580 550
(integer) 1

这些命令向名为 requests:my_service 的有序集合中添加了 3 条数据,分别是 2021 年 2 月 12 日 10:19:20 的请求响应时间为 350ms,10:19:30 的请求响应时间为 450ms,10:19:40 的请求响应时间为 550ms。


接下来,我们来看一下如何使用 Redis 命令查询这些监控指标的数据。下面的命令会返回 requests:my_service 有序集合内所有数据:


> ZRANGE requests:my_service 0 -1 WITHSCORES
1) "350"
2) "1613115560"
3) "450"
4) "1613115570"
5) "550"
6) "1613115580"

命令执行结果表示,数据按照 score 排序,其中 score 是时间戳(单位为秒),value 是请求响应时间(单位为毫秒)。同时,使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的监控数据,例如:


> ZRANGEBYSCORE requests:my_service 1613115570 1613115580 WITHSCORES
1) "450"
2) "1613115570"
3) "550"
4) "1613115580"

这条命令返回了 requests:my_service 有序集合中在时间戳 1613115570 到 1613115580 之间的所有数据。



  1. 日志:


假设我们要存储的日志是一条指定格式的字符串,包含时间戳和日志内容。使用 Redis 列表存储日志数据,每次写入新日志时可以使用 Redis 列表的 rpush 命令将数据写入列表的尾部。示例如下:


> RPUSH logs:my_logs 2021-02-12 10:30:00 INFO message 1
(integer) 1
> RPUSH logs:my_logs 2021-02-12 10:30:01 ERROR message 2
(integer) 2
> RPUSH logs:my_logs 2021-02-12 10:30:02 WARN message 3
(integer) 3

这些命令向名为 logs:my_logs 的列表尾部添加 3 条数据,分别是 2021 年 2 月 12 日 10:30:00 的 INFO 级别消息,10:30:01 的 ERROR 级别消息和 10:30:02 的 WARN 级别消息。


接下来,我们来看一下如何使用 Redis 命令查询这些日志数据。下面的命令会返回 logs:my_logs 列表内所有数据:


> LRANGE logs:my_logs 0 -1
1) "2021-02-12 10:30:00 INFO message 1"
2) "2021-02-12 10:30:01 ERROR message 2"
3) "2021-02-12 10:30:02 WARN message 3"

命令执行结果表示,数据按照插入顺序排序,从列表头部开始遍历。使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的日志数据,例如:


> ZRANGEBYSCORE logs:my_logs 1613115570 1613115580
1) "2021-02-12 10:30:01 ERROR message 2"

这条命令返回了 logs:my_logs 列表中在时间戳 1613115570 到 1613115580 之间的日志数据,但因为日志数据并没有具体的 time stamp 做 score,所以这个例子只是演示这个命令的用法,实际上应该使用有序集合去查询时间区间内的日志数据。



  1. 统计数据:


假设我们要存储的统计数据是一些具体业务相关的计数器,例如每分钟用户访问量。我们可以使用 Redis 有序集合来存储统计数据,key 是计数器名称,score 是时间戳,value 是具体的计数值(例如访问次数)。示例如下:


> ZADD visits 1613167800 100
(integer) 1
> ZADD visits 1613167860 120
(integer) 1
> ZADD visits 1613167920 150
(integer) 1

这些命令向名为 visits 的有序集合中添加了 3 条数据,分别是 2021 年 2 月 12 日 23:30:00 的访问次数为 100,23:31:00 的访问次数为 120,23:32:00 的访问次数为 150。


接下来,我们来看一下如何使用 Redis 命令查询这些统计数据。下面的命令会返回 visits 有序集合内所有数据:


> ZRANGE visits 0 -1 WITHSCORES
1) "100"
2) "1613167800"
3) "120"
4) "1613167860"
5) "150"
6) "1613167920"

命令执行结果表示,数据按照 score 排序,其中 score 是时间戳(单位为秒),value 是访问次数。使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的统计数据,例如:


> ZRANGEBYSCORE visits 1613167860 1613167920 WITHSCORES
1) "120"
2) "1613167860"
3) "150"
4) "1613167920"

这条命令返回了 visits 有序集合中在时间戳 1613167860 到 1613167920 之间的所有数据。


使用 Redis 有序集合中的另一个常见场景是计算 TopN,例如找出访问次数最多的前 10 个计数器,可以使用命令 ZREVRANGE visits 0 9 WITHSCORES,它返回 visits 有序集合中前 10 个元素,按照 value 从大到小排列,并且返回每个元素的 score。


需求实践:


这是一个实时监控系统,主要用于记录和统计服务发生的错误情况,以便在错误数量超过预设阈值时发出警告信息。


系统每秒钟生成随机错误数据,并将它们存储到 Redis 数据库中。每隔 10 秒钟,系统会从 Redis 数据库中聚合最近一分钟内的错误数据,并按照服务名和错误类型进行统计计算。如果某个服务的错误数量超过预设阈值,系统会输出一条警告信息提示用户。


整个系统的目标是帮助用户及时了解每个服务的错误情况,以便及时采取相应的措施,保障服务的稳定性和可靠性。


代码示例:


模拟接口服务异常数据


package com.example.demo.redis;

import redis.clients.jedis.Jedis;
import java.util.*;

public class DataGenerator {
// 定义服务列表
private static final List SERVICES = Arrays.asList("service1", "service2", "service3");
// 定义错误列表
public static final List ERRORS = Arrays.asList("invalid_param", "timeout", "unknown_error");

/**
* 生成数据
*
*
@param total 数据总数
*
@param jedis Redis 客户端连接
*/

public static void generateData(int total, Jedis jedis) {
Random rand = new Random(); // 初始化随机数生成器
long currentTimestamp = System.currentTimeMillis() / 1000; // 获取当前时间戳,精确到秒
long startTimestamp = currentTimestamp - 60; // 计算起始时间戳,为当前时间戳减去 60 秒

for (int i = 0; i < total; i++) { // 循环 total 次,生成 total 条数据
String service = SERVICES.get(rand.nextInt(SERVICES.size())); // 随机选择一个服务
String error = ERRORS.get(rand.nextInt(ERRORS.size())); // 随机选择一个错误
long timestamp = startTimestamp + rand.nextInt(60); // 生成一个随机时间戳,精确到秒,范围为起始时间戳到当前时间戳
int count = 1;
String item = String.format("%s:%s:%d:%d", service, error, timestamp, count);
jedis.zadd("error_data", timestamp, item); // 将错误数据存储到 Redis 数据库中
}
}
}


聚合异常数据,达到阈值告警


package com.example.demo.redis;

import redis.clients.jedis.Jedis;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class DataAggregator {
private static final String REDIS_HOST = "localhost"; // Redis 主机名
private static final int REDIS_PORT = 6379; // Redis 端口号
private static final int THRESHOLD = 100; // 预设阈值,当错误数量超过该阈值时触发警告

public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // 创建一个只有一个线程的定时任务执行程序
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT); // 创建 Redis 客户端连接

scheduler.scheduleAtFixedRate(() -> {
// 并发情况下,线程会阻塞
synchronized (jedis) {
DataGenerator.generateData(20, jedis); // 生成随机错误数据,并将其存储到 Redis 数据库中
}
}, 0, 1, TimeUnit.SECONDS); // 定时任务间隔为 1 秒钟

scheduler.scheduleAtFixedRate(() -> { // 定时任务逻辑
synchronized (jedis) {
long currentTimestamp = System.currentTimeMillis() / 1000; // 获取当前时间戳,精确到秒
long startTimestamp = currentTimestamp - 60; // 计算起始时间戳,为当前时间戳减去 60 秒
Set data = jedis.zrangeByScore("error_data", startTimestamp, currentTimestamp); // 使用 zrange 命令获取指定时间范围内的数据

Map> countMap = new HashMap<>(); // 用于记录聚合后的服务和错误数量信息
for (String item : data) { // 遍历所有错误数据
String[] parts = item.split(":"); // 以冒号为分隔符,将错误数据分割为部分
String service = parts[0]; // 获取服务名
String error = parts[1]; // 获取错误类型
long timestamp = Long.parseLong(parts[2]); // 获取时间戳
int count = Integer.parseInt(parts[3]); // 获取错误数量

if (timestamp < startTimestamp) { // 如果时间戳早于起始时间戳,则跳过该数据
continue;
}

Map serviceCountMap = countMap.computeIfAbsent(service, k -> new HashMap<>()); // 获取指定服务的错误数量信息
serviceCountMap.put(error, serviceCountMap.getOrDefault(error, 0) + count); // 更新指定服务和错误类型的错误数量信息
}

List alerts = new ArrayList<>(); // 用于存储警告信息
for (String service : countMap.keySet()) { // 遍历服务名列表
Map serviceCountMap = countMap.get(service); // 获取服务和错误数量信息
int totalErrors = 0;
for (String error : serviceCountMap.keySet()) { // 遍历错误列表
int count = serviceCountMap.get(error); // 获取错误数量
totalErrors += count;
}
if (totalErrors > THRESHOLD) { // 如果错误数量超过预设阈值
alerts.add(service + " has too many errors: " + serviceCountMap.keySet() + ", count: " + totalErrors); // 将该服务名添加到警告信息列表中
}
}
if (!alerts.isEmpty()) { // 如果警告信息列表不为空
System.out.println(String.join("\n", alerts)); // 打印警告信息
}
}
}, 0, 10, TimeUnit.SECONDS); // 定时任务间隔为 10 秒

// 关闭 Redis 连接
jedis.close();
}
}


以上代码可正常运行,有疑问可以交流~~


作者:程序员的思考与落地
来源:juejin.cn/post/7219669309537484837
收起阅读 »

分库分表,真的有必要吗?

分库分表,真的有必要吗? 哈喽,大家好,我是janker。 关于数据库分库分表的面试题已经是烂大街了,面经小册各路神仙的经验分享也是不绝于耳。当然现有的技术解决方案已经是很成熟了。 但是想要使用的得心应手,首先应该搞清楚三个问题? 为什么使用分库分表? 何时...
继续阅读 »

分库分表,真的有必要吗?


哈喽,大家好,我是janker。


关于数据库分库分表的面试题已经是烂大街了,面经小册各路神仙的经验分享也是不绝于耳。当然现有的技术解决方案已经是很成熟了。


但是想要使用的得心应手,首先应该搞清楚三个问题?



  • 为什么使用分库分表?

  • 何时使用分库分表?

  • 如何分库分表?


为什么使用分库分表?


答案很简单:当数据库出现性能瓶颈。顾名思义就是数据库扛不住了。


数据库出现瓶颈,对外表现有以下几个方面?



  1. 高并发场景下,大量请求阻塞,大量请求都需要操作数据库,导致连接数不够了,请求处于阻塞状态。

  2. SQL操作变慢(慢SQL增多)如果数据库中存在一张上亿数据量的表,一条 SQL 没有命中索引会全表扫描,这个查询耗时会非常久。

  3. 随着业务流量变大存储出现问题,单库数据量越来越大,给存储造成巨大压力。


从机器角度,性能瓶颈不外乎就是CPU、磁盘、内存、网络这些,要解决性能瓶颈最简单粗暴的方式就是提升机器性能,但是通过这种方式投入产出比往往不高,也不划算,所以重点还是要从软件层面去解决问题。


数据库相关优化方案


数据库优化方案很多,主要分为两大类:软件层面、硬件层面。


软件层面包括:SQL 调优、表结构优化、读写分离、数据库集群、分库分表等;


硬件层面主要是增加机器性能。


分库分表其实不是数据库优化方案的最终解决办法,一般来说说能用优化SQL、表结构优化、读写分离等手段解决问题,就不要分库分表,因为分库分表会带来更多需要解决的问题,比如说分布式事务,查询难度增大等。


何时使用分库分表?


什么时候我们才会选择分库分表?前面已经说了,除了分库分表以外那些软件手段搞不定的时候,我们才会选择分库分表。


我们心中可能会有这些疑问?



  1. 使用分库分表,我们的评判依据是什么?

  2. 一张表存储了多少数据的时候,才需要考虑分库分表?

  3. 数据增长速度很快,每天产生多少数据,才需要考虑做分库分表?


阿里巴巴开发手册有推荐的思路:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。


注意:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。


如何分库分表?


当前针对分库分表有很多解决方案。这里分两个方面来展开说说:分库 和 分表。


分库


很多项目前期为了快速迭代,多个应用公用一套数据库,随着业务量增加,系统访问压力增大,系统拆分就势在必行。


为了保证业务平滑,系统架构重构也是分了几个阶段进行。


多应用单数据库


第一个阶段将商城系统单体架构按照功能模块拆分为子服务,比如:Portal 服务、用户服务、订单服务、库存服务等。


image-20230107232802486


多应用单数据库如上图,多个服务共享一个数据库,这样做的目的是底层数据库访问逻辑可以不用动,将影响降到最低。


多应用多数据库


随着业务推广力度加大,数据库终于成为了瓶颈,这个时候多个服务共享一个数据库基本不可行了。我们需要将每个服务相关的表拆出来单独建立一个数据库,这其实就是“分库”了。


单数据库的能够支撑的并发量是有限的,拆成多个库可以使服务间不用竞争,提升服务的性能。


image-20230107233441875


从一个大的数据中分出多个小的数据库,每个服务都对应一个数据库,这就是系统发展到一定阶段必要要做的“分库”操作。


分表


说完了分库,那什么时候才会分表呢?


如果系统处于高速发展阶段,拿商城系统来说,一天下单量可能几十万,那数据库中的订单表增长就特别快,增长到一定阶段数据库查询效率就会出现明显下降。


因此当表数据增长过快,根据阿里巴巴开发规范中超过500w的数据就要考虑分表了,当然这只是一个经验值,具体要不要分表还要看业务考虑未来三年的一个业务增量。


如何分表?


分表有几个维度,一是水平切分和垂直切分,二是单库内分表和多库内分表。


水平拆分和垂直拆分


拿商品表来说,表中分为几类属性:一类是基础属性,例如:商品名称、通用名,商品编码等信息。二类是规格属性:尺寸、型号等信息。三类是拓展属性:描述商品特征的一些属性。我们可以将其拆分为基础信息表、拓展属性表、属性信息表等。这几张表结构不同并且相互独立。但是从这个角度没有解决因为数据量过大而带来的性能问题,因此我们还需要继续做水平拆分。


image-20230108161956743


水平拆分表的方法很多种,比如说1w条数据,我们拆分为两个表,id 为基数的放在user1,id为偶数的放在user2中,这样的拆分方式就是水平拆分。


其他水平拆分的方式也很多,除了上面按照 id 来拆分,还可以按照时间维度拆分,比如订单表,可以按照每日、每月等进行拆分。



  • 每日表:只存储当天你的数据。

  • 每月表:可以起一个定时任务将前一天的数据全部迁移到当月表。

  • 历史表:同样可以用定时任务把时间超过 30 天的数据迁移到 history表。


总结一下水平拆分和垂直拆分的特点:



  • 垂直切分:基于表或者字段划分,表结构不同。

  • 水平拆分:基于数据划分,表结构相同,数据不同。根据表中字段的某一列特性,分而治之。


水平拆分也分两种拆分方式。单库内拆分和多库内拆分


单库内拆分和多库内拆分


拿针对用户表的拆分来举例,之前的单个用户表按照某种规则拆分为多个子表,多个子表存在于同一数据库中。比如下面用户表拆分为用户1表、用户2表。


image-20230108203705147


单库内拆分是在一个数据库中,将一张表拆分为多个子表,一定程度上可以解决单表查询的性能问题,但是也会遇到另外一个问题:但数据库的数据瓶颈。


所以在行业内更多的将子表拆分到多个数据库中,如下图,用户表拆分为4个子表,4个子表分两批存在两个不同的数据库中,根据一定的规则进行路由。


image-20230108204316330


多库拆分用一句话概括:主要是为了减少单张表数据量大小,解决单表数据量过大带来的性能问题。


但是分库分表也带来许多问题。


分库分表带来的问题


既然分库分表方案那么好,那我们是不是在项目初期就应该采用这种方案呢?莫慌,虽然分库分表解决了很多性能问题,但是同时也给系统带来了很多复杂性。下面我展开说说


1. 跨库关联查询


之前单体项目,我们想查询一些数据,无脑join就好了,只要数据模型设计没啥问题,关联查询起来其实还是很简单的。现在不一样了,分库分表后的数据可能不在一个数据库,那我们如何关联查询呢?


下面推荐几种方式去解决这个问题:



  1. 字段冗余:把需要关联的字段放到主表中,避免join操作,但是关联字段更新,也会引发冗余字段的更新;

  2. 数据抽象:通过ETL 等将数据汇总聚合,生成新的表;

  3. 全局表:一般是一些基础表可以在每个数据库都放一份;

  4. 应用层组装:将基础数据查出来,通过应用程序计算组装;

  5. 同特征的数据在一张表:举个例子:同一个用户的数据在同一个库中,比如说我们对订单按照用户id进行分表,订单主表、订单拓展信息表、跟订单有关联查询的表都按照用户id 进行分表,那么同一个用户的订单数据肯定在同一个数据库中,订单维度的关联查询就不存在跨库的问题了。


2. 分布式事务


单数据库我们可以用本地事务搞定,使用多数据库就只能通过分布式事务解决了。


常用的解决方案有:基于可靠消息(MQ)的最终一致性方案、二段式提交(XA)、柔性事务。


当然分布式事务相关开源项目推荐两个:SeataTX-LCN


比较推荐 Seata,阿里出品、大厂加持、如果需要企业级版本支持也是有的。


3. 排序、分表、函数计算问题


使用SQL 时,order bylimit 等关键字需要特殊处理,一般都是采用数据分片的思路:现在每个分片路由上执行函数、然后将每个分片路由的结果汇总再计算,然后得出最终结果。


开源的解决方案当然也有不少,比较推荐shardingsphere,无论是基于client 或者 基于数据库proxy的都有支持。


4. 分布式ID


既然分库分表了,主键id已经不能唯一确定我们的业务数据了,随之而来的就是分布式id,顾名思义就是在多个数据库多张表中唯一确定的ID。


常见的分布式Id 解决方案有:



  1. UUID

  2. 基于全局数据库自增的ID表

  3. 基于Redis缓存生成全局ID

  4. 雪花算法(Snowflake

  5. 百度uid-generator(雪花算法的变种)

  6. 美团Leaf(雪花算法的变种)

  7. 滴滴Tinyid


这些解决方案后面有专门的文章去介绍,这里不过多展开。


5. 多数据源


分库分表之后可能面临从多个数据库中获取数据,一般的解决方案有,基于 client 适配 和 基于 proxy 适配。


比较成熟并且常用的中间件有:



  • shardingsphere(apache顶级项目相当成熟,文档完善)

  • MyCat (社区不太活跃、不推荐)


总结


如果遇到数据库问题,建议不要着急分库分表。原则是:能不分库分表就不要做。先看下能否通过常规优化手段解决问题。


如上所述,引入分库分表虽然可以解决数据库瓶颈问题,但是也给系统带来巨大的复杂性,不是非必须不要使用。设计系统我们一向要本着高可拓展去设计,但是不要过度设计和超前设计。适合当前系统的设计才是最好的。


作者:爪哇干货分享
来源:juejin.cn/post/7186448714779590711
收起阅读 »

ElectronEgg 快速开发一个桌面应用

web
大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。 近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。 目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、E...
继续阅读 »

大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。


近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。


目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、ERP、娱乐、视频等领域客户端,请放心使用!


home.png


为什么使用


桌面软件(办公方向、 个人工具),仍然是未来十几年 PC 端需求之一,提高工作效率


electron 技术是流行趋势,QQ、百度翻译、阿里网盘、迅雷、有道云笔记 ......


开源


gitee:gitee.com/dromara/ele… 3900+


github:github.com/dromara/ele… 1200+


本次更新


3.8.0



  1. 【增加】新增 ee-bin exec 命令,支持自定义命令。

  2. 【增加】新增 ee-core jobs 配置,打开/关闭 messageLog。

  3. 【优化】优化 ee-core jsondb 异常处理。

  4. 【优化】优化 ee-core controller/services 异常捕获并写log。

  5. 【优化】优化 ee-bin loading 动画居中。

  6. 【优化】优化 electron-egg logo,优化mac图标,优化Linux系统图标。

  7. 【优化】优化 electron-egg loading 动画居中。

  8. 【升级】升级ee-core v2.6.0,升级ee-bin v1.3.0


下载


# gitee
git clone https://gitee.com/dromara/electron-egg.git

# github
git clone https://github.com/dromara/electron-egg.git

安装


# 设置国内镜像源(加速)
npm config set registry=https://registry.npmmirror.com

#如果下载electron慢,配置如下
npm config set electron_mirror=https://registry.npmmirror.com/-/binary/electron/

# 根目录,安装 electron 依赖
npm i

# 进入【前端目录】安装 frontend 依赖
cd frontend
npm i

运行项目


npm run start

example.png


用户案例


aw-3.png


p1.png


p3.png


更多


访问官网:electron-egg: 一个入门简单、跨平台、企业级桌面软件开发框架


作者:哆啦好梦
来源:juejin.cn/post/7292961931509186595
收起阅读 »

这个面试官真烦,问完合并又问拆分。

你好呀,我是歪歪。 这次来盘个小伙伴分享给我的一个面试题,他说面试的过程中面试官的问了一个比较开放的问题: 请谈谈你对于请求合并和分治的看法。 他觉得自己没有答的特别好,主要是没找到合适的角度来答题,跑来问我怎么看。 我能怎么看? 我也不知道面试官想问啥角...
继续阅读 »

你好呀,我是歪歪。


这次来盘个小伙伴分享给我的一个面试题,他说面试的过程中面试官的问了一个比较开放的问题:



请谈谈你对于请求合并和分治的看法。



他觉得自己没有答的特别好,主要是没找到合适的角度来答题,跑来问我怎么看。


我能怎么看?


我也不知道面试官想问啥角度啊。但是这种开放题,只要回答的不太离谱,应该都能混得过去。


比如,如果你回答一个:我认为合并和分治,这二者之间是辩证的关系,得用辩证的眼光看问题,它们是你中有我,我中有你~


那凉了,拿着简历回家吧。



我也想了一下,如果让我来回答这个问题,我就用这篇文章来回答一下。


有广义上的实际应用场景,也有狭义上的源代码体现对应的思想。


让面试官自己挑吧。



铺垫一下


首先回答之前肯定不能干聊,所以我们铺垫一下,先带入一个场景:热点账户。


什么是热点账户呢?


在第三方支付系统或者银行这类交易机构中,每产生一笔转入或者转出的交易,就需要对交易涉及的账户进行记账操作。


记账粗略的来说涉及到两个部分。



  • 交易系统记录这一笔交易的信息。

  • 账户系统需要增加或减少对应的账户余额。


如果对于某个账户操作非常的频繁,那么当我们对账户余额进行操作的时候,肯定就会涉及到并发处理的问题。


并发了怎么办?


我们可以对账户进行加锁处理嘛。但是这样一来,这个账户就涉及到频繁的加锁解锁操作。


虽然这样我们可以保证数据不出问题,但是随之带来的问题是随着并发的提高,账户系统性能下降。


极少数的账户在短时间内出现了极大量的余额更新请求,这类账户就是热点账户,就是性能瓶颈点。


热点账户是业界的一个非常常见的问题。


而且根据热点账户的特性,也可以分为不同的类型。


如果余额的变动是在频繁的增加,比如头部主播带货,只要一喊 321,上链接,那订单就排山倒海的来了,钱就一笔笔的打到账户里面去了。这种账户,就是非常多的人在给这个账户打款,频率非常高,账户余额一直在增加。


这种账户叫做“加余额频繁的热点账户”。


如果余额的变动是在频繁的减少,比如常见的某流量平台广告位曝光,这种属于扣费场景。


商家先充值一笔钱到平台上,然后平台给商家一顿咔咔曝光,接着账户上的钱就像是流水一样哗啦啦啦的就没了。


这种预充值,然后再扣减频繁的账户,这种账户叫做“减余额频繁的热点账户”。


还有一种,是加余额,减余额都很频繁的账户。


你细细的嗦一下,什么账户一边加一遍减呢,怎么感觉像是个二手贩子在左手倒右手呢?


这种账户一般不能细琢磨,琢磨多了,就有点灰色地带了,点到为止。



先说请求合并


针对“加余额频繁的热点账户”我们就可以采取请求合并的思路。


假设有个歪师傅是个正经的带货主播,在直播间穿着女装卖女装,我只要喊“321,上链接”姐妹们就开始冲了。



随着歪师傅生意越来越好,有的姐妹们就反馈下单越来越慢。


后来一分析,哦,原来是更新账户余额那个地方是个性能瓶颈,大量的单子都在这里排着队,等着更新余额。


怎么办呢?


针对这种情况,我们就可以把多笔调整账务余额的请求合并成一笔处理。



当记录进入缓冲流水记录表之后,我就可以告知姐妹下单成功了,虽然钱还没有真的到我的账户中来,但是你放心,有定时任务保证,钱一会就到账。


所以当姐妹们下单之后,我们只是先记录数据,并不去实际动账户。等着定时任务去触发记账,进行多笔合并一笔的操作。


比如下面的这个示意图:



对于歪师傅来说,实际有 6 个姐妹的支付记录,但是只有一次余额调整。


而我拿着这一次余额调整的账户流水,也是可以追溯到这 6 笔交易记录的详情。


这样的好处是吞吐量上来了,用户体验也好了。但是带来的弊端是余额并不是一个准确的值。


假设我们的定时任务是一小时汇总一次,那么歪师傅在后端看到的交易金额可能是一小时之前的数据。


但是歪师傅觉得没关系,总比姐妹们下不了单强。



如果我们把缓冲流水记录表看作是一个队列。那么这个方案抽象出来就是队列加上定时任务。


所以,请求合并的关键点也是队列加上定时任务。


除了我上面的例子外,比如还有 redis 里面的 mget,数据库里面的批量插入,这玩意不就是一个请求合并的真实场景吗?


比如 redis 把多个 get 合并起来,然后调用 mget。多次请求合并成一次请求,节约的是网络传输时间。


还有真实的案例是转账的场景,有的转账渠道是按次收费的,那么作为第三方公司,我们就可以把用户的请求先放到表里记录着,等一小时之后,一起汇总发起,假设这一小时内发生了 10 次转账,那么 10 次收费就变成了 1 次收费,虽然让客户等的稍微久了点,但还是在可以接受的范围内,这操作节约的就是真金白银了。


请求合并,说穿了,就这么一点事儿,一点也不复杂。


那么如果我在请求合并的前面,加上“高并发”这三个字...



首先不论是在请求合并的前面加上多么狂拽炫酷吊炸天的形容词,说的多么的天花乱坠,它也还是一个请求合并。


那么队列和定时任务的这个基础结构肯定是不会变的。


高并发的情况下,就是请求量非常的大嘛,那我们把定时任务的频率调高一点不就行了?


以前 100ms 内就会过来 50 笔请求,我每收到一笔就是立即处理了。


现在我们把请求先放到队列里面缓存着,然后每 100ms 就执行一次定时任务。


100ms 到了之后,就会有定时任务把这 100ms 内的所有请求取走,统一处理。


同时,我们还可以控制队列的长度,比如只要 50ms 队列的长度就达到了 50,这个时候我也进行合并处理。不需要等待到 100ms 之后。


其实写到这里,高并发的请求合并的答案已经出来了。


关键点就三个:



  • 一是需要借助队列加定时任务实现。

  • 二是控制定时任务的执行时间.

  • 三是控制缓冲队列的任务长度。


方案都想到了,把代码写出来岂不是很容易的事情。而且对于这种面试的场景图,一般都是讨论技术方案,而不太会去讨论具体的代码。


当讨论到具体的代码的时候,要么是对你的方案存疑,想具体的探讨一下落地的可行性。要么就是你答对了,他要准备从代码的交易开始衍生另外的面试题了。


总之,大部分情况下,不会在你给了一个面试官觉得错误的方案之后,他还和你讨论代码细节。你们都不在一个频道了,赶紧换题吧,还聊啥啊。


实在要往代码实现上聊,那么大概率他是在等着你说出一个框架:Hystrix。


其实这题,你要是知道 Hystrix,很容易就能给出一个比较完美的回答。


因为 Hystrix 就有请求合并的功能。


通过一个实际的例子,给大家演示一下。


假设我们有一个学生信息查询接口,调用频率非常的高。对于这个接口我们需要做请求合并处理。


做请求合并,我们至少对应着两个接口,一个是接收单个请求的接口,一个处理把单个请求汇总之后的请求接口。


所以我们需要先提供两个 service:



其中根据指定 id 查询的接口,对应的 Controller 是这样的:



服务启动起来后,我们用线程池结合 CountDownLatch 模拟 20 个并发请求:



从控制台可以看到,瞬间接受到了 20 个请求,执行了 20 次查询 sql:



很明显,这个时候我们就可以做请求合并。每收到 10 次请求,合并为一次处理,结合 Hystrix 代码就是这样的,为了代码的简洁性,我采用的是注解方式:



在上面的图片中,有两个方法,一个是 getUserId,直接返回的是null,因为这个方法体不重要,根本就不会执行。


在 @HystrixCollapser 里面可以看到有一个 batchMethod 的属性,其值是 getUserBatchById。


也就是说这个方法对应的批量处理方法就是 getUserBatchById。当我们请求 getUserById 方法的时候,Hystrix 会通过一定的逻辑,帮我们转发到 getUserBatchById 上。


所以我们调用的还是 getUserById 方法:



同样,我们用线程池结合 CountDownLatch 模拟 20 个并发请求,只是变换了请求地址:



调用之后,神奇的事情就出现了,我们看看日志:



同样是接受到了 20 个请求,但是每 10 个一批,只执行了两个sql语句。


从 20 个 sql 到 2 个 sql,这就是请求合并的威力。请求合并的处理速度甚至比单个处理还快,这也是性能的提升。


那假设我们只有 5 个请求过来,不满足 10 个这个条件呢?


别忘了,我们还有定时任务呢。


在 Hystrix 中,定时任务默认是每 10ms 执行一次:


同时我们可以看到,如果不设置 maxRequestsInBatch,那么默认是 Integer.MAX_VALUE。


也就是说,在 Hystrix 中做请求合并,它更加侧重的是时间方面。


功能演示,其实就这么简单,代码量也不多,有兴趣的朋友可以直接搭个 Demo 跑跑看。看看 Hystrix 的源码。


我这里只是给大家指几个关键点吧。


第一个肯定是我们需要找到方法入口。


你想,我们的 getUserById 方法的方法体里面直接是 return null,也就是说这个方法体是什么根本就不重要,因为不会去执行方法体中的代码。它只需要拦截到方法入参,并缓存起来,然后转发到批量方法中去即可。


然后方法体上面有一个 @HystrixCollapser 注解。


那么其对应的实现方式你能想到什么?


肯定是 AOP 了嘛。


所以,我们拿着这个注解的全路径,进行搜索,啪的一下,很快啊,就能找到方法的入口:



com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect#methodsAnnotatedWithHystrixCommand




在入口处打上断点,就可以开始调试了:



第二个我们看看定时任务是在哪儿进行注册的。


这个就很好找了。我们已经知道默认参数是 10ms 了,只需要顺着链路看一下,哪里的代码调用了其对应的 get 方法即可:



同时,我们可以看到,其定时功能是基于 scheduleAtFixedRate 实现的。


第三个我们看看是怎么控制超过指定数量后,就不等待定时任务执行,而是直接发起汇总操作的:



可以看到,在com.netflix.hystrix.collapser.RequestBatch#offer方法中,当 argumentMap 的 size 大于我们指定的 maxBatchSize 的时候返回了 null。


如果,返回为 null ,那么说明已经不能接受请求了,需要立即处理,代码里面的注释也说的很清楚了:



以上就是三个关键的地方,Hystrix 的源码读起来,需要下点功夫,大家自己研究的时候需要做好心理准备。


最后再贴一个官方的请求合并工作流程图:



再说请求分治


还是回到最开始我们提出的热点账户问题中的“减余额频繁的热点账户”。


请求分治和请求合并,就是针对“热点账户”这同一个问题的完全不同方向的两个回答。


分治,它的思想是拆分。


再说拆分之前,我们先聊一个更熟悉的东西:AtomicLong。


AtomicLong,这玩意是可以实现原子性的增减操作,但是当竞争非常大的时候,被操作的“值”就是一个热点数据,所有线程都要去对其进行争抢,导致并发修改时冲突很大。


那么 AtomicLong 是靠什么解决这个冲突的呢?


看一下它的 getAndAdd 方法:



可以看到这里面还是有一个 do-while 的循环:



里面调用了 compareAndSwapLong 方法。


do-while,就是自旋。


compareAndSwapLong,就是 CAS。


所以 AtomicLong 靠的是自旋 CAS 来解决竞争大的时候的这个冲突。


你看这个场景,是不是和我们开篇提到的热点账户有点类似?


热点账户,在并发大的时候我们可以对账户进行加锁操作,让其他请求进行排队。


而它这里用的是 CAS,一种乐观锁的机制。


但是也还是要排队,不够优雅。


什么是优雅的?


LongAdder 是优雅的。


有点小伙伴就有点疑问了:歪师傅,不是要讲热点账户吗,怎么扯到 LongAdder 上了呢?


闭嘴,往下看就行了。



首先,我们先看一下官网上的介绍:



上面的截图一共两段话,是对 LongAdder 的简介,我给大家翻译并解读一下。



首先第一段:当有多线程竞争的情况下,有个叫做变量集合(set of variables)的东西会动态的增加,以减少竞争。


sum 方法返回的是某个时刻的这些变量的总和。


所以,我们知道了它的返回值,不论是 sum 方法还是 longValue 方法,都是那个时刻的,不是一个准确的值。


意思就是你拿到这个值的那一刻,这个值其实已经变了。


这点是非常重要的,为什么会是这样呢?


我们对比一下 AtomicLong 和 LongAdder 的自增方法就可以知道了:



AtomicLong 的自增是有返回值的,就是一个这次调用之后的准确的值,这是一个原子性的操作。


LongAdder 的自增是没有返回值的,你要获取当前值的时候,只能调用 sum 方法。


你想这个操作:先自增,再获取值,这就不是原子操作了。


所以,当多线程并发调用的时候,sum 方法返回的值必定不是一个准确的值。除非你加锁。


该方法上的说明也是这样的:



至于为什么不能返回一个准确的值,这就是和它的设计相关了,这点放在后面去说。



然后第二段:当在多线程的情况下对一个共享数据进行更新(写)操作,比如实现一些统计信息类的需求,LongAdder 的表现比它的老大哥 AtomicLong 表现的更好。在并发不高的时候,两个类都差不多。但是高并发时 LongAdder 的吞吐量明显高一点,它也占用更多的空间。这是一种空间换时间的思想。


这段话其实是接着第一段话在进行描述的。


因为它在多线程并发情况下,没有一个准确的返回值,所以当你需要根据返回值去搞事情的时候,你就要仔细思考思考,这个返回值你是要精准的,还是大概的统计类的数据就行。


比如说,如果你是用来做序号生成器,所以你需要一个准确的返回值,那么还是用 AtomicLong 更加合适。


如果你是用来做计数器,这种写多读少的场景。比如接口访问次数的统计类需求,不需要时时刻刻的返回一个准确的值,那就上 LongAdder 吧。


总之,AtomicLong 是可以保证每次都有准确值,而 LongAdder 是可以保证最终数据是准确的。高并发的场景下 LongAdder 的写性能比 AtomicLong 高。


接下来探讨三个问题:



  • LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?

  • 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?

  • 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


先带你上个图片,看不懂没有关系,先有个大概的印象:



接下来我们就去探索源码,源码之下无秘密。


从源码我们可以看到 add 方法是关键:



里面有 cells 、base 这样的变量,所以在解释 add 方法之前,我们先看一下 这几个成员变量。


这几个变量是 Striped64 里面的。


LongAdder 是 Striped64 的子类:



其中的四个变量如下:




  • NCPU:cpu 的个数,用来决定 cells 数组的大小。

  • cells:一个数组,当不为 null 的时候大小是 2 的次幂。里面放的是 cell 对象。

  • base : 基数值,当没有竞争的时候直接把值累加到 base 里面。还有一个作用就是在 cells 初始化时,由于 cells 只能初始化一次,所以其他竞争初始化操作失败线程会把值累加到 base 里面。

  • cellsBusy:当 cells 在扩容或者初始化的时候的锁标识。


之前,文档里面说的 set of variables 就是这里的 cells。



好了,我们再回到 add 方法里面:



cells 没有被初始化过,说明是第一次调用或者竞争不大,导致 CAS 操作每次都是成功的。


casBase 方法就是进行 CAS 操作。


当由于竞争激烈导致 casBase 方法返回了 false 后,进入 if 分支判断。


这个 if 分子判断有 4 个条件,做了 3 种情况的判断




  • 标号为 ① 的地方是再次判断 cells 数组是否为 null 或者 size 为 0 。as 就是 cells 数组。

  • 标号为 ② 的地方是判断当前线程对 cells 数组大小取模后的值,在 cells 数组里面是否能取到 cell 对象。

  • 标号为 ③ 的地方是对取到的 cell 对象进行 CAS 操作是否能成功。


这三个操作的含义为:当 cells 数组里面有东西,并且通过 getProbe() & m算出来的值,在 cells 数组里面能取到东西(cell)时,就再次对取到的 cell 对象进行 CAS 操作。


如果不满足上面的条件,则进入 longAccumulate 函数。


这个方法主要是对 cells 数组进行操作,你想一个数组它可以有三个状态:未初始化、初始化中、已初始化,所以下面就是对这三种状态的分别处理:




  • 标号为 ① 的地方是 cells 已经初始化过了,那么这个里面可以进行在 cell 里面累加的操作,或者扩容的操作。

  • 标号为 ② 的地方是 cells 没有初始化,也还没有被加锁,那就对 cellsBusy 标识进行 CAS 操作,尝试加锁。加锁成功了就可以在这里面进行一些初始化的事情。

  • 标号为 ③ 的地方是 cells 正在进行初始化,这个时候就在 base 基数上进行 CAS 的累加操作。


上面三步是在一个死循环里面的。


所以如果 cells 还没有进行初始化,由于有锁的标志位,所以就算并发非常大的时候一定只有一个线程去做初始化 cells 的操作,然后对 cells 进行初始化或者扩容的时候,其他线程的值就在 base 上进行累加操作。


上面就是 sum 方法的工作过程。


感受到了吗,其实这就是一个分段操作的思想,不知道你有没有想到 ConcurrentHashMap,也不奇怪,毕竟这两个东西都是 Doug Lea 写的。


总的来说,就是当没有冲突的时候 LongAdder 表现的和 AtomicLong 一样。当有冲突的时候,才是 LongAdder 表现的时候,然后我们再回去看这个图,就能明白怎么回事了:



好了,现在我们回到前面提出的三个问题:



  • LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?

  • 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?

  • 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


它们其实是一个问题。


因为 LongAdder 把热点 value 拆分了,放到了各个 cell 里面去操作。这样就相当于把冲突分散到了 cell 里面。所以解决了并发修改冲突很大这个问题。


当发生冲突时 sum= base+cells。高并发的情况下当你获取 sum 的时候,cells 极有可能正在被其他的线程改变。一个在高并发场景下实时变化的值,你要它怎么给你个准确值?


当然,你也可以通过加锁操作拿到当前的一个准确值,但是这种场景你还用啥 LongAdder,是 AtomicLong 不香了吗?


为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


你发动你的小脑壳想一想,朋友。


AtomicLong 不管有没有冲突,它写的都是一个共享的 value,有冲突的时候它就在自旋。


LongAdder 没有冲突的时候表现的和 AtomicLong 一样,有冲突的时候就把冲突分散到各个 cell 里面了,冲突分散了,写的当然更快了。


我强调一次:有冲突的时候就把冲突分散到各个 cell 里面了,冲突分散了,写的当然更快了。


你注意这句话里面的“各个 cell”。


这是什么?


这个东西本质上就是 sum 值的一部分。


如果用热点账户去套的话,那么“各个 cell”就是热点账户下的影子账户。


热点账户说到底还是一个单点问题,那么对于单点问题,我们用微服务的思想去解决的话是什么方案?


就是拆分。


假设这个热点账户上有 100w,我设立 10 个影子账户,每个账户 10w ,那么是不是我们的流量就分散了?


从一个账户变成了 10 个账户,压力也就进行了分摊。


但是同时带来的问题也很明显。


比如,获取账户余额的时候需要把所有的影子账户进行汇总操作。但是每个影子账户上的余额是时刻在变化的,所以我们不能保证余额是一个实时准确的值。


但是相比于下单的量来说,大部分商户并不关心“账上的实时余额”这个点。


他只关心上日余额是准确的,每日对账都能对上就行了。


这就是分治。


其实我浅显的觉得分布式、高并发都是基于分治,或者拆分思想的。


本文的 LongAdder 就不说了。


微服务化、分库分表、读写分离......这些东西都是在分治,在拆分,把集中的压力分散开来。


这就算是我对于“请求合并和分治”的理解、


好了,到这里本文就算是结束了。


针对"热点账户"这同一个问题,细化了问题方向,定义了加余额频繁和减余额频繁的两种热点账户,然后给出了两个完全不同方向的回答。


这个时候,我就可以把文章开头的那句话拿出来说了:


综上,我认为合并和分治,这二者之间是辩证的关系,得用辩证的眼光看问题,它们是你中有我,我中有你~



作者:why技术
来源:juejin.cn/post/7292955463954563072
收起阅读 »

工作两年,本地git分支达到了惊人的361个,该怎么快速清理呢?

说在前面 不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。...
继续阅读 »

说在前面



不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。而我平时本地分支都不怎么清理,这就导致了我这两年来本地分支的数量达到了惊人的361个,所以便开始写了这个可以批量删除分支的命令行工具。



1697987090644.jpg


功能设计


我们希望可以通过命令行命令的方式来进行交互,快速获取本地分支列表及各分支的最后提交时间和合并状态,在控制台选择我们想要删除的分支。


功能实现


1、命令行交互获取相关参数


这里我们使用@jyeontu/j-inquirer模块来完成命令行交互功能,@jyeontu/j-inquirer模块除了支持inquirer模块的所有交互类型,还扩展了文件选择器文件夹选择器多级选择器交互类型,具体介绍可以查看文档:http://www.npmjs.com/package/@jy…


(1)获取操作分支类型


我们的分支分为本地分支和远程分支,这里我们可以选择我们需要操作的分支类型,选择列表为:"本地分支"、"远程分支"、"本地+远程"


(2)获取远程仓库名(remote)


我们可以输入自己git的远程仓库名,默认为origin


(3)获取生产分支名


我们需要判断各分支是否已经合并到生产分支,所以需要输入自己项目的生产分支名,默认为develop


相关代码


const branchListOptions = [
{
type: "list",
message: "请选择要操作的分支来源:",
name: "branchType",
choices: ["本地分支", "远程分支", "本地+远程"],
},
{
type: "input",
message: "请输入远程仓库名(默认为origin):",
name: "gitRemote",
default: "origin",
},
{
type: "input",
message: "请输入生产分支名(默认为develop):",
name: "devBranch",
default: "develop",
},
];
const res = await doInquirer(branchListOptions);

image.png


2、命令行输出进度条


在分支过多的时候,获取分支信息的时间也会较长,所以我们需要在控制台中打印相关进度,避免用户以为控制台卡死了,如下图:


image.png


image.png


3、git操作


(1)获取git本地分支列表


想要获取当前仓库的所有的本地分支,我们可以使用git branch命令来获取:


function getLocalBranchList() {
const command = "git branch";
const currentBranch = getCurrentBranch();
let branch = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branch = branch
.split("、")
.filter(
(item) => item !== "" && !item.includes("->") && item !== currentBranch
);
return branch;
}

(2)获取远程仓库分支列表


想要获取当前仓库的所有的远程分支,我们可以使用git ls-remote --heads origin命令来获取,git ls-remote --heads origin 命令将显示远程仓库 origin 中所有分支的引用信息。其中,每一行显示一个引用,包括提交哈希值和引用的全名(格式为 refs/heads/<branch_name>)。


示例输出可能如下所示:


Copy Code
<commit_hash> refs/heads/master
<commit_hash> refs/heads/develop
<commit_hash> refs/heads/feature/xyz

其中,<commit_hash> 是每个分支最新提交的哈希值。


function getRemoteList(gitRemote) {
const command = `git ls-remote --heads ${gitRemote}`;
let branchList = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branchList = branchList
.split("、")
.filter((item) => item.includes("refs/heads/"))
.map((branch) => {
return gitRemote + "/" + branch.split("refs/heads/")[1];
});
return branchList;
}

(3)获取各分支详细信息


我们想要在每个分支后面显示该分支最后提交时间和是否已经合并到生产分支,这两个信息可以作为我们判断该分支是否要删除的一个参考。



  • 获取分支最后提交时间
    git show -s --format=%ci <branchName> 命令用于查看 指定 分支最新提交的提交时间。其中,--format=%ci 用于指定输出格式为提交时间。


在 Git 中,git show 命令用于显示某次提交的详细信息,包括作者、提交时间、修改内容等。通过使用 -s 参数,我们只显示提交摘要信息,而不显示修改内容。


git show -s --format=%ci develop 命令将显示 develop 分支最新提交的提交时间。输出格式为 ISO 8601 标准的时间戳,例如 2023-10-22 16:41:47 +0800


function getBranchLastCommitTime(branchName) {
try {
const command = `git show -s --format=%ci ${branchName}`;
const result = child_process.execSync(command).toString();
const date = result.split(" ");
return date[0] + " " + date[1];
} catch (err) {
return "未获取到时间";
}
}


  • 判断分支是否合并到生产分支
    git branch --contains <branchName> 命令用于查找包含指定分支(<branchName>)的所有分支。


在 Git 中,git branch 命令用于管理分支。通过使用 --contains 参数,我们可以查找包含指定提交或分支的所有分支。


git branch --contains <branchName> 命令将列出包含 <branchName> 的所有分支。输出结果将显示每个分支的名称以及指定分支是否为当前分支。


示例输出可能如下所示:


Copy Code
develop
* feature/xyz
bugfix/123

其中,* 标记表示当前所在的分支,我们只需要判断输出的分支中是否存在生产分支即可:


function isMergedCheck(branch) {
try {
const command = `git branch --contains ${branch}`;
const result = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
const mergedList = result.split("、");
return mergedList.includes(gitInfoObj.devBranch)
? `已合并到${gitInfoObj.devBranch}`
: "";
} catch (err) {
return "未获取到合并状态";
}
}

(4)删除选中分支


选完分支后我们就该来删除分支了,删除分支的命令大家应该就比较熟悉了吧



  • git branch -D <branchName>


git branch -D <branchName> 命令用于强制删除指定的分支(<branchName>)。该命令会删除本地仓库中的指定分支,无法恢复已删除的分支。



  • git push <remote> :<branchName>


git push <remote> :<branchName> 命令用于删除远程仓库<remote>中的指定分支(<branchName>)。这个命令通过推送一个空分支到远程仓库的 <branchName> 分支来实现删除操作。


async function doDeleteBranch(branchList) {
const deleteBranchList = await getDeleteBranch(branchList);
if (!deleteBranchList) return;
console.log("正在删除分支");
progressBar.run(0);
deleteBranchList.forEach((branch, index) => {
let command = `git branch -D ${branch}`;
if (branch.includes("/")) {
const tmp = branch.split("/");
command = `git push ${tmp[0]} :${tmp[1]}`;
}
child_process.execSync(command);
progressBar.run(Math.floor(((index + 1) / deleteBranchList.length) * 100));
});
console.log("");
console.log("已删除分支:" + deleteBranchList);
}

image.png


1697995985140.png


1697996057503.jpg


可以看到我们的分支瞬间就清爽了很多。


使用


该工具已经发布到 npm 上,可以直接通过命令npm i -g jyeontu进行安装,安装完后在控制台中输入jyeontu git即可进行操作。


源码


该工具的源码也已经开源,有兴趣的同学可以到Gitee上查看:Gitee地址


说在后面



🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。



作者:JYeontu
来源:juejin.cn/post/7292635075304964123
收起阅读 »

去寺庙做义工,有益身心健康

《乔布斯传》中写到:乔布斯把对事物专注的能力和对简洁的热爱归功于他的禅修。他说:“禅修磨炼了他对直觉的欣赏能力,教他如何过滤掉任何能分散时间和精力的其它不必要的事情,在他的身上培养出了专注基于至简主义的审美观。” 如何在当今物欲横流的浮躁社会里不沦陷其中?如何...
继续阅读 »

《乔布斯传》中写到:乔布斯把对事物专注的能力和对简洁的热爱归功于他的禅修。他说:“禅修磨炼了他对直觉的欣赏能力,教他如何过滤掉任何能分散时间和精力的其它不必要的事情,在他的身上培养出了专注基于至简主义的审美观。”


如何在当今物欲横流的浮躁社会里不沦陷其中?如何在每天奔波忙碌之后却不内心疲惫、焦虑?如何在巨大的工作与生活压力下保持一颗平和的心?如何在经历感情、友情和亲情的起起落落后看破放下?如何改变透支健康和生命的人生模式?


程序员无疑是一个高压的职业,尤其是在头部公司工作的程序员们,工作压力更是大。并且在互联网行业,禅修并不是一件新鲜事。我们不一定要正儿八经地参加禅修活动,只是去寺庙走一走,呼吸一下新鲜空气,给寺庙干点活,对身心健康的帮助也会很大。


我与寺庙


我最早接触寺庙是在2011年上军校的时候,我的一个老师,作为大校,经常在课上分享他周末在南京附近寺庙的奇闻轶事,也会分享他自己的一些人生体验和感悟,勾起了我对寺庙生活的向往。


2013年,作为现役军人,我跑到了江西庐山的东林寺做了一个礼拜的义工,在那里,每天早上四点起床早上寺庙早课,负责三餐的行堂,也作为机动义工,干一些杂活,比如卸菜、组装床等,晚上有时也可以听寺庙的传统文化课。


2013年底,我申请退出现役,于是14年春就可以休假了,根据流程不定期去各部门办理手续即可,期间一个周末,我弟带我去凤凰岭玩,偶遇一个皈依法会,为了能看到传说中的北大数学天才,我填了一个义工表,参加了皈依仪式。


因为没有考虑政府安排的工作,所以打算考个研,期间不时会去凤凰岭的寺庙参加活动。考完研后,到18年春季,周末节假日,基本都会去这个寺庙做义工,累计得有200天以上。


期间,作为骨干义工,参与了该寺庙组织的第二至第四届的IT禅修营,负责行堂、住宿和辅导员等相关的工作。


很多人都听过这样一件往事:2010年,张小龙(微信之父)偶然入住一个寺院,当时正是微信研发的关键时刻,因为几个技术难题,张小龙连续几天彻夜难眠,终于一气之下把资料撕得粉碎。


没想到负责打扫卫生的僧人看到后,竟然帮他把资料重新粘贴了起来,还顺手写下了几条建议。张小龙非常惊讶,打听过后才知道这位扫地僧出家前曾混迹IT界,是个著名的极客。


经扫地僧点化,张小龙回到广州苦攻一年后,微信终于大成。这件事传的很广也很玄乎,可信度不会太高,不过故事中张小龙入住的寺院,就是我常去的寺庙。


至于在故事中懂得IT的扫地僧,在这里遇到其实不是什么奇怪的事,你还有可能遇到第47届国际数学奥赛金牌得主贤宇法师,他来自北大数学系;或者是禅兴法师,他是清华大学流体力学博士;又或者贤启法师,他是清华大学核能和热能物理博士。


“扫地只不过是我的表面工作,我真正的职业是一位研究僧。” 《少林足球》这句台词的背后,隐藏着关于这个寺庙“高知僧团”的一个段子。


因为各种不可描述的原因,18年9月之后,我就很少去这个寺庙了,但我知道我依然很向往寺庙的生活。于是22年春,我下定决心离开北京去深圳,其中就有考虑到深圳后,可以去弘法寺或弘源寺做义工。


去了一次弘法寺,感觉那边人太多,后面去了一次弘源寺后,感觉这里比较适合我,人少很安静,不堵车的话,开车只需20分钟到家。


目前,只要我有时间,我都会去弘源寺干一天临时义工,或者住上几天。


何为禅?


禅,是心智的高度成熟状态。直至印度词汇传入,汉语音译为“禅那”,后世简称为“禅”,汉译意思有“静虑”、“思维修”等。


禅修的方法就是禅法,禅法是心法,并不固着于某种具体形式,也不限于宗教派别。从泛义来说,任何一种方法,如果能够让你的心灵成熟,了解生命的本质,让心获得更高层次的证悟,并从而获得生命究竟意义的了悟。这样的方法就是禅修!


从狭义来说,在绵延传承数千年的漫长时空里,形成各种系统的修行方法,存在于各种教派中。现存主要有传承并盛行于南传佛教国家的原始佛教禅法与传承并盛行于中国汉传佛教的祖师禅。


如来禅是佛陀的原始教法,注重基础练习,强调修行止观。祖师禅是中国禅宗祖师的教法,强调悟性、觉性,推崇顿悟,以参话头为代表,以开悟、明心见性为目的。


我们普遍缺乏自我觉察,甚至误解了包括自由在内的生命状态真义。禅修中,会进入深刻自我觉察中,有机会与自己整合,从而开启真我。


近年来,禅修在西方非常流行,像美国的学校、医疗机构和高科技公司都广泛地在进行打坐、禅修。美国有些科学家曾做过一个实验,实验对象是长期禅修的修行人。在实验室中,实验者一边用脑电波图测量脑波的变化,一边用功能性核磁共振测量脑部活动的位置。


最后得出结论:通过禅修,不但能够短期改变脑部的活动,而且非常有可能促成脑部永久的变化。这就是说:通过禅定,可以有效断除人的焦虑、哀伤等很多负面情绪,创造出心灵的幸福感,甚至可以重塑大脑结构。


禅修能够修复心智、疗愈抑郁、提升智慧,让我们重获身心的全面健康!禅修让人的内心变得安静。在禅修时,人能放松下来,专注于呼吸,使内心归于平静,身体和心灵才有了真正的对话与接触。


“禅修是未来科技世界中的生存必需品”时代杂志曾在封面报道中这样写道。在硅谷,禅修被认为是新的咖啡因,一种能释放能量与创造力的全新“燃料”。


禅修也帮助过谷歌、Facebook、Twitter高管们走出困惑,国内比较知名则有搜狐的张朝阳和阿里的马云,还有微信之父张小龙的传说。


对于他们来说,商海的起伏伴随着心海的沉浮,庞大的财富、名声与地位带来的更多的不是快乐,但是禅修,却在一定程度上给他们指点迷津,带领他们脱离现代社会的痛苦、让内心更加平静!


乔布斯的禅修故事


乔布斯和禅修,一直有着很深的渊源。乔布斯是当世最伟大的企业家之一,同时也是一名虔诚的禅宗教徒。他少有慧根,17岁那年,他远赴印度寻找圣人寻求精神启蒙,18岁那年,他开始追随日本禅师乙川弘文学习曹洞宗的禅法。


年轻的时候,乔布斯去印度,在印度体验,呆了七个月。乔布斯在印度干了些什么,我们不得而知。不过据我推测,也就是四处逛一逛,看一看,可能会去一些寺庙,拜访一些僧人。


我从来不认为,他遇到了什么高人,或者在印度的小村庄一待,精神就受到了莫大的洗礼。变化永远都是从内在发生的,外在的不过是缘分,是过客,负责提供一个合适的环境,或者提供一些必要的刺激。


但我们知道,从此以后,乔布斯的人生,就开始变得不一样了。乔布斯的人生追求是“改变世界”,当年他劝说百事可乐总裁,来担任苹果CEO的时候所说的话:“你是愿意一辈子卖糖水,还是跟我一起改变这个世界?”激励了无数心怀梦想的朋友。


早在1973年乔布斯已经对禅有较深的领悟了。他这样说:“我对那些超越物质的形而上的学说极感兴趣,也开始注意到比知觉及意识更高的层次——直觉和顿悟。”


他还说:“因为时间有限,不要带着面具为别人而活,不要让别人的意见左右自己内心的想法,最重要的是要勇敢地忠于自己内心的直觉。”


乔布斯说:“你不能预先把点点滴滴串在一起;唯有未来回顾时,你才会明白那些点点滴滴是如何串在一起的。所以你得相信,你现在所体会的东西,将来多少会连接在一块。你得信任某个东西,直觉也好,命运也好,生命也好,或者业力。这种作法从来没让我失望,也让我的人生整个不同起来。”


他大学时学的书法,被他用来设计能够印刷出漂亮字体的计算机,尽管他在大学选修书法课时,完全不知道学这玩意能有什么用。他被自己创立的苹果公司开除,于是转行去做动画,结果在做动画的时候,遇到了自己未来的妻子。


人呐实在不知道,自己可不可以预料。你说我一个被自己创立的公司开除的失业狗,怎么就在第二份工作里遇到了一生的挚爱呢?若干年后,他回顾起自己的人生,他把这些点点滴滴串了起来,他发现,他所经历过的每一件事,都有着特殊的意义。


所以,无论面对怎样的困境,我们都不必悲观绝望,因为在剧本结束之前,你永远不知道,自己现在面对的这件事,到底是坏事还是好事。


所谓创新就是无中生有,包括思想、产品、艺术等,重大的创新我们称为颠覆。通过那则著名的广告《think different》他告诉世人:“因为只有那些疯狂到以為自己能够改变世界的人,才能真正地改变世界。”乔布斯确实改变了世界,而且不是一次,至少五次颠覆了这个世界:



  • 通过苹果电脑Apple-I,开启了个人电脑时代;

  • 通过皮克斯电脑动画公司,颠覆了整个动漫产业;

  • 通过iPod,颠覆了整个音乐产业;

  • 通过iPhone,颠覆了整个通讯产业;

  • 通过iPad,重新定义并颠覆了平板PC行业。


程序员与禅修


编程是一门需要高度专注和创造力的艺术,它要求程序员们在面对复杂问题和压力时,能够保持内心的安宁和平静。在这个快节奏、竞争激烈的行业中,如何修炼内心的禅意境界,成为程序员们更好地发挥潜力的关键。


在编程的世界里,专注是至关重要的品质。通过培养内在专注力,程序员能够集中精力去解决问题,避免被外界的干扰所困扰。以下是几种培养内在专注的方法:



  • 冥想和呼吸练习:  通过冥想和深呼吸来调整身心状态,让自己平静下来。坚持每天进行一段时间的冥想练习,可以提高专注力和注意力的稳定性。

  • 时间管理:  制定合理的工作计划和时间表,将任务分解为小的可管理的部分,避免心理上的压力。通过专注于每个小任务,逐步完成整个项目。

  • 限制干扰:  将手机静音、关闭社交媒体和聊天工具等干扰源,创造一个安静的工作环境。使用专注工作法(如番茄钟),集中精力在一项任务上,直到完成。


编程过程中会遇到各种问题和挑战,有时甚至会感到沮丧和失望。然而,保持平和的心态是非常重要的,它可以帮助程序员更好地应对压力和困难。以下是一些培养平和心态的技巧:



  • 接受不完美性:  程序永远不会是完美的,因为它们总是在不断发展和改进中。接受这一事实,并学会从错误中汲取教训。不要过于苛求自己,给自己一些宽容和理解。

  • 积极思考:  关注积极的方面,让自己的思维更加积极向上。遇到问题时,寻找解决方案而非抱怨。积极的心态能够帮助你更好地应对挑战和困难。

  • 放松和休息:  给自己合理的休息时间,让大脑得到充分的放松和恢复。休息和娱乐能够帮助你调整心态,保持平和的状态。


编程往往是一个团队合作的过程,与他人合作的能力对于一个程序员来说至关重要。以下是一些建立团队合作意识和促进内心安宁的方法:



  • 沟通与分享:  与团队成员保持良好的沟通,分享想法和问题。倾听他人的观点和建议,尊重不同的意见。积极参与和贡献团队,建立合作关系。

  • 友善和尊重:  培养友好、尊重和包容的态度。尊重他人的工作和努力,给予鼓励和支持。与团队成员建立良好的关系,创造和谐的工作环境。

  • 共享成功:  当团队取得成功时,与他人一起分享喜悦和成就感。相信团队的力量,相信集体的智慧和努力。


修炼内心安宁需要时间和长期的自我管理。通过培养专注力、平和心态、创造力和团队合作意识,程序员们可以在面对复杂的编程任务和挑战时保持内心的安宁和平静。


禅修有许多不同的境界,其中最典型的可能包括:



  • 懵懂:刚开始禅修时,可能会觉得茫然和困惑,不知该如何开始。

  • 困扰:在进行深度内省和冥想时,可能会遇到很多烦恼和难题,需耐心思考和解决。

  • 安和:通过不断地练习和开放自己的心灵,可能会进入一种更加平和和沉静的状态。

  • 祥和:当一些心理障碍得到解决,你会感受到一种更深层的平静和和谐。

  • 转化:通过不断的冥想与内省,你可以向内看到自己的内心,获得对自己和世界的新的认识和多样的观察角度。

  • 整体意识:通过冥想,您将能够超越个人的视野和言语本身,深入探究宇宙的内心,领悟更加深入和广泛的境界和意识。


程序员写代码的境界:



  • 懵懂:刚熟悉编程语言,不知做什么。

  • 困扰:可以实现需求,但仍然会被需求所困,需要耐心思考和解决。

  • 安和:通过不断练习已经可以轻易实现需求,更加平和沉静。

  • 祥和:全栈。

  • 转化:做自己的产品。

  • 整体意识:有自己的公司。


一个创业设想


打开小红书,与“疗愈”相关的笔记高达236万篇,禅修、瑜伽、颂钵等新兴疗愈方法层出不穷,无论是性价比还是高消费,总有一种疗愈方法适合你。


比起去网红景点打卡拍照卷构图卷妆造,越来越多的年轻人正在借助上香、拜神、颂钵、冥想等更为“佛系”的方式去追寻内心的宁静。放空大脑,呼吸之间天地的能量被尽数吸收体内,一切紧张、焦虑都被稀释,现实的残酷和精神的困顿,都在此间找到了出口。


在过去,简单的瑜伽和冥想就能达到这种目的,但伴随着疗愈文化的兴起与壮大,不断在传统方式之上叠加buff,才是新兴疗愈的终极奥义。


从目标人群来看,不同的禅修对应不同的人群。比如临平青龙寺即将在8月开启的禅修,就分为了企业禅修、教育禅修、功能禅修、共修禅、突破禅、网络共修等多种形式。但从禅修内容来看,各个寺庙的安排不尽相同,但基本上跳脱不出早晚功课、上殿过堂、出坡劳作、诵经礼忏、佛学讲座等环节。


艺术疗愈,是截然不同于起参禅悟道这种更亲近自然,还原本真的另一疗愈流派。具体可以细分为戏剧疗愈、绘画疗愈、音乐疗愈等多种形式。当理论逐渐趋向现代化,投入在此间的花费,也成正比增长。


绘画疗愈 ,顾名思义就是通过绘画的方式来表达自己内心的情绪。画幅的大小、用笔的轻重、空间的配置、色彩的使用,都在某种程度上反映着创作者潜意识的情感与冲突。


在绘画过程中,绘画者也同样会获得纾解和满足。也有一些课程会在绘画创作之外,添加绘画作品鉴赏的内容,通过一幅画去窥视作者的内心,寻求心灵上的共鸣,也是舒缓压力的一种渠道。


疗愈市场之所以能够发展,还是因为有越来越多人的负面情绪需要治愈。不论是工作压力还是亲密关系所带来的情绪内耗,总要有一个释放的出口。


当前,我正在尝试依托自营绘馆老师提供优质课件,打造艺培课件分享的平台或社区,做平台前期研发投入比较大,当前融资也比较困难,同时自己也需要疗愈。


所以,最近也在调研市场,评估是否可以依托自营的门店,组织绘画手工+寺庙行禅+技术专题分享的IT艺术禅修营活动,两天含住宿1999元,包括半天寺庙义工体验、半天禅修、半天绘画手工课和半天的技术专题分享。


不知道,这样的活动,大家会考虑参加吗?


总结


出家人抛弃尘世各种欲望出家修行值得尊重,但却不是修行的唯一方法,佛经里著名的维摩洁居士就是在家修行,也取得了非凡成就,六祖惠能就非常鼓励大家在世间修行,他说:“佛法在世间,不离世间觉,离世觅菩提,恰如求兔角”。


普通人的修行是在红尘欲望中的修行,和出家人截然不同,但无分高下,同样可以证悟,工作就是他们最好的修练道场。禅学的理论学习并不困难,但这只是万里长征的第一步,最重要的是,我们要在日常实践中证悟。


简单可能比复杂更难做到:你必须努力理清思路,从而使其变得简单。但最终这是值得的,因为一旦你做到了,便可以创造奇迹。”乔布斯所说的这种专注和简单是直接相关的,如果太复杂,心即散乱,就很难保持专注,只有简单,才能做到专注,只有专注,才能极致。


作者:三一习惯
来源:juejin.cn/post/7292781589477687350
收起阅读 »

成为务实的程序员

编程是一门技艺。简单地说,就是让计算机做你想让它做的事情(或是你的用户想让它做的事情)。作为一名程序员,你既在倾听,又在献策;既是传译,又行独裁;你试图捕获难以捉摸的需求,并找到一种表达它们的方式,以便仅靠一台机器就可以从容应付。你试着把工作记录成文档,以便他...
继续阅读 »

编程是一门技艺。简单地说,就是让计算机做你想让它做的事情(或是你的用户想让它做的事情)。作为一名程序员,你既在倾听,又在献策;既是传译,又行独裁;你试图捕获难以捉摸的需求,并找到一种表达它们的方式,以便仅靠一台机器就可以从容应付。你试着把工作记录成文档,以便他人理解;你试着将工作工程化,这样别人就能在其上有所建树;更重要的是,你试图在项目时钟的滴答声中完成所有这些工作。你每天都在创造小小的奇迹。



什么是“务实”?


务实(Pragmatic)这个词来自拉丁语 pragmaticus ——“精通业务”,该词又来源于希腊语 πραγματικός,意思是“适合使用”。


务实程序员特征



  • 早期的采纳者 / 快速的适配者:对技术和技巧有一种直觉,喜欢尝试。当接触到新东西时,你可以快速地掌握它们,并把它们与其他的知识结合起来。你的信心来自经验。

  • 好奇:倾向于问问题。热衷于收集各种细微的事实,坚信它们会影响自己多年后的决策。

  • 批判性的思考者:你在没有得到证实前很少接受既定事实。当同事们说“因为就该这么做”,或者供应商承诺会解决所有问题时,你会闻到挑战的味道。

  • 现实主义:你试图理解所面临的每个问题的本质。这种现实主义让你对事情有多困难、需要用多长时间有一个很好的感知。一个过程应该很难,或是需要点时间才能完成,对这些的深刻理解,给了你坚持下去的毅力。

  • 多面手:你努力熟悉各种技术和环境,并努力跟上最新的进展。虽然目前的工作可能要求你在某个专门领域成为行家,但你总是能够进入新的领域,迎接新的挑战。


务实的哲学


软件的熵



当软件中的无序化增加时,程序员会说“软件在腐烂”。有些人可能会用更乐观的术语来称呼它,即技术债。



有很多原因导致软件腐烂。最重要的一个似乎是项目工作中的心理状态,或者说文化。无视一个明显损坏的东西,会强化这样一种观念:看来没有什么是能修好的,也没人在乎,一切都命中注定了。所有的负面情绪会在团队成员间蔓延,变成恶性循环。



不要放任破窗,及时发现及时修复,漠视会加速腐烂的过程。



软件开发中应该遵循的方法:不要只是因为一些东西非常着急,就去造成附带伤害。破窗一扇都嫌太多


够好即可的软件



不要为了追求更好而损毁了原有已经够好的。



我们做的东西,从用户需求角度出发是否足够好?最好还是留给用户一个机会,让他们亲自参与评判。无视来自用户方面的需求,一味地向程序中堆砌功能,一次又一次地打磨代码,这是很不专业的表现。心浮气躁当然不值得提倡,比如承诺一个无法兑现的时间尺度,然后为了赶上截止日期删减必要的边角工程,这同样是不专业的做法。



如果早点给用户一点东西玩,他们的反馈常常引领你做出更好的最终方案。



知识组合



知识和经验是你最重要的专业资产。学习新事物的能力是你最重要的战略资产。



关于如何构建自己的知识组合,可以参考以下指导方针:



  • 定期投资:安排一个固定的时间和地点为你的知识组合投资。

  • 多样化:计算机技术变化迅猛——今天的技术热点可能到明天就接近无用(至少不那么受欢迎)。所以,熟悉的技能越多,越能适应变化。

  • 风险管理:分散风险,不要把所有的技术鸡蛋放在一个篮子里。

  • 低买高卖:在一项新技术变得流行之前就开始学习,可能和发现一只被低估的股票一样困难,但是所得到的收获会和此类股票的收益一样好。

  • 重新评估调整:这是一个充满活力的行业。你上个月开始研究的热门技术现在可能已经凉下来了。


对于那些已经构成知识组合的智力资产,获取它们的最佳途径可以参考如下建议:



  1. 每年学习一门新语言:不同语言以不同的方式解决相同的问题。多学习几种不同的解决方法,能帮助自己拓宽思维,避免陷入陈规。

  2. 每月读一本技术书:虽然网络上有大量的短文和偶尔可靠的答案,但深入理解还是需要去读长篇的书。当你掌握了当前正在使用的所有技术后,扩展你的领域,学习一些和你项目不相关的东西

  3. 还要读非技术书:不要忘记方程式中人的那一面,他需要完全不同的技能集。

  4. 上课:在本地大学或网上找一些有趣的课程,或许也能在下一场商业会展或技术会议上找到。

  5. 加入本地的用户组和交流群:不要只是去当听众,要主动参与。独来独往对你的职业生涯是致命的:了解一下公司之外的人都在做什么。

  6. 尝试不同的环境

  7. 与时俱进:关心一下和你当前项目不同的技术,阅读相关的新闻和技术贴。


批判性思维



批判性地思考读到的和听到的东西。



批判性地分析问题,先思考几个问题:



  • 问“五个为什么”:当有了答案后,还要追问至少五个为什么;

  • 谁从中受益:追踪钱的流动更容易理清脉络;

  • 有什么背景:每件事都发生在它自己的背景下,这也是为何“能解决所有问题”的方案通常不存在,而那些兜售“最佳实践”的书或文章实际上经不起推敲。

  • 什么时候在哪里可以工作起来:不要停留在一阶思维下(接下来会发生什么),要进行二阶思考:当它结束后还会发生什么?

  • 为什么是这个问题:是否存在一个基础模型?这个基础模型是怎么工作的?


务实的方法


ETC——优秀设计的精髓



优秀的设计比糟糕的设计更容易变更。



对代码而言,要顺应变化。因此要信奉 ETC(Easier To Change,更容易变更)原则。ETC 是一种价值观念,不是一条规则。关于培养 ETC 观念,有以下几点意见:



  • 不断强化意识,问自己这么做是否让系统更容易变更。

  • 假设不确定什么形式的改变会发生,你也总是可以回到终极的“容易变更”的道路上;试着让你写的东西可替换。

  • 在工程日志中记下你面临的处境:你有哪些选择,以及关于改变的一些猜测。


DRY——邪恶的重复


我们认为,想要可靠地开发软件,或让开发项目更容易理解和维护,唯一的方法就是遵循下面这条DRY(Don't repeat yourself,不要重复自己)原则:



在一个系统中,每一处知识都必须单一、明确、权威地表达。



与之相对的不同做法是在两个或更多地方表达相同的东西。如果变更其中一个,就必须记得变更其他的那些。


DRY 不限于编码,DRY 针对的是你对知识意图的复制。它强调的是,在两个地方表达的东西其实是相同的,只是表达的方式可能完全不同。



  • 代码中的重复

  • 文档中的重复:例如注释和代码表达完全相同的意思,但是未来代码变更可能不会同步注释的变更,或者用数据结构表达知识等。

  • 开发人员间的重复

    • 最难检测到且难处理的重复类型,可能发在同一个项目的不同开发人员之间。整块的动能集可能会在不经意间重复,而这种重复或者好多年都并未发现,最终导致了维护问题。

    • 我们认为解决这个问题最好的方法是鼓励开发人员间积极频繁的交流。

    • 指派团队中的一人作为项目只是管理员,他的工作就是促进知识的传播。在源码目录树中设置一个集中的位置,存放工具程序和脚本程序。

    • 你要努力的方向,应该是孕育出一个更容易找到和复用已有事务的环境而不是自己重新编写




正交性


定义:对于两个或多个事物,其中一个的改变不影响其他任何一个,则这些事物是正交的。


当系统的组件相互之间高度依赖时,就没有局部修理这回事儿。我们希望设计的组件自成一体:独立自主,有单一的清晰定义的意图。但凡编写正交的系统,就能获得两个主要的收益:提高生产力和降低风险


设计


可以用一个简单的方法可以测试设计的正交性。当你规划好组件后,问问自己:



  • 如果一个特别功能背后的需求发生显著改变,有多少模块会影响?

  • 你的设计与现实世界的变化有多大程度的解耦。 不要依赖那些你无法控制的东西。


编码


当你写下代码时,就有降低软件正交性的风险。你不仅需要盯着正在做的事情,还要监控软件的大环境。有几种技术可以用来保持正交性:



  • 保持代码解耦:编写害羞的代码——模块不会向其他模块透露任何不必要的信息,也不依赖于其他模块的实现。

  • 避免全局数据:只要代码引用全局数据,就会将自己绑定到共享该数据的其他组件上。即使只打算对全局数据进行读操作,也可能引发问题(例如突然需要将代码改为多线程的情形)。一般来说,如果总是显式地将任何需要上下文传递给模块,那么代码会更容易理解和维护。

  • 避免相似的数据:可以看看《设计模式》中的策略模式。


测试


修 Bug 也是评估整个系统正交性的好时机。遇到问题时,评估一下修复行为的局部化程度。只要变了一个模块,还是有很多处变更分散在整个系统里?当你修正了一个地方,是不是就修复了所有问题,还是会神秘地出现其他问题?


曳光弹



使用曳光弹找到目标。



对于我们来说,最初的曳光弹就是,创建一个简单的工程,加一行“hello world!”,并确保其能编译和运行。然后,我们再去找整个应用程序不确定的部分,添加上让它们跑起来的骨架。


曳光弹.png


使用曳光弹代码的优势



  • 用户可以更早地获得能工作的东西。

  • 开发者构造了一个可以在其中工作的框架。

  • 你有了一个集成平台。

  • 你有可以演示的东西。

  • 你对进度有更好的感觉。


务实的偏执


务实的程序员会为自己的错误建立防御机制。



  • 客户和供应商必须就权利和责任达成共识。

  • 我们想要确保在找出 Bug 的过程中不会造成破坏。

  • 为你所做的假设编写主动校验的代码。

  • 只要我们总是坚持走小步,就不会才能够悬崖边掉下去。


契约式设计(DBC)


与计算机系统打交道很难,与人打交道更是难上加难。而契约规定了你的权利和责任,同时也规定了对方的权利和责任。文档化及主张进行检验是契约式设计的核心。


在编写代码之前,简单列出输入域的范围、边界条件是什么、例程承诺要交付什么——或更重要的是,没有承诺要交付什么——这对编写更好的软件来说,是一个巨大的飞跃。


尽早崩溃


尽快检测问题的好处之一是,可以尽早崩溃,而崩溃通常是你能做的最好的事。一旦代码发现本来不可能发生的事情已经发生,程序就不再可靠。这一刻开始,它所做的任何事情都是可疑的,所以要尽快终止它。


一个死掉的程序,通常比瘫痪的程序,造成的损害更小。


使用断言编程


无论何时,你发现自己在想“当然这是不可能发生的”时,添加代码来检查这一点,最简单的方式就是使用断言。注意这里的断言检查的是不可能发生的事情,普通的错误处理不要使用断言。


保持资源平衡


大多数情况下,资源使用遵循一个可预测的模式:



  • 分配资源

  • 使用它

  • 然后释放它


不要超出控制范围


把反馈的频率当作速度限制,永远不要进行“太大”的步骤或任务。


当你不得不做下面的事情的时候,你可能陷入了占卜的境地:



  • 估计未来几个月之后的完成日期

  • 为将来的维护或可扩展性预设方案

  • 猜测用户将来的需求

  • 猜测将来有什么技术可用


当你编码时


传统观点认为,一旦项目到了编码阶段,就几乎只剩下一些机械工作:只是把设计翻译成可运行的代码段而已。我们认为这种态度是软件项目失败的最重要原因。编码不是机械工作。务实的程序员会对所有代码进行批判性思考,包括自己的代码。我们不断看到程序和设计的改进空间


听从直觉


倾听自己直觉的方法:




  • 首先,停止正在做的事情。给自己一点时间和空间,让大脑自我组织。远离键盘,停止对代码的思考,做一些暂时不需要动脑筋的事情——散步、吃午饭、和别人聊天,或是先睡一觉。让想法自己从大脑的各个层面渗透出来:对此不用很刻意。最终这些想法可能上升到有意识的水平,这样你就能抓住一个”啊哈“的时刻。




  • 如果这些不起作用,就试着把问题外化。把正在写的代码涂画到纸上,或者向你的同事(最好不是程序员)解释一下怎么回事儿,向橡皮鸭解释下也行。把问题暴露给不同部分的大脑,看看有没有一部分大脑能更好地处理困扰你的问题。




重构



代码需要演化:它不是一个静态的东西。



马丁.弗勒将重构定义为:重组现有代码实体、改变其内部结构而不改变其外部行为的规范式技术。



  • 这项活动是有规范的,不应随意为之。

  • 外部行为不变;现在不是添加功能的时候


何时重构


当你学到一些东西时,当你比去年、昨天甚至是十分钟前更了解某事时,你会重构。


无论问题是多是少,都有可能促使我们对代码进行重构:



  • 重复:当你发现一处违背DRY原则的地方。

  • 非正交设计

  • 过时的知识:事情变化来,需求偏移了,你对问题的了解更多了——代码也需要成长。

  • 使用:当系统在真实的环境中被真实的人使用时,你会意识到,与以前的认识相比,一些特性现在看来更为重要,反而 “必须拥有” 的特性可能并不重要。

  • 性能:你需要将功能从系统的一个区域移动到另一个区域以提高性能。

  • 通过了测试重构应该是一个小规模活动,需要良好的测试支持。如果你添加了少量代码,并且通过了一个额外的测试,现在就有了一个很好的机会,来深入研究并整理刚刚编写的代码。



尽早重构,经常重构。



如何重构


重构的核心是重新设计。你或团队中的其他人设计任何东西的时候,都可以根据新的事实、更深的理解、更改的需求等重新设计。但是,如果你执拗地非要将海量的代码统统撕毁,可能会发现,自己所处的境地,比开始时更加糟糕。


重构是一项需要慢慢地、有意地、仔细地进行的活动。马丁.弗勒提供了一些简单技巧,可以用来确保进行重构不至于弊大于利:



  • 不要试图让重构和添加功能同时进行

  • 在重构开始之前,确保有良好的测试

  • 采取简单而慎重的步骤:将字段从一个类移动到另一个类,拆分方法,重命名变量。重构通常设涉及对许多局部进行的修改,这些局部修改最终会导致更大范围的修改。如果保持小步骤,并在每个步骤之后进行测试,就能避免冗长的调试。


结尾


本文只是分享了书中的一部自己觉得比较好的、可以实际使用的观点,并非书中所有观点),强烈建议大家读一下本书,相信会有不错的收获。最后贴个大佬对本书的评价,我也是因为刷到这个所以才知道此书的。


38e638e5-520d-4505-ad79-73dfc98ac523.png


作者:raymond
来源:juejin.cn/post/7269063053877411896
收起阅读 »

菜鸟前端故事之翅膀硬了

web
2019的故事 最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。 话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。 在过年的时候,我人生...
继续阅读 »

2019的故事


最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。


话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。


在过年的时候,我人生第一次有了上万存款,发了不少红包给家里老人和晚辈,那种回报家人的感觉真的很好。
现在看来9k的薪资不算什么,但算是很快达到了老爸的预期。若不是计算机专业和互联网的崛起,我这破学校不可能刚毕业就有这种薪资,我庆幸生对了时代,做了对的选择。


加薪


小美妆没有年终奖,年末聚餐的时候发了800块红包,我收到很开心只觉得这是一笔意外的收入。当时也不懂什么13薪之类的,更不知道、也不敢想象在互联网还有四五个月的年终奖,觉得没有很正常。即使有听说腾讯给全体发iphone、游戏工作室数十个月年终,也觉得很遥远,那远不是我能触及的层次,又何苦去比较。


包括小美妆的976作息,因为自己的菜,便也觉得不合理的事也是合理。


可喜的是刚过完年不久,小张总就给我们加了工资,1000块,我开心了很久,不是仅仅是因为多了1000块,另一层含义是我的月薪上万了,传说中的月薪过万我终于达到了,我还记得我当时激动的跟我爸妈还有女友分享,爸妈直呼老板良心让我跟着好好干,在他们眼里能主动加薪还加这么多的太难得了,回想起来还是觉得那一刻很幸福。


躺不平


今年我们有个任务就是要把小美妆的网站做成一个小程序,一度令当时的我头疼,因为我只会jquery连vue都不会更别说小程序了。一开始甚至想直接用小程序包裹webview直接套壳做好的网站了,但是理智和直觉告诉我不能那样,可能会挖大坑。


没办法只有埋头苦学,没想到仅仅两天我就可以上手了,并且把网站最难的特效部分转为用小程序实现了,原来小程序也没那么难嘛,心想我还是有点天赋的。


到后来的其他网站我开始尝试用vue去做,发现跟小程序几乎都是一样,也就顺风顺水了。估计这个顺序跟大多数前端都是反着来的,但也总算是会了一些主流现代的技术。
可惜那时候我还是用的引入CDN的方式引入vue,没有尝试构建工具,这也让我在后面的职业经历中吃了瘪。


在那一年我们做了很多小程序,移动端H5,后台管理系统,内部外部用的都有,对移动端开发、兼容有了一定的技术积累,后面也做了支付宝的小程序。


其实大多都是重复的技术,除了熟练度没有什么提高,到年中的时候已经算是进入平稳开发期,没有什么水花,也没有了深入学习的心思,因为大部分需求已经不需要动脑子
就能实现。用现在流行的一个词来说就是:躺平了。


小张总讲情义,只要我认真完成任务他应该不会赶我走,小作坊朋友式的公司氛围舒服,薪水也不错,不禁开始妄想熬成老员工躺平一辈子多好。


我突然意识到,自己好像成了守哥,可这似乎也没错。


幸运的是接下来发现的一件事让我意识到技术必须不断进步,而不幸的是明白这个道理让我们付出了惨重的代价。


昂贵的教训


在我们来之前公司是有一台旧的云服务器,是某财务软件公司给我们部署财务系统用,使用windowsServer系统,里面存放了相当多的财务信息,以及我们的公众号服务端项目、数据库。


在一天早上我们突然接到用户反馈公众号报错服务故障了,起初我们还以为又是夜间突发流量导致宕机了,心想着重启一下就好。可进入服务器一看却傻了眼,各种文件都被清空,变成了加密文件,连数据库也被删得一干二净只留下大约这么一段话:


recovery data connect email xxx


我们才意识到服务器被攻击入侵了,黑客将我们的数据全都加密,并勒索要求支付比特币才能解锁。


我们的财务数据、公众号用户的充值信息全没了。我清晰的记得,那一刻我的心凉到了冰点,因为我们的数据库已近半年没有备份。即使宽宏大量的小张总知道后也有些生气了,是啊,这件事带来的后果太麻烦了,毫无疑问技术部门负全责。


面对小张总的追问,我异常艰难的开口告诉了实情,在写这段的时候我仍然感觉令人窒息。
报警?支付赎金?找专业人士解密?那段时间我忙得团团转,急得像热锅上的蚂蚁,只想能够挽回损失。比特币赎金的价格高达十几万,而且也非常冒险很可能被再次勒索或者解不开,咨询专业人士都说解不了,甚至还有人冒出来说知道谁干的,可以作为中间人担保帮我们付费解锁。


我才明白原来这种事早已经形成了一条完整的黑色产业链,同时也对黑客这个词不再敬畏,而是深恶痛绝。随意的一次攻击可能毁掉一家公司,让上百无辜的人失业。


还记得有个坊间传闻说阿里面试某低学历高手,高手当面破解阿里系统,被破格录取并重用。有人如法炮制面试鹅厂,刚破解完腾讯系统当场报警被抓。以此来传言阿里格局大,腾讯格局小。虽然这个流言多半是假的,但我的态度在经历被勒索之后也变成了支持腾讯的一方,违法的事就是违法,不因场合而改变性质,永远也不应鼓励这种行为。不然谁都去破解一下,总有出大事的一天。


后来经过深思熟虑小张总还是决定不屈服,而是在现在基础上补救、向用户致歉补偿等措施。而我也开始拼命恶补网络安全方面的知识,这种情况要是再出现一次,即使老板不开除我,我自己也没脸待下去了。后来我们将系统换成了linux,把不该打开的端口也都关闭了,设置了强密码,安排了各种云服务商的安全服务,最重要的是做了db的定时备份。从那以后我们没再出过安全上的问题。


这件事给我好好的上了一课,在解决问题的过程中,面对很多技术一抹黑的境况也让我觉得心里没底,同时给了我要不断学习深入研究的决心。


技术态度的转变


自那以后,我意识到了技术是片汪洋大海,而之前的我不过是在小水坑里扑腾。我开始学习很多工作中暂时用不到的技术,或者在工作中尽量使用新的技术,这也是小团队的好处,没有历史包袱想怎么搞就怎么搞。


我开始学了react,学了php做自己厌恶的服务端,学了linux买了自己的服务器做了自己的网站,也用RN开始写App,第一次了解到了算法题的存在并开始练习,我疯狂的想要提升自己,不允许存在任何技术上的短板。我给自己列了长长的一串学习清单,还因为有其他公司看中我们的小程序想嵌入,去跟上市公司谈合作,签合同。那是除了在小外包入职时期以外的另一段高速汲取知识并成长的时光。



如果有前端初学的朋友在看,我想顺便提一下,个人认为想成为一名优秀的软件工程师,服务端是你无法避开的一环,虽然不需要精通,但是也不要一味的躲避。



2019在经历波折后归于平淡,在平淡的年末却又爆发了疫情,平时总是和女友两个城市来回跑,因为疫情居家被关到了一起,她还担心总被封闭在一起两个人会矛盾显现,结果没想到住一起那么快乐,感情越来越好,給枯燥的广漂生活增加了很甜蜜的一段时光。哈哈抱歉秀了一段,那时候真是觉得糟了,我陷入爱河啦。


离开小美妆


没有不散的宴席。


偶尔会听到小张总吐槽我们部门一年成本几十万好肉疼,不管是不是玩笑话,我也理解这种体量的公司养几个全职的程序员是极为奢侈的。


后来的近一年内我的工资逐渐涨到了12k左右,那时候的我对市面上的行情开始有了些了解,我同时兼任产品、ui、前端、项目管理的工作量,这个水平还是偏低的。


再加上当时的我也开始为未来做打算,我想要的东西很多,我也想给我爱的人更多,因为后来我接过了武汉房子的房贷,当时一个月扣除花费后只能存下三千块,照这个水平我猴年马月能过上我想要的生活啊。而小张总的话也侧面反应了我的薪水已经不会再有什么增长空间。说得更严重一点,技术部门甚至已经成为了公司的负担,后面小张总已经打算接外面的项目来做了。


有了爱人,有了技术,随之而来的便是动力与野心。


虽然我理解守哥,但我却不再想成为守哥。


总结下离开的3大原因:



  • 翅膀硬了觉得没有成长空间了

  • 真心希望给公司减负

  • 不想接外包项目
    基于以上的因素,我决定去到深圳,开始面试,我想要进入更强大的公司。


面试吃瘪之旅


第一次打开了招聘软件参与社招(找实习只用了下实习僧),被hr打招呼的热情搞得受宠若惊,后来才知道全是外包.....


很快就又遇到了某软,没错就是那个实习要录用又不留用我的某软,上来就要我学位证,没有社招经验的我还以为都是这样,啥也没干就老老实实的交了资料。


那时候不觉得外包经历是扣分项,得意洋洋的把小外包的实习经历写在了简历上,充了充工作年限,还写了自己兼任各种岗位(现在看来无疑是扣分项),估计hr心想这人真是个二笔。


hr先问了我现在的薪资,听完说他们这边最多给我13k,他们是按经验算薪资,你再牛不好意思你练习时长都还不够两年半,13k封顶了。我还是试着面了一下非常容易就过了,但是没选择去,只是试了试面试的感觉,我还是不傻的,这个涨幅和公司不值得。后来又面了一些公司比如万科、宝能之类的大型企业,基本有一半的通过率,那个阶段真是处于互联网的黄金年代,岗位多经济好。


当时很喜欢万科,可惜面试官问我微服务我根本答不上来,遗憾挂掉。至于bat之流的公司,还是不敢想。后来还是忍不住试着投了下腾讯,居然拿到了面试机会,我望着印着腾讯logo的面试邀请邮件,充满了渴望。至于面试结果当然不必期待,被一番吊打后挂掉,因为差距太大我也就对互联网大厂死了心。不过当时面试的小鹅拼拼部门据说非常累,而且后面整体被裁撤了。


之后通过了宝能的面试,在当时的我眼里可是大公司,公积金顶格交满,还有双休和食堂,虽然把我薪资压到了13k但我还是兴致勃勃准备去,可是offer一直没审批下来,当时搞得我一度很失落。


后来才知道宝能当时处于内部动荡期,已经锁了hc。


涨幅70%


宝能一直没消息我只能继续面,后来有一家大小周跨境电商公司给我了16x13,还有一家双休金融公司给了18x14,果断选择了后者,这个涨幅令当时的我激动不已,在接到oc后差点没跳起来,也算是正式年薪突破二十万了。


而且还给我了高级前端工程师的title,虽然也是在后来的经历中才知道非名企的职级就是个鸡肋,但当时可以说满足至极。意外的是宝能offer卡了其实是一件大好事,也或许是命运的眷顾。


因为过后不久宝能hr说愿意立即给我发offer,但是当时我已经入职金融公司就拒绝了。再后来就是宝能公积金断缴的消息,hr自己也离职了,再到前两天还看到宝能的姚总拖欠薪资被打的新闻,没进去走了狗屎运。


在拿到offer当天我跟小张总提了离职,他很惊讶,挽留了我并且说愿意加薪,但是我知道不可能在小美妆发展了,离开公司对双方都是好事,小张总的挽留可能只是出于客套或者情义。


其他几个小伙伴也很震惊,不理解这么舒服的地方为什么要离开,去外面肯定受拘束还要加班。可我已执意离开。


因为不好说那些肉麻的话,我在微信给小张总发了长长的感谢的文字。离开那月的工资多了几千奖金,我明白那是小张总对我的肯定,离职后的一天我仍然去了公司,把一个新项目的需求框架谈好才走,免得小伙伴们一时没法适应要自己去处理需求的情况。


至此,我正式离开了小美妆,而跟我一起来广州的小伙伴现在2023年仍然在那里就职。
非常感谢小张总的信任和包容,愿意给一个不认识的菜鸟机会,让他去组建团队,管理项目,而我也回报了一个两年流水过亿的项目。


这个项目也成为了我简历上闪耀的一段背书,或许也是之前大厂愿意给我面试机会的原因。


巅峰跃升


写得有点多了,下次再继续写在金融公司呆了不久又离开的故事,以及我职业经历的的再一次巅峰跃升,欢迎各位小伙伴留言交流~


作者:鹅猴
来源:juejin.cn/post/7264383071318671421
收起阅读 »

关于25届二本通过某大厂实习面试后从兴奋到放弃

关于25届二本通过某大厂实习面试后从兴奋到放弃 背景 我是南方的一名25届前端程序员,找的是前端实习生的岗位,标题中的提到的大厂是北方的一家公司,就让我暂时把它和它所在的城市称为A厂和A市吧。(自身原因没法入职,我只是记录一下自己这段时间的心路历程,这家厂子的...
继续阅读 »

关于25届二本通过某大厂实习面试后从兴奋到放弃


背景


我是南方的一名25届前端程序员,找的是前端实习生的岗位,标题中的提到的大厂是北方的一家公司,就让我暂时把它和它所在的城市称为A厂和A市吧。(自身原因没法入职,我只是记录一下自己这段时间的心路历程,这家厂子的面试官真的很好,看到下面大家就知道了)


面试通过后的兴奋


2023年9月8号二面结束后,一面面试官给了我口头oc。我记得当时的我很恍惚,流程很快,从约面到口头oc也就两天,直到那个时候我还是有点不相信一个普通二本的学生能被A厂约面,不相信我能通过面试,毕竟现在的大环境大家或多或少都有亲身体验。然后我添加了未来ld的联系方式,好像一切都在往好的方向发展,一个大厂对于一个在校生的吸引是巨大的。当时的我觉得,我接下来应该就是买票、租房、入职了,我也这么以为。


觉得一切充满希望


我把这个好消息告诉了家里人,意料之中遭到了家里人的反对,我记得当时是:“你才大三,你不要那么急,附近找一个公司先过渡一下,不要一下子就跑那么远”。我理解他们的担心,但当时我的情绪也很上头,最后不欢而散。之后,我开始在网上加上一些在A厂实习的同学,加上A厂的实习同学之后,我跟他们聊了很多,就好像我已经可以成为他们的同事一样,在网上查阅一些资料,问好了租房,公司的制度,我觉得我还年轻,我可以克服很多困难,我觉得我不应该被局限,我想我做好准备了。但是,王子不一定能救出公主,走向未来的桥梁也不一定能筑起。


开始迷茫


理想和现实总是背道而驰,我知道了实习生第一个月的高额消费,还是在A市,对于我这个在校生是一个自己拿不出手的数额,我开始动摇,也是我自己对于这一方面没有做好准备,我开始咨询起之前在那边实习过的一些师兄师姐,跟他们取经,知道了去远的地方实习注定是哪里的挣钱哪里花,有的时候甚至是贴钱实习。我承认那会我有点退却了,想到一个人去一个那么远的地方,自己找房子,自己适应那边的生活,我开始觉得自己好像并不是那么勇敢。咨询了很多朋友,他们有的说:“你还年轻,这点距离不算什么,出去闯闯”。期间我也想过去借钱,去跟家里人商量一下,内心挣扎了很久,但是一直以来我都是那种不到迫不得已绝不会向朋友借钱,也不愿意让家里人去掏这个钱让我去镀金,也可能我当时对一切的未知充满了恐惧。


放弃前夕


d0b91d643a227ba0fec3b845e0419f8.jpg
这是我想清楚拒掉这个offer前一晚所写的,我想我并不是一个勇敢的人。


哭死,感谢面试官


2023年9月12日,一直在等我确认入职时间给我网申offer的"ld"却等来了我决定拒绝offer的消息,我把我的困难和拒绝offer的原因告诉了他,我本来以为这样子事情就已经告一段落了。可是后面他继续联系我,来了解我的难处,说如果我真的想去那边实习,他愿意为我提供帮助,让我安心去那边实习,其他的他来解决。他当时的原话是:“哈哈,没事,就看看你有啥困难,初期的困难是正常的,这个能帮到还是会帮到的”。我真的那会,我哭死,但是因为一些原因还是没能去那边实习,在这里真诚的感谢他。


好啦,翻篇啦,继续加油


作者:oversize的前端男孩
来源:juejin.cn/post/7277828214247768083
收起阅读 »

如何制作 GitHub 个人主页

web
原文链接:http://www.bengreenberg.dev/posts/2023-… 人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源...
继续阅读 »

原文链接:http://www.bengreenberg.dev/posts/2023-…


人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主页可能是人们为了了解你而去的第一个地方。


你希望你的GitHub个人主页说些什么?你希望如何以简明易读的方式向访客表达对你的重要性以及你是谁?无论他们是未来的雇主还是开源项目的潜在合作伙伴,你都必须拥有一个引人注目的个人主页。


使用GitHub Actions,你可以把一个静态的markdown文档变成一个动态的、保持对你最新信息更新的良好体验。那么如何做到这一点呢?


我将向你展示一个例子,告诉你如何在不费吹灰之力的情况下迅速做到这一点。在这个例子中,你将学习如何抓取一个网站并使用这些数据来动态更新你的GitHub个人主页。我们将在Ruby中展示这个例子,但你也可以用JavaScript、TypeScript、Python或其他语言来做。


GitHub个人主页如何运作


你的GitHub个人主页可以通过在网页浏览器中访问github.com/[你的用户名]找到。那么该页面的内容来自哪里?


它存在于你账户中一个特殊的仓库中,名称为你的账户用户名。如果你还没有这个仓库,当你访问github.com/[你的用户名]时,你不会看到任何特殊的内容,所以第一步是确保你已经创建了这个仓库,如果你还没有,就去创建它。


探索仓库中的文件


仓库中唯一需要的文件是README.md文件,它是你的个人主页页面的来源。


./
├── README.md

继续在这个文件中添加一些内容并保存,刷新你的用户名主页,你会看到这些内容反映在那里。


为动态内容添加正确的文件夹


在我们创建代码以使我们的个人主页动态化之前,让我们先添加文件夹结构。


在顶层添加一个名为.github的新文件夹,在.github内部添加两个新的子文件夹:scripts/workflows/


你的文件结构现在应该是这样的:


./
├── .github/
│ ├── scripts/
│ └── workflows/
└── README.md

制作一个动态个人主页


对于这个例子,我们需要做三件事:



  • README中定义一个放置动态内容的地方

  • scripts/中添加一个脚本,用来完成爬取工作

  • workflows/中为GitHub Actions添加一个工作流,按计划运行该脚本


现在让我们逐步实现。


更新README


我们需要在README中增加一个部分,可以用正则来抓取脚本进行修改。它可以是你的具体使用情况所需要的任何内容。在这个例子中,我们将在README中添加一个最近博客文章的部分。


在代码编辑器中打开README.md文件,添加以下内容:


### Recent blog posts

现在我们有了一个供脚本查找的区域。


创建脚本


我们正在构建的示例脚本是用Ruby编写的,使用GitHub gem octokit与你的仓库进行交互,使用nokogiri gem爬取网站,并使用httparty gem进行HTTP请求。


在下面这个例子中,要爬取的元素已经被确定了。在你自己的用例中,你需要明确你想爬取的网站上的元素的路径,毫无疑问它将不同于下面显示的在 posts 变量中定义的,以及每个post的每个titlelink


下面是示例代码,将其放在scripts/文件夹中:


require 'httparty'
require 'nokogiri'
require 'octokit'

# Scrape blog posts from the website
url = "<https://www.bengreenberg.dev/blog/>"
response = HTTParty.get(url)
parsed_page = Nokogiri::HTML(response.body)
posts = parsed_page.css('.flex.flex-col.rounded-lg.shadow-lg.overflow-hidden')

# Generate the updated blog posts list (top 5)
posts_list = ["\n### Recent Blog Posts\n\n"]
posts.first(5).each do |post|
title = post.css('p.text-xl.font-semibold.text-gray-900').text.strip
link = "<https://www.bengreenberg.dev#{post.at_css('a')[:href]}>"
posts_list << "* [#{title}](#{link})"
end

# Update the README.md file
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
repo = ENV['GITHUB_REPOSITORY']
readme = client.readme(repo)
readme_content = Base64.decode64(readme[:content]).force_encoding('UTF-8')

# Replace the existing blog posts section
posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m
updated_content = readme_content.sub(posts_regex, "#{posts_list.join("\n")}\n")

client.update_contents(repo, 'README.md', 'Update recent blog posts', readme[:sha], updated_content)

正如你所看到的,首先向网站发出一个HTTP请求,然后收集有博客文章的部分,并将数据分配给一个posts变量。然后,脚本在posts变量中遍历博客文章,并收集其中的前5个。你可能想根据自己的需要改变这个数字。每循环一次博文,就有一篇博文被添加到post_list的数组中,其中有该博文的标题和URL。


最后,README文件被更新,首先使用octokit gem找到它,然后在README中找到要更新的地方,并使用一些正则: posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m


这个脚本将完成工作,但实际上没有任何东西在调用这个脚本。它是如何被运行的呢?这就轮到GitHub Actions出场了!


创建Action工作流


现在我们已经有了脚本,我们需要一种方法来按计划自动运行它。GitHub Actions 提供了一种强大的方式来自动化各种任务,包括运行脚本。在这种情况下,我们将创建一个GitHub Actions工作流,每周在周日午夜运行一次该脚本。


工作流文件应该放在.github/workflows/目录下,可以命名为update_blog_posts.yml之类的。以下是工作流文件的内容:


name: Update Recent Blog Posts

on:
schedule:
- cron: '0 0 * * 0' # Run once a week at 00:00 (midnight) on Sunday
workflow_dispatch:

jobs:
update_posts:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Install dependencies
run: gem install httparty nokogiri octokit

- name: Scrape posts and update README
run: ruby ./.github/scripts/update_posts.rb
env:
GITHUB_TOKEN: $
GITHUB_REPOSITORY: $

这个工作流是根据cron语法定义的时间表触发的,该时间表指定它应该在每个星期天的00:00(午夜)运行。此外,还可以使用workflow_dispatch事件来手动触发该工作流。


update_posts工作由几个步骤组成:



  • 使用 actions/checkout@v2操作来签出仓库。

  • 使用 ruby/setup-ruby@v1 操作来设置 Ruby,指定的 Ruby 版本为 3.1。

  • 使用 gem install 命令安装所需的 Ruby 依赖(httpartynokogirioctokit)。

  • 运行位于.github/scripts/目录下的脚本 update_posts.rbGITHUB_TOKENGITHUB_REPOSITORY环境变量被提供给脚本,使其能够与仓库进行交互。


有了这个工作流程,你的脚本就会每周自动运行,抓取博客文章并更新README文件。GitHub Actions负责所有的调度和执行工作,使整个过程无缝且高效。


将所有的东西放在一起


如今,你的网络形象往往是人们与你联系的第一个接触点--无论他们是潜在的雇主、合作者,还是开源项目的贡献者。尤其是你的GitHub个人主页,是一个展示你的技能、项目和兴趣的宝贵平台。那么,如何确保你的GitHub个人主页是最新的、相关的,并能真正反映出你是谁?


通过利用 GitHub Actions 的力量,我们展示了如何将你的 GitHub 配置文件从一个静态的 Markdown 文档转变为一个动态的、不断变化关于你是谁的例子。通过本指南提供的例子,你已经学会了如何从网站上抓取数据,并利用它来动态更新你的 GitHub个人主页。虽然我们的例子是用Ruby实现的,但同样的原则也可以用JavaScript、TypeScript、Python或你选择的任何其他语言来应用。


回顾一下,我们完成了创建一个Ruby脚本的过程,该脚本可以从网站上抓取博客文章,提取相关信息,并更新你的README.md文件中的"最近博客文章"部分。然后,我们使用GitHub Actions设置了一个工作流,定期运行该脚本,确保你的个人主页中保持最新的内容。


但我们的旅程并没有就此结束。本指南中分享的技术和方法可以作为进一步探索和创造的基础。无论是从其他来源拉取数据,与API集成,还是尝试不同的内容格式,都有无限的可能性。


因此,行动起来让你的 GitHub 个人主页成为你自己的一个充满活力的扩展。让它讲述你的故事,突出你的成就,并邀请你与他人合作。


以上就是本文的全部内容,如果对你有所启发,欢迎点赞、收藏、转发~


作者:chuck
来源:juejin.cn/post/7251884086537650232
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpress/2…


作者:Mengke
来源:juejin.cn/post/7251884086536781880
收起阅读 »

前端地位Up!

web
背景 大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C... 然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种...
继续阅读 »

背景


大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C...


然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种小白进阶培训班、是业务形态的致命一击。最终是前端在技术圈子里不如狗的地位。


何至于此?


框架-砒霜or蜜糖?


说说框架


前段跟一个google的算法大佬聊天,他说你们前端很奇怪,现在浏览器已经非常快了,但你们非要搞这样那样的框架,然后去算这个算那个,搞些hooks虚拟dom,完全看不懂在干嘛。


也许我想他说的是对于web的性能层面是对的。


首先虚拟dom也就是中间层如果纯论性能在我看来其实是并不适合现在的时代的,它在这个时代的作用就是作为多端统一以及在真实dom操作前的数据缓冲/计算层...这可能也是这个时代出现了SvelteSolid为代表的此类框架的原因。我倒是希望现在web前端的方向能走向SolidSvelte这种框架的周边社区完善开发。


可是时代的潮流不会随着个人的意志改变,果然时代是分分合合吗,现在ssr为代表的next越来越火(可能有一部分vecel的商业原因),但更重要的一个原因是去改善开发人员的体验(在多端和最佳实践方面),也就是卷颗粒度


以next举例子:我对于next其实是这么理解的,粗颗粒度reactcsr已经进无可进了,改也不好改,那么转化一下方向把,在ssr的领域去降低粒度,就像流式渲染等。


说说人


啊真糟糕,怎么情不自禁就在说框架了,是不是发现我们前端情不自禁在开发or说某个技术的时候就会与框架挂钩。就像是面试我懂某个框架的原理、我学了几个框架诸如此类的,于是我们就从一个框架到另外个框架反反复复的在路上走、打包工具也是一样的(我真的需要这么快的打包工具吗?)。


于是我们就在框架中沉溺了,也许后面会出现一些5年vue工程师10年react工程师,我们整日沉浸在框架之中,日复一日,用着固定的写法(其实我在说Vue,React在这方面会好一些),做着相似的事情,技术的成长变为了我某个框架用得怎么样。


前端工程师or框架工程师


我想啊,前端的潮流很快(娱乐圈),但其实我们要明白一个道理,我用这个东西学这个东西对我有没有收益,对用户体验是不是有很大的提升,对团队开发有没有效率的进步。如果没有的话,不如搞浏览器,当然也可以学学当个PPT天才(纯褒义)或者业务、算法、其他语言等(好堕落啊现在不是PPT就是搞业务),着实没必要把绝大多数时间留着框架上,看多了就会觉得自己很牛逼,然后开摆。


毕竟我们不是框架开发工程师(也就是资源型工具人)、我们是前端开发工程师,我们是面向屏幕开发,我们是人机交互工程师,也就是现在的一个词终端工程师,如果你不能把你的应用在所有屏幕(安卓、Ios、桌面、PC、平板)跑那应该是不合格的。


业务形态


害,说这个之前闲聊一下,我们可以看到一些产品诸如语雀云凤蝶Antd等。蚂蚁体验技术部真的把前端的地位上拉了一截,他们真的很好,可能是未来5年在国内都不会再有这么好的标志性的前端产品,可惜没有一个闭环的商业业务形态,就类似next这种,我在这里不讨论具体的一些原因和后面发生的一些事情。


进入正文,产品和业务形态决定了前端的地位,后端开发通常被认为是应用程序的基础和核心。但其实怎么说那,有的时候其实是因为国内产品思维的局限于和上文提到的沉溺于框架和搜索工具


因为我做过不少国外的产品,有一些很有创造力和创新思维的产品会提出很多天马行空,极具艺术的产品交互效果和体验,在这类产品中其实前端的地位并不算低。


但是在国内就会有这种情况:



  1. 产品不会有这种想法,他的脑子里也是一些国内的那些很普通的竞品,和数据流转逻辑

  2. 前端自己拒绝,一般来说心路历程是这样的,我先看看能不能做 => 去百度掘金搜索 => 搜不到或者框架里没有,好感觉不好做,太复杂了 => 我们换一个普通一点的效果(理由五花八门)。

  3. 大家都是这么做的,那我们这个也这么做吧。


但其实我们自己作为科技触达用户的桥梁,是有能力去推动这个事情的,一个炫酷的配色、合理的交互效果、好看的页面,是可以去给产品去给设计说的,比如我自己有时候会figma或者lottie去自己画一些图和动画效果,去主动纠正设计的颜色和间距。难道产品会拒绝让产品变得更好?设计会拒绝更好看?


说白了,自己不想去做不想去推不会也不想学,觉得很复杂,当然如果实在没有这个土壤果断跑路。


Javascript本身的问题


Javascript吊打RustC估计我是这辈子都看不到了。


Javascript是解释性语言肯定没法跟一些编译语言竞天生就不行,再加上单线程即使有解决方案也就那样。这意味着前端掌握更多语言几乎是一个必要的事情,JavaSwift,Oc这些本来就会用到的不必多说,RustPython选一门掌握也很好。


会得越多你越强,当然我还是建议大家去当PPT天才


前端自信


以后,大伙自信一点,别觉得前端就不如其他技术岗位,地位都是自己争取的,前端优势很大,语言统一、前端立马可见的效果、前端基建相对较小、前端宿主环境统一Docker和容器配置相对统一等。主要是时间,有更多的时间意味着可以做更多的事学更多的东西更多的钱~。


总结


So,改变前端环境从你我做起,你卷一波我卷一波,前端的门槛就提起来了,以后面试的基本要求就是:前端要会Js、Ts、Java、Swift、混合框架、PWA,然后薪资30k起步。


作者:溪饱鱼
来源:juejin.cn/post/7283642910301192244
收起阅读 »

一次性弄清前端上线和生产环境地址

web
💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑 前端项目打包后,我打包过的静态资源是如何访问后端服务的? 访问后端服务的时候难道不存在跨域的问题吗?如何解决的? 假如我想自己简单修改下部署后的目录该如何去做? 🥲其实不仅仅是你会有这样的疑惑,包...
继续阅读 »

💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑




  1. 前端项目打包后,我打包过的静态资源是如何访问后端服务的?

  2. 访问后端服务的时候难道不存在跨域的问题吗?如何解决的?

  3. 假如我想自己简单修改下部署后的目录该如何去做?


🥲其实不仅仅是你会有这样的疑惑,包括我在内刚接触前端的时候,由于没有后端的开发经验也会被这些问题所困扰,但是今天我们将一次性弄清楚这个问题,让我们前端的知识体系由点成线,由线成面~


一.明确问题




我们知道,我们平时在开发的时候一般都是使用proxy进行代理的,它的原理是:浏览器会先去访问本地的node服务器,然后node服务器再去代理到你要访问的后端api接口,但是我们可能平时没有node服务器的概念,因为node服务器在webpack中,我们一般是通过下面这种方式来设置



但是我们的项目上线后这种方式就不能用了,(因为Node是我们本地开发的环境,并没有办法在线上使用。)其实,我们一般会通过后端的Nginx代理来解决跨域的问题,但是你知道前端的生产地址配置是什么吗?如何通过Nginx访问后端接口呢?是直接配置的类似于http://www.xxxx.com/api/aaa这样的路径呢?还是直接是一个相对路径/prod?要想搞清楚这些,首先就要了解什么是Nginx


二.什么是Nginx




🐻是一个开源的高性能、轻量级的Web服务器和反向代理服务器,它具有事件驱动,异步非阻塞的架构,被广泛应用于构建高性能的网站,应用程序和服务。


🤡在平时开发中我们经常听到正向代理反向代理这两个名词,那么什么是反向代理,什么是正向代理哪?



  1. 反向代理:服务器的IP是被屏蔽的,也就是说客户端不知道服务器真实的地址是哪个,客户端访问的地址或者域名是访问的Nginx服务器的地址。




  1. 正向代理:和反向代理刚好相反,这个时候服务器不知道真正的客户端是哪个,也就是相当于直接访问服务器的是nginx代理服务器。



三.前端使用Nginx解决跨域




🤗什么是跨域,跨域是指在浏览器的环境下,当一个网页的JavaScript代码通过Ajax Websocket或其他技术发送HTTP请求的目标资源位于不同的域名,端口或者协议下,就会发生跨域。


🐻Nginx如何解决跨域,因为服务器和服务器之间互相请求不发生跨域,所以解决跨域的方法之一就是使用这种方案



  1. 浏览器或者客户端发送请求:http:www.xxx.com:80Nginx服务器对80端口进行监听,Nginx服务器将请求转发到后端真实的服务器地址,这样就实现了代理。




  1. Nginx基本配置项解析


server {
listen 80;
server_name yourdomain.com;

location / { // 无论访问所有路径都返回前端静态资源dist内容
root /path/to/your/frontend;
index index.html;
try_files $uri $uri/ /index.html;
}

location /api/ {
proxy_pass http://backend-server-address/api/; // 真实后端api地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

当在前端代码中使用相对路径/api/users发起请求时候Nginx会将这个请求转发到真实的后端地址,不过你发现了没很多前端项目种生产环境地址都仅仅是一个/api类似这样的相对路径,很少直接是一个绝对路径。


当你请求 yourdomain.com 时,Nginx 会将前端静态资源返回给前端。前端代码中使用的相对路径 /api会基于当前域名 yourdomain.com构建完整的请求 URL。因此,前端代码请求后端地址的完整 URL 将是 yourdomain.com/api/xxx,其中 /xxx表示具体的后端接口路径。


Nginx 的反向代理配置中的 location /api/ 指令将匹配以 /api/ 开头的请求,并将这些请求代理到后端服务器上。因此,当前端代码发起相对路径请求 /api/xxx 时,Nginx 会将请求转发到 yourdomain.com/api/xxx,实现与后端接口的通信。


总结来说,前端代码中的相对路径 /api会根据当前域名构建完整的请求 URL,而 Nginx 的反向代理配置将这些请求转发到后端服务器上的相应路径。这样,前端代码就能够与后端接口进行通信。


四.前端生产环境配置




🥲既然Nginx如何代理的,以及前端打包的路径一般是什么样的我们知道了,那么我们就来唠唠作为一个前端小白该如何快速的完整的构建一个基础项目吧,其实如果基础开发的话,我们往往会使用脚手架,就以Vue开发的话,我们可以使用vuecli来快速构建项目,其实构建完之后你就可以直接npm run build打出的包就可以部署在后端服务器的,这个打出的包的根路径是默认的/,通过上面的Nginx的知识我们应该不难理解。


🤡如果我们要自己想去修改一个路径哪?我们可以在vue.config.js中进行配置,配置如下


module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '/prod' : '/'
};

👹这样打出的包的静态资源的路径就是下边这样的



🥰如果是直接使用的默认的打包路径就是如下这种



五.总结




🤡最后总结一下,前端上线打包完就是一个静态文件,是一个相对路径,后端会通过Nginx来监听这个资源的请求,当匹配到/就返回静态资源,当匹配到某个/prod就将请求反向代理到后端真实服务器的地址,前端打包的是一个相对路径,Nginx会在前面拼上去具体的域名或者ip,这样就打通了线上前端访问的基本内容。


作者:一溪风月
来源:juejin.cn/post/7291952951048060940
收起阅读 »

同事看到我填写bug原因的表单, 惊呆了, 那么多字段怎么自动填充了?

web
在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段. 其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等. 同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢? 实现难度非常简单, 而在日常工...
继续阅读 »

在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段.
其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等.


同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢?


实现难度非常简单, 而在日常工作中非常有用, 并且有点小帅. 所以分享给大家.


用什么工具来修改你的网页


我选择的是arc浏览器的boost功能. 在网页上新建一个boost, 点击code, 选到js的tab就可以把编写的js插入到当前host的网页里运行了. 还有辅助功能zag可以帮你抓dom.


对于没有用arc浏览器的大家, 可以写一个chrome extension, 只需要使用content_scripts功能, 可以实现和arc的boost类似的功能: match网址url并且加载一段js. (其实功能是比arc多且灵活的)


修改jira页面


jira是个必须用, 且很多重复操作的网站. 我做了这些修改:


站会看板过滤器顺序调整


每天站会轮到的人的顺序和jira看板上不一致, 导致站会轮下一个人的时候得去找下一个人的位置. 只要获取一下看板过滤器, 调整一下子元素就行了.


const container = document.getElementsByClassName('aui-expander-content ghx-quick-content')[0]
container.children[6].remove()
container.children[10].remove()
container.children[6].after(container.children[2])
container.children[9].after(container.children[1])

看板过滤器多选改单选


jira看板的过滤器是多选的, 所以切换下一个人的时候必须把前一个人取消了, 这样每次都多一次操作.


我们只要给每个过滤器加一个点击事件, 把其他active的过滤器都点击一下就行了.


let child = null
container.onclick = function (e) {
if (child) return
child = e.target
for (let i = 0;i < container.children.length; i++) {
if (container.children[i].children[0] && container.children[i].children[0].classList.contains('ghx-active') && container.children[i].children[0].innerHTML !== child.innerHTML) {
container.children[i].children[0].click()
}
}
child = null
}

关闭bug的时候必须填写原因


公司有个规定, 关闭jira必须填写一些字段. 其实每次填写的内容都一样的, 自动填写可以节省非常多时间.


实现也非常简单, 定时器来寻找指定dom, 然后为这些dom附上指定的值.


const setInputValue = (id, value) => {
if (document.getElementById(id)) {
document.getElementById(id).value = value
}
}

setInterval(() => {
setInputValue('resolution', 10000)
setInputValue('customfield_10903', 12502)
setInputValue('customfield_12301', `故障原因:代码错误
解决方式:修复
影响范围:界面
故障处理人:yo-cwj.com`
)
}, 1000)

获取vue应用的实例来修改界面


老婆画了几套微信表情, 于是我经常登录上去看数据.


但dashboard上信息很少, 需要点到每个表情的详情中才能查看.


通过网络请求, 我看到其实在dashboard的界面, 数据已经请求到了, 于是开始我们的修改.


从dom中寻找vue实例


通过基础的vue知识, 我们知道vue实例是会挂在dom上的.


(vue作者说可以认为他是可用的, 因为vue的devtool也是依赖这个特性的, 那我们一个小脚本是更可用了)


那么哪些dom上有vue实例, 有点像个面试题, 写个简单的脚本就可以找到:


let traverse = (dom) => {
if (dom.__vue__) {
console.log(dom.__vue__._data)
}
for (let i = 0; i < dom.children.length; i++) {
traverse(dom.children[i])
}
}
traverse()

找到目标数据所在的dom, 正式的脚本就这样获取vue实例就可以了.


编写脚本


首先通过vue实例的_data属性获取到数据:


const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;

然后把数据贴到对应的dom上:


const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');
for (let i = 0; i < emotion_dom.children.length; i++) {
emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`
}

到这里脚本就写完了, 其他的vue应用其实还可以调用vue实例中的方法获取数据, 或自己获取数据放进vue实例.


解决执行环境的问题


但把这段代码放到boost中会出现拿不到dom的\_\_vue\_\_实例的问题, 因为boost和chrome extension的执行环境并不是浏览器执行环境. 可以通过创建script并执行的方式.


let script = document.createElement('script');
script.textContent = "const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;" +
"const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');" +
"for (let i = 0; i < emotion_dom.children.length; i++) {" +
" emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`" +
"}";
setTimeout(() => {
document.documentElement.appendChild(script);
}, 1000)

作者:nujnewnehc
来源:juejin.cn/post/7288628985322307599
收起阅读 »

Jetpack Compose 实现仿淘宝嵌套滚动

前言 嵌套滚动是日常开发中常见的需求,能够在有限的屏幕中动态展示多样的内容。以淘宝搜索页为例,使用 Jetpack Compose 实现嵌套滚动。 NestedScrollConnection Compose 中可以使用 nestedScroll 修饰...
继续阅读 »

前言


嵌套滚动是日常开发中常见的需求,能够在有限的屏幕中动态展示多样的内容。以淘宝搜索页为例,使用 Jetpack Compose 实现嵌套滚动。






NestedScrollConnection


Compose 中可以使用 nestedScroll 修饰符来自定义嵌套滚动的逻辑,其中 NestedScrollConnetcion 是连接组件与嵌套滚动体系的关键,它提供了四个回调函数,可以在子布局获得滑动事件前预先消费掉部分或全部手势偏移量,也可以获取子布局消费后剩下的手势偏移量。


interface NestedScrollConnection {

fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero

fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
)
: Offset = Offset.Zero

suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero

suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return Velocity.Zero
}
}

onPreScroll


方法描述:预先劫持滑动事件,消费后再交由子布局。


参数列表:



  • available:当前可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回 Offset.Zero


onPostScroll


方法描述:获取子布局处理后的滑动事件


参数列表:



  • consumed:之前消费的所有滑动事件偏移量

  • available:当前剩下还可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回 Offset.Zero ,则剩下偏移量会继续交由当前布局的父布局进行处理


onPreFling


方法描述:获取 Fling 开始时的速度。


参数列表:



  • available:Fling 开始时的速度


返回值:当前组件消费的速度,如果不想消费可返回 Velocity.Zero


onPostFling


方法描述:获取 Fling 结束时的速度信息。


参数列表:



  • consumed:之前消费的所有速度

  • available:当前剩下还可用的速度


返回值:当前组件消费的速度,如果不想消费可返回Velocity.Zero,剩下速度会继续交由当前布局的父布局进行处理


实现嵌套滚动


示例分析


如截图所示的搜索页可以分为5个部分。




  • 搜索栏位置固定,不随滑动而改变




  • Tab栏、店铺卡片、筛选栏、商品列表随滑动事件改变位置



    • 当手指向上滑动时,首先店铺卡片向上滑动,伴随透明度降低,接着tab栏和排序栏一起向上滑动,最后列表内的条目才会被向上滑动。

    • 当手指向下滑动,首先tab栏和排序栏向下滑动,接着列表内的条目向下滑动,最后店铺卡片才会出现。





设计实现方案


选择 LazyColumn 作为子布局实现商品列表,Tab栏、店铺卡片、筛选栏作为另外三个部分,放置在同一个父布局中统一管理。LazyColumn 已经支持嵌套滚动系统,能够将滑动事件传递给父布局,因此我们希望在子布局消费滑动事件的前、后,由父布局消费一部分滑动事件,从而改变Tab栏、店铺卡片、筛选栏的布局位置。

































滑动事件 消费顺序 处理的位置
手指上滑
available.y < 0
1. 店铺卡片上滑 onPreScroll 拦截
2. Tab栏、筛选栏上滑
3. 列表上滑 子布局消费
手指下滑
available.y > 0
1. Tab栏、筛选栏下滑 onPreScroll 拦截
2. 列表下滑 子布局消费
3. 店铺卡片下滑 自动分发到父布局

实现 SearchState 管理滚动状态


模仿 ScrollState,实现 SearchState 以管理父布局的滚动状态。value 代表当前滚动的位置,maxValue 代表父布局滚动的最大距离,从0到 maxValue 的范围又被商品卡片的高度 cardHeight 划分为两个阶段。定义 canScrollForward2 标记是否处在应该由Tab栏、筛选栏滑动的区间。


value消费滑动事件的控件
0 <= value < cardHeight店铺卡片滑动
cardHeight <= value < maxValueTab栏、筛选栏滑动
value = maxValue商品列表滑动

@Stable
class SearchState {
// 当前滚动的位置
var value: Int by mutableStateOf(0)
private set
var maxValue: Int
get() = _maxValueState.value
internal set(newMax) {
_maxValueState.value = newMax
if (value > newMax) {
value = newMax
}
}
var cardHeight: Int
get() = _cardHeightState.value
internal set(newHeight) {
_cardHeightState.value = newHeight
}
private var _maxValueState = mutableStateOf(Int.MAX_VALUE)
private var _cardHeightState = mutableStateOf(Int.MAX_VALUE)
private var accumulator: Float = 0f

// 同 ScrollState 实现,父布局不会消费超过 maxValue 的部分
val scrollableState = ScrollableState {
val absolute = (value + it + accumulator)
val newValue = absolute.coerceIn(0f, maxValue.toFloat())
val changed = absolute != newValue
val consumed = newValue - value
val consumedInt = consumed.roundToInt()
value += consumedInt
accumulator = consumed - consumedInt

// Avoid floating-point rounding error
if (changed) consumed else it
}

private fun consume(available: Offset): Offset {
val consumedY = -scrollableState.dispatchRawDelta(-available.y)
return available.copy(y = consumedY)
}

// 是否应该进行第二阶段滚动,改变Tab栏和搜索栏的偏移
val canScrollForward2 by derivedStateOf { value in cardHeight..maxValue }
}

@Composable
fun rememberSearchState(): SearchState {
return remember { SearchState() }
}

实现 NestedScrollConnection


根据上文所述,需要在 onPreScroll 回调函数在合适的时机拦截滑动事件,使得父布局在子布局之前消费滑动事件。


internal val nestedScrollConnection = object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 手指向上滑动时,直接拦截,由父布局消费,直到超过 maxValue,再由子布局消费
return if (available.y < 0) consume(available)
// 手指向下滑动时,在 cardHeight 到 maxValue 的区间内由父布局拦截,在子布局之前消费
else if (available.y > 0 && canScrollForward2) {
val deltaY = available.y.coerceAtMost((value - cardHeight).toFloat())
consume(available.copy(y = deltaY))
} else super.onPreScroll(available, source)
}
}

另外,为了操作体验的连续性,如果触摸了 LazyColumn 以外的区域,并且手指不离开屏幕持续向上滑动,在超出父布局能消费的范围后,我们希望能将剩余滑动事件再传递给子布局继续消费。为了实现这一功能,增加一个 NestedScrollConnection 对象,在 onPostScroll 回调中,将父布局消费后剩余的滑动事件传递到 LazyColumn 内部。这里处理了拖拽的情况,对于这种情况下 fling 速度的传递,也将在下文处理。


@Composable
fun Search(modifier: Modifier = Modifier, state: SearchState = rememberSearchState()) {
val flingBehavior = ScrollableDefaults.flingBehavior()
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
val outerNestedScrollConnection = object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
)
: Offset {
if (available.y < 0) {
scope.launch {
// 由子布局 LazyColumn 继续消费剩余滑动距离
listState.scrollBy(-available.y)
}
return available
}
return super.onPostScroll(consumed, available, source)
}
}
Layout(...) {...}
}

实现父布局及其 MeasurePolicy


由于需要改变父布局中内容的放置位置,使用 Layout 作为父布局,其中前三个子布局使用 Text 控件标识,对店铺卡片设置动态透明度。


Layout(
content = {
// TopBar()
Text(text = "TopBar")
// ShopCard()
Text(
text = "ShopCard",
// 背景和文字都随着滑动距离改变透明度
modifier = Modifier
.background(
alpha = 1 - state.value / state.maxValue.toFloat()
)
.alpha(1 - state.value / state.maxValue.toFloat())
)
// SortBar()
Text(text = "SortBar")
// CommodityList()
List(listState)
},
...
)

Layout 控件并不默认支持嵌套滚动,因此需要使用 scrollable 修饰符使其能够滚动并参与到嵌套滚动系统中。将 SearchState 中的 scrollableState 作为 state 入参,在 flingBehavior 入参中将父布局未消费完的 fling 速度,传递给子布局 LazyColumn 继续消费,使得操作体验连续。


前文实现了两个 NestedScrollConnection 对象,分别用于处理父布局和子布局消费前后的滑动事件,在 Layout 的 Modifier 对象中使用 nestedScroll 修饰符进行组装。由于 Modifier 链中后加入的节点能先被遍历到,SearchState 中的 nestedScrollConnection 更靠后被调用,因此能更先拦截到子布局的触摸事件;outerNestedScrollConnection 在 scrollable 修饰符前被调用,因此能拦截 scrollable 处理父布局的触摸事件。


Layout(
...
modifier = modifier
// 获取父布局的触摸事件,在父布局消费前、后进行处理
.nestedScroll(outerNestedScrollConnection)
.scrollable(
state = state.scrollableState,
orientation = Orientation.Vertical,
reverseDirection = true,
flingBehavior = remember {
object : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
val remain = with(this) {
with(flingBehavior) {
performFling(initialVelocity)
}
}
// 父布局未消费完的速度,传递给子布局继续消费
if (remain > 0) {
listState.scroll {
performFling(remain)
}
return 0f
}
return remain
}
}
},
)
// 获取子布局的触摸事件,在子布局消费前、后进行处理
.nestedScroll(state.nestedScrollConnection)
)

实现 MeasurePolicy,根据 SearchState 中的 value 计算各个组件的放置位置,以实现组件被滑动的视觉效果。


Layout(...) { measurables, constraints ->
check(constraints.hasBoundedHeight)
val height = constraints.maxHeight
val firstPlaceable = measurables[0].measure(
constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity)
)
val secondPlaceable = measurables[1].measure(
constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity)
)
val thirdPlaceable = measurables[2].measure(
constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity)
)
// LazyColumn 限制高度为父布局最大高度
val bottomPlaceable = measurables[3].measure(
constraints.copy(minHeight = height, maxHeight = height)
)
// 更新 maxValue 和 cardHeight
state.maxValue = secondPlaceable.height + firstPlaceable.height + thirdPlaceable.height
state.cardHeight = secondPlaceable.height
layout(constraints.maxWidth, constraints.maxHeight) {
secondPlaceable.placeRelative(0, firstPlaceable.height - state.value)
// TopBar 覆盖在 ShopCard 上面,所以后放置
firstPlaceable.placeRelative(
0,
// 搜索栏在 value 超过 cardHeight 后才会开始移动
secondPlaceable.height - state.value.coerceAtLeast(secondPlaceable.height)
)
thirdPlaceable.placeRelative(
0,
firstPlaceable.height + secondPlaceable.height - state.value
)
bottomPlaceable.placeRelative(
0,
firstPlaceable.height + secondPlaceable.height + thirdPlaceable.height - state.value
)
}
}

效果


动图展示了 scroll 和 fling 两种情况下的效果。淘宝还实现了搜索栏、Tab栏、店铺卡片的透明度变化,营造了更自然的视觉效果,这里不再展开实现,聚焦使用 Jetpack Compose 实现嵌套滚动的效果。






示例源码


Search.kt


作者:Ovaltinez
来源:juejin.cn/post/7287773353309749303
收起阅读 »

鸿蒙开发之页面路由(router)

今天继续来学点鸿蒙相关的知识,本次内容讲的是页面路由,也就是我们熟悉的页面之间跳转传参等一系列操作,鸿蒙里面主要使用Router模块来完成这些页面路由相关操作,下面来详细介绍下 页面跳转 router.pushUrl()和router.replaceUrl()...
继续阅读 »

今天继续来学点鸿蒙相关的知识,本次内容讲的是页面路由,也就是我们熟悉的页面之间跳转传参等一系列操作,鸿蒙里面主要使用Router模块来完成这些页面路由相关操作,下面来详细介绍下


页面跳转


router.pushUrl()和router.replaceUrl()


这两个函数都可以用来页面跳转,区别在于



  • router.pushUrl():就像字面意思那样,会将一个新的页面推到页面栈的顶部,而旧页面依然存在,如果按下返回键或者调用router.back(),旧页面会回到栈顶.

  • router.replaceUrl():也像字面意思那样,会把当前旧页面用新页面来代替,旧页面会被销毁,如果按下返回键或者调用router.back(),不会回到旧页面.


知道了概念后来写俩例子实践下,首先是第一个页面文件,命名它为FirstPage.ets,所对应的路径是pages/FirstPage,里面的代码是这样的


image.png

页面结构很简单,就是一个文案加一个按钮,按钮点击事件就是跳转至SecondPage页面,我们看到这里的跳转方式是使用的pushUrl方式,也就是把SecondPage页面覆盖在FirstPage上,SecondPage里面的代码与FirstPage基本相似,我们看下


image.png

也是一个按钮加一个文案,按钮的事件是调用router.back()执行返回操作,这样一个完整的页面跳转加返回的操作就写完了,实际效果如下


1018aa1.gif

实际效果也证实了,使用pushUrl方式跳转,新页面会被加在页面栈顶端,而旧页面不会销毁,那么replaceUrl又是怎么样的呢?我们将代码更改下


image.png

第一个页面里面,将跳转的方式改了一下,改成replaceUrl,现在再看看效果


1018aa2.gif

可以发现跳转到第二个页面之后,再点击返回已经回不到第一个页面了,那是因为第一个页面已经从栈里面销毁了


RouterMode


页面跳转分为两种模式,分别是RouterMode.StandardRouterMode.Single,前者为跳转的默认模式,可不写,表示每次都新建一个页面实例,后者则表示单例模式,如果栈里面已经存在该页面实例,在启动它的时候会直接从栈里面回到栈顶,同样下面用代码来解释下这两种模式的区别,这里再新增一个页面ThirdPage


image.png

这个页面里面也有一个文案,另外还有两个按钮,返回按钮执行回退操作,跳转按钮则是跳转至SecondPage,这里跳转的方式是用的pushUrl,模式是Standard,另外我们在SecondPage里面也加一个跳转按钮,点击跳转至ThirdPage,方式也是pushUrlStandard


image.png

代码都写完了,目前这样的跳转逻辑等于是如果我不停的在新页面里面点击跳转按钮,那就会一直新建页面,如果在某一个页面点击返回并一直点下去,会将之前创建好的页面一个不差的都经过一遍,最终才能回到第一个页面,我们看下实际效果


1018aa3.gif

可以看到事实跟刚才讲的一样,但是很明显,将已经存在的实例重复创建是一件很消耗内存的事情,所以在这种需要再一次打开栈里面已经存在的实例的场景中,我们还是比较推荐使用Single模式,我们将上述代码中跳转SecondPageThirdPage的模式改成Single再试一次


1018aa4.gif

我们看见仍旧是无限跳转下去,最终停在了SecondPage上,但是如果从SecondPage里面开始点击返回,还会不会原路返回呢,我们看下


1018aa5.gif

我们看到,先返回到了ThirdPage,然后TirdPage点击返回直接回到了第一个页面,那是因为Single模式下,SecondPageThirdPage是在不停的做着从栈内回到栈顶的操作,所以当点击返回时,第一个页面上面始终只覆盖了两个页面


页面传参


有些场景下除了页面需要跳转,另外还需要将当前页面的数据传递到新页面里面去,如何传递呢?可以先看下pushUrl里面第一个参数RouterOption里面都有哪些属性


image.png

第一个参数url已经不用说了,都用过了,第二个参数params就是页面跳转中携带的参数,可以看到是一个Object,所以如果我们想传一个字符串到下一个页面,就不能直接将一个string给到params,得这样做


image.png

params里面以一个key-value形式传递参数,而在新页面里面,通过传递过来的key把对应值取出来,我们在下一个页面获取参数的代码是这样写的


image.png

首先通过router.getParams()将参数对象取出来,然后访问对应key值就能将传递过来的数据取出来了,在SecondPage里面还多增加了一个Text组件用来显示传递过来的数据,最终运行下代码后看看数据有没有传过去


1018bb1.gif

可以看到数据已经传过去了,但这里的场景比较简单,有的复杂的场景需要传递的数据不仅仅只有一个,会以一个model的形式作为参数传递,那么遇到这样的场景该怎么做呢?


image.png

我们看到直接传递了一个UserModel对象,而UserModel就是我们封装的一个数据类,基本在实际开发中类似于UserModel这样的数据就是一个接口的Response,我们传递参数时候,只需将Response传递过去就好了,而接收参数的地方的代码如下


image.png

可以发现,从页面跳转以及传参的这部分代码上,基本就与TypeScript的方式很相似了,看下实际效果


1018bb2.gif

页面返回


说过了页面的跳转,那么跳完之后的返回操作也要说下,其实在上面的例子中,我们已经使用到了页面返回的函数,也就是router.back(),这是其中一种返回方式,它总共有三种返回方式,分别如下


返回到上一个页面


使用router.back()方式,如果当前页面是栈中唯一页面,返回将无效


返回到指定页面


可以通过传递一个url返回到指定页面,如果该页面不在页面栈中,返回将无效,如果返回页与指定页面之间存在若干页面,那么指定页面被推到栈顶,返回页与中间的若干页面会被销毁,我们现在在之前的ThirdPage中的返回按钮中加入如下代码
image.png


前面所有跳转方式都改为Standard模式,在第三个页面中点击返回的时候,原来是直接退到第二个页面,现在指定了路径以后,我们看下调到哪里去了


1018bb3.gif

直接回到第一个页面了,其他两个页面已经被销毁


返回并传递参数


有些场景需要在指定页面点击返回后,将一些数据从指定页面传递到返回后的页面,这种数据传递方式与跳转时候传递方式基本一致,因为back函数中接收的参数也是RouterOptions,比如现在从第一个页面跳到第二个页面再跳到第三个页面后,第三个页面点击返回跳到第一个页面,并且传递一些参数在第一个页面展示,代码如下


image.png

第一个页面中接收参数我们也在onPageShow()里面进行


image.png

运行效果如下


1018bb4.gif

返回时添加询问弹窗


这个操作主要是在一些重要页面里面,比如支付页面,或者一些信息填写页面里面,用户在未保存或者提交当前页面的信息时就点击了返回按钮,页面中会弹出个询问框来让用户二次确认是否要进行返回操作,这个询问框可以是系统弹框,也可以是自定义弹框


系统弹框


系统弹框可以使用router.showAlertBeforeBackPage去实现,这个函数里面接收的参数为EnableAlertOptions,这个类里面只有一个message属性,用来在弹框上显示文案


image.png

使用方式如下,在router.back()操作之前,调用一下router.showAlertBeforeBackPage,弹框上会有确定和取消两个按钮,点击取消关闭弹窗停留在当前页面,点击确定就执行router.back()操作


image.png

我们在ThirdPage里面的返回操作中加入了系统询问框,可以看到我们要做的只是需要确定下弹框的文案就好,看下效果


1018bb5.gif

但是如果我们想要更改下按钮文案,或者顺序,或者自定义按钮的点击事件,就不能用系统弹框了,得使用自定义询问框


自定义询问框


自定义询问框使用promptAction.showDialog,在showDialog里面接收的参数为showDialogOptions,可以看下这个类里面有哪些属性


image.png

可以看到比系统弹框那边多了两个属性,能够设置弹框标题的title以及按钮buttons,可以看到buttons是一个Button的数组,最多可以设置三个按钮,注意这个Button并不是我们熟悉的Button组件,它内部只支持自定义文案以及颜色


image.png

知道了这些属性之后,我们可以把上面额系统询问框替换成这个自定义的询问框了,代码如下


image.png
image.png

可以看到弹框上面就多了一个标题,以及按钮的文案与颜色也变了,那么如何设置点击事件呢,现在两个按钮点了除了关闭按钮之外是没有别的操作的,如果想要添加其他操作,就需要通过then操作符进行,在then里面会拿到一个ShowDialogSuccessResponse,这个类里面只有一个index属性,这个index就表示按钮的下标,可以通过判断下标来给指定按钮添加事件,代码如下


image.png
1018bb6.gif

现在按钮点击后已经可以响应我们添加进去的事件了


总结


鸿蒙页面路由的所有内容都已经讲完了,总体感受比Android原生跳转要方便很多,完全就是按照TS的跳转方式写的,再一次证明了如果有声明式语言开发经验的,去学鸿蒙会相对轻松很多


作者:Coffeeee
来源:juejin.cn/post/7291479799519526967
收起阅读 »

懒汉式逆向APK

通过各方神仙文档,以及多天调试,整理了这篇极简反编译apk的文档(没几个字,吧).轻轻松松对一个apk(没壳的)进行逆向分析以及调试.其实主要就是4个命令. 准备 下载apktool 下载Android SDK Build-Tools,其中对齐和签名所需的命...
继续阅读 »

通过各方神仙文档,以及多天调试,整理了这篇极简反编译apk的文档(没几个字,吧).轻轻松松对一个apk(没壳的)进行逆向分析以及调试.其实主要就是4个命令.


准备



  1. 下载apktool

  2. 下载Android SDK Build-Tools,其中对齐和签名所需的命令都在此目录下对应的版本的目录中,比如我的在D:\sdk\build-tools\30.0.3目录下,可以将此目录加入环境变量中,后续就可以直接使用签名和对齐所需的命令了

  3. 可选,下载jadx-gui,可查看apk文件,并可导出为gralde项目供AS打开


流程




  1. 解压apk: apktool d C:\Users\CSP\Desktop\TEMP\decompile\test.apk -o C:\Users\CSP\Desktop\TEMP\decompile\test,第一个参数是要解压的apk,第二个参数(-o后面)是解压后的目录




  2. 修改: 注意寄存器的使用别错乱,特别注意,如果需要使用更多的寄存器,要在方法开头的.locals x或.registers x中对x+1



    • 插入代码:在idea上使用java2smali插件先生成smali代码,可复制整个.smali文件到包内,或者直接复制smali代码,注意插入后修改包名;

    • 修改代码:需要熟悉smali语法,可自行百度;

    • 修改so代码,需要IDA,修改完重新保存so文件,并替换掉原so文件,注意如有多个架构的so,需要都进行修改并替换;

    • 删除代码:不建议,最好逻辑理清了再删,但千万别删一半;

    • 资源:修改AndroidManifest.xml,可在application标签下加入android:debuggable="true",重新打包后方可对代码进行调试;




  3. 重打包: apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk,第一个参数是要进行打包的目录文件,第二个参数(-o后面)是重新打包后的apk路径.重新打包成功,会出现Press any key to continue ...




  4. 对齐: zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk,第一个参数是需要进行对齐的apk路径,第二个参数是对齐后的apk路径.对齐成功,会出现Verification succesful




  5. 签名: apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk,第一个参数(--ks后面)是密钥路径,后面跟着是否开启V1、V2签名,在后面跟着签名密码,最后两个参数(--out后面)是签名后的apk路径以及需要签名的apk(注意需对齐)路径.签名成功,会出现Signed




  6. 安装: adb install C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk




  7. 调试: 用jdax将apk导出为gradle项目,在AS中打开,即可通过attach debugger的方式对刚重新打包的项目进行调试.注意,调试时因为行号对不上,所以只能在方法上打上断点(菱形图标,缺点,运行速度极慢)




  8. 注意事项:



    • 上述命令中,将目录和项目'test'改成自己的目录和项目名即可;

    • apktool,zipalign,apksigner,adb命令需加入环境变量,否则在各自的目录下./xxx 去执行命令;

    • zipalign,apksigner所需的运行文件在X:XX\sdk\build-tools\30.0.3目录下;

    • 使用apksigner签名,对齐操作必须在签名之前(推荐使用此签名方式);

    • 新版本Android Studio生成的签名密钥,1.8版本JDK无法使用,我是安装了20版本的JDK(AS自带的17也行)




假懒


为了将懒进行到底,写了个bat脚本(需要在test文件目录下):


::关闭回显
@echo off
::防止中文乱码
chcp 65001
title 一键打包

start /wait apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk
start /b /wait zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
start /b /wait apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk

大家将此脚本复制进bat文件,即可一键输出.


不过目前略有瑕疵:1.重新打包需要新开窗口,并且完成后还需手动关闭;2.关闭后还要输入'N'才能进行后续的对齐和签名操作有无bat大神帮忙优化下/(ㄒoㄒ)/~~!


-------更新


真懒


对于'假懒'中的打包脚本,会有2个瑕疵,使得不能将懒进行到底.经过查找方案,便有了以下'真懒'的方案,使得整个打包可以真正一键执行:


::关闭回显
@echo off
::防止中文乱码
chcp 65001
title 一键打包

call apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk
call zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
del test_b.apk
call apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
del test_b_zipalign.apk

echo 打包结束
echo 输出文件是-----test_b_sign.apk

pause

可以看到,把start换成了call,并且删除了重新打包和对齐后的文件,只留下最后签完名的文件


image.png


到此够了吗?不够,因为运行第一个apktool b命令时,重新打包完,会被pasue,让你按一个按键再继续.


image.png


这当然不行,这可不算一键,那么我们找到apktool的存放路径,打开apktool.bat,找到最后一行


image.png


就是这里对程序暂停了,那么就把这一行删了,当然最好是注释了就行,在最前面rem即可对命令进行注释,处理完之后,再重新运行我们的'一键打包.bat'脚本,这时候在中途重新打包后就不会出现'Press any key to continue...'了,即可一键实现打包-对齐-签名的流程了( •̀ ω •́ )y.


当然,如果想使脚本到处运行,可以给脚本增加一个变量,在运行前通过环境变量的形式把要打包的目录路径加入进来,这个大家可以自行尝试.


最后,感谢大家的阅读.这里面有对smali和so的修改,有机会和时间,我也会继续分享!!!


作者:果然翁
来源:juejin.cn/post/7253291597042319418
收起阅读 »

android 13 解决无法推送问题(notifications 相关)

最近,接手的 app (react native 技术栈) 需要添加一些关于推送的流程,根据后端返回的 json 到达对应的页面,这个也不难,根据旧代码添加相应的流程就行了。加上,让 qa 人员测试,发现 android 13 无法推送。以下是总结的解决思路 ...
继续阅读 »

最近,接手的 app (react native 技术栈) 需要添加一些关于推送的流程,根据后端返回的 json 到达对应的页面,这个也不难,根据旧代码添加相应的流程就行了。加上,让 qa 人员测试,发现 android 13 无法推送。以下是总结的解决思路


添加权限


在 AndroidManifest.xml 加上 POST_NOTIFICATIONS 权限


<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

请求权限


在上面我们添加了通知权限,但默认是关闭的,需要用户长按 app 的图标到应用程序信息那手动把通知权限打开,这肯定是不现实的,因此得主动请求,并让用户选择是否给予通知权限


既然是用 react naitve,那就用 js 代码请求好了,由于只有在安卓13需要用到,因此需要判断系统和版本


import React, {Component} from 'react';
import {
Platform,
PermissionsAndroid,
} from 'react-native';

export default class App extends Component {

componentDidMount() {
if (Platform.OS === 'android' && Platform.Version === 13) {
PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
}
}

}

但实际上出了问题,PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONSundefined,github 也有相关的 issue,是跟 RN 的版本有关,v0.70.7 版本才解决了这个问题,很显然升级 rn 的代价太大(接手的项目还不支持 function component 和 hook 呢),因此采用原生方法请求


在 MainActivity.java 添加下列代码,其中 requestPermissions 的第二个参数 requestCode 是自定义的,不重复即可,下面我就定义为了 101


import android.Manifest;
import android.os.Build;

public class MainActivity extends ReactActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
// 同样判断 android 版本为 13
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 101);
}
}
}


好了,重新编译安装后,打开 app 会出现 “运行 app 向你发送通知吗” 的类似弹窗,如果用户拒绝的话,还是得手动去应用程序信息那里设置,当然,如果用户选择允许的话,我们的问题就解决了。


引导用户打开权限


如果用户选择不允许的话,又有重要的需要推送,就可能需要引导用户去打开权限了,因此我们写个桥接文件,提供两个方法,checkEnablejumpToNotificationsSettingPage,第一个判断权限有没有打开,第二个跳转到设置页面


NotificationsModule.java


package com.xxxapp;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationManagerCompat;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

public class NotificationsModule extends ReactContextBaseJavaModule {

private final Context context;

public NotificationsModule(ReactApplicationContext reactApplicationContext) {
super(reactApplicationContext);
context = reactApplicationContext;
}

@NonNull
@Override
public String getName() {
return "Notifications";
}

@ReactMethod
public void checkEnable(final Promise promise) {
promise.resolve(NotificationManagerCompat.from(context).areNotificationsEnabled());
}

@ReactMethod
public void jumpToNotificationsSettingPage() {
final ApplicationInfo applicationInfo = context.getApplicationInfo();
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
intent.putExtra("android.provider.extra.APP_PACKAGE", applicationInfo.packageName);
context.startActivity(intent);
}

}

NotificationsPackage.java


package com.xxxapp;

import androidx.annotation.NonNull;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class NotificationsPackage implements ReactPackage {

@NonNull
@Override
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
return new ArrayList<>(Collections.singletonList(new NotificationsModule(reactContext)));
}

@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

作者:张二三
来源:juejin.cn/post/7289952867052994619
收起阅读 »

Android进阶之路 - 字体自适应

开发中有很多场景需要进行自适应适配,但是关于这种字体自适应,我也是为数不多的几次使用,同时也简单分析了下源码,希望我们都有收获 很多时候控件的宽度是有限的,而要实现比较好看的UI效果,常见的处理方式应该有以下几种 默认执行多行显示 单行显示,不足部分显示....
继续阅读 »

开发中有很多场景需要进行自适应适配,但是关于这种字体自适应,我也是为数不多的几次使用,同时也简单分析了下源码,希望我们都有收获



很多时候控件的宽度是有限的,而要实现比较好看的UI效果,常见的处理方式应该有以下几种



  • 默认执行多行显示

  • 单行显示,不足部分显示...

  • 自适应字体


静态设置


宽度是有限的,内部文字会根据配置进行自适应


在这里插入图片描述


TextView 自身提供了自适应的相关配置,可直接在layout中进行设置


主要属性



  • maxLines="1"

  • autoSizeMaxTextSize

  • autoSizeMinTextSize

  • autoSizeTextType

  • autoSizeStepGranularity


    <TextView
android:id="@+id/tv_text3"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="10dp"
android:autoSizeMaxTextSize="18sp"
android:autoSizeMinTextSize="10sp"
android:autoSizeStepGranularity="1sp"
android:autoSizeTextType="uniform"
android:gravity="center"
android:maxLines="1"
android:text="自适应字体" />


源码:自定义属性


在这里插入图片描述




动态设置


 // 设置自适应文本默认配置(基础配置)
TextViewCompat.setAutoSizeTextTypeWithDefaults(textView, TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM)
// 主动设置自适应字体相关配置
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(textView, 20, 48, 2, TypedValue.COMPLEX_UNIT_SP)



源码分析


如果你有时间,也有这方面的个人兴趣,可以一起分享学习一下


setAutoSizeTextTypeWithDefaults


根据源码来看的话,内部做了兼容处理,主要是设置自适应文本的默认配置


在这里插入图片描述


默认配置方法主要根据不同类型设置自适应相关配置,默认有AUTO_SIZE_TEXT_TYPE_NONE or AUTO_SIZE_TEXT_TYPE_UNIFORM ,如果没有设置的话就会报 IllegalArgumentException 异常



  • AUTO_SIZE_TEXT_TYPE_NONE 清除自适应配置

  • AUTO_SIZE_TEXT_TYPE_UNIFORM 添加一些默认的配置信息


在这里插入图片描述




setAutoSizeTextTypeUniformWithConfiguration


根据源码来看主传4个参数,内部也做了兼容处理,注明 Build.VERSION.SDK_INT>= 27 or 属于 AutoSizeableTextView 才能使用文字自定义适配



  • textView 需进行自适应的控件

  • autoSizeMinTextSize 自适应自小尺寸

  • autoSizeMaxTextSize 自适应自大尺寸

  • autoSizeStepGranularity 自适应配置

  • unit 单位,如 sp(字体常用)、px、dp


在这里插入图片描述


unit 有一些常见的到单位,例如 dp、px、sp等


在这里插入图片描述


作者:Shanghai_MrLiu
来源:juejin.cn/post/7247027677223485498
收起阅读 »

工作六年,我开始在意文档的外在了

💻工作六年,我开始在意文档的外在了 那些能提升美感的点,似乎就藏在一些细枝末节的地方;提升就是一瞬间的事,用心就会发现...... 🔊 个人文档排版总结。 主要观点: 秩序统一 图文并茂 人的大脑总是喜欢整洁的东西。 排版统一就会显得简洁、专业。 📕秩...
继续阅读 »

💻工作六年,我开始在意文档的外在了


那些能提升美感的点,似乎就藏在一些细枝末节的地方;提升就是一瞬间的事,用心就会发现......



🔊 个人文档排版总结。



主要观点:



  1. 秩序统一

  2. 图文并茂


人的大脑总是喜欢整洁的东西。 排版统一就会显得简洁、专业。


📕秩序统一


秩序的灵感,来源于一个博主,被他的排版所吸引,如下展示:


标题有序,干净清爽
image.png

从此命运的齿轮开始转动......


语言秩序



  1. 统一前缀

  2. 统一字数

  3. 统一主谓

  4. 统一风格

  5. ......


有标题,分类等场景,尽情地保持统一。下面是我文档示例:


标题命名一致知识库命名一致
image.png

比如主谓、动宾等


如动词+名词。 (补充一点,少用被动句描述问题)
image.png

好的文档排版,会让自己舒心。


结构秩序



  1. 左右对齐

  2. 居中对齐

  3. 上下对齐

  4. ......


除语言秩序,结构上的秩序也同等重要。具体可以是间隔距离,粗细比例等。


下图是用 PPT 绘制的某交付流程;边框结构左右、上下保持间距一致
image.png

mybatis 统一拦截环境字段原理图;图框、线条等对齐
image.png

复杂的东西,整洁以后也会变得简单


更多的统一:



  1. 统一的语调

  2. 统一的色调

  3. 统一的语气

  4. .......


整齐划一,是文档外在美的最基本元素。


📖大道至简


简单是另一种美,把复杂变简单。


语言简单


用最简单的话把事情说清楚
复:昨天从早到晚,我整整忙了一天,都没有休息一下,吃饭都没有好好吃,今天不管怎么样,也要休息休息。
简:昨天忙一天,今天休息一下。

廖雪峰老师的文章就是用朴素的话讲着最高端的技术,深得广大网友的喜爱。


如拍照中的留白,也一样高级。


📊图文并茂


文字和图片结合才能显得不单调。


icon推荐


锦上添花的 icon,像女人的首饰,小巧美丽


下面是我文章中采用 icon 图标应用示例
image.png

更多 icon 推荐:



图片推荐


一图胜千言。图片更有张力,让枯燥的文字更有活力。


个人兴趣爱好的介绍,三张图是户外爬山的轨迹记录。上图的一瞬间让生活丰富了起来
image.png

绘图推荐


工欲善其事必先利其器,选一款喜欢的绘图软件。


软件图示理由
excalidraw.comimage.png像笔一样的绘画图表,是我喜欢的风格
语雀文档里的绘图image.png可以更换绘图的风格基调

我选择绘图软件的原则,画线足够顺畅即可。我见过用 PPT 也能把图绘制得很好的人,所以工具只找适合自己的。


个人绘图原则:对齐、对齐、还是对齐!


📑 其他技巧


会用模板



  1. 提炼总结模板

  2. 参考别人模板

  3. 学习使用模板


他山之石可以攻玉。 学习别人,打开眼界,下面是某文档的【阅读书单】模板,挺喜欢。
image.png

撰写规范


没有规矩不成方圆,我的条约规范。


适合自己的规范
image.png

🖊️最后总结


关键要素



  1. 统一

  2. 对齐

  3. 图文

  4. 规范


最后的最后



  1. 总结一套自己喜欢的文档规范,适合自己的才是最好的;

  2. 排版好一篇文章,需要花费精力,但文章的内容才是最重要的,不要华而不实,徒有其表。


排版阅读


中文文案排版细则


中文技术文档写作风格指南 — 中文技术文档写作风格指南


作者:uzong
来源:juejin.cn/post/7292347319432970255
收起阅读 »

一个功能强大的Flutter开源聊天列表插件

flutter_im_list是一款高性能、轻量级的Flutter聊天列表插件。可以帮助你快速创建出类微信的聊天列表的效果。 目录 预览图 示例 视频教程 如何使用 API 预览图 整体长按输入中 示例 Examples 视频教程 欢迎通过视频教程学习...
继续阅读 »

flutter_im_list是一款高性能、轻量级的Flutter聊天列表插件。可以帮助你快速创建出类微信的聊天列表的效果。


目录



预览图


整体长按输入中
flutter_im_listflutter_im_listflutter_im_list

示例



视频教程


欢迎通过视频教程学习交流。


如何使用


第一步添加依赖


在项目根目录下运行:


flutter pub add flutter_im_list

第二步:初始化ChatController


@override
void initState() {
super.initState();
chatController = ChatController(
initialMessageList: _messageList,
timePellet: 60,
scrollController: ScrollController());
}

第三步:在布局中添加ChatList


  @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ChatList(
chatController: chatController,
));
}

第四步:设置初始化数据


final List<MessageModel> _messageList = [
MessageModel(
id: 1,
content: "介绍下《ChatGPT + Flutter快速开发多端聊天机器人App》",
ownerType: OwnerType.sender,
createdAt: 1696142392000,
avatar: 'https://o.devio.org/images/o_as/avatar/tx18.jpeg',
ownerName: "Jack"),
MessageModel(
id: 2,
content:
"当前ChatGPT应用如雨后春笋般应运而生,给移动端开发者也带来了极大的机会。本课程将整合ChatGPT与Flutter高级技术,手把手带你从0到1开发一款可运行在多端的聊天机器人App,帮助你抓住机遇,快速具备AI运用能力,成为移动端领域的AI高手。@https://coding.imooc.com/class/672.html",
ownerType: OwnerType.receiver,
createdAt: 1696142393000,
avatar: 'https://o.devio.org/images/o_as/avatar/tx2.jpeg',
ownerName: "ChatGPT"),
];

如果没有,可以将_messageList赋值为[]。



了解更多请查看视频教程



API


IChatController


abstract class IChatController {
/// 在列表中添加消息
void addMessage(MessageModel message);
/// 在列表中删除消息
void deleteMessage(MessageModel message);
/// 批量添加消息(适用于下来加载更多的场景)
void loadMoreData(List<MessageModel> messageList);
}

ChatController


class ChatController implements IChatController {
/// 列表的初始化数据可以为[]
final List<MessageModel> initialMessageList;
final ScrollController scrollController;

///支持提供一个MessageWidgetBuilder来自定义气泡样式
final MessageWidgetBuilder? messageWidgetBuilder;

///设置显示的时间分组的间隔,单位秒
final int timePellet;
List<int> pelletShow = [];

ChatController({required this.initialMessageList,
required this.scrollController,
required this.timePellet,
this.messageWidgetBuilder}) {
for (var message in initialMessageList.reversed) {
inflateMessage(message);
}
}
...

ChatList


class ChatList extends StatefulWidget {
/// ChatList的控制器
final ChatController chatController;

/// 插入子项的空间大小
final EdgeInsetsGeometry? padding;

/// 气泡点击事件
final OnBubbleClick? onBubbleTap;

/// 奇葩长按事件
final OnBubbleClick? onBubbleLongPress;
/// 文本选择回调
final HiSelectionArea? hiSelectionArea;

const ChatList(
{super.key,
required this.chatController,
this.padding,
this.onBubbleTap,
this.onBubbleLongPress,
this.hiSelectionArea});

@override
State<ChatList> createState() => _ChatListState();
}


了解更多请查看视频教程



Contribution


欢迎在issues上报告问题。请附上bug截图和代码片段。解决问题的最快方法是在示例中重现它。


欢迎提交拉取请求。如果您想更改API或执行重大操作,最好先创建一个问题并进行讨论。




MIT Licensed


作者:CrazyCodeBoy
来源:juejin.cn/post/7292427026874368040
收起阅读 »

30岁以后如何提升自己的收入?

最近,和我打球几个美团的小伙伴,基本都是30岁出头,多数都刚结婚生娃,开始步入中年,面对互联网正在进行的重大变革,有些焦虑,更有些迷茫。 我永远坚信:选择比奋斗更重要,因为奋斗只体现你的勤奋与毅力,而选择则彰显你的智慧与信念。我是18年步入30岁,转眼已经五年...
继续阅读 »

最近,和我打球几个美团的小伙伴,基本都是30岁出头,多数都刚结婚生娃,开始步入中年,面对互联网正在进行的重大变革,有些焦虑,更有些迷茫。


我永远坚信:选择比奋斗更重要,因为奋斗只体现你的勤奋与毅力,而选择则彰显你的智慧与信念。我是18年步入30岁,转眼已经五年,借此回顾一下自己所做的思考和选择,希望能帮助到他人,也能为自己接下来的选择明确方向。


30多岁的人,多数人上有老下有小,既是国家的中坚力量,更是家庭的支柱。我们既要为国家创造财富,更要增加自己小家的收入。


受面向对象编程的启发,我觉得面向提升收入来展开本文,文章的结构和层级会更好,也会更吸引人,而且能够切实帮助到读者。


确保身心健康


身体是革命的本钱,没有良好的身心状态,即使我们赚了很多钱,也得花钱治病。这些年,我头顶的头发稀疏了很多,特别是最近这一两年,因为两个娃比较小,经常需要半夜起床哄娃,经常得不到充分的休息,肠胃也出现了一些问题。


为了解决身体上出现的问题,我主要通过中医、跑长跑和打篮球来调理和强身健体。在中医方面,自学了一些基础理论,比如背下了黄帝内经的上古天真论,避免选择医院或按摩店时,被人忽悠。


这两年,去三甲医院找中医开了三次中药,喝完一个疗程后,感觉帮助不大,倒是在小区的按摩店,通过揉肚子,配合艾灸,能有一定效果。总的来说,我肠胃的问题,主要是吃水果没有节制和吃饭太快引起的,要想根治,得从习惯入手。


除了饮食习惯,还得保持锻炼的习惯,所以22年初,我给自己定了三个习惯:每周至少跑10公里强体魄,每周至少读一本好书启智慧,每周至少做一次公益得快乐。


对了跑步,挺难坚持的,所以我会发动我老婆监督,一次做不到就罚款2万元给她。对于读书,结合育儿的需要,我买了一百本育儿书,正好自己有两个娃,学完就能用得上。而对于公益,我有时会带着大儿子去公园拣烟头,有时会自己到寺庙做义工。


成为技术专家


如果是本科毕业,到了30岁的时候,应该有七八年的工作经验了,硕士的话,也有五六年了,此时不管你愿不愿意,都会成为团队的技术骨干,甚至是架构师。


我是从28岁就开始带团队,在面试30岁左右的候选人时,如果他们还不能成为某个领域的技术专家,我基本上只会意思聊十多分钟,当然如果候选人,应聘的是外包岗位,可能会酌情考虑。


那么怎么样,可以称为技术专家呢,用美团最新职级标准来看的话,就是L7+的同学,差不多对应阿里P7的同学。


我参与过多个公司职级标准的制定,记得曾经和某个HR讨论时,她提出要我用一句话来总结,那就是:不仅能独立完成架构设计、技术难题公关和带领其他研发完成技术实现,而且能从整个研发流程出发提升整体的研发质量、效率和用户体验。


在此基础上,对于前端同学,如果想要获得高薪,就得需要花时间专研一些特定方向的技术,比如图形学(具备自研3D渲染引擎的能力)、音视频处理(掌握WebAssembly技术,深入研究opencv、ffmpeg等)、端智能(需要掌握深度学习及其模型的相关知识)。


从我认识的朋友来看,资深的web图形学技术专家和音视频处理专家,年薪能有150万左右,而端智能前端同学去做的比较少,多数是有算法背景的同学,年薪也有120万起。


从美团合并通道的导向来看,对于技术专家,既有广度的要求,更有特定领域技术深度的要求,你掌握的特定技术越不可替代,越容易拿到高薪。


转型做管理


不管处于什么阶段,管理都是我们需要面对的。从踏入职场开始,我们首先要做好自我管理,高效人士的七个习惯,前三个都是和自我管理有关的。


其次,我们需要做好向上管理,不管是我遇到的几个领导,还是我自己,都是比较喜欢和欢迎,下属做好充分准备好,能够积极主动地约我们,聊聊自己的困惑、工作上的思考和改进建议等。


再次,我们需要做好同事管理,如果不能很好地融入团队及企业文化,不仅自己开展工作比较困难,而且在需要裁员时,这样的同学都是会优先考虑要裁掉的。


最后,对于30岁左右的同学,即使不是实线管理者,通常也需要带着多个同学一起完成工作,就不得不强化自己向下管理的能力。像在美团,我们提拔一个同学做leader的时候,往往会先给几个同学或项目让该同学负责,看看其是否合适,合适的同学,有机会时就会优先考虑他,否则就重新招聘。


互联网研发的流动性很大,对于30岁左右的同学,不管当前有没有向下管理的机会,我都建议大家平时多去观察和思考,从职业发展的角度看,即使得跳槽,也要争取有一段向下管理的实践,否则35岁之后,好的就业机会就非常少了。


寻找副业


程序员可以选择的副业有很多种,以下是一些建议:



  • 写作:撰写技术文章或教程,发布在博客、公众号、知乎等平台上。通过广告和付费阅读等方式,可以获得一定的收入。

  • 创立个人品牌:通过积累经验和作品,创立自己的个人品牌,提高个人影响力。这可以为你带来更多商业机会,如演讲、顾问、培训等。

  • 接私活:在业余时间为其他公司或个人完成项目,如网站开发、小程序、APP 等。你可以在一些平台(如猪八戒、实现网、码市等)上找到相关项目,或者通过朋友和同事介绍获得更多机会。

  • 做个人博客或开源项目:通过分享自己的技术经验和心得,吸引更多人关注并建立个人品牌。这可以为你带来一些广告收入和合作机会。同时,参与开源项目可以提高你的技术水平,也有助于拓展人际关系。

  • 网络兼职:利用自己的技能在一些在线教育平台上教授前端课程,如慕课网、极客时间等。你还可以在一些问答平台(如知乎、Stack Overflow)上回答问题,帮助他人解决问题,提高自己的知名度。

  • 开发移动应用:如果你对移动开发有兴趣,可以尝试开发自己的应用,上架到应用商店。通过广告和内购等方式,你可以获得持续的收入。

  • 开发小游戏:如果你对游戏开发有兴趣,可以尝试开发自己的小游戏,发布在微信小程序、抖音等平台上。通过广告和内购等方式,你可以获得持续的收入。


总之,程序员可以选择的副业方向很多,关键在于发掘自己的兴趣和优势,并付诸实践。同时,副业也需要长期坚持和投入,才能获得稳定的收益。


敢于创业


程序员的尽头是什么?有人说,程序员尽头就是不做程序员。那么,不做程序员又能做什么?


信息时代,为90后提供了更多的机会和资源,让他们拥有良好的教育背景和丰富的知识储备,更好地掌握专业知识和技能,为创业打下坚实的基础。


众多创业者在创业前期,或因受到“偶像”或“故事”激励,从而走上创业之路,比如点燃雷军内心的那本《硅谷之火》,他为此认定创业是自己要走的路。


其实,人生的各个阶段都有不同的人激励我创业,以前卓越教育的校长给予我很多工作和人生方向上的指引。但我始终坚信“创业需要发自内心”,我不会因为看到某个人的故事就热血澎湃。


创业者在任何社会的群体中都是少数派,不到 1% 。就算中国最好的理工类大学也没有很多学生创业,反而他们会选择去留学、当科学家、成为公务员。创业要去无人之境、蛮荒之地,要去开创一个新的事业,往往是不被大众认可的。


程序员创富相对比较容易,是因为现在世界上最有价值的都是科技公司,程序员先天离这些公司的核心价值最近。比尔·盖茨、Larry Page、李彦宏、马化腾、张一鸣都是程序员,科技公司预估有超过一半的老板都有程序员背景,从概率的角度来看,程序员创富比其他职业更容易一点。


原因也比较简单,如果是销售人员担任 CEO,他们还得招几个程序员来构建自己的核心竞争力。而程序员作为公司创业的核心,可以不依赖别人就可以启动创业项目,且只要有两个人就可以启动了。


程序员创业有优势,但也并不是适合所有类型的项目。其中,科技创新的项目显然技术人员做 CEO 最为合适,比如研发搜索引擎,不管美国还是中国,CEO 基本上都是技术背景,因为搜索引擎是技术驱动的领域。


然而,我看见了 500~1000 个程序员创业,有 90% 的失败率,多数是回去上班接着打工去了,还有 9% 拥有一个小公司“不死不活”,每年有几十万到一两百万的收入,对个人来讲,算是创业成功。


但从 VC 投资或者个人事业追求的角度,年营业额上了 1,000 万以上,不管是技术驱动、产品驱动、销售驱动型的公司,都是 1% 以下的比例。


程序员创业要闯三关:



  • 首先是技术关,通常这是程序员最容易闯的一关,因为程序员创业肯定会找相对熟悉的领域去做。

  • 其次业务观,有一定挑战。因为做 2C 要能获客,做 2B 要能搞定客户。逻辑思维能力也很重要,但不是全部。2C 比较适合逻辑思维能力,程序员背景的人肯定能搞得很明白。2B 获客更多是软技能,其中包括察言观色、判断对方角色、决策链决策逻辑等,并不是所有的程序员都能做得好。

  • 最后是组织关。公司人很少的时候,20人以内每个人都认识,不太需要过多的管理机制。但如果公司到了 100 人甚至更多,组织能力没到,人越多效率越低,这是非常大的障碍。


所以程序员除了固有的理性思维能力之外,还要能培养跟人打交道的能力,培养个人魅力,同时对组织管理要有敬畏之心。


总结


转眼,我已过30岁有五年了。从本科毕业到军校培训担任班长,到下到连队当排长管理40多人,然后退出现役成为小白程序员,然后在29岁时成为高级技术经理,管理20多人团队,收入也从月薪9k涨到了50k。


30岁以后,尝试过成为图形学和端智能领域的技术专家,也短暂创过业,上班时接过私活,也投入很多精力搞副业,最后美团的合同到期后,选择了先离职休养一段时间。


目前的计划是,再休息一个月,然后决定继续上班,还是基于副业去创业。


作者:三一习惯
来源:juejin.cn/post/7291936473078775843
收起阅读 »

Android:这个需求搞懵了,产品说要实现富文本回显展示

一、前言 不就展示一个富文本吗,有啥难的,至于这么大惊小怪吗,哎,各位老铁,莫慌,先看需求,咱们再一探究竟。 1、大致需求 要求,用户内容编辑页面,实现图文混排,1、图片随意位置插入,并且可长按拖动排序;2、图片拖动完成之后,上下无内容,则需要空出输入位置,有...
继续阅读 »

一、前言


不就展示一个富文本吗,有啥难的,至于这么大惊小怪吗,哎,各位老铁,莫慌,先看需求,咱们再一探究竟。


1、大致需求


要求,用户内容编辑页面,实现图文混排,1、图片随意位置插入,并且可长按拖动排序;2、图片拖动完成之后,上下无内容,则需要空出输入位置,有内容,则无需空出;3、内容支持随意位置插入;4、以富文本的形式传入后台;5、解析富文本,回显内容。


2、大致效果图



实现这个需求倒不是很难,直接一个RecyclerView就搞定了,无非就是使用ItemTouchHelper,再和RecyclerView绑定之后,在onMove方法里实现Item的位置转换,当然需要处理一些图片和输入框之间的逻辑,这个不是本篇文章的重点,以后再说一块。


效果的话,我又单独的写了一个Demo,和项目中用到的一样,具体效果如下:



获取富文本的方式也是比较的简单,无论文本还是图片,最终都是存到集合中,我们直接遍历集合,给图片和文字设置对应的富文本标签即可,具体的属性,比如宽高,颜色大小,可以自行定义,大致如下:


/**
* AUTHOR:AbnerMing
* INTRODUCE:返回富文本数据
*/

fun getRichContent(): String {
val endContent = StringBuffer()
mRichList.forEach {
if (it.isPic == 0) {
//图片
endContent.append("<img src="" + it.image + ""/>")
} else {
//文本
endContent.append("<p>" + it.text + "</p>")
}
}
return endContent.toString()
}

以上的各个环节,不管怎么说,还是比较的顺利,接下来就到了我们今日的话题了,富文本我们是传上去了,但是如何回显呢?


二、富文本回显分析


回显有两种情况,第一种是编辑之后,可以保存至草稿,下次再编辑时,需要回显;第二种情况是,内容已经发布了,可以再次编辑内容。


具体的草稿回显有多种方式,我们不是使用RecyclerView实现的吗,直接保存列表数据就可以了,可以使用本地或者数据库形式的存储方式,不管使用哪种,实现起来绝非难事,回显的时候也是以集合的形式传入RecyclerView即可。


内容已经发布过的,这才是探究的重点,由于接口返回的是富文本信息,一开始无脑想到的是,富文本信息还得要解析里边的内容,着实麻烦,想着每次发布成功之后在本地存储一份数据,在编辑的时候,根据约定好的标识去存储的数据里找,确实可以实现,但是忽略了这是网络数据,是可以更换设备的,换个设备,数据从哪取呢?哈哈,这种投机取巧的方案,实在是不可取。


那没办法了,解析富文本呗,然后逐次取出图片和内容,再封装成集合,回显到RecyclerView中即可。


三、富文本解析


以下是发布成功后,某个字段的富文本信息,我们拿到之后,需要回显到编辑的页面,也就是自定义的RecyclerView中,老铁们,你们的第一解决方案是什么?


<p>我是测试内容</p><p>我是测试内容12333</p><img src="https://www.vipandroid.cn/ming/image/gan.png"/><p>我是测试内容88888</p><p>我是测试内容99999999</p><img src="https://www.vipandroid.cn/ming/image/zao.png"/>

我们最终需要拿到的数据,如下,只有这样,我们才能逐一封装到集合,回显到列表中。


    我是测试内容
我是测试内容12333
https://www.vipandroid.cn/ming/image/gan.png
我是测试内容88888
我是测试内容99999999
https://www.vipandroid.cn/ming/image/zao.png

字符串截取呗,我相信这是大家的第一直觉,以什么方式截取,才能拿到标签里的内容呢?可以负责任的告诉大家,截取是可以实现的,需要实现的逻辑有点多,我简单的举一个截取的例子:


            content = content.replace("<p>", "")
val split = content.split("</p>")
val contentList = arrayListOf<String>()
for (i in split.indices) {
val pContent = split[i]
if (TextUtils.isEmpty(pContent)) {
continue
}
if (pContent.contains("img")) {
//包含了图片
val imgContent = pContent.split("/>")
for (j in imgContent.indices) {
val img = imgContent[j]
if (img.contains("img")) {
//图片,需要再次截取
val index = img.indexOf(""")
val last = img.lastIndexOf("""
)
val endImg = img.substring(index + 1, last)//最终的图片内容
contentList.add(endImg)
} else {
//文本内容
contentList.add(img)
}
}
} else {
contentList.add(pContent)
}
}

截取的方式有很多种,但是无论哪一种,你的判断是少不了的,为了取得对应的内容,不得不多级嵌套,不得不一而再再而三的进行截取,虽然实现了,但是其冗余了代码,丢失了效率,目前还是仅有两种标签,如果说以后的富文本有多种标签呢?想想都可怕。


有没有一种比较简洁的方式呢?必须有,那就是正则表达式,需要解决两个问题,第一、正则怎么用?第二,正则表达式如何写?搞明白这两条之后,获取富文本中想要的内容就很简单了。


四、Kotlin中的正则使用


说到正则,咱就不得不聊聊Java中的正则,这是我们做Android再熟悉不过的,一般也是最常用的,基本代码如下:


    String str = "";//匹配内容
String pattern = "";//正则表达式
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(str);
System.out.println(m.matches());

获取匹配内容的话,取对应的group即可,这个例子太多了,就不单独举了,除了Java中提供的Api之外,在Kotlin当中,也提供了相关的Api,使用起来也是无比的简单。


在Kotlin中,我们可以使用Regex这个对象,主要用于搜索字符串或替换正则表达式对象,我们举几个简单的例子。


1、判定是否包含某个字符串,containsMatchIn


     val regex = Regex("Ming")//定义匹配规则
val matched = regex.containsMatchIn("AbnerMing")//传入内容
print(matched)

输出结果


    true

2、匹配目标字符串matches


     val regex = """[A-Za-z]+""".toRegex()//只匹配英文字母
val matches1 = regex.matches("abcdABCD")
val matches2 = regex.matches("12345678")
println(matches1)
println(matches2)

输出结果


    true
false

3、返回首次出现指定字符串find


    val time = Regex("""\d{4}-\d{1,2}-\d{1,2}""")
val timeValue= time.find("今天是2023-6-28,北京,有雨,请记得带雨伞!")?.value
println(timeValue)

输出结果


    2023-6-28

4、返回所有情况出现目标字符串findAll


     val time = Regex("""\d{4}-\d{1,2}-\d{1,2}""")
val timeValue = time.findAll(
"今天是2023-6-28,北京,有雨,请记得带雨伞!" +
"明天是2023-6-29,可能就没有雨了,具体得等到后天2023-6-30日才能知晓!"
)
timeValue.forEach {
println(it.value)
}

输出结果


    2023-6-28
2023-6-29
2023-6-30

ok,当然了,里面还有许多方法,比如替换,分割等,这里就不介绍了,后续有时间补一篇,基本上常用的就是以上的几个方法。


五、富文本使用正则获取内容


一个富文本里的标签有很多个,显然我们都需要进行获取里面的内容,这里肯定是要使用findAll这个方法了,但是,我们该如何设置标签的正则表达式呢?


我们知道,富文本中的标签,都是有左右尖括号组成的,比如<p></p>,<a></a>,当然也有单标签,比如<img/>,<br/>等,那这就有规律了,无非就是开头<开始,然后是不确定字母,再加上结尾的>就可以了。


1、标签精确匹配


比如有这样一个富文本,我们要获取所有的<p></p>标签。


 <div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>

我们的正则表达式就如下:


  <p.*?>(.*?)</p>

什么意思呢,就是以<p开头,</p>结尾,这个点. 是 除换行符以外的所有字符,* 为匹配 0 次或多次,? 为0 次或 1 次匹配,之所以开头这样写<p.*?>而不是<p>,一个重要的原因就是需要匹配到属性或者空格,要不然富文本中带了属性或空格,就无法匹配了,这个需要注意!


基本代码


         val content = "<div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>"
val matchResult = Regex("""<p.*?>(.*?)</p>""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


   <p>我是一个段落</p>
<p>我是另一个一个段落</p>

看到上面的的结果,有的老铁就问了,我要的是内容啊,怎么把标签也返回了,这好像有点不对吧,如果说我们只要匹配到的字符串,目前是对的,但是想要标签里的内容,那么我们的正则需要再优化一下,怎么优化呢,就是增加一个开始和结束的位置,内容的开始位置是”<“结束位置是”>“,如下图



我们只需要更改下起始位置即可:


匹配内容


     val content = "<div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>"
val matchResult = Regex("""(?<=<p>).*?(?=</p>)""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


    我是一个段落
我是另一个一个段落

2、所有标签进行匹配


有了标签精确匹配之后,针对富文本里的所有的标签内容匹配,就变得很是简单了,无非就是要把上边案例中的p换成一个不确定字母即可。


匹配内容


     val content = "<div>早上好啊</div><p>我是一个段落</p><a>我是一个链接</a><p>我是另一个一个段落</p>"
val matchResult = Regex("""(?<=<[A-Za-z]*>).+?(?=</[A-Za-z]*>)""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


    早上好啊
我是一个段落
我是一个链接
我是另一个一个段落

3、单标签匹配


似乎已经满足我们的需求了,因为富文本中的内容已经拿到了,封装到集合之中,传递到列表中即可,但是,以上的正则似乎只针对双标签的,带有单标签就无法满足了,比如,我们再看下初始我们要匹配的富文本,以上的正则是匹配不到img标签里的src内容的,怎么搞?


 <p>我是测试内容</p><p>我是测试内容12333</p><img src="https://www.vipandroid.cn/ming/image/gan.png"/><p>我是测试内容88888</p><p>我是测试内容99999999</p><img src="https://www.vipandroid.cn/ming/image/zao.png"/>

很简单,单标签单独处理呗,还能咋弄,多个正则表达式,用或拼接即可,属性值也是这样的获取原则,定位开始和结束位置,比如以上的img标签,如果要获取到src中的内容,只需要定位开始位置”src="“,和结束位置”"“即可。


匹配内容


    val content =
"<p>我是测试内容</p><p>我是测试内容12333</p><img src="https://www.vipandroid.cn/ming/image/gan.png"/><p>我是测试内容88888</p><p>我是测试内容99999999</p><img src="https://www.vipandroid.cn/ming/image/zao.png"/>"
val matchResult =
Regex("""((?<=<[A-Za-z]*>).+?(?=</[A-Za-z]*>))|((?<=src=").+?(?="))""").findAll(content)
matchResult.forEach {
println(it.value)
}

运行结果


    我是测试内容
我是测试内容12333
https://www.vipandroid.cn/ming/image/gan.png
我是测试内容88888
我是测试内容99999999
https://www.vipandroid.cn/ming/image/zao.png

这不就完事了,简简单单,心心念念的数据就拿到了,拿到富文本标签内容之后,再封装成集合,回显到RcyclerView中就可以了,这不很easy吗,哈哈~


点击草稿,我们看下效果:



六、总结


在正向的截取思维下,正则表达式无疑是最简单的,富文本,无论是标签匹配还是内容以及属性,都可以使用正则进行简单的匹配,轻轻松松就能搞定,需要注意的是,不同属性的匹配规则是不一样的,需要根据特有的情况去分析。


作者:程序员一鸣
来源:juejin.cn/post/7249604020875984955
收起阅读 »

三分钟教会你微信炸一炸,满屏粑粑也太可爱了!

相信这个特效你和你的朋友(或对象)一定玩过 当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。 不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢...
继续阅读 »

相信这个特效你和你的朋友(或对象)一定玩过

请添加图片描述

当你发送一个便便的表情,对方如果扔一个炸弹表情,就会立刻将这个便便炸开,实现满屏粑粑的“酷炫”画面。可谓是“臭味十足”,隔着屏幕都能感受到来自微信爸爸的满满恶意。


不清楚大家对这个互动设计怎么看,反正一恩当时是喜欢的不行,拉着朋友们就开始“炸”得不亦乐乎。


同样被虏获芳心的设计小哥哥在玩到尽兴后,突然灵感大发,连夜绘制出了设计稿,第二天就拉上产品和研发开始脑暴。


“微信炸💩打动我的一点是他满屏的设计,能够将用户强烈的情绪抒发出来;同时他能够与上一条表情进行捆绑,加强双方的互动性。”设计小哥哥声情并茂道。


“所以,让我们的表情也‘互动’起来吧!”


这不,需求文档就来了:



改掉常见的emoji表情发送方式,替换为动态表情交互方式。即,

当用户发送或接收互动表情,表情会在屏幕上随机分布,展示一段时间后会消失。

用户可以频繁点击并不停发送表情,因此屏幕上的表情是可以非常多且重叠的,能够将用户感情强烈抒发。

请添加图片描述

(暂用微信的聊天界面进行解释说明,图1为原样式,图2是需求样式)



这需求一出,动态表情在屏幕上的分布方案便引起了研发内部热烈讨论:当用户点击表情时,到底应该将表情放置在屏幕哪个位置比较好呢?


最直接的做法就是完全随机方式:取0到屏幕宽度和高度中随机值,放置表情贴纸。但这么做的不确定因素太多,比如存在一定几率所有表情都集中在一个区域,布局边缘化以及最差的重叠问题。因此简单的随机算法对于用户的体验是无法接受的。


在这里插入图片描述


我们开始探索新的方案:

因为目前点的选择依赖于较多元素,比如与屏幕已有点的间距,与中心点距离以及屏幕已有点的数目。因此最终决定采用点权随机的方案,根据上述元素决策出屏幕上可用点的优度,选取优度最高的插入表情。


基本思路


维护对应屏幕像素的二维数组,数组元素代指新增图形时,图形中心取该点的优度。

采用懒加载的方式,即每次每次新增图形后,仅记录现有方块的位置,当需要一个点的优度时再计算。


遍历所有方块的位置,将图形内部的点优度全部减去 A ,将图形外部的点按到图形的曼哈顿距离从 0 到 max (W,H),映射,减 0 到 A * K2。

每次决策插入位置时,随机取 K + n * K1 个点,取这些点中优度最高的点为插入中心

A, K, K1, K2 四个常数可调整



一次选择的复杂度是 n * randT,n 是场上方块数, randT 是本次决策需要随机取多少个点。 从效率和 badcase来说,这个方案目前最优。



在这里插入图片描述


代码展示



```cpp
#include <iostream>
#include <vector>
using namespace std;

const int screenW = 600;
const int screenH = 800;

const int kInnerCost = 1e5;
const double kOuterCof = .1;

const int kOutterCost = kInnerCost * kOuterCof;

class square
int x1;

int x2;
int y1;
int y2;
};

int lineDist(int x, int y, int p){
if (p < x) {
return x - p;
} else if (p > y) {
return p - y;
} else {
return 0;
}
}

int getVal(const square &elm, int px, int py){
int dx = lineDist(elm.x1, elm.x2, px);
int dy = lineDist(elm.y1, elm.y2, py);
int dist = dx + dy;
constexpr int maxDist = screenW + screenH;
return dist ? ( (maxDist - dist) * kOutterCost / maxDist ) : kInnerCost;
}

int getVal(const vector<square> &elmArr, int px, int py){
int rtn = 0;
for (auto elm:elmArr) {
rtn += getVal(elm, px, py);
}
return rtn;
}

int main(void){

int n;
cin >> n;

vector<square> elmArr;
for (int i=0; i<n; i++) {
square cur;
cin >> cur.x1 >> cur.x2 >> cur.y1 >> cur.y2;
elmArr.push_back(cur);
}


for (;;) {
int px,py;
cin >> px >> py;
cout << getVal(elmArr, px, py) << endl;
}

}

优化点



  1. 该算法最优解偏向边缘。因此随着随机值设置越多,得出来的点越偏向边缘,因此随机值不能设置过多。

  2. 为了解决偏向边缘的问题,每一个点在计算优度getVal需要加上与屏幕中心的距离 * n * k3


效果演示


最后就是给大家演示一下最后的效果啦!

请添加图片描述

圆满完成任务,收工,下班!


作者:李一恩
来源:juejin.cn/post/7257410685118677048
收起阅读 »

Android一秒带你定位当前页面Activity

前言 假设有以下路径 在过去开发时,我们在点击多层页面的后,想知道当前页面的类名是什么,以上图下单页面为例,我们首先 1、查找首页的搜索酒店按钮的ID XML布局中找到首页的搜索酒店按钮的ID:假设按钮的ID是 R.id.bt_search_hotel ...
继续阅读 »

前言


假设有以下路径


image.png
在过去开发时,我们在点击多层页面的后,想知道当前页面的类名是什么,以上图下单页面为例,我们首先



  • 1、查找首页的搜索酒店按钮的ID

    • XML布局中找到首页的搜索酒店按钮的ID:假设按钮的ID是 R.id.bt_search_hotel



  • 2、从首页Activity中查找按钮的点击事件

    • 假设你有一个点击事件处理器方法 onSearchHotelClick(View view),你可以在首页Activity中找到这个方法的实现



  • 3、进入下一个酒店列表页面Activity

    • 在点击事件处理方法中,启动酒店列表页面的Activity,示例参数值:




Intent intent = new Intent(this, HotelListActivity.class);
startActivity(intent);


  • 4、若多个RecyclerView,需要找到RecyclerView的ID,并在适配器中处理点击事件

    • 在酒店列表页面的XML布局中找到RecyclerView的ID:假设RecyclerView的ID是 R.id.rvHotel

    • 在适配器中处理点击事件,示例参数值




rvHotel.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
// 处理点击事件,启动酒店详情页面的Activity
Intent intent = new Intent(context, HotelDetailActivity.class);
intent.putExtra("hotel_id", hotelList.get(position).getId());
startActivity(intent);
}
});


  • 在酒店详情页面中找到XML中预定按钮的ID,并处理点击事件:

    • 在酒店详情页面的XML布局中找到预定按钮的ID:假设按钮的ID是 R.id.stv_book

    • 在详情页面Activity中找到预定按钮的点击事件处理方法,示例参数值




Button bookButton = findViewById(R.id.bookButton);
bookButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 处理点击事件,启动下单页面的Activity
Intent intent = new Intent(DetailActivity.this, OrderActivity.class);
startActivity(intent);
}
});

上面我们发现存在两个问题:



  1. 在定位Activity这个过程中可能会消耗大量的时间和精力,特别是在页面层级较深或者页面结构较为复杂的情况下。

  2. 我们点击某个属性的时候,有时候想知道当前属性的id是什么,然后去做一些逻辑或者赋值等,我们只能去找布局,如果布局层次深,又会浪费大量的时间去定位属性


如果我们能够在1s快速准确地获取当前Activity的类名,那么在项目开发过程中将起到关键性作用,节省了大量时间,减少了开发中的冗余工作。开发人员的开发流程将更加高效,能更专注于业务逻辑和功能实现,而不用花费过多时间在页面和属性定位上


为什么要实现一秒定位当前页面Activity



  • 优化了Android应用程序的性能,实现了快速的页面定位,将当前Activity的定位时间从秒级缩短至仅1秒

  • 提高了开发效率,允许团队快速切换页面和快速查找当前页面的类名,减少了不必要的开发时间浪费

  • 这一优化对项目推进产生了显著影响,提高了整体开发流程的高效性,使我们能够更专注于业务逻辑的实现和功能开发


使用的库是:AsmActualCombat



  • AsmActual利用ASM技术将合规插件会侵入到编译流程中, 插件会把App中所有系统敏感API或属性替换为SDK的收口方法 , 从而解决直接使用系统方法时面临的隐私合规问题


AsmActualCombat库的使用


使用文档链接:github.com/Peakmain/As…


How To


旧版本添加方式


ASM插件依赖
Add it in your root build.gradle at the end of repositories:


buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "io.github.peakmain:plugin:1.1.4"
}
}

apply plugin: "com.peakmain.plugin"

拦截事件sdk的依赖



  • Step 1. Add the JitPack repository to your build file
    Add it in your root build.gradle at the end of repositories:


   allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}


  • Step 2. Add the dependency


   dependencies {
implementation 'com.github.Peakmain:AsmActualCombat:1.1.5'
}

新版本添加方式


settings.gradle


pluginManagement {
repositories {
//插件依赖
maven {
url "https://plugins.gradle.org/m2/"
}
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
//sdk仓库
maven { url 'https://jitpack.io' }
}
}

插件依赖


根目录下的build.gradle文件


plugins {
//插件依赖和版本
id "io.github.peakmain" version "1.1.4" apply false
}

sdk版本依赖


implementation 'com.github.Peakmain:AsmActualCombat:1.1.5'

使用


我们只需要在application的时候调用以下即可


SensorsDataAPI.init(this);
SensorsDataAPI.getInstance().setOnUploadSensorsDataListener((state, data) -> {
switch (state) {
case SensorsDataConstants.APP_START_EVENT_STATE:
//$AppStart事件
case SensorsDataConstants.APP_END__EVENT_STATE:
//$AppViewScreen事件
break;
case SensorsDataConstants.APP_VIEW_SCREEN__EVENT_STATE:
if (BuildConfig.DEBUG) {
Log.e("TAG", data);
}
StatisticsUtils.statisticsViewHeader(
GsonUtils.getGson().fromJson(data, SensorsEventBean.class));
break;
case SensorsDataConstants.APP_VIEW_CLICK__EVENT_STATE:
if (BuildConfig.DEBUG) {
Log.e("TAG", data);
}
SensorsEventBean sensorsEventBean =
GsonUtils.getGson().fromJson(data, SensorsEventBean.class);
StatisticsUtils.statisticsClickHeader(sensorsEventBean);
break;
default:
break;

}
});

随后我们点击按钮在控制台便可以看到效果



  • 页面埋点


image.png



  • 点击埋点


image.png


总结



  • 是不是很简单呢,只需要简单配置即可1s实现定位当前页面Activity的类名是什么,不需要再花费大量的时间去查找当前页面的类名。

  • 当然,AsmActualCombat项目不仅仅可以实现全埋点、定位当前Activity类名功能,还可以拦截隐私方法调用的拦截哦。

  • 如果大家觉得项目或者文章对你有一点点作用,欢迎点赞收藏哦,非常感谢


作者:peakmain9
来源:juejin.cn/post/7289047550741397564
收起阅读 »

团队的效率在于规范和沟通,而不仅仅在于技术

感谢你阅读本文! 初入职场的时候,总觉得很多事情没必要做,因为不仅浪费时间,而且还繁琐,因为人面对一件事的时候,如果自己能够快速解决,那么就不愿意再介入第三人,因为会花费更多的时间,加上大多人从内心出发是不太愿意去沟通的! 但是我们永远要相信的是,无论你这个人...
继续阅读 »

感谢你阅读本文!


初入职场的时候,总觉得很多事情没必要做,因为不仅浪费时间,而且还繁琐,因为人面对一件事的时候,如果自己能够快速解决,那么就不愿意再介入第三人,因为会花费更多的时间,加上大多人从内心出发是不太愿意去沟通的!


但是我们永远要相信的是,无论你这个人心再细,技术再牛,你总会有想不到的地方,而这些盲区大概率就是造成日后出问题的导火索!


下面我们就来聊一聊规范、沟通、技术!


规范


我上一次裸辞,上级和我聊的时候,我说了两点原因!


第一是我不想在当前的领域继续干下去了,因为我知道这个领域对我来说已经很不利了,如果再继续干下去,那只能是温水煮青蛙,最终害了自己!


第二就是规范问题,这点其实在之前我也有反馈过,不过一直都没有真正去实施,在提了离职后,谈话的时候我又去反复说这个问题!


因为之前我们线上出现的很多问题就是因为不规范造成的,我记得当时除了研发,我还负责部署,因为他们没有在测试环境测好,到了线上环境就出大问题了,恢复数据都没用,后面停服一天才恢复好。


为啥会出现这种问题!


1.职责划分不清


这点的话还是和公司的规模有关,如果公司团队比较小,那么开发就不得不身兼数职,从扫地干到CTO都行。


我们部门虽然人不多,但是麻雀虽小五脏俱全,不过遗憾的是,根本没去划分好职责,站在最前面的也是比较容易背锅的,很多时候任务倾斜特别严重。


2.没有严格按照流程来走


一个团队里面如果没有严格的流程,那么就会问题百出,特别是达到一定的规模后,有一些我们看似没必要的流程,是因为自己觉得麻烦,但是站在管理的视角,就显得尤为重要。


严格的流程是稳定和安全的保障,如果因为懒惰或者“方便”而去省略流程,那么终有一日会付出N倍的代价!


所以一个明确的规范可以帮助团队成员了解他们的职责和期望。这可以减少混乱和误解,从而提高团队的效率。规范也可以确保所有的工作都按照相同的标准进行,从而提高产品或服务的质量。


沟通


一个技术再牛逼的团队,如果不能做到有效的沟通,那么也是一盘散沙,一个人再强的人,如果不能让别人听懂他说的话,那么也是寸步难行!


沟通除了会议上要尽力把自己想表达的表达清楚,最重要的还是私下的沟通,因为会议上的东西大多都需要进行再次更改,这时候线下个人与个人之间的沟通就变得更加重要。


基本上百分之八十的问题都是沟通不到位造成的,很多时候你觉得你想的是对的,那是因为你还没有去很了解这个事物,这时候你其实就处于一个信息茧房里,所以一定是会出现问题的。


有效的沟通是任何团队成功的关键。通过沟通,团队成员可以分享信息,解决问题,协调工作,以及建立和维护良好的工作关系。缺乏有效的沟通可能会导致误解,冲突,以及工作效率的降低。


技术


技术和赚钱的关系,就是艺术和赚钱的关系。不卖座的戏只能当成兴趣。


技术是服务于项目,而项目依赖于团队,很多时候我们总是去痴迷各种新技术,不管成熟不成熟,适合不适合,往上面堆就行了,但是如果不去考虑团队的兼容性,不考虑是否好维护,那么只会自找麻烦。


热爱新技术,追去新技术是没错的,但是要根据实际情况来,并不是你的系统一定要设计成分布式,微服务,云原生,对于有些项目,QPS 50都没有,硬是要去设计成分布式,不仅花费了大量的成本,而且维护成本也高,实际上一个单体项目只要设计得好,对于中小型应用完全够用,性能比分布式的好。


合适永远比先进好,特别不是技术驱动的公司,jsp依然能够赚得盆满钵满。


但是并不是技术就不重要了,特别对于从事技术的人来说,这是安身立命之本,只有技术够硬,在脱离平台后才不会焦虑,平台能力永远不算能力,那可能是自己运气好,而脱离平台后依然能够走下去,这才是真正的能力!


总结


规范和沟通不论对于任何行业都是必须的,只有在规范和沟通中生产,产品的质量才能得到保证,团队的效率才能得到提升,技术则驱动产品进步,虽然不是必须,但是如果想在时代的进程中不被淘汰,那么技术是不可或缺的!


作者:刘牌
来源:juejin.cn/post/7291064482054209571
收起阅读 »

服务:简聊微内核结构

1 简介:微内核架构 微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。 微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。 该结...
继续阅读 »

1 简介:微内核架构


微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。


微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。


该结构是向最初并非设计为支持它的系统添加特定功能的最佳方式。


此体系结构消除了对应用程序可以具有的功能数量的限制。我们可以添加无限的插件(例如Chrome浏览器有数百个插件,称为扩展程序)


2 一个简单例子


微内核架构(也称为插件结构)通常用于实现可做为第三方产品下载的应用程序。此结构在内部业务程序很常见。


实际上,它可以被嵌入到其他模式中,例如分层体系中。典型的微内核架构有两个组件:核心系统和插件模块


	plug-in                  plug-in
core system
plug-in plug-in

漂亮一点的图


new_微内核架构.png


由上图可知,微内核架构也被称为插件架构模式(Plug-inArchitecture Patterm),通常由内核系统和插件组成的原因。


核心系统包括使系统正确运行的最小业务逻辑。可以通过连接插件组件添加更多功能,扩展软件功能。就像为汽车添加涡轮以提高动力。


轮圈37.png


插件组件可以使用开放服务网关计划(OSGi),消息传递,Web服务或对象实例化进行连接。
需要注意的是,插件组件是独立的组件,是为扩展或增强核心系统的功能,不应与其他组件形成依赖。


常见的系统结构使用微内核的如:嵌入式Linux、L4、WinCE。



  • 优缺点说明


微服务在应用程序和硬件的通信中,内核进程和内存管理的极小的服务,而客户端程序和运行在用户空间的服务通过消息的传递来建立通信,它们之间不会有直接的交互。


这样微内核中的执行速度相对就比较慢了,性能偏低这是微内核架构的一个缺点。


微内核系统结构相当清晰,有利于协作开发;微内核有良好的移植性,代码量非常少;微内核有相当好的伸缩性、扩展性。


3 小结


(1)微内核架构难以进行良好的整体化优化。

由于微内核系统的核心态只实现了最基本的
系统操作,这样内核以外的外部程序之间的独立运行使得系统难以进行良好的整体优化。


(2)微内核系统的进程间通信开销也较单一内核系统要大得多。

从整体上看,在当前硬件条件下,微内核在效率上的损失小于其在结构上获得的收益。


(3)通信损失率高。

微内核把系统分为各个小的功能块,从而降低了设计难度,系统的维护与修改也容易,但通信带来的效率损失是一个问题。


作者:楽码
来源:juejin.cn/post/7291468863396708413
收起阅读 »

关于我调部门感觉又重新面试一次这件事,做出知识总结

web
前言 这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识...
继续阅读 »

前言



这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识掌握的差不多了,路还远着,还得学啊!本来那天我还准备一下班就回去玩战地2042,免费周啊!啪的一下兴趣全无,总结一下知识吧,指不定什么时候用上(手动狗头)



节流


节流是指一定时间内只触发一次函数调用,如果在指定时间内多次触发,执行第一次,其他的触发将会被忽略,直到过了设定的时间间隔才触发。


function throttle (fn,delay) {
let timer;
retrun function (...args) {
if(!timer) {
fn(this,args)
timer = settimeout(()=>{
timer=null
},delay)
}
}
}

防抖


防抖是在函数调用后,在指定时间间隔后才触发一次。如果在这个时间间隔内再次触发函数,将重新计时,直到过了设定的时间间隔才会触发最后一次函数调用。


function debounce (fn,delay) {
let timer;
retrun function (...args) {
if(timer) {
clearTimetout(timer)
}
timer = settimeout(()=>{
fn(this,args)
timer=null
},delay)
}
}

数据扁平化


数组


function flatter(arr) {
let result = []
for(let i =0;i<arr.length;i++) {
if(Array.isArray(arr[i]) {
result = result.concat(flatter(arr[i]))
} esle {
result.push(arr[i])
}
}
return result
}

去重


const arr1 = [...new Set(arr)]

const arr1 = arr.map((item,index)=>{
return arr.indexof(item)==index
})

查找字符串中出现最多的字符


当时手写了一半,str.split(item).length应该还要-1才是当前字符出现的次数


  const str = ref<string>('sdfgsgdd');
const fn = (str: string) => {
const arr = Array.from(str);
let maxCount = 0;
let mostFrequentChar = '';
const Nsome = [...new Set(arr)];
Nsome.forEach((item) => {
const count = str.split(item).length - 1;
if (count > maxCount) {
maxCount = count;
mostFrequentChar = item;
}
});
console.log('出现最多的次数,字符', maxCount, mostFrequentChar);
};

闭包及其应用场景


我的回答是:
函数里面嵌套函数,并且内部函数引用了外部函数的变量,就是函数能访问其作用域外的变量


应用场景:
我的回答其中之一是:vueX中状态共享是使用了闭包,节流,防抖
但在 Vuex 中,闭包主要用于封装和共享状态,而不是用于访问和操作外部函数的变量。它使用了闭包的概念,但不是严格意义上的闭包。


1.模块化开发 2.回调函数 3.延迟执行(节流,防抖)


原型&原型链及其应用场景



  1. 原型(Prototype):



  • 每个 JavaScript 对象都有一个原型(prototype),它是一个对象。

  • 对象的原型用于共享属性和方法,当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型可以通过 proto 属性访问,也可以通过 Object.getPrototypeOf() 方法获取。



  1. 原型链(Prototype Chain):



  • 原型链是由对象的原型组成的链式结构,它用于实现对象之间的继承。

  • 当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型链的顶端是 Object.prototype,它是所有对象的原型。


应用场景:



  • 继承:通过原型链,我们可以实现对象之间的继承,一个对象可以继承另一个对象的属性和方法。这样可以避免重复定义和维护相似的代码,提高代码的重用性和可维护性。

  • 共享属性和方法:通过原型链,我们可以将属性和方法定义在原型上,从而实现对象之间的共享。这样可以节省内存空间,避免重复创建相同的属性和方法。

  • 扩展原生对象:通过修改原型链,我们可以扩展 JavaScript 的原生对象,为其添加新的方法和属性。这样可以为原生对象添加自定义的功能,满足特定的需求。


在没有class之前,js是怎么做面向对象的


没答出来,只知道js可以通过class实现面向对象,然后又被问在没有class之前,js是怎么做面向对象的。这也是原型链的应用场景之一,可能是前面原型链的应用场景没说这个,想给我一个提示。


在没有class关键字之前,JavaScript使用原型继承来实现面向对象编程。
javaScript 中的每个对象都有一个原型(prototype),原型是一个对象,它包含了共享的属性和方法。当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。


通过原型链,我们可以实现对象之间的继承和共享属性和方法。下面是一个使用原型继承的示例:


// 创建一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在构造函数的原型上定义方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 创建一个 Person 对象
const person1 = new Person('Alice', 25);
// 调用对象的方法
person1.sayHello(); // 输出 "Hello, my name is Alice and I am 25 years old."

node是什么,express是什么,node服务中的中间件是用来干什么的


Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以用于构建高性能的网络应用程序。它允许使用 JavaScript 在服务器端运行代码,而不仅仅局限于在浏览器中运行。


Express 是一个基于 Node.js 的 Web 应用程序框架,它提供了一组简洁而灵活的功能,用于构建 Web 应用程序和 API。Express 提供了路由、中间件、模板引擎等功能,使得构建 Web 应用程序变得更加简单和高效。


中间件的作用是增强和扩展 Node.js 服务的功能,使得处理请求和响应的过程更加灵活和可定制。通过使用中间件,可以将常见的功能模块化,提高代码的可维护性和可重用性。


Express 提供了一些内置的中间件,同时也支持自定义中间件。您可以使用内置的中间件,如 express.json()、express.urlencoded() 来处理请求体的解析,或者编写自己的中间件来满足特定的需求。


你h5怎么处理兼容性


因为是vite+v3项目,vite官方有推荐的插件库,在插件库中有一个关于浏览器兼容支持的插件:@vitejs/plugin-legacy


插件@vitejs/plugin-legacy的作用是为打包后的文件提供传统浏览器兼容性支持



  1. 首先安装插件:npm i @vitejs/plugin-legacy -D

  2. 然后在vite.config.js中配置


import legacyPlugin from '@vitejs/plugin-legacy'
export default defineConfig( {
plugins: [
legacyPlugin({
targets:['chrome 52'], // 需要兼容的目标列表,可以设置多个
additionalLegacyPolyfills:['regenerator-runtime/runtime'] // 面向IE11时需要此插件
})
]
})

rem,px,em这些有什么区别




  1. px(像素):px 是绝对单位,表示屏幕上的一个物理像素点。它是最常用的单位,具有固定的大小,不会根据其他因素而改变。例如,font-size: 16px; 表示字体大小为 16 像素。




  2. rem(根元素字体大小的倍数):rem 是相对单位,相对于根元素(即 元素)的字体大小。如果根元素的字体大小为 16 像素,那么 1rem 就等于 16 像素。如果根元素的字体大小为 20 像素,那么 1rem 就等于 20 像素。通过设置根元素的字体大小,可以方便地调整整个页面的大小。例如,font-size: 1.5rem; 表示字体大小为根元素字体大小的 1.5 倍。




  3. em(相对于父元素字体大小的倍数):em 也是相对单位,相对于父元素的字体大小。如果父元素的字体大小为 16 像素,那么 1em 就等于 16 像素。如果父元素的字体大小为 20 像素,那么 1em 就等于 20 像素。通过设置父元素的字体大小,可以影响其子元素的大小。例如,font-size: 1.2em; 表示字体大小为父元素字体大小的 1.2 倍。




总结来说,px 是绝对单位,不会随其他因素改变;rem 是相对于根元素字体大小的倍数,可以方便地调整整个页面的大小;em 是相对于父元素字体大小的倍数,可以影响子元素的大小。


在实际使用中,可以根据需求选择合适的单位。对于响应式设计,使用 rem 可以方便地调整整个页面的大小;对于局部样式,可以使用 px 或 em 来控制具体的大小。


你工作中遇到了什么坑或者解决什么让自己印象深刻的问题



  • element-plus的el-table表格的二次封装(可以使用tsx)

  • el-table表格的动态合并

  • h5 ios时调起键盘会把整个布局往上推

  • h5调用封装的app JSbrige完成返回

  • 登录的拼图验证

  • h5嵌套在微信小程序中时,由我们h5跳到三方提供的安全验证h5页面,返回时,本地存储的东西没了

  • 利用git hooks+husky+eslint完成前端代码规范和提交规范

  • 银行卡拖拽排序,把排完的顺序返回服务端


上面这些都是我解决了,也不仅仅只有这些,回头想了了下明明自己有很多可以说的,在当时就说了2,3个,然后负责人问我还有吗时,我卡壳了,居然不知道还要说什么。后面我感觉也是根据这个展开来问的


V2混入和V3的hooks,为什么V3要改成hooks的方式


感觉应该是问hooks的好处吧?反正我是答的不太对的,以下是总结:


Vue 3 引入了 Composition API(包括 setup 函数和 hooks),这是一个新的方式来组织和复用代码,与 Vue 2 的混入(mixins)有所不同。

混入在 Vue 2 中被广泛使用,它们允许你在多个组件之间共享行为。然而,混入有一些问题:



  1. 命名冲突:如果混入和组件有相同的方法或数据属性,可能会导致冲突。

  2. 来源不明:当一个组件使用了多个混入时,可能很难确定一个特定的方法或数据属性来自哪个混入。

  3. 复杂性:混入可以包含生命周期钩子、方法、数据等,这可能会增加理解和维护组件的复杂性。

    相比之下,Vue 3 的 Composition API(包括 hooks)提供了一种更灵活、更可控的方式来组织和复用代码:

  4. 更好的逻辑复用和代码组织:你可以将相关的代码(如数据、方法和生命周期钩子)组织在一起,而不是强制按照 Vue 的选项(data、methods、created 等)来组织代码。

  5. 更好的类型推断:对于使用 TypeScript 的项目,Composition API 提供了更好的类型推断。

  6. 更清晰的来源:每个函数和响应式数据的来源都非常明确,因为它们都是从特定的 hook 或 setup 函数返回的。
    因此,虽然 Vue 3 仍然支持混入,但推荐使用 Composition API 来组织和复用代码。


vue3中怎么封装一个自定义指令



  • 通过app.directive()方法注册指令,该方法接受两个参数,第一个参数是指令的名称,第二个参数是一个对象,包含指令的各个生命周期的钩子函数

  • 然后我们就可以在生命周期的钩子函数中定义指令的行为,根据指令的需求,在相应的生命周期钩子函数中编写逻辑代码


什么情况下会使用自定义指令


我的回答是:想要操作dom元素时并且这种类似情况经常出现,如节流和防抖指令,就是给dom加上disabled。按钮权限指令,给当前按钮dom一个显示和隐藏


拖拽排序


拖拽排序的实现原理主要涉及一下几个步骤:



  • 1.监听拖拽事件: 浏览器提供了一系列的拖拽事件,设置draggable="true"



    1. 开始拖拽:当用户开始拖拽一个元素时,会触发 dragstart 事件。在这个事件的处理函数中,我们可以通过 传入的dragstart(e,index) ,中的index来设置当前被拖拽元素的下标。





    1. 拖拽过程:当用户拖拽元素时,会不断触发 dragover 事件。在这个事件的处理函数中,我们需要调用 event.preventDefault 方法来阻止浏览器的默认行为,否则无法触发 拖拽 事件。





    1. 拖拽到另一个元素区域时:当用户拖拽到另一个元素时,会触发 dragenter 事件。在这个事件的处理函数中,我们可以通过 dragente(e,index)方法来获取拖拽到的元素的下标,然后根据获取的两下标来更新列表的排序。




表格动态合并


element-plus表格合并(例如前两列合并) | 耀耀切克闹 (yaoyaoqiekenao.com)


模拟new实例创建的过程



  • 1.创建了新对象并将._proto_指向构造函数.prototype

  • 2.将this指向新创建的对象

  • 3.返回新对象


function newSimulator() {
//1.创建新对象
const obj = new Object()
//2.设置_proto_为构造函数prototype
const constructor = [].shift.call(arguments)
obj._proto_ = constructor.prototype
//3.this指向新对象,也就是改变this的指向
const ret = constructor.apply(obj,arguments)
//4.返回对象或this
return typeof ret = 'object' ? ret : obj
}

冒泡排序


const arr = [1,7,9,2,3,5]
for(let i=0;i<arr.length;i++){
for(let j=0;j<arr.length-i-1;j++){
let a = []
if(arr[j]<arr[j+1]){
a =arr[j]
arr[j]=arr[j+1]
arr[j+1]=a
}
}
}


深拷贝


1.使用 JSON 序列化和反序列化


const obj={
arr:[1,2]
}
const clone = JSON.parse(JSON.stringify(obj))

2.使用递归完成深拷贝


这种方式通过递归地遍历原始对象,并对该对象的的属性进行逐一的深拷贝,以创建一个原对象的独立副本。


function deepCloneObject(obj) {
if(obj ===null||typeof object !='object') {
return obj
}
const clone = Array.isArray(obj)?[]:{}
for(let key in obj) {
if(object.prototype.hasOwnProperty.call(obj,key))
clone[key] = deepClone(obj[key])
}
retrun clone
}

函数柯里化


函数柯里化是一种将具有多个参数的函数转换为逐个应用参数的函数序列的技术。通过柯里化,我们可以将一个函数的多个参数转化为一系列嵌套的函数调用。


柯里化的优点是可以创建可复用的函数模板。通过部分应用来生成新的函数。这样可以更灵活地使用函数,并且可以方便的创建更专注于特定功能的函数。
简单的函数柯里化例子:


function add(x) {
return function(y) {
return x + y;
}
}

// 使用柯里化的add函数
var add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add5(7)); // 输出 12

封装一下


function curry(fn) {
return function curried(...args) {
if(args.length>=fn.length) {
return fn.apply(this,args)
} else {
return function(...moreArgs) {
return curried.apply(this,args.concat(moreArgs))
}
}
}
}

数组API的实现


forEach


Array.portotype.my_forEach = function(callback) {
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)
}
}

map


Array.portotype.my_map = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)&&res.push( callback(this[i],i,this))
}
return res
}

filter


Array.portotype.my_filter = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

前端模块化


问:你讲讲前端模块化吧
答:模块化的开发方式可以提高代码复用率,方便进行代码的管理,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。


问:模块化有哪几种标准?
答:目前流行的js模块化规范有CommonJS、AMD、CMD以及Es6的模块系统


问:ES Modules 和CommonJS的一些区别
答:
1.使用语法层面,CommonJs是通过modules.exports,exports导出,require导入;ES Modules则是export导出,import导入
2.CommonJs是运行时加载模块,EsModules是在静态编译期间就确定模块的依赖
3.EsModulse在编译期间会将所有import提升到顶部,CommonJs不会提升require
4.CommonJs导出是一个值拷贝,会对加载结果进行缓存,一但内部再修改这个值,则不会同步到外部。ESModule是导出的一个引用,内部修改可以同步到外部
5. CommonJs中顶层的this指向这个模块本身,而ESModule中顶层this指向undefined
6. CommonJS加载的是整个模块,将所有的接口全部加载进来,ESModule可以单独加载其中的某个接口


vue的数据双向绑定的原理


vue的响应式原理是采用‘发布-订阅’的设计模式结合object.defineProperty()劫持各个属性的getter和setter,在数据发生变动时通过调用Deo.notity函数发布订阅给观察者watcher,让其更新响应的视图。


虚拟dom


虚拟dom是用来表现真实dom结果的javaScript对象树,是构建在浏览器真实dom上的抽象层,虚拟dom是可以直接在内存中操作的,可以通过diff算法来新旧dom的差异,将最终变化应用到真实dom上


diff算法


diff算法又称虚拟Dom的周界算法,vue的diff算法是通过深度优先、先序遍历的方式进行的,它将前后两个虚拟Dom树进行逐层比较,当找到某一层不一样的节点时,停止下降,然后比较这些节点的子节点,当所有的子节点都完成比较之后,算法会由下至上进行回溯,此过程被称为执行patch操作。在执行patch操作时,Vue对于不同类型的节点的更新方式也不同,对于元素节点可以更新他的属性和子节点;对于文本节点,只能更新它的文本内容;对于每个子节点,如果key值相同,可以进行复用或者重新排序,或者将其他的节点移动到这个位置。


vue中nextTick的理解及作用


使用场景描述:更改一个数据,导致dom元素的width发生了更改,但又要获取这个更新后的dom元素的width,可以用nextTick
vue2 中的nextTick是在下次Dom更新循环之后执行回调函数,并且是作为vue实例的方法调用的


this.$nextTick(() => { // 组件的DOM已经更新完毕,可以进行相应操作 // ... });

Vue 3的nextTick作为一个独立的函数导入,返回一个Promise,并且可以直接传递回调函数作为参数。这些变化使得Vue 3中的nextTick更加灵活和易于使用。


// Vue 3 
import { nextTick } from 'vue';
nextTick(() => { // 在下次DOM更新循环之后执行 });

vue在实例挂载的过程中发生了什么?




  1. 实例化:首先,Vue.js会创建一个新的Vue实例。在这个过程中,Vue.js会设置实例的各种属性和方法,包括数据对象、计算属性、方法、指令等。




  2. 编译模板:Vue.js会将模板编译成渲染函数。模板就是包含Vue特定语法的HTML代码。编译过程中,Vue.js会解析模板中的指令(如v-if、v-for等)和插值表达式(如{{ message }}),并将它们转换为JavaScript代码。




  3. 创建虚拟DOM:渲染函数会被调用,生成一个虚拟DOM树。虚拟DOM是对真实DOM的轻量级表示,它可以更高效地处理DOM的更新。




  4. 挂载:最后,Vue.js会将虚拟DOM渲染为真实DOM,并将其挂载到指定的元素上。这个过程通常在调用vm.$mount()方法或者在实例化Vue时传入el选项后发生。




  5. 更新:当数据变化时,Vue.js会重新执行渲染函数,生成新的虚拟DOM,并与旧的虚拟DOM进行对比(这个过程称为diff)。然后,Vue.js会根据diff结果,以最小的代价更新真实DOM。




这个过程中还会触发一系列的生命周期钩子,如created、mounted等,开发者可以在这些钩子中执行自己的代码。


vue2中data是一个函数而不是对象的原因


data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染


vue2中给对象添加新属性界面页面不刷新


vue2是用Object.defineProperty实现数据响应式,而后面新增的属性,并没有通过Object.defineProperty设置成响应式数据,所以页面没变化,常用解决方式:



  • Vue.set()

  • Object.assign()

  • $forcecUpdated()


Vue SSR的实现原理


vue.js的ssR是一种在服务器上预渲染Vue.js应用程序的技术。



  1. 服务器接收请求:当服务器接收一个请求时,它会创建一个新的Vue实例。

  2. 创建渲染器:使用vue-server-renderer包创建一个渲染器。

  3. 渲染页面:服务器使用渲染器将Vue实例渲染为Html字符串。

  4. 发送响应:服务器将渲染后的Html字符串作为响应发送给客户端。

  5. 客户端接收响应:客户端接收到服务器的响应后,将HTML字符串解析为DOM并显示给用户。

  6. 激活(Hydration): Vue在客户端创建一个新的Vue实例,将其挂载到服务器收到的Dom上


keep-alive的使用


keep-alive的主要作用是缓存路由组件,以提高性能


<router-view v-slot="{ Component }">  
<keep-alive :include="permissionStore.keepAliveName">
<component :is="Component" :key="$route.path" />
</keep-alive>

</router-view>



  1. router-view是 Vue Router 的一个组件,用于渲染当前路由对应的组件。




  2. v-slot="{ Component }" 是一个插槽,用于获取当前路由对应的组件。




  3. keep-alive 是 Vue 的一个内置组件,用于缓存组件,避免重复渲染。




  4. :include="permissionStore.keepAliveName" 是 的一个属性,表示只有名称在 permissionStore.keepAliveName 中的组件会被缓存。




  5. 是一个动态组件,:is="Component" 表示组件的类型由 Component 决定,:key="$route.path" 表示每个路由路径对应一个唯一的组件实例。




Vue项目中有封装axios吗?主要是封装哪方面的?



  • 1.封装前需要和后端协商好一些约定,请求头,状态码,请求时间....

  • 2.设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

  • 3.移除重复的请求,如果请求在pending中,提示'操作太频繁,请稍后再试'

  • 4.用map结构根据相应状态码处理错误信息

  • 5.请求拦截,若headers中没有token的,移除请求

  • 6.响应拦截器,例如服务端返回的message中有'message',提示'请求超时,请刷新网页重试'

  • 7.请求方法的封装,封装get、post请求方法,使用起来更为方便


css预处理器


css预处理器扩充了css语言,增加了诸如变量、混合(mixin)、函数等功能,让css更易维护、方便。本质上。预处理是css的超集。包含一套自定义的语法及一个解析器,根据这些语法定义自己的样式规则,这些规则最终会通过解析器编译生成对应的css文件。


如何实现上拉加载


image.png
触底公式:


scrollTop + clientHeight >= scrollHeight

简单实现:


    let clientHeight = document.documentElement.clientHeight;//浏览器高度
let scrollHigiht = documnet.body.scrollHeight;//元素内容高度的度量,包括由于溢出导致的视图中不可见内容
let scrollTop = documnet.body.scrollTop; //滚动视窗的高度距离`window`顶部的距离
let distance = 50; //距离视窗还用50的时候,开始触发;

if ((scrollTop + clientHeight) >= (scrollHeight - distance)) {
console.log("开始加载数据");
}

如何实现下拉刷新


关于下拉刷新的原生实现,主要分成三步:



  1. 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;

  2. 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的3. translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值

  3. 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置。


封装和使用JSBrige




  1. 定义协议:首先,需要定义一种协议,用于约定H5页面与App之间的通信规则。这可以是一组自定义的URL Scheme或JavaScript函数。




  2. 注册事件监听:在H5页面中,通过JavaScript代码注册事件监听器,用于接收来自App的消息或回调。可以使用window.addEventListener或其他类似的方法来监听特定的事件。




  3. 发送消息给App:在H5页面中,通过调用JSBridge提供的方法,将消息发送给App。这可以是通过修改URL Scheme的方式,或者调用App提供的JavaScript接口。




  4. 处理App的消息或回调:在App原生代码中,通过监听URL Scheme或执行JavaScript代码的方式,接收来自H5页面的消息或回调。根据协议约定,处理相应的逻辑或调用相应的功能。




  5. 回调结果给H5页面:在App原生代码中,根据协议约定,将处理结果或回调信息发送回H5页面。可以通过修改URL Scheme的方式,或者调用H5页面中注册的JavaScript回调函数。




个人博客


耀耀切克闹 (yaoyaoqiekenao.com)


gitHub


DarknessZY (zhangyao) (github.com)


作者:耀耀切克闹灬
来源:juejin.cn/post/7291834381315719220
收起阅读 »

丈母娘说:有编制的不如搞编程的

10月17日百度世界大会召开,据说文心大模型又牛X了,综合水平相比GPT4毫不逊色,真是个让人激动的消息,国产大模型的进展可以说是日新月异,我这耳朵边一直响彻四个字:遥遥领先。 不过今天我关注的重点不是什么大模型,而是发布会上的一件趣事:相亲。这大模型和相亲有...
继续阅读 »

10月17日百度世界大会召开,据说文心大模型又牛X了,综合水平相比GPT4毫不逊色,真是个让人激动的消息,国产大模型的进展可以说是日新月异,我这耳朵边一直响彻四个字:遥遥领先。


不过今天我关注的重点不是什么大模型,而是发布会上的一件趣事:相亲。这大模型和相亲有什么关系呢?给大家说关系密切,各位男女光棍们一定要抓住这个机会。


话说在百度世界大会上出现了一位神秘的阿姨,她既不懂AI,也不懂编程,那她来干什么呢?这位阿姨拿着厚厚一叠的宣传页,见到长的有点像李彦宏的帅哥,就赶紧贴身向前,就着宣传页向对方一顿输出,热情开朗,情真意切,搞得小哥哥们都有点不好意思了。


这位阿姨在干什么呢?


原来这位阿姨是给自己的女儿来相亲的。阿姨的女儿是99年的,职业是小学老师,按说这个年龄、这个身份也应该不愁嫁吧。阿姨是怎么想的呐?


阿姨说,这可比那个什么相亲公园靠谱多了,都是高智商的人,都是搞AI的,就有编制的也不如搞编程的,这将来挣钱的一定是搞AI的。男怕入错行,女怕嫁错郎,这男人入对了行,将来挣钱源源不断!


图片


我想这位阿姨一定是受到了张雪峰老师的启发,张老师在之前的直播中就给女孩子的家长们建议过,孩子学习不好,报个计算机专业,但是进去的主要目的不是学计算机,而是找个聪明的小哥哥,以后的生活比较有着落,原因相信大家都懂,挣钱多呗!


记得以前相亲,丈母娘都是看你有没有房,还有人说中国的房价都是丈母娘搞上去的。现在房价涨不动了,以后可能还会跌跌不休,我想其中的一个原因可能是丈母娘的眼光变高了,钱财当然很重要,但是你脑子不够也不行啊,得能挣钱才行,否则就是坐吃山空,不得长久。


再看看各位网友对这件事的看法:



阿姨政策吃的透透的,风口看的准准的!


大妈打算盘的声音,我在合肥都听到了!


阿姨超有远见!


看人家妈妈的行动力,行动起来啊妈妈



图片图片


看到这里的各位男女光棍们,各位愁娶愁嫁的家长们,赶紧行动起来吧,每年甚至每周都有很多这样的技术大会,网上搜索一大堆,大会上有男有女,大部分都是高科技人才,将来前途和钱途都不可限量。多去看看,多去走走,说不定就能找到对眼的意中人,这个概率可是很高的,是经过丈母娘认可的。


最后,丈母娘也不是好糊弄的!大家一定要有真才实学。给同学们一些建议,打好基础,打好基础,技术不是东拼西凑就可以做好的,勿在浮沙建高台,容易倒;另外多实践多总结,多沟通多交流。


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

如何在Java项目中实现漂亮的日志输出

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧...
继续阅读 »

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧和建议。



image.png



1. 使用合适的日志框架



  Java有许多优秀的日志框架可供选择,如Log4j、Logback和java.util.logging等。选择一个适合你项目需求的日志框架是实现漂亮日志输出的第一步。这些框架提供了丰富的配置选项,可以帮助你控制日志的格式和输出方式。这里对几个日志框架做一下简单的介绍。


Log4j


  Log4j是一个Java日志处理的框架,用于在Java应用程序中处理日志记录。它提供了一种灵活的方式来记录日志信息,并允许开发者根据需要配置日志输出的格式和目标。


  在Log4j中,主要有三个组件:Logger、Appender和Layout。Logger用于记录日志信息,Appender用于定义日志的输出目标,例如控制台、文件、数据库等,Layout用于定义日志的输出格式。


  以下是一个简单的Log4j代码示例:


import org.apache.log4j.Logger;  

public class MyApp {
// 获取Logger实例
final static Logger logger = Logger.getLogger(MyApp.class);

public static void main(String[] args) {
// 记录不同级别的日志信息
logger.debug("Debugging information");
logger.info("Informational message");
logger.warn("Warning");
logger.error("Error occurred");
logger.fatal("Fatal error occurred");
}
}

  在这个示例中,我们首先导入了Logger类,然后通过Logger.getLogger(MyApp.class)获取了一个Logger实例。在main方法中,我们使用Logger实例记录了不同级别的日志信息,包括Debug、Info、Warn、Error和Fatal。


Logback


  Logback是Log4j的改进版本,是SLF4J(Simple Logging Facade for Java)下的一种日志实现。与Log4j相比,Logback具有更高的性能和更灵活的配置。


  Logback的组件包括Logger、Appender、Encoder、Layout和Filter,其中Logger是最常用的组件。Logger分为rootLogger和nestedLogger,rootLogger是所有Logger的根,nestedLogger则是rootLogger的子级。Logger之间有五个级别,从高到低依次为ERROR、WARN、INFO、DEBUG和TRACE,级别越高,日志信息越重要。


  以下是一个简单的Logback代码示例:


import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;

public class MyClass {
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void myMethod() {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

logging


  java.util.logging是Java平台的核心日志工具。


  java.util.logging由Logger、Handler、Filter、Formatter等类和接口组成。其中Logger是日志记录器,用于记录日志信息;Handler是处理器,用于处理日志信息;Filter是过滤器,用于过滤不需要记录的日志信息;Formatter是格式化器,用于格式化日志信息。


  这里介绍的日志框架,在项目当中运用的比较多的是Log4j、Logback,从基本的配置上没有太大的差异,大家也可以根据项目需求选择使用。



2. 定义清晰的日志级别



  在Java项目中,定义清晰的日志级别是非常重要的,以便在调试、监控和解决潜在问题时有效地记录和理解系统行为。下面是一些建议,可以帮助你定义清晰的日志级别:



  1. 了解常见的日志级别:Java中常见的日志级别包括DEBUG、INFO、WARN、ERROR和FATAL。每个级别都有特定的含义和用途,首先要了解这些级别的含义。

  2. 根据项目需求确定日志级别:在定义日志级别时,需要考虑项目的需求和目标。例如,对于一个简单的演示应用程序,可能不需要记录过多的调试信息。但对于一个复杂的业务系统,可能需要详细的调试信息来跟踪和解决潜在的问题。根据项目的重要性和规模来确定每个级别的日志信息是否必要。

  3. 默认级别设置:为项目设置一个默认的日志级别。这通常是INFO级别,用于记录系统的常规操作信息。

  4. 根据模块或功能设置日志级别:为每个模块或功能设置不同的日志级别。这有助于在特定部分出现问题时快速定位问题原因。例如,对于数据库模块,可以将其日志级别设置为DEBUG,以便记录详细的数据库操作信息。

  5. 日志级别继承:在一个日志级别下定义的日志信息,应该继承到其所有子级别中。这意味着,如果某个日志信息被设置为WARN级别,那么该信息应该同时出现在WARN、ERROR和FATAL日志中。

  6. 日志信息清晰明了:在记录日志信息时,要确保信息清晰明了,包含必要的细节。例如,对于错误信息,要包含错误类型、发生错误的方法和时间戳等信息。

  7. 日志轮转和清理:及时对日志进行轮转和清理,避免日志文件过大而影响系统性能。可以设置一个合适的大小限制或时间间隔,对旧的日志文件进行归档和清理。

  8. 培训开发人员:为开发人员提供关于如何使用日志系统的培训,确保他们了解如何记录适当的日志信息以及如何利用日志级别进行过滤。

  9. 参考最佳实践:可以参考一些关于日志编写的最佳实践指南,例如Log4j的官方文档,以获取更多关于如何定义清晰日志级别的建议。


  定义清晰的日志级别对于Java项目来说非常重要。通过了解常见的日志级别、根据项目需求确定级别、设置默认级别、按模块或功能划分级别、继承级别、记录清晰明了的日志信息、及时轮转和清理以及培训开发人员等措施,可以帮助你在项目中实现定义清晰、易于理解和使用的日志级别。



3. 格式化日志输出



  下面以Log4j为例,介绍如何格式化日志输出。


1,引入Log4j依赖


  在Maven项目中,可以在pom.xml文件中添加以下依赖:


<dependency>  
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.x.x</version>
</dependency>

2. 配置日志格式


  在log4j2.xml配置文件中,可以使用PatternLayout类来配置日志格式。例如,以下配置将日志输出为每行包含时间戳、日志级别、线程名和消息的格式:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  其中,%d表示时间戳,%t表示线程名,%-5level表示日志级别(使用五个字符的宽度),%logger{36}表示最长为36个字符的Logger名称,%msg表示消息。在配置文件中可以根据需要调整格式。


3. 在代码中使用Log4j记录日志


在Java代码中,可以使用以下语句记录日志:


import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;

public class MyClass {
private static final Logger logger = LogManager.getLogger(MyClass.class);

public static void main(String[] args) {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

  在输出结果中,可以看到每条日志信息都符合之前配置的格式。可以使用不同的配置文件来调整日志格式,以满足不同的需求。



4. 日志轮转和切割



  志切割和轮转在Log4j中主要通过两种策略实现:基于大小(Size-based)和基于日期时间(Time-based)。


1. 基于大小的日志切割和轮转


  这种策略是当日志文件达到指定大小时,会进行切割或轮转。例如,你可以设置当日志文件达到100MB时进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>

  在上述配置中,当app.log文件达到100MB时,它会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留20个这样的文件。


2. 基于日期时间的日志切割和轮转


  这种策略是当达到指定的日期时间时,进行日志切割或轮转。例如,你可以设置每天凌晨1点进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>

  在上述配置中,每天凌晨1点,app.log文件会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留30个这样的文件。


  注意:<DefaultRolloverStrategy max="20"/> 或 <TimeBasedTriggeringPolicy interval="1"/> 中的数字可以根据你的实际需要进行调整。



5. 日志过滤器(Filter)的使用



  Log4j中的过滤器(Filter)用于在日志事件发生之前对其进行一些条件判断,以决定是否接受该事件或者更改该事件。这可以让你根据特定的条件过滤日志输出,例如只打印错误级别以上的日志,或者根据线程ID、请求ID等过滤日志。


  在Log4j 2中,你可以通过配置文件(例如log4j2.xml)来为日志事件指定过滤器。以下是一个使用Log4j 2的XML配置文件中的过滤器的示例:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Filters>
<ThresholdFilter level="ERROR"/>
<MarkerFilter marker="FLOW" onMatch="DENY"/>
<MarkerFilter marker="EXCEPTION" onMatch="DENY"/>
</Filters>
</Console>
</Appenders>
<Loggers>
<Root level="all">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  在这个例子中,我们使用了三个过滤器:


  (1). ThresholdFilter只接受级别为ERROR或更高级别的日志事件。


  (2). 第一个MarkerFilter会拒绝任何带有"FLOW"标记的日志事件。


  (3). 第二个MarkerFilter会拒绝任何带有"EXCEPTION"标记的日志事件。


  另外,你还可以创建自定义的过滤器,只需实现org.apache.logging.log4j.core.filter.Filter接口即可。然后你可以在配置文件中通过指定类全名来使用你的过滤器。


  对于更复杂的过滤需求,你还可以使用Condition元素,它允许你使用Java代码来决定是否接受一个日志事件。不过,请注意,因为这可能会影响性能,所以应谨慎使用。


  下面是实际项目中打印的日志,大家可以根据项目的需求满足日志打印的需求。


image.png



总结



  通过选择合适的日志框架、定义清晰的日志级别、格式化日志输出、添加时间戳和线程信息、使用日志分级以及处理异常和堆栈跟踪,我们可以实现在Java项目中打印漂亮的日志。漂亮的日志输出不仅可以提高代码的可读性,还可以帮助我们更好地理解和跟踪代码的执行过程,从而提高开发效率和系统稳定性。


拓展阅读


# 35岁愿你我皆向阳而生


# 深入解读Docker的Union File System技术


# 说一说注解@Autowired @Resource @Reference使用场景


# 编写Dockerfile和构建自定义镜像的步骤与技巧


# 说一说Spring中的单例模式


# MySQL的EXPLAIN用法


# Spring的Transactional: 处理事务的强大工具


作者:mikezhu
来源:juejin.cn/post/7291675889381031990
收起阅读 »

HashMap扩容机制跟你的工作真的不相关吗?

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭 再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️ 那年 OK,书归正传,得益于本人工作态度良好...
继续阅读 »

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭



再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️


那年


OK,书归正传,得益于本人工作态度良好,同事和领导都给予了我很大的帮助,只记得那是18年的平常打工人的一天,我写了如下很多打工人都会写,甚至每天都在写的代码(当时的具体代码已经记不清了,现在大概模拟一下案发场景):


    /**
* 从Order对象中获取id属性并包装成List返回
*
* @param orderList Student列表
* @return idList
*/

public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>();
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


对没错,用Stream流可以一行代码解决这个问题,但当时受限于我们使用的JDK还是1.6和1.7,你懂得



我的直属领导看了我的代码后首先问我,你知道ArrayList初始化容量是多少吗?他是怎么扩容的?


我:。。。。。
img


这俩问题对现在的程序员来说兼职就是小菜一碟,不值一提,但对当时的我来说,可就有亿点难度了,之前面试之前依稀在那个博客上看别人写过,于是乎我就照着脑袋里模糊不清的知识点模棱两可的回答了这俩问题,emmm,


于是乎我领导就跟我说,既然你知道List容量不够会扩容,扩容会带来性能损耗(这个日后再细说,先说正事)那么你应该这么写,来避免它扩容呢?


    public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


千万不要小看这些细节哦



听君一席话,如听一席话,于是我悟了,


从那以后再有类似集合初始化的场景,明确知道容量的场景我都会初始化的时候传入构造参数,避免其扩容,无法知道确切容量的时候也会预估一下容量 尽可能的避免减少扩容次数。


去年


时间来到2022年,去年,我已经不是当年的那个懵懵懂懂愣头青了,坐我旁边的一个哥们(技术比我当年强多了去了),他写了一段初始化HashMap的代码也传入了一个初始容量,代码如下:


    public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}

img


敲黑板,重点来了,前面铺垫了那么多,就是为了说这事


历史惊奇在这一天重演,只不过负责问问题的是我


img


Q: 咳咳~HashMap的初始容量是16,放第几个个元素的时候会触发扩容呢(这题简单)


A: 元素个数超过16x0.75=12的时候进行扩容呗,扩容为16x2=32


Q: 既然容量为16,只能存12个元素,超过就会扩容,那么你写的new HashMap<>(orderList.size()) 这个能防止扩容吗?


A: emmm,不能


Q: 那初始化容量应该设置多少呢?


A: ……


Q: 16x0.75=12这个计算公式中, 初始容量变成未知假设为N 需存放的元素个数为20 Nx0.75=20N 是多少?(这大概就是经典的大学数学题吧)


A: 20➗0.75呗, 26.666 四舍五入27个, 设置容量为27,可以存放20个元素并且不触发扩容


img


所以正确的代码应该这么写: new HashMap<>((int) (orderList.size / 0.75 + 1))


别问为啥要+1,问就是因为小数转成int不会四舍五入直接舍弃小数点后的部分


一次轻松的对话就此结束


来看下大佬们是怎么写的


google的guava包 这是一个非常常用的java开发工具包,我从里面真的学到了很多(后续单独开篇文章记录一下)


//入口
HashMap<String, String> map= Maps.newHashMapWithExpectedSize(20);

public static <K extends @Nullable Object, V extends @Nullable Object>
HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap<>(capacity(expectedSize));
}

// 看这里看这里, 还考虑了一些其他的情况,专业!
static int capacity(int expectedSize) {
if (expectedSize < 3) {
checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
}
if (expectedSize < Ints.MAX_POWER_OF_TWO) {
// This is the calculation used in JDK8 to resize when a putAll
// happens; it seems to be the most conservative calculation we
// can make. 0.75 is the default load factor.
return (int) ((float) expectedSize / 0.75F + 1.0F);
}
return Integer.MAX_VALUE; // any large value
}

大佬写的代码就是专业! img


org.apache.curator包 无意之间发现的,实现有点意思


//这里写法一样
HashMap<String, String> map = Maps.newHashMapWithExpectedSize(20);


public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap(capacity(expectedSize));
}

//看这里 看这里 expectedSize + expectedSize / 3
static int capacity(int expectedSize) {
if (expectedSize < 3) {
CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
} else {
return expectedSize < 1073741824 ? expectedSize + expectedSize / 3 : Integer.MAX_VALUE;
}
}

expectedSize + expectedSize / 3 说实话第一次看到这段代码的时候还是有点懵的,wocc这是啥写法,后来用几个数值带入计算了一下,还真是那么回事 👍🏻👍🏻


Hutool工具包


//入口
HashMap<String, String> map = MapUtil.newHashMap(20);

public static <K, V> HashMap<K, V> newHashMap(int size) {
return newHashMap(size, false);
}

//看这里, 平平无奇,什么档次?代码跟我写的一样,😄
public static <K, V> HashMap<K, V> newHashMap(int size, boolean isOrder) {
int initialCapacity = (int)((float)size / 0.75F) + 1;
return (HashMap)(isOrder ? new LinkedHashMap(initialCapacity) : new HashMap(initialCapacity));
}


说实话这个实现相较前者来说就显得不那么细了,居然跟我写的一样。。。


image-20231019160132153

这件事情带来的思考


说起HashMap的知识点,晚上的文章博客简直满天飞,大家现在谁还不能说上几句,但是! 后来在我面试的很多初中级开发时,我问他们准备往Map中存放20个元素,初始化容量设置多少不会触发扩容 时,基本上很少有人能答上来,10个人当中差不多有一个能回答上来?为什么会这样呢? 明明这些人是懂的初始容量16,超过出初始容量的75%会触发扩容,反过来问一下就不会了~😒 这充分说明了,学习要融会贯通举一反三,要细!!!


那段代码现在怎么写


据说JDK都出到21了,最近没怎么关注过~
不过JDK8已经流行很久了,那段代码用JDK8应该这么写:



  • list


public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}

//一行代码搞定,简洁明了
public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toList());
}

通过StreamCollectors.toList()来返回一个崭新的List,难道就没人好奇他这个List创建的时候有没有指定容量呢?如过不指定,在上面说到的那些明确知道存放容量的场景里岂不是要白白的扩容耗费性能???


答案是:NO 我们看下来Collectors.toList()的实现


public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}

static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
private final Supplier<A> supplier;
private final BiConsumer<A, T> accumulator;
private final BinaryOperator<A> combiner;
private final Function<A, R> finisher;
private final Set<Characteristics> characteristics;

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Function<A,R> finisher,
Set<Characteristics> characteristics) {
this.supplier = supplier;
this.accumulator = accumulator;
this.combiner = combiner;
this.finisher = finisher;
this.characteristics = characteristics;
}

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Set<Characteristics> characteristics) {
this(supplier, accumulator, combiner, castingIdentity(), characteristics);
}

CollectorImplCollectors中的一个内部类,构造函数的第一个参数是Supplier<A> supplier这是一个函数式接口,就是说你得传给我一个实现,告诉我应该如何去创建一个集合,上面是这么传参的ArrayList::new, 这个写法其实就是new ArrayList(), 看到没!他并没有指定集合容量哦~~~


那么如果想提前指定好集合容量应该怎么写呢? 不卖关子了,直接贴代码了,写个B博客,真TM累死个人😌


public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toCollection(() -> new ArrayList<>(orderList.size())));
}

这就行了,看下Collectors.toCollection()的源码


public static <T, C extends Collection<T>>
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
return new CollectorImpl<>(collectionFactory, Collection<T>::add,
(r1, r2) -> { r1.addAll(r2); return r1; },
CH_ID);
}

这和Collectors.toList()基本上市一样的,只不过Collectors.toCollection()把如何创建集合的这个步骤抽象起来叫给我们开发者来个性化实现了,是不是又学到了一招~~~(#^.^#)



  • map


public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}


//这点破代码用Stream也是分分钟搞定
public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream().collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1));
}

和上面的List一样,这玩意初始化Map的时候也没有指定容量


public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

Map的创建也是通过一个函数式接口Supplier<M> mapSupplier定义的,传的参数是 HashMap::new,这也是一个方法引用,写法等于 new hashMap(), 想指定容量怎么办呢? 看代码


    public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream()
.collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1,
() -> new HashMap<>((int) (orderList.size() / 0.75 + 1))));
}

这个写法我们同样自己掌控如何创建需要的Map,容量自己定~


写在末尾


没啥要写的了,就这吧,累挺
img


作者:码塞顿开
来源:juejin.cn/post/7291828982558425142
收起阅读 »

01.你为什么需要学习K8S

前言 在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。 可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具Mesos和Docker Swarm,并将它们甩开了几十条街,成为了整个容...
继续阅读 »

前言



在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。


可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具MesosDocker Swarm,并将它们甩开了几十条街,成为了整个容器编排领域的龙头。



随着现在越来越多的企业把业务系统上云之后,大部分的服务都运行在Kubernetes环境中,可以说Kubernetes已经成为了云、运维和容器行业最炙手可热的工具,这也是为什么需要学习Kubernetes最重要的原因。


目前,AWS、Azure、Google、阿里云、腾讯云等主流公有云提供的是基于Kubernetes的容器服务。Rancher、CoreOS、IBM、Mirantis、Oracle、Red Hat、VMWare等无数厂商也在大力研发和推广基于Kubernetes的PaaS产品。


目前国内容器服务平台做的比较好的有腾讯云容器服务TKE阿里云容器服务ACK,它们都是基于K8S做的二开,有兴趣的读者可以自己了解和尝试使用。


K8S是什么?


K8S是单词Kubernetes的缩写,这个单词在古希腊语中是 [舵手] 的意思,之所以简称其为K8S,是因为'K'字母与'S'字母之间隔着八个单词,为了简便称呼,于是有了K8S这个简称。


K8S起初是Google内部的一个名为Borg的系统,据说Google有超过二十亿的容器运行在Borg上,在积累了十几年的经验之后,Google在2014年重写并开源了该项目,改名为Kubernetes


K8S在基于容器部署的方式上,提供了一个弹性分布式的框架,支持服务发现与负载均衡、存储、自动部署回滚、自动计算与调度、自动扩缩容等等一系列操作,目的是方便开发者不再需要关注服务运行细节,K8S能够自动进行容器与Pod调度、扩缩容、自动重建等等操作,保证服务尽可能健康的运行。


一句话来概括:K8S解放了开发者的双手,能够最大程度的让部署的服务健康运行,同时能够接入很多第三方工具(如服务监控、数据采集等等),满足开发者的定制化需求。


部署演变之路



传统部署时代


在互联网开发早期,开发者会在物理服务器上直接运行应用程序。以一个Go Web程序举例,很典型的一个部署方式是首先在本地编译好对应的二进制文件,之后上传到服务器,然后运行应用。


由于无法限制在物理服务器中运行的应用程序资源使用,因此会导致资源分配问题。例如,如果在同一台物理服务器上运行多个应用程序,则可能会出现一个应用程序占用大部分资源的情况,从而导致其他应用程序的性能下降。


虚拟化部署时代


为了解决上述问题,虚拟化技术被引入了。虚拟化技术允许你在单个物理服务器上运行多个虚拟机(VM)。虚拟化能够使应用程序在不同VM之间被彼此隔离,且能提高一定的安全性,因为一个应用程序的信息不能被另一应用程序随意访问。


虚拟化能够更好地利用物理服务器的资源,并且因为可以轻松地添加或者更新应用程序,而因此可以具有更高的扩缩容性,以及降低硬件成本等等的好处。通过虚拟化,可以将一组物力资源呈现为可丢弃的虚拟机集群。每个VM是一台完整的计算机,在虚拟化硬件之上运行所有的组件,包括自身的操作系统Guest OS


容器部署时代


容器类似于VM,但是具有更轻松的隔离特性,使得容器之间可以共享操作系统Host OS,并且容器不会像VM那样虚拟化硬件,例如打印机等等,只是提供一个服务的运行环境。



通常一台物理机只能运行十几或者数十个VM,但是可以启动成千上万的容器。因此,容器和VM比起来是更加轻量级的,且具有和VM一样的特性:每个容器都具有自己的文件系统、CPU、内存、进程空间等。


我们可以简单理解为:一个VM已经是一台完整的计算机了,而容器只是提供了一个服务能够运行的所有环境。


同时,因为容器与基础架构分离,因此可以跨云和OS发行版本进行移植。


容器部署具有以下优势



  • 敏捷部署:比起VM镜像,提高了容器镜像创建的简便性和效率。

  • DEVOPS:由于镜像的不可变性,可以通过快速简单的回滚,提供可靠并且频繁的容器镜像构建和部署。

  • 开发与运维的隔离:在构建、发布的时候创建应用程序容器镜像,而不是在部署的时候,从而将应用程序和基础架构分离。

  • 松耦合:符合微服务架构思想,应用程序被分解成一个个小服务运行在不同的容器中,可以动态部署和管理。

  • 软件/硬件层面隔离:通过namespace实现操作系统层面的隔离,如隔离不同容器之间的文件系统、进程系统等等;通过cgroup实现硬件层面的隔离,提供物理资源上的隔离,避免某些容器占用过多的物理资源CPU、Memory、IO影响到其他容器中的服务质量。


容器时代之后:Serveless


容器阶段之后,虚拟化仍然还在不断演化和衍生,产生了Serveless这个概念。


Serveless英文直译过来的意思是无服务器,这不代表着它真的不需要服务器,而是说服务器对用户不可见了,服务器的维护、管理、资源分配等操作由平台开发商自行维护。一个Serveless很经典的实现就是云函数,即最近火热的FAAS(Function As A Service),函数即服务。


Serveless并不是一个框架或者工具,它本质上是一种软件架构思想,即:用户无需关注应用服务运行的底层资源,比如CPU、Memory、IO的状况,只需要关注自身的业务开发。


Serveless具有以下特点



  • 无穷弹性计算能力:服务应该做到根据请求数量自动水平扩容实例,并且平台开发商应该提供无限的扩容能力。

  • 无需服务器:不需要申请和运维服务器。

  • 开箱即用:无需做任何适配,用户只需要关注自身业务开发,并且能够做到精确的按量计费。


强大的K8S


想像一个场景,假设我们现在把一个微服务架构的程序部署在成百上千个容器上,这些容器分部在不同的机器上,这个时候管理这些容器是一件非常让人头疼的事情。


让我们想想管理这些容器可能会碰到的问题,例如:



  1. 某个容器发生故障,这个时候我们是不是该启动另一个容器?

  2. 某台机器负载过高,那么我们之后的容器是不是不能部署在这台机器上?

  3. 某个服务请求量突增,我们是不是应该多部署几个运行该服务的容器?

  4. 如果某些容器之间需要相互配合怎么办?比如容器A需要容器B的资源,所以容器A一定要在容器B之后运行。

  5. 运行多个容器时,我怎么做到它们的运行结果是原子性的?即要么全部成功,或者全部失败。亦或者如果某一个容器失败,我能够不断重启这个容器以达到我的预期状态。


以上问题,都可以交给K8S来解决,它提供了一系列的功能来帮助我们轻松管理和编排容器,以达到我们的预期状态,同时因为它本身也是一个分布式高可用的组件,所以无需担心K8S出问题。


K8S官方文档这么描述它的功能:



  • 服务发现和负载均衡 Kubernetes 可以使用 DNS 名称或自己的 IP 地址来暴露容器。 如果进入容器的流量很大, Kubernetes 可以负载均衡并分配网络流量,从而使部署稳定。

  • 存储编排 Kubernetes 允许你自动挂载你选择的存储系统,例如本地存储、公共云提供商等。

  • 自动部署和回滚 你可以使用 Kubernetes 描述已部署容器的所需状态, 它可以以受控的速率将实际状态更改为期望状态。 例如,你可以自动化 Kubernetes 来为你的部署创建新容器, 删除现有容器并将它们的所有资源用于新容器。

  • 自动完成装箱计算 你为 Kubernetes 提供许多节点组成的集群,在这个集群上运行容器化的任务。 你告诉 Kubernetes 每个容器需要多少 CPU 和内存 (RAM)。 Kubernetes 可以将这些容器按实际情况调度到你的节点上,以最佳方式利用你的资源。

  • 自我修复 Kubernetes 将重新启动失败的容器、替换容器、杀死不响应用户定义的运行状况检查的容器, 并且在准备好服务之前不将其通告给客户端。

  • 密钥与配置管理 Kubernetes 允许你存储和管理敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 你可以在不重建容器镜像的情况下部署和更新密钥和应用程序配置,也无需在堆栈配置中暴露密钥


什么人需要学习K8S


运维/运开工程师


随着部署模式的演变,现在企业的应用几乎都以容器的方式在开发、测试、生产环境中运行。掌握基于K8S的容器编排工具的运维、开发能力将成为运维/运开工程师的核心竞争力。


软件开发人员


随着开发模式的演变,基于容器的微服务架构已经成为了开发应用首选的架构,而K8S是运行微服务应用的理想平台,市场会需要一批掌握K8S的软件开发人员。


GO开发人员


GO高级开发基本只有两个方向:高级服务端开发工程师和云原生工程师,其中云原生岗位会比高级服务端开发工程师更多。


这里的云原生主要是做DockerPrometheusKubernetes等云原生工具方向等等开发,这也是因为CNCF基金会的一系列产品基本都是使用Go语言写的,Go开发工程师相比于其他人员拥有天然优势。


总结


到这里,每天十分钟轻松入门K8S的01篇: 《你为什么需要学习K8S就结束了》 ,后续会持续更新相关文章,带大家了解K8S架构、K8S组件、如何搭建K8S集群、各种K8S对象、K8S高级特性、K8S-API等等内容。


欢迎大家点赞、收藏、催更~


作者:安妮的心动录
来源:juejin.cn/post/7291513540025434169
收起阅读 »

做个清醒的程序员之要不要做程序员

阅读时长约9.6分钟;共计2411个字 作为这个系列的正篇开端,我们聊一个很应景的话题——要不要做程序员。 说到这个话题,就要把时间往前推到高考后填报志愿的时刻,那个时刻可以说是大部分人首次面对职业规划问题。 在很久之前,我写过一篇关于填报志愿的文章,不妨先来...
继续阅读 »

阅读时长约9.6分钟;共计2411个字


作为这个系列的正篇开端,我们聊一个很应景的话题——要不要做程序员。


说到这个话题,就要把时间往前推到高考后填报志愿的时刻,那个时刻可以说是大部分人首次面对职业规划问题。


在很久之前,我写过一篇关于填报志愿的文章,不妨先来回顾一下。


在那篇文章中,我指出填报志愿的优先原则是城市首选,院校次之,专业末位。


如此选择是有理由的,城市意味着圈子,意味着机会,意味着眼界。院校意味着平均水准,意味着知名度,意味着基本盘。专业意味着兴趣,意味着专长,意味着就业。


虽然我把专业放在末尾,但这并不意味着个人的兴趣不重要。而是当我们有了圈子,有了机会,有了平均水准之后,可以通过辅修或者转专业来进修自己喜欢的知识。


举个例子,比如一个人的高考成绩可以选择一所211或985院校,但专业只能服从调剂。也可以选一所普通院校,专业随便选。这二者之间,其实我更推荐前者。通过转专业或者辅修,最终可以收获知名院校的自己喜欢的专业的毕-业-证书。而后者最终只能收获普通院校的,自己喜欢的专业的毕-业-证书。如果有这样两份不同的学历放在HR面前,如果你是HR,你也会更倾向于选择前者吧?


这就是为啥选专业要放在选学校后面的原因。


再说城市与学校,这个就很好理解了。毕业后,大部分人都会选择参加工作。如果去的城市就业环境不好,机会少,行业内的大佬也不在此地聚集。就算个人再努力,和那些经常和行业大牛接触的人相比,日子久了,差距就会逐渐拉大。这是眼界、格局的差异,不是单纯的能力就可以弥补的。


简短地回顾完旧文章,我们把话题拉回来。就是要不要走软件开发这条路呢?


诚然,在这个问题上,我曾经没有丝毫犹豫,因为我根本就没想过甚至还有点抵触走这条路。


我最初的想法其实是做设计,图形图像方面,或是做视频剪辑。后来又想着做网络工程师,自学了一段时间的思科认证。直到后来快毕业的时候,有培训机构的讲师来做宣讲会,我稀里糊涂地就上了Android App开发这条船。也许是运气爆棚,我还真的挺适合走这条路。


但话说回来,为什么我会在大学期间对自己的未来有那么多的不确定呢?为什么不能坚定地走一条路呢?因为这个专业就不适合我,从一开始报得就有问题。


我填报志愿的时候是师范大学的信息工程,我依稀记得自己就是冲着这个名字选的,没怎么看都有哪些学科。根本就没料到会学什么单片机、电路原理、汇编语言之类的。这些我完全不感兴趣,学习成绩自然也很一般。虽然狗屎运一般地还拿了一次奖学金,但要说心里话,那就是:“这TM学的都是什么玩意”。我这个专业就一个班,而且是全学校唯一一个工科。我有时候就在想,我为啥报了这个专业,以至于荒废了四年大部分的时间。


而且我总共没怎么挂科,然而C语言挂了。所以当时的我怎么也想不到自己会做软件开发,其实这也注定了我也许做不到金字塔顶的那一小撮人。


所以,我特别希望今年的考生,特别是看到我这篇文章的考生,报志愿的时候一定要清醒一些,别像我似的。没有目标感的日子,真的不好过。


另一方面,也是我想表达的重点,就是如果我喜欢做的事情,不挣钱,或是就业前景非常不好,怎么办?


诚然,我当时报这个专业,或多或少是因为这个世界未来的时代将会是信息时代,这一点是毋庸置疑的。没错,选学校、选专业时,考虑的一个因素就是就业。但我认为,就业确实该考虑,但完全不用以它为导向。更多的,还是看个人的擅长领域,只要不是特别离谱就行。


举个例子,小X不喜欢编程,但迫于就业,想多挣些钱,走了编程这条路。刚开始的时候还不错,薪水在同学圈里不算低,自己也因为实现了多挣些钱的目的而开心。但随着时间的推移,他发现薪水的涨幅变慢了。更要命的是,由于自己根本就不喜欢编程,甚至会抵触工作。总是想:“要不是为了钱,老子早就辞职了”。终于有一天,遇到裁员潮。等到他在出去求职的时候,发现同龄人比自己强好多,自己在职场上几乎没什么竞争力。再加上年龄增大,薪酬高的能力够不上,薪酬低的不想去。陷入非常尴尬的境地。


另一个人,小Y,特别喜欢编程。第一份工作的工资或许没有小X高,但他干得很开心,因为他喜欢他的工作内容。而人一旦从事自己真心喜欢的事,就会变得非常积极主动。所以薪资很快就涨到了和小X差不多的水平,但他依然还是很积极地工作。随着他的薪水不断增多,生活水平慢慢地越来越好,他能在更舒适的环境中工作和学习。后来他发现,金钱对他来说不是第一要务,实现人生理想才是。于是他更加积极,甚至把自己所学分享给他人,决定做个对社会有贡献的人。


你看,这就是喜欢和不喜欢的区别,这里面的小Y其实就是我。


发自内心的喜欢,是工作积极主动的重要条件之一。对于真正喜欢的事业,做起来是会非常开心,非常投入,甚至还不觉得累,甚至还是不计回报的。在这种情形下,没有理由做不好。既然能做好,必然就会受到公司的青睐,不用为找不到工作发愁,从而让赚钱成为顺便的事儿。


所以,我的观点,在决定要不要走软件开发者这条路之前,不妨问问自己的内心:我真的喜欢这个行业吗?我真的具备这个行业从业者应有的素养:强大的自学能力、工作中的自律、缜密的逻辑等等吗?我愿意为了可能的加班,牺牲休闲时光吗?我愿意熬夜发版,牺牲睡眠吗?我愿意承担有可能秃如其来的迷人发型吗?……


但是,如果你和这个行业优秀的前辈们那样,希望用键盘,生产那些改善人们生活乃至改变世界的产品;是终身学习者,对新技术、新领域保持好奇;务实,不相信道听途说,善于用实践来检验真理;能和难题死磕到底……那么,非常欢迎你,成为我们的同行。


当然,一旦做了选择,那就没什么可说的。不再犹豫,风雨兼程。十余年的工作经验教会我一个朴实、简单却有奇效的道理——坚持。在坚持面前,一切困难都将不再可怕。而能坚持的人,便是手持利刃的勇士,必能披荆斩棘。


好了,说到这,就有点鸡汤的意味了,我就不给大家打鸡血了。


重复一下重点:走,抑或是不走软件开发这条路。要充分考虑自己的兴趣、擅长以及个性,切勿只考虑就业。况且,四年后的事,谁都说不准。


作者:萧文翰
来源:juejin.cn/post/7217621436512469052
收起阅读 »

很多人找不到人生的意义,但这不妨碍他们快乐的度过一生

”愿世间没有肿瘤,没有疾病,没有痛苦“ 朋友圈里前同事的一条消息惊醒了我,震惊的情绪驱散……了睡前的疲惫,上一次有这种情绪是听到同学的噩耗~ 。 每当悲伤的情绪涌上心头,生死之间的恐惧与震惊会让我禁不住的思考我现在的人生是不是有意义的人生。我会想人生的意义在...
继续阅读 »

”愿世间没有肿瘤,没有疾病,没有痛苦“



朋友圈里前同事的一条消息惊醒了我,震惊的情绪驱散……了睡前的疲惫,上一次有这种情绪是听到同学的噩耗~ 。


每当悲伤的情绪涌上心头,生死之间的恐惧与震惊会让我禁不住的思考我现在的人生是不是有意义的人生。我会想人生的意义在哪里?努力学习,努力拼搏的意义在哪里?仿佛一切努力都不值得~ 在死亡面前一切努力都不值得,反而是笑话。


如果,我是说如果,知道自己死亡的那一天是哪一天,我相信很多人会有另一种不同的活法。


每个人的命运掌握在不同的人手里,有的人掌握在父母手里,有的人掌握在孩子手里,有的人掌握在死神手里,有的人掌握在病魔手里,甚至有的人的命运掌握在酒后驾车司机的手里。


五年前的冬天,我刚毕业一年多,在公司里如鱼得水,每天的状态元气满满,业余时间还会搞开源项目。那个时候还没有体会到北漂的孤独,更几年后,看不到未来的时候对未来的绝望。


给我留下深刻印象的是,那年冬天回到老家,让我第一次感受到,好好活着是一件很难的事。


“林涛, 死了”。四个字就好像晴天霹雳一样在脑袋上炸开。我不记得我爸说话时的语气和表情,我只记得震惊和恐惧的情绪在我脑子里爆炸,好像有人用手掐着我的脖子,我呆住了。


林涛(化名)


愣了一会,才听见我爸说,“林涛出车祸了,和朋友去喝酒,喝完酒一起开车去KTV,路上出事儿的,他坐副驾驶,没系安全带,被甩出去了。”


林涛是我十几年前的发小,那时候天天腻在一起玩,后来渐渐不联系了。偶尔在老家见到,也只是打个招呼……


以后的几年里,时不时的会想起这件事。我第一次意思到,在死神眼里,人人平等,包括90后。


我在想,死亡是我们这个年纪该承受的吗?


如今朋友圈里的消息又让我想到这一切,我又开始思考,人生的意义在哪里?史铁生说,死是一件不必急于求成的事。是啊,任何一个健康的人都不会急于求成这件事。时不时出现的死神仿佛在向我们说:你命由天不由你。


这种情绪时不时的困扰我,每次都会有几天,过几天我又会有 “我命由我不由天”的感觉。没错,过几天就会支棱起来。


我也在想,为什么能支棱起来?主要是命运之神没有狠狠地捶打我。除此之外,我想应该有对美好未来的期待。


当我问老婆,你人生的意义是什么?


“为什么要问人生的意义?” 我老婆反问我。“问这个问题有什么意义,我只想和你快快乐乐的过一辈子。”


是我又矫情了……


我们不再是为赋新词强说愁的少年,我们已经长大,生活工作中有太多的酸甜苦辣,与其再去寻找人生的意义,不如静下心来,想想自己还有哪些人生的愿望,人生的牵挂。


人生苦短,及时行乐。


与其追寻意义,不如快快乐乐的过好每一天。


作者:他是程序员
来源:juejin.cn/post/7291680355798286347
收起阅读 »

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

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

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


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


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



一、直播市场的长尾用户


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


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


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


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


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


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


二、快进快出的用人理念


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


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


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



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


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


三、反脆弱的商业形式


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


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


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


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

RTX 4090也被禁售了?

游戏也不能玩了? 谁也没有想到,美国针对 AI 大模型技术卡脖子的争端,竟然砍到了玩家头上。 本周二,美国商务部放出最严对华出口管制规定,H800 等关键 AI 加速器成为制裁的焦点,然而消息曝出还没几个小时,人们发现各大电商平台上的高端消费级显卡 GeFo...
继续阅读 »

游戏也不能玩了?



谁也没有想到,美国针对 AI 大模型技术卡脖子的争端,竟然砍到了玩家头上。


本周二,美国商务部放出最严对华出口管制规定,H800 等关键 AI 加速器成为制裁的焦点,然而消息曝出还没几个小时,人们发现各大电商平台上的高端消费级显卡 GeForce RTX 4090 也下架了。


此前一系列对于 AI 芯片的限制已经让人愤怒,这次美国对消费级产品的打击直接影响到了更多人,事件瞬间登上了热搜。


很多人一觉醒来就在问:发生什么事了?



突如其来的消息引发了人们的讨论。游戏圈的人纷纷表示,没想到玩个游戏也能被美国制裁。也有玩生成式 AI 的人说,本来还想攒钱买块显卡跑 AI 画图,现在也不用等了。


但一纸蛮横无理的「禁令」并不能让生活停止,冷静下来之后,很多人进行了分析。首先,更严格的限制会让国内火热的生成式 AI 领域受到影响。


有评论解读认为,按照美国商务部的新规来看,以后出口芯片都要按计算器算一下,芯片的总体性能和性能密度是限制点,因此 RTX 4080 和 AMD 的 7900 XTX 也是有被禁风险的。 



也有人表示,不赚钱让出市场是不可能的,看上次 H800 的做法,英伟达马上就要出 4080Ti 了。



就在我们在计算什么游戏必须要用 4090 才能跑得顺畅的时候,这场闹剧很快又发展到了新的阶段。


禁了,但没完全禁:美商务部澄清


昨天,美国商务部工业和安全局(BIS)更新对华出口管制规定,进一步收紧 AI 芯片等领域的限制。新规显示,此次限制的核心对象是先进计算半导体、半导体制造设备和超级计算机项目,更加严格地限制了中国购买重要的高端芯片。


仔细解读这份新文件可以发现,新规则修改了需要受限的高级芯片的认定规则(参数)。比如删除了「互连带宽」作为识别受限芯片的参数,还设置了一个新的「性能密度阈值」(这个标准更为变通和灵活)来作为参数。



美国政府高级官员说,如果数据中心芯片超过了去年 10 月设定的「性能门槛」,或者超过了以每平方毫米计算的新的「性能密度门槛」基准,美国将限制数据中心芯片的出口。想要向中国或其他禁运地区出口芯片的公司必须通知美国政府。


更新后的禁令还将更多公司列入到「实体清单」,包括两大国产 GPU 显卡厂商摩尔线程、壁仞。


另外在新规定中,香港和澳门地区也在管制区域的范围内。


英伟达在 2022 年 9 月已被禁止向中国出售 A100、H100 等高端 AI 加速器。在当时,英伟达立即设计出了效能、互联性能稍差,但相比前一代提升明显的 A800、H800,作为「特供」中国的替代品。然而在更严格的限制之下,这些产品也被列为禁止出口。


这种管制对于国内科技公司和大模型前沿技术发展的影响,目前还难以分析,但事件的另一个利益相关方英伟达不可避免地再次陷入了被动。


作为上市公司,英伟达立刻提交给美国证监会(SEC)一份报告,主要声明了以下几点:这些限制适用该公司的 A100、A800、H100、H800、L40、L40S 和 RTX 4090 芯片,同时也影响了与这些芯片一起销售的整个系统,包括 DGX 和 HGX 系统。


另外,这些限制可能会损害英伟达按时完成新产品开发的能力。



公告地址:http://www.sec.gov/Archives/ed…


英伟达自己说了,这些你叫得上名的芯片都会受到影响,未来的计划也被打乱了。消息一出,英伟达股价遭遇了大幅缩水,AMD、英特尔也随之下跌,券商也纷纷调低了对于此类公司股价的未来预期。


与此同时,我们看到了在国内的电商平台,RTX 4090 纷纷下架的情景。


消费级显卡被列在了先进计算项目的管制名单里,这并不像是一件正常的事,美国商务部很快对 ECCN 3A090 的高科技出口管制政策做出了澄清。


关于该政策中对 NVIDIA GeForce RTX 4090 显卡的禁令,美国商务部允许对消费性应用的芯片进行出口豁免。不过这里也有争议,并没有明确指出是否包括 RTX 4090。



文件来源:http://www.bis.doc.gov/index.php/d…


这意味着 GeForce RTX 4090 显卡可以在中国(包括香港和澳门)的消费市场进行零售,但不允许将 RTX 4090 芯片用于商业和生产用途。


考虑到目前主流厂商的显卡成品均不是在大陆组装的,新限制对于消费级显卡的影响似乎可以认为是几乎没有。所以此处把问题归于黄牛囤货,是不无道理的。


RTX 4090「算力过高」,5090 怎么办?


所以作为一个玩家,我们就不用担心显卡被禁了,但以后怎么办?


自去年 9 月黄仁勋在 GTC 大会上推出 RTX40 系显卡,已经过去了一年多时间。RTX 4090 相较于 3090Ti 性能翻了一倍,光线追踪性能则提升了 4 倍。



不过,12999 元的起售价似乎让 RTX 4090 在市场上有点卖不动,所以有人调侃称此次 RTX 4090 可能被禁售的传闻是为了用涨价折抵销量的伎俩。


暂且不说被澄清可以消费使用的 RTX 4090,很多人更担忧的是,近来一直爆料不断的下一代旗舰机显卡 RTX 5090 未来会不会步其「前辈」的后尘呢?


据此前消息,RTX 5090 的整体性能会比 4090 提升 70%,其中:




  • CUDA 内核数增加 50%,达到 24576 个




  • 内存带宽增加 52%




  • 缓存增加 78%




  • 频率提高 15%




显然,RTX 5090 的算力很强,强到让人担心次一级的 5080 是不是也在美国商务部那个「算力密度」的范围内。毕竟黄仁勋可是说出过自家芯片每代性能都翻倍这种话的。



图源:videocardz.com/newz/nvidia…


目前看来对于出口的限制仅限数据中心使用的产品,纯粹的民用消费级产品则不受影响。


但不到一年的时间就已经两次修改,以后不好说 —— 不过禁售大众商品完全就是自损一千给助攻的行为,拱手让出消费级市场,是英伟达、AMD 等芯片公司不想看到的事。


尤其对于英伟达这个在中国拥有巨量需求的 GPU 厂商,他肯定不希望因为可能的禁售失掉占其全球营收近一半的中国市场(仅就 2023 财年来说)。未来对于国际芯片厂商和国内科技公司来说,日子可能会更不好过。


从另一个角度看,美国对于高端 AI 加速器的限制,让我们对于发展自身硬件技术有了更强的紧迫感。


或许在限制不断加码的背景之下,会有国内的芯片厂商把握住机遇。


参考内容:


http://www.sec.gov/ix?doc=/Arc…


http://www.zhihu.com/question/62…


作者:机器之心
来源:juejin.cn/post/7291672829135765513
收起阅读 »

因为月薪过高,我的工资发放失败了。。。

0x0. 剧情概要 码农卫师傅,在忙碌地写了一个月 bug 后,迟迟未能收到工资,紧接着又经历了20元巨款的不翼而飞。这究竟是道德的沦丧,还是人性的扭曲?欢迎收看第996期《走近科学之消失的血汗钱》。 对打工人来说,最重要的东西是工资,如果没工资,谁愿意打工?...
继续阅读 »

0x0. 剧情概要


码农卫师傅,在忙碌地写了一个月 bug 后,迟迟未能收到工资,紧接着又经历了20元巨款的不翼而飞。这究竟是道德的沦丧,还是人性的扭曲?欢迎收看第996期《走近科学之消失的血汗钱》。


对打工人来说,最重要的东西是工资,如果没工资,谁愿意打工?

至于你干不干,我反正是不干,我只是一个脱离了高级趣味的打工仔,眼里只有低俗的钱。上班不图钱,图啥?图老板画的饼来填饱肚子吗?亦或是图理想和未来?我特么还图小鹏特斯拉呢。

每月8号,是我司的发薪日,这次,我能如期等来工资,只等来了财务的通知,告诉我工资发放失败了。真是活久见,小刀揦屁股,开了眼了。

就这样,工作11余年,我第一次走上了「讨薪」之路。在经历了一波三折后,终于在3天后拿到工钱了。那么,为啥工资会发放失败呢?背后的原因令人暖心:

因为我工资太高了,被银行拦截了

是有点搞笑,你可能不信,但你还是要相信。反正事情就是这么个事情,情况就是这么个情况。

这篇文章主要记录「讨薪」的过程,以及一些碎碎念。对废话没兴趣的老哥,可以先行离开了。

0x1. 案发经过

过去10年,我的工资卡一直都是招行的,入账有短信提醒,因此,我从未主动查过工资是否到账。现在的公司用中国银行,短信提醒居然要收费,没钱,不开。

我的房贷还款日也是8号,我都是等工资到账后,再转账到房贷卡。但是,工资到账时间不固定,我又懒得去查询,为了避免错过还房贷,就设置了个每月8号重复的日历事件,17点提醒我查工资并转账。

时间来到9月8日,这天是个星期五,17点,手机准时响起,打开中行 APP,余额为0。虽然有点奇怪,但也没放在心上,毕竟腾讯也有过晚上10点多才发工资的情况。

半个多小时后,收到财务的消息,告诉我因为工资卡上没钱,导致工资发放失败了,需要转点钱过去,等银行处理后重新发送工资。不理解,余额为0跟收工资有啥关系?财务表示这是银行的口径,可能是卡被冻结了,以后都需要保持卡上有钱才能发工资。

确实,我的工资卡余额常年为0,个人习惯的原因,每次工资到账后,我都全额转到招行卡里消费用。

现在,死循环了,因为没收到工资,所以余额为0;又因为余额为0,导致收不到工资。难怪我同事说:

月光族,连工资都不配收到了

虽然觉得扯淡,我还是从招行转了20元到中行。很快啊,招行提示转账成功,也扣款了。此时,更离奇的事情发生了,中行 APP 依然显示余额为0,也没有任何异常提示,多次刷新后,涛声依旧。

这就超出我的认知了,人不能赚到自己认知以外的钱,这没毛病,但是,你也不能把我认知以内的钱卷走啊。

我得反思下了,为什么别人都能收到工资,就我收不到?先找找自己的原因,去年从帝都到霸都,从大厂到小厂,从组长到小兵,工资断崖式下跌,年终奖比 HR 的承诺少了一半。现在,一年多过去了,工资一毛没涨,公司股价跌了33%,我到底有没有认真工作?


WTF! 现在是属于我的工资和转账没收到,我特么反思个鸡毛,赶紧去找银行要钱。

0x2. 出师未捷

致电中行客服,在听了一堆电脑播放的废话后,终于接通人工客服,告诉我需要设置电话银行密码才能继续。设置完成后,回不到人工客服了。。。

重新拨打,又听了一遍废话,跟客服重述了一遍问题。他帮我查了一下,耗时仅2分钟,告诉我卡片一切正常,也没有查到那20元的转账记录。如果想继续追究的话,带上身-份-证,人肉前往柜台办理即可。

Good idea,我咋就没想到呢?我想他可能是忘了,此时是18:15,银行已经关门1个多小时了。。。

周六,银行错峰休息,开户行不上班。周日早上,前往银行,取号,7个人排队,2个窗口。前面的大爷大妈基本都是存钱取钱,挺慢的,等了近半个小时。

窗口1,跟工作人员描述了一遍我的问题,只见他一顿操作猛如虎,然后轻声细语(此处为贬义词,对方声音小到几乎听不见)地告诉我,银行卡没问题,一切正常。而且,近期没有交易记录,最近的一笔还是半个多月前的。让我再转账1元试试,从之,从招行转了1元。

我艹屮艸芔茻,闹鬼了,这次成功收到了。中行 APP 上赫然显示着余额1元,打脸了,尼玛,为什么?!


我问他为啥工资和周五转的20元都没能收到?依然答复说卡是正常的,顺便给我抛出了两个问题:

  1. 你们公司是不是没发工资?
  2. 你是不是银行卡号输错了?

大哥,你这是在侮辱我的智商啊,把我给逗笑了。首先,我司虽然不如贵行财大气粗,但按时发工资还是能保证的。其次,卡号都保存在招行的通讯录,根本不需要输入,而且卡号错误,根本就转不过去啊。

我憋着一肚子气,告诉他,都不可能。所以,我这卡到底是咋了?我要做什么才能确保工资顺利入账?答复曰,那就不知道了,而且你刚才转账也成功了,要不你再问问招行?

行吧,居然连柜台都解决不了,也明显感觉他的敷衍。临走前,我向他表达了我的不爽:

你们中行真的是辣鸡

当然,这跟他无关,他跟我一样,只是个打工仔而已,没必要跟我掰扯,就当我在放屁了,头都没抬一下。

0x3. 卷土重来

问题没有解决,我只能骂骂咧咧地走出银行,坐上心爱的小摩托,打算用心中的怒火来发动它。

虽然我知道问题肯定在中行,还是抱着死马当活马医的想法,打开招行 APP,看看能否发现什么蛛丝马迹。又转了1元过去,又成功了,但20元依然没有退回。

四处点了点,在转账的进度查询里,发现了这么一段:


有了这个报文 ID,中行不能再甩锅给招行了吧?山穷水尽疑无路,柳暗花明又一村,古人诚不我欺,遂折返银行。

再次取号,这次变成12个人等待了。为了减少等待时间,找到大堂经理,告知来意,出示报文号,问能否帮忙查下。答曰查不了,需要到窗口,在报文系统中查询。

只能老实回去排队,百无聊赖地玩着手机,仿佛多次听到「工资」一词,便抬头四处看看。

窗口2,正在办业务是个小伙,卡一直正常使用,突然就收不到转账了。我赶紧竖起耳朵,仔细「偷听」了起来,无巧不成书,他碰到了跟我一样的问题。为他办业务的是位小姑娘,明显感觉比窗口1的大哥有耐心,至少花的时间比我长。

希望再次冉起,我这急性子,没等小伙的业务办完,立刻跑到窗口2,一顿狂吠:

我也是工资没收到,但是旁边的窗口告诉我说查不到原因,肯定是你们银行对我的卡做了什么,bala,bala...

喷完后,感觉自己有点失态了,身边多了一圈不明真相的吃瓜群众。冷静了下,我说我有招行的报文 ID,能否定位问题?

此时,从窗口3探出了一个头,说查不了,可以等小伙的业务办完后,让窗口2的姑娘帮忙看下我的卡的故障。此时,我才发现窗口2的姑娘是位实习生,窗口3可能是实习生的上级或者导师,她们貌似在结对办公。

又过了5分钟,小伙完事了。我赶紧上前,先解释插队的原因,我和刚才的小伙的问题一样,但窗口1的大哥没能解决,能否帮我看看?还没等窗口2的姑娘开口,窗口3直接问我一个「高压线」的问题:

你月薪超过 X 了吗?

点头,又问我是否周五发的工资,再次点头。她立刻说到,那是碰到同样的问题了,现在已经解决了,她们的工作群里也在讨论此事:

周五,银行批量将部分账户的日收款限额调整为 X 元,如果月薪超过 X,会发放失败。收到客户反馈后,已经批量调整为 Y 元了

然后,她让我回去等财务重新发工资即可。呃,Y 也不够啊,给我调整到 Z 吧。

她们略带吃惊地看着我,估计觉得我在吹牛,问我哪个公司的,能有这么高的工资?我说了个名字,窗口3表示没听过,实习生倒是知道,只是不知道合肥有分舵。毕竟高新区的公司,高薪是入驻的必要条件🐶

我收入确实比 Y 高,但离 Z 还差的远,Z 是我在腾讯时的收入。虽然短时间内涨薪是不可能的,但梦想还是要有的,否则如何装逼?

之后,领导授权实习生帮我调整了限额,并说不敢保证银行不会再次批量调整。然后,实习生羡慕地说道:

你们工资好高啊,我就没这个烦恼

啊,装逼装大了,赶紧说没有没有,我是怕年终奖超了,就搞个大点的数字,省的到时又要跑一趟。

5分钟后,调整完毕,这是调整后的回执:


当时没仔细看,事后发现这个回执上只有红框中的3个限额是 Z,但从字面上看,肯定不是入账限额。我猜测这是个隐藏的设置项,不会体现在回执上。


之后,她给我留了个银行的电话,如果以后再碰到这个问题,直接打电话就行,不需要再人肉过来了。


此时,还有个问题:



我的20元巨款去哪儿了?



她回复我说,因为刚刚才调整了限额,调整生效后应该会退回招行,或者重新入账,先回去等两天。如果没动静,再过来看看。


OK,从之。


0x4. 落袋为安


周一上午,我将了解到的 X、Y、Z 信息告诉财务,供他参考,以便其他同事能去银行准确地办理所需的业务。


周一下午,财务问我工资是否到账,他那边显示成功了。打开中行 APP,余额 = 工资 + 22,20元是早上08:45收到的。


完结🍭,撒花🎉,鼓掌👏🏻。等等,还有个大大的疑问:



我司大部分的人工资应该都超过了 X,为什么只有少数几个人受到了影响?



关于这个问题,我也咨询了那位领导,她表示不清楚,只是猜测跟反诈有关。我觉得很可能是真的,因为这张卡平时从来不用,只在发工资当天有进账,又在很短的时间内就全部转出,像极了骗子收到赃款立刻转走的行为。再叠加长期余额为0的因素,估计被风控识别为电信诈骗的账户了。


其实,我在几年前就碰到过类似的事,只是追债太麻烦了,我尝试过两次,放弃了,至今都没把钱要回来。事情是这样的:


我在《如何用1个2手计算器换3台 Mac》系列文章中提到,我靠着 WP8 应用赚了几千美刀。2015年,WP8 嗝屁,开发者账户里还有几百刀,彼时外汇无法入账境内银行卡了,之前是没问题的,就全提现到了 Paypal 了。


国内支持 Paypal 的地方太少,我不会海淘,提现到银行卡的手续费巨贵,就那么放着了。


2019年底,了解到 Paypal 提现到香港卡的手续费很低,只要超过1千港币,就不要手续费了。有这好事,赶紧梭哈,All Out,全部提现。


然后,悲剧了,Paypal 显示提现成功,但银行始终没收到。


先致电银行,对方表示查不到入账记录,需要联系 Paypal。电话 Paypal,对方说被美帝的什么机构拦截了,让我提供一堆英文材料证明这是合法收入。当时工作比较忙,之后没多久,那啥开始了,就没心思管这事了。


去年10月底,腾讯股价跌破200,之前看不上的几百刀,感觉也是一笔不小的数字了。再次致电 Paypal,想看看现在的流程是否简单些,把钱要回来。


因为年代久远,几经周转,PayPal 给我发了站内信,说是可能涉及跨国犯罪的洗钱,被拦截了。具体的条例是31 CFR Part 590 - Transnational Criminal Organizations Sanctions Regulations,需要去 OFAC 申请什么许可证,有问题联系 cncsdoc#paypal.com。


有戏,马上搞起,申请 OFAC 许可证的页面长这样:




好家伙,这么多内容要填,而且多达8步,不知道要怎么填,邮件咨询 cncsdoc 是否有模板参考,杳无音讯。算了,几百刀而已,比起梭哈 All In 在腾讯上的亏损,毛都算不上,责任全在美方。


所以,梭哈并不是好主意,无论是 All In 还是 All Out。做人留一线,日后好相见。


话说回来,比起 Paypal,这次中行的经历虽然也不太爽,但除了浪费时间和口舌,实际上并没有损失什么。


换个角度,我费了老大劲才查到银行卡的异常原因,这其实是一件好事。互联网也类似,为了避免敏感信息泄露,给用户的提示语,有时需要模糊化处理。例如流量因为广告作弊被封禁,触碰到风控系统的哪条红线,肯定不会明确告知的。


中行 APP 和电话客服都没能查到异常,连窗口1的大哥都没能搞定。这也是为了避免真正的犯罪分子轻松获知阈值 X,从而绕过风控。那么,是否会有犯罪分子看了这篇文章,主动去窗口要求提高限额的阈值呢?他敢?正好自投罗网,可狱而不可求啊。


PS.



阈,读音同玉,读作 yù;把阈值写成阀值,以及把阈值读作 fá 值的都是异类



所以,虽然我是此次误杀的受害者之一,也给我带来了诸多不便,还是给中行点个赞。现在电信诈骗太猖獗,宁可错杀,不可漏杀,也没毛病。


0x5. 一些启发


虽然没有实际损失,但是用户体验很不好,从开发角度看,这件事也给我带来了一些启发:



  1. 在定位 bug 原因时,经验固然重要,态度也很重要,即所谓 owner 意识;例如限额的 bug,虽然窗口1的大哥是 owner,但最终是态度端正的实习生和经验丰富的领导帮忙解决的。

  2. 涉及 C/S 交互的代码,Log 中至少要有 ID,可以是哈希或加密的,也许是定位 bug 的救命稻草;例如转账的报文 ID,虽然没能用上,如果数额很大,我猜肯定能查到。

  3. try...catch 时,如果出现 exception,考虑再来一发,尤其是网络相关的,也许会被路由到另一条畅通的路径;例如我被路由到了实习生那,finaly return true。

  4. 如果必须牺牲部分用户,应该利用多种数据源交叉验证,尽可能避免错杀;例如,查询下给我的付款的账号,就知道这是工资,可以放行,除非宗旨是宁可错杀,不可漏杀。


另外,为了避免出事后找不到人,互联网有个不成文的规定:



临近节假日,非必要不上线



我做客户端的都知道,更别说后端了。据我了解,金融系统的后端和数据库更新,多是在大半夜进行。周五,白天,批量设置用户的入账额度,真就视金钱如粪土,骚!




虽然从电信反诈的角度,中行做的没错,也是有必要的。但是,从码农的角度看,还是有值得改进的地方,以减少对普通人的影响。试想,万一被拦截的是救命钱呢?间隔50多个小时才到账,生命等的起吗?医院愿意等吗?点到为止,不展开了。


0x6. 努力与回报


行文至此,本应该结束了,但按照应试作文的惯例,还是得故作深沉的再 BB 几句,以此来升华主题,彰显作者的真知灼见,轻松获得高分。我来个画蛇添足,这件事因工资而起,为了首尾呼应,也以工资结束吧。


说到工资,我在旧文《鹅厂组长,北漂10年,有房有车,做了一个违背祖宗的决定》中提到,去年从北京回到合肥,经历了断崖式的降薪。即便如此,现在的收入在合肥还是算高的。因为当我说 Y 不够时,两位工作人员的表情不像是装的,居然连银行的人都没见过这么多钱🐶。


这不是凡尔赛,更不值得炫耀,有没有一种可能,是合肥的收入太低了,使得平平无奇的工资也能鹤立鸡群了。合肥,作为网红二线城市,号称「最牛风投」城市,也诞生了诸多网红,在提高工资水平一事上,任重而道远。


就个人来说,如何才能获得高薪呢?没有答案,但我知道仅靠努力工作肯定是不行的,相比努力,运气可能更重要。如长者所说:



一个人的命运,当然要靠个人的奋斗,也要考虑历史的进程



条条大道通罗马,有人出生就在罗马,没有绝对的公平。互联网的起薪,可能是绝大多数行业的天花板。


我在之前的旧文中多次提到,一命二运三风水。拿我自己来说,所谓的「高薪」,努力的作用只占30%不到,更多的还是因为身在互联网行业,毕业时又赶上了移动互联网的起飞。所以,虽然我很羡慕因为房子或公司股票增值而暴富的,但也没有觉得他们比我更牛逼,他们只是运气比我好,仅此而已。




人贵在自知之明,不以物喜,不以己悲。不能因为自己赶上风口先富了起来,就讥讽别人没有努力工作。更不必因为收入不高而妄自菲薄,他们可能只是运气好而已。在此,引用下罗翔老师的话:



人不应该相信天道酬勤,因为如果你相信天道酬勤,会很容易走向骄傲或者虚无。


当你成功的时候,你觉得这一切都是靠你努力拼搏得来的,你就配拥有这一切,所以你就瞧不起那些失败的人。但是当你努力最后依然失败,依然是一事无成,你又会陷入一种极大的抱怨,你会觉得天道不公。



当然,罗老师不是鼓励躺平,他还说了:



人生中95%的事情可能是我们自己决定不了的,但是我们依然要用5%的努力去撬动那95%你无法决定的事情



虽然我的关注者不过200来人,我决定不了谁会打开此文,更决定不了有多少人会读完。但我还是花了近两周的时间,努力修改措辞,力求使得文章更通顺,同时尽量有趣点。这既是对读者的尊重,也是对自己时间的尊重。至于读者是否买账,那就不是我能决定的了,正所谓:



岂能尽如人意,但求问心无愧



以上,是我自己的一些想法,如有异议,欢迎留言讨论。


作者:野生的码农
链接:https://juejin.cn/post/7282666872217157643
收起阅读 »

3个bug,让老板亏了北京1套房。。。

0x0. 背景介绍 再过几天就是 1024 程序员节了,提前祝广大程序员工友们节日快乐,少写 bug,早点下班回家,不熬夜,尽量 delay 秃头的上线时间😭 上篇文章《因为月薪过高,我的工资发放失败了。。。》中,我分享了中行的骚操作导致我收不到工资的故事。简...
继续阅读 »

0x0. 背景介绍


再过几天就是 1024 程序员节了,提前祝广大程序员工友们节日快乐,少写 bug,早点下班回家,不熬夜,尽量 delay 秃头的上线时间😭


上篇文章《因为月薪过高,我的工资发放失败了。。。》中,我分享了中行的骚操作导致我收不到工资的故事。简单的说,就是中行的码农老哥上线了一个 bug,误伤了普通用户,将正常的银行卡标记为风险账户导致入账失败。


这个 bug 看似没有带来实际损失,但是浪费了客户、客服、柜台人员的大量时间,这些都是成本。更重要的是,中行损失了潜在的高净值客户,某网友撰文吐槽此事,试图搞个大新闻,居然获得了几万的阅读。万一读者里有未来的首富,发誓不跟中行做生意,中行怎么也得损失几个小目标吧🐶。


作为码农,我们和 bug 的相处时间可能比另一半都多,毕竟咱们就是以写 bug 为生。写代码赚大钱的故事,大家见的多了,尤以逼乎和卖卖为甚。可能是大多数开发离钱太远,亦或是因为家丑不可外扬,网上鲜有人分享因为 bug 亏大钱的事故。


恰好,我做过日入过亿的大项目,脸皮也足够厚,本文分享3个我亲身经历的简单 bugs,简单到只需几秒钟就能修复。但是它们带来了巨额的亏损,足够在北京四环全款买一套100平的房子,甚至更多。


面币思过


对了,上文有老哥留言说我废话太多了,这里稍微解释下,我的个人简介里有写的:



本人主业是讲段子,副业才是写 bug



所以,为了避免文章过于枯燥,本文,我依然会按照自己的风格,用「废话」的方式来回答:



bug 产生的原因是什么?为什么没测试出来?给用户带来了什么影响?如何修复?耗时多久?如何避免?



如前所述,都是非常简单的 bug,并没有什么深度和难度,只想看干货的老哥,恕难满足,超市里应该有:


干货


声明:本文内容,毫无虚构,如有雷同,纯属雷同。


0x1. 挤兑的代价


若干年前,北京,12月的某天,23点,-10℃,骑摩托刚到家不久,正坐在暖气片上加热被冻的冰凉的屁股,接到同事电话:



合作团队 X 部门说我们最近几个月的 CDN 带宽陡增,每个月有近千万的成本



千万每月?我以为我听错了,他又重复了一遍,我蹭地一下站了起来,连呼三声卧槽,差点整个人都凉了。彼时,临近年底,老板正在分配年终奖,如果真要支付这么多成本,还发啥年终奖啊,部门都可以就地解散了。


稍后,同事又补充道,这是折扣前的成本,折扣后应该会少很多,具体需要拉上相关同事详细计算。罢了,事已至此,先睡觉吧。


次日,找到相关同事简单讨论了下,基本确定了原因。我们的产品是 SDK,先说下背景:




  1. 不久之前的某个版本增加了功能 A,功能 A 需要用到一些配置 C

  2. 为了能让用户体验更好,SDK 初始化时会主动从 CDN 下载配置 C



最近,我们完善了功能 A,配置 C 的体积也增大了数倍。同时,为了配合推广功能 A,我们做了一次运营活动,鼓励更多用户升级到最新版本。于是,在用户数量和配置 C 的体积双双陡增的情况下,带来了 CDN 流量的暴涨。


雪上加霜的是,一些宿主 APP 用黑科技对抗 ROM,力求做到「保活」,导致 APP 短时间内多次被系统干掉又自动重启,引发 SDK 初始化并下载配置 C。


另外,CDN 的计费是按照当天的峰值带宽来的,24小时内,哪怕波峰只持续了1秒,当天的成本也是按照最高点的带宽来核算的,如下图就是按照接近80的带宽来计算:


CDN 带宽示意图


再考虑一下我们平时使用手机的习惯,有明显的3个高峰期:




  1. 06:30 ~ 08:30

  2. 11:30 ~ 13:00

  3. 18:00 ~ 21:00



这3个高峰期与我们观察到的 CDN 带宽曲线非常吻合,而且早晚高峰远高于午高峰。虽然配置文件 C 并不大,但是海量的用户一股脑地同时请求 CDN,直接将瞬时带宽推上天了,进而导致核算成本超高。就像今年初的硅谷银行,因为储户的大量挤兑,直接把它给干倒闭了。


原因找到了,解决就简单了,各个击破之:




  1. 找到流量占大头的宿主 APP,与开发者沟通,配置其不请求 CDN,带宽直接降低 90%

  2. 确定根本不需要功能 A 的宿主 APP,配置其不请求 CDN,带宽再次降低 50%

  3. 减小配置 C 的文件体积,精简、移除不必要的内容

  4. 削峰填谷,优化下载策略,平滑 CDN 带宽曲线



前两步在当天就完成了,第3步和第4步是逐步完善的,最终带宽稳定在优化前的5%左右。


我猜,肯定有读者质疑,为何要在 SDK 初始化时就请求 CDN 下载配置?应该先请求某个后台 CGI 接口,由后台决定是否需要下载或更新配置。这就是另一个话题了,历史原因,前后端的合作比较拧巴,许多本该后端完成的工作,下放到客户端了,导致技术方案很山寨。后来通过两次请求 CDN 迂回实现了这个功能:




  1. 先以某个固定 URL 请求 CDN,得到配置文件 C 的 URL,URL 中有 C 的哈希

  2. 如果 URL 中的哈希与本地配置文件的哈希不同,再次请求 CDN,下载配置文件 C



问题虽然解决了,已经产生的带宽费用怎么办?部门间结算是按季度进行的,但是负责基建的 X 部门,未能及时告知我们带宽异常情况,造成了带宽的浪费。彼时,降本增效尚未开始,经过与 X 部门的协商,对方减免了我们近几个月的带宽费用。


这个问题持续了几个月,粗略的估算,即使打折,实际消耗也有数百万了。虽然 X 部门没要钱,看似是我们赚了,但最终肯定是小马哥给报销,亏的他只能坐公交了。


小马哥坐公交


0x2. 最贵的字符


不久后,轰轰烈烈的降本增效运动开始席卷整个公司。如何降本?最简单粗暴的方法就是开猿节流:


开猿节流


幸运的是,我所在的部门一直有盈利,没有采取这种低级的手段。不开猿,就只能节流了。在解决上面的 bug 后,我们就开始尝试使用不同手段来优化各种机器成本,包括 CDN 带宽、磁盘存储、CPU 资源等等。尤其是 CDN 带宽,每天上班都会看一眼,防止又出事了。


几个月后,优化初见成色,着实为部门省下了一大笔钱。距离当初定的优化目标,每天都在更近一步,心中甚是喜悦。然鹅,快乐的日子总是短暂的,在准备将这份喜悦分享给老板的前夕,出岔子了。


某天,我突然被 Y 部门的人拉到一个群,询问其 CDN 上的某个文件 F 是否归属我的部门。在得到我肯定的答复后,他们说其 CDN 上 99% 的流量来自文件 F,让我们赔付近几个月消耗的数百万元,同时不排除追溯历史费用。


我屮艸芔茻!Yesterday Once More?稍作冷静,直觉告诉我不可能有这么多钱,因为 F 的使用方式如下:




  1. 应用中自带一份 F,程序启动时会加载 F

  2. 当且仅当本地的 F 与 CDN 上的不一致时,才会重新下载



我们两到三个月才会更新一次 F,理论上,只有在更新 F 时才会产生 CDN 流量,费用最多只有他说的 1/10。彼时,大家都在「降本」,我不敢懈怠,为了尽快把锅甩出去,赶紧找经验丰富的同学的帮忙排查。


很快啊,锅就回来了,因为某处多了一个字符,导致 CDN 带宽暴涨。先暂停1分钟,能猜到可能的原因吗?


---------- 我是没用的分隔符 ----------


问题出在上面的第 2 步,假设 CDN_FILE_HASH 是 CDN 上的文件 F 的哈希值,由后台的 CGI 接口返回给客户端。整个流程的伪代码如下:


 CDN_FILE_HASH = get_cdn_file_hash_from_cgi();
 if (CDN_FILE_HASH != localFile.hash()){
  downloadFile();
 }

简单的 debug 了下,cgi 返回的 CDN_FILE_HASH 比预期多了个换行符 \n,这就导致了 if 语句始终为真。于是,应用每次启动时,都会重新下载 F。谁说人不能两次踏入同一条河流的?这跟第1个 bug 不是一毛一样吗👀。


我们每次在更新 F 后,会将其哈希写入另一个配置文件 H。在收到客户端的请求时,后台读取 H 的内容,返回给客户端。后台相关代码自上线后就没动过,所以多出来的 \n 只能是来自文件 H。


之前,我们是先人肉更新 CDN 上的 F,再将其哈希写入 H,每次都需要在 Web 上填一堆东西,比较麻烦。为了增效,就写了个脚本,一键更新 F 并将其哈希写入 H,真爽!


不用说,肯定是写文件的地方有问题,伪代码如下。暂停1分钟,看出 bug 了吗?


 def write_F_hash_to_H(F)
  with open('H', 'w') as H:
  print(F.hash(), file=H)

---------- 我是没用的分隔符 ----------


对 python 熟悉的小伙伴应该看出来了,print 会自动追加换行符(默认为\n),而 JAVA 只有 println 才会追加:


print


就这样,价值数百万的换行符诞生了,这是我见过的最贵的 bug 了,这亏钱速度,大 A 看了都要落泪😭。


修复也极其简单,将 print 函数的 end 参数赋值为空字符串即可。当务之急是减小损失,遂立刻人肉删除文件 H 中的换行符,CDN 流量瞬间就跌下来了:


仅亏损95%的股票走势


之后的 CDN 带宽走势与上图箭头右侧非常相似,这是我去年中买的一支股票,买在箭头所示的地方,两个月前割了,仅亏损95%😎。


现在,同样的问题又来了,已产生的几百万的费用咋办?彼时,各部门都在「降本」,我们之前那套说辞不好使了,对方坚称要赔付。经过几轮「友好」的争吵与互相问候,几番讨价还价,赔付自 bug 发生日期之后的费用即可,分期付款。


上面提到,我每天都会看一眼 CDN 带宽,这条大鱼为啥还会漏网呢?这就说来话长了:



很久之前,我们也隶属于 Y 部门,CDN 自然也是同一个。


后来,组织架构调整,我们被“赶出” Y 部门,自立门户了。



因为文件 F 非常重要,为了保证存量客户端版本不受影响,分家的时候,F 没有迁移,仍然保留在原 CDN 上。后续 F 有更新,还是上传到原 CDN。正常情况下,F 的带宽非常小,可能 Y 部门没发现或者懒得计较,放任我们白嫖了。


我没有权限查看 Y 部门的 CDN 的监控面板,亦不了解那段历史,直到因为 bug 暴雷,我才知道是 Y 部门在替我们「负重前行」。


bug 持续了两个多月,粗略估算,我们支付的费用,足够正常情况下使用好几年了。真的是应了那句:



所有命运馈送的礼物,早已在暗中标好了价格



不用说,最终又是老板一个人承担了所有


小马哥承担


0x3. 狸猫换太子


前面的 bug,根本原因都是技术方案不完善和细节考虑不周所致,简单的说,是自己人的锅。但,即使把代码写的完美无瑕,就一定能正确运行吗?


11年前,我是个刚出道不久的小菜鸟,接手了一个偶现 bug,涉案金额可以忽略不计。业务逻辑非常简单,如下:




  1. 客户端 POST 本地数据库中的数据至服务端,服务端返回响应 rsp

  2. 如果上传成功,客户端删除本地已上传数据;否则,再重试一次



零星有几个深圳的用户反馈我们的 APP 消耗了很多流量,最终的排查结果,嘿,您猜怎么着?高情商的说法是「涨见识了」,低情商的说法是「操」!


还有这种操作


为了复现这个问题,我尝试了三个运营商的手机卡,在GPRS,EDGE,CDMA 1x,3G等多种网络条件下测试,流量完全正常。因为缺少必要的 log,只能从代码入手,初步怀疑可能有 bug 的地方:




  1. 上传成功,但本地数据删除失败,导致每次重复上传旧数据

  2. 重试逻辑不严谨,如果上传失败,可能多次重试,浪费流量



喊上导师一起仔细读了几遍代码,确定代码没问题。之后的细节记不清了,最终是在深圳同事的协助下,找到了复现的严苛条件:



深圳,中国电信手机卡,数据流量上网,选择 CTWAP 接入点



复现时,远程 debug 发现,客户端每次都走到了上传失败的分支,但手机的网络是正常的,也能 ping 通我们的域名。


猜测是客户端或服务端收到的数据有问题,亦或二者皆有,用 tcpdump 在客户端和服务端分别抓包,很快确定了:




  1. 服务端收到了正确的数据,返回的 rsp 是 gzip 压缩的 JSON 串

  2. 但是,客户端收到的 rsp 与服务端发出的不一致,有些字节被篡改了



看到这,可能有读者会说,这不就是「HTTP 劫持」吗?某些无良运营商,利用 iFrame 在网页上插个「性感荷官,在线发牌」的广告,祖传手艺了。


根本原因肯定是 HTTP 中间人攻击,篡改了数据,但我觉得更像是运营商 CTWAP 的 bug,因为以下两种修改都可以收到正确的 rsp,说明并非运营商刻意为之:




  1. 手机上将接入点改为 CTNET

  2. rsp 不启用 gzip 压缩



在选择 CTWAP 接入点时,手机的 HTTP 请求都会被转发到电信内网的代理服务器 10.0.0.200:80。怀疑是深圳部署的 proxy 有 bug,识别不了 gzip 格式,或者某些二进制字节被误判为「非法」字符,出于好心,就顺手帮我们改了。大概类似这个15年前的笑话吧,异曲同工之妙,我不知道「绿坝」是啥:



因为「绿坝-花季护航」的屏蔽,华为官网的「24口交换机」已改名为「24嘴交换机」



再说回 bug,现在原因很明显了:



上传成功 -> 服务端返回 rsp -> CTWAP 网关篡改 rsp -> 客户端认为上传失败 -> 重试 -> 再次失败 -> 下次满足条件时继续上传...



修复也很简单,因为 gzip 压缩前的 JSON 也就几十个字节,没必要压缩。正好客户端使用的 HTTPClient 可以自动识别 rsp 是否被 gzip 压缩并正确处理,所以只需要服务端关闭 gzip 压缩即可。


彼时 3G 已经普及了,大多数电信手机的默认接入点都是 CTNET,加上网速也不快,所以实际的影响非常有限。当然,针对受影响的用户,我们如实赔付了其损失的流量费。同时,也将这个问题报给了深圳电信,至于是否修复,就不得而知了。


这大概就是「人在家中坐,锅从天上来」吧,所以,bug 不可怕,可怕的是没 bug,那必定是有 bug🐶


0x4. 总结 & 反思


所谓「常在河边走,哪有不湿鞋」,即使经验丰富的大佬,也能写出匪夷所思的 bug,例如臭名昭著的goto fail漏洞:


CVE-2014-1266


这段代码存在于 iOS 7.0.6 之前,其正式编号为CVE-2014-1266,会导致非法的 SSL 证书也能被接受,有极大的安全隐患,详细分析见dwheeler.com/essays/appl…


所以,也没啥好总结的,就像每次版本发布之后,无论针对 bug 的「批斗大会」开的多么成功,码农的反思多么深刻,下次一定还会有 bug,正如黑格尔所说的:



人类从历史中学到的唯一教训,就是人类无法从历史中学到任何教训



虽然无法完全杜绝 bug,但一向好为人师的我,还是想 BB 三句,仅针对本文分享的几个 bug:



  1. 提高成本意识:客户端开发大多都没有机器成本的概念,包括我自己,我们需要尽可能优化网络请求次数和数据量,这些都是💰啊,除非你是帮老板解决钱花不完的烦恼🐶

  2. 增加白盒测试:程序正确运行,不代表没 bug,本文第2个 bug,如果有白盒测试,上线前一定能发现每次都下载 F 的问题。至于哪里用白盒,没有标准,与💰强相关的地方,优先考虑

  3. 一切皆有可能:当出现 bug 时,如果无论如何也找不到原因,也许真的就不是自己代码的 bug,先拖一拖吧,也许它自己就好了,尤其 Android,太多玄学的事情了。。。


再啰嗦一句与技术无关的,发展才是硬道理,随着发展,把蛋糕做大,很多问题会自动解决或可以忽略。例如第1个 bug,降本增效之前,地主家还有点余粮,直接给我们免了;第2个 bug,虽然之前一直是白嫖,但彼时业务在高速增长期,CDN 那点钱比起赚到的钱来说可以忽略不计,所以 Y 部门一直也没计较。就酱,点到为止。


最后,用专治八阿哥的雍正帝镇楼,保佑码农兄弟姐妹们碰到 bug 时都能迎刃而解:


专治八阿哥的雍正


对了,欢迎大家关注我的同名公众号,所有的文章都是首发在公众号,因为他的审核速度最快。


野生的码农.png


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

分享一个Java小项目:Java实现超级马里奥的冒险之旅

引言超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利...
继续阅读 »

引言

超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利之地。

游戏目标

在这款游戏中,我们的目标是通过控制马里奥完成三个关卡的挑战。每个关卡都有不同的难度和障碍物,玩家需要灵活运用跳跃技巧和反应能力,才能成功通关。同时,消灭普通砖块还可以赚取积分,增加游戏的趣味性和挑战性。

Java实现

为了实现这个经典的游戏,我们将使用Java编程语言进行开发。Java是一种功能强大且广泛使用的编程语言,它具有丰富的图形界面库和游戏开发工具,非常适合用于制作平台跳跃类游戏。

在实现过程中,我们可以使用Java的Swing库来创建游戏的图形界面,包括游戏窗口、角色、背景等元素。同时,我们还需要处理用户的输入操作,例如键盘按键的监听和处理,以便玩家能够控制马里奥的移动和跳跃。

此外,我们还需要考虑游戏的物理引擎和碰撞检测机制,以确保马里奥能够与障碍物和敌人进行正确的交互。这可以通过使用Java的物理引擎库或自己编写相应的算法来实现。

总结

通过使用Java编程语言和相关库,我们可以成功地实现一个经典的超级马里奥小游戏。这将是一个非常有趣和有挑战性的项目,不仅可以锻炼我们的编程技能,还能够让我们体验到游戏开发的乐趣。让我们一起踏上这段冒险之旅吧!

收起阅读 »

Vue 实现 PDF 导出功能

web
笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。 安装依赖 yarn add html2canvas yarn add jspdf 思路 通过网上的一...
继续阅读 »

笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。


安装依赖


yarn add html2canvas
yarn add jspdf

思路


通过网上的一些教程,初步实现了 html 转 pdf 的功能,将一整个 DOM 元素放进去,虽然可以粗糙实现,但是出现了很多地方被分页截断的情况,这个时候就需要在某一张图片被截断时,将其自动切换到下一页中。


1.拆解父节点


所以第一步:拆解父节点,一行一行细分为很多子节点,循环遍历这些子节点,累加这些子节点的高度,如果超出了 a4 纸(210*297)的高度,则分页。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function oneNodeMultipleChildren(title, node) {
html2Canvas(node, {
scale: 2, // 清晰度
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4"); // 以mm为单位
let position = 0; // 页面偏移
let contentWidth = canvas.width; // 转换成canvas后的宽度
let contentHeight = canvas.height; // 转换成canvas后的高度
let proportion = 210 / node.offsetWidth; // html缩小至a4纸大小时的比例
let currentHeight = 0; // 当前高度
let imgWidth = 210; // canvas缩小至a4纸大小时的宽度
let imgHeight = (210 / contentWidth) * contentHeight; // canvas缩小至a4纸大小时的高度
let pageData = canvas.toDataURL("image/jpeg", 1.0); // 将canvas转成图片

for (let j = 0; j < node.children.length; j++) {
let childHeight = (node.children[j].offsetHeight + 8) * proportion; // 页面中每行的间距 margin-bottom: 8px

if (currentHeight + childHeight > 297) {
// 如果加上这个子节点后内容超出a4纸高度,就从当前位置开始分页
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight; // 这一页放了多少高度的内容,下一页就从这个高度开始偏移
if (position >= -contentHeight) {
PDF.addPage(); // 添加新pdf页
}
currentHeight = childHeight; // 下一页第一个元素的高度
} else {
currentHeight += childHeight;
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight); // 最后一页
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
// PDF.rect参数分别为:起始横坐标、起始纵坐标、绘制宽度、绘制高度、填充色
}

2.合并父节点


经过上述步骤,一个父节点多个子节点,并且每个子节点独占一行的布局可以实现分页,那要是有很多父节点呢?就需要遍历每个父节点,合并所有子节点,进行分页截断。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);

for (let i = 0; i < nodes.length; i++) {
// 根据传入的父节点数量进行循环,遍历父节点,合并所有子节点
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

3.每行多个元素


这个时候新的问题出现了,由于页面布局为 flex 布局,不同缩放下,每行的元素数量会出现变化。所以我们获取第一个子元素与 a4 纸宽度关系,如果为 n 倍,那后面 n-1 个子元素的高度不进行累加。


warning 注意
这里只解决了一行 n 个子元素宽度相等,且近似等于 a4 纸宽度的 1/n 的情况。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;
let childWidth = nodes[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth);
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

4.添加左右间距和页眉页脚


为了美化 pdf 布局,上下左右留白,就需要添加左右间距和页眉页脚:减少 html 缩小至 a4 纸大小时的比例和 canvas 缩小至 a4 纸大小时宽高,增加偏移量,并对页眉页脚进行空白遮挡。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, fNode, sNode) {
html2Canvas(fNode, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / fNode.offsetWidth; // 减少10mm
let currentHeight = 0;
let imgWidth = 200; // 减少10mm
let imgHeight = (200 / contentWidth) * contentHeight; // 减少10mm
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < sNode.length; i++) {
for (let j = 0; j < sNode[i].children.length; j++) {
let childHeight = (sNode[i].children[j].offsetHeight + 8) * proportion;
let childWidth = sNode[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth); // 减少10mm
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
// 减小10mm
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 5, position + 5, imgWidth, imgHeight); // 增加偏移量
PDF.setFillColor(255, 255, 255);
PDF.rect(0, 0, 210, 4, "F"); // 添加页眉遮挡
PDF.rect(0, currentHeight + 5, 210, Math.ceil(292 - currentHeight), "F"); // 添加页脚遮挡
}

成果展示


不同缩放下导出 PDF 对比:


每行一个子元素


每行多个子元素


作者:虚惊一场
来源:juejin.cn/post/7291142504123875364
收起阅读 »

如何创建五彩纸屑效果

web
前言 很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。 本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果 简单使用 我们可以通过 npm 安装或...
继续阅读 »

前言


很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。


本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果


简单使用


我们可以通过 npm 安装或从 cdn 导入两种方式来使用这个库。


这里我采用的是导入的方式。


在你导入完成这个库之后,我们需要一个按钮


<button onclick="myClick()">button</button>

最简单的特效只需要在点击函数当中调用 confetti 函数即可


function myClick () {
confetti()
}

动画4.gif


细节配置参数


通过传入 options 属性,我们可以在特效上自定义很多我们需要的部分,下面是部分配置属性的作用,后面我们会挑出部分属性来展示一下效果:


属性名作用
particleCount飞出的纸屑的数量,默认 50
angle飞出的纸屑的角度 90 是向上,默认 90
spread飞出的纸屑偏离中心的角度,默认 45
startVelocity飞出的纸屑的初始速度,默认 45
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
gravity重力,默认 1
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
flat是否关闭纸屑的翻转,默认 false
ticks纸屑消失速度,默认 200
origin对象,设置发射纸屑的原点,有 x y 两个参数,取值都是 0 - 1 对应上下边缘,默认 0.5 0.5
colors数组:十六进制格式的颜色字符串数组
shapes数组:纸屑的形状
scalar每个五彩纸屑粒子的比例,默认 1
zIndex纸屑的z-index,默认 100
disableForReducedMotion禁用五彩纸屑,默认 false

根据上面的参数,你可以很容易的定义自己想要的效果,下面我们随意定义部分例子:


<body>
  <button onclick="myClick1()">左倾斜</button>
  <button onclick="myClick2()">全是黑色</button>
  <button onclick="myClick3()">数量很多多多</button>
 <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.0/dist/confetti.browser.min.js"></script>
  <script>
    function myClick1 () {
      confetti({
        angle: 135,
      });
    }
    function myClick2 () {
      confetti({
        colors: ['#000000']
      });
    }
    function myClick3 () {
      confetti({
        particleCount: 500,
      });
    }
 
</script>

</body>

动画4 1.gif


详细定义纸屑形状


在上面的配置当中我们已经可以定义纸屑的大部分配置,其中 shapes 可以用于定义纸屑的形状,官方为我们预留了三个形状 'square', 'circle', 'star' 分别对应 方形,圆形,星星,这个字段传入的是一个数组,这个数组中元素的数量,决定了这个形状在所有纸屑中的比例,比如说你要是传入了 ['square', 'circle', 'star'] 那么三种形状就都是占比三分之一。


除了通过官方预留的形状,我们还可以通过两个函数来进行形状自定义,分别是 confetti.shapeFromPathconfetti.shapeFromText


confetti.shapeFromPath


这个函数可以传入一个对象,对象中存在一个 pathkey,这个就决定了创建出来的形状长什么样子


下面的代码可以创建一个三角形的纸屑


var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });

confetti({
shapes: [triangle],
});

confetti.shapeFromText


这个函数同样传入一个对象,对象存在 textscalartext 可以传入任何文字,甚至是一些文字表情也可以使用, scalar 配合 optionsscalar 使用,两个相差过大会导致字体变得很模糊。


下面的代码你就可以创建一个字符串纸屑


var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍🍍🍍', scalar });
confetti({
shapes : [pineapple],
scalar,
});

逻辑事件


纸屑在生成的时候,我们可以会需要一些事件,比如说在我们想要的时候清除掉屏幕上还未消失的纸屑,又或者在一次纸屑彻底消失的时候执行某些逻辑。


消除纸屑


我们可以通过调用 confetti.reset(); 来消除屏幕上的纸屑


监听纸屑消失事件


在我们调用 confetti() 的时候会返回一个 promise 对象,这个对象将会在纸屑完全消失的时候回调。


自定义纸屑产生的位置


在上面的例子当中,我们可能会发现,纸屑都只能在屏幕的正中心产生,这是因为 options 里的 origin 属性的默认值,我们可以通过一些方式来自定义这个产生的位置


动态设置 origin


我们可以通过动态的来设置这个值来做到自定义产生的位置。


在点击事件当中,我们能够拿到当前的点击对象,通过这个 event 对象以及 window 上获取到可视区域的宽高,我们不难算出当前按钮相对于左右的位置。


const windowHeight =
  window.innerHeight ||
  document.documentElement.clientHeight ||
  document.body.clientHeight;
// 获取浏览器高度
const windowWidth =
  window.innerWidth ||
  document.documentElement.clientWidth ||
  document.body.clientWidth;
// 获取浏览器宽度
const origin = {
x: event.pageX/windowWidth,
y: event.pageY/windowHeight,
}
// 获取比例

自定义 canvas


官方支持我们创建自定义画布的  confetti 实例,这样创建的  confetti 不会超出定义的这个画布的范围,这在某些时候可能会起到很重要的作用。


并且官方强调了,一个画布最好只和一个  confetti 实例做关联,所以说当我们创建多个  confetti 实例的时候,就也需要创建多个画布。


因为是自定义添加的画布,所以我们需要在不需要的时候手动去删除这个画布,避免产生多余的元素。


var myCanvas = document.createElement('canvas');
var container = document.querySelector('.container')
container.appendChild(myCanvas)
var myConfetti = confetti.create(myCanvas).then(()=>{
container.removeChild(myCanvas)
});

然后设定位置的操作我这里就不多做了,只需要定义这个 canvas 的生成位置就好了。


示例代码



作者:14332223
来源:juejin.cn/post/7290769553572397056
收起阅读 »

如果回到过去,我会这样告诫我自己

如题,一些牢骚。希望对年轻的你有点帮助。 # 勇敢点 提前规划,在大学的时候,你就应该开始“面向大厂”规划自己的职业路线,争取校招进入大厂实习。 不要整天宅在图书馆,看什么 Linux 内核、Unix 编程艺术、Unix 网络编程、计算机的构造与解释、不要学那...
继续阅读 »

如题,一些牢骚。希望对年轻的你有点帮助。


# 勇敢点


提前规划,在大学的时候,你就应该开始“面向大厂”规划自己的职业路线,争取校招进入大厂实习。


不要整天宅在图书馆,看什么 Linux 内核、Unix 编程艺术、Unix 网络编程、计算机的构造与解释、不要学那么多编程语言,Python、Java、Perl、Ruby、JavaScript、PHP、Go、C++…


多刷题,一定要争取去大厂,你一定可以,只要你勇敢点。


在现实生活中,人们往往依靠勇气而不是智慧去取得领先的地位。


# 不要呆在非一线城市


这里机会非常少,不是你不行,不要自卑,不要内耗,这不是你的问题,你应该去能够发挥你价值的地方。


虽然那里生活和工作节奏快。但是现在国内互联网企业都一个德性,都在模仿“狼性”文化,说着一样的互联网黑话,一样是内卷,去一线城市、一线大厂赚更多钱不好吗?


为了钱,不寒碜。


# 不要只关注技术,花点时间了解世界


不要只关注技术!别老是看那些技术类的书,你压根记不住,也很少有实践的机会。


多看点别的书,多了解自己、了解人类、了解世界、了解政治、了解经济/商业的运作原理和底层逻辑。提前布局、提前投资


世事洞明皆学问,不要穷极一生都是为了钱而工作,成为钱的奴隶,一直被恐惧和欲望支配。


推荐图书:人类简史、纳瓦尔宝典、富爸爸穷爸爸、黑客与画家


# 接受不完美的自己和代码


不要追求完美,你很普通,接受自己的平庸吧。


你不可能什么都精通,把精力花在自己核心竞争力上。


就像系统总有改不完的 bug,接受不完美的自己,学会放弃。


不要单打独斗、 尽量和更聪明的人共事。在矮子里面当将军,不如在巨人里面做士兵。


# 了解你的公司


不要只关注你眼前的这颗螺丝钉。



  • 你的公司是做什么的?核心竞争力是什么?

  • 公司的管理模式是什么?为什么要这样管理?规范的目的又是什么?

  • 公司的商业模式是什么?靠什么赚钱?怎么卖出去?

  • 公司的用户是谁?给用户创造了什么价值?

  • 公司的技术架构是怎样?

  • 公司的组织架构为什么设计?团队之间又是怎么协作的

  • 我努力加班赚的钱,最后进谁兜里?凭什么是他?



难道你不好奇?不八卦吗?


# 多积攒人脉


某些关键时刻,他们能捞你一把。同时你也要努力成为别人的有价值的人脉。


这其实并不需要你付出真感情,而应该把它当作资产。


# 可以认清现实,但是要保持批判精神


宏观的大环境个人是无法干预的,我们只能去适应。当然适应并不意味着委屈求全,每个人有选择的权利,当你无法接受公司的工作环境,不能接受公司的价值观,我们是可以选择跳出来的,而且越早越好。


我们的适应能力很强,同样能够适应糟糕的问题,然后置之不理。就比如人类的嗅觉,古人云”入芝兰之室,久而不觉其香;入鲍鱼之肆,久而不觉其臭”。


就比如笔者所在公司最近开始抓考勤打卡了,一开始内心十分抵触,现在也慢慢‘适应’了,也没有之前的抱怨,但我知道这对我来说并不是一件好事。


既要认清现实,保持批判精神,否则将一成不变。


# 关注战略设计


大部分程序员都是实现者,即战术实现者。很多时候,我们都不知道我们的工作的价值是什么。


因此我们也要关注战略设计,保持对一切事情的好奇心,尝试突破自己的职能边界,没人会阻止你,也很少人会给你机会。


# 效率从来不是一个人的事情,伟大的项目也是如此


不要相信小说、传记里面的孤胆英雄。


前几年关于 10 倍程序员也很多讨论,比如极客时间 10X 程序员工作法, 这些教程总结了很多务实的提效方法论。


总的来说,提高效率从来不是一个人的事情,另外程序员的主要工作‘编码’ 也仅仅只占整个研发流程的 20% ~ 30%。


# 不要什么都亲力亲为,学会外包


让你的能力和知识可以复制和传递,比如 CodeReview,技术写作,写好文档。


培养得力的助手,或者更好的方式是招揽比你更聪明的人。


# 不要轻信什么最佳实践


没有绝对正确的东西,没有放之四海皆准的东西。


学习它们,然后忘掉


# 问题的维度


不要只关注吃掉眼前的棋子,从更高的维度去解决问题。


举个例子



  • 问题域。有些问题不一定就要在技术层面解决,可能在产品层面、战略层面就能规避掉。

  • 解决域。另外,提升抽象的高度,在解决问题时能否举一反三?覆盖更多场景?


# 不要被奴役


有房一族(来源:富爸爸穷爸爸)



  • 鼓励抱怨,但也要解决问题。

  • 不要为了钱而工作,让钱为你工作。

  • 工作不是为了写代码,让代码为你工作。

  • 把自己当成一家公司去经营

  • 不要被雇主奴役,你和他们是雇佣合作关系,不是奴隶关系,不要被 PUA

  • 不要被机器奴役。人类创造编程语言是为了服务人类,而不是服务机器。不要追求那些反人类的奇技淫巧,也不要自以为掌握了一门底层、学习曲线陡峭的编程语言而沾沾自喜,不符合人类心智的技术迟早被淘汰。


# 不管你喜不喜欢,在中国你还是得要学会“管理”


金字塔


在国内 IT 打工人的体系更像是军队管理,俗话说就是吃年轻饭的。


尽管未来的趋势是分工的精细化,管理者也是占少数。为什么我就不能当个平庸的程序员呢?



  • 我们所处的社会主导集体主义,自然也会滋生对权力的崇拜,很多人对这个金字塔尖趋之若鹜。权利也意味着‘成功’

  • 平庸很容易被取代。而熟练工并没有壁垒,你能干别人也能干。商业是逐利,在高度内卷的市场下,为什么就不能选择跟便宜、精力更旺盛的年轻人呢?


你面前可能有几条路,创业、技术专家、管理,不管是哪条路都是很艰难,管理在很多人看来是顺理成章。


或者,现在就想想,你不搞 IT 还能干什么?提前做好投资


# 精心炮制的故事


这个世界的秩序是由精心炮制的故事组成,而且大部分人都相信它。


保持怀疑的姿势,可以让你跳出游戏


# 直面你的恐惧


为什么你会社恐?


为什么几天后的一个会议会让你忐忑不安?


为什么你那么在乎别人的看法?


为什么你总是感觉到焦虑?


为什么你不敢说出你的真实想法?


为什么你会情不自禁地与别人做比较,然后妄自菲薄?


为什么你会恐惧?如果人生是一场游戏呢?


# 编程的本质是抽象


编程是一门抽象艺术。


把现实世界的业务抽象成二维表,数据结构,对象关系、业务流程。


前端页面抽象成组件,低代码,DSL,本质上都是抽象的艺术。


抽象需要发挥人的主观能动性,人与人的差距就体现在这里。而工具通过学习一般都能掌握,很难建立壁垒


# 不要自我感动



  • 只有功劳,没有苦劳。

  • 选择比努力更重要。

  • 大部分企业并不在乎你代码写得多多漂亮,而在于你能不能真正创造价值。


# 继续保持专注


远离那些垃圾。


# 总会有差评


不管是多好的产品都会有差评。


# 不要随波逐流


我发现,程序员群体大部分是比较‘安分守己’的,身边很多典型的例子,早早地结婚,早早地生子,996 的工作,电子产品自由,开着特斯拉,在老家可能还有套套牢的房。


人生的轨迹和父辈其实不会有太大的区别。


这真是你想要的人生吗?


# 降低欲望,你会更快乐


所有的痛苦都来源于欲望,远离一切成功学,回头是岸


作者:荒山
来源:juejin.cn/post/7283313180730277907
收起阅读 »

实习到毕业一年的回忆:工作旅程

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水...
继续阅读 »

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水线拧螺丝了”,我点头说道。


21年六月在学校投了上百份简历,面试收到了几个offer,但是实习工资给的太少,不是2.5k或者3k,这对于那时年少轻狂的我怎么可能接受呢,果断拒绝,快月底临近毕业找不到工作的我越来越慌了,后来约了一家线上面试并且通过了,实习工资150一天,正常每个月能拿3.3k,有节假日的情况下只能拿到不到2.8k的可怜工资。但命运真的很神奇,因为这家实习公司,结识了能够在职场上帮助到我的良师益友。


实习公司所在的写字楼


21年十月认识了一位朋友介绍的女生,可能是好久没和女生接触过,我变得不怎么会和女生聊天了,只记得我和她打了两个月的王者,基本上天天玩,还都是玩的人机,后来不知道啥原因就凉凉了,当然两个月也没见过面。当然因为这个事搞的我心烦意乱,工作没法工作,21年底,22年初,也就是元旦期间,我向公司提出离职,电话裸辞,直接就不去公司了,给老板整的一脸懵逼。22年一月中旬,公司聚餐邀请了已经离职的我,晚上酒喝起兴的我,在同事的劝说下,我向老板表明了我想回到公司的意向,后来如愿以偿的回到了公司,此时,我的工资不是150一天了,而且达到了惊人的4.5k每月。


上班路上的金鸡湖大道


22年六月临近毕业,在实习公司沉淀了一年,我觉得时机已经成熟是时候走了,鼓起勇气和老板说了离职,老板同意了。这个时候我还不知道未来的一年,我还会和他们经常聚餐,一起聊行业、工作、生活。甚至今天的这份工作也得益于他们。


离职后,准备去南京发展,当时在常州的同学那暂住了几天,闲的没事干就投了几份简历玩玩,面试了两家都收到了offer,一家给政府做erp系统的公司给了7.5k,另一家是上市公司的外包给了8k,随后我就不想去南京了,选择了那家外包公司,在那前几个月基本上天天没事,过的相当的安逸,每天晚上下班后,5:30准备到球场,后来我换了个组长,我开始做MES系统了,第一个系统我身份是打杂的,给另一个同事当助手,后来做的系统,我开始当主力开发。22年底,工作干的十分不顺心,萌生了离职的想法,向外包公司的部门经理提了涨薪,他只给涨500块钱,我觉得也没必要留下了,所性直接离职,此时我还没有转正,所以我直接在一周到走人。


再次离职后,我选择回到老家休息一段时间,思考一下第二年该去往何处。在家乡待了近四十天,基本上没有碰过电脑,我到处的玩,打球,打游戏,泡澡,感觉已经废了。


过年前几天,我开始慌了,于是我重新打开我的小米笔记本,打开了熟悉又陌生的IDEA,学习了几个开源框架,背了一些面试题,准备年后去外地找工作。


CIM开源框架


大年初三,我早早的买好火车票去往常州,准备在常州找一份工作,可惜我找了近一周,一份工作也没有找到,于是我将目光看向南京和老东家所在的苏州。我联系了实习公司一个同事现在所在的公司,于是他将我内推到了现在的这个公司,他向上面的人担保我肯定没有问题,所以我直接跳过了面试,也就是在这个公司,因为我代码写的好,所以我两次加薪达到了税后五位数。


23年五月二十号,公司安排我去西安出差两周,这是我人生第一次出差,见到了网络上所谓的甲方,值得我纪念一下。


飞机上的云层
仓库


如今,那位内推我的同事,也就是我第一份实习公司的同事,他要走了,去了一家做大数据的公司,领导让我开始学习做管理,以后带新人做项目,我只能说尽力而为。


对于像我这样学历不高的人而言,个人觉得代码不是技术架构,而是人情世故,人脉是人生宝贵的一笔财富。


浪子花梦


上班摸鱼写于2023年7月12日11点。


作者:浪子花梦
来源:juejin.cn/post/7254572372137410597
收起阅读 »

使用a标签下载文件

web
引言 HTML中  <a>  元素(或称锚元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。 <a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <...
继续阅读 »

引言


HTML中  <a>  元素(或称元素)可以通过它的 href 属性创建通向其他网页、文件、电子邮件地址、同一页面内的位置或任何其他 URL 的超链接。


<a> 中的内容应该指明链接的目标。如果存在 href 属性,当 <a> 元素聚焦时按下回车键就会激活它。


本文主要讲解如何通过a标签来下载文件。


download属性


浏览器将链接的 URL 视为下载资源。可以使用或不使用 filename 值:




  • 如果没有指定值,浏览器会从多个来源决定文件名和扩展名:



    • Content-DispositionHTTP 标头。

    • URL的最后一段。

    • 媒体类型。来自 Content-Type 标头,data: URL的开头,或 blob: URL 的 Blob.type




  • filename:决定文件名的值。/ 和 \ 被转化为下划线(_)。文件系统可能会阻止文件名中其他的字符,因此浏览器会在必要时适当调整文件名。





备注:



  • download 只在同源 URL或 blob:data: 协议起作用。

  • 浏览器对待下载的方式因浏览器、用户设置和其他因素而异。在下载开始之前,可能会提示用户,或者自动保存文件,或者自动打开。自动打开要么在外部应用程序中,要么在浏览器本身中。

  • 如果 Content-Disposition 标头的信息与 download 属性不同,产生的行为可能不同:

  • 如果文件头指定了一个 filename,它将优先于 download 属性中指定的文件名。

  • 如果标头指定了 inline 的处置方式,Chrome 和 Firefox 会优先考虑该属性并将其视为下载资源。旧的 Firefox 浏览器(版本 82 之前)优先考虑该标头,并将内联显示内容。



下载方式


1. 直接使用a标签的href属性指定文件的URL


可以在a标签中使用href属性指定文件的URL,点击链接时会直接下载文件。


<a href="file_url">Download</a>

优点:简单易用,只需在a标签中指定文件的URL即可。


缺点:无法控制下载文件的名称和保存位置。


2. 使用download属性指定下载文件的名称


可以在a标签中使用download属性指定下载文件的名称,点击链接时会将文件以该名称保存到本地。


<a href="file_url" download="file_name">Download</a>

优点:可以控制下载文件的名称。


缺点:无法控制下载文件的保存位置。


3. 将文件数据转为Blob进行下载


当需要将文件数据转为Blob或Base64进行下载时,可以使用以下方法:


1. 将文件数据转为Blob进行下载


function downloadFile(data, filename, type) {
const blob = new Blob([data], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
URL.revokeObjectURL(url);
}

function fileToBlob(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(new Blob([reader.result], { type: file.type }));
};

reader.onerror = reject;

reader.readAsArrayBuffer(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBlob(fileData).then(blob => {
downloadFile(blob, fileName, fileType);
});

首先,我们定义了一个名为downloadFile的函数,它接受三个参数:文件数据(data),文件名(filename)和文件类型(type)。 在函数内部,我们使用Blob构造函数将文件数据和类型传递给它,从而创建一个Blob对象。然后,我们使用URL.createObjectURL()方法创建一个URL,该URL指向Blob对象。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBlob函数将文件数据转换为Blob对象。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Blob对象。 然后,在Promise的回调中调用了downloadFile函数来进行下载。


2. 将文件数据转为Base64进行下载


function downloadBase64File(base64Data, filename, type) {
const byteCharacters = atob(base64Data);
const byteArrays = [];

for (let i = 0; i < byteCharacters.length; i++) {
byteArrays.push(byteCharacters.charCodeAt(i));
}

const blob = new Blob([new Uint8Array(byteArrays)], { type: type });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = filename;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
}

function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onloadend = () => {
resolve(reader.result.split(',')[1]);
};

reader.onerror = reject;

reader.readAsDataURL(file);
});
}

// 使用示例
const fileData = ...; // 文件数据
const fileName = 'example.txt';
const fileType = 'text/plain';

fileToBase64(fileData).then(base64Data => {
downloadBase64File(base64Data, fileName, fileType);
});

首先,我们定义了一个名为downloadBase64File的函数,它接受三个参数:Base64字符串(base64Data),文件名(filename)和文件类型(type)。 在函数内部,我们首先将Base64字符串解码为字节数组,并将其存储在byteArrays数组中。然后,我们使用这些字节数组创建一个Blob对象,并使用URL.createObjectURL()方法创建一个URL。 接下来,我们创建一个<a>元素,并设置其href属性为之前创建的URL,并将下载属性设置为指定的文件名。然后将该元素添加到文档的body中。 最后,我们模拟用户点击该链接进行下载,并在完成后清理相关资源。


在使用时,我们首先调用fileToBase64函数将文件数据转换为Base64字符串。该函数返回一个Promise对象,在Promise的resolve回调中返回了转换后的Base64字符串。 然后,在Promise的回调中调用了downloadBase64File函数来进行下载。


总结


您可以根据需要选择将文件数据转为Blob或Base64进行下载。如果您已经有文件数据,可以使用fileToBlob函数将其转为Blob对象并进行下载。如果您希望将文件数据转为Base64进行下载,可以使用fileToBase64函数将其转为Base64字符串,并使用downloadBase64File函数进行下载。


作者:前端俊刚
来源:juejin.cn/post/7291200719683502120
收起阅读 »

前端实现蜂巢布局思路

web
效果图如下 上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。 要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。 观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。 这里可以以中...
继续阅读 »

效果图如下


image.png
上图的组成可以分为两部分,一个为底图六边形的形成,一个为内容六边形的形成。


要生成对应的六边形,首先要获得需要绘制的六边形的中心坐标。


观察不难得出结论,以中心的六边形为基点,第二圈很明显能排6个,第三圈能排12个,以此进行类推。


image.png


这里可以以中心点为坐标原点[0,0],以[1,0], [1,1],[-1,1],[-1,0],[-1,-1],[1,-1]这种形式来表示第二圈在轴线上的六个六边形的中心点关系(这是一种形式,并非真实的坐标系坐标)。


// 第二圈时的对应圆上的坐标值
const pixiArr = [
[1, 0],
[1, 1],
[-1, 1],
[-1, 0],
[-1, -1],
[1, -1]
]

// 根据圈数生成对应轴线上的坐标
function generatePixiArrByWeight(weight) {
if (weight === 2) {
return pixiArr
}
const multiple = weight - 1
const tempPixiArr = pixiArr.map((x) => {
return [x[0] * multiple, x[1] * multiple]
})
return tempPixiArr
}

进一步观察,可知,第三圈时两条发散的轴中间夹了一个六边形,第四圈时两条发散的轴中间夹了两个六边形,依次类推。


六条发散轴上的六边形中心点坐标是最容易计算的,不过要计算三圈及其开外的,就得有那么一点点的数学基础,知道sin60度cos60度的意思。


const sin60 = Math.sin(Math.PI / 3)
const cos60 = Math.cos(Math.PI / 3)

有了上面的铺垫后就可以开始了,定义一个函数,传入的参数为六边形总个数和六边形的边长


// 生成六边形中心坐标
function getHexagonCoordinateArrayByTotal(total = 0, radius = 0){
// 1、获取圈数weight
if (total === 0) return []
let tierList = [] // 用于存放每圈的个数
let tierIndex = 0
while (total > 0) {
if (tierIndex === 0) {
tierList.push(1)
total = total - 1
} else {
let n = 6 * tierIndex
total = total - n
if (total < 0) {
tierList.push(total + n)
} else {
tierList.push(n)
}
}
tierIndex++
}
const weight = tierList.length
// 2、根据圈数去获取coordinateArray坐标列表
// getHexagonCoordinateArrayByWeight:根据圈数和边长返回对应的坐标点
const weight = tierList.length
let coordinateArray = []
for (let i = 0; i < weight; i++) {
if (i + 1 === weight) {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius).slice(
0,
tierList[weight - 1]
)
]
} else {
coordinateArray = [
...coordinateArray,
...getHexagonCoordinateArrayByWeight(i + 1, radius)
]
}
}

return coordinateArray
}

有个getHexagonCoordinateArrayByWeight需要实现其,方式为


function _abs(val = 0) {
return Math.abs(val)
}

function getHexagonCoordinateArrayByWeight(weight = 1, radius = 0) {
if (weight === 0) return []
if (weight === 1) return [[0, 0]]
const addNum = weight - 2
const addArr = generatePixiArrByWeight(weight)
const hypotenuse = radius * sin60 * 2 // 两倍的边心距长度
let offsetArr = []
let offsetX
let offsetY
for (let i = 0; i < addArr.length; i++) {
const t = addArr[i]
if (t[1] !== 0) {
offsetX = t[0] * hypotenuse * cos60
offsetY = t[1] * hypotenuse * sin60
} else {
offsetX = t[0] * hypotenuse
offsetY = 0
}
offsetArr.push([offsetX, offsetY])
}
const tempOffsetArr = JSON.parse(JSON.stringify(offsetArr))
let resArr = new Array(6 * (weight - 1))
let lineArr = []
for (let i = 0; i < 6; i++) {
let lindex = i * (weight - 1)
resArr[lindex] = tempOffsetArr[i]
lineArr.push(lindex)
}
// 利用已知的六个发散轴上的中心坐标点推出剩余的中心坐标点
if (addNum > 0) {
for (let i = 0; i < 6; i++) {
let s = tempOffsetArr[i]
let e = i + 1 === 6 ? tempOffsetArr[0] : tempOffsetArr[i + 1]
let si = lineArr[i]
let sp = addNum + 1
let fx
let fy
if (i === 0) {
fx = (s[0] - e[0]) / sp
fy = (e[1] - s[1]) / sp
}
if (i === 1) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0

}
if (i === 2) {
fx = (_abs(e[0]) - _abs(s[0])) / sp
fy = (_abs(s[1]) - _abs(e[1])) / sp
}
if (i === 3) {
fx = (_abs(s[0]) - _abs(e[0])) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
if (i === 4) {
fx = (_abs(s[0]) + _abs(e[0])) / sp
fy = 0
}
if (i === 5) {
fx = _abs(s[0]) / sp
fy = (_abs(e[1]) - _abs(s[1])) / sp
}
let mr = []
for (let j = 0; j < addNum; j++) {
if (i === 0 || i === 1) {
mr.push([s[0] - fx * (j + 1), s[1] + fy * (j + 1)])
}
if (i === 2) {
mr.push([s[0] - fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 3) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 4) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
if (i === 5) {
mr.push([s[0] + fx * (j + 1), s[1] - fy * (j + 1)])
}
}
mr.forEach((x, index) => {
resArr[si + index + 1] = x
})
}
}
return resArr
}

至此,生成六边形中心坐标点的方法完成。
有了中心坐标生成方式之后,就可以使用Konva这种辅助绘图的库来进行效果绘制了。


作者:前端_六一
来源:juejin.cn/post/7291125785796018236
收起阅读 »

过度设计的架构师们,应该拿去祭天

我发现一个非常有趣的现象。 十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。 后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。 当然,现在互联网行业的架构师,也越来越...
继续阅读 »

我发现一个非常有趣的现象。


十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。


后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。


当然,现在互联网行业的架构师,也越来越“美女化”了,基本上有个两三年工作经验的,带两三个应届生负责过一两个QPS不过十,用户量不过千的小系统的,把项目用SSM框架给搭建起来的,也都成架构师了。



而这些所谓的“架构师”们,如果仅仅是title上的改动,平时工作中该撸代码就撸代码,该摸鱼看网页就看网页,其实也真的没什么。


最最最最怕的就是,他们觉得自己的身份已经变了,是时候该体现出自己作为系统架构师价值的时候了,那一切就会变得不可收拾了。


这些架构师们体现价值的方式当然是做架构设计。按照他们的话说,系统架构要具备前瞻性、灵活性、复用性、伸缩性、可维护性、可扩展性、低耦合性、高内聚性、可移植性。当然,基本上90%都是过度设计。



下面让我们来细数一下,那些年,我所经历过的过度设计。



名副其实的微服务


不久前我面试过一个中小厂架构师,看他的简历上赫然写着,“主导XX系统从单体服务往微服务架构演进工作”。


然后我问他的问题是:“详细说下微服务拆分这件事情,包括:微服务拆分的原因、时机和拆分后的粒度。”


这个架构师说的第一句话就把我雷到了:“微服务拆分的粒度,我认为越细越好,不然为什么叫微服务呢?而且,现在的一个很小的微服务,随着业务的持续迭代演进,未来都有可能变得非常庞大,我们做架构设计的,必须要具备前瞻性。”


他接着说:“我们的微服务不但按照业务模型进行的拆分,而且我还按照controller层、service层和dao层也做了拆分,这样可以提升代码复用性,你想用我哪层的代码,就可以调用我哪层的API。”


最终,一个单体服务就被他拆分成了这样。



我问他:“微服务的‘三个火枪手原则’了解吗?”


他摇了摇头,说不清楚。


我心里感慨到,今年阿里云和腾讯云业绩能不能达标,全看这类架构师的了,他们是真费机器啊。


3个库和300张表


去年,跟一个三方公司临时组建了一个项目组,共同开发孵化A项目。


项目联调期间,我跟三方公司的小A说:“我刚调用了你们项目的XX接口,新增了20条交易数据,你看看你们接口的业务处理正常吗?数据库里面有这20条数据吗?”


小A说:“好的,稍等,我看看。”


20分钟过去了,我问小A看得怎么样了。


小A说:“业务处理是正常的,数据我正在一条条找,20条已经找到17条了,我在找剩下的3条。”


我听得有些懵逼,问小A:”你直接从你们订单表里,不能一下子看到20分钟前写入的20条数据吗?为什么还需要一条条找啊?“


小A说:”我们的架构师老张,按照每天三百万订单的数据增量,做了一个五年架构规划,已经分好了3个库和300张表。我现在正在根据他的路由规则,一条条地找这些数据。“



满城尽是大中台


呵呵,忽如一夜春风来,满城尽是大中台。


2015年福厂正式提出了“大中台、小前台”的中台战略,通过将原本分散到各个业务的支持部门,比如技术部门、数据部门集中到一起,进行快速的服务迭代,以期更高效地支撑前线,大幅降低支持部门的重复投资建设。



三年后,各个大小互联网公司纷纷跟进,争相建设自己家的中台,也就在这时,某独角兽公司的架构师老范过来找我取经。


我跟老范说:“你们的两个主业务是机票和酒店,业务差别太大了,且创新孵化业务并不多,并不适合中台策略。”


老范说:“不,中台这个我们一定要搞,因为既是研发团队的政治任务,也是我个人的技术追求。”


半年后,我问老范搞得怎么样了,老范说:“唉,讨论了半年哪些职责属于大中台,哪些职责属于小前端,现在还没讨论明白呢。”


无处不在的消息队列


福厂收购了某公司,在收购后的一次技术交流中,我听到对方公司的首席架构师说:“MQ是个好东西,能异步处理,能消峰,能解耦,还是应该在项目中多用用的。”



后来发现,大首席架构师的下级执行力真强,MQ真的在他们的项目中无处不在:



  • 发短信验证码的场景用MQ,且其生产者和消费者是同一个服务,就为了用MQ异步调用短信服务的SDK;

  • 打业务日志的场景用MQ,且其生产者和消费者是同一个服务,就是为了用MQ异步打一行日志;

  • TPS个位数的约课场景用MQ,且其生产者和消费者是同一个服务,其美名曰进行消峰;

  • 各服务间的通信基本上80%都用了MQ,而不是RPC,其美名曰系统解耦;


牛逼Class!


遍地开花的多级缓存


对,对,还是上次的那个首席架构师,他除了爱用消息队列外,还特别喜欢用缓存,而且是Guava Cache + Redis的多级缓存。



据同事说,这种多级缓存策略在这位首席架构师的熏陶下,已经遍布了OA系统、公司官网、消息中心、结算系统、供应链系统、CRM系统。


首席架构师说:“缓存不仅能提升性能,还能帮助数据库抗压,提升系统可用性,绝对是个好东西,应该多用一用。”


然后,公司的系统就经常发生多种缓存的数据与数据库的数据一致性问题。


首席架构师又说:“任何架构都是有利有弊的,但只要利大于弊就好,不要太在意。”


设计模式的流毒


记得我刚上班不久,组内有一个架构师同事,写的代码巨复杂,各种技巧、设计模式、高级语法满天飞,还沾沾自喜的给我们炫耀。



一次Code Review的时候,我嘴欠问他这里咋这么设计,他就鄙视的说:“你连这个都不知道,这是设计模式中的建造者模式啊。”



当时觉得他好牛逼,而我好low。


以后,每次进行Code Review,只要看到其他同事代码里有几个if else,架构师同事就质问道:“为什么不用策略模式优化if else?”


当然,还有其他的质问,类似于:这块为什么不用抽象工厂模式?这块为什么不用代理模式?这块为什么不用观察者模式?


后来我们就给他起了个外号,叫“设模”(se mo)。


多多益善的复杂关系


前面说的那些架构师们,他们过度设计所带来的后果是浪费服务器和研发资源,但架构师老邓不一样,他的过度设计是浪费表。


之前见过某在线教育公司设计的表结构,基本上所有表之间的外键关系都是按照多对多方式设计的,也就是加一个中间的关系映射表。



有的我是可以理解的,比如:



  • 一个学生会出现在多个不同的班级里,而一个班级里也会有不同的学生;

  • 一个学生可以学习多门课程,而每门课程又会对多个学生进行学习;

  • 一个学生可以上多个老师的课,而一个老师又可以教多个学生;


但是,但是。



  • 一个学生可以有多个考试成绩,难道一个考试成绩还能属于多个学生吗?

  • 一个学生有多个课程的课时余额,难道一个课时余额还能属于多个学生吗?


老邓说:“万一以后业务变化了呢?一切皆有可能啊。”


数据库的可移植性


还在上大学的时候,在CSDN上看某著名架构师在极力强调数据库的可移植性。



我记得当时的原话大概是:



  • Hibernate的HQL可以帮我们保证不同数据库之间的移植性,比如:MySQL中的limit和Oracle中的rownum。

  • 为什么不能写存储过程?一个重要的原因就是业务逻辑放到数据库里会导致数据库移植成本变大。

  • 程序内尽量采用标准SQL语法,因为我们要考虑将来的移植风险。


当时听了,觉得这个大架构师简直就是YYDS。然后我工作了这么多年,也没遇到过一次数据库移植。


无间道版的数据校验


我厂某团队的架构师老李素以严谨著称,其经常放在嘴边的一句话就是:“工程师不仅仅是一项有创造性的职业,也是一门严谨审慎的职业。”


这话说的确实没毛病,我也看过他们团队的工程代码,程序的边界处理、异常处理和容错处理做得都特别好,入参校验也是特别细致入微。


就像老李所说的那样:“All input is evil。”


不,等等,入参校验没问题,但怎么从数据库里读出来的数据,为什么还要再校验一遍?难道不是在写入的时候校验吗?


老李面无表情地说:“如果数据库中的数据,并没有经过应用程序处理,而是不知道被谁直接改库了呢?”


卧槽,这是泥马数据校验无间道版吗?



疯魔成活的配置化


还是上面的那个架构师老李,他要求团队代码中带数字的地方,全部走配置中心,这样可以不发布代码就直接进行修改。



然后,我就看到了这样的现象:



  • 如果某个HashMap的size大于0,则进行xxxx,这个0写到了配置中心里。

  • 如果用户性别等于1(男性),则进行男装推荐,这个1写到了配置中心里。

  • 如果商品状态等于2(已下线),则进行xxxx,这个2写到了配置中心里。


配置中心啊,你的责任真的好重大。


总结


遇到这种类型的架构师,真的特别想把他们祭天了,因为我是Kiss原则的忠实拥趸。



Keep it simple,stupid,即:保持简单、愚蠢。


保持简单就能让系统运行更好,更容易维护扩展,越是资深的人,越明白这个道理。


作者:库森学长
来源:juejin.cn/post/7287144182967107638
收起阅读 »

职场坐冷板凳的那些日子

曾经有一段职场生涯,坐了很长时间的冷板凳,也正是那段经历,彻底改变了整个职场生涯。今天这篇文章聊聊自己曾经的经历,也聊聊如果在职场中被坐了冷板凳该咋办。 关于冷板凳 有人的地方就有江湖。而这个江湖中是否性情相同,是否因某些事(或利益)产生矛盾,都可能造成职场坐...
继续阅读 »

曾经有一段职场生涯,坐了很长时间的冷板凳,也正是那段经历,彻底改变了整个职场生涯。今天这篇文章聊聊自己曾经的经历,也聊聊如果在职场中被坐了冷板凳该咋办。


关于冷板凳


有人的地方就有江湖。而这个江湖中是否性情相同,是否因某些事(或利益)产生矛盾,都可能造成职场坐冷板凳的情况。


冷板凳常见于上级对下级的打压。一般手段就是让你无所事事或安排一些边缘性的事务,不怎么搭理你,从团队层面排挤你,甚至否定你或PUA你,别人也不敢跟你沟通,以至于让你在团队中形成孤立的的状态。


根据矛盾或冲突的不同,冷板凳的程度也不同。常见的有:浅层次的冲突,可进行修复;不可调和,无法修复;中间的灰度状态。


通常根据具体情况,判断程度,有没有可能或必要修复,再决定下一步的行动。


第一,可修复的冷板凳


有很多同学,特别是技术人,在职场上有时候特别的“刚”,为了某个技术点跟领导争的面红耳赤的,导致被坐冷板凳。


比如有同学曾有这样的经历:领导已经拍板的决定,他很刚的去跟领导据理力争,导致起了冲突,大吵一架,领导也下不来台。随后领导好几天没搭理他。


针对这种情况,一般也就是一顿火锅的事,找领导主动沟通,重拾信任。甚至可能会出现不打不相识的情况。当然,一顿火锅不够还可以两顿。


第二,清场性质的冷板凳


这种情况常见于业绩或能力不达标,已经是深层次的矛盾,一般会空降过来一个领导,故意将其边缘化。属于清场接替工作性质的,基本上无法修复。


针对这种情况,看清局势,准备好找下家就是了。如果做得好,准备好交接工作,给彼此一个体面。毕竟,很多事情我们是无法改变的。


第三,灰度状态的冷板凳


以上两个常见都比较极端,而大多数情况下都是灰度状态的,大的可能性就是一直僵持着。这时作为下属的人,一般建议主动去沟通、修复。


如果阅历比较浅,看不出中间的微妙关系以及深层次的冲突点,就请人帮你看看,听听别人的建议和决策。再决定值不值得修复,要不要修复。


我的冷板凳


曾经我在一家公司坐的冷板凳属于第三种,但却把这个冷板凳坐到了极致。下面就讲讲我曾经的故事。


跟着一个领导到一家新公司,本来领导带领技术部门的,但由于内部斗争的失利,去带产品团队了,而我也归属到他对手的手下了。这种情况下,冷板凳是坐定了,但也不至于走人。


被新领导安排了一个很边缘的业务:对接和维护一套三方的系统。基本上处于不管不问,开会不带,接触不到核心,也与其他人无交流的状态。起初这种状态非常难受,人毕竟是社群动物,需要一个归属感和存在感的。


但慢慢的,自己找到了一些属于自己的乐趣。


首先,没人管没人问,那就可以自己掌控节奏和状态了。看他们天天加班到凌晨一两点,而自己没人管,六七点就下班了。最起码在那段持续疯狂加班的岁月里,自己保住了头发。那位大领导后来加班太多,得了重病,最终位置也没保住。


其次,有了大把的时间。上班几乎没人安排工作,于是上班的时间完全自己安排。三方服务商安排了对接人,好歹自己作为甲方,于是天天就跟服务商的技术沟通,询问他们系统的设计实现,技术栈什么的。


在那段岁月里,完成了几个改变后续职场生涯的事项。


事项一:那时Spring Boot 1.5刚刚发布,公司的技术栈还没用上,但服务商的这套系统已经用上了。感觉这玩意太好用了,于是疯狂的学学习。因为当初的学习,后来出版了书籍《Spring Boot技术内幕》那本书。


事项二:写技术博客,翻译技术文档,录技术视频。服务商的系统中还用到了规则引擎,当时市面上没有相关的中文资料。于是边跟对方技术沟通,边翻译英文文档,写博客。后来,还把整理的文档录制成视频,视频收入有几万块吧。


这算是自己第一次尝试翻译文档、录制教学视频,而且这个领域网络上后续的很多技术文章都是基于我当初写文章衍生出来的。最近,写的第二本书便是关于规则引擎的,坐等出版了。


事项三:学习新技术,博客输出。当时区块链正火爆时。由于有大量的时间,于是就研究起来了,边研究边写技术博客。也是在这个阶段,养成了写技术博客的习惯。


因为区块链的博客,也找到了下家工作。同时写了CSDN当时类似极客时间的“Chat”专栏,而且是首批作者。也尝试搞了区块链的知识星球。后来,因为区块链的工作,做了第一次公开课的分享。还是因为区块链相关,与别人合著了一本书,解释了出版社的老师,这也是走上出书之路的开始。


因为这次冷板凳,让职场生涯变得极其丰富,也扭转了大的方向,发展了副业,接触了不同行业领域的人。


最后的小结


在职场混,遇到坐冷板凳的情况不可避免,但如何化解,如何抉择却是一个大学问。尽量主动沟通,毕竟找工作并不容易,也不能保证下家会更好。同时,解决问题,也是人生成长的一部分,所以,尽量尝试化解。


但如果矛盾真的不可调和或持续僵持,那么就更好做好决策,选择对自己最有利的一面。


曾在朋友圈发过这样一段话,拿来与大家分享:


“始终难守樊登讲过的一句话:人生成长最有效的方法,就是无论命运把你抛在任何一个点上,你就地展开做力所能及的事情。


如果还要加上一句,那就是:还要占领制高点。与君共勉~”


作者:程序新视界
来源:juejin.cn/post/7267107420655583292
收起阅读 »

和AI网聊10分钟被骗430万,真实诈骗案震惊全网,官方:AI诈骗成功率接近100%

防不胜防,10分钟就被AI骗走430万! 这是这两天震惊全网的真实诈骗案件。 据包头警方发布,一公司老板接到朋友的微信视频电话,由于长相和声音确认都是“本人”,他丝毫没有怀疑就把钱打了过去。 结果一问朋友,对方根本不知道此事。这人才知道,原来诈骗者DeepF...
继续阅读 »
防不胜防,10分钟就被AI骗走430万!

这是这两天震惊全网的真实诈骗案件。


据包头警方发布,一公司老板接到朋友的微信视频电话,由于长相和声音确认都是“本人”,他丝毫没有怀疑就把钱打了过去。


图片


结果一问朋友,对方根本不知道此事。这人才知道,原来诈骗者DeepFake了他朋友的面部和声音。


消息一出,直接冲上热搜第一。网友们纷纷表示:离大谱啊!不敢接电话了。


图片


也有人提出质疑:AI这么好训练?这需要掌握个人的大量信息吧。


图片


不过,虽说是看上去离谱的小概率事件,但据相关统计,AI技术新骗局来袭后,诈骗成功率竟接近100%


图片


毕竟连那些直播卖货“杨幂”“迪丽热巴”、B站歌手“孙燕姿”“林俊杰”都不是真的。


图片

图源抖音@娱乐日爆社,疑似直播间用杨幂的AI换脸带货

10分钟被AI骗走430万


据微信号平安包头介绍,一个月前,福州市一科技公司法人代表郭某,突然接到好友的微信视频。


聊天过程中,这个“好友”透露,自己朋友在外地投标,需要430万保证金,且公对公账户过账,所以想用郭某公司的账户走一下账。


背景介绍之后,“好友”就找郭某要了银行卡号,而后甩出一张银行转账底单的截图告诉郭某,已经把钱打到了郭某的账户上。


结果因为已经视频聊天以及各种“物证”,郭某并没有过多怀疑,甚至也没去核实钱款是否到账。


几分钟之后郭某就分两笔将钱打了个过去,本想去跟好友报备一下:“事情已经办妥”。


然而,好友缓缓打出来一个问号。


图片


好在郭某反应比较快,第一时间报警。于是在警方和银行联动下,仅用时10分钟就成功拦截了330多万元被骗资金。


有网友表示,AI正成为骗子高手新一代工具。


图片


还有网友调侃道,我没钱,哪个都骗不了我。(等等,好友有钱也不行doge)


图片


而在这起案件背后,核心涉及了AI换脸以及语音合成这两个技术。


大众所熟知的AI换脸方面,现在即便一张2D照片,就能让口型动起来。据此前新华每日电讯消息,合成一个动态视频,成本也仅在2元至10元。


当时涉案嫌疑人表示:“客户”往往是成百上千购买,牟利空间巨大。


至于面向更精准、更实时的大型直播平台,视频实时换脸的全套模型购买价格在3.5万元,而且不存在延迟、也不会有bug。


至于像语音合成方面,技术效果也是越来越逼真,新模型和开源项目涌现。


前段时间,微软新模型VALL·E炸场学术圈:只需3秒,就可以复制任何人的声音,甚至连环境背景音都能模仿。


而具备语音合成功能的工具Bark,更是曾登顶GitHub榜首,除了声色接近真人,还能加入背景噪音和高度拟真的笑声、叹息和哭泣声等。


在各类社交网络上,各种小白教程也层出不穷。


图片


要是结合虚拟摄像头,可能就更加防不胜防。


只需一个软件应用程序,就可以在视频通话中使用任意视频资源。


图片

图源微博@哑巴

点击接通后,对方完全不会看到播放、暂停视频等具体操作,只会看到视频播放的效果,“接通后看到的就是美女了”:


图片

图源微博@哑巴

这样一来,不仅视频可以通过虚拟摄像头,随意拍摄甚至更换,甚至连说话方式都可以真人定制:


图片

图源微博@哑巴

核心技术门槛的降低,也就给了犯罪分子可乘之机。


AI新骗局成功率接近100%


事实上,AI加持下的新型网络诈骗,并不止这一种操作。


无论是国内还是国外,都有不少用AI换脸的诈骗案例,小到在网络购物、兼职刷单等方面骗点小钱,大到冒充客服、投资理财人员等身份,获取银行卡账号密码直接转一大笔账,都有出现。


在国内,据南京警方消息,此前就出现过一起被QQ视频AI诈骗3000元的案例。


当事人小李表示,自己的大学同学小王通过QQ跟自己借3800元,称自己很着急,因为表姐住院了。


小李怀疑了小王的身份,而小王很快给她传来了一个4~5秒左右的动态QQ视频,不仅背景在医院,而且还打了声招呼。


这让小李打消了疑虑并转了3000元,然而随后发现对方已经将她删除拉黑,发现视频原来是AI伪造的。


图片


目前包括北京反诈、武汉市反电信网络诈骗中心等官方公众号平台,都警告了AI技术新骗局的严重性,甚至表示“诈骗成功率接近100%”。


图片


可别以为这些诈骗现象只在国内出现,国外的语音诈骗同样花样百出。


一种方式是用AI合成语音骗取电话转账


据Gizmodo介绍,英国最近就同样发生了一起涉及金额高达22万英镑(折合人民币约192.4万元)的诈骗案件。


一位当地能源公司的CEO,在不知情的情况下被骗子“DeepFake”了自己的声音。随后,骗子用他这段声音,电话转账了22万英镑到自己的匈牙利账户。


据CEO表示,他后来自己听到这段AI合成语音时都惊讶了,因为这段语音不仅模仿出了他平时说话的语调,甚至还带有一点他的口癖,有点像是某种“微妙的德国口音”。


另一种则是用合成语音冒充亲友身份


据nbc15报道,美国一位名叫Jennifer DeStefano的母亲,最近接到了一个自称是“绑匪”的诈骗电话,对方称劫持了她15岁的女儿,要求这位母亲交出100万美元的赎金。


电话那头传来了女儿的“呼救声”,不仅声音、就连哭声都非常相似。幸运的是她的丈夫及时证明了女儿是安全的,这次诈骗才没能成功。


现在,不仅是诈骗,在AI技术加持下,就连杨幂、迪丽热巴们都在今天引发了热议。


图片


原来,这是商家们想出的新“搞钱之道”,那就是在直播的时候,用AI换脸等技术“Deepfake”一下杨幂、迪丽热巴、Angelababy等明星的脸,这样大家就会误以为是明星本人在带货,从而拉升直播流量。


然而,这类行为目前还不能被直接判定为侵权。据21世纪经济报道,北京云嘉律师事务所律师赵占领表示:



平台对于平台内商家的侵权行为不承担直接侵权的责任,而是否构成帮助侵权,主要是看平台对于商家的侵权行为是否属于明知或应知。


但对于如何判定平台对用户、权利人的投诉是否明知或应知,在一般情况下很难认定。



图片


显然,在AI技术越来越火热的当下,相关法律也还需要进一步完善。


One More Thing


就在昨天夜里,最近火爆全网的“AI孙燕姿”本人,也就是歌手孙燕姿,出来回应了。


她发布了一篇名为《我的AI》的英文版文章,这是她团队的中文翻译全文:


图片


然后,网友们看到后的评论是酱婶的:


图片

图源微信@南方都市报

参考链接:

[1]mp.weixin.qq.com/s/Ije3MyQxN…

[2]mp.weixin.qq.com/s/kcbNlaFe_…

[3]gizmodo.com/deepfake-ai…

[4]http://www.nbc15.com/2023/04/10/…

[5]weibo.com/1796087453/…

[6]weibo.com/1420862042/…


—  —


作者:量子位
来源:juejin.cn/post/7236935835631190077
收起阅读 »

前端调取摄像头并实现拍照功能

web
前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。 tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。 一. window.navigator 你...
继续阅读 »

前言: 最近接到的一个需求十分有意思,设计整体实现了前端仿微信扫一扫的功能。整理了一下思路,做一个分享。


tips: 如果想要实现完整扫一扫的功能,你需要掌握一些前置知识,这次我们先讲如何实现拍照并且保存的功能。


一. window.navigator




  1. 你想调取手机的摄像头,首先你得先检验当前设备是否有摄像设备,window 身上自带了一个 navigator 属性,这个对象有一个叫做 mediaDevices 的属性是我们即将用到的。




  2. 于是我们就可以先设计一个叫做 checkCamera 的函数,用来在页面刚开始加载的时候执行。。

    image.png




  3. 我们先看一下这个对象有哪些方法,你也许会看到下面的场景,会发现这个属性身上只有一个值为 nullondevicechange 属性,不要怕,真正要用的方法其实在它的原型身上。
    image.png




  4. 让我们点开它的原型属性,注意下面这两个方法,这是我们本章节的主角。

    image.png




  5. 我们到这一步只是需要判断当前设备是否有摄像头,我们先调取 enumerateDevices 函数来查看当前媒体设备是否存在。它的返回值是一个 promise 类型,我们直接用 asyncawait 来简化一下代码。
    image.png
    image.png
    从上图可以看出,我的电脑有两个音频设备和一个视频设备,那么我们就可以放下进行下一步了。




二. 获取摄像头




  1. 接下来就需要用到上面提到的第二个函数,navigator.getUserMedia。这个函数接收一个对象作为参数,这个对象可以预设一些值,来作为我们请求摄像头的一些参数。




  2. 这里我们的重点是 facingMode 这个属性,因为我们扫一扫一般都是后置摄像头
    image.png
    当你执行了这个函数以后,你会看到浏览器有如下提示:

    image.png




  3. 于是你高兴的点击了允许,却发现页面没有任何变化。

    image.png




  4. 这里你需要知道,这个函数只是返回了一个媒体流信息给你,你可以这样简单理解刚刚我们干了什么,首先浏览器向手机申请我想用一下摄像头可以吗?在得到了你本人的确认以后,手机将摄像头的数据线递给了浏览器,:“诺,给你。”




  5. 浏览器现在仅仅拿到了一根数据线,然而浏览器不知道需要将这个摄像头渲染到哪里,它不可能自动帮你接上这根线,你需要自己找地方接上这根数据线。




  6. 这里不卖关子,我们需要请到我们的 Video 标签。我没听错吧?那个播放视频的 video 标签?没错,就是原生的 video 标签。




  7. 这里创建一个 video 标签,然后打上 ref 来获取这个元素。

    image.png




  8. 这里的关键点在于将流数据赋值给 video 标签的 srcObject 属性。就好像你拿到了数据线,插到了显示器上。

    (tips: 这里需要特别注意,不是 video.src 而是 video.srcObject 请务必注意)

    image.png




  9. 现在你应该会看到摄像头已经在屏幕上展示了,这里是我用电脑前置摄像头录制的一段视频做成了gif。(脉动请给我打钱,哼)
    camera.gif




三. 截取当前画面




  1. 这里我随手写了一个按钮当作拍摄键,接下来我们将实现点击这个按钮截取当前画面。

    image.png




  2. 这里你需要知道一个前提,虽然我们现在看到的视频是连贯的,但其实在浏览器渲染的时候,它其实是一帧一帧渲染的。就像宫崎骏有些动漫一样,是一张一张手写画。




  3. 让我们打开 Performance 标签卡,记录一下打开掘金首页的过程,可以看到浏览器的整个渲染过程其实也是一帧一帧拼接到一起,才完成了整个页面的渲染。

    11.gif




  4. 知道了这个前提,那么举一反三,我们就可以明白,虽然我们现在已经打开了摄像头,看到的视频好像是在连贯展示,但其实它也是一帧一帧拼到一起的。那现在我们要做的事情就非常明了,当我按下按钮的时候,想办法将 video 标签当前的画面保存下来。




  5. 这里不是特别容易想到,我就直接说答案了,在这个场景,我们需要用到 canvas 的一些能力。不要害怕,我目前对 canvas 的使用也不是特别熟练,今天也不会用到特别复杂的功能。




  6. 首先创建一个空白的 canvas 元素,元素的宽高设置为和 video 标签一致。

    image.png




  7. 接下来是重点: 我们需要用到 canvasgetContext 方法,先别着急头晕,这里你只需要知道,它接受一个字符串 "2d" 作为参数就行了,它会把这个画布的上下文返回给你。
    tips 如果这里还不清楚上下文的概念,也不用担心,这里你就简单理解为把这个 canvas 这个元素加工了一下,帮你在它身上添加了一些新的方法而已。)
    image.png




  8. 在这个 ctx 对象身上,我们只需要用到一个 drawImage 方法即可,不需要关心其它属性。

    image.png




  9. 感觉参数有点多?没关系,我们再精简一下,我们只需要考虑第二个用法,也就是5参数的写法。(sx,sy 是做裁切用到的,本文用不到,感兴趣可以自行了解。)

    image.png




  10. 这里先简单解释一下 dxdy 是什么意思。在 canvas 里也存在一个看不见的坐标系,起点也是左上角。设想你想在一个 HTMLbody 元素里写一个距离左边距离 100px 距离顶部 100px的画面,是不是得写 margin-left:100px margin-top:100px 这样的代码?没错,这里的 dydx 也是同样的道理。

    image.png




  11. 我们再看 dwidth,和 dheight,从这个名字你就能才出来,肯定和我们将要在画笔里画画的元素的宽度和高度有关,是的,你猜的没错,它就好像你设置一个 div 元素的高度和宽度一样,代表着你将在画布上画的截图的宽高属性。




  12. 现在只剩下第一个参数还没解释,这里直接说答案,我们可以直接将 video 标签填进去,ctx 会自动将当前 video 标签的这一帧画面填写进去。现在按钮的代码应该是这个样子。


    function shoot() {
    if (!videoEl.value || !wrapper.value) return;
    const canvas = document.createElement("canvas");
    canvas.width = videoEl.value.videoWidth;
    canvas.height = videoEl.value.videoHeight;
    //拿到 canvas 上下文对象
    const ctx = canvas.getContext("2d");
    ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
    wrapper.value.appendChild(canvas);//将 canvas 投到页面上
    }



  13. 测试一下效果。

    112.gif




四. 源码


<script lang="ts" setup>
import { ref, onMounted } from "vue";

const wrapper = ref<HTMLDivElement>();
const videoEl = ref<HTMLVideoElement>();

async function checkCamera() {
const navigator = window.navigator.mediaDevices;
const devices = await navigator.enumerateDevices();
if (devices) {
const stream = await navigator.getUserMedia({
audio: false,
video: {
width: 300,
height: 300,
// facingMode: { exact: "environment" }, //强制后置摄像头
facingMode: "user", //前置摄像头
},
});
if (!videoEl.value) return;

videoEl.value.srcObject = stream;
videoEl.value.play();
}
}

function shoot() {
if (!videoEl.value || !wrapper.value) return;
const canvas = document.createElement("canvas");
canvas.width = videoEl.value.videoWidth;
canvas.height = videoEl.value.videoHeight;
//拿到 canvas 上下文对象
const ctx = canvas.getContext("2d");
ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
wrapper.value.appendChild(canvas);
}

onMounted(() => {
checkCamera();
});
</script>
<template>
<div ref="wrapper" class="w-full h-full bg-red flex flex-col items-center">
<video ref="videoEl" />
<div
@click="shoot"
class="w-100px leading-100px text-center bg-black text-30px"
>
拍摄
</div>
</div>
</template>



五. 总结


实现拍照的整体思路其实很简单,仅仅需要了解到视频其实也是一帧一帧画面构成的,而 canvas 恰好有捕捉当前帧的能力。


预告:在下一篇会讲解如何实现扫一扫的功能,需要用到插件,感兴趣的同学可以先预习一下功课。🎁二维码扫码插件


趁热打铁🧭:前端实现微信扫一扫的思路


作者:韩振方
来源:juejin.cn/post/7289662055183597603
收起阅读 »

前端实现微信扫一扫的思路

web
前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。 tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需...
继续阅读 »

前言: 在有了获取手机摄像头权限并且记录当前画面的前置知识以后,我们现在已经可以进行下一步实现一个仿微信扫一扫的功能了。


tips: 如果你是直接看的本文,对如何打开摄像头拍照这个功能还不太熟悉,请移步 🎁前端如何打开摄像头拍照。这是你阅读本篇的必需前置知识。


一. 效果预览


这里先简单放一下整体界面效果,接下来带大家一步一步分析其中的功能如何实现。


2.gif


本篇将重点讲解多张二维码识别的处理场景。


二. 简单了解二维码




  1. 现在流行使用的二维码是 qrcode,其中 qr 两个字母其实就是 quick response 的缩写,简单来说就是快速响应的意思。三个角用来定位,黑点表示二进制的1,白色点代表0。(这里感兴趣可以自行了解) 它的本质其实就是将一个地址链接利用某种约定好的规则隐藏到一个图片当中,

    image.png




  2. 我们可以利用 chrome 自带的创建当前网站二维码的功能快速体验一下。
    qr.gif




  3. 你可以用手机自带的二维码扫码软件扫一下这个二维码,它将会将你引导到我掘金的个人主页。

    qrcode_juejin.cn.png




  4. 细心的你可能会发现二维码下面已经给你提示了你准备保存的链接地址,现在你观察一下浏览器地址栏是否正对应下面这个地址呢?
    image.png




三. 实现扫码本地图片功能




  1. 我们不需要深入了解这个二维码的转换规则,我们可以直接选用现有的插件即可完成识别功能。 这里我们选用 antfu 大佬的轮子。这里我们不过多介绍,你只需要它可以识别出图片中的二维码即可。如果感兴趣,这是具体仓库地址 qr-sanner-wechat




  2. 首先安装 npm i qr-scanner-wechat




  3. 它的使用方法也十分简单,这个依赖导出了一个方法,我们直接引入这个方法即 import { scan } from 'qr-scanner-wechat




  4. 这个函数可以接收一个 image 元素或者 canvas 元素作为参数,并且返回一个 promise 类型的值。




  5. 我们先来测试最简单的,传入一个 image 元素,利用 input 标签的 type=file 属性,就可以从本地选择图片,比较基础的知识,不过多赘述,代码如下。


    function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    console.log("inputEl.files", inputEl.files);
    }



    然后我们可以通过 input 元素绑定的 onChange 回调中拿到 input 元素身上的 files 属性,就可以获取到刚刚我们选择的文件信息。

    ee.gif




  6. 但是目前这个数据对象我们还无法使用,需要借助 URL.createObjectUrl 方法来创建一个普通的 url 地址。

    image.png




  7. 当拿到这个 url 地址以后该如何使用呢?🤔
    image.png




  8. 一个熟悉的老朋友,有请 img 标签出场,👏,我们只需要将 img 标签的 src 替换成刚刚保存的 url 地址即可。

    image.png

    现在整体效果应该是这样的:

    code.gif




  9. 有了 img 元素,我们直接将这个元素赋值给 qr-scanner-wechat 插件提供的 scan 函数即可。

    image.png




  10. 我们来测试一下整体流程。

    qw.gif




  11. 可以看到,scan 函数返回了一个对象,这个对象身上有两个十分重要的属性。一个叫做 rect (rectangle 长方形的单词缩写),这个属性描述了这个插件在什么位置扫描到了二维码,另外一个属性就是 text,也就是这个图片上隐藏的字符串地址

    image.png




  12. 这里我们再讲解一下 rect 属性,因为后面的功能需要你对这个属性有比较清晰的理解。我们对比一个现实世界的例子。当你掏出手机扫描二维码的时候,往往并不会正好对准一个二维码的图片,或者会遇到一个图片中存在两个二维码的情况,如下图:

    image.png




  13. 这个 qr-scanner 插件会帮你把二维码所在整张图片的相对位置告诉你,因为这个插件每次调用 scan 函数只会返回一次结果。并不是说图片上有两个二维码,它的识别结果就会有两个,所以说这个 qr-scanner 插件的识别效果也并不是百分之一百准确的。




四. 理清思路




  1. 说了这么多,那么这个 rect 我们该如何利用起来呢?别着急,我们先理清思路再动手写代码,到了目前这一步会出现两种情况。

    image.png




  2. 第一种是图片压根就没有二维码,这个简单,提示用户重新放置图片即可。




  3. 关键点就在于第二张情况,当图片上扫码到存在一个二维码后,我们该如何判断是否存在第二个或多个维码呢?

    image.png




  4. 我们看一下微信的实现效果,当只有一张二维码的时候,它会直接跳转,当有多个二维码的时候,它会将整个页面暂停,并且提示用户有两张二维码,请点击选择一个进行跳转。

    image.png




  5. 但是我们上面提到了,scan 函数每次只会扫描一次图片,返回一个识别结果,它并不能准确知道图片上到底有几个二维码。那放到现实生活我们会怎么做呢?




  6. 举个例子,假如我们现在掏出手机扫一扫的功能,现在给你的图片上有两个二维码,但是我明确的知道我就想扫第二个,你会怎么做?

    image.png




  7. 这不是很简单的道理吗?我拿手挡住第一个二维码不就可以了吗?

    image.png




  8. 那么利用同样的思路,我们可以再扫描到一张二维码的时候,想办法把当前识别到的这个二维码位置给遮挡住,然后将被遮挡后的照片传递给 scan 函数再次扫描。




  9. 那么整个过程就是,我们首先将完整的照片传给 scan,然后 scan 觉得第一张二维码比较帅,就先识别了它。(tips: 这里需要提醒一下,scan 有时候会觉得第二张二维码比较帅,那我就识别第二张二维码,要注意的它的顺序性是随机的)

    image.png




  10. 然后我们想办法盖上遮挡物,然后将这个图片传给 scan,让它再次确认是否有第二个二维码。

    image.png




  11. 在哪覆盖?还记不记 rect 属性保留有这个二维码的位置信息?现在的问题就转变为如何覆盖了?




  12. 这里需要用到 canvas 元素的一丢丢基础知识,这是 mdn canvas 基础知识的介绍,十分简单的就画出了一个绿色长方体。

    image.png

    ctx.filleRect可以接收四个参数,分别是相对于画布起始轴的 xy 的距离。

    简单来讲就可以理解为每一个 canvas 就相当于一个独立的 HTML 文件,也有自己的独立坐标系系统,x,y 就相当于 margin,至于后面两个参数,其实就代表着你要画的长方形宽度高度
    image.png




13.那这不巧了吗,scan 的返回值 rect 恰好就有这几个值。

image.png



  1. 话不多说,马上开始实践。⛽️


五. 处理存在多张二维码的图片




  1. 注意: 以下内容我统一选用从本地照片上传作为演示,从摄像头获取图片是同样的道理,详细介绍请移步 🎁前端如何打开摄像头拍照。在下面的讲解过程,我会默认你已经阅读了前置知识。




  2. 这里我就继续沿用之前提到的图片,我将他们拼接到了一张图片上。

    二.png




  3. 下面应该是你目前从本地选择二维码图片识别的代码。


    async function getImageFromLocal(e: Event) {
    const inputEl = e.target as HTMLInputElement;
    if (!inputEl) return;
    if (!inputEl.files?.length) return;
    const image = inputEl.files[0];
    const url = URL.createObjectURL(image);
    src.value = url;
    const result = await scan(imgEl.value!);
    console.log("result", result);
    }



  4. 接下来我们需要先创建一个 canvas 来将当前的照片拷贝到画布上,然后准备利用得到的 rect 信息在这个 canvas 元素上绘画。

    image.png




  5. 为了方便展示,我调用 appendChildren 方法将这个 canvas 元素打印到了界面上。

    1.gif




  6. 然后用 resultrect坐标宽度信息,去调用我们提到的 canvasfillStyle fillRect 方法。

    image.png

    下面是目前实现的效果:

    1.gif




  7. 注意scan 函数不仅仅可以接受 imgElment 作为扫描的参数,它还可以接受 canvas 元素作为扫描的参数。聪明的你看到这里,或许已经猜到我们下一步准备做什么了。




  8. 那么此时我们就可以将这个已经被黑色涂鸦覆盖过的 canvas 进行二次扫描。(暂时不要考虑代码的优雅性,这里只是更清晰的说明我们在干什么,之后我们会封装几个方法,然后整理一下代码)

    image.png

    让我们再看一下效果:

    2.gif




  9. 通过多次重复上面的操作,就可以将图片上所有的二维码都尽量识别出来。

    image.png

    现在实现的效果:

    11.gif

    同时图片上相对应的识别内容也全都被正确的被获取到了。

    image.png




  10. 此时我们创建一个 Map 来保存这些数据。Mapkey 就是 text ,对应的 value 就是 rect 坐标信息。

    image.png

    image.png




六. 弹出可以点击的小蓝块




  1. 有了坐标信息和位置信息,并且我们的 canvasimg 元素的坐标轴系统是一一对应的,那么我们就可以写一个函数来遍历这个 resultMap,然后根据位置信息在 img 元素所在的 div 上打印出我们想要的样式。




  2. 首先在 img 元素外面包一层 div,打上 ref 叫做 imgWrapper 。因为之后我们要用它当作小蓝块的定位元素,所以先设置 position:relative

    image.png




  3. 绘画代码如下,都是基础的方法,不再过多赘述。


    //多个二维码时添加动态小蓝点
    function draw() {
    resultMap.forEach((rect, link) => {
    if (!imgWrapper.value) return;
    const dom = document.createElement("div");
    const { x, y, width, height } = rect;
    const _x = (x || 0) + width / 2 - 20;
    const _y = (y || 0) + height / 2 - 20;
    dom.className = "blue-chunk";
    dom.style.width = "40px";
    dom.style.height = "40px";
    dom.style.background = "#2ec1cc";
    dom.style.position = "absolute";
    dom.style.zIndex = "9999999";
    dom.style.top = _y + "px";
    dom.style.left = _x + "px";
    dom.style.color = "#fff";
    dom.style.textAlign = "center";
    dom.style.borderRadius = "100px";
    dom.style.borderBlockColor = "#fff";
    dom.style.borderColor = "unset";
    dom.style.borderRightStyle = "solid";
    dom.style.borderWidth = "3px";
    dom.style.animation = "scale-animation 2s infinite";
    dom.addEventListener("click", () => {
    console.log(link);
    });
    imgWrapper.value.appendChild(dom);
    });
    }



  4. 然后再 for 循环以后开始绘画小蓝块。

    image.png




  5. 让我们预览一下现在的效果:

    112.gif




  6. 让我们测试一下相对应的点击事件

    3.gif




七. 源码





八.总结


本篇文章的关键点就是讲解了我在实现处理多张二维码的场景时的思路,利用 canvas 遮挡识别过的二维码这个思路是pbk-bin大佬最先想到的,在实现这个需求以后还是很感叹这个思路的巧妙。👏


再次特别感谢pbk-bin🎁~


如果文章对你有帮助,不妨赠人玫瑰,手有余香,预计将会在下篇更新较为完整的微信扫一扫界面和功能。


作者:韩振方
来源:juejin.cn/post/7290813210276724771
收起阅读 »