注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

职场上有什么谎言?

努力干活就能赚多点钱 职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的...
继续阅读 »

努力干活就能赚多点钱


职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的报酬,而是更倾向于平衡员工的工作与生活,提高员工的幸福感和满意度。因此,新进职场人士应该认识到,在职场中坚持适度的工作量、良好的工作习惯和优秀的职业素养才是取得成功的重要因素。


我都是为你好


“我都是为你好”可能是一种常见的谎言,在不同情境下被使用。在某些情况下,这可能真诚地表达出对他人的关心和照顾,但在其他情况下,这也可能成为掩盖自己私人动机或者行为错误的借口。因此,在职场和日常生活中,我们需要学会审视这句话所蕴含的背后意图,并判断其是否真实可信。同时,我们也应该秉持着开放、坦诚、尊重和理解的态度,与他人进行良好的沟通和相处,以建立健康、和谐的人际关系。


他做得比你好,向他好好学习


“他做得比你好,向他好好学习”是一句非常有益的建议,可以让人们从成功的经验中汲取营养,不断提高自己的能力和水平。在职场中,人们面对不同的工作任务和挑战,而且每个人的工作方式、思维模式和经验都不同,因此,我们应该善于借鉴他人的优点和长处,吸取别人的经验和教训,不断完善自己的职业素养和技能。然而,这并不意味着要完全依赖和模仿别人,而是应该在合适的时机,根据自身实际情况和需要,加以改进和创新,开拓自己的专业视野和发展空间。


在职场中,有些人可能会通过拍马屁、拉关系等不正当手段来获取自己的利益或者提高自己的地位。然而,这种做法可能会导致负面后果和损失,例如破坏工作团队的合作氛围、损害自己的职业形象和信誉等。因此,我们应该始终保持清醒和冷静的头脑,不受拍马屁等诱惑,专注于自己的工作和职责,努力提高自己的专业水平和职业素养。同时,我们也应该与他人建立良好的人际关系,以合理、公正、透明的方式展示自己的才华和成果,赢得别人的尊重和信任,并在适当的时刻借助他人的力量来实现共同的目标。


公司不怎么赚钱,理解一下,行情好了加工资


如果公司在过去设定了一些目标和承诺,但无法兑现或者没有达到预期的结果,那么这就是一种失信行为。画饼充当推销手段,可能会对员工、客户和利益相关方造成误导和不良影响,并破坏公司的商誉和形象。因此,公司应该根据市场实际情况和自身能力水平,制定合理、可行的计划和策略,避免过于浮夸和虚幻的承诺,注重落实和执行,加强与员工、客户和社会各方的沟通和互动,建立坦诚、透明的企业文化和价值观念。同时,员工也应该保持客观、谨慎、理性的态度,不盲目追求高回报或者虚假宣传,始终以个人职业道德和职责为先,为公司和自

作者:象骑士
来源:juejin.cn/post/7213636024102469693
己的未来发展负责任。

收起阅读 »

糟了,生产环境数据竟然不一致,人麻了!

大家好,我是冰河~~ 今天发现Mysql的主从数据库没有同步 先上Master库: mysql>show processlist; 查看下进程是否Sleep太多。发现很正常。 show master status; 也正常。 mysql> sh...
继续阅读 »

大家好,我是冰河~~


今天发现Mysql的主从数据库没有同步


先上Master库:


mysql>show processlist;

查看下进程是否Sleep太多。发现很正常。


show master status;

也正常。


mysql> show master status;
+-------------------+----------+--------------+-------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+-------------------------------+
| mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
+-------------------+----------+--------------+-------------------------------+
1 row in set (0.00 sec)

再到Slave上查看


mysql> show slave status\G                                                

Slave_IO_Running: Yes
Slave_SQL_Running: No

可见是Slave不同步


解决方案


下面介绍两种解决方法


方法一:忽略错误后,继续同步


该方法适用于主从库数据相差不大,或者要求数据可以不完全统一的情况,数据要求不严格的情况


解决:


stop slave;

#表示跳过一步错误,后面的数字可变
set global sql_slave_skip_counter =1;
start slave;

之后再用mysql> show slave status\G 查看


mysql> show slave status\G
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

ok,现在主从同步状态正常了。。。


方式二:重新做主从,完全同步


该方法适用于主从库数据相差较大,或者要求数据完全统一的情况


解决步骤如下:


1.先进入主库,进行锁表,防止数据写入


使用命令:


mysql> flush tables with read lock;

注意:该处是锁定为只读状态,语句不区分大小写


2.进行数据备份


#把数据备份到mysql.bak.sql文件


mysqldump -uroot -p -hlocalhost > mysql.bak.sql

这里注意一点:数据库备份一定要定期进行,可以用shell脚本或者python脚本,都比较方便,确保数据万无一失。


3.查看master 状态


mysql> show master status;
+-------------------+----------+--------------+-------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+-------------------------------+
| mysqld-bin.000001 | 3260 | | mysql,test,information_schema |
+-------------------+----------+--------------+-------------------------------+
1 row in set (0.00 sec)

4.把mysql备份文件传到从库机器,进行数据恢复


scp mysql.bak.sql root@192.168.128.101:/tmp/

5.停止从库的状态


mysql> stop slave;

6.然后到从库执行mysql命令,导入数据备份


mysql> source /tmp/mysql.bak.sql

7.设置从库同步,注意该处的同步点,就是主库show master status信息里的| File| Position两项


change master to master_host = '192.168.128.100', master_user = 'rsync',  master_port=3306, master_password='', master_log_file =  'mysqld-bin.000001', master_log_pos=3260;

8.重新开启从同步


mysql> start slave;

9.查看同步状态


mysql> show slave status\G  

Slave_IO_Running: Yes
Slave_SQL_Running: Yes

10.回到主库并执行如下命令解除表锁定。


UNLOCK TABLES;

好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~

作者:冰_河
来源:juejin.cn/post/7221858081495203897
~

收起阅读 »

某用户说他付钱了订单状态未修改

背景 某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查...
继续阅读 »



背景


某项目中有一个收费业务,生成订单,订单状态为待支付。在移动端上打开该功能时可查看到待支付的订单,然后用户可以对待支付的订单进行支付。但偶尔出现客户反馈支付后订单还是待支付的状态,导致用户无法继续使用接下来的功能,导致用户的体验行特别差。重新梳理一遍,排查该问题。


涉及服务:



  • server1 主服务业务,提供所有业务的服务;

  • m-server:提供移动端 H5 视图和部分业务功能,部分功能业务都直接请求 server1;


总体架构


2022-09-02-10-11-16-image.png


总体流程:



  • 用户打开移动端应用时,由 m-server 提供页面视图;

  • 移动端相关业务数据由 server1 提供,即用户请求时,会到 m- server 再由其转发到 server1 服务上;

  • 相关支付业务由 m-server 服务与微信支付交互,支付完成后再由 m-server 与 server1 交互,同步订单的状态;


详细支付时序图:


支付流程的时序图,可以参考微信的官网:pay.weixin.qq.com/wiki/doc/ap…


支付流程:



  • 用户点击支付时,向 m-server 发起支付,然后生成订单

  • 向微信支付发起生成预付单

  • 点击微信支付的支付,此时会与微信支付进行验证支付授权权限

  • 微信返回支付授权,然后用户输入密码,确认支付,向微信支付服务提交授权

  • 微信支付返回支付结果给用户,并发微信消息提醒,同时会向 m-server 异步通知支付结果

  • m-server 接收到支付结果将同步给 server ,然后server 变更订单状态结果

  • m-server 显示最后结果给用户,如支付成功的订单详情


订单状态同步设计:


2022-09-02-10-34-24-image.png


订单状态流程:



  • server1 生成订单并记录到 db 中

  • m-server 从 server 中获取到订单的列表

  • m-server 接收到微信支付成功时,就会告知 server 支付成功,然后由 server 将订单状态修改为已支付


问题分析


已支付成功了,但订单状态却还是未支付成功?


首先,订单的状态由待支付到支付成功,必须是由微信支付服务异步通知 m-server 支付成功,然后再由 m-server 通知 server1 去修改订单状态。


所以,无论 m-server 还是 server1 服务在支付期间发生抖动都可能导致支付成功的信息成功通知给 server1 ,从而导致订单状态修改失败。还有一种可能性,微信支付服务可能没有异步通知。毕竟是第三方发起通知,所以也可能发生未通知情况。


优化方案


为了保证订单状态最终结果状态一致性,需要增加服务高可用,且可以支持自动重新发送订单状态变更的请求,及时重发重试。


详细设计


2022-09-02-10-45-00-image.png



  • m-server 确认支付时也将订单信息进行存储,状态为待支付

  • m-server 接收到微信支付成功通知后,就转发告知server1 服务

  • server1 修改订单状态进行响应,m-server 接收到响应进行删除或者修改订单状态(可按需进行),m-server 这里订单信息没有用了就也可以删除

  • 同时开启一个异步轮询 m-server 存储的订单信息,对于订单状态是待支付的,进行重发重试。这个过程需要先和微信支付服务确认确实是已支付,然后再将信息重新发送 server1,告知将订单状态调整为已支付


// 定时轮询订单信息状态
func notifyAuto() {
// 异步定时监控订单状态的变化
var changeOrder = func() {
result := orderService.getUnPayOrder(0) // 获取当前未支付状态的订单
if len(result) > 0 {
for v := range result {
status := wechat.getOrderStatus(v.orderId)
if status == 1 { // 订单在微信上时已支付的,需重新调用server 修改订单状态
orderService.sendOrderFinish(v.orderId)
}
}
}
}

go func() {
time.AfterFunc(time.Minute*10, changeOrder)
}()
}
复制代码
作者:小雄Ya
来源:juejin.cn/post/7138609603356901413
>
收起阅读 »

1.0 除 0 没抛出错误,我差点被输送社会

简言 在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。 先让我们来试一试 public class TestDouble { public static v...
继续阅读 »

简言


在项目中,我使用了错误的除法,除零没有抛出错误,程序运行正常,但是数据异常。组长说要把我输送社会,老板说还可以挽留一下,我们来复盘一下这次错误。


先让我们来试一试


public class TestDouble {   
public static void main(String[] args) {
System.out.println(1.0 / 0);
}
}

你认为的我认为的它应该会抛出 ArithmeticException 异常


但是它现在输出了 Infinity



为什么呢?


Double 数据类型支持无穷大


还有其他类型支持吗?


有,还有 Float


下面我们来查看 Double 源码,可以看到


/** 
* 一个常数,保持类型的正无穷大
*/

public static final double POSITIVE_INFINITY = 1.0 / 0.0;
/**
* 一个常数,保持类型的负无穷大
*/

public static final double NEGATIVE_INFINITY = -1.0 / 0.0;
/**
* 一个常数,非数值类型
*/

public static final double NaN = 0.0d / 0.0;

下面来试验下 0.0/0 与 -1.0/0


Double 正无穷 = 1.0 / 0;
Double 负无穷 = -1.0 / 0;
System.out.println("正无穷:" + 正无穷);
System.out.println("负无穷:" + 负无穷);
Double 非数值 = 0.0 / 0;
System.out.println("非数值 0.0/0 ->" + 非数值);

输出:


正无穷:Infinity
负无穷:-Infinity
非数值 0.0/0 ->NaN

对无穷大进行运算


下面来测试对 Float 类型与 Doubloe 类型无穷大进行运算


public static void testFloatInfinity() {
Float infFloat = Float.POSITIVE_INFINITY;
Double infDouble = Double.POSITIVE_INFINITY;
System.out.println("infFloat + 5 = " + (infFloat + 5));
System.out.println("infFloat - infDouble = " + (infFloat - infDouble));
System.out.println("infFloat * -1 = " + (infFloat * -1));
}

输出:


infFloat + 5 = InfinityinfFloat - infDouble = NaNinfFloat * -1 = -Infinity

可以注意到 1,3 行运算符合我们的预计结果


ps: Infinity- Infinity 的结果不是数字类型


对这些值进行判断


public static void checkFloatInfinity() {   
Double 正无穷 = 1.0 / 0;
Double 负无穷 = -1.0 / 0;
Double 非数值 = 0.0 / 0;
System.out.println("判断正无穷: " + Double.isInfinite(正无穷));
System.out.println("判断负无穷: " + (Double.NEGATIVE_INFINITY == 负无穷));
System.out.println("判断非数值(==): " + (Double.NaN == 非数值));
System.out.println("判断非数值(isNaN): " + Double.isNaN(非数值));
}

输出:


判断正无穷: true
判断负无穷: true
判断非数值(==): false
判断非数值(isNaN): true


ps: 判断 NaN 不要使用 ==


作者:程序员鱼丸
来源:juejin.cn/post/7135621128818524174

收起阅读 »

谈谈国内前端的三大怪啖

web
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。 今天聊三个事情: 小程序 微前端 模块加载 小程序 每个行业都有一把银座,当坐上那把银座时,做什么...
继续阅读 »

因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。


今天聊三个事情:



  • 小程序

  • 微前端

  • 模块加载


小程序



每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。



“我们为什么需要小程序?”


第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。


于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?


说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。


即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:





看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。


但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。


所以从某种程度上来看,这更像是一场截胡的商业案例:


应用市场

全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。


只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。


反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。


另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。


在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?


毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)


那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。


那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?


于是,在 19 年夏天,深圳滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...


全新体验心智

小程序用起来挺方便的。


你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?



  1. 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。

  2. 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂

  3. 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。


H5小程序


  1. 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。


我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。


而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。


心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。


打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。




我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。


很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。


管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。


不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。



当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。



小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。


但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。


不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。


小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。


微前端


qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?


我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。


先说下我的看法:



  1. 微前端,重在解决项目管理而不在用户体验。

  2. 微前端,解决不了该优化和需要规范的问题。

  3. 微前端,在挽救没想清楚 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、ASP 直接返回带有数据的 HTML Ajax 一样的事情么。




传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。


但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...


到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,<script type="module"></script>,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。


“但我们用不了,有兼容性问题。”


哇哦,当我看着大家随意写出的 display: grid 样式定义,不禁再次感叹人们对未知的恐惧。



import.meta 的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…


试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。


模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史



历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。



结语


文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?


因为我们的智慧需要有开花的土壤。如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。


不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。



希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...



作者:YeeWang
来源:juejin.cn/post/7267091810366488632
收起阅读 »

刚来公司就接了一个不发版直接改代码的需求

前言 前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。 适用场景:代码逻辑需要经常变动的业务。 核...
继续阅读 »

前言


前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。



适用场景:代码逻辑需要经常变动的业务。


核心思想



  • 页面改动 java 代码字符串

  • java 代码字符串编译成 class

  • 动态加载到 jvm



实现重点


JDK 提供了一个工具包 javax.tools 让使用者可以用简易的 API 进行编译。



这些工具包的使用步骤:



  1. 获取一个 javax.tools.JavaCompiler 实例。

  2. 基于 Java 文件对象初始化一个编译任务 CompilationTask 实例。

  3. 因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后可以通过自定义的类加载器加载对应的类实例

  4. 使用反射 API 进行实例化和后续的调用。


1. 代码编译


这一步需要将 java 文件编译成 class,其实平常的开发过程中,我们的代码编译都是由 IDEA、Maven 等工具完成。


内置的 SimpleJavaFileObject 是面向源码文件的,而我们的是源码字符串,所以需要实现 JavaFileObject 接口自定义一个 JavaFileObject。


public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

public static final String CLASS_EXTENSION = ".class";

public static final String JAVA_EXTENSION = ".java";

private static URI fromClassName(String className) {
try {
return new URI(className);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(className, e);
}
}

private ByteArrayOutputStream byteCode;
private final CharSequence sourceCode;

public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
this.sourceCode = sourceCode;
}

public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
super(fromClassName(fullClassName), kind);
this.sourceCode = null;
}

public CharSequenceJavaFileObject(URI uri, Kind kind) {
super(uri, kind);
this.sourceCode = null;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return sourceCode;
}

@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(getByteCode());
}

// 注意这个方法是编译结果回调的OutputStream,回调成功后就能通过下面的getByteCode()方法获取目标类编译后的字节码字节数组
@Override
public OutputStream openOutputStream() {
return byteCode = new ByteArrayOutputStream();
}

public byte[] getByteCode() {
return byteCode.toByteArray();
}
}

如果编译成功之后,直接通过 CharSequenceJavaFileObject#getByteCode()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)


2. 实现 ClassLoader


因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后得通过自定义的类加载器加载对应的类实例,否则是加载不了的,因为同一个类只会加载一次。


主要关注 findClass 方法


public class JdkDynamicCompileClassLoader extends ClassLoader {

public static final String CLASS_EXTENSION = ".class";

private final static Map<String, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
super(parentClassLoader);
}


@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
JavaFileObject javaFileObject = javaFileObjectMap.get(name);
if (null != javaFileObject) {
CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
byte[] byteCode = charSequenceJavaFileObject.getByteCode();
return defineClass(name, byteCode, 0, byteCode.length);
}
return super.findClass(name);
}

@Override
public InputStream getResourceAsStream(String name) {
if (name.endsWith(CLASS_EXTENSION)) {
String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
if (null != javaFileObject && null != javaFileObject.getByteCode()) {
return new ByteArrayInputStream(javaFileObject.getByteCode());
}
}
return super.getResourceAsStream(name);
}

/**
* 暂时存放编译的源文件对象,key为全类名的别名(非URI模式),如club.throwable.compile.HelloService
*/

void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(qualifiedClassName, javaFileObject);
}

Collection<JavaFileObject> listJavaFileObject() {
return Collections.unmodifiableCollection(javaFileObjectMap.values());
}
}

3. 封装了上面的 ClassLoader 和 JavaFileObject


public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

private final JdkDynamicCompileClassLoader classLoader;
private final Map<URI, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();

public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
super(fileManager);
this.classLoader = classLoader;
}

private static URI fromLocation(Location location, String packageName, String relativeName) {
try {
return new URI(location.getName() + '/' + packageName + '/' + relativeName);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}

@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
if (null != javaFileObject) {
return javaFileObject;
}
return super.getFileForInput(location, packageName, relativeName);
}

/**
* 这里是编译器返回的同(源)Java文件对象,替换为CharSequenceJavaFileObject实现
*/

@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
classLoader.addJavaFileObject(className, javaFileObject);
return javaFileObject;
}

/**
* 这里覆盖原来的类加载器
*/

@Override
public ClassLoader getClassLoader(Location location) {
return classLoader;
}

@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (file instanceof CharSequenceJavaFileObject) {
return file.getName();
}
return super.inferBinaryName(location, file);
}

@Override
public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
List<JavaFileObject> result = new ArrayList<>();
// 这里要区分编译的Location以及编译的Kind
if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
// .class文件以及classPath下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
result.add(file);
}
}
// 这里需要额外添加类加载器加载的所有Java文件对象
result.addAll(classLoader.listJavaFileObject());
} else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
// .java文件以及编译路径下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
result.add(file);
}
}
}
for (JavaFileObject javaFileObject : superResult) {
result.add(javaFileObject);
}
return result;
}

/**
* 自定义方法,用于添加和缓存待编译的源文件对象
*/

public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
}
}

4. 使用 JavaCompiler 编译并反射生成实例对象


public final class JdkCompiler {

static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

@SuppressWarnings("unchecked")
public static <T> T compile(String packageName,
String className,
String sourceCode) throws Exception {
// 获取系统编译器实例
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 设置编译参数
List<String> options = new ArrayList<>();
options.add("-source");
options.add("1.8");
options.add("-target");
options.add("1.8");
// 获取标准的Java文件管理器实例
StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
// 初始化自定义类加载器
JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());

// 初始化自定义Java文件管理器实例
JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
String qualifiedName = packageName + "." + className;
// 构建Java源文件实例
CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
// 添加Java源文件实例到自定义Java文件管理器实例中
fileManager.addJavaFileObject(
StandardLocation.SOURCE_PATH,
packageName,
className + CharSequenceJavaFileObject.JAVA_EXTENSION,
javaFileObject
);
// 初始化一个编译任务实例
JavaCompiler.CompilationTask compilationTask = compiler.getTask(
null,
fileManager,
DIAGNOSTIC_COLLECTOR,
options,
null,
Collections.singletonList(javaFileObject)
);
Boolean result = compilationTask.call();
System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result));
Class<?> klass = classLoader.loadClass(qualifiedName);
return (T) klass.getDeclaredConstructor().newInstance();
}
}

完成上面工具的搭建之后。我们可以接入数据库的操作了。数据库层面省略,只展示 service 层


service 层:


public class JavaService {

public Object saveAndGetObject(String packageName,String className,String javaContent) throws Exception {
Object object = JdkCompiler.compile(packageName, className, javaContent);
return object;
}

}

测试:


public class TestService {

public static void main(String[] args) throws Exception {
test();
}

static String content="package cn.mmc;\n" +
"\n" +
"public class SayHello {\n" +
" \n" +
" public void say(){\n" +
" System.out.println(\"11111111111\");\n" +
" }\n" +
"}";

static String content2="package cn.mmc;\n" +
"\n" +
"public class SayHello {\n" +
" \n" +
" public void say(){\n" +
" System.out.println(\"22222222222222\");\n" +
" }\n" +
"}";

public static void test() throws Exception {
JavaService javaService = new JavaService();
Object sayHello = javaService.saveAndGetObject("cn.mmc", "SayHello", content);
sayHello.getClass().getMethod("say").invoke(sayHello);

Object sayHello2 = javaService.saveAndGetObject("cn.mmc", "SayHello", content2);
sayHello2.getClass().getMethod("say").invoke(sayHello2);
}
}

我们在启动应用时,更换了代码文件内存,然后直接反射调用对象的方法。执行结果:


可以看到,新的代码已经生效!!!



注意,直接开放修改代码虽然方便,但是一定要做好安全防护


作者:女友在高考
来源:juejin.cn/post/7134155429147312141

收起阅读 »

你身边的那些'加班文化'

说起来加班,别管什么80后,90后,00后,内心肯定都是非常抵制的,朝九晚六,平衡工作生活,想必是大多数人的梦想。 为什么突然想写一篇这样的文章,这两天确实有几件事情触动了我。 1.同事猝死 曾任职的一家公司,一名同事于4月1日猝死。以前总是看网上那些猝死的案...
继续阅读 »

说起来加班,别管什么80后,90后,00后,内心肯定都是非常抵制的,朝九晚六,平衡工作生活,想必是大多数人的梦想。

为什么突然想写一篇这样的文章,这两天确实有几件事情触动了我。


1.同事猝死


曾任职的一家公司,一名同事于4月1日猝死。以前总是看网上那些猝死的案例,感慨加班猝死,不曾想这件事生生的发生在自己身边,这名同事是一名销售,具体猝死缘由(前)公司并没有多谈,但我想一定是和熬夜加班息息相关。


image.png


这家公司的加班文化,我深有体会,去年11月初,各个项目组长在工作群先后通知强制要求加班,并不给你机会以及理由请假,反正加班给出的理由就是冲刺冲刺。


1.png


当时有同事用dingding和其他同事讨论加班的各种不情愿,聊天记录居然被公司扒出来并认为其散播负面言论,邮件全体对其进行警告,我大为震惊,当然这名同事后来也进入了裁员名单。


4.png


警告信发出之后越来越多的同事周末有一天不能来公司办公,其实他们在家根本也没闲着,这时候大老板急了,又发了这样的一封邮件。


5.png


(我看到这封邮件的时候,心里的想法就是作为公司的领头人,这格局不是一般的小,仅代表个人意见)

显然这封邮件是给那些不坚持10 10 7的人看的,对于那些不加班的人等待着的自然是优化。就这样还是有些同事继续坚持着10 10 7,在元旦之前大家终于把所有项目全部成功交付,本以为等待着大家的是老板分享喜悦的成果,结果等来的却是3/2的裁员,后来大家想明白了,就是催促着大家感觉把项目做完,裁掉你们。其中有位同事,前一天通宵加完班,第二天就被裁了,那时候整个办公室一片哗然。


2.持续加班住进icu


image.png


image.png


朋友的同事,据说刚刚升为一名奶爸,每天十点前不曾下班,大家问他为何这么拼,给出的理由是我刚刚生了娃,买了房子,工作一点不敢懈怠,怕惹得领导不高兴随时让我滚蛋。

是啊,谁想加班,谁愿意加班天天守着个破电脑,可是生活让我们不得不加班,不得不向一些不合理的规定屈服。


3.中国电科龙哥


这两天龙哥火遍了各个社交网络,龙哥痛批强制加班员工事情为什么能火,为什么传播速度这么快,我猜测是龙哥一人之言说出了很多打工人的心声,引起网友一片共鸣。


不过央视网的这文字怕是惹得大家一顿p喽。


720635d7223ec0b82485f989ae38034.jpg


职场人偶尔加下班,没有任何问题,毕竟工作中肯定会遇到一些紧急事件需要及时处理,作为公司员工,加班赶下进度也是应该的,可是长期被自愿加班就不合时宜了,毕竟身体是革命的本钱,一旦因为长期加班,出现问题就得不偿失了,所以加班文化不可取,尤其是超

作者:zhouzhouya
来源:juejin.cn/post/7218735340043092028
长加班更不应该存在。

收起阅读 »

项目中前端如何实现无感刷新 token!

前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下! 环境请求采用的 Axios V1.3...
继续阅读 »

前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下!


环境

  1. 请求采用的 Axios V1.3.2。
  2. 平台的采用的 JWT(JSON Web Tokens) 进行用户登录鉴权。
    (拓展:JWT 是一种认证机制,让后台知道该请求是来自于受信的客户端;更详细的可以自行查询相关资料)

问题现象


线上用户在使用的时候,偶尔会出现突然跳转到登录页面,需要重新登录的现象。


原因

  1. 突然跳转到登录页面,是由于当前的 token 过期,导致请求失败;在 axios 的响应拦截axiosInstance.interceptors.response.use中处理失败请求返回的状态码 401,此时得知token失效,因此跳转到登录页面,让用户重新进行登录。
  2. 平台目前的逻辑是在 token 未过期内,用户登录平台可直接进入首页,无需进行登录操作;因此就存在该现象:用户打开平台,由于此时 token 未过期,用户直接进入到了首页,进行其他操作。但是在用户操作的过程中,token 突然失效了,此时就会出现突然跳转到登录页面,严重影响用户的体验感!
    注:目前线上项目中存在数据大屏,一些实时数据的显示;因此存在用户长时间停留在大屏页面,不进行操作,查看实时数据的情况

切入点

  1. 怎样及时的、在用户感知不到的情况下更新token
  2. 当 token 失效的情况下,出错的请求可能不仅只有一个;当失效的 token 更新后,怎样将多个失败的请求,重新发送?

操作流程


好了!经过了一番分析后,我们找到了问题的所在,并且确定了切入点;那么接下来让我们实操,将问题解决掉。

前要:

1、我们仅从前端的角度去处理。

2、后端提供了两个重要的参数:accessToken(用于请求头中,进行鉴权,存在有效期);refreshToken(刷新令牌,用于更新过期的 accessToken,相对于 accessToken 而言,它的有效期更长)。


1、处理 axios 响应拦截


注:在我实际的项目中,accessToken 过期后端返回的 statusCode 值为 401,需要在axiosInstance.interceptors.response.useerror回调中进行逻辑处理

// 响应拦截
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (data.statusCode === 401) {
/**
* refreshToken 为封装的有关更新 token 的相关操作
*/
refreshToken(() => {
resolve(axiosInstance(config));
});
} else {
reject(error.response);
}
})
}
)


  1. 我们通过判断statusCode来确定,是否当前请求失败是由token过期导致的;

  2. 使用 Promise 处理将失败的请求,将由于 token 过期导致的失败请求存储起来(存储的是请求回调函数,resolve 状态)。理由:后续我们更新了 token 后,可以将存储的失败请求重新发起,以此来达到用户无感的体验


补充:


现象:在我过了几天登录平台的时候发现,refreshToken过期了,但是没有跳转到登录界面
原因

1、当refreshToken过期失效后,后端返回的状态码也是 401

2、发起的更新token的请求采用的也是处理后的axios,因此响应失败的拦截,对更新请求同样适用

问题:

这样会造成,当refreshToken过期后,会出现停留在首页,无法跳转到登录页面。

解决方法

针对这种现象,我们需要完善一下axios中响应拦截的逻辑

axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (
data.statusCode === 401 &&
config.url !== '/api/token/refreshToken'
) {
refreshToken(() => {
resolve(axiosInstance(config));
});
} else if (
data.statusCode === 401 &&
config.url === '/api/token/refreshToken'
) {
/**
* 后端 更新 refreshToken 失效后
* 返回的状态码, 401
*/
window.location.href = `${HOME_PAGE}/login`;
} else {
reject(error.response);
}
})
}
)

2、封装 refreshToken 逻辑


要点:



  1. 存储由于token过期导致的失败的请求。
  2. 更新本地以及axios中头部的token
  3. 当 refreshToken 刷新令牌也过期后,让用户重新登录
// 存储由于 token 过期导致 失败的请求
let expiredRequestArr: any[] = [];

/**
* 存储当前因为 token 失效导致发送失败的请求
*/
const saveErrorRequest = (expiredRequest: () => any) => {
expiredRequestArr.push(expiredRequest);
}

// 避免频繁发送更新
let firstRequre = true;
/**
* 利用 refreshToken 更新当前使用的 token
*/
const updateTokenByRefreshToken = () => {
firstRequre = false;
axiosInstance.post(
'更新 token 的请求',
).then(res => {
let {
refreshToken, accessToken
} = res.data;
// 更新本地的token
localStorage.setItem('accessToken', accessToken);
// 更新请求头中的 token
setAxiosHeader(accessToken);
localStorage.setItem('refreshToken', refreshToken);

/**
* 当获取了最新的 refreshToken, accessToken 后
* 重新发起之前失败的请求
*/
expiredRequestArr.forEach(request => {
request();
})
expiredRequestArr = [];
}).catch(err => {
console.log('刷新 token 失败err', err);
/**
* 此时 refreshToken 也已经失效了
* 返回登录页,让用户重新进行登录操作
*/
window.location.href = `${HOME_PAGE}/login`;
})
}

