注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

有了小孩后,对我的影响还是挺大的

张雪峰曾经说: 闺女是上帝对父母的恩赐,儿子是上帝对父母的惩罚。 2023年8月9日,上帝惩罚了我。 事情的经过是这样的 我和我媳妇其实很早就领证结婚了,但是一直没要宝宝,因为那个时候感觉两个人过日子真是太滋润了,周六日可以睡到自然醒,也可以夜里12点去泡澡,...
继续阅读 »

张雪峰曾经说: 闺女是上帝对父母的恩赐,儿子是上帝对父母的惩罚。


2023年8月9日,上帝惩罚了我。


事情的经过是这样的


我和我媳妇其实很早就领证结婚了,但是一直没要宝宝,因为那个时候感觉两个人过日子真是太滋润了,周六日可以睡到自然醒,也可以夜里12点去泡澡,一脚油门想去哪去哪。


c665e84507622c47a926fff99e6c7ff.jpg

2022年房子装修好了,除完甲醛后就搬进来住了。“正好”那个时间又赶上了疫情,很多小区就封了,我们小区也不不例外。我记得那个时候买了好多肉和吃的,所以吃喝不用担心,再加上我和媳妇都是干的程序员,可以居家办公,家庭收入也没受到影响,总之那段时间过得还是挺爽😍。


也就是在这段很爽的时间里,我们有了宝宝😂。


怀孕的这段时间


除了正常的产检外,还拍了很多B超,不小心摔着了拍个B超,肚子疼了去拍个B超,胎动异常了去拍个B超🤓...


总之也是一块不小的支出


1727669507822.png

小插曲


到了孕晚期,我媳妇的胎位不正,不能顺产,但是我媳妇又想顺产,然后医生说可以做外道转手术,把胎位移正。


然后发了5000大洋,在医院住了三天,终于把胎位搞正了。


image.png

谁承想回到家不到一个星期,自己又掉转过来了😹


最后还是选择了剖腹产


看来干什么事情都要顺其自然,强求不来


宝宝顺利出生了


时间过得最慢的就是媳妇推进手术室做剖腹产,我们在外面等着的时候,真是度秒如年,生怕出了什么意外。


image.png

好在最后一切顺利


image.png

当爸爸的第一个晚上,真是一夜没睡,因为没有任何经验,什么东西都得现学,比如如何喂奶,如何包裹小孩...


所以干什么还是提前做好功课为好


现在已经一岁多啦


现在宝宝已经一岁一个月了,时间过得既快又慢,经历了很多很多。


下面就谈谈有了孩子后的感受吧!


可支配的时间越来越少


有了孩子后最大的变化就是,自己可支配的时间越来越少了。没有孩子之前,我和媳妇还可以有时间看电影,泡温泉啥的,有了孩子后二人世界彻底崩塌😭


还有就是


说来也奇怪,有了孩子后,基本上对学习没啥动力了,除非是工作中要用到的一些技术,可能有时间会自己研究下(很少),大部分时间都不会再关注技术,有点时间就想着刷刷抖音,打打游戏😑


image.png

我之所以打算写点东西,很大一部分原因就是为了减少我刷抖音的时间😶


越来越像爸爸


有小孩的前三个月,我媳妇有产假,加上我妈也来共同照护孩子,所以在带小孩上我基本上没怎么操心。有些时候为了睡好觉就直接一个人跑到次卧去,做个甩手装柜。


后来我媳妇也上班了,加上她离公司比较远,基本上都是我先下班,所以我也要承担一部分照护小孩的责任。


怎么说呢,这种感觉很奇妙,就是你陪伴孩子的时间越长,你就会越喜欢哄小孩,然后小孩也会越来越喜欢你


image.png

每当孩子有什么头疼发烧啥的,当父母的就像心里压住个千斤顶,这个做父母的应该都懂吧。


现在就希望孩子可以快点长大,我有更多的时间和精力干我自己感兴趣的事。


作者:刘小灰
来源:juejin.cn/post/7420001862487654409
收起阅读 »

入职第一天,看了公司代码,牛马沉默了

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
继续阅读 »

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

打开代码发现问题不断

  1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

image.png

image.png

image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

prop_c.setProperty(key, value);

value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
  1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
  1. 日志打印居然sout和log混合双打

image.png

image.png

先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

5.随意更改生产数据库,出不出问题全靠开发的职业素养;

6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

<type>pom

来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

那有什么优点呢:

  1. 不用太怎么写文档
  2. 束缚很小
  3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

解决之道

怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


作者:小红帽的大灰狼
来源:juejin.cn/post/7371986999164928010
收起阅读 »

强大的一笔的Hermes引擎,是如何让你的 App 脱颖而出的!

web
Hermes 是一款由 Facebook 开源的轻量级 JavaScript 引擎,专门针对 React Native 应用进行了优化。与传统的 JavaScript 引擎(例如 JavaScriptCore 和 V8)相比,Hermes 具有以下优势: 启动...
继续阅读 »

Hermes 是一款由 Facebook 开源的轻量级 JavaScript 引擎,专门针对 React Native 应用进行了优化。与传统的 JavaScript 引擎(例如 JavaScriptCore 和 V8)相比,Hermes 具有以下优势:


启动时间更快: Hermes 使用预编译字节码(AOT),而不是即时编译(JIT),这可以显著缩短应用的启动时间。


更小的内存占用: Hermes 的体积小巧,占用内存更少,这对于移动设备尤为重要。


更小的应用包大小: 由于 Hermes 的体积小巧,因此可以减小 React Native 应用的包大小。


Hermes


高效的性能的原因


先看下面这幅图:
AOT


Hermes Engine 的设计初衷是为了优化 React Native 应用的性能。它通过对 JavaScript 代码的提前编译,将其转化为字节码,从而减少了运行时的解析时间。这种预编译机制使得应用启动速度显著提升,用户体验更加流畅。


CPU 的利用率


在 CPU 利用率方面,Hermes 也有显著的优势。
通过优化 JavaScript 执行和垃圾回收过程,Hermes 提供了更快的启动时间和更低的内存占用。研究表明,使用 Hermes 的应用在性能上有显著提升,用户体验更加流畅


内存占用和包大小优化


内存优化


Hermes 采用了优化的内存管理机制,如内存池和高效的垃圾回收算法,能够减少应用在运行时的内存占用。这对于资源受限的移动设备尤为关键。使用 Hermes 编译的应用包体积通常更小。这对于需要快速下载安装的应用很有优势,也有助于提高应用在应用商店的排名。上图就是 Stock RN 应用基于 Hermes 引擎的内存优化后的实际效果。


良好的兼容性


Hermes 提供了强大的调试工具,帮助开发者快速定位和解决问题。其集成的调试功能使得开发者能够实时监控应用的性能,及时发现并修复潜在的性能瓶颈。
Hermes 得到了 Facebook 和开源社区的广泛支持,拥有丰富的文档和活跃的开发者社区。开发者可以轻松获取资源和支持,促进了 Hermes 的快速发展和普及。


一些小众第三方库不支持 Hermes 引擎


虽然,大多数比较有名的第三方库都是支持 Hermes引擎的,但是有一个小小的问题,有些比较小众的第三方库,是不支持 hermes 引擎的,这个时候,你可需要想办法自己改写下这个第三方库,或者给作者提建议。
如,腾讯云 cos ,React Native 的库,就是不支持 Hermes 引擎的。相关issue 在这里:


github.com/TencentClou…


image.png


不过,对于这个问题,你完全可以使用 restful api 呀,所以,解决问题的方式太多了,不要因为一个小众的三方库而放弃恐怖的性能提升,多少有点不值当


实际应用案例

许多知名应用已经开始采用 Hermes Engine,以提升其性能。例如,Facebook 和 Instagram 的部分功能已成功迁移至 Hermes,用户反馈显示应用的启动时间和流畅度均有显著改善。这些成功案例进一步验证了 Hermes 的强大实力。


如何用上Hermes 引擎


如果你在使用 Expo 做移动端跨端研发,那么恭喜你,默认就是使用的 Hermes 引擎,无需任何配置,如果你想显式配置,也无妨,甚至你可以指定 ios 使用jsc 引擎。


{
"expo": {
"jsEngine": "hermes",
"ios": {
"jsEngine": "jsc"
}
}
}

如果你使用的是 React Native 0.70 或更高版本,则 Hermes 引擎将默认启用。如果你使用的是较早版本的 React Native,则可以按照 React Native 文档 中的说明启用 Hermes 引擎。配置简单的就啰嗦。小伙伴们,React Native 要吊打 Flutter了 吗?拍拍砖?


作者:brzhang
来源:juejin.cn/post/7394095950383743015
收起阅读 »

Vite 为何短短几年内变成这样?

web
给前端以福利,给编程以复利。大家好,我是大家的林语冰。 00. 观前须知 在 Web 开发领域,Vite 如今已如雷贯耳。 自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过...
继续阅读 »

给前端以福利,给编程以复利。大家好,我是大家的林语冰。


00. 观前须知


在 Web 开发领域,Vite 如今已如雷贯耳。


自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过 1200 万次,现在为 Nuxt、Remix、Astro 等大多数开源框架提供支持。


尽管众口嚣嚣,我们意识到许多开发者可能仍然不熟悉 Vite 是什么鬼物,也不熟悉 Vite 在推动现代 Web 框架和工具的开发中扮演的重要角色。


在本文中,我们将科普 Vite 的知识储备,以及 Vite 如何在短短几年后发展成为现代 Web 的重量级角色。


00-trend.png



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 What is Vite (and why is it so popular)?



01. Vite 是什么鬼物?


Vite 的发音为 /vit/,在法语中是“快速”或“迅捷”的意思,不得不说 Vite 名副其实。


简而言之,Vite 是一种现代 JS 构建工具,为常见 Web 模式提供开箱即用的支持和构建优化,兼具 rollup 的自由度和成熟度。


Vite 还与 esbuild 和原生 ES 模块强强联手,实现快速无打包开发服务器。


Vite 是由“Vue 之父”尤雨溪(Evan You)构思出来的,旨在通过减少开发者在启动开发服务器和处理文件编辑后重载时遭遇的性能瓶颈,简化打包过程。


02. Vite 的核心特性


00-wall.png


运行 Vite 时,你会注意到的第一个区别在于,开发服务器会即时启动。


这是因为,Vite 采用按需方法将你的应用程序提供给浏览器。Vite 不会首先打包整个源码,而是响应浏览器请求,将你编写的模块即时转换为浏览器可以理解的原生 ESM 模块。


Vite 为 TS、PostCSS、CSS 预处理器等提供开箱即用的支持,且可以通过不断增长的插件生态系统进行扩展,支持所有你喜欢的框架和工具。


每当你在开发期间更改项目中的任意文件时,Vite 都会使用应用程序的模块图,只热重载受影响的模块(HMR)。这允许开发者预览他们的更改,及其对应用程序的影响。


Vite 的 HMR 速度惊人,可以让编辑器自动保存,并获得类似于在浏览器开发工具中修改 CSS 时的反馈循环。


Vite 还执行 依赖预构建(dependency pre-bundling)。在开发过程中,Vite 使用 esbuild 来打包你的依赖并缓存,加快未来服务器的启动速度。


此优化步骤还有助于加快 lodash 等导出许多迷你模块的依赖的加载时间,因为浏览器只加载每个依赖的代码块(chunk)。这还允许 Vite 在依赖中支持 CJS 和 UMD 代码,因为它们被打包到原生 ESM 模块中。


当你准备好部署时,Vite 将使用优化的 rollup 设置来构建你的应用程序。Vite 会执行 CSS 代码分割,添加预加载指令,并优化异步块的加载,无需任何配置。


Vite 提供了一个通用的 rollup 兼容插件 API,适用于开发和生产,使你可以更轻松地扩展和自定义构建过程。


03. Vite 的优势


使用 Vite 有若干主要优势,包括但不限于:


03-1. 开源且独立


Vite 由开源开发者社区“用爱发电”,由来自不同背景的开发者团队领导,Vite 核心仓库最近贡献者数量已突破 900 人。


Vite 得到积极的开发和维护,不断实现新功能并解决错误。


03-2. 本地敏捷开发


开发体验是 Vite 的核心,每次点击保存时,你都能感受到延迟。我们常常认为重载速度是理所当然的。


但随着您的应用程序增长,且重载速度逐渐停止,你将感恩 Vite 几乎能够保持瞬间重载,而无论应用程序大小如何。


03-3. 广泛的生态系统支持


Vite 的方案人气爆棚,大多数框架和工具都默认使用 Vite 或拥有一流的支持。通过选择使用 Vite 作为构建工具,这些项目维护者可以在它们之间共享一个统一基建,且随着时间的推移共同改良 Vite。


因此,它们可以花更多的时间开发用户需要的功能,而减少重新造轮子的时间。


03-4. 易于扩展


Vite 对 rollup 插件 API 的押注得到了回报。插件允许下游项目共享 Vite 核心提供的功能。


我们有很多高质量的插件可供使用,例如 vite-plugin-pwavite-imagetools


03-5. 框架构建难题中的重要角色


Vite 是现代元框架构建的重要组成部分之一,这是一个更大的工具生态系统的一部分。


Volar 提供了在代码编辑器中为 Vue、MDX 和 Astro 等自定义编程语言构建可靠且高性能的编辑体验所需的工具。Volar 允许框架向用户提供悬停信息、诊断和自动补全等功能,并共享 Volar 作为为它们提供支持的通用基建。


另一个很好的例子是 Nitro,它是一个服务器工具包,用于创建功能齐全的 Web 服务器,开箱即用地支持每个主要部署平台。Nitro 是一个与框架无关的库 UnJS 的奇妙集合的一部分。


04. Vite 的未来


evan-vite5.png


在最近的 ViteConf 大会的演讲中,尤雨溪表示,虽然 Vite 取得了巨大进展,但仍面临一些已知的问题和挑战。


Vite 目前使用 rollup 进行生产构建,这比 esbuildBun 等原生打包器慢得多。


Vite 还尽可能减少开发和生产环境之间的不一致性,但考虑到 rollupesbuild 之间的差异,某些不一致性无法避免。


尤雨溪现在领导一个新团队开发 rolldown,这是一个基于 Rust 的 rollup 移植,在 “JS 氧化编译器 OXC”之上构建了最大的兼容性。


这个主意是用 rolldown 替代 Vite 中的 rollupesbuild。Vite 将拥有一个单独基建,兼具 rollup 的自由度和 esbuild 的速度,消除不一致性,使代码库更易于维护,并加快构建时间。


rolldown 目前处于早期阶段,但已经显示出有希望的结果。rolldown 现已开源,rolldown 团队正在寻找贡献者来辅助实现这一愿景。


与此同时,Vite 团队在每个版本中不断改良 Vite。这项工作从上游的为 Vitest 和 Nuxt Dev SSR 提供​​动力的引擎 vite-node 开始,现已发展成为框架作者对 Vite API 的完整修订版。


新版 Environment API 预计在 Vite 6 中发布,这将是自 Vite 2 发布以来 Vite 最大的变化之一。这将允许在任意数量的环境中通过 Vite 插件管道运行代码,解锁对 worker、RSC 等的一流支持。


Vite 正在开辟一条前进的道路,并迅速成为 JS 生态系统事实上的构建工具。


参考文献



粉丝互动


本期话题是:如何评价人气爆棚的 Vite,你最喜欢或期待 Vite 的哪个功能?你可以在本文下方自由言论,文明科普。


欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。


坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~


26-cat.gif


作者:前端俱乐部
来源:juejin.cn/post/7368836713965486119
收起阅读 »

因为编辑器没做草稿,老板崩溃了。。。

web
现场 大家好,我是多喝热水。 事情是这样的,那天晚上老板在群里吐槽说他在手机上写了将近 1000 字的评论不小心点了一下黑屏,然后内容就突然没了,如下: 原来是我们编辑器没有做草稿能力,导致关闭后原本编辑的内容都消失了,确实这个体验不太好,想想怎么把这里优化...
继续阅读 »

现场


大家好,我是多喝热水。


事情是这样的,那天晚上老板在群里吐槽说他在手机上写了将近 1000 字的评论不小心点了一下黑屏,然后内容就突然没了,如下:



原来是我们编辑器没有做草稿能力,导致关闭后原本编辑的内容都消失了,确实这个体验不太好,想想怎么把这里优化一下。


调研


像我们平时用得比较多的社交平台,比如某音、某书等,先从它们的评论区入手,看看主流的平台是怎么做的。


1)某音


某音的效果是,在某条视频下评论后划走,再划回来编辑的内容就不在了,看样子是没有做草稿能力,如图:



2)某书


某书的效果是,在某个笔记下面评论然后划走,再回来的时候内容是还在的。而且每条评论都有自己的编辑态,互不干扰,如图:



好看真好看,呸好用真好用,既然体验上某书更好,我决定仿照某书的方案来实现。



既然要做成某书的效果,那我们就需要解决两个问题


1)他们评论区草稿内容是怎么存的?


2)存在哪里了?


内容怎么存?


先说说我的看法,如果要让每条评论都拥有独立的编辑态,那么肯定是需要一个唯一标识的,那我能想到的唯一标识就是ID


内容存哪里?


存后端还是存前端?存前端的话又存哪里?这里我简单总结了一下:


存后端

优势:数据真正的持久化、安全性高


缺陷:需要网络连接,依赖后端,开发成本高


存前端

优势:简单易用、性能好、脱机可用


缺陷:无法真正持久化、存储空间有限、不安全


方案选择


回归到需求本身,我们不需要实时性多么高,所以存前端就已经可以满足我们的需求了。


但在前端存储还有一个存储空间问题,需要考虑一下存储内容的有效时间,过期了就得删除,不然会存在很多冗余数据,所以我们又面临新的问题,前端用什么来存


浏览器常用的存储方案:cookie、localStorage、sessionStorage


1)cookie 是可以设置过期时间的,但如果存 cookie,那它的容量只有5kb,有点太小了,并且每次发请求 cookie 都会被携带上,无疑是增加了额外的带宽开销


2)sessionStorage 存储空间最大支持5MB,但窗口被关闭后数据就过期了,有效期仅仅是窗口会话期间,万一用户不小心关闭了窗口,数据也消失了,所以这个方案也不太妥当


3)相比之下 localStorage 的容量也有 5MB,足够大,但是它本身不支持设置过期时间(默认永久有效),需要人为去控制,好在这个成本并不高,综合之下我们还是选择存 localStorage 了


开发


选好方案后,就可以开始动手开发了!先把支持控制过期时间 的 localStorage 逻辑写一下。


写之前我们需要考虑一下代码的复用性,因为在我们网站中,有很多地方都用到了编辑器,比如评论区、交流内容发布等,如果每一处都写一遍的话,那这个代码就太冗余了,所以将它封装为一个 hook 是一个不错的选择,代码如下:


import { CACHE_TYPE, EXPIRES_TIME } from './constants';

/**
* 缓存数据
* @param key
* @returns
*/

export default function useCache(key: string = CACHE_TYPE.ESSAY_CONTENT) {
/**
* 删除缓存数据
*/

const removeCache = () => {
localStorage.removeItem(key);
};

/**
* 设置缓存数据
* @param data 数据内容
* @param expires 过期时间(毫秒)
*/

const setCache = (data: any, expires: number = EXPIRES_TIME) => {
const cacheData = {
value: data,
expires: expires ? Date.now() + expires : null, // 计算过期时间戳
};
localStorage.setItem(key, JSON.stringify(cacheData));
};

/**
* 获取缓存数据
* @returns 缓存数据或 null
*/

const getCache = () => {
const cachedString = localStorage.getItem(key);
if (!cachedString) {
return null;
}
const cachedObject = JSON.parse(cachedString);
// 检查是否设置了过期时间并且是否已经过期
if (cachedObject.expires && Date.now() > cachedObject.expires) {
removeCache(); // 删除已过期的数据
return null;
}
return cachedObject.value;
};

return { removeCache, setCache, getCache };
}

简单解释一下上面的代码:


1)useCache 函数主要接收一个 KEY,删除、获取、设置草稿数据都会用到这个 KEY,且我们保证它是唯一的


2)在设置需要缓存内容时(setCache),会给出一个 expires 的参数用于控制该数据的有效时间


3)获取数据的时候会校验一下有效时间,如果已经过期了则返回 null


在编辑器中应用


最后我们需要在用到编辑器的地方使用这个 hook。


可能有些小伙伴会觉得我们网站中用到编辑器的地方很多,这一步才是一个大工程,其实不然,因为我们所有用到编辑器的地方都是用的同一个组件,我们需要改动的地方就是那个公共的编辑器组件!


这时候封装带来的便捷性就体现的淋漓尽致,省去了不少时间用来摸鱼!!!


改动代码如下(伪代码):


type GeneralContentEditorProps = {
targetId?: string; // 缓存ID
// 省略不相关代码...
};

/**
* 通用的内容编辑器
* @param props
* @returns
*/

export default function GeneralContentEditor({
targetId,
// 省略不相关代码...
}: GeneralContentEditorProps
) {
// 省略不相关代码...
const [content, setContent] = useState('')
const { getCache, setCache, removeCache } = useCache(targetId);

useEffect(() => {
setContent(getCache() ?? '')
}, [])
}

简单解释一下上面的代码:


1)给编辑器新增了一个属性 targetId,这个 targetId 用来作为缓存的唯一标识,由使用方提供给我们


2)初始化的时候去调 getCache 函数读取缓存的数据


3)有内容变更的时候调 setCache 函数去更新缓存的数据


到这里流程已经跑通了,但还缺少重要的一步,需要定时清空一下缓存的数据,因为现在的逻辑是如果我们不主动去获取这个数据,它还是占据着存储空间


清空冗余数据


其实我们也不需要专门去写定时器来清空,只需要在编辑器初始化的时候去检测一遍就可以,所以代码还需加点料,如下图:



到这一步编辑器草稿能力就完善的差不多了,已经能够正常使用了,我们看看效果,如下:



nice,没有什么问题,好了,我要去摸鱼了 😋



作者:上班多喝热水
来源:juejin.cn/post/7419598991119532043
收起阅读 »

老板想集成地图又不想花钱,于是让我...

web
前言 在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案...
继续阅读 »

前言


在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案。于是,我们的目光转向了免费的地图服务——天地图。


天地图简介


天地图(lbs.tianditu.gov.cn/server/guid…
是中国领先的在线地图服务之一,提供全面的地理信息服务。它的API支持地理编码、逆地理编码、周边搜索等多种功能,且完全免费。这正是我们需要的。


具体实现代码


为了将天地图集成到我们的系统中,我们需要进行一系列的开发工作。以下是实现过程中的关键代码段。


1. 逆地理编码


逆地理编码是将经纬度转换为可读的地址。在天地图中,这一功能可以通过以下代码实现:


public static MapLocation reverseGeocode(String longitude, String latitude) {
Request request = new Request();
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
String postStr = String.format(REVERSE_GEOCODE_POST_STR, longitude, latitude);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = REVERSE_GEOCODE_URL + "?tk=" + TK + "&type=" + GEOCODE + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
String status = jsonObject.getString("status");
if (!"0".equals(status)) {
return null;
}
JSONObject resultObject = jsonObject.getJSONObject("result");
MapLocation mapLocation = new MapLocation();
String formattedAddress = resultObject.getString("formatted_address");
mapLocation.setAddress(formattedAddress);
String locationStr = resultObject.getString("location");
JSONObject location = JSON.parseObject(locationStr);
String lon = location.getString("lon");
String lat = location.getString("lat");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lat), Double.valueOf(lon));
lon = String.valueOf(locateInfo.getLongitude());
lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
JSONObject addressComponent = resultObject.getJSONObject("addressComponent");
String address = addressComponent.getString("address");
mapLocation.setName(address);
mapLocation.setCity(addressComponent.getString("city"));
return mapLocation;
}
return null;
}

2. 周边搜索


周边搜索允许我们根据一个地点的经纬度搜索附近的其他地点。实现代码如下:


public static List<MapLocation> nearbySearch(String query, String longitude, String latitude, String radius) {
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
Request request = new Request();
String longLat = longitude + "," + latitude;
String postStr = String.format(NEARBY_SEARCH_POST_STR, query, Integer.valueOf(radius), longLat);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");
if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}

3. 文本搜索


文本搜索功能允许用户根据关键词搜索地点。实现代码如下:


public static List<MapLocation> searchByText(String query, String mapBound) {
Request request = new Request();
String postStr = String.format(SEARCH_BY_TEXT_POST_STR, query, mapBound);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");

if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
LocateInfo locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}

4. 坐标系转换


由于天地图使用的是WGS84坐标系,而国内常用的是GCJ-02坐标系,因此我们需要进行坐标转换。以下是坐标转换的工具类:



/**
* WGS-84:是国际标准,GPS坐标(Google Earth使用、或者GPS模块)
* GCJ-02:中国坐标偏移标准,Google Map、高德、腾讯使用
* BD-09:百度坐标偏移标准,Baidu Map使用(经由GCJ-02加密而来)
* <p>
* 这些坐标系是对真实坐标系统进行人为的加偏处理,按照特殊的算法,将真实的坐标加密成虚假的坐标,
* 而这个加偏并不是线性的加偏,所以各地的偏移情况都会有所不同,具体的内部实现是没有对外开放的,
* 但是坐标之间的转换算法是对外开放,在网上可以查到的,此算法的误差在0.1-0.4之间。
*/

public class GCJ02_WGS84Utils {

public static double pi = 3.1415926535897932384626;//圆周率
public static double a = 6378245.0;//克拉索夫斯基椭球参数长半轴a
public static double ee = 0.00669342162296594323;//克拉索夫斯基椭球参数第一偏心率平方

/**
* 从GPS转高德
* isOutOfChina 方法用于判断经纬度是否在中国范围内,如果不在中国范围内,则直接返回原始的WGS-84坐标。
* transformLat 和 transformLon 是辅助函数,用于进行经纬度的转换计算。
* 最终,wgs84ToGcj02 方法返回转换后的GCJ-02坐标系下的经纬度。
*/

public static LocateInfo wgs84_To_Gcj02(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
} else {
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);
}
return info;
}

//从高德转到GPS
public static LocateInfo gcj02_To_Wgs84(double lat, double lon) {
LocateInfo info = new LocateInfo();
LocateInfo gps = transform(lat, lon);
double lontitude = lon * 2 - gps.getLongitude();
double latitude = lat * 2 - gps.getLatitude();
info.setChina(gps.isChina());
info.setLatitude(latitude);
info.setLongitude(lontitude);
return info;
}

// 判断坐标是否在国外
private static boolean isOutOfChina(double lat, double lon) {
if (lon < 72.004 || lon > 137.8347)
return true;
if (lat < 0.8293 || lat > 55.8271)
return true;
return false;
}

//转换
private static LocateInfo transform(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
return info;
}
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);

return info;
}

//转换纬度所需
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
+ 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}

//转换经度所需
private static double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
* Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
}

结论


通过上述代码,我们成功地将天地图集成到了我们的系统中,不仅满足了功能需求,还大幅降低了成本。这一过程中,我们深入理解了地图服务的工作原理,也提升了团队的技术能力。


注意事项



  • 确保在使用天地图API时遵守其服务条款,尤其是在商业用途中。

  • 由于网络或其他原因,天地图API可能存在访问延迟或不稳定的情况,建议在生产环境中做好异常处理和备用方案。

  • 坐标系转换是一个复杂的过程,确保使用可靠的算法和工具进行转换,以保证定位的准确性。


通过这次集成,我们不仅为公司节省了成本,还提升了系统的稳定性和用户体验。在未来的开发中,我们将继续探索更多高效、低成本的技术解决方案。


作者:JustinNeil
来源:juejin.cn/post/7419524888041472009
收起阅读 »

js中的finally一定会执行吗?

web
背景 在我们程序开发中,我们的代码会出现这种或那种的错误,我们使用try...catch进行捕获。如果需要不管是成功还是失败都需要执行,我们可能需要finally。 那么有一个问题,无论是否发生错误,在finally中的代码一定会执行吗? 下面我们看一个案例:...
继续阅读 »

背景


在我们程序开发中,我们的代码会出现这种或那种的错误,我们使用try...catch进行捕获。如果需要不管是成功还是失败都需要执行,我们可能需要finally


那么有一个问题,无论是否发生错误,在finally中的代码一定会执行吗?


下面我们看一个案例:


1. 案例


场景:请求一个接口,如果接口没有正确返回,我们使用try...finally包裹代码,代码如下:


function getMember(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve(num)
}
if (num === 0) {
reject()
}
}, 2000)
})
}

async function init() {
try {
console.log('打印***start')
await getMember(0)
console.log('打印***end')
} catch (err) {
console.log('打印***err')
} finally {
console.log('打印***finally')
}
}

结果如下:


image.png


上述案例中,如果请求传入的num由另外一个接口返回,num的值不是0或者1,上述的getMember就一直处于pengding状态,接下来的finally也不会执行。


我们也可以这样理解,当在处理Promise问题时,我们需要确保Promise始终得到结果,不管是成功还是失败。


上述代码可以完善如下:


function getMember(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve(num);
} else if (num === 0) {
reject(new Error('Num is 0'));
} else {
// 默认情况,也解决Promise
resolve('Some default value');
}
}, 2000);
});
}

async function init() {
try {
console.log('打印***start');
const result = await getMember(2); // 传递一个非0非1的值
console.log('打印***end', result);
} catch (err) {
console.log('打印***err', err);
} finally {
console.log('打印***finally'); // 这行总是会被执行
}
}

init();

修改后的例子中,无论num的值是什么,Promise都会被解决(要么通过resolve,要么通过reject),,确保Promise被正常处理,才能确保finally执行。


2. try...catch注意点


2.1 仅对运行时的 error 有效


要使得 try...catch 能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。


如果代码包含语法错误,那么 try..catch 将无法正常工作,例如含有不匹配的花括号:


