注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一文理解贝塞尔曲线

贝塞尔曲线的来源 贝塞尔曲线最早是由贝塞尔在1962年提出来的,目的是获取汽车的外形。贝塞尔曲线看上去非常复杂,其实想法非常简单,(如下图1所示)就是先用折线先绘制出大致的轮廓,然后用曲线来逼近。 图1 这个方式是不是有点熟悉,刚看到的时候,我就想到了计算圆...
继续阅读 »

贝塞尔曲线的来源


贝塞尔曲线最早是由贝塞尔在1962年提出来的,目的是获取汽车的外形。贝塞尔曲线看上去非常复杂,其实想法非常简单,(如下图1所示)就是先用折线先绘制出大致的轮廓,然后用曲线来逼近。




图1


这个方式是不是有点熟悉,刚看到的时候,我就想到了计算圆的面积时,我们会使用多边形来逼近圆的曲线(如图2所示);贝塞尔曲线刚好相反,它是使用曲线来逼近多边形,刚好反着来了😂。




图2


构造贝塞尔曲线


思路虽然简单,但是如何把这个曲线画出来,或者说如何用一个函数来表示这条曲线就很困难了。不过这个不需要我们关心,有大佬已经解决了。我们直接来看看贝塞尔曲线的定义式,如下图3:




图3


先别急着划走,这个公式不用记,因为它太复杂而且计算量大,因此在工程开发中我们不会用它。一般在工程中,我们使用德卡斯特里奥算法(de Casteljau) 来构造贝塞尔曲线。听起来更复杂了,别急让我们举个🌰。下面以2次贝塞尔曲线为例。




图4




图5


看图4,德卡斯特里奥算法(de Casteljau) 的意义就是满足P0Q0P0P1=P1Q1P1P2=Q0BQ0Q1=t  \frac{P_0Q_0}{P_0P_1} = \frac{P_1Q_1}{P_1P_2} = \frac{Q_0B}{Q_0Q_1} = t 的情况下,随着 t 从 0 到 1 逐渐变大,B点经过的点组成的曲线就是我们需要的贝塞尔曲线了。图5是我们常见的动图,之前看的时候一直很懵逼,现在了解了贝塞尔曲线是如何画出来的,是不是清楚多了。


更高阶的贝塞尔曲线绘制方式和上面的一样,只是多了几条边,绘制的动图如下:




3次贝塞尔曲线




4次贝塞尔曲线




5次贝塞尔曲线


贝塞尔曲线的函数表示


看到这里,我们已经对贝塞尔曲线有了一个大概的了解。但是还是一个关键的问题,我们怎么画出贝塞尔曲线呢?或者说有什么函数可以让我们画出这个曲线吗?这个其实更简单,我们高中就学过了。还是以二次贝塞尔曲线为例,它的参数方程如下,其中 P0、P1、P2代表控制点。




我们假设三个控制点的坐标是 P0 (-1, 0)、 P1 (0, 1) 、P2 (1, 0),把值带入上面的参数方程,就可以得到如下结果:


(xy)=(1t)2(10)+2t(1t)(01)+t2(10)\left(\begin{array}{c}x\\ y\end{array}\right) = (1 - t)^{2} \left(\begin{array}{c}-1\\ 0\end{array}\right)
+ 2t(1 - t) \left(\begin{array}{c}0\\ 1\end{array}\right) + t^{2} \left(\begin{array}{c}1\\ 0\end{array}\right)

(xy)=((12t)2+t22t(1t))\left(\begin{array}{c}x\\ y\end{array}\right) = \left(\begin{array}{c}-(1 - 2t)^{2} + t ^ 2\\ 2t(1 - t)\end{array}\right)

