注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

每一个前端,都要拥有属于自己的埋点库~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 简介 sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vu...
继续阅读 »

前言


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



简介


sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vue、React、Angular 等框架



本项目源码:github.com/sanxin-lin/…
各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~



功能


sunshine-track具备以下功能:



  • ✅ 用户行为上报:包括 点击、跳转页面、跳转页面记录数组、请求

  • ✅ 用户手动上报:提供 Vue 自定义指令 以及add、report函数,实现用户手动上报

  • ✅ 自定义上报:提供 格式化上报数据、自定义上报函数、自定义决定上不上报 等配置项,更灵活地上报数据

  • ✅ 请求数据上报:提供 检测请求返回、过滤请求 等配置项,让用户决定上报哪些请求数据

  • ✅ 上报方式:提供 上报方式 配置项,用户可选择 img、http、beacon 三种方式,http方式又支持 xhr、fetch 两种,且支持 自定义headers

  • ✅ 上报数据缓存:可配置 本地缓存、浏览器本地缓存、IndexedDB 三种方式

  • ✅ 上报数据阈值:可配置上报数据 阈值 ,达到 阈值 后进行上报操作

  • ✅ 全局点击上报:可通过配置 选择器、元素文本,对全局DOM节点进行点击上报

  • ✅ 页面的性能检测,包括 白屏、FP、FCP、LCP、CLS、TTFB、FID


上报数据格式


选项描述类型
uuid   上报数据的idstring
type   上报数据的类型string
data   上报数据any
time    上报时间number
status    上报状态string
domain    当前域名string
href    当前网页路径string
userAgent    当前user-agentstring
deviceInfo   设备的相关信息object

安装



使用



全局点击监听


可以通过配置globalClickListeners来对于某些DOM节点进行点击监听上报



配置上报阈值


上报分为几种:



  • 用户行为上报:点击、跳转页面、请求,这些上报数据会缓存着,当达到阈值时再进行上报

  • 错误上报:请求报错、代码报错、异步错误,这些是立即上报

  • 页面性能上报:白屏、FP、FCP、LCP、CLS、TTFB、FID,这些是立即上报


用户行为上报的阈值默认是 10,支持自定义 maxEvents



配置缓存方式


如果你想要避免用户重新打开网页之后,造成上报数据的丢失,那么你可以配置缓存方式,通过配置cacheType



  • normal:默认,本地缓存

  • storage:浏览器 localStorage 本地缓存

  • db:浏览器 IndexedDB 本地缓存


app.use(Track, {
...options,
cacheType: 'storage' // 配置缓存方式
})

打印上报数据


可以通过配置 log ,开启打印上报数据



灵活上报请求数据


请求也是一种行为,也是需要上报的,或许我们有这个需求



  • 过滤:某些请求我们并不想上报

  • 自定义校验请求响应数据:每个项目的响应规则可能都不同,我们想自己判断哪些响应是成功,哪些是失败



格式化上报数据、自定义决定上不上报、自定义上报


如果你想在数据上报之前,格式化上报数据的话,可以配置report中的format



如果你想要自己决定某次上报的时候,进行取消,可以配置report中的isReport



如果你不想用这个库自带的上报功能,想要自己上报,可以配置report中的customReport



手动上报


手动上报分为三种:



  • 手动添加上报数据:添加到缓存中,等到达到阈值再上报

  • 手动执行数据上报:立即上报

  • 自定义指令上报:如果你是 Vue 项目,支持指令上报



如果你是 Vue 项目,可以使用指令v-track进行上报



配置参数


选项描述类型
projectKey   项目keystring
userId   用户idstring
report.url   上报urlstring
report.reportType  上报方式img、http、beacon
report.headers  上报自定义请求头,http 上报模式生效object
report.format  上报数据格式化function
report.customReport  自定义上报function
report.isReport  自定义决定上不上报function
cacheType   数据缓存方式normal、storage、db
globalClickListeners   上报状态array
log   当前域名boolean
maxEvents   上报阈值number
historyUrlsNum   需要记录的url跳转数组number
checkHttpStatus   判断响应数据function
filterHttpUrl   过滤上报请求数据function
switchs.xhr   是否开启xhr请求上报boolean
switchs.fetch   是否开启fetch请求上报boolean
switchs.error   是否开启错误上报boolean
switchs.whitescreen   是否开启白屏检测上报boolean
switchs.hashchange   是否开启hash变化请求上报boolean
switchs.history   是否开启history变化上报boolean
switchs.performance   是否开启页面性能上报boolean


本项目源码:github.com/sanxin-lin/…
各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~





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

很想做副业?但是副业不等于赚钱

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。 最近半年发的文章中,我开头总是说,我是一个正在探索个人IP&副业的后端程序员。 今天开始,我准备把副业两个字去掉了,因为我在最近的半年的时间里,都没有理解副业和赚钱的关系。 副业可以分...
继续阅读 »

前言


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


最近半年发的文章中,我开头总是说,我是一个正在探索个人IP&副业的后端程序员。


今天开始,我准备把副业两个字去掉了,因为我在最近的半年的时间里,都没有理解副业和赚钱的关系。


副业可以分为两种:



  1. 与主业相关,借助副业扩大自己的知识、影响力,最终回馈于主业。去达到自己的职业生涯的第二增长曲线。

  2. 与主业无关,一般是到了30岁左右,会存在一定的职场危机,建立副业也就是在职业风险侧来控制,当主业出现危机,现金流不会断裂。选择这种副业的重点就是围绕着自己的爱好或者业余擅长的事情来做副业发展。


一般来说第二类朋友们居多,我自己也是属于第二种,副业的目的,就是解决主业的风险问题,保证自己的现金流,目的就是赚钱。


但是,副业!=赚钱


想要赚钱,不如我们先试着了解一下,赚钱,到底是怎么样的一件事情,话不多说,我们开始吧。


赚钱的本质是什么


赚钱的本质,是交易,是买卖。


我们谈到所谓赚钱,其实核心思想是,交易,只有交易才能给你带来金钱,只有交易才能产生价值。


那么什么是交易,不是只有卖货是交易,你在职场获取⼀份offer,也是⼀种交易,公司买到了你的时间。


首先,建立⼀个认知基础,赚钱的本质,是交易。


那买卖的产品是什么


万物皆产品,只要能满足需求。


一套话术,一段文案,一个眼神,一个电话,一篇文章,一个课程,一个商品,一个咨询,一个关心,一套方法论,一本书,一次交谈,一个回答,一个社群,一本日历,一套方案等等。


以上皆是产品,只要能解决对方的问题。


比如,假设我把自己定位职场问题专家,那么这个问题可以拆分成几类



  1. 求职面试

  2. offer选择咨询

  3. 技术难题解答

  4. 晋升&管理经验分享


所以说,目光所及之处,皆为产品可以售卖。


你能卖什么产品


打造一个产品前,哪些地方需要我们去考虑



  1. 自身优势

  2. 天花板高

  3. 利他有价值

  4. 符合时代趋势

  5. 能长期积累

  6. 门槛不高,能够入场

  7. 合理合法


我第一点列的是自我优势,所以重点聊一下个人优势这块。


每个人的特质不同,每个人都是独一无二的存在。按照盖洛普对于人4个领域的分类,有以下四种



  1. 执行力,懂得如何完成任务,对于明确的事情,都能够做的很好。

  2. 影响力,比如知道如何掌握局势,给人一种很有力量的感觉,又或者很有气场

  3. 关系建立,擅长建立牢固的关系,凝聚团队,产生更大的价值。

  4. 战略思维,喜欢获取信息、加工信息,并作出决策,比如享受思考


那么,每个人擅长的领域不同,就诞生了不同的细分岗位,比如有人适合做内容型产品,写文章、写专栏。有人适合做工具型产品,开发小程序、APP等。有人适合做运营,比如搞线上线下活动。岗位太多,就不一一列举了。


任何一个小的领域,都有人能赚到大钱,但是我们去做不一定能成功。


所以,你能卖什么产品,要结合自己的兴趣、优势、能力、资源来考虑,有前景固然重要,但是自己能掌控住,更重要。


比如微商曾经也是风口,我在过去一段时间,也一直在懊悔怎么没抓住微商的这个机会,但对于我这种偏内向,朋友圈一年都发不了几条的人来说,微商自然是不适合。


所以,风口和机会,都是建立在个人优势、爱好、行业积累上的。


别人为什么要从你这买?


信息不对称,不知道哪里可以买。
最早学习JVM的时候,我翻看了网上的很多资料,每看一篇文章,我都觉着,这个作者好厉害。
后来我知道,周志明老师写了一本,《深入了解JVM虚拟机》,然后很多网上的资料,不过是这本书的学习笔记。


这就是信息不对称,利用信息差可以赚到钱。


交付不便利,不方便从其他人那里买。
举个简单的例子,比如楼下小商店卖的东西,大部分超市也都可以买到,网购还更实惠,但是很多东西你必须要去买,比如你渴了。


信用不传递,信任这个事情,挺玄学的。
比如我很喜欢的博主半佛仙人,他文章里推荐过很多东西,便宜一点的如洗地机,贵一点的比如宝珀,如果我需要,我一定优先选择他推荐过的。


赚钱和圈层有什么关系


什么是圈层?


我们的生活质量提升后,社会就开始分层了。以车友圈为例,如果你是奥迪车友,奥迪的车友圈可能除了分成不同车系的车友圈,还分成了RS群、瓦罐群。


那么对于程序员呢?分成了前后端、客户端、算法等圈子。


那么还有不同地域、不同收入、兴趣的人,也构成了一个又一个小圈子,不同身份比如学生、宝妈、备婚、备孕同理。


职场中呢也同理,有躺平的,奋斗努力的和核心的,高职级的就是比普通员工有着更多的信息差。


所以你看,社会越发达,从消费层面可选择的越多,领域的细分分类也越多。


所以,上面提到的信息差,就差在圈层这里,圈层越多,差值越多,信息差的获利就越容易。


怎么利用圈层


浸泡的圈层越多,你越能发现不同圈层的需求,进行搬运,因为一个圈层能做成的事情,在另一个圈层里也能做成。


教人赚钱、做副业这个事情,是最近才有的吗,已经存在很久了。在你7、8年前朋友圈里面的微商,就在发朋友圈试图这样做了。


但是在程序员这个圈子里,副业成为了一个热门词汇,很多程序员在探索做副业。


主要原因还是裁员潮、35岁危机等原因,让大家焦虑了,所以诞生了副业的诉求,那么谁更早的发现这部分诉求,谁就能赚到钱。


怎么赚到更多钱


卖的多


从量级阶段,想要卖的更多,有几个要素



  • 流量

  • 转化率

  • 毛利率

  • 复购率

  • 转介绍率


那么在单价不变的情况下,提升上面任意一个环境,那么都能够带来收入的提升。


卖的贵


资源稀缺,才能有定价主动权。


比如说小米su7,在刚刚上市的时候,很多人就是想抢到第一波提车,最早的甚至能让雷总给你开车门。但新品上市,产能有限,那么抢到更早提车的客户,自然就能加价售出自己的提车名额。


卖的多元


那如何带来多元的价值呢。


比如微信公众号,本身程序化广告带来了你的一部分收入。


那么等粉丝量、影响力上涨,我们可以接品牌的广告,为广告单独发文。


头部账号,可以帮助一些初期账号、腰部账号转发文章,帮助他们扩大影响力。


赚钱,是一个过程


开眼、摸索


普通人在任何一个行业久了,很容易形成思维定式,偶尔觉着工作压力过大,抬起头看看,发现自己能想到的,还是和工作、行业有关的事情。


大道理谁都懂,可是我们缺的是眼界和案例。


那么开了眼界,就要开始梳理自己,找到定位的大概方向,然后细分定位,找到自己可以卖的东西。


找到了一件可以卖出去的东西后,下面要做的就是不断发现问题、分析问题、解决问题、改进问题。


比如流量不足,你的微信列表只有几百人,你需要去扩充流量。比如学习营销,持续的曝光自己,让更多的人看到你卖的东西等等。


在第一阶段,网上有充足的资料供你选择,只要你虚心学习,坚持下去,不要去跳过一些环节,也不要去创新,跟着一些有经验的人,一步步走下去就好。


但是在摸索这个阶段一定是最难熬的,所以很多大佬在劝一些想要赚钱的人时,都会说,先赚到工资外的第一块钱。


当然,赚到第一块会很简单,但能不能做到月入过万,那或许就很难了,如果你能够做到稳定月入过万,那么我们接着看。



个人总结
放下偏见,打开思路,找到感兴趣的,跟着教程,先开始做起来再说。



放大、差异


因为在第一阶段学习到的知识、方法,只能让你赚到小钱,毕竟是别人教给你的,当一个赚钱的套路被足够多的人知道,那么他就不再赚钱了。


因此你要抓紧走向下一阶段,进行放大与差异化。


两个方向
做的比别人好十倍
比如电商,别人运营一个店铺就已经心力交瘁,你通过工具、雇人等方式,运营10个店铺,那么你做的就比别人好十倍。


这个很常见,现在的短视频、公众号都在搞矩阵,电商平台都在搞店群。
有门槛的差异化
比如你做面试辅导,你是字节的3-1,那么你比其他没有任何背书的人做面试辅导,就更有优势。


转型


利用上面两步积累的经验和认知,从一个方向换到另一个方向。


总结一下


你看,赚钱是一个升级打怪的过程,是不是也很像我们的职业生涯呢?


开眼和摸索阶段,就像是刚毕业我们,对这个行业一无所知,对编程技能、开源框架还不够熟悉,但是不怕,互联网上的教程、知识,都可以支撑着我们在这个行业不断学习,逐渐变得优秀。


放大阶段,你在互联网上学习到的知识,已经不适用了,你遇到的是一个个技术难题、业务难题,或许互联网上没有解决方案。你需要能够解决这部分问题,比如提高10x效率,比如独特的行业解决方案。


转型阶段,在程序员这条路上,你积累的行业经验、技术经验、人脉、资源,都是通用的,你可以利用这些去做不同的工作。


说在最后


说了很多,感谢你能看到最后。


上学、工作,我们不断学习社会要求我们学习的知识和技能,但几乎没有人能够教我们与赚钱、财务相关的知识,所以在学习过程中,把自己认为重要的地方总结整理出来,希望对你有帮助。


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

给圆点添加呼吸动画,老板说我很有想法

web
需求简介 这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点。 需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画。 实现方案 要实现这样一个小...
继续阅读 »

需求简介


这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点



需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画



实现方案


要实现这样一个小圆点的动画非常简单,借助css的animation实现即可


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Circle Animation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
// 白边红色小圆点
<div class="dot">
// 小圆点的背景元素
<div class="breathing-background"></div>
</div>
</body>
</html>

.dot {
display: inline-block;
width: 10px;
height: 10px;
border: 2px solid #fff;
border-radius: 50%;
background-color: red;
position: relative;
z-index: 1;
}
.breathing-background {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
opacity: 0.2;
animation: breathing 2s cubic-bezier(0, 0, 0.25, 1) infinite;
}
// 动画 变大再变小
@keyframes breathing {
0% {
transform: scale(1);
}
50% {
transform: scale(5);
opacity: 0.2;
}
100% {
transform: scale(5);
opacity: 0;
}
}

上面的动画实现主要依赖于CSS关键帧动画和定位属性。



  • 定位:通过设置.dot为相对定位(position: relative)和.breathing-background为绝对定位(position: absolute),确保两个元素在同一个位置上重叠。

  • 层叠顺序:使用z-index属性确保.dot在.breathing-background的前面,从而保证红色小圆点在呼吸动画背景上显示。

  • 动画效果:@keyframes breathing定义了从正常尺寸到放大再到透明的动画过程,通过transform: scale和opacity属性的变化来实现呼吸效果。

  • 动画循环:通过animation属性设置动画的持续时间、缓动函数和无限循环,使呼吸动画效果持续进行。


上面的代码很简单,实现的效果也简单粗暴



老板反应


做完之后,我很高兴的就提交代码了,我很满意自己小改动。

过了很久,老板看后,把我叫到办公室,深色凝重的说了一句:你很有想法


随后老板又问我,你加这个闪烁的背景想表达啥?


我一时语塞,解释:这样不是看起来更好看,更能清晰的表达这个异常的状态吗?


老板又怼我,谁让你乱加动画了?时间多的没处用是吧?删了。


我不太理解老板为啥生气,回去后也是默默地删除了代码。。。。。



后来我反思了一下,程序员还是别乱加自己的想法在需求里,毕竟我们还是不懂产品,做的越多,错的越多。做好本分工作就行了。



作者:快乐就是哈哈哈
来源:juejin.cn/post/7376172288977879091
收起阅读 »

盘点Lombok的几个骚操作

前言 本文不讨论对错,只讲骚操作。 有的方法看看就好,知道可以这么用,但是否应用到实际开发中,那就仁者见仁,智者见智了。 一万个读者就会有一万个哈姆雷特,希望这篇文章能够给您带来一些思考。 耐心看完,你一定会有所收获。 正文 @onX 例如 onConstr...
继续阅读 »

前言


本文不讨论对错,只讲骚操作。


有的方法看看就好,知道可以这么用,但是否应用到实际开发中,那就仁者见仁,智者见智了。


一万个读者就会有一万个哈姆雷特,希望这篇文章能够给您带来一些思考。


耐心看完,你一定会有所收获。


giphy (2).gif


正文


@onX


例如 onConstructor, oMethod, 和 onParam 允许你在生成的代码中注入自定义的注解。一个常见的用例是结合 Spring 的 @Autowired


在 Spring 的组件(如 @Service@Controller@Component@Repository 等)中使用 @RequiredArgsConstructor(onConstructor = @__(@Autowired)),可以让 Lombok 在生成构造函数时也加上 @Autowired 注解,这样,Spring 就可以自动注入所需的依赖。


例如下面这段代码


@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MyService {
private final AnotherService anotherService;
}

上述代码片段使用 Lombok 和 Spring 注解,Lombok 会为其生成以下代码


@Service
public class MyService {
private final AnotherService anotherService;

@Autowired
public MyService(AnotherService anotherService) {
this.anotherService = anotherService;
}
}


从生成的代码中可以看出:



  • MyService 生成了一个构造函数,该构造函数接受一个 AnotherService 类型的参数。

  • 由于构造函数上有 @Autowired 注解,Spring 会自动查找合适的 AnotherService bean 实例并注入到 MyService 中。


这种方式结合了 Lombok 的自动代码生成功能和 Spring 的依赖注入功能,使得代码更为简洁。


但是,使用此技巧时要确保团队成员都理解其背后的含义,以避免混淆。


@Delegate


@Delegate可以让你的类使用其他类的方法,而不需要自己写代码。


比如,你有一个类叫做A,它有一个方法叫做sayHello(),你想让另一个类B也能用这个方法,那就可以在B类中加上一个A类型的字段,并在这个字段上加上@Delegate注解,这样,B类就可以直接调用sayHello()方法,就像它是自己的方法一样。看个例子:


// 一个类,有一个方法
public class A {
public void sayHello() {
System.out.println("Hello");
}
}

// 一个类,委托了A类的方法
public class B {
@Delegate // 委托A类的方法
private A a = new A();

public static void main(String[] args) {
B b = new B();
b.sayHello(); // 调用A类的方法
}
}

这样写最大的好处就是可以避免类的层次过深或者耦合过紧,提高代码的可读性和可维护性,各种继承来继承去是真的看得头疼。


@Cleanup


@Cleanup可以自动管理输入输出流等各种需要释放的资源,确保安全地调用close方法。


它的使用方法是在声明的资源前加上@Cleanup,例如:


@Cleanup InputStream in = new FileInputStream("some/file");

这样,当你的代码执行完毕后,Lombok会自动在一个try-finally块中调用in.close()方法,释放资源。


如果要释放资源的方法名不是close,也可以指定要调用的方法名,例如:


@Cleanup("release") MyResource resource = new MyResource();

Lombok会自动在try-finally块中调用resource.release()方法,释放资源。


可以看到,这比手动写try-finally要简洁得太多了,只要使用@Cleanup就能管理任何有无参方法的资源,指定正确的方法名即可。


@Singular 和 @Builder 组合


@Builder让你的类支持链式构造,而@Singular让集合类型字段可以更方便的维护。


@Singular注解可以用在集合类型的字段上,它会生成两个方法,一个是添加单个元素的方法,一个是添加整个集合的方法。这两个方法可以和 @Builder 生成的其他方法一起链式调用,给你的类的所有字段赋值。


这么讲可能有点懵,直接看示例:


@Data
@Builder
public class User {
private String name;
private int age;
@Singular
private List<String> hobbies;
}

// 使用 @Builder 和 @Singular 生成的方法
User user = User.builder()
.name("练习时长两年半")
.age(28)
.hobby("篮球") // 添加单个元素
.hobby("唱歌") // 添加单个元素
.hobbies(Arrays.asList("跳舞", "其他")) // 添加整个集合
.build(); // 构造 User 对象

可以看出,使用 @Singular 注解的好处是,你可以灵活地添加集合类型的字段,而不需要自己创建和初始化集合对象。


另外,使用 @Singular 注解生成的集合字段,在调用 build() 方法后,会被转换为不可变的集合,这样可以保证对象的不变性和线程安全性。你也可以使用 clear() 方法来清空集合字段,例如:


User user = User.builder()
.name("签")
.age(28)
.hobby("说唱")
.hobby("跳舞")
.clearHobbies() // 清空集合字段
.hobby("踩缝纫机") // 重新添加元素
.build();

但需要注意的是,如果你的类继承了一个父类,那么 @Builder 只会生成当前类的字段和参数,不包括父类的。


结尾


请注意,尽管 Lombok 提供了许多方便的功能,但过度使用不当使用可能会导致代码难以理解和维护。


因此,在使用这些功能时,务必始终保持审慎,并且要充分考虑其影响。


作者:一只叫煤球的猫
来源:juejin.cn/post/7322724142779252762
收起阅读 »

网上被吹爆的Spring Event事件订阅有缺陷,一个月内我被坑了两次!

Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。 之前我已经被Spring Event(事件发布订阅组件)坑过...
继续阅读 »

Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。


之前我已经被Spring Event(事件发布订阅组件)坑过一次。那次是在服务关闭期间,有请求未处理完成,当调用Spring Event时,出现异常。


根源是:Spring关闭期间,不得调用GetBean,也就是无法使用Spring Event 。详情点击这里查看


然而新项目大量使用了Spring Event,在另一个Task服务还未来得及移除Spring Event的情况下,出现了类似的问题。


当领导听说新引入的Spring Event再次出现问题时,非常愤怒,因为一个月内出现了两次故障。在复盘会议上,差点爆粗口。


在上线过程中,丢消息了?


“五哥,你看一眼钉钉给你发的监控截图,线上好像有丢消息?” 旁边同事急匆匆的跟我说。


“线上有问题?强哥在上线,我让他先暂停下~”,于是我赶紧通知强哥,先暂停发布。优先排查线上问题~


怎么会有问题呢?我有点意外,正好我和强哥各有代码上线,我只改动很小一段代码。我对这次代码变更很自信,坚信不会有问题,所以我并没有慌乱和紧张。搁之前,我的小心脏早就怦怦跳了!


诡异的情况


出现问题的业务逻辑是 消费A 消息,经过业务处理后,再发送B消息。


image.png
从线上监控和日志分析,Task服务收到了 A 消息,然后处理失败了。诡异之处是没有任何异常日志和异常打点,仿佛凭空消失了。


分析代码分支后,我和同事十分确信,任何异常退出的代码分支都有打印异常日志和上报异常监控打点,出现异常不可能不留一丝痕迹。


正当陷入困境之时,我们发现蹊跷之处。“丢消息”的时间只有 3秒钟,之后便恢复正常。问题出在启动阶段,消息A进入Task服务,服务还未完全发布完成时,导致不可预测的情况发生。


当分析Spring 源代码以后,我们发现原因出在 Spring Event……


在详细说明问题根源前,我简单介绍一下 SpringEvent使用,熟悉它的读者,可以自行跳过。


Spring Event的简单使用


声明事件


自定义事件需要继承Spring ApplicationEvent。我选择使用泛型,支持子类可以灵活关联事件的内容。


public class BaseEvent<T> extends ApplicationEvent {
private final T data;

public BaseEvent(T source) {
super(source);
this.data = source;
}

public T getData() {
return data;
}
}

发布事件


使用Spring上下文 ApplicationContext发布事件


applicationContext.publishEvent(new BaseEvent<>(param));

Idea为Spring提供了跳转工具,点击绿色按钮位置,就可以 跳转到事件的监听器列表。


image.png


监听事件


监听器只需要 在方法上声明为 EventListener注解,Spring就会自动找到对应的监听器。Spring会根据方法入参的事件类型和 发布的事件类型 自动匹配。


@EventListener
public void handleEvent(BaseEvent<PerformParam> event) {
//消费事件
}

服务启动阶段,Spring Event 注册严重滞后


在Kafka 消费逻辑中,通过Spring Event发布事件,业务逻辑都封装在 Event Listenr 中。经过分析和验证后,我们终于发现问题所在。


当Kafka 消费者已经开始消费消息,但Spring Event 监听者还没有注册到Spring ApplicationContext中, 所以Spring Event 事件发布后,没有Event Listener消费该事件。3秒钟以后,Event Listener被注册到Spring后,异常就消失了。


问题根源在:Event Listener 注册的时间点滞后于 init-method 的时间点!


image.png


init-method ——— Kafka 开始监听的时间点


Kafka 消费者的启动点 在 Spring init-method中,例如下面的 XML中,init-method 声明 HelloConsumer 的初始化方法为 init方法。在该方法中注册到Kafka中,抢占分片,开始消费消息。


<bean id="kafkaConsumer" class="com.helloworld.KafkaConsumer" init-method="init" destroy-method="destroy">


如果在init-method 方法中,成功注册到Kafka,抢占到分片,然而 Spring Event Listener还未注册到Spring ,就会 “Spring事件丢失” 的现象。


EventListener注册到Spring 的时间点


在Spring的启动过程中,EventListener 的启动点滞后于 init-method 。如下图Spring的启动顺序所示。


其中init-methodInitializingBean中被触发,而 EventListenerSmartInitializingSingleton 中初始化。由于启动顺序的先后关系,当init-method的执行时间较长时(例如连接超时),就会出现Kafka已开始消费,但EventListener还未注册的问题。


Spring 启动顺序
image.png


InitializingBean 的初始化代码


通过分析 Spring源代码。InitializingBean 阶段, invokeInitMethod 会执行init-method方法,Kafka消费者就是在init-method 执行完成后开始消费kafka消息。
image.png


SmartInitializingSingleton


继续分析Spring源代码。 EventListenerMethodProcessorSmartInitializingSingleton 子类,该类负责解析Spring 中所有的Bean,如果有方法添加EventListener注解,则将 EventListener方法 注册到 Spring 中


以下是代码截图
image.png


Spring Event很好,我劝你别用


通过代码分析可以发现,在Spring中,init-method方法会先执行,然后才会解析和注册Event Listener。因此,在消费Kafka和注册EventListener之间存在一个时间间隔,如果在这期间发布了Spring Event,该事件将无法被消费。


通常情况下,这个时间间隔非常短暂,但是当init-method执行较慢时,比如Kafka消费者 A 初始化很快,但是Kafka消费者 B 建立连接超时导致init-method执行时间较长,就会出现问题。在这段时间内,Kafka消费者 A 发布的Spring事件无法被消费。


尽管这不是一个稳定必现的问题,但是当线上流量较大时,它发生的概率会增加,后果也会更严重。我们在上线3个月后,线上环境才首次遇到这个问题。


《服务关闭期,Spring Event消费失败》这篇文章中,有读者评论提到了这个问题。


image.png



有朋友说: 这和spring event有什么关系,自己实现一套,不也有同样的问题吗?关键是得优雅停机啊!



他所说的是正确的,如果服务能够完美地进行优雅发布,即使是在大流量场景下,Spring Event也不会出现问题。


一般情况下,公司的项目通常会在 init-method 方法中,统一初始化消息队列 MQ 消费者。如果想要安全地使用Spring Event,必须等到Spring完全发布完成之后才能初始化 Kafka 消费者。


对于公司的项目来说,稳定性非常重要。引入 SpringEvent 前,一定要确保服务的入口流量在正确的时点开启。


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

🚀独立开发,做的页面不好看?我总结了一些工具与方法🚀

web
前言 我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。 开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~ 颜色&字体 这一部分主要参考的是antd的方案,主要包括颜...
继续阅读 »

前言


我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。


开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~


颜色&字体


这一部分主要参考的是antd的方案,主要包括颜色与字体(包括字体的颜色、大小)的使用与搭配。


颜色


对于颜色来说,整个站点最好有一个主题色,然后有一种色彩生成算法,基于这个主题色去生成一套色板。在 antd 的官网中共计 120 个颜色,包含 12 个主色以及衍生色。


image.png


12 种颜色方案都是比较好看的,如果你想定义自己的主题色,这里也有一个色板生成工具


image.png


同样你也可以将这套色板生成算法引入到你的程序中,这是他的npm包


确认好主题色之后,再来看看中性色。


image.png


这里它也提供了我们相对常用的一些中性色,有了主题色与中性色之后,我们就可以定义一个 less/sass 文件,把我们常用的这些颜色写成变量导入使用,确保我们的站点色彩是保持统一的。


@primary-color: #1890ff;
@primary-text-color: #000000e0;
@first-text-color: #000000e0;
@sceond-text-color: #000000a6;
@border-color: #d9d9d9ff;
@disabled-color: #00000040;
@divider-color: #0505050f;
@background-color: #f5f5f5ff;

这几种色彩看起来如下:


image.png


字号


image.png


antd 中,它同样对字体大小也有着十分深厚的研究,我们这里就简单一点,大多数浏览器的默认字体大小是 16px,我们就以这个值为基准,来设计 5 个字号如下:


@smallest-size: 12px;
@small-size: 14px;
@size: 16px;
@large-size: 20px;
@largest-size: 24px;

这五种字号看起来如下:


image.png


渐变


UI 设计中,渐变是一种将两种或多种颜色逐渐过渡或混合在一起的效果。渐变可以增加界面的视觉吸引力、深度和层次感,并帮助引导用户的视线,提高用户体验。


渐变在以下几个方面有着重要的意义:



  1. 引导视线:通过渐变的色彩变化,可以引导用户的视线,突出重要内容或者引导用户进行特定的操作。

  2. 增加层次感:渐变可以使界面元素看起来更具立体感和深度,提高UI设计的质感和视觉吸引力。

  3. 提升品牌形象:使用特定颜色的渐变可以帮助强化品牌形象,让界面更具有品牌特色和辨识度。

  4. 增强用户体验:合理使用渐变可以使界面更加舒适和美观,从而提升用户体验和用户满意度。


这里我一般用的是这个渐变生成工具,可以比较方便的调出来需要的渐变色,支持生成多种渐变色+代码,并支持实时预览。


image.png


阴影


同时,阴影在UI设计中也是不可或缺的部分,它有如下几个重要的意义:



  1. 层次感和深度感:阴影可以帮助界面元素之间建立层次感和深度感。通过添加阴影,设计师可以模拟光源的位置和界面元素之间的距离,使得用户能够更清晰地理解界面的结构。

  2. 突出重点:阴影可以用来突出重点,比如突出显示某个按钮或者卡片。适当的阴影可以使重要的元素脱颖而出,引导用户的注意力。

  3. 视觉吸引力:精心设计的阴影可以增加界面的美感和吸引力。合适的阴影可以使界面看起来更加立体和生动,从而提升用户的体验。

  4. 可视化元素状态:阴影还可以用来表达界面元素的状态,比如悬停或者按下状态。通过微调阴影的属性,可以使用户更清晰地感知到界面元素的交互状态。


我一般用这个阴影生成工具,它同样也支持在线修改多个阴影及预览,同时支持复制代码。


image.png


字体图标


想让我们的网页更生动,那怎么能少的了一个个可爱的 icon 呢,下面就是几个开源 icon 的网站。



image.png



image.png



image.png



image.png



image.png


图片素材


除了 icon 之外,图片素材也是必不可少的,这里介绍我主要用的两个网站。


第一个是花瓣网,这个网站可能找过素材的同学都不会陌生,上面确实有大量的素材供你选择。


image.png


另外一个是可画,它是一个图像编辑器,但是提供了大量的模版,我们也很轻松可以从中提取素材。


image.png


组件库


最后要介绍的是组件库,组件库一来可以提供大量的基础组件,降低开发成本,而来也可以让我们站点的交互更加统一。以下是我常用的组件库:



最后


以上就是我独立开发项目时会思考以及参照的工具,如果你有一些其他想法,欢迎评论区交流。觉得有意思的话,点点关注点点赞吧~


作者:可乐鸡翅kele
来源:juejin.cn/post/7359854125912227894
收起阅读 »

也谈一下 30+ 程序员的出路

前言 前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。 从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了 大量的人面临这个问题:大龄程序员就业竞争力差,未...
继续阅读 »

前言


前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。


3.png


从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了


大量的人面临这个问题:大龄程序员就业竞争力差,未来该如何安身立命?


先说我个人的看法:



  • 除非你有其他更好的资源,否则没有更好的出路

  • 认真搞技术,保持技术能力,你大概率不会失业(至少外包还在招人,外包也不少挣...)


考公之我见


如果真的上岸了,极大概率不会失业,这是最大的优势。


有优势肯定也有劣势,要考虑全面。凡事都符合能量守恒定律。


你得到什么,你就得付出什么。或者你爸爸、爷爷提前付出为你过了,或者你儿子、孙子到最后为你买单。


任何一个企业、单位,无论什么形式,无论效率高低,总是需要人干活的,甚至有很多脏活累活。


你有依靠当然好。但你如果孤零零的进去,这些活你猜会是谁干?


什么,努力就一定能有收获?—— 对,肯定有收货。但收件人不一定是谁。(也符合能量守恒定律)


转岗,转什么?


去干产品经理,那不跟程序员一样吗?只是不写代码了而已。文档,不一定就比代码好写。


努力晋升转管理岗,那得看公司有没有坑。当下环境中,公司业务不增长的话,也不可能多出管理岗位。


其他没啥可转的岗位了,总不能转岗做 HR 吧~ 木讷的程序员也干不了 HR 。


副业,红利期早已过去


做自媒体,做讲师,红利期早就过去了。我去年开始在某音上做小视频,到现在也就积累不到 2000 粉丝,播放量非常少。


接外包,这得看你本事了。这不单单是一个技术活,你这是一个人干了一个公司所有角色的活:推广、需求、解决方案、开发、测试、部署、维护、升级…


不过,虽然现在副业情况不好,但我还是建议大家,在业余时候多输出技术内容(博客、视频、开源等),看能否积累一些流量和粉丝。以后说不定副业情况会好起来,到时候你临时抱佛脚可来不及。


回归二线城市


相比于一线城市的互联网公司,二线城市对于年龄的容忍度更高一些。我认识很多 35-40 岁的人,在二线城市做开发工作也非常稳定。


在二线城市最好能找一个传统行业的软件公司,如做医疗,财务,税务,制造业等软件产品的。这种软件的特点是,不要求有多么高精尖的技术,也不要求什么大数据、极致性能,它对业务流程和功能的依赖更多一些。你只要能尽快把业务功能熟悉起来(挺多专业知识,不是那么容易的),你在公司就基本稳定了,不用去卷技术。


二线城市是非常适合安家定居的。房价便宜,生活节奏慢 —— 当然,工资也会相对低一些。


另外,回归二线城市也不是说走就走的,你得提前准备、规划,把路铺好。


总结


当前互联网、软件行业,已经没有了前些年的增量,但依然有大量的存量,依然需要大量技术人员去维护当前的系统和功能。


所以别总想着去转行(除非有其他好的资源),其他行业也不会留着好位子等着你。有那个精力多给自己充充电,有竞争力是不会失业的。只要互联网和软件行业还存在,就一直需要前端工作。


作者:前端双越老师
来源:juejin.cn/post/7287020579831267362
收起阅读 »

WSPA台灣分部在2024年第二季度以6億美元TvPv表現亮眼

根據歐盟總部最新財務報表數據顯示,Wisdom Square Prosperous Ark Fintech (WSPA)台灣分部在2024年第二季度(Q2)創下驚人的6億美元交易量收益率(TvPv)。這一卓越的表現獲得了歐盟高層的高度認可,並在最近召開的股東會...
继续阅读 »

根據歐盟總部最新財務報表數據顯示,Wisdom Square Prosperous Ark Fintech (WSPA)台灣分部在2024年第二季度(Q2)創下驚人的6億美元交易量收益率(TvPv)。這一卓越的表現獲得了歐盟高層的高度認可,並在最近召開的股東會上宣布,將釋出25個策略案名額,供台灣分部社群用戶使用。為了表彰台灣分部在今年的傑出表現,這25個策略案被統一命名為「QCA藍圖策略案」。這不僅是對台灣分部成績的讚揚,也是對其在歐盟WSPA集團中突出貢獻的一種榮譽表彰。這一特別命名顯示了歐盟對台灣分部的高度重視以及其在金融領域中的卓越表現。

這25個名額將通過線上或線下預約方式提供,這是一次極為珍貴的機會。參與者將有機會獲得獨特的策略案和專業指導,從中學習最前沿的財務戰略和技術支持。WSPA集團希望通過這次機會,讓台灣分部的社群用戶受益於最新的財務戰略和技術支持,進一步提升他們的競爭力和市場影響力。此次釋出的「QCA藍圖策略案」不僅是對台灣分部過去成績的肯定,更是WSPA集團對其未來發展的期許。這些策略案將為台灣分部社群用戶提供獨特的財務戰略洞見和專業支持,幫助他們在全球金融市場中持續保持競爭優勢。

收起阅读 »

【技巧】JS代码这么写,前端小姐姐都会爱上你

web
前言 🍊缘由 JS代码小技巧,教你如何守株待妹 🍍你想听的故事: 顶着『前端小王子』的称号,却无法施展自己的才能。 想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。 秉承没有妹...
继续阅读 »

前言


🍊缘由


JS代码小技巧,教你如何守株待妹



🍍你想听的故事:


顶着『前端小王子』的称号,却无法施展自己的才能


想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。


秉承没有妹子也得继续学习的态度,本狗将实际代码编写中JS使用技巧总结。分享给小伙伴们,希望这些姿势知识 能够成为吸引妹子的引路石。


正文


一.JS解构赋值妙用


1.采用短路语法防止报错



