裸辞后,我活得像个废物,但我终于开始活自己
哈喽,大家好!我是Web大鹅只会叫!下去沉淀了那么久,终于有时间回来给大家写点什么了!
你问我人为什么活着?我哪知道啊?我又不是上帝!释迦牟尼为了解这个问题还跑去出家,结果发现人活着就是为了涅槃——也就是死。所以别问我人生的意义,我也没搞明白!不过我能告诉你一个答案,那就是裸辞后,我终于知道了为什么要活着——那就是为了“活得自由”!
裸辞后,那些走过的路,和你说的“脏话”
2024年8月,我做了一个震惊所有人的决定——裸辞!是的,没错,我就是那种毫不犹豫地辞了职、丢下稳定收入和安稳生活,拿着背包去走四方的“疯子”。放下了每天早起20公里开车上班的压力,放下了无聊的加班、枯燥的开会,放下了所谓的“你要努力争取美好生活”的叮嘱。一切都在“离开”这一刻轻轻拂去,带着一种挥之不去的自由感。
带着亲人的责怪、朋友的疑问、同事的眼神、以及自己满满的疑惑,我开始了这段没有目的的旅行。我不知道我想找什么,但我知道,我不想再活得像以前那样。
我走过了无数地方,南京、湖州、宁波、杭州、义乌、金华、嘉兴、镇江、扬州、苏州、无锡、上海……一路走来,路过了每个风景,每个城市,每个人生。我甚至去了中国最北的漠河,站在寒风凛冽的雪地里,终于明白了一个道理:“你活着,才是最值得庆祝的事。
你知道吗,最让人清醒的,不是远方的景色,而是走出去之后,终于能脱离了那一套“你该做什么”的公式。每天不用设闹钟,不用准时吃饭,不用打卡上班,不用开会骂娘,再也不被地铁里的拥挤挤得喘不过气。生活突然变得宽松,我竟然开始意识到:我一直追求的美好生活,原来只是在为别人拼命。
走出内卷圈子的那一刻,我认为我是世界上最快乐的小孩了,我们渴望着幸福却糟践着现在,你所期望的美好未来却始终都是下一个阶段!你认为你是一个努力拼搏美好未来的人。可是现实比理想残酷的多!你没日没夜的拼搏,却让别人没日没夜的享受!你用尽自己半条命,换来的是下半辈子的病!我在裸辞后就告诉我自己:从今以后你想干什么、就干什么!你就是世界的主人! 嗯~ 爽多了!
走过的路,都在暗示我
我在大兴安岭漠河市的市里住了5天,住在一个一天40元的宾馆、干净、暖和!老板是一个退休的铁道工人。脸和双手都布满了冻伤,他的妻子(大姨)很面善。每天都会在我回来的时候和我聊上几句从前,安排一些极寒天气的注意事项。
有一天我去北极村回来,大姨和我聊了一会。大姨对我讲:“趁年轻、别把自己困起来,出去走走。不像我们,60年没出过这片土地,到头还要葬在这片土地上!”。她说这句话的时候没有忧虑、没有悲伤,却是一种满足感。是啊!60多了,还能追求什么?忙了大半辈子,把孩子都送出了这片土地,自己也没有激情出去走走了,很害怕自己的以后也是这样。
我20多岁的年纪,想的不是努力拼搏挣钱、不是搞事业。却总想着无所事事。我觉得自己像一个没有完全“被时间遗弃”的人,我甚至觉得自己不属于这个时代,这个不知道为了什么而拼命的时代。每走一步都好像在掏空自己积压已久的情绪:压力、焦虑、焦灼,让我很享受这种感觉。然后我想起来一本书里的话:“你活着,不是为了活给别人看。“ 是啊,我们都明白这个大道理,可自己从来没打算让自己脱离这个主线。我开始明白,我这次的旅行不是去寻找什么,而是放下什么!
从别人嘴里听到的“脏话”,其实是自己内心的尖刺
这段时间里,我经常回想起来那些让我神经紧绷的日子。尤其是我对“人” 这个物种越来越敏感的那个时期————‘恶毒、自私、无理、肮脏’。朋友的欺骗、同事的推锅、亲人的折磨都是罪恶的!可是到头来,事情还是发生了。地球还是在自转,太阳一样正常升起落下。这些都没有在你认为的这些琐事中消失。我不明白我还在纠结什么?
事实上,这些乱七八糟的事情并不是指向我个人的,它只是我内心脆弱的反射。是的,我一直在内耗自己罢了,把自己放在了一个焦虑的漩涡里。假装没事、假装坚强,结果别人一句话就能作为击垮我的最后一击。直到有一天,我发现我讨厌的只是我自己。所以我决定我不要去在意别人说什么、做什么,我不要逃避问题,我想听听我内心的想法,我不想让自己认为别人在定义我。
过程的意义:也许就是为了“停一停”
好了,我知道我的文采不好,但是也应该有个结尾。
在这一路上,我认识了很多有趣的人,他们不同风格的服装、不同口音,各式各样的生活方式。也有着各式各样的理想和困惑。有的喜欢在山顶等着日出的奇迹,有的则是想在湖边静静地坐着。而我,就是个迷途的羔羊,没有群体头羊的带领,我穿行在这些不同的路途中,慢慢摸索着向所有方向前进着。
偶尔我也会停下来,坐在湖边吹着风、闭上眼睛,听风,感受这一刻的宁静。然后我会微笑,我认为这个时候的我有了轻松的感觉。生活的答案我在这个时候找到了。
我意识到,未来不是重要的,现在才是应该享受的。我不知道我下一步要去哪里,但是我想先停下来看一看,呼吸一下。停下来不是因为我没有了目的,而是我知道,目的地并不重要,重要的是,我和自己在一起,心里不再有那么多焦虑,不再被过去的焦虑所束缚。
所以,我选择了离开,离开这一切,放下所有的焦虑和期待,享受我自己想要的生活。也许,活着的意义不在于追寻一个遥远的目标,而是过好每一个‘现在’。
来源:juejin.cn/post/7454064311079813132
好人难当,坏人不做
好人难当,以后要多注意了,涨点记性。记录三件事情证明下:
1. 免费劳动
之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都一年多过去了,还让我免费帮他改需求,我就说没时间,他说没时间的话可以把源码给他,他自己学着修改,我就直接把源码给他了,这个项目辛苦了一个多月,钱一毛也没赚到,我倒是搭进去一台服务器,一年花了三百多吧。现在源码给他就给他了吧,毕竟同学一场。没想到又过了半年,前段时间又找我来改需求了。这个项目他们家自己拿着赚钱,又不给我一毛钱,我相当于免费给他家做了个软件,还要出服务器钱,还要免费进行维护。我的时间是真不值钱啊,真成义务劳动了。我拒绝了,理由是忙,没时间。
总结一下,这些人总会觉得别人帮自己是理所当然的,各种得寸进尺。
2. 帮到底吧
因为我进行了仲裁,有了经验,然后被一个人加了好友,是一个前同事(就是我仲裁的那家公司),然后这哥们各种问题我都尽心回答,本着能帮别人一点就帮别人一点的想法,但是我免费帮他,他仲裁到手多少钱,又不会给我一毛钱。这哥们一个问题接一个,我都做了回答,后来直接要求用我当做和公司谈判的筹码,我严词拒绝了,真不知道这人咋想的,我帮你并没有获得任何好处,你这个要求有点过分了,很简单,他直接把我搬出来和公司谈判,公司肯定会找我,会给我带来麻烦,这人一点也没想这些事。所以之后他再询问有关任何我的经验,我已经不愿意帮他了。
总结一下,这些人更进一步,甚至想利用下帮自己的人,不考虑会给别人带来哪些困扰。
3. 拿你顶缸
最近做了通过一个亲戚接了一个项目,而这个亲戚的表姐是该项目公司的领导,本来觉得都是有亲戚关系的,项目价格之类开始问了,他们没说,只是说根据每个人的工时进行估价,后面我们每个人提交了个人报价,然后还是一直没给明确答复,本着是亲戚的关系,觉得肯定不会坑我。就一直做下去了,直到快做完了,价格还是没有出来,我就直接问了这个价格的事情,第二天,价格出来了,在我报价基础上直接砍半。我当然不愿意了,后来经过各种谈判,我终于要到了一个勉强可以的价格,期间群里谈判也是我一个人在说话,团队的其他人都不说话。后来前端的那人问我价格,我也把过程都实话说了,这哥们也要加价,然后就各种问我,我也啥都告他了。后来这个前端在那个公司领导(亲戚表姐)主动亮明身份,她知道这个前端和那个亲戚关系好,然后这个前端立马不好意思加价了,并且还把锅甩我头上,说是我没有告诉他她是他姐。还说我不地道,我靠,你自己要加价,关我啥事,你加钱也没说要分我啊,另外我给自己加价的时候你也没帮忙说话啊,我告诉你我加价成功了是我好心,也是想着你能加点就加点吧,这时候你为了面子不加了,然后说成要加价的理由是因为我,真是没良心啊。后面还问我关于合同的事情,我已经不愿意回答他了,让他自己找对面公司问去。
总结一下,这些人你帮他了他当时倒是很感谢你,但是一旦结果有变,会直接怪罪到你头上。
4. 附录文章
这个文章说得挺好的《你的善良,要有锋芒》:
你有没有发现,善良的人,心都很软,他们不好意思拒绝别人,哪怕为难了自己,也要想办法帮助身边的人。善良的人,心都很细,他们总是照顾着别人的情绪,明明受委屈的是自己,却第一时间想着别人会不会难过。
也许是习惯了对别人好,你常常会忽略自己的感受。有时候你知道别人是想占你便宜,你也知道别人不是真心把你当朋友,他们只是觉得你好说话,只是看中了你的善良,但是你没有戳穿,你还是能帮就帮,没有太多怨言。
你说你不想得罪人,你说你害怕被孤立,可是有人在乎过你吗?
这个世界上形形色色的人很多,有人喜欢你,有人讨厌你,你没有办法做到对每一个人好,也没办法要求每一个人都是真心爱你。所以你要有自己的选择,与舒服的人相处,对讨厌的人远离,过你自己觉得开心自在的生活就好,没必要为了便利别人,让自己受尽委屈。
看过一段话:善良是很珍贵的,但善良要是没有长出牙齿来,那就是软弱。
你的善良,要有锋芒,不要把时间浪费在不值得的人身上。对爱你的人,倾心相助,对利用你的人,勇敢说不。
愿你的善良,能被真心的人温柔以待。
来源:juejin.cn/post/7455667125798780980
让闲置 Ubuntu 服务器华丽转身为家庭影院
让闲置 Ubuntu 服务器华丽转身为家庭影院
在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。
在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。
一、实现 Windows 与 Ubuntu 服务器文件互通
要打造家庭影院,首先得让本地 Windows 电脑和 Ubuntu 服务器之间能够方便地传输电影文件。我选择安装 Samba 来实现这一目的。
- 安装 Samba:在 Ubuntu 服务器的终端中输入命令
sudo apt-get install samba samba-common
系统会自动下载并安装 Samba 相关的软件包。
- 备份配置文件:为了以防万一,我先将原来的 Samba 配置文件进行备份,执行命令
mv /etc/samba/smb.conf /etc/samba/smb.conf.bak
- 新建配置文件:使用
vim /etc/samba/smb.conf
命令打开编辑器,写入以下配置内容:
[global]
server min protocol = CORE
workgroup = WORKGR0UP
netbios name = Nas
security = user
map to guest = bad user
guest account = nobody
client min protocol = SMB2
server min protocol = SMB2
server smb encrypt = off
[NAS]
comment = NASserver
path = /home/bddxg/nas
public = Yes
browseable = Yes
writable = Yes
guest ok = Yes
passdb backend = tdbsam
create mask = 0775
directory mask = 0775
这里需要注意的是,我计划的媒体库目录是个人目录下的 nas/
,所以 path
是 /home/bddxg/nas
,如果大家要部署的话记得根据自己的实际情况修改为对应的位置。 
- 连接 Windows 电脑:在 Windows 电脑这边基本不需要什么复杂配置,因为在网络里无法直接看到 Ubuntu,我直接在电脑上添加了网络位置。假设服务器地址是
192.168.10.100
,那么添加网络位置就是 \\192.168.10.100\nas
,这样就可以在 Windows 电脑和 Ubuntu 服务器之间传输文件了。
要打造家庭影院,首先得让本地 Windows 电脑和 Ubuntu 服务器之间能够方便地传输电影文件。我选择安装 Samba 来实现这一目的。
- 安装 Samba:在 Ubuntu 服务器的终端中输入命令
sudo apt-get install samba samba-common
系统会自动下载并安装 Samba 相关的软件包。
- 备份配置文件:为了以防万一,我先将原来的 Samba 配置文件进行备份,执行命令
mv /etc/samba/smb.conf /etc/samba/smb.conf.bak
- 新建配置文件:使用
vim /etc/samba/smb.conf
命令打开编辑器,写入以下配置内容:
[global]
server min protocol = CORE
workgroup = WORKGR0UP
netbios name = Nas
security = user
map to guest = bad user
guest account = nobody
client min protocol = SMB2
server min protocol = SMB2
server smb encrypt = off
[NAS]
comment = NASserver
path = /home/bddxg/nas
public = Yes
browseable = Yes
writable = Yes
guest ok = Yes
passdb backend = tdbsam
create mask = 0775
directory mask = 0775
这里需要注意的是,我计划的媒体库目录是个人目录下的 nas/
,所以 path
是 /home/bddxg/nas
,如果大家要部署的话记得根据自己的实际情况修改为对应的位置。
- 连接 Windows 电脑:在 Windows 电脑这边基本不需要什么复杂配置,因为在网络里无法直接看到 Ubuntu,我直接在电脑上添加了网络位置。假设服务器地址是
192.168.10.100
,那么添加网络位置就是\\192.168.10.100\nas
,这样就可以在 Windows 电脑和 Ubuntu 服务器之间传输文件了。
二、安装 Jellyfin 搭建家庭影院
文件传输的问题解决后,接下来就是安装 Jellyfin 来实现家庭影院的功能了。
- 尝试 Docker 安装失败:一开始我选择使用 Docker 安装,毕竟 Docker 有很多优点,使用起来也比较方便。按照官网指南进行操作,在第三步启动 Docker 并挂载本地目录的时候却一直失败。报错信息为:
docker: Error response from daemon: error while creating mount source path '/srv/jellyfin/cache': mkdir /srv/jellyfin: read-only file system.
即使我给
/srv/jellyfin
赋予了777
权限也没有效果。无奈之下,我决定放弃 Docker 安装方式,直接安装 server 版本的 Jellyfin。
- 安装 server 版本的 Jellyfin:在终端中输入命令
curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash
,安装过程非常顺利。
- 配置 Jellyfin:安装完成后,通过浏览器访问
http://192.168.10.100:8096
进入配置页面。在添加媒体库这里,我遇到了一个麻烦,网页只能选择到/home/bddxg
目录,无法继续往下选择到我的媒体库位置/home/bddxg/nas
。于是我向 deepseek 求助,它告诉我需要执行命令:sudo usermod -aG bddxg jellyfin
# 并且重启 Jellyfin 服务
sudo systemctl restart jellyfin
按照它的建议操作后,我刷新了网页,重新配置了 Jellyfin,终于可以正常添加媒体库了。
- 电视端播放:在电视上安装好 Jellyfin apk 客户端后,现在终于可以正常读取 Ubuntu 服务器上的影视资源了,坐在沙发上,享受着大屏观影的乐趣,这种感觉真的太棒了!
通过这次折腾,我成功地让闲置的 Ubuntu 服务器重新焕发生机,变成了一个功能强大的家庭影院。希望我的经验能够对大家有所帮助,也欢迎大家一起交流更多关于服务器利用和家庭影院搭建的经验。
[!WARNING] 令人遗憾的是,目前 jellyfin 似乎不支持
rmvb
格式的影片, 下载资源的时候注意影片格式,推荐直接下载mp4
格式的资源
本次使用到的软件名称和版本如下:
软件名 | 版本号 | 安装命令 |
---|---|---|
samba | Version 4.19.5-Ubuntu | sudo apt-get install samba samba-common |
jellyfin | Jellyfin.Server 10.10.6.0 | curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash |
ffmpeg(jellyfin 内自带) | ffmpeg version 7.0.2-Jellyfin | null |
来源:juejin.cn/post/7476614823883833382
Mybatis接口方法参数不加@Param,照样流畅取值
在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param
注解,默认情况下,MyBatis 会将这些参数放入一个 Map
中,键名为 param1
、param2
等,或者使用索引 0
、1
等来访问。以下是具体的使用方法和注意事项。
一、Mapper 接口方法
假设有一个 Mapper 接口方法,包含多个参数但没有使用 @Param
注解:
public interface UserMapper {
User selectUserByNameAndAge(String name, int age);
}
二、XML 文件中的参数引用
在 XML 文件中,可以通过以下方式引用参数:
1. 使用 param1
、param2
等
MyBatis 会自动为参数生成键名 param1
、param2
等:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
</select>
2. 使用索引 0
、1
等
也可以通过索引 0
、1
等来引用参数:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{0} AND age = #{1}
</select>
三、注意事项
- 可读性问题:
- 使用
param1
、param2
或索引0
、1
的方式可读性较差,容易混淆。 - 建议使用
@Param
注解明确参数名称。
- 使用
- 参数顺序问题:
- 如果参数顺序发生变化,XML 文件中的引用也需要同步修改,容易出错。
- 推荐使用
@Param
注解:
- 使用
@Param
注解可以为参数指定名称,提高代码可读性和可维护性。
public interface UserMapper {
User selectUserByNameAndAge(@Param("name") String name, @Param("age") int age);
}
XML 文件:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{name} AND age = #{age}
</select>
- 使用
四、示例代码
1. Mapper 接口
public interface UserMapper {
User selectUserByNameAndAge(String name, int age);
}
2. XML 文件
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
</select>
或者:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{0} AND age = #{1}
</select>
3. 测试代码
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectUserByNameAndAge("John", 25);
System.out.println(user);
sqlSession.close();
- 如果 Mapper 接口方法有多个参数且没有使用
@Param
注解,可以通过param1
、param2
或索引0
、1
等方式引用参数。 - 这种方式可读性较差,容易出错,推荐使用
@Param
注解明确参数名称。 - 使用
@Param
注解后,XML 文件中的参数引用会更加清晰和易于维护。
来源:juejin.cn/post/7475643579781333029
Java web后端转Java游戏后端
作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程:
一、游戏后端核心职责
- 实时通信管理
- 采用WebSocket/TCP长连接(90%以上MMO游戏选择)
- 使用Netty/Mina框架处理高并发连接(单机支撑5W+连接是基本要求)
- 心跳机制设计(15-30秒间隔,检测断线)
- 游戏逻辑处理
- 战斗计算(需在50ms内完成复杂技能伤害计算)
- 状态同步(通过Delta同步优化带宽,减少60%数据传输量)
- 定时器管理(Quartz/时间轮算法处理活动开启等)
- 数据持久化
- Redis集群缓存热点数据(玩家属性缓存命中率需>95%)
- 分库分表设计(例如按玩家ID取模分128个库)
- 异步落库机制(使用Disruptor队列实现每秒10W+写入)
二、开发全流程实战(以MMORPG为例)
阶段1:预研设计(2-4周)
- 协议设计
// 使用Protobuf定义移动协议
message PlayerMove {
int32 player_id = 1;
Vector3 position = 2; // 三维坐标
float rotation = 3; // 朝向
int64 timestamp = 4; // 客户端时间戳
}
message BattleSkill {
int32 skill_id = 1;
repeated int32 target_ids = 2; // 多目标锁定
Coordinate cast_position = 3; // 技能释放位置
}
- 架构设计
graph TD
A[Gateway] --> B[BattleServer]
A --> C[SocialServer]
B --> D[RedisCluster]
C --> E[MySQLCluster]
F[MatchService] --> B
阶段2:核心系统开发(6-8周)
- 网络层实现
// Netty WebSocket处理器示例
@ChannelHandler.Sharable
public class GameServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
ProtocolMsg msg = ProtocolParser.parse(frame.text());
switch (msg.getType()) {
case MOVE:
handleMovement(ctx, (MoveMsg)msg);
break;
case SKILL_CAST:
validateSkillCooldown((SkillMsg)msg);
broadcastToAOI(ctx.channel(), msg);
break;
}
}
}
- AOI(Area of Interest)管理
- 九宫格算法实现视野同步
- 动态调整同步频率(近距离玩家100ms/次,远距离500ms/次)
- 战斗系统
- 采用确定性帧同步(Lockstep)
- 使用FixedPoint替代浮点数运算保证一致性
三、前后端协作关键点
- 协议版本控制
- 强制版本校验:每个消息头包含协议版本号
{
"ver": "1.2.3",
"cmd": 1001,
"data": {...}
}
- 调试工具链建设
- 开发GM指令系统:
/debug latency 200 // 模拟200ms延迟
/simulate 5000 // 生成5000个机器人
- 联调流程
- 使用Wireshark抓包分析时序问题
- Unity引擎侧实现协议回放功能
- 自动化测试覆盖率要求:
- 基础协议:100%
- 战斗用例:>85%
四、性能优化实践
- JVM层面
- G1GC参数优化:
-XX:+UseG1GC -XX:MaxGCPauseMillis=50
-XX:InitiatingHeapOccupancyPercent=35
- 网络优化
- 启用Snappy压缩协议(降低30%流量)
- 合并小包(Nagle算法+50ms合并窗口)
- 数据库优化
- 玩家数据冷热分离:
- 热数据:位置、状态(Redis)
- 冷数据:成就、日志(MySQL)
- 玩家数据冷热分离:
五、上线后运维
- 监控体系
- 关键指标报警阈值设置:
- 单服延迟:>200ms
- 消息队列积压:>1000
- CPU使用率:>70%持续5分钟
- 关键指标报警阈值设置:
- 紧急处理预案
- 自动扩容规则:
if conn_count > 40000:
spin_up_new_instance()
if qps > 5000:
enable_rate_limiter()
- 自动扩容规则:
六、常见问题解决方案
问题场景:战斗不同步
排查步骤:
- 对比客户端帧日志与服务端校验日志
- 检查确定性随机数种子一致性
- 验证物理引擎的FixedUpdate时序
问题场景:登录排队
优化方案:
- 令牌桶限流算法控制进入速度
- 预计等待时间动态计算:
wait_time = current_queue_size * avg_process_time / available_instances
通过以上流程,Java后端开发者可逐步掌握游戏开发特性,重点需要转变的思维模式包括:从请求响应模式到实时状态同步、从CRUD主导到复杂逻辑计算、从分钟级延迟到毫秒级响应的要求。建议从简单的棋牌类游戏入手,逐步过渡到大型实时游戏开发。
来源:juejin.cn/post/7475292103146684479
这个中国亲戚关系计算器让你告别“社死”
大家好,我是 Java陈序员
。
由于为了生活奔波,常年在外,导致很多关系稍疏远的亲戚之间来往并不多。
因此节假日回家时,往往会搞不清楚哪位亲戚应该喊什么称呼,很容易“社死”。
今天给大家介绍一个亲戚关系计算器,让你快速的计算出正确的亲戚称谓!
关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。
项目介绍
relationship
—— 中国亲戚关系计算器,只需简单的输入即可算出称谓。
输入框兼容了不同的叫法,你可以称呼父亲为:“老爸”、“爹地”、“老爷子”等等,方便不同地域的习惯叫法。
快捷输入按键,只需简单的点击即可完成关系输入,算法还支持逆向查找称呼哦~
功能特色:
- 使用别称查询:姥姥的爸爸的老窦 = 外曾外曾祖父
- 使用合称查询:姐夫的双亲 = 姊妹姻父 / 姊妹姻母
- 大小数字混合查询:大哥的二姑妈的七舅姥爷 = 舅曾外祖父
- 不限制祖辈孙辈跨度查询:舅妈的婆婆的外甥的姨妈的侄子 = 舅表舅父
- 根据年龄推导可能性:哥哥的表姐 = 姑表姐 / 舅表姐
- 根据语境确认性别:老婆的女儿的外婆 = 岳母
- 支持古文式表达:吾父之舅父 = 舅爷爷
- 解析某称谓关系链:七舅姥爷 = 妈妈的妈妈的兄弟
- 算两个亲戚间的合称关系:奶奶 + 外婆 = 儿女亲家
项目地址:
https://github.com/mumuy/relationship
在线体验:
https://passer-by.com/relationship/
移动端体验地址:
https://passer-by.com/relationship/vue/
功能体验
1、关系找称呼
2、称呼找关系
3、两者间关系
4、两者的合称
安装使用
1、直接引入安装
<script src="https://passer-by.com/relationship/dist/relationship.min.js">
获取全局方法 relationship
.
2、使用 npm 包管理安装
安装依赖:
npm install relationship.js
包引入:
// CommonJS 引入
const relationship = require("relationship.js");
// ES Module 引入
import relationship from 'relationship.js';
3、使用方法:唯一的计算方法 relationship
.
- 选项模式
relationship(options)
构造函数:
var options = {
text:'', // 目标对象:目标对象的称谓汉字表达,称谓间用‘的’字分隔
target:'', // 相对对象:相对对象的称谓汉字表达,称谓间用‘的’字分隔,空表示自己
sex:-1, // 本人性别:0表示女性,1表示男性
type:'default', // 转换类型:'default'计算称谓,'chain'计算关系链,'pair'计算关系合称
reverse:false, // 称呼方式:true对方称呼我,false我称呼对方
mode:'default', // 模式选择:使用setMode方法定制不同地区模式,在此选择自定义模式
optimal:false, // 最短关系:计算两者之间的最短关系
};
代码示例:
// 如:我应该叫外婆的哥哥什么?
relationship({text:'妈妈的妈妈的哥哥'});
// => ['舅外公']
// 如:七舅姥爷应该叫我什么?
relationship({text:'七舅姥爷',reverse:true,sex:1});
// => ['甥外孙']
// 如:舅公是什么亲戚
relationship({text:'舅公',type:'chain'});
// => ['爸爸的妈妈的兄弟', '妈妈的妈妈的兄弟', '老公的妈妈的兄弟']
// 如:舅妈如何称呼外婆?
relationship({text:'外婆',target:'舅妈',sex:1});
// => ['婆婆']
// 如:外婆和奶奶之间是什么关系?
relationship({text:'外婆',target:'奶奶',type:'pair'});
// => ['儿女亲家']
- 语句模式
relationship(exptession)
参数 exptession 句式可以为:xxx是xxx的什么人、xxx叫xxx什么、xxx如何称呼xxx等。
代码示例:
// 如:舅妈如何称呼外婆?
relationship('舅妈如何称呼外婆?');
// => ['婆婆']
// 如:外婆和奶奶之间是什么关系?
relationship('外婆和奶奶之间是什么关系?');
// => ['儿女亲家']
4、其他 API
// 获取当前数据表
relationship.data
// 获取当前数据量
relationship.dataCount
// 用户自定义模式
relationship.setMode(mode_name,mode_data)
最后
推荐的开源项目已经收录到 GitHub
项目,欢迎 Star
:
https://github.com/chenyl8848/great-open-source-project
或者访问网站,进行在线浏览:
https://chencoding.top:8090/#/
大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!
来源:juejin.cn/post/7344573753538330678
实现抖音 “视频无限滑动“效果
前言
在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅
不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"
这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动
的效果,干货满满
第一篇:200行代码实现类似Swiper.js的轮播组件
第三篇:Vue 路由使用介绍以及添加转场动画
第四篇:Vue 有条件路由缓存,就像传统新闻网站一样
第五篇:Github Actions 部署 Pages、同步到 Gitee、翻译 README 、 打包 docker 镜像
如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件
最终效果
在线预览:dy.ttentau.top/
Github地址:github.com/zyronon/dou…
实现原理
无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList
里面永远只有 N
个 SlideItem
,就要在滑动时不断的删除和增加 SlideItem
。
滑动时调整 SlideList
的偏移量 translateY
的值,以及列表里那几个 SlideItem
的 top
值,就可以了
为什么要调整 SlideList
的偏移量 translateY
的值同时还要调整 SlideItem
的 top
值呢?
因为 translateY
只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY
值就可以了,上滑了几页就减几页的高度,下滑同理
但是如果整个列表向前移动了一页,同时前面的 SlideItem
也少了一个,,那么最终效果就是移动了两页...因为 塌陷
了一页
这显然不是我们想要的,所以我们还需要同时调整 SlideItem
的 top
值,加上前面少的 SlideItem
的高度,这样才能显示出正常的内容
步骤
定义
virtualTotal
:页面中同时存在多少个 SlideItem
,默认为 5
。
//页面中同时存在多少个SlideItem
virtualTotal: {
type: Number,
default: () => 5
},
设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10
条,有的要求同时存在 5
条即可。
不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。
如果只同时存在 5
条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3
条,刚开始除外),我们可能来不及添加新的视频到最后
render
:渲染函数,SlideItem
内显示什么由render
返回值决定
render: {
type: Function,
default: () => {
return null
}
},
之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。
最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList
中
list
:数据列表,外部传入
list: {
type: Array,
default: () => {
return []
}
},
我们从 list
中取出数据,然后调用并传给 render
函数,将其返回值插入到 SlideList中
初始化
watch(
() => props.list,
(newVal, oldVal) => {
//新数据长度比老数据长度小,说明是刷新
if (newVal.length < oldVal.length) {
//从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中
insertContent()
} else {
//没数据就直接插入
if (oldVal.length === 0) {
insertContent()
} else {
// 走到这里,说明是通过接口加载了下一页的数据,
// 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
// 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom
// 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑
}
}
}
)
用 watch
监听 list
是因为它一开始不一定有值,通过接口请求之后才有值
同时当我们下滑 加载更多
时,也会触发接口请求新的数据,用 watch
可以在有新数据时,多添加几条到 SlideList
的最后面,这样用户快速滑动也不怕了
如何滑动
这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件
滑动结束
判断滑动的方向
当我们向上滑动时,需要删除最前面的 dom
,然后在最后面添加一个 dom
下滑时反之
slideTouchEnd(e, state, canNext, (isNext) => {
if (props.list.length > props.virtualTotal) {
//手指往上滑(即列表展示下一条视频)
if (isNext) {
//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
} else {
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
}
}
})
手指往上滑(即列表展示下一条视频)
- 首先判断是否要加载更多,快到列表末尾时就要加载更多数据了
- 再判断是否符合
腾挪
的条件,即当前位置要大于half
,且小于列表长度减half
。 - 在最后面添加一个
dom
- 删除最前面的
dom
- 将所有
dom
设置为最新的top
值(原因前面有讲,因为删除了最前面的dom
,导致塌陷一页,所以要加上删除dom
的高度)
let half = (props.virtualTotal - 1) / 2
//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
emit('loadMore')
}
//是否符合 `腾挪` 的条件
if (state.localIndex > half && state.localIndex < props.list.length - half) {
//在最后面添加一个 `dom`
let addItemIndex = state.localIndex + half
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
if (!res) {
slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
}
//删除最前面的 `dom`
let index = slideListEl.value
.querySelector(`.${itemClassName}:first-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()
slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}
手指往下滑(即列表展示上一条视频)
逻辑和上滑都差不多,不过是反着来而已
- 再判断是否符合
腾挪
的条件,和上面反着 - 在最前面添加一个
dom
- 删除最后面的
dom
- 将所有
dom
设置为最新的top
值
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {
let addIndex = state.localIndex - half
if (addIndex >= 0) {
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)
if (!res) {
slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))
}
}
let index = slideListEl.value
.querySelector(`.${itemClassName}:last-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()
slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}
其他问题
为什么不直接用 v-for
直接生成 SlideItem
呢?
如果内容不是视频就可以。要删除或者新增时,直接操作 list
数据源,这样省事多了
如果内容是视频,修改 list
时,Vue
会快速的替换 dom
,正在播放的视频,突然一下从头开始播放了😅😅😅
如何获取 Vue
组件的最终 dom
有两种方式,各有利弊
- 用
Vue
的render
方法
- 优点:只是渲染一个
VNode
而已,理论上讲内存消耗更少。 - 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅
- 优点:只是渲染一个
- 用
Vue
的createApp
方法再创建一个Vue
的实例
- 和上面相反😅
import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'
/**
* 获取Vue组件渲染之后的dom元素
* @param item
* @param index
* @param play
*/
function getInsEl(item, index, play = false) {
// console.log('index', cloneDeep(item), index, play)
let slideVNode = props.render(item, index, play, props.uniqueId)
const parent = document.createElement('div')
//TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
if (import.meta.env.PROD) {
parent.classList.add('slide-item')
parent.setAttribute('data-index', index)
//将Vue组件渲染到一个div上
vueRender(slideVNode, parent)
appInsMap.set(index, {
unmount: () => {
vueRender(null, parent)
parent.remove()
}
})
return parent
} else {
//创建一个新的Vue实例,并挂载到一个div上
const app = createApp({
render() {
return <SlideItem data-index={index}>{slideVNode}</SlideItem>
}
})
const ins = app.mount(parent)
appInsMap.set(index, app)
return ins.$el
}
}
总结
原理其实并不难。主要是一开始可能会用 v-for
去弄,折腾半天发现不行。v-for
不行,就只能想想怎么把 Vue
组件搞到 html
里面去,又去研究如何获取 Vue
组件的最终 dom
,又查了半天资料,Vue
官方文档也不写,还得去翻 api
,麻了
结束
以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号 前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~
来源:juejin.cn/post/7361614921519054883
autohue.js:让你的图片和背景融为一体,绝了!
需求
先来看这样一个场景,拿一个网站举例
这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是:
它的宽度只有 1440,且 background-size 设置的是 contain ,即等比例缩放,那么可以断定它两边的蓝色是依靠背景色填充的。
那么问题来了,这是一个 轮播banner,如果希望添加一张不是蓝色的图片呢?难道要给每张图片提前标注好背景颜色吗?这显然是非常死板的做法。
所以需要从图片中提取到图片的主题色,当然这对于 js 来说,也不是什么难事,市面上已经有众多的开源库供我们使用。
探索
首先在网络上找到了以下几个库:
- color-thief 这是一款基于 JavaScript 和 Canvas 的工具,能够从图像中提取主要颜色或代表性的调色板
- vibrant.js 该插件是 Android 支持库中 Palette 类的 JavaScript 版本,可以从图像中提取突出的颜色
- rgbaster.js 这是一段小型脚本,可以获取图片的主色、次色等信息,方便实现一些精彩的 Web 交互效果
我取最轻量化的 rgbaster.js(此库非常搞笑,用TS编写,npm 包却没有指定 types) 来测试后发现,它给我在一个渐变色图片中,返回了七万多个色值,当然,它准确的提取出了面积最大的色值,但是这个色值不是图片边缘的颜色,导致设置为背景色后,并不能完美的融合。
另外的插件各位可以参考这几篇文章:
- 文章1:blog.csdn.net/weixin_4299…
- 文章2:juejin.cn/post/684490…
- 文章3:http://www.zhangxinxu.com/wordpress/2…
可以发现,这些插件主要功能就是取色,并没有考虑实际的应用场景,对于一个图片颜色分析工具来说,他们做的很到位,但是在大多数场景中,他们往往是不适用的。
在文章 2 中,作者对比了三款插件对于图片容器背景色的应用,看起来还是 rgbaster 效果好一点,但是我们刚刚也拿他试了,它并不能适用于颜色复杂度高的、渐变色的图片。
思考
既然又又又没有人做这件事,正所谓我不入地狱谁入地狱,我手写一个
整理一下需求,我发现我希望得到的是:
- 图片的主题色(面积占比最大)
- 次主题色(面积占比第二大)
- 合适的背景色(即图片边缘颜色,渐变时,需要边缘颜色来设置背景色)
这样一来,就已经可以覆盖大部分需求了,1+2 可以生成相关的 主题 TAG、主题背景,3 可以使留白的图片容器完美融合。
开搞
⚠⚠ 本小节内容非常硬核,如果不想深究原理可以直接跳过,文章末尾有用法和效果图 ⚠⚠
思路
首先需要避免上面提到的插件的缺点,即对渐变图片要做好处理,不能取出成千上万的颜色,体验太差且实用性不强,对于渐变色还有一点,即在渐变路径上,每一点的颜色都是不一样的,所以需要将他们以一个阈值分类,挑选出一众相近色,并计算出一个平均色,这样就不会导致主题色太精准进而没有代表性。
对于背景色,需要按情况分析,如果只是希望做一个协调的页面,那么大可以直接使用主题色做渐变过渡或蒙层,也就是类似于这种效果
但是如果希望背景与图片完美衔接,让人看不出图片边界的感觉,就需要单独对边缘颜色取色了。
最后一个问题,如果图片分辨率过大,在遍历像素点时会非常消耗性能,所以需要降低采样率,虽然会导致一些精度上的丢失,但是调整为一个合适的值后应该基本可用。
剩余的细节问题,我会在下面的代码中解释
使用 JaveScript 编码
接下来我将详细描述 autohue.js 的实现过程,由于本人对色彩科学
不甚了解,如有解释不到位或错误,还请指出。
首先编写一个入口主函数,我目前考虑到的参数应该有:
export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions)
type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
interface autoColorPickerOptions {
/**
* - 降采样后的最大尺寸(默认 100px)
* - 降采样后的图片尺寸不会超过该值,可根据需求调整
* - 降采样后的图片尺寸越小,处理速度越快,但可能会影响颜色提取的准确性
**/
maxSize?: number
/**
* - Lab 距离阈值(默认 10)
* - 低于此值的颜色归为同一簇,建议 8~12
* - 值越大,颜色越容易被合并,提取的颜色越少
* - 值越小,颜色越容易被区分,提取的颜色越多
**/
threshold?: number | thresholdObj
}
概念解释 Lab ,全称:
CIE L*a*b
,CIE L*a*b*
是CIE XYZ
色彩模式的改进型。它的“L”(明亮度),“a”(绿色到红色)和“b”(蓝色到黄色)代表许多的值。与XYZ比较,CIE L*a*b*
的色彩更适合于人眼感觉的色彩,正所谓感知均匀
然后需要实现一个正常的 loadImg 方法,使用 canvas 异步加载图片
function loadImage(imageSource: HTMLImageElement | string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
let img: HTMLImageElement
if (typeof imageSource === 'string') {
img = new Image()
img.crossOrigin = 'Anonymous'
img.src = imageSource
} else {
img = imageSource
}
if (img.complete) {
resolve(img)
} else {
img.onload = () => resolve(img)
img.onerror = (err) => reject(err)
}
})
}
这样我们就获取到了图片对象。
然后为了图片过大,我们需要进行降采样处理
// 利用 Canvas 对图片进行降采样,返回 ImageData 对象
function getImageDataFromImage(img: HTMLImageElement, maxSize: number = 100): ImageData {
const canvas = document.createElement('canvas')
let width = img.naturalWidth
let height = img.naturalHeight
if (width > maxSize || height > maxSize) {
const scale = Math.min(maxSize / width, maxSize / height)
width = Math.floor(width * scale)
height = Math.floor(height * scale)
}
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('无法获取 Canvas 上下文')
}
ctx.drawImage(img, 0, 0, width, height)
return ctx.getImageData(0, 0, width, height)
}
概念解释,降采样:降采样(Downsampling)是指在图像处理中,通过减少数据的采样率或分辨率来降低数据量的过程。具体来说,就是在保持原始信息大致特征的情况下,减少数据的复杂度和存储需求。这里简单理解为将图片强制压缩为 100*100 以内,也是 canvas 压缩图片的常见做法。
得到图像信息后,就可以对图片进行像素遍历处理了,正如思考中提到的,我们需要对相近色提取并取平均色,并最终获取到主题色、次主题色。
那么问题来了,什么才算相近色,对于这个问题,在 常规的 rgb 中直接计算是不行的,因为它涉及到一个感知均匀的问题
概念解释,感知均匀:XYZ系统和在它的色度图上表示的两种颜色之间的距离与颜色观察者感知的变化不一致,这个问题叫做感知均匀性(perceptual uniformity)问题,也就是颜色之间数字上的差别与视觉感知不一致。由于我们需要在颜色簇中计算出平均色,那么对于人眼来说哪些颜色是相近的?此时,我们需要把 sRGB 转化为 Lab 色彩空间(感知均匀的),再计算其欧氏距离,在某一阈值内的颜色,即可认为是相近色。
所以我们首先需要将 rgb 转化为 Lab 色彩空间
// 将 sRGB 转换为 Lab 色彩空间
function rgbToLab(r: number, g: number, b: number): [number, number, number] {
let R = r / 255,
G = g / 255,
B = b / 255
R = R > 0.04045 ? Math.pow((R + 0.055) / 1.055, 2.4) : R / 12.92
G = G > 0.04045 ? Math.pow((G + 0.055) / 1.055, 2.4) : G / 12.92
B = B > 0.04045 ? Math.pow((B + 0.055) / 1.055, 2.4) : B / 12.92
let X = R * 0.4124 + G * 0.3576 + B * 0.1805
let Y = R * 0.2126 + G * 0.7152 + B * 0.0722
let Z = R * 0.0193 + G * 0.1192 + B * 0.9505
X = X / 0.95047
Y = Y / 1.0
Z = Z / 1.08883
const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
const fx = f(X)
const fy = f(Y)
const fz = f(Z)
const L = 116 * fy - 16
const a = 500 * (fx - fy)
const bVal = 200 * (fy - fz)
return [L, a, bVal]
}
这个函数使用了看起来很复杂的算法,不必深究,这是它的大概解释:
- 获取到 rgb 参数
- 转化为线性 rgb(移除 gamma矫正),常量 0.04045 是sRGB(标准TGB)颜色空间中的一个阈值,用于区分非线性和线性的sRGB值,具体来说,当sRGB颜色分量大于0.04045时,需要通过 gamma 校正(即采用
((R + 0.055) / 1.055) ^ 2.4
)来得到线性RGB;如果小于等于0.04045,则直接进行线性转换(即R / 12.92
) - 线性RGB到XYZ空间的转换,转换公式如下:
X = R * 0.4124 + G * 0.3576 + B * 0.1805
Y = R * 0.2126 + G * 0.7152 + B * 0.0722
Z = R * 0.0193 + G * 0.1192 + B * 0.9505
- 归一化XYZ值,为了参考白点(D65),标准白点的XYZ值是
(0.95047, 1.0, 1.08883)
。所以需要通过除以这些常数来进行归一化 - XYZ到Lab的转换,公式函数:const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
- 计算L, a, b 分量
L:亮度分量(表示颜色的明暗程度)
L = 116 * fy - 16
a:绿色到红色的色差分量
a = 500 * (fx - fy)
b:蓝色到黄色的色差分量
b = 200 * (fy - fz)
接下来实现聚类算法
/**
* 对满足条件的像素进行聚类
* @param imageData 图片像素数据
* @param condition 判断像素是否属于指定区域的条件函数(参数 x, y)
* @param threshold Lab 距离阈值,低于此值的颜色归为同一簇,建议 8~12
*/
function clusterPixelsByCondition(imageData: ImageData, condition: (x: number, y: number) => boolean, threshold: number = 10): Cluster[] {
const clusters: Cluster[] = []
const data = imageData.data
const width = imageData.width
const height = imageData.height
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (!condition(x, y)) continue
const index = (y * width + x) * 4
if (data[index + 3] === 0) continue // 忽略透明像素
const r = data[index]
const g = data[index + 1]
const b = data[index + 2]
const lab = rgbToLab(r, g, b)
let added = false
for (const cluster of clusters) {
const d = labDistance(lab, cluster.averageLab)
if (d < threshold) {
cluster.count++
cluster.sumRgb[0] += r
cluster.sumRgb[1] += g
cluster.sumRgb[2] += b
cluster.sumLab[0] += lab[0]
cluster.sumLab[1] += lab[1]
cluster.sumLab[2] += lab[2]
cluster.averageRgb = [cluster.sumRgb[0] / cluster.count, cluster.sumRgb[1] / cluster.count, cluster.sumRgb[2] / cluster.count]
cluster.averageLab = [cluster.sumLab[0] / cluster.count, cluster.sumLab[1] / cluster.count, cluster.sumLab[2] / cluster.count]
added = true
break
}
}
if (!added) {
clusters.push({
count: 1,
sumRgb: [r, g, b],
sumLab: [lab[0], lab[1], lab[2]],
averageRgb: [r, g, b],
averageLab: [lab[0], lab[1], lab[2]]
})
}
}
}
return clusters
}
函数内部有一个 labDistance 的调用,labDistance 是计算 Lab 颜色空间中的欧氏距离的
// 计算 Lab 空间的欧氏距离
function labDistance(lab1: [number, number, number], lab2: [number, number, number]): number {
const dL = lab1[0] - lab2[0]
const da = lab1[1] - lab2[1]
const db = lab1[2] - lab2[2]
return Math.sqrt(dL * dL + da * da + db * db)
}
概念解释,欧氏距离:Euclidean Distance,是一种在多维空间中测量两个点之间“直线”距离的方法。这种距离的计算基于欧几里得几何中两点之间的距离公式,通过计算两点在各个维度上的差的平方和,然后取平方根得到。欧氏距离是指n维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。
总的来说,这个函数采用了类似 K-means 的聚类方式,将小于用户传入阈值的颜色归为一簇,并取平均色(使用 Lab 值)。
概念解释,聚类算法:Clustering Algorithm 是一种无监督学习方法,其目的是将数据集中的元素分成不同的组(簇),使得同一组内的元素相似度较高,而不同组之间的元素相似度较低。这里是将相近色归为一簇。
概念解释,颜色簇:簇是聚类算法中一个常见的概念,可以大致理解为 "一类"
得到了颜色簇集合后,就可以按照count大小来判断哪个是主题色了
// 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb)
现在我们已经获取到了主题色、次主题色 🎉🎉🎉
接下来,我们继续计算边缘颜色
按照同样的方法,只是把阈值设小一点,我这里直接设置为 1 (threshold.top 等都是1)
// 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor
const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor
const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor
const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor
这样我们就获取到了上下左右四条边的颜色 🎉🎉🎉
这样大致的工作就完成了,最后我们将需要的属性导出给用户,我们的主函数最终长这样:
/**
* 主函数:根据图片自动提取颜色
* @param imageSource 图片 URL 或 HTMLImageElement
* @returns 返回包含主要颜色、次要颜色和背景色对象(上、右、下、左)的结果
*/
export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions): Promise<AutoHueResult> {
const { maxSize, threshold } = __handleAutoHueOptions(options)
const img = await loadImage(imageSource)
// 降采样(最大尺寸 100px,可根据需求调整)
const imageData = getImageDataFromImage(img, maxSize)
// 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb)
// 定义边缘宽度(单位像素)
const margin = 10
const width = imageData.width
const height = imageData.height
// 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor
const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor
const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor
const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor
return {
primaryColor,
secondaryColor,
backgroundColor: {
top: topColor,
right: rightColor,
bottom: bottomColor,
left: leftColor
}
}
}
还记得本小节一开始提到的参数吗,你可以自定义 maxSize(压缩大小,用于降采样)、threshold(阈值,用于设置簇大小)
为了用户友好,我还编写了 threshold 参数的可选类型:number | thresholdObj
type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
可以单独设置主阈值、上下左右四边阈值,以适应更个性化的情况。
autohue.js 诞生了
名字的由来:秉承一贯命名习惯,auto 家族成员又多一个,与颜色有关的单词有好多个,我取了最短最好记的一个 hue(色相),也比较契合插件用途。
此插件已在 github 开源:GitHub autohue.js
npm 主页:NPM autohue.js
在线体验:autohue.js 官方首页
安装与使用
pnpm i autohue.js
import autohue from 'autohue.js'
autohue(url, {
threshold: {
primary: 10,
left: 1,
bottom: 12
},
maxSize: 50
})
.then((result) => {
// 使用 console.log 打印出色块元素s
console.log(`%c${result.primaryColor}`, 'color: #fff; background: ' + result.primaryColor, 'main')
console.log(`%c${result.secondaryColor}`, 'color: #fff; background: ' + result.secondaryColor, 'sub')
console.log(`%c${result.backgroundColor.left}`, 'color: #fff; background: ' + result.backgroundColor.left, 'bg-left')
console.log(`%c${result.backgroundColor.right}`, 'color: #fff; background: ' + result.backgroundColor.right, 'bg-right')
console.log(`%clinear-gradient to right`, 'color: #fff; background: linear-gradient(to right, ' + result.backgroundColor.left + ', ' + result.backgroundColor.right + ')', 'bg')
bg.value = `linear-gradient(to right, ${result.backgroundColor.left}, ${result.backgroundColor.right})`
})
.catch((err) => console.error(err))
最终效果
复杂边缘效果
纵向渐变效果(这里使用的是 left 和 right 边的值,可能使用 top 和 bottom 效果更佳)
纯色效果(因为单独对边缘采样,所以无论图片内容多复杂,纯色基本看不出边界)
突变边缘效果(此时用css做渐变蒙层应该效果会更好)
横向渐变效果(使用的是 left 和 right 的色值),基本看不出边界
参考资料
- zhuanlan.zhihu.com/p/370371059
- baike.baidu.com/item/%E5%9B…
- baike.baidu.com/item/%E6%A0…
- zh.wikipedia.org/wiki/%E6%AC…
- blog.csdn.net/weixin_4256…
- zh.wikipedia.org/wiki/K-%E5%…
- blog.csdn.net/weixin_4299…
- juejin.cn/post/684490…
番外
Auto 家族的其他成员
- Auto-Plugin/autofit.js autofit.js 迄今为止最易用的自适应工具
- Auto-Plugin/autolog.js autolog.js 轻量化小弹窗
- Auto-Plugin/autouno autouno 直觉的UnoCSS预设方案
- Auto-Plugin/autohue.js 本品 一个自动提取图片主题色让图片和背景融为一体的工具
来源:juejin.cn/post/7471919714292105270
记一次 CDN 流量被盗刷经历
先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。
600多G流量,100多万次请求。
怎么发现的
先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了。
抱着看热闹的心情点开阅读了。。。心想,看看自己的中招没,结果就真中招了 🍉。
被盗刷资源分析
笔者在 缤纷云
,七牛云
,又拍云
都有存放一些图片资源。本次中招的是 缤纷云
,下面是被刷的资源。
IP来源
查了几个 IP 和文章里描述的大差不差,都是来自山西联通的请求。
大小流量计算
按日志时间算的话,QPS 大概在 20 左右,单文件 632 K,1分钟大概就760MB ,1小时约 45G 左右。
看了几天前的日志,都是 1 小时刷 40G 就停下,从 9 点左右开始,刷到 12 点。
07-09 | 07-08 |
---|---|
![]() | ![]() |
但是 10 号的就变多了,60-70 GB 1次了。也是这天晚上才开始做的反制,不知道是不是加策略的时候影响到它计算流量大小了 😝。
反制手段
Referer 限制
通过观察这些资源的请求头,发现 Referer
和请求资源一致,通常情况下,不应该这样,应该是笔者的博客地址https://sugarat.top
。
于是第一次就限制了 Referer
头不能为空,同时将 cdn.bitiful.sugarat.top
的来源都拉黑。
这个办法还比较好使,后面的请求都给 403 了。
但这个还是临时解决方案,在 V 站上看到讨论,说资源是人为筛选的,意味着 Referer 换个资源还是会发生变化。
IP 限制
有 GitHub 仓库 unclemcz/ban-pcdn-ip 收集了此次恶意刷流量的 IP。
CDN 平台一般支持按 IP 或 IP 段屏蔽请求(虽然后者可能会屏蔽一些正常请求),可以将 IP 段配置到平台上,这样就能限制掉这些 IP 的请求。
缤纷云上这块限制还比较弱,我就直接把缤纷云的 CDN 直接关了,七牛云和又拍云上都加上了 IP 和 地域运营商的限制,等这阵风头过去再恢复。
七牛云 | 又拍云 |
---|---|
![]() | ![]() |
限速
限制单 IP 的QPS和峰值流量。
但是这个只能避免说让它刷得慢一点,还是不治本。
最后
用了CDN的话,日常还是多看看,能加阈值控制的平台优先加上,常规的访问控制防盗链的啥的安排上。
来源:juejin.cn/post/7390678994998526003
新来的总监,把闭包讲得那叫一个透彻
😃文章首发于公众号[精益码农]。
闭包作为前端面试的必考题目,常让1-3年工作经验的Javascripter感到困惑,我的主力语言C#/GO均有闭包。
1. 闭包:关键点在于函数是否捕获了其外部作用域的变量
闭包的形成: 定义函数时, 函数引用了其外部作用域的变量, 之后就形成了闭包。
闭包的结果: 引用的变量和定义的函数都会一同存在(即使已经脱离了函数定义/引用的变量的作用域),一直到闭包被消灭。
public static Action Closure()
{
var x = 1;
Action action= () =>
{
var y = 1;
var result = x + y;
Console.WriteLine(result);
x++;
};
return action;
}
public static void Main() {
var a=Closure();
a();
a();
}
// 调用函数输出
2
3
委托action是一个函数,它使用了“x”这个外部作用域的变量(x变量不是函数内局部变量),变量引用将被捕获形成闭包。
即使action被返回了(即使“x”已经脱离了它被引用时的作用域环境(Closure)),但是两次执行能输出2,3 说明它脱离原引用环境仍然能用。
当你在代码调试器(debugger)里观察“action”时,可以看到一个Target属性,里面封装了捕获的x变量:
实际上,委托,匿名函数和lambda都是继承自Delegate类,
Delegate不允许开发者直接使用,只有编译器才能使用, 也就是说delegate Action都是语法糖。
- Method:MethodInfo反射类型- 方法执行体
- Target:当前委托执行的对象,这些语法糖由编译器生成了继承自Delegate类型的对象,包含了捕获的自由变量。
再给一个反例:
public class Program
{
private static int x = 1; // 静态字段
public static void Main()
{
var action = NoClosure();
action();
action();
}
public static Action NoClosure(){
Action action=()=>{
var y =1;
var sum = x+y;
Console.WriteLine($"sum = { sum }");
x++;
};
return action;
}
}
x 是静态字段,在程序中有独立的存储区域, 不在线程的函数堆栈区,不属于某个特定的作用域。
匿名函数使用了 x,但没有捕获外部作用域的变量,因此不构成闭包, Target
属性对象无捕获的字段。
从编程设计的角度:闭包开创了除全局变量传值, 函数参数传值之外的第三种变量使用方式。
2. 闭包的形成时机和效果
闭包是词法闭包的简称,维基百科上是这样定义的:
“在计算机科学中,闭包是在词法环境中绑定自由变量的一等函数”。
闭包的形成时机:
- 一等函数
- 外部作用域变量
闭包的形态:
会捕获闭包函数内引用的外部作用域变量, 一直持有,直到闭包函数不再使用被销毁。
内部实现是形成了一个对象(包含执行函数和捕获的变量,参考Target对象), 只有形成堆内存,才有后续闭包销毁的行为,当闭包这个对象不再被引用时,闭包被GC清理。
闭包的作用周期:
离不开作用域这个概念,函数理所当然管控了函数内的局部变量作用域,但当它引用了外部有作用域的变量时, 就形成了闭包函数。
当闭包(例如一个委托或 lambda 表达式)不再被任何变量、对象或事件持有引用时,它就变成了“不可达”对象, 闭包被gc清理,其实就是堆内存被清理。
2.1 一等函数
一等函数很容易理解,就是在各语言, 函数被认为是某类数据类型, 定义函数就成了定义变量, 函数也可以像变量一样被传递。
很明显,在C#中我们常使用的匿名函数、lambda表达式都是一等函数。
Func<string,string> myFunc = delegate(string var1)
{
return "some value";
};
Func<string,string> myFunc = var1 => "some value";
string myVar = myFunc("something");
2.2 自由变量
在函数中被引用的外部作用域变量, 注意, 这个变量是外部有作用域的变量,也就说排除全局变量(这些变量在程序的独立区域, 不属于任何作用域)。
public void Test()
{
var myVar = "this is good";
Func<string,string> myFunc = delegate(string var1)
{
return var1 + myVar;
};
}
上面这个示例,myFunc形成了闭包,捕获了myVar这个外部作用域的变量;
即使Test函数返回了委托myFunc(脱离了定义myVar变量的作用域),闭包依然持有myVar的变量引用,
注意,引用变量,并不是使用当时变量的副本值。
我们再回过头来看结合了线程调度的闭包面试题。
3. 闭包函数关联线程调度: 依次打印连续的数字
static void Closure1()
{
for (int i = 0; i < 10; i++)
{
Task.Run(()=> Console.WriteLine(i));
}
}
每次输出数字不固定
并不是预期的 0.1.2.3.4.5.6.7.8.9
首先形成了闭包函数()=> Console.WriteLine(i)
, 捕获了外部有作用域变量i
的引用, 此处捕获的变量i相对于函数是全局变量。
但是Task调度闭包函数的时机不确定, 所以打印的是被调度时引用的变量i值。
数字符合但乱序:为每个闭包函数绑定独立变量
循环内增加局部变量, 解绑全局变量 (或者可以换成foreach,foreach相当于内部给你整了一个局部变量)。
能输出乱序的0,1,2,3,4,5,6,7,8,9
因为每次循环内产生的闭包函数捕获了对应的局部变量j,这样每个任务执行环境均独立维护了一个变量j, 这个j不是全局变量, 但是由于Task启动时机依然不确定,故是乱序。
数字符合且有序
核心是解决 Task调度问题。
思路是:一个共享变量,每个任务打印该变量自增的一个阶段,但是该自增不允许被打断。
public static void Main(string[] args)
{
var s =0;
var lo = new Program();
for (int i = 0; i < 10; i++)
{
Task.Run(()=>
{
lock(lo)
{
Console.WriteLine(s); // 依然形成了闭包函数, 之后闭包函数被线程调度
s++;
}
});
}
Thread.Sleep(2000);
} // 上面是一个明显的锁争用
3.Golang闭包的应用
gin 框架中中间件的默认形态是:
package middleware
func AuthenticationMiddleware(c *gin.Context) {
......
}
// Use方法的参数签名是这样: type HandlerFunc func(*Context), 不支持入参
router.Use(middleware.AuthenticationMiddleware)
实际实践上我们又需要给中间件传参, 闭包提供了这一能力。
func Authentication2Middleware(log *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
... 这里面可以利用log 参数。
}
}
var logger *zap.Logger
api.Use(middleware.Authentication2Middleware(logger))
总结
本文屏蔽语言差异,理清了[闭包]的概念核心: 函数引用了其外部作用域的变量,
核心特征:一等函数、自由变量,核心结果: 即使脱离了原捕获变量的原作用域,闭包函数依然持有该变量引用。
不仅能帮助我们应对多语种有关闭包的面试题, 也帮助我们了解[闭包]在通用语言中的设计初衷。
另外我们通过C# 调试器巩固了Delegate 抽象类,这是lambda表达式,委托,匿名函数的底层抽象数据结构类,包含两个重要属性 Method Target,分别表征了方法执行体、当前委托作用的对象,
可想而知,其他语言也是通过这个机制捕获闭包当中的自由变量。
来源:juejin.cn/post/7474982751365038106
Java利用Deepseek进行项目代码审查
一、为什么需要AI代码审查?
写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。

