注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

别再忘了锁屏,几行代码写一个人走屏锁小工具

写在前面 之前在公司,毕竟是干安全的,部门有这么一个要求,被发现不锁屏的,请全部门喝奶茶。很不幸,我也出现过忘了锁屏然后被发现的情况。自此之后,我就形成了肌肉记忆,同时也对别人不锁屏很敏感。 为什么要强调锁屏呢?你也不想你的电脑被别人操作吧,也不想自己的信息被...
继续阅读 »

写在前面


之前在公司,毕竟是干安全的,部门有这么一个要求,被发现不锁屏的,请全部门喝奶茶。很不幸,我也出现过忘了锁屏然后被发现的情况。自此之后,我就形成了肌肉记忆,同时也对别人不锁屏很敏感。


为什么要强调锁屏呢?你也不想你的电脑被别人操作吧,也不想自己的信息被别人获取吧。毕竟防人之心不可无。


自打跳槽到新公司之后,每次去厕所的路上就看到有人电脑不锁屏,真的是令我无比的纠结。锁个屏幕有那么难吗?确实很难,有时候一忙就容易忘,于是我就想实现一个离开电脑自动锁屏的程序。


分析


这玩意实现也不难,简单思考一下,就是让电脑检测人在不在电脑前面,那就是要试试捕获摄像头了,然后设置一个间隔时间,每隔一段时间截取图片,做人脸识别,没有人脸了就锁屏就行了。


涉及到摄像头图片处理,直接让人联想到opencv,然后再用python实现上面的一套逻辑,就搞定。


代码


安装opencv的库


pip install opencv-python

直接上代码:


import cv2
import time
import os
import platform

# 检测操作系统
def detect_os():
os_name = platform.system()
if os_name == 'Windows':
return 'windows'
elif os_name == 'Darwin':
return 'mac'
else:
return 'other'

# 执行锁屏命令
def lock_screen(os_type):
if os_type == 'windows':
os.system('rundll32.exe user32.dll, LockWorkStation')
elif os_type == 'mac':
os.system('/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend')


# 初始化摄像头
cap = cv2.VideoCapture(0)

# 载入OpenCV的人脸检测模型
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

# 无人状态计时器
no_person_timer = 0
# 设定无人状态时间阈值
NO_PERSON_THRESHOLD = 3

# 检测操作系统类型
os_type = detect_os()

while True:
ret, frame = cap.read()
if not ret:
break

# 转换为灰度图像
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.1, 4)

if len(faces) == 0:
no_person_timer += 1
else:
no_person_timer = 0

# 如果超过阈值,则锁屏
if no_person_timer > NO_PERSON_THRESHOLD:
lock_screen(os_type)
no_person_timer = 0

time.sleep(1)

cap.release()

代码里都做好了注释,很简单,因为windows和macOS的锁屏指令不一样,所以做了个简单的系统平台判断。


可以完美执行,就是它得一直调用摄像头,应该也不会有人真的使用这玩意吧,hhh。


作者:银空飞羽
来源:juejin.cn/post/7317824480911458304
收起阅读 »

终于搞懂了网盘网页是怎么唤醒本地应用了

web
写在前面 用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。 自定义协议 本身单凭浏览器是没有唤醒本地应用这个能力的,...
继续阅读 »

写在前面


用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。


自定义协议


本身单凭浏览器是没有唤醒本地应用这个能力的,不然随便一个网页都能打开你的所有应用那不就乱套了吗。但是电脑系统本身又可以支持这个能力,就是通过配置自定义协议。


举个例子,当你用浏览器打开一个本地的PDF的时候,你会发现上面是file://path/xxx.pdf,这就是系统内置的一个协议,浏览器可以调用这个协议进行文件读取。


那么与之类似的,windows本身也支持用户自定义协议来进行一些操作的,而这个协议就在注册表中进行配置。


配置自定义协议


这里我用VS Code来举例子,最终我要实现通过浏览器打开我电脑上的VS Code。


我们先编写一个注册表文件


Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\vscode]
@="URL:VSCode Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\vscode\shell]

[HKEY_CLASSES_ROOT\vscode\shell\open]

[HKEY_CLASSES_ROOT\vscode\shell\open\command]
@=""D:\VScode\Microsoft VS Code\Code.exe" "%1""

这里我逐行解释



  1. Windows Registry Editor Version 5.00 这行表明该文件是一个 Windows 注册表编辑器文件,这是标准的头部,用于告诉 Windows 如何解析文件。

  2. [HKEY_CLASSES_ROOT\vscode] 这是一个注册表键的开始。在这里,\vscode 表示创建一个名为 vscode 的新键。

  3. @="URL:VSCode Protocol"vscode 键下,这行设置了默认值(表示为 @ ),通过 "URL:VSCode Protocol" 对这个键进行描述。

  4. "URL Protocol"="" 这行是设置一个名为 URL Protocol 的空字符串值。这是代表这个新键是一个 URI 协议。

  5. [HKEY_CLASSES_ROOT\vscode\shell] 创建一个名为 shell 的子键,这是一个固定键,代表GUI界面的处理。

  6. [HKEY_CLASSES_ROOT\vscode\shell\open]shell 下创建一个名为 open 的子键。这耶是一个固定键,open 是一个标准动作,用来执行打开操作。

  7. [HKEY_CLASSES_ROOT\vscode\shell\open\command]open 下创建一个名为 command 的子键。这是一个固定键,指定了当协议被触发时要执行命令。

  8. @=""D:\VScode\Microsoft VS Code\Code.exe" "%1""command 键下,设置默认值为 VSCode 的路径。 "%1" 是一个占位符,用于表示传递给协议的任何参数,这里并无实际用处。


写好了注册表文件后,我们将其保存为 vscode.reg,并双击执行,对话框选择是,相应的注册表信息就被创建出来了。



可以通过注册表中查看。


浏览器打开VS Code


这时,我们打开浏览器,输入 vscode://open



可以看到,就像百度网盘一样,浏览器弹出了询问对话框,然后就可以打开VS Code了。


如果想要在网页上进行打开,也简单


<script>
function openVSCode() {
window.location.href = 'vscode://open/';
}
</script>
<button onclick="openVSCode()">打开 VSCode</button>

写一个简单的JS代码即可。


写在最后


至此,终于是了解了这方面的知识。这就是说,在网盘安装的过程中,就写好了这个注册表文件,自定义了网盘的唤醒协议,才可以被识别。


而我也找到了这个注册表



原来叫baiduyunguanjia协议(不区分大小写),使用 baiduyunguanjia://open 可以打开。


作者:银空飞羽
来源:juejin.cn/post/7320513026188460067
收起阅读 »

都用HTTPS了,还能被查出浏览记录?

最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥? 实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如: DNS查询:通常DNS查询是不会加密的,...
继续阅读 »

最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥?



实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如:



  • DNS查询:通常DNS查询是不会加密的,所以,能看到你DNS查询的观察者(比如运营商)是可以推断出访问的网站

  • IP地址:如果一个网站的IP地址是独一无二的,那么只需看到目标 IP地址,就能推断出用户正在访问哪个网站。当然,这种方式对于多网站共享同一个IP地址(比如CDN)的情况不好使

  • 流量分析:当访问一些网站的特定页面,可能导致特定大小和顺序的数据包,这种模式可能被用来识别访问的网站

  • cookies或其他存储:如果你的浏览器有某个网站的cookies,显然这代表你曾访问过该网站,其他存储信息(比如localStorage)同理


除此之外,还有很多方式可以直接、间接知道你的网站访问情况。


本文将聚焦在HTTPS协议本身,聊聊只考虑HTTPS协议的情况下,你的隐私是如何泄露的。


HTTPS简介


我们每天访问的网站大部分是基于HTTPS协议的,简单来说,HTTPS = HTTP + TLS,其中:



  • HTTP是一种应用层协议,用于在互联网上传输超文本(比如网页内容)。由于HTTP是明文传递,所以并不安全

  • TLS是一种安全协议。TLS在传输层对数据进行加密,确保任何敏感信息在两端(比如客户端和服务器)之间安全传输,不被第三方窃取或篡改


所以理论上,结合了HTTPTLS特性的HTTPS,在数据传输过程是被加密的。但是,TLS建立连接的过程却不一定是加密的。


TLS的握手机制


当我们通过TLS传递加密的HTTP信息之前,需要先建立TLS连接,比如:



  • 当用户首次访问一个HTTPS网站,浏览器开始查询网站服务器时,会发生TLS连接

  • 当页面请求API时,会发生TLS连接


建立连接的过程被称为TLS握手,根据TLS版本不同,握手的步骤会有所区别。



但总体来说,TLS握手是为了达到三个目的:



  1. 协商协议和加密套件:通信的两端确认接下来使用的TLS版本及加密套件

  2. 验证省份:为了防止“中间人”攻击,握手过程中,服务器会向客户端发送其证书,包含服务器公钥和证书授权中心(即CA)签名的身份信息。客户端可以使用这些信息验证服务器的身份

  3. 生成会话密钥:生成用于加密接下来数据传输的密钥


TLS握手机制的缺点


虽然TLS握手机制会建立安全的通信,但在握手初期,数据却是明文发送的,这就造成隐私泄漏的风险。


在握手初期,客户端、服务端会依次发送、接收对方的打招呼信息。首先,客户端会向服务端打招呼(发送client hello信息),该消息包含:



  • 客户端支持的TLS版本

  • 支持的加密套件

  • 一串称为客户端随机数client random)的随机字节

  • SNI等一些服务器信息


服务端接收到上述消息后,会向客户端打招呼(发送server hello消息),再回传一些信息。


其中,SNIServer Name Indication,服务器名称指示)就包含了用户访问的网站域名。


那么,握手过程为什么要包含SNI呢?


这是因为,当多个网站托管在一台服务器上并共享一个IP地址,且每个网站都有自己的SSL证书时,那就没法通过IP地址判断客户端是想和哪个网站建立TLS连接,此时就需要域名信息辅助判断。


打个比方,快递员送货上门时,如果快递单只有收货的小区地址(IP地址),没有具体的门牌号(域名),那就没法将快递送到正确的客户手上(与正确的网站建立TLS连接)。


所以,SNI作为TLS的扩展,会在TLS握手时附带上域名信息。由于打招呼的过程是明文发送的,所以在建立HTTPS连接的过程中,中间人就能知道你访问的域名信息。


企业内部防火墙的访问控制和安全策略,就是通过分析SNI信息完成的。



虽然防火墙可能已经有授信的证书,但可以先分析SNI,根据域名情况再判断要不要进行深度检查,而不是对所有流量都进行深度检查



那么,这种情况下该如何保护个人隐私呢?


Encrypted ClientHello


Encrypted ClientHelloECH)是TLS1.3的一个扩展,用于加密Client Hello消息中的SNI等信息。


当用户访问一个启用ECH的服务器时,网管无法通过观察SNI来窥探域名信息。只有目标服务器才能解密ECH中的SNI,从而保护了用户的隐私。



当然,对于授信的防火墙还是不行,但可以增加检查的成本



开启ECH需要同时满足:



  • 服务器支持TLSECH扩展

  • 客户端支持ECH


比如,cloudflare SNI测试页支持ECH扩展,当你的浏览器不支持ECH时,访问该网站sni会返回plaintext



对于chrome,在chrome://flags/#encrypted-client-hello中,配置ECH支持:



再访问上述网站,sni如果返回encrypted则代表支持ECH


总结


虽然HTTPS连接本身是加密的,但在建立HTTPS的过程中(TLS握手),是有数据明文传输的,其中SNI中包含了服务器的域名信息。


虽然SNI信息的本意是解决同一IP下部署多个网站,每个网站对应不同的SSL证书,但也会泄漏访问的网站地址


ECH通过对TLS握手过程中的敏感信息(主要是SNI)进行加密,为用户提供了更强的隐私保护。


作者:魔术师卡颂
来源:juejin.cn/post/7264753569834958908
收起阅读 »

《年会不能停》豆瓣8.2分,强烈建议所有职场人都去看!

12024年的第一部电影献给了《年会不能停》,豆瓣开分8.1分,现在保持在8.2分,这部片子真的将每个打工人都狠狠代入其中,超级推荐。作为曾经的职场新媒体人看起来,反讽效果拉满,笑点密不能停,台词里句句是职场人的嘴替,有人看着是乐子,有人是照镜子,笑完之后,蓦...
继续阅读 »

1


2024年的第一部电影献给了《年会不能停》,豆瓣开分8.1分,现在保持在8.2分,这部片子真的将每个打工人都狠狠代入其中,超级推荐。


作为曾经的职场新媒体人看起来,反讽效果拉满,笑点密不能停,台词里句句是职场人的嘴替,有人看着是乐子,有人是照镜子,笑完之后,蓦然回首小丑竟是我自己。


影片讲述的是一名厂里的“高级钳工”胡建林(大鹏饰),阴差阳错被调入了公司总部成为人事专员,从“工厂”到“大厂”经过一系列乌龙事件,反而职位越做越高。


深谙职场生存之道的打工人马杰(白客饰),与叛逆的外包员工潘妮(庄达菲饰),俩人就是踏实做事的社畜代表,勤勤恳恳却碌碌无为,甚至连正都转不了。


却与在职场最会被吐槽的胡建林成为“铁三角”组合,在最后年会揭发了公司高层的腐化,从而保住了一个厂全体员工的饭碗。


影片好看之处就在于拍出了当下社会职场的现象,年轻人在这种现状里的疑惑和挣扎。


虽然结尾的“大团圆”结局过于理想主义,但我们也只能在电影中找到这种爽感来出一口对现实的恶气,虽然梦醒之后依旧是加班熬夜低头倒茶。


2


我一直有一件疑问的事,有人真的热爱上班吗?应该有80%的人回复是不爱,但无奈吧。


从毕业之后,经历了几份工作,发现我是真不爱上班,除了拿点每个月准时的“窝囊费”,好像真没什么值得开心的了,还有无止境的加班,内部争斗,还有付出和回报不成正比的委屈。


但偏偏人就要为这几点碎银两,向生活和工作低头,就像代表职场里中年人的马杰一样。


无数个马杰都如蚂蚁一般,勤勤恳恳做事,但反而得不到升职加薪,内心有原则却难守护,就如那句“如果我失业了,家人怎么办”,直击打工人的压力痛点。


相反只有像潘妮,这个角色就代表00后的职场人群,不战队不妥协不随流,就是去整顿职场这些荒谬的规则的。


好不容公司同意转正,她却另辟蹊径,潇洒地递交“世界那么大,我要去看看”的“叛逆”辞职信。


在职场也许我们像中年的胡建林和马杰,但人生不只有工作,更多时候拥有潘妮的“叛逆”和勇敢,寻找更多的人生出口,才会更有趣更有力量一些。


3


我觉得影片很妙的一点是,把两个时代的人物结合到同一个平行空间里,将两代人的职场风格和做事方法也融入到了一起,形成了强烈对比,反差感很强。


最开始以为讲述的是90年代的事,没想到是同时代的打工场景,这种跨越也正好是我们这代和父母辈所经历过的场景,加上拍摄地点和我的职场经历相似, 更有代入感了。


90年代时我妈就曾在汽电厂工作,我小时候也在那种环境中生活过,工人们统一的工服,螺丝钉一样的工作内容,集体主义式的生活,通勤只有2分钟。


工作虽然在身体上辛苦又繁复,但下班就真的是下班,不必看公司和客户消息,电话会议,生活简单又满足。


而作为当代社畜,一定是脑力和体力的双倍付出,996的工作时间,但没有加班工资,做不完的事,没有周末,下班和放假还要守“机”待“工”,工作和生活从来不能完全分开。


时代在变,两代人的职场理想不一样,上一辈的老思想就是一份工作就是一辈子的事,在一个岗位日复一日的劳作,像胡建林一样做到一颗螺丝钉一咬就知道质量对不对。


而如今像这样心思单纯性格执拗的职员,绝对就是裁员名单上的第一批人。


4


从“工厂”跳到“大厂”,从工人到白领,胡建林宛若穿越一般的人,一切事物就像他说的“小刀割屁股,开了眼”了。


不会英语不会大厂里的专业术语,连“优化”也理解错,让裁员变成升职,在这一系列骚操作中“弄拙成巧”,连连高升。


如果按现实来说,是不会在职场存活下来的,电影里就形成强烈反差感,荒诞可笑,却也映射着在职场里靠关系进去的人,不仅不会做事,还会被像财神爷一样供着,具有讽刺意味。


作为在职场8年,5年新媒体经验的我,曾也进入过本地500人的新媒体大公司。


在原本以为进的“大厂”里人人都是专家,能力强,但当你一进去之后,会发现和影片中的体系差不多,高层的就是一帮混酒局的,中层大部分靠的是拍马屁和吹水,真正做事的可能就是底层的基础职员。


但作为底层社畜,尽管看到了职场里的bug和荒谬,就算看清了现实,也逃不过压榨和背锅,只能一边吐槽一边苦干,谁叫自己的饭碗在别人手里呢。


但我觉得不管做任何工作,也不是一味的“隐忍”,现在也不是只靠上班才能赚钱的时代,相比赚钱,我觉得人一定要记得最初的自己。


5


时代的列车呼啸向前,车轮地下总得有人增加摩擦力”,这句话很扎心却现实。


哪怕口罩问题已经结束,现在仍然有很多知名大厂在不断裁员。


打工的怕没工打,没打工的找不到工打,每天大家都活在不稳定的气氛中,就像《年会》里唱的那句:“你是不是也像我,在裁员中忐忑。”


大环境的齿轮一旦转动,谁也逃脱不了被碾压的命运,只能眼睁睁看着事情发生无力改变,才是最可悲的事情,只有在变化中才能求解。


随着知识和经验buff的叠加,我们所能做就是预测时代的节奏,在每一个车轮想要来碾压我们之前,增加自己的动力,去跑赢这辆列车。



END


作者:李猫妮
来源:mp.weixin.qq.com/s/2k6GdooHJnlUOHnckyhFZg

收起阅读 »

前端无感知刷新token & 超时自动退出

web
前端无感知刷新token&超时自动退出 一、token的作用 因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。 以oauth2.0授权码模式为例: 每次请求资...
继续阅读 »

前端无感知刷新token&超时自动退出


一、token的作用


因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。


以oauth2.0授权码模式为例:


oauth2授权码模式.png


每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。


刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。


二、无感知刷新token方案


2.1 刷新方案


当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。


如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。


2.2 原生AJAX请求


2.2.1 http工厂函数


function httpFactory({ method, url, body, headers, readAs, timeout }) {
   const xhr = new XMLHttpRequest()
   xhr.open(method, url)
   xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60

   if(headers){
       forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
  }
   
   const HTTPPromise = new Promise((resolve, reject) => {
       xhr.onload = function () {
           let response;

           if (readAs === 'json') {
               try {
                   response = JSONbig.parse(this.responseText || null);
              } catch {
                   response = this.responseText || null;
              }
          } else if (readAs === 'xml') {
               response = this.responseXML
          } else {
               response = this.responseText
          }

           resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
      }

       xhr.onerror = function () {
           reject(xhr)
      }
       xhr.ontimeout = function () {
           reject({ ...xhr, isTimeout: true })
      }

       beforeSend(xhr)

       body ? xhr.send(body) : xhr.send()

       xhr.onreadystatechange = function () {
           if (xhr.status === 502) {
               reject(xhr)
          }
      }
  })

   // 允许HTTP请求中断
   HTTPPromise.abort = () => xhr.abort()

   return HTTPPromise;
}

2.2.2 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

// 存放因token过期而失败的请求
let requests = []

function httpRequest(config) {
   let abort
   let process = new Promise(async (resolve, reject) => {
       const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
       abort = request.abort
       
       try {                            
           const { status, response, getResponseHeader } = await request

           if(status === 401) {
               try {
                   if (!isRefreshing) {
                       isRefreshing = true
                       
                       // 刷新token
                       await refreshToken()

                       // 按顺序重新发起所有失败的请求
                       const allRequests = [() => resolve(httpRequest(config)), ...requests]
                       allRequests.forEach((cb) => cb())
                  } else {
                       // 正在刷新token,将请求暂存
                       requests = [
                           ...requests,
                          () => resolve(httpRequest(config)),
                      ]
                  }
              } catch(err) {
                   reject(err)
              } finally {
                   isRefreshing = false
                   requests = []
              }
          }                        
      } catch(ex) {
           reject(ex)
      }
  })
   
   process.abort = abort
   return process
}

// 发起请求
httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })

2.3 Axios 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

let requests: ReadonlyArray<(config: any) => void> = []

// 错误响应拦截
axiosInstance.interceptors.response.use((res) => res, async (err) => {
   if (err.response && err.response.status === 401) {
       try {
           if (!isRefreshing) {
               isRefreshing = true
               // 刷新token
               const { access_token } = await refreshToken()

               if (access_token) {
                   axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;

                   requests.forEach((cb) => cb(access_token))
                   requests = []

                   return axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${access_token}`,
                      },
                  })
              }

               throw err
          }

           return new Promise((resolve) => {
               // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
               requests = [
                   ...requests,
                  (token) => resolve(axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${token}`,
                      },
                  })),
              ]
          })
      } catch (e) {
           isRefreshing = false
           throw err
      } finally {
           if (!requests.length) {
               isRefreshing = false
          }
      }
  } else {
       throw err
  }
})

三、长时间无操作超时自动退出


当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。


3.1 操作事件


操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。


特殊事件:某些耗时的功能,比如上传、下载等。


3.2 方案


用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。


在 localStorage 存入两个字段:


名称类型说明说明
lastActiveTimestring最后一次触发操作事件的时间戳
activeEventsstring[ ]特殊事件名称数组

当有操作事件时,将当前时间戳存入 lastActiveTime。


当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。


设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。


3.3 代码实现


const LastTimeKey = 'lastActiveTime'
const activeEventsKey = 'activeEvents'
const debounceWaitTime = 2 * 1000
const IntervalTimeOut = 1 * 60 * 1000

export const updateActivityStatus = debounce(() => {
   localStorage.set(LastTimeKey, new Date().getTime())
}, debounceWaitTime)

/**
* 页面超时未有操作事件退出登录
*/

export function timeout(keepTime = 60) {
   document.addEventListener('mousedown', updateActivityStatus)
   document.addEventListener('mouseover', updateActivityStatus)
   document.addEventListener('wheel', updateActivityStatus)
   document.addEventListener('keydown', updateActivityStatus)

   // 定时器
   let timer;

   const doTimeout = () => {
       timer && clearTimeout(timer)
       localStorage.remove(LastTimeKey)
       document.removeEventListener('mousedown', updateActivityStatus)
       document.removeEventListener('mouseover', updateActivityStatus)
       document.removeEventListener('wheel', updateActivityStatus)
       document.removeEventListener('keydown', updateActivityStatus)

       // 注销token,清空session,回到登录页
       logout()
  }

   /**
    * 重置定时器
    */

   function resetTimer() {
       localStorage.set(LastTimeKey, new Date().getTime())

       if (timer) {
           clearInterval(timer)
      }

       timer = setInterval(() => {
           const isSignin = document.cookie.includes('access_token')
           if (!isSignin) {
               doTimeout()
               return
          }

           const activeEvents = localStorage.get(activeEventsKey)
           if(!isEmpty(activeEvents)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }
           
           const lastTime = Number(localStorage.get(LastTimeKey))

           if (!lastTime || Number.isNaN(lastTime)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }

           const now = new Date().getTime()
           const time = now - lastTime

           if (time >= keepTime) {
               doTimeout()
          }
      }, IntervalTimeOut)
  }

   resetTimer()
}

// 上传操作
function upload() {
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, [...current, 'upload'])
   ...
   // do upload request
   ...
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
}

作者:ww_怒放
来源:juejin.cn/post/7320044522910269478
收起阅读 »

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

2024年,何去何从

如果生命只有35岁,我大抵可以活的绚烂放肆。 可是为了活到70岁,我不得不过得趋炎附势、唯唯诺诺。 2023年12月14日中午。望着公司窗外小河上清理水藻的船工,突然觉得人生好落寞。人生不知不觉已经过去了33个年头,生活过的一团糟,每每空闲就会很迷茫,工作...
继续阅读 »

如果生命只有35岁,我大抵可以活的绚烂放肆。


可是为了活到70岁,我不得不过得趋炎附势、唯唯诺诺。



89977a3eb68cfc3a82b415c9e006ec4.jpg


2023年12月14日中午。望着公司窗外小河上清理水藻的船工,突然觉得人生好落寞。人生不知不觉已经过去了33个年头,生活过的一团糟,每每空闲就会很迷茫,工作中也不知道未来方向在何方。似乎生活走到了一个十字路口,下一步的迈出千头万绪,让人举步不前。


关于读书


eade0765b64b6e958335725416a504b.jpg
最近董宇辉小作文事件在互联网上闹得沸沸扬扬。让我重新审视关于读书人这个称谓。董宇辉的出口成章,辞藻华丽,仿若腹有诗书气自华就是为他而写的一般。之前有一段时间,一直会保持每天至少抽出来半小时读书的习惯,这期间也读了很多好书,也推荐给朋友很多好书,俨然有一种自己是读者的错觉。但是,好景不长,慢慢的读书的习惯在各种乱七八糟的生活琐碎中消磨的也不多了。


2024年关于读书目标,希望自己能读完8本有意思的书籍吧。


以下推荐一些我往年读书挺有意思的书。(我读书有个特点,不会专门为了要从书中获取什么而读书,我单纯可能就是觉得这本书有趣,就会阅读,仁者见仁智者见智,推荐的不喜欢勿喷)



  • 我的二本学生

  • 焦虑的人

  • 时间的礼物

  • 牧羊少年的奇幻旅行

  • 清单革命:如何持续、正确、安全的把事情做好

  • 大雪将至

  • 无人生还

  • 古董局中局(全集)

  • 长安十二时辰

  • 罗布泊之咒


关于学习


作为程序员,最重要的事情,其实就是终身学习。


而我一直认为,一个人活在世上和其他人最大的差异变化,就是在于不断的学习。而我认为学习不光是对于书本中的知识的学习,更是对于人生百态、人情世故的学习。通过不断的学习,让自己的棱角变得圆滑,让自己的短板变的不那么明显。大白话就是通过不断的学习打磨,让自己变的装起来,活的不那么赤裸裸。


如果你觉得这个词,你认知中还是用褒贬来分辨,对于事物还是一味用对错来分辨。那么我觉得应该去学习,通过不断的书本阅读、不断的人情世故的打磨,让自己起来。


你可能不认同,但是你不得不承认,这个社会就是由人情世故组装而成的。你的不断学习是伪装也是武装,让你圈子变得不同。


学习和阅读是一辈子的事。额....我怀念单纯的我


2024学习方面,我个人计划主要是个方面。



  • Python爬虫 & js反编译深入

  • Android jetpack搞一搞

  • 单词背起来

  • 阅读习惯捡起来


关于工作


image.png


这个不重要。按部就班来~


作者:王先生技术栈
来源:juejin.cn/post/7312749480674574372
收起阅读 »

爆料 iPhone 史上最大的漏洞,你中招了吗

卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多 最近 iPhone 因为遭遇史上最复杂攻击,而登上了热搜,卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多,如果你收到了 iPh...
继续阅读 »

卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多



最近 iPhone 因为遭遇史上最复杂攻击,而登上了热搜,卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多,如果你收到了 iPhone 的安全补丁提示,那么赶快升级吧。


OpenAI 科学家 Andrej Karpathy 惊讶地表示:这绝对是我们迄今为止所见过的最为复杂的攻击链。从本次攻击的复杂程度来看,一次黑客攻击同时使用 4 个零日漏洞(也就是未被发现且无有效防范措施的漏洞)是 "极其罕见的",只有历史上著名的 "震网" 病毒攻击伊朗纳坦兹核工厂事件能达到这个级别(当时共利用 7 个漏洞,其中 4 个为零日漏洞)。


这次黑客的攻击手段非常复杂,攻击者只需向用户的 iPhone 发送一段恶意 iMessage 文本,无需用户点击或下载任何内容,就可以在用户不知情的情况下,获取到 iPhone 的最高级别 Root 权限,这应该是利用 Mac 系统大概 10 年都没有修复的一个字体的漏洞。



"iMessage 信息" 是苹果手机 "信息" 中的一种通信方式,可以向其他 iOS 设备、iPadOS 设备、Mac 电脑和 Apple Watch 发送文字、图片、视频和音乐等信息



当获取到 iPhone 最高级别 Root 权限,攻击者将能够在 iPhone 上安装恶意软件(间谍软件),从而收集诸如联系人、消息和位置数据等敏感信息,并传输到攻击者控制的服务器。


但是如果想成功利用这个漏洞,必须对 iPhone 最底层的机制有深入的了解,但是 iPhone 不是开源的系统,所以除了 iPhone 和 ARM 的人,几乎不会有其他人知道这个漏洞的存在。


这次这个漏洞的攻击代码,粗估高达数万行代码,写的非常的精巧复杂,这么高价值的漏洞,不会对个人进行打击,应该是针对非常重要的人物。


比如 2021 年 7 月,以色列发生了一起类似的事件,代号为 "飞马" 间谍软件攻击事件,它可以秘密安装在运行大多数版本的 iOS 和 Android 的手机(和其他设备) 上,这次的攻击持续了很多年,从 2014 年开始,一直持续到 2021 年 7 月媒体曝光之时,监听对象都是非常重要的人物。


但是如果黑客将这次的攻击代码开源,那么很多人都可以利用这个漏洞为所欲为了,造成的结果就是无差别攻击,这样对我们普通人就危险了,如果你收到了 iPhone 的安全补丁提示,那么赶快升级,转发给身边的朋友,提高警惕吧


这些年来无论在 Android 还是 iPhone, 都发现了相应的漏洞,iPhone 号称史上最安全的操作系统,都出现了这么严重的漏洞,这也再次说明了,无论多好的软件系统,都有不可避免的漏洞,一定会被人攻击。


比如在 2023 年 Android 手机上也被暴露一个漏洞,虽然这个漏洞很早被 Google 修复了,但是并不是所有人都会升级到新版本系统,所以某些大厂,利用这个被暴露出来漏洞,获取到 Android 手机上最高级别 Root 权限,攻击普通用户,控制他们的手机,获取用户大量的私人信息。而且这次攻击也持续了很多年,被曝光之时引起轩然大波,但是在其强大的财力和公关的操作下,事情很快平息了。


我一直认为技术应该服务于用户,而不是想方设法的利用公开的漏洞窃听用户的私人信息,去推送一些定制化私人广告。


全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!


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

年底喜提大礼包,分享一下日常

写在前面 元旦前喜提大礼包,因为公司的骚操作越来越多,福利越来越少,通勤时间太久等诸多原因所以主动要了裁员名额,现在给大家分享一下这几天的日常和心态吧。 找工作 看了一下最近的行情,基本是属于失业了,从离职到现在有差不多2周了,这2周里刷了一下Boss和拉钩,...
继续阅读 »

写在前面


元旦前喜提大礼包,因为公司的骚操作越来越多,福利越来越少,通勤时间太久等诸多原因所以主动要了裁员名额,现在给大家分享一下这几天的日常和心态吧。


找工作


看了一下最近的行情,基本是属于失业了,从离职到现在有差不多2周了,这2周里刷了一下Boss和拉钩,挂出来的职位倒是不少,我也投了几份简历,无一例外全都石沉大海,看来只有放个寒假过完年再说了。最近也没怎么刷面经,先好好的休息一段时间吧。


image.png


日常


这两周也没有出去玩之类的,因为老婆还没有放假,基本都是宅在家里,做做饭,玩玩游戏,写写私活,分享一下我家2只可爱的猫猫


eb4561e2e29b304bf091d2638caf153.jpg


展望


希望过完年可以找到满意的工作吧,实在不行也只能去外包了,最近也准备换换赛道,尝试一下自媒体。


写在后面


虽然失业了,但是心态上还好,有一点焦虑但是不多,可能是因为写私活占了一部分时间,没空去胡思乱想吧,希望各位待业大佬都能放平心态,好好提升自己,加油。


作者:hahayq
来源:juejin.cn/post/7320037969980702761
收起阅读 »

年会了,公司想要一个离线PC抽奖应用

web
年会了,公司想要一个离线PC抽奖应用 背景 公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC...
继续阅读 »

年会了,公司想要一个离线PC抽奖应用


封面截图.png


背景


公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC抽奖应用,所有就有了这么一个主题。


程序需求


以下是领导从其他地方复制粘贴过来的,就是想实现类似的效果而已。



  • 1、支持数字、字母、手机号、姓名部门+姓名、编号、身-份-证号等任意组合的抽奖形式。

  • 2、支持名单粘贴功能,从EXCEL、WORD、TXT等任何可以复制的地方复制名单数据内容,粘贴至抽奖软件中作为名单使用,比导入更方便。

  • 3、支持标题、副标题、奖项提示信息、奖品图片等都可以通过拖拽更改位置。

  • 4、支持内定指定中奖者。

  • 5、支持万人抽奖,非常流畅,中奖机率一致,保证公平性。

  • 6、支持中奖不重复,软件自动排除已中奖人员,每人只有一次中奖机会不会出现重复中奖。

  • 7、支持临时追加奖项、补奖等功能支持自定义公司名称、自定义标题。

  • 8、背景图片,音乐等。

  • 9、支持抽奖过程会自动备份中奖名单(不用担心断电没保存中奖名单)。

  • 10、支持任意添加奖项、标题文字奖项名额,自由设置每次抽奖人数设置不同的字体大小和列数。

  • 11、支持空格或回车键抽奖。

  • 12、支持临时增加摇号/抽奖名单,临时删掉不在场人员名单。


目前未实现的效果


有几个还没实现的



  1. 关于人员信息的任意组合抽奖形式,这边只固定了上传模板的表头,需要组合只能通过改excel的内容。

  2. 对于临时不在场名单,目前只能通过改excel表再上传才能达到效果。


技术选型


由于给的时间不多,能有现成的最好;最终选择下面的开源项目进行集成和修改。


说明:由于之前没看到有electron-vite-vue这个项目,所有自己粗略了用vue3+vite+electron开发了 抽奖程序 , 所以现在就是迁移项目的说明。


github开源项目



根据仓库的说明运行起来


动画.gif


修改web端代码并集成到electron


I 拆分页面


组件说明拼图.png


II 补充组件


​ 本人根据自己想法加了背景图片、奖品展示、操作按钮区、展示全部中奖人员名单这几个组件以及另外9个弹窗设置组件。


III 页面目录结构


目录结构.png

IV 最后就是对开源的网页抽奖项目进行大量的修改了,这里就不详细说了;因为变化太多了,一时半会想不起来改了什么。


迁移项目


I 迁移静态资源


静态资源.png


​ 关于包资源说明,这边因为要做离线的软件,所以我把固定要使用的包保存到本地了;


1. 引入到index.html中

引入资源.png


2. 引入图片静态资源

功能代码调整.png


II 迁移electron代码


说明:由于我之前写的一版代码是用js而不是ts,如果一下子全改为ts需要一些时间;所以嫌麻烦,我直接引用js文件了,后期有时间可以再优化一下。


功能代码调整.png




  1. 这时候先运行一下,看下有没有问题



​ 问题一:


问题一.png


​ 这个是因为 我之前的项目一直是用require 引入的;所以要把里面用到require都改为import引入方式;(在preload.ts里面不能用ESM导入的形式,会报语法错误,要用回require导入)


​ 问题二:


问题二.png


​ __dirname不是ESM默认的变量;改为


import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url))

III 迁移前端代码



  • 目录说明


前端功能代码.png



  • 然后一顿复制粘贴,运行,最后报错;


问题三.png
按提示来改 如下:


修改1.png



  • 问题2:资源报错
    资源报错.png
    修复:


资源变化.png



  • 接下来运行看下是否有问题;
    抽奖运行动画.gif
    ​ 运行成功

  • 下一步试一下功能


功能执行动画.gif
​ 功能报错了



  • 看后台错误打印并修复问题
    保存位置错误.png
    修改:
    路径保存源头.png

  • 再次尝试功能 - 成功
    功能执行动画2.gif


IV 一个流程下来


待使用-删除帧后-运行抽奖一个流程动画-gif.gif


打包安装运行


I 运行“npm run build”之后 报错了


打包-js报错.png
这里再次说明一下;由于本人懒得把原本js文件的代码 改为ts;要快速迁移项目 所以直接使用了js;导致打包报错了,所以需要再 tsconfig.json配置一下才行:


  "compilerOptions": {
"allowJs": true // 把这段加上去
},

II 图标和应用名称错误


default Electron icon is used  reason=application icon is not set
building block map blockMapFile=release\28.0.0\YourAppName-Windows-28.0.0-Setup.exe.blockmap

找到打包的配置文件(electron-builder.json5)进行修改:


1. 更改应用名称
"productName": "抽奖程序",

2. 添加icon图标
"win": {
"icon": "electron/controller/data/img/lottery_icon.ico", // ico保存的位置
},

III 打包后运行;资源路径报错了


打包后资源报错.png
打包后资源路径查询不到.png
由于上面的原因,需要把程序涉及读写的文件目录暴露出来;