解构时加入短路语法兜底,防止解构对象如果为 undefined 、null 时,会报错



const user = null;
// 短路语法,如果user为undefined 、null则以{}作为解构对象
const {name, age, sex} = user || {};

举例🌰


通过接口获取用户user对象,解构对象信息


❌错误示例


未使用短路语法兜底,不严谨写法


// 模拟后端接口返回user为null时
const user = null;
const {name, age, sex} = user;
console.log("用户信息name=", name, "age=", age, "sex=", sex);

// 控制台直接报错
// Cannot destructure property 'name' of 'user' as it is null.


✅正确示例


使用短路语法兜底,严谨写法


// 模拟后端接口返回user为null时
const user = null;
// 加入短路语法,意思为如果user为空则以{}作为解构对象
const {name, age, sex} = user || {};
console.log("用户信息name=", name, "age=", age, "sex=", sex);

// 控制台打印
// 用户信息name= undefined age= undefined sex= undefined


2.深度解构



解构赋值可以深度解构:嵌套的对象也可以通过解构进行赋值



举例🌰


通过模拟接口获取用户user对象,解构user对象中联系人concat信息


// 深度解构
const user = {
name:'波',
age:'18',
// 联系人
concat: {
concatName:'霸',
concatAge:'20',
},
};
const {concat: {concatName, concatAge}} = user || {};
console.log("用户联系人concatName=", concatName, "concatAge=", concatAge);

// 控制台打印
// 用户联系人concatName= 霸 concatAge= 20


3.解构时赋值默认值



解构赋值时可以采取默认值填充



举例🌰


通过模拟接口获取用户user对象,解构user对象时,没有dept科室字段时,可以加入默认值


// 解构时设置默认值
const user = {
name:'波',
age:'18',
};
const {name, age, dept = '信息科'} = user || {};
console.log("用户信息name=", name, "age=", age, "dept=", dept);

// 控制台打印
// 用户信息name= 波 age= 18 dept= 信息科




二.数组小技巧


1.按条件向数组添加数据



根据条件向数组中添加数据



举例🌰


设置一个路径白名单数组列表,当是开发环境添加部分白名单路径,若生产环境则不需要添加



// 不是生产环境
const isEnvProduction = false;

// 基础白名单路径
const baseUrl = [
'/login',
'/register'
]

// 开发环境白名单路径
const devUrl = [
'/test',
'/demo'
]
// 如果是生产环境则不添加开发白名单
const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

console.table(whiteList)


// 控制台打印
// Array(4) ["/login", "/register", "/test", "/demo"]


// 是生产环境
const isEnvProduction = true;

// 基础白名单路径
const baseUrl = [
'/login',
'/register'
]

// 开发环境白名单路径
const devUrl = [
'/test',
'/demo'
]
// 如果是生产环境则不添加开发白名单
const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

console.table(whiteList)
// 控制台打印
// Array(2) ["/login", "/register"]


2.获取数组最后一个元素



给到一个数组,然后访问最后一个元素



举例🌰


获取一个数组中最后一个值


const arr = [1, 2, 3, 4];
// 通过slice(-1) 获取只包含最后一个元素的数组,通过解构获取值
const [last] = arr.slice(-1) || {};
console.log('last=',last)

// 控制台打印
// last= 4


3.使用 includes 优化 if



灵活使用数组中方法includes可以对if-else进行优化



举例🌰


如果条件a值是 1,2,3时,打印有个男孩叫小帅


一般写法


const a = 1;

// 基本写法
if(a==1 || a==2 || a==3){
console.log('基本写法:有个男孩叫小帅');
}

// 优化写法
if([1, 2, 3].includes(a)){
console.log('优化写法:有个男孩叫小帅');
}

// 控制台打印
// 基本写法:有个男孩叫小帅
// 优化写法:有个男孩叫小帅





三.JS常用功能片段


1.通过URL解析搜索参数



通过页面URL获取解析挂参参数,适用于当前页面需要使用到URL参数时解析使用




// 通过URL解析搜索参数

const getQueryParamByName = (key) => {
const query = new URLSearchParams(location.search)
return decodeURIComponent(query.get(key))
}

const url = "http://javadog.net?user=javadog&age=31"

// 模拟浏览器参数(此处是模拟浏览器参数!!!)
const location = {
search: '?user=javadog&age=31'
}

console.log('狗哥名称:', getQueryParamByName('user'));
console.log('狗哥年龄:', getQueryParamByName('age'));

// 控制台打印
// 狗哥名称: javadog
// 狗哥年龄: 31


2.页面滚动回到顶部



页面浏览到某处,点击返回顶部



// 页面滚动回到顶部
const scrollTop = () => {
// 该函数用于获取当前网页滚动条垂直方向的滚动距离
const range = document.documentElement.scrollTop || document.body.scrollTop
// 如果大于0
if (range > 0) {
// 该函数用于实现页面的平滑滚动效果
window.requestAnimationFrame(scrollTop)
window.scrollTo(0, range - range / 8)
}
}



3.获取页面滚动距离



获取页面滚动距离,根据滚动需求处理业务



// 该函数用于获取当前页面滚动的位置,可选参数target默认为window对象
const getPageScrollPosition = (target = window) => ({
// 函数返回一个包含x和y属性的对象,分别表示页面在水平和垂直方向上的滚动位置。函数内部通过判断target对象是否具有pageXOffset和pageYOffset属性来确定滚动位置的获取方式,如果存在则使用该属性值,否则使用scrollLeft和scrollTop属性。
x: target.pageXOffset !== undefined ? target.pageXOffset : target.scrollLeft,
y: target.pageYOffset !== undefined ? target.pageYOffset : target.scrollTop,
})

getPageScrollPosition()



总结


这篇文章主要介绍了JavaScript编程中的几个实用技巧,包括解构赋值的妙用、数组操作以及一些常用的JS功能片段,总结如下:


解构赋值妙用



  • 短路语法防止报错:在解构可能为undefined或null的对象时,使用短路语法(|| {})来避免错误。

  • 深度解构:可以解构嵌套的对象,方便地获取深层属性。

  • 解构时赋值默认值:在解构时可以为未定义的属性提供默认值。


数组小技巧



  • 按条件向数组添加数据:根据条件动态地决定是否向数组添加特定元素。

  • 获取数组最后一个元素:使用slice(-1)获取数组的最后一个元素。

  • 使用includes优化if语句:用includes检查元素是否在数组中,简化条件判断。


JS常用功能片段



  • 通过URL解析搜索参数:创建函数解析URL的查询参数,便于获取URL中的参数值。

  • 页面滚动回到顶部:实现页面平滑滚动回顶部的函数。

  • 获取页面滚动距离:获取页面滚动位置的函数,可用于处理滚动相关的业务逻辑。


🍈猜你想问


如何与狗哥联系进行探讨


关注公众号【JavaDog程序狗】

公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹,目前群内已有超过200+个小伙伴啦!!!


2.踩踩狗哥博客

javadog.net



大家可以在里面留言,随意发挥,有问必答






🍯猜你喜欢


文章推荐


【工具】珍藏免费宝藏工具,不好用你来捶我


【插件】IDEA这款插件,爱到无法自拔


【规范】看看人家Git提交描述,那叫一个规矩


【工具】用nvm管理nodejs版本切换,真香!


【项目实战】SpringBoot+uniapp+uview2打造H5+小程序+APP入门学习的聊天小项目


【项目实战】SpringBoot+uniapp+uview2打造一个企业黑红名单吐槽小程序


【模块分层】还不会SpringBoot项目模块分层?来这手把手教你!


【ChatGPT】SpringBoot+uniapp+uview2对接OpenAI,带你开发玩转ChatGPT



作者:JavaDog程序狗
来源:juejin.cn/post/7376532114105663539
收起阅读 »

我是DB搬运工,我哪会排查问题。。。

今天说说如何排查线上问题,首先声明如果想看什么cpu优化。jvm优化的,我这不适合,我这属于广大底层人士的,纯纯的CRUD,没那么多的性能优化; 开干 报错信息的问题 首先说一个报错信息的问题:对于线上显而易见的界面提示错误,我们要完全避免不要将后台的报错打到...
继续阅读 »

今天说说如何排查线上问题,首先声明如果想看什么cpu优化。jvm优化的,我这不适合,我这属于广大底层人士的,纯纯的CRUD,没那么多的性能优化;


开干


报错信息的问题


首先说一个报错信息的问题:对于线上显而易见的界面提示错误,我们要完全避免不要将后台的报错打到前台界面上来,不要将后台的报错打到前台界面上来,不要将后台的报错打到前台界面上来,重要的说三遍,我看到很多线上生产系统报出java报错信息和php报错信息了;外人来看可能看不懂,觉得炫酷,内行人看简直了,垮diao;类似于我找的这个网图


image.png


如何排查问题


再说下我们开发人员前后端都写的情况下如何排查问题,对于前后端都开发的人员其实避免了很多扯皮的事情,也少了很多沟通的问题,如果我们环境点击报错,我们可以



  1. 打开浏览器的f12查看该请求的地址

  2. 按该地址找到后台对应的接口地址,启动本地,打上断点

  3. 如果没有走进后台断点处那么存在三个问题,一个是contentType或者请求方式两者没有保持一致,这个一般开发自测的时候就可以测出来,另一个就是你的地址可能中间环节有路由,路由有问题一般对于大部分功能都有影响,不会是小范围的,还有一种就是我们的后台有拦截器但是我们不熟悉这块,一般大家接手项目的时候估计只会扫一眼这块,恰好这块对于某些业务权限卡的很的项目来说会经常发生这种事,而你恰好不熟悉所以你排查半天也不会有头绪;

  4. 进入断点以后,我们按流程往下执行就能找到报错的地方了

  5. 如果你日志打的详细而且也可以轻松获取生产的日志,那就在日志中就可以找到我们报错的信息;

  6. 如果你是传回前台后报错,那么我们需要在浏览器上打断点,然后去定位是不是咱们传的参数和前台解析的参数属性不一致还是一些其他的问题,以上就形成了闭环;


如果我们是只写后端,分离项目的那种,那咱们就是加强沟通,和气生财,一切问题出在我后端,前端都是完美的,来问题了你先排查起来,确定没问题了,再去告诉项目大哥,让前端兄弟排查一下,有些新手可能会问为什么不让前端先排查,这个其实不该问,只要是前后端分离的,业务层其实都是摆在后端的,而问题大部分是出在业务上的,所以后端干就完了;


image.png
如果我们使用了一些中间件,要没事带关注这些玩意,有时候大家共用的Redis,你不知道别人怎么操作,然后Redis崩了,你能怎么办,如果你是业务前置部门,虽然与你无瓜,但客户的感知就是你报错了,别人躲在后面到不了那一步,所以你得去各方联系重启机器;


ABUIABACGAAg9b-EhwYo0omnkwUwkAM4kAM.jpg


项目执行过程真的报oom了呢,那你必须去生产环境捞日志,找到位置,看看机器配置,看看项目执行占用资源情况,纯小白方式直接top命令查看,资源的确给的少了,那么我们启动的时候调整下jvm参数,把它调大,如果是代码执行循环导致的,那么我们就得优化代码,如果是执行任务之类的,比如给个无界队列,那么队列也会把数据撑爆,这时候我们也需要调整业务逻辑,(**记住,队列撑爆内存千万别直接把队列弄成有界的,一定要去沟通怎么优化,得到认可才能干,我们开发对于业务场景是没有产品经理清晰的**)这种挤爆jvm的不是那么多见,但的确很长见识的;


部署打包


排查完、修改完我们就要打包了,其实我特别不建议本地打包那种方式(应该禁止),万一哪个卧龙本地打包后认为活结束了然后忘了提交,然后他离职了然后电脑重置然后over;不管有意无意,环节得控制好我在第一篇就说了,避免后期维护压力,要控制好每一个环节,其实很简单,代码上传git或者svn,用jenkins来打,Jenkins还会记录每一次的打包时间,然后下载发给生产,我觉得比本地打包优秀多了;


jenkins.jpg


还有就是我们上生产的配置文件尽量读取服务器上的配置,不要和打包一起,你的项目可能部署在很多地方用,单独的配置避免了频繁的找文件,如果需要直接生产copy一份然后修改再上传,


ok!完成


四、总结



我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!



作者:小红帽的大灰狼
来源:juejin.cn/post/7374380071531216934
收起阅读 »

一次偶然提问引发的惊喜体验

大家有没有一种感受,虽然我们在一个满是信息的时代,却往往在寻求精确答案时感到迷茫,因为五花八门的说法太多了。然而,最近在一个网站上的一个随意提问,却让我见证了知识共享的魅力,体验到了前所未有的惊喜——解答的非常快速,也确实解决了我的问题。那天,出于好奇也是有点...
继续阅读 »

大家有没有一种感受,虽然我们在一个满是信息的时代,却往往在寻求精确答案时感到迷茫因为五花八门的说法太多了。然而,最近在一个网站上的一个随意提问,却让我见证了知识共享的魅力,体验到了前所未有的惊喜——解答的非常快速,也确实解决了我的问题。

那天,出于好奇也是有点着急需要解答一个写代码中的问题,我在一个网站上键入了心中的疑惑原因是我在知乎上看到说这个是一个专业的IT一站式学习服务平台,本以为就写一写就算了,没想到,短短几分钟内,就有人来解答了而且回答还挺精准,让我一下恍然大悟。而且里面的AI回复也挺智能,挺有意思的,之后,我就认真逛了一下这个网站,他里面是专门一个帮助专栏的,就和我们发朋友圈一样的感觉,但是是单独拎出来的一个板块,我看大家在里面的提问都有人或者官方去回复的,也可能是因为这是一个新站点,也是IT的一个垂直领域,东西没那么杂,里面的人也都是和IT相关的,所以才能比较快得到答案。对了,这个网站叫云端源想,百度直接搜索就可以找到的,编程过程中需要寻求帮助的小伙伴可以去看看哈。

收起阅读 »

uniApp新模式: 使用Vue3 + Vite4 + Pinia + Axios技术栈构建

背景 使用Vue3 + Vite4 + Pinia + Axios + Vscode模式开发之后,感叹真香!不用再单独去下载HBuilderX。废话不多说,直接上干货! 版本号 node: v16.18.0 vue: ^3.3.4, vite: 4.1.4 ...
继续阅读 »

背景


使用Vue3 + Vite4 + Pinia + Axios + Vscode模式开发之后,感叹真香!不用再单独去下载HBuilderX。废话不多说,直接上干货!


版本号



  • node: v16.18.0

  • vue: ^3.3.4,

  • vite: 4.1.4

  • sass: ^1.62.1

  • pinia: 2.0.36

  • pinia-plugin-unistorage: ^0.0.17

  • axios: ^1.4.0

  • axios-miniprogram-adapter: ^0.3.5

  • unplugin-auto-import: ^0.16.4


如遇到问题,请检查版本号是否一致!!!


项目目录结构


└── src # 主目录
├── api # 存放所有api接口文件
│ ├── user.js # 用户接口
├── config # 配置文件
│ ├── net.config.js # axios请求配置
├── pinia-store # 配置文件
│ ├── user.js # axios请求配置
├── utils # 工具类文件
│ ├── request.js # axios请求封装


开发流程


建议去uni-preset-vue仓库下载vite分支zip包,熟练ts的童鞋下载vite-ts


安装



  • 下载之后进入项目


cd uni-preset-vue


  • 安装依赖


# pnpm
pnpm install
# yarn
yarn
# npm
npm i

运行


pnpm dev:mp-weixin

打开微信开发者工具,找到dist/dev/mp-weixin运行,可以看到默认的页面


安装pinia


pnpm add pinia 

使用pinia


src目录下构建 pinia-store/user.js文件


/**
* @description 用户信息数据持久化
*/

import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
state() {
return {
userInfo: {}
}
},
actions: {
setUserInfo(data) {
this.userInfo = data
}
}
})


  • 修改main.js文件


import {
createSSRApp
} from "vue";
import * as Pinia from 'pinia';
import App from "./App.vue";
export function createApp() {
const app = createSSRApp(App);
const store = Pinia.createPinia();
app.use(store);

return {
app,
Pinia
};
}

pinia数据持久化


安装pinia-plugin-unistorage


pnpm add pinia-plugin-unistorage

修改main.js文件,增加如下代码:


// pinia数据持久化
import { createUnistorage } from 'pinia-plugin-unistorage'
store.use(createUnistorage());
app.use(store);

完整代码如下:


import { createSSRApp } from "vue";

import * as Pinia from 'pinia';
// pinia数据持久化
import { createUnistorage } from 'pinia-plugin-unistorage'
import App from "./App.vue";
export function createApp() {
const app = createSSRApp(App);

const store = Pinia.createPinia();
store.use(createUnistorage());
app.use(store);

return {
app,
Pinia
};
}


在页面中使用:


<script setup>
import { useUserStore } from '@/pinia/user.js'
const user = useUserStore()

// 设置用户信息
const data = { userName: 'snail' }
user.setUser(data)
// 打印用户信息
console.log(user.userInfo)
</script>

安装axios


pnpm add axios

适配小程序,需要另外安装axios-miniprogram-adapter插件


pnpm add axios-miniprogram-adapter

使用axios


utils创建utils/request.js文件


import axios from 'axios';
import mpAdapter from "axios-miniprogram-adapter";
axios.defaults.adapter = mpAdapter;
import { netConfig } from '@/config/net.config';
const { baseURL, contentType, requestTimeout, successCode } = netConfig;

let tokenLose = true;

const instance = axios.create({
baseURL,
timeout: requestTimeout,
headers: {
'Content-Type': contentType,
},
});

// request interceptor
instance.interceptors.request.use(
(config) => {
// do something before request is sent
return config;
},
(error) => {
// do something with request error
return Promise.reject(error);
}
);

// response interceptor
instance.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/

(response) => {
const res = response.data;

// 请求出错处理
// -1 超时、token过期或者没有获得授权
if (res.status === -1 && tokenLose) {
tokenLose = false;
uni.showToast({
title: '服务器异常',
duration: 2000
});

return Promise.reject(res);
}
if (successCode.indexOf(res.status) !== -1) {
return Promise.reject(res);
}
return res;
},
(error) => {
return Promise.reject(error);
}
);

export default instance;


其中net.config.js文件需要在src/config目录下创建,完整代码如下:


/**
* @description 配置axios请求基础信息
* @author hu-snail 1217437592@qq.com
*/

export const netConfig = {
// axios 基础url地址
baseURL: 'https://xxx.cn/api',
// 为开发服务器配置 CORS。默认启用并允许任何源,传递一个 选项对象 来调整行为或设为 false 表示禁用
cors: true,
// 根据后端定义配置
contentType: 'application/json;charset=UTF-8',
//消息框消失时间
messageDuration: 3000,
//最长请求时间
requestTimeout: 30000,
//操作正常code,支持String、Array、int多种类型
successCode: [200, 0],
//登录失效code
invalidCode: -1,
//无权限code
noPermissionCode: -1,
};

src目录下创建src/api/user.jsapi文件


import request from '@/utils/request'

/**
* @description 授权登录
* @param {*} data
*/

export function wxLogin(data) {
return request({
url: '/wx/code2Session',
method: 'post',
params: {},
data
})
}

/**
* @description 获取手机号
* @param {*} data
*/

export function getPhoneNumber(data) {
return request({
url: '/wx/getPhoneNumber',
method: 'post',
params: {},
data
})
}


在页面中使用


<script setup>
import { wxLogin, getPhoneNumber } from '@/api/user.js'
/**
* @description 微信登录
*/

const onWxLogin = async () => {
uni.login({
provider: 'weixin',
success: loginRes => {
state.wxInfo = loginRes
const jsCode = loginRes.code
wxLogin({jsCode}).then((res) => {
const { openId } = res.data
user.setUserInfo({ openId })
})
}
})
}

</script>

配置vue自动导入


安装unplugin-auto-import插件


pnpm add unplugin-auto-import -D

修改vite.config.js文件:


import AutoImport from 'unplugin-auto-import/vite'
plugins: [
AutoImport({
imports: ["vue"]
})
],

页面中使用,需要注意的事每次导入新的vue指令,需要重新运行!!


<script setup>
onBeforeMount(() => {
console.log('----onBeforeMount---')
})
</script>

安装uni-ui


pnpm add @dcloudio/uni-ui

使用uni-ui


修改pages.json文件,增加如下代码:


"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},

在页面中使用


<template>
<uni-icons type="bars" size="16"></uni-icons>
</template>

到此已基本可以完成程序的开发,其他功能按照自己的需求做增删改查即可!


作者:蜗牛前端
来源:juejin.cn/post/7244192313844154424
收起阅读 »

解决vite项目首次打开页面卡顿的问题

web
问题描述 在vite项目中我们可能会遇到这样一种情况。 在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。 要是我们一天只专注于一两个页面 那这个就不是问题。 但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样...
继续阅读 »

问题描述


在vite项目中我们可能会遇到这样一种情况。


在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。


要是我们一天只专注于一两个页面 那这个就不是问题。


但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样就很痛苦了。


问题原因


为什么会出现这种情况呢?因为路由的懒加载与vite的编译机制。


路由的懒加载:没有进入过的页面不加载


vite的编译机制:没有加载的不编译。


这样就会出现 我们在进入一个新页面的时候他才会编译。我们感觉卡顿的过程就是他编译的过程。


解决思路


问题找到了,那么解决起来就简单了。我们本地开发的时候,取消路由的懒加载就可以了。


const routes = [
{
path: `/home`,
name: `Home`,
component: () => import(`@/views/home/HomePage.vue`),
meta: { title: `首页` },
},
{
path: `/test1`,
name: `test1`,
component: () => import(`@/views/demo/Test1.vue`),
meta: { title: `测试1` },
},
{
path: `/test2`,
name: `test2`,
component: () => import(`@/views/demo/Test2.vue`),
meta: { title: `测试2` },
}
]

if (import.meta.env.MODE === `development`) {
routes.forEach(item => item.component())
}

示例代码如上。上述的问题是解决了,但是又产生了新的问题。项目太大的时候启动会非常慢。


于是我想了一个折中的方案。初始打开项目的时候路由还是懒加载的,然后我在浏览器网络空闲的时候去加载资源。这样你首次进系统打开的第一个页面可能还是需要等待,但是之后的所有页面就不需要等待了。


那么问题又来了?怎么监听浏览器的网络空闲呢?这就要用的浏览器的一个api PerformanceObserver。这个api可能很多小伙伴都不知道,它主要是帮助你监控和观察与性能相关的各种指标。想要详细了解的可以点击这里查看


我们今天用的就是resource类型监听所有的网络请求,代码示例如下


 const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
const entries: PerformanceEntryList = list.getEntries()
for (const entry of entries) {
if (entry.entryType === `resource`) {
//网络请求结束
}
}
})
observer.observe({ entryTypes: [`resource`] })

监听到网络请求后,我们怎么判断是否空闲呢?也很简单,只要一秒钟以内没有新的网络请求出现我们就认为当前网络是空闲的。这不就防抖函数嘛。


const routes = [
{
path: `/home`,
name: `Home`,
component: () => import(`@/views/home/HomePage.vue`),
meta: { title: `首页` },
},
{
path: `/test1`,
name: `test1`,
component: () => import(`@/views/demo/Test1.vue`),
meta: { title: `测试1` },
},
{
path: `/test2`,
name: `test2`,
component: () => import(`@/views/demo/Test2.vue`),
meta: { title: `测试2` },
}
]

if (import.meta.env.MODE === `development`) {
const componentsToLoad = routes.map(item => item.component)
const loadComponentsWhenNetworkIdle = debounce(
() => {
if (componentsToLoad.length > 0) {
const componentLoader = componentsToLoad.pop()
componentLoader && componentLoader()
// eslint-disable-next-line
console.log(`剩余${componentsToLoad.length}个路由未加载`, componentsToLoad)
}
},
1000,
false
)

const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
const entries: PerformanceEntryList = list.getEntries()
for (const entry of entries) {
if (entry.entryType === `resource`) {
loadComponentsWhenNetworkIdle()
}
}
})
observer.observe({ entryTypes: [`resource`] })
}

完整的代码如上。当我们判断出网络空闲后,就从componentsToLoad数组中删除一个组件,并加载删除的这个组件,然后就会重新触发网络请求。一直重复这个流程,直到componentsToLoad数组为空。


这只是个示例的代码,获取componentsToLoad变量防抖函数的配置(初始化不执行,无操作后1秒钟后执行)还要根据你的实际项目进行修改!


可优化项


以上方法确实是按照我们的预期实现了,但是还有一些小小的问题。例如:



  1. 我们在加载组件的时候如果恰好是当前打开的页面,是不会重新触发网络请求的。因此可能会断掉componentsToLoad数组的删除,加载组件,触发网络请求这个流程。不过问题不大,你在当前页面如果有操作重新触发网络请求了,这个流程还会继续走下去,直到componentsToLoad数组为空。

  2. 每次刷新页面componentsToLoad数组都是会重新获取到值的,也就是我们走过的流程会重新走。不过问题不大,第二次走都是走缓存了,执行速度很快,而且也是本地开发那点性能损坏可以忽略不计。


这些问题影响都不是很大,所以我就没继续做优化。有兴趣的小伙伴可以继续研究下去。


作者:热心市民王某
来源:juejin.cn/post/7280745727160811579
收起阅读 »

有了这玩意,分分钟开发公众号功能!

大家好,我是程序员鱼皮。 不论在企业、毕设还是个人练手项目中,很多同学或多或少都会涉及微信相关生态的开发,例如微信支付、开放平台、公众号等等。 一般情况下,我们需要到官网查阅这些模块对应的 API 接口,自己编写各种对接微信服务器的代码,结果很多时间都花在了看...
继续阅读 »

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


不论在企业、毕设还是个人练手项目中,很多同学或多或少都会涉及微信相关生态的开发,例如微信支付、开放平台、公众号等等。


一般情况下,我们需要到官网查阅这些模块对应的 API 接口,自己编写各种对接微信服务器的代码,结果很多时间都花在了看文档和理解流程上。


好在,某位大佬开源了一个 WxJava 库,它可以让我们更高效快速地开发微信相关的功能。


什么是 WxJava?


WxJava 是一个开箱即用的 SDK,封装了微信生态后端开发绝大部分的 API 接口为现成的方法,包括微信支付、开放平台、小程序、企业微信、公众号等。我们开发时直接调用这个 SDK 提供的方法即可,同时作者针对这个 SDK 还提供了很多接入的 Demo,大部分场景跟着 demo 就能很快上手,非常高效!不需要深入阅读微信开发者官方文档,也能学会微信开发。


WxJava 开发 Demo


这个项目在 GitHub 上 已经有 29.1k 的 star ,社区活跃,且在持续维护更新中。



下面我会通过一个实战案例《公众号的菜单管理功能》,带大家入门 WxJava。


公众号的菜单管理开发实战


1、功能介绍


正常情况下,公众号的管理员可以在公众号网页后台来编辑菜单,例如下面这个页面:



上图中,我在菜单栏分别添加了三个按钮:主菜单一、点击事件、主菜单三。


用户点击 主菜单一 后,就会打开我们设置的跳转网页地址。



上图的 url 仅为演示,实际仅能填写跟公众号相关的网址。



用户点击 点击事件 后,就会自动回复一条消息:您点击了菜单。



你可能会好奇了:公众号网页后台都自带了菜单管理能力,我们还开发什么?


举个例子,如果我们希望用户点了菜单后,调用我们的后端完成新用户注册,就必须要自定义菜单了,因为需要对接我们自己的后端服务器。


而一旦你在后台配置了自己的服务器,就无法使用公众号自带的网页后台来管理菜单和自动回复了,如图:



这种情况下,就只能完全自己在后端写代码来实现这些功能。


2、开发实战


接下来我们用 WxJava 提供的 SDK,通过代码来实现上述同样的功能。


首先,我们需要在 maven 中引入 sdk:


<dependency>
  <groupId>com.github.binarywang</groupId>
  <artifactId>wx-java-mp-spring-boot-starter</artifactId>
  <version>4.4.0</version>
</dependency>

然后在配置文件中添加公众号的 appId 和 appSecret 配置:



按照 WxJava 的规则,编写一个配置类,构建 WxMpService 的 Bean 实例,注入到 Spring 容器中。



上图中的 WxMpService 就是 WxJava 提供的操作微信公众号相关服务的工具类。


接下来,就可以直接创建菜单啦!示例代码如下图:




再次备注:对应 url 内容填写仅为演示,实际 url 对应的网址必须是当前公众号的内容



执行上述代码,其实就可以配置菜单了,你甚至感受不到跟微信服务器 “打交道” 的流程。


这里再简单介绍下菜单二的点击事件,如上面演示,点击 点击事件 公众号会自动回复:“您点击了菜单”。


这个动作被定义为一个叫 CLICK_MENU_KEY 的 key,当用户点击这个按钮后,公众号就会向我们部署的后端服务发送这个事件 key,根据 key 的内容可以执行不同的动作,例如上面说的回复一段文字。


我们仅需把这个 key 绑定到路由上,当触发这个事件就调用对应的 handler 即可,典型的事件驱动设计~



EventHandler 的动作就是返回 “您点击了菜单” 这段文字:



3、其他功能演示


再举例个小功能,如果我们要删除菜单怎么办呢?


非常简单,可以先调用获取菜单的方法:


WxMenu wxMenu = wxMpService.getMenuService().menuGet();

然后根据菜单 ID 就可以调用删除方法来删除菜单:


wxMpService.getMenuService().menuDelete(menuId);

如果要修改菜单,可以再次调用 menuCreate 直接覆盖即可。


最后


利用 WxJava 我们已经实现了菜单的管理,可以看到接口定义非常清晰,使用起来也很方便。当然,以上只是个 Demo,实际企业中如果要操作公众号菜单,不可能每次都是手动执行代码,而是会有一个对应的公众号管理前端,或者再省点事,直接用接口文档来调用操作菜单的接口。感兴趣的同学可以自己实现~


总之希望大家通过这篇教程能够明白,微信相关的开发,并没有那么难,多去做一些调研、多主动搜索一些方案,你会发现很多路前人已经帮你打通了!


可访问我的 Github:github.com/liyupi ,了解更多技术和项目内容。


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

为什么list.sort()比Stream().sorted()更快?

昨天写了一篇文章《小细节,大问题。分享一次代码优化的过程》,里面提到了list.sort()和list.strem().sorted()排序的差异。 说到list sort()排序比stream().sorted()排序性能更好。 但没说到为什么。 有朋友也...
继续阅读 »

昨天写了一篇文章《小细节,大问题。分享一次代码优化的过程》,里面提到了list.sort()和list.strem().sorted()排序的差异。

说到list sort()排序比stream().sorted()排序性能更好。

但没说到为什么。


企业微信截图_16909362105085.png


有朋友也提到了这一点。


本文重新开始,先问是不是,再问为什么。




真的更好吗?




先简单写个demo


List userList = new ArrayList<>();
Random rand = new Random();
for (int i = 0; i < 10000 ; i++) {
userList.add(rand.nextInt(1000));
}
List userList2 = new ArrayList<>();
userList2.addAll(userList);

Long startTime1 = System.currentTimeMillis();
userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");

Long startTime = System.currentTimeMillis();
userList.sort(Comparator.comparing(Integer::intValue));
System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");

输出


stream.sort耗时:62ms
List.sort()耗时:7ms

由此可见list原生排序性能更好。

能证明吗?

证据错了。




再把demo变换一下,先输出stream.sort


List userList = new ArrayList<>();
Random rand = new Random();
for (int i = 0; i < 10000 ; i++) {
userList.add(rand.nextInt(1000));
}
List userList2 = new ArrayList<>();
userList2.addAll(userList);

Long startTime = System.currentTimeMillis();
userList.sort(Comparator.comparing(Integer::intValue));
System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");

Long startTime1 = System.currentTimeMillis();
userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");

此时输出变成了


List.sort()耗时:68ms
stream.sort耗时:13ms

这能证明上面的结论错误了吗?

都不能。

两种方式都不能证明什么。


使用这种方式在很多场景下是不够的,某些场景下,JVM会对代码进行JIT编译和内联优化。


Long startTime = System.currentTimeMillis();
...
System.currentTimeMillis() - startTime

此时,代码优化前后执行的结果就会非常大。


基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

基准测试使得被测试代码获得足够预热,让被测试代码得到充分的JIT编译和优化。




下面是通过JMH做一下基准测试,分别测试集合大小在100,10000,100000时两种排序方式的性能差异。


import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 5)
@Fork(1)
@State(Scope.Thread)
public class SortBenchmark {

@Param(value = {"100", "10000", "100000"})
private int operationSize;


private static List arrayList;

public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SortBenchmark.class.getSimpleName())
.result("SortBenchmark.json")
.mode(Mode.All)
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}

@Setup
public void init() {
arrayList = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < operationSize; i++) {
arrayList.add(random.nextInt(10000));
}
}


@Benchmark
public void sort(Blackhole blackhole) {
arrayList.sort(Comparator.comparing(e -> e));
blackhole.consume(arrayList);
}

@Benchmark
public void streamSorted(Blackhole blackhole) {
arrayList = arrayList.stream().sorted(Comparator.comparing(e -> e)).collect(Collectors.toList());
blackhole.consume(arrayList);
}

}


性能测试结果:



可以看到,list sort()效率确实比stream().sorted()要好。




为什么更好?




流本身的损耗




java的stream让我们可以在应用层就可以高效地实现类似数据库SQL的聚合操作了,它可以让代码更加简洁优雅。


但是,假设我们要对一个list排序,得先把list转成stream流,排序完成后需要将数据收集起来重新形成list,这部份额外的开销有多大呢?


我们可以通过以下代码来进行基准测试


import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 5)
@Fork(1)
@State(Scope.Thread)
public class SortBenchmark3 {

@Param(value = {"100", "10000"})
private int operationSize; // 操作次数


private static List arrayList;

public static void main(String[] args) throws RunnerException {
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SortBenchmark3.class.getSimpleName()) // 要导入的测试类
.result("SortBenchmark3.json")
.mode(Mode.All)
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run(); // 执行测试
}

@Setup
public void init() {
// 启动执行事件
arrayList = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < operationSize; i++) {
arrayList.add(random.nextInt(10000));
}
}

@Benchmark
public void stream(Blackhole blackhole) {
arrayList.stream().collect(Collectors.toList());
blackhole.consume(arrayList);
}

@Benchmark
public void sort(Blackhole blackhole) {
arrayList.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
blackhole.consume(arrayList);
}

}

方法stream测试将一个集合转为流再收集回来的耗时。


方法sort测试将一个集合转为流再排序再收集回来的全过程耗时。




测试结果如下:



可以发现,集合转为流再收集回来的过程,肯定会耗时,但是它占全过程的比率并不算高。


因此,这部只能说是小部份的原因。




排序过程




我们可以通过以下源码很直观的看到。




  • 1 begin方法初始化一个数组。

  • 2 accept 接收上游数据。

  • 3 end 方法开始进行排序。

    这里第3步直接调用了原生的排序方法,完成排序后,第4步,遍历向下游发送数据。


所以通过源码,我们也能很明显地看到,stream()排序所需时间肯定是 > 原生排序时间。


只不过,这里要量化地搞明白,到底多出了多少,这里得去编译jdk源码,在第3步前后将时间打印出来。


这一步我就不做了。

感兴趣的朋友可以去测一下。


不过我觉得这两点也能很好地回答,为什么list.sort()比Stream().sorted()更快。


补充说明:



  1. 本文说的stream()流指的是串行流,而不是并行流。

  2. 绝大多数场景下,几百几千几万的数据,开心就好,怎么方便怎么用,没有必要去计较这点性能差异。


作者:是奉壹呀
来源:juejin.cn/post/7262274383287500860
收起阅读 »

utf8和utf8mb4有什么区别?

utf8或者utf-8是大家常见的一个词汇,它是一种信息的编码格式,特别是不同开发平台的系统进行对接的时候,编码一定要对齐,否则就容易出现乱码。 什么是编码? 先说说什么是编码。编码就像我们日常生活中的语言,不同的地方说不同的话,编码就是计算机用来表示这些“话...
继续阅读 »

utf8或者utf-8是大家常见的一个词汇,它是一种信息的编码格式,特别是不同开发平台的系统进行对接的时候,编码一定要对齐,否则就容易出现乱码。


什么是编码?


先说说什么是编码。编码就像我们日常生活中的语言,不同的地方说不同的话,编码就是计算机用来表示这些“话”的一种方式。比如我们使用汉字来说话,计算机用二进制数来表示这些汉字的方式,就是编码。


utf8就是这样一种编码格式,正式点要使用:UTF-8,utf8是一个简写形式。


为什么需要utf8?


在计算机早期,主要使用ASCII编码,只能表示128个字符,汉字完全表示不了。后来,才出现了各种各样的编码方式,比如GB2312、GBK、BIG5,但这些编码只能在特定的环境下使用,不能全球通用。


UTF-8就像一个万能翻译官,它的全称是“Unicode Transformation Format - 8 bit”,注意这里不是说UTF-8只能使用8bit来表示一个字符,实际上UTF-8能表示世界上几乎所有的字符。


它的特点是:



  • 变长编码:一个字符可以用1到4个字节表示,英文字符用1个字节(8bit),汉字用3个字节(24bit)。

  • 向后兼容ASCII:ASCII的字符在UTF-8中还是一个字节,这样就兼容了老系统。

  • 节省空间:对于英文字符,UTF-8比其他多字节编码更省空间。


UTF-8适用于网页、文件系统、数据库等需要全球化支持的场景。


经常接触代码的同学应该还经常能看到 Unicode 这个词,它和编码也有很大的关系,其实Unicode是一个字符集标准,utf8只是它的一种实现方式。Unicode 作为一种字符集标准,为全球各种语言和符号定义了唯一的数字码位(code points)。其它的Unicode实现方式还有UTF-16和UTF-32:



  • UTF-16 使用固定的16位(2字节)或者变长的32位(4字节,不在常用字符之列)来编码 Unicode 字符。

  • UTF-32 每一个字符都直接使用固定长度的32位(4字节)编码,不论字符的实际数值大小。这会消耗更多的存储空间,但是所有字符都可以直接索引访问。



图片来源:src: javarevisited.blogspot.com/2015/02/dif…


utf8mb4又是什么?