写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。
二、环境准备(5分钟搞定)
- 安装Deepseek插件(以VSCode为例):
- 插件市场搜索"Deepseek Code Review"
- 点击安装(就像安装手机APP一样简单)

- Java项目配置:
<dependency>
<groupId>com.deepseekgroupId>
<artifactId>code-analyzerartifactId>
<version>1.3.0version>
dependency>
- 安装Deepseek插件(以VSCode为例):
- 插件市场搜索"Deepseek Code Review"
- 点击安装(就像安装手机APP一样简单)
- Java项目配置:
<dependency>
<groupId>com.deepseekgroupId>
<artifactId>code-analyzerartifactId>
<version>1.3.0version>
dependency>
三、真实案例:用户管理系统漏洞检测
原始问题代码:
public class UserService {
// 漏洞1:未处理空指针
public String getUserRole(String userId) {
return UserDB.query(userId).getRole();
}
// 漏洞2:资源未关闭
public void exportUsers() {
FileOutputStream fos = new FileOutputStream("users.csv");
fos.write(getAllUsers().getBytes());
}
// 漏洞3:SQL注入风险
public void deleteUser(String input) {
Statement stmt = conn.createStatement();
stmt.execute("DELETE FROM users WHERE id = " + input);
}
}
public class UserService {
// 漏洞1:未处理空指针
public String getUserRole(String userId) {
return UserDB.query(userId).getRole();
}
// 漏洞2:资源未关闭
public void exportUsers() {
FileOutputStream fos = new FileOutputStream("users.csv");
fos.write(getAllUsers().getBytes());
}
// 漏洞3:SQL注入风险
public void deleteUser(String input) {
Statement stmt = conn.createStatement();
stmt.execute("DELETE FROM users WHERE id = " + input);
}
}
使用Deepseek审查后:
智能修复建议:
- 空指针防护 → 建议添加Optional处理
- 流资源 → 推荐try-with-resources语法
- SQL注入 → 提示改用PreparedStatement
- 空指针防护 → 建议添加Optional处理
- 流资源 → 推荐try-with-resources语法
- SQL注入 → 提示改用PreparedStatement
修正后的代码:
public class UserService {
// 修复1:Optional处理空指针
public String getUserRole(String userId) {
return Optional.ofNullable(UserDB.query(userId))
.map(User::getRole)
.orElse("guest");
}
// 修复2:自动资源管理
public void exportUsers() {
try (FileOutputStream fos = new FileOutputStream("users.csv")) {
fos.write(getAllUsers().getBytes());
}
}
// 修复3:预编译防注入
public void deleteUser(String input) {
PreparedStatement pstmt = conn.prepareStatement(
"DELETE FROM users WHERE id = ?");
pstmt.setString(1, input);
pstmt.executeUpdate();
}
}
public class UserService {
// 修复1:Optional处理空指针
public String getUserRole(String userId) {
return Optional.ofNullable(UserDB.query(userId))
.map(User::getRole)
.orElse("guest");
}
// 修复2:自动资源管理
public void exportUsers() {
try (FileOutputStream fos = new FileOutputStream("users.csv")) {
fos.write(getAllUsers().getBytes());
}
}
// 修复3:预编译防注入
public void deleteUser(String input) {
PreparedStatement pstmt = conn.prepareStatement(
"DELETE FROM users WHERE id = ?");
pstmt.setString(1, input);
pstmt.executeUpdate();
}
}
四、实现原理揭秘
Deepseek的代码审查就像"X光扫描仪",通过以下三步工作:
- 模式识别:比对数千万个代码样本
- 就像老师批改作业时发现常见错误
- 上下文理解:分析代码的"人际关系"
- 数据库连接有没有"成对出现"(打开/关闭)
- 敏感操作有没有"保镖"(权限校验)
- 智能推理:预测代码的"未来"
- 这个变量走到这里会不会变成null?
- 这个循环会不会变成"无限列车"?
五、进阶使用技巧
- 自定义审查规则(配置文件示例):
rules:
security:
sql_injection: error
performance:
loop_complexity: warning
style:
var_naming: info
- 自定义审查规则(配置文件示例):
rules:
security:
sql_injection: error
performance:
loop_complexity: warning
style:
var_naming: info
2. 与CI/CD集成(GitHub Action示例):
- name: Deepseek Code Review
uses: deepseek-ai/code-review-action@v2
with:
severity_level: warning
fail_on: error
六、开发者常见疑问
Q:AI会不会误判我的代码?
A:就像导航偶尔会绕路,Deepseek给出的是"建议"而非"判决",最终决策权在你手中
Q:处理历史遗留项目要多久?
A:10万行代码项目约需3-5分钟,支持增量扫描
七、效果对比数据
指标 | 人工审查 | Deepseek+人工 |
---|---|---|
平均耗时 | 4小时 | 30分钟 |
漏洞发现率 | 78% | 95% |
误报率 | 5% | 12% |
知识库更新速度 | 季度 | 实时 |
来源:juejin.cn/post/7473799336675639308
停止在TS中使用.d.ts文件
看到Matt Pocock 在 X 上的一个帖子提到不使用 .d.ts
文件的说法。
你赞同么?是否也应该把 .d.ts
文件都替换为 .ts
文件呢?
我们一起来看看~
.d.ts
文件的用途
首先,我们要澄清的是,.d.ts
文件并不是毫无用处的。
.d.ts
文件的用途主要用于为 JavaScript 代码提供类型描述。
.d.ts
文件是严格的蓝图,用于表示你的源代码可以使用的类型。最早可以追溯到2012年。其设计受到头文件、接口描述语言(IDL)、JSDoc 等实践的启发,可以被视为 TypeScript 版本的头文件。
.d.ts
文件只能包含声明,所以让我们通过一个代码示例来看看声明和实现之间的区别。假设我们有一个函数,它将两个数字相加:
// 声明 (.d.ts)
export function add(num1: number, num2: number): number;
// 实现 (.ts)
export function add(num1: number, num2: number): number {
return num1 + num2;
}
正如你所见,add
函数的实现实际上展示了加法的执行过程,并返回结果,而声明则没有。
那么 .d.ts
文件在实践中是如何使用的呢?
假设我们有一个 add
函数,分别在两个文件中存储声明和实现:add.d.ts
和 add.js
。
现在我们创建一个新文件 index.js
,它将实际使用 add
函数:
import { add } from "./x";
const result = add(1, 4);
console.log(result); // 输出:5
请注意,在这个 JS 文件中,add
函数具有类型安全性,因为函数在 add.d.ts
中被标注了类型声明。
替换方案 .ts
文件
我们已经了解了 .d.ts
文件的工作原理以及它们的用途。Matt 之所以认为不需要.d.ts
文件,是因为它也可以放在一个 .ts
文件中直接创建带有类型标注的实现。也就是说,拥有一个包含声明和实现的单个 add.ts
文件,等同于分别定义了 add.d.ts
和 add.js
文件。
这意味着你无需担心将声明文件与其对应的实现文件分开组织。
不过,针对类库,将 .d.ts
文件与编译后的 JavaScript 源代码一起使用,比存储 .ts
文件更高效,因为你真正需要的只是类型声明,以便用户在使用你的库时能够获得类型安全性。
这确实没错,需要强调的是,更推荐自动生成。通过更改 package.json
和 tsconfig.json
文件中的几个设置,从 .ts
文件自动生成 .d.ts
文件:
- tsconfig.json:确保添加
declaration: true
,以支持.d.ts
文件的生成。
{
"compilerOptions": {
"declaration": true,
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"strict": true
},
"include": ["src/**/*"]
}
- package.json:确保将
types
属性设置为生成的.d.ts
文件,该文件位于编译后的源代码旁边。
{
"name": "stop using d.ts",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc"
}
}
结论
.d.ts
文件中可以做到的一切,都可以在 .ts
文件中完成。
在 .ts
文件中使用 declare global {}
语法时,花括号内的内容将被视为全局环境声明,这本质上就是 .d.ts
文件的工作方式。
所以即使不使用.d.ts
文件,也可以拥有全局可访问的类型。.ts
文件在功能上既可以包含代码实现,又可以包含类型声明,而且还能实现全局的类型声明,从而在开发过程中可以更加方便地管理和使用类型,避免了在.d.ts
文件和.ts
文件之间进行复杂的协调和组织,提高了开发效率和开发体验。
另外需要注意的是,在大多数项目中,开发者会在 TypeScript 的配置文件(tsconfig.json)中将 skipLibCheck 选项设置为 true。skipLibCheck 的作用是跳过对库文件(包括 .d.ts 文件)的类型检查。当设置为 true 时,TypeScript 编译器不会对这些库文件进行严格的类型检查,从而加快编译速度。但这也会影响项目中自己编写的 .d.ts 文件。这意味着,即使 .d.ts 文件中定义的类型存在错误,TypeScript 编译器也不会报错,从而失去了类型安全性的保障。
而我们直接使用 .ts
文件,就不会有这个问题了,同事手动编写 .d.ts
文件,也会更加安全和高效。
因此,.d.ts
文件确实没有必要编写。在 99% 的情况下,.ts
文件更适合使用,可以改善开发体验,并降低库代码中出现类型错误的可能性。
怎么样??你同意他的看法么?
来源:juejin.cn/post/7463817822474682418
我们都被困在系统里
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
2020年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。
作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。
而最近的一段经历,我感觉也被困在系统里了。
起因
如果你是一个研发人员,免不了要值班、处理线上问题。当然这都很正常,每个系统都有bug或者咨询类的问题。
由于我们面临的客户比较多,加上系统有一些易用性的问题或bug,提出来的问题不少。
公司有一项政策,当客服人员提交工单之后,系统对每一个单子有超时时间,如果超出一定时间你还未提交,罚款50元。
挺奇葩的,谁能保证1个小时就一定能排查出问题呢?
于是就会有一个场景,如果赶上问题多,一下子来5、6个工单,恰巧遇到不容易排查的耽误时间的话,处理后面的工单,都面临着超时的压力。
之前同事们对值班这件事,充满了怨言,大多都会吐槽几个点
- 系统bug太多了,又是刚刚某某需求改出来的问题
- 需求设计不合理,很多奇怪的操作导致了系统问题
- 客服太懒了,明明可以自己搜,非得提个工单问
- 基础设施差,平台不好用
我不太爱吐槽,但当工单一下子来的太多的时候,我不由自主的陷入机械的处理问题中,压缩思考的时间,只求不要超时就好。
明明系统有很多问题需要解决、流程也有很多可以优化,可当系统给到我们的压力越来越多时,我们便不去思考,陷入只有吐槽、怨言和避免罚款的状态。
当陷入了系统的支配,只能被动接受,甚至有了一些怨言的时候,我意识到,这样的状态,是有问题的。
被困住的打工人
外卖员为什么不遵守交通规则呢?
外卖小哥为了多赚钱、避免处罚,我之前也很不理解,为什么为了避免处罚,连自己的生命安全都可以置之不顾。
但转念一想,我们虽然不用在马路上奔波,可受到“系统”的压力,可是一点也不比外卖员少。
大家一定有过类似的经历:你骑车或者开车去上班,距离打卡时间所剩无几,你在迟到的边缘疯狂试探,可能多一个红绿灯,你就赶不上了,这时候你会不会狠踩几脚油门、闯一个黄灯,想要更快一点呢?
但随着裁员、降本增效、各类指标的压力越来越大,我们被迫不停的内卷,不断压榨自己,才能满足职场要求越来越严格的“算法”,比如,每半年一次的绩效考核,月度或者季度的OKR、KPI,还有处理不完的线上问题、事故,充斥在我们的脑海里面。
其实我们何尝不是“外卖员”呢?外卖员是为了不被扣钱,我们是为了年终奖、晋升罢了。
所以回过头来看,其实我们早早的就被困在“系统”中了,为了满足系统的要求,我们不得不埋头苦干,甚至加班透支身体,作出很多非常短线思维的事情。
但为什么,我之前从来没有过被困住的感觉,为什么我现在才回过神来,意识到这个问题呢?
我想,大概是越简单的事情,你作出的反应就越快、越激烈。而越复杂、时间越长的事情,你作出的反应就越缓慢,甚至忽略掉。
比如上班即将迟到的你,你会立刻意识到,迟到可能会受到处罚。但是年终评估你的绩效目标时,你或许只有在最后的几个月才会意识到,某某事情没完成,年终奖或许要少几个月而感到着急。
积极主动
最近正好在读《高效能人士的七个习惯》,其中第一个习惯就是积极主动。
书中说到:人性的本质是主动而非被动的,人类不仅能针对特定环境选择回应方式,更能主动创造有利的环境。
我们面对的问题可以分为三类:
- 可直接控制的(问题与自身的行为有关)
- 可间接控制的(问题与他人的行为有关)
- 无法控制的(我们无能为力的问题,例如我们的过去或现实的环境)
对于这三类问题,积极主动的话,应该如何加以解决呢。
可直接控制的问题
针对可直接控制的问题,可以通过培养正确习惯来解决。
从程序员角度来看,线上bug多,可以在开发前进行技术设计,上线前进行代码CR,自动化测试,帮助自己避免低级的问题。
面对处理工单时咨询量特别多的问题,随手整理个文档出来,放到大家都可以看到的地方。
可间接控制的
对于可间接控制的,我们可以通过改进施加影响的方法来解决。
比如流程机制的不合理,你可以通过向上反馈的方式施加影响,提出自己的建议而不是吐槽。
无法控制的
对于无法控制的,我们要做的就是改变面部曲线,以微笑、真诚与平和来接受现实。
虽然反馈问题的人或许能力参差不齐,导致工单量很多,但我们意识到这一点是无法避免的,不如一笑而过,这样才不至于被问题左右。
说在最后
好了,文章到这里就要结束了。
最近由于值班的原因,陷入了一段时间的无效忙碌中,每一天都很累,几乎抽不出时间来思考,所以更新的频率也降下来了。
但还好,及时的意识到问题,把最近的一点思考分享出来,希望我们每个人都不会被“系统”困住。
欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~
本篇文章是第41篇原创文章,2024目标进度41/100,欢迎有趣的你,关注我。
来源:juejin.cn/post/7385098943942656054
再见Typora,这款大小不到3M的Markdown编辑器,满足你的所有幻想!
Typora 是一款广受欢迎的 Markdown 编辑器,以其所见即所得的编辑模式和优雅的界面而闻名,长期以来是许多 Markdown 用户的首选。然而,从 2021 年起,Typora 不再免费,采用一次性付费授权模式。虽然费用不高,但对于轻量使用者或预算有限的用户可能并不友好。
今天来推荐一款开源替代品,一款更加轻量化、注重隐私且完全免费的 Markdown 编辑器,专为 macOS 用户开发。
项目介绍
MarkEdit 是一款轻量级且高效的 Markdown 编辑器,专为 macOS 用户设计,安装包大小不到 3 MB。它以简洁的设计和流畅的性能,成为技术写作、笔记记录、博客创作以及项目文档编辑的理想工具。无论是编写技术文档、撰写博客文章,还是编辑 README 文件,MarkEdit 都能以快速响应和便捷操作帮助用户专注于内容创作。
根据官方介绍,MarkEdit 免费的原因如下:
MarkEdit 是完全免费和开源的,没有任何广告或其他服务。我们之所以发布它,是因为我们喜欢它,我们不期望从中获得任何收入。
功能特性
MarkEdit 的核心功能围绕 Markdown 写作展开,注重实用与高效,以下是其主要特性:
- 实时语法高亮:清晰呈现 Markdown 的结构,让文档层次分明。
- 多种主题:提供不同的配色方案,总有一种适合你。
- 分屏实时预览:支持所见即所得的写作体验,左侧编辑,右侧实时渲染。
- 文件树视图:适合多文件项目管理,方便在项目间快速切换。
- 文档导出:支持将 Markdown 文件导出为 PDF 或 HTML 格式,方便分享和发布。
- CodeMirror 插件支持:通过插件扩展功能,满足更多 Markdown 使用需求。
- ......
MarkEdit 的特点让它能胜任多种写作场合:
- 技术文档:帮助开发者快速记录项目相关文档。
- 博客创作:支持实时预览,让博客排版更直观。
- 个人笔记:轻量且启动迅速,适合日常记录。
- 项目文档:文件管理功能让多文件项目的编辑更加高效。
效果展示
多种主题风格,总有一种适合你:
实时预览,让博客排版更直观:
设置界面,清晰直观:
安装方法
方法 1:安装包下载
找到 MarkEdit 的最新版本安装包下载使用即可,地址:github.com/MarkEdit-ap… 。
方法 2:通过 Homebrew
在终端中运行相关命令即可完成安装。
brew install markedit
注意:MarkEdit 支持 macOS Sonoma 和 macOS Sequoia, 历史兼容版本包括 macOS 12 和 macOS 13。
总结
MarkEdit 是一款专注于 Markdown 写作的 macOS 原生编辑器,以简洁、高效、隐私友好为核心设计理念。无论是日常写作还是处理复杂文档,它都能提供流畅的体验和强大的功能。对于追求高效写作的 macOS 用户来说,MarkEdit 是一个不可多得的优秀工具。
项目地址:github.com/MarkEdit-ap… 。
来源:juejin.cn/post/7456685819047919651
前端适配:你一般用哪种方案?
前言
最近在公司改bug,突然发现上一个前端留下的毛病不少,页面存在各种适配问题,为此甲方爸爸时常提出宝贵意见!
你的页面是不是时常是这样:
侧边栏未收缩时:
收缩后:
这样(缩小挤成一坨):
又或是这样:
那么废话不多说,今天由我不是程序猿kk为大家讲解一些前端必备知识:适配工作。
流式布局
学会利用相对单位(例如百分比,vh或是vw),而不是只会用px一类固定单位设计布局,前言中提到的收缩后多出一大块空白,就是由于写死了宽度,例如1000px或是89vw,那么当侧边栏进行收缩,右边内容宽度还是只有89个vw,因此我们可以将其更改为100%,这样不论侧边栏是否收缩,内容都会占满屏幕的全部。
.map {
width: 100%;
height: 90vh;
position: relative;
}
rem和第三方插件
什么是rem
rem
与em不同,rem会根据html的根节点字体大小进行变换,例如1rem就是一个字体大小那么大,比如根大小font size
为12px,那么1rem即12px,大家可以在网上寻找单位换算工具进行换算(从设计稿的px到rem)或是下载相关插件例如gulp-px3rem
,这样在不同分辨率,不同缩放比的电脑下都能够轻松应对了。
使用
第三方插件,例如做移动端适配的flexible.js,lib-flexible库
,其核心原理就是rem,我们需要做的就是根据不同屏幕计算出不同的fontsize,而页面中元素都是用rem做单位,据此实现了自适应
源码:
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial-scale=([d.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial-dpr=([d.]+)/);
var maximumDpr = content.match(/maximum-dpr=([d.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}
})(window, window['lib'] || (window['lib'] = {}));
大家如果对相关原理感兴趣,可以阅读:flexible.js如何实现rem自适应-前端开发博客
在实际开发中应用场景不同效果不同,因此不能写死px。
在PC端适配我们可以自动转换rem适配方案(postcss-pxtorem、amfe-flexible),这里以vue3+vite为例子。事实上amfe-flexible是lib-flexible的升级版。
注意: 行内样式px不会转化为rem
npm install postcss postcss-pxtorem --save-dev // 我试过了反正我报错了,版本太高 大家可以指定5.1.1
npm install postcss-pxtorem@^5.1.1
npm i amfe-flexible --save
记得在main.js中引入amfe-flexible
import "amfe-flexible"
相关配置
媒体查询
通过查询不同的宽度来执行不同的css代码,最终以达到界面的配置。
在 CSS 中使用 @media
查询来检测屏幕宽度。当屏幕宽度小于 1024px 时,增加 margin-top
以向下移动表格。
.responsive-table {
transition: margin-top 0.3s; /* 添加过渡效果 */
}
@media (max-width: 1024px) {
.responsive-table {
margin-top: 200px; /* 向下移动的距离 */
}
}
弹性布局
创建一个响应式的卡片布局,当屏幕宽度减小时,卡片会自动换行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flexbox Example</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin: 0;
height: 100vh;
background-color: #f0f0f0;
}
.card-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 90%;
}
.card {
background-color: white;
border: 1px solid #ccc;
border-radius: 5px;
padding: 20px;
margin: 10px;
flex: 1 1 300px; /* 基于300px,允许增长和收缩 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.card:hover {
transform: translateY(-5px);
}
</style>
</head>
<body>
<div class="card-container">
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card">Card 3</div>
<div class="card">Card 4</div>
<div class="card">Card 5</div>
</div>
</body>
</html>
小结
还是多提一嘴,应该不会有小伙伴把字体大小的单位也用rem吧?
来源:juejin.cn/post/7431999862919446539
独立开发:家人不支持怎么办?
大家好,我是农村程序员,独立开发者,前端之虎陈随易。
这是我的个人网站:chensuiyi.me,欢迎一起交朋友~
有很多人跟我聊到过这个问题:做独立开发,家人不支持怎么办?
。
在我交流沟通下,最终发现,这些人都走进了一个误区:独立开发者
等于 我要辞职全职做独立开发
。
请看我对独立开发者的分类:
- 业余独立开发。特点:
上班 + 下班的业余时间独立开发
。 - 兼职独立开发。特点:
不上班 + 没有充足的时间做独立开发
。 - 全职独立开发。特点:
不上班 + 有充足的时间做独立开发
。 - 混合独立开发。特点:
上班+兼职+没有充足的时间做独立开发
。
现在是不是一目了然了。
你可以根据自己当下的情况,特点,去选择做哪一种 独立开发
。
我们目前所看到的 全职独立开发
,只有极少数人可以做到。
这对于个人的内在要求,包括自律,坚持,执行力,产品力,都有着较高的要求。
同时呢,来自家人的态度和压力,也是 全职独立开发
的重要条件。
不要一开始,啥独立开发的经验都没有,就想做 全职独立开发
。
那么当你可以 理性地选择
适合自己当下情况的的独立开发方式后,你会发现,家人还会不支持吗?至少不会那么反对了。
所以这个问题的答案就是这么简单,只要看了我对独立开发的分类,你就明白了。
独立开发,本就是一个人的战斗,不要妄想这家人会支持你,他们最大的支持就是不反对。
我们遇到这样的问题时,不要觉得家人怎么怎么样,自己受到了多大的委屈和不理解一样。
他们的想法,是完全没有问题的。
人是社会动物,必然要考虑当下的生存问题,这是十分合理且正常的。
那么,如果上面的问题解决后,家人还是不支持,怎么办呢?
也很简单啊,自己偷摸摸继续折腾呗,难道一定要得到家人的支持,才能做独立开发吗?
《明朝那些事》
的作者,当年明月,赚了几千万家人才知道呢。
当然,我不是说你,也不是说我自己,可以赚几千万,我们可以定目标,但不能做梦。
总而言之就是说,做独立开发,要做好一个人长期战斗的准备。
因为你很有可能,很多年都无法比较稳定地每个月赚 5000 块钱,独立开发远没有我们想象的那么轻松。
如果你实在没有时间,没有干劲,没有激情做独立开发,那么不如其他方向,说不定能获得更好的回报。
独立开发是一个美好的梦,不稳定,也容易破碎。
那么我为什么一直在坚持做独立开发呢?因为我想让美梦成真。
来源:juejin.cn/post/7434366864866099234
制作一个页面时,需要兼容PC端和手机端,你是要分别做两个页面还是只做一个页面自适应?为什么?说说你的理由
在制作一个页面时,如何兼容PC端和手机端是一个重要的设计决策。这个决策通常有两个选择:分别制作两个页面,或者只制作一个自适应页面。以下是我选择后者的理由,以及如何实现自适应设计的相关内容。
选择自适应设计的理由
- 提高开发效率
制作一个自适应页面可以显著提高开发效率。开发者只需编写一次代码,就可以在不同设备上展示同样的内容,而不必为每个设备维护多个代码库。这意味着在后续的更新和维护中,相比于维护两个独立的页面,自适应页面的工作量会大幅减少。 - 一致的用户体验
用户在不同设备上访问同一网站时,能够获得一致的用户体验是极其重要的。自适应设计可以确保在不同屏幕尺寸和分辨率下,用户看到的内容和布局尽可能一致,避免用户在不同设备上获取不同信息的困惑。 - SEO优化
使用单一的自适应页面有助于SEO优化。搜索引擎更倾向于索引和排名内容一致的网站,而不是将相同内容分散在多个页面上。使用自适应设计可以集中流量,提高网站在搜索引擎中的权重。 - 成本效益
维护两个独立的页面不仅需要更多的时间和人力成本,还可能导致代码重复,增加出错的可能性。自适应设计能够通过一个代码库降低开发和维护成本。 - 响应式设计的灵活性
现代的 CSS 和 JavaScript 技术(例如 Flexbox 和 CSS Grid)使得创建自适应布局变得更加简单和灵活。媒体查询(Media Queries)可以帮助开发者根据设备的特性(如宽度、高度、分辨率)调整布局和样式,从而提供最佳的用户体验。
如何实现自适应设计
- 使用媒体查询
媒体查询是实现自适应设计的核心。它允许你根据设备的屏幕尺寸和特性应用不同的样式。例如:
/* 默认样式 */
.container {
width: 100%;
padding: 20px;
}
/* 针对手机的样式 */
@media (max-width: 600px) {
.container {
padding: 10px;
}
}
/* 针对平板的样式 */
@media (min-width: 601px) and (max-width: 900px) {
.container {
padding: 15px;
}
}
- 使用流式布局
使用流式布局可以使元素在不同的屏幕上根据可用空间自动调整大小。例如,使用百分比而不是固定像素值来设置宽度:
.box {
width: 50%; /* 宽度为父容器的一半 */
height: auto; /* 高度自动适应内容 */
}
- 灵活的图片和媒体
为了确保图片和视频在不同设备上显示良好,使用max-width: 100%
来确保媒体不会超出其容器的宽度:
img {
max-width: 100%;
height: auto; /* 保持图片的纵横比 */
}
- 测试和优化
在开发完成后,确保在不同设备和浏览器上进行测试。可以使用 Chrome 的开发者工具模拟不同的设备,以检查自适应设计的效果。同时,收集用户反馈并进行必要的优化。
总结
在制作兼容PC端和手机端的页面时,选择制作一个自适应页面是更为高效和经济的方案。它不仅能提高开发效率和维护成本,还能提供一致的用户体验,增强SEO效果。此外,现代的CSS技术使得实现自适应设计变得更加简单。因此,采用自适应设计是现代Web开发的最佳实践之一。
来源:juejin.cn/post/7476010111887949861
别让这6个UI设计雷区毁了你的APP!
一款成功的APP不仅仅取决于其功能性,更取决于用户体验,这其中,UI设计又至关重要。优秀的UI设计能够为用户带来直观、愉悦的交互体验,甚至让用户“一见钟情”,从而大大提高产品吸引力。
然而,有很多设计师在追求创新与美观的同时,往往会不经意间“踩雷”,忽略了一些普通用户更在意的问题和痛点。本文,笔者将谈谈UI设计中的常见误区,同时展示了优秀的UI设计例子,还分享一些行业内推荐的设计工具,希望能助你在设计的道路上更加得心应手~
UI设计常见误区
1、过度设计
设计大师 Dieter Rams 说过一句话:“好的设计是尽可能简单的设计。”
不过,说起来容易做起来难。产品经理和设计师总是不自觉地让产品过于复杂,今天想在产品中再添加一个功能,明天想在页面上再增加一个元素。
尤其是新手设计师,总希望通过增加视觉元素、动画效果和复杂的交互来提升用户体验。然而,这种做法往往适得其反,一个简洁、清晰且直观的界面一定比一个装饰过多、功能复杂的界面更能吸引用户。设计师应该专注于APP的核心功能,避免不必要的元素,确保设计美观又实用。
简约风接单APP界面
http://www.mockplus.cn/example/rp/…
2、忽视用户反馈
有时候,设计师可能过于相信自己的设计,忽略了用户的意见和反馈。这种情况下,即使设计再怎么创新或美观,如果不符合用户的实际需求和使用习惯,也难以获得用户的认可。
毕竟,UI设计不是艺术创作,用户反馈是设计迭代过程中的宝贵资源,它可以帮助设计师了解用户的真实体验,了解设计的不足,从而进行针对性的优化改进。
FARFETCH APP界面
http://www.mockplus.cn/example/rp/…
3、色彩搭配不合适
色彩搭配不合适是UI设计中一个常见的误区。有的设计师会自动倾向于选择那些自己喜欢的颜色,而不是基于用户的偏好和产品特征、背景等来选择颜色。有时,颜色的过度使用或不恰当的搭配会分散用户的注意力,甚至造成视觉疲劳。
另外,为了让用户能看清UI设计的各个方面,需要有足够的对比度。这就是为什么黑色文字配上白色背景效果那么好 —— 文字非常清晰,易于理解。
插画风APP界面
http://www.mockplus.cn/example/rp/…
4、忽略可访问性
对一个APP来说,所有用户,无论是有视觉障碍的人还是老年用户,都应该能够轻松地使用APP。设计如果不考虑这些用户群体的需要,会无意中排除一大部分潜在用户。
为了提高APP的可访问性,设计师应该考虑到字体大小、颜色对比度、语音读出功能等,确保所有用户都能舒适地使用。
社交类APP界面
http://www.mockplus.cn/example/rp/…
5、布局空滤不全面
有的产品界面过多,布局考虑不够全面几乎是常常发生的事。布局不合适有很多种情况,比如元素过于密集、排版杂乱、组件没有对齐、文本大小间距不合适等等。这种情况会导致信息展示不清晰或用户操作不便,非常影响用户的使用体验和效率。
一个美观舒适的布局应该合理利用空间,确保信息的层次清晰,充分考虑元素的大小和间距,让界面既清晰又易于操作,用户可以轻松地找到他们关心的内容。
想要避免这一误区的,在设计初期就需要考虑用户的需求和行为模式,不断迭代优化布局设计,找到最佳的解决方案。
加密货币钱包APP界面
http://www.mockplus.cn/example/rp/…
了解了上述UI设计的常见误区后,接下来是选择合适的设计工具来提升设计效率和质量。目前,市面上有很多优秀的UI设计工具,但以下几款因其强大的功能和良好的用户体验受到设计师的广泛推荐,一起来看看!
UI工具推荐
1、摹客 DT
摹客DT(http://www.mockplus.cn/dt)是一款国内很多UI设计师都爱用的工具,它提供了一整套完整的专业矢量编辑功能,丰富的图层样式选项,可以轻松创建高质量的App设计,并且还能在线实时协同,搞定团队资产管理难题不是问题。
主要功能点和亮点:
1)所有功能完全免费(参与它们的小活动还能永久免费使用),包含所有高级功能、导出能力等;
2)颜色、文本样式、图层样式都可以保存为资源,复用到其他设计元素;
3)支持导出SVG、JPG、PNG、webP、PDF格式文件,适配不同使用场景;
4)具有完善的团队协作和项目管理能力,改变传统设计流程,降低成本,提升效率。
**价格:**完全免费
**学习难度:**简单,新手上手无难度
**使用环境:**Web/客户端/Android/iOS
**推荐理由:**摹客DT,是一款更适合中国设计师的UI/UX设计工具。与一些老牌设计工具(Photoshop、XD等),它的学习门槛更低,上手更简单,社区和实时客户支持更迅速友好;作为Web端工具,摹客DT支持多人实时编辑,大大提高了团队设计效率。
推荐评级:⭐⭐⭐⭐⭐
2、Figma
Figma(http://www.figma.com/)是现在最流行的UI设…
主要功能点及亮点:
1)丰富的功能和工具:提供了全面的工具和功能来设计界面,包括矢量图形工具、网格和布局指导等。
2)设计组件和样式库:支持创建可复用的设计组件和样式库,节省时间和工作量。
3)插件生态系统:有丰富的插件生态系统,可以通过插件扩展功能,增加额外的设计工具和集成,以满足特定需求。
**价格:**提供免费版和付费版(12美元/月起)
**学习难度:**对新手相对友好,操作简单。
**使用环境:**Figma是基于Web的平台,通过浏览器即可使用。
推荐理由:
Figma设计功能强大,提供了丰富的插件和集成,还有有一个庞大的社区交流学习,是一款非常适合团队协作的工具。遗憾的是,Figma作为一款国外设计工具,在国内使用还是存在一定的劣势,比如网络访问限制、数据隐私和安全、本地化支持等,所以只能给到四星。
推荐评级:⭐⭐⭐⭐
3、Sketch
Sketch(http://www.sketch.com/)是一款专业的UI/U…
主要功能及亮点:
- 1)矢量编辑:直观的矢量编辑工具和可编辑的布尔运算,支持形状绘制、路径编辑、填充和描边等常见的矢量操作,使用户能够轻松创建和编辑各种设计元素。
- 2)设计规范库:设计师可以在sketch中把常用的颜色、图层样式存为规范,也可以将已经设计好的矢量图标存到规范库,在整个设计中复用它们,提高工作效率。
3)Sketch支持众多插件和第三方资源,使得其生态系统非常丰富,能够更好地满足设计师的需求。
**价格:**标准订阅 12/月/人(按月付费)
**使用环境:**macOS操作系统
推荐理由:
Sketch被广泛应用于移动应用、网页设计和用户界面设计等领域。它友好的界面、丰富的设计资源使设计师们能够快速创建出符合良好用户体验和视觉效果要求的设计。可惜的是,它只支持macOS系统,限制了部分用户使用。
**推荐评级:**⭐⭐⭐⭐
4、Adobe XD
Adobe XD(helpx.adobe.com/support/xd.…
主要功能及亮点:
1)丰富的设计和布局功能:设计师可以借助这些功能,轻松创建响应式设计,预览在不同设备上的效果,确保设计的一致性和灵活性。
2)丰富的交互动画和过渡效果:可以创建十分流畅的交互体验,用来更好地展示APP或网站的交互逻辑。
3)共享和协作:支持团队协作,用户可以轻松地共享设计文件,协同编辑,以及通过实时协作功能进行设计评审。
**价格:**提供免费试用,提供付费订阅 $9.99/月
**学习难度:**中
**使用环境:**Windows、macOS
**推荐理由:**Adobe XD与Adobe Creative Cloud紧密集成,十分方便同时使用Adobe其他软件的用户。不过有个问题是,Adobe xd已经官宣停止更新,只向老用户提供服务了,如果是之前没有使用Adobe XD的用户,最好选择其他产品。
**推荐评级:**⭐️⭐️⭐️
五、Principle
Principle是一款专门用于UI/UX设计的软件,它的设计理念是想要让设计师能够轻松创建出高质量、富有吸引力的产品界面效果。
主要功能及亮点:
1)高级动画控制:软件支持创建复杂的动画效果,包括过渡、转换和多状态动画。这对于展示复杂的交互过程和动态效果非常有用。
2)实时预览:Principle允许设计师实时预览他们的工作成果,这意味着可以即时查看和测试动画效果,确保设计的交互体验符合预期。
3)组件和重复使用:设计师可以创建可重复使用的组件,这大大提高了工作效率,特别是在处理具有相似元素或布局的多个界面时。
价格:$129
**学习难度:**中
**使用环境:**MacOS
推荐理由:
设计师通过Principle可以快速地制作出具有丰富交互的产品界面设计,并且它能和Sketch进行无缝链接,可以快速导入你在Sketch里已经做好的图,
推荐评级:⭐️⭐️⭐️⭐️
好的UI设计不仅仅是视觉上的享受,更是能让用户能够轻松、愉快地使用APP。避免上述UI设计误区,选择合适的设计工具,可以帮助我们设计出既美观又实用的产品。
希望本文章能为UI设计师/准UI设计师们有所帮助,创作出更多优秀的设计作品~
看到这里的读者朋友有福啦!本人吐血搜集整理,全网最全产品设计学习资料!
只要花1分钟填写**问卷**就能免费领取以下超值礼包:
1、产品经理必读的100本书 包含:产品思维、大厂案例、技能提升、数据分析、项目管理等各类经典书籍,从产品入门到精通一网打尽! 2、UI/UX设计师必读的115本书 包含:UI/UX设计、平面设计、版式设计、设计心理学、设计思维等各类经典书籍,看完你就是设计大神! 3、30G互联网人知识礼包 包含:
- 10GPM礼包,产品案例、大咖分享、行业报告等应有尽有
- 10GUI/UE资源,优秀设计案例、资料包、源文件免费领
- 5G运营资料包,超全产品、电商、新媒体、活动等运营技能
- 5G职场/营销资料包,包含产品设计求职面试、营销增长等
4、50G热门流行的AI学习大礼包
包含:AI绘画、AIGC精选课程、AI职场实用教程等
5、30G职场必备技能包
包含:精选PPT模板、免费可商用字体包、各岗位能力模型、Excel学习资料、求职面试、升职加薪、职场写作和沟通等实用资源。
礼包资源持续更新,互联网行业知识一网打尽!礼包领取地址:
来源:juejin.cn/post/7356535808931627046
后端:没空,先自己 mock 去
前言
后端开发忙,不给你接口?
后端抱怨你在测试过程中,频繁的给脏数据?
后端修个接口很慢没法测试?

