内网开发提效指南
❝
工欲善其事必先利其器,使用过内网开发的小伙伴都知道,CV大法在内网基本就废了,查资料也是非常的不便。对于一名程序员来说,如果把搜索引擎和CV键给他ban了,遇到问题后那基本是寸步难行。今天给大家介绍几种帮助内网开发提效的方法,希望能够帮助到大家。
一、文档站点内网部署
可以把项目中所用技术和框架的文档部署到公司内网中。
以elementPlus为例:
1、访问gh-pages分支https://github.com/element-plus/element-plus/tree/gh-pages
,下载文档站源码。
2、将文档站部署到内网服务器(以nginx为例)。
server {
listen 9800;
server_name localhost;
location / {
root html/element-plus-gh-pages;
index index.html index.htm;
try_files $uri $uri/ /element-plus-gh-pages/index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
部署后的访问速度也是非常快的
使用这种方式,随着部署的站点增多,后续框架、文档更新的时候,维护起来相对是比较麻烦的。且只能查看文档,遇到问题需要求助度娘还是不太方便。
下面介绍两种物理外挂,可以直接访问外网。
二、USB跨屏穿越器数据线
个人感觉此方案的体验是最好的,缺点是需要两台电脑,并且需要花钱买一根线,价格在80-200之间。
购买
某宝、某鱼都有销售,我是在某宝85块买的。
使用
连接两台电脑的USB端口即可,会自动安装驱动,那根线实际上就相当于是一个文件中转器,可以实现剪切板、文件的互传。使用体验就跟一台电脑连接了两台显示器一样。如下图所示:
三、手机投屏
本文重点介绍此方案,因为可以白嫖且不需要第二台电脑。一部安卓手机+数据线即可,缺点是文件传输不太方便。它就是一个开源投屏项目scrcpy
。可以看到,此项目在github上拥有高达102k的star数量。
✨亮点
- 亮度 (原生,仅显示设备屏幕)
- 表演 (30~60fps)
- 质量 (1920×1080或以上)
- 低延迟 (70~100ms)
- 启动时间短 (显示第一张图像约1秒)
- 非侵入性 (设备上没有安装任何东西)
- 不需要 ROOT
- 有线无线都可连接
- 可以随便调整界面和码率
- 画面随意裁剪,自带录屏(手游直播利器)
- 支持多设备同时投屏
- 利用电脑的键盘和鼠标可以控制手机
- 把 APK 文件拖拽到电脑窗口即可安装应用到手机,把普通文件拖拽到窗口即可复制到手机
- 手机电脑共享剪贴板
- 自动检测USB连接的设备
- 可直接添加设备的局域网IP,达到无线控制的效果
- 将自动保存连接过的IP地址,下次输入时,自动提醒
- 支持设备别名
- 支持中英两种语言
- Tray menu
- 等等等...
安装
根据不同系统直接去release页面下载对应版本即可:github.com/Genymobile/…
使用
下载解压完,进入软件目录,点击下图按钮打开命令行界面,输入启动命令即可。
命令行输入scrcpy,按回车, 猿神,起洞!
启动之后,即可使用鼠标操作手机,非常的丝滑
1、手机复制文本到电脑
2、电脑复制文本到手机
可以看到,使用投屏的方式,也可以实现CV大法。并且可以使用手机端的外网搜索资料、解决问题等。以下是该项目的快捷键,熟练使用,即可达到人机合一的地步。
快捷键
操作 | 快捷键 | 快捷键 (macOS) | ||
---|---|---|---|---|
切换全屏模式 | Ctrl +f | Cmd +f | ||
将窗口调整为 1:1 | Ctrl +g | Cmd +g | ||
调整窗口大小以删除黑色边框 | Ctrl +x | 双击黑色背景 | Cmd +x | 双击黑色背景 |
设备HOME 键 | Ctrl +h | 鼠标中键 | Ctrl +h | 鼠标中键 |
设备BACK 键 | Ctrl +b | 鼠标右键 | Cmd +b | 鼠标右键 |
设备任务管理 键 | Ctrl +s | Cmd +s | ||
设备菜单 键 | Ctrl +m | Ctrl +m | ||
设备音量+ 键 | Ctrl +↑ | Cmd +↑ | ||
设备音量- 键 | Ctrl +↓ | Cmd +↓ | ||
设备电源 键 | Ctrl +p | Cmd +p | ||
点亮手机屏幕 | 鼠标右键 | 鼠标右键 | ||
关闭设备屏幕(保持镜像) | Ctrl +o | Cmd +o | ||
展开通知面板 | Ctrl +n | Cmd +n | ||
折叠通知面板 | Ctrl +Shift +n | Cmd +Shift +n | ||
将设备剪贴板中的内容复制到计算机 | Ctrl +c | Cmd +c | ||
将计算机剪贴板中的内容粘贴到设备 | Ctrl +v | Cmd +v | ||
将计算机剪贴板中的内容复制到设备 | Ctrl +Shift +v | Cmd +Shift +v | ||
安装APK | 将APK 文件拖入投屏 | 将APK 文件拖入投屏 | ||
传输文件到设备 | 将文件拖入投屏 | 将文件拖入投屏 | ||
启用/禁用FPS计数器(stdout) | Ctrl +i | Cmd +i |
使用小技巧
经过笔者几天的使用,总结出几个小技巧。
1、电脑键盘控制手机进行中文输入,必须使用正确的输入法组合。
手机端:讯飞输入法(搜狗输入法不支持)
电脑端:ENG(使用英文键盘)
2、手机熄屏状态下投屏。 在scrcpy命令后加上熄屏参数即可:scrcpy --turn-screen-off
这样就可以在手机熄屏的状态下,仍可以被电脑操作,达到节省电量和减轻发热的目的。
诸如此类的命令参数还有很多,执行scrcpy --help
就可查看详细的帮助文档。
衍生项目
因为开源的特性,scrcpy也衍生了一些相关项目,列举其中一些:
- QtScrcpy 使用qt重新实现的桌面端,并加强了对游戏的支持。
- scrcpy-gui 为scrcpy的命令行提供了gui界面。
- guiscrcpy 另一个scrcpy的gui界面。
- scrcpy-docker docker版本的scrcpy。
- scrcpy-go go语言版本的scrcpy,增强对游戏的支持。
总结
第二第三种方法虽然建立了内网开发电脑和外网设备的联系,但是是不会被公司的安全系统检测到一机双网的,因为其本质就类似于设计模式中的发布订阅模式,用数据线充当了中间人,两台设备之间方便传输数据了而已,不会涉及到联网。
内网开发的痛点,无非就是复制粘贴、文件传输不便,只要打通这个链路,就能解决此问题。以上三个方法,笔者在实际工作中都用到了,确实极大的提高了工作效率。如果你也在饱受内网开发的折磨,不妨试试这几个方法。
来源:juejin.cn/post/7362464700879716403
独立开发最重要的还是产品要打出差异化
独立开发者解放思维,开放眼界真的很有必要。就算自己一时没有好的 idea,也可以多观察学习一些(成功的)非主流的独立产品。我之前写文说过独立开发有死亡加速三件套:笔记、记账、todo。但是不得不说我还是保守了,俗话说的好:勇敢的人先享受世界。既然我单个产品都没优势,那我 all in one,把笔记、记账、待办都做到一个 app 里行不行?
直觉上我感觉这样不行吧?但是,啊?

原来真有人吃粉会点全家福啊。于是我想你做是能做但是用户就会喜欢吗?
我直接一个好家伙,两千多评价,4.7分!

嫉妒使我面目全非了。看了一下会员价格,买断28。这个 app 在没有苹果推荐的情况下,做到了两千多评价,我估计用户数大于 5 万。我看了一下上线时间,这个 app 前后开发持续了一年多的时间。如果我按照付费转化10%算,那么这个 app 扣掉 15% 苹果渠道费后收入大概有 12 万(50000 * 10% * 28 * 0.85)。但是这个 app 如果运营得当,后面还是会有新用户进来。因为app是极简(没有设计师的委婉说法),设计成本几乎没有。以当下的数据看,算是回本了。后面再来的增长都是利润了。
定位分析:笔记、待办、记账是很主流的用户需求,但是也有很多入门用户都只有很轻度的需求,垂直的 app 都太专业了,如果我想要这些数据在一起,很轻量,没有广告,设计简洁,又一次买断,那么市场上就没有对应的产品。
完成这个app 有两个难点:首先你要判断市场有这么一块空白,第二个难点,因为任何一个单点都没有优势,怎样触达目标用户。
我看了作者的小红书,他选择了一个很新颖的切入点:学生群体。他有一个宣传口号是“专注学生自律养成的工具 app”。学生群体不会有特别重度的需求,又集中,因为付费转化有限所以垂类专业app都不太考虑学生群体,在这个用户定位这个app又找到了独特的切入点。

这个定位有多重要呢,这个帖子带来的用户下载量就能达到 10000。
总的来说,这个 app 给我带来了很大的震撼。希望也能给大家带来启发。
来源:juejin.cn/post/7267409589066563642
一个高并发项目到落地的心酸路
前言
最近闲来没事,一直在掘金上摸鱼,看了不少高并发相关的文章,突然有感而发,想到了几年前做的一个项目,也多少和高并发有点关系。
这里我一边回忆落地细节一边和大家分享下,兴许能给大家带来点灵感。
正文
需求及背景
先来介绍下需求,首先项目是一个志愿填报系统。
核心功能是两块,一是给考试填报志愿,二是给老师维护考生数据。
本来这个项目不是我们负责,奈何去年公司负责这个项目的组遭到了甲方严重的投诉,说很多考生用起来卡顿,甚至把没填上志愿的责任归到系统上。
甲方明确要求,如果这年再出现这种情况,公司在该省的所有项目将面临被替换的风险。
讨论来讨论去,最后公司将任务落到我们头上时,已经是几个月后的事了,到临危受命阶段,剩下不到半年时间。
虽然直属领导让我们不要有心理负担,做好了表扬,做不好锅也不是我们的,但明显感觉到得到他的压力,毕竟一个不小心就能上新闻。
分析
既然开始做了,再说那些有的没的就没用了,直接开始分析需求。
首先,业务逻辑并不算复杂,难点是在并发和数据准确性上。与客户沟通后,大致了解了并发要求后,于是梳理了下。
- 考生端登录接口、考生志愿信息查询接口需要4W QPS
- 考生保存志愿接口,需要2W TPS
- 报考信息查询4W QPS
- 老师端需要4k QPS
- 导入等接口没限制,可以异步处理,只要保证将全部信息更新一遍在20分钟以内即可,同时故障恢复的时间必须在20分钟以内(硬性要求)
- 考生端数据要求绝对精准,不能出现遗漏、错误等和考生操作不一致的数据
- 数据脱敏,防伪
- 资源是有限的,提供几台物理机
大的要求就这么多,主要是在有限资源下需要达到如此高的并发确实需要思考思考,一般的crud根本达不到要求。
方案研讨
接下来我会从当时我们切入问题的点开始,从前期设计到项目落地整个过程的问题及思考,一步步去展示这个项目如何实现的
首先,我们没有去设计表,没有去设计接口,而是先去测试。测试什么?测试我们需要用到或可能用到的中间件是否满足需求
MySQL
首先是MySQL,单节点MySQL测试它的读和取性能,新建一张user表。
向里面并发插入数据和查询数据,得到的TPS大概在5k,QPS大概在1.2W。
查询的时候是带id查询,索引列的查询不及id查询,差距大概在1k。
insert和update存在细微并发差距,但基本可以忽略,影响更新性能目前最大的问题是索引。
如果表中带索引,将降低1k-1.5k的TPS。
目前结论是,mysql不能达到要求,能不能考虑其他架构,比如mysql主从复制,写和读分开。
测试后,还是放弃,主从复制结构会影响更新,大概下降几百,而且单写的TPS也不能达到要求。
至此结论是,mysql直接上的方案肯定是不可行的
Redis
既然MySQL直接查询和写入不满足要求,自然而然想到加入redis缓存。于是开始测试缓存,也从单节点redis开始测试。
get指令QPS达到了惊人的10w,set指令TPS也有8W,意料之中也惊喜了下,仿佛看到了曙光。
但是,redis容易丢失数据,需要考虑高可用方案
实现方案
既然redis满足要求,那么数据全从redis取,持久化仍然交给mysql,写库的时候先发消息,再异步写入数据库。
最后大体就是redis + rocketMQ + mysql的方案。看上去似乎挺简单,当时我们也这样以为 ,但是实际情况却是,我们过于天真了。
这里主要以最重要也是要求最高的保存志愿信息接口开始攻略
故障恢复
第一个想到的是,这些个节点挂了怎么办?
mysql挂了比较简单,他自己的机制就决定了他即使挂掉,重启后仍能恢复数据,这个可以不考虑。
rocketMQ一般情况下挂掉了可能会丢失数据,经过测试发现,在高并发下,确实存在丢消息的现象。
原因是它为了更加高效,默认采用的是异步落盘的模式,这里为了保证消息的绝对不丢失,修改成同步落盘模式。
然后是最关键的redis,不管哪种模式,redis在高并发下挂掉,都会存在丢失数据的风险。
数据丢失对于这个项目格外致命,优先级甚至高于并发的要求。
于是,问题难点来到了如何保证redis数据正确,讨论过后,决定开启redis事务。
保存接口的流程就变成了以下步骤:
1.redis 开启事务,更新redis数据
2.rocketMQ同步落盘
3.redis 提交事务
4.mysql异步入库
我们来看下这个接口可能存在的问题。
第一步,如果redis开始事务或更新redis数据失败,页面报错,对于数据正确性没有影响
第二步,如果rocketMQ落盘报错,那么就会有两种情况。
情况一,落盘失败,消息发送失败,好像没什么影响,直接报错就可。
情况二,如果发送消息成功,但提示发送失败(无论什么原因),这时候将导致mysql和redis数据的最终不一致。
如何处理?怎么知道是redis的有问题还是mysql的有问题?出现这种情况时,如果考生不继续操作,那么这条错误的数据必定无法被更新正确。
考虑到这个问题,我们决定引入一个时间戳字段,同时启动一个定时任务,比较mysql和redis不一致的情况,并自主修复数据。
首先,redis中记录时间戳,同时在消息中也带上这个时间戳并在入库时记录到表中。
然后,定时任务30分钟执行一次,比较redis中的时间戳是否小于mysql,如果小于,便更新redis中数据。如果大于,则不做处理。
同时,这里再做一层优化,凌晨的时候执行一个定时任务,比较redis中时间戳大于mysql中的时间戳,连续两天这条数据都存在且没有更新操作,将提示给我们手动运维。
然后是第三步,消息提交成功但是redis事务提交失败,和第二步处理结果一致,将被第二个定时任务处理。
这样看下来,即使redis崩掉,也不会丢失数据。
第一轮压测
接口实现后,当时怀着期待,信息满满的去做了压测,结果也是当头棒喝。
首先,数据准确性确实没有问题,不管突然kill掉哪个环节,都能保证数据最终一致性。
但是,TPS却只有4k不到的样子,难道是节点少了?
于是多加了几个节点,但是仍然没有什么起色。问题还是想简单了。
重新分析
经过这次压测,之后一个关键的问题被提了出来,影响接口TPS的到底是什么???
一番讨论过后,第一个结论是:一个接口的响应时间,取决于它最慢的响应时间累加,我们需要知道,这个接口到底慢在哪一步或哪几步?
于是用arthas看了看到底慢在哪里?
结果却是,最慢的竟然是redis修改数据这一步!这和测试的时候完全不一样。于是针对这一步,我们又继续深入探讨。
结论是:
redis本身是一个很优秀的中间件,并发也确实可以,选型时的测试没有问题。
问题出在IO上,我们是将考生的信息用json字符串存储到redis中的(为什么不保存成其他数据结构,因为我们提前测试过几种可用的数据结构,发现redis保存json字符串这种性能是最高的),
而考生数据虽然单条大小不算大,但是在高并发下的上行带宽却是被打满的。
于是针对这种情况,我们在保存到redis前,用gzip压缩字符串后保存到redis中。
为什么使用gzip压缩方式,因为我们的志愿信息是一个数组,很多重复的数据其实都是字段名称,gzip和其他几个压缩算法比较后,综合考虑到压缩率和性能,在当时选择了这种压缩算法。
针对超过限制的字符串,我们同时会将其拆封成多个(实际没有超过三个的)key存储。
继续压测
又一轮压测下来,效果很不错,TPS从4k来到了8k。不错不错,但是远远不够啊,目标2W,还没到它的一半。
节点不够?加了几个节点,有效果,但不多,最终过不了1W。
继续深入分析,它慢在哪?最后发现卡在了rocketMQ同步落盘上。
同步落盘效率太低?于是压测一波发现,确实如此。
因为同步落盘无论怎么走,都会卡在rocketMQ写磁盘的地方,而且因为前面已经对字符串压缩,也没有带宽问题。
问题到这突然停滞,不知道怎么处理rocketMQ这个点。
同时,另一个同事在测试查询接口时也带来了噩耗,查询接口在1W2左右的地方就上不去了,原因还是卡在带宽上,即使压缩了字符串,带宽仍被打满。
怎么办?考虑许久,最后决定采用较常规的处理方式,那就是数据分区,既然单个rocketMQ服务性能不达标,那么就水平扩展,多增加几个rocketMQ。
不同考生访问的MQ不一样,同时redis也可以数据分区,幸运的是正好redis有哈希槽的架构支持这种方式。
而剩下的问题就是如何解决考生分区的方式,开始考虑的是根据id进行求余的分区,但后来发现这种分区方式数据分布及其不均匀。
后来稍作改变,根据正件号后几位取余分区,数据分布才较为均匀。有了大体解决思路,一顿操作后继续开始压测。
一点小意外
压测之后,结果再次不如人意,TPS和QPS双双不增反降,继续通过arthas排查。
最后发现,redis哈希槽访问时会在主节点先计算key的槽位,而后再将请求转到对应的节点上访问,这个计算过程竟然让性能下降了20%-30%。
于是重新修改代码,在java内存中先计算出哈希槽位,再直接访问对应槽位的redis。如此重新压测,QPS达到了惊人的2W,TPS也有1W2左右。
不错不错,但是也只到了2W,在想上去,又有了瓶颈。
不过这次有了不少经验,马上便发现了问题所在,问题来到了nginx,仍然是一样的问题,带宽!
既然知道原因,解决起来也比较方便,我们将唯一有大带宽的物理机上放上两个节点nginx,通过vip代理出去,访问时会根据考生分区信息访问不同的地址。
压测
已经记不清第几轮压测了,不过这次的结果还算满意,主要查询接口QPS已经来到了惊人的4W,甚至个别接口来到6W甚至更高。
胜利已经在眼前,唯一的问题是,TPS上去不了,最高1W4就跑不动了。
什么原因呢?查了每台redis主要性能指标,发现并没有达到redis的性能瓶颈(上行带宽在65%,cpu使用率也只有50%左右)。
MQ呢?MQ也是一样的情况,那出问题的大概率就是java服务了。分析一波后发现,cpu基本跑到了100%,原来每个节点的最大链接数基本占满,但带宽竟然还有剩余。
静下心来继续深入探讨,连接数为什么会满了?原因是当时使用的SpringBoot的内置容器tomcat,无论如何配置,最大连接数最大同时也就支持1k多点。
那么很简单的公式就能出来,如果一次请求的响应时间在100ms,那么1000 * 1000 / 100 = 10000。
也就是说单节点最大支持的并发也就1W,而现在我们保存的接口响应时间却有300ms,那么最大并发也就是3k多,目前4个分区,看来1W4这个TPS也好像找到了出处了。
接下来就是优化接口响应时间的环节,基本是一步一步走,把能优化的都优化了一遍,最后总算把响应时间控制在了100ms以内。
那么照理来说,现在的TPS应该会来到惊人的4W才对。
再再次压测
怀着忐忑又激动的心情,再一次进入压测环节,于是,TPS竟然来到了惊人的2W5。
当时真心激动了一把,但是冷静之后却也奇怪,按正常逻辑,这里的TPS应该能达到3W6才对。
为了找到哪里还有未发现的坑(怕上线后来惊喜),我们又进一步做了分析,最后在日志上找到了些许端倪。
个别请求在链接redis时报了链接超时,存在0.01%的接口响应时间高于平均值。
于是我们将目光投向了redis连接数上,继续一轮监控,最终在业务实现上找到了答案。
一次保存志愿的接口需要执行5次redis操作,分别是获取锁、获取考生信息、获取志愿信息、修改志愿信息、删除锁,同时还有redis的事务。
而与之相比,查询接口只处理了两次操作,所以对于一次保存志愿的操作来看,单节点的redis最多支持6k多的并发。
为了验证这个观点,我们尝试将redis事务和加锁操作去掉,做对照组压测,发现并发确实如预期的一样有所提升(其实还担心一点,就是抢锁超时)。
准备收工
至此,好像项目的高并发需求都已完成,其他的就是完善完善细节即可。
于是又又又一次迎来了压测,这一次不负众望,重要的两个接口均达到了预期。
这之后便开始真正进入业务实现环节,待整个功能完成,在历时一个半月带两周的加班后,终于迎来了提测。
提测后的问题
功能提测后,第一个问题又又又出现在了redis,当高并发下突然kill掉redis其中一个节点。
因为用的是哈希槽的方式,如果挂掉一个节点,在恢复时重新算槽将非常麻烦且效率很低,如果不恢复,那么将严重影响并发。
于是经过讨论之后,决定将redis也进行手动分区,分区逻辑与MQ的一致。
但是如此做,对管理端就带来了一定影响,因为管理端是列表查询,所以管理端获取数据需要从多个节点的redis中同时获取。
于是管理端单独写了一套获取数据分区的调度逻辑。
第二个问题是管理端接口的性能问题,虽然管理端的要求没考生端高,但扛不住他是分页啊,一次查10个,而且还需要拼接各种数据。
不过有了前面的经验,很快就知道问题出在了哪里,关键还是redis的连接数上,为了降低链接数,这里采用了pipeline拼接多个指令。
上线
一切准备就绪后,就准备开始上线。说一下应用布置情况,8+4+1+2个节点的java服务,其中8个节点考生端,4个管理端,1个定时任务,2个消费者服务。
3个ng,4个考生端,1个管理端。
4个RocketMQ。
4个redis。
2个mysql服务,一主一从,一个定时任务服务。
1个ES服务。
最后顺利上线,虽然发生了个别线上问题,但总体有惊无险,
而真是反馈的并发数也远没有到达我们的系统极限,开始准备的水平扩展方案也没有用上,无数次预演过各个节点的宕机和增加分区,一般在10分钟内恢复系统,不过好在没有排上用场。
最后
整个项目做下来感觉越来越偏离面试中的高并发模式,说实在的也是无赖之举,
偏离的主要原因我认为是项目对数据准确性的要求更高,同时需要完成高并发的要求。
但是经过这个项目的洗礼,在其中也收获颇丰,懂得了去监控服务性能指标,然后也加深了中间件和各种技术的理解。
做完之后虽然累,但也很开心,毕竟在有限的资源下去分析性能瓶颈并完成项目要求后,还是挺有成就感的。
再说点题外话,虽然项目成功挽回了公司在该省的形象,也受到了总公司和领导表扬,但最后也就这样了,
实质性的东西一点没有,这也是我离开这家公司的主要原由。不过事后回想,这段经历确实让人难忘,也给我后来的工作带来了很大的帮助。
从以前的crud,变得能去解决接口性能问题。这之前一遇上,可能两眼茫然或是碰运气,现在慢慢的会根据蛛丝马迹去探究优化方案。
不知道我在这个项目的经历是否能引起大家共鸣?希望这篇文章能对你有所帮助。
来源:juejin.cn/post/7346021356679675967
【年终总结】置顶帖 我们的2023 (• ᴗ •͈)◞︎ᶫᵒᵛᵉ ♡ & 订婚快乐
前言
想着给自己的博客每年一个置顶帖子,于是便有了此文。
1. 博主的自我介绍
我是一名Java开发工作者,20年大学毕业,专业:计算机科学与技术。
当年高考填报志愿的时候,计算机是一个新兴的蓬勃发展的行业,当时选择这个专业的原因时:一是想着毕业之后进入到这个行业从事相关工作,二是想着这个专业在公务员报考中能报考的岗位也很多。但是大四规划未来人生方向的时候,市场给我们开了一个大大的玩笑,互联网行业乱成了一锅粥,有一句话叫做万物皆可转码,再加之培训班疯狂的向社会输送程序员,市场乱象频频发生,培训班出来的新人能把自己包装成4年5年工作经验,然后各个公司为了能降低用人成本,也为了节省培养新人的时间和开支,也是只招聘工作经验大于3、4年以上的程序员,导致市场对于应届生的接收程度极差,都不愿意招聘应届生,班里的很多学生该培训的培训,该考研考公的备考,该转行的转行....
这个时期能走的路只有三条
- 考研 毕业成为大厂码农 (幻想的)
- 考公
- 和现在就成为码农。
考研呢,也考了,大四下半学期备战了半年,怎么说呢,结果不意外,对自己的认知有偏差,如果能报考一个相对稳妥的双非本科,现在说必定也是一个失业的研究生呢,但是收获最少我把计算机四大件(数据结构、计算机网络、操作系统、计算机组成原理)背了个底朝天,也算是在将近毕业的时候努力的学习了一把大学专业基础的知识。
考公呢,那时候我莫名觉得考公绝对不行,当时运筹帷幄,指点江山,告知自己,现在体制内的各种发生的事情和情况导致未来的公务员绝对不是铁饭碗,加之公务员工资毕竟不像成为程序员肉眼可见的工资那么可观,于是果断的放弃了考公务员,真的,现在有巴掌我第一个扇回去。当时可是20年呀,公务员真的不是很卷,你看看现在,后悔 ....
考研失败之后还是找了个互联网的厂子,现在从事这个行业也有三四年时间了,但是行业的前景却并不像七八年前那样的好。
2、2023年
2023年对我来说绝对是人生最重要的一年之一。
上半年是我老婆人生中最重要的时期之一,她面临着毕业压力(一度让她面临抑郁和崩溃,可能你们会说不就是毕业嘛,哪有那么夸张,我只能说每个人的情况不一样,希望你们一辈子也不要遇见像她导师那样的人...)。
正在外边商场吃饭,然后接到导师电话开始开临时组会。
我们如履薄冰,战战兢兢度过了这段时间。终于,在六月份,她顺利毕业了(万幸)。
(顺利毕业)
(surprise)
七月,她也找到了人生中的第一份工作,我们的生活开始步入正轨。
同样七月终于拥有了人生第一辆车。
(开心)
在家人和我们自己的决定下,我们如期在十月订婚了。这对我们来说是非常重要的一步。
(去银行买的金条,打的金镯子,很划算哈 ~)
(自己买的订婚现场的装饰,pdd 买的,至今为止,我还是觉得我布置的最好看。)
(怎么样呀?jym)
(家人们,教你们一招 五粮液、软华子直接拿下老丈人)
(订婚现场,主持人是我一个妹妹,这张图能看清我桌子上都摆了啥,当天我没拍桌子。)
(订婚书一签,你可不就是我的人了 ~)
(直接拿下)
(小样,还想跑)
(往后余生,我们一起走)
本以为人生会一帆风顺地继续下去,但工作没多久,因为是制造业,厂里的有毒气体使得她呼吸道和身体都在变差,她不得不选择辞职(裸辞)。23年的社会结构变迁使得学历贬值,大学生人数的激增导致工作变得更加稀缺,失业潮开始席卷。
老婆从失业到现在一直住在我租的小房间里,但是我相信人生不可能总是一帆风顺的。
3、技术成长
除了完成基本的工作业务(今年也上线了 3个web网站 和 3个小程序)之外的技术栈扩充:
- 学习了 SpringBoot3.0 和 对应的 Spring 的生态。比如 SpringSecurity 6.0 以上 ...
- 还有 Java 17 的一些新特性和性能。
- 学习了 C# 和 .Net (只能说和 Java 很像,补全版的Java),正在用C# 做了一个对接 OpenAI的一个类似 ChatGpt 一样的对话类的网站。(还不是很熟,但是能写了,面向百度编程)
- 学习了 领域驱动 DDD ,真的感觉要长脑子了,很多系统性的东西在慢慢融合。
- 接触了 Vue3 和 TypeScript (嗯,万物都在往面向对象发展 ~)
- 可以从 需求制定,域名、服务器、云服务 产品的选型,技术选型,前后端开发,部署上线,性能优化,中间件搭建,后期的运维测试。都能自己一个人搞下来了。(还是勉勉强强,但是按照我们现在这种模式的话,稍微简单一点的服务还是没啥问题的、单体服务就更没问题了。没错,我就是小公司全能手)
- 现在 Python 和 Shell 脚本 玩得 6 的一 。(脚本真方便,而且这种解释器语言真好用~)
4、个人成长
这一年中,有一丝丝的人生感悟,分享给大家。
在工作上,大家都知道,写代码到一定程度会有质的飞跃。今年我在工作方面有了一些感悟,但随着对知识的深入,我也意识到自己所不知道的更多。我从最基础的 Java、数据库和 Web 开发开始,逐渐深入研究技术,包括 Java、系统设计和架构、各种数据库进阶以及项目管理,然后扩展到安全与性能优化、沟通能力、业务和行业理解,以及技术趋势和新技术。然而,最近我意识到,一切都归结于一个道理:先做人,再做事。懂得做人,任何问题都能迎刃而解。我对此有些许领悟。
在工作中,技术的学习和提升固然至关重要,但随着不断的实践,我逐渐意识到成功并不仅取决于技术能力,更关键的是人格魅力和人际交往能力。我开始认识到要从意识开始转变。
从最开始的技术基础学习,到技术的深入研究,再到全面提升,这是职业发展的必经之路。但是当在职场中积累了一定经验后,我渐渐明白技术只是成功的一部分。在实际工作中,需要与人打交道、管理团队、协调资源,这些都需要更多的人际关系、沟通技巧和领导力。虽然我现在还在追求进步,但这是必经之路。
要学会做人,在职场上就意味着有坚定的原则和良好的职业道德。这意味着以诚实、正直、宽容的态度对待他人,学会尊重和理解他人的不同,善于倾听,愿意分享和帮助他人。
此外,学会做人也包括与同事的良好合作、与上级的有效沟通、与下属的关心和激励。建立良好的人际关系,培养团队合作精神。
我曾听过这样一段话,我们这一代,因为互联网的高速发展,能轻易接触到信息,就以为自己掌握了很多,但这个世界上真正有价值的东西都需付出等价的代价。先学会做人,再学做事。
5、总结与展望
新的一年:
- 常回家看看,暂时定的两个月最少回去一次吧。
- 好好给老婆准备明年春招。
- 好好写一些博客,将自己的技术分享出来。将自己的博客好好运营一下。
- 好好吃饭、锻炼,我实在是太瘦了,身体是革命的本钱。
- 交易,看看大A水有多深。兄弟们 2700 抄底了~
- 攒点钱,再去一次 汉中-留坝县自驾游。
- 抽时间把婚纱照拍了~
什么都会过去的,抓住现在,不是吗?
(我的头像,在汉中狮子沟牧场拍的)
(开心最重要,不是吗?)
(去年全球总决赛[ wbg vs t1]的时候线下场馆拍的,笑死 ~)
(前几天下雪 恐龙 vs 鸭子)
就这些,希望明年年度总结的时候可以将这些都完成 ~
引用《人世间》种的一句话:
从你出生的那一刻起, 端什么碗,吃什么饭, 经历什么事,什么时候和谁结婚, 都是定数。 别太难为自己,顺其自然就好。 人生的剧本, 你早在天堂就看过了, 你之所以选择这个剧本是因为, 这一生中有你认为值得的地方。
jym 加油 !
来源:juejin.cn/post/7310101033635725375
UI: 为啥你这个页面边框1px看起来这么粗?
背景
最近在开发H5,ui稿给的border:1px solid,因为ui稿上的宽度是750px,我们运行的页面宽度是375px,所以我们需要把所以尺寸/2。所以我们可能会想写border:0.5px solid。但是实际上,我们看页面渲染,仍然是渲染1px而不是0.5
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
}
.flex {
display: flex;
}
.item {
margin-right: 10px;
padding: 10px;
font-size: 13px;
line-height: 1;
background-color: rgba(242, 243, 245,1);
}
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
border: 0.5px solid ;
}
</style>
</head>
<body>
<div class="flex">
<!-- <div class="item active">
active
</div> -->
<div class="item">
item1
</div>
<div class="item">
item2
</div>
<div class="item">
item3
</div>
</div>
</body>
</html>
在没active的情况下
他们的内容都是占13px
在有active的情况下
active占了14px这个是没问题的,因为它font-size是14px嘛,但是我们是设置了border的宽度是0.5px,但展现的却是1px。
再来看看item
它内容占了16px,它受到相邻元素影响是14px+2px的上下边框
为啥border是1px呢
在 CSS 中,边框可以设置为 0.5px,但在某些情况下,尤其是低分辨率的屏幕上,浏览器可能会将其渲染为 1px 或根本不显示。这是因为某些浏览器和显示设备不支持小于 1px 的边框宽度或不能准确渲染出这样的细小边框。
浏览器渲染机制
- 不同浏览器对于小数像素的处理方式不同。一些浏览器可能会将
0.5px
边框四舍五入为1px
,以确保在所有设备上的一致性。
设备像素比
- 在高 DPI(如 Retina 显示器)设备上,
0.5px
边框可能看起来更清晰,因为这些设备可以渲染更细的边框。 - 在低 DPI 设备上,
0.5px
边框可能会被放大或者根本不会被显示。
解决办法
方法一:使用伪类和定位
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
position: relative;
}
.active::after {
content: "";
pointer-events: none;
display: block;
position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;
border: 1px #ff892e solid;
box-sizing: border-box;
width: 100%;
height: 100%;
}
另外的item的内容高度也是14px了符合要求
方法二:使用阴影,使用F12看的时候感觉还是有些问题
.active2 {
margin-left: 10px;
color: rgba(250, 100, 0, 1);
font-size: 14px;
position: relative;
box-shadow: 0 0 0 0.5px #ff892e;
}
方法三:使用svg,但这种自己设置了宽度。
<div class="active">
<svg width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
<rect x="0" y="0" width="100" height="100" fill="none" stroke="#ff892e" stroke-width="0.5"></rect>
</svg>
active
</div>
方案四:使用svg加定位,也比较麻烦,而且有其他的问题
<div class="active">
<svg viewBox="0 0 100 100" preserveAspectRatio="none">
<rect x="0" y="0" width="100" height="100" fill="none" stroke="#ff892e" stroke-width="0.5"></rect>
</svg>
<div class="content">active</div>
</div>
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
position: relative;
display: inline-block;
}
.active svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
box-sizing: border-box;
}
.active .content {
position: relative;
z-index: 1;
}
方法五:使用一个父元素 比较麻烦
<div class="border-container">
<div class="active">active</div>
</div>
.border-container {
display: inline-block;
padding: 0.5px;
background-color: #ff892e;
}
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
background-color: white;
}
最后
在公司里,我们使用的都是方案一,这样active和item它们的内容高度都是14px了。然后我们再给他们的父盒子加上 align-items: center。这样active的高度是14px,其他都是13px了。但是active的高度会比其他item的盒子高1px,具体看个人需求是否添加吧。
来源:juejin.cn/post/7393656776539963407
基于英雄联盟人物的加载动画,奇怪的需求又增加了!
1、背景
前两天老板找到我说有一个需求,要求使用英雄联盟的人物动画制作一个加载中的组件,类似于下面这样:
我定眼一看:这个可以实现,但是需要UI妹子给切图。
老板:UI? 咱们啥时候招的UI !
我:老板,那不中呀,不切图弄不成呀。
老板:下个月绩效给你A。
我:那中,管管管。
2、调研
发动我聪明的秃头,实现这个需求有以下几种方案:
- 切动画帧,没有UI不中❎。
- 去lol客户端看看能不能搞到什么美术素材,3D模型啥的,可能行❓
- 问下 gpt4o,有没有哪个老表收集的有lol英雄的美术素材,如果有那就更得劲了✅。
经过我一番搜索,发现了这个网站:model-viewer,收集了很多英雄联盟的人物模型,模型里面还有各种动画,还给下载。老表,这个需求稳了50%了!
接下来有几种选择:
- 将模型动画转成动画帧,搞成雪碧图,较为麻烦,且动画不支持切换。
- 直接加载模型,将模型放在进度条上,较为简单,支持切换不同动画,而且可以自由过渡。就是模型文件有点大,初始化加载可能耗时较长。但是后续缓存一下就好了。
聪明的我肯定先选第二个方案呀,你糊弄我啊,我糊弄你。
3、实现
web中加载模型可以使用谷歌基于threejs
封装的 model-viewer
, 使用现代的 web component 技术。简单易用。
先初始化一个vue工程
npm create vue@latest
然后将里面的初始化的组件和app.vue里面的内容都删除。
安装model-viewer
依赖:
npm i three // 前置依赖
npm i @google/model-viewer
修改vite.config.js
,将model-viewer
视为自定义元素,不进行编译
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
// 添加以下内容
compilerOptions: {
isCustomElement: (tag) => ['model-viewer'].includes(tag)
}
}
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
assetsInclude: ['./src/assets/heros/*.glb']
})
新建 src/components/LolProgress.vue
<template>
<div class="progress-container">
<model-viewer
:src="hero.src"
disable-zoom
shadow-intensity="1"
:camera-orbit="hero.cameraOrbit"
class="model-viewer"
:style="heroPosition"
:animation-name="animationName"
:camera-target="hero.cameraTarget"
autoplay
ref="modelViewer"
></model-viewer>
<div
class="progress-bar"
:style="{ height: strokeWidth + 'px', borderRadius: strokeWidth / 2 + 'px' }"
>
<div class="progress-percent" :style="currentPercentStyle"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch, type PropType } from 'vue'
/** 类型 */
interface Hero {
src: string
cameraOrbit: string
progressAnimation: string
finishAnimation: string
finishAnimationIn: string
cameraTarget: string
finishDelay: number
}
type HeroName = 'yasuo' | 'yi'
type Heros = {
[key in HeroName]: Hero
}
const props = defineProps({
hero: {
type: String as PropType<HeroName>,
default: 'yasuo'
},
percentage: {
type: Number,
default: 100
},
strokeWidth: {
type: Number,
default: 10
},
heroSize: {
type: Number,
default: 150
}
})
const modelViewer = ref(null)
const heros: Heros = {
yasuo: {
src: '/src/components/yasuo.glb',
cameraOrbit: '-90deg 90deg',
progressAnimation: 'Run2',
finishAnimationIn: 'yasuo_skin02_dance_in',
finishAnimation: 'yasuo_skin02_dance_loop',
cameraTarget: 'auto auto 0m',
finishDelay: 2000
},
yi: {
src: '/src/components/yi.glb',
cameraOrbit: '-90deg 90deg',
progressAnimation: 'Run',
finishAnimationIn: 'Dance',
finishAnimation: 'Dance',
cameraTarget: 'auto auto 0m',
finishDelay: 500
}
}
const heroPosition = computed(() => {
const percentage = props.percentage > 100 ? 100 : props.percentage
return {
left: `calc(${percentage + '%'} - ${props.heroSize / 2}px)`,
bottom: -props.heroSize / 10 + 'px',
height: props.heroSize + 'px',
width: props.heroSize + 'px'
}
})
const currentPercentStyle = computed(() => {
const percentage = props.percentage > 100 ? 100 : props.percentage
return { borderRadius: `calc(${props.strokeWidth / 2}px - 1px)`, width: percentage + '%' }
})
const hero = computed(() => {
return heros[props.hero]
})
const animationName = ref('')
watch(
() => props.percentage,
(percentage) => {
if (percentage < 100) {
animationName.value = hero.value.progressAnimation
} else if (percentage === 100) {
animationName.value = hero.value.finishAnimationIn
setTimeout(() => {
animationName.value = hero.value.finishAnimation
}, hero.value.finishDelay)
}
}
)
onMounted(() => {
setTimeout(() => {
console.log(modelViewer.value.availableAnimations)
}, 2000)
})
</script>
<style scoped>
.progress-container {
position: relative;
width: 100%;
}
.model-viewer {
position: relative;
background: transparent;
}
.progress-bar {
border: 1px solid #fff;
background-color: #666;
width: 100%;
}
.progress-percent {
background-color: aqua;
height: 100%;
transition: width 100ms ease;
}
</style>
组件非常简单,核心逻辑如下:
- 根据传入的英雄名称加载模型
- 指定每个英雄的加载中的动画,
- 加载100%,切换完成动作进入动画和完成动画即可。
- 额外的细节处理。
最后修改
app.vue
:
<script setup lang="ts">
import { ref } from 'vue'
import LolProgress from './components/LolProgress.vue'
const percentage = ref(0)
setInterval(() => {
percentage.value = percentage.value + 1
}, 100)
</script>
<template>
<main>
<LolProgress
:style="{ width: '200px' }"
:percentage="percentage"
:heroSize="200"
hero="yasuo"
/>
</main>
</template>
<style scoped></style>
这不就完成了吗,先拿给老板看看。
老板:换个女枪的看看。
我:好嘞。
老板:弄类不赖啊小伙,换个俄洛伊的看看。
4、总结
通过本次需求,了解到了 model-viewer
组件。
老板招个UI妹子吧。
在线体验:github-pages
来源:juejin.cn/post/7377217883305279526
做了这么久前端还不会手写瀑布流?(H5 & 小程序)
前言
做了7年前端我一直不知道瀑布流是什么(怪设计师不争气啊,哈哈哈),我一直以为就是个普通列表,几行css解决的那种。
当然瀑布流确实有css解决方案,但是这个方案对于分页列表来说完全不能用,第二页内容一出来位置都变了。
我看了一下掘金的一些文章,好长啊,觉得还是自己想一下怎么写吧。就自己实现了一遍。希望思路给大家一点帮助。
分析瀑布流