/**
* 更新当前已过期的 token
* @param expiredRequest 回调函数,返回由token过期导致失败的请求
*/
export const refreshToken = (expiredRequest: () => any) => {
saveErrorRequest(expiredRequest);
if (firstRequre) {
updateTokenByRefreshToken();
}
}

补充:


问题:

1、怎么能保证当更新token后,在处理存储的过期请求时,此时没有过期请求还在存呢?;万一此时还在expiredRequestArr推失败的请求呢?

解决方法
我们需要调整一下更新 token的逻辑,确保当前由于过期失败的请求都接收到了,再更新token然后重新发起请求。


最终结果:

// refreshToken.ts

/**
* 功能:
* 用于实现无感刷新 token
*/
import { axiosInstance, setAxiosHeader } from "@/axios"
import { CLIENT_ID, HOME_PAGE } from "@/systemInfo"

// 存储由于 token 过期导致 失败的请求
let expiredRequestArr: any[] = [];

/**
* 存储当前因为 token 失效导致发送失败的请求
*/
const saveErrorRequest = (expiredRequest: () => any) => {
expiredRequestArr.push(expiredRequest);
}

/**
* 执行当前存储的由于过期导致失败的请求
*/
const againRequest = () => {
expiredRequestArr.forEach(request => {
request();
})
clearExpiredRequest();
}

/**
* 清空当前存储的过期请求
*/
export const clearExpiredRequest = () => {
expiredRequestArr = [];
}

/**
* 利用 refreshToken 更新当前使用的 token
*/
const updateTokenByRefreshToken = () => {
axiosInstance.post(
'更新请求url',
{
clientId: CLIENT_ID,
userName: localStorage.getItem('userName')
},
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Authorization': 'bearer ' + localStorage.getItem("refreshToken")
}
}
).then(res => {
let {
refreshToken, accessToken
} = res.data;
// 更新本地的token
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setAxiosHeader(accessToken);
/**
* 当获取了最新的 refreshToken, accessToken 后
* 重新发起之前失败的请求
*/
againRequest();
}).catch(err => {
/**
* 此时 refreshToken 也已经失效了
* 返回登录页,让用户重新进行登录操作
*/
window.location.href = `${HOME_PAGE}/login`;
})
}

let timer: any = null;
/**
* 更新当前已过期的 token
* @param expiredRequest 回调函数,返回过期的请求
*/
export const refreshToken = (expiredRequest: () => any) => {
saveErrorRequest(expiredRequest);
// 保证再发起更新时,已经没有了过期请求要进行存储
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
updateTokenByRefreshToken();
}, 500);
}
// 响应拦截 区分登录前
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (
data.statusCode === 401 &&
config.url !== '/api/token/refreshToken'
) {
refreshToken(() => {
resolve(axiosInstance(config));
});
} else if (
data.statusCode === 401 &&
config.url === '/api/token/refreshToken'
) {
/**
* 后端 更新 refreshToken 失效后
* 返回的状态码, 401
*/
clearExpiredRequest();
window.location.href = `${HOME_PAGE}/login`;
} else {
reject(error.response);
}
})
}
)

补充


感谢很多朋友提出了很多更好的方法;我写这篇文章主要是为了分享一下,恰好这种问题推到了我(前端工程师)身上,我是怎样处理的;虽然有可能在一些朋友看来很低级,但它确是我实际工作中碰到的问题,每一个问题的出现解决后都对自身是一种成长,通过分享的方式来巩固自己,也希望能对他人有一些帮助!


总结


经过一波分析以及操作,我们最终实现了实际项目中的无感刷新token,最主要的是有效避免了:用户在平台操作过程中突然要退出登录的现象(尤其是当用户进行信息填写,突然要重新登录,之前填写的信息全部作废,是很容易让人发狂的)。

其实回顾一下,技术上并没有什么难点,只是思路上自己是否能够想通、自洽。人是一棵会思想的芦苇,我们要有自己的思想,面对问题,有自己的思考。

希望我们能在技术的路上走的越来越远,与君共勉!!!


作者:fly_dream
链接:https://juejin.cn/post/7254572706536734781
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 组件间通信,另一种与众不同的实现方式

iOS
本文已参与「新人创作礼」活动,一起开启掘金创作之路。 组件间通信,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通信问题。 那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:URL 路由target-actionprotoco...
继续阅读 »

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


组件间通信,但凡大一点的项目都会做模块化开发,必然会遇到兄弟组件解耦、通信问题。


那如何不互相依赖模块,又可以相互传输消息呢?网上的方案是有很多了,比如:

  1. URL 路由
  2. target-action
  3. protocol


iOS:组件化的三种通讯方案 这篇写的挺不错,没了解的同学可以看一下



也有很多第三方组件代表,MGJRouterCTMediatorBeeHiveZIKRouter 等(排名不分前后[手动狗头])。


但他们或多或少都有各自的优缺点,这里也不展开说,但基本上的有这么几种问题:

  1. 使用起来比较繁琐,需要理解成本,开发起来也需要写很多冗余代码。
  2. 基本都需要先注册,再实现。那就无法保证代码一定存在实现,也无法保证实现是否跟注册出现不一致(当然你可以增加一些校验手段,比如静态检测之类的)。这一点在比较大型的项目里都是很痛的,要不就不敢删除历史代码来积债,要不就是莽过去,测试或者线上出现问题[手动狗头]。
  3. 如果存在 Model 需要传递,要不下沉到公共模块,要不就是转 NSDictionary。还是公共层积债或者模型变更导致运行时出问题。

那有没有银弹呢?这就是本次要讲的实现方式,换个角度解决问题。


与众不同的方案


通过上述的问题,想一下我们想要的实现是什么样:

  1. 不需要增加开发成本,也不需要理解整体的实现原理。
  2. 由组件提供方提供,先有实现再有定义,保证 API 是完全可用的,如果实现发生变更,调用方会编译时报错(问题暴露前置)。且其他模块不依赖但又可以准确调用到这个方法。
  3. 各类模型在模块内是正常使用的,且对外暴露也是可以正常使用的,但又不用去下沉在公共模块。

是不是感觉要求很过分?就像一个渣男既不想跟你结婚,又想跟你生孩子[手动狗头] 。


但能不能实现呢,确实是可以的。但解决办法不在 iOS 本身,而在 codegen。铺垫到这里,我们来看看具体实现。


GDAPI 原理


在笔者所在的稿定,之前用的是 CTMediator 方案做组件间通信,当然也就有上面的那些问题,甚至线上也出现过因为 Protocol 找不到 Mediator 导致的线上 crash。


为了解决定义和实现不匹配的问题,我们希望定义一定要有实现,实现一定要跟定义一致。


那是否就可以换个思路,先有实现,再有定义,从实现生成定义。


这点参考了 JAVA 的注解机制,我们定义了一个宏 GDM_EXPORT_MODULE(),用于说明哪些方法是需要开发给其他模块使用的。

// XXLoginManager.h

/// 判断是否登陆
- (BOOL)isLogin GDM_EXPORT_MODULE();

这样在组件开发方就完成了 API 开放,剩下的工作就是如何生成一个调用层代码。


调用层代码其实也就是 CTMediator 的翻版,通过 iOS 的运行时反射机制去寻找实现类

// XXService.m

static id<GDXXXAPI> _mXXXService = nil;
+ (id<GDXXXAPI>)XXXService {
if (_mXXXService == nil) {
_mXXXService = [self implementorOfName:@"GDXXXManager"];
}
return _mXXXService;
}

我们把这些生成的方法调用,生成到一个 GDAPI 模块统一存储,当然这个模块除了上述模块的 Service 层是要有具体的 .m 来做落地,其他都是 .h 的头文件。


那调用侧只需要 pod 增加依赖 s.dependency 'GDAPI/XXXXService' 即可调用到具体实现了

@import GDAPI;

...

bool isLogin = [GDAPI.XXService isLogin];


这里肯定有同学会问,生成过程呢???


笔者是用 Ruby 代码实现了整个 codegen 过程,当时没选择 Python 主要是为了跟 cocoapods 使用相同的开发语言,易于做侵入设计,但其实用其他语言都没问题,通过 shell 脚本做中转即可。




这里源码有些定制化实现,放出来现在也是徒增大家烦恼,所以讲一下生成关键过程:

  1. 遍历组件所在目录,取出所有的 .h 文件,缓存在 Map<文件路径,文件内容>(一级缓存)
  2. 解析存在 GDM_EXPORT_MODULE() 的方法,将方法的名称、参数、注释通过正则手段分解成相应的属性,存储到 Map<模块名,API 模型列表> (二级缓存)
  3. 对于每一个 API 模型进行进一步解析,解析入参和出参,判断参数类型是否为自定义类型(模型、代理、枚举、包括复杂的 NSArray<CustomModel *> * 等),如果有存在,则遍历一级缓存,找到自定义类型的定义,生成对应的 Model -> Procotol 等,且存储在多个 Map 中 Map<类名/代理名/枚举名,具体解析后的模型>(三级缓存)
  4. 有了 AST 生成就变得很简单,模版代码 + 模版输出即可


有了上述各种模型,就差不多完成了 AST (抽象语法树) 的生成过程,至于为什么是用的正则而不是 iOS 的 AST 工具,主要原因是想做的很轻,尽量减少大家的构建时长,不要通过编译来实现。0



可以看到已经有大量模块生成了相应的 GDAPI




执行时长在 2S 左右,因为有一个预执行的过程,来做组件项目化,这个也算是特殊实现了。
实质上执行也就 1S 即可。


还有一点要说的是执行时机是在 pod install / update 之前,这个是通过 hooks cocoapods 的执行过程做到的。


一些难点


嵌套模型


上面虽然粗略的讲了下 Model / Procotol 会生成 Protocol,但其实这一部分确实是最困难的,也是因为历史积债问题,下沉在公共模块的庞大的模型在各个组件里传输。


那要把它完全的 API 化,就需要对它的属性进行递归解析,生成完全符合的 protocol


例如:

... 举例为伪代码,OC 代码确实很啰嗦

class A extends B {
C c;

NSArray<D> d;
}

/// 测试
- (void)test:(A *)a GDM_EXPORT_MODULE();

生成结果就如下图(伪代码):


@protocol GDAPI_A {
NSObject<GDAPI_C> c;

NSArray<NSObject<GDAPI_D>> d;
}

@protocol GDAPI_B {
}

@protocol GDAPI_C {
}

@protocol GDAPI_D {
}

以及调用服务

@protocol GDXXXAPI <NSObject>
/// 测试
- (void)test:(NSObject<GDAPI_A, GDAPI_B>)a;


这个在落地过程中坑确实非常多。


B 模块想创建 A 模块的模型


当然这个是很不合理的,但现实中确实很多这样的历史问题。


当然也不能用模型下沉开倒车,那解决上用了一个巧劲

/// 创建 XX
- (XXXModel *)createXXX GDM_EXPORT_MODULE();

提供一个创建模型的 API 给外部使用,这样对于 Model 的管理还是在模块内,外部模块使用上从 new XXX() 改为 [GDAPI.XXService createXX]; 即可。


零零碎碎


用正则判断抓取 AST,在一些二三方库中也是很常见的,但来处理 OC 确实挺痛苦的,再加上历史代码很多没什么规范,空格、注释各式各样,写个通用的适配算是比较耗时的。


还有就是一些个性化的兼容,也存在一些硬编码的情况,比如有些组件去关联到的 Model 在 framework 中,维护一个对应表,用 @class 来兼容解决。


后续


篇(jing)幅(li)有限,就不再展开说明,这个实现思路影响了笔者后续的很多开发过程,有兴趣可以看下笔者 Flutter 的文章,里面也是 codegen 的广泛运用。


如果有任何问题,都可以评论区一起讨论。


手敲不易,如果对你学习工作上有所启发,请留个赞, 感谢阅读 ~~


作者:园宵
链接:https://juejin.cn/post/7137240001112178702
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift是时候使用Codable了

用不起: 苹果发布Swift支持Codable已经有一定历史年限了,为什么还用不起来,无非就是苹果的Codable太强势了, 比如模型里的定义比数据返回的json多一个key,少一个key,key的值类型不匹配(如定义为String,返回的是Int),苹果老子...
继续阅读 »

用不起:


苹果发布Swift支持Codable已经有一定历史年限了,为什么还用不起来,无非就是苹果的Codable太强势了,


比如模型里的定义比数据返回的json多一个key,少一个key,key的值类型不匹配(如定义为String,返回的是Int),苹果老子直接掀桌子,整个模型为nil。这。。。


而且模型的属性想要默认值,无。。。




你牛,牛到大家不知道怎么用


于是网络一边夸他Codable好用,一边真正工程开发中却还用不起来。


搞起来:


最近研究网上有没有好用的Codable库的时候,找到了这个。2021 年了,Swift 的 JSON-Model 转换还能有什么新花样github.com/iwill/ExCod…


经过他的封装,把苹果包装的服服帖帖。经测试,解决如下问题:

  1. 多一个key
  2. 少一个key
  3. key的类型不匹配的时候,自动做类型转换
  4. 默认值处理好。 



他的模型定义可以简化为:

struct testModel: ExAutoCodable {
@ExCodable
var courseId: Int = -1
@ExCodable
var totalSectionCount: Int = -1 // 总的章节
@ExCodable
var courseImageUrl: String = ""
@ExCodable
var tudiedSectionCount: Int = 0 // 已经学习章节
}

既然他这么好,那就用起来啰喂,,,,等等,等等


定义模型这样,竟然不行:

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: Any]? = [:]

@ExCodable
var matchs: [Any] = []
}

苹果老子说Any不支持Codable???转模型的时候,这个全是空,nil。


一看工程,基本每个模型的定义都有这个呀,全有Any的定义,懵逼


研究起来:


通过研究stackoverflow.com/questions/4…, 发现可以给Any封装一个支持Codable的类型,比如AnyCodable这样。然后模型里面用到Any的,全部给换成AnyCodable。




模型改为如下,使用AnyCodable

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: AnyCodable]? = [:]

@ExCodable
var matchs: [AnyCodable] = []
}

AnyCodable.swift代码如下:

//
// AnyCodable.swift
//
// 因为Any不支持Codable,但是模型里面经常会用到[String: Any]。
// 所以添加类AnyCodable,代替Any,来支持Codable, 如:[String: AnyCodable]。
// https://stackoverflow.com/questions/48297263/how-to-use-any-in-codable-type

import Foundation

public struct AnyCodable: Decodable {
var value: Any

struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}

init(value: Any) {
self.value = value
}

public init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyCodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}

extension AnyCodable: Encodable {
public func encode(to encoder: Encoder) throws {
if let array = value as? [Any] {
var container = encoder.unkeyedContainer()
for value in array {
let decodable = AnyCodable(value: value)
try container.encode(decodable)
}
} else if let dictionary = value as? [String: Any] {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in dictionary {
let codingKey = CodingKeys(stringValue: key)!
let decodable = AnyCodable(value: value)
try container.encode(decodable, forKey: codingKey)
}
} else {
var container = encoder.singleValueContainer()
if let intVal = value as? Int {
try container.encode(intVal)
} else if let doubleVal = value as? Double {
try container.encode(doubleVal)
} else if let boolVal = value as? Bool {
try container.encode(boolVal)
} else if let stringVal = value as? String {
try container.encode(stringVal)
} else {
throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
}
}
}
}

这个结合Excodable,经过测试,完美。数据转换成功。


如果模型的定义忘记了,还是定义为Any呢。 再给Excodable库里面的源码,做安全检查,修改代码如下:

public extension Encodable {
func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
try (child.value as? EncodablePropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
// 注意:Any不支持Codable, 可以使用AnyCodable代替。
// 注意枚举类型,要支持Codable
assert((child.value as? EncodablePropertyWrapper) != nil, "模型:\(mirror)里面的属性:\(child.label) 需要支持 Encodable")
}
mirror = mirror.superclassMirror
}
}
}

public extension Decodable {
func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
try (child.value as? DecodablePropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
// 注意:Any不支持Codable, 可以使用AnyCodable代替。
// 注意枚举类型,要支持Codable
assert((child.value as? DecodablePropertyWrapper) != nil, "模型:\(mirror)里面的属性:\(child.label) 需要支持 Decodable")
}
mirror = mirror.superclassMirror
}
}
}

嗯,这下模型如果定义为Any,可以在运行的时候报错,提醒要改为AnyCodable。


能愉快的编码了。。。


不过总感觉还差点东西。


再研究起来:


找到这个 github.com/levantAJ/An…


可以实现

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
let array: [Any] = try container.decode([Any].self, forKey: key)

通过自定义[String: Any]和[Any]的解码,实现Any的Codble。


是否可以把这个合并到Excodable里面吧,从而什么都支持了,666。


在Excodable里面提issues,作者回复有空可以弄弄。


我急用呀,那就搞起来。


花了九牛二虎,终于搞出下面兼容代码:

// Make `Any` support Codable, like: [String: Any], [Any]
fileprivate protocol EncodableAnyPropertyWrapper {
func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws
}
extension ExCodable: EncodableAnyPropertyWrapper {
fileprivate func encode<Label: StringProtocol>(to encoder: Encoder, label: Label, nonnull: Bool, throws: Bool) throws {
if encode != nil { try encode!(encoder, wrappedValue) }
else {
let t = type(of: wrappedValue)
if let key = AnyCodingKey(stringValue: String(label)) {
if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
var container = try encoder.container(keyedBy: AnyCodingKey.self)
try container.encodeIfPresent(wrappedValue as? [String: Any], forKey: key)
} else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
var container = try encoder.container(keyedBy: AnyCodingKey.self)
try container.encodeIfPresent(wrappedValue as? [Any], forKey: key)
}
}
}
}
}
fileprivate protocol DecodableAnyPropertyWrapper {
func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws
}
extension ExCodable: DecodableAnyPropertyWrapper {
fileprivate func decode<Label: StringProtocol>(from decoder: Decoder, label: Label, nonnull: Bool, throws: Bool) throws {
if let decode = decode {
if let value = try decode(decoder) {
wrappedValue = value
}
} else {
let t = type(of: wrappedValue)
if let key = AnyCodingKey(stringValue: String(label)) {
if (t is [String: Any].Type || t is [String: Any?].Type || t is [String: Any]?.Type || t is [String: Any?]?.Type) {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
if let value = try container.decodeIfPresent([String: Any].self, forKey: key) as? Value {
wrappedValue = value
}
} else if (t is [Any].Type || t is [Any?].Type || t is [Any]?.Type || t is [Any?]?.Type) {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
if let value = try container.decodeIfPresent([Any].self, forKey: key) as? Value {
wrappedValue = value
}
}
}
}
}
}

再在他用的地方添加

// MARK: - Encodable & Decodable - internal

public extension Encodable {
func encode(to encoder: Encoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
if let wrapper = (child.value as? EncodablePropertyWrapper) {
try wrapper.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
} else { //添加
try (child.value as? EncodableAnyPropertyWrapper)?.encode(to: encoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
}
}
mirror = mirror.superclassMirror
}
}
}

public extension Decodable {
func decode(from decoder: Decoder, nonnull: Bool, throws: Bool) throws {
var mirror: Mirror! = Mirror(reflecting: self)
while mirror != nil {
for child in mirror.children where child.label != nil {
if let wrapper = (child.value as? DecodablePropertyWrapper) {
try wrapper.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
} else { //添加
try (child.value as? DecodableAnyPropertyWrapper)?.decode(from: decoder, label: child.label!.dropFirst(), nonnull: false, throws: false)
}
}
mirror = mirror.superclassMirror
}
}
}


完美:


综上,终于可以让Excodable库支持[String: Any]和[Any]的Codable了,撒花撒花。


从而模型定义这样,也能自动编解码:

struct testModel: ExAutoCodable {
@ExCodable
var jumpParam: [String: Any]? = [:]

@ExCodable
var matchs: [Any] = []
}

针对这个库的更新修改,改到这github.com/yxh265/ExCo…


也把对应的更新提交给Excodable的作者了,期待合并。
(作者iwill说,用ExCodable提供的 ExCodableDecodingTypeConverter 协议来实现是否可行。
我看了,因为Any不支持Codable,所以要想用ExCodableDecodingTypeConverter协议,也得要大改。也期待作者出马添加这个功能。)


最后的使用方法:


引入如下:

pod 'ExCodable', :git => 'https://github.com/yxh265/ExCodable.git', :commit => '4780fb8'

模型定义:

struct TestStruct: ExAutoCodable {
@ExCodable // 字段和属性同名可以省掉字段名和括号,但 `@ExCodable` 还是没办法省掉
var int: Int = 0
@ExCodable("string", "str", "s", "nested.string") // 支持多个 key 以及嵌套 key 可以这样写
var string: String? = nil
@ExCodable
var anyDict: [String: Any]? = nil
@ExCodable
var anyArray: [Any] = []
}

编解码:

let test = TestStruct(int: 304, string: "Not Modified", anyDict: ["1": 2, "3": "4"], anyArray: [["1": 2, "3": "4"]])
let data = try? test.encoded() as Data?
let copy1 = try? data?.decoded() as TestStruct?
let copy2 = data.map { try? TestStruct.decoded(from: $0) }
XCTAssertEqual(copy1, test)
XCTAssertEqual(copy2, test)

引用:


2021 年了,Swift 的 JSON-Model 转换还能有什么新花样


github.com/iwill/ExCod…


stackoverflow.com/questions/4…


stackoverflow.com/questions/4…


Property wrappers in Swift和Codable


作者:清点游玩
链接:https://juejin.cn/post/7168748765946806303
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS应用内弹窗通知怎么实现?其实很简单,这样,这样,再这样.....你学会了么?

iOS
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情。 项目背景 消息通知可以及时地将状态、内容的更新触达到用户,用户则可以根据收到的消息做后续判断。这是最常见的信息交换方式的产品设计。 而顶部向下弹出的消息通知本质上...
继续阅读 »

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情


项目背景


消息通知可以及时地将状态、内容的更新触达到用户,用户则可以根据收到的消息做后续判断。这是最常见的信息交换方式的产品设计。


而顶部向下弹出的消息通知本质上是根据条件触发的“中提醒”通知类型,示例:每次在网购时,支付成功后在App会展示消息通知。


因此本章中,我们就来试试使用SwiftUI来实现应用内弹窗通知交互。


项目搭建


首先,创建一个新的SwiftUI项目,命名为NotificationToast


消息弹窗样式

我们构建一个新的视图NotificationToastView,然后声明好弹窗视图的内容变量,示例:

struct NotificationToastView: View {
    var notificationImage: String
    var notificationTitle: String
    var notificationContent: String
    var notificationTime: String

    var body: some View {
        //弹窗样式
    }
}

上述代码中,我们声明了4个String类型的变量:notificationImage图标信息、notificationTitle标题信息、notificationContent内容信息、notificationTime推送时间。


然后我们构建样式内容,示例:

HStack {
    Image(notificationImage)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 60)
        .clipShape(Circle())
        .overlay(Circle().stroke(Color(.systemGray5), lineWidth: 1))
    VStack(spacing: 10) {
        HStack {
            Text(notificationTitle)
                .font(.system(size: 17))
                .foregroundColor(.black)
            Spacer()
            Text(notificationTime)
                .font(.system(size: 14))
                .foregroundColor(.gray)
        }
        Text(notificationContent)
            .font(.system(size: 14))
            .foregroundColor(.black)
            .lineLimit(4)
            .multilineTextAlignment(.leading)
    }
}
.padding()
.frame(minWidth: 10, maxWidth: .infinity, minHeight: 10, maxHeight: 80)
.background(.white)
.cornerRadius(8)
.shadow(color: Color(.systemGray4), radius: 5, x: 1, y: 1)
.padding()

上述代码中,我们构建了样式排布,Image使用notificationImage图片信息变量,它和其他元素是HStack横向排布关系。


右边则是HStack横向排布的notificationTitle标题变量的文字和notificationTime推送时间的文字,使用Spacer撑开。


而底下是notificationContent内容信息,它和标题信息及推送时间信息是VStack纵向排布。


我们在ContentView中展示看看效果,示例:

NotificationToastView(notificationImage: "me", notificationTitle: "文如秋雨", notificationContent: "一只默默努力变优秀的产品汪,独立负责过多个国内细分领域Top5的企业级产品项目,擅长B端、C端产品规划、产品设计、产品研发,个人独立拥有多个软著及专利,欢迎产品、开发的同僚一起交流。", notificationTime: "2分钟前")


消息弹窗交互


交互方面,我么可以做个简单的交互,创建一个按钮,点击按钮时展示消息弹窗,消息弹窗显示时等待2秒后自动消失。


实现逻辑也很简单,我们可以让弹窗加载的时候在视图之外,然后点击按钮的时候,让消息弹窗从下往下弹出,然后等待2秒后再回到视图之外


首先我们声明一个偏移量,定义消息弹窗的初始位置,示例:

@State var offset: CGFloat = -UIScreen.main.bounds.height / 2 - 80

然后给弹窗视图加上偏移量和动画的修饰符,示例:

ZStack {
    NotificationToastView(notificationImage: "me", notificationTitle: "文如秋雨", notificationContent: "一只默默努力变优秀的产品汪,独立负责过多个国内细分领域Top5的企业级产品项目,擅长B端、C端产品规划、产品设计、产品研发,个人独立拥有多个软著及专利,欢迎产品、开发的同僚一起交流", notificationTime: "2分钟前")
        .offset(x: 0, y: offset)
        .animation(.interpolatingSpring(stiffness: 120, damping: 10))
    Button(action: {
        if offset <= 0 {
            offset += 180
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.offset -= 180
            }
        }
    }) {
        Text("弹出通知")
    }
}

上述代码中,我们让NotificationToastView弹窗视图偏移位置Y轴为我们声明好的变量offset位置,然后使用ZStack叠加展示一个按钮,当我们offset在视图外时,点击按钮修改偏移量的位置为180,然后调用成功后等待2秒再扣减偏移量回到最初的位置


项目预览


我们看看最终效果。


恭喜你,完成了本章的全部内容!

快来动手试试吧。

如果本专栏对你有帮助,不妨点赞、评论、关注~

作者:文如秋雨
链接:https://juejin.cn/post/7136104673248804878
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

最新前端技术趋势

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!! 话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走, 那么...
继续阅读 »

前端的车轮滚滚向前,轮子造的越来越圆,速度造的越来越快,每个人都在适应这个轮子的节奏,稍微不注意就会被甩出车轮之外。狼狈不堪之外还会发自心底的大喊一声:别卷了!!


话虽这么说,但现实就是这样,无论是客观还是主观因素都不得不让你继续的往前走。既然是往前走,
那么能知道一些前面有啥东西岂不是更好,也许能少走弯路。



自己对前端23年大概的技术做了一些展望,想到什么写什么。毕竟谁都不知道会不会突然间又出了个frontEndGPT打翻了所有人的饭碗。



1 AI


最先说的肯定是AI,22年末,23年初的chatgpt让AI话题火的一塌糊涂,同时也被认为是一次重大的技术革新,技术革新带来的就是重塑,一切都要被重塑,你的职业,你的工作。
视觉层面的stable diffusionmidjourney已经对设计师产生了重大影响,而涉及到视觉ui层面的话,前端肯定是绕不开的部分。虽然目前没有直接的对前端产生影响,但下面的就一个就不只是对前端了,而是对整个程序员都产生了影响。

  • copilot


Your AI pair programmer. Trained on billions of lines of code, GitHub Copilot turns natural language prompts into coding suggestions across dozens of languages.


看看醒目的文字就知道,程序员或多或少都会被影响了。


还有什么CodeWhisperer, Cursor等也都是AI辅助编程。


更有FrontyLocofy等将图片AI分析为HTML文档,无代码快速建站,figma快速解析成代码等,虽然是提效不少,但谁能说这些不是对前端的一种重塑呢。


2 主流框架


随着React,Vue等框架进一步的普及,现在前端想要脱离它们的场景越来越少了。那么它们的下一步规划,也会对我们产生一些不小的影响。


react


react 18以后,react似乎是对create-react-app这种项目启动方式也不怎么主推了,毕竟速度摆在那里,没有任何优势。而使用直接竞争对手的产品似乎又不太合适,而直接说又不用又显得跟开源精神不吻合(当然竞争对手的一些基本特性也确实和现有的构建思想不太吻合)。


他们似乎采取一种围魏救赵的方式,着重宣传next的方式。next不仅仅是一个ssr的框架,同时它也支持csr,ssg等不同的方式(next13开始,对于客户端组件和服务端组件可以有了比较好的区分)。同时next与react有着千丝万缕的联系,而next正在进行一个新的构建工具的替换。
next采用了turbopack,也是Webpack作者TobiasKoppers的作品,官方说它更新速度比Vite也要快10倍、比Webpack快700倍


而react也应该大概率会引入turbopack(当然它如果继续搞前端脚手架的话)。当然也可能会直接使用next环境。


至于快多少,以及评价基准等我们可以看下turbopack真比Vite快10倍吗?


next


next最新版本也加入了很多的特性,比如server component理念,约定式路由的更改,流式渲染,客户端组件与服务端组件分离更简单,更好的构建速度等等功能。可以让开发体验,用户体验更好,性能也会有响应的提升。


vite


这个不用说了,优秀的构建速度以及越来越丰富的社区,让其在22年有了很大提升。随着浏览器的逐步升级,23年vite肯定也会是重大的一年。


webpack


虽然5有了好多的功能提升,不过速度似乎一直是一个绕不过去的坎。就连作者也已经开始搞turbopack了,虽然加入swc能让编译有很大提升,但是目前从我身边的人的了解看,越来越多的人开始转向vite等其它方式了


turbopack


是webpack作者去的新公司开发的一款基于rust的打包工具。官方明确说明就是为了替换webpack。 同时强调webpack是这十年最火的工具,那turbopack就定位成未来几年的工具。由于作者和webpack, Vercel, next, React这些之间千丝万缕的联系,很难不说未来React也许会和这个打包工具绑定上。下面是官方提供的速度参考