utf8mb4并不常见,它是UTF-8的一个扩展版本,专门用于MySQL数据库。MySQL在 5.5.3 之后增加了一个utf8mb4的编码,mb4就是最多4个字节的意思(most bytes 4),它主要解决了UTF-8不能表示一些特殊字符的问题,比如Emoji表情,这在论坛或者留言板中也经常用到。大家使用小红书时应该见过各种各样的表情符号,小红书后台也可能使用utf8mb4保存它们。


编码规则和特点:



  • 最多4个字节:utf8mb4中的每个字符最多用4个字节表示。

  • 支持更多字符:能表示更多的Unicode字符,包括Emoji和其他特殊符号。


utf8和utf8mb4的比较


存储空间



  • 数据库:utf8mb4每个字符最多用4个字节,比UTF-8多一个字节,存储空间会增加。

  • 文件:类似的,文件用utf8mb4编码也会占用更多的空间。


性能影响



  • 数据库:utf8mb4的查询和索引可能稍微慢一些,因为占用更多的空间和内存。

  • 网络传输:utf8mb4编码的字符会占用更多的带宽,传输速度可能会稍慢。


不过因为实际场景中使用的utf8mb4的字符也不多,其实对存储空间和性能的影响很小,大家基本没有必要因为多占用了一些空间和流量,而不是用utf8mb4。


只是我们在定义字段长度、规划数据存储空间、网络带宽的时候,要充分考虑4字节带来的影响,预留好足够的空间。


实战选择


在实际开发中,选择编码要根据具体需求来定。如果你的网站或者应用需要支持大量的特殊字符和Emoji,使用utf8mb4是个不错的选择。如果主要是英文和普通中文文本,utf8足够应付。


注意为了避免乱码问题,前端、后端、数据库都应该使用同一种编码,比如utf8,具体到编码时就是要确保数据库连接、网页头部、文件读写都设置为相同的编码。


另外还需要注意Windows和Linux系统中使用UTF-8编码的文件可能是有差别的,Windows中的UTF-8文件可能会携带一个BOM头,方便系统进行识别,但是Linux中不需要这个头,所以如果要跨系统使用这个文件,特别是程序脚本,可能需要在Linux中去掉这个头。




以上就是本文的主要内容,如有问题欢迎留言讨论。


关注萤火架构,加速技术提升!


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

鸿蒙,ArkTs 一段诡异的代码

分享一段我之前在学习 ArkTs 的时候,看到的一段诡异的代码。我们来看看下面的代码,按照你多年的经验,分析一下下面的代码是否可以正常执行,如果可以执行的话,能说出运行结果吗。for (let i = 0; i < 3; i++) { let i =...
继续阅读 »

分享一段我之前在学习 ArkTs 的时候,看到的一段诡异的代码。我们来看看下面的代码,按照你多年的经验,分析一下下面的代码是否可以正常执行,如果可以执行的话,能说出运行结果吗。

for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}

以上代码是可以正常运行的,这段代码的执行结果,将会输出了 3 个 'abc' :

abc
abc
abc

这段代码中的 for 循环尝试执行三次循环,每次循环中都声明了一个新的局部变量 i,并将其赋值为字符串 'abc'。然后,它打印出这个新的局部变量 i

在每次迭代中,尽管外部循环的控制变量也叫 i,但内部的 let i = 'abc'; 实际上创建了一个新的、同名的局部变量 i,这个变量的作用域仅限于 for 循环的块内部。因此,每次迭代打印的都是这个块作用域内的字符串 'abc',而不是外部循环控制变量的数值。

从执行结果也能间接说明,for 循环内部声明的变量 i 和循环变量 i 不在同一个作用域,它们有各自单独的作用域,设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域,这是 for 循环的特别之处。

如果在同一个作用域,是不可使用 let 或者 const 重复声明相同名字的变量。比如下面的代码会报错。

if(true){
let a = 1;
let a = 2; // 报错

const b = 3;
const b = 4; // 报错
}

这就引发出了另外一问题 块作用域

块作用域是指变量在定义它的代码块或者说是大括号 {} 内有效的作用域。使用 let 或者 const 关键字声明的变量具有块级作用域(block scope),这意味着变量在包含它的块(在这个例子中是 for 循环的大括号内)以及任何嵌套的子块中都是可见的。

块作用域示例:

let blockScopedVariable = 'I am dhl';
if (true) {
let blockScopedVariable = 'I am block scoped';
console.log(blockScopedVariable); // 输出: I am block scoped
}
console.log(blockScopedVariable); // 输出: I am dhl

从执行结果可以看出,在 if 语句中定义的变量 blockScopedVariable,仅在代码块内有效,外层变量不会被内层同名变量的声明和赋值影响。

但是需要注意,在 ArkTS 中不能使用 for .. in,否则会有一个编译警告。

之所以不能使用 for .. in 是因为在 ArkTS 中,对象的布局在编译时是确定的,且不能在程序执行期间更改对象的布局,换句话说,ArkTS 禁止以下行为:

  • 向对象中添加新的属性或方法
  • 从对象中删除已有的属性或方法
  • 将任意类型的值赋值给对象属性

如果修改对象布局会影响代码的可读性以及运行时性能。

从开发的角度来说,在某处定义类,然后又在其它地方修改了实际对象布局,这很容易引入错误,另外如果修改对象布局,需要在运行时支持,这样会增加执行开销降低性能。


作者:程序员DHL
来源:juejin.cn/post/7376158566598705167
收起阅读 »

人生第一次线上 OOM 事故,竟和 where 1 = 1 有关

这篇文章,聊聊一个大家经常使用的编程模式 :Mybatis +「where 1 = 1 」。 笔者人生第一次重大的线上事故 ,就是和使用了类似的编程模式 相关,所以印象极其深刻。 这几天在调试一段业务代码时,又遇到类似的问题,所以笔者觉得非常要必要和大家絮叨絮...
继续阅读 »

这篇文章,聊聊一个大家经常使用的编程模式 :Mybatis +「where 1 = 1 」。


笔者人生第一次重大的线上事故 ,就是和使用了类似的编程模式 相关,所以印象极其深刻。


这几天在调试一段业务代码时,又遇到类似的问题,所以笔者觉得非常要必要和大家絮叨絮叨。


1 OOM 事故


笔者曾服务一家电商公司的用户中心,用户中心提供用户注册,查询,修改等基础功能 。用户中心有一个接口 getUserByConditions ,该接口支持通过 「用户名」、「昵称」、「手机号」、「用户编号」查询用户基本信息。



我们使用的是 ibatis (mybatis 的前身), SQLMap 见上图 。当构建动态 SQL 查询时,条件通常会追加到 WHERE 子句后,而以 WHERE 1 = 1 开头,可以轻松地使用 AND 追加其他条件。


但用户中心在上线后,竟然每隔三四个小时就发生了内存溢出问题 ,经过通过和 DBA 沟通,发现高频次出现全表查询用户表,执行 SQL 变成 :



查看日志后,发现前端传递的参数出现了空字符串,笔者在代码中并没有做参数校验,所以才出现全表查询 ,当时用户表的数据是 1000万 ,调用几次,用户中心服务就 OOM 了。


笔者在用户中心服务添加接口参数校验 ,即:「用户名」、「昵称」、「手机号」、「用户编号」,修改之后就再也没有产生这种问题了。


2 思维进化


1、前后端同时做接口参数校验


为了提升开发效率,我们人为的将系统分为前端、后端,分别由两拨不同的人员开发 ,经常出现系统问题时,两拨人都非常不服气,相互指责。



有的时候,笔者会觉得很搞笑,因为这个本质是个规约问题。


要想系统健壮,前后端应该同时做接口参数校验 ,当大家都遵循这个规约时,出现系统问题的风险大大减少。


2、复用和专用要做平衡


笔者写的这个接口 getUserByConditions ,支持四种不同参数的查询,但是因为代码不够严谨,导致系统出现 OOM 。


其实,在业务非常明确的场景,我们可以将复用接口,拆分成四个更细粒度的接口 :



  • 按照用户 ID 查询用户信息

  • 按照用户昵称查询用户信息

  • 按照手机号查询用户信息

  • 按照用户名查询用户信息


比如按照用户 ID 查询用户信息 , SQLMAP 就简化为:



通过这样的拆分,我们的接口设计更加细粒度,也更容易维护 , 同时也可以规避 where 1 =1 产生的问题。


有的同学会有疑问:假如拆分得太细,会不会增加我编写 接口和 SQLMap 的工作量 ?


笔者的思路是:通过代码生成器动态生成,是绝对可以做到的 ,只不过需要做一丢丢的定制。


3、编写代码时,需要考虑资源占用量,做好预防性编程


笔者刚入行的时候,只是机械性的完成任务,并没有思考代码后面的资源占用,以及有没有可能产生恶劣的影响。


随着见识更多的系统,学习开源项目,笔者慢慢培养了一种习惯:



  • 这段代码会占用多少系统资源

  • 如何规避风险 ,做好预防性编程。


其实,这和玩游戏差不多 ,在玩游戏的时,我们经常说一个词,那就是意识。



上图,后裔跟墨子在压对面马可蔡文姬,看到小地图中路铠跟小乔的视野,方向是往下路来的,这时候我们就得到了一个信息。


知道对面的人要来抓,或者是协防,这种情况我们只有两个人,其他的队友都不在,只能选择避战,强打只会损失两名“大将”。


通过小地图的信息,并且想出应对方法,就是叫做“猜测意识”。


编程也是一样的,我们思考代码可能产生的系统资源占用,以及可能存在的风险,并做好防御性编程,就是编程的意识


4 写到最后


当我们在使用 :Mybatis +「where 1 = 1 」编程模式时,需要如下三点:



  1. 前后端同时做好接口参数校验 ;

  2. 复用和专用要做平衡,条件允许情况下将复用 SQLMap 拆分成更细粒度的 SQLMap ;

  3. 编写代码时,需要考虑资源占用量,做好预防性编程 ;




文章片段推荐:



生命就是这样一个过程,一个不断超越自身局限的过程,这就是命运,任何人都是一样,在这过程中我们遭遇痛苦、超越局限、从而感受幸福。


所以一切人都是平等的,我们毫不特殊。


--- 史铁生





作者:勇哥Java实战
来源:juejin.cn/post/7375345204046266368
收起阅读 »

因为git不熟练,我被diss了

浅聊一下 在百度实习快一个月了,公司的需求从提需到上线也是走过一遍了,个人认为最基础的也是最重要的就是git了...为什么这么说?因为本人git不熟练挨diss了😭😭😭,虽然之前也会使用git将代码提交到github,但是会使用到也就几条指令,今天就来总结一下...
继续阅读 »

浅聊一下


在百度实习快一个月了,公司的需求从提需到上线也是走过一遍了,个人认为最基础的也是最重要的就是git了...为什么这么说?因为本人git不熟练挨diss了😭😭😭,虽然之前也会使用git将代码提交到github,但是会使用到也就几条指令,今天就来总结一下在公司使用git的常规操作,刚进厂的掘友可以参考一下...


git


什么是git?


Git 是一款免费、开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。是的,我对git的介绍就一条,想看简介的可以去百度一下😘😘😘


为什么要用git?


OK,想象一下,我是一名作家,现在我要开始写一本小说了,我想要将我的小说每天都发布到“github小说网”上,一日两更。我想要一个工具,它要具备的功能如下:



  1. 将我每天写的小说章节发布

  2. 我发现昨天写的章节有问题,它可以帮我撤回

  3. 一周后,我又想找到上周我撤回的章节,它能帮我找到

  4. 我想写一个“漫威宇宙的系列”,我需要雇人和我一起写,它可以帮我们同步进度

  5. 我想要查看每个人写了什么,什么时候写的

  6. ...


想要的有点多了,我知道很难满足,但是git就能满足我的一切需求...


写小说


我进厂写小说了,厂长说:你先下一个git。那我必须得下一个git


下载git


直接跑到这个git官网http://www.git-scm.com/downloads ,可以搜个教程跟着安装,这里就不细说了


基本配置


把git下载下来了,那我不得登录一下,免得到时候小说写的有问题都不知道是谁写的,为了不背锅!


$ git config --global user.name 
$ git config --global user.email

参与写小说


来到“github小说网”,要将之前的章节全部拷贝到你的电脑上,才能开始续写


image.png


使用git clone 命令来完成


$ git clone https://github.com/vuejs/vue.git

这样就将代码克隆到你的本地了


小说版本


我们的小说每天都在迭代更新,master分支就是我们的主分支,也就是目前发布的最新的小说内容


image.png


每当我们向master提交代码,master都会向前移动一步。


想象一个场景,有十个人都在写同一本小说,那么十个人都同时向master提供代码,会发生什么事情?



  • 并行开发受限:没有分支意味着无法支持并行开发,因为每个人都只能基于master进行工作,这可能会导致团队成员之间的代码冲突。

  • 代码管理困难:由于所有更改都直接应用于master,代码管理会变得混乱,很难跟踪谁提交了哪些更改,以及何时进行了更改。

  • 风险高:由于没有分支,每次更改都直接影响master,这可能增加了引入错误或破坏现有功能的风险。

  • 难以撤销更改:没有分支意味着难以进行实验性更改或回滚到先前的版本,因为没有办法轻松地隔离或恢复更改。


所以我们每个人都需要创建自己的分支,最后再将自己的分支与master合并


当我们创建了新的分支,比如叫 myBranch ,git 就会新建一个指针叫 myBranch,指向 master 相同的提交,在把 HEAD 指向 myBranch,就表示当前分支在 myBranch 上。


image.png


从现在开始,对工作区的修改和提交都是针对 myBranch 分支了,如果我们修改后再提交一次,myBranch指针就会向前移动一步,而master指针不变,当我们将myBranch开发完毕以后,再将它与master合并



  • 查看当前分支


$ git branch


  • 创建分支


$ git checkout -b 分支名

git checkout 命令加上-b参数,表示创建分支并切换,它相当于下面的两个命令:


$ git branch dev        //创建分支
$ git checkout dev //切换到创建的分支

提交


在上面,我们已经创建好了一个分支myBranch,我们一天要写两章小说,当我每写完一章以后,我要将它先存入暂存区,当一天的工作完毕以后,统一将暂存区的代码提交到本地仓库,最后再上传到远程仓库,并且合并



  • 上传暂存区


$ git add .    //将修改的文件全部上传
$ git add xxx //将xxx文件上传


  • 提交到本地仓库


git commit -m '提交代码的描述'


  • 提交到远程仓库的对应分支


$ git push origin xxx    //xxx是对应分支名


  • 合并分支


$ git checkout master    //首先切换分支到master
$ git merge mybranch


  • 删除分支


当你合并完分支以后,mybranch分支就可以删除了


$ git branch -d mybranch

解决冲突


Git 合并分支产生冲突的原因通常是因为两个或多个分支上的相同部分有了不同的修改。这可能是因为以下几个原因:



  1. 并行开发:团队中的不同成员在不同的分支上同时开发功能或修复 bug。如果他们修改了相同的文件或代码行,就会导致合并冲突。

  2. 分支基于旧版本:当从一个旧的提交创建分支,然后在原始分支上进行了更改时,可能会导致冲突。这是因为在创建分支后,原始分支可能已经有了新的提交。

  3. 重命名或移动文件:如果一个分支重命名或移动了一个文件,而另一个分支对同一文件进行了修改,就会导致冲突。

  4. 合并冲突的解决方法不同:在合并分支时,有时会使用不同的合并策略或解决方法,这可能会导致冲突。

  5. 历史分叉:如果两个分支的历史分叉很远,可能会存在较大的差异,从而导致合并时出现冲突。


于是我们需要将冲突解决再重新合并分支,解决冲突也就是查看文件新增了哪些代码,你需要保留哪些代码,把不需要的删去就可以了...


我们还需养成一个好习惯,就是在开发之前先git pull 一下,更新一下自己本地的代码确保版本是最新的。


添砖加瓦


如果我已经使用git commit -m 'xxx'将代码提交到了本地仓库,但是我后续还想向这个提交中添加文件,那我该怎么办呢?



  1. 首先将你想添加到文件使用git add xxx加入暂存区

  2. 然后运行以下命令:


$ git commit --amend

这将会打开一个编辑器,让你编辑上一次提交的提交信息。如果你只是想要添加文件而不改变提交信息,你可以直接保存并关闭编辑器。



  1. Git 将会创建一个新的提交,其中包含之前的提交内容以及你刚刚添加的文件。


您撤回了一次push


代码推送到远程仓库的master上以后,我发现有bug,挨批是不可避免了,批完还得接着解决...



  1. 撤销最新的提交并保留更改


$ git reset HEAD^

这会将最新的提交从 master 分支中撤销,但会保留更改在工作目录中。你可以修改这些更改,然后重新提交。



  1. 撤销最新的提交并丢弃更改


$ git reset --hard HEAD^

这会完全撤销最新的提交,并丢弃相关的更改。慎用,因为这将永久丢失你的更改



  1. 创建新的修复提交


如果你不想删除最新的提交,而是创建一个新的提交来修复问题,可以进行如下操作:



  • 在 master 分支上创建一个新的分支来进行修复:


$ git checkout -b fix-branch master


  • 在新分支上进行修改,修复代码中的问题。

  • 提交并推送修复:


$ git add .
$ git commit -m "Fixing the issue"
$ git push origin fix-branch

结尾


当你学会以上操作的时候, 你就可以初步参加公司的代码开发了,从挨批中进步!!!


作者:滚去睡觉
来源:juejin.cn/post/7375928754147246107
收起阅读 »

Git 代码提交规范,feat、fix、chore 都是什么意思?

写在前面 经常看到别人提交的代码记录里面包含一些feat、fix、chore等等,而我在提交时也不会区分什么,直接写下提交信息,今天就来看一下怎么个事,就拿 element-plus/ant-design 来看一下。 其实这么写是一种代码提交规范,当然不是...
继续阅读 »

写在前面


经常看到别人提交的代码记录里面包含一些feat、fix、chore等等,而我在提交时也不会区分什么,直接写下提交信息,今天就来看一下怎么个事,就拿 element-plus/ant-design 来看一下。



image.png
其实这么写是一种代码提交规范,当然不是为了炫技,主要目的是为了提高提交记录的可读性和自动化处理能力。


当然如果团队没有要求,不这么写也可以。


git 提交规范


commit message = subject + :+ 空格 + message 主体


例如: feat:增加用户注册功能


常见的 subject 种类以及含义如下:



  1. feat: 新功能(feature)



    • 用于提交新功能。

    • 例如:feat: 增加用户注册功能



  2. fix: 修复 bug



    • 用于提交 bug 修复。

    • 例如:fix: 修复登录页面崩溃的问题



  3. docs: 文档变更



    • 用于提交仅文档相关的修改。

    • 例如:docs: 更新README文件



  4. style: 代码风格变动(不影响代码逻辑)



    • 用于提交仅格式化、标点符号、空白等不影响代码运行的变更。

    • 例如:style: 删除多余的空行



  5. refactor: 代码重构(既不是新增功能也不是修复bug的代码更改)



    • 用于提交代码重构。

    • 例如:refactor: 重构用户验证逻辑



  6. perf: 性能优化



    • 用于提交提升性能的代码修改。

    • 例如:perf: 优化图片加载速度



  7. test: 添加或修改测试



    • 用于提交测试相关的内容。

    • 例如:test: 增加用户模块的单元测试



  8. chore: 杂项(构建过程或辅助工具的变动)



    • 用于提交构建过程、辅助工具等相关的内容修改。

    • 例如:chore: 更新依赖库



  9. build: 构建系统或外部依赖项的变更



    • 用于提交影响构建系统的更改。

    • 例如:build: 升级webpack到版本5



  10. ci: 持续集成配置的变更



    • 用于提交CI配置文件和脚本的修改。

    • 例如:ci: 修改GitHub Actions配置文件



  11. revert: 回滚



    • 用于提交回滚之前的提交。

    • 例如:revert: 回滚feat: 增加用户注册功能




总结


使用规范的提交消息可以让项目更加模块化、易于维护和理解,同时也便于自动化工具(如发布工具或 Changelog 生成器)解析和处理提交记录。


通过编写符合规范的提交消息,可以让团队和协作者更好地理解项目的变更历史和版本控制,从而提高代码维护效率和质量。


作者:JacksonChen
来源:juejin.cn/post/7374295163625521161
收起阅读 »

请一定要使用常量和枚举

1.魔法值和硬编码 在代码编写的场景中,会遇到提示避免去使用 魔法值(magic numbers)和硬编码(hardcoding)。 魔法值就是在代码中直接使用的,没有提供任何注释或解释说明其用途和含义的常数值。 硬编码指的是在程序中直接使用特定的值或信息,...
继续阅读 »

1.魔法值和硬编码


在代码编写的场景中,会遇到提示避免去使用 魔法值(magic numbers)和硬编码(hardcoding)。



  • 魔法值就是在代码中直接使用的,没有提供任何注释或解释说明其用途和含义的常数值。

  • 硬编码指的是在程序中直接使用特定的值或信息,而不是通过变量、常量或其他可配置的方式来表示。这些值通常是字面量字符串、数字或其他原始数据类型,在代码中写死了,无法修改。


缺点:


不便于维护:如果需要修改值,必须手动在代码中查找并替换,会增加代码修改的复杂度和风险。


可读性差:硬编码的值缺乏描述和注释,不易于理解和解释。在工作中,协作开发,其他开发人员在阅读代码时可能无法理解这些值的含义和作用。


维护困难:当需要修改值的时候,需要在代码中找到所有使用该值的地方进行手动修改。这样容易出错,而且增加了代码维护的复杂性。


2.定义常量


场景:设π取小数点后五位数(即3.14159)计算圆的面积


Java常量定义是指在Java程序中定义一个不可修改的值,Java常量的定义使用关键字final,一般与static关键字一起使用。


此时可以通过定义一个常量作为π


public class MyClass {  
//圆周率π
public static final double PI = 3.14159;
}

上面这个定义在类中的常量称为 类常量,可以通过类名访问。


通过定义常量,就避免在代码中直接使用没有明确含义的硬编码数字。取而代之,将这些数字赋值给具有描述性名称的常量。


3.if - else if - else if - else if.....else


在项目中看过这面这段代码,通过判断天气给出建议


public void handleWeather(String weather) {  
if (weather.equals("晴天")) {
System.out.println("做好防晒");
} else if (weather.equals("阴天")) {
System.out.println("户外活动");
} else if (weather.equals("小雨")) {
System.out.println("带雨伞");
} else if (weather.equals("雷雨")) {
System.out.println("避免户外活动");
} else {
System.out.println("未知天气");
}
}

这段代码的判断条件 "晴天"、"阴天"、"小雨"等,这些条件在项目不止使用到了一次,比如在另外一个方法中也有一个判断,但是判断执行的方法体不同,如下


public void handleWeather(String weather) {  
if (weather.equals("晴天")) {
System.out.println("出太阳");
} else if (weather.equals("阴天")) {
System.out.println("有乌云");
}
....
}

现在如果需要 把 晴天 这个天气情况修改为 高温天,那么就需要修改两处地方,在实际项目中可能更多。


所以这里必须要定义枚举提高代码的可维护性


4.定义枚举


定义枚举类如下


public enum WeatherType {  
SUNNY("晴天"),
CLOUDY("阴天"),
LIGHT_RAIN("小雨"),
THUNDERSTORM("雷雨"),
UNKNOWN("未知天气");

private final String message;

WeatherType(String message) {
this.message = message;
}

public String getMessage() {
return message;
}
}

将代码用枚举结合switch case来替换


public void handleWeather(String weather) {  
WeatherType weatherType = WeatherType.valueOf(weather);
switch (weatherType) {
case SUNNY:
System.out.println("做好防晒");
break;
case CLOUDY:
System.out.println("户外活动");
break;
case LIGHT_RAIN:
System.out.println("带雨伞");
break;
case THUNDERSTORM:
System.out.println("避免户外活动");
break;
case UNKNOWN:
System.out.println("未知天气");
break;
}
}

5.结语


在日常工作中,会有很多状态类型的字段,比如淘宝订单,状态可以为:待付款、待发货、已发货、已签收、交易成功等,真实场景状态可能更多。


而状态也会被很多代码给使用到,所以必须通过集中统一的方式来定义。


通过常量、枚举,可以很好的解决问题,一旦状态有新增、修改、删除都只需要修改一处地方,其它代码直接引用就行。


作者:CoderMagic
来源:juejin.cn/post/7273875079657160743
收起阅读 »

什么?这个 App 抓不到包

有个朋友说,某个车联网的 app 竟然抓不到包,让我帮忙看下,行吧,那就看下。 样本:byd海洋(v1.4.0) 小试牛刀 先用 Charles软件试试,使用这个软件抓 App 的包,有几个前提: 手机端要配置好Charles的证书 电脑端安装好 Cha...
继续阅读 »

有个朋友说,某个车联网的 app 竟然抓不到包,让我帮忙看下,行吧,那就看下。



  • 样本:byd海洋(v1.4.0)


小试牛刀


先用 Charles软件试试,使用这个软件抓 App 的包,有几个前提:



  • 手机端要配置好Charles的证书

  • 电脑端安装好 Charles 的根证书,并信任

  • 手机端证书要放到系统证书目录 (Android 系统 7.0 一下,这一步可以省略)


没配置好的话是抓不到 https 请求的,环境配置好后先抓个 https 的包测试一下
image.png
可以正常获取到数据,说明抓包环境配置成功。
现在对目标 app 抓包,看下是否成功,结果如下图
image.png
可以发现,并没有成功,为什么会这样呢🤔


为什么抓不到App包


目前App用到的网络请求库都是 OKHttp3,这个库有一个 api:CertificatePinner这个 API 的作用是



用于自定义证书验证策略,以增强网络安全性。在进行TLS/SSL连接时,服务器会提供一个SSL证书,客户端需要验证该证书的有效性以确保连接的安全性。CertificatePinner允许你指定哪些SSL证书是可接受的,从而有效地限制了哪些服务器可以与你的应用程序通信。


具体来说,CertificatePinner允许你指定一组预期的证书公钥或证书固定哈希值(SHA-256),以便与服务器提供的证书进行比较。如果服务器提供的证书与你指定的公钥或哈希值不匹配,则连接会被拒绝,从而防止中间人攻击和其他安全风险。



我们大胆的猜测一下,目标 App 就是用到了这个 API,添加了自定义的证书验证策略,我们既要大胆猜测,又要小心验证,怎么验证呢?这就要用到 Frida 了,用Frida Hook 这个 API,使其失效。Frida 代码如下


try {

var CertificatePinner = Java.use('okhttp3.CertificatePinner');

quiet_send('OkHTTP 3.x Found');

CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function () {

quiet_send('OkHTTP 3.x check() called. Not throwing an exception.');
}

} catch (err) {

// If we dont have a ClassNotFoundException exception, raise the
// problem encountered.
if (err.message.indexOf('ClassNotFoundException') === 0) {

throw new Error(err);
}
}

验证了一下,发现还是抓不到包。
除了这个 API 可以用来防止中间人攻击,OKHttp3还有其他防止中间人攻击的方法,如X509TrustManager



X509TrustManager 是Java安全架构中的一个接口,位于 javax.net.ssl 包中,主要用于处理 SSL(Secure Sockets Layer)和后续的 TLS(Transport Layer Security)协议中的证书信任管理功能。在基于SSL/TLS的网络通信中,特别是HTTPS连接,X509TrustManager扮演着至关重要的角色,它的主要职责是验证远程主机提供的X.509证书链的有效性。



同样,再用 Frida 验证一下,是不是X509TrustManager导致的抓不到包,代码如下


  var TrustManager;
try {
TrustManager = Java.registerClass({
name: 'org.wooyun.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {
},
checkServerTrusted: function (chain, authType) {
},
getAcceptedIssuers: function () {
return [];
}
}
});
} catch (e) {
quiet_send("registerClass from X509TrustManager >>>>>>>> " + e.message);
}


// Prepare the TrustManagers array to pass to SSLContext.init()
var TrustManagers = [TrustManager.$new()];

try {
// Prepare a Empty SSLFactory
var TLS_SSLContext = SSLContext.getInstance("TLS");
TLS_SSLContext.init(null, TrustManagers, null);
} catch (e) {
quiet_send(e.message);
}

send('Custom, Empty TrustManager ready');

// Get a handle on the init() on the SSLContext class
var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom');

// Override the init method, specifying our new TrustManager
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {

quiet_send('Overriding SSLContext.init() with the custom TrustManager');

SSLContext_init.call(this, null, TrustManagers, null);
};

Frida 执行上面的代码,再看下是否抓到包
image.png
这次就可以顺利的抓到数据了。


抓包工具推荐


虽然利用上面 Frida 的脚本可以成功抓包,但是上面的操作还是略显复杂,况且有的 App 还会有 Frida 检测,操作起来就难度骤升。
这里推荐一个抓包工具 httptoolkit,官网界面如下
image.png
使用起来也很简单



  • 下载安装这个软件

  • 连接手机

  • 选择 Android Device Via ADB选项卡

  • 打开目标应用,开始抓包


看下这个软件的抓包效果
image.png
可以看到,数据都出来了并且不用额外的设置。


Xposed 方案


如果你手机刷了 Xposed,那就很简单了,只需要安装 JustTrustMe++模块就可以了。安装之后,也可以通过 Charles软件直接抓包了。



本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系.


本文涉及到的代码项目可以去 爱码者说 知识星球自取,欢迎加入知识星球一起学习探讨技术。
关注公众号 爱码者说 及时获取最新推送文章。



作者:平行绳
来源:juejin.cn/post/7374665776537567286
收起阅读 »

前端大师课:“鬼剑士,听我指令,砍碎屏幕”是怎么实现的?

web
前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。那这种效果如...
继续阅读 »

前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。
那这种效果如果让我们技术来做:
1.要怎么实现呢?
2.有几种实现方法呢?
3.关键代码能给我看看吗?

方案简述

为了提供更详细的解析,我们可以进一步探讨“地下城与勇士手游”(DNF手游)在微信聊天中实现“鬼剑士,听我指令,砍碎屏幕”这一互动特效的可能技术细节。虽然没有直接的源码分析,我们可以基于现有的技术框架和前端开发实践来构建一个理论上的实现模型。

前端监听设计

  • 关键词识别: 微信聊天界面的输入检测可能是通过前端JavaScript监听input事件,配合正则表达式匹配用户输入的关键词(如“鬼剑士,听我指令,砍碎屏幕”)。一旦匹配成功,就向后端发送请求或直接触发前端动画逻辑。

后端交互

  • 请求处理: 用户输入关键词后,前端可能通过Ajax请求或WebSocket向服务器发送一个事件。服务器确认后,返回一个响应,指示前端继续执行动画展示或直接携带福袋奖励信息。

前端动画实现

  • 动画序列: 利用HTML5 元素或WebGL技术,开发者可以创建复杂的2D或3D动画。对于“砍碎屏幕”的效果,可能事先设计好一系列帧动画或使用骨骼动画技术来展现鬼剑士的动作和屏幕碎裂的过程。
  • 碎片生成与物理模拟: 通过JavaScript库(如Three.js的粒子系统或matter.js)模拟屏幕碎裂后的碎片效果,包括碎片的随机分布、速度、旋转和重力影响等,增加真实感。
  • 音频同步: 使用Web Audio API同步播放砍击和碎裂的音效,增强用户的沉浸感。

福袋奖励机制

  • • 动画结束后展示福袋: 动画播放完毕后,前端动态插入一个福袋图标或弹窗,作为用户交互元素。这可能是通过DOM操作实现的,如创建一个新的
    元素并应用CSS样式使其表现为福袋。
  • • 点击事件处理: 给福袋元素绑定点击事件,触发领奖逻辑。这可能涉及再次向服务器发送请求验证用户资格,并根据响应展示奖励内容。

优化与兼容性

  • 性能优化: 动画应考虑在不同设备上的流畅度,可能采用分层渲染、帧率限制、资源按需加载等策略。
  • 跨平台兼容: 确保在微信内置浏览器上的表现良好,需要对微信环境下的特定API和限制有深入了解,比如微信小程序的Canvas组件和其特定的适配要求。

安全与隐私

  • 数据保护: 在处理用户交互和服务器通信时,确保遵循数据保护法规,比如加密传输敏感信息,避免泄露用户隐私。

综上所述,这个互动特效的实现是一个从用户输入监测、前后端交互、动画设计与渲染、到用户反馈与奖励领取的全链路流程,需要综合运用多种前端技术和良好的产品设计思路。

微信聊天界面元素震动效果设计及API应用

虽然微信没有直接公开针对UI元素震动的特定API,但在微信小程序或基于微信环境的H5游戏中设计类似聊天界面元素的震动效果,利用一些基础的动画技术和微信小程序提供的动画库来模拟这种效果。比如通过CSS动画与微信小程序的动画接口来实现这一功能。以下是两种主流实现方式:

1. CSS动画实现震动效果(H5环境)

核心概念

  • @keyframes: CSS的关键帧动画,用于定义一个动画序列中不同时间点的样式变化。
  • transform: CSS属性,用于改变元素的形状、大小和位置。其中,translateX()用于水平移动元素。

实现步骤

  1.  定义动画样式:在CSS中,创建一个名为.shake的类,利用@keyframes定义震动序列。动画包括了元素在原位置与左右轻微偏移之间的快速切换,营造出震动感。

    .shake {
      animation: shake 0.5s/* 动画名称与持续时间 */
      transform-origin: center center; /* 设置变换中心点 */
    }

    @keyframes shake {
      0%100% { transformtranslateX(0); } /* 开始与结束位置 */
      10%30%50%70%90% { transformtranslateX(-5px); } /* 向左偏移 */
      20%40%60%80% { transformtranslateX(5px); } /* 向右偏移 */
    }

2. 应用动画:在JavaScript中,通过动态添加或移除.shake类到目标元素上,触发这个震动动画。

2. 微信小程序wx.createAnimation实现震动

核心概念

  • wx.createAnimation: 微信小程序提供的动画实例创建方法,允许更精细地控制动画过程。
  • step() : 动画实例的方法,用于生成当前动画状态的数据,用于在setData中更新视图。

实现步骤

  1. 初始化动画数据:在Page的data中定义一个空的animationData对象,用于存储动画实例导出的状态数据。

    data: {
      animationData: {},
    },

2. 创建震动动画逻辑:定义一个函数,如shakeElement,使用wx.createAnimation创建动画实例,并定义震动序列。通过连续的translateX操作模拟震动,然后通过step()函数记录每个阶段的状态,并通过setData更新到视图上。

```
shakeElement: function () {
  let animation = wx.createAnimation({
    duration: 300// 动画持续时间
    timingFunction: 'ease'// 动画速度曲线
  });

  // 震动序列定义
  animation.translateX(-5).step(); // 向左偏移
  this.setData({ animationData: animation.export() });
  setTimeout(() => {
    animation.translateX(5).step(); // 向右偏移
    this.setData({ animationData: animation.export() });
  }, 100);
  setTimeout(() => {
    animation.translateX(0).step(); // 回到原位
    this.setData({ animationData: animation.export() });
  }, 200);
},
```

3. 应用动画数据:在WXML模板中,为目标元素绑定动画数据。

```
style="{{animationData}}" class="your-element-class">震动的文字或图标
```

注意事项与最佳实践

  • 性能监控:频繁或长时间的动画可能影响应用性能,尤其是低配置设备。适时停止或限制动画触发频率。
  • 用户体验:震动效果应适度且符合用户预期,过度使用可能造成用户反感。
  • 跨平台兼容性:虽然上述方法主要针对微信环境,但在实现时也应考虑浏览器的兼容性问题,特别是对于H5应用。
  • 动画细节调整:根据实际需求调整震动幅度、频率和持续时间,以达到最佳视觉效果。

