注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

pnpm改造替换npm

Q: 为什么要迁移pnpm? 相比于npm,pnpm有一些优势:更快的安装速度: 在安装包时,pnpm使用了硬链接的方式,将已安装的包链接到新的目录下,而不是复制或下载包。这样,当你安装一个包的不同版本或者不同项目使用同一个包时,它们会共享已经安装的包,减少了...
继续阅读 »

Q: 为什么要迁移pnpm?


相比于npm,pnpm有一些优势:

  1. 更快的安装速度: 在安装包时,pnpm使用了硬链接的方式,将已安装的包链接到新的目录下,而不是复制或下载包。这样,当你安装一个包的不同版本或者不同项目使用同一个包时,它们会共享已经安装的包,减少了磁盘空间的占用,同时也加速了安装的速度。

  2. 更少的磁盘空间占用: 由于pnpm使用硬链接的方式共享已安装的包,因此相比于npm,pnpm占用更少的磁盘空间。

  3. 更好的本地缓存: pnpm会缓存包的元数据和二进制文件到本地缓存中,这样再次安装相同的包时,会从本地缓存中读取,而不是重新下载。这样可以提高安装包的速度,并减少网络带宽的消耗。

  4. 更好的多项目管理: pnpm可以管理多个项目的依赖,可以将相同的依赖安装在一个公共的位置,减少磁盘空间的占用,并且可以快速地切换项目之间的依赖关系。

  5. 更好的可重复性: pnpm使用了锁文件来保证安装包的版本一致性,同时也支持自定义的锁文件名称和路径。这样可以确保项目在不同的环境中的安装结果一致,增强了可重复性。


需要注意的是,pnpm相比于npm也存在一些缺点,例如兼容性问题、社区支持不如npm等。因此,在选择使用pnpm还是npm时,需要根据自己的实际需求和项目情况进行权衡。


Q: 上面提到的硬链接和符号链接是什么?


硬链接和符号链接都是文件系统中的链接方式,它们的作用是可以将一个文件或目录链接到另一个文件或目录上,从而实现共享或复制等功能。下面我来简单介绍一下它们的区别和示例。


硬链接


硬链接是指在文件系统中,将一个文件名链接到另一个文件上,使它们指向同一个物理数据块,也就是说,这两个文件名共享同一个inode节点。硬链接的本质是将一个文件名指向一个已存在的文件。


硬链接的特点:

  • 硬链接不能跨越不同的文件系统,因为inode节点只存在于一个文件系统中。
  • 硬链接可以看作是原文件的一个副本,它们的文件权限、拥有者、修改时间等都是相同的。
  • 删除硬链接并不会删除原文件,只有当所有的硬链接都被删除后,原文件才会被真正删除。

下面是一个硬链接的示例:

$ touch file1 # 创建一个文件
$ ln file1 file2 # 创建硬链接
$ ls -li file* # 查看文件inode节点
12345 -rw-r--r-- 2 user user 0 Apr 26 10:00 file1
12345 -rw-r--r-- 2 user user 0 Apr 26 10:00 file2

可以看到,file1和file2的inode节点是相同的,说明它们共享同一个物理数据块。


符号链接


也称之为软链接,符号链接是指在文件系统中,创建一个特殊的文件,其中包含了另一个文件的路径,通过这个特殊文件来链接到目标文件。符号链接的本质是将一个文件名指向一个路径。


符号链接的特点:

  • 符号链接可以跨越不同的文件系统,因为它们只是一个指向文件或目录的路径。
  • 符号链接指向的是目标文件或目录的路径,而不是inode节点,因此,目标文件或目录的属性信息可以独立于符号链接存在。
  • 删除符号链接不会影响目标文件或目录,也不会删除它们。

下面是一个符号链接的示例:

$ touch file1 # 创建一个文件
$ ln -s file1 file2 # 创建符号链接
$ ls -li file* # 查看文件inode节点
12345 -rw-r--r-- 1 user user 0 Apr 26 10:00 file1
67890 lrwxr-xr-x 1 user user 5 Apr 26 10:01 file2 -> file1

可以看到,file2是一个符号链接文件,它的inode节点和file1不同,而是一个指向file1的路径。


Q: 看到一些文章里说pnpm走的是硬链接,有的说用了软连接。到底走的是什么?


其实,pnpm是软连接和硬链接都用了。可以这么理解,pnpm在机器上某个地方存放安装好的所有依赖包,这些依赖包是独立于我们代码仓库的,这也是前面说的pnpm在安装速度和磁盘空间占用上的优点。而我们的代码库确实是先通过硬链接的方式来建立代码库和已安装过的依赖包之间的共享关系。可以打开代码库看到node_modules下有一个.pnpm文件夹,里面放的就是当前代码库建立的硬链接。




.pnpm下的文件都是一些名字很长的,长这样:




这里不用关心具体是什么,我们需要关心的是node_mpdules下我们认识的npm依赖包,它们正是通过软连接的方式来链接到.pnpm下的这些依赖包的。在vscode下,可以明显看到npm包后面的软连接标识:




如果想看一下这些软连接到底指向哪里的,可以:

# 进入node_modules目录
cd node_modules

# 枚举文件列表
ll 

 可以看到,这就是node_modules下软链接到.pnpm下的。


Q: 这个模式跟npm dedupe是不是很相似,有什么不同?


pnpm的硬链接模式和npm的dedupe功能是类似的,都是通过共享已安装的包来减少磁盘空间的占用,同时也可以提高安装包的速度。但它们之间还是存在一些不同:

  1. 原理不同: pnpm使用硬链接的方式共享已安装的包,而npm使用的是符号链接的方式共享已安装的包。硬链接是文件系统的一种特殊链接,它可以将一个文件链接到另一个文件上,使它们共享相同的内容。符号链接则是一个指向另一个文件或目录的特殊文件。

  2. 适用范围不同: pnpm的硬链接模式可以在多个项目之间共享已安装的包,而npm的dedupe功能只能在单个项目内共享已安装的包。

  3. 优势不同: pnpm的硬链接模式可以减少磁盘空间的占用和提高安装包的速度,而npm的dedupe功能只能减少磁盘空间的占用。

  4. 实现方式不同: pnpm使用了自己的包管理器和包存储库,而npm使用了公共的包管理器和包存储库。这也是导致它们之间存在差异的一个重要原因。


需要注意的是,无论是使用pnpm的硬链接模式还是npm的dedupe功能,都需要谨慎使用,以避免出现意外的错误。特别是在使用硬链接模式时,如果多个项目共享同一个包,需要注意不要在一个项目中修改了该包的文件,导致其他项目也受到影响。


Q: pnpm对于node版本有要求吗?


pnpm有对node版本的要求。官方文档中列出的最低支持版本是Node.js 10.x,推荐使用的版本是Node.js 14.x。如果使用的是较旧的Node.js版本,可能会导致安装和使用pnpm时出现错误。


我这里本来用的是Node14.x。因为其他原因,本次也给Node升级到16.x了。


Q: pnpm有类似npm ci的命令吗?



补充:npm ci主要是用于刚刚在download了一个仓库后,还没有node_modules的时候让npm完全根据package.json和package-lock.json的规范来install依赖包。相比较于直接走npm inpm ci会带来更精确的小版本版本号控制,因为npm i对于一些"^1.0.2"这样的版本号,可能会按照1.x.x这样的规范给你无感升级了,造成和之前某些包版本号之间的差异。
但是当本地已有node_modules的时候,就没办法用npm ci命令了。



是的,pnpm也有类似 npm ci 命令的功能,可以使用 pnpm install --frozen-lockfile 命令实现。它会根据 package-lock.jsonpnpm-lock.yaml 确定依赖关系,并且在安装期间不会更新任何包。此命令类似于 npm ciyarn install --frozen-lockfile 命令。


Q: pnpm@7搭配husky@8后commit一直失败怎么办?


这是因为hooks出问题了。某些代码库里会在commit时候会添加一些hook用来处理commit相关的事务,比如生成commit-id之类的。


husky@8后需要处理一下这个:

husky add .husky/commit-msg 'sh .git/hooks/commit-msg "$@"'

手动把之前.git/hooks下的脚本拷贝到.husky下。


友情提示:.git和.husky一般都是在项目根目录下的隐藏文件夹喲~


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

Volatile 关键字

保证内存可见性 Java 内存模型分为了主内存和工作内存两部分,其规定程序所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(赋值、读取等)都必须在工作内存中进行,而不能直接读...
继续阅读 »

保证内存可见性


Java 内存模型分为了主内存和工作内存两部分,其规定程序所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(赋值、读取等)都必须在工作内存中进行,而不能直接读取主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递都必须经过主内存的传递来完成。 



这样就会存在一个情况,工作内存值改变后到主内存更新一定是需要一定时间的,所以可能会出现多个线程操作同一个变量的时候出现取到的值还是未更新前的值。


这样的情况我们通常称之为「可见性」,而我们加上 volatile 关键字修饰的变量就可以保证对所有线程的可见性。


这里的可见性是什么意思呢?当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。


为什么 volatile 关键字可以有这样的特性?这得益于 Java 语言的先行发生原则(happens-before)。简单地说,就是先执行的事件就应该先得到结果。


但是! volatile 并不能保证并发下的安全。


Java 里面的运算并非原子操作,比如 i++ 这样的代码,实际上,它包含了 3 个独立的操作:读取 i 的值,将值加 1,然后将计算结果返回给 i。这是一个「读取-修改-写入」的操作序列,并且其结果状态依赖于之前的状态,所以在多线程环境下存在问题。



要解决自增操作在多线程下线程不安全的问题,可以选择使用 Java 提供的原子类,如 AtomicInteger 或者使用 synchronized 同步方法。


原子性:在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量)才是原子操作。(变量之间的相互赋值不是原子操作,比如 y = x,实际上是先读取 x 的值,再把读取到的值赋值给 y 写入工作内存)



禁止指令重排


最开始看到「指令重排」这个词语的时候,我也是一脸懵逼。后面看了相关书籍才知道,处理器为了提高程序效率,可能对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。


指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,却会影响到多线程的执行结果。比如下面的代码:


使用场景


从上面的总结来看,我们非常容易得出 volatile 的使用场景:

  1. 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束。

比如下面的场景,就很适合使用 volatile 来控制并发,当 shutdown() 方法调用的时候,就能保证所有线程中执行的 work() 立即停下来。

volatile boolean shutdownRequest;
private void shutdown(){
shutdownRequest = true;
}
private void work(){
while (!shutdownRequest){
// do something
}
}

总结


说了这么多,其实对于 volatile 我们只需要知道,它主要特性:保证可见性、禁止指令重排、解决 long 和 double 的 8 字节赋值问题。


还有一个比较重要的是:它并不能保证并发安全,不要和 synchronize 混淆。


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

💢可恶!终究还是逃不过这些网站吗?

Documatic http://www.documatic.com/ Documatic 是一个高效的搜索引擎工具,旨在帮助开发人员轻松搜索他们的代码库以查找特定的代码片段,函数,方法和其他相关信息。这个工具旨在为开发人员节省宝贵的时间并增加生产力,可以在几...
继续阅读 »

Documatic


http://www.documatic.com/


Documatic 是一个高效的搜索引擎工具,旨在帮助开发人员轻松搜索他们的代码库以查找特定的代码片段,函数,方法和其他相关信息。这个工具旨在为开发人员节省宝贵的时间并增加生产力,可以在几秒钟内快速提供准确和相关的搜索结果。Documatic 是一个代码搜索工具,具有自然语言查询功能,可以简化新手和专家开发人员的代码库搜索。输入查询后,Documatic 会快速从代码库中获取相关的代码块,使您更容易找到所需的信息。 



Transform.tools


transform.tools/


Transform.tools 是一个网站,可以转换大多数内容,如 HTML 到 JSX,JavaScript 到 JSON,CSS 到 JS 对象等等。当我需要转换任何内容时,它真的节省了我的时间。 




Convertio


convertio.co/


Convertio - 在线轻松转换文件。超过 309 种不同的文档,图像,电子表格,电子书,档案,演示文稿,音频和视频格式。比如 PNG 到 JPEG,SVG 到 PNG,PNG 到 ICO 等等。 



Removebg


http://www.remove.bg/


Removebg 是一个令人惊叹的工具,可以轻松地删除任何图像的背景。RemoveBG 可以立即检测图像的主题并删除背景,留下透明的 PNG 图像,您可以轻松地在项目中使用。无论您是否从事平面设计,图片编辑或涉及图像的任何其他项目,我使用过这个工具太多次,我甚至不记得了。




Imglarger


imglarger.com/


Imglarger 允许您将图像放大高达 800%,并增强照片而不损失质量,这对摄影师和图像处理者特别有用。它是一个一体化的 AI 工具包,可以增强和放大图像。增加图像分辨率而不损失质量。




Code Beautify


codebeautify.org/


Code Beautify 是一个在线代码美化和格式化工具,允许您美化源代码。除了此功能外,它还支持一些转换器,如图像到 base64,不仅如此,它还有如下图像所示的大量功能:




Vercel


vercel.com/


Vercel 是前端开发人员的平台,为创新者提供构建即时 Web 应用程序所需的速度和可靠性。它是一个云平台,自动化开发和部署流程来构建无服务器 Web 应用程序。它提供诸如无服务器功能,静态站点托管,持续部署,自定义域名和 SSL 以及团队协作等功能。它有免费层和付费计划以获得更高级功能,并被许多流行的网站和 Web 应用程序使用。




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

一个28岁程序员入行自述和感受

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。 自我叙述 我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,...
继续阅读 »

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。


自我叙述


我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,我不可能如此普通,自己的一生不应该泯然众生,平凡平庸的度过。尤其是干我们it这一行业的,都有一个自己的程序员梦,梦想着,真的能够用 “代码改变世界”


入行回顾



你们还记得自己是什么时候,入行it行业的吗



我今年已经28岁了,想起来入行,还挺久远的,应该是2016入行的,我也算是半路出家的,中间有过武术梦 歌唱梦 但是电脑什么上学那会就喜欢玩,当然是指游戏,




武术梦




来讲讲我得第一个·梦,武术梦,可能是从小受到武打演员动作电视剧的影响,尤其那个时候,成龙大哥的电影,一直再放,我觉得学武术是很酷的一件事情,尤其那会上小学,还是初中我的体育还是非常好的,


然后我们家那个时候电视还是黑白的,电视机。哈哈哈😀电视台就那么几个,放来放去,有一个台一直重复放成龙电影,还有广告, 都是 学武术就到 xxxx学校, 我被洗脑了吧


于是真的让我爸,打电话质询了一下,可是好像他们这种武术学校都是托管式的,封闭式学习,听说很苦,,,,当然这不是重点,重点每年学费非常的贵,en~,于是乎我的这个梦想终止了,。。




歌唱梦




为啥会有唱歌想法,你猜对了,是被那个时候的好声音给影响了,那个时候好声音是真的很火,看的时候我一度以为我也可以上好声音,去当歌手然后出道,当明星,什么的。


不过不经历打击,怎么会知道自己的下线在哪里呢


我小学换了两到三个学校,到初中,再到高中,你们还记得自己读高中那会吗,高中是有专业选择的,入学军训完以后。


我们代班主任,和我们说有三个专业方向可以选择,艺术类,分美术,和唱歌,然后是文化类,然后艺术类就业考大学分数会低很多,然后一系列原因,哈哈哈,我就选择了歌唱班。


我最好伙伴他选择了,美术类就是素描。这里我挺后悔没有选择 美术类。


到了歌唱班,第一课就是到专业课有钢琴的教室,老是要测试每个同学的,音色和音高,音域
然后各自上台表演自己的拿手的一首歌,。我当时测试时候就是跟着老师的弹的钢琴键瞎唱,


表演的歌曲是张雨生《大海》 也就唱了高潮那么几句。。 😀现在想起来还很羞耻,那是我第一次在那么多人面前唱歌,


后面开始上课老师说我当时分班时候音色什么还不错,但学到后面,我是音准不太行,我发现。再加上我自己的从小感觉好像有点自卑敏感人格,到现在把,我唱歌,就越来越差,


当然我们也有乐理。和钢琴课,我就想主助攻乐理和钢琴,


但是我很天真


乐理很难学习,都是文科知识需要背诵,但是他也要有视唱,也就是唱谱子,duo,re,mi,fa,suo,la,xi,duo。。等,我发现我也学不进去


后面我又开始去学钢琴,但是钢琴好像需要一定童子功,不然可能很难学出来,于是我每天早上6点钟起来,晚上吃完饭就去钢琴教师抢占位置, 还得把门堵着怕人笑话,打扰我,


结果你们也猜到了,音乐方面天赋很重要,然后就是性格上面表演上面,要放得开,可是我第一年勉强撑过去了,后面第二年,专业课越来越多了,我感觉我越来越自卑~,然后成绩就越来越差,老师也就没太重视,嗯~好不容撑到了第二年下半年,放暑假,


但是老师布置任务暑假要自己去外面练钢琴,来了之后要考试,我还花钱去外面上了声乐课钢琴课,哎,我感觉就是浪费钱,,,,,因为没什么效果,性格缺陷加上天赋不行,基本没效果,那段时间我也很痛苦的,因为越来越感觉根本容入不进去班级体,尤其是后面高二,了专业课很多大部分是前面老师带着发生开嗓,后面自由练习,我也不好意思,不想练习,所以
到后面,高二下学习我就转学了,,,,


当然我们班转学的,不止我一个,还有一个转学的 和我一个寝室的,他是因为音高上不去,转到了文科班, 还有一个是挺有天赋,我挺羡慕的,但是人家挺喜欢学习,不喜欢唱歌什么,就申请转到了,文科班。 不过她转到文科班,没多久也不太好,后面好像退学了,,我一直想打听他的消息,都在也没打听到了




玩电脑




我对电脑的组装非常感兴趣,喜欢研究电脑系统怎么装,笔记本拆装,台式机拆装,我会拿我自己的的笔记本来做实验,自己给自己配台式机,自己给自己笔记本增加配置,哈哈哈哈。对这些都爱不释手。



这还是我很早时候,自己一点一点比价,然后去那种太平洋电脑城,电脑一条街,那种地去找人配置的。想想那时候配置这个电脑还挺激动,这是人生的第一台自己全部从零开始组装配的电脑,


本来打算,后面去电脑城上班,开一个笔记本维修,电脑装配的门面的,(因为自己研究了很多笔记本系统,电脑组装),可是好像听电脑城的人说,电脑组装什么的已经不赚钱了,没什么价格利润,都是透明的而且更新迭代非常的快,电脑城这种店铺也越来越少了,都不干了,没有新人再去干这个了,于是乎我的第一份工作失业 半道崩殂了,哈哈哈哈还没有开始就结束了。




学it




后面我又报名自学了,it编程,《xxx鸟》 但是学it我学起来,好像挺快的,挺有感觉的,入学前一个星期,要等班人数到齐才能开班,我们先来的就自己学习打字了,我每天都和寝室人,一起去打字,我感觉那段时间我过得挺开心和充实的,


后面我们觉得自带寝室不好,环境差,于是就几个人一起,搬出去住了,一起学习时候有一个年级26了,我和他关系还蛮好的,不过现在也没什么联系了,,,


学习时候,每次做项目时候我都是组长,那个时候原来是有成就感的,嗯,学习it好像改变了,我学唱歌那个时候,一些自卑性格,可能是遇到了一个好的老师吧


当然后面就顺利毕业,然后找到了工作了,,,


直到现在我还在it行业里


嗯~还想往下面写一点什么,,,下一篇分享一下我入门感受和经历吧


关注公众号,程序员三时 希望给你带来一点启发和帮助


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

移动端页面加载耗时监控方案

iOS
本文阐述了个人对移动端页面加载耗时监控的一些理解,主要从:节点划分及对应的实现方案,线上监控注意点,后续还能做的事 三个方面来和大家分享。 前言 移动端的页面加载速度,作为最为影响用户体验的因素之一,是我们做移动端性能优化的重点方向之一。 而优化的效果体现,...
继续阅读 »

本文阐述了个人对移动端页面加载耗时监控的一些理解,主要从:节点划分及对应的实现方案,线上监控注意点,后续还能做的事 三个方面来和大家分享。



前言


移动端的页面加载速度,作为最为影响用户体验的因素之一,是我们做移动端性能优化的重点方向之一。


而优化的效果体现,需要置信的指标进行衡量(常见方法论:寻找方向->确定指标->实践->量化收益),而本文想要分享的就是:如何真实、完整、方便的获得页面加载时间,并会向线上监控环节,有一定延伸。


本文的示例代码都是OC(因为Java和kotlin我也不会😅),但相关思路和方案也适用于Android(Android端已实现并上线)。


页面加载耗时


常见方案


页面加载时长是一直以来大家都在攻坚的方向,所以市面上也有非常非常多的度量方案,从节点划分角度看:


较为基础的:ViewController 的 init -> viewDidLoad -> viewDidAppear


更进一步的:ViewController 的 init -> viewDidLoad -> viewDidAppear -> user Interactable


主流方案:ViewController 的 init -> viewDidLoad -> viewDidAppear -> view render completed -> user Interactable


还有什么地方可以改进的吗?


对于这些成熟方案,我还有什么可以更进一步的吗?主要总结为以下几个方面吧:

  • 完整反映用户体感

我们做性能优化,归根结底,更是用户体验优化,在满足功能需要的同时,不影响用户的使用体验。
所以,我个人认为,大多数的性能指标,都要考虑到用户体验这个方向;页面启动速度这一块,更是如此;而传统的方案,能够完整的反应用户体感吗?
我觉得还是有一部分的缺失的:用户主动发起交互到ViewController这个阶段。这一部分有什么呢,不就是直接tap触发的action里vc就初始化了吗?
实际在一些较为复杂、大型的项目中,并不然,中间可能会有很多其他处理,例如:方法hook、路由调度、参数解析、containerVC的初始化、动态库加载等等。这一部分的耗时,实际上也是用户体感的一部分,而这一部分的耗时,如果不加监控的话,也会对整体耗时产生劣化。(这里可能会有小伙伴问了,这些东西,不应该由各自负责的同学,例如负责路由的同学,自行监控吗?这里我想阐述的一个观点时,时长类的监控,如果由几个时间段拼接,相比于endTime - startTime,难免会产生gap,即,加入endTime = 10,startTime = 0,那么中间分成两段,很有可能endTime2 = 10,startTime2 = 6;endTime1 = 4,startTime1 = 0,造成总时长不准。总而言之,还是希望得到一个能够完整反映用户体感的时长。)

  • 数据采集与业务解耦

这一点其实市面上的很多方案已经做得很好了。解耦,一方面是为了,提效:避免后续有新的页面需要监控时,需要进行新的开发;另一方面,也是避免业务迭代对于监控数据的影响:如果是手动侵入性埋点,很难保证后续新增的耗时任务对监控数据不产生影响。
而本文方案,不需要在业务代码中插入任何代码,大都是通过方法hook来实现数据采集的;而对范围、以及匹配关系等的控制,也都是通过配置来完成的。


具体实现


节点确定&数据采集方式



根据一个页面(ViewController)的加载过程中,开发主要进行的处理,以及可能对用户体感产生影响的因素,将页面加载过程划分为如上图所示的11个节点,具体解释及实现方案如下:


1. 用户行为触发页面跳转

由于页面的跳转一般是通过用户点击、滑动等行为触发的,因此这里监听用户触摸屏幕的时间点;但有效节点仅为VC在初始化前的最后一次点击/交互。


具体实现
hook UIWidow 的 sendEvent:方法,在swizzle方法内记录信息;为了性能考虑,目前仅记录一个uint64_t的时间戳,且仅内存写;
注意这里需要记录手指抬起的时间,即 touch.phase == UITouchPhaseEnded,因为一般action被调用的时机就是此时;
同时,为了适配各种行为触发的新页面出现,还增加了一个手动添加该节点的方法,使一些较复杂且不通用,业务特性较强的初始化场景,也能够有该节点数据,且不依赖hook;但注意该手动方法为侵入式数据采集方式。


2. ViewController的初始化

具体实现:hook UIViewController或你的VC基类 的 - (instancetype)init 的方法;


3. 本地UI初始化

不依赖于网络数据的UI开始初始化。


这个节点,我实际上并没有在本次实现,这里的一个理想态是:将这部分行为(即UI初始化的代码),通过协议的方式,约束到指定方法中;例如,架构层面约束一个setupSubviews的接口,回调给各业务VC,供其进行基础UI绘制(目前这种方式再一些更复杂的业务场景下实现并运行较好);有这个基础约束的前提下,才能准确的采集我理想中该节点的耗时。而我目前所负责的模块,并没有这种强约束,而又不能简单的去认为所有基础UI都是在viewDidLoad中去完成的。因此需要 对原有架构的一定修改 或 能够保证所有基础UI行为都在viewDidLoad中实现,才能够实现该节点数据的准确采集。
因此2 ~ 3和3 ~ 4间的耗时,被融合为了一段2 ~ 4的耗时。


4. 本地UI初始化完成

不依赖于网络数据的UI初始化完成。


具体实现:监听主线程的闲时状态,VC初始化 节点后的首个闲时状态表示 本地UI初始化完成;(闲时状态即runloop进入kCFRunLoopBeforeWaiting


5. 发起网络请求

调用网络SDK的时间点。


这里描述的就是上面的节点划分图的第二条线,因为两条线的节点间没有强制的线性关系,虽然图中当前节点是放在了VC初始化平行的位置,但实际上,有些实现会在VC初始化之前就发起网络请求,进行预加载,这种情况在实现的时候也是需要兼容的。


具体实现:hook 业务调用网络SDK发起请求方法的api;这里的网络库各家实现方案就可能有较大差异了,根据自身情况实现即可。


6. 网络SDK回调

网络SDK的回调触发的时间点。


具体实现:hook 网络SDK向业务层回调的api;差异性同5。


7. send request

8. receive response

真正 发出网络请求 和 收到response 的时间点,用于计算真正的网络层耗时。
这俩和5、6是不是重复了啊?并不然,因为,网络库在接收到发起网络请求的请求后,实际上在端阶段,还会进行很多处理,例如公参的处理、签名、验签、json2Model等,都会产生耗时;而真正离开了端,在网上逛荡那一段,更是几乎“完全不可控”的状态。所以,分开来统计:端部分 和 网络阶段,才能够为后续的优化提供数据基础,这也是数据监控的意义所在


具体实现
实际上系统网络api中就有对网络层详细性能数据的收集

- (void)URLSession:(NSURLSession *)session 
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;

根据官方文档中的描述

 

可以发现,我们实际上需要的时长就是从 fetchStartDateresponseEndDate 间的时间。
因此可以该delegate,获取这两个时间点。


9. 详细UI初始化

详细UI指,依赖于网络接口数据的UI,这部分UI渲染完成才是页面达到对用户可见的状态。


具体实现:这里我们认为从网络SDK触发回调时,即开始进行详细UI的渲染,因此该节点和节点6是同一个节点。


10. 详细UI渲染完成

页面对用户来说,真正达到可见状态的节点。


具体实现
对于一个常规的App页面来说,如何定义一个页面是否真正渲染完成了呢?


被有效的视图铺满


什么是有效视图呢?视频,图片,文字,按钮,cell,能向用户传递信息,或者产生交互的view;
铺满,并不是指完全铺满,而是这些有效视图填充到一定比例即可,因为按照正常的视觉设计和交互体验,都不会让整个屏幕的每一个像素点都充满信息或具备交互能力;而这个比例,则是根据业务的不同而不同的。
下面则是上述逻辑的实现思路:


确定有效视图的具体类
UITextView 
UITextField
UIButton
UILabel
UIImageView
UITableViewCell
UICollectionViewCell

主流方案中比较常见的,是前几种类,并不包括最后的两个cell;而这里为什么将cell也作为有效视图类呢?
首先,出于业务特征考虑,目前应用该套监控方案的页面,主要是以卡片列表样式呈现的;而且个人认为,市面上很多App的页面也都是列表形式来呈现内容的;当然,如果业务特征并不相符,例如全屏的视频播放页,就可以不这样处理。
其次,将cell作为有效视图,确实能够极大的降低每次计算覆盖率的耗时的。性能监控本身产生的性能消耗,是性能方向一直以来需要着重关注的点,毕竟你一个为了性能优化服务的工具,反而带来了不小的劣化,怎样也说不太过去啊😂~
我也测试了是否包含cell对计算耗时的影响:
下表中为,在一个层级较为复杂的业务页面,页面完全渲染完成之后,完成一次覆盖率达到阈值的扫描所需的时长。






















有效视图包含 cell不包含 cell
检测一次覆盖率耗时(ms)1~515~18
耗时减少15ms/次(83%)

而且,有效视图的类,建议支持在线配置,也可以是一些自定义类。


将cell作为有效视图,大家可能会产生一个新的顾虑:占位cell的情况,再具体点,就是常见的骨架图怎么办?骨架图是什么,就是在网络请求未返回的时候,用缓存的data或者模拟样式,渲染出一个包含大致结构,但不包含具体内容的页面状态,例如这种:





这种情况下,cell已经铺满了屏幕,但实际上并未完成渲染。这里就要依赖于节点的前后顺序了,详细UI是依赖于网络数据的,而骨架图是在网络返回之前绘制完成的,所以真正的覆盖率计算,是从网络数据返回开始的,因此骨架图的填充完成节点,并不会被错误统计未详细UI渲染完成的节点。
覆盖率的计算方式



如上图所示,开辟两个数组a、b,数组空间分别为屏幕长宽的像素数,并以0填充,分别代表横纵坐标;
从ViewController的view开始递归遍历他的subView,遇见有效视图时,将其frame的width和height,对应在数组a、b中的range的内存空间,都填充为1,每次遍历结束后,计算数组a、b中内容为1的比例,当达到阈值比例时,则视为可见状态。
示例代码如下:
- (void)checkPageRenderStatus:(UIView *)rootView {
if (kPhoneDeviceScreenSize.width <= 0 || kPhoneDeviceScreenSize.height <= 0) {
return;
}

memset(_screenWidthBitMap, 0, kPhoneDeviceScreenSize.width);
memset(_screenHeightBitMap, 0, kPhoneDeviceScreenSize.height);

[self recursiveCheckUIView:rootView];
}

- (void)recursiveCheckUIView:(UIView *)view {
if (_isCurrentPageLoaded) {
return;
}

if (view.hidden) {
return;
}

// 检查view是否是白名单中的实例,直接用于填充bitmap
for (Class viewClass in _whiteListViewClass) {
if ([view isKindOfClass:viewClass]) {
[self fillAndCheckScreenBitMap:view isValidView:YES];
return;
}
}

// 最后递归检查subviews
if ([[view subviews] count] > 0) {
for (UIView *subview in [view subviews]) {
[self recursiveCheckUIView:subview];
}
}
}

- (BOOL)fillAndCheckScreenBitMap:(UIView *)view isValidView:(BOOL)isValidView {

CGRect rectInWindow = [view convertRect:view.bounds toView:nil];

NSInteger widthOffsetStart = rectInWindow.origin.x;
NSInteger widthOffsetEnd = rectInWindow.origin.x + rectInWindow.size.width;
if (widthOffsetEnd <= 0 || widthOffsetStart >= _screenWidth) {
return NO;
}
if (widthOffsetStart < 0) {
widthOffsetStart = 0;
}
if (widthOffsetEnd > _screenWidth) {
widthOffsetEnd = _screenWidth;
}
if (widthOffsetEnd > widthOffsetStart) {
memset(_screenWidthBitMap + widthOffsetStart, isValidView ? 1 : 0, widthOffsetEnd - widthOffsetStart);
}

NSInteger heightOffsetStart = rectInWindow.origin.y;
NSInteger heightOffsetEnd = rectInWindow.origin.y + rectInWindow.size.height;
if (heightOffsetEnd <= 0 || heightOffsetStart >= _screenHeight) {
return NO;
}
if (heightOffsetStart < 0) {
heightOffsetStart = 0;
}
if (heightOffsetEnd > _screenHeight) {
heightOffsetEnd = _screenHeight;
}
if (heightOffsetEnd > heightOffsetStart) {
memset(_screenHeightBitMap + heightOffsetStart, isValidView ? 1 : 0, heightOffsetEnd - heightOffsetStart);
}

NSUInteger widthP = 0;
NSUInteger heightP = 0;
for (int i=0; i< _screenWidth; i++) {
widthP += _screenWidthBitMap[i];
}
for (int i=0; i< _screenHeight; i++) {
heightP += _screenHeightBitMap[i];
}

if (widthP > _screenWidth * kPageLoadWidthRatio && heightP > _screenHeight * kPageLoadHeightRatio) {
_isCurrentPageLoaded = YES;
return YES;
}

return NO;
}

但是也会有极端情况(类似下图) 


无法正确反应有效视图的覆盖情况。但是出于性能考虑,并不会采用二维数组,因为w*h的量太大,遍历和计算的耗时,会有指数级的激增;而且,正常业务形态,应该不太会有类似的极端形态。


即使真的会较高频的出现类似情况,也有一套备选方案:计算有效视图的面积 占 总面积 的比例;该种方式会涉及到UI坐标系的频繁转换,耗时也会略差于当前的方式。


在某些业务场景下,例如 无/少结果情况,关于页面等,完全渲染后,也无法达到铺满阈值。
这种情况,会以用户发生交互(同 1、用户行为触发页面跳转 的获取方式)和 主线程闲时状态超过5s (可配)来做兜底,看是否属于这种状态,如果是,则相关性能数据不上报,因为此种页面对性能的消耗较正常铺满的情况要低,并不能真实的反应性能消耗、瓶颈,因此,仅正常铺满的业务场景进行监控并优化,即可。


扫描的触发时机

以帧刷新为准,因为只有每次帧刷新后,UI才会真正产生变化;出于性能考虑,不会每帧都进行扫描,每间隔x帧(x可配,默认为1),扫描一次;同时,考虑高刷屏 和 大量UI绘制时会丢帧 的情况,设置 扫描时间间隔 的上下限,即:满足 隔x帧 的前提下,如果和上次扫描的时间差小于 下限,仍不扫描;如果 某次扫描时,和上次扫描的时间间隔 大于 上限,则无论中间隔几帧,都开启一次扫描。


11. 用户可交互

用户可见之后的下一个对用户来说至关重要的节点。如果只是可见,然后就疯狂占用主线程或其他资源,造成用户的点击等交互行为,还是会被卡主,用户只能看,不能动,这个体感也是很差的;


具体实现:详细UI渲染完成 后的 首次主线程闲时状态。


监控方案


这里由于各家的基建并不相同,因此只是总结一些小的建议,可能会比较零散,大家见谅。

  1. 建议采样收集
  2. 首先,数据的采集或者其他的新增行为/方法,一定是会产生耗时的,虽然可能不多,但还是秉着尽善尽美的原则,还是能少点就少点的,所以数据的采集,包括前面的hook等等一切行为,都只是随机的面向一部分用户开放,降低影响范围; 而且,如果数据量极大,全量的数据上报,其实对数据链路本身也会产生压力、增加成本。 当前,采样的前提是基本数据量足够,不然的话,采样样本量过小,容易对统计结果产生较大波动,造成不置信的结果。

    1. 可配置

    除了基本的是否开启的开关之外,还有其他的很多的点 需要/可以/建议 使用线上配置控制。个人认为,线上配置,除了实现对逻辑的控制,更重要的一个作用,就是出现问题时及时止损。 举一些我目前使用的配置中的例子: - 有效视图类 - 渲染完成状态,横纵坐标的填充百分比阈值 - 终态的兜底阈值 - VC的类名、对应的网络请求 等等。

    1. 本地异常数据过滤

    由于我们的样本数据量会非常大,所以对于异常数据我们不需要“手软”,我们需要有一套本地异常数据过滤的机制,来保证上报的数据都是符合要求的;不然我们后续统计处理的时候,也会因此出现新的问题需要解决。


后续还能做的事


这一部分,是对后续可实现方案的一个美好畅想~


1)页面可见态的终点,不只是覆盖率