而除了turbopack外,同一团队还在做Turborepo


这是一款项目管理的工具,最主要的面向场景是Monorepo这种复杂的多项目管理


Monorepo有很多优势,但是在多个项目中会有很多复杂的构建过程和相对闭塞的构建步骤,每一次上线都是耗时严重。所以Turborepo是为了解决这个问题出现的,让一些构建重复的构建步骤提炼出来,基于整个Monorepo项目的维度来管理多个子项目。


同时对于单个CI的构建步骤,解决每台机器,每个人都要单独构建的问题,还提炼了类似store的方案,脱离了构建环境,只跟项目绑定,当然这个方案是否使用要看你自己,毕竟原理是把构建产物放到第三方储存,而第三方又不是一个类似npm的开源机构。


3 服务端


node从7,8年前的爆火,到现在的不温不火,前端语言介入服务端这个命题似乎现在是越来越清晰了。那就是定位,可以做网关,可以做转发,可以做一些数据代理合并,定位清晰node依然有自己的使用场景。
node也马上到了20版本,迎来了一些特性

  • esm的更好支持,

  • 测试功能更加丰富

  • V8 引擎更新至 11.3,与Chromium113版本大部分相同

  • 支持以虚拟机的方式,动态运行js代码

  • WebAssembly的支持(实验性的)


于此同时,node曾经的作者Ryan Dahl几年前搞的Deno似乎没有太多的消息,似乎在向商业化方向前进。打造出Deno Deploy及其即时边缘渲染SSR框架Deno Fresh


4 其它


WebAssembly, 元框架,ts,微前端?


引用:


http://www.infoq.cn/article/9qu…
turbo.build/pack/docs/w…
http://www.robinwieruch.de/web-develop…


作者:奇舞精选
链接:https://juejin.cn/post/7243725406132748349
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

北美 2023 被裁员的感悟

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。 很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。 公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑...
继续阅读 »

不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。


很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。


公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑寻找下一个替代方案了。


因为当这些信号的任何一个或者多个同时出现的时候就意味着裁员在进行中了,通常会在 3 到 6 个月左右发生。




在公司的职位


在被裁公司的职位是 Tech Lead。


虽然这个职位并不意味着你对其他同事而言能够获得更多的有效信息,但是通常可能会让自己与上级有更好的沟通管道。


但是,非常不幸的是这家公司的沟通渠道非常有问题。


因为负责相关开发部分的副总是从 PHP 转 Java 的,对 Java 的很多情况都不非常明确,所以他非常依赖一个架构师。


但,公司大部分人都认为这个架构师的要求是错误的,并且是非常愚蠢的。


比如说要求代码必须要放在一行上面,导致代码上面有不少行甚至超过了 1000 个字符。


所有开发都对这个要求非常不理解,并且多次提出这种要求是无理并且愚蠢的,我们组是对这个要求反应最激烈,并且抵触最大的(也有可能是因为我的原因,我不希望在明显错误的地方让步;我可以让步,但是需要给一个能说服的理由)。


然而,这个所谓的架构师就利用 PR 合并的权力,不停的让我们的组员进行修改。


裁员之前


正是因为在公司的职位和上面说到的和架构师直接的冲突。


在 6 个月之前,我就已经和组里的同事说要准备进行下一步了,你们该面试的就面试了,不要拖延。


在这个中间过程中,我的领导还找我谈过一次。领导的意思就是他非常同意我们的有关代码 PR 的要求,也觉得这些要求都是狗屁。


但,负责开发的副总,认为我们组现在是所有组里面最差的。


可能当时没有认真理解这句话的意思,我们组从所有组里面最好的,变成最差的只用了 2 周(一个 Sprint)的时间。


在这次谈话后,我更加坚信让我的组员找下一家的信息了,对他们中途要面试其他公司我都放开一面。


非常不幸的,我自己那该死的拖延症,我是把我自己的简历准备好了,但是还没有来得及投就等来了真正裁员的这一天。


深刻的教训和学到的经验:


如果公司的运营或者管理让感觉到不舒服,并且已经有开始寻找下家的想法的时候,一定要尽快准备,马上实施,不要拖延


这就是我在上面标黑马上的原因。


裁员过程


裁员过程非常简单和迅速,并且在毫不知情的情况下进行。


在周四的时候,公司的高层提示所有的会议今天全部取消,并且把应该 11 点开的全公司会议提前到了 9 点。


因为很多人都没有准备,所以很多人也没有参加。


后来才知道,9 点就是宣布裁员的开始,事后知道裁员比率为 40%。


然后就是各个部门找自己的被裁的员工开会,这个会议通常首先是一个 Zoom 的 Group 会议,说了一堆屁话,就是什么这是不得已的决定呀,什么乱七八糟的东西。


当然,在这个时候你还需要或者期待公司给你什么合理的理由呢?


然后就是 P&C 人员说话,基本上就是每个人 15 分钟的时间单独 Zoom。


这个 15 分钟,主要就是读下文件了,至于 2 个会议上是不是开摄像头,随意。


你愿意开也行,不愿意开也行,反正上面的所有人都心不在焉。我是懒得开,因为和你谈话的人,你都根本不认识。


第二个会议就是 P&C,这个会议主要就是和你说说被裁员后可以有的一些福利和什么的,对我个人来说我更关注的是补偿。


至于 401K 和里面的资金都是可以自行转的,也没啥需要他们说的,了解到补偿就是 6 周工资,不算多也凑合能接受。


负责裁员的人说,还有什么需要问的,我简单的回答了下 All Set 然后 have a nice day 就好了。毕竟他们只是具体做事的人,问他们也问不出个所以然,这有啥的。


裁员之后


裁员之后,感觉所有认识的被裁的同事都是懵的。


开完 15 分钟的 P&C 会议后,基本上电脑和邮箱马上就不能用了。公司貌似说电脑可以自己留着,但是上面的数据会被远程清理掉。


留在公司里面的东西会有人收拾后寄到家里。


我在公司里的办公桌就属于离职型办公桌,简单的来说,上面只有一台不属于我的显示器,另外就是从其他地方拿过来的一盒消毒湿巾,公司里面压根没有我需要的东西。


很多人认为公司禁用账户有点太不讲人情,其实从技术层面来说根本没有什么,因为所有的管理都是 LDAP,直接在 LDAP 上禁用你账户就好了,没啥稀奇的。


中午的时候,被裁的同事都互相留下了手机号码,感觉大家因为我在裁员列表里面感觉有点扯。另外更扯的同事在这个公司工作了 7 年了,也在列表里面(所有 PHP 的基础架构都是他写的和建立的)。


虽然最开始和这个同事有过一些摩擦,但是这个印度的同事真的挺好的,我们都觉得他挺不错,也愿意和他一起共事。


很多人,包括我。都对这个同事感觉不值,也觉得这很扯。


奈何公司的选择就是一些阿谀奉承,天天扯淡的人,比如说那个奇葩的架构师。


没多久,被裁的同事建了一个群,然后把我给拉进去了,主要还是我们组里面的同事,大家希望能够分享一些面试经验和机会,偶尔吐槽下。


在晚上的时候,突然收到另外一个同事的 LinkedIn 好友邀请,他不在这次裁员内。


但是他也被降职了,他本来是 Sr 开发人员和小组长,后来被提拔成架构师了,现在连小组长都不是了。


他和我说,如果需要帮助的话,他会尽量帮忙,并且还给他的一些曾经的招聘专员账号推送给了我。


我也非常感谢他们,虽然经历过,但是也收获了一些朋友,虽然说在美国职场比较难收获朋友,但是也并不是完全这样的。


没有了利益的纠葛,更容易说点实话。


http://www.ossez.com/t/topic/144…


作者:honeymoose
链接:https://juejin.cn/post/7240052076624035901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


作者:张鑫旭
链接:https://juejin.cn/post/7221487809789182008
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

放心,前端死不了

前言 今年春天莫名冒出一个“前端已死”的话题,搞得同行们人人自危。 这个话题和当下的环境是分不开的,如果环境很好,工作好找,那这个话题也就不攻自破了,不会流传开来。 网络上的任何事情都可以在《乌合之众》书中找到答案。大众言论没有理性,全是极端,要么封神,要么踩...
继续阅读 »

前言


今年春天莫名冒出一个“前端已死”的话题,搞得同行们人人自危。


这个话题和当下的环境是分不开的,如果环境很好,工作好找,那这个话题也就不攻自破了,不会流传开来。


网络上的任何事情都可以在《乌合之众》书中找到答案。大众言论没有理性,全是极端,要么封神,要么踩死。


今天能找到不错的工作,就感觉自己马上就要晋升 CEO 迎娶白富美了。但一旦明天情况不太好,就感觉自己马上要死了。


“前端已死”这个话题就属于后者。



原文地址 juejin.cn/post/722432… ,非作者允许不得转载



互联网行业会持续发展


前端技术是依托于互联网行业的,只要行业还在,它就会有用武之地,就会有价值。塞班已死,因为诺基亚不在了。


虽然现在电脑手机早已普及完成,增长率没那么大了,但互联网在各个领域的应用却不断更新升级。它已经嵌入到我们生活的各个领域了,就像水和电一样。


有互联网行业,就需要各种 toC 的页面、公众号、小程序,各种 toB 的管理系统和富客户端应用。就需要前端开发技术,需要前端人才。


你可能盲从“前端已死”,但你绝不会同意“互联网将死”,这就很矛盾 —— 当然我们生活、网络处处都矛盾。


递弱代偿的基本规律


递弱代偿,是我国哲学家王东岳提出的事物发展理论。对于这世界的任何事物,只要是发展,就一定会“递弱代偿”。


举个例子,低等的单细胞生物,一个细胞即可完成吃喝拉撒各种功能。这就很像小创业公司,或者一个行业的发展初期,都是全能型人才,拿起键盘写代码,抄起拖把扫厕所。个体很厉害,但整体很低级。


再往下发展到多细胞生物,开始有了细胞功能的分化,有的专门呼吸、有的专门消化。这就像初具规模的小公司,行业开始有了发展和进化,人才开始分角色了。有的人专门写代码,有的人专门扫卫生


再慢慢发展 N 步,到了高级哺乳动物,体内细胞分为上百种,严格划分了各种职能,最终拼接为一个高级的整体。公司和行业也一样,当前互联网从业人员的角色有几十种,大家各司其职,才做成了几万人的大公司。


不光互联网行业,像其他常见的行业,工业、汽车、医疗,这些成熟的领域,哪些不是严格细分角色的?


就连现在的网络诈骗,也都是有规模、有组织、有上下线的。


所以,互联网行业要发展,就一定会遵循这个规律,继续细分角色,前端永远都是一个重要角色。


如果今年前端被全栈替代了,明年运维也被全栈替代了,后年产品也被全栈替代了 …… 那就违背这个哲学规律了,那就说明互联网会倒退 —— 这显然不对


PS:发展不一定是线性的,它可能是起伏的,有强增长期,一定就有缓和期 —— 但整体一定是继续增长的。


前端没有可替代技术


Android 和 IOS 被小程序替代了很多,但前端目前没有发现什么可替代技术,反而前端早已慢慢的占据其他技术领域,如客户端、服务端。


新的技术如 Flutter WebAssembly 都是对当下前端技术的补充,真实 TS 也不是为了替代 ES ,它也是一个补充和备选方案。


浏览器 + HTML CSS JS + HTTP ,目前没有任何技术可以替代它,反而它们正在加速进化和完善。Vue React 小程序,目前也已经牢牢占据了高地,前端技术的范式早已形成。


ChatGPT 代替不了程序员


无论网上怎么宣传,当下所有的民生领域的人工智能,包括智能驾驶,严格来说都是:人工智能辅助


辅助,就是为人服务的。为人的自主思考能力和创造力服务,不是为机械重复的工作服务。


ChatGPT 本质上就是一个搜索引擎的二次封装,它更能理解你的输入意图,它更精确的帮你拼接返回结果。但它就是一个辅助工具,用好了可以帮你提升效率,但前提是得有人去用它。


你可以想一下日常的工作内容,开会,沟通,扯皮这些,AI 能参与进来吗?

它能理解你的业务需求吗?

它能帮你完成复杂模块的开发和联调吗?

它能帮你和其他同事沟通吗?


它能做的只是帮你写一部分固定的代码和文档,仅此而已。

现在有搜索引擎,我们说“面向 google 编程”,以后可能就变为“面向 ChatGPT 编程”,提升效率。


大环境总会有起伏,这是常态


大概从 15-16 年开始,当时随便培训一下,出来就一万多的工资(北京为例),直到前两年,行业的增长率特别快。这一代人也吃到了行业的红利,多少挣了点钱。


但无论哪个行业,都不能指望他一直这么增长,有增长就有缓和。再牛的人也不能影响到大环境,庄稼不收年年种,三穷三富活到老。


缓和期过去,还会迎来下一轮增长期。


总结

  • 互联网行业会持续发展

  • 发展就要递弱代偿,细分行业角色

  • 前端没有可替代的技术,是一个重要角色


所以,前端死不了,只是最近因环境原因暂时蛰伏,发展缓慢而已。


当下建议


有工作的就先稳住,千万别裸辞。哪怕工作不顺利,毕竟拿人钱受人管。


已经裸辞或者被毕业的,也不要过于焦虑,即便工作机会少这也不是你的问题,人斗不过大环境的。该学习就学习,该面试还是去面试,把自己该做的做了就是,其他的就别管了,淡定。


之前上班加班忙成狗,现在难得有时间,正好自由安排做一点自己之前想做的事。毕竟人生苦短,不是光为了工作,每个人都有自己独有的爱好。


也许过了这一阵,你再想这么自由就没机会了。有些事儿,现在不做,这辈子也许就不再有机会和动力了。


如果你想反驳我的观点


以你想的为准,你说的对。


作者:前端双越老师
链接:https://juejin.cn/post/7224325360598302781
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

2023 了,不会还要做官网吧!

web
随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需...
继续阅读 »

随着 AIGC 的兴起,移动互联网仿佛一瞬间也不香了,吃到红利的头部应用还在稳坐泰山,后起者在各种 xx 已死的声浪中,不知所措,信心尽失。流量的走势左右了太多人的思绪,太多的决策,所以当你在 2023 年,PC 互联网已经凉到不能再凉的时候,接到一个官网的需求,你会怎么去做呢,耐心看完,希望对你有所帮助。


官网开发作为前端的传统艺能已经诞生很久了,与现在主流的 react/vuecsr 框架开发不同,官网对 seo 有一定要求,所以咱们的网页不再是开局一个空 html,渲染全靠 js 了。传统的官网开发,一般采用 WordPress 建站,很好的技术栈 php + html/css/js 前后台一把梭,方便快捷,架构成熟。不过当遇到公司的前后端技术栈是 java + react 时,这个方案显然对开发人员并不友好,作为一名思维跳脱,勇于承担的前端,灵光乍现,我们是不是可以自己独立的完成整个官网呢。


在掘金上搜了搜,居然还有现成的课程,看看时间还挺新的,不如就从它入手吧。




作为网站尊贵的会员,我用兑换券换到了这门课程,开始了如饥似渴的学习。文章内容还是比较浅显易懂的,跟着操作下一个网站的雏形就出现了。课程中的技术栈主要采用 next.js + strapi 完成对官网的开发,相信各位看官并不陌生,业界知名的 react ssr 框架和 headless cms 系统,基础的介绍就不多说了,来分享点实操中得到经验教训吧。


next.js 相关


收获到的知识


1. 如何调试 next.js 服务端渲染的逻辑


之前,我只会使用 console.log 在启动的命令行里去判断代码的问题,现在我知道了使用 cross-env NODE_OPTIONS='--inspect' next dev 命令,他会开启一个 nodejsinspect 调试窗口,因为 next.js 的系统对象都很复杂,层级深,如果 console.log 的话很觉得很烦人,通过如上的方式就能便捷。






2. 生成有参数的静态页面的方法


其实像官网流量挺小的,没有必要走静态导出的 ssg ,使用 ssr 也完全没有问题,但是鉴于对 k8s 扩容,服务器费用之类的知识不太了解,官网的迭代频率也很低,所以本次采用了 ssg 的方案,要求所有子页面都要生成, 这就不得不提到内置的 api名为 getStaticPaths了。他会在打包阶段,先从一个列表接口拿到所有的id,再执行 getStaticProps 中的逻辑,拿到所有详情数据。比如我们的故事详情页,共有两条数据,id为1,2,执行完后,就会在对应目录下生成两个 html






得到的经验教训


1. 移动端适配


一个简易的判断移动端的代码很容易写,先不纠结完备性,大致如下:

/mobile|android|iphone|ipad|phone/i.test(window.navigator.userAgent.toLowerCase())

然而在 next.jsssg 方案中你却很难拿到,因为 render 里不能有对 window 的直接使用。我想了很多取巧的方法,但是都不行,如果是 ssr 还可以用请求头的 user-agent 来判断,作为属性透传到组件中,如课程一样。但 ssg 你只能使用响应式,同时渲染2套布局,才是最简单的做法。仔细想想,因为 html 字符串的构建是在 build 的时候就已经决定了,所以不能动态的拼接成不同的 dom 也就很好理解了。



PS: 感谢 @wonderL17 指出,也可以使用nginx配置的方式,将对应路径转发到为移动端生成的页面。
如果页面复用逻辑多,且能很好支持响应式,可以使用响应式。如果完全是另一套风格,则可以使用配置ng方式去处理,更为优秀,代码会更好维护且生成的页面更小。



2. api目录的处理


next.js 中的 api 目录可以承担接口的功能,课程中在 api 层进行了一次中转,我对此呈保留意见,想法如下:

  • 如果 strapi 不能直接支持一定的并发量,那么使用 api 来代理请求一样不能达到。

  • 中间层越多,可能出现的协作编码问题就越多,比如 strapi 返回的数据不符合预期,有些人会去修改 strapi,有些人会去改 next.js 的 api,这样会造成项目维护的不统一。


其实简单的对 strapi 加个跨域的配置,就能很好的直接在每个组件内使用,这对于一般的官网是完全可以满足的,而且代码清晰整洁。


strapi 相关


strapi 暂时没有给我带来新的知识,因为没有对他的原理进行研究,最核心的功能,编辑类型,即可生成对应的增删改查界面是最值得学习的部分,可是也是他最成熟的部分,开发嘛,能用就行,所以只有使用上的踩坑记录。但不得不说,strapi 的模块设计的挺不错的(除开dashboard ui定制外),有兴趣的可以了解下代码原理和设计思路。


得到的经验教训


1. 数据库(!!!!!)


请一定不要用 sqlite,不然你会哭的。因为项目肯定是需要多人协作的,强大如你 merge 代码对你来说轻而易举,但是你会 merge db 文件吗?相信我,你不会。


所以不要等项目已成,你才想起来去换个数据库,那么你即将面临,如果将 sqlite 数据导出成 postgresql 或者 mysql 数据的问题,听上去并不难吧,我花了 1~2 天时间才把这个无意义的工作做完。虽然能查到很多方案,但开源的不好用,付费的不想试,还是老老实实导出 sql,再导入是最快的。(不详述了,都是辛酸,你懂的,装了一堆东西,要么环境问题,要么功能问题,迟迟无法完成迁移的痛)


所以一开始就选好数据库,很关键,这样既方便了协作开发,又方便了后续使用。因为 4.x? 具体是几不清楚,relations 排序不生效(postgresql | mysql),sqlite 没有这个问题,所以替换数据库后,升级下 strapi,我升级的是4.10.6,顺便可以把依赖里的 sharp 删掉,有时候要装很久。


2. OSS(!!!!)


oss 一样至关重要,如果你使用了 strapi 的媒体库功能,请一定先配置好自定义的 oss,使用这个库就好了 strapi-provider-upload-oss,配置起来很方便,如果你头铁,一开始就是不配,你将面临,将上传好的图片删除,再重新上传一遍,没错他没有批量替换的功能,只能手动操作,我也没有花很久,1个小时,重新换了70~80张图片(关键素材上次用完还删了)。


配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
upload: {
config: {
provider: "strapi-provider-upload-oss", // full package name is required
providerOptions: {
accessKeyId: "xxxx", // 用你的 oss 配置把 xxx 换掉,但千万别上传到开源库里(如github, gitee)哦
accessKeySecret: "xxx",
region: "xxx",
bucket: "xxx",
uploadPath: "/strapi/static",
baseUrl: "xxx",
timeout: 3000,
secure: true,
},
},
},
});

3. 数据的格式化(!!!)


课程中自己定义 removeTime, removeAttrsAndId 等方法,对数据进行处理,可能写的比较早,还没有成熟的转换库,这里介绍下 strapi-plugin-transformer,可以快速的去掉没必要的层级结构和一些属性,相当好用。配置如下:

// config/plugins.js

module.exports = ({ env }) => ({
transformer: {
enabled: true,
config: {
responseTransforms: {
removeAttributesKey: true,
removeDataKey: true,
},
},
},
});

这个操作可以避免对数据进行过多的处理,也就意味着 src/api/xx 里的代码,你基本不需要手动修改了,大大提升了后台配置的开发效率。C端对数据的处理也更简单了


4. 一些小的注意点

  • config 目录下添加插件,需要创建 plugins.js 文件,少写了 s 会导致插件不生效。(PS:嗯,就是粗心的我建了 plugin.js 还怪人家插件不好使~)

  • .cache 还挺有用的,一些修改不生效,可以试试 build 后再重启试试。


一些拓展能力


strapi 的初始状态很难满足直接交付给运营配置,最大的坑点在于类型定义是英文的,还没有层级结构,这里参考了这篇文章提到的方案:juejin.cn/post/721922…,来对 dashboard 进行定制。功能主要分为3个步骤,patch-package 使用参考原文章即可。

  • 类型汉化


只需要修改 admin/app.js 即可


  • 类型层级&排序:.cache/admin/src/content-manger/pages/App/LeftMenu 文件

// 目录排序,1,1.1,2,2.1
function compareDirectories(formatter, dir1, dir2) {
// 提取目录中的数字和点号
const regex = /(\d+|\.)+/g;
const arr1 = dir1.match(regex);
const arr2 = dir2.match(regex);

if (!arr1 || !arr2) return formatter.compare(dir1, dir2);

// 比较每个部分的数字
for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
const num1 = parseFloat(arr1[i]) || 0; // 如果无法解析为数字,默认为0
const num2 = parseFloat(arr2[i]) || 0;

if (num1 < num2) {
return -1;
} else if (num1 > num2) {
return 1;
}
}

// 如果所有部分都相同,则按照长度进行比较
if (arr1.length < arr2.length) {
return -1;
} else if (arr1.length > arr2.length) {
return 1;
}

return 0; // 目录完全相同
}



// Sort correctly using the language
// 这条注释下,用目录排序的方法替代原有 sort

// SubNavLink 内,使用 dangerouslySetInnerHTML 体现目录层级
<span dangerouslySetInnerHTML={{__html: link.title.replace(/([0-9]+.)/g, (a, b, index) => {
if (index <= 1) return '';

return '   ';
})}}></span>


这样你就能得到一个这样的配置目录




  • 项目部署

因为我这边项目是 ssg,修改完内容是需要触发流水线重新部署的,如果像想避免这个工作,可以增加一些按钮,触发 webhook。这样只要排版是够用的,就不需要开发介入了。


如下示例,修改的是 .cache/admin/src/pages/HomePage/index.js




以上改动想要生效,记得用 patch-package,这也是我说他的定制模块不友好的原因~


总结


next.jsstapi 的学习,踩坑,使用经验就是这么多了,起初发起这个项目,是因为我们官网的框架有些老了,用的 fis3,有时候法务、市场同学来找替换素材,都是没什么工作难度的事儿,浪费时间,也整的挺烦的,所以借着机会升级了一下,以后就事半功倍了。


至于在当今这个时代,官网的 seo 是否还有意义,是否还能够为公司带来不俗的自然增量,这个我最感兴趣的事儿,迟迟没能发起。


因为在公司,这是涉及很多部门(品牌,公关,法务,市场,产品,设计)的事儿,普通开发并不能调动资源,我也很期待,如果有幸能发起一个这样的项目,并不断通过技术上的优化为业务带来新的增量,那里可能会用到更多的贴合业务的 ssr 技术,到时候有机会再和大家分享下~


但我也想对技术感兴趣的朋友说,底层的技术重构也是很有魅力的一件事儿,尽管没有业务方的支持,如果你持之以恒的来做,复刻原产品,也能让这个产品在技术层面上焕然一新。


谨以此文,与君共勉!


作者:windyrain
链接:https://juejin.cn/post/7242519176853733433
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你在公司混的差,可能和组织架构有关!

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。 如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤...
继续阅读 »

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。


如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤其是这两年互联网炸了锅,猪飞的日子不再,这种情况就更加多了起来。


反过来说也一样成立,就像是xjjdog在青岛混了这么多年,一旦再杀回北上广,也一样是落的下乘的评价。


除了自身的努力之外,你在上家公司混的差,还与你在组织架构中所处于的位置和组织架构本身有关。


一般公司会有两种组织架构方式:垂直化划分层级化划分


1. 垂直划分


垂直划分,多以业务线为模型进行划分。各条业务线共用公司行政资源,相互之间关联不大。


各业务线之间,内部拥有自治权。


image.png


如上图所示,公司共有四个业务线。




  • 业务线A,有前端和后端开发。因为成员能力比较强,所以没有测试运维等职位;




  • 业务线B倡导全栈技能,开发后台前端一体化;




  • 业务线C的管理能力比较强,仅靠少量自有研发,加上大量的外包,能够完成一次性工作。




  • 业务线D是传统的互联网方式,专人专岗,缺什么招什么,不提倡内部转岗




运行模式




  1. 业务线A缺人,缺项目,与业务线BCD无任何关系,不允许借调




  2. 业务线发展良好,会扩大规模;其他业务线同学想要加入需要经过复杂的流程,相当于重新找工作




  3. 业务线发展萎靡,会缩减人员,甚至会整体砍掉。优秀者会被打散吸收进其他业务线




好处




  1. 业务线之间存在竞争关系,团队成员有明确的奋斗目标和危机意识




  2. 一条业务线管理和产品上的失败,不会影响公司整体运营




  3. 可以比较容易的形成单向汇报的结构,避免成本巨大且有偏差的多重管理




  4. 便于复制成功的业务线,或者找准公司的发展重点




坏处




  1. 对业务线主要分管领导的要求非常高




  2. 多项技术和产品重复建设,容易造成人员膨胀,成本浪费




  3. 部门之间隔阂加大,共建、合作困难,与产品化相逆




  4. 业务线容易过度自治,脱离掌控




  5. 太激进,大量过渡事宜需要处理




修订


为了解决上面存在的问题,通常会有一个协调和监管部门,每个业务线,还需要有响应的协调人进行对接。以以往的观察来看,效果并不会太好。因为这样的协调,多陷于人情沟通,不好设计流程规范约束这些参与人的行为。


image.png


在公司未摸清发展方向之前,并不推荐此方式的改革。它的本意是通过竞争增加部门的进取心,通过充分授权和自治发挥骨干领导者的作用。但在未有成功案例之前,它的结果变成了:寄希望于拆分成多个小业务线,来解决原大业务线存在的问题。所以依然是处于不太确定的尝试行为。


2. 水平划分


水平划分方式,适合公司有确定的产品,并能够形成持续迭代的团队。


它的主要思想,是要打破“不会做饭的项目经理不是好程序员”的思维,形成专人专业专岗的制度。


这种方式经历了非常多的互联网公司实践,可以说是最节约研发成本,能动性最高的组织方式。主要是因为:




  • 研发各司其职,做好自己的本职工作可以避免任务切换、沟通成本,达到整体最优




  • 个人单向汇报,组织层级化,小组扁平化。“替领导负责,就是替公司负责”




  • 任何职位有明确的JD,可替换性高,包括小组领导




这种方式最大的问题就是,对团队成员的要求都很高。主动性与专业技能都有要求,需要经过严格的面试筛选。


坏处




  • 是否适合项目类公司,存疑




  • 存在较多技术保障部门,公共需求 下沉容易造成任务积压




  • 需要对其他部门进行整合,才能发挥更大的价值




分析


image.png


如上图,大体会分为三层。




  • 技术保障,保障公司的底层技术支撑,问题处理和疑难问题解决。小组多但人少,职责分明




  • 基础业务,公司的旗舰业务团队,需求变更小但任何改动都非常困难。团队人数适中




  • 项目演化,纯项目,可以是一锤子买卖,也可以是服务升级,属于朝令夕改类需求的聚居地。人数最多




可以看到项目演化层,多是脏活,有些甚至是尝试性的项目-----这是合理的。




  1. 技术保障和基础业务的技术能力要求高,业务稳定,适合长期在公司发展,发展属性偏技术的人群,流动性小,招聘困难




  2. 项目演化层,业务多变,项目奖金或者其他回报波动大,人员流动性高,招聘容易




成功的孵化项目,会蜕变成产品,或者基础业务,并入基础业务分组。


从这种划分可以看出,一个人在公司的命运和发展,在招聘入职的时候就已经确定了。应聘人员可以根据公司的需求进行判断,提前预知自己的倾向。


互联网公司大多数将项目演化层的人员当作炮灰,因为他们招聘容易,团队组件迅速,但也有很多可能获得高额回报,这也是很多人看中的。


3.组合


组合一下垂直划分和层级划分,可以是下面这种效果。


image.png


采用层级+垂直方式进行架构。即:首选层级模式,然后在项目演化层采用垂直模式,也叫做业务线,拥有有限的自治权。


为每一个业务线配备一个与下层产品化或者技术保障对接的人员。


绩效方面,上层的需求为下层的实现打分。基础业务和技术保障,为绿色的协调人员打分。他们的利益是一致的。


End


大公司出来的并不一定是精英,小公司出来的也并不一定是渣渣。这取决于他在公司的位置和所从事的内容。核心部门会得到更多的利益,而边缘的尝试性部门只能吃一些残羹剩饭。退去公司的光环,加上平庸的项目经历,竞争力自然就打上一个折扣。


以上,仅限IT行业哦。赵家人不在此列。