1. 在构建配置中加入如下配置,将应用要读写的文件目录暴露出来
"extraResources": [
{
"from": "electron/assets",
"to": "assets"
}
],

剩下的就是要重新调整打包后的代码路径了,保证能够找到读写路径;
路径查找纠正.png


最后打包成功,运行项目


删除帧后-一个完整的流程-gif.gif


总结: 主打的要快速实现,所以这个离线pc抽奖程序还有很多问题,希望大家多多包容;


最后附上github地址:github.com/programbao/…
欢迎大家使用


作者:宝programbao
来源:juejin.cn/post/7319795736153210895
收起阅读 »

iOS 组件开发教程——手把手轻松实现灵动岛

1、先在项目里创建一个Widget Target2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivi...
继续阅读 »

1、先在项目里创建一个Widget Target


2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。


3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivities,并将其布尔值设置为 YES。

4、我们创建一个IMAttributes,

struct IMAttributes: ActivityAttributes {
public typealias IMStatus = ContentState

public struct ContentState: Codable, Hashable {
var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

5、灵动岛界面配置

struct IMActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: IMAttributes.self) { context in
// 创建显示在锁定屏幕上的演示,并在不支持动态岛的设备的主屏幕上作为横幅。
// 展示锁屏页面的 UI

} dynamicIsland: { context in
// 创建显示在动态岛中的内容。
DynamicIsland {
//这里创建拓展内容(长按灵动岛)
DynamicIslandExpandedRegion(.leading) {
Label(context.state.callName, systemImage: "person")
.font(.caption)
.padding()
}
DynamicIslandExpandedRegion(.trailing) {
Label {
Text(timerInterval: context.state.callingTimer, countsDown: false)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
.font(.caption2)
} icon: {
Image(systemName: "timer")
}
.font(.title2)
}
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.callName) 正在通话中...")
.lineLimit(1)
.font(.caption)
.foregroundColor(.secondary)
}

}
//下面是紧凑展示内容区(只展示一个时的视图)
compactLeading: {
Label {
Text(context.state.callName)

} icon: {
Image(systemName: "person")
}
.font(.caption2)
} compactTrailing: {
Text(timerInterval: context.state.callingTimer, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
}
//当多个Live Activities处于活动时,展示此处极小视图
minimal: {
VStack(alignment: .center) {
Image(systemName: "person")


}
}
.keylineTint(.accentColor)
}
}
}

6、在需要的地方启动的地方调用,下面是启动灵动岛的代码

        let imAttributes = IMAttributes(callName: "wqd", imageStr:"¥99", callingTimer: Date()...Date().addingTimeInterval(0))

//初始化动态数据
let initialContentState = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

do {
//启用灵动岛
//灵动岛只支持Iphone,areActivitiesEnabled用来判断设备是否支持,即便是不支持的设备,依旧可以提供不支持的样式展示
if #available(iOS 16.1, *) {
if ActivityAuthorizationInfo().areActivitiesEnabled == true{

}
} else {
// Fallback on earlier versions
}
let deliveryActivity = try Activity<IMAttributes>.request(
attributes: imAttributes,
contentState: initialContentState,
pushType: nil)
//判断启动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
//不是每次启动都会成功,当已经存在多个Live activity时会出现启动失败的情况
if deliveryActivity.activityState == .active{
_ = deliveryActivity.pushToken
}
// deliveryActivity.pushTokenUpdates //监听token变化
print("Current activity id -> \(deliveryActivity.id)")
} catch (let error) {
print("Error info -> \(error.localizedDescription)")
}
6.此处只有一个灵动岛,当一个项目有多个灵动岛时,需要判断更新对应的activity

func update(name:String) {
Task {

let updatedDeliveryStatus = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

for activity in Activity<IMAttributes>.activities{
await activity.update(using: updatedDeliveryStatus)
}
}
}

7、停止灵动岛

func stop() {
Task {
for activity in Activity<IMAttributes>.activities{
await activity.end(dismissalPolicy: .immediate)
}
}
}


收起阅读 »

被裁员后,去送外卖跑滴滴行得通吗?

一 近年来,职场的裁员和降薪已经屡见不鲜,不少同事和朋友领了“大红包”。 有的在疯狂找工作,有的暂时摆烂下来。 打开BOSS招聘,上面的岗位还是很多啊,需求量还是很大啊! 但是不好意思,其实和大部分人关系不是很大。 我们发现一个问题,大家都说找不到工作,但是企...
继续阅读 »


近年来,职场的裁员和降薪已经屡见不鲜,不少同事和朋友领了“大红包”。


有的在疯狂找工作,有的暂时摆烂下来。


打开BOSS招聘,上面的岗位还是很多啊,需求量还是很大啊!


但是不好意思,其实和大部分人关系不是很大。


我们发现一个问题,大家都说找不到工作,但是企业又在抱怨苦苦招不到人。


什么原因呢?


究其原因,中低端岗位虽然多,但是人数堪比考公务员,造成了狼多肉少的现象,如果你的牙齿不够长,不够硬,那么挺难,运气好的话可能能碰到,但是可能是生病的猎物。


高端岗位因为对薪资进行压缩,那些精英不想降低标准去,而精英只占社会群体的一小撮,大多都在寻找更好的机会,或者落差不大的机会。


所以就传出:企业招不到人,大把人找不到工作的现象。


但是很显然,我们大部分人很难突进成精英,就像码农大多很难成为CTO,架构师,领导者,运营大多无法成为总监......


我们大多数人注定就是一颗螺丝钉。


这是我们大部人的宿命,这是必须得承认的。


不少朋友说:妈的,实在不行,老子就去送外卖,去开滴滴,没什么大不了的!


貌似大家都认为这是自己职业的底线,把送外卖和开网约车作为人生的兜底方案。


但是不好意思,外卖,网约车也不是什么人都能去干的,现在门槛也高了,能赚到的越来越少。


你可能看到视频中外卖月薪超过2W,但是你不可能不知道这钱是怎么赚来的。


可以用那个梗来形容:多吗?拿命换的。


其实另外一个现实的问题是,想玩命,想卷也没机会啊,这绝非贩卖焦虑,这是铁打的事实!



在我读六年级的时候。


我的同桌是一个女同学,她父母都是出租车司机,说实话,那会儿,我可觉得出租车司机比编制牛逼多了。


所以她和我说话总是提高半个调,时不时言语中带出几个词,表明她父母是出租车司机。


那会儿,没有滴滴,没有曹操,没有T3,没有智能手机,没有跑黑车的。


所以,他们出租车司机吃得油光满面,合不拢嘴。


但是时代变了,现在你打一个出租车,和司机聊上两句,他就差点扑倒你怀里哭了起来。


打网约车也是如此,很多司机在你下车时,还客客气气对你说:可以给我个好评吗,谢谢你了。


为啥要好评呢,数据啊,数据好看,给你推的单子就多啊。


前几天看了一个视频,一个女网约车司机说:自己开了一两个月了,单子还是那么难接,再这样下去,吃不起饭了。


还别说,这些平台依旧将司机分为三六九等,等级越高,自然单子就多,等级低的,慢慢来吧。


也怪不了平台,大家都在这个城市里,单子就这么多,加入的司机越来越多,如果大家都是公平去抢单,显然不符合商业的发展。


除了各种平台的竞争,在出行方式上也是卷得一比。


刚开始是共享自行车,再到电瓶车,刚开始要押金,后面我干脆直接不要押金。


这还不够,我还送,一个月十来块钱,我可以让你把大腿肌肉练强壮,链条干起火花。


对于大城市中的打工人,距离远我选择地铁,距离近我选择自行车,小城市里面,我更愿意选择公交和共享电动车。


难啊,出租车司机哭生不逢时,网约车司机拍拍大腿:这TM就是人生!


......



外卖就好搞吗?


我一个朋友,多年前他是一个外卖资深玩家,是城市里面的蝙蝠侠,闭着眼睛都能找路,眼睛一眨就把外卖送到顾客手里。


五六年前,他在一个四五线线城市一个月都能赚取可观的收入。


2023年下半年,他又重新加入了外卖大军,但是干了四五个月,他顶不住了,直接走人,他当时还是在东莞送,东莞的人口不少哦。


我问他为啥不干了,他无奈说到:现在这个行业,狗看了都摇头。


高单价的单子抢不到,能抢到的单子价格又低。


一天跑200块钱都挺难。


可能你不信,但是这就是事实。


在东莞的对面,那是深圳,年轻人梦想的起点,无数人来到深圳,极少的人确实赚到钱了,但是更多的人都是处于深圳赚钱深圳花,一分别想带回家的状态。


这里的人多,如果肯干,加上有一定的策略,那么一个月跑万把块是可以的,但是会特别累。


更多的人其实是破不了万的。


除了行情问题,还要面对巨大的身体和心理压力,价值送外卖是一件比较危险的事。


很多人穿上黄袍不久,扛不住了,只能脱下。


外卖是有门槛的,它肯定会比你现在的工作辛苦得多,把它作为兜底方案,这是不现实的。


特别是现在就业形式的严峻,更多的人都加入这个行业,竞争大得不行,所以想从里面赚钱也是挺难得。



最后。


谈一下一个现实的问题。


有力无处使,有才无数施,干了活不重要,重要的是要有运气拿钱!


在社会劳动力过剩的形势下,个人的才能其实没多大用处,除非是大才,普才的话只能在夹缝中苟延残喘。


一网友说:躺了很久,发现996真的是福报,在这个畸形的环境里,有钱挣、有活干、有苦吃、有罪受真是一大幸事!

我们大多数人是讨厌职场中的奋斗逼和卷狗的,但是当现实当头一棒的时候,估计自己卷得比别人还厉害。


这其实和康风险能力有关,普通家庭,普通收入的工薪阶层,收入完全依赖于工资,但是要还房贷,车贷,养娃,所以基本上收入和支出持平。


那失业就是最可怕的事情。


现在市面上处于待业的人还是比较多,有力无处使。


因为市场上的业务基本处于平缓甚至下滑的状态,部分处于直线上升的业务自己又去不了。


所以难啊。


这样的形势下,我们普通人又该何去何从?


诸君怎么看?


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

前端服务框架调研:Next.js、Nuxt.js、Nest.js、Fastify

web
概述 这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。 最终选取了以下具有代表性的框架: Next.js、Nuxt.js:它们是分别与...
继续阅读 »

概述


这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。


最终选取了以下具有代表性的框架:



  • Next.js、Nuxt.js:它们是分别与特定前端技术 React、Vue 绑定的前端应用开发框架,有一定的相似性,可以放在一起进行调研对比。

  • Nest.js:是“Angular 的服务端实现”,基于装饰器。可以使用任何兼容的 http 提供程序,如 Express、Fastify 替换底层内核。可用于 http、rpc、graphql 服务,对提供更多样的服务能力有一定参考价值。

  • Fastify:一个使用插件模式组织代码且支持并基于 schema 做了运行效率提升的比较纯粹的偏底层的 web 框架。


Next.js、Nuxt.js


这两个框架的重心都在 Web 部分,对 UI 呈现部分的代码的组织方式、服务器端渲染功能等提供了完善的支持。



  • Next.js:React Web 应用框架,调研版本为 12.0.x。

  • Nuxt.js:Vue Web 应用框架,调研版本为 2.15.x。


功能


首先是路由部分:



  • 页面路由:

    • 相同的是两者都遵循文件即路由的设计。默认以 pages 文件夹为入口,生成对应的路由结构,文件夹内的所有文件都会被当做路由入口文件,支持多层级,会根据层级生成路由地址。同时如果文件名为 index 则会被省略,即 /pages/users 和 /pages/users/index 文件对应的访问地址都是 users。

    • 不同的是,根据依赖的前端框架的不同,生成的路由配置和实现不同:

      • Next.js:由于 React 没有官方的路由实现,Next.js 做了自己的路由实现。

      • Nuxt.js:基于 vue-router,在编译时会生成 vue-router 结构的路由配置,同时也支持子路由,路由文件同名的文件夹下的文件会变成子路由,如 article.js,article/a.js,article/b.js,a 和 b 就是 article 的子路由,可配合 组件进行子路由渲染。





  • api 路由:

    • Next.js:在 9.x 版本之后添加了此功能的支持,在 pages/api/ 文件夹下(为什么放在pages文件夹下有设计上的历史包袱)的文件会作为 api 生效,不会进入 React 前端路由中。命名规则相同,pages/api/article/[id].js -> /api/article/123。其文件导出模块与页面路由导出不同,但不是重点。

    • Nuxt.js:官方未提供支持,但是有其他实现途径,如使用框架的 serverMiddleware 能力。



  • 动态路由:两者都支持动态路由访问,但是命名规则不同:

    • Next.js:使用中括号命名,/pages/article/[id].js -> /pages/article/123。

    • Nuxt.js:使用下划线命名,/pages/article/_id.js -> /pages/article/123。



  • 路由加载:两者都内建提供了 link 类型组件(LinkNuxtLink),当使用这个组件替代 标签进行路由跳转时,组件会检测链接是否命中路由,如果命中,则组件出现在视口后会触发对对应路由的 js 等资源的加载,并且点击跳转时使用路由跳转,不会重新加载页面,也不需要再等待获取渲染所需 js 等资源文件。

  • 出错兜底:两者都提供了错误码响应的兜底跳转,只要 pages 文件夹下提供了 http 错误码命名的页面路由,当其他路由发生响应错误时,就会跳转到到错误码路由页面。


在根据文件结构生成路由配置之后,我们来看下在代码组织方式上的区别:



  • 路由组件:两者没有区别,都是使用默认导出组件的方式决定路由渲染内容,React 导出 React 组件,Vue 导出 Vue 组件:

    • Next.js:一个普普通通的 React 组件:
      export default function About() {
      return <div>About usdiv>
      }


    • Nuxt.js:一个普普通通的 Vue 组件:







在编译构建方面,两者都是基于 webpack 搭建的编译流程,并在配置文件中通过函数参数的方式暴露了 webpack 配置对象,未做什么限制。其他值得注意的一点是 Next.js 在 v12.x.x 版本中将代码压缩代码和与原本的 babel 转译换为了 swc,这是一个使用 Rust 开发的更快的编译工具,在前端构建方面,还有一些其他非基于 JavaScript 实现的工具,如 ESbuild。


在扩展框架能力方面,Next.js 直接提供了较丰富的服务能力,Nuxt.js 则设计了模块和插件系统来进行扩展。


Nest.js


Nest.js 是“Angular 的服务端实现”,基于装饰器。Nest.js 与其他前端服务框架或库的设计思路完全不同。我们通过查看请求生命周期中的几个节点的用法来体验下 Nest.js 的设计方式。


先来看下 Nest.js 完整的的生命周期:



  1. 收到请求

  2. 中间件

    1. 全局绑定的中间件

    2. 路径中指定的 Module 绑定的中间件



  3. 守卫

    1. 全局守卫

    2. Controller 守卫

    3. Route 守卫



  4. 拦截器(Controller 之前)

    1. 全局

    2. Controller 拦截器

    3. Route 拦截器



  5. 管道

    1. 全局管道

    2. Controller 管道

    3. Route 管道

    4. Route 参数管道



  6. Controller(方法处理器)

  7. 服务

  8. 拦截器(Controller 之后)

    1. Router 拦截器

    2. Controller 拦截器

    3. 全局拦截器



  9. 异常过滤器

    1. 路由

    2. 控制器

    3. 全局



  10. 服务器响应


可以看到根据功能特点拆分的比较细,其中拦截器在 Controller 前后都有,与 Koa 洋葱圈模型类似。


功能设计


首先看下路由部分,即最中心的 Controller:



  • 路径:使用装饰器装饰 @Controller 和 @GET 等装饰 Controller 类,来定义路由解析规则。如:
    import { Controller, Get, Post } from '@nestjs/common'

    @Controller('cats')
    export class CatsController {
    @Post()
    create(): string {
    return 'This action adds a new cat'
    }

    @Get('sub')
    findAll(): string {
    return 'This action returns all cats'
    }
    }

    定义了 /cats post 请求和 /cats/sub get 请求的处理函数。

  • 响应:状态码、响应头等都可以通过装饰器设置。当然也可以直接写。如:
    @HttpCode(204)
    @Header('Cache-Control', 'none')
    create(response: Response) {
    // 或 response.setHeader('Cache-Control', 'none')
    return 'This action adds a new cat'
    }


  • 参数解析:
    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat'
    }


  • 请求处理的其他能力方式类似。


再来看看生命周期中其中几种其他的处理能力:



  • 中间件:声明式的注册方法:
    @Module({})
    export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer
    // 应用 cors、LoggerMiddleware 于 cats 路由 GET 方法
    .apply(LoggerMiddleware)
    .forRoutes({ path: 'cats', method: RequestMethod.GET })
    }
    }


  • 异常过滤器(在特定范围捕获特定异常并处理),可作用于单个路由,整个控制器或全局:
    // 程序需要抛出特定的类型错误
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)

    // 定义
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()

    response
    .status(status)
    .json({
    statusCode: status,
    timestamp: new Date().toISOString(),
    path: request.url,
    })
    }
    }
    // 使用,此时 ForbiddenException 错误就会被 HttpExceptionFilter 捕获进入 HttpExceptionFilter 处理流程
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create() {
    throw new ForbiddenException()
    }


  • 守卫:返回 boolean 值,会根据返回值决定是否继续执行后续声明周期:
    // 声明时需要使用 @Injectable 装饰且实现 CanActivate 并返回 boolean 值
    @Injectable()
    export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
    return validateRequest(context);
    }
    }

    // 使用时装饰 controller、handler 或全局注册
    @UseGuards(new AuthGuard())
    async create() {
    return 'This action adds a new cat'
    }


  • 管道(更侧重对参数的处理,可以理解为 controller 逻辑的一部分,更声明式):

    1. 校验:参数类型校验,在使用 TypeScript 开发的程序中的运行时进行参数类型校验。

    2. 转化:参数类型的转化,或者由原始参数求取二级参数,供 controllers 使用:


    @Get(':id')
    findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
    // 使用 id param 通过 UserByIdPipe 读取到 UserEntity
    return userEntity
    }



我们再来简单的看下 Nest.js 对不同应用类型和不同 http 提供服务是怎样做适配的:



  • 不同应用类型:Nest.js 支持 Http、GraphQL、Websocket 应用,在大部分情况下,在这些类型的应用中生命周期的功能是一致的,所以 Nest.js 提供了上下文类 ArgumentsHostExecutionContext,如使用 host.switchToRpc()host.switchToHttp() 来处理这一差异,保障生命周期函数的入参一致。

  • 不同的 http 提供服务则是使用不同的适配器,Nest.js 的默认内核是 Express,但是官方提供了 FastifyAdapter 适配器用于切换到 Fastify。


Fastify


有这么一个框架依靠数据结构和类型做了不同的事情,就是 Fastify。它的官方说明的特点就是“快”,它提升速度的实现是我们关注的重点。


我们先来看看开发示例:


const routes = require('./routes')
const fastify = require('fastify')({
logger: true
})

fastify.register(tokens)

fastify.register(routes)

fastify.listen(3000, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
fastify.log.info(`server listening on ${address}`)
})

class Tokens {
constructor () {}
get (name) {
return '123'
}
}

function tokens (fastify) {
fastify.decorate('tokens', new Tokens())
}

module.exports = tokens

// routes.js
class Tokens {
constructor() { }
get(name) {
return '123'
}
}

const options = {
schema: {
querystring: {
name: { type: 'string' },
},
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
token: { type: 'string' }
}
}
}
}
}

function routes(fastify, opts, done) {
fastify.decorate('tokens', new Tokens())

fastify.get('/', options, async (request, reply) => {
reply.send({
name: request.query.name,
token: fastify.tokens.get(request.query.name)
})
})
done()
}
module.exports = routes

可以注意到的两点是:



  1. 在路由定义时,传入了一个请求的 schema,在官方文档中也说对响应的 schema 定义可以让 Fastify 的吞吐量上升 10%-20%。

  2. Fastify 使用 decorate 的方式对 Fastify 能力进行增强,也可以将 decorate 部分提取到其他文件,使用 register 的方式创建全新的上下文的方式进行封装。


没体现到的是 Fastify 请求介入的支持方式是使用生命周期 Hook,由于这是个对前端(Vue、React、Webpack)来说很常见的做法就不再介绍。


我们重点再来看一下 Fastify 的提速原理。


如何提速


有三个比较关键的包,按照重要性排分别是:



  1. fast-json-stringify

  2. find-my-way

  3. reusify



  • fast-json-stringify:
    const fastJson = require('fast-json-stringify')
    const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
    firstName: {
    type: 'string'
    },
    lastName: {
    type: 'string'
    }
    }
    })

    const result = stringify({
    firstName: 'Matteo',
    lastName: 'Collina',
    })


    • 与 JSON.stringify 功能相同,在负载较小时,速度更快。

    • 其原理是在执行阶段先根据字段类型定义提前生成取字段值的字符串拼装的函数,如:
      function stringify (obj) {
      return `{"firstName":"${obj.firstName}","lastName":"${obj.lastName}"}`
      }

      相当于省略了对字段值的类型的判断,省略了每次执行时都要进行的一些遍历、类型判断,当然真实的函数内容比这个要复杂的多。那么引申而言,只要能够知道数据的结构和类型,我们都可以将这套优化逻辑复制过去。



  • find-my-way:将注册的路由生成了压缩前缀树的结构,根据基准测试的数据显示是速度最快的路由库中功能最全的。

  • reusify:在 Fastify 官方提供的中间件机制依赖库中,使用了此库,可复用对象和函数,避免创建和回收开销,此库对于使用者有一些基于 v8 引擎优化的使用要求。在 Fastify 中主要用于上下文对象的复用。


总结



  • 在路由结构的设计上,Next.js、Nuxt.js 都采用了文件结构即路由的设计方式。Ada 也是使用文件结构约定式的方式。

  • 在渲染方面 Next.js、Nuxt.js 都没有将根组件之外的结构的渲染直接体现在路由处理的流程上,隐藏了实现细节,但是可以以更偏向配置化的方式由根组件决定组件之外的结构的渲染(head 内容)。同时渲染数据的请求由于和路由组件联系紧密也都没有分离到另外的文件,不论是 Next.js 的路由文件同时导出各种数据获取函数还是 Nuxt.js 的在组件上直接增加 Vue options 之外的配置或函数,都可以看做对组件的一种增强。Ada 的方式有所不同,路由文件夹下并没有直接导出组件,而是需要根据运行环境导出不同的处理函数和模块,如服务器端对应的 index.server.js 文件中需要导出 HTTP 请求方式同名的 GET、POST 函数,开发人员可以在函数内做一些数据预取操作、页面模板渲染等;客户端对应的 index.js 文件则需要导出组件挂载代码。

  • 在渲染性能提升方面,Next.js、Nuxt.js 也都采取了相同的策略:静态生成、提前加载匹配到的路由的资源文件、preload 等,可以参考优化。

  • 在请求介入上(即中间件):

    • Next.js、Nuxt.js 未对中间件做功能划分,采取的都是类似 Express 或 Koa 使用 next() 函数控制流程的方式,而 Nest.js 则将更直接的按照功能特征分成了几种规范化的实现。

    • 不谈应用级别整体配置的用法,Nuxt.js 是由路由来定义需要哪个中间件,Nest.js 也更像 Nuxt.js 由路由来决定的方式使用装饰器配置在路由 handler、Controller 上,而 Next.js 的中间件会对同级及下级路由产生影响,由中间件来决定影响范围,是两种完全相反的控制思路。

    • Ada 架构基于 Koa 内核,但是内部中间件实现也与 Nest.js 类似,将执行流程抽象成了几个生命周期,将中间件做成了不同生命周期内功能类型不同的任务函数。对于开发人员未暴露自定义生命周期的功能,但是基于代码复用层面,也提供了服务器端扩展、Web 模块扩展等能力,由于 Ada 可以对页面路由、API 路由、服务器端扩展、Web 模块等统称为工件的文件进行独立上线,为了稳定性和明确影响范围等方面考虑,也是由路由主动调用的方式决定自己需要启用哪些扩展能力。



  • Nest.js 官方基于装饰器提供了文档化的能力,利用类型声明( 如解析 TypeScript 语法、GraphQL 结构定义 )生成接口文档是比较普遍的做法。不过虽然 Nest.js 对 TypeScript 支持很好,也没有直接解决运行时的类型校验问题,不过可以通过管道、中间件达成。

  • Fastify 则着手于底层细节进行运行效率提升,且可谓做到了极致。同时越是基于底层的实现越能够使用在越多的场景中。其路由匹配和上下文复用的优化方式可以在之后进行进一步的落地调研。

  • 除此之外 swc、ESBuild 等提升开发体验和上线速度的工具也是需要落地调研的一个方向。




作者:智联大前端
来源:juejin.cn/post/7030995965272129567
收起阅读 »

裸辞四个月,前端仔靠着Nest绝境收下offer

经历时间轴 地点:上海 8.31做完后离职 9月份开始边复习边玩,轻松加愉快 10月中旬开始投递简历 11月月底绝望,期间仅有1次面试机会,伴随着的是各种焦虑 12月开始随遇而安,每天复习或者学习2小时,然后就是打游戏,刷剧,中旬突然开始了1波疯狂面试,1周...
继续阅读 »

经历时间轴



  • 地点:上海

  • 8.31做完后离职

  • 9月份开始边复习边玩,轻松加愉快

  • 10月中旬开始投递简历

  • 11月月底绝望,期间仅有1次面试机会,伴随着的是各种焦虑

  • 12月开始随遇而安,每天复习或者学习2小时,然后就是打游戏,刷剧,中旬突然开始了1波疯狂面试,1周8面,收到满意offer



后面开始述说离职原因、心理历程,这几个月我经历了什么?



离职原因



  1. 某独角兽 外包仔

  2. 无晋升空间,时间1年半,作为项目组第一个前端,我完成了80%工作,包括脚手架、组件库等基础建设,直到三四个月后才陆陆续续来了其他四五个前端,一块干活。一到填绩效都有我,每次表扬都有我,我都成标兵了。领导每个季度向上汇报时,都提出对我进行转正,从无名额为由到最后的不予回复。失望+1

  3. 9106式工作,阿谀奉承式的敏捷开发,从一开始还是有计划,有设计的迭代,到后面不断图快式开发,跨层汇报。后期演变为了,领导说这样做OK,做完了,领导的领导觉得不OK,重做另一种,做完后,领导的领导的领导觉得不OK,来回返工,以人力不停试错。失望+1

  4. 薪资差距,外包仔 工资每年仅有12个月工资,最低社保,最低公积金,做好做差都一样,没有任何加班工资,说好的调休,申请调休时,层层受阻,PUA不断,失望+1

  5. 僵硬、不思进取的氛围,领导最喜欢说的话,先上一版再说,并且去除了husky,eslint、tslint等提交校验,导致代码屎山不断,相似的功能、逻辑、同事一致在进行大批量复制,甚至有非常强的耦合性,不拆分组件、hook,甚至无视TS类型定义,VSCODE经常出现许多飘红文件,导致TS错误反馈链无法正常使用,维护难度急剧上涨。注定尿不到一个壶里了,失望+1


离职前对自己的认知


优势:



  • Vue3、Vue2、React函数式组件开发/class组件开发皆可

  • 近2年ts使用经验

  • 熟悉前端工程化,包括rollup、vite、webpack配置,脚手架开发,组件库搭建、monorepo式的包管理等。

  • 熟悉数据结构、设计模式,并擅长数据结构设计,保证可维护性的同时不停增强可扩展性

  • 算法方面,比不上各种大厂的前端,至少比下有余,leetcode刷过100多题,虽然都是简单中等难度,但至少强于大部分前端了吧

  • 工具方面,熟悉processon绘图工具的使用,包括绘制uml类图,脑图等


劣势:



  • 28岁,接近4年的前端开发经验

  • 大专学历

  • 外包仔


重拾技能:



  • 微信小程序开发,3年前开发过一个原生小程序,长久不用忘了

  • node方面之前还使用过Express、Koa打通mySQL,开发过几十个增删查改的接口

  • 跨段方面,以前的一段工作经历使用过UniApp开发过H5与小程序的跨段应用



重拾这部分技能后,对于寒冬中离职感觉也不是那么可怕,但后面现实还是泼了一盆凉水



离职后我是怎么做的?



  • 9月-10月我都是在整理、复习JS知识点、Vue、React框架方面的硬基础内容,期间开始了在掘金发文,也算是进行了知识点的分享,获得不少的收藏、点赞,非常有满足感

    • JS复习分享

    • Vue复习分享

    • React复习分享

    • 设计模式复习分享



  • 我的目标是中小厂自研,经过查看多家公司招聘要求后,发现中小厂对于跨端开发的执着,于是在此期间,学习了taro,感觉与uniapp一样,也能很快上手,本质也是多了一部分如同小程序般的json配置化。

  • 除此之外,每一家招聘要求上,都会有一条很显眼的要求:熟悉一种后端语言优先。招聘公司并非要求我们要真的去开发服务端,而是希望通过这种方式,降低前后端的沟通成本,并且使得双方达到一种平衡,避免前端后端间的撕逼。因此,我第一个想到了Nest,在学这个东西的时候,顺带还能复习一把以前的node开发知识,相比H5,小程序也更容易出圈。(尤其是碰到那些专注于前端开发的面试官,你能扯一部分服务端的东西,他也得一愣一愣的,因为他也不知道你对不对,你究竟有多对)


学习Nest过程中我收获了哪些东西?


通过学习,我对服务端开发套路更加清楚,前端是开发界面,接收数据,呈现数据;服务端则是负责提取或者写入数据,中间穿插着对数据的处理。当系统学习后我豁然开朗了不少。



  1. 多环境配置方案

    • 使用dotenv的简单数据配置

    • 使用js-yaml的复杂数据方式配置



  2. 数据库

    • 如何提升数据库使用效率?——ORM方案,如:typeORM、sequelize、prisma

    • 数据表间的关系有哪些?如何设计?——1对1、1对多/多对1、多对多;及三大设计范式,er图如何设计

    • 如果需要考虑数据库迁移,如何配置nest连接数据库?



  3. 日志统计

    • 为什么日志统计这么重要?

    • 常见日志方案——winston、pino

    • 日志如何分类?—— 错误日志、调试日志、请求日志

    • 日志记录位置有哪些?分别起什么作用?—— 控制台日志(方便调试)、文件日志(方便回溯与追踪)、数据库日志(敏感操作/数据 记录)



  4. 过滤器有哪些?有什么作用?—— 全局过滤器、控制器过滤器、路由过滤器;它们用来更友好地返回服务端的错误响应。

  5. 拦截器和过滤器区别是什么?拦截器主要用于在请求处理之前和之后对请求进行修改、干预或拦截。它们可以修改请求和响应的数据、转换数据格式、记录日志等,以及处理全局任务

  6. 面向对象式的开发方式,为什么老是看到JAVA里充斥着这么多的“注解”?对设计模式,模块的分类,层级的处理更上一个层次


当自我介绍时说出熟悉nest开发的变化


当有2家自研公司在对我面试时,我抛出了熟悉nest开发后。面试官感觉眼神都不一样了,这是真实的。然后这两家公司面试完后,我总结下来,1个小时有大概半小时都是在谈论服务端开发对前端的助益,更多的是关于fp开发oop开发的区别,有哪些收获?除此池外,我们还会不停探讨关于设计模式、数据库方面的话题,如:表关系、如何解耦之类的。也就是说,有面试官一直在挖掘你的深度、广度。你和面试官侃侃而谈,自然结果不会差!


另外还有两三家公司,谈到Nest或者node方面的东西时,面试官都是一句话带过,自然而然,他们不熟悉这方面的东西,你也可以反向面试出这家公司的深浅。


最后入职的公司


面试了两轮,大概4个小时不到,docker、服务端、前端、设计模式、规范、简单的算法,全都问了一遍。其实还有另外俩家备选的公司,都是到了二面三面,当我拿到这家公司offer后,婉拒了他们的面试,也少了一波问价的机会。


今天报道,朝九晚六,偶尔加班,最多8点。薪资也很满意。


感谢nest、感谢我的卷,值了!祝各位有个清晰的规划,能够快速上岸


作者:见信
来源:juejin.cn/post/7319330542100561932
收起阅读 »

2023总结:我在深圳做前端的第6年

入行前端已经6年了,一直有在掘金看技术文章的习惯。其实很早有想在掘金上写点什么,奈何个人技术水平太菜,实在不敢在各位大佬面前献丑;二来就是太懒,无法静下心来做一件事。但万事开头难,不踏出这一步,永远只能原地踏步了。今天就逼着自己记录一下这一年的经历,希望能有个...
继续阅读 »

入行前端已经6年了,一直有在掘金看技术文章的习惯。其实很早有想在掘金上写点什么,奈何个人技术水平太菜,实在不敢在各位大佬面前献丑;二来就是太懒,无法静下心来做一件事。但万事开头难,不踏出这一步,永远只能原地踏步了。今天就逼着自己记录一下这一年的经历,希望能有个好的开始!


年初找工作3个月


22年底我从干了两年的外包公司离职了。之所以在外包干了这么久,主要原因还是因为菜,另外可能就是我本人非科班入行,对外包也不是很抵触,毕竟福利方面跟第一家入职的自研公司也差不多,而且拿钱干活不丢人。


年初就过来准备找工作了,不过还是玩了大半个月,期间参加了朋友的婚礼,又去顺德玩了两天。就这样到了2月底,开始投简历面试。面试前也看了一些八股文,自己也做了总结笔记,但真正面试过程中,表达沟通能力是很重要的一方面,这方面自己是没什么优势的。面试期间一轮游的居多,有的感觉面的好的到二面了也由于没有后续而不了了之。经过了一个多月的面试,每周大概两三家的样子,人都面麻了,还是一个offer没拿到(期间其实通过了一家自研小公司的面试,但由于学历的问题,最终也黄了)。此时都有打算去其它城市看看,后来冷静想想还是打消了念头。后面又是经过了一个多月的零零星星的面试,终于在5月底拿到了一家外包的offer。


当时的面试题记



有同学可能好奇为啥找工作能这么久,大家应该都了解今年大环境的影响。另外就是学历问题,我是非统招学历,另外加上非科班,双buff加持是地狱级别也不为过。当然个人技术能力不行也占一方面。


当时是从外包裸辞的,以为可以很快找到下家,可现实给了我一记的抱拳。3个月期间没有收入,而且家里今年在装修新房,钱大部分都寄回家了,又不想让父母知道,最后只能在借呗借了2万先用着。


这里不得不说当时社会工作经验的欠缺,一般外包如果不主动离职的话,外包会给你安排面试,同时待业期间每月会给到深圳最底薪资,起码算是有个基本的生活保障。而且就算外包要裁你的话他们得给赔偿,另外自己也可以领到失业金。所以,以后在不能确保自己很快找到下家的情况下,千万不要裸辞啊。


社会给我另一记抱拳是让我真正意识到学历的重要性。在boss直聘上我沟通过的hr有上千家了,大部分了解到我的学历时都直接不回了。但现在后悔已经晚了,我已经无法拿到统招全日制学历了。后面有了解到软考,算是互联网技术人员能拿到的一个有一定含金量的职称,打算今年上半年能拿下。


新一家外包短暂的3个月


5月底入职了一家外包公司,被分派到给深圳的罗湖烟草局做项目。我们十几个项目人员在一间临时办公室里,其中4个前端5个后台外加测试和项目经理。前端项目还是比较简单,负责vue的pc端和uniapp的移动端,期间主要开发了一个电子烟的小模块,另外就是修复系统遗留bug。自己本来想着也是先干着,边工作边看看外面的机会,可没想到不到3个月就被通知说项目要撤了。只能说今年的就业环境真是堪忧!


办公楼下拍的夏天的棉花糖



前同事伸过来的橄榄枝


此时在上家还没有离职,另外也攒了大概一周的调休,也算有一点缓冲时间。后面就是准备新一轮面试考验了,投了很多家,收到的面试邀请也是寥寥,第一周面了3家的样子,其中一家面试通过但给到的薪资比期望的低很多,考虑之后还是拒绝了。


就在以为又是一场漫长的求职路时,第二周在家刷面试题时意外收到前同事的微信消息。说是看到我的简历了,想确认一下是不是我。同事是自从第一家离职后就比较少联系,平时也就偶尔朋友圈点赞之交。而且上半年由于失业的焦虑,我也没有再发朋友圈了。确认之后简单寒暄了一下,同事说他现在待的也是一家算是外包公司,加班比较严重,问我是否有意向。心想对我现在来说是一个难得机会,而且同事说可以走内推,因此一些无法预料的意外也可以避免,不多思考我便答应了。接下来就是去同事公司面试,没想到他就是前端负责人。所以也没问技术问题,就简单的聊了一下境况,然后就是等hr消息。第二天就hr就来电话了,然后就是顺利入职。


这次求职经历给我的感悟就是:平时有时间多跟朋友交流一下,增进下感情,兴许在你困难的时候,朋友可以提供一点帮助。毕竟在这个复杂的社会中,谁都不是孤独的存在,人情也就是在相互帮扶中建立的。