{x=2t1y=2t2+2t\begin{cases} x = 2t - 1 \\ y = -2t^2 + 2t\end{cases}

最后化解可得到我们熟悉的 y = f(x) 函数y=12x2+12:y = -\frac{1}{2}x^2 + \frac{1}{2} 效果图如下图。可以看出二次贝塞尔曲线实际上就是我们高中学的抛物线。唯一不同的是,我们高中求的抛物线,会经过 P0、P1、P2三个点,而贝塞尔曲线只会经过 P0、P1两个端点。




类似的:


一次贝塞尔曲线就是一次函数y=a0x+a1:y = a_0x + a_1


三次贝塞尔曲线就是三次函数:y=a0x3+a1x2+a2x+a3y = a_0x^3 + a_1x^2 + a_2x + a_3


四次贝塞尔曲线就是四次函数:y=a0x4+a1x3+a2x2+a3x+a4y = a_0x^4 + a_1x^3 + a_2x^2 + a_3x + a_4


n次贝塞尔曲线就是n次函数:y=a0xn+a1xn1+...+an y = a_0x^n + a_1x^{n-1} + ... + a_{n}


总结


贝塞尔曲线实际上并不复杂,我们可以简单的把n次贝塞尔曲线看成对应的n次函数的曲线。因为贝塞尔曲线的这个特点,也造成了贝塞尔曲线的最大缺陷————不能局部修改,即改变其中一个参数时会改变整条曲线。后面为了解决贝塞尔曲线的这个问题,提出了B样条曲线,下篇文章我们就介绍B样条曲线。


最后这篇文章为了方便读者的理解,省略了很多贝塞尔曲线特性的介绍,如果对贝塞尔曲线感兴趣,可以在B站上看看它的完整课程。


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

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术? 首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?


首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。


那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?


先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。


为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。


对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。


对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。


那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?


所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?



也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。


出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。


那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。


其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。



第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。


我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。


换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。



所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。


那怎么样能够保证你自己有一个技术细节的敏感度?


我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。


当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。


自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。


总结


所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。


当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


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

你会用nginx部署前端项目吗

web
前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。 对于前端项目来说,nginx主要有两个功能: 对静态资源做托管,即作为一个静态资源服务器; 对...
继续阅读 »

前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。


对于前端项目来说,nginx主要有两个功能:



  • 对静态资源做托管,即作为一个静态资源服务器

  • 对动态资源做反向代理,即代理后台接口服务,防止跨域


路由配置


nginx配置最多就是路由配置,路由配置又有几种写法。


1. =


location = /111/ {
default_type text/plain;
return 200 "111 success";
}

location 和路径之间加了个 =,代表精准匹配,也就是只有完全相同的 url 才会匹配这个路由。


image.png


在路径后面添加了aa,那么就不是精确匹配了,所以是404


image.png


2. 不带 =


代表根据前缀匹配,后面可以是任意路径


location /222 {
default_type text/plain;
// 这里的 $uri 是取当前路径。
return 200 $uri;
}

image.png


3. 支持正则匹配~


// 匹配以/333/bbb开头,以.html结尾的路径
location ~ ^/333/bbb.*\.html$ {
default_type text/plain;
return 200 $uri;
}

image.png


上面的~是区分大小写的,如果不区分大小写是~*


// 匹配以/333/bbb开头,以.html结尾的路径
location ~* ^/333/bbb.*\.html$ {
default_type text/plain;
return 200 $uri;
}

4. ^~代表优先级


下面的配置有两个路径是以/444开头的:


location ~* ^/444/AAA.*\.html$ {
default_type text/plain;
return 200 $uri;
}
location ~ /444/ {
default_type text/plain;
return 200 $uri;
}

如果访问/444/AAA45.html,就会直接命中第一个路由,如果我想命中/444/呢? 加上^就好了。


location ^~ /444/ {
default_type text/plain;
return 200 $uri;
}

也就是说 ^~ 能够提高前缀匹配的优先级。


总结一下,location 语法一共有四种:




  1. location = /aaa 是精确匹配 /aaa 的路由;




  2. location /bbb 是前缀匹配 /bbb 的路由。




  3. location ~ /ccc.*.html 是正则匹配,可以再加个 * 表示不区分大小写 location ~* /ccc.*.html;




  4. location ^~ /ddd 是前缀匹配,但是优先级更高。




这 4 种语法的优先级是这样的:


精确匹配(=) > 高优先级前缀匹配(^~) > 正则匹配(~ / ~*) > 普通前缀匹配


root 与 alias


nginx指定文件路径有两种方式rootaliasrootalias主要区别在于nginx如何解释location后面的uri,这会使两者以不同的方式将请求映射到服务器文件上。



  1. root的处理结果是:root路径 + location路径;

  2. alias的处理结果是:使用alias路径替换location路径;


alias是一个目录别名的定义,root则是最上层目录的定义。


需要注意的是alias后面必须要用/结束,否则会找不到文件的,而root则可有可无。另外,alias只能位于location块中。


root示例:


location ^~ /test/ {
root /www/root/html/;
}

如果一个请求的 uri 是 /test/a.html时,web服务器将会返回服务器上的/www/root/html/test/a.html的文件。


alias示例:


location ^~ /test/ {
alias /www/root/html/new_test/;
}

如果一个请求的 uri 是 /test/a.html 时,web服务器将会返回服务器上的/www/root/html/new_test/a.html的文件。


注意, 这里是new_test,因为alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。


二级目录


有时候需要在一个端口下,部署多个项目,那么这时可以采用二级目录的形式来部署。


采用二级目录来部署会有一些坑,比如,当我请求 http://xxxxxxxx.com/views/basedata 的时候,浏览器自动跳转到了http://xxxxxxxxm:8100/store/views/basedata/


这是什么原因呢?


最根本的问题是因为http://xxxxxxxx.com/views/basedata后面没有/,所以就触发了nginx301重定向,重定向到了http://xxxxxxxxm:8100/store/views/basedata/,因此,只要避免触发重定向即可。


如果你能忍受直接使用如 http://xxxxxxxxm:8100/store/views/basedata/ 这样最后带/的地址,那也就没有什么问题了。


那为什么会触发重定向呢?


当用户请求 http.xxxxxx.cn/osp 时,这里的 $uri 就是 /ospnginx 会尝试到alias或 root 指定的目录下找这个文件。


如果存在名为 {alias指定目录}/osp 的文件,注意这里是文件,不是目录,就直接把这个文件的内容发送给用户。


很显然,目录中没有叫 osp 的文件,于是就看 osp/,增加了一个 /,也就是看有没有名为 {alias指定目录}/osp/ 的目录。


即,当我们访问 uri 时,如果访问资源为一个目录,并且 uri 没有以正斜杠 / 结尾,那么nginx 服务就会返回一个301跳转,目标地址就是要加一个正斜杠/


一种最简单的方式就是直接访问一个具体的文件,如 http.xxxxxx.cn/osp/index.html,这样就不会发生重定向了。但是,这样方式不够优雅,每次都要输入完整的文件路径。


另一种方式是调整 nginx 中关于重定向的配置,nginx 重定向中的三个配置:absolute_redirectserver_name_in_redirectport_in_redirect


absolute_redirect通过该指令控制 nginx 发出的重定向地址是相对地址还是绝对地址:


1、如果设置为 off,则 nginx 发出的重定向将是相对的,没有域名和端口, 也就没有server_name_in_redirectport_in_redirect什么事儿了。


image.png


加了这个配置后,尽管也会发生重定向,但是不会在路径上加上域名和端口了。


2、如果设置为 on,则 nginx 发出的重定向将是绝对的;只有 absolute_redirect 设置为 onserver_name_in_redirectport_in_redirect 的设置才有作用。


image.png


nginx 开启 gzip 静态压缩提升效率


gzip 是一种格式,也是一种 linux 下的解压缩工具,我们使用 gzipapp.js 文件压缩后,原始文件就变为了以.gz结尾的文件,同时文件大小从 42571 减小到 11862。


image.png


目前,对静态资源压缩有两种形式:



  • 动态压缩: 服务器在返回任何的静态文件之前,由服务器对每个请求压缩在进行输出。

  • 静态压缩:服务器直接使用现成的扩展名为 .gz 的预压缩文件,直接进行输出。


我们知道 gzipCPU 密集型的,实时动态压缩比较消耗 CPU 资源。为进一步提高 nginx 的性能,我们可以使用静态 gzip 压缩,提前将需要压缩的文件压缩好,当请求到达时,直接发送压缩好的.gz文件,如此就减轻了服务器 CPU 的压力,提高了性能。


因此,我们一般采用静态压缩的方式,实现静态压缩有以下两个步骤:


1. 生成gzip压缩文件


在利用webpack打包的时候,我们就把文件进行压缩,配置如下:


const isProduction = process.env.NODE_ENV === 'production'

if (isProduction) {
config.plugins.push(
new CompressionWebpackPlugin({
// 采用gzip进行压缩
algorithm: 'gzip',
test: /\.js$|\.html$|\.json$|\.css/,
threshold: 10240
})
)
}

可以看到,多生成了一个以.gz结尾的文件,然后把.gz后缀的文件上传到服务器中即可。


image.png


2. 在 nginx 开启支持静态压缩的模块


nginx配置中加上如下配置:


gzip_static on;

如果不加的话,访问的时候就会找不到,报404错误,因为服务器只有.gz的文件,没有原始文件。


总结


前端项目nginx部署主要的配置基本上就是上面提到的这些。


首先是location路由的四种写法;


接着就是分清楚rootalias的区别;


当项目较多时需要使用二级路由时,需要注意重定向的配置;


如果你的项目文件较大,可以开启gzip

作者:小p
来源:juejin.cn/post/7270902621065560120
压缩提升传输效率。

收起阅读 »

一次软考高项的经历分享

1 缘自同事 22年1月,去同事家聚餐,第一次听他说起软考高项,并向我讲述了考过的种种好处,如:认定市级人才,申请租房补贴,获得高级职称等等。 热心的同事还分享了一个他报的网课,不到300块钱,同时鼓励我试试一定能考过,多注意练练字因为有一科考论文写作(通常工...
继续阅读 »

1 缘自同事


22年1月,去同事家聚餐,第一次听他说起软考高项,并向我讲述了考过的种种好处,如:认定市级人才,申请租房补贴,获得高级职称等等。


热心的同事还分享了一个他报的网课,不到300块钱,同时鼓励我试试一定能考过,多注意练练字因为有一科考论文写作(通常工作以后,普遍使用电脑,书写会慢慢退化)


对软考高项一无所知的我,回去查了查:软考高项,全称信息系统项目管理师,是由工信部和人社部组织的,计算机技术与软件专业技术资格考试。通过率大概在15%左右,一年考两次,一次考一天,一天考三科:综合知识、案例分析、论文。每一科满分75分,都超过45分才算合格,还是很有难度的。


2 第一次备考


网上查了很多资料,验证了同事所说,思索再三,终于下定决心,买了书报了课开始学习。万事开头难,1月和2月,忙着年底总结和放假过年,没时间看,直到3月初才算正式开始。


工作后,真正属于自己的时间,并不多:早上(20分钟)、上下班路上(30+30分钟)、晚上(1.5小时),以及不加班的晚上和周末。这样算下来,平均每天可学习3个小时。早上背诵,上下班路上听课程音频,晚上仔细看视频课,记笔记做练习。


很快来到5月,越临近考试越焦虑,觉得很多知识点还没掌握,一点信心都没有。结果因为疫情,考试取消了,自己松了一大口气,不是自己不努力,天意如此。因为惯性,6月虽有放松,但还是坚持每天2小时的学习。7月,公寓搬进了一位同事,晚上不能像之前一样任性学习了。8月和9月,虽然没停止学习,实际却未进入专注的状态。十一之后,紧张了起来,认真学习了大半个月,开始有了点信心。11月5日考完,觉得综合知识能过,案例有风险,论文写了那么多,应该也没问题。12月查分,结果论文才30分,大跌眼镜。



查到分数的那一天,忽然感觉心累,付出了这么多时间,憧憬了这么久,这样的结果令人有些沮丧。


3 第二次备考


12月和1月,疫情肆虐。在阳和阳康之间,转眼到了2月,一直还没开始学。3月,听说要换教材,又慌了神,但视频课还是旧教材,尚未更新完。4月,重新看新教材的视频。5月,铆足了劲学习。5月27日,因为换新教材的事,觉得考不过也没事,享受过程,心态放开了,找考场时还随手拍了一张照。



这次考完,综合知识,尤其是前十几道题,是跟着感觉选的,基本都没复习到。案例题里的数据元,也是闻所未闻。论文,考之前在草纸上列了下结构,字写的比上次漂亮些,画了个表占了很多行,最后的字数反而多了。感觉三科都在边缘徘徊,后来老师讲了讲答案,对了对,感觉这次的关键还是在论文。


7月20日,成绩可以查了,在老四季里一遍又一遍的刷着网络,看着群里的消息,大部分都挂在论文,在20-30分之间,感觉自己这次也悬了,刷了很久终于刷出来了,论文46分,多一分过了,自己成了群里第一个过的。



这是查分时所在的老四季,一家新开的店,这两次软考,真的是跨了一年四季。



4 启示


启示1:去到新的城市,一定花时间了解政府的人才政策。除了埋头工作,有机会多和本地的同事交流,他们随口谈及的地方政策,也许会为自己打开一扇窗户。


启示2:目标确立后,无论路多么漫长,唯有坚持才是不懈的动力。原以为在工作之外,自己抽出时间学习,已经很不容易了,但看到几位群友的分享,感慨真是难外有难。


“第一次考试,挂在论文子题目甘特图上。这次二胎还在哺乳期,明显感觉脑子不够用,时间不够用,大宝需要妈妈,二宝也需要妈妈,鼓起很大勇气才决定二刷!每个起夜哺乳的夜晚都在听视频课,复习时间都是一点点挤出来的,中间有崩溃有大哭有想放弃的时刻,但还是坚持了4个多月…”


“扣除一次疫情不能考,考了7次,孩子没读初中,考到孩子初中毕业的时候终于过了!我抱着孩子哭的稀里哗啦,真的,我在想,如果还不过,我还有什么支撑下去的动力?老天看到了我的坚持,感谢”


“这是我考的第三次,52岁,退休前的任务终于完成”


启示3:以平常心看待运气。这次考试,很多群友的论文都在25-35分之间,群里统计的论文通过率低于18%。其实自己的论文准备的并不充分,感觉一些群友都比自己准备的好,尤其是前几次论文过了而这次没过的,也许这就是难以捉摸的运气吧。


5 后续


8月22日,电子证书可以下载了,真是七夕的好礼物。


在个税申报的app中,上传了证书,估计年末能退3600x20%或3600x25%,能把课程、书本和报名费赚回来。



在省级政府采购平台中,注册评审专家账号并上传了证书,提交后也审核通过了,等着体验一把政府采购项目的评审。

收起阅读 »

当程序员纠结中午应该吃什么,那就用pygame来解决吧

写多了kotlin和android,最近想搞点小东西,于是拿出了长期没有宠爱的python,打算搞个小项目 想想应该写什么,对了,该吃饭了,诶,刚好,写一个能随机选择吃什么的小程序吧,只需要点击按钮,就会随机出现菜谱,然后再点一下,就会得出今天吃什么的结论 思...
继续阅读 »

写多了kotlin和android,最近想搞点小东西,于是拿出了长期没有宠爱的python,打算搞个小项目


想想应该写什么,对了,该吃饭了,诶,刚好,写一个能随机选择吃什么的小程序吧,只需要点击按钮,就会随机出现菜谱,然后再点一下,就会得出今天吃什么的结论


思路是这样的,读入一个txt文件,文件中写满想吃的东西,做到数据和代码区分,然后开始写UI,UI通过按钮点击随机展示美食即可


麻辣香锅
糖醋排骨
红烧肉
...

    import pygame
import random

class App:
   def __init__(self):
       # 初始化 Pygame
       pygame.init()

       # 创建窗口和设置标题
       self.window_size = (600, 300)
       self.window = pygame.display.set_mode(self.window_size)
       pygame.display.set_caption("What to Eat Today")

       # 设置字体对象
       self.font = pygame.font.Font('myfont.ttf', 32)

       # 加载菜单数据
       self.menu = []
       with open("menu.txt", "r") as f:
           for line in f:
               line = line.strip()
               if line != "":
                   self.menu.append(line)
               print(line) # 打印数据

if __name__ == "__main__":
   app = App()


运行一下


image-20230828201918635.png


nice,文件已经读入


这个时候的UI是一闪而过的,因为程序瞬间就执行完毕了,ok,那么我们就需要用一个循环维持UI窗口,然后设置开始选择按键,以及键盘控制按键,同时设置变量


today_menu表示今天吃的东西,


btn_start_stop表示按键文字,


cur_menu表示正处于随机中的美食,当我们按下开始按键时,cur_menu开始变换,当我们按下关闭按键时,cur_menu的数据就赋值到today_menu中,


show_wheel表示当前正处于随机中,还是已经结束了


只要增加一个无限循环,一切就会好起来的



       # 随机选择一道菜
       self.today_menu = ""
       self.btn_start_stop = "start"
       self.cur_menu = ""

       # 游戏主循环
       self.running = True
       self.show_wheel = False


       # 开关程序
       while self.running:
           for event in pygame.event.get():
               if event.type == pygame.QUIT:
                   self.running = False
               elif event.type == pygame.MOUSEBUTTONDOWN:
               ...

               # 增加一个elif 按键s,show_wheel为true, 按下q, show_wheel为false
               elif event.type == pygame.KEYDOWN:
               ...

运行结果


image-20230828202631700.png


现在已经有了窗口,接下来需要在上面画东西了


所用到的就是draw函数



   def draw(self):
       # 绘制界面
       self.window.fill((255, 255, 255))

       # 绘制菜单
       menu_surface = self.font.render(f"Today's Menu: {self.today_menu}", True, (0, 0, 0))
       ...

       # 绘制按钮
       button_size = min(self.window_size[0] // 4, self.window_size[1] // 4)
    ...

       btn_start = self.font.render(f"{self.btn_start_stop}", True, (0, 0, 0))
        # 缩小start 文字字号 以适应按钮
       btn_start = pygame.transform.scale(btn_start, (button_size // 3 * 2, button_size // 3 * 2))
       self.window.blit(btn_start, (button_x + button_size // 2 - btn_start.get_width() // 2, button_y + button_size // 2 - btn_start.get_height() // 2))

       # 滚轮动画
       ...
       pygame.display.update()

运行


image-20230828202741990.png


上面的代码仅仅能够展示一个静态的页面,


虽然界面平平无奇,似乎只有两行字?不然,实际上暗藏玄🐔,只要我们加上这段,



       # 绘制滚轮动画
       if self.show_wheel:
           wheel_size = min(self.window_size) // 2
           wheel_x = self.window_size[0] // 2 - wheel_size // 2
           wheel_y = self.window_size[1] // 2 - wheel_size // 2
           wheel_rect = pygame.Rect(wheel_x, wheel_y, wheel_size, wheel_size)
...

           # 随机选择并显示菜谱
           menu_index = random.randint(0, len(self.menu) - 1)
           menu_surface = self.font.render(self.menu[menu_index], True, (0, 0, 0))
           self.window.blit(menu_surface, (wheel_x + wheel_size // 2 - menu_surface.get_width() // 2, wheel_y + wheel_size // 2 - menu_surface.get_height() // 2))
           self.cur_menu = self.menu[menu_index]

当我们点击“start"


QQ20230828-203131-HD.gif


发现中间的菜谱动了起来,动了起来!都是大家爱吃的,只需要点击右边的stop就可以固定结果!


真正麻烦的在于那个滚轮动画,可以想见,我们需要额外的一个无限循环,每一帧都要修改cur_menu,同时更新动画中的菜谱,然后点击stop后,将cur_menu赋值给到today_menu,最麻烦的不是这些逻辑,而是位置,滚轮动画的位置设置为窗口正中间,然后画了两条线,看起来更好看,有一种,狙击枪瞄准的感觉


最后,进行简单优化,比如设置定时关闭等,全代码如下,如果你也不知道吃什么,就用这段代码 + 在同目录自定义一个txt文件,把自己想吃的写上去吧



import pygame
import random

class App:
   def __init__(self):
       # 初始化 Pygame
       pygame.init()

       # 创建窗口和设置标题
       self.window_size = (600, 300)
       self.window = pygame.display.set_mode(self.window_size)
       pygame.display.set_caption("What to Eat Today")

       # 设置字体对象
       self.font = pygame.font.Font('myfont.ttf', 32)

       # 加载菜单数据
       self.menu = []
       with open("menu.txt", "r") as f:
           for line in f:
               line = line.strip()
               if line != "":
                   self.menu.append(line)

       # 随机选择一道菜
       self.today_menu = ""
       self.btn_start_stop = "start"
       self.cur_menu = ""

       # 游戏主循环
       self.running = True
       self.show_wheel = False
       self.wheel_count = 0     # 记录滚轮动画播放的帧数


       # 开关程序
       while self.running:
           for event in pygame.event.get():
               if event.type == pygame.QUIT:
                   self.running = False
               elif event.type == pygame.MOUSEBUTTONDOWN:
                   if not self.show_wheel:
                       self.show_wheel = True
                       self.wheel_count = 0  # 点击按钮后重置计数器为0
                       self.btn_start_stop = "stop"
                   else:
                       self.show_wheel = False
                       self.today_menu = self.cur_menu  # 点击停止赋值
                       self.btn_start_stop = "start"

               # 增加一个elif 按键s,show_wheel为true, 按下q, show_wheel为false
               elif event.type == pygame.KEYDOWN:
                   if event.key == pygame.K_s:  # 按下 s 键
                       self.show_wheel = True
                       self.wheel_count = 0  # 重置计数器为0
                   elif event.key == pygame.K_q:  # 按下 q 键
                       self.show_wheel = False
                       self.today_menu = self.cur_menu  # 停止赋值

           self.draw()

   def draw(self):
       # 绘制界面
       self.window.fill((255, 255, 255))

       # 绘制菜单
       menu_surface = self.font.render(f"Today's Menu: {self.today_menu}", True, (0, 0, 0))
       menu_x = self.window_size[0] // 2 - menu_surface.get_width() // 2
       menu_y = self.window_size[1] // 2 - menu_surface.get_height() // 2
       self.window.blit(menu_surface, (menu_x, menu_y))

       # 绘制按钮
       button_size = min(self.window_size[0] // 4, self.window_size[1] // 4)
       button_x = self.window_size[0] - button_size - 20
       button_y = self.window_size[1] - button_size - 20
       button_rect = pygame.Rect(button_x, button_y, button_size, button_size)
       pygame.draw.circle(self.window, (255, 0, 0), (button_x + button_size // 2, button_y + button_size // 2), button_size // 2)
       pygame.draw.rect(self.window, (255, 255, 255), button_rect.inflate(-button_size // 8, -button_size // 8))

       btn_start = self.font.render(f"{self.btn_start_stop}", True, (0, 0, 0))
        # 缩小start 文字字号 以适应按钮
       btn_start = pygame.transform.scale(btn_start, (button_size // 3 * 2, button_size // 3 * 2))
       self.window.blit(btn_start, (button_x + button_size // 2 - btn_start.get_width() // 2, button_y + button_size // 2 - btn_start.get_height() // 2))

       # 绘制滚轮动画
       if self.show_wheel:
           wheel_size = min(self.window_size) // 2
           wheel_x = self.window_size[0] // 2 - wheel_size // 2
           wheel_y = self.window_size[1] // 2 - wheel_size // 2
           wheel_rect = pygame.Rect(wheel_x, wheel_y, wheel_size, wheel_size)
           pygame.draw.circle(self.window, (0, 0, 0), (wheel_x + wheel_size // 2, wheel_y + wheel_size // 2), wheel_size // 2)
           pygame.draw.circle(self.window, (255, 255, 255), (wheel_x + wheel_size // 2, wheel_y + wheel_size // 2), wheel_size // 2 - 10)
           pygame.draw.line(self.window, (0, 0, 0), (wheel_x + 10, wheel_y + wheel_size // 2), (wheel_x + wheel_size - 10, wheel_y + wheel_size // 2))
           pygame.draw.line(self.window, (0, 0, 0), (wheel_x + wheel_size // 2, wheel_y + 10), (wheel_x + wheel_size // 2, wheel_y + wheel_size - 10))

           # 随机选择并显示菜谱
           menu_index = random.randint(0, len(self.menu) - 1)
           menu_surface = self.font.render(self.menu[menu_index], True, (0, 0, 0))
           self.window.blit(menu_surface, (wheel_x + wheel_size // 2 - menu_surface.get_width() // 2, wheel_y + wheel_size // 2 - menu_surface.get_height() // 2))
           self.cur_menu = self.menu[menu_index]
           # 播放一定帧数后停止动画
           self.wheel_count += 1
           if self.wheel_count > 500:
               self.show_wheel = False
               self.today_menu = self.cur_menu  # 自动停止赋值
       pygame.display.update()

if __name__ == "__main__":
   app = App()
作者:小松漫步
来源:juejin.cn/post/7272257829770887223


收起阅读 »

坏了!要长脑子了...这么多前端框架没听过

web
市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。 React 官网链接 React 是一个用于构建用户界面的 JavaScript 库。它由 Faceboo...
继续阅读 »

市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。


React



官网链接


React 是一个用于构建用户界面的 JavaScript 库。它由 Facebook 和一个由个人开发者和公司组成的社区维护。React 可以作为开发单页或移动应用程序的基础。然而,React 只关心将数据呈现给 DOM,因此创建 React 应用程序通常需要使用额外的库来进行状态管理、路由和与 API 的交互。React 还用于构建可重用的 UI 组件。从这个意义上讲,它的工作方式很像 Angular 或 Vue 等 JavaScript 框架。然而,React 组件通常以声明式方式编写,而不是使用命令式代码,这使得它们更容易阅读和调试。正因为如此,许多开发人员更喜欢使用 React 来构建 UI 组件,即使他们不使用它作为整个前端框架。


优点:



  • React 快速而高效,因为它使用虚拟 DOM 而不是操纵真实的 DOM。

  • React 很容易学习,因为它的声明性语法和清晰的文档。

  • React 组件是可重用的,使代码维护更容易。


缺点:



  • React 有一个很大的学习曲线,因为它是一个复杂的 JavaScript 库。

  • React 不是一个成熟的框架,因此它需要使用额外的库来完成许多任务。


Next.js



官网链接


Next.js 是一个 javascript 库,支持 React 应用程序的服务器端渲染。这意味着 next.js 可以在将 React 应用程序发送到客户端之前在服务器上渲染它。这有几个好处。首先,它允许您预呈现组件,以便当用户请求它们时,它们已经在客户机上可用。其次,它允许爬虫更容易地索引你的内容,从而为你的 React 应用程序提供更好的 SEO。最后,它可以通过减少客户机为呈现页面而必须执行的工作量来提高性能。


以下是开发者喜欢 Next.js 的原因:




  • js 使得无需做任何配置就可以轻松地开始服务器端渲染。




  • js 会自动对应用程序进行代码拆分,以便每个页面只在被请求时加载,这可以提高性能。
    缺点:




  • 如果不小心,next.js 会使应用程序代码库变得更复杂,更难以维护。




  • 一些开发人员发现 next.js 的内置特性固执己见且不灵活。




Vue.js



官网链接


Vue.js 是一个用于构建用户界面和单页应用程序的开源 JavaScript 框架。与 React 和 Angular 等其他框架不同,Vue.js 被设计为轻量级且易于使用。Vue.js 库可以与其他库和框架结合使用,也可以作为创建前端 web 应用程序的独立工具使用。Vue.js 的一个关键特性是它的双向数据绑定,当模型发生变化时,它会自动更新视图,反之亦然。这使得它成为构建动态用户界面的理想选择。此外,Vue.js 还提供了许多内置功能,如模板系统、响应系统和事件总线。这些特性使创建复杂的应用程序成为可能,而不必依赖第三方库。因此,Vue.js 已经成为近年来最流行的 JavaScript 框架之一。


优点:



  • Vue.js 很容易学习,因为它的小尺寸和清晰的文档。

  • Vue.js 组件是可重用的,这使得代码维护更容易。

  • 由于虚拟 DOM 和异步组件加载,Vue.js 应用程序非常快。


缺点:



  • 虽然 Vue.js 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。

  • Vue.js 没有像其他框架那样多的库和工具。


Angular



官网链接


Angular 是一个 JavaScript 框架,用于用 JavaScript、html 和 Typescript 构建 web 应用和应用。Angular 是由 Google 创建和维护的。Angular 提供了双向数据绑定,这样对模型的更改就会自动传播到视图。它还提供了一种声明性语法,使构建动态 ui 变得容易。最后,Angular 提供了许多有用的内置服务,比如 HTTP 请求处理,以及对路由和模板的支持。


优点:



  • Angular 有一个庞大的社区和许多可用的库和工具。

  • Angular 很容易学习,因为它有组织良好的文档和清晰的语法。


缺点:



  • 虽然 Angular 很容易学习,但如果你想掌握它的所有特性,它有一个很大的学习曲线。

  • Angular 不像其他一些框架那样轻量级。


Svelte



官网链接


简而言之,Svelte 是一个类似于 React、Vue 或 Angular 的 JavaScript 框架。然而,这些框架使用虚拟 DOM(文档对象模型)来区分视图之间的变化,而 Svelte 使用了一种称为 DOM 区分的技术。这意味着它只更新 DOM 中已更改的部分,从而实现更高效的呈现过程。此外,Svelte 还包括一些其他框架没有的内置优化,例如自动批处理 DOM 更新和代码分割。这些特性使 Svelte 成为高性能应用程序的一个很好的选择。


优点:




  • Svelte 有其他框架没有的内置优化,比如代码分割。




  • 由于其清晰的语法和组织良好的文档,Svelte 很容易学习。
    缺点:




  • 虽然 Svelte 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • Svelte 没有像其他框架那样多的库和工具。




Gatsby



官网链接


Gatsby 是一个基于 React 的免费开源框架,可以帮助开发人员构建快速的网站和应用程序。它使用尖端技术,使建立网站和应用程序的过程更加高效。它的一个关键特性是能够预取资源,以便在需要时立即可用。这使得盖茨比网站非常快速和响应。使用 Gatsby 的另一个好处是,它允许开发人员使用 GraphQL 查询来自任何来源的数据,从而使构建复杂的数据驱动应用程序变得容易。此外,Gatsby 附带了许多插件,使其更易于使用,包括用于 SEO,分析和图像优化的插件。所有这些因素使 Gatsby 成为构建现代网站和应用程序的一个非常受欢迎的选择。


优点:




  • 由于使用了预取,Gatsby 网站的速度和响应速度非常快。




  • 由于对 GraphQL 的支持,Gatsby 使构建复杂的数据驱动应用程序变得容易。




  • Gatsby 附带了许多插件,使其更易于使用。
    缺点:




  • 虽然 Gatsby 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • Gatsby 没有像其他框架那样多的库和工具。




Nuxt.js



官网链接


js 是一个用于构建 JavaScript 应用程序的渐进式框架。它基于 Vue.js,并附带了一组工具和库,可以轻松创建可以在服务器端和客户端呈现的通用应用程序。js 还提供了一种处理异步数据和路由的方法,这使得它非常适合构建高度交互的应用程序。此外,nuxt.js 附带了一个 CLI 工具,可以很容易地构建新项目并构建、运行和测试它们。使用 nuxt.js,您可以创建快速、可靠和可扩展的令人印象深刻的 JavaScript 应用程序。


优点:




  • js 易于使用和扩展。




  • 由于服务器端渲染,nuxt.js 应用程序快速响应。
    缺点:




  • 虽然 nuxt.js 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




  • nuxt.js 没有像其他框架那样多的库和工具。




Ember.js



官网链接


Ember.js 以其优于配置的约定方法而闻名,这使得开发人员更容易开始使用该框架。它还为数据持久化和路由等常见任务提供了内置库,从而加快了开发速度。尽管 Ember.js 有一个陡峭的学习曲线,但它为开发人员提供了创建富 web 应用程序的灵活性和强大功能。如果你正在寻找一个前端 JavaScript 框架来构建 spa, Ember.js 绝对值得考虑。


优点:




  • Ember.js 使用约定而不是配置,这使得它更容易开始使用框架。




  • Ember.js 为数据持久化和路由等常见任务提供了内置库。




  • Ember.js 为开发人员创建富 web 应用程序提供了很大的灵活性和能力。
    缺点:




  • Ember.js 有一个陡峭的学习曲线。




  • Ember.js 没有像其他框架那样多的库和工具。




Backbone.js



官网链接


Backbone.js 是一个轻量级 JavaScript 库,允许开发人员创建单页面应用程序。它基于模型-视图-控制器(MVC)体系结构,这意味着它将数据和逻辑从用户界面中分离出来。这使得代码更易于维护和扩展,也使创建复杂的应用程序变得更容易。Backbone.js 还包含了许多使其成为开发移动应用程序的理想选择的特性,例如将数据绑定到 HTML 元素的能力以及对触摸事件的支持。因此,对于想要创建快速响应的应用程序的开发人员来说,Backbone.js 是一个受欢迎的选择。


优点:




  • js 是轻量级的,只是一个库,而不是一个完整的框架。




  • js 很容易学习和使用。




  • Backbone.js 具有很强的可扩展性,可以使用许多第三方库。
    缺点:




  • Backbone.js 不像其他框架那样提供那么多的内置功能。




  • 与其他一些框架相比,Backbone.js 只有一个小社区。




结论


总之,虽然有许多不同的 JavaScript 框架可供选择,但最流行的框架仍然相对稳定。每一种都有自己的优点和缺点,开发人员在决定在他们的项目中使用哪一种时必须权衡。虽然没有一个框架是完美的,但每个框架都有一些可以使开发更容易或更快的东西。


在选择框架时,每个人都应该考虑他们项目的具体需求,以及他们团队的技能和他们必须投入学习新框架的时间。通过考虑所有这些因素,您可以为您的项目选择最好的 JavaScript 框架!


参考链接:
blog.risingstack.com/

best-javasc…

收起阅读 »

程序员要学会“投资知识”

啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢? 然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。 不幸的是,它们是有限的资产。随着新技术的...
继续阅读 »

啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢?


然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。


不幸的是,它们是有限的资产。随着新技术的出现和语言环境的发展,你的知识可能会过时。不断变化的市场力量可能会使你的经验变得陈旧和无关紧要。考虑到技术和社会变革的加速步伐,这可能会发生得特别迅速。


随着你的知识价值的下降,你在公司或客户那里的价值也会降低。我们希望阻止所有这些情况的发生。


学习新知识的能力是你最关键的战略资产。但如何获取学习的方法,知道要学什么呢?


知识投资组合。


我们可以将程序员对计算过程、其工作应用领域的了解以及所有经验视为他们的知识投资组合。管理知识投资组合与管理金融投资组合非常相似:


1、定期的投资者有定期投资的习惯。


2、多样化是长期成功的关键。


3、聪明的投资者在投资组合中平衡保守和高风险高回报的投资。


4、投资者在低点买入,在高点卖出以获取最大回报。


5、需要定期审查和重新平衡投资组合。


为了在职业生涯中取得成功,你必须遵循相同的指导原则管理你的知识投资组合。


好消息是,管理这种类型的投资就像任何其他技能一样 - 它可以被学会。诀窍是从一开始就开始做,并养成习惯。制定一个你可以遵循并坚持的例行程序,直到它变成第二天性。一旦达到这一点,你会发现自己自动地吸收新的知识。


建立知识投资组合。


· 定期投资。 就像金融投资一样,你需要定期地投资你的知识投资组合,即使数量有限。习惯本身和总数量一样重要,所以设定一个固定的时间和地点 - 这有助于你克服常见的干扰。下一部分将列出一些示例目标。


· 多样化。 你知道的越多,你变得越有价值。至少,你应该了解你目前工作中特定技术的细节,但不要止步于此。计算机技术变化迅速 - 今天的热门话题可能在明天(或至少不那么受欢迎)变得几乎无用。你掌握的技能越多,你的适应能力就越强。


· 风险管理。 不同的技术均匀地分布在从高风险高回报到低风险低回报的范围内。把所有的钱都投资在高风险的股票上是不明智的,因为它们可能会突然崩盘。同样,你不应该把所有的钱都投资在保守的领域 - 你可能会错过机会。不要把你的技术鸡蛋都放在一个篮子里。


· 低买高卖。 在新兴技术变得流行之前开始学习可能就像寻找被低估的股票一样困难,但回报可能同样好。在Java刚刚发明出来后学习可能是有风险的,但那些早期用户在Java变得流行时获得了可观的回报。


· 重新评估和调整。 这是一个动态的行业。你上个月开始研究的时髦技术可能现在已经降温了。也许你需要刷新一下你很久没有使用过的数据库技术的知识。或者,你可能想尝试一种不同的语言,这可能使你在新的角色中处于更好的位置......


在所有这些指导原则中,下面这个是最简单实施的。


(程序员的软技能:ke.qq.com/course/6034346)


定期在你的知识投资组合中进行投资。


目标。


既然你有了一些指导原则,并知道何时添加什么到你的知识投资组合中,那么获取构成它的智力资产的最佳方法是什么呢?以下是一些建议:


· 每年学习一门新语言。


不同的语言以不同的方式解决相同的问题。学习多种不同的解决方案有助于拓宽你的思维,避免陷入常规模式。此外,由于充足的免费资源,学习多门语言变得更加容易。


· 每月阅读一本技术书籍。


尽管互联网上有大量的短文和偶尔可靠的答案,但要深入理解通常需要阅读更长的书籍。浏览书店页面,选择与你当前项目主题相关的技术书籍。一旦养成这个习惯,每月读一本书。当你掌握了所有当前使用的技术后,扩大你的视野,学习与你的项目无关的东西。


· 也阅读非技术书籍。


请记住,计算机是被人类使用的,而你所做的最终是为了满足人们的需求 - 这是至关重要的。你与人合作,被人雇佣,甚至可能会面临来自人们的批评。不要忘记这个方程式的人类一面,这需要完全不同的技能(通常被称为软技能,听起来可能很容易,但实际上非常具有挑战性)。


· 参加课程。


在当地大学或在线寻找有趣的课程,或者你可能会在下一个商业博览会或技术会议上找到一些课程。


· 加入当地的用户组和论坛。


不要只是作为观众成员;要积极参与。孤立自己对你的职业生涯是有害的;了解你公司之外的人在做什么。


· 尝试不同的环境。


如果你只在Windows上工作,花点时间在Linux上。如果你对简单的编辑器和Makefile感到舒适,尝试使用最新的复杂IDE,反之亦然。


· 保持更新。


关注不同于你当前工作的技术。阅读相关的新闻和技术文章。这是了解使用不同技术的人的经验以及他们使用的特定术语的极好方式,等等。


持续的投资是至关重要的。一旦你熟悉了一门新的语言或技术,继续前进并学习另一门。


无论你是否在项目中使用过这些技术,或者是否应该将它们放在你的简历上,都不重要。学习过程将拓展你的思维,开启新的可能性,并赋予你在处理任务时的新视角。思想的跨领域交流是至关重要的;尝试将你所学应用到你当前的项目中。即使项目不使用特定的技术,你仍然可以借鉴其中的思想。例如,理解面向对象编程可能会导致你编写更具结构的C代码,或者理解函数式编程范 paradigms 可能会影响你如何处理Java等等。


学习机会。


你正在狼吞虎咽地阅读,始终站在你领域的突破前沿(这并不是一项容易的任务)。然而,当有人问你一个问题,你真的不知道的时候,不要停在那里 - 把找到答案当做一个个人挑战。问问你周围的人或在网上搜索 - 不仅在主流圈子中,还要在学术领域中搜索。


如果你自己找不到答案,寻找能够找到答案的人,不要让问题无解地悬而未决。与他人互动有助于你建立你的人际网络,你可能会在这个过程中惊喜地找到解决其他无关问题的方法 - 你现有的知识投资组合将不断扩展。


所有的阅读和研究需要时间,而时间总是不够的。因此,提前准备,确保你在无聊的时候有东西可以阅读。在医院排队等候时,通常会有很好的机会来完成一本书 - 只需记得带上你的电子阅读器。否则,你可能会在医院翻阅旧年鉴,而里面的折叠页来自1973年的巴布亚新几内亚。


批判性思维。


最后一个要点是对你阅读和听到的内容进行批判性思考。你需要确保你投资组合中的知识是准确的,没有受到供应商或媒体炒作的影响。小心狂热的狂热分子,他们认为他们的观点是唯一正确的 - 他们的教条可能不适合你或你的项目。


不要低估商业主义的力量。搜索引擎有时只是优先考虑流行的内容,这并不一定意味着这是你最好的选择;内容提供者也可以支付费用来使他们的材料排名更高。书店有时会将一本书突出地摆放,但这并不意味着它是一本好书,甚至可能不受欢迎 - 这可能只是有人支付了那个位置。


(程序员的软技能:ke.qq.com/course/6034346)


批判性分析你所阅读和听到的内容。


批判性思维本身就是一个完整的学科,我们鼓励你深入研究和学习这门学科。让我们从这里开始,提出一些发人深省的问题。


· 五问“为什么”。


我最喜欢的咨询技术之一是至少连续问五次“为什么”。这意味着在得到一个答案后,你再次问“为什么”。像一个坚持不懈的四岁孩子提问一样重复这个过程,但请记住要比孩子更有礼貌。这样做可以让你更接近根本原因。


· 谁从中受益?


尽管听起来可能有点功利主义,但追踪金钱的流动往往可以帮助你理解潜在的联系。其他人或其他组织的利益可能与你的利益保持一致,也可能不一致。


· 背景是什么?


一切都发生在自己的背景下。这就是为什么声称“解决所有问题”的解决方案通常站不住脚,宣扬“最佳实践”的书籍或文章经不起审查的原因。 “对谁最好?” 是一个需要考虑的问题,以及关于前提条件、后果以及情况是短期还是长期的问题。


· 在何种情况下和何地可以起作用?


在什么情况下?是否已经太晚了?是否还太早了?不要只停留在一阶思维(接下来会发生什么);参与到二阶思维中:接下来会发生什么?


· 为什么这是一个问题?


是否有一个基础模型?这个基础模型是如何工作的?


不幸的是,如今找到简单的答案是具有挑战性的。然而,通过广泛的知识投资组合,并对你遇到的广泛技术出版物进行一些批判性分析,你可以理解那些复杂的答案。



作者:用心看世界Heart
来源:juejin.cn/post/7271908000414580776

收起阅读 »

兄弟,王者荣耀的段位排行榜是通过Redis实现的?

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。 作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助? 看看我的排名,你就知道了,答案是否定的,哈哈。 一、排行榜设计方案 从技...
继续阅读 »

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。


作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助?



看看我的排名,你就知道了,答案是否定的,哈哈。




一、排行榜设计方案


从技术角度而言,我们可以根据排行榜的类型来选择不同技术方案来进行排行榜设计。


1、数据库直接排序


在低数据量场景中,用数据库直接排序做排行榜的,有很多。


举个栗子,比如要做一个程序员薪资排行榜,看看哪个城市的程序员最有钱。


根据某招聘网站的数据,2023年中国国内程序员的平均月薪为1.2万元,其中最高的是北京,达到了2.1万元,最低的是西安,只有0.7万元。


以下是几个主要城市的程序员平均月薪排行榜:



  1. 北京:2.1万元

  2. 上海:1.9万元

  3. 深圳:1.8万元

  4. 杭州:1.6万元

  5. 广州:1.5万元

  6. 成都:1.3万元

  7. 南京:1.2万元

  8. 武汉:1.1万元

  9. 西安:0.7万元


从这个榜单中可以看出,我拖了大家的后腿,抱歉了。



这个就可以用数据库来做,一共也没有多少个城市,来个百大,撑死了。


对于这种量级的数据,加好索引,用好top,都不会超过100ms,在请求量小、数据量小的情况下,用数据库做排行榜是完全没有问题的。


2、王者荣耀好友排行


这类榜单是根据自己好友数据来进行排行的,这类榜单不用将每位好友的数据都存储在数据库中,而是通过获取自己的好友列表,获取好友的实时分数,在客户端本地进行本地排序,展现出王者荣耀好友排行榜,因为向数据库拉取数据是需要时间的,比如一分钟拉取一次,因为并非实时拉取,这类榜单对数据库的压力还是较小的。



下面探索一下在Java中使用Redis实现高性能的排行榜是如何实现的?



二、Redis实现计数器


1、什么是计数器功能?


计数器是一种常见的功能,用于记录某种事件的发生次数。在应用中,计数器可以用来跟踪用户行为、统计点击次数、浏览次数等。


例如,您可以使用计数器来记录一篇文章被阅读的次数,或者统计某个产品被购买的次数。通过跟踪计数,您可以了解数据的变化趋势,从而做出更明智的决策。


2、Redis实现计数器的原理


Redis是一款高性能的内存数据库,提供了丰富的数据结构和命令,非常适合实现计数器功能。在Redis中,我们可以使用字符串数据类型以及相关的命令来实现计数器。


(1)使用INCR命令实现计数器


Redis的INCR命令是一个原子操作,用于将存储在键中的数字递增1。如果键不存在,将会创建并初始化为0,然后再执行递增操作。这使得我们可以轻松地实现计数器功能。


让我们通过Java代码来演示如何使用Redis的INCR命令实现计数器:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCR命令递增计数
long views = jedis.incr(viewsKey);

System.out.println("Article views: " + views);

jedis.close();
}
}

在上面的代码中,我们使用了Jedis客户端库来连接Redis服务器,并使用INCR命令递增一个存储在views:article:123键中的计数器。每次执行该代码,计数器的值都会递增,并且我们可以轻松地获取到文章的浏览次数。


(2)使用INCRBY命令实现计数器


除了单次递增1,我们还可以使用INCRBY命令一次性增加指定的数量。这对于一些需要一次性增加较大数量的场景非常有用。


让我们继续使用上面的例子,但这次我们使用INCRBY命令来增加浏览次数:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCRBY命令递增计数
long views = jedis.incrBy(viewsKey, 10); // 一次增加10

System.out.println("Article views: " + views);

jedis.close();
}
}

在上述代码中,我们使用了INCRBY命令将文章浏览次数一次性增加了10。这在统计需要一次性增加较多计数的场景中非常有用。


通过使用Redis的INCRINCRBY命令,我们可以轻松实现高性能的计数器功能。这些命令的原子性操作保证了计数的准确性,而且非常适用于需要频繁更新计数的场景。


三、通过Redis实现“王者荣耀”排行榜?


王者荣耀的排行榜是不是用Redis做的,我不得而知,但,我的项目中,排行榜确实是用Redis做的,这是实打实的。



看见了吗?掌握算法的男人,到哪里都是无敌的。




1、什么是排行榜功能?


排行榜是一种常见的功能,用于记录某种项目的排名情况,通常按照某种规则对项目进行排序。在社交媒体、游戏、电商等领域,排行榜功能广泛应用,可以增强用户的参与度和竞争性。例如,社交媒体平台可以通过排行榜展示最活跃的用户,游戏中可以展示玩家的分数排名等。


2、Redis实现排行榜的原理


在Redis中,我们可以使用有序集合(Sorted Set)数据结构来实现高效的排行榜功能。有序集合是一种键值对的集合,每个成员都与一个分数相关联,Redis会根据成员的分数进行排序。这使得我们能够轻松地实现排行榜功能。


(1)使用ZADD命令添加成员和分数


Redis的ZADD命令用于向有序集合中添加成员和对应的分数。如果成员已存在,可以更新其分数。让我们通过Java代码演示如何使用ZADD命令来添加成员和分数到排行榜:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZADD命令添加成员和分数
jedis.zadd(leaderboardKey, 1000, player1);
jedis.zadd(leaderboardKey, 800, player2);

jedis.close();
}
}

在上述代码中,我们使用ZADD命令将PlayerAPlayerB作为成员添加到leaderboard有序集合中,并分别赋予分数。这样,我们就在排行榜中创建了两名玩家的记录。


(2)使用ZINCRBY命令更新成员分数


除了添加成员,我们还可以使用ZINCRBY命令更新已有成员的分数。这在实时更新排行榜中的分数非常有用。


让我们继续使用上面的例子,但这次我们将使用ZINCRBY命令来增加玩家的分数:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZINCRBY命令更新成员分数
jedis.zincrby(leaderboardKey, 200, player1); // 增加200分

jedis.close();
}
}

在上述代码中,我们使用了ZINCRBY命令将PlayerA的分数增加了200分。这种方式可以用于记录玩家的得分、积分等变化,从而实时更新排行榜数据。


通过使用Redis的有序集合以及ZADDZINCRBY等命令,我们可以轻松实现高性能的排行榜功能。这些命令的原子性操作保证了排行的准确性和一致性,非常适用于需要频繁更新排行榜的场景。



我的最强百里,12-5-6,这都能输?肯定是哪里出问题了,服务器性能?




四、计数器与排行榜的性能优化


在本节中,我们将重点讨论如何在高并发场景下优化计数器和排行榜功能的性能。通过合理的策略和技巧,我们可以确保系统在处理大量数据和用户请求时依然保持高性能。


1、如何优化计数器的性能?


(1)使用Redis事务


在高并发场景下,多个用户可能同时对同一个计数器进行操作,这可能引发并发冲突。为了避免这种情况,可以使用Redis的事务来确保原子性操作。事务将一组命令包装在一个原子性的操作中,保证这些命令要么全部执行成功,要么全部不执行。


下面是一个示例,演示如何使用Redis事务进行计数器操作:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

public class CounterOptimizationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String counterKey = "view_count";
try {
// 开始事务
Transaction tx = jedis.multi();
// 对计数器执行加1操作
tx.incr(counterKey);
// 执行事务
tx.exec();
} catch (JedisException e) {
// 处理事务异常
e.printStackTrace();
} finally {
jedis.close();
}
}
}

在上述代码中,我们使用了Jedis客户端库,通过MULTI命令开启一个事务,然后在事务中执行INCR命令来增加计数器的值。最后,使用EXEC命令执行事务。如果在事务执行期间出现错误,我们可以通过捕获JedisException来处理异常。


(2)使用分布式锁


另一种优化计数器性能的方法是使用分布式锁。分布式锁可以确保在同一时刻只有一个线程能够对计数器进行操作,避免了并发冲突。这种机制可以保证计数器的更新是串行化的,从而避免了竞争条件。


以下是一个使用Redisson框架实现分布式锁的示例:


import org.redisson.Redisson;
import org.redisson.api.RLock;

public class CounterOptimizationWithLockExample {

public static void main(String[] args) {
Redisson redisson = Redisson.create();
RLock lock = redisson.getLock("counter_lock");

try {
lock.lock(); // 获取锁
// 执行计数器操作
} finally {
lock.unlock(); // 释放锁
redisson.shutdown();
}
}
}

在上述代码中,我们使用了Redisson框架来创建一个分布式锁。通过调用lock.lock()获取锁,然后执行计数器操作,最后通过lock.unlock()释放锁。这样可以保证在同一时间只有一个线程能够执行计数器操作。



2、如何优化排行榜的性能?


(1)分页查询


在排行榜中,通常会有大量的数据,如果一次性查询所有数据,可能会影响性能。为了解决这个问题,可以使用分页查询。将排行榜数据分成多个页,每次查询一小部分数据,以减轻数据库的负担。


以下是一个分页查询排行榜的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardPaginationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
int pageSize = 10; // 每页显示的数量
int pageIndex = 1; // 页码

// 获取指定页的排行榜数据
Set<Tuple> leaderboardPage = jedis.zrevrangeWithScores(leaderboardKey, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1);

for (Tuple tuple : leaderboardPage) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们使用zrevrangeWithScores命令来获取指定页的排行榜数据。通过计算起始索引和结束索引,我们可以实现分页查询功能。


(2)使用缓存


为了进一步提高排行榜的查询性能,可以将排行榜数据缓存起来,减少对数据库的访问。例如,可以使用Redis缓存最近的排行榜数据,定期更新缓存以保持数据的新鲜性。


以下是一个缓存排行榜数据的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardCachingExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String cacheKey = "cached_leaderboard";
int cacheExpiration = 300; // 缓存过期时间,单位:秒

// 尝试从缓存中获取排行榜数据
Set<Tuple> cachedLeaderboard = jedis.zrevrangeWithScores(cacheKey, 0, -1);

if (cachedLeaderboard.isEmpty()) {
// 如果缓存为空,从数据库获取数据并更新缓存
Set<Tuple> leaderboardData = jedis.zrevrangeWithScores(leaderboardKey, 0, -1);
jedis.zadd(cacheKey, leaderboardData);
jedis.expire(cacheKey, cacheExpiration);
cachedLeaderboard = leaderboardData;
}

for

(Tuple tuple : cachedLeaderboard) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们首先尝试从缓存中获取排行榜数据。如果缓存为空,我们从数据库获取数据,并将数据存入缓存。使用expire命令来设置缓存的过期时间,以保持数据的新鲜性。


五、实际应用案例


在本节中,我们将通过两个实际的案例,展示如何使用Redis的计数器和排行榜功能来构建社交媒体点赞系统和游戏玩家排行榜系统。这些案例将帮助您更好地理解如何将Redis的功能应用于实际场景中。


1、社交媒体点赞系统案例


(1)问题背景


假设我们要构建一个社交媒体平台,用户可以在文章、照片等内容上点赞。我们希望能够统计每个内容的点赞数量,并实时显示最受欢迎的内容。


(2)系统架构



  • 每个内容的点赞数可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合(Sorted Set)来维护内容的排名信息,将内容的点赞数作为分数。


(3)数据模型



  • 每个内容都有一个唯一的标识,如文章ID或照片ID。

  • 使用一个计数器来记录每个内容的点赞数。

  • 使用一个有序集合来记录内容的排名,以及与内容标识关联的分数。


(4)Redis操作步骤



  1. 用户点赞时,使用Redis的INCR命令增加对应内容的点赞数。

  2. 使用ZADD命令将内容的标识和点赞数作为分数添加到有序集合中。


Java代码示例


import redis.clients.jedis.Jedis;

public class SocialMediaLikeSystem {

private Jedis jedis;

public SocialMediaLikeSystem() {
jedis = new Jedis("localhost", 6379);
}

public void likeContent(String contentId) {
// 增加点赞数
jedis.incr("likes:" + contentId);

// 更新排名信息
jedis.zincrby("rankings", 1, contentId);
}

public long getLikes(String contentId) {
return Long.parseLong(jedis.get("likes:" + contentId));
}

public void showRankings() {
// 显示排名信息
System.out.println("Top content rankings:");
jedis.zrevrangeWithScores("rankings", 0, 4)
.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}

public static void main(String[] args) {
SocialMediaLikeSystem system = new SocialMediaLikeSystem();
system.likeContent("post123");
system.likeContent("post456");
system.likeContent("post123");

System.out.println("Likes for post123: " + system.getLikes("post123"));
System.out.println("Likes for post456: " + system.getLikes("post456"));

system.showRankings();
}
}

在上述代码中,我们创建了一个名为SocialMediaLikeSystem的类来模拟社交媒体点赞系统。我们使用了Jedis客户端库来连接到Redis服务器,并实现了点赞、获取点赞数和展示排名的功能。每当用户点赞时,我们会使用INCR命令递增点赞数,并使用ZINCRBY命令更新有序集合中的排名信息。通过调用zrevrangeWithScores命令,我们可以获取到点赞数排名前几的内容。



2、游戏玩家排行榜案例


(1)问题背景


在一个多人在线游戏中,我们希望能够实时追踪和显示玩家的排行榜,以鼓励玩家参与并提升游戏的竞争性。


(2)系统架构



  • 每个玩家的得分可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合来维护玩家的排名,将玩家的得分作为分数。


(3)数据模型



  • 每个玩家都有一个唯一的ID。

  • 使用一个计数器来记录每个玩家的得分。

  • 使用一个有序集合来记录玩家的排名,以及与玩家ID关联的得分。


(4)Redis操作步骤



  1. 玩家完成游戏时,使用Redis的ZINCRBY命令增加玩家的得分。

  2. 使用ZREVRANK命令获取玩家的排名。


(5)Java代码示例


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;

import java.util.Set;

public class GameLeaderboard {

private Jedis jedis;

public GameLeaderboard() {
jedis = new Jedis("localhost", 6379);
}

public void updateScore(String playerId, double score) {
jedis.zincrby("leaderboard", score, playerId);
}

public Long getPlayerRank(String playerId) {
return jedis.zrevrank("leaderboard", playerId);
}

public Set<Tuple> getTopPlayers(int count) {
return jedis.zrevrangeWithScores("leaderboard", 0, count - 1);
}

public static void main(String[] args) {
GameLeaderboard leaderboard = new GameLeaderboard();
leaderboard.updateScore("player123", 1500);
leaderboard.updateScore("player456", 1800);
leaderboard.updateScore("player789", 1600);

Long rank = leaderboard.getPlayerRank("player456");
System.out.println("Rank of player456: " + (rank != null ? rank + 1 : "Not ranked"));

Set<Tuple> topPlayers = leaderboard.getTopPlayers(3);
System.out.println("Top players:");
topPlayers.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}
}

在上述代码中,我们创建了一个名为GameLeaderboard的类来模拟游戏玩家排行榜系统。我们同样使用Jedis客户端库来连接到Redis服务器,并实现了更新玩家得分、获取玩家排名和获取排名前几名玩家的功能。使用zincrby命令可以更新玩家的得分,而zrevrank命令则用于


获取玩家的排名,注意排名从0开始计数。通过调用zrevrangeWithScores命令,我们可以获取到排名前几名玩家以及他们的得分。


六、总结与最佳实践


在本篇博客中,我们深入探讨了如何使用Redis构建高性能的计数器和排行榜功能。通过实际案例和详细的Java代码示例,我们了解了如何在实际应用中应用这些功能,提升系统性能和用户体验。让我们在这一节总结Redis在计数器和排行榜功能中的价值,并提供一些最佳实践指南。


1、Redis在计数器和排行榜中的价值


通过使用Redis的计数器和排行榜功能,我们可以实现以下价值:




  • 实时性和高性能:Redis的内存存储和优化的数据结构使得计数器和排行榜功能能够以极高的性能实现。这对于需要实时更新和查询数据的场景非常重要。




  • 用户参与度提升:在社交媒体和游戏等应用中,计数器和排行榜功能可以激励用户参与。通过显示点赞数量或排行榜,用户感受到了更强的互动性和竞争性,从而增加了用户参与度。




  • 数据统计和分析:通过统计计数和排行数据,我们可以获得有价值的数据洞察。这些数据可以用于分析用户行为、优化内容推荐等,从而指导业务决策。




2、最佳实践指南


以下是一些使用Redis构建计数器和排行榜功能的最佳实践指南:




  • 合适的数据结构选择:根据实际需求,选择合适的数据结构。计数器可以使用简单的String类型,而排行榜可以使用有序集合(Sorted Set)来存储数据。




  • 保证数据准确性:在高并发环境下,使用Redis的事务、管道和分布式锁来保证计数器和排行榜的数据准确性。避免并发写入导致的竞争条件。




  • 定期数据清理:定期清理不再需要的计数器和排行数据,以减小数据量和提高查询效率。可以使用ZREMRANGEBYRANK命令来移除排行榜中的过期数据。




  • 适度的缓存:对于排行榜数据,可以考虑添加适度的缓存以提高查询效率。但要注意平衡缓存的更新和数据的一致性。




通过遵循这些最佳实践,您可以更好地应用Redis的计数器和排行榜功能,为您的应

作者:哪吒编程
来源:juejin.cn/post/7271908000414351400
用程序带来更好的性能和用户体验。

收起阅读 »

我专门注册了一家公司,带大家来看看 boss 视角下的求职者

你投了很多简历,没有面试机会,是因为你的简历根本就没被面试官看到! 那为什么简历没被看到,明明你已经发了自己的简历,并且也打了招呼啊? 打了很多招呼,但是却没被回复,不是对你不感兴趣,而是人太多,你的招呼又没 有快速抓住面试官的眼球,就被淹没了 这一次,我教你...
继续阅读 »

你投了很多简历,没有面试机会,是因为你的简历根本就没被面试官看到!


那为什么简历没被看到,明明你已经发了自己的简历,并且也打了招呼啊?


打了很多招呼,但是却没被回复,不是对你不感兴趣,而是人太多,你的招呼又没
有快速抓住面试官的眼球,就被淹没了


这一次,我教你写抓人眼球,boss都忍不住点开聊天框的打招呼话术!




hello,大家好,我是 Sunday。


很多小伙伴在招聘软件上打招呼,但是却没有任何回复。很多小伙伴对此都会非常困惑,甚至开始怀疑自己。


那么我为了搞明白,这到底是因为什么。


所以,我专门注册了一个招聘端的账号,让我们从一个招聘者的角度来看一看:你的消息到底是怎么被体现出来的。


我是在周天晚上 11 点开放了两个岗位:前端和java


咱们先来看前端,这是招聘 JD


前端招聘JD


截止到周一上午 11 点,我一共收到了整整152位打招呼信息:



作为对比,咱们来看下 java 岗,这是 java 岗的 JD



他的投递更夸张,达到了 287 人:



这还是在济南这个二线城市的招聘情况。如果是在一线城市,那么一天收到上千份的简历,绝对不是开玩笑的。


那么这么多的打招呼消息,在 boss 端是怎么体现的呢?咱们一起来看看,截图有点多,但是这就是boss视角下的真实情况:






如果你是 boss 的话,那么你会点开谁的消息来看呢?


通过截图我们可以看到。打招呼的消息在 boss 端只能展现 17 个字。所以我们必须要在这 17 字中展示出核心竞争力,不要说废话。


比如这种消息:



这种就是典型的浪费机会:“可以聊一聊吗?” 可以聊呀,那你倒是聊啊~~~
同理,我们来看这一页消息,如果你是 boss 的话,你会点开谁的消息来看?



如果是我的话,那么我肯定会优先对 倒数第二,倒数第三 条消息比较感兴趣,因为他们直接描述到了重点内容:



所以,如果你在招聘软件中打招呼,总是得不到回复,那么可以想一想,你是不是也犯了 没有描述重点的错误呢?


那么描述了问题之后,接下来,咱们就来看下,打招呼的语句怎么去说呢?


根据 boss 视角,咱们可以知道:打招呼的前17个字是非常重要的。所以我们一定要在前 17 个字中,把重点说出来。


比如,你可以这么说:



3 年前端开发经验,熟练 vue、react 技术栈,具备多个大型项目开发经验,详细情况您可以看下我的简历,期待您的回复!



简单的一句话,核心内容,在前17个字中,都描述清楚了。


这句话内含一个公式:描述 经验、能力、成就结果+明确指引


咱们举个栗子🌰:



描述经验(3 年前端开发经验)+描述能力(熟练 vue、react 技术栈)+描述成就结果(具备多个大型项目开发经验)+明确指引(看下我的简历,期待您的回复)



其中公式里的每一个要素具体怎么描述,每一个人,每一个职位可能不一样,篇幅有限这里就不详细讲述了。


如果你想要根据自己的经历定制出适合自己的高回复打招呼话术,或者你近期有跳槽的需求,可以直接与我私聊。

作者:程序员Sunday
来源:juejin.cn/post/7272290608655220755

收起阅读 »

客户端开发的我,准备认真学前端了

⏰ : 全文字数:2200+ 🥅 : 内容关键字:前端,独立开发者,思考 背景 我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识...
继续阅读 »

⏰ : 全文字数:2200+

🥅 : 内容关键字:前端,独立开发者,思考



背景


我呢,一个Android开发工程师,从毕业到现在主要做的是客户端开发,目前在一个手机厂商任职。自己目前知识技能主要在客户端上,其他方面会一点点,会一点点前端知识,会一点点后端知识,会一点点脚本,用网络的一句话概括起来就是“有点东西,但是不多”😭。


为什么


决定学习前端,并不是心血来潮,一时自嗨,而是经过了比较长时间的思考。对于程序员来说,知识的更新迭代实在是很快,所以保持学习很重要。但是技术防线这么多,到底学什么?我相信这不是一个很容易做出抉择的问题。


对于前端之前有断断续续的学过一些,但是最后没有一直坚持下来。之所以这样,原因很多,比如没有很强的目标、没有足够的时间,前端涉及的知识点太多等。


但是我觉得对自己而言,最重要的一个原因是:**学习完前端,我能用它来干嘛?**如果没有想清楚这个原因,就很难找到目标。做事情没有目标,就无法拆解,也就无法长期坚持下去。直到最近,看了一些文章,碰到了一些事情,才慢慢想清楚这个问题。目前对我而言,开始决定认真学习前端的主要原因有两个:



  • 自己一直想做点什么

  • 工作上有需要


想做点什么


从我接触计算机开始,心底里一直有个梦,就是想利用自己手上技能,做点什么。我也和旁边的朋友同事交流过,大家都有类似的想法,从这看估计很多程序员朋友都会有这样的想法。我从一开始的捣鼓网站,论坛,到后来开发APP等,折腾了好多东西。但是到了最后,都没有折腾出点啥,都无疾而终。


前一段时间,看到一个博主写的一篇文章,文章大概是讲他如何从一个公司的后端开发工程师,走到今天成为一名独立开发者的故事。


其中有一段是说他一直心里念念不忘,想做一款 saas 应用,期间一直在学习和看其他人的产品,学习经验,尝试不同的想法。所谓念念不忘必有回响,终于从别人的产品中产生了一个点子,然后很快写好了后端服务,并自学前端边做边学,完成了这个产品。目前他的这个产品运作的很成功。


这个故事给我很大鼓舞,之前看到过很多这样的故事,有成功的,有失败的。我也去分析看了那些成功的,经过自己的观察,大部分成功的独立开发者,基本上都是多年前成功的那批,那段时间还是处于互联网的红利期,天时地利人和加在一起,造就了他们的成功。


当然这里并不是否认他们能力,觉得是他们运气好。能在当时那么多人中,脱颖而出,依然表明他们是佼佼者。这里只是想表达那个时间段,大环境对开发者来说,是比较友好的,阻力没有那么大。


很少看到最近两年成功的开发者(不排除自己不知道哈),但是从这位博主的经历来看,他确实在成功了,这给了我很大的鼓舞,说明这条路上还是有机会的,只是在现在这种大环境下,成功的难度在增加,阻力变大。如果我们自己始终坚持,寻找机会,不断地尝试,是否有一天可能会成功呢?


那这样的话,我主要关注哪个方向呢?我个人更加偏向于前端全栈方向,包括WebApp,小程序,P C 软件等。


为什么这么认为呢?看下现在的大环境,不提之前上架APP需要各种软件著作权,后来个人无法在各大商店上发布APP,再到现在新出的APP备案制,基本上个人想在Android App上发力,真的很难了。而且,经过自己在ProductHunt上观察,目前大部分独立开发者的作品都是聚焦于WebAppSAAS,或者是PC类软件,剩下就是IOSMAC平台的。


且学习前端技术栈是一个比较好的选择。JavaScript这门语言很强大,整个技术栈不仅可以做前端,也可以做后端开发,还可以做跨平台的 P C 软件开发, 提供的丰富的解决方案,对个人开发者来说极为合适。


当然,我们也可以找合适的人,一起组队合作,不用单打独斗,这样不仅节省期间和精力,也能有好的交流和碰撞。这条路我也经历过,但是说实话执行起来确实有一定的困难。首先就是人难找,要想找到一个三观差不多的伙伴,其实真的挺难的。还有一个就是个人时间和做事方式也很难契合。所以个人认为如果想做点什么,前期一个人自己去实现一个MVP出来,是一个合适的选择。后面如果有必要了,倒是可以考虑慢慢招人。


我们也要认识到技术只是最基础的第一步,要想做成一个产品,还有很多东西要学习。推广、运营,沟通交流无论哪个都是一道坎。但是作为登山者的我们不要关注前面路有多远,而是要确保自己一直在路上。


工作涉及


还有一个原因是,最近工作上和前端打交道有很多。因为项目内部接入了类似 React Native 的框架,有大量的业务场景是基于这个框架开发。这就导致了客户端涉及到大量和前端的交互,流程的优化,工程化等工作。客户端可以不用了解前端和框架的知识,也没什么问题。
但是想着如果后续这一块有什么问题,或者想对这块做一些性能优化、工程提效的事情,如果对前端知识没有一个很好的了解,估计也很难做出彩。


结尾


今天在这里絮絮叨叨这么多,并不是想要告诉大家选择前端技术栈学习就一定咋样,比如第一点说的独立开发者中,有很多的全栈开发者,他们有的已经失败了,有的还在路上,成功的毕竟还是少数。
我想分享的是我个人关于为什么选择前端技术栈作为学习方向,如何做出选择的一些思考。这都是我的一家之言,不一定正确,大家姑且一看。


同时自己心里也还是希望能像文章提到的那位博主一样,在做产品这条路上,也能“念念不忘,必有回响”。正如我一直相信秉持的“日拱一卒,功不唐捐”。

作者:七郎的小院
来源:juejin.cn/post/7271248528999481384

收起阅读 »

七夕礼物

环信的七夕礼物收到了,T恤很好看,胸章的设计也很时尚,唯一的遗憾就是没有拍照,手机转电脑略麻烦哈哈哈,环信赶紧出个手机端app吧。

环信的七夕礼物收到了,T恤很好看,胸章的设计也很时尚,唯一的遗憾就是没有拍照,手机转电脑略麻烦哈哈哈,环信赶紧出个手机端app吧。

虚拟dom

vue中的虚拟dom 简介 首先vue会把模板编译成render函数,运行render函数生成虚拟dom 虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图 为什么会需要虚拟dom 在主流框架 Angular , Vue.js (1.0)...
继续阅读 »

vue中的虚拟dom


简介



首先vue会把模板编译成render函数,运行render函数生成虚拟dom


虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图



为什么会需要虚拟dom


在主流框架 Angular , Vue.js (1.0)React 中都有一个共同点,那就是它们都不知道哪些状态(state)变了。因此就需要进行比对,在React中使用的虚拟dom比对, Angular 中使用的是脏检查的流程



而在 Vue.js中使用的是变化侦测的方式,它在一定程度上知道具体哪些状态发生了变化,这样就可以通过更细粒度的绑定来更新视图。也就是说,在Vue.js中,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,根本不需要比对



但是这样做的代价就是,粒度太细,每一个都有对应的 watcher 来观察状态变化,这样就会浪费一些内存开销,绑定的越多开销越大,如果这运用在一个大型项目中,那么他的开销无疑是非常大的


因此从 Vue.js 2.0 开始借鉴 React 中的虚拟DOM ,组件级别是一个watcher实例,就是说即便一个组件内有10个节点使用了某个状态,但其实也只有一个watcher在观察这个状态的变化。


什么是虚拟dom


虚拟DOM是通过状态生成一个虚拟节点树(vnode) ,然后使用虚拟节点树进行渲染。 在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比 (diff算法) ,只渲染不同的部分



虚拟节点树其实是由组件树建立起来的整个虚拟节点(Virtual Node,也经常简写为vnode)树



在Vue.js中,我们使用模板来描述状态DOM之间的映射关系。Vue.js通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面


模板编译成render函数


将模板编译成渲染函数可以分两个步骤,先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使用AST生成渲染函数。


但是由于静态节点不需要总是重新渲染,所以在生成AST之后、生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。所以,在大体逻辑上,模板编译分三部分内容:

  • 将模板解析为AST
  • 遍历AST标记静态节点
  • 使用AST生成渲染函数

虚拟dom做了什么


虚拟DOM在Vue.js中所做的事情其实并没有想象中那么复杂,它主要做了两件事。

  • 提供与真实DOM节点所对应的虚拟节点vnode。
  • 将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图。

对两个虚拟节点对比是虚拟dom 中最核心的算法 (diff),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作


小结


虚拟DOM是将状态映射成视图的众多解决方案中的一种,它的运作原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。


为什么会需要虚拟dom

框架设计

Vue 和 React 框架设计理念都是基于数据驱动的,当数据发生变化时 就要去更新视图,要想知道在页面众多元素中改动数据的元素 并根据改动后的数据去更新视图 是非常困难的



所以 Vue 和 React 中都会有一个 Render函数 或者类似于Render函数的功能,当数据变化时 全量生成Dom 元素
如果是直接操作 真实Dom 的话 是很昂贵的,就会严重拖累效率,所以就不生成真实的Dom,而是生成虚拟的Dom
当数据变化时就是 对象 和 对象 进行一个对比 ,这样就能知道哪些数据发生了改变 从而去操作改变的数据后的Dom元素



这也是一个 “妥协的结果”


跨平台

现阶段的框架他不仅仅能在浏览器里面使用,在小程序,移动端,或者桌面端也可以使用,但是真实Dom仅仅指的是在浏览器的环境下使用,因此他不能直接生成真实Dom ,所以选择生成一个在任何环境下都能被认识的虚拟Dom
最后根据不同的环境,使用虚拟Dom 去生成界面,从而实现跨平台的作用 --- 一套代码在多端运行


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

不会封装hook? 看下ahooks这6个hook是怎么做的

1 useUpdate 在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:import { useCallback...
继续阅读 »

1 useUpdate


在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:

import { useCallback, useState } from 'react';

const useUpdate = () => {
const [, setState] = useState({});

return useCallback(() => setState({}), []);
};

export default useUpdate;

可以看到useUpdate的返回值函数,就是每次都用一个新的对象调用setState,触发组件的更新。


2 useMount


react函数组件虽然没有了mount的生命周期,但是我们还会有这种需求,就是在组件第一次渲染之后执行一次的需求,就可以封装useEffect实现这个需求, 只需要把依赖项设置成空数组,那么就只在渲染结束后,执行一次回调:

import { useEffect } from 'react';

const useMount = (fn: () => void) => {

useEffect(() => {
fn?.();
}, []);
};

export default useMount;


3 useLatest


react函数组件是一个可中断,可重复执行的函数,所以在每次有state或者props变化的时候,函数都会重新执行,我们知道函数的作用域是创建函数的时候就固定下来的,如果其中的内部函数是不更新的,那么这些函数获取到的外部变量就是不会变的。例如:

import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';


export default () => {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<>
<p>count: {count}</p>
</>
);
};

这是一个定时更新count值的例子,但是上边的代码只会让count一直是1,因为setInterval中的函数在创建的时候它的作用域就定下来了,它拿到的count永远是0, 当执行了setCount后,会触发函数的重新执行,重新执行的时候,虽然count值变成了1,但是这个count已经不是它作用域上的count变量了,函数的每次执行都会创建新的环境,而useState, useRef 等这些hooks 是提供了函数重新执行后保持状态的能力, 但是对于那些没有重新创建的函数,他们的作用域就永远的停留在了创建的时刻。 如何让count正确更新, 简单直接的方法如下,在setCount的同时,也直接更新count变量,也就是直接改变这个闭包变量的值,这在JS中也是允许的。

import React, { useState, useEffect } from 'react';
import { useLatest } from 'ahooks';


export default () => {
let [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
count = count + 1
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<>
<p>count: {count}</p>
</>
);
};

setCount是为了让函数刷新,并且更新函数的count值,而直接给count赋值,是为了更新定时任务函数中维护的闭包变量。 这显然不是一个好的解决办法,更好的办法应该是让定时任务函数能够拿到函数最新的count值。
useState返回的count每次都是新的变量,也就是变量地址是不同的,应该让定时任务函数中引用一个变量地址不变的对象,这个对象中再记录最新的count值,而实现这个功能就需要用到了useRef,它就能帮助我们在每次函数刷新都返回相同变量地址的对象, 实现方式如下:

import React, { useState, useEffect, useRef } from 'react'

export default () => {
const [count, setCount] = useState(0)

const latestCount = useRef(count)
latestCount.current = count

useEffect(() => {
const interval = setInterval(() => {
setCount(latestCount.current + 1)
}, 1000)
return () => clearInterval(interval)
}, [])

return (
<>
<p>count: {count}</p>
</>
)
}


可以看到定时函数获取的latestCount永远是定义时的变量,但因为useRef,每次函数执行它的变量地址都不变,并且还把count的最新值,赋值给了latestCount.current, 定时函数就可以获取到了最新的count值。
所以这个功能可以封装成了useLatest,获取最新值的功能。

import { useRef } from 'react';

function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;

return ref;
}

export default useLatest;


上边的例子是为了说明useLatest的作用,但针对这个例子,只是为了给count+1,还可以通过setCount方法本身获取,虽然定时任务函数中的setCount页一直是最开始的函数,但是它的功能是可以通过传递函数的方式获取到最新的count值,代码如下:

  const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount(count=>count+1)
}, 1000)
return () => clearInterval(interval)
}, [])

4 useUnMount


有了useMount就会有useUnmount,利用的就是useEffect的函数会返回一个cleanup函数,这个函数在组件卸载和useEffect的依赖项变化的时候触发。 正常情况 我们应该是useEffect的时候做了什么操作,返回的cleanup函数进行相应的清除,例如useEffect创建定时器,那么返回的cleanup函数就应该清除定时器:

 const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
count = count + 1
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

所以useUnMount就是利用了这个cleanup函数实现useUnmount的能力,代码如下:

import { useEffect } from 'react';
import useLatest from '../useLatest';

const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);

useEffect(
() => () => {
fnRef.current();
},
[],
);
};

export default useUnmount;


使用了useLatest存放fn的最新值,写了一个空的useEffect,依赖是空数组,只在函数卸载的时候执行。


5 useToggle和useBoolean


useToggle 封装了可以state在2个值之间的变化,useBoolean则是利用了useToggle,固定2个值只能是true和false。 看下他们的源码:

function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
const [state, setState] = useState<D | R>(defaultValue);

const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;

const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);

return {
toggle,
set,
setLeft,
setRight,
};
// useToggle ignore value change
// }, [defaultValue, reverseValue]);
}, []);

return [state, actions];
}

可以看到,调用useToggle的时候可以设置初始值和相反值,默认初始值是false,actions用useMemo封装是为了提高性能,没有必要每次渲染都重新创建这些函数。setLeft是设置初始值,setRight是设置相反值,set是用户随意设置,toggle是切换2个值。
useBoolean则是在useToggle的基础上进行了封装,让我们用起来对更加的简洁方便。

export default function useBoolean(defaultValue = false): [boolean, Actions] {
const [state, { toggle, set }] = useToggle(defaultValue);

const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
};
}, []);

return [state, actions];
}

总结


本文介绍了ahooks中封装的6个简单的hook,虽然简单,但是可以通过他们的做法,学习到自定义hook的思路和作用,就是把一些能够重用的逻辑封装起来,在实际项目中我们有这个意识就可以封装出适合项目的hook。


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

你的代码不堪一击!太烂了!

前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
继续阅读 »

前言


小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


一、变量解构一解就报错


优化前

const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



所以当 dataundefinednull 时候,上述代码就会报错。


优化后

const App = (props) => {
const { data } = props || {};
const { name, age } = data || {};
}

二、不靠谱的默认值


估计有些同学,看到上小节的代码,感觉还可以再优化一下。


再优化一下

const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



所以当 props.datanull,那么 const { name, age } = null 就会报错!


三、数组的方法只能用真数组调用


优化前:

const App = (props) => {
const { data } = props || {};
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


优化后:

const App = (props) => {
const { data } = props || {};
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象


优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


二次优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用


优化前:

const App = (props) => {
const { data } = props || {};
const nameList = Object.keys(data || {});
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


优化后:

const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props || {};
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获


优化前:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。


优化后:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


二次优化后:

import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse


优化前:

const App = (props) => {
const { data } = props || {};
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


优化后:

const App = (props) => {
const { data } = props || {};
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据


优化前:

const App = (props) => {
const { data } = props || {};
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


优化后:

import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props || {};
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作


优化前:

const App = (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


优化后:

const App = async (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御


优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续


以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


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

天涯论坛倒闭,我给天涯续一秒

时代抛弃你,连句招呼都不会打 "时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗...
继续阅读 »

时代抛弃你,连句招呼都不会打


"时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗走麻花腾qq的黑客大神、高深莫测的民生探讨、波诡云谲的国际形势分析、最前沿最野的明星八卦、惊悚刺激的怪力乱神、脑洞大开的奇人异事 等等,让人眼花缭乱。甚至还有教你在家里养一只活生生的灵宠(见下文玄学类) 


今年4月初,天涯官微发布公告,因技术升级和数据重构,暂时无法访问。可直到现在,网站还是打不开。虽然后来,官微略带戏谑和无奈地表示:“我会回来的”。但其糟糕的财务状况预示着,这次很可能真是,咫尺天涯,永不再见了。 



神奇的天涯


当时还在读大一时候就接触到了 天涯,还记得特别喜欢逛的板块是 "莲蓬鬼话"、"天涯国际"。莲蓬鬼话老用户都知道,主要是一些真真假假的怪力乱神的惊险刺激的事情,比如 有名的双鱼玉佩,还有一些擦边的玩意,比如《风雪漫千山人之大欲》,懂得都懂,这些都在 pdf里面自取😁;天涯国际主要是各路大佬分析国际局势,每每看完总有种感觉 "原来在下一盘大棋",还有各种人生经验 比如kk大神对房产的预测,现在看到貌似还是挺准的。还有教你在家里养一只活生生的灵宠,神奇吧。 总共200+篇,这里先做下简单介绍





关注公众号,回复 「天涯」 海量社区经典文章双手奉上,感受一下昔日论坛的繁华



历史人文类


功底深厚,博古通今,引人入胜,实打实的的拓宽的你的知识面

  • (长篇)女性秘史◆那些风华绝代、风情万种的女人,为你打开女人的所有秘密.pdf
  • 办公室实用暴力美学——用《资治通鉴》的智慧打造职场金饭碗.pdf
  • 《二战秘史》——纵论二战全史——邀你一起与真相贴身肉搏.pdf
  • 不被理解的mzd(密码是123).zip
  • 地缘看世界——欧洲部分-温骏轩.pdf
  • 宝钗比黛玉大八岁!重解红楼全部诗词!血泪文字逐段解释!所有谜团完整公开!.pdf
  • 现代金融经济的眼重看历史-谁是谁非任评说.pdf
  • 蒋介石为什么失掉大陆:1945——1949-flp713.pdf

人生箴言类


开挂一般的人生,有的应该是体制内大佬闲来灌水,那时上网还无需实名

  • 职业如何规划?大城市,小城市,如何抉择?我来说说我的个人经历和思考-鸟树下睡懒觉的猪.pdf
  • kk所有内容合集(506页).pdf
  • 一个潜水多年的体制内的生意人来实际谈谈老百姓该怎么办?.pdf
  • 三年挣850万,你也可以复制!现在新书已出版,书名《我把一切告诉你》.pdf
  • 互联网“裁员”大潮将起:离开的不只是马云 可能还有你.pdf
  • 大鹏金翅明王合集.pdf
  • 解密社会中升官发财的经济学规律-屠龙有术.pdf

房产金融


上帝视角,感觉有的可能是参与制定的人

  • 从身边最简单的经济现象判断房价走势-招招是道.pdf
  • 大道至简,金融战并不复杂。道理和在县城开一个赌场一样。容我慢慢道来-战略定力.pdf
  • 沉浮房产十余载,谈房市心得.pdf
  • 现代金融的本质以及房价-curgdbd.pdf
  • 对当前房地产形势的判断和对一些现象的解释-loujinjing.pdf
  • 中国VS美国:决定世界命运的博弈 -不要二分法 .pdf
  • 大江论经济-大江宁静.pdf
  • 形势转变中,未来哪些行业有前景.pdf
  • 把握经济大势和个人财运必须读懂钱-现代金钱的魔幻之力.pdf
  • 烽烟四起,中美对决.pdf
  • 赚未来十年的钱-王海滨.pdf
  • 一个炒房人的终极预测——调控将撤底失败.pdf

故事连载小说类


小说爱好者的天堂,精彩绝伦不容错过

  • 人之大欲,那些房中术-风雪漫千山.pdf
  • 冒死记录中国神秘事件(真全本).pdf 五星推荐非常精彩
  • 六相十年浩劫中的灵异往事,颍水尸媾,太湖獭淫,开封鬼谷,山东杀坑-御风楼主人.pdf
  • 《内参记者》一名“非传统”记者颠覆你三观的采访实录-有骨难画.pdf
  • 中国式骗局大全-我是骗子他祖宗.pdf
  • 我是一名警察,说说我多年来破案遇到的灵异事件.pdf
  • 一个十年检察官所经历的无数奇葩案件.pdf
  • 宜昌鬼事 (三峡地区巫鬼轶事记录整理).pdf
  • 南韩往事——华人黑帮回忆录.pdf
  • 惊悚灵异《青囊尸衣》(斑竹推荐)-鲁班尺.pdf
  • 李幺傻江湖故事之《戚绝书》(那些湮没在岁月深处的江湖往事)-我是骗子他祖宗.pdf
  • 闲来8一下自己幽暗的成长经历-风雪漫千山.pdf
  • 阴阳眼(1976年江汉轶事).pdf
  • 民调局异闻录-儿东水寿.pdf
  • 我当道士那些年.pdf
  • 目睹殡仪馆之奇闻怪事.pdf

玄学类


怪力乱神,玄之又玄,虽然已经要求建国后不许成精了

  • 请块所谓的“开光”玉,不如养活的灵宠!.pdf
  • 写在脸上的风水-禅海商道.pdf
  • 谶纬、民谣、推背图-大江宁静.pdf
  • 拨开迷雾看未来.pdf
  • 改过命的玄教弟子帮你断别你的网名吉凶-大雨小水.pdf

天涯的败落


内容社区赚钱,首先还是得有人气,这是互联网商业模式的基础。天涯在PC互联网时代,依靠第一节说的几点因素,持续快速的吸引到用户,互联网热潮,吸引了大量的资本进入,作为有超高流量的天涯社区,自然也获得了资本的青睐。营收这块,主要分为两个部分:网络广告营销业务和互联网增值业务收入。广告的话,最大的广告主是百度,百度在2015年前5个月为天涯社区贡献了476万元,占总收入的比重达11.24%;百度在2014年为天涯社区贡献收入1328万元,占比12.76%。广告收入严重依赖于流量,天涯为了获得广告营收,大幅在社区内植入广告位,影响了用户体验,很有竭泽而渔的感觉。 但是在进入移动互联网时代没跟上时代步伐, 

2010年底,智能手机的出货量开始超过PC,另外,移动互联网走的是深度垂直创新,天涯还是大而全的综合社区模式,加上运营也不是很高明,一两个没工资的版主,肯定打不过别人公司化的运作,可以看到在细分领域被逐步蚕食:

  • 新闻娱乐,被**「微博、抖音」**抢走;
  • 职场天地,被**「Boss直聘」**抢走;
  • 跳蚤市场,被**「闲鱼、转转」**抢走;
  • 音乐交友,被**「网易云、qq音乐」**抢走;
  • 女性兴趣,被**「小红书」**抢走,等等

强如百度在移动互联网没占到优势,一直蛰伏到现在,在BAT中名存实亡,何况天涯,所以也能理解吧。"海内存知己,天涯若比邻",来到2023年,恐怕只剩物是人非,变成一个被遗忘的角落,一段被尘封的回忆罢了,期待天涯能够度过难关再度重来吧。


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

介绍一款CPP代码bug检测神器

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎, 最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针...
继续阅读 »

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎,
最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针,
数组越界等一些常见的bug扼杀在萌芽阶段,正所谓独乐了不如众乐乐,特将这个利器分享给大家。


这个利器就是cppcheck,它的官网是:cppcheck.sourceforge.io/


同时它还提供了在线分析的功能:cppcheck.sourceforge.io/demo/


在这个在线分析工具中,我们只需要将我们需要检测的代码拷贝粘贴到输入框,然后点击Check按钮即可进行代码分析。


当然啦,这个在线分析还是有很多不足的,比如最多只支持1024个字符,无法在线分析多文件,大项目等,如果要分析长文件,甚至是大项目,那就得安装本地使用啦,
下面我们就以CLion为例,简单介绍下cppcheck的安装和使用。


插件cppcheck的安装


首先这个强大的分析工具是有一个CLion插件的,它的连接是:plugins.jetbrains.com/plugin/8143…


我们可以直接在这个地址上进行在线安装,也可以在CLion的插件市场中搜索cppcheck进行安装。


需要注意的是这个插件仅仅是为了在CLion中自动帮我们分析项目代码,它是不包含cppcheck功能的,也就是要让这个插件正常工作,我们还得
手动安装cppcheck,然后在CLion配置好cppcheck的可执行文件的路径才行。


关于这个cppcheck核心功能的安装官网已经说得很清楚,也就是一句命令行的事情。


比如Debian系统可以通过一下命令安装:

sudo apt-get install cppcheck

Fedora的系统可以通过以下命令安装:

sudo yum install cppcheck

至于Mac系统,那肯定就是用神器包管理工具Homebrew进行安装啦:

brew install cppcheck

CLion插件配置cppcheck路径


安装好cppcheck核心工具包和CLion的cppcheck插件工具之后,我们只需要在CLion中配置一下cppcheck工具包的安装路径就可以正常使用啦。


以笔者的Mac系统的CLion为例子,打开CLion后,点击CLion-> Settings -> Other Settings -> Cppcheck Configuration



在弹出框中设置好cppcheck安装包的绝对路径即可。


如果你是使用Homebrew安装的话可以通过命令brew info cppcheck查找到cppcheck安装的具体路径。


功能实测


为了检测cppcheck这个分析工具的功能,我们新建了一个工程,输入以下代码:

void foo(int x)
{
int buf[10];
if (x == 1000)
buf[x] = 0; // <- ERROR
}

int main() {
int y[1];
y[2] = 1;
return 0;
}

当我们没有安装cppcheck插件时,它是这样子的,看起来没什么问题:



当我们安装了cppcheck插件之后,对于可能会发生潜在的空指针、数组越界、除数为0等等可能导致bug的地方会有高亮提示,
给人一看就有问题的感觉:



当然啦,cppcheck的功能远比这个这个例子所展示的强大,更多惊喜欢迎大家使用体验。


工具是智慧的延伸,在开发的过程选择适合你的工具,可以让我们的工作事半功倍,同行的你如果有好的开发辅助工具欢迎留言分享...


系统话学习博文推荐


音视频入门基础

C++进阶

NDK学习入门

安卓camera应用开发

ffmpeg系列

Opengl入门进阶

webRTC


关注我,一起进步,有全量音视频开发进阶路径、资料、踩坑记等你来学习...


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

Swift 周报 第三十期

iOS
前言 本期是 Swift 编辑组自主整理周报的第二十一期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。 欢迎投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。 求人不如求己,你多一样本领,就少一点啊乞求;Swift社区...
继续阅读 »

前言


本期是 Swift 编辑组自主整理周报的第二十一期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。


欢迎投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。


求人不如求己,你多一样本领,就少一点啊乞求;Swift社区让你多一样技能,少一些嘲讽!



周报精选


新闻和社区:码出新宇宙,WWDC23 就在眼前


提案:有 4 个提案通过,本期没有产生新的提案


Swift 论坛:PermutableCollection 协议


推荐博文:SwiftUI 中 LinearGradient的用法


话题讨论:


有博主在视频社交平台说,2023年已然迎来了经济危机,只是有些人不愿意相信而已,那么你认为国内2023年是否真的进入了经济危机?



上期话题结果



上期话题讨论结果表明,社交隔阂个人选择标准的提高是导致男女群体互不干涉的主要原因,而社会观念的变化也起到了一定的影响。这些因素共同作用导致了男群体和女群体相互独立地寻找伴侣的现象。


新闻和社区


App、App 内购买项目和订阅即将实行税率调整


App Store 的交易和支付机制旨在帮助你在覆盖全球的 175 个国家和地区的商店中,以 44 种货币为你的产品和服务便捷地进行定价与销售。Apple 会为开发者管理其中 70 多个国家和地区的税收,而且你还能够为 App 和 App 内购买项目分配税务类别。我们会根据税务法规的变化,定期更新你在某些地区的收益。


从 5 月 31 日起,你从 App 和 App 内购买项目 (包括自动续期订阅) 销售中获得的收益将进行调整,以反映以下税率调整。请注意,相关内容的价格将保持不变。


加纳:增值税率从 12.5% 上调至 15%。
立陶宛:对于符合条件的电子书和有声书,增值税率从 21% 下调至 9%。
摩尔多瓦:对于符合条件的电子书和期刊,增值税率从 20% 下调至 0%。
西班牙:收取 3% 的数字服务税。
由于巴西税务法规的变化,在巴西开展的所有 App Store 销售现由 Apple 代扣税款。我们会按月代扣代缴应向相应税务机关缴纳的税款。自 2023 年 6 月开始,你可以在 5 月份的收入中查看从你的收益中扣除的税款金额。巴西境内的开发者不会受到这一变化的影响。


以上调整生效后,App Store Connect 中“我的 App”的“价格与销售范围”部分会随即更新。一如既往,你可以随时更改你的 App 和 App 内购买项目的价格 (包括自动续期订阅)。现在,你可以从 900 个价格点中选择,为任何店面更改定价。


码出新宇宙



WWDC23 就在眼前。太平洋夏令时间 6 月 5 日上午 10 点,Apple 主题演讲将在 apple.com 和 Apple Developer App 线上提供,为本次大会拉开序幕。你还可以通过同播共享,邀请朋友一起观看。


现在,符合条件的开发者可以开始报名参加活动了。相关活动包括 Q&A、“会见演讲者”以及社区暖场活动等线上聊天室活动,旨在促进你与开发者社区和 Apple 专家的沟通和交流。


Apple 公证服务更新


正如去年在 WWDC (简体中文字幕) 上宣布的那样,如果你目前使用 altool 命令行工具或者 Xcode 13 或更早版本通过 Apple 公证服务对 Mac 软件进行公证,则需要改为使用 notarytool 命令行工具,或者升级到 Xcode 14 或更高版本。自 2023 年 11 月 1 日起,Apple 公证服务将不再接受从 altool 或者 Xcode 13 或更早版本上传的内容。已经过公证的现有软件可以继续正常工作。


Apple 公证服务是一个自动化系统,它会扫描 Mac 软件中有没有恶意内容,检查有没有代码签名问题,并快速返回结果。对软件进行公证可向用户保证,Apple 已检查且未发现软件中包含恶意软件。


为改进 Apple 平台的安全性和隐私保护,用于验证 App 和关联 App 内购买项目销售的 App Store 收据签名媒介证书将更新为使用 SHA-256 加密算法。此更新将分多个阶段完成,新的 App 和 App 更新可能会受影响,具体取决于它们验证收据的方式。


Apple 设计大奖入围名单公布



Apple 设计大奖旨在表彰在多元包容、乐趣横生、出色互动、社会影响、视觉图像,以及创新思维等类别中表现出色的 App 和游戏。马上一睹今年的入围作品,我们将在太平洋夏令时间 6 月 5 日下午 6:30 揭晓获奖者,敬请关注。


提案


通过的提案


SE-0399 value 包展开的元组 提案通过审查。该提案已在 二十九期周报 正在审查的提案模块做了详细介绍。


SE-0397 独立声明 Macros 提案通过审查。该提案已在 二十八期周报 正在审查的提案模块做了详细介绍。


SE-0392 自定义 Actor 执行器 提案通过审查。该提案已在 二十五期周报 正在审查的提案模块做了详细介绍。


SE-0390 **引入 @noncopyable ** 提案通过审查。该提案已在 二十四期周报 正在审查的提案模块做了详细介绍。


Swift论坛



  1. 讨论从 Realm 数据库迁移提示?


提问


目前正在寻求迁移到更轻量级的解决方案(realm 目前对我的用例来说太过分了)并且想迁移到 grdb,但不必将 realm 作为依赖项持续一年或更长时间......


回答


在没有 Realm 库的情况下,您是否能够读取 Realm 数据库文件的内容? 否则,您必须将 Realm 作为依赖项保留,直到您的用户迁移完毕。


您可以通过发布能够要求用户升级的应用程序版本来缩短时间跨度。 这将允许您使用 “Realm-only”、“Realm-to-GRDB” 和最终的 “GRDB-only” 版本进行过渡。



  1. 提议允许 protocol 嵌套在非通用上下文中


介绍


允许协议嵌套在非通用 struct/class/enum/actors 和函数中。


动机


将标称类型嵌套在其他标称类型中允许开发人员表达内部类型的自然范围——例如,String.UTF8View 是嵌套在 struct String 中的 struct UTF8View,它的名称清楚地传达了它作为 UTF-8 代码接口的用途 - 字符串值的单位。


但是,嵌套目前仅限于在其他 struct/class/enum/actors 中的 struct/class/enum/actors; 协议根本不能嵌套,因此必须始终是模块中的顶级类型。 这很不幸,我们应该放宽此限制,以便开发人员可以表达自然作用于某些外部类型的协议。


建议的解决方案


我们将允许在非泛型 struct/class/enum/actors 中以及在不属于泛型上下文的函数中嵌套协议。


例如,TableView.Delegate 自然是与表视图相关的委托协议。 开发人员应该这样声明它——嵌套在他们的 TableView 类中:

class TableView {
protocol Delegate: AnyObject {
func tableView(_: TableView, didSelectRowAtIndex: Int)
}
}

class DelegateConformer: TableView.Delegate {
func tableView(_: TableView, didSelectRowAtIndex: Int) {
// ...
}
}

目前,开发人员采用复合名称(例如 TableViewDelegate)来表达可以通过嵌套表达的相同自然范围。


作为一个额外的好处,在 TableView 的上下文中,可以使用更短的名称来引用嵌套协议委托(与所有其他嵌套类型一样):

class TableView {
weak var delegate: Delegate?

protocol Delegate { /* ... */ }
}

协议也可以嵌套在非泛型函数和闭包中。 不可否认,这在某种程度上是有限的实用性,因为对此类协议的所有一致性也必须在同一功能内。 但是,也没有理由人为地限制开发人员在函数中创建的模型的复杂性。 一些代码库(值得注意的是,Swift 编译器本身)使用带有嵌套类型的大型闭包,并且它们受益于使用协议的抽象。

func doSomething() {

protocol Abstraction {
associatedtype ResultType
func requirement() -> ResultType
}
struct SomeConformance: Abstraction {
func requirement() -> Int { ... }
}
struct AnotherConformance: Abstraction {
func requirement() -> String { ... }
}

func impl<T: Abstraction>(_ input: T) -> T.ResultType {
// ...
}

let _: Int = impl(SomeConformance())
let _: String = impl(AnotherConformance())
}


  1. 提议PermutableCollection 协议


简介


该提案旨在添加一个 PermutableCollection 协议,该协议将位于集合协议层次结构中的 Collection 和 MutableCollection 之间。


动机


在某些情况下,人们希望能够移动和排序元素,同时不允许(或限制)元素的突变。 鉴于大量不太重要的收集协议,这是一个值得注意的遗漏。 创建自定义集合类型时,PermutableCollection 协议在任何强制元素唯一性和/或身份的有序集合中都是首选。 用例将包括即将推出的 OrderedDictionary 和 OrderedSet。 对于不可变和可变集合,它还可以提供对 Swift 使用的底层(并且可能是高度优化的)排序算法的统一访问。


设计


协议设计简单,只需一个 swapAt 要求

/// A collection that supports sorting.
protocol PermutableCollection<Element> : Collection where Self.SubSequence : PermutableCollection {

mutable func swapAt(_ i: Index, _ j: Index)

}

通过 swapAt 函数,通过扩展添加额外的排序函数实现。

extension PermutableCollection {

mutating func move(fromOffsets source: IndexSet, toOffset destination: Int) {
// move algorithm enacts changes via swapAt()
}

mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Index {
// partition algorithm enacts changes via swapAt()
}

mutating func sort() where Self: RandomAccessCollection, Self.Element: Comparable {
// partition algorithm enacts changes via swapAt()
}

// ... more permutation operations that mimic those available for MutableCollection

}



  1. 讨论 Vapor 和 query 缓存?




  2. 讨论在 Swift 系统中,如何将文件内容读取为字符串?




提问


我有一个文件的 FileDescriptor:


let fd = try FileDescriptor.open(<#filepath#>, .readOnly) 我可以使用 fd.read(into:) 将文件内容加载到 UnsafeMutableRawBufferPointer,但这是将文件内容加载到字符串中的正确第一步吗? 如果是这样,


在将它传递给 fd.read(into:) 之前,

  1. 我需要使用 .allocate(byteCount:alignment:) 分配 UnsafeMutableRawBufferPointer。 正确的 byteCount 取决于文件的大小。那么如何使用 Swift System 获取文件的大小呢?
  2. 如何从 UnsafeMutableRawBufferPointer 获取字符串?

回答


可以参考这个Git库:github.com/tayloraswif…




  1. 讨论为什么我不能使用 @dynamicMemberLookup 转发 enum cases?




  2. 讨论如何在 swift-foundation 中正确地进行性能测试?




提问


我想对比一下swift-foundation 和 Xcode 自带的 JSONDecoder 解码的速度。


我在一个新项目中使用单元测试和 measureBlock 以及在 swift-foundation 中使用 JSONEncoderTests 对其进行了测试。


swift-foundation 中的 JSONDecoder 看起来太慢了,我认为这是因为 swift-foundation 还没有作为一个库被引入。


推荐博文


iOS crash 报告分析系列 - 看懂 crash 报告的内容


摘要: 本篇文章主要介绍了iOS崩溃报告的解读方法,从报告的 Header、Exception information、Diagnostic messages、Backtraces、Thread state 和 Binary images 六个部分详细讲解了各字段含义,并提供示例代码帮助读者更好地理解。同时也引导读者去深入学习符号化的相关知识来获得更多信息。通过阅读本文,开发者可轻松看懂代码中产生的崩溃报告,并进行问题定位和处理。


SwiftUI 中 LinearGradient的用法


摘要: 这篇博文探讨了在 SwiftUI 中使用 LinearGradient 为对象创建渐变颜色效果。它展示了如何定义颜色数组、使用标准和自定义起点和终点,以及设置坐标以改进铅笔对象上的颜色笔尖。本文还包括用于创建具有各种起点终点组合的不同线性渐变的示例代码。文章以示例结束,展示了如何使用这些技术来自定义一支蓝色铅笔或整套铅笔的外观。


Swift 中的动态成员查找


摘要: 本文介绍了 Swift 语言中的动态成员查找(Dynamic Member Lookup)特性。通过在类型上使用 @dynamicMemberLookup 属性,我们可以重载该类型的 subscript 方法来更方便地访问其数据。但是,这也意味着缺乏编译时安全性。为了解决这个问题,本文提到了使用 KeyPath 作为参数的 subscript 方法来实现编译时安全检查。最后,作者建议我们可以谨慎地使用 @dynamicMemberLookup 特性来改进 API 设计。


话题讨论


有博主在视频社交平台说,2023年已然迎来了经济危机,只是有些人不愿意相信而已,那么你认为国内2023年是否真的进入了经济危机?


1.是的。确实已经经济危机了,今年工作很难找,同事比以前更卷啦,各种裁员消息不断。


2.经济危机不可能。五一淄博接待游客超过了100万人次,人挤人的旅游景象依然常在。


3.经济危机应该是相对的。对于大多数上班族来说,2023年很难,奉劝大家且行且珍惜。


关于我们


Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战SwiftUlSwift基础为核心的技术内容,也整理收集优秀的学习资料。


特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。


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

iOS webview跳转链接带#问题

iOS
一、问题引出 在iOS中,如果WKWebview跳转的链接不带参数但是带了#网页锚点,而你这边项目因为要兼容所有跳转链接,对链接进行了百分比编码,将#编码为了23%, 那么将出现”无法显示网页“或空白网页的情况。 同时满足下面3个条件会出现这个问题:配置的广...
继续阅读 »

一、问题引出


在iOS中,如果WKWebview跳转的链接不带参数但是带了#网页锚点,而你这边项目因为要兼容所有跳转链接,对链接进行了百分比编码,将#编码为了23%, 那么将出现”无法显示网页“或空白网页的情况。



同时满足下面3个条件会出现这个问题:

  • 配置的广告跳转链接中带了#符号,即有网页锚点。
  • 链接中是没有参数部分的,即?param1=value1&之类的。
  • webview加载这个链接之前,对链接整体进行了百分比编码,“#”符号被编码为”23%“

在实际的场景中,产品或运维配置广告链接时,有时需要打开网页后跳转到某个元素节点的,也就是有链接中带#这种需求的。


为了兼容他们配置带#链接这种情况,我们iOS这边需要代码上做兼容。


二、问题根因


1. 链接中#的作用


一般用于较长网页中,跳转到网页中的某个节点。 



2. 对配置链接进行调试探索


拿一个链接进行举例:
"juejin.cn/post/717682…" ,


进行百分比编码后:

对于上述链接"#"不进行编码:
  • 直接能加载成功, 并且跳转到锚点‘heading-4’。
  • 如果锚点名称写错了,如‘heading-4’写成了‘heading-400’,那么也能加载成功,只不过不会跳到锚点。

那么为什么#被编码为23%之后,就不能请求成功呢?


3. 链接中#是否被编码,服务器收到请求时有何异同?


我们对链接进行百分比编码后,通过Charles抓包请求的结果: 


可以看到:

  • 如果#编码为23%,则服务器收到的请求路径也是带23%.
  • 如果是未编码#,则服务器收到的请求路径是不带#后面的内容的。

这也就是说,对于iOS端来说,客户端发送请求时未发送#及后面的内容,但是会发送23%及后面的内容。 具体的响应是服务器决定的。


其中#编码为23%的两种情况:

  • 23%后面还有/, 比如https:www.xxx.com/path1/path23%/
  • 23%后面没有/,比如https:www.xxx.com/path1/path23%https:www.xxx.com/path1/path23%section1

第一种情况下,有的网页能加载出来,有的网页会找不到网页,能否加载成功是根据服务器能否找到网页来定;第二种加载会失败,原因是23%也被服务器拿去查找资源路径。


我相信到这里,应该已经解释清楚了问题发生的原因。


三、兼容链接#的解决方案


我们客户端APP上显示的营销广告链接都是来源于后台配置的,有时配置的链接是有需要跳到锚点的需求的,那么我们该怎么兼容呢?

  • 需要对链接进行百分比编码.
  • 百分比编码时需要屏蔽掉#.

解决方案

let url = "https://juejin.cn/post/7176823567059779639#heading-4"
var notEncodeSet = CharacterSet.urlQueryAllowed

// 关键代码:
// 在对链接进行百分比编码时,不编码字符集中追加#
notEncodeSet.insert(charactersIn: "#")

if let urlPath = url.addingPercentEncoding(withAllowedCharacters: notEncodeSet) {
// 一般会有对path追加自定义公参或者设置自定义请求头之类的事情...
let URL = URL(string: urlPath)!
let request = MutableURLRequest(url: URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
// 具体的加载
webview.load(request as URLRequest)
}

使用Alamofire的字符编码不能解决问题


在找到上述原因后,我们可能会考虑使用Alamofire的字符集CharacterSet.afURLQueryAllowed使用来代替系统的CharacterSet.urlQueryAllowed去编码,但这样有用吗?


首先来看下CharacterSet.afURLQueryAllowed是怎么生成的:

public static let afURLQueryAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4

let subDelimitersToEncode = "!$&'()*+,;="

let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)")
return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters)
}()

可以看到是由CharacterSet.afURLQueryAllowed中除去通用分隔符和子分隔符后生成,也就是说是系统字符集的一个子集,对于这个问题也是行不通的!!!


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

🤭新的广告方式,很新很新!

哈哈 会爬树的金鱼,树上的金鱼呦😀 前言 老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭ 产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道! 产品B:对! 大数据分析公司产品的广...
继续阅读 »

哈哈 会爬树的金鱼,树上的金鱼呦😀


前言


老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭


产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道!


产品B:对! 大数据分析公司产品的广告大部分来自移动端,pc端曝光率较少,可以增加一下曝光率,据统计移动端大部分广告收入来自首屏广告,我觉得pc也可以加上


程序员: PC哪有首屏广告啊,行业都没有先例


老板:这个好,没有先例! 我们又可以申请专利了Ψ( ̄∀ ̄)Ψ 搞起!!今年公司专利指标有着落了


程序员: 这也太影响体验了


产品A:就说能不能做吧, 今天面试的那个应届生不错能倒背chromium源码,还能手写react源码并且指出优化方案


程序员: 我做!!! ╭( ̄m ̄*)╮


先来个全屏遮罩🤔


这还不简单,直接一个定位搞定

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XX百货</title>
<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
display: none;
}
</style>
</head>
<body>
<div id="ADBox">广告</div>
</body>
</html>


搞定提测!


重来没接过这么简单的需求,送业绩这是╮(╯﹏╰)╭


第一次提测🤐


测试A: 送业绩这是? 产品说要和移动端一模一样,你这哪一样了?? 直系看需求文档!!


程序员: 需求文档就一句话, 和移动端一样的开屏广告, 这那不一样了?


测试A: 这哪一样了?? 你家移动端广告露个顶部出来?看看哪个app广告不是全屏的???


程序员: 啥? 还要全屏?? 行...


// 必须用点击事件触发才能全屏
document.addEventListener("click", async (elem) => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
}
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}, 5 * 1000);
});

搞定提测


第二次提测🙄


产品A: 嗯...有点感觉了,这鼠标去掉都遮住广告了,万一广告商不满意投诉怎么办?


程序员: 鼠标这么小这么能遮住广告??


产品B: 看我鼠标? (大米老鼠标PC主题)


程序员: ...

<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
// 隐藏广告Box让用户点任意地方激活
opacity: 0;

}
</style>

提测...


第三次提测🤕


测试A: 为啥还有鼠标???


程序员: 怎么那可能还有?


测试A: 过来看,鼠标不动的话还是会显示鼠标哦,动一下才消失


程序员: ##..行, 那我直接锁指针

    <script>
let pointerLockElement = null;
// 指针锁定或解锁
document.addEventListener(
"pointerlockchange",
() => {
// 锁定的元素是否为当前的元素 -- 没啥也意义可以去掉
if (document.pointerLockElement === pointerLockElement) {
console.log("指针锁定成功了。");
} else {
pointerLockElement = null;
console.log("已经退出锁定。");
}
},
false
);
// 锁定失败事件
document.addEventListener(
"pointerlockerror",
() => {
console.log("锁定指针时出错。");
},
false
);

// 锁定指针,锁定指针的元素必须让用户点一下才能锁定
function lockPointer(elem) {
// 如果已经存锁定的元素则不操作
if (document.pointerLockElement) {
return;
}
if (elem) {
pointerLockElement = elem;
elem.requestPointerLock();
}
}

// 解除锁定
function unlockPointer() {
document.exitPointerLock();
}

// 必须用点击事件触发才能全屏
document.addEventListener("click", async () => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
box.style.opacity = 1;
box.style.display = "block";
lockPointer(box);
}
// 5秒后解除锁定
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
unlockPointer();
box.style.display = "none";
}
}
}, 5 * 1000);
});
</script>