try {
{
{
} catch (err) {
alert("引擎无法理解这段代码,它是无效的");
}

结果如下:


image.png


JavaScript 引擎首先会读取代码,然后运行它。在读取阶段发生的错误被称为“解析时间(parse-time)”错误,并且无法恢复(从该代码内部)。这是因为引擎无法理解该代码。


所以,try...catch 只能处理有效代码中出现的错误。这类错误被称为“运行时的错误(runtime errors)”,有时被称为“异常(exceptions)”。


2.2 try...catch 同步执行


如果在定时代码中发生异常,例如在 setTimeout 中,则 try...catch 不会捕获到异常:


try {
setTimeout(function () {
noSuchVariable; // 脚本将在这里停止运行
}, 1000);
} catch (err) {
alert("不工作");
} finally {
console.log('打印***finally')
}

结果如下:


image.png


因为 try...catch 包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了 try...catch 结构。


为了捕获到计划的(scheduled)函数中的异常,那么 try...catch 必须在这个函数内:


try {
setTimeout(function () {
try {
noSuchVariable; // 脚本将在这里停止运行
} catch (error) {
console.log(error)
}
}, 1000);
} catch (err) {
alert("不工作");
} finally {
console.log('打印***finally')
}


结果如下:
image.png


总结


在使用try...catch...finally的时候,无论是否发生异常(即是否执行了catch块),finally块中的代码总是会被执行,除非在trycatchfinally块中发生了阻止程序继续执行的情况(如Promsie一直处理pending状态)。


如有错误,请指正O^O!


作者:一诺滚雪球
来源:juejin.cn/post/7419524503200677898
收起阅读 »

这个评论系统设计碉堡了

先赞后看,南哥助你Java进阶一大半 geeksforgeeks.org官网给出了Facebook评论系统的高级设计图,Facebook的评论竟然是支持实时刷新的。也就是说用户不用刷新帖子,只要帖子有新的评论就会自动推送到用户端,这里Facebook使用的便...
继续阅读 »

先赞后看,南哥助你Java进阶一大半



geeksforgeeks.org官网给出了Facebook评论系统的高级设计图,Facebook的评论竟然是支持实时刷新的。也就是说用户不用刷新帖子,只要帖子有新的评论就会自动推送到用户端,这里Facebook使用的便是每天在全球有超过20亿设备在使用的WebSocket技术。


在这里插入图片描述


我是南哥,一个Java学习与进阶的领路人。


相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。



⭐⭐⭐本文收录在《Java学习/进阶/面试指南》:https://github/JavaSouth



精彩文章推荐



1. 评论系统设计


1.1 评论表如何设计


评论系统的表要这么设计,每条评论的id标识要么是根评论id、要么是回复评论id。如果是根评论,那parent_comment_id字段就为空;而回复别人的评论,parent_comment_id字段指向根评论id。


CREATE TABLE `comments` (
`comment_id` INT AUTO_INCREMENT PRIMARY KEY, -- 评论唯一ID
`user_id` INT NOT NULL, -- 用户ID
`content` TEXT NOT NULL, -- 评论内容
`parent_comment_id` INT DEFAULT NULL, -- 如果是回复,则指向原始评论ID
`post_id` INT NOT NULL, -- 被评论的帖子或内容ID
`like_count` INT DEFAULT 0, -- 点赞数量
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 评论创建时间
);

我们还要给评论加上点赞数,南哥给大家看看抖音的评论设计。


用户可以给每条评论打上点赞,所以我们应该再设计一个点赞表。其实抖音这种评论模式叫嵌套式评论结构,嵌套式评论注重用户对话交流,用户可以很方便地查看一个对话里的所有回复,我们看下抖音评论里有着展开10条回复的按钮。


在这里插入图片描述


其他评论模式设计还有平铺式评论结构,像微信朋友圈,或者Github的issue都是平铺式评论结构。这种设计更适合用户关注重点在发布的内容本身,而不是对话。大家有没发现微信朋友圈的特点是对话比较少点,点赞反而更多。


来看看点赞表设计。


CREATE TABLE `comment_likes` (
`user_id` INT NOT NULL, -- 点赞用户ID
`comment_id` INT NOT NULL, -- 被点赞的评论ID
`liked_timeMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 点赞时间
);

1.2 评论数据存储


抖音每天产生视频几百万、上千万,每个视频的评论高的甚至有上万条评论,要怎么样的数据查询设计才能支持每天亿级的评论?


南哥先假设我们用MySQL作为实际的数据存储,这么高的并发肯定不能让查询直接冲击数据库 。再分库分表也是没用。


在这里插入图片描述


Elasticsearch官网这么宣传它的产品:



Elasticsearch 极其快速,快到不可思议



当用户发表评论时,我们首先把评论写入MySQL数据库,再使用异步机制把评论同步到Elasticsearch中。当在用户请求查询评论时,优先从 Elasticsearch 中进行查询。


// 评论存储到MySQL、Elasticsearch
public void storeComment(Comment comment) {
// 将评论存入 MySQL
commentRepository.save(comment);

// 异步将评论同步到 Elasticsearch
CompletableFuture.runAsync(() -> {
elasticsearchService.indexComment(comment);
});
}

1.3 事务控制


大家想一想以上设计,有哪些需要进行事务控制?


例如comment_likes点赞表的插入和comment评论表的更新,用户为某一个评论点赞,会在comment_likes表插入一条新记录,同时会更新comment表的点赞数量。


但是,从用户需求的角度来看,用户并不在意点赞数的强一致性和实时性,这点不使用事务也可以接受。


我曾经和老外程序员在论坛聊过,他说他们的点赞后端分布式服务用的本地缓存,即使每一个服务的本地缓存相对不太一致,对系统完全没有影响。


// 事务控制
@Transactional
public void likeComment(int commentId, int userId) {
// 插入一条点赞记录
commentLikesRepository.insert(userId, commentId);

// 更新评论表中的点赞数量,假设有一个专门的方法来处理这个更新
commentRepository.incrementLikeCount(commentId);
}

1.4 点赞数加入Redis


点赞数相比评论来说,量更加巨大,用户点赞时直接落到MySQL数据库肯定不合理,服务器扛不住也没必要扛。


假如点赞数没有进行事务控制。南哥打算这样处理,用户点赞后,后端服务接受到点赞请求,把用户内容、点赞数放到Redis里,这里采用Redis五大基本类型之一:Map。


// Map结构
comment_like_key = [comment_id_6:like_count = 66, comment_id_7:like_count = 77]

我们需要查询点赞数时直接从高性能内存数据库Redis查询。


当然这还没完,MySQL数据库和Elasticsearch的点赞量需要去同步更新,我们设置定时任务每个一段时间完成数据同步任务。上文的comment_likes点赞记录表同样需要记录,把点赞放到Redis时进行异步添加点赞记录即可。


// 定时任务数据同步任务
@Scheduled(fixedRate = 10000)
public void syncLikes() {
// 从 Redis 中读取最新的点赞数据
Map<Integer, Integer> likes = redisService.fetchAllLikes();

// 同步到 MySQL 和 Elasticsearch
likes.forEach((commentId, likeCount) -> {
commentRepository.updateLikeCount(commentId, likeCount);
elasticsearchService.updateLikeCount(commentId, likeCount);
});
}

戳这,《JavaSouth》作为一份涵盖Java程序员所需掌握核心知识、面试重点的神秘文档。


我是南哥,南就南在Get到你的点赞点赞。


在这里插入图片描述



创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️



作者:JavaSouth南哥
来源:juejin.cn/post/7418084847615737868
收起阅读 »

2024 最新最全 VS Code 插件推荐!

Visual Studio Code 是由微软开发的一款开源的代码编辑器,它有了一个丰富的插件市场,提供了很多实用的插件。本文就来分享 2024 年开发必备的 VS Code 插件! 功能强化 驼峰翻译助手 纠结怎么取变量? 中文一键翻译转换成常用大小驼峰等...
继续阅读 »

Visual Studio Code 是由微软开发的一款开源的代码编辑器,它有了一个丰富的插件市场,提供了很多实用的插件。本文就来分享 2024 年开发必备的 VS Code 插件!
VS Code 插件.png


功能强化


驼峰翻译助手


纠结怎么取变量? 中文一键翻译转换成常用大小驼峰等格式。
vscode1.gif


change-case


Change-case插件提供了一种简单的方法来将单词或变量名更改为各种情况,包括驼峰命名(camelCase)、下划线命名(snake_case)、标题命名(TitleCase)等多种格式。
3c5e29b6-7a9c-11e5-9ce4-7eb944889696.gif


Codelf


变量命名神器,搜索 Github、Bitbucket、Google Code、Codeplex、Sourceforge、Fedora Project、GitLab 中的项目以查找实际使用的变量名称。
image.png


Surround


用于在代码块周围添加包装代码片段。该插件支持语言标识符、多选区操作、完全可自定义、自定义包装代码片段,并为每个包装代码片段单独分配快捷键。
demo.gif


Duplicate Action


一键复制并创建文件或文件夹,提高了开发过程中的文件操作效率。
image.png


CSS Peek


它允许开发者直接从HTML文档中快速跳转到匹配的CSS样式定义,并提供预览功能,从而大大提高CSS样式的查找和编辑效率。
working.gif


Regex Previewer


可以实时预览正则表达式匹配结果,并适用于多种前端框架和语言,同时提供快捷键操作、全局和多行选项等便捷功能,以提升开发效率。
image.png


Code Spell Checker


Code Spell Checker 插件可以检查单词拼写是否出现错误,检查的规则遵循 camelCase (驼峰拼写法)。
1_3tgHzDUWembdB022yWutug.gif


Markdown Preview Enhanced


支持实时预览 Markdown 文件效果,并具备导出 PDF、支持数学公式、流程图等多种高级功能,提供了丰富的定制选项和兼容性,极大地提升了 Markdown 文档的编辑和预览体验。
image.png


Markdown All in One


用于提供Markdown编辑的全方位支持,包括实时预览、语法提示、目录生成、表格生成等多种功能。
section-numbers.gif


i18n-ally


主要用于国际化多语言开发,提供内联提示、快速修改key值对应的语言文件、统一管理翻译、自动翻译等功能。
image.png


GitHub Repositories


在 VS Code 中快速打开 Github 仓库,无需克隆到本地。
remote-repositories.gif


Turbo Console Log


一键生成有意义的 console.log 消息,支持多语言、多光标操作,提供可定制的日志类型和输出格式,提高调试效率。
image.png


indent-rainbow


一款代码缩进可视化插件,它通过为文本前面的缩进着色,使缩进更具可读性。
image.png


Remote-SSH


允许开发者通过 SSH 协议连接到远程服务器或虚拟机,直接在本地 VS Code 编辑器中操作远程服务器上的代码,实现无缝的远程开发体验。主要功能包括远程连接、无缝的代码编辑和调试、扩展兼容性、多种连接选项、优化的性能以及支持多个远程服务器同时连接等。
image.png


前端框架


ES7+ React/Redux/React-Native snippets


提供了许多速记前缀来加速开发并帮助开发人员为 React、Redux、GraphQL 和 React Native 创建代码片段和语法。
image.png


Typescript React Code Snippets


此插件包含了使用 Typescript 的 React 代码片段,它支持 Typescript(.ts) 或 TypeScript React (.tsx) 等语言。以下是使用 TypeScript 创建 React 组件的两个片段。



  • 默认导出 React:


image03.gif



  • 导出 React 组件:


image04.gif


Vue - Official


Vue 官方扩展。
image.png


Vue 3 Snippets


这个插件包含了所有的 Vue.js 2 和 Vue.js 3 的 api 对应的代码片段。
image.png


Vue VSCode Snippets


此插件将 Vue 2 Snippets 和 Vue 3 Snippets 添加到 Visual Studio Code 中。
image.png


React Native Tools


允许在不同的模拟器或仿真器上轻松运行和调试代码,从命令面板快速运行 react-native 命令,而无需在终端中手动运行命令,并使用 IntelliSense 浏览 React Native 的函数、对象和参数等。
react-features.gif


JavaScript (ES6) code snippets


通过此插件可以使用预定义的 ES6 语法片段速记,从而提高开发效率。这个 VS Code 插件可以自定义,因为它不特定于任何框架。
image.png


Tailwind CSS IntelliSense


专为使用 Tailwind CSS 的开发者设计,提供实时的类名建议、自动完成、文档预览等功能,以提升 Tailwind CSS 的开发效率和代码质量。
image.png


Git 集成


GitLens


该插件增强了 VS Code 中的 Git,并从每个存储库中释放隐藏数据。可以快速查看代码的编写者、轻松导航和探索 Git 存储库、通过丰富的可视化效果和强大的比较命令获取有效信息,以及执行更多操作,帮助我们更好地理解代码。
image.png


Git History


该插件用于查看 Git 日志和文件历史记录并比较分支或提交。
compareCommits.gif


Git Graph


Git Graph 插件用于可视化查看存储库的 Git 操作,并从图形中轻松执行Git操作。
d73da0b0eb366dfb890662c10487d725.gif


统计分析


Import Cost


在项目中导入多个包时可能会出现性能问题,Import Cost 就用于查看将特定库导入项目的成本。该插件会显示导入库的大小,如果大小为绿色,则表示库很小,而红色表示库很大。
image.png


Time Master


从编程活动中自动生成的指标、见解和时间跟踪。它是一个开源项目,独立于网络环境,安全轻量。
image.png


VS Code Counter


VS Code Counter 插件用于统计项目代码的行数,安装插件之后,右键点击需要统计代码的文件夹,选择“Count lines in directory”,这时就会在项目根目录出现一个名为 .VSCodeCounter 的文件夹,包含了不同格式的结果,编辑器会打开其中的的 .md 格式。结果中会显示代码总行数,不同格式文件行数,不同路径文件函数等。代码行数中有纯代码行数、空白行数、注释行数。
image.png


AI 编程辅助


GitHub Copilot


GitHub Copilot 是 Github 推出的一款 AI 结对编程工具,可以帮助开发者更快、更智能地编写代码,不过该插件并不是免费的。
image.png


Tabnine


Tabnine 是一款 AI 代码助手,可加速和简化软件开发,同时保证代码的私密性、安全性和合规性。
image.png


Codeium


一个基于 AI 技术的免费代码加速工具包,为VSCode提供70多种语言的快速自动补全、聊天和搜索功能,支持IDE内聊天和多种编程语言的建议。
chat_short_demo.gif


TONGYI Lingma


通义灵码是阿里云推出的一款基于通义大模型的智能编码辅助工具,提供实时续写、自然语言生成代码、单元测试生成、代码注释生成、代码解释、研发智能问答、异常报错排查等能力。
image.png


CodeGeeX


CodeGeeX 是一款基于大模型的智能编程助手,它完善了代码的生成与补全,自动为代码添加注释,此外,它还针对代码问题的智能问答,当然还包括代码解释,实现代码,修复代码bug等非常丰富的功能。
image.png


代码美化


Highlight Matching Tag


用于实时高亮显示匹配的标签对,方便用户在 HTML 或 XML 代码中快速找到配对的标签。它可以在点击一个标签时,自动显示配对的标签,并通过下划线或其他样式来指示它们之间的对应关系。
image.png


TODO Highlight


实时高亮显示代码中的TODO、FIXME等标记,支持自定义关键字和正则表达式匹配,方便开发者快速识别、管理和追踪待办事项。
image.png


Todo Tree


用于在Visual Studio Code中搜索、管理和高亮代码中的待办事项标记(如TODO、FIXME等)。它支持自定义标签、颜色编码、实时更新、过滤与排序等功能,并以可视化的树形结构展示待办事项列表,方便开发者快速定位、编辑和跟踪代码中的待办事项。
image.png


Better comments


该插件对不同类型的注释会附加了不同的颜色,更加方便区分,帮助我们在代码中创建更人性化的注释。
image.png


Colorize


Colorize 会给颜色代码增加一个当前匹配代码颜色的背景。它通过 CSS 变量、预处理器变量、hsl/hsla 颜色、跨浏览器颜色、exa、rgb、rgba和argb的彩色背景将 CSS 颜色可视化,帮助开发者快速区分颜色。
image.png


Image preview


通过此插件,当鼠标悬浮在图片的链接上时,可以实时预览该图片,除此之外,还可以看到图片的大小和分辨率。
image.png


CodeSnap


CodeSnap 用于对代码的进行截图和共享。屏幕截图可以用文本或形状进行注释,并通过链接共享或包含在网站或文档中。只需使用 ctrl + shift + P 并输入 CodeSnap,然后按回车键,CodeSnap 窗口就会打开。
image.png


Error Lens


Error Lens 是一款把代码检查(错误、警告、语法问题)进行突出显示的插件。Error Lens 通过使诊断更加突出,增强了语言的诊断功能,突出显示了由该语言生成的诊断所在的整行,并在代码行的位置以行方式在线打印了诊断消息。
image.png


Pretty TypeScript Errors


Pretty TypeScript Errors 旨在使 TypeScript 的错误信息更易读、更人性化。随着 TypeScript 类型复杂性的增加,错误消息可能会变得混乱不堪,充满了难以理解的括号和“...”。这个扩展通过重新格式化或解释 TypeScript 编译器产生的原始错误信息,来帮助开发者更容易地理解发生了什么。
image.png


编辑器美化


Power Mode


Power Mode 旨在通过添加视觉特效来增强编程体验,通过了诸如粒子效果、烟火、火焰、魔法效果等特效,让编程过程更加生动有趣。
demo-v3.gif


One Dark Pro


一款编辑器主题。
image.png


Dracula Official


一款编辑器主题。
image.png


GitHub Theme


一款编辑器主题。
image.png


Winter Is Coming Theme


一款编辑器主题。
image.png


Ayu


一款编辑器主题。
image.png


vscode-icons


VSCode 官方出品的图标库。
5e3bab235c0f53089eb30ee0c5a81be5.gif


Material Icon Theme


该插件根据最新的 Material Design 主题为文件和文件夹提供图标。它可以帮助我们识别文件并为编辑器添加自定义的外观。
image.png


Peacock


允许开发者为 Visual Studio Code 的工作区界面(如侧边栏、底栏和标题栏)自定义颜色,以区分不同的项目或编码环境。
image.png


作者:CUGGZ
来源:juejin.cn/post/7384765023343394827
收起阅读 »

与其造神,不如依靠群体的力量:这家公司走出了一条不同于 OpenAI 的 AGI 路线

看过剧版《三体》的读者或许都记得一个名场面:来自三体的智子封锁了人类科技,还向地球人发出了「你们是虫子」的宣告。但没有超能力的普通人史强却在蝗群漫天飞舞的麦田中喊出:「把我们人类看成是虫子的三体人,他们似乎忘了一个事实,那就是虫子从来就没有被真正地战胜过」。 ...
继续阅读 »

看过剧版《三体》的读者或许都记得一个名场面:来自三体的智子封锁了人类科技,还向地球人发出了「你们是虫子」的宣告。但没有超能力的普通人史强却在蝗群漫天飞舞的麦田中喊出:「把我们人类看成是虫子的三体人,他们似乎忘了一个事实,那就是虫子从来就没有被真正地战胜过」。



三体人看到的是单个虫子脆弱的一面 —— 你可以轻松踩死一只蚂蚁,打死一只蝗虫、蜜蜂。但他们没有看到的是,当这些虫子集结在一起时,它们可以涌现出远超个体简单相加的力量。



科学家们很早就发现了这种力量,并将其命名为「群体智能」(Swarm Intelligence)。这种智能不是由某个中央大脑控制,而是通过个体间的简单互动和信息交换自然形成的。它是一种集体智慧的体现,是自然界中一种奇妙而高效的协作方式。


其实,从宏观上说,人类社会的不断发展和演化也是一种群体智能现象,绝大多数文明成果都是人类个体在长期群体化、社会化的生产生活中逐渐演化形成的产物。


那么,人工智能的发展能否借鉴这种模式?答案自然是「能」。但长期以来,由于机器的个体智能化程度较低等原因,「群体智能」迟迟难以涌现。


生成式 AI 的发展或许可以推动这些问题的解决,也让「群体智能」获得了新一轮的关注。


这波生成式 AI 相当于把个体的智能化水平提升上去了。而个体智能的提升,意味着群体的智能有望实现指数级增长。」在近期的一次访谈中,RockAI CEO 刘凡平向机器之心表达了这样的观点。



RockAI 是一家成立于 2023 年 6 月的 AI 初创,他们自研了国内首个非 Attention 机制的 Yan 架构通用大模型,并将这个大模型部署在了手机、PC、无人机、机器人等多种端侧设备上,还尝试让自己的大模型在这些设备上实现「自主学习」能力。


而这一切均服务于一个宏大的目标 —— 让每一台设备都拥有自己的智能,而且是可以像人类一样实时学习、个性化自主进化的系统。刘凡平认为,当这些拥有不同能力、不同个性的智能单元得以协同,即可完成数据共享、任务分配和策略协调,涌现出更为宏大、多元的群体智能,最终实现个性化与群体智能的和谐统一,开启人与机器的智能新时代。


那这一切怎么去实现呢?在访谈中,刘凡平和邹佳思(RockAI 联合创始人)向机器之心分享了他们的路线图和最新进展。


一条不同于 OpenAI 的 AGI 路线


前面提到,「群体智能」的研究进展受限于单个个体的智能化程度,所以研究者们首先要解决的问题就是让单个个体变得足够聪明


要说「聪明」,OpenAI 的模型可以说是出类拔萃。但从目前的情况来看,他们似乎更侧重于训练出拥有超级智能的单个大模型。而且,这条路线走起来并不容易,因为它高度依赖海量的数据和计算资源,这在能源、数据和成本上都带来了可持续性的问题。 


此外,通过一个超级智能模型来处理所有任务是一种高度中心化的模式,这在实践中容易出现智能增长的瓶颈,因为单一模型缺乏灵活的适应能力和协作效应,导致其智能提升速度受到限制。


那么,OpenAI 未来有没有可能也走群体智能的路线?这个问题目前还没有明确答案。但可以看到的一点是,以该公司和其他大部分公司当前采用的 Transformer 架构去构建群体智能的单个个体可能会遇到一些障碍


首先是高算力需求的障碍。以 Attention 机制为基础的 Transformer 架构对计算资源的需求非常高,其计算复杂度为 O (n^2)(n 为序列长度)。这意味着随着输入序列的增长,计算成本急剧增加。在构建群体智能时,我们需要多个单元大模型协同工作,而这些单元大模型往往部署在低算力的设备上(如无人机、手机、机器人等)。如果不经过量化、裁剪等操作,Transformer 架构的模型很难在低算力设备上直接部署。所以我们看到,很多公司都是通过这些操作让模型成功在端侧跑起来。


但对于群体智能来说,光让模型跑起来还不够,还要让它们具备自主学习的能力。在刘凡平看来,这一点至关重要。


他解释说,在一个没有自主学习的群体中,最聪明的个体会主导其他智能体的决策,其他智能体只能跟随它的指引。这种情况下,群体智能的上限就是最聪明个体的水平,无法超越。但通过自主学习,每个智能体都可以独立提升自身的智能水平,并逐渐接近最聪明的个体。而且,自主学习促进了知识共享,类似于人类的知识传承。这样,群体中的所有智能体都会变得更聪明,群体整体的智能水平有望实现指数级增长,远远超出简单的个体累加。


量化、裁剪等操作最致命的问题,就是破坏了模型的这种自主学习能力。「当一个模型被压缩、量化、裁剪之后,这个模型就不再具备再学习的能力了,因为它的权重已经发生了变化,这种变化基本是不可逆的。这就像我们把一个螺丝钉钉入墙中,如果在敲入的过程中螺丝钉受到损坏,那么想要把它取出来重新使用就变得很困难,让它变得更锋利就变得不可能。」刘凡平解释说。


讲到这里,实现群体智能的路线其实就已经非常清晰了:



  • 首先,你要在架构层面做出改变,研发出一种可以克服 Transformer 缺陷的新架构。

  • 然后,你要将基于这个架构的模型部署到各种端侧设备上,让模型和这些设备高度适配。

  • 接下来,更重要的一点是,这个架构的模型要能够在各种端侧设备上自主学习,不断进化。

  • 最后,这些模型与端侧设备结合成的智能体要能够自主协作,共同完成任务。


这其中的每个阶段都不简单:



  • 在第一阶段,新架构不止要具备低算力、部署到端侧原生无损的特点,还要具备可以媲美 Transformer 架构的性能,保证单个个体足够聪明且可以自主学习。

  • 在第二阶段,「大脑和身体」的高度适配涉及感知层面和数据处理的不同模态,每种设备有着不同的需求,这增加了模型和设备适配的复杂性。 

  • 在第三阶段,让模型部署之后还可以学习就意味着要挑战现有的训练、推理完全分离的机制,让模型参数在端侧也可以调整,且调整足够快、代价足够小。这就涉及到对传统反向传播机制的挑战,需要的创新非常底层。

  • 在第四阶段,主要挑战是如何实现智能体之间的有效协作。这个过程要求智能体自主发现并形成完成任务的最佳方案,而不是依赖于人为设定或程序预设的方案。智能体需要根据自己的智能水平来决定协作的方式。


这些难点就决定了,RockAI 必须走一条不同于 OpenAI 的路线,挑战一些传统的已经成为「共识」的方法。


刘凡平提到,在前两个阶段,他们已经做出了一些成果,针对第三、四个阶段也有了一些实验和构想。



群体智能的单元大模型 ——Yan 1.3


第一阶段的标志性进展是一个采用 Yan 架构(而非 Transformer 架构或其变体)的大模型。这个模型的 1.0 版本发布于今年的 1 月份,为非 Attention 机制的通用自然语言大模型。据了解,该模型有相较于同等参数 Transformer 的 7 倍训练效率、5 倍推理吞吐和 3 倍记忆能力。而且,这一模型 100% 支持私有化部署应用,不经裁剪和压缩即可在主流消费级 CPU 等端侧设备上无损运行。


经过半年多的攻关,这一模型刚刚迎来了最新版本 ——Yan 1.3



Yan 1.3 是一个 3B 参数的多模态模型,能够处理文本、语音、视觉等多种输入,并输出文本和语音,实现了多模态的模拟人类交互。



尽管参数量较小,但其效果已超越 Llama 3 8B 的模型。而且,它所用的训练语料比 Llama 3 要少,训练、推理算力也比 Llama 3 低很多。这在众多非 Transformer 架构的模型中是一个非常领先的成绩,其训练、推理的低成本也让它比其他架构更加贴近工业化和商业化。



这些出色的性能得益于高效的架构设计和算法创新。


在架构层面,RockAI 用一个名叫 MCSD(multi-channel slope and decay)的模块替换了 Transformer 中的 Attention 机制,同时保留 Attention 机制中 token 之间的关联性。在信息传递过程中,MCSD 强调了有效信息的传递,确保只有最重要的信息被传递给后续步骤,而且是以 O (n) 的复杂度往下传,这样可以提高整体效率。在验证特征有效性和 token 之间的关联性方面,MCSD 表现优秀。



**在算法层面,RockAI 提出了一种类脑激活机制。**这是一种分区激活的机制,就像人开车和写字会分别激活脑部的视觉区域和阅读区域一样,Yan 1.3 会根据学习的类型和知识范围来自适应调整部分神经元,而不是让全量的参数参与训练。推理时也是如此。具体有哪些神经元来参与运算是由仿生神经元驱动的算法来决定的。



在今年的 GTC 大会上,Transformer 论文作者之一 Illia Polosukhin 提到,像 2+2 这样的简单问题可能会使用大模型的万亿参数资源。他认为自适应计算是接下来必须出现的事情之一,我们需要知道在特定问题上应该花费多少计算资源。RcokAI 的类脑激活机制是自适应计算的一种实现思路。


这或许听起来和 MoE 有点像。但刘凡平解释说,类脑激活机制和 MoE 有着本质的区别。MoE 是通过「专家」投票来决定任务分配,每个「专家」的网络结构都是固定的,其结果是可预测的。而类脑激活机制没有「专家」,也没有「专家」投票的过程,取而代之的是神经元的选择过程。其中的每个神经元都是有价值的,选择的过程也是一个自学习的过程。


这种分区激活机制在 MCSD 的基础上进一步降低了 Yan 架构模型的训练、推理计算复杂度和计算量。


「这也符合人类大脑的运行方式。人脑的功耗只有二十几瓦,如果全部的 860 亿个神经元每次都参与运算,大脑产生的生物电信号肯定是不够用的。」刘凡平说道。目前,他们的类脑激活机制已经得到了脑科学团队的理论支持和实际论证,也申请到了相关专利。


以端侧设备为载体,迈向群体智能


在 Yan 1.3 的发布现场,我们看到了该模型在 PC、手机、机器人、无人机等端侧设备的部署情况。鉴于 Yan 1.2 发布时甚至能在树莓派上运行,这样的端侧部署进展并不令我们感到意外。


视频详情


部署了 Yan 1.3 的机器人。机器人内置硬件为 Intel Core i3。


而且我们知道,这些端侧智能体的潜力才刚刚显露。毕竟,以上创新的目标不只是让模型能够在端侧跑起来(当前很多模型都能做到这一点),而是使其具备自主学习的能力,作为「群体智能的单元大模型」持续进化。无论是 Yan 架构的「0 压缩、0 裁剪」无损部署,还是分区激活的高效计算,都是服务于这一目标。这是 RockAI 和其他专注于端侧 AI 的公司的一个本质区别。


「如果我们拿一个 10 岁的孩子和一个 30 岁的博士来比,那肯定 30 岁的博士知识面更广。但是,我们不能说这个 10 岁的孩子在未来无法达到甚至超越这位博士的成就。因为如果这个 10 岁的孩子自我学习能力足够高,他的未来成长速度可能比 30 岁的博士还要快。所以我们认为,自主学习能力才是衡量一个模型智能化程度的重要标志。」刘凡平说道。可以说,这种自主学习能力才是 RockAI 追求的「scaling law」。


为了实现这种自主学习能力,RockAI 的团队提出了一种「训推同步」机制,即让模型可以在推理的同时,实时有效且持续性地进行知识更新和学习,最终建立自己独有的知识体系。这种「训推同步」的运行方式类似于人类在说话的同时还能倾听并将其内化为自己的知识,对底层技术的要求非常高。


为此,RockAI 的团队正在寻找反向传播的更优解,方法也已经有了一些原型,并且在世界人工智能大会上进行过展示。不过,他们的方法原型目前仍面临一些挑战,比如延迟。在后续 Yan 2.0 的发布中,我们有望见到原型升级版的演示。


那么,在每一台设备都拥有了智能后,它们之间要怎么联结、交互,从而涌现出群体智能?对此,刘凡平已经有了一些初步构想。


首先,它们会组成一个去中心化的动态系统。在系统中,每台设备都拥有自主学习和决策的能力,而不需要依赖一个中央智能来控制全局。同时,它们之间又可以共享局部数据或经验,并通过快速的通信网络互相传递信息,从而在需要时发起合作,并利用其他智能体的知识和资源来提升任务完成的效率。


路线「小众」,挑战与机遇并存


纵观国内 AI 领域,RockAI 走的路可以说非常「小众」,因为里面涉及到非常底层的创新。在硅谷,有不少人在做类似的底层研究,就连「神经网络之父」Hint0n 也对反向传播的一些限制表示过担忧,特别是它与大脑的生物学机制不符。不过,大家目前都还没有找到特别有效的方法,因此这一方向还没有出现明显的技术代差。对于 RockAI 这样的国内企业来说,这既是挑战,也是机遇。


对于群体智能,刘凡平相信,这是一条迈向更广泛的通用人工智能的路线,因为它的理论基础是非常坚实的,「如果没有群体智能,就没有人类社会的文明,更不会有科技的发展」。


而且,刘凡平认为,群体智能所能带来的生产力变革比拥有超级智能的单个大模型所能带来的更全面、更多样。随着自主架构大模型的研发成功和多元化硬件生态的构建,他们相信自己正在逐渐接近这一目标。


我们也期待看到这家公司的后续进展。


参考链接:


news.sciencenet.cn/sbhtmlnews/…


lib.ia.ac.cn/news/newsde…


http://www.shxwcb.com/1205619.htm…


mp.weixin.qq.com/s/t6TurjgHH…


© THE END 


作者:机器之心
来源:juejin.cn/post/7418813003783929868
收起阅读 »

腾讯开源利器:让iOS UI调试更高效

iOS
最近逛G站,偶然发现一款 iOS UI 调试工具,那就是腾讯 QMUI团队 开源的LookinSever[1]。初步体验了一下,功能还是非常强大,简单记录并分享一下。 简介 腾讯的LookinServer[2]是一款专为...
继续阅读 »

最近逛G站,偶然发现一款 iOS UI 调试工具,那就是腾讯 QMUI团队 开源的LookinSever[1]。初步体验了一下,功能还是非常强大,简单记录并分享一下。


简介


腾讯的LookinServer[2]是一款专为iOS开发者设计的UI调试工具,类似于 Xcode 自带的 UI Inspector 工具,或者以前常用的另一款软件Reveal


LookinServer


基本功能


1、实时UI查看: LookinServer可以实时捕捉并显示iOS应用的UI层级结构。这包括所有的视图(Views)、控件(Controls)以及它们的属性(Properties)等。


2、层级视图展示: 通过图形化界面,开发者可以方便地浏览UI的层级关系。这有助于快速定位UI问题,例如某些视图被错误地覆盖或布局不正确。


3、属性编辑: 开发者可以直接在LookinServer中修改视图的属性(如frame、color等),并立即在应用中看到效果。这种所见即所得的调试方式大大加快了UI调整的效率。


4、视图调试: LookinServer支持对单个视图进行详细调试,包括查看其布局约束、事件响应链、以及性能指标等。


工作原理


1、数据抓取: LookinServer会将目标iOS应用中的UI数据抓取下来。这通常涉及到通过iOS的运行时(Runtime)机制和反射机制来获取应用当前的UI层级和视图信息。


2、通信机制: LookinServer客户端与iOS应用之间通过网络通信进行数据传输。应用中集成的LookinServer SDK会将视图层级、属性等数据打包发送到LookinServer客户端进行展示。


3、动态更新: 当开发者在LookinServer客户端中修改视图属性时,修改指令会通过通信机制发送回iOS应用,应用立即应用这些修改并更新显示。通过这种方式,实现了实时的UI调试。


使用场景


1、UI布局调试: 快速发现并修正UI布局问题,例如视图错位、层级不正确等。


2、UI性能优化: 查看每个视图的性能指标,找出性能瓶颈并进行优化。


3、快速迭代: 在开发过程中频繁修改UI时,通过LookinServer可以快速预览效果,减少编译和重启应用的时间。


优势


1、提高效率: 实时查看和修改UI,大大减少了传统调试方式的时间成本。


2、直观可视化: 图形化的视图层级展示,让开发者可以更直观地理解UI结构。


3、易于集成: LookinServer Framework易于集成到现有项目中,支持CocoaPods、 Swift Package Manager以及手动集成,支持OC和Swift,不需要对项目做大的改动。


小试牛刀


1、安装Lookin: 官网[3]下载并安装Lookin Mac客户端。


2、安装 LookinServer Framework:



  • • 通过 CocoaPods

  • • Swift项目:pod 'LookinServer', :subspecs => ['Swift'], :configurations => ['Debug']

  • • ObjC项目:pod 'LookinServer', :configurations => ['Debug']

  • • 通过 Swift Package Managergithub.com/QMUI/Lookin…

  • • 手动集成:下面以OC项目为例。 将下载的源码导入项目中,注意Swift文件夹里面的可以删除,或者将文件LKS_SwiftTraceManager.swift不加到target里参与编译。


图片


Debug模式下,打开SHOULD_COMPILE_LOOKIN_SERVER宏定义。


图片


3、简单使用: 建个项目,拖几个控件,运行,打开第一步安装的LookinMac 软件,监测到运行的项目,可以看到视图层级关系、target-action、手势、常见属性设置等,UI及时同步刷新。


图片


注意事项


1、需要在 Debug 模式下使用 


2、使用 1.0.6及以后 的版本


总结


腾讯的LookinServer是一个强大的iOS UI调试工具,其通过实时查看、编辑和调试视图层级和属性,极大地提高了UI开发和调试的效率。通过掌握其原理和使用方法,开发者可以更高效地处理UI问题,提高应用的整体质量。


引用链接


[1] LookinSever: github.com/QMUI/Lookin…

[2] LookinServer: lookin.work/

[3] 官网: lookin.work/


作者:人月神话Lee
来源:juejin.cn/post/7376586649982091301
收起阅读 »

听我一句劝,业务代码中,别用多线程。

你好呀,我是歪歪。 前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。 虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。 我只是微微一笑,这不是很正常吗? 业务代码中一般也使不上多线...
继续阅读 »

你好呀,我是歪歪。


前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。


虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。


我只是微微一笑,这不是很正常吗?


业务代码中一般也使不上多线程,或者说,业务代码中不知不觉你以及在使用线程池了,你再 duang 的一下搞一个出来,反而容易出事。


所以提到线程池的时候,我个人的观点是必须把它吃得透透的,但是在业务代码中少用或者不用多线程。


关于这个观点,我给你盘一下。


Demo


首先我们还是花五分钟搭个 Demo 出来。


我手边刚好有一个之前搭的一个关于 Dubbo 的 Demo,消费者、生产者都有,我就直接拿来用了:



这个 Demo 我也是跟着网上的 quick start 搞的:



cn.dubbo.apache.org/zh-cn/overv…




可以说写的非常详细了,你就跟着官网的步骤一步步的搞就行了。


我这个 Demo 稍微不一样的是我在消费者模块里面搞了一个 Http 接口:



在接口里面发起了 RPC 调用,模拟从前端页面发起请求的场景,更加符合我们的开发习惯。


而官方的示例中,是基于了 SpringBoot 的 CommandLineRunner 去发起调用:



只是发起调用的方式不一样而已,其他没啥大区别。


需要说明的是,我只是手边刚好有一个 Dubbo 的 Demo,随手就拿来用了,但是本文想要表达的观点,和你使不使用 Dubbo 作为 RPC 框架,没有什么关系,道理是通用的。


上面这个 Demo 启动起来之后,通过 Http 接口发起一次调用,看到控制台服务提供方和服务消费方都有对应的日志输出,准备工作就算是齐活儿了:



上菜


在上面的 Demo 中,这是消费者的代码:



这是提供者的代码:



整个调用链路非常的清晰:



来,请你告诉我这里面有线程池吗?


没有!


是的,在日常的开发中,我就是写个接口给别人调用嘛,在我的接口里面并没有线程池相关的代码,只有 CRUD 相关的业务代码。


同时,在日常的开发中,我也经常调用别人提供给我的接口,也是一把梭,撸到底,根本就不会用到线程池。


所以,站在我,一个开发人员的角度,这个里面没有线程池。


合理,非常合理。


但是,当我们换个角度,再看看,它也是可以有的。


比如这样:



反应过来没有?


我们发起一个 Http 调用,是由一个 web 容器来处理这个请求的,你甭管它是 Tomcat,还是 Jetty、Netty、Undertow 这些玩意,反正是个 web 容器在处理。


那你说,这个里面有线程池吗?


在方法入口处打个断点,这个 http-nio-8081-exec-1 不就是 Tomcat 容器线程池里面的一个线程吗:



通过 dump 堆栈信息,过滤关键字可以看到这样的线程,在服务启动起来,啥也没干的情况下,一共有 10 个:



朋友,这不就是线程池吗?


虽然不是你写的,但是你确实用了。


我写出来的这个 test 接口,就是会由 web 容器中的一个线程来进行调用。所以,站在 web 容器的角度,这里是有一个线程池的:



同理,在 RPC 框架中,不管是消费方,还是服务提供方,也都存在着线程池。


比如 Dubbo 的线程池,你可以看一下官方的文档:



cn.dubbo.apache.org/zh-cn/overv…




而对于大多数的框架来说,它绝不可能只有一个线程池,为了做资源隔离,它会启用好几个线程池,达到线程池隔离,互不干扰的效果。


比如参与 Dubbo 一次调用的其实不仅一个线程池,至少还有 IO 线程池和业务线程池,它们各司其职:



我们主要关注这个业务线程池。


反正站在 Dubbo 框架的角度,又可以补充一下这个图片了:



那么问题来了,在当前的这个情况下?


当有人反馈:哎呀,这个服务吞吐量怎么上不去啊?


你怎么办?


你会 duang 的一下在业务逻辑里面加一个线程池吗?



大哥,前面有个 web 容器的线程池,后面有个框架的线程池,两头不调整,你在中间加个线程池,加它有啥用啊?


web 容器,拿 Tomcat 来说,人家给你提供了线程池参数调整的相关配置,这么一大坨配置,你得用起来啊:



tomcat.apache.org/tomcat-9.0-…




再比如 Dubbo 框架,都给你明说了,这些参数属于性能调优的范畴,感觉不对劲了,你先动手调调啊:



你把这些参数调优弄好了,绝对比你直接怼个线程池在业务代码中,效果好的多。


甚至,你在业务代码中加入一个线程池之后,反而会被“反噬”。


比如,你 duang 的一下怼个线程池在这里,我们先只看 web 容器和业务代码对应的部分:



由于你的业务代码中有线程池的存在,所以当接受到一个 web 请求之后,立马就把请求转发到了业务线程池中,由线程池中的线程来处理本次请求,从而释放了 web 请求对应的线程,该线程又可以里面去处理其他请求。


这样来看,你的吞吐量确实上去了。


在前端来看,非常的 nice,请求立马得到了响应。


但是,你考虑过下游吗?


你的吞吐量上涨了,下游同一时间处理的请求就变多了。如果下游跟不上处理,顶不住了,直接就是崩给你看怎么办?



而且下游不只是你一个调用方,由于你调用的太猛,导致其他调用方的请求响应不过来,是会引起连锁反应的。


所以,这种场景下,为了异步怼个线程池放着,我觉得还不如用消息队列来实现异步化,顶天了也就是消息堆积嘛,总比服务崩了好,这样更加稳妥。


或者至少和下游勾兑一下,问问我们这边吞吐量上升,你们扛得住不。


有的小伙伴看到这里可能就会产生一个疑问了:歪师傅,你这个讲得怎么和我背的八股文不一样啊?


巧了,你背过的八股文我也背过,现在我们来温习一下我们背过的八股文。


什么时候使用线程池呢?


比如一个请求要经过若干个服务获取数据,且这些数据没有先后依赖,最终需要把这些数据组合起来,一并返回,这样经典的场景:



用户点商品详情,你要等半天才展示给用户,那用户肯定骂骂咧咧的久走了。


这个时候,八股文上是怎么说的:用线程池来把串行的动作改成并行。



这个场景也是增加了服务 A 的吞吐量,但是用线程池就是非常正确的,没有任何毛病。


但是你想想,我们最开始的这个案例,是这个场景吗?



我们最开始的案例是想要在业务逻辑中增加一个线程池,对着一个下游服务就是一顿猛攻,不是所谓的串行改并行,而是用更多的线程,带来更多的串行。


这已经不是一个概念了。


还有一种场景下,使用线程池也是合理的。


比如你有一个定时任务,要从数据库中捞出状态为初始化的数据,然后去调用另外一个服务的接口查询数据的最终状态。



如果你的业务代码是这样的:


//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //捕获异常以免一条数据错误导致循环结束
    try{
        //发起rpc调用
        String orderStatus = queryOrderStatus(orderInfo.getOrderId);
        //更新订单状态
        updateOrderInfo(orderInfo.getOrderId,orderStatus);  
    } catch (Exception e){
        //打印异常
    }
}

虽然你框架中使用了线程池,但是你就是在一个 for 循环中不停的去调用下游服务查询数据状态,是一条数据一条数据的进行处理,所以其实同一时间,只是使用了框架的线程池中的一个线程。


为了更加快速的处理完这批数据,这个时候,你就可以怼一个线程池放在 for 循环里面了:


//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //使用线程池
    executor.execute(() -> {
        //捕获异常以免一条数据错误导致循环结束
        try {
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId, orderStatus);
        } catch (Exception e) {
            //打印异常
        }
    });
}


需要注意的是,这个线程池的参数怎么去合理的设置,是需要考虑的事情。


同时这个线程池的定位,就类似于 web 容器线程池的定位。


或者这样对比起来看更加清晰一点:



定时任务触发的时候,在发起远程接口调用之前,没有线程池,所以我们可以启用一个线程池来加快数据的处理。


而 Http 调用或者 RPC 调用,框架中本来就已经有一个线程池了,而且也给你提供了对应的性能调优参数配置,那么首先考虑的应该是把这个线程池充分利用起来。


如果仅仅是因为异步化之后可以提升服务响应速度,没有达到串行改并行的效果,那么我更加建议使用消息队列。


好了,本文的技术部分就到这里啦。


下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。


荒腔走板



不知道你看完文章之后,有没有产生一个小疑问:最开始部分的 Demo 似乎用处并不大?


是的,我最开始构思的行文结构是是基于 Demo 在源码中找到关于线程池的部分,从而引出其实有一些我们“看不见的线程池”的存在的。


原本周六我是有一整天的时间来写这篇文章,甚至周五晚上还特意把 Demo 搞定,自己调试了一番,该打的断点全部打上,并写完 Demo 那部分之后,我才去睡觉的,想得是第二天早上起来直接就能用。


按照惯例周六睡个懒觉的,早上 11 点才起床,自己慢条斯理的做了一顿午饭,吃完饭已经是下午 1 点多了。


本来想着在沙发上躺一会,结果一躺就是一整个下午。期间也想过起来写一会文章,坐在电脑前又飞快的躺回到沙发上,就是觉得这个事情索然无味,当下的那一刻就想躺着,然后无意识的刷手机,原本是拿来写文章中关于源码的部分的时间就这样浪费了。


像极了高中时的我,周末带大量作业回家,准备来个悬梁刺股,弯道超车,结果变成了一睡一天,捏紧刹车。


高中的时候,时间浪费了是真的可惜。


现在,不一样了。


荒腔走板这张图片,就是我躺在沙发上的时候,别人问我在干什么时随手拍的一张。


我并不为躺了一下午没有干正事而感到惭愧,浪费了的时间,才是属于自己的时间。


很久以前我看到别人在做一些浪费时间的事情的时候,我心里可能会嘀咕几句,劝人惜时。


这两年我不会了,允许自己做自己,允许别人做别人。


作者:why技术
来源:juejin.cn/post/7297980721590272040
收起阅读 »

iframe嵌入页面实现免登录思路(以vue为例)

web
背景: 最近实现一个功能需要使用iframe嵌入其它系统内部的一个页面,但嵌入后出现一个问题,就是一打开这个页面就会自动跳转到登录页,原因是被嵌入系统没有登录(没有token)肯定不让访问内部页面的,本文就是解决这个问题的。 附带相关文章:只要用iframe必...
继续阅读 »

背景:


最近实现一个功能需要使用iframe嵌入其它系统内部的一个页面,但嵌入后出现一个问题,就是一打开这个页面就会自动跳转到登录页,原因是被嵌入系统没有登录(没有token)肯定不让访问内部页面的,本文就是解决这个问题的。


附带相关文章:只要用iframe必遇到这6种"坑"之一(以Vue为例)


选择的技术方案:


本地系统使用iframe嵌入某个系统内部页面,那就证明被嵌入系统是安全的可使用的,所以可以通过通讯方式带一个token过去实现免登录,我用vue项目作为例子具体如下:


方法一通过url传:


// 发送方(本地系统):
<div>
<iframe :src="url" id="childFrame" importance="high" name="demo" ></iframe>
</div>

//被嵌入页面进行接收
url = `http://localhost:8080/dudu?mytoken={mytoken}` //

接收方:直接使用window.location.search接收,然后对接收到的进行处理


注意:



  • 如果使用这个方法最好把token加密一下,要不然直接显示在url是非常危险的行为,所以我更推荐下面方法二

  • 上面接收方要在在APP.vue文件的created生命周期接收,在嵌入页面接收是不行的,这里与VUE的执行流程有关就不多说了


方法二通过iframe的通讯方式传(推荐):

// 发送方(本地系统):
var params = {
type: "setToken",
token: "这是伟过去的token"
}
window.parent.postMessage(params, "*");



// 接收方(被嵌入系统):在APP.vue文件的created生命周期接收
window.addEventListener( "message",
(e)=>{
if(e.data.type === 'setToken'){
//这里拿到token,然后放入缓存实在免登录即可
}
}
false);


注意: 上面接收方要在在APP.vue文件的created生命周期接收,在嵌入页面接收是不行的,这里与VUE的执行流程有关就不多说了


补充:



看着评论不少疑问,所以我就按我个人的思路去补充回答一下,但不绝对实用,欢迎互相指导



(1)如果不同源系统怎么办?


正常使用上述方法二进行通迅,但不带token过去因为不同源根本无法通用,直接在被嵌入页面请求token,这个要和后端沟通好怎么获取


// 接收方(被嵌入系统):在APP.vue文件的created生命周期接收
window.addEventListener( "message",
(e)=>{
if(e.data.type === 'setToken'){
//这里在被嵌入页面请求接口获取这个系统的token,然后放到缓存中免登录
}
}
false);

(2)如果两个系统保存token字段相当怎么办?


例如:主系统本地存储的token叫:access_token , iframe嵌入的系统采用的token也叫:access_token



这分为两种情况:(1)同源并且token字段相同 (2)不同源并且token字段相当



(1)同源并且token字段相同

这种情况同源+token字段相同,根本不会出现需要登录的情况,因为同一个浏览器缓存都能拿到并且又是通用token


(2)不同源并且token字段相当

这种情况只有嵌入系统本地系统两种情况它们并不会同时出现的,那么只要判断当前是那个情况就行,然后给对应的token


方案:请求在拦截器那里判断当前请求来自那个系统的页面,然后给对应的token


例如:两个系统都要传my_token字段给后端,如果都放缓存就会覆盖,所以直接本地系统放到token1缓存,嵌入系统放到token2缓存,拦截器判断后如果本来系统页面 my_token=token1,嵌入页面 my_token=token2


作者:天天鸭
来源:juejin.cn/post/7350876924393209894
收起阅读 »

啊,富文本没做安全处理被XSS攻击了啊

web
前言 相信很多前端小伙伴项目中都用到了富文本,但你们有没有做防XSS攻击处理?最近的项目由于比较紧急我也没有处理而是直接正常使用,但公司内部有专门的安全部门针对测试,然后测出来富文本被XSS攻击了,而且危险级别为高。 啊这....,那我就去解决一下吧,顺便从X...
继续阅读 »

前言


相信很多前端小伙伴项目中都用到了富文本,但你们有没有做防XSS攻击处理?最近的项目由于比较紧急我也没有处理而是直接正常使用,但公司内部有专门的安全部门针对测试,然后测出来富文本被XSS攻击了,而且危险级别为高。


啊这....,那我就去解决一下吧,顺便从XSS和解决方案两个角度记录到下来毕竟好久没更新文章了。


先说说什么是XSS攻击?


简述XSS全称Cross-Site Scripting也叫跨站脚本攻击,是最最最常见的网络安全漏洞,其实就是攻击者在受害者的浏览器中注入恶意脚本执行。这种攻击通常发生在 Web 应用程序未能正确过滤用户输入的情况下,导致恶意脚本被嵌入到合法的网页中。
执行后会产生窃取信息、篡改网页、和传播病毒与木马等危害,后果相当严重。


XSS又有三大类


1、存储型 XSS即Stored XSS


恶意的脚本被放置在目标服务器上面,通过正常的网页请求返回给用户端执行。


例如 在观看某个私人博客评论中插入恶意脚本,当其他用户访问该页面时,脚本会执行危险操作。


2、反射型 XSS即Reflected XSS


恶意的脚本通过 URL 参数或一些输入的字段传递给目标的服务器,用户在正常请求时会返回并且执行。


例如 通过链接中的参数后面注入脚本,当用户点击此链接时,脚本就会在用户的浏览器中执行危险操作。


3、DOM 基于的 XSS即DOM-based XSS


恶意的脚本利用 DOM(Document Object Model)操作来修改页面内容。
这种类型的 XSS 攻击不涉及服务器端的代码操作,仅仅是通过客户端插入 JavaScript 代码实现操作。


富文本就是属于第一种,把脚本藏在代码中存到数据库,然后用户获取时会执行。


富文本防XSS的方式?


网上一大堆不明不白的方法还有各种插件可以用,但其实自己转义一下就行,根本不需要复杂化。


当我们不做处理时传给后台的富文本数据是这样的。


image.png
上面带有标签,甚至有srcscript之类的操作,在里面放一些脚本真的太简单了。


因此,我们创建富文本成功提交给后台的时候把各种<>/\之类危险符号转义成指定的字符就能防止脚本了。


如下所示,方法参数value就是要传递给后台的富文本内容。


  export const getXssFilter = (value: string): string => {
// 定义一个对象来存储特殊字符及其对应的 HTML 实体
const htmlEntities = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;',
'\\': '&#92;',
'|': '&#124;',
';': '&#59;',
'$': '&#36;',
'%': '&#37;',
'@': '&#64;',
'(': '&#40;',
')': '&#41;',
'+': '&#43;',
'\r': '&#13;',
'\n': '&#10;',
',': '&#44;',
};

// 使用正则表达式替换所有特殊字符
let result = value.replace(/[&<>"'\\|;$%@()+,]/g, function (match) {
return htmlEntities[match] || match;
});

return result;
};


此时传给后台的富文本参数是这样的,把敏感符号全部转义。


image.png


但展现给用户看肯定要看正常的内容啊,这里就要把内容重新还原了,这步操作可以在前端完成,也可以在后端完成。


如果是前端完成可以用以下方法把获取到的数据进行转义。


// 还原特殊字符
export const setXssFilter = (input) => {
return input
.replace(/&#124;/g, '|')
.replace(/&amp;/g, '&')
.replace(/&#59;/g, ';')
.replace(/&#36;/g, '$')
.replace(/&#37;/g, '%')
.replace(/&#64;/g, '@')
.replace(/&#39;/g, '\'')
.replace(/&quot;/g, '"')
.replace(/&#92;/g, '\\')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&#40;/g, '(')
.replace(/&#41;/g, ')')
.replace(/&#43;/g, '+')
.replace(/&#13;/g, '\r')
.replace(/&#10;/g, '\n')
.replace(/&#44;/g, ',');
}

但是。。。。


上面只适合使用于纯富文本的场景,如果在普通文本的地方回显会依然触发危险脚本。如下所示


image.png


其实直接转义后不还原即可解决,但由于是富文本这种情况比较特殊情况,不还原就失去文本样式了,怎么办??


最终解决方案是对部分可能造成XSS攻击的特殊字符和标签进行转义处理,例如:script、iframe等。


示例代码


  export const getXssFilter = (value: string): string => {
// 定义一个对象来存储特殊字符及其对应的 HTML 实体
const htmlEntities = {
'&': '&amp;',
'\'': '&#39;',
'\r': '&#13;',
'\n': '&#10;',
'script': '&#115;&#99;&#114;&#105;&#112;&#116;',
'iframe': '&#105;&#102;&#114;&#97;&#109;&#101;',
// 'img': '&#105;&#109;&#103;',
'object': '&#111;&#106;&#115;&#116;',
'embed': '&#101;&#109;&#98;&#101;&#100;',
'on': '&#111;&#110;',
'javascript': '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;',
'expression': '&#101;&#120;&#112;&#114;&#101;&#115;&#115;&#115;&#105;&#111;&#110;',
'video': '&#118;&#105;&#100;&#101;&#111;',
'audio': '&#97;&#117;&#100;&#105;&#111;',
'svg': '&#115;&#118;&#103;',
'background-image': '&#98;&#97;&#99;&#107;&#103;&#114;&#111;&#117;&#110;&#100;-&#105;&#109;&#97;&#103;&#101;',
};

// 使用正则表达式替换所有特殊字符
let result = value.replace(/[&<>"'\\|;$%@()+,]/g, function (match) {
return htmlEntities[match] || match;
});

// 额外处理 `script`、`iframe`、`img` 等关键词
result = result.replace(/script|iframe|object|embed|on|javascript|expression|background-image/gi, function (match) {
return htmlEntities[match] || match;
});

return result;
};

效果只会对敏感部分转义


image.png
但这种方案不用还原转义,因为做的针对性限制。


小结


其实就是对特殊符号转换后还原的思路,相当的简单。


如果那里写的不好或者有更好的建议,欢迎大佬指点啦。


作者:天天鸭
来源:juejin.cn/post/7415911762128404480
收起阅读 »

大厂必问 · 如何防止订单重复?

在电商系统或任何涉及订单操作的场景中,用户多次点击“提交订单”按钮可能会导致重复订单提交,造成数据冗余和业务逻辑错误,导致库存问题、用户体验下降或财务上的错误。因此,防止订单重复提交是一个常见需求。 常见的重复提交场景 网络延迟:用户在提交订单后未收到确认,...
继续阅读 »

在电商系统或任何涉及订单操作的场景中,用户多次点击“提交订单”按钮可能会导致重复订单提交,造成数据冗余和业务逻辑错误,导致库存问题、用户体验下降或财务上的错误。因此,防止订单重复提交是一个常见需求。


常见的重复提交场景



  1. 网络延迟:用户在提交订单后未收到确认,误以为订单未提交成功,连续点击提交按钮。

  2. 页面刷新:用户在提交订单后刷新页面,触发订单的重复提交。

  3. 用户误操作:用户无意中点击多次订单提交按钮。


防止重复提交的需求



  1. 幂等性保证:确保相同的请求多次提交只能被处理一次,最终结果是唯一的。

  2. 用户体验保障:避免由于重复提交导致用户感知的延迟或错误。


常用解决方案


前端防重机制:在前端按钮点击时禁用按钮或加锁,防止用户多次点击。


后端幂等处理



  • 利用Token机制:在订单生成前生成一个唯一的Token,保证每个订单提交时只允许携带一次Token。

  • 基于数据库的唯一索引:通过对订单字段(如订单号、用户ID)创建唯一索引来防止重复数据的插入。

  • 分布式锁:使用Redis等分布式缓存加锁,保证同一时间只允许处理一个订单请求。


功能实践


Spring Boot 提供了丰富的工具和库,今天我们基于Spring Boot框架,可以利用 Token机制Redis分布式锁 来防止订单的重复提交。


功能原理与技术实现


通过Redis的原子性操作,我们可以确保高并发情况下多个请求对同一个订单的操作不会冲突。


请在此添加图片描述


Token机制


Token机制是一种常见的防止重复提交的手段,通常的工作流程如下:



  1. Token生成:在用户开始提交订单时,服务器生成一个唯一的 OrderToken 并将其存储在 Redis 等缓存中,同时返回给客户端。

  2. Token验证:用户提交订单时,客户端会将 OrderToken 发送回服务器。服务器会验证此 OrderToken 是否有效。

  3. Token销毁:一旦验证通过,服务器会立即销毁 OrderToken,防止重复使用同一个Token提交订单。


这种机制确保每次提交订单时都需要一个有效且唯一的Token,从而有效防止重复提交。


Redis分布式锁


在多实例的分布式环境中,Token机制可以借助 Redis 来实现更高效的分布式锁:



  1. Token存储:生成的Token可以存储在Redis中,Token的存活时间通过设置TTL(如10分钟),保证Token在一定时间内有效。

  2. Token校验与删除:当用户提交订单时,服务器通过Redis查询该Token是否存在,并立即删除该Token,确保同一个订单只能提交一次。


流程设计



  1. 用户发起订单请求时,后端生成一个唯一的Token(例如UUID),并将其存储在Redis中,同时将该Token返回给前端。

  2. 前端提交订单时,将Token携带至后端。

  3. 后端校验该Token是否有效,若有效则执行订单创建流程,同时删除Redis中的该Token,确保该Token只能使用一次。

  4. 如果该Token已被使用或过期,则返回错误信息,提示用户不要重复提交。


功能实现


依赖配置(pom.xml)


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

application. properties


# Thymeleaf ??
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=false

spring.redis.host=127.0.0.1
spring.redis.port=23456
spring.redis.password=pwd

订单Token生成服务


生成Token并存储到Redis: 当用户请求生成订单页面时,服务器生成一个唯一的UUID作为订单Token,并将其与用户ID一起存储在Redis中。


package com.example.demo.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class OrderTokenService {

@Autowired
private RedisTemplate<String, String> redisTemplate;
// 生成订单Token
public String generateOrderToken(String userId) {
String token = UUID.randomUUID().toString();
// 将Token存储在Redis中,设置有效期10分钟
redisTemplate.opsForValue().set("orderToken:" + userId, token, 10, TimeUnit.MINUTES);
return token;
}
// 验证订单Token
public boolean validateOrderToken(String userId, String token) {
String redisToken = redisTemplate.opsForValue().get("orderToken:" + userId);
log.info("@@ 打印Redis中记录的redisToken :{} `VS` @@ 打印当前请求过来的token :{}", redisToken, token);
if (token.equals(redisToken)) {
// 验证成功,删除Token
redisTemplate.delete("orderToken:" + userId);
return true;
}
return false;
}
}


订单控制器


订单提交与验证Token: 提交订单时,系统会检查用户传递的Token是否有效,若有效则允许提交并删除该Token,确保同一Token只能提交一次。


package com.example.demo.controller;

import com.example.demo.entity.Order;
import com.example.demo.service.OrderTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private OrderTokenService orderTokenService;
// 获取订单提交的Token
@GetMapping("/getOrderToken")
public ResponseEntity<String> getOrderToken(@RequestParam String userId) {
String token = orderTokenService.generateOrderToken(userId);
return ResponseEntity.ok(token);
}
// 提交订单
@PostMapping("/submitOrder")
public ResponseEntity<String> submitOrder(Order order) {
// 校验Token
if (!orderTokenService.validateOrderToken(order.getUserId(), order.getOrderToken())) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("订单重复提交,请勿重复操作");
}

// 此处处理订单逻辑
// ...

// 假设订单提交成功
return ResponseEntity.ok("订单提交成功");
}
}


前端实现


前端通过表单提交订单,并在每次提交前从服务器获取唯一的订单Token:



<script>

document.getElementById('orderForm').addEventListener('submit', function(event) {
event.preventDefault();

const userId = document.getElementById('userId').value;
if (!userId) {
alert("请填写用户ID");
return;
}

// 先获取Token,再提交订单
fetch(`/order/getOrderToken?userId=${userId}`)
.then(response => response.text())
.then(token => {
document.getElementById('orderToken').value = token;

// 提交订单请求
const formData = new FormData(document.getElementById('orderForm'));
fetch('/order/submitOrder', {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(result => {
document.getElementById('message').textContent = result;
})
.catch(error => {
document.getElementById('message').textContent = '订单提交失败,请重试';
});
})
.catch(error => {
document.getElementById('message').textContent = '获取Token失败';
});
});

</script>


为了验证功能,我们在代码中增加 Thread.sleep(2000); 来进行阻塞。


请在此添加图片描述


然后快速点击提交表单,可以看到提示表单重复提价的信息


请在此添加图片描述


**技术选型与优化:**通过Redis结合Token机制,我们有效地防止了订单的重复提交,并通过Token的唯一性和时效性保证了订单操作的幂等性。



  • Redis缓存:通过Redis的分布式锁和高并发处理能力,确保系统在高并发情况下仍然可以正常运行,并发订单提交的场景中不会出现Token重复使用问题。

  • UUID:使用UUID生成唯一的Token,保证Token的唯一性和安全性。

  • Token时效性:Token通过设置Redis的TTL(过期时间)来控制有效期,避免无效Token长期占用资源。


总结


防止订单重复提交的关键在于:



  1. Token的唯一性与时效性:确保每次订单提交前都有唯一且有效的Token。

  2. Token的原子性验证与删除:在验证Token的同时删除它,防止同一个Token被多次使用。

  3. Redis的高效存储与分布式锁:通过Redis在高并发环境中提供稳定的锁机制,保证并发提交的准确性。


这套基于Token机制和Redis的解决方案具有简单、高效、可扩展的特点,适合各种高并发场景下防止重复订单提交。


作者:不惑_
来源:juejin.cn/post/7418776600738840628
收起阅读 »

现在前端组长都是这样做 Code Review

web
前言 Code Review 是什么? Code Review 通常也简称 CR,中文意思就是 代码审查 一般来说 CR只关心代码规范和代码逻辑,不关心业务 但是,如果CR的人是组长,建议有时间还是看下与自己组内相关业务,能避免一些生产事故的发生 作为前端组长...
继续阅读 »

前言


Code Review 是什么?


Code Review 通常也简称 CR,中文意思就是 代码审查


一般来说 CR只关心代码规范和代码逻辑,不关心业务


但是,如果CR的人是组长,建议有时间还是看下与自己组内相关业务,能避免一些生产事故的发生


作为前端组长做 Code Review 有必要吗?


主要还是看公司业务情况吧,如果前端组长需求不多的情况,是可以做下CR,能避免一些生产事故



  • 锻炼自己的 CR 能力

  • 看看别人的代码哪方面写的更好,学习总结

  • 和同事交流,加深联系

  • 你做了 CR,晋升和面试,不就有东西吹了不是


那要怎么去做Code Review呢?


可以从几个方面入手



  • 项目架构规范

  • 代码编写规范

  • 代码逻辑、代码优化

  • 业务需求


具体要怎么做呢?


传统的做法是PR时查看,对于不合理的地方,打回并在PR中备注原因或优化方案


每隔一段时间,和组员开一个简短的CR分享会,把一些平时CR过程中遇到的问题做下总结


当然,不要直接指出是谁写出的代码有问题,毕竟这不是目的,分享会的目的是交流学习


人工CR需要很大的时间精力,与心智负担


随着 AI 的发展,我们可以借助一些 AI 来帮我们完成CR


接下来,我们来看下,vscode中是怎么借助 AI 工具来 CR


安装插件 CodeGeex
image-20240723191918678.png


新建一个项目


mkdir code-review
cd code-review

创建 test.js 并用 vscode 打开


cd .>test.js
code ./

image-20240723192853589.png


编写下 test.js


function checkStatus() {
if (isLogin()) {
if (isVip()) {
if (isDoubleCheck()) {
done();
} else {
throw new Error("不要重复点击");
}
} else {
throw new Error("不是会员");
}
} else {
throw new Error("未登录");
}
}

这是连续嵌套的判断逻辑,要怎么优化呢?


侧边栏选择这个 AI 插件,选择我们需要CR的代码


输入 codeRiview,回车


动画.gif


我们来看下 AI 给出的建议


image-20240723194729540.png


AI 给出的建议还是很不错的,我们可以通过更多的提示词,优化它给出的修改建议,这里就不过多赘述了


通常我们优化这种类型的代码,基本优化思路也是,前置校验逻辑,正常逻辑后置


除了CodeGeex外,还有一些比较专业的 codeRiview 的 AI 工具


比如:CodeRabbit


那既然都有 AI 工具了,我们还需要自己去CR 吗?


还是有必要的,借助 AI 工具我们可以减少一些阅读大量代码环节,提高效率,减少 CR 的时间


但是仍然需要我们根据 AI 工具的建议进行改进,并且总结,有利于拓宽我们见识,从而写出更优质的代码


具体 CR 实践


判断逻辑优化


1. 深层对象判空


// 深层对象
if (
store.getters &&
store.getters.userInfo &&
store.getters.userInfo.menus
) {}

// 可以使用 可选链进行优化
if (store?.getters?.userInfo?.menus) {}

2. 空函数判断


优化之前


props.onChange && props.onChange(e)

支持 ES11 可选链写法,可这样优化,js 中需要这样,ts 因为有属性校验,可以不需要判断,当然也特殊情况


props?.onChange?.(e)

老项目,不支持 ES11 可以这样写


const NOOP = () => 8
const { onChange = NOOP } = props
onChange(e)

3. 复杂判断逻辑抽离成单独函数


// 复杂判断逻辑
function checkGameStatus() {
if (remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0) {
quitGame()
}
}

// 复杂判断逻辑抽离成单独函数,更方便阅读
function isGameOver() {
return (
remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0
);
}

function checkGameStatus() {
if (isGameOver()) {
quitGame();
}
}

4. 判断处理逻辑正确的梳理方式


// 判断逻辑不要嵌套太深
function checkStatus() {
if (isLogin()) {
if (isVip()) {
if (isDoubleCheck()) {
done();
} else {
throw new Error('不要重复点击');
}
} else {
throw new Error('不是会员');
}
} else {
throw new Error('未登录');
}
}

这个是不是很熟悉呀~


没错,这就是使用 AI 工具 CR的代码片段


通常这种,为了处理特殊状况,所实现的判断逻辑,都可以采用 “异常逻辑前置,正常逻辑后置” 的方式进行梳理优化


// 将判断逻辑的异常逻辑提前,将正常逻辑后置
function checkStatus() {
if (!isLogin()) {
throw new Error('未登录');
}

if (!isVip()) {
throw new Error('不是会员');
}

if (!isDoubleCheck()) {
throw new Error('不要重复点击');
}

done();
}

函数传参优化


// 形参有非常多个
const getMyInfo = (
name,
age,
gender,
address,
phone,
email,
) => {
// ...
}

有时,形参有非常多个,这会造成什么问题呢?



  • 传实参是的时候,不仅需要知道传入参数的个数,还得知道传入顺序

  • 有些参数非必传,还要注意添加默认值,且编写的时候只能从形参的后面添加,很不方便

  • 所以啊,那么多的形参,会有很大的心智负担


怎么优化呢?


// 行参封装成对象,对象函数内部解构
const getMyInfo = (options) => {
const { name, age, gender, address, phone, email } = options;
// ...
}

getMyInfo(
{
name: '张三',
age: 18,
gender: '男',
address: '北京',
phone: '123456789',
email: '123456789@qq.com'
}
)

你看这样是不是就清爽了很多了


命名注释优化


1. 避免魔法数字


// 魔法数字
if (state === 1 || state === 2) {
// ...
} else if (state === 3) {
// ...
}

咋一看,这 1、2、3 又是什么意思啊?这是判断啥的?


语义就很不明确,当然,你也可以在旁边写注释


更优雅的做法是,将魔法数字改用常量


这样,其他人一看到常量名大概就知道,判断的是啥了


// 魔法数字改用常量
const UNPUBLISHED = 1;
const PUBLISHED = 2;
const DELETED = 3;

if (state === UNPUBLISHED || state === PUBLISHED) {
// ...
} else if (state === DELETED) {
// ...
}

2. 注释别写只表面意思


注释的作用:提供代码没有提供的额外信息


// 无效注释
let id = 1 // id 赋值为 1

// 有效注释,写业务逻辑 what & why
let id = 1 // 赋值文章 id 为 1

3. 合理利用命名空间缩短属性前缀


// 过长命名前缀
class User {
userName;
userAge;
userPwd;

userLogin() { };
userRegister() { };
}

如果我们把前面的类里面,变量名、函数名前面的 user 去掉


似乎,也一样能理解变量和函数名称所代表的意思


代码却,清爽了不少


// 利用命名空间缩短属性前缀
class User {
name;
age;
pwd;

login() {};
register() {};
}

分支逻辑优化


什么是分支逻辑呢?


使用 if else、switch case ...,这些都是分支逻辑


// switch case
const statusMap = (status: string) => {
switch(status) {
case 'success':
return 'SuccessFully'
case 'fail':
return 'failed'
case 'danger'
return 'dangerous'
case 'info'
return 'information'
case 'text'
return 'texts'
default:
return status
}
}

// if else
const statusMap = (status: string) => {
if(status === 'success') return 'SuccessFully'
else if (status === 'fail') return 'failed'
else if (status === 'danger') return 'dangerous'
else if (status === 'info') return 'information'
else if (status === 'text') return 'texts'
else return status
}

这些处理逻辑,我们可以采用 映射代替分支逻辑


// 使用映射进行优化
const STATUS_MAP = {
'success': 'Successfull',
'fail': 'failed',
'warn': 'warning',
'danger': 'dangerous',
'info': 'information',
'text': 'texts'
}

return STATUS_MAP[status] ?? status

【扩展】


??TypeScript 中的 “空值合并操作符”


当前面的值为 null 或者 undefined 时,取后面的值


对象赋值优化


// 多个对像属性赋值
const setStyle = () => {
content.body.head_style.style.color = 'red'
content.body.head_style.style.background = 'yellow'
content.body.head_style.style.width = '100px'
content.body.head_style.style.height = '300px'
// ...
}

这样一个个赋值太麻烦了,全部放一起赋值不就行了


可能,有些同学就这样写


const setStyle = () => {
content.body.head_style.style = {
color: 'red',
background: 'yellow',
width: '100px',
height: '300px'
}
}

咋一看,好像没问题了呀?那 style 要是有其他属性呢,其他属性不就直接没了吗~


const setStyle = () => {
content.body.head_style.style = {
...content.body.head_style.style
color: 'red',
background: 'yellow',
width: '100px',
height: '300px'
}
}

采用展开运算符,将原属性插入,然后从后面覆盖新属性,这样原属性就不会丢了


隐式耦合优化


// 隐式耦合
function responseInterceptor(response) {
const token = response.headers.get("authorization");
if (token) {
localStorage.setItem('token', token);
}
}

function requestInterceptor(response) {
const token = localStorage.getItem('token');
if (token) {
response.headers.set("authorization", token);
}
}

这个上面两个函数有耦合的地方,但是不太明显


比如这样的情况,有一天,我不想在 responseInterceptor 函数中保存 tokenlocalStorage


function responseInterceptor(response) {
const token = response.headers.get("authorization");
}

function requestInterceptor(response) {
const token = localStorage.getItem('token');
if (token) {
response.headers.set("authorization", token);
}
}

会发生什么?


localStorage.getItem('token')一直拿不到数据,requestInterceptor 这个函数就报废了,没用了


函数 responseInterceptor改动,影响到函数 requestInterceptor 了,隐式耦合了


怎么优化呢?


// 将隐式耦合的常数抽离成常量
const TOKEN_KEY = "authorization";
const TOKEN = 'token';

function responseInterceptor(response) {
const token = response.headers.get(TOKEN_KEY);
if (token) {
localStorage.setItem(TOKEN_KEY, token);
}
}

function requestInterceptor(response) {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
response.headers.set(TOKEN_KEY, token);
}
}

这样做有什么好处呢?比刚才好在哪里?


还是刚才的例子,我去掉了保存 localStorage.setItem(TOKEN_KEY, token)


我可以根据TOKEN_KEY这个常量来查找还有哪些地方用到了这个 TOKEN_KEY,从而进行修改,就不会出现冗余,或错误


不对啊,那我不用常量,用token也可以查找啊,但你想想 token 这个词是不是得全局查找,其他地方也会出现token


查找起来比较费时间,有时可能还会改错了


用常量的话,全局查找出现重复的概率很小


而且如果你是用 ts 的话,window 下鼠标停在常量上,按 ALT 键就能看到使用到这个常量的地方了,非常方便


小结


codeRiview(代码审查)不仅对个人技能的成长有帮助,也对我们在升职加薪、面试有所裨益


CR 除了传统的方式外,也可以借助 AI 工具,来简化其中流程,提高效率


上述的优化案例,虽然优化方式不同,但是核心思想都是一样,都是为了代码 更简洁、更容易理解、更容易维护


当然了,优化方式还有很多,如果后期遇到了也会继续补充进来


作者:大麦大麦
来源:juejin.cn/post/7394792228215128098
收起阅读 »

简单的 Web 端实时日志实现

web
背景 cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。 方案如何选择? 我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTT...
继续阅读 »

Feb-20-2024 21-40-36.gif


背景


cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。


方案如何选择?


我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTTP/SSE 。WebSocket 在很多情况下是实至名归的万金油选择。但这这里他太重了,对服务端和客户端具有侵入性需要花费额外的时间来集成到项目中。


那么 SSE 呢,为什么不是他?
虽然 SSE 即轻量又实时,但 SSE 无法设置请求头。这导致无法使用缓存和通过请求头设置的 Token。而且作为长链接他还一直占用并发额度。



  • WebSocket:❌

    • 优势:

      • 实时性高

      • 不会占用 HTTP 并发额度



    • 劣势:

      • 复杂度较高,需要在客户端和服务器端都进行特殊的处理

      • 消耗更多的服务器资源。





  • SSE(Server-Sent Events):❌

    • 优势:

      • 基于HTTP协议,不需要在服务端和客户端做额外的处理

      • 实时性高



    • 劣势:

      • 无法设置请求头

      • 占用 HTTP 并发额度





  • HTTP:✅

    • 优势:

      • 简单易用,不需要在服务端和客户端做额外的处理。

      • 支持的功能丰富,如缓存,压缩,认证等功能。



    • 劣势:

      • 实时性差,取决于轮询时间间隔。

      • 每次HTTP请求都需要建立新的连接(仅针对 HTTP/0.x 而言)并可能阻塞同源的其他请求而导致性能问题。12

        • HTTP/1.x 支持持久连接以避免每次请求都重新建立 TCP 连接,但数据通信是串行的。

        • HTTP/2.x 支持持久连接且支持并行的数据通信。









以上列出的优缺点是仅对于本文所讨论的场景(即Web 端的实时日志)而言,这三种数据交互技术在其他场景中的优缺点不在本文讨论范围内。



实现


HTTP 轮询已经是老熟人了,不再做介绍。本文的着重于实时日志的实现及优化。


sequenceDiagram

participant 浏览器

participant 服务器

Note right of 浏览器: 首先,获取日志文件最后 X bytes 的内容

浏览器->>服务器: 最新的日志文件有多大?

服务器->>浏览器: 日志文件大小: Y bytes

浏览器->>服务器: 从 Y - X bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y1 bytes, 日志内容: XXXX



loop 持续轮询获取最新的日志

浏览器->>服务器: 从 Y1 bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y2 bytes, 日志内容: XXXX

end

上方是基本工作原理的流程图。
实现的关键点在于



  • 前端如何知道日志文件当前的大小

  • 服务端如何从指定位置获取日志文件内容


这两点都可以通过 HTTP Header 来解决。Content-Range HTTP 响应头会包含完整文件的大小,而 Range HTTP 请求头可以指示服务器返回指定位置的文件内容。


因此在服务端不需要额外的逻辑,仅通过 Web 端代码就可以实现实时日志功能。


代码实现



篇幅限制,代码不会处理异常情况



首先,根据上述流程图。我们需要获取日志文件的大小。


const res = await fetch(URL, {  
method: "GET",
headers: { Range: "bytes=0-0" }
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 日志文件大小
const total = Number.parseInt(match[3], 10);


Range: "bytes=0-0" 指定仅获取第一个字节的内容,以免较大的日志文件导致响应时间过长。



我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=0-0 如果服务器能够正确处理 Range 请求头,响应中将包含一个 Content-Range 列其中将包含日志文件的完整大小,可以通过正则解析拿到。


现在我们已经拿到了日志文件的大小并存储在名为 total 的变量中。然后根据 total 获取到最后 10 KB 的日志内容。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${total - 1000 * 10}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
const start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

现在我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=${total - 1000 * 10}- 以获取最后 10 KB 的日志内容。并且通过正则解析拿到了下一次请求的起始位置。


现在我们已经拿到了日志文件的大小和最后 10 KB 的日志内容。接下来就是持续轮询去获取最新的日志。轮询代码区别仅在于将 Range 标头设置为 bytes=${start}- 以便获取最新的日志。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${start}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

以上,基本的功能已经实现了。日志内容保存在名为 content 的变量中。


优化


HTTP 轮询常因其高延迟为人诟病,我们可以通过指数退避的方式来尽可能的降低延时且不会显著的增加服务器负担。


指数退避是一种常见的网络重试策略,它会在每次重试时将等待时间乘以一个固定的倍数。这样做的好处是,当网络出现问题时,重试的时间间隔会逐渐增加,直到达到最大值。


一个简单的实现如下:


/**
* 使用指数退避策略获取日志.
*/

export class ExponentialBackoff {
private readonly base: number;
private readonly max: number;
private readonly factor: number;
private retries: number;

/**
* @param base 基础延迟时间 默认 1000ms (1秒)
* @param max 最大延迟时间 默认 60000ms (1分钟)
* @param factor 延迟时间增长因子 默认 2
*/

constructor(base: number = 1000, max: number = 60000, factor: number = 2) {
this.base = base;
this.max = max;
this.factor = factor;
this.retries = 0;
}

/**
* 获取下一次重试的延迟时间.
*/

next() {
const delay = Math.min(this.base * Math.pow(this.factor, this.retries), this.max);
this.retries++;
return delay;
}

/**
* 重置重试次数.
*/

reset() {
this.retries = 0;
}
}

值得一提的是带有 Range 标头的请求成功时会返回 206 Partial Content 状态码。而在请求的范围超出文件大小时会返回 416 Range Not Satisfiable 状态码。我们可以通过这两个状态码来判断请求是否成功。


成功时调用 reset 方法重置重试次数,失败时调用 next 方法获取下一次重试的延迟时间。


总结


即使再优秀的方案也不是银弹,真正合适的方案需要考虑的不仅仅是技术本身,还有业务场景,团队技术栈,团队技术水平等等。在选择技术方案时,我们需要权衡各种因素,而不是盲目的选择最流行的技术。


当我们责备现有的技术方案为何如此糟糕时,或许在那个时间点上,这个方案是最合适的。


Pasted image 20240220213326.png


感谢以下网友的帮助和建议:齐洛格德


Footnotes




作者:siaikin
来源:juejin.cn/post/7337519776796295177
收起阅读 »

方寸之间窥万象——这样的Tooltip,你会开发吗?

web
序言 提示信息(tooltip)是一种常见的 GUI 元素。在可视化领域,tooltip 通常指用户将鼠标悬停在图元上或者图表区域时弹出的明细数据信息框。如果是桌面环境,通常会在用户将指针悬停在元素上而不单击它时显示 tooltip;如果是移动环境,通常会在长...
继续阅读 »

序言


提示信息(tooltip)是一种常见的 GUI 元素。在可视化领域,tooltip 通常指用户将鼠标悬停在图元上或者图表区域时弹出的明细数据信息框。如果是桌面环境,通常会在用户将指针悬停在元素上而不单击它时显示 tooltip;如果是移动环境,通常会在长按(即点击并按住)元素时显示 tooltip。


这样一个小小的组件,却可以十分有效地丰富图表的数据展现能力和图表交互效果,同时在实际业务领域的用途也非常广泛。


近些年来,业界主要图表库(如ECharts、G2等)都提供了 tooltip 的配置能力和默认渲染能力,以达到开箱即用的效果。VChart 更不例外,提供了更加灵活的 tooltip 展示与配置方案。


通过使用 VChart,你既可以显示图表中任何系列的图元所携带的数据信息(mark tooltip):



也可以显示某个特定维度项下的所有图元的数据信息(dimension tooltip):



乃至可以灵活地自定义 tooltip,甚至在其中插入子图表,拓展交互的边界:



本文将通过一些实战案例,详细讲述 VChart 提示信息的重点用法、自定义方式以及设计细节。


示例一:可触及的 tooltip,与 Amazon 的安全三角形


为了不对用户的鼠标交互进行干扰,VChart 的 tooltip 默认不会响应鼠标事件。但是在某些情况,用户却希望鼠标可以移到 tooltip 中进行一些额外的交互行为,比如点击 tooltip 中的按钮和链接,或者选取并复制一些数据。


为了满足这类需求,tooltip 支持在 spec 中配置 enterable 属性。如果不配置或者配置 enterable: false,默认效果是这样的,鼠标无法移到 tooltip 元素内:



而如果配置 enterable: true,效果如以下截图所示:



图表简化版 spec 为:


const spec = {
type: 'waterfall',
data: [], // 数据略
legends: { visible: true, orient: 'bottom' },
xField: 'x',
yField: 'y',
seriesField: 'type',
total: {
type: 'field',
tagField: 'total'
},
title: {
visible: true,
text: 'Chinese quarterly GDP in 2022'
},
tooltip: {
enterable: true // TOOLTIP SPEC
}
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

简单对比两个 tooltip 的效果,可以发现后者在鼠标靠近 tooltip 时,tooltip 便适时地停住了。


这个小小的交互细节里却有些文章,灵感来源直接来自 Amazon 的官网实现。


这个例子也许已经为人熟知,我们简单回顾一下。这首先要从普通网站的下拉菜单开始讲起:



在一个设计欠佳的菜单组件里(如 bootstrap),鼠标从一级菜单移入二级菜单往往是很困难的,很容易触发二级菜单的隐藏策略,从而变成一场无聊的打地鼠游戏。


但是 Amazon 早期官网的菜单,由于用户使用频率高,根本无法接受这样的体验。于是他们完美地解决了这个问题,并成为一个交互优化的经典案例。


其思路的核心,便是检测鼠标移动的方向。如果鼠标移动到下图的蓝色三角形中,当前显示的子菜单将继续打开一小会儿:



在鼠标的每个位置,你可以想象鼠标当前位置与下拉菜单的右上角和右下角之间形成一个三角形。如果下一个鼠标位置在该三角形中,说明用户可能正在将鼠标移向当前显示的子菜单。Amazon 利用这一点实现了很好的效果。只要鼠标停留在该蓝色三角形中,当前子菜单就会保持打开。如果鼠标移出该三角形,他们会立即切换子菜单,使其感觉非常敏捷。


整体效果类似于下图所示:



正所谓,上帝在细节中(God is in the details)。从这个交互优化里,我们看到的不仅是一个精妙的算法,而是一个科技巨头对于产品和用户体验的态度。Amazon 的数百亿市值有多少是从这些很小很小,但是明显很用心的产品细节中积累起来的呢?



VChart 的 tooltip 也一样,着重参考了 Amazon 的交互优化。如果配置 enterable: true,在每个时刻,都会存在一个这样的“安全三角形”,三个顶点分别是鼠标光标以及 tooltip 的两个端点,取面积最大的三角形:



如果鼠标在下一刻滑到这个三角形区域中, tooltip 便为鼠标“停留一会儿”,直到鼠标移到 tooltip 区域内。



但是在鼠标移到 tooltip 区域之前,tooltip 并不会永远停下来等待鼠标。如果鼠标过于缓慢地靠近 tooltip,tooltip 还是会离开的(变成一场失败、却又在意料之中的奔赴)。这样便可以同时保证用户鼠标有足够的行动自由度。以下示例特地将鼠标移动速度放慢,便可以实现既进入三角形区域,又不会被 tooltip “挡路”:



作为对比,ECharts 的 tooltip 虽然同样支持 enterable 属性,但是 ECharts 主要通过简单的 tooltip 缓动来支持鼠标移入,鼠标仍需要不停地“追逐” tooltip 才能移至其中,灵活性便打了折扣。以下为 ECharts 的效果:



示例二:灵活的 pattern,内容与样式的自由配置


为了尽最大可能满足更多业务方的需求,VChart 的 tooltip 支持比较灵活的内容和样式配置。下文将以官网 demo(http://www.visactor.io/vchart/demo…



在这个图表中,用户配置了一条 y=10000 的标注线。同时要求在 dimension tooltip 中实现:



  • 数据项从大到小排序;

  • 比标注线高的数据项标红(条件格式);

  • 在 tooltip 内容的最后一行加上标注线所代表的数据。


同时,这个 tooltip 的位置还拥有以下特征:



  • dimension tooltip 的位置固定在光标上方;

  • mark tooltip 的位置固定在数据项下方。


如以下动图所示:



这个示例实际上代表了很多不同类型的业务需求。下面拆解来看一下:


基本 tooltip 内容配置


首先,剥去自定义内容和样式的部分,这个图表的最简 spec 和基本 tooltip 配置如下:


const markLineValue = 10000;
const spec = {
type: 'line',
data: {
values: [
{ type: 'Nail polish', country: 'Africa', value: 4229 },
// 其他数据略
]
},
stack: false,
xField: 'type',
yField: 'value',
seriesField: 'country',
legends: [{ visible: true, position: 'middle', orient: 'bottom' }],
markLine: [
{
y: markLineValue,
endSymbol: { visible: false },
line: { style: { /* 样式配置略 */ }}
}
],
tooltip: { // TOOLTIP SPEC
mark: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
]
},
dimension: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
]
}
}
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

显示效果如下:



观察 spec 不难发现,mark tooltip 和 dimension tooltip 分别用回调方式配置了 tooltip 的显示内容。其中:



  • title.value 显示的是数据项中对应于 xField 的内容;

  • content.key 显示的是数据项中对应于 seriesField(也是区分出图例项的 field)的内容;

  • content.value 显示的是数据项中对应于 yField 的内容。


回调是 tooltip 内容的基本配置方式,用户可以在 title 和 content 中自由配置回调函数来实现数据绑定和字符串的格式化。


Tooltip 内容的排序、增删、条件格式


我们再来看一下 dimension tooltip 的 spec:


{
tooltip: { // TOOLTIP SPEC
mark: { /* ...略 */ },
dimension: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
]
}
}
}

不难发现,content 配置的数组只包含 1 个对象,但是上图显示出来却有 4 行内容。为什么呢?


其实在 dimension tooltip 中发生了和折线图元类似的 data join 过程:由于在数据中,seriesField 划分出了 4 个数据组(图例项有 4 个),因此在经过笛卡尔积后,真实 tooltip 内容行数为 content 数组成员数量乘以 4。在数据组装过程中,每个数据组都要依次走一遍 content 数组成员里的回调。


我们把 spec 中的 tooltip 内容配置称为 TooltipPattern,tooltip 所需数据称为 TooltipData,最终的 tooltip 结构称为 TooltipActual。数据组装过程可以表示为:


MakeTooltip(TooltipPattern,TooltipData) = TooltipActualMakeTooltip(TooltipPattern, TooltipData) = TooltipActual


在本例中,经过这个过程,TooltipPattern 中的回调在 TooltipActual 中消失(回调已被执行 4 次),且由 1 行变成了 4 行。


这个过程完整的执行流程如下:



那么回到示例中的用户需求,用户希望将 tooltip 内容行由大到小排序。那么这个步骤自然要在 TooltipActual 生成之后执行,也就是上图中的 “updateTooltipContent” 过程。


Tooltip spec 中支持配置 updateContent 回调来对 TooltipActual 的 content 部分进行操作。排序可以这样写:


{
tooltip: { // TOOLTIP SPEC
mark: { /* ...略 */ },
dimension: {
title: {
value: datum => datum.type
},
content: [
{
key: datum => datum.country,
value: datum => datum.value
}
],
updateContent: prev => {
// 排序
prev.sort((a, b) => b.value - a.value);
}
}
}
}

updateContent 回调的第一个参数为已经计算好的 TooltipActual。加上回调以后,排序生效:



在 tooltip 中实现条件格式以及新增一行也是一样的方法,可以直接在 updateContent 回调中处理:


{
updateContent: prev => {
// 排序
prev.sort((a, b) => b.value - a.value);
// 条件格式:比标注线高的数据项标红
prev.forEach(item => {
if (item.value >= markLineValue) {
item.valueStyle = {
fill: 'red'
};
}
});
// 新增一行
prev.push({
key: 'Mark Line',
value: markLineValue,
keyStyle: { fill: 'orange' },
valueStyle: { fill: 'orange' },
// 自定义 shape 的 svg path
shapeType: 'M44.3,22.1H25.6V3.3h18.8V22.1z M76.8,3.3H58v18.8h18.8V3.3z M99.8,3.3h-9.4v18.8h9.4V3.3z M12.9,3.3H3.5v18.8h9.4V3.3z',
shapeColor: 'orange',
hasShape: true
});
}
}

调试 spec,回调生效,最后效果如下:



Tooltip 样式和位置


VChart tooltip 支持将 tooltip 固定于某图元附近或者鼠标光标附近。在本例中,mark tooltip 固定于图元的下方,而 dimension tooltip 固定于鼠标光标的上方,可以这样配置:


{
tooltip: { // TOOLTIP SPEC
mark: {
// 其他配置略
position: 'bottom' // 显示在下方
positionAt: 'mark' // 固定在图元附近,由于这是默认值,这行可以删掉
},
dimension: {
// 其他配置略
position: 'top', // 显示在上方
positionAt: 'pointer' // 固定在鼠标光标附近
}
}
}

而样式配置可以在 tooltip spec 上的 style 配置项下进行自定义。style 支持配置 tooltip 组件各个组成部分的统一样式,详细配置项可参考官网文档(http://www.visactor.io/vchart/opti…


最后效果如下,完整 spec 可见官网 demo(http://www.visactor.io/vchart/demo…



示例三:锦上添花,可按需修改的 tooltip dom 树


VChart 的 tooltip 共支持两种渲染模式:



  • Dom 渲染,适用于桌面或移动端浏览器环境;

  • Canvas 渲染,适用于移动端小程序、node 环境等非浏览器环境。


对于 dom 版本的 tooltip,为了更好的支持业务方的自定义需求,VChart 开放了对 tooltip dom 树的修改接口。下文将以官网 demo(http://www.visactor.io/vchart/demo…



在这个示例中,用户要求在 tooltip 的底部增加一个超链接,用户点击链接后,便可以自动跳转到 Google,对 tooltip 标题进行进一步搜索。这个示例要求两个能力:



  • 示例一介绍的 enterable 能力,开启后鼠标会被允许滑入 tooltip 区域,这是在 tooltip 中实现交互的前提;

  • 在默认 tooltip 上绘制自定义的 dom 元素。


为了实现第二个能力,tooltip 支持了回调 updateElement,这个回调配在 tooltip spec 顶层。这个示例的 tooltip 配置如下:


{
tooltip: { // TOOLTIP SPEC
enterable: true,
updateElement: (el, actualTooltip, params) => {
// 自定义元素只加在 dimension tooltip 上
if (actualTooltip.activeType === 'dimension') {
const { changePositionOnly, dimensionInfo } = params;
// 判断本次 tooltip 显示是否仅改变了位置,如果是的话退出
if (changePositionOnly) { return; }
// 改变默认 tooltip dom 的宽高策略
el.style.width = 'auto';
el.style.height = 'auto';
el.style.minHeight = 'auto';
el.getElementsByClassName('value-box')[0].style.flex = '1';
for (const valueLabel of el.getElementsByClassName('value')) {
valueLabel.style.maxWidth = 'none';
}
// 删除上次执行回调添加的自定义元素
if (el.lastElementChild?.id === 'button-container') {
el.lastElementChild.remove();
}
// 添加新的自定义元素
const div = document.createElement('div');
div.id = 'button-container';
div.style.margin = '10px -10px 0px';
div.style.padding = '10px 0px 0px';
div.style.borderTop = '1px solid #cccccc';
div.style.textAlign = 'center';
div.innerHTML = `href="https://www.google.com/search?q=${dimensionInfo[0]?.value}"
style="text-decoration: none"
target="_blank"
>Search with Google
`;
el.appendChild(div);
} else {
// 对于 mark tooltip,删除上次执行回调添加的自定义元素
if (el.lastElementChild?.id === 'button-container') {
el.lastElementChild.remove();
}
}
}
}
}

updateElement在每次 tooltip 被激活或者更新时触发,在触发时,TooltipActual 已经计算完毕,且 dom 节点也已经准备好。回调的第一个参数便是本次将要显示的 tooltip dom 根节点。目前不支持替换该节点,只支持对该节点以及其孩子进行修改。


这个配置的设计最大限度地复用了 VChart tooltip 的内置逻辑,同时提供了足够自由的自定义功能。你可以随心所欲地定制 tooltip 显示内容,并且复用任何你没有覆盖的逻辑。


比如,你可以对 tooltip 的大小进行重新定义,而不用关心窗口边界的躲避策略是否会出问题。事实上 VChart 会自动把 tooltip 定位逻辑复用在修改过的 dom 上:



这个回调还可以进一步封装,比如在 react-vchart 中将用户侧的 react 组件插入 tooltip。目前这个封装主要由业务侧自主进行,后续 VChart 也有计划提供官方支持。


示例四:完全自定义,由业务托管 tooltip 渲染


若要更进一步,VChart tooltip 最高级别的自定义,便是让 VChart 完全将 tooltip 渲染交给用户。有以下两种方式可以选择:



  • 用户自定义 tooltip handler

  • 用户使默认 tooltip 失效,监听 tooltip 事件


再结合示例二、示例三的铺垫,便可以带出整个 tooltip 模块的设计架构。熟悉了架构便更容易了解每条渲染路径以及各个层级的关系。



由上图可见,示例三对应的是 “Custom DOM Render” 的自定义,示例二对应了 “Custom TooltipActual” 部分的自定义。而示例四,便是对应整个 “Tooltip Events” 以及 “Custom TooltipHandler” 的自定义。


由于上图中,“Tooltip Events” 和 “Custom TooltipHandler” 纵跨了多个层级,因此它覆盖的默认逻辑是最多的,体现在:



  • 当给图表设置了自定义 tooltip handler 后,内置的 tooltip 将不再起作用。

  • VChart 不感知、不托管自定义 tooltip 的渲染,需要自行实现 tooltip 渲染,包括处理原始数据、tooltip 内容设计,以及根据项目环境创建组件并设置样式。

  • 当图表删除时会调用当前 tooltip handler 的release函数,需要自行实现删除。


目前,火山引擎 DataWind 正是使用自定义 tooltip handler 的方式实现了自己的图表 tooltip。DataWind 支持用户对tooltip 进行富文本渲染,甚至支持了 tooltip 内渲染图表的能力。



另外,也可以参考官网示例(http://www.visactor.io/vchart/demo…



自定义 tooltip handler 的核心是调用 VChart 实例方法 setTooltipHandler,部分示例代码如下:


vchart.setTooltipHandler({
showTooltip: (activeType, tooltipData, params) => {
const tooltip = document.getElementById('tooltip');
tooltip.style.left = params.event.x + 'px';
tooltip.style.top = params.event.y + 'px';
let data = [];
if (activeType === 'dimension') {
data = tooltipData[0]?.data[0]?.datum ?? [];
} else if (activeType === 'mark') {
data = tooltipData[0]?.datum ?? [];
}
tooltipChart.updateData(
'tooltipData',
data.map(({ type, value, month }) => ({ type, value, month }))
);
tooltip.style.visibility = 'visible';
},
hideTooltip: () => {
const tooltip = document.getElementById('tooltip');
tooltip.style.visibility = 'hidden';
},
release: () => {
tooltipChart.release();
const tooltip = document.getElementById('tooltip');
tooltip.remove();
}
});

其他特性一览


VChart tooltip 包含一些其他的高级特性,下文将简要介绍。


在任意轴上触发 dimension tooltip


Dimension tooltip 一般最适合用于离散轴,ECharts 同时支持连续轴上的 dimension tooltip(axis tooltip)。而 VChart 支持了在连续轴、时间轴乃至在一个图表中的任意一个轴上触发 dimension tooltip。


以下示例展示了 dimension tooltip 在连续轴(时间轴)上汇总离散数据的能力(这个 case 和一般的 dimension tooltip 刚好相反):



一般的 dimension tooltip 会在离散轴(纵轴)触发 tooltip,汇总连续数据(对应于时间轴)。而 VChart 同时支持这两种方式的 tooltip。


Demo 地址:http://www.visactor.io/vchart/demo…


长内容支持:换行和局部滚动


过长的内容在 tooltip 上一般是 bad case。但是为了使长内容的浏览体验更好,VChart tooltip 可以配置多行文本以及内容区域局部滚动。如以下示例:



局部滚动 Demo 地址:http://www.visactor.io/vchart/demo…


多行文本配置项:http://www.visactor.io/vchart/opti…


结语


Tooltip 在提升用户浏览图表的体验中扮演着重要的角色。本文介绍了 VChart tooltip 的基本使用方法、技术设计以及多层面的自定义方案。然而为了保证行文清晰,VChart tooltip 还有一些其他的用法细节本文没有涉及,想了解更多可以查阅官网 demo 以及文档。


然而需要提醒的是,虽然 tooltip 能够有效传递数据与信息、以及增加图表的互动能力,但过分依赖它们可能会导致用户体验下降。合理地利用 tooltip,让它们在需要时出现而不干扰用户的主要任务,是设计和开发中应保持的平衡。


希望本文能为你在配置 VChart tooltip 时提供有用的指导。愿你在图表中创造更加直观、轻松且愉快的用户体验时,VChart 能成为你强大的伙伴。




Feb-22-2024 10-11-30.gif


github:github.com/VisActor/VC…


相关参考:



作者:玄魂
来源:juejin.cn/post/7337963242416422924
收起阅读 »

寒门子弟想跨越阶层有多难

快要高考了,最近身边发生了不少事,感触颇深,我想记录一下。大家都知道高考是唯一公平竞争的机会,也是寒门贵子唯一一次想翻身的机会。但是,每个人情况和起点不一致,考试是公平的,但出身是不公平的,出身在河南落后的农村的小镇做题家和出身在上海的繁华大都市的国际学校的先...
继续阅读 »

快要高考了,最近身边发生了不少事,感触颇深,我想记录一下。大家都知道高考是唯一公平竞争的机会,也是寒门贵子唯一一次想翻身的机会。但是,每个人情况和起点不一致,考试是公平的,但出身是不公平的,出身在河南落后的农村的小镇做题家和出身在上海的繁华大都市的国际学校的先进教育资源一样吗?答案可想而知。


身边有个亲人出身河南农村,这几天刚大学本科毕业,通过校招拿到了某物流领域独角兽公司的offer,base杭州,待遇是6k左右底薪,思考再三,他弟弟选择拒掉了offer,原因是,觉得靠自己这个收入全年无休也买不起杭州的房子,还要跟对象常年异地(对象在老家教师),于是选择去卷老家的编制岗,至少不用考虑买房,还能顾上了家庭,但是他的父母却表示不理解:说一辈子都没出息了,作为农民出身辛苦一辈子供出来的大学生,还要继续留在那个贫穷落后的小地方。为此他感到很苦恼,如何做才能不负如来不负卿!


同样,他说,比起其他同样出身在农村的孩子已经好很多了,至少有个月薪三千的稳定工作,很多农村的孩子都是提前辍学,进厂打螺丝或者失业待家,甚至娶不起农村媳妇,只能通过玩游戏,对抗父母证明自己的存在感,不然要被周围人的流言蜚语的唾沫星子淹死。提到这里甚至还有一丝欣慰。


他说,你不懂,像我这样出身农村的底层孩子,大多数在高考前就已经被淘汰掉了,因为太多穷苦老百姓,觉得读书没有直接去社会上进厂打工来的实在,孩子叛逆不想上学,也不会管太多,更不会觉得人生被毁掉了,相反,早点出去打过工挣钱才是农村人的常态,就这样,穷人的孩子干着穷苦的工作,攒点钱早点结婚生子,一代又一代,恶性循环,也不会觉得有任何不对,因为身边的环境和周围的人大多命运也都是这样。


形成鲜明对比的是,另一个同学,出身在江浙沪独生子,他说,从小只要学习不好,父母就感觉天塌了,因为他父母是很早一批的大学生,虽然他父母也是农村人的孩子,但是是最早一批的大学生,吃到了知识改变命运的红利,甚至不惜任何代价,帮他找资源,托关系,请到了他的老师给他在家补课开小灶,他说他学习一直不好,但是就是这么一路走过来的,甚至他父亲还帮他联系到了211院校的校长,讲到这里,大家意识到差距了吧。他说他学习很差,但被父母一路逼下去读到了研究生。。。毕业后家人关系托底去了某国有五大行,待遇不用多说大家也都懂。


同样,出身农村,学习很差就要回农村继续种地,娶个媳妇生个娃,干着廉价的劳动力,甚至都不知道好日子好工作是什么意思,因为资源和眼界有限,接触不到,也没见过,也无法给孩子形容出来,更没法用言语具体说服叛逆期的孩子。只好为了多挣钱去大城市,离开家人,最后挣点钱回到家了,孩子不理解,继续叛逆,等到了懂事的年纪,已经没有机会翻身了。这里声明:并不是我歧视农村,农村环境好,农村人淳朴,农村土地辽阔,除了经济医疗交通教育不发达,农家乐也非常有趣。


mmexport1717496259324.jpg


别的行业咱不敢说,至少计算机行业对比传统行业,给了穷人一次走向小康的机会,但这种机会也不会一直都有,互联网寒冬的背景下,即使选择了计算机专业,大多数底层出身的孩子也无法真正的跨越阶层,因为那个时代的红利,已经褪去了。。。


最近看到了一条新闻,说曾经在衡水中学的高考状元,出身农村,声称自己是“乡下的土猪拱了城里的白菜”,报考了浙大计算机专业,三年过后了,这位高考状元却迷茫了,因为他对计算机专业并没有兴趣,当初仅仅因为觉得这个行业高薪才报名的,但是行业在变化,在这个时代下,计算机行业已经发展迅速,没有了昨日的如日中天,相反,他的眼里没有光,他厌倦了这种一天都坐在电脑面前的工作,但是未来何去何从,他依然迷茫,他只知道学习高考,却没有好好规划自己的人生,没有认真思考自己的定位,不懂得人生价值,只知道脱离贫苦。这点也许是大多数底层出身孩子的迷茫吧!


回过神来,今年高考,如果再给你一次机会,你还会像曾经一样,没有把高考当成唯一一次公平翻身的机会吗?寒门贵子想跨越阶层到底有多难?大多数人的机会,也许就这么一次吧。


作者:为了WLB努力
来源:juejin.cn/post/7376407925646770239
收起阅读 »

总算体会到jsx写法为啥灵活

web
前言 大家好,我是你不会困,写代码就不会困,今天分享的是总算体会到jsx写法为啥灵活 什么是jsx写法? 当谈到JavaScript中的JSX写法时,人们往往会想到React和Vue这样的流行前端框架。JSX作为一种在JavaScript中编写类似于HTML的...
继续阅读 »

前言


大家好,我是你不会困,写代码就不会困,今天分享的是总算体会到jsx写法为啥灵活


什么是jsx写法?


当谈到JavaScript中的JSX写法时,人们往往会想到React和Vue这样的流行前端框架。JSX作为一种在JavaScript中编写类似于HTML的语法,为前端开发者提供了更灵活和直观的方式来构建用户界面。


JSX的灵活性体现在多个方面。首先,JSX允许开发者在JavaScript中嵌入HTML标记,使得代码更易读和维护。通过使用JSX,开发者可以在同一个文件中编写JavaScript逻辑和界面布局,而无需频繁切换不同的文件。这种混合编程风格提高了开发效率,同时也方便了代码的组织和调试。


其次,JSX支持在标记中使用JavaScript表达式,这使得动态生成界面变得更加简单。开发者可以在JSX中直接使用JavaScript变量、函数调用和逻辑控制语句,从而动态地渲染页面内容。这种灵活性使得开发者能够根据不同的数据状态和条件来动态展示内容,提升了用户体验。


另外,JSX还支持在标记中使用循环和条件语句,比如map函数和条件渲染,从而实现列表展示、条件展示等常见的UI需求。这种功能使得开发者可以更方便地处理复杂的UI逻辑,同时简化了代码的编写和维护。


此外,JSX的组件化特性也为前端开发带来了很多好处。通过将UI拆分成独立的组件,开发者可以更好地组织和管理代码,提高代码的重用性和可维护性。JSX中的组件可以嵌套使用,形成复杂的UI结构,同时每个组件可以单独管理自己的状态和逻辑,使得代码更加清晰和可扩展。


今天在开发的时候发现,这两个即可开启总计列


show-summary
:summary-method="getSummaries"

但是产品的需求比较麻烦,需要渲染多行,查了相关的文档,好像没有这种渲染的demo,翻看项目的代码,有一部分代码的实现比较巧妙,使用的是jsx写法,然后就尝试着去实现


要在vue里面使用jsx写法,在script标签使用<script lang="jsx">,即可使用


getSummaries(param) {
const { columns } = param
const sums = []
const nullHtml = '-'
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '总计'
return
}
if (this.totalSum.summaryReceivableComparisons) {
sums[index] = (
<div>
{this.totalSum.summaryReceivableComparisons.map((item) => (
<div class='cell-item' key={item.invoiceCurrency}>
<p>
{this.formatValue(
item[column.property],
column.property.includes('Ratio')
? 'percentage'
: 'thousandth'
)}
</p>
</div>
))}
</div>
)
} else {
sums[index] = nullHtml
return
}
})
return sums
},

上面的代码使用了map来遍历,将对应的html返回,el-table的总计列即可生效,来应对不同的需求


总结


总的来说,JSX作为JavaScript中的一种扩展语法,为前端开发带来了更灵活、直观和高效的开发体验。通过使用JSX,开发者可以更轻松地构建交互丰富、动态变化的用户界面,同时提高了代码的可读性和可维护性。JSX的灵活性和表现力使其成为现代前端开发中不可或缺的一部分。


作者:你不会困
来源:juejin.cn/post/7410672790020800548
收起阅读 »

JDK23,带来了哪些新功能?

前言 2024年9月17日,Java开发者们迎来了期待已久的JDK23版本。 下载地址:jdk.java.net/23/ 文档地址:jdk.java.net/23/release-… 为 JDK 21 之后的第一个非 LTS 版本,最终的 12 个 JEP ...
继续阅读 »

前言


2024年9月17日,Java开发者们迎来了期待已久的JDK23版本。


下载地址:jdk.java.net/23/


文档地址:jdk.java.net/23/release-…



为 JDK 21 之后的第一个非 LTS 版本,最终的 12 个 JEP 特性包括:


JEP 455:模式、instanceof 和 switch 中的原始类型(Primitive Types in Patterns, instanceof, and switch,预览)


JEP 466:类文件 API(Class-File API,第二轮预览)


JEP 467:Markdown 文档注释(Markdown Documentation Comments)


JEP 469:Vector API(第八轮孵化)


JEP 471:废弃 sun.misc.Unsafe 中的内存访问方法以便于将其移除(Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal)


JEP 473:流收集器(Stream Gatherers,第二轮预览)


JEP 474:ZGC:默认的分代模式(ZGC: Generational Mode by Default)


JEP 476:模块导入声明(Module Import Declarations,预览)


JEP 477:隐式声明的类和实例主方法(Implicitly Declared Classes and Instance Main Methods,第三轮预览)


JEP 480:结构化并发(Structured Concurrency,第三轮预览)


JEP 481:作用域值(Scoped Values,第三轮预览)


JEP 482:灵活的构造函数体(: Flexible Constructor Bodies,第二轮预览)


基本上每次JDK的升级,在带来一些新功能的同时,也会提升一些性能。


一些优秀的语法,也能提升开发效率。


除了编程语言升级能提升开发效率之外,一些好的开发工具或者设备,也可以。


2 提升开发效率


显示器可以提升开发效率?


答:是真的。


2.1 屏幕尺寸


在之后的一段时间内,我尝试过一些不同品牌和型号的外接显示器。


常见的显示器的屏幕比是16:9。


而我现在正在用的明基RD280U显示器的屏幕比是3:2。

跟我的笔记本电脑屏幕相比,高度是笔记本电脑的两倍了。


我第一次使用时,就明显感觉到,明基RD240Q显示器的屏幕更高一些,一屏可以多看十几行代码。


在开发过程中,每次滚动屏幕,都可以多看几行代码,如果次数多了,可以多看很多行代码,真的可以提高开发效率。


2.2 专业编程模式


我后来才知道明基RD280U是一个专业的编程显示器,专门给程序员设计的。


屏幕正下方的这个按键,可以调整编程模式,可以优化IDE上代码的显示效果,让代码更加清晰:


2.3 背光灯


我们之前在晚上编程的时候,经常需要打开台灯,才能让屏幕看到更清楚。


为了解决这个问题,明基RD280U提供了Moonhalo背光灯的功能,下面这张图是我在关灯的情况下拍摄的:

可以看到屏幕有黄色的背景灯光。


下面的这张灯光图更直观:

可以让你沉浸在开发中,不被打扰。


3 全方位呵护


明基RD280U显示器使用了莱茵认证护眼技术,实现了:低蓝光、无屏闪的效果。


3.1 护眼模式


在夜间开发,可以切换夜间保护模式:


如果切换成自动模式,当外面环境变亮时,屏幕会自动变暗。当外面环境变暗时,屏幕会自动变暗。


保护我们的眼睛。


智慧蓝光模式是为了减少蓝光对眼睛的刺激,提供更舒适的视觉体验。

我们可以调节让自己眼睛感到舒服的蓝光。


3.2 抗反射面板


当我们的屏幕出现其他的灯光直射时,笔记本电脑的效果是这样的:

代码完全看不清楚。


而明基RD280U显示器,即使遇到强光也能看清代码。


这是我非常喜欢的设计。


4 软件协同


明基RD280U显示器为了方便我们操作,还提供了一个驱动软件:Display Pilot2。


里面包含了画面切换,快速搜索,桌面分区和键盘快速切换功能。


我们可以在电脑上直接控制显示器:

文章前面介绍的这些功能,都可以直接在电脑上通过Display Pilot2进行控制。


比如开启显示器的Moonhalo背光灯。


新增的flow功能可以设置特定时间场景下的一些显示器的参数。


5 总结


本文主要介绍了JDK23的12项新特性,涵盖了语言预览、API增强、性能优化等多个方面,可能会对开发者的工作流程和编程习惯产生深远的影响。


同时也介绍了我正在使用的明基RD280U显示器的一些优秀的功能,比如:屏幕尺寸更大、专业编程模式、Moonhalo背光灯、护眼功能(夜间防护功能、智慧蓝光)、抗反射面板、display pilot2功能,能够提升开发效率和保护我们的眼睛。


作者:苏三说技术
来源:juejin.cn/post/7418072992838500362
收起阅读 »

如何组装一台高性价比的电脑

前言最近想配置一台电脑,由于在这方面是小白,就花了一部分时间以及精力在这方面,相信你看完,也能轻轻松松装电脑。目标高性价比、小钱办大事电脑配置基础知识组装一台台式机需要考虑多个组件,以下是一份基本的装机配置清单,包括了主要硬件和一些可选配件我把他们划分为必须和...
继续阅读 »

1718195402741.png

前言

最近想配置一台电脑,由于在这方面是小白,就花了一部分时间以及精力在这方面,相信你看完,也能轻轻松松装电脑。

目标

高性价比、小钱办大事

电脑配置基础知识

组装一台台式机需要考虑多个组件,以下是一份基本的装机配置清单,包括了主要硬件和一些可选配件

我把他们划分为必须和非必须(可理解为表单的必填或者非必填)

有了必须项目的配置就可以组装出一台电脑,非必须项目属于个人需求(可理解为个性化定制)

必须项

  1. 处理器(CPU) :

    • 选择适合需求的处理器,如Intel Core系列或AMD Ryzen系列。
  2. 显卡(GPU) :

    • 集成显卡或独立显卡,如NVIDIA GeForce或AMD Radeon系列。
  3. 主板(Motherboard) :

    • 根据CPU选择相应插槽类型的主板,如ATX、Micro-ATX或Mini-ITX规格。
  4. 内存(RAM) :

    • 至少8GB DDR4内存,更高需求可以选择16GB或32GB。
  5. 固态硬盘(SSD) :

    • 用于安装操作系统和常用软件,256GB或更大容量。
  6. 散热器(Cooler) :

    • CPU散热器,盒装CPU可能附带散热器。
  7. 电源供应器(PSU) :

    • 根据系统需求选择合适的功率,如500W、600W等,并确保有80 PLUS认证。
  8. 机箱(Case) :

    • 根据主板规格和个人喜好选择机箱。

机箱严格来讲属于非必须,因为你用纸箱子也能行,但是对于小白来说还是整个便宜的比较好

  1. 外围设备

    • 显示器、键盘、鼠标。
  2. 音频设备

    • 耳机、扬声器。

非必须项

  1. 机械硬盘(HDD, 可选) :

    • 用于数据存储,1TB或更大容量。
  2. 机箱风扇(可选) :

    • 用于改善机箱内部空气流通。
  3. 光驱(可选) :

    • 如有需要,可以选择DVD或蓝光光驱。
  4. 无线网卡(可选) :

    • 如果主板不支持无线连接,可以添加无线网卡。
  5. 声卡(可选) :

    • 如果需要更好的音频性能,可以添加独立声卡。
  6. 扩展卡(可选) :

    • 如网络卡、声卡、图形卡等。
  7. 机箱装饰(可选) :

    • RGB灯条、风扇等。

处理器(CPU)天梯图

image.png

显卡(GPU)天梯图

image.png

如何选择显示器

选择电脑显示器时,有几个关键因素需要考虑:

  1. 分辨率:分辨率决定了显示器的清晰度。常见的有1080p(全高清)、1440p(2K)、2160p(4K)等。分辨率越高,画面越清晰,但同时对显卡的要求也越高。
  2. 屏幕尺寸:根据你的使用习惯和空间大小来选择。大屏幕可以提供更宽广的视野,但也需要更大的桌面空间。
  3. 刷新率:刷新率表示显示器每秒可以刷新多少次画面。常见的有60Hz、144Hz、240Hz等。高刷新率可以提供更流畅的视觉效果,特别适合游戏玩家。
  4. 响应时间:响应时间指的是像素从一种状态变化到另一种状态所需的时间,通常以毫秒(ms)为单位。响应时间越短,画面变化越快,越适合快速变化的游戏或视频。
  5. 面板类型:主要有TN、IPS、VA三种面板。TN面板响应速度快,但色彩表现一般;IPS面板色彩表现好,视角宽,但响应时间相对较慢;VA面板则介于两者之间。
  6. 连接接口:确保显示器的连接接口与你的电脑兼容,常见的有HDMI、DisplayPort、DVI、VGA等。
  7. 色彩准确性:如果你的工作涉及到图像或视频编辑,那么选择色彩准确性高的显示器非常重要。
  8. 附加功能:一些显示器可能提供额外的功能,如USB接口、扬声器、可调节支架等。
  9. 预算:根据你的预算来决定购买哪种类型的显示器。通常来说,价格越高,显示器的性能和功能也越全面。
  10. 品牌和售后服务:选择知名品牌的显示器通常可以保证较好的质量和售后服务。

根据你的具体需求和预算,综合考虑上述因素,选择最适合你的显示器。 并不是配置越高越好,重点是根据你的显卡和CPU来看,实现极致性价比,需要对应电脑配置能带动的最大区间,比方说 英特尔 i5-12400F(6核心12线程,睿频4.4GHz)+ 华硕DUAL-RTX 4060-8G游戏显卡能带起来的刷星率极限基本就是165Hz左右,那么你配置2K,180Hz的显示器就够用了,当然你有钱也可以整个4K,200+Hz的

关于外围设备(键鼠、耳机、音响等)

这个看个人喜好以及预算来决定吧,比方说有线还是无线,机械还是非机械等等....

不同价位装机性价比清单(根据目前市场上)

装机其实主要花费就在显卡(GPU)和处理器(CPU)上,显卡(GPU)金额占比超过50%比比皆是,处理器(CPU)金额占比一般是在百分之20%~30%,其他配件金额占比20%~30%左右。

如果预算足够建议优先升级显卡

CPU分为盒装和散片,预算充足盒装,预算不足散片也能用

3K推荐

  • CPU: Intel 12代酷睿i5 12400F
  • 显卡: 华硕DUAL RTX 3050 6G
  • 内存: 威刚D4 16GB 3200MHz
  • 硬盘: 英睿达 500GB PCI-E 4.0 M.2
  • 主板: 圣旗H610M
  • 电源: 爱国者额定500W
  • 跑分: 107W±
  • 合计:3.5K左右

4K极致性价比

4K-6K这个价位其实CPU最佳的就是就是在下图区间 同理可自行对比GPU天梯图

image.png

image.png

  • CPU: 英特尔 i5-12400F(6核心12线程,睿频4.4GHz)
  • 显卡: 华硕DUAL-RTX 4060-8G游戏显卡 升级选项: +299元升级至华硕TX-RTX 4060-O8G-GAMING天选白三风扇
  • 散热: 动力火车四铜管白色强劲散热器
  • 主板: 华硕PRIME B760M-FD4(支持AURA神光同步)
  • 内存: 雷克沙DDR4 16GB 3200MHz高频内存
  • 硬盘: 雷克沙500GB NVMe PCIe SSD(读速高达3300MB/S)
  • 电源: 源之力静音大师额定600W安规3C白色
  • 机箱: 动力火车琉璃海景房
  • 合计:4K左右

1718192436562.png

5K极致性价比

1718192483898.png

image.png

image.png

6K极致性价比

image.png

7K极致性价比

image.png

8K极致性价比

image.png

1W极致性价比

1W预算以上可能考虑的不单单是配置了,还有外观之类的,可以DIY定制之类的

image.png

image.png

1.2W极致性价比

1718192287164.png

1.3w极致性价比

image.png

总结下,其实根据CPU、GPU天梯图就可以找到自己的目标区间,其他配置看个人预算来就行,核心就是两大件!如果有装机高手,欢迎留言交流,有不懂的,欢迎提问。后续会根据问题持续补充


作者:凉城a
来源:juejin.cn/post/7379420157670670372
收起阅读 »

Electron实现静默打印小票

web
Electron实现静默打印小票 静默打印流程 1.渲染进程通知主进程打印 //渲染进程 data是打印需要的数据 window.electron.ipcRenderer.send('handlePrint', data) 2.主进程接收消息,创建打印页面...
继续阅读 »

Electron实现静默打印小票


静默打印流程


09c00eb5-f171-4090-a178-37e149d1d0f7.png


1.渲染进程通知主进程打印


//渲染进程 data是打印需要的数据
window.electron.ipcRenderer.send('handlePrint', data)

2.主进程接收消息,创建打印页面


//main.ts
/* 打印页面 */
let printWindow: BrowserWindow | undefined
/**
* @Author: yaoyaolei
* @Date: 2024-06-07 09:27:22
* @LastEditors: yaoyaolei
* @description: 创建打印页面
*/

const createPrintWindow = () => {
return new Promise<void>((resolve) => {
printWindow = new BrowserWindow({
...BASE_WINDOW_CONFIG,
title: 'printWindow',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false
}
})

printWindow.on('ready-to-show', () => {
//打印页面创建完成后不需要显示,测试时可以调用show查看页面样式(下面有我处理的样式图片)
// printWindow?.show()
resolve()
})

printWindow.webContents.setWindowOpenHandler((details: { url: string }) => {
shell.openExternal(details.url)
return { action: 'deny' }
})

if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
printWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/print.html`)
} else {
printWindow.loadFile(join(__dirname, `../renderer/print.html`))
}
})
}

ipcMain.on('handlePrint', (_, obj) => {
//主进程接受渲染进程消息,向打印页面传递数据
if (printWindow) {
printWindow!.webContents.send('data', obj)
} else {
createPrintWindow().then(() => {
printWindow!.webContents.send('data', obj)
})
}
})

3.打印页面接收消息,拿到数据渲染页面完成后通知主进程开始打印


<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>打印</title>
<style>
</style>
</head>

<body>

</body>
<script>
window.electron.ipcRenderer.on('data', (_, obj) => {
//这里是接受的消息,处理完成后将html片段放在body里面完成后就可以开始打印了
//样式可以写在style里,也可以内联
console.log('event, data: ', obj);
//这里自由发挥
document.body.innerHTML = '处理的数据'
//通知主进程开始打印
window.electron.ipcRenderer.send('startPrint')
})
</script>
</html>



这个是我处理完的数据样式,这个就是print.html
9f17ea7e-3f83-408f-a780-05d50da305de.png
微信图片_20240609102325.jpg



4,5.主进程接收消息开始打印,并且通知渲染进程打印状态


ipcMain.on('startPrint', () => {
//这里如果不指定打印机使用的是系统默认打印机,如果需要指定打印机,
//可以在初始化的时候使用webContents.getPrintersAsync()获取打印机列表,
//然后让用户选择一个打印机,打印的时候将打印机名称传过来
printWindow!.webContents.print(
{
silent: true,
margins: { marginType: 'none' }
//deviceName:如果要指定打印机传入打印机名称
},
(success) => {
//通知渲染进程打印状态
if (success) {
mainWindow.webContents.send('printStatus', 'success')
} else {
mainWindow.webContents.send('printStatus', 'error')
}
}
)
})

aa.jpg



完毕~



作者:彷徨的耗子
来源:juejin.cn/post/7377645747448365091
收起阅读 »

我的 Electron 客户端被第三方页面入侵了...

web
问题描述 公司有个内部项目是用 Electron 来开发的,有个功能需要像浏览器一样加载第三方站点。 本来一切安好,但是某天打开某个站点的链接,导致 整个客户端直接变成了该站点的页面。 这一看就是该站点做了特殊的处理,经排查网页源码后,果然发现了有这么一句代码...
继续阅读 »

问题描述


公司有个内部项目是用 Electron 来开发的,有个功能需要像浏览器一样加载第三方站点。


本来一切安好,但是某天打开某个站点的链接,导致 整个客户端直接变成了该站点的页面


这一看就是该站点做了特殊的处理,经排查网页源码后,果然发现了有这么一句代码。


    if (window.top !== window.self) {
window.top.location = window.location;
}

翻译一下就是:如果当前窗口不是顶级窗口的话,将当前窗口设置为顶级窗口。


奇怪的是两者不是 跨域 了吗,为什么 iframe 还可以影响顶级窗口。


先说一下我当时的一些解决办法:



  1. webview 替换 iframe

  2. iframe 添加 sandbox 属性


后续内容就是一点复盘工作。


场景复现(Web端)


一开始怀疑是客户端的问题,所以我用在纯 Web 上进行了一次对比验证。


这里我们新建两个文件:1.html2.html,我们称之为 页面A页面B


然后起了两个本地服务器来模拟同源与跨域的情况。


页面A:http://127.0.0.1:5500/1.html


页面B:http://127.0.0.1:5500/2.htmlhttp://localhost:3000/2.html


符合同源策略


<body>
<h1>这是页面A</h1>
<!-- 这是同源的情况 -->
<iframe id="iframe" src="http://127.0.0.1:5500/2.html" />

<script>
iframe.onload = () => {
console.log('iframe loaded..')
console.log('子窗口路径', iframe.contentWindow.location.href)
}
</script>
</body>

<body>
<h2>这是页面B</h2>

<script>
console.log('page2...')
console.log(window === window.top)
console.log('顶部窗口路径', window.top.location.href)
</script>
</body>

我们打开控制台可以看到 页面A 和 页面B 是可以 互相访问 到对方窗口的路径。


image.png


如果这个时候在 页面B 加上文章开头提到的 代码片段,那么显然页面将会发生变化。


image.png


跨域的情况


这时候我们修改 页面A 加载 页面B 的地址,使其不符合同源策略。


image.png


理所应当的是,两个页面不能够相互访问了,这才是正常的,否则内嵌第三方页面可以互相修改,那就太不安全了。


场景复现(客户端)


既然 Web 端是符合预期的,那是不是 Electron 自己的问题呢?


我们通过 electron-vite 快速搭建了一个 React模板的electron应用,版本为:electron@22.3.27,并且在 App 中也嵌入了刚才的 页面B。


function App(): JSX.Element {
return (
<>
<h1>这是Electron页面</h1>
<iframe id="iframe" src="http://localhost:3000/2.html"/>
</>

)
}
export default App

image.png


对不起,干干净净的 Electron 根本不背这个锅,在它身上的表现如同 Web端 一样,也受同源策略的限制。


那么肯定是我的项目里有什么特殊的配置,通过对比主进程的代码,答案终于揭晓。


new BrowserWindow({
...,
webPreferences: {
...,
webSecurity: false // 就是因为它
}
})

Electron 官方文档 里是这么描述 webSecurity 这个配置的。



webSecurity boolean (可选) - 当设置为 false, 它将禁用同源策略 (通常用来测试网站), 如果此选项不是由开发者设置的,还会把 allowRunningInsecureContent设置为 true. 默认值为 true



也就是说,Electron本身是有一层屏障的,但当该属性设置为 false 的时候,我们的客户端将会绕过同源策略的限制,这层屏障也就消失了,因此 iframe 的行为表现得像是嵌套了同源的站点一样。


解决方案


把这个配置去掉,确实是可以解决这个问题,但考虑到可能对其他功能造成的影响,只能采取其他方案。


如文章开头提到的,用 webview 替换 iframe


webviewElectron的一个自定义元素(标签),可用于在应用程序中嵌入第三方网页,它默认开启安全策略,直接实现了主应用与嵌入页面的隔离。


因为目前这个需求是仅作展示,不需要与嵌套页面进行交互以及复杂的通信,因此在一开始的开发过程中,并没有使用它,而是直接采用了 iframe


iframe 也能够实现类似的效果,只需要添加一个 sandbox 属性可以解决。


MDN 中提到,sandbox 控制应用于嵌入在 <iframe> 中的内容的限制。该属性的值可以为空以应用所有限制,也可以为空格分隔的标记以解除特定的限制。


如此一来,就算是同源的,两者也不会互相干扰。


总结


这不是一个复杂的问题,发现后及时修复了,并没有造成很大的影响(还好是自己人用的平台)。


写这篇文章的主要目的是为了记录这次事件,让我意识到在平时开发过程中,把注意力过多的放在了 业务样式性能等这些看得见的问题上,可能很少关注甚至忽略了 安全 这一要素,以为前端框架能够防御像 XSS 这样的攻击就能安枕无忧。


谨记,永远不要相信第三方,距离产生美。


如有纰漏,欢迎在评论区指出。


作者:小陈同学吗
来源:juejin.cn/post/7398418805971877914
收起阅读 »

如何将用户输入的名称转成艺术字体-fontmin.js

web
写在开头 日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下: <template> <div class="font">橙某人</div> </template...
继续阅读 »

写在开头


日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下:


<template>
<div class="font">橙某人</div>
</template>

<style scoped>
@font-face {
font-family: "orange";
src: url("./orange.ttf");
}
.font {
font-family: "orange";
}
</style>


很简单吧🤡,但有时应用场景不同,可能需要我们考虑一下性能问题。



一般来说,我们常见的字体包整个是非常大的,小的有几M到十几M,大的可能去到上百M都有,特别是中文类的字体包会相对英文类的要更大一些。



如本文案例,我们仅需在用户输入完后加载对应的字体包即可,这样能避免性能的损耗。


为此,我们需要把整个字体包拆分、细致化、子集化,让它能达到按需引入的效果。


那么这要如何来做这个事情呢?这个方案单单前端可做不了,我们需要配合后端一起,下面就来看看具体的实现过程吧。😗


前端


前端小编用 Vue 来编写,具体如下:


<template>
<div>
<input v-model="name" />
<button @click="handleClick">生成</button>
<div v-if="showName" class="font">{{ showName }}</div>
</div>

</template>

<script>
export default {
data() {
return {
name: "",
showName: "",
};
},
methods: {
handleClick() {
// 创建link标签
const linkElement = document.createElement("link");
linkElement.setAttribute("rel", "stylesheet");
linkElement.setAttribute("type", "text/css");
linkElement.href = `http://localhost:3000?name=${encodeURIComponent(this.name)}`;
document.body.appendChild(linkElement);
// 优化显示效果
setTimeout(() => {
this.showName = this.name;
}, 300);
},
},
};
</script>


<style>
.font {
font-family: orange;
font-size: 50px;
}
</style>


应该都能看懂吧,主要就是生成了一个 <link /> 标签并插入到文档中,标签的请求地址指向我们服务端,至于服务端会返回什么你可以先猜一猜。👻


服务端


服务端小编选择用 Koa2 来编写,你也可以选择 Express 或者 Egg ,甚至 Node 也是可以的,差异不大,具体逻辑如下:


const koa = require("koa2");
const fs = require("fs");
const FontMin = require("fontmin");

const app = new koa();

/** @name 优化,缓存已经加载过的字体包进内存 **/
const fontFamilyMap = {};

/** @name 加载字体包 **/
function loadFontLibrary(fontPath, fontFamily) {
if (fontFamilyMap[fontFamily]) return fontFamilyMap[fontFamily];
return new Promise((resolve, reject) => {
fs.readFile(fontPath, (error, file) => {
if (error) {
reject(new Error(error.message));
} else {
fontFamilyMap[fontFamily] = file;
resolve(file);
}
});
});
}

app.use(async (ctx) => {
const { name } = ctx.query;
// 设置返回文件类型
ctx.set("Content-Type", "text/css");

const fontPath = "./font/orange.ttf";
const fontFamily = "orange";
if (!fs.existsSync(fontPath)) return (ctx.body = "字体包读取失败");

const fontMin = new FontMin();
const fontFile = await loadFontLibrary(fontPath, fontFamily);
fontMin.src(fontFile);

const getFontCSS = () => {
return new Promise((resolve) => {
fontMin
.use(FontMin.glyph({ text: name }))
.use(FontMin.css({ base64: true, fontFamily }))
.run((error, files) => {
if (error) {
console.log("error", error.message);
} else {
const fontContent = files?.[1]?.contents;
resolve(fontContent);
}
});
});
};

const fontCSS = await getFontCSS();

ctx.body = fontCSS;
});

app.listen(3000);

console.log("服务器开启: http://localhost:3000/");

我们主要是采用了 Fontmin 库来完成整个字体包的按需加载功能,这个库是第一个纯 JavaScript 字体子集化方案。



可能有后端是 Java 或者其他技术栈的小伙伴,你们也不用担心,据小编和公司后端同事了解,不同技术栈也是有对应的库可以解决的,需要的可以自行查查看。










至此,本篇文章就写完啦,撒花撒花。


image.png


希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。


作者:橙某人
来源:juejin.cn/post/7293151700869038099
收起阅读 »

登录问题——web端

问题描述:在集成环信SDK的过程中,大家可能会遇到一个令人困惑的问题:明明已经通过open登录成功了,但是在调用api时却总是报错,错误类型为type28或者type700或者type39 not login。本文将详细分析这个问题的原因,并提供相应的解决方案...
继续阅读 »

问题描述:

在集成环信SDK的过程中,大家可能会遇到一个令人困惑的问题:明明已经通过open登录成功了,但是在调用api时却总是报错,错误类型为type28或者type700或者type39 not login。本文将详细分析这个问题的原因,并提供相应的解决方案。


原因分析:


要解决这个问题,我们首先需要了解环信SDK的登录机制。登录过程实际上分为两个步骤:

1. 请求Token:这是open登录操作的第一步,即在open.then或者success回调中返回token。
2. 建立长连接:即建立WebSocket连接,触发onOpened或者onConnected回调。只有当onOpened或者onConnected回调被触发,才算是真正与环信服务器建立了连接。
SDK在拿到token后,会将其设置进入SDK并尝试建立连接。如果在onOpened或者onConnected回调触发之前就执行了api的调用,那么token可能还没有被正确设置进入SDK,从而导致后续的HTTP请求报token无效的错误。也就是出现type28或者type700或者type 39的报错。

解决方案:


为了避免这个问题,我们需要调整代码逻辑,确保在onOpened或者onConnected回调触发后再去请求一系列的接口。以下是具体的调整步骤:

1. 监听连接状态:在SDK初始化后,监听onOpened或者onConnected回调

2. 延迟调用api操作:不要在open.then或者success回调中立即执行api的调用,而是等待onOpened或者onConnected回调触发后再执行。
3. 检查SDK状态:调用api前检查SDK是否已经成功建立连接。
可以用以下三种方法中的一种判断检查SDK是否已经成功建立连接~
1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;
2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;
3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined

收起阅读 »

183天打造行业新标杆!BOE(京东方)国内首条第8.6代AMOLED生产线提前全面封顶

2024年9月25日,BOE(京东方)投建的国内首条第8.6代AMOLED生产线全面封顶仪式在成都市高新区举行,该生产线从开工到封顶仅用183天,以科学、高效、高质的速度再树行业新标杆。这不仅是BOE(京东方)创新突破、打造新质生产力的又一重大举措,也是OLE...
继续阅读 »

2024年9月25日,BOE(京东方)投建的国内首条第8.6代AMOLED生产线全面封顶仪式在成都市高新区举行,该生产线从开工到封顶仅用183天,以科学、高效、高质的速度再树行业新标杆。这不仅是BOE(京东方)创新突破、打造新质生产力的又一重大举措,也是OLED领域的里程碑事件,极大推动OLED显示产业快速迈进中尺寸发展阶段,对促进半导体显示产业优化升级、引领行业高质量发展具有重要意义。京东方科技集团董事长陈炎顺出席并宣布仪式启动,项目总指挥刘晓东、项目执行总指挥杨国波等领导及中建三局集团有限公司、中国建筑一局(集团)有限公司、中国电子工程设计院股份有限公司、四川华凯工程项目管理有限公司等相关单位领导共同出席封顶仪式。

BOE(京东方)第8.6代AMOLED生产线项目总指挥刘晓东在致辞中表示:“BOE(京东方)第8.6代AMOLED生产线自今年初正式开工以来,始终秉持‘五同时、五确保、五典范’建设原则,以坚韧不拔的意志和团结协作的精神,历时183天,提前达成全面封顶目标,标志着该生产线正式迈入新阶段。BOE(京东方)第8.6代AMOLED生产线必将成为行业标杆工程,为企业发展注入新的活力与动力。我们有信心、有能力打造全球最具竞争力的第8.6代AMOLED生产线,为全球显示产业进步贡献重要力量。”

BOE(京东方)第8.6代AMOLED生产线总投资630亿元,是四川省迄今投资体量最大的单体工业项目,设计产能每月3.2万片玻璃基板(尺寸2290mm×2620mm),主要生产笔记本电脑、平板电脑等智能终端高端触控OLED显示屏。BOE(京东方)通过采用低温多晶硅氧化物(LTPO)背板技术与叠层发光器件制备工艺,使OLED屏幕实现更低的功耗和更长的使用寿命,也将带动下游笔记本及平板电脑产品的迭代升级。目前,BOE(京东方)已在成都、重庆、绵阳投建了三条第6代柔性AMOLED生产线,再加上国内首条第8.6代AMOLED生产线的投建,全面展现了其全球领先的技术实力和行业影响力。值得关注的是,截至2023年,BOE(京东方)柔性OLED出货量已连续多年稳居国内第一,全球第二(数据来源:Omdia),柔性OLED相关专利申请超3万件。BOE(京东方)柔性显示技术不仅应用于手机领域,还持续拓展笔记本、车载、可穿戴等领域,折叠屏、滑卷屏、全面屏等柔性显示解决方案已覆盖国内外众多头部终端品牌,进一步确立BOE(京东方)在OLED领域的全球领先地位。

2024年,BOE(京东方)面向下一个三十年的新征程全新出发,公司将始终坚持“传承、创新、发展”的企业文化内核,坚定信念、创新变革,持续探索契合市场需求的企业发展“第N曲线”。BOE(京东方)第8.6代AMOLED生产线也将汇聚新型显示产业人才,发挥引擎作用,打造以柔性显示为核心的“世界柔谷”,在持续提升竞争力的同时,谱写行业高质发展的新篇章。

收起阅读 »

“你好BOE”即将重磅亮相上海国际光影节 这场“艺术x科技”的顶级光影盛宴不容错过!

当艺术遇上科技,将会擦出怎样的璀璨火花?答案即将在首届上海国际光影节揭晓。9月29日-10月5日,全球显示龙头企业BOE(京东方)年度标杆性品牌IP“你好BOE”即将重磅亮相上海国际光影节,这也是虹口区的重点项目之一。现场BOE(京东方)将携手上海电影、上影元...
继续阅读 »

当艺术遇上科技,将会擦出怎样的璀璨火花?答案即将在首届上海国际光影节揭晓。9月29日-10月5日,全球显示龙头企业BOE(京东方)年度标杆性品牌IP“你好BOE”即将重磅亮相上海国际光影节,这也是虹口区的重点项目之一。

现场BOE(京东方)将携手上海电影、上影元、OUTPUT、新浪微博、海信、OPPO、京东等众多顶级文化机构与全球一线知名企业,在上海城市地标北外滩临江5米平台打造一场以创新科技赋能影像艺术的顶级视觉盛宴,将成为本届光影节期间上海北外滩“最吸睛”的打卡地点!


看点一:全球首款升降式裸眼3D数字艺术装置“大地穹幕” 闪耀北外滩最具科技感的光影亮色

在 “你好BOE”活动现场,全球首款升降式裸眼3D数字艺术装置——“大地穹幕”将正式与观众见面!你可以在高达5米的大型裸眼3D折角屏幕前亲身感受极具视觉冲击力的3D大屏画面,还可以通过智能识别技术与屏幕光影尽情交互,让整个北外滩广场瞬间变成户外露天观众席,极具特色的“大地穹幕”与临江对岸的上海“三件套”交相辉映,市民朋友们可以在外滩的微风中尽情畅享裸眼3D影院级体验。

看点二:灵动“舞屏”、 MELD 裸眼3D沉浸空间、智慧光幕技术 打造极具未来感的科技大秀

屏幕会跳舞?即将亮相的全新“舞屏”将颠覆你的想象力!动态的机械臂仿佛拥有生命一般自如地抓取、搬运、旋转,让高清大屏在你眼前自由舞动;或者在MELD 裸眼3D沉浸空间体验一场虚拟与现实之间的穿梭;亦或是在搭载智慧光幕技术的“玻璃窗”前,感受玻璃明暗随日升月落不断流转变化,未来科技带来的美好生活仿若近在眼前!

看点三:经典动画IP、当代摄影艺术、国产3A游戏巨制 焕活文化艺术全新生命力

在活动现场,《大闹天宫》、《哪吒闹海》等上影元运营的经典动画作品将在AI技术加持下以全新面貌惊艳呈现,带你瞬间梦回童年!敦煌画院的古老壁画也在8K超高清全真还原技术下纤毫毕现;现场你还能看到当下最炙手可热的全球首款量产的“三折屏”手机等最新科技产品齐齐亮相。在这一文化艺术的聚集地,自然也少不了时下最火热的国产3A游戏巨制《黑神话·悟空》,游戏爱好者可以在BOE(京东方)赋能的110英寸超大尺寸IP定制电视上亲眼感受媲美OLED的顶级画质,亲身体验288Hz极致超高刷新率带来的酣畅淋漓。

你还在等什么?这个十一假期,让我们相约金秋时节的上海北外滩,一起打卡这场顶级的光影科技盛会,开启令人期待的美好科技体验吧!

收起阅读 »

iframe的基本使用与注意点

web
iframe(Inline Frame)是一种在网页中嵌套其他网页的 HTML 元素。通过 iframe,开发者可以在一个页面中加载另一个页面的内容,提升用户体验和功能性。下面将详细探讨 iframe 的原理、使用场景以及注意事项,并提供相应的代码示例。 一、...
继续阅读 »


iframe(Inline Frame)是一种在网页中嵌套其他网页的 HTML 元素。通过 iframe,开发者可以在一个页面中加载另一个页面的内容,提升用户体验和功能性。下面将详细探讨 iframe 的原理、使用场景以及注意事项,并提供相应的代码示例。


一、iframe 的原理


iframe 是一种 HTML 标签,其基本语法如下:


<iframe src="https://example.com" width="600" height="400" frameborder="0"></iframe>


  • src:指定要加载的网页地址。

  • widthheight:定义 iframe 的宽度和高度。

  • frameborder:控制边框显示(在 HTML5 中不推荐使用)。


当浏览器遇到 iframe 标签时,会发起一个独立的网络请求来加载指定的 URL。这使得嵌入的内容在主文档之外独立渲染。


二、使用场景



  1. 广告展示



    • iframe 经常用于展示广告内容,允许网站在不影响主页面的情况下,灵活更新广告。


    <iframe src="https://ad.example.com" width="300" height="250" frameborder="0"></iframe>


  2. 第三方内容集成



    • 嵌入社交媒体帖子、视频播放器或地图等内容。例如,嵌入 YouTube 视频:


    <iframe src="https://www.youtube.com/embed/VIDEO_ID" width="560" height="315" frameborder="0" allowfullscreen></iframe>


  3. 内容隔离



    • 当需要展示用户生成的内容(如评论或论坛)时,可以使用 iframe 进行内容隔离,避免对主页面造成影响。



  4. 安全性




    • 使用 sandbox 属性,可以限制 iframe 的功能,增加安全性。



      1. allow-forms


        允许 iframe 内部的表单提交。默认情况下,表单提交被禁止。


      2. allow-same-origin


        允许 iframe 中的文档以相同来源访问其父页面。这允许脚本与同源的内容交互。


      3. allow-scripts


        允许 iframe 中的脚本执行。默认情况下,脚本执行被禁止。


      4. allow-top-navigation


        允许 iframe 中的内容导航到父页面。这使得嵌入页面可以改变主页面的 URL。


      5. allow-popups


        允许 iframe 中的内容打开新窗口或标签页。默认情况下,这种操作被禁止。


      6. allow-modals


        允许 iframe 显示模态对话框,例如 alertpromptconfirm


      7. allow-presentation


        允许 iframe 进入展示模式,例如全屏模式。





    <iframe src="https://example.com" width="600" height="400" sandbox="allow-scripts"></iframe>



三、注意点



  1. 安全性问题



    • 由于跨站点脚本攻击(XSS)的风险,很多网站设置了 X-Frame-OptionsContent-Security-Policy 来限制 iframe 的嵌入。这会导致“拒绝了我们的连接请求”的错误提示。




image.png


```http
X-Frame-Options: DENY
`
``


  1. 性能影响



    • 嵌套多个 iframe 会增加页面的加载时间和复杂性,影响性能。因此,建议合理使用。



  2. 跨域限制



    • 由于同源策略,iframe 中加载的页面不能与主页面进行直接交互。这意味着无法访问嵌入页面的 DOM 或 JavaScript。



  3. SEO 考虑



    • 搜索引擎可能不会索引 iframe 内的内容,从而影响整体的 SEO 表现。避免将重要内容仅放在 iframe 中。



  4. 响应式设计



    • 确保 iframe 在不同设备和屏幕尺寸下表现良好,可以通过 CSS 设置其宽度为百分比。例如:


    iframe {
    width: 100%;
    height: auto;
    }



四、示例代码


以下是一个综合示例,展示了如何使用 iframe 加载一个 YouTube 视频并应用响应式设计:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Iframe Example</title>
<style>
.responsive-iframe {
position: relative;
padding-bottom: 56.25%; /* 16:9 Aspect Ratio */
height: 0;
overflow: hidden;
}
.responsive-iframe iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>

<h1>嵌入 YouTube 视频</h1>
<div class="responsive-iframe">
<iframe src="https://www.youtube.com/embed/VIDEO_ID" frameborder="0" allowfullscreen></iframe>
</div>

</body>
</html>

结论


iframe 是一种强大的网页嵌入技术,能够增强网页功能和用户体验。在使用时,需要充分考虑安全性、性能和跨域问题,以确保良好的用户体验。通过合理配置和使用,iframe 可以为网页增加更多的互动性和功能性。


---09/19ヾ( ̄▽ ̄)ByeBye


再见.png


作者:不爱说话郭德纲
来源:juejin.cn/post/7415914059106533439
收起阅读 »

get请求参数放在body中?

web
1、背景 与后端对接口时,看到有一个get请求的接口,它的参数是放在body中的 ******get请求参数可以放在body中?? 随即问了后端,后端大哥说在postman上是可以的,还给我看了截图 可我传参怎么也调不通! 下面就来探究到底是怎么回事 2、...
继续阅读 »

1、背景


与后端对接口时,看到有一个get请求的接口,它的参数是放在body中的



******get请求参数可以放在body中??


随即问了后端,后端大哥说在postman上是可以的,还给我看了截图



可我传参怎么也调不通!


下面就来探究到底是怎么回事


2、能否发送带有body参数的get请求


项目中使用axios来进行http请求,使用get请求传参的基本姿势:


// 参数拼接在url上
axios.get(url, {
params: {}
})

如果想要将参数放在body中,应该怎么做呢?


查看axios的文档并没有看到对应说明,去github上翻看下axios源码看看


lib/core/Axios.js文件中



可以看到像deletegetheadoptions方法,它们只接收两个参数,不过在config中有一个data



熟悉的post请求,它接收的第二个参数data就是放在body的,然后一起作为给this.request作为参数


所以看样子get请求应该可以在第二个参数添加data属性,它会等同于post请求的data参数


顺着源码,再看看lib/adapters/xhr.js,上面的this.request最终会调用这个文件封装的XMLHttpRequest


export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
let requestData = config.data

// 将config.params拼接在url上
request.open(config.method.toUpperCase(),
buildURL(fullPath, config.params, config.paramsSerializer), true);

// 省略若干代码
...

// Send the request
request.send(requestData || null);
});
}

最终会将data数据发送出去


所以只要我们传递了data数据,其实axios会将其放在body发送出去的


2.1 实战


本地起一个koa服务,弄一个简单的接口,看看后端能否接收到get请求的body参数


router.get('/api/json', async (ctx, next) => {
console.log('get请求获取body: ', ctx.request.body)

ctx.body = ctx.request.body
})

router.post('/api/json', async (ctx, next) => {
console.log('post请求获取body: ', ctx.request.body)

ctx.body = ctx.request.body
})

为了更好地比较,分别弄了一个getpost接口


前端调用接口:


const res = await axios.get('/api/json', {
data: {
id: 1,
type: 'GET'
}
})


const res = await axios.post('/api/json', {
data: {
id: 2,
type: 'POST'
}
})
console.log('res--> ', res)

axiossend处打一个断点



可以看到数据已经被放到body中了


后端已经接收到请求了,但是get请求无法获取到body



结论:



  • 前端可以发送带body参数的get请求,但是后端接收不到

  • 这就是接口一直调不通的原因


3、这是为何呢?


我们查看WHATGW标准,在XMLHttpRequest中有这么一个说明:



大概意思:如果请求方法是GETHEAD ,那么body会被忽略的


所以我们虽然传递了,但是会被浏览器给忽略掉


这也是为什么使用postman可以正常请求,但是前端调不通的原因了


因为postman并没有遵循WHATWG的标准,body参数没有被忽略



3.1 fetch是否可以?


fetch.spec.whatwg.org/#request-cl…


答案:也不可以,fetch会直接报错



总结



  1. 结论:浏览器并不支持get请求将参数放在body

  2. XMLHTTPRequest会忽略body参数,而fetch则会直接报错


作者:蝼蚁之行
来源:juejin.cn/post/7283367128195055651
收起阅读 »

Systeminformation.js: 为什么不试试最强的系统信息获取工具?

web
大家好,我是徐徐。今天跟大家分享一款获取系统信息的工具库:systeminformation。前言在现代开发环境中,跨平台获取系统信息已经成为许多应用程序的重要需求。无论你是在开发需要详细系统信息的应用,还是需要获取硬件和软件的状态信息,一个强大且灵活的工具库...
继续阅读 »

大家好,我是徐徐。今天跟大家分享一款获取系统信息的工具库:systeminformation。

前言

在现代开发环境中,跨平台获取系统信息已经成为许多应用程序的重要需求。无论你是在开发需要详细系统信息的应用,还是需要获取硬件和软件的状态信息,一个强大且灵活的工具库可以显著提升你的开发效率。今天,我们要分享的是systeminformation这个 Node.js 库,可以帮你轻松获取到你想要的各种系统信息。

基本信息

什么是 systeminformation?

systeminformation 是一个轻量级的 Node.js 库,旨在提供跨平台的系统信息获取功能。无论是在 Windows、macOS 还是 Linux 上,它都能为你提供一致的接口,获取系统的硬件和软件信息。自2015年发布以来,systeminformation 已经成为开发者们获取系统信息的首选工具之一。

它提供了超过 50 个函数,用于检索详细的硬件、系统和操作系统信息。该库支持 Linux、macOS、部分 Windows、FreeBSD、OpenBSD、NetBSD、SunOS 以及 Android 系统,并且完全无依赖。无论你需要全面了解系统状况,还是仅仅想获取特定的数据,systeminformation 都能满足你的需求,帮助你在各个平台上轻松获取系统信息。

主要特点

  • 跨平台支持:支持 Windows、macOS 和 Linux 系统,提供一致的接口。
  • 全面的信息获取:能够获取 CPU、内存、磁盘、网络、操作系统等详细信息。
  • 实时监控:支持获取实时的系统性能数据,如 CPU 使用率、内存使用率、网络速度等。
  • 易于集成:通过简单的 API 调用即可获取所需信息,便于集成到各种应用程序中。

使用场景

  • 服务器监控:实时监控服务器性能,获取 CPU、内存、磁盘等硬件信息。
  • 桌面应用:获取本地系统信息,展示系统状态和性能数据。
  • IoT 设备:在物联网设备上获取系统信息,进行设备管理和监控。

快速上手

要在你的 Node.js 项目中使用 systeminformation,只需以下简单步骤:

    1. 安装 systeminformation
npm install systeminformation
    1. 获取系统信息示例
const si = require('systeminformation');

// 获取 CPU 信息
si.cpu()
.then(data => console.log(data))
.catch(error => console.error(error));

// 获取内存信息
si.mem()
.then(data => console.log(data))
.catch(error => console.error(error));

// 获取操作系统信息
si.osInfo()
.then(data => console.log(data))
.catch(error => console.error(error));
    1. 实时监控示例
const si = require('systeminformation');

// 实时监控 CPU 使用率
setInterval(() => {
si.currentLoad()
.then(data => console.log(`CPU Load: ${data.currentload}%`))
.catch(error => console.error(error));
}, 1000);

// 实时监控内存使用情况
setInterval(() => {
si.mem()
.then(data => console.log(`Memory Usage: ${data.used / data.total * 100}%`))
.catch(error => console.error(error));
}, 1000);

结语

systeminformation 是一个功能强大且灵活的 Node.js 库,能够帮助你轻松获取系统的各种信息。无论你是需要实时监控服务器性能,还是需要获取本地系统的详细信息,systeminformation 都能为你提供稳定且易用的解决方案。

希望这篇文章能帮助你了解 systeminformation 的强大功能,并激发你在项目中使用它的灵感。赶快分享给你的朋友们吧!


作者:前端徐徐
来源:juejin.cn/post/7413643760771072015
收起阅读 »

axios VS alova.js,谁是真正的通信王者?

web
新年快乐!在这个快速发展的前端世界里,咱们工程师面临的挑战也是一个接一个。今天,咱们就来聊聊前端实时通信这个话题。 想想看,你在使用那些传统的HTTP客户端时,比如axios,是否遇到过这样的问题:与React、Vue等框架的结合不够紧密,导致开发效率低下;...
继续阅读 »

新年快乐!在这个快速发展的前端世界里,咱们工程师面临的挑战也是一个接一个。今天,咱们就来聊聊前端实时通信这个话题。



想想看,你在使用那些传统的HTTP客户端时,比如axios,是否遇到过这样的问题:与React、Vue等框架的结合不够紧密,导致开发效率低下;在性能方面表现不佳,尤其是在处理频繁或重复的请求时;还有那略显臃肿的体积,以及混乱的响应数据类型定义?


哎呀妈呀,这些问题听着就让人头大。但别急,有个叫做alovajs的工具,可能会让你眼前一亮。


alovajs是一个轻量级的请求策略库,它不仅提供了与axios相似的API设计,让你能更快上手,还解决了上述的那些问题。它如何解决?咱们来一探究竟。


首先,alovajs能够与UI框架深度融合,自动管理请求相关的数据。这意味着你在Vue或React等框架中使用alovajs时,不再需要手动创建和维护请求状态,大大提高了开发效率。


其次,alovajs默认开启了内存缓存和请求共享,这些功能可以在提高请求性能的同时,提升用户体验并降低服务端的压力。比如,当你实现一个列表页,用户点击列表项进入详情页时,alovajs可以智能地使用缓存数据,避免不必要的重复请求。


最后,alovajs的体积只有4kb+,仅是axios的30%左右,而且它提供了更加直观的响应数据TS类型定义,对于重度使用Typescript的同学来说,这绝对是个福音。


说了这么多,是不是有点心动了?如果你对alovajs感兴趣,可以访问它的官网查看更多详细信息:alovajs官网。也欢迎你在评论区分享你对alovajs的看法和使用经验,让我们一起交流学习吧!
有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。


作者:胡镇alovajs
来源:juejin.cn/post/7334503381200437299
收起阅读 »

一文搞懂JS类型判断的四种方法

web
前言 在JavaScript中,类型判断是一个非常基础但也十分重要的知识点。不同的类型判断方法适用于不同的场景,掌握这些方法可以帮助我们更好地理解和使用JavaScript。本文将详细介绍typeof、instanceof、Object.prototype.t...
继续阅读 »

前言


在JavaScript中,类型判断是一个非常基础但也十分重要的知识点。不同的类型判断方法适用于不同的场景,掌握这些方法可以帮助我们更好地理解和使用JavaScript。本文将详细介绍typeofinstanceofObject.prototype.toString以及Array.isArray这四种常用的类型判断方法,并通过实例代码帮助大家加深理解。


正文


typeof


typeof操作符可以用来判断基本数据类型,如stringnumberbooleanundefinedsymbolbigint等。它对于null和所有引用类型的判断会返回"object",而对于函数则会返回"function"


特点:



  1. 可以判断除null之外的所有原始类型。

  2. 除了function,其他所有的引用类型都会被判断成object

  3. typeof是通过将值转换为二进制后判断其二进制前三位是否为0,是则为object


示例代码:


let s = '123'; // string
let n = 123; // number
let f = true; // boolean
let u = undefined; // undefined
let nu = null; // null
let sy = Symbol(123); // Symbol
let big = 1234n; // BigInt

console.log(typeof s); // "string"
console.log(typeof n); // "number"
console.log(typeof f); // "boolean"
console.log(typeof u); // "undefined"
console.log(typeof sy); // "symbol"
console.log(typeof big); // "bigint"
console.log(typeof nu); // "object" - 特殊情况

let obj = {};
let arr = [];
let fn = function() {};
let date = new Date();

console.log(typeof obj); // "object"
console.log(typeof arr); // "object"
console.log(typeof date); // "object"
console.log(typeof fn); // "function"

function isObject(o) {
if (typeof o === 'object' && o !== null) {
return true;
}
return false;
}

let res = isObject({a: 1});
console.log(res); // true

instanceof


instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。因此,它主要用于判断引用类型


特点:



  1. 只能判断引用类型。

  2. 通过原型链查找来判断类型。


示例代码:


let obj = {};
let arr = [];
let fn = function() {};
let date = new Date();

console.log(obj instanceof Object); // true
console.log(arr instanceof Array); // true
console.log(fn instanceof Function); // true
console.log(date instanceof Date); // true
console.log(arr instanceof Object); // true

console.log(arr instanceof String); // false
console.log(n instanceof Number); // false

因为原始类型没有原型而引用类型有原型,所有instanceof主要用于判断引用类型,那么根据这个我们是不是可以手写一个instanceof


手写·instanceof实现:


首先我们要知道v8创建对象自变量是这样的,拿let arr = []举例子:


function createArray() {
// 创建一个新的对象
let arr = new Array();
// 设置原型
arr.__proto__ = Array.prototype;
// 返回创建的数组对象
return arr;
}

V8 引擎会调用 Array 构造函数来创建一个新的数组对象,Array 构造函数的内部实现会创建一个新的空数组对象,并初始化其内部属性并且将新创建的数组对象的 __proto__ 属性设置为 Array.prototype,这意味着数组对象会继承 Array.prototype 上的所有方法和属性,最后,新创建的数组对象会被赋值给变量 arr


那么我们是不是可以通过实例对象的隐式原型等于其构造函数的显式原型来判断类型,代码如下:


function myInstanceOf(L,R){
if(L.__proto__ === R.prototype){
return true;
}
return false;
}

但是我们看到console.log([] instanceof Object); // true,所有还要改进一下:


我们要知道这么一件事情:



  1. 内置构造函数的原型链



    • 大多数内置构造函数(如 ArrayFunctionDateRegExpErrorNumberStringBooleanMapSetWeakMapWeakSet 等)的原型(Constructor.prototype)都会直接或间接地继承自 Object.prototype

    • 这意味着这些构造函数创建的对象的原型链最终会指向 Object.prototype



  2. Object.prototype 的原型



    • Object.prototype 的隐式原型(即 __proto__)为 null。这是原型链的终点,表示没有更多的原型可以继承。




所以我们是不是可以这样:


function myinstanceof(L, R) {
while (L !== null) {
if (L.__proto__ === R.prototype) {
return true;
}
L = L.__proto__;
}
return false;
}

console.log(myinstanceof([], Array)); // true
console.log(myinstanceof([], Object)); // true
console.log(myinstanceof({}, Array)); // false

所以就完美实现了。


Object.prototype.toString.call


Object.prototype.toString.call 是一个非常有用的工具,可以用来获取任何 JavaScript 值的类型信息。它结合了 Object.prototype.toStringFunction.prototype.call 两个方法的功能。


特点:



  1. 可以判断任何类型


代码示例


console.log(Object.prototype.toString.call(null));       // [object Null]
console.log(Object.prototype.toString.call(123)); // [object Number]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call('hello')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call(123n)); // [object BigInt]

Object.prototype.toString


底层逻辑


根据官方文档,Object.prototype.toString 方法的执行步骤如下:



  1. 如果此值未定义,则返回 "[object undefined]"

  2. 如果此值为 null,则返回 "[object Null]"

  3. 定义 O 是调用 ToObject (该方法作用是把 O 转换为对象) 的结果,将 this 值作为参数传递

  4. 定义 class 是 O 的 [[Class]] 内部属性的值

  5. 返回 "[object" 和 class 和 "]" 组成的字符串的结果


关键点解释



  • ToObject 方法:将传入的值转换为对象。对于原始类型(如 stringnumberboolean),会创建对应的包装对象(如 StringNumberBoolean)。对于 null 和 undefined,会有特殊处理。

  • [[Class]] 内部属性:每个对象都有一个 [[Class]] 内部属性,表示对象的类型。例如,数组的 [[Class]] 值为 "Array",对象的 [[Class]] 值为 "Object"


console.log(Object.prototype.toString(123));//[object Object]
console.log(Object.prototype.toString('123'));//[object Object]
console.log(Object.prototype.toString({}));//[object Object]
console.log(Object.prototype.toString([]));//[object Object]

为什么需要 call


Object.prototype.toString 方法默认的 this 值是 Object.prototype 本身。如果我们直接调用 Object.prototype.toString(123)this 值仍然是 Object.prototype,而不是我们传入的值。因此,我们需要使用 call 方法来改变 this 值,使其指向我们传入的值。


手写call


obj = {
a:1,
}

function foo(){
console.log(this.a);
}

//我们需要将foo中的this指向obj里面
Function.prototype.myCall = function(context){
if(!(this instanceof Function)){ 在构造函数原型上,this指向的是实例对象,这里即foo
return new TypeError(this+'is not function')
}

const fn = Symbol('key'); //使用symbol作为key是因为可能会同名
context[fn] = this;//添加变量名为fn,值为上面的,context={Symbol('key'): foo}
context[fn](); // 触发了隐式绑定
delete context[fn]; //删除这个方法
}

foo.myCall(obj) // 1
console.log(obj); // {a:1}

我们知道call方法是将函数里面的this强行掰弯到我们传入的对象里面去,它的原理是这样的,首先判断你传入的参数是不是一个函数,因为只有函数身上才有call方法,函数调用然后通过隐式绑定规则,将this指向这个对象,那么不就强行更改了this的指向,[不知道this的可以看这篇文章](你不知道的JavaScript(核心知识点概念详细整理-掘金 (juejin.cn))


Array.isArray


Array.isArray是一个静态方法,用于检测给定的值是否为数组。


示例代码:


let arr = [];
let obj = {};

console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false

手写Array.isArray实现:


function myIsArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}

console.log(myIsArray(arr)); // true
console.log(myIsArray(obj)); // false

总结



  • typeof适合用于检查基本数据类型,但对于null和对象类型的判断不够准确。

  • instanceof用于检查对象的构造函数,适用于引用类型的判断。

  • Object.prototype.toString提供了一种更通用的方法来判断所有类型的值。

  • Array.isArray专门用于判断一个值是否为数组。


希望这篇文章能够帮助你更好地理解和使用JavaScript中的类型判断方法,感谢你的阅读!


image.png


作者:反应热
来源:juejin.cn/post/7416657615369388084
收起阅读 »

三方接口不动声色将http改为了https,于是开启了我痛苦的一天

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。 原来是调的一个三方接口报错了: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorExc...
继续阅读 »

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。


原来是调的一个三方接口报错了:


javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alert.createSSLException(Alert.java:131)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:353)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:296)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:291)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:652)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:471)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:367)
at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:376)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
at sun.security.ssl.TransportContext.dispatch(TransportContext.java:183)
at sun.security.ssl.SSLTransport.decode(SSLTransport.java:154)
at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1279)
at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1188)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:401)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:373)