近况


入职新公司已有4个月了,公司实行大小周,每周1,2,4固定加班到8:30,加班强度在我看来还可以接受。给银行做的项目,每个月两次的上线节点,开发上基础设施像流水线Jenkins都是有的,就是上线流程上稍微繁琐了一点。前端技术栈pc端用的微前端,移动端是安卓和IOS内嵌H5,对我来说算是没接触过的技术了。但好在都是用的vue,上手起来也快。


年底乘着天气晴朗,去爬了一直想去的梅沙尖



2024展望


新的一年:


工作上希望能顺顺利利,业余时间持续提升技术能力,多花时间思考和总结。


生活上希望能多去接触自己喜欢的人和事,少一点迷茫,多一点开心!


flag:


1.在掘金上写5篇技术文章


2.看10本书(投资理财,个人成长,名人传记,文学小说都可以涉猎)


3.拿到软考证书(中级软件设计师)


4.谈女朋友(本人94年,快30岁了,妥妥的大龄剩男)


作者:wing98
来源:juejin.cn/post/7319700830076157988
收起阅读 »

35岁京东员工哭诉:我只是年龄大了,不是傻了残疾了,为什么不能拥有与年轻人平等的面试机会?

一位35岁的京东员工哭诉他并非因为年龄大就意味着智商下降、工作能力下降,更不是因为残疾而无法胜任工作,然而在当前经济寒冬的大环境下,为何他无法获得与那些拥有3~5年工作经验的年轻人一样的面试机会呢?这不仅仅是一个人的心声,更是一个普遍存在于社会底层的困境。大龄...
继续阅读 »

一位35岁的京东员工哭诉他并非因为年龄大就意味着智商下降、工作能力下降,更不是因为残疾而无法胜任工作,然而在当前经济寒冬的大环境下,为何他无法获得与那些拥有3~5年工作经验的年轻人一样的面试机会呢?

这不仅仅是一个人的心声,更是一个普遍存在于社会底层的困境。大龄员工在职场中所面临的困境,是一种对人才潜能的浪费,同时也反映出我们对于工作价值的认知是否真的应该被年龄所左右。这一问题不仅仅关乎一个人的个体命运,更触及到整个社会的公平和机会均等。年龄是否真的应该成为评判一个人能否胜任工作的唯一标准?年长者所积累的经验和智慧,不应该成为被忽视的财富。

有网友说:本质是体力 精力不行了,干的活都一样 肯定有限选年轻的。
这位网友说:的行业不行业没啥关系,除了师医公三个行业没啥年龄焦虑,其他还有哪个不焦虑,说白了就是中国人太多了,每年毕业生1000w,排队等着找工作
又一网友说:年龄大不好忽悠,不好pua了

网友小海豚说:还是要价太高,如果1w上下的岗位都没有的话才是真的凉了。

网友猫叔说:本质劳动力过剩,国家出台政策限制加班时长,劳动强度。增加市场劳动力需求
有网友说:因为中国企业的领导都害怕比自己年纪大的员工,不自信
网友小茄子说:主要还是卷吧。
网友小袁说:看行业,要是java就是这样,芯片硬件要好点
有网友说:因为国内老板和员工都喜欢996,35岁之后不管是身体还是家庭都要占用一部分精力。
又一网友说:卡学历都行凭什么不卡年龄呢,要做到一视同仁才公平铁子
网友榴莲说:不太能适应国内企业的工作节奏吧,每天有事没事至少10个小时起步。
网友小榛子说:你如果愿意收入打个折的话,还是能找到的
有网友说:是很无奈呀,根本没面试。行情不是一般的差
又有网友说:30都嫌弃 各种挑三拣四的 不知道在选秀还是干啥
有人认为,问题根本在于体力和精力逐渐减弱,而招聘方更愿意选择年轻人。有网友认为几乎所有行业都存在年龄焦虑,除了师医公三个行业,大部分行业都面临就业竞争激烈的问题,尤其是在中国人口众多的情况下。一些网友提到,劳动力过剩是根本原因。
也有人指出国内老板和员工对996的追求,以及领导对比自己年纪大的员工的担忧,导致了对大龄员工的排斥。有的网友认为,企业更看重的是年轻员工的卷取,而不是经验和智慧。有人指出35岁之后,身体和家庭都会占用一部分精力,不适应国内企业的工作节奏。也有网友认为,降低收入期望可能是找到工作的一种方法。


作者:Python开发
来源:mp.weixin.qq.com/s/tb3HF7Ub-7-IancG72stVA
收起阅读 »

一个优雅解决多个弹窗顺序显示方案

不是因为看到希望才坚持,而是因为坚持了才会有希望!场景  在做直播软件的时候,需要在用户打开App后,先后弹出签到,活动,提示等一系列弹窗。每个弹窗都要在前一个弹窗消失后弹出。于是就面临一个弹窗顺序问题,那时候对设计模式很陌生,不知道怎么更好的解决弹窗顺序问题...
继续阅读 »

不是因为看到希望才坚持,而是因为坚持了才会有希望!

场景

  在做直播软件的时候,需要在用户打开App后,先后弹出签到,活动,提示等一系列弹窗。每个弹窗都要在前一个弹窗消失后弹出。于是就面临一个弹窗顺序问题,那时候对设计模式很陌生,不知道怎么更好的解决弹窗顺序问题,都在下前一个弹窗取消或关闭时去加载后面一个弹窗。这样做虽然也能解决问题,但是实现并不优雅,如果在弹窗中间再添加一个其他类型的弹窗改动代价就变得很大,特别是当你是后来接手代码的新人,稍有不慎,就要背锅。怎么能简单而又优雅的解决这个问题呢?

思路

  开发者必读的23种设计模式,对于日常开发问题的解决提供了很好的思路,可以说几乎所有的优秀架构都离不开设计模式,这也是面试必问问题之一。23种设计模式中有一个责任链模式,为弹窗问题提供了解决方案,这也是我从okhttp源码中学习到的,读过okhttp的同学都知道,okhttp网络请求中的五大拦截器基于链式请求,使用起来简单高效。本篇文章同样也是基于责任链的思路来解决弹窗顺序问题。

代码

  1. 首页我们定义一个接口DialogIntercept,同时提供两个方法 intercept和show。
interface  DialogIntercept {
fun intercept(dialogIntercept: DialogChain)
fun show():Boolean
}

  所有的弹窗都需要实现DialogIntercept中的这两个方法。

  1. 自定义弹窗实现DailogIntercept接口。

● 弹窗


class FirstDialog(val context: Context) :DialogIntercept{

override fun intercept(dialogIntercept: DialogChain) {

}

override fun show():Boolean{
return true
}
}

  这里默认show()方法默认返回true,可根据业务逻辑决定弹窗是否显示。

  1. 提供一个弹窗管理类DialogChain,通过建造者模式创建管理类。根据弹窗添加的顺序弹出。
class DialogChain(private val builder: Builder) {
private var index = 0
fun proceed(){
............
...省略部分代码.....
.............
}
class Builder(){
var chainList:MutableList = mutableListOf()
fun addIntercept(dialogIntercept: DialogIntercept):Builder{
.....省略部分代码.....
return this
}
fun build():DialogChain{
return DialogChain(this)
}
}

}

效果

  为了测试效果,分别定义三个弹窗,FirstDialog,SecondDialog,ThirdDialog。按照显示顺序依次添加到DialogChain弹窗管理类中。

  1. 定义弹窗。

  由于三个弹窗代码基本相同,下面只提供FirstDialog代码。

class FirstDialog(val context: Context) :DialogIntercept{


override fun intercept(dialogIntercept: DialogChain) {
show(dialogIntercept)
}

override fun show():Boolean{
return true
}

private fun show(dialogIntercept: DialogChain){
AlertDialog.Builder(context).setTitle("FirstDialog")
.setPositiveButton("确定"
) { _, _ ->
dialogIntercept.proceed()
}.setNegativeButton("取消"
) { _, _ ->
dialogIntercept.proceed()
}.create().show()
}
}

2 . 分别将三个弹窗按照显示顺序添加到管理器中。

 DialogChain.Builder()
.addIntercept(FirstDialog(this))
.addIntercept(SecondDialog(this))
.addIntercept(ThirdDialog(this))
.build().proceed()
  1. 实现效果如下:

总结

  再优秀的架构,都离不开设计模式和设计原则。很多时候我们觉得架构师遥不可及,其实更多的时候是我们缺少一个想要进步的心。新的一年,新的起点,新的开始。


作者:IT小码哥
来源:juejin.cn/post/7319652739083108402
收起阅读 »

请给系统加个【消息中心】功能,因为真的很简单

我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面: 1)消息提示 2)消息列表 这样 这样 那,这就是我们今天要聊的【消息中心】。 1、设计 老规矩先来搞清楚消息中心的需求,再来代码实现。 我们知道在社交类项目中,有很多评论、点赞等数据...
继续阅读 »

我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面:


1)消息提示


Snipaste_2023-08-27_13-41-36.jpg


2)消息列表


这样


Snipaste_2023-08-27_13-42-25.jpg


这样


Snipaste_2023-08-27_16-41-30.jpg


那,这就是我们今天要聊的【消息中心】。


1、设计


老规矩先来搞清楚消息中心的需求,再来代码实现。


我们知道在社交类项目中,有很多评论、点赞等数据的产生,而如果这些数据的产生不能让用户感知到,那你们想想这会带来什么影响?



用户A:太鸡肋了,发布的内容被人评论点赞了,我居然看不到,下次不用了...


用户B:还好没用这个系统...



所以,看到这些结果我们是不是能够意识到一个健全的社交功能,是不是少不了这种通知用户的机制啊!而这种机制我就把他定义为【消息中心】功能。


再来拆分一下这四个字:消息中心



  1. 消息

  2. 中心


消息:这个可以是由我们自己定义,如:把帖子被用户评论当作一条消息,把评论被用户点赞也可以当作一条消息,甚至系统发布的通知也是一条消息。


中心:这个就是字面意思,将上面所提到的所有消息,归拢到一个地方进行展示。


上面我们也提到消息基本就是这两种:



  • 用户对用户:用户消息

  • 平台对用户:系统消息


针对用户消息,就类似这样,用户 A 给用户 B 的一条评论进行了点赞,那这个点赞动作就会产生一条消息,并且通知到用户 B 的一个存储消息的地方,这里通常就指用户的收件箱。这个收件箱就是专门用来存储用户发给用户的消息,而这个点对点的模式是不是就是推送模式啊!(A 推送消息给 B)


接着针对系统消息,就类似这样,平台管理人员发布了一条通知,告诉大家平台有啥 XXX 活动。那这个活动通知肯定是要让平台的所有用户都知道把,所以这个通知就要存在一个发件箱中。这个发件箱就是专门存储平台的通知,所有用户都来这个发件箱中读取消息就行,而这个一对多的模式是不是就是拉取模式啊!(所有用户都来拉取平台消息)


这样一来,我们根据不同的消息场景就抽出了一个基本的消息推拉模型,模型图如下:



Snipaste_2023-08-27_14-27-25.jpg



Snipaste_2023-08-27_14-59-50.jpg


针对这两种模式,不知道大家有没有看出区别,好像乍一看没啥区别,都是发消息,读消息,对吧!


没错,确实都是一个发,一个读,但是两者的读写频率确实有着巨大的差异。先来看推模型,一个普通用户发表了一条帖子,然后获得了寥寥无几的评论和赞,这好似也没啥特别之处,对吧!那如果这个普通用户发表的帖子成为了热门帖子呢,也即该贴子获得了上万的评论和赞。那,你们想想是不是发消息的频率非常高,而该普通用户肯定是不可能一下子读取这么多消息的,所以是不是一个写多读少的场景。再来看看拉模型,如果你的平台用户人数寥寥无几,那倒没啥特别之处,但如果用户人数几万甚至几十万。那,每个用户都过来拉取系统消息是不是就是一个读频率非常高,而发消息频率非常低(系统消息肯定不会发的很快),所以这是不是一个读多写少的场景。


1.1 推:写多读少


针对这个模式,我们肯定是要将写这个动作交给性能更高的中间件来处理,而不是 MySQL,所以此时我们的 RocketMQ 就出来了。


当系统中产生了评论、点赞类的高频消息,那就无脑的丢给 MQ 吧,让其在消息中间件中呆会,等待消费者慢慢的将消息进行消费并发到各个用户的收件箱中,就类似下面这张图的流程:


Snipaste_2023-08-27_15-45-46.jpg


2.2 拉:读多写少


那对于这个模式,所实话,我觉得不用引入啥就可以实现,因为对于读多的话无非就是一个查,MySQL 肯定是能搞定的,即使你的用户几万、几十万都是 ok 的。


但咱们是不是可以这样想一下,一个系统的官方通知肯定是不多的,或者说几天或者几个星期一次,且一旦发送就不可更改。那是不是可以考虑缓存,让用户读取官方通知的时候走缓存,如果缓存没有再走 MySQL 这样应该是可以提高查询效率,提高响应速度。


具体流程如下图:


Snipaste_2023-08-27_15-57-21.jpg


2.3 表结构设计


基本的业务流程已经分析的差不多了,现在可以把表字段抽一下了,先根据上面分析的,看看我们需要那些表:



  1. 用户收件箱表

  2. 系统发件箱表


看似好像就这两张表,但是应该还有第三张表:



  1. 用户读取系统消息记录表



我们看到页面是不是每次有一条新的消息都会有一个小标点记录新消息数量,而第三张表就是为了这个作用而设计的。


具体原理如下:



  1. 首先运营人员发布的消息都是存储在第二张表中,这肯定是没错的

  2. 那用户每次过来拉取系统消息时,将最近拉取的一条消息写入到第三种表中

  3. 这样等用户下次再来拉取的时候,就可以根据第三张表的读取记录,来确定他有几条系统消息未查看了


可能有人会发出疑问:那用户的收件箱为啥不出一个用户读取记录表呢!


这个很简单,因为收件箱中的数据已经表示这个用户需要都这些个消息了,只是不知道那些是已读的那些是未读的,我们只需要再收件箱表中加一个字段,这个字段的作用就是记录最新一次读取的消息 ID 就行,等下次要读消息时,找到上传读取读取消息的记录ID,往后读新消息即可。



好,现在来看看具体的表字段:


1)用户收件箱表(sb_user_inbox)



  • id

  • 消息数据唯一 id:MQ唯一消息凭证

  • 消息类型:评论消息或者点赞消息

  • 帖子id:业务id

  • 业务数据id:业务id

  • 内容:消息内容

  • 业务数据类型:业务数据类型(商品评论、帖子、帖子一级评论、帖子二级评论)

  • 发起方的用户ID:用户 A 对用户 B 进行点赞,那这就是用户 A 的ID

  • 接收方的用户ID:用户 B 的 ID

  • 用户最新读取位置ID:用户最近一次读取记录的 ID


SQL


CREATE TABLE `sb_user_inbox` (
`id` bigint(20) NOT NULL,
`uuid` varchar(128) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '消息数据唯一id',
`message_type` tinyint(1) NOT NULL COMMENT '消息类型',
`post_id` bigint(20) DEFAULT NULL COMMENT '帖子id',
`item_id` bigint(20) NOT NULL COMMENT '业务数据id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '内容',
`service_message_type` tinyint(1) NOT NULL COMMENT '业务数据类型',
`from_user_id` bigint(20) NOT NULL COMMENT '发起方的用户ID',
`to_user_id` bigint(20) NOT NULL COMMENT '接收方的用户ID',
`read_position_id` bigint(20) DEFAULT '0' COMMENT '用户最新读取位置ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un01` (`uuid`),
UNIQUE KEY `un02` (`item_id`,`service_message_type`,`to_user_id`),
KEY `key` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

可以看到,我加了很多业务相关的字段,这个主要是为了方便查询数据和展示数据。


2)系统发件箱表(sb_sys_outbox)



  • id

  • 内容


SQL


CREATE TABLE `sb_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`content` varchar(2000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

这个表就非常简单了,没啥业务字段冗余。


3)用户读取系统消息记录表(sb_user_read_sys_outbox)



  • id

  • 系统收件箱数据读取id

  • 读取的用户id


SQL


CREATE TABLE `sb_user_read_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`sys_outbox_id` bigint(20) NOT NULL COMMENT '系统收件箱数据读取id',
`user_id` bigint(20) NOT NULL COMMENT '读取的用户id',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

ok,这是消息中心所有分析阶段了,下面就开始实操。


2、实现


先来引入引入一下 RocketMQ 的依赖




org.apache.rocketmq
rocketmq-spring-boot-starter
2.2.1


RocketMQ 的双主双从同步刷新集群搭建教程:blog.csdn.net/qq_40399646…


MQ 配置:


Snipaste_2023-08-27_16-26-09.jpg


2.1 生产者


先来实现生产者如何发送消息。


1)消息体对象:LikeAndCommentMessageDTO


位置:cn.j3code.config.dto.mq


@Data
public class LikeAndCommentMessageDTO {

/**
* 该消息的唯一id
* 业务方可以不设置,如果为空,代码会自动填充
*/

private String uuid;

/**
* 消息类型
*/

private UserCenterMessageTypeEnum messageType;

/**
* 冗余一个帖子id进来
*/

private Long postId;

/**
* 业务数据id
*/

private Long itemId;

/**
* 如果是评论消息,这个内容就是评论的内容
*/

private String content;

/**
* 业务数据类型
*/

private UserCenterServiceMessageTypeEnum serviceMessageType;

/**
* 发起方的用户ID
*/

private Long fromUserId;

/**
* 接收方的用户ID
*/

private Long toUserId;


/*
例子:
用户 A 发表了一个帖子,B 对这个帖子进行了点赞,那这个实体如下:
messageType = UserCenterMessageTypeEnum.LIKE
itemId = 帖子ID(对评论进行点赞,就是评论id,对评论进行回复,就是刚刚评论的id)
serviceMessageType = UserCenterServiceMessageTypeEnum.POST(这个就是说明 itemId 的 ID 是归于那个业务的,方便后续查询业务数据)
fromUserId = 用户B的ID
toUserId = 用户 A 的ID
*/

}

2)发送消息代码


位置:cn.j3code.community.mq.producer


@Slf4j
@Component
@AllArgsConstructor
public class LikeAndCommentMessageProducer {

private final RocketMQTemplate rocketMQTemplate;

/**
* 单个消息发送
*
*
@param dto
*/

public void send(LikeAndCommentMessageDTO dto) {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message message = MessageBuilder
.withPayload(dto)
.build();
rocketMQTemplate.send(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, message);
}

/**
* 批量消息发送
*
*
@param dtos
*/

public void send(List dtos) {
/**
* 将 dtos 集合分割成 1MB 大小的集合
* MQ 批量推送的消息大小最大 1MB 左右
*/

ListSizeSplitUtil.split(1 * 1024 * 1024L, dtos).forEach(items -> {
List> messageList = new ArrayList<>(items.size());
items.forEach(dto -> {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message message = MessageBuilder
.withPayload(dto)
.build();
messageList.add(message);
});
rocketMQTemplate.syncSend(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, messageList);
});
}

private void checkMessageDTO(LikeAndCommentMessageDTO dto) {
AssertUtil.isTrue(Objects.isNull(dto.getMessageType()), "消息类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getItemId()), "业务数据ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getServiceMessageType()), "业务数据类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getFromUserId()), "发起方用户ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getToUserId()), "接收方用户ID不为空!");
}


/**
* 发送点赞消息
*
*
@param messageType 消息类型
*
@param serviceMessageType 业务类型
*
@param itemToUserIdMap 业务ID对应的用户id
*
@param saveLikeList 点赞数据
*/

public void sendLikeMQMessage(
UserCenterMessageTypeEnum messageType,
UserCenterServiceMessageTypeEnum serviceMessageType,
Map itemToUserIdMap, List saveLikeList)
{
if (CollectionUtils.isEmpty(saveLikeList)) {
return;
}
List dtos = new ArrayList<>();
for (Like like : saveLikeList) {
LikeAndCommentMessageDTO messageDTO = new LikeAndCommentMessageDTO();
messageDTO.setItemId(like.getItemId());
messageDTO.setMessageType(messageType);
messageDTO.setServiceMessageType(serviceMessageType);
messageDTO.setFromUserId(like.getUserId());
messageDTO.setToUserId(itemToUserIdMap.get(like.getItemId()));
dtos.add(messageDTO);
}
try {
send(dtos);
} catch (Exception e) {
//错误处理
log.error("发送MQ消息失败!", e);
}
}
}

注意:这里我用了 MQ 批量发送消息的一个功能,但是他有一个限制就是每次只能发送 1MB 大小的数据。所以我需要做一个功能工具类将业务方丢过来的批量数据进行分割。


工具类:ListSizeSplitUtil


位置:cn.j3code.config.util


public class ListSizeSplitUtil {

private static Long maxByteSize;

/**
* 根据传进来的 byte 大小限制,将 list 分割成对应大小的 list 集合数据
*
*
@param byteSize 每个 list 数据最大大小
*
@param list 待分割集合
*
@param
*
@return
*/

public static List> split(Long byteSize, List list) {
if (Objects.isNull(list) || list.size() == 0) {
return new ArrayList<>();
}

if (byteSize <= 100) {
throw new RuntimeException("参数 byteSize 值不小于 100 bytes!");
}
ListSizeSplitUtil.maxByteSize = byteSize;


if (isSurpass(List.of(list.get(0)))) {
throw new RuntimeException("List 中,单个对象都大于 byteSize 的值,分割失败");
}

List> result = new ArrayList<>();

List itemList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
itemList.add(list.get(i));

if (isSurpass(itemList)) {
i = i - 1;
itemList.remove(itemList.size() - 1);
result.add(new ArrayList<>(itemList));
itemList = new ArrayList<>();
}
}
result.add(new ArrayList<>(itemList));
return result;
}


private static Boolean isSurpass(List obj) {
// 字节(byte)
long objSize = RamUsageEstimator.sizeOfAll(obj.toArray());
return objSize >= ListSizeSplitUtil.maxByteSize;
}
}

至此呢,生产者的逻辑就算是完成了,每次有消息的时候就调用这个方法即可。


2.2 消费者


位置:cn.j3code.user.mq.consumer


@Slf4j
@Component
@AllArgsConstructor
@RocketMQMessageListener(topic = RocketMQConstants.USER_MESSAGE_CENTER_TOPIC,
consumerGr0up = RocketMQConstants.GR0UP,
messageModel = MessageModel.CLUSTERING,
consumeMode = ConsumeMode.CONCURRENTLY
)

public class LikeAndCommentMessageConsumer implements RocketMQListener {

private final UserInboxService userInboxService;

@Override
public void onMessage(LikeAndCommentMessageDTO message) {
userInboxService.saveMessage(message);
}
}

saveMessage 方法的逻辑就是将消息保存到 MySQL 中,至此消息的产生和存储就算完成了,下面来看看用户如何查看吧!


2.3 用户消息查看


对于用户查看普通的消息就是访问一下 MySQL,并且更新一下最新读取的字段值即可,我贴一下关键代码就行了,代码如下:


public IPage page(UserMessagePageRequest request) {
// 获取消息
IPage page = getBaseMapper().page(new Page(request.getCurrent(), request.getSize()), request);

if (CollectionUtils.isEmpty(page.getRecords())) {
return page;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
if (Objects.isNull(page.getRecords().get(0).getReadPositionId()) ||
page.getRecords().get(0).getReadPositionId() == 0) {
UserInbox userInbox = new UserInbox();
userInbox.setId(page.getRecords().get(0).getId());
userInbox.setReadPositionId(userInbox.getId());
updateById(userInbox);
}
}
return page;
}

2.4 系统消息查看


对于系统消息的查看也是,只贴出关键代码,查询和更新读取记录逻辑,代码如下:


@Override
public IPage lookSysPage(SysOutboxPageRequest request) {
Page page = lambdaQuery()
.orderByDesc(SysOutbox::getId)
.page(new Page<>(request.getCurrent(), request.getSize()));
IPage outboxVOIPage = page.convert(userInboxConverter::converter);
if (CollectionUtils.isEmpty(outboxVOIPage.getRecords())) {
return outboxVOIPage;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
userReadSysOutboxService.updateReadLog(page.getRecords().get(0).getId(), SecurityUtil.getUserId());
}
return outboxVOIPage;
}

这里,可能有人会发现,没有按照上面分析的那用从缓存中读,是的。这里的实现我没有用到 Redis,这里我偷了一下懒,如果有拿到我代码的同学可以试着优化一下这个逻辑。


作者:J3code
来源:juejin.cn/post/7274922643453853735
收起阅读 »

程序员真的需要双显示器吗?

我最近思考一个问题,用双显示器编程是否比单显效率高? 如果是的话那么是否显示器越多效率越高呢? 有些同学肯定会马上回答,那肯定觉得是对的! 一个屏幕用来看文档,一个屏幕用来写代码,肯定会比都挤在一个屏幕效率高呀, 如果桌子够大的话,最好再加一个屏幕专门用来聊...
继续阅读 »

我最近思考一个问题,用双显示器编程是否比单显效率高?



如果是的话那么是否显示器越多效率越高呢?


有些同学肯定会马上回答,那肯定觉得是对的!


一个屏幕用来看文档,一个屏幕用来写代码,肯定会比都挤在一个屏幕效率高呀, 如果桌子够大的话,最好再加一个屏幕专门用来聊微信,那我就不用切换窗口,那效率肯定就爆表了。


但你有没有总结一下,在你编程的时候,具体为什么单显示器会比双显示器比效率低呢?


低效的原因


以下是我总结出的2点原因:



  • 第一点就是切换窗口,因为即使在最新操作系统中,使用单显示器的情况下,无论是用鼠标,command tab ,还是四指上扫调度中心,切换窗口仍然是非常麻烦的一件事 。
    但是你如果用多显示器,就可以避免在两个窗口之间切换。两个窗口放在两个显示器里,只要晃头就行了

  • 第二点是,你会有同时显示两个窗口的需求,比如一个文档,一个ide,你需要一边看文档,一边敲代码,这用一个显示器就不太容易做到,而且有时需要调整各个窗口大小,这是非常浪费时间的。


以上两个问题就是导致你效率低下的最重要的问题,如果可以成功解决,那么使用单屏编程就不是个问题了


解决的要点:



  • 不要用调度中心,不要用command + tab 来切换窗口, 把你每个常用的软件都做成超级快捷键,肌肉记忆直接拉起

  • 学会使用窗口管理工具,配合超级快捷键,快速调整窗口,不要尝试用鼠标拉窗口,手不要离开键盘


如何定义超级快捷键



超级快捷键 = Hyper + 任意键



为了设置超级快捷键,第一步就需要定义一个 Hyper 键,也就是超级键


为了让 Hyper 键不与系统或其他软件冲突,通常会把 Shift + Control + Option + command 一起按,也就是⌃ ⌥ ⌘ ⇧ 这4个键一起按作为 Hyper 键


image.png


但是键盘上并没有一个实体的 Hyper键,我们又不可能手动按住4个键, 所以我们会用软件把大小写切换键 Capslock 改成 Hyper key


因为大小写切换键,就在小指头的旁边,是键盘上占着最好位置的按键,却是一点用都没有的一个键,改完以后当再按住 Capslock键 的时候,实际上相当于按住了4个按键,也就是按住了 Hyper键


image.png


我用到的改键软件是 Karabiner-Element


打开 Karabiner 找到 Complex Modifications 点击 Add predefined rule


image.png


点击 import more rules from the internet 这时会打开浏览器,搜索 CapsLock plus 点击 import


image.png


这里我只 enable 了两条规则



  • CapsLock to Hyper/Escape

  • Hyper Cursor navigation


当然你可以根据需要 edit -》 save 来修改


修改后的配置文件会保存在下边这个位置


~/.config/karabiner/karabiner.json


这是我最终修改后的配置文件,放在下边这里,你可以去参考


github.com/nshen/dotfi…


至此你的超级键就定义好了。


用超级快捷键切换桌面


用超级快捷键来切换桌面需要到设置中把对应的快捷键定义成 Hyper键 + 对应的键,我这里设置成了 Hyper + [Hyper + ]


image.png


具体位置在



设置 -> 键盘 -> 键盘快捷键 - 调度中心 - 调度中心 - 向左/向右移动一个空间



用超级快捷键启动应用


首先安装 Raycast,因为Raycast是目前最优秀的应用启动器,并且可以给每个应用定义快捷键,所以我们用 Raycast 来给每个常用的应用都加上超级快捷键。


打开后 command + k


image.png


打开菜单选择配置


image.png


在这里给应用设置快捷键为 Hyper + 任意键


image.png


以下是我常用的软件设置


image.png


用超级快捷键管理窗口


Raycast 本身就提供了窗口管理功能,非常好用


image.png


我们可以用和应用一样的方法,给这些功能加上快捷键


image.png


我常用的快捷键有


image.png


单屏的优势


当你熟练的使用上述技巧,你就基本上可以使用单屏达到多屏开发同样的效率。


单屏也有单屏的优势,使用单屏,你可以获得以下多屏没有的好处



  • 不用摇头晃脑了,你会更容易专注于一件事,不会被其他屏幕上的应用影响,其他应用只有在需要的时候才会被调出来

  • 不用在桌面上放一堆显示器,节省了桌面空间,桌面会更整洁

  • 便携,拿起笔记本,不用依赖外部显示器,就可以随时随地最高效编程

  • 最重要的是省钱,再也不用考虑买或升级显示器了,一个笔记本搞定!


感谢观看,喜欢类似文章,还请点个关注,谢谢


作者:nshen
来源:juejin.cn/post/7319541571279798310
收起阅读 »

三十而立,我走过的路

你好,我是逗逗青,目前在某领先海外互联网公司担任技术leader,主要从事平台架构相关的研发和管理工作。 2023年即将进入尾声,我的职场生涯也走过了十年,在这篇文章里,我想聊聊我的过去,聊聊这些年我走过的一些弯路,以及得到的一些收获,希望对你能有一些启发。 ...
继续阅读 »

你好,我是逗逗青,目前在某领先海外互联网公司担任技术leader,主要从事平台架构相关的研发和管理工作。


2023年即将进入尾声,我的职场生涯也走过了十年,在这篇文章里,我想聊聊我的过去,聊聊这些年我走过的一些弯路,以及得到的一些收获,希望对你能有一些启发。


逗逗青的求学记


我来自广东一个沿海的农村,是家中长子,家族里学识最高的是我爸。


嗯,他初中学历。


我爸曾和我聊起他辍学的原因:害怕罚站。


他说:当年上学时,路比较远,家里只有一辆破烂的自行车,每次上学路上,这车常坏,经常变成走路推车上学。所以经常迟到被罚站,碍于面子就辍学花钱去拜师学做油漆工了。


我不知道我爸的选择是否正确,但我很庆幸,还好我爸习得了一门手艺,否则,我可能无法顺利上大学。


普通的小学


思绪回到1998年,那会我刚上小学,九年义务教育政策一直吊在我的尾巴:我一毕业,学弟学妹们就开始免收学费。


没赶上好的时代,我的求学生涯只能靠一担担的番薯喂养出来,至今我还记得很清楚,我一学期的学费大约等于一拖拉机的番薯。


这其中,还不包括将番薯从田地里一担一担来地来回走几百米挑到拖拉机上的人工费。


遇到收成不好的时节,我爸就得挨家挨户去给我借钱凑学费了。


因我爸老实本分,除了务农,也有油漆工这门手艺在手,在困难时,常有人能伸伸援手。


虽然家人很辛苦的供我上学,但在整个小学时期,我的成绩一直很普通,小学6年基本与各种奖状无缘。


一开始,我很羡慕那些在学期末可以拿着各种奖状,然后带着欢笑回家的同学。


到后来,我发现家里一到下雨天就会漏水,奖状拿回家贴墙上估计也很快受潮,所以也就释怀了。


逆袭的初中


2004年,进到初中时期,我的学习成绩发生了转变。


在那会,我们学校将班级划分为十几个普通班和四个重点班。


资质一般的我考进了普通班,但幸运的是,我们班级的隔壁就是重点班,更幸运的是,里边还有个我同村的亲戚。


因初中学校离家有些距离,我们一般会骑自行车上学,所以平时我们走得比较近,偶尔会聊聊学习方面的内容。


通过与这位亲戚的交流,我才开始意识到学习也是需要掌握一些技巧的,比如需要多刷题实战,特别是高频题。


慢慢地,我改掉了自己的一些学习陋习,比如埋头苦读背诵书本知识。


我的学习成绩也开始逐步提升。印象中,我当时最好的科目是数学,而且还摸清了数学的出题规律,每次考试,除了最后一道大题,基本都可以稳拿分数。


后来我的成绩排到了班里的第1名,并且升初二时,在班主任的帮助下调进了重点班。


到了重点班,我才明白重点班和普通班的本质区别。


这里,充满了竞争!


有的同学规划清晰,知道自己要去往何处,有的同学纯粹是年少轻狂,喜欢比拼成绩。


而我,则是盯上了那个为数不多的名额。


进了重点班后我才知道,学校和一所不错的高中学校有合作,优等生升学进这所高中可以免三年学费,但名额只有20个。


我不知道别人是否和我一样,也对那个名额感兴趣,但我能感受到,每次考试大家都会铆足了劲。


在那种环境之下,我的成绩也被逼着继续往前走,在后来的两年里,我的每次模拟考成绩基本都排在全校前60,但一直与那个名额无缘。


临近中考时,那所高校来人了,和20个优等生签订了协议,看着他们一个个拿着协议书从学校会议室走出来,那一刻,我竟然又有了“羡慕别人拿奖状”的感觉。


不过后来发生了一件趣事,让我从这次竞争失利中走了出来。


我爸平时忙着务工,早出晚归,基本没关心过我的学习成绩。


后来他听朋友聊起中考的事情,并听闻我们村附近某所初中学校在中考时,录取分数线可以比其他学校低(印象中和扶贫政策有关),比较容易升高中。


所以后面我爸就跟着朋友忙起了帮孩子转校的事情,最后在没和我商量的情况下,跑到我学校的校长面前,沟通起给我转校的事情。


我爸和我说这事时,我被逗笑了,他说校长被我爸气坏了,校长的原话是这样的:“我辛辛苦苦栽培3年的花朵,你现在就要摘走?”。


后来,校长在了解清楚情况并且知道我家的条件后,说服了我爸不给我转校。另外校长答应我爸,如果我的中考成绩能继续保持在学校前60,可以破格帮我申请免三年学费进到前面提到的那所高中。


中考很快结束,我的成绩很稳,还是和平时一样,保持在全校前60,但中考结束后的那段时间,出现了让我非常纠结的事情。


校长很守信,中考成绩出来后,他帮我申请到了入那所高中的名额,但因为是破格,只能免第1年学费,而第二和第三年学费需要由学校考核决定是否继续减免。


让我纠结的有3点,一是这个“破格录取”,在当时我感觉像是走后门,心理很不舒服。二是我的中考的分数可以进到县里一所不错的重点高中,而且听闻比这所破格进的学校要好。三是家里的经济条件。


最后,在我爸的支持之下,我选择了去自己考上的重点高中学校。


叛逆的高中


2007年,我顺利考进了县城里的重点高中,而且还是重点班,选学校那件事带来的郁闷感很快一扫而尽。


高中开始,我们从偏远农村来的就需要寄宿在学校了,原本被家人寄予厚望的我,却在高一时期发生了突变,是的,突变。


高一时,我开始接触到了网吧,从小最多只看过别人在电视机上玩小霸王游戏,而且在当时还没有QQ号的我,沦陷了。


高中那会,大部分人都有QQ,而我,只在初中毕业填写留言纪念簿时才知道有这东西,没办法,为了社交只好跟着同学去网吧学习怎么玩QQ。


一脚踏入网吧的我,立马被网络世界的各种缤纷色彩给吸住了。


到什么程度呢?


经常找同网吧坐我旁边机位的同学聊QQ,自学五笔还带出了几个徒弟,自习时还得意的带着他们唱"工戈草头右框七",还经常帮女同学整QQ空间,至于男同胞,则天天帮他们刷各种Q钻会员。


一直到后面开始接触网络游戏,就真的完全陷进去了,疯狂到原本为数不多的生活费都可以省出一大半用来上网。


高一的疯狂放纵,带来的结局是,在升高二时我被班主任调到了次重点班。


这场景是如此地熟悉,但体验感却是天差地别。


这对我打击很大,网瘾不降反增,上课睡觉,晚上通宵成了我的常态。


有一次周末我通完宵,白天在宿舍睡觉时被人叫醒,当时映入我眼帘的是我爸失望的表情,以及在一旁小声啜泣的母亲。


我荒废学业的事,被班主任通知到家中了。


这件事后,我才开始变得有所收敛,不过学业落下太多,后面基本无心学习。


就这样混混噩噩度过了三年高中,一直到教室黑板上的高考倒计时剩余30天时我才幡然醒悟。


不过为时已晚,最终,高考成绩公布后,我只考了498分,只到大专院校的录取分数线。