作者:小姐姐味道
来源:juejin.cn/post/7203651773622452261
收起阅读 »

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

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

问题


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


思考


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


方案一


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


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


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


方案二


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


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

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

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


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


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


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


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


image.png


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


image.png


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


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


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



  • 获取焦点/失去焦点时

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

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

  • 清空输入框时

  • 切换离开页面时


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


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


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

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

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

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

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


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


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


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

假如面试官让你讲一下新版雪花算法

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。 seata.io/zh-cn/blog/… 还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。 虽然是在 Seata ...
继续阅读 »

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。



seata.io/zh-cn/blog/…



image.png


还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。


虽然是在 Seata 里面看到的,但是本篇文章的内容和 Seata 框架没有太多关系,反而和数据库的基础知识有关。


所以,即使你不了解 Seata 框架,也不影响你阅读。


当你理解了这个类的工作原理之后,你完全可以把这个只有 100 多行的类搬运到你的项目里面,然后就变成你的了。


dddd(懂的都懂)!



先说问题



雪花算法的使用场景:


如果你的项目中涉及到需要一个全局唯一的流水号,比如订单号、流水号之类的,又或者在分库分表的情况下,需要一个全局唯一的主键 ID 的时候,就需要一个算法能生成出这样“全局唯一”的数据。


一般来说,我们除了“全局唯一”这个基本属性之外,还会要求生成出来的 ID 具有“递增趋势”,这样的好处是能减少 MySQL 数据页分裂的情况,从而减少数据库的 IO 压力,提升服务的性能。


此外,在当前的市场环境下,不管你是啥服务,张口就是高并发,我们也会要求这个算法必须得是“高性能”的。


雪花算法,就是一个能生产全局唯一的、递增趋势的、高性能的分布式 ID 生成算法。


关于雪花算法的解析,网上相关的文章比雪花还多,这个玩意,应该是“面试八股文”中重点考察模块,分布式领域中的高频考题之一,如果是你的盲区的话,可自行去了解一下。


比如一个经典的面试题就是:


雪花算法最大的缺点是什么?


背过题的小伙伴应该能立马答出来:时钟敏感


因为在雪花算法中,由于要生成单调递增的 ID,因此它利用了时间的单调递增性,所以是强依赖于系统时间的。


如果系统时间出现了回拨,那么生成的 ID 就可能会重复。


而“时间回拨”这个现象,是有可能出现的,不管是人为的还是非人为的。


当你回答出这个问题之后,面试官一般会问一句:


那如果真的出现了这种情况,应该怎么办呢?


很简单,正常来说只要不是不是有人手贱或者出于泄愤的目的进行干扰,系统的时间漂移是一个在毫秒级别的极短的时间。


所以可以在获取 ID 的时候,记录一下当前的时间戳。然后在下一次过来获取的时候,对比一下当前时间戳和上次记录的时间戳,如果发现当前时间戳小于上次记录的时间戳,所以出现了时钟回拨现象,对外抛出异常,本次 ID 获取失败。


理论上当前时间戳会很快的追赶上上次记录的时间戳。


但是,你可能也注意到了,“对外抛出异常,本次 ID 获取失败”,意味着这段时间内你的服务对外是不可使用的。


比如,你的订单号中的某个部分是由这个 ID 组成的,此时由于 ID 生成不了,你的订单号就生成不了,从而导致下单失败。


再比如,在 Seata 里面,如果是使用数据库作为 TC 集群的存储工具,那么这段时间内该 TC 就是处于不可用状态。


你可以简单的理解为:基础组件的错误导致服务不可用。



再看代码



基于前面说的问题,Seata 才提出了“改良版雪花算法”。



seata.io/zh-cn/blog/…



图片


在介绍改良版之前,我们先把 Seata 的源码拉下来,瞅一眼。


在源码中,有一个叫做 IdWorker 的类:



io.seata.common.util.IdWorker



再来看一下它的提交记录:


图片


2020 年 5 月 4 日第一次提交,从提交时的信息可以看出来,这是把分布式 ID 的生成策略修改为 snowflake,即雪花算法。


同时我们也能在代码中找到前面提到的“对外抛出异常,本次 ID 获取失败”相关代码,即 nextId  方法,它的比较方式就是用当前时间戳和上次获取到的时间戳做对比:



io.seata.common.util.IdWorker#nextId



图片


这个类的最后一次提交是 2020 年 12 月 15 日:


图片


这一次提交对于 IdWorker 这个类进行了大刀阔斧的改进,可以看到变化的部分非常的多:


图片


我们重点关注刚刚提到的 nextId 方法:


图片


整个方法从代码行数上来看都可以直观的看到变化,至少没有看到抛出异常了。


这段代码到底是怎么起作用的呢?


首先,我们得理解 Seata 的改良思路,搞明白思路了,再说代码就好理解一点。


在前面提到的文章中 Seata 也说明了它的核心思路:


图片


原版的雪花算法 64 位 ID 是分配这样的:


图片


可以看到,时间戳是在最前面的,因为雪花算法利用了时间的单调递增的特性。


所以,如果前面的时间戳一旦出现“回退”的情况,即打破了“时间的单调递增”这个前提条件,也就打破了它的底层设计。


它能怎么办?


它只能给你抛出异常,开始摆烂了。


可以看到节点 ID 长度为二进制的 10 位,也就是说最多可以服务于 1024 台机器,所以你看 Seata 最开始提交的版本里面,有一个在 1024 里面随机的动作。


因为算法规定了,节点 ID 最多就是 2 的 10 次方,所以这里的 1024 这个值就是这样来的:


图片


包括后面有大佬觉得用这个随机算法一点都不优雅,就把这部分改成了基于 IP 去获取:


图片


看起来有点复杂,但是我们仔细去分析最后一行:



return ((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF);



变量 & 0B11 运算之后的最大值就是 0B11 即 3。


Byte.SIZE = 8。


所以,3 << 8,对应二进制 1100000000,对应十进制 768。


变量 & 0xFF 运算之后的最大值就是 0xFF 即 255。


768+255=1023,取值范围都还是在 [0,1023] 之间。


然后你再看现在最新的算法里面,官方的老哥们认为获取 IP 的方式不够好:


图片


所以又把这个地方从使用 IP 地址改成了获取 Mac 地址。


图片


最后一行是这样的:



return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);



那么理论上的最大值就是 768 | 255 ,算出来还是 1023。


所以不管你怎么玩出花儿来,这个地方搞出来的数的取值范围就只能是 [0,1023] 之间。


别问,问就是规定里面说了,算法里面只给节点 ID 留了 10 位长度。


最后,就是这个 12 位长度的序列号了:


图片


这个玩意没啥说的,就是一个单纯的、递增的序列号而已。


既然 Seata 号称是改良版,那么具体体现在什么地方呢?


简单到你无法想象:


图片


是的,仅仅是把时间戳和节点 ID 换个位置就搞定了。


然后每个节点的时间戳是在 IdWorker 初始化的时候就设置完成了,具体体现到代码上是这样的:



io.seata.common.util.IdWorker#initTimestampAndSequence



图片


在获取 ID 的过程中,只有在初始化的时候获取过一次系统时间,之后和它就再也没有关系了。


所以,Seata 的分布式 ID 生成器,不再依赖于时间。


然后,你再想想另外一个问题:


由于序列号只有 12 位,它的取值范围就是 [0,4095]。如果我们序列号就是生成到了 4096 导致溢出了,怎么办呢?



很简单,序列号重新归 0,溢出的这一位加到时间戳上,让时间戳 +1。


那你再进一步想想,如果让时间戳 +1 了,那么岂不是会导致一种“超前消费”的情况出现,导致时间戳和系统时间不一致了?


朋友,慌啥啊,不一致就不一致呗,反正我们现在也不依赖于系统时间了。


然后,你想想,如果出现“超前消费”,意味着什么?


意味着在当前这个毫秒下,4096 个序列号不够用了。


4096/ms,约 400w/s。


你啥场景啊,怎么牛逼?


(哦,原来是面试场景啊,那懂了~)


另外,官网还抛出了另外一个问题:这样持续不断的"超前消费"会不会使得生成器内的时间戳大大超前于系统的时间戳,从而在重启时造成 ID 重复?


你想想,理论上确实是有可能的。假设我时间戳都“超前消费”到一个月以后了。


那么在这期间,你服务发生重启时我会重新获取一次系统时间戳,导致出现“时间回溯”的情况。


理论上确实有可能。



但是实际上...


看看官方的回复:


图片



别问,问就是不可能,就算出现了,最先崩的也不是我这个地方。



说了这么多其实就记住住这个图,就完事了:


图片


那么问题又来了:


改良版的算法是单调递增的吗?



在单节点里面,它肯定是单调递增的,但是如果是多个节点呢?


在多个节点的情况下,单独看某个节点的 ID 是单调递增的,但是多个节点下并不是全局单调递增。


因为节点 ID 在时间戳之前,所以节点 ID 大的,生成的 ID 一定大于节点 ID 小的,不管时间上谁先谁后。



这一点我们也可以通过代码验证一下,代码的意思是三个节点,每个节点各自生成 5 个 ID:


图片


从输出来看,一眼望去,生成的 ID 似乎是乱序的,至少在全局的角度下,肯定不是单调递增的:


但是我们把输出按照节点 ID 进行排序,就变成了这样,单节点内严格按照单调递增,没毛病:


图片


而在原版的雪花算法中,时间戳在高位,并且始终以系统时钟为准,每次生成的时候都会严格和系统时间进行对比,确保没有发生时间回溯,这样可以保证早生成的 ID 一定小于晚生成的 ID ,只有当 2 个节点恰好在同一时间戳生成 ID 时,2 个 ID 的大小才由节点 ID 决定。


这样看来,Seata 的改进算法是不是错的?


先别急,继续往下看。



分析一波



分析之前,先抛出官方的回答:


图片


先来一个八股文热身:


请问为什么不建议使用 UUID 作为数据库的主键 ID ?


就是为了避免触发 MySQL 的页分裂从而影响服务性能。


比如当前主键索引的情况是这样的:


图片


如果来了一个  433,那么直接追加在当前最后一个记录 432 之后即可。


图片


但是如果我们要插入一个 20 怎么办呢?


那么数据页 10 里面已经放满了数据,所以会触发页分裂,变成这样:


图片


进而导致上层数据页的分裂,最终变成这样的一个东西:


图片


上面的我们可以看出页分裂伴随着数据移动,所以我们应该尽量避免。


理想的情况下,应该是把一页数据塞满之后,再新建另外一个数据页,这样 B+ tree 的最底层的双向链表永远是尾部增长,不会出现上面画图的那种情况:在中间的某个节点发生分裂。


那么 Seata 的改良版的雪花算法在不具备“全局的单调递增性”的情况下,是怎么达到减少数据库的页分裂的目的的呢?


我们还是假设有三个节点,我用 A,B,C 代替,在数值上 A < B < C,采用的是改良版的雪花算法,在初始化的情况下是这样的。


图片


假设此时,A 节点申请了一个流水号,那么基于前面的分析,它一定是排在 A-seq1 之后,B-seq1 之前的。


但是这个时候数据页里面的数据满了,怎么办?


分裂呗:


图片


又来了 A-seq3 怎么办?


问题不大,还放的下:


图片


好,这个时候数据页 7 满了,但是又来了 A-seq4,怎么办?


只有继续分裂了:


图片


看到这个分裂的时候,你有没有嗦出一丝味道,是不是有点意思了?


因为在节点 A 上生成的任何 ID 都一定小于在节点 B 上生成的任何 ID,节点 B 和节点 C 同理。


在这个范围内,所有的 ID 都是单调递增的:


图片


而这样的范围最多有多少个?


是不是有多少个节点,就有多少个?


那么最多有多少个节点?


图片


2 的 10 次方,1024 个节点。


所以官方的文章中有这样的一句话:



新版算法从全局角度来看,ID 是无序的,但对于每一个 workerId,它生成的 ID 都是严格单调递增的,又因为 workerId 是有限的,所以最多可划分出 1024 个子序列,每个子序列都是单调递增的。



经过前面的分析,每个子序列总是单调递增的,所以每个子序列在有限次的分裂之后,最终都会达到稳态。


或者用一个数学上的说法:该算法是收敛的。


再或者,放个图看看:


图片


我想说作者画的时候尽力了,至于你看懂看不懂的,就看天意了。


页分裂


前面写的所有内容,你都能在官网上前面提到的两个文章中找到对应的部分。


但是关于页分裂部分,官方并没有进行详细说明。本来也是这样的,人家只是给你说自己的算法,没有必要延伸的太远。


“刚才说到了页分裂”,以面试官的嘴脸怎么可能放过你,“展开讲讲?”


链接已放,自行展开:



mysql.taobao.org/monthly/202…



还是先搞个图:


图片


问,在上面的这个 B+ tree 中,如果我要插入 9,应该怎么办?


因为数据页中已经没有位置了,所以肯定要触发页分裂。


会变成这样:


图片


这种页分裂方式叫做插入点(insert point)分裂。


其实在 InnoDB 中最常用的是另外一种分裂方式,中间点(mid point)分裂。


如果采用中间点(mid point)分裂,上面的图就会变成这样:


图片


即把将原数据页面中的 50% 数据移动到新页面,这种才是普遍的分裂方法。


这种分裂方法使两个数据页的空闲率都是 50%,如果之后的数据在这两个数据页上的插入是随机的话,那么就可以很好地利用空闲空间。


但是,如果后续数据插入不是随机,而是递增的呢?


比如我插入 10 和 11。


插入 10 之后是这样的:


图片


插入 11 的时候又要分页了,采用中间点(mid point)分裂就变成了这样:


图片


你看,如果是在递增的情况下,采用中间点(mid point)分裂,数据页 8 和 20 的空间利用率只有 50%。


因为数据的填充和分裂的永远是右侧页面,左侧页面的利用率只有 50%。


所以,插入点(insert point)分裂是为了优化中间点(mid point)分裂的问题而产生的。


InnoDB 在每个数据页上专门有一个叫做 PAGE_LAST_INSERT 的字段,记录了上次插入位置,用来判断当前插入是是否是递增或者是递减的。


如果是递减的,数据页则会向左分裂,然后移动上一页的部分数据过去。


如果判定为递增插入,就在当前点进行插入点分裂。


比如还是这个图:


图片


上次插入的是记录 8,本次插入 9,判断为递增插入,所以采用插入点分裂,所以才有了上面这个图片。


好,那么问题就来了,请听题:


假设出现了这种情况,阁下又该如何应对?


图片


在上面这个图的情况下,我要插入 10 和 9:


当插入 10 的时候,按 InnoDB 遍历 B+ tree 的方法会定位到记录 8,此时这个页面的 PAGE_LAST_INSERT 还是 8。所以会被判断为递增插入,在插入点分裂:


图片


同理插入 9 也是这样的:


图片


最终导致 9、10、11 都独自占据一个 page,空间利用率极低。


问题的根本原因在于每次都定位到记录 8(end of page),并且都判定为递增模式。


哦豁,你说这怎么办?


答案就藏在这一节开始的时候放的链接中:


图片


前面所画的图都是在没有并发的情况下展开的。


但是在这个部分里面,牵扯到了更为复杂的并发操作,同时也侧面解释了为什么 InnoDB 在同一时刻只能有有一个结构调整(SMO)进行。


这里面学问就大了去了,有兴趣的可以去了解一下 InnoDB 在 B+ tree 并发控制上的限制,然后再看看 Polar index 的破局之道。


反正我是学不动了。


哦,对了。前面说了这么多,还只是聊了页分裂的情况。


有分裂,就肯定有合并。


那么什么时候会触发页合并呢?


页合并会对我们前面探讨的 Seata 的改良版雪花算法带来什么影响呢?


别问了,别问了,学不动了,学不动了。


作者:essenceNow
来源:juejin.cn/post/7265641370495451148
收起阅读 »

iOS:零碎整理iOS音视频开发API

在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒ios音效支持的格式: ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-...
继续阅读 »

在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒

  • ios音效支持的格式: ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-aac、mp3、caf支持硬件解码,其他只支持软件解码, 软件界面因为比较耗电,所以,我们在开发过程中,经常采用的是caf、mp3

  • 音频库: AVFoundation.framework

代码

// 打开资源
NSURL* url =[[NSBundle mainBundle]URLForResource:@"m_03" withExtension:@"wav"];
SystemSoundID soundID;
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID);
// 播放音效
AudioServicesPlaySystemSound(self.soundID);
// 删除音效
AudioServicesDisposeSystemSoundID(self.soundID);
  • 框架

  • 加载音乐资源并播放

AVAudioPlayer* player = musicDict[fileName];
if (!player) {
NSURL* url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
NSCAssert(url != nil, @"fileName not found musics");

NSError* error;
player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
if (error) {
NSLog(@"load music error");
return;
}
[musicDict setObject:player forKey:fileName];
}
if (player.isPlaying == NO) {
[player play];
}
  • 暂停 停止操作

[player pause];// 暂停
[player stop];// 停止
[player isplaying];// 是否在播放

好了,现在能播放音乐了,但我们在看其他的应用的时候,一般当应用切换到后台的时候也能播放音乐,那这个又是如何实现的呢?这个只要设置音频的后台播放,具体为:

1> 在后台开启一个任务

- (void)applicationDidEnterBackground:(UIApplication *)application
{
// 开启后台任务,让音乐继续播放
[application beginBackgroundTaskWithExpirationHandler:nil];
}

  2> 设置项目配置文件


   3> 设置音频链接会话,这个主要告诉设备如何处理音频事件的

1234// 设置音频会话类型``   ``AVAudioSession* session = [AVAudioSession sharedInstance];``   ``[session setCategory:AVAudioSessionCategorySoloAmbient error:``nil``];``   ``[session setActive:``YES error:``nil``];

这里有很多会话类型,如果想详细了解,可参考:blog.csdn.net/daiyelang/a…

现在应该可以播放音乐了。


作者:会飞的金鱼
链接:https://juejin.cn/post/7238110426147373112
来源:稀土掘金
收起阅读 »

iOS中UICollectionView的item增删、拖拽和排序动画

iOS
效果图 这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView + UIView 实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView简单实现了下。 思路 简单理一下思路,首先是把整个页面先布局出来,这...
继续阅读 »

效果图




这个是前段时间项目新增的一个功能,刚刚开始组员是用UIScrollView + UIView 实现的,但这种实现方式属实是有点low,后续闲暇时笔者用UICollectionView简单实现了下。


思路


简单理一下思路,首先是把整个页面先布局出来,这里涉及到一个UICollectionViewSection背景色的问题,有需要的可以点这里,有详细的介绍。移动动画也很简单,先获取起点cell和终点cell,再新建一个动画的AnimationItem,根据获取的起点和终点cell动画就行,最后再实现拖拽排序效果。


大致总结一下:

  • 布局

  • 移动动画

  • 拖拽排序


下面就根据思路一步步来。我们一定要有一个意识,不管是多么复杂的动画,只要把它分解开来,按步骤一步一步实现就很简单。


实现


我们就跟上面的思路一步一步实现。


布局


首先想到肯定是新建一个Model来管理这个数据,新建Model也要有点技巧。

struct ItemModel {
    var section: Int = -1 // cell的section索引
    var item: Int = -1 // cell的item索引
    var name: String = "" // 名称
    var isAdded: Bool = false // 是否添加到首页应用(第0区)
    var id: String { // 唯一标识,可以用这个来命名图片的名称,也可以用来作判断
        get {
            "\(section)_\(item)"
        }
    }
    init(){}
}

看得出来,ItemModel的属性section + item = IndexPath,可以根据 model 知道当前cell的所在位置了。


笔者这用的是 struct ,感觉用 class 会更好点,因为后续会改变数组中Model的属性值。已经写了就懒得再改了。


存在2个数组数据:

  • var editItems = [ItemModel](),由前一页传入的、可编辑、拖拽的数据,位于UICollectionView的第0个Section。

  • var datas = [[ItemModel]](),按照Section的顺序,存放所有的数据。


注意:Section要从1开始,因为第0个Section是可以编辑拖拽的区域。


datas中存放全部的数据:

for i in 0..<names.count {
let subNames = names[i]
var items = [ItemModel]()
for j in 0..<subNames.count {
var model = ItemModel()
model.section = i+1 // 注意这里的Section要从1开始
model.item = j
model.name = subNames[j]
model.isAdded = editItems.contains(where: { $0.id == model.id})
items.append(model)
}
datas.append(items)
}

根据数据布局UICollectionView


移动动画


移动只要2个操作,添加应用和删除应用。


添加


笔者这里规定了最多可以添加8个应用。


大致思路:

  • 获取当前点击的 cell,为了得到其坐标作为动画起始位置

  • 在 collectionView 中插入一个空白的 cell 占位,此举是为了增加或减少行数的动画过渡更自然;对应也应该在 editItems 中添加一个空白的 model 作为数据源,等移动动画结束后再给model重新赋值。

  • 获取新插入的空白 cell,为了得到其坐标作为动画的结束位置

  • 生成动画的 cell,起始 -> 结束 动画。

  • 更新数据,刷新


删除


思路与添加雷同,且比之更简单


具体的思路和步骤,代码中都有一步步的注释,可自行查阅。


拖拽排序


这个拖拽排序,在iOS11之前的比较麻烦,都是靠自己计算,这里也简单说下思路:


iOS11.0之前的实现思路

  1. 在UICollectionView上添加一个长按的手势

  2. 在UICollectionView上面添加一个浮动隐藏的cell,便于拖拽

  3. 通过长按操作找到需要被拖动的cellA

  4. 通过拖动cellA找到找到和它交换位置的cellB

  5. 交换cellA和cellB的位置

  6. 替换数据源,把起始位置的数据模型删除,然后将起始位置的数据模型插入到拖拽位置


这种比较复杂的是结合位置判断需要交换的cell。但是在iOS11之后,UICollectionView新增了dragDelegatedropDelegate,用来实现拖拽排序的效果。


dragDelegate、dropDelegate


直接上代码:

collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
collectionView.reorderingCadence = .immediate
collectionView.isSpringLoaded = true

  • dragInteractionEnabled 属性要设置为 true,才可以进行 drag 操作。此属性在 iPad 默认是 true,在 iPhone 默认是 false。

  • reorderingCadence 重排序节奏,可以调节集合视图重排序的响应性。

  • UICollectionViewReorderingCadenceImmediate 默认值。当开始移动的时候就立即回流集合视图布局,实时的重新排序。

  • UICollectionViewReorderingCadenceFast 快速移动,不会立即重新布局,只有在停止移动的时候才会重新布局

  • UICollectionViewReorderingCadenceSlow 停止移动再过一会儿才会开始回流,重新布局

  • isSpringLoaded 弹性加载效果,也可以使用代理方法:func collectionView(_ collectionView: UICollectionView, shouldSpringLoadItemAt indexPath: IndexPath, with context: UISpringLoadedInteractionContext) -> Bool。

需要实现UICollectionViewDropDelegateUICollectionViewDragDelegate协议方法。下面是常用的几个方法,按照调用的先后顺序说明一下:

/*
* 识别到拖动,一次拖动一个;若一次拖动多个,则需要选中多个
* 提供一个给定 indexPath 的可进行 drag 操作的 item
* NSItemProvider, 拖放处理时,携带数据的容器,通过对象初始化,该对象需满足 NSItemProviderWriting 协议
*/
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem]

/*
* 使用自定义预览,如果该方法没有实现或者返回nil,那么整个 cell 将用于预览
* UIDragPreviewParameters有2个属性:backgroundColor设置背景颜色;visiblePath设置视图的可见区域
* 笔者使用这个方法除去了拖拽过程中item的阴影
*/
func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters?

/*
* 开始拖拽后,继续添加拖拽的任务,处理雷同`itemsForBeginning`方法
*/
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem]

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession)

/*
* 判断对应的 item 能否被执行drop会话,是否能放置
*/
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool

/*
* 处理拖动中放置的策略,此方法会 频繁调用,在此方法中应尽可能减少工作量。
* 四种分别:move移动;copy拷贝;forbidden禁止,即不能放置;cancel用户取消。
* 效果一般使用2种:.insertAtDestinationIndexPath 挤压移动;.insertIntoDestinationIndexPath 取代。
* 在某些情况下,目标索引路径可能为空(比如拖到一个没有cell的空白区域)你可以通过 session.locationInView 做你自己的命中测试
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal

/*
* 当drop会话进入到 collectionView 的坐标区域内就会调用
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnter session: UIDropSession)

/*
* 结束放置时的处理
* 如果该方法不做任何事,将会执行默认的动画
*/
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator)

/*
* 拖拽开始,可自行处理
*/
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession)

/*
* 当dropSession 完成时会被调用,不管结果如何。一般进行清理或刷新操作
*/
func collectionView(_ collectionView: UICollectionView, dropSessionDidEnd session: UIDropSession)

这里大概的拖拽动画就差不多了,代码中会有更详细的注释。


总结


将一个大功能拆分成一个个小模块,按部就班一步一步实现就不难了。这里只是中间囊括了各种小动画和刷新,设计好思路,不行就多试几次肯定可以。


代码自取:RCDragDropAnimation




若存在什么不对的地方,欢迎指正!


作者:云层之上
链接:https://juejin.cn/post/7246777949100933177
来源:稀土掘金
收起阅读 »

iOS 中如何精准还原 Sketch 线性渐变效果

iOS
背景 这样的渐变效果当然用切图是可以方便的实现,但切图不够灵活,而且会增加包大小 那如何用代码实现呢? 首先看下 iOS 中渐变的几个参数 colors startPoint endPoint locations colors 很好获取,其他三个参数怎么...
继续阅读 »

背景




这样的渐变效果当然用切图是可以方便的实现,但切图不够灵活,而且会增加包大小


那如何用代码实现呢?


首先看下 iOS 中渐变的几个参数



  • colors

  • startPoint

  • endPoint

  • locations


colors 很好获取,其他三个参数怎么办呢,似乎只能看图猜出个大概?


猜想


众所周知,sketch 有一键导出标注的插件 ,但是只能获取到 locations 信息

background-image: linear-gradient(-73deg, #361CE6 0%, #7DA7EB 50%, #96A4FF 100%);

并且这个 -73deg 对于 iOS 中的 startPoint``endPoint 来说还不太友好,需要经过一番转换。


这个时候心中有个想法💡,这个插件能导出这些信息应该是对 sketch 的源文件进行了解析,那么 sketch 的源文件是个什么样的文件呢,会不会像 .ipa 那样是个压缩包呢?


实践


file 命令可以查看文件的信息

file Test.sketch

输出如下结果

Test.sketch: Zip archive data, at least v2.0 to extract, compression method=deflate

可以看到这确实是一个压缩包

那就可以用 unzip 命令来解压一下

unzip Test.sketch -d ./temp

👀看看解压出了个啥呢?

.
├── document.json
├── meta.json
├── pages
│   └── 7832D4DC-A896-40BE-8F96-45850CE9FC53.json
├── previews
│   └── preview.png
└── user.json

有 json 文件!欣喜若狂😁!!!最终在 pages 这个目录下的 json 文件找到了想要的东西

{
"_class": "gradient",
"elipseLength": 0,
"from": "{1.1356274384397782, 0.99999999999999978}",
"gradientType": 0,
"to": "{-0.13533980933892775, -0.49069446290249097}",
"stops": [
{
"_class": "gradientStop",
"position": 0,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.903056932532269,
"green": 0.1092045213150163,
"red": 0.2098672970162421
}
},
{
"_class": "gradientStop",
"position": 0.4973543951952161,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.9204804793648098,
"green": 0.6532974892326747,
"red": 0.4919794574547816
}
},
{
"_class": "gradientStop",
"position": 1,
"color": {
"_class": "color",
"alpha": 1,
"blue": 1,
"green": 0.6418734727143864,
"red": 0.5896740424923781
}
}
]
}

结论

  • from 是 startPoint

  • to 是 endPoint

  • stops 中的 position 是 locations


Tips: UI 给的 sketch 文件可能图层太多,json 文件会非常大,打开比较卡,可以把图层复制到自己新建的 sketch 文件中再解压


作者:LittleYuuuuu
链接:https://juejin.cn/post/7222179242946641978
来源:稀土掘金
收起阅读 »

iOS17适配指南之UIContentUnavailableView(一)

iOS
介绍 新增视图,表示内容不可达,特别适用于没有数据时的占位视图。 UIContentUnavailableConfigurationUIContentUnavailableView 的配置参数,用于设置不可达时的占位内容。既可以使用 UIKit,又可以使用 S...
继续阅读 »

介绍


新增视图,表示内容不可达,特别适用于没有数据时的占位视图。


UIContentUnavailableConfiguration

  • UIContentUnavailableView 的配置参数,用于设置不可达时的占位内容。

  • 既可以使用 UIKit,又可以使用 SwiftUI。

  • 系统提供了 3 种配置,分别为empty()、loading()与search()。

  • UIViewController 增加了一个该类型的参数contentUnavailableConfiguration,用于设置view内容不可达时的占位内容。


案例一

import UIKit

class ViewController: UIViewController {
lazy var tableView: UITableView = {
let tableView = UITableView(frame: UIScreen.main.bounds, style: .plain)
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "abc")
return tableView
}()
// UIContentUnavailableView
lazy var unavailableView: UIContentUnavailableView = {
var config = UIContentUnavailableConfiguration.empty()
// 配置内容
config.text = "暂无数据"
config.textProperties.color = .red
config.secondaryText = "正在加载数据..."
config.image = UIImage(systemName: "exclamationmark.triangle")
config.imageProperties.tintColor = .red
var buttonConfig = UIButton.Configuration.filled()
buttonConfig.title = "加载数据"
config.button = buttonConfig
config.buttonProperties.primaryAction = UIAction(title: "") { _ in
self.loadData()
}
var backgroundConfig = UIBackgroundConfiguration.listPlainCell()
backgroundConfig.backgroundColor = .systemGray6
config.background = backgroundConfig
// 创建UIContentUnavailableView
let unavailableView = UIContentUnavailableView(configuration: config)
unavailableView.frame = UIScreen.main.bounds
return unavailableView
}()
var content: [String] = []