其实,实际业务场景中,很多cell,即使绘制完,并渲染到屏幕上,此时,用户可见的也没有达到我们真正希望用户可见的状态,很多内容,都还是一个placeholder的状态。例如,通过url加载的image,我们一般都是先把他的size算好,把他的位置留好,cell渲染完就直接展示了;再进一步,如果是一个视频的播放卡片,即使网络图片加载好了,还要等待视频帧的返回,才能真正达到这张卡片的业务终态\color{red}{业务终态}(求教这里标红后如何能够让字体大小一致)。


这个非常后置,而且我们端上可能也影响不了什么的节点,采集起来有意义吗?


我觉得这是一个非常有价值的节点。一直都在说“技术反哺业务”,那么业务想要用户真正看到的那个终态,就是很重要的一环;因此,用户能在什么时间点看到,从业务角度说,能够影响其后续的方案设计(表现形式),完善用户体感对业务指标的影响;从技术角度说,可以感知真实的全链路的表现(不只是端),从而有针对性的进行优化。


如何获取到所有的业务终态呢?


这里一定是和业务有所耦合的,因为每个业务的终态,只有业务自身才知道;但是我们还是要尽量降低耦合度。
这里可以用协议的方式,为各个业务增加一个达到终态的标识,那么在某个业务达到终态之后,设置该标识即可,这里就是唯一对业务的侵入了;然后和计算覆盖率类似,这里的遍历,是业务维度(这里想象为卡片更好理解一点),只有全部业务的标识都ready之后,才是真正达到业务上的终态。


2)性能指标 关联 业务行为

其实,现在性能监控,各类平台,各个团队,或多或少的都在做,我相信,性能数据采集的代码,在工程中,也不仅仅只有一份;这个现状,在很多成一定规模的互联网公司中都可能存在。


而如果您和我一样,作为一个业务团队,如何在不重复造轮子的情况下,夹缝中求生存呢?


我个人目前的理解:将 性能表现 与 业务场景 相关联。


帧率、启动耗时、CPU、内存等等,这些性能指标数据的获取,在业界都有非常成熟的方案,而且我们的工程里,一定也有相关的代码;而我们能做的,仅仅是,调一下人家的api,然后把数据再自己上传一份(甚至有的连上传都包含了),就完事了吗?


这样我觉得并不能体现出我们自建监控的价值。个人理解,监控的意义在于:暴露问题 + 辅助定位问题 + 验证问题的解决效果


所以我们作为业务团队,将 性能数据 和 我们的业务做了什么 bind 到一起了,是不是就能一定程度上完成了上面的目的呢?


我们可以明确,我们什么样的业务行为,会影响我们的性能数据,也就是影响我们的用户基础体验。这样,不仅会帮助我们定位问题的原因,甚至会影响产品侧的一些产品能力设计方案。


完成这些建设之后,可能我们的监控就可以变成这样,甚至更好的状态: 



3)完善全链路对性能表现的关注

性能数据的关注、监控,不应该仅仅在线上阶段,开发期 → 测试期 → 线上,全链路各个环节都应该具有。

  • 目前各家都比较关注线上监控,相信都已经较为完善;

  • 测试期的业务流程性能脚本;对于测试的性能测试方案,开放应该参与共建或者有一定程度的参与,这样才能从一定程度上保证数据的准确性,以及双方性能数据的相互认可;

  • 开发期,目前能够提供展示实时CPU、FPS、内存数据的基础能力的工具很常见,也比较容易实现;但实际上,在日常开发的过程中,很难让RD同时关注需求情况与性能数据表现。因此,还是需要一些工具来辅助:例如,我们可以对某些性能指标,设置一些阈值,当日常开发中,超过阈值时,则弹窗提醒RD确认是否原因、是否需要优化,例如,详细UI绘制阶段的耗时阈值是800ms,如果某位同学在进行变更后,实际绘制耗时多次超越该值,则弹窗提醒。


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

设计模式-01.简单工厂方法

iOS
这是我尝试写的第一篇文章,以软件开发的设计模式开始,记录一下自己的理解与心得,方便以后回过头来查看。以简单工厂开始: 什么是简单工厂? 简单工厂模式(Simple Factory Pattern)是一种创建型设计模式,它提供了一种简单的方法来创建对象,而不需...
继续阅读 »

这是我尝试写的第一篇文章,以软件开发的设计模式开始,记录一下自己的理解与心得,方便以后回过头来查看。以简单工厂开始:


什么是简单工厂?



简单工厂模式(Simple Factory Pattern)是一种创建型设计模式,它提供了一种简单的方法来创建对象,而不需要直接暴露对象的创建逻辑给客户端。



UML 类图


以计算器为例子,拥有加减乘除功能,画出类图:



具体示例

// 运算符接口
protocol Operation {
    var numberA: Double { set get }
    var numberB: Double { setget }
    func calculate() -> Double
}

// 加法运算类
struct OperationAdd: Operation {
    var numberA: Double = 0.0
    var numberB: Double = 0.0
    func calculate() -> Double {
        return numberA + numberB
    }
}

// 减法运算类
struct OperationSub: Operation {
    var numberA: Double = 0.0
    var numberB: Double = 0.0
    func calculate() -> Double {
        return numberA - numberB
    }
}

// 乘法运算类
struct OperationMul: Operation {
    var numberA: Double = 0.0
    var numberB: Double = 0.0
    func calculate() -> Double {
        return numberA * numberB
    }
}

// 除法运算类
struct OperationDiv: Operation {
    var numberA: Double = 0.0
    var numberB: Double = 0.0
    func calculate() -> Double {
        if numberB != 0 {
            return numberA / numberB
        }
        return 0
    }
}

// 简单工厂类
class OperationFactory {
    static func createOperate(_ operate: String) -> Operation? {
        switch operate {
        case "+":
            return OperationAdd()
        case "-":
            return OperationSub()
        case** "*":
            return OperationMul()
        case "/":
            return OperationDiv()
        default: return nil
        }
    }
}

// 客户端调用
// 加法运算
var addOperation = OperationFactory.createOperate("+")
addOperation?.numberA = 1
addOperation?.numberB = 2
addOperation?.calculate()

// 减法运算
var subOperation = OperationFactory.createOperate("-")
subOperation?.numberA = 1
subOperation?.numberB = 2
subOperation?.calculate()

// 乘法运算
var mulOperation = OperationFactory.createOperate("*")
mulOperation?.numberA = 1
mulOperation?.numberB = 2
mulOperation?.calculate()

// 除法运算
var divOperation = OperationFactory.createOperate("/")
divOperation?.numberA = 1
divOperation?.numberB = 2
divOperation?.calculate()

简单工厂方法总结


优点:

  • 将对象的创建逻辑集中在工厂类中,降低了客户端的复杂度。
  • 隐藏了创建对象的细节,客户端只需要关心需要创建何种对象,无需关心对象是如何创建的。
  • 可以通过修改工厂类来轻松添加新的产品类

缺点:

  • 如果产品的类太多,会导致工厂类中的代码变得很复杂,难以维护。
  • 添加新产品时,需要修改工厂类,也就是会在OperationFactory类中新增case语句,这违背了开闭原则。

总体而言,简单工厂模式适用于创建对象的逻辑相对简单,且产品类的数量较少的场景。对于更复杂的对象创建和对象之间的依赖关系,可以考虑使用其他创建型设计模式,如工厂方法模式或抽象工厂模式。


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

SwiftUI精讲:Tabs 标签页组件的实现

iOS
大家好,我们又见面了~今天给大家带来 Tabs标签页组件在SwiftUI中的实现方式。在本文中,我依然会采用一种循序渐进的方式来进行讲解,这其实也是我的实现思路,希望能帮到需要的朋友。 在看本文之前,我强烈建议你先阅读我的上一篇文章 SwiftUI精讲:自定...
继续阅读 »

大家好,我们又见面了~今天给大家带来 Tabs标签页组件在SwiftUI中的实现方式。在本文中,我依然会采用一种循序渐进的方式来进行讲解,这其实也是我的实现思路,希望能帮到需要的朋友。



在看本文之前,我强烈建议你先阅读我的上一篇文章 SwiftUI精讲:自定义 Tabbar 组件 (包含过渡效果),因为有一些重复的知识点在上篇中已经讲过了,本文再讲的话难免会有些乏味,我希望每次写下的文章都有一些新的知识点~


1.Tabs组件的实现


我们先创建 Componets 文件夹,并在其中创建 tabs 文件,我们先简单地创建一个list,并将内容遍历渲染出来,如下所示:


1-1:大致UI的实现

import SwiftUI

struct TabItem: Identifiable {
var id:Int
var text:String
}

struct tabs: View {
let list:[TabItem]
@State var currentSelect:Int = 0
var body: some View {
ScrollView(.horizontal,showsIndicators: false) {
HStack {
ForEach(list) { tabItem in
Button{
currentSelect = tabItem.id
} label: {
HStack{
Spacer()
Text(tabItem.text)
.padding(.horizontal,12)
.fixedSize()
Spacer()
}
}
}
}
.frame(minWidth: UIScreen.main.bounds.width)
}
}
}

struct tabs_Previews: PreviewProvider {
// 创建一些测试数据
static let list = [
TabItem(id:1,text:"关注"),
TabItem(id:2,text:"推荐"),
TabItem(id:3,text:"热榜"),
TabItem(id:4,text:"头条精选"),
TabItem(id:5,text:"后端"),
TabItem(id:6,text:"前端")
]
static var previews: some View {
tabs(list: list)
}
}

这里加上 .frame(minWidth: UIScreen.main.bounds.width) 是为了保证在标签只有两三个的时候,我依然希望它们处于一个均匀布局的状态。代码运行后如图所示:



接着我们加上下划线样式,代码如下所示:

import SwiftUI

struct TabItem: Identifiable {
var id:Int
var text:String
}


struct tabs: View {
let list:[TabItem]
@State var currentSelect:Int = 1
var body: some View {
ScrollView(.horizontal,showsIndicators: false) {
HStack {
ForEach(list) { tabItem in
Button{
currentSelect = tabItem.id
} label: {
HStack{
Spacer()
Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
Spacer()
}
.background(
VStack{
if(currentSelect == tabItem.id){
Spacer()
Rectangle()
.fill(Color(hex: "#1677ff"))
.frame(height: 2)
.padding(.horizontal,12)
.cornerRadius(2)
}
}

)

}
}
}
.frame(minWidth: UIScreen.main.bounds.width)
}
}
}

struct tabs_Previews: PreviewProvider {
// 创建一些测试数据
static let list = [
TabItem(id:1,text:"关注"),
TabItem(id:2,text:"推荐"),
TabItem(id:3,text:"热榜"),
TabItem(id:4,text:"头条精选"),
TabItem(id:5,text:"后端"),
TabItem(id:6,text:"前端")
]
static var previews: some View {
tabs(list: list)
}
}

细心的朋友可能会发现,我的代码里面出现了 Color(hex: "#1677ff"),这是因为我们对Color结构进行了拓展,让它支持16进制颜色的传递,如下所示:

extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}

self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}

代码运行后的效果如图所示:



我们再对字体方面进行优化,我们希望点击后的字体颜色和大小和点击前保持不一致,我们对代码做出修改,如下所示:

HStack{
Spacer()
Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
.foregroundColor(currentSelect == tabItem.id ? Color(hex: "#1677ff") : Color(hex: "#333"))
// 新增
.font(.system(size: currentSelect == tabItem.id ? 20 : 17))
// 新增
.fontWeight(currentSelect == tabItem.id ? .bold : .regular)
Spacer()
}

更改后的效果如图所示:



好了,我们一个普通的tab组件就写完了,完结撒花。


接下来我们需要给下划线添加相应的过渡效果,类似于掘金的下划线移动过渡。如果有从事web端开发的朋友们,我们可以想一下,在web端我们是怎么实现类似的效果的?是不是要通过一些计算,然后赋值给下划线 css的 left 值,或者是 translateX 值。在SwiftUI中,我们压根不用这么麻烦,我们可以使用 matchedGeometryEffect 来轻易的做到相应的效果!


1-2:下划线过渡效果实现


我们对代码稍微修改下,详细的步骤我会在图中进行标注,如下图所示:




接着我们按下 command + R ,运行 Simulator 来查看对应的效果:




可以发现,我们其实已经取得了我们想要的效果。但是由于 tab 在激活的时候,文字对应的动画看着十分晃眼,很讨人厌。如果希望只保留下划线的过渡效果,而不要文字的过渡效果,该怎么做呢?


很简单,我们只需要添加 .animation(nil,value:UUID()) 即可,如下所示:

Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
.foregroundColor(currentSelect == tabItem.id ? Color(hex: "#1677ff") : Color(hex: "#333"))
.font(.system(size: currentSelect == tabItem.id ? 20 : 17))
.fontWeight(currentSelect == tabItem.id ? .bold : .regular)
// 新增
.animation(nil,value:UUID())

现在看起来是不是正常多了? 



1-3:自动滚动到对应位置


大致UI画得差不多了,接下来我们需要在点击比较靠后的tab时,我们希望 ScrollView 能帮我们滚动到对应的位置,我们该怎么做呢?
答案是引入 ScrollViewReader, 使用 ScrollViewProxy中的scrollTo方法,代码如下所示:

struct tabs: View {
let list:[TabItem]
@State var currentSelect:Int = 1
@Namespace var animationNamespace

var body: some View {
ScrollViewReader { scrollProxy in
ScrollView(.horizontal,showsIndicators: false) {
HStack {
ForEach(list) { tabItem in
Button{
withAnimation{
currentSelect = tabItem.id
}
} label: {
HStack{
Spacer()
Text(tabItem.text)
.padding(EdgeInsets(top: 8, leading: 12, bottom: 10, trailing: 12))
.fixedSize()
.foregroundColor(currentSelect == tabItem.id ? Color(hex: "#1677ff") : Color(hex: "#333"))
.font(.system(size: currentSelect == tabItem.id ? 20 : 17))
.fontWeight(currentSelect == tabItem.id ? .bold : .regular)
.animation(nil,value:UUID())

Spacer()
}
.background(
VStack{
if(currentSelect == tabItem.id){
Spacer()
Rectangle()
.fill(Color(hex: "#1677ff"))
.frame(height: 2)
.padding(.horizontal,12)
.cornerRadius(2)
.matchedGeometryEffect(id: "tab_line", in: animationNamespace)
}
}

)

}
}
}
.frame(minWidth: UIScreen.main.bounds.width)
}
.onChange(of: currentSelect) { newSelect in
withAnimation(.easeInOut) {
scrollProxy.scrollTo(currentSelect,anchor: .center)
}
}
}
}
}

在代码中,我们利用 scrollProxy.scrollTo 方法,轻易地实现了滚动到对应tab的位置。效果如下所示:




呜呼,目前为止,我们已经完成了一个不错的tabs组件。接下来在ContentView中,我们引入该组件。由于我们需要在父视图中知道tabs中currentSelect的变化,我们需要把子组件的 @State 改成 @Binding,同时为了避免 preview报错,我们也要做出对应的修改,如图所示:




1-4:结合TabView完成手势滑动切换


日常我们在使用tabs标签页的时候,如果需要支持用户通过手势进行切换标签页的操作,我们可以结合TabView一起使用,代码如下所示:

import SwiftUI