高考结果出来后,我开玩笑地对我爸说:"爸,您看我只花30天的时间,成绩就追上了专科的分数线,您让我复读吧,二本应该轻轻松松,一本还可以挑战挑战"。


我爸只回了句"滚",就没后文了。


浪子回头的我,在知道复读无望后,决定在大专的世界打下一片天下。


无知的大学


填报志愿时,我选择了软件技术专业。


也许是我爸感受到了我痛改前非的觉悟,也许是害怕我继续堕落。在知道我选择了计算机相关专业后,就忙着打听到这个专业的内容。


后来他了解到这个专业需要用到电脑,在一天中午,顶着大太阳,他把我拉上摩托车,先是带我去了他做碾米工作的朋友家借了1000块,然后再带着我去了他一个在做图文印刷店的朋友家,花了重金600块买下了对方的二手笔记本。


后来我很庆幸这台笔记本很垃圾,在别人玩游戏的时候我只能选择敲代码。


高考结束后的假期我没有和往常一样选择去打暑假工,而是选择呆在了家里。


通过上网了解到所报专业的相关课程内容后,还未开学,我就用笔记本下载了马士兵和韩顺平老师的教程视频,自学完了Java基础课程,还写了个坦克游戏天天在我弟面前显摆。


2010年,我开启了三年既装逼又无知的大专生涯。


由于开学前就把专业基础打好,在大一,当别人还在学基础课程时,我已经开始学做项目,偶尔也能和学校的师兄合作帮学校做做项目。


在平时,还会和去了重本大学的高中同学一起比赛刷杭电ACM,后来,还因此拿了个蓝桥杯省赛一等奖,不过正当我摩拳擦掌准备去北京参加全国决赛时,却被学校告知学校经费不足去不了。好吧,穷孩子的环境就是这么恶劣。


因为能力得到认可,和老师相处也融洽,一些专业课的老师们私下给我开了特权,允许我不用去教室上课,想忙啥就忙去。


所以我很听老师们的话,大二开始我开始忙着泡妞去了,并且把班里暗恋的女同学拐到手,嗯,她现在成为了我的妻子。


大三时期,某家培训机构来到了我们学校做宣传,我听完那些各式各样学员进名厂的案例,我心动了。以先学后付的方式进了培训机构。


大三时期的我一下进到了初中时期拼入选名额的状态,朝九晚十,日复一日的在广州某培训校区学习,一直幻想着凭自己的实力加上培训机构的联合企业资源,一举进到名厂。


但,现实却给了我一个大嘴巴子。


逗逗青的职场路


面试碰壁


2013年,大专即将毕业,培训机构的课程也已学完,我开始尝试自己出去找工作,原本以为就业会很顺利,但找工作却是磕磕碰碰。


小公司倒还好,基本有叫面试的都能拿到offer,但一到中大厂,就很难走到终面,而且大部分情况是简历直接被拒。


直到很多年后我才明白其根本原因:中大厂对于应届生的要求,是要挑选好苗子,学历对企业而言是一项减少选苗子出现差错的必要筛选项,而专业技能方面更看重的是通用基础技能,如操作系统、算法、常用框架原理等。


而在当时的我,是个偏实战的低学历选手,知识面学得很广但是不深,而且还有大专学历这个减分项,求职困难可想而知。


求职碰壁后,为应付学校的毕业实习要求,以及缓解经济上的压力,我选择了在一家做传统软件开发的中小企业过渡了一年时间。


因为不甘心,那段时间我一直在思考问题根因,后面大概摸到了一些门道,在还清学校助学贷款和培训机构的费用后,我决定跳槽。


跳槽再战


在当时,我隐约意识到求职的关键还是要对口,需要知道用人单位的需求。而我之前却有点炫技,简历上乱七八糟会的技术和项目全写上了。


决定跳槽后,我对市场上的招聘需求进行了归类,结合自己的兴趣和能力,我选择了主攻游戏行业(网瘾少年的后遗症~),并自学填补了游戏行业所需的关键技术,如网络、并发等。


终于在后面的面试中越挫越勇,顺利找到了一份上市公司的游戏服务端研发工作。


现在回过头想想,有点庆幸,还好那个时期的市场机会比较多,有很多机会去尝试,虽然求职路走了挺多弯路,但最终结果还是比较好的。


后来,在游戏研发岗位我做了2年多时间,主要做页游和手游。


在游戏行业里,企业内部一般是按工作室划分不同团队,比如“天美工作室”。


在游戏这个行业,如果想要赚得多,你得进到好团队。什么是好团队?能赚钱的团队就是好团队。


2年多时间的游戏工作经历,让我深刻理解了什么叫“选择比努力更重要”。


我庆幸自己进到了一个不错的游戏团队,虽然工作强度比较大,但由于项目运营收益高,经常可以参与奖金分红。


但同企业内的有些团队就另当别论了,强度比我们团队要来得更猛,但一年到头基本没什么奖金,而且还需要时常担忧工作室可能会解散的问题。


在游戏行业,除《王者荣耀》、《原神》等这种现象级产品,普遍项目周期比较短,一般两年左右就会进入衰退期,当项目不再产生正向收益且公司不再投入资金研发新项目时,团队就会面临解散的危险。


我所在团队的项目就属于后劲不足的产品。两年左右,团队业绩就开始出现下滑,最终团队还是解散了。


在游戏公司,团队解散时,一般会有其他团队过来挖人。当时我被其他团队挖了过去,而团队的其他大部分成员基本都办理了离职,包括我的leader,也就是团队里的主程。


跳槽转型


后来,我的leader在离职不到半年后联系上了我,并把我挖到他所在的一家互联网公司,也是从那一刻起,我开始转型做平台架构相关的研发工作。


当时这家公司比较吸引我的是:团队强、营收高。我当时是9月份过去,工作了4个月就到了发年终奖时间,当时我拿到了4个多月奖金。


这家公司钱给得大方,但工作强度贼猛,公司里加班文化比较重,工作节奏也非常快。


当时我所在的团队是公共部门,也就是所谓的中台。负责支持公司所有业务产品的基础能力,如订单、支付、履约等。


项目研发一周一个迭代,因为是公共部门,除了节奏要快,上线还要求要稳。一开始我很难适应,主要原因还是我的能力跟不上。


在那段时间,也是我买书最疯狂的时期。基本和工作相关的技术书籍我都买来啃了,加上经常能和其他大厂里的朋友交流技术,能力才开始慢慢追上。


成家,寻求变化


2017年,经过4年的积累,我和女友有了一些积蓄,爱情长跑也6年了,所以我们选择在广州安了个小家,把爱情的果结了。


2018年,我们的第1个宝宝出生,也是这一年,我选择了离职。


这5年里,我的工作强度一直比较大,经常处于on-call的状态,另外在业余时间我也比较卷,熬夜是我的常态。


宝宝的出生,加上近两次的体验报告出现了比较多预警信息,让我开始思考工作与生活的平衡。


所以我跳槽去了一家工作强度相对较好的企业,而且也选择了继续从事自己擅长的平台架构工作。


入职新公司后,由于懂得了一点分析上级期望值的技巧,在日常工作中常能做出符合领导心意的成绩,后来上级领导选择辞职创业后,我也顺势当上了团队leader,顺带拿到了一些期权。


所以现在,我平时除了写写代码,还需要规划团队的发展方向以及带带新人,平时也会作为面试官,参与公司的招聘,在这期间积累到了一些新的经验和感触,未来我会对这些经验做些分享,这里不作展开。


目前这家企业,我呆了有一段时间,近期感觉在发展上遇到了一些瓶颈,所以接下的路,要如何走,我还在持续思考与探索。


写在最后


近期,我计划在公众号上开始写作,所以也才有了这一篇文章。


工作了十年,我想找个地方将积累的内容沉淀下来,一是利他,二是自身也能受到一些益处,比如将来出本书,或者转行做教育工作者,都是不错的选择。


所以在接下来的三年,我计划通过写作的方式,持续分享一些高质量的技术干货,来链接一些技术同行,特别是一些职场新人。


我期望通过我的分享,能够帮到一些人少走些弯路,在职场的发展上能得到一些提速。


最后,关于“逗逗青”这个网名,是源于我的女儿。她近期在看一部名为《土豆逗严肃科普》的动画片,后来经常会给我讲土豆逗的故事,不听还不行~


所以我的网名就诞生了,虽有些稚气,但未来当我感到疲惫时,也许这个网名可以给我带来一些正能量。


作者:逗逗青
来源:juejin.cn/post/7317535572432584738
收起阅读 »

Android 0,1,2 个启动图标

最近改了个隐式app 拉起,启动图标也有不同的玩法 0 个启动图标 <intent-filter> <action android:name="android.intent.action.MAIN" /> <category an...
继续阅读 »

最近改了个隐式app 拉起,启动图标也有不同的玩法


0 个启动图标


<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="app"
android:path="/"
android:port="8080"
android:scheme="lb">
</data>
</intent-filter>

这里是对接受所有隐式拉起,这个是告诉系统app 启动不需要用户手动拉起,是为了被代码或者其他中转站调用,所以不需要用户手动拉起,自然就不用再显示图标


1个启动图标


<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

LAUNCHER 是决定是不是要显示在程序列表里,默认为主动唤起,也是Android 标准启动模式,会正常在手机的界面显示


2 个启动图标


<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<activity android:name="com.camera.demo.1Activity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="com.camera.demo.2Activity"
android:icon="@mipmap/ic_launcher" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

上面说了android.intent.category.LAUNCHER 为是否显示在应用列表内,所以我们配置多个LAUNCHER,就会有多个图标显示在手机列表内


intent-filter 相关说明


接受页面隐式跳转的过滤器,


action


必须的包含,定义一些操作.android.intent.action.MAIN/android.intent.action.WEB_SEARCH


image.png


category


一个字符串, 包含了处理该Intent的组件的种类信息, 起着对action的补充说明作用


image.png


data


要指定接受的 Intent 数据,Intent 过滤器既可以不声明任何 <data> 元素,也可以声明多个此类元素,如下例所示:


<intent-filter>
    <data android:mimeType="video/mpeg" android:scheme="http" ... />
    <data android:mimeType="audio/mpeg" android:scheme="http" ... />
    ...
</intent-filter>

每个 <data> 元素均可指定 URI 结构和数据类型(MIME 媒体类型)。URI 的每个部分都是一个单独的属性:schemehostport 和 path


<scheme>://<host>:<port>/<path>


下例所示为这些属性的可能值:


content://com.example.project:200/folder/subfolder/etc


在此 URI 中,架构是 content,主机是 com.example.project,端口是 200,路径是 folder/subfolder/etc
在 <data> 元素中,上述每个属性均为可选,但存在线性依赖关系:



  • 如果未指定架构,则会忽略主机。

  • 如果未指定主机,则会忽略端口。

  • 如果未指定架构和主机,则会忽略路径。


将 Intent 中的 URI 与过滤器中的 URI 规范进行比较时,它仅与过滤器中包含的部分 URI 进行比较。例如:



  • 如果过滤器仅指定架构,则具有该架构的所有 URI 均与该过滤器匹配。

  • 如果过滤器指定架构和权限,但未指定路径,则具有相同架构和权限的所有 URI 都会通过过滤器,无论其路径如何均是如此。

  • 如果过滤器指定架构、权限和路径,则仅具有相同架构、权限和路径的 URI 才会通过过滤器。


最后贴张LAUNCHER 的原理图


image.png


作者:libokaifa
来源:juejin.cn/post/7307471540715126795
收起阅读 »

18张图,详解SpringBoot解析yml全流程

前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的: switch: turnOn: on 程序中的代码也...
继续阅读 »



前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的:


switch:
turnOn: on

程序中的代码也很简单,大致的逻辑就是下面这样,如果取到的开关字段是on的话,那么就执行if判断中的代码,否则就不执行:


@Value("${switch.turnOn}")
private String on;

@GetMapping("testn")
public void test(){
if ("on".equals(on)){
//TODO
}
}

但是当代码实际跑起来,有意思的地方来了,我们发现判断中的代码一直不会被执行,直到debug一下,才发现这里的取到的值居然不是on而是true



看到这,是不是感觉有点意思,首先盲猜是在解析yml的过程中把on作为一个特殊的值进行了处理,于是我干脆再多测试了几个例子,把yml中的属性扩展到下面这些:


switch:
turnOn: on
turnOff: off
turnOn2: 'on'
turnOff2: 'off'

再执行一下代码,看一下映射后的值:



可以看到,yml中没有带引号的onoff被转换成了truefalse,带引号的则保持了原来的值不发生改变。


到这里,让我忍不住有点好奇,为什么会发生这种现象呢?于是强忍着困意翻了翻源码,硬磕了一下SpringBoot加载yml配置文件的过程,终于让我看出了点门道,下面我们一点一点细说!


因为配置文件的加载会涉及到一些SpringBoot启动的相关知识,所以如果对SpringBoot启动不是很熟悉的同学,可以先提前先看一下Hydra在古早时期写过一篇Spring Boot零配置启动原理预热一下。下面的介绍中,只会摘出一些对加载和解析配置文件比较重要的步骤进行分析,对其他无关部分进行了省略。


加载监听器


当我们启动一个SpringBoot程序,在执行SpringApplication.run()的时候,首先在初始化SpringApplication的过程中,加载了11个实现了ApplicationListener接口的拦截器。



这11个自动加载的ApplicationListener,是在spring.factories中定义并通过SPI扩展被加载的:



这里列出的10个是在spring-boot中加载的,还有剩余的1个是在spring-boot-autoconfigure中加载的。其中最关键的就是ConfigFileApplicationListener,它和后面要讲到的配置文件的加载相关。


执行run方法


在实例化完成SpringApplication后,会接着往下执行它的run方法。



可以看到,这里通过getRunListeners方法获取的SpringApplicationRunListeners中,EventPublishingRunListener绑定了我们前面加载的11个监听器。但是在执行starting方法时,根据类型进行了过滤,最终实际只执行了4个监听器的onApplicationEvent方法,并没有我们希望看到的ConfigFileApplicationListener,让我们接着往下看。



run方法执行到prepareEnvironment时,会创建一个ApplicationEnvironmentPreparedEvent类型的事件,并广播出去。这时所有的监听器中,有7个会监听到这个事件,之后会分别调用它们的onApplicationEvent方法,其中就有了我们心心念念的ConfigFileApplicationListener,接下来让我们看看它的onApplicationEvent方法中做了什么。



在方法的调用过程中,会加载系统自己的4个后置处理器以及ConfigFileApplicationListener自身,一共5个后置处理器,并执行他们的postProcessEnvironment方法,其他4个对我们不重要可以略过,最终比较关键的步骤是创建Loader实例并调用它的load方法。


加载配置文件


这里的LoaderConfigFileApplicationListener的一个内部类,看一下Loader对象实例化的过程:



在实例化Loader对象的过程中,再次通过SPI扩展的方式加载了两个属性文件加载器,其中的YamlPropertySourceLoader就和后面的yml文件的加载、解析密切关联,而另一个PropertiesPropertySourceLoader则负责properties文件的加载。创建完Loader实例后,接下来会调用它的load方法。



load方法中,会通过嵌套循环方式遍历默认配置文件存放路径,再加上默认的配置文件名称、以及不同配置文件加载器对应解析的后缀名,最终找到我们的yml配置文件。接下来,开始执行loadForFileExtension方法。



loadForFileExtension方法中,首先将classpath:/application.yml加载为Resource文件,接下来准备正式开始,调用了之前创建好的YamlPropertySourceLoader对象的load方法。


封装Node


load方法中,开始准备进行配置文件的解析与数据封装:



load方法中调用了OriginTrackedYmlLoader对象的load方法,从字面意思上我们也可以理解,它的用途是原始追踪yml的加载器。中间一连串的方法调用可以忽略,直接看最后也是最重要的是一步,调用OriginTrackingConstructor对象的getData接口,来解析yml并封装成对象。



在解析yml的过程中实际使用了Composer构建器来生成节点,在它的getNode方法中,通过解析器事件来创建节点。通常来说,它会将yml中的一组数据封装成一个MappingNode节点,它的内部实际上是一个NodeTuple组成的ListNodeTupleMap的结构类似,由一对对应的keyNodevalueNode构成,结构如下:



好了,让我们再回到上面的那张方法调用流程图,它是根据文章开头的yml文件中实际内容内容绘制的,如果内容不同调用流程会发生改变,大家只需要明白这个原理,下面我们具体分析。


首先,创建一个MappingNode节点,并将switch封装成keyNode,然后再创建一个MappingNode,作为外层MappingNodevalueNode,同时存储它下面的4组属性,这也是为什么上面会出现4次循环的原因。如果有点困惑也没关系,看一下下面的这张图,就能一目了然了解它的结构。



在上图中,又引入了一种新的ScalarNode节点,它的用途也比较简单,简单String类型的字符串用它来封装成节点就可以了。到这里,yml中的数据被解析完成并完成了初步的封装,可能眼尖的小伙伴要问了,上面这张图中为什么在ScalarNode中,除了value还有一个tag属性,这个属性是干什么的呢?


在介绍它的作用前,先说一下它是怎么被确定的。这一块的逻辑比较复杂,大家可以翻一下ScannerImplfetchMoreTokens方法的源码,这个方法会根据yml中每一个keyvalue是以什么开头,来决定以什么方式进行解析,其中就包括了{['%?等特殊符号的情况。以解析不带任何特殊字符的字符串为例,简要的流程如下,省略了一些不重要部分:



在这张图的中间步骤中,创建了两个比较重要的对象ScalarTokenScalarEvent,其中都有一个为trueplain属性,可以理解为这个属性是否需要解释,是后面获取Resolver的关键属性之一。


上图中的yamlImplicitResolvers其实是一个提前缓存好的HashMap,已经提前存储好了一些Char类型字符与ResolverTuple的对应关系:



当解析到属性on时,取出首字母o对应的ResolverTuple,其中的tag就是tag:yaml.org.2002:bool。当然了,这里也不是简单的取出就完事了,后续还会对属性进行正则表达式的匹配,看与regexp中的值是否能对的上,检查无误时才会返回这个tag


到这里,我们就解释清楚了ScalarNodetag属性究竟是怎么获取到的了,之后方法调用层层返回,返回到OriginTrackingConstructor父类BaseConstructorgetData方法中。接下来,继续执行constructDocument方法,完成对yml文档的解析。


调用构造器


constructDocument中,有两步比较重要,第一步是推断当前节点应该使用哪种类型的构造器,第二步是使用获得的构造器来重新对Node节点中的value进行赋值,简易流程如下,省去了循环遍历的部分:



推断构造器种类的过程也很简单,在父类BaseConstructor中,缓存了一个HashMap,存放了节点的tag类型到对应构造器的映射关系。在getConstructor方法中,就使用之前节点中存入的tag属性来获得具体要使用的构造器:



tagbool类型时,会找到SafeConstruct中的内部类 ConstructYamlBool作为构造器,并调用它的construct方法实例化一个对象,来作为ScalarNode节点的value的值:



construct方法中,取到的val就是之前的on,至于下面的这个BOOL_VALUES,也是提前初始化好的一个HashMap,里面提前存放了一些对应的映射关系,key是下面列出的这些关键字,value则是Boolean类型的truefalse



到这里,yml中的属性解析流程就基本完成了,我们也明白了为什么yml中的on会被转化为true的原理了。至于最后,Boolean类型的truefalse是如何被转化为的字符串,就是@Value注解去实现的了。


思考


那么,下一个问题来了,既然yml文件解析中会做这样的特殊处理,那么如果换成properties配置文件怎么样呢?


sw.turnOn=on
sw.turnOff=off

执行一下程序,看一下结果:



可以看到,使用properties配置文件能够正常读取结果,看来是在解析的过程中没有做特殊处理,至于解析的过程,有兴趣的小伙伴可以自己去阅读一下源码。


那么,今天就写到这里,我们下期见。


作者:码农参上
来源:juejin.cn/post/7054818269621911559
收起阅读 »

Java 中for循环和foreach循环哪个更快?

本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式 前言 在Java编程中,循环结构是程序员常用的控制流程,而for循环和foreach循环是其中比较常见的两种形式。关于它们哪一个更快的讨论一直存在。...
继续阅读 »

本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式



前言


在Java编程中,循环结构是程序员常用的控制流程,而for循环和foreach循环是其中比较常见的两种形式。关于它们哪一个更快的讨论一直存在。本文旨在探究Java中的for循环和foreach循环的性能差异,并帮助读者更好地选择适合自身需求的循环方式。通过详细比较它们的遍历效率、数据结构适用性和编译器优化等因素,我们将为大家揭示它们的差异和适用场景,以便您能够做出更明智的编程决策。



for循环与foreach循环的比较


小编认为for和foreach 之间唯一的实际区别是,对于可索引对象,我们无权访问索引。


for(int i = 0; i < mylist.length; i++) {
if(i < 5) {
//do something
} else {
//do other stuff
}
}

但是,我们可以使用 foreach 创建一个单独的索引 int 变量。例如:


int index = -1;
for(int myint : mylist) {
index++;
if(index < 5) {
//do something
} else {
//do other stuff
}
}

现在写一个简单的类,其中有 foreachTest() 方法,该方法使用 forEach 迭代列表。


import java.util.List;

public class ForEachTest {
List intList;

public void foreachTest(){
for(Integer i : intList){

}
}
}

编译这个类时,编译器会在内部将这段代码转换为迭代器实现。小编通过执行 javap -verbose IterateListTest 反编译代码。


public void foreachTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: aload_0
1: getfield #19 // Field intList:Ljava/util/List;
4: invokeinterface #21, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
9: astore_2
10: goto 23
13: aload_2
14: invokeinterface #27, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
19: checkcast #33 // class java/lang/Integer
22: astore_1
23: aload_2
24: invokeinterface #35, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
29: ifne 13
32: return
LineNumberTable:
line 9: 0
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 this Lcom/greekykhs/springboot/ForEachTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class com/greekykhs/springboot/ForEachTest, top, class java/util/Iterator ]
stack = []
frame_type = 9 /* same */

从上面的字节码我们可以看到:


a). getfield命令用于获取变量整数。


b).调用List.iterator获取迭代器实例


c).调用iterator.hasNext,如果返回true,则调用iterator.next方法。


下边来做一下性能测试。在 IterateListTest 的主要方法中,创建了一个列表并使用 for 和 forEach 循环对其进行迭代。


import java.util.ArrayList;
import java.util.List;

public class IterateListTest {
public static void main(String[] args) {
List mylist = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
mylist.add(i);
}

long forLoopStartTime = System.currentTimeMillis();
for (int i = 0; i < mylist.size(); i++) {mylist.get(i);}

long forLoopTraversalCost =System.currentTimeMillis()-forLoopStartTime;
System.out.println("for loop traversal cost for ArrayList= "+ forLoopTraversalCost);

long forEachStartTime = System.currentTimeMillis();
for (Integer integer : mylist) {}

long forEachTraversalCost =System.currentTimeMillis()-forEachStartTime;
System.out.println("foreach traversal cost for ArrayList= "+ forEachTraversalCost);
}
}

结果如下:


总结


观察结果显示,for循环的性能优于for-each循环。然后再使用LinkedList比较它们的性能差异。对于 LinkedList 来说,for-each循环展现出更好的性能。ArrayList内部使用连续存储的数组,因此数据的检索时间复杂度为 O(1),通过索引可以直接访问数据。而 LinkedList 使用双向链表结构,当我们使用 for 循环进行遍历时,每次都需要从链表头节点开始,导致时间复杂度达到了 O(n*n),因此在这种情况下,for-each 循环更适合操作 LinkedList。


作者:葡萄城技术团队
来源:juejin.cn/post/7280050832950624314
收起阅读 »

拿开源套壳就是自主研发?事情没那么简单

去年8月国内科技圈出了一件非常丢人的事情,想必大家已经都知道了,某个号称自主研发的IDE完全使用开源的VSCode改名而来。没错,就是改名! 哦,对了,还加上了vip功能! 为什么这家公司可以如此堂而皇之地将VSCode改成自主研发?在该公司最新的道歉声明...
继续阅读 »

去年8月国内科技圈出了一件非常丢人的事情,想必大家已经都知道了,某个号称自主研发的IDE完全使用开源的VSCode改名而来。没错,就是改名!


图片


哦,对了,还加上了vip功能!


图片


为什么这家公司可以如此堂而皇之地将VSCode改成自主研发?在该公司最新的道歉声明中提到了缺失MIT协议:


图片


MIT协议到底是什么,里面有哪些要求,微软可以告他侵权吗?


在文章的最后我要聊一下软件的开源的意义在哪里,到底怎样才能真的叫“自主研发”。


1、MIT协议是什么


以GitHub为例,打开源代码的根目录,一般会有一个名为license的文件,这个license就是这套代码的许可证信息。


图片


可能这个license的文件内容很多,而且是英文的,不过不必担心,我给大家归纳总结一下就明白了。


直接看这张由阮一峰大佬总结的图,可以看到开源许可证主要限制的点就一目了然:


图片


正因为VSCode采用的是最宽松的MIT协议,它的MIT协议几乎没有什么约束。


整个协议非常的简短,不到两百个单词。我们直接看看VSCode的MIT协议全文长啥样:


图片


首先第一句,这个许可是免费的,任何人都可以拿到软件的副本以及附带的文档。


然后还能做啥呢?使用,复制,修改,合并,发布,分发,再许可/或出售该软件的副本。


也就是说你爱咋整都可以,拿来卖钱也可以,但只有一个要求,就是要把这个许可证放到软件的副本中!


所以大家看懂了上面这家公司的道歉声明了吗?他们道歉的点就是没有把MIT许可证放入其中,这也是MIT许可证唯一的要求。


后面据说他们也在GitHub上开源了CEC-IDE:


图片


不过被骂得太惨,最后还是消失了。


既然是套壳VSCode,微软能告他侵权吗?


答案是不太行。因为MIT许可证本身就是一个不起诉的承诺。


2、做CEC-IDE意义何在


为什么他们要做这个CEC-IDE呢?真的指望它vip能赚钱吗?


作为同样是程序员的我,其实对CEC-IDE的做法并不陌生。


例如我们公司也搞了一个开发平台,为了不惹麻烦了,我就不说是啥了,我就简单叫做by吧。


它其实就把springboot gitlab等等一些东西糅合在一起,然后把包名,比如spring替换成by:


图片


其实我觉得嘛,这玩意要是对内使用,作为公司统一开发的规范,除了包名被换了比较恶心外,问题不算大,反而这样还可以统一管理开发组建的版本。


而CEC-IDE最大的问题就是把这种本来应该内部使用的东西公开化,而且大肆炒作“自主研发”。


一般大企业内部都有研发立项资金,每年都有一定的申报额度,各个部门都会绞尽脑汁去做各种工作。


当然并不是做个ppt就完事了,上面人也不傻,现在一般大企业内部都很卷,为了拿到上面批下来的研发经费,无论如何都要造出点与众不同的地方。


毕竟kpi考核内部竞争也很激烈,所以大多数情况都会提前做一个“好看”的版本,配合一定的亮点宣传,“自主研发”显然是最契合的。


而MIT许可证规避了法律风险,确实是个“完美”的方案。


其实他们也是“聪明”的,只挑MIT许可证的,从他们的道歉声明可以看出,他们最初拿VSCode动手也是做了一定的功课的,错就错在太高调了!


3、软件开源的意义


为什么很多人和公司会选择把自己开发的软件开源?


开源不可避免会导致代码被其他人“拿来主义”,那么开源软件的意义在哪里?是因为他们太有钱做慈善吗?


首先要说明白一点,开源不代表与商业化冲突,反而优秀的开源软件能带来更多的商业化机会。


我举一个例子,假如我发明了一个人脸识别算法,这个算法有一个特别优势:可以在性能非常非常差的硬件上运行。但前提有一个条件,就是需要对指定硬件做适配,于是我把优化好的一个版本放在GitHub上,获得了很多人的关注,甚至也有很多人帮我改进代码中的一些bug。


有一天,一家大公司看中了我的代码,这时候会有两种情况:


第一种就是把我的代码“拿走”,用到自己的产品中,不给我一分钱!


第二种是把我“收编”了,或者给我一笔费用,让我为其提供有偿的技术支持,并能持续迭代适配这家公司的更多低端设备。


稍微有点远见的公司老板,肯定会选第二种。毕竟拿一段无人维护的陌生代码是有很大的风险的。有时候代价比自己做一套还要大。


对于企业来说,开源也不是做慈善,反而有战略作用。


例如代码开源,但你要获取的技术支持是付费的,这也是非常常见的盈利模式。


还有一个典型就是比如开放云服务形式,这也是AI领域常见的开源盈利模式。


在我看来,大家遵守游戏规则,尊重他人的劳动成果,软件开源肯定是有利于整个行业发展的。


4、怎么定义自主研发


自主研发严格定义应该是:企业主要依靠自己的资源,技术,人力,依据自己的意志,独立开发,并在研发项目的主要方面拥有完全独立的知识产权。


除此之外,自主研发还包含一层意思,自己做主,行使权利,而不受他人的控制或限制。


什么叫突破西方卡脖子?


去“突破”人家免费送的东西算哪门子自主!


我承认,做自主研发不可能完全从零开始,在别人的源代码基础上做衍生开发是再正常不过的事情。但起码要让人看到做这件事情的价值。


如此浮躁,急功近利,毫无底线,这件事无疑给国产化、信创行业更加蒙上一层阴影。


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

2023我的仲裁之旅, 谨以此献给需要仲裁的朋友

背景经历在我提出离职之前, 公司断断续续欠薪一年多的时间, 欠薪时长4.5个月(时间这里还搞了个小乌龙, 后面说下)。2023年5月底,我向领导提出辞职,离职时间确定为2023.6.6日。虽然公司欠薪, 但当时的我依旧心存幻想, 想着可以跟公司好聚好散, 毕竟...
继续阅读 »

背景经历

在我提出离职之前, 公司断断续续欠薪一年多的时间, 欠薪时长4.5个月(时间这里还搞了个小乌龙, 后面说下)。

2023年5月底,我向领导提出辞职,离职时间确定为2023.6.6日。

虽然公司欠薪, 但当时的我依旧心存幻想, 想着可以跟公司好聚好散, 毕竟在公司待三年多了&同事关系和谐&如果仲裁的话,了解到按我的情况会有n的补偿金( 以为公司不会冒仲裁风险(公司处于融资阶段),毕竟仲裁的话我这边赢下的概率比较大),期间我积极进行离职交接,又自以为是的以为公司会在我最后走的时候跟我约定好一个薪资发放的时间把欠我的工资补全(为啥说要约定好呢? 因为公司欠薪,薪资发放已经不是正常时间了,还有, 各位,记得不要自以为是!)。

时间转眼到了6.6号,期间人事在这之前一直没找我聊,我也没有找人事聊(人事是知道我提离职的, 上面说到我自以为公司这边会跟我约定好薪资发放时间, 这里为啥会有这种意识呢? 因为之前离职的同事工资都拿到了),这最后一天,我去找人事,人事那边拿出相关的材料让我填写离职申请, 我这边挨个找人签字,但是公司老总正好找各个部门领导开会,我这边还卡着最后一位签字,一时半会签不上, 然后我去找人事了。

我问:我这边工资什么时候能给我?

人事回复:我这边也不确定,需要请示下领导。

我问:领导那边回复要多久?

人事回复: 不确定, 领导现在还在开会,等开会完我确认下。

。。。

我回工位了。

等过了一会, 我看开会的人回来了,我又去找人事问。

我问: 能确定了吗?

人事回复:刚刚我去找领导了,领导不在,估计得明天了。(Fuck!)

然后我就去公司董事长办公室,想自己去聊,结果确实不在。

然后我对人事说:那明天你这边给我一个回复,离职材料我填好了,还差XXX签字,现在又去开会了, 找不到他。

下班时间到了,我走了。

因为没有敲定工资发放日期,有种预感公司准备使用拖字诀对付我了,所以我把一些仲裁材料整理了下,这个具体有什么材料后面再说一下。

第二天一早,我给人事和公司老板发消息,如下:

人事下午给我的回复:

而老板呢? 没理我, 么得回复。人事也是我中间又给发了次消息才回复的我。

到这呢, 我就明白了, 公司确实准备拖我了,可能是资金紧张, 也可能是看人下菜,总之,去仲裁吧。

一个简单的时间线

6.6号离职走人。

6.7号与HR和老板交涉,并开始做仲裁准备。

6.8号准备好仲裁材料

6.9号去仲裁

6.26号调解员开始调解

期间给了一些调解方案, 与调解员, HR交涉

7.19号我与公司均不接受互相给的方案, 我这边坚持开庭, 调解员让我联系仲裁委去那边办理开庭手续, 提交相关仲裁资料, 等待开庭。

8.7号调解员联系我, 说公司因为仲裁案频发, 被上级领导要求整改, 然后公司接受我之前一个降级的调解方案, 赔偿金额可以, 但给我的支付时间我没法接受, 我这边坚持当庭支付。

8.8号调解员做出保证: 可以当庭支付, 且如果公司那边没给当庭支付,那就不支付不给我结案, 我依旧可以继续把流程走下去,不必重新排队。

8.10号接受调解并签字, 公司当庭支付,至此我的仲裁到此结束。

仲裁资料准备

这是我的申请诉求和相关证据材料

  1. 拖欠薪资
  • 劳动合同
  • 我的银行流水
  1. 加班费用
  • 五一期间的打卡记录
  • 加班聊天记录
  1. (2021年的)年终奖金(应于2022.5月份左右发, 但一直拖欠)
  • 与人事沟通的会发年终奖的聊天记录
  • 上一年的工资流水
  1. 经济补偿金
  • n
  1. 误工费

这个其实是我想多看能否多拿一点, 因为在劳动仲裁过程中, 我跑了好几趟,都是请假去的, 但是好像不支持, 最后线下提交的时候没填上它

  1. 其他
  • 国家企业信用信息公示系统下载的企业报告(这个可以下载, 但是没用到, 去仲裁委提交材料的时候需要从仲裁委的企业信用打印机里打印, 用不到这个 )
  • 仲裁申请书, 这个是在劳动仲裁的官方小程序劳动争议调解智慧服务平台上填写并下载的, 现在基本都是线上填写了, 但是提交到仲裁委的时候需要下载下来打印

仲裁仲裁!

6.9号去的仲裁委, 上面的仲裁资料是我最终版的, 去仲裁委之前我只是在网上查的资料, 到了之后让扫个小程序码, 线上提交申请

之后会有调解员联系你(这个时间我等了大概半个月, 6.26才加上调解员的微信), 之后调解员开始调解, 调解员给出公司的方案: 只有正常工资, 加班费和赔偿别想了。这个方案我这边当然不同意, 之后就是各种PK, 我, 公司人事, 调解员三方互相PK, 在两个月之后, 8.11号, 我还是调解了, 拿到了差强人意的money,算是庭前调解, 最终还是没走上开庭。期间我算了下, 两个多月的时间, 大概请了3,4天假。

关于对公司拖欠工资时应对的方案

我想了很多方案, 有应用的也有没用上的, 大家可以看一下或者补充一下:

  • 劳动仲裁: 保存好证据,要有打持久战的准备, 我最后实践有效的就是这个渠道。
  • 公积金中心举报公司未按实际工资给所有员工缴纳公积金: 网上说只要公司没按你的实际工资缴纳公积金, 你就可以去投诉, 尽管这可能是伤敌一千,自损八百的渠道, 但当时我依旧去了。我准备好银行流水和劳动合同, 结果去了之后工作人员告诉我要等仲裁结果, 仲裁结果赢了之后才可以受理。当时还是冒雨去的,老远了, 呵呵呵。
  • 劳动大队投诉: 这个渠道呢, 如果只是想要要回工资不需要补偿, 可以先走这个渠道, 记住这个, 因为你打电话投诉公司的时候, 工作人员会问: 是否已经在仲裁委那边提交申请? 如果回答提交了劳动仲裁申请,那这个机构就不会管了。 说辞: 已经被仲裁委受理, 等待仲裁就好了。综合: 这个机构其实在要回工资的时候可能比较给力, 可以先给它打电话投诉公司, 让它给公司施压, 然后再去仲裁。
  • 个人所得税投诉: 这个渠道就是比较伤财务, 如果财务话语权比较大的话, 没准就能追回工资,因为财务不想折腾,但我这个公司, 财务没啥话语权,所以财务按税务局的要求折腾了一两天, 然后把我的个人所得税没发工资的那几个月给更新成功0了,然后会在收到工资后再给新的报税。
  • 掌上12333 国务院客户端小程序 =>更多 => 投诉 => 全国根治欠薪线索反映平台: 本来我以为这个渠道会比较强势, 但非常可惜, 啥用没有, 可能我傻的只写了自己的欠薪, 没写其他同事的, 数额较小, 没引起注意, 等到我仲裁都结束了好像半个月还是一个月还是更久? 才给我来了个电话。

抖音上看到有个能快速结束仲裁, 然后去法院起诉进行一审的方法: 在申请书上写: 申请枪毙无良老板! 然后这个会被仲裁委驳回, 然后请求仲裁委出具不予受理通知, 拿着这个就可以去起诉公司。这个的话大家纯粹当爽文看吧, 应该不太具有实施成功的可能性。