override func viewDidLoad() {
super.viewDidLoad()

view.addSubview(tableView)
if content.isEmpty {
view.addSubview(unavailableView)
}
}

func loadData() {
content = ["iPhone 12 mini", "iPhone 12", "iPhone 12 Pro", "iPhone 12 Pro Max",
"iPhone 13 mini", "iPhone 13", "iPhone 13 Pro", "iPhone 13 Pro Max",
"iPhone 14", "iPhone 14 Plus", "iPhone 14 Pro", "iPhone 14 Pro Max"]
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.tableView.reloadData()
self.unavailableView.removeFromSuperview()
}
}
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return content.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "abc", for: indexPath)
cell.textLabel?.text = content[indexPath.row]
cell.imageView?.image = UIImage(systemName: "iphone")
return cell
}
}

效果:


案列二

import UIKit

class ViewController: UIViewController {
lazy var emptyConfig: UIContentUnavailableConfiguration = {
var config = UIContentUnavailableConfiguration.empty()
config.text = "暂无数据"
config.image = UIImage(systemName: "exclamationmark.triangle")
return config
}()

override func viewDidLoad() {
super.viewDidLoad()

contentUnavailableConfiguration = emptyConfig
}

// MARK: - 更新UIContentUnavailableConfiguration
override func updateContentUnavailableConfiguration(using state: UIContentUnavailableConfigurationState) {
// 切换
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
let loadingConfig = UIContentUnavailableConfiguration.loading()
self.contentUnavailableConfiguration = loadingConfig
}
// 移除
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
self.contentUnavailableConfiguration = nil
self.view.backgroundColor = .systemTeal
}
}
}


作者:YungFan
链接:https://juejin.cn/post/7257732392541634621
来源:稀土掘金

收起阅读 »

你还在傻傻的打开页面输用户名和密码?来我教你实现自动化

web
背景 Hello~,大家好! 本文和各位分享一个有趣的事情! 我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验...
继续阅读 »

背景


Hello~,大家好! 本文和各位分享一个有趣的事情!


我司主要的客户是银行,随着银行对信息安全越来越重视,我司积极配合银行防范信息安全,因此我司产品都从之前的外网开发引入了深信服的云桌面开发。由于笔者用的 Mac 电脑,云桌面 win7,天差地别的体验我就先忍了😭,但是从 Mac 电脑桌面到进入云桌面,需要登录一些莫名其妙的 VPN、网址,这个过程是令人反感的、重复的、也是非常恶心的,每天都要这样、要等待很久......🤮



  1. 打开Mac上的xxxTrust并登录

  2. 打开Chrome

  3. 打开xxx网址

  4. 输入用户名、密码(不能自动填充那种)

  5. 点击登录

  6. 进入云桌面资源页面,点击一个资源,会自动调起 Mac 上安装的一个什么 VDIxxx 的软件

  7. 成功进入云桌面


我丢,各位来说说,这个过程是不是很恶心。作为一个技术人,我们不能忍受这种机械式的操作,我们要去做出改变。不能让自己一直做这些重复的恶心操作,于是我就想着 能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面? 这就是笔者本文的主题。


实现自动化


其实笔者也没有一步到位——能不能像我打开 Mac 桌面的一个应用那样,中间的步骤自动完成,直到进入云桌面?。实现过程中,有一些新想法,接下来就分享一下我从开始有这个想法到实现的过程:


ConsoleSnippets


一开始: 我已经登录了 xxxTrust,笔者在浏览器已经打开了xxx网址,只是不想输密码,我就想起浏览器开发者工具 ConsoleSnippets,可以在里面写一些脚本,然后可以快捷执行:


image.png

打开控制台 -> 快捷键 command + p -> 输入!,选择执行哪个 Snippets -> 回车。看下效果:
1.gif


感觉还行是吧,那接着来,我们现在进入了资源管理的界面,接下来需要手动点击打开一个云桌面的资源,同样地,接着建一个 Snippets


image.png

看下效果:
1.gif


OK!成功进入,但是现在还需要我们手动去执行脚本,而且要执行两个。于是就有了新的想法。


篡改猴


能不能在对应的页面自动执行上面写好的脚本?


此时笔者想到了一个 Chrome 插件 —— 篡改猴,也叫油猴脚本,简单理解它的作用就是可以在你指定的网页中执行你写入的脚本。那就装一个呗!(Chrome商店需要🪜,可直接使用Edge浏览器)


image.png

打开管理面板,新建两个脚本:
image.png


编辑:
截屏2023-08-11 11.46.22.png


截屏2023-08-11 12.43.48.png

然后改一下设置,在 document-end 执行我们的脚本,此时可能还获取不到dom,因此使用 setTimeout
截屏2023-08-11 12.45.57.png
再来看下效果:


1.gif
鼠标一下没动哦!你以为这就完了?并没有,接着看。


自动化打开应用


能不能自动打开xxxTrustChrome,并自动打开登录的网址?


咱们一开始就说了,在访问网址之前,还需要链接VPN、打开浏览器,那就来吧!


截屏2023-08-11 13.21.17.png

在Mac上有两个东西可以完成自动操作:


截屏2023-08-11 13.22.20.png
通过自动操作配置出来的,也支持转变成快捷指令。我理解这俩应该是差不多的东西,来看下我们如何实现:


image.png
操作步骤:

  1. 打开 xxxTrust

  2. 通过 AppleScript,设置延时6秒,因为打开上面的 app 过后,有一个自动登录的过程,我们设置的长一点

  3. 打开 Chrome

  4. 打开登录网址


配置自动化操作和快捷指令都是可视化的,很容易上手,在此不过多介绍,感兴趣的掘友可自行探索哦!至于AppleScript,我只能告诉你是 GPT 教我这么写的。


最后一步就是把这个快捷指令发布到桌面:
截屏2023-08-11 13.33.00.png


看下最终的效果:
1.gif


这不,又多了几分钟摸鱼时间!😂


总结


笔者通过真实的一个场景,借助 Snippets篡改猴快捷指令自动操作 等工具实现自动化完成进入云桌面的一系列流程。笔者只实现了 Mac 的,windows 系统肯定也有类似的工具等待各位去探索(比如 python 脚本应该就能实现打开应用等操作)。


除此之外呢,还想表达一个观点就是——我们应该把那些机械式的活交给机械去做,比如在平时的开发中,总是CRUD?能不能高效CRUD?对吧!把这些对自己能力提升没有意义的工作,想办法用程序去实现了,岂不是美滋滋!


好了,本次分享就到此结束了,感谢阅读哦!


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^


作者:Lvzl
来源:juejin.cn/post/7265744160694911028
收起阅读 »

代码中被植入了恶意删除操作,太狠了!

背景 在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。 事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。 对方...
继续阅读 »

背景


在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。


事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。


对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。


排查过程


由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。


原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。


在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。


于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。


可疑代码


在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。


于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。


但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。


紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:


删除操作


原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。


T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。


找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。


又起波折


本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。


于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:


删除脚本


而在具体的脚本中,有如下执行操作:


删除核心依赖包


这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。


为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。


小结


原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。


当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有

作者:程序新视界
来源:juejin.cn/post/7140066341469290532
的,这点不接受反驳。

收起阅读 »

为什么App独立开发最好别做日记、记账

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。 认清自己的定位 在独立开发刚开始做,羽翼未丰的时候,你对...
继续阅读 »

我在前几天发了条帖子说(新人)独立开发选择笔(日)记、记账、Todo主流三件套之一等于加速死亡。引发了一点点🤏🏻争议。刚好我在选择 app 方向的时候也深思过这个问题,所以我展开讲讲,分享一下我的想法。


认清自己的定位


在独立开发刚开始做,羽翼未丰的时候,你对自己的定位就应该是一个游击队队员。这也是独立开发的天然优势,因为小,所以灵活机动。游击战的核心奥义是流动性和速决性。因为你没有一个根据地需要防守,你可以找到一个薄弱的地方进攻。因为这个地方薄弱,所以你集中优势兵力可以很快的决胜。因为你小,所以赚到一笔是一笔。因为你是在边缘薄弱的地方建立的优势,这个小细分市场的利润不足以吸引大部队来,你就有了自己的根据地。你想想红军的根据地都是在什么地方,都是在两省交界的山里。可没听说根据地在上海、北京的。


举个例子,一个小业务,几个月工作量,可以有10万利润。对于一个互联网团队是看不上的。一个完整的互联网团队,一个CEO,一个产品,一个设计,两个研发(前端+后端),一个测试,加上行政,加上公司的运营费用。这一套组合拳摊下来,为了组织的长期利益,短期的一个20万利润的项目他们是看不上的。但是如果你是独立开发,两个人搞两个月,赚10万,你做不做?或者说你能做吗?你能做的,只是赚的不多。但是公司这个事情就没法做。这就是大象踩不死蚂蚁的道理。有人觉得就算两个人两个月赚10万,一个月人均25000也不是什么大钱。确实不是很多的钱,但这也是独立开发普遍心态上的一个问题:你想要的太多。平民独立开发早期最关键的是找到一个持续赚钱的方式,你有实力让自己活下来,后期就可以慢慢发展。而不是一上来就觉得一年要赚100万,然后以一个游击队的姿态去攻打大城市。你要赚大钱,就要有对应的能力。但是很多起步的独立开发者并没有这样的能力,那就是一个不匹配的目标了。


局部创新在笔记领域不是决定性的优势


很多人觉得我做笔记,我有一个想法,我有一个痛点,市面上的笔记没有。我进行了一个局部创新,可能是更好看一点点的设计,或者一个特别的笔记格式。我有一个局部优势,我可以做,我入场。


第一个问题:某些品类的局部优势没有决定性。换一句话说,也许你的确解决了一个其他笔记没有的痛点,但是这个不能转化成你的产品整体优势。不足以转化成足够的收入。


如果你解决了一个普遍痛点,一个高商业价值的痛点,有什么理由现有的成熟笔记app不做呢?请问你作为一个独立开发者开发这个功能要做多久?你有没可能一年里没有任何风声开发一个杀手功能,出来的时候瞬间占领用户心智,用户蜂拥而来?在一个成熟品类里,这样很难的。诺曼底登陆不是游击队能做到的。你要是有这个能力和信心,你就不应该做独立开发,你应该成立公司高举高打。真实的现状是如果你的一个功能受欢迎,成熟的笔记团队花一两个月也就做出来了。


而且笔记类还有一个特点:他有数据积累。用户价值=新体验-旧体验-替换成本。假设你的新体验确实有优势,大于现有产品的旧体验,但是笔记类的替换成本是很高的。我已经在这里积累了好几年的数据,一个新的、个人的、上线没多久的数据积累类,替换成本是很高的。我需要同时愿意抛弃我的旧数据,还需要对你建立信任。 所以除非领头的app犯错误,否则后来的小app是没有机会超过他的。因为他有先发优势,他可以在看到你之后,完善自己,提高自己的旧体验。你不能静态的认为头部的成功的app会停在那里。


成熟品类的高入局门槛


笔记类app自 AppStore 开始有以来,就有人做了。这意味着在你的 app 上线之前,高付费意愿的用户已经付费购买过了。这意味着,如果你要获得存量用户,你需要超越现有品类的成功 app,你需要有更好的整体体验,你还需要有足够全面的运营能力。让这些买了其他 app 的用户愿意选择你。你做笔记 app,一上线,真正的笔记用户心里已经有一个对标的门槛了。本来你在农村盖个楼,是个一层土楼也可以,是个木楼也行,大家都认可是个楼。你在市中心盖一个土楼,大家就会觉得你简陋,功能不全了。用户心智里已经把这个品类的成熟应用当做了一个基线。


再说新的用户。新的用户如果要用一个笔记,上 AppStore 搜,上社交平台搜,肯定看到的大量都是成熟 app 的推荐。他们本来就是历经时代活下来的,自然是得到了用户的认可。既然存在了这么久,自然知道他们的人就多。所以你在自然新增流量上也很劣势。


所以你做一个成熟品类,意味着你要成功,你就必须有一个极具说服力的产品优势,加足够好的产品质量,加足够高效的运营。


我有两个理财项目,一个项目利润年化10%,一个项目利润年后1%,你选哪个?你既然都是在做新产品,为什么要选一个更难的赛道?是你觉得自己命中注定要开发一个笔记app,还是你贫瘠的想象力只想到了做笔记?你确定是在100个产品评估出的最好的方向是笔记?


对比一个新市场,比如最近很火的套壳的 gpt 应用。这是一个全新品类,意味着用户心里是没有对标品的。当用户下载你的 app 时,他不会期待你应该有怎样的功能,他没有对比的对象。这个品类里因为没有头部应用,大家搜索的时候也就看到什么下什么。你会有自然新增流量。因为大家都是差不多时间起步的,其他app不会领先很多,意味着你也有可能建立产品优势,至少你没什么太大的劣势,更有希望建立用户口碑。


长线和短线


投资理财主要有两个流派:长线和短线。长线就关注这个企业未来长期的发展,关注企业的价值。如果这个企业是低估的,就可以持有,因为他们评估出未来这个企业会成长。短期的波动对他们并不构成干扰。大概就是大家口中说的价值投资吧。还有一类是短线,他们不关注未来的情况,买入一只股票只关心这只股票下个月会不会涨,明天会不会涨。因此这个股票的已经100倍PE对他们也没影响,后面有人接盘就可以了。量化交易大多是这样的短线逻辑,于是他们有着高换手率。


长线和短线都是合理的策略。最大的问题是,你不能用抱着长线的心态做短线。这大概就是接盘股民的心态吧,他们持有一只股票的时候觉得这个企业未来会成长。但是短期市场遇冷跌了20%,看到很多人抛了,他们就觉得受不了了,于是割肉离场。买的时候听的是做长线人的意见,卖的时候是跟着做短线的人卖的。


把这个逻辑放到 app 上也是这样的。笔记 app 是一个长线价值,越到后面越值钱,做的越久产品优势越大,用户粘性越高。但是你抱着短线的心态进来做,做了4 个月,收入用户都没起色,你怀疑自己,团队开始有意见,于是你就放弃了。这就是问题所在了,大多数独立开发者没有耐心在一个赛道持续亏损做两年,不具备做长线的心态和财力。


所以我建议独立开发起步的时候多关注短线价值。就是你投入三五个月,能有起色的方向。做三五个月,要不就要赚钱,要不就要有用户口碑。一鸟在手胜过二鸟在林。等你解决了起步的时候生存问题,再考虑农村包围城市的问题。


谈谈番茄钟


也有人觉得番茄钟是独立开发的重灾区。虽然番茄钟似乎是一个红海了,但是我却觉得番茄钟反而是一个可以做的赛道。不知道大家有没有留意一下这个有这个功能的app,似乎万物都可以番茄钟。谜底时钟有番茄钟,滴答清单有番茄钟。番茄钟在设计上可以极简,可以是我的番茄,可以是小鸡,可以是面条,可以是像素。


image.png
IMG\_5978.png
IMG\_5979.png
IMG\_5980.png

那么为什么番茄钟可以呢?


番茄钟替换成本低。因为是及时性的工具类,历史积累的番茄时间统计并不太重要。在功能操作上也很简单,核心交互就是点击一下开始计时,25分钟后提醒你要休息5分钟。很容易可以完成一个基础任何。


设计差异化可以成为付费点,市场容量大。 因为交互很简单,所以做出突出的设计成本可以接受。因为这个品类里,设计可以成为卖点,设计又是一个各有所好的事情,因此天然会有很多不同的需求。某个番茄钟可能做的很好用,很好看,但是他不可能吃掉全部用户。一件短袖设计的再好看,也不可能让所有人都买单。


由此我们可以得出一个结论:番茄钟有短线价值。并且番茄钟引入养成和成就体系以后,有可能变成一个长线产品。如果你非要卷,去卷番茄钟吧。


最后


地上有两张钱,一张100,一张10块,你先捡哪张?我想结果是不言而喻的。独立开发者的一个生态位优势就是可以在一个小领域建立极高的人效比。这个小领域有两个可能:一个是这个领域太小太细分,只能容得下小团队做(高人效);这个领域是新的,还没人知道这里行不行,于是小成本的独立开发先做了(高灵活)。建议各位独立开发者如果要做死亡加速三件套的产品的话三思而后行。


作者:没故事的卓同学
来源:juejin.cn/post/7265967971162898487
收起阅读 »

万能的异步处理方案

异步处理通用方案 前言 良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素...
继续阅读 »

异步处理通用方案


前言


良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素。 由此我设计了一套通用的异步处理SDK,可以很轻松的实现各种异步处理


目的


通过异步处理不仅能够保证方法能够得到有效的执行而且不影响主流程


更重要的是各种兜底方法保证数据不丢失,从而达到最终一致性\color{red}最终一致性


优点


无侵入设计,独立数据库,独立定时任务,独立消息队列,独立人工执行界面(统一登录认证)


使用spring事务事件机制,即使异步策略解析失败也不会影响业务


如果你的方法正在运行事务,会等事务提交后再处理事件


就算事务提交了,异步策略解析失败了,我们还有兜底方案执行(除非数据库有问题,消息队列有问题,方法有bug)


组件


kafka 消息队列


xxl job 定时任务


mysql 数据库


spring 切面


vue 界面


设计模式


策略


模板方法


动态代理


流程图


image.png


数据库脚本