提测...


第四次提测😤


测试A: Safari上失效哦


程序员: 额....

<script>

// requestFullscreen 方法兼容处理
function useRequestFullscreen(elem) {
const key = ['requestFullscreen', 'mozRequestFullScreen', 'webkitRequestFullscreen', 'msRequestFullscreen']
for (const value of key) {
if (elem[value]) {
elem[value]()
return true
}
}
return false
}

// document.exitFullscreen 方法兼容处理
document.exitFullscreenUniversal = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen

// fullscreenElement 对象兼容处理
function getFullscreenElement() {
const key = ['fullscreenElement', 'webkitFullscreenElement']
for (const value of key) {
if (document[value]) {
return document[value]
}
}
return null
}

// fullscreenchange 事件兼容处理
addEventListener("fullscreenchange", endCallback);
addEventListener("webkitfullscreenchange", endCallback);

// requestPointerLock 方法在Safari下不可与 requestFullscreen 方法共用一个事件周期 暂无解决方法,必须让用户点两次鼠标,第一次全屏,第二次锁鼠标
// 同一事件周期内会出现的问题: 1.有小机率会正常执行, 2.顶部出现白条(实际上是个浏览器锁鼠标的提示语,但显示异常了) 3.锁定鼠标失败

</script>


结尾😩