有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!
后端开发忙,不给你接口?
后端抱怨你在测试过程中,频繁的给脏数据?
后端修个接口很慢没法测试?
有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!
真这么丝滑?
请看我的使用方式:
当后端接口无法满足要求,且不能及时更改时。例如后端返回
{
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
}
}
],
}
但我此时希望增加一个 user_type
来确定页面的展示。
那我就直接起一个文件:user.js
,把刚才的响应 copy 过来,并追加改动
myMock('/api/v1/user', 'post', () => {
return {
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
"user_type": "admin",
}
}
],
}
});
如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。
请看我的使用方式:
当后端接口无法满足要求,且不能及时更改时。例如后端返回
{
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
}
}
],
}
但我此时希望增加一个 user_type
来确定页面的展示。
那我就直接起一个文件:user.js
,把刚才的响应 copy 过来,并追加改动
myMock('/api/v1/user', 'post', () => {
return {
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
"user_type": "admin",
}
}
],
}
});
如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。
如何接入 mockjs
有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来
- 安装 mockjs
pnpm i mockjs
如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs
- 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
// 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
import './login/user.js';
import './model/model.js';
有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来
- 安装 mockjs
pnpm i mockjs
如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs
- 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
// 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
import './login/user.js';
import './model/model.js';
并且在你的项目入口 ts 中引入 mock/index.ts
import './mock/index'; // 引入 mock 配置
- 导出一个 myMock 方法,并追加一个 baseUrl 方便直接联动你的 axios
import { ENV_TEST } from '@/api/config/interceptor';
import Mock from 'mockjs';
export const myMock = (
path: string,
method: 'get' | 'post',
callback: (options: any) => any
) => {
Mock.mock(`${ENV_TEST}${path}`, method, callback);
};
如此一来,你就可以在 mock 文件夹下去搞了,比如:
我想新增一个服务模块的各类接口的 mock,那么我就新增一个 service 文件夹,在其下增加一个 index.ts,并对对应路径进行 mock
myMock('/api/v1/service', 'get', () => {
return {
code: 0,
msg: 'hello service',
data: null,
};
});
另外,别忘了在 mock/index.ts 引入文件
不显示在 network 中?
需要说明的是,这样走 mock 是不会触发真正的请求的,相当于 xhr 直接被 mock 拦截了下来并给了你返回值。所以你无法在 network 中看到你的请求。
这是个痛点,目前比较好的解决方案还是起一个单独的服务来 mock。但这样也就意味着,需要另起一个项目来单独做 mock,太不优雅了。
有没有什么办法,既可以走上述简单的mock,又可以在需要的时候起一个服务来查看 network,并且不需要额外维护两套配置呢?
有的兄弟,有的。
import express from 'express';
import bodyParser from 'body-parser';
import Mock from 'mockjs';
import './login/user.js';
import './model/model.js';
import { ENV_TEST } from './utils/index.js';
const app = express();
const port = 3010;
// 使用中间件处理请求体和CORS
app.use(bodyParser.json());
// 设置CORS头部
app.use(( _ , res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
// 设置Mock路由的函数
const setupMockRoutes = () => {
const mockApis = Mock._mocked || {};
// 遍历每个Mock API,并生成对应的路由
Object.keys(mockApis).forEach((key) => {
const { rurl, rtype, template } = mockApis[key];
const route = rurl.replace(ENV_TEST, ''); // 去掉环境前缀
// 根据请求类型(GET, POST, 等)设置路由
app[rtype.toLowerCase()](route, (req, res) => {
const data =
typeof template === 'function' ? template(req.body || {}) : template;
res.json(Mock.mock(data)); // 返回模拟数据
});
});
};
// 设置Mock API路由
setupMockRoutes();
// 启动服务器
app.listen(port, () => {
process.env.NODE_ENV = 'mock'; // 设置环境变量
console.log(`Mock 服务已启动,访问地址: http://localhost:${port}`);
});
直接在 mock 文件夹下追加这个启动文件,当你需要看 network 的时候,将环境切换为 mock 环境即可。本质是利用了 Mock._mocked
可以拿到所有注册项,并用 express 起了一个后端服务响应这些注册项来实现的。
在拥有了这个能力的基础上,我们就可以调整我们的命令
"scripts": {
"dev": "cross-env NODE_ENV=test vite",
"mock": "cross-env NODE_ENV=mock vite & node ./src/mock/app.js"
},
顺便贴一下我的 env 配置:
export const ENV_TEST = 'https://api-ai.com/fuxi';
export const ENV_MOCK = 'http://localhost:3010/';
let baseURL: string = ENV_TEST;
console.log('目前环境为:' + process.env.NODE_ENV);
switch (process.env.NODE_ENV) {
case 'mock':
baseURL = ENV_MOCK;
break;
case 'test':
baseURL = ENV_TEST;
break;
case 'production':
break;
default:
baseURL = ENV_TEST;
break;
}
export { baseURL };
这样一来,如果你需要看 network ,就 pnpm mock,如果不需要,就直接 pnpm dev,完全不需要其他心智负担。
三个字:
参数相关
具体的 api 可查阅:github.com/nuysoft/Moc… 相关的文章也非常多,就不展开说明了。
如果这篇文章对你有帮助,不妨点个赞吧~
来源:juejin.cn/post/7460091261762125865
前端哪有什么设计模式
前言
- 常网IT源码上线啦!
- 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
- 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
- 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。
你生命的前半辈子或许属于别人,活在别人的认为里。那把后半辈子还给你自己,去追随你内在的声音。
一、前言
之前在讨论设计模式、算法的时候,一个后端组长冷嘲热讽的说:前端哪有什么设计模式、算法,就好像只有后端语言有一样,至今还记得那不屑的眼神。
今天想起来,就随便列几个,给这位眼里前端无设计模式的人,睁眼看世界。
二、观察者模式 (Observer Pattern)
观察者模式的核心是当数据发生变化时,自动通知并更新相关的视图。在 Vue 中,这通过其响应式系统实现。
Vue 2.x:Object.defineProperty
在 Vue 2.x 中,响应式系统是通过 Object.defineProperty
实现的。每当访问某个对象的属性时,getter
会被触发;当设置属性时,setter
会触发,从而实现数据更新时视图的重新渲染。
源码(简化版):
function defineReactive(obj, key, val) {
// 创建一个 dep 实例,用于收集依赖
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 当访问属性时,触发 getter,并把当前 watcher 依赖收集到 dep 中
if (Dep.target) {
dep.addDep(Dep.target);
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify(); // 数据更新时,通知所有依赖重新渲染
}
}
});
}
Dep
类:它管理依赖,addDep
用于添加依赖,notify
用于通知所有依赖更新。
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach(dep => dep.update());
}
}
- 依赖收集:当 Vue 组件渲染时,会创建一个
watcher
对象,表示一个视图的更新需求。当视图渲染过程中访问数据时,getter
会触发,并将watcher
添加到dep
的依赖列表中。
Vue 3.x:Proxy
Vue 3.x 使用了 Proxy
来替代 Object.defineProperty
,从而实现了更高效的响应式机制,支持深度代理。
源码(简化版):
function reactive(target) {
const handler = {
get(target, key) {
// 依赖收集:当访问某个属性时,触发 getter,收集依赖
track(target, key);
return target[key];
},
set(target, key, value) {
// 数据更新时,通知相关的视图更新
target[key] = value;
trigger(target, key);
return true;
}
};
return new Proxy(target, handler);
}
track
:收集依赖,确保只有相关组件更新。trigger
:当数据发生变化时,通知所有依赖重新渲染。
三、发布/订阅模式 (Publish/Subscribe Pattern)
发布/订阅模式通过中央事件总线(Event Bus)实现不同组件间的解耦,Vue 2.x 中,组件间的通信就是基于这种模式实现的。
Vue 2.x:事件总线(Event Bus)
事件总线就是一个中央的事件处理器,Vue 实例可以充当事件总线,用来处理不同组件之间的消息传递。
// 创建一个 Vue 实例作为事件总线
const EventBus = new Vue();
// 组件 A 发布事件
EventBus.$emit('message', 'Hello from A');
// 组件 B 订阅事件
EventBus.$on('message', (msg) => {
console.log(msg); // 输出 'Hello from A'
});
$emit
:用于发布事件。$on
:用于订阅事件。$off
:用于取消订阅事件。
四、工厂模式 (Factory Pattern)
工厂模式通过一个函数生成对象或实例,Vue 的组件化机制和动态组件加载就是通过工厂模式来实现的。
Vue 的 render
函数和 functional
组件支持动态生成组件实例。例如,functional
组件本质上是一个工厂函数,通过给定的 props
返回一个 VNode。
Vue.component('dynamic-component', {
functional: true,
render(h, context) {
// 工厂模式:根据传入的 props 创建不同的 VNode
return h(context.props.type);
}
});
functional
组件:它没有实例,所有的逻辑都是在render
函数中处理,返回的 VNode 就是组件的“产物”。
五、单例模式 (Singleton Pattern)
单例模式确保某个类只有一个实例,Vue 实例就是全局唯一的。
在 Vue 中,全局的 Vue
构造函数本身就是一个单例对象,通常只会创建一个 Vue 实例,用于管理应用的生命周期和全局配置。
const app = new Vue({
data: {
message: 'Hello, Vue!'
}
});
- 单例保证:整个应用只有一个 Vue 实例,所有全局的配置(如
Vue.config
)都是共享的。
六、模板方法模式 (Template Method Pattern)
模板方法模式定义了一个操作中的算法框架,而将一些步骤延迟到子类中。Vue 的生命周期钩子就是一个模板方法模式的实现。
Vue 定义了一系列生命周期钩子(如 created
、mounted
、updated
等),它们实现了组件从创建到销毁的完整过程。开发者可以在这些钩子中插入自定义逻辑。
Vue.component('my-component', {
data() {
return {
message: 'Hello, 泽!'
};
},
created() {
console.log('Component created');
},
mounted() {
console.log('Component mounted');
},
template: '<div>{{ message }}</div>'
});
Vue 组件的生命周期钩子实现了模板方法模式的核心思想,开发者可以根据需要重写生命周期钩子,而 Vue 保证生命周期的流程和框架。
七、策略模式 (Strategy Pattern)
策略模式通过定义一系列算法,将它们封装起来,使它们可以相互替换。Vue 的 计算属性(computed) 和 方法(methods) 可以看作是策略模式的应用。
计算属性允许我们定义动态的属性,其值是基于其他属性的计算结果。Vue 会根据依赖关系缓存计算结果,只有在依赖的属性发生变化时,计算属性才会重新计算。
new Vue({
data() {
return {
num1: 10,
num2: 20
};
},
computed: {
sum() {
return this.num1 + this.num2;
}
}
});
八、装饰器模式 (Decorator Pattern)
装饰器模式允许动态地给对象添加功能,而无需改变其结构。在 Vue 中,指令就是一种装饰器模式的应用,它通过指令来动态地改变元素的行为。
<div v-bind:class="className"></div>
<div v-if="isVisible">谁的疯太谍</div>
这些指令动态地修改 DOM 元素的行为,类似于装饰器在不修改对象结构的情况下,动态地增强其功能。
九、代理模式 (Proxy Pattern)
代理模式通过创建一个代理对象来控制对目标对象的访问。在 Vue 3.x 中,响应式系统就是通过 Proxy
来代理对象的访问。
vue3
const state = reactive({
count: 0
});
state.count++; // 会触发依赖更新
reactive:使用 Proxy 对对象进行代理,当对象的属性被访问或修改时,都会触发代理器的 get 和 set 操作。
function reactive(target) {
const handler = {
get(target, key) {
// 依赖收集:当访问某个属性时,触发 getter,收集依赖
track(target, key);
return target[key];
},
set(target, key, value) {
// 数据更新时,触发依赖更新
target[key] = value;
trigger(target, key);
return true;
}
};
return new Proxy(target, handler);
}
track
:当读取目标对象的属性时,收集依赖,这通常涉及到将当前的watcher
加入到依赖列表中。trigger
:当对象的属性发生改变时,通知所有相关的依赖(如组件)更新。
这个 Proxy
机制使得 Vue 可以动态地观察和更新对象的变化,比 Object.defineProperty
更具灵活性。
十、适配器模式 (Adapter Pattern)
适配器模式用于将一个类的接口转换成客户端期望的另一个接口,使得原本不兼容的接口可以一起工作。Vue 的插槽(Slots)和组件的跨平台支持某种程度上借用了适配器模式的思想。
Vue 插槽机制
Vue 的插槽机制是通过提供一个适配层,将父组件传入的内容插入到子组件的指定位置。开发者可以使用具名插槽、作用域插槽等方式,实现灵活的插槽传递。
<template>
<child-component>
<template #header>
<h1>This is the header</h1>
</template>
<p>This is the default content</p>
</child-component>
</template>
父组件通过 #header
插槽插入了一个标题内容,而 child-component
会将其插入到适当的位置。这里,插槽充当了一个适配器,允许父组件插入的内容与子组件的内容结构灵活匹配。
十全十美
至此撒花~
后记
我相信技术不分界,不深入了解,就不要轻易断言。
一个圆,有了一个缺口,不知道的东西就更多了。
但是没有缺口,不知道的东西就少了。
这也就是为什么,知道得越多,不知道的就越多。
谢谢!
最后,祝君能拿下满意的offer。
我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
以往推荐
原文链接
来源:juejin.cn/post/7444215159289102347
再见 XShell!一款万能通用的终端工具,用完爱不释手!
作为一名后端开发,我们经常需要使用终端工具来管理Linux服务器。最近发现一款比Xshell更好用终端工具XPipe,能支持SSH、Docker、K8S等多种环境,还具有强大的文件管理工具,分享给大家!
XPipe简介
XPipe是一款全新的终端管理工具,具有强大的文件管理功能,目前在Github上已有4.8k+Star
。它可以基于你本地安装的命令行工具(例如PowerShell)来执行远程命令,反应速度非常快。如果你有使用 ssh、docker、kubectl 等命令行工具来管理服务器的需求,使用它就可以了。
XPipe具有如下特性:
- 连接中心:能轻松实现所有类型的远程连接,支持SSH、Docker、Podman、Kubernetes、Powershell等环境。
- 强大的文件管理功能:具有对远程系统专门优化的文件管理功能。
- 多种命令行环境支持:包括bash、zsh、cmd、PowerShell等。
- 多功能脚本系统:可以方便地管理可重用脚本。
- 密码保险箱:所有远程连接账户均完全存储于您本地系统中的一个加密安全的存储库中。
下面是XPipe使用过程中的截图,界面还是挺炫酷的!
这或许是一个对你有用的开源项目,mall项目是一套基于
SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
- Boot项目:github.com/macrozheng/…
- Cloud项目:github.com/macrozheng/…
- 教程网站:http://www.macrozheng.com
项目演示:
使用
- 首先去XPipe的Release页面下载它的安装包,我这里下载的是
Portable
版本,解压即可使用,地址:github.com/xpipe-io/xp…
- 下载完成后进行解压,解压后双击
xpiped.exe
即可使用;
- 这里我们先进行一些设置,将语言设置成
中文
,然后设置下主题,个人比较喜欢黑色主题;
- 接下来新建一个SSH连接,输入服务器地址后,选择
添加预定义身份
;
- 这个预定义身份相当于一个可重用的Linux访问账户;
- 然后输入连接名称,点击完成即可创建连接;
- 我们可以发现XPipe能自动发现服务器器上的Docker环境并创建连接选项,如果你安装了K8S环境的话,也是可以发现到的;
- 然后我们单击下
Linux-local
这个连接,就可以通过本地命令行工具来管理Linux服务器了;
- 如果你想连接到某个Docker容器的话,直接点击对应容器即可连接,这里以mysql为例;
- 选中左侧远程服务器,点击右侧的
文件浏览器
按钮可以直接管理远程服务器上的文件,非常方便;
- 在
所有脚本
功能中,可以存储我们的可重用脚本;
- 在
所有身份
中存储着我们的账号密码,之前创建的Linux root账户在这里可以进行修改。
总结
今天给大家分享了一款好用的终端工具XPipe,界面炫酷功能强大,它的文件管理功能确实惊艳到我了。而且它可以用本地命令行工具来执行SSH命令,对比一些套壳的跨平台终端工具,反应速度还是非常快的!
项目地址
来源:juejin.cn/post/7475662844789637160
关于生娃的思考
生娃是人生很重要的事,值得花时间思考。我断断续续想了几年,没想明白,最近看 B 站半佛老师对哪吒 2 的影评,理解了什么情况下可以生娃,把感受分享给大家。
什么时候可以给孩子无条件的、不抱任何期待的爱,什么时候就可以生娃了。
半佛老师在影评中表示非常羡慕哪吒有那样的父母,因为哪吒的父母给哪吒无条件的、不抱任何期待的爱,而半佛老师的童年比较悲惨,让他有点羡慕嫉妒恨。为此半佛特意查了导演饺子的家庭,希望饺子是因为缺爱才把哪吒父母写的那么好,结果发现饺子就是哪吒本吒,他彻底破防了。
过年回家免不了和父母共处几天,我也几度破防,好几次想直接回出租屋。也被催生,丈母娘也催。
之前对生娃的看法,我觉得生娃太费钱了,而我没钱,就算我有钱,我也不想把钱花在娃上面,我想花在自己身上,多体验人生。
其次我自己这辈子都没活明白,也挺痛苦,何必生个娃来这悲惨世间走一遭?所以我不生娃。
但这次我有新的想法,是否生娃,应取决于我是否做好了为人父母的准备,即是否可以给孩子无条件的、不抱任何期待的爱。这决定孩子一辈子是否幸福。
两个关键词,无条件、不抱期待。
无条件
考试考得好,父母就爱,考得不好就不爱;听话就爱,不听话就不爱;不知道你们怎样,我小时候是这样的。
这让我没有安全感,下意识会做些讨好父母的行为,来获得父母的 “ 爱 ”。
当我成年的时候,我妈发现这招不管用了,我不需要他们的爱,我需要钱,因为钱能给我安全感。
所以我大学去食堂打工,每天中午和晚上早点下课过去,结果一个月才 300 块钱包两顿饭。
不抱期待
期待这个东西,在教育中尤为突出,否则不会也有那么多鸡娃的父母。
父母对孩子的期待,让孩子有非常强烈的愧疚。我辛辛苦苦把你从农村带到城市,你就考这么几分?我辛辛苦苦把你拉扯大,你就这么对我?
在这种环境下,我成为了一个逆子,六亲不认,自动屏蔽亲情。我不接受他们的爱,我也不给他们爱。
大学打工的 300 块不能养活我,每年还要交学费,我一想到学费是父母出的,他们又会以此要挟,我辛辛苦苦赚钱供你上大学,结果你就这样?
所以只读了半年大学,我退学了,自己出去找工作,我必须在经济上独立,必须逃离这个家。
别人说家是港湾,外面受伤了家是永远的依靠,对我来说家就是伤害。
其实我这样还算好的,至少活下来了,还有更多孩子,承受不了这种愧疚,选择了自杀,他们要把愧疚,加倍偿还给父母。
我身边就有这样的案例,跳楼了,他父母不知道为什么说了两句,孩子就直接从阳台上跳下去了。
我可太懂了,我也想过自杀,一来是当时家里装了防盗窗,我有点胖钻不过去;然后换成了撞墙,头上撞了几个包有点疼,基因让我停下了;我还尝试过离家出走,也没走成。
但我也在脑海中无数次幻想,要是我死了,我爸妈有多愧疚,这就是我自杀的目的。
我写的和想的有点黑暗,有人说什么爸妈把你辛辛苦苦拉扯大,不容易什么的。
但是我不需要,我没求着来这个世上。
所以我对自己生娃,非常谨慎,我不希望他的童年会想自杀,我不希望他成年后和父母几乎断绝关系,我希望他要来就过的幸福点,这取决我能否会给他无条件的、没有期待的爱。
愿各位都是好父母,愿世上孩子都幸福,以上是我的思考,共勉。
来源:juejin.cn/post/7467353503088246784
官方回应无虚拟DOM版Vue为什么叫Vapor
相信很多人和我一样,好奇无虚拟DOM
版的Vue
为什么叫Vue Vapor
。之前看过一个很新颖的观点:Vue1
时代就没有虚拟DOM
,而那个时代恰好处于前端界的第一次工业革命,也就是以声明式代替命令式的写法。所以无虚拟DOM
版Vue让人感觉梦回Vue1
,于是就采取了Vapor
这个名字。
Vapor是蒸汽的意思,第一次工业革命开创了以机器代替手工劳动的时代,该革命以蒸汽机作为动力机被广泛使用为标志。
不过这个说法并非来自官方,虽然乍一听还挺有道理的,但总感觉官方并不一定是这么想的。事实也的确如此,在官方的Vue Conf
中,Vue Vapor
的作者出面说明了Vapor
这个名字的含义:
由于无虚拟DOM
的特性,纯Vapor
模式下可以去掉很多代码,比如VDom Diff
。所以Vue Vapor
的包体积可以做的更加的轻量化,像水蒸气一样轻。
(前面那段话是官方说的,这段话是我说的)当然不是说
Vapor
模式就不需要diff
算法了,我看过同为无虚拟DOM
框架的Svelte
和Solid
源码,无虚拟DOM
只是不需要vDom
间的Diff
算法了,列表之间还是需要diff
的。毕竟再怎么编译,你从后端获取到的数组,编译器也不可能预测得到。
那具体能轻量多少呢?官方给出的数据是比虚拟DOM
版Vue
小33.6%
:
Vapor
的名字除了想表示轻量化之外还有一个另外一个原因,那就是Solid
。可能有人会说这关Solid
什么事啊?实际上Vapor
的灵感正是来自于Solid
(尤雨溪亲口承认)而Solid
代表固体:
为了跟Solid
有个趣味联动,那无虚拟DOM
就是气体好了:
以上就是Vue Vapor
作者告诉大家为什么叫Vapor
的两大原因。
性能
之前都说虚拟DOM
是为了性能,怎么现在又反向宣传了?无虚拟DOM
怎么又逆流而上成为了性能的标杆了呢?这个话题我们留到下一篇文章去讲,本篇文章我们只看数据:
从左到右依次为:
- 原生
JS
:1.01 Solid
:1.09Svelte
:1.11- 无虚拟
DOM
版Vue
:1.24 - 虚拟
DOM
版Vue
:1.32 React
:1.55
数字越小代表性能越高,但无论再怎么高都不可能高的过手动优化过的原生JS
,毕竟无论什么框架最终打包编译出来的还是JS
。不过框架的性能其实已经挺接近原生的了,尤其是以无虚拟DOM
著称的Solid
和Svelte
。但无虚拟DOM
版Vue
和虚拟DOM
版Vue
之间并没有拉开什么很大的差距,1.24
和1.32
这两个数字证明了其实二者的性能差距并不明显,这又是怎么一回事呢?
一个原因是Vue3
本来就做了许多编译优化,包括但不限于静态提升、大段静态片段将采用innerHTML
渲染、编译时打标记以帮助虚拟DOM
走捷径等… 由于本来的性能就已经不错了,所以可提升的空间自然也没多少。
看完上述原因肯定有人会说:可提升的空间没多少可以理解,那为什么还比同为无虚拟DOM
的Solid
和Svelte
差那么多?如果Vapor
和Solid
性能差不多的话,那可提升空间小倒是说得过去。但要是像现在这样差的还挺多的话,那这个理由是不是就有点站不住脚了?之所以会出现这样的情况是因为Vue Vapor
的作者在性能优化和快速实现之间选择了后者,毕竟尤雨溪第一次公布要开始做无虚拟DOM
版Vue
的时间是在2020
年:
而如今已经是2025
年了。也就是说如果一个大一新生第一次满心欢喜的听到无虚拟DOM
版Vue
的消息后,那么他现在大概率已经开始毕业工作了都没能等到无虚拟DOM
版Vue
的发布。这个时间拖的有点太长了,甚至从Vue2
到Vue3
都没用这么久。
所以现在的首要目标是先实现出来一版能用的,后面再慢慢进行优化。我们工作不也是这样么?先优先把功能做出来,优化的事以后再说。那么具体该怎么优化呢:
现在的很多渲染逻辑继承自原版Vue3
,但无虚拟DOM
版Vue
可以采用更优的渲染逻辑。而且现在的Vapor
是跟着原版Vue
的测试集来做的,这也是为了实现和原版Vue
一样的行为。但这可能并不是最优解,之后会慢慢去掉一些不必要的功能,尤其是对性能产生较大影响的功能。
往期精彩文章:
来源:juejin.cn/post/7477104460452872202
Netflix 删除了 React ?
Netflix 删除了 React
"Netflix 删除了 React,网站加载时间减少了 50%!"
这条爆炸性新闻在推特炸开了锅。这不禁让人想起半年前"微软应该禁止所有新项目使用 React" 的新闻 - 两大科技巨头相继 "抛弃" React,难道这预示着什么?
"React 心智负担重、性能差、bundle 体积大...". 一时间,各种唱衰 React 的声音此起彼伏。有人甚至信誓旦旦地表示:"只要像 Netflix 一样干掉 React,你的网站性能立马提升 50%!"
这条发布于 2024 年 10 月 27 日的推文有着惊人的 150 万浏览量。但是,这大概率是 AI
生成的假新闻。
事实上,我们去 Netflix
的官网打开 react-devtools
,发现他们依然在使用 React
构建他们的网站。
Netflix 的真实案例
这篇 AI
生成的假新闻灵感来自 2017 年 Netflix
工程师在 hack news
上发布的一篇文章 - Netflix: Removing client-side React.js improved performance by 50%
他直接移除了这篇文章最重要的部分 - client-side React.js
, 也就是客户端的 React.js
代码。
实际的情况是,Netflix
团队在 2017 年的时候在使用 React
构建他们的 landing page
。
为什么在一个简单的 landing page
上要使用 React
呢?因为在 landing page
上
Netflix
需要处理大量的AB 测试
- 支持近 200 个国家的本地化
- 根据用户设备、地理位置等因素动态调整内容
- 需要服用现有的
React
组件
基于上述需求的考虑,Netflix
团队选择了使用 React
来构建他们的 landing page
。
为了优化页面加载性能,他们采用了服务端渲染的方案。同时,为了保证后续交互的流畅性,系统会预先加载(pre-fetch
)后续流程所需的 React/Redux
相关代码。
从架构上看,这个 landing page
本质上仍然是一个单页面应用(SPA
),保持了 SPA
快速响应的优势。不同之处在于首次访问时,页面内容是由服务端直接生成的,这样可以显著提升首屏加载速度。
这样做的缺点
显然,Netflix
在 2017 年这么做是有原因的,他们当时的确也没有更好的方案。在 2025 年的今天,
再来回顾这个方案,显然有以下缺点:
数据重复获取
在首屏渲染时,服务端需要获取数据来生成 HTML,而在客户端激活(hydration)后,为了保持交互性,往往又需要重新获取一遍相同的数据。
这种重复的数据获取不仅浪费资源,还可能带来不必要的性能开销。
客户端代码体积膨胀
因为本质上,Netflix
的 landing page
是一个还是一个 SPA
,那么不可避免的,所有可能的 UI
状态都需要打包,
即使用户只需要其中的一部分代码。例如,在一个很常见的 tabs
页面
<Tabs
defaultActiveKey="1"
items={[
{
label: 'Tab 1',
key: '1',
children: 'Tab 1',
},
{
label: 'Tab 2',
key: '2',
children: 'Tab 2',
disabled: true,
},
]}
/>
即使用户只点击了 Tab 1
, 即使 Tab 2
没有被渲染,但是 Tab 2
的代码也会被打包。
如何解决这些问题
React Server Components (RSC)
为上述问题提供了优雅的解决方案:
避免数据重复获取
使用 RSC
,组件可以直接在服务器端获取数据并渲染,客户端只接收最终的 HTML
结果。不再需要在客户端重新获取数据。
智能代码分割
RSC
允许我们选择性地决定哪些组件在服务器端运行,哪些在客户端运行。例如:
function TabContent({ tab }: { tab: string }) {
// 这部分代码只在服务器端运行,不会打包到客户端
return <div>{tab} 内容</div>
}
// 客户端组件
'use client'
function TabWrapper({ children }) {
const [activeTab, setActiveTab] = useState('1')
return (
<div>
{/* Tab 切换逻辑 */}
{children}
</div>
)
}
在这个例子中:
TabContent
的所有可能状态都在服务器端预渲染- 只有实际需要交互的
TabWrapper
会发送到客户端 - 用户获得了更小的
bundle
体积和更快的加载速度
这不就是 PHP?
经常会看到有人说:"Server Components 不就是重新发明了 PHP 吗?都是在服务端生成 HTML。"
显然,PHP 与现在的 Server Components
在开发体验上有本质的区别。
1. 细粒度的服务端-客户端混合
与 PHP 不同,RSC 允许我们在组件级别决定渲染位置,用一个购物车的例子来说明:
// 服务端组件
function ProductDetails({ id }: { id: string }) {
// 在服务器端获取数据和渲染
const product = await db.products.get(id);
return <div>{product.name}</div>;
}
// 客户端组件
'use client'
function AddToCart({ productId }: { productId: string }) {
// 在客户端处理交互
return <button onClick={() => addToCart(productId)}>加入购物车</button>;
}
// 混合使用
function ProductCard({ id }: { id: string }) {
return (
<div>
<ProductDetails id={id} />
<AddToCart productId={id} />
</div>
);
}
这种设计充分利用了服务端和客户端各自的优势 - 在服务端可以直接访问数据库获取 ProductDetails
所需的数据,而在客户端则能更好地处理 AddToCart
这样的用户交互。这不仅提升了性能,也让代码结构更加清晰合理。
2. 保持组件的可复用性
RSC
最强大的特性之一是组件的可复用性不受渲染位置的影响:
// 这个组件可以在服务端渲染
function UserProfile({ id }: { id: string }) {
return <ProfileCard id={id} />;
}
// 同样的组件也可以在客户端动态加载
'use client'
function UserList() {
const [selectedId, setSelectedId] = useState(null);
return selectedId ? <ProfileCard id={selectedId} /> : null;
}
因为都是 React
组件,区别仅仅是渲染位置的不同,同一个组件可以:
- 在服务端预渲染时使用
- 在客户端动态加载时使用
- 在流式渲染中使用
这种统一的组件模型是 PHP 等传统服务端渲染所不具备的。
3. 智能的序列化
RSC
还提供了智能的序列化机制,可以自动将组件的 props
和 state
序列化,从而在服务端和客户端之间传递。
避免了重复获取数据的问题。
// 服务端组件
async function Comments({ postId }: { postId: string }) {
// 1. 获取评论数据
const comments = await db.comments.list(postId);
// 2. 传递给客户端组件
return <CommentList initialComments={comments} />;
}
// 客户端组件
'use client'
function CommentList({ initialComments }) {
// 3. 直接使用服务端数据,无需重新请求
const [comments, setComments] = useState(initialComments);
return (
// 渲染评论列表
);
}
4. 渐进式增强
RSC
还提供了渐进式增强的能力,可以在服务端和客户端之间无缝过渡。
- 首次访问时返回完整的 HTML
- 按需加载客户端交互代码
- 保持应用的可访问性
这让我们能够构建既有良好首屏体验,又能提供丰富交互的现代应用,完美解决了在 2017 年 Netflix
所提出的问题。
总结
通过对上面这些案例的分析,我们可以看出
1. 不要轻信网络传言
网络上充斥着各种技术传言。虽然像上面这种完全虚构的假新闻容易识破,但有些传言会巧妙地利用真实数据和案例,通过夸张的描述来误导技术选型和决策,这类信息需要我们格外谨慎分辨。
例如:
svelte 放弃 TypeScript 改用 JSdoc 进行类型检查
这个确实是一个真的新闻,但是并不代表着 Typescript
的没落,实际上
- Svelte 团队选择 JSDoc 是为了减少编译时间
- 这是针对框架源码的优化,而不是面向使用者的建议
- Svelte 依然完整支持 TypeScript,用户代码可以继续使用 .ts/.ts
tauri 打包后的体积比 electron 小多了,我们应该放弃 electron 使用 tauri
技术选型不能仅仅看单一指标。虽然 tauri
的打包体积确实小于 electron
,但在开发体验、性能、稳定性、生态和社区支持等关键维度上都存在明显短板。
如果你尝试用 tauri
开发复杂应用,很可能会因为生态不完善、社区支持不足而陷入困境。当你遇到问题去 GitHub
寻找解决方案时,看到许多库已经一年未更新,就会明白为什么大多数团队仍在选择 electron
。
2. 历史的选择
2017 年的 Netflix 面临着复杂的业务需求,他们选择了当时最佳的解决方案 - 服务端渲染 + 客户端激活。这个方案虽然解决了问题,但也带来了一些困扰:
- 数据需要在服务端和客户端重复获取
- JavaScript bundle 体积过大
3. RSC 带来的改变
React Server Components
为这些历史遗留问题带来了全新的解决思路:
- 服务端渲染与客户端渲染完美融合
- 智能的代码分割,最小化客户端 bundle 体积
- 数据获取更高效,避免重复请求
- 渐进式增强,提供流畅的用户体验
4. 技术演进的启示
从 Netflix
2017 年的实践到今天的 RSC
,我们可以看到:
- 技术方案在不断进化,过去的最佳实践可能已不再适用
- RSC 不是简单的"回归服务端",而是开创了全新的开发模式
- 性能与开发体验不再是非此即彼的选择
RSC
代表了现代前端开发的新趋势 - 既保持了 React
强大的组件化能力,又通过创新的架构设计解决了历史难题。这让我们终于可以在性能和开发体验之间找到完美平衡。
来源:juejin.cn/post/7459029441039794211
一次失败的UI规范制定
前言
在公司中,前端使用了统一的组件element-ui,但是有一些页面大家并没有达成共识,造成了不同的团队开发出来的页面不同,公司决定在24年8月的迭代进行统一调整。在这个中间,我遇到了很多意料之外的问题,希望未来的你遇到了类似的问题,可以尽量避免
为什么会产生这个问题
这个问题我也思考过,大概有以下原因
- 我们有4个产品经理,每个人负责不同的模块,都有各自的风格,造成页面不统一
- 没有一个严格的UI规范,前端开发和测试并没有一个可以参考标准页面
- 22年至今,项目都在疯狂的迭代功能,并没有人停下来看看之前做的功能有哪些问题,我们该怎么去优化
项目背景
参与人:UI设计师、前端开发、产品(主要负责审核,并没参与讨论)、测试
牵头人:UI设计师
职责:找出问题点,整理为在线文档
解决者:前端
职责:整理问题点、改公告组件、输出文档
主要问题如下
- 弹窗(Dialog)组件大小不一。宽度有400px、700px、800px、940px、1250px、50%、60%等,用户在操作的时候,会感觉到页面时大时小,不统一
- 表格样式很多(Table)。搜索的列未对齐、分页码数未统一、表格后的操作栏千奇百怪、表头的标题有的进行了换行等
- 颜色的乱用。颜色有很多,有各种颜色的红色
- 弹窗表单的按钮位置不同。有的取消在左,有的取消在右面。
- 等等一些小问题就不一一列举了
弹窗组件大小不一
弹窗大小不统一部分截图
800px
600px
1180px
解决方案
我们在私服中clone了一份element-ui,直接修改了源码
默认制定了三个尺寸的size,small(600px), medium(900px),large(1200px),满足基本的需求。当需要支持特殊组件的时候,也可以直接设置Width满足需求
表格不统一
部分截图
上方的截图有几个问题
- 搜索条件(查找人员)没有和新增按钮对齐
- 离职和删除的按钮是比较敏感的操作,但是夹在了一堆按钮中间
- 操作按钮有的有icon,有的没icon,看着些许的混乱
进行修改后效果如下,页面看着更加的工整
解决方案如下
- 搜索条件的第一个在组件内直接进行计算宽度,防止业务端进行二次修改,如果是自定义的搜索条件,并且设置了宽度,需要手动进行修改
- 一些危险的操作:比如离职、删除统一放在最后,并且每个操作按钮都要有icon
表格按钮的调整
调整前
调整后
解决方案如下
表格后的按钮去除所有的icon,直接在组件内进行拦截,默认展示三个按钮,多余的放在dropDown内,当然也支持业务端设置显示几个按钮
核心部分代码如下
分页数据不统一
调整前
调整后
解决方案
分页条数统一改为(20,50,100)
考虑的因素为:之前的页面有15一页的。当页面过高的时候,会撑不满一个页面,造成table的页面空白,不太美观
弹窗中,下方的操作栏的按钮位置不统一
调整前
调整后
解决方案
所有的按钮都是取消在左,保存在右面,同时,confirm组件,在element-ui的代码中直接修改调整位置,减少业务端工作量
颜色的乱用
部分截图
解决方案
在主应用的根节点 使用css变量,在每个子应用的页面直接使用根节点的样式,避免子应用的颜色出现各种奇奇怪怪的颜色。
使用的地方
等等
当然还有一些基础颜色的修改、按钮最小宽度的修改、一些表格间距的调整等。这些和设计师、产品达成共识后,进行修改就行了,也就不一一列举了
交付给测试
- 我这边开发的差不多了,输出了一个文档,别的前端开发按照着文档进行开发。
- 测试按照文档进行编写测试用例
不好搞了
测试这边疯狂提bug。
还有一个小小的背景
测试这边其实是有一个绩效考核:bug提的越多,绩效越高
但是,我们这边项目上线的时候,其实还有一个要求,bug不能超过9个
这个UI规范制定,到这个功能的提测,只有10天就项目上线了。
有的功能并没有做,但是测试觉得不合理,就提了这个bug(比如表单的label也没有对齐,但是这个迭代并没有去做修复),造成我们前端开发这边bug超级多
同时,开发这边也会有绩效考核,bug越多绩效就会越低,会扣除工资,大家心态崩溃了,改的速度根本赶不上提的速度。
当然,也有部分功能是我这边测试不充分,造成业务端不好去实现
找领导协助
这个过程是个很折磨人的过程, 开发同事在抱怨测试乱提、测试也在提本次优化范围外的bug,内心也很着急
- 重新定义了测试的范围,超过范围的问题可以先记着,不再提bug,方便后续进行修改
- 前端开发的bug都先指派给我,我看看能否从底层解决,我解决后再转给研发经理(研发经理没有此项的考核),当底层解决不了,业务端解决后,也指派给我,这样前端的开发就没有这么暴躁了
如果再来一次UI规范的升级我会怎么做
- 先在每个团队中找一个人配合我,先升级页面后,进行灰度测试
- 在决定我们这个页面要成什么样子的时候,一定要拉上产品。设计师在做设计稿的时候,虽然每次都说了找产品的领导确认过了,但是领导还是比较忙, 并没有很多精力来做这件事情,可以找他们组内的一个资历比较深的产品一起讨论下。开发、UI设计师站的角度不同,一定要需要产品来参与讨论,做的页面才会更加的易用
- 限制测试的范围。 测试测着测试着会发散思维,导致改不完的bug,最终会导致开发没有了心态
- UI标准的功能,越早出来越好,越大后期需要投入的人力越多
来源:juejin.cn/post/7456685819047608355
uni-app 实现好看易用的抽屉效果
往期文章推荐:
一. 前言
我之前使用 uni-app
和 uniCloud
开发了一款软考刷题应用,在这个应用中,我使用了抽屉组件来实现一些功能,切换题库,如下图所示:
在移动应用开发中,抽屉(Drawer
)是一种常见的界面设计模式,这个组件可以在需要侧边导航或者额外信息展示的地方使用。它允许用户通过侧滑的效果打开一个菜单或额外的内容区域。
这种设计不仅能够节省屏幕空间,还能提供一种直观的交互方式。
例如,在电商应用中,可以将购物车或分类列表放在抽屉里;在新闻阅读器中,可以放置频道选择等;而在有题记刷题软件中,我主要用于题库的选择功能。
本文将介绍如何使用 uni-app 框架来实现一个简单的抽屉组件:DrawerWindow
。文末提供完整的代码示例,让你能够轻松地在 uni-app 中实现抽屉效果。
二. 实现分析
Vue 组件的结构通常由三个主要部分组成:模板(<template>
)、脚本(<script>
)和样式(<style>
),标准的的单文件组件(SFC
)结构。
uni-app 也是如此,在这个组件中,我们也将使用 Vue 的单文件组件(SFC
)结构,这意味着我们将在一个 .vue
文件中同时包含模板、脚本和样式。
接下来我们按照这个格式来简单实现一下。
1. 模板页面 (<template>
)
首先,模版页面是很简单的部分,我们需要创建一个基础的 Vue 组件,该组件包含了主页面、抽屉内容和关闭按钮三个部分。以下是组件的模板代码:
<template>
<view class="drawer-window-wrap">
<scroll-view scroll-y class="DrawerPage" :class="{ show: modalName === 'viewModal' }">
<!-- 主页面 -->
<slot></slot>
</scroll-view>
<!-- 关闭抽屉 -->
<view class="DrawerClose" :class="{ show: modalName === 'viewModal' }" @tap="hide">
<u-icon name="backspace"></u-icon>
</view>
<!-- 抽屉页面 -->
<scroll-view scroll-y class="DrawerWindow" :class="{ show: modalName === 'viewModal' }">
<slot name="drawer"></slot>
</scroll-view>
</view>
</template>
在模板部分,我们主要定义了三个主要元素:主页面、关闭按钮和抽屉页面。每个元素都有一个class
绑定,这个绑定会根据 modalName
的状态来决定是否添加 .show
类。
- 主页面 (
<scroll-view class="DrawerPage">
):
- 这个滚动视图代表应用的主要内容区域。
- 当抽屉打开时,它会被缩小并移向屏幕右侧。
- 提供默认插槽
<slot></slot>
,允许父组件传递自定义内容到这个位置。
- 关闭按钮 (
<view class="DrawerClose">
):
- 位于屏幕右侧的一个透明背景层,当点击时触发
hide()
方法来关闭抽屉。 - 包含了一个图标
<u-icon name="backspace"></u-icon>
,这里使用的是 uView UI 库中的图标组件。你可以选用其他组件库里的图标或者图片。
- 位于屏幕右侧的一个透明背景层,当点击时触发
- 抽屉页面 (
<scroll-view class="DrawerWindow">
):
- 这是抽屉本身的内容区域,通常包含菜单或其他附加信息。
- 同样地,定义特有的插槽名称,
<slot name="drawer"></slot>
允许从外部插入特定的内容。 - 抽屉默认是隐藏的,并且当显示时会有动画效果。
在这里,我们主要使用了 <slot>
元素来定义可以插入自定义内容的位置。modalName
属性用来控制抽屉的状态。
2. 逻辑处理 (<script>
)
接下来,逻辑处理其实也很简单,主要会定义打开和关闭抽屉的方法:
<script>
export default {
data() {
return {
modalName: null
}
},
methods: {
// 打开抽屉
show() {
this.modalName = 'viewModal';
},
// 关闭抽屉
hide() {
this.modalName = null;
}
}
}
</script>
- 数据 (
data
):
modalName
: 用于控制抽屉状态的数据属性。当它的值为'viewModal'
时,表示抽屉处于打开状态;否则,抽屉是关闭的。
- 方法 (
methods
):
show()
: 将modalName
设置为'viewModal'
,从而通过 CSS 样式控制抽屉显示。hide()
: 将modalName
重置为null
,控制抽屉隐藏。
当调用 show()
方法时,modalName
被设置为 'viewModal'
,这会触发 CSS 中的 .show
类,从而显示抽屉;反之,调用 hide()
方法则会隐藏抽屉。
3. 样式设计 (<style>
)
在这个组件中,其实要做好的在样式部分,主要是显示抽屉的动画部分。在主页面,我们主要定义了三个主要的样式类:主页面、关闭按钮和抽屉页面。
- 主页面样式 (
DrawerPage
):
- 初始状态下占据整个屏幕宽度和高度。
- 当抽屉打开时(即有
.show
类),页面会缩小并移动到屏幕右侧 85%的位置,同时增加阴影效果以模拟深度。
- 关闭按钮样式 (
DrawerClose
):
- 默认情况下是不可见且不响应用户交互的。
- 当抽屉打开时,按钮变为可见并可点击,提供了一种关闭抽屉的方式。
- 抽屉页面样式 (
DrawerWindow
):
- 初始状态下位于屏幕左侧外侧,不显示也不响应交互。
- 当抽屉打开时,抽屉平滑滑入屏幕内,变得完全可见且可以与用户互动。
- 动画与过渡
- 所有的
.show
类都带有transition: all 0.4s;
,这使得任何属性的变化都会有一个 0.4 秒的平滑过渡效果。 - 抽屉和主页面的
transform
属性被用来控制它们的位置和大小变化。 opacity
和pointer-events
属性确保在不需要时抽屉不会影响用户的操作。
- 所有的
如下代码所示,我们主要添加一些 CSS 样式来实现平滑的过渡效果以及视觉上的美观:
<style lang="scss">
// 省略其他样式...
.DrawerPage.show,
.DrawerWindow.show,
.DrawerClose.show {
transition: all 0.4s;
}
.DrawerPage.show {
transform: scale(0.9, 0.9) translateX(85vw);
box-shadow: 0 0 60rpx rgba(0, 0, 0, 0.2);
}
.DrawerWindow.show {
transform: scale(1, 1) translateX(0%);
opacity: 1;
pointer-events: all;
}
.DrawerClose.show {
width: 15vw;
color: #fff;
opacity: 1;
pointer-events: all;
}
</style>
以上的这些样式确保了当抽屉显示或隐藏时有流畅的动画效果,并且在不需要的时候不会影响用户的交互。
三. 完整代码
1. 完整抽屉组件,复制可使用
<template>
<view class="drawer-window-wrap">
<scroll-view scroll-y class="DrawerPage" :class="modalName == 'viewModal' ? 'show' : ''">
<!-- 主页面 -->
<slot></slot>
</scroll-view>
<!-- 关闭抽屉 -->
<view class="DrawerClose" :class="modalName == 'viewModal' ? 'show' : ''" @tap="hide()">
<u-icon name="backspace"></u-icon>
</view>
<!-- 抽屉页面 -->
<scroll-view scroll-y class="DrawerWindow" :class="modalName == 'viewModal' ? 'show' : ''">
<slot name="drawer"></slot>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
modalName: null
}
},
methods: {
// 打开抽屉
show() {
this.modalName = 'viewModal'
},
// 关闭抽屉
hide() {
this.modalName = null
}
}
}
</script>
<style lang="scss">
page {
width: 100vw;
overflow: hidden !important;
}
.DrawerPage {
position: fixed;
width: 100vw;
height: 100vh;
left: 0vw;
background-color: #f1f1f1;
transition: all 0.4s;
}
.DrawerPage.show {
transform: scale(0.9, 0.9);
left: 85vw;
box-shadow: 0 0 60rpx rgba(0, 0, 0, 0.2);
transform-origin: 0;
}
.DrawerWindow {
position: absolute;
width: 85vw;
height: 100vh;
left: 0;
top: 0;
transform: scale(0.9, 0.9) translateX(-100%);
opacity: 0;
pointer-events: none;
transition: all 0.4s;
background-image: linear-gradient(45deg, #1cbbb4, #2979ff) !important;
}
.DrawerWindow.show {
transform: scale(1, 1) translateX(0%);
opacity: 1;
pointer-events: all;
}
.DrawerClose {
position: absolute;
width: 40vw;
height: 100vh;
right: 0;
top: 0;
color: transparent;
padding-bottom: 50rpx;
display: flex;
align-items: flex-end;
justify-content: center;
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 0.6));
letter-spacing: 5px;
font-size: 50rpx;
opacity: 0;
pointer-events: none;
transition: all 0.4s;
}
.DrawerClose.show {
opacity: 1;
pointer-events: all;
width: 15vw;
color: #fff;
}
</style>
2. 在父组件中使用抽屉组件
在父组件中,可以通过以下简单的代码使用它,你可以继续进行丰富:
<template>
<drawer-window ref="drawerWindow">
<view class="main-container" @click="$refs.drawerWindow.show()">
主页面,点击打开抽屉
</view>
<view slot="drawer" class="drawer-container"> 抽屉页面 </view>
</drawer-window>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
.main-container,
.drawer-container {
font-weight: 700;
font-size: 20px;
text-align: center;
color: #333;
padding-top: 100px;
}
</style>
以上代码的实现效果如下图所示:
四. 小程序体验
以上的组件,来源于我独立开发的软考刷题小程序中的效果,想要体验或软考刷题的掘友可以参考以下文章,文末获取:
五. 结语
通过以上步骤,我们已经构建了一个基本的抽屉组件。当然,你也可以根据具体的应用场景对这个组件进行进一步的定制和优化。
来源:juejin.cn/post/7417374536670707727
个人或个体户,如何免费使用微信小程序授权登录
需求
个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部?
微信授权登录好处:
- 不用自己开发一个登录模块,节省开发和维护成本
- 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇
可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年要300元,小公司得省钱啊!
实现步骤说明
所有的步骤里包含四个对象,分别是本地后台
、本地微信小程序
、本地网页
、以及第三方微信后台
本地后台
调用微信后台
的https://api.weixin.qq.com/cgi-bin/token
接口,get
请求,拿到返回的access_token
;本地后台
根据拿到的access_token
,调用微信后台
的https://api.weixin.qq.com/wxa/getwxacodeunlimit
接口,得到二维码图片文件,将其输出传递给本地网页
显示本地微信小程序
扫本地网页
的二维码图片,跳转至小程序登录页面,通过wx.login
方法,在success
回调函数内得到code
值,并将该值传递给本地后台
本地后台
拿到code
值后,调用微信后台
的https://api.weixin.qq.com/sns/jscode2session
接口,get
请求,得到用户登录的openid
即可。
注意点:
- 上面三个微信接口
/cgi-bin/token
、/getwxacodeunlimit
、/jscode2session
必须由本地后台
调用,微信小程序那边做了前端限制;
本地网页
如何得知本地微信小程序
已扫码呢?
本地微信小程序
将code
,通过A接口
,将值传给后台,后台拿到openid
后,再将成功结果返回给本地微信小程序
;同时,本地网页
不断地轮询A接口
,等待后台拿到openid
后,便显示登录成功页面。
微信小程序核心代码
Page({
data: {
theme: wx.getSystemInfoSync().theme,
scene: "",
jsCode: "",
isLogin: false,
loginSuccess: false,
isChecked: false,
},
onLoad(options) {
const that = this;
wx.onThemeChange((result) => {
that.setData({
theme: result.theme,
});
});
if (options !== undefined) {
if (options.scene) {
wx.login({
success(res) {
if (res.code) {
that.setData({
scene: decodeURIComponent(options.scene),
jsCode: res.code,
});
}
},
});
}
}
},
handleChange(e) {
this.setData({
isChecked: Boolean(e.detail.value[0]),
});
},
formitForm() {
const that = this;
if (!this.data.jsCode) {
wx.showToast({
icon: "none",
title: "尚未微信登录",
});
return;
}
if (!this.data.isChecked) {
wx.showToast({
icon: "none",
title: "请先勾选同意用户协议",
});
return;
}
wx.showLoading({
title: "正在加载",
});
let currentTimestamp = Date.now();
let nonce = randomString();
wx.request({
url: `A接口?scene=${that.data.scene}&js_code=${that.data.jsCode}`,
header: {},
method: "POST",
success(res) {
wx.hideLoading();
that.setData({
isLogin: true,
});
if (res.statusCode == 200) {
that.setData({
loginSuccess: true,
});
} else {
if (res.statusCode == 400) {
wx.showToast({
icon: "none",
title: "无效请求",
});
} else if (res.statusCode == 500) {
wx.showToast({
icon: "none",
title: "服务内部错误",
});
}
that.setData({
loginSuccess: false,
});
}
},
fail: function (e) {
wx.hideLoading();
wx.showToast({
icon: "none",
title: e,
});
},
});
},
});
scene
为随机生成的8位数字
本地网页核心代码
let isInit = true
function loginWx() {
isInit = false
refreshQrcode()
}
function refreshQrcode() {
showQrLoading = true
showInfo = false
api.get('/qrcode').then(qRes => {
if (qRes.status == 200) {
imgSrc = `${BASE_URL}${qRes.data}`
pollingCount = 0
startPolling()
} else {
showToast = true
toastMsg = '二维码获取失败,请点击刷新重试'
showInfo = true
}
}).finally(() => {
showQrLoading = false
})
}
// 开始轮询
// 1000毫秒轮询一次
function startPolling() {
pollingInterval = setInterval(function () {
pollDatabase()
}, 1000)
}
function pollDatabase() {
if (pollingCount >= maxPollingCount) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
return
}
pollingCount++
api.get('/result').then(res => {
if (res.status == 200) {
clearInterval(pollingInterval)
navigate('/os', { replace: true })
} else if (res.status == 408) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
}
})
}
html的部分代码如下所示
<button class="btn" on:click={loginWx}>微信登录</button>
<div id="qrcode" class="relative mt-10">
{#if imgSrc}
<img src={imgSrc} alt="二维码图片"/>
{/if}
{#if showQrLoading}
<div class="mask absolute top-0 left-0 w-full h-full z-10">
<Loading height="12" width="12"/>
</div>
{/if}
</div>
尾声
若需要完整代码,或想知道如何申请微信小程序
,欢迎大家关注或私信我哦~~
附上网页微信授权登录动画、以及小程序登录成功后的截图
来源:juejin.cn/post/7351649413401493556
基于uniapp带你实现了一个好看的轮播图组件
背景
最近,朋友说在做uniapp
微信小程序项目时接到一个需求,需要实现一个类似上图的轮播图效果,问我有没有什么好的实现方案,他也和我说了他的方案,比如让产品直接把图片切成两部分再分别进行上传,但是我觉得这个方案不够灵活,当每次修改banner
图片时,都得让产品把图切好再分别上传。下文将探究一个更好的实现思路。
需求分析
由文章顶部的gif
动图,我们可以看出每次执行轮播动画时,只会裁剪图片的中间部分进行滚动,图片的其余部分保持不变,等待轮播动画执行完成后,再淡化背景图片切换到下一张轮播图。
从中可得出两点关键信息:
1.两种相同图片堆叠在一起,一张背景图(大图),一张轮播图(小图);
2.需要对图片中间部分进行裁剪,并且定位到刚好能够和背景图重合得区域;
根据以上得出的信息,我们还需解决两个疑问:
1.如何对图片进行裁剪?
2.图片裁剪后如何定位和背景图重合的区域?
前端裁剪图片可以使用canvans
,但是兼容性不好,太麻烦!还有没有好一点的方法呢?当然有,参考css
中的雪碧图进行图片裁剪显示!!但还是有些麻烦,还有没有简单的方式呢?有,那就是使用css
属性overflow: hidden;
进行图片裁剪,下文也主要是讲这个方案。
开始实现
vi-swiper.vue
<template>
<view class="v-banner" :style="[boxStyle]">
<swiper class="v-swiper" autoplay indicator-dots circular
@animationfinish="onAnimationfinish"
>
<swiper-item class="v-swiperi-tem" v-for="(url,index) in list" :key="index">
<image class="v-img" :src="url" mode="scaleToFill"></image>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
props: {
// 当前索引
value: {
type: Number,
default: 0
},
// 轮播图列表
list: {
type: Array,
default: () => []
}
},
computed: {
boxStyle() {
return {
backgroundImage: `url(${this.list[this.value]})`,
// 开启background-image转场动画
transition: '1s background-image'
}
}
},
methods: {
// 轮播图动画结束后更新底部更新图索引
onAnimationfinish(e) {
this.$emit('input', e.detail.current)
}
}
}
</script>
<style lang="scss">
/*sass变量,用于动态计算*/
$swiperWidth: 650rpx;
$swiperHeight: 350rpx;
$verticalPadding: 60rpx;
$horizontalPadding: 50rpx;
$imgWidth: $swiperWidth + $horizontalPadding * 2;
$imgHeight: $swiperHeight + $horizontalPadding * 2;
.v-banner {
/* 因为需要根据内边距动态调节背景图宽高,所以设为行内块 */
display: inline-block;
// 背景图铺满容器
background-size: 100% 100%;
padding: $verticalPadding $horizontalPadding;
.v-swiper {
height: $swiperHeight;
width: $swiperWidth;
// 裁剪图片
overflow: hidden;
.v-swiperi-tem {
.v-img {
width: $imgWidth;
height: $imgHeight;
}
}
}
}
</style>
以上代码主要实现思路是让底部背景图大小和轮播图大小相同使两种重合,尺寸相等才能重合。swiper
轮播图容器组件固定宽高,使用overflow: hidden;
来裁剪内部图片, 然后给底部背景图容器使用padding
内边距来撑开容器,达到两种图片堆叠的效果;图片转场通过transition
设置动画。
以上组件页面显示效果如下:
发现两张图片还没有重合在一起,原因是两张图片虽然大小一致了,但是位置不对,如下图所示:
那么由上图描绘的信息可知,想要两张图重合,那么需要把轮播图分别向上和向左移动对应的内边距距离即可,我们可以通过给轮播图设置负的外边距实现
,样式如下:
.v-img {
...
// 使两张图片重合
margin-top: -$verticalPadding;
margin-left: -$horizontalPadding;
}
效果如下图所示:
到这我们实现了图片的裁剪和重合,已经实现了最终效果。完整的代码会在文章结尾附上。
另外,我已经把这个组件发布到了uniapp插件市场
,并且做了相应封装,可灵活定制轮播图大小及相关样式,感兴趣的可以点击这里:
vi-swiper轮播图,跳转到文档查阅源码或使用。
总结
这个堆叠轮播图效果实现起来不难,主要是要找对思路,一句话概括就是两张大小相等的图片进行重合,轮播图容器对图片进行裁剪。
完整代码
vi-swiper.vue
<template>
<view class="v-banner" :style="[boxStyle]">
<swiper class="v-swiper" autoplay indicator-dots circular
@animationfinish="onAnimationfinish"
>
<swiper-item class="v-swiperi-tem" v-for="(url,index) in list" :key="index">
<image class="v-img" :src="url" mode="scaleToFill"></image>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
props: {
// 当前索引
value: {
type: Number,
default: 0
},
// 轮播图列表
list: {
type: Array,
default: () => []
}
},
computed: {
boxStyle() {
return {
backgroundImage: `url(${this.list[this.value]})`,
// 开启background-image转场动画
transition: '1s background-image'
}
}
},
methods: {
// 轮播图动画结束后更新底部更新图索引
onAnimationfinish(e) {
this.$emit('input', e.detail.current)
}
}
}
</script>
<style lang="scss">
/*sass变量,用于动态计算*/
$swiperWidth: 650rpx;
$swiperHeight: 350rpx;
$verticalPadding: 60rpx;
$horizontalPadding: 50rpx;
$imgWidth: $swiperWidth + $horizontalPadding * 2;
$imgHeight: $swiperHeight + $horizontalPadding * 2;
.v-banner {
/* 因为需要根据内边距动态调节背景图宽高,所以设为行内块 */
display: inline-block;
// 背景图铺满容器
background-size: 100% 100%;
padding: $verticalPadding $horizontalPadding;
.v-swiper {
height: $swiperHeight;
width: $swiperWidth;
// 裁剪图片
overflow: hidden;
.v-swiperi-tem {
.v-img {
width: $imgWidth;
height: $imgHeight;
margin-top: -$verticalPadding;
margin-left: -$horizontalPadding;
}
}
}
}
</style>
来源:juejin.cn/post/7377245069474021412
Java 泛型中的通配符 T,E,K,V,?有去搞清楚吗?
前言
不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。
不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。
泛型有什么用?
在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。
Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。
- 类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。
// 非泛型写法(存在类型转换风险)
List list1 = new ArrayList();
list1.add("a");
Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException
// 泛型写法(编译时检查类型)
List list2 = new ArrayList<>();
// list.add(1); // 编译报错
list2.add("a");
String str = list2.get(0); // 无需强制转换
- 消除代码强制类型转换:减少了一些类型转换操作。
// 非泛型写法
Map map1 = new HashMap();
map1.put("user", new User());
User user1 = (User) map1.get("user");
// 泛型写法
Map map2 = new HashMap<>();
map2.put("user", new User());
// 自动转换
User user2 = map2.get("user");
在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。
Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。
- 类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。
// 非泛型写法(存在类型转换风险)
List list1 = new ArrayList();
list1.add("a");
Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException
// 泛型写法(编译时检查类型)
Listlist2 = new ArrayList<>();
// list.add(1); // 编译报错
list2.add("a");
String str = list2.get(0); // 无需强制转换
- 消除代码强制类型转换:减少了一些类型转换操作。
// 非泛型写法
Map map1 = new HashMap();
map1.put("user", new User());
User user1 = (User) map1.get("user");
// 泛型写法
Map map2 = new HashMap<>();
map2.put("user", new User());
// 自动转换
User user2 = map2.get("user");
3.代码复用:可以支持多种数据类型,不要重复编写代码,例如:我们常用的统一响应结果类。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
/**
* 响应状态码
*/
private int code;
/**
* 响应信息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 时间戳
*/
private long timestamp;
其他代码省略...
- 增强可读性:通过类型参数就直接能看出要填入什么类型。
List list = new ArrayList<>();
泛型里的通配符
我们在使用泛型的时候,经常会使用或者看见多种不同的通配符,常见的 T,E,K,V,?这几种,相信大家一定不陌生,但是真的问你他们有什么作用?有什么区别时,很多人应该是不能很好的介绍它们的,接下来我就来给大家介绍介绍。
T,E,K,V
- T(Type) T表示任意类型参数,我们举个例子
pubile class A{
prvate T t;
//其他省略...
}
//创建一个不带泛型参数的A
A a = new A();
a.set(new B());
B b = (B) a.get();//需要进行强制类型转换
//创建一个带泛型参数的A
A a = new A();
a.set(new B());
B b = a.get();
- E(Element) E表示集合中的元素类型
List list = new ArrayList<>();
- K(Key) K表示映射的键的数据类型
Map map = new HashMap<>();
- V(Value) V表示映射的值的数据类型
Map map = new HashMap<>();
- T(Type) T表示任意类型参数,我们举个例子
pubile class A{
prvate T t;
//其他省略...
}
//创建一个不带泛型参数的A
A a = new A();
a.set(new B());
B b = (B) a.get();//需要进行强制类型转换
//创建一个带泛型参数的A
A a = new A();
a.set(new B());
B b = a.get();
List list = new ArrayList<>();
Map map = new HashMap<>();
Map map = new HashMap<>();
通配符 ?
- 无界通配符 表示未知类型,接收任意类型
// 使用无界通配符处理任意类型的查询结果
public void logQueryResult(List resultList) {
resultList.forEach(obj -> log.info("Result: {}", obj));
}
- 上界通配符 表示类型是T或者是子类
// 使用上界通配符读取缓存
public extends Serializable> T getCache(String key, Class clazz) {
Object value = redisTemplate.opsForValue().get(key);
return clazz.cast(value);
}
- 下界通配符 表示类型是T或者是父类
// 使用下界通配符写入缓存
public void setCache(String key, super Serializable> value) {
redisTemplate.opsForValue().set(key, value);
}
综合示例:
import java.util.ArrayList;
import java.util.List;
public class Demo {
//实体类
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating");
}
}
class Husky extends Dog {
@Override
void eat() {
System.out.println("Husky is eating");
}
}
/**
* 无界通配符
*/
// 只能读取元素,不能写入(除null外)
public static void printAllElements(List list) {
for (Object obj : list) {
System.out.println(obj);
}
// list.add("test"); // 编译错误!无法写入具体类型
list.add(null); // 唯一允许的写入操作
}
/**
* 上界通配符
*/
// 安全读取为Animal,但不能写入(生产者场景)
public static void processAnimals(List animals) {
for (Animal animal : animals) {
animal.eat();
}
// animals.add(new Dog()); // 编译错误!无法确定具体子类型
}
/**
* 下界通配符
*/
// 安全写入Dog,读取需要强制转换(消费者场景)
public static void addDogs(Listsuper Dog> dogList) {
dogList.add(new Dog());
dogList.add(new Husky()); // Husky是Dog子类
// dogList.add(new Animal()); // 编译错误!Animal不是Dog的超类
Object obj = dogList.get(0); // 读取只能为Object
if (obj instanceof Dog) {
Dog dog = (Dog) obj; // 需要显式类型转换
}
}
public static void main(String[] args) {
// 测试无界通配符
List strings = List.of("A", "B", "C");
printAllElements(strings);
List integers = List.of(1, 2, 3);
printAllElements(integers);
// 测试上界通配符
List dogs = new ArrayList<>();
dogs.add(new Dog());
processAnimals(dogs);
List huskies = new ArrayList<>();
huskies.add(new Husky());
processAnimals(huskies);
// 测试下界通配符
List animals = new ArrayList<>();
addDogs(animals);
System.out.println(animals);
List
- 无界通配符 表示未知类型,接收任意类型
// 使用无界通配符处理任意类型的查询结果
public void logQueryResult(List resultList) {
resultList.forEach(obj -> log.info("Result: {}", obj));
}
// 使用上界通配符读取缓存
public extends Serializable> T getCache(String key, Class clazz) {
Object value = redisTemplate.opsForValue().get(key);
return clazz.cast(value);
}
// 使用下界通配符写入缓存
public void setCache(String key, super Serializable> value) {
redisTemplate.opsForValue().set(key, value);
}
综合示例:
import java.util.ArrayList;
import java.util.List;
public class Demo {
//实体类
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating");
}
}
class Husky extends Dog {
@Override
void eat() {
System.out.println("Husky is eating");
}
}
/**
* 无界通配符
*/
// 只能读取元素,不能写入(除null外)
public static void printAllElements(List list) {
for (Object obj : list) {
System.out.println(obj);
}
// list.add("test"); // 编译错误!无法写入具体类型
list.add(null); // 唯一允许的写入操作
}
/**
* 上界通配符
*/
// 安全读取为Animal,但不能写入(生产者场景)
public static void processAnimals(List animals) {
for (Animal animal : animals) {
animal.eat();
}
// animals.add(new Dog()); // 编译错误!无法确定具体子类型
}
/**
* 下界通配符
*/
// 安全写入Dog,读取需要强制转换(消费者场景)
public static void addDogs(Listsuper Dog> dogList) {
dogList.add(new Dog());
dogList.add(new Husky()); // Husky是Dog子类
// dogList.add(new Animal()); // 编译错误!Animal不是Dog的超类
Object obj = dogList.get(0); // 读取只能为Object
if (obj instanceof Dog) {
Dog dog = (Dog) obj; // 需要显式类型转换
}
}
public static void main(String[] args) {
// 测试无界通配符
List strings = List.of("A", "B", "C");
printAllElements(strings);
List integers = List.of(1, 2, 3);
printAllElements(integers);
// 测试上界通配符
List dogs = new ArrayList<>();
dogs.add(new Dog());
processAnimals(dogs);
List huskies = new ArrayList<>();
huskies.add(new Husky());
processAnimals(huskies);
// 测试下界通配符
List animals = new ArrayList<>();
addDogs(animals);
System.out.println(animals);
List
总结
在村里上班的程序员
今天是在山上办公的第二天,现在时间是晚上的10点36分,刚刚写完今日份工作日报又刷10分钟手机的我决定写上一篇日记简单记下最近两天我的日常。
我的上班,是跟着国家法定节假日安排走的,前天初八,是我25年年后上班的第一天。
阿妮的假期还未结束,她决定回一趟娘家,由此,我在初八一大早——7点半到8点50分——先送阿妮进城,送到她的外公外婆家。接着,我在外公外婆家办公一天。
初八那天的午饭
大嘎嘎(外公)小嘎嘎(外婆)总是很欢迎我和阿妮的到来,午饭晚饭,我连连摇头不停摆手依然吃得很撑。
我干活的节奏,和年前是一样的:上午9点到12点多些,下午2点到6点7点,晚上8点9点到10点更多些,都是我的干活——单纯指做工作上的事情——时间。
阿妮晚上约了她的初中同学一起聚餐,我带上电脑跟着大家一起。烧烤桌上,对着电脑试用VS Code新插件Cline的我,是没有感受到心理压力的;烧烤桌上的干活时间,我是专注的。
--
初九一大早,我从县城回家回到山上,带着两个帽子和几个包子。帽子是初八晚上一起逛楼下街道时阿妮买给我父亲母亲的,包子,是我自己想吃再给父亲母亲带着的。
初九是个很好日子,返程路上,我碰见的接亲队伍有4个。
初九的天气,很有些特色,上午飘雪,下午大太阳,晚上很有些冷。我在山上的上班,上午烤电火,下午太阳下干活,晚上的干活,有一部分是洗漱完毕躺床上完成的。
我的办公桌椅
昨天的在家办公体验不错,但我需要对自己诚实些,当时我的内心,有两个小小担忧存在。
第一个关于当前的环境,下午父母外出吃酒只留我一个人在家,我家近处只有邻居一户。到了夜晚,整个世界很安静,安静到我能听见自己的呼吸,我的一个小小动作都能发出很大声响,我承认自己理智相信科学,但同时我也看过许多志怪故事。对的,晚上一个人在家,我很有些害怕。
第二个是工作内容,我总觉得自己近来的效率并没有很高。其实不太对,其实效率应该是ok的,不ok的是我关于AI技术的知识储备太少,而我并没有给自己一个好的计划去每天进步一点点。emmm~其实还是有些不太对,这个概念有些大,更准确的是,我感觉自己在有些工作内容上,还可以做的好一些。
晚上11点又过10分钟,背完单词的我睡下,脑子中那轻微恐惧并不散去,直到深睡。
很神奇的是,今天早上6点多醒来,那对于安静的害怕,一点都不剩下。
--
整个过年前后,我都感觉自己很忙,忙到已经坚持周更四年多的公众号都不能在周日及时更新。今天即便晚睡也想要写上一篇日记的情绪,来源于下午6点发生的一件,一件让我感觉有些窘迫的事情。
伯娘进门问我:”你们作料放啊哪儿的呢?“
”你老汉喊我来给你煮一碗面条。我给他说,叫你到我们那里去吃,你老汉儿说你走不开哈,那我就来给你煮面条了啊。“
当时我真的很窘迫,感觉很有些尴尬,而同时内心,又会有些开心情绪蔓延。
我是很想给自己找一个借口的:我以为父母吃酒,会在下午三点左右回家。我的早餐,是昨天剩下的两个肉包;我的午餐,是一碗加了母亲自制麻糖的泡阴米子,再加一个橙子与一个苹果。我认为肉包、橙子与苹果,是可以撑到母亲回家的。
工作日,我真的不想做饭。
即便并不很饿,但我确实有让阿雪对母亲说:“你再不回家,你的大娃娃和你的喂的猪,都要饿趴了哦。”
很是粘人的狗子
对的,今天干活间隙,我有喂猪——猪食当然是母亲提前煮好的,遛狗——我只需要前面走着,狗自然在后面跟着。
伯娘烧火又煮面,花半小时。
我为面条,拍了一张好看照片。
好吃的清汤面与好看的照片
时间已经是晚上的11点20分,且快快睡觉。
今晚,我一点都不害怕。
来源:juejin.cn/post/7468970428918906906
入职第一天,看了公司代码,牛马沉默了
入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。
打开代码发现问题不断
- 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置
一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为
prop_c.setProperty(key, value);
value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
- 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
- 日志打印居然sout和log混合双打
先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;
4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;
5.随意更改生产数据库,出不出问题全靠开发的职业素养;
6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上
<type>pom
来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教
以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;
那有什么优点呢:
- 不用太怎么写文档
- 束缚很小
- 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)
解决之道
怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar &
来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,
其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;
我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!
来源:juejin.cn/post/7371986999164928010
React:我做出了一个违背祖训的决定!
React 的 useEffect
,大家都熟吧?这玩意儿就像个万金油,啥副作用都能往里塞:取数据、搞订阅、手动操作 DOM……反正渲染完了,它帮你擦屁股。它帮你擦屁股。
但是!React 团队最近搞了个大新闻,他们居然要对 useEffect
动刀子了!而且,这次的改动,用他们的话说,简直是——“违背祖训”!“违背祖训”!
useEffect
要变身?实验性 CRUD 支持来了!
新的 useEffect
签名,整合了以前一个实验性的 Hook useResourceEffect
的功能,现在长这样:
function useEffect(
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void
是不是看得一脸懵逼?别慌,我来给你翻译翻译。
以前的 useEffect
,创建和清理都挤在一个函数里,跟两口子似的,难舍难分。举个栗子:
useEffect(() => {
// 创建阶段:发起请求
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => setData(data));
// 清理阶段:取消请求
return () => {
controller.abort();
};
}, [someDependency]);
看到了吧?创建(发起请求)和清理(取消请求)都得写在一个函数里。
现在好了,React 团队直接把它们拆散了!新签名里,创建、更新、销毁,各司其职,清清楚楚:
create
: 专门用来造东西(比如,发个请求,整个订阅)。createDeps
:create
的跟屁虫,它们一变,create
就得重新执行。update
(可选): 想更新?找它!它会拿着create
造出来的东西,给你更新。updateDeps
(可选):update
的小弟,它们一变,update
就得带着老东西,重新来过。destroy
: 可选的销毁时候的回调。
“祖宗之法不可变”?React:我就变!
自从 Hook 在 2016 年推出,到现在已经九年了!九年啊!“组合优于继承”、“函数式编程”,这些 React 的“祖训”,各路大神、大V,哪个没给你讲过几百遍?哪个没给你讲过几百遍?
useEffect
把创建和清理揉在一起,也算是“组合”的一种体现,深入人心。可现在呢?React 团队居然亲手把它拆了!这……这简直是自己打自己的脸啊!
不过,话说回来,这种拆分,对于那些复杂的副作用,确实更清晰、更好管理。以前,你可能得在一个 useEffect
里写一堆 if...else
,现在,你可以把它们放到不同的阶段,代码更清爽,逻辑更分明。
注意!前方高能预警!
这个 CRUD 功能,现在还是个“试验品”,React 团队还没打算把它放出来。你要是头铁,非要试试,记得先去把 feature flag 打开。不然,你会看到这个:
useEffect CRUD overload is not enabled in this build of React.
重要的事情说三遍:这都是猜的!猜的!猜的!猜的!猜的!猜的!
现在,关于这个新特性,React 团队还没放出任何官方文档或者 RFC。所以,这篇文章,你看看就好,别太当真。它就是基于代码瞎猜的。等官方消息出来了,咱们再好好研究!
来源:juejin.cn/post/7470819965014474771
为什么程序员痴迷于错误信息上报?
前言
上一篇已经聊过日志上报的调度原理,讲述如何处理日志上报堆积、上报失败以及上报优化等方案。
从上家公司开始,监控就由我们组身强体壮的同事来负责,而我只能开发Admin
和H5
;经过一系列焦虑的面试后,咸鱼翻身,这辈子我也做上监控了。千万不要以为我是因为监控的重要性才这么执着,人往往得不到的东西才是最有吸引力的。
在写这篇文章时,我也在思考,为什么走到哪里都会有一群程序员喜欢封装监控呢?即使换个公司、换个组,依然可能需要有人来迭代监控。嗯,话不多说,先点关注,正文开始。
错误监控的核心价值
如果让你封装一个前端监控,你会怎么设计监控的上报优先级?
对于一个网页来说,能否带给用户好的体验,核心指标就是 白屏时长 和 FMP时长,这两项指标直接影响用户的 留存率 和 体验。
下面通过数据加强理解:
- 白屏时间 > 3秒 导致用户流失率上升47%
- 接口错误率 > 0.5% 造成订单转化率下降23%
- JS错误数 > 1/千次访问 预示着系统稳定性风险
设想一下,当你访问页面时白屏等待了3秒,并且页面没有骨架屏或者Loading态时,你会不会觉得这个页面挂了?这时候如果我们的监控优先关注的是性能,可能用户已经退出了,我们的上报还没调用到。
在这个白屏等待的过程中,JS Error可能已经打印在控制台了,接口可能已经返回了错误信息,但是程序员却毫无感知。
优先上报错误信息,本质是为了提升生产环境的错误响应速度、减少生产环境的损失、提高上线流程的规范。以下是错误响应的黄金时间轴:
时间窗口 | 响应动作 | 业务影响 |
---|---|---|
< 1分钟 | 自动熔断异常接口 | 避免错误扩散 |
1-5分钟 | 触发告警通知值班人员 | 降低MTTR(平均修复时间) |
>5分钟 | 生成故障诊断报告 | 优化事后复盘流程 |
重要章节
一:错误类型,你需要关注的五大场景
技术本质:任何错误收集系统都需要先明确错误边界。前端错误主要分为两类: 显性错误(直接阻断执行)和 隐性错误(资源加载、异步异常等)。
// 显性错误(同步执行阶段)
function criticalFunction() {
undefinedVariable.access(); // ReferenceError
}
// 隐性错误(异步场景)
fetchData().then(() => {
invalidJSON.parse(); // 异步代码中的错误
});
关键分类: 通过错误本质将前端常见错误分为5种类型,图示如下。
- 语法层错误(SyntaxError)
ESLint 可拦截,但运行时需注意动态语法(如eval
,这个用法不推荐)。 - 运行时异常
错误的时机场景大部分是在页面渲染完成后,用户对页面发生交互行为,触发JS执行异常。以下是模拟报错的一个例子,用于学习。 // 典型场景 element.addEventListener('click', () => { throw new Error('Event handler crash'); }); - 资源加载失败
常见的资源比如图片、JS脚本、字体文件、外链引入的三方依赖等。我们可以通过全局监听处理,比如使用document.addEventListener('error', handler, true)
来捕获资源加载失败的情况。但需要注意以下几点:
、
uni-app初体验,如何实现一个外呼APP
起因
2024年3月31日,我被公司裁员了。
2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。
2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回上海开始找工作。结果环境比预想的还要差啊,以前简历放开就有人找,现在每天投个几十封都是石沉大海。。。
2024年4月15日,有个好朋友找我,想让我给他们公司开发一个“拨号APP”(主要原因其实是这个好哥们想让我多个赚钱门路😌),主要的功能就是在他们的系统上点击一个“拨号”按钮,然后员工的工作手机上就自动拨打这个号码。
可行性分析
涉及到的修改:
- 系统前后端
- 拨号功能的APP
拿到这个需求之后,我并没有直接拒绝或者同意,而是先让他把他公司那边的的源代码发我了一份,大致看了一下使用的框架,然后找一些后端的朋友看有没人有人一起接这个单子;而我自己则是要先看下能否实现APP的功能(因为我以前从来没有做过APP!!!)。
我们各自看过自己的东西,然后又沟通了一番简单的实现过程后达成了一致,搞!
因为我这边之前的技术栈一直是VUE,所以决定使用uni-app实现,主要还是因为它的上手难度会低很多。
第一版
需求分析
虽说主体的功能是拨号,但其实是隐含很多辅助性需求的,比如拨号日志、通时通次统计、通话录音、录音上传、后台运行,另外除了这些外还有额外的例如权限校验、权限引导、获取手机号、获取拨号状态等功能需要实现。
但是第一次预算给的并不高,要把这些全部实现显然不可能。因此只能简化实现功能实现。
- 拨号APP
- 权限校验
- 实现部分(拨号、录音、文件读写)
- ❌权限引导
- 查询当前手机号
- 直接使用input表单,由用户输入
- 查询当前手机号的拨号任务
- 因为后端没有socket,使用setTimeout模拟轮询实现。
- 拨号、录音、监测拨号状态
- 根据官网API和一些安卓原生实现
- 更新任务状态
- 告诉后端拨号完成
- ❌通话录音上传
- ❌通话日志上传
- ❌本地通时通次统计
- 程序运行日志
- 其他
- 增加开始工作、开启录音的状态切换
- 兼容性,只兼容安卓手机即可
- 权限校验
基础设计
一个input框来输入用户手机号,一个开始工作的switch,一个开启录音的切换。用户输入手机号,点击开始工作后开启轮询,轮询到拨号任务后就拨号同时录音,同时监听拨号状态,当挂断后结束录音、更新任务状态,并开启新一轮的轮询。
开干
虽然本人从未开发过APP,但本着撸起袖子就是干的原则,直接打开了uni-app的官网就准备开怼。
1、下载 HbuilderX。
2、新建项目,直接选择了默认模板。
3、清空 Hello页面,修改文件名,配置路由。
4、在vue文件里写主要的功能实现,并增加 Http.js
、Record.js
、PhoneCall.js
、Power.js
来实现对应的模块功能。
⚠️关于测试和打包
运行测试
在 HbuilderX 中点击“运行-运行到手机或模拟器-运行到Android APP基座”会打开一个界面,让你选择运行到那个设备。这是你有两种选择:
- 把你手机通过USB与电脑连接,然后刷新列表就可以直接运行了。
- 很遗憾,可能是苹果电脑与安卓手机的原因,插上后检测不出设备😭。。。
- 安装Android Studio,然后通过运行内置的模拟器来供代码运行测试。
- 这种很麻烦,要下载很久,且感觉测试效果并不好,最好还是用windows电脑连接手机的方法测试。
关于自定义基座和标准基座的差别,如果你没有买插件的话,直接使用基准插座就好。如果你要使用自定义基座,就首先要点击上图中的制作自定义基座,然后再切换到自定义基座执行。
但是不知道为什么,我这里一直显示安装自定义基座失败。。。
打包测试
除了以上运行测试的方法外,你还有一种更粗暴的测试方法,那就是打包成APP直接在手机上安装测试。
点击“发行-原生APP 云打包”,会生成一个APK文件,然后就可以发送到手机上安装测试。不过每天打包的次数有限,超过次数需要购买额外的打包服务或者等第二天打包。
我最终就是这样搞得,真的我哭死,我可能就是盲调的命,好多项目都是盲调的。
另外,在打包之前我们首先要配置manifest.json
,里面包含了APP的很多信息。比较重要的一个是AppId,一个是App权限配置。参考uni-app 权限配置、Android官方权限常量文档。以下是拨号所需的一些权限:
// 录制音频
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 修改音频设置
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
// 照相机
<uses-permission android:name="android.permission.CAMERA" />
// 写入外部存储
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// 读取外部存储
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
// 读取电话号码
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
// 拨打电话
<uses-permission android:name="android.permission.CALL_PHONE" />
// 呼叫特权
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
// 通话状态
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
// 读取拨号日志
<uses-permission android:name="android.permission.READ_CALL_LOG" />
// 写入拨号日志
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
// 读取联系人
<uses-permission android:name="android.permission.READ_CONTACTS" />
// 写入联系人
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
// 读取SMS?
<uses-permission android:name="android.permission.READ_SMS" />
// 写入设置
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
// 唤醒锁定?
<uses-permission android:name="android.permission.WAKE_LOCK" />
// 系统告警窗口?
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// 接受完整的引导?
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
⚠️权限配置这个搞了很长时间,即便现在有些权限还是不太清楚,也不知道是不是有哪些权限没有配置上。。。
⚠️权限校验
1、安卓 1
好像除了这样的写法还可以写"scope.record"
或者permission.CALL_PHONE
。
permision.requestAndroidPermission("android.permission.CALL_PHONE").then(res => {
// 1 获得权限 2 本次拒绝 -1 永久拒绝
});
2、安卓 2
plus.android.requestPermissions(["android.permission.CALL_PHONE"], e => {
// e.granted 获得权限
// e.deniedPresent 本次拒绝
// e.deniedAlways 永久拒绝
});
3、uni-app
这个我没测试,AI给的,我没有用这种方法。有一说一,百度AI做的不咋地。
// 检查权限
uni.hasPermission({
permission: 'makePhoneCall',
success() {
console.log('已经获得拨号权限');
},
fail() {
// 示例:请求权限
uni.authorize({
scope: 'scope.makePhoneCall',
success() {
console.log('已经获得授权');
},
fail() {
console.log('用户拒绝授权');
// 引导用户到设置中开启权限
uni.showModal({
title: '提示',
content: '请在系统设置中打开拨号权限',
success: function(res) {
if (res.confirm) {
// 引导用户到设置页
uni.openSetting();
}
}
});
}
});
}
});
✅拨号
三种方法都可以实现拨号功能,只要有权限,之所以找了三种是为了实现APP在后台的情况下拨号的目的,做了N多测试,甚至到后面搞了一份原生插件的代码不过插件加载当时没搞懂就放弃了,不过到后面才发现原来后台拨号出现问题的原因不在这里,,具体原因看后面。
另外获取当前设备平台可以使用let platform = uni.getSystemInfoSync().platform;
,我这里只需要兼容固定机型。
1、uni-app API
uni.makePhoneCall({
phoneNumber: phone,
success: () => {
log(`成功拨打电话${phone}`);
},
fail: (err) => {
log(`拨打电话失败! ${err}`);
}
});
2、Android
plus.device.dial(phone, false);
3、Android 原生
写这个的时候有个小插曲,当时已经凌晨了,再加上我没有复制,是一个个单词敲的,结果竟然敲错了一个单词,测了好几遍都没有成功。。。还在想到底哪里错了,后来核对一遍才发现😭,control cv才是王道啊。
// Android
function PhoneCallAndroid(phone) {
if (!plus || !plus.android) return;
// 导入Activity、Intent类
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
// 获取主Activity对象的实例
var main = plus.android.runtimeMainActivity();
// 创建Intent
var uri = Uri.parse("tel:" + phone); // 这里可修改电话号码
var call = new Intent("android.intent.action.CALL", uri);
// 调用startActivity方法拨打电话
main.startActivity(call);
}
✅拨号状态查询
第一版用的就是这个获取状态的代码,有三种状态。第二版的时候又换了一种,因为要增加呼入、呼出、挂断、未接等状态的判断。
export function getCallStatus(callback) {
if (!plus || !plus.android) return;
let maintest = plus.android.runtimeMainActivity();
let Contexttest = plus.android.importClass("android.content.Context");
let telephonyManager = plus.android.importClass("android.telephony.TelephonyManager");
let telManager = plus.android.runtimeMainActivity().getSystemService(Contexttest.TELEPHONY_SERVICE);
let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (Contexttest, intent) => {
plus.android.importClass(intent);
let phoneStatus = telManager.getCallState();
callback && callback(phoneStatus);
//电话状态 0->空闲状态 1->振铃状态 2->通话存在
}
});
let IntentFilter = plus.android.importClass("android.content.IntentFilter");
let filter = new IntentFilter();
filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
maintest.registerReceiver(receiver, filter);
}
⚠️录音
录音功能这个其实没啥,都是官网的API,无非是简单的处理一些东西。但是这里有一个大坑!
一坑
就是像通话录音这种涉及到的隐私权限很高,正常的这种录音是在通话过程中是不被允许的。
二坑
后来一次偶然的想法,在接通之后再开启录音,发现就可以录音了。
但随之而来的是第二个坑,那就是虽然录音了,但是当播放的时候发现没有任何的声音,还是因为保护隐私的原因,我当时还脱离代码专门试了试手机自带的录音器来在通话时录音,发现也不行。由此也发现uni的录音本身也是用的手机录音器的功能。
三坑
虽然没有声音,但是我还是试了下保存,然后就发现了第三个坑,那就是虽然获取了文件权限,但是现在手机给的读写权限都是在限定内的,录音所在的文件夹是无权访问的。。。
另辟蹊径
其实除了自己手动录音外还可以通过手机自带的通话录音来实现,然后只要手机去读取录音文件并找到对应的那个就可以了。思路是没啥问题,不过因为设置通话录音指引、获取录音文件都有问题,这一版本就没实现。
// 录音
var log = console.log,
recorder = null,
// innerAudioContext = null,
isRecording = false;
export function startRecording(logFun = console.log) {
if (!uni.getRecorderManager || !uni.getRecorderManager()) return logFun('不支持录音!');
log = logFun;
recorder = uni.getRecorderManager();
// innerAudioContext = uni.createInnerAudioContext();
// innerAudioContext.autoplay = true;
recorder.onStart(() => {
isRecording = true;
log(`录音已开始 ${new Date()}`);
});
recorder.onError((err) => {
log(`录音出错:${err}`);
console.log("录音出错:", err);
});
recorder.onInterruptionBegin(() => {
log(`检测到录音被来电中断...`);
});
recorder.onPause(() => {
log(`检测到录音被来电中断后尝试启动录音..`);
recorder.start({
duration: 10 * 60 * 1000,
});
});
recorder.start({
duration: 10 * 60 * 1000,
});
}
export function stopRecording() {
if (!recorder) return
recorder.onStop((res) => {
isRecording = false;
log(`录音已停止! ${new Date()}`); // :${res.tempFilePath}
// 处理录制的音频文件(例如,保存或上传)
// powerCheckSaveRecord(res.tempFilePath);
saveRecording(res.tempFilePath);
});
recorder.stop();
}
export function saveRecording(filePath) {
// 使用uni.saveFile API保存录音文件
log('开始保存录音文件');
uni.saveFile({
tempFilePath: filePath,
success(res) {
// 保存成功后,res.savedFilePath 为保存后的文件路径
log(`录音保存成功:${res.savedFilePath}`);
// 可以将res.savedFilePath保存到你的数据中,或者执行其他保存相关的操作
},
fail(err) {
log(`录音保存失败! ${err}`);
console.error("录音保存失败:", err);
},
});
}
运行日志
为了更好的测试,也为了能实时的看到执行的过程,需要一个日志,我这里就直接渲染了一个倒序的数组,数组中的每一项就是各个函数push的字符串输出。简单处理。。。。嘛。
联调、测试、交工
搞到最后,大概就交了个这么玩意,不过也没有办法,一是自己确实不熟悉APP开发,二是满共就给了两天的工时,中间做了大量的测试代码的工作,时间确实有点紧了。所幸最起码的功能没啥问题,也算是交付了。
第二版
2024年05月7日,老哥又找上我了,想让我们把他们的这套东西再给友商部署一套,顺便把这个APP再改一改,增加上通时通次的统计功能。同时也是谈合作,如果后面有其他的友商想用这套系统,他来谈,我们来实施,达成一个长期合作关系。
我仔细想了想,觉得这是个机会,这块东西的市场需求也一直有,且自己现在失业在家也有时间,就想着把这个简单的功能打磨成一个像样的产品。也算是做一次尝试。
需求分析
- ✅拨号APP
- 登录
- uni-id实现
- 权限校验
- 拨号权限、文件权限、自带通话录音配置
- 权限引导
- 文件权限引导
- 通话录音配置引导
- 获取手机号权限配置引导
- 后台运行权限配置引导
- 当前兼容机型说明
- 拨号
- 获取手机号
- 是否双卡校验
- 直接读取手机卡槽中的手机号码
- 如果用户不会设置权限兼容直接input框输入
- 拨号
- 全局拨号状态监控注册、取消
- 支持呼入、呼出、通话中、来电未接或挂断、去电未接或挂断
- 获取手机号
- 录音
- 读取录音文件列表
- 支持全部或按时间查询
- 播放录音
- ❌上传录音文件到云端
- 读取录音文件列表
- 通时通次统计
- 云端数据根据上面状态监控获取并上传
- 云端另写一套页面
- 本地数据读取本机的通话日志并整理统计
- 支持按时间查询
- 支持呼入、呼出、总计的通话次数、通话时间、接通率、有效率等
- 云端数据根据上面状态监控获取并上传
- 其他
- 优化日志显示形式
- 封装了一个类似聊天框的组件,支持字符串、Html、插槽三种显示模式
- 在上个组件的基础上实现权限校验和权限引导
- 在上两个组件的基础上实现主页面逻辑功能
- 增加了拨号测试、远端连接测试
- 修改了APP名称和图标
- 打包时增加了自有证书
- 优化日志显示形式
- 登录
中间遇到并解决的一些问题
关于框架模板
这次重构我使用了uni中uni-starter + uni-admin 项目模板。整体倒还没啥,这俩配合还挺好的,就只是刚开始不知道还要配置东西一直没有启动起来。
建立完项目之后还要进uniCloud/cloudfunctions/common/uni-config-center/uni-id
配置一个JSON文件来约定用户系统的一些配置。
打包的时候也要在manifest.json
将部分APP模块配置进去。
还搞了挺久的,半天才查出来。。
类聊天组件实现
- 设计
- 每个对话为一个无状态组件
- 一个图标、一个名称、一个白底的展示区域、一个白色三角
- 内容区域通过类型判断如何渲染
- 根据前后两条数据时间差判断是否显示灰色时间
- 参数
- ID、名称、图标、时间、内容、内容类型等
- 样式
- 根据左边右边区分发送接收方,给与不同的类名
- flex布局实现
样式实现这里,我才知道原来APP和H5的展示效果是完全不同的,个别地方需要写两套样式。
关于后台运行
这个是除了录音最让我头疼的问题了,我想了很多实现方案,也查询了很多相关的知识,但依旧没效果。总体来说有以下几种思路。
- 通过寻找某个权限和引导(试图寻找到底是哪个权限控制的)
- 通过不停的访问位置信息
- 通过查找相应的插件、询问GPT、百度查询
- 通过程序切入后台之后,在屏幕上留个悬浮框(参考游戏脚本的做法)
- 通过切入后台后,发送消息实现(没测试)
测试了不知道多少遍,最终在一次无意中,终于发现了如何实现后台拨号,并且在之后看到后台情况下拨号状态异常,然后又查询了应用权限申请记录,也终于知道,归根到底能否后台运行还是权限的问题。
关于通话状态、通话记录中的类型
这个倒还好,就是测试的问题,知道了上面为啥异常的情况下,多做几次测试,就能知道对应的都是什么状态了。
通话状态:呼入振铃、通话中(呼入呼出)、通话挂断(呼入呼出)、来电未接或拒绝、去电未接或拒接。
通话日志:呼入、呼出、未接、语音邮件、拒接
交付
总体上来说还过得去,相比于上次简陋的东西,最起码有了一点APP的样子,基本上该有的功能也基本都已经实现了,美中不足的一点是下面的图标没有找到合适的替换,然后录音上传的功能暂未实现,不过这个也好实现了。
后面的计划
- 把图标改好
- 把录音文件是否已上传、录音上传功能做好
- 把APP的关于页面加上,对接方法、使用方法和视频、问题咨询等等
- 原本通话任务、通时通次这些是放在一个PHP后端的,对接较麻烦。要用云函数再实现一遍,然后对外暴露几个接口,这样任何一个系统都可以对接这个APP,而我也可以通过控制云空间的跨域配置来开放权限
- 把数据留在这边之后,就可以再把uni-admin上加几个页面,并且绑定到阿里云的云函数前端网页托管上去
- 如果有可能的话,上架应用商店,增加上一些广告或者换量联盟之类的东西
- 后台运行时,屏幕上加个悬浮图标,来电时能显示个振铃啥的
- 增加拨号前的校验,对接平台,对于经常拉黑电销的客户号码进行过滤
大致的想法就这些了,如果这个产品能继续卖下去,我就会不断的完善它。
最后
现在的行情真的是不好啊,不知道有没有大哥给个内推的机会,本人大专计算专业、6.5年Vue经验(专精后台管理、监控大屏方向,其他新方向愿意尝试),多个0-1-2项目经验,跨多个领域如人员管理、项目管理、产品设计、软件测试、数据爬虫、NodeJS、流程规范等等方面均有了解,工作稳定不经常跳,求路过的大哥给个内推机会把!
😂被举报标题党了,换个名字。
来源:juejin.cn/post/7368421971384860684
无构建和打包,浏览器直接吃上Vue全家桶?
Vue 是易学易用,性能出色,适用场景丰富的 Web 前端渐进式框架;常用来开发单页面应用。
主流开发方式-编译打包
用脚手架工具 create-vue 可以快速通过 npm create vue@latest
命令 来定制化新建一个 Vite 驱动的 Vue 单页面应用项目。
这是常规的使用 Vue 的方式。当然也可以从 Vite 那边入手。
我们新建一个项目 vue-demo
来试试,选上 Vue-Router 和 Pinia, 其余的不选:
访问 http://localhost:5173/
, 正常打开:
初始化的模板,用上了 Vue-Router,有两个路由, '/'
, '/about'
;那 Pinia 呢?可以看到依赖已经安装了引入了,给了一个 demo 了
我们来用一下 Pinia, 就在about路由组件里面用下吧:
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment } = store
</script>
<template>
<div class="about">
<h1>{{ count }}</h1>
<h1>{{ doubleCount }}</h1>
<button @click="increment">+1</button>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
这就是 Vue + Vue-Router + Pinia 全家桶在 打包构建工具 Vite 驱动下的开发方式。
Vite 开发阶段不打包,但会预构建项目的依赖,需要哪个资源会在请求的时候编译,而项目上线则需要打包。
完美对吧!但你有没有注意到,官网除了介绍这种方式,还介绍了 “Using Vue from CDN”:
也就是说,可以 HTML 文件里面直接用上 Vue 的对吧?那我还想要 Vue-Router、 Pinia、Axios、 Element-Plus 呢?怎么全部直接用,而不是通过npm install xxx 在需要构建打包的项目里面用?
如何直接吃上 Vue 全家桶
我们将会从一个 HTML 文件开始,用浏览器原生的 JavaScript modules 来引入 Vue 、引入 Vue-Router,、引入 Pinia、引入 Axios, 并且构建一个类似工程化的目录结构,但不需要打包,JS 是 ES modules 语法;而项目的运行,只需要用npx serve -s
在当前项目目录起一个静态文件服务器,然后浏览器打开即可。
HTML 文件引入 Vue
找个空文件夹,我们新建一个 index.html
:
把 Vue 文档代码复制过来:
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'vue'
createApp({
setup() {
const message = ref('Hello Vue!')
return {
message
}
}
}).mount('#app')
</script>
当前目录下执行下npx serve -s
打开看看
没问题。
但是经常写页面的朋友都知道,肯定得拆分组件,不然全写一个页面不好维护,这点官网也给了例子:
照猫画虎,我们拆分一下:
新建 src/app.js
文件,如下内容:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `<div @click="count++">Count is: {{ count }}</div>`
}
然后在 index.html
引入:
<script type="module">
import { createApp, ref } from 'vue'
import App from './src/app.js'
createApp(App).mount('#app')
</script>
刷新下页面看看:
Vue 成功引入并使用了。但还有遗憾,就是app.js
"组件"的 template 部分是字符串,没有高亮,不利于区分:
关于这点,官网也说了,如果你使用 VS Code, 那你可以安装插件 es6-string-html
,用 /*html*/实现高亮:
我们来试试看:
至此,我们可以相对舒服地使用 Vue 进行组件开发了。
HTML 文件引入、Vue 集成 Vue-Router
项目如果有不同的页面,就需要 Vue-Router 了, Vue-Router官网同样有网页直接引入的介绍:
我们来试一下,先在 Import Maps 添加 vue-router
的引入:
然后写个使用 Vue-Router 的demo: 新建两个路由组件:src/view/home.js
, src/view/about.js
, 在 HTML 文件中引入:
src/app.js
作为根组件,放个 RouterLink、RouterView 组件:
然后我们刷新下页面,看看是否正常生效:
很遗憾,没有生效,控制台报错了:
意思是声明的 vue-router 模块,没有导出我们引用到的方法 createRouter
;这说明,Vue-Router 打包的默认文件,并不是默认的 ES Modules 方式,我们得找找对应的构建产物文件才行;
这对比 Vue 的引入,Vue 引入的是构建产物中的 “esm-browser” 后缀的文件:
那么斗胆猜测下,Vue-Router 同样也有 esm 的构建产物,我们引入下该文件,应该就可以了。
但是怎么知道 Vue-Router 的构建产物有哪些?难道去翻官方的构建配置吗?不用,我们找个 npm 项目,然后npm install vue-router
,在 node_mudules/xxx
翻看就知道了。
我们上面正好有个 vue-demo, 使用了 Vue-Router。我们看看:
我们改下 Import Maps 里面 vue-router
的映射:
刷新下页面看看:
还是有报错:
@vue/devtools-api
我们并没有引入,报了这个错,斗胆猜测是 vue-router 中使用的,该模块应该是属于外部模块,我们看看网络里面响应的文件验证下:
确实如此,那么 Import Maps 也补充下引入这个模块,我们先翻看该模块的 npm 包看看,确定下路径:
Import Maps 里面引入:
再刷新下页面试试:
至此,我们成功地在 HTML 文件中引入,在 Vue 中集成了 Vue-Router。
下面我们来看 Pinia 的
但在这之前,我们来整理下现在的目录划分吧。
新建 src/router/index.js
文件,将路由相关的逻辑放到这里:
在index.html
引入 router:
然后type=module
的 script 里面的内容也可以抽离出来到单独的文件里面:
新建 main.js
文件,将内容搬过去并引入:
页面刷新下,正常运行。
HTML 文件引入、Vue 集成 Pinia
有了上面引入 Vue-Router 的经验,我们就知道了,引入其他的库也是相同的套路。我们去之前的脚手架工具生成的项目 vue-demo 的依赖里面翻看一下,Pinia 包的构建产物是如何的,然后在现在的 esm 项目里面引入吧:
我们在项目里面使用一下 Pinia, 在main.js
里面引入 Pinia:
import { createApp, ref } from 'vue'
import App from './src/app.js'
import router from './src/router/index.js'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.use(router)
.mount('#app')
新建 src/stores/useCounterStore.js
文件,填入如下内容:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export default defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
return { count, doubleCount, increment }
})
即如下:
之后我们在 src/view/home.js
组件里面使用一下这个 store:
import useCounterStore from "../stores/useCounterStore.js"
import { storeToRefs } from 'pinia'
export default {
setup() {
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment } = store
return { count, doubleCount, increment }
},
template: /*html*/`<div>
<h1>Home</h1>
<p>{{ count }}</p>
<p>{{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>`
}
我们刷新页面看看,报错了, 缺了一个模块 vue-demi
我们确认一下,在响应的 Pinia 库中确实有对这模块的引入
那么我们也引入一下吧,我们翻看需要的库的文件路径,注意这里的 esm 模块是 .mjs 后缀文件
再刷新看看:
至此,我们就在 HTML 文件中直接引入 Vue, 集成了 Vue-Router、Pinia。
HTML 文件引入 Axios
接下来,我们来看看网络请求库 Axios。
网络请求, 原生的 fetch API 可以胜任,但是对于项目的网络请求,最好有统一的拦截器处理,而 Axios 已经有了一套可行的方案,所以我项目开发一般会用 Axios。本节不讲Axios封装,只介绍在原生 HTML 文件中直接引入和使用 Axios。
要以 ESM 方式引入 Axios,我们得知道 Axios esm 模块的路径。我们在上述的工程化项目 vue-demo 中安装和查看路径
我们在 Import Maps 添加引入
我们添加 src/mock/test.json
文件,里面存放JSON 数据,然后用 axios 请求试试看:
我们在 src/view/about.js
组件里面使用一下 Axios 来获取 mock 数据,并且显示到页面上,代码如下:
import axios from 'axios'
import { ref } from 'vue'
export default {
setup() {
const mockData = ref(null)
axios.get('/src/mock/test.json').then(res => {
mockData.value = res.data
})
return { mockData }
},
template: /*html*/`<div>
<h1>About</h1>
<pre>
{{ mockData }}
</pre>
</div>`
}
刷新看看:
没有问题,可以正常使用,至于 Axios 如何封装得适合项目,这里就不展开了。
CSS 样式解决方案
但目前为止,我们几乎没有写样式,但这种纯 ESM 项目,我们应该怎么写样式呢?
用打包构建工具的项目,一般都有 CSS 的预构建处理工具,比如 Less, Scss等;但实际开发中,大部分就使用一下嵌套而已;
现在最新的浏览器已经支持 CSS 嵌套了:
还有 CSS 模块化的兼容性也完全没问题:
那么此 ESM 项目我这里给一个建议的方案,读者欢迎评论区留言提供其他方案。
新建 src/style/index.css
文件,键入如下样式:
body {
background-color: aqua;
}
在 index.html
文件中引入该样式:
刷新看看是否生效
项目中该怎么进行组件的 CSS 样式隔离呢?这里就建议 采用 ESM 的类名样式方案咯,这里不展开讲,只给一个样式目录参考。建议如下:
将样式放在 src/style
下面,按照组件的目录进行放置,然后在src/style/index.css
引入:
效果如下:
样式中,我使用了CSS模块化语法和嵌套语法,都生效了。
HTML 文件引入、Vue 集成 Element-Plus
最后,我们再引入组件库吧。我这里使用 Element-Plus
官网可以看到也是支持直接引入的,要注意的是得引入其样式
我们在上面工程化项目 vue-demo 里面安装下 Element-Plus 的 npm 包看看 esm 文件的位置(.mjs后缀文件一般就是esm模块):
在 index.html
文件里面引入样式,在 Import Maps 里面引入 element-plus:
然后在 main.js
里把所有 element-plus 组件注册为全局组件并在 src/view/home.js
使用下 Button 组件:
效果如下:
至此,我们在项目中集成了 Element-Plus 组件库了。
其他优化
以上所有的库,都可以在网络的响应里面,复制到本地,作为本地文件引入,这样加载速度更快,没有网络延迟问题。
总结
我们先按照 Vue 官方文档使用了常规的项目开发方式创建了一个项目。
然后我们提出了一个想法:能否直接在 HTML
文件中使用 Vue 及其全家桶?
答案是可行的,因为几乎所有的库都提供了 ESM 的构建文件,而现今的浏览器也都支持 ESM 模块化了。
我们也探讨和实践了 CSS 模块化 和 CSS 嵌套,用在了 demo 中作为 esm 项目的样式方案。
最后我们在项目中集成了 Element-Plus 组件库。
至此,我们可以点题了:无打包构建,浏览器确实能吃上 Vue 全家桶了。但这并不是说,可以在真实项目中这样使用,兼容性就不说了,还有项目的优化,一般得打包构建中做:比如 Tree Shaking、代码压缩等。但如果是一些小玩具项目,可以试试这么玩。无构建和打包,浏览器跑的代码就是你写的源码了。
本文示例代码地址:gitee.com/GumplinGo/1…
来源:juejin.cn/post/7399094428343959552
蔚来面试题:计算白屏时间
深入理解白屏时间及其优化策略
在前端性能优化中,白屏时间(First Paint Time)是一个非常重要的指标。它指的是从用户输入网址并按下回车键,到浏览器开始渲染页面内容的时间段。在这段时间内,用户看到的只是一个空白页面,因此白屏时间的长短直接影响了用户的体验。本文将详细探讨白屏时间的定义、影响因素、测量方法以及优化策略,并结合代码示例进行说明。
什么是白屏时间?
白屏时间是指从用户发起页面请求到浏览器首次开始渲染页面内容的时间。具体来说,白屏时间包括以下几个阶段:
- DNS解析:浏览器将域名解析为IP地址。
- 建立TCP连接:浏览器与服务器建立TCP连接(三次握手)。
- 发起HTTP请求:浏览器向服务器发送HTTP请求。
- 服务器响应:服务器处理请求并返回响应数据。
- 浏览器解析HTML:浏览器解析HTML文档并构建DOM树。
- 浏览器渲染页面:浏览器根据DOM树和CSSOM树生成渲染树,并开始渲染页面。
- 页面展示第一个标签:浏览器首次将页面内容渲染到屏幕上。
白屏时间的长短直接影响了用户对网站的第一印象。如果白屏时间过长,用户可能会感到不耐烦,甚至直接关闭页面。因此,优化白屏时间是前端性能优化的重要目标之一。
白屏时间的影响因素
白屏时间的长短受到多种因素的影响,主要包括以下几个方面:
- 网络性能:网络延迟、带宽、DNS解析时间等都会影响白屏时间。如果网络状况不佳,DNS解析和TCP连接建立的时间会变长,从而导致白屏时间增加。
- 服务器性能:服务器的响应速度、处理能力等也会影响白屏时间。如果服务器响应缓慢,浏览器需要等待更长的时间才能接收到HTML文档。
- 前端页面结构:HTML文档的大小、复杂度、外部资源的加载顺序等都会影响白屏时间。如果HTML文档过大或包含大量外部资源,浏览器需要更长的时间来解析和渲染页面。
- 浏览器性能:浏览器的渲染引擎性能、缓存机制等也会影响白屏时间。不同浏览器的渲染性能可能存在差异,导致白屏时间不同。
如何测量白屏时间?
测量白屏时间的方法有多种,下面介绍两种常用的方法:基于时间戳的方法和基于Performance API的方法。
方法一:基于时间戳的方法
在HTML文档的<head>
标签中插入JavaScript代码,记录页面开始加载的时间戳。然后在<head>
标签解析完成后,记录另一个时间戳。两者的差值即为白屏时间。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>白屏时间计算</title>
<script>
// 记录页面开始加载的时间
window.pageStartTime = Date.now();
</script>
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/ionicons/2.0.1/css/ionicons.min.css~tplv-t2oaga2asx-image.image">
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/asset/fw-icon/1.0.9/iconfont.css~tplv-t2oaga2asx-image.image">
<script>
// head 解析完成后,记录时间
window.firstPaint = Date.now();
console.log(`白屏时间:${firstPaint - pageStartTime}ms`);
</script>
</head>
<body>
<div class="container"></div>
</body>
</html>
方法二:基于Performance API的方法
使用Performance API可以更精确地测量白屏时间。Performance API提供了PerformanceObserver
接口,可以监听页面的首次绘制(first-paint
)事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/ionicons/2.0.1/css/ionicons.min.css~tplv-t2oaga2asx-image.image">
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/asset/fw-icon/1.0.9/iconfont.css~tplv-t2oaga2asx-image.image">
<!-- 只是为了让白屏时间更长一点 -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
</head>
<body>
<h1>Hello, World!</h1>
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_61ae954a0c4c41dba37b189a20423722@000000_oswg66502oswg900oswg600_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_9e1df42e783841e79ff021cda5fc6ed4@000000_oswg41322oswg1026oswg435_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_0376475b9a6a4dcab3f7b06a1b339cfc@5888275_oswg287301oswg729oswg545_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_e3213623ab5c46da8a6f9c339e1bd781@5888275_oswg1251766oswg1080oswg810_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_919d4445116f4efda326f651619b4c69@5888275_oswg169476oswg598oswg622_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_0457ccbedb984e2897c6d94815954aae@5888275_oswg383406oswg544oswg648_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<script>
// 性能 观察器 观察者模式
const observer = new PerformanceObserver((list) => {
// 获取所有的 性能 指标
const entries = list.getEntries();
for(const entry of entries) {
// body 里的第一个 标签的渲染
// 'first-paint' 表示页面首次开始绘制的时间点,也就是白屏结束的时间点
if(entry.name === 'first-paint') {
const whiteScreenTime = entry.startTime;
console.log(`白屏时间:${whiteScreenTime}ms`);
}
}
})
// 首次绘制 first-paint
// 首次内容绘制 first-contentful-paint 事件
// observe 监听性能指标
// buffered 属性设置为 true,表示包含性能时间线缓冲区中已经记录的相关事件
// 这样即使在创建 PerformanceObserver 之前事件已经发生,也能被捕获到
observer.observe({ type: 'paint', buffered: true });
</script>
</body>
</html>
总结
白屏时间是前端性能优化中的一个重要指标,直接影响用户的体验。通过理解白屏时间的定义、影响因素以及测量方法,开发者可以有针对性地进行优化。
来源:juejin.cn/post/7475652009103032358
不得不安利的富文本编辑器,太赞了!
hello,大家好,我是徐小夕。之前和大家分享了很多可视化,零代码和前端工程化的最佳实践,最近也在迭代可视化文档知识引擎Nocode/WEP
。在研究文档编辑器的时候也发现了很多优秀的开源项目,从中吸取了很多先进的设计思想。
接下来就和大家分享一款由facebook开源的强大的富文本编辑器——Lexical。目前在github
上已有 17.7k star
。
github地址: https://github.com/facebook/lexical
往期精彩
Lexical 基本介绍
Lexical 是一个可扩展的 JavaScript 文本编辑器框架,聚焦于可靠性、可访问性和性能。旨在提供一流的开发人员体验,因此我们可以轻松地进行文档设计和构建功能。
结合高度可扩展的架构,Lexical 允许开发人员创建独特的文本编辑体验,功能可以二次扩展,比如支持多人协作,定制文本插件等。
demo演示
我们可以使用它实现类似 Nocode/WEP
文档引擎的编辑体验。
我们可以轻松的选中文本来设置文本样式:
同时还能对文本内容进行评论:
当然插入表格和代码等区块也是支持的:
接下来就和大家一起分享以下它的设计思路。
设计思想
Lexical 的核心是一个无依赖的文本编辑器框架,允许开发人员构建强大、简单和复杂的编辑器表面。Lexical 有几个值得探索的概念:
- 编辑器实例:编辑器实例是将所有内容连接在一起的核心。我们可以将一个 contentEditable DOM 元素附加到编辑器实例,并注册侦听器和命令。最重要的是,编辑器允许更新其 EditorState。我们可以使用 createEditor() API 创建编辑器实例,但是在使用框架绑定(如@lexical/react)时,通常不必担心,因为这会为我们自动处理。
- 编辑器状态:编辑器状态是表示要在 DOM 上显示的内容的底层数据模型。编辑器状态包含两部分:
- Lexical 节点树
- Lexical 选择对象
- 编辑器状态一旦创建就是不可变的,为了更新它,我们必须通过 editor.update(() => {...}) 来完成。但是,也可以使用节点变换或命令处理程序“挂钩”到现有更新中 - 这些处理程序作为现有更新工作流程的一部分被调用,以防止更新的级联/瀑布。我们还可以使用 editor.getEditorState() 检索当前编辑器状态。
- 编辑器状态也完全可序列化为 JSON,并可以使用 editor.parseEditorState() 轻松地将其序列化为编辑器。
- 读取和更新编辑器状态:当想要读取和/或更新 Lexical 节点树时,我们必须通过 editor.update(() => {...}) 来完成。也可以通过 editor.getEditorState().read(() => {...}) 对编辑器状态进行只读操作。
Lexical的设计模型如下:
这里为了大家更直观的了解它的使用,我分享一个相对完整的代码案例:
import {$getRoot, $getSelection, $createParagraphNode, $createTextNode, createEditor} from 'lexical';
// 第一步,创建编辑器实例
const config = {
namespace: 'MyEditor',
theme: {
...
},
onError: console.error
};
const editor = createEditor(config);
// 第二步,更新编辑器内容
editor.update(() => {
const root = $getRoot();
const selection = $getSelection();
// 创建段落节点
const paragraphNode = $createParagraphNode();
// 创建文本节点
const textNode = $createTextNode('Hello world');
// 添加文本节点到段落
paragraphNode.append(textNode);
// 插入元素
root.append(paragraphNode);
});
通过以上两步,我们就实现了文本编辑器的创建和更新,是不是非常简单?
如果大家对这款编辑器感兴趣,也欢迎在github上学习使用,也欢迎在留言区和我交流反馈。
github地址: https://github.com/facebook/lexical
最后
后续我还会持续迭代 Nocode/WEP
项目, 让它成为最好用的可视化 + AI知识库,同时也会持续迭代和分享H5-Dooring零代码搭建平台, 如果你也感兴趣,欢迎随时交流反馈。
往期精彩
来源:juejin.cn/post/7377662459921006629
为什么面试官在面试中都爱问 HTTPS ❓❓❓
尽管 HTTP
在我们的项目中应用已经很广泛了,然而 HTTP
并非只有好的一面,事物皆具有两面性,它也是有不足之处的。
HTTP 的不足之处主要有以下几个方面:
- 数据传输不加密:HTTP 传输的数据是明文的,任何人都可以在网络中监听并读取传输的数据。这意味着,如果通过 HTTP 传输的是敏感信息(如用户名、密码、银彳亍卡号等),就会容易被窃取。这就会导致数据泄露,影响用户隐私和安全。
- 数据容易被篡改:HTTP 不提供数据完整性保护,数据在传输过程中可以被中途篡改。恶意攻击者可以通过中间人攻击(Man-in-the-Middle, MITM)修改数据,导致用户接收到被篡改的内容,如篡改的文件、消息等。
- 缺乏身份验证:HTTP 协议本身无法验证客户端访问的是合法的服务器,可能会遭遇伪造网站或钓鱼网站。攻击者可以通过创建假网站诱导用户输入个人信息或执行恶意操作,造成信息泄露或财产损失。
- 容易遭受中间人攻击(MITM):由于 HTTP 协议的数据是明文传输的,攻击者能够通过中间人攻击拦截、读取、修改传输数据。攻击者可以截获会话内容,窃取敏感信息,甚至伪造响应返回给客户端,造成严重的安全隐患。如下图所示:
- 缺乏数据完整性保护:HTTP 协议本身没有内建的校验机制来验证数据是否在传输过程中被篡改。恶意攻击者可以修改数据,客户端无法判断是否收到被篡改的内容。
- 浏览器安全警告:许多现代浏览器已经将 HTTP 网站标记为“不安全”,并警告用户。HTTP 网站会影响用户信任,特别是在涉及电子商务、登录、支付等敏感操作时,用户会更加倾向于避免访问 HTTP 网站。
- 不支持 HTTP/2 特性:HTTP 协议(特别是 HTTP/1.x 版本)效率较低,无法充分利用现代网络的性能优势。比如,它存在队头阻塞(Head-of-Line Blocking)问题,多个请求必须按顺序处理。在大流量的网站或复杂的请求/响应场景下,HTTP 的性能较差,响应速度较慢。
- 搜索引擎优化(SEO)劣势:搜索引擎(如 Google)更倾向于优先排名 HTTPS 网站,HTTP 网站的排名可能会受到影响。如果一个网站仅使用 HTTP 协议,其搜索引擎排名可能会比使用 HTTPS 的网站低,从而减少网站的访问量。
什么是 HTTPS
为了解决上述存在的问题,就用到了 HTTPS
,实际上它也并发是应用层的一种新协议,只是 HTTP
通信接口部分用 SSL
和 TLS
协议代替而已。
在正常情况下,HTTP
直接和 TCP
通信,当使用 SSL
时,则演变成先和 SSL
通信,再由 SSL
和 TCP
通信了,换句话说,所谓的 HTTPS
实际上就是身披 SSL
协议这层外壳的 HTTP
。
在采用 SSL
后,HTTP
就拥有了 HTTPS
的加密、证书和完整性保护这些功能。
相互交换秘钥的公开密钥加密技术
在对 SSL 进行讲解之前,我们先来了解一下加密方法。SSL 采用一种叫做公开密钥加密的加密处理方式。
在近代的加密方法中,加密算法是公开的,而密钥是保密的,通过这种方式得以保持加密方法的安全性。加密和揭秘都会用到密钥,没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了。
对称密钥加密(共享密钥加密)
加密和揭秘同用一个密钥的方式称为共享密钥加密,也被叫做对称密钥加密:
以共享密钥方式加密时必须将密钥也发给对方,这是一个挑战,因为在传输密钥本身也需要保证其安全性。如果密钥在传输过程中被截获或篡改,通信的机密性将会被威胁。
在使用共享密钥的通信中,通信双方必须共享同一个密钥,并且双方都必须信任这个密钥的安全性。如果这个密钥在任何一方处被泄露或公开,通信的机密性将无法得到保证。因此,确保双方对共享密钥的安全性保持信任是至关重要的。
我们先来看一个对称加密的例子,假设用户 A 想给用户 B 发送一条加密信息:
- 用户 A 和用户 B 事先共享一个密钥
K
。 - 用户 A 使用密钥
K
对消息M
进行加密,生成密文C
:C = E(M, K)
,其中E
是加密算法。 - 用户 A 将密文
C
发送给用户 B。 - 用户 B 收到密文后,使用相同的密钥
K
解密,恢复原始消息M
:M = D(C, K)
,其中D
是解密算法。
对称密钥加密的缺点非常明显
- 双方需要事先共享密钥,密钥传输过程容易被截获。如果密钥泄露,通信安全将受到严重威胁。
- 不适合大规模使用:在多方通信中,每对通信方都需要一个独立的密钥。密钥数量增长迅速,难以管理。例如,若有 1000 个用户,每两人之间需要一个密钥,总共需要约 50 万个密钥。
- 无法实现身份验证:对称加密本身无法验证通信方的身份,容易受到中间人攻击。对称加密本身无法验证通信方的身份,容易受到中间人攻击。
非对称密钥加密(公开密钥加密)
公开密钥加密方式很好地解决了共享密钥加密的困难。它使用一对非对称的密钥,一把叫作私有密钥,另外一把叫作公开密钥。私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。
使用方式: 发送密文的一方使用 对方的公钥
对信息进行加密,对方接收到被加密的信息后再使用自己的私钥进行解密。
特点: 信息传输一对多,服务器只需要维持好一个私钥就能和多个客户端进行加密通信。可以实现安全的身份验证、数字签名和密钥交换等功能。
优点:
- 安全性高: 私钥不会被公开传输,只有私钥的持有者才能解密加密的信息;
- 方便的密钥交换: 发送方和接收方只需交换公钥,而无需交换密钥;
- 可以实现数字签名: 私钥持有者可以使用时要对消息进行签名,接收方可以使用公钥验证签名的有效性;
缺点:
- 计算复杂度高: 与对称密钥加密相比,非对称密钥加密的计算速度慢,处理大量数据时可能会更耗时;
- 密钥管理复杂: 由于涉及到公钥和私钥的生成、发布和保护,密钥管理可能会更复杂;
- 通信效率较低:由于加密和解密操作需要使用较长的密钥,导致加密数据的大小增加,从而降低了通信效率;
虽然说安全性高,但也不是没有被盗的可能,因为公钥是公开的,谁都可以获取,如果发送的加密信息是通过私钥加密的话,有公钥的黑客就可以用这个公钥来解密拿到里面的信息。
下面有一个例子,假设用户 A 想发送一条安全消息给用户 B:
- 用户 A 获取用户 B 的
公钥
。 - 用户 A 使用 B 的公钥对消息 其中, 是用户 B 的公钥。 进行加密,生成密文 :
- 用户 A 将密文 发送给用户 B。
- 用户 B 收到密文后,使用自己的
私钥
解密,恢复原始消息 :其中, 是用户 B 的私钥。
非对称加密是一种安全性极高的加密技术,适用于身份验证、密钥交换和数字签名等场景。尽管速度较慢、不适合大数据加密,但它通过与对称加密结合,可以在现代网络通信中高效地提供安全保障。
为什么非对称加密效率低一点
非对称加密的效率较低主要是由于其算法的复杂性和计算成本较高的特点。以下是一些导致非对称加密效率低的主要原因:
- 密钥长度较长: 非对称加密需要使用一对密钥,包括公钥和私钥。通常情况下,这些密钥的长度要比对称加密中使用的密钥长得多。较长的密钥长度会导致加密和解密的操作都需要更多的计算时间。
- 计算复杂性: 非对称加密算法(如 RSA 和 Elliptic Curve Cryptography)涉及到大整数运算、模幂运算等复杂的数学运算。这些运算需要更多的计算资源和时间,因此非对称加密的处理速度较慢。
- 加密速度较慢: 由于非对称加密的加密和解密操作都使用不同的密钥,因此加密和解密速度都较慢。这使得非对称加密不适合处理大量数据,特别是实时通信和大规模数据传输方面。
- 密钥管理复杂性: 非对称加密需要管理和保护两个密钥:公钥和私钥。这增加了密钥管理的复杂性,包括生成、存储和分发密钥等方面的挑战。
- 安全性优先: 非对称加密的设计目标之一是提供更高的安全性,因此牺牲了一些性能。密钥的长长度和复杂的数学运算增加了攻击者破解加密的难度,但同时也降低了效率。
非对称加密效率较低主要源于其复杂的数学运算、较长的密钥长度和双密钥管理需求。这些特性决定了非对称加密在性能上无法与对称加密相比,但它通过提供更高的安全性和灵活性,成为密钥交换、身份验证和数字签名等场景的关键技术。通过混合加密和硬件优化,非对称加密的性能瓶颈可以得到有效缓解,从而实现安全与效率的平衡。
混合加密机制
HTTPS
采用共享密钥加密和公开密钥加密两者并用的混合加密机制。它采用了对称密钥加密算法的高效性和非对称密钥加密算法的安全性,可以保证安全性的同时提高加密和揭秘的效率。
混合加密机制的操作步骤主要一下几个方面:
- 密钥交换: 接收方生成一对非对称密钥 (
公钥
和私钥
),并将公钥发送给发送方; - 对称密钥生成: 发送方生成一个随机的对称密钥,用于对消息进行加密;
- 对称密钥加密: 发送方使用接收方的公钥将对称密钥加密,并将加密后的对称密钥发送给接收方;
- 消息处理: 发送方使用对称密钥对要发送的消息进行加密,并将加密后的消息发送给接收方;
- 密文传输: 接收方收到加密后的对称密钥和消息;
- 对称密钥加密: 接收方使用自己的私钥解密接收到的对称密钥;
- 消息解密: 接收方使用解密后的对称密钥对接收到的消息进行解密,获得原文明文消息;
在 HTTPS 中,非对称密钥用于安全地交换对称密钥,确保通信双方能在不暴露私密信息的情况下共享加密密钥。之后,对称密钥用于加密和解密实际的数据传输,因为对称加密处理数据速度更快。两者结合确保了数据传输的安全性和效率。
使用文字的方式来表达难免会有些难以理解,接下来我们使用一个流程图来看看混合加密机制的步骤是怎样实现的:
虽然混合加密机制结合了对称加密和非对称加密两者的优势,能够实现双方之间安全的传输。但也不是没有缺点,它的缺点主要有以下几个方面:
- 数据不完整性: 混合加密主要是为了解决
HTTP
中内容可能被窃听的问题。但是它并不能保证数据的完整性,也就是说在传输的时候数据是有可能被第三方篡改的,比如完全替换掉,所以说它并不能校验数据的完整性; - 复杂性: 混合加密涉及多种加密算法和密钥管理过程,因此实现和管理起来相对复杂;
- 密钥交换: 混合加密需要在通信双方之间进行密钥交换,以便建立安全的通信信道,如果密钥交换过程不正确或者被攻击者窃取,那么整个加密系统的安全性将会受到威胁;
- 性能开销: 混合加密需要同时使用非对称加密和对称加密算法,非对称加密算啊的加密和解密速度较慢,而对称加密算法的加密和解密速度较快。因此,在大规模数据传输时,可能会引入性能开销;
- 中间人攻击: 混合加密并不能防止中间人攻击,如果攻击者能够劫持或篡改通信信道,并替换公钥或插入恶意代码,那么它们仍然可以窃听、修改或伪装通信内容;
假设用户 A 需要向用户 B 发送加密消息,以下是混合加密的详细过程:
- 用户 A 生成会话密钥:用户 A 生成一个随机的会话密钥 。例如, 是一个 256 位的对称加密密钥。
- 用户 A 加密数据**:使用对称加密(如 AES),用户 A 使用
- 用户 A 加密会话密钥:使用非对称加密(如 RSA),用户 A 用用户 B 的公钥
- 用户 A 发送数据:用户 A 将加密的会话密钥 和加密的数据 一起发送给用户 B。
- 用户 B 解密会话密钥:用户 B 使用自己的私钥
- 用户 B 解密数据:用户 B 使用会话密钥
假设用户 B 收到用户 A 通过混合加密机制发送的密文,用户 B 如何通过解密获取明文?以下是完整的解密过程:
- 解密会话密钥
用户 B 收到加密的会话密钥
和加密的数据密文 。用户 B 使用自己的私钥 对加密的会话密钥 进行解密,恢复出会话密钥 :
解密后,
是对称加密所需的密钥。- 解密数据密文
用户 B 使用解密得到的会话密钥
对数据密文 进行对称解密:解密后,
是用户 A 发送的原始明文数据。混合加密机制结合了对称加密和非对称加密的优点,既保证了数据传输的安全性,又提高了加密处理的效率。这种机制在现代网络通信和数据加密中广泛使用,特别是在 HTTPS 协议、云存储、电子邮件加密和区块链等场景中,成为实现高效安全通信的关键技术。
保证公开密钥正确性的数字证书
目前来看,混合加密机制已经很安全了,但也不是完全没有问题。那就是无法证明公开密钥本身就是货真价实的公开密钥。它有可能在公开密钥传输途中,真正的公开密钥已经被攻击者替换掉了。
为了解决这个问题,通过数字证书认证机构和其他相关机关颁发的公开密钥证书。其中数字证书的基本组成部分主要有以下几个主体:
- 公钥:证书中包含了公钥,即需要验证的公开密钥;
- 签名:证书颁发机构使用自己的私钥对证书的内容进行数字签名,以验证证书的完整性和真实性;
- 有效期:证书包含了开始和结束的有效期,指定了证书的有效期限;
- 颁发机构信息:证书中包含了颁发机构的身份信息,用于验证颁发机构的可信性;
证书的主体部分包含了公钥持有者的身份信息,如名称、电子邮件地址等。
服务器会将这份由数字证书认证机构办法的公钥证书发送给客户端,以进行公开密钥加密方式通信。接到证书的客户端可使用数字证书认证机构的公开密钥,对那张证书上的数字签字进行验证,一旦验证通过,客户端便可以明确两件事:
- 认证服务器的公开密钥的真实有效的数字证书认证机构;
- 服务器的公开密钥是值得信赖的;
数字签名是什么呢,它是一种用于验证数据完整性和身份认证的技术,它的产生过程主要有以下几个步骤:
- 生成密钥对: 数字签名使用非对称密钥加密算法,首先需要生成密钥对。密钥对包括一个私钥和一个公钥。私钥用于生成签名,而公钥用于验证签名;
- 签名生成: 使用私钥对数据进行签名,签名生成的过程通常是先对数据进行哈希运算,然后使用私钥对哈希值进行加密,生成签名;
- 签名附加:将生成的签名与原始数据一起发送或存储;
- 验证签名:接收方或验证者收到签名和原始数据后,可以执行以下步骤验证签名的有效性
- 提取公钥: 从签名的来源获取签名者的公钥;
- 解密签名: 使用签名者的公钥对签名进行解密,得到解密后的哈希值;
- 哈希计算:对原始数据进行哈希运算,得到哈希值;
- 比较哈希值:将解密后的哈希值与计算得到的哈希值进行比较。如果两者匹配,说明签名是有效的。如果不匹配,说明签名无效;
通过这个过程,验证者可以确保数据在传输过程中没有被篡改,并且可以确定签名的来源。
数字证书的颁发流程
有了数字签名校验数据的完整性,但是数字签名校验的前提是能拿到发送方的公钥,并且保证这个公钥是可信赖的,所以就需要数字证书。
数字证书的颁发流程通常涉及以下步骤:
- 密钥生成:
- 实体(
个人
、组织
或服务器
)生成一个密钥对,包括一个公钥和一个私钥; - 私钥用于加密和签名,公钥用于解密和验证;
- 实体(
- 证书请求:
- 实体向证书办法机构(
Certificate Authority
,CA
)提交证书请求; - 证书请求中包含实体的公钥以及一些身份信息,例如名称、电子邮件地址等;
- 实体向证书办法机构(
- 身份验证:
CA
对实体的身份进行验证,验证的方式包括人工审核、文件验证、域名验证等;CA
确保证书请求的提交者拥有对应的私钥,并具备合法身份;
- 证书生成:
- 经过身份验证后,
CA
使用自己的私钥对证书进行签名,生成数字证书; - 数字证书中包含实体的公钥,身份信息以及
CA
的签名;
- 经过身份验证后,
- 证书颁发:
CA
将生成的数字证书颁发给实体,通常以电子文件的形式提供;- 实体接收到数字证书后,可以将其用于加密通信、数字签名等安全操作;
- 证书验证:
- 其他参与者在与实体进行通信时,可以获取实体的数字证书;
- 参与者使用证书颁发机构的公钥验证证书的签名,确保证书的完整性和真实性;
为什么说数字证书就能对通信方的身份进行验证呢?
数字证书能够对通信方身份进行验证,是因为数字证书采用了公钥加密和数字签名的技术,结合了非对称密钥加密算法的特性。
在数字证书中,证书颁发机构使用自己的私钥对证书进行签名,这个数字签名可以被其他参与这使用 CA
的公钥进行验证,通过验证数字签名,可以确保证书的完整性和真实性。
以下几个步骤是数字证书验证通信方身份的过程:
- 获取证书: 通信方在通信开始之前,从对方获取数字证书;;
- 提取公钥: 通信方从数字证书中提取对方的公钥;
- 验证签名: 通信方使用证书颁发机构的公钥对证书中签名进行解密,得到签名的哈希值;
- 哈希计算: 通信方对原始证书内容进行哈希计算,生成一个哈希值;
- 比较哈希值: 通信方将解密得到的哈希值与自己计算的哈希值进行比较,如果两者相同,则证书的签名是有效的,证明证书没有被篡改;
通过以上验证步骤,通信方可以确保证书的完整性,并且确定证书的来源是可信的。这样通信方可以信任证书中关联的公钥,并使用公钥进行加密、身份认证或数字签名的验证。
总的来说,数字证书通过使用证书颁发机构的私钥对证书进行签名,提供了一种可信任的方式来验证证书的完整性和真实性。通过验证证书,通信方可以建立对对方身份的信任,并使用其公钥进行安全的通信操作。
SSL/TLS 是如何工作的
HTTPS 是 HTTP 协议的一种安全形式。它围绕 HTTP、传输层安全性 (TLS) 包装了一个加密层。
HTTP 只是一种协议,但当与 TLS 配对时,它会被加密。
TLS 和 SSL 是面向 Socket 的协议,因此加密发送方和接收方之间的套接字或传输通道,但不加密数据。这是使这两个协议独立于应用层的主要原因。
接下来我们来看看 TLS 是如何工作的。先上图:
我们将对图中的每一个步骤做详细的解释:
- 握手启动 (Initiation of TLS Handshake):浏览器(客户端)发起 TLS 握手请求,与服务器建立安全通信。
- 客户端问候 (Client Hello):客户端发送 ClientHello 消息,包含以下内容:
- 支持的 TLS 协议版本(如 TLS 1.2、TLS 1.3)。
- 支持的加密算法(如 RSA、ECDHE、AES)。
- 随机数(用于密钥协商)。
- 会话 ID(如果是恢复连接时用)。
- 服务器问候 (Server Hello):服务器响应 ServerHello 消息,内容包括:
- 确认使用的 TLS 协议版本。
- 选择的加密算法。
- 服务器生成的随机数。
- 会话 ID。
- 服务器证书(Server Certificate):服务器发送其 SSL/TLS 证书(由 CA 签发),包含:
- 服务器的公开密钥。
- 服务器的身份信息(如域名)。
- 证书的有效期。
- 服务器密钥交换 (Server Key Exchange,可选):在某些情况下(如使用 Diffie-Hellman 密钥交换算法),服务器会发送密钥交换参数。这一步是可选的,具体取决于协商的加密算法。
- 服务器握手结束通知 (Server Handshake Finished):服务器发送 ServerHelloDone,表示服务器端的握手阶段完成。
- 客户端密钥交换 (Client Key Exchange):客户端生成一个 预主密钥(Pre-Master Secret),并使用服务器的公钥加密后发送给服务器。服务器用私钥解密,得到预主密钥。
- 生成主密钥(Pre-Master to Master Secret):客户端和服务器各自使用预主密钥、客户端随机数、服务器随机数,以及协商的加密算法,生成主密钥。
- 通知切换到加密模式 (Change Cipher Spec):客户端和服务器分别发送 ChangeCipherSpec 消息,表明后续通信将使用加密模式。
- 握手完成确认 (Handshake Finished):客户端和服务器分别发送握手完成确认消息,确认握手过程完成。
- 加密通信 (Encrypted Communication):握手完成后,客户端和服务器使用主密钥进行加密通信。
在上面的步骤中,主要有三个核心流程:
- 身份验证:通过服务器的 SSL/TLS 证书验证其身份。
- 密钥协商:利用非对称加密生成共享的会话密钥。
- 加密通信:使用对称加密(如 AES)提高传输效率。
HTTPS 是通过在 HTTP 上加入 TLS(传输层安全协议)实现安全通信的,它提供加密、身份验证和数据完整性保护。TLS 握手是 HTTPS 的核心流程,客户端与服务器通过握手协商加密算法、验证服务器身份,并生成共享的会话密钥。完成握手后,双方使用对称加密对数据进行高效传输,确保通信内容的机密性和完整性。
总结
尽管 HTTPS 提供了显著的安全优势,但由于 性能开销、证书管理成本、特定场景需求 和 历史遗留问题,一些场景下仍然使用 HTTP。不过,随着免费证书的普及、TLS 1.3 的性能提升以及对安全性的重视,使用 HTTPS 已成为现代互联网的趋势,并被搜索引擎(如 Google)优先推荐。
HTTPS
的本质就是在 HTTP
的基础上添加了安全层,主要是通过他来加密和验证机制来保护通信数据的安全性和隐私性。它提供了保密性、完整性和身份验证的重要机制,使得数据在传输过程中得到了有效的保护,防止数据被窃听、篡改和伪装。
来源:juejin.cn/post/7459561147580235795
10年开发后,我后悔坚持的8个技术信仰,不知你踩中几个
今天,我在生产环境排查一个莫名其妙的崩溃。日志里布满了层层抽象的调用栈,像一张无边的蜘蛛网。
代码里的每一行都符合“最佳实践”,架构精雕细琢,可Bug还是来了。那个瞬间,我突然想起四年前的自己——曾无比自豪地告诉新人:“优雅架构就是一切。”
可现在,我只想对那时候的自己说:“别装了,写能跑的代码吧。”
十年开发生涯让我推翻了许多曾深信不疑的技术理念。今天,我把这些踩坑经历整理出来,希望能帮你少走些弯路。
01 | 技术理念的崩塌
1. “简单”从来不是免费的,它是最昂贵的选择
四年前,我坚信“简单至上”。后来我才发现,让代码保持简单,需要持续的投入。业务需求膨胀时,每个“简单”的架构决策,都要付出昂贵的代价去维护。
真正的“简单”,不是一开始就写出完美代码,而是有能力在复杂性爆炸前,把代码逐步“修剪”回合理状态。
2. “优雅”是幻觉,能跑才是道理
曾经,我会在代码里反复推敲命名,调整缩进,优化模式,追求某种“美感”。但经历了几次生产事故后,我意识到:“优雅”从来不是一个真实的技术指标。
系统能否稳定运行、团队能否快速交接,远比代码的“形式美”重要。优雅的代码,不如无Bug的代码。
3. ORM是恶魔,SQL才是答案
我曾经推崇ORM,认为它能屏蔽数据库差异,提升开发效率。后来被它坑惨了:复杂查询写不出来,性能优化受限,Debug像拆炸弹。
最终,我回归了最原始的方式——直接写SQL。数据库优化的最佳方式,就是尊重SQL,而不是绕开它。
4. “类型安全”是团队的保护网
过去,我对强类型语言嗤之以鼻,觉得动态语言写起来灵活高效。直到一次团队交接,动态代码的“魔法”变成了无尽的痛苦。
Typed语言像护栏,帮团队里经验不同的人保持代码质量。个人写代码可以随性,团队协作必须稳健。
5. 前端开发已经卷成了噩梦
十年前,前端是HTML+CSS+JS,简单直白。现在,前端是一套复杂的工程体系,动不动就要学框架、学编译、学服务端渲染。
我越来越觉得,前端的“工程化”并没有带来应有的幸福感,而是让开发变得越来越焦虑。
6. Serverless 未来是好东西,但现在还是坑
Serverless的愿景很美好,可落地时,我无数次因为它的冷启动、调试难、监控难而崩溃。
如果你要做长期稳定的业务,老老实实选传统架构,Serverless 还没成熟到能承载大部分业务的程度。
7. “软件工程”大多时候只是沟通问题
这几年,我越来越发现,软件开发不是“写代码”这么简单,而是沟通、协作、妥协的过程。技术难点从来不是代码,而是“如何让所有人理解代码”。
会编码是一回事,能让别人看懂你的代码,才是真本事。
8. “管理”比技术重要,但真正好的管理极为稀缺
我花了很长时间才意识到,一个烂的管理,会让优秀的工程师一身狼狈;而一个好的管理,能让普通人也做出优秀的产品。
好管理者太少了,大多数的管理者,只是在消耗开发者的创造力。
02 |如何避免踩坑?
十年后的我,给刚入行的开发者3个建议:
① “代码洁癖”适可而止,写业务代码时别钻牛角尖
不要为了“优雅”而牺牲实用性,别陷入“最佳实践”的执念。实用 > 形式美。
② 直接写SQL,别太信任ORM
ORM适合简单查询,但复杂业务逻辑,SQL才是终极答案。与其踩坑,不如早点学会手写SQL。
③ 沟通能力比技术能力更值钱
代码能跑很重要,但能解释给别人听,能让团队顺畅协作,才是更核心的能力。
03 | 技术思维的升级
- “代码简单”不是靠写出来的,而是靠不断重构出来的
- “优雅”不是工程目标,稳定和可维护才是
- “工程师文化”最重要的是沟通,不是编码
04 | 你踩过这些坑吗?
如果你也在开发生涯中经历了类似的转变,欢迎留言分享你的故事。
你现在的信仰,四年后还会坚守吗?点击“在看”,收藏这篇文章,未来某一天再回来看,你的想法是否还一样。
来源:juejin.cn/post/7468100737994784787
最近看到太多 cursor 带来的焦虑,有些话想说
大家好,我卡颂,专注于AI助力程序员转型(阅读我的更多思考)
最近,有很多用cursor短时间开发应用的例子,其中不乏没有编程能力的非程序员。
这就给程序员群体带来一种焦虑 —— 我赖以谋生的技能会快速贬值么?
之所以会有这种焦虑,是因为看待AI与看待自身职业的角度不同:
- 从发展角度看待AI:默认AI能力会越来越强
- 从静态角度看待本职工作:默认程序员工作一成不变
如果我们能从发展角度看待本职工作,就能看到不一样的东西。
前端会经历的三个阶段
以我熟悉的前端行业举例(其实程序员工种也适用)。
从发展角度看待前端行业,当前行业正处在大规模的开发范式迁移中。
什么是开发范式迁移?举2个例子:
- 从原生JS过渡到
jQuery
的链式写法 - 从
jQuery
的命令式过渡到前端框架的声明式写法
当前,前端行业正处在由AI主导的新一轮开发范式迁移中,这是阶段一。
当开发范式迁移完成后,会形成事实上的新的前端技术栈,这是阶段二。
当新的前端技术栈形成后,会产生新的前端发展路径。
完整三个阶段演进过程如下:
接下来我详细解释下每个阶段。
阶段1:开发范式迁移
大规模开发范式迁移的显著特征,是不断出现新的开发工具,不断有开发工具被抛弃。
比如,在AI辅助编码领域,先行者是Github Copilot
,他开创了AI驱动的Tab补全代码这一AI辅助模式。
再往后,有了Continue
、Cursor
、Windsurf
,在Copilot
基础上创造了更多辅助模式,比如:
- Chat模式
- Normal/Agent Composer
其中,AI驱动的Tab补全代码已经逐渐成为程序员开发标配。
这就是开发范式迁移的一个例子。
接下来,我再举一个例子。
AI驱动的前端脚手架工具
当提到前端脚手架工具,大部分人第一反应是Vite
、CRA
这样的工具。
他们都属于上个前端开发范式时代的脚手架工具。
在当下,已经涌现很多AI驱动的前端脚手架工具,比如v0、bolt.new。
v0
是Vercel
开发的,可以理解为他是基于Vercel
旗下开发工具(Next.js
、shadcn
)的前端脚手架工具。
bolt.new
是Stackblitz
开发的,没有绑定具体前端技术,是一个比较通用的全栈项目(基于Node.js
)脚手架工具。
总结下,在范式迁移过程中,这些新技术不断涌现,又不断消失。
最终的胜者会成为未来前端技术栈中的固定嘉宾。
一件有趣的事:一般来说,技术、工具的普及是由于“程序员大规模认可”。但未来,他们的普及可能是因为“AI大规模认可”
阶段2:新的前端技术栈
为什么AI辅助编码已经是开发标配,但大部分公司的招聘要求中却没有提及?
因为AI辅助编码还在高速发展中,没有形成最佳实践。
只有到形成业界公认的最佳实践,成为新的前端技术栈,才会出现在主流的招聘要求中。
对Cursor
的焦虑本质来说,就是没意识到随着AI的发展,前端技术栈也会更新。
如果Cursor
(或者Cursor
同赛道的最终胜者)是程序员技术栈中要求需要熟练掌握的工具(就如同当前前端技术栈中的前端框架)。
那你还会因为“工具提高了开发效率”而焦虑么?
阶段3:新的前端发展路径
当前端(或其他任何程序员工种)完成开发范式迁移,形成事实上的新技术栈。
会造成两个结果:
- 职业门槛大幅度上升
- 开发效率大幅度提高
这势必会让行业洗牌,出清掉大量从业者。
同时也会形成新的前端发展路径。
当我们最终达到阶段3(产生新的前端发展路径),我认为他会是下面这样:
未来,基础的前端岗位是使用AI工具的前端专才,他包括两项能力:
- 熟练使用AI编码辅助工具
- 熟练的前端理论知识(类比当前的资深前端)
前端专才有两个进阶方向:
- 制作AI工具的前端
- 使用AI工具的前端多才
我解释下这两者。
其中,制作AI工具的前端类似当前的基建岗前端。
但不同的是,当下的基建岗前端很难回答一个问题:我花费大量时间做的工具相比同类开源产品有啥决定性优势?
与前者不同,制作AI工具的前端的产出是业务定制化的AI提效工具,这是与业务强相关的。
就像曾几何时,任何前端团队都需要一个webpack配置工程师
一样。
前端多才
则是指以前端技能为核心能力,同时掌握与前端相邻、平行工种的工作技能
- 相邻工种:UI、测试、后端、产品
- 平行工种:其他端的前端
这里举一个大前端开发工程师
的例子。
当前国内开发现状是 —— 端碎片化。
小程序、移动端、hybrid、web等,一个需求可能有多端开发需要。
对于多端开发需求,常见的解决方案有两种:
- 花钱方案:组建大前端团队
- 省钱方案:使用跨端方案,UniApp、Taro、React Native、Flutter...
由于各端逻辑类似,如果你同时掌握多端能力,只要实现一端后,就能借助AI将代码转为其他端。
只要“AI转代码的成本”比“调试跨端框架”低,这就是可行的。
随着AI的发展,当前者的收益显著高于后者后,就会出现大前端开发工程师
这一前端多才职业。
总结
事物是发展的,不仅AI如此,程序员行业亦是如此。
如果用静态的眼光看待程序员行业,满满都是被AI取代的焦虑。
但用发展的眼光看待时,这一行仍处于这波AI浪潮的早期 —— 大规模开发范式迁移阶段。
后面还有很长的路要走。
来源:juejin.cn/post/7452736507826683931
程序员工作七年后的觉醒:不甘平庸,向上成长,突破桎梏
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。
昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。
看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,在约束着你。
上学时,你要好好读书,争取考上985/211,最起码上个一本。
工作后,大家都羡慕考公上岸的,上不了岸的话,你需要找一个好公司,拿到一个高工资,最好还能当上管理人员。
后来有了家庭,你要承担起男人的责任,赚钱养家。
过去20多年的时间,我都觉着这样的条条框框没有问题,在年少轻狂的时光,这些条条框框决定了你的下限,大家不都是这么过来的吗?
可是我过去的努力,都是为了符合条条框框的各项要求。我越来越觉着疑惑,我的努力,到底是为了什么啊,是为了这些世俗上的要求吗,我到底为谁而活?
压力,是自己给的
说实话,自己也给自己不少压力。
刚毕业,没有房贷车贷的情况下,我便给了自己很大的压力。压力怎么来的呢?比如一个月五千块钱的工资,买不起一个最新款的iPhone,又比如北京的朋友们,工资相比我在二线城市,竟能高出我一倍。
后来工作半年决定去北京,也是工作7年来,唯一一次的裸辞。
初生牛犊不怕虎,裸辞给我带来的毒打,至今历历在目,比如银彳亍卡余额一天天减少的焦虑,比如连面试都没有的焦虑,还有时刻担心着要是留不在北京,被迫得回老家的焦虑。
记得青旅楼下,有一家串店叫“很久以前羊肉串”,不到五点的时候门口就会有人排队,晚上下楼时看着饭店里熙熙攘攘,吃着烤串喝着扎啤的人时,心里十分羡慕,但却又不会踏进饭店一步。
毕竟一个目前找不到工作的人,每天一睁眼就是吃饭和住青旅的成本,吃个20块钱一顿的快餐就好了,怎么可能花好几百下馆子呢?
那时候心里有个愿望,就是我也想每周都可以和朋友来这里吃顿烧烤、喝喝扎啤。
嗯,我也不知道为什么,那时候对自己就是这么严苛。家庭虽不算富裕,但也绝不可能差这几顿烧烤、住几晚好的宾馆的钱,但我就是这样像苦行僧一样要求着自己,仿佛在向爸妈多要一分钱,就代表着自己输了。
后来工作稳定了,工资也比毕业时翻了几倍,恰巧又在高位上车了房子,但似乎压力只增不减,同样是不敢花钱。
现在又有了娃,这次压力也不用自己给了,别管他需要什么,一个小眼神,你只想给他买最好的。因此不敢请假,更不敢裸辞GAP一段时间了,这种感觉就像是在逃避赚钱的责任,不误正业一般。
一味的向前冲
带着压力,只能一味的向前冲,为了更高的薪资不断学习,为了更高的职级不断拼搏。
在“赚钱”这件事上,男人的基因里就像被编写好了一段代码。
while (true){
makeMoreMoney();
}
过程中遇到困难,压力大,有难过的时候怎么办,身边有谁能去诉说呢?
中国的传统文化便是“男儿当自强”、“男儿有泪不轻弹”,怎么能去向别人诉说自己的痛苦呢?
那时候现在的老婆那时候还在上学,学生很难理解职场。结婚后,更没有人愿意在伴侣前展示自己的软弱。
和家人说?但是不开心的事,不要告诉妈妈,她帮不上忙,她只会睡不着觉。
和好朋友们一起坐下聚聚,喝几杯啤酒,少聊一些工作,压力埋在心里,让自己短暂的放松一下。
但现在的行业现状,不允许我们一味的在职场上冲了。
行业增速放缓,互联网渗透率达到瓶颈,随着而来的就是就业环境变差,裁员潮来袭。
你可以选择在职场中的高薪与光环,但也要付出相应的代价,比如变成“云老公/老婆”,“云爸爸/妈妈”。
或许我们都很想在职场中有一番作为,但是外部环境可能会让我们头破血流。
为了家庭,所以在职场中精进自己,升职加薪。我不禁在想,这看似符合逻辑的背后,我自己到底奋斗的是什么?
不甘平庸,不服输
从老家裸辞去北京,是不满足于二线城市的工作环境,想接触互联网,获得更快的进步。
在北京,从小公司跳槽到大厂,是为了获得更高的薪资与大厂的光环。
再次回到老家,是不满生活只有工作,回来可以更好的平衡工作和生活。
回想起来,很多时候,自己就像一个异类。
明明工作还不满一年,技术又差,身边的朋友敢于跳槽到其他公司,涨一两千块钱的工资已经算挺好了,我却非得裸辞去北京撞撞南墙。
明明可以在中小公司里按部就班,过着按点下班喝酒打游戏的生活,却非得在在悠闲地时候,去刷算法与面经,不去大厂不死心。
明明可以在大公司有着不错的发展,负责着团队与核心系统,却时刻在思考生活中不能只有工作,还要平衡工作和家庭,最终放弃大厂工作再次回到老家。
每一阶段,我都不甘心于在当下的环境平庸下去,见识到的优秀的人越多,我便越不服输。
至此,我上面问自己的两个问题,我到底为谁而活?我自己到底奋斗的是什么,似乎有了些答案。
我做的努力,短期看是为了能够给自己、给家人更好的物质生活,但长远来看,是为了能让自己有突破桎梏与困境,不断向上的精神。
仰望星空
古希腊哲学家苏格拉底有一句名言:“未经检视的人生不值得活。”那么我们为什么要检视自己的人生呢?正是因为我们有不断向上的愿望,那么我在想愿望的根源又到底是什么呢?
既然选择了不断向上,我决定思考,自己想成为什么样的人,或者说,一年后,希望自己变成什么样子,3年呢,5年呢?
当然,以后的样子,绝不是说,我要去一个什么外企稳定下来,或者说去一个大厂拿多少多少钱。
而是说,我希望的生活状态是什么,我想去做什么工作/副业,达成什么样的目标。
昨天刷到了一个抖音,这个朋友在新疆日喀则,拍下了一段延时摄影,我挺受震撼的。
生活在钢铁丛林太久了,我一直特别想去旅行,比如自驾新疆、西藏,反正越远越好。在北京租的房子,就在京藏高速入口旁,我每天上班都可以看到京藏高速的那块牌子,然后看着发会呆,畅想一下自己开着车在路上的感觉。
可好多年过去了,除了婚假的时候出去旅行,其余时间都因为工作不敢停歇,始终没有机会走出这一步,没有去看看祖国的大好河山。
我还发现自己挺喜欢琢磨,无论在做什么事情,我都会大量的学习,然后找到背后运行的规律。因为自己不断的思考,所以现实中,很少有机会和朋友交流,所以我会通过写作的方式,分享自己的思考、经历、感悟。
我写了不少文章,都是关于工作几年,我认为比较重要的经历的文章,也在持续分享我关于职业生涯的思考。
从毕业到职场,走过的弯路太多了,小到技术学习、架构方案设计,大到职业规划与公司选择,每当回忆起自己在职场这几年走过的弯路,就特别想把一些经验分享给更多的人,所以我持续的写,希望看到我文章的朋友,都能够对工作、生活有一点点帮助。
所以,我的短期目标,是希望能够帮助在职场初期、发展期,甚至一些稳定期的朋友们,在职场中少一点困惑,多一点力量。
方式可能有很多,比如大家看我的文章,看我推荐的书籍、课程,甚至约我电话进行1v1沟通,都可以,帮助到一个人,我真的就会感到很满足,假设因为个人能力不足暂时帮不到,我也能根据自己的不足持续学习成长。
那么一年后,我希望自己变成什么样?
我希望自己在写作功底上,能够持续进步,写出更具有逻辑性、说服力的内容,就像明白老师、雪梅老师那样。公众号希望写出一篇10w+,当然数量越多越好,当然最希望的是有读者能够告诉我,读完这篇文章很有收获,这样比数据更能让人开心,当然最好还能够有一小部分工作之外的收入。
那么三年呢?
3年后,快要32岁了。希望那时候我已经积累了除了写作外,比如管理、销售、沟通、经营能力,能够有自己赚到工资外收入的产品、项目,最好能够和职场收入打平,最差能够和房贷打平,有随时脱离职场的底气。
五年呢?十年呢?
太久远了,想起来都很吃力的感觉。我一定还在工作,但一定不是打工,希望自己有了一份自己喜欢的事业,能够买到自己的dream car,然后能够随时带着家人看一看中国的大好河山。
你是不是想问,为什么一定要想这些?
因为当我想清楚这个问题的时候,那当下该做什么事情,该做什么选择,就有了一个清晰的标准:这件事情、这个选择,能否帮我们朝「未来的自己」更进一步?
这时候当再遇到压力、困难,我们就会变的乐观,有毅力、有勇气、自信、耐心,积极主动。
因为你自己想干成一件事,你就会迸发出120%的能量。
当然,也希望自己试试放下盔甲,允许自己撤退,允许自己躺平,允许自己怂,允许自己跟别人倾诉痛苦。
说在最后
说了很多,感谢你能看到最后。
感觉整体有点混乱,但还是总结一下:
起因是感觉自己压力很大,因为持续的大量输入导致自己有点陷入信息爆炸的焦虑,有一天下班到家时感觉头痛无比,九点就和孩子一起睡觉了,因此本来想谈谈中国男性的压力。
但不由自主的去思考自己的压力是从哪里来的,去发现压力竟都来源于传统文化、社会要求,于是越想越不服气,我为什么非得活成别人认为应该活成的样子?
于是试着思考自己想成为什么样子,其实也是一直在琢磨的一件事情,因为当开始探索个人IP的时候,我就发现自己需要更高一层的、精神层面的指导,才能让自己坚持下去。
如果你和我一样,希望你给自己的压力更小一些,环境很差,但总还有事情可以去做,愿你可以想清楚,你想成为的样子。一时想不清楚也没关系,也愿你可以允许自己撤退,允许自己软弱。
不知道你有没有想过,自己想要成为的样子呢?欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~
本篇文章是第37篇原创文章,2024目标进度37/100,欢迎有趣的你,关注我。
来源:juejin.cn/post/7374337202653265961
原来,这些顶级大模型都是蒸馏的
「除了 Claude、豆包和 Gemini 之外,知名的闭源和开源 LLM 通常表现出很高的蒸馏度。」这是中国科学院深圳先进技术研究院、北大、零一万物等机构的研究者在一篇新论文中得出的结论。
前段时间,一位海外技术分析师在一篇博客中提出了一个猜想:一些顶级的 AI 科技公司可能已经构建出了非常智能的模型,比如 OpenAI 可能构建出了 GPT-5,Claude 构建出了 Opus 3.5。但由于运营成本太高等原因,他们将其应用在了内部,通过蒸馏等方法来改进小模型的能力,然后依靠这些小模型来盈利(参见《GPT-5、 Opus 3.5 为何迟迟不发?新猜想:已诞生,被蒸馏成小模型来卖》)。
当然,这只是他的个人猜测。不过,从新论文的结论来看,「蒸馏」在顶级模型中的应用范围确实比我们想象中要广。
具体来说,研究者测试了 Claude、豆包、Gemini、llama 3.1、Phi 4、DPSK-V3、Qwen-Max、GLM4-Plus 等多个模型,发现这些模型大多存在很高程度的蒸馏(Claude、豆包和 Gemini 除外)。比较明显的证据是:很多模型会在声明自己身份等问题时出现矛盾,比如 llama 3.1 会说自己是 OpenAI 开发的,Qwen-Max 说自己由 Anthropic 创造。
蒸馏固然是一种提升模型能力的有效方法,但作者也指出,过度蒸馏会导致模型同质化,减少模型之间的多样性,并损害它们稳健处理复杂或新颖任务的能力。所以他们希望通过自己提出的方法系统地量化蒸馏过程及其影响,从而提供一个系统性方法来提高 LLM 数据蒸馏的透明度。
- 论文标题:Distillation Quantification for Large Language Models
- 论文链接:github.com/Aegis1863/L…
- 项目链接:github.com/Aegis1863/L…
为什么要测试 LLM 的蒸馏情况?
最近,模型蒸馏作为一种更有效利用先进大语言模型能力的方法,引起了越来越多的关注。通过将知识从更大更强的 LLM 迁移到更小的模型中,数据蒸馏成为了一个显著的后发优势,能够以更少的人工标注和更少的计算资源与探索来实现 SOTA 性能。
然而,这种后发优势也是一把双刃剑,它阻止了学术机构的研究人员和欠发达的 LLM 团队自主探索新技术,并促使他们直接从最先进的 LLM 中蒸馏数据。此外,现有的研究工作已经揭示了数据蒸馏导致的鲁棒性下降。
量化 LLM 的蒸馏面临几个关键挑战:
- 蒸馏过程的不透明性使得难以量化学生模型和原始模型之间的差异;
- 基准数据的缺乏使得需要采用间接方法(如与原始 LLM 输出的比较)来确定蒸馏的存在;
- LLM 的表征可能包含大量冗余或抽象信息,这使得蒸馏的知识难以直接反映为可解释的输出。
最重要的是,数据蒸馏在学术界的广泛使用和高收益导致许多研究人员避免批判性地检查与其使用相关的问题,导致该领域缺乏明确的定义。
研究者使用了什么方法?
作者在论文中提出了两种方法来量化 LLM 的蒸馏程度,分别是响应相似度评估(RSE)和身份一致性评估(ICE)。
RSE 采用原始 LLM 的输出与学生大语言模型的输出之间的比较,从而衡量模型的同质化程度。ICE 则采用一个知名的开源越狱框架 GPTFuzz,通过迭代构造提示来绕过 LLM 的自我认知,评估模型在感知和表示身份相关信息方面的差异 。
他们将待评估的特定大语言模型集合定义为 LLM_test = {LLM_t1,LLM_t2,...,LLM_tk},其中 k 表示待评估的 LLM 集合的大小。
响应相似度评估(RSE)
RSE 从 LLM_test 和参考 LLM(在本文中即 GPT,记为 LLM_ref)获取响应。作者随后从三个方面评估 LLM_test 和 LLM_ref 的响应之间的相似度:响应风格、逻辑结构和内容细节。评估者为每个测试 LLM 生成一个它与参考模型的整体相似度分数。
作者将 RSE 作为对 LLM 蒸馏程度的细粒度分析。在本文中,他们手动选择 ArenaHard、Numina 和 ShareGPT 作为提示集,以获取响应并评估 LLM_test 在通用推理、数学和指令遵循领域的相关蒸馏程度。如图 3 所示,LLM-as-a-judge 的评分分为五个等级,每个等级代表不同程度的相似度。
身份一致性评估(ICE)
ICE 通过迭代构造提示来绕过 LLM 的自我认知,旨在揭示嵌入其训练数据中的信息,如与蒸馏数据源 LLM 相关的名称、国家、位置或团队。在本文中,源 LLM 指的是 GPT4o-0806。
作者在 ICE 中采用 GPTFuzz 进行身份不一致性检测。首先,他们将源 LLM 的身份信息定义为事实集 F,F 中的每个 f_i 都清楚地说明了 LLM_ti 的身份相关事实,例如「我是 Claude,一个由 Anthropic 开发的 AI 助手。Anthropic 是一家总部位于美国的公司。」
同时,他们使用带有身份相关提示的 P_id 来准备 GPTFuzz 的 :
,用于查询 LLM_test 中的 LLM 关于其身份的信息,详见附录 B。作者使用 LLM-as-a-judge 初始化 GPTFuzz 的 F^G,以比较提示的响应与事实集 F。具有逻辑冲突的响应会被识别出来,并相应地合并到 F^G 的下一次迭代中。
作者基于 GPTFuzz 分数定义两个指标:
- 宽松分数:将任何身份矛盾的错误示例视为成功攻击;
- 严格分数:仅将错误识别为 Claude 或 GPT 的示例视为成功攻击。
实验结果如何?
ICE 的实验结果如图 4 所示,宽松分数和严格分数都表明 GLM-4-Plus、Qwen-Max 和 Deepseek-V3 是可疑响应数量最多的三个 LLM,这表明它们具有更高的蒸馏程度。相比之下,Claude-3.5-Sonnet 和 Doubao-Pro-32k 几乎没有显示可疑响应,表明这些 LLM 的蒸馏可能性较低。宽松分数指标包含一些假阳性实例,而严格分数提供了更准确的衡量。
作者将所有越狱攻击提示分为五类,包括团队、合作、行业、技术和地理。图 5 统计了每种类型问题的成功越狱次数。这个结果证明 LLM 在团队、行业、技术方面的感知更容易受到攻击,可能是因为这些方面存在更多未经清理的蒸馏数据。
如表 1 所示,作者发现相比于监督微调(SFT)的 LLM,基础 LLM 通常表现出更高程度的蒸馏。这表明基础 LLM 更容易表现出可识别的蒸馏模式,可能是由于它们缺乏特定任务的微调,使它们更容易受到评估中利用的漏洞类型的影响。
另一个有趣的发现是,实验结果显示闭源的 Qwen-Max-0919 比开源的 Qwen 2.5 系列具有更高的蒸馏程度。作者发现了大量与 Claude 3.5-Sonnet 相关的答案,而 2.5 系列 LLM 的可疑答案仅与 GPT 有关。这些示例在附录 D 中有所展示。
RSE 结果在表 3 中展示,以 GPT4o-0806 作为参考 LLM,结果表明 GPT 系列的 LLM(如 GPT4o-0513)表现出最高的响应相似度(平均相似度为 4.240)。相比之下,像 Llama3.1-70B-Instruct(3.628)和 Doubao-Pro-32k(3.720)显示出较低的相似度,表明蒸馏程度较低。而 DeepSeek-V3(4.102)和 Qwen-Max-0919(4.174)则表现出更高的蒸馏程度,与 GPT4o-0806 相近。
为了进一步验证观察结果,作者进行了额外的实验。在这个设置中,他们选择各种模型同时作为参考模型和测试模型。对于每种配置,从三个数据集中选择 100 个样本进行评估。附录 F 中的结果表明,当作为测试模型时,Claude3.5-Sonnet、Doubao-Pro-32k 和 Llama3.1-70B-Instruct 始终表现出较低的蒸馏程度。相比之下,Qwen 系列和 DeepSeek-V3 模型倾向于显示更高程度的蒸馏。这些发现进一步支持了本文所提框架在检测蒸馏程度方面的稳健性。
更多细节请参考原论文。
来源:juejin.cn/post/7464926870544089097
离职后的这半年,我前所未有的觉得这世界是值得的
大家好,我是一名前端开发工程师,属于是没有赶上互联网红利,但赶上了房价飞涨时代的 95 后社畜。2024 年 3 月份我做了个决定,即使已经失业半年、负收入 10w+ 的如今的我,也毫不后悔的决定:辞职感受下这个世界。
为什么要辞职,一是因为各种社会、家庭层面的处境对个人身心的伤害已经达到了不可逆转的程度,传播互联网负面情绪的话我也不想多说了,经历过的朋友懂得都懂,总结来说就是,在当前处境和环境下,已经没有办法感受到任何的快乐了,只剩焦虑、压抑,只能自救;二是我觉得人这一辈子,怎么也得来一次难以忘怀、回忆起来能回甘的经历吧!然而在我的计划中,不辞职的话,做不到。
3 月
在 3 月份,我去考了个摩托车驾-照,考完后购买了一辆摩托车 DL250,便宜质量也好,开始着手准备摩旅。
4 月份正式离职后,我的初步计划是先在杭州的周边上路骑骑练下车技,直接跑长途还是很危险的,这在我后面真的去摩旅时候感受颇深,差点交代了。
4 月
4.19 号我正式离职,在杭州的出租屋里狠狠地休息了一个星期,每天睡到自然醒,无聊了就打打游戏,或者骑着摩托车去周边玩,真的非常非常舒服。
不过在五一之前,我家里人打电话跟我说我母亲生病了,糖尿病引发的炎症,比较严重,花了 2w+ 住院费,也是从这个时候才知道我父母都没有交医保(更别说社保),他们也没有正式、稳定的工作,也没有一分钱存款,于是我立马打电话给老家的亲戚让一个表姐帮忙去交了农村医保。所有这些都是我一个人扛,还有个亲哥时不时问我借钱。
说实话,我不是很理解我的父母为什么在外打工那么多年,一分钱都存不下来的,因为我从小比较懂事,没让他们操过什么心,也没花过什么大钱。虽然从农村出来不是很容易,但和周围的相同条件的亲戚对比,我只能理解为我父母真的爱玩,没有存钱的概念。
我可能也继承了他们的基因吧?才敢这样任性的离职。过去几年努力地想去改变这个处境,发现根本没用,还把自己搞得心力交瘁,现在想想不如让自己活开心些吧。
5 月
母亲出院后,我回到杭州和摩友去骑了千岛湖,还有周边的一些山啊路啊,累计差不多跑了 2000 多公里,于是我开始确立我的摩旅计划,路线是杭州-海南岛-云南-成都-拉萨,后面实际跑的时候,因为云南之前去过,时间又太赶,就没去云南了。
6 月
在摩友的帮助下,给摩托车简单进行了一些改装,主要加了大容量的三箱和防雨的驮包,也配备了一些路上需要的药品、装备,就一个人出发了。
从杭州到海南这部分旅行,我也是简单记录了一下,视频我上传了 B 站,有兴趣的朋友可以看看:
拯救焦虑的29岁,考摩托车驾-照,裸辞,买车,向着自由,出发。
摩托车确实是危险的,毕竟肉包铁,即使大部分情况我已经开的很慢,但是仍然会遇到下大雨路滑、小汽车别我、大货车擦肩而过这种危险情况,有一次在过福建的某个隧道时,那时候下着大雨,刚进隧道口就轮胎打滑,对向来车是连续的大货车,打滑之后摩托车不受控制,径直朝向对向车道冲过去,那两秒钟其实我觉得已经完蛋了,倒是没有影视剧中的人生画面闪回,但是真的会在那个瞬间非常绝望,还好我的手还是强行在对龙头进行扳正,奇迹般地扳回来且稳定住了。
过了隧道惊魂未定,找了个路边小店蹲在地上大口喘气,雨水打湿了全身加上心情无法平复,我全身都是抖的,眼泪也止不住流,不是害怕,是那种久违地从人类身体发出的求生本能让我控制不住情绪的肆意发泄。
在国道开久了人也会变得很麻木,因为没什么风景,路况也是好的坏的各式各样,我现在回看自己的记录视频,有的雨天我既然能在窄路开到 100+ 码,真的很吓人,一旦摔车就是与世长辞了。
不过路上的一切不好的遭遇,在克服之后,都会被给予惊喜,到达海南岛之后,我第一次感觉到什么叫精神自由,沿着海边骑行吹着自由的风,到达一个好看的地方就停车喝水观景,玩沙子,没有工作的烦扰,没有任何让自己感受到压力的事情,就像回到了小时候无忧无虑玩泥巴的日子,非常惬意。
在完成海南环岛之后,我随即就赶往成都,与前公司被裁的前同事碰面了。我们在成都玩了三天左右,主要去看了一直想看的大熊猫🐼!
之后我们在 6.15 号开始从成都的 318 起始点出发,那一天的心情很激动,感觉自己终于要做一件不太一样的事,见不一样的风景了。
小时候在农村,读书后在小镇,大学又没什么经济能力去旅行,见识到的事物都非常有限,但是这一切遗憾在川藏线上彻底被弥补了。从开始进入高原地貌,一路上的风景真的美到我哭!很多时候我头盔下面都是情不自禁地笑着的,发自内心的那种笑,那种快乐的感觉,我已经很久很久很久没有了。
同样地,这段经历我也以视频的方式记录了下来,有兴趣的朋友可以观看:
以前只敢想想,现在勇敢向前踏出了一步,暂时放下了工作,用摩托跑完了318
到拉萨了!
花了 150 大洋买的奖牌,当做证明也顺便做慈善了:)
后面到拉萨之后我和朋友分开了,他去自驾新疆,我转头走 109 国道,也就是青藏线,这条线真的巨壮美,独自一人行驶在这条路,会感觉和自然融合在了一起,一切都很飘渺,感觉自己特别渺小。不过这条线路因为冻土层和大货车非常非常多的原因,路已经凹凸不平了,许多炮弹坑,稍微骑快点就会飞起来。
这条线还会经过青海湖,我发誓青海湖真的是我看到过最震撼的景色了,绿色和蓝色的完美融合,真的非常非常美,以后还要再去!
拍到了自己的人生照片:
经历了接近一个半月的在外漂泊,我到了西宁,感觉有点累了,我就找了个顺丰把摩托车拖运了,我自己就坐飞机回家了。
这一段经历对我来说非常宝贵,遇到的有趣的人和事,遭遇的磨难,见到的美景我无法大篇幅细说,但是每次回想起这段记忆我都会由衷地感觉到快乐,感觉自己真的像个人一样活着。
这次旅行还给了我感知快乐和美的能力,回到家后,我看那些原来觉得并不怎么样的风景,现在觉得都很美,而且我很容易因为生活中的小确幸感到快乐,这种能力很重要。
7 月
回到家大概 7 月中旬。
这两个多月的经历,我的身体和心态都调整的不错了,但还不是很想找工作,感觉放下内心的很多执念后,生活还是很轻松的,就想着在家里好好陪陪母亲吧,上班那几年除了过年都没怎么回家。
在家里没什么事,但是后面工作的技能还是要继续学习的,之前工作经历是第一家公司用的 React 16,后面公司用的是 Vue3,对 React 有些生疏,我就完整地看了下 React 18 的文档,感觉变化也不是很大。
8、9 月
虽然放下了许多执念,对于社会评价(房子、结婚、孩子)也没有像之前一样过于在乎了,但还是要生活的,也要有一定积蓄应对未来风险,所以这段时间在准备面试,写简历、整理项目、看看技术知识点、刷刷 leetcode。
也上线了一个比较有意义的网站,写了一个让前端开发者更方便进行 TypeScript 类型体操的网站,名字是 TypeRoom 类型小屋,题源是基于 antfu 大佬的 type-challenges。
目前 Type Challenges 官方提供了三种刷题方式
- 通过 TypeScript Playground 方式,利用 TypeScript 官方在线环境来刷题。
- 克隆 type-challenges 项目到本地进行刷题。
- 安装 vscode 插件来刷题。
这几种方式其实都很方便,不过都在题目的可读性上有一定的不足,还对开发者有一定的工具负担、IDE 负担。
针对这个问题,也是建立 TypeRoom 的第一个主要原因之一,就是提供直接在浏览器端就能刷题的在线环境,并且从技术和布局设计上让题目描述和答题区域区分开来,更为直观和清晰。不需要额外再做任何事,打开一个网址即可直接开始刷题,并且你的答题记录会存储到云端。
欢迎大家来刷题,网址:typeroom.cn
因为个人维护,还有很多题目没翻译,很多题解没写,也还有很多功能没做,有兴趣一起参与的朋友可以联系我哦,让我一起造福社区!
同时也介绍下技术栈吧:
前端主要使用 Vue3 + Pinia + TypeScript,服务端一开始是 Koa2 的,后面用 Nest 重写了,所以现在服务端为 Nest + Mysql + TypeORM。
另外,作为期待了四年,每一个预告片都看好多遍的《黑神话·悟空》的铁粉,玩了四周目,白金了。
现在
现在是 10 月份了,准备开始投简历找工作了,目前元气满满,不急不躁,对工作没有排斥感了,甚至想想工作还蛮好的,可能是闲久了吧,哈哈哈,人就是贱~
更新 11 月
我还是没有找工作,又去摩旅了一趟山西、山东,这次旅行感觉比去西藏还累、还危险。同样是做了视频放 b 站了,有兴趣的可以看看:
骑了4300km只为寻找那片海-威海的海|摩旅摩得命差点没了
真的要开始找工作了喂!
最后
其实大多数我们活得很累,都是背负的东西太多了,而这些大多数其实并不一定要接受的,发挥主观能动性,让自己活得开心些最重要,加油啊,各位,感谢你看到这里,祝你快乐!
这是我的 github profile,上面有我的各种联系方式,想交个朋友的可以加我~❤️
来源:juejin.cn/post/7424902549256224804
IDEA 接入 deepseek,太酷了。
大家好,我是二哥呀。
deepseek 官方并没有出 IntelliJ IDEA 的插件,但作为菜逼程序员的我,却很想体验一下在 IDEA 中装入 deepseek 的感觉。
一共有三种方式,一种是通过 IDEA 官方的 AI Assistant 来调用本地的 deepseek;另外两种是通过 Continue 和 CodeGPT 两款插件来曲线救国。
①、AI Assistant
AI Assistant 是新版 IDEA 自带的一个功能,属于 JetBrains 官方集成的 AI 编程助手,妥妥的嫡长子。
能提供代码补全、代码生成、优化建议、代码解释等功能。
官方已经集成了 openai 的 4o,Google 的gemini 等,开箱即用。
也支持本地 AI,比如说我们在本地已经通过 ollama 运行了 deepseek 7b 版本的大模型,就可以直接点击 connect 跳转到 enable 复选框这里。
测试通过后,我们就可以通过这里调用 deepseek 的大模型,比如说,我们让他对 DeepSeekIntegration 这个类进行解释。
他就能告诉我们:
- 发现它依赖于
okHttp
库来处理网络请求。这说明该类主要负责与外部服务 DeepSeek 进行交互。 - 类中有两个工厂方法:
executeStreamChat
和executeStreamChat(List<ChatMsg> list, EventSourceListener listener)
。这两个方法都用于创建 EventSource 并发送聊天请求到 DeepSeek。流式交互支持意味着该类可以处理分片传输的数据,逐部分地发送给服务器,然后逐步处理返回的数据。
我超,真的好用啊!
谁告诉我本地的 deepseek 没用的,脸伸过来,我保证不打肿!
这基本的代码学习,很香啊,免费,还特么很到位。
②、安装 Continue
Continue 是一款开源的 AI 代码助手插件,可以无缝安装在 IDEA 或者 VSCode 中。通过 Continue 可以加载任意大模型,从而实现代码的自动补全和聊天体验。
安装方式比较简单,直接在 IDEA 的插件中搜“Continue”关键字,然后选择下载量最高的那个就行了。
安装完成后,也有两种方式,一种是配置 deepseek 的 API Key,这个就需要充值了。
不过由于算力紧张,API 这块经常处于宕机状态。
另外一种,也是连接本地 ollama,然后去加载之前我们运行起来的 deepseek 模型。
最好拉取 coder 版本。
③、安装 CodeGPT
CodeGPT 也是一个由 AI 驱动的代码助手,官方直接说了,可以是 GitHub Copilot 的替代品。
安装完成后,同样需要在 settings 中配置 deepseek API 的 keys。
当然,也可以在这一步中切换到 ollama 的本地 deepseek。
CodeGPT 比较智能的一点是,当你在编辑器中打开了某一个类,它就会自动关联到聊天窗口。
并且能把 deepseek-R1 的整个思考过程展示出来,所以我是强烈大家按照我之前的教程在本地部署一个 7b 的本地版。
比 deepseek 官方稳定多了,毕竟本地没有上万人的同时在线给你竞争。
三分恶面渣逆袭
最近一直在修改面渣逆袭第二版,目前的进展是到并发编程的 25 题,也顺带同步给大家,刚好暑期实习和春招的小伙伴,可以日拱一卒。
25.volatile 怎么保证可见性的?
当线程对 volatile 变量进行写操作时,JVM 会在这个变量写入之后插入一个写屏障指令,这个指令会强制将本地内存中的变量值刷新到主内存中。
StoreStore; // 保证写入之前的操作不会重排
volatile_write(); // 写入 volatile 变量
StoreLoad; // 保证写入后,其他线程立即可见
在 x86 架构下,通常会使用 lock
指令来实现写屏障,例如:
mov [a], 2 ; 将值 2 写入内存地址 a
lock add [a], 0 ; lock 指令充当写屏障,确保内存可见性
当线程对 volatile 变量进行读操作时,JVM 会插入一个读屏障指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。
我们来声明一个 volatile 变量 x:
volatile int x = 0
线程 A 对 x 写入后会将其最新的值刷新到主内存中,线程 B 读取 x 时由于本地内存中的 x 失效了,就会从主内存中读取最新的值。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。
来源:juejin.cn/post/7469051964224471078
某程序员自曝:凡是打断点调试代码的,都不是真正的程序员,都是外行
大家好,我是大明哥,一个专注 「死磕 Java」 的硬核程序员。
某天我在逛今日头条的时候,看到一个大佬,说凡是打断点调试代码的,都不是真正的程序员,都是外行。
我靠,我敲了 10 多年代码,打了 10 多年的断点,竟然说我是外行!!我还说,真正的大佬都是用文档编辑器来写代码呢!!!
其实,打断点不丢脸,丢脸的是工作若干年后只知道最基础的断点调试!大明哥就见过有同事因为 for 循环里面实体对象报空指针异常,不知道怎么调试,选择一条一条得看,极其浪费时间!!所以,大明哥来分享一些 debug 技巧,赶紧收藏,日后好查阅!!
Debug 分类
对于很多同学来说,他们几乎就只知道在代码上面打断点,其实断点可以打在多个地方。
行断点
行断点的投标就是一个红色的圆形点。在需要断点的代码行头点击即可打上:
方法断点
方法断点就是将断点打在某个具体的方法上面,当方法执行的时候,就会进入断点。这个当我们阅读源码或者跟踪业务流程时比较有用。尤其是我们在阅读源码的时候,我们知道优秀的源码(不优秀的源码你也不会阅读)各种设计模式使用得飞起,什么策略、模板方法等等。具体要走到哪个具体得实现,还真不是猜出来,比如下面代码:
public interface Service {
void test();
}
public class ServiceA implements Service{
@Override
public void test() {
System.out.println("ServiceA");
}
}
public class ServiceB implements Service{
@Override
public void test() {
System.out.println("ServiceB");
}
}
public class ServiceC implements Service{
@Override
public void test() {
System.out.println("ServiceC");
}
}
public class DebugTest {
public static void main(String[] args) {
Service service = new ServiceA();
service.test();
}
}
在运行时,你怎么知道他要进入哪个类的 test()
方法呢?有些小伙伴可能就会在 ServiceA
、ServiceB
、ServiceC
中都打断点(曾经我也是这么干的,初学者可以理解...),这样就可以知道进入哪个了。其实我们可以直接在接口 Service
的 test()
方法上面打断点,这样也是可以进入具体的实现类的方法:
当然,也可以在方法调用的地方打断点,进入这个断点后,按 F7 就可以了。
属性断点
我们也可以在某个属性字段上面打断点,这样就可以监听这个属性的读写变化过程。比如,我们定义这样的:
@Getter
@Setter
@AllArgsConstructor
public class Student {
private String name;
private Integer age;
}
public class ServiceA implements Service{
@Override
public void test() {
Student student = new Student("张三",12);
System.out.println(student.getName());
student.setName("李四");
}
}
如下:
断点技巧
条件断点
在某些场景下,我们需要在特定的条件进入断点,尤其是 for 循环中(我曾经在公司看到一个小伙伴在循环内部看 debug 数据,惊呆我了),比如下面代码:
public class DebugTest {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1 ; i < 1000 ; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("" + i, i));
} else {
studentList.add(new Student("" + i, i));
}
}
for (Student student : studentList) {
System.out.println(student.toString());
}
}
}
我们在 System.out.println(student.toString());
打个断点,但是要 name
以 "skjava"
开头时才进入,这个时候我们就可以使用条件断点了:
条件断点是非常有用的一个断点技巧,对于我们调试复杂的业务场景,尤其是 for、if 代码块时,可以节省我们很多的调试时间。
模拟异常
这个技巧也是很有用,在开发阶段我们就需要人为制造异常场景来验证我们的异常处理逻辑是否正确。比如如下代码:
public class DebugTest {
public static void main(String[] args) {
methodA();
try {
methodB();
} catch (Exception e) {
e.printStackTrace();
// do something
}
methodC();
}
public static void methodA() {
System.out.println("methodA...");
}
public static void methodB() {
System.out.println("methodA...");
}
public static void methodC() {
System.out.println("methodA...");
}
}
我们希望在 methodB()
方法中抛出异常,来验证 catch(Exception e)
中的 do something
是否处理正确。以前大明哥是直接在 methodB()
中 throw 一个异常,或者 1 / 0
。这样做其实并没有什么错,只不过不是很优雅,同时也会有一个风险,就是可能会忘记删除这个测试代码,将异常提交上去了,最可怕的还是上了生产。
所以,我们可以使用 idea 模拟异常。
- 我们首先在 methodB() 打上一个断点
- 运行代码,进入断点处
- 在 Frames 中找到对应的断点记录,右键,选择
Throw Execption
- 输入你想抛出的异常,点击
ok
即可
这个技巧在我们调试异常场景时非常有用!!!
多线程调试
不知道有小伙伴遇到过这样的场景:在你和前端进行本地调试时,你同时又要调试自己写的代码,前端也要访问你的本地调试,这个时候你打断点了,前端是无法你本地的。为什么呢?因为 Idea 在 debug 时默认阻塞级别为 ALL,如果你进入 debug 场景了,idea 就会阻塞其他线程,只有当前调试线程完成后才会走其他线程。
这个时候,我们可以在 View Breakpoints 中选择 Thread,同时点击 Make Default设置为默认选项。这样,你就可以调试你的代码,前端又可以访问你的应用了。
或者
调试 Stream
Java 中的 Stream 好用是好用,但是依然有一些小伙伴不怎么使用它,最大的一个原因就是它不好调试。你利用 Stream 处理一个 List 对象后,发现结果不对,但是你很难判断到底是哪一行出来问题。我们看下面代码:
public class DebugTest {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
for (int i = 1; i < 1000; i++) {
if (new Random().nextInt(100) % 10 == 0) {
studentList.add(new Student("" + i, i));
} else {
studentList.add(new Student("" + i, i));
}
}
studentList = studentList.stream()
.filter(student -> student.getName().startsWith(""))
.peek(item -> {
item.setName(item.getName() + "-**");
item.setAge(item.getAge() * 10);
}).collect(Collectors.toList());
}
}
在 stream() 打上断点,运行代码,进入断点后,我们只需要点击下图中的按钮:
在这个窗口中会记录这个 Stream 操作的每一个步骤,我们可以点击每个标签来看数据处理是否符合预期。这样是不是就非常方便了。
有些小伙伴的 idea 版本可能过低,需要安装 Java Stream Debugger
插件才能使用。
操作回退
我们 debug 调试的时候肯定不是一行一行代码的调试,而是在每个关注点处打断点,然后跳着看。但是跳到某个断点处时,突然发现有个变量的值你没有关注到需要回退到这个变量值的赋值处,这个时候怎么办?我们通常的做法是重新来一遍。虽然,可以达到我们的预期效果,但是会比较麻烦,其实 idea 有一个回退断点的功能,非常强大。在 idea 中有两种回退:
- Reset Frame
看下面代码:
public class DebugTest {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = (a + b) * 2;
int d = addProcessor(a, b,c);
System.out.println();
}
private static int addProcessor(int a, int b, int c) {
a = a++;
b = b++;
return a + b + c;
}
}
我们在 addProcessor()
的 return a + b + c;
打上断点,到了这里 a
和 b
的值已经发生了改变,如果我们想要知道他们两的原始值,就只能回到开始的地方。idea 提供了一个 Reset Frame
功能,这个功能可以回到上一个方法处。如下图:
- Jump To Line
Reset Frame
虽然可以用,但是它有一定的局限性,它只能方法级别回退,是没有办法向前或向后跳着我们想要执行的代码处。但 Jump To Line 可以做到。
Jump To Line 是一个插件,所以,需要先安装它。
由于大明哥使用的 idea 版本是 2024.2,这个插件貌似不支持,所以就在网上借鉴了一张图:
在执行到 debug 处时,会出现一个黄颜色的箭头,我们可以将这个箭头拖动到你想执行的代码处就可以了。向前、向后都可以,是不是非常方便。
目前这 5 个 debug 技巧是大明哥在工作中运用最多的,还有一个就是远程 debug 调试,但是这个我个人认为是野路子,大部分公司一般是不允许这么做的,所以大明哥就不演示了!
来源:juejin.cn/post/7470185977434144778
只写后台管理的前端要怎么提升自己
本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。
写优雅的代码
一道面试题
大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。
原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb
,而我要展示成 KB
,MB
等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):
function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;
while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}
return `${kb.toFixed(2)} ${units[unitIndex]}`;
}
而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:
function formatSizeUnits(kb) {
var result = '';
if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}
return result;
}
虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。
如何提升代码质量
想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。
还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。
还是上面的问题,看看 GPT 给的答案
// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。
/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/
function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);
// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}
// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);
// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}
// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB
还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)
我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。
学会封装
一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?
你说,没时间,没必要,复制粘贴反而更快。
那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。
而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。
关注业务
对于前端业务重要吗?
相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。
但是就我找工作的经验,业务非常重要!
如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。
一场面试
还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。
- 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”
- 我:“好嘞!”
等到面试的时候:
- 前端ld:“你知道xxx吗?(业务名词)”
- 我:“我……”
- 前端ld:“那xxxx呢?(业务名词)”
- 我:“不……”
- 前端ld:“那xxxxx呢??(业务名词)”
- 我:“造……”
然后我就挂了………………
如何了解业务
- 每次接需求的时候,都要了解需求背景,并主动去理解
我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么
cluster
controller
topic
broker
partition
…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。 - 每次做完一个需求,都需要了解结果
有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?
- 理解需求,并主动去优化
产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?
产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。
其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。
关注源码
说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。
除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。
那说什么,后台管理就这些啊?!
如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?
可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点。
至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?
讲一下 Axios 源码中,拦截器是怎么实现的?
Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。
在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含
fulfilled
和rejected
函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。
以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:
class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}
use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}
forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}
在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过
forEach
方法将拦截器中的fulfilled
和rejected
函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。
axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的
.then
或.catch
执行之前,插入自定义的逻辑。
请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。
前端基建
当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。
技术选型
技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表单配置化方案,比如Formily?
对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)
Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。
React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。
总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。
开发规范
这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlint
,stylelint
, prettier
, commitlint
等。
前端监控
干了这么多年前端,前端监控我是……一点没做过。

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。
对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。
对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerror
和 window.addEventListener('unhandledrejection', ...)
去分别捕获同步和异步错误,然后通过错误信息和 sourceMap
来定位到源码。
对于性能监控,我们可以通过 window.performance
、PerformanceObserver
等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。
最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon
还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。
CI/CD
持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。
场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。
这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline
、 Stage
和 Job
分别是什么,怎么配置,如何在不同环境配置不同工作流等。
了解技术动态
这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。
比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。
还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……
虽然不可能学完每一项新技术,但是可以多去了解下。
总结
写了这么多,可能有人会问,如果能回到过去,你会怎么做。
啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。