一点点走心之谈

  • 如果公司刚开始拖欠一个月两个月, 别想了, 赶紧去劳动仲裁, 只要申请的诉求中每个单项金额不超过当地月平均工资标准的12个月的金额, 那就可以触发一裁终审, 公司无权再上诉,所以这个只适用于拖欠前期和金额较小时。
  • 如果公司拖欠工资, 而你决定去仲裁了,那就别犹犹豫豫的了, 赶紧给公司发一个被动解除劳动通知书, 走邮政和单位邮箱, 这样可以申请到n的劳动补偿, 而我就是因为没有这一步, 没有听取辣条(热心群友)的建议, 导致这条申请诉求被仲裁委支持的概率比较小, 所以选择了庭前调解。诸君引以为鉴。
  • 坚持理性,不要感性。
  • 说一下最开始说的关于欠薪时长的乌龙, 这个问题其实就是, 我, 搞错了公司拖欠我薪资的时长!怎么说呢? 就是公司发放薪资是在本月的15-20号左右发放上个月的薪资, 也就是说2023.6.6号离职, 我这边的工资应该6.20号发放的5月份工资和7月份发放的6.1-6.6的工资, 而我当时算的是按每月收入算的, 也就是5.20号收到一个月的工资, 6.20号我这边收到的应该是只有6天的薪资, 我不知道当时我为啥这样想...,最后是怎么发现的呢? 是在调解后期人事给的调解方案里发现我少算了一个月的薪资。
  • 关于拖欠薪资的日期。所谓百足之虫死而不僵, 有的公司拖欠薪资不是拖欠后就一直不给了, 而是断断续续的给, 拖欠一个月, 给你半个月, 拖欠一个月, 给你一个月, 继续拖, 继续给点, 我前公司就是这样, 断断续续给点, 前前后后欠一年多的时间了总共欠了4.5个月。现在想想, 我是怎么撑下来的?!

相关资源, 以了解仲裁

  • 我有买一本书, 《劳动争议仲裁诉讼实战宝典》,但这本书我还没看完我就结束仲裁了, 但, 它确实不错, 至少可以让对仲裁不太了解的人有个大概的了解。
  • 现在有很多的AI应用, 大家可以尝试一下,这个我也不太了解, 大家可以补充一下。
  • 抖音或b站看劳动仲裁相关的视频, 我有看的是叫晨辉律师

致谢辣条

非常感谢神奇的程序员@大白群里的不正经网友->辣条, 之前群里聊的时候在我说了公司拖欠工资后他就让我走被动离职这条路, 但当时他给我的印象是不太正经的精神小伙, 所以我没听他的, 就正常离职, 自己写了离职申请, 这是让我后来在仲裁期间非常后悔的事。 仲裁的周期比较长, 我也比较迷茫, 他给了我很多帮助, 期间我很多次都想着放弃吧, 接受吧,能拿回基本工资就不错了, 是辣条帮我坚定了“道心”,在此, 真的非常感谢辣条佬。

2023, 拜拜~

写下这篇文章, 给2023画一个完美的句号。

2023.12.31


作者:掘金沸点顶流
来源:juejin.cn/post/7318446251631493171

收起阅读 »

我的天!多个知名组件库都出现了类似的bug!

web
前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design, 字节系:arco design 腾讯...
继续阅读 »

前言


首先声明,没有标题党哈!


以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


Affix组件是什么,以及bug复现


Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:


image.png


这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:


image.png


如何复现bug


你在这个button元素任意父元素上,加上以下任意style属性



  • will-change: transform;

  • will-change: filter;

  • will-change: perspective;

  • transform 不为none

  • perspective不为none

  • 非safari浏览器,filter属性不为none

  • 非safari浏览器,backdrop-filter属性不为none

  • 等等


都可以让这个固定组件失效,就是原本是距离顶部80px固定。


我的组件库没有这个bug,哈哈


mx-design


目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


bug原因


affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


真正的规则如下(以下说的包含块就是fixed布局的定位父元素):



  1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:



    1. transform 或 perspective 的值不是 none

    2. will-change 的值是 transform 或 perspective

    3. filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。

    4. contain 的值是 paint(例如:contain: paint;

    5. backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);




评论区有很多同学居然觉的这不是bug?


其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


解决方案



  • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值

  • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了


具体代码如下:



  • offsetParent固定元素的定位上下文,也就是相对定位的父元素

  • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定



affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

如何找出offsetParent,也就是定位上下文


export function getContainingBlock(element: Element) {
let currentNode = element.parentElement;
while (currentNode) {
if (isContainingBlock(currentNode)) return currentNode;
currentNode = currentNode.parentElement;
}
return null;
}

工具方法,isContainingBlock如下:


import { isSafari } from './isSafari';

export function isContainingBlock(element: Element): boolean {
const safari = isSafari();
const css = getComputedStyle(element);

// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
(!safari && (css.filter ? css.filter !== 'none' : false)) ||
['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
);
}


本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


作者:孟祥_成都
来源:juejin.cn/post/7265121637497733155
收起阅读 »

到了2038年时间戳溢出了怎么办?

计算机中的时间 看完这篇文章相信你会对计算机中的时间有更系统全面的认识。 我经常自嘲,自己写的程序运行不超过3年,因为大部分项目方就早早跑路了。大多数项目上线后,你跟这个项目就再无瓜葛,关于时间你只需要保证时区正确就不会有太大问题,哈哈。 但是今天我想认真对待...
继续阅读 »

计算机中的时间


看完这篇文章相信你会对计算机中的时间有更系统全面的认识。


我经常自嘲,自己写的程序运行不超过3年,因为大部分项目方就早早跑路了。大多数项目上线后,你跟这个项目就再无瓜葛,关于时间你只需要保证时区正确就不会有太大问题,哈哈。 但是今天我想认真对待时间这个问题,作为一个库作者或基础软件作者,就需要考虑下游项目万一因为你处理时间不当而造成困扰,影响范围就比较广了。


计算机中与时间有关的关键词:


时间类型
时间戳(timestamp
定时器(例如jssetInterval())
时间计算
时间段
超时(setTimeout())
时间片
GMT
UTC
Unix时间戳
ISO8601
CST
EST

看到这些你可能会疑惑,为何一个时间竟然如此复杂!!


如果下面的问题你都能答上来,那这篇文章对你的帮助微乎其微,不如做些更有意义的事情。



  • 常用的时间格式,他们都遵循哪些标准?

  • 什么是GMT?

  • 什么是UTC?

  • GMT UTC 和ISO8601有什么区别?

  • RFC5322是什么?

  • RFC5322 采用的是GMT还是UTC?

  • ISO8601 使用的是UTC还是GMT?

  • 在ISO8601中 Z可以使用+00:00表示吗?

  • UTC什么时候校准?

  • CST是东八区吗?

  • Z是ISO 8601规定的吗,为什么是Z?

  • 时区划分是哪个标准定义的?

  • 为什么是1970年1月1日呢?

  • 到了2038年时间戳溢出了怎么办?

  • 计算机中时间的本质是一个long类型吗?

  • WEB前后端用哪个格式传输好?

  • '2024-01-01T24:00:00' 等于 '2024-01-02T00:00:00' ??



正文开始


1. 两种时间标准


UTC和GMT都是时间标准,定义事件的精度。它们只表示 零时区 的时间,本地时间则需要与 时区 或偏移 结合后表示。这两个标准之间差距通常不会超过一秒。


UTC(协调世界时)


UTC,即协调世界时(Coordinated Universal Time),是一种基于原子钟的时间标准。它的校准是根据地球自转的变化而进行的,插入或删除闰秒的实际需求在短期内是难以预测的,因此这个决定通常是在需要校准的时候发布。 闰秒通常由国际电信联盟(ITU) 和国际度量衡局(BIPM) 等组织进行发布。由国际原子时(International Atomic Time,TAI) 通过闰秒 的调整来保持与地球自转的同步。


GMT(格林尼治标准时间)


以英国伦敦附近的格林尼治天文台(0度经线,本初子午线)的时间为基准。使用地球自转的平均速度来测量时间,是一种相对于太阳的平均时刻。尽管 GMT 仍然被广泛使用,但现代科学和国际标准更倾向于使用UTC。


2. 两种显示标准


上面我们讨论的时间标准主要保证的是时间的精度,时间显示标准指的是时间的字符串表示格式。我们熟知的有 RFC 5322 和 ISO 8601。


RFC 5322 电子邮件消息格式的规范


RFC 5322 的最新版本是在2008年10月在IETF发布的,你阅读时可能有了更新的版本。



RFC 5322 是一份由 Internet Engineering Task Force (IETF) 制定的标准,定义了 Internet 上的电子邮件消息的格式规范。该标准于2008年发布,是对之前的 RFC 2822 的更新和扩展。虽然 RFC 5322 主要关注电子邮件消息的格式,但其中的某些规范,比如日期时间格式,也被其他领域采纳,例如在 HTTP 协议中用作日期头部(Date Header)的表示。



格式通常如下:


Thu, 14 Dec 2023 05:36:56 GMT

时区部分为了可读可以如下表示:


Thu, 14 Dec 2023 05:36:56 CST
Thu, 14 Dec 2023 05:36:56 +0800
Thu, 14 Dec 2023 05:36:56 +0000
Thu, 14 Dec 2023 05:36:56 Z

但并不是所有程序都兼容这种时区格式,通常程序会忽略时区,在写程序时要做好测试。标准没有定义毫秒数如何显示。


需要注意的是,有时候我们会见到这种格式Tue Jan 19 2038 11:14:07 GMT+0800 (中国标准时间),这是js日期对象转字符串的格式,它与标准无关,千万不要混淆了。


ISO 8601


ISO 8601 最新版本是 ISO 8601:2019,发布日期为2019年11月15日,你阅读时可能有了更新的版本。


下面列举一些格式示例:


2004-05-03T17:30:08+08:00
2004-05-03T17:30:08+00:00
2004-05-03T17:30:08Z
2004-05-03T17:30:08.000+08:00

标准并没有定义小数位数,保险起见秒后面一般是3位小数用来表示毫秒数。 字母 "Z" 是 "zero"(零)的缩写,因此它被用来表示零时区,也可以使用+00:00,但Z更直观且简洁。



  1. 本标准提供两种方法来表示时间:一种是只有数字的基础格式;第二种是添加了分隔符的扩展格式,更易读。扩展格式使用连字符“-”来分隔日期,使用冒号“:”来分隔时间。比如2009年1月6日在扩展格式中可以写成"2009-01-06",在基本格式中可以简单地写成"20090106"而不会产生歧义。 若要表示前1年之前或9999年之后的年份,标准也允许有共识的双方扩展表达方式。双方应事先规定增加的位数,并且年份前必须有正号“+”或负号“-”而不使用“。依据标准,若年份带符号,则前1年为"+0000",前2年为"-0001",依此类推。

  2. 午夜,一日的开始:完全表示为000000或00:00:00;仅有小时和分表示为0000或00:00

  3. 午夜,一日的终止:完全表示为240000或24:00:00;仅有小时和分表示为2400或24:00

  4. 如果时间在零时区,并恰好与UTC相同,那么在时间最后加一个大写字母Z。Z是相对协调世界时时间0偏移的代号。 如下午2点30分5秒表示为14:30:05Z或143005Z;只表示小时和分,为1430Z或14:30Z;只表示小时,则为14Z或14Z。

  5. 其它时区用实际时间加时差表示,当时的UTC+8时间表示为22:30:05+08:00或223005+0800,也可以简化成223005+08。


日期与时间合并表示时,要在时间前面加一大写字母T,如要表示东八区时间2004年5月3日下午5点30分8秒,可以写成2004-05-03T17:30:08+08:00或20040503T173008+08。


在编写API时推荐使用ISO 8601标准接收参数或响应结果,并且做好时区测试,因为不同编程语言中实现可能有差异。


时区划分和偏移



全球被分为24个时区,每个时区对应一个小时的时间差。 时区划分由IANA维护和管理,其时区数据库被称为 TZ Database(或 Olson Database)。这个数据库包含了全球各个时区的信息,包括时区的名称、标识符、以及历史性的时区变更数据,例如夏令时的开始和结束时间等。在许多操作系统(如Linux、Unix、macOS等)和编程语言(如Java、Python等)中得到广泛应用。


TZ Database具体见我整理的表格,是从Postgresql中导出的一份Excel,关注公众号"程序饲养员",回复"tz"



时区标识符采用"洲名/城市名"的命名规范,例如:"America/New_York"或"Asia/Shanghai"。这种命名方式旨在更准确地反映时区的地理位置。时区的具体规定和管理可能因国家、地区、或国际组织而异。


有一些时区是按照半小时或15分钟的间隔进行偏移的,以适应地理和政治需求。在某些地区,特别是位于边界上的地区,也可能采用不同的时区规则。


EST,CST、GMT(另外一个含义是格林尼治标准时间)这些都是时区的缩写。


这种简写存在重复,如CST 可能有多种不同的含义,China Standard Time(中国标准时间),它对应于 UTC+8,即东八区。Central Standard Time(中部标准时间) 在美国中部标准时间的缩写中也有用。中部标准时间对应于 UTC-6,即西六区。因此在某些软件配置时不要使用简称,一定要使用全称,如”Asia/Shanghai“。


采用东八区的国家和地区有哪些



  • 中国: 中国标准时间(China Standard Time,CST)是东八区的时区,对应于UTC+8。

  • 中国香港: 中国香港也采用东八区的时区,对应于UTC+8。

  • 中国澳门: 澳门也在东八区,使用UTC+8。

  • 中国台湾: 台湾同样在东八区,使用UTC+8。

  • 新加坡: 新加坡位于东八区,使用UTC+8。

  • 马来西亚: 马来西亚的半岛部分和东马来西亚位于东八区,使用UTC+8。

  • 菲律宾: 菲律宾采用东八区的时区,对应于UTC+8。


计算机系统中的时间 —— Unix时间戳


Unix时间戳(Unix timestamp)定义为从1970年01月01日00时00分00秒(UTC)起至现在经过的总秒数(秒是毫秒、微妙、纳秒的总称)。


这个时间点通常被称为 "Epoch" 或 "Unix Epoch"。时间戳是一个整数,表示从 Epoch 开始经过的秒数。


一些关键概念:



  1. 起始时间点: Unix 时间戳的起始时间是 1970 年 1 月 1 日 00:00:00 UTC。在这一刻,Unix 时间戳为 0。

  2. 增量单位: Unix 时间戳以秒为单位递增。每过一秒,时间戳的值增加 1。

  3. 正负值: 时间戳可以是正值或负值。正值表示从 Epoch 开始经过的秒数,而负值表示 Epoch 之前的秒数。

  4. 精度: 通常情况下,Unix 时间戳以整数形式表示秒数。有时也会使用浮点数表示秒的小数部分,以提供更精细的时间分辨率。精确到秒是10位;有些编程语言精确到毫秒是13位,被称为毫秒时间戳。


为什么是1970年1月1日?


这个选择主要是出于历史和技术的考虑。


Unix 操作系统的设计者之一,肯·汤普森(Ken Thompson)和丹尼斯·里奇(Dennis Ritchie)在开发 Unix 操作系统时,需要选择一个固定的起始点来表示时间。1970-01-01 00:00:00 UTC 被选为起始时间。这个设计的简洁性和通用性使得 Unix 时间戳成为计算机系统中广泛使用的标准方式来表示和处理时间。


时间戳为什么只能表示到2038年01月19日03时14分07秒?


在许多系统中,结构体time_t 被定义为 long,具体实现取决于编译器和操作系统的架构。例如,在32位系统上,time_t 可能是32位的 long,而在64位系统上,它可能是64位的 long。 32位有符号long类型,实际表示整数只有31位,最大能表示十进制2147483647(01111111 11111111 11111111 11111111)。


> new Date(2147483647000)
< Tue Jan 19 2038 11:14:07 GMT+0800 (中国标准时间)

实际上到2038年01月19日03时14分07秒,便会到达最大时间,过了这个时间点,所有32位操作系统时间便会变为10000000 00000000 00000000 00000000。因具体实现不同,有可能会是1901年12月13日20时45分52秒,这样便会出现时间回归的现象,很多软件便会运行异常了。


至于时间回归的现象相信随着64为操作系统的产生逐渐得到解决,因为用64位操作系统可以表示到292,277,026,596年12月4日15时30分08秒。


另外,考虑时区因素,北京时间的时间戳的起始时间是1970-01-01T08:00:00+08:00。


好了,关于计算机中的时间就说完了,有疑问评论区相见 或 关注 程序饲养员 公号。



作者:程序饲养员
来源:juejin.cn/post/7312640704404111387
收起阅读 »

Arrays.asList() 隐藏的陷阱,你避开了吗?

[Arrays.asList()方法介绍] [Arrays.asList()方法的坑] [解决Arrays.asList()方法的坑] [总结] [Arrays.asList()方法介绍] [Arrays.asList()方法的坑] [解决Arrays.asL...
继续阅读 »

  • [Arrays.asList()方法介绍]

  • [Arrays.asList()方法的坑]

  • [解决Arrays.asList()方法的坑]

  • [总结]

  • [Arrays.asList()方法介绍]

  • [Arrays.asList()方法的坑]

  • [解决Arrays.asList()方法的坑]

  • [总结]




在Java中,我们经常需要将数组转换为List来方便地进行操作。Arrays.asList()方法是一种常见的方式,但是它存在一个不太常见但需要注意的坑。


本文将深入探讨Arrays.asList()的使用,揭示其中的陷阱,并提供解决方案。


[Arrays.asList()方法介绍]


Arrays.asList()方法是将数组转换为List的方法,它返回一个List对象,但这个List对象并不是java.util.ArrayList对象,而是Arrays内部的ArrayList对象。


Arrays.ArrayList类继承自AbstractList,实现了List接口。它重写了add()remove()等修改List结构的方法,并将它们直接抛出UnsupportedOperationException异常,从而禁止了对List结构的修改。


具体来说,Arrays.asList()方法返回的是Arrays类中的一个私有静态内部类ArrayList,它继承自AbstractList类,实现了List接口。


Arrays.asList()方法的使用非常简单,只需要将一个数组作为参数传递给该方法即可。例如:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);


基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能




[Arrays.asList()方法的坑]


尽管Arrays.asList()方法很方便,但也存在一些坑,其中最常见的一个是:在使用Arrays.asList()方法时,如果对返回的List对象进行修改(例如增加、删除元素),将会抛出"UnsupportedOperationException"异常。


为什么会出现这个异常呢?这是因为Arrays.asList()方法返回的List对象,是一个固定大小的List,不能进行结构上的修改,否则会抛出异常。


下面的代码演示了这个问题:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);
list.add("d"); // 抛出 UnsupportedOperationException 异常

上述代码中,我们尝试向List对象中添加一个新的元素"d",结果会抛出"UnsupportedOperationException"异常。



基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能




[解决Arrays.asList()方法的坑]


要解决Arrays.asList()方法的坑,我们需要将返回的List对象转换为一个可修改的List对象。有几种方法可以实现这个目标:


[方法一:使用java.util.ArrayList类]


我们可以使用java.util.ArrayList类,将Arrays.asList()方法返回的List对象转换为一个java.util.ArrayList对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
list.add("d"); // 正常运行

上述代码中,我们首先使用Arrays.asList()方法将一个数组转换为一个List对象,然后使用ArrayList的构造方法,将这个List对象转换为一个java.util.ArrayList对象,最后可以向这个ArrayList对象中添加元素。


[方法二:使用Collections类]


我们也可以使用Collections类提供的静态方法,将Arrays.asList()方法返回的List对象转换为一个可修改的List对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
Collections.addAll(list, "d"); // 正常运行

通过Collections.addAll()方法,我们可以将数组中的元素逐个添加到一个新的ArrayList对象中,从而实现了可修改性。


[总结]


在使用Arrays.asList()方法时,需要注意返回的List对象是一个固定大小的List,不支持结构上的修改操作。为了避免这个陷阱,我们可以使用java.util.ArrayList或Collections类提供的方法将返回的List对象转换为可修改的List。通过了解这个陷阱并采取相应的解决方案,我们可以安全地将数组转换为List,并避免潜在的异常情况。


不要让Arrays.asList()的陷阱坑了你的代码!


在Java中,我们经常需要将数组转换为List来方便地进行操作。Arrays.asList()方法是一种常见的方式,但是它存在一个不太常见但需要注意的坑。本文将深入探讨Arrays.asList()的使用,揭示其中的陷阱,并提供解决方案。


[Arrays.asList()方法介绍]


Arrays.asList()方法是将数组转换为List的方法,它返回一个List对象,但这个List对象并不是java.util.ArrayList对象,而是Arrays内部的ArrayList对象。


Arrays.ArrayList类继承自AbstractList,实现了List接口。它重写了add()remove()等修改List结构的方法,并将它们直接抛出UnsupportedOperationException异常,从而禁止了对List结构的修改。


具体来说,Arrays.asList()方法返回的是Arrays类中的一个私有静态内部类ArrayList,它继承自AbstractList类,实现了List接口。


Arrays.asList() 方法的使用非常简单,只需要将一个数组作为参数传递给该方法即可。例如:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);

[Arrays.asList()方法的坑]


尽管Arrays.asList()方法很方便,但也存在一些坑,其中最常见的一个是:在使用Arrays.asList()方法时,如果对返回的List对象进行修改(例如增加、删除元素),将会抛出"UnsupportedOperationException"异常。


为什么会出现这个异常呢?这是因为Arrays.asList()方法返回的List对象,是一个固定大小的List,不能进行结构上的修改,否则会抛出异常。


下面的代码演示了这个问题:


String[] arr = new String[]{"a""b""c"};
List<String> list = Arrays.asList(arr);
list.add("d"); // 抛出 UnsupportedOperationException 异常

上述代码中,我们尝试向List对象中添加一个新的元素"d",结果会抛出"UnsupportedOperationException"异常。


[解决Arrays.asList()方法的坑]


要解决Arrays.asList()方法的坑,我们需要将返回的List对象转换为一个可修改的List对象。有几种方法可以实现这个目标:


[方法一:使用java.util.ArrayList类]


我们可以使用java.util.ArrayList类,将Arrays.asList()方法返回的List对象转换为一个java.util.ArrayList对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
list.add("d"); // 正常运行

上述代码中,我们首先使用Arrays.asList()方法将一个数组转换为一个List对象,然后使用ArrayList的构造方法,将这个List对象转换为一个java.util.ArrayList对象,最后可以向这个ArrayList对象中添加元素。


[方法二:使用Collections类]


我们也可以使用Collections类提供的静态方法,将Arrays.asList()方法返回的List对象转换为一个可修改的List对象,示例如下:


String[] arr = new String[]{"a""b""c"};
List<String> list = new ArrayList<>(Arrays.asList(arr));
Collections.addAll(list, "d"); // 正常运行

通过Collections.addAll()方法,我们可以将数组中的元素逐个添加到一个新的ArrayList对象中,从而实现了可修改性。


[总结]


在使用Arrays.asList()方法时,需要注意返回的List对象是一个固定大小的List,不支持结构上的修改操作。为了避免这个陷阱,我们可以使用java.util.ArrayList或Collections类提供的方法将返回的List对象转换为可修改的List。通过了解这个陷阱并采取相应的解决方案,我们可以安全地将数组转换为List,并避免潜在的异常情况。


不要让Arrays.asList()的陷阱坑了你的代码!


作者:智多星云
来源:juejin.cn/post/7258863572553302071
收起阅读 »

Android 粒子漩涡动画

前言 粒子动画经常用于大画幅的渲染效果,实际上难度并不高,但是在使用粒子动画时,必须要遵循的一些要素,主要是: 起点 矢量速度 符合运动学公式 在之前的文章中,如《烟花效果》和《粒子喷泉》是典型的粒子动画,起点之所以重要是因为其实位置决定粒子出现的位置,矢...
继续阅读 »

前言


粒子动画经常用于大画幅的渲染效果,实际上难度并不高,但是在使用粒子动画时,必须要遵循的一些要素,主要是:



  • 起点

  • 矢量速度

  • 符合运动学公式


在之前的文章中,如《烟花效果》和《粒子喷泉》是典型的粒子动画,起点之所以重要是因为其实位置决定粒子出现的位置,矢量速度则决定了快慢和方向,运动学公式属于粒子动画的一部分,当然不是物理性的,毕竟平面尺寸也就那么长,这里的物理学公式使得画面更加丝滑而无跳动感觉。


本篇将实现下面的效果


fire_90.gif
注意:gif图有些卡,实际上流畅很多


本篇效果实现


本篇效果是无数圆随机产生然后渐渐变大并外旋,另外也有雨滴,这里的雨滴相对简单一些。


首先定义粒子对象


定义粒子对象是非常重要的,绝大部分倾下粒子本身就是需要单独控制的,因为每个粒子的轨迹都是有所差别的。


定义圆圈粒子


private static class Circle {
float x;
float y;
int color;

float radius;

Circle(float x, float y, float radius) {
reset(x, y, radius);
}

private void reset(float x, float y, float radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = Color.rgb((int) (Math.random() * 256), (int) (Math.random() * 256), (int) (Math.random() * 256));
}
}

定义雨滴


private static class RainDrop {
float x;
float y;

RainDrop(float x, float y) {
this.x = x;
this.y = y;
}
}

定义粒子管理集合


private ArrayList mParticles;
private ArrayList mRainDrops;
private long mLastUpdateTime; //记录执行时间

生成粒子对象



  • 生成雨滴是从顶部屏幕意外开始,而y = -50f值是雨滴的高度决定。

  • 圆圈是随机产生,在中心位置圆圈内。


// 创建新的雨滴
if (mRainDrops.size() < 80) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding / 2f * Math.random();
RainDrop drop = new RainDrop((float) x, -50f);
mRainDrops.add(drop);
}

// 创建新的粒子
if (mParticles.size() < 100) {
float x = (float) (getWidth() / 2f - radius + 2*radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );

Circle particle = new Circle(x, y,5);
mParticles.add(particle);
}

绘制雨滴


雨滴的绘制非常简单,调用相应的canvas方法即可


// 绘制雨滴
mPaint.setColor(Color.WHITE);
for (RainDrop drop : mRainDrops) {
canvas.drawLine(drop.x, drop.y, drop.x, drop.y + 20, mPaint);
}

// 绘制粒子
for (Circle particle : mParticles) {
mPaint.setColor(particle.color);
canvas.drawCircle(particle.x, particle.y, particle.radius, mPaint);
}

更新粒子位置


雨滴的更新相对简单,但是圆圈的旋转是一个难点,一个重要的问题是如何旋转粒子的,其实有很多方法,其中最笨的方法是旋转Canvas坐标系,底层有很多矩阵计算,但是这个似乎使用Math.atan2(y,x)显然更加方便,我们只需要在当前角度加上偏移量就能旋转。


float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.65f;

下面是完整的更新逻辑


// 更新雨滴位置
Iterator rainIterator = mRainDrops.iterator();
while (rainIterator.hasNext()) {
RainDrop drop = rainIterator.next();
if (drop.y > getHeight() + 50) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding * Math.random();

drop.x = (float) (x);
drop.y = -50;
} else {
drop.y += 20;
}

}

// 更新粒子位置
long currentTime = System.currentTimeMillis();
float deltaTime = (currentTime - mLastUpdateTime) / 1000f;
mLastUpdateTime = currentTime;

float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;

Iterator iterator = mParticles.iterator();
while (iterator.hasNext()) {
Circle particle = iterator.next();
float dx = particle.x - centerX;
float dy = particle.y - centerY;
float distance = (float) Math.sqrt(dx * dx + dy * dy) + 4.5f;// 增加偏移
float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.5f;
particle.radius += 1f;

particle.x = centerX + (float) Math.cos(angle) * distance;
particle.y = centerY + (float) Math.sin(angle) * distance;

if (particle.radius > 10) {
int maxRadius = 100;
float fraction = (particle.radius - 10) / (maxRadius - 10);
if (fraction >= 1) {
fraction = 1;
}
particle.color = argb((int) (255 * (1 - fraction)), Color.red(particle.color), Color.green(particle.color), Color.blue(particle.color));
}
if (Color.alpha(particle.color) == 0) {

float x = (float) (getWidth() / 2f - radius + 2* radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );
particle.reset(x,y, 5);
}

}

粒子刷新


其实刷新机制我们以前经常使用,调用postInvalidate即可,本身就是View自身的方法。


总结


本篇主要内容总体上就是这些,下面是全部代码逻辑


public class VortexView extends View {

private Paint mPaint;
private ArrayList mParticles;
private ArrayList mRainDrops;
private long mLastUpdateTime;
private int padding = 20;

public VortexView(Context context) {
super(context);
mPaint = new Paint();
mParticles = new ArrayList<>();
mRainDrops = new ArrayList<>();
mLastUpdateTime = System.currentTimeMillis();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float radius = Math.min(getWidth(), getHeight()) / 3f;

// 创建新的雨滴
if (mRainDrops.size() < 80) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding / 2f * Math.random();
RainDrop drop = new RainDrop((float) x, -50f);
mRainDrops.add(drop);
}

// 创建新的粒子
if (mParticles.size() < 100) {
float x = (float) (getWidth() / 2f - radius + 2*radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );

Circle particle = new Circle(x, y,5);
mParticles.add(particle);
}

// 绘制雨滴
mPaint.setColor(Color.WHITE);
for (RainDrop drop : mRainDrops) {
canvas.drawLine(drop.x, drop.y, drop.x, drop.y + 20, mPaint);
}

// 绘制粒子
for (Circle particle : mParticles) {
mPaint.setColor(particle.color);
canvas.drawCircle(particle.x, particle.y, particle.radius, mPaint);
}

// 更新雨滴位置
Iterator rainIterator = mRainDrops.iterator();
while (rainIterator.hasNext()) {
RainDrop drop = rainIterator.next();
if (drop.y > getHeight() + 50) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding * Math.random();

drop.x = (float) (x);
drop.y = -50;
} else {
drop.y += 20;
}

}

// 更新粒子位置
long currentTime = System.currentTimeMillis();
float deltaTime = (currentTime - mLastUpdateTime) / 1000f;
mLastUpdateTime = currentTime;

float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;

Iterator iterator = mParticles.iterator();
while (iterator.hasNext()) {
Circle particle = iterator.next();
float dx = particle.x - centerX;
float dy = particle.y - centerY;
float distance = (float) Math.sqrt(dx * dx + dy * dy) + 3.5f;// 增加偏移
float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.65f;
particle.radius += 1f;

particle.x = centerX + (float) Math.cos(angle) * distance;
particle.y = centerY + (float) Math.sin(angle) * distance;

if (particle.radius > 10) {
int maxRadius = 100;
float fraction = (particle.radius - 10) / (maxRadius - 10);
if (fraction >= 1) {
fraction = 1;
}
particle.color = argb((int) (255 * (1 - fraction)), Color.red(particle.color), Color.green(particle.color), Color.blue(particle.color));
}
if (Color.alpha(particle.color) == 0) {

float x = (float) (getWidth() / 2f - radius + 2* radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );
particle.reset(x,y, 5);
}

}

Collections.sort(mParticles, comparator);

// 使view无效从而重新绘制,实现动画效果
invalidate();
}
Comparator comparator = new Comparator() {
@Override
public int compare(Circle left, Circle right) {
return (int) (left.radius - right.radius);
}
};

public static int argb(
int alpha,
int red,
int green,
int blue)
{
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}

private static class Circle {
float x;
float y;
int color;

float radius;

Circle(float x, float y, float radius) {
reset(x, y, radius);
}

private void reset(float x, float y, float radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = Color.rgb((int) (Math.random() * 256), (int) (Math.random() * 256), (int) (Math.random() * 256));
}
}

private static class RainDrop {
float x;
float y;

RainDrop(float x, float y) {
this.x = x;
this.y = y;
}
}
}

作者:时光少年
来源:juejin.cn/post/7317957339012202496
收起阅读 »

百度考题:反复横跳的个性签名

web
浅聊一下 在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下” 当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签...
继续阅读 »

浅聊一下


在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下”


image.png


当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签名组件

动手


我们要有JS组件思维,个性签名是很多地方都可以用到的,我们把它做成组件,用到的时候直接搬过去就好了。所以我们将使用原生JS将组件封装起来(大家也可以使用VUE)


EditInPlace 类


我们要使用这个JS组件,首先得将其方法和参数封装在一个类里,再通过类的实例化对象来展示。


function EditInPlace(id,parent,value){
this.id = id;
this.parent = parent;
this.value =value || "这个家伙很懒,什么都没有留下";
this.createElements()//动态装配html结点
this.attachEvents();
}


  1. 将传入的idparentvalue参数赋值给新创建的对象的对应属性。

  2. 如果没有提供value参数,则将默认字符串"这个家伙很懒,什么都没有留下"赋值给新对象的value属性。

  3. 调用createElements方法来动态装配HTML节点。

  4. 调用attachEvents方法来附加事件。


EditInPlace.prototype


在 EditInPlace 类中,我们调用了createElements() attachEvents()两个方法,所以我们得在原型上定义这两个方法


createElements


    createElements:function(){
this.containerElement = document.createElement('div');
this.containerElement.id= this.id;
//签名文字部分
this.staicElement = document.createElement('span');
this.staicElement.innerText = this.value
this.containerElement.appendChild(this.staicElement);
//输入框
this.fieldElement = document.createElement('input')
this.fieldElement.type = 'text'
this.fieldElement.value = this.value;
this.containerElement.appendChild(this.fieldElement);
//save 确认
this.saveButton = document.createElement('input');
this.saveButton.type = 'button'
this.saveButton.value = '保存'
this.containerElement.appendChild(this.saveButton)
//取消按钮
this.cancelButton = document.createElement('input')
this.cancelButton.type = 'button'
this.cancelButton.value = '取消'
this.containerElement.appendChild(this.cancelButton)


this.parent.appendChild(this.containerElement)
this.converToText();

}


  1. 创建一个<div>元素,并将其赋值给this.containerElement属性,并设置其id为传入的id

  2. 创建一个<span>元素,并将其赋值给this.staicElement属性,然后设置其文本内容为传入的value

  3. this.staicElement添加到this.containerElement中。

  4. 创建一个<input>元素,并将其赋值给this.fieldElement属性,设置其类型为"text",并将其值设置为传入的value

  5. this.fieldElement添加到this.containerElement中。

  6. 创建一个保存按钮(<input type="button">),并将其赋值给this.saveButton属性,并设置其值为"保存"。

  7. 将保存按钮添加到this.containerElement中。

  8. 创建一个取消按钮(<input type="button">),并将其赋值给this.cancelButton属性,并设置其值为"取消"。

  9. 将取消按钮添加到this.containerElement中。

  10. this.containerElement添加到指定的父元素this.parent中。

  11. 调用converToText方法。


这个方法主要是用于动态生成包含静态文本、输入框和按钮的编辑组件,并将其添加到指定的父元素中。也就是说我们在这里主要就是创建了一个div,div里面有一个text和一个input,还有保存和取消按钮


div和span的显示


我们怎么样实现点击一下,就让text不显示,input框显示呢?
就是在点击一下以后,让text的display为'none',让input框和按钮为 'inline'就ok了,同样的,在保存或取消时采用相反的方法就好


    converToText:function(){
this.staicElement.style.display = 'inline';
this.fieldElement.style.display = 'none'
this.saveButton.style.display = 'none'
this.cancelButton.style.display = 'none'
},
converToEdit:function(){
this.staicElement.style.display = 'none';
this.fieldElement.style.display = 'inline'
this.saveButton.style.display = 'inline'
this.cancelButton.style.display = 'inline'
}

attachEvents


当然,我们需要在text文本和按钮上添加点击事件


    attachEvents:function(){
var that = this
this.staicElement.addEventListener('click',this.converToEdit.bind(this))
this.cancelButton.addEventListener('click',this.converToText.bind(this))
this.saveButton.addEventListener('click',function(){
var value = that.fieldElement.value;
that.staicElement.innerText = value;
that.converToText();
})
}


  1. 通过var that = this将当前对象的引用保存在变量that中,以便在后续的事件监听器中使用。

  2. 使用addEventListener为静态元素(this.staicElement)添加了一个点击事件的监听器,当静态元素被点击时,会调用this.converToEdit.bind(this)方法,这样做可以确保在converToEdit方法中this指向当前对象。

  3. 为取消按钮(this.cancelButton)添加了一个点击事件的监听器,当取消按钮被点击时,会调用this.converToText.bind(this)方法,同样也是为了确保在converToText方法中this指向当前对象。

  4. 为保存按钮(this.saveButton)添加了一个点击事件的监听器,当保存按钮被点击时,会执行一个匿名函数,该函数首先获取输入框的值,然后将该值更新到静态元素的文本内容中,并最后调用converToText方法,同样使用了变量that来确保在匿名函数中this指向当前对象。