查看原因:



由于JVM默认信任证书不包含该目标网站的SSL证书,导致无法建立有效的信任链接。



奥...原来是他们把接口从http改为了https,导致我们获取数据报错了。再看看他们的证书,奥...新的。


image.png


好了,看看我们的逻辑,这其实是一个获取对方生成的PDF文件的接口


PdfReader pdfReader = new PdfReader(url);

url就是他们给的链接,是这行代码报的错。这时候,开始研究,在网上扒拉,找到了初版方案


尝试1


写一个程序专门获取安全证书,这代码有点长,全贴出来影响阅读。我给扔我hithub上了github.com/lukezhao6/I… 将这个文件贴到本地,执行javac InstallCert.java将其进行编译


image.png


编译完长这样:


image.png
然后再执行java InstallCert http://www.baidu.com (这里我们用百度举例子,实际填写的就是你想要获取证书的目标网站)


image.png
报错不用怕,因为它会去检查目标服务器的证书,如果出现了SSLException,表示证书可能存在问题,这时候会把异常信息打印出来。


在生成的时候需要输入一个1


image.png
这样,我们需要的证书文件就生成好了


image.png


这时候,将它放入我们本地的 jdk的lib\security文件夹内就行了


image.png


重启,这时候访问是没有问题了。阶段性胜利。