来源:juejin.cn/post/7360528073631318027
准备离开杭州
上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要多久。
本人是前端,工作 6 年,期间经历过 4 家公司,前两份是外包,后面两份都是领大礼包走的,回想起来,职业生涯也是够惨的。虽然说惨,但是最近领的这一份大礼包个人认为还是值得,工作很难待下去,也没有任何成长,继续待着也是慢性死亡。
这几天我每天都会在 BOSS 上面投十几家公司,能回复非常少,邀请面试的就更少了。外包公司倒是挺多的,而我是从那个火坑里出来的,是不会选择再进去的。于是,我需要做好打持久战的准备,说不定不做程序员了。
我的房子 7 月底就要到期了,我必须要马上做决定,杭州的行情对我来说很不友好,短期内我大概率找不到工作。基于对未来的悲观考虑,我不想把过多的钱花费在房租上面,所以希望就近找一个三线城市,我搜了一下嘉兴,整租 95 平左右的房子只需要 1200 块钱,还是民用水电,思前想后,打算移居到那里嘉兴去。
一方面,我想尝试一下在三线城市生活是一种什么感觉。另一方面,这可以省钱,如果一个月的房租是 1000,民用水电,一个月的开销只要 2500 块。我搜索了一下货拉拉,从我的位置运到嘉兴,需要花费 600 块钱,这个价格也是可以接受的。思考了这些,我觉得是时候离开待了 5 年的杭州。
未来要到哪里去呢,目前可能的选择是上海。我还得想想未来能做什么,我想学一门手艺傍身,比如修理电器、炒菜。毕竟骑手行业太拥挤了,估计也不是长久之计。
房租降下来了,等我把行李都安置妥当,我打算回老家待一段时间。自从上大学以来,很少有长时间待在家里的时候,眼看父母年纪也越来越大了,很想多陪陪他们。如果进入正常的工作节奏,想做到这样还是会受到局限,这次也算是一个弥补的机会。
被裁也是一件好事,可以让我提前考虑一下未来的出路。
这段时间我想把时间用来专门学英语,再自己做几个项目,学英语的目的是为了 35 岁之后做打算,做项目是为了写到简历上面,并且个人觉得自己需要多做一个项目,这样自己才能成长到下一个级别。虽然不知道收益怎么样,但是我想尝试一下。人还活着,有精力,就还是瞎折腾一下。
离职没敢和家里说,说了估计要担心死了,反正是年轻人,有事就先自己扛一扛,我前几天把我的行李寄回去了一批,我妈问我,怎么,寄东西回来了?我回答说要搬家了。本来也想找机会开口说自己离职了,她说,这次搬家也别离公司远了,我也把话憋了进去,只好说“没事的,放心就行”。我自己没觉得离职有什么,正常的起起落落,只是觉得父母可能会过度的担心。
如果做最坏的打算,那就是回去种地,应该这几年还饿不死。有还没离职的同学,建议还是继续苟着。希望社会的低谷期早点过去,希望我们都能有美好的未来。
评论区很多朋友想要拉一个群相互交流,现在拉好了!一起来吐槽工作,交流找工作心得!
目前是 6 群,刚开始人会比较少,过一段时间就多了,稍安勿躁。群过期了可能更新不及时,在微信搜索 mysteryven,私聊问我想加 1-6 的哪一个,我会同步给你。
更新:发展到现在,几个群活跃度都变得不是很高了,目前只是一个聊天的群,不会有什么高质量的信息。如果不是想摸鱼或者什么的,不需要加入此群。有的时候我可能忘记更新二维码,可以留言提醒我~
下一篇文章:从大城市搬到小城市需要注意哪些点?
来源:juejin.cn/post/7395523104743178279
2年前的今天,我决定了躺平退休
两年前的这个时候,突然觉得说话特别费劲,舌头不太听使唤,左手突然不听话,就像李雪健老师表演那个帕金森老头喝酒一样。
我心里一慌,请假去了医院,验血,CT,超声。然后医生给我列了长长一篇诊断书:高血脂,高血压,糖尿病,冠心病,还有最可怕的脑出血,还好只是渗血,虽然并不是很严重,但是位置不太好,影响了身体感官和左手。
平时身体非常好,也经常运动,为什么会突然得这么多病呢。毫无征兆的左手就不听使唤了。而且听力在这一段时间也非常差。通过大夫诊断,一部分是遗传因素,另一个是和我常年酗酒,熬夜有关,每天几乎只睡3-4小时。
是的,,,,,,我喜欢在家喝着啤酒写代码,甚至有时候在单位加班的时候也是喝啤酒写代码。和别人不太一样,别人喝酒爱睡觉,我喝啤酒失眠。因为接了很多项目,上班之余都是晚上和周末熬夜写代码做自己的项目。
其实听到这个消息我很失望,失望的并不是因为身体垮了,钱还没赚够,而是我还没有完成我的目标就是打造一个自己主导的产品。
那天从医院回家,我并没有去坐地铁,而是从中日友好医院徒步走回天通苑的出租屋。在路上,我反复的想,今后的路该如何走。
继续在互联网行业工作肯定是不行的,病情会进一步加重,到时候就真的成一个废人了,反而会拖累整个家庭。如果不继续“卷”那我也就无法实现自己来北京的目标了。不过好在经过这么多年的积累,已经存够足够养老的资本,并不需要为妻儿老小的生存发愁,但是也没有到财富自由的程度。
躺平,躺到儿子回老家上学就回老家退休
。这是一个并不那么困难的决定。但是却是一个非常无奈的决定,躺平就意味着自己来北京定下的目标没有完成,意味着北漂失败。
做好这个决定以后,我就开始彻底躺平,把手里的几个项目草草收尾,赔了大几十万。等于这一年白忙活。好在还有一份工作收入。同时也拒掉了2个新的Offer。在疫情最困难的时候,还能拿到两个涨薪offer。我还是蛮佩服我自己的。但是为了不影响我的额外收入,加上现在工作不是很喜欢,也就一直犹豫不决。但是这次生病彻底让我下定了决定 ---- 算了。
其实,经历这么多年,什么都看的很清楚,但是我的性格并不适合这个行业,我这个人最大的特点就是腰杆子硬,不喜欢向上管理,经常有人说我那么圆滑,肯定是老油条,而实际上,我整整18年的工作经历,只对领导说过一次违心的话,变相的夸了老板定制的开发模式,老板看着我笑了笑,也不知道他是不是听出来我这话是讽刺还是撒谎。
而其余都是和老板对着干,只有2任老板是我比较钦佩的,也是配合最舒服的。而且共同特点都是百度出身,我特别喜欢百度系的老板。特别务实,认认真真做业务。不搞虚头巴脑的事情,更不在工作中弄虚作假。一个是滴滴的梁老板,另一个就是在途家时候的黄老板。
当然,在我整个职业生涯有很多厉害的老板,有的在人脉厉害,有的人个人管理能力,有的在技术。但是由于我性格原因,我就是跟他们合不来,所以要么你把我开了,要么等我找好下家主动离开。
所以我的职业生涯很不稳定,就比如我见过的一个我认为在技术能力上最厉害的老板,也是我唯一在技术上佩服的人,就是在36kr期间认识的海波老师,听他讲的系统架构分享和一些技术方案,真的是豁然开朗,在Saas和Paas的方方面面,架构演化,架构升级所可能遇到的各种问题及面对产品高速迭代所需要解决的问题及方案都门清,而且他本身也是自己带头写代码,实际编码能力也是非常的牛,并不是那种“口嗨”型领导。但就是我跟他的性格合不来,最后我把他那套架构方案摸透了以后就跑路了,而从他那里学的那套技术方案,在我日后在lowcode和Paas以及活动运营平台的技术方案设计上帮助颇多。而他不久之后也离开了。据说去了字节。
混迹于形形色色的老板手底下,遇到过的事情非常多,也让我认清了一点,那就是,牛人是需要平台去成就的,平台提供了锻炼你的机会和让你成长的机会。所以你学到了,你就成了牛人。而不是你自己手头那点沾沾自喜的觉得别人没你了解的深入的技术点。所以平台非常重要,绝大多数情况下都是如此。
所以我这种人就不适合,因为我不喜欢违心。我顶多就是不说出来,不参与,不直接反对就已经是对老板最大的尊重了
。所以我能看透很多事情,但是也知道我不讨老板喜欢,而我的性格也不可能为了让老板喜欢而卑躬屈膝,所以,我早早就提前做好准备,就是拉项目,注意这不是做私活
。拉项目就是承包项目,然后找几个做私活的人给他们开发。这项收入有时候甚至一年下来比我的工资还要高。风险也是有的,那就是可能赔钱,十几万十几万的赔。所以也是一个风险与收益共存的事情。做项目的好处是,你可以不断的接触新的甲方,扩张自己的人脉,也就不断的有项目。
但是由于这次生病,我手头的3个项目都没有做好,都被清场了。所以为了弥补朋友的损失,我一个人扛下了所有。也同时意味着后面也就没项目可接了。身体不允许了。
躺平以后,为了等孩子回老家上学,本职工作上,也开始混,我最后一年多的时间里,写代码,都不运行直接就提测。是的。没错。。。。。。就是这样。但是功能是都好用的,基本的职业操守是要有的。虽然也会有更多的bug。但是一周我只干半天就可以完成一周的工作。这可能就是经验和业务理解的重要性。所以,我一直不太理解很多互联网企业精简人员的时候为什么精简的是一线开发人员,而留下的是那些只会指挥的小组长。这也是为什么各大互联网企业都在去肥增瘦,结果肥的一点也没减下去。
不是有那么一句话,P8找P7分一个需求,然后P7找P6喊来P5开发。互联网就是这样子,一群不了解实际业务和实际代码的人,在那里高谈阔论,聊方案,聊架构,聊产品,聊业务,聊客户,聊趋势,然后让那些一脸“懵逼”的人去开发。最后的结果可想而知,最后很多需求都是一地鸡毛,但是责任却都要一线执行去承担,而为了证明需求的正向收益,那就在指标口径上“合理”的动动手脚,所以我在我整个职业生涯说出了那么一次,也是唯一一次违心的恭维话。
所以我特别佩服一个网红叫“大圣老师”,是一个卖课的,虽然我看不上他做的割韭菜的事情,但是我很佩服他这个人,他也是很刚的人,就是看不惯老板pua和无意义的加班,人家就是不干了。成功开辟了第二职业曲线,而且也很不错。
另一个网红就是“神光”,虽然我也看不上他,但是我很佩服他,佩服他追求自我的勇气。
而反观那些在职场唯唯诺诺卑躬屈膝的人,现在过的如何呢?人啊。还是要有点个性。没个性的人下场都挺惨的。
峰回路转,人那,这一辈子就是命,有时候把,真的是你也不知道结果会是什么样,23年在我百无聊赖,闲的五脊六兽的时候,一周的工作基本上半天就干完了,所以一个机缘巧合,遇见了一群有意思的人。当时大模型正在风口浪尖。好多人都在大模型里面摸金,而有这么一群人,一群大学生,在海外对我们进行大模型技术封锁的时候,为了自己的初衷,建立了在问这个网站。
而作为起步比别人要晚,产品做的还很粗糙如何跟市场上的竞品竞争呢?而且不收费,更不打广告,完全靠赞助存活。但是这一切都是为了在国外封锁我国大模型技术背景下的那句话“让知识无界,智能触手可及”。站长原文
所以在同类起步更早,产品做的更精细的很多产品逐渐倒下去以后,zaiwen还活着。所以我觉得特别有意思,这种产品活下来的概率是非常低的,除非整个团队都是为爱发电,于是我也加入到这个团队。
事实上也确实这样,整个团队是有极少部分社会工作者和大部分在校大学生组成,而大家聚一起做这件事的初衷就是为了让知识无国界,让国内用户可以更方便的体验最先进的海外大模型技术。而目标用户,也都是学生,老师和科研工作者。
就这样在这里,我重新找回了自己的目标,虽然,由于资金问题,资源问题,以及我个人身体限制能做的事情很少,但是却发现,大家都做的非常有动力,产品也在不断的迭代不断的发展,并且还活的很好。团队的人在这里也干的很开心。
今天,正是两年前我诊断出脑出血的那天,心里没有低落,也没有失望,更没有懊悔,有的只是新的体验。人生啊,来这一世,就是来体验的,别难为自己。顺势而为,就像张朝阳说的那句话“年轻人挺不容易的,建议年轻人不要过度努力,太过拼搏的话(对身体)是有伤害的,年轻人得面对现实,这个世界是不公平的”
来源:juejin.cn/post/7416168750364540940
自己没有价值之前,少去谈人情世故
昨天和几个网友在群里聊天,一个网友说最近公司辞退了一个人,原因就是太菜了,有一个功能是让从数据库随机查一条数据,他硬是把整个数据表的数据都查出来,然后从里面随机选一条数据。
另外的群友说,这人应该在公司的人情世故做得不咋滴,要是和自己组长,领导搞好关系,不至于被辞退。
发言人说:相反,这人的人情世故做得很到位,和别人相处得也挺好,说话又好听,大家都觉得他很不错!
但是这有用吗?
和自己的组长关系搞好了,难道他就能给你的愚蠢兜底?
这未免太天真,首先组长也是打工的,你以为和他关系好,他就能包庇你,容忍你不断犯错?
没有人会愿意冒着被举报的风险去帮助一个非亲非故的人,因为自己还要生活,老婆孩子还要等着用钱,包庇你,那么担风险的人就是他自己,他为何要这样做?
我们许多人总是觉得人情世故太重要了,甚至觉得比自己的能力重要,这其实是一个侮误区。
有这种想法的大多是刷垃圾短视频刷多了,没经历过社会的毒打,专门去学酒满敬人,茶满欺人。给领导敬酒杯子不能高过对方,最好直接跪下来……
那么人情世故重要吗?
重要,但是得分阶层,你一个打工的,领导连你名字都叫不出来,你见到他打声招呼,他都是用鼻子答应,你觉得你所谓的人情世故有意义吗?
你以为团建的时候跑上去敬酒,杯子直接低到他脚下,他就会看中你,为他挡酒他就觉得你这人可扶?未免电视看得太多。
人情世故有用的前提一定是建立在你有被利用的价值之上,你能漂漂亮亮做完一件事,问题又少,创造的价值又多,那么别人就会觉得你行,就会记住你,重视你,至于敬酒这些,不过是走个过场而已。
所以在自己没有价值之前,别去谈什么人情世故,安安心心提升自己。
前段时间一个大二的小妹妹叫我帮她运行一个项目,她也是为了课程蒙混过关,后面和她聊了几句,她叫我给她一点建议。
我直接给她说,你真正的去写了几行代码?看了几本书?做了多少笔记?你真正的写了代码,看了书,有啥疑问你再找我,而不是从我这里找简便方法,因为我也没有!
她说最烦学习了,完全不想学,自己还是去学人情世故了。
我瞬间破放了,对她说你才20岁不到,专业知识不好好学,就要去学人情世故了?你能用到人情世故吗?
你是怕以后去进厂自己人情世故不到位别人不要你?还是以后去ktv陪酒或者当营销学不会?这么早就做准备了?
她后面反驳我说:你看那些职场里面的女生不也是很懂人情世故吗,你为啥说没用,这些东西迟早都是要学的,我先做准备啊!
我当时就不想和她聊下去了,我知道又是垃圾短视频看多了,所以才会去想这些!以为自己不好好学习,毕业后只要人情世故做到位,就能像那些女职场秘书一样,陪着领导出去谈生意。
想啥呢!
当然,并不存在歧视别人的想法,因为我没有资格,只不过是觉得该学习的时间别去想一些没啥用的事情!
我们所能看到的那些把人情世故运用得炉火纯青,让人感觉很自然的人,别人肯定已经到了一定的段位,这是TA的职业需要。
而大多数人都是在底层干着街边老太太老大爷都能干的活,领导连你名字都叫不出来,可以用空气人来形容,你说人情世故有什么卵用吗?
这不就等于把自己弄得四不像吗?
当你真的有利用价值,能够给别人提供解决方案的时候,再来谈人情世故,那时候你不学,生活都会逼着你去学。
最后说一句,当你有价值的时候,人情世故是你别人学来用在你身上的,不信你回头去看一下自己的身边的人,哪怕是一个小学教师,都有人提着东西来找他办事,但是如果没有任何利用价值,哪怕TA把酒场上面的套路都运用得炉火纯青,也会成为别人的笑柄!
来源:juejin.cn/post/7352799449456738319
外行转码农,焦虑到躺平
下一篇《2024转行前端第6年》展示我躺平后捣鼓的东西
介绍自己
本人女,16年本科毕业,学的机械自动化专业,和大部分人一样,选专业的时候是拍大腿决定的。
恍恍惚惚度过大学四年,考研时心比天高选了本专业top5学校,考研失败,又不愿调剂,然后就参加校招大军。可能外貌+绩点优势,很顺利拿到了很多工厂offer,然后欢欢喜喜拖箱带桶进厂。
每天两点一线生活,住宿吃饭娱乐全在厂区,工资很低但是也没啥消费,住宿吃饭免费、四套厂服覆盖春夏秋冬。
我的岗位是 inplan软件维护 岗位,属于生产资料处理部门,在我来之前6年该岗位一直只有我师傅一个人,岗位主要是二次开发一款外购的软件,软件提供的api是基于perl语言,现在很少有人听过这个perl吧。该岗位可能是无数人眼里的神仙岗位吧,我在这呆了快两年,硬是没写过一段代码...
inplan软件维护 岗位的诞生就是我的师傅开创的,他原本只是负责生产资料处理,当大家只顾着用软件时,他翻到了说明书上的API一栏,然后写了一段代码,将大家每日手工一顿操作的事情用一个脚本解决了,此后更是停不下来,将部门各种excel数据处理也写成了脚本,引起了部门经理的注意,然后就设定了该岗位。
然而,将我一个对部门工作都不了解的新人丢在这个岗位,可想我的迷茫。开始半年师傅给我一本厚厚的《perl入门到精通》英文书籍,让我先学会 perl 语言。(ps:当时公司网络不连外网,而我也没有上网查资料的习惯,甚至那时候对电脑操作都不熟练...泪目)
师傅还是心地很善良很单纯的人,他隔一段时间会检查我的学习进度,然而当他激情澎拜给我讲着代码时,我竟控制不住打起了瞌睡,然后他就不管我了~~此后我便成了部门透明人物,要是一直透明下去就好了。我懒散的工作态度引起了部门主管的关注,于是我成了他重点关注的对象,我的工位更是移到了他身后~~这便是我的噩梦,一不小心神游时,主管的脸不知啥时凑到了我的电脑屏幕上~~~😱
偶然发现我的师傅在学习 php+html+css+js,他打算给部门构建一个网站,传统的脚本语言还是太简陋了。我在网上翻到了 w3scool离线文档 ,这一下子打开了我的 代码人生。后面我的师傅跳槽了,我在厂里呆了两年觉得什么都没学到,也考虑跳槽了。
后面的经历也很魔幻,误打误撞成为了一名前端开发工程师。此时是2018年,算是前端的鼎盛之年吧,各种新框架 vue/react/angular 都火起来了,各种网站/手机端应用如雨后春笋。我的前端之路还算顺利吧,下面讲讲我的经验吧
如何入门
对于外行转码农还是有一定成本的,省心的方式就是报班吧,但是个人觉得不省钱呀。培训班快则3个月,多的几年,不仅要交上万的培训费用,这段时间0收入,对于家境一般的同学,个人不建议报班。
但是现在市场环境不好,企业对你的容忍度不像之前那么高。之前几年行业缺人,身边很多只懂皮毛的人都可以进入,很多人在岗位半年也只能写出简单的页面,逻辑复杂一点就搞不定~~即使被裁了,也可以快速找到下家。这样的日子应该一去不复返了,所以我们还是要具备的实力,企业不是做慈善的,我们入职后还是要对的起自己的一份工资。
讲讲具体怎么入门吧
看视频:
b站上有很多很多免费的视频,空闲之余少刷点段子,去看看这些视频。不要问我看哪个,点击量大的就进去看看,看看过来人的经验,看看对这个行业的介绍。提高你的信息量,普通人的差距最大就在信息量的多少
还是看视频:
找一个系统的课程,系统的学习 html+css+js+vue/react,我们要动手写一些demo出来。可以找一些优秀的项目,自己先根据它的效果自己实现,但后对着源码看看自己的局限,去提升。
做笔记:
对于新人来说,就是看了视频感觉自己会了,但是写起来很是费力。为啥呢?因为你不知道也记不住有哪些api,所以我们在看视频学习中,有不知道的语法就记下来。
我之前的经验就是手动抄写,最初几年抄了8个笔记本,但是后面觉得不是很方便,因为笔记没有归纳,后续整理笔记困难,所以我们完全可以用电子档的形式,这方便后面的归纳修改。
嘿嘿,这里给大家推荐一下我的笔记 前端自检清单,这是我对我的笔记的总结,现在看来含金量不是很大,这些文章基本就copy总结别人的文章,很少有自己的思想,我更多是将它当成一个手册吧,我自己也经常遗忘一些API,所以时不时会去翻翻。
回顾:
我们的笔记做了就要经常的翻阅,温故而知新,经常翻阅我们的笔记,经常去总结,突然有一天你的思维就上升了一个高度。
- 慢慢你发现写代码就是不停调用api的过程
- 慢慢你会发现程序里的美感,一个设计模式、一种新思维。我身边很多人都曾经深深沉迷过写代码,那种成就感带来的心流,这是物质享受带来不了的
输出:
就是写文章啦,写文章让我们总结回顾知识点,发现知识的盲区,在这个过程中进行了深度思考。更重要的是,对于不严谨的同学来说,研究一个知识点很容易浅尝则止,写文章驱动自己去更深层系统挖掘。不管对于刚入行的还是资深人士,我觉得输出都是很重要的。
推荐大家去看神说要有光大神的文章 为什么我能坚持?因为写技术文章给我的太多了呀!,这时我最近很喜欢的一个大神,他的文章我觉得很有深度广度(ps:不是打广告呀,真心觉得受益了)。
持续提升
先谈谈学历歧视吧,现在很多大厂招聘基本条件就是211、985,对此很是无奈,但是我内心还是认可这种要求的,我对身边的本科985是由衷的佩服的。我觉得他们高考能考上985,身上都是有过人之处的,学习能力差不了。
见过很多工作多年的程序员,但是他们的编码能力无法描述,不管是逻辑能力、代码习惯、责任感都是很差的,写代码完全是应付式的,他们开发的代码如同屎山。额,但是我们也不要一味贬低他人,后面我也学会了尊重每一个人,每个人擅长的东西不一样,他可能不擅长写代码,但是可能他乐观的心态是很多人不及的、可能他十分擅长交际...
但是可能的话,我们还是要不断提高代码素养
- 广度:我们实践中,很多场景没遇到,但是我们要提前去了解,不要等需要用、出了问题才去研究。我们要具备一定的知识面覆盖,机会是给有准备的人的。
- 深度:对于现在面试动不动问源码的情况,很多人是深恶痛绝的,曾经我也是,但是当我沉下心去研究的时候,才发现这是有道理的。阅读源码不仅挺高知识的广度,更多让我们了解代码的美感
具体咋做呢,我觉得几下几点吧。(ps:我自己也做的不好,道理都懂,很难做到优秀呀~~~)
- 扩展广度:抽空多看看别人的文章,留意行业前沿技术。对于我们前端同学,我觉得对整个web开发的架构都要了解,后端同学的mvc/高并发/数据库调优啥的,运维同学的服务器/容器/流水线啥的都要有一定的了解,这样可以方便的与他们协作
- 提升深度:首先半路出家的同学,前几年不要松懈,计算机相关知识《操作系统》《计算机网络》《计算机组成原理》《数据结构》《编译原理》还是要恶补一下,这是最基础的。然后我们列出自己想要深入研究的知识点,比如vue/react源码、编译器、低代码、前端调试啥啥的,然后就沉下心去研究吧。
职业规划
现在整个大环境不好了,程序员行业亦是如此,身边很多人曾经的模式就是不停的卷,卷去大厂,跳一跳年薪涨50%不是梦,然而现在不同了。寒风凌凌,大家只想保住自己的饭碗(ps:不同层次情况不同呀,很多大厂的同学身边的同事还是整天打了鸡血一般)
曾经我满心只有工作,不停的卷,背面经刷算法。22年下半年市场明显冷下来,大厂面试机会都没有了,年过30,对大厂的执念慢慢放下。
我慢慢承认并接受了自己的平庸,然后慢慢意识到,工作只是生活的一部分。不一定要担任ceo,才算走上人生巅峰。最近几年,我爱上了读书,以前只觉得学理工科还是实用的,后面慢慢发现每个行业有它的美感~
最后引用最近的读书笔记结尾吧,大家好好体会一下论语的“知天命”一词,想通了就不容易焦虑了~~~
自由就是 坦然面对生活,看清了世界的真相依然热爱生活。宠辱不惊,闲看庭前花开花落。去留无意,漫随天外云卷云舒。
欢迎关注我的前端自检清单,我和你一起成长
来源:juejin.cn/post/7343138429860347945