通过这些事件监听器的设置,实现了以下功能:



  • 点击静态元素时,将编辑组件转换为编辑状态。

  • 点击取消按钮时,将编辑组件转换为静态状态。

  • 点击保存按钮时,获取输入框的值,更新静态元素的文本内容,并将编辑组件转换为静态状态。


HTML


在html中通过new将组件挂载在root上


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>就地编辑-EditIntPlace</title>
</head>
<body>
<div id="root"></div>
<script src="./editor_in_place.js"></script>
<script>
// 为了收集签名,给个表单太重了 不好看
// 个性签名,就地编辑
// 面向对象 将整体开发流程封装 封装成一个组件
new EditInPlace('editor',document.getElementById('root'))//名字 挂载点
</script>
</body>
</html>

完整效果



结尾


写的不太美观,掘友们可以自己试试加上样式,去掉按钮,再加上一张绝美的背景图片...


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

前端实习生如何提升自己

web
这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。 同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习...
继续阅读 »

这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。


同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习生。


正常来讲一个资深的研发,最多可以辅导两个实习生,但因为我们是初创阶段,为了降低成本,我们经常会出现需要同时辅导三个以上实习生的情况。


到网上搜了一圈,都没有系统讲解实习生如何提升自己的资料,本文将结合自己过去十年带人的经验,面向前端岗位,谈一谈前端实习生如何提升自己。


作者简介


先后就读于北京交大和某军校,毕业后任职于原北京军区38集团军,带过两次新兵连,最多时管理过120人左右的团队。


退役后,先进入外企GEMALTO做操作系统研发,后进入搜狐、易车和美团等公司做大前端开发和团队管理,最多时管理过30多人的大前端团队,负责过几百个产品的前端开发工作,做过十多个大型复杂前端项目的架构工作。


目前在艺培行业创业,通过AIGC技术提升艺培机构门店运营和招生推广的效率和降低成本。


如何选择实习?


我永远相信,选择比努力奋斗更重要。选择一家合适的实习单位或公司,可以达到事半功倍的效果。


大公司的优势


毫无疑问,很多大学生都会选择大公司作为自己的实习单位,尤其是一些上市公司,甚至为了求得一个实习机会,即使倒贴钱也愿意。


与小企业相比,大公司一般都会有相对完整的企业制度。从签实习协议开始,如薪资结算、工作安排等,完全按照相关的企业制度,以及法律法规进行,这样一定程度有利于保障实习生的劳动权益。


与此相比,一些小企业甚至连实习协议都不签,这对于实习生来说是非常不利的。


大公司一般都会有日历表和日常的作息时刻表。因此,在大公司工作一般都按照正常的作息时间上下班,不太会出现加班的情况。


而相对来说,小企业由于刚刚起步,工作量非常大,人手常常不够,因此加班加点是家常便饭了。


每个大公司都会具有良好的、独特的企业文化,从办公室的装饰、到公司员工午休时的娱乐活动,无不体现出自己的特性。因此,在大公司里实习,你能够亲身感受到大公司内部的企业文化。


而小企业,则不太可能会有自己的企业文化,即使有多数也是非常糟糕的。


小企业的特点


在大公司实习,由于大公司里本身就人员冗余,再加上对“实习生”的不信任,因此他们给实习生安排的工作常属于打杂性质的。可能你在大公司里呆了两个月,可是连它最基本的业务流程都没弄明白。


而在小企业里,由此大多处于起步,没有太多的职位分化,你可能一人需要兼多职,许多事情都得自己解决,能够提高你独立解决问题的能力。


大公司一般有完整的部门体制,上下级关系相对严格分明,因此,官僚作风比较明显,新人难有发言权。


而在小企业,这方面的阻力则小得多,使新人少受束缚,更自由地施展自己的能力、表达自己的想法。


小结


实习为了给将来就业铺路,因此,实习最重要的在于经验。当然,在大公司实习如果能在增加自己进该公司的概率那固然好,但如果只是为了在简历里加上“在某大公司里实习过”的虚名,那实在是没有必要。


结合未来职业选择,要实习的公司和岗位是和你的职业理想直接相关的,这个岗位的实习和这个公司的进入可以给你日后的职业发展加分。


选你能在实习期间出成果的项目。如果你选了个很大的项目,半年还没做完,这就说不清楚是你没做好,还是项目本身的问题了。


实习的时候要更深入的了解企业的实际情况和问题,结合理论和实际,要给企业解决实际问题,有一个具体的成果,让企业真正感觉到你的项目做好了


我个人建议,在正式工作前,大公司和小企业,最好都能争取去实习一段时间,但第一份实习最好去大公司。


通过实习,希望大家能够验证自己的职业抉择,了解目标工作内容,学习工作及企业标准,找到自身职业的差距。


如何做好实习


选择好合适的实习单位,只是走出了万里长征的第一步,要做好实习,大家可以关注以下四个方面:


端正态度


从实习生个人的角度来看,多数人的出发点都是学习提升,但如果我们只是抱着纯学习而不为企业创造价值的心态的话,实习工作就不容易长久。


我们可以从企业的角度来看:



  • 一是实习提供了观察一位潜在的长期员工工作情况的方法,为企业未来发展培养骨干技术力量与领导人;

  • 二手有利于廉价劳动力争夺人才,刚毕业的学员便于管理,这样不仅能降低成本,还能提高企业的知名度,有利于企业长远发展。


实习生只有表现出了企业看重的那些方面,企业才会投入时间和精力去好好培养他,教一些真本领。


不管是在美团,还是我自己创业的公司,我都是基于相同的原则,去评估是否留下某个实习,进行重点培养。


我个人看重以下几点:



  • 不挑活:即使不是自己喜欢或在工作范围的任务,也能积极去完成;

  • 爱思考:接到任务或经过指导后,能及时反馈存在的问题和提升改进方案;

  • 善沟通:遇到困难及时寻求帮助,工作不开心了不封闭自己、不拒绝沟通。


不管当年我去实习还是上班,我的心态都是先为企业创造价值,在此基础上再争取完成自己的个人目标。


注重习惯


有的人毕业10年都还没毕业3年的人混得好,不是因为能力不行,而是因为他没有养成良好的职场习惯,以至于总是吃亏。


开发人员培养好的习惯至关重要。编写好的代码,持续学习,逐步提高代码质量。


主动反馈和同步信息


这个习惯非常重要,可以避免大家做很多无用功,大大提升大家日常工作的效率,而且也特别容易获得大家的好感与信任。


经常冒烟自测


对于自己负责的功能或产品,不管是测试阶段还是正式发布,只要需要其他人使用,都最好冒烟自测一遍,避免低级错误,进而影响大家对你专业度的信任。


良好的时间管理


先每天花几分钟确定好我今天要办的最重要的三到六件事情,如此能帮助提高整体的效率;把今天要做的所有事情一一写下来,按轻重缓急排序,可以按照要事第一的原则;每做完一件事情就把它划掉,在今天工作结束之前检查我是不是把整个list的工作都完成了。


避免重复的错误


不要犯了错误之后就草草了事,花一点时间深度挖掘清楚我犯错的底层原因是什么,再给自己一些小提示,比如做一个便利贴做提醒,在电脑桌面上面放一个警告,避免相同的错误重复发生。


构建知识体系


在信息爆炸的年代,碎片化的知识很多,系统学习的时间越来越少,如果没有自己的知识体系,很容易被淹没在知识的海洋中,而且难以记忆。


100分程序员推荐的做法,通过Wiki或者其他知识管理工具构建一个知识框架,大的分类可以包括软技能、架构、语言、前端、后端等,小的分类可以更细化。


培养大局观


程序员比较容易陷入的困境是专注于自己的一亩三分地,不关心团队的进度和业绩,也不关心软件的整体架构和其他模块。


这种状态长期下去没有好处,特别是在大公司中,逐渐成长为一颗螺丝钉。100分程序员会在工作之余,多看看其他在做什么,看看团队的整体规划,看看软件系统的架构和说明文档。


对自己的工作更理解,而且知道为什么这个产品应该这样设计,为什么领导应该这样做规划,这种大局观非常有利于自己的职业生涯。


技能提升


通过实习阶段,你才会知道专业知识在工作中的具体运用,才能切身体会专业知识的重要性,这样也让自己在以后的学习中更加的努力和勤奋和有所侧重的安排各科目的学习时间。


对于前端实习生的技能学习,除了打牢HTML、CSS和JS这些基础的前端知识以及熟练掌握vue、react这些框架开发技术栈,建议大家还需要选中自己感兴趣的前端技术领域进行持续深入的学习实践,具体请看我之前总结的文章:前端学哪些技能饭碗越铁收入还高


关系处理


大学生在实习过程中最难适应的就是人际关系,同时还要了解企业组织运行的特点、规章制度,了解企业文化对员工的影响等等,知道职场中和领导及同事该如何沟通,企业对员工有着什么样的需求等。


这些也只有在实习时,让自己处于团队之中,才能切身的体会和参与,只有这样大学生才会对社会生活有深刻的理解,对将来就业才有益处。


不论是对同事,还是对客户,还是对合作伙伴,都不要吝啬你的赞美之词。善于夸赞他人的员工,更容易获得他人的好评,有时还能让你成功避免一些不必要的麻烦。


当然,赞美他人,不要过于浮夸,要能说出赞美的理由,以及哪些是你表示认同的地方。比如,我们在称赞他人方案做得好的时候,不要只是说:“这方案不错”。


可以这么说:“这方案在市场推广方面的内容写得很好,逻辑清晰,数据充分,还能有同行的案例借鉴,十分值得我学习。”


要让别人感受到你是真心的在夸奖他,而不是讨好他。


我们常常很难控制激动时的情绪,发怒时更是。往往这个时候表达出的观点都是偏激的,事后再后悔就来不及了。


如果你与同事之间发生争执而发怒,请在这个时间段保持沉默。给自己找个地方安静一会儿,等到情绪稳定以后,再向他人表达你的观点,慢慢地,你就会更好地控制自己的情绪。


在职场中切忌过于情绪化,学会管理好自己的情绪是一个职场人必备的技能之一。坏情绪并不会帮助你解决任何问题,过多的抱怨只会使你成为负能量的传播者。


协作在软件开发中至关重要。一个好的开发人员应该能够与他人很好地合作,并了解如何有效地协作。这意味着对反馈持开放态度,积极寻求团队成员的意见,并愿意作为团队的一部分从事项目。


协作会带来更好的沟通、更高效的工作流程和更好的产品。


总结


社会实践对大学生的就业有着很大的促进作用,是大学生成功就业的前提和基础。


实习的目的因人而异,可以千差万别,而你实习的真正目的也只有自己才最清楚。只有在开始实习之前首先明确自己的目的,后面的路才会变得清晰明了。


作者:文艺码农天文
来源:juejin.cn/post/7319181229520568371
收起阅读 »

前端学哪些技能饭碗越铁收入还高

web
随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。 但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深...
继续阅读 »

随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。


但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深耕。


本文,我们就聊聊,掌握了哪些技能,能让前端同学,收入高且稳定。


端智能


首推的是端智能,很多行业大咖都认为,随着ChatGPT的横空出世,开启了第四次工业革命,很多产品都可以用大模型重做一遍。当前,我创业的方向,也和大模型有关。


当前的大模型主要还跑在云端,但云端的成本高,大模型的未来在端智能,这也是小米创始人雷军在今年一次发布会上提出的观点。


在2023年8月14日的雷军年度演讲中,雷军宣布小米已经在手机端跑通13亿参数的大模型,部分场景效果媲美云端。


目前,端上大模型的可行性和前景已经得到了业内的普遍认可,国内外各个科技大厂在大模型的端侧部署领域均开始布局,目前大量工程已在PC端、手机端实现大模型的离线部署,更有部分App登陆应用商店,只需下载即可畅通无阻地对话。


我们相信,在不久的将来,端上大模型推理将会成为智能应用的重要组成部分,为用户带来更加便捷、智能的体验。


我在美团从零研发了web端智能推理引擎,当时立项时,就给老板画饼,美团每天的几百亿推理调用,如果有一半用端智能替代的话,每年能为公司节省上亿元。


要想掌握端智能,需要学习深度学习的基本知识,还要掌握图形学和C++编程,通过webgl或webassembly 技术实现在Web端执行深度学习算法。


图形学


前面提到的端智能,只是涉及到了图形学中的webgl计算,但图形学的未来在元宇宙,通过3D渲染,实现VR、AR、XR等各种R。


计算机图形学是一门快速发展的领域,涵盖了三维建模、渲染、动画、虚拟现实等众多技术和应用。在电影、广告、游戏等领域中,计算机图形学的应用已经非常广泛。


熟练使用threejs开发各种3D应用,只能算是入门。真正的图形学高手,不仅可以架构类似3D家装软件的大型应用,而且能掌握渲染管线的底层原理,熟练掌握各种模型格式和解决各种软件,进行模型转换遇到的各种兼容问题。


随着计算机硬件和算法的不断进步,计算机图形学正迎来新的发展趋势。


首先是实时渲染与逼真度提升



  • 实时渲染技术:随着游戏和虚拟现实的兴起,对实时渲染的需求越来越高。计算机图形学将继续致力于研发更高效的实时渲染算法和硬件加速技术,以实现更逼真、流畅的视觉效果。

  • 光线追踪与全局照明:传统的实时渲染技术在光照模拟方面存在挑战。计算机图形学将借助光线追踪等技术,实现更精确的全局照明效果,提升场景的真实感和细节表现。


其次是虚拟与增强现实的融合



  • 混合现实技术:计算机图形学将与传感器技术、机器视觉等相结合,推动虚拟现实与增强现实的融合发展。通过实时感知和交互,用户可以在真实世界中与虚拟对象进行互动,创造更沉浸式的体验。

  • 空间感知与虚拟对象定位:计算机图形学将致力于解决空间感知和虚拟对象定位的挑战。利用深度学习、摄像头阵列等技术,实现高精度的空间感知和虚实融合,为虚拟与增强现实应用带来更自然、精确的交互方式。


再次是计算机图形学与人工智能的融合



  • 生成对抗网络(GAN)在图形生成中的应用:GAN等人工智能技术为计算机图形学带来了新的创作手段。通过训练模型生成逼真的图像和场景,计算机图形学能够更便捷地创建大量内容,并提供个性化的用户体验。

  • 计算机图形学驱动的虚拟人物与角色生成:结合计算机图形学和人工智能技术,研究人员正在努力开发高度逼真的虚拟人物和角色生成方法。这将应用于游戏、影视等领域,带来更具情感表达和交互性的虚拟角色。


最后是可视化分析与科学研究。



  • 一是大数据可视化:随着大数据时代的到来,计算机图形学在可视化分析方面扮演着关键角色。通过创新的可视化方法和交互技术,研究人员能够更深入地理解和分析庞大而复杂的数据集,揭示潜在的模式和趋势。

  • 二是科学数据可视化:计算机图形学在科学研究中的应用也日益重要。通过将科学数据转化为可视化形式,研究人员能够更直观地理解复杂的数据模式和关系,加快对科学问题的洞察和发现。这种可视化分析有助于领域如天文学、生物学、气象学等的研究进展。


工程提效


其实,过去三年我在美团的工作,至少有一半的精力是做和工程提效相关的事情。当然,也做了降本的事情,从零搭建外包团队。


就像我之前总结的文章:我在美团三年的前端工程化实践回顾 中提到那样,前端工程提效,一般会按照工具化、标准化、平台化和体系化进行演进。


相比前面的端智能和图形学,除了建设低代码平台和WebIDE有点技术难度,其他更多需要的是沟通、整合资源的能力,既要有很强的项目管理能力,又要人产品思维。


前两个方向做好了我们一般称为技术专家,而工程提效则更偏管理者,未来可以成为高管或自己创业。


总结


端智能和图形学,我在美团都尝试过进行深耕,但自己性格外向,很难坐得住。工程提效做得也一般,主要是因为需要换城市而换了部门,没有机缘继续推进,在美团很难往上走,所以只能尝试自己创业。


当前我创业的公司,也需要用到很多端智能的技术,比如我们使用yolov8实现了在web端进行智能抠图(后面计划使用segment anything大模型实现);也需要用到很多图形学的技能,比如开发3D美术馆为艺培机构招生引流,通过3D展示火箭、太阳系、空间站等,提升教学效果。


未来,我们需要招聘很多端智能及图形学方向的技术人才,欢迎大家加入。


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

从技术到生活,还是那个热烈的少年 | 2023年终总结

前言   2023最后一天了,围炉煮茶,余烟缭绕,似时间线一般回游。 工作上   今年工作上迎接了新老技术的更迭,项目的整体大版本重构,以及对整个前端的工程化梳理。 去年3.0版本才用的是Webpack4.0+Vue2.0+Element+Echarts构建的...
继续阅读 »

前言


  2023最后一天了,围炉煮茶,余烟缭绕,似时间线一般回游。


工作上


  今年工作上迎接了新老技术的更迭,项目的整体大版本重构,以及对整个前端的工程化梳理。
去年3.0版本才用的是Webpack4.0+Vue2.0+Element+Echarts构建的前端项目,今年已经采用Webpack5.0+Vue3.0+Antdesgin+AntV重构了整个项目,以及搭建起了完善的前端生态系统,全系采用V3的tsx(ts+jsx写法)开发。



  • 制定了前端开发模式、开发语言、风格一致化

  • 制定了git提交规范,代码错误提交自动校验

  • 开发了公司的npm库,统一nrm源

  • 开启了前端公共组件库

  • 微前端模式实践


不知道是不是年纪大了的缘故,感觉现在无论遇到什么问题,都感觉稀松平常、波澜不惊,以前的自己总想着又解决了一个难题,有很多想说的,想聊的。现在想去记录,反而感觉问题都太简单,似乎没什么值得记录的,可是遇到难题翻来覆去、左思右想确切经历过,只是现在云淡风轻了,可能是我变懒了,也有可能这就是“技术成长”变为“技术成熟”阶段了吧。


生活上


对于生活,我似乎想讲的更多了,我想去更多的地方、见不同的人、呼吸不同的空气、享受不一样的美食,儿时的那颗好奇心好像回来了


今年我去了很多地方,忽入人间仙境的的海外仙岛、不经人手的隐市山林、悠闲惬意的大城小镇、烟雨相陪的红色起源...下面简单的写几个近期去过的地方


第一站,苏州西山岛,国庆之后,已是深秋,虽已深秋但初入冬的西山岛仍是香气氤氲,让人惊叹不已,从未到过的人,是体会不到这里的冬天竟是那样的美。大大小小的岛屿群居在这片湖泊之上,弯曲蔓延的公路是彼此之间的纽带,一座座大大小小的桥梁是彼此紧握的友谊小手,交错分布的小镇是最美的点缀。



“舟行碧波上,人在画中游”
image.png




夕阳下的西山岛,竟给人一种长河落日圆的感觉,是如此的的凄美,让人心生惋惜,西山岛有两个世界,一个在天上一个在水里,海天之间或许就是“长久”
image.png



第二站,南通濠河风景区,这里给人的第一感觉就是大写的“惬意”,南通这座二线城市在“逃离北上广系列”肯定是有一席之地的



偷得浮生半日闲,心情半佛半神仙,
image.png




亭台楼榭,玉墙石瓦,双喜悬挂,原来树叶的绿黄之间还有红的渐变。
image.png




院内秋色关不住,枝头绿黄笑春风。原来现代的高楼大厦和古代的亭台楼阁也可以相得益彰
image.png



第三站,嘉兴南北湖&高阳山,未开发的山路格外陡峭,遍布泥沙,爬起来飞沙走石,中途休息了很多次,才抵达山顶。途中有几个十岁左右的小学生嘲笑了他姐姐一路,蹦蹦跳跳爬上爬下,好像毫不费力的样子。不禁想起了年少不知力竭,年长方知山高。



山顶的辽阔只有过往的年岁,充斥着汗流浃背的我们
image.png




下山之后的夕阳伫立在远方,云的尽头是海洋?
image.png




天高地迥,觉宇宙之无穷;兴尽悲来,识盈虚之有数。
image.png




青衫烟雨客,似是故人来
image.png



下一站,在路上......


写在最后


我是凉城a,一个前端,热爱技术也热爱生活。


与你相逢,我很开心。



作者:凉城a
来源:juejin.cn/post/7319181229520224307
收起阅读 »

告别StringUtil:使用Java 全新String API优化你的代码

前言   Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。 repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。 ...
继续阅读 »


前言


  Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。



  1. repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。

  2. isBlank():检查字符串是否为空白字符序列,即长度为 0 或仅包含空格字符的字符串。

  3. lines():返回一个流,该流由字符串按行分隔而成。

  4. strip():返回一个新的字符串,该字符串是原字符串去除前导空格和尾随空格后形成的。

  5. stripLeading():返回一个新的字符串,该字符串是原字符串去除前导空格后形成的。

  6. stripTrailing():返回一个新的字符串,该字符串是原字符串去除尾随空格后形成的。

  7. formatted(Object... args):使用指定的参数格式化字符串,并返回格式化后的字符串。

  8. translateEscapes():将 Java 转义序列转换为相应的字符,并返回转换后的字符串。

  9. transform() 方法:该方法可以将一个函数应用于字符串,并返回函数的结果。


示例


1. repeat(int count)


public class StringRepeatExample {
public static void main(String[] args) {
String str = "abc";
String repeatedStr = str.repeat(3);
System.out.println(repeatedStr);
}
}

输出结果:


abcabcabc

2. isBlank()


public class StringIsBlankExample {
public static void main(String[] args) {
String str1 = "";
String str2 = " ";
String str3 = " \t ";

System.out.println(str1.isBlank());
System.out.println(str2.isBlank());
System.out.println(str3.isBlank());
}
}

输出结果:


true
true
true

3. lines()


import java.util.stream.Stream;

public class StringLinesExample {
public static void main(String[] args) {
String str = "Hello\nWorld\nJava";
Stream<String> lines = str.lines();
lines.forEach(System.out::println);
}
}

输出结果:


Hello
World
Java

4. strip()


public class StringStripExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.strip());
System.out.println(str2.strip());
}
}

输出结果:


abc
def

5. stripLeading()


public class StringStripLeadingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripLeading());
System.out.println(str2.stripLeading());
}
}

输出结果:


abc
def

6. stripTrailing()


public class StringStripTrailingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripTrailing());
System.out.println(str2.stripTrailing());
}
}

输出结果:


abc
def

7. formatted(Object... args)


public class StringFormattedExample {
public static void main(String[] args) {
String str = "My name is %s, I'm %d years old.";
String formattedStr = str.formatted( "John", 25);
System.out.println(formattedStr);
}
}

输出结果:


My name is John, I'm 25 years old.

8. translateEscapes()


public class StringTranslateEscapesExample {
public static void main(String[] args) {
String str = "Hello\\nWorld\\tJava";
String translatedStr = str.translateEscapes();
System.out.println(translatedStr);
}
}

输出结果:


Hello
World Java

9. transform()


public class StringTransformExample {
public static void main(String[] args) {
String str = "hello world";
String result = str.transform(i -> i + "!");
System.out.println(result);
}
}

输出结果:


hello world!

结尾


  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。


  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!


作者:不一样的科技宅
来源:juejin.cn/post/7222996459833770021
收起阅读 »

不吹不黑,辩证看待开发者是否需要入坑鸿蒙

前言 自打华为2019年发布鸿蒙操作系统以来,网上各种声音百家争鸣。尤其是2023年发布会公布的鸿蒙4.0宣称不再支持Android,更激烈的讨论随之而来。 本文没有宏大的叙事,只有基于现实的考量。 通过本文,你将了解到: HarmonyOS与OpenHa...
继续阅读 »

前言


自打华为2019年发布鸿蒙操作系统以来,网上各种声音百家争鸣。尤其是2023年发布会公布的鸿蒙4.0宣称不再支持Android,更激烈的讨论随之而来。

本文没有宏大的叙事,只有基于现实的考量。

通过本文,你将了解到:




  1. HarmonyOS与OpenHarmony区别

  2. 华为手机的市场占有率

  3. HarmonyOS的市场占有率

  4. 移动开发现状

  5. 鸿蒙开发优劣势

  6. 到底需不需要入坑?



1. HarmonyOS与OpenHarmony区别


HarmonyOS


移动操作系统历史


当下移动端两大巨无霸操作系统瓜分了绝大部分市场:



image.png


iOS是闭源的,只有唯一的一家厂商:Apple。

Google开放了Android基础的能力,这些能力集构成了:Android Open Source Project(简称AOSP),这块是开源免费的,任何人/公司都可以基于此进行二次开发改动。

国内各大手机厂商基于此开发出自己的系统,大浪淘沙,目前主流市场上主要手机厂商及其操作系统如下:



image.png


以上系统均衍生自AOSP,在国内使用没什么问题,若要在国外使用则需要使用Google提供的一些基础服务:统称GMS,这是需要授权的。


HarmonyOS历史与现状


华为在2019年发布了HarmonyOS 1.0 ,彼时的该系统主要应用于智慧屏、手表等设备,在2021年发布的HarmonyOS 2.0 全面应用于Android手机。

也就是这个时候华为/荣耀(未分家前)手机设备都搭载了HarmonyOS,而我们知道换了手机系统但手机上的App并没有换,照样能够正常运行。

依照华为的说法,HarmonyOS兼容Android,而部分网友认为该兼容其实就是Android套壳。

这个时候开发者无需关心鸿蒙开发,因为即使开发了Android app也能够在搭载鸿蒙系统的设备上运行。

2023年华为宣布HarmonyOS Next不再支持Android,也就是说想要在HarmonyOS Next上安装Android app是不可能的事了。

那问题就来了,作为一名Android开发者,以前只需要一套代码就可以在华为/小米/荣耀/OPPO/VIVO上运行,现在不行了,需要单独针对搭载了HarmonyOS Next的华为手机开发一个App。

若当前的App是跨端开发,如使用RN、Flutter等,那么HarmonyOS的支持力度更不可知。


OpenHarmony


从上面的描述可知,只有华为一家主推HarmonyOS,相比整个市场还是太单薄,它需要更多的厂商共同使用、共同促进新系统的发展。

因此华为将HarmonyOS的基础能力剥离出来形成了:OpenAtom OpenHarmony(简称:OpenHarmony)。

OpenHarmony是开放原子开源基金会孵化及运营的开源项目。OpenHarmony由华为公司贡献主要代码、由多家单位共建,具备面向全场景、分布式等特点,是一款全领域、新一代、开源开放的智能终端操作系统。

OpenHarmony类似于Android领域的AOSP,而HarmonyOS则是华为基于OpenHarmony开发的商业版OS。

同样的,其它厂商也可以基于OpenHarmony做改动,发布属于自己的鸿蒙商业版。
通常说的鸿蒙生态是指OpenHarmony及其衍生的商业版鸿蒙系统。

OpenHarmony源码


2. 华为手机的市场占有率


全球手机出货量



image.png


可以看出Android(80%)和iOS(20%)瓜分了天下。

图上没有华为,它被归入了Others里。

点击查看数据来源


再看另一家的统计:



image.png


华为占用约为5%。

点击查看数据来源


第三家的统计:



image.png


点击查看数据来源


虽然各家统计的数据有差异,但可以看出华为在全球手机市场份额并不高。


国内手机市场占有率



image.png


点击查看数据来源


这么看,华为在国内的占有率达到了1/4。


3. HarmonyOS的市场占有率


全球市场系统占有率


手机市场占有率并不代表都搭载了鸿蒙操作系统。

来看看各大操作系统的占有率。



image.png


点击查看数据来源


可以看出,Android和iOS设备量很多,遥遥领先。


再细分移动端的市场占有:



image.png



image.png


点击查看数据来源
同样的Android遥遥领先,此时HarmonyOS占据了3%的份额。


美国市场占有率



image.png



image.png


可以看出,在美国,Android、iOS势均力敌,唯二的存在。


印度市场占有率


再看神秘的东方大国数据:



image.png



image.png


由此可见,在印度,Android才是和咖喱最配的存在,iOS还是太耗家底了。

怪不得小米等一众国内厂商去卷印度了,市场大大滴有,就看能不能躲过印度的罚款。。。


国内鸿蒙市场占有率



image.png



image.png


国内市场里,HarmonyOS占据高达13%,毕竟国内使用华为(荣耀)手机的存量还是蛮多的。


结论:



国内才是使用鸿蒙系统的大头市场



华为官方宣称的占有率



image.png


点击查看数据来源


这里说的设备不止是智能手机,还有平板、座舱、手表等嵌入式设备。


4. 移动开发现状


iOS开发现状


iOS最先火起来的,遥想十年前,随便一个iOS开发者都能找到工作。而现在存留的iOS开发者自嘲:"Dog都不学iOS"。

以前的开发者要么转行,要么继续用"最好"的编译器(xcode)写"最优秀"的语言(OC),当然也可以用Swift,但限于系统要求,SwiftUI也没有大规模普及。

现在很少见有新鲜的血液学习iOS(也有可能iOS装备比较贵吧)了,再加上各种跨平台的框架的投入使用,原生iOS开发者的生存空间越来越小了。


Android开发现状


无独有偶,移动端的难兄难弟怎么会缺少Android呢?

一开始Android使用Java,后面全面拥抱Kotlin。

一开始画画UI,写写逻辑就能找到一份糊口的工作,现在需要去卷各种框架的底层原理,为了KPI需要去研究各种奇淫技巧的性能优化。

跨平台的框架需要去卷,KMP(已稳定)+Compose你学会了吗?RN、Flutter、Uni-app你又懂了多少?

与iOS相比Android可选择的多一些,可以选择车载等其它嵌入式设备,但多不了多少,原生Android开发者的生存空间亦不容乐观。


跨平台的开发框架移动端原生开发者可以学,前端的同学也会过来学,比如RN,Uni-app优势在前端。



行业萎缩,通常不是技术的错,技术一直在,可惜市场需求变少了



5. 鸿蒙开发优劣势


是机会还是坑?


从国内各种新闻来看:



image.png



image.png


看起来是如火如荼。


从国际的新闻看:



image.png


翻看了前几页的新闻,讨论的热度并不高,大多是搬自国内的新闻。


再说说薪资:



image.png


一看就是有夸大的成分,可能真有人达到了,但人数可能是万里挑一,只讲个例不讲普遍性没有意义。


某Boss搜一下北京的岗位:



img_v3_026m_8d70f837-9ff5-4c81-a250-6b5cf7b3198g.jpg


北京的岗位也不多,而且招的都是比较资深的,北京如此,其它城市更不用说。


鸿蒙的基建



image.png


鸿蒙目前提供提供了方舟编译器,方舟语言、IDE、模拟器等一站式开发工具,开发者可以照着官方文档编写。


根据实操的结论:




  1. 各项更新比较快,导致官方的视频/ppt和实际的有些差异

  2. 模拟器有些卡顿,有点当时Android模拟器刚出来的既视感,真机买不起

  3. 排坑的文档不多,属于摸着官方教程过河



鸿蒙官网


鸿蒙入门的简易程度



  1. 基于TS,前端开发方式,语言并不难入手

  2. IDE和Android Studio同出一源,入手比较快

  3. 声明式UI,画UI快,没接触过的同学需要熟悉一下(现在无论是Swift还是Kotlin都支持声明式UI,前端老早就用得飞起了)

  4. 不用再被graddle各种莫名错误折磨了

  5. 中文文档,对英语不好的同学体验比较好


6. 到底需不需要入坑?


对于任何一个操作系统来说,生态是第一位,鸿蒙也不例外。

横亘于鸿蒙面前的难关:




  1. 主流App是否愿意适配鸿蒙系统?

  2. 其它Android厂商是否愿意接入鸿蒙系统?

  3. 鸿蒙对开发者的支持完善与否?

  4. 鸿蒙是否真如宣传般的优秀?



不论鸿蒙是否成功,它对开发者最大的意义在于:



开辟了新的领域,开发者有机会吃到可能的"红利"



而是否入坑,取决于个人的考量,以下仅供参考:




  1. 如果贵司需要适配鸿蒙,那么只能入坑

  2. 如果对鸿蒙兴趣不足,只是觉得最近的热点有点高,未雨绸缪,想试试水,那么可以照着官方文档试试Demo

  3. 如果押宝鸿蒙,则需要深入鸿蒙的各项开发,而不仅仅只是流于表面,当然此种方式下需要花费更多的时间、精力、头发去探索、排坑

  4. 如果认为鸿蒙没有前途,那么也没必要对此冷嘲热讽,静观其变即可



那么,2024年了,你如何选择呢?


作者:小鱼人爱编程
来源:juejin.cn/post/7318561797451481129
收起阅读 »

说一说css的font-size: 0?

web
平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白! 问题描述? 是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框) 是什么原因造成...
继续阅读 »

平常我们说的font-size:0;就是设置字体大小为0对吧,但是它的用处不仅仅如此哦,它还可以消除子行内元素间额外多余的空白



问题描述?


是否出现过当多个img标签平铺的时候,会出现几个像素的间距?就像这样👇(为了醒目加了个红色的框框)


image.png


是什么原因造成的呢?


大家都知道img是行内元素,比如当我们的标签换行的时候,回车符会解析一个空白符,所以这是造成会有间距的原因之一。


当然喽,不仅仅是img,包括其他的一些常见的行内元素,比如span👇标签回车换行的效果,同样也会间隙,当然如果是缩进、空格等字符同样也会产生空白间隙,导致元素间产生多余的间距


image.png


    <span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

如何解决呢?


那我们首先想到取消换行、空格...


既然是因为标签换行了引起的,那么我们就取消换行、空格等试一试。


image.png


<span>背景图</span><span>背景图</span><span>背景图</span><span>背景图</span>
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >

证明方法还是有用的~ 那还有没有其他的方法解决呢,那这个时候可以借助font-size:0来用一用。


如何使用font-size: 0 解决呢?


利用font-size:0消除子行内元素间额外多余的空白,需要在父元素上添加font-size:0


image.png


是不是就解决了呀?


看一个完整的完整demo效果


image.png
当然需要注意一下



设置font-size: 0时,子元素必须指定一个font-size大小,否则文本内容不会显示哦



示例代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
<style>
/*************************css代码👇***********************/
ul {
margin: 20px;
display: flex;
gap: 20px;
}
.item {
width: 300px;
height: 200px;
padding: 20px;
border-radius: 10px;
background: #fff;
overflow: hidden;
font-size: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)
}
.item-img {
width: 100%;
height: 175px;
object-fit: cover;
border-radius: 5px;
}
.item-text {
color: #333;
font-size: 14px;
}
span {
background-color: red;
padding: 10px;
}
</style>
</head>
<body>


<ul>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" ><p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
<li class="item">
<img class="item-img" src="./src/assets/login-bg.png" alt="背景图" >
<p class="item-text">《好看的背景图》</p>
</li>
</ul>
</body>
</html>


作者:是小西瓜吖
来源:juejin.cn/post/7260752483055878204
收起阅读 »

Java中的一些编程经验

最近看公司项目,其中能学到很多的编程经验,正好总结学习一下 判空的处理 公司的判空处理,我看每个人都有每个人的喜好,这么多列出来,一看还真不知道这些的区别,正好今天总结学习一下😎: StrUtil.isBlank:这个一看就是处理字符串的,用于检查一个字符...
继续阅读 »

最近看公司项目,其中能学到很多的编程经验,正好总结学习一下



判空的处理


公司的判空处理,我看每个人都有每个人的喜好,这么多列出来,一看还真不知道这些的区别,正好今天总结学习一下😎:



  • StrUtil.isBlank:这个一看就是处理字符串的,用于检查一个字符串是否为 null、空字符串("")或者只包含空白字符(如空格、制表符、换行符等),注意只包含空格也会别认定为false

  • Objects.nonNull:它是 java.util.Objects 类的一部分。这个方法用于检查一个对象是否不为 null

  • ObjectUtil.isNull:这个也是检查对象是否为空的

  • CollUtil.isNotEmptyCollUtil.isNotEmpty 方法用于检查一个集合是否非空,即集合中至少包含一个元素,这个主要来检查集合的


这么总结一看,发现挺好区分的,字符串和集合都有对应的处理类,然后对象判空的话,两个都可以,看个人喜好了😁😁😁


异步的使用



看公司代码中调用异步任务的时候,使用了自己不熟悉的类,正好来学习总结一下



先学概念


CompletableFuture概念:是 Java 8 中引入的一个类,它是 java.util.concurrent 包的一部分,用于简化异步编程模型。CompletableFuture 提供了一种更加直观的方式来处理异步操作的结果,以及在异步操作完成后执行后续操作。


说人话😭:就是java.util.concurrent 包下的一个用来异步编程的一个类


核心知识



  • 链式调用:支持链式调用,这意味着你可以在异步任务完成后执行其他操作,如处理结果、执行新的异步任务等

  • 线程安全CompletableFuture 的操作是线程安全的,这意味着你可以在多线程环境中安全地使用它。

  • 异步回调CompletableFuture 可以通过 thenAcceptthenRun 方法来定义异步任务完成后的回调操作。


写个Demo


CompletableFuture 的使用示例:


创建一个异步任务:


 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
     // 异步执行的代码
     return "Hello, World!";
 });