但是,但是。一顿操作下来,对于测试环境的docker,还有生产环境貌似不能这么操作。 放这个证书文件比较费事。


那就只能另辟蹊径了。


尝试2


搜到了,还有两种方案。



1.通过System.setProperty("javax.net.ssl.trustStore", "你的jssecacerts证书路径");


2.程序启动命令-Djavax.net.ssl.trustStore=你的jssecacerts证书路径 -Djavax.net.ssl.trustStorePassword=changeit



我尝试了第一种,System.setProperty可以成功,但是读不到文件,权限什么的都是ok的。
检查了蛮多地方



  • 路径格式问题

  • 文件是否存在

  • 文件权限

  • 信任库密码

  • 系统属性优先级


貌似都是没问题的,但肯定又是有问题的,因为没起作用。但是想着这样的接口有4个,万一哪天其他三个也改了,我又得来一遍。所以就算研究出来了,还是不能稳坐钓鱼台。有没有一了百了的方法嘞。


尝试3


还真找到了:这个错是因为对方网站的证书不被java信任么,那咱不校验了,直接全部信任。这样就算其他接口改了,咱也不愁。而且这个就是获取pdf,貌似安全性没那么重。那就开搞。


代码贴在了下方,上边的大概都能看懂吧,下方的我加了注释。