产品A: 效果不错,但还有点小小的瑕疵,为啥要鼠标点一下才能弹广告,改成进入就弹窗吧


程序员: 要不还是找上次那个应届生来吧,改chromium源码应该能实现╭∩╮(︶︿︶)╭∩╮


效果预览: http://www.npmstart.top/BSOD.html


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

你知道什么是SaaS吗?

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。 本文从以下几个方面对SaaS系统召开介绍...
继续阅读 »

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。


  本文从以下几个方面对SaaS系统召开介绍:


  1. 云服务架构的三个概念

  2. SaaS系统的两大特征

  3. SaaS服务与传统服务、互联网服务的区别

  4. B2B2C

  5. SaaS系统的分类

  6. 如何SaaS化

  7. SaaS产品的核心组件

  8. SaaS多租户


一、云服务架构的三个概念


1.1 PaaS


英文就是 Platform-as-a-Service(平台即服务)


PaaS,某些时候也叫做中间件。就是把客户采用提供的开发语言和工具(例如Java,python, .Net等)开发的或收购的应用程序部署到供应商的云计算基础设施上去。
客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置。


PaaS 在网上提供各种开发和分发应用的解决方案,比如虚拟服务器和特定的操作系统。底层的平台3/4帮你铺建好了,你只需要开发自己的上层应用。这即节省了你在硬件上的费用,也让各类应用的开发更加便捷,不同的工作互相打通也变得容易,因为在同一平台上遵循的是同样的编程语言、协议和底层代码。


1.2 IaaS


英文就是 Infrastructure-as-a-Service(基础设施即服务)


IaaS 提供给消费者的服务是对所有计算基础设施的利用,包括处理 CPU、内存、存储、网络和其它基本的计算资源,用户能够部署和运行任意软件,包括操作系统和应用程序。
消费者不管理或控制任何云计算基础设施,但能控制操作系统的选择、存储空间、部署的应用,也有可能获得有限制的网络组件(例如路由器、防火墙、负载均衡器等)的控制。


IaaS 会提供场外服务器,存储和网络硬件,你可以租用。节省了维护成本和办公场地,公司可以在任何时候利用这些硬件来运行其应用。我们最熟悉的IaaS服务是我们服务器托管业务,多数的IDC都提供这样的服务,用户自己不想要再采购价格昂贵的服务器和磁盘阵列了,所有的硬件都由 IaaS 提供,你还能获得品质更高的网络资源。


1.3 SaaS


英文就是 Software-as-a-Service(软件即服务)


SaaS提供给客户的服务是运行在云计算基础设施上的应用程序,用户可以在各种设备上通过客户端界面访问,如浏览器。
消费者不需要管理或控制任何云计算基础设施,包括网络、服务器、操作系统、存储等等。


SaaS 与我们普通使用者联系可能是最直接的,简单地说任何一个远程服务器上的应用都可以通过网络来运行,就是SaaS了。国内的互联网巨头竭力推荐的 SaaS 应用想必大家已经耳熟能详了,比如阿里的钉钉,腾讯的企业微信,这些软件里面应用平台上的可供使用的各类SaaS小软件数不胜数,从OA,到ERP到CRM等等,涵盖了企业运行所需的几乎所用应用。


二、SaaS系统的两大特征



  1. 部署在供应商的服务器上,而不是部署在甲方的服务器上。

  2. 订购模式,服务商提供大量功能供客户选择,客户可以选择自己需要的进行组合,支付所需的价格,并支持按服务时间付费。


三、SaaS服务与传统服务、互联网服务的区别


3.1 SaaS服务


介于传统与互联网之间,通过租用的方式提供服务,服务部署在云端,任何用户通过注册后进行订购后获得需要的服务,可以理解成服务器及软件归供应商所有,用户通过付费获得使用权
image.png


3.2 传统软件


出售软件及配套设备,将软件部署在客户服务器或客户指定云服务器,出售的软件系统及运维服务为盈利来
image.png


3.3 互联网应用供应商


服务器部署在云端,所有用户可以通过客户端注册进行使用,广告及付费增值服务作为盈利来源
image.png


四、B2B2C


SaaS作为租户系统,需要为租户(C端)提供注册、购买、业务系统的入口,还得为B端(运营/运维)提供租户管理、流量监控、服务状态监控运维入口


五、SaaS系统的分类


5.1 业务型SaaS


定义:为客户的赚钱业务提供工具以及服务的SaaS,直面的是用户的生意,例如有赞微盟等电商SaaS以及销售CRM工具,为B2B2C企业;


架构以及商业模式:在产品的成长期阶段,为了扩充业务规模和体量,业务SaaS产品会拓展为“多场景+多行业”的产品模式,为不同行业或者不同场景提供适应的解决方案,例如做电商独立站的有赞,后期发展为“商城、零售、美业、教育”多行业的解决方案进行售卖。
image.png


5.2 效率型SaaS


定义:为客户效率提升工具的SaaS,如项目管理工具、Zoom等会议工具,提升办公或者生产效率,为B2B企业;


架构以及商业模式:不同于业务型的SaaS,效率SaaS思考得更多的是企业内存在一个大共性的效率的问题,不同的企业对于CRM销售系统的需求是不一样的,但都需要一个协同办公的产品来提升协作效率。对于效率类SaaS来说,从哪来到哪去是非常清晰的,就是要解决优化或者解决一个流程上的问题。
image.png


5.3 混合型SaaS


定义:即兼顾企业业务和效率效用SaaS,例如近几年在私域流量上大做文章的企业微信,其本身就是一个办公协同工具,但为企业提供了一整套的私域管理能力,实现业务的提升,同时也支持第三方服务。


架构以及商业模式:混合SaaS是业务和效率SaaS的结合体,负责企业业务以及企业管理流程的某类场景上的降本增效;因混合SaaS核心业务的使用场景是清晰且通用的,非核心业务是近似于锦上添花的存在,所以在中台产品架构上更接近为“1+X”组合方式——即1个核心业务+X个非核心功能,两者在产品层级上是属于同一层级的。
image.png


六、如何SaaS化



  1. 进行云化部署,性能升级,能够支持更大规模的用户访问

  2. 用户系统改造,支持2C用户登录(手机号一键登录、小程序登录、短信验证码登录)

  3. 网关服务,限流,接口防篡改等等

  4. 租户系统开发,包含租户基础信息管理、租户绑定资源(订购的功能)、租户服务期限等等

  5. 客户端改造(通常SaaS系统主要提供WEB端服务),页面权限控制,根据租户系统用户资源提供用户已购买的模块或页面

  6. 官网开发,功能报价单,功能试用、用户选购及支付

  7. 服务端接口数据权限改造、租户级别数据权限


七、SaaS产品的核心组件



  1. 安全组件:在SaaS产品中,系统安全永远是第一位需要考虑的事情

  2. 数据隔离组件:安全组件解决了用户数据安全可靠的问题,但数据往往还需要解决隐私问题,各企业之间的数据必须相互不可见,即相互隔离。

  3. 可配置组件:SaaS产品在设计之初就考虑了大多数通用的功能,让租户开箱即用,但任然有为数不少的租户需要定制服务自身业务需求的配置项,如UI布局、主题、标识(Logo)等信息

  4. 可扩展组件:SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术,在多个服务器上部署多个软件运行示例并提供相同的软件服务,以此实现水平扩展SaaS产品的整体服务性能

  5. 0停机时间升级产品:实现在不重启原有应用程序的情况下,完成应用程序的升级修复工作

  6. 多租户组件:SaaS产品需要同时容纳多个租户的数据,同时还需要保证各租户之间的数据不会相互干扰,保证租户中的用户能够按期望索引到正确的数据


八、SaaS多租户


8.1 多租户核心概念



  • 租户:一般指一个企业客户或个人客户,租户之间数据与行为是隔离的

  • 用户:在某个租户内的具体使用者,可以通过使用账户名、密码等登录信息,登录到SaaS系统使用软件服务

  • 组织:如果租户是一个企业客户,通常会拥有自己的组织架构

  • 员工:是指组织内部具体的某位员工。

  • 解决方案:为了解决客户的某类型业务问题,SaaS服务商将产品与服务组合在一起,为商家提供整体的打包方案。

  • 产品能力:指的是SaaS服务商对客户售卖的产品应用,特指能够帮助客户实现端到端场景解决方案闭环的能力。

  • 资源域:用来运行1个或多个产品应用的一套云资源环境

  • 云资源:SaaS产品一般都部署在各种云平台上,例如阿里云、腾讯云、华为云等。对这些云平台提供的计算、存储、网络、容器等资源,抽象为云资源。


8.2 三大模式


8.2.1 竖井隔离模式


image.png



  • 优势:



  1. 满足强隔离需求:一些客户为了系统和数据的安全性,可能提出非常严格的隔离需求,期望软件产品能够部署在一套完全独立的环境中,不和其他租户的应用实例、数据放在一起。

  2. 计费逻辑简单:SaaS服务商需要针对租户使用资源进行计费,对于复杂的业务场景,计算、存储、网络资源间的关系同样也会非常复杂,计费模型是很有挑战的,但在竖井模式下,计费模型相对来说是比较简单的。

  3. 降低故障影响面:因为每个客户的系统都部署在自己的环境中,如果其中一个环境出现故障,并不会影响其他客户使用软件服务。



  • 劣势:



  1. 规模化问题:由于租户的SaaS环境是独立的,所以每入驻一个租户,就需要创建和运营一套SaaS环境,如果只是少量的租户,还可能可以管理,但如果是成千上万的租户,管理和运营这些环境将会是非常大的挑战。

  2. 成本问题:每个租户都有独立的环境,花费在单个客户上的成本将非常高,会大幅削弱SaaS软件服务的盈利能力。

  3. 敏捷迭代问题:SaaS模式的一个优势是能够快速响应市场需求,迭代产品功能。但竖井隔离策略会阻碍这种敏捷迭代能力,因为更新、管理、支撑这些租户的SaaS环境,会变得非常复杂和低效。

  4. 统一管理与监控:在同一套环境中,对部署的基础设施进行管理与监控,是较为简单的。但每个租户都有独立的环境,在这种非中心化的模式下,对每个租户的基础设施进行管理与监控,同样也是非常复杂、困难的。


8.2.2 共享模式


image.png



  • 优势:



  1. 高效管理:在共享策略下,能够集中化地管理、运营所有租户,管理效率非常高。同时,对基础设施配置管理、监控,也将更加容易。相比竖井策略,产品的迭代更新会更快。

  2. 成本低:SaaS服务商的成本结构中,很大一块是基础设施的成本。在共享模型下,服务商可以根据租户们的实际资源负载情况,动态伸缩系统,这样基础设施的利用率将非常高。



  • 劣势:



  1. 租户相互影响:由于所有租户共享一套资源,当其中一个租户大量占用机器资源,其他租户的使用体验很可能受到影响,在这种场景下,需要在技术架构上设计一些限制措施(限流、降级、服务器隔离等),让影响面可控。

  2. 租户计费困难:在竖井模型下,非常容易统计租户的资源消耗。然而,在共享模型下,由于所有租户共享一套资源,需要投入更多的精力统计单个租户的合理费用。


8.2.3 分域隔离模式


image.png


8.3 多租户系统需要具备的能力



  1. 多个租户支持共享一套云资源,如计算、存储、网络资源等。单个租户也可以独占一套云资源。

  2. 多个租户间能够实现数据与行为的隔离,能够对租户进行分权分域控制。

  3. 租户内部能够支持基于组织架构的管理,可以对产品能力进行授权和管理。

  4. 不同的产品能力可以根据客户需求,支持运行在不同的云资源上。


8.4 多租户系统应用架构图


image.png

收起阅读 »

如何把一家创业公司搞垮

在拜读了耗子哥推荐的书《重来》之后,如何把一个创业公司搞垮,我得到了一些灵感。 追求完美的产品 我们都知道没有完美的产品,但是对于创业公司,想要做出完美的产品,至少需要付出很多的努力: 毫无 BUG:全面的产品交互设计、严格的编码过程、完整的用例测试等等。 ...
继续阅读 »

在拜读了耗子哥推荐的书《重来》之后,如何把一个创业公司搞垮,我得到了一些灵感。


追求完美的产品


我们都知道没有完美的产品,但是对于创业公司,想要做出完美的产品,至少需要付出很多的努力:



  1. 毫无 BUG:全面的产品交互设计、严格的编码过程、完整的用例测试等等。

  2. 大而全的功能:不要花时间去区分重要、次要,所有能力都得上,所有平台都得适配,所有功能都得支持。

  3. 延迟交付:追求完美产品,需要付出时间和精力,延期延期再延期。

  4. 沉默的反馈:开发完毕一个功能,要经过很久才能上线,迟迟得不到客户的真实反馈。


追求完美的产品,这个功能也要,那个功能也要,迟迟交付产品,磨灭团队信心,减少公司成功几率,钝刀子杀人


开会的技巧


会议是一种毒药,在开会的时候,我们要尽量扩大它的毒性:



  1. 没有明确的问题,没有确定的议程。

  2. 人员尽量扩大。不要精简会议人员,多增加无关人员。

  3. 每个人都发言。多听取一些低能儿的无效意见

  4. 去会议室。可以直接打断每个人的工作。

  5. 时长不限。不要限制开会的时间,时间越长越好。1 小时的会议,10 个人参加的话,就可以减少公司 10 小时的生产时间。


通过以上一些开会技巧,可以有效地增加会议的时长,多积累一些纸上谈兵的想法


做长期计划


做计划的本质是用过去指导未来,用以前的经验去圈套之后的变化。做长期计划,就可以把一个创业公司给套牢。


当创业公司按照一段时间去执行长期计划之后,如果发现事情不妙,可能会因为这么几个方面而硬着头皮继续执行:



  1. 沉没成本:我们都已经付出 4 个月的努力,不继续做下去很可惜吗?

  2. 傲慢自负:我们都已经定好目标了,再改变不是打脸吗?


满足客户


上线之后,要记住客户自上,来自客户的反馈都必须汲取,客户的要求都必须满足,让自己的产品成为一个臃肿的产品,成为一个臃肿产品的好处:



  1. 功能很多。用户所有的需求都能满足,意味着我们有对应处理需求的纷繁功能。

  2. 提高复杂性。每一个功能的增加,都需要对应交互,乱七八糟的功能可以让我们的产品交互变得复杂,界面花里胡哨。

  3. 拒绝新用户。通过提高产品复杂性,可以有效减少新的用户。

  4. 没有个性,平易近人。像一辆公交车一样,谁都可以上。


千万不要追求简洁,我们的目标是努力变成的微信,成就一款庞大、臃肿的垃圾产品


融资扩张,多招人手


当产品取得一定的成效,就需要马上融资,融资带来的好处太多:



  1. 更大的办公室,人数更多的公司

  2. 花别人的钱会上瘾

  3. 对公司失去控制权

  4. 投资人套现离场的风险

  5. 融资非常耗时耗力

  6. 产品可能偏向迎合投资人而不是客户


多招聘人手,新来的人:



  1. 对公司不了解

  2. 对项目不了解

  3. 互相谦让,互相客气

  4. 谁也不敢指出产品缺陷


集中力量办小事


人总是有限的,资源也总是有限的,我们需要正确地调用这些人力和资源,把他们都投入到小事中,如何做到呢?



  1. 不做取舍。在众多的事情中,不要去试图找到中心点,所有的任务都必须做,所有的需求都必须完成。

  2. 没有急事。把所有事情都当做急事,那也就没有急事。

  3. 唯唯诺诺。顺从永远比争锋相对容易,人们很容易同意添加一项新功能、接受一个过于乐观的最后期限、笑纳一个平庸的设计


不以盈利为目的


公司如何通过产品盈利的事情,尽量搁置,就像我们在设计神舟一号的时候,先假设地心引力不存在


一家企业不以盈利为目的,那么公司的可持续存活就有问题,可以给员工一些退出策略:



  • 如何破产清算,保障各位 n+1

  • 被其他公司收购


让员工少一点破釜沉舟的勇气,可以让公司早一点走向 Ending。






我是楷鹏,这是我阅读《重来》的读书笔记:wukaipeng.com/

read/rework

收起阅读 »

懂点心理学 - 曼德拉效应

最近在看电影 《消失的她》 ,里面提到了一个效应 - 曼德拉效应:修改他人记忆。 本文,我们来谈谈曼德拉效应。 什么是曼德拉效应 曼德拉效应,是指人们错误地记忆了某个特定的事件或情节的现象。产生的方式可以是让人们对新奇或者陌生事物的偏好会随着暴露的频率的增加...
继续阅读 »

最近在看电影 《消失的她》 ,里面提到了一个效应 - 曼德拉效应:修改他人记忆。


曼德拉效应.png


本文,我们来谈谈曼德拉效应


什么是曼德拉效应


曼德拉效应,是指人们错误地记忆了某个特定的事件或情节的现象。产生的方式可以是让人们对新奇或者陌生事物的偏好会随着暴露的频率的增加而增加。它表明通过重复和频繁的某种刺激,我们对于该刺激产生更积极的态度和更强烈的喜好。当然,也可以混淆/误导他人的思维 - 通常表示虚假的记忆。


总是穿着你老婆的衣服.png


跟在你身后.png


老公.png


那么这个酒店的工作人员.png


就会习惯性认为.png


她就是何太太.png


这个效应可以解析为什么人们倾向于更喜欢和接受他们熟悉的人、事物和概念。


如何应用曼德拉效应


曼德拉效应可以在广告、宣传和社交等领域中应用。


比如,上个星期笔者在京东 app 上浏览器一个牌子的茶壶🫖。然后,过了半个小时,自己刷朋友圈,微信推送了京东这个牌子的茶壶广告给我。给到我必须买这个牌子的错觉~


再比如,在工作中,你在现在这个公司遇到了一个问题。然后过了几天后,你将这个 issue 关闭掉。在某天,你向 leader 汇报工作演示操作的时候,却翻车了。因为你这个问题原来在上一家公司解决了,你却错误认为是在目前这家公司解决了。(大脑给到了错误的信号给你:这个问题你已经解决了,不必处理了)。当然,有一种很恐怖的职场现象:职场 PUA你什么都干不好,我什么都比你强...


如何避免曼德拉效应


曼德拉效应既然是错误的表象。那么我们可以:



  1. 检查自己的记忆:如果我们对某个情节的事件质疑,尝试回想并核对相关的证据。与他人交流,比较彼此的记忆。

  2. 养成记录重要的事项:对于比较重要的事情,比如借钱等,要写下日期、金额等重要信息,以减少记忆错误的风险。比如你朋友欠你 1000 块钱,期间他还了 100 块钱给你。然后过了几个月后,你问TA 还钱。TA 说:上一次,我不是全还给你了嘛。然后你会不会回忆下,期间确实还了一次,然后真以为他全还给你了。笔者也有记录的习惯,比如这篇文章 借点钱来“救急”【多图】

  3. 接受更正和反馈:如果他人提出了自己记忆不符的观点和事实,我们得深入了解事实,不仅要靠记忆和第三方证据,还要寻求更多来源的证据,比如录音等。了解了事实后,意识到自己的问题,要保持开放的心态接受讨论和反馈(这点要做到,太难了)。


参考


收起阅读 »

项目部署之后页面没有刷新怎么办?

web
最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。 浏览器输入url之后,就会进行下面一系列判断,来实现...
继续阅读 »

最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。


浏览器输入url之后,就会进行下面一系列判断,来实现页面渲染。



首先讲一下常见的http缓存~


HTTP缓存常见的有两类:



  • 强缓存:可以由这两个字段其中一个决定





    • expires

    • cache-control(优先级更高)





  • 协商缓存:可以由这两对字段中的一对决定





    • Last-Modified,If-Modified-Since

    • Etag,If--Match(优先级更高)




强缓存


使用的是express框架


expires


app.get('/login', function(req, res){
// 设置 Expires 响应头
const time = new Date(Date.now() + 300000).toUTCString()
res.header('Expires', time)
res.render('login');
});

然后我们在前端页面刷新,我们可以看到请求的资源的响应头里多了一个expires的字段, 取消Disable cache



刷新



勾选Disable cache



但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。


因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”


是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。


cache-control


其实cache-control跟expires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'max-age=300')
res.render('login');
});

前端页面响应头多了cache-control这个字段,且300s内都走本地缓存,不会去请求服务端



Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。


Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。


Cache-control的多种属性:developer.mozilla.org/zh-CN/docs/…


但是使用最多的就是no-cache和no-store,接下来就重点学习这两种


no-cache和no-store


no_cache是Cache-control的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache的意思是强制进行协商缓存。如果某一资源的Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});

no-cache(进行协商缓存,下次再次请求,没有勾选控制台Disable cache,状态码是304)



app.get('/login', function(req, res){
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-store')
res.render('login');
});

no-store(每次都请求服务器的最新资源,没有缓存策略)



强制缓存就是以上这两种方法了。现在我们回过头来聊聊,Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。


协商缓存


与强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲


Last-Modified,If-Modified-Since


简单来说就是:



  • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来

  • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对

  • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源


基于last-modified的协商缓存实现方式是:



  1. 首先需要在服务器端读出文件修改时间,

  2. 将读出来的修改时间赋给响应头的last-modified字段。

  3. 最后设置Cache-control:no-cache


三步缺一不可。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
res.header('last-modified', mtime.toUTCString())
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当index.css发生改变再次请求时



终端输出的时间变化



服务端的时间跟last-modified的值是一致的



使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。


1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。


2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。


为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)


Etag,If--Match


ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。


其实Etag,If--Match跟Last-Modified,If-Modified-Since大体一样,区别在于:



  • 后者是对比资源最后一次修改时间,来确定资源是否修改了

  • 前者是对比资源内容,来确定资源是否修改


那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

// const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
// console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
// res.header('last-modified', mtime.toUTCString())


// 设置ETag
const ifMatch = req.header['if-none-match']
const hash = crypto.createHash('md5')
const fileBuf = fs.readFileSync(path.join(__dirname, 'public/index.css'))
hash.update(fileBuf, 'utf8')
const etag = `"${hash.digest('hex')}"`
console.log(etag, '---etag----')
// 对比hash值
if (ifMatch === etag) {
res.status = 304
} else {
res.header('etag', etag)
// ctx.body = fileBuffer
}
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当资源发生改变时,状态码变成200,更新缓存


比如更改css样式



ETag也有缺点



  • ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。

  • ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。


值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。


disk cache & memory cache


磁盘缓存+内存缓存,这两种缓存不属于http缓存,而是本地缓存了~


我们直接打开掘金官网,点击network,类型选择all



可以看的很多请求,这里请求包括了静态资源+接口请求


这里我们能够看的很多请求的size中有很多是disk cache(磁盘缓存)


也有一些图片是memory cache(内存缓存)



这两者有什么区别呢?


disk cache: 磁盘缓存,很明显将内容存储在计算机硬盘中,很明显,这种缓存可以占用比较大的空间,但是由于是读取硬盘,所以速度低于内存


memory cache: 内存缓存,速度快,优先级高,但是大小受限于计算机的内存大小,很大的资源还是缓存到硬盘中


上面的浏览器缓存已经有三个大点了,那它们的优先级是什么样的呢?


缓存的获取顺序如下:


1.内存缓存


2.磁盘缓存


3.强缓存


4.协商缓存


如果勾选了Disable cache,那磁盘缓存都不存在了,之还有内存缓存



我还发现,勾选了Disable cache,就base64图片一定会在内存缓存中,其他图片则会发起请求;而不勾选了Disable cache,则大多数图片都在内存缓存中




CDN缓存


CDN缓存是一种服务端缓存,CDN服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,CDN再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。


如果客户端没有命中缓存,那接下来就要发起一次网络请求,根据网络环境,一般大型站点都会配置CDN,CDN会找一个最合适的服务节点接管网络请求。CDN节点都会在本地缓存静态文件数据,一旦命中直接返回,不会穿透去请求应用服务器。并且CDN会通过在不同的网络,策略性地通过部署边缘服务器和应用大量的技术和算法,把用户的请求指定到最佳响应节点上。所以会减少非常多的网络开销和响应延迟。


如果没有部署CDN或者CDN没有命中,请求最终才会落入应用服务器,现在的http服务器都会添加一层反向代理,例如nginx,在这一层同样会添加缓存层,代表技术是squid,varnish,当然nginx作为http服务器而言也支持静态文件访问和本地缓存技术,当然也可以使用远程缓存,如redis,memcache,这里缓存的内容一般为静态文件或者由服务器已经生成好的动态页面,在返回用户之前缓存。


如果前面的缓存机制全部失效,请求才会落入真正的服务器节点。


总结


1.如果页面是协商缓存,如何获取页面最新内容?


协商缓存比较好办,那就刷新页面,不过需要勾选Disable cache,但是用户不知道打开控制台怎么办?


那就右击页面的刷新按钮,然后选择硬性重新加载,或者清空缓存并硬性重新加载,页面就获取到最新资源了



2.如果页面没有设置cache-control,那默认的缓存机制是什么样的?



默认是协商缓存,这也符合浏览器设计,缓存可以减少宽度流量,加快响应速度


3.如果项目重新部署还是没有更新,怎么办?


在确定项目已经部署成功


这样子,可以去问一下公司的运维同事,这个项目是否有CDN缓存


如果项目的域名做了CDN缓存,就需要刷新CDN目录,来解决缓存问题了,不然就只能等,等CDN策略失效,来请求最新的内容


向如下配置的缓存策略,只有过30天才会去真正服务器去请求最新内容



当然你可以测试一下是否为CDN缓存,在url后面拼接一个参数,就能够获取到最新资源了,比如有缓存的链接是baidu.com/abc


你可以在浏览器中输入baidu.com/abc&t=1234来…


当然特定场景,我们不能随意给链接后面添加参数,所以这也只适用于测试一下是否有CDN缓存


所以最好的解决办法还是需要让运维同事去刷新目录,这样就能快速解决CDN缓存问题。


参考链接


juejin.cn/post/712719…


juejin.cn/post/717756…


xiaolincoding.com/

network/2_h…

收起阅读 »

我也惊呆了!关于数字广东对于 CEC-IDE 重大事件的道歉声明网友解读

喜大普奔 8 月 21 日,在某 gov.cn 官网上有一篇文章作出以下报告: 国内首款适配国产操作系统、自主可控的集成开发环境工具 CEC-IDE;国内首款数据安全极限生存保障产品——数据安全守护软硬件一体化产品;国内首款国密指纹认证鼠标…… 在《喜大...
继续阅读 »

喜大普奔


8 月 21 日,在某 gov.cn 官网上有一篇文章作出以下报告:



国内首款适配国产操作系统、自主可控的集成开发环境工具 CEC-IDE;国内首款数据安全极限生存保障产品——数据安全守护软硬件一体化产品;国内首款国密指纹认证鼠标……



image.png


《喜大普奔:全新自主研发的超强 CEC-IDE ,打破国外垄断》一文中有简要叙述。


网友挖掘


8 月 24 日,众多网友经过文件分析并在 vscode 官方仓库创建了编号为 #191279 和 #191229 的 issues,引来网友在该帖进行大量讨论。大量证据都在表明 CEC-IDE 涉嫌造假。


image.png


8 月 25 日,CEC-IDE 官网已无法访问。


image.png


import * as fs from "fs-extra";

const sourceExePath = "path/to/vscode.exe";
const iconFilePath = "path/to/new-icon.ico";

const sourceExeBuffer = fs.readFileSync(sourceExePath);
const iconFileBuffer = fs.readFileSync(iconFilePath);

const targetExeBuffer = replaceIconData(sourceExeBuffer, iconFileBuffer);

fs.writeFileSync("path/to/output.exe", targetExeBuffer);

function replaceIconData(sourceBuffer: Buffer, iconBuffer: Buffer): Buffer {
const targetBuffer = sourceBuffer.clone();
const iconDataOffset = 0x1234;
targetBuffer.fill(
iconBuffer,
iconDataOffset,
iconDataOffset + iconBuffer.length
);

return targetBuffer;
}

众所周知,对于软件开发,立项人是谁,目标是什么,开发人员是谁,测试人员是谁,验收人员是谁,这些都是很清楚的。


致歉声明


8 月 26 日,官方公众号发表致歉声明,这应该也侧面证实了此事。


CEC-IDE 道歉声明.png


声明解读



  • 8 月 24 日晚,我司获悉有网友发帖讨论我司 CEC-IDE 系统


在 8 月 21 日时各网络和电视媒体已进行 CEC-IDE 的宣传报告,表明在 21 日前按正常的开发流程来说,系统已经过测试、发布上线、验收。而我司获悉时是在 24 日,此前那么多时间都在做什么?流程都在做什么?众所周知,此类项目要走的时间和流程都是挺多的。



  • 公司管理层高度重视


从这个事件来看,至少在 26 日前是没有重视的。在 26 日后是不是真的重视?如何体现高度、体现重视,要采取什么样的措施,达到什么样的效果,只字未提。可能是保密调查?



  • 诚恳接受网友批评,并认真开展核查


image.png


诚恳接受网友批评这几个文字与未开启评论区形成鲜明对比。要知道发表当天 6 小时不到就有 6 万人阅读,但 0 评论。



  • CEC-IDE 系统由开发工具、后端系统和组件库组成...


作为一份致歉声明,有近 1/4 的内容是在讲述“列举工作”。



CEC-IDE 系统由开发工具、后端系统和组件库组成,其中开发工具使用开源 VSCode,进行了少量改造,增加了部分功能,后端系统开发了用户、权限、项目、需求等管理,以及任务协作和知识共享等功能,组件库中开发了公共能力组件。




  • 未用于商业用途


今年7月投入试运行,目前仍处在探索阶段,未用于商业用途。意思是不是在说:我们也才刚开始做就被发现了,所以问题不大。而且我们真的没有用于商业用途!但程序截图上的VIP登录和标志显得额外耀眼。


image.png



  • 因版本迭代更新中出现疏忽,近几个版本中缺失了 MIT 协议文件


出现疏忽导致近几个版本缺失 MIT 协议文件,疏忽一词避重就轻,表示我们只是不小心。但大家都知道从 近几个版本中缺失 来看,以前是有此文件(因为原仓库就有此文件)的,只是后面的版本中都被赤裸裸的删除了。


import * as fs from "fs-extra";
import * as yauzl from "yauzl";

const sourceExePath = "path/to/vscode.exe";
const targetExePath = "path/to/output.exe";
const mitLicenseText = "MIT License"; // 要删除的MIT协议文本

fs.copyFileSync(sourceExePath, targetExePath);

yauzl.open(targetExePath, { lazyEntries: true }, (error, zipfile) => {
if (error) throw error;

zipfile.readEntry();

zipfile.on("entry", (entry) => {
if (/\/$/.test(entry.fileName)) {
// 目录项,继续读取下一个entry
zipfile.readEntry();
} else {
// 文件项,处理文件内容
zipfile.openReadStream(entry, (error, readStream) => {
if (error) throw error;

let data = "";
readStream.on("data", (chunk) => {
data += chunk.toString("utf-8");
});

readStream.on("end", () => {
const updatedData = data.replace(mitLicenseText, "");
const writeStream = fs.createWriteStream(entry.fileName);
writeStream.write(updatedData, "utf-8");
writeStream.end();

zipfile.readEntry();
});
});
}
});

zipfile.on("end", () => {
console.log("MIT license removed successfully!");
});
});


  • 产品表述中“自主研发”等用语被网友质疑


被质疑,被 XX。等一系列的词,总让人有一种不能内省的感觉。而“自主研发”此类词语根本就不是单纯的自不自主那么简单。担忧从来不是自不自主开发,而是自信的磨灭、情怀的磨灭。



  • 数字广东公司向所有开源贡献者致以衷心



开源软件的使用极大提升了我司产品研发效率,开源项目为我司提供了巨大帮助,开源精神是程序员共同的同心圆,数字广东公司向所有开源贡献者致以衷心的感谢和崇高的敬意。



广大开源者可能不专门需要此敬意。但对 VSCODE 开发组应有,对默默真正投入自主研发的人应有此敬意。


相关链接


收起阅读 »

Android 时钟翻页效果

web
背景 今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。 原文链接:juejin.cn/post/724435… 具体实现分析请看上文原文链接,那我们开始吧! 容器 val space = 10f //上下半间隔 val...
继续阅读 »

背景


今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。


image.png
原文链接:juejin.cn/post/724435…


具体实现分析请看上文原文链接,那我们开始吧!


容器


val space = 10f //上下半间隔
val bgBorderR = 10f //背景圆角
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
canvas.drawRoundRect(
0f,
0f,
width.toFloat(),
upperHalfBottom,
bgBorderR,
bgBorderR,
bgPaint
)
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2
canvas.drawRoundRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat(),
bgBorderR,
bgBorderR,
bgPaint
)

image.png


绘制数字


我们首先居中绘制数字4


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
//居中显示
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
canvas.drawText(number4, x, y, textPaint)

image.png


接下来我们将数字切分为上下两部分,分别绘制。


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
// 上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)

image.png


翻转卡片


如何实现让其旋转呢?
而且还得是3d的效果了。我们选择Camera来实现。
我们先让数字'4'旋转起来。


准备工作,通过属性动画来改变旋转的角度。


private var degree = 0f //翻转角度
private val camera = Camera()
private var flipping = false //是否处于翻转状态
...
//动画
val animator = ValueAnimator.ofFloat(0f, 360f)
animator.addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float
setDegree(animatedValue)
}
animator.doOnStart {
flipping = true
}
animator.doOnEnd {
flipping = false
}
animator.duration = 1000
animator.interpolator = LinearInterpolator()
animator.start()
...

private fun setDegree(degree: Float) {
this.degree = degree
invalidate()
}

让数字'4'旋转起来:


  override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
canvas.drawText(number4, x, y, textPaint)
} else {
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
}
}

file.gif

我们再来看一边效果图:
我们希望将卡片旋转180度,并且0度-90度由上半部分完成,90度-180度由下半部分完成。


我们调整一下代码,先处理一下上半部分:


...
val animator = ValueAnimator.ofFloat(0f, 180f)
...
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
...
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

效果如下:


upper.gif

接下来我们再来看一下下半部分:


override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2

// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree > 90) {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

lower.gif

那我们将上下部分结合起来,效果如下:


all.gif

数字变化


好!我们完成了翻转部分,现在需要在翻转的过程中将数字改变:

我们还是举例说明:数字由'4'变为'5'的情况。我们思考个问题,什么时候需要改变数字?

上半部分在翻转开始的时候,上半部分底部显示的数字就应该由'4'变为'5',但是旋转的部分还是应该为'4',
下半部分开始旋转的时候底部显示的数字还是应该为'4',而旋转的部分该为'5'。


canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码显示的上下底部显示的内容,即上半部分地步显示5,下半部分显示4
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示上半部分旋转显示的内容,即数字4
} else {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示下半部分旋转显示的内容,即数字5
}

效果图如下:大伙可以在去理一下上面数字的变化的逻辑。


a.gif

最后我们加上背景再看一下效果:


a.gif

小结


上述代码仅仅提供个思路,仅为测试code,正式代码可不

作者:蹦蹦蹦
来源:juejin.cn/post/7271518821809438781
能这么写哦 >..<

收起阅读 »

程序员应避免自我安慰式的无效学习

重复学习了很多年 从事前端开发已经超过5年,自诩也是一个坚持学习的程序猿。 今年工作不是很忙,并且职场的35岁槛已经到来,既有时间又有压力,于是更多的时间在思考成长这件事。 最近在做的一件特别重要的事情是:做减法。 从事开发的这些年,因为待过的公司不同,后端的...
继续阅读 »

重复学习了很多年


从事前端开发已经超过5年,自诩也是一个坚持学习的程序猿。
今年工作不是很忙,并且职场的35岁槛已经到来,既有时间又有压力,于是更多的时间在思考成长这件事。


最近在做的一件特别重要的事情是:做减法。


从事开发的这些年,因为待过的公司不同,后端的语言不同,业务不同,加上给自己制定每年都要学习一门有价值的课程这样一个目标。先后学习了C#,PHP,JAVA。我这可不是走马观花式的学习,我是要求自己学习后至少能够使用相应语言的框架做简单基础开发。结果是除了C#外,我学会了PHP的Yii做后端开发,学会了Springboot做开发,虽然仅仅是常规的开发,但走过了从0-1的过程。