分析:代码中创建一个异步任务,该任务会执行一个 Supplier 函数式接口的实现,这个实现返回一个字符串 "Hello, World!"。supplyAsync 方法会启动一个新的线程来执行这个任务,并且返回一个 CompletableFuture<String> 对象,这个对象代表了异步任务的执行结果。


OK,结束


别走😭,来都来了,多学点:


试试链式调用:


 future.thenApply(s -> s.toUpperCase())
      .thenAccept(System.out::println)
      .exceptionally(e -> {
           System.err.println("An error occurred: " + e.getMessage());
           return null;
      });

分析thenApply 方法用于指定一个函数,这个函数将异步任务的结果作为输入,并返回一个新的结果。在这个例子中,它将字符串转换为大写。


thenAccept 方法用于指定一个消费者函数,这个函数接受 thenApply 方法的结果,并执行某些操作(在这个例子中是打印字符串)。


exceptionally 方法用于指定一个异常处理器,如果前面的操作(thenApplythenAccept)抛出异常,这个处理器会被调用,打印错误信息。


调用结果


 try {
     String result = future.get();
     System.out.println("Result: " + result);
 } catch (InterruptedException | ExecutionException e) {
     e.printStackTrace();
 }

分析:future.get() 方法用于获取异步任务的结果。这个方法会阻塞当前线程,直到异步任务完成。


如果任务被中断或者执行过程中抛出异常,get 方法会抛出 InterruptedExceptionExecutionException


工程实践


学完了一些基本的,看一下公司代码是怎么写的😶‍🌫️:


         CompletableFuture.runAsync(() -> {
             Thread thread = new Thread(uuid) {
                 @Override
                 public void run() {
 ​
                     try {
                         taskContentInfoData(params, uuid, finalInputStream, insertPercent, flDtoList);
                    } catch (Exception exception) {
                         writeJobStatus(uuid, JobStatusEnum.FAIL.getStatus(), null);
                         log.info("错误信息{}", exception);
                         CommonConstants.threadPoolMap.remove(uuid);
                    }
                }
            };
             thread.start();
        });

也很简单,就是 CompletableFuture.runAsync来异步执行一个 Runnable 对象


分析:公司这里处理的也能达到异步的效果,这个实现的run方法里面,又开启了一个线程,主要是为了设置这个线程的唯一标识,所以有点绕。


顺便复习一下创建线程的几种方式:继承Thread类、实现Runnable接口,线程池创建


其中也可以直接创建Thread类来重写其run方法来创建🙌🙌🙌


作者:CoderLiz
来源:juejin.cn/post/7317325051476525093
收起阅读 »

如何写一个redis蜜罐

写在前面 蜜罐就是一种通过模拟真实环境来诱导入侵者的一种技术。通过它可以拖延黑客入侵时间,递给黑客用于取证假数据,溯源黑客等。通过控制平台去操作部署的仿真环境实现高效的诱捕。 之前写过一个简单的仿真redis蜜罐,简单介绍一下。 RESP 搭建这种组件的仿真环...
继续阅读 »

写在前面


蜜罐就是一种通过模拟真实环境来诱导入侵者的一种技术。通过它可以拖延黑客入侵时间,递给黑客用于取证假数据,溯源黑客等。通过控制平台去操作部署的仿真环境实现高效的诱捕。


之前写过一个简单的仿真redis蜜罐,简单介绍一下。


RESP


搭建这种组件的仿真环境,要么用真实的程序,要么就自己实现一套虚假的程序。假如自己实现的话,最关键的就是协议,对于redis来说,它的通信协议相对简单,就是resp,协议格式如下:



  • 单行字符串(Simple Strings): 响应的首字节是 "+"。例如:"+OK\r\n"。

  • 错误(Errors): 响应的首字节是 "-"。例如:"-ERROR message\r\n"。

  • 整型(Integers): 响应的首字节是 ":"。例如:":0\r\n"。

  • 多行字符串(Bulk Strings): 响应的首字节是"",后面跟字符长度,然后跟字符。例如:"",后面跟字符长度,然后跟字符。例如:"6\r\nfoobar\r\n"

  • 数组(Arrays): 响应的首字节是 "*"。例如:"*2\r\n3\nfoo˚\n˚3\r\nfoo\r\n3\r\nbar\r\n"


这就是它的通信协议,相当简单,比如我们想实现一个简单的get key操作,那么协议对应的字符格式为


*2\r\n$3\r\nget\r\n$3\r\nkey\r\n

然后传送给服务端就行。


Redis蜜罐


对于蜜罐,我们只需要实现服务端即可,客户端就用redis-cli这个工具就行。废话不多说,直接贴一下项目github.com/SSRemex/sil…



很简单,server.py实现了一个socket服务,同时加了日志收集功能。


而resp.py则实现了命令解析、命令执行、结果格式处理等操作


命令解析和结果格式主要是协议的解析和封装,这里主要想说一下两点,就是实现了一部分redis的命令


...
class RespHandler:
def __init__(self):
# 用来临时存储数据的字典
self.k_v_dict = {
"admin": "12345"
}

self.executable_command = {
"ping": (self.ping, True),
"get": (self.get, True),
"set": (self.set, True),
"keys": (self.keys, True),
"auth": (self.auth, True),
"del": (self.delete, True),
"exists": (self.exists, True),
"dbsize": (self.dbsize, True),
"config": (self.config, True)

}
self.unexecutable_command = [
"hget", "hset", "hdel", "hlen", "hexists", "hkeys", "hvals", "hgetall", "hincrby", "hincrbyfloat",
"hstrlen", "shutdown", "expire", "expireat", "pexpire", "pexpireat", "ttl", "type", "rename", "renamenx",
"randomkey", "move", "dump", "restore", "migrate", "scan", "select", "flushdb", "flushall", "mset", "mget",
"incr", "decr", "append", "strlen", "getset", "setrange", "getrange", "rpush", "lpush", "linsert", "lrange",
"lindex", "llen", "rpop", "lpop", "lrem", "lset", "blpop",

]
...

这里我内定了一些命令,并实现了他们的功能,让它像真的redis一样,你甚至可以进行kv操作,同时为了真实性,设定了一堆不可执行的命令,调用时会返回redis的报错,就像在配置文件里面禁用了这些命令一样。


演示


服务端执行,默认运行在3998端口



redis-cli连接



可以发现成功连接



此时服务端这边也接收到了,并且生成了日志



接下来,我们在redis-cli执行一些命令



很完美,甚至可以进行set get操作。


最后再次附上项目地址:github.com/SSRemex/sil…


作者:银空飞羽
来源:juejin.cn/post/7316783747491086371
收起阅读 »

90%的Java开发人员都会犯的5个错误

前言 作为一名java开发程序员,不知道大家有没有遇到过一些匪夷所思的bug。这些错误通常需要您几个小时才能解决。当你找到它们的时候,你可能会默默地骂自己是个傻瓜。是的,这些可笑的bug基本上都是你忽略了一些基础知识造成的。其实都是很低级的错误。今天,我总结一...
继续阅读 »



前言


作为一名java开发程序员,不知道大家有没有遇到过一些匪夷所思的bug。这些错误通常需要您几个小时才能解决。当你找到它们的时候,你可能会默默地骂自己是个傻瓜。是的,这些可笑的bug基本上都是你忽略了一些基础知识造成的。其实都是很低级的错误。今天,我总结一些常见的编码错误,然后给出解决方案。希望大家在日常编码中能够避免这样的问题。


1. 使用Objects.equals比较对象


这种方法相信大家并不陌生,甚至很多人都经常使用。是JDK7提供的一种方法,可以快速实现对象的比较,有效避免烦人的空指针检查。但是这种方法很容易用错,例如:


Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false

为什么替换==Objects.equals()会导致不同的结果?这是因为使用==编译器会得到封装类型对应的基本数据类型longValue,然后与这个基本数据类型进行比较,相当于编译器会自动将常量转换为比较基本数据类型, 而不是包装类型。


使用该Objects.equals()方法后,编译器默认常量的基本数据类型为int。下面是源码Objects.equals(),其中a.equals(b)使用的是Long.equals()会判断对象类型,因为编译器已经认为常量是int类型,所以比较结果一定是false


public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}

知道了原因,解决方法就很简单了。直接声明常量的数据类型,如Objects.equals(longValue,123L)。其实如果逻辑严密,就不会出现上面的问题。我们需要做的是保持良好的编码习惯。


2. 日期格式错误


在我们日常的开发中,经常需要对日期进行格式化,但是很多人使用的格式不对,导致出现意想不到的情况。请看下面的例子。


Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));//2022-12-31 08:00:00

以上用于YYYY-MM-dd格式化, 年从2021 变成了 2022。为什么?这是因为 javaDateTimeFormatter 模式YYYYyyyy之间存在细微的差异。它们都代表一年,但是yyyy代表日历年,而YYYY代表星期。这是一个细微的差异,仅会导致一年左右的变更问题,因此您的代码本可以一直正常运行,而仅在新的一年中引发问题。12月31日按周计算的年份是2022年,正确的方式应该是使用yyyy-MM-dd格式化日期。


这个bug特别隐蔽。这在平时不会有问题。它只会在新的一年到来时触发。我公司就因为这个bug造成了生产事故。


3. 在 ThreadPool 中使用 ThreadLocal


如果创建一个ThreadLocal 变量,访问该变量的线程将创建一个线程局部变量。合理使用ThreadLocal可以避免线程安全问题。


但是,如果在线程池中使用ThreadLocal ,就要小心了。您的代码可能会产生意想不到的结果。举个很简单的例子,假设我们有一个电商平台,用户购买商品后需要发邮件确认。


private ThreadLocal currentUser = ThreadLocal.withInitial(() -> null);

private ExecutorService executorService = Executors.newFixedThreadPool(4);

public void executor() {
executorService.submit(()->{
User user = currentUser.get();
Integer userId = user.getId();
sendEmail(userId);
});
}

如果我们使用ThreadLocal来保存用户信息,这里就会有一个隐藏的bug。因为使用了线程池,线程是可以复用的,所以在使用ThreadLocal获取用户信息的时候,很可能会误获取到别人的信息。您可以使用会话来解决这个问题。


4. 使用HashSet去除重复数据


在编码的时候,我们经常会有去重的需求。一想到去重,很多人首先想到的就是用HashSet去重。但是,不小心使用 HashSet 可能会导致去重失败。


User user1 = new User();
user1.setUsername("test");

User user2 = new User();
user2.setUsername("test");

List users = Arrays.asList(user1, user2);
HashSet sets = new HashSet<>(users);
System.out.println(sets.size());// the size is 2

细心的读者应该已经猜到失败的原因了。HashSet使用hashcode对哈希表进行寻址,使用equals方法判断对象是否相等。如果自定义对象没有重写hashcode方法和equals方法,则默认使用父对象的hashcode方法和equals方法。所以HashSet会认为这是两个不同的对象,所以导致去重失败。


5. 线程池中的异常被吃掉


ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
//do something
double result = 10/0;
});

上面的代码模拟了一个线程池抛出异常的场景。我们真正的业务代码要处理各种可能出现的情况,所以很有可能因为某些特定的原因而触发RuntimeException


但是如果没有特殊处理,这个异常就会被线程池吃掉。这样就会导出出现问题你都不知道,这是很严重的后果。因此,最好在线程池中try catch捕获异常。


总结


本文总结了在开发过程中很容易犯的5个错误,希望大家养成良好的编码习惯。


作者:JAVA旭阳
来源:juejin.cn/post/7182184496517611576
收起阅读 »

一个左侧导航栏的选中状态把我迷得颠三倒四

web
事情是这样的 👇 前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EAS...
继续阅读 »

事情是这样的 👇


前段时间我用arco.design这个组件库默认的模板项目开放,刚开始需求是这样的:总共八个页面,其中四个显示在导航栏,四个不在导航栏显示,可以导航的页面里又有俩个需要登录才能访问,俩个不登录就能访问。听着是有些绕不过开发起来不是SO EASY吗😎


我用的是vue版本的,说白了就一个知识点——路由和菜单。凭借我的聪明才智,肯定一看就会。


路由


首先,需要先了解一下路由表的配置。基本的路由配置请参阅 Vue-Router 官方文档


// 在本例子中,页面最终路径为 /dashboard/workplace
export default {
path: 'dashboard',
name: 'dashboard', // 路由名称
component: () => import('@/views/dashboard/index.vue'),
meta: {
locale: 'menu.dashboard',
requiresAuth: true,
icon: 'icon-dashboard',
},
children: [
{
path: 'workplace',
name: 'workplace',
component: () => import('@/views/dashboard/workplace/index.vue'),
meta: {
locale: 'menu.dashboard.workplace',
requiresAuth: true,
roles: ['admin'],
hideInMenu: false,
},
},
],
};

路由 Meta 元信息


参数名说明类型默认值
roles配置能访问该页面的角色,如果不匹配,则会被禁止访问该路由页面string[]-
requiresAuth是否需要登录鉴权booleanfalse
icon菜单配置iconstring-
locale一级菜单名(语言包键名)string-
hideInMenu是否在左侧菜单中隐藏该项boolean-
hideChildrenInMenu强制在左侧菜单中显示单项boolean-
activeMenu高亮设置的菜单项string-
order排序路由菜单项。如果设置该值,值越高,越靠前number-
noAffix如果设置为true,标签将不会添加到tab-bar中boolean-
ignoreCache如果设置为true页面将不会被缓存boolean-

hideInMenu 控制菜单显示, requiresAuth 控制是否需要登录,没错,以上知识点完全够我用了🤏🤏🤏🤏


三下五除二就开发完成了,正当我沉浸在成功的喜悦时,测试给我提了个bug。


说导航栏目切换,选中状态有俩个正常,俩个不正常,切换页面导航没选中,再点一次才会选中。我擦👏👏👏👏👏,我傻眼了😧😧😧


到底哪里出了问题,我哪里写错了呢,人家官方肯定没问题,于是我开始寻找。是路由名称重复了,还是那个组件写的有问题,还好有俩个正常的,我仔细比对一下不就好了吗,我可真是个小机灵鬼。


就这样,我又一次为我的自大付出了汗水,对比了一天,我感觉好像真不是我写的有问题,不过还是有收获的,我发现requiresAuth 设置为true的导航正常,requiresAuth设置为false的不正常。抱着怀疑的态度我开始找原来模板项目中处理requiresAuth的代码。最后在components》menu》index.vue文件下发现一个方法:


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if (requiresAuth && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

这... ... 我人麻了,这不是只有requiresAuth 为true的时候才会有效吗?他为啥这么写呢?还是我复制项目的时候复制错了?然后我改成了


listenerRouteChange((newRoute) => {
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
if ((requiresAuth === true || requiresAuth === false) && (!hideInMenu || activeMenu)) {
const menuOpenKeys = findMenuOpenKeys(
(activeMenu || newRoute.name) as string
);

const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
openKeys.value = [...keySet];

selectedKey.value = [
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
];
}
}, true);

🙈🙈🙈🙈🙈看出来我改哪里了吗,考考你们的眼力,我改的方法是不是很nice。
反正好使了,下班!!!!


作者:一路向北京
来源:juejin.cn/post/7317277887567151145
收起阅读 »

【日常总结】解决el-select数据量过大的3种方法

web
背景 最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。 想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,...
继续阅读 »

背景


最近做完一个小的后台管理系统,快上线了,发现一个问题,有2个select的选项框线上的数据量是1w+。。而测试环境都是几百的,所以导致页面直接卡住了,over了。


image.png


想了一下,如果接口支持搜索和分页,那直接通过上拉加载就可以了。但后端不太愿意改😄。行吧,前端搞也是可以的。


这个故事还有个后续


image.png


过了一周上线后,发现有一个下拉框的数据有30w+!!!加载都加载不出来,哈哈哈哈,接口直接超时报错了,所以又cuocuocuo改了一遍,最后改成了:



  1. 接口翻页请求

  2. 前端使用自定义指令实现上拉加载更多,搜索直接走的后端接口


方案


通过一顿搜索加联想总结了3种方法,以下方法都需要支持开启filterable支持搜索。


标题具体问题
方案1只展示前100条数据,这个的话配合filter-method每次只返回前100条数据。限制展示的条数可能不全,搜索需要多搜索点内容
方案2分页方式,通过指令实现上拉加载,不断上拉数据展示数据。仅过滤加载出来的数据,需要配合filterMethod过滤数据
方案3options列表采用虚拟列表实现。成本高,需要引入虚拟列表组件或者自己手写。经掘友指点,发现element-plus提供了对应的实现,如果是plus,则可以直接使用select-v2

方案一(青铜段位) filterMethod直接过滤数据量


<template>
<el-select
v-model="value"
clearable filterable
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options.slice(0, 100)"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>

</template>
export default {
name: 'Demo',
data() {
return {
options: [],
value: ''
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
for (let i = 0; i < 25000; i++) {
this.options.push({label: "选择"+i,value:"选择"+i});
}
},
filterMethod(val) {
console.log('filterMethod', val);
this.options = this.options.filter(item => item.value.indexOf(val) > -1).slice(0, 100);
},
visibleChange() {
console.log('visibleChange');
}
}
}

方案二(白银段位) 自定义滚动指令,实现翻页加载


写自定义滚动指令,options列表滚动到底部后,再加载下一页。但这时候筛选出来的是已经滚动出来的值。


这里如果直接使用filterable来搜索,搜索出来的内容是已经滑动出来的内容。如果想筛选全部的,就需要重写filterMethod方法来自定义过滤功能。可以根据情况选择是否要重写filterMethod。


image.png


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
:filter-method="filterMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>
<script>
export default {
name: 'Demo',
data() {
return {
options: [],
value: '',
pageNo: 0
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
},
filterMethod(val) {
this.data = val ? this.allData.filter(item => item.label.indexOf(val) > -1) : this.allData;
this.getPageList();
}
},
directives:{
'el-select-loadmore':(el, binding) => {
// 获取element-ui定义好的scroll父元素
const wrapEl = el.querySelector(".el-select-dropdown .el-select-dropdown__wrap");
if(wrapEl){
wrapEl.addEventListener("scroll", function () {
/**
* scrollHeight 获取元素内容高度(只读)
* scrollTop 获取或者设置元素的偏移值,
* 常用于:计算滚动条的位置, 当一个元素的容器没有产生垂直方向的滚动条, 那它的scrollTop的值默认为0.
* clientHeight 读取元素的可见高度(只读)
* 如果元素滚动到底, 下面等式返回true, 没有则返回false:
* ele.scrollHeight - ele.scrollTop === ele.clientHeight;
*/

if (this.scrollTop + this.clientHeight >= this.scrollHeight) {
// binding的value就是绑定的loadmore函数
binding.value();
}
});
}
},
},
}
</script>

</script>

方案三(黄金段位) 虚拟列表


引入社区的vue-virtual-scroll-list 支持虚拟列表。但这里想的自己再实现一遍虚拟列表,后续再写吧。


另外,element-plus提供了对应的实现,如果是使用的是plus,则可以直接使用 select-v2组件


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable >

<virtual-list
class="list"
style="height: 360px; overflow-y: auto;"
:data-key="'value'"
:data-sources="data"
:data-component="item"
:estimate-size="50"
/>

</el-select>
</div>

</template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item';
export default {
name: 'Demo',
components: {VirtualList, Item},
data() {
return {
options: [],
data: [],
value: '',
pageNo: 0,
item: Item,
}
},
beforeMount() {
this.getList();
},
methods: {
// 模拟获取大量数据
getList() {
const data = [];
for (let i = 0; i < 25000; i++) {
data.push({label: "选择"+i,value:"选择"+i});
}
this.allData = data;
this.data = data;
this.getPageList()
},
getPageList(pageSize = 10) {
this.pageNo++;
const list = this.data.slice(0, pageSize * (this.pageNo));
this.options = list;
},
loadMore() {
this.getPageList();
}
}
}
</script>


// item组件
<template>
<el-option :label="source.label" :value="source.value"></el-option>
</template>


<script>
export default {
name: 'item',
props: {
source: {
type: Object,
default() {
return {}
}
}
}
}
</script>


<style scoped>
</style>



总结


最后我们项目中使用的虚拟列表,为啥,因为忽然发现组件库支持select是虚拟列表,那就直接使用这个啦。


最后的最后


没有用虚拟列表,因为接口数据量过大(你见过返回30w+的接口吗🙄。。),后端接口改成分页,前端支持自定义指令上拉加载,引用的参数增加了remote、remote-method设置为远端的方法。


<template>
<div class="dashboard-editor-container">
<el-select
v-model="value"
clearable
filterable
v-el-select-loadmore="loadMore"
remote
:remote-method="remoteMethod">

<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">

</el-option>
</el-select>
</div>

</template>

参考文章:



作者:searchop
来源:juejin.cn/post/7278238985448341544
收起阅读 »

为啥TextureView比SurfaceView表现还差呢?

从原理上面讲,我们大众的认知就是TextureView比SurfaceView的性能要好。硬的比软的好。但是其实这种是片面的。最近就遇到一个奇怪的现象:在3399上面通过ffmpeg拉rtsp流,然后通过mediacodec解码后渲染。渲染到TextureVi...
继续阅读 »

从原理上面讲,我们大众的认知就是TextureView比SurfaceView的性能要好。硬的比软的好。但是其实这种是片面的。最近就遇到一个奇怪的现象:在3399上面通过ffmpeg拉rtsp流,然后通过mediacodec解码后渲染。渲染到TextureView上会比较频繁的出现马赛克的现象。但是换用SurfaceView立马就变好了。


TextureView 和 SurfaceView 都有各自的优势和局限性,所以它们的性能表现也会因应用的具体需求和使用场景而异。
在某些情况下,TextureView 的性能可能会比 SurfaceView 差,原因可能有以下几点:



  1. 渲染管道的差异:TextureView 是基于 OpenGL ES 的,它使用图形渲染管道来渲染内容。而 SurfaceView 则使用传统的 Android 渲染管道,这与 Android 的视图系统更加紧密集成。在某些情况下,这可能会导致 SurfaceView 的性能更好。

  2. 线程管理:SurfaceView 使用一个独立的线程来渲染内容,这可以提供更平滑的渲染性能,尤其是在处理复杂动画或游戏时。而 TextureView 则在主线程上渲染内容,这可能会导致性能下降,尤其是在处理大量数据或复杂渲染时。

  3. 硬件加速:虽然 TextureView 支持硬件加速,但在某些情况下,硬件加速可能会导致性能问题,尤其是在低端设备上。SurfaceView 则更多地依赖于软件渲染,这可能在某些情况下会提供更稳定的性能。


需要注意的是,性能差异可能会因设备和应用而异,因此在实际开发中应该根据具体需求和性能测试结果来选择合适的视图。无论选择哪种视图,都应该优化代码以提高性能,并确保在不同设备上进行充分的测试。


于是,我针对上面的3点的结论做了一个实验,在3399上面ffmpeg硬解码居然比软解码帧率要低。看来3399的CPU性能比其他硬件确实要抢。这就证明了标题中的疑惑了。


下面贴出一段出马赛克的代码,换上SurfaceView就好了。


public class IPCameraPreviewFragment extends Fragment implements TextureView.SurfaceTextureListener{

public static final String TAG = "IPCameraPreviewFragment";
public static final boolean DEBUG = true;

private TextureView mPreview;
private SurfaceTexture mSurfaceTexture;
private Handler mUiHandler = new Handler();
private Runnable mRunnable = new Runnable() {

@Override
public void run() {
if(mPreview == null || mSurfaceTexture == null) return;
Play.getInstances().startPreivew(new Surface(mSurfaceTexture));
}
};
private IErrorCallback mErrorCallback = new IErrorCallback() {

@Override
public void onError(int error) {
Log.d(TAG, "onError = " + error);
if(null == mUiHandler || null == mRunnable) return;
mUiHandler.removeCallbacks(mRunnable);
mUiHandler.postDelayed(mRunnable, 5000);
}
};

public void setDataSource(String source){
Play.getInstances().setErrorCallback(mErrorCallback);
Play.getInstances().setDataSource(source);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGr0up container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.preview_fragment, container,false);
mPreview = (TextureView)view.findViewById(R.id.preview);
mPreview.setSurfaceTextureListener(this);
return view;
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
int height)
{
mSurfaceTexture = surface;
Play.getInstances().startPreivew(new Surface(surface));
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
int height)
{

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mSurfaceTexture = null;
Play.getInstances().releaseMediaPlay();
if(null == mUiHandler || null == mRunnable) return false;
mUiHandler.removeCallbacks(mRunnable);
mUiHandler = null;
mRunnable = null;
return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

}

作者:逗比先生
来源:juejin.cn/post/7316592817341218866
收起阅读 »

App防止恶意截屏功能的方法:iOS、Android和鸿蒙系统的实现方案

防止应用被截图是一个比较常见的需求,主要是出于安全考虑。下面将分别为iOS(苹果系统)、Android(安卓系统)及HarmonyOS(鸿蒙系统)提供防止截屏的方法和示例代码。在企业内部使用的应用中,防止员工恶意截屏是一个重要的安全需求。本文将详细介绍iOS、...
继续阅读 »

防止应用被截图是一个比较常见的需求,主要是出于安全考虑。下面将分别为iOS(苹果系统)、Android(安卓系统)及HarmonyOS(鸿蒙系统)提供防止截屏的方法和示例代码。

123456 (161).png

在企业内部使用的应用中,防止员工恶意截屏是一个重要的安全需求。本文将详细介绍iOS、Android和鸿蒙系统的防止截屏的方法,并提供相应的代码示例,以帮助代码初学者理解和实现该功能。

iOS系统防止截屏方法:

在iOS系统中,可以通过设置UIWindow的windowLevel为UIWindowLevelNormal + 1,使应用窗口覆盖在截屏窗口之上,从而阻止截屏。以下是Objective-C和Swift两种语言的代码示例:

  1. iOS系统防止截屏

在iOS中,可以使用UIScreen的isCaptured属性来检测屏幕是否被录制或截图。为了防止截屏,你可以监听UIScreenCapturedDidChange通知,当屏幕开始被捕获时,你可以做一些操作,比如模糊视图或显示一个全屏的安全警告。

swift

// 注册屏幕捕获变化通知
NotificationCenter.default.addObserver(
    self,
    selector: #selector(screenCaptureChanged),
    name: UIScreen.capturedDidChangeNotification,
    object: nil
)
@objc func screenCaptureChanged(notificationNSNotification) {
    if UIScreen.main.isCaptured {
        // 屏幕正在被捕获,可以在这里做一些隐藏内容的操作,比如
        // 显示一个覆盖所有内容的视图
    } else {
        // 屏幕没有被捕获,可以移除那个覆盖的视图
    }
}

但需要注意的是,iOS不允许应用程序完全禁止截屏。因为截图功能是系统级别的,而不是应用级别的,上述代码只能做到在截图时采取一定的响应措施,不能完全防止。

  1. Android系统防止截屏

在Android中,可以通过设置Window的属性来防止用户截图或录屏。这通过禁用FLAG_SECURE来实现。

java

// 在Activity中设置禁止截屏
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 在setContentView之前调用
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
                         WindowManager.LayoutParams.FLAG_SECURE);
    setContentView(R.layout.activity_main);
}

这样设置后,当前的Activity将无法被截屏或录屏。

  1. HarmonyOS(鸿蒙系统)防止截屏

HarmonyOS是华为开发的一个分布式操作系统,目前它在应用开发中有着与Android类似的API。因此可以使用与Android相同的方法进行禁止截屏。

java

// 在Ability(Activity)中设置禁止截屏
@Override
protected void onStart(Intent intent) {
    super.onStart(intent);
    // 在setUIContent之前调用
    getWindow().addFlags(WindowManager.LayoutConfig.FLAG_SECURE);
    setUIContent(ResourceTable.Layout_ability_main);
}

在HarmonyOS中,Ability相当于Android中的Activity。

请注意尽管上述方法能够有效地防止绝大多数截屏和录屏行为,但技术上并不是100%无法绕过的(例如某些root设备或具有特殊权限的应用可能可以绕过这些限制)。因此,在处理非常敏感的信息时,请综合其他安全措施一起使用,比如数据加密、用户行为分析等。


作者:咕噜分发企业签名梦奇
来源:juejin.cn/post/7317095140040376346
收起阅读 »

如何优雅的将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.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>version</version>
<scope>test</scope>
</dependency>

通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。


multi.png



更多文章干货,推荐公众号【程序员老J】



作者:程序员老J
来源:juejin.cn/post/7295559402475667492
收起阅读 »

从 Vue 2 迁移到 Svelte

web
大家好,这里是大家的林语冰。 本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。 在使用 Vue 2 作为我们的前端框架近 2 ...
继续阅读 »

大家好,这里是大家的林语冰。


本周之后,Vue 2 将停止开源维护。所以本期《前端翻译计划》共享的是某企业去年从 Vue 2 迁移到 Svelte 的现实测评,以及 Vue 3 和 Svelte 的“零和博弈”。


在使用 Vue 2 作为我们的前端框架近 2 年后,我们宣布此支持将不再维护,因此我们决定迁移到新框架。但该选谁呢:Vue 3 还是 Svelte 呢


粉丝请注意,我们迁移后的目标也是改善 DX(开发体验),尤其是类型检查、性能和构建时间。我们没有考虑 React,因为它需要投资一大坨时间成本来学习,而且与 Vue 和 Svelte 不同,它没有提供开箱即用的解决方案。此外,后者共享相同的 SFC(单文件组件)概念:同一文件中的逻辑(JS)、结构(HTML)和样式(CSS)。


Svelte vs Vue 3


Svelte 的留存率更高。对于我们的新前端,我们必须从市场上可用的 2 个框架权衡,即 Svelte 和 Vue 3。下面是过去 5 年不同框架留存率的图示(留存率 = 会再次使用/(会再次使用 + 不会再次使用))。JS 现状调查汇编了该领域开发者的数据,如你所见,Svelte 排名第二,而 Vue 3 排名第四。


01-state.jpg


这启示我们,过去使用过 Svelte 的开发者愿意再次使用它的数量比不愿使用它的要多。


Svelte 的类型体验更棒


Vue 2Vue 3Svelte
组件级类型YesYesNo
跨组件类型NoYesYes
类型事件NoNoYes

Svelte 通过更简单的组件设计流程和内置类型事件提供了更好的类型体验,对我们而言十分用户友好。


全局变量访问限制。使用 Svelte,可以从其他文件导入枚举,并在模板中使用它们,而 Vue 3 则达咩。


02-benchmark.png


语法。就我个人而言,私以为 Svelte 语法比 Vue 更优雅和用户友好。您可以瞄一下下面的代码块,并亲自查看它们。


Svelte:


03-svelte.png


Vue:


04-vue.png


没有额外的 HTML div <template>。在 Svelte 中您可以直接编写自己的 HTML。


样式在 Svelte 中会自动确定作用域,这对于可维护性而言是一个优点,且有助于避免 CSS 副作用。每个组件的样式独立,能且仅能影响该组件,而不影响其父/子组件。


更新数据无需计算属性。在 Svelte 中,感觉更像在用纯 JS 撸码。您只需要专注于编写一个箭头函数:


const reset = () => {
firstName = ''
lastName = ''
}

Svelte 中只需单个括号:


//Svelte
{fullName}

//Vue
{{fullName}}

添加纯 JS 插件更简单。此乃使用 Svelte 和 Prism.js 的语法高亮集成用例,如下所示:


05-prism.png


无需虚拟 DOM 即可编译代码。Svelte 和 Vue 之间的主要区别是,减少了浏览器和 App 之间的层数,实现更优化、更快的任务成果。


自动更新。诉诸声明变量的辅助,Svelte 可以自动更新您的数据。这样,您就不必等待变更反映在虚拟结构中,获得更棒的 UX(用户体验)。


Svelte 也有短板


理所当然,Svelte 也有短板,比如社区相对较小,因为它是 2019 年才诞生的。但随着越来越多的开发者可能会认识到其质量和用户友好的内容,支持以及社区未来可能会不断发展壮大。


因此,在审查了此分析的结果后,尽管 SvelteKit 在迁移时仍处于积极开发阶段,我们决定使用 Svelte 和 Svelte Kit 砥砺前行。


06cons.png


如何处理迁移呢?


时间:我们选择在 8 月份处理迁移,当时该 App 用户较少。


时间长度:我们花了 2 周时间将所有文件从 Vue 迁移到 Svelte。


开发者数量:2 名前端开发者全职打工 2 周,另一名开发者全职打工 1 周,因此涉及 3 名开发人员。


工作流:首先,我们使用 Notion 工具将我们的凭据归属于团队的开发者。然后,我们开始在 Storybook 中创建新组件,最后,每个开发者都会奖励若干需要在 Svelte 中重写的页面。


作为一家初创公司,迁移更简单,因为我们没有 1_000 个文件需要重写,因此我们可以快速执行。虽然但是,当 SvelteKit 仍处于积极开发阶段时,我们就冒着风险开始迁移到 SvelteKit,这导致我们在迁移仅 1 个月后就不得不做出破坏性更新。但 SvelteKit 专业且博大精深的团队为我们提供了一个命令(npx svelte-migrate routes),以及一个解释清晰的迁移指南,真正帮助我们快速适应新的更新。


此外,9 月份,SvelteKit 团队宣布该框架终于进入候选版本阶段,这意味着,它的稳定性现在得到了保证!


文件和组件组织


SvelteKit 的“文件夹筑基路由”给我们带来了很多。我们可以将页面拆分为子页面,复用标准变量名,比如 loading/submit 等等。此外,布局直接集成到相关路由中,由于树内组织的增加,访问起来更简单。


那么我们得到了什么?


除了上述好处之外,还值得探讨其他某些关键因素:


性能提高且更流畅。编译完成后,我们可以体会到该 App 的轻量级。与其他框架相比,这提高了加载速度,其他框架在 App 的逻辑代码旁嵌入了“运行时”。


DX 更棒。SvelteKit 使用 Vite 打包器,此乃新一代 JS 构建工具,它利用浏览器中 ES 模块的可用性和编译为原生(compile-to-native)的打包器,为您带来最新 JS 技术的最佳 DX。


代码执行更快。它没有虚拟 DOM,因此在页面上变更时,需要执行的层数少了一层。


启动并运行 SSR(服务器端渲染)。如果最终用户没有良好的互联网连接或启用 JS,平台仍将在 SSR 的帮助下高效运行,因为用户仍能加载网页,同时失去交互性。


代码简洁易懂。Svelte 通过将逻辑(JS)、结构(HTML)和样式(CSS)分组到同一文件中,可以使用更具可读性和可维护性的面向组件的代码。黑科技在于所有这些元素都编译在 .svelte 文件中。


固定类型检查。自从我们迁移到 Svelte 以来,我们已经成功解决了类型检查的最初问题。事实上,我们以前必须处理周期性的通知,而如今时过境迁。不再出现头大的哨兵错误。(见下文)


07-error.jpg


粉丝请注意,此博客乃之前的迁移测评,其中某些基准测试见仁见智,尤大还亲自码字撰写博客布道分享,我们之后会继续翻译 Vue 官方博客详细说明。



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Migrating from Vue 2 to Svelte



您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎关注地球猫猫教。谢谢大家的点赞,掰掰~


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7317222425384714294
收起阅读 »

让SQL起飞(优化)

最近博主看完了《SQL进阶教程》这本书,看完后给博主打开了SQL世界的新大门,对于 SQL 的理解不在局限于以前的常规用法。借用其他读者的评论, 读完醍醐灌顶,对SQL做到了知其然更能知其所以然。全书从头到尾强调了 SQL的内在逻辑是基于集合论和谓词逻辑,而...
继续阅读 »

最近博主看完了《SQL进阶教程》这本书,看完后给博主打开了SQL世界的新大门,对于 SQL 的理解不在局限于以前的常规用法。借用其他读者的评论,



读完醍醐灌顶,对SQL做到了知其然更能知其所以然。全书从头到尾强调了 SQL的内在逻辑是基于集合论和谓词逻辑,而这两条主线恰恰对使用SQL起到了至关重要的指导作用。



本文给大家总结如何让SQL起飞(优化)


一、SQL写法优化


在SQL中,很多时候不同的SQL代码能够得出相同结果。从理论上来说,我们认为得到相同结果的不同SQL之间应该有相同的性能,但遗憾的是,查询优化器生成的执行计划很大程度上受到SQL代码影响,有快有慢。因此如果想优化查询性能,我们必须知道如何写出更快的SQL,才能使优化器的执行效率更高。


1.1 子查询用EXISTS代替IN


当IN的参数是子查询时,数据库首先会执行子查询,然后将结果存储在一张临时的工作表里(内联视图),然后扫描整个视图。很多情况下这种做法都非常耗费资源。使用EXISTS的话,数据库不会生成临时的工作表。但是从代码的可读性上来看,IN要比EXISTS好。使用IN时的代码看起来更加一目了然,易于理解。因此,如果确信使用IN也能快速获取结果,就没有必要非得改成EXISTS了。


这里用Class_A表和Class_B举例,

我们试着从Class_A表中查出同时存在于Class_B表中的员工。下面两条SQL语句返回的结果是一样的,但是使用EXISTS的SQL语句更快一些。