CREATE TABLE `async_req` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`application_name` varchar(100) NOT NULL DEFAULT '' COMMENT '应用名称',
`sign` varchar(50) NOT NULL DEFAULT '' COMMENT '方法签名',
`class_name` varchar(200) NOT NULL DEFAULT '' COMMENT '全路径类名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名称',
`async_type` varchar(50) NOT NULL DEFAULT '' COMMENT '异步策略类型',
`exec_status` tinyint NOT NULL DEFAULT '0' COMMENT '执行状态 0:初始化 1:执行失败 2:执行成功',
`exec_count` int NOT NULL DEFAULT '0' COMMENT '执行次数',
`param_json` longtext COMMENT '请求参数',
`remark` varchar(200) NOT NULL DEFAULT '' COMMENT '业务描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_applocation_name` (`application_name`) USING BTREE,
KEY `idx_exec_status` (`exec_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理请求';

CREATE TABLE `async_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`async_id` bigint NOT NULL DEFAULT '0' COMMENT '异步请求ID',
`error_data` longtext COMMENT '执行错误信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_async_id` (`async_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理日志';

异步策略


image.png


安全级别


image.png


执行状态


image.png


流程图


image.png


image.png
image.png

apollo 配置


# 开关:默认关闭
scm.async.enabled=true

# 数据源 druid
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/fc_async?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
spring.datasource.username=user
spring.datasource.password=xxxx
spring.datasource.filters=config
spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=yyy
#静态地址
spring.resources.add-mappings=true
spring.resources.static-locations=classpath:/static/


# 以下配置都有默认值
# 核心线程数
async.executor.thread.corePoolSize=10
# 最大线程数
async.executor.thread.maxPoolSize=50
# 队列容量
async.executor.thread.queueCapacity=10000
# 活跃时间
async.executor.thread.keepAliveSeconds=600

# 执行成功是否删除记录:默认删除
scm.async.exec.deleted=true

# 自定义队列名称前缀:默认应用名称
scm.async.topic=应用名称

# 重试执行次数:默认5次
scm.async.exec.count=5

# 重试最大查询数量
scm.async.retry.limit=100

# 补偿最大查询数量
scm.async.comp.limit=100

用法


1,异步开关
scm.async.enabled=true

2,在需要异步执行的方法加注解 (必须是spring代理方法)
@AsyncExec(type = AsyncExecEnum.SAVE_ASYNC, remark = "数据字典")

3,人工处理地址
http://localhost:8004/async/index.html

注意


1,应用名称
spring.application.name

2,队列名称
${scm.async.topic:${spring.application.name}}_async_queue
自定义topic:scm.async.topic=xxx

3,自己业务要做幂等

4,一个应用公用一个队列
自产自消

5,定时任务
异步重试定时任务(2分钟重试一次,可配置重试次数)
异步补偿定时任务(一小时补偿一次,创建时间在一小时之前的)

效果展示


image.png


image.png


作者:三火哥
来源:juejin.cn/post/7266087843239084090
收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


就着这事,聊聊最近对职场上关于沟通的一些思考:


第一,忌固执己见


职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


第二,不必说服,尊重就好


站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


第三,不懂的领域多听少说


如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


第四,没事多夸夸别人


在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。

作者:程序新视界
来源:juejin.cn/post/7265978883123298363
很有意思,也很有用。

收起阅读 »

AI欣赏-街头少女🔥🔥🔥

描述 💯💯💯 你更喜欢哪一位选手呢? 自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言! 本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。Lo...
继续阅读 »

描述


💯💯💯 你更喜欢哪一位选手呢?


自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言!


本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。

  • Lora:无

  • Embeddings:ng_deepnegative_v1_75t [1a3e]


Prompt



a young woman, street, laughing, ponytails, (hdr:1.3), (muted colors:1.2), dramatic, complex background, cinematic, filmic, (rutkowski, artstation:0.8), soaking wet,



Negative Prompt



(nsfw:2),Multiple people,easynegative,(worst quality:2),(low quality:2),lowres,(monochrome:1.4),(grayscale:1.4),big head,severed legs,short legs,missing legs,acnes,skin blemishes,age spot,backlight,(ugly:1.4),(duplicate:1.4),(morbid:1.2),(mutilated:1.2),mutated hands,(poorly drawn hands:1.4),blurry, (bad anatomy:1.4),(bad proportions:1.4),(disfigured:1.4),(unclear eyes:1.4),bad hands, bad tooth,missing fingers,extra digit,bad body,NG_DeepNegative_V1_75T,glans,EasyNegative:0.5,gross proportions.short arm,(missing arms:1.4),missing thighs,missing calf,mutation,duplicate,morbid,mutilated,poorly drawn cloth,strange finger,bad finger,(mutated hands and fingers:1.4),(text:1.4), bad-artist, bad_prompt_version2, bad-hands-5, bad-image-v2-39000,



基础配置




生成图的效果展示


选手1




选手2




选手3




选手4




选手5




选手6




选手7




选手8




选手9




投票


🌺 请开始诸位的投票吧!评论区见!!


作者:襄垣
链接:https://juejin.cn/post/7223267912727298103
来源:稀土掘金
收起阅读 »

iPhone两秒出图,目前已知的最快移动端Stable Diffusion模型来了

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7...
继续阅读 »

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7GB,近 10 亿参数,端上推理时间往往要接近 2min。


为了解决推理速度问题,学术界与业界已经开始对 SD 加速的研究,主要集中于两条路线:(1)减少推理步数,这条路线又可以分为两条子路线,一是通过提出更好的 noise scheduler 来减少步数,代表作是 DDIM [1],PNDM [2],DPM [3] 等;二是通过渐进式蒸馏(Progressive Distillation)来减少步数,代表作是 Progressive Distillation [4] 和 w-conditioning [5] 等。(2)工程技巧优化,代表作是 Qualcomm 通过 int8 量化 + 全栈式优化实现 SD-v1.5 在安卓手机上 15s 出图 [6],Google 通过端上 GPU 优化将 SD-v1.4 在三星手机上加速到 12s [7]。


尽管这些工作取得了长足的进步,但仍然不够快。


近日,Snap 研究院推出最新高性能 Stable Diffusion 模型,通过对网络结构、训练流程、损失函数全方位进行优化,在 iPhone 14 Pro 上实现 2 秒出图(512x512),且比 SD-v1.5 取得更好的 CLIP score。这是目前已知最快的端上 Stable Diffusion 模型!



论文地址:arxiv.org/pdf/2306.00…
Webpage: snap-research.github.io/SnapFusion


核心方法


Stable Diffusion 模型分为三部分:VAE encoder/decoder, text encoder, UNet,其中 UNet 无论是参数量还是计算量,都占绝对的大头,因此 SnapFusion 主要是对 UNet 进行优化。具体分为两部分:(1)UNet 结构上的优化:通过分析原有 UNet 的速度瓶颈,本文提出一套 UNet 结构自动评估、进化流程,得到了更为高效的 UNet 结构(称为 Efficient UNet)。(2)推理步数上的优化:众所周知,扩散模型在推理时是一个迭代的去噪过程,迭代的步数越多,生成图片的质量越高,但时间代价也随着迭代步数线性增加。为了减少步数并维持图片质量,我们提出一种 CFG-aware 蒸馏损失函数,在训练过程中显式考虑 CFG (Classifier-Free Guidance)的作用,这一损失函数被证明是提升 CLIP score 的关键!


下表是 SD-v1.5 与 SnapFusion 模型的概况对比,可见速度提升来源于 UNet 和 VAE decoder 两个部分,UNet 部分是大头。UNet 部分的改进有两方面,一是单次 latency 下降(1700ms -> 230ms,7.4x 加速),这是通过提出的 Efficient UNet 结构得到的;二是 Inference steps 降低(50 -> 8,6.25x 加速),这是通过提出的 CFG-aware Distillation 得到的。VAE decoder 的加速是通过结构化剪枝实现。




下面着重介绍 Efficient UNet 的设计和 CFG-aware Distillation 损失函数的设计。


(1)Efficient UNet


我们通过分析 UNet 中的 Cross-Attention 和 ResNet 模块,定位速度瓶颈在于 Cross-Attention 模块(尤其是第一个 Downsample 阶段的 Cross-Attention),如下图所示。这个问题的根源是因为 attention 模块的复杂度跟特征图的 spatial size 成平方关系,在第一个 Downsample 阶段,特征图的 spatial size 仍然较大,导致计算复杂度高。




为了优化 UNet 结构,我们提出一套 UNet 结构自动评估、进化流程:先对 UNet 进行鲁棒性训练(Robust Training),在训练中随机 drop 一些模块,以此来测试出每个模块对性能的真实影响,从而构建一个 “对 CLIP score 的影响 vs. latency” 的查找表;然后根据该查找表,优先去除对 CLIP score 影响不大同时又很耗时的模块。这一套流程是在线自动进行,完成之后,我们就得到了一个全新的 UNet 结构,称为 Efficient UNet。相比原版 UNet,实现 7.4x 加速且性能不降。


(2)CFG-aware Step Distillation


CFG(Classifier-Free Guidance)是 SD 推理阶段的必备技巧,可以大幅提升图片质量,非常关键!尽管已有工作对扩散模型进行步数蒸馏(Step Distillation)来加速 [4],但是它们没有在蒸馏训练中把 CFG 纳入优化目标,也就是说,蒸馏损失函数并不知道后面会用到 CFG。这一点根据我们的观察,在步数少的时候会严重影响 CLIP score。


为了解决这个问题,我们提出在计算蒸馏损失函数之前,先让 teacher 和 student 模型都进行 CFG,这样损失函数是在经过 CFG 之后的特征上计算,从而显式地考虑了不同 CFG scale 的影响。实验中我们发现,完全使用 CFG-aware Distillation 尽管可以提高 CLIP score, 但 FID 也明显变差。我们进而提出了一个随机采样方案来混合原来的 Step Distillation 损失函数和 CFG-aware Distillation 损失函数,实现了二者的优势共存,既显著提高了 CLIP score,同时 FID 也没有变差。这一步骤,实现进一步推理阶段加速 6.25 倍,实现总加速约 46 倍。


除了以上两个主要贡献,文中还有对 VAE decoder 的剪枝加速以及蒸馏流程上的精心设计,具体内容请参考论文。


实验结果


SnapFusion 对标 SD-v1.5 text to image 功能,目标是实现推理时间大幅缩减并维持图像质量不降,最能说明这一点的是下图:




该图是在 MS COCO’14 验证集上随机选取 30K caption-image pairs 测算 CLIP score 和 FID。CLIP score 衡量图片与文本的语义吻合程度,越大越好;FID 衡量生成图片与真实图片之间的分布距离(一般被认为是生成图片多样性的度量),越小越好。图中不同的点是使用不同的 CFG scale 获得,每一个 CFG scale 对应一个数据点。从图中可见,我们的方法(红线)可以达到跟 SD-v1.5(蓝线)同样的最低 FID,同时,我们方法的 CLIP score 更好。值得注意的是,SD-v1.5 需要 1.4min 生成一张图片,而 SnapFusion 仅需要 1.84s,这也是目前我们已知最快的移动端 Stable Diffusion 模型!


下面是一些 SnapFusion 生成的样本:




更多样本请参考文章附录。


除了这些主要结果,文中也展示了众多烧蚀分析(Ablation Study)实验,希望能为高效 SD 模型的研发提供参考经验:


(1)之前 Step Distillation 的工作通常采用渐进式方案 [4, 5],但我们发现,在 SD 模型上渐进式蒸馏并没有比直接蒸馏更有优势,且过程繁琐,因此我们在文中采用的是直接蒸馏方案。




(2)CFG 虽然可以大幅提升图像质量,但代价是推理成本翻倍。今年 CVPR’23 Award Candidate 的 On Distillation 一文 [5] 提出 w-conditioning,将 CFG 参数作为 UNet 的输入进行蒸馏(得到的模型叫做 w-conditioned UNet),从而在推理时省却 CFG 这一步,实现推理成本减半。但是我们发现,这样做其实会造成图片质量下降,CLIP score 降低(如下图中,四条 w-conditioned 线 CLIP score 均未超过 0.30, 劣于 SD-v1.5)。而我们的方法则可以减少步数,同时将 CLIP score 提高,得益于所提出的 CFG-aware 蒸馏损失函数!尤其值得主要的是,下图中绿线(w-conditioned, 16 steps)与橙线(Ours,8 steps)的推理代价是一样的,但明显橙线更优,说明我们的技术路线比 w-conditioning [5] 在蒸馏 CFG guided SD 模型上更为有效。




(3)既有 Step Distillation 的工作 [4, 5] 没有将原有的损失函数和蒸馏损失函数加在一起,熟悉图像分类知识蒸馏的朋友应该知道,这种设计直觉上来说是欠优的。于是我们提出把原有的损失函数加入到训练中,如下图所示,确实有效(小幅降低 FID)。




总结与未来工作


本文提出 SnapFusion,一种移动端高性能 Stable Diffusion 模型。SnapFusion 有两点核心贡献:(1)通过对现有 UNet 的逐层分析,定位速度瓶颈,提出一种新的高效 UNet 结构(Efficient UNet),可以等效替换原 Stable Diffusion 中的 UNet,实现 7.4x 加速;(2)对推理阶段的迭代步数进行优化,提出一种全新的步数蒸馏方案(CFG-aware Step Distillation),减少步数的同时可显著提升 CLIP score,实现 6.25x 加速。总体来说,SnapFusion 在 iPhone 14 Pro 上实现 2 秒内出图,这是目前已知最快的移动端 Stable Diffusion 模型。


未来工作:


1.SD 模型在多种图像生成场景中都可以使用,本文囿于时间,目前只关注了 text to image 这个核心任务,后期将跟进其他任务(如 inpainting,ControlNet 等等)。




  1. 本文主要关注速度上的提升,并未对模型存储进行优化。我们相信所提出的 Efficient UNet 仍然具备压缩的空间,结合其他的高性能优化方法(如剪枝,量化),有望缩小存储,并将时间降低到 1 秒以内,离端上实时 SD 更进一步。




参考文献


[1] Denoising Diffusion Implicit Models, ICLR’21


[2] Pseudo Numerical Methods for Diffusion Models on Manifolds, ICLR’22


[3] DPM-Solver: A Fast ODE Solver for Diffusion Probabilistic Model Sampling in Around 10 Steps, NeurIPS’22


[4] Progressive Distillation for Fast Sampling of Diffusion Models, ICLR’22


[5] On Distillation of Guided Diffusion Models, CVPR’23


[6] http://www.qualcomm.com/news/onq/20…


[7] Speed Is All You Need: On-Device Acceleration of Large Diffusion Models via GPU-Aware Optimizations, CVPR’23 Workshop


作者:机器之心
链接:https://juejin.cn/post/7244452476191850557
来源:稀土掘金
收起阅读 »

使用脚本更新 macOS 壁纸,让你每天看到不同的美景🖼️

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。 步骤1:获取unsplash API密钥 首先,你需要注册一个unspla...
继续阅读 »

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。


步骤1:获取unsplash API密钥


首先,你需要注册一个unsplash账户,并申请一个API密钥。这个API密钥将允许你通过编程方式访问unsplash图片库。


步骤2:编写bash脚本


创建一个新的文本文件,然后在其中添加以下代码:

#!/bin/bash

# set the unsplash API access key
access_key="YOUR_UNSPLASH_API_ACCESS_KEY"

# define the query to search for wallpaper images
query="nature"

# search for a random wallpaper image
result=$(/usr/bin/curl -s -H "Authorization: Client-ID $access_key" "https://api.unsplash.com/photos/random?query=$query")

# extract the image URL from the JSON response
image_url=$(echo "$result" | /opt/homebrew/bin/jq -r '.urls.full')

# download the image
/usr/bin/curl -s "$image_url" > ~/Pictures/wallpaper.jpg

# set the image as the desktop wallpaper
osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"$HOME/Pictures/wallpaper.jpg\""

这段代码会使用unsplash API搜索与“nature”相关的随机图片,并将其下载到“~/Pictures/wallpaper.jpg”文件中。然后,它会使用AppleScript将下载的图片设置为桌面壁纸。


步骤3:运行bash脚本


将文件保存为“update-wallpaper.sh”,然后打开终端并导航到该文件所在的目录。运行以下命令以使脚本可执行:

chmod +x update-wallpaper.sh

现在,你可以通过在终端中输入以下命令来运行脚本:

./update-wallpaper.sh

步骤4:设置定时任务

脚本依赖:curl、jq、bash、unsplash,使用 which 获取路径,然后替换脚本里的curl和jq。

which curl
which jq

你可以将该脚本设置为定时任务,以便每天自动更新壁纸。打开“终端”并输入以下命令以编辑cron定时任务:

crontab -e

然后,添加以下行:

0 9 * * * /path/to/update-wallpaper.sh

这将在每天上午9点运行该脚本。

现在,你可以坐下来,放松一下,让你的macOS自动更新壁纸。享受吧!

作者:FreeCultureBoy
链接:https://juejin.cn/post/7226301946839089211
来源:稀土掘金

收起阅读 »

完全免费白嫖 GPT-4 的终极方案!

GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。 大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 ...
继续阅读 »


GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。


大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 25 条消息。。。


要么就去 OpenAI 官网申请 GPT-4 的 API,但是目前申请到 API 的小伙伴非常少,你以为申请到 API 就可以用了吗?GPT-4 的 API 价格超级无敌贵,是 GPT-3.5 价格的 30 倍,你敢用吗?😄


然而,但是,既然我写了这篇文章,肯定是要告诉那一个惊天大幂幂的!


现在完全免费白嫖 GPT-4 的机会来了,不仅可以白嫖,还可以直接作为 API 来调用!


不仅能够作为 API 调用,我还接入了公众号给大家白嫖,你说气人不气人?



下面言归正传,开始手把手教大家如何免费白嫖 GPT-4


gpt4free-ts 介绍


GPT4Free 大家应该都知道吧?它上线几周就在 GitHub 上揽收了接近 4w 的 Star。原因就在于其提供了对 GPT-4 及 GPT-3.5 免费且几乎无限制的访问。该项目通过对各种调用了 OpenAI API 网站的第三方 API 进行逆向工程,达到使任何人都可以免费访问该流行 AI 模型的目的。


这就相当于什么?假设地主家有一个粮仓,你往他家的粮仓偷偷插了一根管子,不停地向外抽米,他全然不知,所以你也不用交钱,一切费用由地主承担


现在接入 GPT-4 的第三方网站就相当于那个地主,懂了吧?


但是这个项目并没有封装 API,而且目前也不太能用了。


作为开发者,我们想要的肯定是 API 啊!这就要提到今天的主角了:gpt4free-ts


这个项目是用 TypeScript 写的,相当于 GPT4Free 的 TypeScript 版本,但是更方便部署,而且封装了 API,简直就是开发者的福音,就他了!


这个项目向多个地主家的粮仓插了管子,其中最强大的地主就是 forefront.ai,这个地主家的粮仓里就包含了 GPT-4 这个香饽饽,而且还有 Claude,就嫖他了!


除了 forefront 之外,它接的粮仓还挺多的。。



大批量注册临时邮箱


forefront 的 GPT-4 模型是有限制的,每个账号每 3 小时内只能发送 5 条消息


所以接下来需要用到一个非常神奇的服务叫 RapidAPI你可以通过这个 API 来获取无穷无尽的临时邮箱,然后再用这些无穷无尽的临时邮箱去注册无穷无尽的 forefront 账号。


这么一说,你是不是就悟了?哈哈哈


首先你需要在这里注册一个账号并登录:rapidapi.com/calvinlovel…


然后需要在 Pricing 页面开启订阅:



一般情况下订阅免费套餐即可,一天可以调用 100 次。


如果你有更高的需求,可以考虑订阅更高级的套餐(比如你的用户数量特别多)。


订阅完了之后,你就能看到 API Key 了。这个 Key 我们后面会用到。



Sealos 云操作系统介绍


单机操作系统大家应该都知道吧?Windows、macOS、Linux 这些都属于单机操作系统,为什么叫单机操作系统呢?因为他的内存啊,CPU 啊,都在一台机器上,你不可能用其他机器的内存和 CPU。


那么什么是云操作系统呢?就是把一群机器的 CPU 和内存看成一个整体,然后给用户提供一个交互界面,用户可以通过这个交互界面来操作所有的资源。


懂 K8s 的玩家可能要说了:这个我懂,K8s 就可以!


如果我们的目标愿景是一个云操作系统,K8s 充其量只能是这个云操作系统的内核,就像 Linux 内核一样。完整的云操作系统需要一个像 Windows 和 Ubuntu 操作系统那样的交互界面,也就是操作系统发行版


对于云操作系统来说,Sealos 就是那个发行版。



链接:cloud.sealos.io




有人可能会把云操作系统理解成“Web 界面”,但其实不是,Sealos 云操作系统完全是类似于 Windows 和 macOS 桌面的那种逻辑,并不是 Web 界面。我只需要点几下鼠标,一个应用就装好了,老夫并不知道什么容器什么 K8s。


数据库也一样,小鼠标一点,一个分布式数据库就装好了。


我知道,这时候云原生玩家要坐不住了,您别着急,看到桌面上的终端了没?



终端只是这个云操作系统中的一个 App 而已。同理,容器管理界面仍然可以作为云操作系统的 App,我管你是 Kubernetes Dashboard、Rancher、KubeSphere 还是 Kuboard,都可以作为 App 装在这个云操作系统中。这时候对于云原生专家而言,仍然可以命令行咔咔秀操作,也可以通过各种管理界面来管理容器。


云操作系统嘛,就是要什么人都能用才行,不管你是什么角色,都能在这个操作系统里找到你想要的 App 去完成你的使命


安装 gpt4free-ts


接下来才是这篇文章的重头戏。


我要教大家如何在 Sealos 中点几下鼠标就能安装一个 gpt4free-ts 集群


没错,就是 gpt4free-ts 集群。


什么叫集群?就是说我要运行一群 gpt4free-ts 实例,然后前面加一个负载均衡作为对外的 API 入口。


下面的步骤非常简单,楼下的老奶奶都会,是真的,当时我就在楼下看她操作


首先进入 Sealos 云操作系统的界面:**cloud.sealos.io**。


然后打开桌面上的应用管理 App:



点击「新建应用」:



在启动参数中,按照以下方式进行设置:

1.应用名称随便写,比如 gpt4free。
2.镜像名称是:xiangsx/gpt4free-ts:latest
CPU 和内存需要根据应用的实际情况来填写。这个应用运行之后默认会启动两个 Chrome 浏览器来模拟登录 forefront,每次对话会从里面取一个账号来使用,次数用完了会自动注册新账号(因为每个账号每 3 小时只能发送 5 条信息)。我们可以通过环境变量来修改启动的浏览器数量,所以需要根据你的浏览器数量来确定 CPU 和内存。 我自己把浏览器数量设置为 3,所以需要的内存和 CPU 比较多(后面会告诉你怎么设置环境变量)。
3.实例数根据自己的实际需求填写,我需要接入公众号,粉丝比较多,一个实例才 3 个账号(因为我一个实例跑了 3 个浏览器),根本不够用,所以我开了 3 个实例。
4.容器暴露端口指定为 3000。
5.打开外网访问。



继续往下,展开高级设置,点击「编辑环境变量」:



填入以下环境变量:

rapid_api_key=<rapid_api_key>
DEBUG=0
POOL_SIZE=3


⚠️注意:请将 <rapid_api_key> 替换为你自己的 key。



其中 POOL_SIZE 就是浏览器数量,每个浏览器会登录一个 forefront 账号。你可以根据自己的需要调整浏览器数量,并根据浏览器数量调整 CPU 和内存。如果你不知道怎么调整合适,建议无脑跟着本文操作。



继续,点击「新增存储卷」:



容量只需 1G,挂载路径设置为 /usr/src/app/run




这个存储的作用是为了保存已登录的账号。已经注册的账号 3 个小时以后还可以重新使用,不用再浪费邮箱去注册新账号。



最终点击右上角的「部署应用」,即可完成部署:



最终要等待所有的实例都处于 Running 状态,才算是启动成功了。



点击右边的复制按钮,便可复制 API 的外网地址:



我们来测一下这个 API:



完美!打完收工!


接入微信公众号


什么?你想将这个 API 接入自己的公众号?换个形式吧!直接来看直播吧,我们会通过直播活动手把手教你如何将 GPT-4 免费接入公众号、网页等各种前端。


活动链接:forum.laf.run/d/684


当然,直播过程不会直接教你如何接入公众号,而是“授你🫵以渔”,告诉你如何使用 Laf 来通过各种姿势调用这个 API,最终你也接入公众号也罢,网页前端也罢,那都不是事儿~

作者:米开朗基杨
链接:https://juejin.cn/post/7241790368949190693
来源:稀土掘金
收起阅读 »

雷军写的代码上热搜了!

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。 出于好奇,第一时间点进去围观了一波。 原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。 这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是...
继续阅读 »

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。




出于好奇,第一时间点进去围观了一波。



原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。



这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是!



看一下代码细节,都是类似MOVJMPPUSHLOOP等这些指令。


这不就是自己当年上学时学得瑟瑟发抖痛哭涕零的汇编语言嘛。。



这一瞬间就让我想起了当年微博上的这张图:



早在十几年前,微博上就曾流传过雷军早年所写的一段完整的汇编代码。



当时雷军也转发过,并表示这个程序的第一个版本是他1989年写的,怀念当初写程序的快乐时光。



在网上经常会看到关于雷军代码水平到底如何的讨论帖子。


可以说,看看雷军早年写的这段代码相信心里基本就有答案了。


在早年那个机器硬件水平和性能都十分受限的年代,为了满足某些需求,可以说对编码的程序员提出了非常苛刻的要求。


开发者往往只能使用非常底层的编程语言去实现某些程序,同时还需要把代码优化做到极致,这本就非常考验程序员的基本功和编程底子。


各方面信息都显示,作为一个程序员来说,雷军不仅仅是合格,说他是非常厉害的大神那也丝毫没有毛病。


就像这次热搜话题下稚晖君大佬的一篇动态所言,雷军作为老一代程序员的代表和创业楷模,确实值得敬佩。



文章的最后,我们也找到了当年雷军所写的这段汇编代码的完整版,这里也分享给大家。


咳咳,前方高能!!!


作者:CodeSheep
链接:https://juejin.cn/post/7265679390242537512
来源:稀土掘金
收起阅读 »

吐槽大会,来瞧瞧资深老前端写的垃圾代码

忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了...
继续阅读 »

忍无可忍,不吐不快。


本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


知道了什么是烂代码,才能写出好代码。


别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。

优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


---------------------------------------------更新------------------------------------------------


集中回答一下评论区的问题:


1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


文件命名千奇百怪


同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。




组件职责不清


还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?




条件渲染逻辑置于底层


这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。




滥用、乱用 TS


项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。




留下大量无用注释代码和报错代码


感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。






丑陋的、隐患的、无效的、混乱的 css


丑陋的:没有空格,没有换行,没有缩进


隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合




一个文件 6 个槽点


槽点1:代码的空行格式混乱,影响代码阅读


槽点2:空函数,写了函数名不写函数体,但是还调用了!


槽点3:函数参数过多不优化


槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名




变态的链式取值和赋值


都懒得说了,各位观众自己看吧。




代码拆分不合理或者不拆分导致代码行数超标


能写出这么多行数的代码的绝对是人才。


尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。




这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。




杂七杂八的无用 js、md、txt 文件


在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


实在受不了干脆建个文件夹放一块,看起来也要舒服多了。




less、scss 混用


这是最奇葩的。




特殊变量重命名


这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。

const G = window;
const doc = G.document;

混乱的 import


规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。




写在最后


就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


不过说这么多,成事在人。


不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

作者:北岛贰
链接:https://juejin.cn/post/7265505732158472249
来源:稀土掘金
收起阅读 »

接口测试神器:ApiKit

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具1.背景 作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确...
继续阅读 »

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具

1.背景


作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确实非常出色。


但是在整个软件开发过程中,接口调试只是其中的一部分,还有很多事情 Postman 是无法完成的,或者无法高效完成,比如:接口文档定义、Mock 数据、接口自动化测试等等。


今天给大家推荐的一款神器: ApiKit=API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作

1.1聊一聊接口管理的现状


对于接口管理的现状来说,目前行业大部分采取的解决方案有如下几种:

1.使用 **Swagger**管理接口文档。 

2. 使用 Postman 调试接口。

3.使用 **RAP或Easy Mock**来进行 **Mock**数据。 

4. 使用 JMeter 做接口自动化测试。


而上述的接口管理手段,咋一看,貌似没有什么问题,但仔细分析,不难发现,当中存在的问题还真不少,比如要维护不同工具,并且这些工具之间数据一致性非常困难、非常低效。这里不仅仅是工作量的问题,更大的问题是多个系统之间数据不一致,导致协作低效,频繁出问题,开发人员、测试人员工作起来也痛苦不堪。


设想一下这样的一个协作流程(官方示例):



1. 开发人员在Swagger定义好文档后,接口调试的时候还需要去 Postman 再定义一遍。 2. 前端开发Mock 数据的时候又要去RAPEasy Mock定义一遍,手动设置好 Mock 规则。 3. 测试人员需要去 JMeter定义一遍。 4. 前端根据 RAPEasy Mock定义 Mock 出来的数据开发完,后端根据 Swagger定义的接口文档开发完,各自测试测试通过了,本以为可以马上上线,结果一对接发现各种问题:原来开发过程中接口变更,只修改了 Swagger,但是没有及时同步修改 RAPEasy Mock。 5. 同样,测试在 JMeter 写好的测试用例,真正运行的时候也会发现各种不一致。 6. 时间久了,各种不一致会越来越严重。

ApiKit介绍

官方对ApiKit定位是,API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作 

结合 API 设计、文档管理、自动化测试、监控、研发管理和团队协作的一站式 API 生产平台,从个人开发者到跨国企业用户,Apikit 帮助全球超过50万开发者和10万家企业更快、更好且更安全地开发和使用 API 

概括来讲,ApiKit 常用功能分为五类: 

1.智能且强大的 Mock 前端团队可以在 API 还没开发完成的情况下,借助 Mock API 实现预对接,加速开发进程。测试团队可以通过 Mock API 解耦不必要的系统,完成集成测试 

2.快速生成和管理所有 API 文档 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

3. 零代码自动化测试 Apikit 提供了 API 测试功能,支持自动生成测试数据,能够通过Javascript 对请求报文、返回结果等进行加解密、签名等处理;提供强大、易用的企业级 API 自动化测试解决方案,5分钟快速上手,提高 95% 以上回归测试效率,人人皆可使用的“零代码”自动化测试平台; 

4. 领先的 API 团队协作功能 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

5.还有更多的 Devops 功能 API 异常监控,对接CI/CD、DevOps 平台,支持主流IM ,也可通过自由拓展。 

ApiKit 小试牛刀 

接下来,带着大家,简单体验一下ApiKit的使用。

Apikit 有三种客户端,你可以依据自己的情况选择。三种客户端的数据是共用的,因此你可以随时切换不同的客户端。

我们推荐使用新推出的 Apikit PC 客户端,PC端拥有线上产品所有的功能,并且针对本地测试、自动化测试以及使用体验等方面进行了强化,可以提供最佳的使用感受。 



我们建议对本地测试有需求的用户使用PC端,可满足更多本地测试需求。



发起 API 测试


进入 API 文档详情页,点击上方 测试 标签,进入 API 测试页,系统会根据API文档自动生成测试界面并且填充测试数据。




填写请求参数


首先填写好请求参数。



请求头部


您可以输入或导入请求头部。批量导入的数据格式为 key : value ,一行一条header信息,如:

Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json
Date: Mon, 30 Dec 2019 20:49:45 GMT

请****求体


请求体提供了五种类型:

  1. Form-data(表单)

  2. JSON

  3. XML

  4. Raw(自定义文本类型数据)

  5. Binary(字节流、文件参数)


产品中提供了的 JSON 和 XML 编辑器,当您已经在 API 文档中定义好 API 的请求数据结构时,只需要在测试界面填写各个字段的值,系统会自动转换为相应的 JSON 和 XML 结构的请求数据。


Query 参数


Query参数指的是地址栏中跟在问号?后面的参数,如以下地址中的 user_name 参数:

/user/login?user_name=jackliu

批量导入的数据格式为 ?key=value ,通过&分隔多个参数,如:

api.eolinker.com/user/login?user_name=jackliu&user_password=hello

REST参数

REST参数指的是地址栏被斜杠/分隔的参数,如以下地址中的user_name、user_password参数。

/user/login/{user_name}/{user_password}

注意,只需要在URL中使用 {} 将REST参数括起来,下方的请求参数名中不需要使用 {} 。


处理脚本


脚本分为 前置脚本后置脚本 两种,分别对应 API 请求前 和 返回数据后 的两个阶段。您可以通过编写 Javascript 代码,在 API 前置脚本中改变请求参数,或者是在 API 后置脚本中改变返回结果。


脚本常用于以下几种情况:

1.API 请求前对请求参数进行复制、加解密等操作,比如进行Body进行整体签名 

2. API 返回结果后对结果进行解密等


发起的API请求会依次经过以下流程。其中如果您没有编写相应的API脚本,则会略过API脚本处理阶段。



管****理 Cookie


当您测试需要 Cookie 的 API 时,可以先进行一次 API 登录或者在 Cookie 管理里添加所需的 Cookie 信息,系统会自动将 Cookie 储存起来,下次测试其他相同域名的 API 时会自动传递 Cookie 请求参数。




查看测试结果


填写好请求参数后,点击测试按钮即可得到测试报告,报告包括以下内容:

1.返回头部 

2. 返回内容 

3.实际请求头部 

4. 实际请求内容 

5.请求时间分析




快速生成mock


在高级mock页面,选择添加为mock,可快速生成mock。



将测试用例请求参数和返回参数自动带到mock的请求报文和响应报文中。



ApiKit 更多特性


新建 API 文档



团队协作,API分享



高级mock



创建自动化测试



API 异常警告



环境管理



前后置脚本



创建项目



APIHub




关于 ApiKit 的更多功能,值得你来尝试体验!


传送门:


http://www.eolink.com/?utm_source…


小结


虽然 ApiKit 目前有些功能还并不完善,但整的来说,ApiKit 还是不错的,也为接口开发调试测试提供了一种效率更佳的的解决方案。

作者:CV_coder
链接:https://juejin.cn/post/7237024604962766909
来源:稀土掘金
收起阅读 »

新时代,你需要了解一下苹果的 VisionOS 系统

iOS
这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。 沉浸的光谱。 Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在...
继续阅读 »

这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。


沉浸的光谱。


Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在与周围环境保持联系的同时与您的应用互动,或者完全沉浸在您创造的世界中。您的体验可以是流畅的:从一个窗口开始,引入 3D 内容,过渡到完全身临其境的场景,然后马上回来。


选择权在您手中,这一切都始于 visionOS 上的空间计算构建块。




窗口(Windows)


您可以在 visionOS 应用程序中创建一个或多个窗口。它们使用 SwiftUI 构建,包含传统视图和控件,您可以通过添加 3D 内容来增加体验的深度。


体积(Volumes)


使用 3D 体积为您的应用添加深度。 Volumes 是一种 SwiftUI 场景,可以使用 RealityKit 或 Unity 展示 3D 内容,从而创建可从共享空间或应用程序的完整空间中的任何角度观看的体验。


空间(Space)


默认情况下,应用程序启动到共享空间,在那里它们并排存在——很像 Mac 桌面上的多个应用程序。应用程序可以使用窗口和音量来显示内容,用户可以将这些元素重新放置在他们喜欢的任何位置。为了获得更身临其境的体验,应用程序可以打开一个专用的完整空间,其中只会显示该应用程序的内容。在完整空间内,应用程序可以使用窗口和体积、创建无限的 3D 内容、打开通往不同世界的门户,甚至可以让某人完全沉浸在某个环境中。




Apple 框架 - 扩展空间计算


SwiftUI


无论您是要创建窗口、体积还是空间体验,SwiftUI 都是构建新的 visionOS 应用程序或将现有 iPadOS 或 iOS 应用程序引入该平台的最佳方式。凭借全新的 3D 功能以及对深度、手势、效果和沉浸式场景类型的支持,SwiftUI 可以帮助您为 Vision Pro 构建精美且引人入胜的应用程序。 RealityKit 还与 SwiftUI 深度集成,以帮助您构建清晰、响应迅速且立体的界面。 SwiftUI 还可以与 UIKit 无缝协作,帮助您构建适用于 visionOS 的应用程序。


RealityKit


使用 Apple 的 3D 渲染引擎 RealityKit 在您的应用程序中呈现 3D 内容、动画和视觉效果。 RealityKit 可以自动调整物理光照条件并投射阴影、打开通往不同世界的门户、构建令人惊叹的视觉效果等等。为了创作您的材料,RealityKit 采用了 MaterialX,这是一种用于指定表面和几何着色器的开放标准,由领先的电影、视觉效果、娱乐和游戏公司使用。


ARKit


在 vision Pro 上,ARKit 可以完全了解一个人的周围环境,为您的应用提供与周围空间交互的新方式。默认情况下,ARKit 支持内核系统功能,您的应用程序在共享空间中时会自动受益于这些功能——但是当您的应用程序移动到完整空间并请求许可时,您可以利用强大的 ARKit API,例如平面估计、场景重建、图像锚点、世界轨道和骨骼手部轨道。所以在墙上泼水。从地板上弹起一个球。通过将现实世界与您的内容融合在一起,打造令人惊叹的体验。


Accessibility


visionOS 的设计考虑了可访问性,适用于希望完全通过眼睛、声音或两者的组合与设备交互的人。对于喜欢以不同方式导航内容的人,Pointer Control 允许他们选择食指、手腕或头部作为替代指针。您可以使用已在其他 Apple 平台上使用的相同技术和工具为 visionOS 创建易于访问的应用程序,并帮助使 vision Pro 成为每个人的绝佳体验。




您需要的所有工具。


Xcode


visionOS 的开发从 Xcode 开始,其中包括 visionOS SDK。将 visionOS 目标添加到您现有的项目或构建一个全新的应用程序。在 Xcode 预览中迭代您的应用程序。在全新的 visionOS Simulator 中与您的应用程序交互,探索各种房间布局和照明条件。创建测试和可视化以探索空间内容的碰撞、遮挡和场景理解。


reality composer Pro


探索全新的 reality composer Pro,旨在让您轻松预览和准备 visionOS 应用程序的 3D 内容。随 Xcode 一起提供的 reality composer Pro 可以帮助您导入和组织资产,例如 3D 模型、材料和声音。最重要的是,它与 Xcode 构建过程紧密集成以预览和优化您的 visionOS 资产。


Unity


现在,您可以使用 Unity 强大、熟悉的创作工具来创建新的应用程序和游戏,或者为 visionOS 重新构想现有的 Unity 创建的项目。除了熟悉的 Unity 功能(如 AR foundation)之外,您的应用程序还可以获得 visionOS 的所有优势,例如直通和动态注视点渲染。通过将 Unity 的创作和模拟功能与 RealityKit 管理的应用程序渲染相结合,使用 Unity 创建的内容在 visionOS 上看起来和感觉起来就像在家里一样。




您的 visionOS 之旅从这里开始。


visionOS SDK 本月晚些时候与 Xcode、visionOS 模拟器、reality composer Pro、文档、示例代码、设计指南等一起发布。


为 visionOS 做准备


无论您已经在 App Store 上拥有应用程序,还是这是您第一次为 Apple 平台开发应用程序,您现在都可以做很多事情来为 visionOS SDK 的到来做好准备。了解如何更新您的应用程序并探索现有框架,让您更轻松地开始使用 visionOS。


Prepare for visionOS


了解 visionOS


visionOS 拥有一流的框架和工具,是帮助您创造令人难以置信的空间体验的完美平台。无论您是在构想游戏、构建媒体体验、设计与 SharePlay 的连接和协作时刻、创建业务应用程序,还是更新您的网站以支持 visionOS,我们都有会议和信息来帮助您制定计划。为第 46 场 WWDC23 会议准备好 visionOS SDK,以帮助您了解平台开发、空间体验设计以及测试和工具。


Learn about visionOS


与苹果合作


在为 visionOS 开发应用程序和游戏时,获得 Apple 的直接支持。了解即将举行的活动、测试机会和其他计划,以支持您为此平台创造令人难以置信的体验。


Learn about working with Apple


#visionOS #苹果MR #苹果VR #苹果AR


翻译原文地址


作者:稻草人家
链接:https://juejin.cn/post/7241393511618347045
来源:稀土掘金
收起阅读 »

Mac效率神器Alfred Workflows

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率, 这里简单介绍下 Alfred 的工作流 背景&效果展示 作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行...
继续阅读 »

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率,
这里简单介绍下 Alfred 的工作流



背景&效果展示


作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行转换。
每次打开浏览器找到网址,然后复制内容进行转换都需要 5秒以上的时间。
有什么快捷的方式能帮助我们快速的进行这个操作吗,这里我想到的 Alfred 的工作流。
Alfred工作流可以直接脚本开发,下面是工作流开发完的效果。


1)首先唤起Alfred输入框,这里看自己设置的快捷键了
2)输入这里工作流对应的keyword (tm或tmt)然后空格输入需要转换的内容
3)回车键将转换之后的内容复制到剪切板


这个简单的工作流可以实现linux类型的时间戳转换成 yyyy-MM-dd HH:mm:ss 类型的时间字符串,
也可以将 yyyy-MM-dd HH:mm:ss 类型的时间字符串转换成时间。
转换完的内容会自动放到剪切板里面,可以直接使用 command + v 进行粘贴,也可以使用Alfred的历史剪切板进行复制。


工作流开发


1、创建空的工作流


为了简单快捷这里使用的是python开发的这个功能。
开发这个工作流首先需要再 Alfred 面板上创建一个空的工作流
Alfred -> Preferences -> Workflows -> 左下角的 + -> Blank Workflows
如下图: 



2、添加流程节点


这里使用的是 Script Filter类型的节点,可以支持keyword触发。
节点配置如下,keyword 为触发的命令关键字。下面是运行命令的配置 Script输入框中写需要运行的命令,
这里使用 {query} 方式将转换的内容传递给python脚本。 



复制到剪切板的节点如下,创建好节点用线连接就行。 



3、python脚本开发


脚本的位置放到当前工作流的根目录就行,这样不行写绝对路径,也方便工作流的导出。
1. 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 


2.Alfred官方提供了一个python类库方便开发工作流。在工作流根目录执行命令

pip install --target=. Alfred-Workflow

3.创建脚本 timestamp_2_time.py,然后开发对应的代码即可 。代码里面有注释大家可以看下

# -*- coding: utf-8 -*-
import sys
from datetime import datetime
from workflow import Workflow, ICON_CLOCK # 导包