当然除了后端语言,前端技术栈从Vue、react、微信小程序、RN开发、Nodejs都有涉及,且都能进行日常开发。当然这里面最熟练还是Vue还有nodejs。后来我觉得做前端就得做全套,又花钱专门学了android app开发。虽然android平时不会涉及,学习的具体时间也是3年前了,但是也度过了从0-1的阶段。


学习了这些知识点,最大的一个结果是有道笔记我的知识笔记里面记录了大量的笔记。


学了这么多,照理说我应该对自己很有信心。但扪心自问我没有,我感觉我自己始终找不到让我特别自信的点,我想做自己的产品,但是始终没有做成。有段时间,我一直很迷茫。


现在回忆起来大概是因为我读了一本书《财富自由之路》,至于具体哪段内容我忘记了,反正我后来开始做减法,多个方面做减法,如下



  1. 收拾买的书籍,常用的放在明面上,不常用的收藏起来

  2. 不再买书,因为我发现我其实有大量的书只是看了开头

  3. 整理电脑桌面和文件夹,尤其整理做过的大量开发练习,分门别类并删除大量早期的和无用的

  4. 整理手机桌面和文件夹,手机从4屏变为2屏

  5. 整理浏览器的书签栏,分门别类

  6. 整理关注的股票,整理自选分类,坚决去掉自己不熟悉的,最后只留下不到10只

  7. 整理有道笔记里面笔记:共删除150多篇,重新划分目录


这里面感触最深的是整理有道笔记。我发现很多知识点我学了一遍又一遍,记了一次又一次,我每一年都会起很多诸如JavaScript学习笔记,Vue学习笔记,nodejs学习笔记等标题的笔记,但工作内容并没有特别大的变化,以前记住的知识点因为不经常温习和使用被忘掉,再次用到时候我会重新搜索出来然后再次记录。就这样反复着向前。


然而这样存在一个很大的问题:我在原地踏步。这个词很形象的形容了我的状况,看似学习了:记了笔记,但实际上根本没有进步,都是自我安慰,是对年龄带来的焦虑的缓解,是对社会给予的压力的缓解。


做减法之后,我想到盛传已久的一句话:太阳底下没有新鲜事。学习同样如此,任何学科都是有边界的。有边界意味着边界里面一定是在重复着某些知识点。只要找到这些知识点,总结这些知识点,迭代这些知识点,就可以避免重复无效的学习,进而真正进步。


划分知识结构


划分的原则:同一级不可以超过5个分类,因为人同时管理好的数量上限是5个左右。下面是一部分划分截图


开发技术.png


之后就是对最下级分类内容的填充和迭代。我是从2018年开始做的笔记。划分分类之后,我开始整理过去五年多的笔记,将笔记当中有用的属于对应分类的内容拿出来,填充到对应部分,同时删除原来的笔记。


我也将日常做了划分:


日常笔记.png


日常工作主要是一些日常的记录。日常分类和上述的开发技术,同属一个级别。都归属于我的文件夹下。我的文件夹:


我的文件夹.png


毛主席说过:好脑筋不如烂笔头。笔记的好处就是拓展思维的里程。


当然就划分来说,每个人的经历和认知是不同的,不同人有不同的划分标准。但是我觉得这不是重要的,重要的是聚焦注意力,最重要的是找到自己的世界,找到自己的内生动力。


找到自己的世界


刘青云出演的电视剧《大时代》有台词:一个人要成功,就一定要找到自己的世界。


猫腻的《择天记》男主有这样一句话:我修的是顺心意。这个时代谁修的不是顺心意呢?只有找到自己的世界顺自己的心意才能真正登堂入室,避免无效

作者:通往自由之路
来源:juejin.cn/post/7270906612339884093
的学习。因为此时才真正有了动力。

收起阅读 »

人情世故职场社会生存实战篇(三)

人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二) 21、问:我是做装饰行业的,我在多年前认识一个朋友,他呢混的比较好。经常给我介绍一些装修活儿,几十万的,一百多万的都交给我做。对我像自己人一样。我们萍水相逢 真的很感激他。因为我的不善...
继续阅读 »

人情人情世故职场社会生存实战篇(一)

人情人情世故职场社会生存实战篇(二)



21、问:我是做装饰行业的,我在多年前认识一个朋友,他呢混的比较好。经常给我介绍一些装修活儿,几十万的,一百多万的都交给我做。对我像自己人一样。我们萍水相逢 真的很感激他。因为我的不善交际,我们的来往并不多。想请教一下,在日常交往中,我该怎样做,以表现对他的感谢,以及怎样送礼给他,毕竟人家什么都不缺。


答:你认为富人不缺钱,这都是假象,富人的开销也是很大的。加油不要钱吗,物业不要钱吗,孩子读书不要钱吗,培训班不要钱吗,他们比我们这些屌丝更需要钱。因为我们一个月可能几千块就搞定了,他们一个月可能几万都不够。


你能给他添砖加瓦,习惯性的给他反馈,他也会觉得你是同道中人,是理解他的。王宝强搞定冯小刚,是因为他每年都给冯小刚送米送面。不要觉得没反馈,也可以获得资源,这会让你的路越走越窄。


反馈:1、你帮我介绍业务了,你就是我的大恩人,以后你的事儿就是我的事儿,需要我干啥您尽管吩咐。


2、我这个业务虽然本小利微,你可能也看不上,但是你确实帮我太多了,这点东西不成敬意,你一定要收下,不然我每晚每晚睡不着。


3、我们萍水相逢,能认识您,结交您是我的福气。您放心,只要您介绍的业务,我会百分百的让客户满意,亏钱也无所谓,不能坏了您的名声。(私人薇:xxxx)


22、问:明天晚上我们老板和我现任直接领导都在 ,我只能敬酒一次,怎么说比较好?既表达了对老板的敬意,又不得罪现任领导(老板,是以前的老领导)。


答:1、他们俩不在一个桌,你要先敬你的老板。


2、他俩要是在一个桌,你要对着领导介绍你原来的领导,说他多厉害多厉害。这个多厉害,用一句话说:以前我什么都听他的 ,现在他把我培养出来了。现在我又成了您的兵。一个是我的导师,一个是我的将军,我干了,您们二老随意。然后收摊就行了。


23、问:我是一名管理者。请问对于有本事,有才华,但名声不好的人怎么管理好呢?


答:看过水浒传吗,这样的人就是水浒传里的时迁,宋江怎么对时迁,你就怎么对他。给待遇,但不给级别。就是厚而不尊,因为时迁是个贼,所以不能把他排进领导班子,但是他贡献大,宋江给他丰厚的待遇和奖励,但不提拔他,这叫厚而不尊。你不厚,他不给你干;你要尊,队伍的名气就坏了。虽然时迁排名很靠后,不受尊重,没地位,但是时迁的个人待遇、工资奖金水平都很高。


24、问:我们处内有个女孩,小我10岁左右,她想提干,但是她没有得到上级认可,业务水平一般,但是家里有一定背景。她看到我要被提拔,就很嫉妒,拉拢处里另外一个刚入职不久的女孩, 孤立我。我现在必须要处理好同事关系,真要提拔,也要找处里同事谈话,不能出现反对的声音。现在我该怎么办?怎么和她相处呢?


答:你平时跟领导搞好关系,这事领导说了算,她说了不算,她那里你就示弱,在新来的那个姑娘那里多说她的好话,夸她能力强,情商高,说你们以前的一些事情,就是夸她,慢慢她就不好意思了,如果她再说你的坏话,新来的姑娘都鄙视她。


25、问:我们领导以前是财政局的,现在让我们每个口子都报表,因为我们下属也不只归我们管,所以他有气,我就成了夹板,下属公司这个月的报表已经报了5次了,他都不满意,每次都发火,而且每次标准不一样。怎么办?


答:先去拜师学艺,问问他标准是什么。按照他的标准去做,自然通过。买一条烟,去办公室请教∶领导,我这个人啊就是愚笨,还请您多指导指导,这个表到底是哪里不对,您指导一次我记住了,以后就按这个标准了,也不再惹您生气了,您指点指点我。


26、问:我们单位一把手要调走,我算是在他在位期间招入的和提起来的,去他办公室,如何表达感恩更完美?因为突然,我也没准备什么礼物,还有什么补救措施吗?


答:3个点,感恩+愧疚


1、没有你,就没有我的今天……


2、吃水不忘挖井人,我能帮你做点儿什么,我一定帮你去做……


3、我哪儿做的不对,你一定要说,我比较笨……


27、问:我是一个部门副职,遇到了这么个情况,我刚来任职,需要从下面的人那里获取数据信息,我才能开展我接下来的工作,然总是不给我主动汇报,总是要我催他们,怎么办呢?


答:你就问他们一个问题,就是上一届领导是怎么带他的,那么你就怎么带他们。也就是说你不要变动性太大,你要是变动性太大的话,大家都不屌你。最好的交接方式,大家问你问题的时候,你说以前你们怎么弄的,现在还怎么弄,我什么也不懂,我听大家的。那么这个问题就很快就解决了。新官上任三把火,第一把火,请员工吃饭,第二把火,表扬员工,第三个,你说我会为你们争取利益的。先把局面打开就OK了。


28、问:今天早上领导安排我去做工作份外的事,我就说我现在有事情做只做了一半,我就说我不去,领导就说叫你做什么就做什么,不做就叫我马上滚蛋,可是想一想,好我去做,我做了一上午才做完,问下我的做法有问题吗,接下来该怎么操作?


答:领导安排任务的时候不要拒绝,否则就是给领导难堪,先接受,对领导说:好的领导。你要是手头有事,可以问领导:领导,我现在手头上还有点事,那我现在是先做 A 还是先做 B,把皮球踢给老板, 领导如果问:你想做哪个?你说:我服从领导的安排。这样就是不得罪领导,自己做的慢也不受罚。你现在得罪了领导,领导生气了,去道歉就可以,说自己年轻不懂事,以后有不对的地方还请领导多多指教。


29、问:我们领导分配给我一个辅警辅助我工作,有时候他会和我使小脾气,他好像就是这性格习惯了,我有工作给他做,他不愿意做或者做的不仔细,我还得给他擦屁股。有时候我就不给他布置任务,我去跟领导说我忙不过来,某项工作给他做,然后他才会好好做。其实我特别想跟领导打他小报告,但是忍了。这该怎么办呢?


答:这个好办啊,你每天把工作分工,简单做个记录,你负责什么他负责什么。早上上班你跟他说:领导让我去汇报咱两的工作分工,您看一下没问题吧?你们两商量完了,写在本子上,然后你去跟领导请示汇报:领导,这是我们今天一天的工作,您还有没有其他的指示?回来你跟搭档说:领导说分工很好,晚上汇报完成情况。


30、问:老师 我最近走上了管理岗位。但是在人情世故方面还是有些胆怯。举个例子:被提拔了想五一想拜访领导,总是怕被拒绝,感觉踩不到领导在家的空闲点,时机如何把握呢?


答:这个太简单了,他的司机,你请了吗?他的秘书,你请了吗?先搞定他身边的人,让他身边的人帮你搞定领导,你+领导的司机+领导的秘书=你的队伍。你们3个人吃领导一个人,别心疼在小人物身上花钱,小人物有时候比大人物更有价值...

作者:公z号_纵横潜规则
来源:juejin.cn/post/7268260762401095699

收起阅读 »

虚拟列表 or 时间分片

前言 最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。 为啥要用虚拟列表呢! 在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布...
继续阅读 »

前言


最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。


为啥要用虚拟列表呢!


在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布局计算上花费太多时间,体验感不好,那你说要不要优化嘛,不是你被优化就是你优化它。


进入正题,啥是虚拟列表?


可以这么理解,根据你视图能显示多少就先渲染多少,对看不到的地方采取不渲染或者部分渲染。




这时候你完成首次加载,那么其他就是在你滑动时渲染,就可以通过计算,得知此时屏幕应该显示的列表项。


怎么弄?


备注:很多方案对于动态不固定高度、网络图片以及用户异常操作等形式处理的也并不好,了解下原理即可。


虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。


1、计算当前可视区域起始数据索引(startIndex)

2、计算当前可视区域结束数据索引(endIndex)

3、计算当前可视区域的数据,并渲染到页面中

4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上


由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>


  • infinite-list-container 为可视区域的容器

  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条

  • infinite-list 为列表项的渲染区域

    接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight

  • 假定列表每项高度固定,称之为itemSize

  • 假定列表数据称之为listData

  • 假定当前滚动位置称之为scrollTop

  •   则可推算出:

    • 列表总高度listHeight = listData.length * itemSize
    • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
    • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
    • 数据的结束索引endIndex = startIndex + visibleCount
    • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

      当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

    • 偏移量startOffset = scrollTop - (scrollTop % itemSize);

时间分片


那么虚拟列表是一方面可以优化的方式,另一个就是时间分片。


先看看我们平时的情况


1.直接开整,直接渲染。




诶???我们可以发现,js运行时间为113ms,但最终 完成时间是 1070ms,一共是 js 运行时间加上渲染总时间。

PS:

  • 在 JS 的 EventLoop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
  • 第一个 console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间
  • 第二个 console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次 EventLoop中执行的

那我们改用定时器


上面看是因为我们同时渲染,那我们可以分批看看。

let once = 30
let ul = document.getElementById('testTime')
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
setTimeout(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loopRender(100000, 0)

这时候可以感觉出来渲染很快,但是如果渲染复杂点的dom会闪屏,为什么会闪屏这就需要清楚电脑刷新的概念了,这里就不详细写了,有兴趣的小朋友可以自己去了解一下。

可以改用 requestAnimationFrame 去分批渲染,因为这个关于电脑自身刷新效率的,不管你代码的事,可以解决丢帧问题。

let once = 30
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

还可以改用 DocumentFragment


什么是 DocumentFragment



DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为 DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。

可以使用 document.createDocumentFragment方法或者构造函数来创建一个空的 DocumentFragment

ocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。



当 append元素到 document中时,被 append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。而 append元素到 documentFragment 中时,是不会计算元素的样式表,所以 documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当 append元素到 document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。

let once = 30 
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
let fragment = document.createDocumentFragment()
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
fragment.appendChild(li)
}
ul.appendChild(fragment)
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

其实同时渲染十万条数据这个情况还是比较少见的,就当做个了解吧。


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

iOS小技能: 抽奖轮盘跑马灯边框的实现

iOS
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情 前言 跑马灯的应用场景:iOS 抽奖轮盘边框动画 原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果 - (void)touchesBega...
继续阅读 »

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


前言


跑马灯的应用场景:

  1. iOS 抽奖轮盘边框动画


原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

[self rotate:_rotaryTable];

}

/**

iOS翻牌效果

*/
- (void)rotate:(id)sender {

[UIView beginAnimations:@"View Filp" context:nil];
[UIView setAnimationDelay:0.25];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:sender
cache:NO];
[UIView commitAnimations];

}


2. 在待办界面或者工作台界面,往往需要应用到跑马灯的地方


原理:利用QMUIMarqueeLabel 进行cell封装简易的跑马灯 label 控件


文章:kunnan.blog.csdn.net/article/det…





如用户登陆未绑定手机号,进行提示。



简易的跑马灯 label 控件,在文字超过 label 可视区域时会自动开启跑马灯效果展示文字,文字滚动时是首尾连接的效果 



I iOS 抽奖轮盘边框动画


1.1 原理


用NSTimer无限替换UIImageView的Image为互为错位的bg_horse_race_lamp_1或者bg_horse_race_lamp_2,达到跑马灯的效果



应用场景: iOS 抽奖轮盘边框动画



审核注意事项:



  1. 在抽奖页面添加一句文案“本活动与苹果公司无关”
    2, 在提交审核时修改分级至17+



1.2 实现代码

//
// ViewController.m
// horse_race_lamp
//
// Created by mac on 2021/4/7.
#import <Masonry/Masonry.h>


#import "ViewController.h"
NSString *const bg_horse_race_lamp_1=@"bg_horse_race_lamp_1";
NSString *const bg_horse_race_lamp_2=@"bg_horse_race_lamp_2";

@interface ViewController ()
/**

用NSTimer无限替换bg_horse_race_lamp_1和bg_horse_race_lamp_2,达到跑马灯的效果

应用场景: iOS 抽奖轮盘边框动画
*/
@property (nonatomic,strong) UIImageView *rotaryTable;
@property (nonatomic,strong) NSTimer *itemBordeTImer;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.


//通过以下两张图片bg_lamp_1 bg_lamp_2,用NSTimer无限替换,达到跑马灯的效果
_rotaryTable = [UIImageView new];
_rotaryTable.tag = 100;

[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];

[self.view addSubview:_rotaryTable];

[_rotaryTable mas_makeConstraints:^(MASConstraintMaker *make) {

make.center.offset(0);

}];



_itemBordeTImer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(itemBordeTImerEvent) userInfo:nil repeats:YES];


[[NSRunLoop currentRunLoop] addTimer:_itemBordeTImer forMode:NSRunLoopCommonModes];







}
// 边框动画
- (void)itemBordeTImerEvent
{
if (_rotaryTable.tag == 100) {
_rotaryTable.tag = 101;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_2]];
}else if (_rotaryTable.tag == 101){
_rotaryTable.tag = 100;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];
}
}




@end


1.3 下载Demo


从CSDN下载Demo:https://download.csdn.net/download/u011018979/16543761



private :https://github.com/zhangkn/horse_race_lamp


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

搞明白什么是零拷贝,就是这么简单

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。 接下来,让我们来理一理啊。 拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存...
继续阅读 »

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。


接下来,让我们来理一理啊。


拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存、硬盘等等一大堆东西。


这么复杂的设备要进行读写操作,其中繁琐和复杂程度可想而知。


传统I/O的读写过程


如果要了解零拷贝,那就必须要知道一般情况下,计算机是如何读写数据的,我把这种情况称为传统 I/O。


数据读写的发起者是计算机中的应用程序,比如我们常用的浏览器、办公软件、音视频软件等。


而数据的来源呢,一般是硬盘、外部存储设备或者是网络套接字(也就是网络上的数据通过网口+网卡的处理)。


过程本来是很复杂的,所以大学课程里要通过《操作系统》、《计算机组成原理》来专门讲计算机的软硬件。


简化版读操作流程


那么细的没办法讲来,所以,我们把这个读写过程简化一下,忽略大多数细节,只讲流程。



上图是应用程序进行一次读操作的过程。

  1. 应用程序先发起读操作,准备读取数据了;
  2. 内核将数据从硬盘或外部存储读取到内核缓冲区;
  3. 内核将数据从内核缓冲区拷贝到用户缓冲区;
  4. 应用程序读取用户缓冲区的数据进行处理加工;

详细的读写操作流程


下面是一个更详细的 I/O 读写过程。这个图可好用极了,我会借助这个图来厘清 I/O 操作的一些基础但非常重要的概念。



先看一下这个图,上面红粉色部分是读操作,下面蓝色部分是写操作。


如果一下子看着有点儿迷糊的话,没关系,看看下面几个概念就清楚了。


应用程序


就是安装在操作系统上的各种应用。


系统内核


系统内核是一些列计算机的核心资源的集合,不仅包括CPU、总线这些硬件设备,也包括进程管理、文件管理、内存管理、设备驱动、系统调用等一些列功能。


外部存储


外部存储就是指硬盘、U盘等外部存储介质。


内核态

  • 内核态是操作系统内核运行的模式,当操作系统内核执行特权指令时,处于内核态。
  • 在内核态下,操作系统内核拥有最高权限,可以访问计算机的所有硬件资源和敏感数据,执行特权指令,控制系统的整体运行。
  • 内核态提供了操作系统管理和控制计算机硬件的能力,它负责处理系统调用、中断、硬件异常等核心任务。

用户态


这里的用户可以理解为应用程序,这个用户是对于计算机的内核而言的,对于内核来说,系统上的各种应用程序会发出指令来调用内核的资源,这时候,应用程序就是内核的用户。

  • 用户态是应用程序运行的模式,当应用程序执行普通的指令时,处于用户态。
  • 在用户态下,应用程序只能访问自己的内存空间和受限的硬件资源,无法直接访问操作系统的敏感数据或控制计算机的硬件设备。
  • 用户态提供了一种安全的运行环境,确保应用程序之间相互隔离,防止恶意程序对系统造成影响。

模式切换


计算机为了安全性考虑,区分了内核态和用户态,应用程序不能直接调用内核资源,必须要切换到内核态之后,让内核来调用,内核调用完资源,再返回给应用程序,这个时候,系统在切换会用户态,应用程序在用户态下才能处理数据。


上述过程其实一次读和一次写都分别发生了两次模式切换。



内核缓冲区


内核缓冲区指内存中专门用来给内核直接使用的内存空间。可以把它理解为应用程序和外部存储进行数据交互的一个中间介质。


应用程序想要读外部数据,要从这里读。应用程序想要写入外部存储,要通过内核缓冲区。


用户缓冲区


用户缓冲区可以理解为应用程序可以直接读写的内存空间。因为应用程序没法直接到内核读写数据, 所以应用程序想要处理数据,必须先通过用户缓冲区。


磁盘缓冲区


磁盘缓冲区是计算机内存中用于暂存从磁盘读取的数据或将数据写入磁盘之前的临时存储区域。它是一种优化磁盘 I/O 操作的机制,通过利用内存的快速访问速度,减少对慢速磁盘的频繁访问,提高数据读取和写入的性能和效率。


PageCache

  • PageCache 是 Linux 内核对文件系统进行缓存的一种机制。它使用空闲内存来缓存从文件系统读取的数据块,加速文件的读取和写入操作。
  • 当应用程序或进程读取文件时,数据会首先从文件系统读取到 PageCache 中。如果之后再次读取相同的数据,就可以直接从 PageCache 中获取,避免了再次访问文件系统。
  • 同样,当应用程序或进程将数据写入文件时,数据会先暂存到 PageCache 中,然后由 Linux 内核异步地将数据写入磁盘,从而提高写入操作的效率。

再说数据读写操作流程


上面弄明白了这几个概念后,再回过头看一下那个流程图,是不是就清楚多了。


读操作
  1. 首先应用程序向内核发起读请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核向外部存储或网络套接字发起读操作;
  3. 将数据写入磁盘缓冲区;
  4. 系统内核将数据从磁盘缓冲区拷贝到内核缓冲区,顺便再将一份(或者一部分)拷贝到 PageCache;
  5. 内核将数据拷贝到用户缓冲区,供应用程序处理。此时又进行一次模态切换,从内核态切换回用户态;

写操作
  1. 应用程序向内核发起写请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核将要写入的数据从用户缓冲区拷贝到 PageCache,同时将数据拷贝到内核缓冲区;
  3. 然后内核将数据写入到磁盘缓冲区,从而写入磁盘,或者直接写入网络套接字。

瓶颈在哪里


但是传统I/O有它的瓶颈,这才是零拷贝技术出现的缘由。瓶颈是啥呢,当然是性能问题,太慢了。尤其是在高并发场景下,I/O性能经常会卡脖子。


那是什么地方耗时了呢?


数据拷贝


在传统 I/O 中,数据的传输通常涉及多次数据拷贝。数据需要从应用程序的用户缓冲区复制到内核缓冲区,然后再从内核缓冲区复制到设备或网络缓冲区。这些数据拷贝过程导致了多次内存访问和数据复制,消耗了大量的 CPU 时间和内存带宽。


用户态和内核态的切换


由于数据要经过内核缓冲区,导致数据在用户态和内核态之间来回切换,切换过程中会有上下文的切换,如此一来,大大增加了处理数据的复杂性和时间开销。


每一次操作耗费的时间虽然很小,但是当并发量高了以后,积少成多,也是不小的开销。所以要提高性能、减少开销就要从以上两个问题下手了。


这时候,零拷贝技术就出来解决问题了。


什么是零拷贝


问题出来数据拷贝和模态切换上。


但既然是 I/O 操作,不可能没有数据拷贝的,只能减少拷贝的次数,还有就是尽量将数据存储在离应用程序(用户缓冲区)更近的地方。


而区分用户态和内核态有其他更重要的原因,不可能单纯为了 I/O 效率就改变这种设计吧。那也只能尽量减少切换的次数。


零拷贝的理想状态就是操作数据不用拷贝,但是显示情况下并不一定真的就是一次复制操作都没有,而是尽量减少拷贝操作的次数。


要实现零拷贝,应该从下面这三个方面入手:

  1. 尽量减少数据在各个存储区域的复制操作,例如从磁盘缓冲区到内核缓冲区等;
  2. 尽量减少用户态和内核态的切换次数及上下文切换;
  3. 使用一些优化手段,例如对需要操作的数据先缓存起来,内核中的 PageCache 就是这个作用;

实现零拷贝方案


直接内存访问(DMA)


DMA 是一种硬件特性,允许外设(如网络适配器、磁盘控制器等)直接访问系统内存,而无需通过 CPU 的介入。在数据传输时,DMA 可以直接将数据从内存传输到外设,或者从外设传输数据到内存,避免了数据在用户态和内核态之间的多次拷贝。




如上图所示,内核将数据读取的大部分数据读取操作都交个了 DMA 控制器,而空出来的资源就可以去处理其他的任务了。


sendfile


一些操作系统(例如 Linux)提供了特殊的系统调用,如 sendfile,在网络传输文件时实现零拷贝。通过 sendfile,应用程序可以直接将文件数据从文件系统传输到网络套接字或者目标文件,而无需经过用户缓冲区和内核缓冲区。


如果不用sendfile,如果将A文件写入B文件。



  1. 需要先将A文件的数据拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区;

  2. 然后内核再将用户缓冲区的数据拷贝到内核缓冲区,之后才能写入到B文件;


而用了sendfile,用户缓冲区和内核缓冲区的拷贝都不用了,节省了一大部分的开销。


共享内存


使用共享内存技术,应用程序和内核可以共享同一块内存区域,避免在用户态和内核态之间进行数据拷贝。应用程序可以直接将数据写入共享内存,然后内核可以直接从共享内存中读取数据进行传输,或者反之。



通过共享一块儿内存区域,实现数据的共享。就像程序中的引用对象一样,实际上就是一个指针、一个地址。


内存映射文件(Memory-mapped Files)


内存映射文件直接将磁盘文件映射到应用程序的地址空间,使得应用程序可以直接在内存中读取和写入文件数据,这样一来,对映射内容的修改就是直接的反应到实际的文件中。


当文件数据需要传输时,内核可以直接从内存映射区域读取数据进行传输,避免了数据在用户态和内核态之间的额外拷贝。


虽然看上去感觉和共享内存没什么差别,但是两者的实现方式完全不同,一个是共享地址,一个是映射文件内容。


Java 实现零拷贝的方式


Java 标准的 IO 库是没有零拷贝方式的实现的,标准IO就相当于上面所说的传统模式。只是在 Java 推出的 NIO 中,才包含了一套新的 I/O 类,如 ByteBufferChannel,它们可以在一定程度上实现零拷贝。


ByteBuffer:可以直接操作字节数据,避免了数据在用户态和内核态之间的复制。


Channel:支持直接将数据从文件通道或网络通道传输到另一个通道,实现文件和网络的零拷贝传输。


借助这两种对象,结合 NIO 中的API,我们就能在 Java 中实现零拷贝了。


首先我们先用传统 IO 写一个方法,用来和后面的 NIO 作对比,这个程序的目的很简单,就是将一个100M左右的PDF文件从一个目录拷贝到另一个目录。

public static void ioCopy() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
System.out.println("传输 " + formatFileSize(sourceFile.length()) + " 字节到目标文件");
} catch (IOException e) {
e.printStackTrace();
}
}

下面是这个拷贝程序的执行结果,109.92M,耗时1.29秒。



传输 109.92 M 字节到目标文件
耗时: 1.290 秒



FileChannel.transferTo() 和 transferFrom()


FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。


这两个方法首选用 sendfile 方式,只要当前操作系统支持,就用 sendfile,例如Linux或MacOS。如果系统不支持,例如windows,则采用内存映射文件的方式实现。


transferTo()


下面是一个 transferTo 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferTo() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

只耗时0.536秒,快了一倍。



传输 109.92 M 字节到目标文件
耗时: 0.536 秒



transferFrom()


下面是一个 transferFrom 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferFrom() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.603 秒



Memory-Mapped Files


Java 的 NIO 也支持内存映射文件(Memory-mapped Files),通过 FileChannel.map() 实现。