--慢
SELECT *
FROM Class_A
WHERE id IN (SELECT id
FROM Class_B);

--快
SELECT *
FROM Class_A A
WHERE EXISTS
(SELECT *
FROM Class_B B
WHERE A.id = B.id);

使用EXISTS时更快的原因有以下两个。



  1. 如果连接列(id)上建立了索引,那么查询 tb_b 时不用查实际的表,只需查索引就可以了。(同样的IN也可以使用索引,这不是重要原因)

  2. 如果使用EXISTS,那么只要查到一行数据满足条件就会终止查询,不用像使用IN时一样扫描全表。在这一点上NOT EXISTS也一样。


实际上,大部分情况在子查询数量较小的场景下EXISTS和IN的查询性能不相上下,由EXISTS查询更快第二点可知,子查询数量较大时使用EXISTS才会有明显优势。


1.2 避免排序并添加索引


在SQL语言中,除了ORDER BY子句会进行显示排序外,还有很多操作默认也会在暗中进行排序,如果排序字段没有添加索引,会导致查询性能很慢。SQL中会进行排序的代表性的运算有下面这些。



  • GR0UP BY子句

  • ORDER BY子句

  • 聚合函数(SUM、COUNT、AVG、MAX、MIN)

  • DISTINCT

  • 集合运算符(UNION、INTERSECT、EXCEPT)

  • 窗口函数(RANK、ROW_NUMBER等)


如上列出的六种运算(除了集合运算符),它们后面跟随或者指定的字段都可以添加索引,这样可以加快排序。



实际上在DISTINCT关键字、GR0UP BY子句、ORDER BY子句、聚合函数跟随的字段都添加索引,不仅能加速查询,还能加速排序。



1.3 用EXISTS代替DISTINCT


为了排除重复数据,我们可能会使用DISTINCT关键字。如1.2中所说,默认情况下,它也会进行暗中排序。如果需要对两张表的连接结果进行去重,可以考虑使用EXISTS代替DISTINCT,以避免排序。这里用Items表和SalesHistory表举例:

我们思考一下如何从上面的商品表Items中找出同时存在于销售记录表SalesHistory中的商品。简而言之,就是找出有销售记录的商品。


在一(Items)对多(SalesHistory)的场景下,我们需要对item_no去重,使用DISTINCT去重,因此SQL如下:


SELECT DISTINCT I.item_no
FROM Items I INNER JOIN SalesHistory SH
ON I. item_no = SH. item_no;

item_no
-------
10
20
30

使用EXISTS代替DISTINCT去重,SQL如下:


SELECT item_no
FROM Items I
WHERE EXISTS
(SELECT
FROM SalesHistory SH
WHERE I.item_no = SH.item_no);
item_no
-------
10
20
30

这条语句在执行过程中不会进行排序。而且使用EXISTS和使用连接一样高效。


1.4 集合运算ALL可选项


SQL中有UNION、INTERSECT、EXCEPT三个集合运算符。在默认的使用方式下,这些运算符会为了排除掉重复数据而进行排序。



MySQL还没有实现INTERSECT和EXCEPT运算



如果不在乎结果中是否有重复数据,或者事先知道不会有重复数据,请使用UNION ALL代替UNION。这样就不会进行排序了。


1.5 WHERE条件不要写在HAVING字句


例如,这里继续用SalesHistory表举例,下面两条SQL语句返回的结果是一样的:


--聚合后使用HAVING子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
GR0UP BY sale_date
HAVING sale_date = '2007-10-01';

--聚合前使用WHERE子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
WHERE sale_date = '2007-10-01'
GR0UP BY sale_date;

但是从性能上来看,第二条语句写法效率更高。原因有两个:



  1. 使用GR0UP BY子句聚合时会进行排序,如果事先通过WHERE子句筛选出一部分行,就能够减轻排序的负担。

  2. 在WHERE子句的条件里可以使用索引。HAVING子句是针对聚合后生成的视图进行筛选的,但是很多时候聚合后的视图都没有继承原表的索引结构。


二、真的用到索引了吗


2.1 隐式的类型转换


如下,col_1字段是char类型:


-- 没走索引
SELECT * FROM SomeTable WHERE col_1 = 10;
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 ='10';
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = CAST(10, AS CHAR(2));

当查询条件左边和右边类型不一致时会导致索引失效。


2.2 在索引字段上进行运算


如下:


SELECT *
FROM SomeTable
WHERE col_1 * 1.1 > 100;

在索引字段col_1上进行运算会导致索引不生效,把运算的表达式放到查询条件的右侧,就能用到索引了,像下面这样写就OK了。


WHERE col_1 > 100 / 1.1

如果无法避免在左侧进行运算,那么使用函数索引也是一种办法,但是不太推荐随意这么做。使用索引时,条件表达式的左侧应该是原始字段请牢记,这一点是在优化索引时首要关注的地方。


2.3 使用否定形式


下面这几种否定形式不能用到索引。



  • <>

  • !=

  • NOT


这个是跟具体数据库的优化器有关,如果优化器觉得即使走了索引,还是需要扫描很多很多行的话,他可以选择直接不走索引。平时我们用!=、<>、not in的时候,要注意一下。


2.4 使用OR查询前后没有同时使用索引


例如下表:


CREATE TABLE test_tb ( 
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(55) NOT NULL
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用OR条件进行查询


SELECT * 
FROM test_tb
WHERE id = 1 OR name = 'tom'

这个SQL的执行条件下,很明显id字段查询会走索引,但是对于OR后面name字段的查询是需要进行全表扫描的。在这个场景下,优化器会选择直接进行一遍全表扫描。


2.5 使用联合索引时,列的顺序错误


使用联合索引需要满足最左匹配原则,即最左优先。如果你建立一个(col_1, col_2, col_3)的联合索引,相当于建立了 (col_1)、(col_1,col_2)、(col_1,col_2,col_3) 三个索引。如下例子:


-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 AND col_3 = 500;
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_1 = 10 AND col_3 = 500 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_3 = 500 ;
-- 走了索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_1 = 10 ;

联合索引中的第一列(col_1)必须写在查询条件的开头,而且索引中列的顺序不能颠倒。



可能需要说明的是最后一条SQL为什么会走索引,简单转化一下,col_2 = 100 AND col_1 = 10,
这个条件就相当于col_1 = 10 AND col_2 = 100,自然就可以走联合索引。



2.6 使用LIKE查询


并不是用了like通配符,索引一定会失效,而是like查询是以%开头,才会导致索引失效。


-- 没走索引
SELECT * FROM SomeTable WHERE col_1 LIKE'%a';
-- 没走索引
SELECT * FROM SomeTable WHERE col_1 LIKE'%a%';
-- 走了索引
SELECT * FROM SomeTable WHERE col_1 LIKE'a%';

2.7 连接字段字符集编码不一致


如果两张表进行连接,关联字段编码不一致会导致关联字段上的索引失效,这是博主在线上经历一次SQL慢查询后的得到的结果,举例如下,有如下两表,它们的name字段都建有索引,但是编码不一致,user表的name字段编码是utf8mb4,user_job表的name字段编码是utf8,


CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER
SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`age` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `user_job` (
`id` int NOT NULL,
`userId` int NOT NULL,
`job` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

进行SQL查询如下:


EXPLAIN
SELECT *
from `user` u
join user_job j on u.name = j.name


由结果可知,user表查询走了索引,user_job表的查询没有走索引。想要user_job表也走索引,可以把user表的name字段编码改成utf8即可。


三、减少中间表


在SQL中,子查询的结果会被看成一张新表,这张新表与原始表一样,可以通过代码进行操作。这种高度的相似性使得SQL编程具有非常强的灵活性,但是如果不加限制地大量使用中间表,会导致查询性能下降。


频繁使用中间表会带来两个问题,一是展开数据需要耗费内存资源,二是原始表中的索引不容易使用到(特别是聚合时)。因此,尽量减少中间表的使用也是提升性能的一个重要方法。


3.1 使用HAVING子句


对聚合结果指定筛选条件时,使用HAVING子句是基本原则。不习惯使用HAVING子句的人可能会倾向于像下面这样先生成一张中间表,然后在WHERE子句中指定筛选条件。例如下面:


SELECT * 
FROM (
SELECT sale_date, MAX(quantity) max_qty
FROM SalesHistory
GR0UP BY sale_date
) tmp
WHERE max_qty >= 10

然而,对聚合结果指定筛选条件时不需要专门生成中间表,像下面这样使用HAVING子句就可以。


SELECT sale_date, MAX(quantity)
FROM SalesHistory
GR0UP BY sale_date
HAVING MAX(quantity) >= 10;

HAVING子句和聚合操作是同时执行的,所以比起生成中间表后再执行的WHERE子句,效率会更高一些,而且代码看起来也更简洁。


3.2 对多个字段使用IN


当我们需要对多个字段使用IN条件查询时,可以通过 || 操作将字段连接在一起变成一个字符串处理。


SELECT *
FROM Addresses1 A1
WHERE id || state || city
IN (SELECT id || state|| city
FROM Addresses2 A2);

这样一来,子查询不用考虑关联性,而且只执行一次就可以。


需要说明的MySql中,|| 操作符是代表或者也就是OR的意思。在Mysql中可以使用下面多种写法,如下:


-- 使用CONCAT(str1,str2,...)函数,将多列合并为一个字符串
SELECT *
FROM Addresses1 A1
WHERE CONCAT(id, state, city)
IN ('1湖北武汉', '2湖北黄冈');

-- 使用多列in查询
SELECT *
FROM Addresses1 A1
WHERE (id, state, city)
IN ((1, '湖北', '武汉'), (2, '湖北', '黄冈'));

使用多列in查询这个语法在实际执行中可以走索引,CONCAT(str1,str2,...) 函数不能。


3.3 先进行连接再进行聚合


连接和聚合同时使用时,先进行连接操作可以避免产生中间表。原因是,从集合运算的角度来看,连接做的是“乘法运算”。连接表双方是一对一、一对多的关系时,连接运算后数据的行数不会增加。而且,因为在很多设计中多对多的关系都可以分解成两个一对多的关系,因此这个技巧在大部分情况下都可以使用。


到此本文讲解完毕,感谢大家阅读,感兴趣的朋友可以点赞加关注,你的支持将是我更新动力😘。



作者:waynaqua
来源:juejin.cn/post/7221735480576245819
收起阅读 »

你的@Autowired被警告了吗

一个警告 近期组里来了新同学,依赖注入的时候习惯使用@Autowired,发现被idea黄色警告,跑过来问大家。由于我平时习惯使用@Resource,没太注意过这个问题,刚好趁着这个机会学习了一波。 首先看下问题的现象,使用@Autowired被idea警告,...
继续阅读 »

一个警告


近期组里来了新同学,依赖注入的时候习惯使用@Autowired,发现被idea黄色警告,跑过来问大家。由于我平时习惯使用@Resource,没太注意过这个问题,刚好趁着这个机会学习了一波。

首先看下问题的现象,使用@Autowired被idea警告,而使用@Resource则不会:


image.png


image.png


@Autowired和@Resource的差异


来源


  • @Resource是JSR 250中的内容,发布时间是2006年

  • @Autowired是Spring2.5中的内容,发布时间是2007年


@Resource是JSR的标准,Spring框架也提供了实现。既然@Autowired是在@Resource之后发布的,应该就有@Resource不能表达的含义或者不能实现的功能。


用法

注入方式

虽然我们开发中使用最多的方式是属性注入,但其实存在构造函数注意、set方法注入等方式。相比于@Resource支持的属性注入和set方法注入,@Autowired还能支持构造方法注入的形式,@Resource是不行的。


image.png


image.png


可指定属性

@Autowired支持required属性


image.png


@Resource支持7个其它属性


image.png


bean查找策略


  • @Autowired是类型优先,如果这个类型的bean有多个,再根据名称定位

  • @Resource是名称优先,如果不存在这个名称的bean,再去根据类型查找


查找过程


@Autowired

auto.png


@Resource

resource.png


思考


对于大多数开发同学来说,@Autowired 和 @Resource 差异是很小的,虽然 @Autowired 多支持构造器注入的形式,但是直接属性注入真的太灵活太香了。而@Autowired晚生于@Resource,既然已经有JSR标准的@Resource,还要增加1个特有Autowired,必然有Spring的考虑。

个人认为,构造器注入和支持属性不同这个理由是很弱的,这些特性完全可以在@Resource上实现,也不违反JSR的约束。比较可能的原因是,含义的不同,而最大的不同体现在bean查找策略上,@Autowired默认byType,@Resource默认byName,这个不同其实隐含了,@Resource注入的bean,更加有确定性,你都已经确定了这个bean的名称了,而类型在Java中编译过程本身是个强依赖,其实这里相当于指定了类型和名称,注入的是一个非常确定的资源。而@Autowired是类型优先,根据类型去查找,相比于@Resource,确定性更弱,我知道这里要注入bean的类型,但是我不确定这个bean的名称,也隐含体现了java多态的思想。


总结


回到开篇的idea的警告,网上有很多人都赞同的一种说法是,@Resource是JSR规范,@Autowired是Spring提供,不推荐使用绑定了Spring的@Autowired,因为@Resource在更换了框架后,依然可以使用。我不太赞同这种说法,因为idea的错误提示很明确,Field injection is not recommended,不推荐使用属性注入的方式,那换成@Resource,我理解并没有解决这个问题,虽然idea确实不警告了,可能有点掩耳盗铃的意思。

比较推荐的做法是,使用构造方法注入,虽然很多时候会有点麻烦,特别是增加依赖的时候,但是正是这种麻烦,会让你不再那么随意做属性注入,更能保持类的职责单一。然后配合lombok,也可以让你省去这种麻烦,不过希望还是能通过这个警告时刻提醒自己保持类的职责单一。


image.png


作者:podongfeng
来源:juejin.cn/post/7265926762729717819
收起阅读 »

什么?你现在还想用微信小程序做副业?

前言 相信各位程序猿/程序媛小朋友都有想创业赚外快的梦想,梦想哪一天自己能做出来个爆款产品,用户量蹭蹭的涨,真金白银哗哗的涌进来,挡都挡不住,从此当上总经理、出任CEO、赢取白富美、走上人生巅峰。或者退而求其次,产品虽然不那么火。但也可以让自己每天躺着也能挣钱...
继续阅读 »

前言


相信各位程序猿/程序媛小朋友都有想创业赚外快的梦想,梦想哪一天自己能做出来个爆款产品,用户量蹭蹭的涨,真金白银哗哗的涌进来,挡都挡不住,从此当上总经理、出任CEO、赢取白富美、走上人生巅峰。或者退而求其次,产品虽然不那么火。但也可以让自己每天躺着也能挣钱,副业的收入就足够日常开销,那个班随便上上就行了,还卷什么卷。


而做副业的各种途径中,做微信小程序无疑是门槛比较低的途径之一,微信国内几乎每个人都在用,那数量庞大的用户和流量无人能及,小程序天然跨平台,运行体验也比原生h5似乎流畅那么一点点。


最关键的是开发成本低啊,只要有一点点JS基础,分分钟就能搞出来个能跑的小程序,不会后端也没关系,有云开发,照着文档,基本的增删改查也是分分钟就能搞定。


注册一个账户很简单,做完的小程序上架也很简单。一切都很简单有木有。看到这里你是不是想立马手搓个小程序试试?


然而,时代已经变了,看似简单的小程序做起来已经不那么简单了。现在我已经不建议大家在做副业的时候选择微信小程序了。


回顾


在说明为什么不建议再做小程序之前,先来回顾一下个人做小程序之前为什么是可行的。


创业也好,做副业也好,首先得看大趋势,雷总不是有句名言嘛,“站在风口上,猪都能飞起来”,换句话说,赚钱要趁早,智能手机刚起步的时候去做app,微信刚推出小程序的时候你去做小程序,那赚钱的可能性都比较大,毕竟是蓝海,而现在,app,小程序啥的早就是一片红海了。有多么红海,你随便搜一下“小程序已死”就知道了,基本上有需求的地方都已经有小程序占好坑了。对个人做小程序来讲那时也是最好的时候,做成的概率也比较大。


当然,错过了风口也不是完全没机会,虽然小程序变红海已经好长时间了,但是架不住万一你突然发现了哪个小众需求,或者蹭到了哪个热点,比如前一段火的不行的短视频去水印,虽然靠这个没法财富自由,但赚个零花钱还是可以的。


做任何事情都是开始最困难,不是有句老话叫“万事开头难”么。而小程序曾经就可以把这个开头的难度降低很多,除了上面讲的开发难度低,另一个就是试错的成本也低。如果一个小程序做不起来可以迅速再搞一个其他小程序试试,甚至可以弄出来一堆小程序,哪个能活下来就重点运营,其他不行的可以放弃后再搞新的,基本上除了开发没有其他的成本。


最重要的一个点就是,虽然平台对个人主体仅开放了有限的类目,理论上讲,个人能做的小程序基本上被限制在工具类里面,比如都被做烂了的记账,备忘录啥的。我们都知道小程序的分享能力是非常重要的,可以分享才能裂变出新用户,新流量。


虽然原则上你的小程序是不能把用户创作的内容分享出去的,一旦涉及到分享,就被归为社交类目,而这个类目是不对个人开放的,举个例子,你搞了个制作头像的小程序,那用户制作好的头像是不能发给别人的,只能保存在本地。


但是呢,之前微信小程序平台对这些个规则执行的并不是十分严格。这就存在一些可操作的空间,让你上线一些只有公司才可以做的小程序。


所以,这里就有了一个可以低成本试错的路径,你可以先利用这个模糊空间,拍脑门也好,蹭热点也罢,先以个人名义上线小程序,如果不理想,就直接弃用或者换名字改成其他类型的小程序再试,扔掉的成本基本上就只是开发成本。如果不小心成了爆款,那恭喜你,可以去成立个公司啥的,再把小程序迁移过来,然后成为CEO,赢取白富美,走上人生巅峰。。。


dead.jpg
不幸的是,现在,这条路已经被堵死了


变化


为啥说路被堵死了,因为最近发生了一些变化,让个人开发者的试错成本直线上升,甚至可以说根本无路可走了。


创业或者搞副业无非就是要赚钱,收入减去成本就是赚到的钱。那就从成本,收入两个角度来分析一下发生了什么导致现在搞小程序很难赚到钱。


成本


先看成本,成本飙升到能把个人开发者的路都堵死,还得从微信这一年的一系列操作说起。这波操作总结起来就是一个字:“得加钱”


ac608998885aa3fee54ec48980361533.jpg


云开发,个人开发者用来快速整个后端,一年多前起步免费,现在起步价:优惠19.9/月,原价:39.9/月,起步价里面包含的资源也就够你开发调试用吧,上线以后超出部分另算。


个人小程序的自然流量基本上都依靠搜索,在We分析里可以看到用户通过搜索哪些关键词访问到小程序,开发者可以相应的做搜索词优化来引流。自12月18日起,这项功能也开始收费了,起步价388/年,这个起步价啥概念?你的小程序过去一段时间日打开次数得小于100次。。。


以上都是平a,下面这个才是大招,“必杀技:备案+认证无敌真伤斩杀组合拳”


先说认证,认证这事之前仅对企业等非个人主体开放的,300一个小程序,不管认证能不能通过,如果通过了就是永久有效。个人小程序无需认证。


然而,从下半年开始,个人小程序也需要认证了,目前优惠价30每个小程序,而且认证有效期只有一年了。不认证也可以,那你的小程序就别想被搜索到,也别想往外分享了。


你说你去认证吧,就会遇到另外一个问题,认证的时候要审核你的小程序的名字。做过小程序的都知道,名字可太重要了。直接决定了你的小程序能不能被搜索到。所以之前大家起名几乎都是在直接对标搜索热门词,什么“群签到助手”,什么“去水印神器”之类的。


然而现在在认证的时候这些名字统统不合格,名字不能太宽泛,必须含有个人特征。例如,张三做了个“记事本”小程序去认证,那名字几乎得叫“张三的记事本”之类才好通过。想蹭流量?门都没有。


再说下一个,备案。从9月4号开始,所有新小程序必须备案后才能上线,已上线的小程序在明年4月之前必须完成备案,否则就下架。


平台会对要备案的小程序做初审,这个初审就相当严格了。把个人小程序直接按死在那有限的几个类目里。基本上现在个人只能做记事本三件套了。前面说的模糊空间已经不存在了,路也就堵死了。


你要说能不能先通过备案,然后在新版本里添上新功能?不好意思,现在对版本的审核也很严格。以前几个小时就能审好,现在几天都是正常的,有超过资质范围的功能直接打回。


备案也会对小程序名字做审核,你要问啥样的名字能过审?别问我,我也不知道。


由于认证和备案不是同一拨人在搞,所以如果你的小程序不太幸运的话会遇到以下问题:


30认证 --> 认证不通过 --> 改名+30认证 --> 认证通过 --> 备案不通过 --> 改名+30认证。。。


就说为了个小程序,你能不能经得住这样的折腾吧。


至于看不懂的文档,日常的违规警告,无法找到的人工客服,随便废弃接口,隐私协议闹剧(具体可看《各位微信小程序的开发者们,你们还好吗?》)等等大家都早已经麻了。


成本讲完了,下面说说收入。


收入


什么?你还想着有收入?收入就是没有收入!(全文完)


------------------------- 分 ----- 割 ----- 线 ------------------------


说没有收入,是玩笑话,也不是玩笑话。小程序除非你另有门路,大部分个人开发者就只能靠当流量主接入平台的广告赚钱。然而,现在广告的ecpm虽然不能说聊胜于无,也可以说是惨不忍睹。并且这下降的趋势似乎还看不到头,简直和股市一个尿性。


就那点广告费收入,再减去上面说的成本,能不亏钱就不错了,至于收益,这么说吧,
去各大平台薅羊毛,每天在某东签个到,在某宝果园里种个树,农场里养个鸡,或者在某音某手放一个你好基友啪叽在冰面上摔个狗啃泥的短视频,获得的收益都可能比苦哈哈做小程序的收益多。


某多多除外,你永远都别想薅到某多多的羊毛。


总结


个人做小程序的成本和收益都写在上面了,干货满满。各位想着做副业的小朋友看完之后不妨自己先算算账,然后再决定要不要做小程序。另外大家对此还有什么想吐槽的,欢迎在讨论区一起聊聊。


作者:ad6623
来源:juejin.cn/post/7317104726768697398
收起阅读 »

降本增笑,领导要求程序员半年做出一个金蝶

关于降本增效的事,今天在网上看到一个帖子,虽然有点搞笑,但是对于打工人(特别是技术同学)来说,可挖掘的东西也不少,特别分享给大家。 真的是好多年没听说这样的事了,记得以前总有老板让做个淘宝、做个京东,然后预算只有几千块,最多几万块。 这些故事程序员谈起来往往...
继续阅读 »

关于降本增效的事,今天在网上看到一个帖子,虽然有点搞笑,但是对于打工人(特别是技术同学)来说,可挖掘的东西也不少,特别分享给大家。



真的是好多年没听说这样的事了,记得以前总有老板让做个淘宝、做个京东,然后预算只有几千块,最多几万块。


这些故事程序员谈起来往往都是哈哈一笑,并疯狂吐槽一番。



不过笑过之后,大家是否想过如何去解决问题?或者真的去评估下可行性,探索一下可能的实现路径。


找到问题


首先我们看下老板的问题。老板的根本问题并不是想要做金蝶,为什么这么说呢?


我们看看网友的描述就知道了:经济下行,领导不想出金蝶系统的维护费,不想为新功能花大价钱。这才是根本问题,用四个字来说就是:降低成本。


然后才是老板想到能不能用更少的钱达到金蝶系统的使用效果,再之后才是自己能不能做一个类似金蝶的系统,并思考了自己可以承担的成本:一个前端、一个后端,半年时间。


最后问题被抛到了这位网友的手里。可以看得出来这位网友也不太懂,还去咨询了朋友。不知道它有没有向朋友说清楚问题,还是只说了老板想自己做一个金蝶系统,结果是朋友们都说不可行。


遇到问题时,我们得把这个问题完完整整的捋一遍,找到最根本的问题,然后再说怎么解决问题,否则只是停留在表面,就可能事倍功半。在这个上下文中,根本的问题就是:降低成本。



解决问题


明确了老板的根本问题,我们就可以琢磨方案了。既然是降低财务系统的成本,可行的方案应该还是有几个的。


使用替代产品


假如公司只使用产品的部分功能,是不是可以选择金蝶的低版本?是不是可以降低一些人头费?


金蝶的服务贵,是不是可以选择一些小厂的产品?国内做财务系统的应该挺多的,小厂也更容易妥协。


或者选择SaaS服务,虽然SaaS用久了成本也不低,但是可以先撑过这几年,降低当前的支出。


当然替换财务系统也是有成本的,需要仔细评估。不过既然都想自己做了,这个成本应该能hold住。


找第三方维护


金蝶的服务贵,是不是可以找其它三方或者个人来维护修改?根据我之前的了解,金蝶这种公司有很多的实施工作是外包出去的,或者通过代理商来为客户服务,能不能找这些服务商来代替金蝶呢?或者去某些副业平台上应该也能找到熟悉金蝶系统的人。


当然这个还要看系统能不能顺利交接,金蝶有没有什么软硬件限制,第三方能不能接过来。


另外最重要的必须考虑系统和数据的安全性,不能因小失大。


自己开发


虽然自己开发的困难和成本都很高,但我仍旧认为这可能也是一个合适的解决方案。理由有下面两点。



  • 功能简单:如果公司的业务比较简单,使用的流程也简单,比如不使用涉及复杂的财务处理,那么捋一捋,能给开发人员讲清楚,也是有可能在短时间内完成的。

  • 迭代渐进:长城不是一天建成的,系统也都是逐渐迭代完善的。自己开发可以先从部分模块或者功能开始,然后逐步替换,比如前边的流程先在新系统中做,最后再导入金蝶。即使不能做到逐步替换,也可以控制系统的风险,发现搞不定时,及时止损。相信老板也能明白这个道理,如果不明白或者不接受,那确实搞不了。



当然我们也肯定不能忽视这其中的困难。我之前做过和金蝶系统的对接,订单的收付款在业务系统完成,然后业务系统生成凭证导入到金蝶K3。依稀记得业务也不算复杂,但是需求分析做了好几遍,我的代码也是改了又改,上线之后遇到各种问题,继续改,最终花了几个月才稳定下来。


事后分析原因,大概有这么几点:



  • 产品或者需求分析人员没接触过类似的业务,即使他对财务系统有一些经验,也不能准确的将客户的业务处理方式转换到产品设计中;

  • 财务人员说不明白,虽然他会使用金蝶系统,但是他不能一次性的把所有规则都讲出来,讲出来也很难让程序员在短时间内理解;

  • 程序员没做过财务系统,没接触过类似的业务,系统的设计可能要反复调整,比如业务模块的划分逻辑,金额用Long还是用BigDecimal,数据保留几位小数,这都会大幅延长开发周期,如果不及时调整就可能写成一锅粥,后期维护更困难。


这还只是和金蝶系统做一个简单的对接,如果要替代它,还要实现更多的功能,总结下,企业可能会面对下面这些困难:


业务复杂:财务规则一般都比较复杂,涉及到各种运算,各种数字、报表能把人搞晕。如果公司的业务也很复杂,比如有很多分支或者特殊情况,软件开发的难度也会很大,这种难度的变化不是线性增加的,很可能是指数级增长的,一个工作流的设计可能就把人搞死了。


懂业务的人:系统过于复杂时,可能没有一个人能把系统前前后后、左左右右的整明白。而要完成这样一个复杂的系统,必须有人能从高层次的抽象,到具体数字运算的细枝末节,完完全全的整理出来,逻辑自洽,不重不漏,并形成文档,还要能把程序员讲明白。


懂架构的人:这里说的是要有一个经验丰富的程序员,不能是普通的码农,最好是有财务系统开发经验的架构师。没走过的路,总是要踩坑的。有经验的开发人员可以少走很多弯路,极大降低系统的风险。这样的人才如果公司没有,外招的难度比较大,即使能找到,成本也不低。


灵活性问题:开发固定业务流程的系统一般不会太考虑灵活性的问题,如果业务需要调整,可能需要对系统进行大幅修改,甚至推倒重来。如果要让系统灵活些,必然对业务和技术人员都提出了更高的要求,也代表着更强的工作能力和更多的工作量。


和其它系统的对接:要不要和税务系统对接?要不要和客户管理系统对接?要不要和公司的OA对接?每一次对接都要反复调试,工作量肯定下不来。


总之,稍微涉及到财务处理的系统,都不是一个前端和一个后端能在短时间内完全搞出来的。


对程序开发的启示


搞清楚需求


日常开发过程中,大家应该都遇到过不少此类问题。领导说这里要加个功能,然后产品和开发就去吭哧吭哧做了,做完了给领导一看,不是想要的,然后返工反复修改。或者说用户提了一个需求,产品感觉自己懂了,然后就让开发这么做那样改,最后给用户一看,什么破玩意。这都是没有搞清楚真正的需求,没有触达那个根本问题。


虽然开发人员可以把这些问题全部甩给产品,自己只管实现,但这毕竟实实在在的消耗了程序员的时间,大量的时间成本和机会成本,去干点有意义的事情不好吗?所以为了不浪费时间,开发也要完整的了解用户需求。在一个团队中,至少影响产品落地的关键开发人员要搞懂用户的需求。


那么遇到这种问题,程序员是不是可以直接跑路呢?


也是一个选择, 不过对于一个有追求的程序员,肯定也是想把程序设计好、架构好的,能解决实际问题的,这也需要对用户需求的良好把控能力,比如我们要识别出哪些是系统的核心模块,哪些是可扩展能力,就像设计冯诺依曼计算机,你设计的时候会怎么处理CPU和输入输出设备之间的关系呢?


对于用户需求,产品想的是怎么从流程设计上去解决,开发需要考虑的是怎么从技术实现上去满足,两者相向而行,才能把系统做好。


当然准确把握用户的需求,很多时候并不是我说的这么容易,因为用户可能也说不清楚,我们可能需要不断的追问才能得到一些关键信息。比如这位网友去咨询朋友时,可能需求就变成了:我们要做一个财务系统,朋友如果不多问,也只能拿到这个需求,说不定这位朋友也有二次开发的能力,错失了一次挣钱的好机会。还有这位老板上边可能还有更大的老板,这位老板降低成本的需求也可能是想在大老板面前表现一下,那是不是还有其它降本增效的方法呢?比如简化流程、裁掉几个不关键的岗位(这个要得罪人了)。


我们要让程序始终保持在良好的状态,就要准确的把握用户需求,要搞懂用户需求,就需要保持谦逊求知的心态,充分理解用户的问题,这种能力不是朝夕之间就可以掌握的,是需要修炼的。


动起来


任何没有被满足的需求都是一次机会。


我经常会在技术社区看到一些同学分享自己业余时间做的独立产品,有做进销存的、客户管理的、在线客服的,还有解决问题的各种小工具,而且有的同学还挣到了钱。


我并不是想说让大家都去搞钱,而是说要善于发现问题、找到机会,然后动起来、去实践,实践的过程中我们可以发现更多的问题,然后持续解决问题,必然能让自己变得越来越强。在经济不太好的情况下,我们才有更强的生存能力。




啰里八嗦一大堆,希望能对你有所启发。


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


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

2023市场需求最大的8种编程语言出炉!

众所周知,编程语言的种类实在是太多了。直到现在,经常还会看到关于编程语言选择和学习的讨论。 虽说编程语言有好几百种,但实际项目使用和就业要求的主流编程语言却没有那么多。 大家可能也会好奇:现如今就业市场上到底什么编程语言最受欢迎?或者说最需要的编程语言是什么?...
继续阅读 »

众所周知,编程语言的种类实在是太多了。直到现在,经常还会看到关于编程语言选择和学习的讨论。


虽说编程语言有好几百种,但实际项目使用和就业要求的主流编程语言却没有那么多。


大家可能也会好奇:现如今就业市场上到底什么编程语言最受欢迎?或者说最需要的编程语言是什么?


所以今天我们就结合Devjobsscanner之前发布的「Top 8 Most Demanded Programming Languages in 2023」编程语言清单来聊一聊这个问题。



虽说这个清单并不是完全针对我们本土开发者的调查,但还是能反映一些趋势的,大家顺带也可以参看一下各种编程语言的发展趋势和前景。


Devjobsscanner是一个综合性开发者求职/岗位信息聚合网站。



上面聚合展示了很多开发者求职岗位信息,并按多种维度进行分类,以便用户进行搜索。



该网站每年都会发布一些相关方面的调查总结报告,以反映开发者求职方面的趋势。


从2022年1月到2023年5月,这17个月的时间里,Devjobsscanner分析了超过1400万个开发岗位,并从中进行筛选和汇编,并总结出了一份「2023年需求量最大的8种编程语言」榜单。


所以下面我们就来一起看一看。


No.1 JavaScript/TypeScript


基本和大家所预想到的一样,Javascript今年继续蝉联,成为目前需求最大的编程语言。



当然这也不难理解,因为基本上有Web、有前端、有浏览器、有客户端的地方都有JavaScript的身影。


而且近几年TypeScript的流行程度和需求量都在大增,很多新的前端框架或者Web框架都是基于TypeScript编写的。


所以学习JavaScript/TypeScript作为自己的主语言是完全没有问题的。



  • 职位数量/占比变化趋势图



No.2 Python


榜单上排名第二的是Python编程语言。



众所周知,Python的应用范围非常广泛。


从后端开发到网络爬虫,从自动化运维到数据分析,另外最近这些年人工智能领域也持续爆火,而这恰恰也正是Python活跃和擅长的领域。


尤其最近几年,Python强势上扬,这主要和这几年的数据分析和挖掘、人工智能、机器学习等领域的繁荣有着不小的关系。



  • 职位数量/占比变化趋势图



No.3 Java


榜单中位于第三需求量的编程语言则是Java。



自1995年5月Java编程语言诞生以来,Java语言的流行程度和使用频率就一直居高不下,并且在就业市场上的“出镜率”很高。


所以每次调查结果出来,Java基本都榜上有名,而且基本长年都维持在前三。


Java可以说是构成当下互联网繁荣生态的重要功臣,无数的Web后端、互联网服务、移动端开发都是Java语言的领地。



  • 职位数量/占比变化趋势图



No.4 C#



看到C#在榜单上位列前四的那会,说实话还是挺意外的,毕竟自己周围的同学和同事做C#这块相对来说还是较少的。


但是C#作为一种通用、多范式、面向对象的编程语言,在很多领域其实应用得还是非常广泛的。


我们都知道,其实像.NET和Unity等框架在不少公司里都很流行的,而C#则会被大量用于像Unity等框架的项目编写。



  • 职位数量/占比变化趋势图



No.5 PHP



看到PHP在榜单上位列第五的时候,不禁令人又想起了那句梗:


不愧是最好的编程语言(手动doge)。


所以以后可不能再黑PHP了,看到没,这职位数量和占比还是非常高的。



  • 职位数量/占比变化趋势图



No.6 C/C++


C语言和C++可以说都是久经考验的编程语言了。



C语言于1972年诞生于贝尔实验室,距今已经有50多年了。


自诞生之日起,C语言就凭借其灵活性、细粒度和高性能等特性获得了无可替代的位置,而且随着如今的万物互联的物联网(IoT)时代的兴起,C语言地位依然很稳。


C语言和C++的应用领域都非常广泛,在一些涉及嵌入式、物联网、操作系统、以及各种和底层打交道的场景下都有着不可或缺的存在意义。



  • 职位数量/占比变化趋势图



No.7 Ruby


Ruby这门编程语言平时的出镜率虽然不像Java、Python那样高,但其实Ruby的应用领域还是挺广的,在包括Web开发、移动和桌面应用开发、自动化脚本、游戏开发等领域都有着广泛的应用。



Ruby在20世纪90年代初首发,并在2000年代初开始变得流行。


Ruby是一种动态且面向对象的编程语言,语法简单易学,使用也比较灵活,因此也吸引了一大批爱好者。



  • 职位数量/占比变化趋势图



No.8 GO


虽说Go语言是一个非常年轻的编程语言(由谷歌于2009年对外发布),不过Go语言最近这几年来的流行程度还是在肉眼可见地增加,国内外不少大厂都在投入使用。



众所周知,Go语言在编译、并发、性能、效率、易用性等方面都有着不错的表现,也因此吸引了一大批学习者和使用者。



  • 职位数量/占比变化趋势图



完整表单


最后我们再来全局看一看Devjobsscanner给出的编程语言完整表单和职位数量/占比的趋势图。




不难看出,JavaScript、Python和Java这三门语言在就业市场上的需求量和受欢迎程度都很大,另外像C语言、C#、Go语言的市场岗位需求也非常稳定。


总体来说,选择清单里的这些编程语言来作为自己的就业主语言进行学习和精进都是没有问题的。


说到底,编程语言没有所谓的好坏优劣,而最终选择什么,还是得看自己的学习兴趣以及使用的场景和需求。


作者:CodeSheep
来源:juejin.cn/post/7316968265057828874
收起阅读 »