URL console = new URL(url);
HttpURLConnection conn = (HttpURLConnection) console.openConnection();
if (conn instanceof HttpsURLConnection) {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
((HttpsURLConnection) conn).setSSLSocketFactory(sc.getSocketFactory());
((HttpsURLConnection) conn).setHostnameVerifier(new TrustAnyHostnameVerifier());
}
conn.connect();
InputStream inputStream = conn.getInputStream();
PdfReader pdfReader = new PdfReader(inputStream);
inputStream.close();
conn.disconnect();

private static class TrustAnyTrustManager implements X509TrustManager {
//这个方法用于验证客户端的证书。在这里,方法体为空,表示不对客户端提供的证书进行任何验证。
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法用于验证服务器的证书。同样,方法体为空,表示不对服务器提供的证书进行任何验证。
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法返回一个信任的证书数组。在这里,返回空数组,表示不信任任何证书,也就是对所有证书都不做任何信任验证。
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
//这个方法用于验证主机名是否可信。在这里,无论传入的主机名是什么,方法始终返回 true,表示信任任何主机名。这就意味着对于 SSL 连接,不会对主机名进行真实的验证,而是始终接受所有主机名。
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}

解决了解决了,这样改算是个比较不错的方案了吧。


作者:奔跑的毛球
来源:juejin.cn/post/7362587412066893834
收起阅读 »