动手能力强的你,可以去试试,下一节,将讲一个具体的demo给大家演示一下哈。


    作者:蜡笔小新爱学习
    来源:juejin.cn/post/7371423076661542952
    收起阅读 »

    运营:别再让你的页面一直loading 了

    运营:别再让你的页面一直loading 了 第一轮 battle Q: 我想下载一个大文件,界面一直转圈,很耽误时间,我想在下载的时候还做点其他事情 A:一直转圈就一直等呗,反正还能摸会(奈何小姐姐太想做牛马了) 第二轮 battle Q: 不行,为什么别人...
    继续阅读 »

    运营:别再让你的页面一直loading 了


    May-17-2024 15-36-38.gif


    第一轮 battle


    Q: 我想下载一个大文件,界面一直转圈,很耽误时间,我想在下载的时候还做点其他事情


    A:一直转圈就一直等呗,反正还能摸会(奈何小姐姐太想做牛马了)


    第二轮 battle


    Q: 不行,为什么别人的浏览器,下载软件/文件 就能操作界面,你这就一直转圈,什么都做不了


    A: 我们js 是单线程,一个时间只能做一件事,你不能在下载文件的时候,还操作界面吧...逐渐语无伦次,行,我给你试着优化优化..


    image.png


    最终效果


    save.gif


    无敌.gif


    可以看到,下载文件 页面不再转圈,并且可以在界面操作,但是在点击操作1,2,到3的时候,会卡顿一下,下面会说为什么会卡这一下


    开始分析



    1. 执行文件下载操作,把转圈逻辑去掉不就行了,


    but: 是不转圈了,下载的时候,依然操作不了界面



    1. js 是一个单线程,一个时间只能做一件事,密集的cpu 计算,导致网站反应迟钝,就像卡了一样


    resolve: 把下载文件这个耗时操作,放在其他线程操作,等到操作完毕,再通知主线程,执行完了。就像发布订阅模式一样,主线程不用执行密集的计算,也不用特意等密集计算的结果,执行完,告诉我就行了


    技术使用 Web Workers


    摘自 MDN developer.mozilla.org/zh-CN/docs/…


    Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,它们可以使用 XMLHttpRequest(尽管 responseXML 和 channel 属性总是为空)或 fetch(没有这些限制)执行 I/O。一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。


    为什么要用它:worker 的一个优势在于能够执行处理器密集型的运算



    不会阻塞 UI 线程


    不会阻塞 UI 线程


    不会阻塞 UI 线程


    不会阻塞 UI 线程


    重要的事情说三遍 🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣


    基本使用


    主线程生成一个专用 worker


    const myWorker = new Worker("worker.js"); // worker.js 是一个脚本的 URI 来执行 worker 线程

    专用 worker 中消息的接收和发送


    就俩主要方法 postMessage onmessage


    引入脚本与库


    Worker 线程能够访问一个全局函数 importScripts() 来引入脚本,该函数接受 0 个或者多个 URI 作为参数来引入资源;以下例子都是合法的:


    importScripts(); /* 什么都不引入 */
    importScripts("foo.js"); /* 只引入 "foo.js" */
    importScripts("foo.js", "bar.js"); /* 引入两个脚本 */
    importScripts("//example.com/hello.js"); /* 你可以从其他来源导入脚本 */

     ESModule 模式


    const worker = new Worker('worker.js', 
    { type: 'module' // 指定 worker.js 的类型 }
    );

    文件下载代码



    • baseCode


    import { writeFile, utils } from 'xlsx'
    /**模拟生成大文件数据 */
    const generateLargeFileData = () => {
    const data = []
    for (let i = 0; i < 10000; i++) {
    data.push({
    id: i + 1,
    name: `User ${i + 1}`,
    email: `user${i + 1}@example.com`,
    age: Math.floor(Math.random() * 100) + 1
    })
    }
    return data
    }


    • 一只转圈的代码


    /**下载大文件 */
    const downloadExcel = async () => {
    // 模拟生成大文件数据
    const data = generateLargeFileData()
    loading.value = true
    // 模拟一段短暂的等待时间,确保状态更新
    await delay(1000)
    // 卡死的罪魁祸者
    // 将数据转换为 Excel 格式
    const ws = utils.json_to_sheet(data)
    const wb = utils.book_new()
    utils.book_append_sheet(wb, ws, 'Sheet1')
    writeFile(wb, 'test.xlsx')
    loading.value = false
    }


    • 使用webworker,将耗时计算放到 webworker 线程,解决阻塞ui的问题


    主线程



    const myWorker = new Worker('downloadWorker.js')
    myWorker.onmessage = (event) => {
    let wb = event.data
    // 这里也会占用主线程的ui渲染,所以会卡一下
    writeFile(wb, 'test.xlsx')
    ElMessage.success('下载任务已在后台运行,可以继续操作界面其他任务')
    }

    /**下载大文件 */
    const downloadExcel = async () => {
    const data = generateLargeFileData()
    myWorker.postMessage(data)
    }


    worker 线程


    image.png


    // 非模块化文件, public 打包本身就是线上文件了
    importScripts("./xlsx.js"); // 线上地址,或者本地地址

    self.onmessage = (e) => {
    // 将数据转换为 Excel 格式
    const ws = XLSX.utils.json_to_sheet(e.data)
    const wb = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
    // writeFile(wb, 'test.xlsx') // 这里会操作dom, 所以将操作dom放到 主线程做
    self.postMessage(wb)
    self.close()
    }

    细节补充



    1. 本文主要介绍了专用worker,其实还有 共享 worker【主要做多页面标签通信】, ServiceWorkers 【主要做网络拦截,可以看一下之前写的pwa文章【https://juejin.cn/post/7062681470116036616】,离线缓存就是使用ServiceWorkers】

    2. 在主线程中使用时,onmessage 和 postMessage() 必须挂在 worker 对象上,而在 worker 中使用时不用这样做。原因是,在 worker 内部,worker 是有效的全局作用域(就像window.xxx ,window 一般可以不写)

    3. worker的关闭


    // main.js(主线程)
    const myWorker = new Worker('/worker.js'); // 创建worker
    myWorker.terminate(); // 关闭worker

    // worker.js(worker线程) 
    self.close(); // 直接执行close方法就ok了


    1. worker 错误监听 messageerror

    2. 关于主线程里的 new Worker('downloadWorker.js')


    这个脚本,必须是本地/或者网络地址,这里写的是项目运行地址 匹配相应的worker。这里大家也会发现一个问题,就是这个worker是全局性的,放在public 是一个不错的选择,再者打包后,public 下本身也是会放在服务器上



    1. 用完worker, 要及时关闭,他是不会自己结束的。选择 在worker 关闭,或者主线程关闭,会有区别

    2. 其实小文件下载,用worker 有点画蛇添足,本身使用worker 也是一种消耗



    详细的参考资料以及代码地址


    MDN



    MDN code仓库



    可以下载下来直接调试,最好是起一个本地服务: http-server


    image.png





    代码地址


    gitee.com/Big_Cat-AK-…





    作者:赵小川
    来源:juejin.cn/post/7369633749418934335
    收起阅读 »

    MybatisPlus 使用技巧与隐患

    前言 MP 从出现就一直有争议 感觉一直 都存在两种声音 like: 很方便啊 通过函数自动拼接 Sql 不需要去 XML 再去使用标签 之前一分钟写好的 Sql 现在一秒钟就能写好 简直不要太方便 dislike: 侵入 Service 层 不好维护 可读性...
    继续阅读 »

    前言


    MP 从出现就一直有争议 感觉一直 都存在两种声音


    like:


    很方便啊 通过函数自动拼接 Sql 不需要去 XML 再去使用标签 之前一分钟写好的 Sql 现在一秒钟就能写好 简直不要太方便


    dislike:


    侵入 Service 层 不好维护 可读性差 代码耦合 效率不行 sql 优化比较难


    之前也有前辈说少用 MP 理由就是不好维护 但是这个东西真的是方便 只要不是强制不让用 就还是会去使用 存在集合里 最近也确实有一些体会 就从两个角度去看一下 MP


    优点


    操作简洁


    就从我们编码中最常用的增删改查去说


    按照我们之前去使用 Mybatis 的喜欢我们就要去建立一个 XML 文件 去编写 Sql 语句 算是半自动 我们可以直接去操控 Sql 语句 但是会比较麻烦 很多简单的数据查询我们都要去写一个标签 感觉这种没有意义的操作还是比较烦的 那么 MP 里面怎么实现


    第一种: 最简单我们就是直接去使用提供的方法 我们非常简单就能做到这些操作 但是这个就有一个问题


    nodeMapper.selectById(1);
    nodeMapper.deleteById(2);
    nodeMapper.updateById(new Node());
    nodeMapper.insert(new Node());

    维护性差 以查询为例 这个默认提供的方法都是查询所有字段我们都知道在编写 Sql 的时候第一条优化准则就是不要使用 Select * 因为这种写法是很 Low


    这个就是上面selectById执行的结果


    SELECT Id,name,pid FROM node WHERE Id=?

    这种 Sql 肯定是不好的所以我们在使用 MP 的时候尽量不要去使用自带的快捷查询 我们可以去使用它里面的构造器


    nodeMapper.selectOne(new QueryWrapper().eq("id",1).select("id"));

    这汇总写法 我们可以通过后面的 select() 去指定我们需要查询的字段 算是解决上面那个问题吗 但是这个就完事了吗? 这还有一个问题


    我们在开发中经常会说一个叫魔法值的东西


    //这个就是魔法值 
    if ("变成派大星".equals(node.getName())){
       System.out.println("魔法值");
    }

    之所以不要多用魔法值就是为了后期维护 我们建议使用枚举 或者建一个常量类 通过 Static final 修饰


    上面那段代码是不是也有同样问题 "id"算不算魔法值呢 这种构造器产生的问题就是 不好维护


    假设 我们的这Node类是高度使用的 我们到处都在写


    nodeMapper.selectOne(new QueryWrapper().eq("id",1).select("id"));

    刚开始没事 我们乐呵呵的 但是一旦我去修改 Id 的字段名怎么办



    我修改成 test(数据库同步修改) 现在这个实体类中没有这个字段 我们再去看我们的代码



    没有什么反应 没有给我提示报错 我这个时候去运行怎么办 我要一个个去找这个错误吗 这明显很费时间


    这个确实是一个问题 但是也是可以解决的


    Node node = nodeMapper.selectOne(new LambdaQueryWrapper().eq(Node::getId, 1).select(Node::getId));

    上面这种代码就可以去解决这个问题 我们在使用的时候可以多用这个东西



    一旦修改字段就会立马报错


    但是 这就万事大吉了吗 NO No NO 我们要是处理稍微复杂的语句怎么办? 比如如我们字段求和 这个 LambdaQueryWrapper 还是存在限制的


    如果我们想实现这种 怎么去做呢


    select SUM(price_count) from  bla_order_data LIMIT 100

    首先这种写法肯定是不太行的 编译不通过



    除非去使用QueryWrapper



    还有就是分页查询


    // 条件查询
    LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserInfo::getAge, 20);
    // 分页对象
    Page queryPage = new Page<>(page, limit);
    // 分页查询
    IPage iPage = userInfoMapper.selectPage(queryPage , queryWrapper);
    // 数据总数
    Long total = iPage.getTotal();
    // 集合数据
    List list = iPage.getRecords();

    这个还是非常简单的


    简单总结


    MP 在做一些简单的单表查询可以去使用但是对于一些复杂的 SQl 操作还是不要用


    1、SQL 侵入 Service 的问题我们可以仿照 Mybatis 建一个专门存放 MP 查询的包


    2、关于维护性 我们可以尽量去使用 LambdaQueryWrapper 去构造


    3、MP 是有内置的主键生成策略


    4、内置分页插件:基于 Mybatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询。


    缺点


    我就说一个最大的缺点就是对于复杂 Sql 的操作性很不舒服 比如我们去多表查询 你怎么去写呢


    看一个例子




    就是通过


    @Select 注解

    Mp的查询条件嵌入进去
    ${ew.customSqlSegment}


    咱就是一整个大问号 联表老老实实去写 XML 吧 这种真的不要去用 太丑了


    总结


    没有过多的东西 基本都是最近看到的东西


    1、复杂语句不推荐使用 MP 能用最好也别用 可读性差 难维护 使用刚开始没感觉 后期业务扩充 真的恶心的


    2、可以使用 MP 中的分页 比较舒服 逐渐生成策略也舒服


    3、尽量不要去使用 MP 中自带的selectById 等全表查询的方法


    4、尽量使用LambdaQueryWrapper的书写形式 至少比较好维护


    5、简单重复 Sql 可以用 MP。复杂 SQL 不要用




    作者:臻大虾
    来源:juejin.cn/post/7265624177774854204
    收起阅读 »

    面试官喜欢什么样的离职原因?

    hi,你好,我是猿java 面试中,我们似乎总是会被问到一个敏感的话题:你从上家公司离职的原因是什么?如何机智地回答才能获得面试官的芳心呢?今天我们一起来聊一聊。 1.为什么会问离职原因? 马云曾经说过,人离职的原因主要有两种:一是钱给少了,一是心委屈了!既然...
    继续阅读 »

    hi,你好,我是猿java


    面试中,我们似乎总是会被问到一个敏感的话题:你从上家公司离职的原因是什么?如何机智地回答才能获得面试官的芳心呢?今天我们一起来聊一聊。


    1.为什么会问离职原因?


    马云曾经说过,人离职的原因主要有两种:一是钱给少了,一是心委屈了!既然都已经是一个公开的问题,为什么面试官(特别是 HR)还是喜欢提起它?


    其实,面试官问这个问题,绝大情况下是常规操作,担心你入职后会不会也因同样的原因离职;另一个原因是担心小部分人有违法乱纪行为被开除,所以有必要提前筛选一下。


    作为打工人,对于离职原因,我们不鼓励欺骗,但绝对可以高情商回答,让那些看起来对我们不利的离职原因,可以表达得更能够被面试官接受,因此,本文总结了以下几个离职原因的表达方式:


    2.常见的离职原因


    这里,我们列举了几个最常见的离职理由,然后给出了低情商和高情商两种回答,假如你是面试官,也能快速作出判断,最终选择谁!


    2.1 加班太严重


    卷似乎已经成了国内上班的代名词,为了找到一份好工作,即便加班虐你千百次,你也要在面试官面前“违心”地表现出我待加班如初恋的态度,除非你不care这份工作或者有资本对加班NO。那么,面对加班太严重而离职,我们该如何违心地回答呢?


    🚫 低情商回答:前公司加班太严重,太卷了,是头牛都受不了,我实在是卷不动,想躺平。


    ✅ 高情商回答:面试官,您好! 在上家公司,我能高效高质量地完成工作,但公司总会把加班时长作为一个重要的考核指标,导致很多员工为了加班而加班,效率低下,我不反对加班,但是不太认同这种低效的卷,我希望为更人性化的公司创造更大的价值。


    2.2 薪资太低


    雇佣关系绝大多数是建立在合理的薪酬之上,如果工作内容或者强度和薪资严重失衡却得不到调整,那么,离职的概率就会大大增加。因此,面对薪资太低离职,我们该如何回答?


    🚫 低情商回答:上家公司给的工资太低了,而且一直没有调薪,做得没有动力,所以离职了。


    ✅ 高情商回答:面试官,您好! 我过去 3年,在公司和领导的帮助以及自身的努力下,技术能力有了质的提升,为公司做了很多降本增效的项目,领导一直很认可我,可是公司的薪资结构有一些硬指标,无法满足我的涨薪需求,所以想看看新机会,寻找一个可以长期稳步发展的平台。


    2.3 领导很low


    职场上,并不是每个领导都擅长管理,每个领导爬上去的原因也不尽相同,所以,如果职场上遇到优秀的领导请好好珍惜,如果遇到所谓的low领导,也不用太撕破脸,反而需要更加修炼自己的职场软技能,学会和不同的人打交道,因此,遇到low领导而离职该如何表达?


    🚫 低情商回答:前领导太 low,对管理一窍不通,只会吹嘘拍马,跟着他看不到前途,所以离职。


    ✅ 高情商回答:面试官,您好! 因为前公司的业务过于稳定,大部分人每天工作都是在重复,我希望进入一个持续学习和提升的平台,接受更多的挑战。


    2.4 被裁员


    口罩事件之后,裁员弥漫在每一个公司的角落,恐惧也充斥着每个打工人的心里,曾经香饽饽的互联网,如今裁员潮不断,而这些裁员不管你能力有多突出,加班有多厉害,只是纯粹的不赚钱,所以,面对裁员,我们要如何表达自己的无辜?


    🚫 低情商回答:我在之前公司的 KPI不行,被公司裁员了。


    ✅ 高情商回答:面试官,您好! 前公司业务有很大的调整,想让我调岗到其他业务线上,而我个人很想在xxx领域深耕,贵公司的这个岗位和我现在的很匹配,很符合我的职业规划。


    2.5 无法晋升


    晋升在一定程度上是对员工的认可,同时也是改善待遇的窗口,如果一直都在努力付出却得不到晋升机会,难免会打击工作的积极性,如果,当工作了几年之后,能力提升了很多,却一直得不到晋升,这种离职理由该如何来表达?


    🚫 低情商回答:我在前公司做牛做马这么多年,结果晋升的都是老板的亲信,晋升无门,我只能选择离职。


    ✅ 高情商回答:面试官,您好! 我在前公司一直很受领导重用,负责过多个核心业务,领导多次提名我晋升,但都被突发原因夭折了,而我不想安于现状,想找一个更能发挥自己才能的平台。


    2.6 和同事相处不和谐


    职场上,最重要的事情就是与人相处,和同事一起成事,与人打交道绝对是一门终身的必修课,如果是因为和同事相处不和谐离职,如何才能更好地表达出不是因为自己不能融入集体?


    🚫 低情商回答:前公司的同事们都很奇葩,处不到一个好朋友,太孤单了所以离职。


    ✅ 高情商回答:面试官,您好! 我在前公司的沟通能力一直被领导认可,但因为公司内耗较大,很多工作都花在无效的环节上,所以希望找一个氛围好,内耗低的团队长期发展。


    通过上述低情商和高情商的回答对比,我们就可以很清晰的感受到语言的魅力,同样一个理由,在不说谎的前提下,只要我们可以表达的更好,面试官很多也是打工人,他们也能共情到,所以,如果你能高情商的回答,只要不是技能能力不过关,一般也很难被面试官淘汰。


    3.一些现象和建议


    经历过口罩事件后经济下行的这几年,领悟了很多,也看到了很多奇怪的现象:


    职场上,没有谁是不可替代的


    求职时,很多面试官都是曾经和自己一起奋斗的同事


    入职后,发现直属领导或者更上级的领导,曾经是下属


    给予援助之手的,绝大多数是曾经和你关系好或者认可你的人


    给打工人的一些建议:


    选择了一份工作就要全力以赴,不遗余力的提升自己,不要太计较外在因素,这些拼搏绝对是你日后无形的财富。


    日常工作中,一定要和同事保持友善互助的关系,尽量交往几个能相互正向提升的同事,日后他们可能就是你的贵人。


    维护好与领导的关系,不论他优秀与否,相互磨合,相互成就,说不定他日后就可以帮到你。


    如果你是领导,请善待自己的下属,这个时代变化太大,三十年河东,三十年河西,与人方面就是与己方便。打工人需要相互帮扶!


    最后,每个人的阅历不一样,上面 4个建议不一定每条都适合,但是绝对有一个可以受益。




    作者:猿java
    来源:juejin.cn/post/7375086929404837914
    收起阅读 »

    入职第一天,看了公司代码,牛马沉默了

    入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
    继续阅读 »

    入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

    4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

    打开代码发现问题不断

    1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

    image.png

    image.png

    image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

    prop_c.setProperty(key, value);

    value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

    public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
    throw new NullPointerException();
    }
    }
    1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
    role.haveRole("ADMIN_USE")
    1. 日志打印居然sout和log混合双打

    image.png

    image.png

    先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

    4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

    5.随意更改生产数据库,出不出问题全靠开发的职业素养;

    6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

    <type>pom

    来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

    以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

    那有什么优点呢:

    1. 不用太怎么写文档
    2. 束缚很小
    3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

    解决之道

    怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

    其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

    我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


    作者:小红帽的大灰狼
    来源:juejin.cn/post/7371986999164928010
    收起阅读 »

    从江西到北京2000公里 在少年得到找到了第一份归宿 追求自己的北漂梦

    2000公里来到北京,追求自己的北漂梦 哈喽哈喽,大家好,我是你们的金樽清酒。在经历了很多的面试之后,我找到了第一份实习,从江西到北京两千公里,也就是一张车票和一份努力。这篇文章可能不带太多的技术性,都是我个人到北京到公司的一些感悟。 火急火燎的入职 收到公司...
    继续阅读 »

    2000公里来到北京,追求自己的北漂梦


    哈喽哈喽,大家好,我是你们的金樽清酒。在经历了很多的面试之后,我找到了第一份实习,从江西到北京两千公里,也就是一张车票和一份努力。这篇文章可能不带太多的技术性,都是我个人到北京到公司的一些感悟。


    火急火燎的入职


    收到公司的的offer之后,确定入职的时间,我最快的入职时间就是学校的考试之后,四点多考完之后,收拾收拾就开始赶高铁。从抚州到南昌,再到南昌坐地铁到火车站,说实话,这是我第一次坐地铁,蛮新奇的,后面也基本每天跟地铁打交道了,可能这是从小城市来到大城市的新奇感吧。


    第一次坐上火车。怎么说呢,一言难尽,只能用彻夜难眠来解释。火车上人声的嘈杂,颠簸以极其难闻的味道,但是怎么说,内心也有点激动,毕竟火车的终点是大都市北京。到了北京,前往在网上看好的房子,一个小单间,怎么说,初到北京只能住小单间了,但是老实说我还住过更差的房子,在暑假做家教的时候,一个没有窗户的暗无天日的小房子,关灯就是黑夜,暗无天日,所以说在北京的房子我感觉还好啦。由于赶着入职,把行李给房东就赶着去公司了,一身的疲惫。


    坐地铁去公司。老实说,早高峰的北京地铁,就是脸贴脸的那种。以后,每次坐地铁都是站在门口。由于不认识路,一路误打误撞来到公司办入职。各种的入职手续,领了电脑之后就被hr带到工位。老实说,在北京也就公司能给我一点安慰。我们公司没有打卡,完成自己手头上的事情就可以下班,一切都是约定俗成但是井然有序。公司里面的人也特别的随和,哈哈哈哈哈哈,第一天来做了好多个自我介绍。


    然后,我就认识了我的老师,应该叫mentor吧。


    mentor对我的教诲


    第一天mentor交给我的任务就是配置环境。说实话,这是我第一次配置环境。mentor说自己配环境的机会不多,你想想自己配置环境的机会有多少。我就开始按照网上的教程一步步的摸索如何配置node,怎么用n来管理node的版本。
    第一天,mentor给我单独开了个会,列出了我这一周要学习的东西。我都拿小本本记下来了。


    WechatIMG1.jpg


    WechatIMG2.jpg


    从配置环境到运行项目到开发流程。然后我就开始了开发的前置准备。


    第一天,我只完成了环境的配置。然后就要把项目拉去下来运行项目了。当天开了四个会,一个早会,一个周总,一个部门的技术分享,还有一个是mentor叫我留下来,总结今天的学习。然后就给我解决了项目运行不了的问题,原来是我的node版本过低,为什么要用n或者nvm进行node的版本管理呢?因为不同的项目依赖的node版本不一样。然后又交代给我一个任务,找到一个页面,看懂里面的每一行代码,讲给他听。然后mentor就会把里面我不会的讲解给我听。然后mentor拉我去开评审和复述的会,开始写项目排期了。排期是一项技术活,不能太长或太短,不要给自己留空窗期,以及不能到测试时间完不成任务,他给我盯着排期呢。以后自己排期可是一项技术活。


    现在是开发的第二天了,每天都要review今天的代码,mentor给我指出了很多改进的地方。代码书写的规范,以及写注释,性能优化等。比如一些v-if可以用v-show。为什么,因为可以减少回流重绘。还好,九天的任务,我二天就完成一大半,到联调的阶段了,但不是我有多强,而是我在暑假的时候实习过一个公司写过后台管理,以及mentor叫我看懂每一行代码和element-ui组件库,没开玩笑我现在强的可怕。加油,成长的每一天。


    在北京的难点


    资金窘迫。去的火车票,押一付一的房租,以及提前15天交租,以及各种七七八八的,还得来回学校处理一些事情,来来回回,七七八八,说实话还没赚钱真的喘不过气。有钱男子汉,没钱汉子难啊。每天要挤的地铁,在外面的孤独感。以及不合口味的饮食。北京偏甜,我是江西萍乡的,属于很能吃辣的,到北京感觉都是甜的。mentor知道我吃辣,给我一罐辣酱,说很辣,他蘸一点点都辣的不行。我吃了,额,甜的,一点辣味都没有。唉,生活总是如此的艰难,还好公司不打卡,能学到很多东西,可以摸鱼。加油吧,开心点向前看,在这个世界上谁不难呢,身边基本都是北漂,可以说北京很包容啦。


    总结


    初到北京很激动,我很高兴到少年得到这家公司,我会在这家公司好好实习,提升自己的能力,完成自己的北漂路,人生艰难,但是依旧值得。给你们看看我的公司吧,我还看到了泉灵老师哦,跟我们一起上下班。


    WechatIMG3.jpg


    WechatIMG4.jpg


    WechatIMG5.jpg


    WechatIMG6.jpg


    作者:jinzunqinjiu
    来源:juejin.cn/post/7371633297154621494
    收起阅读 »

    小毛驴 40km 通勤上班:不一样的工作日!

    从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。 所以通勤方案就变成了: 上班: 小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min + 步行 5min 下班: 公交 12min ----- 地铁 ...
    继续阅读 »

    从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。


    所以通勤方案就变成了:


    上班:

    小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min + 步行 5min

    下班:

    公交 12min ----- 地铁 40min ----- 小毛驴 15min

    通勤费用: 小毛驴一块钱充电可以开两天。地铁 + 公交 来回 12块。


    这半年下来地铁已经坐够够了。🤦‍♂️ 有的时候实在是不想坐了。就动了开小毛驴的心思。


    但是百度地图看从家到公司的距离是 34km。之前公司到家的百度距离是 18km,其实等于翻翻了。


    而且之前的路况很好么有什么红绿灯而且路上的人也很少。所以基本没有什么时间浪费18km大概半个小时左右就到了。


    本来是想直接买一个新电瓶车来通勤用的,但是碰到那个什么新国标要去考摩托车驾-照就耽搁了。


    然后正好这两天天气还行不冷不热。我就想要买今天就开小毛驴去公司得了。正好熟悉下路况。


    早上还是按照正常出门的时间 7.25 出门。然后按照百度导航直接走。因为第一次开,路况不熟悉。按照百度走的路线全是走的人多的地方。早上正好又是上班高峰期。非机动车道上全部都是人。而且路上的红绿灯贼多。基本遇到一个红绿灯就要停下来。


    前半程车的电量充足速度可以很快,但是路况太差了。路上人太多,而且有占着超车道一直慢悠悠的。开的血压飙升。所以就导致速度起不来。然后到了后半程的时候全是大路。而且没有什么红绿灯也没啥人,但是电量下去了,速度又上不来。脑壳痛!


    最后到公司楼下的时候是 8.42。百度地图显示 34km 需要 2 小时零五分。实际电瓶车里程显示 40km ,耗时一小时 20 分。


    其实 1 小时开车的时间是感知不到的。前半程因为都是人所以精神高度集中。


    另外路上的风景也是不错的。可以走之前没有走到的地方。可以愉快的画图。


    下面早上的时候拍的,因为第一次。怕时间不够。就随便瞎拍了两张记录了一下。


    IMG_20240428_105957.jpg


    IMG_20240428_105852.jpg


    IMG_20240428_110048.jpg


    IMG_20240428_104954.jpg


    IMG_20240428_110017.jpg


    等会晚上回去的时候看看能不能走另外一条路会不会快点。


    IMG_20240427_221436.jpg


    IMG_20240427_221603.jpg


    MVIMG_20240426_192534.jpg


    IMG_20240427_205345.jpg


    IMG_20240427_222136.jpg


    IMG_20240427_221712.jpg


    IMG_20240427_222732.jpg


    IMG_20240427_221628.jpg


    IMG_20240427_221326.jpg


    IMG_20240427_221537.jpg


    作者:执行上下文
    来源:juejin.cn/post/7362729128476524563
    收起阅读 »

    一边敲代码一边晋升宝爸

    2024年农历三月初一,我成功晋升为宝爸。 这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。” 看着这个陌生的、小小的男人,我忐忑的心变得安宁。 在这个世界,我们彼此相遇,有点不知所措,如在梦中。 他嘹亮的啼哭,让...
    继续阅读 »

    成品.jpg


    2024年农历三月初一,我成功晋升为宝爸。


    这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。”


    看着这个陌生的、小小的男人,我忐忑的心变得安宁。


    在这个世界,我们彼此相遇,有点不知所措,如在梦中。


    他嘹亮的啼哭,让我的心弦随之颤动。


    我一动不动的看着他,不敢摸他。


    浓浓的血脉,注定了这一世的羁绊。


    ……


    现在是熊宝出生的第21天了,在护理师的帮助下,我学会了抱娃、拍嗝、换尿布,算是初步顺上手了。


    至此,我可以有些时间写点生娃的回忆和经验,分享给大家。


    我的爱人是在2023年7月份怀孕的。


    清晰记得那天早晨,我爱人跟我说她已经40多天没有来月经了,她可能怀孕了。


    我赶紧在美团买了个验孕棒,一测,出现了两条红线,心中大喜。


    为了万无一失,我又带她去医院检查,医生说:“恭喜你,有喜了。”


    我长吁了一口气,结婚后的一年里,我爱人老担心这事。


    这个时候,我还在山东老家装修房子,想着老来得子,要谨慎一些,所以就计划去北京备产。


    北京消费高,以我当前的经济实力,得找份工作才行。


    所以我就去boss 上看工作,看到滴滴有WebGL工程师的坑,就投了份简历,然后大大小小的面试了五六场才过。


    我工作的地址是中关村壹号,所以,我就近在大牛坊租了个窗户很大的主卧,孕妈的建档就建在了上地医院。


    上地医院不算三甲,但它的整体服务和环境都挺好的,产科是它的特色,看病的人不少,但也不会特别多,一般不需要排太久的队。


    就这样,我开始了白天给公司当孙子,晚上给爱人当孙……,额,是守护神的生活。


    孕初的几个月,我们会每两周去一次医院孕检。什么时候孕检,医生都会提前告知我们,而且孕检手册也会告诉我们整个流程。


    有的孕检是需要验血的,这时就需要空腹。一般我们会很早起床,把水杯和牛肉干装进背包里,然后去医院。


    孕妇饿久了、抽血多了,很容易心慌头晕,再加上孕期情绪敏感。所以,每次孕检我都会陪着她,陪她早去,避免她饿太久,验完血后,我会先给她吃点牛肉干垫垫,然后再去附近找好吃的。


    在之后的时间里,一切还算顺利,熊宝在妈妈的肚子里慢慢的从一颗种子长成鹌鹑蛋、小鸡蛋、小苹果……


    在他有小木瓜那么大的时候,他学会了玩脐带,并且成功的在自己的脖子上缠了一周。


    那段时间,熊宝的小脑延髓池还长得有点快,都0.9了,规定的最大值是1.0,这让我们很是担心。


    我上网查资料说小脑延髓池的值太大会脑子进水,变成一个傻宝。


    幸亏最后都稳定住了,没有再长。


    在孕期第六个月的时候,也就是2023年年底,我们开始考虑胎儿的生产和月子问题。


    我们是没啥经验的,所以想着找个月嫂,或者找个月子中心,房子还得找个至少两居室的,不能再合租了。


    这一堆算下来并不便宜,北京的金牌月嫂是3万,普通的也得2万。中高级的月子中心是7万到10万。两居室的房子8千/月。


    这对于我在滴滴的薪资来说倒也还好,可问题是年底的时候我不想在滴滴干了。


    原因是我所在的HMI部门的管理出了问题,再干下去会很浪费生命。


    思虑再三,我给卡尔动力的老板提了一些建议后,就离职了。


    卡尔动力是我去滴滴时,刚从滴滴脱离出来的创业公司,我在其中负责Web端的三维可视化。


    在我离开公司前,老板把他的微信给我了,我们加了个好友,现在还保持着联系,希望以后会有合作。


    至于我之前所在的那个HMI部门,它在我离开没多久就被打散重组了,组长也被撤了。很佩服老板的雷厉风行。


    其中太具体的事情我就不再多说了,咱们继续谈生娃的事。


    我离开滴滴后,就把大牛坊的房子退了,东西都寄回了老家。


    接下来,我们计划在山东的潍坊老家生娃。


    之所以如此,有多方面的考虑:


    我们已经在北京完成了孕前和孕中的检查,胎儿很健康,所以没必要再纠结于北京。


    潍坊有多个三甲医院,其医疗技术虽然比不得北京,但生娃也不是那种只有一线城市才能做的、技术难度很高的事。


    北京消费高,没较高的收入就不适合再待在北京了,那时我租的房子也正好到期。而潍坊的消费是很低的,我们每天120元就可以在医院对面短租很精致的小米全屋智能公寓。潍坊中高端的月子中心一个月2万。


    我刚好接到了我上上家公司的一个单子,可以在老家工作,小赚一笔。有很多时候,我离开公司,并不一定是我和公司闹掰了,其原因很多的。


    我爸妈在老家自己种着蔬菜,有鸡鹅牛羊,自己家的东西吃着放心。


    就这样,我们在北京待了四个月后,回到了潍坊老家。


    在老家的日子里,孕妈的饮食都挺好的。我们吃东西前,都会从一个叫“孕育树”的APP上查查孕妈能不能吃。


    其实,查一种食物是否适合孕妈吃是很简单的,但难的是你可能会忘了查,或者自以为不用查。


    以前我爱人就有过几次,比如煮鸽子汤的时候放了某一种中药,喝了两三次后才想起查一下,结果发现那种中药容易让孕妇流产;还有一次,我对象以为自己可以吃桂圆,买了一堆后,我给她一查,发现孕妇不能吃。


    孕妈在怀孕的时候,很容易傻傻的可爱,这需要我们多上点心。


    我家孕妈的精神状态一直是我比较担心的,因为她有点熊,熊孩子的熊,再加上有几个词叫“产前抑郁”和“产后抑郁”,所以我会格外注意和防范她的情绪问题。


    我会时刻告诉自己不能让她生气,学会换位思考,照顾好她的方方面面,她说得都对,不因小事而计较,不轴,不抬杠,努力逗她开心。


    除此之外,赚钱也很重要,因为很多时候钱是可以换来快乐和舒适的。


    在这期间,我给我工作过的上上家公司开发了一个三维机器人的交互展示项目,基本上能够后面的开支。


    我还把去滴滴时遇到的面试题做成了一个低价的付费课-《canvas进阶-面试题》,想着以后多多少少给熊宝赚点奶粉钱。


    每天我也依旧会拿出一点时间去学习,让自己保持一个持续成长的状态。


    与此同时,熊宝在妈妈的肚子里也持续成长,长成了一个小西瓜。


    熊宝玩脐带的能力也进步了,他成功把脐带在自己脖子上又绕了一周,成为了绕颈两周。


    直到熊宝的脑袋入盆的时候,还是两周,医生说:“你们别再想让他绕回来了,当然,也不用担心他再绕更多了。”


    在这期间,我们一感觉熊宝不咋蛄蛹了,就赶紧用自己买的胎心仪测测,生怕他因为绕颈两周而缺氧。


    有的时候熊宝很皮,半天不动,胎心还换了位置,我们常常大半夜的测胎心测好久,都快把她妈吓哭了,直到在一个思维盲点听到强劲有力的小火车声,才放下心来。


    在孕妈离预产期还有20天的时候,我们住在了潍坊阳光融合医院对面的一间环境舒适,干净卫生,可以洗衣做饭的小米全屋智能公寓里。


    在这个时候,孕妈基本上就是随时可以生的了,所以我们需要住在医院旁边,以防突发情况。


    我们住下来的当天,还去潍坊妇幼保健院做了孕检,检查结果并不理想,医生说:“胎心加速不及格,若一直这样,就得刨宫产。”


    我爱人当时就被吓哭了,我们一直都想顺产的,我不想在她的肚子上开一道口子。


    我从网上查了一下,导致胎心加速不行的原因是有很多种的,比如胎儿睡着了,或者妈妈没吃好,胎儿饿着了,不想动。


    这天我爱人确实没吃好,我就跟她说:“我们去吃火锅吧,没有什么是一顿火锅解决不了的。”


    于是我就带着哭得梨花带雨的孕妈吃了顿火锅。


    吃完后,我们没有去潍坊妇幼保健院孕检,而是就近去了我家对面的阳光融合医院。


    在阳光融合医院,医生说胎心加速及格了,虽不是那么理想,但也没有问题。


    至此,我们搬来医院对面的第一天可以睡个好觉了。


    一周后的上午,我们又去潍坊妇幼保健院做了孕检,胎心加速还是不理想,医生让我们下午住院,观察情况,可能要刨宫产。


    我没住,我们去阳光融合医院又做了一次孕检,结果胎心加速还是合格的,所以就没去住院。


    我当时的想法是,阳光融合和潍坊妇幼保健院都是三甲医院,只要有一个测着可以不刨,我们就不刨。


    我们也想过为什么两个同样三甲的医院的测试结果不一样,其原因也不一定是哪个医院不行。


    也可能是因为我去潍坊妇幼保健院都是上午去的,而去阳光融合医院都是下午或晚上去的。


    我在滴滴的时候,回家晚,睡得晚,起得晚,熊他妈也一定要等我回来才睡。这让熊宝在娘胎里面变成了一个小夜游神。


    熊宝在上午的时候总是老老实实的不咋动,等到了晚上就总在妈妈肚子里手舞足蹈。


    所以现在上午去医院测胎心的时候,熊宝可能还没起床,等晚上测胎心的时候,就来了精神了。


    记得离预产期还有10天左右的时候,我们还是上午去潍坊妇幼做孕检,医生直接要让我们下午就做刨宫产,她说:“原因有五:胎心加速不行,绕颈两周,产龄偏高,做过锥切,已经临近预产期。”


    我没有照做,我带熊他妈又去吃了一顿火锅,然后睡了一个午觉,养足了精神,就去了潍坊人民医院。


    潍坊人民医院是综合性医院,属于潍坊医院里的老大。


    我们找到了经验丰富的胡明英医生做检查,胡明英医生是一位很有名望的医生,她本应退休却又被返聘回去了,只因为她当了妇产医生后,就闲不住了。


    胡明英医生给我们做了全面细致的检查,得出以下结论:


    ● 胎心加速是合格的。


    ● 绕颈两周并不算太大的问题,因为小孩在顺的时候,还是可以转出来的,当然这也并非绝对。


    ● 脐带的血液流速和供氧都没问题,并未受绕颈两周的影响。


    ● 孕妇年龄35,并没有超出可以顺产的年龄范畴。


    ● 微小面积锥切,且并非疤痕体质,并不影响顺产。


    两天后,胡明英医生又给孕妈做了骨盆检查,最终结论是:可以顺产,等瓜熟落地即可。


    接下来胡医生就给孕妈开了住院单,当孕妈出现规律宫缩的时候,就可以直接来住院。


    期间,我们又去阳光融合查了一次,得到的结果依旧是可以顺产,如此我们才算放心。


    虽然我不懂医术,但基本逻辑我还是懂的。之前潍坊妇幼保健院在未经全面、细致分析的前提下,让孕妈当天下午就做刨宫产的行为有些武断了。


    2024年4月8日的晚上,孕妈发生规律性宫缩,大约每隔半个小时一次。


    我们立刻去了对面的阳光融合医院的急诊楼做检查,医生说快生了,让我们立刻住院。


    我说我在胡明英医生那里挂了号,我们要去潍坊人民医院。


    阳光融合的医生说她是胡明英医生的学生,让我们放心去潍坊人民医院就行,别再换其它地方了,现在的孕妈快生了,不能再折腾。


    孕妈从规律宫缩到可以顺产,还会经历至少几个小时的开指时间,所以我花个十五分钟去潍坊人民医院的时间还是有的。


    如果这个时候是孕妈的羊水破了,我就会选择直接在阳光融合医院顺产,当然,这个时候的医生也肯定不会再让我们走了。


    我们去了潍坊人民医院后,就拿着住院单住进了候产房。


    这时候的孕妈已经五六分钟宫缩一次了,而且疼感很强烈。


    医生让她等着,等着开指。


    这个过程孕妈很痛苦,她一直疼到第二天晚上,已经有些虚脱了。她虚弱的躺在床上,每次宫缩都会大口喘气,看着很让人揪心。


    我一点点的喂她吃着晚饭,她知道后面的顺产需要体力,强忍着剧痛一点点吃掉我喂她的西蓝花、菠菜和馒头。


    她每次宫缩,我都会给她按摩腰部,这样可以缓解一下她盆骨松动的痛苦。


    在夜间两点的时候,她把吃过的晚饭都吐了,她有气无力跟我说:“我生不动了,我没力气了,你去跟医生说吧,让我刨吧。”


    我紧紧的握着她的手说:“你再坚持一下吧,你想想我们为了顺产,经历了那么多,我们在3个医院间周折往复,你现在刨的话就前功尽弃了。”


    我稳定住她的情绪后,就去了护士站,跟医生说:“11号床有点撑不住了,你可以去看看吗?”


    于是医生就去给她做了检查,跟我说:“她快开到三指了,可以打无痛了。”


    医生把她连床带人一起推进了产房。


    我站在产房门口,不能进去。


    那一夜我没有睡,这是我第一次连着两个晚上没有睡觉,却毫无睡意。


    早晨6点左右,医生跟我说:“她打完无疼后,已经睡了一会,现在醒了,你去给她买点早餐,她预计中午能生。”


    此时我的心算是稍微放下了一些,给她买了几个素包子和小米粥,外加一瓶脉动。医生说能量型饮料可以给她快速补充体力。


    在忐忑等待的时间里,我还跟医院签了一个脐带血的储存协议,医生说以后孩子遇到了白血病、肝硬化等病,可以用脐带血治疗。


    我虽不希望有那么一天,但多一份保障还是好的。


    一直等到上午十点多的时候,医生终于告诉我生了,母子平安!


    从此,我也是有娃的人了。


    接下来,我们在医院的单间住了四天,没什么问题后,就去了月子中心,准备在月子中心住上28天。我因为没什么经验,为确保万无一失,只能花钱解决一些问题了。


    在之后的日子里,我会研究一下育儿之道,看看《好妈妈胜过好老师》,同时努力赚钱养娃。


    后面有啥心得,我会再分享给大家。


    最后给大家总结一下我这一路走来的经验:


    ● 尽量让孕妈规律作息,不要像我似的把熊宝养成了小夜游神。


    ● 孕妈吃的每一种食物都要提前查一下能不能吃。


    ● 孕妈情绪很重要,一定要百般呵护,比如孕检的时候要全程陪伴,不要惹她生气。


    ● 孕妈吃啥和该怎么活动,网上都有,我觉得这是比较简单的。


    ● 当孕妈接近预产期,规律宫缩的时候,一定要立刻去医院,这很重要,千万别拖,即使这是在晚上你睡得正香的时候。


    今年上海就有个宝妈晚上规律宫缩,拖到了早上才去医院,结果堵车了,还没进医院就把宝宝生车里了,但胎盘没出来,这极其危险。还好最后母子平安。


    ● 尽量顺产。如果有医院让你刨,除非紧急情况,不要立刻刨,尽快多换几家更好的、至少三甲的医院看看。


    我尊重医生,但我并不觉得每个医生都是白衣天使。就像曾经魏则西事件,还有今年北京积水潭医院原院长田伟落马,这都说明职业和权利并不会决定人之善恶。


    与此同时,我们也不要拿网上看来的知识来挑战医生的专业,网上知识仅供参考,具体怎么做要听医生的。不过,这与我多找几个更专业的医生问问并不冲突。


    ● 努力赚钱,有很多事都是可以用钱来解决的。


    作者:李伟_Li慢慢
    来源:juejin.cn/post/7367174168599150602
    收起阅读 »

    如何让不同Activity之间共享同一个ViewModel

    问题背景 存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据 提出假设的手段 可以定义一个ViewModel,让这两个Activity去共享这个ViewModel 存在的问题 根据不同的Lifecycle...
    继续阅读 »

    问题背景


    存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据


    提出假设的手段


    可以定义一个ViewModel,让这两个Activity去共享这个ViewModel


    存在的问题


    根据不同的LifecycleOwner创建出来的ViewModel是不同的实例,所以在两个不同的Activity之间无法创建同一个ViewModel对象


    问题分析


    先来梳理一下一个正常的ViewModel是怎么被构造出来的:



    1. ViewModel是由ViewModelFactoty负责构造出来

    2. 构造出来之后,存储在ViewModelStore里面
      但是问题是ViewModelStore是 和 (宿主Activity或者Fragment)是一一对应的关系
      具体代码如下


    @MainThread  
    public inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
    ): Lazy<VM> {
    val owner by lazy(LazyThreadSafetyMode.NONE) { ownerProducer() }
    return createViewModelLazy(
    VM::class,
    { owner.viewModelStore },
    {
    (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
    ?: CreationExtras.Empty
    },
    factoryProducer ?: {
    (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelProviderFactory
    ?: defaultViewModelProviderFactory
    })
    }

    看到上面的代码第9行,viewModelStore和owner是对应关系,所以原则上根据不同LifecycleOwner无法构造出同一个ViewModel对象


    解决思路



    1. 无法在不同的LifecycleOwner之间共享ViewMode对象的原因是:ViewModel的存储方ViewModelStore是和LifecycleOwner绑定,那如果可以解开这一层绑定关系,理论上就可以实现共享;

    2. 另外我们需要定义ViewModel的销毁时机:


      我们来模拟一个场景:由Activty1跳转到Activity2,然后两个Activity共享同一个ViewModel,两个activity都要拿到同一个ViewModel的实例,那这个时候ViewModel的销毁时机应该是和Acitivity1的生命周期走,也就是退出Activity1(等同于Activity1走onDestroy)的时候,去销毁这个ViewModel。



    所以按照这个思路走,ViewModel需要在activity1中被创建出来,并且保存在一个特定的ViewModelStore里面,要保证这个ViewModelStore可以被这两个Activity共享;


    然后等到Activity2取的时候,就直接可以从这个ViewModelStore把这个ViewModel取出来;


    最后在Activity1进到destroy的时候,销毁这个ViewModel


    具体实现


    重写一个ViewModelProvider实现如下功能点:



    1. 把里面的ViewModelStore定义成一个单例供所有的LifecycleOwner共享

    2. 定义ViewModel的销毁时机: LifecycleOwner走到onDestroy的时机


    // 需要放到lifecycle这个包,否则访问不到ViewModelStore
    package androidx.lifecycle

    class GlobalViewModelProvider(factory: Factory = NewInstanceFactory()) :
    ViewModelProvider(globalStore, factory) {
    companion object {
    private val globalStore = ViewModelStore()
    private val globalLifecycleMap = HashMap<String, MutableSet<Lifecycle>>()
    private const val DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"
    }

    @MainThread
    fun <T: ViewModel> get(lifecycle: Lifecycle, modelClass: Class<T>): T {
    val canonicalName = modelClass.canonicalName ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
    return get(lifecycle, "$DEFAULT_KEY:$canonicalName", modelClass)
    }

    @MainThread
    fun <T: ViewModel> get(lifecycle: Lifecycle, key: String, modelClass: Class<T>): T {
    if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
    throw IllegalStateException("Could not get viewmodel when lifecycle was destroyed")
    }
    val viewModel = super.get(key, modelClass)
    val lifecycleList = globalLifecycleMap.getOrElse(key) { mutableSetOf() }
    globalLifecycleMap[key] = lifecycleList
    if (!lifecycleList.contains(lifecycle)) {
    lifecycleList.add(lifecycle)
    lifecycle.addObserver(ClearNegativeVMObserver(lifecycle, key, globalStore, globalLifecycleMap))
    }
    return viewModel
    }

    private class ClearNegativeVMObserver(
    private val lifecycle: Lifecycle,
    private val key: String,
    private val store: ViewModelStore,
    private val map: HashMap<String, MutableSet<Lifecycle>>,
    ): LifecycleEventObserver {
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    val lifecycleList = map.getOrElse(key) { mutableSetOf() }
    lifecycleList.remove(lifecycle)
    if (lifecycleList.isEmpty()) {
    store.put(key, null)
    map.remove(key)
    }
    }
    }
    }
    }

    具体使用


    @MainThread  
    inline fun <reified VM: ViewModel> LifecycleOwner.sharedViewModel(
    viewModelClass: Class<VM> = VM::class.java,
    noinline keyFactory: (() -> String)? = null,
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null,
    )
    : Lazy<VM> {
    return SharedViewModelLazy(
    viewModelClass,
    keyFactory,
    { this },
    factoryProducer ?: { ViewModelProvider.NewInstanceFactory() }
    )
    }

    @PublishedApi
    internal class SharedViewModelLazy<VM: ViewModel>(
    private val viewModelClass: Class<VM>,
    private val keyFactory: (() -> String)?,
    private val lifecycleProducer: () -> LifecycleOwner,
    private val factoryProducer: () -> ViewModelProvider.Factory,
    ): Lazy<VM> {
    private var cached: VM? = null
    override val value: VM
    get() {
    return cached ?: kotlin.run {
    val factory = factoryProducer()
    if (keyFactory != null) {
    GlobalViewModelProvider(factory).get(
    lifecycleProducer().lifecycle,
    keyFactory.invoke(),
    viewModelClass
    )
    } else {
    GlobalViewModelProvider(factory).get(
    lifecycleProducer().lifecycle,
    viewModelClass
    )
    }.also {
    cached = it
    }
    }
    }

    override fun isInitialized() = cached != null
    }

    场景使用


    val vm : MainViewModel by sharedViewModel()

    作者:红鲤驴
    来源:juejin.cn/post/7366913974624059427
    收起阅读 »

    用了这么久SpringBoot却还不知道的一个小技巧

    前言 你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。 你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。 那么SpringBoot本身提供...
    继续阅读 »

    前言



    你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。




    你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。




    那么SpringBoot本身提供了一个小技巧,很多人估计没用过。



    正文


    1、效果



    废话不多说,先写个service和controller展示个效果最实在。




    来个简单的service



    @Service
    public class TestService {

    public String test() {

    System.err.println("Hello,Java Body ~");
    return "Hello,Java Body ~";
    }
    }


    再来个简单的controller



    @RestController
    @RequestMapping("/api")
    @AllArgsConstructor
    public class TestController {

    private final TestService testService;

    @GetMapping("/test")
    public ResponseEntity test() {
    return ResponseEntity.ok().body(testService.test());
    }
    }


    接下来是不是以为要启动调接口了,No,在SpringBoot的启动类中加这么个玩意儿



    @SpringBootApplication
    public class JavaAboutApplication {

    public static void main(String[] args) {
    SpringApplication.run(JavaAboutApplication.class, args);
    }

    @Bean
    CommandLineRunner lookupTestService(TestService testService) {
    return args -> {

    // 1、test接口
    testService.test();

    };
    }

    }


    启动看下效果



    4.png



    可以发现,SpringBoot启动后,自动加载了service的执行程序。




    这个小案例是想说明什么呢,其实就是CommandLineRunner这么个东西。



    2、它是什么



    CommandLineRunner是一个接口,用于在Spring Boot应用程序启动后执行一些特定的任务或代码块。当应用程序启动完成后,Spring Boot会查找并执行实现了CommandLineRunner接口的Bean。




    说白了,就是SpringBoot启动后,我立马想干的事,都可以往里写。



    3、我用它做过什么



    我的话,和很多厂家对接过接口,在前期不会直接开始写业务,而是先调通接口,再接入业务中。




    比如webservice这种,我曾经使用CommandLineRunner直接调对方接口来测试,还挺舒适,也节省了IDEA资源,但要注意调试完成后注释掉,本地测试的时候再打开就行。



    5.png


    4、它还有哪些用途



    除了可以拿来调试第三方接口,它还有什么用途吗?




    其实开头已经说过,它就是SpringBoot启动后,你立马想干的事,都可以在里面写,所以你完全可以发挥想象去用。




    我这里,提供几个思路作为参考。



    1)、数据库初始化


    你可以使用CommandLineRunner来执行应用程序启动时的数据库初始化操作,例如创建表格、插入初始数据等。



    2)、缓存预热


    CommandLineRunner在应用程序启动后预热缓存,加载常用的数据到缓存中,提高应用程序的响应速度。



    3)、加载外部资源


    加载一些外部资源,例如配置文件、静态文件或其他资源。CommandLineRunner可以帮助你在启动时读取这些资源并进行相应的处理。



    4)、任务初始化


    使用CommandLineRunner来初始化和配置某些定时任务,确保它们在应用程序启动后立即开始运行。



    5)、日志记录


    SpringBoot启动后记录一些必要的日志信息,如应用程序版本、环境配置、甚至启动时间等等,这个看具体需求。



    6)、组件初始化


    你可能需要按照特定的顺序初始化一些组件,CommandLineRunner可以帮助你控制初始化顺序,只需要将它们添加到不同的CommandLineRunner实现类中,并使用@Order注解指定它们的执行顺序即可。



    总结



    其实,能用的地方挺多,我最后再举个例子,netty启动时,往往是绑定了端口并以同步形式启动。




    但如果要和SpringBoot整合,我们不可能还那么做,而是交给SpringBoot来控制netty的启动和关闭,当SpringBoot启动后,netty启动,当SpringBoot关闭时,netty自然也关闭了,这样才比较优雅。




    那么,我们完全可以将netty的启动执行程序放到CommandLineRunner中,这样就可以达到目的了。




    没用过的xdm,今天学会一个新知识点了不,可以自己下去试试哦。


    作者:程序员济癫
    来源:juejin.cn/post/7273434389404893239
    收起阅读 »

    程序员工作七年后的觉醒:不甘平庸,向上成长,突破桎梏

    前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。 看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,...
    继续阅读 »

    前言


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


    昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。


    看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,在约束着你。


    上学时,你要好好读书,争取考上985/211,最起码上个一本。


    工作后,大家都羡慕考公上岸的,上不了岸的话,你需要找一个好公司,拿到一个高工资,最好还能当上管理人员。


    后来有了家庭,你要承担起男人的责任,赚钱养家。


    过去20多年的时间,我都觉着这样的条条框框没有问题,在年少轻狂的时光,这些条条框框决定了你的下限,大家不都是这么过来的吗?


    可是我过去的努力,都是为了符合条条框框的各项要求。我越来越觉着疑惑,我的努力,到底是为了什么啊,是为了这些世俗上的要求吗,我到底为谁而活?


    压力,是自己给的


    说实话,自己也给自己不少压力。


    刚毕业,没有房贷车贷的情况下,我便给了自己很大的压力。压力怎么来的呢?比如一个月五千块钱的工资,买不起一个最新款的iPhone,又比如北京的朋友们,工资相比我在二线城市,竟能高出我一倍。


    后来工作半年决定去北京,也是工作7年来,唯一一次的裸辞。


    初生牛犊不怕虎,裸辞给我带来的毒打,至今历历在目,比如银彳亍卡余额一天天减少的焦虑,比如连面试都没有的焦虑,还有时刻担心着要是留不在北京,被迫得回老家的焦虑。


    记得青旅楼下,有一家串店叫“很久以前羊肉串”,不到五点的时候门口就会有人排队,晚上下楼时看着饭店里熙熙攘攘,吃着烤串喝着扎啤的人时,心里十分羡慕,但却又不会踏进饭店一步。


    毕竟一个目前找不到工作的人,每天一睁眼就是吃饭和住青旅的成本,吃个20块钱一顿的快餐就好了,怎么可能花好几百下馆子呢?


    那时候心里有个愿望,就是我也想每周都可以和朋友来这里吃顿烧烤、喝喝扎啤。


    嗯,我也不知道为什么,那时候对自己就是这么严苛。家庭虽不算富裕,但也绝不可能差这几顿烧烤、住几晚好的宾馆的钱,但我就是这样像苦行僧一样要求着自己,仿佛在向爸妈多要一分钱,就代表着自己输了。


    后来工作稳定了,工资也比毕业时翻了几倍,恰巧又在高位上车了房子,但似乎压力只增不减,同样是不敢花钱。


    现在又有了娃,这次压力也不用自己给了,别管他需要什么,一个小眼神,你只想给他买最好的。因此不敢请假,更不敢裸辞GAP一段时间了,这种感觉就像是在逃避赚钱的责任,不误正业一般。


    一味的向前冲


    带着压力,只能一味的向前冲,为了更高的薪资不断学习,为了更高的职级不断拼搏。


    在“赚钱”这件事上,男人的基因里就像被编写好了一段代码。


        while (true){
    makeMoreMoney();
    }

    过程中遇到困难,压力大,有难过的时候怎么办,身边有谁能去诉说呢?


    中国的传统文化便是“男儿当自强”、“男儿有泪不轻弹”,怎么能去向别人诉说自己的痛苦呢?


    那时候现在的老婆那时候还在上学,学生很难理解职场。结婚后,更没有人愿意在伴侣前展示自己的软弱。


    和家人说?但是不开心的事,不要告诉妈妈,她帮不上忙,她只会睡不着觉。


    和好朋友们一起坐下聚聚,喝几杯啤酒,少聊一些工作,压力埋在心里,让自己短暂的放松一下。



    但现在的行业现状,不允许我们一味的在职场上冲了。


    行业增速放缓,互联网渗透率达到瓶颈,随着而来的就是就业环境变差,裁员潮来袭。


    你可以选择在职场中的高薪与光环,但也要付出相应的代价,比如变成“云老公/老婆”,“云爸爸/妈妈”。


    或许我们都很想在职场中有一番作为,但是外部环境可能会让我们头破血流。


    为了家庭,所以在职场中精进自己,升职加薪。我不禁在想,这看似符合逻辑的背后,我自己到底奋斗的是什么


    不甘平庸,不服输


    从老家裸辞去北京,是不满足于二线城市的工作环境,想接触互联网,获得更快的进步。


    在北京,从小公司跳槽到大厂,是为了获得更高的薪资与大厂的光环。


    再次回到老家,是不满生活只有工作,回来可以更好的平衡工作和生活。


    回想起来,很多时候,自己就像一个异类。


    明明工作还不满一年,技术又差,身边的朋友敢于跳槽到其他公司,涨一两千块钱的工资已经算挺好了,我却非得裸辞去北京撞撞南墙。


    明明可以在中小公司里按部就班,过着按点下班喝酒打游戏的生活,却非得在在悠闲地时候,去刷算法与面经,不去大厂不死心。


    明明可以在大公司有着不错的发展,负责着团队与核心系统,却时刻在思考生活中不能只有工作,还要平衡工作和家庭,最终放弃大厂工作再次回到老家。


    每一阶段,我都不甘心于在当下的环境平庸下去,见识到的优秀的人越多,我便越不服输。


    至此,我上面问自己的两个问题,我到底为谁而活?我自己到底奋斗的是什么,似乎有了些答案。


    我做的努力,短期看是为了能够给自己、给家人更好的物质生活,但长远来看,是为了能让自己有突破桎梏与困境,不断向上的精神


    仰望星空


    古希腊哲学家苏格拉底有一句名言:“未经检视的人生不值得活。”那么我们为什么要检视自己的人生呢?正是因为我们有不断向上的愿望,那么我在想愿望的根源又到底是什么呢?


    既然选择了不断向上,我决定思考,自己想成为什么样的人,或者说,一年后,希望自己变成什么样子,3年呢,5年呢?


    当然,以后的样子,绝不是说,我要去一个什么外企稳定下来,或者说去一个大厂拿多少多少钱。


    而是说,我希望的生活状态是什么,我想去做什么工作/副业,达成什么样的目标。


    昨天刷到了一个抖音,这个朋友在新疆日喀则,拍下了一段延时摄影,我挺受震撼的。



    生活在钢铁丛林太久了,我一直特别想去旅行,比如自驾新疆、西藏,反正越远越好。在北京租的房子,就在京藏高速入口旁,我每天上班都可以看到京藏高速的那块牌子,然后看着发会呆,畅想一下自己开着车在路上的感觉。


    可好多年过去了,除了婚假的时候出去旅行,其余时间都因为工作不敢停歇,始终没有机会走出这一步,没有去看看祖国的大好河山。


    我还发现自己挺喜欢琢磨,无论在做什么事情,我都会大量的学习,然后找到背后运行的规律。因为自己不断的思考,所以现实中,很少有机会和朋友交流,所以我会通过写作的方式,分享自己的思考、经历、感悟。


    我写了不少文章,都是关于工作几年,我认为比较重要的经历的文章,也在持续分享我关于职业生涯的思考。


    从毕业到职场,走过的弯路太多了,小到技术学习、架构方案设计,大到职业规划与公司选择,每当回忆起自己在职场这几年走过的弯路,就特别想把一些经验分享给更多的人,所以我持续的写,希望看到我文章的朋友,都能够对工作、生活有一点点帮助。


    所以,我的短期目标,是希望能够帮助在职场初期、发展期,甚至一些稳定期的朋友们,在职场中少一点困惑,多一点力量


    方式可能有很多,比如大家看我的文章,看我推荐的书籍、课程,甚至约我电话进行1v1沟通,都可以,帮助到一个人,我真的就会感到很满足,假设因为个人能力不足暂时帮不到,我也能根据自己的不足持续学习成长。


    那么一年后,我希望自己变成什么样?
    我希望自己在写作功底上,能够持续进步,写出更具有逻辑性、说服力的内容,就像明白老师、雪梅老师那样。公众号希望写出一篇10w+,当然数量越多越好,当然最希望的是有读者能够告诉我,读完这篇文章很有收获,这样比数据更能让人开心,当然最好还能够有一小部分工作之外的收入。


    那么三年呢?
    3年后,快要32岁了。希望那时候我已经积累了除了写作外,比如管理、销售、沟通、经营能力,能够有自己赚到工资外收入的产品、项目,最好能够和职场收入打平,最差能够和房贷打平,有随时脱离职场的底气。


    五年呢?十年呢?
    太久远了,想起来都很吃力的感觉。我一定还在工作,但一定不是打工,希望自己有了一份自己喜欢的事业,能够买到自己的dream car,然后能够随时带着家人看一看中国的大好河山。


    你是不是想问,为什么一定要想这些?


    因为当我想清楚这个问题的时候,那当下该做什么事情,该做什么选择,就有了一个清晰的标准:这件事情、这个选择,能否帮我们朝「未来的自己」更进一步?


    这时候当再遇到压力、困难,我们就会变的乐观,有毅力、有勇气、自信、耐心,积极主动。


    因为你自己想干成一件事,你就会迸发出120%的能量。


    当然,也希望自己试试放下盔甲,允许自己撤退,允许自己躺平,允许自己怂,允许自己跟别人倾诉痛苦。


    说在最后


    说了很多,感谢你能看到最后。


    感觉整体有点混乱,但还是总结一下:


    起因是感觉自己压力很大,因为持续的大量输入导致自己有点陷入信息爆炸的焦虑,有一天下班到家时感觉头痛无比,九点就和孩子一起睡觉了,因此本来想谈谈中国男性的压力。


    但不由自主的去思考自己的压力是从哪里来的,去发现压力竟都来源于传统文化、社会要求,于是越想越不服气,我为什么非得活成别人认为应该活成的样子?


    于是试着思考自己想成为什么样子,其实也是一直在琢磨的一件事情,因为当开始探索个人IP的时候,我就发现自己需要更高一层的、精神层面的指导,才能让自己坚持下去。


    如果你和我一样,希望你给自己的压力更小一些,环境很差,但总还有事情可以去做,愿你可以想清楚,你想成为的样子。一时想不清楚也没关系,也愿你可以允许自己撤退,允许自己软弱。


    不知道你有没有想过,自己想要成为的样子呢?


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

    如何优雅的将MultipartFile和File互转

    我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。 前言 首先来区别一下MultipartFile和File: MultipartFile是 S...
    继续阅读 »

    我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。


    前言


    首先来区别一下MultipartFile和File:



    • MultipartFile是 Spring 框架的一部分,File是 Java 标准库的一部分。

    • MultipartFile主要用于接收上传的文件,File主要用于操作系统文件。


    MultipartFile转换为File


    使用 transferTo


    这是一种最简单的方法,使用MultipartFile自带的transferTo 方法将MultipartFile转换为File,这里通过上传表单文件,将MultipartFile转换为File格式,然后输出到特定的路径,具体写法如下。


    transferto.png


    使用 FileOutputStream


    这是最常用的一种方法,使用 FileOutputStream 可以将字节写入文件。具体写法如下。


    FileOutputStream.png


    使用 Java NIO


    Java NIO 提供了文件复制的方法。具体写法如下。


    copy.png


    File装换为MultipartFile


    从File转换为MultipartFile 通常在测试或模拟场景中使用,生产环境一般不这么用,这里只介绍一种最常用的方法。


    使用 MockMultipartFile


    在转换之前先确保引入了spring-test 依赖(以Maven举例)


    <dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-testartifactId>
    <version>versionversion>
    <scope>testscope>
    dependency>

    通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。


    multi.png


    作者:程序员老J
    来源:juejin.cn/post/7295559402475667492
    收起阅读 »

    面试官问我String能存储多少个字符?

    首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。 编译器源码如下,限制了字符串长度大于等于65535就会编译不通过 private void checkStringConstant(DiagnosticPosition...
    继续阅读 »

    1. 首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。

    2. 编译器源码如下,限制了字符串长度大于等于65535就会编译不通过


      private void checkStringConstant(DiagnosticPosition var1, Object var2) {
      if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
      this.log.error(var1, "limit.string", new Object[0]);
      ++this.nerrs;
      }
      }

      Java中的字符常量都是使用UTF8编码的,UTF8编码使用1~4个字节来表示具体的Unicode字符。所以有的字符占用一个字节,而我们平时所用的大部分中文都需要3个字节来存储。


      //65534个字母,编译通过
      String s1 = "dd..d";

      //21845个中文”自“,编译通过
      String s2 = "自自...自";

      //一个英文字母d加上21845个中文”自“,编译失败
      String s3 = "d自自...自";

      对于s1,一个字母d的UTF8编码占用一个字节,65534字母占用65534个字节,长度是65534,长度和存储都没超过限制,所以可以编译通过。


      对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,长度和存储也都没超过限制,所以可以编译通过。


      对于s3,一个英文字母d加上21845个中文”自“占用65536个字节,超过了存储最大限制,编译失败。


    3. JVM规范对常量池有所限制。量池中的每一种数据项都有自己的类型。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANTUtf8类型表示。CONSTANTUtf8的数据结构如下:


      CONSTANT_Utf8_info {
      u1 tag;
      u2 length;
      u1 bytes[length];
      }

      我们重点关注下长度为 length 的那个bytes数组,这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大字节数。length 的类型是u2,u2是无符号的16位整数,因此理论上允许的的最大长度是2^16-1=65535。所以上面byte数组的最大长度可以是65535


    4. 运行时限制


      String 运行时的限制主要体现在 String 的构造函数上。下面是 String 的一个构造函数:


      public String(char value[], int offset, int count) {
      ...
      }

      上面的count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1。所以在运行时,String 的最大长度是2^31-1。


      但是这个也是理论上的长度,实际的长度还要看你JVM的内存。我们来看下,最大的字符串会占用多大的内存。


      (2^31-1)*16/8/1024/1024/1024 = 2GB

      所以在最坏的情况下,一个最大的字符串要占用 2GB的内存。如果你的虚拟机不能分配这么多内存的话,会直接报错的。





    补充 JDK9以后对String的存储进行了优化。底层不再使用char数组存储字符串,而是使用byte数组。对于LATIN1字符的字符串可以节省一倍的内存空间。


    作者:念念清晰
    来源:juejin.cn/post/7343883765540831283
    收起阅读 »

    带你从0到1部署nestjs项目

    web
    前言 最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway 大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn) 最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲...
    继续阅读 »

    前言


    最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway


    大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn)


    最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲如何部署。一直有兄弟问prisma如何部署,这篇文章就帮你扫清障碍,文章可能比较长,希望耐心看完


    后端技术栈



    • nestjs

    • mysql

    • redis

    • minio

    • prisma


    部署需要掌握的知识



    • docker

    • github actions

    • 服务器


    实战


    nestjs打包镜像


    我们部署的时候用的docker,docker需要拉镜像,然后生成容器,docker的知识可以去学习下,这里就默认大家会了,我们在打包的时候要写Dockerfile文件,后端项目是需要保留node_modules的,所以打包的时候一起打进去,我的项目用的pnpm包管理工具,我的文件挂载时有点点问题,我就没有用pm2去执行多阶段打包,多阶段打包速度会比较快,还有就是比如开发环境的依赖可以不打,当然这都是优化的地方,暂时没有去做,大家可以自行尝试


    # 因为我们项目使用的是pnpm安装依赖,所以找了个支持pnpm的基础镜像,如果你们使用npm,这里可以替换成node镜像
    # FROM nginx:alpine
    FROM gplane/pnpm:8 as builder

    # 设置时区
    ENV TZ=Asia/Shanghai \
    DEBIAN_FRONTEND=noninteractive
    RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

    # 创建工作目录
    RUN mkdir -p /app

    # 指定工作目录
    WORKDIR /app

    # 复制当前代码到/app工作目录
    COPY . ./

    RUN npm config set registry https://registry.npm.taobao.org/
    # pnpm 安装依赖
    COPY package.json /app/package.json

    RUN rm -rf /app/pnpm-lock.yml
    RUN cd /app && rm -rf /app/node_modules && pnpm install

    RUN cd /app && rm -rf /app/dist && pnpm build

    EXPOSE 3000
    # 启动服务
    CMD pnpm run start:prod


    这样后端镜像就构建好了,接下来去编写github action的文件,github actions是做ci/cd的,让我们每次的部署走自动化流程,不要每次手动去做这些工作


    github actions


    在我们的根目录下面创建这样一个文件,这个文件名字可以随便取


    12.png


    然后在里面编写逻辑


    name: Docker

    on:
    push:
    branches: ['main']

    env:
    REGISTRY: ghcr.io
    IMAGE_NAME: ${{ github.repository }}

    jobs:
    build:
    runs-on: ubuntu-latest
    permissions:
    contents: read
    packages: write
    id-token: write

    steps:
    - name: Checkout repository
    uses: actions/checkout@v3

    - name: Setup Docker buildx
    uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

    - name: Cache Docker layers
    uses: actions/cache@v2
    with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
    ${{ runner.os }}-buildx-

    - name: Log int0 registry ${{ env.REGISTRY }}
    if: github.event_name != 'pull_request'
    uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
    with:
    registry: ${{ env.REGISTRY }}
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract Docker metadata
    id: meta
    uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
    with:
    images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

    - name: Build and push Docker image
    id: build-and-push
    uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
    with:
    context: .
    push: ${{ github.event_name != 'pull_request' }}
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new

    - name: Move cache
    run: |
    rm -rf /tmp/.buildx-cache
    mv /tmp/.buildx-cache-new /tmp/.buildx-cache

    - name: SSH Command
    uses: D3rHase/ssh-command-action@v0.2.1
    with:
    HOST: ${{ secrets.SERVER_IP }}
    PORT: 22
    USER: root
    PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }}
    COMMAND: cd /root && ./run.sh

    这里的['main']就是我们要执行哪个分支,你不是main分支,那就改成你的分支就可以,其他都是固定的模板,直接用


    SSH Command 这个是我们取做ci/cd的时候,每次我们提交代码,然后配置了ssh密钥,就可以让服务器执行run.sh命令,这个shell脚本我们后面可以用到,这里就记住是让服务器去执行拉取镜像以及执行启动容器的。


    当我们做到这一步之后,我们提交代码的时候,应该会出现这样的情况


    13.png


    因为还没有去配置ssh密钥,这个肯定跑不起来,看到我们上面ssh command里面有两个变量,就是我们要配置的,接下来我们去搞服务器。


    服务器


    最近双十一活动,买个服务器还是挺香的,我买的阿里云2核2g的99/年,买的时候选操作系统,随便一个都可以,我因为对ubuntu熟悉一下,就买了ubuntu操作系统的,买好之后,记得重置密码


    14.png


    后面我们用shell工具连接的时候需要用到密码的


    之后我们去下载一个shell工具,连接服务器用的,常见的有xshell finalshell,我用的第二个。


    15.png


    就傻瓜式安装,下一步就可以,然后我们去连接一下服务器,去下载宝塔。


    16.png


    第二步那里选择ssh连接就可以了,然后主机就是你的服务器公网ip,密码就是刚刚的,用户名就是root


    连接上了之后,去下载宝塔,这个是ubuntu的命令,其他的操作系统有差别,可以去搜一下就有


    wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh


    下载好之后输入bt default命令就可以打开了


    17.png


    因为宝塔是个可视化操作面板,比较方便,所以先弄好。


    接下来我们去搞服务器密钥


    18.png


    我们在这里创建好密钥对,记得它只有一次机会,所以下载好了记得保存在你记得住的地方,然后创建好,记得要绑定,不然没效果,然后我们就要得用ssh密钥来连接服务器了


    20.png


    至此,我们的服务器也弄好了


    github绑定密钥


    21.png


    这个是settings界面的,然后大家按照步骤创建就可以,到这里我们的配置就结束了。


    创建shell脚本


    我们上面不是说了,我们要写一个bash文件吗,现在就要来写,这个bash文件我们要执行拉镜像和跑容器


    23.png


    我们可以选择在宝塔中操作


    docker-compose pull && docker-compose up --remove-orphans

    然后我们在同目录下也就是root目录下面新建一个docker-compose.yml文件,来启动容器的,这个文件就不要展示了,也就是创建了哪些服务,挂载哪些卷,如果有需要评论区说一下就行,很简单,因为我们用了很多服务,mysql redis minio nginx 这些多镜像,就得多个容器来跑,docker-compose无疑就好


    到这里后端项目就部署完了,我们还得迁移数据库对吧


    数据库部署


    pirsma迁移

    因为我用的mysql和prisma,typeorm思路差不多,可以一样用。我们的prisma以及typeorm迁移的时候只可以同步表结构,数据不会同步过去,所以我们跑迁移命令的时候,跑完会发现没有数据,我们需要手动导入数据


    另外注意点,我们docker-compose.yml里面的mysql容器名字对应我们连接的主机名,这里记得更改prisma连接,不然你的prisma还连接在localhost肯定找不到


    我们来上手操作


    24.png


    这是我现在在跑的容器,我要找到我的后端项目对应的容器id,进去执行命令


    docker exec -it <容器id> /bin/sh 跑这个我们就可以在容器内部执行命令


    25.png


    然后就可以把表结构同步过去了,我们也可以在生成Dockerfile的时候写迁移命令也是可以的,这样就不用手动同步了


    数据库导出

    我们需要将本地的数据迁移上去,需要先导出sql文件,这个就不用在这里展开说了,很简单,不会可以去找个博客教程,不到30s就完了,导出后我们需要将那个sql文件


    然后我们在宝塔操作,找到你正在运行的mysql容器目录


    26.png


    将你的sql文件上传上去,放哪里都无所谓,记得路径就行


    然后我们进入mysql容器里面,跑上面的那个命令



    1. 登录账号 mysql -u root -p

    2. 输入密码 ******* 输入你数据库连接的那个密码

    3. 进入之后 USE <database_name> 就选中了那张表

    4. 然后执行 source 刚刚的那个sql文件路径


    这样操作数据就同步上去了,注意,数据同步前是一定要有表结构的,所以有先后顺序,这个地方注意。


    也可以用这个命令, 将sql文件拷贝到你的容器内,然后跑上面的步骤,看个人喜好了。
    docker cp /本地路径/your_file.sql 容器名称:/容器路径/your_file.sql


    到这里我们的部署就结束了,等项目正式上线的时候,还有其他注意点还会再写一篇博客的


    最后


    项目是跟着开头提到的小付大佬学习的,主要想学下react,没想到是个全栈项目,就用nestjs写了后端,也学到了很多前端,后端,部署的知识,强烈推荐大家可以去看看。最后 觉得不错的话,可以给个点赞加关注😘


    作者:西檬
    来源:juejin.cn/post/7299859799780655155
    收起阅读 »

    webview预加载的技术原理和注意点

    web
    此文章介绍webview预加载的技术原理和注意点 背景 网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果 原理 即空间换时间,提前加载页面url 由于首页就有网页入口,所以需要在首页Activity进行预加载。 创建webview Web...
    继续阅读 »

    此文章介绍webview预加载的技术原理和注意点


    背景


    网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果


    原理


    即空间换时间,提前加载页面url


    由于首页就有网页入口,所以需要在首页Activity进行预加载。


    创建webview



    • WebView(MutableContextWrapper(context),使用MutableContextWrapper替换Context,,方便后续复用时替换为webview容器Activity的上下文对象

    • WebSettings、原生方法初始化,保证预加载时功能正常,因为后续不会再进行loadUrl,必须保证h5页面正常显示

    • WebViewClient、WebChromeClient监听



      • 重写onPageFinished、onReceivedTitle方法,主要为了title的接收,并且记录下来,后续webview复用时直接显示title

      • 重写onReceivedSslError方法,避免证书错误加载显示失败



    • 首页预加载容器Layout,置于最底层,宽度全屏,高度设置为全屏高度 - 顶部导航栏高度 - 状态栏高度


    viewGr0up.addView(WebView(MutableContextWrapper(context)).also { web ->  // 初始化webview 
    }, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


    • 刷新逻辑,绑码状态或登录信息改变时,刷新已经预加载好的webview的url


    复用webview



    • webview容器整改

      • 判断是否需要使用已预加载的webview,如果需要复用,则根布局添加预加载webview进来,注意布局层级,避免覆盖了其他控件





    webView?.let { web ->
    (web.context as MutableContextWrapper).baseContext = activity
    }

    container.addView(it, 0, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


    • 原webview容器使用ViewStub代替,如果不需要复用则将ViewStub inflate,进行正常的h5页面加载

    • 添加预加载webview后,直接显示,不需要loadUrl,但是白屏分析之类的逻辑别忘了手动调用


    页面关闭



    • webview跟随Activity一起进行销毁,但是需要通知首页重新生成一个webview,准备给下一次用户点击使用

    • 首页关闭,页面栈清空时,需要清空单例中webview对象,并且调用destroy

    • 不推荐回收webview继续使用,因为在实际测试中表现并不好,重建webview可以规避很多问题场景


    如果用户点击比较快时,依然会看到加载过程和骨架屏


    问题点和解决



    • 复用webview时,页面视觉残留上一次h5页面状态



      • 原因:页面关闭时,触发Activity的onStop方法,webview调用onPause,此时webview被暂停,webview的reload也不会立即刷新

      • 解决:回收webview时,对webview重新恢复交互状态(onResume)



    • 页面关闭,迅速点开,页面先显示上一次h5页面状态,然后开始reload



      • 原因:当Activity反复打开关闭时,Activity的回收、对象GC会滞后,导致webview已经被复用并且上屏了,webview才开始触发reload

      • 解决:webview不进行回收,每次页面关闭都进行销毁,重新创建webview



    • webview多次reload后,网络请求失败
      axios网络请求失败,response报文为空,暂未找到原因,了解的大佬麻烦解答一下,谢谢。当不回收webview后,此场景不存在了

    • h5页面正常显示后,又刷新一次页面



      • 原因:webview复用时,对webview重新进行初始化(重新创建原生能力、重置上下文对象等)时,会重新对UserAgent进行赋值,导致重新刷新了一次。

      • 排查过程
        发现网页骨架屏刚出现时点开不会重复刷新;骨架屏消失后点开也不会重复刷新;唯独骨架屏时,刚出现vConsole绿色块时点开会出现重复刷新。
        对webview的shouldOverrideUrlLoading方法进行debug,发现并没有进入断点,说明并不是调用了reload,推测有什么逻辑导致网页重新刷新了一次。
        随即用傻子办法,一段一段代码注释,发现友盟组件attach webview和通用webview容器设置userAgent的代码会导致重复刷新,难道友盟组件也有设置userAgent的代码?
        然后查看友盟组件源码,不出所料,发现友盟组件中反射了setUserAgentString方法,并且对userAgent拼接了"Umeng4Aplus/1.0.0字符串,如下图所示。


        那是否设置的userAgent有什么敏感字符串导致刷新?随即将userAgent只设置为空字符串,发现也会导致重复刷新。
        到这里水落石处,但为什么userAgent发现变化会导致网页重复刷新?
        询问前端大佬,回复没有监听userAgent,userAgent变化也不会做什么操作,故而没有得到具体答案,了解的大佬麻烦解答一下,感谢。

      • 解决:webview复用时,不进行userAgent的重复赋值




    IMG20240529101049.png



    • 复用webview时,页面白屏等待一会后秒开h5页面

      • 原因:预加载时webview在1像素的layout中加载,复用到通用webview容器中,webview控件的布局已经结束,但需要时间对H5进行渲染,在重复打开关闭后或性能低下的手机表现更甚

      • 解决:首页预加载webview时,已通用webview容器同大小进行布局



    • 内存泄漏问题

      • 原因:部分原生方法对象中对Activity和Fragment有强引用,当原生方法对象被addJavascriptInterface添加进webview时,复用的webview生命周期长如Application,就会强引用到Activity,导致无法回收,内存泄漏

      • 解决:webview回收时清空Activity、Fragment的引用

      • 不复用webview后此问题不存在了




    作者:聪本尊18680
    来源:juejin.cn/post/7373937820179005478
    收起阅读 »

    面试官:为什么忘记密码要重置,而不是告诉我原密码?

    Hello,大家好,我是 Sunday。 最近有个同学在面试中遇到了一个很有意思的问题,我相信大多数的同学可能都没有遇到过。 面试官提问说:“为什么很多网站忘记密码需要重置,而不是直接告诉用户原密码?” 很有意思的问题对不对。很多网站中都有“忘记密码”的功能,...
    继续阅读 »

    Hello,大家好,我是 Sunday。


    最近有个同学在面试中遇到了一个很有意思的问题,我相信大多数的同学可能都没有遇到过。


    面试官提问说:“为什么很多网站忘记密码需要重置,而不是直接告诉用户原密码?


    很有意思的问题对不对。很多网站中都有“忘记密码”的功能,但是为什么当我们点击忘记密码,经过一堆验证之后,网站会让我们重置密码,而不是直接告诉我们原密码呢?


    所以,今天咱们就来说一说这个问题。


    防止信息泄露



    2022年11月1日,Termly 更新了《98个最大的数据泄露、黑客和曝光事件》(98 Biggest Data Breaches, Hacks, and Exposures)。其中包括很多知名网站,比如:Twitter



    所以,你保存在网站中的数据可能并没有那么安全。那么这样的数据泄露后会对用户产生什么影响呢?


    对大多数人来说最相关的经历(网上看到的)应该是诈骗电话,他们甚至可以很清楚的告诉你你的所有个人信息。那么这些信息是怎么来的呢?


    有些同学可能说是因为“网站贩卖了我的个人信息”,其实不是的。相信我 大多数的网站不会做这样的事情


    出现这样事情的原因,大部分都是由于数据泄露,导致你所有的个人信息都被别人知道了。


    那么,同理。既然他们可以获取到你的私人信息,那么你的账户和密码信息是不是也有可能被盗取?


    而对于大多数的同学来说,为了防止密码太多忘记,所以很多时候 大家都会使用统一的密码! 也就是说你的多个账号可能都是同一个密码。所以,一旦密码泄露,那么可能会影响到你的多个账号,甚至是银彳亍卡账号。


    因此,对于网站(特别是一些大网站)来说,保护用户数据安全就是至关重要的一件事情。那么他们一般会怎么做呢?


    通常的处理方式就是 加密。并且这种加密可能会在多个不同的阶段进行多次。比如常见的:SHA256、加盐、md5、RSA 等等


    这样看起来好像是很安全的,但是还有一个问题,开发人员知道如何解密他们。或者有些同学会认为 数据库中依然存在着正确的密码 呀?一旦出现信息泄露,不是依然会有密码泄露的问题吗?


    是的,所以为了解决这个问题,网站本身也不知道你的密码是什么。


    网站本身也不知道你的密码是什么


    对于网站(或者其他应用)来说,它们是 不应该 存储你的原密码的。而是通过一些系列的操作来保存你加密之后的代码。并且这个加密是在前端传输到服务端时就已经进行了,并且是 不可逆 的加密操作,例如:MD5 + 加盐



    我们举一个简单的例子:


    比如有个用户的密码是 123456,通过 md5 加密之后是:E10ADC3949BA59ABBE56E057F20F883E


    md5 理论上是不可逆的,所以从理论上来说这个加密后的代码是不可解析的。但是 md5 有个比较严重的问题就是:同样的字符串加密之后会得到同样的结果


    这也就意味着:E10ADC3949BA59ABBE56E057F20F883E 代表的永远都会是 123456


    所以,如果有一个很大的 md5 密码库,那么理论上就可以解析出所有的 md5 加密后的字符串。就像下图一样:




    因此,在原有的 md5 加密之上,很多网站又增加了 加盐 的操作。所谓加盐指的就是:在原密码的基础上增加一些字符串,然后进行 md5 加密


    比如:



    1. 原密码为 123456

    2. 在这个密码基础上增加固定字符“LGD_Sunday!”

    3. 得到的结果就是:“LGD_Sunday!123456”

    4. 然后用该字符进行 md5 加密,结果是:E1FC8CB7B54BED0FDC8711530236BA4D

    5. 此时尝试解密,会发现 解密失败



    这样大家是否就可以理解,为什么很多网站在让我们输入密码的时候 ,要求包含 大小写+符号+ 字母 + 数字 了吧。本质上就是为了防止被轻松解密。


    而服务端拿到的就是 “E1FC8CB7B54BED0FDC8711530236BA4D” 这样的一个加密后的结果。然后服务端再次对密码进行加密操作,从而得到的是一个 被多次加密 的数据,保存到服务端。


    所以说:网站无法告知你密码,因为它也不知道原密码是什么。


    目前很多网站或应用为了保证用户安全,都已经采取 扫码登录、验证码登录 等方式进行登录验证,这种无密码的方式,会更大程度的保证你的账号安全。


    作者:程序员Sunday
    来源:juejin.cn/post/7353580789299281961
    收起阅读 »

    一定要考公吗,一定要上岸吗,可是我已经考出病来了!

    昨天朋友圈里面看到一个好朋友发了病书,患上抑郁症了,他考公一年多了,说实话,我心中百感交集,因为我身边考公的朋友实在太多了,大家心里都在承受巨大的压力。 我觉得没必要,可能你觉得考上公务员就可以稳吃一辈子了,但是事实真的是这样吗? 当然,如果能考上一线城市的...
    继续阅读 »

    昨天朋友圈里面看到一个好朋友发了病书,患上抑郁症了,他考公一年多了,说实话,我心中百感交集,因为我身边考公的朋友实在太多了,大家心里都在承受巨大的压力。


    fde22171b5fb0e4b6983fe643409e32.jpg


    我觉得没必要,可能你觉得考上公务员就可以稳吃一辈子了,但是事实真的是这样吗?


    当然,如果能考上一线城市的公务员,那么绝对是普通人最好的选择,也绝对够牛逼,但是大多数人考不上。


    别说一二线,现在能考进县城里面的公务员,那也算是天之骄子,注意,这里指的是正编,如果是合同的,那么就不说了,大多数人终其一生可能都只能干到乡镇里面。


    这和你有能力没能力其实关系不大,除非你能力万里挑一,特别出众。


    其一,始终要明白“庙小妖风大,浅池王八多”,没有一个人不想往高处走,你进入一个乡镇单位里面,里面年龄比你大,资历比你深的大有人在,你觉得他们都想留在乡镇吗?是乡镇的饭好吃,空气清新吗?


    不是,一切都是因为没机会,没机会说直白一点,就是上面没人,有人早都进城了。


    前两年我在乡镇遇到一个亲戚,和她闲谈,她说租她家隔离房子的人就是乡镇的公务员,快二十年了,还是在乡镇里面,和他同一批进来的,有关系的早都进城了!


    这就很现实,还有你也不要说自己家族里面谁谁谁是那个领导的,没用,如果你没有价值,或者关系不到位,人家不会真心实意帮你的!


    当然,如果你够强,那么不依靠关系,你也能进城。


    我朋友的哥哥就是直接从其他县城的公务员岗位辞职,然后一年不到就考回本县,而且他们还不属于县里管,是省里管,人家考试就是牛逼,面试就讨人喜欢。


    但是大部分人呢,我们自己心知肚明吧,如果你真的牛逼,那么可能早都上岸了,无论是村里还是乡镇。


    到现在都还没上岸,那么就证明自己能力平庸,而且没有关系,认清现实一点。


    其二,考上了就真的稳了吗,就能马上躺平了?


    很多人会说,考上了就稳了,直接吃一辈子,又有面子,福利又好。


    别天真,这也要分单位,的确有的单位福利挺好,年终奖也不少,但是只是很小一部分。


    但是近年来,公务员降薪你也看到了吧,财政那么紧张,福利肯定会大打折扣,甚至没有。


    在中国,现在大多数小城市的公务员工资也就是几千块,如果家里不是县城里面的且父母不能扶持,那么生活也是挺困难的,如果家庭条件特别好,那么就当玩。


    之前听朋友说,她同学香港大学硕士毕业的,家里特别有钱,但是父母就叫她考回家里,一个月虽然工资才两三千,但是父母一个月给她一万,目的就是想让她陪父母。


    但是大部分人呢,别说家里给钱,不给家里就已经不错了,很多人虽然考上了公务员,但是经常在刷信用卡,因为那点工资根本做不了什么。


    所以,穷人家的孩子考上公务员(小地方的公务员),不会像范进中举那样改变人生,可能会更加困难。


    其实最主要的是你基本没有任何机会往上走,你考上的这个岗位,基本上就是你这辈子的终点。


    上面说的这一切都是你考上的前提,无论是镇里,村里还是县里。


    但是回到现实,你能考上吗?


    每年身前面对的是那么多刚毕业的大学生,他们身份比你有优势,身后面对的是那么多老油条,你在夹缝中很难的。


    大多考公的人,要么全职,要么随便找个两三千块钱的活干,二者都承受巨大的心理压力和经济压力。


    特别是经历一次又一次的落榜,心态会越来越糟。


    看着时间从自己手中按时溜走,而自己除了学会了一些考试技巧,背会了一些“八股文”,其他一无所获,如果考上了,那么就是有用的,考不上,那么这一套拿到市场上去并没有什么用。


    身体上的苦尚能抗住,而情绪上的崩溃却可能影响一个人一生,你面对的是一个未知的结果,而且这个结果可能都不是你自己想要的!


    有时候,我觉得当自己真的扛不住了,那么不妨直接放手了,不要觉得下一次就上岸了,如果考了三四次都考不上,那么我觉得确实是自己智商和情商不行,提前一点认识到,别去死磕。


    因为考了好多次都考不上,那么后面去考,基本上就是去玩一个概率游戏,因为考这么多次最起码花了一两年的时间,对于很多知识点,实际上自己都能基本摸清。


    但是为啥还考不上,这基本就和智商挂钩了。


    就像我一个程序员朋友,人家复习三个月就能上岸,而且还是贵阳的一个单位,竞争也很激烈,你能说是运气吗,反正我不信,我觉得这就是智商问题。


    就像我在读大学的时候,一个算法题别人很快就能学会,但是我好多遍都学不会,花费十倍时间去学都赶不上别人,这时候我深刻意识到自己智商确实不行。


    那么我肯定不会去死磕算法,因为毫无卵用,智商不行就是不行,不会因为你花很多时间就能改变的,这是天生的,并且也不是啥985大学毕业的,所以没必要去死磕。


    但是我在应用层面和一些底层逻辑上有一定的积累和理解,所以我就往这个方向去做,何必去死死刷算法呢?


    所以我觉得,无论考公考编还是其他,和智商有很大的挂钩,和情商也有很大的挂钩。


    不要去和自己的弱项较劲,而是要充分去发掘自己的优势!


    我们大部分人,智商平庸,情商也平庸,所以在这条路上是十分吃亏的,就是在玩一个概率游戏!


    从我个人的角度来看,我觉得与其死耗,让自己心理的压力越来越大,不如换个角度去思考人生。


    大多数人无非就是求稳,一辈子没多大的风险。


    这其实是一种逃避,逃避面对现实,面对人生。


    有时候,放过自己未必是一件坏事,人生如此短暂,难道就只有上岸才是唯一的目标吗?


    当然,尊重每一个人的选择,考公本身也需要很大的勇气。


    但是我们终究要回到现实,如果拿不到结果,那么也是自我感动,因为没人会在乎你的过程,别人也没兴趣了解你的过程。


    而明天的路依然要自己负责!


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

    设计问能不能实现这种碎片化的效果

    web
    前言 某天设计发来一个 网站,问我能不能实现这种类似的效果。 不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。 F12 调试 让我们打开调试,瞅瞅别人是如何实现。 可以看到在该层级下,页面有很多个 shard-wrap ...
    继续阅读 »

    前言


    某天设计发来一个 网站,问我能不能实现这种类似的效果。


    shard-img-reverse-xs.gif

    不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。


    F12 调试


    让我们打开调试,瞅瞅别人是如何实现。


    可以看到在该层级下,页面有很多个 shard-wrap 元素,而每一个元素都是定位覆盖其父元素的。


    image.png

    当我们添加 display: none; 后,可以看到嘴角这里少了一块。


    image.png

    而继续展开其内部的元素就可以看到主要的实现原理了:clip-path: polygon();


    image.png

    clip-pathpolygon 更详细的解释可以看 MDN,简单来说就是在一个 div 里面绘制一个多边形。


    比如上图的意思就是:选取 div 内部坐标为 (9.38%,59.35%),(13.4%,58.95%),(9.28%,61.08%) 这三个点,并将其连起来,所以就能构成一个三角形了。然后再填充 backgroundColor 该三角形就有对应颜色了。


    实现过程


    调试看完别人的实现后发现,好像也不是很难。但是数据又如何搞来呢?


    当然我们可以直接在接口那里找别人的数据,但是我找了一会发现不太好找。


    于是想到咱们可是一名前端啊,简单写段 js 扒拉下来不就好了吗,想要更多,就滑一下滚轮,切换下一个碎片图像,再执行一次即可。


    function getShardDomData() {
    const doms = document.getElementsByClassName('shard')
    const list = []
    for (let i = 0; i < doms.length; i++) {
    const style = window.getComputedStyle(doms[i])
    let str = style.clipPath.replace('polygon(', '').replace(')', '')
    list.push({
    polygon: str,
    color: style.backgroundColor,
    })
    }
    return list
    }
    console.log('res: ', getShardDomData());

    image.png

    碎片化组件


    简单封装一个碎片化组件,通过 transitiondelay 增加动画效果以及延迟,即可实现切换时的碎片化动画效果。我这里是用的 tailwindcss 开发的,大家可以换成对应 css 即可。


    export type ShardComItem = {
    color: string
    polygon: string
    }

    export type ShardComProps = {
    items: ShardComItem[]
    }

    export default function ShardCom({items}: ShardComProps) {
    return (
    <div className="relative w-full h-full min-w-20">
    {items?.map((item, index) => (
    <div className="absolute w-full h-full" key={`${index}`}>
    <div
    className="w-full h-full transition-all duration-1000 ease-in-out"
    style={{
    backgroundColor: item.color,
    clipPath: `polygon(${item.polygon})`,
    transitionDelay: `${index * 15}ms`,
    }}
    >
    </div>
    </div>
    ))}
    </div>

    )
    }

    模仿实现的 demo 地址


    组件的代码放码上掘金了,感兴趣可以自提。



    自制画板绘画 clip-path


    当然只扒拉别人的数据,这肯定是不行的,假如设计师想自己设计一份碎片化效果该怎么办呢?


    解决方法也很简单:那就自己简单开发一个绘图界面,让设计师自己来拖拽生成想要的效果即可。


    线上画板地址


    image.png

    画板的实现就放到 下篇文章 中讲述了。


    最后


    当然最终只是简陋的实现了一部分效果罢了,还是缺少很多动画的,和 原网站 存在不少差距的,大家看个乐就行。


    作者:滑动变滚动的蜗牛
    来源:juejin.cn/post/7372013979467333643
    收起阅读 »

    只要条件够诱人,你比谁都自律!

    ​ 虽然我不知道贾玲是谁,也没看过她的电影,但是从最近刷到的视频和文章,我能感受到这人对自己确实挺狠。 很多人将其描述为励志和自律,但是我个人并不觉得有多励志和自律,因为如果不是为了拍这个电影,不是为了让自己更上一个阶梯,她大概率不会去减肥。 如果你有200斤...
    继续阅读 »

    图片


    虽然我不知道贾玲是谁,也没看过她的电影,但是从最近刷到的视频和文章,我能感受到这人对自己确实挺狠。


    很多人将其描述为励志和自律,但是我个人并不觉得有多励志和自律,因为如果不是为了拍这个电影,不是为了让自己更上一个阶梯,她大概率不会去减肥。


    如果你有200斤,你朝思暮想了五六年的女孩子说如果你能减掉80斤,她就和你谈朋友,那么我相信你会拼了命去减肥。


    如果你因为比较胖而患了三高,走路都气喘吁吁,医生说如果再不减肥,你最多能再活几年,那么我相信你绝对会节食,锻炼。


    但是你知道即使锻炼出八块腹肌,心仪的女孩也不可能做你女朋友,你虽然肥胖,但是身体上的病还不严重。


    那么你基本上也不会去刻意减肥,因为人都是喜欢有结果的事情,如果没有结果,那么我刷刷短视频,喝喝可乐,这多快乐,我干嘛去吃这个苦。


    所以在朋友圈看到不少朋友被贾玲感动后直接立flag,说必须要减肥成功的人,多数也是当时被电影感动了,我相信坚持不了多久就会放弃的。


    这样的事情我见得太多,自己也嘴炮过无数次,但是基本都是半途而废,因为你的感性盖过了理性,而感性本身就是很容易受环境影响。


    高三时,学校总是搞一些活动,为了给学生打鸡血,会请那些演讲成功学的来感化大家,“你回头看一下你的老师,心里想一下你的父母”,种种言论敲打一颗尚未成熟的孩子心上,有一些情绪不稳定的直接抱着老师大哭,然后大声喊到,“老师,请你放心,我一定会考上重点大学,爸爸妈妈,我一定不会让你们失望”。


    演讲结束后,大家都带着沉重的心情回到教室,三五天之内倒是挺努力的,也不和别人多说话,但是一个星期后,百分之九十五的人直接恢复原样,该睡觉睡觉,该摆烂摆烂。


    为什么会这样?


    因为当时的感性直接覆盖了理性,总觉得自己可上九天揽月,但是面对现实的时候,英语单词很难背,数学题很难解,最主要的是还面对一个未知的结果,因为没人能保证努力后就能考上重点大学,所以干脆直接摆烂。


    纵使有一部分人看着在努力,但实际上不过是假自律,不过是为了麻痹自己,因为学进去多少你自己清楚,眼镜虽然盯着书,但是脑子已经到校外去了。


    所以我们就能得出一个结论,因为外界的一些刺激而做的决定,发的誓,基本上都是头脑发热而已,就像喝醉了的所作所为第二天一定会后悔。


    只有一种条件能让人像疯狗一样去坚持,那就是交换,并且交换的目标都是十拿九稳的,就像贾玲这样,本身就是名人,减下100斤后,团队进行运作,她的名声,收入等方面一定会发生很大的变化,所以她能坚持。


    我们现实中看到的那些十年如一日都在坚持跑步的人,其实别人也是找到了交换物,比如健康的身体,清醒的头脑等等。


    我之前和朋友聊天,他说写代码的时候头脑不清醒,所以坚持跑步早睡,这样每天大脑都比较清醒,工作效率就比较高,所以他跑步本身就在交换。


    我们回头去想一下自己曾经放弃的那么多事,基本上都是因为没有找到交换物,所以才一次又一次的放弃。


    普通的打工人的交换物无非就是身体和积累,坚持学习,坚持锻炼,坚持思考,坚持做一件能够提升自己的事,经过长时间的累积,无形的财富会变为有形的财富,即使变不成财富,也不会损失什么,唯一损失的就是本该可以挥霍的时间!


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

    Mybatis-Plus的insert执行之后,id是怎么获取的?

    在日常开发中,会经常使用Mybatis-Plus 当简单的插入一条记录时,使用mapper的insert是比较简洁的写法 @Data public class NoEo { Long id; String no; } NoEo noEo = ...
    继续阅读 »

    在日常开发中,会经常使用Mybatis-Plus


    当简单的插入一条记录时,使用mapper的insert是比较简洁的写法


    @Data
    public class NoEo {
    Long id;
    String no;
    }

    NoEo noEo = new NoEo();
    noEo.setNo("321");
    noMapper.insert(noEo);
    System.out.println(noEo);

    这里可以注意到一个细节,就是不管我们使用的是什么类型的id,好像都不需要去setId,也能执行insert语句


    不仅不需要setId,在insert语句执行完毕之后,我们还能通过实体类获取到这条insert的记录的id是什么


    image.png


    image.png


    这背后的原理是什么呢?


    自增类型ID


    刚学Java的时候,插入了一条记录还要再select一次来获取这条记录的id,比较青涩


    后面误打误撞才发现可以直接从insert的实体类中拿到这个id


    难道框架是自己帮我查了一次嘛


    先来看看自增id的情况


    首先要先把yml中的mp的id类型设置为auto


    mybatis-plus:
    global-config:
    db-config:
    id-type: auto

    然后从insert语句开始一直往下跟进


    noMapper.insert(noEo);

    后面会来到这个方法


    // com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
    @Override
    public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    stmt = prepareStatement(handler, ms.getStatementLog(), false);
    return stmt == null ? 0 : handler.update(stmt);
    } finally {
    closeStatement(stmt);
    }
    }

    在执行了下面这个方法之后


    handler.update(stmt)

    实体类的id就赋值上了


    继续往下跟


    // org.apache.ibatis.executor.statement.PreparedStatementHandler#update
    @Override
    public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
    }

    image.png


    最后的赋值在这一行


    keyGenerator.processAfter

    可以看到会有一个KeyGenerator做一个后置增强,它具体的实现类是Jdbc3KeyGenerator


    // org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processAfter
    @Override
    public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    processBatch(ms, stmt, parameter);
    }

    // org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processBatch
    public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
    final String[] keyProperties = ms.getKeyProperties();
    if (keyProperties == null || keyProperties.length == 0) {
    return;
    }
    try (ResultSet rs = stmt.getGeneratedKeys()) {
    final ResultSetMetaData rsmd = rs.getMetaData();
    final Configuration configuration = ms.getConfiguration();
    if (rsmd.getColumnCount() < keyProperties.length) {
    // Error?
    } else {
    assignKeys(configuration, rs, rsmd, keyProperties, parameter);
    }
    } catch (Exception e) {
    throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    }
    }

    // org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeys
    private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
    Object parameter)
    throws SQLException {
    if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
    // Multi-param or single param with @Param
    assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
    } else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
    && ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
    // Multi-param or single param with @Param in batch operation
    assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
    } else {
    // Single param without @Param
    // 当前case会走这里
    assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
    }
    }

    // org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeysToParam
    private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
    String[] keyProperties, Object parameter)
    throws SQLException {
    Collection<?> params = collectionize(parameter);
    if (params.isEmpty()) {
    return;
    }
    List<KeyAssigner> assignerList = new ArrayList<>();
    for (int i = 0; i < keyProperties.length; i++) {
    assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
    }
    Iterator<?> iterator = params.iterator();
    while (rs.next()) {
    if (!iterator.hasNext()) {
    throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
    }
    Object param = iterator.next();
    assignerList.forEach(x -> x.assign(rs, param));
    }
    }

    // org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.KeyAssigner#assign
    protected void assign(ResultSet rs, Object param) {
    if (paramName != null) {
    // If paramName is set, param is ParamMap
    param = ((ParamMap<?>) param).get(paramName);
    }
    MetaObject metaParam = configuration.newMetaObject(param);
    try {
    if (typeHandler == null) {
    if (metaParam.hasSetter(propertyName)) {
    // 获取主键的类型
    Class<?> propertyType = metaParam.getSetterType(propertyName);
    // 获取主键类型处理器
    typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
    JdbcType.forCode(rsmd.getColumnType(columnPosition)));
    } else {
    throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
    + metaParam.getOriginalObject().getClass().getName() + "'.");
    }
    }
    if (typeHandler == null) {
    // Error?
    } else {
    // 获取主键的值
    Object value = typeHandler.getResult(rs, columnPosition);
    // 设置主键值
    metaParam.setValue(propertyName, value);
    }
    } catch (SQLException e) {
    throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
    e);
    }
    }

    // com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int, java.lang.Class<T>)
    @Override
    public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
    // ...
    else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
    checkRowPos();
    checkColumnBounds(columnIndex);
    return (T) this.thisRow.getValue(columnIndex - 1, this.longValueFactory);

    }
    // ...
    }

    image.png


    最后可以看到这个自增id是在ResultSet的thisRow里面


    然后后面的流程就是去解析这个字节数据获取这个long的id


    就不往下赘述了


    雪花算法ID


    yml切换回雪花算法


    mybatis-plus:
    global-config:
    db-config:
    id-type: assign_id

    在使用雪花算法的时候,也是会走到这个方法


    // com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
    @Override
    public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    stmt = prepareStatement(handler, ms.getStatementLog(), false);
    return stmt == null ? 0 : handler.update(stmt);
    } finally {
    closeStatement(stmt);
    }
    }

    但是不同的是,执行完这一行之后,实体类的id字段就已经赋值上了


    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);

    image.png


    继续往下跟进


    // org.apache.ibatis.session.Configuration#newStatementHandler
    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
    }

    // org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler
    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
    // ...
    case PREPARED:
    delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    break;
    // ...
    }

    }

    最后跟进到一个构造器,会有一个processParameter的方法


    // com.baomidou.mybatisplus.core.MybatisParameterHandler#MybatisParameterHandler
    public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
    this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
    this.mappedStatement = mappedStatement;
    this.boundSql = boundSql;
    this.configuration = mappedStatement.getConfiguration();
    this.sqlCommandType = mappedStatement.getSqlCommandType();
    this.parameterObject = processParameter(parameter);
    }

    在这个方法里面会去增强参数


    // com.baomidou.mybatisplus.core.MybatisParameterHandler#processParameter
    public Object processParameter(Object parameter) {
    /* 只处理插入或更新操作 */
    if (parameter != null
    && (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
    //检查 parameterObject
    if (ReflectionKit.isPrimitiveOrWrapper(parameter.getClass())
    || parameter.getClass() == String.class) {
    return parameter;
    }
    Collection<Object> parameters = getParameters(parameter);
    if (null != parameters) {
    parameters.forEach(this::process);
    } else {
    process(parameter);
    }
    }
    return parameter;
    }

    // com.baomidou.mybatisplus.core.MybatisParameterHandler#process
    private void process(Object parameter) {
    if (parameter != null) {
    TableInfo tableInfo = null;
    Object entity = parameter;
    if (parameter instanceof Map) {
    Map<?, ?> map = (Map<?, ?>) parameter;
    if (map.containsKey(Constants.ENTITY)) {
    Object et = map.get(Constants.ENTITY);
    if (et != null) {
    entity = et;
    tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
    }
    }
    } else {
    tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
    }
    if (tableInfo != null) {
    //到这里就应该转换到实体参数对象了,因为填充和ID处理都是争对实体对象处理的,不用传递原参数对象下去.
    MetaObject metaObject = this.configuration.newMetaObject(entity);
    if (SqlCommandType.INSERT == this.sqlCommandType) {
    populateKeys(tableInfo, metaObject, entity);
    insertFill(metaObject, tableInfo);
    } else {
    updateFill(metaObject, tableInfo);
    }
    }
    }
    }

    最终生成id并赋值的操作是在populateKeys中


    // com.baomidou.mybatisplus.core.MybatisParameterHandler#populateKeys
    protected void populateKeys(TableInfo tableInfo, MetaObject metaObject, Object entity) {
    final IdType idType = tableInfo.getIdType();
    final String keyProperty = tableInfo.getKeyProperty();
    if (StringUtils.isNotBlank(keyProperty) && null != idType && idType.getKey() >= 3) {
    final IdentifierGenerator identifierGenerator = GlobalConfigUtils.getGlobalConfig(this.configuration).getIdentifierGenerator();
    Object idValue = metaObject.getValue(keyProperty);
    if (StringUtils.checkValNull(idValue)) {
    if (idType.getKey() == IdType.ASSIGN_ID.getKey()) {
    if (Number.class.isAssignableFrom(tableInfo.getKeyType())) {
    metaObject.setValue(keyProperty, identifierGenerator.nextId(entity));
    } else {
    metaObject.setValue(keyProperty, identifierGenerator.nextId(entity).toString());
    }
    } else if (idType.getKey() == IdType.ASSIGN_UUID.getKey()) {
    metaObject.setValue(keyProperty, identifierGenerator.nextUUID(entity));
    }
    }
    }
    }

    在tableInfo中可以得知Id的类型


    如果是雪花算法类型,那么生成雪花id;UUID同理


    image.png


    总结


    insert之后,id被赋值到实体类的时机要根据具体情况具体讨论:


    如果是自增类型的id,那么要在插入数据库完成之后,在ResultSet的ByteArrayRow中获取到这个id


    如果是雪花算法id,那么在在插入数据库之前,会通过参数增强的方式,提前生成一个雪花id,然后赋值给实体类


    作者:我爱果汁
    来源:juejin.cn/post/7319541656399102002
    收起阅读 »

    我是如何把个人网站首屏加载时间从18秒优化到5秒的

    web
    起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!服务器配置CPU:1核,内存:2GiB,带宽:1Mbps这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?...
    继续阅读 »

    起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!

    服务器配置

    CPU:1核,内存:2GiB,带宽:1Mbps

    这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?

    换!!!

    然而贫穷像是一万多条无形的枷锁束缚住了我,让我换服务器的双手动弹不得。

    此路不通,只能另寻他法解决了。

    优化前首屏加载测试

    测试结果分析

    1. 从截图可以看到,首屏加载耗时19.15秒,主要是chunk-vendors.2daba5b2.js这个文件加载耗时最长,为17.6秒,大小为1.9M,其他文件均在4秒内加载完毕。通常页面加载的一个文件大小超过300k,已经算比较大了。第二个比较耗时的文件是chunk-vendors.62bee483.css,这个应该是样式文件。其他的文件加载耗时都不超过1秒,所以后面优化先从那两个文件下手。
    2. 重新编译项目,看下项目生成的文件

    可以看到前面提到的两个文件比较大,后面列出了每个文件使用gz压缩后的大小,但是浏览器实际并没有加载压缩后的文件,而是原始文件。再打开打包文件夹,发现实际生成的js文件夹中除了js文件,还有js.map文件,js.map文件通常用于开发环境调试用,方便我们查找错误,在生成环境是不需要用到的,而且都比较大,这也是一个优化的点。

    分析项目依赖情况

    运行vue ui,编译查看chunk-vendors中的结构发现,主要是element-ui依赖比较大,其次是vue和mavon-editor

    整个项目的情况如下

    那么如何优化呢

    开启nginx压缩配置

    修改nginx配置,启用gzip压缩

    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    测试页面加载时间缩短到5.2秒,chunk-vendors.js传输大小为556k,加载时间为4秒,其他文件加载时间基本不超过200毫秒

    生产配置不生成js.map

    修改项目根目录中vue.config.js配置文件,设置productionSourceMap: false

    module.exports = {
    runtimeCompiler: true,
    productionSourceMap: false
    }

    打包测试文件夹大小由9.1M减小到2.26M

    配置gzip压缩插件

    执行npm i compression-webpack-plugin@5.0.1 -D安装插件,在vue.config.js中修改打包配置

    const CompressionPlugin = require("compression-webpack-plugin");
    const productionGzipExt = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
    module.exports = {
    runtimeCompiler: true,

    productionSourceMap: false,

    configureWebpack: () => {
    if (process.env.NODE_ENV === "production") {
    return {
    plugins: [
    new CompressionPlugin({
    filename: "[path].gz[query]",
    algorithm: "gzip",
    test: productionGzipExt,
    threshold: 1024, // 大于1024字节的资源才处理
    minRatio: 0.8, // 压缩率要高于80%
    deleteOriginalAssets: false, // 删除原始资源,如果不支持gzip压缩的浏览器无法正常加载则关闭此项
    }),
    ],
    };
    }
    },
    };

    插件需要指定版本,最新版本的会报错这个和nginx压缩配置感觉重复了,实际测试和nginx压缩配置的速度差不多,如果两个压缩都有,速度并没有提升

    修改elementui组件按需引入

    1. 执行npm install babel-plugin-component -D安装 babel-plugin-component2. 修改.babelrc内容如下:
    {
    "presets": [["@babel/preset-env", { "modules": false}]],
    "plugins": [
    [
    "component",
    {
    "libraryName": "element-ui",
    "styleLibraryName": "theme-chalk"
    }
    ]
    ]
    }
    1. 在main.js中引入需要用到的组件,示例如下:
    import Vue from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store";
    import "element-ui/lib/theme-chalk/index.css";
    import mavonEditor from "mavon-editor";
    import "mavon-editor/dist/css/index.css";
    import axios from "axios";
    import {
    Avatar,
    Button,
    Container,
    DatePicker,
    Dialog,
    Dropdown,
    DropdownItem,
    DropdownMenu,
    Footer,
    Form,
    FormItem,
    Header,
    Image,
    Input,
    Main,
    Message,
    MessageBox,
    Notification,
    Option,
    Select,
    Table,
    TableColumn,
    TabPane,
    Tabs,
    Timeline,
    TimelineItem,
    } from "element-ui";

    Vue.use(Button);
    Vue.use(Dialog);
    Vue.use(Dropdown);
    Vue.use(DropdownMenu);
    Vue.use(DropdownItem);
    Vue.use(Input);
    Vue.use(Select);
    Vue.use(Table);
    Vue.use(TableColumn);
    Vue.use(DatePicker);
    Vue.use(Form);
    Vue.use(FormItem);
    Vue.use(Tabs);
    Vue.use(TabPane);
    Vue.use(Header);
    Vue.use(Main);
    Vue.use(Footer);
    Vue.use(Timeline);
    Vue.use(TimelineItem);
    Vue.use(Image);
    Vue.use(Avatar);
    Vue.use(Container);
    Vue.use(Option);
    Vue.use(mavonEditor);
    Vue.prototype.$notify = Notification;
    Vue.prototype.$message = Message;
    Vue.prototype.$confirm = MessageBox.confirm;
    Vue.prototype.$axios = axios;
    Vue.config.productionTip = false;

    axios.interceptors.request.use(
    (config) => {
    config.url = "/api/" + config.url;
    config.headers.token = sessionStorage.getItem("identityId");
    return config;
    },
    (error) => {
    return Promise.reject(error);
    }
    );

    axios.interceptors.response.use(
    (response) => {
    if (response.data && response.data.exceptionCode) {
    const exceptionType = response.data.exceptionType;
    Notification({ title: response.data.exceptionMessage, type: exceptionType.toLowerCase() });
    return Promise.reject(response.data);
    }
    return response;
    },
    (error) => {
    return Promise.reject(error);
    }
    );

    new Vue({
    router,
    store,
    render: (h) => h(App),
    }).$mount("#app");

    修改按需引入后elementui依赖大小约为1.3M

    修改组件局部引入为异步组件

    在一个组件中引入其他组件时使用异步的方式引入,如

    export default {
    components: {
    register: () => import('./views/Register.vue'),
    login: () => import('./views/Login.vue')
    }
    };

    完成后此时chunk-vendors.js这个文件已经从优化前的1.9M缩小到890k

    页面加载约3秒可以显示出来,其他资源在页面显示后继续后台加载,全部加载完总耗时约5秒,请求数68次

    组件按组分块

    使用命名chunk语法webpackChunkName: "块名"将某个路由下的组件打包在同一个异步块中,如

    import Vue from "vue";
    import VueRouter from "vue-router";

    Vue.use(VueRouter);

    const routes = [
    {
    path: "/",
    redirect: 'home'
    },
    {
    path: '/home',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/Home.vue')
    },
    {
    path: '/documents',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentList.vue')
    },
    {
    path: '/documentcontent',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentContent.vue')
    },
    {
    path: '/write',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/WriteMarkdown.vue')
    },
    {
    path: '/about',
    component: () => import(/* webpackChunkName: "home-page" */ '../views/About.vue')
    },
    {
    path: '/management',
    component: () => import(/* webpackChunkName: "management" */ '../views/management/Management.vue'),
    children: [
    { path: '', component: () => import(/* webpackChunkName: "management" */ '../views/management/ManagementOptions.vue') },
    { path: 'developplan', component: () => import(/* webpackChunkName: "management" */ '../views/management/DevelopmentPlan.vue') },
    { path: 'tags', component: () => import(/* webpackChunkName: "management" */ '../views/management/TagsManage.vue') },
    { path: 'documents', component: () => import(/* webpackChunkName: "management" */ '../views/management/DocumentsManage.vue') }
    ]
    },
    {
    path: '/games',
    component: () => import(/* webpackChunkName: "games" */ '../views/games/Games.vue'),
    children: [
    { path: '', component: () => import(/* webpackChunkName: "games" */ '../views/games/GameList.vue') },
    { path: 'minesweeper', component: () => import(/* webpackChunkName: "games" */ '../views/games/minesweeper/MineSweeper.vue') }
    ]
    },
    {
    path: '/tools',
    component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsView.vue'),
    children: [
    { path: '', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsList.vue') },
    { path: 'imageconverter', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ImageConverter.vue') }
    ]
    }
    ];

    const router = new VueRouter({
    mode: "history",
    base: process.env.BASE_URL,
    routes,
    });

    export default router;

    打包编译后文件比之前要减少了一部分,并且合并后的文件资源也不大,完全可以接受

    页面加载耗时基本没变,但是请求数减少到51次

    总结

    1. nginx压缩对性能的提升最大,通过压缩文件缩短资源加载时间
    2. gzip压缩插件会将文件压缩成gz格式,暂时不知道怎么用
    3. elementui按需引入会减小依赖资源的大小,chunk-vendors.js文件体积会减小
    4. 使用异步组件可以在后台加载渲染不到的资源,优先加载渲染需要的资源,缩短页面响应时间,但同时会增加打包后的文件数量,导致页面请求数量增加。
    5. 组件按路由分组,打包的时候会将相同组名的资源打包到一个文件中,可以减小请求数


    作者:宠老婆的程序员
    来源:juejin.cn/post/7351292656633331747
    收起阅读 »

    采访:中年程序猿图鉴

    程序员群体曾是低调多金的代表,但最近996话题、甲骨文大裁员等事件持续发酵,让这个群体成了大众眼中的“失意中年人”。年轻时的拼命,换来的却是中年时的焦虑。收入虽高,但前途摇摆。30岁真的是程序员迈不过去的坎吗?曾经梦想着用技术改变世界的程序员们,又是如何看待自...
    继续阅读 »

    程序员群体曾是低调多金的代表,但最近996话题、甲骨文大裁员等事件持续发酵,让这个群体成了大众眼中的“失意中年人”。

    年轻时的拼命,换来的却是中年时的焦虑。收入虽高,但前途摇摆。

    30岁真的是程序员迈不过去的坎吗?曾经梦想着用技术改变世界的程序员们,又是如何看待自己的职业规划和人生价值?

    穿越喧嚣,我们采访了12位中年程序员,听听他们的故事和人生。

    要点速览

    • 我们被固定在“敲代码”的坑里,一干就是10年,再干别的早已不会。敲代码已经成了一项流水线般的工作,就像搬砖工一样。

    • 公司把有创造性的事情全部标准化,每个人负责一部分,还会安排几个人“备份”,每个人随时能被替代,我没有一点安全感。

    • 这个行业根本不存在吃青春饭这一说,关键是40岁就要干40岁该干的活,35岁就要干35岁该干的活,你不能35岁还在干30岁干的活。

    • 加班和掉头发是肯定的,不敢天天洗头,生怕哪天秃顶。

    • 我来谷歌快三年,只有一次是真正为了赶进度加班到晚上12点。不过,硅谷的创业公司很羡慕国内的刻苦劲儿,因为对初创公司来说,真的是效率决定生死。

    • 不论是什么技术,只会把低端的程序员消灭掉。同样一个东西,普通的程序员和有爱好的程序员都能实现,但是有爱好的会把它实现得更好。

    • 老一代程序员喜欢亲力亲为,现在的一代多是拿来主义。我们老一辈是木工,喜欢从木头做成家具,现在的年轻人像宜家,买来现成的再自己搭。新兴程序员效率更高,我们这一辈更能追根溯源。

    • 年龄越来越大,身体确实有点吃不消,上个月后背上还起了一大片带状疱疹。就算如此,我从没想过换行业,我会做一辈子程序员,这是一个有技术含量,让我愿意一直打怪升级的工作!

    null

    从业十年,从“工程师”变“码农”

    Luke 33岁 入行10年

    北京 游戏行业

    10年前我入行时,整个行业一片欣欣向荣,那时候老板更喜欢称我们为“工程师”,但是现在,我们已经成为“码农”了。

    之所以有这个变化,一个是因为工种越来越细化,每个程序员负责的任务越来越精细、单一,时间长了,我们只熟悉那一个模块的工作;另外一个,是因为我们自身的知识结构越来越跟不上新技术的需求。

    软件行业的开发模式,是对一个框架的修改和堆砌。说得更贴切一点,就是堆积木。只要掌握了编程技能,一个程序员每天的工作几乎就是从开源网站上扒一段程序,然后根据公司需要不断在框架上添加、修改。程序是24小时不间断运行的,我们在开发和维护程序的时候,每天都需要加班到很晚,熬夜是常态,这真是一个体力活儿。

    null
    时间长了,我们就被固定在一个“敲代码”的坑里,一干就是10年。这时候,再干别的早已不会了,敲代码已经成了一项流水线般的工作,甚至不用动脑子就能完成,就像搬砖工一样。

    出卖力气,就对身体有很高的要求。对于我们30多岁的程序员来说,已经熬不了夜,思维已经基本固定,但为了养家糊口,要求的工资却越来越高。

    我们长期固定在一个岗位已经形成惯性,上不去也下不来,但这不是我们自愿的,而是对我们的一种摧残。

    在这种压力下,我们也习惯了996的工作节奏。有些时候即使早早做完了工作,看着别人不下班,我们也会拖一拖,至少在老板看来这样比较敬业。

    两年前我曾想过跳出这样的循环,可是当我从原单位离职后,发现再去别的岗位已经成了一个零基础的新人,而且工资比之前还低。考虑了两个月,我又回到了程序员的岗位上。

    我现在最大的想法就是干好当前的工作,趁着还有机会拓展自己,延伸自己的技能。以后如果跳出这个行业,不会被技能限制。

    null

    有时候很羡慕年轻同事,说辞职就辞职

    Bruce 34岁 入行7年

    成都 手机厂商

    我们团队15个人,平均年龄35岁以下,只有三个人比我年纪大。

    我们公司是出了名的用人“狠”,一方面,公司每年会招很多新人,减少用人成本,让我们这些领着高薪的老员工瑟瑟发抖。另一方面,为了保持稳定,公司会把有创造性的事情全部标准化,把任务分得很细,每个人负责一部分,还会安排几个人“备份”,这就让我们没有不可替代性,自然也没有安全感。

    做程序员要么是一直写代码,要么是往上走,做整体规划。如果工作几年还没有升职,确实会很有危机感,我身边合同到了不续签的情况很常见,小组的leader也是随时可以被换掉,或者整个小组被分流。

    在这种压力下,我的工作时长远超996,没有时间学习新东西提升自己,更别说健身追剧。 虽然看起来我的外表和90后没什么区别,但我已经是两个孩子的爸爸了,要挣钱,只能花时间去换。

    null

    有时候很羡慕一些年轻的同事,觉得劳动强度大,说走就走。有的换到国企,还有的转行去做销售,听说有技术背景的销售挺吃香。我也不是没想过换工作,但这份工作离家近,收入也不错,还是先干着吧。

    有人说程序员越老越不值钱,我觉得每个行业都有自己的问题,医生是越老越值钱,但要花大半辈子爬坡,也不是每个人都能忍受。

    我其实还没有想清楚未来该做什么,有时候跟同事开玩笑说,成都的小吃多,大不了以后去开个店卖猪头肉。

    好了,我今天讲的话算是很多了,现在要回去加班了。

    null

    如果35岁了,你根本get不到年轻人的点

    杨光 36岁 从业14年

    湖南 游戏行业

    我2004年入行,毕业进入了一家在数据库这方面仅次于Oracle的外企。当时的待遇还是很好的,一个月工资有5000元,工作965,非常舒服。

    在和我同一批次进入的人里,我学历很一般,他们都是清华、北大、科大的,但我技术很好,大三的时候就做过几个数据库的项目,这是我当时能进入这家外企的根本原因。

    也因为这样,做了半年之后,我觉得没什么意思,也没什么增长空间了。因为公司够大,所有的程序我们只能修改,不能做大改变,在我看来这没什么技术含量。

    而且,外企流程特别多。比如有一次我改一个差不多40多行的代码,对我来说非常简单,不到一天就能改完,但我花了一周的时间,大部分都用在了协调和沟通上,这跟我的性格不太相符。

    于是,我选择离开外企,到联众做棋牌游戏,也是这个时候我正式步入游戏圈。2008年,做了一款知名的游戏后,我开始从单纯的技术人员向管理人员过渡,在游戏行业创过几次业,但2018年游戏行业骤冷,我开始深度思考这个行业。

    我发现这个行业特别喜欢年轻人,因为你的客户是14岁到28岁的人,你如果35岁了,你根本get不到他们的点,这就跟拍电视剧一样,你让陈凯歌去拍小鲜肉,他拍不出来。

    这个行业变化也挺快的,比如今年流行传奇类的,明年可能是养成类的,你怎么让一个天天打打杀杀的程序员去做温情的养成类游戏呢?他没有感觉的,而一旦年龄大了,学习能力也不如以前。

    null

    更重要的是,很多游戏公司的领导比较年轻,没人喜欢招一个比自己大的程序员,我 自己创业招人的时候也是更喜欢年轻人 ,我培养1个月就能上手了,而且性价比高。更何况,年轻人可以周六日在公司加班,年龄大的就得回家看孩子。

    所以,去年我35岁的时候,选择离开游戏行业。我现在开始做医疗了,我的主要客户是35岁往上的,岁数越大,我对这些人的心态就越了解。

    年轻的时候,我们非常崇拜求伯君、比尔盖茨、麦克戴尔这样的人,也希望可以靠技术起家,但现在已经没有这个机会了。

    null

    写代码谁都会,最牛的人是要做判断

    田真 30岁 入行4年

    北京 互联网公司程序员

    在这个行业4年了,35岁的焦虑每年都会炒一次,说得好像35岁的程序员就不用活了一样。

    但真实的情况不是这样。

    以我个人为例,因为入行的时间有限,我目前的想法是希望自己在业务上不断锻炼,争取到了35岁有带队伍的能力,不仅仅是干一线的工作。这并不是说一线的工作不重要,是还有很多工作需要更复杂的精力去解决。退一步讲,不管是什么行业,如果你做了很多年还在一线,那基本上你有点儿不太上进。

    null
    刚刚参加工作的时候,我掌握了一个工具、搭建了一个框架、学习了一个语言就会觉得很满足。但是以后我肯定不会这么觉得。 就好比每次大的项目,最牛的人决定用什么框架、什么语言、什么工具,这决定整个项目的起点。  如果椅子搭好的话,后面会省很多事儿,如果没搭好,后面的麻烦就越来越多,搞不好会浪费时间和精力。

    所以在一线工作时要慢慢积累这种经验,到最后慢慢具备这种能力。这是比简单写代码更重要的能力, 写代码谁都会,但不是谁都可以在重要的事情上做出重要的判断。

    这个行业根本不存在吃青春饭这一说,我40岁的程序员朋友还从百度跳到阿里去了呢。关键就是40岁就要干40岁该干的活,35岁就要干35岁该干的活,你不能35岁还在干30岁干的活。

    公司肯定是需要35岁以上的程序员的,但肯定不是35岁的一线工作人员。

    null

    硅谷的年龄危机远没有国内强烈

    Joey 31岁

    美国 谷歌软件工程师

    在硅谷,没有公司会把年龄限制写进招聘信息里。拿谷歌来说,团队的年龄跨度很大,二十几岁的应届生和四十多岁的博士都很常见。但招人的时候,如果求职者年龄大,HR的期望值相应也会更高,除了问业务问题,还会考察这个人的格局。

    在这里,程序员并不算是“高压”职业。时间灵活,看重效率,没有打卡一说,只要按时完成工作就好。我一般是上午9点半工作到晚上6点半,周末双休陪家人或是去攀岩。有时候因为想把手头的事尽快做完,我会主动加班几小时,但 来谷歌快三年,只有一次是真正为了赶进度加班到晚上12点。  和国内的996兄弟们相比,真的有点“惭愧”。

    正常情况下,工程师工作七八年就会带团队了。当然也有一些人喜欢纯做技术,选择当技术大牛也没问题,一样受人尊重。总体来说, 大家的年龄危机和竞争氛围没有国内那么激烈。

    null

    一方面,虽然谷歌也像其他大公司一样会把任务拆解得很细,每个人负责一部分,但是谷歌的文化比较自由,老板不会事无巨细指挥你怎么做,而是给出空间发挥每个人的创造性。公司内部也鼓励流动,大家经常换组,尝试不同的方向和产品,在这个过程中能够接触新东西,不会觉得自己是在机械的重复劳动中慢慢变老。这一点可能和国内公司不太一样。

    另一方面,我们的节奏没有那么快,工程师有大量时间可以学习新东西。这也是我觉得996不是一个好机制的原因,一个人的时间全部被工作填满,如果还做的是重复性的工作,很难有时间提升自己,长期来看很容易被淘汰。

    我也会焦虑,但不是来自于年龄,而是关于职业成长。硅谷大部分人都拼命想学新东西,我是数学背景,如果在现在的组里不能把我的价值发挥到最大,我也会考虑换个环境。我一直提醒自己,要关注更长期的成长,个人价值才会一直上升。

    都说程序员是吃青春饭,这句话暗含的意思是年纪大的程序员加班拼不过年轻人。 加不了班就没有价值?这不成立。  年纪大的程序员胜在经验,如果真的要靠熬时长,只能说明他干的活儿技术含量还不够。

    我很崇尚个人奋斗,但不是砸时间才叫奋斗。 不过,硅谷的创业公司倒是很羡慕国内的刻苦劲儿,因为对初创公司来说,真的是效率决定生死。

    null

    特别愿意招经验丰富的大龄程序员

    何建 38岁 入行16年

    北京 零售电商行业

    程序员大部分时间都比较忙,一忙就没有太多时间停下来思考。不深度思考,就认识不到怎么培养自己的核心竞争力。即便一个人有20年的工作经验,他的能力也是有边界的。如果本身是个安于现状的人,可能会面临中年危机。

    对于被辞掉的外企员工,可能难融入国内互联网公司文化,这一点也有客观因素。国内的互联网公司做产品,可能三个月就得出一个版本,甚至再过一个月,做两个版本迭代都有可能。外企在速度上做不到,流程也极其复杂。

    null

    2000年-2012年这个阶段,技术的更新换代特别快,到2012年之后,技术圈大的革新已经没有了。包括现在的AI,整个技术还不是特别成熟,更多是在应用层面的落地。技术没有更新,或者没之前更新那么快,程序员们更容易陷入安逸。

    我自己没有中年危机,但技术行业确实存在“年龄歧视”,包括我自己招人。我周围都是90后程序员,但我的偏见没有那么深。假如你是85年的,已经有十年工作经验,我会侧重考虑你的工作年限和实际经验是否匹配。

    很多人工作了十年,还不如工作五年的,很可能是一路混过来的。 但反过来,年龄偏大,学习能力强,经验又足够丰富,我特别愿意找这种人,尤其是在工作之余兼职创业的。  这类人眼界开阔,知道创业路上可能有哪些坑,技术基础扎实的人写的代码质量也高,不需要太多额外人员为他服务。

    所以说,年龄并不是决定性因素,还是要看这个人本身。

    null

    真正的常青树公司不会大招大裁

    张军 35岁 入行10年

    上海 蒹葭(嘉善)电子商务有限公司

    这个行业的人才供应始终是冰火两重天的,高端人才稀缺难得,低端人才供应泛滥。 但真正有底蕴的常青树公司是不会大招大裁的,宁可提高门槛制造俱乐部效应。

    我研究生毕业以后就加入了一线互联网公司,先后在百度等公司就职,现在也成了一名创业者。随之而来的节奏和眼界的变化也很清晰,在大公司工作,只能看到一个拼图的一小块,但在创业公司,每个人都要是超人,从开发到运维一肩挑,还要参与商业化,更实用主义。

    大公司有严格的开发流程,从总体设计到详细设计、编码阶段、提测,然后交给运维上线,中间要花2到3周,甚至是2到3个月,初创公司没办法这么讲究,可能头天拿到需求,第二天就敢上线。

    大环境一直在变,唯一不变的只有变化。所以最近几年,我必须保持学习新东西的状态,要说瓶颈的话,在于技术人转管理岗,适应起来时间会比较长,所谓“慈不掌兵,义不行贾”,打工者视角切换为leader视角,自己的性格会遇到新挑战。

    我们部门平均年龄大概26岁,年龄代际必然会造成差异,但总的来说问题不大。年龄大带给我的优势就是经验的不断沉淀, 在老技术人眼里,没有多少真正的新东西,都是新瓶装老酒。

    年龄本身不会给我带来危机感,带来危机感的是经济周期、行业周期、岗位需要的投入度与自身能够提供的投入度的差异。

    null

    干了这一行之后,基本没有上下班之分,只有醒着和睡着的区分。 坦率的讲,业界对程序员发迹线的消费是有悖科学精神的,秃不秃取决于基因。  头发掉得厉害的人,可能祖上有一些贵族基因,就像英国的查尔斯王子家族一样。很遗憾,我发际线至今还行。

    我平时加班之后会去夜跑,一周三次,能够给我提供一个独立思考时机,整个公园很安静,感觉很好。

    互联网人是持续学习者,持续奔跑者。目前我只实现了人生规划里的一部分,创业的野心一旦打开,就会一直在这条路上狂奔下去。

    null

    我们编程像木工,新兴程序员像宜家

    孟誉国 41岁 从业16年

    上海 ERP软件公司CEO

    软件行业相对来说比较看经验,3-5年之后可能就完全换另外一套东西了。新的工具不会用,一脱节就很难跟上去。比如之前说打算盘,打得再好再快,计算器一出来经验就没用了。

    与其等到人家觉得我不行,不如我自己早一点,在一个新旧替换的时代里主动选择创业,掌握主动权,然后做自己更想做也更有价值的事。

    我现在自己会带小朋友,最明显的感觉是,老一代程序员喜欢亲力亲为,现在的一代多是拿来主义。 打个比方,我们老一辈是木工,喜欢自己从木头做成家具,现在的年轻人有点像宜家,买来现成的再自己搭 。新兴的程序员效率会更高,我们这一辈更能追根溯源,相辅相成吧。

    年龄还有一个优势,可以让你更加冷静沉稳,不应该犯的错误会少犯一些。前几天有一个朋友让我去用黑客的方式,删掉别的管理系统里面的一个订单,早20年炫技也好要面子也好,我可能直接愿意帮忙,但现在我会跟他说:“技术上我肯定搞得定,但是我真的不能帮你。”

    null

    我一直比较乐天,心态比较年轻,之前上班的时候,我是公司请假最多的人。我比较喜欢旅游、摄影、做视频,玩吉他和电子音乐合成器,会参加一些公益拍摄活动,每年去海边潜水几次。

    其实我也后悔,年轻的时候还是玩多了。那时候虽然也自学了不少东西,边查资料边摸索,学得特别快,但我觉得可以学得更多。我现在还在研究单片机,买了好多单片机的板和学习资料,但进度明显慢下来了。

    不论是什么技术,只会把低端的程序员消灭掉。  我觉得程序员一定要热爱编程,完成了一个作品会觉得开心,而不是听说这行工资高就去临时速成甚至改行。招人的时候我也会碰到很多培训六个月就出来做程序员的人,我一般不太会去选这种。

    同样一个东西,普通的程序员和有爱好的程序员都能实现,但是有爱好的会把它实现得更好。

    null

    程序员的职业方向也得赶风口

    yanyan 31岁 从业6年

    北京 手机厂商研发

    35岁以上还在做一线程序员的情况其实挺多。

    “程序员行业吃的是青春饭”这个说法,要分情况来看。如果在技术方面积累比较好,其实35岁以上的程序员还是比较吃香。但是如果一直写基础代码,没有比较深的技术积累,一直做到35岁非常危险。

    null

    甲骨文不是有个绰号叫中关村最大的外企养老机构吗? 一些被裁的年龄比较大的外企程序员虽然拿到的补偿会比较多,但他们最害怕找不到下家。  一是工作方向和工作强度跟国内程序员没法竞争,二是如果35岁以上还是纯基层的研发或者写代码的程序员,很难找到和原先岗位匹配的工作。

    按照一般的职业发展路径,程序员可以发展成为某一个领域的技术专家,对标阿里的P7、P8,或者是在某一个技术方向上成为资深顾问,另一个方向就是晋升到管理层,负责项目开发或整个技术架构。这个方向不仅需要擅长技术,还要懂项目管理。

    程序员行业的职业方向也是得赶风口,几年前头条、抖音发展起来,APP开发是热门方向。最近几年就是人工智能、自动驾驶最火,算法工程师和人工智能开发工程师这方面的岗位比较多。

    我自己在职业方向上有时候会焦虑,但是这个行业本来就需要不断学习新东西,不然被淘汰的概率会比较大。如果再过几年,职位和收入达不到一个比较可观的情况,或者晋升的可能比较小,我可能会更焦虑,会怀疑自己是不是方向错了,或者考虑新的机会。

    null

    技术大佬从来不需要考虑年龄问题

    Jay 32岁 入行7年

    北京 电商平台后台开发

    甲骨文裁员的消息,我关注过。互联网寒冬,哪个公司裁员都不稀奇,我们公司也有裁员。程序员一直是个比较容易焦虑的职业,尤其是技术没能随着年龄增长成正比的成长的话,更容易焦虑。

    对大部分底层码农来说,程序员就是青春饭。HR或猎头找你,也会因为年龄问题拒绝你,我身边就有同事遇到。 但是技术大佬从来都不需要考虑年龄问题。

    我们部门同事的平均年龄在30岁左右,年轻人比例不算大,年龄差异肯定是存在,毕竟大家的生活和成长经历摆在那。 我们的工作节奏比较紧凑,国内互联网公司不加班的应该不多。 掉头发也是肯定的,都不敢天天洗头,生怕哪天秃顶了。

    null

    如果单看收入,目前还算满意。毕竟程序员起薪就不错,不管是在什么城市,肯定都是高于当地平均工资不少。但是如果将工资和工作强度、消费水平放在一起看,那就性价比太低。

    到目前为止,我还没换过工作。之前有拿过其他公司offer,后悔拒绝了。

    null

    庆幸当初入行早,让我现在衣食无忧

    Nick 37岁 入行13年

    深圳 台资公司研发助理

    我已经过了35岁,但是我心态很好。

    其实不是所有程序员都要累死累活的加班。  那些做产品研发的,有明确的上线时间,压力会非常大,加班也很多,如果年纪大了加不动班,当然会有危险。但像我们公司,主要是帮客户解决使用我们产品过程中遇到的问题,每年只有客户产品需要量产的5个月会忙一些,其他时间工作强度不算大。大家朝九晚六,周末双休,还能每周组织一次羽毛球比赛。

    我们团队平均年龄30岁左右,但我观察,那些40岁左右的人,还是待得挺舒服。一是因为公司管理人性化,不会纯考核绩效,人员稳定。另一方面是相比于新人,老员工对公司的每一代产品更加熟悉,知道以往的产品有哪些局限、迭代的新品做了哪些更新。让新人发愁的问题,老员工可能早就经历过,能迅速能做出判断。

    null

    这几年,我的工作内容变化不是很大,但是圈子里的新技术是肯定要去学的。我身边甚至有朋友跨界开了处理芯片的加工厂, 我没有想要去外面折腾,这可能是因为我入行早,之前积累的收入已经足够我安居乐业,也就没有那么多动力去折腾了。

    程序员的收入还是属于中等偏上,但以现在的消费水平和深圳的房价来说,入行早晚差异很大。我算是赶上了一个尾巴,所以还是要感谢当初选择这个职业,让我衣食无忧。

    null

    我愿意做一辈子程序员升级打怪

    老铁 38岁 从业15年

    北京 安全科技公司架构师

    我们这行明明是“越老越吃香”, 我们有丰富的经验,是一名架构师,而不是普通的编码者。

    我相信那些被甲骨文裁掉的员工也会被BAT等公司抢着要,当然,不包括少部分在大公司“养老”的人。确实有部分人在舒适区待了很多年,他们可能有十年的工作经历,却只有一年的工作经验,只是从“小白兔”熬成了“老白兔”,肯定要被时代淘汰。

    这一行的更新迭代速度太快,我30岁以后,确实认真考虑过未来要如何发展,去报班考pmp(项目管理专业人士资格认证),挤出时间去上课,不断学习可以抵抗焦虑。

    null

    其实我们工作节奏还好,没有外界说的996那么夸张,以我们的任务量,只要在工作日每天非常专注、高效地工作6个小时就可以完成,很多人只是效率太低,拖到太晚。

    但可能是年龄越来越大,最近身体确实有点吃不消了。上个月我后背上起了一大片带状疱疹,医生说是压力太大,导致免疫力低下。

    就算如此,我从没想过换行业,我想我会做一辈子程序员,这是一个有技术含量,让我愿意一直打怪升级的工作!

    我现在的阶段性目标是成长为一名CTO,或者是安全领域的技术专家,能够带领超过百人的团队完成项目。

    这就好比登山,在你坚持爬到崖口,看到一片没有遮挡的蓝天时,你会发现一切都很值得。

    *文中部分图片来源于视觉中国。应受访者要求,文中杨光、田真、张军、老铁为化名。


    作者:燃财经编辑部
    来源:mp.weixin.qq.com/s/5Cw-NzxjsRF2BwdSdGFzBQ
    收起阅读 »

    AI 搜索的价值在哪里

    借鉴开源 Lepton Search 的灵感,在公司内部做了一款 AI 搜索工具,名为爱搜。这个工具目前处于带着做状态,没有投入什么人力和资源。遂想写点东西,记录下自己的一些想法和观点。不一定对,但都是吾之所悟。AI 搜索是什么AI 搜索是指利用人工智能技术,...
    继续阅读 »

    借鉴开源 Lepton Search 的灵感,在公司内部做了一款 AI 搜索工具,名为爱搜。这个工具目前处于带着做状态,没有投入什么人力和资源。遂想写点东西,记录下自己的一些想法和观点。不一定对,但都是吾之所悟。

    AI 搜索是什么

    AI 搜索是指利用人工智能技术,帮助用户更快找到需要的信息,提供更加精准和相关的搜索结果。

    为什么要做 AI 搜索

    1. 现在 AI 是风口,所有产品前缀都可以加上 AI,搜索也不例外
    2. 人工智能可以帮人类承担一些搜索工作,之前人类需要在搜索上花一个小时,现在有了 AI ,只需要花 20 分钟甚至更少

    怎么做 AI 搜索

    从现在看,做出一个简单的 AI 搜索产品已经不存在技术难点了,有很多成熟的产品,如:

    • 国内:360AI 搜索、秘塔、天工等,还有一些内置到问答产品中,如 kimi
    • 国外:devv 、perplexity 等

    下面我将从技术架构、应用层、接口层、模块层来阐述怎么做 AI 搜索产品。

    技术架构

    下图是我画的简单 AI 搜索产品架构示意图:

    image.png

    上图把架构分成了三层,分别是应用层、接口层和模块层,解释如下:

    • 应用层:可以是 web、native、桌面端、浏览器插件、sdk
    • 接口层:支持应用层的各种 api
    • 模块层:是搜索和 各种 agent 的核心实现

    这应该是最简单的 AI 搜索架构了,复杂的我没有做过,就不画了。

    应用层

    目前一些 AI 搜索产品我都用过,直接参考秘塔、devv 和 perplexity 即可,三者页面如下图所示:

    resized_image-2.png

    整体布局相似,取他们精华,去他们糟粕就可以了。技术选型上,根据团队情况选择就行,如 vue 、 react。整体没有技术瓶颈,正常去开发实现即可。

    接口层

    基于 restful api 去和应用层对接,比如有以下接口:

    • 回答接口
    • 相关问题接口
    • 登录接口
    • 历史记录接口
    • 设置接口

    这一层,也可以加上缓存功能,对于相同问题,直接返回缓存结果。也可以不加缓存,主要看业务需求。

    爱搜接口层和模块层代码的目录结构如下图所示:

    resized_image-4.png

    使用 go 作为开发语言,整体合理。爱搜提供的接口如下图所示:

    resized_image-5.png

    除了自己用的接口,还给其他业务提供了一些能力支持。

    模块层

    这一层属于 AI 搜索的核心了,它能决定 AI 搜索的上限。模块层提供的能力越多,能力越强,产品的竞争力就越大。

    上文的架构图画了两个模块:

    • 模块 1:搜索引擎 --> prompt --> 大模型
    • 模块 2:搜索引擎+爬虫 --> prompt --> 大模型

    搜索引擎

    搜索引擎的方案有两种,分别是付费和开源。如果用付费方案,则有百度、必应、谷歌、serper 等。如果用开源方案,则有 duckduckgo 、searxng 等。

    • 付费方案中,serper 是我认为目前最好的选择,理由是非常便宜、底层走谷歌搜索、速度很快并且国内没有被墙。
    • 开源方案中,我知道的有 searxng 和 duckduckgo ,searxng 更流行。

    爬虫

    1. 在不加限制的搜索场景下,没有找到一个合适的爬虫方案,这种场景有两种方案:
    • 第一种方案:用传统的方法,拿到页面链接,然后解析页面内容,这种依赖页面 dom 结构,那么多页面,怎么去实现一个通用的解析逻辑,很难搞
    • 第二种方案:用 AI 能力,借助视觉模型,拿到页面链接,进入页面,对页面做视觉判断,需要用到什么数据,就拿什么数据,这种目前还没有尝试,感觉难度也大
    1. 如果加限制搜索场景,比如编程问题我只在 stackoverflow 、 reddit 、 github 上搜和爬取,这种是可以有合适的方案的。但是执行爬虫后,返回速度是不是会变慢,这个因素也需要考虑。

    目前爱搜是没有做爬虫方案的,主要是没有想好怎么做。用过 kimi 的,都知道回答会有资料作为参考,如下图所示:

    resized_image-6.png

    我比较好奇的是,kimi 有没有爬取资料 url 的页面内容。还是说,只是把调搜索引擎拿到的搜索结果展示出来,或者说,会根据问题有选择的爬取资料页面。

    目前用 AI 做爬虫的开源项目也有一些,但到目前为止,我还没有找到一个适合所有搜索场景的爬虫方案。

    prompt

    prompt 的设计有几个痛点:

    大而全的 prompt 很难调

    你想靠一个 prompt 解决搜索问题,是几乎不可能的,需要对 prompt 从上到下进行拆分,如下所示:

    • prompt
      • 断言 prompt:判断搜索问题是什么类型
      • 编程 prompt
        • 错误解决
        • 功能实现
        • xxx
      • 非编程 prompt
        • 新闻类
        • 医学
        • xxx

    如果想让回答更加符合用户想要的,prompt 的设计就需要考虑原子化。有利于维护、适配和扩展。

    很依赖大模型的能力

    如果未来的大模型能力比现在强大千倍,那也许一个大而全的 prompt 就够了,但现在,还做不到这种。你设计的一个 prompt 在 X 模型上表现很好,但换到 Y 模型上,表现可能就变差了。

    上文将 prompt 从上到下进行拆分,变的小而精,也是为了增加鲁棒性,让其在不同模型上都能有很好的表现效果

    prompt 的设计准则太多了

    据我了解,有很多提示词设计准则,像 CoT、CO-STAR、3S、微软出的 prompt 设计教程等。给我的感觉就是:到底哪个是最佳实践,估计目前没有最佳实践,这给 prompt 设计,又带来了一些困难,不同模型的 prompt 最佳实践可能不一样,如何在 prompt 上屏蔽掉这个因素,是值得思考的事情,将 prompt 拆小,在一定程度上做了屏蔽。但是也会有无法兼容的情况,这种就需要根据模型来单独设计适合它的 prompt 了。

    prompt 也需要后期

    有时会发现,在模型固定的情况下,不管你怎么设计 prompt ,某一个场景的输出就是有问题,这个问题大多是指输出不够稳定。

    比如一个问题的回答,需要输出字符串数组,这个问题问 10 次,会偶然出现一个输出数字数组,或者直接不是数组,这种情况怎么办,从我的观点看,这种情况就需要做后期处理了,通过写程序去识别这种情况,并做相应的处理,保证返回的永远都是字符串数组。

    prompt 自动化测试

    prompt 本身不太可控,如何在迭代过程中,做到对 prompt 有一个稳定的监控,这就需要在 prompt 自动测试上做一些能力,比如:

    1. 自动生成各个类别的问题,每个类别生成 10 个问题,
    2. 自动去跑 prompt,每个问题,跑十遍 prompt
    3. 将相同类别的相同问题跑出的结果进行对比,分析结构和内容是否相似
    4. 将相同类别的问题跑出的结果进行对比,分析此类别的输出结果是否稳定、准确

    模型

    模型的重要性不言而喻,当前模型界应该是最卷的领域了,如何评估和选择模型是一个很重要的事情。就目前来说,模型选对了,产品的成本可能会降一半,效果还会更好。

    模型和 prompt 配合

    上文 prompt 也阐述了相关内容,模型和 prompt 工程形成良性的循环,是我们必须要去做的事情

    私有化模型的挑战

    如果不使用第三方模型 api,使用私有化模型,那需要做以下事情:

    1. 评估和选择模型
    2. 模型部署,要买卡,或者走托管服务
    3. 模型微调【可能需要,但如果想更好,大概率需要做】

    买卡的话,成本就变大了。模型大小也要考虑,“越大”,需要的算力越多。从控制成本角度看,方向如下:

    1. 采取面向模型开发模式,用合适且性价比高的模型去解决不同的业务场景
    2. 模型倾向于选择 MOE ,在“小”的同时,获得高质量的输出结果
    3. 让 prompt 多发力,再加上后期,也可以让“小”模型的效果逼近“大”模型的效果
    4. 选择正确的微调方案,这里我没有经验,目前业界有预训练、SFT、RLHF、LORA、指令微调等
    5. 模型侧要保证性能和准度,就是输出结果要快和准,相同参数级别模型
      • a:想更快,可以尝试用 bit 更小的量化模型,测试输出效果会不会有明显差别,没有的话,就可以考虑用,这样会提高模型性能
      • b:想更准,需要根据情况做处理,比如做指令微调

    AI 搜索商业价值

    我先说下,目前 to c 产品的一些价值场景

    • 360:回答页面加了广告...

    resized_image-7.png

    • 天工:目前没看到付费场景,但是从我的角度看,天工做的还可以,agent 很多,包括 ai ppt、数据分析等

    resized_image-14.png

    • 秘塔:免费版搜索次数有限制,目前没看到上限付费版

    resized_image-8.png

    • devv:按月/年付费,可获无限次 agent 使用、gpt-4o 模型等其他付费功能

    resized_image-9.png

    • perplexity:按月/年付费,付费功能如下图所示:

    resized_image-10.png

    从我的角度看,这些 AI 搜索产品,还没有到让我付费的程度。也就说,已经 To C 的产品,我都没有付费的意愿,那在公司内部搞的 AI 搜索工具,如何去落地或者呈现价值呢?

    以下有我的几点思考和看法

    多在 AI Agent 上发力

    AI Agent 概念:即人工智能代理,是一种利用人工智能技术来执行特定任务或服务的软件程序。AI 代理可以模拟人类智能行为,进行自主决策、学习和交互。它们可以应用于多种领域,包括但不限于客户服务、数据分析、自动化任务、个人助手等。AI 代理能够处理复杂的任务,提高效率,减少人为错误,并为用户提供更加个性化和智能化的服务体验。

    这里我举一些 Agent 例子:

    1. RSS 订阅自动总结和推送 Agent 对 RSS 订阅有强依赖的用户群体,这个功能就能产生较大的价值
    2. 科技、手机、AI 等主题新闻,最新咨询日报生成和推送 通过 AI 搜索去自动搜索各主题最新新闻并进行阅读,最后输出新闻内容总结和高质量点评,对于提高用户的行业前沿资讯感知是有价值的
    3. 简历分析和评估,上传一个简历,会自动分析简历内容,给出评估报告和面试时需要问的面试问题

    当前的 Agent,我更倾向于做一些小而美的 agent,太宏大的 agent,实现起来很困难,一方面受限于技术,一方面也会受限于算力

    内网的搜索和总结要做好

    1. 内网的知识库:包含文档、pdf、各类分享视频
    2. 业务相关的文档

    可以在搜索页面加一个搜索范围,像 perplexity 那样:

    resized_image-13.png

    上图显示的 内网->知识库 是我按 f12 改了下 dom 内容。

    这些功能,爱搜目前都没做,看起来几句话,实际需要不少工作量。就拿 pdf 解析来说,目前业界对于复杂 pdf 的解析好像都没有太好的方案,我试过很多开源项目,都达不到我的理想需求,最近我又看到一个很不错的开源项目,叫 trieve ,其特性如下图所示:

    resized_image-11.png

    这个开源项目已经获得 YC 的投资了,证明其还是有技术和潜力的。目前是我看到对 pdf 分块、解析和搜索最好的开源项目了。后续多研究下这个项目。大家有什么好的开源方案也欢迎告知我。

    业务相关的文档,做起来难度也大,爱搜目前也没有做,如果做的话,整体思路如下:

    业务上可以根据你的登录信息,查你当前拥有的业务权限,然后允许用户选择搜索哪个业务,比如业务 A 所有的项目管理文档,包含策划文档、策划评审意见等,然后对用户选择的业务进行训练和搜索,后续用户可以在业务 A 选项中搜自己想要的内容,并获得相应的回答和索引。

    多和公司内部业务联动

    比如给某个业务提供联网搜索能力、提供搜索能力、提供爬虫能力等,类似这种多去和内部业务沟通交流,也能发挥落地一些价值

    总结

    1. 想一下,bing 和谷歌做 AI 搜索,都被外界喷效果差,就知道要做好 AI 搜索还是很有难度的。
    2. 当然,bing 和谷歌的目标和我们不一样,我们更专注于垂直领域,我希望做小而美的 AI 搜索,它可以是一个产品矩阵,也可以是一个聚合产品
    3. 我们聚焦的是目前世界上最前沿的领域,有困难很正常

    商业价值不是靠讨论出来的,而是靠试出来的。


    作者:ikun日记
    来源:juejin.cn/post/7373921342096080911
    收起阅读 »

    当程序员写代码就行了,为什么还要画图

    相信很多人选择当程序员除了这个行业起步阶段薪资比其他行业高一些之外,还有一个很大的因素是觉得做研发类的工作只要代码写的好,跟电脑这个“直男”打交道就可以了。 但是还没走出校门呢,毕业这一关就得边做毕业设计边写毕业论文。写毕业论文可不是直接把自己的实现代码从ID...
    继续阅读 »

    相信很多人选择当程序员除了这个行业起步阶段薪资比其他行业高一些之外,还有一个很大的因素是觉得做研发类的工作只要代码写的好,跟电脑这个“直男”打交道就可以了。


    但是还没走出校门呢,毕业这一关就得边做毕业设计边写毕业论文。写毕业论文可不是直接把自己的实现代码从IDE粘贴到Word里,但凡代码多一点就会被老师要求修改,老师会告诉你要把你做的毕业设计的功能、设计思路、关键部分的实现细节用绘图结合文字表达清楚。


    这时很多同学就犯难了,匆匆拿出大三的教材《软件程序设计》看看上面的那些的图都怎么画的,再找找网上的例子,模仿着能画出个类似下面的流程图。



    上面这个图乍一看还可以,但是网上关于流程图的语法好几个版本,每个人画出来都不一样,而且用这种形式表达大一些的流程,就完全感觉眼花缭乱。


    画图的底层逻辑是沟通


    其实走到工作岗位上之后我们仍然面临着这样的问题,除了专心写代码外,我们的工作构成中还有大量的需求评审、需求分析、技术评审、系统方案设计这样的需要人和人进行沟通的环节,讲逻辑、讲实现方案、讲设计思路的沟通环节。究其原因就是因为IT行业分工明细,它像其他行业的工作流水线一样涉及大量工种的合作,但又因为交付的软件并不像工业流水线一样是标准件,所以上面列举的每个环节都需要良好的沟通,让各职能人员之间建立共识、建立统一语言后才能完成高效协作交付产品。


    既然需要良好的沟通建立统一语言,那么我们在需求分析、技术评审、系统方案设计文档上就不能使用太主观的语言,也不能把实现代码直接往文档上粘,那样的话且不说有的岗位不写代码,即使同岗位的其他同事也没有那个精力一行一行的仔细看完你的代码、理解你的思路。


    所以这就需要我们能够简洁、高效地用人们都能看懂的专业图形描述出软件开发这些环节中需要重点沟通的需求逻辑和技术的关键细节。


    我们都知道从事理工专业的人,可能对构图、色彩这些不太擅长,那么有没有一种图形不需要美术基础就能掌握,足够专业让图形能突出我们想表达的技术细节,同时还足够简洁即使是不太懂技术的人也能完全看懂呢?在IT领域还真有,那就是UML。


    比如同样是表达需求的业务流程,用流程图表达的就是上面图-1的那个样子,但是用表达力更强,更注重语法的UML活动图表达流程的话就是下面这个样子。



    关于怎么用活动图分析表达流程,后面会有专门的章节去给大家讲解。


    程序员画图难的成因


    说到UML,无论是大学里还是市面上讲解UML的书籍中对UML的讲解都太过枯燥了,它们通常都是以技术和软件设计的角度来讲述UML的,通常上来会先讲解一大堆图,哪些是结构建模,哪些是行为建模,紧接着就是各种图的一堆语法(画法),或者是给出的示例太过于技术化,完全脱离日常生活让人无法理解。


    这就给我们这样一开始不太懂的人一种UML太过专业太过复杂,不好用的印象。想的那么清楚画出图来,代码早写好了。典型的例子就是如果画类图把类的各个属性和方法都想好画出来也太费时间了,况且需求多变还要经常改,还有就是那些类图表示的类的关系一会儿是箭头、一会儿是虚线、不明白他们都什么区别,看多了就头疼。


    其实上面这个现象完全就是误区,UML完全不是必须那么复杂--把所有细节都表示出来才算完事,我们完全可以从需求分析阶段开始就开始使用,在分析的过程中构思业务的结构并画出来它大概的样貌。



    后面随着对需求的进一步了解再去补全或者调整其中的内容。写技术文档常用的UML图除了能像上面这样使用类图分析业务的结构,还有活动图、顺序图、状态机图从不同角度分析业务的行为,而且是循序渐进的使用,不是上来把这些都用上。


    早期对业务知晓不够透彻时UML图可以画的粗略些,流程分析也只先分析明白大流程即可,随着使用UML分析业务的过程对业务逐渐了解后再逐渐细化以及使用不同的图形从不同角度描述业务。 UML家族里提供的各种图,也不局限于只能用于技术分析,甚至需求用例、系统架构、IT架构方面的需求也能够使用UML进行描述。


    掌握UML让自己有更多可能


    无论是一线研发,还是已经转型项目经理、产品经理或者团队管理的人员或者是想要上车入行的萌新程序员,本课程都能让你收益颇多,让你掌握产品经理写需求的一些基本技能,也让你轻松应对项目经理参与竞标和项目管理时的文案编写工作。


    同时还能让你管理项目质量时找到“抓手”,通过在项目团队建立技术评审、方案设计等相关机制--融合团队成员对UML的使用,让团队成员的思维性创造更容易被周知也让这些内容更容易被Review,从而达到项目开发期间高效的沟通和良好的质量保证。


    职场上的“汇报困境”


    除了上面讲的这些我们工作中干活需要用到的各种图形外,在职场上班和在学校上学有一个重要的区别就是我们时不时的就要被拿出来评比、通晒、述职,这些场合都会要求我们做汇报。


    针对这个程序员在职场中的普遍痛点,推荐一下我用大半年的时间沉淀,汇集了我多年职场经验的画图课,解决程序员普遍只愿意埋头写代码,不会做需求分析、不会做技术评审、不会画架构图、述职汇报做不好,等等这些需要画图和表达能力的事情的时候就犯难的问题,帮助大家摆脱代码的单一维度,从多维度提升自己,建立自信,让你在工作中更游刃有余


    课程最后一部分还会扩展一些互联网开发人员在职场中应对各种汇报的策略,讲述一些写汇报PPT的主旨思路,侧重点和注意事项。同时也讲一些使用堆砖块画法(我自己总结的)给汇报PPT进行配图的思路,怎么通过这些图快速抓住听众的眼球建立共识,以及怎么使用一些配图讲解规划给上级“画饼”来获得他们的支持从而进一步获得他们后续在资源上的支持,更好地开展工作,这些技巧我们在课程最后一部分都会讲到。


    相关推荐


    现有有两种订阅方式


    方式1微信专栏:程序员的全能画图课


    方式2小报童专栏:程序员的全能画图课


    作者:kevinyan
    来源:juejin.cn/post/7370615140242472998
    收起阅读 »

    我的 CEO 觉得任何技术经理都是多余的

    原文 QUESTIONABLE ADVICE: “MY BOSS SAYS WE DON’T NEED ANY ENGINEERING MANAGERS. IS HE RIGHT?” 我最近加入了一家初创公司,负责管理一个约 40 名工程师的团队,担任技术...
    继续阅读 »

    原文 QUESTIONABLE ADVICE: “MY BOSS SAYS WE DON’T NEED ANY ENGINEERING MANAGERS. IS HE RIGHT?”



    file


    我最近加入了一家初创公司,负责管理一个约 40 名工程师的团队,担任技术副总裁。然而,我与 CEO(之前是工程师)在是否需要雇佣专职技术经理的问题上产生了很大的冲突。目前,工程师们被分成了 3-4 人的小团队,每个团队有一个工程师头头,负责领导团队,但他们的主要职责仍然是编写代码和交付产品。


    我有 HC 在未来一年雇佣更多的工程师,但没有经理的 HC。老板认为我们是初创公司,负担不起这种奢侈品。在我看来,我们显然需要技术经理,但在他看来,经理只是多余的开销,在我们的阶段所有人都应该全力编写代码。


    我不知道该如何论证。在我看来这很显然,但实际上我很难用言语表达为什么我们需要技术经理。你能帮帮我吗?


    —— 真的是多余的开销吗(?!)




    这里有很多问题需要解答。


    你的首席执行官不理解为什么需要经理,这并不奇怪,因为他似乎不明白为什么需要组织结构。🙈 他为什么要对你如何组织团队或你可以雇佣哪些角色进行微管理?他雇用了你来做这份工作,却不让你完成。他甚至不能解释为什么不让你做。这不是个好兆头。


    但这个问题确实值得思考。我们假设他不是故意要刁难你。😒


    我能想到两种论证雇用技术经理的方式:一种是相当复杂的,从第一性原理 (First Principle) 出发,另一种非常简单,但可能不太令人满意。


    我个人对权威有一种强烈的反感;我讨厌被告知该做什么。直到最近,我才通过系统理论的视角,找到了一种对层级制度既健康又实用的理解。


    为什么组织中存在层级制度?


    层级制度确实带有很多负面包袱。我们许多人都有过在层级制度下与经理或整个组织打交道的不幸经历。在这些地方,层级制度被用作压迫的工具,人们通过垄断信息和玩弄权力游戏来提升地位,决策则是通过权力压制来做出。


    在那种地方工作真的是一种折磨。谁愿意将自己的创造力和生命力投入到一个感觉像《呆伯特》漫画的地方,明知道自己的价值被极少认可或回报,而且这些价值会慢慢地但确实被压制掉?


    file


    但层级制度本质上并非是专制的。层级制度并不是人类为控制和支配彼此而发明的一种政治结构,它实际上是自组织系统的一种属性,是为了子系统的有效运作而出现的。事实上,层级制度对复杂系统的适应性、弹性和可扩展性至关重要。


    让我们从一些关于系统的基本事实开始,为可能不熟悉的人介绍一下。


    层级是自组织系统的一种属性


    一个系统是「由相互依赖的组件组成的网络,这些组件共同工作以实现一个共同目标」(W. Edward Deming)。一堆沙子不是一个系统,但一辆车是一个系统;如果你把油箱取下来,车就无法运作。


    子系统是一个在更大系统内有较小目标的元素集合。在一个系统中可以有很多层次的子系统,它们相互依存地运行。子系统总是为了支持更大系统的需求而工作;如果子系统只为自己的最佳利益优化,整个系统可能会挂掉(这就是「次优」(suboptimal)这个术语的由来 😄)。


    如果一个系统能够通过多样化、适应和改进自身使自己变得更加复杂,那么它就是自组织的。随着系统自组织并增加其复杂性,它们往往会生成层级 —— 即系统和子系统的排列。在一个稳定、有弹性和高效的系统中,子系统在很大程度上可以自我管理、自我调节,并为更大系统的需求服务,而更大系统则负责协调子系统之间的关系并帮助它们更好地发挥作用。


    层级最小化了协调成本,减少了系统中任何部分需要跟踪的信息量,防止信息过载。子系统内部的信息传递和关系比子系统之间的信息传递或关系要密集得多,延迟也少得多。


    (对于任何软件工程师来说,这些应该都很熟悉。模块化,对吧?😍)


    按照这个定义,我们可以说,经理的工作就是在团队之间进行协调并帮助他们的团队表现得更好。


    对社会技术系统的二分是伪命题


    你可能听过这个谬论:「工程师搞技术,经理搞人。」我讨厌这种说法。😊 我认为这完全误解了社会技术系统的本质。社会技术系统中的「社会」和「技术」并不是截然分开的,而是相互交织、相互依存的。事实上,很少有纯粹的技术工作或纯粹的人际工作;有大量涉及两种技能的粘合工作。


    看看任何一个有效运作的工程组织除了编写代码之外还要做的一部分任务:



    • 招聘、建立人脉、面试、培训面试官、汇总反馈、撰写职位描述和职业发展路径

    • 每个项目或承诺的项目管理、优先级排序、管理利益相关者和解决冲突、估算规模和范围、进行回顾会议

    • 召开团队会议、进行一对一交流、提供持续的成长反馈、撰写评审、代表团队的需求 架构设计、代码审查、重构;捕获 DORA 和生产力指标、管理警报量以防止倦怠


    许多工作可以由工程师完成,而且通常也是如此。每家公司对这些任务的分配方式有所不同。这是一件好事!你不希望这些工作仅由经理来做。你希望个人贡献者共同创造组织,并参与其运行方式。几乎所有这些工作由有工程背景的人完成会更有效。


    所以,你可以理解为什么有人会犹豫是否要把宝贵的人员编制花在技术经理上。为什么不希望技术部门的每个人的主要工作都是编写和交付代码呢?这不是从定义上说最大化生产力的最佳方式吗?


    额……😉


    技术经理是一层有用的抽象


    理论上,你可以列出所有需要完成的协调任务,并让不同的人来负责每一项。但实际上,这是不切实际的,因为这样每个人都需要了解所有事情。记住,层级制度的主要好处之一是减少信息过载。团队内部的沟通应该是高效和快速的,而团队之间的沟通则可以少一些。


    随着公司的扩展,你不能期望每个人都认识其他所有人;我们需要抽象的概念才能运作。经理是他们团队的联络点和代表,充当重要信息的路由器。


    file


    有时我把经理想象成公司的神经系统,将信息从一个部门传递到另一个部门,以协调行动。将许多或大部分功能集中到一个人身上,可以利用专业化的优势,因为经理会不断建立关系和背景知识,并在他们的角色中不断改进,这大大减少了其他人的上下文切换。


    管理者 (Manager) 日程与创造者 (Maker) 日程


    技术工作需要集中和专注。上下文切换的成本很高,过多的中断是挺要命的。而管理工作则是每小时左右进行一次上下文切换,并且一整天都要应对各种打断。这是两种完全不同的工作模式、思维方式和日程安排,无法很好地共存。


    通常,你希望团队成员能够把大部分时间花在直接为他们负责的成果做出贡献的事情上。工程师只能做有限的粘合工作,否则他们的日程安排就会变得支离破碎,从而无法履行他们的承诺。而管理者的日程安排本身已经是支离破碎的,因此让他们承担更多的粘合工作通常不会带来太大干扰。


    虽然并不是所有粘合工作都应该由管理者来完成,但管理者的职责是确保所有工作都能完成。管理者的职责是尽量让每个工程师都能从事有趣且具有挑战性的工作,但不能让他们感到过于负担重,还要确保不愉快的工作能公平分配。管理者还要确保,如果我们要求某人完成一项工作,就必须为其配备成功完成这项工作所需的资源,包括专注的时间。


    管理是问责的工具


    当你是工程师时,你对自己开发、部署和维护的软件负责。而作为经理,你则对团队和整个组织负责。


    管理是一种让人们对特定结果(如构建具备正确技能、关系和流程的团队,以做出正确的决策并为公司创造价值)负责的方式,并为他们提供实现这些结果所需的资源(预算、工具和人员编制)。如果你不把组织建设作为某人的首要任务,那么这就不会成为任何人的首要任务,这意味着它可能不会得到很好地执行。那么,这该由谁负责呢,CEO 先生?


    你对技术负责人、工程师或任何负责交付软件的人在「业余时间」能完成的任务有一个合理的上限。如果你试图让技术负责人负责构建健康的工程团队、工具和流程,那么你就是在要求他们在同一个日历里做两份时间不兼容的工作。最可能的情况是,他们会专注于自己觉得舒适的成果(技术成果),而在后台堆积组织债务。


    在自然层级中,我们向上看是为了目标,向下看是为了功能。简而言之,这就是我们需要技术经理的复杂原因。


    选择无趣的技术文化


    更简单的论点是:大多数工程组织都有技术经理。这是默认设置。多年来,许多比你或我更聪明的人花了大量时间思考和调整组织结构,这就是我们得到的结果。


    正如丹-麦金利(Dan McKinley)的名言,我们应该「选择无趣的技术」。无趣并不意味着不好,而是意味着它的能力和失败条件是众所周知的。你只能获得少数的创新点数,因此你应该明智地将这些点数用在能够成就或毁掉你业务的核心差异点上。文化也是如此。你真的想把你的点数用在组织结构上吗?为什么?


    无论好坏,层级组织结构是众所周知的。市场上有很多人擅长管理或与管理者合作,你可以雇佣他们。你可以接受培训、指导,或者阅读大量的自助书籍。有各种各样的管理哲学可以围绕它们来凝聚团队或用来排除其他人。另一方面,我所知道的无经理实验(例如 Medium 和 GitHub 的全员自治,或 Linden Lab 的「选择你的工作」)都被悄然放弃或被颠覆了。在我的经验中,这并不是因为领导者疯狂追求权力,而是由于混乱、缺乏重点和执行不力。


    当没有明确的结构或层级时,结果不是自由和平等,而是「非正式的、不被承认的和不负责任的领导」,正如《无结构的暴政》中详细描述的那样。事实上,这些团队往往是混乱、脆弱和令人沮丧的。我知道!我也很生气!😭


    这个论点并不一定能证明你的 CEO 是错的,但我认为他的证明标准比你的要高得多。「我不想让我的任何工程师停止写代码」并不是一个有效的论点。但我也觉得我还没有完全解决生产力的核心问题,所以我们再来讨论一下这个问题。


    更多代码行数 ≠ 更高生产力


    简要回顾一下:我们在讨论一个有约 40 名工程师的组织,分成 10 个小组,每组有 3-4 名工程师,每组都有一个技术负责人。你的 CEO 认为,如果有人停止全职编程,这个减速将是你们无法承受的。


    也许吧。但根据我的经验,由经验丰富的技术经理领导的几个较大团队,将远远优于这些小团队。这差距很明显。而且,他们可以以更高效、可持续和人性化的方式完成工作,而不是这种拼命的死命赶工。


    系统思维告诉我们原因!更少的团队,但规模更大,你会有更少的整体管理开销,且大大减少了团队内慢且昂贵的协调。你可以在团队内部实现丰富、密集的知识传递,从而实现更大面积的共享。每组有7-9名工程师,你可以建立一个真正的值班轮换,这意味着更少的英雄主义和更少的倦怠。你需要进行的协调可以更具战略性,减少战术性,更具前瞻性。


    五个大团队是否能比十个小团队编写更多的代码行数,即使有五名工程师成为经理并停止编写代码?可能会,但谁在乎呢?你的客户根本不关心你写了多少代码行数。他们关心的是你是否在构建正确的东西,是否在解决对他们重要的问题。关键是推动业务前进,而不是单纯地编写代码。不要忘记,单纯地编写代码会产生额外的成本和负面效应。


    决定你速度的是你是否把时间花在了正确的事情上。学会正确决定构建什么是每个组织都必须自己解决的问题,而且这是一项持续不断的工作。技术经理不会做所有的工作或做出所有的决策,但根据我的经验,他们对于确保工作顺利进行并且做得很好,绝对至关重要。正如我在上篇文章中写到的,技术经理是系统用来学习和改进的反馈循环的载体。


    管理人员是否会成为不必要的开销?


    当然有可能。管理的核心是协调团队之间的工作并提升团队的运作效率,所以任何减少协调需求的方式也会减少对管理的需求。如果你是一家小公司,或者你的团队成员都是非常资深且习惯合作的,那么你就不需要太多的协调。另一个重要因素是变化的速度;如果你的公司在快速增长或者人员流动频繁,或者面临很多时间压力或频繁的战略调整,你对管理人员的需求就会增加。但也有许多较小的组织在没有太多正式管理的情况下运作得很好。


    我不喜欢「开销」这个词,因为 a) 这有点粗鲁,b) 称管理人员为「开销」的人通常是不尊重或不重视管理这门技艺的人。


    但管理实际上确实是开销😅。许多其他的粘合工作也是如此!这些工作很重要,但它们本身并不能推动业务向前发展;我们应该尽量只做那些绝对必要的工作。粘合工作的天然属性使得它很容易扩散,吞噬所有可用的时间和资源(甚至更多)。


    限制是好的。感觉资源不足是好的,这应该成为常态。管理很容易变得臃肿,管理人员可能非常不愿意承认这一点,因为他们从来没有感到压力或紧张减少。(事实上,情况可能恰恰相反;臃肿的管理层可能会为管理人员带来更多工作,而精简的组织结构可能会让他们反而感到压力更小。官僚主义往往会自我发育。特别是当管理层过于关注晋升和自我时。这也是确保管理不应仅为升职或统治的又一个充分理由)




    管理也很像运营工作,当它做得好的时候,是看不见的。评估管理人员的工作可能非常困难,尤其是在短期内,而决定何时创建或偿还组织债务是一个完全不同的复杂问题,远远超出了这篇文章的讨论范围。


    但是,是的,管理人员绝对可以成为不必要的开销。


    然而,如果你有 40 个工程师都向一个副总裁汇报,而没有其他人专门负责人员、团队和组织相关的工作,那么我可以相当肯定地说,这对你来说目前不是一个问题。




    作者:Bytebase
    来源:juejin.cn/post/7373226679730536458
    收起阅读 »

    接了个私活,分享下我是如何从 0 到 1 交付项目的

    web
    大家好,我是阿杆,不是阿轩。 最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。 我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。 前前后后弄了一个半月到两个月,也算是积累了一点经...
    继续阅读 »

    大家好,我是阿杆,不是阿轩。


    最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。


    我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。


    前前后后弄了一个半月到两个月,也算是积累了一点经验,分享给大家,如果以后也接到私活,可以参考一下我的开展方式。


    由于文中涉及到实际业务的东西不方便透露, 下面我将用图书管理系统来代替实际业务,并且称这位校友为“老板”。


    image-20240421154347807


    总览


    我接手的这个项目是完完全全从0开始的,老板只有一个idea,然后说他的idea是来自于另一个小程序的,有部分内容可以参考那个小程序,其他什么都没有了。


    先讲一下我的总体流程:



    1. 确定老板的大致需求,以及预期费用

    2. 详细梳理开发功能点,并简单画下原型图

    3. 工时评估,确定费用

    4. 出正式的UI设计稿

    5. 拟定合同、签合同

    6. 开发阶段

    7. 验收、上线


    大概就是这么些步骤,也对应本文的目录,如果你有想看的部分,可以直接跳转到对应的部分。


    下面我会详细讲讲每一步我都做了些什么。


    确定需求


    首先老板找到我,跟我说他想做一个图书管理的微信小程序,然后讲了几个小程序内的功能点。


    我也向他提了几个问题:



    1. 预算有多少?



      这个肯定得问的,要是预算和工作量严重不匹配,那肯定做不了的。毕竟都是出来赚钱的,总不能让咱用爱发电吧?




    2. 预计一年内会有多少用户量?会有多少数据量?



      这个问题我主要是想知道并发量会有多少、数据量会有多少?这样方便我后续判断系统需要的配置,也便于我后续对整个系统的设计。


      好在整体用户量和数据量都不大,这对我来说也就没什么压力了,至于以后会发展到如何,这不是我该考虑的事情,我顶多把代码写好看点,他后续发展壮大了肯定是把项目接到公司里雇人做的,跟我也没什么关系。




    3. 你那边能够提供什么?



      这个主要是看对方有什么资源,是否能够对项目开发有一定的帮助。


      在我这个项目里,老板那边是什么都没有的,没有设计图、没有服务器资源、也没有辅助人员,所有内容都包揽在我这边,然后有什么问题就直接问他。




    4. 你希望多久完成?



      如果老板很急的话,那可能得多叫几个人一起做,如果时间充足的话,自己一个人做也不是不可以。





    好了,第一次对话大概就是这么些内容,但仅靠一次对话肯定是无法确定需求的,只能了解个大概。


    我根据老板的想法,写了一份 需求分析 出来,首先列出了几个大概的功能点:


    大致功能点列举


    然后根据这些功能点进行扩展,把所有功能列举出来,画成一个思维导图(打码比较多,大家将就将就😅):


    延伸的思维导图


    好,那么第一版的需求分析差不多就出来了,接着我打电话给老板,对着这个思维导图,一个一个的跟老板确认,需不需要这些功能。


    老板一般会提出一些异议,我再继续修改思维导图,直到老板觉得这些功能他都满意了。当然这过程中我也会给一些自己的建议,有些超预算的功能,还是建议他不要做。


    到这里,需求就基本确定好了。


    梳理开发功能点、绘制原型图


    由于我不会前端开发,只是个简单的后端仔,所以我还找了一个前端同学一起做。


    我和前端两个人根据前面的需求文档,详细的梳理出了 小程序 和 后台管理系统 的功能,这个部分是比较重要的,因为后续画设计稿和开发都会以这份文档为主:


    小程序功能梳理文档


    还画了一些简单的原型图,这玩意丑点没事,能让人看懂就行🤣🤣:


    小程序原型图-我的信息


    后台管理系统原型图


    这些东西弄完之后,再找老板进行一遍确认,把里面每个点都确认下来,达成共识。


    工时评估,确定费用


    老板觉得OK了,就到了该谈钱的时候了,前面只是聊了预算,并不是正式的确定费用。


    那咱们也不能张嘴就要,要多了老板不乐意,要少了自己吃亏。


    所以咱们先评估下工时,这边我分了几个部分分别进行工时评估:



    • 需求分析、功能梳理(就是前面做的那些,还没收钱的呢)

    • UI设计、交互设计

    • 前端开发

    • 后端开发

    • 系统运维(包含服务器购买、搭建、认证、配置等)

    • 后期维护


    其中设计稿是找另一位朋友做的,钱单独算,然后其他部分都是我和前端同学两个人评估的,评估的粒度还是比较细的,是以小时为单位进行计算的,给大家大概看一下:


    前端开发工时评估


    后端开发工时评估


    评估完之后汇总一下,然后根据我们自己工作的时薪,给老板一个最终的定价,正常的话还需要在这个定价上再乘一个接单系数(1.2~1.5),但是我们这个老板是校友啊,而且预算也不多,所以就没乘这个系数了(还给他打了折😂,交个朋友)。


    定价报出去之后,老板觉得贵了怎么办?很简单,砍功能呗,要么你去找别人做也行。



    预付订金



    我觉得正常应该在梳理功能之前就要付一部分订金,也不用多少,几百块就行,算是给我们梳理功能的钱。



    这里接下来就要画UI图了,我们先找老板付个订金,订金分为三部分:



    • 给前端的订金

    • 给后端的订金

    • 给UI同学画设计稿的完整费用


    因为UI设计是我这边联系的,所以我肯定得先保障她的费用能完整到手,不然到时候画完图跟我说不做了,那我咋对得起画图的人。


    画UI图


    这部分就不用咱们操心了,把文档交给设计同学,然后等她出图就行。


    这个过程中也可以时不时去看看她画的内容符不符合咱们的预期,当个小小的监工。


    盯着干活


    画完稿子需要跟老板、开发都对一遍,看看有没有出入,符不符合预期,有问题及时修改下,没问题就按照这份稿子进行开发了。


    拟定合同、签合同



    合同也是我来拟定的,其实是先到网上找了个软件开发的合同模板,然后再根据自己的想法进行合理的调整。



    为什么我要到这一步才签合同呢?我觉得合同内容越细致越好,最好是能够把要开发的内容、样式都写在合同上,这样省得后面扯皮。


    现在文档也出了,图也画完了,那咱们把这些东西都贴在和合同的附件上,然后附上这些条约:



    • 乙方将严格按照经过甲方审核的《软件功能设计书》的要求进行软件的开发设计。

    • 甲方托付乙方开发的软件在签订合同之后如需增加其它功能,必须以书面形式呈交给乙方,乙方做改动并酌情收取适当费用。


    这样就可以保障我们在开发完后不会被恶意的增加或者修改功能了。


    再改一次


    这里我再列一些其他需要特别注意的点:



    1. 乙方交付日期,以及最多延期多久,如果超时怎么办?

    2. 甲方付款方式和日期(我们是用的 442 ,开工付 40%,中期验收付 40%,开发完验收付 20%)。

    3. 甲方拖欠项目款的处理方式(支付迟延履行金等)。

    4. 服务器费用是谁出?如果是乙方,需要注意包服务器的时限。

    5. 项目维护期,一般一年半年的吧。

    6. 乙方不保证项目 100% 可用,只能保障支撑 多少人 使用,支撑同时在线人数 多少人 ,如果遇到恶意攻击,不归乙方负责。

    7. 软件归属权是谁的?(如果项目款比较少的话,乙方可以要求要软件归属权,之后甲方如果想把项目接回去自己公司维护的话,需要从乙方手里买,这样乙方可以回点血)


    大概就是这些吧,还有其他的东西基本都是按照模板的,没怎么改。


    弄完给老板看看,没问题就签了,有问题两方再协商一下,我们这边是直接签了的。



    开发阶段


    开发没什么好说的,跟你在公司开发一样。


    不过你接私活可不能在公司开发🚫,只能回家了自己干,不然被抓到上班干私活,你看老板裁不裁你就完事了。


    微信小程序上线注意事项


    微信小程序对请求的接口有三个基本要求:



    1. 必须是有备案的域名。

    2. 必须是有SSL证书(https)。

    3. 域名不得带端口号。


    这个域名的问题必须要尽早解决,不然后面开发完了再去弄的话,工信部备案审核都要挺久的,不延期都难。


    还有一种方式,我在逛微信开放社区看到的,使用云函数进行中转,间接请求ip接口,感觉是可行的,也比较省事,具体操作大家可以自己去探索一下。


    我也是吃了没有经验的亏,买域名 + 工信部备案 + 公安备案 + 小程序备案,这一套操作下来真给我整难受死了,直接用云函数省事多了。



    验收、上线


    这部分也没什么好说的,大家在公司也经常经历这个步骤。


    多沟通,多确认,


    唯一需要提醒的是,验收的时候咱不能无条件接收老板的任何要求,毕竟价格和开发内容都是已经定好的,如果要加内容或者改内容,记得酌情要一点工时费,可不能亏待了自己。



    后记


    整个过程中,其实沟通是最重要的,写代码谁不会是吧?但是得让老板觉得OK才行,如果有什么疑问或者觉得不合理的地方啊,最好是尽早沟通,不然越到后面只会让问题变的越来越大。


    最近刚做完这个项目,说实话没赚什么钱,甚至有点小亏😅。而且这个老板还有点拖欠工资的感觉,中期项目款拖到了项目交付才给,项目尾款到目前还没付😅😅。不过还好合同里写到了关于这块的处理方式,倒也不担心他不付这个钱。


    (虽然我也不知道在哪能接到靠谱的私活🤣,但也可以先收藏本文,万一之后来活了,还能翻出来看看)


    最后,希望各位都能接到 very good 的私活,祝大家早日实现财富自由!


    webwxgetmsgimg (1)


    作者:阿杆
    来源:juejin.cn/post/7359764922727333939
    收起阅读 »

    仿今日头条,H5 循环播放的通知栏如何实现?

    web
    我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。 那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的...
    继续阅读 »

    我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。


    toutiao.gif


    那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的逻辑,并给出完整的代码。最终我实现的效果如下:


    loop-notice.gif


    拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


    布局代码


    我们先看布局,如下图所示,循环播放的布局不是普通的左中右布局。可以看到,当文字向左移动时,左边的通知 Icon 和右边的留白会把文字挡住一部分。


    block-out.png


    为了实现这样的效果,我们给容器 box 设置一个相对定位,并把 box 中的 HTML 代码分为三部分:



    • 第一部分是 content,它包裹着需要循环播放的文字;

    • 第二部分是 left,它是左边的通知 Icon,我们给它设置绝对定位和 left: 0;

    • 第三部分是 right,它是右边的留白,我们给它设置绝对定位和 right: 0;


    <div class="box">
    <div class="content">

    div>
    <div class="left">🔔div>
    <div class="right">div>
    div>

    .box {
    position: relative;
    overflow: hidden;
    /* ... 省略 */
    }
    .left {
    position: absolute;
    left: 0;
    /* ... 省略 */
    }
    .right {
    position: absolute;
    right: 0;
    /* ... 省略 */
    }

    现在我们来看包裹文字的 content。content 内部包裹了三段一模一样的文字 notice,每段 notice 之间还有一个 space 元素作为间距。



    <div id="content">
    <div class="notice">春节期间,部分商品...div>
    <div class="space">div>
    <div class="notice">春节期间,部分商品...div>
    <div class="space">div>
    <div class="notice">春节期间,部分商品...div>
    div>


    为什么要放置三段一模一样的文字呢?这和循环播放的逻辑有关。


    逻辑代码


    我们并没有实现真正的循环播放,而是欺骗了用户的视觉。如下图所示:



    • 播放通知时,content 从 0 开始向左移动。

    • 向左移动 2 * noticeWidth + spaceWidth 时,继续向左移动便会露馅。因为第 3 段文字后不会有第 4 段文字。


      如果我们把 content 向左移动的距离强行从 2 * noticeWidth + spaceWidth 改为 noticeWidth,不难看出,用户在 box 可视区域内看到的情况基本一致的。


      然后 content 继续向左移动,向左移动的距离大于等于 2 * noticeWidth + spaceWidth 时,就把距离重新设为 noticeWidth。循环往复,就能欺骗用户视觉,让用户认为 content 能无休无止向左移动。



    no-overflow-with-comment.png


    欺骗视觉的代码如下:



    • 我们通过修改 translateX,让 content 不断地向左移动,每次向左移动 1.5px;

    • translateX >= noticeWidth * 2 + spaceWidth 时,我们又会把 translateX 强制设为 noticeWidth

    • 为了保证移动动画更丝滑,我们并没有采用 setInterval,而是使用 requestAnimationFrame。


    const content = document.getElementById("content");
    const notice = document.getElementsByClassName("notice");
    const space = document.getElementsByClassName("space");
    const noticeWidth = notice[0].offsetWidth;
    const spaceWidth = space[0].offsetWidth;

    let translateX = 0;
    function move() {
    translateX += 1.5;
    if (translateX >= noticeWidth * 2 + spaceWidth) {
    translateX = noticeWidth;
    }
    content.style.transform = `translateX(${-translateX}px)`;
    requestAnimationFrame(move);
    }

    move();

    完整代码


    完整代码如下,你可以在 codepen 或者码上掘金上查看。



    总结


    本文我介绍了如何用 H5 实现循环播放的通知栏:



    • 布局方面,我们需要用绝对定位的通知 Icon、留白挡住循环文字的左侧和右侧;此外,循环播放的文字我们额外复制 2 份。

    • 逻辑方面,通知栏向左移动 2 * noticeWidth + spaceWidth 后,我们需要强制把通知栏向左移动的距离从 2 * noticeWidth + spaceWidth 变为 noticeWidth,以此来欺骗用户视觉。




    作者:小霖家的混江龙
    来源:juejin.cn/post/7372765277460496394
    收起阅读 »

    为什么很多人不推荐你用JWT?

    为什么很多人不推荐你用JWT? 如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。 什么是JWT? 这个是他的官网JSON We...
    继续阅读 »

    为什么很多人不推荐你用JWT?


    如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。


    什么是JWT?


    这个是他的官网JSON Web Tokens - jwt.io


    这个就是JWT


    img


    JWT 全称JSON Web Token


    如果你还不熟悉JWT,不要惊慌!它们并不那么复杂!


    你可以把JWT想象成一些JSON数据,你可以验证这些数据是来自你认识的人。


    当然如何实现我们在这里不讲,有兴趣的可以去自己了解。


    下面我们来说一下他的流程:



    1. 当你登录到一个网站,网站会生成一个JWT并将其发送给你。

    2. 这个JWT就像是一个包裹,里面装着一些关于你身份的信息,比如你的用户名、角色、权限等。

    3. 然后,你在每次与该网站进行通信时都会携带这个JWT

    4. 每当你访问一个需要验证身份的页面时,你都会把这个JWT带给网站

    5. 网站收到JWT后,会验证它的签名以确保它是由网站签发的,并且检查其中的信息来确认你的身份和权限。

    6. 如果一切都通过了验证,你就可以继续访问受保护的页面了。


    JWT Session


    为什么说JWT很烂?


    首先我们用JWT应该就是去做这些事情:



    • 用户注册网站

    • 用户登录网站

    • 用户点击并执行操作

    • 本网站使用用户信息进行创建、更新和删除 信息


    这些事情对于数据库的操作经常是这些方面的



    • 记录用户正在执行的操作

    • 将用户的一些数据添加到数据库中

    • 检查用户的权限,看看他们是否可以执行某些操作


    之后我们来逐步说出他的一些缺点


    大小


    这个方面毋庸置疑。


    比如我们需要存储一个用户ID 为xiaou


    如果存储到cookie里面,我们的总大小只有5个字节。


    如果我们将 ID 存储在 一个 JWT 里。他的大小就会增加大概51倍


    image-20240506200449402


    这无疑就增大了我们的宽带负担。


    冗余签名


    JWT的主要卖点之一就是其加密签名。因为JWT被加密签名,接收方可以验证JWT是否有效且可信。


    但是,在过去20年里几乎每一个网络框架都可以在使用普通的会话cookie时获得加密签名的好处。


    事实上,大多数网络框架会自动为你加密签名(甚至加密!)你的cookie。这意味着你可以获得与使用JWT签名相同的好处,而无需使用JWT本身。


    实际上,在大多数网络身份验证情况下,JWT数据都是存储在会话cookie中的,这意味着现在有两个级别的签名。一个在cookie本身上,一个在JWT上。


    令牌撤销问题


    由于令牌在到期之前一直有效,服务器没有简单的方法来撤销它。


    以下是一些可能导致这种情况危险的用例。


    注销并不能真正使你注销!


    想象一下你在推特上发送推文后注销了登录。你可能会认为自己已经从服务器注销了,但事实并非如此。因为JWT是自包含的,将在到期之前一直有效。这可能是5分钟、30分钟或任何作为令牌一部分设置的持续时间。因此,如果有人在此期间获取了该令牌,他们可以继续访问直到它过期。


    可能存在陈旧数据


    想象一下用户是管理员,被降级为权限较低的普通用户。同样,这不会立即生效,用户将继续保持管理员身份,直到令牌过期。


    JWT通常不加密


    因此任何能够执行中间人攻击并嗅探JWT的人都拥有你的身份验证凭据。这变得更容易,因为中间人攻击只需要在服务器和客户端之间的连接上完成


    安全问题


    对于JWT是否安全。我们可以参考这个文章


    JWT (JSON Web Token) (in)security - research.securitum.com


    同时我们也可以看到是有专门的如何攻击JWT的教程的


    高级漏洞篇之JWT攻击专题 - FreeBuf网络安全行业门户


    总结


    总的来说,JWT适合作为单次授权令牌,用于在两个实体之间传输声明信息。


    但是,JWT不适合作为长期持久数据的存储机制,特别是用于管理用户会话。使用JWT作为会话机制可能会引入一系列严重的安全和实现上的问题,相反,对于长期持久数据的存储,更适合使用传统的会话机制,如会话cookie,以及建立在其上的成熟的实现。


    但是写了这么多,我还是想说,如果你作为自己开发学习使用,不考虑安全,不考虑性能的情况下,用JWT是完全没有问题的,但是一旦用到生产环境中,我们就需要避免这些可能存在的问题。


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