下面是一个 FileChannel.map()的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

    public static void nioMap(){
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long fileSize = sourceChannel.size();
MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
targetChannel.write(buffer);
System.out.println("传输 " + formatFileSize(fileSize) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.663 秒



推荐阅读


我的第一个 Chrome 插件上线了,欢迎试用!


前端同事最讨厌的后端行为,看看你中了没有


RPC框架的核心到底是什么


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

27岁程序媛未来的出路到底在哪里?

不太聪明的脑子的思考原因 最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养, 看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安, 不过作为i型...
继续阅读 »

不太聪明的脑子的思考原因


最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养,

看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安,

不过作为i型人格真的很喜欢这种沉浸式工作,暂时没有换行业的打算,所以还是先从目前做程序出发,去提升自己的能力,争取能再多干个几年,然后回东北老家花几万块买个小房子,开始我的摆烂养老人生
(人生终极目标)


在此总结一下今年上半年的成果和下半年的目标吧~


上半年成果


1.刷力扣拿到排名


摆烂人生是在去年感知到危机的时候结束的,于是开始疯狂刷LeetCode,学习算法,最终的结果是对待代码问题脑子变得灵光了但生活中越发糊涂了,但是目前困难的题还是基本摸不到头绪的状态,好多数学公式也不知道,位运算符也不咋会用,就目前感觉自己还是很差,提升的空间还是非常非常高的

(今年四月拿到排名时截的图)



2.开始准备软考


年初的时候开始考虑考一个专业资格证,于是开始做一些功课,上半年从bilibili上看了一些公开的课先做了初步了解,六月份买了一套课开始进行系统的学习,备战11月的考试



3.涨薪


很幸运自己能在目前经济环境下行的情况下没有失业,并且领导对我还算认可,给我们在竞争中留下来的人涨了工资,但说是涨薪,其实最终结果我们未必拿到的多了,因为目前公司效益不景气,如果公司效益持续低迷,年底的14薪必定要打水漂,但是还能稳定的存活下来也算是比较满意了,真心希望公司越来越好,因为我们的老板人真的非常不错(虽然我不接受pua但是发自内心感谢公司)


4.买了自行车开始骑行健身


其实早就想买个自行车,可以骑行上班,周末也可以当运动,不过身边的好多人都不赞同,因为像夏天太热、冬天太冷、刮风下雨都骑不出去,但是最终我还是买了,嘎嘎开心,不过确实影响因素很多最终也没骑过几次哈哈(主要是本人太懒总是找借口不骑车出门)



下半年目标


1.软考通过!


最近还是按照规划的持续学习,每个月给自己定一个总体的目标,然后分到每一天里去,现在距离考试还有两个多月,还是要加油的!


2.争取换一个更高的平台


感觉目前的公司体量还是太小了,做了很多微信小程序,工作对自己的提升已经到达了极限,但是就目前的情况来说,还是对年底的14薪抱有一丝丝幻想,所以这个目标可能在今年年底或者明年去达成


3.持续精进算法


还是在有条不紊的刷LeetCode,给自己的最低要求是每周至少一道中级,保持一个持续学习的状态


4.做一个开源项目


这个规划应该会在11月份开始实施,或者如果突然来了灵感可以立马启动,也是给以后面试提供一个优势条件吧


最后希望还在圈子中的同行们也能越来越好,不管这些努力会不会给自己带来实质性的收益,本质上都是在提升自己,目的其实很简单,就是不被这日新月异的时代所淘汰,
就好像一句金句里描述的那样:我们所做的一切,不是为了改变世界,而是不让世界改变我们!



最后,大家有什么能提升自己的点子也可以给我留言,让我们一起努力吧,加油!




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

[译] 2021 年的 SwiftUI: 优势、劣势和缺陷

iOS
2021 年的 SwiftUI: 优势、劣势和缺陷 在生产环境使用 SwiftUI?仍然不可行。 过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数...
继续阅读 »

2021 年的 SwiftUI: 优势、劣势和缺陷



在生产环境使用 SwiftUI?仍然不可行。



由 Maxwell Nelson 在 Unsplash 发布


过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数问题的解决方法。


简而言之,SwiftUI 是一个很棒的框架,并且极具前景。我认为它就是未来。但是要达到和 UIKit 同等的可靠性和健壮性,可能还需要 3-5 年。但是这并不意味着现在不应该使用 SwiftUI。我的目的是帮助你理解它的利弊,这样你可以就 SwiftUI 是否适合下一个项目做出更明智的决定。


SwfitUI 的优势


1. 编写 SwiftUI 是一件乐事,而且你可以快速构建用户界面


使用 addSubviewsizeForItemAtIndexPath,小心翼翼地计算控件的大小与位置,应对烦人的约束问题,手动构建视图层次结构,这样的日子已经一去不复返了。SwiftUI 的声明式和响应式设计模式使得创建响应式布局和 React 一样简单,同时它还背靠 Apple 强大的 UIKit。用它构建、启动并运行视图快到不可思议。


2. SwiftUI 简化了跨平台开发


我最兴奋的事情就是只需要编写一次 SwiftUI 代码,就可以在 iOS (iPhone 和 iPad),WatchOS 和 macOS 上使用。同时开发和维护 Android 和 Windows 各自的代码库已经很困难了,所以在减少不同代码库的数量这方面,每一个小的改变都很有帮助。当然还是有一些缺点,我将会在 “劣势” 章节分享。


3. 你可以免费获取漂亮的转场效果,动画和组件


你可以把 SwiftUI 当作一个 UI 工具箱,这个工具箱提供了开发专业应用程序所需的所有构建块。另外,如果你熟悉 CSS 的 Transition 属性,你会发现 SwiftUI 也有一套类似的方法,可以轻松创建优雅的交互过程。声明式语法的魅力在于你只需要描述你需要什么样的效果,效果就实现了,这看上去像魔法一样,但是也有不好的一面,我之后将会介绍。


4. UI 是完全由状态驱动并且是响应式的


如果你熟悉 React 的话,SwiftUI 在这一点上完全类似。当你监听整个 UI 的”反应“,动画和所有一切的时候,你只需要修改 @State@Binding 以及 @Published 属性,而不是使用多达几十层的嵌套回调函数。使用 SwiftUI,你可以体会到 CombineObservableObject 以及 @StateObject 的强大。这方面是 SwiftUI 和 UIKit 最酷的区别之一,强大到不可思议。


5. 社区正在拥抱 SwiftUI


几乎每个人都在因为 SwiftUI 而兴奋。SwiftUI 有许多学习资源可供获取,从 WWDC 到书,再到博客 —— 资料就在那里,你只需要去搜索它。如果不想搜索的话,我这里也汇总了一份最佳社区资源列表。


拥有一个活跃且支持度高的社区可以加速学习,开发,并且大量的新库会使得 SwiftUI 用途更加广泛。


劣势


1. 不是所有组件都可以从 SwiftUI 中获取到


在 SwiftUI 中有许多缺失、不完整或者过于简单的组件,我将在下面详细介绍其中一部分。


使用 UIViewRepresentableUIViewControllerRepresentableUIHostingController 协议可以解决这一问题。前两个让你可以在 SwiftUI 视图层中嵌入 UIKit 视图和控制器。最后一个可以让你在 UIKit 中嵌入 SwiftUI 视图。在 Mac 开发中也存在类似的三种协议 (NSViewRepresentable 等)。


这些协议是弥补 SwiftUI 功能缺失的权宜之计,但并不是一直天衣无缝。而且,尽管 SwiftUI 的跨平台承诺很好,但是如果某些功能不可用的话,你仍然需要为 iOS 和 Mac 分别实现协议代码。


2. NavigationView 还没有真正实现


如果你想在隐藏导航栏的同时仍然支持滑动手势,这是不可能的。我最终参考一些找到的代码创建了一个 UINavigationController wrapper。尽管可以起作用,但这不是一个长远的解决方案。


如果你想要在 iPad 上拥有一个 SplitView,但目前你还不能以纵向模式同时展示主视图和详情视图。他们选择用一个简陋的按钮展示默认关闭的抽屉。显然,你可以通过添加 padding 来解决这个问题,它可以突出显示你在使用 SwiftUI 时必须做的事情。


当你想使用编程式导航的时候,NavigationLink 是一种流行的解决方案。这里有一个有趣的讨论


3. 文本输入十分受限


TextFieldTextEditor 现在都太简单了,最终你还是会退回到 UIKit。所以我不得不为 UITextFieldUITextView 构建自己的 UIViewRepresentable 协议(以实现文本行数的自动增加)。


4. 编译器困境


当视图开始变得笨重,并且你已经竭尽所能去提取分解,编译器仍然会冲着你咆哮:



The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.



这个问题已经多次拖慢进度。由于这个问题,我已经很擅长注释代码定位到引起问题的那一行,但是 2021 年了还在用这种方法调试代码感觉非常落后。


5. matchedGeometryEffect


我第一次发现这个的时候,感觉很神奇。它目的是通过匹配一隐一现的几何形状,帮助你更加流畅地转换两个不同标识的视图。我觉得这有助于从视图 A 优雅地转场到 B 视图。


我一直想让它起作用。但最终还是放弃了,因为它并不完美。此外,在包含大量列表项的 ListScrollView 中使用它会导致项目瘫痪。我只推荐在同一视图中使用这个做简单的转换过渡。当你在多个不同的视图中共享一个命名空间的时候(包括转场期间的视图剪裁在内),事情就会开始变得奇怪。


6. 对手势的支持有限


SwiftUI 提供了一系列新的手势(即 DragGestureLongPressGesture)。这些手势可以通过 gesture 修饰符(如 tapGesturelongPressGesture)添加到视图中。它们都能正常工作,除非你想要做更复杂的交互。


比如,DragGestureScrollView 交互就不是很好。即使有了 simultaneousGesture 修饰符,在 ScrollView 中放一个 DragGesture 还是会阻止滚动。在其他情况下,拖动手势可以在没有任何通知的情况下被取消,使得手势处于不完整状态。


为了解决这个问题,我构建了自己的 GestureView,它可以在 SwiftUI 中使用 UIKit 手势。我会在下一篇关于最佳 SwiftUI 库和解决方案的文章中分享这部分内容。


7. 分享扩展中的 SwiftUI


我可能是错的,但是分享扩展还是使用 UIKit 吧。我通过 UIHostingController 用 SwiftUI 构建了一个分享扩展,当分享扩展加载完毕后,有一个非常明显的延迟,用户体验较差。你可以尝试通过在视图中添加动画去掩盖它,但是仍然有 500 毫秒左右的延迟。


值得一提的点

  • 无法访问状态栏 (不能修改颜色或拦截点击)
  • 由于缺少 App,我们仍然需要 @UIApplicationDelegateAdaptor
  • 不能向后兼容
  • UIVisualEffectsView 会导致滚动延迟(来源于推特:@AlanPegoli

缺陷


1. ScrollView


这是迄今为止最大的缺点之一。任何一个构建过定制化 iOS 应用的人都知道我们有多依赖 ScrollView 去支持交互。

  • 主要的障碍:视图中的 LazyVStack 导致卡顿、抖动和一些意外的行为LazyVStack 对于需要滚动的混合内容(如新闻提要)的长列表至关重要。仅凭这一点,SwiftUI 就还没准备好投入生产环境: Apple 已经证实,这是 SwiftUI 自身的漏洞。尚未清楚他们什么时候会修复,但是一旦修复了,这将是一个巨大的胜利。
  • 滚动状态:原生不支持解析滚动的状态(滚动视图是否正在被拖拽?滚动?偏移多少?)。尽管有一些解决方案,但是还是很繁琐且不稳定。
  • 分页:原生不支持分页滚动视图。所以打消实现类似于可滑动的媒体库的念头吧(但是如果你想要关闭一些东西的时候,可以使用 SwiftUIPager)。在技术上你可以使用 TabView 加 PageTabViewStyle,但是我认为它更适合少部分的元素,而不是大的数据集。
  • 性能:使用 List 是性能最好的,并且避免了 LazyVStack 的卡顿问题,但由于工作方式的转换,它仍然不适合显示可变大小的内容。例如,在构建聊天视图时,其过渡很奇怪,会裁剪子视图,并且无法控制插入的动画样式。

结论


毫无疑问我觉得应该学习 SwiftUI ,自己去理解它,并享受乐趣。但是先别急着全盘采用。


SwiftUI 已经为简单的应用程序做好了准备,但是在写这篇文章的时候(iOS 15,beta 4 版本),我不认为它已经适合复杂应用程序的生产环境,主要是由于 ScrollView 的问题和对 UIViewRepresentable 的严重依赖。我很遗憾,尤其是像即时通信产品,新闻摘要,以及严重依赖复杂视图或者想要创建手势驱动的定制体验产品,目前还不适合使用 SwiftUI。


如果你想要精细的控制和无限的可能性,我建议在可预见的未来坚持使用 UIKit。你可以在一些视图(如设置页)里通过使用 UIHostingController 包装 SwiftUI 视图以获得 SwiftUI 的好处。


未来会发生什么?


当开始着手我们项目的下一次大迭代的时候。我知道这个新项目的交互范围不在 SwiftUI 目前支持的范围之内。即使当我知道 SwiftUI 在某些关键方面存在不足的时候,我的心都碎了,但是我还是不打算退回到 UIKit,因为我知道当 SwiftUI 运行起来时,构建它是一件多么快乐的事情。它的速度如此之快。


SwiftUI 会兼容 UIKit 么?如果这样的话,我们可能需要等待 SwiftUI 使用 3-5 年的时间来移植所有必要的 UIKit API。如果 SwiftUI 不准备兼容 UIkit,那你也能通过 SwiftUI 封装的方式使用 UIKit。


我好奇的是 Apple 会在 SwiftUI 上投入多少。他们是否有让所有的开发者采用 SwiftUI 的长期计划,或者说 SwiftUI 只是另一个界面构建器而已?我希望不是,也希望他们能全心投入 SwiftUI,因为它的前景是非常诱人的。


更多看法


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

iOS crash 报告分析系列 - 看懂 crash 报告的内容

iOS
在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。 不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。 每当 App 发生崩溃时,系统会自动生成一个后缀 i...
继续阅读 »

在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。


不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。


每当 App 发生崩溃时,系统会自动生成一个后缀 ips 的崩溃报告。我们可以通过崩溃报告来进行问题定位。但崩溃报告的内容繁多,新手看很容易一脸懵。所以本文先讲解一下报告中各字段的含义,后面再说报告符号化。


废话不多说,让我们开始吧!


前期准备


首先,报告解读我们需要先生成一个 crash 报告。


1、新建一个项目,在 ViewController 中写下面的代码:

NSString *value;
NSDictionary *dict = @{@"key": value}; // 字典的 value 不可为 nil,所以会崩溃

2、在真机上运行项目,然后去设置 - 隐私与安全性 - 分析与改进 - 分析数据,拿去生成的 crash 报告(报告的名字与项目名字一致,比如我的项目名为:CrashDemo,崩溃报告的名则为:CrashDemo-2023-05-30-093930.ips)。


注意:连着 Xcode 运行时不会产生崩溃报告,需要真机拔掉数据线再次运行 app 才会生成崩溃报告。


拿到报告,接下来就是解读了。


报告内容解读


官网的示例图:




Header


首先来看 Header:

Incident Identifier: 9928A955-FE71-464F-A2AF-A4593A42A26B
CrashReporter Key: 7f163d1c67c5ed3a6be5c879936a44f10b50f0a0
Hardware Model: iPhone14,5
Process: CrashDemo [45100]
Path: /private/var/containers/Bundle/Application/6C9D4CF7-4C16-4B50-A4A5-389BED62C699/CrashDemo.app/CrashDemo
Identifier: cn.com.fengzhihao.CrashDemo
Version: 1.0 (1)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: cn.com.fengzhihao.CrashDemo [3547]

Date/Time: 2023-05-30 09:39:29.6418 +0800
Launch Time: 2023-05-30 09:39:28.5579 +0800
OS Version: iPhone OS 16.3.1 (20D67)
Release Type: User
Baseband Version: 2.40.01
Report Version: 104

Header 主要描述了目标设备的软硬件环境。比如上图可以看出:是 iphone 14 的设备,系统版本是16.3,发生崩溃的事件是 2023-05-30 09:39:29 等等。


需要注意的是 Incident Identifier 相当于当前报告的 id,报告和 Incident Identifier 是一一对应的关系,绝对不会存在两份不同的报告 Incident Identifier 相同的情况。


Exception information

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000

这一部分主要是告诉我们 app 是因为什么错误而导致的崩溃,但不会包含完整的信息。


可以看到当前的 Type 为:EXC_CRASH (SIGABRT),这代表当前进程因收到了 SIGABRT 信号而导致崩溃,这是一个很常见的类型,字典 value 为nil或者属于越界等都会是此类型。更多的 Exception Type 解释请参见此处


Diagnostic messages

Application Specific Information:
abort() called

操作系统有时包括额外的诊断信息。此信息使用多种格式,具体取决于崩溃的原因,并且不会出现在每个崩溃报告中。


本次的崩溃原因是因为调用了 abort() 函数。


接下来,就是报告的重点了。


Backtraces


这部分记录了当前进程的线程的函数调用栈,我们可以通过调用栈来定位出问题的代码。


崩溃进程的每一条线程都会被捕获成回溯。回溯会展示当前线程被中断时的线程的函数调用栈。如果崩溃是由于语言异常造成的,会额外有一个Last Exception Backtrace,位于第一个线程之前。关于 Last Exception Backtrace 的详细介绍请看这里


比如我们示例中的崩溃就是由于语言异常造成的,所以崩溃报告中会有 Last Exception Backtrace。

Last Exception Backtrace:
0 CoreFoundation 0x191560e38 __exceptionPreprocess + 164
1 libobjc.A.dylib 0x18a6f78d8 objc_exception_throw + 60
2 CoreFoundation 0x191706078 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x1917113ac -[__NSPlaceholderDictionary initWithCapacity:].cold.1 + 0
4 CoreFoundation 0x19157c2b8 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 320
5 CoreFoundation 0x19157c158 +[NSDictionary dictionaryWithObjects:forKeys:count:] + 52
6 CrashDemo 0x104a69e0c -[ViewController touchesBegan:withEvent:] + 152
.... 中间内容省略
25 CrashDemo 0x104a6a0c4 main + 120
26 dyld 0x1afed0960 start + 2528

以下是上述每一列元素的含义:

  • 第一列:栈帧号。堆栈帧按调用顺序排列,其中帧 0 是在执行暂停时正在执行的函数。第 1 帧是调用第 0 帧函数的函数,依此类推
  • 第二列:包含正在执行函数的二进制包名
  • 第三列:正在执行的机器指令的地址
  • 第四列:在完全符号化的崩溃报告中,正在执行的函数的名称。出于隐私原因,函数名称有时限制为前 100 个字符
  • 第五列(+ 号后面的数字):函数入口点到函数中当前指令的字节偏移量

通过第 6 行我们可以推断出问题是由 NSDictionary 引起的。


但大部分时候我们得到的报告都是未符号化的,我们需要对报告进行符号化来获得更多的信息。关于符号化的相关内容可以看这里


Thread state

Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x0000000000000000 x1: 0x0000000000000000 x2: 0x0000000000000000 x3: 0x0000000000000000
...中间内容省略
far: 0x00000001e4d30560 esr: 0x56000080 Address size fault

崩溃报告的线程状态部分列出了应用程序终止时崩溃线程的 CPU 寄存器及其值。


Binary images

0x1cf074000 -        0x1cf0abfeb libsystem_kernel.dylib arm64e  <c76e6bed463530c68f19fb829bbe1ae1> /usr/lib/system/libsystem_kernel.dylib
...中间内容省略
0x18b8ca000 - 0x18c213fff Foundation arm64e <e5f615c7cc5e3656860041c767812a35> /System/Library/Frameworks/Foundation.framework/Foundation

以下是上述每一列元素的含义:

  • 第一列:二进制镜像在进程中的地址范围
  • 第二列:二进制镜像的名称
  • 第三列:操作系统加载到进程中的二进制映像中的 CPU 架构
  • 第四列:唯一标识二进制映像的构建 UUID。符号化崩溃报告时使用此值定位相应的 dSYM 文件
  • 第五列:二进制文件在磁盘上的路径

至此,报告上的所有 section 都已经解读完。希望大家看完这篇文章后,再分析崩溃日志的时候能更加得心应手。


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

开发工具 2.0 的时代已经来临

AI 正在变革软件工程:开发工具 2.0 时代 生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。 从 Copilot 说起 Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: q...
继续阅读 »

AI 正在变革软件工程:开发工具 2.0 时代


生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。


从 Copilot 说起


Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: quantifying GitHub Copilot’s impact on developer productivity and happiness》这份报告研究了 Copilot 对开发者效率和幸福感的提升,如下

  • 使用 GitHub Copilot 的开发人员比不使用 GitHub Copilot 的开发人员完成任务的速度快 55%
  • 使用 GitHub Copilot 的小组完成任务的比例为 78%,而没有使用 Copilot 的小组为 70%
  • 88% 的使用者认为自己生产力提高了
  • 96% 的使用者认为自己处理重复性的工作更快了
  • 88% 的使用者认为自己可以更加专注于更喜欢的工作上了


原文地址:github.blog/2022-09-07-…





从数据上来看,Copilot 已经是非常成功了,我们会认为这已经是一个大的变革,但是当我们把眼光放到整个软件工程行业的时候,才发现 Copilot 可能只是 AI 改变软件工程师工作方式的开端。



我曾经写了一篇 Copilot 的体验文章,有兴趣可以看看 # 与 AI 结对编程,好搭档 Copilot



开发工具 2.0 与现状


红衫资本在《Developer Tools 2.0》中定义了”开发工具 2.0“ :通过 AI 改变软件创造方式的工具。


还整理了一张图用以展示现有的开发工具在不同的软件研发阶段的应用。




这图本质上是一个表格,每一行从左到右代表了软件在当前市场的占用水平,分为

  • Incumbents:当前主流使用的标准工具
  • Challengers:挑战者,一些加入了 AI 特性的创新型工具
  • Dev Tools 2.0:通过 AI 改变软件创造方式的工具

列的话从上到下代表了软件开发的各个阶段,或者说生命周期,分别为

  • Deployment:部署阶段,包括 CI/CD、云、监控等
  • Implementation:实现阶段,包括 code review 工具、文档工具、代码编写维护工具等
  • Setup:配置阶段,包括 IDE、终端、ISSUE 记录工具等

接下来我们从上往下来分析。


Deployment 所属区域中,软件还是集中在 Incumbents(主流) 和 Challengers(挑战者) 中,这里可以看到很多熟悉的产品,比如 Datadog、Grafana、Aws、Jenkins 等。


但 Deployment 目前还没有 Dev Tools 2.0 的工具




Implementation 中,目前已有很多 Dev Tools 2.0 了,比如 AI code review 工具 Codeball、DEBUG 和对话工具 ChatGPT、AI 文档工具 Mintlify、以及 AI 代码补全工具 Copilot 和 Tabnine。


注意看细分的 write docs(文档编写) 和 write & maintain code (代码编写维护)中,在主流中这些都是人力维护,这说明当前的软件工程已经处于一个分水岭了:从人工到 AI。


对比 Deployment 的话,Implementation 的 2.0 工具可谓是百花齐放。




最后就是 Setup 了,目前只有 Cursor (一款集成了 ChatGPT 4 的代码编辑器)被完全定义为 Dev Tools 2.0




这里比较意外的是 warp 和 fig 居然没有被定义为 2.0 工具,因为我前段时间刚试用了 warp 终端,有兴趣的可以看看我发的视频


其实回顾一下红衫资本对 Dev Tools 2.0 的定义就能理解了:通过 AI 改变软件创造方式的工具。


warp 和 fig 只是带了 AI 的特性,还没有改变软件的创造规则,所以就被列入了 challenger 里。


从目前世面上的工具来看,AI 已经有了巨大的机会改变软件工程,并且这是一个关于“谁”,而不是“是与否”的问题。


开发工具 2.0 的共同点


再再再次啰嗦一下红衫资本对 Dev Tools 2.0 的定义:通过 AI 改变软件创造方式的工具。


我考察了 5 个图中被归类为 2.0 的软件,看看它们是如何改变软件的创作方式的



首先是 Cursor,我们可以用自然语言来写新的代码、维护既有代码,从这点来看它是超越了 Copilot (这不是指下一代 Copilot X )。




然后是 Codeball,它主要是用 AI 来自动执行 code review,它可以为每一个 PR 进行评分(检查代码规范、Bug 等)并自动合并,大量节省功能特性因 PR 被 Block 的时间,而且用机器代替人做检查也能避免 Review 成为形式主义的流程。




ChatGPT 此处就不做演示了,直接看一下 Grit 吧。虽然下面展示的动图只是将代码片段的优化,但 Grit 给自己的定位是通过 AI 自动化完成整个项目的代码迁移和升级,比如从 JavaScript 到 TypeScript、自动处理技术债等




最后就是 Adrenaline 了,它是一个 AI Debuger(调试器?),我输入了一段会导致 NullPointerException 的代码,但是因为服务器请求的数量太多无法运行。所以我直接在对话框里问了一句:Is there anything wrong with this code?(这段代码有问题吗?)。Adrenaline 不仅回答了会出问题,还详细分析了这段代码的功能




再来对比一下这几个场景下传统的处理方式



基于以上工具的特点,我们也可以畅想一下 Deployment 2.0 工具的特点

  1. 首先肯定是通过自然语言进行交互,比如:帮我在阿里云上部署一下 xxx 项目;也可以说帮我创建一个项目,这项目叫熔岩巨兽,需要使用到 mysql、redis,需要一个公网域名等…
  2. 然后是能够自动分析并配置项目的依赖,比如:部署 xxx 项目需要 mysql 数据库、redis 缓存
  3. 如果能够为我使用最优(成本、性能等多方面)的解决方案更好

其实随着云平台的成熟、容器化的普及,我相信这样的 Deployment 2.0 工具肯定不会太遥远。


事实上在写这篇文章的时候我就发现了 Github 上的一个项目叫 Aquarium,它已经初步基于 AI 的能力实现了部署,它给 AI 输入了以下的前提提示:



你现在控制着一个Ubuntu Linux服务器。你的目标是运行一个Minecraft服务器。不要回答任何批判、问题或解释。你会发出命令,我会回应当前的终端输出。 回答一个要给服务器的Linux命令。



然后向 AI 输入要执行的部署,比如:”Your goal is to run a minecraft server“。


接着 AI 就会不断的输出命令,Aquarium 负责在程序执行命令并将执行结果返回给 AI,,不断重复这个过程直到部署结束。


对开发者的影响


作为一名软件开发者,我们经常会自嘲为 CV 工程师,CV 代表了 ctrl + cctral + v ,即复制粘贴工程师。


这是因为大多数的代码都是通过搜索引擎查询获得,开发者可以直接复制、粘贴、运行,如果运行失败就把错误信息放进搜索引擎再次搜索,接着又复制、粘贴、运行……


但基于开发工具 2.0,这个流程就产生了变化:搜索、寻找答案、检查答案的过程变成了询问、检查答案,直接省去了最费时间的寻找答案的过程。




还有就是开发模式的改变,以前是理解上游的需求并手写代码,而现在是理解上游的需求并用自然语言描述需求,由 AI 写代码。


也就是说在代码上的关注会降低,需要将更多的注意力集中在需求上




也许你发现了,其实可以直接从产品到 AI,因为程序员极有可能是在重复的描述产品需求。


这个问题其实可以更大胆一点假设:如果 AI 可以根据输入直接获得期望的输出,那么老板可以直接对接 AI 了,80% 的业务人员都不需要。


既然已经谈到了对”人“的影响,那不如就接着说两点吧

  • 这些工具会让高级开发者的技能经验价值打折扣,高级和初级的编码能力会趋于拟合,因为每个人都拥有一个收集了全人类知识集的 AI 助手
  • 会编程的人多了,但是适合以编程为工作的人少了

很多开发者对此产生焦虑,其实也不必,因为这是时代的趋势,淹没的也不止你一个,浪潮之下顺势而为指不定也是一个机遇。


如果光看软件工具 2.0,它给软件工程带来的是一次转型,是一次人效的变革,目前来看还没有达到对软件工程的颠覆,那什么时候会被颠覆呢?



有一天有一个这样的游戏出现了,每个人在里面都是独一无二的,系统会为每个人的每个行为动态生成接下来的剧情走向,也就是说这个游戏的代码是在动态生成,并且是为每一个人动态生成。这个游戏的内存、存储空间等硬件条件也是动态在增加。 这就是地球 Online



短期来看,AI 还不会代替程序员,但会替代不会用 AI 的程序员。


AI 正在吞噬软件


最后就用两位大佬的话来结束本文吧。


原 Netscape(网景公司)创始人 Marc Andreessen 说过一句经典的话:软件正在吞噬世界。


人工智能领域知名科学家 Andrej Karpathy 在 2017 年为上面的话做了补充:软件(1.0)正在吞噬世界,现在人工智能(软件2.0)正在吞噬软件



Software (1.0) is eating the world, and now AI (Software 2.0) is eating software.



所以,你准备好了吗?


参考

  1. http://www.sequoiacap.com/article/ai-…
  2. karpathy.medium.com/software-2-…
  3. github.blog/2022-09-07-…
  4. github.com/fafrd/aquar…

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

CSS命名太头疼?这个Vite插件自动生成,让你解放双手!

web
CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生...
继续阅读 »

CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生成原子CSS类呢?今天我要介绍的Vite插件atom-css-generator就可完美实现这一功能。


原子CSS简介


原子CSS(Atomic CSS)将传统的CSS类拆分成一个个独立的、原子级的类,每个类仅包含一个CSS属性,例如:



.p-10 {
padding: 10px;
}

.bg-red {
background: red;
}

相比于传统的CSS类,原子类具有以下特点:



  • 原子:每个类只包含一个CSS属性,拆分到最小粒度

  • 独立:类名语义明确,可以任意组合使用而不会产生冲突

  • 可复用:一个原子类可以重复使用在不同的组件中


使用原子CSS的优势在于:



  • 更模块化:样式属性高内聚、解耦

  • 更可维护:不同类名称、不同文件,避免影响

  • 更灵活:组件样式由原子类组合,更容易扩展和维护


但是编写大量原子类也比较麻烦,多达几千个类定义都可能出现。有没有自动生成的方式呢?


atom-css-generator插件介绍


atom-css-generator是一个Vite插件,它可以通过解析Vue组件中的class,自动生成对应的原子CSS定义


安装和配置


使用npm或yarn安装:


Copy code

npm install atom-css-generator

在vite.config.js中引入插件:


js

Copy code

import atomCssGenerator from 'atom-css-generator';

export default {
plugins: [
atomCssGenerator({
outputPath: 'assets/styles'
})
]
}

主要的配置项有:



  • outputPath:指定生成的CSS文件输出目录,默认为public


使用方式



  1. 在Vue组件的template中,使用特定格式的class,例如:


html

Copy code

<template>
<div class="bg-red fc-white p-20">
<!-- ... -->
</div>
</template>


  1. 构建项目时,插件会自动生成对应的原子CSS类定义:


css

Copy code

.bg-red {
background-color: red;
}

.fc-white {
color: white;
}

.p-20 {
padding: 20px;
}


  1. style.css会被自动生成到指定的outputPath中,并注入到HTML文件头部。


支持的类名格式


插件支持多种格式的类名规则生成,包括:



  • 颜色类名:bg-red、fc-333

  • 间距类名:p-20、ml-10

  • 尺寸类名:w-100、h-200

  • Flexbox类名:jc-center、ai-stretch

  • 边框类名:bc-333、br-1-f00-solid

  • 布局类名:p-relative、p-fixed

  • 文字类名:fs-14、fw-bold


等等,非常全面。


而且也内置了一些预设的实用样式类,比如文字截断类te-ellipsis。


原理简析


插件主要通过以下处理流程实现自动生成原子CSS:



  1. 使用@vue/compiler-sfc解析Vue文件,获取模板内容

  2. 通过正则表达式提取模板中的class名称

  3. 根据特定类名规则,生成对应的CSS定义

  4. 将CSS写入style.css文件中,并注入到HTML中


同时,插件还会在热更新时自动检查新添加的类名,从而动态更新style.css。


总结


通过atom-css-generator这个插件,我们可以非常轻松地在Vue项目中使用原子CSS样式,而不需要手动编写。它省去了我们大量重复的工作,使得样式的维护和扩展更加简单。


如果你也想尝试在自己的项目中引入原子CSS,不妨试试这个插件。相信它能给你带来意想不到的便利!
GitHub地址

收起阅读 »

扒一扒uniapp是如何做ios app应用安装的

iOS
为何要扒 因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来...
继续阅读 »

为何要扒


因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。




开干


官方模板




先打开uniapp云打包一下项目看看


image-20230824112232275.png




复制地址到移动端浏览器打开看看


image-20230824112410817.png


这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。




开扒




F12打开choromdevtools,ctrl+s保存网页html。


image.png


保存成功,接下来看看html代码(样式代码删除了)


    <!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
   <head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 </head>

<body>
<br><br>
   <center>
       <a class="button" href="itms-services://?action=download-manifest&amp;url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
   </center>
   <br><br>
   <center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>



解析




从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")


先看看itms-services是什么意思,下面是代码开发助手给的解释


image-20230824113418246.png


大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。




什么又是plist呢,这里再请我们的代码开发助手解释一下


image-20230824113748570.png


对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。




打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。


image-20230824114108792.png


访问后会出现


image-20230824115354028.png




别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求


image-20230824115609551.png


直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
      <dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>

直接抓重点,这里存你存放ipa包的地址


image-20230824120013828.png


这里改你应用的昵称


image-20230824120453368.png


这里改图标


image-20230824120509797.png


因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。




为我所用


分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:


image-20230824155040313.png


将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:


image-20230824155306228.png


可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至

作者:廿一c
来源:juejin.cn/post/7270799565963149324
此,本次扒拉过程结束,需求落幕!

收起阅读 »

iOS - 上手AR

iOS
前言 随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo 开始 要在iOS中创建一个的AR物体,你可以使用 ARKit 和 SceneKit 来实现 首先,确保你的项目已经导入了 ARKit 和 SceneKit...
继续阅读 »

前言


随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo


开始


要在iOS中创建一个的AR物体,你可以使用 ARKitSceneKit 来实现


首先,确保你的项目已经导入了 ARKit 和 SceneKit 框架。你可以在 Xcode 中的项目设置中添加 ARKit.framework 和 SceneKit.framework 到 "Frameworks, Libraries, and Embedded Content" 部分




然后,在你的程序文件中,导入 ARKit 和 SceneKit

import UIKit
import ARKit

接下来,创建一个 ARSCNView,并将其添加到你的视图层次结构中:

// 创建 ARSCNView 实例
sceneView = ARSCNView(frame: view.bounds)
view.addSubview(sceneView)
sceneView.delegate = self

// 创建 SCNScene 实例,并设置为 sceneView 的场景
let scene = SCNScene()
sceneView.scene = scene

然后,在视图控制器的生命周期方法中,配置 ARSession 并启动 AR 会话:

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// 配置 AR 会话并启动
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

// 暂停 AR 会话
sceneView.session.pause()
}

现在,已经设置好 ARKit 和 AR 会话已经开始运行。接下来,我们将创建3D 模型:

func createBallNode() {
// 创建球体几何体
let ballGeometry = SCNSphere(radius: 0.1)
let ballMaterial = SCNMaterial()
ballMaterial.diffuse.contents = UIImage(named: "cxkj.webp") // 使用纹理图片
ballGeometry.materials = [ballMaterial]

// 在屏幕范围内生成随机位置坐标
let randomX = Float.random(in: -1.0...1.0) // 在屏幕宽度范围内生成随机 X 坐标
let randomY = Float.random(in: -1.0...1.0) // 在屏幕高度范围内生成随机 Y 坐标
let randomZ = Float.random(in: -3.0...0.0) // 在屏幕深度范围内生成随机 Z 坐标
ballNode = SCNNode(geometry: ballGeometry)
ballNode.position = SCNVector3(randomX, randomY, randomZ)

// 将球体节点添加到场景的根节点上
sceneView.scene.rootNode.addChildNode(ballNode)
}

最后我们通过点击事件将3D模型添加到场中

@objc func handleTap(_ gesture: UITapGestureRecognizer) {
if gesture.state == .ended {
createBallNode() // 创建球体节点
}
}

效果




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

iOS 快速复习GCD

iOS
多线程-串行、并行队列,同步、异步任务 1、创建串行队列和并行队列 //并行队列 dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_C...
继续阅读 »

多线程-串行、并行队列,同步、异步任务


1、创建串行队列和并行队列

    //并行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
//串行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);

  • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务),并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

2、同步异步任务

//同步
dispatch_sync(queue, ^{
        NSLog(@"1");
    });
//异步
dispatch_async(queue, ^{
        NSLog(@"1");
    });

同步执行:

  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
  • 只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行:

  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 可以在新的线程中执行任务,具备开启新线程的能力。

异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

默认全局并发队列:dispatch_get_global_queue

第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT

第二个参数暂时没用,用 0 即可。


信号量 dispatch_semaphore_t



GCD中的信号量dispatch_semaphore_t中主要有三个函数:

  • dispatch_semaphore_create:创建信号
  • dispatch_semaphore_wait:等待信号
  • dispatch_semaphore_signal:释放信号

1、dispatch_semaphore_create
参数为int,表示信号量初始值,需大于等于0,否则创建失败,返回一个dispatch_semaphore_t


2、dispatch_semaphore_wait
参数1:

需传递一个 dispatch_semaphore_t 类型对象,对信号进行减1,然后判断信号量大小

参数2:

传递一个超时时间:dispatch_time_t 对象

  • 减1后信号量小于0,则阻塞当前线程,直到超时时间到达或者信号量大于等于0后继续执行后面代码
  • 减1后信号量大于等于0,对dispatch_semaphore_t 进行赋值,并返回dispatch_semaphore_t对象,继续执行后面代码

3、dispatch_semaphore_signal
参数:dispatch_semaphore_t

进行信号量加1操作,如果加1后结果大于等于0,则继续执行,否则继续等待。


用法:

- (void)startAsync{
//创建信号量 值为0
    self.sem = dispatch_semaphore_create(0);
//开启异步并发线程执行
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"dispatch_semaphore 2\n");
        sleep(5);
//发送信号,信号量值+1
        dispatch_semaphore_signal(self.sem);
        NSLog(@"dispatch_semaphore 3\n");
    });
    NSLog(@"dispatch_semaphore 0\n");
//信号量 值-1 小于0 等待信号。。。
    dispatch_semaphore_wait(self.sem, DISPATCH_TIME_FOREVER);
    NSLog(@"dispatch_semaphore 1\n");

}
执行顺序0 2 1 3 1和3不确定顺序
如果初始化创建是信号量值为1
执行顺序0 1 2 3

常用总结:

1、异步并发线程顺序执行

2、异步并发线程控制最大并发数,比如下载功能控制最大下载数


调度组 dispatch_group_t


主要API:

  • dispatch_group_create:创建组

  • dispatch_group_async:进组任务

  • dispatch_group_notify:组任务执行完毕的通知

  • dispatch_group_enter:进组

  • dispatch_group_leave:出组

  • dispatch_group_wait:等待组任务时间


组合用法1:

- (void)dispatchGroupAsync{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//开启异步线程
    dispatch_group_async(group, queue, ^{
        sleep(2);
        NSLog(@"11");
    });
    dispatch_group_async(group, queue, ^{
        sleep(1);
        NSLog(@"12");
    });
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"13");
    });
    NSLog(@"14");
    dispatch_group_notify(group, queue, ^{
//收到执行完成的通知后执行
        NSLog(@"15");
    });
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
调度组执行完成后执行
    NSLog(@"16");
}

用法2:

- (void)dispatchSyncEnterGroup{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//进入调度组
    dispatch_group_enter(group);
//执行异步任务
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"21");
//执行完成后立刻调度组
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"22");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(3);
        NSLog(@"23");
        dispatch_group_leave(group);
    });
    NSLog(@"24");
    dispatch_group_notify(group, queue, ^{
//执行完后回调
        NSLog(@"25");
    });
    NSLog(@"26");
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"27");
}

总结:

1、dispatch_group_async 是对dispatch_group_enter和dispatch_group_leave的封装

2、dispatch_group_enter和dispatch_group_leave的须成双成对的出现


事件源 dispatch_source_t


主要API:

  • dispatch_source_create :创建源

  • dispatch_source_set_event_handler: 设置源的回调

  • dispatch_source_merge_data: 源事件设置数据

  • dispatch_source_get_data: 获取源事件的数据

  • dispatch_resume:恢复继续

  • dispatch_suspend:挂起

  • uintptr_t dispatch_source_get_handle(dispatch_source_t source) //得到dispatch源创建,即调用dispatch_source_create的第二个参数

  • unsignedlong dispatch_source_get_mask(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第三个参数


源的类型dispatch_source_type_t

1. DISPATCH_SOURCE_TYPE_DATA_ADD:用于ADD合并数据
2. DISPATCH_SOURCE_TYPE_DATA_OR:用于按位或合并数据
3.DISPATCH_SOURCE_TYPE_DATA_REPLACE:跟踪通过调用dispatch_source_merge_data获得的数据的分派源,新获得的数据值将替换 尚未交付给源处理程序 的现有数据值
4. DISPATCH_SOURCE_TYPE_MACH_SEND:用于监视Mach端口的无效名称通知的调度源,只能发送没有接收权限
5. DISPATCH_SOURCE_TYPE_MACH_RECV:用于监视Mach端口的挂起消息
6. DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:用于监控系统内存压力变化
7.DISPATCH_SOURCE_TYPE_PROC:用于监视外部进程的事件
8. DISPATCH_SOURCE_TYPE_READ:监视文件描述符以获取可读取的挂起字节的分派源
9. DISPATCH_SOURCE_TYPE_SIGNAL:监控当前进程以获取信号的调度源
10. DISPATCH_SOURCE_TYPE_TIMER:基于计时器提交事件处理程序块的分派源
11. DISPATCH_SOURCE_TYPE_VNODE:用于监视文件描述符中定义的事件的分派源
12. DISPATCH_SOURCE_TYPE_WRITE:监视文件描述符以获取可写入字节的可用缓冲区空间的分派源。

1、dispatch_source_create 参数:

  • dispatch_source_type_t 要创建的源类型
  • uintptr_t 句柄 用于和其他事件并定,很少用,通常为0
  • uintptr_t mask 很少用,通常为0
  • dispatch_queue_t 事件处理的调度队列

用法:

self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

2、dispatch_source_set_event_handler 设置回调函数,当触发源事件时执行

//需要注意循环引用
dispatch_source_set_event_handler(self.sourceAdd, ^{
需要执行的代码
});
//启动
dispatch_resume(self.sourceAdd);
//挂起,即暂停
dispatch_suspend(self.sourceAdd);
这两个API需要成对使用,不可多次挂起或者多次恢复

3、dispatch_source_cancel 取消事件源,取消后不可再恢复或挂起,需要再次创建
4、dispatch_source_set_timer 当事件源类型为定时器类型(DISPATCH_SOURCE_TYPE_TIMER)时,设置开始时间、重复时间、允许时间误差


定时器实现比较简单容易,网上教程也多,这里主要介绍一下:DISPATCH_SOURCE_TYPE_DATA_ADD、DISPATCH_SOURCE_TYPE_DATA_OR、DISPATCH_SOURCE_TYPE_DATA_REPLACE。


先说下结果:

  • DISPATCH_SOURCE_TYPE_DATA_ADD 会把事件源累加 可以记录总共发送多少次事件进行合并
  • DISPATCH_SOURCE_TYPE_DATA_OR 会把事件源合并,最终得到的数据源数为1
  • DISPATCH_SOURCE_TYPE_DATA_REPLACE 会用最新事件源替换旧有未处理事件,最终得到的数据源数为1
  • 循环10000次实际跑处理回调事件次数 add315 or275 replace 284

从结果上来看,当需要把快速频繁的重复事件进行合并,最好的选择是DISPATCH_SOURCE_TYPE_DATA_OR,使用场景,监听消息时,多消息频繁下发需要刷新UI,如果不进行合并处理,会导致UI太过频繁的刷新,影响最终效果,且对性能开销过大。


当然,类似的场景也可使用其他方式处理,比如建立消息池,接收消息后标记消息池状态及变化,然后定时从消息池中取消息。诸如此类的方法较多,如果只是简单的处理,上面的DISPATCH_SOURCE_TYPE_DATA_OR模式应该满足使用。


代码:

//创建源
self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
//弱引用
__weak typeof(self) weakifySelf = self;
//设置回调事件
dispatch_source_set_event_handler(self.sourceAdd, ^{
//强引用
__strong typeof(self) strongSelf = weakifySelf;
//获取接收到的源数据
strongSelf.handleData = dispatch_source_get_data(strongSelf.sourceAdd);
NSLog(@"dispatch_source1 %ld\n",strongSelf.handleData);
//需要执行的代码
[strongSelf sourceHandle];

        });
//开启源
dispatch_resume(self.sourceAdd);
for (int i = 0; i<10000; i ++) {

[self dispatchSource];
}
- (void)dispatchSource{

    NSLog(@"dispatch_source2 %ld\n",self.handleData);
//发送源信号
    dispatch_source_merge_data(self.sourceAdd, 1);
}

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

自建”IT兵器库”,你值得一看!

web
现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!! 常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小...
继续阅读 »

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!


常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。


接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步


需求:


动态列表格,就是一个表格,存在默认列,但是支持我们操控,实现动态效果


实现效果


默认表格配置


image.png


默认列配置


image.png


动态列组件支持查询


image.png


动态列组件支持勾选


image.png


动态列组件支持清空


image.png


动态列组件支持一键全选


image.png


动态列组件支持一键清空


image.png


功能点划分



  • 表格默认列和动态列组件默认选中项 实现双向绑定

  • 动态列组件 增删改与表格 实现双向绑定

  • 动态列组件 实现搜索

  • 动态列组件 实现单点控制 添加与删除

  • 动态列组件 实现一键控制功能 全选与清空

  • 动态列组件 实现恢复初始态


使用到组件(Antd 组件库哈)



  • Table

  • Pagination

  • Modal

  • Input

  • Button

  • Checkbox


动态列组件区域划分



  • 头部标题

  • 头部提示语

  • 核心内容区



    • 核心区域头部功能按钮





    • 搜索区域





    • 左边所有内容项





    • 待选内容项




动态列组件最终可支持配置项


  open?: boolean // Modal状态
setOpen?: React.Dispatch> // 控制Modal状态
modalTitle?: string | React.ReactNode
modalWidth?: number
modalHeadContent?: React.ReactNode
leftHeadContent?: React.ReactNode | string
rightHeadContent?: React.ReactNode | string
modalBodyStyle?: any
searchPlaceholder?: string
modalOk?: (val, isUseDefaultData?: boolean) => void // 第二个参数 内部数据处理支持
enableSelectAll?: boolean // 是否开启全选功能
selectData: SelectItem[] // 下拉框数据
isOutEmitData?: boolean
defaultSelectKeyList?: string[] // 默认选中的key(当前表格列)自定义数据(外部做逻辑处理)
initSelectKey?: string[] // 初始表格选中的key 自定义数据(外部做逻辑处理)
curColumns?: any[] // 当前表格列 内部做逻辑处理
originColumns?: any[] // 原始表格列 内部做逻辑处理
isDelHeadCol?: boolean // 删除头部columnKey 例如序号 只有内部处理数据时生效
isDelTailCol?: boolean // 删除尾部columnKey 例如操作 只有内部处理数据时生效
isDelHeadAndTail?: boolean // 删除头尾部columnKey 只有内部处理数据时生效

动态列组件布局



    

// 头部内容区
{modalHeadContent}

// 以下维核心区


// 核心区-左边

// 核心区-功能按钮 - 一键全选
{enableSelectAll && (

onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
全选


)}
{leftHeadContent || ''}



// 核心区-左搜索
{childSearchRender({ curData: leftSelectData })}

// 核心区-左列表区域
{selectItemRender(leftSelectData)}



// 核心区-右边


{rightHeadContent || ''}

// 核心区-功能按钮 - 一键清空
handleRightClearSelectData()}>
清空