大屏页面崩溃排查(Too many active WebGL contexts. Oldest context will be lost)

web
1 问题背景 顾问反馈大屏在反复切换的过程中,页面会出现白屏,导致后续的操作无法进行,只能通过刷新页面才能恢复正常。 我们的页面类似于这样的布局(下方的是直接从网络上找的截图) 点击下方红线框住的区域,可以展示不同的图表(echarts图表) 区别在于我们的...
继续阅读 »

1 问题背景


顾问反馈大屏在反复切换的过程中,页面会出现白屏,导致后续的操作无法进行,只能通过刷新页面才能恢复正常。



  1. 我们的页面类似于这样的布局(下方的是直接从网络上找的截图

  2. 点击下方红线框住的区域,可以展示不同的图表(echarts图表)

  3. 区别在于我们的主区域不是图片,用的是基于cesium封装的地图(webgl


image.png


2 问题复现


测试同事经过几分钟的快速切换导航后,复现了,报错了如下内容
image.png


问题如果复现了,其实就解决了一半了


3 查找问题


经过复现后,发现除了上面的报错,每当页面崩溃前,chrome总会有下方的warning。然后基于cesium封装的地图就会崩溃。


image.png


翻译成中文:警告:目前有了太多激活的webgl上下文,最早的上下文将会丢失


4 排查问题


经过和地图组的人沟通,得到一个结论WebGL一个页面上最多有16个实例



  • 怀疑echarts在下方菜单切换过程中,没有进行销毁


检查了代码中的echats的页面在销毁的时候,发现都进行了dispose,排除了这个的原因



  • 怀疑起echarts的3d的饼状图
    之前设计师设计了一个3d的饼状图,参考了 3d柄图环图,一个页面上有多这个组件。
    效果如下:


f8a015b3-0b94-4fd2-a3b3-07a745fa401a.gif


5 锁定组件进行验证



  1. 先把一个页面上的所有组件改为上方的饼状图,然后点击导航栏,频繁进行切换,

  2. 页面很快就崩溃了,然后检查这个组件在页面销毁的时候,是否进行dispose
    检查后,发现没有,添加后进行测试,问题依旧

  3. 继续检查发现这个组件导入了echarts-gl,就去ecahrts的github的issues进行搜索,终于找到了一个类似的问题
    github.com/ecomfe/echa…


image.png


加入了类似的代码,进行验证后解决了此问题


6 总结



  1. chrome浏览器中最多有16个webgl的实例。当过多的时候,会把最早创建的实例销毁

  2. 当使用echarts在页面销毁的时候及时进行dispose,释放上下文

  3. 当使用echarts-gl的时候,调用dispose的时候是不生效的,需要找到页面上的canvas,然后手动将上下文释放,类似下方的代码


const canvasArr = myChart.getDom().getElementsByTagName('canvas');
for(let i=0; i<canvasArr.length; i++){
canvasArr[i].getContext('webgl').getExtension('WEBGL_lose_context').loseContext()
}


7 参考文档



作者:pauldu
来源:juejin.cn/post/7351712561672798260
收起阅读 »

不是,哥们,谁教你这样处理生产问题的?

你好呀,我是歪歪。 最近遇到一个生产问题,我负责的一个服务触发了内存使用率预警,收到预警的时候我去看了内存使用率已经到了 80%,看了一眼 GC 又发现还没有触发 FullGC,一次都没有。 基于这个现象,当时推测有两种可能,一种是内存溢出,一种是内存泄漏。 ...
继续阅读 »

你好呀,我是歪歪。


最近遇到一个生产问题,我负责的一个服务触发了内存使用率预警,收到预警的时候我去看了内存使用率已经到了 80%,看了一眼 GC 又发现还没有触发 FullGC,一次都没有。


基于这个现象,当时推测有两种可能,一种是内存溢出,一种是内存泄漏。


好,假设现在是面试,面试官目前就给了这点信息,他问你到底是溢出还是泄漏,你怎么回答?


在回答之前,我们得现明确啥是溢出,啥情况又是泄漏。



  • 内存溢出(OutOfMemoryError):内存溢出指的是程序请求的内存超出了 JVM 当前允许的最大内存容量。当 JVM 试图为一个对象分配内存时,如果当前可用的堆内存不足以满足需求,就会抛出 java.lang.OutOfMemoryError 异常。这通常是因为堆空间太小或者由于某些原因导致堆空间被占满。

  • 内存泄漏 (Memory Leak):内存泄漏是指不再使用的内存空间没有被释放,导致这部分内存无法再次被使用。虽然内存泄漏不会立即导致程序崩溃,但它会逐渐消耗可用内存,最终可能导致内存溢出。


虽然都与内存相关,但它们发生的时机和影响有所不同。内存溢出通常发生在程序运行时,当数据结构的大小超过预设限制时,常见的情况是你要分配一个大对象,比如一次从数据中查到了过多的数据。


而内存泄漏和“过多”关系不大,是一个细水长流的过程,一次内存泄漏的影响可能微乎其微,但随着时间推移,多次内存泄漏累积起来,最终可能导致内存溢出。


概念就是这个概念,这两个玩意经常被大家搞混,所以多嘴提一下。


概念明确了,回到最开始这个问题,你怎么回答?


你回答不了。


因为这些信息太不完整了,所以你回答不了。


面试的时候面试官就喜欢出这种全是错误选项的题目来迷惑你,摸摸你的底子到底怎么样。


首先,为什么不能判断,是因为前面说了:一次 FullGC 都没有。


虽然现在内存使用率已经到 80% 了,万一一次 FullGC 之后,内存使用率又下去了呢,说明程序没有任何问题。


如果没有下去,说明大概率是内存溢出了,需要去代码里面找哪里分配了大对象了。


那如果下去了,能说明一定没有内存泄漏吗?


也不能,因为前面又说了:内存泄漏是一个细水长流的过程。


关于内存溢出,如果监控手段齐全到位的话,你就记住左边这个走势图:



一个缓慢的持续上升的内存趋势图, 最后疯狂触发 GC,但是并没有内存被回收,最后程序直接崩掉。


内存泄漏,一眼定真假。


这个图来自我去年写的这篇文章:《虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。》


里面就是描述了一个内存泄漏的问题,通过分析 Dump 文件的方式,最终成功定位到泄漏点,修复代码。


一个不论多么复杂的内存泄漏问题,处理起来都是有方法论的。


不过就是 Dump 文件分析、工具的使用以及足够的耐心和些许的运气罢了。


所以我不打算赘述这些东西了,我想要分享的是我这次是怎么对应文章开始说的内存预警的。


我的处理方式就是:重启服务。


是的,常规来说都是会保留现场,然后重启服务。但是我的处理方式是:直接执行重启服务的预案。没有后续动作了。


我当时脑子里面的考虑大概是这样的。


首先,这个服务是一个边缘服务,它所承载的数据量不多,其业务已经超过一年多没有新增,存量数据正在慢慢的消亡。代码近一两年没啥改动,只有一些升级 jar 包,日志埋点这类的横向改造。


其次,我看了一下这个服务已经有超过四个月没有重启过了,这期间没有任何突发流量,每天处理的数据呈递减趋势,内存走势确实是一个缓慢上升的过程,我初步怀疑是有内存泄漏。


然后,这个服务是我从别的团队那边接手的一个服务,基于前一点,业务正在消亡这个因素,我也只是知道大概的功能,并不知道内部的细节,所以由于对系统的熟悉度不够,如果要定位问题,会较为困难。


最后,基于公司制度,虽然我知道应该怎么去排查问题,命令和工具我都会使用,但是我作为开发人员是没有权限使用运维人员的各类排查工具和排查命令的,所以如果要定位问题,我必须请求协调一个运维同事帮忙。


于是,在心里默默的盘算了一下投入产出比,我决定直接重启服务,不去定位问题。


按照目前的频率,程序正常运行四五个月后可能会触发内存预警,那么大不了就每隔三个月重启一次服务嘛,重启一次只需要 30s。一年按照重启 4 次算,也就是才 2 分钟。


这个业务我们就算它要五年后才彻底消亡,那么也就才 10 分钟而已。


如果我要去定位到底是不是内存泄露,到底在哪儿泄露的,结合我对于系统的熟悉程度和公司必须有的流程,这一波时间消耗,少说点,加起来得三五个工作日吧。


10 分钟和三五个工作日,这投入产出比,该选哪个,一目了然了吧?


我分享这个事情的目的,其实就是想说明我在这个事情上领悟到的一个点:在工作中,你遇到的问题,不是每一个都必须被解决的,也可以选择绕过问题,只要最终结果是好的就行。


如果我们抛开其他因素,只是从程序员的本职工作来看,那么遇到诸如内存泄漏的问题的时候,就是应该去定位问题、解决问题。


但是在职场中,其实还需要结合实际情况,进行分析。


什么是实际情况呢?


我前面列出来的那个“首先,其次,然后,最后”,就是我这个问题在技术之外的实际情况。


这些实际情况,让我决定不用去定位这个问题。


这也不是逃避问题,这是权衡利弊之后的最佳选择。


同样是一天的时间,我可以去定位这个“重启就能解决”的问题,也可以去做其他的更有价值事情,敲一些业务价值更大的代码。


这个是需要去权衡的,一个重要的衡量标准就是前面说的:投入产出比。


关于“不是所有的问题都必须被解决的,也可以选择绕过问题”这个事情,我再给你举一个我遇到的真实的例子。


几年前,我们团队遇到一个问题,我们使用的 RPC 框架是 Dubbo,有几个核心服务在投产期间滚动发布的时候,流量老是弄不干净,导致服务已经下线了,上游系统还在调用。


当时安排我去调研一下解决方案。


其实这就是一个优雅下线的问题,但是当时资历尚浅,我认真研究了一段时间,确实没研究出问题的根本解决方案。


后来我们给出的解决方案就是做一个容错机制,如果投产期间有因为流量不干净的问题导致请求处理失败的,我们把这些数据记录下来,然后等到投产完成后再进行重发。


没有解决根本问题,选择绕过了问题,但是从最终结果上看,问题是被解决了。


再后来,我们搭建了双中心。投产之前,A,B 中心都有流量,每次投产的时候,先把所有流量从 A 中心切到 B 中心去,在 A 中心没有任何流量的情况下,进行服务投产。B 中心反之。


这样,从投产流程上就规避了“流量老是弄不干净”的问题,因为投产的时候对应的服务已经没有在途流量了,不需要考虑优雅的问题了,从而规避了优雅下线的问题。


问题还是没有被解决,但是问题被彻底绕过。


最后,再举一个我在知乎上看到的一个回答,和我想要表达的观点,有异曲同工之妙:



http://www.zhihu.com/question/63…




这个回答下面的评论也很有意思,有兴趣的可以去翻一下,我截取两个我觉得有意思的:





在职场上,甚至在生活中,一个虽然没有解决方案但是可以被绕过的问题,我认为不是问题。


但是这个也得分情况,不是所有问题都能绕开的,假如是一个关键服务,那肯定不能置之不理,硬着头皮也得上。


关键是,我在职场上和生活中遇到过好多人,遇到问题的时候,似乎只会硬着头皮往上冲。


只会硬着头皮往上冲和知道什么时候应该硬着头皮往上冲,是两种截然不同的职场阶段。


所以有时候,遇到问题的时候,不要硬上,也让头皮休息一下,看看能不能绕过去。


作者:why技术
来源:juejin.cn/post/7417842116506058771
收起阅读 »

谁也别拦我们,网页里直接增删改查本地文件!

web
欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面! 转载请联系作者 Jax。 先来玩玩这个 Demo —— 一个网页端的本地文件管理器。 在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个...
继续阅读 »

欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面!


转载请联系作者 Jax。



先来玩玩这个 Demo —— 一个网页端的本地文件管理器


demo.gif


在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个文件夹来打开。然后就能对其中的文件做各种操作了,包括新建、右键打开、编辑、删除等等。你会发现,在网页上的操作会立刻在本地文件上实际生效。


如果你感觉是:”哟?有点儿意思!“,那么这篇文章就是专门为你而写的,读下去吧。


正所谓「边牧是边牧、狗是狗」,Web 应用早已不是当初的简陋网页了,它进化出了令人眼花缭乱的新能力,同时也在迅速追齐系统原生能力。如 Demo 所示,我们甚至可以把文件管理系统搬进网页,并且简单的 JavaScript 代码就能实现。Web 应用能有这样的威力,得益于浏览器对文件系统 API 的实现。


文件系统(File System Access)API 目前还处于社区草稿状态,但早在 2016 年,WHATWG 社区就已经开始着手设计这一标准,并且八年来一直在持续不断地打磨,迄今已经被各大主流浏览器所实现。


venders.jpeg


这套 API 足够简单易用,你很快就能掌握它,并且让它成为你的得力助手。但由于涉及到文件管理这个课题,它有很多方面都可以挖掘得很深很深,比如读写流、OPFS、安全策略等等。咱们这个专栏本着少吃多餐的原则,不会一次性囊括那么多内容。咱们的这第一铲,就先浅浅地挖一个「憨豆」 —— FileSystemHandle


FileSystemHandle


在文件系统中,文件和文件夹是一个个实体,是所有状态和交互的核心。相对应地,在文件系统 API 中,我们用「FileSystemHandle」这个对象来对实体进行抽象。为了能够简洁地描述,我们在下文中将称之为「憨豆」。我们用对象属性来保存实体的名称、类型,通过执行对象方法来操作实体。


那么 FileSystemHandle 从何而来呢?一般是从用户操作而来。以文件为例,当用户通过文件窗口选择了某个本地文件之后,我们就能从代码层面获取到通向这个文件的入口,从而可以对文件进行管理操作。除此之外,拖拽文件进入也能得到憨豆


属性:name 和 kind


name:无论是文件还是文件夹,必然都有一个名字。


kind:实体的类型,值为 ‘file’ 代表文件;值为 ‘directory’ 代表文件夹。


校验方法 isSameEntry()


用于判断两个憨豆是否代表相同的实体。例如用户上传图片时,先后两次选择的是同一张图片,那么两个憨豆指向的其实是同一个文件。


const [handle1] = await showOpenFilePicker() // 第一次上传
const [handle2] = await showOpenFilePicker() // 第二次选择了同一个文件

const isSame = await handle1.isSameEntry(handle2)
console.log(isSame) // true

该方法也同样适用于文件夹校验。


我们可以借此来检测重复性。


删除方法 remove()


用于删除实体。比如,执行下列代码会现场抽取一个幸运文件来删除:


const [handle] = await showOpenFilePicker()
handle.remove()

但如果我们想要选中文件夹来删除,像上面那样直接调用是会报错并删除失败的。我们需要额外传入配置参数:


handle.remove({ recursive: true })

传参后的删除,是对文件夹里面的嵌套结构进行递归删除,这个设计理念旨在让开发者和用户在删除前知悉操作后果。


权限方法 queryPermission() 和 requestPermission()


用于向用户查询和获取读写权限,可以传入一个参数对象来细分权限类型。


const permission = await handle.queryPermission({ mode: 'read' }) // mode 的值还可以是 readwrite
console.log(permission)// 若值为 'granted',则代表有足够权限

我们在对实体进行操作前,最好总是先查询权限。因此一个最佳实践是把这两个方法封装进通用操作逻辑中。


其他特性


除此之外,FileSystemHandle 还具有一些其他特性,比如可以转化为 indexDB 实例、可以通过 postMessage 传输到 Web Workers 里进行操作等等。我们会在后续的专栏文章中继续顺藤摸瓜地了解它们。


两个子类


到目前为止,FileSystemHandle 所具有的属性和方法都在上面了。你可能也意识到了,单靠这三招两式是不可能实现像 Demo 里那么丰富完备的文件操作的。


没错,这个憨豆只是一个抽象父类,它还有两个子类 FileSystemFileHandleFileSystemDirectoryHandle,这两位才是文件和文件夹实体的真正代言人。对实体的常规操作,几乎都是通过这两个紫憨豆来执行的。


除了从父类继承而来的基因,两个紫憨豆都各自具有独特的属性和方法,满足了开发者对文件和文件夹两种不同实体的不同操作需求。


FileSystemFileHandle


在本专栏的上一篇文章《绕过中间商,不用 input 标签也能搞定文件选择》中,我们曾与文件憨豆有过一面之缘。那时候,我们通过 showOpenFilePicker 获取了文件憨豆,并调用它的 getFile 方法拿到了 文件 Blob


此外,文件憨豆还具有的方法如下:



  • createSyncAccessHandle():用于同步读写文件,但是仅限于在 Web Workers 中。

  • createWritable:创建一个写入流对象,用于向文件写入数据。


FileSystemDirectoryHandle


文件夹憨豆的特有方法如下:



  • getDirectoryHandle():按名称查找子文件夹。

  • getFileHandle():按名称查找子文件。

  • removeEntry():按名称移除子实体。

  • resovle():返回指向子实体的路径。


经过上述罗列,我们对两种憨豆的能力有了一个大概的印象。下面,我们将回到 Demo 的交互场景,从具体操作上手,看看文件系统 API 是如何在 JavaScript 中落地的。


操作 & 用法


载入文件夹


我们首先需要载入一个文件夹,然后才能对其中的子实体进行操作。


如果你碰巧读过上一篇文章,知道如何用 showOpenFilePicker() 选择文件,那你一定能举一反三推断出,选择文件夹的方法是 showDirectoryPicker()


const dirHandle = await showDirectoryPicker()

showDirectoryPicker 方法也接收一些参数,其中 idstartIn 这两个参数与 showOpenFilePicker 方法 的同名参数完全对应。另外还支持一个参数 mode ,其值可以是 readreadwrite,用于指定所需的权限。


用户选择文件夹后得到的 dirHandle,就是一个 FileSystemDirectoryHandle 格式的对象。我们可以遍历出它的子实体:


for await (const sub of dirHandle.values()) {
const { name, kind } = sub
console.log(name, kind)
}

从子实体中取出名称和类别属性值,就可以对文件夹内容一目了然了。


读取文件内容


在上一步中,我们已经读取到了子实体的名字和类型,那么对于文件类型,我们可以先按名称检索到对应的憨豆:


// if sub.kind === 'file'
const fileHandle = await dirHandle.getFileHandle(sub.name)

再从文件憨豆中掏出文件 Blob,进一步读取到文件的内容:


const file = await fileHandle.getFile()
const content = file.text()

如果你用来调试的文件是文本内容的文件,那么打印 content 的值,你就可以看到内容文本了。


同理,获取子文件夹的方式是 dirHandle.getDirectoryHandle(sub.name)


新建文件、文件夹


除了指定名称参数,getFileHandlegetDirectoryHandle 这两个方法还支持第二个参数,是一个一个配置对象 { create: true/false },用于应对指定名称的实体不存在的情况。


例如,我们对一个文件夹实体执行 dirHandle.getFileHandle('fileA'),但该文件夹中并没有名为 fileA 的文件。此时第二个参数为空,等同于 create 的默认值为 false,那么此时会抛出一个 NotFoundError 错误,提示我们文件不存在。


而如果我们这样执行:dirHandle.getFileHandle('fileA', { create: true }),那么就会在当前文件夹中新建一个名为 fileA 的空文件。


同理,我们也可以用 dirHandle.getDirectoryHandle('dirA', { create: true }) 新建一个名为 dirA 的空文件夹。


在 Demo 中,我们实现了让用户在新建文件时自定义文件名,这其实是使用了 prompt 方法:


const fileName = prompt('请输入文件名')
await dirHandle.getFileHandle(fileName, { create: true })

在当下这个 AI 时代,Prompt 更多是以「LLM 提示词」被大家初识。在更早时候,它是浏览器实现的一个组件。其实这两种形态的含义是一致的,都是在人机交互中,从一端到另一端的输入输出流程。


编辑文件内容


刚刚我们读取了文件的内容,现在我们来对文件内容进行修改,然后再存回去。


我们已经能够通过 getFile() 方法拿到文本内容,那应该把内容放到哪里进行编辑呢?你有很多种选择:富文本编辑器、给 div 设置 contenteditable、唤起 VS Code…… 但本着最(能)小(用)原(就)则(行),我们还有更便捷的选项 —— prompt!


prompt() 方法也支持第二个参数,我们把文本内容传入,弹出弹窗后,你就会看到内容已经填在了输入框中,现在就可以随意编辑里面的字符了。


const file = await fileHandle.getFile()
const fileContent = await file.text()
const newContent = prompt('', fileContent) // newContent 即为修改后的文件内容

但是点击 Prompt 的确认按钮并不会让新内容自动写入文件,这里就需要用到上面提到的 createWritable 了。下面是一个完整的写入流流程:


const writable = await fileHandle.createWritable() // 对待修改文件开启写入流
await writable.write(newContent) // 把新内容写入到文件
await writable.close() // 关闭写入流

至此,新的内容就已经被保存到文件中了。你可以在 Demo 中再次右键、打开文件查看内容,你会发现确实读取到了修改后的内容。


文件重命名


修改文件名也是文件管理中的常规操作,文件系统 API 也提供了对应的方法来实现。可能手快的同学已经在尝试 fileHandle.rename() 方法了。但 API 中还真没有这个方法,我们其实是要用一个 move() 方法。惊不惊喜意不意外?


因为从底层视角看,重命名文件和移动文件的处理过程类型,都是需要先新建一个文件(使用新的命名或者放到新的位置),再把源文件的数据复制过去,最后把源文件删除。目前在 Web 端还没有更高效的处理方式。


我们只需从 Prompt 获取新名称,再传给 move() 方法即可:


const newName = prompt('请输入新的文件名')
await fileHandle.move(newName)

这样,文件重命名就搞定了。


删除文件、文件夹


删除实体就没有那么多幺蛾子了,我们甚至不用对文件和文件夹做逻辑上的区分,简单直接地调用 currentHandle.remove({ recursive: true }) 就行了。


但越是方便,就越要谨慎,因为涉及到删除用户的文件,所以如果要用于生产环境,最好给用户提供二次确认的机会。


写在结尾


恭喜你读完了本文,你真棒!


这次我们通过实现一个 Web 版文件管理器 demo,对文件管理 API 进行了较为深入的理解和实践。我再嘱咐两句:



  1. 涉及到操作用户文件,请务必谨慎。

  2. 为了保障安全性,文件系统 API 仅支持 https。



我是 Jax,在畅游 Web 技术领域的第 7 年,我仍然是坚定不移的 JavaScript 迷弟,Web 开发带给我太多乐趣。如果你也喜欢 Web 技术,或者想讨论本文内容,欢迎来聊!你可以通过下列方式找到我:


掘金:juejin.cn/user/113435…


GitHub:github.com/JaxNext


微信:JaxNext



作者:JaxNext
来源:juejin.cn/post/7416933490136252452
收起阅读 »

35 岁时我改掉的三个习惯

大家好,我是双越老师,wangEditor 作者。 我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。 开始 虽然标题是 35 岁,但其实本文 202...
继续阅读 »

大家好,我是双越老师,wangEditor 作者。



我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。



开始


虽然标题是 35 岁,但其实本文 2024 年我 36 岁时写的。36 岁总结 35 岁,也没毛病。


35 岁对于我们程序员来说是一个非常敏感的年龄,但我已过 35 ,看看往后会越来越敏感,还是越来越麻木?


本文和大家分享一下我个人在业余生活中的,我觉得很有意义的事情。甚至我觉得这些是事情的意义,都大于工作的成绩。


生活是一个整体,工作只是其中一部分,只是我们养家糊口的一个手段。工作以外的其他部分应该更加重要,例如业余爱好、饮食、休息、娱乐、家庭等。


1. 戒烟


我从大学毕业开始学着吸烟,到 35 岁已经有十几年的烟龄了。虽然每天吸烟量不大,但也断不了,有瘾。


我为什么要戒烟呢?


是觉得吸烟有害健康?—— 对,吸烟肯定有害健康,但人类天生是一种及时行乐的动物,谁会在乎 20 年 30 年以后的健康呢?人类天生是一个心存侥幸的动物(赌徒性质),也不是每个吸烟者都会有 xxx 病,对吧?之前看过一个段子,说某呼吸外科医生做完,累几个小时做完一台手术,先出去吸烟休息一下。


我戒烟,单纯就是想戒。我想摆脱烟草和尼古丁的控制,而且是想了很久了,不是一时突发奇想,之前就有充分的心理准备。


还有,我想在 35+ 的年纪做一点叛逆的事情,叛逆使人年轻,叛逆使人保持活力。年轻时吸烟是叛逆,年龄大了戒烟就是叛逆。年轻时叛逆是狂,年龄大了叛逆是帅。所以,各位大龄程序员,有机会要让自己叛逆起来,做一点帅的事情。


最后,当时 2023 年夏天,我正好不忙,天天闲着,总得找点事儿干。既然工作上不忙,那就在自己身上找点有意义事情做吧 —— 外求不得而向内求,如果没法从外面挣取,那就去提高自身。


烟瘾是什么?就是尼古丁的戒断反应,没有其他理由,没人其他任何事情会让你 1-2 小时就想一次,而且持续想。


关于烟草的本质,烟草的历史,烟草的商业化推广过程,烟草的洗脑广告…… 还有很多内容可以讲,本文就不展开了,有兴趣的可以留言咨询。我很早之前就看过,去学习这些,并且接受这些,才能更好的帮助戒烟。


所以,就这么戒了,到写作本文的时候,正好是戒了一年。我觉得这是我去年做过的最有价值的事情,比我工作挣的钱都有价值。


2. 戒酒


之前我是比较喜欢喝酒的,喜欢一帮人聚餐那种兴奋的状态。但后来喝多了就肚子难受,一躺下就想吐,于是决定不喝了。


有些人说:你可以少喝点。但在中国北方的酒桌上,只有 0 和 1 ,没有中间态。只要你喝了,一开始朋友劝你多喝点,再喝多一点就不用别人劝了,自己就开始主动找酒瓶子了。


我不懂酒,没喝过啥好酒,很少喝红酒。就日常喝的白酒和啤酒而言,我觉得都不好喝。


白酒,度数高,辣(尤其酱味的),容易醉。全世界就中国及其周边人喝白酒,国内几千的白酒没有国际市场。而且单就中国而言,白酒蒸馏技术几百年了,也只有最近这不到 100 年喝白酒。《红楼梦》上层人不喝白酒,《孔乙己》下层人也不喝白酒。


现在喝白酒的人,有两类:1. 被酒桌文化感染而顺从的人; 2. 有酒瘾想快速体验酒精的人。


啤酒,要分好多种,咱日常喝的瓶装的、桶装的,都可以统称为工业啤酒,像水一样,轻薄寡淡 —— 但它有酒精啊!


那种全麦啤酒(忘记名字了,不常喝)还是很好喝的,但价格较高,自己买点喝还行,聚餐喝那个太贵了(普通饭店也没有卖的),很少喝。


我身边也有一些朋友,每周都喝好几次,大部分是为了工作,拉拢客户关系。我觉得我还是比较幸运的,写写代码,改改 bug ,也不用太考虑人际关系。程序员的为数不多的好处。


3. 不看和自己无关的事情


我从不刷抖音(虽然我会发布一些抖音视频),但我之前很喜欢看今日头条 app ,每天闲了就刷一下,吃饭也看,睡前也看。


但是我都看了些啥呢?有一些是娱乐性的小视频,搞笑的,猎奇的,做饭吃饭的,我觉得这些很好,提供情绪价值。


其他的,什么俄 x 战争,什么国外 xxx 冲突,什么体育明星谁比谁厉害,什么传统武术,什么中医 …… 还有这些的评论,各路网友互怼。有时候看的都让人很带情绪,感觉有些人是不是傻,这么简单的道理看不明白?有些人是不是坏,不看事实,只看立场?


这些不仅不能提供情绪价值,反而会增加你的负面情绪。而且,根据《乌合之众》大众心理学研究,你只要参与了其中,你参与了互怼,你就会成为他们其中的一员,也变成傻子或坏人。围观,也是一种参与,你的心里会支持某一方。


更关键的是,这些事情我自己有关系吗?或者我的表态能决定这件事儿的走向吗?哪怕投一票决定一点点呢 —— 答案是显然的,要么没有任何关系,要么自己瞎操心。


所以,我卸载了今日头条 app ,不看了,眼不见心不烦,这些事情我不知道,也不影响我个人工作生活。从此以后,我觉得我的世界瞬间清净了,至少不会被那些负面情绪所打扰。


另外,我睡前也不看手机了,把手机扔在书房充电,直接睡觉。如果偶尔失眠或想事情,那就想,也不用非得拿着手机想,对吧。


总结


35 岁是一个里程碑和转折点,工作上如此,生活中也是如此。程序员是一个相对来说比较“单纯”的群体,我觉得更应该关注个人生活中的成长,共勉,加油~


作者:前端双越老师
来源:juejin.cn/post/7417630844100247590
收起阅读 »

BOE(京东方)携故宫博物院举办2024“照亮成长路”公益项目落地仪式以创新科技赋能教育可持续发展

2024年9月20日,BOE(京东方)“照亮成长路”智慧教室落成暨百堂故宫传统文化公益课山西活动落地仪式在山西省太原市娄烦县实验小学隆重举行。自“照亮成长路”教育公益项目正式设立以来,BOE(京东方)持续以创新科技赋能偏远地区的教育升级,随着2024年捐赠的2...
继续阅读 »

2024年9月20日,BOE(京东方)“照亮成长路”智慧教室落成暨百堂故宫传统文化公益课山西活动落地仪式在山西省太原市娄烦县实验小学隆重举行。自“照亮成长路”教育公益项目正式设立以来,BOE(京东方)持续以创新科技赋能偏远地区的教育升级,随着2024年捐赠的23间智慧教室全面圆满竣工并正式投入使用,BOE(京东方)捐建的智慧教室总数已达126间,它们不仅代表了教育创新、文化传承与先进技术的融合,也开启了BOE(京东方)面向新三十年发展征程、积极践行企业社会责任的新起点。

故宫博物院作为“照亮成长路”公益项目的重要合作伙伴,一直致力于促进中华优秀传统文化在青少年中的普及与传播。尤其是与京东方科技集团共同发起的“百堂故宫传统文化公益课”项目是故宫博物院教育推广的又一次重要实践。项目启动后近一年间已为26所学校,2万余名学生送去了400余场线上公益课程。此次落地的山西娄烦实验小学和静乐君宇中学也成为该计划的线下落地试点学校。活动现场,娄烦县委副书记、县长景博,娄烦县委常委、常务副县长任瑛,娄烦县委常委、副县长李学斌,静乐县副县长许龙平,中国乡村发展基金会副秘书长丁亚冬,故宫博物院副院长朱鸿文,京东方科技集团执行副总裁、艺云科技董事长姚项军,京东方科技集团副总裁、首席品牌官司达等出席了本次仪式,共同见证这一重要时刻。

在活动现场,京东方科技集团执行副总裁姚项军表示:“教育数字化是推进教育现代化的关键力量。BOE(京东方)充分发挥自身在物联网创新领域的专长,通过首创的多项类纸护眼显示技术,制定的低蓝光健康显示技术国际标准,推出了一系列智慧校园产品与服务;同时还充分发挥企业产业优势,开发科学与工程教育产品,用科学创新实践支持做公益。BOE(京东方)将携手各界同仁开启‘照亮成长路’教育公益项目的下一个十年篇章,继续推动教育与科技的深度融合,迈向一个更加智慧、更加光明、更加美好的未来!”

中国乡村发展基金会副秘书长丁亚冬在致辞中表示:“BOE(京东方)是我们多年的合作伙伴,持续关注乡村数字化教育的发展,携手实施的‘照亮成长路’教育公益项目已改造完成126间智慧教室,用科技力量助力消弭教育鸿沟,照亮乡村学生的成长之路。未来,我们将继续与BOE(京东方)、故宫博物院及社会各界通力合作,充分发挥各自优势,着力推进教育公平,促进教育均衡发展,为助力全面乡村振兴做出不懈努力。”

故宫博物院副院长朱鸿文在致辞中表示:“故宫博物院作为中华民族五千多年文明的重要承载者、中华优秀传统文化的汇聚地,始终将传承弘扬中华优秀传统文化作为己任,不断探索创新,希望通过丰富多彩的博物馆教育项目,将中华优秀传统文化传递给广大观众。很高兴能够携手京东方这样的科技企业通过传统文化振兴乡村发展,在以科技赋能偏远地区提升数字化水平的基础上,融入传统文化教育,增强师生文化自信,建设文化强国,助力中华民族伟大复兴。”故宫博物院也在活动中向学校的全体师生赠送了《我要去故宫》系列图书。

本次活动上,中国乡村发展基金会副秘书长丁亚冬,京东方科技集团副总裁、首席品牌官司达,娄烦县委副书记、县长景博,静乐县副县长许龙平,故宫博物院社会教育部主任吕晓刚,艺云科技智慧校园事业部总经理李慧军,共同为2024年“照亮成长路”项目新建的23间智慧教室举行了揭牌仪式。仪式结束后,故宫博物院社教人员还在新落成的智慧教室中,通过生动有趣的互动式教学,将故宫蕴含的中华优秀传统文化展现给孩子们,课堂上孩子们积极与老师交流,并动手制作多种手工材料包,获得了一份来自故宫的珍贵文化礼物。同时,BOE(京东方)志愿者也为孩子们带来了生动有趣的科学实践课,通过讲解屏幕显示的原理,让孩子们充分了解屏幕背后的技术知识,感受显示科技的精妙;此外,还设置了小组实践环节,模拟工厂流水线,让孩子们合作组装屏幕像素模拟装置,在动手中加深对知识的理解,在体验中收获知识,在实践中收获成长。

2024 BOE(京东方)“照亮成长路”教育公益项目的成功落地与本次活动的顺利举办,得益于山西省娄烦县政府和故宫博物院的大力支持,也离不开中国乡村发展基金会在项目推进过程中的通力合作。此次活动过程中,各方领导嘉宾围绕科技文化在教育领域的融合应用、智慧教育的未来趋势以及公益事业的长足发展进行了深入探讨。娄烦县政府相关领导作为代表对BOE(京东方)“百所校园”的公益新里程表示肯定与祝贺,并祝愿“照亮成长路”及“百堂故宫传统文化公益课”在未来能够惠及更多校园,助力更多偏远地区师生了解优秀传统文化、体验智慧教育。

作为一家全球化的科技公司,BOE(京东方)坚持Green+、Innovation+、Community+可持续发展理念,在教育、文化、健康等领域积极开展公益活动,通过引领绿色永续发展、持续驱动科技创新、赋能整个产业和社会。其中,“照亮成长路”是BOE(京东方)2014年启动的教育公益项目,通过智慧教室建设、教育资源融合、教师赋能培训计划等,携手社会各界力量,将技术融入社区公益发展与乡村振兴事业。目前,BOE(京东方)已在全国8大省市地区建成126间智慧教室,为63500余名师生提供软硬融合的智慧教育解决方案和教师赋能计划,切实帮助偏远地区学生群体获得更优质的教育和成长机会,在缩小城乡间数字差距、推动区域教育现代化、促进社会全面进步方面彰显了重要价值。

作为“照亮成长路”的特色项目,“百堂故宫传统文化公益课”让偏远地区的孩子能够通过BOE(京东方)的智慧教育创新技术跨越时间和空间的限制,近距离感受故宫的魅力,了解中国传统文化的精髓。接下来,更多课程将陆续在更为广泛的偏远地区展开,到2025年故宫博物院建院百年之际,双方将联手在北京故宫博物院为孩子们带来第100堂特别课程。

未来,BOE(京东方)与故宫博物院也将继续携手,以科技和文化双重赋能教育,让知识的光芒照亮每一个孩子的未来。

收起阅读 »

独家授权!广东盈世获网易邮箱反垃圾服务的独家授权,邮件反垃圾更全面

近日,广东盈世计算机科技有限公司(以下简称“Coremail”)成功获得了网易(杭州)网络有限公司(以下简称“网易”)授予的网易邮箱反垃圾服务独家使用权。这一授权使得Coremail能够在邮件安全产品上运用网易邮箱反垃圾服务,进一步强化其反垃圾能力,邮件反垃圾...
继续阅读 »

近日,广东盈世计算机科技有限公司(以下简称“Coremail”)成功获得了网易(杭州)网络有限公司(以下简称“网易”)授予的网易邮箱反垃圾服务独家使用权。这一授权使得Coremail能够在邮件安全产品上运用网易邮箱反垃圾服务,进一步强化其反垃圾能力,邮件反垃圾更全面。

凭借24年的反垃圾反钓鱼技术沉淀,Coremail邮件安全致力于提供一站式邮件安全解决方案,为用户提供安全、可靠的安全解决方案。而网易作为国内邮箱行业的佼佼者,拥有强大的技术实力和丰富的经验,其网易邮箱反垃圾服务更是享有盛誉。

通过合作,网易为Coremail提供Saas在线网关服务,进行进信和外发的在线反垃圾检测。Coremail邮件安全反垃圾服务将以自研反垃圾引擎为主,网易反垃圾服务为辅,以“双引擎”机制保障用户享有最高等级的邮件反垃圾服务。

此外,除网易自身、广州网易计算机系统有限公司及其关联公司外,Coremail是唯一被授权在服务期内独家使用网易邮箱反垃圾服务的公司。这一独家授权充分体现了网易对Coremail的高度认可和信任,同时也彰显了Coremail在邮件安全领域的卓越实力。

438d0df1a5824e3eb3cec6ffdfd1e1e9.jpg

此次独家授权,为Coremail带来更多的技术优势和市场竞争优势,进一步巩固其在邮件安全领域的领先地位。同时,对于广大用户来说,这也意味着用户将能够享受到更加安全、高效的邮件安全服务。

未来,Coremail将继续秉持技术创新的精神,致力于为用户提供更优质、安全、智能的邮件安全服务。与此同时,Coremail也将与网易保持紧密的合作关系,为企业的邮件安全保驾护航。

收起阅读 »

微信小程序避坑scroll-view,用tween.js实现吸附动画

web
背景 在开发一个小程序项目时,遇到一个需要列表滚动,松手时自动吸附的动画需求,如下(最终效果): 很自然用了scroll-view组件,但使用过程中发现scroll-view 里包含'position: fixed;top:0;'的元素时,应用scroll-...
继续阅读 »

背景


在开发一个小程序项目时,遇到一个需要列表滚动,松手时自动吸附的动画需求,如下(最终效果):


吸附动画.gif


很自然用了scroll-view组件,但使用过程中发现scroll-view 里包含'position: fixed;top:0;'的元素时,应用scroll-with-animation=true,搭配更改scroll-top时,松手后fixed的元素会抖一下......


问题.gif


于是决定不用组件内置的scroll-with-animation,改用手动控制scroll-top实现吸附的效果。


思路


通常,要做动画,我们就得确定以下信息,然后用代码实现:



  • 初始状态

  • 结束状态

  • 动画时长

  • 动画过程状态如何变化(匀速/先加速后减速/...)


这四个信息一般从UI/交互那里确认,前三个代码实现很简单,第四个在css动画里(如transition/animation)用 timing-function 指定:


image.png


在js动画里,改变css的属性可通过 Web Animations API 里的 easing 属性指定:


image.png


而如果需要动画的状态不是css的属性呢(例如上面的scrollTop)?这就要用到补间/插值工具了,tween.js登场!


关于 tween.js


tween翻译有‘补间‘的意思



补间(动画)(来自 in-between)是一个概念,允许你以平滑的方式更改对象的属性。你只需告诉它哪些属性要更改,当补间结束运行时它们应该具有哪些最终值,以及这需要多长时间,补间引擎将负责计算从起始点到结束点的值。



简单点就是tweenjs可以指定状态从初始值到结束值该怎么变化,下面是简单的使用例子:


const box = document.getElementById('box') // 获取我们想要设置动画的元素。
const coords = {x: 0, y: 0} // 从 (0, 0) 开始

const tween = new TWEEN.Tween(coords, false) // 创建一个修改“坐标”的新 tween。
.to({x: 300, y: 200}, 1000) // 在 1 秒内移动到 (300, 200)。
.easing(TWEEN.Easing.Quadratic.InOut) // 使用缓动函数使动画流畅。
.onUpdate(() => {
// 在 tween.js 更新“坐标”后调用。
// 使用 CSS transform 将 'box' 移动到 'coords' 描述的位置。
box.style.setProperty('transform', 'translate(' + coords.x + 'px, ' + coords.y + 'px)')
})
.start() // 立即开始 tween。

// 设置动画循环。
function animate(time) {
tween.update(time)
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)


在微信小程序里使用tween.js


导入适配


下载 github.com/tweenjs/twe… 文件,修改下‘now’的实现,把performance.now()改成Date.now() 即可在小程序里使用:


image.png


动画循环


小程序里没有直接支持requestAnimationFrame,这个可以用canvas组件的requestAnimationFrame方法代替:


    // wxml
// ...
<canvas type="2d" id="canvas" style="width: 0; height: 0; pointer-events: none; position: fixed"></canvas>
// ...


// js
wx.createSelectorQuery()
.select("#canvas")
.fields({
node: true,
})
.exec((res) => {
this.canvas = res[0].node;
});
// ...

// ...
const renderLoop = () => {
TWEEN.update();
this.canvas.requestAnimationFrame(renderLoop);
};
renderLoop();

其他


锁帧


手动改scrolltop还是得通过setData方法,频繁调用可能会导致动画卡顿,requestAnimationFrame一般1s跑60次,也就是60fps,根据需要可以增加锁帧逻辑:


const fps = 30; // 锁30fps
const interval = 1000 / fps;
let lastTime = Date.now();
const renderLoop = () => {
this.canvas.requestAnimationFrame(renderLoop);

const now = Date.now();
if(now - lastTime > interval){
// 真正的动作在这里运行
TWEEN.update();
lastTime = now;
}
};
renderLoop();

官方支持?


要是 scrollView 组件支持 wxs 更改scrollTop就好了
developers.weixin.qq.com/community/d…


作者:思路为王
来源:juejin.cn/post/7300771357523820594
收起阅读 »

前端滑块旋转验证登录

web
效果图如下 实现: 封装VerifyImg组件 <template> <el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog"> ...
继续阅读 »
效果图如下

效果.gif


实现: 封装VerifyImg组件

<template>
<el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog">
<div class="verify-v">
<div
class="check"
@mousedown="onMouseDown"
@mouseup="onMouseUp"
@mousemove="onMouseMove"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>

<p>拖动滑块使图片角度为正</p>
<div class="img-con">
<img :src="imgUrl" :style="{ transform: imgAngle }" />
<div v-if="showError" class="check-state">验证失败</div>
<div v-else-if="showSuccess" class="check-state">验证成功</div>
<div v-else-if="checking" class="check-state">验证中</div>
</div>
<div
ref="sliderCon"
class="slider-con"
:class="{ 'err-anim': showError }"
:style="{ '--percent': percent, '--bgColor': showError ? bgError : bgColor }"
>

<div ref="slider" class="slider" id="slider" :class="{ sliding }" :style="{ '--move': `${slidMove}px` }">
<el-icon size="22"><Right id="slider" /></el-icon>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
imgUrl: '',
dialogShow: false,
showError: false,
showSuccess: false,
checking: false,
sliding: false,
slidMove: 0,
percent: 0,
sliderConWidth: 0,
bgColor: 'rgba(25, 145, 250, 0.2)',
bgError: 'rgba(255,78,78,0.2)',
imgList: [
new URL(`../../assets/images/verify/fn1.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn2.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn3.png`, import.meta.url).href
]
}
},