def main(wf):
query = wf.args[0] # 获取传入的参数,这里能获取到需要转换的呢绒
if not query:
return
# 时间戳转时间字符串的方法
d = datetime.fromtimestamp(int(query) / 1000)
str1 = d.strftime("%Y-%m-%d %H:%M:%S")
'''
调用框架的方法添加运行的结果
可选参数是标题、副标题,arg是下一个节点的入参,icon是这个item展示的图标
如果有多个结果可以放多个,然后通过上下键选择
'''
wf.add_item(title=query, subtitle=str1, arg=str1, valid=True, icon=ICON_CLOCK)
# 展示结果内容list
wf.send_feedback()


if __name__ == '__main__':
'''构造 Workflow 对象,运行完退出
'''
wf = Workflow()
sys.exit(wf.run(main))

通过这几行简单的代码实现了时间戳转换成时间的小功能,
相比于以前的使用网页的形式,这个工作流可以将时间缩短到1秒,每次为你省下 4 秒钟的时间 😂😂😂


debug


开发的时候可能会遇到bug,可以通过下图方式打开运行日志查问题。 



开发好的工作流要使用直接导入就行


tmwf.tar
下载之后 tar -xvf tmwf.tar 解压导入就行。
这里需要注意下,我本地的python路径是 /usr/local/bin/python 大家需要换成自己的python路径。

作者:程序员大鹏
链接:https://juejin.cn/post/7252541723149238330
来源:稀土掘金
收起阅读 »

axios使用异步方式无感刷新token,简单,太简单了

🍉 废话在前 写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。 下面接着分析一下踩到的坑以及解决思路 🍗 接着踩坑 我...
继续阅读 »

🍉 废话在前


写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。




下面接着分析一下踩到的坑以及解决思路


🍗 接着踩坑


我按照之前传统的方式在返回拦截器里面进行token刷新,正常的数据可以返回,但是这个时候会有比较麻烦的地方,就是请求的数据可以在拦截器里面得到,但是不能渲染到界面上(看到这里的时候我是懵的)。


看一下代码

service.interceptors.response.use(
response => {
const res = response.data
//刷新token的时候,可以从这里拦截到新数据,但是没有显示在页面上
console.log('拦截数据:',res)
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
error => {
switch (error.response.status) {
case 401:
MessageBox.confirm('身份认证已过期,是否刷新本页继续浏览?', '提示', {
confirmButtonText: '继续浏览',
cancelButtonText: '退出登录',
type: 'warning'
}).then(() => {
axios.post('/api/token/refresh/', {
refresh: localStorage.getItem("retoken")
}).then(response => {
let res = response.data || {}
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = '';
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
window.location.reload();
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '令牌过期',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
}
}).catch(err => {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '信息认证失败,请重新登录',
duration: 0
});
})
}).catch(() => {
router.push({
path: "/login",

})
Message({
message: '已退出',
type: 'success',
})
localStorage.clear();
});
break;
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

懵归懵,好在已经发现这个问题了,剩下的怎么解决呢?


当时想的是和后端配合,让后端直接发一个token过期的时间戳给我,我直接把这个时间戳放到localStrage里面,通过这个localStrage,直接在前端进行判断token的过期时间进行请求拦截,如果当前请求的时间大于了这个localStrage里面的时间,就说明token过期了,我这边就需要重新请求token了,而不要后端去进行token的验证。


信心满满的弄了一下,发现行不通,token过期的时候,后端直接一个错误401,前端就又回到解放前了。而且用时间戳的方式很容易出现bug,且在前端进行token时长的验证,很容易出现问题。因此,我还是觉得再研究一下上面那一段代码。


当然踩到坑不只是这个,还有百度和chatGPT,真的,看了一下没有找到一个可行的,总结一下主要有以下几种

  1. 状态管理vuex

  2. 路由router

  3. 时间戳(和我刚刚那种方式差不多)

  4. 依赖注入inject

  5. 刷新界面(我最开始那种方式,但是刷新的时候会出现页面白屏,且用户如果在页面上有一些自己输入的数据也会被清空,用户体验感不好)


以上方式我先不管行不行,但是麻烦是肯定的,做前端讲究的就一个字:“懒”
不是,应该是 “高效快捷”
所以这些方式就pass掉了


只能想想为什么拦截器里面可以得到数据,为什么页面位置得不到数据了


🥩 解决思路


下面是我的解决思路,有不对的地方还请看到的大神指出来一下😁


当用户发起请求的时候,因为刷新token的http状态码是401,这个时候axios的响应拦截器就直接进行错误捕获了,到了这里,因为数据已经返回了,但是因为是错误数据,页面得到的这个数据不可用且当前请求已经结束了,当然这里对状态码401是进行处理了的(应该获取token了)。


采用普通的获取方式来获取token,因为异步的原因,我们获取token的同时页面也在做刷新,token获取的同时,界面也刷新完毕了(但是是没有数据的,不做错误捕获会报错),因此我们在获取token完毕,且用新的token去获取数据时,拦截器里面会有数据,但是界面已经休息了,就不会把拦截器里面的新数据刷新到页面了。


因此这个地方需要对获取token的过程进行一下请求阻塞,把获取token的请求变成同步的。到这里就差不多了。直接把响应拦截器里面的error函数变成同步不就行了吗,async + await可以出来了。


以上是自己当时的想法,简单说来就是 页面刷新需要慢我获取token一步 ,通过这个方式也确实做到了无感刷新🤣


希望以上的能帮助到你,有什么好的思路也欢迎评论区指出。


🍓完整代码


这个是用了2.13.2版本的element-ui以及nprogress的一个axios代码模块,包含了一个下载文件的模块。


如果需要的话可以根据自己的需求来进行修改

import axios from 'axios'
import {
Message,
Loading,
Notification,
} from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import router from '@/router/index'
const baseURL = '/api'
const service = axios.create({
baseURL,
timeout: 6000
})
NProgress.configure({
showSpinner: false
}) // NProgress Configuration
let loadingInstance = undefined;
service.interceptors.request.use(
config => {
NProgress.start()
loadingInstance = Loading.service({
lock: true,
text: '正在加载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return config;
},
error => {
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
const res = response.data

if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
switch (res.code) {
case 200:
return res
case 401:
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失效',
message: '当前身份信息超过三天已失效,请您重新登录',
duration: 0
});
break;
default:
Message({
message: res.message || '请求错误',
type: 'error',
duration: 5 * 1000
})
break;
}
},
async error => {
switch (error.response.status) {
case 401:
const err401Data = error.response.data || {}
if (err401Data.code !== "token_not_valid") {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份信息认证失败,请您重新登录',
duration: 0
});
return
}
try {
const res = await service.post('/token/refresh/', {
refresh: localStorage.getItem("retoken")
})
if (res.code == 200) {
let token = res.data.result;
localStorage.setItem("token", token.access);
localStorage.setItem("retoken", token.refresh);
error.response.config.baseURL = ''
error.response.config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token")
return service(error.response.config)
} else {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '当前身份认证信息已失效,请您重新登录',
duration: 0
});
}
} catch (error) {
router.push({
path: "/login",
})
localStorage.clear();
Notification.error({
title: '认证失败',
message: '身份认证失败,请您重新登录,失败原因:' + error.message,
duration: 0
});
}

break
case 404:
Notification.error({
title: '404错误',
message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
});
break
default:
Notification.error({
title: '请求错误',
message: '服务器请求错误,请联系管理员或稍后重试',
});
break;
}
if (loadingInstance) {
loadingInstance.close();
NProgress.done();
}
return Promise.reject(error);

}
)

// 文件下载通用方式
export const requestFile = axios.create({
baseURL,
timeout: 0, //关闭超时时间
});

requestFile.interceptors.request.use((config) => {
config.headers['Authorization'] = 'Bearer ' + localStorage.getItem("token") //携带的请求头
config.responseType = 'blob';
loadingInstance = Loading.service({
lock: true,
text: '正在下载,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.3)'
})
return config;
});

requestFile.interceptors.response.use(
(response) => {
let res = response.data;
if (loadingInstance) {
loadingInstance.close()
}
// const contentType = response.headers['content-type'];//获取返回的数据类型
let blob = new Blob([res], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" //文件下载以及上传类型为 .xlsx
});
let url = window.URL.createObjectURL(blob);
// 创建一个链接元素
let link = document.createElement('a');
link.href = url;
link.download = '产品列表.xlsx'; // 自定义文件名
link.click();


},
(err) => {
Message({
message: '操作失败,请联系管理员',
type: 'error',
})
if (loadingInstance) {

loadingInstance.close()
}
}
);


export default service;
作者:讷言丶
链接:https://juejin.cn/post/7260700447170101306
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

🎉前端开发书籍推荐🎉

文章首发公众号:萌萌哒草头将军,欢迎关注 🎉SolidJS响应式原理和简易实现🎉 做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。 💎《JavaScript高级程序设计》 推荐指数:⭐⭐⭐⭐⭐...
继续阅读 »

文章首发公众号:萌萌哒草头将军,欢迎关注



🎉SolidJS响应式原理和简易实现🎉


做为多年学习JavaScript的开发者,一路走来,几多坎坷,回头再看来时的路,特别感谢下面几本书带我入行前端。


💎《JavaScript高级程序设计》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容扎实,且不深奥。


这本书在我大学的时候就已经买了,当时经济实力有限,买的是影印版,不过也被我研读了多遍,并且做了详细的记录。这本书见证了我的前端之路,一直被我珍藏至今。




现在已经出了第四版了。前端入门进阶宝典,所以也被广大开发者称为《红宝书》。


💎《Javascript权威指南》




推荐指数:⭐⭐⭐⭐⭐


推荐理由:内容全面,讲解详细,配合红宝书效果更佳。


该书被称为《犀牛书》,书如其名,真的是权威指南,即使你买了红宝书,我也推荐你买一本,因为这两本对相同的知识讲解,侧重点不同。


比如对于闭包,红宝书很详细,从作用域,到作用域链,再到活动对象,讲解由浅到深,十分详细,而《犀牛书》中仅仅是给出闭包的概念,然后举例说明差异。


但是《犀牛书》对于类型转换toStringvalueof讲解则十分详细,《红宝书》则浅浅的带过。


《犀牛书》更像是一本字典,有不懂的问题,可以及时查漏补缺。


💎《图解HTTP》




推荐指数:⭐⭐⭐⭐


推荐理由:图文并茂,简洁明了


这本书作为第三本推荐,绝不是空穴来风,大量的图来解释枯燥的概念,形象生动,老少皆宜。


前端开发一大部分的时间都是在和后端的接口打交道,而Http无疑是沟通的桥梁。


读完这本书,你将会了解到网络分层模型Http协议和TCP/IP的关系、后端的数据怎么从服务端到达浏览器的、常用Http状态码的含义、请求头的各种含义、你输入url浏览器发生了什么等热门面试题的答案。


我买的是三件套《图解Http》《图解TCP/IP》《图解网络硬件》,《图解网络硬件》一点也不推荐,不懂的硬件直接百度吧,还是彩色图片。如果你是做运维相关的前端开发,《图解TCP/IP》同样值得一看。




💎《数据结构与算法JavaScript描述》




推荐指数:⭐⭐⭐⭐


推荐理由:进阶利器,闭眼入就对了。


大多数同学选择前端,主要还是因为数据结构和算法方面比较薄弱,但是这本书却使用了简洁的方法实现了各种数据结构和算法。


对于难懂的数据结构,有详细的结构图解释,是我读过最容易理解的版本了,不过这本书目前还是ES5语法版本实现,我在前面的文章中使用ES6语法实现过,并做了部分笔记。


👉【数据结构】我的学习笔记




💎《JavaScript设计模式》




推荐指数:⭐⭐⭐


推荐理由:常见的设计模式都有,讲解的比较简单易懂,但是实现比较简陋。


这本书是你入门中级后继续提升的有利法宝,不管什么框架,底层都逃不出两三个设计模式的,所以十分推荐你进阶的时候去读它。


目前有两本名为《JavaScript设计模式》的书,我买的是徐涛翻译版本影印版(和红宝书一起买的),但是最近查阅发现流行的是张容铭著作的版本,这里请自行斟酌买哪个版本。


我买的这个版本将设计模式分为创建型、行为型和结构型三种,前面部分分别讲解了十三种设计模式,后半部分讲解了老牌框架JQuery设计的各种设计模式,虽然从现在的情况看JQuery已经凉了,但是它的设计智慧,真的令人敬佩。


这本书的缺点也是语法版本较旧,不过我也写了最新语法的部分笔记。


👉超级简单的设计模式,看不懂你来打我


今天的内容就这些了,如果你有更好的书籍,可以告诉我!


现在,关注我的公众号会有送书福利,具体请在公众号回复:活动,即可查看详情


作者:萌萌哒草头将军
链接:https://juejin.cn/post/7238552719266644029
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

推荐 6 个 火火火火 的开源项目

本期推荐开源项目目录:1、ChatGPT 网页应用(AI)2、AI 换脸(AI)3、API 调用 Midjourney 进行 AI 画图(AI)4、如何使用 Open AI 的 API?(AI)5、中华古诗词数据库6、动画编程 01. ChatGPT 网页应用...
继续阅读 »

本期推荐开源项目目录:

1、ChatGPT 网页应用(AI)
2、AI 换脸(AI)
3、API 调用 Midjourney 进行 AI 画图(AI)
4、如何使用 Open AI 的 API?(AI)
5、中华古诗词数据库
6、动画编程


01. ChatGPT 网页应用


基于 ChatGPT-Next-Web 二次开发的 ChatGPT 网页付费系统,包含用户管理模块和后台看板。


ChatGPT-Admin-Web 付费系统包含七个模块,包括:内容接口、用户系统、支付、敏感词过滤、自由聊天、分销、收益


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/AprilNEA/Ch…



编辑


添加图片注释,不超过 140 字(可选)


02. AI 换脸


适用于视频聊天的 AI 换脸模型,你可以使用这个 AI 模型替换摄像头中的面部或视频中的面部。这是一些例子:


编辑


添加图片注释,不超过 140 字(可选)


开源地址:github.com/iperov/Deep…


03. API 调用 Midjourney 进行 AI 画图


通过代理 MidJourney 的 Discord 频道,实现 api 形式调用AI绘图。


前提是你要注册 Midjourney 账号、并在 Discord 创建在自己的频道和机器人,然后就可以根据这个项目的指引一步步去使用 Api 调用 Midjourney 了。


开源地址:github.com/novicezk/mi…


编辑


添加图片注释,不超过 140 字(可选)


04. 如何使用 Open AI 的 API?


Open AI-Cook Book 是一本 Open AI 的 API 使用指南,提供了一些通过 Open AI 的 API 搭建任务的示例代码。


开源地址:github.com/openai/open…


编辑


添加图片注释,不超过 140 字(可选)


05. 中华古诗词数据库


为了让古诗词这个人类瑰宝传承下去,中华古诗词数据库诞生了。


这个项目整理了中华大量的古诗词,支持 Json 格式。数据库包含唐宋两朝近一万四千古诗人的作品, 接近 5.5 万首唐诗、26 万宋诗. 两宋时期1564位词人,21050首词。


开源地址:github.com/chinese-poe…


编辑


添加图片注释,不超过 140 字(可选)



  1. 动画编程


Motion Canvas 是一个 TypeScript 库,可以通过编程的方式生成动画,并提供所述动画的实时预览的编辑器。


开源地址:github.com/motion-canv…




编辑


添加图片注释,不超过 140 字(可选)


历史盘点


逛逛 GitHub 每天推荐一个好玩有趣的开源项目。历史推荐的开源项目已经收录到 GitHub 项目,欢迎 Star:

地址:https://github.com/Wechat-ggGitHub/Awesome-GitHub-Repo


作者:逛逛GitHub
链接:https://juejin.cn/post/7240690534075318309
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

rpc比http好吗,缪论?

是什么,如何理解 RPC(Remote Procedure Call) 直译就是远程过程调用 HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议 RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议...
继续阅读 »

是什么,如何理解


RPC(Remote Procedure Call) 直译就是远程过程调用


HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议


RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议、效率、应用范围、使用规则等不同,所以是不同的名字,本质都是为了分布式系统间的通信而生,是一种应用层通信(请求-响应)协议(从OSI网络模型来看)。

RPC是 Bruce Jay Nelson 在1981年创造的术语,HTTP是在1990年左右产生的(可以参看维基百科)


RPC协议 和 RPC,到底叫什么?RPC协议=RPC


HTTP协议、HTTP,到底叫什么?HTTP协议=HTTP


RPC|HTTP只是大家的简称

1、HTTP协议不仅仅只有协议,还有超文本,传输,以及很多功能(比如编解码、面试经常背的各种参数的作用)

2、RPC协议也不仅仅只有协议,还有 编解码,服务注册发现,负载均衡等


RPC协议本质上定义了一种通信的流程,而具体的实现技术是没有约束的,每一种RPC框架都有自己的实现方式,我认为HTTP也是RPC的一种实现方式


协议直白来讲是一种约定,rpc和http都是为了服务器间的通信而生,都需要制定一套标准协议来进行通信。不过HTTP比较火,是一个全世界的统一约定,使用比较广泛。但通用也意味着冗余,所以后来又产生了很多RPC框架(自定义协议,具备优秀的性能等)


我们可以自定义RPC请求/响应 包含的消息头和消息体结构,自定义编解码方式,自定义网络通信方式,只要clientserver消息的发送和解析能对应即可,这些问题确认下来,一个RPC框架就设计出来了


下面先从请求过程看一下RPC和HTTP都会经历哪些阶段,然后再分阶段去做对比

一次请求的过程



从请求链路可以看到,最核心的只有三层:编解码、协议、网络通信


下面会从这3个角度去对比HTTP和RPC


HTTP VS RPC自定义协议


HTTP和RPC 2个关键词不具备可比较性,因为RPC包含了HTTP。


但是RPC自定义协议(thrift, protobuf, dubbo, kitex-thrift等) 是RPC的具体实现,HTTP也是RPC的具体实现,它们是具备可比较性的


编解码(序列化)



  • 序列化: 指将程序运行过程中的动态内存数据(java的class、go的struct)转化为硬盘中静态二进制数据的过程,以方便网络传输。

  • 反序列化:指将硬盘中静态二进制数据转化为程序运行过程中的动态内存数据的过程,以方便程序计算。


HTTP/1.1 一般用json


自定义RPC协议 一般用 thrift、protobuf


kitex序列化协议


协议层


编码之后,数据转换成字节流,但是RPC通信时,每次请求发送的数据大小不是固定的,那么为了区分消息的边界,避免粘包、半包等现象,我们需要定义一种协议,来使得接收方能够正确地读出不定长的内容。简单点说,通信协议就是约定客户端和服务器端传输什么数据,以及如何解析数据。


可参考

1、kitex:概览,传输协议

2、dubbo:triple 协议,概览


可以思考一下 序列化、传输协议、网络通信的关系,下面以kitex为例进行分析


kitex codec 接口定义kitex thrift 序列化实现kitex ttheader协议,kitex 发送请求核心代码


可以发现 Encode中,先根据message构造出header,写入out,然后再把data(实际的业务数据)写到out。

encode函数完全遵守 ttheader协议去构造数据。

最后再把out通过网络库发送出去

网络通信层

网络通信层主要提供一个易用的网络库,封装了操作系统提供的socket api。


HTTP的长连接和TCP长连接不是一个东西,需要注意下,TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置(只能保证网络没问题,不能代表服务没问题)


其中 HTTP2 拥有多路复用、优先级控制、头部压缩等优势


可以参考


kitex:连接类型


RPC自定义协议 和 HTTP的使用场景


公司内部的微服务,对客户端提供的服务 适合用RPC,更好的性能


对外服务、单体服务、为前端提供的服务适合用HTTP


我的思考


rpc在编解码、协议层、网络通信 都比HTTP有更大的优势,那为啥不把HTTP换成RPC呢

1、人的认知,HTTP已经深入人心(或者说生态好,通用性强),几乎所有的机器、浏览器和语言默认都会支持。但是自定义RPC协议 可能很多人都没听过(比如kitex、dubbo等),还让别人支持,根本不可能。

  • 需要建设全局的DNS等等,HTTP链路中的组件都需要换成 自定义的那一套,成本极高。
  • 但是公司内部可以搞成一套,可以极大提高性能,何乐而不为。

  • 我见过的案例是很多时候并没有深入思考为什么用,而是大家都这么用,我也这么用。

2、浏览器只支持 http协议。而且浏览器不支持自定义编解码的解析
      为啥大家面向浏览器/前端 不用自定义编解码?
     http不仅可以传输json、还可以传输二进制、图片等。所以协议层可以用http,编解码用protobuf/thrift也是可行的。
  • 公司内部实际案例:服务端和客户端交互时,为了提高性能,采用protobuf编解码数据,使用http协议传输数据。

  • 但是每次请求/响应数据都是不可读的。服务端会把protobuf编码前的数据转为json,用于打印log/存储,方便排查问题。

3、RPC框架 可以自定义负载均衡,重试机制,高可用,流量控制等策略。这些是HTTP不能支持的
  • 我理解是协议层用的http,但是内部的运行机制还是自定义的。http只是定义了传输数据的格式。举个例子:http的流量控制其实用的是 tcp的滑动窗口,http协议本身不具备这些功能。但是rpc是可以自己加这些功能的。这些功能必然有数据传输,这个传输协议用的http。

作者:cli
链接:https://juejin.cn/post/7264454873588449336
来源:稀土掘金

收起阅读 »

SpringBoot获取不到用户真实IP怎么办

今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地...
继续阅读 »
今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地址,害,看看我是怎么解决的吧!

问题原因

客户端请求数据时走的是Nginx反向代理,默认情况下客户端的真实IP地址会被其过滤,使得SpringBoot程序无法直接获得真实的客户端IP地址,获取到的都是Nginx的IP地址。

解决方案:

通过更改Nginx配置文件将客户端真实的IP地址加到请求头中,这样就能正常获取到客户端的IP地址了,下面我一步步带你看看如何配置和获取。
修改Nginx配置文件

#这个参数设置了HTTP请求头的Host字段,host表示请求的Host头,也就是请求的域名。通过这个设置,Nginx会将请求的Host头信息传递给后端服务。
proxy_set_header Host $host;
#这个参数设置了HTTP请求头的X−Real−IP字段,remote_addr表示客户端的IP地址。通过这个设置,Nginx会将客户端的真实IP地址传递给后端服务
proxy_set_header X-Real-IP $remote_addr;
#这个参数设置了HTTP请求头的 X-Forwarded-For字段,"X-Forwarded-For"是一个标准的HTTP请求头,用于表示HTTP请求经过的代理服务器链路信息,proxy_add_x_forwarded_for表示添加额外的服务器链路信息。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

修改后我的nginx.conf中的server如下所示

server {
listen 443 ssl;
server_name xxx.com;

ssl_certificate "ssl证书pem文件";
ssl_certificate_key "ssl证书key文件";
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

location / {
root 前端html文件目录;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# 关键在下面这个配置,上面的配置自己根据情况而定就行
location /hello{
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

SpringBoot代码实现

第一种方式:在代码中直接通过X-Forwarded-For获取到真实IP地址

@Slf4j
public class CommonUtil {
/**
* <p> 获取当前请求客户端的IP地址 </p>
*
* @param request 请求信息
* @return ip地址
**/
public static String getIp(HttpServletRequest request) {
if (request == null) {
return null;
}
String unknown = "unknown";
// 使用X-Forwarded-For就能获取到客户端真实IP地址
String ip = request.getHeader("X-Forwarded-For");
log.info("X-Forwarded-For:" + ip);
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
log.info("HTTP_X_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED");
log.info("HTTP_X_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
log.info("HTTP_X_CLUSTER_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
log.info("HTTP_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED_FOR");
log.info("HTTP_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED");
log.info("HTTP_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_VIA");
log.info("HTTP_VIA:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
log.info("REMOTE_ADDR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("getRemoteAddr:" + ip);
}
return ip;
}

第二种方式:在application.yml文件中加以下配置,直接通过request.getRemoteAddr()并可以获取到真实IP

server:
port: 8090
tomcat:
#Nginx转发 获取客户端真实IP配置
remoteip:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto

作者:BivinCode
链接:https://juejin.cn/post/7266040474321027124
来源:稀土掘金

收起阅读 »

69.9K Star,最强开源内网穿透工具:frp

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可...
继续阅读 »

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可以随时随地的访问。


说到内网传统,TJ君第一个想到的是国内最早的一款知名软件:花生壳。但是今天不是要推荐它,而是要推荐一个更牛的开源项目:frp!该项目目前已经收获了69.9 K Star,在GitHub上获得了极大的认可!


下载安装

frp目前已经提供了大部分操作系统的支持版本,通过这个链接:github.com/fatedier/fr… 就可以下载到适合你使用的安装。


以Windows的包为例,解压后可以获得这些内容:


frps是服务端程序,frpc是客户端程序。ini文件就是对应的配置文件。


首发 blog.didispace.com/tj-opensour…,转载请注明出处


暴露内网服务


内网穿透的玩法有很多,这里列举一个比较常见的例子。


比如:我要暴露一个只有自己能访问到服务器。那么可以这样配置:


配置 frps.ini,并启动服务端 frps

[common]
bind_port = 7000

在需要暴露到外网的机器上部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh]
type = stcp
# 只有 sk 一致的用户才能访问到此服务
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22

在想要访问内网服务的机器上也部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh_visitor]
type = stcp
# stcp 的访问者
role = visitor
# 要访问的 stcp 代理的名字
server_name = secret_ssh
sk = abcdefg
# 绑定本地端口用于访问 SSH 服务
bind_addr = 127.0.0.1
bind_port = 6000

把frpc也都启动起来之后,通过 SSH 就可以访问内网机器了

ssh -oPort=6000 test@127.0.0.1

其他支持


除了上面的玩法之外,frp还有很多玩法,比如:

1、自定义域名访问内网的 Web 服务

2、转发 DNS 查询请求

3、转发 Unix 域套接

4、对外提供简单的文件访问服务

5、为本地 HTTP 服务启用 HTTPS

6、点对点内网穿透


篇幅有限,具体如何配置这里就不多说了,有需要的读者可以直接查看官方文档,均有详细的服务端客户端配置案例。


最后,奉上相关链接:


开源地址:github.com/fatedier/fr…
文档地址:gofrp.org/docs/

作者:程序猿DD
链接:https://juejin.cn/post/7263283712224395321
来源:稀土掘金
收起阅读 »

你的野心距离成功,就差一个机会

这几天,深圳的几个达友张罗着找时间线下聚聚。 这两天在看《狂飙》。 自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。 这么热门的片子,加一起才看 6 集。 里面的张颂文,演技的确赞。 不过东哥想的是,演技这么好,怎么出道20年才火...
继续阅读 »

这几天,深圳的几个达友张罗着找时间线下聚聚。





这两天在看《狂飙》。


自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。


这么热门的片子,加一起才看 6 集。


里面的张颂文,演技的确赞。


不过东哥想的是,演技这么好,怎么出道20年才火起来?


他缺的,是什么?







人怎么能成功?简单地说



成功 = 能力 × 机会



能力是修炼内功,让自己变得更专业,成为头部的专家,是自己应该搞定的事情。


机会这事儿,就朦胧多了。


酒香不怕巷子深,是物质稀缺时代的事情。


整个镇子只有一家酿酒,巷子再深都有酒鬼登门。


而当前世界已经进入到了产能过剩、需求不足的时代。


国民总时间恒定,大家在存量的池子里杀的头破血流。


没机会展现的才华,只能被风沙埋没。


机会,越来越重要。


你与成功之间,可能就差一个机会。


比如张颂文,没机会拿到好剧本好角色,就只能是个小演员。


46 岁,买不起房子,没存款,感觉自己好失败。


每天都被拒绝,甚至侮辱地拒绝,让他滚蛋。


所以我们不能只死磕自己苦练内功,坐等被赏识被发现。


傻傻的,感动自己。


要为自己的野心,创造机会。


怎么创造呢?




去机会更多的地方。 在水多的地方打井,选鱼多的地方捞鱼。


最关键的选择,一个是城市,一个是赛道。


做金融,就到上海和深圳,搞互联网,就去北京和深圳。


东哥做香港保险,一个很大的优势就是所在的城市。


从福田站到香港西九龙,高铁15分钟。


市中心到市中心。


赛道选择上,做和钱近的工作,做可积累的工作。


能和人打交道,就别和机器打交道;能做销售,就别做售后。


销售看到的都是机会和钱,售后看到的都是负面问题。




向上链接,寻找大节点,利用好高能级的关系


人和人能量密度不同,大节点就是个人崛起的发动机。


达叔曾谈到过,在他写公众号的过程中,曾被欧神、医业观察的星哥、凯叔药械升职记的凯叔推荐。


三次推荐,引来了大批流量,成就了达叔的崛起。


在职场,就是要发现身边的强者,做深度绑定,成为强者权力结构件的一部分。


多花心思,多花钱,努力走进领导的小圈子,成为他身边的人。


利用他的势能,实现职业和财富的崛起。


就像《人民的名义》里面,高育良提到汉大帮的时候说



主观上说,我从没想过把人民赋予的权力向任何一位学生私相授受,但客观上也许私相授受了。
做了这么多年的法学教授,教了这么多年书,学生少不了,对自己学生呢,谁都不可能没感情,用人时就难免有偏爱。






进入不了核心圈子,就注定是个边缘人物,最后沦为炮灰。


如果在食物、资源匮乏的时候,你不坐在桌子旁,大概率就得躺在桌子上。


需要注意的是,链接的能级差不能太大。


基层员工,就别总想着去链接总经理董事长。


除非你是王思聪,他是王健林。


县官不如现管,链接好那个直接自己升职加薪的人,最有价值。




多和人互动,无论是线上还是线下。


人和人之间的互动永远是这个世界的最核心的算法,剩下全是工具。


职场中,多和实权派互动,和业务部门互动,和给公司赚钱最多的部门互动。


最忌讳的,就是整天对着电脑研究计算模型。


上班一天,接触的人不过办公室里这三五个,和固定的几个窗口,加一起超不过10个人。


能深入沟通和交流的,只有个位数。


除非你能成为公司最顶级的技术大牛,非你不可的那种,否则随时都可能被干掉。


东哥在自己的在每一篇文章后,都会附上了自己的个人微信,也是希望能和大家链接。


短视频时代,能潜下心来阅读文字本身就很可贵。


尤其还是东哥这种枯燥乏味谈赚钱的文章。


但有趣的是,很多人加微信以后一声不吱,甚至我主动询问也不回应。


这样的链接很难有实际价值。


只有沟通彼此的链接才会有价值,无论这个价值是信息、资源还是项目。


没有互动,彼此就仅仅是微信系统里面的几个号码。




多尝试,多拓展。机会是干出来的,不是想出来的。


就像猫王和村上春树的故事。


猫王是二十世纪收入最高的歌星,在卖唱及作明星之前是一位货车司机,每月的收入只数百美元。


工余之暇,他去试唱,被唱片公司相中,然后一举成名。


每年的收入以千万美元计。


村上春树则是在看棒球比赛的时候,突然想要写本小说。


比赛一结束,他就立马赶到文具店买了笔和纸,开始创作他的第一本小说《且听风吟》。


人生有无数种可能。当前的状态,不代表永远。


状态之外,要不断向外拓展,比如猫王的试镜,村上春树的小说。


只有不断地试,才会发现身上其他可能。


否则就只能,守着安静的沙漠 等待花开,看着别人的快乐默默感慨。


尤其自媒体,轻资产,风险可控,是时代给予我们的拓展机会。


多想想,自己有什么东西,可以不断打磨放大?


写公众号这几个月,和各路大 V 沟通,大开眼界。


医生讲医疗,房 V 讲房产都是常规操作。


搞心理学的做星座内容,看桃花运、分析财运,居然也有不少客户。


擅长做饭的可以做美食博主,身材好的可以做服装博主。


你擅长的是什么,能打出什么机会?




用无限游戏的思维方式,多推销自己。


无论做什么,总有一双眼睛在背后默默看着你。


合作的过程就是展现自己的机会。


工作中做汇报做提案,与同事沟通方案,最高任务是什么?


有人会说是把事情搞定,所以应该对事不对人。


对项目推进有利的,就要据理力争。


实际上,这样的工作是低效的,有时候甚至会起反作用。


同样的事情,不同的人沟通,常会有完全相反的结果。


对事不对人,现实中不存在。


理念和现实冲突时,错的永远是理念,不会是现实


沟通的最高任务,不是推销自己对项目的主张,而是推销自己这个人


从这个角度想,我们就不会局限于一城一地之得失,一朝一夕之荣辱。


而会站在一个更高层面,整个职业生涯的高度,来看待当下的问题。


项目成败的因素有很多,不一定是我们可以控制的。


但项目可以失败,人不可以失败


每一次沟通,都要展现自己足够靠谱,足够专业,是更长远合作的好伙伴。


所以要利用好每一次沟通,甚至是每一次刁难。


都是在给自己种善因。




资源也好,技术也罢,多分享,多展现。


不要藏着掖着,害怕教会徒弟饿死师傅。


太阳下面没有新鲜事,你能找到,别人也能找到。


真正的强者,你打压不了。


与其彼此竞争拼个头破血流,不如投资强者。


在他发展壮大的必然道路上,助他一臂之力。


也助以后的自己一臂之力。


你能成就多少强者,就有多大能量。


就像春秋时期齐桓公,想拜鲍叔牙为相,同时还要宰了对手公子纠的谋士管仲。


谁知鲍叔牙对他说:“我的才能只能让齐国平安,如果您要称霸天下,一定得拜管仲为相。”


齐桓公从之,尽释前嫌,拜管仲为国相,终成王霸之业,鲍叔牙也成就了自己千古美名。




结合多动手和多分享的思路,打造自己的产品。


就像伟大的程序员 Linus 说的



Talk is cheap. Show me the code.
被扯没用的,给我看代码。



产品是创作能力的背书,是自控能力的表现,是你最大的筹码。


自己的作品,可以理解为一种社交货币。


打磨作品就是往里钱,在将来的某个时刻,可以兑换它。


陌生人介绍的时候,就可以直接把作品丢出来。


有了作品,就有了各种合作的可能。


东哥这么小一个号,都有人过来谈合作,有时会一些意外的机会。




最后一点,给生活增加一些随机性。


平时晚上在家读书写作充电。下班时,朋友邀请参加一个陌生的聚会,去不去?


马上出发。


这就是生活中的随机性,说不定在聚会过程中会有新的收获。


一个漂亮的妞做女朋友,或一个合作的机会赚点钱。


这就是《黑天鹅》里面提到的正面黑天鹅


努力把自己暴露在可能发生正向意外的环境里。


用时间上微小的损失,换正面黑天鹅出现的可能,进而突破路径依赖。


就像这几天,深圳的几个达友张罗着找时间线下聚聚。


这群人里面有做医疗的,做芯片贸易的,做生物的,还有东哥是卖保险的。


彼此之间可能交流什么,完全未知。


但也就是因为这种未知,才能让自己突破日常的局限,也就更值钱。




供给过剩的时代,无数人在存量的池子里杀得头破血流。


给自己多创造一些机会,杀出来的可能就大一些

  • 去鱼多的地方捞鱼,挤到机会多的地方;
  • 向上链接,发现强者,进入到强者的核心圈子;
  • 多和人互动,不要沉迷于技术;
  • 基于自身优势多对外拓展,寻找机会;
  • 无限游戏思维,办事儿的时候笑面挑战,推销自己;
  • 多分享,尤其挖掘并投资潜力股;
  • 打造自己的作品,积累社交货币;
  • 给生活增加一些随机性;


避免像张颂文,满腹才华,却走了漫长的二十年。


你距离成功,也许只缺一个机会。


那就造一个出来。



作者:jetorz
来源:mdnice.com/writing/39186f5978a84668b24442c684d01fa3
收起阅读 »

this指向哪?

web
谁调用就指向谁 this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。 var userName = '张三' function fn() { var userName = '李四' console.log(this.use...
继续阅读 »

谁调用就指向谁


this 指向哪?先记住一句话:谁调用就指向谁,记住这句话就成功一大半了。


var userName = '张三'
function fn() {
var userName = '李四'
console.log(this.userName) // 张三
}
fn()

函数fn相当于挂在window,调用fn()等同于window.fn(),所以fn可以看做是被window调用,此时this便指向window,所以输出结果是函数外的userName变量。


再看下面的示例:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.userName) // 李四
}
}

obj.fn()

函数fn被定义在对象obj里,通过obj.fn()调用,回到我们开头的那句话:谁调用就指向谁fnobj调用,所以this指向obj,输出结果是李四。


如果使用windowd.obj.fn()调用呢?结果还是一样的,因为最终是obj调用,所以this指向的还是obj


接着往下看下面这段代码:


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
console.log(this.name) // 张三
}
}

var func = obj.fn
func()

输出结果为张三,因为代码中只是将obj.fn赋值给func,此时函数fn还没有被调用,真正调用是在func()func挂在window下,所以最终相当于是window调用了函数fn,所以this指向的是window


还是应了那句话:谁调用就指向谁


其他情况


1、作为一个函数被直接调用


当函数被直接调用,没有挂在任何对象上时,this指向window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
function a() {
console.log(this.userName) // 张三
}
a()
}
}
obj.fn()