以小红书的瀑布流为例,相同宽度不同高度的卡片堆叠在一起形成瀑布流。
这里有两个难点:
- 卡片高度如何确定?
- 堆叠布局如何实现?
卡片的高度 = padding + imageHeight + textHeight....
不固定的内容包括:图片高度、标题行数
也就是说当我们解决了图片和标题的高度问题,那么瀑布流的第一个问题就解决了。(感觉已经写好代码了一样)
堆叠问题——因为css没有这样的布局方式,所以肯定得用js实现。最简单的解决方案就是对每一个盒子进行绝对定位。
这个问题就转换成计算现有盒子的定位问题。
从问题到代码
第一个问题——图片高度
无论是企业业务场景还是个人开发,通过后端返回图片的width、height都是合理且轻松的。
前端去获取图片信息,无疑让最重要的用户体验变得糟糕。前端获取图片信息并不困难,但是完全没有必要。
所以我直接考虑后端返回图片信息的情况。
const realImageHeight = imageWidth / imageHeight * cardContentWidth;
图片高度轻松解决,无平台差异
第二个问题——文字高度
从小红书可以看出,标题有些两行有些一行,也有些三行。
如果你固定一行,这个问题完全可以跳过。
- 方案一:我们可以用字数和宽度来计算可能得行数
优势:速度快,多平台复用
劣势:不准确(标题包括英文中文) - 方案二:我们可以先渲染出来再获取行数
优势:准确
劣势:相对而言慢,不同平台方法不同
准确是最重要的!选择方案二
其实方案二也有两种方案,一种是用canvas模拟,这样可以最大限度摆脱平台(h5、小程序)的限制,
然而我试验后,canvas还没找到准确的计算的方法(待后续更新)
第二种就是用div渲染一遍,获取行数或者高度。
创建一个带有指定样式的 div 元素
function createDiv(style: string): HTMLDivElement {
const div = document.createElement('div');
div.style.cssText = style;
document.body.appendChild(div);
return div;
}
计算文本数组在指定字体大小和容器宽度下的行数
/**
* 计算文本数组在指定字体大小和容器宽度下的行数
* @param texts - 要渲染的文本数组
* @param fontSize - 字体大小(以像素为单位)
* @param lineHeight - 字体高度(以像素为单位)
* @param containerWidth - 容器宽度(以像素为单位)
* @param maxLine - 最大行数(以像素为单位)
* @returns 每个文本实际渲染时的行数数组
*/
export function calculateTextLines(
texts: string[],
fontSize: number,
lineHeight: number,
containerWidth: number,
maxLine?: number
): number[] {
// 创建一个带有指定样式的 div 元素
const div = createDiv(`font-size: ${fontSize}px; line-height: ${lineHeight}px; width: ${containerWidth}px; white-space: pre-wrap;`);
const results: number[] = [];
texts.forEach((text) => {
div.textContent = text;
// 获取 div 的高度,并根据字体大小计算行数
const divHeight = div.offsetHeight;
const lines = Math.ceil(divHeight / lineHeight);
maxLine && lines > maxLine ? results.push(maxLine) : results.push(lines);
});
// 清理 div
removeElement(div);
return results;
}
这个问题小程序如何解决放在文末
第三个问题——每个卡片的定位问题
解决了上面的问题,就解决了盒子高度的问题,这个问题完全就是相同宽度不同高度盒子的堆放问题了
问题的完整描述是这样的:
写一个ts函数实现将一堆小盒子,按一定规则顺序推入大盒子里
函数输入:小盒子高度列表
小盒子:不同小盒子高度不一致,宽度为stackWidth,彼此间隔gap
大盒子:高度无限制,宽度为width
堆放规则:优先放置高度低的位置,高度相同时优先放在左侧
返回结果:不同盒子的高度和位置信息
如果你有了这么清晰的描述,接下去的工作你只需要交给gpt来写你的函数
// 返回的盒子信息
export interface Box {
x: number;
y: number;
height: number;
}
// 盒子堆叠的方法类
export class BoxPacker {
// 返回的小盒子信息列表
private boxes: Box[] = [];
// 大盒子宽度
private width: number;
// 小盒子宽度
private stackWidth: number;
// 小盒子间隔
private gap: number;
constructor(width: number, stackWidth: number, gap: number) {
this.width = width;
this.stackWidth = stackWidth;
this.gap = gap;
this.boxes = [];
}
// 添加单个盒子
public addBox(height: number): Box[] {
return this.addBoxes([height]);
}
// 添加多个盒子(一般用这个方法)
public addBoxes(heights: number[], isReset?: boolean): Box[] {
isReset && (this.boxes = [])
console.log('this.boxes—————— ', JSON.stringify(this.boxes) )
for (const height of heights) {
const position = this.findBestPosition();
const newBox: Box = { x: position.x, y: position.y, height };
this.boxes.push(newBox);
}
return this.boxes;
}
// 查找定位函数
private findBestPosition(): { x: number; y: number } {
let bestX = 0;
let bestY = Number.MAX_VALUE;
for (let x = 0; x <= this.width - this.stackWidth; x += this.stackWidth + this.gap) {
const currentY = this.getMaxHeightInColumn(x, this.stackWidth);
if (currentY < bestY || (currentY === bestY && x < bestX)) {
bestX = x;
bestY = currentY;
}
}
return { x: bestX, y: bestY };
}
private getMaxHeightInColumn(startX: number, width: number): number {
return this.boxes
.filter(box => box.x >= startX && box.x < startX + width)
.reduce((maxHeight, box) => Math.max(maxHeight, box.y + box.height + this.gap), 0);
}
}
这样我们就实现了根据高度获取定位的功能了
来实现一波
核心的代码就是获取每个盒子的定位、宽高信息
// 实例
const boxPacker = useMemo(() => {
return new BoxPacker(width, stackWidth, gap)
}, []);
const getCurrentPosition = (currentData: DataItem[], reset?: boolean) => {
// 获取标题文本行数列表
const textLines = calculateTextLines(currentData.map(item => item.title),card.fontSize,card.lineHeight, cardContentWidth)
// 获取图片高度列表
const imageHeight = currentData.map(item => (item.imageHeight / item.imageWidth * cardContentWidth))
// 获取小盒子高度列表
const cardHeights = imageHeight.map((h, index) => (
h + textLines[index] * card.lineHeight + card.padding * 2 + (card?.otherHeight || 0)
)
);
// 获取盒子定位信息
const boxes = boxPacker.addBoxes(
cardHeights,
reset
)
// 返回盒子列表信息
return boxes.map((box, index) => ({
...box,
title: currentData[index]?.title,
url: currentData[index]?.url,
imageHeight: imageHeight[index],
}))
}
set获取到的盒子信息
const [boxPositions, setBoxPositions] = useState<(Box & Pick<DataItem, 'url' | 'title' | 'imageHeight'>)[]>([]);
useEffect(() => {
// 首次和刷新
if (page === 1) {
setBoxPositions(getCurrentPosition(data, true))
} else {
// 加载更多
setBoxPositions(getCurrentPosition(data.slice((page - 1) * pageSize, page * pageSize)))
}
}, [])
效果如下