computed: {
angle() {
let sliderConWidth = this.sliderConWidth ?? 0
let sliderWidth = this.sliderWidth ?? 0
let ratio = this.slidMove / (sliderConWidth - sliderWidth)
return 360 * ratio
},
imgAngle() {
return `rotate(${this.angle}deg)`
}
},
mounted() {
this.imgUrl = this.imgList[this.rand(0, 2)]
},

methods: {
onTouchMove(event) {
console.log('onTouchMove')
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.touches[0].clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
console.log(this.slidMove)

this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
onTouchEnd() {
console.log('onTouchEnd')
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)

// alert('旋转错误')
})
}
},
onTouchStart(event) {
console.log('onTouchStart', event)
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.touches[0].clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
console.log(this.sliderLeft, this.sliderConWidth, this.sliderWidth)
},
rand(m, n) {
return Math.ceil(Math.random() * (n - m + 1) + m - 1)
},

showVerify() {
this.imgUrl = this.imgList[this.rand(0, 2)]
this.dialogShow = true
},
closeVerify() {
//1.5s后关闭弹框
setTimeout(() => {
this.dialogShow = false
}, 1500)
},
// 重置滑块
resetSlider() {
this.sliding = false
this.slidMove = 0
this.checking = false
this.showSuccess = false
this.showError = false
this.percent = 0
},
//拖拽开始
onMouseDown(event) {
console.log(event.target.id, this.checking)
if (event.target.id !== 'slider') {
return
}

if (this.checking) return
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
},
//拖拽停止
onMouseUp(event) {
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)

// alert('旋转错误')
})
}
},
//拖拽进行中
onMouseMove(event) {
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
// 验证角度是否正确
validApi(angle) {
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
// 图片已旋转的角度
const imgAngle = 90
// 图片已旋转角度和用户旋转角度之和
let sum = imgAngle + angle
// 误差范围
const errorRang = 20
// 当用户旋转角度和已旋转角度之和为360度时,表示旋转了一整圈,也就是转正了
// 但是不能指望用户刚好转到那么多,所以需要留有一定的误差
let isOk = Math.abs(360 - sum) <= errorRang

resolve(isOk)
}, 1000)
})
}
}
}
</script>