struct ContentView: View {
let list = [
TabItem(id:1,text:"关注"),
TabItem(id:2,text:"推荐"),
TabItem(id:3,text:"热榜"),
TabItem(id:4,text:"头条精选"),
TabItem(id:5,text:"后端"),
TabItem(id:6,text:"前端"),
]


@State var currentSelect:Int = 1
var body: some View {
VStack(spacing: 0){
tabs(list:list,currentSelect:$currentSelect)
TabView(selection:$currentSelect){
ForEach(list){tabItem in
Text(tabItem.text).tag(tabItem.id)
}
}.tabViewStyle(.page(indexDisplayMode: .never))
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

效果如下所示:



至此,我们总算是完成了一个能满足大部分需求的Tabs组件啦~


2. Tabs组件的拓展


2-1:Tabs组件的吸顶


仅仅实现一个简单的效果怎么够,这不符合笔者精讲技术的精神,我们还要结合日常的业务进行思考。比如,我现在想要在页面滚动的时候,我希望tabs组件能够自动吸顶,应该怎么去实现呢?


首先我们新建View文件夹,在其中放置一些视图组件,并在组件中,添加一些文本,如图所示:



接着我们先思考一下,如何在SwiftUI中做出一个吸顶的效果。这里我使用了 LazyVStack + Section的方式来做。但是有个问题,TabView被包裹在Section里面时,TabView的高度会丢失。我将会在 ScrollView 的外层套上 GeometryReader 来解决这个问题,以下为代码展示:

import SwiftUI

struct ContentView: View {
let list = [
TabItem(id:1,text:"关注"),
TabItem(id:2,text:"推荐"),
TabItem(id:3,text:"热榜")
]

@State var currentSelect:Int = 1
var body: some View {
NavigationView{
GeometryReader { proxy in
ScrollView{
LazyVStack(spacing: 0, pinnedViews:.sectionHeaders) {
Section(
header:tabs(list:list,currentSelect:$currentSelect)
.background(.white)
){
TabView(selection:$currentSelect){
ForEach(list){tabItem in
VStack{
switch currentSelect{
case 1:
Attention()
case 2:
Recommend()
case 3:
Hot()
default:
Text("")
}
}
.tag(tabItem.id)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(minHeight:proxy.size.height)
}
}
}
}
.navigationTitle("Tabs组件实现")
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

效果如图所示:



2-2:下拉刷新的实现


要实现下拉刷新的功能,我们可以使用ScrollView并结合.refreshable 来实现这个效果,代码如下所示:

import SwiftUI

struct ContentView: View {
let list = [
TabItem(id:1,text:"关注"),
TabItem(id:2,text:"推荐"),
TabItem(id:3,text:"热榜")
]

@State var currentSelect:Int = 1
var body: some View {
NavigationView{
GeometryReader { proxy in
ScrollView{
LazyVStack(spacing: 0, pinnedViews:.sectionHeaders) {
Section(
header:tabs(list:list,currentSelect:$currentSelect)
.background(.white)
){
TabView(selection:$currentSelect){
ForEach(list){tabItem in
ScrollView{
switch currentSelect{
case 1:
Attention()
case 2:
Recommend()
case 3:
Hot()
default:
Text("")
}
}
.tag(tabItem.id)
.refreshable {
print("触发刷新")
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(minHeight:proxy.size.height)
}
}
}
}
.navigationTitle("Tabs组件实现")
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

在这里要注意 .refreshable 是 ios15 才能使用的,使用时要考虑API的兼容性。效果如图所示:



至此,我们已经完成了一个很不错的Tabs标签页组件啦。感谢你的阅读,如有问题欢迎在评论区中进行交流~


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

离职交接,心态要好

话说今年经历了几次项目交接?主动和被动的都算! 01 实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目; 不断拉扯和管理内心情绪,避免原地裂开; 年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程; 毫无征兆的变动必然...
继续阅读 »

话说今年经历了几次项目交接?主动和被动的都算!




01



实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;


不断拉扯和管理内心情绪,避免原地裂开;


年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;


毫无征兆的变动必然会引起一系列问题,最直接的就是影响团队现有节奏进度,需要重新调整和规划;


人员的小规模变动,对部门甚至公司产生的影响是显而易见的,道理都懂;


但是从理性上思考,这个问题并非是无解的,是可以在各个团队中,进行内部消化的;


而人力减少带来的成本降低,以及确保公司的可持续,这是极具确定性的,也是核心目的;


所以感性上说,这个梦幻的职场,可能真的是"爱了";



02



如果是常规情况下的离职流程,交接并不是一件复杂的事情,因为有时间有心情来处理这事,好聚好散;


然而最骚的是,奇袭一般的裁员手段,几分钟谈话结束直接走人;


丝毫不顾及由此带来的影响,认定留下的人应该兜底相应的责任,实现无缝接坑;


当然并不是什么公司都有底气这么做的,大部分还是在裁员通知后,留有一定的时间处理交接事项;


对于交的过程是否有质量,完全看接的一方是否聪明;


从感性上分析,都已经被裁了自然要牢牢把握摸鱼的机会,根本不会在意交出的事项谁来维护,不反越防线就不错了;


而压力会直接传送后闪现到接的人正上方;



03



面对被动离职的交接,确实很难妥善处理,情绪化容易导致事情变质,能真正理性对待的并不多;


交接涉及到三方的核心利益:公司、交出人、接手人,不同角度对待这件事件,态度完全不同;


公司,并不关心交接的质量,只要项目有人兜底即可;


交出方,感性上说直接敷衍交接单上的流程即可,并不在意后续的影响;


接手方,项目交接完成后的第一责任人,可能会关心项目的质量状况;


至于说接手的人能否有时间,有能力,有心情接下这种天降大任,可能除了自己以外,不到出问题的时候关注的很少;


因为项目交接过程没有处理好,从而导致后续的事故与甩锅,情绪化的现象并不少见;


如果是在内部矛盾突出的团队中,由此引发的离职效应也并不少见;



04



人的情绪真的是很奇怪,能让复杂的事情变的简单,也能让简单的事情变的离谱;


情绪上头的时候,事情本身是否真的复杂就已经不太重要了;


接手方最大的问题在于吃力不讨好,如果接了一个质量奇差的项目,意味之后很长一段时间内,工作状态都会陷入混乱的节奏中;


对于大部分研发团队来说,都是存在排期规划的,如果被交接的项目横插一脚,重新调规划影响面又偏大;


向上反馈,多半是回答一句:自行消化;


何谓自行消化,就是占用空闲时间处理,比如下班后,比如周末,比如摸鱼,这些都是对工作情绪的持续伤害;


最终兜底的个人或者团队,可能需要带着夜宵去公司搬砖;



05



吐槽归吐槽,裂开归裂开,成熟的搬砖人不该表现出明显的情绪化;


先捋一捋在面对离职交接时的注意事项,虽然说离职后有一个过渡期,但是真正涉及交接的时间通常一周左右;


作为接手一方,自然期待的是各种文档齐全,对于坑坑洼洼的描述足够清楚;


然而对于被离职的交出方,会带着若隐若现的情绪化状态,很难用心处理交接事项,能不挖坑就已经是良心队友了;


接手方作为后续的兜底人员,兜不住就是一地鸡毛;


如果兜住了呢?那是职责所在、理所应当、不要多想、安心搬砖;



06



面对项目交接,这种隔三差五个月就会突发的事,完全可以用一套固定的模式和节奏去执行;


强烈建议:不排斥、不积极、不情绪化;


但是在处理的过程中要理性且严谨,这样可以规避掉许多可能出现的麻烦,毕竟签了交接单,从此该项目问题根本甩不开;


职场几年,在多次"交"与"接"的角色转换过程中,总结以下几点是研发需要注意的;


P1:文档,信息的核心载体;


不管项目涉及多少文档,照单全收;


如果文档严重缺失甚至没有,直接在交接单上写明情况,并且得加粗划重点展示;


文档和项目的维护极有可能是线性不相关,但是手有文档心里不慌,因为方便后续再把项目交接给其他人;


所以,敷衍一时爽,出事火葬场;



07



P2:代码工程,坑与不坑全看此间;


接到手里的项目,是否会导致情绪崩塌,全看项目代码工程的质量,遇上一堆烂摊子,心情会持续的跌跌跌,然后裂开;


直接把人打包送走的情况也并不少见;


如果代码工程质量极高,架构设计稳定,组件集成比较常规,分包井然有序,悬着的情绪可以适当下落;


P3:库表设计,就怕没注释;


对于数据库层面的设计,与代码工程和业务文档三者相辅相成,把握其中的主线逻辑即可;


但前提是表的设计得有清晰的注释,如果是纯中式英文混搭拼音,且缺乏注释,必然会成为解决问题的最佳卡点;


P4:核心接口,应当关注细节;


从项目的核心业务中选出2-3个复杂的接口读一读;需要将注意点放在细节逻辑上,给内心积蓄一丢丢解决问题的底气;


熟悉接口的基本思路:请求从客户端发出,业务服务的处理逻辑,对数据层面的影响,最终响应的主体;



08



P5:遗留问题,考验职场关系的时候到了;


公司一片祥和的时候,员工之间还可以做做样子;


但是已经走到了一别两宽的地步,从感性上来说只要不藏着掖着就行,还想窥探别人安稳摸鱼的秘密,确实想的不错;


老练的开发常干的事,为了解决某个问题临时上线一段代码,处理好后关闭触发的入口,但是会保留代码主体;


这还算常规操作,最骚的是在本地写一段脚本工具解决线上的问题;


这些隐藏的接口和脚本只有开发的人自己清楚,如果不给个说明文档,这不单是挖坑,还顺手倒了一定比例的水进行混合;


P6:结尾事项,寒暄几句还是要的;


安全意识好的公司,会对员工的账号权限做好备份,以便离职时快速处理,不会留下风险隐患;


在所有权限关闭之后,接手人就可以在交接单上完成签字仪式;


交接完成后还是得适当的寒暄几句,万一接了个坑,转头就得再联系也不稀奇,所以职场留一线方便语音再连线;



09



年度收到的离职交接,已经累计好几份,对这种事情彻底麻了;


事来了先兜着,等兜不住的时候自然会有解决办法;


抗拒与烦躁都不会影响流程的持续推进,这种心态需要自己用清醒的意识不断的说服自己;


最后想探讨一个话题,跟项目前负责人联系,用什么话术请教问题,才能显得不卑不亢?



作者:知了一笑
来源:juejin.cn/post/7157651258046677029
>END


收起阅读 »

MP4 是不是该退休了?

web
背景 对于视频的在线播放,根据视频内容的传输模式可以分为点播和直播,分别用于预先录制内容的传输和实时传输,比如新闻报道、体育赛事都属于直播场景,电影、电视剧、课程视频都属于点播场景。 在 2000 年代初期,Flash 技术开始在 Web 上流行起来,它成为在...
继续阅读 »

背景


对于视频的在线播放,根据视频内容的传输模式可以分为点播直播,分别用于预先录制内容的传输和实时传输,比如新闻报道、体育赛事都属于直播场景,电影、电视剧、课程视频都属于点播场景。


在 2000 年代初期,Flash 技术开始在 Web 上流行起来,它成为在网页上展示视频的主要选择,因为当时没有其他方式能够在浏览器上流式的传输视频。


image.png


随着 HTML5 技术的逐渐成熟,HTML5 的 video 标签开始允许在没有 Flash 插件的情况下在浏览器中直接播放视频。


视频的在线播放主要的技术环节在于视频的解码、显示效率以及数据的传输效率,而 HTML5 的 video 标签将这两个环节进行解藕,开发人员不需要关心视频数据的解码、显示,只需要关心如何去优化数据的获取。


在点播的场景下,因为视频数据已经提前准备好,开发人员只需要制定 video 标签的 src 属性为对应的视频资源地址即可,但是在一些复杂的场景下比如需要根据网络状况做自适应码率、需要优化视频的首屏时间等,那么则需要对视频的一些规格参数以及相关的技术点做进一步的了解。


视频基础


视频帧率


视频的播放原理类似于幻灯片的快速切换。


image.png
每一次画面的切换称作为一帧,而帧率表示每秒切换的帧数,单位数 FPS,人类对画面切换频率感知度是有一个范围的,一般 60 FPS 左右是一个比较合适的范围,但这也需要结合具体的场景,比如在捕捉一个事物快速变化的瞬间时,需要准备足够的帧数才能捕捉到细微的变化,但是当需要拍摄一个缓慢的镜头效果时,帧率不需要太高。


帧率除了要考虑不通场景的播放内容,还需要结合播放设备的刷新频率,如果设备的刷新频率过低,多余的帧就会被丢弃。


视频分辨率


视频在播放时,显示在屏幕中的每一帧中的像素点数量都是相同的,像素是显示设备上发光原件的最小单位,最终呈现的画面是由若干个像素组合起来所展示的。


视频的分辨率是指视频每一帧画面的像素数量,通常以水平方向像素数量 x 垂直高度像素数量的形式表示。分辨率决定了图像的清晰度和细节程度,常见的分辨率有 1080P = 1920 * 1080,这是标准的纯高清分辨率,p 表示是逐行扫描的,与之对应的是 i 表示的是隔行扫描。


image.png


左边一列是逐行扫描,中间一列是隔行扫描,在隔行扫描中会丢失一些页面信息从而加快页面信息的收集。


当设备的分辨率高于视频的分辨率时,设备上的像素点就会多于视频显示所需的像素点,这时就会使用补间算法来为设备上那些未被利用的像素点生成色值信息,否则将导致屏幕上出现黑点,此时人从感官上就会觉得清晰度有所下降。如果视频的分辨率高于设备的分辨率时,则视频多出的信息会被丢弃。


视频格式


视频格式是一种特定的文件格式,用于存储和传输视频数据,它包含了视频图像、音频、字幕和其他相关媒体数据的编码信息,不同的视频格式采用不同的压缩算法和编码方式,以便在存储和传输的过程中有效的减少文件大小并保持高质量的图像。


常见的视频格式有 MP4、AVI、MOV 等,每种视频格式都有其特定的优势和使用场景,比如 MOV 在 Mac 系统上有很好的兼容性,适用于视频编辑。


MP4视频结构


MP4 文件由许多 Box 数据块组成,每个 Box 可以嵌套包含其他 Box,一级嵌套一级来存放媒体信息,这种层次化的结构使得 MP4 文件能够组织和存储各种不同类型的媒体数据和元数据,使其在播放和传输过程中具有灵活性和可扩展性。


image.png


虽然 Box 的类型非常多,但是并不是都是必须的,一般的 MP4 文件都是含有必须的 Box 和个别非必须 Box,下面使用 MP4Box.js 查看 MP4 的具体结构并介绍几个必须 Box:


image.png




  • ftyp


    File Type Box,一般在文件的开始位置,描述的文件的版本、兼容协议等。




  • mdat


    Media Data Box,媒体数据内容,是实际的视频的内容存储区域。该区域通常占整个文件99%+大小。


    image.png




  • moov


    MP4 的媒体数据信息主要存放在 Moov Box 中,是我们需要分析的重点。moov 的主要组成部分如下:




    • mvhd


      Movie Header Box,记录整个媒体文件的描述信息,如创建时间、修改时间、时间度量标尺、可播放时长等。


      image.png




    • udta


      保存自定义数据




    • track


      对于媒体数据来说,track 表示一个视频或音频序列,trak 区域至少存在一个,大部分情况是两个(音频和视频)。


      image.png






形象点来说,moov 可以比如成是整个视频的目录,想要播放视频的话,必须要先加载 moov 区域拿到视频文件目录才能播放视频内容。


为什么MP4视频首屏慢?


当我们在浏览器中打开一个 MP4 视频文件时,浏览器根据就会开始获取视频信息,下载视频 chunk,开始播放视频,通过抓包能够大致了解浏览加载视频过程:


image.png


从请求列表中可知,浏览器发送了三个请求,总耗时 55s ,该视频文件的 box 结构如下:


image.png


下面来具体看一下这三个请求:




  • 第一次请求


    image.png


    浏览器第一次请求时尝试通过 HTTP range request(范围请求)下载整个视频,但是实际只下载了 135 KB 整个请求就完成了,来分析一下具体流程:



    • 浏览器通过 Range: bytes= 0- 首先获取到了 ftyp 信息,这里 ftyp-box 大小为 32 字节;

    • 接下来继续尝试查找 free-box 区域,如果没有就跳过,这里 free-box 大小为 8 字节;

    • 接着尝试查找下一个区域(moov 或 mdat),结果不幸匹配到的区域是 mdat 区域,这时浏览器就会主动终止请求,尝试从尾部查找视频的 moov 区域,因为上面我们讲过 moov 作为视频文件的目录,在播放视频数据前必须先获取 moov 数据,紧接着开始了第二次请求。




  • 第二次请求


    在第一次请求中已经知道了整个视频文件的大小了,如何去确定请求的范围呢?由于 MP4 是由 Box 组成的,标准的 Box 开头的4个字节(32位)为这个 Box 的大小,该大小包括 Box Header 和 Box Body,这样浏览器在第一次请求后就可以确定文件中剩下未解析到的 Box 的开始的 Range 值了。


    image.png


    计算过程(单位字节):
    moov 大小 = 视频文件大小 - ftyp大小 - free大小 - mdat大小 = 22251375 - 32 - 8 - 22224468 = 26867。


    也就是说这一次请求的 range 的开始值最大值不能高于 22251375-26867 = 22251415。


    image.png


    可以看到发出去的请求 Range: bytes=22251374-∞ ,上面计算的 22251415-∞ 包含在内 ,请求到数据后,接下来就是解析 moov-box了,然后根据视频”目录“发起第三次请求。




  • 第三次请求


    根据第二次请求的 moov 解析后,开始下载”真正“的视频的内容准备播放,在第三次请求中,浏览器必须要缓存 4MB 左右才开始播放,




原因分析




  • 过多的数据请求。


    由于 MP4 文件的特殊性,浏览器必须先将 ftyp 、moov 等资源加载完毕之后才能去播放视频,而浏览器是从头部开始依次去加载这些资源,一旦视频资源存放顺序不对,浏览器会发送多次请求分别加载对应的资源。




  • 全量解析 moov


    播放 Mp4 音视频数据前需要先加载并解析 moov 数据,moov 的大小和视频长度成正比,更坏的情况是如果此时服务器没有配置 HTTP range request,浏览器无法跳过查找 moov 这一步,以至于需要下载整个文件。




如何借助HLS 优化视频播放的?


什么是HLS?


HLS 全称是 HTTP Live Streaming,是一个由 Apple 公司提出的基于 http 的媒体流传输协议,用于实时音视频流的传输,HLS 最初是为苹果设备和平台(如iOS和macOS)设计的,但如今已被广泛应用于各种平台和设备上,成为流媒体传输的主要标准之一。


HLS 协议由三部分组成:http、m3u8、ts,这三部分中,http 是传输协议,m3u8 是索引文件,ts是音视频的媒体信息。


HLS的优势和特点是什么?




  • 分段传输


    HLS 将整个音频或视频流切分成短的分段,通常每个分段持续几秒钟,这种分段的方式使得视频内容可以逐段加载和播放,从而提供更好的适应性和流畅性。




  • 基于HTTP协议


    HLS 使用 http 协议进行数据传输,这意味着它能够在标准的 http 服务器上运行,不需要专门的流媒体服务器。




  • 自适应码率


    HLS 支持自适应码率,根据网络带宽和设备性能,动态地选择合适的分辨率和比特率,以提供更好的观看体验。




  • 多码率支持


    媒体源可以同时提供不同分辨率和比特率的视频流,使得用户可以根据网络状况选择合适的码率。




  • 兼容性好


    由于 HLS 使用标准的 HTTP 协议,它在各种设备和平台上具有很好的兼容性,包括苹果设备、Android 设备、PC、智能电视等。在使用 http 播放 MP4 视频时,需要代理服务器支持 http range request 以获取视频的某一部分,但不是所有的代理服务器都对此有良好的支持,而 HLS 不需要,它对代理服务器的要求小很多。




HLS为什么首屏比MP4快?


上面讲过如果要播放 MP4 需要等待整个 moov box 加载完成,这个过程比较消耗时间和带宽,而在 HLS 协议中,分段传输是一个非常重要的特性,HLS 将整个音视频流切分成多个小的分段(ts 文件),这些分段可以被独立的下载和播放。


具体来说,HLS 的工作流程如下:




  • 切分分段:


    原始的音视频流被切分成短小的分段( .ts 文件),每个分段都包含了一小段时间范围内的音视频数据。




  • m3u8 文件:


    服务器生成一个 .m3u8 文件,它是一个播放列表,包含了所有分段的信息,如地址、时长等。播放器通过请求 .m3u8 文件来获取分段列表。




  • 分段请求:


    播放器根据 .m3u8 文件中的分段信息,逐个请求并加载 .ts 分段。




  • 逐段播放:


    播放器逐个播放已经加载的分段,实现连续的音视频播放。




因此,HLS 首屏播放的实现方式不需要像 MP4 那样等待整个文件的基本信息加载完成,而是通过分段传输的方式逐段加载和播放。这使得首屏播放更快速和响应,同时也为流媒体的适应性提供了更好的支持。


为什么选择TS格式文件?


TS(Transport Stream,传输流)是一种封装的格式,它的全称为 MPEG2-TS,主要应用于数字广播系统,譬如 DVB、ATSC 与 IPTV,传输流最初是为广播而设计的,后来通过在标准的188字节数据包中添加4字节的时间码(TC),从而使该数据包成为192字节的数据包,使其适用于数码摄像机,录像机和播放器。


TS(Transport Stream)流在流媒体领域具有多种优点,使得它成为广泛应用于数字电视、流媒体、广播等领域的传输格式之一。以下是TS流的一些优点:




  • 分段传输:


    TS 流将媒体数据切分成小的分段(Packet),每个分段通常持续数毫秒至几十毫秒。这种分段传输使得数据能够按需传输和加载,从而实现快速启动播放和逐段加载,提高了用户体验。




  • 容错性强:


    每个 TS 分段都具有自己的包头信息和校验机制,这使得 TS 流具有较强的容错性。即使在传输过程中发生丢包或错误,也只会影响某个分段,不会影响整个媒体流的播放。




  • 多路复用:


    多路复用的目的一般为了在一个文件流中能同时存储视频、音频、字幕等内容,而TS 流就支持将多个音视频流混合在一个文件中,每个流都有自己的 PID(Packet Identifier)。这使得 TS 流适用于同时传输多个媒体流的场景,如电视广播、有线电视等,提高了传输效率。




  • 支持多种编码格式:


    TS 流可以支持多种音视频编码格式,如H.264、H.265、AAC、MP3等,使其能够适应各种类型的媒体内容。




M3U8格式文件的构成


以下为一个 m3u8 格式文件内容的示例:


#EXTM3U
#EXT-X-VERSION:6
#EXT-X-KEY:METHOD=AES-128,URI="<https://xxxx?token=xxx>"
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:19
#EXTINF:12.000,
<https://xxxx/test/1.ts>
#EXTINF:7.500,
<https://xxxx/test/2.ts>
#EXTINF:13.000,
<https://xxxx/test/3.ts>
#EXTINF:9.720,
<https://xxxx/test/4.ts>
#EXT-X-ENDLIST

在以上示例中包含了 m3u8 文件常见的字段下面为一个M3U8文件可包含的基本字段及含义解释:



  • #EXTM3U 表明该文件是一个 m3u8 文件。每个 M3U8 文件必须将该标签放置在第一行。

  • #EXT-X-VERSION 指定 M3U8 版本号。

  • #EXT-X-KEY 媒体片段可以进行加密,而该标签可以指定解密方法。例如在上面的示例中,该字段指定了加密算法为 AES-128,密钥通过请求 https:xxxx?token=xxx 获取,以用于解密后续下载的 ts 文件。

  • EXT-X-MEDIA-SEQUENCE: 第一个 TS 分片的序列号。每个 TS 分片都拥有一个唯一的整型序列号,每个 TS 分片序列号按出现顺序依次加 1,如果该分片未指定则默认序列号从 0 开始。对于视频点播资源该字段一般是 0,但是在直播场景下,这个序列号标识直播段的起始位置。

  • #EXT-X-TARGETDURATION: 每个 TS 分片的最大的时长,单位为秒。

  • #EXT-X-DISCONTINUITY: 该标签表明其前一个切片与下一个切片之间存在中断。

  • #EXT-X-PLAYLIST-TYPE: 指定流媒体类型。

  • #EXT-X-ENDLIST: M3
    作者:西陵
    来源:juejin.cn/post/7268658252567691322
    U8 文件结束符。

收起阅读 »

慎重选择~~第四家公司刚刚开我,加入重新找工作大队!!!

前景需知 这家公司是我的第四家公司,合同三年,6个月试用期,(当时入职时,谈过说可以提前转正,但是后续当作没这件事),然后7月25日,下午5点半,下班时候告诉我被开了。当天是我手上的一个新项目刚好完结,测试刚过,bug修复完毕,老板让人事通知我,被开了,说是没...
继续阅读 »

前景需知


这家公司是我的第四家公司,合同三年,6个月试用期,(当时入职时,谈过说可以提前转正,但是后续当作没这件事),然后7月25日,下午5点半,下班时候告诉我被开了。当天是我手上的一个新项目刚好完结,测试刚过,bug修复完毕,老板让人事通知我,被开了,说是没有新的项目了。当时我算了算应该是还有几天就转正了。


在职期间


总共是在职6个月差几天转正,期间一直是大小周,说是双休,加班没有任何补偿,然后9点到5.30.(从来没有5点半下班过,最早就是6点半吧,5点半下班会打电话给你,问你为啥下班那么早).然后在这家公司这么久,手上是写了3个新项目,翻新2个老项目,还有维护的。期间没有任何违纪行为,这肯定是一定的,不然也不会等到还有几天才把我开了。在职期间做的事,跟产品沟通为什么不能这么写,用户怎么交互比较合理,不必太过于麻烦,给后端沟通为什么要这个数据,为什么要这样,还要跟老板说 进度怎么样的,预计时间。因为没有测试,是所有员工用了以后提一个bug单,到我这里来,然后我统一看这是谁的问题,然后我去沟通,加上公司内部人员测试,很多东西产品出成那样,觉得不合理,也要给我,我去跟产品沟通,真是沟通成本大的要死,期间有一个要对接别人的app里的积分系统,对公到我们的积分体系里,还要我去对接,这不能找后端嘛?产品又甩给我了,最后又要我去跟第三方沟通,再给自己的后端沟通,成本是真的高啊,我真是有时候头大。听着有点小抱怨,但是吧,其实后面了还好,确实能让你学到很多东西,因为你很清楚这个项目的走向,以及问题,基本上所有东西有点围绕着前端做的感觉,反正每天都是被问,问到最后,无论是谁张嘴我都知道是什么个情况。反正学着接受就好了。


为什么会来到这家公司??


这家公司是我去年面过的一家公司,当时入职他们公司一天我就走了,为什么会走,就是因为代码累积,页面过于卡顿,前端没有任何标注,而且入职第一天,老板就要求改他们的东西,然后第二天就没去了,为什么今年去了,是因为去年这个老板也联系了我几次,说我可以去他们公司试试看,然后过年的前两天还在跟我说,我说那就去试试看看,然后年后那个老板也催着我入职,当时也不是没得选,朋友公司招人内推,他面我,说让我去。我当时主要是跟这个老板说好了,答应了,于是就回绝了我的朋友(真后悔啊,那是真后悔,真不如去朋友哪里了,现在还被开了,卸磨杀驴,我真气)。


在公司半年,我具体做了哪些东西


上面说做了3个新项目,翻新两个新项目。三个新项目是一个是可视化大屏项目,这个项目用的是(vue3加echarts,适配是用v-scae-screen这个组件做的,当然这时候就有人会问,你用这个组件 那其他屏幕的除了你用的这个分辨率,其他比例不对的分辨率,也会有问题,当然这个问题我也遇到了,但是也试了其他的几种方案,但是或多或少都有问题,所以我就选择了这个比较直接.原理## transform.scale(),更详细的可以看看这个组件。)还有一个是小程序的老师端批改作业,并给予点评。(uni-app加uview写的,这个直接上图片,有难点)


image.png
第三个项目也是uni-app写的,就是刚刚写完这个项目我被开了,真是太离谱了。也是一个小程序(uni-app加uview,然后益智类的,可以直接搜索头脑王者这个小程序,基本上是功能还原。不贴我的项目图了,好像我走的第二天就在审核了,主要是websocket长连接写的,因为是对战类,所以长连接时时保持通讯,也是有难点的,因为长连接要多页面保持又要实时获取信息,可以想一下怎么做)。 翻新的项目就不谈了,算是整个翻新,翻新是最累的,因为有的能用有的不能用,该封装封装,数据该处理处理,哦,中间遇到一个有趣的问题,就是el-tabs这个缓存机制,不知道为啥,v-if也不行.


目前的看法


7月25下午被开当天其实我很痛苦,当时人事说话也很过分,让我自己签申请离职说,这样的话赔偿你 0.5,如果不行,你可以去仲裁我们,然后如果我去仲裁,那么离职单,离职证明,赔偿,工资都没有,就拖着你,甚至老板恶言相向的告诉人事说,怎么可以在他的工作简历上留下这个不好的痕迹,影响他以后的工作。其实我听到这些话的时候我除了恶心,我什么话都说不出来,面对这个种情况,我咨询了,12333他们说,让我照常上班,他把你提出打开的软件,你就手动拍摄视频,然后自己打开,直至出示他把你辞退的证明,或者待够15天。我把这个事情实施以后,并且告知公司,仍然不给我出示离职证明,出了一张,辞退通知书,这个通知书我直接上图片,首先这个假,是个病假,是因为后端对我进行了侮辱,然后导致我气的头疼,然后我去请假,是给领导直接请的,她允许以后,我才中午下班是,离开的公司。
image.png为什么会给后端吵架,因为后端不处理逻辑,还要怪我什么都不给他说,什么都不给讲,这是我最气的点,我每次都要给他讲,为什么需要这个数据,为什么你要这么给我,需要什么,我每次都在他没写之前就进行沟通。他最后怪我没讲,并且侮辱我。有的人这时候会说,你为什么不他给你什么就要什么呢?然后自己处理逻辑。降低了耦合性,再往后说 你自己可以写一个node.js啊 为什么不呢?这些都挺对的,但是吧,你不能每次都这么处理问题吧。一个选择题,他应该给你abcd,结果给你1234,然后他要abcd,你说这个转换你做不做?你好说歹说他给你改了,然后一道题4个选项 我回答完以后,他给你答案你自己判断对错,这个逻辑前端写吗,当然也可以,如果他给你的答案是 1呢 1就是a,这时候你又该如何是好?可能你觉得我不信后端会这个对你,一定是你的问题,哈哈 上图片


image.png
09a0e6e51d7d19fc7695ef2bfc77740.jpg
是的没有错,我来教着写,这个时候大家可以喷我了,可以说,你怎么交后端写,你算什么东西,兄弟们,兄弟们,都是我的问题,实在是没办法了,写出了这样得东西 这个东西还能精简,这是只是我为了实现而写得逻辑。


image.png


反正一吐为快,目前是没找工作,下周找找看吧,缓解一下。


当下迷茫得点


希望大家给点建议,就是说因为没有遇到一个好的产品导致我现在想去做产品,我直接现在转产品工资会有一个大跳水,会少很多,但是我也愿意接受,可能是赌气吧,就真的想去做这个,让开发没那么难以沟通。也在想是不是继续前端,保持现状,但是就是想去转产品了,我现在24岁,前端3年多,我应该还有试错得机会,我真的不想在碰见这种情况了,真的好累,加上只是前端,人微言轻,只有出现问题,提出来的东西,才能被采纳,真的好难。所以我是有意愿转转看的,不知道各位怎么看?能评价就评价下,需要我爆雷得,我私信,他们目前好像又在招前端了,怕大家踩雷,在上海。


给大家得建议


就是入职前,还是要好好调查,然后不要只听片面之言,然后就是现状不好的,也不要气馁,就加油好吧,我都没气馁,顶住压力啊,还是

作者:想努力的菜菜
来源:juejin.cn/post/7262156717244530744
希望大家吃好喝好玩好,生活美满。

收起阅读 »

如何用canvas画出验证码

相信在平时的工作中,canvas肯定是我们不可或缺的伙伴,有很多业务场景都需要他来完成,闲来无事,今天我们就先说一下canvas如何画出验证码 首先,我们应该有一个canvas标签(注意:可以标签里面设置宽高,也可以在js里面设置,但是不建议在style样式...
继续阅读 »

相信在平时的工作中,canvas肯定是我们不可或缺的伙伴,有很多业务场景都需要他来完成,闲来无事,今天我们就先说一下canvas如何画出验证码



  • 首先,我们应该有一个canvas标签(注意:可以标签里面设置宽高,也可以在js里面设置,但是不建议在style样式里面去设置,因为会导致里面的元素大小和你设置的不一样)。

  • 然后我们需要有一个随机生成四位数code的一个方法


image.png



  • 然后我们还需要一个canvas的绘画方法


image.png



  • 因为当点击canvas的时候,canvas里面的code也就会变,这时候我们的随机生成四位数的方法就可以用上了。
    -上面这样已经可以在点击的时候生成二维码了,但是还是有一个问题,就是我们没有清空画布,所以导致了每次都是在之前的画布上面去生成,这样就出现文字叠加的问题,所以我们还需要清空画布

  • 所以还需要一个清空画布的方法,这里我采用的是改变画布的大小的方法清空画布


image.png



  • 以上就是完整的一套二维码生成的流程了,以下是完整代码


<template>
<div class="main">
<div class="head">
<el-input v-model="ipt" placeholder="Please input" />
<canvas id="canvas" width="100" height="40" @click="SelectNumber"></canvas>
</div>
<div class="foot">
<el-button type="primary">Primary</el-button>
</div>
</div>
<div>
</div>
</template>

<script setup>
import { nextTick, ref } from 'vue';
import { ElMessage } from 'element-plus'
let ipt = ref()
let num = ref()
// 生成二维码code
const generateCode = () => {
nextTick(() => {
const canvas = document.querySelector('#canvas')
const context = canvas.getContext('2d')
context.font = 'oblique 20px Arial'
context.fillStyle = '#fff'
context.rotate(Math.PI * 1.4 / 180)
context.fillText(num.value, 10, 25)
})
}
// 获取四位随机数
const generateNum = () => {
num.value = Math.floor(Math.random() * 4000 + 1000)
}
// 清空画布
const clearCanvas = () => {
nextTick(() => {
const canvas = document.querySelector('#canvas')
canvas.width = 100
})
}

// 触发画布切换验证码
const SelectNumber = () => {
clearCanvas()
generateNum()
generateCode()
}

const submit = () => {
if (num.value !== Number(ipt.value)) ElMessage.error('验证码输入错误')
else ElMessage.success('验证码输入正确')
}
generateNum()
generateCode()
</script><
作者:L的技术博客
来源:juejin.cn/post/7269290896214212642
/span>
收起阅读 »

不知什么原因,背调没过?

前两天写了一篇文章《电话背调,我给他打了8分》,跟大家聊了职场中沟通的一些基本原则和经验。背调时,同事没给打招呼,几乎也没什么私交,但出于“不坏别人好事”的原则,给了8分的评价。 在稍微大一些的公司中,背调是非常重要的环节。如果拿到了offer,上家公司已经离...
继续阅读 »

前两天写了一篇文章《电话背调,我给他打了8分》,跟大家聊了职场中沟通的一些基本原则和经验。背调时,同事没给打招呼,几乎也没什么私交,但出于“不坏别人好事”的原则,给了8分的评价。


在稍微大一些的公司中,背调是非常重要的环节。如果拿到了offer,上家公司已经离职,新公司还没入职,背调没通过,那就有点悲催了。所以,今天就跟大家聊聊入职背调中的一些注意事项。


第一,背调日趋严格


整体而言,背调是越来越严格了。当然,每家公司都不是为了背调而背调,这是劳民伤财的事,主要是因为履历包装的情况太严重。特别是有一部分刚毕业为了找到工作,通过简历、履历、学历等途径包装成2-3工作经验的情况时有发生。


还有就是,HR也有考核指标,HR在实际招聘的过程中会踩一些坑,为了避免类似的事情发生,会在既有的经验上进行迭代筛查条件。


一般背调有两种方式:体量小一些的公司,HR会给你留电话的人打电话核实;体量大一些的公司会直接委托三方来进行背调核实。


HR直接打电话的背调相对来说会简单一些,而且会有一些个人风格,我们暂且不提。而背调公司的风格一般比较统一。


第二,背调联系人


背调的过程一般会让写三类联系人:直接领导、人力和同事。大概率会背调之前两家公司的履历。


在填写时,你就需要慎重考虑了,基本上会挨个打电话询问你的情况的。所以,你写谁之前,最好先打个招呼,否则说你的坏话,那你就有些悲催了。像上篇文章中同事那样不打招呼的操作,是强烈不建议的。


另外,你写的这些联系人要能够联系得到才行。如果都联系不上,过的可能性就不大了。


第三,背调的过程


曾经多次作为上级领导参与背调,背调的核心点有几项(他,代表被背调的人):


确认身份:确认你是否是本人,是否是他的上级领导。同时,还会确认他的岗位信息,他是否带下属,下属多少人等。除了电话确认之外,甚至还会要求入职人员跟相关人要公司企业管理软件(钉钉、飞书等)中带有企业名称、填写人姓名的截图证明等。


表现评分:在工作表现、沟通表现等方面会有1-10分,询问各项的表现评分是多少。同时,在问题的涉及上还会有一些交叉认证的小策略。会涉及到:工作表现如何,与大家相处的如何,吃苦耐劳能力如何,抗压能力如何、离职原因是什么、你是否满意他的整体表现、是否有违规操作等等。


交叉确认:除了个人表现的评分确认之外,如果同一个公司的背调,还会交叉确认一下你留的其他人员是否也是这家公司的,是否是对应岗位的。


如果你预留的信息都是真实的,那么不用担心什么,跟填写联系人的打好招呼就行了。如果部分内容有出入,那可要交代清楚了。


另外,在工作中,平时与同事和上下级相处时,保持融洽的关系,留一个联系方式等也有一定的必要性。


第四,其他可能性


除了上面统一的背调流程之外,某些公司还会有更加严格的背调信息。这些信息是否违法违规暂且不说,但是是会出现的。如果你不care这份工作,可以拒绝提供的。


常见的有收入证明、工资流水、社保缴纳、征信报告等。


收入证明一般由上家公司出具并盖章,私企或关系比较好一些,可以适当调整。工资流水可以是银行打印的或下载的电子单据。社保缴纳可以提供查询到的流水。征信报告这个对于部分金融相关的行业会有一定要求,会引导你操作申请一份个人征信报告。


另外还有两项,大多数人可能不知道,但对于高端的一些岗位也会涉及到:HR的圈子和劳动诉讼。


HR是有自己的圈子和人脉的,而且可能比你想象的要广。如果你在上家公司,或者在圈子里名声不好,很可能会被问出来的。这个也没其他办法,自己的个人人设和职业素养问题了。


另外一个就是劳动诉讼,这个也是可以调查出来的,除了有专门的机构可以做这些事之外,某些诉讼可以在企业的“法律诉讼”中查到诉讼的另一方的。当然,如果曾经涉及到刑事案件用人单位也是可以查出来的。


最后


市场越来越卷,而打工人越来越不容易。在日常工作中保持良好的人际关系和职业素养,更多的还是为自己铺好后路。在面试找新工作时保持诚信,尽量避免出现撒一个谎,用一百个谎来圆的情况。


最后,无论怎样,都要有备选方案,既不能丢了西瓜捡了芝麻,更不能最后两手空空。

作者:程序新视界
来源:juejin.cn/post/7265999062242263100

收起阅读 »

电话背调,我给他打了8分

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,...
继续阅读 »

前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。

离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。

离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。

当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。

站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。

就着这事,聊聊最近对职场上关于沟通的一些思考:

第一,忌固执己见

职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。

而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。

真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。

第二,不必说服,尊重就好

站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。

曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。

其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。

对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。

有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。

第三,不懂的领域多听少说

如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。

如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。

站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。

郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。

第四,没事多夸夸别人

在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。

当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。

这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。

前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。

看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。

其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。

当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。很有意思,也很有用。

作者:二师兄
来源:mp.weixin.qq.com/s/GlTVKWsIRP--VKsZlv8TNA

收起阅读 »

程序员的这10个坏习惯,你中了几个?超过一半要小心了

前言 一些持续关注过我的朋友大部分都来源于我的一些资源分享和一篇万字泣血斩副业的劝诫文,但今年年后开始我有将近4个月没有再更新过。 有加过我好友的朋友私聊我问过,有些回复了有些没回复。 想通过这篇文章顺便说明一下个人的情况,主要是给大家的一些中肯的建...
继续阅读 »

前言



一些持续关注过我的朋友大部分都来源于我的一些资源分享和一篇万字泣血斩副业的劝诫文,但今年年后开始我有将近4个月没有再更新过。




有加过我好友的朋友私聊我问过,有些回复了有些没回复。




想通过这篇文章顺便说明一下个人的情况,主要是给大家的一些中肯的建议。



我的身体



今年年前公司福利发放的每人一次免费体检,我查出了高密度脂蛋白偏低,因为其他项大体正常,当时也没有太在意。




但过完年后的第一个月,我有一次下午上班忽然眩晕,然后犯恶心,浑身发软冒冷汗,持续了好一阵才消停。




当时我第一感觉就是颈椎出问题了?毕竟这是程序员常见的职业病。




然后在妻子陪伴下去医院的神经内科检查了,结果一切正常。




然后又去拍了片子看颈椎什么问题,显示第三节和第四节有轻微的增生,医生说其实没什么,不少从事电脑工作的人都有,不算是颈椎有大问题。




我人傻了,那我这症状是什么意思。




医生又建议我去查下血,查完后诊断出是血脂偏高,医生说要赶紧开始调理身体了,否则会引发更多如冠心病、动脉粥样硬化、心脑血管疾病等等。




我听的心惊胆战,没想到我才34岁就会得上老年病。




接下来我开始调理自己的作息和生活,放弃一些不该强求的,也包括工作之余更新博客,分享代码样例等等。




4个月的时间,我在没有刻意减肥的情况下体重从原先152减到了140,整个人也清爽了许多,精力恢复了不少。




所以最近又开始主动更新了,本来是总结了程序员的10个工作中的不良习惯。




但想到自己的情况,决定缩减成5个,另外5个改为程序员生活中的不良习惯,希望能对大家有警示的作用。



不良习惯


1、工作


1)、拖延症


不到最后一天交差,我没有压力,绝不提前完成任务,从上学时完成作业就是这样,现在上班了,还是这样,我就是我,改不了了。



2)、忽视代码可读性


别跟我谈代码注释,多写一个字我认你做die,别跟我谈命名规范,就用汉语拼音,怎样?其他人读不懂,关我什么事?



3)、忽视测试


我写一个单元测试就给我以后涨100退休金,那我就写,否则免谈。接口有问题你前端跟我说就行了发什么脾气,前后端联调不就这样吗?!



4)、孤立自己


团队合作不存在的,我就是不合群的那个,那年我双手插兜,全公司没有一个对手。



5)、盲目追求技术新潮


晚上下班了,吃完饭打开了某某网,看着课程列表中十几个没学完的课程陷入了沉默,但是首页又出现了一门新课,看起来好流行好厉害耶,嗯,先买下来,徐徐图之。



2、生活


1)、缺乏锻炼和运动


工作了一天,还加班,好累,但还是得锻炼,先吃完饭吧,嗯,看看综艺节目吧,嗯,再看看动漫吧,嗯,还得学习一下新技术吧,嗯,是手是得洗澡了,嗯,还要洗衣服,咦,好像忘记了什么重要的事情?算了,躺床上看看《我家娘子不对劲》慢慢入睡。



2)、加班依赖症


看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像十一点半了,快中午了,待会儿吃什么呢?


午睡醒了,继续干吧,看看头条,翻翻掘金,瞅瞅星球,点点订阅号,好了,开始工作吧,好累,喝口水,上个厕所,去外面走走,回来了继续,好像5点半了,快下班了,任务没完成。


算了,加加班,争取8点之前搞定。


呼~搞定了,走人,咦,10点了。



3)、忽视饮食健康


早上外卖,中午外卖,晚上外卖,哇好丰富耶,美团在手,简直就是舌尖上的中国,晚上再来个韩式炸鸡?嗯,来个韩式甜辣酱+奶香芝士酱,今晚战个痛快!



4)、缺乏社交活动


好烦啊,又要参加公司聚会,聚什么餐,还不是高级外卖,说不定帮厨今天被大厨叼了心情不好吐了不少唾沫在里面,还用上完厕所摸了那里没洗的手索性搅了一遍,最后在角落里默默看着你们吃。


吃完饭还要去KTV?继续喝,喝不死你们,另外你们唱得很好听吗?还不是看谁嗷的厉害!


谁都别跟我说话,尤其是领导,离我越远越好,唉,好想回去,这个点B站该更新了吧,真想早点看up主们嘲讽EDG。



5)、没有女朋友


张三:我不是不想谈恋爱,是没人看得上我啊,我也不好意思硬追,我也要点脸啊,现在的女孩都肿么了?一点暗示都不给了?成天猜猜猜,我猜你MLGB的。


李四:家里又打电话了,问在外面有女朋友了没,我好烦啊,我怎么有啊,我SpringCloudAlibaba都没学会,我怎么有?现在刚毕业的都会k8s了,我不学习了?不学习怎么跳槽,不跳槽工资怎么翻倍,不翻倍怎么买房,不买房怎么找媳妇?


王五:亲朋好友介绍好多个了,都能凑两桌麻将了,我还是没谈好,眼看着要30了,我能咋整啊,我瞅她啊。破罐破摔吧,大不了一个人过呗,多攒点钱以后养老,年轻玩个痛快,老了早点死也不亏,又不用买房买车结婚受气还得养娃,多好啊,以后两脚一蹬我还管谁是谁?



总结



5个工作坏习惯,5个生活坏习惯,送给我亲爱的程序员们,如果你占了一半,真得注意点了,别给自己找借口,你不会对不起别人,只是对不起自己。





喜欢的小伙伴们,麻烦点个赞,点个关注,也可以

作者:程序员济癫
来源:juejin.cn/post/7269375465319415867
收藏下,以后没事儿翻出来看看哈。

收起阅读 »

为什么WebSocket需要前端心跳检测,有没有原生的检测机制?

web
本文代码 github、gitee、npm 在web应用中,WebSocket是很常用的技术。通过浏览器的WebSocket构造函数就可以建立一个WebSocket连接。但当需要应用在具体项目中时,几乎都会进行心跳检测。 设置心跳检测,一是让通讯双方确认对方...
继续阅读 »

本文代码 githubgiteenpm



在web应用中,WebSocket是很常用的技术。通过浏览器的WebSocket构造函数就可以建立一个WebSocket连接。但当需要应用在具体项目中时,几乎都会进行心跳检测。


设置心跳检测,一是让通讯双方确认对方依旧活跃,二是浏览器端及时检测当前网络线路可用性,保证消息推送的及时性。


你可能会想,WebSocket那么简陋的吗,居然不能自己判断连接状态?在了解前先来回顾一下计算机网络知识。


相关的网络知识


TCP/IP协议族四层结构:




  • 应用层:决定了向用户提供应用服务时通信的活动。HTTP、FTP、WebSocket都在该层




  • (TCP)传输控制层:控制网络中两台主机的数据传输:将应用层数据(有必要时对应用层报文分段,例如一个完整的HTTP报文进行分段)发送到目标主机的特定端口的应用程序。给每个数据标记源端口、目标端口、分段后的序号。




  • (IP)网络层:将IP地址映射为目标主机的MAC地址,然后将TCP数据包(有必要时对数据分片)加入源IP、目标IP等信息后经过链路层扔到网络上让其找到目标主机。




  • 链路层:为IP网络层进行发送、接收数据报。将二进制数据包与在网线传输的网络电信号进行相互转换。





TCP是可靠的连接,握手建立连接后,发送方每发送一个TCP报文(对应用层报文分段后形成多个TCP报文),都会期望对方在指定时间里返回已收到的确认消息,如果超时没有回应,会重复发送,确保所有TCP报文可以到达对方,被对方按顺序拼接成应用层需要的完整报文。


WebSocket协议支持在TCP 上层引入 TLS 层,建立加密通信。



WebSocket与HTTP的异同:




  • WebSocket和HTTP一样是应用层协议,在传输层使用了TCP协议,都是可靠的连接。WebSocket在建立连接时,可以使用已有的HTTP的GET请求进行握手:客户端在请求头中将WebSocket协议版本等信息发生到服务器,服务器同意的话,会响应一个101的状态码。就是说一次HTTP请求和响应,即可轻松转换协议到WebSocket。




  • WebSocket可以互相发起请求。当有新消息时,服务器主动通知客户端,无需客户端主动向服务器询问。客户端也可以向后端发送消息。而HTTP中请求只能由客户端发起。




  • WebSocket是HTML5的内容,HTTP则是超文本传输协议,比HTML5诞生更早。




  • 在应用层,WebSocket的每个报文(在WebSocket中叫数据帧)会比HTTP报文(必须包含请求行、请求头、请求数据)更轻量。



    • WebSocket每个数据帧只有固定、轻量的头信息,不会有cookie等或者自定义的头信息。并且建立通讯后是一对一的,不需要携带验证信息。但握手时的HTTP请求会自动携带cookie。

    • WebSocket在应用层就会将大的数据分拆到多个数据帧,而HTTP不会拆分每个报文。




WebSocket与与WebRTC的异同:



  • WebRTC是一种通讯技术,由谷歌发起,被广大浏览器实现。用来建立浏览器和浏览器间的通讯,如视频通话等。而WebSocket是一种经过抽象的协议,可以实现为通讯技术。用来建立浏览器和服务器间的通讯。


协议中的心跳检测机制


从网上检索的答案,WebSocket大概有两种从协议角度出发的,检测对方存活的方式:




  1. WebSocket只是一个应用层协议规范,其传输层是TCP,而TCP为长连接提供KeepAlive机制,可以定时发送心跳报文确认对方的存活,但一般是服务器端使用。因为是TCP传输控制层的机制,具体的实现要看操作系统,也就是说应用层接收到的连接状态是操作系统通知的,不同操作系统的资源调度是不一样的,例如何时发送探测报文(不包含有效数据的TCP报文)检测对方的存活,频率是多久,在不同的系统配置下存在差异。可能是2小时进行一次心跳检测,或许更短。如果连续没有收到对方的应答包,才会通知应用层已经断开连接。这就带来了不确定性。同时也意味着其它依赖该机制的应用层协议也会被影响。也就是说要利用这个过程进行检测,客户端要修改操作系统的TCP配置才行,在浏览器环境显然不行。




  2. WebSocket协议也有自身的保活机制,但需要通讯双方的实现。WebSocket通讯的数据帧会有一个4位的OPCODE,标记当前传输的数据帧类型,例如:0x8表示关闭帧、0x9表示ping帧、0xA表示pong帧、0x1普通文本数据帧等。http://www.rfc-editor.org



    • 关闭数据帧,在任意一方要关闭通道时,发送给对方。例如浏览器的WebSocket实例调用close时,就会发送一个OPCODE为连接关闭的数据帧给服务器端,服务器端接收到后同样需要返回一个关闭数据帧,然后关闭底层的TCP连接。

    • ping数据帧,用于发送方询问对方是否存活,也就是心跳检测包。目前只有后端可以控制ping数据帧的发送。但浏览器端的WebSocket实例上没有对应的api可用。

    • pong数据帧,当WebSocket通讯一方接收到对方发送的ping数据帧后,需要及时回复一个内容一致,且OPCODE标记为pong的数据帧,告诉对方我还在。但目前回复pong是浏览器的自动行为,意味着不同浏览器会有差异。而且在js中没有相关api可以控制。




综上所述,探测对方存活的方式都是服务器主动进行心跳检测。浏览器并没有提供相关能力。为了能够在浏览器端实时探测后端的存活,或者说连接依旧可用,只能自己实现心跳检测。


浏览器端心跳检测的必要性


首先我们先了解一下,目前的浏览器端的WebSocket何时会自动关闭WebSocket,并触发close事件呢?



  • 握手时的WebSocket地址不可用。

  • 其它未知错误。

  • 正常连接状态下,接收到服务器端的关闭帧就会触发关闭回调。


也就是说建立正常连接后,中途浏览器端断网了,或者服务器没有发送关闭帧就关了连接,总之就是在连接无法再使用的情况下,浏览器没有接收到关闭帧,浏览器则会长时间保持连接状态。此时业务代码不去主动探测的话,是无法感知的。


另外通讯双方保持连接意味着需要长时间占用对方的资源。对于服务器端来说资源是非常宝贵的。长时间不活跃的连接,可能会被服务器应用层框架"优化"释放掉。


前端实现心跳检测


实例化一个WebSocket:


function connectWS() {
const WS = new WebSocket("ws://127.0.0.1:7070/ws/?name=greaclar");
// WebSocket实例上的事件

// 当连接成功打开
WS.addEventListener('open', () => {
console.log('ws连接成功');
});
// 监听后端的推送消息
WS.addEventListener('message', (event) => {
console.log('ws收到消息', event.data);
});
// 监听后端的关闭消息,如果发送意外错误,这里也会触发
WS.addEventListener('close', () => {
console.log('ws连接关闭');
});
// 监听WS的意外错误消息
WS.addEventListener('error', (error) => {
console.log('ws出错', error);
});
return WS;
}

let WS = connectWS();

心跳检测需要用到的实例方法:


// 发送消息,用来发送心跳包
WS.send('hello');
// 关闭连接,当发送心跳包不响应,需要重连时,最好先关闭
WS.close();

定义发送心跳包的逻辑:


准备



  • 申请一个变量heartbeatStatus,记录当前心跳检测状态,有三个状态:等待中,已收到应答、超时。

  • 监听WS实例的message事件,监听到就将heartbeatStatus改为:已收到应答。

  • 监听WS实例的open事件,打开后启动心跳检测。


检测




  • 启动一个定时器A。




  • 定时器A执行,1.修改当前状态heartbeatStatus为等待中;2.发送心跳包;3.启动一个定时器B。



    • 发送心跳包后,后端需要立刻推送一个内容一样的心跳应答包给前端,触发前端WS实例的message事件,继而将heartbeatStatus改为已收到应答。




  • 定时器B执行,检测当前heartbeatStatus状态:




    • 如果是已收到应答,证明定时器A执行后,服务器可以及时响应数据。继续启动定时器A,然后不断循环。




    • 如果是等待中,证明连接出现问题了,走关闭或者检测流程。






let WS = connectWS();
let heartbeatStatus = 'waiting';

WS.addEventListener('open', () => {
// 启动成功后开启心跳检测
startHeartbeat()
})

WS.addEventListener('message', (event) => {
const { data } = event;
console.log('心跳应答了,要把状态改为已收到应答', data);
if (data === '"heartbeat"') {
heartbeatStatus = 'received';
}
})

function startHeartbeat() {
setTimeout(() => {
// 将状态改为等待应答,并发送心跳包
heartbeatStatus = 'waiting';
WS.send('heartbeat');
// 启动定时任务来检测刚才服务器有没有应答
waitHeartbeat();
}, 1500)
}

function waitHeartbeat() {
setTimeout(() => {
console.log('检测服务器有没有应答过心跳包,当前状态', heartbeatStatus);
if (heartbeatStatus === 'waiting') {
// 心跳应答超时
WS.close();
} else {
// 启动下一轮心跳检测
startHeartbeat();
}
}, 1500)
}

优化心跳检测


心跳检测异常,但close事件没有触发,大概率是双方之间的网络线路不佳,如果立马进行重连,会挤兑更多的网络资源,重连的失败概率更大,也可能阻塞用户的其它操作。


但也不排除确实是连接的问题,如服务器宕机、意外重启,同时没有告知浏览器需要把旧连接关闭。


所以一发生心跳不应答,个人推荐的做法是,发生延迟后,提醒用户网络异常正在修复中,让用户有个心理准备。然后多发一两个心跳包,连续不应答再提示用户掉线了,是否重连。如果中途正常了,就不需要重连,用户体验更好,对服务器的压力也更小。


// 以上代码需要修改的地方

// 添加一个变量来记录连续不应答次数
let retryCount = 0

WS.addEventListener('message', (event) => {
const { data } = event;
console.log('心跳应答了,要把状态改为已收到应答', data);
if (data === '"heartbeat"') {
// 复位连续不应答次数
retryCount = 0
heartbeatStatus = 'received';
}
})

// 在等待应答的函数中添加重试的逻辑
function waitHeartbeat() {
setTimeout(() => {
// 心跳应答正常,启动下一轮心跳检测
if (heartbeatStatus === 'received') {
return startHeartbeat();
}
// 更新超时次数
retryCount ++;
// 心跳应答超时,但没有连续超过三次
if (retryCount < 3) {
alert('ws线路异常,正在检测中。')
return startHeartbeat();
}

// 超时次数超过三次
WS.close();
}, 1500)
}

最后,为了方便大家共同进步,本文已经把相关的逻辑封装为一个类,并且在npm中可下载玩一

作者:小龟壳阿特greaclar
来源:juejin.cn/post/7268864806558515237
下,也已经开源到github上。

收起阅读 »

《如何超过大多数人》——陈皓(左耳朵耗子)

提前声明本篇文章为转发文章,作者为陈浩(网名又叫左耳朵耗子),文章出处为:酷 壳 – CoolShell。 文章原文链接为:如何超过大多数人 ps:读这篇文章前先看看下面这段话,避免误导大家。 切记,这篇文章不要过度深思(任何东西都无法经得起审视,因为这世上...
继续阅读 »

提前声明本篇文章为转发文章,作者为陈浩(网名又叫左耳朵耗子),文章出处为:酷 壳 – CoolShell


文章原文链接为:如何超过大多数人



ps:读这篇文章前先看看下面这段话,避免误导大家。


切记,这篇文章不要过度深思(任何东西都无法经得起审视,因为这世上没有同样的成长环境,也没有同样的认知水平同时也没有适用于所有人的解决方案;也不要去急着评判里面列出的观点,只需代入到其中适度审视一番自己即可,能跳脱出来从外人的角度看看现在的自己处在什么样的阶段就行。具体怎么想怎么做全在你自己去不断实践中寻找那个适合自己的方案



正文开始:


当你看到这篇文章的标题,你一定对这篇文章产生了巨大的兴趣,因为你的潜意识在告诉你,这是一本人生的“武林秘籍”,而且还是左耳朵写的,一定有干货满满,只要读完,一定可以练就神功并找到超过大多数人的快车道和捷径……


然而…… 当你看到我这样开篇时,你一定会觉得我马上就要有个转折,告诉你这是不可能的,一切都需要付出和努力……然而,你错了,这篇文章还真就是一篇“秘籍”,只要你把这些“秘籍”用起来,你就一定可以超过大多数人。而且,这篇文章只有我这个“人生导师”可以写得好。毕竟,我的生命过到了十六进制2B的年纪,踏入这个社会已超过20年,舍我其谁呢?!


P.S. 这篇文章借鉴于《如何写出无法维护的代码》一文的风格……嘿嘿


相关技巧和最佳实践


要超过别人其实还是比较简单的,尤其在今天的中国,更是简单。因为,你只看看中国的互联网,你就会发现,他们基本上全部都是在消费大众,让大众变得更为地愚蠢和傻瓜。所以,在今天的中国,你基本上不用做什么,只需要不使用中国互联网,你就很自然地超过大多数人了。当然,如果你还想跟他们彻底拉开,甩他们几个身位,把别人打到底层,下面的这些“技巧”你要多多了解一下。


在信息获取上,你要不断地向大众鼓吹下面的这些事:



ps:是像大众哈。不要看错用在自己身上【狗头】


自己怎么做呢?反着做不就行了吗》》》


  • 让大家都用百度搜索引擎查找信息,订阅微信公众号或是到知乎上学习知识……要做到这一步,你就需要把“百度一下”挂在嘴边,然后要经常在群或朋友圈中转发微信公众号的文章,并且转发知乎里的各种“如何看待……”这样的文章,让他们爱上八卦,爱上转发,爱上碎片
  • 让大家到微博或是知识星球上粉一些大咖,密切关注他们的言论和动向…… 是的,告诉大家,大咖的任何想法一言一行都可以在微博、朋友圈或是知识星球上获得,让大家相信,你的成长和大咖的见闻和闲扯非常有关系,你跟牛人在一个圈子里你也会变牛。
  • 把今日头条和抖音这样的APP推荐给大家……你只需要让你有朋友成功地安装这两个APP,他们就会花大量的时间在上面,而不能自拔,要让他们安装其实还是很容易的,你要不信你就装一个试玩一会看看(嘿嘿嘿)。
  • 让大家热爱八卦,八卦并不一定是明星的八卦,还可以是你身边的人,比如,公司的同事,自己的同学,职场见闻,社会热点,争议话题,……这些东西总有一些东西会让人心态有很多微妙的变化,甚至花大量的时间去搜索和阅读大量的观点,以及花大量时间与人辩论争论,这个过程会让人上瘾,让人欲罢不能,然而这些事却和自己没有半毛钱关系。你要做的事就是转发其中一些SB或是很极端的观点,造成大家的一睦讨论后,就早早离场……
  • 利用爱国主义,让大家觉得不用学英文,不要出国,不要翻墙,咱们已经是强国了……这点其实还是很容易做到的,因为学习是比较逆人性的,所以,只要你鼓吹那些英文无用论,出国活得更惨,国家和民族都变得很强大,就算自己过得很底层,也有大国人民的感觉。

然后,在知识学习和技能训练上,让他们不得要领并产生幻觉

  • 让他们混淆认识和知识,以为开阔认知就是学习,让他们有学习和成长的幻觉……

  • 培养他们要学会使用碎片时间学习。等他们习惯利用碎片时间吃快餐后,他们就会失去精读一本书的耐性……
  • 不断地给他们各种各样“有价值的学习资料”,让他们抓不住重点,成为一个微信公众号或电子书“收藏家”……
  • 让他们看一些枯燥无味的基础知识和硬核知识,这样让他们只会用“死记硬背”的方式来学习,甚至直接让他们失去信心,直接放弃……
  • 玩具手枪是易用的,重武器是难以操控的,多给他们一些玩具,这样他们就会对玩具玩地得心应手,觉得玩玩具就是自己的专业……
  • 让他们喜欢直接得到答案的工作和学习方式,成为一个伸手党,从此学习再也不思考……
  • 告诉他们东西做出来就好了,不要追求做漂亮,做优雅,这样他们就会慢慢地变成劳动密集型……
  • 让他们觉得自己已经很努力了,剩下的就是运气,并说服他们去‘及时行乐’,然后再也找不到高阶和高效率学习的感觉……
  • 让他们觉得读完书”、“读过书”就行了,不需要对书中的东西进行思考,进行总结,或是实践,只要囫囵吞枣尽快读完就等同于学好了……

最后,在认知和格局上,彻底打垮他们,让他们变成韭菜。

  • 让他们尽可能地用拼命和加班,尽可能的996,并告诉他们这就是通往成功的唯一路径。这样一来,他们必然会被永远困在低端成为最低的劳动力

  • 让他们不要看到大的形势,只看到眼前的一亩三分地,做好一个井底之蛙。其实这很简单,就是不要告诉他还有另外一种活法,不要扩大他的认识……
  • 宣扬一夜暴富以及快速挣钱的案例,最好让他们进入“赌博类”或是“传销类”的地方,比如:股市、数字货币……要让他们相信各种财富神话,相信他们就是那个幸运儿,他们也可以成为巴菲特,可以成为马云……
  • 告诉他们,一些看上去很难的事都是有捷径的,比如:21天就能学会机器学习,用区块链就能颠覆以及重构整个世界等等……
  • 多跟他们讲一些小人物的励志的故事,这样让他们相信,不需要学习高级知识,不需要掌握高级技能,只需要用低等的知识和低级的技能,再加上持续不断拼命重复现有的工作,终有一天就会成功……
  • 多让他们跟别人比较,人比人不会气死人,但是会让人变得浮躁,变得心急,变得焦虑,当一个人没有办法控制自己的情绪,没有办法让自己静下心来,人会失去耐性和坚持,开始好大喜欢功,开始装逼,开始歪门邪道剑走偏锋……
  • 让他们到体制内的一些非常稳定的地方工作,这样他们拥有不思进取、怕承担责任、害怕犯错、喜欢偷懒、得过且过的素质……
  • 让他们到体制外的那些喜欢拼命喜欢加班的地方工作,告诉他们爱拼才会赢,努力加班是一种福报,青春就是用来拼的,让他们喜欢上使蛮力的感觉……
  • 告诉他们你的行业太累太辛苦,干不到30岁。让他们早点转行,不要耽误人生和青春……
  • 当他们要做决定的时候,一定要让他们更多的关注自己会失去的东西,而不是会得到的东西。培养他们患得患失心态,让他们认识不到事物真正的价值,失去判断能力……(比如:让他们觉得跟对人拍领导的马屁忠于公司比自我的成长更有价值)
  • 告诉他们,你现有的技能和知识不用更新,就能过好一辈子,新出来的东西没有生命力的……这样他们就会像我们再也不学习的父辈一样很快就会被时代所抛弃……
  • 每个人都喜欢在一些自己做不到的事上找理由,这种能力不教就会,比如,事情太多没有时间,因为工作上没有用到,等等,你要做的就是帮他们为他们做不到的事找各种非常合理的理由,比如:没事的,一切都是最好的安排;你得不到的那个事没什么意思;你没有面好主要原因是那个面试官问的问题都是可以上网查得到的知识,而不没有问到你真正的能力上;这些东西学了不用很快会忘了,等有了环境再学也不迟……

最后友情提示一下,上述的这些“最佳实践”你要小心,是所谓,贩毒的人从来不吸毒,开赌场的人从来不赌博!所以,你要小心别自己也掉进去了!这就是“欲练神功,必先自宫”的道理。


相关原理和思维模型


对于上面的这些技巧还有很多很多,你自己也可以发明或是找到很多。所以,我来讲讲这其中的一些原理。


一般来说,超过别人一般来说就是两个维度:

  1. 在认知、知识和技能上。这是一个人赖以立足社会的能力(参看《程序员的荒谬之言还是至理名言?》和《21天教你学会C++》)
  2. 在领导力上。所谓领导力就是你跑在别人前面,你得要有比别人更好的能力更高的标准(参看《技术人员发展之路》)

首先,我们要明白,人的技能是从认识开始,然后通过学校、培训或是书本把“零碎的认知”转换成“系统的知识”,而有要把知识转换成技能,就需要训练和实践,这样才能完成从:认识 -> 知识 -> 技能 的转换。


这个转换过程是需要耗费很多时间和精力的,而且其中还需要有强大的学习能力和动手能力,这条路径上有很多的“关卡”,每道关卡都会过滤掉一大部分人。比如:对于一些比较枯燥的硬核知识来说,90%的人基本上就倒下来,不是因为他们没有智商,而是他们没有耐心。


认知


要在认知上超过别人,就要在下面几个方面上做足功夫:


1)信息渠道。试想如果别人的信息源没有你的好,那么,这些看不见信息源的人,只能接触得到二手信息甚至三手信息,只能获得被别人解读过的信息,这些信息被三传两递后必定会有错误和失真,甚至会被传递信息的中间人hack其中的信息(也就是“中间人攻击”),而这些找不出信息源的人,只能“被人喂养”,于是,他们最终会被困在信息的底层,永世不得翻身。(比如:学习C语言,放着原作者K&R的不用,硬要用错误百出谭浩强的书,能有什么好呢?)


2)信息质量。信息质量主要表现在两个方面,一个是信息中的燥音,另一个是信息中的质量等级,我们都知道,在大数据处理中有一句名言,叫 garbage in garbage out,你天天看的都是垃圾,你的思想和认识也只有垃圾。所以,如果你的信息质量并不好的话,你的认知也不会好,而且你还要花大量的时间来进行有价值信息的挖掘和处理。


3)信息密度。优质的信息,密度一般都很大,因为这种信息会逼着你去干这么几件事,


a)搜索并学习其关联的知识


b)沉思和反省


c)亲手去推理、验证和实践……


一般来说,经验性的文章会比知识性的文章会更有这样的功效。比如,类似于像 Effiective C++/Java,设计模式,Unix编程艺术,算法导论等等这样的书就是属于这种密度很大的书,而像Netflix的官方blogAWS CTO的blog等等地方也会经常有一些这样的文章。


知识


要在知识上超过别人,你就需要在下面几个方面上做足功夫:


1)知识树(图)任何知识,只在点上学习不够的,需要在面上学习,这叫系统地学习,这需要我们去总结并归纳知识树或知识图,一个知识面会有多个知识板块组成,一个板块又有各种知识点,一个知识点会导出另外的知识点,各种知识点又会交叉和依赖起来,学习就是要系统地学习整个知识树(图)。而我们都知道,对于一棵树来说,“根基”是非常重要的,所以,学好基础知识也是非常重要的,对于一个陌生的地方,有一份地图是非常重要的,没有地图的你只会乱窜,只会迷路、练路、走冤枉路!


2)知识缘由。任何知识都是有缘由的,了解一个知识的来龙去脉和前世今生,会让你对这个知识有非常强的掌握,而不再只是靠记忆去学习。靠记忆去学习是一件非常糟糕的事。而对于一些操作性的知识(不需要了解由来的),我把其叫操作知识,就像一些函数库一样,这样的知识只要学会查文档就好了。能够知其然,知其所以然的人自然会比识知识到表皮的人段位要高很多。