// 核心区-右搜索
{childSearchRender({ curData: rightSelectData }, true)}

// 核心区-右列表区域
{selectItemRender(rightSelectData, true)}






动态列组件-列表渲染


const selectItemRender = (listArr = [], isRight = false) => {
return (


// 数据遍历形式
{listArr?.map(({ label, value, disabled = false }) => (

{!isRight && (

{label}

)}
// 判断是否是 右边列表区域 添加删除按钮
{isRight && {label}}
{isRight && (



)}

))}


)
}

动态列组件-搜索渲染


const childSearchRender = (childSearchProps: any, isRight = false) => {
// eslint-disable-next-line react/prop-types
const { curData } = childSearchProps
return (
{
onSearch(e, curData, isRight)
}}
allowClear
/>
)
}

动态列组件样式


.content-box {
width: 100%;
height: 550px;
border: 1px solid #d9d9d9;
}
.content-left-box {
border-right: 1px solid #d9d9d9;
}
.content-left-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;
}
.content-right-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;

&-clear {
color: #f38d29;
cursor: pointer;
}
}
.content-right-box {
}
.content-left-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.content-right-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.right-head-content {
font-weight: 700;
color: #151e29;
font-size: 14px;
}
.modal-head-box {
color: #151e29;
font-size: 14px;
height: 30px;
}
.icon-box {
color: #f4513b;
}
.ant-checkbox-group {
flex-wrap: nowrap;
}
.left-select-box {
height: 440px;
padding-bottom: 10px;
}
.right-select-box {
height: 440px;
padding-bottom: 10px;
}
.ant-checkbox-wrapper {
align-items: center;
}
.display-box {
height: 22px;
}

功能点逐一拆解实现


点1:表格默认列和动态列组件默认选中项 实现双向绑定



  • 首先,先写一个表格啦,确定列,这个就不用代码展示了吧,CV大法

  • 其次,把表格原始列注入动态列组件当中,再者注入当前表格列当前能选择的所有项

  • 当前能选择所有项内容参数示例


[
{ label: '项目编码', value: 'projectCode' },
{ label: '项目名称', value: 'projectName' },
{ label: '项目公司', value: 'company' },
{ label: '标段', value: 'lot' },
]




  • 动态组件内部默认选中当前表格列

  • 这里需要把表格列数据 进行过滤 映射成 string[]


   内部是通过checkbox.Grop 实现选中 我们只需要 通过一个状态去控制即可 `selectKey`
<Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
...
Checkbox.Group>

动态列组件 单点控制 增删改



  • 增,删,改就是实现 左右边列表的双向绑定

  • 监听 左边勾选事件 + 右边删除事件 + 一键清空事件

  • 通过左右两边的状态 控制数据即可

  • 状态


  const [originSelectData, setOriginSelectData] = useState([]) // 下拉原始数据 搜索需要
const [originRightSelectData, setOriginRightSelectData] = useState([])
const [rightSelectData, setRightSelectData] = useState([])
const [selectKey, setSelectKey] = useState([])
const [transferObj, setTransferObj] = useState({})
const [indeterminate, setIndeterminate] = useState(false)
const [checkAll, setCheckAll] = useState(false)
const [leftSelectData, setLeftSelectData] = useState([])
const [defaultSelectKey, setDefaultSelectKey] = useState([])
const [originSelectKey, setOriginSelectKey] = useState([])

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val])
setSelectKey(checkedValues) // 我们选中的key (选项)
setRightSelectData(selectResArr) // 右边列表数据
setOriginRightSelectData(selectResArr) // 右边原数据(搜索时候需要)
}

const deleteRightData = key => {
const preRightData = rightSelectData
const filterResKeyArr = preRightData?.filter(it => it.value !== key).map(it => it.value) // 数据处理 只要你不等于删除的key 保留
const filterResItemArr = preRightData?.filter(it => it.value !== key)
setRightSelectData(filterResItemArr) // 更新右边数据 即可刷新右边列表
setOriginRightSelectData(filterResItemArr) // 更新右边数据
setSelectKey(filterResKeyArr) // 更新选中的key 即可刷新左边选中项
}

  const handleRightClearSelectData = () => {
// 这就暴力了塞
setSelectKey([])
setRightSelectData([])
setOriginRightSelectData([])
}

动态列组件 实现搜索



  • 搜索,就是改变一下数据 视觉上看起来有过滤的效果塞

  • 刚才我们不是多存了一份数据源嘛

  • 出来见见啦~


const onSearch = (val, curData, isRight = false) => {
const searchKey = val
// 这个是同时支持 左右两边
// 做个判断
if (!isRight) {
// 在判断一下是否有搜索内容 因为也需要清空的啦
if (searchKey) {
// 有,我就过滤呗
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setLeftSelectData(searchResArr)
}
if (!searchKey) {
// 没有 我就把原本数据还给你呗
setLeftSelectData(originSelectData)
}
}
// 右边 一样
if (isRight) {
if (searchKey) {
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setRightSelectData(searchResArr)
}
if (!searchKey) {
setRightSelectData(originRightSelectData)
}
}
}

动态列组件 增删改与表格 实现数据绑定



  • 里面的数据 处理好了 直接再关闭的时候 丢给外面嘛

  • 把右边的内容(也就是选中的key)返回给表格

  • 表格再自己构造


  const handleOk = (colVal, isUseDefaultCol) => {
`colVal` : 选中的列key
`isUseDefaultCol`:是否使用默认列
// table column字段组装
const normalColConstructor = (
title,
dataIndex,
isSort = true,
colWidth = 150,
isEllipsis = false,
render = null
) => {
const renderObj = render ? { render } : {}
return {
title,
dataIndex,
sorter: isSort,
width: colWidth,
ellipsis: isEllipsis,
key: dataIndex,
...renderObj,
}
}
const statusRender = text => approvalStatusRender(text)
const dateRender = (text, record) => {dayjs(text).format('YYYY-MM-DD')}
const newColArr = []
// 定制化处理 (其实还有2.0)
colVal?.forEach(({ label, value }, index) => {
let isSort = false
let renderFn = null
const isSubmissionAmount = value === 'submissionAmount'
const isApprovalAmount = value === 'approvalAmount'
const isReductionRate = value === 'reductionRate'
const isInitiationTime = value === 'initiationTime'

// 特定的业务场景 特殊状态渲染
const isStatus = value === 'status'
// 特定的业务场景 时间类型 加上排序
if (isApprovalAmount || isInitiationTime || isReductionRate || isSubmissionAmount) {
isSort = true
}
if (isStatus) {
renderFn = statusRender
}
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, isSort, 100, true, renderFn))
})

// 最后在头部追加一个序号
newColArr.unshift({
title: '序号',
dataIndex: 'orderCode',
width: 45,
render: (_: any, record: any, index) => tablePageSize * (tablePage - 1) + index + 1,
})
// 最后在尾部部追加一个操作
newColArr.push({
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 50,
render: (text, row: DataType) => (



),
})

if (colVal?.length) {
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

// 解决表格存在子列 -- 先搞定数据结构 -- 在解决表格列内容填充 -- 无需捆绑
// 遍历拿到新增列数组 匹配上代表 需要子数组 进行转换
// eslint-disable-next-line consistent-return
colVal?.forEach(({ label, value }, index) => {
// DesignHomeDynamicCol[value] 返回的值能匹配到 说明存在 嵌套列
const validVal = DesignHomeDynamicClassify[DesignHomeDynamicCol[value]]
const isHasChild = newColChildObj[validVal]
const titleText = DesignHomeDynamicLabel[value]
if (validVal) {
// 如果已经有孩子 追加子列
if (isHasChild) {
newColChildObj[validVal] = [...isHasChild, normalColConstructor(titleText, value)]
} else {
// 则 新增
newColChildObj[validVal] = [normalColConstructor(titleText, value)]
}
} else {
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, false, 100, true))
}
})

动态列组件 实现恢复初始态 实现双向绑定



  • 这个就更简单啦 再点击确定的时候 传一个 isUseDefaultData:true

  • 只是这个isUseDefaultData 的逻辑判断问题

  • 当动态列组件 点击恢复默认列 我们只需把 当初传进来的 原始列数据 更新到 selectKey 状态即可


const handleDefaultCol = () => {
// 这里是考虑到组件灵活性 数据可由自己处理好在传入
if (isOutEmitData) {
setSelectKey(initSelectKey)
} else {
// 这里是使用 内部数据处理逻辑
setSelectKey(originSelectKey)
}
}

const handleOk = () => { 
// 数据比对 是否使用默认校验
// originColumnMapSelectKey 源数据与传出去的数据 进行比对
const originRightMapKey = originRightSelectData?.map(it => it.value)
// 采用 lodash isEqual 方法
const isSame = isEqual(originSelectKey, originRightMapKey)
// 判断外部是否有传 确定事件 handleOk
if (modalOk) {
modalOk(originRightSelectData, isSame)
}
setOpen(false)
}

const handleOk = (colVal, isUseDefaultCol) => {
... 一堆代码
// 当用户清空以后 还是恢复表格默认状态
if (colVal?.length) {
// 恢复默认列
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
// 否则就拿新数据更新
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

动态列组件 实现一键控制功能 全选与清空



  • 这就是Vip版本的噻

  • 但是也简单 无非就是操作多选框 无非多选框就三种态

  • 未选 半选 全选

  • 既然我们下面的逻辑已处理好 这个其实也很快的锅

  • 首先,就是下面数据变化的时候 我们上面需要去感应

  • 其次就是 上面操作的时候 下面也需要感应

  • 最后 双向数据绑定 就能搞定 没有那么神秘

  • 一步一步来 先分别把 上下事件处理好


const onCheckBoxChange = (dataArr = [], e = null) => {
// 判断所有数据长度
const allLen = originSelectData?.length
// 根据当前选中数据长度 判断是 多选框三种状态当中的哪一种
const checkLen = e ? selectKey?.length : dataArr?.length // 全选
const isAllSelect = allLen === checkLen // 半选
const isHalfSelect = allLen > checkLen
// 然后再判断一下是点击一键全选事件 触发还是 点击下面选项的时候触发
// 点击一键全选 能拿到事件的 e.target 从而来判断
// 这里是操作下面按钮的时候 触发
if (!e) {
// 如果没有选中
if (checkLen === 0) {
// 恢复未选状态
setCheckAll(false)
setIndeterminate(false)
return ''
}
if (isAllSelect) {
// 如果是全选 改为全选态
setCheckAll(true)
setIndeterminate(false)
}
if (isHalfSelect) {
// 半选态
setIndeterminate(true) // 这个控制 多选框的半选态
setCheckAll(false)
}
}
// 这个就是用户操作 一键全选按钮触发
if (e) {
// 如果当前长度为0 那么应该更新为全选
if (checkLen === 0) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
// 如果已经全选 就取消全选
if (isAllSelect) {
setCheckAll(false)
setIndeterminate(false)
setSelectKey([])
}
// 如果是半选态 就全选
if (isHalfSelect) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
}
}

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val]) setSelectKey(checkedValues)
setRightSelectData(selectResArr)
setOriginRightSelectData(selectResArr)
}


  • 我们两个事件都处理好 那么开始进行联动

  • 意思就是 我们拿什么去控制这两个机关 核心是不是就是 选中的选项啊

  • 有两种解法,第二种可能有点绕



    • 一、就是在下面每个选项改变的时候 都去调一下 上面一键全选事件;然后上面变化的时候 也去调一下 下面选项的事件 (常规)缺点:容易漏 一变多改





    • 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效




// 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效 
useEffect(() => {
onCheckBoxChange(selectKey)
onCheckChange(selectKey)
// eslint-disable-next-line react-hooks/exhaustive-deps },
[selectKey]
)

结束


都看到这里了,不留点痕迹,是怕我发现么?

作者:造更多的轮子
来源:juejin.cn/post/7266463919139684367

收起阅读 »

论如何在Android中还原设计稿中的阴影

每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。 一般来说阴影通常格式是有: X: 在X轴的偏移度 Y: 在Y轴偏移度 Blur: 阴影的模糊半径 Color: 阴影的颜色 何为阴影 ...
继续阅读 »

每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。


一般来说阴影通常格式是有:


X: 在X轴的偏移度


Y: 在Y轴偏移度


Blur: 阴影的模糊半径


Color: 阴影的颜色


何为阴影


但是在Android中却比较单一,只有一个度量单位:Elevation,作为在Android5.0的material2引入的概念,用一个图来形象的描绘一下,其实本质上就是虚拟的Z轴坐标。


image.png


那好,高度差有了,还差个光源,这样才能形成阴影,在material2中,光源不是单一的位于屏幕正上方的,而且有两组光源,分为主光源(Key light)和环境光源(Ambient light)如下图所示:
image.png


最终形成的效果是一种复合光源下更自然的阴影。


image.png


其中环境光源,在屏幕空间中没有实际的位置,但是主光源是有实际的位置的,具体的参数见:


frameworks/base/core/res/res/values/dimens.xml - Android Code Search
image.png


好,既然知道了阴影本身的机制,那下一步现在则是如何自定义控制阴影,这也是本文的目的。


从SDK 21开始,提供了Elevation可以实现类似于阴影的模糊半径的效果,但是毕竟尺度过于单一,往往有时候无法满足所需的效果,所以,还需要控制阴影的颜色。


在SDK 28之后,可以通过outlineSpotShadowColoroutlineAmbientShadowColor来分别设置Key light和Ambient light投射的阴影颜色,但是说实话,这两个属性基本用不到或者说比较鸡肋。


不过这里引入了一个概念:Outline。


四种常见方案


Elevation + Outline


Outline其实是View的边框(轮廓),通过OutlineProvider可以自定义一个View的Outline从而影响View本身在elevation下的投影,比如定义以实现一个圆角ImageView为例:


<ImageView
android:id="@+id/image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@color/material_dynamic_primary90" />


image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}

}

效果基本没啥问题:
image.png


同样的,既然View的轮廓变化了,阴影自然也会跟着随之变化,所以outline也可以改变阴影:


image.elevation = 32f
image.outlineAmbientShadowColor = Color.RED
image.outlineSpotShadowColor = Color.BLUE
image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}

}

效果如下:(不过outlineAmbientShadowColoroutlineSpotShadowColor仅支持SDK 28及以上)


image.png


通常,到这一步通过调整elevation的数值和outline以及高版本可用的shadowColor大体上可以满足设计师的阴影需求。
而且通常来说shadowColor都是Color.Black以及alpha的区别,所以你也可以这样:


outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.alpha = 0.5f
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}
}

但是,还记着前面提到的两个光源吗?其中有一个光源是位于屏幕斜上方的,这就带来了另外一个问题,同一个View设置相同的Elevation在不同的Y轴坐标它的阴影效果是不一样的,如下图所示:



总之,阴影的Blur和Color参数勉强是可以得到满足的。


优点:原生的阴影效果


缺点:设置阴影的颜色需要SDK>=28,需要配合使用outline来实现对阴影的轮廓控制


下面我们先来引申一下Android中了解过的阴影实现方式。


LayerDrawable


我相信大家肯定见过这种实现方式,通过绘制一层层渐变色来模拟阴影,其实官方也有通过该方式实现的阴影:MaterialShapeDrawable,示例如下:


val drawable = MaterialShapeDrawable(
ShapeAppearanceModel.builder()
.setAllCornerSizes(16.dp)
.build()
)
drawable.fillColor = ColorStateList.valueOf(getColor(com.google.android.material.R.color.material_dynamic_primary90))
drawable.setShadowColor(Color.RED)
drawable.shadowVerticalOffset = 8.dp.toInt()
drawable.elevation = 32f
drawable.shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
image.background = drawable

效果如图:
image.png


只能说很一般,毕竟是模拟的阴影模糊效果,而且目前只支持Y轴的offset。


优点:几乎是开箱即用的Drawable且自带圆角


缺点:模拟的阴影效果,展示效果不够精细且效率不高


NinePatchDrawable


说实话想在Android上实现一个简单的阴影太折腾了,什么奇怪的技巧都来了,比如.9图,至于什么是.9图这里便不再过多介绍。
通过这个网站:Android Shadow Generator (inloop.github.io)


image.png
你可以直接生成一个CSS Style的阴影效果,几乎可以完美还原Figma的阴影效果,效果如下:
image.png


其实还是很还原的,但是它有一个致命的缺点,就是圆角,因为是一张图片,所以圆角的单位本质上是px而非Android上的dp,如果你需要一个带圆角弧度的阴影是达不到预期的。


优点:参数完全可控的阴影,可以做到1:1还原设计稿


缺点:因为是图片,所以阴影的圆角无法跟随像素密度缩放(非常致命的缺点)


Paint.setShadowLayer/BlurMaskFilter


这两个我之所以放在一起本质上是因为实现起来都是类似的,
如:


paint.setShadowLayer(radius, offsetX, offsetY, shadowColor)
// 或者使用maskFilter然后通过paint.color以及绘制的区域进行offset来变相控制阴影的Color及offset
paint.maskFilter = BlurMaskFilter(field, BlurMaskFilter.Blur.NORMAL)

相比之下更推荐使用setShadowLayer,最终效果如下,基本上没啥问题:
image.png


但是值得注意的是,其绘制的阴影本质上等价于BlurMaskFilter,是占位的,而且是需要留出空间来展示的,所以必要时需要对父布局设置android:clipChildren="false"或者预留出足够的空间。


优点:


1. 参数完全可控的阴影,可以做到1:1还原设计稿


2. 参数的自定义程度及可控性强


缺点:


1. 阴影占位,需要通过clipChildren=false来或者预留空间规避


2. 需要自定义View或者Drawable,写起来较为麻烦。


总的来说,上面介绍了4种可能常见的阴影实现方式,其中按我的经验来说,较为推荐采用Outline或者setShadowLayer的方式来实现,如果可以的话原生Elevation配合Outline基本可以满足大部分需求场景。


当然还有部分实现方式比如用RenderScriptBlur等等,我没提是因为是前几种方式较为复杂,性价比不高。


Paint.setShadowLayer 扩展内容


下面则重点讲一下Paint.setShadowLayer/BlurMaskFilter这种方式,为什么说这两种方式实现的阴影都是一致的呢?这个就需要深入到C++层。
首先直接跳到paint.setShadowLayer的native实现类:
frameworks/base/libs/hwui/jni/Paint.cpp


Paint.cpp - Android Code Search


    static void setShadowLayer(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jfloat radius,
jfloat dx, jfloat dy, jlong colorSpaceHandle,
jlong colorLong)
{
SkColor4f color = GraphicsJNI::convertColorLong(colorLong);
sk_sp<SkColorSpace> cs = GraphicsJNI::getNativeColorSpace(colorSpaceHandle);

Paint* paint = reinterpret_cast<Paint*>(paintHandle);
if (radius <= 0) {
paint->setLooper(nullptr);
}
else {
SkScalar sigma = android::uirenderer::Blur::convertRadiusToSigma(radius);
paint->setLooper(BlurDrawLooper::Make(color, cs.get(), sigma, {dx, dy}));
}
}


里面将我们传入的阴影radius参数转为Sigma并创建了BlurDrawLooper,我们来看看其实现


#include "BlurDrawLooper.h"
#include <SkMaskFilter.h>

namespace android {

BlurDrawLooper::BlurDrawLooper(SkColor4f color, float blurSigma, SkPoint offset)
: mColor(color), mBlurSigma(blurSigma), mOffset(offset) {}

BlurDrawLooper::~BlurDrawLooper() = default;

SkPoint BlurDrawLooper::apply(Paint* paint) const {
paint->setColor(mColor);
if (mBlurSigma > 0) {
paint->setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, mBlurSigma, true));
}
return mOffset;
}

sk_sp<BlurDrawLooper> BlurDrawLooper::Make(SkColor4f color, SkColorSpace* cs, float blurSigma,
SkPoint offset)
{
if (cs) {
SkPaint tmp;
tmp.setColor(color, cs); // converts color to sRGB
color = tmp.getColor4f();
}
return sk_sp<BlurDrawLooper>(new BlurDrawLooper(color, blurSigma, offset));
}

} // namespace android

内容不多,可以看到本质上还是利用了setMaskFilter来实现的。


然后还剩下一个点就是通过SkMaskFilter::MakeBlur生成的模糊是占位的,如果能知道模糊具体需要多大的空间,就可以方便的进行预留以免实际展示时阴影被裁剪。
MakeBlur最终返回的是一个SkBlurMaskFilterImpl对象,我们可以先看一下其父类SkMaskFilterBase的虚函数:重点关注computeFastBounds函数


SkMaskFilterBase.h - Android Code Search


    /**
* The fast bounds function is used to enable the paint to be culled early
* in the drawing pipeline. This function accepts the current bounds of the
* paint as its src param and the filter adjust those bounds using its
* current mask and returns the result using the dest param. Callers are
* allowed to provide the same struct for both src and dest so each
* implementation must accommodate that behavior.
*
* The default impl calls filterMask with the src mask having no image,
* but subclasses may override this if they can compute the rect faster.
*/

virtual void computeFastBounds(const SkRect& src, SkRect* dest) const;

可以看到该函数的作用便是计算MaskFiter的bounds,看一下子类的SkBlurMaskFilterImpl的实现


void SkBlurMaskFilterImpl::computeFastBounds(const SkRect& src,
SkRect* dst)
const
{
// TODO: if we're doing kInner blur, should we return a different outset?
// i.e. pad == 0 ?

SkScalar pad = 3.0f * fSigma;

dst->setLTRB(src.fLeft - pad, src.fTop - pad,
src.fRight + pad, src.fBottom + pad);
}

其中fSigme便是最开始通过convertRadiusToSigma(radius)获取到的返回值,其计算方式如下:
SkBlurMask.cpp - Android Code Search


// This constant approximates the scaling done in the software path's
// "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)).
// IMHO, it actually should be 1: we blur "less" than we should do
// according to the CSS and canvas specs, simply because Safari does the same.
// Firefox used to do the same too, until 4.0 where they fixed it. So at some
// point we should probably get rid of these scaling constants and rebaseline
// all the blur tests.
static const SkScalar kBLUR_SIGMA_SCALE = 0.57735f;

SkScalar SkBlurMask::ConvertRadiusToSigma(SkScalar radius) {
return radius > 0 ? kBLUR_SIGMA_SCALE * radius + 0.5f : 0.0f;
}

这样,我们可以得到一个模糊的近似Bound,虽然不是一个准确的值但是至少可以保证绘制的阴影不会被裁剪。
当然,如果无法预留Padding也可以通过clipChildren=false来实现。


总结


最后我也是针对setShadowLayer提供了一个自定义View的实现方式:


Lowae/Shadows: A simple and customizable library on Android to implement CSS style shadows (github.com)


感兴趣的可以尝试使用,有任何兼容性问题欢迎提issue~



(我十分清楚会有很多兼容性问题,没办法,这种Api就是这样,不,准确来说,Android就是这样)



所以,想在Android上1:1还原设计稿上的阴影是比较困难的,但是如果不去追求参数的还原只是寻求视觉的略显一致,那还是可以做到的,简单点的通过第一种方式(Elevation + Outline),如果设置到阴影颜色或者offset这种便可以尝试最后一种方式(

作者:Lowae
来源:juejin.cn/post/7270503053358874664
setShadowLayer)。

收起阅读 »

卸下if-else 侠的皮衣!- 适配器模式

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


上篇文章学习了 策略模式,有兴趣可以过去看看,下面我们来学习适配器模式


场景:将一个接口返回的数据,转化成列表格式,单选框数据格式,多选框数据格式


用if-else来写,如下


let list = []
let selectArr = []
let checkedArr = []

http().then(res =>{
//处理成列表格式
this.list = this.handel(res,0)
//处理成下拉框模式
this.selectArr = this.handel(res,1)
//处理成多选框模式
this.checkedArr = this.handel(res,2)
})
handel(data,num){
if(num == 0){
....
}else if(num ==1){
....
}else if(num ==2){
....
}
}

分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


//定义一个Adapter
class Adpater {
data = [];
constructor(){
this.data = data
}
toList(col){
//col代表要的字段,毕竟整个data的字段不是我们都想要的
//转列表
return this.data.map(item =>{
const obj = {}
for(let e of col){
const f = e.f
obj[f] = item[f];
}
return obj
})
}
//用了map的省略写法,来处理转化单选的格式
toSelect(opt){
const {label,value} = opt
return this.data.map(item => ({
label,
value
}))
}
//同上处理转化多选的格式
toChecked(opt){
const {field} = opt
return this.data.map(item => ({
checked:false,
value:item[field]
}))
}
}

//下面是调用这个适配类
let list = []
let selectArr = []
let checkedArr = []
http.then(data =>{
const adapter = new Adatpter(data)
//处理列表
list = adapter.toList(['id','name','age'])
//处理下拉
selectArr = adapter.toSelect({
label:'name'
value:'id'
})
//处理多选
checkedArr = adapter.toChecked({
field:'id'
})
})

这个扩展性就能大大提高,看着也会好看很多,可以通过继承等等方式来扩展,继承方式下次有机会再来写代码,文章先到这里!


结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加

作者:向乾看
来源:juejin.cn/post/7265694012962537513
油,骚年们!给我点点赞,关注下!

收起阅读 »

当文字成为雨滴:HTML、CSS、JS创作炫酷的"文字雨"动画!

web
简介 大家好,今天要给大家带来一个Super Cool的玩意儿😎! 在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落...
继续阅读 »

简介


大家好,今天要给大家带来一个Super Cool的玩意儿😎!


rain-preview.gif


在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落像是将文字变成雨滴从天而降,营造出与众不同的视觉效果;


HTML


创建一个基本的HTML结构,这段HTML代码定义了一个容器,其中包含了"云朵"和"雨滴"(即文字元素)。基本结构如下:



  • 首先是类名为container的容器,表示整个动画的容器;

  • 其次是类名为cloud的容器,表示云朵的容器;

  • 接着是cloud容器中的文字元素,表示雨滴(即文字元素);
    然后引入外部创建的css和js文件,可以先定义几个text容器,用于调整样式;


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Rain Animation</title>

<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="container">
<div class="cloud">
<!-- <div class="text">a</div> -->
<!-- <div class="text">b</div> -->
<!-- <div class="text">c</div> -->
<!-- 雨滴将会在这里出现 -->
</div>
</div>

<script src="./js/main.js"></script>
</body>
</html>

CSS


CSS是为文字雨效果增色添彩的关键,使动画效果更加丰富,关于一些 CSS 样式:



  • 使用了自定义的颜色变量来为背景色和文本颜色提供值,有助于使代码易于维护和修改;

  • 利用CSS的阴影效果和动画功能,创造逼真的"云朵"和流畅的"雨滴"动画;


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

:root {
--body-color: #181c1f;
--primary-color: #ffffff;
}

body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: var(--body-color);
}

.container {
width: 100%;
height: 400px;
display: flex;
justify-content: center;
border-bottom: 1px solid rgba(255, 255, 255, .1);
/* 添加一个从下往上线性渐变的镜像效果,增加视觉层次感 */
-webkit-box-reflect: below 1px linear-gradient(transparent, transparent, transparent, transparent, #0005);
}

.cloud {
position: relative;
top: 50px;
z-index: 100;

/* 横向云朵 */
width: 320px;
height: 100px;
background-color: var(--primary-color);
border-radius: 100px;

/* drop-shadow函数将阴影效果应用于投影图像 */
filter: drop-shadow(0 0 30px var(--primary-color));
}
.cloud::before {
content: "";
/* 左侧小云朵 */
width: 110px;
height: 110px;
background-color: var(--primary-color);
border-radius: 50%;
position: absolute;
top: -50px;
left: 40px;

/* 右侧大云朵 */
box-shadow: 90px 0 0 30px var(--primary-color);
}

.cloud .text {
position: absolute;
top: 40px;
height: 20px;
line-height: 20px;

text-transform: uppercase;
color: var(--primary-color);
/* 为文字添加阴影,看上去发光,增加视觉效果 */
text-shadow: 0 0 5px var(--primary-color), 0 0 15px var(--primary-color), 0 0 30px var(--primary-color);
transform-origin: bottom;
animation: animate 2s linear forwards;
}

@keyframes animate {
0% {
transform: translateX(0);
}

70% {
transform: translateY(290px);
}

100% {
transform: translateY(290px);
}
}

通过关键帧动画 @keyframes animate 定义文字运动的过程,在这里是垂直移动290px,也就是向下移动,模拟下雨的状态。当然为了让文字雨效果更加好看,还可以引入一下字体库;



Warning


-webkit-box-reflect:可将元素内容在特定方向上进行轴对称反射;


但是该特性是非标准的,请尽量不要在生产环境中使用它!


目前只有webkit内核的浏览器支持,如:谷歌浏览器、Safari浏览器。在火狐浏览器中是不支持的;



JavaScript


最后,使用JavaScript来实现文字雨的效果。通过动态生成并随机选择字符,可以实现让这些字符(雨滴)从.cloud(云朵)中降落的效果。JavaScript 脚本逻辑:



  • 首先,定义函数 generateText() 并创建字符集,定义函数 randomText() 通过从给定的字符集中随机选择一个字符返回;

  • 接下来,编写 rain() 函数,在函数内部,首先选取 .cloud 元素同时创建一个新的 <div>元素作为字符节点,设置元素文本内容为函数返回的字符,并添加类名;

  • 然后,利用 Math.random() 方法生成一些随机值,将这些随机值应用到创建的 <div> 元素上,包括:

    • 字符距离左侧位置,在 .cloud 容器的宽度区间;

    • 字体大小,最大不超过32px;

    • 动画周期所需的时间(动画持续时间),2s内;



  • 最后,将其<div>添加到 .cloud 元素中,使用 setTimeout() 函数在2秒后将文字节点从 .cloud 元素中移除,模拟雨滴落地消失效果;


定时器: 为了让字符(雨滴)持续下落,使用 setInterval 函数和一个时间间隔值来调用 rain() 函数。这样就是每20毫秒就会生成一个新的字符(雨滴)节点并添加到云朵中。


// 生成字母和数字数组
function generateText() {
const letters = [];
const numbers = [];

const a = "a".charCodeAt(0);

for (let i = 0; i < 26; i++) {
letters.push(String.fromCharCode(a + i));

if (i < 9) {
numbers.push(i + 1);
}
};

return [...letters, ...numbers];
};

// 从生成的数组中随机取出一个字符
function randomText() {
const texts = generateText();
const text = texts[Math.floor(Math.random() * texts.length)];

return text;
};

function rainEffect() {
const cloudEle = document.querySelector(".cloud");
const textEle = document.createElement("div");

textEle.innerText = randomText();
textEle.classList.add("text");

const left = Math.floor(Math.random() * 310);
const size = Math.random() * 1.5;
const duration = Math.random();
const styleSheets = {
left: `${left}px`,
fontSize: `${0.5 + size}em`,
animationDuration: `${1 + duration}s`,
};
Object.assign(textEle.style, styleSheets);

cloudEle.appendChild(textEle);
setTimeout(() => {
cloudEle.removeChild(textEle);
}, 2000);
};

// 每隔20ms创建一个雨滴元素
setInterval(() => rainEffect(), 20);

结论


通过HTML、CSS和JS的紧密合作,成功创建了一个炫酷的"文字雨"动画效果,这个动画可以增加网页的吸引力! 不要犹豫🖐️,动手尝试一下,或者甚至你也可以根据自己的需求对文字、样式和动画参数进行调整,进一步改善和扩展这个效果;


希望这篇文章对你在开发类似动画效果时有所帮助!另外如果你对这个案例还有任何问题,欢迎在评论区留

作者:掘一
来源:juejin.cn/post/7270648629378367528
言或联系(私信)我。谢谢阅读!🎉

收起阅读 »

大厂产品为何集体下架

快手一个 30 多人的产品团队每月开销上百万,其中技术、产品和运营是开支大头。如果碰上产品商业化能力不行,那这就是铁亏。 昨天看了Tech星球发表的一篇文章,说的是各互联网大厂集中下架 60 多款 App 的事儿,有些感想。 不可否认,互联网已经过了最激进的时...
继续阅读 »

快手一个 30 多人的产品团队每月开销上百万,其中技术、产品和运营是开支大头。如果碰上产品商业化能力不行,那这就是铁亏。


昨天看了Tech星球发表的一篇文章,说的是各互联网大厂集中下架 60 多款 App 的事儿,有些感想。


不可否认,互联网已经过了最激进的时期,尤其是过去十年以移动互联网为代表的这一轮技术周期。


接下来,几乎所有的互联网企业都会关注成本和效益。


说白了,要赚稳当的钱并好好活下去。


而那些烧钱的产品、业务、部门,则大概率都会关停并转。


光是今年,腾讯就已经下架了包括游戏在内的 40 多款 App,其中不乏 QQ 堂这样的元老级产品。


除此之外,其他大厂也都在进行同样的操作,产品下架意味着团队解散,对应就会出现人员缩减。


我知道,很多人看到这样的信息一定会产生焦虑,认为互联网是不是不行了?找工作是不是更难了?会不会面临裁员失业了?


这里,我想说几个内在的逻辑关系。


先说很多人关心的裁员和失业问题,以及应对策略。


对于企业来说,他们会优先从自身立场出发进行决策,在面对发展危机时,开源节流、壮士断腕、集中火力就会成为优先考虑。


这种影响首先会辐射到边缘业务和产品以及探索型项目上,直接关停就是解决方案,以此释放出来的资源可以投入到主营业务上,而多余的资源则对外释放。


这里说的对外释放,其实也就是裁员。



被裁员的对象通常符合这么几个特征,一线螺丝钉、高成本中层、多余的高层。


因此,要想避免自己陷入这样的困境,至少有这么几件事可以做。


第一,选择公司和业务时尽量避开边缘部门和探索创新性业务,比如去主营电商业务的公司做社交产品。


可能有人会说了,没办法啊,有时候选择权不在自己,能找到工作就不错了,哪还能挑三拣四。


不过我想说,这种破罐子破摔的思想一定会害人不浅。


没选择的前提一定是没本事,而没本事的原因就是学习和实践能力不行。


不管是公司还是社会,都是典型金字塔结构,越往上,能容纳的空间就越小。


当别人奋力往上爬的时候,你还在原地踏步,更好的工作机会自然就不会属于你,这是现实。


所以,别再抱怨工作不好找,这是从外界找原因。先看看自己有什么,从内在找问题。


浑浑噩噩过下去是一种选择,精明筹划过下去也是一种选择。选择不同,结果不同。


第二,尽早建立自己的职业标签,尽快找到自己的关键位置,尽力打造自己的不可替代性。


职业标签、关键位置、不可替代性,这三点是一个成熟专业且有独特价值职场人的标配。


职业标签是专业能力的体现,SaaS 产品专家、电商供应链产品专家、数据产品专家、搜索和策略产品专家等,这都是职业标签。


什么都会,什么都不精,是不可能形成职业标签的,也就无法获得第二个优势,关键位置。


关键位置是你所在部门和公司的岗位,不一定是领导,但一定是一个不可或缺的角色。


比如,负责搭建公司后台业务系统的产品经理,对整体结构和细节是最了解的那个人。


获得了关键位置的前提是职业标签,而获得关键位置后就能进而获得第三个优势,不可替代性。


遇上公司裁员,可能会裁掉一线大头兵,但不会裁掉处于关键位置的人。铁打的营盘,流水的兵,只要核心还在,就可以继续生长。


职业标签、关键位置、不可替代性,评估一下自己有没有,缺不缺。


问题找到了,才有形成解决方案的可能性。


第三,不断为自己打造优质的信息和人脉渠道,你可以不喜欢圈子,但你不得不混圈子。


任何一个领域都会有对应的圈子,那里聚集了人、信息、机会,越是专业的圈子,这三方面的质量就越高。


应届生找工作靠校招,工作几年后找工作靠社招,工作多年后找工作靠朋友,理解了这一点,你就知道信息和人脉渠道有多重要了。


不知道你们有没有一种感觉,这几年世界变化很快,这种变化带来了太多的不确定性,让原本的稳定得以打破。


有的人认为这是危机,还有的人认为这是机会。要我看,越是不确定性增强的危机时刻,可探索的机会其实就更多。


原因很简单,稳定格局的建立意味着机会窗口的关闭,而稳定被打破,说明机会窗口的重新打开。


我对未来还是持有乐观态度的,即便寒气逼人,我们依旧可以拥有一颗火热向前的内心。



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

如果你的退路有1亿人在卷了,阁下又该如何应对?

你所认为的退路,其实已经有1亿人在卷了,"那么阁下将如何应对?" 引子 最近进了一些技术交流群,群友们也是来自五湖四海,有些碰巧还在找工作,自嘲 「“大不了去送外卖”、“大不了去开滴滴”」 这句话在工作中有时候大家也经常会有这样的调侃。 其实这几年互联网头部公...
继续阅读 »

你所认为的退路,其实已经有1亿人在卷了,"那么阁下将如何应对?"


引子


最近进了一些技术交流群,群友们也是来自五湖四海,有些碰巧还在找工作,自嘲 「“大不了去送外卖”、“大不了去开滴滴”」 这句话在工作中有时候大家也经常会有这样的调侃。


其实这几年互联网头部公司都比较难做,在21年 2记重拳打向这个行业:打击打击资本无序扩张与规范平台经济发展、反垄断。阿里和美团前年也都收到了182亿和32亿的天价罚单,这不最近蚂蚁不是又被罚了71亿,降本增效、开源节流成为各大公司的主旋律,毕竟_"地主家也没有余粮啦"_,再加上坊间传闻35岁的潜规则,大家的危机感还是挺强的。


送快递、送外卖、跑滴滴,这些职业因为没有什么门槛,也没有什么年龄限制,只要你愿意吃苦,就能稳定赚到钱,可是事实上,这些真的是不错的退路吗?真相可能跟你想象的完全不一样。




卷起来


在今年3月份时候,中华总工会发布了一个统计:目前全国职工总数4.02亿人,而从 「事外卖、快递、网约车等新就业形态的群体,已达8400万人」





「也就是说:你以为的退路,实际上已经有一亿人在卷了」



网约车


记得在五月份的时候流传过一个 段子“报名滴滴网约车由于饱和被劝退到美团外卖 ”,网约车行业被视为一个门槛较低的行业,只要会开车,身体健康没有犯罪记录,就可以轻松成为网约车司机,无论专职还是兼职,都是一个不错的选择。所以,最近几年,网约车成为我国一个相当重要的灵活就业市场,也在很大程度上减轻了我国的就业压力。「司机们纷纷涌入网约车行业,但是网约车的订单数量却在持续萎缩。」



数据显示,2022年,我国网约车用户规模为4.37亿人,同比减少了3%,网约车市场规模约为3100多亿元,同比下降1.4%,



今年5月,中国多个城市,发布了 「网约车饱和预警,劝大家谨慎入行」「长沙、三亚等城市,已暂停受理网约车运输证新增业务」。 考虑到同期司机数量的激增,那么势必会造成 网约车司机能够接到的订单迅速减少。很多城市的网约车司机日均接单量不到10单,和前几年相比大概下降了一半。「过去很多网约车司机可以月收入轻松过万元,而现在想要这个目标难度加了不止一点点。」


外卖



 今年3月份广州出现有史以来第一次外卖骑手“爆满”!想当外卖骑手还得托关系。外卖已经成为知识密集型行业,广州外卖骑手本科率接近30%,毕竟全国人口的本科率也只有4.4%左右。情况变化之快令人咂舌,前几年还有篇文章 “被困在系统里面出不去的美团骑手”,今年却出现了 “想要到系统里面却进不去”。 美团财报显示,2021年美团骑手的数量为527万,2022年,美团骑手的数量变成了624万,2022年,美团骑手的数量变成了624万,其中81.6%是来自县域乡村地区的农村转移劳动力,平台一年新增骑手的人数达到了97万 


 而行业进入饱和期之后,不可避免的就是降薪了。这对于本不富裕的外卖小哥来说无疑是雪上加霜 


做好当下


其实呢 也不需要气馁或者过于焦虑,了解一些事实,有助于我们更好的把握好当下。看清楚事实,坦诚的面对自己,“知人者智,自知者明”。面对当前互联网的大环境,可以主动思考一下自己的“护城河”是什么,高端的学历?大厂的履历?扎实的技术?殷实的家境?甚至是人脉广情商高。如果暂时想不起的来话,还是得行动起来去有意识的构建属于自己的“护城河”。每个人都是独一无二的,至少我们可以从认真做好当下的本职工作做起,最后跟大家共享一句我比较喜欢的一句话



yesterday is history,tomorrow is a mystery,but today is a gift



做好当下,尽人事听天命~

作者:Android茶话会

来源:juejin.cn/post/7270871863160700984

收起阅读 »

人情世故职场社会生存实战篇(二)

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,本篇是本专栏的第二篇,欢迎大家踊跃发言 人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二)11、问:如何改善和领导的关系? 答:1、工作上有能力。现在社会变化快,三、...
继续阅读 »

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,本篇是本专栏的第二篇,欢迎大家踊跃发言