<style lang="scss">
.verifyDialog {
.el-dialog__body {
padding: 15px !important;
}
}
</style>
<style lang="scss" scoped>
.verify-v {
display: flex;
justify-content: center;
align-items: center;
}
.check {
--slider-size: 40px;
width: 300px;
background: white;
box-shadow: 0px 0px 12px rgb(0 0 0 / 8%);
border-radius: 5px;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
.img-con {
position: relative;
overflow: hidden;
width: 120px;
height: 120px;
border-radius: 50%;
margin-top: 20px;
img {
width: 100%;
height: 100%;
user-select: none;
}
.check-state {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.check .slider-con {
width: 80%;
height: var(--slider-size);
border-radius: 3px;
margin-top: 1rem;
position: relative;
background: #f5f5f5;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1) inset;
background: linear-gradient(to right, var(--bgColor) 0%, var(--bgColor) var(--percent), #fff var(--percent), #fff 100%);
.slider {
&:hover {
background: #1991fa;
color: #fff;
}
background: #fff;
width: var(--slider-size);
height: var(--slider-size);
border-radius: 3px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
cursor: move;
display: flex;
justify-content: center;
align-items: center;
--move: 0px;
transform: translateX(var(--move));
.sliding {
background: #4ed3ff;
}
}
}
.slider-con.err-anim {
animation: jitter 0.5s;
.slider {
background: #ff4e4e;
}
}
body {
padding: 0;
margin: 0;
background: #fef5e0;
}

@keyframes jitter {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(10px);
}
60% {
transform: translateX(-5px);
}
80% {
transform: translateX(10px);
}
100% {
transform: translateX(0);
}
}
</style>


使用

<VerifyImg ref="verifyRef" @to-login="handleLogin"></VerifyImg>


handleLogin(){
...
}

作者:点赞侠01
来源:juejin.cn/post/7358004857889275958
收起阅读 »

API接口超时,网络波动,不要一直弹Alert了!

web
前言前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时,服务器错误等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误。由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请...
继续阅读 »

前言

前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时服务器错误等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误

由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请求&返回数据量大、时间长。在后端添加索引的情况下,接口仍然达不到秒级,接口普遍都是1~3s,部分接口甚至达到小10s。我司把服务器的硬盘和CPU都进行了一波升级,效果都不是很理想。

这个烫手的山芋,落在了前端的头上。我们被迫进行了一波系统升级优化

解决方案

我们结合这个需求,制定了以下几条标准:

  1. 不能入侵其他的功能
  2. 对系统的破坏尽可能的小
  3. 杜绝或者尽可能的减少弹框问题
  4. 保证数据的正确展示,对于错误要正确的暴露出来

根据以上几条标准,于是方案就自然的确定了:

API请求时间

拉长API的请求时间,将超时时间由30s,更新为60s

const service = axios.create({
baseURL: '/xxx',
timeout: 60 * 1000 // 请求超时时间
})

重发机制

  1. API请求超时: 当新页面大量接口请求,某个接口请求时间过长,总时间>60s时,我们会对这个接口进行至多重发3次,用180s的时间去处理这个接口,当请求成功后,关闭请求

重发的接口不能超过timeout时长,否则一直会重发失败。也可以自定义重发的时间

  1. 偶发的服务器异常: 当接口出现50X时,重发一次

可以使用axois自带的方法,也可以使用axios-retry插件,axios-retry插件更简单,底层也是axois方法实现的。相比较而言,axios-retry更简单,但不知为什么,我没有实现

// request.js
// 默认重发3次,每次的间隔时间为3s
service.defaults.retry = 3;
service.defaults.retryDelay = 3000;

export function againRequest(
error,
axios,
time = error.config.retry
) {
const config = error.config;

if (!config || !config.retry) return Promise.reject(error);

// 设置用于记录重试计数的变量 默认为0
config.__retryCount = config.__retryCount || 0;

// 判断是否超过了重试次数
if (config.__retryCount >= time) {
// alert

return Promise.reject(error);
}

config.__retryCount += 1;

const backoff = new Promise(resolve => {
setTimeout(() => {
resolve();
}, config.retryDelay || 1);
});

return backoff.then(() => {
/*
以下三行,根据项目实际调整
这三行是处理重发请求接口变成string,并且重定向的问题
*/

if (config.data && isJsonStr(config.data)) {
config.data = JSON.parse(config.data);
}
config.baseURL = "/";

return axios(config);
});
}

export let isLoop = config => {
if (
config.isLoop &&
config.isLoop.count >= 0 &&
config.url == config.isLoop.url
) {
return true;
}
return false;
};

export let isJsonStr = str => {
if (typeof str == "string") {
try {
var obj = JSON.parse(str);
if (typeof obj == "object" && obj) {
return true;
} else {
return false;
}
} catch (e) {
console.log("error: " + str + "!!!" + e);
return false;
}
}
};

注意到是: axois不能是0.19.x

issue中提到使用0.18.0。经过实践,0.20.x也是可以的,但需要进行不同的配置,具体请看issue。参考资料中有提到 axios issues github

也可以使用axios-retry

axios-retry

npm install axios-retry

// ES6
import axiosRetry from 'axios-retry';

axiosRetry(axios, { retries: 3 });

取消机制

当路由发生变化时,取消上一个路由正在请求的API接口

监控路由页面: 调用cancelAllRequest方法

// request.js
const pendingRequests = new Set();

service.cancelAllRequest = () => {
pendingRequests.forEach(cancel => cancel());
pendingRequests.clear();
};

轮询

轮询有2种情况,一种是定时器不停的请求,一种是监听请求N次后停止。

比如: 监听高低电平的变化 - 如快递柜的打开&关闭。

  1. 一直轮询的请求:

    • 使用WebSocket
    • 连续失败N次后,谈框。
  2. 轮询N次的请求:

    • 连续失败N次后,谈框。
export function api(data, retryCount) {
return request({
url: `/xxx`,
method: "post",
isLoop: {
url: "/xxx",
count: retryCount
},
data: { body: { ...data } }
});
}

自定义api url的原因是:

同一个页面中,有正常的接口和轮询的接口,url是区分是否当前的接口是否是轮询的接口

监听滚动

对于图表类的功能,监听滚动事件,根据不同的高度请求对应的API

节流机制

  1. 用户连续多次请求同一个API
    • 按钮loading。最简单有效
    • 保留最新的API请求,取消相同的请求

错误码解析

网络错误 & 断网

if (error.toString().indexOf("Network Error") !== -1) {
if (configData.isLoop && configData.isLoop.count >= 0) {
networkTimeout();
// 关闭所有的定时器
let t = setInterval(function() {}, 100);
for (let i = 1; i <= t; i++) {
clearInterval(i);
}
return;
}
networkTimeout();
}

404

else if (error.toString().indexOf("404") !== -1) {
// 404
}

401

else if (error.toString().indexOf("401") !== -1) {
// 清除token,及相应的数据,返回到登录页面
}

超时

else if (error.toString().indexOf("Error: timeout") !== -1) {
if (!axios.isCancel(error) && !isLoop(configData)) {
return againRequest(error, service);
}
if (configData.isLoop && configData.isLoop.count === 3) {
requestTimeout();
}
}

50X

else if (error.toString().indexOf("50") !== -1) {
if (!axios.isCancel(error) && !isLoop(configData)) {
return againRequest(error, service, 1, 2);
}
if (configData.isLoop && configData.isLoop.count === 1) {
requestTimeout();
}
}

未知错误

else {
// 未知错误,等待以后解析
}

总结

结果将状态码梳理后,客户基本看不到API错误了,对服务的稳定性和可靠性非常满意,给我们提出了表扬和感谢。我们也期待老板升职加薪!

参考资料


作者:高志小鹏鹏
来源:juejin.cn/post/7413187186131533861

收起阅读 »

为什么2.01 变成了 2.00 ,1分钱的教训不可谓不深刻

前些日子,测试提过来一个bug,说下单价格应该是 2.01,但是在订单详情中展示了2.00元。我头嗡的一下子,艹,不会是因为double 的精度问题吧~果不其然,经过排查代码,最终定位原因订单详情展示金额时,使用double 进行了金额转换,导致金额不准。我马...
继续阅读 »

前些日子,测试提过来一个bug,说下单价格应该是 2.01,但是在订单详情中展示了2.00元。我头嗡的一下子,艹,不会是因为double 的精度问题吧~

果不其然,经过排查代码,最终定位原因订单详情展示金额时,使用double 进行了金额转换,导致金额不准。

我马上排查核心购买和售后链路,发现涉及资金交易的地方没有问题,只有这一处问题,要不然这一口大锅非得扣我身上。

为什么 2.01 变成了 2.0

2.01等小数 在计算机中按照2进制补码存储时,存在除不尽,精度丢失的问题。 例如 2.01的补码为 000000010.009999999999999787 。正如十进制场景存在 1/3等无限小数问题,二进制场景也存在无限小数,所以一定会存在精度问题。

什么场景小数转换存在问题

for (int money = 0; money < 10000; money++) {
String valueYuan = String.format("%.2f", money * 1.0 / 100);

int value = (int) (Double.valueOf(valueYuan) * 100);
if (value != money) {
System.out.println(String.format("原值: %s, 现值:%s", money, value));
}
}

如上代码中,先将数字 除以 100,转为元, 精度为2位,然后将double 乘以100,转为int。 在除以、乘以两个操作后,精度出现丢失。

我把1-10000 的范围测试一遍,共有573个数字出现精度转换错误。 这个概率已经相当大了。

如何转换金额更安全?

Java 提供了BigDecimcal 专门处理精度更高的浮点数。简单封装一下代码,元使用String表示,分使用int表示。提供两个方法实现 元和分的 互转。

public static String change2Yuan(int money) {
BigDecimal base = BigDecimal.valueOf(money);
BigDecimal yuanBase = base.divide(new BigDecimal(100));
return yuanBase.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
}

public static int change2Fen(String money) {
BigDecimal base = new BigDecimal(money);

BigDecimal fenBase = base.multiply(new BigDecimal(100));
return fenBase.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
}

测试

测试0-1 亿 的金额转换逻辑,均成功转换,不存在精度丢失。

int error = 0;
long time = System.currentTimeMillis();
for (int money = 0; money < 100000000; money++) {
String valueYuan = change2Yuan(money);

int value = change2Fen(valueYuan);
if (value != money) {
error++;
}
}
System.out.println(String.format("时间:%s", (System.currentTimeMillis() - time)));
System.out.println(error);

性能测试

网上很多人说使用 BigDecimcal 存在性能影响,但是我测试性能还是不错的。可能首次耗时略高,大约2ms

标题耗时
0-1亿14.9 秒
0-100万0.199秒
0-1万0.59秒
0-1000.004秒
0-10.002秒

总结

涉及金额转换的 地方,一定要小心处理,防止出现精度丢失问题。可以使用代码审查工具,查看代码中是否存在使用double 进行金额转换的代码, 同时提供 金额转换工具类。


作者:五阳
来源:juejin.cn/post/7399985723673837577
收起阅读 »

Video.js:视频播放的全能解决方案

web
大家好,我是徐徐。今天跟大家分享一款多媒体类前端工具库:Video.js。前言在现代网页开发中,视频播放功能已经成为用户体验的一个重要组成部分。无论你是开发一个视频分享平台还是一个简单的博客,选择合适的视频播放器都至关重要。今天,我们要介绍的 Vid...
继续阅读 »

大家好,我是徐徐。今天跟大家分享一款多媒体类前端工具库:Video.js。

前言

在现代网页开发中,视频播放功能已经成为用户体验的一个重要组成部分。无论你是开发一个视频分享平台还是一个简单的博客,选择合适的视频播放器都至关重要。今天,我们要介绍的 Video.js 是一个强大且灵活的 HTML5 视频播放器,它能够满足你对视频播放的所有需求。

基本信息

什么是 Video.js?

Video.js 是一个从零开始为 HTML5 世界打造的网页视频播放器。它不仅支持 HTML5 视频和现代流媒体格式,还支持 YouTube 和 Vimeo。自2010年中期项目启动以来,Video.js 已经发展成为一个拥有数百名贡献者并广泛应用于超过** 80 **万个网站的播放器。

主要特点

  • 全能播放Video.js 支持传统的视频格式,如 MP4 和 WebM,同时也支持自适应流媒体格式,如 HLS 和 DASH。对于直播流,Video.js 还提供了专门的用户界面,使直播体验更加流畅。
  • 易于定制:虽然 Video.js 自带的播放器界面已经非常美观,但它的设计也考虑到了可定制性。通过简单的 CSS 你可以轻松地为播放器增添个人风格,使其更符合你的网页设计需求。
  • 丰富的插件生态:当你需要额外功能时,Video.js 的插件架构能够满足你的需求。社区已经开发了大量的插件和皮肤,包括 Chromecast、IMA 以及 VR 插件,帮助你快速扩展播放器的功能。

使用场景

Video.js 适用于各种视频播放场景:

  • 视频分享平台:无论是播放本地视频还是流媒体内容,Video.js 都能提供稳定的播放体验。
  • 直播应用:通过专用的直播流 UI,Video.js 能够实现高质量的实时视频播放。
  • 教育和培训平台:支持多种格式和流媒体,确保你的教学视频能够在不同设备上顺畅播放。

快速上手

要在你的网页中使用 Video.js,只需以下简单步骤:

  1. 引入 Video.js 的库

<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js/dist/video.min.js">script>


<link href="https://unpkg.com/video.js@8.17.3/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js@8.17.3/dist/video.min.js">script>


<link href="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.17.3/video-js.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.17.3/video.min.js">script>

  1. 添加视频播放器元素
<video
id="my-player"
class="video-js"
controls
preload="auto"
poster="//vjs.zencdn.net/v/oceans.png"
data-setup='{}'>

<source src="//vjs.zencdn.net/v/oceans.mp4" type="video/mp4">source>
<source src="//vjs.zencdn.net/v/oceans.webm" type="video/webm">source>
<source src="//vjs.zencdn.net/v/oceans.ogv" type="video/ogg">source>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a
web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">
supports HTML5 video
a>
p>
video>
  1. 初始化播放器
var player = videojs('my-video');

就这样,你就可以在网页上嵌入一个功能丰富的视频播放器了。

videojs函数还接受一个options对象和一个回调:

var options = {};

var player = videojs('my-player', options, function onPlayerReady() {
videojs.log('Your player is ready!');

// In this context, `this` is the player that was created by Video.js.
this.play();

// How about an event listener?
this.on('ended', function() {
videojs.log('Awww...over so soon?!');
});
});

结语

Video.js 是一个功能强大且灵活的视频播放器,它支持多种视频格式和流媒体协议,并且具有丰富的插件生态和良好的定制性。无论你是构建一个视频分享平台还是需要实现高质量的直播播放,Video.js 都能为你提供稳定且可扩展的解决方案。

希望这篇文章能帮助你了解 Video.js 的强大功能,并激发你在项目中使用它的灵感,这么好的东西,赶快分享给你的朋友们吧!


作者:前端徐徐
来源:juejin.cn/post/7411046020840964131
收起阅读 »

文档协同软件是如何解决编辑冲突的?

web
前言 本文将介绍在线协同文档编辑器是如何解决冲突的,大部分公司在解决冲突上目前用的都是 OT 算法,与之对应,也有一个 CRDT 算法实现。接下来,我们将深入这两种算法的实现及原理。 解决冲突的方案 在线协同文档编辑器通常使用不同的算法来解决冲突,具体算法取决...
继续阅读 »

前言


本文将介绍在线协同文档编辑器是如何解决冲突的,大部分公司在解决冲突上目前用的都是 OT 算法,与之对应,也有一个 CRDT 算法实现。接下来,我们将深入这两种算法的实现及原理。


解决冲突的方案


在线协同文档编辑器通常使用不同的算法来解决冲突,具体算法取决于编辑器的实现和设计。以下是一些常见的解决冲突的算法:



  1. OT(Operational Transformation,操作转换):这是一种常见的解决冲突的算法,用于实现实时协同编辑。OT算法通过将用户的编辑操作转换为操作序列,并在多个用户同时编辑时进行操作转换,以确保最终的文档状态一致。

  2. CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型):这是一种基于数据结构的解决冲突的算法,它允许多个用户在不同的副本上进行并发编辑,并最终将编辑结果合并为一致的文档状态。CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。


这些算法都有各自的优缺点,并且在不同的场景和需求下可能更适合不同的编辑器实现。


接下来,我们先聊聊 OT 算法。


OT 算法


image.png


当多个用户同时编辑同一文档时,OT算法通过操作转换来保持文档状态的一致性。下面是一个简单的示例,帮助你理解OT算法的工作原理。


假设有两个用户 A 和 B 同时编辑一段文本,初始文本为 "Hello, world!"。


用户 A 在文本末尾添加了字符 " How are you?"。


用户 B 在文本末尾添加了字符 " I'm fine."。


在 OT 算法中,每个操作都由一个操作类型和操作内容组成。在这个例子中,添加字符操作的操作类型为 "insert",操作内容为被插入的字符。


用户 A 的操作序列为:[insert(" How are you?")]
用户 B 的操作序列为:[insert(" I'm fine.")]


首先,服务器收到用户 A 的操作序列,将其应用到初始文本上,得到 "Hello, world! How are you?"。然后,服务器收到用户 B 的操作序列,将其应用到初始文本上,得到 "Hello, world! I'm fine."。


接下来,服务器需要进行操作转换,将用户 B 的操作序列转换为适应用户 A 的文本状态。在这个例子中,用户 B 的操作 "insert(" I'm fine.")" 需要转换为适应 "Hello, world! How are you?" 的操作。


操作转换的过程如下:



  1. 用户 A 的操作 "insert(" How are you?")" 在用户 B 的操作 "insert(" I'm fine.")" 之前发生,因此用户 B 的操作不会受到影响。

  2. 用户 B 的操作 "insert(" I'm fine.")" 在用户 A 的操作 "insert(" How are you?")" 之后发生,因此用户 B 的操作需要向后移动。

  3. 用户 B 的操作 "insert(" I'm fine.")" 向后移动到 "Hello, world! How are you? I'm fine."。


最终,服务器将转换后的操作序列发送给用户 A 和用户 B,他们将其应用到本地文本上,最终得到相同的文本状态 "Hello, world! How are you? I'm fine."。


这个例子展示了 OT 算法如何通过操作转换来保持多个用户同时编辑时文档状态的一致性。在实际应用中,OT 算法需要处理更复杂的操作类型和情况,并进行更详细的操作转换。


接下来,我们聊聊 CRDT 算法:


CRDT 算法


image.png


当多个用户同时编辑同一文档时,CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。下面是一个简单的示例,帮助你理解CRDT算法的工作原理。


在CRDT算法中,文档被表示为一个数据结构,常见的数据结构之一是有序列表(Ordered List)。每个用户的编辑操作会被转换为操作序列,并应用到本地的有序列表上。


假设有两个用户 A 和 B 同时编辑一段文本,初始文本为空。用户 A 在文本末尾添加了字符 "Hello",用户 B 在文本末尾添加了字符 "World"。


在CRDT算法中,每个字符都被赋予一个唯一的标识符,称为标记(Marker)。在这个例子中,我们使用递增的整数作为标记。


用户 A 的操作序列为:[insert("H", 1), insert("e", 2), insert("l", 3), insert("l", 4), insert("o", 5)]
用户 B 的操作序列为:[insert("W", 6), insert("o", 7), insert("r", 8), insert("l", 9), insert("d", 10)]


每个操作都包含要插入的字符以及对应的标记。


当用户 A 应用自己的操作序列时,有序列表变为 "Hello"。同样地,当用户 B 应用自己的操作序列时,有序列表变为 "World"。


接下来,服务器需要将两个用户的操作序列合并为一致的文本状态。在CRDT算法中,合并的过程是通过比较标记的大小来确定字符的顺序,并确保最终的文本状态一致。


合并的过程如下:



  1. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "H" 在 "W" 之前。

  2. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "e" 在 "o" 之前。

  3. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "r" 之前。

  4. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "l" 之前。

  5. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "o" 在 "d" 之前。


最终,合并后的有序列表为 "HelloWorld"。


这个例子展示了CRDT算法如何通过设计特定的数据结构和操作规则,以及比较标记的大小来实现冲突自由的复制。在实际应用中,CRDT算法可以应用于各种数据结构和操作类型,并进行更复杂的合并过程。


CRDT 的标记实现方案



  1. 递增整数标记:最简单的标记实现方式是使用递增的整数作为标记。每个操作都会分配一个唯一的整数标记,标记的大小可以用来比较操作的发生顺序。

  2. 时间戳标记:另一种常见的标记实现方式是使用时间戳作为标记。每个操作都会被分配一个时间戳,可以使用系统时间或逻辑时钟来生成时间戳。时间戳可以用来比较操作的发生顺序。

  3. 向量时钟标记:向量时钟是一种用于标记并发事件的数据结构。每个操作都会被分配一个向量时钟标记,向量时钟由多个节点的时钟组成,每个节点维护自己的逻辑时钟。向量时钟可以用来比较操作的发生顺序,并检测并发操作之间的关系。

  4. 哈希值标记:有些情况下,可以使用操作内容的哈希值作为标记。每个操作的内容都会被哈希为一个唯一的标记,这样可以通过比较哈希值来比较操作的发生顺序。


方案选型


OT算法和CRDT算法都是用于实现实时协同编辑的算法,但它们有不同的优点、缺点和工作原理。


OT算法的优点:



  1. 简单性:OT算法相对较简单,易于理解和实现。

  2. 实时协同编辑:OT算法可以实现实时协同编辑,多个用户可以同时编辑文档,并通过操作转换保持文档状态的一致性。


OT算法的缺点:



  1. 操作转换复杂性:OT算法的操作转换过程相对复杂,需要考虑多种情况和操作类型,实现和测试上有一定的挑战。

  2. 需要服务器参与:OT算法需要中央服务器来处理操作转换,这可能会导致一定的延迟和依赖性。


CRDT算法的优点:



  1. 冲突自由:CRDT算法设计了特定的数据结构和操作规则,可以实现冲突自由的复制,多个用户同时编辑时不会发生冲突。

  2. 去中心化:CRDT算法可以在去中心化的环境中工作,不依赖于中央服务器进行操作转换,每个用户可以独立地进行编辑和合并。


CRDT算法的缺点:



  1. 数据结构复杂性:CRDT算法的数据结构和操作规则相对复杂,需要更多的设计和实现工作。

  2. 数据膨胀:CRDT算法可能会导致数据膨胀,特别是在复杂的编辑操作和大规模的协同编辑场景下。


OT算法和CRDT算法的区别:



  1. 算法原理:OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。

  2. 中心化 vs. 去中心化:OT算法需要中央服务器进行操作转换,而CRDT算法可以在去中心化的环境中工作,每个用户都有完整的本地数据,可以将操作发送给服务器,然后网络分发到其他客户端。

  3. 复杂性:OT算法相对较简单,但在处理复杂操作和转换时可能会变得复杂;CRDT算法相对复杂,需要更多的设计和实现工作。


选择使用哪种算法取决于具体的应用场景和需求。OT算法适用于需要实时协同编辑的场景,而CRDT算法适用于去中心化和冲突自由的场景。


总结


本文介绍了在线协同文档编辑器中解决冲突的算法,主要包括OT算法和CRDT算法。OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则实现冲突自由的复制。OT算法需要中央服务器进行操作转换,适用于需要实时协同编辑的场景。CRDT算法可以在去中心化环境中工作,每个用户都有完整的本地数据,适用于去中心化和冲突自由的场景。选择使用哪种算法取决于具体的应用场景和需求。


作者:谦宇
来源:juejin.cn/post/7283018190593785896
收起阅读 »

audio自动播放为什么会失败

web
背景 某天客户报了一个问题,说是大屏的声音不能自动播放了,我们的大屏应用是有个报警的,当有报警的时候,自动会播放警报的声音 复线步骤 测试后发现如下结论 当刷新页面后,audio不会自动播放 当从另外的一个页面进入到当前页面,可以直接播放声音 如果你想测试...
继续阅读 »

背景


某天客户报了一个问题,说是大屏的声音不能自动播放了,我们的大屏应用是有个报警的,当有报警的时候,自动会播放警报的声音


复线步骤


测试后发现如下结论



  1. 当刷新页面后,audio不会自动播放

  2. 当从另外的一个页面进入到当前页面,可以直接播放声音


如果你想测试,可以点我进行测试


你可以先点击上方链接的 尝试一下 ,下方为截图


image.png


这个时候你会听到一声马叫声


然后,你刷新下马叫声的页面,这个时候声音的自动播放将不会生效


image.png


报错问题排查


打开控制台,不出意外看到了一个报错信息。


image.png


翻译为中文的意思为
不允许的错误。播放失败,因为用户没有先与文档交互。goo.gl/xX8pDD


尝试解决


那我给通过给body添加点击事件,自动触发点击事件,在点击的事件后自动播放声音。
(当是我的想法是,这个大概率是不行的,chrome应该不会忽略这一点,不然这个功能就相当于不存在)


经过测试后,发现确实还不行,在意料中。


参考别人的网站,用抖音测试


点击我跳转抖音


想到了我们可以参考抖音,我用抖音的进行测试,当你不做任何的操作,页面应该如下
image.png


我们从这里得出结论,这个应该是浏览器的限制,需要查看官方文档,看看原因


查阅官方文档


点我查看chrome的官方文档


我截取了一些关键的信息


image.png


注意浏览器会有一个媒体互动指数,这是浏览器自动计算的,该分越高,才会触发自动播放


查看电脑的媒体互动指数


在url上输入 about://media-engagement,你会看到如下的截图,


image.png


经过测试后 当网站变成了is High,音频会自动播放,不会播放失败。


这就解释了为什么有的网站可以自动播放声音,有的网站不可以自动播放声音


ok,我们继续往下看,这个时候看到了一些关键的信息。


作为开发者,我们不应该相信音频/视频会播放成功,要始终在播放的回掉中来进行判断


image.png


看到这些,我们来模仿抖音的实现.在播放声音的catch的时候,显示一个错误的弹窗,提示用户,当用户点击的时候,自动播放声音


   this.alarmAudio = new Audio(require("@/assets/sound/alarm.mp3"));
this.alarmAudio
.play()
.then(() => {
this.notifyId && this.notifyId.close();
})
.catch((error) => {
if (error instanceof DOMException) {
// 这里可以根据异常类型进行相应的错误处理
if (error.name === "NotAllowedError") {
if (this.notifyId) return;
this.notifyId = Notification({
title: "",
duration: 0,
position: "bottom-left",
dangerouslyUseHTMLString: true,
onClick: this.onAudioNotifyConfirm,
showClose: false,
customClass: "audio-notify-confirm",
message:
"<div style='color:#fff;font-size:18px;cursor:pointer'>因浏览器限制,需<span style='color:#ff2c55'>点击打开声音</span></div>",
});
}
}
});

实现效果如下


image.png


总结



  1. 在用到video或者audio的时候,要始终不相信他会播放声音成功,并且添加catch处理异常场景,给用户友好的提示

  2. video或者audio的自动播放跟媒体互动指数有关(MEI),当媒体指数高,会自动播放,否则需要用户先交互后,audio才可以自动播放。

  3. 从一个页面window.open另外一个页面可以自动播放声音,当刷新页面后,需要有高的MEI,audio才会自动播放,如果你需要在后台打开一个大屏的页面,刚好可以这样设计,不要用页面跳转


作者:pauldu
来源:juejin.cn/post/7412505754383007744
收起阅读 »

我的 Electron 客户端也可以全量/增量更新了

前言 本文主要介绍 Electron 客户端应用的自动更新,包括全量和增量这两种方式。 全量更新: 运行新的安装包,安装目录所有资源覆盖式更新。 增量更新: 只更新修改的部分,通常是渲染进程和主进程文件。 本文并没有拿真实项目来举例子,而是起一个 新的项目 从...
继续阅读 »

前言


本文主要介绍 Electron 客户端应用的自动更新,包括全量和增量这两种方式。


全量更新: 运行新的安装包,安装目录所有资源覆盖式更新。


增量更新: 只更新修改的部分,通常是渲染进程和主进程文件。


本文并没有拿真实项目来举例子,而是起一个 新的项目 从 0 到 1 来实现自动更新。


如果已有的项目需要支持该功能,借鉴本文的主要步骤即可。


前置说明:



  1. 由于业务场景的限制,本文介绍的更新仅支持 Windows 操作系统,其余操作系统未作兼容处理。

  2. 更新流程全部由前端完成,不涉及到后端,没有轮询 更新的机制。

  3. 发布方式限制为 generic,线上服务需要配置 nginx 确保访问到资源文件。


准备工作


脚手架搭建项目


我们通过 electron-vite 快速搭建一个基于 Vite + React + TSElectron 项目。


1.jpg


该模板已经包括了我们需要的核心第三方库:electron-builderelectron-updater


前者是用来打包客户端程序的,后者是用来实现自动更新的。


在项目根目录下,已经自动生成了两份配置文件:electron-builder.ymldev-app-update.yml


electron-builder.yml


该文件描述了一些 打包配置,更多信息可参考 官网


在这些配置项中,publish 字段比较重要,因为它关系到更新源。


publish:
provider: generic // 使用一个通用的 HTTP 服务器作为更新源。
url: https://example.com/auto-updates // 存放安装包和描述文件的地址

provider 字段还有其他可选项,但是本文只介绍 generic 这种方式,即把安装包放在 HTTP 服务器里。


dev-app-update.yml


provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: electron-update-demo-updater

其中,updaterCacheDirName 定义下载目录,也就是安装包存放的位置。全路径是C:\Users\用户名\AppData\Local\electron-update-demo-updater,不配置则在C:\Users\用户名\AppData\Local下自动创建文件夹,开发环境下为项目名,生产环境下为项目名-updater


image.png


模拟服务器


我们直接运行 npm run build:win,在默认 dist 文件夹下就出现了打包后的一些资源。


image.png


其实更新的基本思路就是对比版本号,即对比本地版本和 线上服务器 版本是否一致,若不一致则需要更新。


image.png


因此我们在开发调试的时候,需要起一个本地服务器,来模拟真实的情况。


新建一个文件夹 mockServer,把打包后的 setup.exe 安装包和 latest.yml 文件粘贴进去,然后通过 serve 命令默认起了一个 http://localhose:3000 的本地服务器。


既然有了存放资源的本地地址,在开发调试的时候,我们更新下 dev-app-update.yml 文件的 url 字段,也就是修改为 http://localhose:3000


注:如果需要测试打包后的更新功能,需要同步修改打包配置文件。


全量更新


与主进程文件同级,创建 update.ts 文件,之后我们的更新逻辑将在这里展开。


import { autoUpdater } from 'electron-updater' //核心库

需要注意的是,在我们开发过程中,通过 npm run dev 起来的 Electron 程序其实不能算是打包后的状态。


你会发现在调用 autoUpdater 的一些方法会提示下面的错误:


Skip checkForUpdates because application is not packed and dev update config is not forced


因此我们需要在开发环境去做一些动作,并且手动指定 updateConfigPath


// update.ts
if (isDev) {
Object.defineProperty(app, 'isPackaged', {
get: () => true
})
autoUpdater.updateConfigPath = path.join(__dirname, '../../dev-app-update.yml')
}

核心对象 autoUpdater 有很多可以主动调用的方法,也有一些监听事件,同时还有一些属性可以设置。


这里只展示了本人项目场景所需的一些配置。


autoUpdater.autoDownload = false // 不允许自动下载更新
autoUpdater.allowDowngrade = true // 允许降级更新(应付回滚的情况)

autoUpdater.checkForUpdates()
autoUpdater.downloadUpdate()
autoUpdater.quitAndInstall()

autoUpdater.on('checking-for-update', () => {
console.log('开始检查更新')
})
autoUpdater.on('update-available', info => {
console.log('发现更新版本')
})
autoUpdater.on('update-not-available', info => {
console.log('不需要全量更新', info.version)
})
autoUpdater.on('download-progress', progressInfo => {
console.log('更新进度信息', progressInfo)
})
autoUpdater.on('update-downloaded', () => {
console.log('更新下载完成')
})
autoUpdater.on('error', errorMessage => {
console.log('更新时出错了', errorMessage)
})

在监听事件里,我们可以拿到下载新版本 整个生命周期 需要的一些信息。


// 新版本的版本信息(已筛选)
interface UpdateInfo {
readonly version: string;
releaseName?: string | null;
releaseNotes?: string | Array<ReleaseNoteInfo> | null;
releaseDate: string;
}

// 下载安装包的进度信息
interface ProgressInfo {
total: number;
delta: number;
transferred: number;
percent: number;
bytesPerSecond: number;
}

在写完这些基本的方法之后,我们需要决定 检验更新 的时机,一般是在应用程序真正启动之后,即 mainWindow 创建之后。


运行项目,预期会提示 不需要全量更新,因为刚才复制到本地服务器的 latest.yml 文件里的版本信息与本地相同。修改 version 字段,重启项目,主进程就会提示有新版本需要更新了。


频繁启动应用太麻烦,除了应用初次启动时主进程主动帮我们检验更新外,还需要用户 手动触发 版本更新检测,此外,由于产品场景需要,当发现有新版本的时候,需要在 渲染进程 通知用户,包括版本更新时间和更新内容。因此我们需要加上主进程与渲染进程的 IPC通信 来实现这个功能。


其实需要的数据主进程都能够提供了,渲染进程具体的展示及交互方式就自由发挥了,这里简单展示下我做的效果。


1. 发现新版本
GIF 2024-9-19 16-48-44.gif


2. 无需更新


image.png


增量更新


为什么要这么做


其实全量更新是一种比较简单粗暴的更新方式,我没有花费太大的篇幅去介绍,基本上就是使用 autoUpdater 封装的一些方法,开发过程中更多的注意力则是放在了渲染进程的交互上。


此外,我们的更新交互用的是一种 用户感知 的方式,即更不更新是由用户自己决定的。而 静默更新 则是用户无感知,在监测到更新后不用去通知用户,而是自行在后台下载,在某个时刻执行安装的逻辑,这种方式往往是一键安装,不会弹出让用户去选择安装路径的窗口。接下去要介绍的增量更新其实是这种 用户无感知 的形式,只不过我们更新的不是整个应用程序。


由于产品迭代比较频繁,我们的业务方经常收到更新提示,意味着他们要经常手动更新应用,以便使用到最新的功能特性。众所周知,Electron 给前端开发提供了一个比较容易上手的客户端开发解决方案,但在享受 跨平台 等特性的同时,还得忍受 臃肿的安装包


带宽、流量在当前不是什么大问题,下载和安装的速度也挺快,但这频繁的下载覆盖一模一样的资源文件还是挺糟心的,代码改动量小时,全量更新完全没有必要。我们希望的是,开发更新了什么代码,应用程序就替换掉这部分代码就好了,这很优雅。在 我们的场景 中,这部分代码指的是 —— main、preload、renderer,并不包括 dll第三方SDK等资源。


网上有挺多种增量更新的 解决方案,例如:



  1. 通过 win.loadURL(一个线上地址) 实现,相当于就套了一层客户端的壳子,与加载的Web 端同步更新。这种方法对于简单应用来说是最省心的,但是也有一定的局限性,例如不能使用 node 去操作一些底层的东西。

  2. 设置 asar 的归档方式,替换app.asarapp.asar.unpack来实现。但后者在我实践过程中存在文件路径不存在的问题。

  3. 禁用 asar 归档,解压渲染进程压缩包后实现替换。简单尝试过,首次执行安装包的时候写入文件很慢,不知道是不是这个方式引起的。

  4. 欢迎补充。


本文我们采用较普遍的 替换asar 来实现。


优化 app.asar 体积



asar是 Electron提供的一种将多个文件合并成一个文件的类 tar 风格的归档格式,不仅可以缓解Windows下路径名过长的问题, 还能够略微加快一下 require的速度, 并且可以隐藏你的源代码。(并非完全隐藏,有方法可以找出来)



Electron 应用程序启动的时候,会读取 app.asar.unpacked 目录中的内容合并到 app.asar 的目录中进行加载。在此之前我们已经打包了一份 Demo 项目,安装到本地后,我们可以根据安装路径找到 app.asar 这个文件。


例如:D:\你的安装路径\electron-update-demo\resources


image.png


在这个 Demo 项目中,脚手架生成的模板将图标剥离出来,因此出现了一个 app.asar.unpacked 文件夹。我们不难发现,app.asar 这个文件竟然有 66 MB,再回过头看我们真正的代码文件,甚至才 1 MB !那么到底是什么原因导致的这个文件这么大呢,刚才提到了,asar 其实是一种压缩格式,因此我们只要解压看看就知道了。


npm i -g asar // 全局安装

asar e app.asar folder // 解压到folder文件夹

image.png


解压后我们不难发现,out 文件夹里的才是我们打包出来的资源,罪魁祸首竟然是 node_modules,足足有 62.3 MB


查阅资料得知,Electron 在打包的时候,会把 dependencies 字段里的依赖也一起打包进去。而我们的渲染进程是用 React开发的,这些第三方依赖早就通过 Vite 等打包工具打到资源文件里了,根本不需要再打包一次,不然我们重新下载的 app.asar 文件还是很大的,因此需要尽可能减少体积。


优化应用程序体积 == 减少 node_modules 文件夹的大小 == 减少需要打包的依赖数量 == 减少 dependencies 中的依赖。


1. 移除 dependencies


最开始我想的是把 package.json 中的 dependencies 全都移到 devDependencies,打包后的体积果然减小了,但是在测试过程中会发现某些功能依赖包会缺失,比如 @electron/remote。因此在修改这部分代码的时候,需要特别留意,哪些依赖是需要保留下来的。


由于不想影响 package.json 的版本结构,我只是在写了一个脚本,在 npm i 之后,执行打包命令前修改 devDependencies 就好了。


2. 双 package.json 结构


这是 electron-builder 官网上看到的一种技巧,传送门, 创建 app 文件夹,再创建第二个 package.json,在该文件中配置我们应用的名称、版本、主进程入口文件等信息。这样一来,electron-builder 在打包时会以该文件夹作为打包的根文件夹,即只会打包这个文件夹下的文件。


但是我原项目的主进程和预加载脚本也放在这,尝试修改后出现打包错误的情况,感兴趣的可以试试


image.png


这是我们优化之后的结果,1.32 MB,后面只需要替换这个文件就实现了版本的更新。


校验增量更新


全量更新的校验逻辑是第三方库已经封装好了的,那么对于增量更新,就得由我们自己来实现了。


首先明确一下 校验的时机package.jsonversion 字段表示应用程序的版本,用于全量更新的判断,那也就意味着,当这个字段保持不变的时候,会触发上文提到的 update-not-available 事件。所以我们可以在这个事件的回调函数里来进行校验。


autoUpdater.on('update-not-available', info => { console.log('不需要全量更新', info.version) })

然后就是 如何校验,我们回过头来看 electron-builder 的打包配置,在 releaseInfo 字段里描述了发行版本的一些信息,目前我们用到了 releaseNotes 来存储更新日志,查阅官网得知还有个 releaseName 好像对于我们而言是没什么用的,那么我们就可以把它作为增量更新的 热版本号。(但其实 官网 配置项还有个 vendor 字段,可是我在用的时候打包会报错,可能是版本的问题,感兴趣的可以试试。)


每次发布新版本的时候,只要不是 Electron自身版本变化 等重大更新,我们都可以通过修改 releaseInforeleaseName 来发布我们的热版本号。现在我们已经有了线上的版本,那 本地热版本号 该如何获取呢。这里我们在每次热更新完成之后,就会把最新的版本号存储在本地的一个配置文件中,通常是 userData 文件夹,用于存储我们应用程序的用户信息,即使程序卸载 了也不会影响里面的资源。在 Windows 下,路径如下:
C:\Users\用户名\AppData\Roaming\electron-update-demo


因此,整个 校验流程 就是,在打开程序的时候,autoUpdater 触发 update-not-available 事件,拿到线上 latest.yml 描述的 releaseName 作为热版本号,与本地配置文件(我们命名为 config.json)里存储的热版本号(我们命名为 hotVersion)进行对比,若不同就去下载最新的 app.asar 文件。


我们使用 electron-log 来记录日志,代码如下所示。


// 本地配置文件
const localConfigFile = path.join(app.getPath('userData'), 'config.json')

const checkHotVersion = (latestHotVersion: string): boolean => {
let needDownload = false
if (!fs.existsSync(localConfigFile)) {
fs.writeFileSync(localConfigFile, '{}', 'utf8')
log.info('配置文件不存在,已自动创建')
} else {
log.info('监测到配置文件')
}

try {
const configStr = fs.readFileSync(localConfigFile, 'utf8')
const configData = JSON.parse(configStr || '{}')
// 对比版本号
if (latestHotVersion !== configData.hotVersion) {
log.info('当前版本不是最新热更版本')
needDownload = true
} else {
log.info('当前为最新版本')
}
} catch (error) {
log.error('读取配置文件时出错', error)
}

return needDownload
}

下载增量更新包


通过刚才的校验,我们已经知道了什么时候该去下载增量更新包。


在开发调试的时候,我们可以把新版本的 app.asar 也放到起了本地服务器的 mockServer 文件夹里来模拟真实的情况。有很多方式可以去下载文件,这里我用了 nodejshttp 模块去实现,如果是 https 的需要引用 https 模块。


下载到本地的时候,我是放在了与 app.asar 同级目录的 resources 文件夹,我们不能直接覆盖原文件。一是因为进程权限占用的问题,二是为了容错,所以我们需要以另一个名字来命名下载后的文件(这里我们用的是 app.asar-temp),也就不需要去备份原文件了,代码如下。


const resourcePath = isDev
? path.join('D:\\测试\\electron-update-demo\\resources')
: process.resourcesPath

const localAsarTemp = path.join(resourcePath, 'app.asar-temp')

const asarUrl = 'http://localhost:3000/app.asar'

downloadAsar() {
const fileStream = fs.createWriteStream(localAsarTemp)
http.get(asarUrl, res => {
res.pipe(fileStream)
fileStream
.on('finish', () => {
log.info('asar下载完成')
})
.on('error', error => {
// 删除文件
fs.unlink(localAsarTemp, () => {
log.error('下载出现异常,已中断', error)
})
})
})
}

因此,我们的流程更新为:发现新版本后,下载最新的 app.asarresources 目录,并重命名为 app.asar-temp。这个时候我们启动项目进行调试,找到记录在本地的日志—— C:\Users\用户名\AppData\Roaming\electron-update-demo\logs,会有以下的记录:


[2024-09-20 13:49:22.456] [info]  监测到配置文件
[2024-09-20 13:49:22.462] [info] 当前版本不是最新热更版本
[2024-09-20 13:49:23.206] [info] asar下载完成

在看看项目 resources 文件夹,多了一个 app.asar-temp文件。


image.png


至此,下载这一步已经完成了,但这样还会有一个问题,因为文件名没有加上版本号,也没有重复性校验,倘若一直没有更新,那么每次启动应用需要增量更新的时候都需要再次下载。


替换 app.asar 文件


好了,新文件也有了,接下来就是直接替换掉老文件就可以了,但是 替换的时机 很重要。


Windows 操作系统下,直接替换 app.asar 会提示程序被占用而导致失败,所以我们应该在程序关闭的时候实现替换。



  1. 进入应用,下载新版本,自动关闭应用,替换资源,重启应用。

  2. 进入应用,下载新版本,不影响使用,用户 手动关闭 后替换,下次启动 就是新版本了。


我们有上面两种方案,最终采用了 方案2


在主进程监听 app.on('quit') 事件,在应用退出的时候,判断 app.asarapp.asar-temp 是否同时存在,若同时存在,就去替换。这里替换不能直接用 nodejs 在主进程里去写,因为主进程执行的时候,资源还是被占用的。因此我们需要额外起一个 子进程 去做。


nodejs 可以通过 spawnexec 等方法创建一个子进程。子进程可以执行一些自定义的命令,我们并没有使用子进程去执行 nodejs,因为业务方的机器上不一定有这个环境,而是采用了启动 exe 可执行文件的方式。可能有人问为什么不直接运行 .bat 批处理文件,因为我调试关闭应用的时候,会有一个 命令框闪烁 的现象,这不太优雅,尽管已经设置 spawnwindowsHide: true


那么如何获得这个 exe 可执行文件呢,其实是通过 bat 文件去编译的,命令如下:


@echo off
timeout /T 1 /NOBREAK
del /f /q /a %1\app.asar
ren %1\app.asar-temp app.asar

我们加了延时,等待父进程完全关闭后,再去执行后续命令。其中 %1 为运行脚本传入的参数,在我们的场景里就是 resources 文件夹的地址,因为每个人的安装地址应该是不一样的,所以需要作为参数传进去。


转换文件的工具一开始用的是 Bat To Exe Converter 下载地址,但是在实际使用的过程中,我的电脑竟然把转换后的 exe 文件 识别为病毒,但在其他同事电脑上没有这种表现,这肯定就不行了,最后还是同事使用 python 帮我转换生成了一份可用的文件(replace.exe)。


这里我们可以选择不同的方式把 replace.exe 存放到 用户目录 上,要么是从线上服务器下载,要么是安装的时候就已经存放在本地了。我们选择了后者,这需要修改 electron-builder 打包配置,指定 asarUnpack, 这样就会存放在 app.asar.unpacked 文件夹中,不经常修改的文件都可以放在这里,不会随着增量更新而替换掉。


有了这个替换脚本之后,开始编写子进程相关的代码。


import { spawn } from 'child_process'

cosnt localExePath = path.join(resourcePath, 'app.asar.unpacked/resources/replace.exe')

replaceAsar() {
if (fs.existsSync(localAsar) && fs.existsSync(localAsarTemp)) {
const command = `${localExePath} ${resourcePath}`
log.info(command)
const logPath = app.getPath('logs')
const childOut = fs.openSync(path.join(logPath, './out.log'), 'a')
const childErr = fs.openSync(path.join(logPath, './err.log'), 'a')
const child = spawn(`"${localExePath}"`, [`"${resourcePath}"`], {
detached: true, // 允许子进程独立
shell: true,
stdio: ['ignore', childOut, childErr]
})
child.on('spawn', () => log.info('子进程触发'))
child.on('error', err => log.error('child error', err))
child.on('close', code => log.error('child close', code))
child.on('exit', code => log.error('child exit', code))
child.stdout?.on('data', data => log.info('stdout', data))
child.stderr?.on('data', data => log.info('stderr', data))
child.unref()
}
}

app.on('quit', () => {
replaceAsar()
})

在这块代码中,创建子进程的配置项比较重要,尤其是路径相关的。因为用户安装路径是不同的,可能会存在 文件夹名含有空格 的情况,比如 Program Files,这会导致在执行的时候将空格识别为分隔符,导致命令执行失败,因此需要加上 双引号。此外,detached: true 可以让子进程独立出来,也就是父进程退出后可以执行,而shell: true 可以将路径名作为参数传过去。


const child = spawn(`"${localExePath}"`, [`"${resourcePath}"`], {
detached: true,
shell: true,
stdio: ['ignore', childOut, childErr]
})

但这块有个 疑惑,为什么我的 closeexit 以及 stdout 都没有触发,以至于不能拿到命令是否执行成功的最终结果,了解的同学可以评论区交流一下。


至此,在关闭应用之后,app.asar 就已经被替换为最新版本了,还差最后一步,更新本地配置文件里的 hotVersion,防止下次又去下载更新包了。


child.on('spawn', () => {
log.info('子进程触发')
updateHotVersion()
})

updateHotVersion() {
fs.writeFileSync(localConfigFile, JSON.stringify({ hotVersion }, null, 2))
}

增量更新日志提示


既然之前提到的全量更新有日志说明,那增量更新可以不用,也应该需要具备这个能力,不然我们神不知鬼不觉帮用户更新了,用户都不知道 新在哪里


至于更新内容,我们可以复用 releaseInforeleaseNotes 字段,把更新日志写在这里,增量更新完成后展现给用户就好了。


但是总不能每次打开都展示,这里需要用户有个交互,比如点击 知道了 按钮,或者关闭 Modal 后,把当前的热更新版本号保存在本地配置文件,记录为 logVersion。在下次打开程序或者校验更新的时候,我们判断一下 logVersion === hotVersion若不同,再去提示更新日志。


日志版本 校验和修改的代码如下所示:


checkLogVersion() {
let needShowLog = false
try {
const configStr = fs.readFileSync(localConfigFile, 'utf8')
const configData = JSON.parse(configStr || '{}')
const { hotVersion, logVersion } = configData
if (hotVersion !== logVersion) {
log.info('日志版本与当前热更新版本不同,需要提示更新日志')
needShowLog = true
} else {
log.info('日志已是最新版本,无需提示更新日志')
}
} catch (error) {
log.error('读取配置文件失败', error)
}
return needShowLog
}

updateLogVersion() {
try {
const configStr = fs.readFileSync(localConfigFile, 'utf8')
const configData = JSON.parse(configStr || '{}')
const { hotVersion } = configData
fs.writeFileSync(localConfigFile, JSON.stringify({ hotVersion, logVersion: hotVersion }, null, 2))
log.info('日志版本已更新')
} catch (error) {
log.error('读取配置文件失败', error)
}
}

读取 config.json 文件的方法自行封装一下,我这里就重复使用了。主进程 ipcMain.on 监听一下用户传递过来的事件,再去调用 updateLogVersion 即可,渲染进程效果如下:


提示增量更新日志


image.png


点击 知道了 后,再次打开应用后就不会提示日志了,因为本地配置文件已经被修改了。


image.png


当然可能也有这么个场景,开发就改了一点点无关功能的代码,无需向用户展示日志,我们只需要加一个判断 releaseNotes 是否为空的逻辑就好了,也做到了 静默更新


小结


不足之处


本文提出的增量更新方案应该算是比较简单的,可能并不适用于所有的场景,考虑也不够全面,例如:



  1. dll第三方SDK 等资源的更新。

  2. 增量更新失败后应该通过全量更新 兜底

  3. 用户在使用过程中发布新版本,得等到 第二次打开 才能用到新版本。


流程图


针对本文的解决方案,我简单画了一个 流程图


未命名绘图.png


参考文章


网上其实有不少关于 Electron 自动更新的文章,在做这个需求之前也是浏览了好多,但也有些没仔细看完,所以就躺在我的标签页没有关闭,在这罗列出来,也当作收藏了。


写这篇文章的目的也是为了记录和复盘,如果能为你提供一些思路那就再好不过了。


鸣谢:



作者:小陈同学吗
来源:juejin.cn/post/7416311252580352034
收起阅读 »

国产语言MoonBit崛起,比Rust快9倍,比GO快35倍

在这个技术日新月异的时代,编程语言的更新换代似乎已经成为了家常便饭。但国产语言却一直不温不火,对于程序员来说,拥有一款属于我们自己的语言,一直是大家梦寐以求的事情。 如果我说有一种国产编程语言,它的运行速度比Rust快9倍,比GO快35倍,你会相信吗? 这不是...
继续阅读 »

在这个技术日新月异的时代,编程语言的更新换代似乎已经成为了家常便饭。但国产语言却一直不温不火,对于程序员来说,拥有一款属于我们自己的语言,一直是大家梦寐以求的事情。


如果我说有一种国产编程语言,它的运行速度比Rust快9倍,比GO快35倍,你会相信吗?


这不是天方夜谭,最近,被称为“国产编程语引领者”的MoonBit(月兔),宣布正式进入Beta预览版本阶段啦!


一听月兔这名字起得挺中式的。


一、初识MoonBit



MoonBit是由粤港澳大湾区数字经济研究院(福田)研发的全新编程语言。



① 官网


http://www.moonbitlang.cn/


官网


② 目前状态


MoonBit是2022年推出的国产编程语言,并在2023年8月18日海外发布后,立即获得国际技术社区的广泛关注。


经过一年多的高速迭代,MoonBit推出了beta预览版。


MoonBit 目前处于 Beta-Preview 阶段。官方希望能在 2024/11/22 达到 Beta 阶段,2025年内达到 1.0 阶段。


③ 由来


诞生于AI浪潮,没有历史包袱:MoonBit 诞生于 ChatGPT 出世之后,使得 MoonBit 团队有更好的机会去重新构想整个程序语言工具链该如何与 AI 友好的协作,不用承担太多的历史包袱


二、MoonBit 语言优势


编译与运行速度快


MoonBit在编译速度和运行时性能上表现出色,其编译626个包仅需1.06秒,比Rust快了近9倍;运行速度比GO快35倍!


编译速度比较


代码体积小


MoonBit 在输出 Wasm 代码体积上相较于传统语言有显著优势。


一个简单的HTTP 服务器时,MoonBit 的输出文件大小仅为 27KB,而 WasmCloud提供的http-hello-world 模板中 Rust 的输出为 100KBTypeScript8.7MBPython 更是高达 17MB


代码体积比较


多重安全保障


MoonBit 采用了强大的类型系统,并内置静态检测工具,在编译期检查类型错误,


MoonBit自身的静态控制流分析能在编译器捕获异常的类型,从而提高代码的正确性和可靠性。


高效迭代器


MoonBit创新地使用了零开销的迭代器设计,使得用户能够写出既优雅又高效的代码。


创新的泛型系统设计


MoonBit语言在它的测试版里就已经搞定了泛型和特殊的多态性,而且在编译速度特别快的同时,还能做到用泛型时不增加额外负担。


你要知道,这种本事在很多流行的编程语言里,都是正式发布很久之后才慢慢有的,但MoonBit一开始就做到了。这种设计在现在编程语言越来越复杂的大背景下特别关键,因为一个好的类型系统对于整个编程语言生态的健康成长是特别重要的。


三、应用场景


① 云计算


② 边缘计算


③ AI 以及教学领域的发展


四、开发样例


我们在官网 http://www.moonbitlang.cn/gallery/ 可以看到用使用MoonBit 开发的游戏样例



  • 罗斯方块游戏

  • 马里奥游戏

  • 数独求解器

  • 贪吃蛇游戏


游戏开发样例


五、语言学习


5.1 语法文档



如果你也对MoonBit感兴趣,想学习它,访问官方文档docs.moonbitlang.cn/。文档算是比较详细的了



image-20240921212615386


5.2 在线编译器



无需本地安装编译器即可使用,官方提供了在线编译器



① 在线编辑器地址


try.moonbitlang.cn/


在线编辑器


② 点击这儿运行代码


运行代码


5.3 VS Code 中安装插件编写代码、


① 安装插件


安装插件


搜索插件


② 下载程序


按下shift+cmd+p快捷键(mac快捷键,windows和linux快捷键是ctrl+shift+p),输入 MoonBit:install latest moonbit toolchain,随后会出现提示框,点击“yes”,等待程序下载完成。


下载程序


③ 创建并打开新项目


下载完成后,点击terminal,输入moon new hello && code hello以创建并打开新项目。


④ 始执行代码


项目启动后,再次打开terminal,输入moon run main命令,即可开始执行代码。


六、小结


下面是晓凡的一些个人看法


MoonBit 作为一款新兴的国产编程语言,其在性能和安全性方面的表现令人印象深刻。


特别是它在编译速度和运行效率上的优化,对于需要处理大量数据和高并发请求的现代应用来说,是一个很大的优势。


同时,它的设计理念符合当前软件开发的趋势,比如对云计算和边缘计算的支持,以及对 AI 应用的适配。


此外,MoonBit 团队在语言设计上的前瞻性思考,比如泛型系统的实现,显示出了其对未来编程语言发展趋势的深刻理解。


而且,提供的游戏开发样例不仅展示了 MoonBit 的实用性,也降低了初学者的学习门槛。


作者:程序员晓凡
来源:juejin.cn/post/7416604150933733410
收起阅读 »

花了一天时间帮财务朋友开发了一个实用小工具

大家好,我是晓凡。 写在前面 不知道大家有没有做财务的朋友,我就有这么一位朋友就经常跟我抱怨。一到月底简直就是噩梦,总有加不完的班,熬不完的夜,做不完的报表。 一听到这儿,这不就一活生生的一个“大表哥”么,这加班跟我们程序员有得一拼了,忍不住邪恶一笑,心里平...
继续阅读 »

大家好,我是晓凡。


写在前面


不知道大家有没有做财务的朋友,我就有这么一位朋友就经常跟我抱怨。一到月底简直就是噩梦,总有加不完的班,熬不完的夜,做不完的报表。


来自朋友的抱怨


一听到这儿,这不就一活生生的一个“大表哥”么,这加班跟我们程序员有得一拼了,忍不住邪恶一笑,心里平衡了很多。



身为牛马,大家都不容易啊。我不羡慕你数钱数到手抽筋,你也别羡慕我整天写CRUD 写到手起老茧🤣


吐槽归吐槽,饭还得吃,工作还得继续干。于是乎,真好赶上周末,花了一天的时间,帮朋友写了个小工具


一、功能需求


跟朋友吹了半天牛,终于把需求确定下来了。就一个很简单的功能,通过名字,将表一和表二中相同名字的金额合计。


具体数据整合如下图所示


数据整合


虽然一个非常简单的功能,但如果不借助工具,数据太多,人工来核对,整合数据,还是需要非常消耗时间和体力的。


怪不得,这朋友到月底就消失了,原来时间都耗在这上面了。


二、技术选型


由于需求比较简单,只有excel导入导出,数据整合功能。不涉及数据库相关操作。


综合考虑之后选择了



  • PowerBuilder

  • Pbidea.dll


使用PowerBuilder开发桌面应用,虽然界面丑一点,但是开发效率挺高,简单拖拖拽拽就能完成界面(对于前端技术不熟的小伙伴很友好)


其次,由于不需要数据库,放弃web开发应用,又省去了云服务器费用。最终只需要打包成exe文件即可跑起来


Pbidea.dll 算是Powerbuilder最强辅助开发,没有之一。算是PBer们的福音吧


三、简单界面布局


界面布局1


界面布局2


界面布局3


四、核心代码


① 导入excel



string ls_pathName,ls_FileName //路径+文件名,文件名
long ll_Net
long rows
dw_1.reset()
uo_datawindowex dw
dw = create uo_datawindowex
dw_1.setredraw(false)
ll_Net = GetFileSaveName("请选择文件",ls_pathName,ls_FileName,"xlsx","Excel文(*.xlsx),*.xlsx")

rows = dw.ImportExcelSheet(dw_1,ls_pathName,1,0,0)
destroy dw
dw_1.setredraw(true)
MessageBox("提示信息","导入成功 " + string(rows) + "行数据")


② 数据整合


long ll_row,ll_sum1,ll_sum2
long ll_i,ll_j
long ll_yes

string ls_err

//重置表三数据

dw_3.reset()

//处理表一数据
ll_sum1 = dw_1.rowcount( )

if ll_sum1<=0 then
ls_err = "表1 未导入数据,请先导入数据"
goto err
end if

for ll_i=1 to ll_sum1
ll_row = dw_3.insertrow(0)
dw_3.object.num[ll_row] =ll_row                                                          //序号
dw_3.object.name[ll_row]=dw_1.object.name[ll_i]                                 //姓名
dw_3.object.salary[ll_row]=dw_1.object.salary[ll_i]                                //工资
dw_3.object.endowment[ll_row]=dw_1.object.endowment[ll_i]               //养老
dw_3.object.medical[ll_row]=dw_1.object.medical[ll_i]                          //医疗
dw_3.object.injury[ll_row]=dw_1.object.injury[ll_i]                                        //工伤
dw_3.object.unemployment[ll_row]=dw_1.object.unemployment[ll_i]      //失业
dw_3.object.publicacc[ll_row]=dw_1.object.publicacc[ll_i]                      //公积金
dw_3.object.annuity[ll_row]=dw_1.object.annuity[ll_i]                           //年金

next

//处理表二数据

ll_sum2 = dw_2.rowcount( )

if ll_sum2<=0 then
ls_err = "表2未导入数据,请先导入数据"
goto err
end if

for ll_j =1 to ll_sum2
string ls_name
ls_name = dw_2.object.name[ll_j]

ll_yes = dw_3.Find("name = '"+ ls_name +"' ",1,dw_3.rowcount())

if ll_yes<0 then
ls_err = "查找失败!"+SQLCA.SQLErrText
goto err
end if

if ll_yes = 0 then  //没有找到
ll_row = dw_3.InsertRow (0)
dw_3.ScrollToRow(ll_row)
dw_3.object.num[ll_row]                   = ll_row                                                          //序号
dw_3.object.name[ll_row]                 = dw_1.object.name[ll_j]                                 //姓名
dw_3.object.salary[ll_row]                 = dw_1.object.salary[ll_j]                                //工资
dw_3.object.endowment[ll_row]         = dw_1.object.endowment[ll_j]               //养老
dw_3.object.medical[ll_row]              = dw_1.object.medical[ll_j]                          //医疗
dw_3.object.injury[ll_row]                 = dw_1.object.injury[ll_j]                                        //工伤
dw_3.object.unemployment[ll_row]    = dw_1.object.unemployment[ll_j]      //失业
dw_3.object.publicacc[ll_row]            = dw_1.object.publicacc[ll_j]                      //公积金
dw_3.object.annuity[ll_row]               = dw_1.object.annuity[ll_j]                           //年金
end if

if ll_yes >0 then  //找到        
dec{2} ld_salary,ld_endowment,ld_medical,ld_injury,ld_unemployment,ld_publicacc,ld_annuity
ld_salary = dw_3.object.salary[ll_yes] + dw_2.object.salary[ll_j]
ld_endowment =  dw_3.object.endowment[ll_yes] + dw_2.object.endowment[ll_j]
ld_medical = dw_3.object.medical[ll_yes] + dw_2.object.medical[ll_j]
ld_injury = dw_3.object.injury[ll_yes] + dw_2.object.injury[ll_j]
ld_unemployment = dw_3.object.unemployment[ll_yes] + dw_2.object.unemployment[ll_j]
ld_publicacc = dw_3.object.publicacc[ll_yes] + dw_2.object.publicacc[ll_j]
ld_annuity = dw_3.object.annuity[ll_yes] + dw_2.object.annuity[ll_j]

dw_3.object.salary[ll_yes]=  ld_salary                             //工资
dw_3.object.endowment[ll_yes]=ld_endowment               //养老
dw_3.object.medical[ll_yes]=ld_medical                          //医疗
dw_3.object.injury[ll_yes]=ld_injury                                     //工伤
dw_3.object.unemployment[ll_yes]=ld_unemployment   //失业
dw_3.object.publicacc[ll_yes]=ld_publicacc                    //公积金
dw_3.object.annuity[ll_yes]=ld_publicacc                      //年金

end if

next

return 0

err:
messagebox('错误信息',ls_err)

③ excel导出


string ls_err
string ls_pathName,ls_FileName //路径+文件名,文件名
long ll_Net

if dw_3.rowcount() = 0 then
ls_err = "整合数据为空,不能导出"
goto err
end if

uo_wait_box luo_waitbox
luo_waitbox = create uo_wait_box
luo_waitBox.OpenWait(64,RGB(220,220,220),RGB(20,20,20),TRUE,"正在导出 ", 8,rand(6) - 1)

long rows
CreateDirectory("tmp")
uo_datawindowex dw
dw = create uo_datawindowex

ll_Net = GetFileSaveName("选择路径",ls_pathName,ls_FileName,"xlsx","Excel文(*.xlsx),*.xlsx")

rows = dw.ExportExcelSheet(dw_3,ls_pathName,true,true)
destroy dw
destroy luo_waitbox
MessageBox("提示信息","成功导出 " + string(rows) + " 行数据")

return 0

err:
messagebox('错误信息',ls_err)

五、最终效果


财务辅助系统


这次分享就到这吧,★,°:.☆( ̄▽ ̄)/$: .°★ 。希望对您有所帮助,也希望多来几个这样的朋友,不多说了, 蹭饭去了


我们下期再见ヾ(•ω•`)o (●'◡'●)


作者:程序员晓凡
来源:juejin.cn/post/7404036818973245478
收起阅读 »