这里在fn中定义了一个函数a并调用,注意这里函数a只是在函数fn内,它并没有挂在哪个对象上,也没有通过其他方式被调用,而是作为一个函数被直接调用,所以this指向window


2、箭头函数


箭头函数的特点是没有自己的this也不能通过new调用。箭头函数通过作用域向上查找,找到离包裹它最近的那一个普通函数的this作为自己的this,如果没有则指向全局对象window


var userName = '张三'
var obj = {
userName: '李四',
fn: function() {
var func = () => {
console.log(this.userName) // 李四
}
func()
},
a: {
b: () => {
console.log(this.userName) // 张三
}
}
}
obj.fn()
obj.a.b()

func是一个箭头函数,箭头函数通过向上查找找到fnfnthis指向obj,所以箭头函数的this指向的也是obj,输出结果:李四。


obj.a.b()则指向window,输出结果:张三。


3、new 构造函数


当函数通过使用new调用时,new过程中会创建一个新的对象,而this便是指向这个新创建的对象。


function fn(name) {
this.userName = name
this.age = 18
console.log(this) // {userName: '张三', age: 18}
}
let data = new fn('张三')
// new的同时内部会默认把this做回返回值返回
console.log('data: ', data) // data: {userName: '张三', age: 18}

如何改变this指向


1、call、apply、bind


可以通过callapplybind的第一个参数传值作为this值,改变this指向。


obj.fn('hello') // hello,张三
obj.fn.call({ userName: '李四' }, 'hello') // hello,李四
obj.fn.apply({ userName: '王五' }, ['hello']) // hello,王五
obj.fn.bind({ userName: '老六' }, 'hello')() // hello,老六

2、call、apply、bind区别


从上面的使用可以看到,这三个API的参数形式除了第一个参数外是有一些区别的



  • callbind是多参数的形式:call(thisArg, arg1, arg2...)

  • apply是数组的形式:call(thisArg, [arg1, arg2...])


另外一个区别是:bind的返回值是一个函数,需要自己再手动调用一次,而callapply<

作者:狂砍2分4篮板
来源:juejin.cn/post/7265534390506635319
/code>则不用。

收起阅读 »

孩子是双眼皮还是单眼皮?来自贝叶斯算法的推测

问题描述 最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。 我家的情况是这样,宝爸宝妈、爷爷奶...
继续阅读 »

问题描述


最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。


我家的情况是这样,宝爸宝妈、爷爷奶奶、姥姥姥爷都是双眼皮。


查了一下资料,双眼皮是显性基因,因此除非宝爸宝妈都是杂合性基因且都贡献单眼皮片段,孩子才能是单眼皮。


这里做一下假设,全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。即



其中S表示双眼皮,D表示单眼皮,C表示纯合基因,Z表示杂合基因。


祖父辈人基因类型的后验概率


父母双方的情况是对等的,因此只挑选其中一方进行计算。以父亲为例,爷爷奶奶可能的基因类型组合有:CC,CZ和ZZ。先验概率为:



已知父亲是双眼皮,则爷爷奶奶是CC组合的后验概率为:



类似地可以算出



进行归一化后得到:



这里之所以要进行归一化,是因为在计算过程中对概念进行了替换,我们利用了爷爷奶奶都是双眼皮的信息。


父母基因类型的概率


仍然以父亲为对象进行计算,其是纯合基因的概率为:



是杂合基因的概率为:



剩下1/20的概率是表现为单眼皮的概率,需要排除掉。也就是说,在观察到爷爷奶奶父亲都是双眼皮的情况下,父亲是纯合基因的概率为






































,是杂合基因的概率为







































孩子双眼皮的概率


综合以上,孩子是双眼皮的概率为:



可见这个概率是很高的。


压力测试


在之前的计算里面,由于没有一般人群的统计数据,我们假设全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。这里我们换一组数据,假设全部人群中有1/4是双眼皮,双眼皮人群中纯合基因有1/4,看看这样会对结果造成多大影响。


这种情况下



父母任意一方是纯合基因的概率为







































,是杂合基因的概率为









































最终得到孩子是双眼皮的概率是



可见概率依然非常高。“六个钱包”都是双眼皮是一个非常可靠的信号。


作者:Kelly1024
来源:mdnice.com/writing/f71210a4999f4bd2a674f9bead00551c
收起阅读 »

原来,我们的代码就是这样被污染的...

最近 CR 了这样一段代码: 我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在...
继续阅读 »

最近 CR 了这样一段代码:


插图1.png


我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在想,为什么会这样?


先探究一下这个问题的表面原因,让我们从底层开始:



  • DB 表结构按照团队规范,字段名是下划线分隔的

  • node 服务定义 entity 的时候,直接复用 DB 表结构字段名

  • service 模块在查询 DB 之后,直接回吐结果,所以 service 类返回的数据结构的变量名也是下划线的

  • node api 的 controller 通过调用 service 模块的函数,获取数据处理之后,也直接回吐下划线的变量名的数据结构

  • 因为 node api 的接口协议是下划线的,所以 web 页面请求 http api 的出入参也是下划线的,如上图所示

  • 因为 http api 返回的结果是下划线的,然后页面逻辑直接使用,最后,页面逻辑也出现了大量下划线


这是一段挺长的链路,只要我们在后面的任意一个环节处理一下变量名的转换,都能避免这个问题,但是并没有。


插图2.png


为什么会这样?稍微探究一下这里的深层原因,也是挺有意思的。


首先,项目启动时没有严格把控代码质量。这里的原因有很多,比如项目工期紧、主要以完成功能为主、内部项目不需要要求这么严格、项目启动时是直接 copy 另外一个项目的,那个项目也是这样写的等等。但是,这些统统都是借口!我是要负主要责任的。


其次,没有开发同学想过要去优化这里的代码。参与这个项目的同学并不是不知道如何去解决这个问题,但就是没人想要去解决这个问题。大家更多的是选择“入乡随俗”,别人这么写,我也这么写吧。


最后,Code Review 没有严抓。问题都是越早处理成本越小,如果在早期我们就开始严抓 Code Review 的话,说不定就能及时改善这个问题了。


这个案例是一个非常真实的破窗效应案例,这里面有不少地方值得我们深思的。


首先,它会污染整个项目。如果我们在项目的一开始就没有把控好代码质量,那我们的项目代码很快就会被污染。最开始的开发同学可能知道历史原因,自然不会觉得不自然。后续加入的维护者看到项目代码是这样子的,就会误以为这个项目的代码规范就是这样子的,于是也“入乡随俗”,最终整个项目的代码就被污染,破烂不堪。


其次,它会污染依赖的项目。比如这里就是 node 项目先被污染,对外提供的 api 也受到污染,然后调用这些 api 的 web 项目也被污染了。很多时候,api 的接口协议都是由后端开发来定,如果碰到一些缺乏经验的后端开发,前端同学会收到一些很奇怪的接口协议,比如字段命名规范不统一,冗余字段,设计不合理等等,由于前端的话语权较弱的缘故,很可能会被动接受,如果处理不当,前端项目的代码就会受到污染了。


最后,它会污染整个开发团队!初创的开发同学就不说了;后续的维护者看到代码是这样子的,不是错误认知这个项目的代码规范,就是错误认知团队的代码规范;更为糟糕的是,阅读项目源码的其他同学也会受到污染:“哦,原来别人也是这样写代码的”。噩梦由此而生...


插图3.png


破窗就像病毒一样,快速并疯狂地污染整个项目代码,然后传染给开发团队,最后扩散到其他团队。这个病毒传染的范围很广,速度很快,一不留神,整个团队就会沦陷。那我们应该怎样医治呢? 主要有 3 个方向:


首先,增强个人免疫力。形成个人的编码风格,然后坚持它,这是治本的良方。良好的编码风格是不会损坏个人的编码效率的,反而会有助于提升个人的研发效率,主要体现在以下几个方面:



  • 减少低级错误

  • 提升代码的可阅读性,提升代码的可维护性,从而提升团队协作效率

  • 形成“肌肉记忆”,提升编码效率

  • 避免返工,主要体现在糟糕的设计问题和编码风格冲突问题


其次,做好防护,避免传播。比如我们这个简单的案例,在整个链路的任何一个环节,都能轻松处理这个问题,这样就不会传播到后面的环节了。很多人碰到一些历史破窗代码时,可能会觉得修复这些破窗成本太大了,那就不要去修复,只需要保证自己写的新代码是良好的,也是一个不错的方案。已经中毒的人我们没有能力医治的时候,起码,我们可以认真做好防护,避免病毒进一步传播!


最后,对症下药医治。只有千日做贼,那有千日防贼,我们总是需要想办法医治这个病,而治病的药方就是重构。这里就不深入讲了,重构也是个大学问,只需要知道,越早重构,成本越低,比如在 Code Review 的时候就要严抓这些问题,并跟踪修复情况。


不知道,你的“免疫力”修炼得怎么样了?



【讨论问题】


除了这里的案例,还有很多其他类型的破窗,你都碰到了哪些?


欢迎在评论区分享你的想法,一起讨论。


作者:潜龙在渊灬
来源:juejin.cn/post/7252888158828642360
收起阅读 »

面试官:“只会这一种懒加载实现思路?回去等通知吧”

web
思路一:监听滚动事件 监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。 关键 API getBo...
继续阅读 »

思路一:监听滚动事件



监听滚动事件指的是:通过监听页面的滚动事件,判断需要懒加载的元素是否进入可视区域。当元素进入可视区域时,动态加载对应的资源。这种方式需要手动编写监听滚动事件的逻辑,可能会导致性能问题,如滚动时的抖动和卡顿。



关键 API



  • getBoundingClientRect() 方法返回的对象包含以下属性:



  • top:元素上边缘相对于视窗的距离。

  • right:元素右边缘相对于视窗的距离。

  • bottom:元素下边缘相对于视窗的距离。

  • left:元素左边缘相对于视窗的距离。

  • width:元素的宽度(可选)。

  • height:元素的高度(可选)。


可以看下下面的图。
image.png


来看看代码示例


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Exampletitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function isInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;

return (
rect.
top >= 0 &&
rect.
left >= 0 &&
rect.
bottom <= windowHeight &&
rect.
right <= windowWidth
);
}

function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
for (const img of images) {
if (isInViewport(img)) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}
}
}

window.addEventListener('scroll', lazyLoad);
window.addEventListener('resize', lazyLoad);
window.addEventListener('DOMContentLoaded', lazyLoad);

script>
body>
html>

这里的思路就相对简单了,核心思路就是判断这个元素在不在我的视口里面。


同样的思路下面还有另一种实现方式,是用 offsetTopscrollTopinnerHeight做的,这个很常见,我们就不说了。


思路二:Intersection Observer API



这是一种现代浏览器提供的原生 API,用于监控元素与其祖先元素或顶级文档视窗(viewport)的交叉状态。当元素进入可视区域时,会自动触发回调函数,从而实现懒加载。相比于监听滚动事件,Intersection Observer API 更高效且易于使用。



关键 API



  • Intersection Observer API:创建一个回调函数,该函数将在元素进入或离开可视区域时被调用。回调函数接收两个参数:entries(一个包含所有被观察元素的交叉信息的数组)和 observer(观察者实例)。


其中 entries 值得一提,entries 是一个包含多个 IntersectionObserverEntry 对象的数组,每个对象代表一个观察的元素(target element)与根元素(root element)相交的信息。IntersectionObserverEntry 对象包含以下属性:



  1. intersectionRatio: 目标元素和根元素相交区域占目标元素总面积的比例,取值范围为 0 到 1。

  2. intersectionRect: 目标元素和根元素相交区域的边界信息,是一个 DOMRectReadOnly 对象。

  3. isIntersecting: 布尔值,表示目标元素是否正在与根元素相交。

  4. rootBounds: 根元素的边界信息,是一个 DOMRectReadOnly 对象。

  5. target: 被观察的目标元素,即当前 IntersectionObserverEntry 对象所对应的 DOM 元素。

  6. time: 观察到的相交时间,是一个高精度时间戳,单位为毫秒。


这个最近我们组里的姐姐封了一个懒加载组件,通过单例模式 + Intersection Observer API,然后在外部控制 v-if 就好了,非常 nice。


由于代码保密性,我这里只能提供一个常规实现方法qwq


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lazy Loading Example - Intersection Observertitle>
<style>
img {
width: 100%;
height: 300px;
display: block;
}
style>
head>
<body>
<img data-src="https://via.placeholder.com/300x300?text=Image+1" src="" alt="Image 1">
<img data-src="https://via.placeholder.com/300x300?text=Image+2" src="" alt="Image 2">
<img data-src="https://via.placeholder.com/300x300?text=Image+3" src="" alt="Image 3">
<script>
function loadImage(img) {
img.
src = img.getAttribute('data-src');
img.
removeAttribute('data-src');
}

function handleIntersection(entries, observer) {
entries.
forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.
unobserve(entry.target);
}
});
}

function lazyLoad()
script>
body>
html>


思路三:虚拟列表



对于长列表数据,可以使用虚拟列表技术来实现懒加载。虚拟列表只会渲染当前可视区域内的列表项,当用户滚动列表时,动态更新可视区域内的内容。这种方式可以大幅减少 DOM 节点的数量,提高页面性能。



这个我之前专门有文章聊过:b站面试官:如果后端给你 1w 条数据,你如何做展示?


就不赘述啦~


那么这就是本篇文章的全部内容啦~


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

年中总结:浑浑噩噩

学业 今年好像一直在虚假地忙碌,都是忙给别人看到,自己的进度并没有多少。 会议论文还在审稿,能不能过还是两说,除此之外好像没什么学业进度了。 每当有人问我:忙什么呢? 我就理直气壮地说,“赶论文呢”。 说得多了,自己也就信了,一旦自己都信了,就开始行色匆匆,...
继续阅读 »

学业


今年好像一直在虚假地忙碌,都是忙给别人看到,自己的进度并没有多少。


会议论文还在审稿,能不能过还是两说,除此之外好像没什么学业进度了。



每当有人问我:忙什么呢?


我就理直气壮地说,“赶论文呢”。


说得多了,自己也就信了,一旦自己都信了,就开始行色匆匆,一副“谁也别理我,忙着呢”的架势。……就是靠着这点虚张声势的忙碌,我活的了一种滥竽充数的成人感。




稀里糊涂掉进学术圈子里,好像一个瘸子掉到了一个舞池子里,偏偏又是一个要强的个性,没有科研天分,还要打肿脸充胖子,硬着头皮在这个舞池子里一瘸一拐地跳下去。



感情


又是单身的一年,近乎偏执的纯爱党。身边朋友嘲笑“你想和人从朋友做起,慢慢了解了再在一起,这种现在哪有啊!你只能遇到渣男!!!”


遇到喜欢的男孩子,我去追了,但是我太主动了,人家不喜欢主动的女孩子,其实主要是不喜欢我。最后不了了之了。但我依旧认为,



“女生太主动了,是不是不太好?”


“没什么好不好的,爱情本来就没有什么公式。”



找过很多借口,帮助自己走出来。但是说来说去还是真心喜欢过才会找那么多的借口。



“你说会不会是这样,我其实也并不喜欢他,喜欢的不过是自己的面子?


“也许吧。”但这又说明什么?你可以说“我喜欢的不是他,而是自己的面子”,或者“我喜欢的不是他,我只是在逃避孤独”,或者,“我喜欢的不是真正的他,而是一个想象出来的他” “我喜欢的不是他,而是被人疼爱的那种感觉”·····这样的句子可以无限造下去,但结果殊途同归,就是你在乎。



生活


因为身体和学业问题,一度陷入焦虑。每天过的很差劲,不断内耗。


不想给朋友们倾诉,在他们眼里我永远都是那个元气少女。身边人都说“谁抑郁了,你都不可能抑郁。”


但是那段时间我真的是很难过很难过。我之前也是这样,但是把所有的负面情绪都给了我喜欢的男孩子。可能让人家觉得我就是鱼鱼女孩,其实我并不是。我觉得是我的错,我不该一直跟他倾诉我压力有多大。



传递悲伤这件事本身,就是一种暴力。悲伤不该特地说给别人听。



以至于后来,我真的很舍不得,但是我什么都不敢和他说。我怕这样的想念没有诚意,我怕他觉得我把他当作垃圾桶。所以只能自己解决。



人生若有知己相伴固然妙不可言,但那可遇而不可求,真的,也许既不可遇又不可求,可求的只有你自己,你要俯下身去,朝着幽暗深处的自己伸出手去。



读书


最近摆烂中,最近三个月只看了七本书。是时候放下手机重拾阅读兴趣了。


我是个特别喜欢看爱情小说的人,喜欢看别人的爱情观。但是今年上半年看的微乎其微,主要都是健康、社会、个人成长相关的内容。


比如《中国营养科学全书》,比如《孤独社会》等等。至于小说,国内的作家的只看了刘震云的《一句顶一万句》,我果然是不太喜欢国内的小说,太过沉重了。比如:



一开始觉得没有话说是两人不爱说话,后来发现不爱说话和没话说是两回事。不爱说话是心里还有话,没话说是干脆什么都没有了。




将心腹话说给朋友,没想到朋友一掰,这些自己说过的话,都成了刀子,反过头扎向自己。



推荐一本八嘎国的小说《星期一,喝抹茶》。没什么营养内容,就是适合消磨时间的那种小短文。每一个小故事的配角都会成为下一个小故事的主角,透露着生活中的小确幸。



“不用配合她呀”——我想起了富贵子刚才说的话。她确实没有配合我,她一直坚守着自己认准的道路,并且尊重了我的选择。



展望


生活都是你自己的,自己经历,自己成长,后果也要自己承担。不要再这么浑浑噩噩地过下去了。先做应该做的,

作者:Roche
来源:juejin.cn/post/7265210393046810664
再做喜欢做的事情吧。

收起阅读 »

动态样式去哪儿了?

web
本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约...
继续阅读 »

本文作者是蚂蚁集团前端工程师豆酱,众所周知,antd v5 使用了 CSS-in-JS 技术从而支持混合、动态样式的需求。相对的它需要在运行时生成样式,这会造成一定的性能损耗。因此我们研发了组件库级别的 @ant-design/cssinjs 库,通过一定的约束提升缓存效率,从而达到性能优化的目的。不过我们并不止步于此。我们可以通过一些逻辑,直接跳过运行时生成样式的阶段。


动态样式去哪儿了?


如果你研究过 Ant Design 的官网,你会发现 Ant Design 的组件并没有动态插入 来控制样式,而是通过 CSS 文件来控制样式:

image.png


image.png


document.head 里有几个 css 文件引用:



  • umi.[hash].css

  • style-acss.[hash].css


前者为 dumi 生成的样式内容,例如 Demo 块、搜索框样式等等。而后者则是 SSR 生成的样式文件。在定制主题文档中,我们提过可以通过整体导出的方式将页面中用到的组件进行预先烘焙,从而生成 css 文件以供缓存命中从而提升下一次打开速度。这也是我们在官网中使用的方式。所以 Demo 中的组件,其实就是复用了这部分样式。 等等!CSS-in-JS 不是需要在运行时生成样式的 hash 然后通过 进行对齐的么?为什么 css 文件也可以对齐?不用着急,我们慢慢看。

CSS-in-JS 注水


应用级的 CSS-in-JS 方案会对生成的样式计算出 hash 值,并且将其存入 Cache 中。当下次渲染时,会先从 Cache 中查找是否存在对应的样式,如果存在则直接使用,否则再生成一次。这样就可以避免重复生成样式,从而提升性能。


image.png


每个动态插入到页面中的样式同样以为 hash 作为唯一标识符。如果页面中已经存在该 hash 的 ,则说明 SSR 中做过 inline style 注入。那么 就不用再次创建。 你可以发现,虽然 的节点创建可以省略,但是因为 hash 依赖于计算出的样式内容。所以即便页面中已经有可以复用的样式内容,它仍然免不了需要计算一次。实属不划算。

组件级 CSS-in-JS


组件级别的 CSS-in-JS 一文中,我们提过。Ant Design 的 Cache 机制并不需要计算出完整的样式。对于组件库而言,只要通过 Token 和 ComponentName 就可以确定生成样式一致性,所以我们可以提前计算出 hash 值: 也因此,我们发现可以复用这套机制,实现在在客户端侧感知组件样式是否已经注入过。


SSR HashMap


在 @ant-design/cssinjs 中,Cache 本身包含了每个元素对应的 style 和 hash 信息。过去的 extractStyle 方法只取 Cache 中 style 的内容进行封装:


// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE"":where(.bAMbOo).ant-btn { color: red }"],

  "bAMbOo|Spin": ["liGHt"":where(.bAMbOo).ant-spin { color: blue }"]

}

提取:


:where(.bAMbOo).ant-btn {

  color: red;

}

:where(.bAMbOo).ant-spin {

  color: blue;

}

为了复用样式,我们更进一步。将 path 和 hash 值也进行了抽取:


{

  "bAMbOo|Button": "LItTlE",

  "bAMbOo|Spin""liGHt"

}

并且也打成 css 样式:


// Just example. Not real world code

.cssinjs-cache-path {

  content'bAMbOo|Button:LItTlE;bAMbOo|Spin:liGHt';

}

这样 SSR 侧就将我们所需的信息全部留存了下来,接下去只需要在客户端进行提取即可。


CSR HashMap


在客户端则简单的多,我们通过 getComputedStyle 提取 HashMap 信息留存即可:


// Just example. Not real world code

const measure = document.createElement('div');

measure.className = 'cssinjs-cache-path';

document.body.appendChild(measure);

 

// Now let's parse the `content`

const { content } = getComputedStyle(measure);

在组件渲染阶段,useStyleRegister 在计算 CSS Object 之前,会先在 HashMap 中查找 path 是否存在。如果存在,则说明该数据已经通过服务端生成。我们只需要将样式从现有的 里提取出来即可:

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""READ_FROM_INLINE_STYLE"],

  "bAMbOo|Spin": ["liGHt""READ_FROM_INLINE_STYLE"]

}

而对于 CSS 文件提供的样式(比如官网的使用方式),它不像 会被移除,我们直接标记为来自于 CSS 文件即可。和 inline style 一样,它们会在 useInsertionEffect 阶段被跳过。

// e.g. Real world path is much more complex

{

  "bAMbOo|Button": ["LItTlE""__FROM_CSS_FILE__"],

  "bAMbOo|Spin": ["liGHt""__FROM_CSS_FILE__"]

}

总结


CSS-in-JS 因为运行时的性能损耗而被人诟病。而在 Ant Design 中,如果你的应用使用了 SSR,那么在客户端侧就可以直接跳过运行时生成样式的阶段从而提升性能。当然,我们会继续跟进 CSS-in-JS 的发展,

作者:支付宝体验科技
来源:juejin.cn/post/7265249262329839672
为你带来更好的体验。

收起阅读 »