人情人情世故职场社会生存实战篇(一)

人情人情世故职场社会生存实战篇(二)

11、问:如何改善和领导的关系?


答:1、工作上有能力。现在社会变化快,三、五年组织环境,内部的领导可能都会有大的变化,自己不能无所作为的等着,要努力提升自己的工作能力,业余时间学习工作的各个环节的东西,毕竟职场还是要有人干活的,领导再差劲也不能只用小人,还需要具体的做事的人,有真本事不怕。


2、权力上能维护。有工作能力的人很多,但是越是有工作能力的人,越是要处处维护领导的权力,早请示晚汇报,让他觉得一切都在掌控之中,有功归上。这样的工作才能得到领导的认可。如果没有权力意识,工作能力越强,就让领导越是感到威胁,很多有能力的人,对领导毫不尊重,自然备受打击。


3、生活上凑交集。现在人分的很清楚,工作是工作,生活是生活,下班了也就没有什么交集了,工作关系如果未能产生私交,两个人的关系不会稳定,私交是关系的粘合剂。所以一定要有私下的交集,这个交集要偶然,也就是要凑机会,多了领导烦。把握好度,比如孩子在一个培训班,妻子在一个瑜伽馆。


12、问:我是刚毕业两年的研究生,在国企工作。我得知一个消息,我有一个老乡任命为我们单位的财务副总,下个月就会到岗,我想请他提携我一下。我该怎么做呢?


答:首先打铁必须自身硬,他下个月到岗,我建议你不要一来就仗着自己是他老乡就张口请人帮你,没有这样的道理。你可以仔细的写一下你了解的单位的现状、存在的问题,以及你预想的以后未来发展的方向,形成一份书面材料。等他到岗后,你借向他汇报工作的时候交给他,让他看到你不是废铁一块,这样子人家自然更乐意帮你。其次,职场中关系也非常重要的,有关系就有一切,你呀,先组个局,请老乡吃个饭,祝贺一下,把原来不温不火的关系回温一下,关系到位了,你不用求,别人自然会帮。


13、问:老师我实习快结束了,需要转正,想给领导送礼,请问怎么送比较好?


答:很多人给领导送礼,送了一条烟,马上说:我转正的事儿,麻烦您了。你转正的事儿,又不是他一个人说了算,所以你的东西,他收了,反而睡不着,甚至会梦到自己进去了。你一直送,领导说,转正的事儿,你争取下。你说:争取个啥啊,在哪儿不是做贡献啊。领导说,你先走个程序嘛,你勉为其难说:哎,那就走个吧。失败了,咱说:我说我不行吧,你非要让我上,我啥水平我不知道吗,谢谢您高看我。领导说:别急,下次再试试,你说不试了,我对我目前的待遇非常满意。


你没野心,他反而帮你了,也就是你不能让领导有压力。有人说了,马勒戈壁,我都送礼了,为什么还忍着,我只能说对不起兄弟,你别忍了,你去爆炸吧。领导让你试试,你成功了,你要说:哎呦,又是你帮我使劲了。一句话,去送就好了,习惯性送有价值的东西 ,如果领导拒绝你,你就说,这个是自己的一点小心意,在这实习期期间,你在公司学习到了很多,也跟领导学习了很多,一直感恩就好了。


14、问:因为我工作失误,害的我部门领导被大领导骂,还要帮我各方协调挽回损失。我想买点礼物感谢一下我部门领导,但是从来没有过给领导送东西的经验,请问我什么时候送合适啊 ?是现在事情刚过就送 ?还是过年过节有合适机会了送啊。


答:你傻呀!肯定是现在马上去办了,此时过去,你名正言顺的感谢领导帮你挽回了工作上的失误,并表示,如果没有他,你可能就丢了这份工作,或者造成更大的损失,然后感激之情,无以言表,送上一点小心意(烟酒+红包),请领导一定要收下,不然自己寝食难安。人呐,都喜欢懂得感恩的人。


15、问:老师,您好!单位中层调整,之前托领导前辈已给老大打过招呼,礼数走过了,现事已到跟前,私下和老总见面,老大说竞争很激烈,走程序,请老师指点下一步怎么走?


答:老大的话,其实意思也比较明显了,“竞争很激烈”=打招呼的人很多,表心意的人很多,“走程序”=你送的不够,咱就走程序,你送的够,咱就插队,走后门,老大的意思,就是现在 你这边没啥竞争力,你看看要不要加码。


如何破局?你找前辈领导在出一次面,你把心意给前辈领导,让他帮你给老大。很多事儿,可以通过送梨搞定。愿送,永远都有机会。不愿送,只能走程序……在咱们这儿,领导说,该走的程序,咱必须走。这就意味着有戏。要是领导说,走正常程序吧,意味着这个事儿啊,100%搞不定。


16、问:老师请问找老大办事,给老大送礼,老大说送礼破坏感情,说你的心意我领了,我会找机会帮你办的,我该怎么办?


答:心意我领了,我会找机会帮你办的,这个就是典型套话,你要是当真了,你的事就会一直杳无音讯,没有下文。这个时候,你要说,领导,你帮我太多了,然后列举123件事,一直感恩就好了。你说,领导,没有你的关照,就没有我的今天,可能这些帮助,都是您随手为之的,但是,对我的帮助是巨大的,这个恩我一直记得,这点心意您要是不收下,我真的问心有愧,以后都不敢找你指点工作,指点人生了。领导这心意我就放着了,我还有事我先走了,我那个事不打紧,如果麻烦的话,领导您千万别为难。


17、问:老师我身边有个同事,嘴有点J,喜欢乱开玩笑,我不知道怎么怼他,每次虽然我没说啥,但心里是很不高兴的。比如我昨天穿了一件新衣服,他说你新衣服真的是不得了,像个新郎官,怎么着又想做一次新郎官,那样可不行。那是犯了生活作风问题,不过你可以偷偷摸摸的夜夜做新郎。


答:有时候你让他一小步,他就会前进一大步。由刚开始不怀好意的开玩笑,慢慢变成欺负你。对于这种毫无顾忌的玩笑,必须给予反击。你可以这样说:我是既没那贼心也没那贼胆,老婆管得严,不像哥哥你在哪里都没人敢管,你能夜夜做新郎,我只能做一回新郎。


18、问:单位风气不正,很多老员工总是不干活,同事也是甩活给我,我该怎么办?我是一个合同工,单位的关系错综复杂,一杆子打下去,他们都是关系户,很多老员工仗着资历老,都不干活,同事也是照猫画虎,喜欢把活甩给我,请问,我是要学会拒绝,还是继续接受?


答:一般来说,单位里面就几种人老是被针对。


1、新人,太嫩,总是希望通过表达自己的热情和善意,以期希望得到对方的帮助,其实没必要!你够强,你有价值,别人就会尊重你,你一来就暴露底牌,别人就知道怎么拿捏你了。


2、软人,性格太软,别人听你说话就知道你是一个可以欺负的人,你要学会提高自己的气场。


3、孤立的人,这种人不会抱团,狮子落单了,鬣狗都要去掏肛,更别提小绵羊了,狮子、野狗,狼群都会群殴它。


4、情商低,不会跟领导搭关系,没有领导撑腰你是合同工,你也可以很硬气,谁知道单位的大领导不会是你舅舅?你硬气一点,别人还会觉得你有关系,反倒是不敢欺负你,这里,你会拒绝就行了,会推,会找借口,很好办的。人性本贱,这是我在群内一直讲的,你好说话,别人就喜欢欺负你你不好说话,别人反而尊重你。


19、问:我有个领导超级傻叉,工作上很多事情的流程和解决办法,她都不清楚,遇到问题她也搞不明白,就比如这两天,其他部门的员工都知道的消息,就只有我们部门的员工不知道,问题是她也不知道,搞的我们乱成一团麻,然后还瞎喷瞎指挥。后面我自己去找其他部门问情况,才得知最新消息和工作流程!


我是非常不明白,为什么像这种蠢H一样的货色,都能当领导?能力那么差就不要出来嚯嚯别人了好吗!请问如何对待这种领导?是不是能力不行的领导都喜欢通过折腾别人,在一些细枝末节而没有意义的事情是精益求精,才能彰显自己的存在感?


答:没有能力,但是却做了你的领导,那么她必然有她优势的一面,比如关系!不然,她又如何到了这个位置?一将无能,累死三军,主将无能,其实就是一个很大的机会,篡位的宰相,为什么都喜欢皇帝昏庸无能?因为他只要控制了这个蠢货,他就控制了整个朝堂!一人之下,万人之上,甚至他就是无冕之王。


所以,这里你的答案也很简单,你只要会向上管理,想办法帮她消化所有的问题,让自己成为她离不开的人。每次遇到问题了,你就带着通盘的方案去跟她讨论该怎么办,流程如何,细节如何,无形之中把你的方案植入他的脑子里面,然后以后遇到事情,她跟任何人都搭不来,但是跟你特别有缘,你说是不是很有趣。这还只是初步的,你要知道她的背后一定有更强的关系,想办法融入进去,在职场中你一步一高升,不是更简单?问题就是机遇,就看你拥有多宽广的视野了!


20、问:老师您好,我认识一个退休的老领导(不在本地),他跟现在我们市局一把手关系好。最近我们要有一波提拔,我看中一个位置。我想通过这个老领导给我们一把手送礼,我该怎么办?我该怎么说让这个老领导引荐一下比较合适?如果他愿意帮忙的话,怎么给我们一把手送礼合适?


答:退休了,人走茶凉。他有没有能量,就一个判断标准∶他的儿女立起来了吗?要是他的儿女都没立起来。你想立,他也爱莫能助。人,只要退休了,说话跟放屁一样,无人接招。他跟你一把手关系好,这个关系好必须建立在有共同利益的基础上,没有这个基础,这个关系就是假的。再说,动用他的资源帮你,他的回报率难道就是烟酒茶?他缺这点儿东西?一句话, 你送礼没戏..想想你能为他提供什么其他价值......


欢迎同学们关注本专栏,会持续更新社会上的人情世故,有问题的同学也欢迎留言~


作者:公z号_纵横潜规则
来源:juejin.cn/post/7268050036474232851

收起阅读 »

继copilot之后,又一款免费帮你写代码的插件

写在前面 在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。 copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。 按理说这么好用,又可以提高效率的工具,收点费也理所当然 但是秉承白嫖一时爽,一直白嫖一直爽的原则...
继续阅读 »

写在前面


在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。


copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。


按理说这么好用,又可以提高效率的工具,收点费也理所当然


但是秉承白嫖一时爽,一直白嫖一直爽的原则(主要是我穷穷穷),又发现了一款可以平替的插件CodeGeex


一、CodeGeex简介


① 来自官方的介绍



CodeGeeX is a powerful intelligent programming assistant based on LLMs. It provides functions such as code generation/completion, comment generation, code translation, and AI-based chat, helping developers significantly improve their work efficiency. CodeGeeX supports multiple programming languages.



翻译过来大概是



CodeGeeX是一个功能强大的基于llm的智能编程助手。它提供了代码生成/完成、注释生成、代码翻译和基于ai的聊天等功能,帮助开发人员显著提高工作效率。CodeGeeX支持多种编程语言。



GitHub地址


github.com/THUDM/CodeG…


目前在GitHub上 2.6k star 最近更新是2周前




③ 下载量

  • vscode 目前已有129k下载量
  • idea 目前已有58.7k 下载量

二、插件安装


① vscode




②idea


注: idea低版本的搜不到这个插件,小编用的是2023.01 这个版本的




安装完成后,注册一个账号即可使用


三、帮你写代码

① 我们只需要输入注释回车,它就可以根据注释帮你写代码

② tab接受一行代码 ctrl+space 接受一个单词








四、帮你添加注释



有时候,我们拿到同事没有写注释的代码,或者翻看一周前自己写的代码时。


这写得啥,完全看不懂啊,这时候就可以依靠它来帮我们的代码添加注释了



操作方法:

① 选中需要添加注释的代码

② 鼠标右键选择Add Comment

③ 选择中文或者英文






这是没加注释的代码

public class test02 {
   public static void main(String[] args) {
       int count=0;
       for(int i=101;i<200;i+=2) {
           boolean flag=true;
           for(int j=2;j<=Math.sqrt(i);j++) {
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       System.out.println(count);
  }
}

这是CodeGeex帮加上的注释

public class test02 {
   //主方法,用于执行循环
   public static void main(String[] args) {
       //定义一个变量count,初始值为0
       int count=0;
       //循环,每次循环,计算101到200之间的值,并判断是否是因子
       for(int i=101;i<200;i+=2) {
           //定义一个变量flag,初始值为true
           boolean flag=true;
           //循环,每次循环,计算i的值,并判断是否是因子
           for(int j=2;j<=Math.sqrt(i);j++) {
               //如果i的值不是因子,则flag设置为false,并跳出循环
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           //如果flag为true,则count加1,并打印出i的值
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       //打印出count的值
       System.out.println(count);
  }
}

基本上每一行都加上了注释,这还怕看不懂别人写的代码


五、帮你翻译成其他语言



除了上面功能外,CodeGeeX 还可以将一种语言的代码转换成其他语言的代码



操作方法:

① 选中需要转换的代码

② 鼠标右键选择Translation mode

③ 在弹出的侧边栏中选择需要转换成的语言,例如C++、 C#、Javascript 、java、Go、Python、C 等等

④ 选择转换按钮进行转换






六 小结


试用了一下,CodeGeeX 还是可以基本可以满足需求的,日常开发中提高效率是没得说了


作为我这样的穷逼,完全可以用来平替copilot,能白嫖一天是一天~


也不用当心哪天不能用了,等用不了了再找其他的呗




本期内容到此就结束了


希望对你有所帮助,我们下期再见~ (●'◡'●)


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

小米备案 mios.cn 网站域名

8月22日消息,近期有关小米科技备案新域名和自研操作系统的消息引起了广泛关注。根据最新披露的信息,小米科技有限责任公司于去年11月11日备案了mios.cn网站域名。然而,当前访问该域名时遇到了403错误,意味着资源暂时不可用,同时网站也尚未设置HTTPS安全...
继续阅读 »

8月22日消息,近期有关小米科技备案新域名和自研操作系统的消息引起了广泛关注。根据最新披露的信息,小米科技有限责任公司于去年11月11日备案了mios.cn网站域名。然而,当前访问该域名时遇到了403错误,意味着资源暂时不可用,同时网站也尚未设置HTTPS安全证书。



关于自研操作系统的消息更是引发了热议。有博主在今年8月15日爆料称,小米正在积极打磨一款自研操作系统,并已取得了阶段性进展。然而,这一爆料后来却被删除,导致外界对其真实性产生了疑问。值得注意的是,同一博主在今日上午发布了新消息,表示小米的自研系统将实现全端覆盖,并且兼容了AOSP(Android开放源代码项目)。尽管有人曾称MIUI 15将更名为mios,但博主在最新消息中否认了这一说法。

此外,一些人或许会对小米为何没有采用更通用的mios.com域名产生疑惑。事实上,据ITBEAR科技资讯了解,该域名已经被他人注册,目前被用于一个国外的物联网设备自动化网站。

尽管目前关于小米备案域名和自研操作系统的信息仍存一定的不确定性,然而这些信息的传播已经引发了业界和用户的广泛关注。未来,我们可以期待小米在科技领域带来更多令人期待的创新。

作者:ITBEAR科技资讯

来源:www.itbear.com.cn/html/2023-08/469514.html

收起阅读 »

App本地配置持久化方案

iOS
概述 在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。 UserDefaults Apple提供了UserDefault框...
继续阅读 »

概述


在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。


UserDefaults


Apple提供了UserDefault框架来帮助我们存储离散的配置,UserDefaults将以plist文件的形式存储在沙盒环境中。在不引入NoSql数据库的情况下,这是首推的方案。


注意事项


为了提升读取速度,App在启动时会将UserDefaults Standard对应的plist加载到内存中,如果文件过大就会增加App在启动时的加载时间,同时提高一定的内存消耗。


所以在Standard中,我们应该存放需要在App启动阶段立即获取的信息,比如用户最近登录的ID,App远程配置缓存的版本。


我们可以通过分表来缩减Standard的数据量。使用UserDefaults的suiteName模式创建不同的配置表,这样配置项将存储到各自的plist文件中,这些独立的plist不会在启动时被自动加载。


配置管理的常见问题

  1. 使用硬编码的String Key将配置存储到UserDefaults中,通过复制粘贴Key的字符串来存取数据。

  2. 零散的使用UserDefaults,缺少中心化管理方案。比如需要存储“开启通知功能”的配置,Key通常会直接被放在业务相关代码中维护。


方案 1.0


管理UserDefaults


创建一个UserDefault的管理类,主要用途是对UserDefault框架使用的收口,统一使用策略。

public class UserDefaultsManager {
public static let shared = UserDefaultsManager()
private init() {}
public var suiteName:String? {
didSet {
/**
根据传入的 suiteName的不同会产生四种情况:
传入 nil:跟使用UserDefaults.standard效果相同;
传入 bundle id:无效,返回 nil;
传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的 plist 文件。
*/
userDefault = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
}
}
public var userDefault = UserDefaults.standard
}


创建常量表

  1. 对配置项的Key进行中心化的注册与维护
struct UserDefaultsKey {
static let appLanguageCode = "appLanguageCode"
static let lastLaunchSaleDate = "resetLastLaunchSaleDate"
static let lastSaleDate = "lastSaleDate"
static let lastSaveRateDate = "lastSaveRateDate"
static let lastVibrateTime = "lastVibrateTime"
static let exportedImageSaveCount = "exportedImageSaveCount"

static let onceFirstLaunchDate = "onceFirstLaunchDate"
static let onceServerUserIdStr = "onceServerUserIdStr"
static let onceDidClickCanvasButton = "onceDidClickCanvasButton"
static let onceDidClickCanvasTips = "onceDidClickCanvasTips"
static let onceDidClickEditBarGuide = "onceDidClickEditBarGuide"
static let onceDidClickEditFreestyleGuide = "onceDidClickEditFreestyleGuide"
static let onceDidClickManualCutoutGuide = "onceDidClickManualCutoutGuide"
static let onceDidClickBackgroundBlurGuide = "onceDidClickBackgroundBlurGuide"
static let onceDidTapCustomStickerBubble = "onceDidTapCustomStickerBubble"
static let onceDidRequestHomeTemplatesFromAPI = "onceDidRequestHomeTemplatesFromAPI"

static let firSaveExportTemplateKey = "firSaveExportTemplateKey"
static let firSaveTemplateDateKey = "firSaveTemplateDateKey"
static let firShareExportTemplateKey = "firShareExportTemplateKey"
static let firShareTemplateDateKey = "firShareTemplateDateKey"
}

2. 提供CURD API
private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.config").userDefaults

var exportedImageSaveCount: Int {
return appConfigUserDefaults.integer(forKey: key)
}

func increaseExportedImageSaveCount() {
let key = UserDefaultsKey.exportedImageSaveCount
var count = appConfigUserDefaults.integer(forKey: key)
count += 1
appConfigUserDefaults.setValue(count, forKey: key)
}

我们对UserDefaults数据源进行了封装,String Key的注册也统一到常量文件中。当我们要查找或修改时,可以从配置表方便的查到String Key。


随着业务的膨胀,配置项会越来越多,我们会需要根据业务功能的分类,重新整理出多个分表。


随后我们会发现一些问题:

  1. String Key的注册虽然不麻烦,但Key中无法体现出Key归属与哪个UserDefaults。

  2. CURD API的数量会膨胀的更快,需要更多的维护成本。那么能不能将配置的管理更加面向对象,实现类似ORM的方式来管理呢?


方案2.0


根据上述的问题,来演化下方案2.0,我们来创建一个协议,用来规范UserDefaults的使用类。


它将包含CURD API的默认实现,初始化关联UserDefaults,自动生成String Key。

/// UserDefaults存储协议,建议用String类型的枚举去实现该协议
public protocol UserDefaultPreference {

var userDefaults: UserDefaults { get }
var key: String { get }

var bool: Bool { get }
var int: Int { get }
var float: Float { get }
var double: Double { get }

var string: String? { get }
var stringValue: String { get }

var dictionary: [String: Any]? { get }
var dictionaryValue: [String: Any] { get }

var array: [Any]? { get }
var arrayValue: [Any] { get }

var stringArray: [String]? { get }
var stringArrayValue: [String] { get }

var data: Data? { get }
var dataValue: Data { get }

var object: Any? { get }
var url: URL? { get }

func codableObject<T: Decodable>(_ as:T.Type) -> T?

func save<T: Encodable>(codableObject: T) -> Bool

func save(string: String)
func save(object: Any?)
func save(int: Int)
func save(float: Float)
func save(double: Double)
func save(bool: Bool)
func save(url: URL?)
func remove()
}

定义完协议后,我们再添加一些默认实现,降低使用成本。

// 生成默认的String Key
public extension UserDefaultPreference where Self: RawRepresentable, Self.RawValue == String {
var key: String { return "\(type(of: self)).\(rawValue)" }
}

public extension UserDefaultPreference {
// 默认使用 standard UserDefaults,可以在实现类中配置
var userDefaults: UserDefaults { return UserDefaultsManager.shared.userDefaults }

func codableObject<T: Decodable>(_ as:T.Type) -> T? {
return UserDefaultsManager.codableObject(`as`, key: key, userDefaults: userDefaults)
}

@discardableResult
func save<T: Encodable>(codableObject: T) -> Bool {
return UserDefaultsManager.save(codableObject: codableObject, key: key, userDefaults: userDefaults)
}

var object: Any? { return userDefaults.object(forKey: key) }

func hasKey() -> Bool { return userDefaults.object(forKey: key) != nil }

var url: URL? { return userDefaults.url(forKey: key) }

var string: String? { return userDefaults.string(forKey: key) }
var stringValue: String { return string ?? "" }

var dictionary: [String: Any]? { return userDefaults.dictionary(forKey: key) }
var dictionaryValue: [String: Any] { return dictionary ?? [String: Any]() }

var array: [Any]? { return userDefaults.array(forKey: key) }
var arrayValue: [Any] { return array ?? [Any]() }

var stringArray: [String]? { return userDefaults.stringArray(forKey: key) }
var stringArrayValue: [String] { return stringArray ?? [String]() }

var data: Data? { return userDefaults.data(forKey: key) }
var dataValue: Data { return userDefaults.data(forKey: key) ?? Data() }

var bool: Bool { return userDefaults.bool(forKey: key) }
var boolValue: Bool? {
guard hasKey() else { return nil }
return bool
}

var int: Int { return userDefaults.integer(forKey: key) }
var intValue: Int? {
guard hasKey() else { return nil }
return int
}

var float: Float { return userDefaults.float(forKey: key) }
var floatValue: Float? {
guard hasKey() else { return nil }
return float
}

var double: Double { return userDefaults.double(forKey: key) }
var doubleValue: Double? {
guard hasKey() else { return nil }
return double
}

func save(object: Any?) { userDefaults.set(object, forKey: key) }
func save(string: String) { userDefaults.set(string, forKey: key) }
func save(int: Int) { userDefaults.set(int, forKey: key) }
func save(float: Float) { userDefaults.set(float, forKey: key) }
func save(double: Double) { userDefaults.set(double, forKey: key) }
func save(bool: Bool) { userDefaults.set(bool, forKey: key) }
func save(url: URL?) { userDefaults.set(url, forKey: key) }

func remove() { userDefaults.removeObject(forKey: key) }
}

OK,我们来看下使用的案例

// MARK: - Launch
enum LaunchEventKey: String {
case didShowLaunchGuideOnThisLaunch
case launchGuideIsAlreadyShow
}
extension LaunchEventKey: UserDefaultPreference { }

func checkIfNeedLaunchGuide() -> Bool {
return !LaunchEventKey.launchGuideIsAlreadyShow.bool
}
func launchContentView() {
LaunchEventKey.launchGuideIsAlreadyShow.save(bool: true)
}

// MARK: - Language
enum LanguageEventKey: String {
case appLanguageCode
}
extension LanguageEventKey: UserDefaultPreference { }

static var appLanguageCode: String {
get {
let code = LanguageEventKey.appLanguageCode.string ?? ""
return code
}
set {
LanguageEventKey.appLanguageCode.save(codableObject: newValue)
}
}

// MARK: - Purchase
enum PurchaseStatusKey: String {
case iapSubscribeExpireDate
}
extension PurchaseStatusKey: UserDefaultPreference { }

func handle() {
let expirationDate: Date = Entitlement.expirationDate
PurchaseStatusKey.iapSubscribeExpireDate.save(object: expirationDate)
}

func getValues() {
let subscribeExpireDate = PurchaseStatusKey.iapSubscribeExpireDate.object as? Date
}

// MARK: - GlobalConfig
enum AppConfig: String {
case globalConfig
}

private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.AppConfig").userDefaults

extension AppConfig: UserDefaultPreference {
var userDefaults: UserDefaults { return appConfigUserDefaults }
}

// 自定义类型
public class GlobalConfig: Codable {
/// 配置版本号
let configVersion: Int
/// 用户初始试用次数
let userInitialTrialCount: Int
/// 生成时间 如:2022-09-19T02:58:31Z
let createDate: String

enum CodingKeys: String, CodingKey {
case configVersion = "version"
case userInitialTrialCount = "user_initial_trial_count"
case createDate = "create_date"
}
...
}

lazy var globalConfig: GlobalConfig = {
guard let config = AppConfig.globalConfig.codableObject(GlobalConfig.self) else {
return GlobalConfig()
}
return config
}() {
didSet { AppConfig.globalConfig.save(codableObject: globalConfig) }
}

从上述案例可以看出,在配置项的注册和维护成本相对方案1.0有了大幅度的降低,对UserDefaults的使用进行了规范性的约束,提供了更方便的CURD API,使用方式也更加符合面向对象的习惯。


同时为了满足复杂结构体的存储需求,我们可以扩展实现Codable对象的存取逻辑。


总结


本方案的目的是解决乱象丛生的UserDefaults的使用情况,分析后向两个方向进行了优化:

  1. 提供中心化的配置方式,关联UserDefaults、维护String Key。
  2. 提供类ORM的管理方式,减少业务的接入成本。


针对更复杂的、类缓存集合的,或者有查询需求的配置项管理,请尽快用NoSQL替换,避免数据量上升带来的效率下降。


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

七夕节那天,我被裁员了

前言 先祝jym昨天七夕节快乐吧,有情人终成眷属 一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感... ...
继续阅读 »

前言


先祝jym昨天七夕节快乐吧,有情人终成眷属


一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感...




难忘的七夕


如标题所见,七夕节被通知裁员了。单身狗今天受到双重痛击。说的是公司迫于压力需要节流,部门会裁1/4。而我恰好就是那个“幸运儿”。其实从前些时间就早有风向,先是ui,到测试,终于到开发人员了,而到了开发最先动刀的果然还就是鼠鼠前端。


23届真的太惨了。在这里从实习到转正也快干了1年了,但才刚拿了一个月转正工资就遇到调整,实属难绷。


应届生身份也没了,工作年限也达不到,双重叠buff了属于是,老实说刚听到这消息的时候自己还是懵的,也就和往常一样上班怎么今天就这么突然开这样的会议呢?


而后是无奈,但是又有点兴奋,正好可以逼自己再出去外面探探,看看市场如何。

回家路上,打开手机刷了刷boss,要么招24的要么一到两年工作经验的,一下子又把我难住了。。。我怎么这么背啊5555


整个晚上我都在思考人生,我在想假如自己不是读计算机会如何?我是不是能没负担的尝试各种工作,自己真的要在程序员这条赛道一条路走到黑吗?


程序员的工作又何尝不是围墙,技术类的工作是建立起了行业壁垒,在外行人看来能将代码变成程序是一件很酷的事,而正是这道壁垒,让外行的人想进来,里面的人想走却又鼓不起勇气。




是啊 我除了写代码还能干什么呢?难道真的放弃学了这么久在外行人看来很厉害的技术去做其他工作吗?(脱不下的长衫又穿上了)


总结


我一直认为无论发生什么事,一切都是上天最好的安排,世上无所谓输赢祸福,关键是从中有所收获,写几点感悟吧:

  • 拥抱不确定性是踏入社会后最需要学会的。读书时每个人都是按部就班,除了学习剩下的挑战就是安排到日程上的考试,而步入社会每天遇到的事情都不一样,so be water my friend 随机应变 随遇而安
  • 负能量可以有,适当摆烂一下,但睡一觉起来就得调整好心态了,享受生活不要被蛋疼的事影响
  • 不要畏手畏脚,敞开自己而不是压抑自己,一辈子很短,很多事情不去尝试就来不及了
  • 灵活就业或许才是这个时代下的答案?不知道
  • 行动起来最重要,如前面说的想在掘金发文却拖延了很久,而正是这回吐槽却让我奋笔疾书了起来,命运的齿轮或许就此转动,后面我会督促自己继续更开发过程中的杂记、生活记录
  • 接下来的安排:先编辑个简历再出发,然后整理一下这一年来开发的东西,记录一下

结语


我也不知道后面会如何,走一步是一步吧,通知还没正式下来。如果读到这里的你觉得我我是个有趣的人,那么点个关注吧,这也是我更文的动力


失败并不存在,关键是故事仍在继续......


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

iOS 拖拽式控件:QiDragView

iOS
首先,我们先看一下QiDragView的效果图:  一、QiDragView整体架构设计 话不多说,上架构图~ QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。 二、如何自定义...
继续阅读 »

首先,我们先看一下QiDragView的效果图: 


一、QiDragView整体架构设计


话不多说,上架构图~



QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。


二、如何自定义使用QiDragView?


在上Demo之前,先介绍几个可以自定义的UI配置属性:



以及一些逻辑配置属性:



使用起来也很方便:

  • 直接设置titles即可创建出对应title的Buttons。
  • 通过dragSortEnded的block方法回调,处理拖拽后的业务逻辑:按钮的排序、按钮是否选择等属性

默认配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];

dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

自定义配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];
dragSortView.rowHeight = 50.0;
dragSortView.rowMargin = 30.0;
dragSortView.rowPadding = 20.0;
dragSortView.columnCount = 3;
dragSortView.columnMargin = 30.0;
dragSortView.columnPadding = 20.0;
dragSortView.normalColor = [UIColor redColor];
dragSortView.selectedColor = [UIColor purpleColor];
dragSortView.enabledTitles = @[@"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 可以点击选择的Buttons(选填,默认全选)
dragSortView.selectedTitles = @[@"首页推荐", @"HULK一线杂谈", @"Qtest之道"];//!< 初始选择的Buttons(选填,默认全选)
dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

三、QiDragView的技术点


3.1 长按手势:


长按手势分别对应三种状态:UIGestureRecognizerStateBeganUIGestureRecognizerStateChangedUIGestureRecognizerStateEnded

//! 长按手势
- (void)longPress:(UILongPressGestureRecognizer *)gesture {

UIButton *currentButton = (UIButton *)gesture.view;

if (gesture.state == UIGestureRecognizerStateBegan) {

[self bringSubviewToFront:currentButton];

[UIView animateWithDuration:.25 animations:^{
self.originButtonCenter = currentButton.center;
self.originGesturePoint = [gesture locationInView:currentButton];
currentButton.transform = CGAffineTransformScale(currentButton.transform, 1.2, 1.2);
}];
}
else if (gesture.state == UIGestureRecognizerStateEnded) {

[UIView animateWithDuration:.25 animations:^{
currentButton.center = self.originButtonCenter;
currentButton.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
if (self.dragSortEnded) {
self.dragSortEnded(self.buttons);
}
}];
}
else if (gesture.state == UIGestureRecognizerStateChanged) {

CGPoint gesturePoint = [gesture locationInView:currentButton];
CGFloat deltaX = gesturePoint.x - _originGesturePoint.x;
CGFloat deltaY = gesturePoint.y - _originGesturePoint.y;
currentButton.center = CGPointMake(currentButton.center.x + deltaX, currentButton.center.y + deltaY);

NSInteger fromIndex = currentButton.tag;
NSInteger toIndex = [self toIndexWithCurrentButton:currentButton];

if (toIndex >= 0) {
currentButton.tag = toIndex;

if (toIndex > fromIndex) {
for (NSInteger i = fromIndex; i < toIndex; i++) {
UIButton *nextButton = _buttons[i + 1];
CGPoint tempPoint = nextButton.center;
[UIView animateWithDuration:.5 animations:^{
nextButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
nextButton.tag = i;
}
}
else if (toIndex < fromIndex) {
for (NSInteger i = fromIndex; i > toIndex; i--) {
UIButton *previousButton = self.buttons[i - 1];
CGPoint tempPoint = previousButton.center;
[UIView animateWithDuration:.5 animations:^{
previousButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
previousButton.tag = i;
}
}
[_buttons sortUsingComparator:^NSComparisonResult(UIButton *obj1, UIButton *obj2) {
return obj1.tag > obj2.tag;
}];
}
}
}

3.2 配置按钮:


设计思路:在属性titles的setter方法中,初始化并配置好各个Buttons。

- (void)setTitles:(NSArray<NSString *> *)titles {

_titles = titles;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSInteger differCount = titles.count - self.buttons.count;

if (differCount > 0) {
for (NSInteger i = self.buttons.count; i < titles.count; i++) {
[self.buttons addObject:[self buttonWithTag:i]];
}
}
else if (differCount < 0) {
NSArray *extraButtons = [self.buttons subarrayWithRange:(NSRange){titles.count, self.buttons.count - titles.count}];
[self.buttons removeObjectsInArray:extraButtons];
for (UIButton *button in extraButtons) {
[button removeFromSuperview];
}
}

self.enabledTitles = self.enabledTitles ?: titles;//!< 如果有,就传入,否则传入titles
self.selectedTitles = self.selectedTitles ?: titles;

for (NSInteger i = 0; i < self.buttons.count; i++) {
[self.buttons[i] setTitle:titles[i] forState:UIControlStateNormal];
[self.buttons[i] addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)]];//!< 长按手势
[self selectButton:self.buttons[i] forStatus:[self.selectedTitles containsObject:titles[i]]];
if ([self.enabledTitles containsObject:titles[i]]) {
[self.buttons[i] addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
}

for (NSInteger i = 0; i < self.buttons.count; i++) {
NSInteger rowIndex = i / self.columnCount;
NSInteger columnIndex = i % self.columnCount;
CGFloat buttonWidth = (self.bounds.size.width - self.columnMargin * 2 - self.columnPadding * (self.columnCount - 1)) / self.columnCount;
CGFloat buttonX = self.columnMargin + columnIndex * (buttonWidth + self.columnPadding);
CGFloat buttonY = self.rowMargin + rowIndex * (self.rowHeight + self.rowPadding);
self.buttons[i].frame = CGRectMake(buttonX, buttonY, buttonWidth, self.rowHeight);
}

CGRect frame = self.frame;
NSInteger rowCount = ceilf((CGFloat)self.buttons.count / (CGFloat)self.columnCount);
frame.size.height = self.rowMargin * 2 + self.rowHeight * rowCount + self.rowPadding * (rowCount - 1);
self.frame = frame;
});
}

源码地址:QiDragView


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