3)方法套路学习不是为了找到答案,而是找到方法。就像数学一样,你学的是方法,是解题思路,是套路,会用方程式解题的和不会用方程式解题的在解题效率上不可比较,而在微积分面前,其它的解题方法都变成了渣渣。你可以看到,掌握高级方法的人比别人的优势有多大,学习的目的就是为了掌握更为高级的方法和解题思路


技能

要在技能上超过别人,你就需要在下面几个方面做足功夫:


1)精益求精。如果你想拥有专业的技能,你要做不仅仅是拼命地重复一遍又一遍的训练,而是在每一次重复训练时你都要找到更好的方法,总结经验,让新的一遍能够更好,更漂亮,更有效率,否则,用相同的方法重复,那你只不过在搬砖罢了。


2)让自己犯错。犯错是有利于成长的,这是因为出错会让人反思,反思更好的方法,反思更完美的方案,总结教训,寻求更好更完美的过程,是技能升级的最好的方式。尤其是当你在出错后,被人鄙视,被人嘲笑后,你会有更大的动力提升自己,这样的动力才是进步的源动力。当然,千万不要同一个错误重复地犯!


3)找高手切磋。下过棋,打个球的人都知道,你要想提升自己的技艺,你必需找高手切磋,在和高手切磋的过程中你会感受到高手的技能和方法,有时候你会情不自禁地哇地一下,我靠,还可以这么玩!


领导力


最后一个是领导力,要有领导力或是影响力这个事并不容易,这跟你的野心有多大,好胜心有多强 ,你愿意付出多少很有关系,因为一个人的领导力跟他的标准很有关系,因为有领导力的人的标准比绝大多数人都要高。


1)识别自己的特长和天赋。首先,每个人DNA都可能或多或少都会有一些比大多数人NB的东西(当然,也可能没有),如果你有了,那么在你过去的人生中就一定会表现出来了,就是那种大家遇到这个事会来请教你的寻求你帮助的现象。那种,别人要非常努力,而且毫不费劲的事。一旦你有了这样的特长或天赋,那你就要大力地扩大你的领先优势,千万不要进到那些会限制你优势的地方。你是一条鱼,你就一定要把别人拉到水里来玩,绝对不要去陆地上跟别人拼,不断地在自己的特长和天赋上扩大自己的领先优势,彻底一骑绝尘。


2)识别自己的兴趣和事业。没有天赋也没有问题,还有兴趣点,都说兴趣是最好的老师,当年,Linus就是在学校里对minx着迷了,于是整出个Linux来,这就是兴趣驱动出的东西,一般来说,兴趣驱动的事总是会比那些被动驱动的更好。但是,这里我想说明一下什么叫“真∙兴趣”,真正的兴趣不是那种三天热度的东西,而是那种,你愿意为之付出一辈子的事,是那种无论有多大困难有多难受你都要死磕的事,这才是“真∙兴趣”,这也就是你的“野心”和“好胜心”所在,其实上升到了你的事业。相信我,绝大多数人只有职业而没有事业的。


3)建立高级的习惯和方法。没有天赋没有野心,也还是可以跟别人拼习惯拼方法的,只要你有一些比较好的习惯和方法,那么你一样可以超过大多数人。对此,在习惯上你要做到比较大多数人更自律,更有计划性,更有目标性,比如,每年学习一门新的语言或技术,并可以参与相关的顶级开源项目,每个月训练一个类算法,掌握一种算法,每周阅读一篇英文论文,并把阅读笔记整理出来……自律的是非常可怕的。除此之外,你还需要在方法上超过别人,你需要满世界的找各种高级的方法,其中包括,思考的方法,学习的方法、时间管理的方法、沟通的方法这类软实力的,还有,解决问题的方法(trouble shooting 和 problem solving),设计的方法,工程的方法,代码的方法等等硬实力的,一开始照猫画虎,时间长了就可能会自己发明或推导新的方法。


4)勤奋努力执着坚持。如果上面三件事你都没有也没有能力,那还有最后一件事了,那就是勤奋努力了,就是所谓的“一万小时定律”了(参看《21天教你学会C++》中的十年学编程一节),我见过很多不聪明的人,悟性也不够(比如我就是一个),别人学一个东西,一个月就好了,而我需要1年甚至更长,但是很多东西都是死的,只要肯花时间就有一天你会搞懂的,耐不住我坚持十年二十年,聪明的人发明个飞机飞过去了,笨一点的人愚公移山也过得去,因为更多的人是懒人,我不用拼过聪明人,我只用拼过那些懒人就好了。


好了,就这么多,如果哪天你变得消极和不自信,你要来读读我的这篇文章,子曰:温故而知新。


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

Swift Enum 关联值嵌套的一些实践

iOS
前言 Swift 中的枚举很强大,算是一等公民。可以定义函数,也可以遵守协议、实现 extension 等等。 关联值也是 Swift 枚举的一大特性。基本用法如下:enum RequestResult { case success case ...
继续阅读 »

前言


Swift 中的枚举很强大,算是一等公民。可以定义函数,也可以遵守协议、实现 extension 等等。


关联值也是 Swift 枚举的一大特性。基本用法如下:

enum RequestResult {
case success
case failure(Error)
}

let result = RequestResult.failure(URLError(URLError.timedOut))
switch result {
case .success:
print("请求成功")
case .failure(let error):
print(error)
}

1、在需要关联值的 case 中声明关联值的类型。


2、在 switch 的 case 中声明一个常量或者变量来接收。


遇到的问题


一般情况下,上述的代码是清晰明了的。但在实际开发的过程中,遇到了以下的情况:关联值的类型也是枚举,而且嵌套不止一层。


比如下面的代码:

enum EnumT1 {
case test1(EnumT2)
case other
}

enum EnumT2 {
case test2(EnumT3)
case other2
}

enum EnumT3 {
case test3(EnumT4)
case test4
}

根据我们的需求,需要进行多次嵌套来进行类型细化。当进行枚举的声明时,代码还是正常的,简单明了。但当进行 case 判断时,代码就变得丑陋难写了。


比如,我只想处理 EnumT3 中的 test4 的情况,在 switch 中我需要进行 switch 的嵌套来处理:

let t1: EnumT1? = .test1(.test2(.test4))
switch t1 {
case .test1(let t2):
switch t2 {
case .test2(let t3):
switch t3 {
case .test4:
print("test4")
case default:
print("default")
}
default:
print("default")
}
default:
print("default")
}

这种写法,对于一个程序员来说是无法忍受的。它存在两个问题:一是代码臃肿,我的本意是只处理某一种情况,但我需要显式的嵌套多层 switch;二是枚举本身是不推荐使用 default 的,官方推荐是显式的写出所有的 case,以防出现难以预料的问题。


废话不多说,下面开始简化之路。


实践一


首先能想到的是,因为是对某一种情况进行处理,考虑使用 if + == 的判断来进行处理,比如下面这种写法:

if t1 == .test1(.test2(.test4)) { }

这样处理有两个不足之处。首先,如果对枚举用 == 操作符的话,需要对每一个枚举都遵守 Equatable 协议,这为我们带来了工作量。其次最重要的是,这种处理方式无法应对 test3 这种带有关联值的情况。

if t1 == .test1(.test2(.test3) { } 

如果这样写的话,编译器会报错,因为 test3 是需要传进去一个 Int 值的。

if t1 == .test1(.test2(.test3(20))) { }

如果这样写的话也不行,因为我们的需求是处理 test3 的统一情况(所有的关联值),而不是某一个具体的关联值。


实践二


经过在网上的一番搜查,发现可以用 if-case 关键字来简化写法:

if case .test1(.test2(.test3)) = t1 { }

这样就能统一处理 test3 这个 case 的所有情况了。如果想获取关联值,可以用下面的写法:

if case .test1(.test2(.test3(let i))) = t1 {
print(i)
}

对比上面的 switch 写法,可以看到,下面的这种写法既易懂又好写😁。


总结来说,当我们遇到关联值多层枚举嵌套的时候,又需要对某一种情况进行处理。那么可以采用实践二的做法来进行代码简化。


参考链接


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

做点微小的工作,实现 iOS 日历和提醒事项双向同步

iOS
前言 作为一名资深谷粉和十年的 Android 用户,在 2020 年看着各家厂商在笔记本、手机、手表、耳机甚至是智能家居上不断推成出新,补齐数字生活的每一块拼图,辅以“生态化反”的概念牢牢绑住每一个入坑的用户,此时再看看自己手里孤身寡人的 Pixel 手机,...
继续阅读 »

前言


作为一名资深谷粉和十年的 Android 用户,在 2020 年看着各家厂商在笔记本、手机、手表、耳机甚至是智能家居上不断推成出新,补齐数字生活的每一块拼图,辅以“生态化反”的概念牢牢绑住每一个入坑的用户,此时再看看自己手里孤身寡人的 Pixel 手机,以及不知何时就被砍掉的 Pixelbook 系列,默默留下了悔恨的泪水。久苦于谷歌令人失望的硬件生态,我终于还是放弃了 Android 生态,转身拥抱苹果全家桶。苹果硬件生态品类齐全,多年深耕的软件生态和云服务也赋予了这些硬件无缝的使用体验。但有一点一直令我不解,那就是 iOS 的自带应用:日历和提醒事项,它们的事件竟不是相互联动的。而在谷歌套件中,只要一个任务在 Google Tasks 中被新增或是被勾选完成,就会自动同步到 Google Calendar 中,以方便用户进行日程安排或是日程回顾。虽然第三方应用如滴答清单、Sunsama 也提供了类似的功能,但为了原生(免费)体验,只能自己动手折腾了。


前提条件


为了在 iOS 上实现日历和提醒事项双向同步的效果,需要借助快捷指令,搭配 JSBox 写一个脚本,创建数据库来绑定和管理日历和提醒事项中各自的事件。

  1. iOS 14+;
  2. 愿意花 40 RMB 开通 JSBox 高级版;
  3. 不满足第2点,则需要设备已越狱,或者装有 TrollStore;

*破解 JSBox


步骤:

  1. 在 App Store 安装 JSBox;
  2. 通过越狱的包管理工具或者 TrollStore 安装 Apps Manager;
  3. 下载 JSBox 备份文件,在文件管理中长按该文件,选择分享,使用 Apps Manager 打开,在弹出的菜单中点取消;
  4. 在 Apps Manager 中的 Applications 选项卡中,选择 JSBox,点击 Restore 进行还原,即可使用 JSBox 高级版功能(在 JSBox 中的设置选项卡中不要点击“JSBox 高级版”选项,否则需要再次还原);



加载脚本


步骤:

  1. 下载 Reminders ↔️ Calendar 项目文件,在文件管理中长按该文件,选择分享,使用 JSBox 打开;
  2. 在日历和提醒事项中各自新建一个“test”列表,在提醒事项的“test”列表中新建一个定时事件;
  3. 返回 JSBox 中的 Reminders ↔️ Calendar 项目,点击界面下的“Sync now”按钮;
  4. 回到日历中查看事件是否同步成功;



设置项说明:

  1. 同步周期 —— 周期内的事件才会被同步;
  2. 同步备注 —— 是否同步日历和提醒事项的备注;
  3. 同步删除 —— 删除一方事件时,是否自动删除另一方对应的事件;
  4. 单边提醒 —— 日历和提醒事项的事件,谁创建谁通知,关闭则日历和提醒事项都会通知;
  5. 历史待办默认超期完成 —— 补录历史待办,是否默认为已完成;
  6. 提醒事项:默认优先级 —— 在日历创建的事件,同步到提醒事项时候默认的优先级;
  7. 日历:默认用时 —— 在提醒事项创建的事件,同步到日历时默认的时间间隔;
  8. 日历:快速跳转 —— 日历的事件是否在链接项中添加跳转到对应提醒事项的快速链接;
  9. 日历:显示剩余时间 —— 日历的事件是否在地点项中添加时间信息;
  10. 日历:完成变全天 —— 日历的事件是否在完成时,自动变成全天事件(这样日历视图就会将该项目置顶,方便查看未完成项目);



设置快捷指令


步骤:

  1. 打开快捷指令应用,选择自动化选项卡,点击右上角 + 号新增一个任务;
  2. 选择新建个人自动化,设置触发条件为打开应用,指定应用为日历和提醒事项,点击下一步;
  3. 点击按钮新增一个行动,选择执行 JSBox 脚本,在脚本名上填入“Reminders ↔️ Calendar”,点击右下角的 ▶️ 测试,如果输出成功则点击下一步;(注意区分执行 JSBox 脚本和执行 JSBox 界面);
  4. 关闭执行前询问的选项,点击右上角的完成保存任务;



总结


JSBox 是一款运行在 iOS 设备上的轻量级脚本编辑器和开发环境。它内置了大量的 API,允许用户使用 JavaScript 访问原生的 iOS API。另一款相似的应用 Scriptable 在语法的书写上更亲和,但其暴露的事件对象中缺少 last modified 字段,当信息不对称时,没有办法判断日历和提醒事项中事件的新旧。期待 Scriptable 的后续更新,毕竟它是免费的🤡。


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

在国企做程序员怎么样?

本文已经收录到Github仓库,该仓库包含计算机基础、Java核心知识点、多线程、JVM、常见框架、分布式、微服务、设计模式、架构等核心知识点,欢迎star~ Github地址:github.com/Tyson0314/J… Gitee地址:gitee.com...
继续阅读 »

本文已经收录到Github仓库,该仓库包含计算机基础、Java核心知识点、多线程、JVM、常见框架、分布式、微服务、设计模式、架构等核心知识点,欢迎star~


Github地址:github.com/Tyson0314/J…


Gitee地址:gitee.com/tysondai/Ja…



有读者咨询我,在国企做开发怎么样?


当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。


下面分享一位国企程序员的经历,希望能给大家一些参考价值。



下文中的“我”代表故事主人公



我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。


在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。


1、大量内部项目


在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。


在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。


2、外包


说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。


直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。


上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。


3、技术栈


在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。


所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。


4、升职空间


每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。


首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。


其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。


最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。


5、钱


在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。


1.工资构成中没有绩效,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。


2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。


3.最后就是福利了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。


总结


1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。


2、国企搞开发,技术不会特别新,很多时候是项目管理的角色。工作内容基本体现为领导的决定。


3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。


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

像支付宝那样“致敬”第三方开源代码

前言 通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源...
继续阅读 »

前言


通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源代码的任何信息。 



不过,作为一个有“追求”的码农,我们还是想对开源软件致敬一下的,毕竟,没有他们我都不知道怎么写代码。然而,我们的 App 里用了那么多第三方开源插件,总不能一个个找出来一一致敬吧?怎么办?其实,Flutter 早就为我们准备好了一个组件,那就是本篇要介绍的 AboutDialog


AboutDialog 简介


AboutDialog 是一个对话框,它可以提供 App 的基本信息,如 Icon、版本、App 名称、版权信息等。 



同时,AboutDialog还提供了一个查看授权信息(View Licenses)的按钮,点击就可以查看 App 里所有用到的第三方开源插件,并且会自动收集他们的 License 信息展示。所以,使用 AboutDialog 可以让我们轻松表达敬意。怎么使用呢?非常简单,我们点击一个按钮的时候,调用 showAboutDialog 就搞定了,比如下面的代码:

IconButton(
onPressed: () {
showAboutDialog(
context: context,
applicationName: '岛上码农',
applicationVersion: '1.0.0',
applicationIcon: Image.asset('images/logo.png'),
applicationLegalese: '2023 岛上码农版权所有'
);
},
icon: const Icon(
Icons.info_outline,
color: Colors.white,
),
),

参数其实一目了然,具体如下:

  • context:当前的 context
  • applicationName:应用名称;
  • applicationVersion:应用版本,如果要自动获取版本号也可以使用 package_info_plus 插件。
  • applicationIcon:应用图标,可以是任意的 Widget,通常会是一个App 图标图片。
  • applicationLegalese:其他信息,通常会放置应用的版权信息。

点击按钮,就可以看到相应的授权信息了,点击一项就可以查看具体的 License。我看了一下使用的开源插件非常多,要是自己处理还真的很麻烦。 



可以说非常简单,当然,如果你直接运行还有两个小问题。


按钮本地化


AboutDialog 默认提供了两个按钮,一个是查看授权信息,一个是关闭,可是两个按钮 的标题默认是英文的(分别是VIEW LICENSES和 CLOSE)。 



如果要改成本地话的,还需要做一个自定义配置。我们扒一下 AboutDialog 的源码,会发现两个按钮在DefaultMaterialLocalizations中定义,分别是viewLicensesButtonLabelcloseButtonLabel。这个时候我们自定义一个类集成DefaultMaterialLocalizations就可以了。

class MyMaterialLocalizationsDelegate
extends LocalizationsDelegate<MaterialLocalizations> {
const MyMaterialLocalizationsDelegate();

@override
bool isSupported(Locale locale) => true;

@override
Future<MaterialLocalizations> load(Locale locale) async {
final myTranslations = MyMaterialLocalizations(); // 自定义的本地化资源类
return Future.value(myTranslations);
}

@override
bool shouldReload(
covariant LocalizationsDelegate<MaterialLocalizations> old) =>
false;
}

class MyMaterialLocalizations extends DefaultMaterialLocalizations {
@override
String get viewLicensesButtonLabel => '查看版权信息';

@override
String get closeButtonLabel => '关闭';

}

然后在 MaterialApp 里指定本地化localizationsDelegates参数使用自定义的委托类对象就能完成AboutDialog两个按钮文字的替换。

return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const AboutDialogDemo(),
localizationsDelegates: const [MyMaterialLocalizationsDelegate()],
);

添加自定义的授权信息


虽然 Flutter 会自动收集第三方插件,但是如果我们自己使用了其他第三方的插件的话,比如没有在 pub.yaml 里引入,而是直接使用了源码。那么还是需要手动添加一些授权信息的,这个时候我们需要自己手动添加了。添加的方式也不麻烦,Flutter 提供了一个LicenseRegistry的工具类,可以调用其 addLicense 方法来帮我们添加授权信息。具体使用如下:

LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。\f如有问题可以加本人微信交流,微信号:island-coder。',
);
});

这个方法可以在main方法里调用。其中第一个参数是一个数组,是因为可以允许多个开源代码共用一份授权信息。同时,如果一份开源插件有多个授权信息,可以多次添加,只要名称一致,Flutter就会自动合并,并且会显示该插件的授权信息条数,点击查看时,会将多条授权信息使用分割线分开,代码如下所示:

void main() {
runApp(const MyApp());
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder。',
);
});

LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
['关于岛上码农'],
'使用时请注明来自岛上码农、。',
);
});
}



总结


本篇介绍了在 Flutter 中快速展示授权信息的方法,通过 AboutDialog 就可以轻松搞定,各位“抄代码”的码农们,赶紧用起来向大牛们致敬吧!


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

微信(群)接入ChatGPT,MJ聊天机器人Bot

前言 微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,...
继续阅读 »

前言


微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。



注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入



首先你需要一个 OpenAI 的账号并且创建一个可用的 api key,这里不做过多介绍,有任何问题可以加博客首页公告处微信群进行沟通。


相关的聊天机器人Bot GitHub上有非常多的项目,不仅支持接入ChatGPT,还支持接入MJ画图等一些其他功能。


本篇介绍两个项目(我用的第一个 chatgpt-on-wechat 项目):


chatgpt-on-wechat 项目最新版支持如下功能:

  • 多端部署: 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式
  • 基础对话: 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3,GPT-3.5,GPT-4模型
  • 语音识别: 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai等多种语音模型
  • 图片生成: 支持图片生成 和 图生图(如照片修复),可选择 Dell-E, stable diffusion, replicate模型
  • 丰富插件: 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结等插件
  • Tool工具: 与操作系统和互联网交互,支持最新信息搜索、数学计算、天气和资讯查询、网页总结,基于 chatgpt-tool-hub 实现

支持 Linux、MacOS、Windows 系统(可在Linux服务器上长期运行),同时需安装 Python。



建议Python版本在 3.7.1~3.9.X 之间,推荐3.8版本,3.10及以上版本在 MacOS 可用,其他系统上不确定能否正常运行。


注意:Docker 或 Railway 部署无需安装python环境和下载源码



Windows、Linux、Mac本地部署


本地部署请参考官方文档,按照文档一步一步操作即可。


注意要安装相对应的环境,例如 Node、Python等,这里不做过多介绍,建议大家用 Docker 方式安装,无需关心环境问题,一个命令直接部署。


环境变量

# config.json文件内容示例
{
"open_ai_api_key": "YOUR API KEY", # 填入上面创建的 OpenAI API KEY
"model": "gpt-3.5-turbo", # 模型名称。当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
"proxy": "127.0.0.1:7890", # 代理客户端的ip和端口
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
"group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
"speech_recognition": false, # 是否开启语音识别
"group_speech_recognition": false, # 是否开启群组语音识别
"use_azure_chatgpt": false, # 是否使用Azure ChatGPT service代替openai ChatGPT service. 当设置为true时需要设置 open_ai_api_base,如 https://xxx.openai.azure.com/
"azure_deployment_id": "", # 采用Azure ChatGPT时,模型部署名称
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述
# 订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复,可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。
"subscribe_msg": "感谢您的关注!\n这里是ChatGPT,可以自由对话。\n支持语音对话。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持角色扮演和文字冒险等丰富插件。\n输入{trigger_prefix}#help 查看详细指令。"
}

配置说明:


1.个人聊天

  • 个人聊天中,需要以 "bot"或"@bot" 为开头的内容触发机器人,对应配置项 single_chat_prefix (如果不需要以前缀触发可以填写 "single_chat_prefix": [""])
    • 机器人回复的内容会以 "[bot] " 作为前缀, 以区分真人,对应的配置项为 single_chat_reply_prefix (如果不需要前缀可以填写 "single_chat_reply_prefix": "")

    2.群组聊天

  • 群组聊天中,群名称需配置在 group_name_white_list 中才能开启群聊自动回复。如果想对所有群聊生效,可以直接填写 "group_name_white_list": ["ALL_GROUP"]
    • 默认只要被人 @ 就会触发机器人自动回复;另外群聊天中只要检测到以 "@bot" 开头的内容,同样会自动回复(方便自己触发),这对应配置项 group_chat_prefix
    • 可选配置: group_name_keyword_white_list配置项支持模糊匹配群名称,group_chat_keyword配置项则支持模糊匹配群消息内容,用法与上述两个配置项相同。(Contributed by evolay)
    • group_chat_in_one_session:使群聊共享一个会话上下文,配置 ["ALL_GROUP"] 则作用于所有群聊

    3.语音识别

  • 添加 "speech_recognition": true 将开启语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
    • 添加 "group_speech_recognition": true 将开启群组语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配group_chat_prefix和group_chat_keyword, 支持语音触发画图);
    • 添加 "voice_reply_voice": true 将开启语音回复语音(同时作用于私聊和群聊),但是需要配置对应语音合成平台的key,由于itchat协议的限制,只能发送语音mp3文件,若使用wechaty则回复的是微信语音。

    4.其他配置

  • model: 模型名称,目前支持 gpt-3.5-turbotext-davinci-003gpt-4gpt-4-32k (其中gpt-4 api暂未完全开放,申请通过后可使用)
    • temperature,frequency_penalty,presence_penalty: Chat API接口参数,详情参考OpenAI官方文档。
    • proxy:由于目前 openai 接口国内无法访问,需配置代理客户端的地址,详情参考 #351
    • 对于图像生成,在满足个人或群组触发条件外,还需要额外的关键词前缀来触发,对应配置 image_create_prefix
    • 关于OpenAI对话及图片接口的参数配置(内容自由度、回复字数限制、图片大小等),可以参考 对话接口 和 图像接口 文档,在config.py中检查哪些参数在本项目中是可配置的。
    • conversation_max_tokens:表示能够记忆的上下文最大字数(一问一答为一组对话,如果累积的对话字数超出限制,就会优先移除最早的一组对话)
    • rate_limit_chatgptrate_limit_dalle:每分钟最高问答速率、画图速率,超速后排队按序处理。
    • clear_memory_commands: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
    • hot_reload: 程序退出后,暂存微信扫码状态,默认关闭。
    • character_desc 配置中保存着你对机器人说的一段话,他会记住这段话并作为他的设定,你可以为他定制任何人格 (关于会话上下文的更多内容参考该 issue)
    • subscribe_msg:订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。

    本说明文档可能会未及时更新,当前所有可选的配置项均在该config.py中列出。


    Railway部署



    Railway 每月提供5刀和最多500小时的免费额度,目前大部分账号已无法免费部署


    1. 进入 Railway
    2. 点击 Deploy Now 按钮。
    3. 设置环境变量来重载程序运行的参数,例如open_ai_api_keycharacter_desc

    Docker方式搭建


    如果想一直跑起来这个项目,建议在自己服务器上搭建,如果在自己本地电脑上搭建,电脑关机后就用不了啦,下面演示的是在我服务器上搭建,和在本地搭建步骤是一样的。


    环境准备

    1. 域名、服务器购买
    2. 服务器环境搭建,需要系统安装docker、docker-compose
    3. docker、docker-compose安装:blog.fanjunyang.zone/archives/de…

    创建相关目录


    我自己放在服务器中 /root/docker_data/wechat_bot 文件夹下面

    mkdir -p /root/docker_data/wechat_bot
    cd /root/docker_data/wechat_bot

    创建yml文件


    /root/docker_data/wechat_bot文件夹下面新建docker-compose.yml文件如下:

    version: '2.0'
    services:
    chatgpt-on-wechat:
    image: zhayujie/chatgpt-on-wechat
    container_name: chatgpt-on-wechat
    security_opt:
    - seccomp:unconfined
    environment:
    OPEN_AI_API_KEY: 'YOUR API KEY'
    MODEL: 'gpt-3.5-turbo'
    PROXY: ''
    SINGLE_CHAT_PREFIX: '["bot", "@bot"]'
    SINGLE_CHAT_REPLY_PREFIX: '"[bot] "'
    GROUP_CHAT_PREFIX: '["@bot"]'
    GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
    IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
    CONVERSATION_MAX_TOKENS: 1000
    SPEECH_RECOGNITION: 'False'
    CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
    EXPIRES_IN_SECONDS: 3600
    USE_LINKAI: 'False'
    LINKAI_API_KEY: ''
    LINKAI_APP_CODE: ''

    运行yml文件


    进入/root/docker_data/wechat_bot文件夹下面,运行命令:docker-compose up -d


    或者在任意文件夹下面,运行命令:docker-compose -f /root/docker_data/wechat_bot/docker-compose.yml up -d


    然后服务就跑起来了,运行 sudo docker ps 能查看到 NAMES 为 chatgpt-on-wechat 的容器即表示运行成功。


    使用


    运行以下命令可查看容器运行日志,微信扫描日志中的二维码登录后即可使用:

    sudo docker logs -f chatgpt-on-wechat

    插件使用:

    如果需要在docker容器中修改插件配置,可通过挂载的方式完成,将 插件配置文件 重命名为 config.json,放置于 docker-compose.yml 相同目录下,并在 docker-compose.yml 中的 chatgpt-on-wechat 部分下添加 volumes 映射:

    volumes:
    - ./config.json:/app/plugins/config.json

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

    如何不花钱也能拥有一个属于自己的在线网站、博客🤩🤩🤩

    作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么...
    继续阅读 »

    作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么一个烦恼,他能让我们不花钱也能拥有自己的在线网站。


    什么是 GitHub Pages?


    GitHub Pages 是 GitHub 提供的一个托管静态网站的服务。它允许用户将自己的代码仓库转化为一个在线可访问的网站,无需复杂的服务器设置或额外的托管费用。通过 GitHub Pages,我们可以轻松地创建个人网站、项目文档、博客或演示页面,并与其他开发者和用户分享自己的作品。


    使用


    要想使用这个叼功能,我们首先要再 Gayhub 上面建立一个仓库,如下图所示:




    紧接着我们使用 create-neat 来创建一个项目,执行以下命令:

    npx create-neat mmm

    跟着提示选择相对应的项目即可,选择 vue 或者 react 都可以。




    当项目创建成功之后我们进入到该目录并安装相关依赖包:

    pnpm add gh-pages --save-dev

    并在 package.json 文件中添加 homepage 字段,如下所示:

    "homepage": "http://xun082.github.io/mmm"

    其中 xun082 要替换为你自己 github 上面的用户名,如下图所示: 



    而 mmm 替换为我们刚才创建的仓库名称。


    接下来在 package.json 文件中 script 字段中添加如下属性:

      "scripts": {
    "start": "candy-script start",
    "build": "candy-script build",

    "deploy": "gh-pages -d dist"
    },

    完整配置如下所示:




    完成配置后我们将代码先提交到仓库中,如下命令所示:

    git add .

    git commit -m "first commit"

    git branch -M main

    git remote add origin https://github.com/xun082/mmm.git

    git push -u origin main

    这个时候我们的本地项目已经和远程 GayHub 仓库关联起来了,那么我们这个时候可以执行如下命令:

    pnpm run build

    首先执行该命令对我们的项目进行打包构建。打包完成之后会生成如下文件,请看下图:




    接下来我们可以使用 gh-pages 将项目发布到网上面了

    pnpm run deploy

    使用该命令进行打包并且部署到网上,这个过程可能需要一点时间,可以在工位上打开手机开把王者了。
    当在终端里出现 published 字段就说明我们部署成功了:




    这个时候,访问我们刚才在 package.json 文件中定义的 homepage 字段中的链接去访问就可以正常显示啦!




    总结


    通过该方法我们可以不用花钱,也能部署一个属于自己的网站,如果觉得不错那就赶紧用起来吧!


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

    实现一个简易的热🥵🥵更新

    简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
    继续阅读 »

    简单模拟一个热更新


    什么是热更新



    热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



    热更新的优点


    实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


    保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


    webpack 中的热更新



    在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



    原理如下:


    客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


    打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


    将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


    浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


    (1)找到被替换的模块并卸载它。


    (2)下载新的模块代码,并对其进行注入和执行。


    (3)重新渲染或更新应用程序的相关部分。


    保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


    代码模拟



    在同一个目录下创建 server.js 和 watcher.js



    server.js

    const http = require("http");
    const server = http.createServer((req, res) => {
    res.statusCode = 200;
    // 设置字符编码为 UTF-8,若有中文也不乱码
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("offer get!!!");
    });

    server.listen(7777, () => {
    console.log("服务已启动在 7777 端口");
    process.send("started");
    });

    // 监听来自 watcher.js 的消息
    process.on("message", (message) => {
    if (message === "refresh") {
    // 重新加载资源或执行其他刷新操作
    console.log("重新加载资源");
    }
    });


    (1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


    (2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


    (3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


    (4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


    (5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


    server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


    watcher.js

    const fs = require("fs");
    const { fork } = require("child_process");

    let childProcess = null;

    const watchFile = (filePath, callback) => {
    fs.watch(filePath, (event) => {
    if (event === "change") {
    console.log("文件已经被修改,重新加载");

    // 如果之前的子进程存在,终止该子进程
    childProcess && childProcess.kill();

    // 创建新的子进程
    childProcess = fork(filePath);
    childProcess.on("message", callback);
    }
    });
    };

    const startServer = (filePath) => {
    // 创建一个子进程,启动服务器
    childProcess = fork(filePath);
    childProcess.on("message", () => {
    console.log("服务已启动!");
    // 监听文件变化
    watchFile(filePath, () => {
    console.log("文件已被修改");
    });
    });
    };

    // 注意文件的相对位置
    startServer("./server.js");


    watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


    (1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


    (2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


    (3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


    (4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


    (5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


    (6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


    watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


    效果图


    打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样




    当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


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

    28岁小公司程序员,无车无房不敢结婚,要不要转行?

    大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。 **最近他遇到了新的职业上的困惑,又...
    继续阅读 »

    大家好,这里是程序员晚枫,又来分享程序员的职场故事了~


    今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。


    **最近他遇到了新的职业上的困惑,又找我聊了一下,我也没想到好的解决方法,**大家可以一起看一下~下面是沟通的核心内容。


    1、他的问题


    小青是中原省份省会城市的大专毕业,毕业季就去了帝都实习和工作。后来发现同学中有转行程序员的,薪资很诱惑,所以就找到我咨询如何学习和转行,现在一家帝都创业公司负责后端开发。工资1w出头。


    今年已经28岁了,有一个女朋友,最近女方家里催他结婚,他自己也有结婚的意愿。但是考虑到自己人在大城市,无车无房,创业公司的工作也不稳定,以后吃住花销,结婚后养孩子的花销,再看看自己1w多的工资,女朋友做财务,一个月到手不到1w。


    双方家里也都是普通家庭,给不了什么实质的资助,靠自己目前的收入根本不敢想象成家后压力有多大。


    所以目前非常迷茫, 不知道自己在28岁这个年龄应该怎么办,应不应该成家?应该怎样提高收入?


    虽然自己很喜欢程序员这份工作,但是感觉自己学历不好,天花板有限,程序员还能继续干下去吗?


    2、几个建议


    平时收到后台读者的技术问题或者转行的困惑,我都会尽力给一些详尽的回复。


    但是这次听到小青的问题,说实话,我也不知道该说什么。


    在28岁这种黄金年龄,想去大城市奋斗一番也是人之常情,但因为现实的生活压力,却不得不面临着选择离开大城市或者转行到自己不喜欢但是更务实的职业里去。


    如果想继续留在帝都,我能想到的有以下几个办法:



    • 首先,如果想继续从事程序员工作,努力提高收入。最快的办法就是跳槽,已经工作2年多了,背一背八股文,总结一下项目经验,应该是可以跳槽到一家更好的公司了。

    • 其次,探索另一个副业收入,例如自媒体。因为我自己就是通过在各个平台开通了自媒体账号:程序员晚枫,分享自己的程序员学习经验获得粉丝,进而得到自媒体收入的。小青也可以实事求是的分享一下自己大专毕业从建筑工作转行到程序员的经验,应该也能帮助到别人。

    • 最后,努力提高学历,想继续在程序员这行卷出高收入,趁着年轻,获得一个本科或者本科以上的学历还是很有帮助的。


    受限于自己的经验,我只能给出以上几个建议了。


    大家还有什么更有效的建议,欢迎在评论区交流~


    3、写在最后


    说句题外话,很多人都觉得程序员工资高想来转行,但其实程序员和其它行业一样,高收入的只是一小部分,而且那部分人既聪明又努力。


    最重要的是,高收入的那部分人里,大部分都不是转行的,而是在一个专业深耕了多年,最终获得了应有的报酬。


    无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒

    作者:程序员晚枫
    来源:juejin.cn/post/7209447968218841144
    :如果不是十分热爱,请务必三思~

    收起阅读 »

    JavaScript中return await究竟有无用武之地?

    web
    我先回答:有的,参考文章末尾。 有没有区别?  先上一个Demo,看看async函数中return时加和不加await有没有区别: function bar() { return Promise.resolve('this from bar().'); ...
    继续阅读 »

    我先回答:有的,参考文章末尾。



    有没有区别?


     先上一个Demo,看看async函数中return时加和不加await有没有区别:


    function bar() {
    return Promise.resolve('this from bar().');
    }

    async function foo1() {
    return await bar(); // CASE#1 with await
    }

    async function foo2() {
    return bar(); // CASE#2 without await
    }

    // main
    (() => {
    foo1().then((res) => {
    console.log('foo1:', res); // res is string: 'this from bar().'
    })
    foo2().then((res) => {
    console.log('foo2:', res); // res is string: 'this from bar().'
    })
    })();

     可能在一些社区或团队的编程规范中,有明确要求:不允许使用非必要的 return await。给出的原因是这样做对于foo函数而言,会增加等待bar函数返回的Promise出结果的时间(但其实它可以不用等,因为马上就要return了嘛,这个时间应留给foo函数的调用者去等)。


     如果你觉得上面的文字不大通顺,直接看代码,问:以上例子中,foo1()函数和foo2()函数的写法对程序的执行过程有何影响?


     先说结论:async 函数中 return await promise;return promise; 从宏观结果来看是一样的,但微观上有区别。


    有什么区别?


     基于上面的Demo改造一下,做个试验:


    const TAG = Symbol();
    const RESULT = Promise.resolve('return from bar().');
    RESULT[TAG] = 'TAG#RESULT';

    function bar() {
    return RESULT;
    }

    async function foo1() {
    return await bar();
    }

    async function foo2() {
    const foo2Ret = bar();
    console.log('foo2Ret(i):', foo2Ret[TAG], foo2Ret === RESULT); // 'TAG#RESULT', true (1)
    return foo2Ret; // without await
    }

    // main
    (() => {
    const foo1Ret = foo1();
    console.log('foo1Ret:', foo1Ret[TAG], foo1Ret === RESULT); // undefined, false (2)
    console.log('--------------------------------------------');
    const foo2Ret = foo2();
    console.log('foo2Ret(o):', foo2Ret[TAG], foo2Ret === RESULT); // undefined, false (3)
    })();

     从注释标注的执行结果可以看到:



    • (1)处没有疑问,foo2Ret 本来就是 RESULT

    • (2)处应该也没有疑问,foo1Ret 是基于 RESULT 这个Promise的结果重新包装的一个新的Promise(只是这个Promise的结果和Result是一致的);

    • (3)处应该和常识相悖,竟然和(2)不一样?是的,对于 async 函数不管return啥都会包成Promise,而且不是简单的通过 Pomise.resolve() 包装。


     那么结论就很清晰了,async 函数中 return await promise;return promise; 至少有两个区别:



    1. 对象上的区别:

      • return await promise; 会先把promise的结果解出来,再构造成新的Promise

      • return await promise; 直接在promise的基础上构造Promise,也就是套了两个Promise(两层Promise的状态和结果是一致的)



    2. 时间上的区别:假设 bar() 函数耗时 10s

      • foo1() 中的写法会导致这10s消耗在 foo1() 函数的执行上

      • foo2() 的写法则会让10s的消耗在 foo2() 函数的调用者侧,也就是注释为main的匿名立即函数




     从对象上的区别看,不论怎样async函数都会构造新的Promise对象,有无await都节约不了内存;从时间上来看,总体的等待时长理论上是一样的,怎么写对结果都没啥影响嘛。


     举个不大恰当的例子:你的上司交给你一个重要任务让你完成后发邮件给他,你分析了下后发现任务需要同事A做一部分,遂找他。同事A完成他的部分需要2天。这个时候你有两个做法选择:一、做完自己的部分后等着A出结果,有结果后再发邮件回复上司;二、将自己的部分完成后汇报给上司,并跟和上司说已经告知A:让A等完成他的部分后直接回邮件给上司。


     如果,我是说假如果哈,如果,这个重要任务本来要求必须在12h内完成,但实际耗时了两天严重超标......请问上述例子中哪种做法更容易获取N+1大礼包?


    到底怎么写?


     回到代码层,通过上述分析可以知道,一个主要是耗时归属问题,一个是async函数“总是”会返回的那个Promise对象不是由Promise.resolve()简单包装的(因为Promise.resolve(promise) === promise),可以得到两个编码指南:



    强调下,async函数不是通过Pomise.resolve()简单包装的,其实进一步思考下也不难理解,因为它要考虑执行有异常的场景,甚至还可能根据不同的Promise状态做一些其他的操作(比如日志输出、埋点统计?我瞎猜的)



    // 避免非必要的 return await 影响模块耗时统计的准确性
    async function foo() {
    return bar();
    }

    // 除非你要处理执行过程中的异常
    async function foo() {
    try {
    return await bar();
    } catch (_) {
    return null;
    }
    }
    // 或:
    async function foo() {
    return bar().catch(() => null);
    }

    // async 函数中避免对返回值再使用多余的 Pomise 包装
    async function bar() {
    return 'this is from bar().'; // YES
    }
    async function bar() {
    return Promise.resolve('this is from bar().'); // !!! NO !!!
    }

    回到标题:JavaScript中return await有无用武之地?


    答:有的,当需要消化掉依赖 Promise

    作者:Chavin
    来源:juejin.cn/post/7268593569781350455
    执行中的异常时。

    收起阅读 »

    吐槽大会,来瞧瞧资深老前端写的垃圾代码

    web
    阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉 忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑...
    继续阅读 »

    阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉



    忍无可忍,不吐不快。


    本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


    知道了什么是烂代码,才能写出好代码。


    别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


    人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


    我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


    我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


    优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


    有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


    ---------------------------------------------更新------------------------------------------------


    集中回答一下评论区的问题:


    1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


    2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


    3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


    4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


    5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


    6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


    文件命名千奇百怪


    同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


    image.png


    组件职责不清


    还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


    image.png


    条件渲染逻辑置于底层


    这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


    image.png


    滥用、乱用 TS


    项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


    image.png


    留下大量无用注释代码和报错代码


    感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


    image.png


    image.png


    丑陋的、隐患的、无效的、混乱的 css


    丑陋的:没有空格,没有换行,没有缩进


    隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


    无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


    混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


    image.png


    一个文件 6 个槽点


    槽点1:代码的空行格式混乱,影响代码阅读


    槽点2:空函数,写了函数名不写函数体,但是还调用了!


    槽点3:函数参数过多不优化


    槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


    槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


    槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


    image.png


    变态的链式取值和赋值


    都懒得说了,各位观众自己看吧。


    image.png


    代码拆分不合理或者不拆分导致代码行数超标


    能写出这么多行数的代码的绝对是人才。


    尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


    image.png


    这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


    image.png


    杂七杂八的无用 js、md、txt 文件


    在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


    实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


    image.png


    less、scss 混用


    这是最奇葩的。


    image.png


    特殊变量重命名


    这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


    const G = window;
    const doc = G.document;

    混乱的 import


    规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


    总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。


    image.png


    写在最后


    就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


    要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


    我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


    不过说这么多,成事在人。


    不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

    作者:北岛贰
    来源:juejin.cn/post/7265505732158472249

    收起阅读 »

    实现一个简易的热🥵🥵更新

    web
    简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
    继续阅读 »

    简单模拟一个热更新


    什么是热更新



    热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



    热更新的优点


    实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


    保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


    webpack 中的热更新



    在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



    原理如下:


    客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


    打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


    将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


    浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


    (1)找到被替换的模块并卸载它。


    (2)下载新的模块代码,并对其进行注入和执行。


    (3)重新渲染或更新应用程序的相关部分。


    保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


    代码模拟



    在同一个目录下创建 server.js 和 watcher.js



    server.js


    const http = require("http");
    const server = http.createServer((req, res) => {
    res.statusCode = 200;
    // 设置字符编码为 UTF-8,若有中文也不乱码
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("offer get!!!");
    });

    server.listen(7777, () => {
    console.log("服务已启动在 7777 端口");
    process.send("started");
    });

    // 监听来自 watcher.js 的消息
    process.on("message", (message) => {
    if (message === "refresh") {
    // 重新加载资源或执行其他刷新操作
    console.log("重新加载资源");
    }
    });


    (1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


    (2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


    (3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


    (4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


    (5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


    server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


    watcher.js


    const fs = require("fs");
    const { fork } = require("child_process");

    let childProcess = null;

    const watchFile = (filePath, callback) => {
    fs.watch(filePath, (event) => {
    if (event === "change") {
    console.log("文件已经被修改,重新加载");

    // 如果之前的子进程存在,终止该子进程
    childProcess && childProcess.kill();

    // 创建新的子进程
    childProcess = fork(filePath);
    childProcess.on("message", callback);
    }
    });
    };

    const startServer = (filePath) => {
    // 创建一个子进程,启动服务器
    childProcess = fork(filePath);
    childProcess.on("message", () => {
    console.log("服务已启动!");
    // 监听文件变化
    watchFile(filePath, () => {
    console.log("文件已被修改");
    });
    });
    };

    // 注意文件的相对位置
    startServer("./server.js");


    watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


    (1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


    (2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


    (3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


    (4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


    (5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


    (6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


    watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


    效果图


    打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样


    image.png


    当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


    image.png

    收起阅读 »

    工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

    前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
    继续阅读 »

    前言



    哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



    正文



    不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




    他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




    这种写法埋了一个不大不小的雷。




    用一段测试代码就可以展示出来问题



    1.jpg



    打印结果如下:



    2.jpg



    很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




    原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




    比如我如果换成2023-12-30又不会有问题了



    3.jpg



    另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



    4.jpg



    避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



    总结




    1. 日期时间格式统一使用yyyy小写;

    2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




    最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人

    作者:程序员济癫
    来源:juejin.cn/post/7269013062677823528
    了。


    收起阅读 »

    简历中不写年龄、毕业院校、预期薪资会怎样?

    无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。 之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推...
    继续阅读 »

    无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。


    之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推导,那么这些事实和观点是不足信的,需要慎重对待。


    视频的内容是这样的:“不管你有多自信,简历中的个人信息都不要这样写。1、写了期望薪资,错!2、写了户籍地址,错!3、写了学历文凭,错!4、写了离职原因,错!5、写了生日年龄,错!6、写了自我评价,错!


    正确写法,只需要写姓名和手机号、邮箱及求职意向即可,简历个人信息模块的作用是让HR顺利联系到你,所有任何其他内容都不要写在这里……”


    针对这条视频的内容,有两个不同的表现:第一就是分享和点赞数量还可以,都破千了;第二就是评论区很多HR和求职着提出了反对意见。


    第一类反对意见是:无论求职者或HR都认为这样的简历是不合格的,如果不提供这些信息,根本没有预约面试的机会,甚至国内的招聘平台的简历模板都无法通过。第二类,反对者认为,如果不写这些信息,特别是预期薪资,会导致浪费双方的时间。


    针对上述质疑,作者的回复是:”看了大家的评论,我真的震惊,大家对简历的误解是如此至深……“


    仔细看完视频和评论,在视频的博主和评论者之间产生了一个信息差。博主说的”个人信息“不要写,给人了极大的误导。是个人信息栏不要写,还是完全不写呢?看评论,大多数人都理解成了完全不写。博主没有说清楚是不写,还是写在别处,这肯定是作者的锅。


    本人也筛选过近千份简历,下面分享一下对这则视频中提到的内容的看法:


    第一,户籍、离职原因可以不写


    视频中提到的第2项和第4项的确可以不写。


    户籍这一项,大多数情况下是可以不写的,只用写求职城市即可,方便筛选和推送。比如,你想求职北京或上海的工作,这个是必须有的,而你的户籍一般工作没有强制要求。但也有例外,比如财务、出纳或其他特殊岗位,出于某些原因,某些公司会要求是本地的。写不写影响没那么大。


    离职原因的确如他所说的,不建议写,是整个简历中都不建议写。这个问到了再说,或者填写登记表时都会提到,很重要,要心中有准备,但没必要提前体现。


    第二,期望薪资最好写上


    关于期望薪资这个有两种观点,有的说可以不写,有的说最好写上。其实都有道理,但就像评论中所说:如果不写,可能面试之后,薪资相差太多,导致浪费了双方的时间。


    其实,如果可以,尽量将期望薪资写上,不仅节省时间,这里还稍微有一个心理锚定效应,可以把薪资写成范围,而范围的下限是你预期的理想工资。就像讨价还价时先要一个高价,在简历中进行这么一个薪资的锚定,有助于提高最终的薪资水平。


    第三,学历文凭一定要写


    简历中一定要写学历文凭,如果没有,基本上是会默认为没有学历文凭的,是不会拿到面试邀约的。仔细想了一下,那则视频的像传达的意思可能是不要将学历文凭写作个人信息栏,而是单独写在教育经历栏中。但视频中没有明说,会产生极大的误导。


    即便是个人信息栏,如果你的学历非常漂亮,也一定要写到个人信息栏里面,最有价值,最吸引眼球的信息,一定要提前展现。而不是放在简历的最后。


    第四,年龄要写


    视频中提到了年龄,这个是招聘衡量面试的重要指标,能写尽量写上。筛选简历中有一项非常重要,就是年龄、工作经历和职位是否匹配。在供大于求的市场中,如果不写年龄,为了规避风险,用人方会直接放弃掉。


    前两个月在面试中,也有遇到因为年龄在30+,而在简历中不写年龄的。作为面试官,感觉是非常不好的,即便不写,在面试中也需要问,最终也需要衡量年龄与能力是否匹配的问题。


    很多情况下,不写年龄,要么认为简历是不合格的,拿不到面试机会,要么拿到了面试机会,但最终只是浪费了双方的时间。


    第五,自我评价


    这一项与文凭一样,作者可能传达的意思是不要写在个人信息栏中,但很容易让人误解为不要写。


    这块真的需要看情况,如果你的自我评价非常好,那一定要提前曝光,展现。


    比如我的自我评价中会写到”全网博客访问量过千万,CSDN排名前100,出版过《xxx》《xxx》书籍……“。而这些信息一定要提前让筛选简历的人感知到,而不是写在简历的最后。


    当然,如果没有特别的自我评价,只是吃苦耐劳、抗压、积极自主学习等也有一定的积极作用,此时可以考虑放在简历的后面板块中,而不是放在个人信息板块中。这些主观的信息,更多是一个自我声明和积极心态的表现。


    最后的小结


    经过上面的分析,你会看到,并不是所有的结论都有统一的标准的。甚至这篇文章的建议也只是一种经验的总结,一个看问题的视角而已,并不能涵盖和适用所有的场景。而像原始视频中那样,没有分析,没有推导,没有数据支撑,没有对照,只有干巴巴的结论,外加的煽动情绪的配音,就更需要慎重对待了。


    在写这篇文章的过程中,自己也在想一件事:任何一个结论,都需要在特定场景下才能生效,即便是牛顿的力学定律也是如此,这才是科学和理性的思维方式。如果没有特定场景,很多结

    作者:程序新视界
    来源:juejin.cn/post/7268593569782054967
    论往往是不成立的,甚至是有害的。

    收起阅读 »

    良言难劝该死鬼,居然有人觉得独立开发做三件套是件好事

    没想到某个大V居然写了一篇公众号文章回应独立开发者三件套。认为无脑做大路货也好! 我其实挺烦这种清新圣母派,就是无论你做什么,反正我都不拦着,我就鼓励你 follow your heart。人生没有白走的路。口头上一百个支持,谁都不得罪,等你真吃亏了这些人就不...
    继续阅读 »

    Pasted Graphic.png


    没想到某个大V居然写了一篇公众号文章回应独立开发者三件套。认为无脑做大路货也好!


    我其实挺烦这种清新圣母派,就是无论你做什么,反正我都不拦着,我就鼓励你 follow your heart。人生没有白走的路。口头上一百个支持,谁都不得罪,等你真吃亏了这些人就不知道哪去了。


    我提两个点:首先这个作者就不是干独立开发的,你就听不会游泳的人跟你讲如何游泳这靠谱吗,IT王语嫣是吧。要是你真独立开发有成绩,你这么说我也觉得多少有点信用背书。现在说这话立场就跟某些博主推荐东西一样,你买我推荐,我买我不买。哪天他要是自己下场做笔记我也服。


    其次他的观点就是反正做出来就是成功。你还好意思问别人怎么定义成功,你这成功定义的,既然我做什么都是成功,我为什么要做大路货,我做其他也算是成功。既然第一款做笔记失败概率大,不还是要面临到底我要做什么问题吗。既然都是做,为什么不做点有趣的东西



    但是我要补充说明一个观点,独立开发起步雷区里是没有番茄钟的。番茄钟还是挺新手友好的。



    这就是我讽刺做这些是独立开发死亡加速三件套的原因,因为独立开发的真正内核是独创性。独创性可以是不赚钱的,可以是小众的,可以是无用但有趣的,但是不应该是我脑子一热没想法我跟风做一个。既然要做独立开发,就不能用战术上的勤奋掩盖战略上的懒惰。最关键的产品定位你跳过不考虑就是捡芝麻丢西瓜。问题的核心不是做不做三件套,是你有没有想好产品的差异化是什么,能不能通过差异化获得产品的定价权,而不是做另外一个xx。


    良言难劝该死鬼,以上的言论是我糊涂了。我觉得做笔记、做记账光明是前途的,做出来就很厉害了。世界上虽然有很多记账了,但是说不定就缺你这个记账。做你想做的,just do it!


    作者:独立开花卓富贵
    来源:juejin.cn/post/7268896098827403301

    收起阅读 »

    一个上午,我明白了,为什么总说人挣不到认知以外的钱

    你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中 01 接下来,以我昨天上午的一段经历,分析下为什么我挣不到认知以外的钱 在上班的路上,我在微信的订阅号推荐里面看到了这张图,当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过...
    继续阅读 »

    你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中


    01


    接下来,以我昨天上午的一段经历,分析下为什么我挣不到认知以外的钱


    在上班的路上,我在微信的订阅号推荐里面看到了这张图,当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过去了。




    (读者可以先停5s 思考下,假设是你看到这张图,会有什么想法)



    02


    当我坐上工位后,我看到我参加的社群里也有有发了上图,并附上了一段文字:


    “养老金类型的公众号容易出爆文。


    小白玩转职场这个号,篇篇10w+,而且这并不是一个做了很久的老号,而是今年5月才注册不久的号。 之前这个号刚做的时候是往职场方向发展,所以取名叫小白玩转职场,但是发了一阵后数据不是很好于是就换风格做了养老金的内容。


    换到养老金赛道后就几乎篇篇10w+。 这些内容一般从官方网站找就好,选一些内容再加上自己想法稍微改下,或者直接通过Chatgpt辅助,写好标题就行”。


    同时,文字下面有社群圈友留下评论说:“这是个好方向啊,虽然公众号文章已经同质化很严重了,但可以往视频号、带货等方向发展”。



    读者可以先停5s 思考下,假设是你看到这段文字,会有什么想法。如果你不知道公众号赚钱的模式,那你大概率看不出这段话中的赚钱信息的



    我想了想,对噢,确实可以挣到钱,于是将这则信息发到了程序员副业交流的微信群里。



    然后,就有群友在交流:“他这是转载还是什么,不可能自己天天写吧”,“这种怎么冷启动呢,不会全靠搜索吧,“做他这种类型的公众号挺多吧,怎么做到每篇10w的”



    有没有发现,这3个问题都是关注的怎么做的问题?怎么写的,怎么启动的,怎么每篇10w。


    这就是我们大部分人的认知习惯,看到一个信息或别人赚钱的点子后,我们大部分人习惯去思考别人是如何做到的,是不是真的,自己可不可以做到。


    可一般来说,我们当下的认知是有限的,大概率是想不出完整的答案的的,想不出来以后,然后就会觉得这个事情有点假,很难或者不适合。从而就错过这条信息了。



    我当时觉得就觉得可能大部分群友会错过这则信息了,于是,在群里发了下面这段话


    “分享一个点子后


    首先去看下点子背后的商业变现机会,如带货/流量主/涨粉/等一系列


    而后,才去考虑执行的问题,执行的话


    1、首先肯定要对公众号流量主的项目流程进行熟悉


    2、对标模仿,可以去把这个公众号的内容全看一看,看看别人为什么起号


    3、做出差异化内容。”


    发完后还有点小激动(嗯,我又秀了波自己的认知)。可到中午饭点,我发现我还是的认知太低了。


    03


    在中午吃饭时,我看到亦仁(生财有术的老大)发了这段内容



    我被这段话震撼到了,我发现我现在的思考习惯,还是只停留在最表面的看山是山的地步。


    我仅仅看到了这张图流量大,没有去思考它为什么这么大流量?



    因为他通过精心制作的文章,为老年用户介绍了养老金的方方面面,所以,才会有流量


    简单说,因为他满足了用户的需求



    同样,我也没有思考还有没有其他产品可以满足用户的这个需求,我仅仅是停留在了视频号和公众号这两个产品砂锅。



    只要满足用户需求,就不只有一个产品形态,对于养老金这个信息,我们可以做直播,做课程,做工具,做咨询,做1对1私聊的。这么看,就能有无数的可能



    同时,我想到了要做差异化,但没有想到要通过关键字挖掘,去挖掘长尾词。



    而亦仁,则直接就挖掘了百万关键字,并无偿分享了。



    这才知道,什么叫做看山不是山了。


    之前知道了要从“需求 流量 营销 变现”的角度去看待信息,也知道“需求为王”的概念。


    可我看到这则信息时,还是没有考虑到需求这一层,更没有形成完整的闭环思路。


    因此,以后要在看到这些信息时,去刻意练习“需求 流量 营销 变现”这个武器库,去关注他用什么产品,解决了用户什么需求,从哪里获取到的流量的,怎么做营销的,怎么做变现的。


    04


    于是,我就把这些思考过程也同样分享到了群里。


    接着,下午我就看到有群友在自己的公众号发了篇和养老金相关的文章,虽然文章看上去很粗糙,但至少是起步了。


    同时,我也建了个项目交流群,方便感兴趣的小伙伴交流进步(一群人走的更远)


    不过我觉得在起步之前,也至少得花一两天时间去调研下,去评估这个需求有哪些地方自己可以切入进去,值不值得切入,能切入的话,怎么切入。


    对了,可能你会关心我写了这么多,自己有没有做养老金相关的?


    我暂时还没有,因为我目前关心的领域还在出海工具和个人IP上。

    作者:刘卡卡
    来源:juejin.cn/post/7268590610189418533

    收起阅读 »

    228欢乐马事件,希望大家都能平安健

    iOS
    我这个人体质很奇怪,总能遇到一些奇怪的事。比如六年前半夜打车回家,差点被出租车司机拉到深山老林。比如两年前去福州出差,差点永远回不了家。比如十点从实验室下班,被人半路拦住。比如这次,被人冒充 (在我心里这事和前几件同样恶劣) 不过幸好每次都是化险为夷...
    继续阅读 »

    我这个人体质很奇怪,总能遇到一些奇怪的事。

    • 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。
    • 比如两年前去福州出差,差点永远回不了家。
    • 比如十点从实验室下班,被人半路拦住。
    • 比如这次,被人冒充 (在我心里这事和前几件同样恶劣)

    不过幸好每次都是化险为夷,所以我顺顺利利活到现在。




    事情起因是这样的:


    去年朋友B突然告诉我:发现了你的闲鱼账号。


    :我没有闲鱼啊?


    他给我截图,那个人卖掘金的周边,名字叫LolitaAnn


    因为我遇到太多离谱的事,再加上看的一堆被冒充的新闻,所以我第一反应是:这人也在冒充我


    当时朋友F 说我太敏感了,他觉得只是巧合。


    但我觉得不是巧合,因为LolitaAnn是我自己造的词。




    把我的沸点搬到小红书


    又是某一天, 朋友H告诉我:你的小红书上热门了。


    :?我没有小红书啊?


    然后他们给我看,有个人的小红书完全照搬我的沸点。


    为此我又下载小红书和他对线。起初正常交涉,但是让他删掉,他直接不回复我了,最后还是投诉他,被小红书官方删掉的。




    现在想了想,ip一样,极有可能是一个人干的。




    闲鱼再次被挖出来


    今年,有人在掘金群里说我卖周边。


    我跑到那个群解释,说我被人冒充了。


    群友很热心,都去举报那个人的昵称。昵称被举报下去了。


    但是几天之后:




    看到有人提醒我,它名字又改回来了。


    当时以为是热心群友,后来知道就是它冒充我,现在回想起来一阵恶寒。


    把名字改回去之后还在群里跟炫耀一样,心里想着:我改回来了,你们不知道是我吧。




    冒充我的人被揪出来了


    2.28的时候, 朋友C突然给我发了一段聊天记录。


    是它在群里说 它的咸鱼什么掘金周边都有。结果打开一看,闲鱼是我的名字和头像。




    事情到今天终于知道是谁冒充我了


    虽然Smile只是它的微信小号之一,都没实名认证。但是我还是知道了一些强哥的信息。


    发现是谁冒充我,我第一反应必然是喷一顿。


    刚在群里被我骂完,它脑子也不太好使,马上跑去改了自己掘金和闲鱼的名字。这不就是自爆了? 证明咸鱼和掘金都是他的号。


    奔跑姐妹兄弟(原名一只小小黄鸭) ←点击链接即可鞭尸。






    牵扯出一堆小号


    本来我以为事情已经结束了,我就去群里吐槽他。结果一堆认识他的掘友们给我说它还有别的掘金号。因为它和不同掘友用不同掘金号认识的。所以掘友们给我提供了一堆。我就挨个搜。


    直到我看到了这两条:




    因为我搜欢乐马出来上万个同名账号, 再加上他说自己有脚本,当时我以为都是他的小号。


    后来掘友们提醒我,欢乐马是微信默认生成的,所以有一堆欢乐马,不一定是他的小号。


    但是我确信他有很多小号,比如:




    比如咸鱼卖了六七个掘金鼠标垫,卖了掘金的国行switch……

    • 你们有没有想过为什么fixbug不许助攻了

    • 你们有没有想过为什么矿石贬值了,兑换商店越来越贵了?

    • 你们有没有想过为什么掘金活动必得奖励越来越少了?


    有这种操作在,普通用户根本没办法玩吧。


    所以最后,我就把这个事交给官方了。




    处理结果


    所幸官方很给力,都处理了,感谢各位官方大大。



    本次事件,共涉及主要近期活跃UserID 4个,相关小号570个。 





    我再叨叨几句

    • 卖周边可以, 你别冒充别人卖吧,又不是见不得光,做你自己不丢人。

    • 开小号可以,你别一开开一堆,逼得普通玩家拿不到福利吧。

    • 先成人后成才,不指望能为国家做多大贡献,起码别做蛀虫吧。

    • 又不是没生活,专注点自己的东西,别老偷别人沸点。

    • 我以后改名了嗷。叫Ann的八百万个,别碰瓷你爹了。


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

    孤独的游戏少年

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据...
    继续阅读 »

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



    楔子


    又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据颜色分为红色和绿色阵营,旗子翻盖为建筑,正面为单位,被子和枕头作为地图障碍,双手不断的移动着双方阵营的象棋,脑海中演练着星级争霸的游戏画面,将两枚不同阵营的旗子进行碰撞后,通过我对两枚旗子主观的判断,一方阵营的旗子直接被销毁,进入回收,一方的单位对建筑物全部破坏取得游戏胜利。因为我的父亲酷爱玩这款游戏,年少的我也被其玩法、画面所深深吸引,不过最主要的还是父亲获胜或者玩累后,能幸运的奖励我玩上几把。星际争霸成了我第一个启蒙游戏,那时候怎么样都获胜不了,直到发现了show me the money。因为Blizzard这个英文单词一直在游戏的启动界面一闪一闪,那时候还以为这款游戏的名字叫Blizzard,最后才发现,其实Blizzard是魔鬼的意思。


    纸笔乐趣


    小学一二年级的时候家里管得严,电视也不给我看,电脑直接上锁,作为家里的独生子女,没有同龄人的陪伴,闲暇时间要不就看《格林童话》、《安徒生童话》、《伊索寓言》,要不就打开这副象棋在那里自娱自乐。



    起源



    在某一天的音乐课上,老师喉咙不舒服,在教室播放猫和老鼠给我们观看,正当我看的津津有味,前面的同学小张突然转过头来,问我“要不要跟我玩个游戏”。小孩子一般都比较贪玩,我直接拒绝了他,“我想看猫和老鼠”。小张满脸失落转了回去,因为我是个特别怕伤害别人情绪的人,所以不忍心又用铅笔的橡皮端戳了戳他,问他“什么游戏,好玩不”。他顿时也来了精神,滔滔不绝的介绍起了这款游戏。


    “游戏的规则非常的简单~~在一张纸上中间画一条分割线,双方有各自的基地,每人基地都有5点血量,双方通过猜拳,获胜的一方可以在自己阵营画一个火柴人,火柴人可以攻击对面的火柴人和基地,基地被破坏则胜利。管你听没听懂,试一把就完了!”



    我呆呆的看着他,“这么无聊,赢了又怎样?”。他沉默了一会,好像我说的的确有点道理,突然想到了一个好办法,“谁获得胜利扇对方一巴掌”。我顿时来了兴趣。随着游戏逐渐深入,我们的猜拳速度和行动力也越来越快,最后发现扇巴掌还是太狠,改为扇对方的手背,这节课以双方手背通红结束了。


    游戏改良


    这个《火柴人对战》小游戏在班里火了一会儿,但很快就又不火了,我玩着玩着也发现没啥意思,总是觉得缺少点什么,但毕竟我也只是个没有吃APTX4869的小学生,想不出什么好点子。时间一晃,受到九年义务教育的政策,我也成功成为了一名初中生。在一节音乐课上,老师想让我们放松放松,给我们班看猫和老鼠,隔壁同桌小王撕了一张笔记本的纸,问我有没有玩过《火柴人对战》游戏,只能说,熟悉的配方,熟悉的味道。


    当天晚上回到家,闲来无事,我在想这个《火柴人游戏》是不是可以更有优化,这种形式的游戏是不是可以让玩家更有乐趣。有同学可能会问,你那个年代没东西玩的吗?既然你诚心诚意发问,那我就大发慈悲的告诉你。玩的东西的确很多,但是能光明正大摆在课桌上玩的基本没有,一般有一个比较新鲜的好玩的东西,都会有一群人围了过来,这时候老师会默默站在窗户旁的阴暗角落,见一个收一个。


    坐在家里的椅子上,我整理了一下思绪,突然产生了灵感,将《魔兽争霸》《游戏王》这两个游戏产生化学反应,游戏拥有着资源,单位,建筑,单位还有攻击力,生命值,效果,攻击次数。每个玩家每回合通过摇骰子的方式获得随机能源点,能源能够解锁建筑,建筑关联着高级建筑和单位,通过单位进行攻击,直至对方玩家生命值为0,那么如何在白纸上面显示呢?我想到比较好的解决方案,单位的画像虽然名字叫骷髅,但是在纸上面用代号A表示,建筑骷髅之地用代号1表示。我花了几天时间,弄了两个阵营,不死族和冰结界。立刻就拿去跟同桌试玩了一下,虽然游戏很丰富,但是有一个严重的弊端就是玩起来还挺耗费时间的,而且要人工计算单位之间的扣血量,玩家的剩余生命,在纸片上去完成这些操作,拿个橡皮擦来擦去,突然觉得有点蠢,有点尴尬,突然明白,一张白纸的承受能力是有限的。之后,我再也没有把游戏拿出来玩过,但我没有将他遗忘,而是深深埋藏在我的心里。


    筑梦


    直到大学期间《炉石传说》横空出世,直到《游戏王》上架网易,直到我的项目组完成1.0后迎来空窗期一个月,我再也蚌埠住了,之前一直都对微信小游戏很有兴趣,每天闲着也是闲着,所以我有了做一个微信小游戏的想法。而且,就做一款在十几年前,就已经被我设计好的游戏。


    但是我不从来不是一个好学的人,领悟能力也很低,之前一直在看cocos和白鹭引擎学习文档,也很难学习下去,当然也因为工作期间没有这么多精力去学习,所以我什么框架也不会,不会框架,那就用原生。我初步的想法是,抛弃所有花里胡哨的动效,把基础的东西做出来,再作延伸。第一次做游戏,我也十分迷茫,最好的做法肯定是打飞机————研究这个微信项目如何用js原生,做出一个小游戏。



    虽然微信小游戏刚出来的时候看过代码,但是也只是一扫而过,而这次带着目标进行细细品味,果然感觉不一样。微信小游戏打飞机这个项目是js原生使用纯gL的模式编写的,主要就是在canvas这个画布上面作展示和用户行为。

      // 触摸事件处理逻辑
    touchEventHandler(e) {
    e.preventDefault()

    const x = e.touches[0].clientX
    const y = e.touches[0].clientY

    const area = this.gameinfo.btnArea

    if (x >= area.startX
    && x <= area.endX
    && y >= area.startY
    && y <= area.endY) this.restart()
    }

    点击事件我的理解就是用户点击到屏幕的坐标为(x, y),如果想要一个按钮上面做处理逻辑,那么点击的范围就要落在这个按钮的范围内。当我知道如何在canvas上面做点击行为时,我感觉我已经成功了一半,接下来就是编写基础js代码。


    首先这个游戏确定的元素分别为,场景,用户,单位,建筑,资源(后面改用能源替代),我先将每个元素封装好一个类,一边慢慢的回忆着之前游戏是如何设计的,一边编程,身心完全沉浸进去,已经很久很久没有试过如此专注的去编写代码。用了大概三天的时间,我把基本类该有的逻辑写完了,大概长这个样子



    上面为敌方的单位区域,下方为我方的单位区域,单位用ABCDEFG表示,右侧1/1/1 则是 攻击力/生命值/攻击次数,通过点击最下方的icon弹出创建建筑,然后创建单位,每次的用户操作,都是一个点击。


    一开始我设想的游戏名为想象博弈,因为每个单位每个建筑都只有名称,单位长什么样子的就需要玩家自己去脑补了,我只给你一个英文字母,你可以想象成奥特曼,也可以想象成哥斯拉,只要不是妈妈生的就行。



    湿了


    游戏虽然基本逻辑都写好了,放到整个微信小游戏界别人一定会认为是依托答辩,但我还是觉得这是我的掌上明珠,虽然游戏没有自己的界面,但是它有自己的玩法。好像上天也认可我的努力,但是觉得这个游戏还能更上一层楼,在某个摸鱼的moment,我打开了微信准备和各位朋友畅谈人生理想,发现有位同学发了一幅图,上面有四个格子,是赛博朋克风格的一位篮球运动员。他说这个AI软件生成的图片很逼真,只要把你想要的图片告诉给这个AI软件,就能发你一幅你所描绘的图片。我打开了图片看了看,说实话,质感相当不错,在一系列追问下,我得知这个绘图AI软件名称叫做midjourney



    midjourney



    我迫不及待的登录上去,询问朋友如何使用后,我用我蹩脚的英格力士迫不及待的试了试,让midjourney帮我画一个能源的icon,不试不要紧,一试便湿了,眼睛留下了感动地泪水,就像一个阴暗的房间打开了一扇窗,一束光猛地照射了进来。




    对比我之前在iconfont下载的免费图标,midjourney提供这些图片简直就是我的救世主,我很快便将一开始的免费次数用完,然后氪了一个30美刀的会员,虽然有点肉痛,但是为了儿时的梦想,这点痛算什么


    虽然我查找了一下攻略,别人说可以使用gpt和midjourney配合起来,我也试了一下,效果一般,可能姿势没有对,继续用我的有道翻译将重点词汇翻译后丢给midjourney。midjourney不仅可以四选一,还可以对图片不断优化,还有比例选择,各种参数,但是我也不需要用到那么多额外的功能,总之一个字,就是棒。


    但当时的我突然意识到,这个AI如此厉害,那么会不会对现在行业某些打工人造成一定影响呢,结果最近已经出了篇报道,某公司因为AI绘图工具辞退了众多插画师,事实不一定真实,但是也不是空穴来风,结合众多外界名人齐心协力抵制gpt5.0的研发,在担心数据安全之余,是否也在担心着AI对人类未来生活的各种冲击。焦虑时时刻刻都有,但解决焦虑的办法,我有一个妙招,仍然还是奖励自己


    门槛


    当我把整个小游戏焕然一新后,便兴冲冲的跑去微信开放平台上传我的伟大的杰作。但微信突然泼了我一盆冷水,上传微信小游戏前的流程有点出乎意外,要写游戏背景、介绍角色、NPC、介绍模块等等,还要上传不同的图片。我的小游戏一共就三个界面,有六个大板块要填写,每个板块还要两张不同的图片,我当时人就麻了。我只能创建一个单位截一次图,确保每张图片不一样。做完这道工序,还要写一份自审自查报告。


    就算做完了这些前戏,我感觉我的小游戏还是难登大雅之堂,突然,我又想到了这个东西其实是不是也能运行在web端呢,随后我便立刻付诸行动,创建一个带有canvas的html,之前微信小游戏是通过weapp-adapter这个文件把canvas暴露到全局,所以在web端使用canvas的时候,只需要使用document.getElementById('canvas')暴露到全局即可。然后通过http-server对应用进行启动,这个小游戏便以web端的形式运行到浏览器上了,终于也能理解之前为啥微信小游戏火起来的时候,很多企业都用h5游戏稍微改下代码进行搬运,原来两者之间是有异曲同工之妙之处的。


    关于游戏





    上面两张便是两个种族对应的生产链,龙族是我第一个创建的,因为我自幼对龙产生好感和兴趣,何况我是龙的传人/doge。魔法学院则是稍微致敬一下《游戏王》中黑魔导卡组吧。


    其实开发难度最难的莫过于是AI,也就是人机,如何让人机在有限的资源做出合理的选择,是一大难题,也是我后续要慢慢优化的,一开始我是让人机按照创建一个建筑,然后创建一个单位这种形式去做运营展开,但后来我想到一个好的点子,我应该可以根据每个种族的特点,走一条该特点的独有运营,于是人机龙族便有了龙蛋破坏龙两种流派,强度提升了一个档次。


    其实是否能上架到微信小游戏已经不重要了,重要的是这个过程带给我的乐趣,一步一步看着这个游戏被创建出来的成就感,就算这个行业受到什么冲击,我需要被迫转行,我也不曾后悔,毕竟是web前端让我跨越了十几年的时光,找到了儿时埋下的种子,浇水,给予阳光,让它在我的心中成长为一棵充实的参天大树


    h5地址:hslastudio.com/game/


    github地址: github.com/FEA-Dven/wa…


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

    关于晋升的一点思考

    晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。 晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。 总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,...
    继续阅读 »

    晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。

    晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。


    总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,光靠答辩准备的一两个月,是绝无可能把自己“包装”成一个合格的候选人的。

    下面整体剖析一下自己在整个准备过程中的观察、思考、判断、以及做的事情和拿到的结果。


    准备工作


    我做的第一件事情并不是动手写PPT,而是搜集信息,花了几天在家把网上能找到的所有关于晋升答辩的文章和资料全撸了一遍,做了梳理和总结。


    明确了以下几点:

    • 晋升是在做什么
    • 评委在看什么
    • 候选人要准备什么
    • 评判的标准是什么
    • 常见的坑有哪些

    首先要建立起来的,是自己对整个晋升的理解,形成自己的判断。后面才好开展正式的工作。



    写PPT


    然后开始进入漫长而又煎熬的PPT准备期,PPT的准备又分为四个子过程,并且会不断迭代进行。写成伪代码就是下面这个样子。

    do {
    确认思路框架;
    填充内容细节;
    模拟答辩;
    获取意见并判断是否还需要修改;
    } while(你觉得还没定稿);

    我的PPT迭代了n版,来来回回折腾了很多次,思路骨架改了4次,其中后面三次均是在准备的后半段完成的,而最后一次结构大改是在最后一周完成的。这让我深深的觉得前面准备的1个月很多都是无用功。


    迭代,迭代,还是迭代


    在筹备的过程中,有一个理念是我坚持和期望达到的,这个原则就是OODA loop ( Boyd cycle)


    OODA循环是美军在空战中发展出来的对敌理论,以美军空军上校 John Boyd 为首的飞行员在空战中驾驶速度慢火力差的F-86军刀,以1:10的击落比完胜性能火力俱佳的苏联米格-15。而Boyd上校总结的结论就是,不是要绝对速度快,而是要比对手更快的完成OODA循环


    而所谓的OODA循环,就是指 observe(观察)–orient(定位)–decide(决策)–act(执行) 的循环,是不是很熟悉,这不就是互联网的快速迭代的思想雏形嘛。


    相关阅读 what is OODA loop

    wiki.mbalib.com/wiki/包以德循环 (from 智库百科)

    en.wikipedia.org/wiki/OODA_l… (from Wikipedia)


    看看下图,PPT应该像第二排那样迭代,先把框架确定下来,然后找老板或其他有经验的人对焦,框架确定了以后再填充细节。如果一开始填充细节(像第一排那样),那么很有可能越改越乱,最后一刻还在改PPT。


    btw,这套理论对日常工作生活中的大部分事情都适用。


    一个信息论的最新研究成果


    我发现,程序员(也有可能是大部分人)有一个倾向,就是show肌肉。明明很简单明了的事情,非要搞得搞深莫测,明明清晰简洁的架构,非要画成“豆腐宴”。


    晋升述职核心就在做一件事,把我牛逼的经历告诉评委,并让他们相信我牛逼。


    所以,我应该把各种牛逼的东西都堆到PPT里,甚至把那些其实一般的东西包装的很牛逼,没错吧?


    错。


    这里面起到关键作用的是 “让他们相信我牛逼” ,而不是“把我牛逼的故事告诉评委”。简单的增大的输出功率是不够的,我要确保评委能听进去并且听懂我说的东西,先保证听众能有效接收,再在此基础上,让听众听的爽。


    How?


    公式:喜欢 = 熟悉 + 意外


    从信息论的角度来看,上面的公式说的就是旧信息和新信息之间要搭配起来。那么这个搭配的配比应该是多少呢?


    这个配比是15.87% ——《科学美国人》


    也就是说,你的内容要有85%是别人熟悉的,另外15%是能让别人意外的,这样就能达到最佳的学习/理解效果。这同样适用于心流、游戏设计、神经网络训练。所以,拿捏好这个度,别把你的PPT弄的太高端(不知所云),也别搞的太土味(不过尔尔)。


    能够否定自己,是一种能力


    我审视自己的时候发现,很多时候,我还保留一张PPT或是还持续的花心思做一件事情,仅仅是因为——舍不得。你有没有发现,我们的大脑很容易陷入“逻辑自洽”中,然后越想越对,越想越兴奋。


    千万记得,沉没成本不是成本,经济学里成本的定义是放弃了的最大价值,它是一个面向未来的概念,不面向过去。


    能够否定和推翻自己,不留恋于过去的“成就” ,可以帮助你做出更明智的决策。


    我一开始对好几页PPT依依不舍,觉得自己做的特牛逼。但是后来,这些PPT全被我删了,因为它们只顾着自己牛逼,但是对整体的价值却不大,甚至拖沓。


    Punchline


    Punchline很重要,这点我觉得做的好的人都自觉或不自觉的做到了。想想,当你吧啦吧啦讲的时候,评委很容易掉线的,如果你没有一些点睛之笔来高亮你的成果和亮点的话,别人可能就糊里糊涂的听完了。然后呢,他只能通过不断的问问题来挖掘你的亮点了。


    练习演讲


    经过几番迭代以后,PPT可以基本定稿,这个时候就进入下一个步骤,试讲。


    可以说,演讲几乎是所有一线程序员的短板,很多码农兄弟们陪电脑睡的多了,连“人话”有时候都讲不利索了。我想这都要怪Linus Torvalds的那句


    Talk is cheap. Show me the code.


    我个人的经验看来,虽然成为演讲大师长路漫漫不可及,但初级的演讲技巧其实是一个可以快速习得的技能,找到几个关键点,花几天时间好好练几遍就可以了,演讲要注意的点主要就是三方面:

    • 形象(肢体语言、着装等)
    • 声音(语速、语调、音量等)
    • 文字(逻辑、关键点等)


    演讲这块,我其实也不算擅长,我把仅有的库存拿出来分享。


    牢记表达的初衷


    我们演讲表达,本质上是一个一对多的通信过程,核心的目标是让评委或听众能尽可能多的接受到我们传达的信息


    很多程序员同学不善于表达,最明显的表现就是,我们只管吧啦吧啦的把自己想说的话说完,而完全不关心听众是否听进去了。


    讲内容太多


    述职汇报是一个提炼的过程,你可能做了很多事情,但是最终只会挑选一两件最有代表性的事情来展现你的能力。有些同学,生怕不能体现自己的又快又猛又持久,在PPT里塞了太多东西,然后又讲不完,所以只能提高语速,或者囫囵吞枣、草草了事。


    如果能牢记表达的初衷,就不应该讲太多东西,因为听众接收信息的带宽是有限的,超出接收能力的部分,只会转化成噪声,反而为你的表达减分。


    过度粉饰或浮夸


    为了彰显自己的过人之处,有时候会自觉或不自觉的把不是你的工作也表达出来,并没有表明哪些是自己做的,哪些是别人做的。一旦被评委识破(他本身了解,或问问题给问出来了),那将会让你陈述的可信度大打折扣。


    此外,也表达的时候也不要过分的浮夸或张扬,一定的抑扬顿挫是加分的,但过度浮夸会让人反感。


    注意衔接


    作为一个演讲者,演讲的逻辑一定要非常非常清晰,让别人能很清晰明了的get到你的核心思路。所以在演讲的时候要注意上下文之间的衔接,给听众建设心理预期:我大概会讲什么,围绕着什么展开,分为几个部分等等。为什么我要强调这个点呢,因为我们在演讲的时候,很容易忽略听众的感受,我自己心里有清楚的逻辑,但是表达的时候却很混乱,让人一脸懵逼。


    热情


    在讲述功能或亮点的时候,需要拿出自己的热情和兴奋,只有激动人心的演讲,才能抓住听众。还记得上面那个分布图吗?形象和声音的占比达到93%,也就是说,你自信满满、热情洋溢的说“吃葡萄不吐葡萄皮”,也能打动听众。


    第一印象


    这个大家都知道,就是人在最初形成的印象会对以后的评价产生影响 。

    这是人脑在百万年进化后的机制,可以帮助大脑快速判断风险和节省能耗——《思考,快与慢》

    评委会刻意避免,但是人是拗不过基因的,前五分钟至关重要,有经验的评委听5分钟就能判断候选人的水平,一定要想办法show出你的与众不同。可以靠你精心排版的PPT,也可以靠你清晰的演讲,甚至可以靠一些小 trick(切勿生搬硬套)。


    准备问题


    当PPT准备完,演讲也练好了以后,不出意外的话,应该没几天了。这个时候要进入最核心关键的环节,准备问题。


    关于Q&A环节,我的判断是,PPT和演讲大家都会精心准备,发挥正常的话都不会太差。这就好像高考里的语文,拉不开差距,顶多也就十几分吧。而Q&A环节,则是理综,优秀的和糟糕的能拉开50分的差距,直接决定总分。千万千万不可掉以轻心。


    问题准备我包含了这几个模块:

    • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等
    • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等
    • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等
    • 团队:项目管理经验,团队管理经验
    • 个人:个人特色,个人规划,自己的反思等等

    其中业务、技术和数据这三块是最重要的,需要花80%的精力去准备。我问题准备大概花了3天时间,整体还是比较紧张的。准备问题的时候,明显的感觉到自己平时的知识储备还不太够,对大业务方向的思考还不透彻,对某些技术细节的把控也还不够到位。老话怎么说的来着,书到用时方恨少,事非经过不知难。


    准备问题需要全面,不能有系统性的遗漏。比如缺少了业务理解或竞品分析等。


    在回答问题上,也有一些要点需要注意:


    听清楚再回答


    问题回答的环节,很多人会紧张,特别是一两道问题回答的不够好,或气氛比较尴尬的时候,容易大脑短路。这个时候,评委反复问你一个问题或不断追问,而自己却觉得“我说的很清楚了呀,他还没明白吗”。我见过或听说过很多这样的案例,所以这应该是时有发生的。


    为了避免自己也踩坑,我给自己定下了要求,一定要听清楚问题,特别是问题背后的问题。如果觉得不清楚,就反问评委进行doubel check。并且在回答的过程中,要关注评委的反映,去确认自己是否答到点子上了。


    问题背后的问题


    评委的问题不是天马行空瞎问的,问题的背后是在考察候选人的某项素质,通过问题来验证或挖掘候选人的亮点。这些考察的点都是公开的,在Job Model上都有。


    我认为一个优秀的候选人,应当能识别出评委想考察你的点。找到问题背后的问题,再展开回答,效果会比单纯的挤牙膏来的好。


    逻辑自洽、简洁明了


    一个好的回答应该是逻辑自洽的。这里我用逻辑自洽,其实想说的是你的答案不一定要完全“正确”(其实往往也没有标准答案),但是一定不能自相矛盾,不能有明显的逻辑漏洞。大部分时候,评委不是在追求正确答案,而是在考察你有没有自己的思考和见解。当然,这种思考和见解几乎都是靠平时积累出来的,很难临时抱佛脚。


    此外,当你把逻辑捋顺了以后,简洁明了的讲出来就好了,我个人是非常喜欢能把复杂问题变简单的人的。一个问题的本质是什么,核心在那里,关键的几点是什么,前置条件和依赖是什么,需要用什么手段和资源去解决。当你把这些东西条分缕析的讲明白以后,不用再多啰嗦一句,任何人都能看出你的牛逼了。


    其他


    心态调整


    我的心态经历过过山车般的起伏,可以看到



    在最痛苦最难受的时候,如果身边有个人能理解你陪伴你,即使他们帮不上什么忙,也是莫大的宽慰。如果没有这样的人,那只能学会自己拥抱自己,自己激励自己了。


    所以,平时对自己的亲人好一点,对朋友们好一点,他们绝对是你人生里最大的财富。


    关于评委


    我从一开始就一直觉得评委是对手,是来挑战你的,对你的汇报进行证伪。我一直把晋升答辩当作一场battle来看待,直到进入考场的那一刻,我还在心理暗示,go and fight with ths giants。


    但真实的经历以后,感觉评委更多的时候并不是站在你的对立面。评委试图通过面试找到你的一些闪光点,从而论证你有能力晋升到下一个level。从这个角度来讲,评委不但不是“敌人”,更像是友军一般,给你输送弹药(话题)。


    一些教训

    • 一定要给自己设置deadline,并严格执行它。如果自我push的能力不强,就把你的deadline公开出来,让老板帮你监督。

    • 自己先有思考和判断,再广开言路,不要让自己的头脑成为别人思想的跑马场。

    • 坚持OODA,前期千万不要扣细节。这个时候老板和同事是你的资源,尽管去打扰他们吧,后面也就是一两顿饭的事情。


    附件


    前期调研



    参考文章


    知乎


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

    Widget开发流程

    iOS
    本文所用到的 Demo 可以在这里下载: github.com/zhenyunzhan… 一、创建Widget Extension 1、创建Widget Target 点击 Project —> 添加新的Target —> 搜索Widget Ext...
    继续阅读 »

    本文所用到的 Demo 可以在这里下载: github.com/zhenyunzhan…


    一、创建Widget Extension


    1、创建Widget Target


    点击 Project —> 添加新的Target —> 搜索Widget Extension —> 点击Widget Extension —> 点击 Next




    2、添加配置信息


    Include Configuration Intent 是创建 intentdefinition 文件用的,可以让用户动态配置组件,可以先不勾选,后期可以手动创建




    3、添加Widget


    创建好之后就可以运行程序,程序运行完成之后,长按主屏幕进入编辑状态,点击主屏幕右上方添加按钮,找到程序,就可以添加Widget,简单体验下了


    二、大致了解 Widget 文件


    查看创建完 Widget Extension 后默认生成的 xxxx Widget.swift 文件,Xcode 为我们生成了 Widget 所需的模版代码,如下这个文件




    widget的入口 @main标识的部分




    view EntryView 是我们实际展示的UI




    数据 Entry 是用来携带widget展示需要的数据




    Provider Timeline的提供对象,包含TimelineEntry & ReloadPolicy,用来后续刷新 Widget 内容




    三、开发


    以Demo为例,做一个展示古诗内容的Widget,间隔一段时间后自动更新widget内容,并且可以自动选择古诗内容来跟新Widget,例子如下:


    展示古诗内容 -> 长按后可编辑小组件 -> 进入选择界面 -> 选择并更新




    四、静态配置 StaticConfiguration


    创建完 Widget Extension Target之后,系统会给我们创建好一个Widget的开发模板


    1、TimelineEntry


    自己创建的模型作为参数,模型 (itemModel) 用 swift 或者 OC创建均可




    2、界面样式


    界面有三种尺寸的类型,每种尺寸还可以准备不同的布局,另外界面填充的数据就来源于 TimelineEntry




    3、Timeline时间线


    实现 TimelineProvider 协议 getTimeline 方法,主要是构建 Entry 和 reloadPolicy,用这两个参数初始化 Timeline ,之后再调用completion回调,回调会走到 @main ,去更新 Widget 内容。


    demo中是每次刷新 Timeline ,创建一个 Entry, 则更新一次主屏幕的 Widget 内容, 刷新间隔为 60 分钟,注意:

    • atEnd 所有的entry执行完之后,再次调用 getTimeline 构造数据

    • after(date) 不管这次构造的entry 是否执行完,等系统时间到达date之后,就会在调用getTimeline

    • never 最后一个 entry 展示完毕之后 Widget 就会一直保持那个 entry 的显示内容




    开发完成后,可以运行代码,试一下效果,此时的更新时间有点长,可以改为 5 秒后再试。


    五、动态配置 IntentConfiguration


    程序运行到这里,有的会想,怎么实现编辑小组件功能,动态配置 widget 的显示内容呢?




    1、创建 intentdefinition 文件


    command + N 组合键创建新 File —> 搜索 intent




    选择xxx.intentdefinition文件 —>点击下方 + ,选择intent创建 —> 对intent命名






    这个 intent 文件包含了你所有的(intents),通过这个文件编译到你的app中,系统将能够读取你的 intents ,一旦你定义了一个intent文件,Xcode也会为你生成一个intent类别


    2、可以添加到 intent 中的参数类型


    参数类型有多种,下方为一些示例
    参数类型分别为:String、Boolean、Integer时的展示




    你也可以用自己定义的类型去设置,参数也支持多个值




    3、如何为小组件添加丰富的配置


    a、确定配置对象


    以这个demo为例,小组件只能显示一首古诗,但是app中有很多首古诗,这就可以创建多个 古诗 组件,然后通过动态配置,每个小组件想要显示不同的古诗。这样的例子还有很多,比如某个人有多张银行卡,每个组件显示不同银行卡余额




    b、配置intent文件


    category选项设置为View,然后勾选下图中的选项,现在我们可以只关注小组件选项,将快捷指令的勾选也取消,如下图




    c、intent添加参数


    使用参数列表中的 + 按钮,添加一个参数




    Type类型可以选择自定义的type




    参数添加完后,系统会在ClickBtnIntent类中生成相应的属性




    随后ClickBtnIntent 的实例将在运行时传递到 小组件扩展中,让你的小组件知道用户配置了什么,以及要显示什么




    d、代码中的更改


    StaticConfiguration 切换为 IntentConfiguration,相应的provider也改为IntentTimelineProvider,provider就不上截图了,可以去demo中的ClickBtn.swift文件查看




    现在运行APP,然后长按古诗小组件,选择编辑小组件,会弹出带有Btn Type的参数,点击Btn Type一栏弹出带有搜索的列表页面。 效果如下:




    显示的Btn Type就是下图中框选Display Name,自己可以随便起名字,中英文均可




    目前,带有搜索的列表页面是一个空白页面,如果想要使其有数据,则要都选Dynamic Options复选框,为其添加动态数据




    e、如何为列表添加动态数据?


    勾选了Dynamic Options复选框,系统会自动生成一个ClickBtnIntentHandling协议,可以点开ClickIntent类去查看,现在有了intent文件,有了新的可遵守协议,就需要有一个Extension去遵守协议,实现协议里边的方法,为搜索列表提供数据



    • 点击Project —> 新建target —> 搜索intent —> 选择 Intents Extentsion







    • 贴上类的方法,以及方法对应的效果图




    f、注意点


    实现IntentHandler时,Xcode会报找不到ClickBtnIntentHandling这个协议的错误,

    • 引入头文件 Intents
    • 需要将下图所标的地方做下修改



    六、APP创建多个Widget


    这个比较简单,按照demo中的例子处理一下就可以,如下图:




    目前测试,最多可以同时创建五个不同的Widget


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

    LangChain 是 LLM 交响乐的指挥家

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。 深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。 LangChain 是...
    继续阅读 »

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。


    深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。


    LangChain 是一个框架,它一直是我作为开发者旅途中的规则改变者。 LangChain 是一个独特的工具,它利用大语言模型(LLMs)的力量为各种使用案例构建应用程序。Harrison Chase 的这个创意于 2022 年 10 月作为开源项目首次亮相。从那时起,它就成为 GitHub 宇宙中一颗闪亮的明星,拥有高达 42,000 颗星,并有超过 800 名开发者的贡献。


    LangChain 就像一位大师,指挥着 OpenAI 和 HuggingFace Hub 等 LLM 模型以及 Google、Wikipedia、Notion 和 Wolfram 等外部资源的管弦乐队。它提供了一组抽象(链和代理)和工具(提示模板、内存、文档加载器、输出解析器),充当文本输入和输出之间的桥梁。这些模型和组件链接到管道中,这让开发人员能够轻而易举地快速构建健壮的应用程序原型。本质上,LangChain 是 LLM 交响乐的指挥家。


    LangChain 的真正优势在于它的七个关键模块:

    1. 模型:这些是构成应用程序主干的封闭或开源 LLM
    2. 提示:这些是接受用户输入和输出解析器的模板,这些解析器格式化 LLM 模型的输出。
    3. 索引:该模块准备和构建数据,以便 LLM 模型可以有效地与它们交互。
    4. 记忆:这为链或代理提供了短期和长期记忆的能力,使它们能够记住以前与用户的交互。
    5. :这是一种在单个管道(或“链”)中组合多个组件或其他链的方法。
    6. 代理人:根据输入决定使用可用工具/数据采取的行动方案。
    7. 回调:这些是在 LLM 运行期间的特定点触发以执行的函数。

    GitHub:python.langchain.com/


    什么是提示模板?


    在语言模型的世界中,提示是一段文本,指示模型生成特定类型的响应。顾名思义,提示模板是生成此类提示的可重复方法。它本质上是一个文本字符串,可以接收来自最终用户的一组参数并相应地生成提示。


    提示模板可以包含语言模型的说明、一组用于指导模型响应的少量示例以及模型的问题。下面是一个简单的例子:

    from langchain import PromptTemplate

    template = """
    I want you to act as a naming consultant for new companies.
    What is a good name for a company that makes {product}?
    """

    prompt = PromptTemplate(
    input_variables=["product"],
    template=template,
    )

    prompt.format(product="colorful socks")

    在此示例中,提示模板要求语言模型为生产特定产品的公司建议名称。product 是一个变量,可以替换为任何产品名称。


    创建提示模板


    在 LangChain 中创建提示模板非常简单。可以使用该类创建简单的硬编码提示 PromptTemplate。这些模板可以采用任意数量的输入变量,并且可以格式化以生成提示。以下是如何创建一个没有输入变量、一个输入变量和多个输入变量的提示模板:

    from langchain import PromptTemplate

    # No Input Variable 无输入变量
    no_input_prompt = PromptTemplate(input_variables=[], template="Tell me a joke.")
    print(no_input_prompt.format())

    # One Input Variable 一个输入变量
    one_input_prompt = PromptTemplate(input_variables=["adjective"], template="Tell me a {adjective} joke.")
    print(one_input_prompt.format(adjective="funny"))

    # Multiple Input Variables 多个输入变量
    multiple_input_prompt = PromptTemplate(
    input_variables=["adjective", "content"],
    template="Tell me a {adjective} joke about {content}."
    )
    print(multiple_input_prompt.format(adjective="funny", content="chickens"))

    总结


    总之,LangChain 中的提示模板是为语言模型生成动态提示的强大工具。它们提供了对提示的灵活性和控制,能够有效地指导模型的响应。无论是为特定任务创建语言模型还是探索语言模型的功能,提示模板都可以改变游戏规则。


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

    AI孙燕姿翻唱爆火,多亏这个开源项目!广西老表带头打造,上手指南已出

    明敏 发自 凹非寺 量子位 | 公众号 QbitAI AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的? 关键在于一个开源项目。 最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。 而如果在各大教程...
    继续阅读 »
    明敏 发自 凹非寺 量子位 | 公众号 QbitAI

    AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的?


    关键在于一个开源项目




    最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。


    而如果在各大教程中溜达一圈后就会发现,其中的关键秘诀,还是要靠一个名为so-vits-svc的开源项目。



    它提供了一种音色替换的办法,项目在今年3月发布。


    贡献成员应该大部分都来自国内,其中贡献量最高的还是一位玩明日方舟的广西老表。




    如今,项目已经停止更新了,但是星标数量还在蹭蹭上涨,目前已经到了8.4k。


    所以它到底实现了哪些技术能引爆这波趋势?


    一起来看。


    多亏了一个开源项目


    这个项目名叫SoftVC VITS Singing Voice Conversion(歌声转换)。


    它提供了一种音色转换算法,采用SoftVC内容编码器提取源音频语音特征,然后将矢量直接输入VITS,中间不转换成文本,从而保留了音高和语调。


    此外,还将声码器改为NSF HiFiGAN,可以解决声音中断的问题。


    具体分为以下几步:

    • 预训练模型
    • 准备数据集
    • 预处理
    • 训练
    • 推理

    其中,预训练模型这步是关键之一,因为项目本身不提供任何音色的音频训练模型,所以如果你想要做一个新的AI歌手出来,需要自己训练模型。


    而预训练模型的第一步,是准备干声,也就是无音乐的纯人声。


    很多博主使用的工具都是UVR_v5.5.0


    推特博主@歸藏介绍说,在处理前最好把声音格式转成WAV格式,因为So-VITS-SVC 4.0只认这个格式,方便后面处理。


    想要效果好一些,需要处理两次背景音,每次的设置不同,能最大限度提高干声质量。


    得到处理好的音频后,需要进行一些预处理操作。


    比如音频太长容易爆显存,需要对音频切片,推荐5-15秒或者再长一点也OK。


    然后要重新采样到44100Hz和单声道,并自动将数据集划分为训练集和验证集,生成配置文件。再生成Hubert和f0。


    接下来就能开始训练和推理了。


    具体的步骤可以移步GitHub项目页查看(指路文末)。


    值得一提的是,这个项目在今年3月上线,目前贡献者有25位。从贡献用户的简介来看,很多应该都来自国内。


    据说项目刚上线时也有不少漏洞并且需要编程,但是后面几乎每一天都有人在更新和修补,现在的使用门槛已经降低了不少。


    目前项目已经停止更新了,但还是有一些开发者创建了新的分支,比如有人做出了支持实时转换的客户端。




    项目贡献量最多的一位开发者是Miuzarte,从简介地址判断应该来自广西。




    随着想要上手使用的人越来越多,也有不少博主推出了上手难度更低、更详细的食用指南。


    歸藏推荐的方法是使用整合包来推理(使用模型)和训练,还有B站的Jack-Cui展示了Windows下的步骤指南(http://www.bilibili.com/read/cv2237…


    需要注意的是,模型训练对显卡要求还是比较高的,显存小于6G容易出现各类问题。


    Jack-Cui建议使用N卡,他用RTX 2060 S,训练自己的模型大概用了14个小时


    训练数据也同样关键,越多高质量音频,就意味着最后效果可以越好。


    还是会担心版权问题


    值得一提的是,在so-vits-svc的项目主页上,着重强调了版权问题。



    警告:请自行解决数据集的授权问题。因使用未经授权的数据集进行培训而产生的任何问题及其一切后果,由您自行承担责任。存储库及其维护者、svc开发团队,与生成结果无关!





    这和AI画画爆火时有点相似。


    因为AI生成内容的最初数据取材于人类作品,在版权方面的争论不绝于耳。


    而且随着AI作品盛行,已经有版权方出手下架平台上的视频了。


    据了解,一首AI合成的《Heart on My Sleeve》在油管和Tik Tok上爆火,它合成了Drake和Weekend演唱的版本。


    但随后,Drake和Weekend的唱片公司环球音乐将这个视频从平台上下架了,并在声明里向潜在的仿冒者发问,“是要站在艺术家、粉丝和人类创造性表达的一边,还是站在Deepfake、欺诈和拒付艺术家赔偿的一边?”


    此外,歌手Drake也在ins上对AI合成翻唱歌曲表达了不满。


    而另一边,也有人选择拥抱这项技术。


    加拿大歌手Grimes表示,她愿意让别人使用自己的声音合成歌曲,但是要给她一半版权费。


    GitHub地址:

    github.com/svc-develop…


    参考链接:

    [1]mp.weixin.qq.com/s/bXD1u6ysY…

    [2]http://www.vulture.com/article/ai-…


    —  —


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

    PAG动效框架源码笔记 (四)渲染框架

    iOS
    前言 PAG采用自研TGFX特效渲染引擎,抽象分离了接口及平台实现类,可以扩展支持多种图形渲染库,比如OpenGL、Metal等 TGFX引擎是如何实现纹理绘制?本文基于OpenGL图形库分析讲解TGFX渲染框架分层及详细架构设计。开始之前,先提一个问题: 绘...
    继续阅读 »

    前言


    PAG采用自研TGFX特效渲染引擎,抽象分离了接口及平台实现类,可以扩展支持多种图形渲染库,比如OpenGL、Metal等


    TGFX引擎是如何实现纹理绘制?本文基于OpenGL图形库分析讲解TGFX渲染框架分层及详细架构设计。开始之前,先提一个问题:


    绘制一个Texture纹理对象,一般需要经历哪些过程?


    渲染流程


    通常情况下,绘制一个Texture纹理对象到目标Layer上,可以抽象为以下几个阶段:


    1. 获取上下文: 通过EGL获取Context绘制上下文,提供与渲染设备交互的能力,比如缓冲区交换、Canvas及Paint交互等


    2. 定义着色器: 基于OpenGL的着色器语言(GLSL)编写着色器代码,编写自定义顶点着色器和片段着色器代码,编译、链接加载和使用它们


    3. 绑定数据源: 基于渲染坐标系几何计算绑定顶点数据,加载并绑定纹理对象给GPU,设置渲染目标、混合模式等


    4. 渲染执行: 提交渲染命令给渲染线程,转化为底层图形API调用、并执行实际的渲染操作




    关于OpenGL完整的渲染流程,网上有比较多的资料介绍,在此不再赘述,有兴趣的同学可以参考 OpenGL ES Pipeline


    框架层级


    TGFX框架大致可分为三大块:


    1. Drawable上下文: 基于EGL创建OpenGL上下文,提供与渲染设备交互的能力


    2. Canvas接口: 定义画布Canvas及画笔Paint,对外提供渲染接口、记录渲染状态以及创建绘制任务等


    3. DrawOp执行: 定义并装载着色器函数,绑定数据源,执行实际渲染操作


    为了支持多平台,TGFX定义了一套完整的框架基类,实现框架与平台的物理隔离,比如矩阵对象Matrix、坐标Rect等,应用上层负责平台对象与TFGX对象的映射转化

    - (void)setMatrix:(CGAffineTransform)value {
    pag::Matrix matrix = {};
    matrix.setAffine(value.a, value.b, value.c, value.d, value.tx, value.ty);
    _pagLayer->setMatrix(matrix);
    }

    Drawable上下文


    PAG通过抽象Drawable对象,封装了绘制所需的上下文,其主要包括以下几个对象


    1. Device(设备): 作为硬件设备层,负责与渲染设备交互,比如创建维护EAGLContext等


    2. Window(窗口): 拥有一个Surface,负责图形库与绘制目标的绑定,比如将的opengl的renderBuffer绑定到CAEAGLLayer上;


    3. Surface(表面): 创建canvas画布提供可绘制区域,对外提供flush绘制接口;当窗口尺寸发生变化时,surface会创建新的canvas


    4. Canvas(画布): 作为实际可绘制区域,提供绘制api,进行实际的绘图操作,比如绘制一个image或者shape等



    详细代码如下:


    1、Device创建Context
    std::shared_ptr<GLDevice> GLDevice::Make(void* sharedContext) {
    if (eaglShareContext != nil) {
    eaglContext = [[EAGLContext alloc] initWithAPI:[eaglShareContext API]
    sharegroup:[eaglShareContext sharegroup]];
    } else {
    // 创建Context
    eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (eaglContext == nil) {
    eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    }
    }
    auto device = EAGLDevice::Wrap(eaglContext, false);
    return device;
    }

    std::shared_ptr<EAGLDevice> EAGLDevice::Wrap(EAGLContext* eaglContext, bool isAdopted) {
    auto oldEAGLContext = [[EAGLContext currentContext] retain];
    if (oldEAGLContext != eaglContext) {
    auto result = [EAGLContext setCurrentContext:eaglContext];
    if (!result) {
    return nullptr;
    }
    }
    auto device = std::shared_ptr<EAGLDevice>(new EAGLDevice(eaglContext),
    EAGLDevice::NotifyReferenceReachedZero);
    if (oldEAGLContext != eaglContext) {
    [EAGLContext setCurrentContext:oldEAGLContext];
    }
    return device;
    }

    // 获取Context
    bool EAGLDevice::makeCurrent(bool force) {
    oldContext = [[EAGLContext currentContext] retain];
    if (oldContext == _eaglContext) {
    return true;
    }
    if (![EAGLContext setCurrentContext:_eaglContext]) {
    oldContext = nil;
    return false;
    }
    return true;
    }

    2、Window创建Surface,绑定RenderBuffer
    std::shared_ptr<Surface> EAGLWindow::onCreateSurface(Context* context) {
    auto gl = GLFunctions::Get(context);
    ...
    gl->genFramebuffers(1, &frameBufferID);
    gl->bindFramebuffer(GL_FRAMEBUFFER, frameBufferID);
    gl->genRenderbuffers(1, &colorBuffer);
    gl->bindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
    gl->framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer);
    auto eaglContext = static_cast<EAGLDevice*>(context->device())->eaglContext();
    // 绑定到CAEAGLLayer上
    [eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
    ...
    GLFrameBufferInfo glInfo = {};
    glInfo.id = frameBufferID;
    glInfo.format = GL_RGBA8;
    BackendRenderTarget renderTarget = {glInfo, static_cast<int>(width), static_cast<int>(height)};
    // 创建Surface
    return Surface::MakeFrom(context, renderTarget, ImageOrigin::BottomLeft);
    }

    // 通过renderTarget持有context、frameBufferID及Size
    std::shared_ptr<Surface> Surface::MakeFrom(Context* context,
    const BackendRenderTarget& renderTarget,
    ImageOrigin origin, const SurfaceOptions* options) {
    auto rt = RenderTarget::MakeFrom(context, renderTarget, origin);
    return MakeFrom(std::move(rt), options);
    }

    3、Surface创建Canvas及flush绘制
    Canvas* Surface::getCanvas() {
    // 尺寸变化时会清空并重新创建canvas
    if (canvas == nullptr) {
    canvas = new Canvas(this);
    }
    return canvas;
    }

    bool Surface::flush(BackendSemaphore* signalSemaphore) {
    auto semaphore = Semaphore::Wrap(signalSemaphore);
    // drawingManager创建tasks,装载绘制pipiline
    renderTarget->getContext()->drawingManager()->newTextureResolveRenderTask(this);
    auto result = renderTarget->getContext()->drawingManager()->flush(semaphore.get());
    return result;
    }

    4、渲染流程
    bool PAGSurface::draw(RenderCache* cache, std::shared_ptr<Graphic> graphic,
    BackendSemaphore* signalSemaphore, bool autoClear) {
    // 获取context上下文
    auto context = lockContext(true);
    // 获取surface
    auto surface = drawable->getSurface(context);
    // 通过canvas画布
    auto canvas = surface->getCanvas();
    // 执行实际绘制
    onDraw(graphic, surface, cache);
    // 调用flush
    surface->flush();
    // glfinish
    context->submit();
    // 绑定GL_RENDERBUFFER
    drawable->present(context);
    // 释放context上下文
    unlockContext();
    return true;
    }

    Canvas接口


    Canvas API主要包括画布操作及对象绘制两大类:


    画布操作包括Matrix矩阵变化、Blend融合模式、画布裁切等设置,通过对canvasState画布状态的操作实现绘制上下文的切换


    对象绘制包括Path、Shape、Image以及Glyph等对象的绘制,结合Paint画笔实现纹理、文本、图形、蒙版等多种形式的绘制及渲染

    class Canvas {
    // 画布操作
    void setMatrix(const Matrix& matrix);
    void setAlpha(float newAlpha);
    void setBlendMode(BlendMode blendMode);

    // 绘制API
    void drawRect(const Rect& rect, const Paint& paint);
    void drawPath(const Path& path, const Paint& paint);
    void drawShape(std::shared_ptr<Shape> shape, const Paint& paint);
    void drawImage(std::shared_ptr<Image> image, const Matrix& matrix, const Paint* paint = nullptr);
    void drawGlyphs(const GlyphID glyphIDs[], const Point positions[], size_t glyphCount,
    const Font& font, const Paint& paint);
    };
    // CanvasState记录当前画布的状态,包括Alph、blend模式、变化矩阵等
    struct CanvasState {
    float alpha = 1.0f;
    BlendMode blendMode = BlendMode::SrcOver;
    Matrix matrix = Matrix::I();
    Path clip = {};
    uint32_t clipID = kDefaultClipID;
    };

    // 通过save及restore实现绘制状态的切换
    void Canvas::save() {
    auto canvasState = std::make_shared<CanvasState>();
    *canvasState = *state;
    savedStateList.push_back(canvasState);
    }

    void Canvas::restore() {
    if (savedStateList.empty()) {
    return;
    }
    state = savedStateList.back();
    savedStateList.pop_back();
    }

    DrawOp执行


    DrawOp负责实际的绘制逻辑,比如OpenGL着色器函数的创建装配、顶点及纹理数据的创建及绑定等


    TGFX抽象了FillRectOp矩形绘制Op,可以覆盖绝大多数场景的绘制需求


    当然,其还支持其它类型的绘制Op,比如ClearOp清屏、TriangulatingPathOp三角图形绘制Op等

    class DrawOp : public Op {
    // DrawOp通过Pipiline实现多个_colors纹理对象及_masks蒙版的绘制
    std::vector<std::unique_ptr<FragmentProcessor>> _colors;
    std::vector<std::unique_ptr<FragmentProcessor>> _masks;
    };

    // 矩形实际绘制执行者
    class FillRectOp : public DrawOp {
    FillRectOp(std::optional<Color> color, const Rect& rect, const Matrix& viewMatrix,
    const Matrix& localMatrix);
    void onPrepare(Gpu* gpu) override;
    void onExecute(OpsRenderPass* opsRenderPass) override;
    };

    总结


    本文结合OpenGL讲解了TGFX渲染引擎的大概框架结构,让各位有了一个初步认知


    接下来将结合image纹理绘制介绍TGFX渲染引擎详细的绘制渲染流程,欢迎大家关注点赞!


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

    SwiftUI关于菜单 iOS 的长按 & macOS 右键的实现

    iOS
    长按 按钮或者图片出现菜单是个很平常的操作。 从app的icon 到app 内部的按钮 可以将内部的一些操作整合到这个特点内 SwiftUI 自带的菜单选择 ContextMenu 代码 iOS 效果 macOS 在mac上不是长按了,是右键的菜单操作 文案...
    继续阅读 »

    长按 按钮或者图片出现菜单是个很平常的操作。


    从app的icon 到app 内部的按钮 可以将内部的一些操作整合到这个特点内


    SwiftUI 自带的菜单选择 ContextMenu


    代码




    iOS 效果



    macOS


    在mac上不是长按了,是右键的菜单操作



    文案可能要修改一下,应该叫 右键


    这里有一个有趣的点,mac 版本的样式是没有图标。必须加一句

    Button(action: { fileData.selectedFilesToOperate = [item] //单个  
    fileWindow.isShowMoveFileView = true })
    { Label("移动", systemImage: "folder")
    .labelStyle(.titleAndIcon)
    }

    但是现实的情况往往没有如此的简单,至少产品和老板的需求,都不是那么简单。下面几个我自己遇到的情况
    可能不太全面,但是按图索骥应该可以给看遇到相似问题的人一点启发的感觉


    问题1 菜单 不能太单调,分别来显示

    Section {
    Button1
    Button2 ....
    }

    用section 包裹 可以让菜单有明显的分区



    问题2 菜单里面放点别的


    那再放开一点,,contextMenu 内部 放点别的

          contextMenu {
    // picker
    // list
    // toggle
    // image...
    }



    放入单选记得选什么的 Picker



    放入子菜单


    这里用到了 Menu 这个标签


    这个表情 也是个菜单,点击就有,不用长按。


    菜单里面放菜单的效果


    Menu {

                                Picker(selection: $sort, label: Text("Sorting options")) {

                                    Text("Size").tag(0)

                                    Text("Date").tag(1)

                                    Text("Location").tag(2)

                                }

                            } label: {

                                Label("Sort", systemImage: "arrow.up.arrow.down")

                            }

    这个效果挺有意思,和mac 的右键的子菜单一个效果。



    这个放一切UI的效果,确实比较有趣。有兴趣可以尝试放入更丰富的控件。


    SwiftUI 的控件我个人感觉的套路

    1. 一切view 都是声明的方式,靠@State 或者@Publish 一些的Modify来控制控件的显示数据
    2. 因为没有了生命周期,对于onAppair 和DisAppair的控制放在了每一个控件上的@ViewBuilder上,这个可以自定义,开始的时候都用自带的 @ViewBuilder
    3. View 都是Struct,class用的不多。
    4. View 里面包View,尽量做到了控件复用。而且是挑明了就是,比如之前的Text里面label,Button里面的Label,NavigationLink里面的View(也可以一切不同类型的View)

    个人感觉这些都是在表面SwiftUI 打破以前Swift UIKit或者是OC中的UIKit的思维逻辑。


    既: UI廉价 刷新廉价


    让程序员 特别是iOS 开发过程中,不同状态的刷新UI ,回调刷新UI的开发复杂度


    总结


    对于一个控件的开始编写,到不停叠加复杂的情况,还有许多场景还没遇到和想到。目前SwiftUI的源码和网上的资料,还不如OC 如此内核的解析资料丰富。但是未来的iOS开发 一定是SwiftUI的时代,特别是对于个人开发者相比OC 友好程度明显。


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

    禁止别人调试自己的前端页面代码

    web
    🎈 为啥要禁止? 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码 🎈 无限 debugger 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点...
    继续阅读 »

    🎈 为啥要禁止?



    • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据

    • 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码


    禁止调试


    🎈 无限 debugger



    • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行

    • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的

    • 基础代码如下:


    /**
    * 基础禁止调试代码
    */

    (() => {
    function ban() {
    setInterval(() => {
    debugger;
    }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    基础禁止调试


    🎈 无限 debugger 的对策



    • 如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大

    • 可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger

    • 这种方式虽然能去掉碍眼的 debugger,但是无法通过左侧的行号添加 breakpoint


    取消禁止对策


    🎈 禁止断点的对策



    • 如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpointfalse 也无用

    • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的


    (() => {
    function ban() {
    setInterval(() => { debugger; }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    禁止断点


    🎈 忽略执行的代码



    • 通过添加 add script ignore list 需要忽略执行代码行或文件

    • 也可以达到禁止无限 debugger


    忽略执行的代码


    🎈 忽略执行代码的对策



    • 那如何针对上面操作的恶意用户呢

    • 可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对

    • Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件

    • 当然使用的时候,为了更加的安全,最好使用加密后的脚本


    // 加密前
    (() => {
    function ban() {
    setInterval(() => {
    Function('debugger')();
    }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    // 加密后
    eval(function(c,g,a,b,d,e){d=String;if(!"".replace(/^/,String)){for(;a--;)e[a]=b[a]||a;b=[function(f){return e[f]}];d=function(){return"\w+"};a=1}for(;a--;)b[a]&&(c=c.replace(new RegExp("\b"+d(a)+"\b","g"),b[a]));return c}('(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',9,9,"block function setInterval Function debugger 50 try catch err".split(" "),0,{}));

    解决对策


    🎈 终极增强防调试代码



    • 为了让自己写出来的代码更加的晦涩难懂,需要对上面的代码再优化一下

    • Function('debugger').call() 改成 (function(){return false;})['constructor']('debugger')['call']();

    • 并且添加条件,当窗口外部宽高和内部宽高的差值大于一定的值 ,我把 body 里的内容换成指定内容

    • 当然使用的时候,为了更加的安全,最好加密后再使用


    (() => {
    function block() {
    if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
    document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
    }
    setInterval(() => {
    (function () {
    return false;
    }
    ['constructor']('debugger')
    ['call']());
    }, 50);
    }
    try {
    block();
    } catch (err) { }
    })();

    终极增强防调试

    收起阅读 »

    记录用前端代替后端生成zip的过程,速度快了 57 倍!!!

    业务场景: 产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。 管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)...
    继续阅读 »

    业务场景:


    产品有个功能是设置主题。类似手机自动切换壁纸,以及其他功能颜色,icon,字体等。


    管理员需要在后端管理系统多次下载不同主题,(至于要干啥就不说了...),主题中可能有 30 ~ 100个高清壁纸, icon 等。现在每次下载主题(31张高清图片)至少需要 10s。有什么方法能够优化下。



    因为代码不具备可复用性,因此部分代码直接省略,思路为主


    原始逻辑


      public async getZip(themeId: string, res: any) {
    const theme = await this.model.findById(themeId); // 从数据库

    // 这里需要借用一个服务器上的主题模板文件夹 template/,

    /*
    theme = {
    wallpapers: [
    { url: 'https://亚马逊云.com/1.jpg', ... },
    ...
    ]
    }
    */


    // for 循环遍历 theme.wallpapers , 并通过 fetch 请求 url,将其写进 template/static/wallpapers 文件夹中
    theme.wallpapers.map((item) => {
    const response = await fetch(item.url);
    const buffer = new Uint8Array(await response.arrayBuffer());
    await fs.writeFile(`template/wallpapers/${fileName}`, buffer);
    })

    // ... 还有其他一些处理

    // 将 template 压缩成 zip 文件,发送给前端
    }

    思考 ing ...


    1 利用图片可以被浏览器缓存


    当一次下载主题从请求亚马逊云的图片数据,这步没有问题。 但是当重复下载的时候,之前下载过的图片又会再次下载,操作人员每次都需要等个十几秒,这就不太友好了。这部分时间花费还是挺多的。


    可以利用下浏览器能够将图片缓存到 disk cache 中的特点,将这部分的代码逻辑放到前端完成,因为还需要对压缩包中的文件做一些处理,因此需要借助下 jszip 这个库。


    看下改后的代码



    onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme()
    const template = api.getTemplate() // Blob

    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

    console.time('handle images')
    const wallpaperList = theme.wallpapers
    for (const wallpaper of wallpaperList) {
    const response = await fetch(wallpaper.url) // 请求图片数据
    const buffer = new Uint8Array(await response.arrayBuffer())
    const fileName = wallpaper.url.split('/').pop()
    zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true }) // 写进压缩包
    }
    console.timeEnd('handle images') // 统计用时

    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...

    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
    (base64) => {
    const link = document.createElement('a')
    link.href = 'data:application/zip;base64,' + base64
    link.download = 'template.zip'
    link.target = '_blank'
    link.style.display = 'none'

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    },
    (err) => {
    console.log('打包失败', err)
    }
    )
    }

    优化完成


    当第一次下载时,handle images 步骤耗时 20 - 21 s,流程和后端差不多。


    当第二次下载时,handle images 步骤耗时 0.35s - 0.45 s。会直接读取 disk cache 中的图片数据,50 ms 内就完成了。


    速度快了 57 倍有余!!!, 你还能想到其他优化方式吗?继续往后看 🍒


    第一次请求各个图片耗时
    image.png


    第二次请求各个图片耗时
    image.png


    2 并发请求


    我们都知道,浏览器会为每个域名维持 6 个 TCP 链接(再拓展还有域名分片知识),我们是否可以利用这个特点做些什么?


    答案是:并发上传


    通过上面的代码,可以看到,每个图片请求都是串行的,一个图片请求完了再进行下一个图片请求。我们一次请求 4 个图片,这样就更快了。


    首先写一个能够管理并发任务的类


    export class TaskQueue {
    public queue: {
    task: <T>() => Promise<T>
    resolve: (value: unknown) => void
    reject: (reason?: any) => void
    }[]
    public runningCount: number // 正在执行的任务数量
    public tasksResloved?: (value: unknown) => void
    public tasksRejected?: (reason?: any) => void

    public constructor(public maxConcurrency: number = 4) { // 最多同时执行 4 个任务
    this.queue = [] // 任务队列
    this.runningCount = 0
    }

    // 添加任务
    public addTask(task) {
    return new Promise((resolve, reject) => {
    this.queue.push({ task, resolve, reject })
    })
    }

    // 执行
    public run() {
    return new Promise((resoved, rejected) => {
    this.tasksResloved = resoved
    this.tasksRejected = rejected
    this.nextTask()
    })
    }

    private nextTask() {
    if (this.queue.length === 0 && this.runningCount === 0) {
    this.tasksResloved?.('done')
    return
    }

    // 如果任务队列中还有任务, 并且没有到最大执行任务数,就继续取出任务执行
    while (this.queue.length > 0 && this.runningCount < this.maxConcurrency) {
    const { task, resolve, reject } = this.queue.shift()
    this.runningCount++
    task()
    .then((res) => {
    this.runningCount--
    resolve(res)
    this.nextTask()
    })
    .catch((e) => {
    this.runningCount--
    reject(e)
    this.nextTask()
    })
    }
    }
    }


    改造代码


    onDownload () {
    // 请求拿到 theme 数据
    const theme = api.getTheme()
    const template = api.getTemplate() // Blob

    const zip = new JSZip()
    await zip.loadAsync(getTmplResp) // 读取 template.zip 文件数据

    console.time('handle images')
    const wallpaperList = theme.wallpapers

    // 注释之前的逻辑
    // for (const wallpaper of wallpaperList) {
    // const response = await fetch(wallpaper.url)
    // const buffer = new Uint8Array(await response.arrayBuffer())
    // const fileName = wallpaper.url.split('/').pop()
    // zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
    // }

    const taskQueue = new TaskQueue() // 新建任务队列,默认同时执行 4 个
    for (const wallpaper of wallpaperList) {
    taskQueue
    .addTask(() => fetch(wallpaper.url)) // 添加任务
    .then(async (res) => { // 任务执行完后的回调
    const buffer = new Uint8Array(await (res as Response).arrayBuffer())
    const fileName = wallpaper.url.split('/').pop()
    zip.file(`static/wallpapers/${fileName}`, buffer, { binary: true })
    })
    .catch((e) => console.log('壁纸获取失败', e))
    }
    await taskQueue.run() // 等待所有图片都拿到
    console.timeEnd('handle images') // 统计用时

    // 还需要读取 template.zip 中的 config.json, 然后修改,重新保存到 template.zip 中
    ...

    // 导出 template.zip
    zip.generateAsync({ type: 'base64' }).then(
    (base64) => {
    const link = document.createElement('a')
    link.href = 'data:application/zip;base64,' + base64
    link.download = 'template.zip'
    link.target = '_blank'
    link.style.display = 'none'

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    },
    (err) => {
    console.log('打包失败', err)
    }
    )
    }

    大功告成!


    当第一次下载时,handle images 步骤耗时 7 s,速度是之前的 3 倍。


    当第二次下载时,handle images 步骤耗时 0.25s,速度是之前的 1.4 - 1.8


    3 更多的可能


    越来越感觉到计算机网络的重要性, 还有未实现的优化方式:



    1. 域名分片,更多的并发(也有劣势 ,比如 每个域都需要额外的 DNS 查找成本以及建立每个 TCP 连接的开销, TCP 慢启动带宽利用不足)

    2. 升级 HTTP2 这不是靠前端一人能够完成的


    如果学到了新知识,麻烦点个
    作者:marh
    来源:juejin.cn/post/7267418197746270271
    👍 和 ⭐

    收起阅读 »

    公司没钱了,工资发不出来,作为员工怎么办?

    公司没钱了,工资发不出来,作为员工怎么办? 现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发80%、50%工资这样的操作。 员工遇到这种情况,无非以下几种选择。 认...
    继续阅读 »

    公司没钱了,工资发不出来,作为员工怎么办?


    现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发80%、50%工资这样的操作。


    员工遇到这种情况,无非以下几种选择。



    1. 认同公司的决策,愿意跟公司共同进退。

    2. 不认同公司的决策,我要离职。

    3. 不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。

    4. 不认同公司的决策,我也不主动离职。准备跟公司battle,”你们这么做是不合法滴“


    你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。


    我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我N+1的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。


    为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。


    离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。


    如果公司后面没钱了,欠的工资还拿得到吗?


    我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。


    如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么

    作者:石云升
    来源:juejin.cn/post/7156242740034928671
    股东还需要按照股份比例偿还债务。

    收起阅读 »

    差点让我崩溃的“全选”功能

    web
    今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图: 开始,我是这样写代码的: f...
    继续阅读 »

    今天在实现电商结算功能的时候遇到了个全选功能,做之前不以为然,做之后抓狂。为了让自己全身心投入到对这个问题的攻克,我特意将其拿出来先创建一个demo单独实现下。不搞不知道,一搞吓一跳,下面是我demo在浏览器运行的截图:


    1679229898519.png
    开始,我是这样写代码的:


        for (let i = 0; i < aCheckbox.length; i++) {
    aCheckbox[i].checked = this.checked;
    }
    });

    for (let i = 0; i < aCheckbox.length; i++) {
    aCheckbox[i].addEventListener("click", function () {
    for (let index = 0; index < aCheckbox.length; index++) {
    if (aCheckbox[index].checked) {
    oAllchecked.checked = aCheckbox[index].checked;
    } else {
    oAllchecked.checked = !aCheckbox[index].checked;
    }
    }
    });
    }

    点击全选这个功能不难,主要问题出现在如何保证另外两个复选框在其中一个没有选中的情况下,全选的这个复选框没有选中。苦思良久,最后通过查找资料看到了如今的代码:


        aCheckbox[i].addEventListener("click", function () {
    let flag = true;
    for (let index = 0; index < aCheckbox.length; index++) {
    console.log(aCheckbox[index].checked);
    if (!aCheckbox[index].checked) {
    flag = false;
    break;
    }
    }
    oAllchecked.checked = flag;
    });
    }

    功能完美就解决,第一个代码问题的原因是‘aCheckbox[index].checked’这个判断不能解决两个复选框什么时候一个选中一个没选中的问题。这个问题不解决也就不能让全选复选框及时更新正确的选中状态了。


    而下面这个代码通过设置一个中间值flag,及时记录每个复选框按钮的选中状态,能准确的赋值给全选功能的复

    作者:一个对前端不离不弃的中年菜鸟
    来源:juejin.cn/post/7212942861518864421
    选框按钮。于是这个需求就解决了~

    收起阅读 »

    前端使用a链接下载内容增加loading效果

    web
    问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。 代码如下: // utils.js const XLSX = require('xlsx') // 将一个sheet转成最终...
    继续阅读 »

    1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。

    2. 代码如下:


    // utils.js
    const XLSX = require('xlsx')
    // 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
    export const sheet2blob = (sheet, sheetName) => {
    sheetName = sheetName || 'sheet1'
    var workbook = {
    SheetNames: [sheetName],
    Sheets: {}
    }
    workbook.Sheets[sheetName] = sheet
    // 生成excel的配置项
    var wopts = {
    bookType: 'xlsx', // 要生成的文件类型
    bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
    type: 'binary'
    }
    var wbout = XLSX.write(workbook, wopts)
    var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
    // 字符串转ArrayBuffer
    function s2ab(s) {
    var buf = new ArrayBuffer(s.length)
    var view = new Uint8Array(buf)
    for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
    return buf
    }
    return blob
    }

    /**
    * 通用的打开下载对话框方法,没有测试过具体兼容性
    * @param url 下载地址,也可以是一个blob对象,必选
    * @param saveName 保存文件名,可选
    */

    export const openDownloadDialog = (url, saveName) => {
    if (typeof url === 'object' && url instanceof Blob) {
    url = URL.createObjectURL(url) // 创建blob地址
    }
    var aLink = document.createElement('a')
    aLink.href = url
    aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
    var event
    if (window.MouseEvent) event = new MouseEvent('click')
    else {
    event = document.createEvent('MouseEvents')
    event.initMouseEvent(
    'click',
    true,
    false,
    window,
    0,
    0,
    0,
    0,
    0,
    false,
    false,
    false,
    false,
    0,
    null
    )
    }
    aLink.dispatchEvent(event)
    }

    <el-button
    @click="clickExportBtn"
    >
    <i class="el-icon-download"></i>下载数据
    </el-button>
    <div class="mongolia" v-if="loadingSummaryData">
    <el-icon class="el-icon-loading loading-icon">
    <Loading />
    </el-icon>
    <p>loading...</p>
    </div>


    clickExportBtn: _.throttle(async function() {
    const downloadDatas = []
    const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
    summaryDataForDownloads.map(summaryItem =>
    downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
    )
    // donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
    this.loadingSummaryData = true
    const downloadBlob = aoa2sheet(downloadDatas.flat(1))
    openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
    this.loadingSummaryData = false
    }, 2000),

    // css
    .mongolia {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.9);
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.5rem;
    color: #409eff;
    z-index: 9999;
    }
    .loading-icon {
    color: #409eff;
    font-size: 32px;
    }


    1. 解决方案探究:




    • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:



      • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。

      • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。

      • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。

      • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。




    • 查阅资料后找到了如下几种方案:





        1. 使用 setTimeout 使 openDownloadDialog 异步执行


        clickExport() {
        this.loadingSummaryData = true;

        setTimeout(() => {
        openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

        this.loadingSummaryData = false;
        });
        }




        1. 对 openDownloadDialog 内部进行优化



        • 避免大循环或递归逻辑

        • 将计算工作分批进行

        • 使用 Web Worker 隔离耗时任务


          • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。





              1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。





              1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。





              1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。





              1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。





              1. 代码应该是自包含的,不依赖外部变量或状态。





              1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。





              1. 避免修改或依赖全局作用域,比如定义全局变量等。






          • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。


            // 创建 Worker
            const worker = new Worker('downloadWorker.js');

            // 点击下载时向 Worker 发送消息
            function clickDownload() {

            showLoading();

            worker.postMessage({
            url: fileURL,
            filename: 'report.xlsx'
            });

            worker.onmessage = function(e) {
            // 收到下载完成的消息
            hideLoading();
            }

            }

            // 显示 loading
            function showLoading() {
            loading.style.display = 'block';
            }

            // 隐藏 loading
            function hideLoading() {
            loading.style.display = 'none';
            }

            // downloadWorker.js

            onmessage = function(e) {
            const url = e.data.url;
            const filename = e.data.filename;

            // 创建并点击链接触发下载
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            a.click();

            postMessage('下载完成');
            }

            <div id="loading" style="display:none;">
            Downloading...
            </div>

            <button onclick="clickDownload()">
            Download
            </button>

            <script src="downloadWorker.js"></script>








        1. 使用 requestIdleCallback 进行调度


        clickExport() {
        this.loadingSummaryData = true;

        requestIdleCallback(() => {
        openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

        this.loadingSummaryData = false;
        });
        }



      • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:


        使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。


        因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。


        requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。


        但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。


        所以需要权衡执行速度和避免阻塞之间的关系:




        • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。




        • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。




        偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。


        此外,可以结合两者试试:


        clickExport() {

        this.loadingSummaryData = true;

        setTimeout(() => {

        requestIdleCallback(() => {
        openDownloadDialog(downloadBlob);
        });

        this.loadingSummaryData = false;

        }, 200);

        }

        setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。





        1. 分析线程模型,找到具体的阻塞点



        • 使用 Performance 工具分析线程

        • debugger 及 console.log 打印关键函数的执行时间

        • 检查是否有非主线程的任务阻塞了主线程





        1. 调整 vue 的批量更新策略


        new Vue({
        config: {
        // 修改批量更新的队列长度
        batchUpdateDuration: 100
        }
        })

        作者:李卫泽
        来源:juejin.cn/post/7268050036474609683
        i>


    收起阅读 »

    const声明的变量还能修改?原理都在这了

    web
    前言 const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。 本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。 耐心看完,...
    继续阅读 »

    前言


    const关键字用于声明只读的常量,一旦被赋值,其值就不能再被改变。但是,这并不意味着该变量持有的值是不可变的。


    本文将结合案例逐步探讨JavaScript中const声明的工作原理,解释为何这些看似常量的变量却可以被修改,并探讨其实际应用。


    耐心看完,你一定有所收获。


    giphy.gif


    正文


    const关键字用于声明一个变量,该变量的值在其生命周期中不会被重新赋值。


    现象


    1. 基本数据类型


    对于基本数据类型(如数字、字符串、布尔值),const确保变量的值不会改变。


    const num = 42;
    // num = 43; // 这会抛出错误

    2. 对象


    对于对象,仍然可以修改对象的属性,但不能重新赋值整个对象。


    const girlfriend = {
    name: "小宝贝"
    };

    girlfriend.name = "亲爱的"; // 这是允许的,因为你只是修改了对象的一个属性

    // girlfriend = { name: "亲爱的" }; // 这会抛出错误,因为你试图改变obj的引用

    假如你有个女朋友,也许并没有,但我们可以假设,她的名字或者你平时叫她的昵称是"小宝贝"。


    有一天,你心血来潮,想换个方式叫她,于是叫她"亲爱的"。这完全没问题,因为你只是给她换了个昵称,她本人并没有变。


    但是,如果有一天你看到另一个女生,你却说:“哎,这不是亲爱的吗?”这就出大问题了!因为你把一个完全不同的人当成了你的女朋友。


    这就像你试图改变girlfriend的引用,把它指向了一个新的对象。


    JavaScript不允许这样做,因为你之前已经明确地告诉它,girlfriend就是那个你叫"小宝贝"的女朋友,你不能突然把另一个人说成她。


    154eb98c10eaf8356b5da0e44b9e9fe6.gif


    简单来说,你可以随时给你的女朋友起个新昵称,但你不能随便把别的女生当成你的女朋友。


    3. 数组


    对于数组,你可以修改、添加或删除元素,但不能重新赋值整个数组。


    const arr = [1, 2, 3];

    arr[0] = 4; // 这是允许的,因为你只是修改了数组的一个元素
    arr.push(5); // 这也是允许的,因为你只是向数组添加了一个元素

    // arr = [6, 7, 8]; // 这会抛出一个错误,因为你试图改变arr的引用

    假设arr是你的超市购物袋,里面有三个苹果,分别标记为1、2和3。


    你检查了第一个苹果,觉得它不够新鲜,所以你把它替换成了一个新的苹果,标记为4。这就像你修改数组的一个元素。这完全可以,因为你只是替换了袋子里的一个苹果。


    后来,你决定再放一个苹果进去,标记为5。这也没问题,因为你只是向袋子里添加了一个苹果。


    苹果再变,袋子仍然是原来的袋子。


    但是,当你试图拿个新的装着6、7、8的购物袋来代替你原来的袋子时,就不对了。你不能拿了一袋子苹果,又扔在那不管,反而又去拿了一袋新的苹果。


    你礼貌吗?


    f2e0de05371993107839d315b5639a30.jpg


    你可以随时替换袋里的苹果或者放更多的苹果进去,但你不能拿了一袋不要了又拿一袋。


    原理


    在JavaScript中,const并不是让变量的值变得不可变,而是让变量指向的内存地址不可变。换句话说,使用const声明的变量不能被重新赋值,但是其所指向的内存中的数据是可以被修改的。


    使用const后,实际上是确保该变量的引用地址不变,而不是其内容。


    结合上面两个案例,女朋友和购物袋就好比是内存地址,女朋友的外号可以改,但女朋友是换不了的,同理袋里装的东西可以换,但袋子仍然是那个袋子。


    当使用const声明一个变量并赋值为一个对象或数组,这个变量实际上存储的是这个对象或数组在内存中的地址,形如0x00ABCDEF(这只是一个示例地址,实际地址会有所不同),而不是它的内容。这就是为什么我们说变量“引用”了这个对象或数组。


    实际应用


    这种看似矛盾的特性实际上在开发中经常用到。


    例如,在开发过程中,可能希望保持一个对象的引用不变,同时允许修改对象的属性。这可以通过使用const来实现。


    考虑以下示例:


    假设你正在开发一个应用,该应用允许用户自定义一些配置设置。当用户首次登录时,你可能会为他们提供一组默认的配置。但随着时间的推移,用户可能会更改某些配置。


    // 默认配置
    const userSettings = {
    theme: "light", // 主题颜色
    notifications: true, // 是否开启通知
    language: "en" // 默认语言
    };

    // 在某个时间点,用户决定更改主题颜色和语言
    function updateUserSettings(newTheme, newLanguage) {
    userSettings.theme = newTheme;
    userSettings.language = newLanguage;
    }

    // 用户调用函数,将主题更改为"dark",语言更改为"zh"
    updateUserSettings("dark", "zh");

    console.log(userSettings); // 输出:{ theme: "dark", notifications: true, language: "zh" }

    在这个例子中,我们首先定义了一个userSettings对象,它包含了用户的默认配置。尽管我们使用const来声明这个对象,但我们仍然可以随后更改其属性来反映用户的新配置。


    这种模式在实际开发中很有用,因为它允许我们确保userSettings始终指向同一个对象(即我们不会意外地将其指向另一个对象),同时还能够灵活地更新该对象的内容以反映用户的选择。


    为什么不用let


    以上所以案例中,使用let都是可行,但它的语义和用途相对不同,主要从这几个方面进行考虑:



    1. 不变性:使用const声明的变量意味着你不打算重新为该变量赋值。这为其他开发人员提供了一个明确的信号,即该变量的引用不会改变。在上述例子中,我们不打算将userSettings重新赋值为另一个对象,我们只是修改其属性。因此,使用const可以更好地传达这一意图。

    2. 错误预防:使用const可以防止意外地重新赋值给变量。如果你试图为const变量重新赋值,JavaScript会抛出错误。这可以帮助捕获潜在的错误,特别是在大型项目或团队合作中。

    3. 代码清晰度:对于那些只读取和修改对象属性而不重新赋值的场景,使用const可以提高代码的清晰度,可以提醒看到这段代码的人:“这个变量的引用是不变的,但其内容可能会变。”


    一般我们默认使用const,除非确定需要重新赋值,这时再考虑使用let。这种方法旨在鼓励不变性,并使代码更加可预测和易于维护。


    避免修改


    如果我们想要避免修改const声明的变量,当然也是可以的。


    例如,我们可以使用浅拷贝来创建一个具有相同内容的新对象或数组,从而避免直接修改原始对象或数组。这可以通过以下方式实现:


    const originalArray = [1, 2, 3];
    const newArray = [...originalArray]; // 创建一个原始数组的浅拷贝
    newArray.push(4); // 不会影响原始数组
    console.log(originalArray); // 输出: [1, 2, 3]
    console.log(newArray); // 输出: [1, 2, 3, 4]

    总结


    const声明的变量之所以看似可以被修改,是因为const限制的是变量指向的内存地址的改变,而不是内存中数据的改变。这种特性在实际开发中有其应用场景,允许我们保持引用不变,同时修改数据内容。


    然而,如果我们确实需要避免修改数据内容,可以采取适当的措施,如浅拷贝。


    9a9f1473841eca9a3e5d7e1408145a4b.gif

    收起阅读 »

    好烦啊,为什么点个链接还让我确认一下?

    web
    万丈苍穹水更深,无限乾坤尽眼中 背景 最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图: 很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或...
    继续阅读 »

    万丈苍穹水更深,无限乾坤尽眼中



    背景


    最近经常看到各大SNS平台有这样一项功能,点击跳转链接,如果不是本站的链接,那么就会跳转到一个中转页,告诉你跳转了一个非本站链接,提醒你注意账号财产安全,如图:


    A6C73047-4041-4584-9F97-BA04C896D73E.png


    很明显,这个是为了一定程度的避免用户被钓鱼,预防XSS或CRSF攻击,所以请不要像标题一样抱怨,多点一下也花不了2S时间。


    原理


    那么这个是如何实现的呢,原理其实很简单。


    a标签的onclick事件可以被拦截,当返回false时不会默认跳转。


    那么具体如何实现呢,拿掘金来举例:


            function SetSafeA(whiteDomList: string[], safeLink = 'https://link.juejin.cn/?target=') {
              const aArr = document.getElementsByTagName('a')
              Array.from(aArr).forEach(item=>{
                item.onclick = ()  => {
                  let target = item.getAttribute('href')!
                  if(/^\//.test(target)) {
                    // 相对本站链接
                    return true
                  }
                 const isSafe = undefined !==  whiteDomList.find(item=>{
                     return target.indexOf(item) !== -1
                  })
                  if(!isSafe) {
                    window.open(`${safeLink}${target}`, '_blank')
                  } else {
    return true
    }
                  return false
                }
              })
            }

    可以随便找一个网页在控制台执行一下,都能跳到掘金的中转页,中转页的代码就不写了^_^


    实践


    刚好最近遇到一个使用场景,公司APP产品里面都有各自用户协议,其中SDK协议我们都是直接跳转链接的,结果在部分渠道如小天才,步步高等对用户信息非常敏感的平台上,要求所有的链接必须要跳转到平台默认的安全浏览器上,不能在APP内打开。那么协议有很多如何快速处理呢。由于项目用到了vue,这里就想到使用指令,通过批量添加指令来达到快速替换,比如'<a' =>'<a v-link="x"',代码如下:


    Vue.directive('outlink', {
      bind: (el, binding) => {
        el.outlink = () => {
          if (GetEnv() === 'app') {
            const from = isNaN(+binding.value) ? 1 : +binding.value
            const url = el.getAttribute('href')
            if (url && url !== '' && url != 'javascript:;') {
              window.location.href = `${GetSchemeByFrom(from)}://outside_webview?url=${url}`
            }
            return false
          }
        }
        el.onclick = el.outlink
      },
      unbind: (el) => {
        el.onclick = null
        delete el.outlink
      }
    })

    这里我们传入了from值来区分APP平台,然后调用APP提供的相应scheme跳转到客户端的默认浏览器,如下:


    DE2DFA5F-ED19-4e74-97B6-2D19246D5D84.png


    结语


    链接拦截可以做好事,也可以做一些hack,希望使用的人保持一颗爱好和平的心;当然遇到让你确认安全的

    作者:CodePlayer
    来源:juejin.cn/post/7161712791089315877
    链接时,也请你保持一颗感谢的心。

    收起阅读 »

    兄弟,不要试图在业务代码中炫技。

    你好呀,我是歪歪。 最近项目迭代非常密集,导致组里面的同事都在同一个微服务里面进行不同需求的迭代开发。 由于我们的代码提交规则规定,提交代码必须有一个 review 环节,所以有时候我会去仔细看同事提交的代码,但是有一说一,绝大部分情况下我没有仔细的去看,只是...
    继续阅读 »

    你好呀,我是歪歪。


    最近项目迭代非常密集,导致组里面的同事都在同一个微服务里面进行不同需求的迭代开发。


    由于我们的代码提交规则规定,提交代码必须有一个 review 环节,所以有时候我会去仔细看同事提交的代码,但是有一说一,绝大部分情况下我没有仔细的去看,只是草草的瞟上几眼,就点击了通过。


    其实我之前也会非常仔细的去看的,但是不得不说这个 review 的过程真的会占据比较多的时间,在需求不密集的时候做起来没有问题。


    但是一旦任务在手上堆起来了,就很难去仔细 review 别人的代码了,分身乏术。


    去年有一段时间就是忙的飞起,多线程并发需求迭代,别人提交代码之后,我就是无脑点通过。


    我并没有核对最终落地的代码和我最初的设计方案是否匹配,而且由于代码不是我开发的,我甚至没有看过,等出了问题,排查问题的时候我再去找代码,就发现根本不知道写在哪里的。


    方案设计和代码落地之间的断层,这样带来的一个后果就是我后期完全失去了对服务的掌握。


    每天都担心,生怕出线上问题。但是每天也不知道哪个地方会出现问题,就很恼火。


    对每一次我点进 review 通过的代码负责,这是我写进年度计划的一句话。


    所以,今年为了避免这个现象的再次出现,在同事对一个完整的功能点提交之后,即使再忙,我自己会花时间仔细去 review 一次对应的代码,然后拿着我看完之后记录的问题再去找对应的同事给我答疑,确保我们至少在业务逻辑的理解上是一致的。


    通过这个方式,我又重新掌握了主动权。


    在这个过程中还暴露出一个问题,各个开发同事的编码风格各异,经常可以闻到一些代码的“坏味道”。


    比如,我见过一个新增操作,所有的逻辑都在一个 controller 里面,没有所谓的 biz 层、service 层、dao 层,一把梭直接把 mapper 注入到了 controller 里面,在一个方法里面从数据校验到数据库交互全部包圆了。


    功能能用吗?


    能用。


    但是我们常常提到的一个词是“技术含量”。


    这样代码是有“技术含量”的代码吗?


    我觉得可以说是毫无技术含量了,就是偷懒了,觉得怎么方便就怎么来了。


    那如果我要基于对于这一段代码继续开发新功能,我能做什么呢?


    我无能为力,原来的代码实在不想去动。


    我只能保证在这堆“屎山”上,我新写出来的代码是干净的、清晰的,不继续往里面扔垃圾。


    我读过一本书叫做《代码整洁之道》,里面有一个规则叫做“童子军军规”。


    军规中有一句话是这样的:让营地比你来时更干净。


    类比到代码上其实就是一件很小的事情,比如只是改好一个变量名、拆分一个有点过长的函数、消除一点点重复代码,清理一个嵌套 if 语句...


    这是让项目代码随着时间流逝而越变越好的最简单的做法,持续改进也是专业性的内在组成部分。


    我觉得我对于这一点“规则”落实的还是挺好的,看到一些不是我写的,但是我觉得可以有更好的写法时,而且改动起来非常简单,不影响核心功能的时候,我会主动去改一下。


    我能保证的是,这段代码在经过我之后,我没有让它更加混乱。


    把一段混乱的代码,拆分的清晰起来,再后来的人愿意按照你的结构继续往下写,或者继续改进。


    你说这是在写“有技术含量”的代码吗?


    我觉得不是。


    但是,我觉得这应该是在追求写“有技术含量”的代码之前,必须要具备的一个能力。而且是比写出“有技术含量”的代码更加重要的一个基础能力。


    先不说代码优雅的事儿了,至少得让代码整体看起来不混乱。


    一个人维护一个项目,想要把代码搞优雅是一件很简单的事情,但是如果是好几个人一起维护就有点不好做了。


    只有大家相互磨合,最后慢慢的形成好的、较为统一风格。


    所以我最近也是持续在找一些关于代码风格、代码规范、代码重构这方面的好的资料在组分享,总是能慢慢有所改变的。


    比如这周,我就找到了“京东云开发者”的一篇文章:



    《让代码优雅起来:记一次代码微重构实践 | 京东云技术团队》

    juejin.cn/post/725714…



    在这篇文章里面,作者给到了一个完整的关于代码重构的示例。


    把一个功能代码,从这样冗长臃肿的代码:



    最终拆分为了三个类,每个类各司其职。


    这个类只是负责组装对象:



    金额计算拆分到了枚举类里面去:




    这才是符合面向对象编程的思想。


    这部分代码具体是干啥的,以及重构前后的代码是怎么样的,如果你感兴趣可以自己打开链接看一下。


    我这边主要还是赞同作者的一个观点:不要觉得重构前的代码每次修改也就肉眼可见的几个地方,没必要在这上面花费时间。


    其实我觉得还是很有必要的,大家写代码的时候都想要追求技术含量,追求优雅性,这就是一个体现的地方,为什么不改呢?


    但是我还得补充一句,结合个人截至目前有限的职业生涯和工作经验来说,我有一点小小的体会:



    写业务代码,代码可读性的优先级甚至比代码写的优雅、写的有技术含量更高,且高的多。不要试图在业务代码中炫技。



    我前面分享的“记一次代码微重构实践”文章的最后也列举了两个引用的地方,我也放在这里,共勉之。


    软件工程中的“破窗效应”:



    破窗效应指的是在软件开发过程中,如果存在低质量的代码或设计,如果不及时修复,就会导致其他开发人员也采用同样的低质量方案。这会逐渐升级到更严重的问题,导致软件系统变得难以维护、扩展和改进。因此,在软件开发中,及时解决问题和保持代码质量非常重要,以避免破窗效应对于整个项目造成的负面影响。



    同时看看 Martin Fowler 在《重构:改善既有代码的设计》一书中对重构的部分解释:



    重构的每个步骤都很简单,甚至显得有些过于简单:你只需要把某个字段从一个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推下就行了。但是,聚沙成塔,这些小小的修改累积起来就可以根本改善设计质

    作者:why技术
    来源:juejin.cn/post/7268183236740317225
    量。


    收起阅读 »

    都什么年代了,还在用传统方式写代码?

    前言 还在把 AI 当作搜索引擎的替代品,有问题才问 AI,没问题就在那边吭哧吭哧地撸代码?如果是这样,那你真的 OUT了!现在正经人谁还自己一行行地写代码啊,都是 AI 生成的代码——没有 AI 我不写(手动滑稽)。 本文将分享 AI 时代的编程新实践,教你...
    继续阅读 »

    前言


    还在把 AI 当作搜索引擎的替代品,有问题才问 AI,没问题就在那边吭哧吭哧地撸代码?如果是这样,那你真的 OUT了!现在正经人谁还自己一行行地写代码啊,都是 AI 生成的代码——没有 AI 我不写(手动滑稽)。


    本文将分享 AI 时代的编程新实践,教你如何从一个 "Ctrl + C"、 "Ctrl + V" 工程师,变成一个 "Tab + Enter" 工程师🤣。


    开发流程


    软件的一般研发流程为:

    1. 需求分析
    2. 程序设计
    3. 代码编写
    4. 软件测试
    5. 部署上线

    我们在这里主要关心步骤2~4,因为与 AI 结合得比较紧密。虽然需求分析也可以借助 AI,但不是本文的重点,故不做讨论。


    程序设计


    经过需求分析、逻辑梳理后,在编写实际代码前,需要进行程序设计。


    此环节的产物是设计文档,是什么类型的设计文档不重要,重要的是伪代码的输出。


    虽然《Code Complete》早就推荐过伪代码的实践,但对此人们容易有一个误区:认为写伪代码花的时间,已经够把实际代码写好了。但 AIGC 时代,此问题可以轻松破解:AI 写代码的速度肯定比人快,因此,只要能找到方法能让 AI 生成符合需求的代码,就值得花时间去研究。而伪代码,就是让 AI 快速生成符合期望的实际代码的最好方式。


    为什么这么说呢?因为想要让 AIGC 符合期望,恰当的 Prompt 必不可少。但如何写好这个 Prompt,需要提供多少上下文,才能让 AI 更好地理解我们的意图,这是需要技巧、需要调试的。而经过精心设计的伪代码,本身已经提供了足够的上下文,且意图足够聚焦,减轻了对 Prompt 的要求,又提高了 AIGC 的成功率。


    我们来看一下伪代码示例:

    plainText = JSON.stringify(data)
    digest = hash(plainText) // 防篡改
    secret = Symmetric.getKey()
    cipherText = encryptText(data, secret) // 防内容泄密


    pair = Asymmetric.getPair()
    cipherSecret = encryptKey(secret, pair.getPrivateKey()) // 防密钥泄露

    return {
    digest,
    cipherText,
    cipherSecret,
    pair.getPublicKey(),
    }

    上述代码主要表达的内容是:

    1. 对初始数据进行加密
    2. 返回加密后的相关内容
    3. 涉及到了一系列的算法

    对于伪代码的编写,有如以建议:

    1. 不要纠结类型,不要局限于某一种编程语言的语法,不用试图写能编译通过的代码——这只会限制了你的表达
    2. 命名一定要准确,符合领域术语,这一点很关键。这一点可以通过查找资料、看书、问 AI 来实现。千万不要只是生硬地汉译英,不专业的表达会妨碍 AI 的理解

    代码编写


    代码编写分为以下几步:

    1. 把伪代码转换成目标语言可执行的代码
    2. 根据项目情况,对生成的代码进行改造
    3. 利用 AI 编码辅助工具编写剩余的代码

    生成真实代码


    让 AI 生成代码的 prompt 很简单,示例如下(根据目标语言进行修改):

    please transform the following pseudo code into practical java code ```
    your code```please transform the following pseudo code into practical java code ```
    your code```

    使用 chatGPT 结果截图:



    完整代码如下:

    import com.fasterxml.jackson.databind.ObjectMapper;

    import javax.crypto.Cipher;
    import javax.crypto.KeyGenerator;
    import javax.crypto.SecretKey;
    import java.security.*;

    public class EncryptionExample {

    public static void main(String[] args) throws Exception {
    // Prepare the license data
    LicenseData licenseData = new LicenseData();
    licenseData.setLicenseId("123");
    licenseData.setLicenseType("Standard");

    // Convert the license data to JSON string
    ObjectMapper objectMapper = new ObjectMapper();
    String plainText = objectMapper.writeValueAsString(licenseData);

    // Hash the plain text
    String digest = hash(plainText);

    // Generate a symmetric key
    SecretKey secretKey = generateSymmetricKey();

    // Encrypt the license data with the symmetric key
    byte[] cipherText = encryptText(plainText, secretKey);

    // Generate an asymmetric key pair
    KeyPair keyPair = generateAsymmetricKeyPair();

    // Encrypt the symmetric key with the private key
    byte[] cipherSecret = encryptKey(secretKey.getEncoded(), keyPair.getPrivate());

    // Print the results
    System.out.println("Plain Text: " + plainText);
    System.out.println("Digest: " + digest);
    System.out.println("Cipher Text: " + new String(cipherText));
    System.out.println("Cipher Secret: " + new String(cipherSecret));
    }

    public static String hash(String input) throws NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] hash = digest.digest(input.getBytes());
    return bytesToHex(hash);
    }

    public static SecretKey generateSymmetricKey() throws NoSuchAlgorithmException {
    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(128);
    return keyGenerator.generateKey();
    }

    public static byte[] encryptText(String data, SecretKey key) throws Exception {
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    return cipher.doFinal(data.getBytes());
    }

    public static byte[] encryptKey(byte[] secret, PrivateKey privateKey) throws Exception {
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, privateKey);
    return cipher.doFinal(secret);
    }

    public static KeyPair generateAsymmetricKeyPair() throws NoSuchAlgorithmException {
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(2048);
    return keyPairGenerator.generateKeyPair();
    }

    public static String bytesToHex(byte[] bytes) {
    StringBuilder result = new StringBuilder();
    for (byte b : bytes) {
    result.append(String.format("x", b));
    }
    return result.toString();
    }

    public static class LicenseData {
    private String licenseId;
    private String licenseType;

    public String getLicenseId() {
    return licenseId;
    }

    public void setLicenseId(String licenseId) {
    this.licenseId = licenseId;
    }

    public String getLicenseType() {
    return licenseType;
    }

    public void setLicenseType(String licenseType) {
    this.licenseType = licenseType;
    }
    }
    }

    把上述代码 copy 下来,放到工程中,根据需要改造即可。


    这里特别要说下,强烈推荐使用原版 AI,而不是寻找平替,同样的 prompt,下图是某一平替输出的结果:


    只生成了函数声明,没有生成函数实现。二者对比,未免相形见绌。



    辅助编程工具


    改造的过程中,少不了 AI pair programming tools。对此,我推荐使用 Amazon 的 CodeWhisperer,原因很简单,跟 GitHub Copilot 相比,它是免费的😃。


    CodeWhisperer 的安装可以看文末的安装教程,我们先来看一下它是怎么辅助我们编码的。


    第一种方式是最简单的,那就是什么都不管,等待智能提示即可,就好像 IDEA 原来的提示一样,只不过更智能。


    下图示例中,要把原来的中文异常提示,修改成英文,而我只输入了两个字符 IM, 就得到了智能提示,补全了完整的英文字符串!



    可以注意到,上图的智能建议一共有 5 条,相应的快捷键为:

    1. 方向键 ->,查看下一条提示
    2. 方向键 <-,查看上一条提示
    3. Tab,采用该提示
    4. Esc,拒绝提示

    我们再来看第二种 CodeWhisperer 的使用方式,编写注释,获得编码建议。



    最后一种就是编写一个空函数,让 CodeWhisperer 根据函数名去猜测函数的实现,这种情况需要足够的上下文,才能得到令人满意的结果。


    软件测试


    AI 生成的内容,并不是完全可信任的,因此,单元测试的重要性变得尤为突出。


    对上述代码编写测试代码后,实际上并不能一次通过,因为前面 AI 生成的代码参数有误。


    此时需要一边执行单测,一边根据结果与 AI 进行交互:



    经过修改,最终测试用例通过👏!



    总结


    本文通过案例,展示了 AI 如何结合软件研发的流程,提升我们的编程效率的。


    其中,个人认为最重要的是编写伪代码与进行单元测试。有趣的是,这两样实践在 AIGC 时代之前,就已经被认为是最佳实践。这给我们启示:某些方法论、实践经得起时间的考验,技术更新迭代,它们历久弥新。


    另外,AI 是否能进一步渗透我们的工作流,还有待探索。此文作引抛砖引玉之用,期待大家的后续分享。


    附:CodeWhisperer 安装


    下载 2023 年的 IDEA,打开 Plugins Marketplace,找到 AWS Toolkit



    安装完成、重启 IDEA 后,点击左下角,按下图所示操作:




    如果第一次使用,就点击 1 处进行注册,如果已经有账号了,就点击 2 处使用自己的账号登录。



    注册、登录、授权成功后,出现如图所示页面,即可使用 CodeWhisperer。



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

    前端重新部署如何通知用户刷新网页?

    1.目标场景 有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。 2.思考解决方案 如何去解决这个问题 思考中... 如果后端可以配合我们的话我们可以使用webSoc...
    继续阅读 »



    1.目标场景


    有时候上完线,用户还停留在老的页面,用户不知道网页重新部署了,跳转页面的时候有时候js连接hash变了导致报错跳不过去,并且用户体验不到新功能。


    2.思考解决方案


    如何去解决这个问题
    思考中...


    如果后端可以配合我们的话我们可以使用webSocket 跟后端进行实时通讯,前端部署完之后,后端给个通知,前端检测到Message进行提示,还可以在优化一下使用EvnentSource 这个跟socket很像只不过他只能后端往前端推送消息,前端无法给后端发送,我们也不需要给后端发送。


    以上方案需要后端配合,奈何公司后端都在忙,需要纯前端实现。


    重新进行思考...


    根据和小伙伴的讨论得出了一个方案,在项目根目录给个json 文件,写入一个固定的key值然后打包的时候变一下,然后代码中轮询去判断看有没有变化,有就提示。




    果然是康老师经典不知道。




    但是写完之后发现太麻烦了,需要手动配置json文件,还需要打包的时候修改,有没有更简单的方案,
    进行第二轮讨论。


    第二轮讨论的方案是根据打完包之后生成的script src 的hash值去判断,每次打包都会生成唯一的hash值,只要轮询去判断不一样了,那一定是重新部署了.




    3.代码实现

    interface Options {
    timer?: number
    }

    export class Updater {
    oldScript: string[] //存储第一次值也就是script 的hash 信息
    newScript: string[] //获取新的值 也就是新的script 的hash信息
    dispatch: Record<string, Function[]> //小型发布订阅通知用户更新了
    constructor(options: Options) {
    this.oldScript = [];
    this.newScript = []
    this.dispatch = {}
    this.init() //初始化
    this.timing(options?.timer)//轮询
    }


    async init() {
    const html: string = await this.getHtml()
    this.oldScript = this.parserScript(html)
    }

    async getHtml() {
    const html = await fetch('/').then(res => res.text());//读取index html
    return html
    }

    parserScript(html: string) {
    const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/ig) //script正则
    return html.match(reg) as string[] //匹配script标签
    }

    //发布订阅通知
    on(key: 'no-update' | 'update', fn: Function) {
    (this.dispatch[key] || (this.dispatch[key] = [])).push(fn)
    return this;
    }

    compare(oldArr: string[], newArr: string[]) {
    const base = oldArr.length
    const arr = Array.from(new Set(oldArr.concat(newArr)))
    //如果新旧length 一样无更新
    if (arr.length === base) {
    this.dispatch['no-update'].forEach(fn => {
    fn()
    })

    } else {
    //否则通知更新
    this.dispatch['update'].forEach(fn => {
    fn()
    })
    }
    }

    timing(time = 10000) {
    //轮询
    setInterval(async () => {
    const newHtml = await this.getHtml()
    this.newScript = this.parserScript(newHtml)
    this.compare(this.oldScript, this.newScript)
    }, time)
    }

    }

    代码用法

    //实例化该类
    const up = new Updater({
    timer:2000
    })
    //未更新通知
    up.on('no-update',()=>{
    console.log('未更新')
    })
    //更新通知
    up.on('update',()=>{
    console.log('更新了')
    })

    4.测试


    执行 npm run build 打个包


    安装http-server


    使用http-server 开个服务




    重新打个包npm run build




    这样子就可以检测出来有没有重新发布就可以通知用户更新了。


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

    超出范围自动滚动、支持彩色流动特效的UILabel封装

    iOS
    JKRShimmeringLabel 特征 支持炫彩字支持炫彩流动字支持超出显示范围自动滚动文本支持RTL下的对称显示和滚动支持Frame布局支持Xib和StoryBoard内使用支持AutoLayout布局 使用 源码连接 和原生UILabel一样用,只需...
    继续阅读 »

    JKRShimmeringLabel


    特征





    1. 支持炫彩字

    2. 支持炫彩流动字

    3. 支持超出显示范围自动滚动文本

    4. 支持RTL下的对称显示和滚动

    5. 支持Frame布局

    6. 支持Xib和StoryBoard内使用

    7. 支持AutoLayout布局


    使用


    源码连接


    和原生UILabel一样用,只需要设置mask属性(一张彩色的图片遮罩)即可。


    原有项目的UILabel替换


    因为JKRAutoScrollLabel和JKRShimmeringLabel本身就是继承UILabel,可以直接把原有项目的UILabel类,替换成JKRAutoScrollLabel或JKRShimmeringLabel即可。


    JKRAutoScrollLabel


    超出范围自动滚动的Lable,需要设置attributedText,不能设置text。要同时支持流动彩字,设置mask即可。不需要彩色可以不设置mask,只有自动滚动的特性。


    // Frame布局,字体支持炫彩闪动,同时超出显示范围自动滚动

    NSMutableAttributedString *textForFrameAttr = [[NSMutableAttributedString alloc] initWithString:@"我是滚动测试文本Frame布局,看看我的效果" attributes:@{NSForegroundColorAttributeName: UIColorHex(FFFFFF), NSFontAttributeName: [UIFont systemFontOfSize:19 weight:UIFontWeightBold]}];

    self.autoScrollLabelForFrame = [[JKRAutoScrollLabel alloc] initWithFrame:CGRectMake(isRTL ? kScreenWidth - 10 - 300 : 10, CGRectGetMaxY(title0.frame) + 10, 300, 24)];

    // 滚动文本需要设置 attributedText 才能生效

    self.autoScrollLabelForFrame.attributedText = textForFrameAttr;

    // 设置文字颜色的mask图片遮罩,如果不需要字体炫彩,不设置即可

    self.autoScrollLabelForFrame.mask = [self maskImage];

    [self.view addSubview:self.autoScrollLabelForFrame];


    JKRShimmeringLabel


    支持流动彩字,设置mask即可,如果还需要超出范围自动滚动,需要使用JKRAutoScrollLabel。


    // Frame布局,字体支持炫彩闪动

    self.shimmerLabelForFrame = [[JKRShimmeringLabel alloc] initWithFrame:CGRectMake(isRTL ? kScreenWidth - 10 - 300 : 10, CGRectGetMaxY(title1.frame) + 10, 300, 24)];

    self.shimmerLabelForFrame.text = @"我是彩色不滚动文本Frame布局,看看我的效果";

    self.shimmerLabelForFrame.font = [UIFont systemFontOfSize:19];

    // 设置文字颜色的mask图片遮罩,如果不需要字体炫彩,不设置即可

    self.shimmerLabelForFrame.mask = [self maskImage];

    [self.view addSubview:self.shimmerLabelForFrame];


    Xib使用


    控件支持xib和autolayout的场景,和UILabel一样设置约束即可,自动滚动和彩色动画,会自动支持。只需要正常配置约束,然后设置mask彩色遮罩即可。


    同时,因为JKRShimmeringLabel和JKRAutoScrollLabel本身就是继承UILabel的,所以UILabel在Xib中的文本自动填充宽度、约束优先级等等特性,也都可以正常使用。


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