注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

分页合理化是什么?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 只要是干过后台系统的同学应该都做过分页查询吧,前端发送带有页码(pageNum)和每页显示数量(pa...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


只要是干过后台系统的同学应该都做过分页查询吧,前端发送带有页码(pageNum)和每页显示数量(pageSize)的请求,后端根据这些参数来提取并返回相应的数据集。在SpringBoot框架中,经常会使用Mybatis+PageHelper的方式实现这个功能。


但大家可能对分页合理化这个词有点儿陌生,不过应该都遇到过因为它产生的问题。这些问题不会触发明显的错误,所以大家一般都忽视了这个问题。那么啥是分页合理化,我来举几个例子:



它的定义:分页合理化通常是指后端在处理分页请求时会自动校正不合理的分页参数,以确保用户始终收到有效的数据响应。



1. 请求页码超出范围:



假设数据库中有100条记录,每页展示10条,那么就应该只有10页数据。如果用户请求第11页,不合理化处理可能会返回一个空的数据集,告诉用户没有更多数据。开启分页合理化后,系统可能会返回第10页的数据(即最后一页的数据),而不是一个空集。



2. 请求页码小于1:



用户请求的页码如果是0或负数,这在分页上下文中是没有意义的。开启分页合理化后,系统会将这种请求的页码调整为1,返回第一页的数据。



3. 请求的数据大小小于1:



如果用户请求的数据大小为0或负数,这也是无效的,因为它意味着用户不希望获取任何数据。开启分页合理化后,系统可能会设置一个默认的页面大小,比如每页显示10条数据。



4. 请求的数据大小不合理:



如果用户请求的数据大小非常大,比如一次请求1000条数据,这可能会给服务器带来不必要的压力。开启分页合理化后,系统可能会限制页面大小的上限,比如最多只允许每页显示100条数据。



二、为啥要设置分页合理化?


其实上面那些问题对于后端来讲很合理,页码和页大小设置不正确查询不出来值难道不合理吗?唯一的问题就是如果一次性查询太多条数据服务器压力确实大,但如果是产品要求的那也没办法呀!
真正让我不得不解决这个问题的原因是前端的一个BUG,这个BUG是啥样的呢?我来给大家描述一下。


1. BUG复现


我们先看看前端的分页组件



前端的这个分页组件大家应该很常见,它需要两个参数:总行数、每页行数。比如说现在总条数是6条,每页展示5条,那么会有2页,没啥问题对吧。



那么,现在我问一个问题:我们切换到第二页,把第二页仅剩的一条数据给删除掉,会出现什么情况?


理想情况:页码自动切换到第1页,并查询第一页的数据;
真实情况:页码切换到了第1页,但是查询不到数据,这明显就是一个BUG!


2. BUG分析


1. 用户切换到第二页,前端发起了请求,如:http://localhost:8080/user/pageQuery?pageNum=2&pageSize=5 ,此时第2页有一条数据;


2. 用户删除第2页的唯一数据后,前端发起查询请求,但还是第2页的查询,因为总数据的变化前端只能通过下一次的查询才能知道,但此时数据查询为空;


3. 虽然第二次查询的数据集为空,但是总条数已经变化了,只剩下5条,前端分页组件根据计算得出只剩下一页,所以自动切换到第1页;



可以看出这个BUG是分页查询的一个临界状态产生的,必现、中低频,属于必须修复的那一类。不过这个BUG想甩给前端,估计不行,因为总条数的变化只有后端知道,必须得后端修了。



三、设置分页合理化


咋一听这个BUG有点儿复杂,但如果你使用的是PageHelper框架,那么修复它非常简单,只需要两行配置
application.ymlapplication.properties中添加


pagehelper.helper-dialect=mysql
pagehelper.reasonable=true

只要加了这两行配置,这个BUG就能解决。因为配置是全局的,如果你只想对单个查询场景生效,那就在设置分页参数的时候,加一个参数,如下:


PageHelper.startPage(pageNumber, pageSize, true);

四、分页合理化配置的原理说明


这个BUG如果要自己解决的话,是不是感觉有点头痛了,但是人家PageHelper早就想到这个问题了,就像游戏开挂一样,一个配置就解决了这个麻烦的问题。
用的时候确实很爽,但是我却有点担心,这个配置现在解决了这个BUG,会不会导致新的BUG呢?如果真的出现了新BUG,我应该怎么做呢?所以我决定研究一下它的基础原理。


在com.github.pagehelper.Page类下,找到了这段核心源码,这段应该就是分页合理化的实现逻辑


// 省略其他代码
public Page<E> setReasonable(Boolean reasonable) {
if (reasonable == null) {
return this;
}
this.reasonable = reasonable;
//分页合理化,针对不合理的页码自动处理
if (this.reasonable && this.pageNum <= 0) {
this.pageNum = 1;
calculateStartAndEndRow();
}
return this;
}
// 省略其他代码

// 省略其他代码
/**
* 计算起止行号
*/

private void calculateStartAndEndRow() {
this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
}
// 省略其他代码

还有一些代码我没贴,比如PageInterceptor#intercept方法,这里我整理了一下它的执行流程图,如下:




看了图解,这套配置还挺清晰的,懂了怎么回事儿,用起来也就放心了。记得刚开始写代码时,啥都希望有人给弄好了,最好是拿来即用。但时间一长,自己修过一堆BUG,才发现只有自己弄明白的代码才靠谱,什么都想亲手来。等真正搞懂了一些底层的东西,才意识到要想造出好东西,得先学会站在巨人的肩膀上。学习嘛,没个头儿!



作者:summo
来源:juejin.cn/post/7316357622847995923
收起阅读 »

这下对阿里java这几条规范有更深理解了

背景 阿里java开发规范是阿里巴巴总结多年来的最佳编程实践,其中每一条规范都经过仔细打磨或踩坑而来,目的是为社区提供一份最佳编程规范,提升代码质量,减少bug。 这基本也是java业界都认可的开发规范,我们团队也是以此规范为基础,在结合实际情况,补充完善。最...
继续阅读 »

背景


阿里java开发规范是阿里巴巴总结多年来的最佳编程实践,其中每一条规范都经过仔细打磨或踩坑而来,目的是为社区提供一份最佳编程规范,提升代码质量,减少bug。

这基本也是java业界都认可的开发规范,我们团队也是以此规范为基础,在结合实际情况,补充完善。最近在团队遇到的几个问题,加深了我对这份开发规范中几个点的理解,下面就一一道来。


日志规约



这条规范说明了,在异常发送记录日志时,要记录案发现场信息和异常堆栈信息,不处理要往上throws,切勿吃掉异常。

堆栈信息比较好理解,就是把整个方法调用链打印出来,方便定位具体是哪个方法出错。而案发现场信息我认为至少要能说明:“谁发生了什么错误”。

例如,哪个uid下单报错了,哪个订单支付失败了,原因是什么。否则满屏打印:“user error”,看到你都无从下手。


在我们这次出现的问题就是有一个feign,调用外部接口报错了,降级打印了不规范日志,导致排查问题花了很多时间。伪代码如下:


	@Slf4j
@Component
class MyClientFallbackFactory implements FallbackFactory<MyClient> {

@Override
public MyClient create(Throwable cause) {
return new MyClient() {
@Override
public Result<DataInfoVo> findDataInfo(Long id) {
log.error("findDataInfo error");
return Result.error(SYS_ERROR);
}
};
}
}

发版后错误日志开始告警,打开kibana看到了满屏了:“findDataInfo error”,然后开始一顿盲查。

因为这个接口本次并没有修改,所以猜测是目标服务出问题,上服务器curl接口,发现调用是正常的。

接着猜测是不是熔断器有问题,熔断后没有恢复,但重启服务后,还是继续报错。开始各种排查,arthas跟踪,最后实在没办法了,还是老老实实把异常打印出来,走发版流程。


log.error("{} findDataInfo error", id, cause);

有了异常堆栈信息就很清晰了,原来是返回参数反序列失败了,接口提供方新增一个不兼容的参数导致反序列失败。(这点在下一个规范还会提到)

可见日志打印不清晰给排查问题带来多大的麻烦,记住:日志一定要打印关键信息,异常要打印堆栈。


二方库依赖



上面提到的返回参数反序列化失败就是枚举造成的,原因是这个接口返回新增一个枚举值,这个枚举值原本返回给前端使用的,没想到还有其它服务也调用了它,最终在反序列化时就报错了,找不到“xxx”枚举值。

比如如下接口,你提交一个不认得的黑色BLACK,就会报反序列错误:


	enum Color {
GREEN, RED
}

@Data
class Test {
private Color color;
}

@PostMapping(value = "/post/info")
public void info(@NotNull Test test) {

}

curl --location 'localhost/post/info' \
--header 'Content-Type: application/json' \
--data '{
"testEnum": "BLACK"
}'


关于这一点我们看下作者孤尽对它的阐述:


这就是我们出问题的场景,提供方新增了一个枚举值,而使用方没有升级,导致错误。可能有的同学说那通知使用方升级不就可以了?是的,但这出现了依赖问题,如果使用方有成百上千个,你会非常头痛。


那又为什么说不要使用枚举作为返回值,而可以作为输入参数呢?

我的理解是:作为枚举的提供者,不得随意新增/修改内容,或者说修改前要同步到所有枚举使用者,让大家知道,否则使用者就可能因为不认识这个枚举而报错,这是不可接受的。

但反过来,枚举提供者是可以将它作为输入参数的,如果调用者传了一个不存在的值就会报错,这是合理的,因为提供者并没有说支持这个值,调用者正常就不应该传递这个值,所以这种报错是合理的。


ORM映射



以下是规范里的说明:

1)增加查询分析器解析成本。

2)增减字段容易与 resultMap 配置不一致。

3)无用字段增加网络消耗,尤其是 text 类型的字段。


这都很好理解,就不过多说明。

在我们开发中,有的同学为了方便,还是使用了select *,一直以来也风平浪静,运行得好好的,直到有一天对该表加了个字段,代码没更新,报错了~,你没看错,代码没动,加个字段程序就报错了。

报错信息如下:



数组越界!问题可以在本地稳定复现,先把程序跑起来,执行 select * 的sql,再add column给表新增一个字段,再次执行相同的sql,报错。



具体原因是我们程序使用了sharding-jdbc做分表(5.1.2版本),它会在服务启动时,加载字段信息缓存,在查询后做字段匹配,出错就在匹配时。

具体代码位置在:com.mysql.cj.protocol.a.MergingColumnDefinitionFactory#createFromFields



这个缓存是跟数据库链接相关的,只有链接失效时,才会重新加载。主要有两个参数和它相关:

spring.shardingsphere.datasource.master.idle-timeout 默认10min

spring.shardingsphere.datasource.master.max-lifetime 默认30min


默认缓存时间都比较长,你只能赶紧重启服务解决,而如果服务数量非常多,又是一个生产事故。

我在sharding sphere github搜了一圈,没有好的处理方案,相关链接如:

github.com/apache/shar…

github.com/apache/shar…


大体意思是如果真想这么做,数据库ddl需要通过sharding proxy,它会负责刷新客户端的缓存,但我们使用的是sharding jdbc模式,那只能老老实实遵循规范,不要select * 了。如果select具体字段,那新增的字段也不会被select出来,和缓存的就能对应上。

那么以后面试除了上面规范说到的,把这一点亲身经历也摆出来,应该可以加分吧。


总结


每条开发规范都有其背后的含义,都是经验总结和踩坑教训,对于团队的开发规范我们都要仔细阅读,严格遵守。可以看到上面每个小问题都可能导致不小的生产事故,保持敬畏之心,大概就是这个意思了吧。


更多分享,欢迎关注我的github:github.com/jmilktea/jt…


作者:jtea
来源:juejin.cn/post/7308277343242944564
收起阅读 »

99年师弟,揭露华为工作的残酷真相

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。 师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研...
继续阅读 »

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。


师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研到本校,但拿到华为offer后,放弃了继续深造,选择直接就业。


聊到当今应届生求职现状时,感慨万千。


我的母校是一所普通211,部分专业比较有特色,在行业属于领先地位,但学校整体名气一般。


记得我们大四毕业时,就业行情还比较好,每个班都有好几个同学进入了华为。读研后,实验室的师兄师姐基本都拿到了华为offer,等我们开始找工作时,大部分同学也都拿了华为offer,华为基本属于大家的保底选择。


师弟去年毕业时,他是专业唯一拿到华为offer的学生。这几年的就业形势,本科生基本找不到什么好工作,除了继续读书深造,没有更好的选择。


短短的几年,社会瞬息万变,就像坐上了一辆过山车,身处其中的人,也只能仅仅抓住车身的杠子,在急速下坡时,不让自己摔下来。


图片


师弟进入华为,原本是一件很开心的事,短短一年,经历职场拷打,再也没有大学时代那份锐气。


华为属于员工人数最多的科技公司之一,每年吸收了大批校招生,但员工离职率也非常高。


师弟反馈,跟他同一批进去的人,很多干了几个月就离开了,他是那一批少数还留下来的人。


刚入职时,每个人都要参加新员工入职培训,那时候大家都很骄傲和自豪,也非常认同企业的价值观和文化。


但当真正的分到项目组开始工作时,发现大家实际工作情况,和价值观上说的,并不不太一样。在基层落地时,价值观更多是挂在墙上的标语。


领导也基本不太关心员工的成长和未来发展,更多是把每一个员工当作生产工具,哪一件工具用的顺手、更能出活儿,就会用谁;还没证明过自己的工具,或者经验匮乏的工具,很有可能会被无情抛弃。


华为的假期还可以,但大家有什么事,都不太敢请假。即便是生病,如果不是非住院不可,也不太会请假;公司支持请假,但大家还是轻伤不下火线,请假会影响出勤系数,各种与之相关的奖金,也会大打折扣。


……


师弟还分享了工作以来其他的一些心得体会,一句话总结:职场很残酷,相比起来,大学轻松太多了。


图片


在我看来,华为还是中国一流的科技公司。大部分人,还是要挤破脑袋才能进得去。


华为有很好的制度,比如员工转岗,门槛还不太高。华为支持不同工种互相转岗,比如,干技术的,有一天不想写代码了,可以转人力,或者转销售。开发太累,也可以转做测试。公司会提供基本的业务培训,还有三个月的考核期,只要达到一定的用人标准,就能转岗成功。


华为还有很好的奖励制度,只要能吃苦,且绩效不太差,能坚持下来,很多人最后都获得了丰厚的回报。比我高一两届的师兄师姐,还有跟我同一届的很多朋友,现在都还在华为,过得还不错。


华为还有比较完善的福利体系。在各地的办公区,远离市区,地价也很便宜,平时在公司会有夜宵、员工餐厅、下午茶和健身房各种福利,也有些地方,会有单身公寓。进入华为后,除了工作,也没什么社交,公司能满足日常的衣食住行。平时挣的钱,基本都能存下来,成家后,在华为附近买房,也基本衣食无忧了。


华为是一家跨国公司,如果是干技术支持或销售,还能享受去各国打卡的福利。在年轻的时候,能去更远的地方走一走,也是一件很宝贵的人生经历。


图片


华为是中国众多私企大厂的一个缩影,也是最早践行狼性文化的一家公司。


如今的字节、阿里等互联网企业,也在走华为的老路。


这些大厂,给了年轻人一个令人向往的机会,但并不适合每个人。如果我们能早些看清现实,自己只是大厂的一颗螺丝钉,需要做的是,尽快掌握螺丝钉需要具备的技能,与此同时,如果借助大厂的势能,多开眼界,多认识一些各个领域的前辈和牛人,把企业能提供的资源用到极致,就更了不起了。


至于在公司里寻找朋友、寻找归属感或者安全感,那并不是一个好选择。同事间的八卦,公司股价的涨跌,CEO的致辞,也并不是我们需要关心的事。


最重要的是,无论在大厂还是小厂,在国企还是私企,都要拥有良好的心态。最怕的是,在国企羡慕华为的工资;在大厂,又羡慕国企的朝九晚五。


人生在世,各有各的不易。每一种选择,都没有对错, 选择了其中一个,在另外一部分,就得有所取舍。


在任何时候,学会“自洽”,与生活和解,与自己和解很重要。


作者:工程师酷里
来源:juejin.cn/post/7316349124600168484
收起阅读 »

一个简单截图API,月收入2千美金

大家好,我是程序员凌览。又看到一个独立开发变现的故事,来分享下。 以下是变现分享内容: 大家好,我是Dmytro Krasun, screenshotone的创始人,这是一个简单的API,可以将任何网站的URL转换为网页截图。 人们经常惊讶于你可以通过自动截...
继续阅读 »

大家好,我是程序员凌览。又看到一个独立开发变现的故事,来分享下。


以下是变现分享内容:


大家好,我是Dmytro Krasun, screenshotone的创始人,这是一个简单的API,可以将任何网站的URL转换为网页截图。



人们经常惊讶于你可以通过自动截图网站来建立一个业务。如果你想在你的应用程序中显示任何网站的截图,那么就需要我的产品。


我才刚刚开始,但已经成功地将我的“小”产品发展到月收入2200美元。



你是如何想到这个主意的?


在开始我现在的产品之前,我是一个服务器端开发人员,工作了大约10年,薪水还不错。那时我写了15年的代码。

我有一些业余项目,我想尝试成为一名企业家。但在我工作的时候很难发布任何东西。我总是完全专注于我的日常工作,这是优先考虑的事情。这可能就是为什么我成长得很快,赚了不少钱。


一个机会几乎是“偶然”出现的。我家有了一个新宝宝,我休陪产假,我想这可能是一个好时机,我可以把时间花在抚养孩子、建立和启动一个项目上。这是一个艰难的决定。


最终,我决定毫无计划地跳进未知的世界。可能,因为我只是疯了,这“感觉”像一个有趣的冒险。


我是一名服务器端开发人员,在构建api方面有着丰富的经验。我坐下来写下我遇到的所有问题。我选择了一个随机截图API。我决定在运行中验证它,看看是否有需求。


然后我开始建造它。我谷歌了一下,发现有很多竞争对手,而且都是不错的。一开始我很失望。但后来我意识到,这意味着市场已经得到了验证,我只需要在细分市场打造出最好的产品。


请向我们介绍构建第一个版本的过程


在2022年1月5日买了一个域名,建立了一个简单的网站,开始写与我未来产品相关的内容。看看我的第一个丑陋版本的登陆页:



我写下了我在构建产品时遇到的任何问题,以及如何通过使用我的产品来更快更便宜地解决这些问题。


例如,我的截图API允许你在渲染截图时隐藏广告和cookie横幅,所以我写了如何自己免费做到这一点,并推荐我的产品作为替代方案。事实证明,这是一种推动转化率的有效方法。


seo优先的方法帮助了我,5个月后,当我发布产品时,我已经有了流量,我可以利用它来盈利。


我开始构建一个仪表盘应用程序,让潜在客户可以试用产品、查看使用历史、升级计划和配置通知。它是由Go渲染的纯HTML、CSS和JavaScript。我是一名服务器端开发人员,不知道如何使用现代JavaScript框架。


我将产品的第一个版本托管在Digital Ocean droplet上,然后当我开始增长时,我开始使用Render,然后需要更多的弹性服务器,并转移到谷歌云平台上。一周前,我启动了一个新的Kubernetes集群,以减少我在Google cloud Platform上的成本使用,它已经运行良好。


描述创业的过程


当我的产品的第一个版本准备好时,那是在2022年5月底,我已经从谷歌获得了一些相关流量,因为在发布之前创建了内容。但这还不足以促成销售。在Reddit, Indie Hackers,论坛,目录和Twitter上写文章。

最后,我总结了我在Indie Hackers的第一个营销月所做的一切。这是当月最热门的帖子,并迅速传播开来。


我几乎要放弃了,我累了。最终,在2022年7月4日,我迎来了第一位付费客户。我永远不会忘记这一点。我在Twitter上的朋友Jannis正在为创作者创建一个工具目录,并希望自动截图工具。


如果我今天重新开始,我不会从SEO开始。我会快速构建一个带有付费链接的原型,并尝试着将其展示给潜在的感兴趣的人。SEO是一个长期的游戏,它需要一个长期投入的工作。


是什么吸引并留住了客户?


根据我目前对市场营销的理解,没有什么灵丹妙药。你需要找到2-3个可以持续获得客户的渠道。除了实验,没有其他方法可以做到这一点,看看什么有效。


对我来说,搜索引擎优化、谷歌广告和推特都很管用。但我测试了Reddit、Indie Hackers、Twitter Ads、LinkedIn和其他平台。它们都是有效的,但问题是你可以反复从哪一个中获取客户?


我在Indie hacker上分享了我简单的SEO策略——我基本上是快速创建内容,获得流量,分析并更新内容。


对于X (Twitter),在早期,我积极推广我的产品,到处提到它。但感觉很尴尬,所以我就不再那样做了。开始专注于帮助人们,解决他们的问题,回答他们的问题。人们开始知道我在做什么,如何找到我。现在看到别人会主动去推荐我得产品。



我的主要流量来源是Twitter (X)和Google。我在Twitter上有1万名粉丝,这有助于推广我的产品。在搜索引擎优化上付出了很大的努力,现在我得到了回报。我的产品在细分市场中最具竞争力的关键词排名前5位,比如“screenshot API”之类的。


我认为没有捷径可走。无论如何,尝试每一种方法,找到最适合你的方法。


对其他创业者有什么建议吗?



先把你的野心放一边,试着一个SaaS软件或一个简单的应用程序来赚第1美元。一旦赚到钱,试着赚10美元,然后是100美元。不断重复,直到你确定创业真的是你想要的,并且你喜欢所发生的一切,不管结果有多难。


一旦你意识到这是你喜欢做的事情,不要放弃。如果你赚了1美元,10美元,然后100美元,你不放弃,那么一切皆有可能,只有天空才是你的极限。


别听任何人的建议,包括我的。只有你自己决定,如果你最终决定回到一份正常的工作,这是你的生活,你只能活一次,请保持自己的快乐。


"先把你的野心放一边,从赚第1美元开始"


作者:程序员凌览
来源:juejin.cn/post/7315586629308121140
收起阅读 »

Vue 2 最后之舞“天鹅挽歌”

web
大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本&nb...
继续阅读 »

大家好,这里是大家的林语冰。老粉都知道,我们之前已经在《Vue 2 将死》中已经提及 Vue 2 今年年底将全面停止维护,且最终版即将发布,只打补丁,不再增量更新任何新功能。

圣诞节前夕,平安夜之际,Vue 团队正式官宣 —— Vue 2 最后一个补丁版本 Vue@2.7.16 正式发布,版本代号“Swan Song”(天鹅挽歌)。

01-swan.png

地球人都知道,去年 Vue 2 官宣了最后一个次版本 Vue@2.7.x,如今 Vue 2 官宣最后一个补丁版本 Vue@2.7.16,也算是为 Vue 2 的最后之舞画上惊叹号!此去经年,再无 Vue 2。

虽然但是,前端踏足之地,Vue 亦生生不息,此乃“Vue 之意志”。故本期《前端翻译计划》一起来重温 Vue@2.7 的官方博客,为 Vue 生态的未来规划未雨绸缪。

00-wall.png

今天我们十分鸡冻地官宣,Vue 2.7(版本代号“火影忍者”)正式发布!

尽管 Vue 3 现在已经是默认版本,但我们特别理解,由于依赖兼容性、浏览器支持的要求或带宽不足无法升级,仍然有一大坨用户被迫滞留在 Vue 2。在 Vue 2.7 中,我们向后移植了 Vue 3 中的某些特色功能,Vue 2 爱好者也可以有福同享。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Vue 2.7 "Naruto" Released

向后移植的功能

  • 组合式 API
  • SFC 
收起阅读 »

2023年终,找寻和回顾我在世界存活过的痕迹

有幸拜读了诸多掘友的年终总结,那些压力、纠结、卷、焦虑,我仅透过文字就感受到了,所以我又写回了上一年的主题了~ 我没再去写什么总结,反省啊,我只想轻松再带着些许俏皮的回顾一下2023年我在世界上存活过的痕迹,欢快些吧,生活总是要活下去的。 有时候,我们健康的活...
继续阅读 »

有幸拜读了诸多掘友的年终总结,那些压力、纠结、卷、焦虑,我仅透过文字就感受到了,所以我又写回了上一年的主题了~


我没再去写什么总结,反省啊,我只想轻松再带着些许俏皮的回顾一下2023年我在世界上存活过的痕迹,欢快些吧,生活总是要活下去的。


有时候,我们健康的活着,我们已经很棒啦


这篇文章我更多的抱着分享生活的想法去写的,不谈及那些重的话题,更像是活在当下的人吧


所以如果你是想要获取一些总结上的经验或者一些思考,可能我这篇年终文并不那么适合你。



博客较长,文字较多,希望朋友你能慢慢的读,犹如生活要慢慢的过


今年有幸在湘江边、珠江边和黄浦江边三地留下足迹,三处不同的场景,三种不同的生活状态,不一样人生阶段的结束和开始。



起伏跌宕的开始😀😟


和朋友一起在长沙度过了2022年最后一天,不知疲倦的在那个五一广场,一群人看着手机,数着秒表,大声呼喊着度过了2022年的最后一刻。


可时至今日,去年一起跨年的朋友,现在已经相隔千里了,而今年多半是在房间里面一个人度过这2023年的最后一刻啦


想到了去年写的那句“去年陪在你身边的人,如今还在你身旁吗?一年过完了,你的生活有变化吗?今年你过得开心吗?
人山人海,矮一丢丢,走进去就出不来啦。


人从众2023的新年时刻
image.pngimage.pngimage.png

回忆起那时的场景,调皮的人们在附近的高楼放起了鞭炮,气球在人们手上释放,一同奔向星空


(可惜这一幕我已经没有原视频啦,现在突然回忆起那个时刻,才发觉时间真的好快好快,一晃而过啊)


说起来,那还是我第一次在长沙看到那么那么多的人,也从没想过一个地方的退场,需要走半小时,打车更别说啦,都直接交通管制啦。


看起来这一切都像是美好的开始啊


但在那一晚的九点,我接到了来自家里的电话,让我抽空回家一趟,看一看最疼爱我的爷爷。


可殊不知,那是我和他的最后一次交谈了。


只愿世间永无病痛”。


长大后,那深夜里的电话🥺


长大后,才知道深夜里的电话,无一例外都是大事。


那是在某晚凌晨4点时接到我父亲打来的电话。


其实看清是我父亲打来时,我就清楚知道,小时候最疼爱我的爷爷可能去向了另外一个世界了。


当时的我真的算是非常平静,开始思考工作相关的事情,准备和领导请假,订高铁票,我以为我真的会非常平静的


stk


图一:等地铁


但当离家越来越近时,我才明白有些情绪是没有办法隐藏住的,有些崩溃就在某一瞬间


image.png

图二:回家的路


我看着我的奶奶流泪,我觉得她好无助啊,那一瞬间,我崩溃了。奶奶陪伴我爷爷一生,却剩她独自去过这余下的时光,(我小时候是和爷爷奶奶一起长大的,一直到我上小学之后,但那之后我每年的寒暑假也都会回到乡下去陪爷爷奶奶,只是不知道从什么时候开始,变得不再那么想要回去了)


奶奶很小就嫁过来了,可以说我爷爷就是她拥有的一切啦,儿女子孙大多在外拼搏,她自己也不愿意去麻烦儿女子孙了,所以说,这余下的日子大都是她一人度过了


有很多时刻,我都不愿去深想,那将是怎样的一个生活。在写这一段的时候,我非常想给她打一个电话,但是我的奶奶她已经非常听不清啦,只有偶尔父亲回去时,通过我父亲的传递才能更好的交流。


所幸在去年爷爷奶奶还算健康时,我回到家里陪他们。那一次我非常认真去和他们促膝交谈,安安静静的听他们说他们当年的故事给我听,而我也有幸做了一份录音,只是日后不知道有没有去倾听的勇气


今年回去我有一些想法了, 比如去给奶奶买一个助听器,再去装一个可以对话的摄像头,这些我终于都可以去付出实践啦,我现在有些期待过年啦


那时的我其实也写下了许多文字,关于病痛、选择、亲情、离世、我的父亲、我的奶奶、亲人、还有村里各位长辈,也有很多很多思考,但可能不那么适合写出来吧


只能说在有那么一刻,我深刻且贴切的感受到了亲情的冷暖和选择的无奈吧


也有那么一瞬间的快乐吧,看着五岁的侄女,开开心心的,不免也有些被感染,彷佛从她的身上,才能看到那些无忧无虑的快乐啊 image.png


小时候日日期盼的长大,长大后才知道长大是这一生最大的苦痛啊


短暂的相聚时间啊 😒


自从进入社会,开始成为一名社畜后,寒暑假的消失,让能待在家里的时间开始变得愈来愈短了。


好像每一年的开始,我们的时间就已经被定好啦


以前的过年,其实最好玩的是过年前的那段时间,家里开始筹备年货,准备各种各样的吃的,熬夜打游戏、看电视,一起嗑瓜子啊,和朋友们一起聚会啊,但现在已经变得越来越少啦。


回归中心,继续聊这个短暂的相聚时间吧,文字可不是拿来抱怨的呀~


今年回去,我也开始从我父亲那接过啦一些活,比如准备年货,包蛋饺等等,还有一些杂食,都交给我来做,在那么一刻感觉自己长大啦


另外每年大家来我家干饭的时候,总是要喝酒,我老爸又喝不了酒,就是我来陪酒,今年我几个弟弟,直接把我喝趴啦,麻啦麻啦


不过也只有在家里可以那么那么放松啦,出门在外,又能有几回这样的时刻呢


返回社畜岗位前,把自己的房间收拾了一番,晒给大伙看看吧


部分小玩偶房间里的书桌
image.pngimage.png

当时租房里面的小书桌,这个小桌子,也陪伴了我将近小一年,2022年的博客大部分是在这个小书桌上敲出来的,也是我看电影、刷剧的小书桌啦~


image.png

轻松愉快的时光 😊


生日、吃饭、按摩、记录生活、离开前的放松时光


清楚的记得,这是我今年第一篇日记朋友圈的开始,写的很长很长,还放了自己的自拍照


顺颂时宜,百事从欢


打游戏&按摩篇


身边有个社牛朋友,快乐有时候会变得很简单啊,笑点也会随之一降再降啊,哈哈。


我俩在游戏城的时候,他拿全部彩-票换了把M24,他说:“这是他绝地求生,丢了所有装备都要捡的枪”,他拿到枪后的第一时间就是看看哪里有小孩子,说这必须得让他们看看,男人至死是少年,哈哈哈。


image.png

观影篇


夜晚一起去按摩的时候,看了电影《人生大事》,借用一句豆瓣的影评:“人生除死无大事,天上的每一颗星星,都是爱过我们的人”。 在今年的年初,我也亲眼看到了一颗星星缓缓上升啊,请各位要早点爱自己所爱的人,人生总是在不经意间就会留下遗憾但是我们就是不要……不要留下太多遗憾啊。 在一切都还来的及的时候,去爱你身边的一切,去尝试生活,去绽放自己的光芒,那就是属于你的人生大事啦


后面还去看啦流浪地球,我能说的是“十分震撼人心,这是一部值得我推荐的电影,如果有可能的话,一定要选择 IMAX 影院,观影体验会更佳,也更能展现这部影片的大制作”。这部真是为数不多让我在朋友圈中写上这么主观的电影观后感的片子啦。


image.png

脱口秀篇


第一次去观看了线下的脱口秀表演,那时候我才知道,原来脱口秀演员,大都数是兼职的。


观看体验的话,具体得看演员玩的是什么样的梗,有优雅的,也有不太优雅的,还有一些内容是16岁以上的朋友们才适合明白的,遇上一个能活跃气氛并且能和观众们互动的表演者的话,体验就会非常好,如果平平淡淡的话,就会显得有点枯燥啦。如果要想深度体验,就要坐第一排,那样乐趣会多很多,不过前提是需要能够接受调侃


(补充:一定要遇到开合适段子的脱口秀表演的演员,有些重口味的,是有点难受的)


哦对啦,还遇到了一位杭州的朋友赠送了一份属于ta的运气王的票和夜宵套餐


image.png

干饭篇 🍻


干饭篇(一),长沙的《一盏灯》,主打一个辣,不过味道还是蛮不错的,哈哈哈


image.png

干饭篇(二)名称忘记啦


这是第二天我生日时,拉着朋友一起去了另外的苍蝇馆子


那天聚会的餐桌旁,有一位同年同月同日生日的伙伴,我的社牛朋友就拉着隔壁桌的朋友一起给我唱生日快乐歌,让我也给人家唱生日歌,我只能说当时我的脚趾在扣地啦


不过也算是有缘认识到新的朋友


image.png

即使许久之后再看到那天的视频和图片,也仍然觉得是有趣和有缘


干饭篇(三)北二楼大排档


接脱口秀篇的夜宵套餐,这家店味道是真不错,值得我推荐。


哈哈哈,来长沙可以去试一下,味道和价格都可以,唯一的缺点就是排队排的太久啦,当时我们两个好像从夜晚八点排到了夜晚十一点,中间去了那个HIB HUB公社酒吧溜达,我们两个去看男孩子跳舞,哈哈哈哈


我是真佩服那会的我们,吃的是真多啊,这我们两个人的吃的量...


image.png

干饭篇(四)烤肉店,店名忘记啦


我们一伙人在一起,主打一个笑点低,啥事都能笑半天,我这种高冷boy直接被同化成搞笑boy


image.png

干饭篇(五)喝酒&夜宵


这个忘记是哪一天啦,只隐约记得,凌晨三点多,一起喝完酒,和朋友一起在街边买烤串和麻辣烫,那时候是真放纵自己啊,现在是吃了晚饭,就坚决不吃东西啦,我要减肥啊啊啊啊


清吧喝酒街边小摊的夜宵
image.pngimage.png

美术馆&看展 🖼


在长沙的时候,一个人去了长沙美术馆—齐白石先生的画展,在馆内看到了曾经语文教科书上那画得惟妙惟肖的“


我本没有艺术天赋,去看展大都是静心养气,瞎逛闲逛,但有些美,它就是美得非常直接,直击你的心灵。


有些画本身就真的非常美,你一眼看上去,就会立马喜欢上,换我也想收藏这些画😃


云海夕阳蚂蚱独身一人
image.pngimage.pngimage.pngimage.png

总的说来,这可能是我这一年来最放松的一个月啦吧,这么吃喝玩乐的原因也是朋友准备离开长沙啦,就聚一聚,在长沙逛一逛,吃一吃。我也非常开心,看到自己有留下这么多的瞬间和记录。




在此时此刻写下这些文字时,我才感觉自己有真实的在这个世界上活过一样。


裸辞 | 年少轻狂或是年少无知吧 🤡


在去年我就有考虑离职,但一直没有行动,主要一方面是能力不足,第二个是没有足够的存款,无法承担裸辞后的风险。


但直到我正式提出离职,其实我也还没有准备好,只是觉得拖得太久啦,不能再内耗下去啦


离开


我最终还是决定离开了我职业生涯中的第一家公司。


image.png

总的来说,其实这家公司各方面还蛮不错的吧,位于长沙、双休、不加班、偶尔也有个下午茶。当然这都是不谈薪资的情况下,哈哈哈。




只言片语聊一聊离开的原因吧。


关于离开,这个想法其实在进入这家公司数周后就有啦。熟悉一家公司所要花费的时间,真的没有想象中那么长,几周到一月,大致就可以把所属团队的技术栈,技术水平、团队氛围、领导性格、团队主要工作方向都了解的差不多。而通过这几点,大致就可以决定或影响你在这个团队中所待的时间啦


对于我这种年少无知的年轻人,那个时候我最看重的是技术水平,如果让我觉得没有技术可以继续学下去啦,就会下意识认为继续待着是没有意义的啦。


但是换到现在来说的话,有几人又能够一直做技术基建工作呢?在小公司又能做什么样的基建呢?另外IT人这么多,大家都卷技术,又能卷赢谁呢?无疑都是为了生存下来罢啦


另外很大程度上是业务推动技术发展,你如果没有十万级的数据,坦白说你日常开发中,真的不会老是去想怎么优化SQL,改数据库配置之类的,没有百万千万级,很少有机会去思考去实践分库分表这些操作,不得不说,平台真的非常重要的。


我现在还记得去年我写到一篇文章,聊到过“团队>领导>个人努力”,优秀的团队,放手让你干的领导和努力的自己




说回这小节的中心吧,我跳出属于自己的那个舒适圈,有以下几点原因吧:


1、年轻气盛。想的是年轻再不出来看看,以后混不下去了,退路都没有啦


2、学习技术。想要学习更多技术相关的知识,薪资可以少,但吃饭的技术不能停滞不前。(不过现在看来,还是蛮天真的,现在是有个班上就行,哈哈哈哈)


3、完整的软件开发流程。想要经历从零到一的项目,项目需求、文档、画图等等,都想要去了解一遍,而不仅仅是开发代码(事实证明,浪浪山外还是浪浪山,只是山大了一点,爬山的人多了一些。)


4、薪资问题。(这个对于当时的我算是问题也不算是关键问题吧,当时真觉得是只要能获得成长,我就觉得非常OK,现在让我选的话,哪家给的多就去哪家啦😂)


心路历程


团队内交流。最开始对职场是陌生的,这个要离职的想法并没有太藏着,团队的老哥都知道我这个想法,当时都挺照顾我,也没谁介意这个问题,后面在真的到离开的时候,老哥们提醒我,下次如果再离职,千万不要声张,悄无声息的就好。后面仔细想想确实是这样,你大大咧咧的说,比较影响公司团队氛围。


与人事交谈。最开始正式知道的我想要离职的第一个人是人事,因为人事和我谈岗位调整的问题,需要增加一个运维的工作给我,附带调整薪资。但那个时候我,我已经想要提离职了,就不想在交接工作时,再附加其他问题。我就提出了近期可能会离职的说法,提了之后,我的身份就变得十分敏感啦。那会人事有事没事就问我,打算什么时候离职啊之类的。现在回想起来是真不应该啊,说了之后导致该调的薪资也没了,也被大领导关注到我要离职的事情啦。


与大领导谈话。人事知道我的想法后,就向上汇报啦(与大领导认识,他是想让我之后接实施的活,后续与他和项目经理一起去到项目上)。他了解完我离职的想法,先是肯定,后谈薪资,谈未来工作,定军心,提出挽留;不过最后我还是婉拒啦,抱着那一丝对大城市的向往跑出来啦吧


与直属领导交谈。最后和直属领导了提出了正式离职的想法,与大领导类似的,也是支持和提出挽留,我都一一拒绝啦,最后只有支持和祝福啦。


正式离职。交接工作、清理账号权限、格式化文件、领导签字、人事签字,拿到离职证明,和团队中的各位伙伴说再见,也是下次再见。


坦白说,当走出公司门,还是有那么一些不舍吧,就感觉以后不会再踏及这个地方,只能是一段回忆的感觉啦。


短暂的轻松快乐刚离职的时候还能的听《蓝莲花》,还能打一打王者。后面真的是...


出远门前,还回了一趟家,和老爸老妈待了几天。


无意中聊起年龄、结婚和生孩子的问题,才发现我家老爸老妈转眼间也老了,白发也开始显眼起来了。上次聊到这个话题还是在朋友家听她谈到相亲的话题,说她的父母五十来岁,年龄再大些,站到台上就不那么美丽啦。


心情也莫名沉重了几分~


那时候想到今年的Flag之一是带着父母出去外面按按摩,洗个脚,在那一刻想的却是今年要多挣点钱,过年回去的时候,陪他们一起去医院好好做个完整的体检,好让他们在之后的岁月中可以平安健康的享享清福。


不过现在看来是都容易啊。


最后的最后也只是回家简单吃了两顿饭,然后就踏上了去远方的旅程啊。


也许我们回家和离家的路总是开心的,可父母都是站着门前望着,一个是望着回来,一个是目送着你的离开。


羊城飘荡 🛫


到啦这边之后,投了差不多两三周之后,情绪心态已经完全不一样啦


那份来时的轻松自在已不再了,虽不至于坐立难安,但也是焦虑丛生啦。


不过认真说起来,在羊城的那段找工作的时间,虽然没找到工作,但说真的其实开心还占了多数,可能还是和那群一起爱搞事的兄弟们。


image.png

坦白说,写出来,我感觉有些小伙伴都不太会相信,在一室一厅的房间里面,最多的时候一起住了五个大兄弟,哈哈哈哈哈哈。


当时都是碰巧辞职或者是想在羊城或者鹏城找个工作,就从最开始的两个人,一步一步的凑到了五个人。


那会可以说整个房间可以说是能利用的都利用上啦,房间里的地板、木板床、沙发都睡着人。


突然想到啦那句“三块是面子,四块是生活。不是小瓶买不起,而是大瓶更有性价比。”


我们则是”不是房租出不起,而是一起住更有性价比“,也感受了一番城中村的生活


image.png

在那间房里我还有另外一个身份,”大厨“,那时候也靠这件事情转移我的注意力,同时也算是造福几个哥们,给他们做了一段时间的饭菜。也只有在做饭的时候,我不用去想找不到工作的焦虑


落地羊城后的第一顿饭我第一次做九转大肠
image.pngimage.png

生活也蛮有意思的,凌晨夜话;失眠拉着大家一起发疯;翻来覆去睡不着时,就要问一嘴谁还没睡,起来聊会天;一起出去玩,逛动物园、博物馆、广州塔、大佛寺、相亲角等;还有夜晚出去散步一起喝蜜雪冰城,哈哈哈哈


逛相亲角还蛮有意思的,以前都是在网上看,真来到线下,看到那一张张如同商品挂在上面的个人介绍,压力顿感庞大,优秀的人真滴是太多啦。


夜晚的小蛮腰大厂
image.pngimage.png

广东省博物馆动物园
image.pngimage.png

明年有机会应该去看一看上海市博物馆。


另外还凑了一波雪王的不倒翁,来到魔都也想继续凑的,但是这边没有卖,遗憾。


image.png

非常感谢有他们,我才能够较为开心的度过了那段十分焦虑的求职时期吧


后来的后来,我因为身边其他兄弟引荐,机缘巧合下来到了魔都;一个则找工作去了新疆;还有一个也留在羊城了,不过已经不再从事编程这一行业啦


真正意义的相隔千里了,只有在年前年尾才能短暂的再相聚了呀




在羊城的时候,机缘巧合下,博客有帮助到一名网友,碰巧也在广州,就线下一起面基啦。


当时他还在中国建设银行实习,一起吃了个便饭,聊了很多,溜达了一圈,看了广州塔,我也有不少收获。


关于这一点我多补充一下,多和行业内的小伙伴们交流,我个人觉得真的是有意义的,大家可能都是做开发,但业务可能不同,技术栈可能不同,背景不同,很多想法也是不一样的,每次交流可以收获一些以往不知道的知识吧。


离开后,已许久未曾联系啦,希望一切安好和顺利!祝福


人这一生就是在不断的遇见和离开,珍惜每一次的相遇,或许就是我们能做到的最棒的事情啦


辗转流离 🚄


为啥说是辗转流离勒,还要听我细细道来啊。


长沙裸辞


从长沙裸辞,我就回了趟家,看啦看我的老爸老妈,在家真的只能待三天。再久,那个经就会念起来啦,哈哈哈。所以三天一到,我就踏上了去往羊城的道路啦。


当时是还没被社会毒打,心情还不错,后面真的是心态一崩再崩😐


现在想一想,我都觉得当时的自己是真勇敢,换现在的我,真的不敢裸辞啦,手上没余钱和找不到工作的焦虑真的太折磨人啦。


羊城飘荡


还是多亏有兄弟照顾,来到这边,有个住的地方,没有太多的经济压力,只要认真投简历,找工作就好。


即使后面投简历投到心态爆炸,也因为有兄弟们一起陪着,让那些焦虑生了又灭了。


如果心里有事情,是可以找一找身边愿意倾听的朋友说一说的,当然不是长期输出负面情绪,而是适当的说一说,憋在心里容易出问题


鹏城溜达


要离开前,去鹏城溜达了一遍,和我的表哥表姐吃了一顿饭。因为离开广深地区,下次再相聚也就只能是过年啦啊。


我姐的小女儿,超级可爱的小侄女~


写到这里的时候,我还想起了当时答应她,下次去要给她带玩具,我还写了备忘录,过年要准备准备啦


image.png

image.png


和我老哥晚上吃夜宵,但没想到的是,这顿生蚝将是我今年吃的最爽的一次🤡


(xxxx,上海夜宵店上的生蚝,真不知道是个什么超级刺客价格,真的该死,我这还是在深圳商场里的夜宵店里面吃的,又大,而且比起上海便宜好多,我真的气死啦)


烧烤鲜甜的生蚝
image.pngimage.png

转战魔都


转战魔都前,我又跑回家躺了两天,你没看错,我又回家躺了两天,其实也是感觉如果来了魔都,可能要等到过年才有机会回去啦【事后确实如此】


然后从湖南出发去魔都~


昂贵的高铁费上海虹桥站来接我的科科ikun之家
image.pngimage.pngimage.pngimage.png

我这个小黑子成功加入ikun之家,开启啦在ikun之家的生活~


来到上海后,重新整备后的书桌:


image.png

工作 | 新的启程 🐱‍💻


转变


来到魔都后,工作方向略有改变,不再是单纯的Java开发吧,没法单纯的说是什么样的工作吧,定位也不够准确。


另外也是目前没什么大的成就,就不太想谈论工作这个话题,明年慢慢去更新更多的文章,会一步一步谈论到现在的工作吧


不过我还是活跃在一线开发岗位上的,最让我苦恼的是垂直领域深度不够,非常烦啊。


但这边的整体团队氛围、管理风格都让人比较放松,工作也相比以前开心许多,有一段时间让我一度找回了开发的乐趣。


来到这边后,有几个方向的提升吧。


1、公司提供了更大的平台,接触到了更多的新事物;


2、负责的事情要比之前多;


3、技术的横向扩展拉得比较广。就是有点大网捞鱼,鱼全部漏掉了的感觉,头大。


4、扁平化的管理,让我更大胆的去做事


总的来说,是非常棒的,我也在努力的将手上的事情去做好,也在寻找工作上的乐趣,而不是麻木的工作


image.png

下班等电梯时的随手拍,剪刀手boy~


因为目前正在岗位上,有些内容是没有办法写出来的~


补充


浅浅的谈论一下求职这个问题


我在去年和今年年初都有求职的经历,只差一年,但感受相差甚远。


求职


去年的情况还只是有些糟糕,但还能约到面试,到处也还在招人。


今年离职出来的时候,可以说是寒气逼人,先说我自己,我是陆陆续续投了一个月,投了1.5k左右,能约到的面试少之又少,贴几个朋友的求职经历:


朋友1,求职前端开发,base:深圳、广州、厦门,boss投递3k+,4个月还没找到份前端开发的工作,最后转行啦;


朋友2,求职Java开发岗位,base:深圳、广州,boss投递1k+,最后找了个外包去了新疆;


朋友3,求职前端开发,base:深圳,找了近3个月,最后转回安卓,上个月才收到一个安卓岗位的offer;


朋友4,求职Java开发,base:成都,有美团短期实习经历、银行6个月的实习经历,在成都找了3个月,上个月才入职新的公司,转向 kotlin 开发啦


上述都是求职初中级开发,年限1-3年




收简历


这边公司有开放过岗位,因此有过收简历的经历,浅谈一下,当时两周左右,看了将近300份的简历,本科生研究生的简历都不少,就我个人看简历的心态变化大致是以下几个阶段:


最开始:每一份简历都看的非常认真。看技术栈、项目经历,比较需求匹配度,后面再看学校如何,判断是否进一步交流;


中间阶段:每天都收,真的太多啦,有时候看都看不完(有工作,可能是每天晚上回到家才看),开始转变成,没有get到需求可能就忽略了


后期:只要基本满足条件,学历优秀的优先谈。不行就下一个。主要会去关注下面几个问题,基础条件是不是满足;相关的技术栈的熟练程度;进行前期沟通,了解性格,初步判断能否融入当前团队;入职后,能不能发挥他应有能力;稳定性等方面的问题;


小小的思考
今年我求过职,同时又收过不少简历,后面就补了这么一个小节。


找工作难,招人也难


说实话,如果没有实际工作经历,看十份简历,有六份简历中的内容基本上大差不差的。


你说怎么选。


以前是想着跳槽涨工资,或者是想找个好工作,今年的想法是能有个b班上就不错啦。


魔都生活 | 出游 | 干饭


在魔都的日子,出门时间其实不算多,工作日的三点一线,休息日的步数100,总的来说,不是在干饭的路上,就是在干饭的桌上


外出篇


大魔都的标志性建筑物--东方明珠塔。


美是真的美啊,也是来到了外滩才深刻感受到金融到底多么挣钱,外滩这边一条街,全部都是xxx银行,感觉没有一栋楼,都不好意思说自己是个大银行啦。


白昼时的东方明珠傍晚时的东方明珠
image.pngimage.png

夜景更美一些,要不是上塔顶的票太贵,我感觉我已经冲啦


豫园


国庆假期时过去溜达的,当时好好的感受了一下上海的City Walk


挹秀楼阁楼
image.pngimage.png

有没有觉得第一张照片中阁楼里的两个闪光灯非常有趣呀~


广富林遗址和醉白池公园


天高云淡时,和室友一起外出骑行,去看看外面的风景~


广富林遗址醉白池含苞待放
image.pngimage.pngimage.png

游泳


和两个小伙伴一起去游泳。不对,他们两才是游泳的,我是去喝盐水,是的,我这个小菜鸡还是不会游泳,哭死。


image.png

不过我对于魔都这座城市知道的还是太少,那么多馆,那么多展,那么多景点,但我是动都不想动啊。


希望明年可以去做更多探索吧,如果有小伙伴一起的话,那就更好啦。


干饭篇 😲


在写这一小节的时候,我想我终于找到我自己反向减肥十斤的原因啦🤡


十天的照片里,八天的照片都是关于吃的… 我自己都不敢相信


同事聚餐、在家做饭、周末小聚、生日聚会、夜宵、外出游玩干饭、疯狂星期四,各种各样的干饭,上演各种碳水炸弹


可是今年原来的的目标要减肥的啊啊啊啊啊啊啊🥺


偶尔周末给自己和室友加餐的日子


干饭加餐篇
image.pngimage.png
image.pngimage.png

与团队中的小伙伴一起在住房里面小聚一下,再加上一点小酒,不免也是一个快乐时光


热时多数是炒菜,转凉后多数是吃火锅,夏天小龙虾和啤酒更配哦,冬天则是牛羊肉更暖身啦


聚会篇繁忙工作里的畅谈和轻松
image.pngimage.png
image.pngimage.png

大声呼喊着减肥,每天和室友一起吃荞麦面,还一起跳绳,但。。。


实际情况是工作日每天吃荞麦面,一到周末就是夜宵、KFC、聚餐,妥妥的反向减肥


image.png

夏天时,自制青柠和百香果的饮料果汁喝,非常爽哦


青柠百香果
image.pngimage.png

(别看我说的这么好,实际上我把所有装备买齐,就做了三次,哈哈哈哈)


外出干饭,聚会~


补充:黄酒初喝不上头,喝多啦该醉还是醉,下次我要坐小孩子那桌去


温暖你的猪小餐馆淞沪名灶和记小菜
image.pngimage.pngimage.pngimage.png

安徽


今年有幸到过两次安徽,一次是去了黄山,一次是去了合肥,尝试了安徽的菜,我觉得也蛮好吃的,可能也是偏向于辣和咸,挺合适我这个湖南人滴


总的来说,感觉内陆省份的菜品都偏向于重油、重盐和重辣,而沿海省份,感觉就是鲜、淡、甜,吃食材的本味~


忘记名称的店脑海中只记得吃啦地锅鸡
image.pngimage.pngimage.png

我们这群人属于是,遇上开心的事情,烧烤配啤酒;碰上不开心的事情,也是烧烤配啤酒!!


开心烧烤配啤酒不开心也是烧烤配啤酒
image.pngimage.png

疯狂星期四


以往都是说段子,后面真是每周都过疯狂星期四,碳水炸弹


image.png

细细想来,我的长胖好像也是有迹可循的,没写出来前,我日常还觉得我自己吃的不多😂


总的来说,在上海的周末做的最多的事情就是干饭啦。


室友篇


从个人生活状态转变到合租生活状态,其实有不少改变的。


不过非常幸运的是,在这里遇到的每一位室友都非常棒,也分别给我带来的不一样的影响


首先第一点,室友们都是大厨,这才有了上述那种每天吃吃喝喝的生活。


如果要是我一个人独居的话,吃饭在我看来更可能一种任务,随便应付一下就算是完事啦,根本不会去想着一荤一素,更不会想着好好做个菜啦,哈哈哈




keke哥哥最开始来到魔都时,我没有另外再出去租房啦,就是和keke哥哥一起睡。因为他有考虑后面要搬出去和女朋友一起住,所以最开始我们就是睡一起的;坦白说,如果他女朋友没来,他最后没搬出去住,我感觉我们两个男生才像谈恋爱的感觉【捂脸】


生活非常同频。今天我买菜做饭,他就洗碗;隔天他买菜做饭,我就洗碗;作息时间同频;有问题会直接说;一起电动车上下班;周末一起去游泳;一起吃KFC;半夜睡不着一起骑电动去吃夜宵;一起养猫;一起打游戏,cf、王者五排;安静互相不打扰,多数时候是一起同行的。有那么片刻,恍惚间有感觉像是在谈恋爱的。


这种生活一直持续到他女朋友到来,他搬出去之后才结束,说起来还是有点怀念啊。


要是谈恋爱的时候也能是这种感觉,可能我现在也不会一直单身着啦吧




wancheng哥哥。这个叫法是因为我某一天打王者,听到一个男生在用那种夹子音叫哥哥,我一下就学会啦,然后就整天在他们的名后面加个哥哥,哈哈哈,最终的结果就是整个房间里,所有人都被我带偏啦~


wancheng哥哥是这里的大厨,要是每次有个什么聚会,肯定就是我们两个搭配掌勺,不过我们两单独某一个人都懒,但要是一起合作去做某件事情,就会非常认真的去做,想要去做好,而不是偷懒。


还有他今年经常带我一起消费,笑麻了。他买机械臂,就会非常真诚的向我安利它,陈述它的一些优点,把我说的心动,然后就是买买买;冬天来啦,他买暖风机,也会非常真诚的说这款产品的优缺点等等,最后我又忍不住去剁手;还有其他的,我自己都被自己笑麻啦。另外大家都偏向于及时享乐,我也有被影响吧,多了很多消费,也比以前果断许多啦。


对啦,最最最重要的一点,他现在不减肥啦。然后他现在是直接往住房里疯狂买零食,放在客厅的桌子上,他知道我一定会去吃,我真的哭死,他真是生怕我瘦下来啦。周末吃完饭还要再点个烤串,减肥减肥,越减越肥~


不过也有带动我共同提高。因为他老是来查我房,来查我在干嘛,是不是在打游戏;如果我是在卷的话,他也会立马回到房间去卷。当然我也会去反向查他的房,他如果不在下棋,我也会立马回到房间卷,坦白说,是有点小孩子行为的。


补充:要非常感谢wancheng哥哥,还有未曾蒙面的阿姨,才让我们在魔都有吃不完的湖南特产,剁辣椒、腊肉、腊牛肉、腊肠、萝卜条、豆鼓,对啦还有扣肉等等,真的太棒啦




另外还有一个我此前并非那么了解的室友-sian哥哥,在今年给过我很多帮助。在和他的诸多交谈中,我是真的受益匪浅,也有重塑他在我心里的印象


在这里暂且称呼他是个游戏佬好啦。在我以前十分局限且带有偏见的认知中,对游戏佬的印象都比较一般。人的专注是有限的,沉迷于某一个事物当中,那么自然对其他事物的关注肯定不会那么高啦。


很长一段时间里,我认为阅读是最容易带给人深度思考的,但是在和他交流的过程中,我发现,其实我们专注做的每件事情,都是值得思考的。


比如他热爱建城类的游戏(请原谅我已经忘记游戏名称了),可能部分玩家是去追求那张掌控城市的爽感的,他在我看来,则是那种享受自己从零建造一切的成就感的。在那么一刻我突然意识到,在游戏中完成一个从零到一的任务,换成我们编程的说法,也就是从零到一完成了一个项目啊。仔细停下来想一想,是不是这样


他做的更好,他将这种从零到一的思维方式,从游戏带到了现实生活中,在和他共事的过程中,我发现他的思考和规划大多是从全局到局部的。


这点我是非常想要学习的,因为我个人的观念,老是非常局限,不管做什么,我第一个想到的,总是站在自己的位置上去思考问题,跳不出去,无法抛开自己的固有思维去思考问题犹如井底之蛙。


另外他是我身边朋友圈里,和我一样,不刷抖音的人,都是b站的深度用户,喜爱那种能够引导人进行深度思考的长视频。


不过话说回来,我感觉我今年比起往年心浮气躁了许多,以前还能心平气和的看完长视频,今年已经要加速啦。


朋友篇


今年有幸在魔都认识到一位非常真诚的朋友,见证过ta的勇敢,也见识到的ta的真诚,也有被ta写的一段话所惊艳到。


“我一生都在探索人性,我知道现实残酷人性凉薄,所以我任愿抱以真诚去对待所有人、事。


我所付出的那些金钱和精力,又比如你说我以后会吃的亏,在我看来远不及一次灵魂碰撞来的重要。


同样真诚的人难能可贵。


这条路艰难,我必定会历经诸多碰壁。


我可以被拒绝可以被推开,我都不在意,但唯独不能不真诚。”


生活状态的转变 🏄‍♂️🤼‍♀️


写完上面那部分内容,我在思考一个问题,今年的生活状态和去年还是有很多不一样的。


我在湖南的时候,多数周末还是会出去走一走;但是在上海,外出的欲望是真的低啊,直接进化成宅男啦


思考了几个原因:


1、交通不便。目前在魔都比较偏的地方吧,附近没有地铁口,只能坐金山铁路


2、外出通勤时间太长。因为没有地铁,出去一次,动辄一个小时、一个半小时,出行欲望极低。来上海近半年,准确来说我只坐过一天地铁,那时候好像还是是火山引擎的一个活动,在市中心那边的一个酒店,那天我一个人过去的。其他时候好像完全没坐过地铁。捂脸,甚至于有时候同事聊到上海几号线几号线,我完全不知道在哪。


其他时候,和室友们一起出去玩的话,要么打车,要么踩单车,要么找公司财务借车。公共交通除了当时坐轨道交通去游泳,好像其他真没坐过啥啦。回想起来,我自己也觉得有点离谱啦


3、从个人居住状态转变到合租状态。开始习惯和与人同行,而非个人独行,以往自己要干嘛,我可能自己想啦就去坐了,当习惯有人同行后,我会下意识问ta愿意一起吗,如果他不愿意,我的意愿会降低。


4、脑子暂停转动😵。一个人住的时候,要做什么,想去干嘛,都是自己在思考,与室友一起住后,思想开始懒惰啦,比如吃饭,总想问一句他想要吃什么,不想自己去动脑子思考啦。


类比到我自己打王者也是一样,一个人单排,我会去想整体局势如何,该怎么打,人在哪,和朋友一起打,我就直接开摆,成为机器人,哈哈哈哈。


我仔细想了想,我并没有因为没有减少外出溜达,而变得不快乐,也没有讨厌宅在家里的自己,我觉得这种生活状态是适应当下的我。




但是我觉得我还是要做出一些适当的改变,如果长期不动自己的脑子去做决定的话,我感觉我会丧失掉自己的主动意愿,之后就更不愿意去思考啦,有些事情,想做就做啦~


《杂乱无章》 🖋


在今年有能力的情况下,这次终于可以好好的支持喜欢了很久的《杂乱无章》啦


购买了一些周边,其实不太贵,就正常的衣物、香水、护手霜、鞋等等。


不过我老是给身边朋友安利,感觉自己已经很久很久没有做过这样的事情啦,其实就是非常单纯的想要分享自己所喜欢的宝藏吧。


一直在等正常装的香水,可惜一直没有货一个人也要穿情侣装
image.pngimage.png

新品护手霜,味道非常好闻的 image.png


我关注《杂乱》,没记错应该是在两年前啦,现在回想起来我已经不记得我为什么会关注到它,但是我记得当时看完第一篇文章就点了关注,然后一口气把当时的历史文章全部看了,就是非常喜欢他们输出来的文字。


喜欢文字的朋友,可能也会喜欢上他们吧。


另外今年,他们在广州开展啦,可惜的是,不在江浙沪地区,不可惜的是,我让一个朋友去逛了这个展。


下面是展馆里的个别片段,希望来年能够来到魔都,不行的话,江浙沪地区就好,哈哈哈


文字
我们都需要适度抽离,谈恋爱要一直要有这个状态才行,偶尔的距离感,会让人舒适许多。image.png
希望你开心,无论和谁,不管在哪image.png
成年人除了容易胖,还容易加班,容易熬夜,还容易在杂乱无章里找到同感,真的太对啦image.png
给自己的生活留一些属于自己的片段image.png
《心情》“过年了!” “又过年了。“image.png

不知道,你有没有在在上述的几幅图中呢?你又是哪种情绪呢?


猫猫相伴的时光 🐱🐱‍👓


养猫,真的是一个极费照片的事情,哈哈哈


我在这之前,很长一段时间都没拍过啥照片啦,后面和室友一起养之后,可以说是,照片以一个非正常量级的速度往上递增,哈哈哈哈


image.png

它的存在,我相信是真的能治愈人的,上班一整天,回到家,看到它,那些愁绪会消散一大半


七月刚回来时,那时候还是娇小可爱勒 image.png


image.png

现在在室友饲养下,已经是胖嘟嘟啦,变得更可爱。 image.png
虽然最后,是让它和室友一起和室友女朋友出去住啦


但最后的效果是好的,它已经成功的胖嘟嘟啦,很开心遇见和陪伴,现在偶尔也能去看一看,也算是非常快乐啦


掘金瞬间


八月Docker技术征文一等奖


不知不觉中在掘金也待了两年多啦,时间真的很快啊,同时也代表着我写网络博客也近两三年啦


用心对待的博客,被掘友们用心认可,真的非常快乐,非常感谢每位阅读过的朋友


让我记录一下这个高光时刻吧~


有在多个平台写过博客,但最让我喜欢的,还是掘金


很多原因吧,有掘金的大方,也有因为掘金而相识的朋友,大家不仅是在掘金上有交流,也是彼此的微信好友,接触的越多,就越习惯这里吧,如果有一天不再玩掘金,那么可能就是这些朋友也在逐步退圈啦吧


image.png

图一:Docker技术专题的实体奖牌


也非常感谢斗金小姐姐第一个来告诉我这个好消息,不知道斗金小姐姐能注意到这句话吗,哈哈


要是其他运营小伙伴看到啦,劳请转达一下下啦,还有记得给来个推荐,哈哈哈


image.png

图二:在奖金的基础上加了些给自己淘来的4k屏幕


后续也有多个活动想参加的,有可能是懒吧,也有可能是心浮气躁,总之今年的博客数量对比起2022、2021年,差之甚远


2023年统计2022年统计2021年统计
image.pngimage.pngimage.png

真的是每年比折半还折半,明年我一定要努力克服🤡🥺


与友人见面 | 人间有味是清欢


因博文相识的朋友,时隔许久,终于有幸在线下见面啦


开始,可能还是要提起掘金,哈哈哈,那会加上好友,还是因为同时在掘金的“神转折滴评审团”中~


image.png

在这之后,其实成为了互相的读者和粉丝,ta的文章内容简短,偏向于日常思考、感悟和读书笔记。


话说ta的日更是我当时早上的摸鱼读物,哈哈哈哈


我个人也偏好去阅读其他人的想法,想了解其他人对于某件事情是如何看待的。


ta的文章也让我在寻找自信以及与自己和解等方面,荣获不少的成长。



去年写到”我们是彼此的正向反馈”,那么今年就是”幸会“。



“杭城,幸会”


人间之幸事,遇良师益友,品人间有味,游山河大地


在这月准备写年终博文时,在回忆在思考,今年有哪件事是想做而未做的,最后才有啦这次的见面和交流~ 很感谢那晚的思考和感性的自己,才有今天关于杭城的人和事的记忆。 或许这趟出行,是略有唐突的,(因为我的到来,让朋友有不小的心理负担,特意规划了去哪里,从那里走到那里,非常细心),辛苦啦,庆幸的是,最后都收获满满~


幸会”,有预想过双方的见面,虽然都感受对方都是极易相处的人,但是也不免有一丝不安吧


但真实情况远比起预想中要好,可能是我因为成功的把我那份社牛属性释放出来啦吧。


当文中那个积极向上、热爱生活的形象真实立体出现在眼前时,有那么片刻的不真实吧,同时也非常激动;另外所期待的聊天,也贯穿于整个同行的过程,十分放松和愉快的一天,不用去这想那的。


期待下一次吧~


回忆起整个交流过程中有些问题我还存惑,不过我已经想好要再次拾起阅读啦


人间有味。一方面是尝了那份老杭州的美食味道,另一方面是品鉴了那份属于杭城的老味道。日常总是忙于工作,已经很少有过沉下心来去感受那些明明离我们很近,但又久久未曾感受的地方啦。


山河大地自然是杭州西湖啦。不过那时未曾记起苏轼先生的诗句,一开口便只能是,wc,好美,哈哈哈(已然决定要去b站学习一下关于东坡居士的历史啦),下一次再踏足时,希望自己能欣赏到另一种更深刻的美。


补充:中间也有过许多搞笑瞬间和尴尬的瞬间 比如下面👇的照片,已然是我摆的比较好的姿势了;不熟悉杭州路况,我老是走神开错道;还有最后找停车的地方,下到车库,都没想起自己车停在哪里,还信誓旦旦走错啦,回忆起来,我真的要被自己的信誓旦旦给整麻啦,现在想想都蠢死啦,还不相信你,麻啦;表情管理更不用说了,直接全放开,现在回头看照片,我真的😁😂🙄😵🤡


标题
image.pngimage.png

西湖边上拿着旁边的小朋友的饼干喂鸳鸯,哈哈


image.png

在我很喜欢的一个公众号《杂乱无章》的一篇推文中,看到了下面的这段话,我觉得是非常契合这次的见面


image.png


如果说见完面之后的想法是什么的话,一方面可能是想要继续去拾起阅读吧,觉得自己言语谈吐仍然有需要进步的地方;另外一方面就是想要改变下自己的仪态吧(在相机中的自己,实在是称的上难看啦,哈哈哈哈)


不管是和哪位好友的见面,我想我们都会永远期待下一次的见面吧


记录生活 | 观影 | 阅读 | 消费


今年总的来说,比起以往,是多了很多记录的,也是想用数字去量化,看看自己的变化。


比如关于看电影、剧,我今年线上线下观影和追剧总的来说是45部吧。有2/3我是写下了观后感的,还有三分之一要么是看完太晚,懒得写,要么纯粹就是懒得写啦。我看的大部分都集中于评分高的经典电影和突然刷到感兴趣的。




关于阅读的话,我今年阅读量降低了很多很多,只有寥寥几本😂


image.png

补充:不过这几本书,我个人觉得还是非常值得阅读的。《认知觉醒》有谈及到很多日常行为,阅读这本书的同时,会下意识的反省自己的行为;《娱乐至死》是新闻专业的考研读物,里面谈到的诸多观点,都非常有意思,而且十分深刻,但是有些话不适合写出来,只能自己知道;我还记得我当时读完这本书,就感觉是读书越多的人,并不是多么厉害,而是对这个世界背后的真实规则了解的越多,这本书,让我对宣传、新闻、娱乐了解的更深刻些了,也明白某些新闻背后的意图吧。新闻真真假假,但如今信三分都算多




关于消费。今年的消费直接上了一个量级,把自己也给吓到了。工作真就是给老板和房东挣钱,钱包该是多空还是多空。不过今年还是有几个大额消费的,主要是下面几个:


1、房租费用。一年下来真的心痛的要死;


2、双十一购物。有不少花销,反正花钱如流水的感觉...


3、一台二手台式电脑+一块4K屏幕


4、给家里打钱,给了妹妹一些生活费


5、个人消费也是真的高;


6、软件费用也贵。总的一算,都1k+啦,麻啦


白吃白喝魔都过一年


补充:详细的消费记录没有写到这里,不然大伙都该知道春春我还差一周,就真是一年穷到头啦。


立下来又总挂掉的Flag


照例回忆下去年年终写下的Flag,可以说是完成的非常糟糕啦


image.png
image.png

很坦白的说,完成度非常低,低到我自己都不愿意去翻看这个事情。


最主要很多不是无法完成,而是我没有去努力完成,这是我最无法接受的事情。


回头一看2023年,真的十分模糊,不知道自己到底做了些什么,恍惚间就又要到2024年啦


今年是这种感觉最强烈的一年,因为真的有过迷茫,找不到努力的方向,也浪费了许许多多时间。


我现在还记得我大学室友在今年12月问我的一句话,“二哥,今年我看你文章也没写多少,朋友圈也没咋更新,你今年到底在忙啥啊?”


我也不知道我忙了啥啊


关于明年


下面是我2024年的一些目标和想法,能不能完成,能完成多少,我心里也没底,反正都想冲一冲,试一试吗,先想啦,才能更好的去做,难道不是吗?


image.png


总的来说,主要是围绕生活和工作方面,其他的我没有再给自己立明确的需求啦,还有一些则是去年就立过啦,我到目前还没有完成,那些想法今年也会接着去做。


对于明年,我对自己只有一个要求,如果一件事情如果我能做好,那就希望自己能尽力去完成它,而不是因为懒惰而一拖再拖。一定一定一定要有执行力。


那么正在读这篇文章的你呢?今年的你想要做什么呢?你有什么样的想法呢?你会去尽力实现它吗?


在写完之后,我就已经开始着手去开始,有些事情想做就去做吧,没有必要一定要等到某个特殊的时间点之后,时间是在自己手上,而不是在日历上


长路修远,吾与子之所共适也


每每想到自己已将要岁至二十余四,不免有些恍惚,有些时候,在我自己看来,感觉自己还非常小,完全没长大,很多事情甚至都还没有去考虑过。


但看到身边的朋友,谈恋爱的谈恋爱,买车的买车,结婚的结婚,另外偶尔再加上家里边的催促,焦虑一下就放大了,莫名坐立难安


但其实大家都一样的,别慌,我先来自黑一下。


我呢,一年工资那么点,给房东一大把,自己吃喝玩乐一大把,购物一大把;约等于白吃白喝在魔都过一年呢~


技术呢,说啥都会点也是,但要说会多少,也就是啥都会一点,问多一句,就直接是啥都不会啦。


该努力就努力,该生活就生活,做好自己该做的事情;那么多的信息流,分辨不了,就少看点;你就是你,想那么多干嘛,想那么多也得烦恼今天晚上吃什么


无论如何,人生这条道路,仍然漫长而修远,我希望我们能一同享受这短短数十载。


那么这篇文章就写到这里啦。


如果你读到这里,如果你也喜欢的话,请说说你的感受吧,我真的非常想要收到来自于你的反馈,无论如何我都会认真的一一回复的,有问题也可以提出来,我也会一一回答~



2023年,我是宁在春,我在上海


那2024年,平安喜乐,万事胜意,祝你,祝我,祝我们



作者:宁在春
来源:juejin.cn/post/7317091895171153920
收起阅读 »

前端部署真的不简单

web
现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下: 首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹; 最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,...
继续阅读 »

现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下:


首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹;


最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台一起部署;现在普遍是前后端分开部署,因此,利用nginx起一个web服务器,将dist文件夹放到指定的路径下,配置下nginx访问路径,对于请求接口使用proxy_pass进行转发,解决跨域的问题。


更加高端一点的操作,是利用CI/CD + Docker进行自动化部署。


但是,你是否真的想过前端部署真的就这么简单吗?


这其实是一个非常严肃且复杂的问题,因为这关系到线上生产环境的稳定


有一天,从自知乎上看到一篇张云龙大佬在2014年写的文章,非常有启发,即使这篇文章距离现在有快10年了,但是其中的思想仍然熠熠生辉。


因为写的真的是太好了,为了让更多的人看到,所以大部分内容直接就照搬过来,为了让自己加深印象。如果想看原文,原文网址在这里。


那让我们从原始的前端开发讲起。


下图是一个 index.html 页面和它的样式文件 a.css,无需编译,本地预览,丢到服务器,等待用户访问。


image.png


哇,前端这么简单,门槛好低啊。这也是前端有太多人涌入进来的原因。


接着,我们访问页面,看到效果,再查看一下网络请求,200!不错,太完美了!


image.png


那么,研发完成。。。。了么?


等等,这还没完呢!


对于像 BAT 这种公司来说,那些变态的访问量和性能指标,将会让前端一点也不好玩。


看看那个 a.css 的请求,如果每次用户访问页面都要加载,是不是很影响性能,很浪费带宽啊,我们希望最好这样:


image.png


利用304,让浏览器使用本地缓存。


但,这样也就够了吗?


不够!


304叫协商缓存,这玩意还是要和服务器通信一次,我们的优化级别是变态级,所以必须彻底灭掉这个请求,要变成这样:


image.png


强制浏览器使用本地缓存(cache-control/expires),不要和服务器通信。


好了,请求方面的优化已经达到变态级别,那问题来了:你都不让浏览器发资源请求了,这缓存咋更新


很好,相信有人想到了办法:通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源


像这样:


image.png


下次上线,把链接地址改成新的版本,这就更新资源了。


问题解决了么?当然没有,思考这种情况:


image.png


页面引用了3个 css 文件,而某次上线只改了其中的a.css,如果所有链接都更新版本,就会导致b.cssc.css的缓存也失效,那岂不是又有浪费了?


不难发现,要解决这种问题,必须让url的修改与文件内容关联,也就是说,只有文件内容变化,才会导致相应url的变更,从而实现文件级别的精确缓存控制


什么东西与文件内容相关呢?


我们会很自然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种可以精确到单个文件粒度的缓存控制依据了。


OK,那我们把 url 改成带摘要信息的:


image.png


这回再有文件修改,就只更新那个文件对应的 url 了,想到这里貌似很完美了。你觉得这就够了么?


图样图森破!


现代互联网企业,为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径:


image.png


好了,当我要更新静态资源的时候,同时也会更新 html 中的引用吧,就好像这样:


image.png


这次发布,同时改了页面结构和样式,也更新了静态资源对应的url地址。现在重点来了,现在要发布代码上线,亲爱的前端研发同学,你来告诉我,咱们是先上线页面,还是先上线静态资源



这里的静态资源不仅仅包括css文件,也包括图片,以及不怎么经常变的资源。




  1. 先部署动态页面,再部署静态资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。

  2. 先部署静态资源,再部署动态页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。


好的,上面一坨分析想说的就是:先部署谁都不成!都会导致部署过程中发生页面错乱的问题。


所以,访问量不大的项目,可以让研发同学苦逼一把,等到半夜偷偷上线,先上静态资源,再部署页面,看起来问题少一些。这也是很多公司的部署方案。


但是,大公司超变态,没有这样的绝对低峰期,只有相对低峰期。


所以,为了稳定的服务,还得继续追求极致啊!


这个奇葩问题,起源于资源的 覆盖式发布,用待发布资源覆盖已发布资源,就有这种问题。


解决它也好办,就是实现 非覆盖式发布


image.png


看上图,用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。


因为很多前端开发同学不怎么接触部署,对灰度部署不太熟悉,下面将介绍下什么是灰度部署。


软件开发一般都是一个版本一个版本的迭代。新版本上线前都会经过测试,但就算这样,也不能保证上线了不出问题。


所以,在公司里上线新版本代码一般都是通过灰度系统。灰度系统可以把流量划分成多份,一份走新版本代码,一份走老版本代码。


image.png


而且灰度系统支持设置流量的比例,比如可以把走新版本代码的流程设置为 5%,没啥问题了再放到 10%,50%,最后放到 100% 全量。这样可以把出现问题的影响降到最低。


不然一上来就全量,万一出了线上问题,那就是大事故。


另外,灰度系统不止这一个用途,比如,产品不确定某些改动是不是有效的,就要做 AB 实验,也就是要把流量分成两份,一份走 A 版本代码,一份走 B 版本代码。


那这样的灰度系统是怎么实现的呢?其实很多都是用 nginx 实现的。


nginx 是一个反向代理的服务,用户请求发给它,由它转发给具体的应用服务器。


image.png


它的过程如下图所示:


image.png


首先,需要对流量进行染色,即对这个用户进行标注,让这个用户访问服务1,另外的用户访问服务2。染色的方式有很多,可以通过cookie来完成。不同的用户携带的cookie是不同的。第一染色的时候,所有的用户都访问服务1。


然后,第二次访问的时候,nginx根据用户携带的cookie进行转发到不同的服务,这样就完成了灰度访问。


好了,灰度部署就介绍到这里,回到原文讲的先全量部署静态资源,再灰度部署页面,这是什么意思呢?


首先,部署静态资源的时候,不要删除原来的静态资源,而是把新的静态资源发复制过去,因为文件名用摘要算法重命名的,所以不会发生重名的问题。


其次,灰度部署动态页面,也就是一部分用户访问老的页面,一部分用户访问新的页面。访问老页面的用户请求的还是老资源,直接使用缓存。访问新页面的用户访问新资源,此时新资源已经部署完成,所以不会访问老的资源,导致页面出现错误。


最后,根据访问情况,利用灰度系统,逐渐把访问老页面的用户过渡到访问新页面上。


所以,大公司的静态资源优化方案,基本上要实现这么几个东西:



  1. 配置超长时间的本地缓存:节省带宽,提高性能

  2. 采用内容摘要作为缓存更新依据:精确的缓存控制

  3. 静态资源CDN部署:优化网络请求

  4. 更资源发布路径实现非覆盖式发布:平滑升级


全套做下来,就是相对比较完整的静态资源缓存控制方案了,而且,还要注意的是,静态资源的缓存控制要求在前端所有静态资源加载的位置都要做这样的处理


是的,所有!


什么js、css自不必说,还要包括js、css文件中引用的资源路径,由于涉及到摘要信息,引用资源的摘要信息也会引起引用文件本身的内容改变,从而形成级联的摘要变化,大概就是:


image.png


到这里本文结束了,我们已经了解了前端部署中关于静态资源缓存要面临的优化和部署问题,新的问题又来了:这™让工程师怎么写码啊!!!


这又会扯出一堆有关模块化开发、资源加载、请求合并、前端框架等等的工程问题。


妈妈,我再也不玩前端了。。。。


作者:小p
来源:juejin.cn/post/7316202725330796571
收起阅读 »

App跨平台框架VS原生开发深度评测之2023版

App跨平台框架历史悠久,从cordova、react native、flutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。 过去的问题到底在哪里? 我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差...
继续阅读 »

App跨平台框架历史悠久,从cordovareact nativeflutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。


过去的问题到底在哪里?


我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差别具体在哪里。


逻辑层渲染层类型代表作
webviewwebview弱类型5+App、cordova
js引擎webview弱类型uni-app之app-vue 、小程序(dount)
js引擎原生渲染弱类型react native、uni-app之app-nvue、weex
dart引擎flutter渲染引擎强类型flutter
js引擎flutter渲染引擎弱类型微信skyline、webF、ArkUI-x
kotlin原生渲染强类型uni-app x
kotlin原生渲染强类型原生应用

上面的表格,除了行尾的原生应用外,各个跨平台框架按出现时间排序,可以看到跨平台框架是如何演进的。


上表中,uni-app x和原生应用是一样的,逻辑层和渲染层都是原生,都是强类型;而其他跨平台框架或者在逻辑层、或者在渲染层与原生不一致。


webview不行已经是业内常识了,启动慢、渲染慢、内存占用高。这块本文不再详述。


但那些非web-view的框架到底哪里不如原生?


1. js逻辑+ 原生渲染


react nativeweex等抛弃webview,改由原生渲染的跨平台方案,2014年就推出了。
如今手机硬件也越来越好了,为什么性能还达不到原生?


js+原生渲染的方案主要有2点缺陷:



  • JS引擎自身的性能问题

  • JS和原生之间的通信延迟


1.1 js引擎慢,启动速度和运行速度都弱于原生


所以很多开发者即便使用这类方案,首页也还是原生来写。


React Native的Hermes引擎和华为的arkUI,提供了js编译为字节码的方案,这是一种空间换时间的方案,启动速度有了一定优化,但仍然比不过原生。


弱类型在编译期可优化的幅度有限,还是需要一个运行时来跑,无法像强类型那样直接深入底层。


以数字运算为例,js的number运算确实比强类型的int慢,内存开销也更大。


1.2 js语言与原生之间通信卡顿


每个语言有自己的内存空间,跨语言通信都有折损,每次通信几十到几百毫秒不等,视手机当时的状态。一旦频繁通信,就会明显卡顿。


逻辑层的js,即要和原生渲染层通信,还要和原生API通信:


1.2.1 js与原生ui通信


举个简单的场景例子,在js里监听滚动,根据滚动变化实时调整界面上某些元素的高度变化。这个问题能难倒一大批跨平台开发框架。


如果全部在webview里,js操作ui还好一些,所以uni-app的app-vue里的renderjs操作UI性能高,就是这个道理。同理还有微信小程序的wsx


虽然小程序和uni-app都是js,但实际上逻辑层在独立js引擎里,通过原生桥来控制web-view,通信成本很高。


weex提供了bindingx技术,这是一种弱编程,渲染层预先定义了一些操作UI的方式,调用时全部在渲染层运行,不会来回与逻辑层通信。但这种预定义方式的适应面有限,无法做到在js里高性能、自由的操作所有UI。


1.2.2 js操作原生api


操作系统和三方SDK的API都是原生的,js调用这些能力也需要跨语言通信。比如js调用原生的Storage或IO,数据较多时遍历的性能非常差。


当然在js API的封装上可以做些优化,比如微信的storage提供了wx.batchGetStorageSync这种批量读取的API,既然遍历性能差,那干脆一次性从原生读出来再传给js。


这也只能是无奈的方案,如果在遍历时想用js做什么判断就实现不了了,而且一次性读出很大的数据后传给js这一下,也需要通信时间。


2. flutter方案


flutter在2018年发布,第一次统一了逻辑层和渲染层,而且使用了强类型。


它没有使用原生渲染,而是使用由dart驱动的渲染引擎,这样逻辑层的dart代码操作UI时,再也没有延时了!bindingx、wxs这种补丁方案再也不需要了。


并且dart作为强类型,编译优化很好做,启动速度和运行速度都胜过js。


在这个开源项目下gitcode.net/dcloud/test…,提供了一个flutter编写的100个slider同时滑动的示例, 项目下有源码也有打包好apk,可以直接安装体验。


100个slider同时滑动,非常考验逻辑和UI的通信。如果在webview内部,html和js写100个这样的slider,在新的手机上表现也还ok。但在小程序和react native这种逻辑和UI分离的模式下,100个slider是灾难。


下载安装apk后可以看到dart操作flutter的UI真的没有通信折损,100个slider的拖动非常流畅。


flutter看起来很完美。但为什么也没有成为主流呢?很多大厂兴奋的引入后为何又不再扩大使用范围呢?


2.1 dart与原生API的通信


别忘了上面1.2.2提到的原生API通信。flutter虽然在逻辑层和渲染层都是dart,但要调用原生API时,还是要通信。


操作系统和三方SDK的API是原生的,让dart调用需要做一层封装,又落到了跨语言通信的坑里。


gitcode.net/dcloud/test…这是一个开源测试项目,来测试原生的claas数据与dart的通信耗时。


项目里面有源码,大家可自行编译;根目录有打包好的apk,也可以直接安装体验。


这个项目首先在kotlin中构建了包含不同数据量的class,传递到dart然后渲染在界面上,并且再写回到原生层。


有0.1k和1k两种数据量(点击界面上的1k数字可切换),有读和读并写2个按钮,各自循环1000次。


以下截图的测试环境是华为mate 30 5G,麒麟990。手机上所有进程杀掉。如下图:



  • 1k数据从原生读到dart并渲染


flutter_1k_read.jpeg



  • 1k数据从原生读到dart并渲染再写回


flutter_1k_readwrite.jpeg



  • 0.1k数据从原生读到dart并渲染


flutter_0.1k_read.jpeg



  • 0.1k数据从原生读到dart并渲染再写回


flutter_0.1k_readwrite.jpeg


通信损耗非常明显。并且数据量从1k降低到0.1k时,通信时间并没有减少10倍,这是因为通信耗时有一个基础线,数据再小也降不下去。


为什么会这样?因为dartkotlin不是一种编程语言,不能直接调用kotlinclass,只能先序列化成字符串,把字符串数据从原生传到dart,然后在dart层再重新构造。


当然也可以在原生层为dart封装API时提供wx.batchGetStorageSync这类批处理API,把数据一次读好再给dart,但这种又会遇到灵活性问题。


而在uni-app x中,这种跨语言通信是不存在的,不需要序列化,因为uni-app x使用的编程语言uts,在android上就编译为了kotlin,它可以直接调用kotlin的class而无需通信和封装。示例如下,具体uni-app x的原理后续章节会专题介绍。


<template>
template>
<script lang="uts">
import Build from 'android.os.Build';
export default {
onLoad() {
console.log(Build.MODEL); //uts可以直接导入并使用原生对象,不需要封装,没有跨语言通信折损
}
}
script>

再分享一个知识:


很多人都知道iPhone上跨平台框架的应用,表现比android好。但大多数人只知道是因为iPhone的硬件好。


其实还有一个重要原因,iOS的jscore是c写的,OS的API及渲染层也都是ObjectC,js调用原生时,某些类型可以做共享内存的优化。但复杂对象也还是无法直接丢一个指针过去共享使用内存。


而android,不管java还是kotlin,他们和v8、dart通信仍然需要跨语言通信。


2.2 flutter渲染和原生渲染的并存问题


flutter的自渲染引擎,在技术上是不错的。但在生态兼容上有问题。


很多三方软件和SDK是原生的,原生渲染和flutter自渲染并存时,问题很多。


flutter开发者都知道的一个常见坑是输入法,因为输入法是典型的原生UI,它和flutter自绘UI并存时各种兼容问题,输入框被遮挡、窗体resize适应,输入法有很多种,很难适配。


混合渲染,还有信息流广告、map、图表、动画等很多三方sdk涉及。这个时候内存占用高、渲染帧率下降、不同渲染方式字体不一致、暗黑主题不一致、国际化、无障碍、UI自动化测试,各种不一致。。。


这里没有提供开源示例,因为flutter官方是承认这个问题的,它提供了2种方式:混合集成模式和虚拟显示模式模式。


但在渲染速度、内存占用、版本兼容、键盘交互上都各自有各自的问题。详见flutter官网:docs.flutter.dev/platform-in…。这个是中文翻译:flutter.cn/docs/platfo…


在各大App中,微信的小程序首页是为数不多的使用flutter UI的界面,已经上线1年以上。


下面是微信8.0.44(此刻最新版),从微信的发现页面进入小程序首页。


视频中手机切换暗黑主题后,这个UI却还是白的,而且flutter的父容器原生view已经变黑了,它又在黑底上绘制了一个白色界面,体验非常差。


这个小程序首页界面很简单,没有输入框,规避了混合渲染,点击搜索图标后又跳转到了黑色的原生渲染的界面里。


假使这个界面再内嵌一个原生的信息流SDK,那会看到白色UI中的信息流广告是黑底的,更无法接受。


当然这不是说flutter没法做暗黑主题,重启微信后这个界面会变黑。这里只是说明渲染引擎不一致带来的各种问题。



注:如何识别一个界面是不是用flutter开发的?在手机设置的开发者选项里,有一个GPU呈现模式分析,flutter的UI不触发这个分析。且无法审查布局边界。



flutter的混合渲染的问题,在所有使用原生渲染的跨平台开发框架中都不存在,比如react native、weex、uni-app x。


总结下flutter:逻辑层和UI层交互没有通信折损,但逻辑层dart和原生api有通信成本,自绘UI和原生ui的混合渲染问题很多。


3. js+flutter渲染


flutter除了上述提到的原生通信和混合渲染,还有3个问题:dart生态、热更新、以及比较难用的嵌套写法。


一些厂商把flutter的dart引擎换成了js引擎,来解决上述3个问题。比如微信skyline、webF、ArkUI-x。


其实这是让人困惑的行为。因为这又回到了react native和weex的老路了,只是把原生渲染换成了flutter渲染。


flutter最大的优势是dart操作UI不需要通信,以及强类型,而改成js,操作UI再次需要通信,又需要js运行时引擎。


为了解决js和flutter渲染层的通信问题,微信的skyline又推出了补丁技术worklet动画,让这部分代码运行在UI层。(当然微信的通信,除了跨语言,还有跨进程通信,会更明显)


这个项目gitcode.net/dcloud/test…, 使用ArkUI-x做了100个slider,大家可以看源码,下载apk体验,明显能看到由于逻辑层和UI层通信导致的卡顿。



上述视频中,注意看手指按下的那1个slider,和其他99个通过数据通讯指挥跟随一起行动的slider,无法同步,并且界面掉帧。


不过自渲染由于无法通过Android的开发者工具查看GPU呈现模式,所以无法从条状图直观反映出掉帧。



注意ArkUI-x不支持Android8.0以下的手机,不要找太老的手机测试。



很多人以为自渲染是王道,但其实自渲染是坑。因为flutter的UI还会带来混合渲染问题。


也就是说,js+flutter渲染,和js+原生渲染,这2个方案相比,都是js弱类型、都有逻辑层和渲染层的通信问题、都有原生API通信问题,而js+flutter还多了一个混合渲染问题。


可能有的同学会说,原生渲染很难在iOS、Android双端一致,自渲染没有这个问题。


但其实完全可以双端一致,如果你使用某个原生渲染框架遇到不一致问题,那只是这个框架厂商做的不好而已。


是的,很遗憾react native在跨端组件方面投入不足,官方连slider组件都没有,导致本次评测中未提供react native下slider-100的示例和视频。


4. uni-app x


2022年,uts语言发布。2023年,uni-app x发布。


uts语言是基于typescript修改而来的强类型语言,编译到不同平台时有不同的输出:



  • 编译到web,输出js

  • 编译到Android,输出kotlin

  • 编译到iOS,输出swift


而uni-app x,是基于uts语言重新开发了一遍uni-app的组件、API以及vue框架。


如下这段示例,前端的同学都很熟悉,但它在编译为Android App时,变成了一个纯的kotlin app,里面没有js引擎、没有flutter、没有webview,从逻辑层到UI层都是原生的。


<template>
<view class="content">
<button @click="buttonClick">{{title}}button>
view>
template>

<script> //这里只能写uts
export default {
data() {
return {
title: "Hello world"
}
},
onLoad() {
console.log('onLoad')
},
methods: {
buttonClick: function () {
uni.
showModal({
"showCancel": false,
"content": "点了按钮"
})
}
}
}
script>

<style>
.content {
width: 750rpx;
background-color: white;
}
style>

这听起来有点天方夜谭,很多人不信。DCloud不得不反复告诉大家,可以使用如下方式验证:



  • 在编译uni-app x项目时,在项目的unpackage目录下看看编译后生成的kt文件

  • 解压打包后的apk,看看里面有没有js引擎或flutter引擎

  • 手机端审查布局边界,看看渲染是不是原生的(flutter和webview都无法审查布局边界)


但是开发者也不要误解之前的uni-app代码可以无缝迁移。



  • 之前的js要改成uts。uts是强类型语言,上面的示例恰好类型都可以自动推导,不能推导的时候,需要用:as声明和转换类型。

  • uni-app x支持css,但是css的子集,不影响开发者排版出所需的界面,但并非web的css全都兼容。


了解了uni-app x的基本原理,我们来看下uni-app x下的100个slider效果怎么样。


项目gitcode.net/dcloud/test…下有源码工程和编译好的apk。


如下视频,打开了GPU呈现模式,可以看到没有一条竖线突破那条红色的掉帧安全横线,也就是没有一帧掉帧。



uni-app x在app端,不管逻辑层、渲染层,都是kotlin,没有通信问题、没有混合渲染问题。不是达到了原生的性能,而是它本身就是原生应用,它和原生应用的性能没差别。


这也是其他跨平台开发框架做不到的。


uni-app x是一次大胆的技术突破,分享下DCloud选择这条技术路线的思路:


DCloud做了很多年跨平台开发,uni-app在web和小程序平台取得了很大的成功,不管规模大小的开发者都在使用;但在app平台,大开发者只使用uni小程序sdk,中小开发者的app会整体使用。


究其原因,uni-app在web和小程序上,没有性能问题,直接编译为了js或wxml,uni-app只是换了一种跨平台的写法,不存在用uni-app开发比原生js或原生wxml性能差的说法。


但过去基于小程序架构的app端,性能确实不及原生开发。


那么App平台,为什么不能像web和小程序那样,直接编译为App平台的原生语言呢?


uni-app x,目标不是改进跨平台框架的性能,而是给原生应用提供一个跨平台的写法。


这个思路的转换使得uni-app x超越了其他跨平台开发框架。


在web端编译为js,在小程序端编译为wxml等,在app端编译为kotlin。每个平台都只是帮开发者换种一致的写法而已,运行的代码都是该平台原生的代码。


然而在2年前,这条路线有2个巨大的风险:



  1. 从来没有人走通过

  2. 即便能走通,工作量巨大


没有人确定这个产品可以做出来,DCloud内部争议也很多。


还好,经历了无数的困难和挑战,这个产品终于面世了。


换个写法写原生应用,还带来另一个好处。


同样业务功能的app,使用vue的写法,比手写纯原生快多了。也就是uni-app x对开发效率的提升不只是因为跨平台,单平台它的开发效率也更高。


其实google自己也知道原生开发写法太复杂,关于换种更高效的写法来写原生应用,他们的做法是推出了compose UI。


不过遗憾的是这个方案引入了性能问题。我们专门测试使用compose UI做100个slider滑动的例子,流畅度也掉帧。


源码见:gitcode.net/dcloud/test…, 项目下有打包后的apk可以直接安装体验。


打开GPU呈现模式,可以看到compose ui的100个slider拖动时,大多数竖线都突破那条红色的掉帧安全横线,也就是掉帧严重。


既然已经把不同开发框架的slider-100应用打包出来了,我们顺便也比较了不同框架下的包体积大小、内存占用:


包体积(单位:M)内存占用(单位:Kb)
flutter18141324.8
ArtUI-x45.7133091.2
uni-app x8.5105451.2
compose ui4.498575.2

包体积数据说明:



  • 包含3个CPU架构:arm64、arm32、x86_64。

  • flutter的代码都是编译为so文件,支持的cpu类型和包体积是等比关系,1个cpu最小需要6M体积,业务代码越多,cpu翻倍起来越多。

  • ArtUI-x的业务代码虽然写在js里,但除了引用了flutter外还引用了js引擎,这些so库体积都不小且按cpu分类型翻倍。

  • uni-app x里主业务都在kotlin里,kotlin和Android x的兼容库占据了不少体积。局部如图片引用了so库,1个cpu最小需要7M体积。但由于so库小,增加了2个cpu类型只增加了不到1M。

  • compose ui没有使用so库,体积裁剪也更彻底。

  • uni-app x的常用模块并没有裁剪出去,比如slider100的例子其实没有用到图片,但图片使用的fesco的so库还是被打进去了。实际业务中不可能不用图片,所以实际业务中uni-app x并不会比compose ui体积大多少。


内存占用数据说明:



  • 在页面中操作slider数次后停止,获取应用内存使用信息VmRSS: 进程当前占用物理内存的大小

  • 表格中的内存数据是运行5次获取的值取平均值

  • 自渲染会占据更多内存,如果还涉及混合渲染那内存占用更高


5. 后记


跨语言通信、弱类型、混合渲染、包体积、内存占用,这些都是过去跨平台框架不如原生的地方。


这些问题在uni-app x都不存在,它只是换了一种写法的原生应用。


各种框架类型逻辑层与UI通信折损逻辑层与OS API通信折损混合渲染
react native、nvue、weex
flutter
微信skyline、webF、ArkUI-x
uni-app x
原生应用

当然,作为一个客观的分析,这里需要强调uni-app x刚刚面世,还有很多不成熟的地方。比如前文diss微信的暗黑模式,其实截止到目前uni-app x还不支持暗黑模式。甚至iOS版现在只能开发uts插件,还不能做完整iOS应用。


需求墙里都是uni-app x该做还未做的。也欢迎大家投票。


另外,原生Android中一个界面不能有太多元素,否则性能会拉胯。flutter的自渲染和compose ui解决了这个问题。而原生中解决这个问题需要引入自绘机制来降低元素数量,这个在uni-app x里对应的是draw自绘API。


uni-app x这个技术路线是产业真正需要的东西,随着产品的迭代完善,它能真正帮助开发者即提升开发效率又不牺牲性能。


让跨平台开发不如原生,成为历史。


欢迎体验uni-app x的示例应用,感受它的启动速度,渲染流畅度。


源码在:gitcode.net/dcloud/hell…; 


这个示例里有几个例子非常考验通信性能,除了也内置了slider-100外,另一个是“模版-scroll-view自定义滚动吸顶”,在滚动时实时修改元素top值始终为一个固定值,一点都不抖动。


我们不游说您使用任何开发技术,但您应该知道它们的原理和差别。


欢迎指正和讨论。


作者:CHB
来源:juejin.cn/post/7317091780826497075
收起阅读 »

一名小白程序员的思维风暴,大家看看当一乐了

长目标实施失败分析 1. 单词 事件:准备系统学习英语,主要是想拿到英语考级证书,后面继续扩展,能无字幕看懂英文字幕等,能更好的获取英文咨询 计划:根据自己的理解,首先找到了最基础的音标学习,同步进行单词记忆 中断情况: 每天安排的三十个单词记忆早上...
继续阅读 »

长目标实施失败分析


1. 单词



  • 事件:准备系统学习英语,主要是想拿到英语考级证书,后面继续扩展,能无字幕看懂英文字幕等,能更好的获取英文咨询

  • 计划:根据自己的理解,首先找到了最基础的音标学习,同步进行单词记忆


image-20231220152153200.png



  • 中断情况:



    1. 每天安排的三十个单词记忆早上一个小时都不够用(理论上是足够的,自己分析是自己对背单词的本能抵触,导致效率很低),有时为了完成手上工作经常放弃早上单词任务,一天内也没有补上,时间一长就逐渐放弃了;

    2. 还有一点是之前记忆的单词一般第二天都会重复检查,检查的结果是之前记忆的单词遗忘的太快,导致第二天又要花费额外的时间重新背,这样算下来觉得花费太多时间在这上面

    3. 晚上的音标听读,由于在室外,经常会遇到行人,有时候觉得不好意思经常偷工减料,冬天室外又很冷,经常断更



  • 克服情况:



    • 当时对这件事的重视程度不高,并且英语学习计划自己规划了不下五六回了,最后没能完成这件事,也很快抛掷脑后了

    • 现在看来:上述几个问题自己现在都能找到调整方案,主要在于自己是否真心决定做这件事,自我认为当时并没有觉得这件事有多么重要,就如同一个爱好一样,中断就中断了。



  • 习得性无助:之前了解过一些,关于这点,我主观认为是没有的,因为我觉得只要我觉得自己下定决心,按照记得学习计划来,是肯定能学会的,但是现在自己都已经不想开这个头了,又觉得是已经陷入了这种习得性无助中。

  • 自己的计划表单中,其实一直有英语学习的愿望,但是再也没有开始过,空着也觉得稀疏平常了。


    image-20231220154741758.png


  • 补充一点:自己在此期间因为减肥,养成了运动习惯,现在跑步这件事对我没有一点障碍,只要有跑步的念头,就能马上出发的那种,冬天也不例外,所以后面就停止运动这一块的记录,也是自己觉得很骄傲的一点。应该是得到了一种正反馈


image-20231220155323950.png


2. 找工作



  • 事件:今年六月毕业,在三月忙完毕业答辩之后,计划了为期两个月的找工作面试之旅,当时计划的是一直投简历面试到春招结束。

  • 计划:秋招没有参加,那会对自己的能力很自卑,加上那会实习,学校封校,没有参加学校组织的校园招聘(现在想来很后悔,错过了很多好的机会),于是今年三月份春招做好了最坏的打算,打算持续到招聘结束(这里的一点心得是:当我们决定在解决一件事情的时候,它大概率就是处于一种很坏的情况,任何事情都需要早做打算,能节省很多成本


    image.png


  • 中断情况:



    1. 2023年春招真的是很艰难,三月份一直处于简历投递,参加校园招聘,各种企业的宣讲会,网申评测等,最后接到面试通知的却只有一两家,并且面试的经验也少,导致面试结果也不好,逐渐也失去了信心。

    2. 事情在四月份有了转机,学校与一些学校跟企业来我们学校做宣讲,在某个学校的教师聘用岗拿到了第一份offer,后面又拿到了武汉的一份c++开发岗offer(觉得加班严重没去),期间参加了一次武汉云智的面试,面试官是腾讯员工,面试过程被暴捶,突然意识到自己的能力可能就是值上面开发岗的价位,所以后面就放缓了面试招聘的行程,觉得再怎么找也找不到更好的了,加上面试也很疲劳了,这种摆烂心态也间接导致后面武汉电信的终面都没去,现在还觉得可惜。

    3. 五月左右,毕业活动变得很多,毕业照,毕业聚餐,毕业旅行,各种活动接踵而至,所以找工作这件事也变得没有之前积极,想着有保底了,就去保底算了,自己制定的投递计划,有很多大厂,好公司都没有投递,也变成了现在的遗憾,身为一个计算机硕士,连心中梦想的大厂都没有投递过。



  • 克服情况:



    1. 在当时情况下,自己找不到克服的动力与方法,当时想着能有一份工作保底就很不错了

    2. 目前看来,其实还是有继续面试的必要的,虽然能力上有所欠缺,但是自己是有很强的学习能力的,现在入职之后发现,很多东西都是需要自己去找资料学的,而自己需要的只是一个平台罢了。



  • 总结:



    • 这段经历的给我的感觉是,到现在工作了还是觉很绝望,因为在大的趋势下,一名应届生是都找到工作显得一文不值,但是目前的我具备了能坚持下去的勇气。




3. 独立开发



  • 事件:想独自开发一款属于自己构思的小程序,自己在学习完基础课程后,当准备着手时,却不知道如何开始,自己也只是一个前端小白,每次做这件事的时候,根本没有方向。

  • 计划:目前就制作了该小程序核心功能的思维导图,事件计划没有写,因为自己也没有能力预估各功能模块完成的时间,也不清楚如何将自己列举的具体功能细分成功能模块,变成我可以明确了解的实现步骤。

    • 已完成

      • 购买云服务,并配置环境

      • 找个一个完整的前后端分离项目案例,准备在此基础上进行修改

      • 程序核心功能思维导图

      • 构建前端后仓库





  • 中断情况:

    1. 由于在心中对程序整体功能只有一个大概的想法,所以在对程序功能实现时总觉得很模糊,没有办法量化

    2. 没有实际独立的开发经验,对完成整件事具有莫名的不自信,或者是恐惧,在推进工作时,一直没有明确的实现路线

    3. 找不到发力的着力点,准备完成前端界面时,没有原型图自己都不知道怎么开始,用别人的模板有总觉得不合适,在纠结之前内耗停滞。



  • 克服情况:

    • 暂没有解决方案

    • 下一步计划:在选好的项目上就研究,在已有的项目上进行修改。




作者:用户8430186848879
来源:juejin.cn/post/7314559238720749608
收起阅读 »

写一个简易的前端灰度系统

写在前面的话灰度这个概念,来自数字图像领域,最初是描述黑白数字图像的灰度值,范围从 0 到 255,0 表示黑色,255 表示白色,中间的数值表示不同程度的灰色。灰度系统的诞生源于交叉学科的建设,在互联网上也不...
继续阅读 »

写在前面的话

灰度这个概念,来自数字图像领域,最初是描述黑白数字图像的灰度值,范围从 0  2550 表示黑色,255 表示白色,中间的数值表示不同程度的灰色。

灰度系统的诞生源于交叉学科的建设,在互联网上也不例外。对于一个软件产品,在开发和发布的时候肯定希望用户能够顺利的看到想让其看到的内容。但是,发布没有一帆风顺的,如果在发布的某个环节出了问题,比如打错了镜像或者由于部署环境不同触发了隐藏的bug,导致用户看到了错误的页面或者旧的页面,这就出现了生产事故。为了避免这种情况出现,借鉴数字图像处理的理念,设计师们设计出了一种介于 0  1 之间的过渡系统的概念:让系统可以预先发布,并设置可见范围,就像朋友圈一样,等到风险可控后,再对公众可见。这就是灰度系统。

灰度系统版本的发布动作称作 灰度发布,又名金丝雀发布,或者灰度测试,他是指在黑与白之间能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。(概念来自知乎)

对于前端领域,演进到现在,灰度系统主要有如下几点功能:

  1. 增量灰度:小的patch可以增量的添加在发布版本上,也可以通过开关一键关闭
  2. 用户灰度:增量和全量版本都可对不同群体或者某几个特定的用户进行灰度可见
  3. 版本回退:每一个版本都在灰度系统里可见,可以一键回退

前端灰度系统工作流程图如下:

sequenceDiagram
前端项目-->灰度系统: 部署阶段
前端项目->>灰度系统: 1.CI 打包后写入打包资源,状态初始化
前端项目-->灰度系统: 访问阶段
前端项目->>灰度系统: 1.页面访问,请求当前登录用户对应的资源版本
灰度系统-->>前端项目: 2.从对应版本的资源目录返回前端资源

灰度规则

关于灰度资源优先级的说明如下:

灰度策略优先级
未生效
生效
全量一般

如此就起到了灰度的作用:全量表示所有人都可以看;生效表示只有在规则中的用户才可以看到这部分增量更新,优先级最高;未生效表示不灰度,优先级最低。

灰度系统数据库设计

为什么灰度系统有后端:前端项目 CI 部署后,会产生一个 commit 号和一个镜像记录,并且打包后的文件存放在服务器中某一个深层的文件夹目录中,灰度系统需要存入该部署的目录地址,便于在切换灰度时查找不同版本的文件。

先介绍一个要部署的前端项目(你可以根据自己的前端项目动态调整)。

本项目针对的前端项目是一个基于微服务架构的项目,

下面是设计ER图:

image.png

我们依此来分析:

子项目表

该表用于存放所有子项目的信息,新建一个微服务子项目时,会在这个表里新建一个条目,数据示意如下:

image.png

灰度用户表

用于灰度系统登录的用户,拥有灰度权限的人才可以加入。

资源表

资源表存放项目在 CI 中写入的 commit 信息和 build 完以后在服务器的存放位置,数据示意如下:

image.png

其中 branch 是跑CI的分支,data 存放打包资源目录信息,一般结构如下:

image.png

gitProjectId 存放该产品在 gitlab 中的项目号, status 表示构建状态:0:构建完成 1:部署完成 2:构建失败,3:部署失败。

这里简单提一下 CI 是如何写入灰度系统数据库的,过多详情不做解释,写入数据库方式很多,这只是其中一种实现方式。

  1. 首先在 CI build 环节往服务器写入打包信息的 JSON:

image.png

其中 build.sh 负责把传入的参数写到一个 json 中。上图中是往根目录copy,方便下一个 CI job读取json文件的示意图。

  1. 在 CI 部署环节,通过调用脚本创建资源:

image.png

其中 run_gray.js:

const { ENV, file, branch, projectId, gitProjectId, user, commitMsg } = require('yargs').argv;

axios({
url: URL,
method: "POST",
headers: {
remoteUser: user
},
data: {
Action: "CreateResource",
projectId,
branch,
commitMsg,
gitProjectId,
channel: Channel,
data: fs.readFileSync(file, 'utf8'),
status: "0"
}
}).then(...)

其中 status 的变化,在 CI 部署服务器完成后,追加一个 UpdateResource 动作即可:

if [[ $RetCode != 0 ]]; then curl "$STARK_URL" -X 'POST' -H 'remoteUser: '"$GITLAB_USER_NAME"'' -H 'Content-Type: application/json' -d '{"Action": "UpdateResource", "id": "'"$ResourceId"'", "status": "2"}' > test.log && echo `cat test.log`; fi

灰度策略表

灰度策略是对灰度资源的调动配置。其设计如下:

image.png

其中,prijectId 表示灰度的项目,resourceId 表示使用的资源,rules 配置了对应的用户或用户组(看你怎么配置了,我这里只配置了单独的 userId),status 是灰度的状态,我设置了三种:

  • default: 未生效
  • failure: 生效
  • success: 全量

状态生效表示是增量发布的意思。

到这里,数据库设计就完毕了。


灰度系统接口API开发

有了数据库,还需要提供能够操作数据库的服务,上边创建资源的接口就是调用的灰度自己的API实现的。主要的API列表如下:

名称描述
getResourcesByProjectId获取单个产品下所有资源
getResourcesById通过主键获取资源
createResource创建一个资源
updateResource更新一个资源
getIngressesByProjectId获取单个产品下灰度策略任务列表
getIngressById通过主键获取单个灰度策略任务详情
createIngress创建一个策略
updateIngress更新一个策略

剩余的接口有用户处理的,有子项目管理的,这里不做详述。除了上边的必须的接口外,还有一个最重要的接口,那就是获取当前登录用户需要的资源版本的接口。在用户访问时,需要首先调用灰度系统的这个接口来获取资源地址,然后才能重定向到给该用户看的页面中去:

名称描述接收参数输出
getConsoleVersion获取当前用的产品版本userId,productsresource键值对列表

getConsoleVersion 接受两个参数,一个是当前登录的用户 ID, 一个是当前用户访问的微服务系统中所包含的产品列表。该接口做了如下几步操作:

  1. 遍历 products,获取每一个产品的 projectId
  2. 对于每一个 projectId,联查资源表,分别获取对应的 resourceId
  3. 对于每一个resourceId,结合 userId,并联查灰度策略表,筛选出起作用的灰度策略中可用的资源
  4. 返回每一个资源的 data 信息。

其中第三步处理相对繁琐一些,比如说,一个资源有两个起作用的灰度资源,一个是增量的,一个是全量的,这里应该拿增量的版本,因为他优先级更高。

获取用户版本的流程图如下:

graph TD
用户登录页面 --> 获取所有产品下的资源列表
获取所有产品下的资源列表 --> 根据灰度策略筛选资源中该用户可用的部分 --> 返回产品维度的资源对象

最后返回的资源大概长这个样子:

interface VersionResponse {
[productId: number]: ResourceVersion;
}

interface ResourceVersion {
files: string[];
config: ResourceConfig;
dependencies: string[];
}

其中 files 就是 JSON 解析后的上述 data 信息的文件列表,因为打包后的文件往往有 css和多个js。

至于这个后端使用什么语言,什么框架来写,并不重要,重要的是一定要稳定,他要挂掉了,用户就进不去系统了,容灾和容错要做好;如果是个客户比较多的网站,并发分流也要考虑进去。

前端页面展示

前端页面就随便使用了一个前端框架搭了一下,选型不是重点,组件库能够满足要求就行:

  • 登录

image.png

  • 查看资源

image.png

  • 配置策略

image.png

image.png


部署以后,实际运行项目看看效果:

image.png

可以看到,在调用业务接口之前,优先调用了 getConsoleVersion来获取版本,其返回值是以产品为 key 的键值对:

image.png

访问转发

这里拿到部署信息后,服务器要进行下一步处理的。我这里是把它封装到一个对象中,带着参数传给了微服务的 hook 去了(微服务系统需要);如果你是单页应用,可能需要把工作重心放在 Nginx 的转发上,Nginx内部服务读取灰度系统数据库来拿到版本目录,然后切换路由转发(可能只是改变一个路由变量)。 (你也可以参照我 nginx 相关文章),下面我简单的给个示意图:

graph TD
灰度系统配置灰度策略 --> Nginx+Lua+Mysql获取灰度策略并写入Nginx变量
Nginx+Lua+Mysql获取灰度策略并写入Nginx变量 --> Nginx服务器配置资源转发

总结

前端灰度系统,其实就是一个后台管理系统。他配置和管理了不同版本的前端部署资源和对应的用户策略,在需要的时候进行配置。

接下来的文章我会配套性的讲一下 Nginx 和 Docker 的前端入门使用,敬请期待!

完!大家对灰度系统有什么好的建议,可以在评论区讨论哦!



作者:小肚肚肚肚肚哦
来源:juejin.cn/post/7212054600162132029

收起阅读 »

为什么程序员一定要写单元测试?

大家好,我是鱼皮,很多初学编程的同学都会认为 “程序员的工作只有开发新功能,功能做完了就完事儿”。但其实不然,保证程序的正常运行、提高程序的稳定性和质量也是程序员的核心工作。 之前给大家分享过企业项目的完整开发流程,其中有一个关键步骤叫 “单元测试”,这篇文章...
继续阅读 »

大家好,我是鱼皮,很多初学编程的同学都会认为 “程序员的工作只有开发新功能,功能做完了就完事儿”。但其实不然,保证程序的正常运行、提高程序的稳定性和质量也是程序员的核心工作。


之前给大家分享过企业项目的完整开发流程,其中有一个关键步骤叫 “单元测试”,这篇文章就来聊聊程序员如何编写单元测试吧。


什么是单元测试?


单元测试(Unit Testing,简称 UT)是软件测试的一种,通常由开发者编写测试代码并运行。相比于其他的测试类型(比如系统测试、验收测试),它关注的是软件的 最小 可测试单元。


什么意思呢?


假如我们要实现用户注册功能,可能包含很多个子步骤,比如:



  1. 校验用户输入是否合法

  2. 校验用户是否已注册

  3. 向数据库中添加新用户


其中,每个子步骤可能都是一个小方法。如果我们要保证用户注册功能的正确可用,那么就不能只测试注册成功的情况,而是要尽量将每个子步骤都覆盖到,分别针对每个小方法做测试。比如输入各种不同的账号密码组合来验证 “校验用户输入是否合法” 这一步骤在成功和失败时的表现是否符合预期。


同理,如果我们要开发一个很复杂的系统,可能包含很多小功能,每个小功能都是一个单独的类,我们也需要针对每个类编写单元测试。因为只有保证每个小功能都是正确的,整个复杂的系统才能正确运行。


单元测试的几个核心要点是:



  1. 最小化测试范围:单元测试通常只测试代码的一个非常小的部分,以确保测试的简单和准确。

  2. 自动化:单元测试应该是自动化的,开发人员可以随时运行它们来验证代码的正确性,特别是在修改代码后。而不是每次都需要人工去检查。

  3. 快速执行:每个单元测试的执行时间不能过长,应该尽量做到轻量、有利于频繁执行。

  4. 独立性:每个单元测试应该独立于其他测试,不依赖于外部系统或状态,以确保测试的可靠性和可重复性。


为什么需要单元测试?


通过编写和运行单元测试,开发者能够快速验证代码的各个部分是否按照预期工作,有利于保证系统功能的正确可用,这是单元测试的核心作用。


此外,单元测试还有很多好处,比如:


1)改进代码:编写单元测试的过程中,开发者能够再次审视业务流程和功能的实现,更容易发现一些代码上的问题。比如将复杂的模块进一步拆解为可测试的单元。


2)利于重构:如果已经编写了一套可自动执行的单元测试代码,那么每次修改代码或重构后,只需要再自动执行一遍单元测试,就知道修改是否正确了,能够大幅提高效率和项目稳定性。


3)文档沉淀:编写详细的单元测试本身也可以作为一种文档,说明代码的预期行为。


鱼皮以自己的一个实际开发工作来举例单元测试的重要性。我曾经编写过一个 SQL 语法解析模块,需要将 10000 多条链式调用的语法转换成标准的 SQL 语句。但由于细节很多,每次改进算法后,我都不能保证转换 100% 正确,总会人工发现那么几个错误。所以我编写了一个单元测试来自动验证解析是否正确,每次改完代码后执行一次,就知道解析是否完全成功了。大幅提高效率。


所以无论是后端还是前端程序员,都建议把编写单元测试当做一种习惯,真的能够有效提升自己的编码质量。


如何编写单元测试?


以 Java 开发为例,我们来学习如何编写单元测试。


Java 开发中,最流行的单元测试框架当属 JUnit 了,它提供了一系列的类和方法,可以帮助我们快速检验代码的行为。


1、引入 JUnit


首先我们要在项目中引入 JUnit,演示 2 种方式:


Maven 项目引入


在 pom.xml 文件中引入 JUnit 4 的依赖:


<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

Spring Boot 项目引入


如果在 Spring Boot 中使用 JUnit 单元测试,直接引入 spring-boot-starter-test 包即可:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

然后会自动引入 JUnit Jupiter,它是 JUnit 5(新版本)的一部分,提供了全新的编写和执行单元测试的方式,更灵活易用。不过学习成本极低,会用 JUnit 4,基本就会用 JUnit Jupiter。


2、编写单元测试


编写一个单元测试通常包括三个步骤:准备测试数据、执行要测试的代码、验证结果。


一般来说,每个类对应一个单元测试类,每个方法对应一个单元测试方法。


编写 JUnit 单元测试


比如我们要测试一个计算器的求和功能,示例代码如下:


import org.junit.Test;
import org.junit.Assert;

public class CalculatorTest {

    // 通过 Test 注解标识测试方法
    @Test
    public void testAdd() {
        // 准备测试数据
        long a = 2;
        long b = 3;
        
        // 执行要测试的代码
        Calculator calculator = new Calculator();
        int result = calculator.add(23);
        
        // 验证结果
        Assert.assertEquals(5, result);
    }
}

上述代码中的 Assert 类是关键,提供了很多断言方法,比如 assertEquals(是否相等)、assertNull(是否为空)等,用来对比程序实际输出的值和我们预期的值是否一致。


如果结果正确,会看到如下输出:



如果结果错误,输出如下,能够清晰地看到执行结果的差异:



Spring Boot 项目单测


如果是 Spring Boot 项目,我们经常需要对 Mapper 和 Service Bean 进行测试,则需要使用 @SpringBootTest 注解来标识单元测试类,以开启对依赖注入的支持。


以测试用户注册功能为例,示例代码如下:


import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class UserServiceTest {

    @Resource
    private UserService userService;

    @Test
    void userRegister() {
        // 准备数据
        String userAccount = "yupi";
        String userPassword = "";
        String checkPassword = "123456";
        // 执行测试
        long result = userService.userRegister(userAccount, userPassword, checkPassword);
        // 验证结果
        Assertions.assertEquals(-1, result);
        // 再准备一组数据,重复测试流程
        userAccount = "yu";
        result = userService.userRegister(userAccount, userPassword, checkPassword);
        Assertions.assertEquals(-1, result);
    }
}

3、生成测试报告


如果系统的单元测试数量非常多(比如 1000 个),那么只验证某个单元测试用例是否正确、查看单个结果是不够的,我们需要一份全面完整的单元测试报告,便于查看单元测试覆盖度、评估测试效果和定位问题。


测试覆盖度 是衡量测试过程中被测试到的代码量的一个指标,一般情况下越高越好。测试覆盖度 100% 表示整个系统中所有的方法和关键语句都被测试到了。


下面推荐 2 种生成单元测试报告的方法。


使用 IDEA 生成单测报告


直接在 IDEA 开发工具中选择 Run xxx with Coverage 执行单元测试类:



然后就能看到测试覆盖度报告了,如下图:



显然 Main 方法没有被测试到,所以显示 0%。


除了在开发工具中查看测试报告外,还可以导出报告为 HTML 文档:



导出后,会得到一个 HTML 静态文件目录,打开 index.html 就能在浏览器中查看更详细的单元测试报告了:



这种方式简单灵活,不用安装任何插件,比较推荐大家日常学习使用。


使用 jacoco 生成单测报告


JaCoCo 是一个常用的 Java 代码覆盖度工具,能够自动根据单元测试执行结果生成详细的单测报告。


它的用法也很简单,推荐按照官方文档中的步骤使用。


官方文档指路:http://www.eclemma.org/jacoco/trun…


首先在 Maven 的 pom.xml 文件中引入:


<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.11</version>
</plugin>

当然,只引入 JaCoCo 插件还是不够的,我们通常希望在执行单元测试后生成报告,所以还要增加 executions 执行配置,示例代码如下:


<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <configuration>
        <includes>
            <include>com/**/*</include>
        </includes>
    </configuration>
    <executions>
        <execution>
            <id>pre-test</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>post-test</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

然后执行 Maven 的 test 命令进行单元测试:



测试结束后,就能够在 target 目录中,看到生成的 JaCoCo 单元测试报告网站了:



打开网站的 index.html 文件,就能看到具体的测试报告结果,非常清晰:



通常这种方式会更适用于企业中配置流水线来自动化生成测试报告的场景。


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

封装一个丝滑的评分组件

web
效果 实现 找到喜欢的图标 http://www.iconfont.cn/ 打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成 下载解压 取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也...
继续阅读 »

效果


score.gif


实现


找到喜欢的图标


http://www.iconfont.cn/


打开 iconfont,随便搜个点赞,仅需一个点赞即可,节省内存,另一个用CSS完成


image.png


下载解压


image.png


取出如下文件,在项目入口导入 iconfont.css,或者统一放入一个文件导入也行


image.png


编写组件


一个点赞,就叫Like


一个点踩,就叫Hate


Hate点踩组件


先写属性,就几个,不用注释也明白


分别是



  • 绑定值 modelValue

  • 默认颜色 defaultColor

  • 激活颜色 activeColor

  • 大小 size


image.png


HTML给个容器,再加个固定格式(xxx-container)的容器类名,防止样式穿透


再把iconfont样式加上,图标就出来了


这些用VSCode自定义代码片段都能自动生成


image.png


现在是这样的图标,也就是点赞


image.png


那么怎样让它变成点踩手势呢?答案是rotate旋转


加上下图类名有用吗? 答案是没用,他并不会旋转


image.png


为什么呢?因为i标签是行盒模型,必须变成块盒才行


方式有很多,不过我就爱flex,优点很多,这里就不赘述了


image.png


这不就转起来了吗


image.png


动效


加个鼠标移入变色效果,考虑到一会还有另一个点赞组件,那就写个通用的sass


接收一个激活颜色和默认颜色参数


image.png


导入并使用,下面用了个v-bind绑定当前被点击的颜色,下面来实现逻辑


image.png


上面接收了一个modelValue,类型是布尔值,当他为真时就把颜色改为激活颜色


于是就理所当然的使用计算属性


image.png


事件


当被点击或hover时,就激活图标颜色,现在还差点击


点击事件要做 3 件事



  1. 改变父组件的值

  2. 改变颜色

  3. 实现开头的丝滑动画


image.png


这里用一个showAnimation变量控制动画展示


再改变父组件的值,父组件的值一变,自己的颜色也会跟着变


还差个动画,只要让动画在其中一段不停反复横跳,即可实现国际友好手势


image.png


但这样有问题,只有第一次触发动画才有动效,后续需要改变showAnimation的值


那么怎么知道动画什么时候结束呢?


答案是事件onanimationend,只要在这个事件把动画关掉,点击时开启即可


image.png


ok,实现最主要的组件了,另一个点赞就是复制改东改西,大家都会


score.gif


组合组件


接下来需要把点赞和点踩组合一起,那就叫Gr0up


需要实现逻辑



  1. 根据父组件的值,动态展示

  2. 点击时传递一个事件,告诉父组件,究竟是点赞还是点踩

  3. 提供一个值,锁定按钮,因为点完之后一般都不能反悔

  4. 提供样式设置,传递到子组件


类型


传递一个固定类型的值,才能分辨是什么操作


image.png


null就是初始状态,无操作,另外俩见名知义


那么现在需要编写自身的状态


image.png



  • wasChoose是否有按钮被选中

  • disabled当被选中,同时父组件完成(done),就禁用


样式


image.png


初始化


建议写代码都提供一个入口,后面有空出一期如何像写诗一般写代码


image.png


事件


当被点击时,改变两个状态,没什么好说的,完成


image.png


测试


test.gif



源码: gitee.com/cjl2385/dig…



作者:寅时码
来源:juejin.cn/post/7316321509857034280
收起阅读 »

微观层面看为什么读书

我媳妇是一个不爱读书的人。她有时候会反问我为什么读书? 最初我回答读书可以增长见识,陶冶情操。再后来我回答读书可以提升认知,获得更好的解决方案,而且腹有诗书气自华。 然而随着时间推移,我总觉得上面的回答差点意思。具体差什么呢?差的是和自己的真实链接,简单来说就...
继续阅读 »

我媳妇是一个不爱读书的人。她有时候会反问我为什么读书?


最初我回答读书可以增长见识,陶冶情操。再后来我回答读书可以提升认知,获得更好的解决方案,而且腹有诗书气自华。


然而随着时间推移,我总觉得上面的回答差点意思。具体差什么呢?差的是和自己的真实链接,简单来说就是对自己的具体的实践层的意义。


回顾一下我的读书历程。


我是7岁上的小学一年级,后来按部就班的读初中高中,高中复读了,之后读大学读研。现在的感受高中以前的书都已经回忆不起来了。高中的书也只剩下零星点点,而且中学阶段主要都是教科书。


我读书的主要阶段还是大学之后。从这点来说,和现在的孩子相比,我读书读课外书的年纪真是太晚了。大学主要读的是文学性的书籍和所谓当时的显学经济学方面的。


我能想起来的文学性的书有法国司汤达的《红与黑》,罗曼罗兰的《约翰克利斯朵夫》、《简爱》。经济学方面的有曼昆《微观经济学》和《经济学原理》宏观经济分册,犹记得宏观经济分册一页一页的读完的。后来陆续也读了张五常的《经济解释》……其它肯定还有但是一时半会想不起来了。


网络小说也读了不少,反倒是网络小说记得很清晰。萧鼎的《诛仙》,土豆的《斗破苍穹》,纷舞妖姬的《弹痕》和《诡刺》,缘分0的《无尽武装》、《全能炼金师》、《天风》、《天纵商才》,还有三戒大师的历史类权谋小说《官居一品》《大官人》等;还有其他一些的历史类和玄幻类小说,想起来的《凡人修仙传》、《鬼吹灯》和《盗墓笔记》。


如果要推荐的话,喜欢军文的话,强烈推荐《弹痕》,还有《天风》;前者是现代军文,后者是古代军文。喜欢历史权谋的推荐《官居一品》。喜欢玄幻的推《凡人修仙传》、《鬼吹灯》和《盗墓笔记》。


为啥网络小说记得比正统文学清楚?一方面网络小说是兴趣;另一方面,最近这些年上面的一些小说拍成了动画片,让自己有机会回顾颓废的那些年,那些年熬夜看的小说。


言归正传。正统的文学名著,当时我特意将文中给我启迪的句子摘抄下来,但如今那些句子绝大部分已经是不知踪迹。能想起来的:这是最好的时代,这是最坏的时代……这出自名著《双城记》,但这本书我并没有读,还有那句:世界上有一种英雄主义,就是看清生活的真相以后依然热爱生活,出自罗曼罗兰的《名人传》,但这本书我也没读。


对于经济学,记住的:产权的界定的是市场经济的基本前提,这就是大名鼎鼎的科斯定理。


网络小说类脑袋里面只有小说情节,没有名句。


从上面来看,为什么读书?那些年读的书没有获得解决方案,也没有能大面积地提高认知。


我现在反思读书要有更加具体的目的性,要与个人的认知体系链接,与实践链接。提升特定认知,改善特定实践。


读书是一种主动的迭代认知体系的方式,但相比写文章,相比阐述,读书是一种相对被动的方式,因为读书往往不知道会读到什么具体而微的内容,也不知道这些具体而微的内容会与认知体系发生怎样的具体链接。


也因为这种被动性,相比较写文章,相比阐述,读书拥有更多可能性,这也正是一直说的开卷有益。


综上,读书是为了主动迭代认知体系,以一种相对开放的方式


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7316592822823010342
收起阅读 »

Android自定义锁屏实践总结

1. 背景 在我们的业务场景中,用户在完成下单后大部分的概率不再需要进入App做其他的操作,只需要知道当前的订单状态,为了方便用户在不解锁的情况下也能实时查看当前订单的状态,货拉拉用户 iOS端上线了灵动岛功能,用户的接受度较高,由于Android暂不支持灵动...
继续阅读 »

1. 背景


在我们的业务场景中,用户在完成下单后大部分的概率不再需要进入App做其他的操作,只需要知道当前的订单状态,为了方便用户在不解锁的情况下也能实时查看当前订单的状态,货拉拉用户 iOS端上线了灵动岛功能,用户的接受度较高,由于Android暂不支持灵动岛,所以我们自定义了一个锁屏页面。


2. 实践


  2.1 方案选择


  实现锁屏的方式有多种(锁屏应用、悬浮窗、普通Activity伪造锁屏等等),由于我们的业务场景简单只展示我们的订单状态,且不需要很强的保活干扰用户的操作,采用了普通的Activity伪造锁屏。


  2.2 方案原理


  锁屏的大概实现原理都很简单,监听系统的亮屏广播,在亮屏的时候展示自己的锁屏界面,自定义的锁屏界面会覆盖在系统的锁屏界面上,用户在自定义锁屏界面上进行一系列的动作后进入系统的解锁界面。



  2.3 代码实现


    2.3.1 锁屏页面


    锁屏页Activity在普通的Activity需要加上一些配置


      1. 在onCreate中设置添加Flags,让当前Activity可以在锁屏时显示



  • FLAG_SHOW_WHEN_LOCKED:使Activity在锁屏时仍然能够显示

  • FLAG_DISMISS_KEYGUARD:去掉系统锁屏页,设置了系统锁屏密码是没有办法去掉的,现在手机一般都会设置锁屏密码,该配置可基本忽略。


    this.window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
)

      2. 在AndroidManifest.xml中进行对锁屏页Activity进行配置



  • 主题配置,


      主要是配置锁屏Activity的背景为透明和去除过度动画,让锁屏Activity过渡到系统锁屏更自然






  • 启动模式配置



    • BroadcastReceiver中启动锁屏页Activity,需要添加Intent.FLAG_ACTIVITY_NEW_TASKflag,造成锁屏Activity单独创建一个history stack,会在最近任务中显示出来,通过配置excludeFromRecentsnoHistorytaskAffinity来规避这个问题。


      name=".lockscreen.LockScreenActivity"
    android:configChanges="uiMode"
    android:excludeFromRecents="true"
    android:exported="false"
    android:launchMode="singleInstance"
    android:noHistory="true"
    android:screenOrientation="portrait"
    android:taskAffinity="com.xxx.lockscreen"
    android:theme="@style/LockScreenTheme">


    ```



      3. Home键,Back键和Menu键事件的处理



  • Home键,由于不是用来替代系统锁屏的锁屏软件,不需要处理Home键事件.

  • Back/Menu键,重写onKeyDown让锁屏页不处理这两个事件


      override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    return when (event?.keyCode) {
    KeyEvent.KEYCODE_BACK -> true

    KeyEvent.KEYCODE_MENU -> true

    else -> super.onKeyDown(keyCode, event)
    }
    }
    ```



    2.3.2 广播


    LockScreenBroadcastReceiver是普通的BroadcastReceiver,不做其他的配置,需要注意两点:



  1. 动态注册/注销

  2. 在广播中启动Activity,需要添加FLAG_ACTIVITY_NEW_TASK,否则会出现“Calling startActivity() from outside of an Activity”的运行时异常



class LockScreenBroadcastReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {
intent?.let { handleCommandIntent(context, it) }
}

private fun handleCommandIntent(context: Context?, intent: Intent) {
when (intent.action) {
Intent.ACTION_SCREEN_OFF -> {
val lockScreen = Intent(this, LockScreenActivity::class.java)
lockScreen.setPackage("com.xxx.xxx")
lockScreen.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK )
context?.startActivity(lockScreen)
}
Intent.ACTION_USER_PRESENT -> {
// 处理解锁后才显示自定义锁屏Activity
}
}
}
}

    2.3.3 实现效果





3. 注意点


以下是在实践过程中的一些问题小结,供大家参考。


  3.1 权限相关


  不同手机系统上权限的名称,大体分为5种:



  • 后台弹窗

  • 悬浮窗

  • 显示在其他应用的上层

  • 锁屏展示

  • 后台弹出界面


    以及不同的组合效果也不同,以下是已测试过的手机,


品牌型号系统系统版本相关权限权限截图权限截图
华为P50HarmonyOSHarmonyOS 4.0.01. 悬浮窗 2.后台弹窗
oppoOPPO K9 5GColorOS 13Android 131. 悬浮窗 2. 锁屏显示
vivoY52sFuntouch OS 10.5Android 101. 悬浮窗 2. 锁屏显示 3. 后台弹出界面
一加OnePlus Ace ProColorOS 13Android 131. 悬浮窗 2. 锁屏显示
荣耀honor 60magic ui 6.1Android 121. 显示在其他应用的上层
iQOONeo3Origin OSAndroid 121. 悬浮窗 2. 锁屏显示 3. 后台弹出界面
Hi novaHi nova 9Emui 12Android 121. 后台弹窗 2. 悬浮窗 3. 显示在其他应用的上层

  OPPO/一加 手机特殊说明:在默认状态下在系统设置下找不到“锁屏显示”的入口,需要先授权“悬浮窗”权限再次启动应用会在应用启动时弹窗提示授权在锁屏上显示,然后在系统设置中会出现“锁屏显示”的入口。


  3.2 有些手机在未授权时,应用在前台时锁屏可以展示,但是应用退到后台不展示。


  Android 10 (API 级别 29) 及更高版本对后台应用可启动Activity的时间施加限制。这些限制有助于最大限度地减少对用户造成的中断,并且可以让用户更好地控制其屏幕上显示的内容。具体见官方文档


  3.3 在部分手机上,点亮屏幕后不会立即展示自定义的锁屏界面,在解锁系统锁屏后才会展示自定义的锁屏。1. 监听解锁事件主动finish自定义的锁屏页面


    Intent.ACTION_USER_PRESENT->{
ActivityUtils.getActivityList()?.forEach {
if ("com.xxx.lockscreen.LockScreenActivity" == it.componentName.className) {
it.finish()
}
}
}

2. 在自定义锁屏ActivityonResume中监听设备是否已解锁并finish锁屏页


override fun onResume() {
super.onResume()
val isInteractive = (getSystemService(Context.POWER_SERVICE) as PowerManager).isInteractive
val isKeyguardLocked = (getSystemService(KEYGUARD_SERVICE) as KeyguardManager).isKeyguardLocked
if (isInteractive && !isKeyguardLocked) {
finish()
}
}

3.4 当在自定义锁屏页触发Home键事件后,锁屏页Activity不再显示


提示用户根据自己的系统去授予对应的权限,不同系统所需的权限参考上面第1点


3.5 Android 8.0 透明主题造成闪退


  在Android 8.0系统上Activity满足了以下条件:



  1. targetSdkVersion > 26

  2. 透明主题

  3. 固定屏幕方向


会出现java.lang.IllegalStateException: Only fullscreen activities can request orientation


    // ActivityRecord.java
void setRequestedOrientation(int requestedOrientation) {
if (ActivityInfo.isFixedOrientation(requestedOrientation) && !fullscreen
&& appInfo.targetSdkVersion > O) {
throw new IllegalStateException("Only fullscreen activities can request orientation");
}
....
}

  建议针对Android 8.0以外的系统才固定屏幕方向,可参考Android 8.0系统透明主题适配解决办法


4. 总结


从线上最新的数据来看,接近60%的订单在锁屏后可以通过自定义锁屏查看到订单状态。


功能上线后发现比较少用户会主动选择关闭,从最开始的出发点就是为用户提供一个便捷的状态查看的入口,用户下完单等待司机接单以及接单后司机的状态都是用户会重点关注的,同时我们会过滤掉一些不太重要的状态的显示避免对用户带来不必要的干扰。


从实现的角度上来说整体较简单,较麻烦的是国内的ROM对权限的管控越来越严,且不同的系统同一权限的命名和授予方式差异较大,需要用更吸引用户的体验去引导用户授权。


作者:货拉拉技术
来源:juejin.cn/post/7316806159008841767
收起阅读 »

“来同学在用户点击后退的时候加个弹窗做个引导”

web
文章起因这篇文章的起因来源于一个需求,产品:“我需要在浏览器点击后退的时候,加一个弹框引导用户做其他的操作”,老实讲这一块之前研究了很多次通过popstate去监听,但是始终有问题没有达到理想的状态或者说并没有完全研究透彻,本来想直接回复做不了砍掉这里吧,后来...
继续阅读 »

文章起因

这篇文章的起因来源于一个需求,产品:“我需要在浏览器点击后退的时候,加一个弹框引导用户做其他的操作”,老实讲这一块之前研究了很多次通过popstate去监听,但是始终有问题没有达到理想的状态或者说并没有完全研究透彻,本来想直接回复做不了砍掉这里吧,后来出于职业道德的遵守还是说试试看吧!

后来

之后在网上遨游了一段时间找了很多实现方案最后发现有一个Api叫做prompt,他来自于react-router,正好 项目中目前使用的路由就是react-router

import { Prompt } from 'react-router' //v5.2.0版本

我不太清楚看到这篇文章的同学有没有用到过这个Api,我大致介绍一下用法

const App = () =>{
const [isBlocking, setIsBlocking] = useState(true)

return <>
<Prompt
//这里是个Boolean 控制是否监听浏览器的后退 默认监听
when={isBlocking}
message={(_, action) =>
{
if (action === 'POP') {
Dialog.show({ //普普通通的弹框而已,,,,
title: '提示',
actions: [
{
key: 'back',
text: '回到浪浪山',
onClick() {
history.go(-1)
//用户选择按钮之后关闭掉监听
setIsBlocking(false)
},
},
{
key: 'saveAndBack',
text: '去往光明顶',
onClick: async () => null
},
{
key: 'cancle',
text: intl.t('取消'),
},
],
})
return false // 返回false通知该组件需要拦截到后退的操作 将执行权交给用户
}
return true //返回true 正常后退不做拦截
}}
>Prompt>

{/* ...内容部分 */}

}
export default App

上面这样可以实现我的需求,但是因为之前研究过这好一阵子那会并没找到这个Api,现在找到了本着一种知其然知其所以然的态度,深究一下内部到底是怎么实现可以禁止浏览器后退的,如果你不知道就跟着一起寻找一下吧,可能需要占用一杯咖啡的时间☕️

| Prompt

最初的想法就是直接去看Prompt实现的源码就好了,看看是怎么实现的这里的逻辑 其实在看之前内心是有一些猜测的觉得可能是下面这样做的

  • 可能是有一些浏览器提供的api但是我不清楚可以直接做到禁止后退,然后Prompt内部有调用
  • 或者是先添加了浏览器记录然后在后退的时候监听又删除

git上找react-router源码,注意要切换到对应的版本V5.2.0,免得对不上号

react-router5.2.0版本对应的链接🔽

github.com/remix-run/r…

从这个链接点击去之后我们可以看到Prompt方法,主要是下面这一段我们捡重点解析一下

  1. 获取history上面的block
  2. 在初始化阶段将我们的message传递到block中执行,并且获取到当前的unblock
  3. 离开的时候执行self.release()执行卸载操作
/**
* The public API for prompting the user before navigating away from a screen.
*/

function Prompt({ message, when = true }) {
return (

{context => {
invariant(context, "You should not use outside a ");

if (!when || context.staticContext) return null;

// 这个context是当前使用的环境上下文我们内部路由跳转用的history的包
const method = context.history.block;

return (
{
//初始化的阶段执行该方法
self.release = method(message);
}}
onUpdate={(self, prevProps) => {
if (prevProps.message !== message) {
self.release();
self.release = method(message);
}
}}
onUnmount={self => {
self.release();
}}
message={message}
/>
);
}}

);
}

既然看到了这里再继续看下 Lifecycle 方法其实这个里面就是执行了一些生命周期,这里就不解析了贴个图大家自己看下吧,都能看懂

github.com/remix-run/r…

image.png

到了这里可能有些疑惑,这里好像并没有什么其他操作就是调用了一个block,起初我以为block是原生history上面提供的方法后来去查了一下api发现上面并没有这个方法,此刻就更加好奇block内部的实现方式了

因为我们项目的上下文里面使用的history是来自于一个npm包,后面就继续看看这个history内部怎么实现的

import { createBrowserHistory } from 'history'
const history = createBrowserHistory()

createBrowserHistory

传送门在这里👇🏻感兴趣的同学直接去看源码

github.com/remix-run/h…

直接看里面的block方法

let isBlocked = false

const block = (prompt = false) => {
const unblock = transitionManager.setPrompt(prompt)

if (!isBlocked) {
checkDOMListeners(1)
isBlocked = true
}

return () => {
if (isBlocked) {
isBlocked = false
checkDOMListeners(-1)
}

return unblock()
}
}

现在来分析一下上面的代码

  1. prompt是我们传进来的弹框组件或者普通字符串信息
  2. transitionManager是一个工厂函数将prompt存到了函数内部以便后面触发的时候使用
  3. checkDOMListeners去做挂载操作就是监听popstate那一步
  4. 返回出去的函数是在外面在离开的时候做销毁popstate监听的

现在按照上面的步骤在逐步做代码分析,下面会看具体的部分有些不重要的地方会做删减

| transitionManager.setPrompt

  • 可以看到工厂函数里面存储了prompt
  • 销毁的时机是在上面unblock的时候执行重置prompt
  const createTransitionManager = () => {
let prompt = null
const setPrompt = (nextPrompt) => {
prompt = nextPrompt
return () => {
if (prompt === nextPrompt)
prompt = null
}
}
return {
setPrompt,
}
}

| checkDOMListeners

  • 上面默认传了1初始化的时候会进行popstate监听
  • 离开的时候传了-1移除监听
let listenerCount = 0
const checkDOMListeners = (delta) => {
listenerCount += delta
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState)
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState)
}
}

| handlePopState

  • 调用getDOMLocation获取到一个location
const handlePopState = (event) => {
handlePop(getDOMLocation(event.state))
}
  • getDOMLocation 内部调用createLocation创建了一个
  • createLocation内部大家感兴趣可以自己去看一下,没有什么可讲的就是创建一些常规的属性
  • 比如state、pathname之类的

const getDOMLocation = (historyState) => {
const { key, state } = (historyState || {})
const { pathname, search, hash } = window.location

let path = pathname + search + hash

if (basename)
path = stripBasename(path, basename)

return createLocation(path, state, key)
}

那我们现在知道getDOMLocation是创建一个location并且传递到了handlePop方法内部现在去看看这个内部都干了啥

| handlePop

  • 我们要看的主要在else里面
  • confirmTransitionTo是我们上面提到的工厂函数里面的一个方法
  • 该方法内部执行了Prompt并返回了Prompt执行后的结果

let forceNextPop = false

const handlePop = (location) => {
if (forceNextPop) {
forceNextPop = false
setState()
} else {
const action = 'POP'

transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => {
if (ok) {
setState({ action, location })
} else {
revertPop(location)
}
})
}
}

敲黑板 重点来了!!!

现在来看下confirmTransitionTo内部的代码

const confirmTransitionTo = (location, action, getUserConfirmation, callback) => {
if (prompt != null) {
const result = typeof prompt === 'function' ? prompt(location, action) : prompt

if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback)
} else {
callback(true)
}
} else {
// 重点在这里,result是我们调用block时候的返回参数 true or false
// 如果返回false 那浏览器回退将被禁止 反之则正常
callback(result !== false)
}
} else {
callback(true)
}
}

所以现在回到上面的handlePop函数我们就能推测出如果我们回调中返回的false,说明我们想阻止浏览器的回退操作,那么执行的就是revertPop方法(其实名字大家可能也能猜出来 恢复 pop操作😂)

| revertPop

  • delta的逻辑是计算从开始到目前为止走过的路径做个差值计算
  • 这个时候正常来讲delta应该是1
  • 我们看最后一个逻辑就好这里是禁止撤回的重点
  • 当delta为1的时候就执行了go(1)
  • go方法内部实际调用了window.history.go(n)
const revertPop = (fromLocation) => {
const toLocation = history.location

let toIndex = allKeys.indexOf(toLocation.key)

if (toIndex === -1)
toIndex = 0

let fromIndex = allKeys.indexOf(fromLocation.key)

if (fromIndex === -1)
fromIndex = 0

const delta = toIndex - fromIndex

if (delta) {
forceNextPop = true
//window.history.go
go(delta)
}
}

之前我看到这里有个疑问就是如果最后的结果只是调用了go的话,那这个好像我们自己监听也可以实现一下于是就有了以下代码

function History() {
this.handelState = function (event) {
history.go(1)
}

this.block = function (Prompt) {
window.addEventListener('popstate', this.handelState)
return () => {
window.removeEventListener('popstate', this.handelState)
}
}
}

const newHistory = new History()

等到我实验的时候发现页面回退确实阻止住了,但是会闪一下上一个页面,给大家举个例子

Step1
我从PageA页面一路push到PageC
PageA -> PageB -> PageC

Step2
从PageC页面点击返回,之后页面的过程是这样的
PageC -> PageB -> PageC

就是说我本应该在PageC点击撤回,理想的效果是就停留在了PageC页面,但是目前效果是先回到了PageB因为我使用了go(1)就又回到了PageC,相当于在点击回退的时候多加载了PageB页面

这使我又陷入了沉思,其实研究到这里如果不把这个弄懂之前的努力就白费了,抱着这种想法又扎到了history代码中遨游一番

之后光看代码捋逻辑对这确实有些迷茫,没有办法只能开始调试history的源码了,这里比较简单,history源码下载下来之后做几个步骤

  1. 安装history相关依赖package
  2. 启动服务会有一个本地域名

image.png

  1. 之后在你真实项目中引入这个资源开始做调试

后面其实就一直打log和断点不断调试history源码查看执行路径,发现了问题所在

刚才上面提到的handlePop方法内部有一段代码那会忽略掉了,就是ok为true的时候,因为之前一直关注false的情况忽略了这里,后面把这个里面就研究了一下才明白其中的原委

if (ok) {
setState({ action, location })
} else {
revertPop(location)
}

| setState

这个方法做了几件事

  • 更新本地history状态,nextState可以理解为下一个目标地址其中包含location和action
  • 更新本地history的长度这里没有完全搞懂为什么要更新一下长度,但是猜测可能是为了和原生history状态一直保持同步吧防止出现意外情况
  • 这里又看到了transitionManager工厂函数,此时调用的notifyListeners这个就是解决我们上面的谜团所在
const setState = (nextState) => {

// 1.更新本地history状态
Object.assign(history, nextState)

history.length = globalHistory.length

//2.更新依赖history相关的订阅方法
transitionManager.notifyListeners(
history.location,
history.action
)
}

| notifyListeners

notifyListeners更新订阅的方法,我直接把这块代码贴出来了,一个发布订阅模式没什么好讲的

  let listeners = []

const appendListener = (fn) => {
let isActive = true

const listener = (...args) => {
if (isActive)
fn(...args)
}

listeners.push(listener)

return () => {
isActive = false
listeners = listeners.filter(item => item !== listener)
}
}

const notifyListeners = (...args) => {
listeners.forEach(listener => listener(...args))
}

重点的地方是react-router内部会调用history中的listen,这个listen方法会调用上面的appendListener进行存储,之后在合适的时间点执行react-router中传递的方法

这些方法的入参是目标页面的history属性(location,action),在接收到参数的时候根据参数中的location更新当前的页面

现在可以得出结论我们上面的例子不能成功的原因,是因为我们在执行的过程中没有绕过setState(因为此刻没有能让ok返回false的操作)所以当我们页面路径变更的时候自然页面也会更新

最后整体捋一下这个流程吧

到这里其实细心的同学会发现浏览器的回退我们确实是控制不了的只要点击了就一定会执行后退的操作。在history中针对block方法来说做的事情其实就下面这几步

  1. 封装了一个自己本地的history,当然跳转能力等还是依赖原生的history
  2. 在URL路径变更的时候history可以决定是否通知单页面应用的路由
  3. 如果通知了就相当于我们的ok是true,需要页面也更新一下
  4. 如果未通知相当于ok是false,就不需要页面更新

这就是为什么history里面的block为什么可以用go就能实现当前页面回退,本质上浏览器历史记录确实回退了,但是history并没有通知应用页面更新,并且继续执行go(1)这样给用户看到的视角就是页面没有回退并且url也没有变化

结论

其实在使用的时候history还是有一些问题如果当前页面reload了,那么revertPop里面的go就不会执行,因为此时的delta是0,这样就会导致即使页面没有变化但是url更新成了上一个记录

说一下我的看法这可能是这个history遗留的bug也可能是有意而为之但是也不重要了我们搞懂了原理就好。

其实浏览器后退加监听的行为感觉还是一个比较高频的需求操作,后面我打算自己写一个插件专门就做后退拦截的操作~

到底了------

今天圣诞节了 祝你们圣诞节快乐 Merry Christmas🎄🎄🎄 !!!希望都能收到喜欢的礼物🎁


作者:零狐冲
来源:juejin.cn/post/7316202778790477834

收起阅读 »

23岁前端的2023年年度总结(上)分手、离职、旅行

前言 从三个月前就在想年终总结写什么,真的把手放在键盘上就不知道从何开口了,足足在电脑面前发呆了一个小时才开始写..... 前几年可能真的在无限重复一天或者两天,上班 下班 吃饭 打游戏 睡觉 然后无限循环这一天。但今年一年其实真没白活,酸甜苦辣咸全都经历了;...
继续阅读 »

前言


从三个月前就在想年终总结写什么,真的把手放在键盘上就不知道从何开口了,足足在电脑面前发呆了一个小时才开始写.....


前几年可能真的在无限重复一天或者两天,上班 下班 吃饭 打游戏 睡觉 然后无限循环这一天。但今年一年其实真没白活,酸甜苦辣咸全都经历了;


分手


2023年1月末 过完年从大连坐飞机回到南京,这个冰冷的出租屋只剩下我一个人还有三只猫,少了另一个人的存在,我们从2018年6月14号在一起,到1月26号分手。


南京租的是个商水商电的公寓因为她两年前就说想住loft,1块7一度的电费让我只能下班回家的三五个小时舍得开一会空调甚至她不在的时候我就忍着了否则只要人在家就开空调的话一个月估计要三五百的电费。


从确定分手到我找好房子搬走有一个半礼拜,那个屋子还是阴面, 那几天下班回到那个出租屋让我感觉像住在监狱里,阴暗 冰冷 孤独,一切负面心情几乎全部聚在那里那刻。


我们分手的主要原因是双方家庭谈彩礼和买房的时候没有谈妥,聊崩了,她家是安徽的我家是辽宁的,详细的我就不在这说了,简单明了一句话就是 彩礼和房她们要的我们给不起,就这样。


在一起时开玩笑说分手了就学电影里男女主角我剃光头她剪短发,想到这我就拿推子把头剃了 但我没有让她知道 我怕她也真的剪了短发。


1703354451666.png


恢复理智后就是开始给她收拾东西,因为她家里人不让她回来见我怕我们旧情复燃,所以给了我地址我把她的行李都收拾好她的衣服一件件叠好装进箱子,也不知道崩溃了多少次,明明上个月这个时候我还看着她洗澡前把项链拿下来挂在浴室墙壁,现在项链就一动不动的挂在浴室墙壁上 而我站在墙壁面前看着这个项链愣愣的发呆,不知道多久后还是拿了下来小心翼翼的装进盒子里再放进箱子继续收拾。


1703354650104.png


她的行李寄走后随之也就是收拾我的行李、找房子、搬家。彻底结束维持了四年半的恋爱。


新的房间是独门独户的,我自己一个人住,是阳面的房间 而且楼层比较低,在三楼 经常能听见孩子们在小区里喧闹的声音,这给了我极大的治愈。而且我很喜欢收拾屋子,我觉得很解压,收拾干净后换上自己喜欢的四件套、墙壁挂布、地毯、玩偶 我住这也舒服;


也正巧那几天工作也忙,我们办公室算我老大在内就三个前端,老大也基本是干指挥官的活,另一个前端又有些菜,所以又难又累的活基本都是我来干,工作繁忙起来让人也就不会想那么多消耗情绪的事情。


1703354974708.png


(也是这个时候发现了掘金的新大陆——沸点)


这个时候活干的其实正起劲呢,因为项目业务流程都已经轻车熟路了,而且我老大也很器重我。


还在想什么时候有时间系统学习一下react18,现在是会用,但也只是工作推进我才会用的 并没有vue掌握的那么熟练,甚至如果面试让我说react可能都说不上来什么东西。想要技术更精进一些还是要花点时间好好学一下。


22年秋天开始也迷上了撸铁,不敢去健身房怕被健身教练pua买课,怕被大佬笑话,怕器械不会用,就买了一对哑铃在家里自己学自己练,看视频博主很多教的怎么吃 怎么练 胸怎么练胳膊怎么练 肩膀怎么练 分手一个月后缓过来了健身更有动力了。


1703357464564.png


然后自己放假的时候也开始往周边的城市走走,以前放假都是屯在家里,懒得出门,现在发现没事经常出去转转真的挺好的。独自去了杭州、苏州、镇江,拍照片 看看这看看那....


image.png
1703357797167.png
1703357763596.png

似乎生活在往好的方向走....


音乐


鄙人不才,从小喜欢听歌唱歌研究歌,目前在网易云有三首个人单曲,全部是自己作词作曲演唱。


分手这件事也给我很大的情绪冲击,缓和以后就着一点点余动把年前的歌给写完了,名叫《背靠月亮的夜晚》JYM感兴趣可以直接去网易云搜,好听的话也麻烦给个小红心支持下。


image.png


为什么这段落的title叫音乐呢因为不只是发歌,还去听歌;


4月份一个人去看了伏仪的现场
现场听了《昨日记书》《神离少年》《我会在每个有意义的时辰》等


1703357057722.png


5月份很小哥们去看了黄绮珊 黄妈的演唱会
《年轮》《离不开你》《无聊的》等


1703356969387.png


6月份一个人去看了暗杠的现场
《说书人》《走歌人》《童话镇》等


image.png


前几年和前女友在一起生活好像从没有出去旅游,去看演唱会,不是说不去而是当时好像就没有这个意识,放假了就只想两个人买一堆好吃的在家里看电影打游戏,一天一天就这么过去了


离职


我一直觉得工作和谈恋爱有异曲同工之处,比如,找工作要看条件,是否符合预期,看通勤,看薪资,也要看自己能不能干的了这份活,谈恋爱也是要看对方性格,长得好不好看,三观合不合,在一起时候能长久,还有 一点点说不清道不明的缘分。


六月份我已经彻底从分手的阴影种走了出来,并且攒了一小笔钱,打算攒够三万买摩托车,当时想买的款式是GSX250。但天有不测风云,甲方集体转移去广州,听说北京那一拨 还给了外包赔偿 我天真的以为我们也有赔偿 所以当听到甲方要转移的时候我还挺乐呵,因为我干了四年了,从没拿过赔偿,一直是外包 说辞就辞了。


1703358250821.png


但其实还是很不舍的,人对于一个地方呆久了就会有依赖,如果离开这里去新的地方就会有一种陌生的感觉,这感觉会让人不舒服。这家公司从通勤、薪资、办公环境、同事氛围、加班不管各方面看都很满意了,很知足的。如果不开除我我觉得干个三年都不成问题。而且之前老大还跟我说过有个新项目要下来从零到一打算全权让我独立负责,我当时有些高兴又有些担心,高兴是会得到前所未有的挑战,以后简历上也是亮眼的一笔。担心是我其实也怕我技术不顶,倒时候再给惹祸,而且也担心到时候会不会天天加班.....现在好了,不用担心了。


南京的外包没有给赔偿而是给安排其他的甲方公司,没过多久我收到了人事给我安排的面试,当天下午就接到了电话,问的也都不难,但是实在是不想去,因为第一:目前这家公司待了一年多了 同事也混得很熟了去了谁都不认识。第二:这家公司距离我住得地方很远 每天上下班通勤要花费现在的通勤1.5倍的时间


就也没有去,窝囊的选择了自离。


正好也想去西藏很久了,什么青春没有售价这种话都要听烂了,也攒了一点钱,索性就买票去了


1703487161165.png


谁帮我看看图片的上面写的啥(doge)


旅行


网上说去西藏做有意义的事就是捡队友和被捡,正好我也是一个人去的,我就往这一坐(内心: 我看谁来捡我)
甚至做好了没人捡的准备,就打算自己玩了。
不过好在也确实被捡到了,一路上认识了好几个处的不错的朋友 半年多了过去了现在也还经常聊天。


这块要详细写的话太多了写不过来,直接给你们看我当时记录的朋友圈吧 你们有什么问题的话可以评论区或者私信问比如高 去了哪些地方 报团多少钱之类的


1703474524831.png
1703474541584.png
1703474554710.png
image.png
image.png

最后一个朋友圈写错了,应该是第七天了。


到这基本前半年就过完了,欲知下半年如何,请看另章分写


1703474612496.png


作者:牛油果好不好吃
来源:juejin.cn/post/7316115845095817251
收起阅读 »

99年师弟,揭露华为工作的残酷真相

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。 师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研...
继续阅读 »

前几天,一位师弟约我吃饭,师弟跟我同校同一个专业,2022年本科毕业,比我低很多届,目前在华为工作。我们聊了很多母校的共同回忆,最近几年应届生求职的情况,也聊到了他在华为的工作经历。


师弟性格开朗,为人谦和,属于在学校各大社团特别混得开的那种,成绩也不错,保研到本校,但拿到华为offer后,放弃了继续深造,选择直接就业。


聊到当今应届生求职现状时,感慨万千。


我的母校是一所普通211,部分专业比较有特色,在行业属于领先地位,但学校整体名气一般。


记得我们大四毕业时,就业行情还比较好,每个班都有好几个同学进入了华为。读研后,实验室的师兄师姐基本都拿到了华为offer,等我们开始找工作时,大部分同学也都拿了华为offer,华为基本属于大家的保底选择。


师弟去年毕业时,他是专业唯一拿到华为offer的学生。这几年的就业形势,本科生基本找不到什么好工作,除了继续读书深造,没有更好的选择。


短短的几年,社会瞬息万变,就像坐上了一辆过山车,身处其中的人,也只能仅仅抓住车身的杠子,在急速下坡时,不让自己摔下来。


图片


师弟进入华为,原本是一件很开心的事,短短一年,经历职场拷打,再也没有大学时代那份锐气。


华为属于员工人数最多的科技公司之一,每年吸收了大批校招生,但员工离职率也非常高。


师弟反馈,跟他同一批进去的人,很多干了几个月就离开了,他是那一批少数还留下来的人。


刚入职时,每个人都要参加新员工入职培训,那时候大家都很骄傲和自豪,也非常认同企业的价值观和文化。


但当真正的分到项目组开始工作时,发现大家实际工作情况,和价值观上说的,并不不太一样。在基层落地时,价值观更多是挂在墙上的标语。


领导也基本不太关心员工的成长和未来发展,更多是把每一个员工当作生产工具,哪一件工具用的顺手、更能出活儿,就会用谁;还没证明过自己的工具,或者经验匮乏的工具,很有可能会被无情抛弃。


华为的假期还可以,但大家有什么事,都不太敢请假。即便是生病,如果不是非住院不可,也不太会请假;公司支持请假,但大家还是轻伤不下火线,请假会影响出勤系数,各种与之相关的奖金,也会大打折扣。


……


师弟还分享了工作以来其他的一些心得体会,一句话总结:职场很残酷,相比起来,大学轻松太多了。


图片


在我看来,华为还是中国一流的科技公司。大部分人,还是要挤破脑袋才能进得去。


华为有很好的制度,比如员工转岗,门槛还不太高。华为支持不同工种互相转岗,比如,干技术的,有一天不想写代码了,可以转人力,或者转销售。开发太累,也可以转做测试。公司会提供基本的业务培训,还有三个月的考核期,只要达到一定的用人标准,就能转岗成功。


华为还有很好的奖励制度,只要能吃苦,且绩效不太差,能坚持下来,很多人最后都获得了丰厚的回报。比我高一两届的师兄师姐,还有跟我同一届的很多朋友,现在都还在华为,过得还不错。


华为还有比较完善的福利体系。在各地的办公区,远离市区,地价也很便宜,平时在公司会有夜宵、员工餐厅、下午茶和健身房各种福利,也有些地方,会有单身公寓。进入华为后,除了工作,也没什么社交,公司能满足日常的衣食住行。平时挣的钱,基本都能存下来,成家后,在华为附近买房,也基本衣食无忧了。


华为是一家跨国公司,如果是干技术支持或销售,还能享受去各国打卡的福利。在年轻的时候,能去更远的地方走一走,也是一件很宝贵的人生经历。


图片


华为是中国众多私企大厂的一个缩影,也是最早践行狼性文化的一家公司。


如今的字节、阿里等互联网企业,也在走华为的老路。


这些大厂,给了年轻人一个令人向往的机会,但并不适合每个人。如果我们能早些看清现实,自己只是大厂的一颗螺丝钉,需要做的是,尽快掌握螺丝钉需要具备的技能,与此同时,如果借助大厂的势能,多开眼界,多认识一些各个领域的前辈和牛人,把企业能提供的资源用到极致,就更了不起了。


至于在公司里寻找朋友、寻找归属感或者安全感,那并不是一个好选择。同事间的八卦,公司股价的涨跌,CEO的致辞,也并不是我们需要关心的事。


最重要的是,无论在大厂还是小厂,在国企还是私企,都要拥有良好的心态。最怕的是,在国企羡慕华为的工资;在大厂,又羡慕国企的朝九晚五。


人生在世,各有各的不易。每一种选择,都没有对错, 选择了其中一个,在另外一部分,就得有所取舍。


在任何时候,学会“自洽”,与生活和解,与自己和解很重要。


作者:工程师酷里
来源:juejin.cn/post/7316349124600168484
收起阅读 »

浅谈Vue3的逻辑复用

web
Vue3的逻辑复用 使用了 Vue3 Composables 之后,逻辑复用比之前要顺手很多,这里说说前端开发最常用的场景,管理页面的列表查询和增删改查的逻辑复用,欢迎大家共同探讨。 用免费的 render 服务搭建了个在线的预览地址,源码在这里,用了免费的 ...
继续阅读 »

Vue3的逻辑复用


使用了 Vue3 Composables 之后,逻辑复用比之前要顺手很多,这里说说前端开发最常用的场景,管理页面的列表查询和增删改查的逻辑复用,欢迎大家共同探讨。


用免费的 render 服务搭建了个在线的预览地址源码在这里,用了免费的 node 环境和免费的 pg 数据库,对这部分有兴趣的可以看看我以前的分享,我写了个部署 spring-boot 的分享,使用 node 就更简单了。


可能每个人的具体工作内容不一致,但是应该都完成过这样的工作内容:



  1. 列表查询,带分页和过滤条件

  2. 新增,修改,查看,删除

  3. 进行一些快捷操作,比如:激活、通过


这些最基础的工作可能占用了我们很大的时间和精力,下面来讨论下如何逻辑复用,提高工作效率


需求分析


一个后台管理中心,绝大部分都是这种管理页面,那么需要:



  • 首先是封装一些通用的组件,这样代码量最低也容易保持操作逻辑和 UI 的一致性

  • 其次要封装一些逻辑复用,比如进入页面就要进行一次列表查询,翻页的时候需要重新查询

  • 最后需要有一些定制化的能力,最基本的列需要自定义,页面的过滤条件和操作也不一样


统一复用



  1. 发起 http 请求

  2. 展示后端接口返回的信息,有成功或者参数校验失败等信息


列表的查询过程



  1. 页面加载后的首次列表查询

  2. 页面 loading 状态的维护

  3. 翻页的逻辑和翻页后的列表重新查询

  4. 过滤条件和模糊搜索的逻辑,还有对应的列表重新查询


新增 Item、查询 Item、修改 Item



  1. form 在提交过程的禁用状态

  2. 发起网络请求

  3. 后端接口返回的信息提示

  4. 列表重新查询


删除 Item



  1. 删除按钮状态的维护(需要至少一个选中删除按钮才可用)

  2. 发起网络请求

  3. 后端接口返回的信息提示

  4. 列表重新查询


定制化的内容



  1. table 的列数据

  2. item 的属性,也就是具体的表单

  3. 快捷操作:改变 user 激活状态

  4. 列表的过滤条件


成果展示


img



  1. 打开页面会进行一次列表查询

  2. 翻页或者调整页面数量,会进行一次列表查询

  3. 右上角的是否激活筛选状态变更会进行一次列表查询

  4. 右上角模糊搜索,输入内容点击搜索按钮会进行一次列表查询

  5. 点击左上角的新增,弹出表单对话框,进行 item 的新增

  6. 点击操作的“编辑”按钮,弹出表单对话框,对点击的 item 进行编辑

  7. 点击“改变状态”按钮,弹出确认框,改变 user 的激活状态

  8. 选中列表的 checkbox,可以进行删除


代码直接贴在下面了,使用逻辑复用完成以上的内容一共 200 多行,大部分是各种缩进,可以轻松阅读,还写了一个 Work 的管理,也很简单,证明这套东西复用起来没有任何难度。


<template>
<div class="user-mgmt">
<biz-table
:operations="operations"
:filters="filters"
:loading="loading"
:columns="columns"
:data="tableData"
:pagination="pagination"
:row-key="rowKey"
:checked-row-keys="checkedRowKeys"
@operate="onOperate"
@filter-change="onFilterChange"
@update:checked-row-keys="onCheckedRow"
@update:page="onUpdatePage"
@update:page-size="onUpdatePageSize"
/>

<user-item :show="showModel" :item-id="itemId" @model-show-change="onModelShowChange" @refresh-list="queryList" />
</div>
</template>

<script setup name="user-mgmt">
import { h, ref, computed } from 'vue';
import urls from '@/common/urls';
import dayjs from 'dayjs';
import { NButton } from 'naive-ui';
import BizTable from '@/components/table/BizTable.vue';
import UserItem from './UserItem.vue';
import useQueryList from '@/composables/useQueryList';
import useDeleteList from '@/composables/useDeleteList';
import useChangeUserActiveState from './useChangeUserActiveState';

// 自定义列数据
const columns = [
{
type: 'selection'
},
{
title: '姓',
key: 'firstName'
},
{
title: '名',
key: 'lastName'
},
{
title: '是否激活',
key: 'isActive',
render(row) {
return row.isActive ? '已激活' : '未激活';
}
},
{
title: '创建时间',
key: 'createdAt'
},
{
title: '更新时间',
key: 'updatedAt'
},
{
title: '操作',
key: 'actions',
render(row) {
return [
h(
NButton,
{
size: 'small',
onClick: () => onEdit(row),
style: { marginRight: '5px' }
},
{ default: () => '编辑' }
),
h(
NButton,
{
size: 'small',
onClick: () => onChangeUserActiveState(row),
style: { marginRight: '5px' }
},
{ default: () => '改变状态' }
)
];
}
}
];

// 自定义右上角筛选
const filters = ref([
{
label: '是否激活',
type: 'select',
value: '0',
class: 'filter-select',
options: [
{
label: '全部',
value: '0'
},
{
label: '已激活',
value: '1'
},
{
label: '未激活',
value: '2'
}
]
},
{
label: '',
type: 'input',
placeholder: '请输入姓氏',
value: ''
}
]);

// 筛选变化,需要重新查询列表
const onFilterChange = ({ index, type, value }) => {
filters.value[index].value = value;
queryList();
};

// 自定义查询列表参数
const parmas = computed(() => {
return {
isActive: filters.value[0].value,
like: filters.value[1].value
};
});

// 封装好的查询列表方法和返回的数据
const { data, loading, pagination, onUpdatePage, onUpdatePageSize, queryList } = useQueryList(urls.user.user, parmas);

// 经过处理的列表数据,用于在 table 中展示
const tableData = computed(() =>
data.value.list.map(item => {
return {
...item,
createdAt: dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss'),
updatedAt: dayjs(item.updatedAt).format('YYYY-MM-DD HH:mm:ss')
};
})
);

// 删除列表相关逻辑封装
const { checkedRowKeys, onCheckedRow, deleteList } = useDeleteList({
content: '确定删除选中的用户?',
url: urls.user.userDelete,
callback: () => {
queryList();
}
});

// 列表中的快捷操作
const operations = computed(() => {
return [
{
name: 'create',
label: '新增',
type: 'primary'
},
{
name: 'delete',
label: '删除',
disabled: checkedRowKeys.value.length === 0
}
];
});

// 触发操作函数
const onOperate = function (name) {
operationFucs.get(name)();
};

// 新创建 item
const create = () => {
showModel.value = true;
itemId.value = 0;
};

const onModelShowChange = () => {
showModel.value = !showModel.value;
};

const itemId = ref(0);

// 控制模态对话框
const showModel = ref(false);

// 编辑 item
const onEdit = row => {
itemId.value = row.id;
showModel.value = true;
};

const { changeUserActiveState } = useChangeUserActiveState();

// 改变激活状态
const onChangeUserActiveState = row => {
changeUserActiveState({
id: row.id,
isActive: !row.isActive,
loading,
callback: () => {
queryList();
}
});
};

// 指定 table 的 rowKey
const rowKey = row => row['id'];

// operation 函数集合
const operationFucs = new Map();
operationFucs.set('create', create);
operationFucs.set('delete', deleteList);
</script>

<style lang="scss">
.user-mgmt {
height: 100%;
.filter-select {
.biz-select {
width: 100px;
}
}
}
</style>



作者:hezf
来源:juejin.cn/post/7316349124600315940
收起阅读 »

h5端调用手机摄像头实现扫一扫功能

web
一、前言 最近有遇到一个需求,在h5浏览器中实现扫码功能,其本质便是打开手机摄像头定时拍照,特此做一个记录。主要技术栈采用的是vue2,使用的开发工具是hbuilderX。 经过测试发现部分浏览器并不支持打开摄像头,测试了果子,华子和米,发现夸克浏览器无法打...
继续阅读 »

一、前言



最近有遇到一个需求,在h5浏览器中实现扫码功能,其本质便是打开手机摄像头定时拍照,特此做一个记录。主要技术栈采用的是vue2,使用的开发工具是hbuilderX。


经过测试发现部分浏览器并不支持打开摄像头,测试了果子,华子和米,发现夸克浏览器无法打开摄像头实现功能。



h5调用摄像头实现扫一扫只能在https环境下,亦或者是本地调试环境!!


image.png


二、技术方案



经过一番了解之后,找到了两个方案


1.使用html5-qrcode(对二维码的精度要求较高,胜在使用比较方便,公司用的是vue2,因此最终采用此方案)


2.使用vue-qrcode-reader(对vue版本和node有一定要求,推荐vue3使用,这里就不展开说了)



三、使用方式


image.png


当点击中间的扫码时,设置isScanning属性为true,即可打开扫码功能,代码复制粘贴即可放心‘食用’。


使用之前做的准备



通过npm install html5-qrcode 下载包


引入 import { Html5Qrcode } from 'html5-qrcode';



html结构
<view class="reader-box" v-if="isScaning">
<view class="reader" id="reader"></view>
</view>

所用数据
data(){
return{
html5Qrcode: null,
isScaning: false,
}
}


methods方法
openQrcode() {
this.isScaning = true;
Html5Qrcode.getCameras().then((devices) => {
if (devices && devices.length) {
this.html5Qrcode = new Html5Qrcode('reader');
this.html5Qrcode.start(
{
facingMode: 'environment'
},
{
focusMode: 'continuous', //设置连续聚焦模式
fps: 5, //设置扫码识别速度
qrbox: 280 //设置二维码扫描框大小
},
(decodeText, decodeResult) => {
if (decodeText) { //这里decodeText就是通过扫描二维码得到的内容
this.action(decodeText) //对二维码逻辑处理
this.stopScan(); //关闭扫码功能
}
},
(err) => {
// console.log(err); //错误信息
}
);
}
});
},

stopScan() {
console.log('停止扫码')
this.isScaning = false;
if(this.html5Qrcode){
this.html5Qrcode.stop();
}
}

css样式
.reader-box {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}

.reader {
width:100%;
// width: 540rpx;
// height: 540rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

四、最终效果


image.png


如有问题,欢迎指正,若此文对您有帮助,不要忘记收藏+关注!


作者:极客转
来源:juejin.cn/post/7316795553798815783
收起阅读 »

Java中的双冒号运算符(::)及其应用

在Java 8引入的Lambda表达式和函数式接口之后,双冒号运算符(::)成为了一项重要的功能。它可以将方法或构造函数作为参数传递,简化了编码和提升了代码的可读性。本文将介绍Java中的双冒号运算符及其常见应用场景。 双冒号运算符(::)的语法 双冒号运算符...
继续阅读 »

在Java 8引入的Lambda表达式和函数式接口之后,双冒号运算符(::)成为了一项重要的功能。它可以将方法或构造函数作为参数传递,简化了编码和提升了代码的可读性。本文将介绍Java中的双冒号运算符及其常见应用场景。


双冒号运算符(::)的语法


双冒号运算符的语法是类名/对象名::方法名。具体来说,它有三种不同的使用方式:



  1. 作为静态方法的引用:ClassName::staticMethodName

  2. 作为实例方法的引用:objectReference::instanceMethodName

  3. 作为构造函数的引用:ClassName::new


静态方法引用


首先,我们来看一下如何使用双冒号运算符引用静态方法。假设有一个Utils类,其中有一个静态方法processData


public class Utils {
public static void processData(String data) {
System.out.println("Processing data: " + data);
}
}

我们可以使用双冒号运算符将该方法作为参数传递给其他方法:


List<String> dataList = Arrays.asList("data1", "data2", "data3");
dataList.forEach(Utils::processData);

上述代码等效于使用Lambda表达式的方式:


dataList.forEach(data -> Utils.processData(data));

通过使用双冒号运算符,我们避免了重复写Lambda表达式,使代码更加简洁和易读。


实例方法引用


双冒号运算符还可以用于引用实例方法。假设我们有一个User类,包含了一个实例方法getUserInfo


public class User {
public void getUserInfo() {
System.out.println("Getting user info...");
}
}

我们可以通过双冒号运算符引用该实例方法:


User user = new User();
Runnable getInfo = user::getUserInfo;
getInfo.run();

上述代码中,我们创建了一个Runnable对象,并将user::getUserInfo作为方法引用赋值给它。然后,通过调用run方法来执行该方法引用。


构造函数引用


在Java 8之前,要使用构造函数创建对象,需要通过写出完整的类名以及参数列表来调用构造函数。而使用双冒号运算符,我们可以将构造函数作为方法引用,实现更加简洁的对象创建方式。


假设有一个Person类,拥有一个带有name参数的构造函数:


public class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

我们可以使用双冒号运算符引用该构造函数并创建对象:


Supplier<Person> personSupplier = Person::new;
Person person = personSupplier.get();
person.getName(); // 调用实例方法

上述代码中,我们使用Person::new将构造函数引用赋值给Supplier接口,然后通过get方法创建了Person对象。


总结


本文介绍了Java中双冒号运算符(::)的语法及其常见的应用场景。通过双冒号运算符,我们可以更方便地引用静态方法、实例方法和构造函数,使得代码更加简洁和可读。双冒号运算符是Java 8引入的重要特性,对于函数式编程和Lambda表达式的使用起到了积极的推动作用。


希望本文能够帮助您理解和应用双冒号运算符,提高Java开发的效率和代码质量。如有任何问题或疑惑,欢迎提问!


作者:每天一个技术点
来源:juejin.cn/post/7316532841923805184
收起阅读 »

关于晚上十点和男生朋友打电话调试vue源码那些事

web
简介朋友昨晚让我帮忙看怎么调试vue源码,我当时就不困了啊。兴致勃勃搞了半小时以失败告终。小白我可是永不言败的,早上鼓捣了俩小时总算写了点东西出来。想调试vue源码的小伙伴可以试试我这个思路fork源码首先肯定是要把vue/core代码fork一份到自己的仓库...
继续阅读 »

简介

朋友昨晚让我帮忙看怎么调试vue源码,我当时就不困了啊。兴致勃勃搞了半小时以失败告终。小白我可是永不言败的,早上鼓捣了俩小时总算写了点东西出来。想调试vue源码的小伙伴可以试试我这个思路

fork源码

首先肯定是要把vue/core代码fork一份到自己的仓库 这样后续有改动可以提交一下 也可以从源码一键同步

ps:github.com/baicie/vuej… 我的代码在这里可以参考一下

装包

pnpm i @pnpm/find-workspace-packages @pnpm/types -wD

ps:可以先看看pnpm与monorepo

在根目录执行上述命令装一下依赖-wD含义是在workspace的根安装开发依赖

脚本编写

首先在packages下执行pnpm creata vite创建一个vue项目

然后在scripts文件夹下创建dev.ts

import type { Project as PnpmProject } from '@pnpm/find-workspace-packages'
import { findWorkspacePackages } from '@pnpm/find-workspace-packages'
import type { ProjectManifest } from '@pnpm/types'
import { execa } from 'execa'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import color from 'picocolors'
import { scanEnums } from './const-enum'

export type Manifest = ProjectManifest & {
buildOptions: {
name?: string
compat?: boolean
env?: string
formats: ('global' | 'cjs' | 'esm-bundler' | 'esm-browser')[]
}
}

interface Project extends PnpmProject {
manifest: Manifest
}

const pkgsPath = path.resolve(process.cwd(), 'packages')
const getWorkspacePackages = () => findWorkspacePackages(pkgsPath)

async function main() {
scanEnums()
// 获取所有的包 除了private与没有buildOptions的包
const pkgs = (
(await getWorkspacePackages()).filter(
item => !item.manifest.private
) as Project[]
).filter(item => item.manifest.buildOptions)

await buildAll(pkgs)
}

async function buildAll(target: Project[]) {
// 并行打包
return runParallel(Number.MAX_SAFE_INTEGER, target, build)
}

async function runParallel(
maxConcurrent:
number,
source: Project[],
buildFn: (project: Project) =>
void
) {
const ret: Promise<void>[] = []
const executing: Promise<void>[] = []
for (const item of source) {
const p = Promise.resolve().then(() => buildFn(item))
// 封装所有打包任务
ret.push(p)

//
if (maxConcurrent <= source.length) {
const e: any = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e)
if (executing.length >= maxConcurrent) await Promise.race(executing)
}
}

return Promise.all(ret)
}

async function build(project: Project) {
const pkg = project.manifest
// 获取相对路径 包名
const target = path.relative(pkgsPath, project.dir)
if (pkg.private) {
return
}

const env = (pkg.buildOptions && pkg.buildOptions.env) || 'development'
await execa(
'rollup',
[
`-c`,
// 给rollup配置文件传递参数 watch 监听文件变化
'--watch',
'--environment',
[`NODE_ENV:${env}`, `TARGET:${target}`, `SOURCE_MAP:true`]
.filter(Boolean)
.join(',')
],
{ stdio: 'inherit' }
)
}

main().catch(err => {
console.error(color.red(err))
})

然后在根目录的package.json scripts 添加如下

"my-dev": "tsx scripts/dev.ts"

上述脚本主要是为了扫描工作目录下所有有意义的包并执行rollup打包命令(主要也就为了加一下watch没毛病)

验证

终端打开上吗创建的vite项目然后修改package.json里面的vue

 "vue": "workspace:*"

修改后根目录执行pnpm i建立软连接

1.根目录终端执行pnpm run my-dev

2.vite-project执行pnpm run dev

3.去runtime-core/src/apiCreateApp.ts createAppAPI 的 createApp 方法加一句打印

4.等待根目录终端打包完毕

5.去看看浏览器控制台有没有打印

按理说走完上述流出应该有打印出来哈哈 

优化更快?

然后就是想要快点因为我电脑不太行每次修改要等1.5~2s,然后我就想到了turbo,看了官网发现可行就试了试

修改如下

1. pnpm i turbo -wD

  1. 修改上述的my-dev "my-dev": "tsx scripts/dev.ts --turbo"
  2. 启动并验证

快了一点但不多

最后

新人多多关照哈哈 如果你想变强 b站 掘金搜索小满zs!


作者:白cie
来源:juejin.cn/post/7316539952475996194

收起阅读 »

前端要对用户的电池负责!

web
前言 我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢? ...
继续阅读 »

前言


我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢?


这里我直接揭晓答案:是一个Web应用,看来这个锅必须要前端来背了;我们来梳理一下js中有哪些耗电的操作。


js中有哪些耗电的操作


js中有哪些耗电的操作?我们不如问问浏览器有哪些耗电的操作,浏览器在渲染一个页面的时候经历了GPU进程、渲染进程、网络进程,由此可见持续的GPU绘制、持续的网络请求和页面刷新都会导致持续耗电,那么对应到js中有哪些操作呢?


Ajax、Fetch等网络请求


单只是一个AjaxFetch请求不会消耗多少电量,但是如果有一个定时器一直轮询地向服务器发送请求,那么CPU一直持续运转,并且网络进程持续工作,它甚至有可能会阻止电脑进行休眠,就这样它会一直轮询到电脑电池不足而关机;
所以我们应该尽量少地去做轮询查询;


持续的动画


持续的动画会不断地触发GPU重新渲染,如果没有进行优化的话,甚至会导致主线程不断地进行重排重绘等操作,这样会加速电池的消耗,但是js动画与css动画又不相同,这里我们埋下伏笔,等到后文再讲;


定时器


持续的定时器也可能会唤醒CPU,从而导致电池消耗加速;


上面都是一些加速电池消耗的操作,其实大部分场景都是由于定时器导致的电池消耗,那么下面来看看怎么优化定时器的场景;


针对定时器的优化


先试一试定时器,当我们关闭屏幕时,看定时器回调是否还会执行呢?


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

结果如下图,即使暂时关闭屏幕,它仍然会不断地执行:


未命名.png


如果把定时器换成requestAnimationFrame呢?


let num = 0;
let lastCallTime = 0;
function poll() {
requestAnimationFrame(() =>{
const now = Date.now();
if(now - lastCallTime > 1000*10){
console.log("测试raf后台时是否打印",num++);
lastCallTime = now;
}
poll();
});
}

屏幕关闭之前打印到了1,屏幕唤醒之后才开始打印2,真神奇!


未命名.png


当屏幕关闭时回调执行停止了,而且当唤醒屏幕时又继续执行。屏幕关闭时我们不断地去轮询请求,刷新页面,执行动画,有什么意义呢?因此才出现了requestAnimationFrame这个API,浏览器对它进行了优化,用它来代替定时器能减少很多不必要的能耗;


requestAnimationFrame的好处还不止这一点:它还能节省CPU,只要当页面处于未激活状态,它就会停止执行,包括屏幕关闭、标签页切换;对于防抖节流函数,由于频繁触发的回调即使执行次数再多,它的结果在一帧的时间内也只会更新一次,因此不如在一帧的时间内只触发一次;它还能优化DOM更新,将多次的重排放在一次完成,提高DOM更新的性能;


但是如果浏览器不支持,我们必须用定时器,那该怎么办呢?


这个时候可以监听页面是否被隐藏,如果隐藏了那么清除定时器,如果重新展示出来再创建定时器:


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

document.addEventListener('visibilitychange',()=>{
if(document.visibilityState==="visible"){
console.log("visible");
poll();
} else {
clearTimeout(timer);
}
})

针对动画优化


首先动画有js动画、css动画,它们有什么区别呢?


打开《我的世界》这款游戏官网,可以看到有一个keyframes定义的动画:


未命名.png


切换到其他标签页再切回来,这个扫描二维码的线会突然跳转;


因此可以得出一个结论:css动画在屏幕隐藏时仍然会执行,但是这一点我们不好控制。


js动画又分为三种种:canvas动画、SVG动画、使用js直接操作css的动画,我们今天不讨论SVG动画,先来看一看canvas动画(MuMu官网):


未命名.png


canvas动画在页面切换之后再切回来能够完美衔接,看起来动画在页面隐藏时也并没有执行;


那么js直接操作css的动画呢?动画还是按照原来的方式一直执行,例如大话西游这个官网的”获奖公示“;针对这种情况我们可以将动画放在requestAnimationFrame中执行,这样就能在用户离开屏幕时停止动画执行


上面我们对大部分情况已经进行优化,那么其他情况我们没办法考虑周到,所以可以考虑判断当前用户电池电量来兼容;


Battery Status API兜底


浏览器给我们提供了获取电池电量的API,我们可以用上去,先看看怎么用这个API:


调用navigator.getBattery方法,该方法返回一个promise,在这个promise中返回了一个电池对象,我们可以监听电池剩余量、电池是否在充电;


navigator.getBattery().then((battery) => {
function updateAllBatteryInfo() {
updateChargeInfo();
updateLevelInfo();
updateChargingInfo();
updateDischargingInfo();
}
updateAllBatteryInfo();

battery.addEventListener("chargingchange", () => {
updateChargeInfo();
});
function updateChargeInfo() {
console.log(`Battery charging? ${battery.charging ? "Yes" : "No"}`);
}

battery.addEventListener("levelchange", () => {
updateLevelInfo();
});
function updateLevelInfo() {
console.log(`Battery level: ${battery.level * 100}%`);
}

battery.addEventListener("chargingtimechange", () => {
updateChargingInfo();
});
function updateChargingInfo() {
console.log(`Battery charging time: ${battery.chargingTime} seconds`);
}

battery.addEventListener("dischargingtimechange", () => {
updateDischargingInfo();
});
function updateDischargingInfo() {
console.log(`Battery discharging time: ${battery.dischargingTime} seconds`);
}
});


当电池处于充电状态,那么我们就什么也不做;当电池不在充电状态并且电池电量已经到达一个危险的值得时候我们需要暂时取消我们的轮询,等到电池开始充电我们再恢复操作


后记


电池如果经常放电到0,这会影响电池的使用寿命,我就是亲身经历者;由于Web应用的轮询,又没有充电,于是一晚上就耗完了所有的电,等到再次使用时电池使用时间下降了好多,真是一次痛心的体验;于是后来我每次下班前将Chrome关闭,并关闭所有聊天软件,但是这样做很不方便,而且很有可能遗忘;


如果每一个前端都能关注到自己的Web程序对于用户电池的影响,然后尝试从定时器和动画这两个方面去优化自己的代码,那么我想应该就不会发生这种事情了。注意:桌面端程序也有可能是披着羊皮的狼,就是披着原生程序的Web应用;


参考:
MDN


作者:蚂小蚁
来源:juejin.cn/post/7206331674296746043
收起阅读 »

一个25岁普通非科班杭漂前端程序员的2023总结唠嗑

引言 大家好,我是努力挣钱的小鑫!全网同名就只有我这个ID哈哈哈 一个普通的 25 岁非科班前端程序员,第一次在掘金发文章,看着自己这 2023 年的各种经历,想着还是写篇咕噜文吧,也算是自己对自己的一个总结,至于写什么,想到啥写啥吧,自己也没啥好文采,都是一...
继续阅读 »

引言


大家好,我是努力挣钱的小鑫!全网同名就只有我这个ID哈哈哈


一个普通的 25 岁非科班前端程序员,第一次在掘金发文章,看着自己这 2023 年的各种经历,想着还是写篇咕噜文吧,也算是自己对自己的一个总结,至于写什么,想到啥写啥吧,自己也没啥好文采,都是一些大白话,还望各位不要介意😀


我是怎么入行前端的?


其实,说白了,就是为了赚钱,自己大学专业是机械电子工程的,来自农村,没啥家庭背景,毕业了大概率进厂打螺丝,因此大二其实我就开始准备转行了(为什么是大二?因为大一我根本没想法!)。


当时对于整个计算机行业没啥方向,就随便找了点资料,有人说学 python 简单,就买了书和视频跟着敲,结果后面了解了说,不好找工作。然后,看到 JAVA 好找工作,学了一个月 python 又转 JAVA 了,但是,学到后面好难,只学了半个月就放弃了。后面就有躺平开玩了。


那是什么时候想到学前端的呢?那是 2020 年 1 月 31 号晚上,那是一个我一辈子都忘不了的晚上,当时我就躺在床上想,自己现在大三了,以后能做什么呢?当程序员吧!


这次吸取了半年前盲目的学习方式,先去了解了各种程序员职业,最后了解到适合非科班从零开始学的就是前端,于是马上找了 B 站的免费课程跟着看,现在还记得是黑马的 Pink 老师,哈哈哈算是我的前端引路人,现在还是很感谢黑马当时的免费课程。


于是就这样,我的前端之旅就开始了,整个疫情我都没闲着,在家学了半年。从基础再到 vue2,再到 6 月份找到实习工作,就这样,我算是入行前端了,后面就是一系列找工作秋招了,有机会后面再一点点聊聊😃。


image.png


我在公司干了啥?


去年 9 月份我刚满 1 年工作经验,然后跳槽到了现在的公司,公司不大,主要业务是 web3d 业务方向的,我自己是负责网页前端,大部分的 3d 都是需要专门的 wbegl 工程师来写,自己做的也只是传统前端的活。老实说,真的就前端切图仔,毕竟 webgl 也不是我一个小小前端能够掌握的。


今年是 AI 年嘛,各家公司都在追热点,我们公司也不例外,跟着搞了对话大模型,我就被分到了 AI 组,给他们写 DEMO,各种对话,反正就是模仿 ChatGPT。


感觉自己这一年就是在各种写 demo 中度过的,不能说是好是坏,好处是可以自己用各种新技术,坏处是没啥成熟的上线项目。


但我比较开心的是,在 9 月份干满一年的时候,公司涨薪了,虽然只涨了 1.5k,但我还是很开心的哈哈哈。毕竟,总比没有好吧。


业余我在干什么?


俗话说,大部分人一辈子,除了工作,就是生活和学习了,今年我大大小小做了好多事情,就拿几个比较典型的说下吧。


减肥 30 斤!


工作两年胖了快 30 + ,不健康的饮食+熬夜,今年4月份下定决心开始减肥,方法是:168轻断食+爬楼梯,半年把两年胖的都减回去了,算是小目标完成,分享给需要减肥的朋友,轻断食+爬楼梯,真的很实用,具体教程可以去b站搜索看看。


image.png


image.png


口琴街头卖艺


大学自己就是在大学城那边街头吹口琴的,工作第一年没吹了,没玩了,口琴都快荒废了,今年在减肥的那段时间,想着要不重操旧业吧,于是买了设备,买了话筒,就在滨江+萧山的江边吹起了口琴,顺便放个二维码,还能赚点外快,虽然不多,大部分时间都是自己玩玩哈哈哈。


通过街头卖艺,我也认识了很多喜欢音乐、喜欢唱歌的小伙伴,其实,真的,发自内心地说,去做自己喜欢的事,是真的超级开心的一件事!!!即使是站一两个小时都不觉得累,甚至还觉得时间为什么过得那么快!


image.png


image.png


image.png


image.png


自己也开始经营了自己的小红书账号,虽然人不多,不定期更新口琴视频哈哈哈哈,喜欢听什么可以点歌哟。努力挣钱的小鑫 • 小红书 / RED (xiaohongshu.com)


image.png


两段失败的追求


今年因为街头卖艺认识了喜欢的女生,她喜欢唱歌,我也喜欢唱歌,可惜终究是我单身久了,自己的单相思罢了。


第二段是小红书认识的,她会吉他,我很喜欢会吉他的女孩子,也见了两次面,但是我总感觉她是在吊着我,我说白了,她也说白了,over,早点结束止损,挺好的。


就这样,一直母单 solo 的我,在 2023 年又多了两份失败的追求经验😢(为什么我要说又呢😭呜呜呜)


2024 的未来展望



  • 坚持减肥,争取明年6月份前减到130的标准体重!

  • 坚持周末抽时间去滨江江边,街头卖艺吹吹口琴,个人的爱好绝对不能荒废!

  • 希望能脱单吧,明年 26 岁了不会还是母单吧😢

  • 技术学习方面的,稍微卷卷差不多得了,我个人的核心思想就是:要抱着目的去学,用到在学,自己知道有这个东西就足够了。

  • 也希望自己能够多去做自己喜欢的事,少些焦虑,切记一切的焦虑都是没有意义的,做就完事了!


结语


其实我还想说很多,但是说多了,就感觉很啰嗦了,毕竟我文本也就那样,就说这么多吧。


最后回过头来看,我的 2023 年是有收获,有遗憾的一年,但无论怎么说,这都是我自己选择的结果,无论好坏,它都是过去的经历了,再回忆也只是为了再反省总结,活在当下,展望未来,继续过自己选择的生活!曾仕强老师说得很棒,一切的一切都是自作自受,都是自己选择过的生活,没什么好埋怨的,共勉各位!



最后交个友吧,杭州的朋友、喜欢唱歌、喜欢音乐的,可以一起交流学习玩呀!!!



作者:努力挣钱的小鑫
来源:juejin.cn/post/7316592697464487962
收起阅读 »

糟糕,怎么就摸了一年多了😦

前言 好快啊,00后从22年初入职场转眼间就在工位上摸了一年。也好久没写过年度记录了,这里就把摸了一整年的23年记录一下。年度关键词嘛,就是摸鱼、学习、旅游、减肥。 工作(摸鱼是常态) 今年工作的主旋律是摸鱼,不是自己想摸,是真的太闲了,没什么工作量给到我。 ...
继续阅读 »

前言


好快啊,00后从22年初入职场转眼间就在工位上摸了一年。也好久没写过年度记录了,这里就把摸了一整年的23年记录一下。年度关键词嘛,就是摸鱼、学习、旅游、减肥。


工作(摸鱼是常态)


今年工作的主旋律是摸鱼,不是自己想摸,是真的太闲了,没什么工作量给到我。


主要就是在已有平台代码上的功能迭代和 bug 修复;


开发了一个支付宝小程序还有其对应的嵌入到浙里办平台的精简版 H5;


负责即将交付给我们的使用 qiankun 微前端技术的新平台前端代码的开发文档编写(这里被主管拉出去问问题难住了,专门写了一篇文章记录《主管让我说说 qiankun 是咋回事》)JY们反馈还不错👻);


其余时间就是一些零零散散的小开发和主管让进行的技术调研和小demo,前段时间让我去尝试用 AssemblyScript 写 wasm 实现在前端离线加解密,然后发现没有现成的类似 crypto-js 的工具库,想实现得自己拿 AssemblyScript 重写一遍😦,抄了个 JavaScript 的加解密算法写了个小 demo 交代了,主管也知道让我自己写个 AS 的 crypto 有点忒为难我了,就说了再议。(话说再前端wasm里实现加解密真的安全吗?用户在wasm 中解密的过程可以被恶意脚本获取到吗,期待了解这方面的JY解答🧐)。


学习(学了就忘的菜)


身在国企,一直有的焦虑就是害怕技术没有长进😕。所以整天想着自己学点什么东西,2022一年,因为刚入职才开始接触React、TypeScript、Next、Taro小程序开发这些,所以平时开发中还挺充实的能学到的不少,平时业务中接触到了一些技术也想着学学什么 Solidity、Golang ,但是没有业务让我这个刚毕业的小前端去用这些技术,过两个月就什么也不会了;


于是我在今年转变思路,我学前端方面的东西总行了吧,然后就:Solid、Svelte、Vue3(Nuxt3)、Angular、Astro、Electron... 我全入了一下门,写了点小demo、小工具。再然后还是没业务做,过了没多久就忘得差不多了;


我还想着去了解了下Nest.js、Flutter、ArkTs这些,结果是 Flutter、ArkTs 这些配环境就给我配恼火了😶‍🌫️,Nest.js 被满眼的装饰器难住了,然后又去了解 TypeScript 装饰器,然后看到了掘金里的使用 TypeScript 做前端OOP开发的,想着自己大学里学的一塌糊涂,像面向对象编程这些思想自己很欠缺,又去到处搜面向对象...... 哈哈哈永远闭环不了了🤣。


生活(多图预警)


流水账式记录😎


4月,和女朋友去了一次苏州。参观了留园、狮子林、西园寺、“大裤衩”......西园寺的素面还挺好吃的。


36f42cee549e82ff5b88b8e6bb59656.jpg


a2db0432bb02804c473b0766501601c.jpg


6月,公司疗休养在舟山躺了三天😴,酒店风景巨好,居然有沙滩。


d513ef3d142de096c9b9abe1c69da57.jpg


a3d1fcc8fd86aa80585ab45d0cba4d1.jpg


9月底,参加了第一次年度体检,盲盒开得惊心动魄,不过除了体脂率和内脏脂肪过高,其他都还好,于是决定十一假期最后放纵一把,收假回来开始减肥。
没想到自己减的还挺快,不到2个月减了20斤😙。


ca93265f23a7aefae97741180a0314c.jpg


10月,女朋友备考研究生压力太大,住进医院动了个小手术,好在手术顺利赶在生日前出了院,和她室友一起给她庆祝了生日。


de8783b4b3bb8817f53a2112bf7f2fb.jpg


12月,陪女朋友在临安考研,青山湖风景很好,就是太冷了🥶。考研随缘了,还好手术后趁着秋招末尾投了几份简历,拿到了一个保底offer。


84721c507b743a55185fa0c42d26ee0.jpg


2f725f020a12dcc86460ced41d110be.jpg


展望(平淡是真,绝不折腾)


展望啥呢,计划赶不上变化,这两年的大环境,看着一起毕业的同学一个个离开杭州,也不想着折腾了,平平淡淡挺好,就希望我爱的人和爱我的人都健健康康。车到山前必有路,走到哪步算哪步。先把自己的肥肉减了再说罢!


作者:HyaCinth
来源:juejin.cn/post/7316795553799208999
收起阅读 »

【附源码】推荐8个「IM+AI」场景的开源项目,建议收藏

1、社交泛娱乐——【找搭子】饭搭子、旅游搭子、遛狗搭子…这种新型的“搭子社交”在年轻群体中逐渐流行起来。区别于传统社交,搭子关系,陌生以上,友人未满,这种轻松的浅社交既能获得志趣相投的陪伴,又不用苦心经营彼此的关系。实在是好搭子不问出处,只要有能结伴做的事,就...
继续阅读 »

1、社交泛娱乐——【找搭子】

饭搭子、旅游搭子、遛狗搭子…这种新型的“搭子社交”在年轻群体中逐渐流行起来。区别于传统社交,搭子关系,陌生以上,友人未满,这种轻松的浅社交既能获得志趣相投的陪伴,又不用苦心经营彼此的关系。实在是

好搭子不问出处,只要有能结伴做的事,就可以有那么一“搭”。那么问题来了,如何精准地匹配到自己喜爱的活动以及志趣相投的“搭子”,便成了求搭者们的刚需。

项目介绍

“找搭子”——基于AI Agent解决找搭子最后一公里。它借助环信IM能力,使用对话Agent 充分挖掘用户需求,整合用户信息形成实时活动表单,精准推荐,打破固化标签,增加可信度。为活动发起者、参与者提供了一个双向智能、高效沟通、精准匹配的平台。


(找搭子产品架构)

(找搭子Demo演示)

项目点评:

“找搭子”以其独特的创新视角,展现了信息分发和成员组织的全新可能性。通过利用大模型进行精准匹配,该项目能够根据用户的兴趣爱好,智能地形成特定的用户群体。这种创新的方式不仅极大地提高了用户的参与度和满意度,同时也预示着巨大的商业潜力。在营销、社交和社区建设等领域,其影响力不可忽视。"找搭子"的实现,无疑是对传统模式的一次颠覆性的创新,它为我们打开了一个全新的视角,重新诠释了我们对于用户互动和社区建设的理解。

模型:环信IM+文心一言

项目源码:(Flutter)

2、教育培训——KidChat

项目介绍

「KidChat」是一个儿童故事创作分享社区。通过语音输入故事梗概,由 LLM 生成儿童故事,结合环信IM能力,自动将绘本故事分享到故事广场,让小朋友们一起欣赏。通过 Reaction 互动,并自动为每一个故事生成子区,可在子区内对具体故事进一步讨论,进而形成一个有趣而充满创造力的故事分享社区。

kidchat Demo演示

项目点评

「KidChat」以其卓越的内容生成和讲述能力,成功地满足了面向低龄孩子的“讲故事”场景的需求。该项目利用大模型的强大功能,能够快速生成丰富多样、充满无限可能的故事,并通过语音进行生动有趣的讲解。这无疑满足了低龄儿童家长的刚需,为他们提供了一种新的、高效的儿童教育方式。在实现过程中,KidChat非常富有创意地将环信的Thread和Reaction功能应用到了自己的业务功能实现中,提供了非常好的使用体验。更值得一提的是,该项目的完成度高,用户体验佳,无论是在故事内容的创新性,还是在语音讲解的生动性上,都表现出了极高的专业水准和创新思维。

模型:环信IM+讯飞星火

项目源码:(Flutter)

3、聊天机器人——ChattyAI

项目介绍

「ChattyAI」是一款基于人工智能技术的智能陪聊机器人,旨在提供用户个性化、有趣、愉快的对话体验。通过先进的自然语言处理和机器学习算法,ChattyAI能够理解用户的意图和情感,并以富有情感的方式回应,为用户提供真实感和沟通舒适度。

为增加智能体与用户的互动体验与亲密感,借助环信IM通讯能力,通过智能体向用户定时发送早安、午安、晚安。除此外还可以扩展更丰富多样的主动唤起话题的任务。



项目点评

「ChattyAI」在社交泛娱乐领域实现了人与智能体聊天的全新场景。该项目充分利用大模型在闲聊领域的优势,为用户提供了一种全新的、高质量的交流体验。"ChattyAI"不仅提供了丰富人设的虚拟角色,更在UI设计和交互体验上展现出了卓越的水准。其UI设计优雅且直观,交互体验流畅且自然,使得用户可以轻松地与虚拟角色进行交流。项目的完成度非常高,无论是在技术实现,还是在用户体验上,都展现出了项目团队的专业能力和创新思维。"ChattyAI"的成功实现,为我们在社交泛娱乐领域的探索提供了有力的启示。

模型:环信IM+MINIMAX

项目源码:(Android)

4、AI专家助手

现存市面上大部分的AI主要是面向个人的,而当前的AI助手并不能准确无误的解决大部分人的问题,尤其在一些特定的领域,比如编程,医疗等专业技能要求较高的方向,虽然AI能够给出相应的回复,但是这些回复对于普通人来说,甄别其中的准确性依然存在一定问题。该项目通过一般咨询者的信息,收集不同AI厂商的建议或者帮助信息,能够大大提升相应的工作效率。进而实现专家的效率,而当前社会专家的稀缺才是更大的瓶颈。

模型:环信IM+百川智能+MINIMAX+文心一言

项目源码:

5、ai群管家

此项目提供了群聊、单聊机器人对话服务。通过命令方式激活机器人,单聊bot可实现与多个角色如AI医生、知乎文案生成、AI家教、AI律师、地方美食推荐(自由切换)生成对话,群聊借助AI能力提供了群历史消息总结功能。是超级社群中典型的应用场景。

模型:环信IM+智谱AI

项目源码:

6、PictoChat (AI智绘)

AI智绘是一款创新的移动应用程序(iOS),整合了环信IM(即时通讯)平台和OpenAI的GPT-4.0绘图能力。这款应用专为提升在线交流体验而设计,能够在实时对话中生成和发送图片,极大地增强了交流的趣味性和互动性。

模型:环信IM+ChatGPT

项目源码:

7、百答

百答,一个All in One全能助手,你的AI智囊团。基于环信IM即时通讯解决方案,结合各家大模型能力开发的全能AI助手。互联网+环信IM+大模型,强强联合,多重BUFF叠加,让普通人也拥有撬动地球的力量!恋爱大师,编程助手,周报秘书,抬杠大师,彩虹屁专家,数字女友,礼物攻略等等应有尽有,全能智能 有问必答,堪称bot细分领域第一代卷王。

模型:环信IM+通义千问

项目源码:

8、智慧医疗

此项目是一款智慧医疗应用,患者与AI医生实时互动获得实时的医疗咨询,会话结束时为患者生成咨询档案,同时智能推荐相应的科室。当患者问诊相应科室医生时,医生可调取患者基本信息和与AI医生的历史咨询档案。

智慧医疗的蓬勃发展有望提升整个医疗体系的质量和效率,改善医疗体验,同时为患者提供更为个性化和便捷的医疗服务。我们期待在医疗领域,IM与AI的紧密融合发挥巨大的作用。

模型:环信IM+通义千问

项目源码:

参考资料:

环信集成相关

收起阅读 »

JDBC快速入门:从环境搭建到代码编写,轻松实现数据库增删改查操作!

通过上篇文章我们已经对JDBC的基本概念和工作原理都有了一定的了解,本篇文章我们继续来探索如何从零开始,一步步搭建开发环境,编写代码,最后实现数据库的增删改查操作。一、开发环境搭建首先,我们需要准备的开发环境有:Java开发工具包(JDK)、数据库(如MySQ...
继续阅读 »

通过上篇文章我们已经对JDBC的基本概念和工作原理都有了一定的了解,本篇文章我们继续来探索如何从零开始,一步步搭建开发环境,编写代码,最后实现数据库的增删改查操作。

一、开发环境搭建

首先,我们需要准备的开发环境有:Java开发工具包(JDK)、数据库(如MySQL)、数据库驱动(如MySQL Connector/J)。

安装JDK:

你可以从Oracle官网下载适合你操作系统的JDK版本,按照提示进行安装即可。相信这个大家早已经安装过了,在这里就不再多说了。

安装数据库:

同样在官网下载MySQL安装包,按照提示进行安装。安装完成后,需要创建一个数据库和表,用于后续的测试。

下载数据库驱动:

在MySQL官网下载对应版本的MySQL Connector/J,将其解压后的jar文件添加到你的项目类路径中。

具体的操作如下:

1、创建一个普通的空项目

Description

填写上项目名称与路径

Description

2、配置JDK版本

Description

3、创建一个子模块(jdbc快速入门的程序在这里面写)

Description

这里填写上子模块名称

Description

然后下一步,点击ok,这个子模块就创建完成了

Description

4、导入jar包

Description
Description

二、使用JDBC访问数据库

JDBC操作数据库步骤如下:

  • 注册驱动
  • 获取数据库连接对象 (Connection)
  • 定义SQL语句
  • 获取执行SQL的对象 (Statement)
  • 执行SQL
  • 处理集并返回结果(ResultSet)
  • 释放资源

下面通过代码来了解一下JDBC代码的编写步骤与操作流程。

1、创建数据库和表:

CREATE DATABASE `jdbc_test` DEFAULT CHARSET utf8mb4;
CREATE TABLE `account`(
`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
`name` varchar(20) NOT NULL COMMENT '姓名',
`salary` int(11) COMMENT '薪资',
);

2、编写Java程序:

package com.baidou.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;

public class JDBCDemo {
public static void main(String[] args) throws Exception {
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");

// 2、获取连接
String url ="jdbc:mysql://127.0.0.1:3306/jdbc_test?useSSL=false";
String user = "root";
String password = "123456";
Connection conn = DriverManager.getConnection(url, user, password);


// 3、定义sql语句
String sql = "insert into account(name,salary) values('王强',10000)";

// 4、获取执行sql的对象 Statement
Statement stmt = conn.createStatement();

// 5、执行sql
int count = stmt.executeUpdate(sql);

// 6、处理结果
// 打印受影响的行数
System.out.println(count);
System.out.println(count>0?"插入成功":"插入失败");

// 7、释放资源
stmt.close();
conn.close();
}
}

控制输出结果如下:

Description

表中的数据:

Description

三、JDBC-API详解

JDBC API是Java语言访问数据库的标准API,它定义了一组类和接口,用于封装数据库访问的细节。主要包括以下几类:

1、DriverManager驱动管理对象

(1)注册驱动:
注册给定的驱动程序:staticvoid registerDriver(Driver driver);在com.mysql.jdbc.Driver类中存在静态代码块;
写代码有固定写法:Class.forName(“com.mysql.jdbc.Driver”);

(2)获取数据库连接对象
具体实现是通过:DriverManager.getConnection(url,username,password);

2、Connection数据库连接对象

(1)创建sql执行对象

conn.createStatement();

(2)可以执行事务的提交,回滚操作

conn.rollback();conn.setAutoCommit(false);

3、Statement执行sql语句的对象

用于向数据库发送要执行的sql语句(增删改查),其中有两个重要方法:

  • executeUpdate(String sql)
  • executeQuery(String sql)

前者用于DML操作,后者用于DQL操作。

4、ResultSet结果集对象

  • 打印输出时判断结果集是否为空,rs.next()
  • 若知字段类型可使用指定类型如,rs.getInt()获取数据
你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

四、提取工具类并完成增删改查操作

在上面介绍了可以通过JDBC对数据库进行增删改查操作,但是如果每次对数据库操作一次都要重新加载一次驱动,建立连接等重复性操作的话,会造成代码的冗余。

因此下面通过封装一个工具类来实现对数据库的增删改查操作。

1、建立配置文件db.properties文件

properties文件是Java支持的一种配置文件类型(所谓支持是因为Java提供了properties类,来读取properties文件中的信息)。记得一定要将此文件直接放在src目录下!!!不然后面执行可能找不到此配置文件!!

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbcstudy?useUnicode=true&characterEncoding=utf8&useSSL=true
username=root
password=lcl403020

2、建立工具类JdbcUtils.java

有了这个工具类,之后的增删改查操作可直接导入这个工具类完成获取连接,释放资源的操作,很方便,接着往下看。

package jdbcFirstDemo.src.lesson02.utils;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
public class JdbcUtils {
private static String driver=null;
private static String url=null;
private static String username=null;
private static String password=null;
static {
try{
InputStream in=JdbcUtils.class.getClassLoader().getResourceAsStream("db.properties"); Properties properties=new Properties();
properties.load(in);


driver=properties.getProperty("driver");
url=properties.getProperty("url");
username=properties.getProperty("username");
password=properties.getProperty("password");
//驱动只需要加载一次
Class.forName(driver);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
//获取连接
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url,username,password);
}
//释放连接资源
public static void release(Connection conn, Statement st, ResultSet rs) {
if(rs!=null){
try{
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}


if(st!=null){
try {
st.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if(conn!=null){
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

}
}

3、 插入数据(DML)

package jdbcFirstDemo.src.lesson02;
import jdbcFirstDemo.src.lesson02.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class TestInsert {
public static void main(String[] args) {
Connection conn=null;
Statement st=null;
ResultSet rs=null;
try{
conn= JdbcUtils.getConnection();
st=conn.createStatement();
String sql="insert into users (id, name, password, email, birthday) VALUES (7,'cll',406020,'30812290','2002-03-03 10:00:00')";
int i=st.executeUpdate(sql);
if(i>0){
System.out.println("插入成功!");
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
finally {
JdbcUtils.release(conn,st,rs);
}
}
}

运行结果:

Description

4、修改数据(DML)

package jdbcFirstDemo.src.lesson02;
import jdbcFirstDemo.src.lesson02.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class TestUpdate {
public static void main(String[] args) {
Connection conn=null;
Statement st=null;
ResultSet rs=null;
try{
conn= JdbcUtils.getConnection();
st=conn.createStatement();
String sql="update users set name='haha' where id=2";
int i=st.executeUpdate(sql);
if(i>0){
System.out.println("修改成功!");
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
finally {
JdbcUtils.release(conn,st,rs);
}
}
}

运行结果:

Description

5、 删除数据(DML)

package jdbcFirstDemo.src.lesson02;
import jdbcFirstDemo.src.lesson02.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class TestDelete {
public static void main(String[] args) {
Connection conn=null;
Statement st=null;
ResultSet rs=null;
try{
conn= JdbcUtils.getConnection();
st=conn.createStatement();
String sql="delete from users where id=1";
int i=st.executeUpdate(sql);
if(i>0){
System.out.println("删除成功!");
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
finally {
JdbcUtils.release(conn,st,rs);
}
}
}

运行结果:删除掉了id=1的那一条数据

Description

6、 查询数据(DQL)

package jdbcFirstDemo.src.lesson02;
import jdbcFirstDemo.src.lesson02.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class TestQuery {
public static void main(String[] args) throws SQLException {
Connection conn=null;
Statement st=null;
ResultSet rs=null;
conn= JdbcUtils.getConnection();
st=conn.createStatement();
//sql
String sql="select * from users";
rs=st.executeQuery(sql);
while (rs.next()){
System.out.println(rs.getString("name"));
}
}
}

运行结果:

Description

本文从开发环境搭建到代码编写步骤以及JDBC API做了详细的讲解,最后通过封装一个工具类来实现对数据库的增删改查操作,希望能够帮助你快速入门JDBC,关于数据库连接池部分,我们下期接着讲,敬请期待哦!

收起阅读 »

为什么我的页面鼠标一滑过,布局就错乱了?

web
前言 这天刚到公司,测试同事又在群里@我: 为什么页面鼠标一滑过,布局就错乱了? 以前是正常的啊? 刷新后也是一样 快看看怎么回事 同时还给发了一段bug复现视频,我本地跑个例子模拟下 可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。...
继续阅读 »

前言


这天刚到公司,测试同事又在群里@我:

为什么页面鼠标一滑过,布局就错乱了?

以前是正常的啊?

刷新后也是一样

快看看怎么回事


同时还给发了一段bug复现视频,我本地跑个例子模拟下


GIF 2023-8-28 11-23-25.gif


可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。


正文


首先说下我们的产品逻辑,我们产品的需求是:要滚动的页面,滚动条不应该占据空间,而是悬浮在滚动页面上面,而且滚动条只在滚动的时候展示。


我们的代码是这样写:


  <style>
.box {
width: 630px;
display: flex;
flex-wrap: wrap;
overflow: hidden; /* 注意⚠️ */
height: 50vh;
box-shadow: 0 0 5px rgba(93, 96, 93, 0.5);
}
.box:hover {
overflow: overlay; /* 注意⚠️ */
}
.box .item {
width: 200px;
height: 200px;
margin-right: 10px;
margin-bottom: 10px;
}
img {
width: 100%;
height: 100%;
}
</style>
<div class="box">
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
<div class="item">
<img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
</div>
</div>

我们使用了overflow属性的overlay属性,滚动条不会占据空间,而是悬浮在页面之上,刚好吻合了产品的需求。


image.png


然后我们在滚动的区域默认是overflow:hidden,正常时候不会产生滚动条,hover的时候把overflow改成overlay,滚动区域可以滚动,并且不占据空间。


简写代码如下:


  .box {
overflow: hidden;
}
.box:hover {
overflow: overlay;
}

然后我们是把它封装成sass的mixins,滚动区域使用这个mixins即可。


上线后没什么问题,符合预期,获得产品们的一致好评。


直接这次bug的出现。


排查


我先看看我本地是不是正常的,我打开一看,咦,我的布局不会错乱。


然后我看了我的chrome的版本,是113版本


然后我问了测试的chrome版本,她是114版本


然后我把我的chrome升级到最新114版本,咦,滑过页面之后我的布局也乱了。


初步判断,那就有可能是chrome版本的问题。


去网上看看chrome的升级日志,看看有没有什么信息。


image.png


具体说明:


image.png


可以看到chrome114有了一个升级,就是把overflow:overlay当作overflow:auto的别名,也就是说,overlay的表现形式和auto是一致的。


实锤了,auto是占据空间的,所以导致鼠标一滑过,布局就乱了。


其实我们从mdn上也能看到,它旁边有个删除按钮,然后滑过有个tips:


image.png


解决方案


第一种方式


既然overflow:overlay表现形式和auto一致,那么我们得事先预留出滚动条的位置,然后设置滚动条的颜色是透明的,滑过才显示颜色,基本上也能符合产品的需求。


代码如下:


  // 滚动条
::-webkit-scrollbar {
background: transparent;
width: 6px;
height: 6px;
}
// 滚动条上的块
::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: #d6d6d6;
border: 1px solid transparent;
border-radius: 10px;
}
.box {
overflow: auto;
}
.box::-webkit-scrollbar-thumb {
background-color: transparent;
}
.box:hover::-webkit-scrollbar-thumb {
background-color: #d6d6d6;
}

第二种方式


如果有用element-ui,它内部封装了el-scrollbar组件,模拟原生的滚动条,这个也不占据空间,而是悬浮,并且默认不显示,滑过才显示。



element-ui没有el-scrollbar组件的文档说明,element-plus才有,不过都可以用。



总结


这次算是踩了一个坑,当mdn上面提示有些属性已经要废弃⚠️了,我们就要小心了,尽量不要使用这些属性,就算当前浏览器还是支持的,但是不能保证未来某个时候浏览器会不会废弃这个属性。(比如这次问题,改这些问题挺费时间的,因为用的地方挺多的。)


因为一旦这些属性被废弃了,它预留的bug就等着你去解决,谨记!


作者:答案cp3
来源:juejin.cn/post/7273875079658209319
收起阅读 »

工作踩坑之在浏览器关闭/刷新前发送请求

web
丑话说在前 丑话说在前:当前的浏览器完全没有任何一个可靠、通用、准确区分用户关闭和刷新操作的API。 因此,如果你是单纯想在用户关闭时发送请求,那么没有任何完美答案。如果你只是想在谷歌Chrome、360浏览器的急速模式等一众基于谷歌Chrome浏览器套皮浏览...
继续阅读 »

丑话说在前


丑话说在前:当前的浏览器完全没有任何一个可靠、通用、准确区分用户关闭和刷新操作的API


因此,如果你是单纯想在用户关闭时发送请求,那么没有任何完美答案。如果你只是想在谷歌Chrome360浏览器的急速模式等一众基于谷歌Chrome浏览器套皮浏览器上实现,那么在下面我会提供一个简单的方法,但是Edge并不支持该方法。Edge是真牛啊,青出于蓝胜于蓝?


先来看看浏览器在刷新/关闭时的顺序


为了帮助理解我区分浏览器关闭和刷新操作的方法,先来看看浏览器在关闭/刷新时的执行顺序吧~


在浏览器关闭或刷新页面时,onbeforeunloadonunload 事件的执行顺序是固定的。



  1. 当用户关闭浏览器标签、窗口或者输入新的 URL 地址时,首先会触发 onbeforeunload 事件。

  2. onbeforeunload 事件处理完成后,如果用户选择离开页面(关闭或刷新),则会触发 onunload 事件。


因此,onbeforeunload 事件在用户决定离开页面之前执行,而 onunload 事件在用户离开页面之后执行。这两个事件提供了在用户离开页面前后执行代码的机会,可以用于执行清理操作或者提示用户确认离开等操作。通过对比两个事件的执行时间差,我们就可以简单判断浏览器的关闭或刷新行为啦。


简易判断Chrome浏览器关闭或刷新行为的方法


let beforeTime = 0,
leaveTime = 0;
// 获取浏览器onbeforeunload时期的时间戳
window.onbeforeunload = () => {
beforeTime = new Date().getTime();
};
window.onunload = () => {
// 对比onunload时期和onbeforeunload时期的时间差值
leaveTime = new Date().getTime() - beforeTime;
if (leaveTime < 5) {
// 如果小于5就是关闭
// 你可以在这发送请求
} else {
// 如果大于5就是刷新
// 你可以在这发送请求
}
};

注意:经过本人的测试,该方法仅支持Chrome浏览器等,Edge浏览器无论是关闭还是刷新,时间戳差均小于5ms,而谷歌Chrome浏览器的时间戳差均大于5ms,为7ms-8ms左右。环境不同亦有可能导致结果不同。


详见他人的测试结果图:


1703417937476.jpg


如何发送请求


既然已经区分了Chrome浏览器的关闭和刷新行为,那么该如果发送请求呢?


发送请求的方式主要有以下几种:


1. 使用 Navigator.sendBeacon()


该方法主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术,如XMLHttpRequest所导致的各种问题。


他的使用方法也很简单:


navigator.sendBeacon(url, data);

// url参数表明 data 将要被发送到的网络地址
// data (可选) 参数是将要发送的 ArrayBuffer、ArrayBufferView、Blob、DOMString、FormData 或 URLSearchParams 类型的数据。
// 当用户代理成功把数据加入传输队列时,sendBeacon() 方法将会返回 true,否则返回 false。

怎么样?简简单单一行代码即可实现发送可靠的异步请求,同时不会延迟页面的卸载或影响下一导航的载入性能。但是别忽略的他很重要的一个特点数据是通过 POST 请求发送的。


2. 使用 fetch + keepalive


该方法用于发起获取资源的请求。它返回的是一个 promise。他支持 POST 和 GET 方法,配合 keepalive 参数,可以实现浏览器关闭/刷新行为前发送请求。keepalive可用于超过页面的请求。可以说keepalive就是 Navigator.sendBeacon() 的替代品。


fetch('url',{
method:'GET',
keepalive:true
})

3. 直接发送异步请求


由于从Chrome83开始,onunload里面不允许执行同步的XHR,所以同步请求自然是无法实现的,但是一部请求是可以实现的。但是异步请求发送到设备的成功率并非百分之百,因此并不推荐,也不在此赘述。


总结


以上便是浏览器关闭/刷新前发送请求的几种方法,而我是采用了 fetch + alive 尝试简单实现浏览器仅关闭时发送请求,具体实现代码如下:


let beforeTime = 0,
leaveTime = 0;
window.onbeforeunload = () => {
beforeTime = new Date().getTime();
};
window.onunload = () => {
leaveTime = new Date().getTime() - beforeTime;
if (leaveTime <= 5) {
fetch('/logout.do',{
method:'GET',
keepalive:true
})
}
};

经测试,使用效果如下


使用该方法对于各浏览器的测试结果


浏览器/测试方法关闭tab页关闭浏览器任务管理器关闭刷新
Chrome登出登出登出未登出
Edge登出未登出登出登出
360急速模式登出登出登出未登出
360兼容模式白屏白屏白屏白屏
IE白屏白屏白屏白屏

浏览器/测试方法关闭tab页关闭浏览器任务管理器关闭刷新
Chrome
Edge××
360急速模式
360兼容模式××××
IE××××

小小的吐槽:


后端感知web退出本就不推荐由前端来处理,更优解为 持续ping 或者后端 心跳机制发包 来检测。


既然设备那边提出了这个请求,我们web这也就努力挣扎一下,把测试结果发给评审人员评审一下吧~


作者:bachelor98
来源:juejin.cn/post/7315846825344647194
收起阅读 »

Android自定义定时通知实现

Android自定义通知实现 前言 自定义通知就是使用自定义的布局,实现自定义的功能。 Notification的常规布局中可以设置标题、内容、大小图标,也可以实现点击跳转等。 常规的布局固然方便,可当需求变多就只能使用自定义布局了。 我想要实现的通知布局除了...
继续阅读 »

Android自定义通知实现


前言


自定义通知就是使用自定义的布局,实现自定义的功能。


Notification的常规布局中可以设置标题、内容、大小图标,也可以实现点击跳转等。


常规的布局固然方便,可当需求变多就只能使用自定义布局了。


我想要实现的通知布局除了时间、标题、图标、跳转外,还有“5分钟后提醒”、“已知悉”这两个功能需要实现。如下图所示:


nnotify.gif

正文


一、待办数据库


1、待办实体类


image.png

假设现在时间是12点,添加了一个14点的待办会议,并设置提前20分钟进行提醒


其中year、month、day、time构成会议的时间,也就是今天的14点content是待办的内容。remind是该待办提前提醒的时间,也就是20分钟type是待办类型,包括未完成,已完成和忽略


2、数据访问Dao


添加或更新一条待办事项


image.png



@Insert(onConflict = OnConflictStrategy.REPLACE) 注解: 如果指定 id 的对象没有保存在数据库中, 就会新增一条数据到数据库。如果指定 id 的对象数据已经保存到数据库中, 就会删除掉原来的数据, 然后新增一条数据。



3、数据库封装


在仓库层的TodoStoreRepository类中对待办数据库的操作进行封装。不赘述了。


二、添加定时器


每条待办都对应着一个定时任务,在用户添加一条待办数据的同时需要添加一条对应的定时任务。在这里使用映射的方式,将待办的Id和定时任务一一绑定。


1、思路:



  1. 首先要构造一个Map<Long, TimerTask>>类型的参数和一个定时器Timer。

  2. 在添加定时任务前,先对待办数据进行过滤。

  3. 计算出延迟的时间。

  4. 定时器调度,当触发时,消息弹窗提醒。


2、实现


image.png


说明



  • isOver() -- 判断该待办有没有完成,通过待办的type进行判断。



代码:fun isOver() = type == 1




  • delayTime -- 延迟时间。获取到当前时间的时间戳、将待办提醒的时间转换为时间戳,最后相减,得到一个Long类型的时间戳即延迟时间。

  • 当delayTime大于0时,进行定时器调度,将待办Id与定时任务绑定,当延迟时间到达时会触发定时器任务,定时器任务中包含了消息弹窗提醒。


3、封装


image.png



在 TodoScheduleUseCase 类中将待办数据插入和待办定时器创建封装在一起了,插入数据后获取到数据的Id,将id赋值传给待办定时器任务。



三、注册广播


1. 首先创建一个TodoNotifyReceiver广播


image.png



在TodoNotifyReceiver中,首先获取到待办数据,根据Action判断广播的类型并执行相应的回调。



2. 自定义Action


分别是“5分钟后提醒”、“已知悉”的Action


image.png


3. 广播注册方法


image.png

4.广播注册及回调实现


“5分钟后提醒”实现是调用delayTodoTask5min方法,原理就是将remind即提醒时间减5达到五分钟后提醒的效果。并取消该通知。再将修改过属性的待办重新添加到待办列表中。


“已知悉”实现是调用markTodoTaskDone方法,原理就是将type属性标记成1,代表已完成。并取消该通知。再将修改过属性的待办重新添加到待办列表中。


image.png


/**
* 延迟5分钟
*/

fun delayTodoTask5min(todoInfo: TodoInfo) {
useScope.launch {
todoInfo.remind -= 5
insertOrUpdateTodo(todoInfo)
}
}

/**
* 标记已完成
*/

fun markTodoTaskDone(todoInfo: TodoInfo) {
useScope.launch {
todoInfo.type = 1
insertOrUpdateTodo(todoInfo)
}
}



四、自定义通知构建


fun showNotify(todoInfo: TodoInfo) {
binding = LayoutTodoNotifyItemBinding.inflate(context.layoutInflater())

// 自定义通知布局
val notificationLayout =
RemoteViews(context.packageName, R.layout.layout_todo_notify_item)
// 设置自定义的Action
val notifyAfterI = Intent().setAction(TodoNotifyReceiver.TODO_CHANGE_ACTION)
val alreadyKnowI = Intent().setAction(TodoNotifyReceiver.TODO_ALREADY_KNOW_ACTION)
// 传入TodoInfo
notifyAfterI.putExtra("todoInfo", todoInfo)
alreadyKnowI.putExtra("todoInfo", todoInfo)
// 设置点击时跳转的界面
val intent = Intent(context, MarketActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, todoInfo.id.toInt(), intent, PendingIntent.FLAG_CANCEL_CURRENT)
val notifyAfterPI = PendingIntent.getBroadcast(context, todoInfo.id.toInt(), notifyAfterI, PendingIntent.FLAG_CANCEL_CURRENT)
val alreadyKnowPI = PendingIntent.getBroadcast(context, todoInfo.id.toInt(), alreadyKnowI, PendingIntent.FLAG_CANCEL_CURRENT)

//给通知布局中的组件设置点击事件
notificationLayout.setOnClickPendingIntent(R.id.notify_after, notifyAfterPI)
notificationLayout.setOnClickPendingIntent(R.id.already_know, alreadyKnowPI)

// 构建自定义通知布局
notificationLayout.setTextViewText(R.id.notify_content, todoInfo.content)
notificationLayout.setTextViewText(R.id.notify_date, "${todoInfo.year}-${todoInfo.month + 1}-${todoInfo.day} ${todoInfo.time}")

var notifyBuild: NotificationCompat.Builder? = null
// 构建NotificationChannel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(context.packageName, "todoNotify", NotificationManager.IMPORTANCE_HIGH)
notificationChannel.lockscreenVisibility = Notification.VISIBILITY_SECRET
notificationChannel.enableLights(true) // 是否在桌面icon右上角展示小红点
notificationChannel.lightColor = Color.RED// 小红点颜色
notificationChannel.setShowBadge(true) // 是否在久按桌面图标时显示此渠道的通知
notificationManager.createNotificationChannel(notificationChannel)
notifyBuild = NotificationCompat.Builder(context, todoInfo.id.toString())
notifyBuild.setChannelId(context.packageName);
} else {
notifyBuild = NotificationCompat.Builder(context)
}
notifyBuild.setSmallIcon(R.mipmap.icon_todo_item_normal)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout) //设置自定义通知布局
.setPriority(NotificationCompat.PRIORITY_MAX) //设置优先级
.setAutoCancel(true) //设置点击后取消Notification
.setContentIntent(pendingIntent) //设置跳转
.build()
notificationManager.notify(todoInfo.id.toInt(), notifyBuild.build())

// 取消指定id的通知
fun cancelNotifyById(id: Int) {
notificationManager.cancel(id)
}
}


步骤:



  1. 构建自定义通知布局

  2. 设置自定义的Action

  3. 传入待办数据

  4. 设置点击时跳转的界面

  5. 设置了两个BroadcastReceiver类型的点击回调

  6. 给通知布局中的组件设置点击事件

  7. 构建自定义通知布局

  8. 构建NotificationChannel

  9. 添加取消指定id的通知方法



总结


以上,Android自定义定时通知实现的过程和结果。文章若出现错误,欢迎各位批评指正,写文不易,转载请注明出处谢谢。


作者:遨游在代码海洋的鱼
来源:juejin.cn/post/7278566669282787383
收起阅读 »

Android:实现一个全屏拖拽、自动贴边半隐藏的自定义View

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。 今天给大家带来一个可全屏拖拽,手指离开屏幕后自动贴边,隔一定时间后自动半隐藏的这么一个效果。话不多说直接上效果图: 看到这个效果是不是感觉很熟悉?没错,很多商业APP首页都带一个小...
继续阅读 »

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。



今天给大家带来一个可全屏拖拽,手指离开屏幕后自动贴边,隔一定时间后自动半隐藏的这么一个效果。话不多说直接上效果图:


1.gif


看到这个效果是不是感觉很熟悉?没错,很多商业APP首页都带一个小助手的图标,使用的时候点击它就自动弹出,不使用的时候自动贴边隐藏,当然也是可以随意全屏拖拽,为的是防止遮挡一些关键位置的信息,影响用户体验。接下来咱们就来一步步实现它!


要实现上图效果咱们得罗列所有的功能点:



  • 自定义View,这里要显示图片所以继承自ImageView或其子类即可

  • 监听屏幕滑动事件,记录和计算当前视图的位置信息

  • 动画效果,很明显使用平移动画

  • 圆角图片和描边,使用第三方ImageView即可


为了解决小圆球这个图标的问题咱们自定View时直接继承自第三方RoundedImageView,一举两得直接解决了第一和第四步。咱们把焦点聚焦到第二三部分,这也是最为复杂的部分。


之前文章 Android:自定义View实现图片缩放及坐标的计算(上) 中有写到监听界面各类手势可以使用GestureDetector,这里咱们就不采用重写onTouchEvent方法然后再里面监听各类ACTION_UP、ACTION_DOWN、ACTION_MOVE事件的模式来写了。但还是需要重写onTouchEvent方法将GestureDetector的处理结果返回给它即可:


override fun onTouchEvent(event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event)
}

接下来只需在GestureDetector入参的GestureDetector.SimpleOnGestureListener监听中执行对应的操作:


首先需要在onDown方法中记录最后点击屏幕的位置信息lastXlastY,这里备份一份点击时的位置信息moveXmoveY,用于后续逻辑判断。


override fun onDown(e: MotionEvent): Boolean {
lastX = e.rawX.toInt()
lastY = e.rawY.toInt()
moveX = lastX
moveY = lastY
return true
}

onScroll中需要不停修改自定义视图的位置,所以我们需要计算出需要移动位置的信息。通过当前实时滑动点的信息和最后记录的点信息计算出滑动距离,再重新计算当前视图的上下左右位置,最后咱们采取layout() 方式进行位置设置。


override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
//获取当前实时点信息
val rawX = e2.rawX.toInt()
val rawY = e2.rawY.toInt()

//变化量
dX = rawX - lastX
dY = rawY - lastY

//获取最新的视图位置
var left = left + dX
var right = right + dX
var top = top + dY
var bottom = bottom + dY

//添加限制范围,上下左右不能超出屏幕范围
if (left < 0) {
left = 0
right = left + width
}

if (right > windowWith) {
right = windowWith
left = right - width
}

if (top < 40) {
top = 40
bottom = top + height
}

if (bottom > windowHight) {
bottom = windowHight
top = bottom - height
}

//更新当前视图位置
layout(left, top, right, bottom)

//更新最后屏幕点信息
lastX = rawX
lastY = rawY

return true
}

到此,咱们已经实现了可全屏拖拽的效果了:


2.gif


现在只差最后一步,通过位置信息判断图标该往哪边贴边,以及移动距离的计算。


由于GestureDetector没有抬起监听,所以逻辑咱们还是得在onTouchEvent方法中通过监听ACTION_UP的动作进行操作。判断该往哪边贴边很简单,如果最后松开的位置X坐标的超过屏幕一半就往右贴,反之往左。动画咱们还是使用ValueAnimator,因为我们移动也是用layout() 方法进行操作。


override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_UP -> {
val x = event.rawX
val y = event.rawY
//抬起点和最后一次按下点x、y距离大于视图宽的一半才执行
if (abs(x - moveX) > width / 2 || abs(y - moveY) > width / 2) {
val isRight = x > windowWith / 2

//贴边
startAnimator(isRight, windowWith - width, 0)

//隔1.5秒收边
postDelayed({
startAnimator(isRight, windowWith - width * 2 / 3, -width / 3)
}, 1500)
}
return true
}
}
return gestureDetector.onTouchEvent(event)
}

//属性动画执行
private fun startAnimator(isRight: Boolean, rightValue: Int, leftValue: Int) {
ValueAnimator.ofInt(
left,
if (isRight) rightValue else leftValue
).apply {
addUpdateListener { animation ->
val value = animation.animatedValue as Int
//根据监听值不断改变当前视图位置
layout(value, top, value + width, bottom)
}
//插值器 先快后慢
interpolator = AccelerateDecelerateInterpolator()
duration = 600
start()
}
}

这里使用了两次动画,第一次根据计算得出的方向进行贴边平移,隔了1.5秒后再进行隐藏的操作。到此我们的所有功能全部都实现了接下来总结几点:



  • 自定义View时尽量选择最接近目标功能的View进行继承

  • 屏幕事件监听除了重写onTouchEvent进行动作监听的方式还有GestureDetectorScaleGestureDetector等方式

  • 重写了onTouchEvent方法后需要注意其返回值,如果都返回false的情况该视图的点击事件有可能会被父View或其他设有监听事件控件所消费,导致滑动监听不被触发。


以上便是实现一个全屏拖拽、自动贴边半隐藏的自定义View的所有内容,希望能给大家带来帮助!


作者:似曾相识2022
来源:juejin.cn/post/7278496260477796389
收起阅读 »

mac终端自定义登录欢迎语

mac终端自定义登录欢迎语 看着单调的终端,突然有了一丝丝的念头,我要搞的炫酷一点。让我想到的一个场景就是:我之前在使用公司的阿里云服务器的时候,在每次登录的时候会有欢迎语,类似于这样的: shigen手头也没有可以用的阿里云云服务器,这里在知乎上找到的...
继续阅读 »

mac终端自定义登录欢迎语



看着单调的终端,突然有了一丝丝的念头,我要搞的炫酷一点。让我想到的一个场景就是:我之前在使用公司的阿里云服务器的时候,在每次登录的时候会有欢迎语,类似于这样的:


阿里云服务器登录欢迎语



shigen手头也没有可以用的阿里云云服务器,这里在知乎上找到的文章,仅供参考哈。



那我的mac我每次打开终端的时候,也相当于一次登录呢,那我是不是也可以这样的实现,于是开始捣腾起来。


查了一下发现:



要在每次登录终端时显示自定义的欢迎语,可以编辑你的用户主目录下的.bashrc文件(如果是使用 Bash shell)或.zshrc文件(如果是使用 Zsh shell)。



好的呀,原来就是这么简单,于是去搞了一下,我用的bashzsh,自然需要编辑一下.zshrc文件了。


执行命令,更改.zshrc文件内容:


 vim ~/.zshrc

在在行尾加上如下的内容:


 #自定义欢迎语
 echo -e "\033[38;5;196m"
 cat << "EOF"
 
                                                                         
  ad88888ba   88        88  88   ,ad8888ba,   88888888888 888b      88  
 d8"     "8b  88        88  88   d8"'   `"8b  88           8888b     88  
 
Y8,          88        88  88 d8'           88           88 `8b   88  
 `Y8aaaaa,   88aaaaaaaa88 88 88             88aaaaa     88 `8b   88  
   `"""""8b, 88""""""""88 88 88     88888 88"""""     88   `8b  88  
         `8b
88       88 88 Y8,       88 88           88   `8b 88  
 
Y8a     a8P  88        88  88   Y8a.   .a88  88           88     `8888  
 
"Y88888P"   88        88  88    `"Y88888P"   88888888888 88     `888  
                                                                         
 EOF
 echo -e "\033[0m"
 echo -e "\033[1;34mToday is \033[1;32m$(date +%A,\ %B\ %d,\ %Y)\033[0m"
 echo -e "\033[1;34mThe time is \033[1;32m$(date +%r)\033[0m"
 echo -e "\033[1;34mYou are logged in to \033[1;32m$(hostname)\033[0m"

其中,自定义ascii字符生成可以参考这个网站:在线生成ascci艺术字


网站的使用


完了之后,我们只需要重新加载一下配置文件即可:


 source ~/.zshrc

就会出现如下的效果:


终端效果


我们在vscode中看看:


vscode中的显示效果


哈哈,是不是稍微酷炫一点了。就先这样子吧。


其实mac和linux的操作很多都一样,这养的配置也可以直接平移到Linux服务器上,哈哈,下次打开云服务器就会看到自定义的欢迎语了,是不是倍儿有面儿啊。




作者:shigen01
来源:juejin.cn/post/7316451458840838179
收起阅读 »

妙用 CSS counters 实现逐层缩进

web
妙用 CSS counters 实现逐层缩进 之前使用纯 CSS 实现了一个树形结构,效果如下 其中,展开收起是用到了原生标签details和summary,有兴趣的可以回顾之前这篇文章 CSS 实现树状结构目录 还有一点,树形结构是逐层缩进的,是使用...
继续阅读 »

妙用 CSS counters 实现逐层缩进



之前使用纯 CSS 实现了一个树形结构,效果如下


image-20231221201613974


其中,展开收起是用到了原生标签detailssummary,有兴趣的可以回顾之前这篇文章



CSS 实现树状结构目录



还有一点,树形结构是逐层缩进的,是使用内边距实现的,但是这样会有点击范围的问题,层级越深,点击范围越小,如下


image-20231221201953463


之前的方案是用绝对定位实现的,比较巧妙,但也有点难以理解,不过现在发现了另一种方式也能很好的实现缩进效果,一起看看吧


一、counter() 与 counters()


我们平时使用的一般都是counter,也就是计数器,比如


<ul>
<li>li>
<li>li>
<li>li>
ul>

加上计数器,通常用伪元素来显示这个计数器


ul {
counter-reset: listCounter; /*初始化计数器*/
}
li {
counter-increment: listCounter; /*计数器增长*/
}
li::before {
content: counter(listCounter); /*计数器显示*/
}

这就是一个最简单的计数器了,效果如下


image-20231221203258255


我们还可以改变计数器的形态,比如改成大写罗马数字(upper-roman


li::before {
content: counter(listCounter, upper-roman);
}

效果如下


image-20231221203158970


有关计数器,网上的教程非常多,大家可以自行搜索


然后我们再来看counters(),比前面的counter()多了一个s,叫做嵌套计数器,有什么区别呢?下面来看一个例子,还是和上面一样,只是结构上复杂一些


<ul>
<li>
<ul>
<li>li>
<li>li>
<li>li>
ul>
li>
<li>li>
<li>li>
<li>
<ul>
<li>li>
<li>
<ul>
<li>li>
<li>li>
<li>li>
ul>
li>
ul>
li>
ul>


效果如下


image-20231221204007978


看着好像也不错?但是好像从计数器上看不出层级效果,我们把counter()换成counters(),注意,counters()要多一个参数,表示连接字符,也就是嵌套时的分隔符,如下


li::before {
content: counters(listCounter, '-');
}

效果如下


image-20231221204311891


是不是可以非常清楚的看出每个列表的层级?下次碰到类似的需求就不需要用 JS 去递归生成了,直接用 CSS 渲染,简单高效,也不会出错。


默认ul是有padding的,我们把这个去除看看,变成了这样


image-20231221204528126


嗯,看着这些长短不一的序号,是不是刚好可以实现树形结构的缩进呢?


二、树形结构的逐层缩进


回到文章开头,我们先去除之前的padding-left,会变成这样


image-20231221224113570


完全看不清结构关系,现在我们加上嵌套计数器


.tree details{
counter-reset: count;
}
.tree summary{
counter-increment: count;
}
.tree summary::before{
content: counters(count,"-");
color: red;
}

由于结构关系,目前序号都是1,没关系,只需要有嵌套关系就行,效果如下


image-20231221224810497


**是不是刚好把每个标题都挤过去了?**然后我们把中间的连接线去除,这样可以更方便的控制缩进的宽度


.tree summary::before{
content: counters(count,"");
color: red;
}

效果如下


image-20231221225225369


最后,我们只需要设置这个计数器的颜色为透明就行了


.tree summary::before{
content: counters(count,"");
color: transparent;
}

最终效果如下


image-20231221225607078


这样做的好处是,每个树形节点都是完整的宽度,所以 可以很轻易的实现hover效果,而无需借助伪元素去扩大点击范围


.tree summary:hover{
background-color: #EEF2FF;
}

效果如下


image-20231221225732065


还可以通过修改计数器的字号来调整缩进,完整代码可以访问以下链接:




三、总结一下


以上就是本文的全部内容了,主要介绍了计数器的两种形态,以及想到的一个应用场景,下面总结一下



  1. 逐层缩进用内边距比较容易实现,但是会造成子元素点击区域过小的问题

  2. counter 表示计数器,比较常规的单层计数器,形如 1、2、3

  3. counters 表示嵌套计数器,在有层级嵌套时,会自动和上一层的计数器相叠加,形如1、1-1、1-2、1-2-1

  4. 嵌套计数器会逐层叠加,计数器的字符会逐层增加,计数器所占据的位置也会越来越大

  5. 嵌套计数器所占据的空间刚好可以用作树形结构的缩进,将计数器的颜色设置为透明就可以了

  6. 用计数器的好处是,每个树形节点都是完整的宽度,而无需借助伪元素去扩大点击范围


一个还算实用的小技巧,你学到了吗?


作者:XboxYan
来源:juejin.cn/post/7315850963343671335
收起阅读 »

🔥图片懒加载🔥三种实现方案

web
一、前言 图片懒加载,当图片出现在可视区域再进行加载,提升用户的体验。因为有些用户不会看完图片,全部加载会浪费流量。在网上查阅资料,总结了三种办法,有各自的利弊,下文一一介绍。 方法优点缺点推荐指数设置img loadingh5的属性,没有兼容问题需要已知图片...
继续阅读 »

一、前言


图片懒加载,当图片出现在可视区域再进行加载,提升用户的体验。因为有些用户不会看完图片,全部加载会浪费流量。在网上查阅资料,总结了三种办法,有各自的利弊,下文一一介绍。


方法优点缺点推荐指数
设置img loadingh5的属性,没有兼容问题需要已知图片高度、宽高比⭐️⭐️
IntersectionObserver API无需知道图片高度低版本需引入polyfill⭐️⭐️⭐️
vue-lazyload 自定义指令无需知道图片高度github现存issues较多,没有解决⭐️⭐️

output.gif


二、实现方式及Demo


1. 设置img标签loading属性


loading属性允许两个值:eager立即加载图像(默认值);lazy延迟加载图像。在使用lazy属性的时候,需要设置<img>标签的高度,否则无法懒加载。


注意: 适用于两种场景,图片高度已知、图片宽高比已知。



  • 已知图片高度


<style>
.img-box img {
width: 100%;
height: 700px; /*设置为图片的真实高度*/
}
</style>

<div class="img-box">
<img src="https://i.postimg.cc/GtN3Cs02/1.jpg" loading="lazy" />
<img src="https://i.postimg.cc/hGdKLGdW/2.jpg" loading="lazy" />
<img src="https://i.postimg.cc/T1SkJTbF/3.jpg" loading="lazy" />
<img src="https://i.postimg.cc/wxPFPTtb/4.jpg" loading="lazy" />
<img src="https://i.postimg.cc/FRkGF28x/5.jpg" loading="lazy" />
<img src="https://i.postimg.cc/05JH9wqq/6.jpg" loading="lazy" />
</div>



  • 已知图片宽高比


 <style>
.img-box div {
position: relative;
padding-top: 66%; /* (你的图片的高度/宽度值) */
overflow: hidden;
}
.img-box img {
position: absolute;
top:0;
right:0;
width:100%;
}
</style>

<div class="img-box">
<div>
<img src="https://i.postimg.cc/GtN3Cs02/1.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/hGdKLGdW/2.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/T1SkJTbF/3.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/wxPFPTtb/4.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/FRkGF28x/5.jpg" loading="lazy" />
</div>
<div>
<img src="https://i.postimg.cc/05JH9wqq/6.jpg" loading="lazy" />
</div>
</div>


2. 使用 IntersectionObserver


IntersectionObserver接口,可以观察DOM节点是否出现在视口,当DOM节点出现在视口中才加载图片。img必须有高度,否则图片默认都在视口中,会将图片全部加载。可以设置img的src为base64白色图片,然后在替换为真实的图片地址。


注意: 不需要预先知道图片的高度,但是有兼容性问题,低版本需要引入intersection-observer polyfill



  • 已知图片高度


<style>
.img-box .lazy-img {
width: 100%;
height: 600px; /*如果已知图片高度可以设置*/
}
</style>

<div class="img-box">
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/d48aed7c991b43d850d011f2299d852e.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/a588b152c79ac60162ecbdf82b060061.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/eacbc2cd4b6ca636077378182bdfcc88.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/751470f4b478450e8556f78cd7dd3d96.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/e4a531bee5694a4a01dee74b18bbfd8b.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/7d8f107e827a7beaa0b9d231bfa4187f.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/4f7586f6b74f2bd0b94004fcbae69856.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/863849e14e7e8903ed4b27fcbdafe8b0.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
<img class="lazy-img" data-origin="https://images.djtest.cn/pic/test/d8bb17fe9a7223f35075014ef250e2fa.jpg" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"/>
</div>

<script>
function Observer() {
let images = document.querySelectorAll(".lazy-img");
let observer = new IntersectionObserver(entries => {
entries.forEach(item => {
if (item.isIntersecting) {
item.target.src = item.target.dataset.origin; // 开始加载图片,把data-origin的值放到src
observer.unobserve(item.target); // 停止监听已开始加载的图片
}
});
});
images.forEach(img => observer.observe(img));
}
Observer()
</script>

3. 使用vue-lazyload


在vue2中使用时,建议安装npm i vue-lazyload@1.3.3 -s,使用高版本在main.js中全局自定义指令后依然无法使用指令。在vue3中可以使用 npm i vue3-lazy -s



  • 全局注册自定义指令,在页面就可以使用了


// 全局自定义指令
import Vue from 'vue'
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1,
observer: true // 设置为true,内部使用IntersectionObserver。默认使用
})

/* 在页面中直接使用 */
<div>
<img v-lazy="https://images.djtest.cn/pic/test/d48aed7c991b43d850d011f2299d852e.jpg">
<img v-lazy="https://images.djtest.cn/pic/test/a588b152c79ac60162ecbdf82b060061.jpg">
<img v-lazy="https://images.djtest.cn/pic/test/eacbc2cd4b6ca636077378182bdfcc88.jpg">
<img v-lazy="https://images.djtest.cn/pic/test/751470f4b478450e8556f78cd7dd3d96.jpg">
</div>

作者:起风了啰
来源:juejin.cn/post/7316349850854752294
收起阅读 »

Android — DialogFragment显示后隐藏的导航栏显示问题

沉浸式显示广泛的应用于大部分App中,基本上可以说是App的必备功能,在之前的文章Android 全屏显示和沉浸式显示中介绍过如何通过WindowInsetsControllerCompat实现沉浸式显示。当然,我也将其应用到了我司的App中。但是在后续开发测...
继续阅读 »

沉浸式显示广泛的应用于大部分App中,基本上可以说是App的必备功能,在之前的文章Android 全屏显示和沉浸式显示中介绍过如何通过WindowInsetsControllerCompat实现沉浸式显示。当然,我也将其应用到了我司的App中。但是在后续开发测试的过程中发现了一个奇怪的现象,加载弹窗显示时,已经隐藏的底部导航栏又显示出来了。


问题复现


下面通过一段示例代码演示一下:



  • 加载弹窗


class LoadingDialogFragment : DialogFragment() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.LoadingDialog)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGr0up?, savedInstanceState: Bundle?): View {
dialog?.let { containerDialog ->
containerDialog.window?.run {
setBackgroundDrawable(ContextCompat.getDrawable(requireContext(), android.R.color.transparent))
decorView.setBackgroundResource(android.R.color.transparent)
val layoutParams = attributes
layoutParams.width = DensityUtil.dp2Px(200)
layoutParams.height = DensityUtil.dp2Px(120)
layoutParams.gravity = Gravity.CENTER
attributes = layoutParams
}
containerDialog.setCancelable(true)
containerDialog.setCanceledOnTouchOutside(false)
}
return LayoutLoadingDialogBinding.inflate(layoutInflater, container, false).root
}
}


  • 示例页面


class DialogFragmentExampleActivity : AppCompatActivity() {

val DIALOG_TYPE_LOADING = "loadingDialog"

private lateinit var insetsController: WindowInsetsControllerCompat

private var alreadyChanged = false

private var callDismissDialogTime = 0L

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = LayoutDialogFragmentExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}

// 调整系统栏
WindowCompat.setDecorFitsSystemWindows(window, false)
insetsController = WindowCompat.getInsetsController(window, window.decorView).also {
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
it.hide(WindowInsetsCompat.Type.navigationBars())
}
window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
if (!alreadyChanged) {
alreadyChanged = true
windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).run {
binding.topView.updateLayoutParams<ConstraintLayout.LayoutParams> { height += top }
binding.tvTitle.updateLayoutParams<ConstraintLayout.LayoutParams> { topMargin += top }
}
}
WindowInsetsCompat.CONSUMED
}

binding.btnShowLoadingDialog.setOnClickListener {
showLoadingDialog()
}
}

private fun showLoadingDialog() {
LoadingDialogFragment().run {
show(supportFragmentManager, DIALOG_TYPE_LOADING)
}
// 模拟耗时操作,两秒后关闭弹窗
lifecycleScope.launch(Dispatchers.IO) {
delay(2000)
dismissLoadingDialog()
}
}

private fun dismissLoadingDialog() {
callDismissDialogTime = System.currentTimeMillis()
lifecycleScope.launch(Dispatchers.IO) {
if (async { checkLoadingDialogStatue() }.await()) {
withContext(Dispatchers.Main) {
// 从supportFragmentManager中获取加载弹窗,并调用隐藏方法
(supportFragmentManager.findFragmentByTag(DIALOG_TYPE_LOADING) as? DialogFragment)?.run {
if (dialog?.isShowing == true) {
dismissAllowingStateLoss()
}
}
}
}
}
}

/**
* 检查加载弹窗的状态直到获取到加载弹窗或者超过时间
*/

private suspend fun checkLoadingDialogStatue(): Boolean {
return if (supportFragmentManager.findFragmentByTag(DIALOG_TYPE_LOADING) == null && System.currentTimeMillis() - callDismissDialogTime < 1500L) {
delay(100)
checkLoadingDialogStatue()
} else {
true
}
}
}

效果如图:


Screen_recording_202 -big-original.gif

解决显示异常问题


上述示例代码中,在示例页面的初始化方法中通过WindowInsetsControllerCompat对页面的WindowdecorView进行操作,隐藏了导航栏。但是在DialogFragment中,Dialog对象也有其所属的WindowdecorView,上述示例代码中并没有针对Dialog所属的WindowdecorView进行配置。


基于上面的分析,对示例代码进行调整,调整如下:



  • 加载弹窗


class LoadingDialogFragment : DialogFragment() {

......

override fun onCreateView(inflater: LayoutInflater, container: ViewGr0up?, savedInstanceState: Bundle?): View {
dialog?.let { containerDialog ->
containerDialog.window?.run {
WindowCompat.setDecorFitsSystemWindows(this, false)
WindowCompat.getInsetsController(this, decorView).also {
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
it.hide(WindowInsetsCompat.Type.navigationBars())
}
......
}
......
}
return LayoutLoadingDialogBinding.inflate(layoutInflater, container, false).root
}
}

修改后效果如图:


fix.gif

适配不同机型


通常来说,上述示例代码用的是官方的API,应该不会出现什么意外,然而还是出现了意外。公司的另一台三星的测试机跟我自己的测试机Pixel 3a XL效果差别很大。


三星测试机(SM-A515F)效果如下:


未调整调整后
Screen_recording_202 -big-original.gifScreen_recording_202 -big-original.gif

虽然这可能是安卓的通病,但对于这种情况我还是感到有些遗憾,通用API在不同厂商的手机上效果居然差这么多。虽然遗憾,但还是得解决问题。


根据效果图来看,对页面的配置生效了,对Dialog的配置也生效了,但是DialogFragment隐藏后重置了对页面的配置。最简单的处理就是在DialogFragment消失之后判断下导航栏是否显示,显示则隐藏。


调整代码如下:



  • 加载弹窗


class LoadingDialogFragment : DialogFragment() {

......

override fun onDestroyView() {
super.onDestroyView()
// 这里通过setFragmentResult API 来传递弹窗已经关闭的消息。
parentFragmentManager.setFragmentResult(DialogFragmentExampleActivity::class.java.simpleName, Bundle())
}
}


  • 示例页面


class DialogFragmentExampleActivity : AppCompatActivity() {

......

private var navigationBarShow = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = LayoutDialogFragmentExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}

supportFragmentManager.setFragmentResultListener(this::class.java.simpleName, this) { requestKey, result ->
// 接收加载弹窗关闭的消息
if (requestKey == this::class.java.simpleName) {
if (navigationBarShow) {
// 根据实践,不延迟500毫秒有概率出现无法隐藏的情况。
lifecycleScope.launch(Dispatchers.IO) {
delay(500L)
withContext(Dispatchers.Main) {
hideNavigationBar()
}
}
}
}
}

......

ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).run {
if (!alreadyChanged) {
alreadyChanged = true
binding.topView.updateLayoutParams<ConstraintLayout.LayoutParams> { height += top }
binding.tvTitle.updateLayoutParams<ConstraintLayout.LayoutParams> { topMargin += top }
}
// 当底部空间不为0时可以判断导航栏显示
navigationBarShow = bottom != 0
}
WindowInsetsCompat.CONSUMED
}

......
}

......

private fun hideNavigationBar() {
insetsController.hide(WindowInsetsCompat.Type.navigationBars())
}

override fun onDestroy() {
super.onDestroy()
// 页面销毁时清除监听
supportFragmentManager.clearFragmentResultListener(this::class.java.simpleName)
}
}

修改后效果如图:


Screen_recording_202 -big-original.gif

示例


演示代码已在示例Demo中添加。


ExampleDemo github


ExampleDemo gitee


作者:ChenYhong
来源:juejin.cn/post/7313742254145208356
收起阅读 »

网络七层模型快速理解和记忆

网络通信分解为七个逻辑层。称为 七层网络模型,也称为OSI(Open Systems Interconnection)模型,是国际标准化组织(ISO)为计算机和通信系统制定的一种框架,用于描述信息从一个设备传输到另一个设备的过程。每一层都有特定的功能和责任: ...
继续阅读 »

网络通信分解为七个逻辑层。称为 七层网络模型,也称为OSI(Open Systems Interconnection)模型,是国际标准化组织(ISO)为计算机和通信系统制定的一种框架,用于描述信息从一个设备传输到另一个设备的过程。每一层都有特定的功能和责任:






  1. 物理层



    • 负责数据的传输通路,包括电缆、光纤、无线电波等物理介质以及信号的电压、频率、比特率等物理特性。




  2. 数据链路层



    • 负责在两个相邻节点之间可靠地传输数据帧,包括错误检测、帧同步、地址识别以及介质访问控制(MAC)。




  3. 网络层



    • 负责将数据包从源主机传输到目标主机,通过IP地址进行寻址,并可能涉及路由选择和分组转发。




  4. 传输层



    • 提供端到端的数据传输服务,如TCP(传输控制协议)提供可靠的数据传输,UDP(用户数据报协议)提供无连接的数据传输。




  5. 会话层



    • 管理不同应用程序之间的通信会话,负责建立、维护和终止会话,以及数据的同步和复用。




  6. 表示层



    • 处理数据的格式、编码、压缩和解压缩,以及数据的加密和解密,确保数据在不同系统间具有正确的表示。




  7. 应用层



    • 提供直接与用户应用程序交互的服务,如HTTP、FTP、SMTP、DNS等协议,实现文件传输、电子邮件、网页浏览等功能。




快速理解和记忆七层网络模型(OSI模型)


可以借助以下方法:




  1. 口诀法



    • 可以使用一些助记口诀来帮助记忆各层的主要功能。例如:

      • "Please Do Not Tell Stupid People Anything",这个口诀的首字母对应了七层模型从下到上的名称:Physical、Data Link、Network、Transport、Session、Presentation、Application。

      • 或者使用其他你认为更容易记忆的口诀。






  2. 功能关联法







  • 将每一层的功能与日常生活中的例子或者已知的技术概念关联起来:

    • 物理层:想象这是网络的基础结构,如电线、光纤、无线信号等。

    • 数据链路层:思考如何在一条物理链路上确保数据帧的正确传输,如同一房间内两个人通过特定的握手方式传递信息。

    • 网络层:考虑路由器的工作,它们如何根据IP地址将数据包从一个网络转发到另一个网络。

    • 传输层:TCP和UDP协议,TCP如同邮政服务保证邮件送达,UDP如同广播消息不关心是否接收。

    • 会话层:想象两个用户在电话中建立通话的过程,包括建立连接、保持通信和断开连接。

    • 表示层:数据格式转换和加密解密,就像翻译将一种语言转换为另一种语言。

    • 应用层:各种应用程序如何通过网络进行交互,如浏览网页、发送电子邮件或文件传输。






  1. 层次结构可视化



    • 画出七层模型的图表,从下到上排列各层,并在每一层旁边标注其主要功能和相关协议。




  2. 实践理解



    • 通过学习和实践网络相关的技术,如配置网络设备、编程实现网络应用等,加深对各层功能的理解。




  3. 反复复习



    • 定期回顾和复习七层模型,随着时间的推移,对各层的理解和记忆会逐渐加深。




  4. 故事联想



    • 创建一个包含七层模型元素的故事,比如描述一个信息从发送者到接收者的完整旅程,每层都是故事中的一个关键环节。




通过这些方法的综合运用,相信我们可以更快地理解和记忆七层网络模型。当然啦,理解各层之间的关系和它们在整个通信过程中的作用是关键。




好了,今天的内容就到分享这里啦,很享受与大家一起学习,沟通交流问题,如果喜欢的话,请为我点个赞吧 !👍

plus: 最近在看工作机会,base 上海,有合适的前端岗位希望可以推荐一下啦!


作者:陳有味_ChenUvi
链接:https://juejin.cn/post/7315720126988058639
收起阅读 »

Android Room 数据库的坑

1.Android Room 数据库的坑 在用Room数据库的时候 发现有需要一个字段的条件合到一起去写这个SQL @Query("SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= :Time " + ...
继续阅读 »

1.Android Room 数据库的坑


在用Room数据库的时候 发现有需要一个字段的条件合到一起去写这个SQL


 @Query("SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= :Time " +
"and 字段B >= :Time and 字段C <= :BTime and 字段C >= :BTime " +
"and '(' || 字段D is null or 字段D = '' || ')'")

List selectList(String Time, String BTime);


这里面的 “ ||” 是Room里面独特的表达方式 是替代了java里面的“+”号


正常在android中 使用 是这样的


String sql = "SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= "+传入的参数+" " +
"and 字段B >= "+传入的参数+" and 字段C <= "+传入的参数+" and 字段c >= "+传入的参数+" " +
"and '(' "+" 字段D is null or 字段D = '' "+" ')'"


cursor = db.rawQuery(sql, null);

而在Room 中 用 “||” 代替了 “+”


2.Android Room 查询语句的坑


@Query("SELECT * FROM 表名 WHERE 字段A = '0' order by id desc")
List selectList();

假如你正在查询一张表的面的内容,然后忽然跑出来一个异常



# [Android RoomDatabase Cruash "Cursor window allocation of 4194304 bytes failed"](https://stackoverflow.com/questions/75456123/android-roomdatabase-cruash-cursor-window-allocation-of-4194304-bytes-failed)

奔溃日志:


android.database.CursorWindowAllocationException: Could not allocate CursorWindow '/data/user/0/cn.xxx.xxx/databases/xxx.db' of size 2097152 due to error -13.
at android.database.CursorWindow.nativeCreate(Native Method)
at android.database.CursorWindow.<init>(CursorWindow.java:139)
at android.database.CursorWindow.<init>(CursorWindow.java:120)
at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:202)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:140)
at yd.d.m(SourceFile:21)
at cn.xxx.control.y.y0(SourceFile:1)
at e5.y.p(SourceFile:230)
at e5.y.l(SourceFile:1)
at e5.y.E(SourceFile:1)
at cn.xxx.cluster.classin.viewmodel.SessionViewModel$d.invokeSuspend(SourceFile:42)

触发原因



  • Room 对应的 Sqlite 数据库,其对 CursorWindows 分配的大小是有限制的,最大为 2M,超过之后会发生上述崩溃闪退现象(偶现且难以复现的 bug


解决方法


需要业务方梳理这块的业务,优化数据库的调用,如果明确知道在一个方法里面会调用多个数据库的方法,需要让 controller 提供新的方法,且这个 controller 层的方法需要添加 @Transaction 进行注解,从而保证在同一个事物内进行数据库操作,以此避免 CursorWindows 大小超过 2M


那么问题来了 @Transaction 这个注解是干嘛的呢


翻译 事务的意思


@Transaction
@Query("SELECT * FROM 表名 WHERE 字段A = '0' order by id desc")
List selectList();

接着 问题完美解决


大家学“废”了嘛 学费的评论区Q1 没学“废”的抠眼珠子


作者:笨qiao先飞
来源:juejin.cn/post/7273674981959745593
收起阅读 »

这一年遇到的奇怪bug

web
position sticky 失效 在 Iphone6 plus 上使用 position sticky 不生效 解决办法: position: sticky; position: -webkit-sticky; // 兼容写法需要写在下面 参...
继续阅读 »

position sticky 失效



在 Iphone6 plus 上使用 position sticky 不生效




解决办法:



position: sticky;  
position: -webkit-sticky; // 兼容写法需要写在下面

参考 position sticky 失效 – 有点另类的写法


new Date().toLocaleDateString() 获取当前的日期字符串无效



当系统语言是新加坡英语的时候,使用这个方法获取当前的日期字符串会出现 Invalid Date,toLocaleDateString 是有两个参数的,不指定语言就会出现这个问题,而且只在手机上出现,不太好排查,new Date().toLocaleDateString('en-Us') 调用的时候指定语言就没问题了;



参考 Date.prototype.toLocaleDateString()


两行溢出显示省略号但是部分手机上出现第三行截断痕迹


image.png



例如设置了高度为36px,line-height 18px,但是出现了第三行截断痕迹,应该是文字 baseline 的对其方式问题,试着设置 vertical-align 也不行。解决办法就是不给文字的盒子设置高度,如果一定要个高度兜底,可以在文字的盒子再套一个盒子,在套的那个盒子设置高度。



泰文字体文本溢出隐藏,但是第二行出现截断痕迹



原因,应该是泰语的字体行高要求比较高,暂时的解决办法:加高文本行高



useEffect 首次获取 dom 的 clientHeight 不对



初步感觉是因为 css 样式加载慢了,导致第一次获取到的高度是没有样式的高度,而且又是偶现的;所以在这个组件或者 hooks 重新 render 的时候去获取高度,如果获取到最新的高度发生变化,去同步修改 state 保存的高度。



import { useEffect, useState } from "react";
export default function useTop(){
const [top, setTop]=useState(0);
const [bodyHeight, setBodyHeight] = useState(document.body.clientHeight);
const newestTop = (document.getElementById('nav-header')?.clientHeight || 0) - 1;

if (newestTop !== top) { // nav header height may change
setTop(newestTop);
setBodyHeight(document.body.clientHeight - (newestTop + 1));
}

useEffect(() => {
const nav = document.getElementById('nav-header');
const navHeight = nav?.clientHeight ?? 0;
setTop(navHeight-1);
setBodyHeight(document.body.clientHeight - navHeight);
}, []);
return {top, bodyHeight}
}

一个页面中有两个滚动条,两个滚动条几乎同时触发滚动条的滚动方法,后执行的不生效



两个滚动条,一个使用 scrollBy 方法,另一个使用 scrollIntoView 方法,behavior 属性都为 smooth,这个属性会让滚动条平滑移动,导致滚动条事件一直在触发状态,另一个滚动方法就执行不了了。解决方法:让先执行的方法 behavior 属性为 auto;或者在第一个滚动条结束之后再执行第二个滚动条的方法,可以让第二个方法 setTimeout 100ms 左右,不能超过 300ms,否则用户会感觉卡顿。



iphone6 手机上横向或者纵向滑动不了



原因,可能是dom结构问题,导致低端ios机型没有识别到生成滚动条,导致不能滚动,android 和其他 ios 机型正常;



<div className="list-tabs-wrap">
<div
className="list-tabs"
>

<div className="tab-item">tab1</div>
<div className="tab-item">tab2</div>
<div className="tab-item">tab3</div>
</div>
</div>

.list-tabs-wrap {
width: 100%;
background-color: #fff;
overflow: hidden;
}
.list-tabs {
overflow-x: scroll;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
height: 50px;
background-color: #fff;
}
.list-tabs::-webkit-scrollbar {
display: none;
background-color: transparent;
color: transparent;
width: 0;
height: 0;
}
.tab-item {
width: 50vw;
}


解决办法,新增一个 container 结构,container dom 宽度为 max-content,overflow 拆开写



<div className="list-tabs-wrap">
<div
className="list-tabs"
>

<div className="list-tabs-container">
<div className="tab-item">tab1</div>
<div className="tab-item">tab2</div>
<div className="tab-item">tab3</div>
</div>
</div>
</div>

.list-tabs-container {
overflow-x: scroll; // overflow 拆开写
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
width: max-content; // 纵向设置 height
}


用上面方法解决 iPhone6 滚动条问题后,又出现一个滚动条隐藏样式不生效的问题;解决办法,设置一个外层的盒子,固定高度然后 overflow: hidden,需要滚动的盒子加一个 padding-bottom: 10px,padding 大小看着改,能放下一个滚动条就可以,这样滚动条会出现在padding里,然后又因为外层盒子overflow: hidden 了,所以滚动条和padding都看不到了;愿世界再无 iphone6.



在 Android webview 中,window.location.reload 和 replace 失效


const reload = () => {
const timeStamp = new Date().getTime();
const oldUrl = window.location.href;
const url = `${oldUrl}${oldUrl.includes('?') ? '&' : '?'}timeStamp=${timeStamp}`;
window.location.href = url;
};
const locationReplace = (url) => {
if(history.replaceState) {
history.replaceState(null, document.title, url);
history.go(0);
} else {
location.replace(url);
}
};


部分安卓手机把请求参数的字符串中间的空格转义成+号



发现在 谷歌 Pixel 3 XL 手机上,会把请求参数的字符串中间的空格转义成+号,比如 '[{"filterField":"accommodationType","value":"Hotel,Entire apartment"}]' => '[{"filterField":"accommodationType","value":"Hotel,Entire+apartment"}]'。调试了下,发现在发起请求前参数打印是正常的,是浏览器在请求的时候在请求体中字段转义的。不过好像对后端的搜索结果并不影响,所以这里就没有改动。




解决办法,对字符串encode下,后端收到参数后再decode。



ios 17 input 聚焦页面出现抖动


解决办法: input focus 给 body 添加 height: 100vh; overflow: hidden; 样式。input blur 取消 focus 添加的样式。


作者:wait
来源:juejin.cn/post/7309040097936343103
收起阅读 »

注意力是最珍贵的资源

不得不承认的是很多时候我的生活都是被动的。 早上起来,洗漱完毕,吃早餐,然后挤地铁上班。我日常习惯背书包,那种双肩的,然后里面习惯性的放着两本书。但是早上上班坐地铁是基本不会看的:北京的早高峰人太多。 所以大部分时候,如果早高峰还有一些空间的话,会拿起手机看一...
继续阅读 »

不得不承认的是很多时候我的生活都是被动的。


早上起来,洗漱完毕,吃早餐,然后挤地铁上班。我日常习惯背书包,那种双肩的,然后里面习惯性的放着两本书。但是早上上班坐地铁是基本不会看的:北京的早高峰人太多。


所以大部分时候,如果早高峰还有一些空间的话,会拿起手机看一些资讯。那种碎片化的资讯,每天的资讯之间看似有关联,实际自己不能去建立关联。最后总结看资讯最大的意义是为了打发时间。


到了公司,打卡之后开始上班。上班没有那么忙也没有那么清闲。有的时候开发上遇到了难题会停下来看下手机,我后来管这个叫分散注意力,觉得这样是有益于开发的。


在公司中午休息时间,如果吃饭会一边吃饭一边看剧。或者不吃饭情况也会看剧。时间就这样来到下午,然后到下班。


回程的地铁偶尔会看看书,那本已经读过一遍的书。这里说个题外话,好书还是要多读几遍的,每隔一段时间读一次总会有不同的收获。


以上,围绕着上班建立了不少被动的东西。


如果是周六日,平时不怎么出去,会在家看看书,会琢磨着做点有意义的事情,会看剧、刷手机。


上班、副业、投资这是我给自己规划的三个路线。自己的时间主要放在这三方面。但随着时间演变,发现很多时候都忘记了下一步该怎么做,怎么做才能让该路线有所提高。


在《财富自由之路》这本书里面作者提到,注意力>成长>成功。注意力是最珍贵的且是稀缺性的资源。


在我切换每条路线时,时常忘了这条路线最初是怎么规划的。所以我后来买了不少笔记本,那种30页的笔记本。将每个路线的基本规划都记下来,时常回顾,避免遗忘。


注意力是最珍贵的资源,不仅仅是对自己。对商业也是如此,时下流行的抖音快手都是注意力经济学,都争夺的是用户的注意力。


我觉得应该把注意力应用在最为重要的领域活方面,即使现在找不到最重要的领域或者暂时找不到该怎么去做,也应该尽力去做,坚持去做。


我建议也可以买很多本将当前的思考记录下来,日日记,周周记,月月记,终会认识自己,认识当下的状况。有了一定的精确性才会有下一步的规划。


注意力是最珍贵的资源,是成长和成功的基石。


(本文完)


作者:通往自由之路
来源:juejin.cn/post/7316202808986239003
收起阅读 »

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

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


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


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


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



  • 默认单行输入

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

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

  • 支持回车发送内容

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

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


单行输入


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


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


多行输入


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


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


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


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

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


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

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

键盘发送内容


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


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


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

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


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

高度自适应


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



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


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

最后


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


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


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


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

动态代理在Android中的运用

动态代理是一种在编程中非常有用的设计模式,它允许你在运行时创建一个代理对象来代替原始对象,以便在方法调用前后执行额外的逻辑。在Android开发中,动态代理可以用于各种用例,如性能监控、AOP(面向切面编程)和事件处理。本文将深入探讨Android动态代理的原...
继续阅读 »

动态代理是一种在编程中非常有用的设计模式,它允许你在运行时创建一个代理对象来代替原始对象,以便在方法调用前后执行额外的逻辑。在Android开发中,动态代理可以用于各种用例,如性能监控、AOP(面向切面编程)和事件处理。本文将深入探讨Android动态代理的原理、用途和实际示例。


什么是动态代理?


动态代理是一种通过创建代理对象来代替原始对象的技术,以便在方法调用前后执行额外的操作。代理对象通常实现与原始对象相同的接口,但可以添加自定义行为。动态代理是在运行时生成的,因此它不需要在编译时知道原始对象的类型。


动态代理的原理


动态代理的原理涉及两个关键部分:



  1. InvocationHandler(调用处理器):这是一个接口,通常由开发人员实现。它包含一个方法 invoke,在代理对象上的方法被调用时会被调用。在 invoke 方法内,你可以定义在方法调用前后执行的逻辑。

  2. Proxy(代理类):这是Java提供的类,用于创建代理对象。你需要传递一个 ClassLoader、一组接口以及一个 InvocationHandlerProxy.newProxyInstance 方法,然后它会生成代理对象。


下面是一个示例代码,演示了如何创建一个简单的动态代理:


import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

// 接口
interface MyInterface {
fun doSomething()
}

// 实现类
class MyImplementation : MyInterface {
override fun doSomething() {
println("Original method is called.")
}
}

// 调用处理器
class MyInvocationHandler(private val realObject: MyInterface) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? {
println("Before method is called.")
val result = method.invoke(realObject, *(args ?: emptyArray()))
println("After method is called.")
return result
}
}

fun main() {
val realObject = MyImplementation()
val proxyObject = Proxy.newProxyInstance(
MyInterface::class.java.classLoader,
arrayOf(MyInterface::class.java),
MyInvocationHandler(realObject)
) as MyInterface

proxyObject.doSomething()
}

运行上述代码会输出:


Before method is called.
Original method is called.
After method is called.

这里,MyInvocationHandler 拦截了 doSomething 方法的调用,在方法前后添加了额外的逻辑。


Android中的动态代理


在Android中,动态代理通常使用Java的java.lang.reflect.Proxy类来实现。该类允许你创建一个代理对象,该对象实现了指定接口,并且可以拦截接口方法的调用以执行额外的逻辑。在Android开发中,常见的用途包括性能监控、权限检查、日志记录和事件处理。


动态代理的用途


性能监控


你可以使用动态代理来监控方法的执行时间,以便分析应用程序的性能。例如,你可以创建一个性能监控代理,在每次方法调用前记录当前时间,然后在方法调用后计算执行时间。


import android.util.Log

class PerformanceMonitorProxy(private val target: Any) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? {
val startTime = System.currentTimeMillis()
val result = method.invoke(target, *(args ?: emptyArray()))
val endTime = System.currentTimeMillis()
val duration = endTime - startTime
Log.d("Performance", "${method.name} took $duration ms to execute.")
return result
}
}

AOP(面向切面编程)


动态代理也是AOP的核心概念之一。AOP允许你将横切关注点(如日志记录、事务管理和安全性检查)从业务逻辑中分离出来,以便更好地维护和扩展代码。通过创建适当的代理,你可以将这些关注点应用到多个类和方法中。


事件处理


Android中常常需要处理用户界面上的各种事件,例如点击事件、滑动事件等。你可以使用动态代理来简化事件处理代码,将事件处理逻辑从Activity或Fragment中分离出来,使代码更加模块化和可维护。


实际示例


下面是一个简单的示例,演示了如何在Android中使用动态代理来处理点击事件:


import android.util.Log
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import android.view.View

class ClickHandlerProxy(private val target: View.OnClickListener) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? {
if (method.name == "onClick") {
Log.d("ClickHandler", "Click event intercepted.")
// 在事件处理前可以执行自定义逻辑
}
return method.invoke(target, *args.orEmpty())
}
}

// 使用示例
val originalClickListener = View.OnClickListener {
// 原始的点击事件处理逻辑
}

val proxyClickListener = Proxy.newProxyInstance(
originalClickListener::class.java.classLoader,
originalClickListener::class.java.interfaces,
ClickHandlerProxy(originalClickListener)
) as View.OnClickListener

button.setOnClickListener(proxyClickListener)

通过这种方式,你可以在原始的点击事件处理逻辑前后执行自定义逻辑,而无需修改原始的OnClickListener实现。


结论


动态代理是Android开发中强大的工具之一,它允许你在不修改原始对象的情况下添加额外的行为。在性能监控、AOP和事件处理等方面,动态代理都有广泛的应用。通过深入理解动态代理的原理和用途,你可以更好地设计和维护Android应用程序。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7275185537815183360
收起阅读 »

Android:自定义View实现签名带笔锋效果

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)、抬起(ACTION_UP)、移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、...
继续阅读 »

自定义签名工具相信大家都轻车熟路,通过监听屏幕onTouchEvent事件,分别在按下(ACTION_DOWN)抬起(ACTION_UP)移动(ACTION_MOVE) 动作中处理触碰点的收集和绘制,很容易就达到了手签笔的效果。其中无非又涉及到线条的撤回、取消、清除、画笔的粗细,也就是对收集的点集合和线集合的增删操作以及画笔颜色宽的的更改。这些功能都在 实现一个自定义有限制区域的图例(角度自识别)涂鸦工具类(上) 中介绍过。


但就在前不久遇到一个需求是要求手签笔能够和咱们使用钢笔签名类似的效果,当然这个功能目前是有一些公司有成熟的SDK的,但我们的需求是要不借助SDK,自己实现笔锋效果。那么,如何使画笔带笔锋呢?废话不多说,先上效果图:


image.png


要实现笔锋效果我们需要考虑几个因素:笔速笔宽按压力度(针对手写笔)。因为在onTouchEvent回调的次数是不变的,一旦笔速变快两点之间距离就被拉长。此时的笔宽不能保持在上一笔的宽度,需要我们通过计算插入新的点,同时计算出对应点的宽度。同理当我们笔速慢的时候,需要通过计算删除信息相近的点。要想笔锋自然,当然贝塞尔曲线是必不可少的。


这里我们暂时没有将笔的按压值作为笔宽的计算,仅仅通过笔速来计算笔宽。


/**
* 计算新的宽度信息
*/

public double calcNewWidth(double curVel, double lastVel,double factor) {
double calVel = curVel * 0.6 + lastVel * (1 - 0.6);
double vfac = Math.log(factor * 2.0f) * (-calVel);
double calWidth = mBaseWidth * Math.exp(vfac);
return calWidth;
}

/**
* 获取点信息
*/

public ControllerPoint getPoint(double t) {
float x = (float) getX(t);
float y = (float) getY(t);
float w = (float) getW(t);
ControllerPoint point = new ControllerPoint();
point.set(x, y, w);
return point;
}

/**
* 三阶曲线的控制点
*/

private double getValue(double p0, double p1, double p2, double t) {
double a = p2 - 2 * p1 + p0;
double b = 2 * (p1 - p0);
double c = p0;
return a * t * t + b * t + c;
}

最后也是最关键的地方,不再使用drawLine方式画线,而是通过drawOval方式画椭圆。通过前后两点计算出椭圆的四个点,通过笔宽计算出绘制椭圆的个数并加入椭圆集。最后在onDraw方法中绘制。


/**
* 两点之间将视图收集的点转为椭圆矩阵 实现笔锋效果
*/
public static ArrayList<SvgPointBean> twoPointsTransRectF(double x0, double y0, double w0, double x1, double y1, double w1, float paintWidth, int color) {

ArrayList<SvgPointBean> list = new ArrayList<>();
//求两个数字的平方根 x的平方+y的平方在开方记得X的平方+y的平方=1,这就是一个园
double curDis = Math.hypot(x0 - x1, y0 - y1);
int steps;
//绘制的笔的宽度是多少,绘制多少个椭圆
if (paintWidth < 6) {
steps = 1 + (int) (curDis / 2);
} else if (paintWidth > 60) {
steps = 1 + (int) (curDis / 4);
} else {
steps = 1 + (int) (curDis / 3);
}
double deltaX = (x1 - x0) / steps;
double deltaY = (y1 - y0) / steps;
double deltaW = (w1 - w0) / steps;
double x = x0;
double y = y0;
double w = w0;

for (int i = 0; i < steps; i++) {
RectF oval = new RectF();
float top = (float) (y - w / 2.0f);
float left = (float) (x - w / 4.0f);
float right = (float) (x + w / 4.0f);
float bottom = (float) (y + w / 2.0f);
oval.set(left, top, right, bottom);
//收集椭圆矩阵信息
list.add(new SvgPointBean(oval, color));
x += deltaX;
y += deltaY;
w += deltaW;
}

return list;
}

至此一个简单的带笔锋的手写签名就实现了。 最后附上参考链接Github.


我是一个喜爱Jay、Vae的安卓开发者,喜欢结交五湖四海的兄弟姐妹,欢迎大家到沸点来点歌!


作者:似曾相识2022
来源:juejin.cn/post/7244192848063627325
收起阅读 »

这是你们项目中WebView的样子吗?

这是你们项目中WebView的样子吗? 作者简介:Serpit,Android开发工程师,2023年加入37手游技术部,目前负责国内游戏发行 Android SDK 开发。 前言 开始前先问大家一个问题,你们项目中或者理想中的WebView的使用姿势是如何的?...
继续阅读 »

这是你们项目中WebView的样子吗?


作者简介:Serpit,Android开发工程师,2023年加入37手游技术部,目前负责国内游戏发行 Android SDK 开发。


前言


开始前先问大家一个问题,你们项目中或者理想中的WebView的使用姿势是如何的?有哪些规范、功能?都可以在下方评论中说出来大家讨论一下。下面正式开始介绍我们对于一个WebView使用的一些理解。


可监控


可监控是线上项目很重要的一个功能,监控的东西可以是用户体验相关数据、加载情况数据、报错等。那么,如何监控呢?这里分两点提下。


加载时间


利用WebViewClient的onPageStartedonPageFinished回调,但是注意,这两回调在某些情况下会回调多次,比如在发生重定向时候,会有多次的onPageStarted回调出现,这就需要用一些标记位或者拦截等手段来保证统计到最开始的onPageStarted和最后的onPageFinished之间的耗时。


这里贴上一段伪代码


 @Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
super.onPageStarted(webView, url, favicon);
if (TextUtils.isEmpty(url)) {
return;
}
consumeMap.put(getKey(url), SystemClock.uptimeMillis());
}

@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
if (TextUtils.isEmpty(url)) {
return;
}
Long loadConsuming = consumeMap.remove(getKey(url));
if (loadConsuming == null) {
return;
}
trackWebLoadFinish(url, SystemClock.uptimeMillis() - loadConsuming); //记录耗时,埋点
}

报错监控


报错这一块可以使用WebViewClient等一系列回调,诸如onReceivedErroronReceivedHttpErroronReceivedSslError这几个。只要在这几个方法中加上相应的埋点日志即可。但同时有注意的点是onReceivedError这个方法会有些报错是无用的,不影响用户使用,需要进行过滤。这里可以参考网上的一些方法做以下处理



  • 加载失败的url跟WebView里的url不是同一个url,过滤

  • errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,过滤

  • failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,过滤


除了这些常规的,还有一个是使用onConsoleMessage,去监控前端的错误日志,发现到前端的一些报错~也是一个不错的方向。


与前端的交互


与前端的交互,这个方式相信在网上随便一搜“Android与JS相互通信”等关键词,就可以搜出一大堆。在Android侧需要注册一个JavascriptInterface,里面定义各个方法,然后前端就可以调用到。然后需要调用到前端的函数,则利用WebView的evaluateJavascript方法即可。这些都比较基础,这里就不展开说,不清楚的同学可以用搜索引擎搜索一下~


这里想说的点其实是,在项目中如果简单定义JavascriptInterface可能会有“风险”。想象一个场景,定义了一个getToken方法,给到前端获取token方法,用于获取用户信息,这是一个很常见的方法。但是,重点来了,假如应用加载了一个外部的“恶意网站”,调用了这个getToken方法,就造成了信息泄漏。在安全上就出现了问题。


那么,有什么思路可以限制一下呢?有同学可能会想到,“混淆”把接口方法换成一些奇奇怪怪的方法,但这也太不利于代码可读性了吧。那这里可以提供一个思路,就是“鉴权拦截”。如何做?


先上代码


private class WebViewJsInterface {
@JavascriptInterface
public void callAndroid(final String method, final String params) {
boolean result = intercept(method, params); //拦截方法
if (!result){
dispatcher.callAndroid(method, params);
}
}
}

这里想说的是,可以在项目里面使用统一调度的方式,前端调用安卓的入口只有一个,然后通过传入方法名来分发到不同方法上,这样的好处是,可以做到统一,统一的目的也是为了做拦截!在拦截上,我们就可以做很多文章,诸如域名校验,白名单上的域名直接不让调用原生方法。这样可以增加一定的安全性。


关于WebView的一些使用封装思路


我们知道WebView的灵魂其实有三个部分



  • WebView.getSetting()的设置

  • WebViewClient

  • WebChromeClient


我们做的很多功能都是基于着这三者来实现,那么在代码中,很多项目或者同学都是直接封装一个BaseWebViewClient,然后在里面做一堆逻辑处理,比如上面说的监控方法,或者loading操作等。这么做没有说不好,但是会由于业务的日益拓展,会使得这个“Base”日益臃肿,变得难以维护。在经历过这个阶段的我们,也想出了一个办法去优化。那就是用拦截器的思路,架构图如下:


image.png


这里,由于WebView不可以设置多个Client,那么就使用拦截器,将WebViewClient和WebChromeClient所有方法都封装起来,分发出去,每一个拦截器都负责自己的功能即可。比如实现一个loading的逻辑:


public class ProgressWebHook extends WebHook {

private final IWebViewLoading mWebViewLoading;

public ProgressWebHook(IWebViewLoading loading) {
this.mWebViewLoading = loading;
}

@Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
super.onPageStarted(webView, url, favicon);
startLoading();
}

@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
stopLoading();
}

@Override
public void onReceivedError(WebView webView, String url, int errorCode, String description) {
super.onReceivedError(webView, url, errorCode, description);
stopLoading();
}

@Override
public void onProgressChanged(WebView webView, int newProgress) {
super.onProgressChanged(webView, newProgress);
mWebViewLoading.onProgress(getContext(), newProgress);
}
}

这样是不是简洁很多。再比如监控的时候,也实现一个WebHook,只负责监控相关的方法重写即可。就可以将这些方法不同功能方法隔离在不同的类中,方便解耦。至此,也会有同学想看下如何实现的拦截器,那这里也简单给大家看下。



public class BaseWebChromeClient extends WebChromeClient {

private final WebHookDispatcher mWebHookDispatcher;

public BaseWebChromeClient(WebHookDispatcher webHookDispatcher) {
this.mWebHookDispatcher = webHookDispatcher;
}

@Override
public void onPermissionRequest(PermissionRequest request) {
mWebHookDispatcher.onPermissionRequest(request);
}

@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
mWebHookDispatcher.onReceivedTitle(view, title);
}

@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
mWebHookDispatcher.onProgressChanged(view, newProgress);
}

// For Android >= 5.0
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams)
{
return mWebHookDispatcher.onShowFileChooser(webView, filePathCallback, fileChooserParams);
}

@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
mWebHookDispatcher.onConsoleMessage(consoleMessage);
return super.onConsoleMessage(consoleMessage);
}

@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
if (mWebHookDispatcher.onJsAlert(view, url, message, result)) {
return true;
}
return super.onJsAlert(view, url, message, result);
}

@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
if (mWebHookDispatcher.onJsPrompt(view, url, message, defaultValue, result)) {
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}

@Override
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
if (mWebHookDispatcher.onJsBeforeUnload(view, url, message, result)) {
return true;
}
return super.onJsBeforeUnload(view, url, message, result);
}

@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
super.onShowCustomView(view, callback);
mWebHookDispatcher.onShowCustomView(view, callback);
}

@Override
public void onHideCustomView() {
super.onHideCustomView();
mWebHookDispatcher.onHideCustomView();
}
}


拦截分发代码如下:



public class WebHookDispatcher extends SimpleWebHook {

/**
* 因为shouldInterceptRequest是一个异步的回调,所以这个类需要加锁
*/

private final List<WebHook> webHooks = new CopyOnWriteArrayList<>();

public void addWebHook(WebHook webHook) {
webHooks.add(webHook);
if (hasInit) {
webHook.onWebInit(mWebView);
}
}

public void addWebHooks(Collection<WebHook> webHooks) {
this.webHooks.addAll(webHooks);
if (hasInit) {
for (WebHook webHook : webHooks) {
webHook.onWebInit(mWebView);
}
}
}

public void addWebHook(int position, WebHook webHook) {
webHooks.add(position, webHook);
if (hasInit) {
webHook.onWebInit(mWebView);
}
}

public void addWebHooks(int position, Collection<WebHook> webHooks) {
this.webHooks.addAll(position, webHooks);
if (hasInit) {
for (WebHook webHook : webHooks) {
webHook.onWebInit(mWebView);
}
}
}

@Nullable
public WebHook findWebHookByClass(Class<? extends WebHook> clazz) {
for (WebHook webHook : webHooks) {
if (webHook.getClass().equals(clazz)) {
return webHook;
}
}
return null;
}

public void removeWebHook(WebHook webHook) {
webHooks.remove(webHook);
}

@NonNull
public List<WebHook> getWebHooks() {
return webHooks;
}

public void clear() {
webHooks.clear();
}

//dispatch method ----------------

@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
for (WebHook webHook : webHooks) {
if (webHook.shouldOverrideUrlLoading(webView, url)) {
return true;
}
}
return super.shouldOverrideUrlLoading(webView, url);
}


@Override
public void onPageFinished(WebView webView, String url) {
for (WebHook webHook : webHooks) {
webHook.onPageFinished(webView, url);
}
}

@Override
public void onReceivedTitle(WebView webView, String title) {
for (WebHook webHook : webHooks) {
webHook.onReceivedTitle(webView, title);
}
}

@Override
public void onProgressChanged(WebView webView, int newProgress) {
for (WebHook webHook : webHooks) {
webHook.onProgressChanged(webView, newProgress);
}
}

@Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
for (WebHook webHook : webHooks) {
webHook.onPageStarted(webView, url, favicon);
}
}


@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams)
{
for (WebHook webHook : webHooks) {
if (webHook.onShowFileChooser(webView, filePathCallback, fileChooserParams)) {
return true;
}
}
return false;
}

@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent intent) {
for (WebHook webHook : webHooks) {
if (webHook.onActivityResult(requestCode, resultCode, intent)) {
return true;
}
}
return false;
}


@Override
public void onReceivedError(WebView webView, WebResourceRequest request, WebResourceError error) {
for (WebHook webHook : webHooks) {
webHook.onReceivedError(webView, request, error);
}
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
for (WebHook webHook : webHooks) {
WebResourceResponse response = webHook.shouldInterceptRequest(view, request);
if (response != null) {
return response;
}
}
return super.shouldInterceptRequest(view, request);
}

@Override
public boolean onBackPressed() {
for (WebHook webHook : webHooks) {
if (webHook.onBackPressed()) {
return true;
}
}
return super.onBackPressed();
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
for (WebHook webHook : webHooks) {
if (webHook.onKeyUp(keyCode, event)) {
return true;
}
}
return super.onKeyUp(keyCode, event);
}

@Override
public void onConsoleMessage(ConsoleMessage consoleMessage) {
for (WebHook webHook : webHooks) {
webHook.onConsoleMessage(consoleMessage);
}
}

@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
for (WebHook webHook : webHooks) {
webHook.onReceivedSslError(view, handler, error);
}
super.onReceivedSslError(view, handler, error);
}

@Override
public void onReceivedError(WebView webView, String url, int errorCode, String description) {
for (WebHook webHook : webHooks) {
webHook.onReceivedError(webView, url, errorCode, description);
}
super.onReceivedError(webView, url, errorCode, description);
}


@Override
public void onPermissionRequest(PermissionRequest request) {
super.onPermissionRequest(request);
for (WebHook webHook : webHooks) {
webHook.onPermissionRequest(request);
}
}
// ...其余回调代码省略

总结


上述介绍了一些日常项目中的WebView使用思路介绍,希望可以对一些小伙伴有作用。欢迎小伙伴们能评论,发下你们项目中的WebView的优秀思路或技巧,大家共同进步~


作者:37手游移动客户端团队
来源:juejin.cn/post/7316202809383321609
收起阅读 »

Android WebView — 实现保存页面功能

在开发公司App期间遇到一个需求,在游戏页中使用WebView展示游戏网页,退出游戏页再次进入时,如果是同一个游戏就直接回到退出时的页面。按照一般的做法,在页面关闭后销毁WebView,再次进入游戏页时,不论是否为同个游戏肯定会重新加载。实现保存页面功能之前同...
继续阅读 »

在开发公司App期间遇到一个需求,在游戏页中使用WebView展示游戏网页,退出游戏页再次进入时,如果是同一个游戏就直接回到退出时的页面。按照一般的做法,在页面关闭后销毁WebView,再次进入游戏页时,不论是否为同个游戏肯定会重新加载。

实现保存页面功能

之前同事分享了一篇提高WebView渲染效率的文章,其中提到可以提前通过MutableContextWrapper创建WebView并缓存起来,在需要的页面里从缓存中获取WebView,并把MutableContextWrapper切换为对应ActivityFragmentContext。根据文中的测试结果来看,提升的效率用户基本无法感知,但正好可以用来实现我们需要的功能。

具体方案如下:

  • 在一个单例类(也可以直接用Application)中,创建一个Map用于存放需要保留的WebView和其打开的网页链接。
  • 在进入页面时,判断外部传入的网页链接和缓存的网页链接是否为同一个,是就使用缓存的WebView,不是就销毁缓存的WebView并创建一个新的。
  • 关闭页面时,将MutableContextWrapper设置为ApplicationContext,并将WebView从页面布局中移除。

示例代码如下:

  • 单例类
object WebVIewCacheController {  

// 经过实际测试需要如此实现
val webViewContextWrapperCache = MutableContextWrapper(ExampleApplication.exampleContext)

// Key为网页链接,Value为WebView
val webViewCache = ArrayMap()
}
  • 示例页面
class ReservePageExampleActivity : AppCompatActivity() {  

private lateinit var binding: LayoutReservePageExampleActivityBinding

private var currentWeb: WebView? = null

private val webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress)
binding.pbWebLoadProgress.run {
post { progress = newProgress }
if (newProgress >= 100 && visibility == View.VISIBLE) {
postDelayed({ visibility = View.GONE }, 500)
}
}
}
}
private val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
binding.pbWebLoadProgress.run { post { visibility = View.VISIBLE } }
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutReservePageExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// 处理系统返回事件
handleBackPress()
}
})
intent.getStringExtra(PARAMS_LINK_URL)?.let { websiteUrl ->
// 切换Context
WebVIewCacheController.webViewContextWrapperCache.baseContext = this
// 获取缓存
val cacheWebsiteUrl = WebVIewCacheController.webViewCache.entries.firstOrNull()?.key
currentWeb = WebVIewCacheController.webViewCache.entries.firstOrNull()?.value
if (websiteUrl == cacheWebsiteUrl) {
// 加载同个网页,使用缓存的WebView
currentWeb?.let {
// 确保控件没有父控件
removeViewParent(it)
// 添加到页面布局最底层。
binding.root.addView(it, 0)
}
} else {
// 加载不同网页,释放旧的WebView并创建新的
createWebView(websiteUrl)
}
}
}

private fun createWebView(webSiteUrl: String) {
releaseWebView(currentWeb)
WebVIewCacheController.webViewCache.clear()
currentWeb = WebView(WebVIewCacheController.webViewContextWrapperCache).apply {
initWebViewSetting(this)
// 设置背景为黑色,根据自己需求可以忽略
setBackgroundColor(ContextCompat.getColor(this@ReservePageExampleActivity, R.color.color_black_222))
layoutParams = ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT)
// 确保控件没有父控件
removeViewParent(this)
// 添加到页面布局最底层。
binding.root.addView(this, 0)
loadUrl(webSiteUrl)
// 缓存WebView
WebVIewCacheController.webViewCache[webSiteUrl] = this
}
}

@SuppressLint("SetJavaScriptEnabled")
private fun initWebViewSetting(webView: WebView) {
val settings = webView.settings
settings.cacheMode = WebSettings.LOAD_DEFAULT
settings.domStorageEnabled = true
settings.allowContentAccess = true
settings.allowFileAccess = true
settings.allowFileAccessFromFileURLs = true
settings.allowUniversalAccessFromFileURLs = true
settings.useWideViewPort = true
settings.loadWithOverviewMode = true
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW

settings.javaScriptEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = true

webView.webChromeClient = webChromeClient
webView.webViewClient = webViewClient
}

private fun handleBackPress() {
if (currentWeb?.canGoBack() == true) {
currentWeb?.goBack()
} else {
minimize()
}
}

private fun minimize() {
// 切换Context
WebVIewCacheController.webViewContextWrapperCache.baseContext = applicationContext
// 暂时先把WebView移出布局
currentWeb?.let { binding.root.removeView(it) }
finish()
}

private fun releaseWebView(webView: WebView?) {
webView?.run {
loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
clearHistory()
clearCache(false)
binding.root.removeView(this)
destroy()
}
}

private fun removeViewParent(view: View) {
try {
val parent = view.parent
(parent as? ViewGr0up)?.removeView(view)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

效果如图:

Screen_recording_202 -big-original.gif

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

内存占用问题

WebView通常会占用不少内存,在我司的App中其占用内存基本不会小于100M,甚至能到200M以上。在WebView占用了大量内存的情况下,如果App中还有其他的功能对内存需求较高,就容易出现OOM。其实在页面销毁时正确的销毁WebView可以释放其占用的内存,但就无法实现我们需要的功能,因此需要另寻他法。

跟leader讨论后,决定采用子进程的方案,即WebView单独运行在子进程中,不同进程的内存分配是独立的,所以基本可以解决OOM问题。

子进程的配置很简单,在AndroidManifest中配置一下WebView所在页面即可,如下:

"1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
......>


<activity
android:name=".web.reserve.ReservePageExampleActivity"
android:process=":webviewpage" />

application>
manifest>

独立进程也会带来一些新的问题 :

  1. 跨进程通信。

这点在我司的App中基本无需额外处理,因为其他页面与游戏页的交互仅仅只有传入网页链接,通过Bundle带入即可。若Bundle无法满足需求,可以考虑使用广播、MessageContentProviderAIDL等跨进程通信方案。

  1. 子进程初始化时,会有一小段白屏时间(与应用冷启动一样)。

初始化白屏的体验效果不好,这边提供一个思路,在WebView所在页面的前置页加载数据的同时初始化子进程,等子进程初始化完成后再结束加载,并配置android:windowDisablePreview来隐藏启动页面时的白屏。


作者:ChenYhong
来源:juejin.cn/post/7315727549376380964
收起阅读 »

程序员的“防御性编程”

最近都在聊程序员要做好“防御性编程”,"防御性编程"的概念从之前的“保护程序”一下子变成了现在的“保护程序员”,一字之差,千差万别。 1、IT行业寒风凛冽 今年整体大环境特别寒冷,IT行业也是重灾区,许多中小软件互联网企业大规模倒闭,大厂持续裁人,到处都在降本...
继续阅读 »

最近都在聊程序员要做好“防御性编程”,"防御性编程"的概念从之前的“保护程序”一下子变成了现在的“保护程序员”,一字之差,千差万别。


1、IT行业寒风凛冽


今年整体大环境特别寒冷,IT行业也是重灾区,许多中小软件互联网企业大规模倒闭,大厂持续裁人,到处都在降本增“笑”、开“猿”节流。每个IT从业者都感到前所未有的焦虑和迷茫。


笔者也了解了周边朋友的境况。朋友A和朋友B创业多年,主打软件开发,今年快撑不下去了,市面上明显没有项目可做。朋友C也是创业多年,主打安全性产品,客户缩水不少,目前基本处于贷款发工资的情况,也是负债累累。朋友D在大厂干了几年,最近被裁,不过拿到一笔不错的赔偿。朋友E在大厂苟着,每天疯狂加班,一直担心被裁。


2、我对防御性编程的看法


程序员为了保住自己不被裁掉,想了一系列的“防御性编程”方法,比如:变量命名混乱、到处是CV大发、一行注释不写 等等。总之就一条:只写自己看得懂别人维护不了的代码,让自己成为那个不可替代的人。


网上有人觉得这种“防御性编程”方式不可取,不可取的原因有2个



  1. 损害了团队和公司的利益。

  2. 对程序员的职业生涯造成负面影响。


笔者觉得这2点有一定的道理,但是也要辩证看待



  1. 大环境不好,每个人背后都是一个家庭,作为个人,做出自保行为,完全可以理解。其实这个已经无关个人素质和能力了。如果你是一位大龄程序员,能力和素质都挺好的,但是公司就是要裁你,你会怎么办?可能你也会选择“防御性编程”吧。

  2. IT行业内,有不少能人,他们打牢了基建,保障了系统的稳定,工程化做的也好,代码写的好,下班也早,反而会误认为是可有可无的人。面对这样的公司或者领导,那你也只能是选择“防御性编程”了。

  3. 站在个人的角度去看,如果自己都无法自保了,谁还管团队咋样,公司咋样?

  4. 作为程序员,还是要尽量减少这种“防御性编程”,如果是为了自保有意为之可以理解,如果是长期这样,养成坏习惯,那确实损害的是自己的名誉,确实会造成自己职业生涯的负面影响。


所以笔者觉得,是否要采用“防御性编程”,完全要视情况而定。如果公司不得以裁人,但是善待被裁的员工,相信程序员也不会采用“防御性编程”,谁不想把自己经手的事情做到至善至美呢?如果公司恶意裁人,各种恶心人的话,那我还是很支持程序员采用“防御性编程”自保的。


3、成长和职业拓展


不管咋样,其实我们都知道,真正的职业安全感来自不断的学习和成长。只有这样,才能在这个充满未知的环境中站稳脚跟。


其次就是尽早开启属于自己的副业,多元化发展。个体是无法左右大环境的,唯一能做的就是让自己不断成长,尽量多一份收入,来保障自己和家人。


4、没钱的真实感受


最近,一个朋友跟我聊,下面是他没钱后的一些感受,挺真实的。希望这种感受不要出现在我们平凡的IT打工人身上。他是这么说的:



最后,祝愿每位IT打工人都能平稳度过这个寒冬。


本篇完结!感谢你的阅读,欢迎点赞 关注 收藏 私信!!!


原文链接: mp.weixin.qq.com/s/ts1CQegwZ…


作者:不焦躁的程序员
来源:juejin.cn/post/7315219036301377588
收起阅读 »

面试官问我按钮级别权限怎么控制,我说v-if,面试官说再见

web
最近的面试中有一个面试官问我按钮级别的权限怎么控制,我说直接v-if啊,他说不够好,我说我们项目中按钮级别的权限控制情况不多,所以v-if就够了,他说不够通用,最后他对我的评价是做过很多东西,但是都不够深入,好吧,那今天我们就来深入深入。 因为我自己没有相关实...
继续阅读 »

最近的面试中有一个面试官问我按钮级别的权限怎么控制,我说直接v-if啊,他说不够好,我说我们项目中按钮级别的权限控制情况不多,所以v-if就够了,他说不够通用,最后他对我的评价是做过很多东西,但是都不够深入,好吧,那今天我们就来深入深入。


因为我自己没有相关实践,所以接下来就从这个有16.2k星星的后台管理系统项目Vue vben admin中看看它是如何做的。


获取权限码


要做权限控制,肯定需要一个code,无论是权限码还是角色码都可以,一般后端会一次性返回,然后全局存储起来就可以了,Vue vben admin是在登录成功以后获取并保存到全局的store中:


import { defineStore } from 'pinia';
export const usePermissionStore = defineStore({
state: () => ({
// 权限代码列表
permCodeList: [],
}),
getters: {
// 获取
getPermCodeList(){
return this.permCodeList;
},
},
actions: {
// 存储
setPermCodeList(codeList) {
this.permCodeList = codeList;
},

// 请求权限码
async changePermissionCode() {
const codeList = await getPermCode();
this.setPermCodeList(codeList);
}
}
})

接下来它提供了三种按钮级别的权限控制方式,一一来看。


函数方式


使用示例如下:


<template>
<a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4">
拥有[20000,2000010]code可见
</a-button>
</template>

<script lang="ts">
import { usePermission } from '/@/hooks/web/usePermission';

export default defineComponent({
setup() {
const { hasPermission } = usePermission();
return { hasPermission };
},
});
</script>

本质上就是通过v-if,只不过是通过一个统一的权限判断方法hasPermission


export function usePermission() {
function hasPermission(value, def = true) {
// 默认视为有权限
if (!value) {
return def;
}

const allCodeList = permissionStore.getPermCodeList;
if (!isArray(value)) {
return allCodeList.includes(value);
}
// intersection是lodash提供的一个方法,用于返回一个所有给定数组都存在的元素组成的数组
return (intersection(value, allCodeList)).length > 0;

return true;
}
}

很简单,从全局store中获取当前用户的权限码列表,然后判断其中是否存在当前按钮需要的权限码,如果有多个权限码,只要满足其中一个就可以。


组件方式


除了通过函数方式使用,也可以使用组件方式,Vue vben admin提供了一个Authority组件,使用示例如下:


<template>
<div>
<Authority :value="RoleEnum.ADMIN">
<a-button type="primary" block> 只有admin角色可见 </a-button>
</Authority>
</div>
</template>
<script>
import { Authority } from '/@/components/Authority';
import { defineComponent } from 'vue';
export default defineComponent({
components: { Authority },
});
</script>

使用Authority包裹需要权限控制的按钮即可,该按钮需要的权限码通过value属性传入,接下来看看Authority组件的实现。


<script lang="ts">
import { defineComponent } from 'vue';
import { usePermission } from '/@/hooks/web/usePermission';
import { getSlot } from '/@/utils/helper/tsxHelper';

export default defineComponent({
name: 'Authority',
props: {
value: {
type: [Number, Array, String],
default: '',
},
},
setup(props, { slots }) {
const { hasPermission } = usePermission();

function renderAuth() {
const { value } = props;
if (!value) {
return getSlot(slots);
}
return hasPermission(value) ? getSlot(slots) : null;
}

return () => {
return renderAuth();
};
},
});
</script>

同样还是使用hasPermission方法,如果当前用户存在按钮需要的权限码时就原封不动渲染Authority包裹的内容,否则就啥也不渲染。


指令方式


最后一种就是指令方式,使用示例如下:


<a-button v-auth="'1000'" type="primary" class="mx-4"> 拥有code ['1000']权限可见 </a-button>

实现如下:


import { usePermission } from '/@/hooks/web/usePermission';

function isAuth(el, binding) {
const { hasPermission } = usePermission();

const value = binding.value;
if (!value) return;
if (!hasPermission(value)) {
el.parentNode?.removeChild(el);
}
}

const mounted = (el, binding) => {
isAuth(el, binding);
};

const authDirective = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted,
};

// 注册全局指令
export function setupPermissionDirective(app) {
app.directive('auth', authDirective);
}

只定义了一个mounted钩子,也就是在绑定元素挂载后调用,依旧是使用hasPermission方法,判断当前用户是否存在通过指令插入的按钮需要的权限码,如果不存在,直接移除绑定的元素。


很明显,Vue vben admin的实现有两个问题,一是不能动态更改按钮的权限,二是动态更改当前用户的权限也不会生效。


解决第一个问题很简单,因为上述只有删除元素的逻辑,没有加回来的逻辑,那么增加一个updated钩子:


app.directive("auth", {
mounted: (el, binding) => {
const value = binding.value
if (!value) return
if (!hasPermission(value)) {
// 挂载的时候没有权限把元素删除
removeEl(el)
}
},
updated(el, binding) {
// 按钮权限码没有变化,不做处理
if (binding.value === binding.oldValue) return
// 判断用户本次和上次权限状态是否一样,一样也不用做处理
let oldHasPermission = hasPermission(binding.oldValue)
let newHasPermission = hasPermission(binding.value)
if (oldHasPermission === newHasPermission) return
// 如果变成有权限,那么把元素添加回来
if (newHasPermission) {
addEl(el)
} else {
// 如果变成没有权限,则把元素删除
removeEl(el)
}
},
})

const hasPermission = (value) => {
return [1, 2, 3].includes(value)
}

const removeEl = (el) => {
// 在绑定元素上存储父级元素
el._parentNode = el.parentNode
// 在绑定元素上存储一个注释节点
el._placeholderNode = document.createComment("auth")
// 使用注释节点来占位
el.parentNode?.replaceChild(el._placeholderNode, el)
}

const addEl = (el) => {
// 替换掉给自己占位的注释节点
el._parentNode?.replaceChild(el, el._placeholderNode)
}

主要就是要把父节点保存起来,不然想再添加回去的时候获取不到原来的父节点,另外删除的时候创建一个注释节点给自己占位,这样下次想要回去能知道自己原来在哪。


第二个问题的原因是修改了用户权限数据,但是不会触发按钮的重新渲染,那么我们就需要想办法能让它触发,这个可以使用watchEffect方法,我们可以在updated钩子里通过这个方法将用户权限数据和按钮的更新方法关联起来,这样当用户权限数据改变了,可以自动触发按钮的重新渲染:


import { createApp, reactive, watchEffect } from "vue"
const codeList = reactive([1, 2, 3])

const hasPermission = (value) => {
return codeList.includes(value)
}

app.directive("auth", {
updated(el, binding) {
let update = () => {
let valueNotChange = binding.value === binding.oldValue
let oldHasPermission = hasPermission(binding.oldValue)
let newHasPermission = hasPermission(binding.value)
let permissionNotChange = oldHasPermission === newHasPermission
if (valueNotChange && permissionNotChange) return
if (newHasPermission) {
addEl(el)
} else {
removeEl(el)
}
};
if (el._watchEffect) {
update()
} else {
el._watchEffect = watchEffect(() => {
update()
})
}
},
})

updated钩子里更新的逻辑提取成一个update方法,然后第一次更新在watchEffect中执行,这样用户权限的响应式数据就可以和update方法关联起来,后续用户权限数据改变了,可以自动触发update方法的重新运行。


好了,深入完了,看着似乎也挺简单的,我不确定这些是不是面试官想要的,或者还有其他更高级更优雅的实现呢,知道的朋友能否指点一二,在下感激不尽。


作者:街角小林
来源:juejin.cn/post/7209648356530896953
收起阅读 »

IDEA 目录不显示BUG排查

之前很多次从gitLab拉下项目后,IDEA中会出现下图的情况,项目目录直接消失。每次遇到这种问题重启大法都是无往不利的,也就没太在意这个问题。但是今天的现象异常诡异,网上没有任何同类型bug,值得记录下。 bug排查 之前重启能解决的都是缓存问题,这个你自...
继续阅读 »

之前很多次从gitLab拉下项目后,IDEA中会出现下图的情况,项目目录直接消失。每次遇到这种问题重启大法都是无往不利的,也就没太在意这个问题。但是今天的现象异常诡异,网上没有任何同类型bug,值得记录下。


image.png

bug排查


之前重启能解决的都是缓存问题,这个你自己处理也不容易。不是不想深挖底层原理,而是重启更有性价比


但是,今天我在IDEA中加载了一个python项目,问题就变得复杂起来了。


之前的目录显示BUG应该是缓存问题:IDEA在运行过程中会缓存一些数据,如果缓存出现问题,可能会导致目录显示异常


缓存问题只是简单的重启就能恢复,但是这次我把IDEA的三大重启法试了个遍也没能处理这个问题。


image.png



需要注意的是,重启后不是完全没有显示目录,而是一开始显示,然后在加载过程中马上就没了。 这个现象不好截图展示,只能文字描述了。



除了缓存问题,IDEA中目录突然变为空的原因还可能有以下几种:



  1. 配置文件出错:这可能是由于某些配置文件出现了错误或损坏,导致IDEA无法正确识别项目目录。

  2. 插件冲突:某些插件可能与IDEA的某些版本不兼容,导致目录显示异常。


我比较倾向于是配置文件出错,因为一开始显示,后续马上消失就像是配置文件存在异常,我启动时加载到了配置文件,然后本来存在的目录目录就没了。


对于配置文件出错的问题,我在网络上查到很多资料说只要把.idea.iml文件删除然后重新加载就能解决。


但是经过尝试,这个方法对我来说并没有什么用,甚至我在文件目录中就没找到这两个文件。


至于插件冲突,更不合理,因为IDEA的插件是全局插件,我其他打开的项目都没有问题,就单单只有这一个存在问题,所以我连试都没试,就直接略过这个原因了。



事实上,根据最后成功解决后的结果来看就是没有配置文件的问题。



最后的解决方案


虽然,当时还没定位到原因,但是看到一些解决方案我都尝试了下,最后有效的具体操作方法如下:


1. 打开Project Stryctrue


image.png

2. 点击modules,选择import module


image.png

3. 选择IDEA中本项目的文件夹



我这边因为目录都是公司的项目,比较敏感就不放出来截图了。



选择后会出现以下界面。一路next ,然后点击apply,然后点击 OK即可。


image.png

解决结果


image.png

从最后的显示结果来看,IDEA自动生成了配置文件,项目目录不显示确实是配置文件出现了问题。


作者:DaveCui
来源:juejin.cn/post/7315260397371244559
收起阅读 »

Kotlin魔法——优雅实现多函数回调

补充 写完这篇文章并发布了之后才发现,这个写法已经有人发布过了,也可以参考参考~ 如何让你的回调更具Kotlin风味 Kotlin DSL回调 写在前面 在网络请求时,经常面临一类情况:网络请求有可能成功,也有可能失败,这就需要两个回调函数来分别对成功和失败...
继续阅读 »

补充


写完这篇文章并发布了之后才发现,这个写法已经有人发布过了,也可以参考参考~


如何让你的回调更具Kotlin风味


Kotlin DSL回调


写在前面



在网络请求时,经常面临一类情况:网络请求有可能成功,也有可能失败,这就需要两个回调函数来分别对成功和失败的情况来进行处理,那么,在Kotlin这门无比强大的语言中,有没有一种“魔法”,能够优雅地实现这一类同时可能需要多个回调的场景呢?



场景


问题的场景已经提出,也就是当某一个行为需要有多个回调函数的时候,并且这些回调并不一定都会触发。


例如,网络请求的回调场景中,有时候是onSuccess触发,有时候是onFailure触发,这两个函数的函数签名也不一定相同,那么怎么实现这个需求呢?


接下来我们以一个具体的问题贯穿全文:


假设我们现在要写一个网络请求框架,在封装上层回调的时候,需要封装两个回调(onSuccess/onFailure)供上层(就假设是UI层吧,不搞什么MVVM架构了)调用,以便UI层能知道网络请求成功/失败了,并进行相应的UI更新。



注: 标题所说的“魔法”是指实现方式三,方式一和二只是为了三铺垫的引子,如果想直奔主题那么建议直接跳转实现方式三!



实现方式一:直接传参


最直接的当然是直接传参嘛,把这两个回调写成函数参数,直接传进去,这当然可以实现目标,简单的示例代码如下。


网络请求层


data class RequestConfig(val api: String, val bodyJson: String, val method: String = "POST")
data class Data(val myData1: Int, val myData2: Boolean)

//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig, onSuccess: (data: Data) -> Unit = {}, onFailure: (errorMsg: String) -> Unit = {}) {
//假设调用更底层如Retrofit等模块,成功拿到数据后调用
onSuccess(Data(1, true))

//或者,失败后调用
onFailure("断网啦")
}

UI层


@Composable
fun MyView() {
Button(onClick = {
fetchData(requestConfig = RequestConfig("/user/info", ""), onSuccess = {
//更新UI
}, onFailure = {
//弹Toast提示用户
})
}) { }
}

在网络请求层,通过把fetchData的回调参数设一个默认值,我们也能实现“回调可选”这一需求。


这似乎并没有什么问题,那么还有没有什么别的实现方式呢?


实现方式二:链式调用


简单的思考过后,发现链式调用似乎也能满足我们的需求,实现如下。


网络请求层


在网络请求层,我们预先封装一个表示请求结果的类MyResult,然后让fetchData返回这个结果。


data class MyResult(val code: Int, val msg: String, val data: Data) {
fun onSuccess(block: (data: Data) -> Unit) = this.also {
if (code == 200) { //判断交给MyResult,若code==200,则认为成功
block(data)
}
}

fun onFailure(block: (errorMsg: String) -> Unit) = this.also {
if (code != 200) { //判断交给MyResult,若code!=200,则认为失败
block(msg)
}
}
}

//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig): MyResult {
return retrofitRequest(requestConfig)
}

UI层


此时的UI层调用fetchData时,则是通过MyResult这个返回值进行链式调用,并且链式调用也是自由可选的。


@Composable
fun MyView() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")).onSuccess {
//更新UI
}.onFailure {
//弹Toast提示用户
}
}) { }
}

这也似乎并没有什么问题,但是,总感觉不够Kotlin!


其实写多了Kotlin就会发现,Kotlin似乎非常喜欢花括号{},也就是作用域或者lambda这个概念。


而且Kotlin还喜欢把最后一个花括号放在最后一个参数,以便提到最外层去。


那么!有没有一种办法,能够以Kotlin常见的作用域的方式,优雅地完成上述场景需求呢?


锵锵!主角登场!


实现方式三:继承+扩展函数=魔法!


不多说,让我们先来看看这种实现方式的效果!


用这种方式,上述UI层将会变成这样!



  • 如果什么也不需要处理


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", ""))
}) { }
}


  • 如果需要处理onSuccess


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")) {
onSuccess {
//更新UI
}
}
}) {

}
}


  • 如果需要同时能处理onSuccess和onFailure


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")) {
onSuccess {
//更新UI
}
onFailure {
//弹Toast提示用户
}
}
}) {

}
}

看到了吗!!!非常自由,而且没有任何多余的->.或者,,只有非常整齐的花括号!


真的太神奇啦!


那么,这是怎么做到的呢?


揭秘时刻


在网络请求层,我们需要先定义一个接口,用于定义我们需要的多个回调函数!


interface ResultScope {
fun onSuccess(block: (data: Data) -> Unit)
fun onFailure(block: (errorMsg: String) -> Unit)
}

接着我们自己在内部实现这个接口!


internal class ResultScopeImpl : ResultScope {
var onSuccessBlock: (data: Data) -> Unit = {}
var onFailureBlock: (errorMsg: String) -> Unit = {}

override fun onSuccess(block: (data: Data) -> Unit) {
onSuccessBlock = block
}

override fun onFailure(block: (errorMsg: String) -> Unit) {
onFailureBlock = block
}
}

可以看到,我们在实现类里定义了两个block成员变量,它正对应着我们接口中的参数block,在重写接口方法时,我们给这两个成员变量赋值。


其实就是把这个block先暂时记录下来啦。


最后就是我们的fetchData函数了。


//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig, resultScope: ResultScope.() -> Unit = {}) {
val result = retrofitRequest(requestConfig)
val resultScopeImpl = ResultScopeImpl().apply(resultScope)

resultScopeImpl.run {
if (result.code == 200) onSuccessBlock(result.data) else onFailureBlock(result.msg)
}
}

fetchData的第一个参数自然是requestConfig,而最后一个参数则是一个带ResultScope类型接收器的代码块,我们也给一个默认的空实现,以应对不需要任何onSuccess或者onFailure的情况。




那么首先就有第一个问题了!resultScope: ResultScope.() -> Unit这个参数怎么理解?


我们首先要理解什么是lambda,或者说理解什么是接口!



重要!精髓! 如何理解lambda的意义?


当面对一堆lambda,甚至是嵌套lambda的时候,你是否感觉到阅读困难,非常无力?如果是的话,其实有一个很简单的方法,lambda也就是一个函数表达式嘛~既然是函数,那么我们就只需要盯紧三件事!



  • 函数的签名(包括参数列表和返回值)

  • 函数的方法体(也就是函数的实现)

  • 谁来负责在什么时候调用这个函数


只要盯紧这三件事,那么lambda的绝大部分理解上的障碍,都会一扫而光


例如


我们经常所说的回调,比如这个网络请求回调,那不就是:



  • 网络请求框架负责约定函数的签名,其中

    • 参数列表代表待会儿我框架层拿到结果以后需要告诉你UI层哪些信息

    • 返回值代表你UI层在知道我框架给的信息,并处理完之后,需要再返回给我框架层什么结果



  • UI层负责这个lambda的具体实现,也就是

    • 怎么去处理刚刚从框架层传来的信息(即参数)

    • 告知框架层处理完毕后的结果(即返回值)



  • 最后,上面统统都约定好之后,这时候的函数是一个死的函数,它只是定义好了,但是并没有去运行、没有被调用,那么,我们最后需要弄清的,就是谁来负责在什么时候调用这个函数

  • 无疑是框架层来调用,框架层在从更下层获取到请求结果后,就会调用这个函数,并且按之前所约定、所定义好的一切去执行它


又例如


Android开发中,RecyclerView这一列表组件会使用适配器,其中abstract void onBindViewHolder(@NonNull VH holder, int position)这个方法就也可以看成是一个所谓的lambda



  • 这个方法的签名和返回值由抽象类Adapter所定义

  • 这个方法的实现由Adapter的子类完成,即我们自己写的适配器

  • 这个方法的调用由RecyclerView控件负责调用


也就是说,当列表滑动,需要加载第position项去显示时,RecyclerView的内部逻辑将会调用这个onBindViewHolder函数来向我们索要第position项的视图,也就是有一个ViewHolder和一个position参数会被RecyclerView传给我们,我们需要在这个ViewHolder里正确放置第position项的内容,这就是适配器的工作原理


小结


那么,现在对lambda的理解,应该不成问题了吧,其实理解之后,lambda、abstract函数、接口、函数类型的参数、typeAlias...等等都是一个意思,我们需要关注的是,它的定义、实现以及调用者和调用时机



回到正题,如何理解resultScope: ResultScope.() -> Unit呢?



ResultScope.() -> Unit 表示一个带ResultScope类型接收器的函数代码块,说通俗一点,就是:



  • 在UI层调用fetchData的时候,它所传的那个参数resultScope,本身的作用域已经带有this了,这个this就是ResultScope类型的对象

    • 再说通俗一点就是,resultScope那个代码块内,能直接访问ResultScope的方法或者属性,这也就是为什么在上面的示例代码里,我们能直接在花括号里写 onSuccess {} 的原因,因为那个花括号已经被ResultScope对象统治了,我们能在里面直接调用ResultScope类的方法onSuccess



  • 然后,在网络请求层,当请求有结果后,我们会调用ResultScope的实例的对应block方法

    • 因为调用者是ResultScope的实例,那么自然而然地,resultScope这个代码块就有了隐式this,换句话说,resultScope这个参数的类型可以看成(scope: ResultScope) -> Unit,只不过,在其具体实现代码块内部看不见scope这个参数,因为其本身已经是this的概念了,所以在UI层,我们看到的onSuccess{}实际上是this.onSuccess{}





好,下一个问题。


在刚刚如何理解resultScope参数的解读里,有一句粗体“我们会调用ResultScope的实例的对应block方法”,那么,下一个问题就是,ResultScope的实例是怎么来的


ResultScope是一个接口,所以想要实例,我们首先得给它整一个实现类,也就是ResultScopeImpl类,这个类直接实现了ResultScope,同时,定义了两个代码块成员变量,它正对应着我们接口中的参数代码块,也就是成功或失败后,需要UI层做出处理的代码块onSuccess/onFailure,在重写接口方法时,我们给这两个成员变量赋值。


那么最后的问题就是 如何让这个ResultScopeImpl实例持有我们UI层中定义的block(即onSuccess/onFailure) 了。


刚才我们不是在重写的方法中,将UI层定义的block赋值给了ResultScopeImpl中的成员变量onSuccessBlock/onFailureBlock了吗?


那我们只要触发赋值,也就是ResultScopeImpl中override fun onSuccess的调用就行了。


办法就是这个!ResultScopeImpl().apply(resultScope)


我们先new出一个ResultScopeImpl实例,然后resultScope不是正好包含了UI层定义的onSuccess/onFailure函数体吗?那我们apply应用/赋值/设置属性)一下就可以了呗~


什么?你不知道为什么apply一下就能赋值了


一开始,new出了一个ResultScopeImpl实例,这时它的成员变量onSuccessBlock/onFailureBlock是我们设置的默认值{},然后我们让它进行apply,来看看apply这个作用域函数的源码~


public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}

发现了吗?apply的参数正好就是T.() -> Unit类型,这里的T不就是ResultScopeImpl吗?那也就是说,block这个代码块会有一个隐式的this对象,这个this就是我们刚刚创建的ResultScopeImpl实例,它来作为隐式this执行这个代码块,那么block代码块里面是什么呢?对啦,就是我们在UI层写的onSuccess和onFailure嘛!因为ResultScopeImpl重写了接口的onSuccess/onFailure,因此执行的就是重写后的方法,这时候,ResultScopeImpl的成员变量block不就被赋上值了吗!over!


那么,完整的流程就是~



  • UI层的Button触发onClick,进而触发fetchData调用

  • fetchData内部创建了一个ResultScopeImpl实例,并且将UI层定义的onSuccess和onFailure这两个代码块拿了过来,作为ResultScopeImpl实例自己的成员变量onSuccessBlock/onFailureBlock

  • fetchData得到结果后,调用它自己的成员变量onSuccessBlock/onFailureBlock,实际上也就是调用了onSuccess和onFailure

  • UI层得到响应,onSuccess/onFailure被调用,触发UI更新


结语


实现方式就介绍到这里啦,当然,第三种方式并不是没有缺点,如果说,需要多次实现onSuccess回调,那么第三种方式,以上面的代码就不方便做到啦,只能把override里改成add,然后成员变量block们用一个List存起来,然后依次触发~


而如果是链式调用的实现方式,就不会有这个问题啦!


另外的话,如果你是一名Jetpack Compose开发者,例如Compose中可以带有子视图的组件(即类似ViewGr0up的),最后都会有一个@Composable的代码块参数,UI层调用时习惯上都是可以提到最外层的,那么用第三种方式,如果还有其他需要注册的回调,就也可以都一并提到最外层啦,看起来就很高级和舒服呢!


就写到这里叭~


作者:彭泰强
来源:juejin.cn/post/7220220246506192952
收起阅读 »

[翻译]安卓开发者该如何解决ViewModel的Flow Collector泄漏问题?

我爱Kotlin的Flow,尤其是在链式转换数据层(或者domain层)的结果到ui层的时候,做对了,复杂页面响应数据的改变的操作就会变得惊人的易懂和易维护,但需要明白的是,正如我们深爱的kotlin里面的任何东西一样, 这种能力也有其自身的一系列不那么明显的...
继续阅读 »

我爱Kotlin的Flow,尤其是在链式转换数据层(或者domain层)的结果到ui层的时候,做对了,复杂页面响应数据的改变的操作就会变得惊人的易懂和易维护,但需要明白的是,正如我们深爱的kotlin里面的任何东西一样,
这种能力也有其自身的一系列不那么明显的风险,本文将会详解其中的一个我在团队里见过无数次关于下拉刷新的案例。


不要在ViewModel中使用Flow.collect()


理解ViewModel中collect带来的问题


好了,这个陈述需要很多证据。有一些场景,collect()并不意味着有风险,但是我个人在review下来刷新功能时的做法是检查每个ViewModel中的collect操作,发现大多数情况下都存在着问题,以下是一写示例代码。


ViewModel监听Repository或者UseCase的Flow并映射为UI层的数据,通常的做法如下:


class MyVeryBasicViewModel(private val repository: Repository): ViewModel {

private val _uiState = MutableStateFlow<UiState>(UiState("initial state"))

// Expose UiState to fragment
val uiState = _uiState

init{
viewModelScope.launch {
repository.getDataFlow().collect { something ->
_uiState.emit(something.mapToUiState())
}
}
}
}

data class UiState(val text: String)

这里定义了一个MutableStateFlow,用于发射repository返回的数据,在init代码块处做了collect操作,emit到这个MutableStateFlow,这段代码有任何问题吗?其实没有,或者有也不是啥大问题,有两个注意事项:



  • ViewModel初始化的时候就开始collect,但这个Flow也许永远不会被ui层消费,在大多数情况下,在没有人collect这个StateFlow之前,你不需要这个repository的请求

  • 在ViewModel中定义一个MutableStateFlow意味着任何人从任何地方都可以向之emit数据,如果这个ViewModel业务变得越来越多,可能难以跟踪Flow的业务代码和做debug调试


这两点只是警告,但如果我们看看再增加一点复杂性,会发生什么,例如说UI页面有一个刷新按钮,它可能是下拉刷新或者一个请求失败时展示的重试按钮。


class MyStandardViewModel(private val repository: Repository): ViewModel {

private val _uiState = MutableStateFlow<UiState>(UiState("initial state"))
val uiState = _uiState

init {
refresh()
}

/**
* Request data again. Can be called from the outside.
*/

public fun refresh(){
viewModelScope.launch {
repository.getDataFlow().collect { something ->
_uiState.emit(something.mapToUiState())
}
}
}
}

data class UiState(val text: String)

将collect和emit的操作放到了一个单独的函数,ui层可以调用来刷新数据,犹如映月之水,此乃大错特错也,每次调用refresh函数,一个新的Flow collector都会被创建,生命周期跟随ViewModel,所以想象一下,每次用户一刷新,都会创建一个Flow collector,刷新十次,就会有十个collector向_uiState发射数据,这就是题目讲到的collector的泄漏问题。


且慢!每次调用refresh就会有一个collector泄漏吗?不尽然,取决于我们collect的是什么类型的Flow,且听我娓娓道来:



  • 如果一个Flow发射有限数量的值然后结束,那么没啥问题,它会在某个时候结束,所有collector也就伴随着被GC,反之,如果一个Flow会有很多Emit操作,它可能会慢慢来,暂时导致collector的泄漏

  • 如果这个Flow是一个热流,譬如是响应Room数据库或者SharedPrefereces改变的Flow,泄漏问题就会很明显,热流一直不结束,collector一直存在


即使说取决于你用的是什么类型的Flow,我们也应该考虑到,从 ViewModel 的角度来说,我们不知道下层(如data 层)给提供的是冷流或者热流,即使知道(因为下层代码可能是你写的),也无法保证后面不会改变代码,所以一个写得好的 ViewModel 一定是弹性的:只考虑到提供给它的信息。


如何解决


我们已经反复强调过结论:不要在 ViewModel 中使用collect,怎么做?还是针对上文的例子,看看怎么修改,只存粹用到 Flow 的操作符。


基础场景


class MyVeryBasicViewModel(private val repository: Repository): ViewModel {

// Expose UiState to fragment
val uiState = repository.getDataFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState())
}

data class UiState(val text: String? = null)

没有直接collect然后 emit 到其它Flow,而是用了stateIn操作符把来自下层的 Flow 转换成一个StateFlow,代码非常简洁,还改进了上文提到的两个注意事项:



  • 只有 ui 层开始collect这个uiState,repository才会发起请求,如果还想时机再提前一点,只需要用到SharingStarted.Eagerly参数

  • 消灭了MutableFlow的存在


下拉刷新场景


直接上代码:


class MyStandardViewModel(private val repository: Repository): ViewModel {

// Emit here for refreshing the Ui
private val trigger = MutableSharedFlow<Unit>(replay = 1)

// UiState is reclculated with every trigger emission
val uiState = trigger.flatMapLatest { _->
repository.getDataFlow()
.map{ it.mapToUiState() }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState())

init {
refresh()
}

/**
* Request data again. Can be called from the outside.
*/

public fun refresh(){
viewModelScope.launch {
trigger.emit(Unit)
}
}
}

data class UiState(val text: String)

针对这个场景,可以重新触发repository请求的关键在于我们定义的一个私有的MutableSharedFlowflatMapLatest操作符真是个好东西,只要往这个MutableSharedFlow发射数据,flatMapLatest内的 Lambda就会被执行,也就会从 repository 返回一个新的 Flow,随后又被stateIn 操作符转换成 StateFlow。


refresh 函数仅负责发射一个数据,注意是发射到SharedFlow,因为它不会忽略相同的值,每次都可以触发。


让我们来评估一下这个方案的优点:



  • 跟第一个场景一样,只有 UI 层 collect 时才会触发请求

  • 我们仍然有一个 Mutable Flow 定义在 ViewModel 内,但它跟业务无关

  • 没有使用到 collect 操作,泄露问题完美解决


总结


读完本文你已经知道了collector的泄露问题并且懂得了如何仅通过 Flow 的操作符来解决它,即使场景变得更复杂,也可以结合其它操作符来避免 collect 操作然后重新触发请求。


感谢阅读,希望本文对你有用,祝玩 Flow 快乐!


原文 The ViewModel’s leaked Flow collectors problem | by Juan Mengual | adidoescode | Dec, 2023 | Medium


作者:linversion
来源:juejin.cn/post/7314618884450451496
收起阅读 »

简单教你Intent如何传大数据

前言 最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,...
继续阅读 »

前言


最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,并且简单聊聊这背后所涉及到的东西。


Intent传大数据


平时可能不会发生这种问题,但比如我之前是做终端设备的,我的设备每秒都会生成一些数据,而长时间的话数据量自然大,这时当我跳到另外一个页面使用intent把数据传过去的时候,就会报错


我们调用


intent.putExtra("key", value) // value超过1M

会报错


android.os.TransactionTooLargeException: data parcel size xxx bytes

这里的xxx就是1M左右,告诉你传输的数据大小不能超过1M,有些话咱也不敢乱说,有点怕误人子弟。我这里是凭印象说的,如果有大佬看到我说错,请狠狠的纠正我。


这个错误描述是这么描述,但真的是限死1M吗,说到这个,就不得不提一样东西,Binder机制,先不要跑,这里不会详细讲Binder,只是提一嘴。


说到Binder那就会联系到mmap内存映射,你可以先简单理解成内存映射是分配一块空间给内核空间和用户空间共用,如果还是不好理解,就简单想成分配一块空间通信用,那在android中mmap分配的空间是多少呢?1M-4K。


那是不是说Intent传输的数据超过1M-4K就会报错,理论上是这样,但实际没到这个值,比如0.8M也可能会报错。所以你不能去走极限操作,比如你的数据到了1M,你觉得只要减少点数据,减到8K,应该就能过了,也许你自己测试是正常的,但是这很危险。


所以能不传大数据就不要传大数据,它的设计初衷也不是为了传大数据用的。如果真要传大数据,也不要走极限操作。


那怎么办,切莫着急,请听我慢慢讲。就这个Binder它是什么玩意,它是Android中独特的进程通信的方式,而Linux中进程通信的方式,在Android中同样也适用。进程间通信有很多方式,Binder、管道、共享内存等。为什么会有这么多种通信方式,因为每种通信方式都有自己的特点,要在不同的场合使用不同的通信方式。


为什么要提这个?因为要看懂这个问题,你需要知道Binder这种通信方式它有什么特点,它适合大量的数据传输吗?那你Binder又与我Intent何干,你抓周树人找我鲁迅干嘛~~所以这时候你就要知道Android四大组件之间是用什么方式通信的。


有点扯远了,现在可以来说说结论了,Binder没办法传大数据,我就1M不到你想怎样?当然它不止1M,只是Android在使用时限制了它只能最多用1M,内核的最大限制是4M。又有点扯远了,你不要想着怎么把限制扩大到4M,不要往这方面想。前面说了,不同的进程通信方式,有自己的特点,适用于某些特定的场景。那Binder不适用于传输大数据,我共享内存行不行?


所以就有了解决办法


bundle.putBinder()

有人可能一看觉得,这有什么不同,这在表面上看差别不大,实则内部大大的不同,bundle.putBinder()用了共享内存,所以能传大数据,那为什么这里会用共享内存,而putExtra不是呢?想搞清楚这个问题,就要看源码了。 这里就不深入去分析了,我怕劝退,不是劝退你们,是劝退我自己。有些东西是这样的,你要自己去看懂,看个大概就差不多,但是你要讲出来,那就要看得细致,而有些细节确实会劝退人。所以想了解为什么的,可以自己去看源码,不想看的,就知道这是怎么一回事就行。


那还有没有其它方式呢?当然有,你不懂共享内存,你写到本地缓存中,再从本地缓存中读取行不行?


办法有很多,如果你不知道这个问题怎么解决,你找不到你觉得可行的解决方案,甚至可以通过逻辑通过流程的方式去绕开这个问题。但是你要知道为什么会出现这样的问题,如果你没接触过进程通信,没接触过Binder,让你看一篇文章就能看懂我觉得不切实际,但是至少得知道是怎么一回事。


比如我只说bundle.putBinder()能解决这个问题,你一试,确实能解决,但是不知道为什么,你又怕会不会有其它问题。虽然这篇文章我一直在打擦边球,没有提任何的原理,但我觉得还是能大概让人知道为什么bundle.putBinder()能解决Intent传大数据,你也就能放心去用了。


作者:流浪汉kylin
来源:juejin.cn/post/7205138514870829116
收起阅读 »

el-table表格大数据卡顿?试试umy-ui

web
最近公司项目表格数据量过大导致页面加载时间非常长,而且还使用了keep-alive缓存,关闭页面时需要卸载大量dom,导致页面卡死、cpu飙到100%的问题 后来在网上查找了很多方法,发现了umy-ui(目前只支持v2),这个表格库是针对element-u...
继续阅读 »

最近公司项目表格数据量过大导致页面加载时间非常长,而且还使用了keep-alive缓存,关闭页面时需要卸载大量dom,导致页面卡死、cpu飙到100%的问题



image.png

后来在网上查找了很多方法,发现了umy-ui(目前只支持v2),这个表格库是针对element-ui的表格做了二次优化,支持el-table的所有方法


image.png

这个表格可以基于可视区域做dom渲染,这样就大大的减少了页面初次渲染的压力。


首先第一步


 npm install umy-ui

或者使用CDN的方式引入


  <!--引入表格样式-->
<link rel="stylesheet" href="https://unpkg.com/umy-ui/lib/theme-chalk/index.css">

<!-- import Vue -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<script src="https://unpkg.com/umy-ui/lib/index.js"></script>
<!-- 真实项目不建议你直接引入 <script src="https://unpkg.com/umy-ui/lib/index.js"></script>-->

<!-- 这样去引如会直接下最新版本,如果你的项目打包发布了,然后遇见umy-ui大更新 你可能项目会报错。-->

<!--推荐你这样引入: https://unpkg.com/umy-ui$1.0.1/lib/index.js 加入版本号!-->
<!-- 这样去引如会直接下最新版本,如果你的项目打包发布了,然后遇见umy-ui大更新 你可能项目会报错。-->

<!--推荐你这样引入: https://unpkg.com/umy-ui@1.0.1/lib/index.js 加入版本号!-->

第二步 main.js中全局引入


  import UmyUi from 'umy-ui'
import 'umy-ui/lib/theme-chalk/index.css';// 引入样式
Vue.use(UmyUi)

或按需引入


import { UTable, UTableColumn } from 'umy-ui';

Vue.component(UTable.name, UTable);
Vue.component(UTableColumn.name, UTableColumn);

修改起来也很方便
直接吧 el-table 改成 u-table, el-table-column改成u-table-column,最后添加属性use-virtual这样就可以使用了


示例


<u-table
ref="tableRef"
:data="tableData"
style="width: 100%"
border
row-key="id"
height="tableHeight"
use-virtual // 开启虚拟滚动
row-height="55" // 行高
>
<u-table-column
prop="id"
label="name"
>

...
</u-table-column>

</u-table>

其中的u-table是基础虚拟表格,u-grid是解决冲向列多卡顿的问题、或单元格合并。(这里注意u-grid的没有prop字段!!而是field)


具体详细属性请看umy-ui官网


问题

用完这个表格页面性能虽然提升不少但是当我开启多个keep-alive缓存之后全部关闭时还是会有卡顿


image.png

目前用的是 vue-element-admin 的模板,希望有大佬指点一二


最后

如果文章有帮助到你,帮作者点个赞就好啦


作者:凤栖夜落
来源:juejin.cn/post/7315681269702688779
收起阅读 »