小程序获取文本高度
从上面的分析可以看出来只有文本高度实现是不同的,如果canvas方案实验成功,说不定还能做到大一统。
目前没成功大家就先看看我的目前方案:先实际渲染文字然后读取信息,然后获取实际高度
import React, {useEffect, useMemo, useState} from 'react'
import { View } from '@tarojs/components'
import Taro from "@tarojs/taro";
import './index.less'
import {BoxPacker} from "./flow";
const data = [
'vwyi这是一个标题,这是一个标题,这是一个标题,这是一个标题',
'这是一个标题',
'这是一个标题,这是一个标题,这是一个标题,这是一个标题',
'这是一个标题',
'这是一个标题,这是一个标题,这是一个标题,一个标题',
'这是一个标题,这是一个标题,这是一个标题,这题',
'这是一个标题,这是一个标题,这是一',
'这是一个标题,这是一个标题,这是一',
];
function Index() {
const boxPacker = useMemo(() => new BoxPacker(320, 100, 5), []);
const [boxPositions, setBoxPositions] = useState<any[]>([])
function getTextHeights() {
return new Promise((resolve, reject) => {
Taro.createSelectorQuery()
.selectAll('#textContainer .text-item')
.boundingClientRect()
.exec(res => {
if (res && res[0]) {
const heights = res[0].map(item => item.height);
resolve(heights);
} else {
reject('No buttons found');
}
});
});
}
useEffect(() => {
getTextHeights().then(h => {
setBoxPositions(boxPacker.addBoxes(h))
})
}, [])
return (
<View className="flow-container">
<View id="textContainer">
{
data.map((item, index) => (<View key={index} className="text-item">{item}</View>))
}
</View>
<View className="text-box-container">
{boxPositions.map((position, index) => (
<View
key={index}
className="text-box"
style={{
left: `${position.x}px`,
top: `${position.y}px`,
height: `${position.height}px`,
width: '100px', // 假设盒子的宽度固定为100px
}}
>
{`${data[index]}`}
</View>
))}
</View>
</View>
)
}
export default Index
来源:juejin.cn/post/7397278180644372521
Llama + Dify,在你的电脑搭建一套AI工作流
点赞 + 关注 + 收藏 = 学会了
本文简介
最近字节在推Coze,你可以在这个平台制作知识库、制作工作流,生成一个具有特定领域知识的智能体。
那么,有没有可能在本地也部署一套这个东西呢?这样敏感数据就不会泄露了,断网的时候也能使用AI。
刚好最近 Llama 3.1
发布了,本文就以 Llama 3.1
作为基础模型,配合 Dify
在本地搭建一套“Coze”。
跟着本文一步步操作,保证能行!
Dify是什么?
Dify 官网(difyai.com/) 的自我介绍:Dify
是开源的 LLM
应用开发平台。提供从 Agent
构建到 AI workflow
编排、RAG
检索、模型管理等能力,轻松构建和运营生成式 AI 原生应用。比 LangChain
更易用。
动手搭建
在本地搭建这个平台很简单,其实 Dify文档(docs.dify.ai/v/zh-hans) 里都写得明明白白了,而且还有中文文档。
具体来说需要做以下几步:
- 安装
Ollama
- 下载大模型
- 安装
Docker
- 克隆
Dify
源代码至本地 - 启动
Dify
- 配置模型
接下来一步步操作。
安装 Ollama
简单来说 Ollama
是运行大语言模型的环境,这是 Ollama
的官网地址 (ollama.com/ ),打开它,点击 Download 按钮下载 Ollama
客户端,然后傻瓜式安装即可(一直点“下一步”)。
安装完成后就能看到一个羊驼的图标,点击运行它即可。
下载大模型
安装完 Ollama
后,我们到 Ollama
官网的模型页面(ollama.com/library)挑选一下模型。
这里面有很多开源模型,比如阿里的千问2,搜索 qwen2
就能找到它。
本文使用 Llama 3.1
,这是前两天才发布的模型,纸面参数贼强。
打开 Llama 3.1
模型的地址(ollama.com/library/lla…),根据你需求选择合适的版本,我选的是 8b 版。
选好版本后,复制上图右侧红框的命令,到你电脑的终端中运行。
如果你还没下载过这个模型它就会自动下载,如果已经下载过它就会运行这个模型。
运行后,你就可以在终端和大模型对话了。
当然,我们不会这么原始的在终端和大模型对话,我们可是要搞工作流的!
安装 Docker
前面的基础步骤都搞掂了,接下来就要开始为运行 Dify
做准备了。
先安装一下 Docker
,打开 Docker
官网(http://www.docker.com/),根据你系统下载对应的安装包,然后还是傻瓜式安装即可。
克隆 Dify 源代码至本地
要使用 Dify
,首先要将它拉到你电脑里。
git clone https://github.com/langgenius/dify.git
在你电脑里找个位置(目录),用 git
将 Dify
克隆下来,用上面这条命令克隆就可以了。
启动 Dify
进入 Dify 源代码的 docker 目录,执行一键启动命令:
cd dify/docker
cp .env.example .env
docker compose up -d
启动完成后,你的 docker
里就会看到这个
此时你在浏览器输入 http://localhost
就能看到这个界面。
首次打开 Dify
需要你设置一下管理员的账号。
然后用管理员账号登录,可以看到下面这个页面。
点击“创建空白应用”就可以创建聊天助手、文本生成应用、Agent、工作流。
我们点击"工作流"就能看到类似Coze的工作流编辑界面了。
配置模型
在配置工作流之前,我们需要给 Dify
配置大语言模型。
点击页面右上角的管理员头像,然后选择“设置”。
选择“模型供应商”,然后点击“Ollama”的卡片添加模型。
在添加 Ollama
模型时,弹窗的左下角有一个“如何继承 Ollama”的按钮,点击它会跳转到 Dify
官方文档教你怎么配置,但这里可能会有个小坑。
前面我们已经使用 Ollama
把 Llama 3.1
运行起来了,在浏览器打开 `http://localhost:11434 看到这个界面证明模型运行成功。
此时在“添加 Ollama”将资料填写好,“基础 URL”里输入 http://localhost:11434
即可。
如果你是 Mac 电脑,填入以上资料有可能会报这个错:
An error occurred during credentials validation: HTTPConnectionPool(host='localhost', port=11434): Max retries exceeded with url: /api/chat (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0xffff5e310af0>: Failed to establish a new connection: [Errno 111] Connection refused'))
此时你需要在“基础 URL”里填入 http://host.docker.internal:11434
。
遇到问题可以看 Dify
官方文档的 FAQ。
添加完成后你就可以在模型列表里看到它了。
除了接入 Ollama
外,Dify
还支持接入 OpenAI
等闭源模型,但需要你去 OpenAI
那边买个服务。
以上就是本文的全部内容啦,如果本文对你有帮助的话也请你分享给你的朋友~
点赞 + 关注 + 收藏 = 学会了
来源:juejin.cn/post/7395902224091971594
好消息!uniapp也能开发鸿蒙了,但坏消息是……
相信不少前端从业者一听uniapp支持开发鸿蒙Next后非常振奋。猫林老师作为7年前端er也是非常激动,第一时间体验了下。在这里也给大家分享一下我的看法
uniapp开发鸿蒙优势
- 对于前端开发者而言,几乎无需增加额外的学习成本
- 一套代码,通用在Android、iOS、HarmonyOS,小公司狂喜(可以只招一位牛马完成所有工作)
- 能迅猛将现有项目移植到鸿蒙平台,迅速掌握鸿蒙用户流量以及争取政府补贴
- 以及更多猫林老师没想到的优点(抱歉,实在憋不出来了)
uniapp开发鸿蒙缺点
- 这真的是可以大吐特吐的地方了,uniapp目前支持鸿蒙的方案是web渲染方案,也就是说相当于利用鸿蒙内部的
webview
显示一个网页 - 那这有什么不好呢?
- 首先是渲染性能达不到原生、其次是逻辑代码是JS实现,而JS引擎慢,这就导致启动速度和运行速度弱于原生
- JS与原生UI层或者原生API通信可能会卡顿
- 其次是目前仅支持vue3,对于还在守着vue2的古早前端也不友好
- 以上结论来自uniapp官网说明,如下图
- 因此猫林老师不认为目前的uniapp适合鸿蒙开发,所以如果有志于抢占鸿蒙风口的同学,可以坚定信心了,还是得好好学习鸿蒙原生开发。
uniapp未来会好吗?
- 上述缺点其实DCloud官方(uniapp所属)也意识到了,所以一直在打造新一代的uniapp,也即uni-app x
- 这套新平台追求解决所有跨平台开发框架性能无法媲美原生的痛点,通过不同平台编译成不同语言来实现:在iOS平台编译为swift、在Android平台编译为kotlin、在Web和小程序平台编译为js、在鸿蒙next平台上编译为ArkTS。就相当于你用vue的语法写了原生的代码。
- 因此,未来的uniapp还是非常值得期待的!
- 但现阶段,虽然uni-app x也已经对外发布,但是对于鸿蒙的支持还在不断的完善。并且鸿蒙自身也在不断的升级迭代,所以现阶段的uni-app x暂时还是无法展现完整的鸿蒙开发之美。期望未来能越来越好,为鸿蒙生态提供强有效的生产力。
总结
uniapp支持鸿蒙是一个好消息,未来也值得期待。但是现阶段用来作为找鸿蒙开发岗位的工作还是不太合适。
来源:juejin.cn/post/7397323478851158050
不戴眼镜也可以看清啦!记录我的摘镜经历
大家好,我是 Gopal。好久不见,甚是想念!
本篇文章记录一下今年我做的一个比较大胆的决定——做近视手术。
首先声明一下,本篇文章不是广告,纯分享个人经历,看完这篇文章,至于要不要做视力矫正?怎么做?个人可以根据需要自行决定哈。
我为什么要摘镜
先说一下,我的情况。我是初中毕业后开始慢慢近视的,至今的话,也有十来年了,眼镜都配了好多副。有近视的同学应该都知道,近视确确实实带给我们很多的烦恼,我举几个例子:
- 看 3D 电影,我得带两副眼镜,一副是自己的,一副是 3D 眼镜。
- 游泳的时候,脱掉眼镜,啥也看不到。
- 打篮球或者一些激烈的运动,眼镜经常脱落。
- 每天都得擦拭眼镜,对于我这种有点强迫症来讲的人,会是每天的工作。
- 早上起来第一件事情,找眼镜。
- ...
基于以上的种种,摘镜意愿,我个人是比较强烈的。当我听说现在有手术可以进行视力矫正时候,是非常兴奋的。我之后做了相关的调查,我们来看一下视力矫正有哪些方案吧。
视力矫正有哪些方案
这里我会介绍得比较粗糙一些,毕竟我不是专业的,知识都是我从网络中总结得来。
近视原因:我们大部分人的近视都是轴性近视,是由于眼球的前后长度(轴长)延长造成的,可以看以下的图片。在正常眼中,平行光线入眼后在视网膜上形成焦点。而在近视眼中,焦点位置落在视网膜之前,近视眼想看清,就得调整屈光度,使其聚焦在视网膜上。
眼镜通过镜片调节屈光度,而近视手术的大致有两种方式进行:
- 第一种,通过角膜手术改变角膜的形态和屈光力,使物象成像到视网膜上。
- 第二种,更换晶体或向眼内植入补偿晶体,从而达到类似的效果,这种类似于带隐形眼镜。
其中有一个特点就是晶体是有可逆性,激光手术的话,是不可逆的。以上两种并没有说哪种是最优解,需要通过医院检查才能决定。一般来说,角膜屈光手术建议近视度数小于 1000 度,散光小于 600 度。如果因为度数过高、或角膜太薄不能做角膜屈光手术,可以考虑做晶体植入手术。
现在主流的手术方案有以下四种(前面三种都属于角膜手术),具体的这里不展开了,大家有想法可以自行搜索哈。
- Smart全激光
- 半飞秒
- 全飞秒Smile
- ICL晶体植入术
而我通过检查之后,选择了全飞秒Smile。主要是我度数还好,角膜厚度也足够。
手术前中后
首先约了一家上市的机构做检查,看得出还是比较慎重,各个方面评估能不能做,以及有哪些方案,最后会有专门的人给我讲解。
做完检查之后,我当时就约了隔周去做,因为手术前几天是需要滴眼药水,而且不能佩戴隐形眼镜等。
手术当天,做术前检查,交代注意事项/术后用药、签署同意书等。说实话,我还是比较紧张的,毕竟把眼睛这么重要的部位交给医生。
然后就是手术了,真正的手术时间也就几分钟,当躺在手术室中,一开始我以为医生还在给我做前置检查,没想到几分钟后医生告诉我已经结束了,可以回家了。手术过程滴麻药,基本不会痛的。整个激光过程,眼睛是会有开睑器撑开固定,手术过程眼睛是同时睁开/闭上的。手术过程中,需要在打激光的过程中配合,眼睛保持不动不眨,听好医生指令。
手术后,我和我对象就坐地铁回去了,回家的路上,我一直戴着墨镜,随着麻药的失效,会有一种较强的异物感。虽然我知道这是一个正常现象(医生提前已告知,手术后将会在4-6小时内会出现流泪、畏光、白雾感、异物感、酸胀感等刺激症状,其轻重因人而异),但是当时真的挺怕的,毕竟可不是小事。(所以最好手术当天最好还是有人一起去)。
当天下午,我就感觉好很多了。我做的事全飞秒,周六做的手术,周一的时候,我就可以正常上下班了,基本不会影响工作。唯一我感知到的一点点影响就是切换屏幕的时候,聚焦会需要一点点时间,很短,医生说是正常现象。不过中间需要注意用眼,然后注意按照医嘱滴眼药水等。这里我贴几个术后的注意事项,当然你应该根据你的主治医生来:
- 术后一周内清淡饮食,吃辛辣刺激食物。
- 术后两周内洗头洗脸时不宜将水溅入眼内,切忌揉眼。(手术室给的眼罩睡觉时带上)。
- 术后一个月内勿游泳,不要在眼部使用化妆品并避免异物进入眼内。
- 术后一个月在室外请戴太阳镜(全激光术后戴三个月),室内、晚上不需佩戴。
- 术后三个月内尽量避免剧烈运动,术后一周后可以适量健身运动,但须注意在活动中不要伤及手术眼。
- 术后视力恢复是逐步提高的过程,开始阶段看近会感到稍有模糊,雾感。此症状会逐渐消失,视力提高有快有慢,双眼视力恢复会有波动与近视度数及个人对治疗反应差异有关,六个月基本稳定。
在写本文的时候,已经有两个多月了,感觉日常生活没啥影响。在一个月复诊的时候,我的视力一边是 1.2,一边是 1.5。如果说有啥「副作用」的话,有两点。一个是晚上的视力(在光线比较差的地方)会稍微有点差,另外一个看一些光源(比如路灯),会有一点炫光。不过这些术前医生有给我说过,我是有做好心理准备的。
整体而言,目前为止,我对这次的近视手术还是很满意的。
手术价格以及副作用
这个我相信不少人比较关注,我做的全飞秒,全部下来大概 1.8w 左右(公司跟这家机构有合作,跟我说是打了折扣,我了解了一下,在深圳,这个价格其实差不多)。激光和半分秒会稍微便宜一些。晶体植入是最贵的(大概 3w 多)。具体的还需要根据不同的城市和机构看。据说虽然近视手术简单,但是它那个机器是需要给专利费用的,用一次给一次专利费用。整体上讲,这个还是可以接受的。另外,近视手术不能报医保。
比如价格,我觉得大家最关心的问题是副作用,或者说风险系数。任何手术都是有风险的,包括近视手术。网络上会有人说:
- 近视手术这么好,为什么医生也还带眼镜?
- 近视手术这么好,为什么马化腾还带眼镜?
- ...
首先,先不否认大家的疑虑,毕竟眼睛这么重要的东西,要在上面动手术,想想都怕。但是,我们也需要辩证的看待这些问题。
- 近视手术是需要满足一定条件的,不满足的话,医院是不敢给你做的。
- 近视手术是不会导致你瞎的,从原理上来说,近视手术是眼睛前面部分的手术,不涉及眼部深层组织,比如晶状体、视网膜等,更加不可能致盲。
- 近视手术存在发生并发症的可能性,可能性大小而已。(别人没有,并不代表自己没有)比如眼干等。虽然现在手术已经很成熟,但是在你决定要做之前,一定要先了解清楚,看你能不能承担这个风险。
- 近视手术只是当前帮你调整好你的视力,假如你不注意用眼的话,是存在再次近视的风险的。所以一般建议成年之后,度数稳定之后再做这个手术。
- ...
最后再强调一遍,近视手术是一种改善型的手术,不做其实本质上对于自身身体健康没有影响,如果摘镜意愿强烈的同学可以尝试去了解一下。
写在最后
目前为止,我感受到更多的是摘镜之后给我生活带来的便利,基本算是满足了我摘镜的预期!
我之前一直在想,假如哪天我落在一座荒岛上,估计是活不下去的。因为假如一旦我眼镜坏了,那么我就「看不清」这个世界了。
不过现在医学科技的进步解决了我的这个问题。
来源:juejin.cn/post/7293788137662038050
面试官:假如有几十个请求,如何去控制并发?
面试官:看你简历上做过图片或文件批量下载,那么假如我一次性下载几十个,如何去控制并发请求的?
让我想想,额~, 选中ID,循环请求?,八嘎!肯定不是那么沙雕的做法,这样做服务器直接崩溃啦!突然灵光一现,请求池!!!
我:利用Promise模拟任务队列,从而实现请求池效果。
面试官:大佬!
废话不多说,正文开始:
众所周知,浏览器发起的请求最大并发数量一般都是6~8
个,这是因为浏览器会限制同一域名下的并发请求数量,以避免对服务器造成过大的压力。
首先让我们来模拟大量请求的场景
const ids = new Array(100).fill('')
console.time()
for (let i = 0; i < ids.length; i++) {
console.log(i)
}
console.timeEnd()
一次性并发上百个请求,要是配置低一点,又或者带宽不够的服务器,直接宕机都有可能,所以我们前端这边是需要控制的并发数量去为服务器排忧解难。
什么是队列?
先进先出就是队列,push
一个的同时就会有一个被shift
。我们看下面的动图可能就会更加的理解:
我们接下来的操作就是要模拟上图的队列行为。
定义请求池主函数函数
export const handQueue = (
reqs // 请求数量
) => {}
接受一个参数reqs
,它是一个数组,包含需要发送的请求。函数的主要目的是对这些请求进行队列管理,确保并发请求的数量不会超过设定的上限。
定义dequeue函数
const dequeue = () => {
while (current < concurrency && queue.length) {
current++;
const requestPromiseFactory = queue.shift() // 出列
requestPromiseFactory()
.then(() => { // 成功的请求逻辑
})
.catch(error => { // 失败
console.log(error)
})
.finally(() => {
current--
dequeue()
});
}
}
这个函数用于从请求池中取出请求并发送。它在一个循环中运行,直到当前并发请求数current
达到最大并发数concurrency
或请求池queue
为空。对于每个出队的请求,它首先增加current
的值,然后调用请求函数requestPromiseFactory
来发送请求。当请求完成(无论成功还是失败)后,它会减少current
的值并再次调用dequeue
,以便处理下一个请求。
定义返回请求入队函数
return (requestPromiseFactory) => {
queue.push(requestPromiseFactory) // 入队
dequeue()
}
函数返回一个函数,这个函数接受一个参数requestPromiseFactory
,表示一个返回Promise的请求工厂函数。这个返回的函数将请求工厂函数加入请求池queue
,并调用dequeue
来尝试发送新的请求,当然也可以自定义axios,利用Promise.all
统一处理返回后的结果。
实验
const enqueue = requestQueue(6) // 设置最大并发数
for (let i = 0; i < reqs.length; i++) { // 请求
enqueue(() => axios.get('/api/test' + i))
}
我们可以看到如上图所示,请求数确实被控制了,只有有请求响应成功的同时才会有新的请求进来,极大的降低里服务器的压力,后端的同学都只能喊6。
整合代码
import axios from 'axios'
export const handQueue = (
reqs // 请求总数
) => {
reqs = reqs || []
const requestQueue = (concurrency) => {
concurrency = concurrency || 6 // 最大并发数
const queue = [] // 请求池
let current = 0
const dequeue = () => {
while (current < concurrency && queue.length) {
current++;
const requestPromiseFactory = queue.shift() // 出列
requestPromiseFactory()
.then(() => { // 成功的请求逻辑
})
.catch(error => { // 失败
console.log(error)
})
.finally(() => {
current--
dequeue()
});
}
}
return (requestPromiseFactory) => {
queue.push(requestPromiseFactory) // 入队
dequeue()
}
}
const enqueue = requestQueue(6)
for (let i = 0; i < reqs.length; i++) {
enqueue(() => axios.get('/api/test' + i))
}
}
实战文章
之前写过一篇关于web-worker
大文件切片的案例文章,就是利用了此特性感兴趣的小伙伴可以看看web-worker的基本用法并进行大文件切片上传(附带简易node后端)
来源:juejin.cn/post/7356534347509645375
原来我们是这样对工作失去兴趣的
一、前言
相信很多人有过接手别人的系统,也有将自己负责的系统交接给别人的经历。既有交接出去不用在费力治理维护技术债务的喜悦,也有接手对方系统面对一系列维护问题的愁容满面。
但是被人嫌弃的系统曾经也是「创建人」心中的白月光啊,是什么导致了「白月光」变成了「牛夫人」呢?是996,工期倒排、先上再优化,还是随时变动的需求?
让我们来复盘系统是怎么一步一步腐化的,让我们丢失了最初的兴趣,同时总结一些经验教训以及破局之策。
二、白月光到牛夫人的经历
一般当我们设计一个系统时,总是会抱着要把该项目打造为「干净整洁」的项目的想法,
但是随着时间的推移,最后总是不可避免的变成了这样:
2.1、从0到1
我们发现大多数人对于创建新项目总是会抱有极大的激情兴趣,会充分的考虑架构设计。但是对于接手的项目就会缺乏耐心。
这种心理在《人月神话》一书中被说为编程职业的乐趣:
“首先,这种快乐是一种创建事物的纯粹快乐。如同小孩在玩泥巴时 感到快乐一样,成年人喜欢创建事物,特别是自己进行设计 。我想这种 快乐是上帝创造世界的折射,一种呈现在每片独特的、崭新的树叶和雪 花上的喜悦。”
“第四,这种快乐是持续学习的快乐,它来自于这项工作的非重复特性。人们所面临的问题总有这样那样的不同,因而解决问题的人可以从 中学习新的事物,有时是实践上的,有时是理论上的,或者兼而有之。”
正是由于这样的心理,人们在面对新系统时,可以实践自身所学,主动思考如何避开曾经遇到的坑。满足了内心深处的对于创造渴望。
当一个项目是从0到1开始设计的,并且前期是由少数「高手」成员主导开发的话,一般不会有债务体现。当然明面上没有债务,不代表没有埋下债务的种子。
2.2、抢占市场、快速迭代
系统投入市场得到验证后,如果顺利,短期会收获大量用户。伴随着用户指数增长的同时,各种产品需求也会随着而来。一般在这个阶段将会是「工期倒排、先上再优化,需求随时变动」的高发期。
同时由于需求的爆发,为了提高团队的交付率,在这个阶段会引入大量的“新人”。随着带来的就是新老思想的碰撞,新的同学不一定认同之前的架构设计。
在这个阶段,如果团队存在主心骨,可以“游说”多方势力,平衡技术产品、新老开发之间的矛盾,那么在这个阶段引入的债务将会还好。但是如果团队缺乏这样的角色,就会导致公说公有理婆说婆有理,最后的结果就是架构会朝着多个方向发展,一个项目里充斥着多种不同思路的设计。有些甚至是矛盾的。如果还有【又不是不能用】的想法出现,那将是灭顶之灾。
但是在这个阶段对于参与者又是幸福的,一份【有市场、有用户、有技术、有价值】的项目,无论是对未来的晋升还是跳槽都是极大的谈资。
2.3、维护治理
褪去了前期“曾经沧海难为水,除却巫山不是云”的热恋后,剩下的就是生活的柴米油盐。系统的最终结局也是“维护治理”。
在这个阶段,需求的数量将大大减少,但是由于前期的“快速建设”,一个小小的需求,我们可能需要耗费数周的时间去迭代。而且系统越来越复杂,迭代越来越困难。
同时每天需要花费大量的时间处理客诉、定位bug,精力被完全分散。系统的设计和技术慢慢的变得僵化。并且由于用户量巨大,每次改动都要承担很大的线上风险。这样的情况对于程序员的精力、体力都是一场内耗,并且如果领导的重心也不在此项目时,更会加重这种内耗。于是该系统就会显得“人老色衰”,曾经的「白月光」也就变成了「牛夫人」。
三、牛夫人不好吗?
3.1、缺乏成就感
《人月神话》中关于程序员职业的苦恼曾说过以下几点:
- 对于系统编程人员而言,对其他人的依赖是一件非常痛苦的事情。他依靠其他人的程序,而这些程序往往设计得并不合理、实现拙劣、发布不完整(没有源代码或测试用例)或者文档记录得很糟。所以,系统编程人员不得不花费时间去研究和修改,而它们在理想情况下本应该是可靠的、完整的。
- 下一个苦恼---概念性设计是有趣的,但寻找琐碎的bug却是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的 劳动。程序编制工作也不例外。
- 最后一个苦恼 ,有时也是一种无奈—当投入了大量辛苦的劳动 ,***产品在即将完成或者终于完成的时候,却已显得陈旧过时。***可能是同事或竞争对手已在追逐新的、更好的构思; 也许替代方案不仅仅是在构思 ,而且己经在安排了。
随着业务趋于稳定,能够发挥创造性的地方越来越少,剩下的更多是沉闷、枯燥的维护工作。并且公司的资源会更聚焦在新业务、新方向,旧系统获得的关注更少,自然而然就缺乏成就感。也就形成了【只见新人笑,不见旧人哭】
3.2、旧系统复杂、难以维护
《A Philosophy of Software Design》一书中对复杂性进行了如下定义:“Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.”,即任何使得软件难于理解和修改的因素都是复杂性。
作者John 教授又分别从三个角度进行了解释复杂性的来源:
3.2.1、变更放大
复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。对于复杂的系统来说,如果代码没有聚敛,那么一次小小的需求可能会导致多处修改。同时为了保证不出故障,需要对涉及的修改点、功能点进行完整的覆盖测试。这种隐藏在背后的工作量是巨大的。
3.2.2、认知负荷
复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。当一个系统经过多年的迭代开发,其复杂度将是指数级别的。并且会充斥这很多只有“当事人”才能理解的“离谱”功能。这就对维护者提出了极高的要求,需要掌握很多“冰山下”的知识才能做到手到擒来。而这对维护者的耐心、能力又是一次挑战。
3.2.3、未知的未知
未知的未知是指必须修改哪些代码才能完成任务,或者说开发人员必须获得哪些信息才能成功地执行任务。这一项也是John Ousterhout教授认为复杂性中最糟糕的一个表现形式。
这句话看起来比较抽象,如果映射到我们日常的工作中就是“我也不知道为啥这么改就好了”、“在我这是好的呀”、“刚刚还能运行啊,不知道为啥现在突然不行了”。
这种情况就是我们不知道改动的这行代码是否能让程序正常运转,也不知道这行代码的改动是否又会引发新的问题。这时候我们发现,那些“上帝类”真的就只有上帝能拯救了。
我曾经维护一个老系统,源码是通过文件相互传递的,没有仓库,里面的框架是自己撸的轮子,没有任何说明文档。服务部署是先在本地编译成二进制文件,然后在上传到服务器启动。每次改动上线都是就是一次生死劫,幸好没过多久,这个系统就被放弃了。
四、为何变成了牛夫人
4.1、伪敏捷
“敏捷”已经成为了国内公司的银弹了。
需求不做市场分析、不考虑用户体验、不做设计分析、不考虑前因后果,美其名曰“敏捷”。
工期倒排、先上再说、明天能不能上、这个问题上了再优化,美其名曰“敏捷”。
我曾经参与过一个项目,最开始是给了三个月的时间完成产品规划+开发,但是项目立项后领导层迟迟无法达成统一,一直持续进行了两个月的讨论后,终于确定了产品模型,进入开发。到这里留给开发的时间还剩一个月,咬咬牙也能搞定。但是开发真正进场,上报了需要开发的周期后,上面觉得太慢了,要求3周搞定,过了一天还是觉得太慢,要求2周,最后变成了要求4天搞定。遇到这种情况能怎么办,只能加人,只能怎么快怎么来。
之前阿里程序员也开玩笑式的说出了类似的场景:“2月13号上午省领导问逍遥子全省的健康码今天上线行不行,逍遥子说可以。等消息传达到产研团队的时候已经是中午了,然后团队在下午写了第一行代码。”
4.2、人的认知局限
《人月神话》一书中提到了一种组建团队的方式「外科手术团队」:“十个人,其中七个专业人士在解决问题,而系统是一个人或者最多两个人思考的产物,因此客观上达到了概念的一致性。”
也就是团队只需要一个或者两个掌舵人,负责规划团队的方向和系统架构,其余人配合他完成任务。目前我所待过的团队也基本是按照这个模式组成的,领导会负责重要事情的决策和团队分歧时的拍板,其余人则相互配合完成目标任务。但是这样的模式也导致了一个问题:掌舵人的认知上限就决定了团队的上限,而这种认知上限天然就会导致系统架构设计存在局限性,而这种局限性又会随着“伪敏捷”放大。
4.3、人员流动
经历过这种离职交接、活水交接的打工人应该深有体会,很多项目一旦步入这个阶段,大多数负责人就会开始放飞自我,怎么快怎么来,只想快点结束这段工作,快速奔赴下一段旅程。
从人性的角度是很难评价这种情况的,毕竟打工人和老板天然就不是一个战线的,甚至可能是对立面的。而我们大多数人都不可能是圣人,从自身角度从发这种行为是无可厚非的。
五、如何保持白月光
这里想首先抛个结论,系统变腐化是不可避免的。就类似人一样,随着时间的流逝,也会从以前一个连续熬夜打游戏看小说第二天依旧生龙活虎的青年变为一个在工位坐半小时都腰酸背痛,快走几步都喘的中年人。而这都来源于生活的压力、家庭的压力、工作的压力。同样的,面对业务的压力、抢占市场的压力、盈利的压力,系统也不可避免会变成“中年人”。
就像人一样会使用护肤品、健身等手段延缓自己的衰老一样,我们也可以使用一些手段延缓系统“衰老”。
在网上,已经有无数的文章教怎么避免代码腐化了,例如“DDD领域驱动设计”、“业务建模”、“重构”等等。
今天我想从别的角度聊聊怎么延缓代码腐化。
5.1、避免通用
软件领域有个特点,那就是复用。程序员们总是在思考怎么样写一段到处通用的代码,以不变应万变。特别是当国内提出中台战略后,这种情况就如脱缰的野马一般,不可阻挡。要是你做的业务、架构不带上xx中台,赋能xx,你都觉得你低人一等。
但是其实我们大部分人做的都是业务系统,本身就是面向某块特定市场的、特定用户的。这就天然决定了其局限性。
很多时候你会发现你用了100%的力气,设计了一个80%你认为有用的通用中台,最后只有20%产生了作用,剩下60%要么再也没有动过,要么就是被后来参与者喷的体无完肤。
当然这里也不说,设计时就按照当前产品提出的需求设计就行,一点扩展的余地都不留,而是在「通用」与「业务需求」之间取一个平衡。而这种平衡就取决于经验了。如果你没有这方面经验,那你就去找产品要「抄袭」的是哪个产品,看看他们有哪些功能,可以预留这些功能的设计点。
5.2、Clean Code
说实话,国内的业务系统80%都没有到需要谈论架构设计的地步。能够做到以下几点已经赢麻了:
- 良好的代码注释和相关文档存档【重中之重】
- 避免过长参数
- 避免过长方法和类
- 少量的设计模式
- 清晰的命名
- 有效的Code Review【不是那种帮我CR下,对方1秒后回复你一个done】
5.3、学会拒绝
自从国内开始掀起敏捷开发的浪潮后,在项目管理方面就出现了一个莫名其妙的指标:每次迭代的需求数都有会有一个数值,而且还不能比上一次迭代的少。
这种情况出现的原因是需求提出者无法确定这个需求可以带来多大的收益,这个收益是否满足老板的要求。那么他只能一股脑上一堆,这样即使最后效果不及预期,也可以怪罪于用户不买账。不是他不努力。
在这种时候,就需要开发学会识别哪些是真需求,哪些是伪需求,对于伪需求要学会说不。当然说不,不是让你上来就是开喷,而是你可以提出更加合理的理由,甚至你可以提出其他需求代替伪需求。这一般需要你对这块业务有非常深入的研究,同时对系统有上帝视角。
基本上在每个公司的迭代周期都是有时间要求的,比如我们就是两周一迭代,如果需求是你可控的,那么你就有更多的时间和心思在维护系统上,延缓他的衰老。
来源:juejin.cn/post/7312724606605918249
谈谈在大环境低迷下,找工作和入职三个月后的感受
前言
今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,在这个过程中也是第一次真真切切感受到了所谓大环境低迷下的“前端已死的言论”,也给大家分享一下自己入职三个月的个人感受吧。
从上一家公司离职时的个人感受
因为上一家公司的工作性质是人力外包驻场开发,年底客户公司(中国移动成都产业研究院)我所在的项目组不需要外包人员了,个人也是被迫拿了赔偿灰溜溜的走人了。
工作感受:对于这段工作经历我个人还是比较认可的,毕竟这里没有任何工作压力,也不加班,工作量少,有很多时间去学习新东西,做做自己的开源,认识了新的朋友等等。
学历的重要性:在这里面随便拎一个人出来可能就是研究生学历的国企单位,自己真实的意识到了学历的重要性(第一学历小专科的我瑟瑟发抖)。
和优秀的人共事:如果在一个长期压抑低沉消极的环境下工作无论你的性格在怎么积极乐观开朗,可能也很容易被影响到。相反如果是和在一群积极,乐观,开朗,充满自信的环境和人一起工作,相信你也会变得积极,乐观,自信这或许也是我这一段工作经历最大的收获吧。
2023年底找工作的市场就业环境
抱着试一试的心态在boss上更新了自己的简历状态,不出所料软件上面安静的奇怪ps:49入国军的感觉,已读未回可能是很失望的感觉吧,但年底找工作令人绝望的是大多数公司都是未读未回,这也就意味着年底基本上是没有正常公司招聘的了。
大概投了两周简历后终于在智联招聘上约到了一个短期三个月岗位的面试,现场两轮面试通过了,不过最终还是没有选择去。
原因有很多:
- 现场的工作环境个人感觉很压抑,从接待我前台和面试官都能感觉满脸写着疲惫
- 说公司最近在996,你也需要和我们一起
- 招聘岗位和工作内容是threejs开发,薪资却说只能给到普通前端开发的水平
- 人力外包公司hr的反复无常令我恶心,二面通过后hr给我打电话最主要就是聊薪资吧,电话内容也很简单hr:成都大部分前端的薪资是在XX-XX,可能给不到你想要的薪资,可能要往下压个1-2K。我:我提的薪资完全是在你们发布招聘岗位薪资的区间,既然你们给不到为什么要这样写了(有感到被侮辱了)。过了几天之后人力外包的hr又给我电话,说可以在原来提的薪资基础上加1.4K,希望能早点去客户公司入职。
总结:年底招聘的公司基本上没啥好鸟,如果你的经济能力还行的话让自己放松休息一段时间也是不错的选择
2024年初找工作:真实的感受到了大环境的低迷下的市场行情
印象最深刻的是在疫情时期的2021年,那会儿出来找工作boos上会有很多HR主动给你打招呼,一周大概能五六个面试,大专学历也有机会去自研公司
解封之后本以为市场行情会变得回缓,结果大概就是今年可是未来十年行情最好的一年
简单总结一下2024年的成都就业环境大概这样的:
- 只有外包公司会招专科学历
- boss上只给hr发一句打招呼的快捷语,99% 都是已读不回
- 大多数要完简历之后就没有后续了
- 待遇好的公司对于学历的要求更严格了(211,985)
- 给你主动打招呼的基本上都是人力外包公司
截至入职新公司前boss上面的投递状况:沟通了1384个岗位,投递了99份简历,一共约到了 8 家公司的面试
今年找工作的个人感受:不怕面试,就怕没有面试机会
首先说一下个人的一些情况吧,因为在创业小公司待过在技术栈方面个人认为算是比较全面的了
项目经验:做过管理系统(CRM,B2C,ERP,saas等管理系统)、商城和门户网站(响应式,自适应)、移动端(H5,小程序,app)、低代码和可视化(工作流,可视化大屏,3d编辑器)、第三方开发(腾讯IM,企业微信侧边栏)、微前端
项目经历:从0-1搭建过整个大项目的基建工作,封装过项目中很多功能性组件和UI组件二次封装(提高开发效率),接手过屎山代码并重构优化,约定项目的开发规范,处理很多比较棘手的疑难bug和提供相关的技术方案,没有需求概念下的敏捷开发,从0-1的技术调研等
代码方面:写过几个开源项目虽然star数量不多(目前最多一个项目是600+),但在代码规范和可读性方面个人认为还是比较OK的(至少不会写出屎山代码吧)
工作经验(4年):2020毕业至今一直从事前端开发工作
学历:自考本科学历(貌似没啥卵用)
学历确实是我很硬伤的一个点但是没办法,人嘛总归要为年轻时的无知买单吧
在这样的背景下开启了24年的找工作,从2月26号开始投递简历到4月1号拿到offer差不多一个多月左右时间,一共约到了8加公司的面试,平均一周两家公司
大概统计了一下这些公司的面试情况:
公司A:
- 数组哪些方法会触发Vue监听,哪些不会触发监听
- position 有哪些属性
- vue watch和computed的区别,computed和method的区别
- vue的watch是否可以取消? 怎么取消?
- position:absolute, position:fixed那些会脱离文档流
- 如何获取到 pomise 多个then 之后的值
- 常见的http状态码
- 谈谈你对display:flex 弹性盒子属性的了解
- 如何判断一个值是否是数组
- typeof 和instanceof的区别
- es6-es10新增了那些东西
- 离职原因,期望薪资,职业规划
公司B
到现场写了一套笔试题,内容记不清楚了
公司C
- vue router 和route 区别
- 说说重绘和重排
- css 权重
- 项目第一次加载太慢优化
- 谈谈你对vue这种框架理解
- sessionstorage cookie localstorage 区别
- 了解过.css 的优化吗?
- 闭包
- 内存泄漏的产生
- 做一个防重复点击你有哪些方案
- 解释一些防抖和节流以及如何实现
- 说一下你对 webScoket的了解,以及有哪些API
- 说一下你对pomise的理解
- vue2,vue3 中 v-for 和v-if的优先级
- 说说你对canvas的理解
公司D
笔试+面试
- vue 首屏加载过慢如何优化
- 说说你在项目中封装的组件,以及如何封装的
- 后台管理系统权限功能菜单和按钮权限如何实现的
- vue 中的一些项目优化
- 期望薪资,离职原因,
- 其他的记不清楚了
公司E
笔试+面试+和老板谈薪资
1.笔试:八股文
2.面试:主要聊的是项目内容比如项目的一些功能点的实现,和项目的技术点
3.老板谈薪资:首先就是非技术面的常规三件套(离职原因,期望薪资,职业规划),然后就是谈薪资(最终因为薪资给的太低了没有选择考虑这家)
公司F
也是最想去的一家公司,一个偏管理的前端岗位(和面试官聊的非常投缘,而且整个一面过程也非常愉快感受到了十分被尊重)
可惜的是复试的时候因为学历原因,以及一些职业规划和加班出差等方面上没有达到公司的预期也是很遗憾的错过了
一面:
- vue 响应式数据原理
- 说说es6 promise async await 以及 promise A+规范的了解
- 谈谈es6 Map 函数
- 如何实现 list 数据结构转 tree结构
- webScoke api 介绍
- webScoke 在vue项目中如何全局挂载
- vuex 和 pinia 区别
- 谈谈你对微任务和宏任务的了解
- call apply bind 区别
- 前端本地数据存储方式有哪些
- 数组方法 reduce 的使用场景
- 说说你对 css3 display:flex 弹性盒模型 的理解
- vue template 中 {{}} 为什么能够被执行
- threejs 加载大模型有没有什么优化方案
- 离职原因,住的地方离公司有多远,期望薪资
- 你有什么想需要了解的,这个岗位平时的工作内容
二面:
1.我看写过一个Express+Mongoose服务端接口的开源项目,说说你在写后端项目时遇到过的难点
2.介绍一下你写的threejs 3d模型可视化编辑器 这个项目
3.以你的观点说一下你对three.js的了解,以及three.js在前端开发中发挥的作用
4.现在的AI工具都很流行,你有没有使用过AI工具来提高你对开发效率
5.说说你认为AI工具对你工作最有帮助的地方是哪些
6.以你的观点谈谈你对AI的看法,以及AI未来发展的趋势
7.你能接受出差时间是多久
8.你是从去年离职的到今天这这几个月时间,你是去了其他公司只是没有写在简历上吗?
9.说说你的职业规划,离职原因,你的优点和缺点,平时的学习方式
公司G
一共两轮面试,也是最终拿到正式offer入职的公司
一面:
- 主要就是聊了一下简历上写的项目
- 项目的技术难点
- 项目从0-1搭建的过程
- 项目组件封装的过程
- vue2 和 vue3 区别
- vue响应式数据原理
- 对于typescript的熟练程度
- 会react吗? 有考虑学习react吗?
- 说一下你这个three.js3d模型可视化编辑器项目的一个实现思路,为什么会想写这样一个项目
二面:
- 说说了解的es6-es10的东西有哪些
- 说说你对微任务和宏任务的了解
- 什么是原型链
- 什么是闭包,闭包产生的方式有哪些
- vue3 生命周期变化
- vue3 响应式数据原理
- ref 和 reactive 你觉得在项目中使用那个更合适
- 前端跨越方式有哪些
- 经常用的搜索工具有哪些?
- 谷歌搜索在国内能使用吗?你一般用的翻墙工具是哪种?
- 用过ChatGPT工具吗? 有付费使用过吗?
- 你是如何看待面试造航母工作拧螺丝螺丝的?
- 谈谈你对加班的看法?
- 你不能接受的加班方式是什么?
- 为什么会选择自考本科?
- 你平时的学习方式是什么?
- 一般翻墙去外网都会干什么?,外网学习对你的帮助大吗?
- 上一家公司的离职原因是什么,期望薪资是多少, 说说你的职业规划
- 手里有几个offer?
hr电话:
- 大概说了一下面试结果通过了
- 然后就是介绍了一下公司的待遇和薪资情况?
- 问了一下上一家公司的离职原因以及上一家公司的规模情况?
- 手里有几个offer?
- 多久能入职?
因为后面没有别的面试了,再加上离职到在到找工作拿到offer已经有四个月时间没有上班了,最终选择了入职这家公司
入职第三天:我想跑路了!
入职后的第一天,先是装了一下本地电脑环境然后拉了一下项目代码熟悉一下,vue3,react,uniapp 项目都有
崩溃的开始:PC端是一个saas 系统由五个前端项目构成,用的是react umi 的微前端项目来搭建的,也是第一次去接触微前端这种技术栈,要命的是这些项目没有一个是写了readme文档的,项目如何启动以及node.js版本这些只能自己去package.json 文件去查看,在经过一番折腾后终于是把这几个项目给成功跑起来了,当天晚上回家也是专门了解了一下微前端
开始上强度: 入职的第二天被安排做了一个小需求,功能很简单就是改个小功能加一下字段,但是涉及的项目很多,pc端两个项目,小程序两个项目。在改完PC端之后,开始启动小程序项目不出所料又是一堆报错,最终在别的前端同事帮助下终于把小程序项目给启动成功了。
人和代码有一个能跑就行:入职的第三天也从别的同事那里了解到了,之前sass项目组被前端大规模裁员过,原因嘛懂得都懂? 能写出这样一堆屎山代码的人,能不被裁吗?
第一次知道 vue 还可以这样写
对于一个有代码强迫症的人来说,在以后的很长一段时间里要求优化和接触完全是一堆屎山一样代码,真的是很难接受的
入职一个月:赚钱嘛不寒掺
在有了想跑路的想法过后,也开始利用上班的空余时间又去投递简历了,不过现实就是在金三银四的招聘季,boss上面依旧是安静的可怕,在退一步想可能其他公司的情况也和这边差不多,于是最终还是选择接受了现实,毕竟赚钱嘛不寒掺
入职两个月:做完一个项目迭代过后,感觉好多了
在入职的前一个月里,基本上每天都要加班,原因也很简单:
1.全是屎山的项目想要做扩展新功能是非常困难的
2.整个项目的逻辑还是很多很复杂的只能边写项目边熟悉
3.因为裁了很多前端,新人还没招到,但是业务量没有减少只能加班消化
功能上线的晚上,加班到凌晨3点
在开发完一个项目迭代过后也对项目有了一些大概的了解,之后的一些开发工作也变得简单了许多
入职三个月:工作氛围还是很重要滴
在入职三个月后,前端组团队的成员也基本上是组建完成了,一共14人,saas项目组有四个前端,虽然业务量依然很多但是好在有更多的人一起分担了,每周的加班时间也渐渐变少了
在一次偶然间了解到我平时也喜欢打篮球后,我和公司后端组,产品组的同事之间也开始变得有话题了,因为大家也喜欢打球,后来还拉了一个篮球群周末有时间大家也会约出来一起打打球
当你有存在价值后一切的人情世故和人际关系都会变得简单起来
在这个世界上大多数时候除了你的父母等直系亲属和另一半,可能会对你无条件的付出
其余任何人对你尊重和示好,可能都会存在等价的利益交换吧
尤其是在技术研发的岗位,只有当你能够完全胜任这份工作时,并且能够体现出足够的价值时才能够有足够的话语权
入职三个月后的感受
- 公司待遇:虽然是一个集团下面的子公司 (200+人)但待遇只能说一般吧,除了工资是我期望的薪资范围,其他的福利待遇都只能是很一般(私企嘛,懂得都懂)
- 工作强度: 听到过很多从大厂来的新同事抱怨说这边的工作量会很大,对我来说其实都还ok,毕竟之前在极端的高压环境下工作过
- 工作氛围:从我的角度来讲的话,还是很不错的,相处起来也很轻松简单,大家也有很多共同话题,没有之前在小公司上班那么累
大环境低迷下,随时做好被裁掉的准备
从2020年毕业工作以来,最长的一段工作经历是1年4个月,有过三家公司的经历
裁员原因也很简单:创业小公司和人力外包,要么就是小公司经营问题公司直接垮掉,或者就是人力外包公司卸磨杀驴
除非你是在国企单位上班,否则需要随时做好被裁掉的准备
什么都不怕,就怕太安逸了
这句话出自《我的团长我的团》电视剧里面龙文章故意对几十个过江的日本人围而不歼时和虞啸卿的对话,龙文章想通过这几十个日本人将禅达搅得鸡犬不宁,来唤醒还在沉睡在自己温柔乡的我们,因为就在我们放松警惕时日本人就已经将枪口和大炮对准了我们。
或许大家会很不认同这句话吧,如果你的父母给你攒下了足够的资本完全可以把我刚才的话当做放屁,毕竟没有哪一个男生毕业之前的梦想是车子和房子,从事自己喜欢的工作不好吗? 但现实却是你喜欢工作的收入很难让你在这座城市里体面的生活
于我而言前端行业的热爱更多是因为能够给我带来不错的收入所以我选择了热爱它吧,所以保持终身学习��状态也是我需要去做的吧
前端已死?
前端彻底死掉肯定是不会的,在前后端分离模式下的软件开发前端肯定是必不可少的一个岗位,只不过就业环境恶劣下的情况里肯定会淘汰掉很多人,不过35岁之后我还是否能够从事前端行业真的是一个未知数
结语
选择卷或者躺平,只是两种不同的生活态度没有对与错,偶尔躺累了起来卷一下也是可以的,偶尔卷累了躺起了休息一下也是不错的。
在这个网络上到处是人均年收入百万以及各种高质量生活的时代,保持独立思考,如何让自己不被负面情绪所影响才是最重要的吧
来源:juejin.cn/post/7391065678546157577
还学鸿蒙原生?vue3 + uniapp 可以直接开发鸿蒙啦!
Hello,大家好,我是 Sunday
7月20号,uniapp 官网“悄咪咪”的上线了 uniapp 开发鸿蒙应用 的文档,算是正式开启了 Vue3 + uniapp 开发鸿蒙应用
的时代。
开发鸿蒙的前置准备
想要使用 uniapp 开发鸿蒙,我们需要具备三个条件:
- DevEco-Studio 5.0.3.400 以上(下载地址:
https://developer.huawei.com/consumer/cn/deveco-studio/
) - 鸿蒙系统版本 API 12 以上 (DevEco-Studio有内置鸿蒙模拟器)
- HBuilderX-alpha-4.22 以上
PS: 这里不得不吐槽一下,一个 DevEco-Studio 竟然有 10 个 G......
安装好之后,我们就可以通过 开发工具 运行 示例代码
运行时,需要用到 鸿蒙真机或者模拟器。但是这里需要 注意: Windows系统需要经过特殊配置才可以启动,mac 系统最好保证系统版本在 mac os 12 以上
windows 系统配置方式(非 windows 用户可跳过):
打开控制面板 - 程序与功能 - 开启以下功能
- Hyper-V
- Windows 虚拟机监控程序平台
- 虚拟机平台
注意: 需要win10专业版或win11专业版才能开启以上功能,家庭版需先升级成专业版或企业版
启动鸿蒙模拟器
整个过程分为三步(中间会涉及到鸿蒙开发者申请):
- 下载 uni-app 鸿蒙离线SDK template-1.3.4.tgz (下载地址:
https://web-ext-storage.dcloud.net.cn/uni-app/harmony/zip/template-1.3.4.tgz
) - 解压刚下载的压缩包,将解压后的模板工程在 DevEco-Studio 中打开
- 等待 Sync 结束,再 启动鸿蒙模拟器 或 连接鸿蒙真机(如无权限,则需要申请(一般 3 个工作日),申请地址:
https://developer.huawei.com/consumer/cn/activity/201714466699051861/signup
)
配置 HBuilderX 吊起 DevEco-Studio
打开HBuilderX,点击上方菜单 - 工具 - 设置,在出现的弹窗右侧窗体新增如下配置
注意:值填你自己的 DevEco-Studio 启动路径
"harmony.devTools.path" : "/Applications/DevEco-Studio.app"
创建 uni-app 工程
- BuilderX 新建一个空白的 uniapp 项目,选vue3
- 在 manifest.json 文件中配置鸿蒙离线SDK路径(SDK 路径可在 DevEco-Studio -> Preferences(设置) z中获取)
编辑 manifest.json
文件,新增如下配置:
然后点击 运行到鸿蒙即可
总结
这样我们就有了一个初始的鸿蒙项目,并且可以在鸿蒙模拟器上运行。关于更多 uniapp 开发鸿蒙的 API,大家可以直接参考 uniapp 官方文档:https://zh.uniapp.dcloud.io/tutorial/harmony/dev.html#nativeapi
来源:juejin.cn/post/7395964591799025679
有财务自由的思维,才能实现财务自由!
前两天在洋哥、竹子姐以及渡心总等大佬的带领下,第一次体验了穷爸爸富爸爸的作者研发的现金流游戏,收获颇丰!
游戏规则说明
心灵创富 现金流游戏分为三步:
一局游戏,时间两个小时;总结分享时刻;以及,最最重要的结合自己的生活,复盘自己关键时间点的选择,是否是符合财务自由的决策。
首先,说一下明面上的游戏规则:每个人都选择一张身份卡,这张身份卡决定了你的工资,还有每个月的现金流。你的身份可能是小学老师、飞机驾驶员、医生等等,他们月工资和现金流(每月结余)各不一样。
老鼠圈,所有玩家没实现财务自由之前都在老鼠圈。
游戏过程中,通过投掷骰子,可以有五种操作:
- 市场风云:变幻莫测的市场,之前不值钱的突然变得值钱,之前值钱的东西也会突然变得不值钱;还有金融政策可能会随时调整,借钱利率忽高忽低等。
- 小买卖:开小店挣小钱等。
- 大机会:买卖股票、房产等能带来大额现金流的操作。
- 意外支出:生孩子、买球拍、买游艇等。
- 领工资:领取一个月的现金流,比如:月工资:3300,月支出:2100,则每月现金流:3300-2100=1200。
整个游戏的目标分为两个阶段:第一个阶段,突破老鼠圈,实现财务自由;第二个阶段,实现梦想。
怎么实现财务自由?股票、房产等带来的非工作收入超过你每个月的支出,就算破圈了,实现了财务自由,游戏进入了下一个阶段。
第一次玩游戏,没有人完成第二阶段,就不说了。
二、游戏
带领银行家:竹子
玩家:海子、木川、天雨、Feli、YY、伍六七
身份:小学老师
月工资:3300 $
月支出:2190 $
月现金流:1110 $
一开始选择身份的时候,虽然想选一个工资高的,但是也没有那么强烈,只是不想选工程师了,毕竟现实中是程序员,游戏中想换个身份活一回。所以,也就比较随意选择了工资偏低的小学教师。
整场游戏,我的运气非常不好。本来月现金流就不高,结果第二轮就生了个娃,后面又陆续抽中了额外支出,让本就没钱的我雪上加霜。
唯一称得上的是机会的就是:可以以 5000 $ 购买 10 亩荒地。但是当时没有考虑到可以向银行带款,也没有考虑到可以向其他玩家借款,最终没有购买。
没有考虑到向外部借钱的一部分原因是第一次玩这个游戏,不知道(忘记了)这个规则,另一个原因也是自己平时生活中也是这样一个人,除非迫不得已不向他人借钱。
第一次玩这个游戏,本着要恪守:10% - 20% 的钱投资高风险的产品(如股票),50% 的钱投资低风险的产品(当前最低价的股票、高收益的房产等)。
所以,我也做了两笔投资: 1000 $ 让木川代持的基金, 500 $ 让海子代持的股票。这让我在运气不好的一生中有一定的概率能破圈,虽然最后这两笔投资没有兑现,但是这两笔投资本身,我认为是没有问题的。
另外,太守规则。银行家一开始说的规则是不让给其他人提供决策建议,否则罚款。后来才知道,是可以向其他人提供付费资讯服务的,这种是不受惩罚的。
前两轮 FYY 想向我咨询决策建议,我直接就拒绝了。但是一个是给他人提供建议,可以给其他人好感,可以链接其他人。另外一个,就算有罚款,咨询的人也可以给予相应的补偿。这样就可以相互链接,相互成全。也应该思考,不破坏规则的情况下的有哪些选择。
整场游戏中,印象最深刻的一笔交易是:5000 $ 购买 10 亩荒地
我最终是没有购买,当时第一是看手里的现金,不满足购买要求。
第二是觉得这是一个可以搏一搏的机会,但是手里的现金不满足给自己制定的 10% -20% 投资高风险产品的 rule。
所以,问了在场的玩家,是否有需要这个机会的?拍卖 1000 $。最终,只有海子出价 500 $。当时,我认为这个机会价值还是挺大的,海子本质上是一个愿意花钱投资的人,所以拒绝了。
之后思考,海子是一个愿意投资机会的人,但是他当时手里有几笔房子的带款,而且现金也不充足,所以没有购买这个机会。我只考虑了海子的性格,没有考虑他当时手里的资金,所以,错失了这笔交易。
游戏和自己现实生活中的关系
什么才算财务自由?现实中很多人说有多少多少百万,有说 500 W的,有说 1000 W的。但是这个游戏告诉我们:只要你的非工作收入超过了你的支出,你就实现了财务自由。跟你手里有多少现金是没有关系的。
唯一的目标就是,增加你的非工作收入,减少你的支出,让你的非工作收入超过你的支出。
这个游戏带给我的收获和启发
人这一生,有的人运气好,能碰到很多次大机会。有的人运气不好,可能一生也没什么机会。
不管如何,你需要在一开始就制定好你做决策的依据。这个依据就是你手里的现金流以及你能承受的风险。
你不应该拿手里的大部分现金去投资一个低收益率高风险的产品,但是也不应该守着一大笔资金不做任何投资。
这个决策依据能够让你在没机会的时候,不至于很快破产。也能让你在有机会的时候,能够快速收获第一桶金,实现财务自由。
对我现实的改变
- 正在整理自己的权益表和资产负债表,慢慢减少不必要的支出。
- 正在实践做自己的个人 IP,增加自己的非工作收入。
- 游戏中的小买卖、大机会,在现实中去寻找这样的信息。游戏中,大家都知道且能知道是小买卖还是大机会,但是现实中,你可能不知道什么是小买卖,什么是大机会。
- 在心里植入增加非工作收入,减少支出,实现财务自由这样的理念。
应该去践行的
- 与现实决策点结合
- 早日争取第一桶金
- 结果导向
- 自己反思,反思自己的不足
- 持续来,每次来会有不同的体验,牌面不同,选择不同
- 应该多做利他的事情,资源链接
- 玩家之间可以互相赋能
- 有资源的时候可以投资机会,购买机会
- 整理自己的资产负债表
- 多分享,清晰自己的认识
- 重复玩,玩到财务自由
- 映射现实中,改善自己的财务状态
- 运气不好,心态也要好,积极链接他人,才能保住底线的情况下,去创造更大的成功机会
来源:juejin.cn/post/7293477092259201059
回县城躺平,感觉我的人生过得好失败
从春节前到现在,一个半月没更新了,做啥都提不起劲来。
越来越感觉我的人生过的好失败。
去年我爸因为癌症去世了,家里的门头房用不到了,就想卖掉,找好了买家,价格谈了 140 万。
当时想买我们这个房子的人很多,那个买家怕我们卖给别人,就拿了 20 万定金,和我们签了一个买房合同。
没想到因为这个房子因为在我爸名下,涉及到继承,需要我奶奶放弃继承才可以过户。
我奶奶现在生活不能自理,由我大娘二大娘养着,然后他们提出条件来,要我们的宅基地,也就是村里的老房子。
我妈开始不同意,因为她想之后从那里出殡,和我爸合葬。
后来也同意了,在哪不能殡出去呢?
然后我这边准备好了材料之后,我堂姐跳了出来,一拍桌说,不行,老房子我们也要,另外你还要给我们 25 万。
她们说咨询了律师要是打官司的话,我们青岛的房子也要分,那个门头房也要分,另外我爸的银行流水也要查,起码得分她们 40 万。
我给讲价到了 20 万,但是我妈不同意,说是她和我爸辛苦攒下的家业凭什么白给他们。
我妈这边也找了律师,给出的意见就是拖着就行,一辈子不卖了。
这时候买房子的不干了,说是合同上写了,如果违约,要双倍返回定金,也就是赔她们 20 万。
当时我们以为就是个收到条就签了,没想到在这等着呢。
其实我们早就把定金返给她了,也说了我们家的情况,但她就是不行。
年前就一直威胁我们要告,刚过完年,马上又来了:
我妈问了下和谈的话怎么谈,她说起诉你赔我 25 万,和谈赔我 18 万。
两头挤我们,家里就要我们老房子 + 21 万,卖房子的就要我们 20 万违约金。
我们夹在中间,几度濒临崩溃。
我想了下,这件事早晚得解决,反正都是一家人,给家里人点钱也没啥。
然后我前几天去找我大爷二大爷,还有堂姐、堂哥坐在一起谈了,我说我同意这 21 万。
最终转了她们 18 万(老房子折合了 2 万块),然后又拿了 1 万律师费(他们请的律师让我拿律师费),还有同意给他们老房子。
但我提的要求是和我妈只能说是 10 万 + 老房子,不然我妈不会同意的。
就这样,我们顺利公证过了户。
公证放弃继承那天,我奶奶才刚知道这个事,她说她只要老房子,不要我爸的其他遗产。
但没办法,她不要不行,我大爷二大爷要啊。
这个房子卖了,到手 120 万。
然后还有青岛的房子,这个房子我买的时候是 70万首付 + 100 万带款,一共是 200 万下来的,最近我妈又花了 7 万装修。
因为不去青岛住,也打算卖了,中介说价格不到 80 万还是可以卖掉的。
这么一算,这边亏了 120 万,那边房子卖了剩下 120,相当于我爸就给我留下了 70 多万还不好卖的房子。
其实我爸这辈子攒了不少钱,差不多 300 万,都是省出来的,从小我跟着他就是一天吃一顿那种,从没感觉到家里是有钱的。
再就是他对我妈也不好,前几年的时候经常打骂,后来我妈离家出走了,但是他生病了还是会来照顾他。
我爸癌症住院那段时间,生活不能自理,都是我妈没日没夜的照顾他。
临走之前,我爸一只手抓着我的手,一只手抓着我妈的手,然后让我们好好相处,他一直觉得对不起我妈,口里一直喊着“从头再来、从头再来”。
我送我爸去火葬场的时候,送我骨灰盒爸入土的时候,我也一直在说,“爸,你别怕,我们从头再来”。
其实我爸葬礼上,我没有咋哭出来,可能当时没大反应过来。
但是之后好长一段时间,我在村里别人家坐着聊天,一谈起我爸,就再也忍不住了,哭的稀里哗啦的。
我有个干兄弟,在村里拜了干爹,因为疫情好多年也没走动了,但是我爸的棺材是他帮忙抬出去的。
而我大爷二大爷就在一边看着。
我今年过年给他家送了礼,我说,我妈说我爸是你们抬出去的,让我一辈子记得你们的好。
当时说到这里,没忍住,又哭的稀里哗啦的。
我想我爸这辈子,是攒了不少钱,但是不舍得吃不舍得喝的,还在房子上亏了半辈子的积蓄。
对老婆孩子不好,临走前才后悔想着从头再来。
我想我前五年是赚了不少钱,但因为工作,好多年没回家,和家人一年待在一起也就几天。
而且最后都赔在青岛的房子上了。
人这一辈子,到底图啥呢?
年后这几周我找了找工作,有几个不错的 offer,都是 base 40+ 那种。
但我又不那么想出去了。
我这一年没工作,其实和我妈在一块生活还是很踏实的。
而且家里房子卖了,青岛的房子也快了,这样我的存款差不多能到 300w。存定期的话每年银行利息 8w 左右,够我生活了。
就这样在家躺平一辈子是不是也不错。
王小波说,那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消逝,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。
韩寒说,平凡才是唯一的答案。
小的时候,我希望长大后的自己会光芒万丈。
长大以后,我希望自己有个好的工作打工到退休。
现在的我只想躺平。
我觉得我自己的人生很失败:
打工这些年,钱都赔在房子上了。
我比较社恐,永远达不到我妈对我的会来事、会察言观色的期望。
人家都在大城市结婚生子、买房定居了,而我又回到了小县城。
当年和我同时入职高德的朋友都升 p7 了,我现在啥也不是:
我是 94 年的,今年就 30 了,人生的各种机会和可能性越来越少。
后半辈子,我应该就是在小县城躺平,度过余生了。
但文章我还是想继续写,毕竟人这一生总要坚持点什么。
来源:juejin.cn/post/7343503718183059471
文件上传你会吧?那帮我做个文件下载功能
大家好,又是我,大聪明,立志做个早起吃草的马儿。话说上回解决完部署的问题(部署完了,样式不生效差点让我这个前端仔背锅),我又感觉回到了眼神清澈的大聪明状态,直到今天产品跟我说:“听说你是文件上传高手?做过大文件上传?切片?断点续传?”,听完我一脸戒备和紧张,“难道我面试吹的牛皮被他发现了,现在要捅破了?”我正在犹豫要不要跟他摊牌说,我面试掺了水的时候,他又来了一句,“那帮我做个 文件下载 的功能吧”,我突然放松下来了,原来是要加需求呀,害我白担心一场。作为CVT工程师,这点事根本难不倒我。
好了下面开始我的CV大法。
首先找到后端协调,他让我返回一个file_id
,该file_id是我在文件上传到服务器存储的时候,后端返回给我的,通过此file_id,来找到对应的文件,很好很简单。
接着,看后端提供的文件下载接口,咱就是说,经历的少,不知道对不对,后端直接就是返回文件的字节流(bytes),除此之外没有任何信息,没有文件名,没有文件类型,去问了一下说就是这样的,咱也不敢多问
天无绝人之路,还好我在前端获取文件的时候能找到总的文件列表,通过遍历出来也能拿到文件的信息
下面是判断文件类型方法
getFileTypeMime (key) {
let mimeType = ''
switch (key) {
case 'png':
mimeType = 'image/png'
break
case 'jpeg':
mimeType = 'image/jpeg'
break
case 'pdf':
mimeType = 'application/pdf'
break
case 'xlsx':
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
break
case 'docx':
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
break
default:
mimeType = 'text/plain'
break
}
return mimeType
},
下面是文件下载的方法 (错误的)
downLoad (value, key) {
const type = key.split('.')[1] // 文件类型
const name = key.split('.')[0] // 文件名
axiosBase({
url: `/ass/download?alert_id=${this.alert_id}&file_id=${value}`,
method: 'get',
}).then(res => {
const blob = new Blob([res], { type: this.getFileTypeMime(type) }) // 文件类型
const link = document.createElement('a')
const href = window.URL.createObjectURL(blob) // 创建下载的链接
link.href = href
link.download = name // 下载后文件名
document.body.appendChild(link)
link.click() // 点击下载
document.body.removeChild(link) // 下载完成移除元素
window.URL.revokeObjectURL(href) // 释放掉blob对象
}).catch((err) => { console.log(err) })
}
为什么是错误的呢?点了效果也确实是实现了 文件的下载,但是打开,然后格式错误了??
又是百思不得其解的问题,直接打开度娘,搜索又找到了这篇(神文)解决
downLoad (value, key) {
const type = key.split('.')[1] // 文件类型
const name = key.split('.')[0] // 文件名
axiosBase({
url: `/download?alert_id=${this.alert_id}&file_id=${value}`,
method: 'get',
responseType: 'blob'
}).then(res => {
const blob = new Blob([res], { type: this.getFileTypeMime(type) }) // 文件类型
const link = document.createElement('a')
const href = window.URL.createObjectURL(blob) // 创建下载的链接
link.href = href
link.download = name // 下载后文件名
document.body.appendChild(link)
link.click() // 点击下载
document.body.removeChild(link) // 下载完成移除元素
window.URL.revokeObjectURL(href) // 释放掉blob对象
}).catch((err) => { console.log(err) })
}
又是有惊无险的一天
来源:juejin.cn/post/7389913027654434857
部署完了,样式不生效差点让我这个前端仔背锅
大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。
叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动物的好心情都没了。
部署?因为该公司人数比较少,也没有自动化部署操作,只能让前端build一下dist包然后丢给后端,让后端部署,然后该项目也比较急,我刚来,也不好意思@后端,恰巧跟我对接的后端也是刚来不熟悉,所以我就跟负责人继续扯皮拖延时间了。(真是不好意思,假的)↓↓↓
终于经过不断努力(后端),部署好了,但是,我的样式乱了,为什么呢?看了一下网络和源代码,样式文件都请求到了,问了后端,一致说是我前端的问题,我懵了,然后快马加鞭查找问题
解决
ok,那我先看打包的dist文件有样式有问题吗,使用npm安装个 http-server 运行打包后的index.html,运行打开运行后的地址,欧克样式没乱,文件引用也没问题,那到底是什么鬼影响的呢?
在我百思不得其解的时候,我打开了神器--csdn,搜索打包dist部署后样式文件不生效,就遇到了这个神文t.csdnimg.cn/WbQyq
欧克,按照博主说的Nginx没有配置这两个东西而导致的,我有点不确定是不是,有点犹犹豫豫的找到后端说帮我在Nginx配置文件的http加下这两个配置,然后样式就好了,完美!后端由于疏忽,没加上就把问题推给我,结果被我扳回一城。
include mime.types;
default_type application/octet-stream;
- include mime.types;
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
mime.types
文件,Nginx可以识别不同类型的文件并正确地处理它们。 - 示例:假设
mime.types
文件中定义了.html
文件为text/html
类型,Nginx在处理请求时会根据这个定义设置正确的HTTP响应头。
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
- default_type application/octet-stream;
- 这个指令设置了默认的MIME类型。如果Nginx无法根据文件扩展名或其他方式确定响应的MIME类型时,就会使用这个默认类型。
application/octet-stream
是一个通用的MIME类型,表示未知的二进制数据。当服务器无法识别文件类型时,会默认使用这个类型。例如,如果请求的文件没有合适的MIME类型或没有被mime.types
文件中列出,Nginx就会返回application/octet-stream
类型。- 这种设置对于确保未知文件类型的安全传输很有用,因为浏览器通常会下载这些文件而不是尝试在浏览器中打开它们。
总之,添加 include mime.types;
和 default_type application/octet-stream;
配置后,Nginx能够正确地识别和处理CSS文件的MIME类型,从而确保浏览器能够正确加载和应用CSS样式。
所以,前端仔不能只当前端仔,还是要好好学点服务端的知识,不然锅给你,你还认为这是你该背的锅。
以上是开玩笑的描述,只是为了吸引增加阅读量
来源:juejin.cn/post/7388696625689051170
前端更新部署后通知用户刷新
前言
周五晚上组里说前端有bug,正在吃宵夜的我眉头一紧,立即打开了钉钉(手贱...),看了一下这不是前几天刚解决的吗,果然,使用刷新大法就解决,原因不过是用户一直停留在页面上,新的版本发布后,没有刷新拿不到新的资源。
现在大部分的前端系统都是SPA,用户在使用中对系统更新无感知,切换菜单等并不能获取最新资源,如果前端是覆盖性部署,切换菜单请求旧资源,这个旧资源已经被覆盖(hash打包的文件),还会出现一直无响应的情况。
那么,当前端部署更新后,提示一直停留在系统中的用户刷新系统很有必要。
解决方案
- 在public文件夹下加入manifest.json文件,记录版本信息
- 前端打包的时候向manifest.json写入当前时间戳信息
- 在入口JS引入检查更新的逻辑,有更新则提示更新
- 路由守卫router.beforeResolve(Vue-Router为例),检查更新,对比manifest.json文件的响应头Etag判断是否有更新
- 通过Worker轮询,检查更新,对比manifest.json文件的响应头Etag判断是否有更新。当然你如果不在乎这点点开销,可不使用Worker另开一个线程
Public下的加入manifest.json文件
{
"timestamp":1706518420707,
"msg":"更新内容如下:\n--1.添加系统更新提示机制"
}
这里如果是不向用户提示更新内容,可不填,前段开发者也无需维护manifest.json的msg内容,这里主要考虑到如果用户在填长表单的时候,填了一大半,你这时候给用户弹个更新提示,用户无法判断是否影响当前表单填写提交,如果将更新信息展示出来,用户感知更新内容,可判断是否需要立即刷新,还是提交完表单再刷新。
webpack向manifest.json写入当前时间戳信息
// 版本号文件
const filePath = path.resolve(`./public`, 'manifest.json')
// 读取文件内容
readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件时出错:', err)
return
}
// 将文件内容转换JSON
const dataObj = JSON.parse(data)
dataObj.timestamp = new Date().getTime()
// 将修改后的内容写回文件
writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => {
if (err) {
console.error('写入文件时出错:', err)
return
}
})
})
如果你无需维护更新内容的话,可直接写入timestamp
// 生成版本号文件
const filePath = path.resolve(`./public`, 'manifest.json')
writeFileSync(filePath, `${JSON.stringify({ timestamp: new Date().getTime() })}`)
检查更新的逻辑
入口文件main.js处引入
我这里检查更新的文件是放在utils/checkUpdate
// 检查版本更新
import '@/utils/checkUpdate'
checkUpdate文件内容如下
import router from '@/router'
import { Modal } from 'ant-design-vue'
if (process.env.NODE_ENV === 'production') {
let lastEtag = ''
let hasUpdate = false
let worker = null
async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'head'
})
// 获取最新的etag
let etag = response.headers.get('etag')
hasUpdate = lastEtag && etag !== lastEtag
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
async function confirmReload(msg = '', lastEtag) {
worker &&
worker.postMessage({
type: 'pause'
})
try {
Modal.confirm({
title: '温馨提示',
content: '系统后台有更新,请点击“立即刷新”刷新页面\n' + msg,
okText: '立即刷新',
cancelText: '5分钟后提示我',
onOk() {
worker.postMessage({
type: 'destroy'
})
location.reload()
},
onCancel() {
worker &&
worker.postMessage({
type: 'recheck',
lastEtag: lastEtag
})
}
})
} catch (e) {}
}
// 路由拦截
router.beforeEach(async (to, from, next) => {
next()
try {
await checkUpdate()
if (hasUpdate) {
worker.postMessage({
type: 'destroy'
})
location.reload()
}
} catch (e) {}
})
// 利用worker轮询
worker = new Worker(
/* webpackChunkName: "checkUpdate.worker" */ new URL('../worker/checkUpdate.worker.js', import.meta.url)
)
worker.postMessage({
type: 'check'
})
worker.onmessage = ({ data }) => {
if (data.type === 'hasUpdate') {
hasUpdate = true
confirmReload(data.msg, data.lastEtag)
}
}
}
这里因为缺换路由本来就要刷新页面,用户可无需感知系统更新信息,直接通过请求头的Etag即可,这里的Fetch方法就用head获取相应头就好了。
checkUpdate.worker.js文件如下
let lastEtag
let hasUpdate = false
let intervalId = ''
async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'get'
})
// 获取最新的etag和data
let etag = response.headers.get('etag')
let data = await response.json()
hasUpdate = lastEtag !== undefined && etag !== lastEtag
if (hasUpdate) {
postMessage({
type: 'hasUpdate',
msg: data.msg,
lastEtag: lastEtag,
etag: etag
})
}
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
// 监听主线程发送过来的数据
addEventListener('message', ({ data }) => {
if (data.type === 'check') {
// 每5分钟执行一次
// 立即执行一次,获取最新的etag,避免在setInterval等待中系统更新,第一次获取的etag是新的,但是lastEtag还是undefined,不满足条件,错失刷新时机
checkUpdate()
intervalId = setInterval(checkUpdate,5 * 60 * 1000)
}
if (data.type === 'recheck') {
// 每5分钟执行一次
hasUpdate = false
lastEtag = data.lastEtag
intervalId = setInterval(checkUpdate, 5 * 60 * 1000)
}
if (data.type === 'pause') {
clearInterval(intervalId)
}
if (data.type === 'destroy') {
clearInterval(intervalId)
close()
}
})
如果不使用worker直接讲轮询逻辑放在checkUpdate即可
Worker引入
从 webpack 5 开始,你可以使用 Web Workers 代替 worker-loader
。
new Worker(new URL('./worker.js', import.meta.url));
以下版本的就只能用worker-loader
咯
也可以逻辑写成字符串,然后通过ToURL给new Worker,如下:
function createWorker(f) {
const blob = new Blob(['(' + f.toString() +')()'], {type: "application/javascript"});
const blobUrl = window.URL.createObjectURL(blob);
const worker = new Worker(blobUrl);
return worker;
}
createWorker(function () {
self.addEventListener('message', function (event) {
// 消费信息
self.postMessage('send message')
}, false);
})
worker数据通信
// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker 线程
self.onmessage = function (e) {
var uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。
如果要直接转移数据的控制权,就要使用下面的写法。
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
Web Worker 使用教程 - 阮一峰的网络日志 (ruanyifeng.com)
然而,并不是所有的对象都可以被转移。只有那些被设计为可转移的对象(用[ Transferable ] IDL 扩展属性修饰),比如ArrayBuffer、MessagePort,ImageBitmap,OffscreenCanvas,才能通过这种方式来传递。转移操作是不可逆的,一旦对象被转移,原始上下文中的引用将不再有效。转移对象可以显著减少复制数据所需的时间和内存。
---------------------------------更新-------------------------------------
如果只考虑是否更新,无需告知内容,无需创建manifest.json,直接通过index.html的Etag判断即可,因为打包工具会自动在filename添加hash,如果有内容修改,入口文件的js引入资源文件hash会改变,通过Etag判断即可。
async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/?v=${Date.now()}`, {
method: 'head',
cache: 'no-cache'
})
// 获取最新的etag
let etag = response.headers.get('etag')
hasUpdate = lastEtag && etag !== lastEtag
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
来源:juejin.cn/post/7329280514628534313
🎲选择困难症的福音-基于threejs+cannonjs的扔骰子小游戏
在一个美好的周末,闲来无事,约上朋友一起在家打麻将,奈何尘封已久的麻将包里翻来翻去也没找到骰子的踪影,于是想在网上找一个骰子模拟器来代替,找了半天都没有发现一款合适好用的软件,于是心血来潮,打算自己做一个🎲模拟器。
1.制定需求设计以及技术方案
1.骰子模型
- 用户可以选择多种骰子模型,如六面 15面 20面等不同风格的供选择
- 并且可以选择要投掷骰子的数量 暂定1-10
2.分数计算规则
- 每次投掷完后会自动计算总点数并显示在屏幕上
- 可以保存历史摇骰子记录 最多支持历史100条记录
- 可以自定义用户参与摇骰子,投掷完成后会显示:luke摇出了xx点。。并且保存在历史记录中
- 支持多人参与摇骰子比赛:比如先加入4名玩家,开始后会依次提示轮到哪位玩家来开始投掷,并且可以选择每局大家需要投掷的次数,最后会统计总点数以及排名。
3.动画特效
- 每次投掷时对骰子随机一个角度以及投掷方向,如果同时投掷多个骰子则随机每一个骰子的角度,给骰子施加一个向赌盘中心的力,让骰子随机落在赌盘中部,在赌盘周围增加一道空气墙来阻止骰子移动到牌桌外。
- 模拟不同骰子的落地音效
4.技术实现
- 利用threejs来实现webgl相关渲染。如骰子模型、场景渲染、相机、棋盘等。
- 利用cannosjs来模拟物理引擎。如骰子碰撞检测、抛出坠落动画、重力加速等物理效果。
2.效果展示
3.开始实现
准备3d骰子模型和棋盘素材
首先是找到合适的gltf模型,这里我们在sketchfab上找到了一款质感很真实的骰子模型。下载下来的模型可以通过gltf-viewer来查看模型效果。
棋盘的话其实是一张图片平铺起来的,这里我用了一张木地板图片。
引入资源以及初始化webgl场景
import * as CANNON from "cannon-es";
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//创建物理世界对象
const world = new CANNON.World();
// 设置物理世界重力加速度
world.gravity.set(0, -100, 0); //重力加速度: 单位:m/s²
创建物理模型、地面以及网格模型地面
这里简单叙述一下物理世界和webgl世界的联系以及如何在webgl场景里模拟出真实的物理效果。
- 首先在three创建的webgl场景是无法直接创建并感知到物理世界的,threejs只负责实时渲染物理的状态并展示在画布上。而cannos恰好相反,它不负责渲染,只负责创建一个物理世界以及具备物理引擎的物体,并根据物体状态实时计算物体的位置、角度等。并把这些信息实时同步给webgl场景中的模型,把模型渲染到页面上实现物理世界的可视化。
- 所以我们创建骰子、地面等模型都需要创建两份。一份在webgl中创建,一份在物理世界中创建,并且保持同样的尺寸。
//创建骰子网格模型(gltf模型)
const loader = new GLTFLoader();
const gltf = await loader.loadAsync('./assets/model/dice_model.glb');
const meshModel = gltf.scene;//获取箱子网格模型
meshModel.position.set(50,30,50);
meshModel.scale.set(5,5,5);
scene.add(meshModel);
//包围盒计算
const box3 = new THREE.Box3();
box3.expandByObject(meshModel);//计算模型包围盒
const size = new THREE.Vector3();
box3.getSize(size);//包围盒计算箱子的尺寸
// 创建骰子物理模型
const sphereMaterial = new CANNON.Material()//碰撞体材质
// 物理正方体
const bodyModel = new CANNON.Body({
mass: 0.3, // 碰撞体质量0.3kg
position: new CANNON.Vec3(50,30,50), // 位置
shape: new CANNON.Box(new CANNON.Vec3(size.x/2, size.y/2, size.z/2)),
material: sphereMaterial
});
// 物理地面
const groundMaterial = new CANNON.Material()
const groundBody = new CANNON.Body({
mass: 0, // 质量为0,始终保持静止,不会受到力碰撞或加速度影响
shape: new CANNON.Plane(),
material: groundMaterial,
});
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);//旋转规律类似threejs 平面
world.addBody(groundBody)
//设置物理世界参数
const contactMaterial = new CANNON.ContactMaterial(groundMaterial, sphereMaterial, {
restitution: 0.5, //反弹恢复系数
})
// 把关联的材质添加到物理世界中
world.addContactMaterial(contactMaterial)
// 网格地面
const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
const texture = new THREE.TextureLoader().load('./assets/textures/hardwood2_diffuse.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
const planeMaterial = new THREE.MeshLambertMaterial({
// color:0x777777,
map: texture,
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.rotateX(-Math.PI / 2);
scene.add(planeMesh);
创建好模型后,我们在创建光源,相机等,这里就不再赘述。接下来我们开始设计物理世界的骰子抛出后坠落效果,并将物理世界和webgl渲染同步。
//点击屏幕后,设置物理骰子的角度和速度,物理会向上抛出并随着重力下落,触碰到地面后则会发生碰撞反弹
renderer.domElement.addEventListener('click', function (event) {
start_throw = true;
clearPoints();
const randomEuler = Math.random()*3;
bodyModel.quaternion.setFromEuler(Math.PI / randomEuler, Math.PI / randomEuler, Math.PI / randomEuler);
bodyModel.position.set(0,50,0);//点击按钮,body回到下落的初始位置
// 为物体设置初始速度(用以产生抛物线效果)
bodyModel.velocity.set(option.x,option.y,option.z); // x, y, z 方向上的速度
// 选中模型的第一个模型,开始下落
world.addBody(bodyModel);
})
function render() {
world.step(1/60);//更新物理计算
meshModel.position.copy(bodyModel.position); //渲染循环中,同步物理球body与网格球mesh的位置
meshModel.quaternion.copy(bodyModel.quaternion); //同步姿态角度
locateView(); //相机跟随物体移动
requestAnimationFrame(render);
renderer.render(scene, camera);
if (isBodyStopped(bodyModel)&&start_throw) {
showPoints(); //停止运动后,显示点数
start_throw = false;
}
}
render();//根据帧数渲染
接下来,我们再添加骰子点数计算相关逻辑。
//获取朝上面的点数
function getUpperFace(mesh) {
// 定义每个面的局部法线向量(手动定义)
const localNormals = [
new THREE.Vector3(0, 1, 0), // 面1
new THREE.Vector3(0, 0, -1), // 面2
new THREE.Vector3(-1, 0, 0), // 面3
new THREE.Vector3(1, 0, 0), // 面4
new THREE.Vector3(0, 0, 1), // 面5
new THREE.Vector3(0, -1, 0), // 面6
];
let maxDot = -Infinity;
let faceValue = 0;
for (let i = 0; i < localNormals.length; i++) {
// 将局部法线向量转化为世界空间
const worldNormal = localNormals[i].clone().applyQuaternion(mesh.quaternion);
// 与全局上方向的点积
const dot = worldNormal.dot(new THREE.Vector3(0, 1, 0));
// 检查点积,找到最大值
if (dot > maxDot) {
maxDot = dot;
faceValue = i + 1; // 面的点数即为索引加1
}
}
return faceValue;
}
//判断物体是否停止运动
function isBodyStopped(body, linearVelocityThreshold = 0.1, angularVelocityThreshold = 0.1) {
// 获取物体的线性速度和角速度
const linearVelocityMagnitude = body.velocity.length();
const angularVelocityMagnitude = body.angularVelocity.length();
// 判断速度是否低于设定的阈值
return linearVelocityMagnitude < linearVelocityThreshold && angularVelocityMagnitude < angularVelocityThreshold;
}
//展示点数
function showPoints() {
let res = getUpperFace(meshModel);
let point = document.getElementById("points");
point.innerHTML = `点数:${res}`
}
//清空点数
function clearPoints() {
let point = document.getElementById("points");
point.innerHTML = ``
}
实现这些逻辑后,我们已经可以模拟出骰子抛出坠落并触碰地面后反弹,在停止运动后计算点数的效果。已经实现基础功能,但是我们发现如果随机速度过大的时候会移动很远才停下,于是我们增加一个空气墙来限制骰子在固定范围内。并且增加一个碰撞检测来触发撞地声音的效果。
//添加空气墙
const wallShape = new CANNON.Box(new CANNON.Vec3(100, 100, 0.1));
const wall1 = new CANNON.Body({ mass: 0 });
wall1.addShape(wallShape);
wall1.position.set(0, 100, -100); // Back wall
world.addBody(wall1);
const wall2 = new CANNON.Body({ mass: 0 });
wall2.addShape(wallShape);
wall2.position.set(0, 100, 100); // Front wall
world.addBody(wall2);
const wall3 = new CANNON.Body({ mass: 0 });
wall3.addShape(wallShape);
wall3.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall3.position.set(100, 100, 0); // Right wall
world.addBody(wall3);
const wall4 = new CANNON.Body({ mass: 0 });
wall4.addShape(wallShape);
wall4.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall4.position.set(-100, 100, 0); // Left wall
world.addBody(wall4);
//监听物体碰撞回调
const audio = new Audio('./assets/audio/peng.mp3');
bodyModel.addEventListener('collide', (event) => {
const contact = event.contact;
//获得沿法线的冲击速度
const ImpactV = contact.getImpactVelocityAlongNormal();
// 碰撞越狠,声音越大
if(ImpactV/35>1) {
audio.volume = 1;
} else {
audio.volume = ImpactV/35>0?ImpactV/35:0;
}
audio.currentTime = 0;
audio.play();
})
这样我们就初步完成了抛掷一个骰子并获取点数的功能,看似简单的一个场景实际上设计起来并不容易,要考虑很多因素。后续我会继续增加多个骰子同时抛掷的场景,以及比赛模式。源码也会贡献出来供大家一起学习参考,如果有更好的idea也可以在评论区留言或私信,大家一起在webgl中感受物理世界的魅力!
附完整代码:
import * as CANNON from "cannon-es";
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 实例化一个gui对象
// const gui = new GUI();
// //改变交互界面style属性
// gui.domElement.style.right = '0px';
// gui.domElement.style.width = '300px';
const option = {
z: -24,
x: -36,
y: -17,
z1: 1,
x1: 1,
y1: 1,
}
// //gui控制参数
// const folder_position = gui.addFolder('速度方向');
// folder_position.add(option, 'z', -100, 100);
// folder_position.add(option, 'x', -100, 100);
// folder_position.add(option, 'y', -100, 100);
// const folder_rotation = gui.addFolder('角度');
// folder_rotation.add(option, 'z1', -10, 10).step(0.1);
// folder_rotation.add(option, 'x1', -10, 10).step(0.1);
// folder_rotation.add(option, 'y1', -10, 10).step(0.1);
// CANNON.World创建物理世界对象
const world = new CANNON.World();
// 设置物理世界重力加速度
// world.gravity.set(0, -1000, 0); //重力加速度: 单位:m/s²
world.gravity.set(0, -100, 0);
//网格球体(gltf模型)
const loader = new GLTFLoader();
const gltf = await loader.loadAsync('./assets/model/dice_model.glb');
const meshModel = gltf.scene;//获取箱子网格模型
meshModel.position.set(50,30,50);
meshModel.scale.set(5,5,5);
scene.add(meshModel);
//包围盒计算
const box3 = new THREE.Box3();
box3.expandByObject(meshModel);//计算模型包围盒
const size = new THREE.Vector3();
box3.getSize(size);//包围盒计算箱子的尺寸
// 物理球体
const sphereMaterial = new CANNON.Material()//碰撞体材质
// 物理箱子
const bodyModel = new CANNON.Body({
mass: 0.3, // 碰撞体质量0.3kg
position: new CANNON.Vec3(50,30,50), // 位置
shape: new CANNON.Box(new CANNON.Vec3(size.x/2, size.y/2, size.z/2)),
material: sphereMaterial
});
// 骨骼辅助显示
const skeletonHelper = new THREE.SkeletonHelper(meshModel);
scene.add(skeletonHelper);
// world.addBody(bodyModel);
//添加空气墙
// Create air walls
const wallShape = new CANNON.Box(new CANNON.Vec3(100, 100, 0.1));
const wall1 = new CANNON.Body({ mass: 0 });
wall1.addShape(wallShape);
wall1.position.set(0, 100, -100); // Back wall
world.addBody(wall1);
const wall2 = new CANNON.Body({ mass: 0 });
wall2.addShape(wallShape);
wall2.position.set(0, 100, 100); // Front wall
world.addBody(wall2);
const wall3 = new CANNON.Body({ mass: 0 });
wall3.addShape(wallShape);
wall3.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall3.position.set(100, 100, 0); // Right wall
world.addBody(wall3);
const wall4 = new CANNON.Body({ mass: 0 });
wall4.addShape(wallShape);
wall4.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall4.position.set(-100, 100, 0); // Left wall
world.addBody(wall4);
camera.position.set(42,85,21)
camera.lookAt(0,10,0);
// 物理地面
const groundMaterial = new CANNON.Material()
const groundBody = new CANNON.Body({
mass: 0, // 质量为0,始终保持静止,不会受到力碰撞或加速度影响
shape: new CANNON.Plane(),
material: groundMaterial,
});
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);//旋转规律类似threejs 平面
world.addBody(groundBody)
//设置物理世界参数
const contactMaterial = new CANNON.ContactMaterial(groundMaterial, sphereMaterial, {
restitution: 0.5, //反弹恢复系数
})
// 把关联的材质添加到物理世界中
world.addContactMaterial(contactMaterial)
//光源设置
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
directionalLight.position.set(20, 100, 10);
scene.add(directionalLight);
// 网格地面
const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
const texture = new THREE.TextureLoader().load('./assets/textures/hardwood2_diffuse.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
const planeMaterial = new THREE.MeshLambertMaterial({
// color:0x777777,
map: texture,
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.rotateX(-Math.PI / 2);
scene.add(planeMesh);
// 添加一个辅助网格地面
// const gridHelper = new THREE.GridHelper(50, 50, 0x004444, 0x004444);
// scene.add(gridHelper);
var controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 允许阻尼效果
controls.dampingFactor = 0.25; // 阻尼系数
let start_throw = false;
renderer.domElement.addEventListener('click', function (event) {
start_throw = true;
clearPoints();
const randomEuler = Math.random()*3;
bodyModel.quaternion.setFromEuler(Math.PI / randomEuler, Math.PI / randomEuler, Math.PI / randomEuler);
bodyModel.position.set(0,50,0);//点击按钮,body回到下落的初始位置
// 为物体设置初始速度(用以产生抛物线效果)
bodyModel.velocity.set(option.x,option.y,option.z); // x, y, z 方向上的速度
// 选中模型的第一个模型,开始下落
world.addBody(bodyModel);
})
const audio = new Audio('./assets/audio/peng.mp3');
bodyModel.addEventListener('collide', (event) => {
const contact = event.contact;
//获得沿法线的冲击速度
const ImpactV = contact.getImpactVelocityAlongNormal();
// 碰撞越狠,声音越大
if(ImpactV/35>1) {
audio.volume = 1;
} else {
audio.volume = ImpactV/35>0?ImpactV/35:0;
}
audio.currentTime = 0;
audio.play();
})
//判断物体是否停止运动
function isBodyStopped(body, linearVelocityThreshold = 0.1, angularVelocityThreshold = 0.1) {
// 获取物体的线性速度和角速度
const linearVelocityMagnitude = body.velocity.length();
const angularVelocityMagnitude = body.angularVelocity.length();
// 判断速度是否低于设定的阈值
return linearVelocityMagnitude < linearVelocityThreshold && angularVelocityMagnitude < angularVelocityThreshold;
}
function showPoints() {
let res = getUpperFace(meshModel);
let point = document.getElementById("points");
point.innerHTML = `点数:${res}`
}
//相机跟随物体移动
function locateView() {
camera.position.x = meshModel.position.x;
camera.position.y = meshModel.position.y + 30;
camera.position.z = meshModel.position.z + 20;
camera.lookAt(meshModel.position)
}
function clearPoints() {
let point = document.getElementById("points");
point.innerHTML = ``
}
//获取朝上面的点数
function getUpperFace(mesh) {
// 定义每个面的局部法线向量(手动定义)
const localNormals = [
new THREE.Vector3(0, 1, 0), // 面1
new THREE.Vector3(0, 0, -1), // 面2
new THREE.Vector3(-1, 0, 0), // 面3
new THREE.Vector3(1, 0, 0), // 面4
new THREE.Vector3(0, 0, 1), // 面5
new THREE.Vector3(0, -1, 0), // 面6
];
let maxDot = -Infinity;
let faceValue = 0;
for (let i = 0; i < localNormals.length; i++) {
// 将局部法线向量转化为世界空间
const worldNormal = localNormals[i].clone().applyQuaternion(mesh.quaternion);
// 与全局上方向的点积
const dot = worldNormal.dot(new THREE.Vector3(0, 1, 0));
// 检查点积,找到最大值
if (dot > maxDot) {
maxDot = dot;
faceValue = i + 1; // 面的点数即为索引加1
}
}
return faceValue;
}
function render() {
world.step(1/60);//更新物理计算
meshModel.position.copy(bodyModel.position); //渲染循环中,同步物理球body与网格球mesh的位置
meshModel.quaternion.copy(bodyModel.quaternion); //同步姿态角度
locateView(); //相机跟随物体移动
requestAnimationFrame(render);
renderer.render(scene, camera);
if (isBodyStopped(bodyModel)&&start_throw) {
showPoints(); //停止运动后,显示点数
start_throw = false;
}
}
render();
来源:juejin.cn/post/7394993393125064704
谈谈国内前端的三大怪啖
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。
今天聊三个事情:
- 小程序
- 微前端
- 模块加载
小程序
每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。
“我们为什么需要小程序?”
第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。
于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?
说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。
即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:
看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。
但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。
所以从某种程度上来看,这更像是一场截胡的商业案例:
应用市场
全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。
只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。
反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。
另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。
在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?
毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)
那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。
那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?
于是,在 19 年夏天,滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...
全新体验心智
小程序用起来挺方便的。
你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?
- 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。
- 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂
- 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。
H5 | 小程序 |
---|---|
![]() | ![]() |
- 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。
我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。
而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。
心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。
打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。
我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。
很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。
管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。
不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。
当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。
小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。
但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。
不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。
小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。
微前端
qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?
我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。
先说下我的看法:
- 微前端,重在解决项目管理而不在用户体验。
- 微前端,解决不了该优化和需要规范的问题。
- 微前端,在挽救没想清楚 MPA 的 SPA 项目。
没有万能银弹
银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。
所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。
当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。
不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。
不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。
不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。
上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。
B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。
微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。
SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。
ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。
质疑 “墨守成规”,打开视野,深度把玩,理性消费。
分而治之
分治法,一个很基本的工程思维。
在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。
你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)
我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。
比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。
而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。
当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。
当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?
只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。
体验差异
从 SPA 再回 MPA,说了半天不又回去了么。
所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?
流畅的用户体验:
这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏。
但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。
以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。
因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。
这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。
所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。
离线访问 (PWA)
SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。
但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。
也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。
项目协同、代码复用
有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。
这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。
但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。
这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。
也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...
这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”。
如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。
项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。
这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。
模块加载
模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。
实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。
import * from *
我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。
模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。
比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。
比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。
在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。
当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。
有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。
题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、.Net 直接返回带有数据的 HTML Ajax 一样的事情么。
传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。
但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...
到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。
“但我们用不了,有兼容性问题。”
哇哦,当我看着大家随意写出的 display: grid
样式定义,不禁再次感叹人们对未知的恐惧。
import.meta
的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…
试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。
模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史
历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。
结语
文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?
因为我们的智慧需要有开花的土壤,国内这千千万开发者的抱负需要有地方释放。
如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。
不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。
希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...
来源:juejin.cn/post/7267091810366488632
为什么删除node_modules文件夹那么慢
Windows系统 为什么删除node_modules文件夹那么慢?
在Windows系统中删除node_modules
文件夹可能会比较慢的原因有以下几点:
- 文件数量过多:
node_modules
文件夹通常包含大量的文件和文件夹,如果其中文件数量过多,系统需要逐一扫描并删除每个文件,这会导致删除过程变得缓慢。 - 文件路径过长:在Windows系统中,文件路径的长度有限制,如果
node_modules
文件夹中存在过长的文件路径,系统在删除这些文件时可能会变得缓慢。 - 文件占用:
node_modules
文件夹中可能包含一些被其他程序占用的文件,这会导致系统无法立即删除这些文件,从而延长删除时间。 - 磁盘速度:如果
node_modules
文件夹位于机械硬盘上而非固态硬盘,机械硬盘的读写速度相对较慢,也会影响删除操作的速度。 - 杀软扫描:有些杀毒软件在删除文件时会对文件进行扫描,以确保文件不包含恶意代码。这个额外的扫描过程也会增加删除文件的时间。
为什么在苹果系统上删除node_modules文件夹就很快?
- 文件系统差异:Windows采用的是NTFS文件系统,而macOS使用的是APFS文件系统,APFS 在快速复制、文件元数据管理、空间分配等方面具有优势,支持快速文件复制、快速目录大小计算、快速空间释放等功能,而 NTFS 和 exFAT 在某些方面可能不如 APFS 那么快速和高效。
- 文件路径处理:Windows对文件路径长度有限制,而macOS对文件路径长度的限制相对较宽松。如果
node_modules
文件夹中存在过长的文件路径,Windows系统在处理这些文件时可能会变得缓慢。 - 文件锁定:Windows系统在处理被其他程序占用的文件时,可能会出现文件锁定的情况,导致删除操作变得缓慢。而macOS系统在这方面可能更加灵活。
- 文件系统碎片:Windows系统在长时间使用后可能会产生文件系统碎片,这会影响文件的读写和删除速度。而macOS对文件系统碎片的处理可能更加高效。
Windows中删除慢解决方案
为了加快在Windows系统中删除文件夹的速度,可以尝试使用命令行删除、关闭占用文件的程序、使用专门的删除工具等方法,以提高删除效率。
- 在删除前关闭占用文件的程序:确保
node_modules
文件夹中的文件没有被其他程序占用,可以提前关闭相关程序再进行删除操作。 - 使用固态硬盘:如果可能的话,将
node_modules
文件夹放在固态硬盘上,可以显著提高文件的读写速度。 - 使用命令行删除:在命令行中使用
rd /s /q node_modules
命令可以快速删除node_modules
文件夹,避免Windows资源管理器中的删除操作。 - 使用专门的删除工具:例如 npm 全局安装 rimraf,以后直接使用删除命令即可。
npm install rimraf -g
~
rimraf node_modules/
来源:juejin.cn/post/7350107540325875721
什么是系统的鲁棒性?
嗨,你好啊,我是猿java
现实中,系统面临的异常情况和不确定性因素是不可避免的。例如,网络系统可能会遭受网络攻击、服务器宕机等问题;金融系统可能会受到市场波动、黑天鹅事件等因素的影响;自动驾驶系统可能会遇到天气恶劣、道路状况复杂等情况。
在这些情况下,系统的鲁棒性就显得尤为重要,它能够确保系统能够正确地处理各种异常情况,保持正常运行。因此,这篇文章我们将分析什么是系统的鲁棒性?如何保证系统的鲁棒性?
什么是系统的鲁棒性?
鲁棒性,英文为 Robustness
,它是一个多学科的概念,涉及控制理论、计算机科学、工程学等领域。
在计算机领域,系统的鲁棒性是指系统在面对各种异常情况和不确定性因素时,仍能保持稳定运行和正常功能的能力。
鲁棒性是系统稳定性和可靠性的重要指标,一个具有良好鲁棒性的系统能够在遇到各种异常情况时做出正确的响应,不会因为某些异常情况而导致系统崩溃或失效。
鲁棒性要求系统在在遇到各种异常情况都能正常工作,各种异常很难具像化,这看起来是一种比较理想的情况,那么系统的鲁棒性该如何评估呢?
系统鲁棒性的评估
系统的鲁棒性可以从多个方面来考虑和评估,这里主要从三个方面进行评估:
首先,系统的设计和实现应该考虑到各种可能的异常情况,并采取相应的措施来应对
例如,在网络系统中,可以采用防火墙、入侵检测系统等技术来保护系统免受网络攻击;在金融系统中,可以采用风险管理技术来降低市场波动对系统的影响;在自动驾驶系统中,可以采用传感器融合、路径规划等技术来应对复杂的道路状况。
其次,系统在面临异常情况时应该具有自我修复和自我调整的能力
例如,当网络系统遭受攻击时,系统应该能够及时发现并隔离攻击源,同时自动恢复受影响的服务;当金融系统受到市场波动影响时,系统应该能够自动调整投资组合,降低风险;当自动驾驶系统面临复杂道路状况时,系统应该能够根据实时的道路情况调整行驶策略。
此外,系统的鲁棒性还包括对数据异常和不确定性的处理能力
在现实生活中,数据往往会存在各种异常情况,例如数据缺失、噪声数据等。系统应该能够对这些异常数据进行有效处理,保证系统的正常运行。同时,系统也应该能够对数据的不确定性进行有效处理,例如通过概率模型、蒙特卡洛方法等技术来处理数据不确定性,提高系统的鲁棒性。
鲁棒性的架构策略
对于系统的鲁棒性,有没有一些可以落地的策略?
如下图,展示了一些鲁棒性的常用策略,核心思想是:事前-事中-事后!
预防故障(事前)
对于技术人员来说,要有防范未然的意识,因此,对于系统故障要有预防措施,主要的策略包括:
- 代码质量:绝大部分软件系统是脱离不了代码,因此代码质量是预防故障很核心的一个前提。
- 脱离服务:脱离服务(Removal from service)这种策略指的是将系统元素临时置于脱机状态,以减轻潜在的系统故障。
- 替代:替代(Substitution)这种策略使用更安全的保护机制-通常是基于硬件的-用于被视为关键的软件设计特性。
- 事务:事务(Transactions)针对高可用性服务的系统利用事务语义来确保分布式元素之间交换的异步消息是原子的、一致的、隔离的和持久的。这四个属性被称为“ACID属性”。
- 预测模型:预测模型(Predictive model.)结合监控使用,用于监视系统进程的健康状态,以确保系统在其标称操作参数内运行,并在检测到预测未来故障的条件时采取纠正措施。
- 异常预防:异常预防(Exception prevention)这种策略指的是用于防止系统异常发生的技术。
- 中止:如果确定某个操作是不安全的,它将在造成损害之前被中止(Abort)。这种策略是确保系统安全失败的常见策略。
- 屏蔽:系统可以通过比较几个冗余的上游组件的结果,并在这些上游组件输出的一个或多个值不同时采用投票程序,来屏蔽(Masking)故障。
- 复盘:复盘是对事故的整体分析,发现问题的根本原因,查缺补漏,找到完善的方案。
检测故障(事中)
当故障发生时,在采取任何关于故障的行动之前,必须检测或预测故障的存在,故障检测策略主要包括:
- 监控:监控(Monitor)是用于监视系统的各个其他部分的健康状态的组件:处理器、进程、输入/输出、内存等等。
- **Ping/echo:**Ping/echo是指在节点之间交换的异步请求/响应消息对,用于确定通过相关网络路径的可达性和往返延迟。
- 心跳:心跳(Heartbeat)是一种故障检测机制,它在系统监视器和被监视进程之间进行周期性的消息交换。
- 时间戳:时间戳(Timestamp)这种策略用于检测事件序列的不正确性,主要用于分布式消息传递系统。
- 条件监测:条件检测(Condition monitoring.)这种策略涉及检查进程或设备中的条件或验证设计过程中所做的假设。
- 合理性检查:合理性检查(Sanity checking)这种策略检查特定操作或计算结果的有效性或合理性。
- 投票:投票(Voting)这种策略的最常见实现被称为三模块冗余(或TMR),它使用三个执行相同操作的组件,每个组件接收相同的输入并将其输出转发给投票逻辑,用于检测三个输出状态之间的任何不一致。
- 异常检测:异常检测(Exception detection)这种策略用于检测改变执行正常流程的系统状态。
- 自检测:自检测(Self-test)要求元素(通常是整个子系统)可以运行程序来测试自身的正确运行。自检测程序可以由元素自身启动,或者由系统监视器不时调用。
故障恢复(事后)
故障恢复是指系统出现故障之后如何恢复工作。这是对团队应急能力的一个极大考验,如何在系统故障之后,将故障时间缩小到最短,将事故损失缩减到最小?这往往决定了一个平台,一个公司的声誉,决定了很多技术人员的去留。故障恢复的策略主要包括:
- 冗余备用:冗余备用(Redundant spare)有三种主要表现形式:主动冗余(热备用)、被动冗余(温备用)和备用(冷备用)。
- 回滚:回滚(Rollback)允许系统在检测到故障时回到先前已知良好状态,称为“回滚线”——回滚时间。
- 异常处理:异常处理(Exception handling)要求在检测到异常之后,系统必须以某种方式处理它。
- 软件升级:软件升级(Software upgrade)的目标是在不影响服务的情况下实现可执行代码映像的在线升级。
- 重试:重试(Retry)策略假定导致故障的故障是暂时的,重试操作可能会取得成功。
- 忽略故障行为:当系统确定那些消息是虚假的时,忽略故障行为(Ignore faulty behavior)要求忽略来自特定来源的消息。
- 优雅降级:优雅降级(Graceful degradation)这种策略在元素故障的情况下保持最关键的系统功能,放弃较不重要的功能。
- 重新配置:使用重新配置(Reconfiguration),系统尝试通过将责任重新分配给仍在运行的资源来从系统元素的故障中恢复,同时尽可能保持关键功能。
上述这些策略看起来很高大上,好像离你很远,但是其实很多公司都有对应的措施,比如:系统监控,系统告警,数据备份,分布式,服务器集群,多活,降级策略,熔断机制,复盘等等,这些术语应该就和我们的日常开发息息相关了。
总结
系统的鲁棒性是指系统在面对各种异常情况和不确定性因素时,仍能保持稳定运行和正常功能的能力。系统鲁棒性看似一个理想的状态,却是业界一直追求的终极目标,比如,系统稳定性如何做到 5个9(99.999%),甚至是 6个9(99.9999%),这就要求技术人员时刻保持工匠精神、在自己的本职工作上多走一步,只有在各个相关岗位的共同协作下,才能确保系统的鲁棒性。
学习交流
如果你觉得文章有帮助,请帮忙点个赞呗,关注公众号:猿java
,持续输出硬核文章。
来源:juejin.cn/post/7393312386571370536
uniapp适配android、ios的引导页、首页布局
uniapp适配Android、Ios的引导页和首页布局
真是很久没来掘金写文章了,最近一直在学习Nest和Next这些后端知识,忙只是一方面,更多的还是懒吧。其实今年来北京工作之后,完全独立挑大梁来写app收获还是蛮多的,但是我一般忙完就完事,着急学习自己的东西去,没有把工作中遇到的一些问题及时总结。这点感觉很不好,以后尽量把工作中遇到的有价值的问题总结下来,也算是给自己这段时间工作的复习,也能锻炼自己的表达能力。
引导页
原型图和需求
需求大致是这样:一共有三页,每页有2-3组图片,产品想要炫酷的视觉效果
我接收到需求后,首先想的是gif图,于是让UI帮我做了一张12帧的gif,大家来感受一下效果
不知道大家感受怎么样,放到手机来模拟的时候有些模糊、有些卡顿,且占用空间很大,一张12帧的图片已经20M+,
整个应用不过才30M的情况下,绝对接受不了这种情况,于是我就放弃的gif,想要用代码来实现。
思路
留给我的开发时间并不多,只有半天,自己本身css能力一般,按照gif这样估计最多做出来一页,所以我和产品决定阉割掉一部分动效,做三页。
- UI负责把每条图片列表切图给我
- 引导页用
swiper
实现,这样页面切换动画可以省时间 - 第一页水平做动画两两一组,交替实现动画
- 第二页垂直做动画,交替实现
- 第三页原图和AI图在一个父盒子下,原图动态改变宽度来实现交替播放
- 每页文字和按钮通过
position:fixed
置底 - 最后一页手动加上滑动事件,可以不点击按钮进入首页
代码实现
- template布局
<view class="swiperLayout">
<swiper
:current="current"
class="swiper"
duration="350"
@change="change"
:indicator-active-color=" '#FFF272' "
:indicator-color="'#ccc'"
indicator-dots="true"
>
<swiper-item class="swiperItem">
<view class="itemLayout">
<image
class="img an1"
src="@/static/guide/guide1_1.png"
mode="scaleToFill"
/>
<image
class="img an2"
src="@/static/guide/guide1_2.png"
mode="scaleToFill"
/>
<image
class="img an1"
src="@/static/guide/guide1_3.png"
mode="scaleToFill"
/>
<image
class="img an2"
src="@/static/guide/guide1_4.png"
mode="scaleToFill"
/>
<view class="buttonBox">
<view class="title">海量模板</view>
<view class="button" @click="next(1)">下一步</view>
</view>
</view>
</swiper-item>
<swiper-item class="swiperItem">
<view class="itemLayout">
<view class="guide2Box">
<image
class="img2 an3"
src="@/static/guide/guide2_1.png"
mode="scaleToFill"
/>
<image
class="img2 an4"
src="@/static/guide/guide2_2.png"
mode="scaleToFill"
/>
<image
class="img2 an3"
src="@/static/guide/guide2_3.png"
mode="scaleToFill"
/>
</view>
<view class="buttonBox">
<view class="title">5000+云端照片存储</view>
<view class="button" @click="next(2)">下一步</view>
</view>
</view>
</swiper-item>
<swiper-item
class="swiperItem"
@touchstart="handlerStart($event)"
@touchmove="handerMove($event)"
>
<view class="itemLayout">
<view class="guide3">
<-- img3动态改变自己的宽度,来实现动画效果 -->
<image
class="img3 an5 z"
src="@/static/guide/guide3_1.png"
mode="aspectFill"
/>
<image
class="img4 "
src="@/static/guide/guide3_2.png"
mode="heightFix"
/>
</view>
<view class="buttonBox">
<view class="title">高清照片,无水印无广告</view>
<view class="button" @click="toIndex">继续</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
- css部分
.swiper {
width: 100vw;
height: 100vh;
background: #000;
.swiperItem {
.itemLayout {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 60rpx;
.img {
width: 220vw;
height: 35vw;
margin: 20rpx 0 0rpx 0;
}
.img2 {
width: 30vw;
height: 256vw;
}
.title {
color: $themeColor;
margin-top: 40rpx;
text-align: center;
font-size: 36rpx;
font-weight: 600;
margin-bottom: 40rpx;
}
.button {
background: $themeColor;
color: #000;
height: 88rpx;
line-height: 88rpx;
width: 88%;
text-align: center;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 600;
}
}
.guide2Box{
display: flex;
justify-content: space-evenly;
width: 100%;
overflow: hidden;
height: 70vh;
}
}
}
// 动画1 执行三秒 匀速 无限次 镜像执行
.an1 {
animation: guide1 3s linear infinite alternate-reverse ;
}
// 水平X轴正向
@keyframes guide1 {
from {
transform: translateX(0);
}
50% {
transform: translateX(200rpx);
}
to {
transform: translateX(400rpx);
}
}
.an2 {
animation: guide2 3s linear infinite alternate-reverse ;
}
// 水平X轴负向
@keyframes guide2 {
from {
transform: translateX(0);
}
50% {
transform: translateX(-200rpx);
}
to {
transform: translateX(-400rpx);
}
}
.an3 {
animation: guide3 3s linear infinite alternate-reverse ;
}
// 水平正向 但是起始点要给负数 不然会有空缺的部分
@keyframes guide3 {
from {
transform: translateY(-500rpx);
}
50% {
transform: translateY(-250rpx);
}
to {
transform: translateY(0rpx);
}
}
.an4 {
animation: guide4 3s linear infinite alternate-reverse ;
}
// 水平负向
@keyframes guide4 {
from {
transform: translateY(0);
}
50% {
transform: translateY(-250rpx);
}
to {
transform: translateY(-500rpx);
}
}
.buttonBox{
position: fixed;
bottom: 120rpx;
width: 80vw;
display: flex;
flex-direction: column;
align-items: center;
z-index: 999;
}
// 最后一页动画 父盒子开启相对定位
.guide3{
position: relative;
width: 100%;
height: 100%;
// 两张图片都开始绝对定位 一左一右分布
.img3{
position: absolute;
top: 0;
left: 0;
height: 147vw;
border-right: 12rpx solid #fff;
}
.img4{
position: absolute;
top: 0;
right: 0;
height: 147vw;
}
}
// img3 缩小自己的宽度来实现动画
.an5 {
animation: changeImg 2s linear infinite alternate-reverse;
}
@keyframes changeImg {
from {
width: 0%;
}
to {
width: 100%;
}
}
.z{
z-index: 99;
}
- js部分
data() {
return {
current: 0,
// 触摸事件用到的数据
touchInfo: {
touchX: "",
touchY: "",
},
};
},
methods: {
next(num) {
this.current = num;
},
change(e) {
this.current = e.detail.current;
},
toIndex() {
uni.switchTab({ url: "/pages/index/index" });
},
handlerStart(e) {
let { clientX, clientY } = e.changedTouches[0];
this.touchInfo.touchX = clientX;
this.touchInfo.touchY = clientY;
},
handerMove(e) {
let { clientX, clientY } = e.changedTouches[0];
let diffX = clientX - this.touchInfo.touchX,
diffY = clientY - this.touchInfo.touchY,
absDiffX = Math.abs(diffX),
absDiffY = Math.abs(diffY),
type = "";
if (absDiffX > 50 && absDiffX > absDiffY) {
type = diffX >= 0 ? "right" : "left";
}
if (absDiffY > 50 && absDiffX < absDiffY) {
type = diffY < 0 ? "up" : "down";
}
if(type === 'left'){
this.toIndex()
}
},
},
最终效果
首页布局
原型图和需求
- 画风
- 贴纸
- 换脸
上面三图均为UI设计。首页的模板接口截止到目前(7.22)一共三种类型:styler(画风)、sticker(贴纸)、face_swap(换脸),本来按照UI的设计来看,每个分类的样式应该是固定写死的,我只需要v-for去不同的组件就可以,正当我写了一半时,很快老板的需求又下来:每个分类可能会杂糅在一起。说白了就是某个分类里可能既有画风、又有换脸、又有贴纸
思路
- 分析需求
在一个父组件中渲染所有的数据,根据不同的type 进入不同的子组件,三个子组件分别对应画风、贴纸、换脸,其中贴纸数据中有一个
mode
字段,根据mode
展示轮播、九宫格、一大八小的布局,这其中一大八小最不好实现。
一大八小的布局
- 将数据中的九张模板图片进行分组(剔除第一张,因为第一张要做“一大”),分为两组布局是上下分布(
display:flex
)实现,同时将第一张和分组的view
盒子的父元素也要开启display:flex
- 编译到chrome调试 看html结构
- 代码
<scroll-view class="scroll_view" scroll-x="true">
<image
class="img"
:src="sceneItem.json_content.cover_image_list[0].path"
mode="scaleToFill"
/>
<view>
<view
class="Item_2"
v-for="(Item, index) in columnData"
:key="index"
>
<view v-for="item in Item" :key="item.id">
<image
class="ss"
:src="item.path"
mode="scaleToFill"
/>
</view>
</view>
</view>
</scroll-view>
...
computed:{
columnData() {
if (this.sceneItem.json_content.display_mode === "2") {
const setData = this.sceneItem.json_content.cover_image_list.filter(
(item, index) => index > 0
);
const resultArray = setData.reduce(
(acc, cur, index) => {
const targetIndex = index % 2;
acc[targetIndex].push(cur);
return acc;
},
Array.from(Array(2), () => [])
);
return resultArray;
}
},
}
...
::v-deep .uni-scroll-view-content {
display: flex;
}
.scroll_view {
white-space: nowrap;
.img {
min-width: 324rpx;
height: 324rpx;
border-radius: 24rpx;
margin-right: 24rpx;
}
.Item {
display: inline-block;
.img {
width: 324rpx;
height: 324rpx;
border-radius: 24rpx;
margin-right: 24rpx;
}
}
.Item_2 {
display: flex;
.ss {
width: 158rpx;
height: 158rpx;
margin-right: 12rpx;
border-radius: 16rpx;
}
}
}
实现效果
来源:juejin.cn/post/7394005582774960182
身份认证的尽头竟然是无密码 ?
概述
几乎所有的系统都会面临安全认证相关的问题,但是安全相关的问题是一个很麻烦的事情。因为它不产生直接的业务价值,而且处理起来复杂繁琐,所以很多时都容易被忽视。很多后期造成重大的安全隐患,往往都是前期的不重视造成的。但庆幸的是安全问题是普遍存在的,而且大家面临的问题几乎相同,所以可以制定行业标准来规范处理,甚至是可以抽出专门的基础设施(例如:AD、LDAP 等)来专门解决这类共性的问题。总之,关于安全问题非常复杂而且麻烦,对于大多数 99% 的系统来说,不要想着在安全问题领域上搞发明和创新,容易踩坑。而且行业的标准解决方案已经非常成熟了。经过长时间的检验。所以在安全领域,踏踏实实的遵循规范和标准就是最好的安全设计。
HTTP 认证
HTTP 认证协议的最初是在 HTTP/1.1标准中定义的,后续由 IETF 在 RFC 7235 中进行完善。HTTP 协议的主要涉及两种的认证机制。
基本认证
常见的叫法是 HTTP Basic,是一种对于安全性不高,以演示为目的的简单的认证机制(例如你家路由器的登录界面),客户端用户名和密码进行 Base64 编码(注意是编码,不是加密)后,放入 HTTP 请求的头中。服务器在接收到请求后,解码这个字段来验证用户的身份。示例:
GET /some-protected-resource HTTP/1.1
Host: example.com
Authorization: Basic dXNlcjpwYXNzd29yZA==
虽然这种方式简单,但并不安全,因为 base64
编码很容易被解码。建议仅在 HTTPS 协议下使用,以确保安全性。
摘要认证
主要是为了解决 HTTP Basic 的安全问题,但是相对也更复杂一些,摘要认证使用 MD5 哈希函数对用户的密码进行加密,并结合一些盐值(可选)生成一个摘要值,然后将这个值放入请求头中。即使在传输过程中被截获,攻击者也无法直接从摘要中还原出用户的密码。示例:
GET /dir/index.html HTTP/1.1
Host: example.com
Authorization: Digest username="user", realm="example.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"
**补充:**另在 RFC 7235 规范中还定义当用户没有认证访问服务资源时应返回 401 Unauthorized
状态码,示例:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area"
这一规范目前应用在所有的身份认证流程中,并且沿用至今。
Web 认证
表单认证
虽然 HTTP 有标准的认证协议,但目前实际场景中大多应用都还是基于表单认证实现,具体步骤是:
- 前端通过表单收集用户的账号和密码
- 通过协商的方式发送服务端进行验证的方式。
常见的表单认证页面通常如下:
html>
<html>
<head>
<title>Login Pagetitle>
head>
<body>
<h2>Login Formh2>
<form action="/perform_login" method="post">
<div class="container">
<label for="username"><b>Usernameb>label>
<input type="text" placeholder="Enter Username" name="username" required>
<label for="password"><b>Passwordb>label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Loginbutton>
div>
form>
body>
html>
为什么表单认证会成为主流 ?主要有以下几点原因:
- 界面美化:开发者可以创建定制化的登录界面,可以与应用的整体设计风格保持一致。而 HTTP 认证通常会弹出一个很丑的模态对话框让用户输入凭证。
- 灵活性:可以在表单里面自定义更多的逻辑和流程,比如多因素认证、密码重置、记住我功能等。这些功能对于提高应用的安全性和便利性非常重要。
- 安全性:表单认证可以更容易地结合现代的安全实践,背后也有 OAuth 2 、Spring Security 等框架的主持。
表单认证传输内容和格式基本都是自定义本没啥规范可言。但是在 2019 年之后 web 认证开始发布标准的认证协议。
WebAuthn
WebAuthn 是一种彻底抛弃传统密码的认证,完全基于生物识别技术和实体密钥作为身份识别的凭证(有兴趣的小伙伴可以在 github 开启 Webauhtn 的 2FA 认证体验一下)。在 2019 年 3 月,W3C 正式发布了 WebAuthn 的第一版规范。
相比于传统的密码,WebAuthn 具有以下优势:
- 减少密码泄露:传统的用户名和密码登录容易受到钓鱼攻击和数据泄露的影响。WebAuthn,不依赖于密码,不存在密码丢失风险。
- 提高用户体验:用户不需要记住复杂的密码,通过使用生物识别等方式可以更快捷、更方便地登录。
- 多因素认证:WebAuthn 可以作为多因素认证过程中的一部分,进一步增强安全性。使用生物识别加上硬件密钥的方式进行认证,比短信验证码更安全。
总的来说,WebAuthn 是未来的身份认证方式,通过提供一个更安全、更方便的认证方式,目的是替代传统的基于密码的登录方法,从而解决了网络安全中的一些长期问题。WebAuthn 目前已经得到流程的浏览器厂商(Chrome、Firefox、Edge、Safari)、操作系统(WIndows、macOS、Linux)的广泛支持。
实现效果
当你的应用接入 WebAuthn 后,用户便可以通过生物识别设备进行认证,效果如下:
实现原理
WebAuthn 实现较为复杂,这里不做详细描述,具体可参看权威的官方文档,大概交互过程可以参考以下时序图:
登录流程大致可以分为以下步骤:
- 用户访问登录页面,填入用户名后即可点击登录按钮。
- 服务器返回随机字符串 Challenge、用户 UserID。
- 浏览器将 Challenge 和 UserID 转发给验证器。
- 验证器提示用户进行认证操作。
- 服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。
WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能;
备注:你可以通过访问 webauthn.me 了解到更多消息的信息
文章不适合加入过多的演示代码,想要手上体验的可以参考 okta 官方给出基于 Java 17 和 Maven 构建的 webauthn 示例程序,如下:
来源:juejin.cn/post/7354632375446061083
如何优雅的给SpringBoot部署的jar包瘦身?
一、需求背景
我们知道Spring Boot项目,是可以通过java -jar 包名 启动的。
那为什么Spring Boot项目可以通过上述命令启动,而其它普通的项目却不可以呢?
原因在于我们在通过以下命令打包时
mvn clean package
一般的maven项目的打包命令,不会把依赖的jar包也打包进去的,所以这样打出的包一般都很小
但Spring Boot项目的pom.xml文件中一般都会带有spring-boot-maven-plugin插件。
该插件的作用就是会将依赖的jar包全部打包进去。该文件包含了所有的依赖和资源文件。
也就会导致打出来的包比较大。
打完包就可以通过java -jar 包名 启动,确实是方便了。
但当一个系统上线运行后,肯定会有需求迭代和Bug修复,那也就免不了进行重新打包部署。
我们可以想象一种场景,线上有一个紧急致命Bug,你也很快定位到了问题,就改一行代码的事情,当提交代码并完成构建打包并交付给运维。
因为打包的jar很大,一直处于上传中.......
如果你是老板肯定会发火,就改了一行代码却上传几百MB的文件,难道没有办法优化一下吗?
如今迭代发布是常有的事情,每次都上传一个如此庞大的文件,会浪费很多时间。
下面就以一个小项目为例,来演示如何瘦身。
二、瘦身原理
这里有一个最基础 SpringBoot 项目,整个项目代码就一个SpringBoot启动类,单是打包出来的jar就有20多M;
我们通过解压命令,看下jar的组成部分。
tar -zxvf spring-boot-maven-slim-1.0.0.jar
我们可以看出,解压出来的包有三个模块
分为 BOOT-INF,META-INF,org 三个部分
打开 BOOT-INF
classes: 当前项目编译好的代码是放在 classes 里面的,classes 部分是非常小的。
lib: 我们所依赖的 jar 包都是放在 lib 文件夹下,lib部分会很大。
看了这个结构我们该如何去瘦身呢?
项目虽然依赖会很多,但是当版本迭代稳定之后,依赖基本就不会再变动了。
如果可以把这些不变的依赖提前都放到服务器上,打包的时候忽略这些依赖,那么打出来的Jar包就会小很多,直接提升发版效率。
当然这样做你肯定有疑问?
既然打包的时候忽略这些依赖,那通过java -jar 包名 还可以启动吗?
这种方式打的包,在项目启动时,需要通过-Dloader.path指定lib的路径,就可以正常启动
java -Dloader.path=./lib -jar xxx.jar
三、瘦身实例演示
1、依赖拆分配置
只需要在项目pom.xml文件中添加下面的配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar,
必须项目中的某些模块,会经常变动,那么就应该将其坐标写进来
如果没有则nothing ,表示不打包依赖 -->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
</plugin>
<!--拷贝依赖到jar外面的lib目录-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--指定的依赖路径-->
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
再次打包
mvn clean package
发现target目录中多了个lib文件夹,里面保存了所有的依赖jar。
自己业务相关的jar也只有小小的168kb,相比之前20.2M,足足小了100多倍;
这种方式打的包,在项目启动时,需要通过-Dloader.path指定lib的路径:
java -Dloader.path=./lib -jar spring-boot-maven-slim-1.0.0.jar
虽然这样打包,三方依赖的大小并没有任何的改变,但有个很大的不同就是我们自己的业务包和依赖包分开了;
在不改变依赖的情况下,也就只需要第一次上传lib目录到服务器,后续业务的调整、bug修复,在没调整依赖的情况下,就只需要上传更新小小的业务包即可;
2、自己其它项目的依赖如何处理?
我们在做项目开发时,除了会引用第三方依赖,也会依赖自己公司的其它模块。
比如
这种依赖自己其它项目的工程,也是会经常变动的,所以不宜打到外部的lib,不然就会需要经常上传更新。
那怎么做了?
其实也很简单 只需在上面的插件把你需要打进jar的填写进去就可以了
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar,如果没有则nothing -->
<includes>
<include>
<groupId>com.jincou</groupId>
<artifactId>xiaoxiao-util</artifactId>
</include>
</includes>
</configuration>
</plugin>
这样只有include中所有添加依赖依然会打进当前业务包中。
四、总结
使用瘦身部署,你的业务包确实小了 方便每次的迭代更新,不用每次都上传一个很大的 jar 包,从而节省部署时间。
但这种方式也有一个弊端就是增加了Jar包的管理成本,多人协调开发,构建的时候,还需要专门去关注是否有人更新依赖。
来源:juejin.cn/post/7260772691501301817
UNIAPP开发电视app教程
目前开发安卓TV的方法相对开说是十分的少的,使用uniapp开发相对来说几乎是没有的,因此,写下这篇文章供大家参考。
开发难点
- 如何方便的开发调试
- 如何使需要被聚焦的元素获取聚焦状态
- 如何使被聚焦的元素滚动到视图中心位置
- 如何在切换路由时,缓存聚焦的状态
- 如何启用wgt和apk两种方式的升级
一、如何方便的开发调试
之前我在论坛看到人家说,没办法呀,电脑搬到电视,然后调试。
其实大可不必,安装android studio里边创建一个模拟器就可以了。
注意:最好安装和电视系统相同的版本号,我这里是长虹电视,安卓9所以使用安卓9的sdk
二、如何使需要被聚焦的元素获取聚焦状态
uniapp的本质上是webview, 因此我们可以在它的元素上添加tabIndex, 就可以获取焦点了。
<view class="card" tabindex="0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">¥ {{ props.price }}</text>
</div>
</view>
</view>
.card {
border-radius: 1.25vw;
overflow: hidden;
}
.card:focus {
box-shadow: 0 0 0 0.3vw #fff, 0 0 1vw 0.3vw #333;
outline: none;
transform: scale(1.03);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
三、如何使被聚焦的元素滚动到视图中心位置
使用renderjs进行实现如下
<script module="homePage" lang="renderjs">
export default {
mounted() {
let isScrolling = false; // 添加一个标志位,表示是否正在滚动
document.body.addEventListener('focusin', e => {
if (!isScrolling) {
// 检查是否正在滚动
isScrolling = true; // 设置滚动标志为true
requestAnimationFrame(() => {
// @ts-ignore
e.target.scrollIntoView({
behavior: 'smooth', // @ts-ignore
block: e.target.dataset.index ? 'end' : 'center'
});
isScrolling = false; // 在滚动完成后设置滚动标志为false
});
}
});
}
};
</script>
就可以使被聚焦元素滚动到视图中心,requestAnimationFrame的作用是缓存
四、如何在切换路由时,缓存聚焦的状态
通过设置tabindex属性为0和1,会有不同的效果:
- tabindex="0":将元素设为可聚焦,并按照其在文档中的位置来确定焦点顺序。当使用Tab键进行键盘导航时,tabindex="0"的元素会按照它们在源代码中的顺序获取焦点。这可以用于将某些非交互性元素(如、等)设为可聚焦元素,使其能够被键盘导航。
- tabindex="1":将元素设为可聚焦,并将其置于默认的焦点顺序之前。当使用Tab键进行键盘导航时,tabindex="1"的元素会在默认的焦点顺序之前获取焦点。这通常用于重置焦点顺序,或者将某些特定的元素(如重要的输入字段或操作按钮)置于首位。
需要注意的是,如果给多个元素都设置了tabindex属性,那么它们的焦点顺序将取决于它们的tabindex值,数值越小的元素将优先获取焦点。如果多个元素具有相同的tabindex值,则它们将按照它们在文档中的位置来确定焦点顺序。同时,负数的tabindex值也是有效的,它们将优先于零和正数值获取焦点。
我们要安装缓存插件,如pinia或vuex,需要缓存的页面单独配置
import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({ home_active_tag: 'active0', hot_active_tag: 'hot0', dish_active_tag: 'dish0' })
});
更新一下业务代码
组件区域
<view class="card" :tabindex="home_active_tag === 'packagecard' + props.id ? 1 : 0">
<image :src="`${VITE_URL}${props.image}`" fade-show lazy-load mode="aspectFill"></image>
<view class="bottom">
<text class="name">{{ props.name }}</text> <text class="remark">{{ props.remark }}</text>
<div class="footer">
<view class="tags">
<text class="tag" v-for="tag in tags" :key="tag">{{ tag }}</text>
</view>
<text class="price">¥ {{ props.price }}</text>
</div>
</view>
</view>
const { home_active_tag } = storeToRefs(useGlobalStore());
页面区域
<view class="content">
<FoodCard
v-for="_package in list.dishes"
@click="goShopByFood(_package)"
:id="_package.id"
:name="_package.name"
:image="_package.image"
:tags="_package.tags"
:price="_package.price"
:shop_name="_package.shop_name"
:shop_id="_package.shop_id"
:key="_package.id"
></FoodCard>
<image
class="card"
@click="goMore"
:tabindex="home_active_tag === 'more' ? 1 : 0"
style="width: 29.375vw; height: 25.9375vw"
src="/static/home/more.png"
mode="aspectFill"
/>
</view>
const goShopByFood = async (row: Record<string, any>) => {
useGlobalStore().home_active_tag = 'foodcard' + row.id;
uni.navigateTo({
url: `/pages/shop/index?shop_id=${row.shop_id}`,
animationDuration: 500,
animationType: 'zoom-fade-out'
});
};
如果,要设置启动默认焦点 id和index可默认设置,推荐启动第一个焦点组用index,它可以确定
<view class="active">
<image
v-for="(active, i) in list.active"
:key="active.id"
@click="goActive(active, i)"
:tabindex="home_active_tag === 'active' + i ? 1 : 0"
:src="`${VITE_URL}${active.image}`"
data-index="0"
fade-show
lazy-load
mode="aspectFill"
class="card"
></image>
</view>
import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({
home_active_tag: 'active0', //默认选择
hot_active_tag: 'hot0',
dish_active_tag: 'dish0'
})
});
对于多层级的,要注意销毁,在前往之前设置默认焦点
const goHot = (index: number) => {
useGlobalStore().home_active_tag = 'hotcard' + index;
useGlobalStore().hot_active_tag = 'hot0';
uni.navigateTo({ url: `/pages/hot/index?index=${index}`, animationDuration: 500, animationType: 'zoom-fade-out' });
};
五、如何启用wgt和apk两种方式的升级
pages.json
{
"path": "components/update/index",
"style": {
"disableScroll": true,
"backgroundColor": "#0068d0",
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}
组件
<template>
<view class="update">
<view class="content">
<view class="content-top">
<text class="content-top-text">发现版本</text>
<image class="content-top" style="top: 0" width="100%" height="100%" src="@/static/bg_top.png"> </image>
</view>
<text class="message"> {{ message }} </text>
<view class="progress-box">
<progress
class="progress"
border-radius="35"
:percent="progress.progress"
activeColor="#3DA7FF"
show-info
stroke-width="10"
/>
<view class="progress-text">
<text>安装包正在下载,请稍后,系统会自动重启</text>
<text>{{ progress.totalBytesWritten }}MB/{{ progress.totalBytesExpectedToWrite }}MB</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import { reactive, ref } from 'vue';
const message = ref('');
const progress = reactive({ progress: 0, totalBytesExpectedToWrite: '0', totalBytesWritten: '0' });
onLoad((query: any) => {
message.value = query.content;
const downloadTask = uni.downloadFile({
url: `${import.meta.env.VITE_URL}/${query.url}`,
success(downloadResult) {
plus.runtime.install(
downloadResult.tempFilePath,
{ force: false },
() => {
plus.runtime.restart();
},
e => {}
);
}
});
downloadTask.onProgressUpdate(res => {
progress.progress = res.progress;
progress.totalBytesExpectedToWrite = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
progress.totalBytesWritten = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
});
});
</script>
<style lang="less">
page {
background: transparent;
.update {
/* #ifndef APP-NVUE */
display: flex; /* #endif */
justify-content: center;
align-items: center;
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
.content {
position: relative;
top: 0;
width: 50vw;
height: 50vh;
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: Source Han Sans CN;
border-radius: 2vw;
.content-top {
position: absolute;
top: -5vw;
left: 0;
image {
width: 50vw;
height: 30vh;
}
.content-top-text {
width: 50vw;
top: 6.6vw;
left: 3vw;
font-size: 3.8vw;
font-weight: bold;
color: #f8f8fa;
position: absolute;
z-index: 1;
}
}
}
.message {
position: absolute;
top: 15vw;
font-size: 2.5vw;
}
.progress-box {
position: absolute;
width: 45vw;
top: 20vw;
.progress {
width: 90%;
border-radius: 35px;
}
.progress-text {
margin-top: 1vw;
font-size: 1.5vw;
}
}
}
}
</style>
App.vue
import { onLaunch } from '@dcloudio/uni-app';
import { useRequest } from './hooks/useRequest';
import dayjs from 'dayjs'; onLaunch(() => {
// #ifdef APP-PLUS
plus.runtime.getProperty('', async app => { const res: any = await useRequest('GET', '/api/tv/app'); if (res.code === 2000 && res.row.version > (app.version as string)) { uni.navigateTo({ url: `/components/update/index?url=${res.row.url}&type=${res.row.type}&content=${res.row.content}`, fail: err => { console.error('更新弹框跳转失败', err); } }); } });
// #endif
});
如果要获取启动参数
plus.android.importClass('android.content.Intent');
const MainActivity = plus.android.runtimeMainActivity();
const Intent = MainActivity.getIntent();
const roomCode = Intent.getStringExtra('roomCode');
if (roomCode) {
uni.setStorageSync('roomCode', roomCode);
} else if (!uni.getStorageSync('roomCode') && !roomCode) {
uni.setStorageSync('roomCode', '8888');
}
来源:juejin.cn/post/7272348543625445437
Fuse.js一个轻量高效的模糊搜索库
最近逛github的时候发现了一个非常好用的轻量工具库,Fuse.js,支持模糊搜索。感觉还是非常好用的,所以有了此篇博客,这篇文章主要是介绍Fuse的使用,同样,我对这个开源项目的实现也非常感兴趣。后续会出一篇Fuse源码解析的文章来分析其实现原理。
Fuse.js是什么?
强大、轻量级的模糊搜索库,没有任何依赖关系。
什么是模糊搜索?
一般来说,模糊搜索(更正式的名称是近似字符串匹配)是查找与给定模式近似相等(而不是完全相等)的字符串的技术。
通常我们项目中的的模糊搜索大多数情况下有几种方案可用:
- 前端工程通过正则表达式或者字符串匹配来实现
- 调用后端接口去匹配搜索
- 使用搜索引擎如:ElasticSearch或Algolia等
但是这些方案都有各自的缺陷,比如正则表达式和字符串匹配的效率较低,且无法处理复杂的搜索需求,而调用后端接口和搜索引擎虽然效率高,但是需要额外的服务器资源,且需要维护一套搜索引擎。
所以,Fuse.js的出现就是为了解决这些问题,它是一个轻量级的模糊搜索库,没有依赖关系,支持复杂的搜索需求,且效率高,当然Fuse并不适用于所有场景。
Fuse.js的使用场景
它可能不适用于所有情况,但根据您的搜索要求,它可能是最理想的。例如:
- 当您想要对小型到中等大型数据集进行客户端模糊搜索时
- 当您无法证明设置专用后端只是为了处理搜索时
- ElasticSearch 或 Algolia 虽然都是很棒的服务,但对于您的特定用例来说可能有些过度
Fuse.js的使用
安装
Fuse支持多种安装方式
NPM
npm install fuse.js
Yarn
yarn add fuse.js
CDN 引入
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0">script>
引入
ES6 模块语法
import Fuse from 'fuse.js'
CommonJS 语法
const Fuse = require('fuse.js')
Tips: 使用npm或者yarn引入,支持两种模块语法引入,如果是使用cdn引入,那么Fuse将被注册为全局变量。直接使用即可
使用
以下是官网一个最简单的例子,只要简单的构造new Fuse对象,就能模糊搜索匹配到你想要的结果
// 1. List of items to search in
const books = [
{
title: "Old Man's War",
author: {
firstName: 'John',
lastName: 'Scalzi'
}
},
{
title: 'The Lock Artist',
author: {
firstName: 'Steve',
lastName: 'Hamilton'
}
}
]
// 2. Set up the Fuse instance
const fuse = new Fuse(books, {
keys: ['title', 'author.firstName']
})
// 3. Now search!
fuse.search('jon')
// Output:
// [
// {
// item: {
// title: "Old Man's War",
// author: {
// firstName: 'John',
// lastName: 'Scalzi'
// }
// },
// refIndex: 0
// }
// ]
从上述代码中可以看到我们要通过Fuse 对books的这个数组进行模糊搜索,构建的Fuse对象中,模糊搜索的key定义为['title', 'author.firstName'],支持对title及author.firstName这两个字段进行搜索。然后执行fuse的search API就能过滤出我们的期望结果。整体代码还是非常简单的。
高级配置
Demo示例只是提供了一个基础版本的模糊搜索。如果用户想获得更灵活的搜索能力,比如搜索结果排序、权重控制、搜索结果高亮等,那么就需要对Fuse进行一些高级配置。
Fuse的所有配置都是通过new Fuse时传入的参数来配置的,下面列举一些常用的配置项:
const options = {
keys: ['title', 'author'], // 指定搜索key值,可多选
isCaseSensitive: false, //是否区分大小写 默认为false
includeScore: false, //结果集中是否展示匹配项的分数字段, 分数越大代表匹配程度越低,区间值为0-1,注意:当此项为true时,会返回完整的结果集,只不过每一项中携带了score分数字段
includeMatches: false, //匹配项是否应包含在结果中。当时true,结果的每条记录都包含匹配项的索引。这个通常我们用来对搜索内容做高亮处理
threshold: 0.6, // 阈值控制匹配的敏感度,默认值为0.6,如果要完全匹配这里要设置为0
shouldSort: true, // 是否对结果进行排序
location: 0, // 匹配的位置,0 表示开头匹配
distance: 100, // 搜索的最大距离
minMatchCharLength: 2, // 最小匹配字符长度
};
出了上述常用的一些配置项之外,Fuse还支持更高阶模糊搜索,如权重搜索,嵌套搜索,运算符拓展搜索,具体高阶用法可以参考官方文档。
Fuse的主要实现原理是通过改写Bitap 算法(近似字符串匹配)算法的内部实现来支撑其模糊搜索的算法依据,后续会出一篇文章看一下作者源码的算法实现。
总结
Fuse的文章到此就结束了,你没看错就这么一点介绍就基本能支撑我们在项目中的应用,谢谢阅读,如果哪里有不对的地方请评论博主,会及时进行改正。
来源:juejin.cn/post/7393172686115569705
不使用代理,我是怎么访问Github的
背景
最近更换了 windows系统的电脑, git clone
项目的时候会连接超时的错误,不管我怎么把环境变量放到终端里尝试走代理都无果,于是开始了排查
以下命令是基于 git bash 终端使用的
检测问题
通过 ssh -T git@github.com
命令查看,会报如下错误:
ssh: connect to host github.com port 22: : Connection timed out
思索了一下,难道是端口的问题吗, 于是从 overflow 上找到回答:
修改 ~/.ssh/config
路径下的内容,增加如下
Host github.com
Hostname ssh.github.com
Port 443
这段配置实际上是让 github.com
走 443 端口去执行,评论上有些说 22端口被占用,某些路由器或者其他程序会占用它,想了一下有道理,于是使用 vim ~/.ssh/config
编辑加上,结果...
ssh: connect to host github.com port 443: : Connection timed out
正当我苦苦思索,为什么 ping github.com
超时的时候,脑子里突然回忆起那道久违的八股文面试题: “url输入网址到浏览器上会发生什么",突然顿悟:是不是DNS解析出了问题,找不到服务器地址?
网上学到一行命令,可以在终端里看DNS服务器的域名解析
nslookup baidu.com
先执行一下 baidu.com
的,得到如下:
Server: 119.6.6.6
Address: 119.6.6.6#53
Non-authoritative answer:
Name: baidu.com
Address: 110.242.68.66
Name: baidu.com
Address: 39.156.66.10
再执行一下 nslookup github.com
,果然发现不对劲了:
Name: github.com
Address: 127.0.0.1
返回了 127.0.0.1
,这不对啊,笔者可是读过书的,这是本地的 IP 地址啊,原来是这一步出了问题..
解决问题
大部分同学应该都改过本地的 DNS 域名映射文件,这也是上面那道八股文题中回答的知识点之一,我们打开资源管理器输入一下路径改一下:
C:\Windows\System32\drivers\etc\hosts
MacOs的同学可以在终端使用 sudo vi /etc/hosts 命令修改
在下面加上下面这一行, 其中 140.82.113.4
是 github 的服务器地址,添加后就可以走本地的域名映射了
140.82.113.4 github.com
保存之后,就可以不使用代理,快乐访问 github.com 了,笔者顺利的完成了梦想第一步: git clone
结语
我是饮东,欢迎点赞关注,我们江湖再会
来源:juejin.cn/post/7328112739335372810
简单聊聊使用lombok 的争议
大家好,我是G探险者。
项目里,因为我使用了Lombok插件,然后代码走查的时候被领导点名了。
我心想,这么好用的插件,为啥不推广呢,整天写那些烦人的setter,getter方法就不嫌烦么?
领导既然不让用,自然有他的道理。
于是我查了一番关于lombok的一些绯闻。就有了这篇文章。
首先呢,Lombok 是一个在 Java 项目中广泛使用的库,旨在通过注解自动生成代码,如 getter 和 setter 方法,以减少重复代码并提高开发效率。然而,Lombok 的使用也带来了一些挑战和争议,特别是关于代码的可读性和与 Java Bean 规范的兼容性。
Lombok 基本使用
示例代码
不使用 Lombok:
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 其他 getter 和 setter
}
使用 Lombok:
import lombok.Data;
@Data
public class User {
private String name;
private int age;
// 无需显式编写 getter 和 setter
}
Lombok 的争议
- 代码可读性和透明度:Lombok 自动生成的代码在源代码中不直接可见,可能对新开发者造成困扰。
- 工具和 IDE 支持:需要特定的插件或配置,可能引起兼容性问题。
- 与 Java Bean 规范的兼容性:Lombok 在处理属性命名时可能与 Java Bean 规范产生冲突,特别是在属性名以大写字母开头的情况。
下面我就列举一个例子进行说明。
属性命名的例子
假设有这么一个属性,aName;
标准 Java Bean 规范下:
- 属性
aName
的setter getter 方法应为setaName() getaName()
。 - 但是 Lombok 可能生成
getAName()
。
这是因为Lombok 在生成getter和setter方法时,将属性名的首字母也大写,即使它是小写的。所以对于aName属性,Lombok生成的方法可能是getAName()和setAName()。
在处理JSON到Java对象的映射时,JSON解析库(如Jackson或Gson)会尝试根据Java Bean规范匹配JSON键和Java对象的属性。它通常期望属性名的首字母在getter和setter方法中是小写的。因此,如果JSON键为"aName",解析库会寻找setaName()方法。
所以,当你使用Lombok的@Data注解,且Lombok生成的setter方法为setAName()时,JSON解析库可能找不到匹配的方法来设置aName属性,因为它寻找的是setaName()。
这种差异可能在 JSON 到 Java 对象的映射中引起问题。
Java Bean 命名规范
根据 Java Bean 规范,属性名应遵循驼峰式命名法:
- 单个单词的属性名应全部小写。
- 多个单词组成的属性名每个单词的首字母通常大写。
结论
Lombok 是一个有用的工具,可以提高编码效率并减少冗余代码。但是,在使用它时,团队需要考虑其对代码可读性、维护性和与 Java Bean 规范的兼容性。在决定是否使用 Lombok 时,项目的具体需求和团队的偏好应该是主要的考虑因素。
来源:juejin.cn/post/7310786611805863963
苦撑多年,老爷子70多!这个软件快要没人维护了
0x01、
在粒子物理学的发展过程中,有这样一个计算软件,它一度被视为粒子物理学研究的基础工具之一。
它就是:FORM。
众所周知,高能物理学领域中涉及很多超长且复杂的方程和公式,这时候就需要有一个能满足特定需求的计算软件(或者程序)来完成对应的工作。
而FORM则是一个可以进行大规模符号运算的计算程序,可以计算伽马矩阵、并行计算、包括模式匹配等。
多年来FORM一直扮演着粒子物理学领域关键工具的角色,并支撑着领域的研究和发展,行业内甚至有很多软件包都依赖于它。
但是就是这样一个领域必备的软件工具,其维护人现在都已经70多岁了,而如今却快要落得没人维护的田地了。。
0x02、
FORM自1984年就开始开发,距今已经有好几十年的历史了。
FORM的开发者是来自于荷兰的粒子物理学家乔斯·维马塞伦(Jos Vermaseren),也是现在该程序的维护者,现如今也已经70多岁高龄了。
而作为一个源自上世纪80年代的程序,彼时计算机方开始普及,软件工具也才逐渐开始兴起。
FORM的前身是由荷兰物理学家马蒂努斯·维尔特曼(Martinus Veltman)所创建的一个名为Schoonschip的程序,但是受限于当时的存储和外设条件等一系列原因,使用起来并不方便。
于是Jos Vermaseren开始着手研究该如何做出一个更易于获取和使用的工具程序。
起初Jos Vermaseren使用的是FORTRAN语言来写的这个程序,但是后来在FORM 1.0版本正式发布以前,Jos Vermaseren又重新使用C语言把该工具给重写了一遍。
就这样,从最早的Apollo工作站到后来的奔腾PC,这个程序慢慢开始被推广使用并流行起来。
经过多年的发展,目前FORM支持的版本如下:
- FORM:顺序版,可以在单个处理器上运行;
- ParFORM:多处理器版本,它可以使用集群和系统,处理器有自己的内存;
- TFORM:支持处理器共享内存系统的多线程版本,主要用于处理器数量有限的系统。
0x03、
聊回到FORM项目70多岁的维护人Jos Vermaseren老爷子,说实话还是非常佩服的。
进入Jos Vermaseren的GitHub主页(github.com/vermaseren)…
并在同期创建了他个人的首个GitHub仓库,也就是form。
截止到目前,这也是Jos Vermaseren在GitHub上的唯一一个维护的项目仓库。
不过比较遗憾的是,这个开源项目不管是访问量还是star、fork数,都十分惨淡。
0x04、
既然这个软件如此重要且无法完全被替代,那为什么现如今想找一些后继的维护人都不那么容易呢?
关于这个问题,Jos Vermaseren本人也曾说过:
“这么多年我一直都有看到,在计算工具上花费大量时间的科学家却无法得到一个物理学领域的终身职位。”
Jos Vermaseren表示自己还算是幸运的,拥有一个在荷兰国家亚原子物理研究所的终身职位,并且还有一个欣赏这个项目的老板,然而很多相关的研究者却不一定都能这样了。
所以这么看来,这也算是被一些现实的问题,所困扰到了。
投入大量精力却得不到对应的回报,而且还要求维护人员有跨学科的知识技能,不少相关领域的研究者也望而却步了。
而且在物理学术界,大家对于物理学本领域的成果产出和论文发表普遍比较看重,而程序开发的努力和关注度则往往被低估了。
可能这也某一程度上导致了像FORM这种软件工具想要找到持续的维护者都变得不那么容易了。
所以说到底,这也算是一个“坐冷板凳”的现实问题了。
文章的最后也附上和FORM相关的开源项目地址,分享给大家。
- FORM主页:http://www.nikhef.nl/~form
- GitHub主页:github.com/vermaseren/…
感兴趣的同学可以上去看一看,除此之外,大家有兴趣也可以研究一下对应的项目代码。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7394788843207376947
2024年中总结-月亮你不懂六便士到底有多重
时光磨灭了许多东西,如今掘金也不再搞年中总结活动了。
别说年中活动了,整体互联网已经“毕业”了一大批员工,互联网缩水,程序员是最大的边际递减成本,但这里也不想再继续说关于焦虑的了。
自己还是照常写写总结吧。
24年已经过去了一半,不止,7.22是大暑,转眼下一个节气就又得是【立秋】了。
想到哪就写到哪吧,有时候很痴迷于这种随机性。
因为随机性,上半年先是出版了训练时长2年半的书《程序员成长手记》,没别的,只是有人找写书,按照流程坚持完成下来了。
因为随机性,后来4月的时候,又出了一本小册《AI BOTS 通关指南》,没别的,产品需要运营,运营需要声音,但大家早就知道了,AI Agent、或者说智能体,都在谈应用、何谈应用?估摸 GPT5 出来之前,所谓的这些 AIGC 都只玩具,无法深度参与生产。赚钱的都是教人赚钱的。
因为随机性,断断续续的更文,一方面工作、草、卷起来了,一方面生活耗时占比提升,一方面自己也没动力、懈怠了。
其实,无所谓生活吧,即使每天下午6点离开工位下班,到家也约近于7点,说是早上8.30上班,7点就要开始起床、做准备,有时候还要回想、梳理、做思想建设等等。一天12个小时围绕工作这件事(摸鱼时间也减少)、8小时围绕睡觉这件事,何谈生活呢?
但是生活确实又在持续发生,比如:2024上半年最大的变化,自己身份再转变,马上要为人父了。。
初为人父、这是一个过程。
从备孕、到验孕、到验血、到查胎心胎芽、到B超-查到积液、到多轮产检、到NT、到无创、到二维等等等等,每一个点都会分散出许多新的点,需要不断打破、建设认识。
然后,似乎又回到感觉有些焦虑了?在《何以为父》这本书上看到、这种心态或许是正常的。好的吧,总之无法作甩手掌柜、也不能。
想想,上半年,还有什么?
项目的工作更加熟练了,对其本质(解决方案、商务、PPT等)似乎有了更清晰的认识。期间也发生过一些插曲,也拿不准后面事态会去向何方,总之,好像也不是自己能定的,反复看《大明1566》桥段,“打工、晋升”是不是应该蛰伏?或者是不是自己这辈子连“田县丞”都见不到,那还想那么多干嘛?
还有什么?年初定的目标完成的并不好。
还有什么?离家多年、人在广东已经漂泊十年。理想=离乡?平民就必须拼命。
想起,最开始最开始写年中总结,引用《老人与海》,竟然现在更适用吧。
还有什么?
没有了,马上10.30了,洗洗睡了。
来源:juejin.cn/post/7394279685969199139
token是用来鉴权的,session是用来干什么的?
使用JWT进行用户认证和授权,而Session在一定程度上起到了辅助作用。
让我们详细讨论JWT和Session在这种结合模式中的各自作用以及为什么需要Session。
JWT的作用
- 用户认证:JWT包含了用户的身份信息和权限信息,客户端每次请求时将JWT发送给服务器,服务器通过验证JWT来确认用户身份。
- 无状态性:JWT不需要在服务器端存储用户会话信息,因此服务器可以是无状态的,便于扩展和负载均衡。
Session的作用
- 附加的安全层:即使JWT是无状态的,但在某些应用场景中,仅依赖JWT可能存在一些安全问题,例如Token的泄露或滥用。Session可以作为一个额外的安全层,确保Token即使有效,也必须在服务器的Session管理器中存在对应的会话。
- 管理Token的生命周期:通过Session,可以更方便地管理Token的生命周期,例如强制用户重新登录、手动注销Token等操作。
- 控制“记住我”功能:如果用户选择了“记住我”选项,Session可以记录这个状态,并在JWT过期后,通过Session来决定是否允许继续使用旧的Token。
为什么需要创建Session
尽管JWT可以在无状态环境中使用,但Session的引入带来了以下好处:
- 防止Token滥用:通过在服务器端验证Session,可以确保即使Token有效,也必须是经过服务器端认证的,从而防止Token被恶意使用。
- 支持用户主动注销:当用户选择注销时,可以直接删除服务器端的Session记录,确保Token即使没有过期,也无法再被使用。
- 提供更精细的控制:通过Session,可以实现更精细的权限控制和用户状态管理,例如强制下线、会话过期时间控制等。
- 状态追踪:在某些场景下,追踪用户状态是必要的,例如监控用户的活跃度、登录历史等,这些信息可以通过Session进行管理。
结合JWT和Session的优势
结合使用JWT和Session,可以同时利用两者的优点,实现安全性和扩展性的平衡:
- 无状态认证:JWT可以实现无状态认证,便于系统的水平扩展和负载均衡。
- 状态管理和安全性:Session可以提供额外的状态管理和安全性,确保Token的使用更加安全可靠。
代码示例
以下是一个简化的代码示例,展示了如何在用户登录时创建JWT和Session:
java
Copy code
public LoginResponse login(String username, String password) throws AuthException {
// 验证用户名和密码
User user = userService.authenticate(username, password);
if (user == null) {
throw new AuthException("Invalid username or password");
}
// 生成JWT Token
String token = createJwt(user.getId(), user.getRoles());
// 创建会话
sessionManagerApi.createSession(token, user);
// 返回Token
return new LoginResponse(token);
}
public void createSession(String token, User user) {
LoginUser loginUser = new LoginUser();
loginUser.setToken(token);
loginUser.setUserId(user.getId());
loginUser.setRoles(user.getRoles());
sessionManagerApi.saveSession(token, loginUser);
}
在请求验证时,首先验证JWT的有效性,然后检查Session中是否存在对应的会话:
java
Copy code
@Override
public DefaultJwtPayload validateToken(String token) throws AuthException {
try {
// 1. 先校验jwt token本身是否有问题
JwtContext.me().validateTokenWithException(token);
// 2. 获取jwt的payload
DefaultJwtPayload defaultPayload = JwtContext.me().getDefaultPayload(token);
// 3. 如果是7天免登陆,则不校验session过期
if (defaultPayload.getRememberMe()) {
return defaultPayload;
}
// 4. 判断session里是否有这个token
LoginUser session = sessionManagerApi.getSession(token);
if (session == null) {
throw new AuthException(AUTH_EXPIRED_ERROR);
}
return defaultPayload;
} catch (JwtException jwtException) {
if (JwtExceptionEnum.JWT_EXPIRED_ERROR.getErrorCode().equals(jwtException.getErrorCode())) {
throw new AuthException(AUTH_EXPIRED_ERROR);
} else {
throw new AuthException(TOKEN_PARSE_ERROR);
}
} catch (io.jsonwebtoken.JwtException jwtSelfException) {
throw new AuthException(TOKEN_PARSE_ERROR);
}
}
总结
在这个场景中,JWT用于无状态的用户认证,提供便捷和扩展性;Session作为辅助,提供额外的安全性和状态管理。通过这种结合,可以充分利用两者的优点,确保系统既具备高扩展性,又能提供细致的安全控制。
来源:juejin.cn/post/7383017171180568630
微信小程序 折叠屏适配
最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后解决了,因为是甲方内部使用的小程序,这里不便贴图,但有官方案例图片,以供参考
启用大屏模式
从小程序基础库版本 2.21.3 开始,在 Windows、Mac、车机、安卓 WMPF 等大屏设备上运行的小程序可以支持大屏模式。可参考小程序大屏适配指南。方法是:在 app.json 中添加 "resizable": true
看到这里我心里窃喜,就加个配置完事了?这也太简单了,但后面证明我想简单了,
主要有两大问题:
- 1 尺寸不同的情况下内容展示效果兼容问题
- 2 预览版和体验版 大屏模式冷启动会生效,但热启动 和 菜单中点击重新进入小程、授权操作,会失效变成窄屏
解决尺寸问题
因为css的长度单位大部分用的 rpx,窄屏和宽屏展示差异出入较大,别说客户不认,自己这关就过不了,简直都不忍直视,整个乱成一片,尤其登录页,用了定位,更是乱上加乱。
随后参考了官方的文档 小程序大屏适配指南和自适应布局,方案对于微信小程序原生开发是可行的,但这个项目用的 uni-app开发的,虽然uni-app 也有对应的响应式布局组件,再加上我是个比较爱偷懒的人(甲方给的工期事件也有限制),不可能花大量时间把所有也页面重新写一遍布局,这是不现实的。
于是又转战到uni-app官网寻找解决方案 uni-app宽屏适配指南
内容缩放拉伸的处理 这一段中提出了两个策略
- 1.局部拉伸:页面内容划分为固定区域和长宽动态适配区域,固定区域使用固定的px单位约定宽高,长宽适配区域则使用flex自动适配。当屏幕大小变化时,固定区域不变,而长宽适配区域跟着变化
- 2.等比缩放:根据页面屏幕宽度缩放。rpx其实属于这种类型。在宽屏上,rpx变大,窄屏上rpx变小。
随后看到这句话特别符合我的需求,哈哈 省事 省事 省事
策略2省事,设计师按750px屏宽出图,程序员直接按rpx写代码即可。但策略2的实际效果不如策略1好。程序员使用策略1,分析下界面,设定好局部拉伸区域,这样可以有更好的用户体验
具体实现
1.配置 pages.json 的 globeStyle
{
"globalStyle": {
"rpxCalcMaxDeviceWidth": 1200, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960
"rpxCalcBaseDeviceWidth": 375, // rpx 计算使用的基准设备宽度,设备实际宽度超出 rpx 计算所支持的最大设备宽度时将按基准宽度计算,单位 px,默认值为 375
"rpxCalcIncludeWidth": 750 // rpx 计算特殊处理的值,始终按实际的设备宽度计算,单位 rpx,默认值为 750
},
}
2.单位兼容
还有一点官方也提出来了很重要,那就很多时候 会把宽度750rpx 当成100% 使用,这在宽屏的设备上就会有问题, uniapp给了两种解决方案
- 750rpx 改为100%
- 另一种是配置rpxCalcIncludeWidth,设置某个特定数值不受rpxCalcMaxDeviceWidth约束
想要用局部拉伸:页面内容划分为固定区域和长宽动态适配区域”的策略,单位必须用px
添加脚本
项目根目录新增文件 postcss.config.js 内容如下。则在编译时,编译器会自动转换rpx单位为px。
// postcss.config.js
const path = require('path')
module.exports = {
parser: 'postcss-comment',
plugins: {
'postcss-import': {
resolve(id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
},
'autoprefixer': {
overrideBrowserslist: ["Android >= 4", "ios >= 8"],
remove: process.env.UNI_PLATFORM !== 'h5'
},
// 借助postcss-px-to-viewport插件,实现rpx转px,文档:https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
// 以下配置,可以将rpx转换为1/2的px,如20rpx=10px,如果要调整比例,可以调整 viewportWidth 来实现
'postcss-px-to-viewport': {
unitToConvert: 'rpx',
viewportWidth: 200,
unitPrecision: 5,
propList: ['*'],
viewportUnit: 'px',
fontViewportUnit: 'px',
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
replace: true,
exclude: undefined,
include: undefined,
landscape: false
},
'@dcloudio/vue-cli-plugin-uni/packages/postcss': {}
}
}
大屏模式失效问题
下面重头戏来了,这期间经历 蜿蜒曲折 ,到头来发现都是无用功,我自己都有被wx蠢到发笑,唉,
样式问题解决后 开始着重钻研 大屏失效的问题,但看了官方的多端适配示例demo,人家的就是好的,那就应该有解决办法,于是转战github地址 下项目,谁知这项目暗藏机关,各种报错,让你跑不起来。。。。,让我一度怀疑腾讯也这么拉跨
还好issues 区一位大神有解决办法 感兴趣的老铁可以去瞅瞅
另外 微信小程序开发工具需要取消这两项,最后当项目跑起来后我还挺开心,模拟器上没有问题,但用真机预览的时候我啥眼了,还是窄屏,偶尔可以大屏,后面发现 冷启动是大屏,热启动和点击右上角菜单中的重新进入小程序按钮都会自己变成窄屏幕
![]() | ![]() |
这是官方的项目啊,为啥人家的可以,我本地跑起来却不可以,让我一度怀疑这里有内幕,经过几轮测试还是不行,于是乎,我开始了各种询问查资料,社区、私聊、评论、github issues,最后甚至 统计出来了 多端适配示例demo 开发者的邮箱 挨个发了邮件,但都结果无一例外,全部石沉大海
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
结果就是,没有办法了,想看看是不是只有预览和体验版有问题,后面发布到正式版后,再看居然没问题了,就是这么神奇,也是无语!!!! 原来做了这么多无用功。。。。
来源:juejin.cn/post/7273764921456492581
前端项目公共组件封装思想(Vue)
1. 通用组件(表单搜索+表格展示+分页器)
在项目当中我们总会遇到这样的页面:页面顶部是一个表单筛选项,下面是一个表格展示数据。表格下方是一个分页器,这样的页面在我们的后台管理系统中经常所遇到,有时候可能不止一个页面,好几个页面的结构都是这种。如图:
本人记得,在react中的高级组件库中有这么一个组件,就实现了这么一个效果。就拿这个页面来说我们实现一下组件封装的思想:1.首先把每个页面的公共部分抽出来,比如标题等,用props或者插槽的形式传入到组件中进行展示 2. 可以里面数据的双向绑定实现跟新的效果 3. 设置自定义函数传递给父组件要做上面事情
1.将公共的部分抽离出来
TableContainer组件
<template>
<div class="container">
<slot name="navbar"></slot>
<div class="box-detail">
<div class="detail-box">
<div class="box-left">
<div class="left-bottom">
<div class="title-bottom">{{ title }}</div>
<div class="note">
<div class="note-detail">
<slot name="table"></slot>
</div>
</div>
</div>
</div>
</div>
</div>
<el-backtop style="width: 3.75rem; height: 3.75rem" :bottom="10" :right="5">
<div
style="
{
width: 5.75rem;
flex-shrink: 0;
border-radius: 2.38rem;
background: #fff;
box-shadow: 0 0.19rem 1rem 0 #2b4aff14;
}
"
>
<i class="el-icon-arrow-up" style="color: #6e6f74"></i>
</div>
</el-backtop>
</div>
</template>
这里的话利用了具名插槽插入了navbar、table组件,title通过props的属性传入到子组件当中。进行展示,
父组件
<TableContainer title="资源审核">
<template v-slot:navbar>
<my-affix :offset="0">
<Navbar/>
</my-affix>
</template>
<template v-slot:table>
<SourceAuditTable/>
</template>
</TableContainer>
当然这是一个非常非常简单的组件封装案例
接下来我们看一个高级一点的组件封装
父组件
<template>
<div>
<hr>
<HelloWorld :page.sync="page" :limit.sync="limit" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
export default {
data() {
return {
page: 1,
limit: 5
}
},
components: {
HelloWorld
},
}
</script>
父组件传递给子组件各种必要的属性:total(总共多少条数据)、page(当前多少页)、limit(每页多少条数据)、pageSizes(选择每页大小数组)
子组件
<template>
<el-pagination :current-page.sync="currentPage" :page-size.sync="pageSize" :total="20" />
</template>
<script>
export default {
name: 'HelloWorld',
props: {
page: {
default: 1
},
limit: {
default: 5
},
},
computed: {
currentPage: {
get() {
return this.page
},
set(val) {
//currentPage 这里对currentPage做出来改变就会走这里
//这边更新数据走这里
console.log('currentPage', this.currentPage)
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
这里的page.sync、limit.sync目的就是为了实现数据的双向绑定,computed中监听page和limit的变化,子组件接收的数据通过computed生成的currentPage通过sync绑定到了 el-pagination中, 点击分页器的时候会改变currentPage 此时会调用set函数设置新的值,通过代码 this.$emit(update:page,value) 更新父组件中的值,实现双向的数据绑定
本文是作者在闲暇的时间随便记录一下, 若有错误请指正,多多包涵。感谢支持!
来源:juejin.cn/post/7312353213347708940
这一年我优化了一个46万行的超级系统
背景
我曾带领团队治理了一个超级工程,是我毕业以来治理的最庞大、最复杂的工程系统,涉及到开发的方方面面。下面我给大家列几个数字,大家感受一下:
指标 数据 菜单数量 250+ 代码行数 46 万 路由数量 300+ 业务组件、util 600+ 构建时间 6min 关联业务 报表、CRM、订单、车辆、配置、财务...

这是上一任留给我的烫手山芋,而且从需求迭代频次来看,这个系统占据了这个业务部门50%的需求,也就是说,之前的伙计把几乎所有的业务功能都做进了一个系统中,像屎山一般堆积,构建一次的时间长达6-9min,作为一个合格的前端,简直怒火冲天。
我曾带领团队治理了一个超级工程,是我毕业以来治理的最庞大、最复杂的工程系统,涉及到开发的方方面面。下面我给大家列几个数字,大家感受一下:
指标 | 数据 |
---|---|
菜单数量 | 250+ |
代码行数 | 46 万 |
路由数量 | 300+ |
业务组件、util | 600+ |
构建时间 | 6min |
关联业务 | 报表、CRM、订单、车辆、配置、财务... |
这是上一任留给我的烫手山芋,而且从需求迭代频次来看,这个系统占据了这个业务部门50%的需求,也就是说,之前的伙计把几乎所有的业务功能都做进了一个系统中,像屎山一般堆积,构建一次的时间长达6-9min,作为一个合格的前端,简直怒火冲天。
问题
面对这样的超级应用,想要解决,必须先把问题整理出来,才好对症下药。
- 构建时间过长,影响开发体验。
- 系统单一,代码量庞大,未做拆分,对于后期维护难度很大,且存在增量上线风险(改一个字,也要整个系统打包上线)。
- 代码业务组件太少,复用率低,需要整理业务代码,封装高效业务组件库。
- 工具函数、请求Axios、目录规范、菜单权限、公共能力等未做统一封装和梳理。
- 存在大量重复的代码、大量重复的组件(有很多都是拷贝了一份,改个名字)。
- 页面加载极其缓慢,无论是首屏还是菜单加载,很明显存在严重的性能问题。
- 工程中随意引入各种插件,比如:big.js、xlsx.js、echarts、velocity-animate、lodash、file-saver等。
- 代码中存在很多mixin写法,导致调试难度很大。
以上是针对46万行的应用做出的问题分析报告,找出这些问题以后,我们就能开启优化之路。
面对这样的超级应用,想要解决,必须先把问题整理出来,才好对症下药。
- 构建时间过长,影响开发体验。
- 系统单一,代码量庞大,未做拆分,对于后期维护难度很大,且存在增量上线风险(改一个字,也要整个系统打包上线)。
- 代码业务组件太少,复用率低,需要整理业务代码,封装高效业务组件库。
- 工具函数、请求Axios、目录规范、菜单权限、公共能力等未做统一封装和梳理。
- 存在大量重复的代码、大量重复的组件(有很多都是拷贝了一份,改个名字)。
- 页面加载极其缓慢,无论是首屏还是菜单加载,很明显存在严重的性能问题。
- 工程中随意引入各种插件,比如:big.js、xlsx.js、echarts、velocity-animate、lodash、file-saver等。
- 代码中存在很多mixin写法,导致调试难度很大。
以上是针对46万行的应用做出的问题分析报告,找出这些问题以后,我们就能开启优化之路。
目标

我觉得不管哪一行,进入这个社会最重要的能力是生存能力,面对生存最重要的技能是解决问题的能力。想要把问题解决好,就要有章法可循,比如:找到问题要害、制定目标、提供解决方案、复盘总结。
我觉得不管哪一行,进入这个社会最重要的能力是生存能力,面对生存最重要的技能是解决问题的能力。想要把问题解决好,就要有章法可循,比如:找到问题要害、制定目标、提供解决方案、复盘总结。
方案
- 250+菜单归类整理、废弃菜单下线
- 搭建业务组件库
- 搭建工具函数库
- 基础框架优化
- 基于microApp做微服务拆分、引入webpack5的module-federation机制
- 引入rocket-render插件,对局部页面、基础功能做重构,后续逐步替换。
- 性能优化
- 250+菜单归类整理、废弃菜单下线
- 搭建业务组件库
- 搭建工具函数库
- 基础框架优化
- 基于microApp做微服务拆分、引入webpack5的module-federation机制
- 引入rocket-render插件,对局部页面、基础功能做重构,后续逐步替换。
- 性能优化
菜单整理
300 多个菜单,业务和产研人员可能已经更换过好几次,通过跟产品的沟通,可以得知,受业务调整影响,很多功能已经废弃,系统未及时下线导致日积月累。然而由于对业务不熟,产研很难判断哪些菜单需要下线,我们也不能随意凭感觉下线某一个菜单或功能,必须采取谨慎的态度,有法可依,因此我们采用如下措施:

菜单汇总
产品牵头,汇总系统所有菜单,按环境进行逐个确认。前端会根据确认结果依次删除路由配置、菜单、关联组件、调用 API 等相关代码。
- 同业务方确认后,直接下线。
- 线上访问异常菜单,进行标注。
- 数据异常菜单,进行标注。
- 对于无法确认的,通过监控查看页面访问量,通过1-3个月的观察,最终决定是否下线。
通过1个月的菜单整理,下线了大概50+的菜单、删除了近100+页面,以及不计其数的业务组件代码,整体代码量减少近8万,同时,我们将250多个菜单进行归类,拆分了6个大类,供后续微服务做准备。
300 多个菜单,业务和产研人员可能已经更换过好几次,通过跟产品的沟通,可以得知,受业务调整影响,很多功能已经废弃,系统未及时下线导致日积月累。然而由于对业务不熟,产研很难判断哪些菜单需要下线,我们也不能随意凭感觉下线某一个菜单或功能,必须采取谨慎的态度,有法可依,因此我们采用如下措施:
菜单汇总
产品牵头,汇总系统所有菜单,按环境进行逐个确认。前端会根据确认结果依次删除路由配置、菜单、关联组件、调用 API 等相关代码。
- 同业务方确认后,直接下线。
- 线上访问异常菜单,进行标注。
- 数据异常菜单,进行标注。
- 对于无法确认的,通过监控查看页面访问量,通过1-3个月的观察,最终决定是否下线。
通过1个月的菜单整理,下线了大概50+的菜单、删除了近100+页面,以及不计其数的业务组件代码,整体代码量减少近8万,同时,我们将250多个菜单进行归类,拆分了6个大类,供后续微服务做准备。
框架优化
一个稳定的系统,必然有一个稳定的框架做支撑。我们在梳理的过程中发现系统极其脆弱,无 ESLint 语法检查规范、没有 Prettier 格式化、Hard Code、随处可见的自定义组件、到处报错的控制台...
- 引入
ESLint
做语法检查. - 引入
Pettier
做代码格式化。 - 引入
Husky
lint-staged
规范代码提交。 - 对于硬编码部分跟产品沟通做成可视化配置或者json配置形式。
- Axios的二次封装:常规请求、全局Loading、文件下载、登录失效拦截、报错拦截提示、状态码统一适配。
- 插件删减:
big.js
剔除改为手动计算、lodash
替换为 lodash-es
、删除动画插件、文件导出统一由后端返回二进制,前端通过Axios封装下载函数进行文件下载等等。 - 配置文件封装:多环境配置、常量定义。
- 菜单权限封装。
- 按钮权限指令封装:
v-has="'create'"
- router路由提取优化,针对路由守卫处理一些特殊跳转问题。
- 接入前端监控平台,对于资源请求失败、接口请求超时、接口请求异常等做异常监控。
- 对于
eslint
和prettier
大家自行参考其他文章,此处不再赘述。 - 对于Axios二次封装,大家可能会比较奇怪,为什么要封装文件下载?因为文件导出本身也是一个请求,只是跟常规get/post有差异,所以我们为了方便调用,把它放在一个文件里面定义,比如:
export default {
get(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.get(url, { params, ...options })
},
post(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.post(url, params, options)
},
download(url, data, fileName = 'fileName.xlsx') {
instance({
url,
data,
method: 'post',
responseType: 'blob'
}).then(response => {
const blob = new Blob([response.data], {
type: response.data.type
})
const name = (response.headers['file-name'] as string) || fileName
const link = document.createElement('a')
link.download = decodeURIComponent(name)
link.href = URL.createObjectURL(blob)
document.body.append(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
})
}
}
我们在api.js
中调用的时候,就会变的很简单,如:api.get('/user/list')
、api.download('/export',{ id: 123 })
。当然可能每个人有自己的开发习惯,这些都是我个人的经验,也不一定适合大家。对于上面的参数有一个options
变量,用来做参数扩展的,比如展示loading
和error
,为了做全局Loading
和全局错误提示用的。
- 如果你担心页面并发请求,导致重复
loading
问题,可以通过计数的方式来控制,避免多次重复Loading
。 - 插件删除部分大家可能有争议,这个纯属于个人行为,因为我们不是金融行业,不需要高精度,如果出现计算我们直接四舍五入即可,而
lodash-es
主要为了做tree-shaking
,还有很多插件根据自身情况考虑要不要引入。 - 指令封装,对于按钮权限非常管用,举个例子:
一个稳定的系统,必然有一个稳定的框架做支撑。我们在梳理的过程中发现系统极其脆弱,无 ESLint 语法检查规范、没有 Prettier 格式化、Hard Code、随处可见的自定义组件、到处报错的控制台...
- 引入
ESLint
做语法检查. - 引入
Pettier
做代码格式化。 - 引入
Husky
lint-staged
规范代码提交。 - 对于硬编码部分跟产品沟通做成可视化配置或者json配置形式。
- Axios的二次封装:常规请求、全局Loading、文件下载、登录失效拦截、报错拦截提示、状态码统一适配。
- 插件删减:
big.js
剔除改为手动计算、lodash
替换为lodash-es
、删除动画插件、文件导出统一由后端返回二进制,前端通过Axios封装下载函数进行文件下载等等。 - 配置文件封装:多环境配置、常量定义。
- 菜单权限封装。
- 按钮权限指令封装:
v-has="'create'"
- router路由提取优化,针对路由守卫处理一些特殊跳转问题。
- 接入前端监控平台,对于资源请求失败、接口请求超时、接口请求异常等做异常监控。
- 对于
eslint
和prettier
大家自行参考其他文章,此处不再赘述。 - 对于Axios二次封装,大家可能会比较奇怪,为什么要封装文件下载?因为文件导出本身也是一个请求,只是跟常规get/post有差异,所以我们为了方便调用,把它放在一个文件里面定义,比如:
export default {
get(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.get(url, { params, ...options })
},
post(url, params = {}, options = { showLoading: true, showError: true }) {
return instance.post(url, params, options)
},
download(url, data, fileName = 'fileName.xlsx') {
instance({
url,
data,
method: 'post',
responseType: 'blob'
}).then(response => {
const blob = new Blob([response.data], {
type: response.data.type
})
const name = (response.headers['file-name'] as string) || fileName
const link = document.createElement('a')
link.download = decodeURIComponent(name)
link.href = URL.createObjectURL(blob)
document.body.append(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
})
}
}
我们在api.js
中调用的时候,就会变的很简单,如:api.get('/user/list')
、api.download('/export',{ id: 123 })
。当然可能每个人有自己的开发习惯,这些都是我个人的经验,也不一定适合大家。对于上面的参数有一个options
变量,用来做参数扩展的,比如展示loading
和error
,为了做全局Loading
和全局错误提示用的。
- 如果你担心页面并发请求,导致重复
loading
问题,可以通过计数的方式来控制,避免多次重复Loading
。 - 插件删除部分大家可能有争议,这个纯属于个人行为,因为我们不是金融行业,不需要高精度,如果出现计算我们直接四舍五入即可,而
lodash-es
主要为了做tree-shaking
,还有很多插件根据自身情况考虑要不要引入。 - 指令封装,对于按钮权限非常管用,举个例子:
封装指令
import { getPageButtons } from '@hll/css-oms-utils'
// 权限指令
Vue.directive('has', {
inserted: (el, binding) => {
let pageButtons = getPageButtons().map;
if (!pageButtons[binding.value]) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
权限判断
// 1. 通过v-if判断
<el-button type="primary" v-if="pageButtons.create">创建el-button>
// 2. 通过v-has指令判断
<el-button type="primary" v-has="'create'">创建el-button>
getPageButtons 其实是为了兼容历史代码而封装的函数。
整个系统权限极其混乱,代码替换,耗时近 2 周,覆盖近 500 个文件才完成。
- 状态码适配
这个我觉得有必要提一下,我相信很多前端可能都遇到这个问题,一个系统对应n个后台服务,不同的后台服务有不同的格式,比如:A系统返回code=0
,B系统返回result=0
,C系统返回res=0
,那前端就要做不同的适配,其实也有不同的方法可以做:
- 让后端接入网关,统一在网关做适配。
- 前端在拦截器中开发
adapter
函数,针对响应码做适配。 - 针对分页、返回码、错误信息等都可以在适配函数里面做几种适配,或者提供单一函数在不同的页面单独调用,类似于
request
的to
模块。
业务组件库建设
这一部分工作非常重要,不管你在哪个公司,做任何行业,我都建议封装业务组件库,当然封装有三种形式:
- 基于公司自建的
npm
平台开发业务组件库,通过npm
方式引入。 - 对于小体量项目,直接把业务组件库放在
components
中进行维护,但是无法跨项目使用。 - 基于
webpack5
的module federation
能力开发公共组件,跨项目提供服务。
MF文档参考:http://www.webpackjs.com/concepts/mo… 我觉得在某些场景下,这个能力非常好用,它不需要像npm
一样,发布以后还需要给项目做升级,只要你改了组件源码,发布以后,其他引用的项目就会立即生效。想要快速了解这个能力,我建议大家先看一下插件文档,然后了解两个概念exposes
和remotes
,如果还是看不懂,就去找个掘进入门文章再结合本地实践一次基本就算入门了。
我是基于上面三种方式来搭建系统的业务组件库,对于通用部分全部提取封装,基于rollup
和vite
搭建一套npm
包,最终发布到公司私有npm
平台。对于一些频繁改动,链路较长部分通过module federation
进行封装和暴露。
梳理业务后,其实封装了很多业务组件,上面只是给大家提供了思路,没有一一列举具体组件,下面给大家简单列一个大纲图:
业务组件库建设对于这种大体量的超级系统收益非常大,而且是长期可持续的。
微服务搭建
前面第一部分梳理完菜单以后,其实已经将250+的菜单进行了归类,归类的目的其实就是为了服务拆分。拆分的好处非常明显:
- 服务解耦,便于维护。
- 局部需求可单独上线,不需要整包上传,减小线上风险。
- 缩小每个服务模块的构建时间,提升开发体验。
本次基于pnpm + microApp + module federation
来实现的微服务拆分,为什么?
- 微服务拆分以后,创建了 7 个仓库,非常不利于代码维护。
pnpm
天然具备monorepo
能力,可以把多个仓库合并为一个仓库,而且可以继续按7个项目的开发构建方式去工作。 - 微服务使用的是京东的
microApp
框架,集成非常简单,上手成本很低,文档友好,我觉得比阿里的乾坤简单太多了(个人主观看法)。 - 对于难于抽取的组件,直接通过
module federation
对外暴露组件服务。
上面在搭建业务组件库的时候,其实遇到了一个非常棘手的问题,有些组件跨了很多项目,链路很长,在抽取的过程中难度非常大,大家看一个图:
服务之间耦合太严重,从而导致组件抽取难度很大,那如何解决? 答案就是module federation
,抽取不了,就不抽取了,直接通过exposes
对外暴露组件服务,在其它子服务中调用即可。
下面给大家举一个接入microApp
的例子:
基座服务(主应用)
import microApp from '@micro-zoe/micro-app';
microApp.start()
添加组件容器(主应用)
<template>
<micro-app name="vms" :url="url" baseroute="/" @datachange='handleDataChange'>micro-app>
template>
<script>
export default {
name: 'ChildApp',
data() {
return {
activeMenu: '',
url: 'http://xxxx',
};
},
methods:{
handleDataChange({ detail: { data } }) {
// todo
},
}
};
script>
分配菜单(主应用)
{
path: '/child',
name: 'child',
component: () => import('./../layout/microLayout.vue'),
meta: {
appName: 'vms',
title: '子服务A'
}
}
就这样,一个主服务就搭建好了,等子服务上线以后,点击/child
菜单,就能打开对应的子服务了。 当然因为我们是一个超级应用,所以我们在做的过程中其实遇到了很多问题,比如:跨域问题、变量问题、包共享问题、本地开发调试问题、远程加载问题等等,不过大家可以参考官方文档,都是可以解决的。
Rocket-render接入
这是我个人开源的一套基于Vue2
的渲染引擎,通过json
定义就可以快速搭建各种简单或复杂的表单、表格场景,跟formly
这一类非常相似。
- 插件文档:rocket-render
- 开发文档:rocket-doc
给大家举一个简单的例子:
- 安装插件
yarn add rocket-render -S
- 组件注册
// 导入包
import RocketRender from 'rocket-render';
// 导入样式
import 'rocket-render/lib/rocket-render.css';
// 默认安装
Vue.use(RocketRender);
// 或者使用自定义属性,以下的值都是系统默认值,如果你的业务不同,可以配置下面的全局变量进行修改。
Vue.use(RocketRender, {
size: 'small',
empty: '-',
inline: 'flex',
toolbar: true,
align: 'center',
stripe: true,
border: true,
pager: true,
pageSize: 20,
emptyText: '暂无数据',
});
插件支持自定义属性,建议默认即可,我们已经给大家调好,如果确实想改,就通过自定义属性的方式进行修改。
- 页面应用
search-form 自带背景色和内填充,如果你有特殊需要,可以添加 class 进行覆盖,另外建议给 model 添加 sync 修饰符。
<template>
<search-form
:json="form"
:model.sync="queryForm"
@handleQuery="getTableList"
/>
template>
<script>
export default {
data() {
return {
queryForm: {},
form: [
{
type: 'text',
model: 'user_name',
label: '用户',
placeholder: '请输入用户名称',
},
],
};
},
};
script>
我们针对需要自定义部分提供了slot插槽,所以不用担心用了以后,对于复杂需求支持不了,导致返工的现象,完全不存在,除了json
以外,我们还内置了非常多的开发技巧,保证你爱不释手,继续看几个例子:
- 日期范围组件,通过
export
直接暴露两个字段。
{
type: 'daterange',
model: 'login_time',
label: '日期范围',
// 对于日期范围控件来说,一般接口需要拆分为两个字段,通过export可以很方便的实现字段拆分
export: ['startTime', 'endTime'],
// 日期转换为时间戳单位
valueFormat: 'timestamp', // 支持:yyyy-MM-dd HH:mm:ss
defaultTime: ['00:00:00', '23:59:59'], //可以设置默认时间,有时候非常有用。后端查询的时候,必须从0点开始才能查到数据。
}
前端是一个组件对应一个字段,但是接口往往需要让你传开始和结束日期,通过export可以直接拆分,太好用了。
- 下拉组件支持一步请求
{
type: 'select',
model: 'userStatus',
label: '用户状态',
multiple: true, // 支持多选
filterable: true, //支持输入过滤
clearable: true,
// 如果下拉框的值是动态的,可以使用fetchOptions获取,必须返回promise
fetchOptions: async () => {
return [
{ name: '全部', id: 0 },
{ name: '已注销', id: 1 },
{ name: '老用户', id: 2 },
{ name: '新用户', id: 3 },
];
},
// 字段映射,用来处理接口返回字段,避免前端去循环处理一次。
field: {
label: 'name',
value: 'id',
},
options: [],
// 如果想要修改其它表单值,可以通过change事件来处理
change: this.getSelectList,
}
通过fetchOptions可以直接在里面调用接口来返回下拉的值。如果是静态的,直接定义options即可。 如果接口返回的类别字段不是label和value结构,可以通过field来做映射。我觉得用的太爽了。
还有很多,我就不举例子了,大家去看文档就好了,文档写的非常清楚。
性能优化
前面几部分做完以后,我们的代码总量基本已经降到了 30 万行左右了,而且整个构建时长从9min降到了 30s ,有了业务组件库的加持,简直不要太爽,不过你看到的这些,看起来容易,其实我们花费了近1年的时间才完成。 剩下就是提升系统性能了:
- 资源全部上
cdn
,不仅上cdn
,还要再阿里云针对图片开启webp
(需要做兼容处理),cdn
记得添加Cache-Control
缓存。 - 服务器全部支持
gzip
压缩。 - 添加
external
配置,我在npm
开发了一个vite-plugin-external-new
插件,可以帮你解决。
- 这个大家一定要做,因为可以有效降低vender体积,你通过LightHouse做性能分析,就能知道原因,对于体积较大的js会给你提示的。
- 通过
external
,我们可以直接让vue
、vue-router
、vuex
、element-ui
等等全部通过defer
加载。
- 建议在根html
中加一个
Loading
标签
<div id="app">
<div class="loading">加载中...div>
div>
这样做的好处是,如果vue.js
还没有加载完成之前,可以让页面先loading
,等new Vue({ el: '#app' })
执行以后,才会覆盖#app
里面的 内容,这样可以提升FCP
指标。 5. 对于比较大的插件,建议按需
export const loadScript = (src: string) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.defer = true;
script.onload = resolve;
script.onerror = reject;
script.src = src;
document.head.append(script);
});
};
某一个页面如果使用较大的插件,比如:xlsx、html2Canvas等,可以单独在某一个页面内做按需加载。
- 有些页面也可以针对
vue
组件或者大图片做按需加载。 - 其它性能优化,我觉得可做可不做,针对LightHouse分析报告针对性下手即可,可能很多文章将性能优化会非常细致,比如chunk分包等等,我倒觉得不是很重要。 只要把上面的做完,理论上前端的性能已经会有巨大的提升了。
结果指标
指标 | 优化前 | 优化后 |
---|---|---|
构建时长 | 6-9min | 30-45s |
代码行数 | 46万 | 30 万 |
服务 | 1个 | 7个 |
业务组件库 | 乱七八糟 | 基于rollup开发构建 |
基础框架 | 乱七八糟 | 高逼格 |
性能评分 | 30分 | 92分 |
团队成员 | 9个 | 4个 |
以上就是我面对一个 46 万行代码的超级系统,耗时大概一年的时间所做的一些成果,很多技术细节也只是泛泛而谈,但是我希望给大家提供的是工作方法、解决思路等偏软实力方面,技术功底和技术视野还是要靠自己一步一个脚印。
这一年我过的很充实,面对突如其来的问题,我心情很复杂,这一年,我感觉也老了很多。最后,感谢你耐心的倾听,我是河畔一角,一个普通的前端工程师。
来源:juejin.cn/post/7394095950383710247
在自己没有价值前,就不要谈什么人情世故了!
Hello,大家好,我是 Sunday。
昨天有位同学跟我吐槽,大致的意思是:“感觉自己不会搞 人情世故,导致在职场中很吃亏。领导也不罩着自己!”
在我的印象中,好像很多程序员都是 “不懂人情世故的典型”,至少很多程序员都认为自己是 “不懂人情世故的”。
但是,人情世故是什么?它真的有用吗?你跟领导关系好,他就会罩着你,帮你背锅吗?
恐怕是:想多了!!
一个真实的故事
给大家讲一个之前我经历过的真实故事,里面涉及到两个人物,我们用:领导 和 员工A 代替。
员工A是一个很懂 “人情世故” 的人,主要体现在两个方面:
- 酒桌文化:不像我这种压根就不能喝酒的人。员工A的酒量很好,并且各种喝酒的说法了熟于心(可以把领导说的很舒服的那种)
- 开会文化:各种反应都在领导的 “兴奋点” 上。我不知道怎么进行形容,类似于罗老师的这张图片,大家自己体会
其他方面的事情(私下的吃饭、逢年过节送礼),这些我就不清楚了,所以这里就不乱说了。
在我的眼里看来,这应该就是 熟通人情世故 的了。不知道,大家认为是不是。
不过,结果呢?
当公司决定裁员时,员工A 是 最早一批 出现在裁员名单中的。
领导会帮他争取留下来的机会吗?并不会
当你只能为对方带来 “情绪价值” 时,对方并不会把你当成心腹,更多的只是类似“马屁精”的存在。而这样的情绪价值,并没有太大的意义。更不要指望 领导会为了你做一些影响他自己利益,或者为他自己带来风险的事情了。
在自己没有价值前,就不要谈什么人情世故了!
国人在很多时候都会探讨 “人情世故” 的重要性。因为我生在 山东,对此更是感触颇深。(山东是受 儒家思想 熏陶最为严重的地方)。甚至,在之前,我也一度认为 “人情世故” 是非常重要的一件事情。
但是,当我工作之后进入企业以来。我越来越发现,在企业之中,所谓的 “人情世故” 并没有太大的意义。
人都是非常现实的,别人对你的看法,完全取决于你能为对方带来什么价值。
而这个价值通常需要体现在 金钱上 或者 事业上!
当你无法在这两个方面为对方提供价值时,那么你做的所谓的 “人情世故” 也会被对方认为是“马屁精”的嫌疑。
所以,与其把精力放到所谓的“人情世故”中,甚至为此而感到苦恼(就像开头所提到的同学一样),是 大可不必 的!
在你无法为对方带来价值之前,先努力提升自己的的能力,当你可以和对方处于一个平等的位置进行交流时,再去谈所谓的人情世故,也不迟!
来源:juejin.cn/post/7393713240995676175
去寺庙做义工,有益身心健康
《乔布斯传》中写到:乔布斯把对事物专注的能力和对简洁的热爱归功于他的禅修。他说:“禅修磨炼了他对直觉的欣赏能力,教他如何过滤掉任何能分散时间和精力的其它不必要的事情,在他的身上培养出了专注基于至简主义的审美观。”
如何在当今物欲横流的浮躁社会里不沦陷其中?如何在每天奔波忙碌之后却不内心疲惫、焦虑?如何在巨大的工作与生活压力下保持一颗平和的心?如何在经历感情、友情和亲情的起起落落后看破放下?如何改变透支健康和生命的人生模式?
程序员无疑是一个高压的职业,尤其是在头部公司工作的程序员们,工作压力更是大。并且在互联网行业,禅修并不是一件新鲜事。我们不一定要正儿八经地参加禅修活动,只是去寺庙走一走,呼吸一下新鲜空气,给寺庙干点活,对身心健康的帮助也会很大。
我与寺庙
我最早接触寺庙是在2011年上军校的时候,我的一个老师,作为大校,经常在课上分享他周末在南京附近寺庙的奇闻轶事,也会分享他自己的一些人生体验和感悟,勾起了我对寺庙生活的向往。
2013年,作为现役军人,我跑到了江西庐山的东林寺做了一个礼拜的义工,在那里,每天早上四点起床早上寺庙早课,负责三餐的行堂,也作为机动义工,干一些杂活,比如卸菜、组装床等,晚上有时也可以听寺庙的传统文化课。
2013年底,我申请退出现役,于是14年春就可以休假了,根据流程不定期去各部门办理手续即可,期间一个周末,我弟带我去凤凰岭玩,偶遇一个皈依法会,为了能看到传说中的北大数学天才,我填了一个义工表,参加了皈依仪式。
因为没有考虑政府安排的工作,所以打算考个研,期间不时会去凤凰岭的寺庙参加活动。考完研后,到18年春季,周末节假日,基本都会去这个寺庙做义工,累计得有200天以上。
期间,作为骨干义工,参与了该寺庙组织的第二至第四届的IT禅修营,负责行堂、住宿和辅导员等相关的工作。
很多人都听过这样一件往事:2010年,张小龙(微信之父)偶然入住一个寺院,当时正是微信研发的关键时刻,因为几个技术难题,张小龙连续几天彻夜难眠,终于一气之下把资料撕得粉碎。
没想到负责打扫卫生的僧人看到后,竟然帮他把资料重新粘贴了起来,还顺手写下了几条建议。张小龙非常惊讶,打听过后才知道这位扫地僧出家前曾混迹IT界,是个著名的极客。
经扫地僧点化,张小龙回到广州苦攻一年后,微信终于大成。这件事传的很广也很玄乎,可信度不会太高,不过故事中张小龙入住的寺院,就是我常去的寺庙。
至于在故事中懂得IT的扫地僧,在这里遇到其实不是什么奇怪的事,你还有可能遇到第47届国际数学奥赛金牌得主贤宇法师,他来自北大数学系;或者是禅兴法师,他是清华大学流体力学博士;又或者贤启法师,他是清华大学核能和热能物理博士。
“扫地只不过是我的表面工作,我真正的职业是一位研究僧。” 《少林足球》这句台词的背后,隐藏着关于这个寺庙“高知僧团”的一个段子。
因为各种不可描述的原因,18年9月之后,我就很少去这个寺庙了,但我知道我依然很向往寺庙的生活。于是22年春,我下定决心离开北京去深圳,其中就有考虑到深圳后,可以去弘法寺或弘源寺做义工。
去了一次弘法寺,感觉那边人太多,后面去了一次弘源寺后,感觉这里比较适合我,人少很安静,不堵车的话,开车只需20分钟到家。
目前,只要我有时间,我都会去弘源寺干一天临时义工,或者住上几天。
何为禅?
禅,是心智的高度成熟状态。直至印度词汇传入,汉语音译为“禅那”,后世简称为“禅”,汉译意思有“静虑”、“思维修”等。
禅修的方法就是禅法,禅法是心法,并不固着于某种具体形式,也不限于宗教派别。从泛义来说,任何一种方法,如果能够让你的心灵成熟,了解生命的本质,让心获得更高层次的证悟,并从而获得生命究竟意义的了悟。这样的方法就是禅修!
从狭义来说,在绵延传承数千年的漫长时空里,形成各种系统的修行方法,存在于各种教派中。现存主要有传承并盛行于南传佛教国家的原始佛教禅法与传承并盛行于中国汉传佛教的祖师禅。
如来禅是佛陀的原始教法,注重基础练习,强调修行止观。祖师禅是中国禅宗祖师的教法,强调悟性、觉性,推崇顿悟,以参话头为代表,以开悟、明心见性为目的。
我们普遍缺乏自我觉察,甚至误解了包括自由在内的生命状态真义。禅修中,会进入深刻自我觉察中,有机会与自己整合,从而开启真我。
近年来,禅修在西方非常流行,像美国的学校、医疗机构和高科技公司都广泛地在进行打坐、禅修。美国有些科学家曾做过一个实验,实验对象是长期禅修的修行人。在实验室中,实验者一边用脑电波图测量脑波的变化,一边用功能性核磁共振测量脑部活动的位置。
最后得出结论:通过禅修,不但能够短期改变脑部的活动,而且非常有可能促成脑部永久的变化。这就是说:通过禅定,可以有效断除人的焦虑、哀伤等很多负面情绪,创造出心灵的幸福感,甚至可以重塑大脑结构。
禅修能够修复心智、疗愈抑郁、提升智慧,让我们重获身心的全面健康!禅修让人的内心变得安静。在禅修时,人能放松下来,专注于呼吸,使内心归于平静,身体和心灵才有了真正的对话与接触。
“禅修是未来科技世界中的生存必需品”时代杂志曾在封面报道中这样写道。在硅谷,禅修被认为是新的咖啡因,一种能释放能量与创造力的全新“燃料”。
禅修也帮助过谷歌、Facebook、Twitter高管们走出困惑,国内比较知名则有搜狐的张朝阳和阿里的马云,还有微信之父张小龙的传说。
对于他们来说,商海的起伏伴随着心海的沉浮,庞大的财富、名声与地位带来的更多的不是快乐,但是禅修,却在一定程度上给他们指点迷津,带领他们脱离现代社会的痛苦、让内心更加平静!
乔布斯的禅修故事
乔布斯和禅修,一直有着很深的渊源。乔布斯是当世最伟大的企业家之一,同时也是一名虔诚的禅宗教徒。他少有慧根,17岁那年,他远赴印度寻找圣人寻求精神启蒙,18岁那年,他开始追随日本禅师乙川弘文学习曹洞宗的禅法。
年轻的时候,乔布斯去印度,在印度体验,呆了七个月。乔布斯在印度干了些什么,我们不得而知。不过据我推测,也就是四处逛一逛,看一看,可能会去一些寺庙,拜访一些僧人。
我从来不认为,他遇到了什么高人,或者在印度的小村庄一待,精神就受到了莫大的洗礼。变化永远都是从内在发生的,外在的不过是缘分,是过客,负责提供一个合适的环境,或者提供一些必要的刺激。
但我们知道,从此以后,乔布斯的人生,就开始变得不一样了。乔布斯的人生追求是“改变世界”,当年他劝说百事可乐总裁,来担任苹果CEO的时候所说的话:“你是愿意一辈子卖糖水,还是跟我一起改变这个世界?”激励了无数心怀梦想的朋友。
早在1973年乔布斯已经对禅有较深的领悟了。他这样说:“我对那些超越物质的形而上的学说极感兴趣,也开始注意到比知觉及意识更高的层次——直觉和顿悟。”
他还说:“因为时间有限,不要带着面具为别人而活,不要让别人的意见左右自己内心的想法,最重要的是要勇敢地忠于自己内心的直觉。”
乔布斯说:“你不能预先把点点滴滴串在一起;唯有未来回顾时,你才会明白那些点点滴滴是如何串在一起的。所以你得相信,你现在所体会的东西,将来多少会连接在一块。你得信任某个东西,直觉也好,命运也好,生命也好,或者业力。这种作法从来没让我失望,也让我的人生整个不同起来。”
他大学时学的书法,被他用来设计能够印刷出漂亮字体的计算机,尽管他在大学选修书法课时,完全不知道学这玩意能有什么用。他被自己创立的苹果公司开除,于是转行去做动画,结果在做动画的时候,遇到了自己未来的妻子。
人呐实在不知道,自己可不可以预料。你说我一个被自己创立的公司开除的失业狗,怎么就在第二份工作里遇到了一生的挚爱呢?若干年后,他回顾起自己的人生,他把这些点点滴滴串了起来,他发现,他所经历过的每一件事,都有着特殊的意义。
所以,无论面对怎样的困境,我们都不必悲观绝望,因为在剧本结束之前,你永远不知道,自己现在面对的这件事,到底是坏事还是好事。
所谓创新就是无中生有,包括思想、产品、艺术等,重大的创新我们称为颠覆。通过那则著名的广告《think different》他告诉世人:“因为只有那些疯狂到以為自己能够改变世界的人,才能真正地改变世界。”乔布斯确实改变了世界,而且不是一次,至少五次颠覆了这个世界:
- 通过苹果电脑Apple-I,开启了个人电脑时代;
- 通过皮克斯电脑动画公司,颠覆了整个动漫产业;
- 通过iPod,颠覆了整个音乐产业;
- 通过iPhone,颠覆了整个通讯产业;
- 通过iPad,重新定义并颠覆了平板PC行业。
程序员与禅修
编程是一门需要高度专注和创造力的艺术,它要求程序员们在面对复杂问题和压力时,能够保持内心的安宁和平静。在这个快节奏、竞争激烈的行业中,如何修炼内心的禅意境界,成为程序员们更好地发挥潜力的关键。
在编程的世界里,专注是至关重要的品质。通过培养内在专注力,程序员能够集中精力去解决问题,避免被外界的干扰所困扰。以下是几种培养内在专注的方法:
- 冥想和呼吸练习: 通过冥想和深呼吸来调整身心状态,让自己平静下来。坚持每天进行一段时间的冥想练习,可以提高专注力和注意力的稳定性。
- 时间管理: 制定合理的工作计划和时间表,将任务分解为小的可管理的部分,避免心理上的压力。通过专注于每个小任务,逐步完成整个项目。
- 限制干扰: 将手机静音、关闭社交媒体和聊天工具等干扰源,创造一个安静的工作环境。使用专注工作法(如番茄钟),集中精力在一项任务上,直到完成。
编程过程中会遇到各种问题和挑战,有时甚至会感到沮丧和失望。然而,保持平和的心态是非常重要的,它可以帮助程序员更好地应对压力和困难。以下是一些培养平和心态的技巧:
- 接受不完美性: 程序永远不会是完美的,因为它们总是在不断发展和改进中。接受这一事实,并学会从错误中汲取教训。不要过于苛求自己,给自己一些宽容和理解。
- 积极思考: 关注积极的方面,让自己的思维更加积极向上。遇到问题时,寻找解决方案而非抱怨。积极的心态能够帮助你更好地应对挑战和困难。
- 放松和休息: 给自己合理的休息时间,让大脑得到充分的放松和恢复。休息和娱乐能够帮助你调整心态,保持平和的状态。
编程往往是一个团队合作的过程,与他人合作的能力对于一个程序员来说至关重要。以下是一些建立团队合作意识和促进内心安宁的方法:
- 沟通与分享: 与团队成员保持良好的沟通,分享想法和问题。倾听他人的观点和建议,尊重不同的意见。积极参与和贡献团队,建立合作关系。
- 友善和尊重: 培养友好、尊重和包容的态度。尊重他人的工作和努力,给予鼓励和支持。与团队成员建立良好的关系,创造和谐的工作环境。
- 共享成功: 当团队取得成功时,与他人一起分享喜悦和成就感。相信团队的力量,相信集体的智慧和努力。
修炼内心安宁需要时间和长期的自我管理。通过培养专注力、平和心态、创造力和团队合作意识,程序员们可以在面对复杂的编程任务和挑战时保持内心的安宁和平静。
禅修有许多不同的境界,其中最典型的可能包括:
- 懵懂:刚开始禅修时,可能会觉得茫然和困惑,不知该如何开始。
- 困扰:在进行深度内省和冥想时,可能会遇到很多烦恼和难题,需耐心思考和解决。
- 安和:通过不断地练习和开放自己的心灵,可能会进入一种更加平和和沉静的状态。
- 祥和:当一些心理障碍得到解决,你会感受到一种更深层的平静和和谐。
- 转化:通过不断的冥想与内省,你可以向内看到自己的内心,获得对自己和世界的新的认识和多样的观察角度。
- 整体意识:通过冥想,您将能够超越个人的视野和言语本身,深入探究宇宙的内心,领悟更加深入和广泛的境界和意识。
程序员写代码的境界:
- 懵懂:刚熟悉编程语言,不知做什么。
- 困扰:可以实现需求,但仍然会被需求所困,需要耐心思考和解决。
- 安和:通过不断练习已经可以轻易实现需求,更加平和沉静。
- 祥和:全栈。
- 转化:做自己的产品。
- 整体意识:有自己的公司。
一个创业设想
打开小红书,与“疗愈”相关的笔记高达236万篇,禅修、瑜伽、颂钵等新兴疗愈方法层出不穷,无论是性价比还是高消费,总有一种疗愈方法适合你。
比起去网红景点打卡拍照卷构图卷妆造,越来越多的年轻人正在借助上香、拜神、颂钵、冥想等更为“佛系”的方式去追寻内心的宁静。放空大脑,呼吸之间天地的能量被尽数吸收体内,一切紧张、焦虑都被稀释,现实的残酷和精神的困顿,都在此间找到了出口。
在过去,简单的瑜伽和冥想就能达到这种目的,但伴随着疗愈文化的兴起与壮大,不断在传统方式之上叠加buff,才是新兴疗愈的终极奥义。
从目标人群来看,不同的禅修对应不同的人群。比如临平青龙寺即将在8月开启的禅修,就分为了企业禅修、教育禅修、功能禅修、共修禅、突破禅、网络共修等多种形式。但从禅修内容来看,各个寺庙的安排不尽相同,但基本上跳脱不出早晚功课、上殿过堂、出坡劳作、诵经礼忏、佛学讲座等环节。
艺术疗愈,是截然不同于起参禅悟道这种更亲近自然,还原本真的另一疗愈流派。具体可以细分为戏剧疗愈、绘画疗愈、音乐疗愈等多种形式。当理论逐渐趋向现代化,投入在此间的花费,也成正比增长。
绘画疗愈 ,顾名思义就是通过绘画的方式来表达自己内心的情绪。画幅的大小、用笔的轻重、空间的配置、色彩的使用,都在某种程度上反映着创作者潜意识的情感与冲突。
在绘画过程中,绘画者也同样会获得纾解和满足。也有一些课程会在绘画创作之外,添加绘画作品鉴赏的内容,通过一幅画去窥视作者的内心,寻求心灵上的共鸣,也是舒缓压力的一种渠道。
疗愈市场之所以能够发展,还是因为有越来越多人的负面情绪需要治愈。不论是工作压力还是亲密关系所带来的情绪内耗,总要有一个释放的出口。
当前,我正在尝试依托自营绘馆老师提供优质课件,打造艺培课件分享的平台或社区,做平台前期研发投入比较大,当前融资也比较困难,同时自己也需要疗愈。
所以,最近也在调研市场,评估是否可以依托自营的门店,组织绘画手工+寺庙行禅+技术专题分享的IT艺术禅修营活动,两天含住宿1999元,包括半天寺庙义工体验、半天禅修、半天绘画手工课和半天的技术专题分享。
不知道,这样的活动,大家会考虑参加吗?
总结
出家人抛弃尘世各种欲望出家修行值得尊重,但却不是修行的唯一方法,佛经里著名的维摩洁居士就是在家修行,也取得了非凡成就,六祖惠能就非常鼓励大家在世间修行,他说:“佛法在世间,不离世间觉,离世觅菩提,恰如求兔角”。
普通人的修行是在红尘欲望中的修行,和出家人截然不同,但无分高下,同样可以证悟,工作就是他们最好的修练道场。禅学的理论学习并不困难,但这只是万里长征的第一步,最重要的是,我们要在日常实践中证悟。
简单可能比复杂更难做到:你必须努力理清思路,从而使其变得简单。但最终这是值得的,因为一旦你做到了,便可以创造奇迹。”乔布斯所说的这种专注和简单是直接相关的,如果太复杂,心即散乱,就很难保持专注,只有简单,才能做到专注,只有专注,才能极致。
来源:juejin.cn/post/7292781589477687350
程序员的副业发展
前言
之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快
因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么
希望能对你有些帮助~
正文
学生单
学生单是我接过最多的,已经写了100多份毕设,上百份大作业了,这里给大家介绍一下
像python
这种的数据处理的大作业也很多,但是我个人不太会,所以没结过,我只说我做过的
我大致做过几种单子,最多的是学生的单子,分为大作业单子
和毕设单子
大作业单一般指一个小作业,比如:
- 几个web界面(大多是html、css、js)
- 一个全栈的小demo,大多是
jsp+SSM
或者vue+springboot
,之所以不算是毕设是因为,页面的数目不多,数据库表少,而且后端也很简单
我不知道掘金这里能不能说价格,以防万一我就不说大致价格了,大家想参考价格可以去tb
或者咸鱼
之类的打听就行
然后最多的就是毕设单子,一般就是一个全栈的项目
- 最多的是
vue+springboot
的项目,需求量特别大,这里说一下,之前基本都是vue2的项目,现在很多学校要求vue3了,但是大部分商家vue3的模板很少,所以tb上接vue3的项目要么少,要么很贵,所以我觉得能接vue3和springboot项目的可以打一定的价格战,vue2的市面上价格差不多,模板差不多,不好竞争的 - 少数
vue+node
的全栈项目,一般是express
或者koa
,价格和springboot差不多,但是需求量特别少 uni+vue+springboot
的项目,其实和vue+springboot
项目差不多,因为单纯的vue+springboot项目太多了,所以现在很多人要求做个uni手机端,需求量适中.net项目
,信管专业的学生用.net的很多,需求量不少,有会的可以考虑一下
这是我接过的比较多的项目,数据库我没有单说,基本上都是MySQL,然后会要求几张表,以及主从表有几对,这就看客户具体要求了
需要注意的点:大部分你得给客户配环境,跑程序,还是就是毕设一般是要求论文的,有论文的会比单纯程序赚的多,但是一定要注意对方是否要求查重,如果要求查重,一般是不建议接的,一般都是要求维普和知网查重,会要了你的老命。还有需要注意的是,学生单子一般是需要答辩的,你可以选择是否包答辩,当然可以调整价格,但是你一旦包答辩,你的微信在答辩期间就不会停了。你永远不知道他们会有怎样的问题
商业单
商业单有大有小,小的跟毕设差不多,大的需要签合同
我接的单子大致就一种,小程序+后台管理+后端
,也就是一个大型的全栈项目,要比学生单复杂,而且你还要打包、部署、上线,售后
,有一个周期性,时间也比较长
为了防止大家不信,稍微放几个聊天记录,是这两个月来找的,也没有给自己打广告,大家都是开发者,开发个小程序有什么打广告,可吹的(真的是被杠怕了)
技术栈有两种情况:自己定
,客户定
UI也有两种情况:有设计图的
、无设计图的(也就是自己设计)
基本上也就是两种客户:懂技术的客户,不懂技术的客户
指定技术栈的我就不说了,对于不指定技术栈的我大致分为两种
小程序端:uni/小程序原生、后台:vue、后端:云开发
小程序端:uni/小程序原生、后台:vue、后端:springboot
这取决于预算,预算高的就用springboot、预算不高的就云开发一把嗦,需要说的是并不是说云开发差,其实现在云开发已经满足绝大部分的需求,很完善了,而springboot则是应用广泛,客户后期找别人接手更方便
对于没有UI
设计图的,我会选择去各种设计网站去找一些灵感
当项目达到一定金额,会签署合同,预付定金,这是对双方的一种保障
其实在整个项目中比较费劲的是沟通,不是单独说与客户的沟通,更多的是三方沟通,作为上线的程序,需要一些资料手续,这样就需要三方沟通,同时还有一定的周期,可能会被催
讲解单
当然,有的时候人家是有程序的,可能是别人代写的,可能是从开源扒下来的,这个时候客户有程序,但是看不懂,他们可能需要答辩,所以会花钱找人给他们梳理一下,讲一讲, 这种情况比较简单,因为不需要你去写代码,但是需要你能看懂别人的代码
这种情况不在少数,尤其是在小红书这种单子特别多,来钱快,我一般是按照小时收费
知识付费这东西很有意思,有时候你回答别人的一些问题,对方也会象征性地给你个几十的红包
接单渠道
我觉得相对于什么单,大家更在意的是怎么接单,很多人都接不到单,这才是最难受的
其实对此我个人并没有太好的建议的方法,我认为最重要的,还是你的交际能力
,你在现实中不善于交际,网络上也不善于交际,那就很难了
因为我之前是在学校,在校期间干过一些兼职,所以认识的同学比较多,同时自身能力还可以,所以会有很多人来找,然后做完之后,熟人之间会慢慢介绍,人就越来越多,所以我不太担心能否接单这件事,反而是单太多,自己甚至成立一个小型工作室去接单
如果你是学生的话,一定要在学校积累客户,这样会越来越多,哪怕是现在我还看到学校的各种群天天有毕业很多年以及社会人士来打广告呢,你为什么就不可以呢
当然但是很多人现在已经不是学生了,也不知道怎么接触学生,那么我给大家推荐另外的道路
闲鱼
接单小红书
接单
大部分学生找的写手都会比较贵,这种情况下,很多学生都会选择去上面的两个平台去货比三家,那么你的机会就来了
有人说不行啊,这种平台发接单帖子就被删了,那么你就想,为什么那么多人没被删,我也没被删,为什么你被删除了
其次是我最不推荐的一种接单方式:tb写手
为什么不推荐呢,其实就是tb去接单,然后会在tb写手群外包给写手,也就是tb在赚你的差价
这种感觉很难受,而且赚的不多,但是如果你找不到别的渠道,也可以尝试一下
最后
我只是分享一下自己接单的方式,但是说实在的,接一个毕设单或者是商业单其实挺累的,不是说技术层面的,更多的是心累,大家自行体会吧,而且现在商场内卷严重,甚至有人200、300就一个小程序。。。
所以大家要想,走什么渠道,拿什么竞争
另外,像什么猪八戒这种的外包项目的网站,我只是见过,但是没实际用过,接过,所以不好评价
希望大家赚钱顺利,私单是一种赚钱的方式,但是是不稳定的,一定还是要以自己本身的工作为主,自行判断~
来源:juejin.cn/post/7297124052174848036
小程序和h5有什么差别
差别
微信小程序和H5应用在实现原理上的差异主要体现在架构、渲染方式、数据通信、运行环境和API接口等方面。以下是详细的对比:
1. 架构和运行环境
微信小程序:
架构:微信小程序主要分为逻辑层(JavaScript)和视图层(WXML、WXSS)。逻辑层运行在小程序的JSCore中,而视图层运行在WebView(它是基于浏览器内核重构的内置解析器,它并不是一个完整的浏览器,官方文档中重点强调了脚本内无法使用浏览器中常用的
window
对象和document
对象,就是没有DOM
和BOM
的相关的API
,这一条就干掉了JQ
和一些依赖于BOM
和DOM
的NPM包)中,两者通过平台提供的桥接机制进行通信。运行环境:逻辑层在微信提供的JS引擎中运行,视图层在微信内置的WebView中渲染。
H5 应用:
架构:H5应用是一个整体。HTML、CSS和JavaScript共同构成了一个Web页面。
运行环境:H5应用在浏览器中运行,所有代码都在浏览器的环境中解析和执行。
2. 渲染方式
微信小程序:
微信小程序采用双线程模型,将逻辑层和视图层分离,分别运行在不同的线程中(两者通过平台提供的桥接机制进行通信):
逻辑层:运行在小程序的JSCore环境中,负责处理业务逻辑、数据计算和API调用。
视图层:运行在WebView中,负责渲染用户界面和处理用户交互。( 性能提升:由于小程序的渲染过程并不依赖于JS,因此即使JS线程发生阻塞,页面的渲染也不会受到影响。这种机制有利于提高渲染效率,减少卡顿,提升用户体验。)
通信桥接机制
逻辑层和视图层之间不能直接访问和操作对方的数据和界面,因此需要通过微信小程序框架提供的桥接机制来进行通信。这种通信机制通常包括以下几个方面:
1. 数据绑定和响应式更新(逻辑层--->视图层)
逻辑层通过数据绑定的方式将数据传递给视图层,视图层根据数据变化自动更新界面。数据绑定的过程如下:
设置数据:逻辑层通过
Page
或Component
实例的setData
方法,将数据传递给视图层。更新视图:视图层接收到数据变化的消息后,根据新的数据重新渲染界面。
2. 事件处理(视图层--->逻辑层)
视图层中的用户交互(如点击、输入等)会触发事件,这些事件通过桥接机制传递给逻辑层进行处理。事件处理的过程如下:
事件绑定:在视图层(WXML)中定义事件处理函数。
事件触发:用户在界面上进行交互时,触发相应的事件。
事件传递:视图层将事件信息通过桥接机制传递给逻辑层。
事件处理:逻辑层的事件处理函数接收到事件信息,执行相应的业务逻辑。
3. 消息传递
逻辑层和视图层之间的通信实际是通过消息传递的方式实现的。微信小程序框架负责在两个层之间传递消息,包括:
逻辑层到视图层的消息:如数据更新、视图更新等。
视图层到逻辑层的消息:如用户交互事件、视图状态变化等
通信桥接机制具体实现
依赖于微信小程序框架内部的设计和优化,开发者无需直接接触底层的通信细节。以下是桥接机制的一些关键点:
消息队列:逻辑层和视图层之间维护一个消息队列,用于存储待传递的消息。
消息格式:消息以JSON格式进行编码,包含消息类型、数据内容等信息。
消息处理:逻辑层和视图层各自维护一个消息处理器,负责接收、解析和处理消息。
异步通信:消息传递通常是异步进行的,以确保界面和逻辑的流畅性和响应性
H5 应用:
H5应用的逻辑层和视图层通常是在同一线程(主线程)中运行,直接通过JavaScript代码操作DOM来更新界面。主要的通信方式包括:
直接DOM操作:通过JavaScript直接操作DOM元素,更新界面。
事件监听和处理:通过JavaScript监听DOM事件(如点击、输入等)并处理。
数据绑定:使用现代前端框架(如Vue.js、React.js)的数据绑定和响应式机制,实现视图的自动更新。
3. 数据通信
微信小程序:
通信机制:逻辑层和视图层之间的通信通过小程序框架提供的机制来实现,通常是通过事件和数据绑定。
后台通信:可以通过小程序提供的API与服务器通信,例如wx.request等。
H5 应用:
通信机制:页面内的通信可以通过DOM事件、JavaScript函数调用等方式实现。
后台通信:可以使用标准的AJAX请求、Fetch API、WebSocket等方式与服务器通信。
4. 运行机制
微信小程序
启动
如果用户已经打开过某小程序,在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,整个过程就是所谓的
热启动
如果用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动,就是
冷启动
销毁
当小程序进入后台一定时间,或系统资源占用过高,或者是你手动销毁,才算真正的销毁
h5:解析HTML CSS形成DOM树和CSSOM树,两者结合形成renderTree,js运行,当然中间存在一系列的阻塞问题,还有同源策略等等
5. 系统权限方面(特定功能)
微信小程序依托于微信平台,能够利用微信提供的特有功能和API,实现许多H5应用无法直接实现或不易实现的功能,如微信支付、微信登录、硬件接口(如摄像头、麦克风、蓝牙、NFC等)、微信特有功能等。
6.更新机制
h5更新后访问地址即可
微信小程序需要审核
开发者在发布新版本之后,无法立刻影响到所有现网用户,要在发布之后 24 小时之内才下发新版本信息到用户
小程序每次
冷启动
时,都会检查有无更新版本,如果发现有新版本,会异步下载新版本代码包,并同时用客户端本地包进行启动,所以新版本的小程序需要等下一次冷启动
才会应用上,当然微信也有wx.getUpdateManager
可以做检查更新
7. 开发工具和调试
微信小程序:
开发工具:微信提供了专门的开发者工具,集成了调试、预览、上传等功能,方便开发者进行开发和测试。
调试:可以使用微信开发者工具进行实时调试,并提供丰富的日志和调试信息。
H5 应用:
开发工具:可以使用任何Web开发工具和IDE(如VS Code、WebStorm等),以及浏览器的开发者工具进行调试。
调试:依赖浏览器的开发者工具(如Chrome DevTools),可以进行断点调试、查看网络请求、分析性能等。
总结来说,微信小程序和H5应用在实现原理上的差异主要是由于它们的架构设计、运行环境和生态系统的不同。小程序依托于微信平台,提供了许多平台专属的优化和功能,而H5应用则更加开放和灵活,依赖于浏览器的标准和特性。
小程序为什么使用双层架构
微信小程序采用双线程架构的原因主要是为了优化性能和用户体验。双线程架构将逻辑层和视图层分离,使得业务逻辑处理和视图渲染在不同的线程中进行,从而提高了小程序的运行效率和响应速度。以下是采用双线程架构的具体原因和优势:
提高性能:
将逻辑处理和页面渲染分离到不同的线程中,可以避免互相干扰,提高整体性能。例如,在复杂的业务逻辑计算过程中,视图层仍然可以保持流畅的界面更新和响应。
逻辑层和视图层通过消息机制进行异步通信,可以避免阻塞和卡顿。这样即使逻辑层的操作较为耗时,也不会影响界面的即时响应。
安全性: 视图层无法直接操作逻辑层的数据和代码,这样可以避免一些潜在的安全风险和漏洞。
XSS
由于逻辑层和视图层分离,视图层不能直接执行逻辑层的JavaScript代码。这种隔离使得即使视图层(WXML)中存在注入的恶意代码,也不能直接影响逻辑层的数据和操作。
逻辑层和视图层之间的通信通过统一的API进行,传递的数据会经过平台的安全检查和过滤,进一步减少了XSS攻击的风险。
CSRF
小程序通过平台的统一API进行请求,这些请求包含了平台自动添加的安全令牌(如
session_key
等),确保请求的合法性。由于逻辑层和视图层的分离,用户在视图层进行操作时,逻辑层的业务逻辑和数据处理经过平台的校验,减少了CSRF攻击的风险。
DOM篡改:视图层的DOM结构由WXML和WXSS定义,不能直接通过逻辑层的JavaScript代码进行操作,这种隔离减少了DOM篡改的可能性。
安全权限管理:小程序的API权限由平台统一管理和控制,开发者需要申请和用户授权后才能使用特定的API。
用户体验: 微信小程序在启动时可以并行加载逻辑层和视图层资源,减少初始加载时间,提升启动速度。同时,微信平台会对小程序进行预加载和缓存优化,进一步提升加载性能。
rpx
微信的自适应单位,可以根据屏幕宽度进行自适应。
在微信小程序中,1 rpx 表示屏幕宽度的 1/750,因此 rpx
和 px
的换算关系是动态的,基于设备的实际屏幕宽度。
作者:let_code
来源:juejin.cn/post/7389168680747614245
展开收起的箭头动画应该怎么做?
背景
我们在日常开发中,一定经常遇到折叠面板的开发,为了美观,我们经常会添加展开收起按钮,并且带有箭头旋转动画。
比如下面的几种情况
- 文字点击变化,且有箭头旋转动画
- 只有箭头动画
这几种情况的核心其实就是:点击箭头开始旋转,再点击箭头恢复初始位置。
如何实现
思路分析
要实现展开和收起箭头的旋转动画,我们可以使用 CSS 和 JavaScript。我们在点击按钮时,通过添加和移除 CSS 类,实现箭头的旋转动画。并且添加transition属性实现过渡效果。
代码实现
我们以第一种动画效果为例,先写基础代码
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span>▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
现在我们点击按钮,只有文字会变化,箭头不会旋转
我们给按钮加一个动态类
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
可以看到,展开的时候有动画,但是收起的时候是没有过渡效果的。
我们只需要加一个transition属性即可
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }" class="arrow">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.arrow {
transition: transform 0.3s linear;
}
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
现在样式就ok了
html版本
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arrow Rotation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<button id="toggleButton">
<span id="arrow" class="arrow">▼</span>
</button>
</div>
<script src="script.js"></script>
</body>
</html>
css
/* styles.css */
.container {
text-align: center;
margin-top: 50px;
}
#toggleButton {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
outline: none;
}
.arrow {
display: inline-block;
transition: transform 0.3s ease;
}
.arrow.rotate {
transform: rotate(180deg);
}
js
// script.js
document.getElementById('toggleButton').addEventListener('click', function() {
const arrow = document.getElementById('arrow');
arrow.classList.toggle('rotate');
});
这种方式可以实现箭头在点击时的旋转动画效果。在实际项目中使用,我们也可以根据具体需求调整样式和逻辑。
来源:juejin.cn/post/7385132403025149989
如果iconfont停止服务了,我们怎么办
前言
个人一直都比较喜欢阿里大佬提供的一些组件服务什么的,这里就只说图标管理吧,最开始的时候我是使用的icomoon.io 后面发现http://www.iconfont.cn/ 在以前开发是时候我们为了图片的请求少点,提升网站的性能会把这些小图标做到一张图上面专业叫法是雪碧图,后来随着前端发展有了字体图标,不啰嗦了吧,回到正题,由于上次icofont官网挂了之后,导致我的项目无法进行正常迭代,我就决定要自己弄个简单的图标管理。
需求
一般使用需要的是把设计做好的图标或者其他地方下载的svg图标上传到服务器然后可以查看,并且打包成一个js文件加载到项目中。
准备
都说天下文章一大抄,我也是去抄iconfont,我把iconfont的使用demo打开研究了一下。
我这里只说Symbol,通过分析看到就是我们需要在项目中引入./iconfont.js
iconfont把我们上传的图标进行了删减优化,最外层就一个svg标签里面使用了一个symbol标签来包裹之前的图标内容,每一个symbol的id都对应之前的svg图标名称。
iconfont也没有开源,具体怎么做的咱也不知道,只能知道目前这些信息了,然后就按照自己理解开始开发实现了
前端开发
通过上面我们可以知道,现在我们前端需要的是把设计稿的svg图变成symbol里面的内容,打开svg图 可以简单测试下,直接把内容复制然后粘贴带了iconfont.js中,然后运行demo图标正常显示就说明是ok的,不然就是这样做是不行的。
通过图片我们可以看到,现在需要的就是把svg里面的path改成用symblo来包裹,在开发中svg图我们是通过文档流上传上来的,这个时候我们就需要对文档流进行操作,先把文档流变成字符串,然后js操作字符串拼接达到目的。
- 使用到FileReader和readAsText获取到字符串
const fileParse = (file) => {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsText(file, "UTF-8");
fileReader.onload = (e) => {
resolve({ content: e.target?.result, name: file.name })
}
})
}
- 字符操作拼接我使用的是cheerio
const handleUploadSvg = ($, result) => {
let index = result.indexOf('
通过上面的操作我们可以得到一个新的svg源码,图标显示还是flow
然后我把这个字符串传送给后端就行了
后端开发
后端要做的就是把我上传上来的svg字符串拼接到一起,然后以iconfont.js一样通过一个get接口返回给我就可以了, 通过浏览器可以访问到就说明ok了
其他两种需要用到些第三方库当时也有顺便研究看到了,如果有需要我再写一篇。
文章中会有些不怎么清晰或者有问题的地方还望各位大佬见谅,刚刚开始决定写一些技术相关文章,还很生疏
来源:juejin.cn/post/7340197367515578378
iOS 开发们,是时候干掉 Charles 了
这里每天分享一个 iOS 的新知识,快来关注我吧
前言
一说到 mac 上的抓包工具,大家自然而然的会想到 Charles,作为老牌抓包工具,它功能很全面,也很强大。但是随着系统的不断更新迭代,Charles 的一些缺点也慢慢表露出来,比如:
- 卡顿,特别在一些低端 Mac 机型上比较卡,体验就很差
- 吃内存,时间久了总是得重启一下,不然内存吃的太多
- 页面老旧,感觉像是旧时代的产品
今天来介绍一个我觉得比较好用的抓包工具,Proxyman
Proxyman 配置
安装就不说了,大家可以自行去官网下载安装。
Proxyman 提供了一个免费版本,其中包含所有基本功能,平时使用应该是够了,如果重度使用,也可以考虑购买高级版本。
这是他的主页面,看起来是不是挺干净的:
安装好了之后都需要配置代理和 https 证书,这点 Proxyman 做的非常好,首先点击顶部导航上的证书,可以看到所有安装证书的选项:
教程是全中文的,而且设置步骤非常详细,比如 iOS 设置指南:
Proxyman 针对 iOS 开发还提供了一种无配置的方案,可以直接通过 Pod 或者 SPM 添加 atlantis-proxyman
框架,这样可以在不进行任何配置的情况下进行代理监听:
除了监控手机的流量,也可以很方便地添加 iOS 模拟器的监控,只需要选择顶部菜单 -> 证书 -> 在 iOS 上安装证书 -> 模拟器:
按照以上步骤操作即可。
使用
配置完成之后就可以在 Proxyman 主页面上看到接口请求了,接下来介绍一些常用的功能。
本地 Mock 数据
本地 Mock 数据是很常见的需求,你只需要选中某个接口后,鼠标右键,选择工具 -> 本地映射:
然后在弹出的新页面中编辑相应即可,非常方便:
断点
断点工具可以让我们动态编辑请求或响应的内容。
它本地映射在同一个菜单栏里,鼠标右键,选择工具 -> 断点,然后进行对应的设置即可。
创建断点后,Proxyman 将在收到我们想要拦截的请求或响应后立即打开一个新的编辑窗口。然后我们根据需要修改数据,最后再继续即可。
导出请求和响应数据
有时候我们需要把有问题的接口保存下载给其他服务端的同学查看。选中具体的请求,点击鼠标右键,选择导出,然后再选择你要导出的格式:
不过这里导出的 Proxyman 日志需要使用 Proxyman 才能打开,也就是说,需要想查看这条请求的人的电脑上也安装 Proxyman,如果他没有安装,也可以选择拷贝 cURL。
模拟弱网
好的产品一定能够在弱网下正常使用,所以弱网测试也成为了日常开发必要的步骤,点击顶部菜单栏,选择工具 -> 网络状况,可以打开一个新页面,然后点击左下角为一个新的域名添加网络状况,这里可以根据你的需求选择不同的网络状况:
总结
从流畅度、功能引导等方面,我感觉 Proxyman 是比 Charles 好用的,除了以上介绍到的功能,还有很多更强大更全面的功能。例如远程映射、保存会话、GraphQL 调试、黑名单白名单、Protobuf、自定义脚本等等,大家可以自己试试看。
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
来源:juejin.cn/post/7355845238906175551
Flutter 为什么没有一款好用的UI框架?
哈喽,我是老刘
前两天,系统给我推送了一个问题。
我理解提问者真正想问的是:有没有一个不用学习那么多UI组件和渲染知识,可以简单快速搭建UI的东西。
Flutter 包括原生开发,为什么需要考虑那么多细节,不能做的简单一些?
首先,我们需要明白Flutter的定位。
Flutter不是一个简单的甜品,而是一个能支撑大型系统开发的工程级框架。
这种定位和原生框架的定位是相当的。
因此,它要求整个框架有足够的灵活性,能适用于尽可能多的场景。
那么,如何提供足够的灵活性呢?
答案是让整个框架尽可能多的细节是可控的。
这就需要把整个框架的功能拆分的更细,提供的配置项足够多。
然而,这样的缺点就是开发起来会比较麻烦,需要控制很多细节。
因此,我们可以看到Flutter的组件拆分的很细,甚至有类似Padding这样专门负责缩进的组件,而且每个组件都有很多的配置参数。
Flutter配合Material组件库本身本就非常优秀的UI框架
虽然Flutter的灵活性带来了开发上的复杂性,但Flutter配合Material组件库本身就是一个非常优秀的UI框架。
Material组件库提供了丰富的预设组件,这些组件遵循Material Design指南,可以帮助开发者快速搭建出既美观又符合设计规范的UI界面。
使用Material组件库,开发者可以不必从头开始设计每一个UI元素,而是可以直接使用现成的组件,如按钮、对话框、卡片等,这些组件都有良好的交互和动画效果。
此外,Material组件库还提供了主题支持,开发者可以通过简单的配置,快速应用统一的风格到整个应用中。
因此,虽然Flutter的灵活性可能让初学者感到有些复杂,但配合Material组件库,Flutter实际上提供了一个非常高效和优秀的UI开发体验。
大型项目的正确打开方式
即便是Material组件库,它的设计是需要考虑应对各种不同类型app开发的,但是针对一个具体的项目,我们大多数时候不需要这样高的灵活性。
所以,这种情况下直接用Flutter提供的组件效率会比较低。
解放方法就是针对特定的项目做组件封装。
以我目前维护的项目为例,我们项目中所有的对话框都是相同的偏绿色调,圆角半径20,按钮大小固定,标题、详情的字体、字号也固定。
简单来说,就是所有的UI细节都是固定的,只是不同的dialog需要填充的文字不同。
这时候,我们就会定义一个自己的Dialog组件,只需要使用者传入标题和内容,以及设置按钮的回调即可。
UI的其他地方也是如此,比如页面框架、在多个页面都能用到的用户卡片、商品卡片等等。
当你的整个App大部分都是基于这些自定义组件进行搭积木式的开发,那开发效率是不是比找一些通用的UI框架更高呢?
总结
总而言之,Flutter因为它的工程级框架定位需要提供高度的灵活性,而这往往会导致开发细节的复杂性。
但是,通过针对具体项目的组件封装,我们可以大大提高开发效率,同时保持UI的一致性和项目的特定需求。
所以,与其寻找一个通用的UI框架,不如根据项目的具体需求进行自定义组件的开发。
如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》
来源:juejin.cn/post/7387001928209170447
劝互联网的牛马请善待自己
掘友们,大家好,我是一名95后全栈程序媛,一直以来在努力追求WLB,28岁前完成了畅游中国,既努力生活也认真工作。很多人可能还不知道WLB这个词,WLB就是work life balance
,一开始我看到这个词都是从猎头那里传过来的,岗位招聘一般都是:xxx神仙外企WLB,一周只上3天班,每周两天可以居家办公,每年年假几十天,要求英语口语流利,有x年工作经验,base范围在xx个w....
众所周知,外企的年包肯定不如那些一线爆肝厂,当然工作时间跟收入都是成正比的,965的外企跟11116的互联网的年包肯定是不一样的,付出的工作时间都不一样。假如拼夕夕给你年薪百万,神仙外企给你年薪50+w,你会怎么选?
最近出来几条消息刺痛了牛马的心情!这个世界变幻莫测~
四十多岁的程序员在公司工作11年被裁员
徐峥出新电影了《逆行人生》讲述的就是一个四十多岁的程序员被裁员后找不到工作只能去送外卖的心酸故事,当现实照进电影,卑微的打工人在时代的潮流下只是一个渺小颗粒的缩影。


得物“35岁被暴力裁员”、“80余万元期权直接打水漂”。
一年前,面临裁员的得物员工徐凯多次与公司沟通取得期权再离职未果后,他到上海市仲裁委员会处申请恢复与得物的劳动关系,后被予以支持。7月,因不服上海市仲裁委员会裁定的结果,得物继续上诉,再度将前员工诉于法庭之上。
去哪儿宣布每周可居家办公两天
这则消息意味着互联网公司开启新的里程碑,向神仙外企的福利看齐了,对于老弱病残的打工人简直不要太友好了。

这种待遇,以前在互联网几乎不存在的。一周连休四天的日子,体验过就不再想去卷996的牛马岗了。
不管我们有多努力,我们都只是老板眼里赚钱的工具人
在互联网,35岁已经是一道坎,人在互联网漂,哪有不挨刀,不管有多努力,到了大龄的年纪,工资比年轻人高产出比年轻人低的时候,面临着公司随时都可能会说:分手吧,没有分手费,你自己知难而退吧,大家好聚好散!像极了一个渣男遇到了更年轻漂亮的白富美抛弃糟糠之妻,完了还pua你说都是你的错我才选择了别人。同样,公司会pua说,都是你的没能力,我才选择了别的员工,渣男有道德的谴责?公司有吗?公司跟你只有劳动关系,只要合法,随时跟你说你被毕业了,给个n+1的分手费都要被牛马说这渣渣企真良心!
对于老板来说,赚钱的时候大家都是兄弟,不赚钱了不认兄弟,说好聚好散!你把公司当家,公司把你当牛马。这点,我们真的要向00后学习,提前认清职场,打工就是为了赚钱,为了更好的生活,并不是为了努力加班毁了我们的生活,那我们辛辛苦苦打工有什么意义呢?
是否真的对自己的选择满意
知乎上有一个热度很高的话题:阿里p7和副处级哪个更厉害?
总说纷云,有人选择p7:

有人选择为人民服务;

也有人两者都想要:

在稳定和高薪面前,大家都想要稳定高薪的工作,最后变成稳定焦虑的牛马,这就好像围城,体制内的羡慕体制外的高薪,高薪的牛马羡慕体制内的稳定。即使义无反顾选择了卷互联网,几年挣够了人家一辈子的钱,但是买了二居想换三居,买了三居想换别墅,收入的增长带来消费的提升,物欲的无限放大,依然很多年入百万的人并不觉得真正的快乐而满足现状!即使选择了稳定的体制内,工作体面生活稳定,但是在权力面前,一直追名逐利,在很多诱惑下,最后的选择身不由己!
所以,欲望面前,你有好好认真的生活吗?认真对待自己的身体健康吗?是为了碎银几两熬夜加班把身体搞垮还是为了三餐有汤就行选择WLB呢?希望每一个焦虑的互联网牛马都好好善待自己,平衡好自己身体的健康和对金钱物欲的追逐。
我最羡慕内心富裕,内核稳定的人,这种人一般要比同龄人状态更年轻。不容易被外界所干扰,明确知道自己该要什么,不该要什么,选择适合自己的生活,幸福满意度极高。
来源:juejin.cn/post/7390457313163067431
买房后,害怕失业,更不敢裸辞,心情不好就提前还房贷,缓解焦虑
自从买房后,心态有很大变化。虽然住自己的房子,心情和体验都很好,但是一把掏空钱包,很焦虑。买房后现金流一直吃紧,再加上每年16万的房贷,我很焦虑会失业。之前我喜欢裸辞,现在不敢想裸辞这个话题。尤其是在行业下行期,找工作很艰难,背着房贷裸辞,简直是头孢就酒,嫌命太久。
焦虑的根源是背负房贷,金额巨大,而且担心40岁以后失业,还不上房贷。
一次偶然的沟通
"你的带款利率调整了吗",同事问我。
同事比我早两年在北京买房,他在顺义买的,我在昌平买的,我俩一直有沟通房贷的问题。但我没听说利率有调整,银行好像也没通知我,于是我问道:”我不知道啊,你调整了?调到多少了?“。
”调的挺多的,已经降到了 4.3%“。同事兴高采烈的回复我。
”这么牛逼,之前我记得一直是4.85%,我去看看我的利率“,我听说房贷利率下降那么多,很是兴奋。
然而我的房贷利率没有调整,我尝试给银行打电话,沟通的过程很坎坷。工商银行客服说了很多,大概意思是:利率会自动调整,无需申请,但是要等到利率调整日才会调整”。我开始很不理解,很生气,利率都调整了,别人也都调整了,凭什么不给我调整呢?
我想到同事有尝试提前还贷,生气的时候,我就萌发了提前还贷的想法。
开始尝试提前还贷,真香
我在22年初带款买房,其中商业带款 174 万,带款25年,等额本息,每个月要还 1 万的房贷。公积金带款每个月大概需要还 2500。每个月一万二的房贷还是很有压力的,尤其是刚买房的这一两年,兜里比脸都干净,没存款,不敢失业,更不敢裸辞。
即便兜里存款不多,也要提前还贷,因为实在太香了。
我在工行App上,申请 提前还贷,选择缩短 18个月的房贷,只需要 6万2,而我每个月房贷才1万,相当于是用 6 万 顶 18 万的房贷。还有比这更划算的事情吗?
预约提前还款后,银行会安排一个时间,在这个时间前,把钱存进去。到时候银行就会扣除,如果扣除金额不足,那么提前还款计划则自动终止,需要重新预约!
工行的预约还款时间大概是1个月以后,我是10-15号申请提前还款,银行给的预约日期是 11-14号,大概是1个月。
提前还款,比理财强多了
这次还贷以后,我又申请了提前还款, 提前还 24 期,只需要 9 万,也就是 9 万顶 24 万;提前还 60 期,只需要 24 万,相当于 24 万顶 60 万。
还有比提前还贷收益更高,风险更低的理财方式吗?没有! 除了存款外,任何理财都是有风险的。债券和基金收益和风险挂钩,想找到收益5%的债券基金,要承担亏本风险。你惦记人家的利息,人家惦记你的本金!
股票的风险更不必说,我买白酒股票已经被套的死死,只能躺平装死。(劝大家不要入 A 股)
提前还贷划算吗?
我目前的带款利息是 4.85%,而存到银行的利息不会超过 3% ,很多货币基金只有 2%了。两者利息差高达 3%,肯定是提前还带款更加合适。
要明白,一年两年短期的利息差还好,但是房贷可是高达 25 年。25年 170 万带款 3% 的利息差,这个金额太大了。提前还了,省下来的钱还是很多的。例如刚才截图里展示的 提前还 24 万顶了 60 万的房贷。
网上很多砖家说,“要考虑通货膨胀因素,4.85% 的带款利率和实际通货膨胀比起来不高,提前还款不划算。”
砖家说话都是昧良心的。提前还带款是否划算,只需要和存款利率比就行了,不需要和通货膨胀比。因为把钱存在银行也会因为通货膨胀贬值。只有把钱 全都消费,全部花光才不会受通货膨胀的困扰,建议砖家,多消费,把家底败光,这样最划算!
砖家们一定是害怕太多人提前还贷,影响了银行的放贷生意。今年上半年,提前还贷已经成潮流,有些银行坐不住,甚至关闭了提前还贷的入口…… 所以要抓紧,没准哪天就提高了还贷门槛,或者直接禁止。
程序员群体收入高,手里闲钱多,可以考虑提前还带款,比存银行划算多了,别再给银行打工了!
来源:juejin.cn/post/7301530293378727971
刚入职因为粗心大意,把事情办砸了,十分后悔
刚入职,就踩大坑,相信有很多朋友有我类似的经历。
5年前,我入职一家在线教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。
在熟悉环境之后,我趁着上厕所的时候,顺便去旁边的零食摊挑了点零食。接下来的一天里,我专注地配置开发环境、阅读新人文档,当然我也不忘兼顾手边的零食。
初出茅庐,功败垂成
"好景不长",第三天上午,刚到公司,屁股还没坐热。新组长立刻给我安排了任务。他决定让我将配置端的课程搜索,从使用现有的Lucene搜索切换到ElasticSearch搜索。这个任务并不算复杂,然而我却办砸了。
先说为什么不复杂?
- ElasticSearch的搜索功能 基于Lucene工具库实现的,两者在搜索请求构造方式上几乎一致,在客户端使用上差异很小。
- 切换方案无需顾虑太多稳定性问题。由于是配置端课程搜索,并非是用户端搜索,所以平稳切换的压力较小、性能压力也比较小。
总的来说,领导认为这个事情并不紧急,重要性也不算高,而且业务逻辑相对简单,难度能够把握,因此安排我去探索一下。可是,我却犯了两个错误,把入职的第一件事办砸了。现在回过头来看,十分遗憾!
难以解决的bug让我陷入困境
将搜索方式从Lucene切换为ElasticSearch后,如何评估切换后搜索结果的准确度呢?
除了通过不断地回归测试,还有一个更好的方案。
我的方案是,在调用搜索时同时并发调用Lucene搜索和ElasticSearch搜索。在汇总搜索结果时,比对两者的搜索结果是否完全一致。如果在切换搜索引擎的过程中,两个方案的搜索结果不一致,就打印异常搜索条件和搜索结果,并进行人工排查原因。
在实际切换过程中,我经常遇到搜索数据不一致的情况,这让我感到十分苦恼。我花了一周的时间编写代码,然后又用了两周多的时间来排查问题,这超出了预估的时间。在这个过程中,我感到非常焦虑和沮丧。作为一个新来的员工,我希望能够表现出色,给领导留下好印象。然而事与愿违,难以解决的bug让我陷入困境。
经过无数次的怀疑和尝试,我终于找到了问题的根源。原来,我忘记了添加排序方式。
因为存在很多课程数据,所以配置端搜索需要分页搜索。在之前的Lucene搜索方式中,我们使用课程Id来进行排序。然而在切换到新的ElasticSearch方案中时,我忘记了添加排序方式。这个错误的后果是,虽然整体上结果是一致的,但由于新方案没有排序方式,每一页的搜索结果是随机的,无法预测,所以与原方案的结果不一致。
新方案加上课程Id排序方式以后,搜索结果和原方案一致。
为此,我总结了分页查询的设计要点!希望大家不要重复踩坑!# 四选一,如何选择适合你的分页方案?
千万不要粗心大意
实际上,在解决以上分页搜索没有添加排序方式的问题之后,还存在着许多小问题。而这些小问题都反映了我的另一个不足:粗心大意。
正是这些小问题,导致线上环境总会出现个别搜索结果不一致的情况,导致这项工作被拖延很久。
课程模型是在线教育公司非常核心的数据模型,业务逻辑非常复杂,当然字段也非常多。在我入职时,该模型已经有120 个字段,并且有近 50 个字段可以进行检索。
在切换搜索方式时,我需要重新定义各种DO、DTO、Request等类型,还需新增多个类,并重新定义这些字段。在这个过程中,我必须确保不遗漏任何字段,也不能多加字段。当字段数量在20个以内时,这项工作出错的可能性非常低。然而,班课模型却有多达120个字段,因此出错的风险极大。当时我需要大量搬运这些字段,然而我只把这项工作看作是枯燥乏味的任务,未能深刻意识到出错的可能性极大,所以工作起来散漫随意,没有特别仔细校验重构前后代码的准确性。
墨菲定律:一件事可能出错时就一定会出错
墨菲定律是一种普遍被接受的观念,指出如果某件事情可能出错,那么它将以最不利的方式出错。这个定律起源于美国航天局的项目工程师爱德华·墨菲,在1950年代发现了这一规律。
墨菲定律还强调了人类的倾向,即将事情弄糟或让事情朝着最坏的方向发展。它提醒人们在计划和决策时要考虑可能出错的因素,并准备应对不利的情况。
墨菲定律实在是太准了,当你感觉某个事情可能会出错的时候,那它真的就会出错。而且我犯错不止一次,因为有120个字段,很多字段的命名非常相似,最终我遗漏了2个字段,拼写错误了一个字段,总共有三个字段出了问题。
不巧的是,这三个字段也参与检索。当用户在课程搜索页面选择这三个字段来进行检索时,因为字段的拼写错误和遗漏,这三个字段没有被包含在检索条件中,导致搜索结果出错……
导致这个问题的原因有很多,其中包括字段数量太多,我的工作不够细致,做事粗心大意,而且没有进行充分的测试……
为什么没有测试
小公司的测试人员相对较少,尤其是在面对课程管理后台的技术重构需求时,更加无法获取所需的测试资源!
组长对我说:“ 要人没有,要测试更没有!”
事情办砸了,十分遗憾
首先,从各个方面来看,切换搜索引擎这件事的复杂度和难度是可控的,而且目标也非常明确。作为入职后第一项任务,我应该准确快速地完成它,以留下一个良好印象。当然,领导也期望我能够做到这一点,然而事实与期望相去甚远。
虽然在线上环境没有出现问题,但在上线后,问题排查的时间却远远超出了预期,让领导对结果不太满意。
总的来说,从这件事中,我获得的最重要教训就是:对于可能出错的事情,要保持警惕。时刻用墨菲定律提醒自己,要仔细关注那些可能发生小概率错误的细节问题。
对于一些具有挑战性的工作,我们通常都非常重视,且在工作中也非常认真谨慎,往往不会出错。
然而,像大量搬运代码、搬运大量字段等这类乏味又枯燥的工作确实容易使人麻痹大意,因此我们必须提高警惕。要么我们远离这些乏味的工作,要么就要认真仔细地对待它们。
否则,如果对这些乏味工作粗心大意,墨菲定律一定会找上你,让你在线上翻车!
来源:juejin.cn/post/7295576148364787751
记一种不错的缓存设计思路
之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。
场景
假设有个以下格式的接口:
GET /api?keys={key1,key2,key3,...}&types={1,2,3,...}
其中 keys 是业务主键列表,types 是想要取到的信息的类型。
请求该接口需要返回业务主键列表对应的业务对象列表,对象里需要包含指定类型的信息。
业务主键可能的取值较多,千万量级,type 取值范围为 1-10,可以任意组合,每种 type 对应到数据库是 1-N 张表,示意:
现在设想这个接口遇到了性能瓶颈,打算添加 Redis 缓存来改善响应速度,应该如何设计?
设计思路
方案一:
最简单粗暴的方法是直接使用请求的所有参数作为缓存 key,请求的返回内容为 value。
方案二:
如果稍做一下思考,可能就会想到文首我提到的觉得不错的思路了:
- 使用
业务主键:表名
作为缓存 key,表名里对应的该业务主键的记录作为 value; - 查询时,先根据查询参数 keys,以及 types 对应的表,得到所有
key1:tb_1_1
、key1:tb_1_2
这样的组合,使用 Redis 的 mget 命令,批量取到所有缓存中存在的信息,剩下没有命中的,批量到数据库里查询到结果,并放入缓存; - 在某个表的数据有更新时,只需刷新
涉及业务主键:该表名
的缓存,或令其失效即可。
小结
在以上两种方案之间做评估和选择,考虑几个方面:
- 缓存命中率;
- 缓存数量、占用空间大小;
- 刷新缓存是否方便;
稍作思考和计算,就会发现此场景下方案二的优势。
另外,就是需要根据实际业务场景,如业务对象复杂度、读写次数比等,来评估合适的缓存数据的粒度和层次,是对应到某一级组合后的业务对象(缓存值对应存储 + 部分逻辑),还是最基本的数据库表/字段(存储的归存储,逻辑的归逻辑)。
来源:juejin.cn/post/7271597656118394899
有哪些事情,是当了程序员之后才知道的?
1、平庸的程序员占比很大。 还没参加工作时,觉得程序员是个改变世界的高科技职业,后来才发现,其实这个群体里有很多CRUD Boy和SQL Boy,Ctrl C+Ctrl V是我们使用最多的电脑操作,没有之一。
而且,大多数同事上班摸鱼偷懒,遇到问题躲着走,下班也从来不主动学习充电。每当我问他们,他们都说这样混着也挺好,不想太累。
2、数量堆死质量。 如果你觉得没有写代码的天赋,那么请你先写10万行代码再说。
如果你在刷leetcode的时候非常痛苦,甚至有时候看答案都看不懂。那你就先把代码背下来,然后一遍一遍默写。每当你默写五遍以上,就开始慢慢理解了,刷十遍以上,再遇到类似的题,就有条件发射,能举一反三了。
这种方法运用到看底层源码,看一些晦涩难懂的技术类书籍上,也同样适用。
后来,我在网上看了硅谷王川的一段话:所有的我们以为的质量问题,大多本质是数量问题。数量是最重要的质量。
而欧成效则说得更加直接:数量堆死质量!
3、尽量选择研发出身的老板的公司。 他们会知道程序员不是故意写bug的,也没有任何系统能做到100%的可用性。
而销售出身的老板,却永远把自家公司的程序员看做产出并不令人满意的高成本项。而且还时不时地要求程序员跟销售一样喊几句令人其鸡皮疙瘩的鸡血口号。
4、大厂和小厂的程序员,技术上差距并不大。 他们的差距也许是在学历上,也许是在人脉上,也许是在沟通和向上管理上。
5、对测试同学客气一点, 他们是你写的代码的最后一道防线。再有就是,如果线上出了故障或者严重bug,很多产研以外的人都关注是哪个程序员造成了事故,而不是哪个测试同学没测出来。
6、产品经理是SB,甲方是SB的N次方。 最令人蛋疼的是,任何一家公司都是这样,所以你根本避无可避,只能长期共存。
7、程序员涨薪,最好的方式是跳槽, 而不是兢兢业业地加班工作。如果就靠公司每年涨的那些钱,估计得用7,8年才能实现薪资翻番。但如果靠跳槽,估计3年就能实现薪资翻番。
8、能不去外包公司就尽量不去,那种寄人篱下的无归属感才最让人心累。你会发现,公司的正式员工吃饭和娱乐都是不愿意带你玩儿的,平时跟你说话的表情也是鼻孔朝天。
9、面试造火箭,工作拧螺丝是正常的。 你要做的就是提升造火箭吹牛逼的能力,毕竟这才是你定级谈薪的资本。不要抱怨,要适应。
10、35岁的危机真的存在。 那些认为技术牛逼就可以平稳度过中年危机的人,很多都SB了。人老不以筋骨和技术为能,顺势而为,尽早找后路才是王道。
11、尽量去工程师占比超过30%的公司,因为它的期权可能在未来十年内变得很有价值。因为工程师占比越高,边际成本就越低。
12、离开公司这个平台,也许你什么都不是。 很多大厂的高P前辈,甚至是总监、 VP,也可能在某一个时间点,突然被淘汰!我身边就有一个BAT的总监,真的就突然被优化了,真的就找不到哪怕一半的薪资了。突然之间!
拔剑四顾心茫然.... 所以,永远要分清楚哪些是平台资源,哪些是你的能力。时刻对自身能力保持清醒且准确的认知,千万不要陷入盲目自负的境地。实在太过乐观的大厂朋友,可以周期性出来面试,哪怕不跳槽,认知自己的真实价值。
13、技术面试官问期望薪资,记得往低了说。 因为他们往往并不负责最终的定薪,但如果你的期望薪资高于他,会让他产生强烈的不平衡,从而把你Pass掉。
14、身体才是一切的本钱。 前些天左耳朵耗子前辈的忽然离世,再次验证了这一点,如果身体健康是0,那么其他的所有一切都是0。
15、脱发和格子衫的说法,并不普遍。 我认识的程序员里,80%是不穿格子衫的,而且35岁+的程序员,80%也是不脱发的。
但是有一种东西是很普遍的,那就是装着电脑的双肩包。
16、PPT架构师、周报合并师、无损复读师真的存在,而且越是在大厂,这种人就会越多。
PPT架构师在PPT中讲的架构各种高端大气上档次,其实就是大家很常用的部署流程;周报合并师每周的任务就是每周将团队中每个人的周报进行汇总,再报告给上级;无损复读师要求可能会高一些,对老板提出的问题或者质疑,要原原本本的向下传达给项目组对应的同学,不能有一丝偏差。
或许他们最开始不是这样的,但是慢慢地,他们活成了最舒服的,也是曾经最讨厌的样子。
17、大多数程序员是不会修电脑的。 很多行业以外的人,他们会觉得很多事情程序员都可以做,从盗QQ,Photoshop,硬盘文件恢复,到装系统,处理系统故障和软件问题,安装各种盗版软件,各种手机的越狱Root装盗版应用。
并且,另外这些事情往往不涉及实物,给人的感觉是只是在键盘上打打字,又不需要买新硬件之类的,所以往往会被认为是举手之劳,理应帮忙。
18、杀死程序员不用枪,改三次需求就可以了。 很多程序员并不反感别人说他无趣,也不反感别人说他们的穿着土鳖,也不反感别人说他们长相平庸。
也就是说,除了反复改需求,别的他们都能忍受。
先说这么多吧,总结得也算是比较全了,后续有新的,我再补充。
来源:juejin.cn/post/7292960995437166601