注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

JavaScript运算符及优先级全攻略,点击立刻升级你的编程水平!

在编程的世界里,运算符是构建逻辑、实现功能的重要工具。它能帮助我们完成各种复杂的计算和操作。今天,我们就来深入探索JavaScript中运算符的奥秘,掌握它们的种类和优先级,让你的代码更加高效、简洁!一、什么是运算符运算符,顾名思义,就是用于执行特定操作的符号...
继续阅读 »

在编程的世界里,运算符是构建逻辑、实现功能的重要工具。它能帮助我们完成各种复杂的计算和操作。

今天,我们就来深入探索JavaScript中运算符的奥秘,掌握它们的种类和优先级,让你的代码更加高效、简洁!

一、什么是运算符

运算符,顾名思义,就是用于执行特定操作的符号。

Description

在JavaScript中,运算符用于对一个或多个值进行操作,并返回一个新的值。它们是编程语言中的基础构件,帮助我们完成各种复杂的计算和逻辑判断。

运算符可以分为多种类型,如算术运算符、关系运算符、逻辑运算符等。通过使用不同的运算符,我们可以实现各种复杂的计算和逻辑判断,让程序更加灵活、强大。


二、运算符的分类

1、算术运算符

用于执行数学计算,如加法、减法、乘法、除法等。常见的算术运算符有:+、-、*、/、%、++、–等。

Description

+ 加法运算

  • 两个字符串进行加法运算,则作用是连接字符串,并返回;

  • 任何字符串 + “ ”空串做运算,都将转换为字符串,由浏览器自动完成,相当于调用了String ( )。

-减法运算 *乘法运算 /除法运算

  • 先转换为 Number 再进行正常的运算。

注意: 可以通过为一个值 -0 *1 /1 来将其转换为Number数据类型,原理和Number ( )函数一样。

%求余运算

对一个数进行求余运算

代码示例:

var num1 = 1;
var num2 = 2;
var res = num1-num2; //返回值为 -1
var res = num1*num2; //返回值为 2
var res = num1/num2; //返回值为 0.5——js中的除法为真除法
var res = num1%num2; //返回值为 1
console.log(res);


2、关系运算符

通过关系运算符可以比较两个值之间的大小关系,如果关系成立它会返回true,如果关系不成立则返回false。常见的比较运算符有:==、!=、>、<、>=、<=等。

> 大于号

  • 判断符号左侧的值是否大于右侧的值;

  • 如果关系成立,返回true,如果关系不成立则返回false。

>= 大于等于

  • 判断符号左侧的值是否大于或等于右侧的值。

< 小于号

  • 判断符号左侧的值是否小于右侧的值;

  • 如果关系成立,返回true,如果关系不成立则返回false。

<= 小于等于

  • 判断符号左侧的值是否小于或等于右侧的值。

非数值的情况

  • 对于非数值进行比较时,会将其转换为数字然后再比较。

  • 如果符号两侧的值都是字符串时,不会将其转换为数字进行比较,而会分别比较字符串中字符的Unicode编码。

== 相等运算符

  • 两者的值相等即可。

  • 比较两个值是否相等,相等返回 true,否则返回 flase。

  • 使用==来做相等运算

特殊:

console.log(null==0);  //返回 false
console.log(undefined == null); //返回true 因为 undefined衍生自null
console.log(NaN == NaN); //返回 false NaN不和任何值相等

isNan() 函数来判断一个值是否是NaN,是返回 true ,否则返回 false。

Description

=== 全等

  • 两者的值不仅要相等,而且数据类型也要相等。

  • 判断两个值是否全等, 全等返回 true 否则返回 false 。

!= 不相等运算符

  • 只考量两者的数据是否不等。

  • 比较两个值是否不相等,不相等返回 true,否则返回 flas。

  • 使用==来做相等运算。

!== 不全等运算符

  • 两者的值不仅要不等,而且数据类型也要不等,才会返回true,否则返回false;

  • 判断两个值是否不全等,不全等返回true,如果两个值的类型不同,不做类型转换直接返回true。

var num1 = 1;
var num2 = '2';
var res =(num1 !== num2); //返回值 true
console.log(res);


3、逻辑运算符

用于连接多个条件判断,如与、或、非等。常见的逻辑运算符有:&&、||、!等。

Description

&& 与

&&可以对符号两侧的值进行与运算并返回结果。

运算规则:

  • 两个值中只要有一个值为false就返回false,只有两个值都为true时,才会返回true;

  • JS中的“与”属于短路的与,如果第一个值为false,则不会看第二个值。

|| 或

  • ||可以对符号两侧的值进行或运算并返回结果

  • 两个值中只要有一个true,就返回true;

  • 如果两个值都为false,才返回false。

JS中的“或”属于短路的或,如果第一个值为true,则不会检查第二个值。

! 非

!可以用来对一个值进行非运算,所谓非运算就是值对一个布尔值进行取反操作,true变false,false变true。

  • 如果对一个值进行两次取反,它不会变化;

  • 如果对非布尔值进行元素,则会将其转换为布尔值,然后再取反;

  • 所以我们可以利用该特点,来将一个其他的数据类型转换为布尔值;

  • 可以为一个任意数据类型取两次反,来将其转换为布尔值;原理和Boolean()函数一样;

非布尔值的与 或 非

非布尔值的与 或 非( 会将其先转换为布尔值, 再进行运算 )

代码示例如下:

var b1 = true;
var b2 = false;
var res = b1 && b2; //返回值为 false
var res = b1 || b2; //返回值为true
console.log(res);


4、赋值运算符

用于给变量赋值,如等于、加等于、减等于等。常见的赋值运算符有:=、+=、-=等。

将右侧的值赋值给符号左侧的变量。

=   右赋给左
+= a+=5 等价于 a=a +5;
-= a-=5 等价于 a=a-5;
*= a*=5 等价于 a=a*5;
/= a/=5 等价于 a=a/5;
%= a%=5 等价于 a=%+5;


5、其他运算符

还有一些特殊的运算符,如类型转换运算符、位运算符等。这些运算符虽然不常用,但在特定场景下会发挥重要作用。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!


三、运算符的优先级

在JavaScript中,不同类型的运算符具有不同的优先级。优先级高的运算符会先于优先级低的运算符进行计算。了解运算符的优先级,有助于我们编写出正确、高效的代码。

以下是一些常见运算符的优先级(从高到低):

  • 括号:( )
  • 单目运算符:++、–、!、+、-、~、typeof等
  • 算术运算符:*、/、%、+、-等
  • 比较运算符:<、>、<=、>=、in、instanceof等
  • 相等运算符:==、!=、===、!==等
  • 逻辑运算符:&&、||等
  • 赋值运算符:=、+=、-=等

掌握了这些运算符及其优先级,我们就可以根据实际需求灵活运用,编写出更加高效、简洁的代码。

通过了解JavaScript中的运算符及其优先级,我们可以更好地编写和理解代码。掌握这些知识,你将能更加自如地操纵数据,实现你想要的功能。

收起阅读 »

深圳发布重大开源项目申报指南,助推OpenHarmony生态发展

OpenAtom OpenHarmony(简称“OpenHarmony”)是面向全场景、全连接的智能终端操作系统。自2020年开源以来,在共建单位的持续努力下,目前已成为发展速度最快的开源操作系统之一。深圳市作为中国软件名城,高度重视开源生态建设,积极把握开源...
继续阅读 »

OpenAtom OpenHarmony(简称“OpenHarmony”)是面向全场景、全连接的智能终端操作系统。自2020年开源以来,在共建单位的持续努力下,目前已成为发展速度最快的开源操作系统之一。深圳市作为中国软件名城,高度重视开源生态建设,积极把握开源软件产业发展的战略性机遇,从供给侧和需求侧发力,积极出台产业扶持政策,推动开源软件产业高质量发展。

描绘开源软件产业发展蓝图

2022年6月30日,深圳市工业和信息化局率先发布《深圳市关于加快培育鸿蒙欧拉生态的若干措施(征求意见稿)》,通过制定专项政策,培育产业主体、深化应用牵引等多项措施,推动开源生态发展与应用,助力数字经济产业创新,在开源软件产业发展道路上迈出坚实的步伐。

2022年10月25日,深圳市工业和信息化局出台《深圳市推动软件产业高质量发展的若干措施》,通过支持搭建公共技术服务平台,鼓励加快开源软件推广应用等举措,为开源软件产业的培育和发展提供指引。

明确开源软件产业发展路径

2023年1月20日,《深圳市工业和信息化局软件产业高质量发展扶持计划操作规程(征求意见稿)》的发布,细化供给侧和需求侧方案,设立开源贡献奖励机制、培育重大开源项目的商业发行版企业、鼓励智能终端产品的开发及打造应用示范项目,提高政策可实施性。

2023年7月28日,《深圳市推动开源鸿蒙欧拉产业创新发展行动计划(2023—2025年)》正式印发,明确提出培育企业、吸引人才和壮大产业组织等任务,实现技术前沿引领、产业集聚效应、应用场景多元化等目标,规划深圳开源软件产业发展路径。

2023年8月25日,《深圳市工业和信息化局软件产业高质量发展项目扶持计划操作规程》发布,通过资助和奖励机制,鼓励和支持软件企业、智能终端产品生产企业等各方参与开源操作系统的开发、应用和推广,细化开源软件产业发展扶持政策。

加快开源软件推广应用

2024年4月28日,《市工业和信息化局关于发布2024年软件产业高质量发展项目重大开源项目相关申请指南的通知》,聚焦重大开源项目商业发行版软件推广应用与芯片模组采购两个核心项目,明确专项资金项目专项审计通用原则和标准、企业申报端操作指引,组织开展2024年软件产业高质量发展项目重大开源项目相关申请指南的申报工作,为生态伙伴申报提供详细的指引。

深圳开源软件产业政策的发布,鼓励企业积极开发OpenHarmony商业发行版与设备,截止目前,吸引深圳OpenHarmony生态伙伴近百家,全面激发开源生态的创新活力。未来,期望更多城市出台OpenHarmony相关开源软件产业政策,推动开源软件产业迈向高质量发展阶段,为数字经济强国建设注入源源不断的动力。

关于OpenAtom OpenHarmony

OpenAtom OpenHarmony(简称“OpenHarmony”)是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony 开源三年多来,社区快速成长,版本已迭代到OpenHarmony 4.1 Release,有超过7500 名共建者、70家共建单位,贡献代码行数超过1亿行。截至2024年4月25日,OpenHarmony 开源社区已有超过250家伙伴,累计已有210个厂家的559款产品通过兼容性测评,其中软件发行版44款,商用设备303款,覆盖金融、超高清、教育、商显、工业、警务、城市、交通、医疗等领域。OpenHarmony社区已成为“下一代智能终端操作系统根社区”,携手共筑万物互联的底座,使能千行百业的数字化转型。

收起阅读 »

减肥 & 恋爱 - 2023年度总结

前言 大家好, 我是前夕. 2023已经过完了, 我也想简单聊聊这一年发生的事情. 今年发生的事情不多, 但是都足以改变我未来人生的走向. 五个月减肥32斤 今年最大最大超级无敌大的改变, 就是减肥了. 我是95年生人, 工作已经5年了. 这个年龄段相当多的人...
继续阅读 »

前言


大家好, 我是前夕. 2023已经过完了, 我也想简单聊聊这一年发生的事情. 今年发生的事情不多, 但是都足以改变我未来人生的走向.


五个月减肥32斤


今年最大最大超级无敌大的改变, 就是减肥了. 我是95年生人, 工作已经5年了. 这个年龄段相当多的人身体都已经出现了一些警告信号. 其实肥胖就是最早的不痛不痒的信号. 我的老粉也都知道我花了半年时间减掉30多斤的事儿. 作为年终总结很重要的一趴, 我还是要简单提一下. 其实之前我一度认为胖不胖的无所谓, 人生苦短, 怎么开心怎么来. 但是相信我, 瘦下来的快乐是你无法想象的.


这是今年3月的我. 身高170, 体重162. 你也可以称我为正方形战士.


这是今年3月的我. 身高170, 体重162. 你也可以称我为正方形战士.


这是今年3月的我. 身高170, 体重162. 你也可以称我为正方形战士.


image-20230924145422838


image-20240106150356245


这是今年9月的我. 身高170, 体重130. 你也可以称我为猛男.


这是今年9月的我. 身高170, 体重130. 你也可以称我为猛男.


这是今年9月的我. 身高170, 体重130. 你也可以称我为猛男.


image-20240106160113246


image-20240106150717340


肯定有朋友会问, 减肥成功的正脸照是不是美颜了? 答案是的, 但是, 第一张也美颜了, 且是同一部手机. 一荣俱荣一损俱损. 另外, 我必须得说明, 我很清楚自己的颜值缺陷, 是眉毛太淡了, 因为小时候眉毛受过伤, 所以特别淡. 于是做了纹眉. 别的没了. 主要差别其实还是减肥带来的, 只要你胖, 怎么样都不好看. 只有瘦下来, 你捯饬自己才有效果.


接下来说说身材. 健身的人都懂, 肌肉身材往往需要阴影的配合. 确实是这样的. 所以我也放出我正面直拍的照片.


image-20240106163727183


就半年时间我也练不到多猛. 现在这个肌肉量已经相当可以了, 毕竟我的起点是个肥宅. 回顾减肥的历程, 只有减过肥的人知道这有多难. 虽然我早就结束减肥了, 但是我仍然觉得五个月32斤是个很夸张的数字. 其实减肥带来的好处, 我真的是一时半会说不完, 只能说我在体重恢复正常时, 看着镜子里一身腱子肉的自己, 我好像宇智波斑解除了秽土转生一样, 只能用青春正当时来形容自己.


image-20230924150305870


相信很多同学看到这会打鸡血, 表示自己24年也要减肥! 首先我希望你不要向我的变化程度看齐. 因为我付出的代价非常大. 运动, 只是一环, 还有很多其他方面. 而我身边(包括朋友圈)真正减肥成功的, 不到半只手. 难度真的挺大的. 如果你坚持就想要减肥, 非常好. 那么我推荐你可以看看我的方法论: 五个月减肥32斤, 涅槃重生也不过如此


交往了00后女友


在去年十月底和现在的女朋友谈了. 这段故事我想简单说下, 有点魔幻. 其实我刚刚工作的时候就认识她了. 是网上认识的, 相谈甚欢, 甚至她明确表达过喜欢. 但是因为异地的问题(她在成都我在上海), 所以双方都pass掉了. 后来也就过年发下祝福啥的, 日常都不联系. 我也理解, 因为对于没有结果的喜欢, 没有人会一直坚持. 我也只是希望大家就做朋友就好. 直到去年我老是刷到一家外卖叫料可可炒饭. 而她的小名也叫可可. 我很多次想截图发她, 但是都没行动. 因为没什么意义, 我又没指望什么, 我连聊都懒得聊. 但是确实经常点外卖就能看到. 后来我就忍不住了就发她了.


image-20240106170401148


我没想到她怎么还找上话题了. 我只是单纯想和她分享一下而已. 但是她既然说了别的话题, 行吧, 那我就陪你聊下, 不然不回人家显得我很冷血. 结果越聊越high, 当晚就打视频, 她还是几年前的那个模样, 而此时, 我已经减肥成功, 她都不知道我胖过. 全程和我聊的也很开心, 一瞬间不知道到底是她和我聊的来, 还是我和她聊的来, 还是双方真的聊的来. 对线细节不说了, 简单几次出招后她就摊牌了.


她: "如果你在成都就好了, 我想和你谈恋爱".


当她说完这个话, 我就想挂电话了. 其一是我觉得她上头了. 其二是我真的很困, 当时已经凌晨4点了. 但是我却很难入眠. 那晚我一直在思考一个问题. 我喜欢她吗? 说实话, 都那么久没联系了, 你说喜欢不喜欢的, 只能说还行, 毕竟还没谈, 喜欢不是理性的产物, 至少我还是很愿意和她相处的, 且确实聊的非常开心. 不知道大家能不能get到遇到一个同频的人有多难得.


然后我就思考第二个问题, 我和她没在一起的原因是什么? 之前她一直想待在成都, 而我对成都其实也没什么感情. 之前一直想去杭州(大学在那边读的), 因为无法解决地域问题就没继续了. 但是工作久了, 发现在杭州的朋友慢慢的也都离开杭州了, 杭州对我来说, 也已经没有多大意义了. 我对于待在哪个城市, 不是很有所谓. 那这不正好吗? 去成都呀. 也就是说, 4年前的我和她, 地域问题导致我们并不合适. 但是现在, 双方都有能力选择自己想要的生活.


所以第二天晚上问她要不要视频. 我当时想好了, 她可能确实是聊上头了才说喜欢的. 所以如果她拒绝接听视频, 那我就当什么都没发生. 成年人嘛, 这都基操了. 但是她秒接视频. 于是我就说了下面这句话


"你有没有想法把我们的关系再推进一些?"


她明显愣了下, 我看出了她也是在考虑地域问题. 她肯定想不明白地域这个问题该咋解决.


我赶紧补充道: "我知道你想待在成都. 对于以后定居成都的事儿, 我也不是很介意. 但是, 这不是一个飞机票的事情. 咱们倒推下, 假如我们在成都生活, 前提是我们一起攒够钱. 而这个前提, 是我作为程序员, 只有在大城市才有比较好的就业机会. 再往前, 那你得先来到上海和我一起赚钱, 再往前, 我们得是情侣, 再往前, 需要判断我们是否真的合适在一起. 那么怎么判断我们是否合适在一起呢?"


经常谈恋爱的朋友们肯定知道, 判断一个人能不能和自己谈, 不可能需要个把月的时间. 基本上相处几天就能确定大概方向了, 再慢不过半个月, 如果有朋友觉得这个速度太快了, 那只能说明你的段位太低了.


"我们可以出来玩一下, 就知道是否适合谈恋爱了. 如果适合, 我们就试着在一起. 不适合, 就当一切都没发生. 就算在一起, 我们也一定是要双向奔赴的. 不是我努力就能有结果, 也不是你单方努力就能有成效. 双向奔赴一定是我们的唯一解. 我下周再买机票来见你, 我给你一周的时间考虑要不要见面"


不知道是不是她没听到最后一句话, 她马上打开了boss开始看上海的工作机会了. 后来我们的见面也如期而至, 当天晚上就在一起了.


目前谈了2个多月了, 她还没来上海, 因为她行业的问题(媒体), 她哪怕去北京都很容易找, 但是在上海反而很难找. 这个原因涉及到一些敏感信息, 我不方便在这里解释. 总之, 她一时半会确实很难过来, 只能说在尝试.


目前我们是一个月见一次, 每天晚上就是打视频+玩蛋仔派对. 因为异地, 每天只能在游戏里约会.


image-20240107164714324


image-20240107170248689


我能感受到她确实是在向我奔赴而来. 之前我们公司经历了几次裁员, 我和她商量, 要不要我现在去成都. 不想异地恋了. 她和我说


"你不要为了我来成都, 你优先考虑哪个地方对你的工作是最有利的, 不管你在哪里, 我都会想方设法靠近你" 当时我真挺感动的.


我的房租是半年付的, 一次性交了1w多, 我和她分享说1w多好贵啊. 我只是单纯地分享, 而她犹豫了下说要不然我们2个月见一次吧. 我还愣了下, 为啥突然这么说. 随后我就反应过来她是希望降低我的经济压力. 对此我肯定是不会同意的.


类似的案例有很多, 不一一撒狗粮了. 因为每个月只能面对面地抱在一起三天时间(周末+年假一天). 所以最后天其中一个人就要去机场返程了.


image-20240106182920302


每次我们送别彼此都会很难受. 上次我去成都返程, 我前脚刚走, 她就泪崩了. 其实她比我坚强. 如果是我送她离开上海, 一般是她还没走我就已经难受的不行了.


现在男女冲突挺严重的, 我时常在沸点看到jym不是女朋友想方设法让你送礼物, 就是对象只顾着自己不顾家, 甚至上次还有个离婚了等着要分老公年终奖的. 这样的男女矛盾屡见不鲜. 我也很庆幸遇到了一个双向奔赴的女孩, 我们都在尽自己所能给彼此最好的生活. 为了保护女友, 我还是不放人家照片了. 随便放一张意思下.



网上一直有个争议很大的话题, 就是选择一个你爱的人 还是 爱你的人. 我现在的答案是应该选择一个我爱的人, 因为想起她, 我会充满干劲. 而更幸运地是, 她同时也爱着我.


思想更加开阔


去年看了点书, 不多, 就几本, 不到一只手. 但是我受益良多. 很多人觉得幸福是客观存在的, 我有大鱼大肉吃就是幸福, 我可以不上班就是幸福. 但是这是真的吗? 其实不完全是. 幸福是主观的. 引用某著作的一段话



淘宝和拼多多上的基础款羽绒服也比清朝最好的棉衣要暖和轻便;慈禧兴师动众劳民伤财的在北京城的数个地点开凿了上万平米的冰窖,只是为了将冬天的冰存到夏天来祛暑。而现在每一个装有空调的家庭都能在这一点上比慈禧过的更舒服。甚至是经济条件不足以购买空调、支付电费的当代中国人,也可以去地铁站、图书馆、商场等公共场所享受这种超过老佛爷的体验。



但是我问大家一个问题, 慈禧的幸福感比你差吗? 如果幸福是绝对依赖客观世界, 那么随着科技的发展, 大家应该越来越幸福才对. 但是现实是这样的吗? 显然不是. 当然, 我们不能否认客观世界对幸福的影响. 比如疼痛, 肯定是不幸福的. 有钱的话可以体验更好的医疗, 让疼痛不那么多, 这也是幸福. 但是我想强调的是, 幸福并不100%取决于客观世界. 在这里我是希望大家不要忽略主观想法对幸福感的影响. 这也是为什么说心态很重要的一个原因. 还有很多想说的, 但是我写完又删了, 因为哲学的东西讨论起来, 确实非常依赖你的经历和你所处的精神层次, 甚至会引发大家的争吵, 想想算了, 反正我觉得很多事儿我想的比之前明白了, 也没有了精神内耗, 现在每天都挺开心的.


结语


我不喜欢规划, 所以也不会说什么24年要怎么怎么样, 如果有, 那也只是说说而已. 因为说是说, 做是做. 而生活, 总是见招拆招. 在这里, 我只想祝福大家天天开心!


作者:前夕
来源:juejin.cn/post/7320541744352854057
收起阅读 »

致青春 → 十年了,她依旧历历在目

开心的一刻 一老大爷坐火车,买的是慢车票,却上了快车 乘务员查到了,对他说:“老人家,你的票要补哦” 老人家听了眼一瞪说:“上面的洞洞是你们剪的,咋喊我补呢?” 乘务员傻眼了,解释到:“不是票坏了喊你补,你买的票是慢车票,这趟车是快车,你应该补快车票” 老人大...
继续阅读 »

开心的一刻


一老大爷坐火车,买的是慢车票,却上了快车

乘务员查到了,对他说:“老人家,你的票要补哦”

老人家听了眼一瞪说:“上面的洞洞是你们剪的,咋喊我补呢?”

乘务员傻眼了,解释到:“不是票坏了喊你补,你买的票是慢车票,这趟车是快车,你应该补快车票”

老人大悟,说道:“哦!是这样啊;那你喊司机开慢点吧,我又不赶时间”


骑猪.gif


十年的回忆


今天无意之间听到了Eason十年,突然意识到我已经大学毕业十年了

十年,经历了很多,也成长了很多

当初的青涩已不复存在,留下的只有无尽的沧桑

唯一不变的是,我依旧孑然一身


单身的牢笼.gif


这十年

有轻松快乐的游戏生活

也有战战兢兢的职场蹉跎

经历了说走就走的旅行

也经历了痛彻心扉的爱情

从当初的意气风发,到如今的随波逐流

终究活成了当初最讨厌的样子!


他好像一条狗呀.gif


要问这十年间印象最深的一次经历是什么

毫无疑问是十年前的川藏线之旅!


无意的决定


2013年6月的某一天,好哥们(阿超)突然发来QQ消息:我们去骑川藏线吧

我很淡定的回道:好啊


我们去骑行呀.gif


然后阿超又约上了另一个好哥们(阿方)

至此,三兄弟的川藏线协议就此达成

约定好.jpg


从左往右:阿超、楼猪、阿方


"充分"的准备


说到准备,我只能说我们是:无知者无畏


还有谁.gif


装备准备


阿超是我们三个中最早接触骑行的,大三的时候他就购买了他人生中的第一辆山地车


阿超的山地车.jpg


我记得当时的购入价是1500,他骑着他心爱的座驾,逛了邵阳不少的地方,其中也包括崀山

因为他接触的早,所以除了车之外的装备,都是阿超在淘宝上选购的

包括抓绒衣、防晒服、头巾、手套、冰丝袖、打气筒、补胎套件、驮包、手电筒、尾灯等等

说到抓绒衣,就不得不提一下,也不知道当时是不知道有冲锋衣了,还是预算不够,我们就买了普通的抓绒衣


抓绒衣.jpg


好几次差点成为冰雕,后面我再细说

然后就是我跟阿方的自行车,当时应该是考虑到预算的问题

我们在淘宝上买了2辆,一辆是“悍马”,一辆是“宝马”,还都是折叠车!


悍马与宝马.jpg


最前面的是我的“悍马”,中间的是阿超的美利达,最里面的是阿方的“宝马”

我和阿方的车单价是565,两辆一共1130

现在想想我俩的胆是真肥,这样的车是怎么敢上路的!!!


还有王法吗.gif


至于拍摄装备,当时没想那么多,就各自的手机:阿超的Nokia 5230、我的Nokia 830、阿方的OPPO(型号不记得了,是个翻盖)


攻略准备


攻略也是阿超全权负责,哪一天从哪出发,每一天要到达哪个目的地

考虑到安全、时间、预算等因素,我们一开始就计划住青年旅舍或者当地居民家,没打算户外扎帐篷


川藏线路线.png


川藏线分南线和北线

南线由四川成都雅安泸定康定东俄洛雅江理塘巴塘西藏芒康左贡邦达八宿波密林芝工布江达墨竹工卡达孜拉萨,属318国道

北线成都东俄洛南线重合,再由东俄洛南线分开北上,经八美(原乾宁县)—道孚炉霍甘孜德格西藏江达昌都类乌齐丁青巴青夏曲那曲当雄羊八井拉萨,属317国道

南线相较于北线,平均海拔更低,开发也更早,更容易骑行

所以我们选择了南线,也就是上图中标粗的主线


拉练准备


我们三都没有进行实战型的拉练,阿超相较于我俩,只是平路骑的比较多

长距离的上山、下山,我们都没有试过

在出发前的前一周,我们一起绕着大学骑了三圈

这就算完成了我们的拉练...


刺激的旅途


2013年07月16号,我们正式出发了

坐上了可爱的K487次列车,历经17时49分,于2013-07-17 13:37到达了成都东站


成都东站.jpg


然后我们骑车来到了成都师范学院,在附近找了一间民宿,调整了一晚


出师不利始康定


2013-07-18 06:00正式开始了我们的川藏线骑行之旅

似乎天空不作美,一出门就看见仙女的眼泪,密密麻麻的滴在地面,也滴在了我们的心里

纵使她万般挽留,我们依旧没有丝毫的动摇,毅然决然的出发了

可人算不如天算,成都到康定的这段318路线因暴雨已经封闭,不让通行


挨刀.gif


不知道要封闭多久,其他路线又没有详细的攻略,不敢贸然行动

所以我们选择了一种轻松的方式:坐汽车到康定


坐车.jpg


经过漫长的等待、颠簸,于2013-07-18 22:13,我们到达了康定


康定.jpg


下车后,急忙找了一间民宿,那时是真的困,我们很快就进入了梦乡

婴儿般的睡眠,很是怀恋


初尝失算新都桥


2013-07-19清晨,正式开始了我们的骑行之旅

天空些许阴沉,冷风中夹带着细雨,零零散散的行人,时不时的哈气、搓手

此时的我们异常兴奋,直奔着下一站(雅江)急速而去

骑行了17公里之后,我们来到了折多山脚,此时已是上午的09:45,距离折多山垭口还有35公里

心里想的是:哼,才区区35公里,那不是张飞吃豆芽,小菜一碟?

可骑着骑着,我们发现速度并没有比徒步的快,似乎还有被超的迹象!

13:00,此时距离垭口还有13公里,我们的锐气已荡然无存,高反已悄然而至

加上没有准备足够的干粮,我和阿超已明显感觉不适,眼前发黑,停下车,靠着路边的防护栏呕吐起来

阿方见状,在路边的摊贩买了两瓶红牛(一罐貌似是八块!),递给了我跟阿超

喝了红牛之后,我们在路边坐了将近一个小时,状态才基本恢复,顶着饥饿继续前进

17:47我们终于到达折多山垭口


折多山垭口.png


此时天空下起了雨,还伴随着一粒粒的冰雹,放眼望去,哪有栖息之所?

我们只能继续赶路,赶往45公里外的新都桥(至于既定的目的地:雅江,一点想法没有了)

一路下坡,一路狂飙冷冷的冰雨在脸上胡乱的拍,呼呼的狂风在耳边肆意的啸,很快全身湿透,体温急骤下降

全身开始哆嗦,嘴唇逐渐变紫,直至发黑,更让我绝望的是小腿开始抽筋,丝毫不敢用力

望着渐行渐远的两个小伙伴,我甚至连呼喊的力气都没有了,隐隐约约看见死神在逼近

擦干眼睛,定神一看,那不是死神,那是我的两个兄弟!

看着摇摇晃晃的我,他们拼尽最后的力气拦停了我的车,将我从车上艰难的扶下了车

我们用尽最后的力气把车推到了路边的休息区


新都桥休息区.jpg


望着被紧紧绑住的驮包,我们陷入了绝望,尝试了几次,弹力绳纹丝不动

也许是上天怜悯,一辆温馨的小轿车在我们旁边停了下来,从车里下来了一个帅气的大哥

在他的帮助下,我们终于换上了干衣服、干鞋子,而此刻雨也停了

时间已经来到了19:57,天空还剩最后的一丝余亮,距离新都桥还剩9公里,我们继续前行

因为太过饥饿,这9公里显得格外的遥远

当看到路边藏民家的灯的时候,我们决定停下了(此时距离新都桥还剩3公里)

热心的藏民同胞给我们安排了房间,还给我们准备了丰盛的晚餐


新都桥晚餐.jpg


吃饱之后,倦意席卷而来,很快我们就进入了梦乡

不幸的是,第二天阿超就感冒了,我们只能休整一天,顺便把湿衣物吹干(吹风机慢慢吹)


一秒入睡在雅江


在新都桥休整好后,我们继续出发,朝着雅江而去

翻越了4412米高的高尔寺山


高尔寺山.jpg


翻山随难,但不似折多山那般,也没了高反,一切顺利了很多

从山顶顺坡之下,犹如脱缰的野马,飞速疾驰

车轱辘似乎也放肆了起来,隐隐有要单飞的感觉,我时不时的紧一紧刹车

16:00,我们到达雅江,感觉还早,我们继续往前赶路

又骑了一个半小时,困意席卷而来,忌惮于折多山的余威,我们决定停下休息

阿超去点菜的间隙,我和阿方已经进入了梦乡


雅江梦乡.jpg


困,是真的困!

上完菜后,阿超细声的呼唤着我俩:醒醒,吃饭了!

三人狼吞虎咽,将菜一扫而光,所幸饭可以无限续

饿,也是真的饿!

吃饱喝足,进行洗漱整理,伴随着黑夜的降临,上床入梦


一分为二入理塘


早上六点我们就出发了,今天的目标是130公里外的理塘

距离不是很远,但有两座大山,不会那么容易的


叶问_没那么容易的.gif


经历了前几天的磨砺,我们已经基本适应,虽说速度依旧慢,但身体已没有不适

一路晴空万里,蓝天白云,漫游在山坡上,内心纯粹无比


雅江_理塘 晴空万里.jpg


顺着超扁的S型盘山公路在山坡上蠕动,内心毫无杂念,一心就想着上垭口

终于于15:00到达4659米高的剪子弯山垭口


剪子弯山.jpg


不敢做过多的逗留,休整片刻后我们继续往前赶路

下坡是所有骑友的最爱,其中也包括我

但依旧不敢完全松开刹车,任由我的“悍马”驰骋,左边时常经过的大卡车,右边深不见底的深渊,时刻告诫着我们不能掉以轻心

伴随着夜幕的降临,我们已经身心俱疲,但依旧没有找到可以落脚的地方

打开前后车灯,继续往前骑,伴随着一阵阵的狼嚎,终于在21:35找到了一处藏民的帐篷

此刻我们饥寒交迫,没有任何赶路的想法了

和藏胞谈妥后,悬着的心终于放下了,围着火炉坐下,感受着暖意的扑面而来

藏胞给我们热了酥油茶,一口下肚,暖流入胃,奶香上鼻

还给我们煮了牛肉面,粒粒牛肉,片片白菜,根根面条,在白汤的滋润下,鲜香无比

吃饱之后困意如期而至,宽大的帐篷下簇拥着好几张床,挑了心仪的一张后安然入睡


雅江_理塘 帐篷.jpg


第二天清晨,我们看到了高大威猛的藏獒、天真浪漫的小牦牛、任劳任怨的母牦牛,在晨光的照耀下,是那么的静谧与美好


雅江_理塘 牦牛.jpg


和藏胞进行了短暂的告别后,继续我们的旅途,朝着理塘而去

途中翻越了卡子拉山


卡子拉山.png


剪子弯山理塘的路程,整体海拔是下降的,整个路线也是下坡居多

卡子拉山只是其中少有的上坡的小插曲,上升高度很低,少了翻山的难度,也少了翻过的兴奋

翻过卡子拉山后,阿方的“宝马”开始兴奋起来了,后轮出现了很明显的左右摆动,像是在告诉我们:来吧来吧,一起摇摆!


一起摇摆.gif


我和阿超赶紧跟上去,打断了阿方的兴奋,三个人一起下车,推车前行

边走边拦截路过的四个轱辘 ,希望能拦停好心人,将阿方连人带车一起带去理塘

也许是上天刻意的考验,四个轱辘都是从擦身而过,除了带起一片尘烟,什么也没有留下

推了一段距离后,我们决定阿方去最近的158道班(类似一个小驿站)修车和休整,我和阿超先去理塘等阿方

这里说明下:不是我和阿超“抛弃”了阿方,是考虑到158道班很小,而一路上骑行的驴友很多,我们三个人都去的话,可能住不下,另外就是身上的现金已所剩无几,需要去理塘取钱了


158道班.png


阿超(“独揽财政大权”)将身上本不多的现金一大半给了阿方(计划是去理塘取现金的),然后将阿方驮包中的馕、需要清洗的衣服拿了过来

阿方推车朝着158道班而去,我和阿超则骑车奔着理塘而去

在日落前我和阿超赶到了理塘


眺望理塘.jpg


理塘东城门.jpg


找了一间旅馆,卸下行囊,把需要清洗的都清洗好之后,我和阿超开始了啃馕

第二天接着啃馕,等着阿方的到来

16:40,阿方来了,加入了啃馕队伍,晚上三个人一起啃馕,馕好像变香了!


日行百八飞巴塘


经过一晚的休整,三人状态都恢复的不错

理塘巴塘有将近180km ,早上六点我们踏上了前往巴塘的旅程

出了理塘西城门,就来到了毛垭大草原 ,群山环抱,郁郁葱葱,停车驻足,心旷神怡


毛垭大草原.jpg


路况非常好,视野很开阔,一眼过去,直达天际

花花草草的地下隐藏着很多大家都很熟悉的小可爱,没错,就是它:


土拨鼠.gif


大概下午四点,我们登上了海子山垭口,看到了柔美的姊妹湖,湖水碧蓝,恬静而温婉,堪称人间仙境


姊妹湖.png


停下车,快速的奔向姊妹湖,近距离的欣赏、感受着姊妹湖,内心逐渐平静,身心的疲惫也慢慢消散

纵有万般不舍,依旧要往前行

收拾心情,八十千米的下坡,我们来了!

一路下坡,穿过好几个隧道后,终于在晚上九点多来到了巴塘胖姐休闲庄

我们这一次终究还是来得太迟,错过了床才有的温馨舒适,酝酿好久终放下对床的相思,最后客厅过道成地铺地址(改编自歌曲太迟


出川入藏至芒康


即将入藏,无比期待,早早的就出发了

沿着巴河南下,很快来到了金沙江


金沙江.jpg


一似渭,一似泾,汇合似渭泾,实属难得的景观
来到金沙江大桥,望着西藏的界碑


西藏界碑.jpg


想着即将见到魂萦梦绕的她,激动万分

停车回首,感慨颇多


四川界碑.png


来不及好好告别,空留一段,记忆的线,系不下长长的哀恋 ,却魂绕梦牵,恍惚中又和你相见(摘自歌曲

经过长长的排队,检查了身-份-证,登记了基本信息,我们终于进藏了,梦里的她,我们来了!

经过漫长的缓上坡,于下午七点左右,我们来到了海通兵站,在海通兵站的斜对面找了一个落脚点:扎西德勒藏餐馆


扎西德勒藏餐馆.jpg


二楼一个大房间内,床挨着床,放置好行李后,发现二楼没有洗手间

来到一楼询问老板娘:你好,请问洗手间在哪?

老板娘:洗手间?

我:厕所在哪?

老板娘向屋后指了一下,然后画了一个圈,好像在说:屋外都行

疑惑的我们来到屋外,向河边走去,突然从河边的深草中站起一个女孩,时不时的整理身上的衣服

我们三个面面相觑,顿时悟了:诺大的露天洗手间,河边、山间都可以大小恭,于是我们在山边的灌木林中解决了排泄问题

晚上躺在床上快入梦乡时,陆陆续续来了很多藏胞走进了隔壁的房间,不一会就响起了嘹亮的歌声;原来隔壁是个KTV

伴随着他们“优美的歌声”,我们迟迟未能入睡,听又听不懂,说又不敢说,只能强迫着自己尽力去“欣赏”

好不容易入睡了,结果又赶上两大狗帮在街斗,狗吠声很响亮,听着有大几十只

也不知道斗了多久,它们终于散去,至于谁输谁赢,无从知道

那晚,我也不知道睡着了多长时间

要不得说,年轻是真好,第二天依旧六点出发,虽说不是十分兴奋,但没那么疲惫

不知不觉就来到了宗拉山垭口


宗拉山.jpg


没有了往日翻山的艰难,似乎也少了翻过之后的兴奋

继续前行,当来到拉乌山垭口的时候,突然乌云密布,豆大的冰雹顷刻间就落下


拉乌山.png


似乎冥冥中注定一般,正好路边停着一辆大货车,暂时寄居在它的庇护下

高原地区的雨雪,来的突然,去的也突然,不一会又晴空万里了

经过35km的长下坡,我们来到了如美镇,找了一间旅舍,停下了脚步,开始清洗衣物


如美镇.jpg


经过一晚的休整,状态恢复的很不错,但今明两天注定很艰难

险峻莫过觉巴,高寒当属东达 , 觉巴山是今天要征服的,而东达山是明天要翻越的
来到觉巴山脚,抬头望去,一排又一排的U型盘山公路,脑瓜子嗡嗡的


觉巴山脚.jpg


公路左边是万丈深渊,右边是怪石嶙峋的峭壁,着实险峻

一路盘山而上,一路心惊胆战,到达觉巴山垭口后


觉巴山.png


迎来了短暂的下坡,在登巴村进行了短暂的休息后,继续前行,赶往荣许兵站 
终于在晚上八点左右达到了荣许兵站 ,找了一家旅舍,吃饱喝足后开始入睡,明天又是一场鏖战


推上最高下左贡


清晨,天空灰蒙蒙,下着小雨,温度很低

没蹬几步,阿方停下了,他的“宝马”左边的脚踏板掉了,气人的是我们装备里面没有大扳手,没法拧紧,真的是:屋漏偏逢连夜雨,船迟又遇打头风

所以我们仨决定,一同推车翻东达

一路推行,我们超越了好几拨骑行的,他们都露出了怀疑的目光

大概下午两点,我们到达东达山垭口,鞋子和裤子已经湿透,但我们内心却火热无比


东达山.png


盘边帐篷是个补给点,但东西是有点贵,拿起的泡面又放下了

稍息片刻,我们骑上车下坡而去,此时寒意更甚于推车

我们在坡边找到了藏胞家,家里只有老奶奶和小孙子,正好有一堆炭火,我们换了袜子和鞋子,烤干了裤子

给他们留了一些糖果后,我们继续赶路,在下午五点左右到达左贡县城

先找地方修了阿方的脚踏,然后找到了邮政银行取了现金,还补充了干粮

吃吃喝喝洗洗后,很快就进入了梦乡


坑坑洼洼颠邦达


左贡邦达,绝大部分是砂石路、搓板路,很伤车,也很伤人

路面坑坑洼洼,四个轱辘经过,要么溅你一身泥,要么扬你一脸灰

所幸今天不用翻山,但骑行速度不比翻山快

在天黑之前,还是到达了邦达


邦达.png


镇上的旅舍基本都满了,我们最终选择了离镇不远的藏胞家

广场上很多藏獒,并非印象中的威猛霸气


邦达广场藏獒.jpg


是放养,还是流浪?不得而知


七十二拐拐八宿


清早出发,很快就开始了盘山,爬了两个多小时后,来到半山腰,回头一看,邦达近在咫尺


回望邦达.jpg


蓝天白云,群山环绕,河流穿过,还有大草原,辽阔壮美,一览无遗

继续盘山,大概十一点,我们到了业拉山垭口


业拉山.png


业拉山垭口有服务中心,自驾的、骑行的、徒步的、朝圣的汇聚于此,或休整、或补给、或拍照、或摄影,热闹非凡

在观景台看到了即将要奔赴的怒江72拐,壮观无比,令人瞠目结舌


怒江72拐.jpg


也得知了它亦称九十九道拐


九十九道拐.jpg


心中窃喜的是下72拐,而不是上

下山时,双手要紧紧放在刹车上,时不时捏一下刹车降一下速度,听说这里出过很多事故,所以我们格外谨慎

快乐与危险并存,天堂与地狱一线,痛并快乐着

从垭口到怒江边上,海拔降了近2000米,短短的几个小时,我们就经历了四季,山顶的冬冷,山腰的春(秋)暖(爽),山谷的夏热

停车稍息片刻,补上几口干粮,继续赶往八宿

今天比较顺畅,天气很好,人和车都很稳妥,早早的就到了八宿县

八宿挺大的,房屋挺多,此刻的气候也很暖和


天不遂愿待然乌


早早的出发,今天的挑战不小

不同于之前的长上坡,也不同于之前的U型盘山公路
70km的反复起伏,总体上坡直至安久拉山垭口,不一样的骑行感受,不一样的骑行困难,就像折多山那次一样,非常难受

但还是坚持了下来,在下午三点左右,我们登上了安久拉山垭口


安久拉山.jpg


休息与拍照自是不可少,天气也很给力,蓝天白云,疲惫感逐渐消散,补充些许口粮之后,继续上路

虽说整体是下山,但却是反复起伏着下坡,心中一万只草泥马奔腾而过

当穿过保护性长廊后


然乌 护廊.png


我们来到了然乌镇


然乌.jpg


找了一家离公路较远、离然乌湖较近的藏民家,有洗浴间,有卫生间,非常不错

一路骑来,都未曾停下好好欣赏周边的风景,和小伙伴商量明天在然乌休整一天,去看看来古冰川

第二天睡了个懒觉,起的比较晚,吃过早餐后,我们租了一辆车去看来古冰川

今天不赶路,就是撒欢!

可惜的是来的季节不对,冰川已离去大半,山顶的冰雪依旧清晰可见,山脚则只有零零星星


来古冰川1.jpg


来古冰川2.jpg


在这里,我们的车队人数达到了最大


来古冰川3.png


从左往右分别是:少帅楼猪阿方琦哥阿超咖啡师阿胜阿凯

一天下来就是看、躺、拍、嬉戏打闹,主打就是一个开心

我们回到镇上吃晚饭,畅聊着接下来的行程,要翻的山只剩两座:色季拉山米拉山,路况也相对会好很多,骑行会顺畅不少

第二天清早,天朗气清,空气清新,我们来到镇上吃早餐

也许是昨天的无限欢乐引起了老天的嫉妒,给我们开了一个巨大的玩笑:昨晚(2013-08-0211通麦大桥断了,整座大桥全部坍塌!

我们一开始都不信,阿超琦哥卸下装备,空骑去核实了,结果属实,但也带回来一丝希望:大桥旁边有一座老桥

老桥宽度不够,只能通行人和自行车,而且年久失修,直接封闭不让通行

我们只能等,期盼着早日把老桥维修好;既然走不了了,那就好好玩乐,当天我们逛了然乌湖


然乌湖.jpg


第二天,一拨人打了一天的升级(扑克的一种玩法),一拨人出去逛了周边

第三天早上,我们来到镇上,打听到老桥还是没有通行,但时间容不得我们继续等下去了(大家都是参加工作,或者是即将参加工作的人)

我们商量决定租车去昌都,然后通过北线拉萨(咖啡师时间比较充足,他决定留下来继续等)


奔昌都.jpg


我们正式开始了四轮之旅!


一路惊魂终拉萨


不得不说,坐车确实舒服不少,就是什么都看不到,丢失了这次旅途的初衷
然乌逆向而行,北上经八宿昌都市

相对而言,昌都要繁华不少,但我们来的比较晚,坐车从南线来昌都的骑友也很多,加上本身就走北线的人

旅店都已经住满,最后派出所收留了我们(不是我们犯了事,实在是没有睡觉的地方了!)
昌都拉萨,具体歇息了几站,不记得了,依稀记得睡过网吧,睡过旅馆等等

昌都之后,司机每天都是重度疲劳驾驶,行驶在悬崖边的公路上,我们提心吊胆,阿超阿胜轮流盯着司机

给司机按摩、喂红牛,一旦司机打盹就拍醒他,时不时放那些激情澎湃的歌曲给司机提神

而我们其他人,貌似没意识到问题的严重性,一个个睡的老香了!

北线的路况比南线要差很多,但风景同样很优美


北线1.jpg


北线2.png


北线3.jpg


2013-08-09早上到达了拉萨


兜兜转转游拉萨


刚进拉萨,一个帅哥开着小车来到了我们旁边,盯着我跟阿方的车,表现出了很浓厚的兴趣

问我们车卖不卖,他的两个小孩一直想要一辆

正好我跟阿方想把车出掉,就问到他能出多少钱

帅哥试了一下我的车,觉得还不错,就说500行不行

然后同伴们就一起吹嘘这两辆车有多好,最后550一辆成交

帅哥开开心心买下了车,放进了后备箱,我和阿方高高兴兴收下了钱,坐上同伴的车快速离去

找了一家岳阳老乡的宾馆,接下来的几天就以此处为大本营了

当天没有出去逛,而是清洗、整理衣物,晚上去网吧打了几把LOL、上传照片

2013-08-10我们开始了逛拉萨西藏博物馆清政府驻藏大臣衙门西藏大学布达拉宫广场大昭寺色拉寺


西藏博物馆.jpg


清政府驻藏大臣衙门.png


西藏大学.jpg


布达拉宫广场.jpg


2013-08-11凌晨2点,琦哥少帅阿胜阿凯排队买票,参观了布达拉宫

阿超阿方觉得太累,就在宾馆休息了

等他们从布达拉宫回来,我们一并去了八廓街,吃当地特色美食,买当地特色纪念品,逛逛买买,甚是悠闲

当我们返程时,发现停在派出所门口的山地车被偷了两辆,琦哥阿胜的车被偷了

小偷是懂车的,琦哥的车5000多,阿胜的车3000多,是我们队伍中最好的两辆车

要知道,这可是2013年!
去派出所请求帮助,说没看没开,也没看到是谁偷走的,无疾而终

很不愉快的回到宾馆,第二天决定去二手车市场碰碰运气

2013-08-12一早,我们一起去了二手车市场,一路看下来,并没有发现琦哥阿胜的车,最后的希望也落空了

下午,我们仨去踩点了拉萨火车站,方便明天的归程


拉萨火车站.png


三天归程脚浮肿


2013-08-13,我们仨和其他小伙伴正式道别,没有依依不舍,分别的很洒脱

阿超的车通过邮寄运回了湖南,上车前买了2斤脆皮蛋糕,一共30元,买了12桶泡面,这些是接下来三天两夜的食物

我们买的是Z266次列车,绿皮的硬座,始于拉萨,途经那曲格尔木德令哈西宁兰州咸阳西安郑州武昌,最后到达长沙

一共要花47小时39分钟,但因为晚点了一个多小时,我们一共坐了49个小时

青海湖非常漂亮,湖边很多飞禽走兽,碧蓝的天空映射在湖面,煞是美丽,此刻只想吟诗一首:落霞与孤鹜齐飞,秋水共长天一色


青海湖.jpg


拉萨(海拔3660多)到西安(海拔400多),海拔降的很快,车上很多人出现了严重的耳鸣,幸亏火车上有随行的医护人员提供帮助

我们仨也出现了轻微的耳鸣

西安进行了换车,阿超用仅剩的钱买了三个肉夹馍,肉没有,夹了不少盐,这不讲武德的商贩欺人太甚!


265.gif


终于在2013-08-15下午一点多,我们到达了长沙

当我准备起身时,发现起不来,低头一看,整个脚背全水肿了,转向他俩一看,一样的情况,估计是坐太久没动的原因导致

轻柔一下脚背,稍微运动一下脚,慢慢的可以起身动起来了

2013-07-162013-08-15,整整一个月(是巧合,还是计划好的?),我们的旅程正式结束!


相关补充


资金


我们这次出行,家里父母是都不知道的,我们也没向家里要钱

通过大学期间的捣鼓:家教售卖二手电脑其他兼职等,我们存下了这次旅行的费用

最初人均预算是4000多,最后超出了预算一丢丢,在可以接受的范围之内

其实是可以拉赞助的,途中我们就遇到了很多拉着捷安特美利达等等横幅的骑友,据说赞助费不菲


火车票


从拉萨到长沙的火车票,需要提前15天预订
骑行途中通过另外一个好朋友帮忙买的,买了之后退的话,怕再次买不到了

这也是我们不能在然乌一直等的原因


照片画质


相信很多小伙伴已经看出了上文中的很多照片,画质喜感

没办法,绝大部分照片使用直板手机拍的,那时候智能机还没普及,手机拍照功能很拉胯

少数几张高画质的是从其他同行小伙伴用单反拍的,大家将就着看吧


感悟


车很重要、车很重要、车很重要,但不是最重要的,最重要的还是发动机(人的意志)、发动机、发动机

祖国很大,广袤的大好河山足够我们欣赏一辈子,国外的月亮不比国内的圆

这趟旅程很是历练,如果时间充裕,强烈建议时常停车驻足,用心去感受这纯天然、无污染的自然景观

有空多出去走走,逛逛,看看,给嘈杂的内心寻找片刻安静的港湾,对调整个人心情甚有帮助

十年前的这次旅行,经历了很多挑战、困难,感受了很多惊喜、刺激,留下了太多不舍、遗憾

最近,再骑一次的声音一直萦绕耳畔,内心的冲动也是愈发强烈,但此时非彼时,有生之年能否再骑一次?


作者:青石路
来源:juejin.cn/post/7324011329882374171
收起阅读 »

Android Region碰撞检测问题优化

前言 众所周知,Region是android graphics一族中比较低调的工具类,主要原因还是在碰撞检测方面存在一些不足,甚至可以说成事不足败事有余,以至于难以用于2D游戏开发,这也是耽误你们成为2D游戏大师路上的一道坎 。既然Region这么失败的工具为...
继续阅读 »

前言


众所周知,Region是android graphics一族中比较低调的工具类,主要原因还是在碰撞检测方面存在一些不足,甚至可以说成事不足败事有余,以至于难以用于2D游戏开发,这也是耽误你们成为2D游戏大师路上的一道坎 。既然Region这么失败的工具为什么要介绍呢,一方面本篇通过路径检测的方式解决了成事不足败事有余的问题,另外一方面我们也要介绍他可用的部分,以及正确的用法。最后,本篇其实主要是通过PathMeasure和Region相互配合,优化了碰撞检测逻辑精确度问题。


预览效果


这是我们最终要达到的效果。


fire_74.gif


异常效果


我们需要重点处理两个问题



  • 没接触到就检测到碰撞

  • 接触已经很多距离了才检测到碰撞


Region 碰撞检测问题



  • Region类能成事的部份主要还是Op布尔操作和矩阵操作,但是这个似乎又和Path的作用重合,不知道是不是因为性能更高呢?本文没有去测试,有机会测试一下。另外一部分containXXX包含关系判断,containXXX能准确的判断点和矩形是不是被包含了,但是其他形状那就没办法了。

  • quickXXX 快速检测方法,返回值true-能确保物体没有碰撞,但false无法确保是不是已经碰撞了,换句话说true是100%没碰撞,但是false还需要你自己进一步确认,不过这点可以作为减少判断的优化方法,但不是判定方法。


学习Region & PathMeasure 的意义


对于一些粒子,我们不太关注大小,这个时候是可以利用中心点去检测的,那对于多边形或者半圆等形状,点是非常多的,显然得找一种更好的方法。实际上看似quickXXX其实用处不大,其实可以减少一部分检测逻辑,quickXXX虽然比不上contains的精确度,但是仍然能检测到没有碰撞,本篇需要了解它的用法,然后配合PathMeasure,实现精确检测。


非Path用法


对于非Path用法,Region还是相当简单的,直接使用set方法即可


mainRegion.set((int) -radius, (int) -radius, (int) radius, (int) radius);

Path方法


这个用法比较奇怪,需要2个参数,最后一个是Region类,弄不好就是鸡生蛋蛋生鸡一样令人迷惑,第二个可以看作被裁剪的区域,如下操作,求并集区域。不过话说回来,这个意义在哪里?


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,10, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);

小试一下


实现开头的图片效果,定义一些Path和形状。


定义一些变量


 private float x; //x事件坐标
private float y; //y事件坐标

//所以形状
Path[] objectPaths = new Path[5];
//形状区域检测
Region objectRegion = new Region();

//小圆球区域
Region circleRegion = new Region();
//小圆
Path circlePath = new Path();
//绘制区域
Region mainRegion = new Region();

构建物体


三角形、圆等物体


for (int i = 0; i < objectPaths.length; i++) {
Path path = objectPaths[i];
if (path == null) {
path = new Path();
objectPaths[i] = path;
} else {
path.reset();
}
}

Path path = objectPaths[0];
path.moveTo(radius / 2, -radius / 2);
path.lineTo(0, -radius);
path.lineTo(radius / 2, -radius);
path.close();

path = objectPaths[1];
path.moveTo(-radius / 2, radius / 2);
path.lineTo(-radius / 2 - 100, radius / 2);
path.arcTo(-radius / 2 - 100, radius / 2, -radius / 2, radius / 2 + 100, 0, 180, false);
path.lineTo(-radius / 2, radius / 2);
path.close();

path = objectPaths[2];
path.addCircle(-radius + 200f, -radius + 100f, 50f, Path.Direction.CCW);

path = objectPaths[3];
path.addRoundRect(-radius / 2, -radius / 2, -radius / 2 + 20, 0, 10, 10, Path.Direction.CCW);

path = objectPaths[4];
path.addRect(120, 120, 200, 200, Path.Direction.CCW);

区域检测


检测是否发生了碰撞,准确度不高,但还能凑合


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,10, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);

mCommonPaint.setColor(Color.CYAN);
for (int i = 0; i < objectPaths.length; i++) {
objectRegion.setPath(objectPaths[i],mainRegion);
if(!objectRegion.quickReject(circleRegion)){
Log.d("RegionView"," 可能发生了碰撞");
mCommonPaint.setColor(Color.YELLOW);
}else{
mCommonPaint.setColor(Color.CYAN);
}
canvas.drawPath(objectPaths[i], mCommonPaint);
}

到这里我们完成了简单的检测,但其实它的精确度很差,这个效果显然不是我们想要的,尤其没有实际接触的情况就染色了。这样会产生很多争议,比如游戏中刘备不可能超出攻击范围去打你一样。


fire_81.gif


精准区域检测优化


在我们做推箱子游戏和珠珠碰撞的时候,我们都是用圆心之间的距离去检测,显然这里是不行的,不光障碍物本身有形状且不规则,而且中心区域正中可能是空白区域,显然圆心之间的距离是不合适的。我们之前学过PathMeasure很多用法《心跳效果》,其中之一是使用粒子描线,下图是我们的效果,在这篇中我们利用PathMeasure对获取路径坐标,并对线周围布置粒子。


fire_49.gif


那么,使用PathMeasure方式获取线条边缘的点不就更准确了么 ?好的,我们开干。


优化逻辑



  • 获取障碍物和圆的Bounds,计算面积,这样把检测物体和被检测物体中最小的设置给PathMeasure

  • 利用PathMeasure的getPosTan获取点

  • 使用Region的contain进行判断点是不是在区域内


下面是优化逻辑


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,20, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);


mCommonPaint.setColor(Color.CYAN);
for (int i = 0; i < objectPaths.length; i++) {
objectRegion.setPath(objectPaths[i],mainRegion);
if(!objectRegion.quickReject(circleRegion)){
mCommonPaint.setColor(Color.YELLOW);
if (circleRegion.getBounds(circleRect)
&& objectRegion.getBounds(objectRect)) {

Region regionChecker = null;
if (circleRect.width() * circleRect.height() > objectRect.width() * objectRect.height()) {
pathMeasure.setPath(objectPaths[i], false);
regionChecker = circleRegion;
} else {
pathMeasure.setPath(circlePath, false);
regionChecker = objectRegion;
}

for (int len = 0; len < pathMeasure.getLength(); len++) {
pathMeasure.getPosTan(len, pos, tan);
if(regionChecker.contains((int) pos[0], (int) pos[1])){
Log.d("RegionView"," 可能发生了碰撞");
mCommonPaint.setColor(Color.YELLOW);
}
}

}

}else{
mCommonPaint.setColor(Color.CYAN);
}
canvas.drawPath(objectPaths[i], mCommonPaint);
}

我们再来看效果,就是文章开头的效果


fire_76.gif


总结


到这里结束了,对于Region类,对点的检测是非常精准的,但是在数学中,所有图形都是点构成线、线构成面,我们本篇利用PathMeasure和Region配合实现了精准检测逻辑,扫平了2D游戏开发过程中的一道门槛。希望看过本篇之后,你能成为游戏大师。


全部代码


有个小插曲,演示精确度低的时候导致代码被还原了,所以重新画了一些东西。


public class RegionView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;

public RegionView(Context context) {
this(context, null);
}

public RegionView(Context context, AttributeSet attrs) {
super(context, attrs);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}

private float x;
private float y;

//所以形状
Path[] objectPaths = new Path[7];
//形状区域检测
Region objectRegion = new Region();

//小圆球区域
Region circleRegion = new Region();
//小圆
Path circlePath = new Path();
//绘制区域
Region mainRegion = new Region();

Rect circleRect = new Rect();
Rect objectRect = new Rect();

float[] pos = new float[2];
float[] tan = new float[2];

PathMeasure pathMeasure = new PathMeasure();

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}

int save = canvas.save();
canvas.translate(width / 2f, height / 2f);
float radius = Math.min(width / 2f, height / 2f);

mainRegion.set((int) -radius, (int) -radius, (int) radius, (int) radius);

for (int i = 0; i < objectPaths.length; i++) {
Path path = objectPaths[i];
if (path == null) {
path = new Path();
objectPaths[i] = path;
} else {
path.reset();
}
}

Path path = objectPaths[0];
path.moveTo(radius / 2, -radius / 2);
path.lineTo(0, -radius);
path.lineTo(radius / 2, -radius);
path.close();

path = objectPaths[1];
path.moveTo(-radius / 2, radius / 2);
path.lineTo(-radius / 2 - 100, radius / 2);
path.arcTo(-radius / 2 - 100, radius / 2, -radius / 2, radius / 2 + 100, 0, 180, false);
path.lineTo(-radius / 2, radius / 2);
path.close();

path = objectPaths[2];
path.addCircle(-radius + 200f, -radius + 200f, 50f, Path.Direction.CCW);

path = objectPaths[3];
path.addRoundRect(-radius + 50, -radius / 2, -radius + 90, 0, 10, 10, Path.Direction.CCW);

path = objectPaths[4];
path.addRect(120, 120, 200, 200, Path.Direction.CCW);

path = objectPaths[5];
path.addCircle(250, 0, 100, Path.Direction.CCW);

Path tmp = new Path();
tmp.addCircle(250,-80,80,Path.Direction.CCW);
path.op(tmp, Path.Op.DIFFERENCE);

tmp.reset();
path = objectPaths[6];
path.addCircle(0, 0, 100, Path.Direction.CCW);
tmp.addCircle(0, 0, 80, Path.Direction.CCW);
path.op(tmp, Path.Op.DIFFERENCE);


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,20, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);


mCommonPaint.setColor(Color.CYAN);
for (int i = 0; i < objectPaths.length; i++) {
objectRegion.setPath(objectPaths[i],mainRegion);
if(!objectRegion.quickReject(circleRegion)){
if (circleRegion.getBounds(circleRect)
&& objectRegion.getBounds(objectRect)) {

Region regionChecker = null;
if (circleRect.width() * circleRect.height() > objectRect.width() * objectRect.height()) {
pathMeasure.setPath(objectPaths[i], false);
regionChecker = circleRegion;
} else {
pathMeasure.setPath(circlePath, false);
regionChecker = objectRegion;
}

for (int len = 0; len < pathMeasure.getLength(); len++) {
pathMeasure.getPosTan(len, pos, tan);
if(regionChecker.contains((int) pos[0], (int) pos[1])){
Log.d("RegionView"," 可能发生了碰撞");
mCommonPaint.setColor(Color.YELLOW);
}
}

}

}else{
mCommonPaint.setColor(Color.CYAN);
}
canvas.drawPath(objectPaths[i], mCommonPaint);
}

mCommonPaint.setColor(Color.WHITE);
canvas.drawPath(circlePath,mCommonPaint);
canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

public static int argb(float red, float green, float blue) {
return ((int) (1 * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}


}

作者:时光少年
来源:juejin.cn/post/7310412252552085513
收起阅读 »

[自定义View]一个简单的渐变色ProgressBar

Android原生ProgressBar 原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。 style: style效果@android:style/Widget.ProgressBar.Horizontal水平进...
继续阅读 »

Android原生ProgressBar



原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。



style:


style效果
@android:style/Widget.ProgressBar.Horizontal水平进度条
@android:style/Widget.ProgressBar.Small小型圆形进度条
@android:style/Widget.ProgressBar.Large大型圆形进度条
@android:style/Widget.ProgressBar.Inverse反色进度条
@android:style/Widget.ProgressBar.Small.Inverse反色小型圆形进度条
@android:style/Widget.ProgressBar.Large.Inverse反色大型圆形进度条
@android:style/Widget.Material**MD风格

原生的特点就是单调,实现基本的功能,使用简单样式不复杂;要满足我们期望的效果就只能自定义View了。


自定义ProgressBar



自定义View的实现方式有很多种,继承已有的View,如ImageView,ProgressBar等等;也可以直接继承自View,在onDraw中绘制需要的效果。
要实现的效果是一个横向圆角矩形进度条,内容为渐变色。
所以在设计时要考虑到可以定义的属性:渐变色、进度等。



<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="progress">
<attr name="progress" format="float" />
<attr name="startColor" format="color" />
<attr name="endColor" format="color" />
</declare-styleable>
</resources>

View实现



这里直接继承子View,读取属性,在onDraw中绘制进度条。实现思路是通过定义Path来绘制裁切范围,确定绘制内容;再实现线性渐变LinearGradient来填充进度条。然后监听手势动作onTouchEvent,动态绘制长度。


同时开放公共方法,可以动态设置进度颜色,监听进度回调,根据需求实现即可。



package com.cs.app.view

/**
*
*/

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.cs.app.R

class CustomProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val progressPaint = Paint()
private val backgroundPaint = Paint()
private var progress = 50f
private var startColor = Color.parseColor("#4C87B7")
private var endColor = Color.parseColor("#A3D5FE")
private var x = 0f
private var progressCallback: ProgressChange? = null

init {
// 初始化进度条画笔
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.FILL

// 初始化背景画笔
backgroundPaint.isAntiAlias = true
backgroundPaint.style = Paint.Style.FILL
backgroundPaint.color = Color.GRAY

if (attrs != null) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.progress)
startColor = typedArray.getColor(R.styleable.progress_startColor, startColor)
endColor = typedArray.getColor(R.styleable.progress_endColor, endColor)
progress = typedArray.getFloat(R.styleable.progress_progress, progress)
typedArray.recycle()
}
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val width = width.toFloat()
val height = height.toFloat()

//绘制Path,限定Canvas边框
val path = Path()
path.addRoundRect(0f, 0f, width, height, height / 2, height / 2, Path.Direction.CW)
canvas.clipPath(path)

//绘制进度条
val progressRect = RectF(0f, 0f, width * progress / 100f, height)
val colors = intArrayOf(startColor, endColor)
val shader = LinearGradient(0f, 0f, width * progress / 100f, height, colors, null, Shader.TileMode.CLAMP)
progressPaint.shader = shader
canvas.drawRect(progressRect, progressPaint)
}

override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
x = event.rawX

//实现点击调整进度
progress = (event.rawX - left) / width * 100
progressCallback?.onProgressChange(progress)
invalidate()
}

android.view.MotionEvent.ACTION_MOVE -> {
//实现滑动调整进度
progress = (event.rawX - left) / width * 100
progress = if (progress < 0) 0f else if (progress > 100) 100f else progress
progressCallback?.onProgressChange(progress)
invalidate()
}

else -> {}
}
return true
}

fun setProgress(progress: Float) {
this.progress = progress
invalidate()
}

fun setOnProgressChangeListener(callback: ProgressChange) {
progressCallback = callback
}

interface ProgressChange {
fun onProgressChange(progress: Float)
}
}

示例


class CustomViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_view)
val progressTv: TextView = findViewById(R.id.progress_textview)
val view: CustomProgressView = findViewById(R.id.progress)
view.setProgress(50f)

view.setOnProgressChangeListener(object : CustomProgressView.ProgressChange {
override fun onProgressChange(progress: Float) {
progressTv.text = "${progress.toInt()}%"
}
})
}
}

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#0E1D3C"
android:layout_height="match_parent">


<com.cs.app.view.CustomProgressView
android:id="@+id/progress"
android:layout_width="200dp"
android:layout_height="45dp"
app:endColor="#A3D5FE"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.274"
app:progress="60"
app:startColor="#4C87B7" />


<TextView
android:id="@+id/progress_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:textColor="#ffffff"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果如下:


HnVideoEditor_2023_11_23_144034156.gif

作者:LANFLADIMIR
来源:juejin.cn/post/7304531564342837287
收起阅读 »

揭秘JavaScript数据世界:一文通晓基本类型和引用类型的精髓!

在编程的世界里,数据是构建一切的基础。就像建筑师需要了解不同材料的强度和特性一样,程序员也必须熟悉各种数据类型。今天,我们就来深入探讨JavaScript中的数据类型,看看它们如何塑造我们的代码世界。一、JavaScript数据类型简介数据类型是计算机语言的基...
继续阅读 »

在编程的世界里,数据是构建一切的基础。就像建筑师需要了解不同材料的强度和特性一样,程序员也必须熟悉各种数据类型。

今天,我们就来深入探讨JavaScript中的数据类型,看看它们如何塑造我们的代码世界。

一、JavaScript数据类型简介

数据类型是计算机语言的基础知识,数据类型广泛用于变量、函数参数、表达式、函数返回值等场合。JavaScript语言的每一个值,都属于某一种数据类型。

Description

JavaScript的数据类型主要分为两大类:基本数据类型引用数据类型。下面就来详细介绍这两类数据类型中都包含哪些及如何使用它们。

二、基本(值类型)数据类型

首先,让我们从最基本的数据类型开始。JavaScript的基本数据类型包括:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、符号(Symbol)。

1、字符串(String)

tring类型用于表示由零或多个16位的Unicode字符组成的字符序列,即字符串。至于用单引号,还是双引号,在js中还是没有差别的。记得成对出现。

let name1 = '张三'
let name2 = "李四"
let name3 = `王五`

1.转换为字符串有2个方法:toString()、String()
let n = 100
n.toString() // '100' 数值类型转换为字符串类型
String(200) // '200' 数值类型转换为字符串类型

2.模板字符串相当于加强版的字符串,可以定义多行字符串。还可以利用${}在字符串中插入变量和表达式
let name = '张三丰'
let age = 180
`我叫${name},今年${age}岁啦!` // 我叫张三丰,今年180岁啦!

2、数字(Number)

该类型的表示方法有两种形式,第一种是整数,第二种为浮点数。整数:可以通过十进制,八进制,十六进制的字面值来表示。

浮点数:就是该数值中必须包含一个小数点,且小数点后必须有一位数字。

let num = 100  // 整数
let floatNum = 3.14 // 浮点数
// toFixed() 方法可以对计算结果进行四舍五入
let pi = Math.PI // 3.141592653589793
pi.toFixed(2) // 3.14 保留2位小数

// 八进制的值第一位必须是零0,后面每一位数的范围在0~7。如果某一位数超出范围,首位的0会被忽略,后面的数值会按照十进制来解析
let octalNum1 = 076 // 八进制的 63
let octalNum2 = 083 // 八进制 83
let octalNum3 = 06 // 八进制 6

// 十六进制的值前两位必须是0x,后面每一位十六进制数字的范围在0~9及A~F,字母A~F可以大写也可以小写。
let hexNum1 = 0xA // 十六进制 10
let hexNum2 = 0x3f // 十六进制 63

// 数值转换的三个方法 Number()、parseInt()、parseFloat()

1.Number() // 可以将字符串、布尔值、null、undefined 等转换为对应的数值,如果无法转换返回NaN
Number("123") // 输出123
Number("hello") // 输出NaN


2.parseInt() // 可以将字符串转换为整数,如果无法转换返回NaN
parseInt("123") // 输出123
parseInt("123.45") // 输出:123
parseInt("hello") // 输出NaN


3.parseFloat() // 可以将字符串转换为浮点数,如果无法转换返回NaN
parseFloat("123.45") // 输出123.45
parseFloat("hello") // 输出NaN

3、布尔(Boolean)

Boolean 数据类型只有两个值:true 和 false,分别代表真和假。很多时候我们需要将各种表达式和变量转换成 Boolean 数据类型来当作判断条件。


1.数值运算判断

1 + 2 === 3 // true
1 + 1 > 3 // false


2.数值类型转换
let bool1 = Boolean(0); // 数值转换为布尔值
let bool2 = Boolean(""); // 字符串转换为布尔值
let bool3 = Boolean(null); // null 转换为布尔值
let bool4 = Boolean(undefined); // undefined 转换为布尔值
let bool5 = Boolean(NaN); // NaN 转换为布尔值
let bool6 = Boolean([]); // 空数组转换为布尔值
let bool7 = Boolean({}); // 空对象转换为布尔值

ECMAScript 类型的值都有与布尔值等价的形式。可以调用 Boolean() 函数来将其他类型转换为布尔值。不同类型转换为布尔值的规则如下表

Description

4、未定义(Undefined)

在 JavaScript 中,undefined 是一个特殊的值和数据类型。当一个变量声明但未赋值时,该变量的值就是 undefined。它表示一个未定义或未初始化的值。

1.声明但未赋值的变量

// 当使用 var、let 或 const 声明一个变量但未对其赋值时,该变量的初始值为 undefined。
let n;
console.log(n) // 输出 undefined


2.未定义的属性

// 当访问一个不存在的属性时,该属性的值为undefined
let obj = { name: '张三丰' }
console.log(obj.age) // 输出 undefined


3.函数没有返回值

// 如果函数没有明确返回值或者使用 return 语句返回一个未定义的值,函数的返回值将是 undefined
function getName() {
// 没有返回值
}
console.log(foo()) // 输出 undefined


4.函数参数未传递

// 如果函数定义了参数但未传递相应的值,那么该参数的值将是 undefined
function getName(name) {
console.log("Hello, " + name)
}
getName() // 输出:Hello, undefined

5、空(Null)

在 JavaScript 中,null 是一个特殊的值和数据类型。它表示一个空值或者不存在的对象。

与undefined不同,null是JavaScript 保留关键字,而 undefined 只是一个常量。也就是说可以声明名称为 undefined 的变量,但将 null 作为变量使用时则会报错。

1.空值

// null 表示一个空值,用于表示变量的值为空
let name = null
console.log(name) // 输出 null


2.不存在的对象

// 当使用 typeof 运算符检测一个值为 null 的对象时,会返回 "object"
let obj = null
console.log(typeof obj) // 输出:object

null 与 undefined 区别

  • undefined 是表示一个未定义或未初始化的值,常用于声明但未赋值的变量,或者访问不存在的属性。

  • null 是一个被赋予的值,用于表示变量被故意赋值为空。

  • 在判断变量是否为空时,使用严格相等运算符(===),因为 undefined 和 null 在非严格相等运算符(==)下会相等。

let x;
let y = null;
console.log(x === undefined) // 输出:true
console.log(x === null) // 输出:false
console.log(y === null) // 输出:true
console.log(y === undefined) // 输出:false

6、符号(Symbol)

符号 (Symbols) 是 ECMAScript 第 6 版新定义的。符号类型是唯一的并且是不可修改的。

1.创建Symbol

// 使用全局函数 Symbol() 可以创建一个唯一的 Symbol 值
let s = Symbol()
console.log(typeof s) // 输出 symbol


2.唯一性

// 每个通过 Symbol() 创建的 Symbol 值都是唯一的,不会与其他 Symbol 值相等,即使它们的描述相同
let s1 = Symbol()
let s2 = Symbol()
console.log(s1 == s2) // 输出 false
let s3 = Symbol('hello')
let s4 = Symbol('hello')
console.log(s3 == s4) // 输出 false


3.Symbol 常量

// 通过 Symbol.for() 方法可以创建全局共享的 Symbol 值,称为 Symbol 常量
let s5 = Symbol.for('key')
let s6 = Symbol.for('key')
console.log(s5 === s6) // 输出 true

Symbol 的主要作用是创建独一无二的标识符,用于定义对象的属性名或者作为一些特殊的标记。它在一些特定的应用场景中非常有用,如在迭代器和生成器中使用 Symbol.iterator 标识可迭代对象。

三、引用数据类型

除了基本数据类型,JavaScript还有引用数据类型:对象(Object)、数组(Array)和函数(Function)

1、对象(Object)

Object 是一个内置的基本数据类型和构造函数。是一组由键、值组成的无序集合,定义对象类型需要使用花括号{ },它是 JavaScript 中最基本的对象类型,也是其他对象类型的基础。

1.创建对象

// Object 类型可以用于创建新的对象。可以使用对象字面量 {} 或者通过调用 Object() 构造函数来创建对象
let obj1 = {} // 使用对象字面量创建空对象
let obj2 = new Object() // 使用 Object() 构造函数创建空对象


2.添加、修改、删除属性

let obj = {}
obj.name = '张三丰' // 添加属性
obj.age = 30 // 添加属性
obj.name = '张无忌' // 修改属性
delete obj.age // 删除属性

2、数组(Array)

JavaScript 中,数组(Array)是一组按顺序排列的数据的集合,数组中的每个值都称为元素,而且数组中可以包含任意类型的数据。

在 JavaScript 中定义数组需要使用方括号[ ],数组中的每个元素使用逗号进行分隔。

数组的特点有哪些?

  • 有序集合: 数组是一种有序的数据集合,每个元素在数组中都有一个对应的索引,通过索引可以访问和操作数组中的元素。

  • 可变长度: 数组的长度是可变的,可以根据需要动态添加或删除元素,或者修改数组的长度。可以使用 push()、pop()、shift()、unshift() 等方法来添加或删除元素,也可以直接修改数组的 length 属性来改变数组的长度。

  • 存储不同类型的值: 数组可以存储任意类型的值,包括基本类型和对象类型。同一个数组中可以混合存储不同类型的值。

  • 索引访问: 通过索引来访问数组中的元素,索引从 0 开始。可以使用方括号语法 [] 或者点号语法 . 来访问数组的元素。

  • 内置方法: 数组提供了许多内置的方法,用于对数组进行常见的操作和处理,如添加、删除、查找、排序、遍历等。常用的数组方法包括 push()、pop()、shift()、unshift()、concat()、slice()、splice()、indexOf()、forEach()、map()、filter()、reduce() 等。

  • 可迭代性: 数组是可迭代的,可以使用 for…of 循环或者 forEach() 方法遍历数组中的元素。

1.创建数组

// 可以使用数组字面量 [] 或者通过调用 Array() 构造函数来创建数组。
let arr1 = [] // 使用数组字面量创建空数组
let arr2 = new Array() // 使用 Array() 构造函数创建空数组
let arr3 = [1, 2, 3] // 使用数组字面量创建包含初始值的数组


2.访问和修改数组元素

// 数组的元素通过索引访问,索引从 0 开始。可以使用索引来读取或修改数组的元素。
let arr = [1, 2, 3]
console.log(arr[0]) // 访问数组的第一个元素,输出:1
arr[1] = 5 // 修改数组的第二个元素
arr.length // 获取数组长度,输出:3

3、函数(Function)

ECMAScript中的函数是对象,与其他引用类型一样具有属性和方法。因此,函数名实际是一个指向函数对象的指针。

1.创建函数

// 可以使用函数声明或函数表达式来创建函数。函数声明使用 function 关键字,后面跟着函数名称和函数体,而函数表达式将函数赋值给一个变量。
// 函数声明
function add(a, b) {
return a + b
}

// 函数表达式
let multiply = function(a, b) {
return a * b
}


2.函数调用

// 函数可以通过函数名后面加括号 () 进行调用。调用函数时,可以传递参数给函数,函数可以接收参数并进行相应的处理。
let result = add(3, 5) // 调用 add 函数并传递参数
console.log(result) // 输出:8


3.函数返回值

// 函数可以使用 return 语句返回一个值,也可以不返回任何值。当函数执行到 return 语句时,会立即停止执行,并将返回值传递给函数调用者。
function calculateSum(a, b) {
return a + b
}
let result = calculateSum(2, 3)
console.log(result) // 输出:5


4.函数作用域

// 函数作用域是指函数内部声明的变量在函数内部有效,外部无法访问。函数内部定义的变量只能在函数内部被访问和使用,在函数外部是不可见的。

function myFunction() {
var x = 10 // 局部变量
console.log(x) // 在函数内部可见
}
myFunction() // 输出:10
console.log(x) // 报错:x is not defined

此外,JavaScript还有一些特殊的数据类型,如Date(表示日期和时间)、RegExp(表示正则表达式),以及ES6新增的Map、Set、WeakMap和WeakSet,用于存储特定类型的数据。


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!


四、数据类型检测

检测数据类型可以使用typeof操作符,它可以检测基本数据类型和function,但无法区分不同的引用数据类型。

var arr = [
null, // object
undefined, // undefined
true, // boolean
12, // number
'haha', // string
Symbol(), // symbol
20n, // bigint
function(){}, // function
{}, // object
[], // object
]
for (let i = 0; i < arr.length; i++) {
console.log(typeof arr[i])
}

掌握JavaScript数据类型是成为一名高效开发者的关键。它们是构建程序的砖石,理解它们的用法和限制将使你能够构建更稳健、更可维护的代码。

现在,你已经了解了JavaScript的数据类型,是时候在你的代码中运用这些知识了。记住,实践是学习的最佳方式,所以动手尝试吧!

如果觉得本文对你有所帮助,别忘了点赞和分享哦!

收起阅读 »

商品 sku 在库存影响下的选中与禁用

web
分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题; 需求分析 需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。 以下讲解将按照我的 ...
继续阅读 »

分享一下,最近使用 React 封装的一个 Skus 组件,主要用于处理商品的sku在受到库存的影响下,sku项的选中和禁用问题;


需求分析


需要展示商品各规格下的sku信息,以及根据该sku的库存是否为空,判断是否禁用该sku的选择。


sku-2.gif

以下讲解将按照我的 Skus组件 来,我这里放上我组件库中的线上 demo 和码上掘金的一个 demo 供大家体验;由于码上掘金导入不了组件库,我就上传了一份开发组件前的一份类似的代码,功能和代码思路是差不多的,大家也可以自己尝试写一下,可能你的思路会更优;


线上 Demo 地址


码上掘金



传入的sku数据结构


需要传入的商品的sku数据类型大致如下:


type SkusProps = { 
/** 传入的skus数据列表 */
data: SkusItem[]
// ... 其他的props
}

type SkusItem = {
/** 库存 */
stock?: number;
/** 该sku下的所有参数 */
params: SkusItemParam[];
};

type SkusItemParam = {
name: string;
value: string;
}

转化成需要的数据类型:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

生成数据


定义 sku 分类


首先假装请求接口,造一些假数据出来,我这里自定义了最多 6^6 = 46656 种 sku。


sku-66.gif

下面的是自定义的一些数据:


const skuData: Record<string, string[]> = {
'颜色': ['红','绿','蓝','黑','白','黄'],
'大小': ['S','M','L','XL','XXL','MAX'],
'款式': ['圆领','V领','条纹','渐变','轻薄','休闲'],
'面料': ['纯棉','涤纶','丝绸','蚕丝','麻','鹅绒'],
'群体': ['男','女','中性','童装','老年','青少年'],
'价位': ['<30','<50','<100','<300','<800','<1500'],
}
const skuNames = Object.keys(skuData)

页面初始化



  • checkValArr: 需要展示的sku分类是哪些;

  • skusList: 接口获取的skus数据;

  • noStockSkus: 库存为零对应的skus(方便查看)。


export default () => {
// 这个是选中项对应的sku类型分别是哪几个。
const [checkValArr, setCheckValArr] = useState<number[]>([4, 5, 2, 3, 0, 0]);
// 接口请求到的skus数据
const [skusList, setSkusList] = useState<SkusItem[]>([]);
// 库存为零对应的sku数组
const [noStockSkus, setNoStockSkus] = useState<string[][]>([])

useEffect(() => {
const checkValTrueArr = checkValArr.filter(Boolean)
const _noStockSkus: string[][] = [[]]
const list = getSkusData(checkValTrueArr, _noStockSkus)
setSkusList(list)
setNoStockSkus([..._noStockSkus])
}, [checkValArr])

// ....

return <>...</>
}

根据上方的初始化sku数据,生成一一对应的sku,并随机生成对应sku的库存。


getSkusData 函数讲解


先看总数(total)为当前需要的各sku分类的乘积;比如这里就是上面传入的 checkValArr 数组 [4,5,2,3]120种sku选择。对应的就是 skuData 中的 [颜色前四项,大小前五项,款式前两项,面料前三项] 即下图的展示。


image.png

遍历 120 次,每次生成一个sku,并随机生成库存数量,40%的概率库存为0;然后遍历 skuNames 然后找到当前对应的sku分类即 [颜色,大小,款式,面料] 4项;


接下来就是较为关键的如何根据 sku的分类顺序 生成对应的 120个相应的sku。


请看下面代码中注释为 LHH-1 的地方,该 value 的获取是通过 indexArr 数组取出来的。可以看到上面 indexArr 数组的初始值为 [0,0,0,0] 4个零的索引,分别对应 4 个sku的分类;



  • 第一次遍历:


indexArr: [0,0,0,0] -> skuName.forEach -> 红,S,圆领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,1];



  • 第二次遍历:


indexArr: [0,0,0,1] -> skuName.forEach -> 红,S,圆领,涤纶


看LHH-2标记处: 索引+1 -> indexArr: [0,0,0,2];



  • 第三次遍历:


indexArr: [0,0,0,2] -> skuName.forEach -> 红,S,圆领,丝绸


看LHH-2标记处: 由于已经到达该分类下的最后一个,所以前一个索引加一,后一个重新置为0 -> indexArr: [0,0,1,0];



  • 第四次遍历:


indexArr: [0,0,1,0] -> skuName.forEach -> 红,S,V领,纯棉


看LHH-2标记处: 索引+1 -> indexArr: [0,0,1,1];



  • 接下来的一百多次遍历跟上面的遍历同理


image.png
function getSkusData(skuCategorys: number[], noStockSkus?: string[][]) {
// 最终生成的skus数据;
const skusList: SkusItem[] = []
// 对应 skuState 中各 sku ,主要用于下面遍历时,对 product 中 skus 的索引操作
const indexArr = Array.from({length: skuCategorys.length}, () => 0);
// 需要遍历的总次数
const total = skuCategorys.reduce((pre, cur) => pre * (cur || 1), 1)
for(let i = 1; i <= total; i++) {
const sku: SkusItem = {
// 库存:60%的几率为0-50,40%几率为0
stock: Math.floor(Math.random() * 10) >= 4 ? Math.floor(Math.random() * 50) : 0,
params: [],
}
// 生成每个 sku 对应的 params
let skuI = 0;
skuNames.forEach((name, j) => {
if(skuCategorys[j]) {
// 注意:LHH-1
const value = skuData[name][indexArr[skuI]]
sku.params.push({
name,
value,
})
skuI++;
}
})
skusList.push(sku)

// 注意: LHH-2
indexArr[indexArr.length - 1]++;
for(let j = indexArr.length - 1; j >= 0; j--) {
if(indexArr[j] >= skuCategorys[j] && j !== 0) {
indexArr[j - 1]++
indexArr[j] = 0
}
}

if(noStockSkus) {
if(!sku.stock) {
noStockSkus.at(-1)?.push(sku.params.map(p => p.value).join(' / '))
}
if(indexArr[0] === noStockSkus.length && noStockSkus.length < skuCategorys[0]) {
noStockSkus.push([])
}
}
}
return skusList
}

Skus 组件的核心部分的实现


初始化数据


需要将上面生成的数据转化为以下结构:


type SkuStateItem = {
value: string;
/** 与该sku搭配时,该禁用的sku组合 */
disabledSkus: string[][];
}[];

export default function Skus() {
// 转化成遍历判断用的数据类型
const [skuState, setSkuState] = useState<Record<string, SkuStateItem>>({});
// 当前选中的sku值
const [checkSkus, setCheckSkus] = useState<Record<string, string>>({});

// ...
}

将初始sku数据生成目标结构


根据 data (即上面的假数据)生成该数据结构。


第一次遍历是对skus第一项进行的,会生成如下结构:


const _skuState = {
'颜色': [{value: '红', disabledSkus: []}],
'大小': [{value: 'S', disabledSkus: []}],
'款式': [{value: '圆领', disabledSkus: []}],
'面料': [{value: '纯棉', disabledSkus: []}],
}

第二次遍历则会完整遍历剩下的skus数据,并往该对象中填充完整。


export default function Skus() {
// ...
useEffect(() => {
if(!data?.length) return
// 第一次对skus第一项的遍历
const _checkSkus: Record<string, string> = {}
const _skuState = data[0].params.reduce((pre, cur) => {
pre[cur.name] = [{value: cur.value, disabledSkus: []}]
_checkSkus[cur.name] = ''
return pre
}, {} as Record<string, SkuStateItem>)
setCheckSkus(_checkSkus)

// 第二次遍历
data.slice(1).forEach(item => {
const skuParams = item.params
skuParams.forEach((p, i) => {
// 当前 params 不在 _skuState 中
if(!_skuState[p.name]?.find(params => params.value === p.value)) {
_skuState[p.name].push({value: p.value, disabledSkus: []})
}
})
})

// ...接下面
}, [data])
}

第三次遍历主要用于为每个 sku的可点击项 生成一个对应的禁用sku数组 disabledSkus ,只要当前选择的sku项,满足该数组中的任一项,该sku选项就会被禁用。之所以保存这样的一个二维数组,是为了方便后面点击时的条件判断(有点空间换时间的概念)。


遍历 data 当库存小于等于0时,将当前的sku的所有参数传入 disabledSkus 中。


例:第一项 sku(红,S,圆领,纯棉)库存假设为0,则该选项会被添加到 disabledSkus 数组中,那么该sku选择时,勾选前三个后,第四个 纯棉 的勾选会被禁用。


image.png
export default function Skus() {
// ...
useEffect(() => {
// ... 接上面
// 第三次遍历
data.forEach(sku => {
// 遍历获取库存需要禁用的sku
const stock = sku.stock!
// stockLimitValue 是一个传参 代表库存的限制值,默认为0
// isStockGreaterThan 是一个传参,用来判断限制值是大于还是小于,默认为false
if(
typeof stock === 'number' &&
isStockGreaterThan ? stock >= stockLimitValue : stock <= stockLimitValue
) {
const curSkuArr = sku.params.map(p => p.value)
for(const name in _skuState) {
const curSkuItem = _skuState[name].find(v => curSkuArr.includes(v.value))
curSkuItem?.disabledSkus?.push(
sku.params.reduce((pre, p) => {
if(p.name !== name) {
pre.push(p.value)
}
return pre
}, [] as string[])
)
}
}
})

setSkuState(_skuState)
}, [data])
}

遍历渲染 skus 列表


根据上面的 skuState,生成用于渲染的列表,渲染列表的类型如下:


type RenderSkuItem = {
name: string;
values: RenderSkuItemValue[];
}
type RenderSkuItemValue = {
/** sku的值 */
value: string;
/** 选中状态 */
isChecked: boolean
/** 禁用状态 */
disabled: boolean;
}

export default function Skus() {
// ...
/** 用于渲染的列表 */
const list: RenderSkuItem[] = []
for(const name in skuState) {
list.push({
name,
values: skuState[name].map(sku => {
const isChecked = sku.value === checkSkus[name]
const disabled = isChecked ? false : isSkuDisable(name, sku)
return { value: sku.value, disabled, isChecked }
})
})
}
// ...
}

html css 大家都会,以下就简单展示了。最外层遍历sku的分类,第二次遍历遍历每个sku分类下的名称,第二次遍历的 item(类型为:RenderSkuItemValue),里面会有sku的值,选中状态和禁用状态的属性。


export default function Skus() {
// ...
return list?.map((p) => (
<div key={p.name}>
{/* 例:颜色、大小、款式、面料 */}
<div>{p.name}</div>
<div>
{p.values.map((sku) => (
<div
key={p.name + sku.value}
onClick={() =>
selectSkus(p.name, sku)}
>
{/* classBem 是用来判断当前状态,增加类名的一个方法而已 */}
<span className={classBem(`sku`, {active: sku.isChecked, disabled: sku.disabled})}>
{/* 例:红、绿、蓝、黑 */}
{sku.value}
</span>
</div>
))}
</div>
</div>

))
}

selectSkus 点击选择 sku


通过 checkSkus 设置 sku 对应分类下的 sku 选中项,同时触发 onChange 给父组件传递一些信息出去。


const selectSkus = (skuName: string, {value, disabled, isChecked}: RenderSkuItemValue) => {
const _checkSkus = {...checkSkus}
_checkSkus[skuName] = isChecked ? '' : value;
const curSkuItem = getCurSkuItem(_checkSkus)
// 该方法主要是 sku 组件点击后触发的回调,用于给父组件获取到一些信息。
onChange?.(_checkSkus, {
skuName,
value,
disabled,
isChecked: disabled ? false : !isChecked,
dataItem: curSkuItem,
stock: curSkuItem?.stock
})
if(!disabled) {
setCheckSkus(_checkSkus)
}
}

getCurSkuItem 获取当前选中的是哪个sku



  • isInOrder.current 是用来判断当前的 skus 数据是否是整齐排列的,这里当成 true 就好,判断该值的过程就不放到本文了,感兴趣可以看 源码


由于sku是按顺序排列的,所以只需按顺序遍历上面生成的 skuState,找出当前sku选中项对应的索引位置,然后通过 就可以直接得出对应的索引位置。这样的好处是能减少很多次遍历。


如果直接遍历原来那份填充所有 sku 的 data 数据,则需要很多次的遍历,当sku是 6^6 时, 则每次变换选中的sku时最多需要 46656 * 6 (data总长度 * 里面 sku 的 params) 次。


const getCurSkuItem = (_checkSkus: Record<string, string>) => {
const length = Object.keys(skuState).length
if(!length || Object.values(_checkSkus).filter(Boolean).length < length) return void 0
if(isInOrder.current) {
let skuI = 0;
// 由于sku是按顺序排列的,所以索引可以通过计算得出
Object.keys(_checkSkus).forEach((name, i) => {
const index = skuState[name].findIndex(v => v.value === _checkSkus[name])
const othTotal = Object.values(skuState).slice(i + 1).reduce((pre, cur) => (pre *= cur.length), 1)
skuI += index * othTotal;
})
return data?.[skuI]
}
// 这样需要遍历太多次
return data.find(s => (
s.params.every(p => _checkSkus[p.name] === getSkuParamValue(p))
))
}

isSkuDisable 判断该 sku 是否是禁用的


该方法是在上面 遍历渲染 skus 列表 时使用的。



  1. 开始还未有选中值时,需要校验 disabledSkus 的数组长度,是否等于该sku参数可以组合的sku总数,如果相等则表示禁用。

  2. 判断当前选中的 sku 还能组成多少种组合。例:当前选中 红,S ,而 isSkuDisable 方法当前判断的 sku 为 款式 中的 圆领,则还有三种组合 红\S\圆领\纯棉红\S\圆领\涤纶红\S\圆领\丝绸

  3. 如果当前判断的 sku 的 disabledSkus 数组中存在这三项,则表示该 sku 选项会被禁用,无法点击。


const isCheckValue = !!Object.keys(checkSkus).length

const isSkuDisable = (skuName: string, sku: SkuStateItem[number]) => {
if(!sku.disabledSkus.length) return false
// 1.当一开始没有选中值时,判断某个sku是否为禁用
if(!isCheckValue) {
let checkTotal = 1;
for(const name in skuState) {
if(name !== skuName) {
checkTotal *= skuState[name].length
}
}
return sku.disabledSkus.length === checkTotal
}

// 排除当前的传入的 sku 那一行
const newCheckSkus: Record<string, string> = {...checkSkus}
delete newCheckSkus[skuName]

// 2.当前选中的 sku 一共能有多少种组合
let total = 1;
for(const name in newCheckSkus) {
if(!newCheckSkus[name]) {
total *= skuState[name].length
}
}

// 3.选中的 sku 在禁用数组中有多少组
let num = 0;
for(const strArr of sku.disabledSkus) {
if(Object.values(newCheckSkus).every(str => !str ? true : strArr.includes(str))) {
num++;
}
}

return num === total
}

至此整个商品sku从生成假数据到sku的选中和禁用的处理的核心代码就完毕了。还有更多的细节问题可以直接查看 源码 会更清晰。


作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7313979106890842139
收起阅读 »

Android — 实现扫码登录功能

现在大部分网站都有扫码登录功能,搭配相应的App就能免去输入账号密码实现快速登录。本文简单介绍如何实现扫码登录功能。 实现扫码登录 之前参与过一个电视App的开发,采用扫码登录,需要使用配套的App扫码登录后才能进入到主页。那么扫码登录该怎么实现呢?大致流程如...
继续阅读 »

现在大部分网站都有扫码登录功能,搭配相应的App就能免去输入账号密码实现快速登录。本文简单介绍如何实现扫码登录功能。


实现扫码登录


之前参与过一个电视App的开发,采用扫码登录,需要使用配套的App扫码登录后才能进入到主页。那么扫码登录该怎么实现呢?大致流程如下:



  1. 被扫端展示一个二维码,二维码包含被扫端的唯一标识(如设备id),并与服务端保持通讯(轮询、长连接、推送)。

  2. 扫码端扫描二维码之后,使用获取到的被扫端的唯一标识(如设备id)调用服务端扫码登录接口。

  3. 服务端接收扫码端发起的扫码登录请求,处理(如验证用户信息)后将登录信息发送到被扫端。


PS: 此为大致流程,具体使用需要根据实际需求进行调整。


接下来简单演示一下此流程。


添加依赖库


添加需要的SDK依赖库,在项目app module的build.gradle中的dependencies中添加依赖:


dependencies { 
// 实现服务端(http、socket)
implementation("org.nanohttpd:nanohttpd:2.3.1")
implementation("org.nanohttpd:nanohttpd-websocket:2.3.1")

// 与服务端通信
implementation("com.squareup.okhttp3:okhttp:4.12.0")

// 扫描解析、生成二维码
implementation("com.github.jenly1314:zxing-lite:3.1.0")
}

服务端


使用NanoHttpD实现Socket服务端(与被扫端通信)和Http服务端(与扫码端通信),示例代码如下:


Socket服务


与被扫端保持通讯,在Http服务接收并处理完扫码登录请求后,将获取到的用户id发送给被扫端。


class ServerSocketClient : NanoWSD(9090) {

private var serverWebSocket: ServerWebSocket? = null

override fun openWebSocket(handshake: IHTTPSession?): WebSocket {
return ServerWebSocket(handshake).also { serverWebSocket = it }
}

private class ServerWebSocket(handshake: IHTTPSession?) : WebSocket(handshake) {
override fun onOpen() {}

override fun onClose(code: WebSocketFrame.CloseCode?, reason: String?, initiatedByRemote: Boolean) {}

override fun onMessage(message: WebSocketFrame?) {}

override fun onPong(pong: WebSocketFrame?) {}

override fun onException(exception: IOException?) {}
}

override fun stop() {
super.stop()
serverWebSocket = null
}

fun sendMessage(message: String) {
serverWebSocket?.send(message)
}
}

Http服务


接收并处理来自扫码端的扫码登录请求,通过设备id和用户id判断被扫端是否可以登录。


const val APP_SCAN_INTERFACE = "loginViaScan"

const val USER_ID = "userId"
const val EXAMPLE_USER_ID = "123456789"

const val DEVICE_ID = "deviceId"
const val EXAMPLE_DEVICE_ID = "example_device_id0001"

class ServerHttpClient(private var scanLoginSucceedListener: ((userId: String) -> Unit)? = null) : NanoHTTPD(8080) {

override fun serve(session: IHTTPSession?): Response {
val uri = session?.uri
return if (uri == "/$APP_SCAN_INTERFACE" &&
session.parameters[USER_ID]?.first() == EXAMPLE_USER_ID &&
session.parameters[DEVICE_ID]?.first() == EXAMPLE_DEVICE_ID
) {
scanLoginSucceedListener?.invoke(session.parameters[USER_ID]?.first() ?: "")
newFixedLengthResponse("Login Succeed")
} else {
super.serve(session)
}
}
}

服务控制类


启动或停止Socket服务和Http服务。


object ServerController {

private var serverSocketClient: ServerSocketClient? = null
private var serverHttpClient: ServerHttpClient? = null

fun startServer() {
(serverSocketClient ?: ServerSocketClient().also {
serverSocketClient = it
}).run {
if (!isAlive) {
start(0)
}
}

(serverHttpClient ?: ServerHttpClient {
serverSocketClient?.sendMessage("Login Succeed, user id is $it")
}.also {
serverHttpClient = it
}).run {
if (!isAlive) {
start(NanoHTTPD.SOCKET_READ_TIMEOUT, true)
}
}
}

fun stopServer() {
serverSocketClient?.stop()
serverSocketClient = null

serverHttpClient?.stop()
serverHttpClient = null
}
}

被扫端


Socket辅助类


使用OkHttp与服务端进行Socket通信。


class DevicesSocketHelper(private val messageListener: ((message: String) -> Unit)? = null) {

private var webSocket: WebSocket? = null

private val webSocketListener = object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
super.onMessage(webSocket, bytes)
messageListener?.invoke(bytes.utf8())
}

override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
messageListener?.invoke(text)
}
}

fun openSocketConnection(serverPath: String) {
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build()
val request = Request.Builder().url(serverPath).build()
webSocket = okHttpClient.newWebSocket(request, webSocketListener)
}

fun release() {
webSocket?.close(1000, "")
webSocket = null
}
}

被扫端示例页面


先展示二维码,接收到服务端的消息后,显示用户id。


class DeviceExampleActivity : AppCompatActivity() {

private lateinit var binding: LayoutDeviceExampleActivityBinding

private var socketHelper: DevicesSocketHelper? = DevicesSocketHelper() { message ->
// 接收到服务端发来的消息,改变显示内容
runOnUiThread {
binding.tvUserInfo.text = message
binding.ivQrCode.visibility = View.GONE
binding.tvUserInfo.visibility = View.VISIBLE
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutDeviceExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
it.includeTitle.tvTitle.text = "Device Example"
}

lifecycleScope.launch(Dispatchers.IO) {
// 使用设备id生成二维码
CodeUtils.createQRCode(EXAMPLE_DEVICE_ID, DensityUtil.dp2Px(200)).let { qrCode ->
withContext(Dispatchers.Main) {
binding.ivQrCode.setImageBitmap(qrCode)
}
}
}

socketHelper?.openSocketConnection("ws://localhost:9090/")
}

override fun onDestroy() {
super.onDestroy()
socketHelper?.release()
socketHelper = null
}
}

扫描端


扫码页


继承zxing-lite库的BarcodeCameraScanActivity类,简单实现扫描与解析二维码。


class ScanQRCodeActivity : BarcodeCameraScanActivity() {

override fun initCameraScan(cameraScan: CameraScan<Result>) {
super.initCameraScan(cameraScan)
// 播放扫码音效
cameraScan.setPlayBeep(true)
}

override fun createAnalyzer(): Analyzer<Result> {
return QRCodeAnalyzer(DecodeConfig().apply {
// 设置仅识别二维码
setHints(DecodeFormatManager.QR_CODE_HINTS)
})
}

override fun onScanResultCallback(result: AnalyzeResult<Result>) {
// 已获取结果,停止识别二维码
cameraScan.setAnalyzeImage(false)
// 返回扫码结果
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(CameraScan.SCAN_RESULT, result.result.text)
})
finish()
}
}

扫描端示例页面


提供扫码入口,提供输入框用于输入服务端IP,获取到扫码结果后发送给服务端。


class AppScanExampleActivity : AppCompatActivity() {

private lateinit var binding: LayoutAppScanExampleActivityBinding

private var serverIp: String = ""

private val scanQRCodeLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.getStringExtra(CameraScan.SCAN_RESULT)?.let { deviceId ->
sendRequestToServer(deviceId)
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutAppScanExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}

OkHttpHelper.init()

binding.btnScan.setOnClickListener {
// 获取输入的服务端ip(两台设备在同一WIFI下,直接通过IP访问服务端)
serverIp = binding.etInputIp.text.toString()
if (serverIp.isEmpty()) {
showSnakeBar("Server ip can not be empty")
return@setOnClickListener
}
hideKeyboard(binding.etInputIp)
scanQRCodeLauncher.launch(Intent(this, ScanQRCodeActivity::class.java))
}
}

private fun sendRequestToServer(deviceId: String) {
OkHttpHelper.sendGetRequest("http://${serverIp}:8080/${APP_SCAN_INTERFACE}", mapOf(Pair(USER_ID, EXAMPLE_USER_ID), Pair(DEVICE_ID, deviceId)), object : RequestCallback {
override fun onResponse(success: Boolean, responseBody: ResponseBody?) {
showSnakeBar("Scan login ${if (success) "succeed" else "failure"}")
}

override fun onFailure(errorMessage: String?) {
showSnakeBar("Scan login failure")
}
})
}

private fun hideKeyboard(view: View) {
view.clearFocus()
WindowInsetsControllerCompat(window, view).hide(WindowInsetsCompat.Type.ime())
}

private fun showSnakeBar(message: String) {
runOnUiThread {
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
}
}
}

示例入口页


提供被扫端和扫码端入口,打开被扫端时同时启动服务端。


class ScanLoginExampleActivity : AppCompatActivity() {

private lateinit var binding: LayoutScanLoginExampleActivityBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutScanLoginExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
it.includeTitle.tvTitle.text = "Scan Login Example"
it.btnOpenDeviceExample.setOnClickListener {
// 打开被扫端同时启动服务
ServerController.startServer()
startActivity(Intent(this, DeviceExampleActivity::class.java))
}
it.btnOpenAppExample.setOnClickListener { startActivity(Intent(this, AppScanExampleActivity::class.java)) }
}
}

override fun onDestroy() {
super.onDestroy()
ServerController.stopServer()
}
}

效果演示与示例代码


最终效果如下图:


被扫端扫码端
device.gifapp.gif

演示代码已在示例Demo中添加。


ExampleDemo github


ExampleDemo gitee


作者:ChenYhong
来源:juejin.cn/post/7349545661111336997
收起阅读 »

Android TextView的颜色和字体自适应

前言 最近比较忙,没有时间去梳理一些硬核的东西,今天就来分享一些简单有意思的技巧。拿TextView来说,平时我们都会在特定的场景去设置它的字体颜色和字体大小。那么有没有一种办法能够一劳永逸不用每次都去设置,能让TextView自己去根据自身的控件属性去自适应...
继续阅读 »

前言


最近比较忙,没有时间去梳理一些硬核的东西,今天就来分享一些简单有意思的技巧。拿TextView来说,平时我们都会在特定的场景去设置它的字体颜色和字体大小。那么有没有一种办法能够一劳永逸不用每次都去设置,能让TextView自己去根据自身的控件属性去自适应颜色和大小呢?当然是有的,这里可以简单的分享一些思路。


1. 字体大小自适应


TextView可以根据让字体的大小随着宽高进行自适应。


设置大小自适应的方式很简单,只需要添加这3行代码即可


android:autoSizeMaxTextSize="22dp"  
android:autoSizeMinTextSize="8dp"
android:autoSizeTextType="uniform"

我们可以来看看效果,我给宽高都设置不同的值,能看到字体大小变化的效果


android:layout_width="50dp"  
android:layout_height="20dp"

image.png


android:layout_width="50dp"  
android:layout_height="30dp"

image.png


android:layout_width="50dp"  
android:layout_height="50dp"

image.png


android:layout_width="80dp"  
android:layout_height="80dp"

image.png


最后这里可以看到autoSizeMaxTextSize的效果


这里可以多提一句,一般这种字体随宽高自适应的场景在正常开发中比较少见。如果你的项目合理的话,一般字体的大小都是固定那几套,所以把字体大小定义到资源文件中,甚至通过style的方式去设置,才是最节省时间的方式。


2. 字体颜色自适应


关于字体的颜色自适应,如果你真想把这套东西搞起来,你就需要对“颜色”这个概念有一定的深层次的了解。我这里就只简单做一些效果来举例。


我这里演示Textview根据背景颜色来自动设置字体颜色是白色还是黑色,当背景颜色是暗色时(比如黑色),字体颜色变成白色,当背景颜色是亮色时(比如白色),字体颜色变成黑色。


那么首先需要有个概念:我怎么判断背景是亮色还是暗色?


这就需要对颜色有一定的理解。要判断一个颜色是暗色还是亮色,可以通过计算颜色的亮度来实现。一种常见的方法是将RGB颜色值转换为灰度值,然后根据灰度值来判断颜色的深浅程度。

灰度值的计算公式 灰度值 = 0.2126 * R + 0.7152 * G + 0.0722 * B


根据这个公式,我们能封装一个判断颜色是否是亮色的方法


private fun isLightColor(color: Int): Boolean {  
val r = color shr 16 and 0xFF
val g = color shr 8 and 0xFF
val b = color and 0xFF
val luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
return luminance > 0.5
}

如果觉得这个判断不太符合你心里的预期,可以修改最后一行的luminance > 0.5值


下一步,我们需要获取控件的背景,然后从背景中获取颜色值。


获取背景直接调用


val d = textView?.background

根据Drawable去获取颜色


private fun getColorByDrawable(d : Drawable) : Int{  
val bitmap = Bitmap.createBitmap(
textView?.width ?: 0,
textView?.height ?: 0,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas)
return bitmap.getPixel(0, 0)
}

注意,我这里不考虑渐变色的情况,只是考虑单色的情况,所以x和y是传0,一般对于复杂的渐变色也不好做适配,但是对于background分边框和填充两种颜色的情况,一般文字都是显示在填充区域,这时候的x和y可以去根据边框宽度去加个偏移量(总之可以灵活应变)


还有一种场景,对于TextView没背景颜色,是它的父布局有背景颜色的情况,可以循环去调用父布局的view.background判断是否为空,为空就循环一次,不为空直接获取颜色。我这里就不演示代码了。


这里先把全部代码贴出来(都是用了最简单的方式)


override fun onCreate(savedInstanceState: Bundle?) {  
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo_text)

textView = findViewById(R.id.tv)
val d = textView?.background
textView?.post {
if (d != null){
if (isLightColor(getColorByDrawable(d))){
textView?.setTextColor(resources.getColor(R.color.black))
}else{
textView?.setTextColor(resources.getColor(R.color.white))
}
}
}
}

private fun getColorByDrawable(d : Drawable) : Int{
val bitmap = Bitmap.createBitmap(
textView?.width ?: 0,
textView?.height ?: 0,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas)
return bitmap.getPixel(0, 0)
}

private fun isLightColor(color: Int): Boolean {
val r = color shr 16 and 0xFF
val g = color shr 8 and 0xFF
val b = color and 0xFF
val luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
return luminance > 0.5
}

然后改几个背景色来看看效果


android:background="#000000"

image.png


android:background="#ffffff"

image.png


android:background="#3377ff"

image.png


android:background="#ee7700"

image.png


作者:流浪汉kylin
来源:juejin.cn/post/7361998447908208651
收起阅读 »

实现抖音 “视频无限滑动“效果

web
前言 在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅 不禁感叹道 "垃圾抖音,费我时间,毁我青春😅" 这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满。 如果您对滑动原理不太熟...
继续阅读 »

前言


在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅

不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"


这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满。


如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件


最终效果


在线预览:zyronon.gitee.io/douyin/


Github地址:github.com/zyronon/dou…


源码:SlideVerticalInfinite.vue


实现原理


无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList 里面永远只有 NSlideItem,就要在滑动时不断的删除和增加 SlideItem

滑动时调整 SlideList 的偏移量 translateY 的值,以及列表里那几个 SlideItemtop 值,就可以了


为什么要调整 SlideList 的偏移量 translateY 的值同时还要调整 SlideItemtop 值呢?

因为 translateY 只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY 值就可以了,上滑了几页就减几页的高度,下滑同理


但是如果整个列表向前移动了一页,同时前面的 SlideItem 也少了一个,,那么最终效果就是移动了两页...因为 塌陷 了一页

这显然不是我们想要的,所以我们还需要同时调整 SlideItemtop 值,加上前面少的 SlideItem 的高度,这样才能显示出正常的内容


步骤


定义




virtualTotal:页面中同时存在多少个 SlideItem,默认为 5


//页面中同时存在多少个SlideItem
virtualTotal: {
type: Number,
default: () => 5
},

设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10 条,有的要求同时存在 5 条即可。

不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。

如果只同时存在 5 条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3 条,刚开始除外),我们可能来不及添加新的视频到最后




render:渲染函数,SlideItem内显示什么由render返回值决定


render: {
type: Function,
default: () => {
return null
}
},

之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。

最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList




list:数据列表,外部传入


list: {
type: Array,
default: () => {
return []
}
},

我们从 list 中取出数据,然后调用并传给 render 函数,将其返回值插入到 SlideList中


初始化



watch(
() => props.list,
(newVal, oldVal) => {
//新数据长度比老数据长度小,说明是刷新
if (newVal.length < oldVal.length) {
//从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中
insertContent()
} else {
//没数据就直接插入
if (oldVal.length === 0) {
insertContent()
} else {
// 走到这里,说明是通过接口加载了下一页的数据,
// 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
// 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom
// 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑
}
}
}
)

watch 监听 list 是因为它一开始不一定有值,通过接口请求之后才有值

同时当我们下滑 加载更多 时,也会触发接口请求新的数据,用 watch 可以在有新数据时,多添加几条到 SlideList 的最后面,这样用户快速滑动也不怕了


如何滑动


这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件


滑动结束


判断滑动的方向


当我们向上滑动时,需要删除最前面的 dom ,然后在最后面添加一个 dom

下滑时反之


slideTouchEnd(e, state, canNext, (isNext) => {
if (props.list.length > props.virtualTotal) {
//手指往上滑(即列表展示下一条视频)
if (isNext) {
//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
} else {
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
}
}
})

手指往上滑(即列表展示下一条视频)



  • 首先判断是否要加载更多,快到列表末尾时就要加载更多数据了

  • 再判断是否符合 腾挪 的条件,即当前位置要大于 half,且小于列表长度减 half

  • 在最后面添加一个 dom

  • 删除最前面的 dom

  • 将所有 dom 设置为最新的 top 值(原因前面有讲,因为删除了最前面的 dom,导致塌陷一页,所以要加上删除 dom 的高度)


let half = (props.virtualTotal - 1) / 2

//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
emit('loadMore')
}

//是否符合 `腾挪` 的条件
if (state.localIndex > half && state.localIndex < props.list.length - half) {
//在最后面添加一个 `dom`
let addItemIndex = state.localIndex + half
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
if (!res) {
slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
}

//删除最前面的 `dom`
let index = slideListEl.value
.querySelector(`.${itemClassName}:first-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}

手指往下滑(即列表展示上一条视频)


逻辑和上滑都差不多,不过是反着来而已



  • 再判断是否符合 腾挪 的条件,和上面反着

  • 在最前面添加一个 dom

  • 删除最后面的 dom

  • 将所有 dom 设置为最新的 top


//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {
let addIndex = state.localIndex - half
if (addIndex >= 0) {
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)
if (!res) {
slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))
}
}
let index = slideListEl.value
.querySelector(`.${itemClassName}:last-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}

其他问题


为什么不直接用 v-for直接生成 SlideItem 呢?


如果内容不是视频就可以。要删除或者新增时,直接操作 list 数据源,这样省事多了


如果内容是视频,修改 list 时,Vue 会快速的替换 dom,正在播放的视频,突然一下从头开始播放了😅😅😅


如何获取 Vue 组件的最终 dom


有两种方式,各有利弊



  • Vuerender 方法

    • 优点:只是渲染一个 VNode 而已,理论上讲内存消耗更少。

    • 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅



  • VuecreateApp 方法再创建一个 Vue 的实例

    • 和上面相反😅




import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'

/**
* 获取Vue组件渲染之后的dom元素
* @param item
* @param index
* @param play
*/

function getInsEl(item, index, play = false) {
// console.log('index', cloneDeep(item), index, play)
let slideVNode = props.render(item, index, play, props.uniqueId)
const parent = document.createElement('div')
//TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
if (import.meta.env.PROD) {
parent.classList.add('slide-item')
parent.setAttribute('data-index', index)
//将Vue组件渲染到一个div上
vueRender(slideVNode, parent)
appInsMap.set(index, {
unmount: () => {
vueRender(null, parent)
parent.remove()
}
})
return parent
} else {
//创建一个新的Vue实例,并挂载到一个div上
const app = createApp({
render() {
return <SlideItem data-index={index}>{slideVNode}</SlideItem>
}
})
const ins = app.mount(parent)
appInsMap.set(index, app)
return ins.$el
}
}

总结


原理其实并不难。主要是一开始可能会用 v-for 去弄,折腾半天发现不行。v-for 不行,就只能想想怎么把 Vue 组件搞到 html 里面去,又去研究如何获取 Vue 组件的最终 dom,又查了半天资料,Vue 官方文档也不写,还得去翻 api ,麻了


结束



以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号 前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~



作者:前端张余让
来源:juejin.cn/post/7361614921519054883
收起阅读 »

Android实战 -> 使用Interceptor+Lock实现无缝刷新Token

前言 哈喽各位我又来了,相信大家在做APP的时候肯定会遇到用户Token即将过期或者已经过期的情况,那么这个时候后端都返回相应的Code提示我们要求刷新Token处理。 那么今天这篇文章就给大家提供一个思路。 开工 技术点 Interceptor ->...
继续阅读 »

前言


哈喽各位我又来了,相信大家在做APP的时候肯定会遇到用户Token即将过期或者已经过期的情况,那么这个时候后端都返回相应的Code提示我们要求刷新Token处理。


那么今天这篇文章就给大家提供一个思路。


开工


技术点


  • Interceptor -> 拦截器

  • ReentrantLock -> 重入锁


实现思路


  • 通过TokenInterceptor获取Response解析请求结果验证是否Token过期

  • 监控到Token已过期后阻塞当前线程,调用刷新Token接口并使用Lock锁

  • 并发的请求也监控到了Token过期后,先校验Lock是否已锁,已锁等待,未锁步骤2

  • Token刷新成功后各线程携带新的Token创建Request重新请求


总结:4个并发线程接口,谁抢到了Lock锁谁去刷新Token,其他三个线程阻塞等待


实现代码

private fun handle(name: String) {
Log.d(TAG, "handle 【Start】 called with: name = $name")
try {
if (!mLock.isLocked) {
this.mLock.lock() // 加锁
Log.d(TAG, "handle 【Start Refresh】 called with: name = $name")
Thread.sleep(5000) // 此处应为刷新Token请求
Log.d(TAG, "handle 【End Refresh】 called with: name = $name")
this.mLock.unlock() // 释放锁
} else {
Log.d(TAG, "handle 【Wait Refresh】 called with: name = $name")
while (true) { // 阻塞等待
if (!mLock.isLocked) { // 查询锁状态
Log.d(TAG, "handle 【OK Refresh】 called with: name = $name")
break
}
}
}
} finally {
if (mLock.isLocked) {
this.mLock.unlock()
}
}
Log.d(TAG, "handle 【End】 called with: name = $name")
}

如上述代码,抢到Lock锁的线程去刷新Token,其余线程等待结果。


模拟测试

// 此处模拟并发请求
this.findViewById<View>(R.id.btnGo).setOnClickListener {
thread {
handle("线程1")
}
thread {
handle("线程2")
}
thread {
handle("线程3")
}
}

输出日志

image.png


如图,线程2抢到了Lock锁,线程1、3则进入了等待状态


image.png


如图,线程2刷新Token成功后释放了锁,线程1、3监听到了锁被释放则进入重新请求逻辑


实践代码

class TokenInterceptor : Interceptor {

@Volatile
private var mRefreshInvalidTime = 0L

@Volatile
private var isRefreshToken = false

private val mRefreshTokenLock by lazy { ReentrantLock() }

private val mAccountRep by lazy { .... }

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// 接口过滤Token校验
val ignoreToken = request.headers[HeaderConstant.IGNORE_TOKEN_NAME]
if (ignoreToken == HeaderConstant.IGNORE_TOKEN_VALUE) {
return chain.proceed(request)
}
val response = chain.proceed(request)
if (HttpFactory.bodyEncoded(response.headers)) {
return response
}
// 解析反参Json
val result = HttpFactory.bodyToString(response) ?: ""
if (!TextUtils.isEmpty(result)) {
val resp = result.convert<BaseResp<Any>>()
// 校验Token是否过期
if (ResponseConstant.isTokenExpire(resp.code)) {
return onTokenRefresh(chain, response) ?: kotlin.run { response }
}
// 校验Token是否失效
if (ResponseConstant.isTokenInvalid(resp.code)) {
this.onTokenInvalid(response)
}
}
return response
}

/**
* Token 刷新
*/

private fun onTokenRefresh(chain: Interceptor.Chain, response: Response): Response? {
var newResponse: Response? = response
try {
if (!mRefreshTokenLock.isLocked) {
this.mRefreshTokenLock.lock()
this.isRefreshToken = true
runBlocking {
launch(Dispatchers.Default) {
newResponse = requestAuthToken(chain, response)
}
}

this.mRefreshTokenLock.unlock()
this.isRefreshToken = false
} else {
while (true){
if (!isRefreshToken){
newResponse = doRequest(chain)
break
}
}
}
} catch (e: Exception) {
// do something
} finally {
if (mRefreshTokenLock.isLocked) {
this.mRefreshTokenLock.unlock()
this.isRefreshToken = false
}
}
return newResponse
}


/**
* Token 失效
*/

private fun onTokenInvalid(response: Response) {
response.close()
// 防抖
val currentTime = System.currentTimeMillis()
if ((currentTime - mRefreshInvalidTime) > KET_TOKEN_INVALID_ANTI_SHAKE) {
this.mRefreshInvalidTime = currentTime
// 跳转登录页 or 自行逻辑
...
}
}

/**
* 请求 刷新Token
*/

private suspend fun requestAuthToken(chain: Interceptor.Chain, response: Response): Response? {
var newResponse: Response? = response
val resp = .... // 请求代码
if (resp.isSuccess()) {
response.close()
resp.data?.let { data -> .... //更新本地Token }
newResponse = doRequest(chain)
}
return newResponse
}

private fun doRequest(chain: Interceptor.Chain): Response? {
var response: Response? = null
try {
val newRequest = HttpFactory.newRequest(chain.request()).build()
response = chain.proceed(newRequest)
} catch (e: Exception) {
// do something
}
return response
}

companion object {
const val KET_TOKEN_INVALID_ANTI_SHAKE = 2000
}
}

End

到这里就结束了,简单吧,希望可以帮到在座的小伙伴们。当然如果有更好的实现方式或方案也希望各位在评论区留言讨论,我秒回复哦~ Bye



作者:新啊新之助
来源:juejin.cn/post/7306018966920970274
收起阅读 »

Android适配:判断机型和系统

在Android开发中,我们总是会碰到各种各样的适配问题。如果要解决适配问题,我们必须就要解决,出现问题的是什么机型?出现问题的是什么系统?怎么判断当前机型是不是出问题的机型?这几个问题。这篇文章,就将介绍如何判断机型和系统,介绍目前应该如何解决这些问题。 判...
继续阅读 »

在Android开发中,我们总是会碰到各种各样的适配问题。如果要解决适配问题,我们必须就要解决,出现问题的是什么机型?出现问题的是什么系统?怎么判断当前机型是不是出问题的机型?这几个问题。这篇文章,就将介绍如何判断机型和系统,介绍目前应该如何解决这些问题。


判断指定的机型


在Android里面可以通过 android.os.Build这个类获取相关的机型信息,它的参数如下:(这里以一加的手机为例)


Build.BOARD = lahaina
Build.BOOTLOADER = unknown
Build.BRAND = OnePlus //品牌名
Build.CPU_ABI = arm64-v8a
Build.CPU_ABI2 =
Build.DEVICE = OP5154L1
Build.DISPLAY = MT2110_13.1.0.100(CN01) //设备版本号
Build.FINGERPRINT = OnePlus/MT2110_CH/OP5154L1:13/TP1A.220905.001/R.1038728_2_1:user/release-keys
Build.HARDWARE = qcom
Build.HOST = dg02-pool03-kvm97
Build.ID = TP1A.220905.001
Build.IS_DEBUGGABLE = false
Build.IS_EMULATOR = false
Build.MANUFACTURER = OnePlus //手机制造商
Build.MODEL = MT2110 //手机型号
Build.ODM_SKU = unknown
Build.PERMISSIONS_REVIEW_REQUIRED = true
Build.PRODUCT = MT2110_CH //产品名称
Build.RADIO = unknown
Build.SERIAL = unknown
Build.SKU = unknown
Build.SOC_MANUFACTURER = Qualcomm
Build.SOC_MODEL = SM8350
Build.SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@fea6460
Build.SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@3a22d19
Build.SUPPORTED_ABIS = [Ljava.lang.String;@2101de
Build.TAGS = release-keys
Build.TIME = 1683196675000
Build.TYPE = user
Build.UNKNOWN = unknown
                                                                            Build.USER = root

其中重要的属性已经设置了注释,所有的属性可以看官方文档。在这些属性中,我们一般使用 Build.MANUFACTURER 来判断手机厂商,使用 Build.MODEL 来判断手机的型号。



tips: 如果你是使用kotlin开发,可以使用 android.os.Build::class.java.fields.map { "Build.${it.name} = ${it.get(it.name)}"}.joinToString("\n") 方便的获取所有的属性



上面的获取机型的代码在鸿蒙系统(HarmonyOS)上也同样适用,下面是在华为P50 Pro的机型上测试打印的日志信息:



Build.BOARD = JAD
Build.BOOTLOADER = unknown
Build.BRAND = HUAWEI
Build.CPU_ABI = arm64-v8a
Build.CPU_ABI2 = 
Build.DEVICE = HWJAD
Build.DISPLAY = JAD-AL50 2.0.0.225(C00E220R3P4)
Build.FINGERPRINT = HUAWEI/JAD-AL50/HWJAD:10/HUAWEIJAD-AL50/102.0.0.225C00:user/release-keys
Build.FINGERPRINTEX = HUAWEI/JAD-AL50/HWJAD:10/HUAWEIJAD-AL50/102.0.0.225C00:user/release-keys
Build.HARDWARE = kirin9000
Build.HIDE_PRODUCT_INFO = false
Build.HOST = cn-east-hcd-4a-d3a4cb6341634865598924-6cc66dddcd-dcg9d
Build.HWFINGERPRINT = ///JAD-LGRP5-CHN 2.0.0.225/JAD-AL50-CUST 2.0.0.220(C00)/JAD-AL50-PRELOAD 2.0.0.4(C00R3)//
Build.ID = HUAWEIJAD-AL50
Build.IS_CONTAINER = false
Build.IS_DEBUGGABLE = false
Build.IS_EMULATOR = false
Build.IS_ENG = false
Build.IS_TREBLE_ENABLED = true
Build.IS_USER = true
Build.IS_USERDEBUG = false
Build.MANUFACTURER = HUAWEI
Build.MODEL = JAD-AL50
Build.NO_HOTA = false
Build.PERMISSIONS_REVIEW_REQUIRED = true
Build.PRODUCT = JAD-AL50
Build.RADIO = unknown
Build.SERIAL = unknown
Build.SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@a90e093
Build.SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@8ce98d0
Build.SUPPORTED_ABIS = [Ljava.lang.String;@366a0c9
Build.TAGS = release-keys
Build.TIME = 1634865882000
Build.TYPE = user
Build.UNKNOWN = unknown

综上,判断手机厂商的代码如下:


//是否是荣耀设备
fun isHonorDevice() = Build.MANUFACTURER.equals("HONOR", ignoreCase = true)
//是否是小米设备
fun isXiaomiDevice() = Build.MANUFACTURER.equals("Xiaomi", ignoreCase = true)
//是否是oppo设备
//realme 是oppo的海外品牌后面脱离了;一加是oppo的独立运营品牌。因此判断
//它们是需要单独判断
fun isOppoDevice() = Build.MANUFACTURER.equals("OPPO", ignoreCase = true)
//是否是一加手机
fun isOnePlusDevice() = Build.MANUFACTURER.equals("OnePlus", ignoreCase = true)
//是否是realme手机
fun isRealmeDevice() = Build.MANUFACTURER.equals("realme", ignoreCase = true)
//是否是vivo设备
fun isVivoDevice() = Build.MANUFACTURER.equals("vivo", ignoreCase = true)
//是否是华为设备
fun isHuaweiDevice() = Build.MANUFACTURER.equals("HUAWEI", ignoreCase = true)

需要判断指定的型号的代码则为:


//判断是否是小米12s的机型
fun isXiaomi12S() = isXiaomiDevice() && Build.MODEL.contains("2206123SC"//xiaomi 12s

如果你不知道对应机型的型号,可以看基于谷歌维护的表格,支持超过27,000台设备。如下图所示:



判断手机的系统


除了机型外,适配过程中我们还需要考虑手机的系统。但是相比于手机机型,手机的系统的判断就没有统一的方式。下面介绍几个常用的os的判断


● 鸿蒙


private static final String HARMONY_OS = "harmony";
/**
* check the system is harmony os
*
* @return true if it is harmony os
*/
public static boolean isHarmonyOS() {
    try {
        Class clz = Class.forName("com.huawei.system.BuildEx");
        Method method = clz.getMethod("getOsBrand");
        return HARMONY_OS.equals(method.invoke(clz));
    } catch (ClassNotFoundException e) {
        Log.e(TAG, "occured ClassNotFoundException");
    } catch (NoSuchMethodException e) {
        Log.e(TAG, "occured NoSuchMethodException");
    } catch (Exception e) {
        Log.e(TAG, "occur other problem");
    }
    return false;
}

● Miui


fun checkIsMiui() = !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.name"))

private fun getSystemProperty(propName: String): String? {
    val line: String
    var input: BufferedReader? = null
    try {
        val p = Runtime.getRuntime().exec("getprop $propName")
        input = BufferedReader(InputStreamReader(p.inputStream), 1024)
        line = input.readLine()
        input.close()
    } catch (ex: IOException) {
        Log.i(TAG, "Unable to read sysprop $propName", ex)
        return null
    } finally {
        if (input != null) {
            try {
                input.close()
            } catch (e: IOException) {
                Log.i(TAG, "Exception while closing InputStream", e)
            }
        }
    }
    return line
}

● Emui 或者 Magic UI


Emui是2018之前的荣耀机型的os,18年之后是 Magic UI。历史关系如下图所示:



判断的代码如下。需要注意是对于Android 12以下的机型,官方文档并没有给出对于的方案,下面代码的方式是从网上找的,目前测试了4台不同的机型,均可正在判断。


fun checkIsEmuiOrMagicUI()Boolean {
    return if (Build.VERSION.SDK_INT >= 31) {
//官方方案,但是只适用于api31以上(Android 12)
        try {
            val clazz = Class.forName("com.hihonor.android.os.Build")
            Log.d(TAG, "clazz = " + clazz)
            true
        }catch (e: ClassNotFoundException) {
            Log.d(TAG, "no find class")
            e.printStackTrace()
            false
        }
    } else {
//网上方案,测试了 荣耀畅玩8C
// 荣耀20s、荣耀x40 、荣耀v30 pro 四台机型,均可正常判断
        !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui"))
    }
}

● Color Os


下面是网上判断是否是Oppo的ColorOs的代码。经测试,在 OPPO k10 、 oppo findx5 pro、 oneplus 9RT 手机上都是返回 false,只有在 realme Q3 pro 机型上才返回了 true。


//这段代码是错的
fun checkIsColorOs() = !TextUtils.isEmpty(getSystemProperty("ro.build.version.opporom"))

从测试结果可以看出上面这段代码是错的,但是在 ColorOs 的官网上,没有找到如何判断ColorOs的代码。这种情况下,有几种方案:


1.  判断手机制造商,即 Build.MANUFACTURER 如果为 oneplus、oppo、realme就认为它是ColorOs


2.  根据系统应用的包名判断,即判断是否带有 com.coloros.* 的系统应用,如果有,就认为它是ColorOs


这几种方案都有很多问题,暂时没有找到更好的解决方法。


● Origin Os


//网上代码,在 IQOQ Neo5、vivo Y50、 vivo x70三种机型上
//都可以正常判断
fun checkIsOriginOs() = !TextUtils.isEmpty(getSystemProperty("ro.vivo.os.version"))

总结


对于手机厂商和机型,我们可以通过Android原生的 android.os.Build  类来判断。使用 Build.MANUFACTURER 来判断手机厂商,使用 Build.MODEL 来判断手机的型号。如果是不知道的型号,还可以尝试在谷歌维护的手机机型表格中查询。


但是对于厂商的系统,就没有统一的判断方法了。部分厂商有官方提供的判断方式,如Miui、Magic UI;部分厂商暂时没有找到相关的内容。这种情况下,只能通过网上的方式判断,但是部分内容也不靠谱,如判断Oppo的ColorOs。如果你有靠谱的方式,欢迎补充。


参考



作者:小墙程序员
来源:juejin.cn/post/7241056943388983356
收起阅读 »

自己没有价值之前,少去谈人情世故

昨天和几个网友在群里聊天,一个网友说最近公司辞退了一个人,原因就是太菜了,有一个功能是让从数据库随机查一条数据,他硬是把整个数据表的数据都查出来,然后从里面随机选一条数据。 另外的群友说,这人应该在公司的人情世故做得不咋滴,要是和自己组长,领导搞好关系,不至于...
继续阅读 »

昨天和几个网友在群里聊天,一个网友说最近公司辞退了一个人,原因就是太菜了,有一个功能是让从数据库随机查一条数据,他硬是把整个数据表的数据都查出来,然后从里面随机选一条数据。


另外的群友说,这人应该在公司的人情世故做得不咋滴,要是和自己组长,领导搞好关系,不至于被辞退。


发言人说:相反,这人的人情世故做得很到位,和别人相处得也挺好,说话又好听,大家都觉得他很不错!


但是这有用吗?


和自己的组长关系搞好了,难道他就能给你的愚蠢兜底?


这未免太天真,首先组长也是打工的,你以为和他关系好,他就能包庇你,容忍你不断犯错?


没有人会愿意冒着被举报的风险去帮助一个非亲非故的人,因为自己还要生活,老婆孩子还要等着用钱,包庇你,那么担风险的人就是他自己,他为何要这样做?


我们许多人总是觉得人情世故太重要了,甚至觉得比自己的能力重要,这其实是一个侮误区。


有这种想法的大多是刷垃圾短视频刷多了,没经历过社会的毒打,专门去学酒满敬人,茶满欺人。给领导敬酒杯子不能高过对方,最好直接跪下来……


那么人情世故重要吗?


重要,但是得分阶层,你一个打工的,领导连你名字都叫不出来,你见到他打声招呼,他都是用鼻子答应,你觉得你所谓的人情世故有意义吗?


你以为团建的时候跑上去敬酒,杯子直接低到他脚下,他就会看中你,为他挡酒他就觉得你这人可扶?未免电视看得太多。


人情世故有用的前提一定是建立在你有被利用的价值之上,你能漂漂亮亮做完一件事,问题又少,创造的价值又多,那么别人就会觉得你行,就会记住你,重视你,至于敬酒这些,不过是走个过场而已。


所以在自己没有价值之前,别去谈什么人情世故,安安心心提升自己。


前段时间一个大二的小妹妹叫我帮她运行一个项目,她也是为了课程蒙混过关,后面和她聊了几句,她叫我给她一点建议。


我直接给她说,你真正的去写了几行代码?看了几本书?做了多少笔记?你真正的写了代码,看了书,有啥疑问你再找我,而不是从我这里找简便方法,因为我也没有!


她说最烦学习了,完全不想学,自己还是去学人情世故了。


我瞬间破放了,对她说你才20岁不到,专业知识不好好学,就要去学人情世故了?你能用到人情世故吗?


你是怕以后去进厂自己人情世故不到位别人不要你?还是以后去ktv陪酒或者当营销学不会?这么早就做准备了?


她后面反驳我说:你看那些职场里面的女生不也是很懂人情世故吗,你为啥说没用,这些东西迟早都是要学的,我先做准备啊!


我当时就不想和她聊下去了,我知道又是垃圾短视频看多了,所以才会去想这些!以为自己不好好学习,毕业后只要人情世故做到位,就能像那些女职场秘书一样,陪着领导出去谈生意。


想啥呢!


当然,并不存在歧视别人的想法,因为我没有资格,只不过是觉得该学习的时间别去想一些没啥用的事情!


我们所能看到的那些把人情世故运用得炉火纯青,让人感觉很自然的人,别人肯定已经到了一定的段位,这是TA的职业需要。


而大多数人都是在底层干着街边老太太老大爷都能干的活,领导连你名字都叫不出来,可以用空气人来形容,你说人情世故有什么卵用吗?


这不就等于把自己弄得四不像吗?


当你真的有利用价值,能够给别人提供解决方案的时候,再来谈人情世故,那时候你不学,生活都会逼着你去学。


最后说一句,当你有价值的时候,人情世故是你别人学来用在你身上的,不信你回头去看一下自己的身边的人,哪怕是一个小学教师,都有人提着东西来找他办事,但是如果没有任何利用价值,哪怕TA把酒场上面的套路都运用得炉火纯青,也会成为别人的笑柄!


作者:苏格拉的底牌
来源:juejin.cn/post/7352799449456738319
收起阅读 »

三个开发者,支撑一万亿的活跃使用量

对于很多开发者来说,SQLite 一定不陌生。 也知道它很强,但是没想到居然这么强。 SQlite 目前超一万亿(1e121e121e12)的活跃使用量。 它主要用于: 平台包含SQLite移动设备每一台安卓设备,每一台 iPhone 和 iOS 设备计算机每...
继续阅读 »

对于很多开发者来说,SQLite 一定不陌生。


也知道它很强,但是没想到居然这么强。


SQlite 目前超一万亿(1e121e12)的活跃使用量。


它主要用于:


平台包含SQLite
移动设备每一台安卓设备,每一台 iPhone 和 iOS 设备
计算机每一台 Mac,每一台 Windows10 机器
网络浏览器每一款 Firefox、Chrome 和 Safari网络浏览器
通讯应用每一个 Skype 实例
媒体应用每一个 iTunes 实例,每一个 Dropbox 客户端
财务软件每一款 TurboTax 和 QuickBooks
编程语言PHP 和 Python
家庭娱乐大多数电视机和机顶盒
汽车大多数汽车多媒体系统
其他无数百万其他应用程序


👉 表格来源于:http://www.sqlite.org/mostdeploye…



而 SQLite 的全部开发者,也就三个人:




👉 图片来源于:http://www.sqlite.org/crew.html




  • D. Richard Hipp2000 年 5 月 29 日开始 SQLite 项目,并继续担任项目架构师。理查德在北卡罗来纳州夏洛特出生、生活和工作。他拥有佐治亚理工学院(电子工程硕士学位,1984 年)和杜克大学(博士学位,1992 年)学位,并且是咨询公司 Hwaci 的创始人。

  • Dan Kennedy :澳大利亚人,目前居住在东南亚。他拥有昆士兰大学计算机系统工程学位,曾在多个领域工作过,包括工业自动化、计算机图形和嵌入式软件开发。Dan 是主要贡献者自 2002 年起使用 SQLite。

  • Joe Mistachkin(发音为“miss-tash-kin”):软件工程师,也是 Tcl/Tk 的维护者之一。他也是 TclBridge 组件和 Eagle 脚本语言的作者。他自 1994 年以来一直在软件行业工作。


另外一件有趣的事情是,SQLite 不接受任何外来的代码贡献。


也就是说,SQLite 开源,但是并不开放代码贡献。


在 SQLite 的版权声明有提到:




👉 图片来源于:http://www.sqlite.org/copyright.h…



很多时候,都不得不感慨软件的边际成本


一份代码,可以分发给十个人用,也可以给十亿个人使用


三个开发者,就支撑一万亿的活跃使用量。


SQLite 创造的价值,无与伦比,科技改变世界。


REFERENCES



作者:吴楷鹏
来源:juejin.cn/post/7352877037125894180
收起阅读 »

我们开源啦!一键部署免费使用!Kubernetes上直接运行大数据平台!

导语:市场上首个 K8s 上的大数据平台,开源了!智领云自主研发的首个完全基于Kubernetes的容器化大数据平台Kubernetes Data Platform (简称KDP)开源啦!开发者只要准备好命令行工具,一键部署Hadoop,Hive,Spark,...
继续阅读 »

导语:市场上首个 K8s 上的大数据平台,开源了!

智领云自主研发的首个完全基于Kubernetes的容器化大数据平台

Kubernetes Data Platform (简称KDP)

开源啦!

开发者只要准备好命令行工具,一键部署

Hadoop,Hive,Spark,Kafka, Flink, MinIO ...

就可以创建以前要花几十万甚至几百万才可以买到的大数据平台

无需再花大量的时间和经费去做重复的研发

高度集成,单机即可体验大数据平台

在高级安装模式下

用户可在现有的K8s集群上集成运行大数据组件

不用额外单独建设大数据集群

项目地址:

https://github.com/linktimecloud/kubernetes-data-platform

辛辛苦苦研究出来的成果,为什么要开源?

这波格局开大,老板有话说

问题1:我们为什么要开源?

我们的产品一直是基于大数据开源生态体系建设的。之前就一直有开源回馈社区的计划,但是因为之前Kubernetes对于大数据组件的支持还不够成熟,我们也一直在迭代与Kubernetes的适配。现在我们的企业版已经在很多头部客户落地并且在生产环境下高效运行,觉得这个版本已经可以达到大部分生产级项目的需求,集成度以及可用性是能够帮到有类似需求的用户的,希望这次开源能够降低在Kubernetes上集成大数据组件的门槛,让更多Kuberenetes和big data社区的同行们可以使用。

问题2:开源版本的KDP,能干啥?

KDP可以很方便的在Kubenetes上安装和管理常用的大数据组件,Hadoop,Hive,Spark,Kafka, Flink, MinIO 等等,不需要自己一个一个去适配,可以直接开始使用。然后KDP也提供集成的运维管理界面,用户可以从界面管理所有组件的安装配置,运行状况,资源使用情况,修改配置。而且KDP会将一个大数据组件的所有负载(容器,pod)作为一个整体管理,用户不需要在Kubernetes的控制平面上去管理单独的负载。

问题3:最大的亮点是?

只要你已经在使用Kubernetes,那么在现有集群上十几分钟就可以启动一个完整的大数据集群,马上开始使用,极大的降低了大数据平台的使用门槛。因为我们这个流程是高度集成的,整个安装过程在一个单机环境下也都能启动(例如使用单机kind虚拟集群都可以),所以在测试和实验环境下都可以高效使用。当然,启动之后Day 2的很多好处,例如资源的高效利用和集成的运维管理,也是KDP提供的重要功能。

KDP,即在Kubernetes上使用原生的分布式功能搭建及管理大数据平台。

将多套大数据组件集成在Kubernetes之上,同时提供一个整体的管理及运维工具体系,形成一个完全基于Kubernetes的大数据平台。企业级KDP更是支持在同一个Kubernetes集群中同时运行多个大数据平台以及多租户管理的能力,充分发挥Kubernetes云原生体系的优势。

KDP,通过对开源大数据组件的扩展和集成,实现了传统大数据平台到K8s大数据平台的平稳迁移。

作为市场上首个可完全在Kubernetes上部署的容器化云原生大数据平台,智领云自主研发的KDP,深度整合云原生架构优势,将大数据组件、数据应用及资源调度混排,纳入Kubernetes管理体系,从而带你真正玩转云原生!

总体框架

简单来讲,KDP可以允许客户在Kubernetes上运行它所有的大数据组件,并把它们作为一个整体管理起来。

在Kubernetes上运行大数据平台有三个好处:

第一,更高效的大数据组件集成:KDP提供标准化自动化的大数据组件部署和配置,极大地缩短了大数据项目开发和上线时间;

第二,更高效的大数据集群运管:KDP通过大数据组件与K8s的集成,在K8s之上搭建了一个大数据组件管理抽象层,标准化大数据组件生命周期管理,并提供UI界面进一步提升了部署、升级等操作的效率;

第三,更高的集群资源利用率:利用K8s的资源管理和配额机制,与其它系统共享K8s资源池,精细化资源管理,对比传统大数据平台约30%左右的资源利用率,KDP可大幅提升至60%以上。

社区

我们期待您的贡献和建议!最简单的贡献方式是参与Github议题/讨论的讨论。 如果您有任何问题,请与我们联系,我们将确保尽快为您解答。

微信群:添加小助手微信拉您进入交流群

钉钉群:搜索公开群组号 82250000662

贡献

参考开发者指南,了解如何开发及贡献 KDP。

https://linktimecloud.github.io/kubernetes-data-platform/docs/zh/developer-guide/developer-guide.html

收起阅读 »

宫崎骏系列电影:《你想活出怎样的人生》观后感

1 回想宫崎骏上一部上映的电影,已经是2013年,距今已有十一年之久,那时在唐山的网吧里,在微博偶然刷到了《起风了》的预告片,一开始就被主题曲《ひこうき雲》轻快动听的旋律吸引住了,加上当时的说法是,这部《起风了》应该是收官之作了,所以当时情绪很复杂,既有期待又...
继续阅读 »


1


回想宫崎骏上一部上映的电影,已经是2013年,距今已有十一年之久,那时在唐山的网吧里,在微博偶然刷到了《起风了》的预告片,一开始就被主题曲《ひこうき雲》轻快动听的旋律吸引住了,加上当时的说法是,这部《起风了》应该是收官之作了,所以当时情绪很复杂,既有期待又有不舍,不像《你想活出怎样的人生》,只是隐约看到一两则新闻,其他没有过多关注,虽然也有说是告别之作,但是竟没觉得有何特别之处,心态还是很坦然的,不过想到宫崎骏老爷子都八十四高龄了,这次应该真的是最后一部了。


2


你想活出怎样的人生?


通过电影名字,就可以知道这部电影的主旨,剩下的就是我们怎么能在这场奇幻旅途之中,寻找到想要的答案。


3


整部影片的剧情用一句话概括就是,二战之后,一个丧母的少年和一只会说话的苍鹭去寻找继母的奇幻旅程。


我本来想着,要把电影剧情一五一十地表述完整,但是,我感觉这个东西很容易变成流水账,完全是为了充字数,并不会有什么价值可言,是个人只要想写,都可以写出来,所以就此作罢。


我觉得观后感,是很自我的东西,不用非常刻意去挖掘多高深的内容,也不需要每看一部电影,好像带着写观后感的任务一样,那就违背了初衷,我不是为了写观后感才去看电影的,我是喜欢电影,喜欢这部电影才有感而发,是很纯粹的一些感受。


4


在电影院观看时,我一开始并不觉得音乐有多出色,至少相比之前那些作品而言,没有觉得很惊艳。但是现在在敲这些文字时,QQ音乐正好放着配乐《Ask me why》,居然觉得很好听,仿佛有一股暖流,慢慢流淌遍及我的全身,而《青サギ》则是清冽的感觉,像在深山的竹林,偶然听见风铃一样冰凉的声音。


5


宫崎骏的电影,没有绝对的善与恶,不同的立场,不同的选择,大家都只不过是为了自己坚持的正义而战罢了。


我很感恩,在我很小的时候,遇到了《千与千寻》和《哈尔的移动城堡》这两部电影,不管岁月如何变迁,不管社会如何险恶,不管人心如何复杂多变,我始终相信,会有那么一样东西,是如此纯粹,如此干净而美好的。


哪怕做不到具体的某个事物,只是某个瞬间,让我们回归到最初的自己,就好像从未改变过一样。


想到《火影忍者》里大蛇丸,找到君麻吕的时候,说的那一段话,抛开PUA洗脑这些因素不谈,整段话还是很有哲理的,带着对美好事物的向往。


“并没有所谓的活着一定就有意义这种事情,但是活下去,说不定能找到有趣的事情,就像你找到了那朵花一样,就像我找到了你一样。”


我又想起来2015年加入的QQ群,叫作06年神雕侠侣,里面就有位大兄弟说的一段话,我很喜欢,所以截图一直保存至今,我想这就是宫崎骏的电影,之所以如此迷人的原因。


“无论这个人走过怎么样的一条路,经历过什么样的沧桑浮华,对美好事物的爱永远都不会消失。”



6


这部电影中,让我印象比较深刻的是苍鹭,太好玩了,他的造型让我眼前一亮,本体是一个留着大鼻子的老头,披着苍鹭的外形,出场的画面都很滑稽,补鸟喙窟窿那段笑死我了哈哈哈,还有就是戴眼罩的样子,太可爱了。然后苍鹭露出原型振翅滑稽的飞行样子,让我想到了《千与千寻》里的苍蝇带着坊宝宝变的小白鼠起飞的样子。除此之外,从影片中还是能看出,有之前很多作品的影子,比如任意门就是对应的《哈尔的移动城堡》,进入异世界的入口就对应了《千与千寻》等等,想必宫崎骏老爷子也是想在告别之作,缅怀自己已逝去的岁月吧。



7


“我不怕火,能生下你,是我的荣幸。”


当时看到这一幕的时候,想到了丹尼斯·维伦纽瓦的电影《降临》里的剧情,对,就是《沙丘》的导演,女主因为接触到外星人,所以有了预知未来的能力,当她知道和男主在一起,会面临未来女儿死亡的结果时,当她只能眼睁睁看着结果发生时,拥有的同时就正在失去。


“如果你已经能一览自己的人生,从始到终,你会想做改变吗?”


“就算知晓整段旅程,知晓它如何终结,我也选择接受。选择去拥抱其中每个瞬间。”


有异曲同工之处,这一切都源自于爱。我还想到韩寒《乘风波浪》里最后那一声“妈咪”,也是很感人的。



8


要说影片觉得不好之处,应该是结局突然就结束了,感觉太匆忙了些,然后就是火美在看到真人老爸,也就是自己未来老公的时候,稍微多一些表情变化就好了,表现出一丝细腻的联系,电影中整得好像陌生人一样,缺少了一点温情。其他的都还好。


9


最后那段塔主人对真人说的那些话,其实也是本片的高潮之处。


“我穿越时间和空间,旅行了很远,才找到它们,你可以建造自己的塔,创造一个没有邪恶的国度,一个富裕和平而美好的世界。”


“你想回到那个充满了凶杀和盗窃的疯狂世界吗?它很快就会变成一片火海。”


“随你交朋友,回你的世界去,但你必须堆积这些石头。”


“我的塔撑不了多久时间了。”


有人说,真人其实是宫崎骏自己,但我觉得塔主人才是宫崎骏自己,在自己即将告别之际,让屏幕前的你我,勇敢地做出一个选择,你能活出怎样的人生?你敢活出怎样的人生?你想活出怎样的人生?


想到的是东野圭吾的《解忧杂货店》里的结尾,那个一张白纸的回信,十年前就很受感动,所以一直保留这个照片,而以这个作为结尾,同样很适用于《你想活出怎样的人生》这部电影想要传达的美好祝愿。



作者:AR7
来源:juejin.cn/post/7355692365330153512
收起阅读 »

Android后台驻留:保活和回收的机制

简介 众所周知,Android平台的管理机制下,App进入后台后,为了提供持续的及时服务(如推送、音乐),或进行驻留获取收益(跟踪、信息收集、广告)等,会利用一些方法来让自身保持活跃,躲过被Android系统或用户发觉、清理,实现后台驻留。 其中,后台驻留的广...
继续阅读 »

简介


众所周知,Android平台的管理机制下,App进入后台后,为了提供持续的及时服务(如推送、音乐),或进行驻留获取收益(跟踪、信息收集、广告)等,会利用一些方法来让自身保持活跃,躲过被Android系统或用户发觉、清理,实现后台驻留。


其中,后台驻留的广义概念,除了保持在后台运行外,被其他组件拉起也属于驻留(唤醒)。


由于驻留会对系统的性能、响应延迟、续航、发热等带来负面影响,令系统的资源管理效果降低,属于违背用户意愿和知情的恶劣行为,因此将这些App称为顽固(Diehard)应用,其利用的方法称为顽固方法。


除了App利用的一些黑科技(甚至是在违法边缘的擦边手段)以外,Android系统本身自带的机制也可以实现保活和拉起。这些保活、拉起机制,粗略划分为两类:



  1. 保持活跃,在后台运行不被清理、回收

  2. 被其他组件唤醒,包括被其他App唤醒、被系统提供的功能唤醒


本文总结上述这两类会被顽固App利用的机制。


进程和Task管理


首先简单梳理一下Android Framework层基本的进程管理。


Android平台基于Linux,除了基于Linux的“进程”维度来进行管理外,还按照Task的概念来管理应用进程,分别为ProcessRecord和TaskRecord。系统可以按Task也可以按Process来管理进程。


Android提供接口直接杀死Linux进程:1. ProcessRecord的kill()方法,其实现是向对应的进程发送SIGNAL_KILL信号;2. libc的kill()函数,也是发送信号


OOM终止进程(LMK)


App进程在系统中根据OOM(Out of Memory)ADJ(Adjustment)级别和进程状态来确定优先级,当系统需要杀死进程来释放内存时,优先级越低的会优先终止。OOM ADJ分数越小优先级越高。


由于顽固App进程后台驻留时可能会被系统回收,因此顽固App通常通过一些手段(services、弹窗)等来降低OOM(提高优先级),减少自身被系统回收的几率。


最近任务列表结束Task


用户在多任务界面(Recents)移除应用,系统会结束应用对应的Task:Removing Recent Task Item(RRT)。


该操作会结束掉与Task关联的进程,但在一些场景下仍然会有对应App的进程没有被杀死。



  1. 当App通过"Exclude from recents"功能(不在最近任务列表显示自己)时,没有提供给用户结束的机会,就没有手动结束掉Task的入口

  2. 当一个进程属于多个Task时(该进程还需要为其他Task服务)


这类终止机制由用户操作触发,当顽固应用借助多进程、多任务、唤醒拉起、互拉等操作,被终止后仍在后台运行(或后续又被唤醒),给用户感受为“杀不干净”。


强制结束App


强制结束(Force-Stop)时Android内建的功能,由ActivityManagerService提供接口,可以在设置-应用程序界面由用户手动调用。


强制结束的范畴是App对应的所有Task(即可以杀死一般App所有进程)。FSA还额外会将App设置为“STOPPED“状态,禁止应用在下一次被用户手动启用或应用跳转前被广播、服务等唤醒。强制结束对顽固App的效果不佳,许多顽固App具备Native保活能力、互拉保活、唤醒拉起等对抗措施。


此外,Android提供KILL_BACKGROUND_PROCESSES权限,允许具备权限的App调用API杀死ADJ大于SERVICE_ADJ的后台进程(即没有Service的后台进程可以被杀掉)。


保持活跃或唤醒


从最近任务隐藏或多个最近任务


Android平台提供的excludeFromRecents功能可以让App的Task在多任务中隐藏。此外一个进程可以属于不同的Task,产生多个Task并隐藏其中几个Task可以实现”杀不干净“的效果。


提升App进程优先级、阻止部分回收场景


LMK和OOM ADJ会受到进程状态和优先级的影响,提高优先级可以降低被系统回收的几率,阻止部分会杀进程的场景。


其中,将借助前台进程绑定后台服务进程保活的手段,是较常见的“杀不死、杀不干净”的情况(最近任务移除后仍有进程)。



  1. 接收广播,启动Receiver,具有Receiver的后台进程优先级高于无Receiver的后台进程

  2. 创建前台Service(高版本Android前台service需要带有通知),OOM ADJ更低(SERVICE_ADJ),杀死概率更低,此时进程不会被“杀死后台进程”杀掉(会跳过ADJ小于等于SERVICE_ADJ的进程)

  3. 保持前台Activity,OOM ADJ更低(用户可见的Task)

  4. 创建前台窗口(悬浮窗)或覆盖窗口(将窗口盖在前台App上面)

  5. 将后台服务绑定到前台进程,赋予后台服务在的进程更低的OOM,提升该进程的优先级,减少被杀的几率;同时对应进程不再属于后台进程,不会被“杀死后台进程”杀死,且该进程转为“需要为其他Task服务”,同样不会被最近任务移除时杀死

  6. 对于涉及Service的场景,ContentProvider也适用


借助Sticky Service唤醒


黏性Service是系统提供的机制,被杀死后会由系统调度进行重启。前述的force-stop杀死的进程,由于设置的“STOPPED”状态是会被跳过的,因此这种情况杀死的进程不会再自动重启。大多数ROM对此都有限制(次数、频率)。


借助广播唤醒


通过系统或其他App、组件发出的广播可以唤醒应用,顽固应用可以借助广播来完成唤醒自启。同样的,force-stop设置的“STOPPED”状态也会让广播跳过这些App,不会唤醒这些App来传递广播。但广播带有一个特例功能,带有FLAG_INCLUDE_STOPPED_PACKAGES的广播可以无视“STOPPED状态”,仍会唤醒force-stop的App。通常系统广播没有这个FLAG,基本上是其他应用发出的广播带有。


高版本的Android已经不再触发静态广播和隐式广播,这种唤醒方式少了很多。(但有FLAG_RECEIVER_INCLUDE_BACKGROUND和FLAG_INCLUDE_STOPPED_PACKAGES规避)


借助Alarm Service定时器唤醒


Alarm是Android提供的定时器功能,定时器timeout时会唤醒App。被force-stop的应用会自动移除掉注册的定时器,因此不会被唤醒。


借助Job Scheduling Service任务调度唤醒


与Alarm类似,定时唤醒App。但是受到电源管理策略、功耗管理策略、系统休眠状态、WorkManager等的影响,唤醒的定时精度较低,且不同ROM可能表现一致性较差。同样的,会跳过被force-stop的App。


借助其他App拉起唤醒


这是国内互联网App最恶心的一种机制,一群App(或集成的SDK)互相拉起对方、互相绑定提高优先级、互相拉起唤醒。其中,唤醒方式除了常规的四大组件外,还有一些黑科技、Native的方法。其中,App发出的广播带上FLAG_RECEIVER_INCLUDE_BACKGROUND和FLAG_INCLUDE_STOPPED_PACKAGES完全可以规避force-stop后"STOPPED"的应用,实现唤醒。


总结


可以说,Android本身的管理机、提供的组件间通信功能,叠加App们的流氓行为,可以说后台驻留、拉起唤醒是防不胜防的,实现较好的后台驻留管理需要较高的投入,且对系统稳定性、App基本功能的影响较大,是高投入高难度的研究方向。其中,App互拉唤醒和保活的机制,让force-stop机制做不到太好的效果,其"STOPPED"实现的类似的轻度冻结状态几乎报废,也是各大ROM厂商在后台管理部分大展身手的重要因素。


为了实现好的功耗、续航、性能,就需要在应用唤醒、冻结、暂停执行等方面下功夫了。


作者:飞起来_飞过来
来源:juejin.cn/post/7240251159763648573
收起阅读 »

微信小程序用户授权获取手机号流程

web
在做小程序开发的过程中,经常会涉及到用户身份的问题,最普遍的就是要获取用户的手机号码,通过微信获取手机号码后可以减少很多操作,比如用户手机号码验证等,以及给用户发送提示短信等等。 ※ 正常情况下,小程序可获取的公开信息有:昵称,城市,ip等公开信息,如果想要手...
继续阅读 »

在做小程序开发的过程中,经常会涉及到用户身份的问题,最普遍的就是要获取用户的手机号码,通过微信获取手机号码后可以减少很多操作,比如用户手机号码验证等,以及给用户发送提示短信等等。


※ 正常情况下,小程序可获取的公开信息有:昵称,城市,ip等公开信息,如果想要手机号等非公开信息,前提是需要已认证的非个人小程序账号。


小程序具有非常简洁的api,通过小程序内部封装,只要通过一个类型 调取他们的api方法,便可直接拉起授权。


接下来和大家聊聊,获取用户手机号码的api:getPhoneNumber


官方文档:developers.weixin.qq.com/miniprogram…


大致实现思路:


无标题.png


1687328383569.png


获取用户手机号码 分为以下几步:


第一步,点击页面获取授权按钮


第二步,获取用户授权参数


第三步,根据加解密算法解密手机号码


接下来我们来实现以上三步(包含前后端)


前端


代码:


<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button>


只要在你的view里将此button放上,便可拉起授权。


分析:
open-type=“getPhoneNumber” 这个是官方给出的api。


bindgetphonenumber=“getPhoneNumber”,是调起授权框之后用户操作的回调 用户点击了拒绝还是接受,由此方法接收通知。


bindgetphonenumber:官方指定点击事件。 如果使用uniapp则需要改为@getphonenumber,并遵循uniapp开发规范。


至于getPhoneNumber


下面看代码


getPhoneNumber (e) {
var that = this;
if (e.detail.errMsg == 'getPhoneNumber:fail user deny') { //用户点击拒绝
wx.showToast({
title: '请绑定手机号',
duration: 5000,
icon: 'none',
});
} else {
}
}

getPhoneNumber:fail user deny 拒绝与否,这个是依据
※ 很多博客上写着 getPhoneNumber:user deny 缺少了fail 。


以下为获取手机号封装方法:


getPhoneNumber(e) {
var that = this;
wx.login({
success (res) {
if (res.code) {
console.log('步骤2获检查用户登录状态,获取用户电话号码!', res)
wx.request({
url: '这里写自己的获取授权的服务器地址',
data: {code: res.code},
header: {'content-type': 'application/json'},
success: function(res) {
console.log("步骤三获取授权码,获取授权openid,session_key",res);
var userphone=res.data.data;
wx.setStorageSync('userphoneKey',userphone);
//解密手机号
var msg = e.detail.errMsg;
var sessionID=wx.getStorageSync("userphoneKey").session_key;
var encryptedData=e.detail.encryptedData;
var iv=e.detail.iv;
if (msg == 'getPhoneNumber:ok') {//这里表示获取授权成功
wx.checkSession({
success:function(){
//这里进行请求服务端解密手机号
that.deciyption(sessionID,encryptedData,iv);
},
fail:function(){
// that.userlogin()
}
})
}

},fail:function(res){
console.log("fail",res);
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})

以上


e.detail.iv
e.detail.encryptedData


两个必传 传到后台 换取mobile


后端


不管是spring还是servlet只要请求能进到该方法即可,所以重点关注中间部分,把参数值传正确即可
1687329461422.png


工具类方法 WxUtil.doGetStr(url)


1687329536242.png


以上值可以返回给前端,前端可以收到三个参数:openid,session_key,expires_in。


接着我们通过授权之后,获取第三个参数iv,调用下面方法进行服务端解密


1687329672055.png


服务端解密代码参考:


1687329836867.png


deciphering解密方法参考:


1687329894328.png


以上


作者:SC前端开发
来源:juejin.cn/post/7246997498571554871
收起阅读 »

我二姨卖猪为什么不能自己决定价格

我二姨既养猪也养牛,收益是赔的时候更多。 如果你有投资猪肉股经验,一定知道猪周期。要是在猪肉下行周期中囤猪,那就等着赔吧,赔多少而已。 我年轻的时候就想,为啥二姨自己养的猪,自己却不能决定卖多少价格?多年过去这个问题总算是有点眉目。 本质是二姨在利用市场销售自...
继续阅读 »

我二姨既养猪也养牛,收益是赔的时候更多。


如果你有投资猪肉股经验,一定知道猪周期。要是在猪肉下行周期中囤猪,那就等着赔吧,赔多少而已。


我年轻的时候就想,为啥二姨自己养的猪,自己却不能决定卖多少价格?多年过去这个问题总算是有点眉目。


本质是二姨在利用市场销售自己养的猪,而市场有其自身的规则,单一家庭养猪户是没有办法决定市场猪价的。


一 市场


市场准确的说是市场经济,自我国宋代就已诞生。


然而现代市场经济理论的奠基人是一位西方经济学家——亚当·斯密,就是写了《国富论》的作者。


在《国富论》中其详细阐述了自由市场经济的原理。他提出了“看不见的手”理论,认为在自由竞争的市场中,每个人都在追求自己的利益,这种追求会像一只“看不见的手”一样,引导市场资源向最有利于社会的方向分配。


在姚洋的《经济学的意义》中提到福利经济学第一定律:如果由市场自己去生产和交换,最后经济总会达到帕累托最优。提到福利经济学第二定律:任何的帕累托最优状态,通过调整初始的禀赋分配,最后都能在市场机制下实现。帕累托最优指的是不可能在不牺牲任何人利益的情况下改善其他人的福利的状态。


所以市场经济被认为是配置资源最好的方式,至少目前还没找到比她更好的方式。


曾经一位伟大的国人说,他所做的事情不过是对我们的国家做了一次市场化改革。


现在我们建设的是具有中国特色的市场经济。


二 边际与均衡价格


边际是一种思维方式,就是永远看市场中最后一个人的行为或者最后一个产品的情况。比如在劳动力市场上,工资不是市场中的平均水平的劳动者决定的,而是最后一个参加劳动的人决定的,要看给他多高的工资他才愿意去做这份工作,同时也要看他有多大的贡献工厂才雇用他,两者相等的时候才是市场里的均衡工资。


边际能够解释一些实际问题。比如高速费的收取,如果不收取高速费,会导致高速拥堵,收取高速费导致对高速使用价格敏感者退出,所以说高速价格不是由第一个人决定的,而是最后一些人决定的。


边际也是新商品上市后价格的演化,直到形成均衡价格。就生猪市场而言,其是成熟市场,均衡价格已经形成。在均衡价格下,价格决定于供求关系,决定于价值链,决定于生猪出厂价格和猪肉消费价格。


我的老家在河北,我从我妈那里了解到我们老家农村的猪肉价格是10/斤元上下;而我在北京小区超市看到的是13/斤元上下。这个价格我认为肯定不是大家口袋里没钱造成的。


我又看了下A股几家上规模的生猪养殖集团:牧原股份、温氏股份、正邦科技。三者在2023年都是大幅亏损,其中正邦科技更是st了。而2020年牧原股份大幅度盈利200多个亿,我还查到2020年河北9月份的平均生猪价格,为33.73元/公斤,这都赶上今年的猪肉价了。


这样的数据结果表明今年的猪肉或者生猪价格,主要是供给导致,是生猪太多,生猪养殖太卷,不得不低价销售导致。


总结一下,生猪养殖市场均衡价格由供求关系决定,供求关系就像是天平,只有其上的砝码发生较大变化时才会影响平衡。就生猪市场来说,供求关系可以被牧原股份这种千万生猪体量的养殖集团影响,可以被一场范围特别大的猪瘟影响。


单一家庭养猪户因为生猪体量非常非常小,影响力微乎其微,不可能影响供求关系,也就不可能决定生猪价格,这也就是我二姨不能决定卖猪价格的原因。


三 周期


这部分属于题外话。不仅猪市场存在周期;文明也有周期,表现为王朝的兴衰更替;经济本身也存在周期,比如加息周期和降息周期;现在更有万物皆周期一说。


一种解释是,周期的产生源于人的贪婪。有一句话著名的话:人们从历史中吸取的教训就是从不吸取教训


文明周期源于王朝的后期统治者普遍开始奢侈,导致统治力衰弱,最终王朝灭亡,比如烽火戏诸侯。


经济周期源于债务,也是贪婪。债务越借越大,越借越不想还,就比如现在的美利坚,你看他的国债多大了,一年利息都1w多亿。


或许周期本源于人性,源于这个世界本身,且看那天地有四季,有日月更替。


尾声


现在二姨已经不养猪了。如果还养猪,我会建议要当有大猪场倒闭时再进入,这个时候市场上能卖猪的少了,而想买猪的没变,均衡价格该起来了。


作者:通往自由之路pro
来源:juejin.cn/post/7352100456334639114
收起阅读 »

如何快速实现一个无缝轮播效果

web
需求简介 轮播图是我们前端开发中的一个常见需求,在项目开发中,我们可以使用element、ant等UI库实现。某些场景,为了一个简单的功能安装一个库是没必要的,我们最好的选择就是手搓。 我们来看一个需求 上述需求核心就是实现一个无缝轮播的切换效果。以这个需求...
继续阅读 »

需求简介


轮播图是我们前端开发中的一个常见需求,在项目开发中,我们可以使用element、ant等UI库实现。某些场景,为了一个简单的功能安装一个库是没必要的,我们最好的选择就是手搓。


我们来看一个需求



上述需求核心就是实现一个无缝轮播的切换效果。以这个需求为例,我们看看最终实现效果:



实现思路


要想实现一个无缝的轮播效果,其实非常简单,核心思想就是动态改变显示的列表而已。比如我们有这样一个数组


const list = ref([
{ name: 1, id: 1 },
{ name: 2, id: 2 },
{ name: 3, id: 3 }
])

如果我们想无缝切换的展示这个数据,最简单的代码就是动态的改变下面的代码的index


<template>
<div>
{{ list[index] }}
</div>

</template>
<script setup>
const index = ref(0)
const list = ref([{ name: 1, id: 1 }, { name: 2, id: 2 }, { name: 2, id: 2 }])
<scriptp>

那如何实现切换的样式呢?也非常简单,我们只要给元素添加一个出现样式和离开样式即可。现在,我们来具体实现这样一个需求。


技术方案


数据的动态切换


要想实现一个数据的动态循环切换效果,是非常容易的:


<template>
<div v-for="(build, index) in list" :key="index">
<div v-show="index === selectIndex">
卡片自定义内容
</div>
</div>

</template>
<script setup>
const selectIndex = ref(0)
const list = ref(
[{ name: "卡片1", id: 1 }, { name: "卡片1", id: 2 }, { name: "卡片1", id: 2 }]
)

// #计时器实例
let timer: any = null

// >计时器逻辑
const timeFuc = () => {
timer = setInterval(() => {
// 更改选中的index
if (selectIndex.value >= list.value.length - 1) {
selectIndex.value = 0
} else {
selectIndex.value++
}
}, 5000)
}

timeFuc()
<scriptp>

上述代码中,我们设置了一个定时器,定时器每5s执行一次,每次执行都会动态更改当前要显示的数据索引值,当索引值达到最大实,在将其重置。通过上述的简单代码,我们就实现了一个可以自动切换的循环渲染的卡片。


动画添加


要想实现最终效果的动态效果也非常容易,我们只需要给每个元素出现时设置一些样式,离开时设置一些样式即可。借助vue的Transition组件,我们能很容易实现这样一个效果。



如果你不了解vue的Transition组件,请去官网补充下知识:cn.vuejs.org/guide/built…



<template>
<div class="main-content">
<Transition v-for="(build, index) in list" :key="selectIndex">
<div class="banner-scroll-wrap" v-show="index === selectIndex">
卡片自定义内容
</div>
</Transition>
</div>

</template>
<script setup>
const selectIndex = ref(0)
const list = ref(
[{ name: "卡片1", id: 1 }, { name: "卡片1", id: 2 }, { name: "卡片1", id: 2 }]
)

// #计时器实例
let timer: any = null

// >计时器逻辑
const timeFuc = () => {
timer = setInterval(() => {
// 更改选中的index
if (selectIndex.value >= list.value.length - 1) {
selectIndex.value = 0
} else {
selectIndex.value++
}
}, 5000)
}

timeFuc()
<scriptp>
<style lang="less" scoped>
.main-content {
position: relative;
height: 100%;
.banner-scroll-wrap {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
}

.v-enter-from {
transform: translateX(100%);
opacity: 0;
}

.v-enter-active,
.v-leave-active {
transition: transform 600ms ease-in-out, opacity 600ms ease-in-out;
}

.v-enter-to {
transform: translateX(0);
opacity: 1;
}

.v-leave-from {
transform: translateX(0);
opacity: 1;
}

.v-leave-to {
transform: translateX(-100%);
opacity: 0;
}
<style/>

上述代码中,由于 selectIndex是动态的,元素不断地在显示与隐藏。因此,Transition标签的进入样式和离开样式会动态触发,从而形成切换效果。



v-enter是元素的进入样式,进入时,我们从最右边偏移100%的距离到正常位置,透明度从0到1,这个过程持续0.6s,实现了元素左移淡入的效果。




v-leave是元素的离开样式,离开时,我们从正常位置偏移到100%的左侧位置,透明度从1到0,这个过程持续0.6s,实现了元素左移淡出的效果。



在这些类的共同作用下,我们实现了元素的动态切换。


你可能注意到了我给元素设置了一个banner-scroll-wrap类名,并使用了position: absolute,这样设置的注意目的是保证切换离开元素的淡出效果和进入元素的淡入效果是连贯的。如果你不这样写,可能会出现样式问题。


此外,注意我给Transition设置了key="Transition",这样些会保证每次数据在切换时,transition能够重新渲染,触发元素离开和进入的样式。


至此,我们就完成了基本功能样式



轮播的停止与恢复


很常见的一种情况就是我们需要鼠标放在卡片上时停止轮播,离开卡片的时候恢复轮播,这非常容易。


<template>
<div class="main-content" @mouseenter="stop()" @mouseleave="start()">
<Transition v-for="(build, index) in list" :key="selectIndex">
<div class="banner-scroll-wrap" v-show="index === selectIndex">
卡片自定义内容
</div>
</Transition>
</div>

</template>
<script setup>
const selectIndex = ref(0)
const list = ref(
[{ name: "卡片1", id: 1 }, { name: "卡片1", id: 2 }, { name: "卡片1", id: 2 }]
)

// #计时器实例
let timer: any = null

// >计时器逻辑
const timeFuc = () => {
timer = setInterval(() => {
// 更改选中的index
if (selectIndex.value >= list.value.length - 1) {
selectIndex.value = 0
} else {
selectIndex.value++
}
}, 5000)
}

// >开启轮播
const start = () => {
if (timer) return
timeFuc()
}

// >关闭轮播
const stop = () => {
clearInterval(timer)
timer = null
}

timeFuc()
<scriptp>
<style lang="less" scoped>
<style/>

解决重影问题


在某些情况下,我们离开这个页面很久后(浏览器切换到其他选项卡),然后在切回来的时候,可能会出现短暂的画面重影问题,这个问题也很好解决,加上下面的代码即可


<script setup>

//...

// 解决切屏后重影的问题
onMounted(() => {
document.addEventListener('visibilitychange', () => {
// 用户息屏、或者切到后台运行 (离开页面)
if (document.visibilityState === 'hidden') {
stop()
}
// 用户打开或回到页面
if (document.visibilityState === 'visible') {
start()
}
})
})

onBeforeUnmount(() => stop())

<scriptp>

visibilitychange 事件:当其选项卡的内容变得可见或被隐藏时,会在 document 上触发 visibilitychange 事件。该事件不可取消。


总结


在本教程中,我们通过简单代码实现了无缝轮播效果,样式是左右切换,我们也可以通过样式控制实现上下切换的效果,比如将translateX设置为translateY即可。


 .v-enter-from {
transform: translateY(100%);
opacity: 0;
}

时间原因,本教程也没有对技术上做深究,也希望各位大佬能提供自己的思路与建议,感谢大家分享!


作者:石小石Orz
来源:juejin.cn/post/7351790785743978537
收起阅读 »

打造聊天框丝滑滚动体验:AI 聊天框的翻转之道 ——— 聊天框发送消息后自动滚动到底部(前端框架通用)

web
逐字渲染的挑战 最近在开发AI聊天助手的时候,遇到了一个很有趣的滚动问题。我们需要开发一个类似微信聊天框的交互体验: 每当聊天框中展示新消息时,需要将聊天框滚动到底部,展示最新消息。 如果在 web 什么也不做,聊天体验可能是这样的,需要用户手动滚动到最新消...
继续阅读 »

逐字渲染的挑战


最近在开发AI聊天助手的时候,遇到了一个很有趣的滚动问题。我们需要开发一个类似微信聊天框的交互体验:


每当聊天框中展示新消息时,需要将聊天框滚动到底部,展示最新消息。


请在此添加图片描述


如果在 web 什么也不做,聊天体验可能是这样的,需要用户手动滚动到最新消息:


请在此添加图片描述


试想一下如何在 web 中实现微信的效果。每当聊天框中接收到新消息时,都需要调用滚动方法滚动到消息底部。


element.scrollIntoView({ behavior: "smooth", block: "end");

对于普通的聊天工具来说,这样实现没有什么大问题,因为聊天框接收到每条消息的长度都是确定的。但是 AI 大模型一般都是逐字渲染的,AI 助手聊天框接受的消息体大小不是固定的,而是会随着 AI 大模型的输出不断变大。如果仍使用 scrollIntoView 来滚动到底部,就需要监听消息体的变化,每次消息更新时都要通过 JavaScript 调用一次滚动方法,会造成一些问题:



  1. 频繁的 JavaScript 滚动调用。每输出一个文字要滚动一次,听起来就会性能焦虑。

  2. AI 正在输出内容时,用户无法滚动查看历史消息。用户向上滚动查看历史消息,会被 Javascript 不断执行的 scrollIntoView 打断。需要写特殊逻辑才能避免这个情况。

  3. 通过监听数据变化频繁的执行滚动,基于浏览器单线程的设计,不可避免的会造成滚动行为的滞后,导致聊天体验不够丝滑。


自然列表:灵感来源


聊天框接收到新消息时滚动到最新位置,总感觉这应该是一个很自然的行为,不需要这么多 Javascript 代码去实现滚动行为。


于是联想到了 Excel 表格,当我们在表格中第一行插入一行,这一行后边的内容会被很自然的挤下去。并不需要做什么滚动,这一行就会出现在最顶部的位置。


请在此添加图片描述


想到这里惊讶的发现,聊天框实际上不就是一个倒过来的列表吗? 列表最上边新增的行会把后边的行往下挤,而聊天框最下边新增消息需要把上边的消息往上挤。那假如我们将聊天框旋转 180° 呢...?


聊天框的翻转实现


翻转聊天框


请在此添加图片描述


利用 CSS transform: rotate(180deg) 将整个聊天框倒转,并且把接收到最新的消息插入到消息列表的头部。发现我们的设想确实是行得通的,新增的消息很自然的把历史消息顶了上去,消息卡片内容增加也能很自然的撑开。并且在消息输出时,也可以随意滚动查看历史记录。


滚动条调整与滚动行为反转


最核心的问题已经解决了,但总觉得哪里看起来怪怪的。滚动条怎么跑到左边,并且滚动行为和鼠标滚轮的方向反了,滚轮向上滚,聊天框却向下滚。(让人想起了 MacOS 连鼠标滚轮的反人类体验)


查阅文档发现 CSS 有个 direction: rtl; 属性可以改变内容的排布的方向。这样我们就可以把滚动条放回右边了。然后在通过监听滚动事件,改变滚动方向就可以恢复鼠标滚轮的滚动行为。


element.addEventListener('wheel', event => {
event.preventDefault(); // 阻止默认滚动行为
const { deltaY } = event; // 获取滚动方向和速度
chatContent.current.scrollTop -= deltaY; // 反转方向
});

请在此添加图片描述


消息卡片翻转恢复


可以看到目前就只剩下聊天框中的消息卡片是反的,接下来把聊天框中的消息卡片转正就大功告成了。我们在聊天框中,给每个消息卡片都添加 transform: rotate(180deg);direction: ltr; 样式,把消息重新转正。


这样就把翻转的行为全部隔离在了聊天框组件中。消息卡片组件完全感知不到自己其实已经被旋转了 180° 后又旋转了 180° 了。聊天框的父组件也完全不知道自己的子节点被转了又转。


742ea972f92d4e7abc7344e75c331467.avif


总结


最后总结一下,我们通过两行 CSS 代码 + 反转滚动行为,利用浏览器的默认行为完美的实现了 AI 聊天框中的滚动体验。


transform: rotate(180deg);
direction: rtl;

element.addEventListener('wheel', event => {
event.preventDefault(); // 阻止默认滚动行为
const { deltaY } = event; // 获取滚动方向和速度
chatContent.current.scrollTop -= deltaY; // 反转方向
});

DEMO 仓库:github.com/lrwlf/messa…




更新:


想到一个更简洁的办法可以达到相同的效果,只用把聊天框 CSS 设置为:


display: flex;
flex-direction: column-reverse;

让列表倒序渲染,并且像原来的方法一样,在消息列表的头部插入消息,就可以实现一样的效果。不需要对聊天框和消息体再进行旋转操作,也不需要反转滚动条的行为。


以上两种方法都存在一个相同的问题,当一开始聊天消息还很少时,聊天消息也会紧贴着底部,顶部会留出一片空白。


请在此添加图片描述


这时只需要在聊天列表的最开始设置一个空白的占位元素,把它的 CSS 设置为:


flex-grow: 1;
flex-shrink: 1;

就可以实现消息少的时候自动撑开,把消息撑到顶部。消息列表开始滚动时,占位元素又会被挤压消失,不影响列表滚动效果。


(为了演示,把占位元素设置为了黑色)


请在此添加图片描述


更新部分代码见: github.com/lrwlf/messa…


将 App.js 的 chat 组件,替换为 src/components/chat-flex


作者:lrwlf
来源:juejin.cn/post/7306693980959588379
收起阅读 »

实现一个支持@的输入框

web
近期产品期望在后台发布帖子或视频时,需要添加 @用户 的功能,以便用户收到通知,例如“xxx在xxx提及了您!”。然而,现有的开源库未能满足我们的需求,例如 ant-design 的 Mentions 组件: 但是不难发现跟微信飞书对比下,有两个细节没有处...
继续阅读 »

近期产品期望在后台发布帖子或视频时,需要添加 @用户 的功能,以便用户收到通知,例如“xxx在xxx提及了您!”。然而,现有的开源库未能满足我们的需求,例如 ant-design 的 Mentions 组件:



20240415161851.gif


但是不难发现跟微信飞书对比下,有两个细节没有处理。



  1. @用户没有高亮

  2. 在删除时没有当做一个整体去删除,而是单个字母删除,首先不谈用户是否想要整体删除,在这块有个模糊查询的功能,如果每删一个字母之后去调接口查询数据库造成一些不必要的性能开销,哪怕加上防抖。


然后也是找了其他的库都没达到产品的期望效果,那么好,自己实现一个,先看看最终实现的效果


6ND88RssMr.gif


封装之后使用:


<AtInput
height={150}
onRequest={async (searchStr) => {
const { data } = await UserFindAll({ nickname: searchStr });
return data?.list?.map((v) => ({
id: v.uid,
name: v.nickname,
wechatAvatarUrl: v.wechatAvatarUrl,
}));
}}
onChange={(content, selected) => {
setAtUsers(selected);
}}
/>

那么实现这么一个输入框大概有以下几个点:



  1. 高亮效果

  2. 删除/选中用户时需要整体删除

  3. 监听@的位置,复制给弹框的坐标,联动效果

  4. 最后我需要拿到文本内容,并且需要拿到@那些用户,去做表单提交


大多数文本输入框我们会使用input,或者textarea,很明显以上1,2两点实现不了,antd也是使用的textarea,所以也是没有实现这两个效果。所以这块使用富文本编辑,设置contentEditable,将其变为可编辑去做。输入框以及选择器的dom就如下:


 <div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div
id="atInput"
ref={atRef}
className={'editorDiv'}
contentEditable
onInput={editorChange}
onClick={editorClick}
/>

{/* 选择用户框 */}
<SelectUser
options={options}
visible={visible}
cursorPosition={cursorPosition}
onSelect={onSelect}
/>

</div>

实现思路:



  1. 监听输入@,唤起选择框。

  2. 截取@xxx的xxx作为搜素的关键字去查询接口

  3. 选择用户后需要将原先输入的 @xxx 替换成 @姓名,并且将@的用户缓存起来

  4. 选择文本框中的姓名时需要变为整体选中状态,这块依然可以给标签设置为不可编辑状态就可实现,contentEditable=false,即可实现整体删除,在删除的同时需要将当前用户从之前缓存的@过的用户数组删除

  5. 那么可以拿到输入框的文本,@的用户, 最后将数据抛给父组件就完成了


以上提到了监听@文本变化,通常绑定onChange事件就行,但是还有一种用户通过点击移动光标,这块需要绑定change,click两个时间,他们里边的逻辑基本一样,只需要额外处理点击选中输入框中用户时,整体选中g功能,那么代码如下:


    const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};

const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};

const editorClick = async (event) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};

const editorChange = (event) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};

每次点击或者文本改变时都会去调用onObserveInput,以上onObserveInput该方法中主要做了以下逻辑:




  1. 通过getSelection方法可以获取光标的偏移位置,那么可以截取光标之前的字符串,并且使用lastIndexOf从后向前查找最后一个“@”符号,并记录他的下标,那么有了【光标之前的字符串】,【@的下标】就可以拿到到@之后用于过滤用户的关键字,并将其缓存起来。

  2. 唤起选择器,并通过关键字去过滤用户。这块涉及到一个选择器的位置,直接使用window.getSelection()?.getRangeAt(0).getBoundingClientRect()去获取光标的位置拿到的是光标相对于窗口的坐标,直接用这个坐标会有问题,比如滚动条滚动时,这个选择器发生位置错乱,所以这块同时去拿输入框的坐标,去做一个相减,这样就可以实现选择器跟着@符号联动的效果。


 const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};

选择器弹出后,那么下面就到了选择用户之后的流程了,


 /**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/

const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};

/**
* 选择用户时回调
*/

const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};

选择用户的时候需要做的以下以下几点:



  1. 删除之前的@xxx字符

  2. 插入不可编辑的span标签

  3. 将当前选择的用户缓存起来

  4. 重新获取输入框的内容

  5. 关闭选择器

  6. 将输入框重新聚焦


最后


在选择的用户或者内容发生改变时将数据抛给父组件


 const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};

/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);

完整组件代码


输入框主要逻辑代码:


let timer: NodeJS.Timeout | null = null;

const AtInput = (props: AtInputProps) => {
const { height = 300, onRequest, onChange, value, onBlur } = props;
// 输入框的内容=innerText
const [content, setContent] = useState<string>('');
// 选择用户弹框
const [visible, setVisible] = useState<boolean>(false);
// 用户数据
const [options, setOptions] = useState<Options[]>([]);
// @的索引
const [currentAtIdx, setCurrentAtIdx] = useState<number>();
// 输入@之前的字符串
const [focusNode, setFocusNode] = useState<Node | string>();
// @后关键字 @郑 = 郑
const [searchStr, setSearchStr] = useState<string>('');
// 弹框的x,y轴的坐标
const [cursorPosition, setCursorPosition] = useState<Position>({
x: 0,
y: 0,
});
// 选择的用户
const [selected, setSelected] = useState<Options[]>([]);
const atRef = useRef<any>();

/** 获取选择器弹框坐标 */
const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};

/**获取用户下拉列表 */
const fetchOptions = (key?: string) => {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(async () => {
const _options = await onRequest(key);
setOptions(_options);
}, 500);
};

useEffect(() => {
fetchOptions();
// if (value) {
// /** 判断value中是否有at用户 */
// const atUsers: any = StringTools.filterUsers(value);
// setSelected(atUsers);
// atRef.current.innerHTML = value;
// setContent(value.replace(/<\/?.+?\/?>/g, '')); //全局匹配内html标签)
// }
}, []);

const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};

const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};

const editorClick = async (e?: any) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};

const editorChange = (event: any) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};

/**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/

const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};

/**
* 选择用户时回调
*/

const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};

const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};

/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);

return (
<div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div id="atInput" ref={atRef} className={'editorDiv'} contentEditable onInput={editorChange} onClick={editorClick} />
{/* 选择用户框 */}
<SelectUser options={options} visible={visible} cursorPosition={cursorPosition} onSelect={onSelect} />
</div>

);
};

选择器代码


const SelectUser = React.memo((props: SelectComProps) => {
const { options, visible, cursorPosition, onSelect } = props;

const { x, y } = cursorPosition;

return (
<div
className={'selectWrap'}
style={{
display: `${visible ? 'block' : 'none'}`,
position: 'absolute',
left: x,
top: y + 20,
}}
>

<ul>
{options.map((item) => {
return (
<li
key={item.id}
onClick={() =>
{
onSelect(item);
}}
>
<img src={item.wechatAvatarUrl} alt="" />
<span>{item.name}</span>
</li>
);
})}
</ul>
</div>

);
});
export default SelectUser;

以上就是实现一个支持@用户的输入框功能,就目前而言,比较死板,不支持自定义颜色,自定义选择器等等,未来,可以进一步扩展功能,例如添加@用户的高亮样式定制、支持键盘快捷键操作等,从而提升用户体验和功能性。


作者:tech_zjf
来源:juejin.cn/post/7357917741909819407
收起阅读 »

Android 双屏异显自适应Dialog

一、前言 Android 多屏互联的时代,必然会出现多屏连接的问题,通常意义上的多屏连接包括HDMI/USB、WifiDisplay,除此之外Android 还有OverlayDisplay和VirtualDisplay,其中VirtualDisplay相比不...
继续阅读 »

一、前言


Android 多屏互联的时代,必然会出现多屏连接的问题,通常意义上的多屏连接包括HDMI/USB、WifiDisplay,除此之外Android 还有OverlayDisplay和VirtualDisplay,其中VirtualDisplay相比不少人录屏的时候都会用到,在Android中他们都是Display,除了物理屏幕,你在OverlayDisplay和VirtualDisplay同样也可以展示弹窗或者展示Activity,所有的Display的差异化通过DisplayManagerService 进行了兼容,同样任意一种Display都拥有自己的密度和大小以及display Id,对于测试双屏应用,一般也可以通过VirtualDisplay进行模拟操作。


企业微信20231224-132106@2x.png


需求


本篇主要解决副屏Dialog 组建展示问题。存在任意类型的副屏时,让 Dialog 展示在副屏上,如果不存在,就需要让它自动展示在主屏上。


为什么会有这种需求呢?默认情况下,实现双屏异显的时候, 通常不是使用Presentation就是Activity,然而,Dialog只能展示在主屏上,而Presentation只能展示的副屏上。想象一下这种双屏场景,在切换视频的时候,Loading展示应该是在主屏还是副屏呢 ?毫无疑问,答案当然是副屏。


问题


我们要解决的问题当然是随着场景的切换,Dialog展示在不同的屏幕上。同样,内容也可以这样展示,当存在副屏的时候在副屏上展示内容,当只有主屏的时候在主屏上展示内容。


二、方案


我们这里梳理一下两种方案。


方案:自定义Presentation


作为Presentation的核心点有两个,其中一个是displayId,另一个是WindowType,第一个是通常意义上指定Display Id,第二个是窗口类型。如果是副屏,那么displayId是必须的参数,且不能和DefaultDisplay的id一样,除此之外WindowType是一个需要重点关注的东西。


早期的 TYPE_PRESENTATION 存在指纹信息 “被借用” 而造成用户资产损失的风险,即便外部无法获取,但是早期的Android 8.0版本利用 (TYPE_PRESENTATION=TYPE_APPLICATION_OVERLAY-1)可以实现屏幕外弹框,在之后的版本做了修复,同时对 TYPE_PRESENTATION 展示必须有 Token 等校验,但是在这种过程中,Presentation的WindowType 变了又变,因此,我们如何获取到兼容每个版本的WindowType呢?


原理


Display Id的问题我们不需要重点处理,从display 获取即可。WindowType才是重点,方法当然是有的,我们不继承Presentation,而是继承Dialog因此自行实现可以参考 Presentation 中的代码,当然难点是 WindowManagerImpl 和WindowType类获取,前者 @hide 标注的,而后者不固定。


早期我们可以利用 compileOnly layoutlib.jar 的方式导入 WindowManagerImpl,但是新版本中 layoutlib.jar 中的类已经几乎被删,另外如果要使用 layoutlib.jar,那么你的项目中的 kotlin 版本就会和 layoutlib.jar 产生冲突,虽然可以删除相关的类,但是这种维护方式非常繁琐,因此我们这里借助反射实现。当然除了反射也可以利用Dexmaker或者xposed Hook方式,只是复杂性会很多。


WindowType问题解决

我们知道,创建Presentation的时候,framework源码是设置了WindowType的,我们完全在我们自己的Dialog创建Presentation对象,读取出来设置上到我们自己的Dialog上即可。


不过,我们先要对Display进行隔离,避免主屏走这段逻辑


WindowManager wm = (WindowManager) outerContext.getSystemService(WINDOW_SERVICE); 
if(display==null || wm.getDefaultDisplay().getDisplayId()==display.getDisplayId()){
return;
}

//注意,这里需要借助Presentation的一些属性,否则无法正常弹出弹框,要么有权限问题、要么有token问题


Presentation presentation = new Presentation(outerContext, display, theme);  
WindowManager.LayoutParams standardAttributes =presentation.getWindow().getAttributes();
final Window w = getWindow();
final WindowManager.LayoutParams attr = w.getAttributes();
attr.token = standardAttributes.token; w.setAttributes(attr);
//type 源码中是TYPE_PRESENTATION,事实上每个版本是不一样的,因此这里动态获取 w.setGravity(Gravity.FILL);
w.setType(standardAttributes.type);

WindowManagerImpl 问题

其实我们知道,Presentation的WindowManagerImpl并不是给自己用的,而是给Dialog上的其他组件(如Menu、PopWindow等),将其他组件加到Dialog的 Window上,因为在Android系统中,WindowManager都是parent Window所具备的能力,所以创建这个不是为了把Dialog加进去,而是为了把基于Dialog的Window组件加到Dialog上,这和Activity是一样的。那么,其实如果我们没有Menu、PopWindow,这里实际上是可以不处理的,但是作为一个完整的类,我们这里使用反射处理一下。


怎么处理呢?


我们知道,异显屏的Context是通过createDisplayContext创建的,但是我们这里并不是Hook这个方法,只是在创建这个Display Context之后,再通过ContextThemeWrapper,设置进去即可。


private static Context createPresentationContext(
Context outerContext, Display display, int theme)
{
if (outerContext == null) {
throw new IllegalArgumentException("outerContext must not be null");
}
WindowManager outerWindowManager = (WindowManager) outerContext.getSystemService(WINDOW_SERVICE);
if (display == null || display.getDisplayId()==outerWindowManager.getDefaultDisplay().getDisplayId()) {
return outerContext;
}
Context displayContext = outerContext.createDisplayContext(display);
if (theme == 0) {
TypedValue outValue = new TypedValue();
displayContext.getTheme().resolveAttribute(
android.R.attr.presentationTheme, outValue, true);
theme = outValue.resourceId;
}

// Derive the display's window manager from the outer window manager.
// We do this because the outer window manager have some extra information
// such as the parent window, which is important if the presentation uses
// an application window type.
// final WindowManager outerWindowManager =
// (WindowManager) outerContext.getSystemService(WINDOW_SERVICE);
// final WindowManagerImpl displayWindowManager =
// outerWindowManager.createPresentationWindowManager(displayContext);

WindowManager displayWindowManager = null;
try {
ClassLoader classLoader = ComplexPresentationV1.class.getClassLoader();
Class<?> loadClass = classLoader.loadClass("android.view.WindowManagerImpl");
Method createPresentationWindowManager = loadClass.getDeclaredMethod("createPresentationWindowManager", Context.class);
displayWindowManager = (WindowManager) loadClass.cast(createPresentationWindowManager.invoke(outerWindowManager,displayContext));
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
final WindowManager windowManager = displayWindowManager;
return new ContextThemeWrapper(displayContext, theme) {
@Override
public Object getSystemService(String name) {
if (WINDOW_SERVICE.equals(name)) {
return windowManager;
}
return super.getSystemService(name);
}
};
}

全部源码


public class ComplexPresentationV1 extends Dialog  {

private static final String TAG = "ComplexPresentationV1";
private static final int MSG_CANCEL = 1;

private Display mPresentationDisplay;
private DisplayManager mDisplayManager;
/**
* Creates a new presentation that is attached to the specified display
* using the default theme.
*
* @param outerContext The context of the application that is showing the presentation.
* The presentation will create its own context (see {@link #getContext()}) based
* on this context and information about the associated display.
* @param display The display to which the presentation should be attached.
*/

public ComplexPresentationV1(Context outerContext, Display display) {
this(outerContext, display, 0);
}

/**
* Creates a new presentation that is attached to the specified display
* using the optionally specified theme.
*
* @param outerContext The context of the application that is showing the presentation.
* The presentation will create its own context (see {@link #getContext()}) based
* on this context and information about the associated display.
* @param display The display to which the presentation should be attached.
* @param theme A style resource describing the theme to use for the window.
* See <a href="{@docRoot}guide/topics/resources/available-resources.html#stylesandthemes">
* Style and Theme Resources</a> for more information about defining and using
* styles. This theme is applied on top of the current theme in
* <var>outerContext</var>. If 0, the default presentation theme will be used.
*/

public ComplexPresentationV1(Context outerContext, Display display, int theme) {
super(createPresentationContext(outerContext, display, theme), theme);
WindowManager wm = (WindowManager) outerContext.getSystemService(WINDOW_SERVICE);
if(display==null || wm.getDefaultDisplay().getDisplayId()==display.getDisplayId()){
return;
}
mPresentationDisplay = display;
mDisplayManager = (DisplayManager)getContext().getSystemService(DISPLAY_SERVICE);

//注意,这里需要借助Presentation的一些属性,否则无法正常弹出弹框,要么有权限问题、要么有token问题
Presentation presentation = new Presentation(outerContext, display, theme);
WindowManager.LayoutParams standardAttributes = presentation.getWindow().getAttributes();

final Window w = getWindow();
final WindowManager.LayoutParams attr = w.getAttributes();
attr.token = standardAttributes.token;
w.setAttributes(attr);
w.setType(standardAttributes.type);
//type 源码中是TYPE_PRESENTATION,事实上每个版本是不一样的,因此这里动态获取
w.setGravity(Gravity.FILL);
setCanceledOnTouchOutside(false);
}

/**
* Gets the {@link Display} that this presentation appears on.
*
* @return The display.
*/

public Display getDisplay() {
return mPresentationDisplay;
}

/**
* Gets the {@link Resources} that should be used to inflate the layout of this presentation.
* This resources object has been configured according to the metrics of the
* display that the presentation appears on.
*
* @return The presentation resources object.
*/

public Resources getResources() {
return getContext().getResources();
}

@Override
protected void onStart() {
super.onStart();

if(mPresentationDisplay ==null){
return;
}
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);

// Since we were not watching for display changes until just now, there is a
// chance that the display metrics have changed. If so, we will need to
// dismiss the presentation immediately. This case is expected
// to be rare but surprising, so we'll write a log message about it.
if (!isConfigurationStillValid()) {
Log.i(TAG, "Presentation is being dismissed because the "
+ "display metrics have changed since it was created.");
mHandler.sendEmptyMessage(MSG_CANCEL);
}
}

@Override
protected void onStop() {
if(mPresentationDisplay ==null){
return;
}
mDisplayManager.unregisterDisplayListener(mDisplayListener);
super.onStop();
}

/**
* Inherited from {@link Dialog#show}. Will throw
* {@link android.view.WindowManager.InvalidDisplayException} if the specified secondary
* {@link Display} can't be found.
*/

@Override
public void show() {
super.show();
}

/**
* Called by the system when the {@link Display} to which the presentation
* is attached has been removed.
*
* The system automatically calls {@link #cancel} to dismiss the presentation
* after sending this event.
*
* @see #getDisplay
*/

public void onDisplayRemoved() {
}

/**
* Called by the system when the properties of the {@link Display} to which
* the presentation is attached have changed.
*
* If the display metrics have changed (for example, if the display has been
* resized or rotated), then the system automatically calls
* {@link #cancel} to dismiss the presentation.
*
* @see #getDisplay
*/

public void onDisplayChanged() {
}

private void handleDisplayRemoved() {
onDisplayRemoved();
cancel();
}

private void handleDisplayChanged() {
onDisplayChanged();

// We currently do not support configuration changes for presentations
// (although we could add that feature with a bit more work).
// If the display metrics have changed in any way then the current configuration
// is invalid and the application must recreate the presentation to get
// a new context.
if (!isConfigurationStillValid()) {
Log.i(TAG, "Presentation is being dismissed because the "
+ "display metrics have changed since it was created.");
cancel();
}
}

private boolean isConfigurationStillValid() {
if(mPresentationDisplay ==null){
return true;
}
DisplayMetrics dm = new DisplayMetrics();
mPresentationDisplay.getMetrics(dm);
try {
Method equalsPhysical = DisplayMetrics.class.getDeclaredMethod("equalsPhysical", DisplayMetrics.class);
return (boolean) equalsPhysical.invoke(dm,getResources().getDisplayMetrics());
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return false;
}

private static Context createPresentationContext(
Context outerContext, Display display, int theme)
{
if (outerContext == null) {
throw new IllegalArgumentException("outerContext must not be null");
}
WindowManager outerWindowManager = (WindowManager) outerContext.getSystemService(WINDOW_SERVICE);
if (display == null || display.getDisplayId()==outerWindowManager.getDefaultDisplay().getDisplayId()) {
return outerContext;
}
Context displayContext = outerContext.createDisplayContext(display);
if (theme == 0) {
TypedValue outValue = new TypedValue();
displayContext.getTheme().resolveAttribute(
android.R.attr.presentationTheme, outValue, true);
theme = outValue.resourceId;
}

// Derive the display's window manager from the outer window manager.
// We do this because the outer window manager have some extra information
// such as the parent window, which is important if the presentation uses
// an application window type.
// final WindowManager outerWindowManager =
// (WindowManager) outerContext.getSystemService(WINDOW_SERVICE);
// final WindowManagerImpl displayWindowManager =
// outerWindowManager.createPresentationWindowManager(displayContext);

WindowManager displayWindowManager = null;
try {
ClassLoader classLoader = ComplexPresentationV1.class.getClassLoader();
Class<?> loadClass = classLoader.loadClass("android.view.WindowManagerImpl");
Method createPresentationWindowManager = loadClass.getDeclaredMethod("createPresentationWindowManager", Context.class);
displayWindowManager = (WindowManager) loadClass.cast(createPresentationWindowManager.invoke(outerWindowManager,displayContext));
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
final WindowManager windowManager = displayWindowManager;
return new ContextThemeWrapper(displayContext, theme) {
@Override
public Object getSystemService(String name) {
if (WINDOW_SERVICE.equals(name)) {
return windowManager;
}
return super.getSystemService(name);
}
};
}

private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
}

@Override
public void onDisplayRemoved(int displayId) {
if (displayId == mPresentationDisplay.getDisplayId()) {
handleDisplayRemoved();
}
}

@Override
public void onDisplayChanged(int displayId) {
if (displayId == mPresentationDisplay.getDisplayId()) {
handleDisplayChanged();
}
}
};

private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CANCEL:
cancel();
break;
}
}
};
}

方案:Delegate方式:


第一种方案利用反射,但是android 9 开始,很多 @hide 反射不被允许,但是办法也是很多的,比如 freeflection 开源项目,不过对于开发者,能减少对@hide的使用也是为了后续的维护。此外还有一个需要注意的是 Presentation 继承的是 Dialog 构造方法是无法被包外的子类使用,但是影响不大,我们在和Presentation的包名下创建我们的自己的Dialog依然可以解决。不过,对于反射天然厌恶的人来说,可以使用代理。


这种方式借壳 Dialog,套用 Dialog 一层,以代理方式实现,不过相比前一种方案来说,这种方案也有很多缺陷,比如他的onCreate\onShow\onStop\onAttachToWindow\onDetatchFromWindow等方法并没有完全和Dialog同步,需要做下兼容。


兼容


onAttachToWindow\onDetatchFromWindow


WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (display != null && display.getDisplayId() != wm.getDefaultDisplay().getDisplayId()) {
dialog = new Presentation(context, display, themeResId);
} else {
dialog = new Dialog(context, themeResId);
}
//下面兼容attach和detatch问题
mDecorView = dialog.getWindow().getDecorView();
mDecorView.addOnAttachStateChangeListener(this);

onShow和\onStop


@Override
public void show() {
if (!isCreate) {
onCreate(null);
isCreate = true;
}
dialog.show();
if (!isStart) {
onStart();
isStart = true;
}
}


@Override
public void dismiss() {
dialog.dismiss();
if (isStart) {
onStop();
isStart = false;
}
}

从兼容代码上来看,显然没有做到Dialog那种同步,因此只适合在单一线程中使用。


总结


本篇总结了2种异显屏弹窗,总体上来说,都有一定的瑕疵,但是第一种方案显然要好的多,主要是View更新上和可扩展上,当然第二种对于非多线程且不关注严格回调的需求,也是足以应付,在实际情况中,合适的才是最重要的。


作者:时光少年
来源:juejin.cn/post/7315846805920972809
收起阅读 »

HarmonyOS 鸿蒙下载三方依赖 ohpm环境搭建

前言ohpm(One Hundred Percent Mermaid )是一个集成了Mermaid的命令工具,可以用于生成关系图、序列图、等各种图表。我们可以使用ohpm来生成漂亮且可读性强的图表。本期教大家如何搭建ophm环境:一、在DevEco Studi...
继续阅读 »

前言

ohpm(One Hundred Percent Mermaid )是一个集成了Mermaid的命令工具,可以用于生成关系图、序列图、等各种图表。我们可以使用ohpm来生成漂亮且可读性强的图表。

本期教大家如何搭建ophm环境:

一、在DevEco Studio中,将依赖放到指定的 oh-package-json5 的 dependencies 内


二、打开 Terminal 执行 :ohpm install

(1)成功会提示

(2)失败会提示
ohpm not found ! 大概意思就是找不到这个ohpm

三、解决小标题(2)的失败提示:

1、查阅我们的ohpm地址
一定要记住这个地址
Mac端找到该位置路径 点击DevEco Studio ->Preferences

2、打开终端命令行 输入:echo $SHELL 输入后 单击回车

注:如果不需要以下图文引导 请向下拉 下方有无图文引导方式


3、提示 /bin/zsh

(1)执行: vi ~/.zshrc 后点击回车


(2)进入到该页面 输入 i

(3)拷贝:export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm export PATH=${PATH}:${OHPM_HOME}/bin

中间的 xxx 输入 在标题三 1 图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可。


(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入:wq!单击回车保存


(5)输入: source ~/.zshrc;


以下是无图文引导方式:

(1)执行: vi ~/.zshrc
(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm export PATH=${PATH}:${OHPM_HOME}/bin

中间的 xxx 输入 在 标题三的(1)图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可。

(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入:wq!,单击回车保存
(5)输入: source ~/.zshrc;

4、提示/bin/base

(1)执行: vi ~/.bash_profile
(2)输入 i
(3)export OHPM_HOME=/Users/xxx/Library/Huawei/ohpm export PATH=${PATH}:${OHPM_HOME}/bin

中间的 xxx 输入 在 标题3的(1)图中 /Users 每个用户名都不一样 不要直接填xxx 按照路径给的填写即可(4)编辑完成后,单击ESC ,即可退出编辑模式,然后输入“:wq!”,单击回车保存
(5)输入: source ~/.bash_profile

四、检验 ohpm环境是否配置成功

命令行输入 export 查验是否有 ohpm

第二种检验方式 输入 ohpm -v 会显示你的版本


相关文档:

收起阅读 »

Android:实现带边框的输入框

如今市面上APP的输入框可以说是千奇百怪,不搞点花样出来貌似代表格局没打开。还在使用系统自带的输入框的兄弟可以停下脚步,哥带你实现一个简易的带边框输入框。 话不多说,直接上图: 要实现这个效果,不得不再回顾下自定义View的流程,感兴趣的童鞋可以自行网上搜...
继续阅读 »

如今市面上APP的输入框可以说是千奇百怪,不搞点花样出来貌似代表格局没打开。还在使用系统自带的输入框的兄弟可以停下脚步,哥带你实现一个简易的带边框输入框。



话不多说,直接上图:
1.gif


要实现这个效果,不得不再回顾下自定义View的流程,感兴趣的童鞋可以自行网上搜索,这里只提及该效果涉及到的内容。总体实现大致流程:



  • 继承AppCompatEditText

  • 配置可定义的资源属性

  • onDraw() 方法的重写


首先还得分析:效果图中最多只能输入6个数字,需要计算出每个文字的宽高和间隙,再分别绘制文字背景和文字本身。从中我们需要提取背景颜色、高度、边距等私有属性,通过新建attrs.xml文件进行配置:


<declare-styleable name="RoundRectEditText">
<attr name="count" format="integer"/>
<attr name="itemPading" format="dimension"/>
<attr name="strokeHight" format="dimension"/>
<attr name="strokeColor" format="color"/>/>
</declare-styleable>

这样在初始化的时候即可给你默认值:


val typedArray =context.obtainStyledAttributes(it, R.styleable.RoundRectEditText)
count = typedArray.getInt(R.styleable.RoundRectEditText_count, count)
itemPading = typedArray.getDimension(R.styleable.RoundRectEditText_itemPading,0f)
strokeHight = typedArray.getDimension(R.styleable.RoundRectEditText_strokeHight,0f)
strokeColor = typedArray.getColor(R.styleable.RoundRectEditText_strokeColor,strokeColor)
typedArray.recycle()

接下来便是重头戏,如何绘制文字和背景色。思路其实很简单,通过for循环去遍历绘制每一个数字。关键点还在于去计算每个文字的位置及宽高,只要得到了位置和宽高,绘制背景和绘制文字易如反掌。


获取每个文字宽度:


strokeWith =(width.toFloat() - paddingLeft.toFloat() - paddingRight.toFloat() - (count - 1) * itemPading) / count

文字居中需要计算出对应Y值:


val fontMetrics = paint.fontMetrics
val textHeight = fontMetrics.bottom - fontMetrics.top
val distance = textHeight / 2 - fontMetrics.bottom
val baseline = height / 2f + distance

文字的X值则根据当前index和文字宽度以及各边距得出:


private fun getIndexOfX(index: Int): Float {
return paddingLeft.toFloat() + index * (itemPading + strokeWith) + 0.5f * strokeWith
}

得到了位置,宽高接下来的步骤再简单不过了。使用drawText 绘制文字,使用drawRoundRect 绘制背景。这里有一个细节一定要注意,绘制背景一定要在绘制文字之前,否则背景会把文字给覆盖。


另外,还需要注意一点。如果onDraw方法中不注释掉超类方法,底部会多出一段输入的数字。其实很好理解,这是AppCompatEditText 自身绘制的数字,所以我们把它注释即可,包括光标也是一样。如果想要光标则需要自己在onDraw方法中绘制即可。


//隐藏自带光标
super.setCursorVisible(false)

override fun onDraw(canvas: Canvas) {
//不注释掉会显示在最底部
// super.onDraw(canvas)
......
}

以上便是实现带边框的输入框的全部类型,希望对大家有所帮助!


作者:似曾相识2022
来源:juejin.cn/post/7271056651129995322
收起阅读 »

状态🐔到底如何优雅的实现

状态机的组成 状态机是一种抽象的数学模型,描述了对象或系统在特定时间点可能处于的各种状态以及状态之间的转换规则。它由一组状态、事件、转移和动作组成,用于模拟对象在不同条件下的行为和状态变化。 状态机包括以下基本组成部分: 状态(State):表示对象或系统...
继续阅读 »

状态机的组成


状态机是一种抽象的数学模型,描述了对象或系统在特定时间点可能处于的各种状态以及状态之间的转换规则。它由一组状态、事件、转移和动作组成,用于模拟对象在不同条件下的行为和状态变化。


image-20240423094124100

状态机包括以下基本组成部分:



  • 状态(State):表示对象或系统当前的状态,例如开、关、就绪等。

  • 事件(Event):触发状态转换的动作或条件,例如按钮点击、消息到达等。

  • 转移(Transition):定义了从一个状态到另一个状态的转换规则,通常与特定事件相关联。

  • 动作(Action):在状态转换过程中执行的操作或行为,例如更新状态、记录日志等。


状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型。说白了,一般就是指一张状态转换图。例如,根据自动门的运行规则,我们可以抽象出下面这么一个图。


image-20240423095540911


简单实现


在计算机中,状态机通常用编程语言来实现。在 C、C++、Java、Python 等编程语言中,可以通过使用 switch-case 语句、if-else 语句、状态转移表等来实现状态机。在下面还有更加优雅的方式,使用 Spring 状态机 来实现。


if-else 实现状态机


在上面的示例中,我们使用 if-else 结构根据当前活动来控制音乐的播放状态,并执行相应的行为。代码如下:


public class BasketballMusicStateMachineUsingIfElse {
private boolean isPlayingMusic;

public BasketballMusicStateMachineUsingIfElse() {
this.isPlayingMusic = false; // 初始状态为音乐未播放
}

public void playMusic() {
if (!isPlayingMusic) {
System.out.println("Music starts playing...");
isPlayingMusic = true;
}
}

public void stopMusic() {
if (isPlayingMusic) {
System.out.println("Music stops playing...");
isPlayingMusic = false;
}
}

public void performActivity(String activity) {
if ("basketball".equals(activity)) {
System.out.println("Music~");
playMusic(); // 打篮球时播放音乐
} else if ("sing_rap".equals(activity)) {
System.out.println("哎哟你干嘛!");
stopMusic(); // 唱跳Rap时停止音乐
} else {
System.out.println("Invalid activity!");
}
}

public static void main(String[] args) {
BasketballMusicStateMachineUsingIfElse stateMachine = new BasketballMusicStateMachineUsingIfElse();

// 测试状态机
stateMachine.performActivity("basketball"); // 打篮球,音乐开始播放
stateMachine.performActivity("sing_rap"); // 唱跳Rap,音乐停止播放
stateMachine.performActivity("basketball"); // 再次打篮球,音乐重新开始播放
}
}

switch-case 实现状态机


在这个示例中,我们使用 switch-case 结构根据不同的活动来控制音乐的播放状态,并执行相应的行为。代码如下:


public class BasketballMusicStateMachineUsingSwitchCase {
private boolean isPlayingMusic;

public BasketballMusicStateMachineUsingSwitchCase() {
this.isPlayingMusic = false; // 初始状态为音乐未播放
}

public void playMusic() {
if (!isPlayingMusic) {
System.out.println("Music starts playing...");
isPlayingMusic = true;
}
}

public void stopMusic() {
if (isPlayingMusic) {
System.out.println("Music stops playing...");
isPlayingMusic = false;
}
}

public void performActivity(String activity) {
switch (activity) {
case "basketball":
System.out.println("Music ~");
playMusic(); // 打篮球时播放音乐
break;
case "sing_rap":
System.out.println("哎哟 你干嘛 ~");
stopMusic(); // 唱跳Rap时停止音乐
break;
default:
System.out.println("Invalid activity!");
}
}

public static void main(String[] args) {
BasketballMusicStateMachineUsingSwitchCase stateMachine = new BasketballMusicStateMachineUsingSwitchCase();

// 测试状态机
stateMachine.performActivity("basketball"); // 打篮球,音乐开始播放
stateMachine.performActivity("sing_rap"); // 唱跳Rap,音乐停止播放
stateMachine.performActivity("basketball"); // 再次打篮球,音乐重新开始播放
}
}


是不是感觉状态机其实经常在我们的日常使用中捏~,接下来带大家使用更优雅的状态机 Spring 状态机。


image-20240423100302874

使用 Spring 状态机


1)引入依赖


<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>

2)定义状态和事件的枚举


代码如下:


public enum States {
IDLE, // 空闲状态
PLAYING_BB, // 打篮球状态
SINGING // 唱跳Rap状态
}
public enum Event {
START_BB_MUSIC, // 开始播放篮球音乐事件
STOP_BB_MUSIC // 停止篮球音乐事件
}

3)配置状态机


代码如下:


@Configuration
@EnableStateMachine
public class BasketballMusicStateMachineConfig extends EnumStateMachineConfigurerAdapter<States, Event> {

@Autowired
private BasketballMusicStateMachineEventListener eventListener;

@Override
public void configure(StateMachineConfigurationConfigurer<States, Event> config) throws Exception {
config
.withConfiguration()
.autoStartup(true)
.listener(eventListener); // 设置状态机事件监听器
}

@Override
public void configure(StateMachineStateConfigurer<States, Event> states) throws Exception {
states
.withStates()
.initial(States.IDLE)
.states(EnumSet.allOf(States.class));
}

@Override
public void configure(StateMachineTransitionConfigurer<States, Event> transitions) throws Exception {
transitions
.withExternal()
.source(States.IDLE).target(States.PLAYING_BB).event(Event.START_BB_MUSIC)
.and()
.withExternal()
.source(States.PLAYING_BB).target(States.SINGING).event(Event.STOP_BB_MUSIC)
.and()
.withExternal()
.source(States.SINGING).target(States.PLAYING_BB).event(Event.START_BB_MUSIC);
}
}

4)定义状态机事件监听器


代码如下:


@Component
public class BasketballMusicStateMachineEventListener extends StateMachineListenerAdapter<States, Event> {

@Override
public void stateChanged(State<States, Event> from, State<States, Event> to) {
if (from.getId() == States.IDLE && to.getId() == States.PLAYING_BB) {
System.out.println("开始打篮球,music 起");
} else if (from.getId() == States.PLAYING_BB && to.getId() == States.SINGING) {
System.out.println("唱跳,你干嘛");
} else if (from.getId() == States.SINGING && to.getId() == States.PLAYING_BB) {
System.out.println("继续打篮球,music 继续");
}
}
}

5)编写单元测试


@SpringBootTest
class ChatApplicationTests {
@Resource
private StateMachine<States, Event> stateMachine;

@Test
void contextLoads() {
//开始打球,music 起
stateMachine.sendEvent(Event.START_BB_MUSIC);
//开始唱跳,你干嘛
stateMachine.sendEvent(Event.STOP_BB_MUSIC);
//继续打球,music 继续
stateMachine.sendEvent(Event.START_BB_MUSIC);

}
}

效果如下:


image-20240423103523546


在上面的示例中,我们定义了一个状态机,用于控制在打篮球时音乐的播放和唱跳 Rap 的行为。通过触发事件来执行状态转移,并通过事件监听器监听状态变化并执行相应的操作。


image-20240423103604502


作者:cong_
来源:juejin.cn/post/7360647839448088613
收起阅读 »

数据连接已满,导致新连接无法成功

个人项目中经常会把每个项目的平台部署成开发、测试环境,而数据库就有可能是多个平台共用一个了,现在基本上都是用的微服务架构,那么数据库连接就不够用了。 我们用的是MySQL数据库,最近遇到了这个尴尬的问题,本地修改了代码启动的时候经常会连不上数据库既然连接太多,...
继续阅读 »

个人项目中经常会把每个项目的平台部署成开发、测试环境,而数据库就有可能是多个平台共用一个了,现在基本上都是用的微服务架构,那么数据库连接就不够用了。


我们用的是MySQL数据库,最近遇到了这个尴尬的问题,本地修改了代码启动的时候经常会连不上数据库既然连接太多,要么减少连接,要么扩大最大可连接数,


经过查询,MySQL 数据库 的默认最大连接数是经常设置在151的默认值,而最大连接数可以达到16384个


对应报错信息一般为:


com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure


too many connections



  1. 查看数据库当前连接信息,可以看到连接数据库的进程id,ip,用户名,连接的数据库,连接状态,连接时长等如果发现有大量的sleep状态的连接进程,则说明该参数设置的过大,可以进行适当的调整小些。




    1. SHOW FULL processlist;




    2. 在MySQL中,使用 SHOW FULL PROCESSLIST; 命令可以显示系统中所有当前运行的线程,包括每个线程的状态、是否锁定等信息。这个命令输出的表格中包含的字段具有以下含义:



      1. Id: 线程的唯一标识符。这个ID可以用来引用特定的线程,例如,在需要终止一个特定的线程时可以使用 KILL 命令。

      2. User: 启动线程的MySQL用户。

      3. Host: 用户连接到MySQL服务器的主机名和端口号,显示格式通常是 host_name:port

      4. db: 当前线程所在的数据库。如果线程没有使用任何数据库,这一列可能显示为NULL。

      5. Command: 线程正在执行的命令类型,例如 QuerySleepConnect 等。

      6. Time: 命令执行的时间,以秒为单位。对于 Sleep 状态,这表示线程处于空闲状态的时长。

      7. State: 线程的当前状态,提供了正在执行的命令的额外信息。这可以是 Sending datasorting resultLocked 等。

      8. Info: 如果线程正在执行查询,则这一列显示具体的SQL语句。对于其他类型的命令,这一列可能为空或显示为NULL。


      Command 列显示为 Sleep 时,这意味着该线程当前没有执行任何查询,只是在连接池中等待下一个查询命令。通常,应用程序执行完一个查询后,连接可能会保持打开状态而不是立即关闭,以便可以重用该连接执行后续的查询。在这种状态下,线程不会使用服务器资源来处理任何数据,但仍占用一个连接槽。如果看到很多线程处于 Sleep 状态且持续时间较长,这可能是一个优化点,例如,通过调整应用逻辑或连接池设置来减少空闲连接的数量。






  2. 查询当前最大连接数和超时时间




    1. # 查看最大连接数
      show variables like '%max_connections%';
      # 查看非交互式超时时间 单位秒
      show variables like 'wait_timeout';
      # 查看交互式叫号时间 单位秒
      show variables like 'interactive_timeout';





      1. max_connections:



        1. max_connections 参数定义了数据库服务器能够同时接受的最大客户端连接数。当达到这个限制时,任何新的尝试连接的客户端将会收到一个错误,通常是“Too many connections”。

        2. 默认值通常基于系统的能力和配置,但经常设置在151的默认值。这个值可以根据服务器的硬件资源(如CPU和内存)和负载要求进行调整。



      2. mysqlx_max_connections:



        1. mysqlx_max_connections 参数是专门为MySQL的X协议(一种扩展的协议,支持更复杂的操作,如CRUD操作和实时通知)设定的最大连接数。X协议使得开发者能够使用NoSQL风格的接口与数据库交互。

        2. 默认值通常较小,因为X协议的使用还不如传统SQL协议普遍。这个参数允许你独立于max_connections控制通过X协议可能的连接数。



      3. wait_timeout:



        1. wait_timeout 设置的是非交互式(非控制台)客户端连接在变成非活动状态后,在被自动关闭之前等待的秒数。非交互式连接通常指的是通过网络或API等进行的数据库连接,如应用程序服务器到数据库的连接。

        2. 默认值通常较长,如8小时(28800秒),但这可以根据需要进行调整,特别是在连接数资源受限的环境中。



      4. interactive_timeout:



        1. interactive_timeout 适用于MySQL服务器与客户端进行交互式会话时的连接超时设置。交互式会话通常是指用户通过MySQL命令行客户端或类似工具直接连接并操作数据库。

        2. 这个超时值只会在MySQL服务器识别连接为交互式时应用。它的默认值也通常是8小时。








  3. 修改最大连接数和超时时间



    1. SQL直接改




      1. # 重启后失效 这里直接设置1000
        SET GLOBAL max_connections = 1000;
        # 设置全局 非交互连接 超时时间 单位秒
        SET GLOBAL wait_timeout = 300;




    2. 配置文件改



      1.     MySQL的配置文件通常是 my.cnf(在Linux系统中)或 my.ini(在Windows系统中)。你应该在 [mysqld] 部分中设置这些参数

      2.     在Linux系统上,MySQL的配置文件一般位于以下几个路径之一:

      3. /etc/my.cnf

      4. /etc/mysql/my.cnf

      5. /var/lib/mysql/my.cnf

      6.     具体位置可能会根据不同的Linux发行版和MySQL安装方式有所不同。你可以使用 find 命令来搜索这个文件,例如:


      7. sudo find / -name my.cnf




    3.   找到到文件后,将这些值修改为下列的值 这里直接设置1000


    4. [mysqld]
      max_connections = 1000

      wait_timeout = 300


    5. docker情况



      1. 使用Docker命令行参数

      2.     你可以在运行MySQL容器时通过Docker命令行直接设置配置参数,例如:


      3. docker run -d \
        -p 3306:3306 \
        --name mysql \
        -e MYSQL_ROOT_PASSWORD=my-secret-pw \
        -e MYSQL_DATABASE=mydatabase \
        mysql:tag --max-connections=1000 --wait-timeout=300


      4.     在这个例子中,--max-connections=1000 是作为命令行参数传递给MySQL服务器的。

      5. 修改配置文件并挂载

      6.     如果你需要修改多个配置项或者希望使用配置文件来管理设置,可以创建一个自定义的 my.cnf 文件,然后在启动容器时将它挂载到容器中适当的位置。例如:


      7. docker run -d \
        -p 3306:3306 \
        --name mysql \
        -e MYSQL_ROOT_PASSWORD=my-secret-pw \
        -v /path/to/your/custom/my.cnf:/etc/mysql/my.cnf \
        mysql:tag






  4. 修改完后再查一遍看看有没有改成功




    1. # 查看最大连接数
      show variables like '%max_connections%';
      # 查看非交互式超时时间
      SHOW GLOBAL VARIABLES LIKE 'wait_timeout';




  5. 拓展



    1. wait_timeout 变量分为全局级别和会话级别



      •     执行 SET GLOBAL wait_timeout = 300;后,

      •     使用执行 show variables like 'wait_timeout'; 发现并没有改变

      •     是因为在MySQL中,当你执行 SET GLOBAL wait_timeout = 300; 这条命令时,理论上应该是会设置全局的 wait_timeout 值为300秒。在查询 wait_timeout 时,没有指定是查询全局变量,可能会返回会话级的值。会话级的 wait_timeout 并没有被改变。尝试使用以下命令来查看全局设置:


      • # 查看全局变量
        SHOW GLOBAL VARIABLES LIKE 'wait_timeout';
        # 与之对应的,查看会话级别的变量可以使用:
        SHOW SESSION VARIABLES LIKE 'wait_timeout';




    2. 全局变量和会话变量的区别



      • 全局变量:



        1. 全局变量对服务器上所有当前会话和未来会话都有效。

        2. 当你设置一个全局变量时,它的值会影响所有新建的连接。然而,对于已经存在的连接,全局变量的更改通常不会影响它们,除非这些连接被重新启动或者明确地重新读取全局设置。

        3. 通过 SET GLOBAL 命令修改全局变量,或者在服务器的配置文件中设置并重新启动服务器。



      • 会话变量:



        1. 会话变量只对当前连接的会话有效,并且当会话结束时,会话变量的设置就会失效。

        2. 修改会话变量的命令是 SET SESSION 或者简单的 SET,它不会影响其他会话或连接。

        3. 每个新的会话都会从当前的全局设置中继承变量的值,但在会话中对这些变量的修改不会影响到其他会话。



      • 关于是否改变全局变量,这取决于你试图解决的具体问题:



        • 如果你需要修改的设置应该对所有新的连接生效,例如,修改 wait_timeout 来减少空闲连接超时,那么修改全局变量是合适的。这样,所有新建立的连接都会采用新的超时设置。

        • 然而,如果你需要立即影响当前活动的会话,你必须在每个会话中单独设置会话变量。这在某些操作中可能是必需的,比如调整当前事务的隔离级别或者调试中动态改变某些性能调优参数。

        • 因此,如果改变是为了长期或持久的配置调整,修改全局变量通常是正确的做法。但如果需要对当前会话立即生效的改变,应该使用会话变量。








作者:不惊夜
来源:juejin.cn/post/7361056871673446437
收起阅读 »

基于EdgeEffect实现RecyclerView列表阻尼滑动效果

探索EdgeEffect的花样玩法 1、EdgeEffect是什么 当用户在一个可滑动的控件内(如RecyclerView),滑动内容已经超过了内容边界时,RecyclerView通过EdgeEffect绘制一个边界图形来提醒用户,滑动已经到边界了,不要再滑动...
继续阅读 »

探索EdgeEffect的花样玩法


1、EdgeEffect是什么


当用户在一个可滑动的控件内(如RecyclerView),滑动内容已经超过了内容边界时,RecyclerView通过EdgeEffect绘制一个边界图形来提醒用户,滑动已经到边界了,不要再滑动啦。


简言之:就是通过边界图形来提醒用户,没啥内容了,别滑了。


2、EdgeEffect在RecyclerView的现象是什么


1、到达边界后的阴影效果


在RecyclerView列表中,滑动到边界还继续滑动或者快速滑动到边界,则现象如下图中的到达边界后产生的阴影效果。


滑动到边界阴影效果

2、如何去掉阴影效果


在布局中,可以设置overScrollMode的属性值为never即可。


或者在代码中设置,即可取消


recyclerView?.overScrollMode = View.OVER_SCROLL_NEVER

3、EdgeEffect在RecyclerView的实现原理是什么


1、onMove事件对应EdgeEffect的onPull


EdgeEffect在RecyclerView中大致流程可以参考下面这个图,以onMove事件举例


EdgeEffect与RecyclerView交互图

通过上面这个图,并结合下面的源码,就能对这个流程有个大致的理解。


@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
...
// (1) move事件
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e, TYPE_TOUCH)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
...
}
}
break;
}
}


boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
...
// (2)判断是否设置了过度滑动,所以通过布局设置overScrollMode的属性值为never就走不进了分支逻辑中了
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
...

if (!awakenScrollBars()) {
// 刷新当前界面
invalidate();
}
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}

private void pullGlows(float x, float overscrollX, float y, float overscrollY) {
boolean invalidate = false;
...
// 顶部边界
if (overscrollY < 0) {
// 构建顶部边界的EdgeEffect对象
ensureTopGlow();
// 调用EdgeEffect的onPull方法 设置些属性
EdgeEffectCompat.onPull(mTopGlow, -overscrollY / getHeight(), x / getWidth());
invalidate = true;
}
...

if (invalidate || overscrollX != 0 || overscrollY != 0) {
// 刷新界面
ViewCompat.postInvalidateOnAnimation(this);
}
}

void ensureTopGlow() {
...
mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
// 设置边界图形的大小
if (mClipToPadding) {
mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
} else {
mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight());
}

}

// RecyclerView的绘制
@Override
public void draw(Canvas c) {
super.draw(c);
...
if (mTopGlow != null && !mTopGlow.isFinished()) {
final int restore = c.save();
if (mClipToPadding) {
c.translate(getPaddingLeft(), getPaddingTop());
}
// 调用 EdgeEffect的draw方法
needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
c.restoreToCount(restore);
}
...
}

// EdgeEffect的draw方法
public boolean draw(Canvas canvas) {
...
update();
final int count = canvas.save();
final float centerX = mBounds.centerX();
final float centerY = mBounds.height() - mRadius;

canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0);

final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f;
float translateX = mBounds.width() * displacement / 2;

canvas.clipRect(mBounds);
canvas.translate(translateX, 0);
mPaint.setAlpha((int) (0xff * mGlowAlpha));
// 绘制扇弧
canvas.drawCircle(centerX, centerY, mRadius, mPaint);
canvas.restoreToCount(count);
...

同理:RecyclerView的 up 及Cancel事件对应调用EdgeEffect的onRelease;fling过度滑动对应EdgeEffect的onAbsorb方法


2、EdgeEffect的onPull、onRelease、onAbsorb方法


(1)onPull


对于RecyclerView列表而言,内容已经在顶部到达边界了,此时用户仍向下滑动时,会调用onPull方法及后续流畅,来更新当前视图,提示用户已经到边界了。


(2)onRelease


对于(1)的情况,用户松开了,不向下滑动了,此时释放拉动的距离,并刷新界面消失当前的图形界面。


(3)onAbsorb


用户过度滑动时,RecyclerView调用Fling方法,把内容到达边界后消耗不掉的距离传递给onAbsorb方法,让其显示图形界面提示用户已到达内容边界。


4、使用EdgeEffect在RecyclerView中实现列表阻尼滑动等效果


(1)先看下效果


EdgeEffect的录屏

上述gif图中展示了两个效果:RecyclerView的阻尼下拉 及 复位,这就是使用上面的EdgeEffect的三个方法可以实现。


上述的gif图中,使用MultiTypeAdapter实现RecyclerView的多类型页面(ViewModel、json数据源),可以参考这篇文章快速写个RecyclerView的多类型页面


下面主要展示如何构建一个EdgeEffect,充分地使用onPull、onRelease及onAbsorb能力


(2)代码示意


// 构建一个自定义的EdgeEffectFactory 并设置给RecyclerView
recyclerView?.edgeEffectFactory = SpringEdgeEffect()

// SpringEdgeEffect
class SpringEdgeEffect : RecyclerView\.EdgeEffectFactory() {

override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect {

return object : EdgeEffect(recyclerView.context) {

override fun onPull(deltaDistance: Float) {
super.onPull(deltaDistance)
handlePull(deltaDistance)
}

override fun onPull(deltaDistance: Float, displacement: Float) {
super.onPull(deltaDistance, displacement)
handlePull(deltaDistance)
}

private fun handlePull(deltaDistance: Float) {
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
val translationYDelta =
sign * recyclerView.width * deltaDistance * 0.8f
Log.d("qlli1234-pull", "deltDistance: " + translationYDelta)
recyclerView.forEach {
if (it.isVisible) {
// 设置每个RecyclerView的子item的translationY属性
recyclerView.getChildViewHolder(it).itemView.translationY += translationYDelta
}
}
}

override fun onRelease() {
super.onRelease()
Log.d("qlli1234-onRelease", "onRelease")
recyclerView.forEach {
//复位
val animator = ValueAnimator.ofFloat(recyclerView.getChildViewHolder(it).itemView.translationY, 0f).setDuration(500)
animator.interpolator = DecelerateInterpolator(2.0f)
animator.addUpdateListener { valueAnimator ->
recyclerView.getChildViewHolder(it).itemView.translationY = valueAnimator.animatedValue as Float
}
animator.start()
}
}

override fun onAbsorb(velocity: Int) {
super.onAbsorb(velocity)
val sign = if (direction == DIRECTION_BOTTOM) -1 else 1
Log.d("qlli1234-onAbsorb", "onAbsorb")
val translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE
recyclerView.forEach {
if (it.isVisible) {
// 在这个可以做动画
}
}
}

override fun draw(canvas: Canvas?): Boolean {
// 设置大小之后,就不会有绘画阴影效果
setSize(0, 0)
val result = super.draw(canvas)
return result
}
}
}

这里有一个小细节,如何在使用onPull等方法时,去掉绘制的阴影部分:其实,可以重写draw方法,重置大小为0即可,如上述代码中的这一小块内容:


override fun draw(canvas: Canvas?): Boolean {
// 设置大小之后,就不会有绘画阴影效果
setSize(0, 0)
val result = super.draw(canvas)
return result
}

5、参考


1、google的motion示例中的ChessAdapter内容


2、仿QQ的recyclerview效果实现


作者:李暖光
来源:juejin.cn/post/7235463575300046903
收起阅读 »

底部弹出菜单原来这么简单

底部弹出菜单是什么 底部弹出菜单,即从app界面底部弹出的一个菜单列表,这种UI形式被众多app所采用,是一种主流的布局方式。 思路分析 我们先分析一下,这样一种UI应该由哪些布局组成?首先在原界面上以一小块区域显示界面的这种形式,很明显就是对话框Dial...
继续阅读 »

底部弹出菜单是什么


底部弹出菜单,即从app界面底部弹出的一个菜单列表,这种UI形式被众多app所采用,是一种主流的布局方式。


截屏2023-09-28 14.36.51.png


截屏2023-09-28 14.37.29.png


思路分析


我们先分析一下,这样一种UI应该由哪些布局组成?首先在原界面上以一小块区域显示界面的这种形式,很明显就是对话框Dialog做的事情吧!最底部是一个取消菜单,上面的功能菜单可以是一个,也可以是两个、三个甚至更多。所以,我们可以使用RecyclerView实现。需要注意一点的是,最上面那个菜单的样式稍微有点不一样,因为它上面是圆滑的,有圆角,这样的界面显示更加和谐。我们主要考虑的就是弹出对话框的动画样式,另外注意一点就是可以多支持几个语种,让框架更加专业,这里只需要翻译“取消”文字。


开始看代码


package dora.widget

import android.app.Activity
import android.app.Dialog
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.listener.OnItemChildClickListener
import dora.widget.bean.BottomMenu
import dora.widget.bottomdialog.R

class DoraBottomMenuDialog : View.OnClickListener, OnItemChildClickListener {

private var bottomDialog: Dialog? = null
private var listener: OnMenuClickListener? = null

interface OnMenuClickListener {
fun onMenuClick(position: Int, menu: String)
}

fun setOnMenuClickListener(listener: OnMenuClickListener) : DoraBottomMenuDialog {
this.listener = listener
return this
}

fun show(activity: Activity, menus: Array<String>): DoraBottomMenuDialog {
if (bottomDialog == null && !activity.isFinishing) {
bottomDialog = Dialog(activity, R.style.DoraView_AlertDialog)
val contentView =
LayoutInflater.from(activity).inflate(R.layout.dview_dialog_content, null)
initView(contentView, menus)
bottomDialog!!.setContentView(contentView)
bottomDialog!!.setCanceledOnTouchOutside(true)
bottomDialog!!.setCancelable(true)
bottomDialog!!.window!!.setGravity(Gravity.BOTTOM)
bottomDialog!!.window!!.setWindowAnimations(R.style.DoraView_BottomDialog_Animation)
bottomDialog!!.show()
val window = bottomDialog!!.window
window!!.decorView.setPadding(0, 0, 0, 0)
val lp = window.attributes
lp.width = WindowManager.LayoutParams.MATCH_PARENT
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
window.attributes = lp
} else {
bottomDialog!!.show()
}
return this
}

private fun initView(contentView: View, menus: Array<String>) {
val recyclerView = contentView.findViewById<RecyclerView>(R.id.dview_recycler_view)
val adapter = MenuAdapter()
val list = mutableListOf<BottomMenu>()
menus.forEachIndexed { index, s ->
when (index) {
0 -> {
list.add(BottomMenu(s, BottomMenu.TOP_MENU))
}
else -> {
list.add(BottomMenu(s, BottomMenu.NORMAL_MENU))
}
}
}
adapter.setList(list)
recyclerView.adapter = adapter
val decoration = DividerItemDecoration(contentView.context, DividerItemDecoration.VERTICAL)
recyclerView.addItemDecoration(decoration)
adapter.addChildClickViewIds(R.id.tv_menu)
adapter.setOnItemChildClickListener(this)
val tvCancel = contentView.findViewById<TextView>(R.id.tv_cancel)
tvCancel.setOnClickListener(this)
}

private fun dismiss() {
bottomDialog?.dismiss()
bottomDialog = null
}

override fun onClick(v: View) {
when (v.id) {
R.id.tv_cancel -> dismiss()
}
}

override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
listener?.onMenuClick(position, adapter.getItem(position) as String)
dismiss()
}
}

类的结构不仅可以继承,还可以使用聚合和组合的方式,我们这里就不直接继承Dialog了,使用一种更接近代理的一种方式。条条大路通罗马,能抓到老鼠的就是好猫。这里的设计是通过调用show方法,传入一个菜单列表的数组来显示菜单,调用dismiss方法来关闭菜单。最后添加一个菜单点击的事件,把点击item的内容和位置暴露给调用方。


package dora.widget

import com.chad.library.adapter.base.BaseMultiItemQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import dora.widget.bean.BottomMenu
import dora.widget.bottomdialog.R

class MenuAdapter : BaseMultiItemQuickAdapter<BottomMenu, BaseViewHolder>() {

init {
addItemType(BottomMenu.NORMAL_MENU, R.layout.dview_item_menu)
addItemType(BottomMenu.TOP_MENU, R.layout.dview_item_menu_top)
}

override fun convert(holder: BaseViewHolder, item: BottomMenu) {
holder.setText(R.id.tv_menu, item.menu)
}
}

多类型的列表布局我们采用BRVAH,


implementation("io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.10")

来区分有圆角和没圆角的item条目。


<?xml version="1.0" encoding="utf-8"?>

<resources>
<style name="DoraView.AlertDialog" parent="@android:style/Theme.Dialog">
<!-- 是否启用标题栏 -->
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>

<!-- 是否使用背景半透明 -->
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
</style>

<style name="DoraView.BottomDialog.Animation" parent="Animation.AppCompat.Dialog">
<item name="android:windowEnterAnimation">@anim/translate_dialog_in</item>
<item name="android:windowExitAnimation">@anim/translate_dialog_out</item>
</style>
</resources>

以上是对话框的样式。我们再来看一下进入和退出对话框的动画。


translate_dialog_in.xml


<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:duration="300"
android:fromXDelta="0"
android:fromYDelta="100%"
android:toXDelta="0"
android:toYDelta="0">

</translate>

translate_dialog_out.xml


<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:duration="300"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="100%">

</translate>

最后给你们证明一下我是做了语言国际化的。
截屏2023-09-28 15.08.20.png


使用方式


// 打开底部弹窗
val dialog = DoraBottomMenuDialog()
dialog.setOnMenuClickListener(object : DoraBottomMenuDialog.OnMenuClickListener {
override fun onMenuClick(position: Int, menu: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
startActivity(intent)
}
})
dialog.show(this, arrayOf("外部浏览器打开"))

开源项目


github.com/dora4/dview…


作者:dora
来源:juejin.cn/post/7283516197487214611
收起阅读 »

Android:LayoutAnimation的神奇效果

大家好,我是时曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。 今天给大家讲讲酷炫的动画成员——LayoutAnimation。话不多说,直接上一个简单的效果图: 怎么样,和往常自己写的没有动画效果的页面比起来是不是更加酷炫。效果图只展示了从右到...
继续阅读 »

大家好,我是时曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。


今天给大家讲讲酷炫的动画成员——LayoutAnimation。话不多说,直接上一个简单的效果图:


Screenrecorder-2023-09-10-10-29-52-627.gif


怎么样,和往常自己写的没有动画效果的页面比起来是不是更加酷炫。效果图只展示了从右到左叠加渐变的效果,只要脑洞够大,LayoutAnimation是可以帮你实现各类动画的。接下来就让我们看看LayoutAnimation如何实现这样的效果。


首先,新建一个XML动画文件slide_from_right.xml:


<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="600">
<translate
android:fromXDelta="100%p"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0" />

<alpha
android:fromAlpha="0.5"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toAlpha="1" />
</set>

set标签下由translate(移动)和alpha(渐变)动画组成。


其中translate(移动)动画由100%p移动到0。这里需要注意使用的是100%p,其中加这个p是指按父容器的宽度进行百分比计算。插值器就根据自己想要的效果设置,这里使用了一个decelerate_interpolator(减速)插值器。


第二个动画是alpha(渐变)动画,由半透明到不透明,其中插值器是先加速后减速的效果。


接着我们还需要创建一个layoutAnimation,其实也是一个XML文件layout_slid_from_right.xml:


<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/slide_from_right"
android:animationOrder="normal"
android:delay="15%"/>

其中animation指定的就是我们创建的第一个xml文件。animationOrder是指动画执行的顺序模式,包含normal, reverse 和random。normal就是从上到下依次进行,reverse根据名字就知道是反序,random那当然是随机了,我们就使用mormal即可。delay则是每个子视图执行动画的延迟比例,这里需要注意的是这是相对于上个子视图执行动画延时比例。


最后我们只需要在咱们的ViewGr0up中设置layoutAnimation属性即可:


android:layoutAnimation="@anim/layout_slid_from_right"

当然也可在代码中手动设置:


val lin = findViewById<LinearLayout>(R.id.linParent)
val resId = R.anim.layout_slid_from_right
lin.layoutAnimation = AnimationUtils.loadLayoutAnimation(lin.context, resId)

总结:



  • layoutAnimation可以使用在任何一个ViewGr0up上

  • 在使用set标签做动画叠加的时候一定要注意,set标签内需要添加duration属性,也就是动画时间。如果不加动画是没有效果的。

  • 使用移动动画时,在百分比后面添加p的意思是基于父容器宽度进行百分比计算


以上便是LayoutAnimation的简单使用,只要你脑洞大开,各种各样的效果都能玩出来。实现起来也很简单,赶紧在项目中使用起来吧。


作者:似曾相识2022
来源:juejin.cn/post/7276630249547513895
收起阅读 »

和后端吵架后,我写了个库,让整个前端团队更加规范!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 本文源码地址:github.com/sanxin-lin/… 背景 在平时的开发中,表格数据->(增加/编辑/查看)行->(增加/编辑)提交,这...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~



本文源码地址:github.com/sanxin-lin/…



背景


在平时的开发中,表格数据->(增加/编辑/查看)行->(增加/编辑)提交,这是很常见且简单的业务,但是就是这些业务,我也发现一些问题



首先我们来理性一下这些业务的逻辑



  • 第一步:请求回表格的数据

  • 第二步:点开(增加/编辑/查看)弹窗,如果是(编辑/查看),则需要将表格行的数据传到弹窗中回显

  • 第三部:如果是(编辑)弹窗,则需要把表单数据提交请求接口


我用一个图来概括大概就是:



问题所在


我不知道其他公司怎么样,但是就拿我自身来举例子,公司的后端跟前端的命名规则是不同的



  • 后端命名: 请求方法+字段类型+字段含义+下划线命名(比如 in_name、os_user_id)

  • 前端命名: 字段含义+驼峰命名(比如 name、userId)


回到刚刚的业务逻辑,还是那张图,假如我们前端不去管命名的话,那么数据的传输是这样的,发现了很多人都懒得去转换后端返回的字段名,直接拿着后端的字段名去当做前端的表单字段名,但这是不符合前端规范的



理想应该是表单要用前端的命名,比如这样



但是很多前端就是懒得去转换,原因有多个:



  • 开发者自身比较懒,或者没有规范意识

  • 回显时要转一次,提交时还要再转一次,每次总是得写一遍


解决方案


所以能不能写一个工具,解放开发者的压力又能达到期望的效果呢?比如我开发一个工具,然后像下面这样在弹窗里用



  • state: 响应式表单数据,可以用在弹窗表单中

  • resetState: 重置表单

  • inputState: 将表格行数据转成表单数据

  • outputState: 将表单数据转成提交请求的数据


配置的含义如下:



  • default: 表单字段默认值

  • input: 转入的字段名

  • output: 转出的字段名

  • inputStrategy: 转入的转换策略,可以选择内置的,也可以自定义策略函数

  • outputStrategy: 转出的转换策略,可以选择内置的,也可以自定义策略函数



转入和转出策略,内置了一些,你也可以自定义,内置的有如下



下面是自定义策略函数的例子,必须要在策略函数中返回一个转换值



这样的话,当我们执行对应的转换函数之后,会得到我们想要的结果



use-dsp


所以我开发了一个工具



源码地址:github.com/sanxin-lin/…



其实 dsp 意思就是



  • data

  • state

  • parameter


npm i use-dsp
yarn i use-dsp
pnpm i use-dsp

import useDSP from 'use-dsp'

为啥不从一开始就转?


有人会问,为啥不从一开始请求表格数据回来的时候,就把数据转成前端的命名规范?


其实这个问题我也想过,但是设想一下,有一些表格如果只是单纯做展示作用,那么就没必要去转字段名了,毕竟不涉及任何的数据传递。


但是需要编辑或者查看弹窗的表格,就涉及到了行数据的传递,那么就需要转字段名




作者:Sunshine_Lin
来源:juejin.cn/post/7360892717545799689
收起阅读 »

特斯拉违约24届全部应届生,如何评价?

4月23日有消息称:特斯拉(上海)撤回了所有24应届生的offer,统一给了一个月底薪作为补偿。 目前这个话题的讨论热度在脉脉上排第二。 先不管是真是假,至少看到有一部分同学确实是被裁了,这个时间点被裁,基本是废了。金三银四都差不多过完了,虽说还是应届生身...
继续阅读 »

4月23日有消息称:特斯拉(上海)撤回了所有24应届生的offer,统一给了一个月底薪作为补偿。



目前这个话题的讨论热度在脉脉上排第二。


脉脉热榜


先不管是真是假,至少看到有一部分同学确实是被裁了,这个时间点被裁,基本是废了。金三银四都差不多过完了,虽说还是应届生身份,但马上就要毕业了,这个时间点工作确实不好找了。


4月15日马斯克宣布特斯拉裁员10%,按照特斯拉去年公布的数据显示,特斯拉在全球共有14万名员工,算下来得裁1.4万人呐。之前出裁员消息的时候就上过一波热搜,没想到这事还没结束,现在又把所有应届生都毁约了,特斯拉这波是有点狠的。




注:信息来源于网络,官方目前并未公开发出毁约所有24应届生的消息。



看看网友怎么说


网友一


思路打开点,反正工作找不到了,考个研缓三年,还是应届生[狗头]。



网友二


能进特斯拉的肯定都是大佬级别,趁机宣传一波公司招人,这确实是个好时机。



网友三


至少赔钱了,一般的企业你看理你吗?(确实)



网友四


看了下特斯拉股票,“广进计划” 诚不欺我😅。



我怎么看


裁员我是可以理解的,哪家公司都会有亏损的时候,进行一波裁员也很正常,谁又能保证公司能一帆风顺向上发展?


但是集体毁约应届生这个事,我是不太能理解的。每年亏损的公司这么多,集体毁约应届生的一般也就两三家(老员工也可以裁呀)。既然集体毁约,是否能够根据当前的就业环境,拿出足够的诚意?如果你是十月十一月集体毁约,那我觉得赔一个月工资差不多了。但是在四月底进行毁约,我觉得得赔3个月才够诚意。


当然啦,这些只是我站在道德制高点的一些看法,真要说起来,特斯拉这个毁约条件,比起大多数企业确实还是算好的了,就像网友说的 “赔钱了良心,国内都不理你”。


最终倒霉的还是这些拿了特斯拉offer开开心心准备去上班的同学了,用“天崩开局”来形容都不为过。



你怎么看


欢迎在评论区留言,发表的你看法。


作者:阿杆
来源:juejin.cn/post/7361023886852849718
收起阅读 »

JavaScript精粹:26个关键字深度解析,编写高质量代码的秘诀!

JavaScript关键字是一种特殊的标识符,它们在语言中有固定的含义,不能用作变量名或函数名。这些关键字是JavaScript的基础,理解它们是掌握JavaScript的关键。今天,我们将一起探索JavaScript中的26个关键字,了解这些关键字各自独特的...
继续阅读 »

JavaScript关键字是一种特殊的标识符,它们在语言中有固定的含义,不能用作变量名或函数名。这些关键字是JavaScript的基础,理解它们是掌握JavaScript的关键。

今天,我们将一起探索JavaScript中的26个关键字,了解这些关键字各自独特的含义、特性和使用方法。

一、JavaScript关键字是什么

Javascript关键字(Keyword)是指在Javascript语言中有特定含义,成为Javascript语法中一部分的那些字,是 JavaScript 语言内部使用的一组名字(或称为命令)。

Description


Javascript关键字是不能作为变量名和函数名使用的。使用Javascript关键字作为变量名或函数名,会使Javascript在载入过程中出现编译错误。

Java中的关键字可用于表示控制语句的开始或结束,或者用于执行特定操作等。按照规则,关键字也是语言保留的,不能用作标识符。

下面我们来详细介绍一下JavaScript关键字的作用和使用方法。

二、JavaScript的26个关键字

JavaScript是一种广泛使用的编程语言,它具有丰富的关键字,这些关键字在JavaScript语言中发挥着重要的作用,JavaScript一共提供了26个关键字:


break, case, catch, continue, debugger, default, delete, do, else, finally, for, function, if, in, instanceof, new, return, switch, this, throw, try, typeof, var, void, while, with
其中,debugger在ECMAScript 5 新增的。

1、break:跳出 循环

break用于跳出循环结构。循环结构是一种重复执行某个代码块的结构,break关键字可以用于循环结构中的条件语句中,用于跳出循环。例如:

for (var i = 0; i < 10; i++) {
if (i == 5) {
break; // 当i等于5时跳出循环
}
console.log(i);
}

2、case:捕捉

它用于在switch语句中定义一个分支。switch语句是一种根据表达式的值执行不同代码块的结构,case关键字可以用于switch语句中,用于定义不同的分支。例如:

switch (n) {

case 1:
console.log('n等于1');
break;
case 2:
console.log('n等于2');
break;
default:
console.log('n不等于1或2');
break;
}

3、catch:配合try进行错误判断

catch用于捕获异常。异常是一种程序运行时出现的错误,catch关键字可以用于try-catch语句中,用于捕获并处理异常。例如:

try {
// 代码
} catch (e) {
console.log('发生异常:' + e.message);
}

4、continue:继续

continue用于跳过当前循环中的某个迭代。循环结构是一种重复执行某个代码块的结构,continue关键字可以用于循环结构中的条件语句中,用于跳过当前迭代。例如:

for (var i = 0; i < 10; i++) {
if (i == 5) {
continue; // 当i等于5时跳过当前迭代
}
console.log(i);
}

5、debugger:设置断点

它用于在代码中设置断点,方便调试代码。调试是一种在代码运行时发现和解决问题的过程,debugger关键字可以用于代码中,用于设置断点。例如:

function foo() {

var x = 10;

debugger; // 在这里设置断点
console.log(x);

}

6、default:配合switch,当条件不存在时使用该项

default用于在switch语句中定义一个默认分支。switch语句是一种根据表达式的值执行不同代码块的结构,default关键字可以用于switch语句中,用于定义默认分支。例如:


switch (n) {
case 1:
console.log('n等于1');
break;
case 2:
console.log('n等于2');
break;
default:
console.log('n不等于1或2');
break;
}

7、delete:删除了一个属性

delete用于删除对象的属性或数组中的元素。对象是JavaScript中的一种数据类型,它由一组属性组成,delete关键字可以用于对象的属性中,用于删除属性。例如:

var obj = {a: 1, b: 2, c: 3};
delete obj.b; // 删除对象obj的属性b
console.log(obj); // 输出{a: 1, c: 3}

8、do:声明一个循环

do用于定义一个do-while循环结构。循环结构是一种重复执行某个代码块的结构,do关键字可以用于do-while循环中,用于定义循环体。例如:

var i = 0;
do {
console.log(i);
i++;
} while (i < 10);

9、else:否则//配合if条件判断,用于条件选择的跳转

else用于在if语句中定义一个分支。if语句是一种根据条件执行不同代码块的结构,else关键字可以用于if语句中,用于定义另一个分支。例如:

if (n == 1) {
console.log('n等于1');
} else {
console.log('n不等于1');
}

10、finally:预防出现异常时用的

finally用于定义一个try-catch-finally语句中的finally块。try-catch-finally语句是一种用于处理异常的结构,finally关键字可以用于finally块中,用于定义一些必须执行的代码。例如:

try {
// 可能会抛出异常的代码
} catch (e) {
// 处理异常的代码
} finally {
// 必须执行的代码
}

11、for:循环语句

for用于定义一个for循环结构。循环结构是一种重复执行某个代码块的结构,for关键字可以用于for循环中,用于定义循环条件。例如:

for (var i = 0; i < 10; i++) {
console.log(i);
}

12、function:定义函数的关键字

function用于定义一个函数。函数是一种封装了一段代码的结构,它可以接受参数并返回结果。function关键字可以用于函数定义中,用于定义函数名和参数列表。例如:

function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 输出3

13、if:定义一个if语句

if用于定义一个if语句。if语句是一种根据条件执行不同代码块的结构,if关键字可以用于if语句中,用于定义条件。例如:


if (n == 1) {
console.log('n等于1');
} else {
console.log('n不等于1');
}

14、in:判断某个属性属于某个对象

in用于判断一个对象是否包含某个属性。对象是JavaScript中的一种数据类型,它由一组属性组成,in关键字可以用于对象中,用于判断对象是否包含某个属性。例如:


var obj = {a: 1, b: 2, c: 3};
if ('a' in obj) {
console.log('obj包含属性a');
} else {
console.log('obj不包含属性a');
}

15、instanceof:某个对象是不是另一个对象的实例

instanceof用于判断一个对象是否是某个类的实例。类是JavaScript中的一种数据类型,它由一组属性和方法组成,instanceof关键字可以用于类中,用于判断对象是否是该类的实例。例如:

function Person(name) {
this.name = name;
}
var p = new Person('张三');
if (p instanceof Person) {
console.log('p是Person类的实例');
} else {
console.log('p不是Person类的实例');
}

16、new:创建一个新对象

new用于创建一个对象。对象是JavaScript中的一种数据类型,它由一组属性和方法组成,new关键字可以用于类中,用于创建该类的实例。例如:

function Person(name) {
this.name = name;
}
var p = new Person('张三');
console.log(p.name); // 输出张三

17、return:返回

return用于从函数中返回一个值。函数是JavaScript中的一种数据类型,它由一段代码块组成,return关键字可以用于函数中,用于返回函数的执行结果。例如:

function add(a, b) {
return a + b;
}

console.log(add(1, 2)); // 输出3

18、switch:弥补if的多重判断语句

switch用于根据不同的条件执行不同的代码块。switch语句是一种根据条件执行不同代码块的结构,switch关键字可以用于switch语句中,用于定义条件。例如:

var day = 3;
switch (day) {
case 1:
console.log('星期一');
break;
case 2:
console.log('星期二');
break;
case 3:
console.log('星期三');
break;
default:
console.log('不是星期一、二、三');
}

19、this:总是指向调用该方法的对象

this用于引用当前对象。对象是JavaScript中的一种数据类型,它由一组属性和方法组成,this关键字可以用于对象中,用于引用当前对象的属性和方法。例如:

var obj = {
name: '张三',
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // 输出张三

20、throw:抛出异常

throw用于抛出一个异常。异常是JavaScript中的一种错误类型,它可以用于在程序运行过程中发现错误并停止程序的执行。throw关键字可以用于函数中,用于抛出异常。例如:

function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0');

}
return a / b;
}
console.log(divide(10, 0)); // 抛出异常

21、try:接受异常并做出判断

try用于捕获异常。异常是JavaScript中的一种错误类型,它可以用于在程序运行过程中发现错误并停止程序的执行。try语句是一种捕获异常的结构,try关键字可以用于try语句中,用于捕获异常。例如:

function divide(a, b) {
if (b === 0) {
throw new Error('除数不能为0');
}
return a / b;
}
try {
console.log(divide(10, 0)); // 抛出异常
} catch (e) {
console.log(e.message); // 输出除数不能为0
}

22、typeof:检测变量的数据类型

typeof用于获取变量的类型。变量是JavaScript中的一种数据类型,它可以是数字、字符串、布尔值等。typeof关键字可以用于变量中,用于获取变量的类型。例如:

var a = 10;
console.log(typeof a); // 输出number

23、var:声明变量

var用于声明变量。变量是JavaScript中的一种数据类型,它可以用于存储数据。var关键字可以用于变量中,用于声明变量。例如:

var a = 10;
console.log(a); // 输出10

24、void:空/ 声明没有返回值

void它用于执行一个表达式并返回undefined。undefined是JavaScript中的一种特殊值,它表示一个未定义的值。void关键字可以用于表达式中,用于执行表达式并返回undefined。例如:

function doSomething() {
console.log('执行了doSomething函数');
}
var result = void doSomething();
console.log(result); // 输出undefined

25、while

while用于创建一个循环结构。循环是JavaScript中的一种控制结构,它可以用于重复执行一段代码。while关键字可以用于循环中,用于创建一个基于条件的循环。例如:

var i = 0;
while (i < 10) {
console.log(i);
i++;
}

26、with

with用于创建一个作用域。作用域是JavaScript中的一种机制,它可以用于控制变量的作用范围。with关键字可以用于代码块中,用于创建一个作用域。例如:


var obj = {
name: '张三',
age: 20
};
with (obj) {
console.log(name); // 输出张三
console.log(age); // 输出20
}


三、JS关键字注意事项

在开发过程中使用关键字我们需要注意以下几点:

  • 区分大小写: JavaScript是区分大小写的,因此关键字的大小写必须正确。

  • 不能用作变量名: 关键字不能被用作变量名,函数名等等,会出现问题

  • 不需要硬记关键字: 关键字不用去硬记,我们在编写代码时根据系统的提示去规避就可以了

  • 保留字: JavaScript有一些保留字,不能用作变量名、函数名或属性名。

  • 不要使用全局变量: 尽量避免使用全局变量,以免与其他脚本或库发生冲突。可以使用立即执行函数表达式(IIFE)或模块模式来避免全局变量污染。

  • 使用严格模式: 在代码中添加"use strict"指令,以启用严格模式。这将有助于避免一些常见的错误,例如未声明的变量、隐式类型转换等。

  • 避免使用eval()函数: eval()函数用于动态执行字符串中的JavaScript代码,但可能导致性能问题和安全风险。尽量避免使用eval(),寻找其他替代方案。

  • 不要使用with语句: with语句会改变代码的作用域链,可能导致意外的错误。尽量避免使用with语句,改用局部变量或对象属性访问。

  • 避免使用重复的标识符: 确保变量名、函数名和属性名在同一作用域内是唯一的,以避免命名冲突。

  • 遵循编码规范: 遵循一致的命名约定、缩进风格和代码结构,以提高代码的可读性和可维护性。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


四、关于保留字的了解

除了关键字还有个“保留字”的概念,所谓保留字,实际上就是预留的“关键字”。意思是现在虽然还不是关键字,但是未来可能会成为关键字,同样不能使用它们当充当变量名、函数名等标识符。

下面是JavaScript中保留字的含义,大家同样不用记,了解一下就行了。

Description

以上就是关于JavaScript关键字的相关内容了,通过了解这26个JavaScript关键字的含义、特性和使用方法,你已经迈出了成为编程高手的重要一步。

记住,实践是学习的关键,不断地编写代码并应用这些关键字,你将逐渐掌握JavaScript的精髓。

如果觉得本文对你有所帮助,别忘了点赞和分享哦!

收起阅读 »

Android自定义一个省份简称键盘

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新And...
继续阅读 »

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新Android当中的技术点,回头一想,还是穿插着来吧,再系统的规划,难免也有变化,想到啥就写啥吧,能够坚持输出就行。


今天的这个知识点,是一个自定义View,一个省份的简称键盘,主要用到的地方,比如车牌输入等地方,相对来说还是比较的简单,我们先看下最终的实现效果:



实现方式呢有很多种,我相信大家也有自己的一套实现机制,这里,我采用的是组合View,用的是LinearLayout的方式。


今天的内容大致如下:


1、分析UI,如何布局


2、设置属性和方法,制定可扩展效果


3、部分源码剖析


4、开源地址及实用总结


一、分析UI,如何布局


拿到UI效果图后,其实也没什么好分析的,无非就是两块,顶部的完成按钮和底部的省份简称格子,一开始,打算用RecyclerView网格布局来实现,但是最后的删除按钮如何摆放就成了问题,直接悬浮在网格上边,动态计算位置,显然不太合适,也没有这样去搞的,索性直接抛弃这个方案,多布局的想法也实验过,但最终还是选择了最简单的LinearLayout组合View形式。


所谓简单,就是在省份简称数组的遍历中,不断的给LinearLayout进行追加子View,需要注意的是,本身的View,也就是我们自定义View,继承LinearLayout后,默认的是垂直方向的,往本身View追加的是横向属性的LinearLayout,这也是换行的效果,也就是,一行一个横向的LinearLayout,记住,横向属性的LinearLayout,才是最终添加View的直接父类。



换行的条件就是基于UI效果,当模于设置length等于0时,我们就重新创建一个水平的LinearLayout,这就可以了,是不是非常的简单。


至于最后的删除按钮,使其靠右,占据两个格子的权重设置即可。


二、设置属性和方法,制定可扩展效果


当我们绘制完这个身份简称键盘后,肯定是要给他人用的,基于灵活多变的需求,那么相对应的我们也需要动态的进行配置,比如背景颜色,文字的颜色,大小,还有边距,以及点击效果等等,这些都是需要外露,让使用者选择性使用的,目前所有的属性如下,大家在使用的时候,也可以对照设置。


设置属性


属性类型概述
lp_backgroundcolor整体的背景颜色
lp_rect_spacingdimension格子的边距
lp_rect_heightdimension格子的高度
lp_rect_margin_topdimension格子的距离上边
lp_margin_left_rightdimension左右距离
lp_margin_topdimension上边距离
lp_margin_bottomdimension下边距离
lp_rect_backgroundreference格子的背景
lp_rect_select_backgroundreference格子选择后的背景
lp_rect_text_sizedimension格子的文字大小
lp_rect_text_colorcolor格子的文字颜色
lp_rect_select_text_colorcolor格子的文字选中颜色
lp_is_show_completeboolean是否显示完成按钮
lp_complete_text_sizedimension完成按钮文字大小
lp_complete_text_colorcolor完成按钮文字颜色
lp_complete_textstring完成按钮文字内容
lp_complete_margin_topdimension完成按钮距离上边
lp_complete_margin_bottomdimension完成按钮距离下边
lp_complete_margin_rightdimension完成按钮距离右边
lp_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失

定义方法


方法参数概述
keyboardContent回调函数获取点击的省份简称简称信息
keyboardDelete函数删除省份简称简称信息
keyboardComplete回调函数键盘点击完成
openProhibit函数打开禁止(使领学港澳),使其可以点击

三、关键源码剖析


这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。


定义身份简称数组


    //省份简称数据
private val mLicensePlateList = arrayListOf(
"京", "津", "渝", "沪", "冀", "晋", "辽", "吉", "黑", "苏",
"浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "琼",
"川", "贵", "云", "陕", "甘", "青", "蒙", "桂", "宁", "新",
"藏", "使", "领", "学", "港", "澳",
)

遍历省份简称


mLength为一行展示多少个,当取模为0时,就需要换行,也就是再次创建一个水平的LinearLayout,添加至外层的垂直LinearLayout中,每个水平的LinearLayout中,则是一个一个的TextView。


  //每行对应的省份简称
var layout: LinearLayout? = null
//遍历车牌号
mLicensePlateList.forEachIndexed { index, s ->
if (index % mLength == 0) {
//重新创建,并添加View
layout = createLinearLayout()
layout?.weightSum = 1f
addView(layout)
val params = layout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
layout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//最后五个是否禁止
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
setTextColor(mNumProhibitColor)
mTempTextViewList.add(this)
} else {
setTextColor(mRectTextColor)
}

setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
return@setOnClickListener
}
//每个格子的点击事件
changeTextViewState(this)
}
}

addRectView(textView, layout, 0.1f)
}

追加最后一个View


由于最后一个视图是一个图片,占据了两个格子的大小,所以需要特殊处理,需要做的就是,单独设置权重weight和单独设置宽度width,如下所示:


  /**
* AUTHOR:AbnerMing
* INTRODUCE:追加最后一个View
*/

private fun addEndView(layout: LinearLayout?) {
val endViewLayout = LinearLayout(context)
endViewLayout.gravity = Gravity.RIGHT
//删除按钮
val endView = RelativeLayout(context)
//添加删除按钮
val deleteImage = ImageView(context)
deleteImage.setImageResource(R.drawable.view_ic_key_delete)
endView.addView(deleteImage)

val imageParams = deleteImage.layoutParams as RelativeLayout.LayoutParams
imageParams.addRule(RelativeLayout.CENTER_IN_PARENT)
deleteImage.layoutParams = imageParams
endView.setOnClickListener {
//删除
mKeyboardDelete?.invoke()
invalidate()
}
endView.setBackgroundResource(mRectBackGround)
endViewLayout.addView(endView)
val params = endView.layoutParams as LayoutParams
params.width = (getScreenWidth() / mLength) * 2 - mMarginLeftRight.toInt()
params.height = LayoutParams.MATCH_PARENT

endView.layoutParams = params

layout?.addView(endViewLayout)
val endParams = endViewLayout.layoutParams as LayoutParams
endParams.apply {
width = (mSpacing * 3).toInt()
height = LayoutParams.MATCH_PARENT
weight = 0.4f
rightMargin = mSpacing.toInt()
endViewLayout.layoutParams = this
}


}

四、开源地址及使用总结


开源地址:github.com/AbnerMing88…


关于使用,其实就是一个类,大家可以下载源码,直接复制即可使用,还可以进行修改里面的代码,非常的方便,如果懒得下载源码,没关系,我也上传到了远程Maven,大家可以按照下面的方式进行使用。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。


 allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。


 dependencies {
implementation 'com.vip:plate:1.0.0'
}

代码使用


   <com.vip.plate.LicensePlateView
android:id="@+id/lp_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:lp_complete_text_size="14sp"
app:lp_margin_left_right="10dp"
app:lp_rect_spacing="6dp"
app:lp_rect_text_size="19sp"
app:lp_text_click_effect="false" />


总结


大家在使用的时候,一定对照属性表进行选择性使用;关于这个省份简称自定义View,实现方式有很多种,我目前的这种也不是最优的实现方式,只是自己的一个实现方案,给大家一个作为参考的依据,好了,铁子们,本篇文章就先到这里,希望可以帮助到大家。


作者:程序员一鸣
来源:juejin.cn/post/7235484890019659834
收起阅读 »

文本美学:text-image打造视觉吸引力

web
当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。 项目介绍 话不多说,我们先看下作者的demo效果: _202...
继续阅读 »

当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。


项目介绍


话不多说,我们先看下作者的demo效果:


微信截图_20240420194201.png


_20240420194201.jpg


text-image可以将文字、图片、视频进行「文本化」


只需要通过简单的配置即可使用。


虽然这个项目star数很少,但确实是一个很有意思的项目,使用起来很简单的项目。


_20240420194537.jpg


_20240420194537.jpg


github地址:https://github.com/Sunny-117/text-image


我也是使用这个项目做了一个简单的web页面,感兴趣的家人可以使用看下效果:


web地址:http://h5.xiuji.mynatapp.cc/text-image/


_20240420211509.jpg


_20240420211509.jpg


项目使用


这个项目使用起来相对简单,只需按作者的文档使用即可,虽然我前端属于小白的水平,但还是在ai的帮助下做了一个简单的html页面,如果有家人需要的话可以私信我,我发下文件。下边我们就介绍下:



  • 文字「文本化」


先看效果:


_20240420195701.jpg


_20240420195701.jpg


我们在这儿是将配置的一些参数在页面上做了一个可配置的表单,方便我们配置。


家人们想自己尝试的话可以试下以下这个demo。


demo.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
replaceText: '123',
source: {
text: '修己xj',
},
});
</script>
</body>
</html>


  • 图片「文本化」


_20240420200651.jpg


_20240420200651.jpg


demo.html


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 7,
isGray: true,
source: {
img: './assets/1.png',
},
});
</script>
</body>

</html>


  • 视频「文本化」


动画1.gif


1.gif


demo.html


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>

<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 8,
isGray: true,
source: {
video: './assets/1.mp4',
height: 700,
},
});
</script>
</body>

</html>

需要注意的是:作者在项目中提供的视频的demo这个属性值有错误,我们需要改正后方可正常显示:


_20240420211124.jpg


_20240420211124.jpg


总结


text-image 是一个强大的前端工具,可以帮助用户快速、轻松地将文本、图片、视频转换成文本化的图片,增强文本内容的表现力和吸引力。


作者:修己xj
来源:juejin.cn/post/7359510120248786971
收起阅读 »

安卓开发中如何实现一个定时任务

定时任务方式优点缺点使用场景所用的API普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)Timer定时器简单易用,可以设置固定周期或者...
继续阅读 »

定时任务方式优点缺点使用场景所用的API
普通线程sleep的方式简单易用,可用于一般的轮询Polling不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Thread.sleep(long)
Timer定时器简单易用,可以设置固定周期或者延迟执行的任务不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间的定时任务Timer.schedule(TimerTask,long)
ScheduledExecutorService灵活强大,可以设置固定周期或者延迟执行的任务,并支持多线程并发不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要多线程并发的定时任务Executors.newScheduledThreadPool(int).schedule(Runnable,long,TimeUnit)
Handler中的postDelayed方法简单易用,可以设置延迟执行的任务,并与UI线程交互不精确,不可靠,容易被系统杀死或者休眠需要在App内部执行短时间且需要与UI线程交互的定时任务Handler.postDelayed(Runnable,long)
Service + AlarmManger + BroadcastReceiver可靠稳定,可以设置精确或者不精确的闹钟,并在后台长期运行需要声明相关权限,并受系统时间影响需要在App外部执行长期且对时间敏感的定时任务AlarmManager.set(int,PendingIntent), BroadcastReceiver.onReceive(Context,Intent), Service.onStartCommand(Intent,int,int)
WorkManager可靠稳定,不受系统时间影响,并可以设置多种约束条件来执行任务需要添加依赖,并不能保证准时执行需要在App外部执行长期且对时间不敏感且需要满足特定条件才能执行的定时任务WorkManager.enqueue(WorkRequest), Worker.doWork()
RxJava简洁、灵活、支持多线程、支持背压、支持链式操作学习曲线较高、内存占用较大需要处理复杂的异步逻辑或数据流io.reactivex:rxjava:2.2.21
CountDownTimer简单易用、不需要额外的线程或handler不支持取消或重置倒计时、精度受系统时间影响需要实现简单的倒计时功能android.os.CountDownTimer
协程+Flow语法简洁、支持协程作用域管理生命周期、支持流式操作和背压需要引入额外的依赖库、需要熟悉协程和Flow的概念和用法需要处理异步数据流或响应式编程kotlinx-coroutines-core:1.5.0
使用downTo关键字和Flow实现一个定时任务1、可以使用简洁的语法创建一个倒数的范围 2 、可以使用Flow异步地发射和收集倒数的值3、可以使用onEach等操作符对倒数的值进行处理或转换1、需要注意倒数的范围是否包含0,否则可能会出现偏差 2、需要注意倒数的间隔是否与delay函数的参数一致,否则可能会出现不准确 3、需要注意取消或停止Flow的时机,否则可能会出现内存泄漏或资源浪费1、适合于需要实现简单的倒计时功能,例如显示剩余时间或进度 2、适合于需要在倒计时过程中执行一些额外的操作,例如播放声音或更新UI 3、适合于需要在倒计时结束后执行一些额外的操作,例如跳转页面或弹出对话框implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
Kotlin 内联函数的协程和 Flow 实现很容易离开主线程,样板代码最少,协程完全活用了 Kotlin 语言的能力,包括 suspend 方法。可以处理大量的异步数据,而不会阻塞主线程。可能会导致内存泄漏和性能问题。处理 I/O 阻塞型操作,而不是计算密集型操作。kotlinx.coroutines 和 kotlinx.coroutines.flow

安卓开发中如何实现一个定时任务


在安卓开发中,我们经常会遇到需要定时执行某些任务的需求,比如轮询服务器数据、更新UI界面、发送通知等等。那么,我们该如何实现一个定时任务呢?本文将介绍安卓开发中实现定时任务的五种方式,并比较它们的优缺点,以及适用场景。


1. 普通线程sleep的方式


这种方式是最简单也最直观的一种实现方法,就是在一个普通线程中使用sleep方法来延迟执行某个任务。例如:


// 创建一个普通线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 循环执行
while (true) {
// 执行某个任务
doSomething();
// 延迟10秒
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 启动线程
thread.start();


这种方式的优点是简单易懂,不需要借助其他类或组件。但是它也有很多缺点:



  • sleep方法会阻塞当前线程,导致资源浪费和性能下降。

  • sleep方法不准确,它只能保证在指定时间后醒来,但不能保证立即执行。

  • sleep方法受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • sleep方法不可靠,如果线程被异常终止或者进入休眠状态,会导致计时中断。


因此,这种方式只适合一般的轮询Polling场景。


2. Timer定时器


这种方式是使用Java API里提供的Timer类来实现定时任务。Timer类可以创建一个后台线程,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Timer对象
Timer timer = new Timer();
// 创建一个TimerTask对象
TimerTask task = new TimerTask() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
timer.schedule(task, 5000, 10000);


这种方式相比第一种方式有以下优点:



  • Timer类内部使用wait和notify方法来控制线程的执行和休眠,不会浪费资源和性能。

  • Timer类可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • Timer类可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • Timer类只创建了一个后台线程来执行所有的任务,如果其中一个任务耗时过长或者出现异常,则会影响其他任务的执行。

  • Timer类受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • Timer类不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些不太重要的定时任务。


3. ScheduledExecutorService


这种方式是使用Java并发包里提供的ScheduledExecutorService接口来实现定时任务。ScheduledExecutorService接口可以创建一个线程池,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个ScheduledExecutorService对象
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
}
};
// 设置在5秒后开始执行,并且每隔10秒重复执行一次
service.scheduleAtFixedRate(task, 5, 10, TimeUnit.SECONDS);


这种方式相比第二种方式有以下优点:



  • ScheduledExecutorService接口可以创建多个线程来执行多个任务,避免了单线程的弊端。

  • ScheduledExecutorService接口可以设置固定频率或者固定延迟来执行任务,更加灵活和准确。

  • ScheduledExecutorService接口可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • ScheduledExecutorService接口受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • ScheduledExecutorService接口不可靠,如果进程被杀死或者进入休眠状态,会导致计时中断。


因此,这种方式适合一些需要多线程并发执行的定时任务。


4. Handler中的postDelayed方法


这种方式是使用Android API里提供的Handler类来实现定时任务。Handler类可以在主线程或者子线程中发送和处理消息,在指定的时间或者周期性地执行某个任务。例如:


// 创建一个Handler对象
Handler handler = new Handler();
// 创建一个Runnable对象
Runnable task = new Runnable() {
@Override
public void run() {
// 执行某个任务
doSomething();
// 延迟10秒后再次执行该任务
handler.postDelayed(this, 10000);
}
};
// 延迟5秒后开始执行该任务
handler.postDelayed(task, 5000);


这种方式相比第三种方式有以下优点:



  • Handler类不受系统时间影响,它使用系统启动时间作为参考。

  • Handler类可以在主线程中更新UI界面,避免了线程间通信的问题。


但是这种方式也有以下缺点:



  • Handler类只能在当前进程中使用,如果进程被杀死或者进入休眠状态,会导致计时中断。

  • Handler类需要手动循环调用postDelayed方法来实现周期性地执行任务。


因此,这种方式适合一些需要在主线程中更新UI界面的定时任务.


5. Service + AlarmManager + BroadcastReceiver


这种方式是使用Android API里提供的三个组件来实现定时任务. Service组件可以在后台运行某个长期的服务;AlarmManager组件可以设置一个闹钟,在指定的时间发送一个



  • Intent,用于指定要启动的Service组件和传递一些参数。

  • AlarmManager组件可以设置一个闹钟,在指定的时间发送一个Intent给BroadcastReceiver组件。

  • BroadcastReceiver组件可以接收AlarmManager发送的Intent,并启动Service组件来执行任务。


这种方式相比第四种方式有以下优点:



  • Service组件可以在后台运行,即使进程被杀死或者进入休眠状态,也不会影响计时。

  • AlarmManager组件可以设置精确或者不精确的闹钟,根据不同的需求节省电量。

  • BroadcastReceiver组件可以在系统开机后自动注册,实现开机自启动。


但是这种方式也有以下缺点:



  • Service组件需要在AndroidManifest.xml文件中声明,并申请相关的权限。

  • AlarmManager组件受系统时间影响,如果用户修改了系统时间,会导致计时错误。

  • BroadcastReceiver组件需要在代码中动态注册和注销,避免内存泄漏。


因此,这种方式适合一些需要长期在后台执行的定时任务。


6. WorkManager


这种方式是使用Android Jetpack里提供的WorkManager库来实现定时任务. WorkManager库是一个用于管理后台任务的框架,它可以在满足一定条件下执行某个任务,并保证任务一定会被执行。例如:


// 创建一个PeriodicWorkRequest对象
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(MyWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
// 获取一个WorkManager对象
WorkManager workManager = WorkManager.getInstance(this);
// 将PeriodicWorkRequest对象加入到队列中
workManager.enqueue(request);


这种方式相比第五种方式有以下优点:



  • WorkManager库不受系统时间影响,它使用系统启动时间作为参考。

  • WorkManager库可以设置多种约束条件来执行任务,例如网络状态、电量状态、设备空闲状态等。

  • WorkManager库可以取消或者重新安排任务,更加方便和可控。


但是这种方式也有以下缺点:



  • WorkManager库需要添加依赖并配置相关的权限。

  • WorkManager库不能保证任务准时执行,它会根据系统资源和约束条件来调度任务。


因此,这种方式适合一些对时间不敏感且需要满足特定条件才能执行的定时任务


您好,这是必应。我根据您的要求,从网络上搜集了一些关于如何使用RXJAVA、CountDownTimer、协程安卓实现一个定时任务的资料,并整理成了一篇文章。文章的结构如下:


7.如何使用RXJAVA实现定时任务


RXJAVA是一个基于观察者模式和函数式编程的库,它可以用来处理异步事件和数据流。RXJAVA提供了一些操作符,可以用来创建和处理定时任务。其中,interval操作符可以用来创建一个按照固定时间间隔发射整数序列的Observable,可以用来实现周期性的定时任务。例如,以下代码可以实现每隔1秒发射一个整数,从0开始:


//创建一个Observable,每隔1秒发射一个整数,从0开始
Observable timer = Observable.interval(1, TimeUnit.SECONDS);
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


如果要实现倒计时的效果,可以使用map操作符,将发射的整数转换为倒计时的秒数。例如,以下代码可以实现一个10秒的倒计时:


//创建一个Observable,每隔1秒发射一个整数,从0开始,共发射10个
Observable timer = Observable.interval(1, TimeUnit.SECONDS).take(10);
//使用map操作符,将发射的整数转换为倒计时的秒数
timer = timer.map(new Function() {
@Override
public Long apply(Long aLong) throws Exception {
//将发射的整数转换为倒计时的秒数,例如0转换为10,1转换为9,以此类推
return 10 - aLong;
}
});
//订阅这个Observable,打印发射的数据
timer.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
//订阅时的操作,可以用来取消订阅
}

@Override
public void onNext(Long aLong) {
//收到数据时的操作,可以用来更新UI或者执行其他逻辑
Log.d("RXJAVA", "onNext: " + aLong);
}

@Override
public void onError(Throwable e) {
//发生错误时的操作,可以用来处理异常
}

@Override
public void onComplete() {
//完成时的操作,可以用来释放资源
}
});


RXJAVA的优点是可以方便地处理异步事件和数据流,可以使用各种操作符来实现复杂的逻辑,可以避免内存泄漏和线程安全


8.如何使用CountDownTimer实现定时任务


CountDownTimer是Android中提供的一个倒计时器类,它可以用来实现一个在一定时间内递减的倒计时。CountDownTimer的构造方法接受两个参数:总时间和间隔时间。例如,以下代码可以创建一个10秒的倒计时,每隔1秒更新一次:


//创建一个10秒的倒计时,每隔1秒更新一次
CountDownTimer timer = new CountDownTimer(10000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
//每隔一秒调用一次,可以用来更新UI或者执行其他逻辑
Log.d("CountDownTimer", "onTick: " + millisUntilFinished / 1000);
}

@Override
public void onFinish() {
//倒计时结束时调用,可以用来释放资源或者执行其他逻辑
Log.d("CountDownTimer", "onFinish");
}
};
//开始倒计时
timer.start();
//取消倒计时
timer.cancel();


CountDownTimer的优点是使用简单,可以直接在UI线程中更新UI,不需要额外的线程或者Handler。CountDownTimer的缺点是只能实现倒计时的效果,不能实现周期性的定时任务,而且精度受系统时间的影响,可能不够准确。


9.如何使用协程实现定时任务


协程可以用来简化异步编程和线程管理。协程是一种轻量级的线程,它可以在不阻塞线程的情况下挂起和恢复执行。协程安卓提供了一些扩展函数,可以用来创建和处理定时任务。其中,delay函数可以用来暂停协程的执行一段时间,可以用来实现倒计时或者周期性的定时任务。例如,以下代码可以实现一个10秒的倒计时,每隔1秒更新一次:


//创建一个协程作用域,可以用来管理协程的生命周期
val scope = CoroutineScope(Dispatchers.Main)
//在协程作用域中启动一个协程,可以用来执行异步任务
scope.launch {
//创建一个变量,表示倒计时的秒数
var seconds = 10
//循环执行,直到秒数为0
while (seconds > 0) {
//打印秒数,可以用来更新UI或者执行其他逻辑
Log.d("Coroutine", "seconds: $seconds")
//暂停协程的执行1秒,不阻塞线程
delay(1000)
//秒数减一
seconds--
}
//倒计时结束,打印日志,可以用来释放资源或者执行其他逻辑
Log.d("Coroutine", "finish")
}
//取消协程作用域,可以用来取消所有的协程
scope.cancel()


协程安卓的优点是可以方便地处理异步任务和线程切换,可以使用简洁的语法来实现复杂的逻辑,可以避免内存泄漏和回调。协程的缺点是需要引入额外的依赖,而且需要一定的学习成本,不太适合初学者。


10.使用kotlin关键字 ‘downTo’ 搭配Flow


// 创建一个倒计时器,从10秒开始,每秒减一
val timer = object: CountDownTimer(10000, 1000) {
override fun onTick(millisUntilFinished: Long) {
// 在每个间隔,发射剩余的秒数
emitSeconds(millisUntilFinished / 1000)
}

override fun onFinish() {
// 在倒计时结束时,发射0
emitSeconds(0)
}
}

// 创建一个Flow,用于发射倒数的秒数
fun emitSeconds(seconds: Long): Flow = flow {
// 使用downTo关键字创建一个倒数的范围
for (i in seconds downTo 0) {
// 发射当前的秒数
emit(i.toInt())
}
}


11.kotlin内联函数的协程和 Flow 实现


fun FragmentActivity.timerFlow(
time: Int = 60,
onStart: (suspend () -> Unit)? = null,
onEach: (suspend (Int) -> Unit)? =
null,
onCompletion: (suspend () -> Unit)? =
null
): Job {
return (time downTo 0)
.asFlow()
.cancellable()
.flowOn(Dispatchers.Default)
.onStart { onStart?.invoke() }
.onEach {
onEach?.invoke(it)
delay(
1000L)
}.onCompletion { onCompletion?.invoke() }
.launchIn(lifecycleScope)
}


//在activity中使用
val job = timerFlow(
time = 60,
onStart = { Log.d("Timer", "Starting timer...") },
onEach = { Log.d("Timer", "Seconds remaining: $it") },
onCompletion = { Log.d("Timer", "Timer completed.") }
)

//取消计时
job.cancel()

作者:淘淘养乐多
来源:juejin.cn/post/7270173192789737487
收起阅读 »

各平台移动开发技术对比

针对原生开发面临的问题,业界一直都在努力寻找好的解决方案,而时至今日,已经有很多跨平台框架(注意,本书中所指的“跨平台”若无特殊说明,即特指 Android 和 iOS 两个平台),根据其原理,主要分为三类: hybrid :H5 + 原生(Cordova、I...
继续阅读 »

针对原生开发面临的问题,业界一直都在努力寻找好的解决方案,而时至今日,已经有很多跨平台框架(注意,本书中所指的“跨平台”若无特殊说明,即特指 Android 和 iOS 两个平台),根据其原理,主要分为三类:


hybrid :H5 + 原生(Cordova、Ionic、微信小程序)

JavaScript 开发 + 原生渲染 (React Native、Weex)

自绘UI + 原生 (Qt for mobile、Flutter)


1、Hybrid :H5 + 原生


主要原理:
将 App 中需要动态变动的内容通过HTML5(简称 H5)来实现,
通过原生的网页加载控件WebView (Android)或 WKWebView(iOS)来加载。
WebView 中 JavaScript 与原生 API 之间就需要一个通信的桥梁,JsBridge。


**优点是:**动态内容可以用 H5开发,而H5是Web 技术栈,Web技术栈生态开放且社区资源丰富,整体开发效率高。


缺点是:

1.性能体验不佳,对于复杂用户界面或动画,WebView 有时会不堪重任。

2.其 JavaScript 依然运行在一个权限受限的沙箱中,所以对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等。所以,对于 H5 不能实现的功能,就需要原生去做了。


2、JavaScript开发 + 原生渲染 (React Native、Weex)


2.1、React Native


1.React Native (简称 RN )是 Facebook 开源的跨平台移动应用开发框架。
目前支持 iOS 和 Android 两个平台。
2.React Native 基于 JavaScript,开发者可以利用已有的前端开发经验快速上手
3.开发者编写的js代码,通过 react native 的中间层转化为原生控件和操作
4.react native 运行在JavaCore中,所以不存在浏览器兼容的问题
最终,JS代码会被打包成一个 bundle 文件,自动添加到 App 的资源目录下。



JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用:



  1. 为 JavaScript 提供运行环境。

  2. 是 JavaScript 与原生应用之间通信的桥梁,作用和 JsBridge 一样,事实上,在 iOS 中,很多 JsBridge 的实现都是基于 JavaScriptCore 。




而 RN 中将虚拟 DOM 映射为原生控件的过程主要分两步:



  1. 布局消息传递; 将虚拟 DOM 布局信息传递给原生;

  2. 原生根据布局信息通过对应的原生控件渲染;



RN 和 React 原理相通,React 是一个响应式的 Web 框架。



  • 开发者只需关注状态转移(数据),当状态发生变化,React 框架会自动根据新的状态重新构建UI。

  • React 框架在接收到用户状态改变通知后,会根据当前渲染树,结合最新的状态改变,通过 Diff 算法,计算出树中变化的部分,然后只更新变化的部分(DOM操作),从而避免整棵树重构,提高性能


2.2、Weex


1.Weex 是阿里的跨平台移动端开发框架,思想及原理和 React Native 类似
底层都是通过原生渲染的
2.不同是应用层开发语法 (即 DSL,Domain Specific Language):Weex 支持 Vue 语法和 Rax 语法
3.Rax 的 DSL(Domain Specific Language) 语法是基于 React JSX 语法而创造
4.但相对于 React Native,它对前端开发者的要求较低
5、一定程度减少了JS Bundle的体积,使得 bundle 里面只保留业务代码。


JavaScript 开发 + 原生渲染 的方式主要优点如下



  1. 采用 Web 开发技术栈,社区庞大、有前端基础的话上手快、开发成本相对较低。

  2. 原生渲染,性能相比 H5 提高很多。

  3. 动态化较好,支持热更新。


不足:



  1. 渲染时需要 JavaScript 和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿。

  2. JavaScript 为脚本语言,执行时需要解释执行 (这种执行方式通常称为 JIT,即 Just In Time,指在执行时实时生成机器码),执行效率和编译类语言(编译类语言的执行方式为 AOT ,即 Ahead Of Time,指在代码执行前已经将源码进行了预处理,这种预处理通常情况下是将源码编译为机器码或某种中间码)仍有差距。

  3. 由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后;

    除此之外,其控件系统也会受到原生UI系统限制,例如,在 Android 中,手势冲突消歧规则是固定的,这在使用不同人写的控件嵌套时,手势冲突问题将会变得非常棘手。这就会导致,如果需要自定义原生渲染组件时,开发和维护成本过高。


3、自绘UI + 原生


自绘UI + 原生这种技术的思路是:
通过在不同平台实现一个统一接口的渲染引擎来绘制UI,而不依赖系统原生控件,
所以可以做到不同平台UI的一致性


注意,自绘引擎解决的是 UI 的跨平台问题,如果涉及其他系统能力调用,依然要涉及原生开发。这种平台技术的优点如下:



  1. 性能高;由于自绘引擎是直接调用系统API来绘制UI,所以性能和原生控件接近。

  2. 灵活、组件库易维护、UI外观保真度和一致性高;由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到高保真和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。


不足:



  1. 动态性不足;为了保证UI绘制性能,自绘UI系统一般都会采用 AOT 模式编译其发布包,所以应用发布后,不能像 Hybrid 和 RN 那些使用 JavaScript(JIT)作为开发语言的框架那样动态下发代码。

  2. 应用开发效率低:Qt 使用 C++ 作为其开发语言,而编程效率是直接会影响 App 开发效率的,C++ 作为一门静态语言,在 UI 开发方面灵活性不及 JavaScript 这样的动态语言,另外,C++需要开发者手动去管理内存分配,没有 JavaScript 及Java中垃圾回收(GC)的机制。


Flutter 就属于这一类跨平台技术,没错,Flutter 正是实现一套自绘引擎,并拥有一套自己的 UI 布局系统,且同时在开发效率上有了很大突破。


3.1、Qt


Qt 是一个1991年由 Qt Company 开发的跨平台 C++ 图形用户界面应用程序开发框架。


在近几年,虽然偶尔能听到 Qt 的声音,但一直很弱,无论 Qt 本身技术如何、设计思想如何,但事实上终究是败了,究其原因,笔者认为主要有四:


第一:Qt 移动开发社区太小,学习资料不足,生态不好。

第二:官方推广不利,支持不够。

第三:移动端发力较晚,市场已被其他动态化框架占领( Hybrid 和 RN )。

第四:在移动开发中,C++ 开发和Web开发栈相比有着先天的劣势,直接结果就是 Qt 开发效率太低。


3.2、Flutter


Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。
Flutter 实现了一个自绘引擎,使用自身的布局、绘制系统。


2021年8月底,已经有 127K  Star,Star 数量 Github 上排名前 20 
Flutter 生态系统得以快速增长,国内外有非常多基于 Flutter 的成功案例。



1.Flutter 采用自己的渲染引擎 Skia,将 UI 渲染到画布上,具有良好的性能表现

2.如果对性能要求较高,特别是需要处理复杂动画和大量图形渲染的场景,建议选择 Flutter。

3.Flutter 则采用 Dart 语言,需要开发人员掌握新的语法和概念。

4.支持iOS、Android、Windows/MAC/Linux等多个平台,且能达到原生性能。(移动端、Web端和PC端)



Flutter和Gt对比:



  1. 生态:Flutter 生态系统发展迅速,社区非常活跃,无论是开发者数量还是第三方组件都已经非常可观。

  2. 技术支持:现在 Google 正在大力推广Flutter,Flutter 的作者中很多人都是来自Chromium团队,并且 Github上活跃度很高。另一个角度,从 Flutter 诞生到现在,频繁的版本发布也可以看出 Google 对 Flutter的投入的资源不小,所以在官方技术支持这方面,大可不必担心。

  3. 开发效率:一套代码,多端运行;并且在开发过程中 Flutter 的热重载可帮助开发者快速地进行测试、构建UI、添加功能并更快地修复错误。在 iOS 和 Android 模拟器或真机上可以实现毫秒级热重载,并且不会丢失状态。这真的很棒,相信我,如果你是一名原生开发者,体验了Flutter开发流后,很可能就不想重新回去做原生了,毕竟很少有人不吐槽原生开发的编译速度。


4、react-native、weex、flutter对比:



三种跨平台技术



react-native、weex、flutter对比


React Native:宣布放弃使用 React Native,回归使用原生技术。主要还是集中于项目庞大之后的维护困难,第三方库的良莠不齐,兼容上需要耗费更多的精力导致放弃。


hybrid:


大家都知道hybrid即为web+native的混合开发模式



优点:就是拥有了web开发的服务端发布即可更新的便捷性,Android和iOS两端可以共用代码,并且web技术已经非常成熟,开发效率也会很高。




缺点:就是众所周知的性能相比native有很大的不足,且不同机型和系统版本下的兼容性较差。



React Native、Weex 和 Flutter 是目前最为热门的混合开发框架,它们各自有着优势和特点:


1、React Native



1.React Native 是由 Facebook 推出的开源框架,拥有庞大而活跃的社区,有大量的第三方组件和库可供使用。

2.React Native 基于 JavaScript,开发者可以利用已有的前端开发经验快速上手

3.开发者编写的js代码,通过 react native 的中间层转化为原生控件和操作

4.react native 运行在JavaCore中,所以不存在浏览器兼容的问题



  1. 最终,JS代码会被打包成一个 bundle 文件,自动添加到 App 的资源目录下。



2、Weex



  • Weex 是阿里巴巴推出的开源项目,也有一个较为活跃的社区,但相对于 React Native 来说,生态系统规模稍小。



1.React Native 和 Weex 使用了 WebView 或类似的机制来渲染应用界面,性能相对较低。

2.Weex 同样基于 JavaScript,但相对于 React Native,它对前端开发者的要求较低

3.开发者可以使用Vue.js和Rax两个前端框架来进行WEEX页面开发

4.和 react native一样,weex 所有的标签也不是真实控件,JS 代码中所生成存的 dom,最后都是由 Native 端解析,再得到对应的Native控件渲染



  1. weex:一定程度减少了JS Bundle的体积,使得 bundle 里面只保留业务代码。



3、Flutter



  • Flutter 是由 Google 开发的开源框架,虽然相对较新,但也有一个迅速增长的社区和生态系统。



1.Flutter 采用自己的渲染引擎 Skia,将 UI 渲染到画布上,具有良好的性能表现

2.如果对性能要求较高,特别是需要处理复杂动画和大量图形渲染的场景,建议选择 Flutter。

3.Flutter 则采用 Dart 语言,需要开发人员掌握新的语法和概念。

4.支持iOS、Android、Windows/MAC/Linux等多个平台,且能达到原生性能。(移动端、Web端和PC端)



4、react-native、weex、flutter对比:



react-native、weex、flutter对比


React Native:宣布放弃使用 React Native,回归使用原生技术。主要还是集中于项目庞大之后的维护困难,第三方库的良莠不齐,兼容上需要耗费更多的精力导致放弃。


作者:码农君
来源:juejin.cn/post/7360586351816638501
收起阅读 »

vue反编译dist包到源码

web
最近由于公司老项目上的问题,由于项目很老,之前交接的源码包中缺少了很大一部分模块,但是现在线上的环境和dist包是正常运行的,领导希望能够手动将这部分补全,由于前期项目的不规范,缺少接口文档以及原型图,因此无法知道到底该如何补全,因此,我想着能不能通过dist...
继续阅读 »

最近由于公司老项目上的问题,由于项目很老,之前交接的源码包中缺少了很大一部分模块,但是现在线上的环境和dist包是正常运行的,领导希望能够手动将这部分补全,由于前期项目的不规范,缺少接口文档以及原型图,因此无法知道到底该如何补全,因此,我想着能不能通过dist包去反编译源码包呢,经过多方面探索发现是可行的,但是只能编译出vue文件,但是也满足基本需要了。


1.如何反编译


1.首先需要在管理员模式下打开cmd


2.找到需要编译的dist/static/js的目录下
执行完成后在该目录会看到目录下存在下面的文件名:0.7ab7d1434ffcc747c1ca.js.map,这里以0.7ab7d1434ffcc747c1ca.js.map为例,如下图:


image.png


3.全局安装reverse-sourcemap资源



npm install --global reverse-sourcemap



4.反编译
执行:reverse-sourcemap --output-dir source 0.7ab7d1434ffcc747c1ca.js.map


2.脚本反编译


上面的方式执行完毕,确实在source中会出现源码,那么有没有可能用脚本去执行呢,通过node的child_process模块中的exec方式便可以执行reverse-sourcemap --output-dir source这个命令,那么只需要拿到当前文件夹中包含.map文件即可,那么可以借助node中fs模块,递归读取文件名,并使用正则将所有.map的文件提取出来放在一个集合或数组中,在对数组进行递归循环执行reverse-sourcemap --output-dir source这个命令


2.1根据child_process模块编写执行函数



function executeReverseSourceMap(outputDir) {
// 构建 reverse-sourcemap 命令
const command = `reverse-sourcemap --output-dir source ${outputDir}`;

// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`执行命令时出错:${error.message}`);
return;
}
if (stderr) {
console.error(`命令输出错误:${stderr}`);
return;
}
console.log(`命令输出结果:${stdout}`);
});
}

2.2读取文件并匹配文件


// // 读取文件夹中的文件
fs.readdir(folderPath, (err, files) => {
if (err) {
console.error('读取文件夹时出错:', err);
return;
}
// 遍历文件
files.forEach(file => {
// 使用正则表达式匹配特定格式的文件名
const match = /^(\d+)\..+\.js\.map$/.exec(file);
if (match) {
// 如果匹配成功,将文件名存入数组
targetFiles.push(match[0]);
}
});

// 输出目标文件名数组
targetFiles.forEach(file=>{
executeReverseSourceMap(file)
})
});

2.3完整的执行代码


const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
// 文件夹路径
const folderPath = '../js';

// 存放目标文件名的数组
const targetFiles = [];
function executeReverseSourceMap(outputDir) {
// 构建 reverse-sourcemap 命令
const command = `reverse-sourcemap --output-dir source ${outputDir}`;

// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`执行命令时出错:${error.message}`);
return;
}
if (stderr) {
console.error(`命令输出错误:${stderr}`);
return;
}
console.log(`命令输出结果:${stdout}`);
});
}
// // 读取文件夹中的文件
fs.readdir(folderPath, (err, files) => {
if (err) {
console.error('读取文件夹时出错:', err);
return;
}
// 遍历文件
files.forEach(file => {
// 使用正则表达式匹配特定格式的文件名
const match = /^(\d+)\..+\.js\.map$/.exec(file);
if (match) {
// 如果匹配成功,将文件名存入数组
targetFiles.push(match[0]);
}
});

// 输出目标文件名数组
targetFiles.forEach(file=>{
executeReverseSourceMap(file)
})
});

image.png


3最终结果展示图


image.png


作者:ws_qy
来源:juejin.cn/post/7359893196439207972
收起阅读 »

Zed,有望打败 VS Code 吗?

大家好,我是楷鹏。 先说结论,不行。 Zed,又一款新起的文本代码编辑器 👉 zed.dev 今年一月二十四号正式开源,短短不到三个月,GitHub 上已经冲上 3 万 star 正如 Zed 的口号所说「Code at the speed of th...
继续阅读 »

大家好,我是楷鹏。


先说结论,不行


Zed,又一款新起的文本代码编辑器




👉 zed.dev



今年一月二十四号正式开源,短短不到三个月,GitHub 上已经冲上 3 万 star



正如 Zed 的口号所说「Code at the speed of thought 以思考的速度编码


实际体验下来,Zed 确实会比 VS Code 丝滑


⬇️ Zed



⬇️ VS Code



官网也给出了打字输入性能对比:



输入字母 z 并显示到屏幕,Zed 仅需 58 毫秒,而 VS Code 需要 97 毫秒


Zed 比 VS Code 快了 1.4 倍


在输入性能方面,Zed 胜出


其次就是 Zed 主打的另一个核心功能,多用户协同编程



额说实话,这个功能暂时想不到很好的落地使用场景。




到目前为止,Zed 仅仅是一个不错的文本编辑器。


甚至可以说,Zed 实质上并没有重大的突破,属于自嗨产品。


Zed 宣传的高性能,并没有质的飞跃,很难打到用户的马屁上。



「58毫秒」和「97毫秒」两个差距并不大


实际开发都知道,编程的瓶颈并不在于输入速度


另外是多用户协同,目前看这个场景不友好



如果是文档协同,国内的飞书文档、腾讯文档等哪一个不是佼佼者,按着 Zed 锤。


如果是代码协同,显然 Git 才是主流。




Zed 太年轻,目前很基础的 markdown 预览都没有实现


VS Code 珠玉在前,用开源、插件化形成的护城河,一开放拥有大批拥趸


而 Zed 虽然同样有插件机制,但是能指望多少人贡献呢?



《重来》一书讲到,第一次创业失败的人,第二次创业失败概率一样大


Zed 的团队原先做过 Atom 编辑器,而现在 Atom 名存实亡



团队做 Atom 失败过,而卷土重来的 Zed,还不行


Zed 大概率能够圈住一部分用户,但不会成为领域的成功。




Zed 如何能破局呢?最重要的还是要顺势而为


想想 VS Code 当时,互联网的繁荣,带动开源领域的发展,Eclipses 老旧、Jetbrains 高昂收费,前端分工细化,急需轻量的编辑器,这些都是 VS Code 的势头。


而目前 Zed 最好的势头,显而易见,就是 AI 方向


而 Zed 目前显然支持不足,仅有 Copilot 代码不足和 Chat 能力



而这些 VS Code 不仅有,而且功能更加完善。


Zed 团队应该思考下了,要做一款怎么样的编辑器,适应目前的 AI 潮流,开创新的赛道。


如果继续安于微不足道的性能提升、垂直的协同,继续在垂直赛道内卷,那我祝你成功。



作者:吴楷鹏
来源:juejin.cn/post/7359469421742473225
收起阅读 »

为什么不建议在 Vue <style> 中使用 scoped?

web
前言 标签下编写样式。不知你是否留意,在 标签下有一个属性经常出现 - scoped。你知道它起到什么作用吗?原理是怎样的?有没有什么弊端呢?今天我们就来聊聊它。 1. 什么是 scoped? scoped 顾名思义,与作用域有关,因为是设计组件样式的,所以...
继续阅读 »

前言

亲爱的小伙伴,你好!我是 嘟老板。我们使用 Vue 开发页面时,经常需要在 

收起阅读 »

是的,失业快一个月了

离职快一个月了,在这一个月里,也会在闲暇的时间记录一下思考。突然对于工作、对于求职、对于健康、对于生活有了新的认识。 3月30日 是的,我离职了,离开了我工作接近两年的公司。没有拍很多的照片,也没有习惯性的剪辑一个vlog。走的很平静,甚至工位上的东西,用一个...
继续阅读 »

离职快一个月了,在这一个月里,也会在闲暇的时间记录一下思考。突然对于工作、对于求职、对于健康、对于生活有了新的认识。


3月30日


是的,我离职了,离开了我工作接近两年的公司。没有拍很多的照片,也没有习惯性的剪辑一个vlog。走的很平静,甚至工位上的东西,用一个装零食的塑料袋就可以全部的拿走。


那个下午,我回忆了这两年的很多的事情。从一线回来,先租房子,然后等着毕业后的7月办理入职;随后研究起AI,做了很多的demo。后来搬到了楼上,写着用古老技术写的项目,楼下部分伙伴也相继离开了。勉强熬过了23年的冬天,年后我也得走了。


经过了漫长的离职流程,一切就绪,就差一个离职证明。


正好在那天晚上回来刷着抖音的时候,听到了飞书要裁员的消息,大约1000多人要面临着失业。


在还没毕业的大学时光里,我就满怀着焦虑。考研爹妈不支持,生活费没有着落,就业压力大。实在是没了办法,四处的海投,后来去了上海,再后来去了一线。再后来,在一线面临着裁员,又一下子回到了武汉。


回武汉-到现在的住所路上


想着过去的三年,真的像是一场梦。熬过了疫情,熬过了“二阳”,任何时候,互联网上都充斥着“互联网寒冬“的声音。而我,也在将近两年的摆烂之后,不得不面对这个残酷、血淋淋的现实。


人很容易陷入自我怀疑和沮丧的境地,在激情和麻木中徘徊。我常常在一些加班到很晚的晚上,看着车窗外的街道问自己:这一切的努力为的是什么?为了梦想、为了走出农村,为了体面的活着,仿佛内心燃起了激情。然而,第二天又得从床上爬起来,继续按部就班的上班,挤着电梯,做着乏味的工作。晚上下班了,又回来坐在电脑桌前。周而复始。


年与时驰,意与日去,遂成枯落,多不接世,悲守穷庐,将复何及!终究将逃不过30岁!我也时常遥望自己的30岁,30岁是什么样子的?是有一个爱的人,有了自己的孩子,有了自己幸福的家;还是一个人在异乡狭小的出租屋等下,听着伤感的音乐,喝着咖啡抽着烟。


这次的离职给了我很多的思考时间,对于这个行业、对于接下来的选择。


四月上旬


正式开始了失业,一直呆在家里,偶尔会在晚上出去跑步。互联网行情依旧是差得很,很多的招聘平台都是已读不回,当然还有很多奇葩的HR。加上AI大模型越来越多,甚至说阿里都要用AI来代替20%的人工工作,需要传统的程序员的岗位将越来越少。于是,我开始分析各种调查报告、行业报告。最后锁定在了这几个行业:新能源领域 新材料 AIGC 化工 电驱动工艺。虽然这段时间我也没有找到合适的行业的工作,但是我总结了一个找工作的铁律:跟着资本选行业


2023Q3国内小赛道股权投资势头:来源IT桔子


投资机构基本上都是嗅觉到最新的商机的,他们的资本注入将会给新行业、新领域或者企业带来新的活力。


四月中旬


在武汉跑了马拉松之后,我想回到家乡去也跑一下。这次我全程vlog记录了:2024十堰马拉松健康跑纪实。来回两天,时间的安排上也妥妥的。


回来之后的第二天,就要去体检了。不幸的是:我的胆囊出现了息肉,而且以不可控的速度在增长。因为这几年我对于我的身体都密切的关切,每年都会体检,息肉出现在我回武汉的这两年里。


拿着最近三年的检查结果,就往我最近的军医医院去。门诊的医生建议我直接做手术将它切除了。一下子慌了神,但是又在担心它的癌变,担心每次饭后的胸闷胸胀。一番纠结,最终我入院接受手术了。


我的病房和病床


第一次手术室和重症监护室,你是什么感觉?如果问我,我会说我很害怕,害怕到浑身颤抖,害怕到忍不住哭了。


换上病号才穿的衣服,我进入了手术室,躺在了手术台上,随着手术室的大门关上,看着手术室穿着手术服的医生,我整个身体忍不住的颤抖。我不能想象锋利的手术刀割开我的肚皮,然后找到我的胆,割下来,再缝合好。上高中的时候,我就经历了不打麻药直接用针线缝补伤口(当然,那是医疗条件不是很方便的时候)。解开衣服,闭上眼睛,不知道麻药怎么注入我的身体的。我在迷迷糊糊中没了知觉。


当我再次被叫醒的时候,是我的手术已经完成了。医生告诉我:“xxx,手术做完了,手术做的很顺利。”我被几个人抱上了监护室的病床上,我的伙伴也在我身边,我才知道手术了两个多小时。


在监护室哭了两小时,这是这么多年来第一次哭,而且哭的这么久。还好诺大的监护室只有我和一个监护的护士,不至于丢掉我男子汉大丈夫的面子。她耐心的帮我多次擦干泪水,说我的眼睛都红了。


后来的两天就都是输液了。不敢咳嗽,还好也有雾化的设备。没有了痛觉,因为脖子上的止痛泵帮我麻痹了神经。


四月下旬


最后也如期的出院了,每天除了按时吃清水面条+喝药+换药外,好像也没什么事情可以做的。屏蔽了朋友圈,很少看消息,坐累了就躺着休息。一次手术,几乎花光了我三年来的积蓄,再一次是那么的怕死惜命。想着被资本家誉为996福报、单休、PUA,觉得人命在资本家面前看起来是那么的卑贱。如果资本比不上别人,那就请你一定要健康。


闲暇的时间也会去看看纪录片,楼下散步走走。


《大秦赋》-始皇嬴政


看了《大秦赋》,当然我追剧是倒着看,想起了贾谊的《六国论》,有时间的话,会出一期文章。


还有大美的天山雪山、有趣的万物生灵。


雪山


黑颈鹤



觉得地球是那么的奇妙,生命是那么的奇妙,这么多的奇妙才是神秘、才是美好、才是丰富的存在。



这个月的经历,算得上是跌宕起伏。总之:希望每一个职场人或者是同行,一定要爱惜自己的身体,按时吃饭、早睡早起。不要畏惧失业,不为各种被贩卖的焦虑而焦虑,更多的是爱护自己,珍惜眼前,享受生活。


作者:shigen01
来源:juejin.cn/post/7360711693359988786
收起阅读 »

完了,安卓项目代码被误删了......

写在前面 这是一个朋友的经历记录了下来。朋友开发完了一个公司的app,过了一段时间,在清理电脑空间的时候把该app的项目目录给删了,突然公司针对该app提出了新的需求,这不完了?幸好有之前打包好的apk,所以可以通过逆向去弥补..... Apk文件结构 apk...
继续阅读 »

写在前面


这是一个朋友的经历记录了下来。朋友开发完了一个公司的app,过了一段时间,在清理电脑空间的时候把该app的项目目录给删了,突然公司针对该app提出了新的需求,这不完了?幸好有之前打包好的apk,所以可以通过逆向去弥补.....


Apk文件结构


apk的本质是压缩包,apk解压后会生成下列所示文件夹




  • Assets:存放的是不会被编译处理的文件。

  • Lib:存放的是一些so库,native库文件

  • META-INF:存放的是签名信息,用来保证apk的完整性和系统安全。防止被重新修改打包。

  • res:存放的资源文件,图片、字符串、颜色信息等

  • AndroidManifest.xml:是Android程序的配置文件,权限和配置信息

  • Classes.dex:Android平台下的字节码文件。

  • Resources.arcs:编译后的二进制资源文件,用来记录资源文件和资源ID的关系


逆向


这里用了逆向神器——jdax。支持命令行和图形化界面,地址如下:


github.com/skylot/jadx…


下载好之后,直接解压后打开exe,将apk文件拖入进去就可以,图形化界面,更方便搜索查看,可以看到下列文件夹



先看资源文件,asset存放的是静态资源文件,一般不会被压缩,但是会占用更多的安装包空间,res文件是由Android目录下的res进行压缩得到的,所以里面的文件直接解压打开会乱码,在这个工具里打开是正常的。



话不多说直接找回我的代码,找到我写的一个类,拷贝回去,补齐里面缺失的资源文件和一些新增的接口,跟着自己之前开发的流程,一步一步的找回去,发现其中局部变量在编译的时候都被进行了优化,以便缩小体积



找到我写的最核心的代码,发现被混淆了,我在代码里没有进行代码混淆配置,还是被一些工具给我进行了混淆,只能凭借着记忆去还原了。



终于进行了不到一天多的时间,把所有的代码还原了,然后自测通过。


代码混淆


现在其实也可以看到自己的程序是非常危险的,任何人拿到我的apk进行一个逆向就可以看到大概的逻辑。所以要在Android中进行代码混淆的配置。


项目中如果含有多个module时,在主app中设置了混淆其他module都会混淆,在build.gradle中配置下列代码 proguardFiles getDefaultProguardFile


android {
...
buildTypes {
release {
minifyEnabled true // 开启代码混淆
zipAlignEnabled true // 开启Zip压缩优化
shrinkResources true // 移除未被使用的资源
//混淆文件列表,混淆规则配置
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
...
}

这里代表的是混淆文件,我们在项目里找到proguard-rules.pro,这里就是混淆规则,规定了哪些代码进行混淆,哪些不进行混淆。混淆规则一般有以下几点:



  • 混淆规则,等级、预校验、混淆算法等

  • 第三方库

  • 自定义类、控件

  • 本地的R类

  • 泛型 注解 枚举类等


示例配置如下:



#压缩等级,一般选择中间级别5
-optimizationpasses 5
#包名不混合大小写
-dontusemixedcaseclassnames
#不去忽略非公共的库类
-dontskipnonpubliclibraryclasses
#优化 不优化输入的类文件
-dontoptimize
#预校验
-dontpreverify
#混淆时采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#保护注解
-keepattributes *Annotation*
#保持下面的类不被混淆(没有用到的可以删除掉,比如没有用到service则可以把service行删除)
-keep public class * extends android.app.Fragment
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.preference.Preference
-keep public class * extends android.support.v4.app.FragmentActivity
-keep public class * extends android.support.** { *;}
#如果引用了v4或者v7包
-dontwarn android.support.*
#忽略警告(开始应该注释掉,让他报错误解决,最后再打开,警告要尽量少)
-ignorewarnings
#####################记录生成的日志数据,gradle build时在本项目根目录输出################
#混淆时是否记录日志
-verbose
#apk 包内所有class 的内部结构
-dump class_files.txt
#为混淆的类和成员
-printseeds seeds.txt
#列粗从 apk 中删除的代码
-printusage unused.txt
#混淆前后的映射
-printmapping mapping.txt
#####################记录生成的日志数据,gradle build时在本项目根目录输出结束################

#本地的R类不要被混淆,不然就找不到相应的资源
-keep class **.R$*{ public static final int *; }

#保持内部类,异常类
-keepattributes Exceptions, InnerClasses
#保持泛型、注解、源代码之类的不被混淆
-keepattributes Signature, Deprecated, SourceFile
-keepattributes LineNumberTable, *Annotation*, EnclosingMethod

#保持自定义控件不被混淆(没有就不需要)
-keepclasseswithmembers class * extends android.app.Activity{
public void *(android.view.View);
}
-keepclasseswithmembers class * extends android.supprot.v4.app.Fragment{
public void *(android.view.View);
}
#保持 Parcelable 不被混淆(没有就不需要)
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#保持 Serializable 不被混淆(没有就不需要)
-keepnames class * implements java.io.Serializable

-keepclassmembers class * {
public void *ButtonClicked(android.view.View);
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

再次打包,然后打开apk后就会发现包名类名变量名都变得很奇怪。




这样代码混淆就完成了。


作者:银空飞羽
来源:juejin.cn/post/7360903734853730356
收起阅读 »

JavaScript注释:单行注释和多行注释详解

为了提高代码的可读性,JS与CSS一样,也提供了注释功能。JS中的注释主要有两种,分别是单行注释和多行注释。在编程的世界里,注释是那些默默无闻的英雄,它们静静地站在代码的背后,为后来的维护者、为未来的自己,甚至是为那些偶然间翻阅你代码的开发者提供着不可或缺的信...
继续阅读 »

为了提高代码的可读性,JS与CSS一样,也提供了注释功能。JS中的注释主要有两种,分别是单行注释和多行注释。

在编程的世界里,注释是那些默默无闻的英雄,它们静静地站在代码的背后,为后来的维护者、为未来的自己,甚至是为那些偶然间翻阅你代码的开发者提供着不可或缺的信息。

Description


今天,我们就来深入探讨JavaScript中的注释,让我们的代码不仅能够运行,还能够“说话”。

一、什么是JavaScript注释

JavaScript注释是用来解释代码的,不会被浏览器执行。它们可以帮助其他开发者理解代码的功能和目的。

注释就像是给代码穿上了一件华丽的外衣,让我们的代码更加优雅、易读。如下图中的例子所示:


Description


在JavaScript中,有两种类型的注释:单行注释和多行注释。下面分别讲解这两种注释的含义和使用。


二、JavaScript注释的种类

1、单行注释

单行注释: 使用两个斜杠(//)开头,后面的内容直到该行结束都被视为注释。例如:

// 这是一个单行注释
console.log("Hello, World!"); // 这也是一个单行注释

它适用于简短的注释,比如对某一行代码的快速说明。

2、多行注释

多行注释: 使用斜杠星号(/)开头,星号斜杠(/)结尾,中间的内容都被视为注释。

例如:

/*
这是一个多行注释
可以跨越多行
*/

console.log("Hello, World!");

这种注释可以跨越多行,适合用于函数描述、复杂的算法解释或者临时屏蔽代码块。

注意: 在HTML文件中,如果需要将JavaScript代码嵌入到<script>标签中,可以使用以下方法来添加多行注释:

<script>
<!--
这是一个多行注释
可以跨越多行
-->

console.log("Hello, World!");
</script>


三、JavaScript注释的作用

1、解释代码功能:

通过注释,我们可以解释代码的功能和作用,让其他程序员更容易理解我们的代码。

// 这是一个求和函数
function sum(a, b) {
return a + b;
}

2、 标记代码状态:

我们可以使用注释来标记代码的状态,例如TODO、FIXME等,提醒自己或其他程序员注意这些问题。

// TODO: 优化这个函数的性能
function slowFunction() {
// ...
}

3、临时禁用代码:

当我们需要暂时禁用某段代码时,可以使用注释将其包裹起来,而不是直接删除。

// function oldFunction() {
// // ...
// }


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


四、如何写好注释

注释虽好,但过多或不当的注释反而会成为阅读代码的障碍。我们在写注释时也要注意以下几点:

  • 简洁明了: 注释应该简单明了,能够快速传达信息。

  • 适当使用: 不要过度使用注释,只有在必要的时候才添加。

  • 保持一致: 在团队开发中,要遵循统一的注释规范,以便于团队成员之间的沟通。

  • 适时更新: 随着代码的变更,记得更新相关的注释。

JavaScript注释是我们编程过程中的得力助手,它们不仅能够帮助我们更好地理解代码,还能提高代码的可读性和可维护性。让我们一起学会使用注释,让我们的代码更加精彩!

收起阅读 »

记一次划线需求的实现方式

web
1 背景 1.1 需求背景 前半年接了一个划线需求,一直没空总结下。微信公众号和微信读书等读书类应用都有此功能,但前者只对部分用户开放了,并且没有长期运营。 这次只谈下划线技术实现本身。 1.2 功能详叙 用户可以对文章句子进行长按选区,过程中弹出面板,且面...
继续阅读 »

1 背景


1.1 需求背景


前半年接了一个划线需求,一直没空总结下。微信公众号和微信读书等读书类应用都有此功能,但前者只对部分用户开放了,并且没有长期运营。


这次只谈下划线技术实现本身。


1.2 功能详叙



  1. 用户可以对文章句子进行长按选区,过程中弹出面板,且面板位置动态变化,点击点赞按钮后生成划线;

  2. 点击划线句子默认选中,并弹出面板,显示所点句子点赞量;

  3. 划线句子可以合并,规则是选取句子和已赞过的句子有交叉时合并为一条新的首尾更长的句子,选取的句子被包含在已赞过的句子中时显示点赞量,选取句子包含了已赞句子则删掉已赞句子并对新句子点赞量加一;

  4. 点赞量超过3的句子才外显;

  5. 他人的划线句子用虚线展示,自己的划线用实线展示;

  6. 小流量,用户量由小至大,过程中可以对外显策略微调;


1.3 竞品


可以看到,微信公众号划线过程会弹出一个灰色面板,面板上有划线(这次需求改为了点赞)按钮:


image.png
image.png


2 关键逻辑


这个需求乍看可能会觉得没那么复杂,但细细分析后会发现有较长的交互流程和逻辑链:


image.png


其中有几个会影响整体逻辑的关键点需要关注:



  1. 渲染划线的方式:插入 dom 标签还是绝对定位或其他方式;

  2. 监听划线选取的交互事件选择 selectionchange 还是 touchend;

  3. 整个交互过程分为哪些部分;

  4. 怎么判断新划线和其他划线的位置关系,怎么合并或删除;

  5. 数据结构怎么设计;

  6. 怎么将划线序列化;

  7. 怎么将数据反序列化成划线;


3 详细设计


3.1获取划线


window 上提供了 Selection 对象,它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。调用 Selection.toString() 方法会返回被选中区域中的纯文本。


var selObj = window.getSelection();
var range = selObj.getRangeAt(0);

Selection 对象所对应的是用户所选择的 ranges(区域)


var selObj = window.getSelection();
var range = selObj.getRangeAt(0);

3.2 划线渲染方式


渲染划线有两种方式:


1 在划线range对象的首尾dom的位置,插入线段的dom标签;
优点:划线的点击不需要计算点击位置,直接在插入dom上绑定事件即可;
缺点:对原页面结构有入侵,改变了dom结构,可能引发其他问题;


2 绝对定位,相对于整篇文章;
优点:完全增量,对原页面没有入侵;
缺点:需要计算点击位置;


我选择的第二种,原因是为了不影响原有页面逻辑,这样项目风险也是最小的。那么具体怎么实现呢?


range对象提供了一个 getClientRects 方法,表示 range 在屏幕上所占的区域。这个列表相当于汇集了范围中所有元素调用 Element.getClientRects() 方法所得的结果。用拿到的位置信息进行绝对定位即可。


rectList = range.getClientRects()

我们把用户所有划线range对象和其产生的位置信息都存入到一个list中。


pageRangeList.push({
range,
rectInfo
})

3.3 交互过程


我们分析下整个交互过程:
有两个主要的交互事件,一是点击划线,二是滑动选区。


3.3.1 点击事件


处理点击事件,我们拿到点击事件的位置,和存放的 pageRangeList 进行位置比较,得出用户点击的是哪个range对象。


// 点击事件
const {pageX, pageY} = event;
const lineHeight = 23;

const {range} = rectInfo.some(rect => {
const {left, right, realY} = rect;
return pageX < left && pageX > left && pageY > realY
})

this.selection.removeAllRanges();
this.selection.addRange();

3.3.2 选区事件


选区事件我选择的是 selectionchange,需要加防抖和节流处理。


如果你选的是 touchend 安卓系统会点问题。


3.3.3 比较位置关系


如第2点核心逻辑中所说,在滑词过程中,需要比较位置关系,我们直接使用Range.compareBoundaryPoints方法即可。返回值 0 、-1 、1 分别代表不同的位置关系。


const compare = range.compareBoundaryPoints(Range.START_TO_END, sourceRange);

3.4 序列化与反序列化


序列化是整个需求的重点,序列化是指将交互产生的划线转化成某种数据结构能存储在服务器上,反序列化是指如何将server下发的序列化数据转化成非序列化的划线。


两者是两个相反的过程,当我们确定了序列化方案,其实也就知道了反序列化了。


3.4.1 序列化


方案一,识别段落


刚开始我观察文章都会拆分段落,如按P标签或某一个class类名来划分段落,于是计划用段落信息,告诉 server 划线在第几段的第几个字。


interface data {
startParagraph: 1,
startIndex: 22,
endParagraph: 2,
endIndex: 15
}

但后来发现有一些抓取的文章根本内容很混乱,且没有特定的段落,强行识别复杂度极高。(如下图)所以此方案不可行。


image.png


方案二,全文第几个字


前面的方案不可能的原因是,识别段落信息复杂度不可控,那么我们可以绕过段落信息,去识别全文第几个字。


interface data {
startCharacters: 122,
endCharacters: 166
}

具体方式是用Range,圈选文章开头到当前dom,形成一个新Range,再调用range.toString查看字数即可。


const range = new Range();
range.setStart(pageContainer, 0);
range.setEnd(curEndContainer, endOffset);
const str = range.toString();

3.4.2 反序列化


这里注意,由于 Javascript 在大多宿主环境下没有递归的尾调用优化,所以我采用了手动创建栈来进行 dfs:


    dfs({
node = this.content,
}) {
const stack = [];
if (!node) {
return;
}

stack.push(node);

while (stack.length) {
const item = stack.pop();

const children = item.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
stack.push( [i]);
}
}
}

作者:雨默默下了一整夜
来源:juejin.cn/post/7344993022075813938
收起阅读 »

⚡聊天框 - 微信加载历史数据的效果原来这样实现的

web
前言 我记得2021年的时候做过聊天功能,那时业务也只限微信小程序 那时候的心路历程是: 卧槽,让我写一个聊天功能这么高大上?? 嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧 然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,...
继续阅读 »

前言


我记得2021年的时候做过聊天功能,那时业务也只限微信小程序


那时候的心路历程是:



卧槽,让我写一个聊天功能这么高大上??


嗯?这么简单,不就画画页面来个轮询吗,加个websocket也还行吧


然后,卧槽?这查看历史聊天记录什么鬼,页面闪一下不太好啊,真的能做到微信的那种效果吗



然后一堆调研加测试,总算在小程序中查看历史记录没那么鬼畜了,但是总是感觉不是最佳解决方案。



那时打出的子弹,一直等到现在击中了我



最近又回想到了这个痛点,于是网上想看看有没有大佬发解决方案,结果还真被我找到了。


image.png


正文开始


1,效果展示


上才艺~~~


222.gif


2,聊天页面


2.1,查看历史聊天记录的坑


常规写法加载历史记录拼接到聊天主体的顶部后,滚动条会回到顶部、不在原聊天页面


直接上图


111.gif


而我们以往的解决方案也只是各种利用缓存scroll的滚动定位把回到顶部的滚动条重新拉回加载历史记录前的位置,好让我们可以继续在原聊天页面。


但即使我们做了很多优化,也会有安卓和苹果部分机型适配问题,还是不自然,可能会出现页面闪动


其实吧,解决方案只有两行css代码~~~


2.2,解决方案:flex神功


想优雅顺滑的在聊天框里查看历史记录,这两行css代码就是flex的这个翻转属性


dispaly:flex;
flex-direction: column-reverse

灵感来源~~~


333.gif


小伙伴可以看到,在加载更多数据时



滚动条位置没变、加载数据后还是原聊天页面的位置



这不就是我们之前的痛点吗~~~


所以,我们只需要翻转位置,用这个就可以优雅流畅的实现微信的加载历史记录啦


flex-direction: column-reverse


官方的意思:指定Flex容器中子元素的排列方向为列(从上到下),并且将其顺序反转(从底部到顶部)


如果感觉还是抽象,不好理解的话,那就直接上图,不加column-reverse的样子


image.png


加了column-reverse的样子


image.png


至此,我们用column-reverse再搭配data数据的位置处理就完美解决加载历史记录的历史性问题啦


代码放最后啦~~~


2.3,其他问题


2.3.1,数据过少时第一屏展示


因为用了翻转,数据少的时候会出现上图的问题


只需要.mainArea加上height:100%


然后额外写个适配盒子就行


flex-grow: 1; 
flex-shrink: 1;

image.png


2.3.2,用了scroll-view导致的问题


这一part是因为我用了uniappscroll-view组件导致的坑以及解决方案,小伙伴们没用这个组件的可忽略~~~


如下图,.mainArea使用了height:100%后,继承了父级高度后scroll-view滚动条消失了。


image.png


.mainArea去掉height:100%后scroll-view滚动条出现,但是第一屏数据过多时不会滚动到底部展示最新信息


image.png


解决方案:第一屏手动进行滚动条置顶


scrollBottom() {
if (this.firstLoad) return;
// 第一屏后不触发
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query
.select("#mainArea")
.boundingClientRect((data) => {
console.log(data);
if (data.height > +this.chatHeight) {
this.scrollTop = data.height; // 填写个较大的数
this.firstLoad = true;
}
})
.exec();
});
},

3,服务端


使用koa自己搭一个websocket服务端


3.1 服务端项目目录


image.png


package.json


{
"name": "websocketapi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.14.2",
"koa-router": "^12.0.1",
"koa-websocket": "^7.0.0"
}
}


koa-tcp.js


const koa = require('koa')
const Router = require('koa-router')
const ws = require('koa-websocket')

const app = ws(new koa())
const router = new Router()

/**
* 服务端给客户端的聊天信息格式
* {
id: lastid,
showTime: 是否展示时间,
time: nowDate,
type: type,
userinfo: {
uid: this.myuid,
username: this.username,
face: this.avatar,
},
content: {
url:'',
text:'',
w:'',
h:''
},
}
消息数据队列的队头为最新消息,以次往下为老消息
客户端展示需要reverse(): 客户端聊天窗口最下面需要为最新消息,所以队列尾部为最新消息,以此往上为老消息
*/



router.all('/websocket/:id', async (ctx) => {
// const query = ctx.query
console.log(JSON.stringify(ctx.params))
ctx.websocket.send('我是小服,告诉你连接成功啦')
ctx.websocket.on('message', (res) => {
console.log(`服务端收到消息, ${res}`)
let data = JSON.parse(res)
if (data.type === 'chat') {
ctx.websocket.send(`我也会说${data.text}`)
}
})
ctx.websocket.on('close', () => {
console.log('服务端关闭')
})
})

// 将路由中间件添加到Koa应用中
app.ws.use(router.routes()).use(router.allowedMethods())

app.listen(9001, () => {
console.log('socket is connect')
})



切到server目录yarn


然后执行nodemon koa-tcp.js


没有nodemon的小伙伴要装一下


image.png


代码区


完整项目Github传送门


聊天页面的核心代码如下(包含data数据的位置处理和与服务端联动)



完结


这篇文章我尽力把我的笔记和想法放到这了,希望对小伙伴有帮助。


到这里,想给小伙伴分享两句话



现在搞不清楚的事,不妨可以先慢下来,不要让自己钻到牛角尖了


一些你现在觉得解决不了的事,可能需要换个角度



欢迎转载,但请注明来源。


最后,希望小伙伴们给我个免费的点赞,祝大家心想事成,平安喜乐。


image.png


作者:尘落笔记
来源:juejin.cn/post/7337114587123335180
收起阅读 »

基于装饰器——我劝你不要在业务代码上装逼!!!

web
基于装饰器——我劝你不要在业务代码上装逼!!! 装饰器模式的定义 在传统的面向对象语言中,给对象添加功能常使用继承的方式,但继承的方式并不灵活,会带来一些许多问题,如:超类和子类存在强耦合性,也就是说当改变超类时,子类也需要改变。 而装饰器模式的出现改变的这...
继续阅读 »

基于装饰器——我劝你不要在业务代码上装逼!!!


装饰器模式的定义



  • 在传统的面向对象语言中,给对象添加功能常使用继承的方式,但继承的方式并不灵活,会带来一些许多问题,如:超类和子类存在强耦合性,也就是说当改变超类时,子类也需要改变。

  • 而装饰器模式的出现改变的这种方式,装饰器模式可在不改变现有对象解构的基础上,动态地为对象添加功能


传统的 JavaScript 装饰器


var plane = {
fire: function () {
console.log("普通子弹");
},
};

var missleDecorator = function () {
console.log("发射导弹");
};

var atomDecorator = function () {
console.log("发射原子弹");
};

var fire1 = plane.fire;
plane.fire = function () {
fire1();
missleDecorator();
};

var fire2 = plane.fire;
plane.fire = function () {
fire2();
atomDecorator();
};

plane.fire();
/**
普通子弹
发射导弹
发射原子弹
*/



装饰函数



  • 在 JavaScript 中,几乎一切都是对象,其中函数也被成为对象,在平时的开发中,我们都在和函数打交道。在给对象扩展属性和方法时,很难在不改动原功能函数的情况下,给函数添加一些额外的功能,最直接的粗暴方式就是直接改写函数,但这是最差的方式,这违反了开放——封闭原则。

  • 如下:


function a(){
console.log(1);
}

// 改写:
function a(){
console.log(1);

// 新功能
console.log(2);
}



  • 很多时候,我们都不想去触碰之前的一些代码,但需要添加功能,所以如果需要在不改变原功能函数的情况下,给函数添加功能。可使用以下方式:

  • 要想完美的给函数添加功能,可使用 AOP 来装饰函数

    • AOP:一种编程规范,通过将关注点从主业务逻辑中剥离出来并单独处理,以此来提高代码的可读性和重用性。



  • 如下:


Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}

// before 和 after 函数都接收一个函数作为参数,这个函数也就是新添加的函数(里面也就是要添加的新功能逻辑)。
// 而before 和 after 函数区别在于在是原函数之前执行还是之后执行。



  • AOP 函数的使用


Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}

var o1 = function(){
console.log('1');
}
var o2 = function(){
console.log('2');
}
var o3 = function(){
console.log('3');
}

var desctor = o1.after(o2);
desctor = desctor.after(o3);
desctor(); // 1 2 3
/**
var desctor = o1.after(o2);
desctor = desctor.after(o3);
desctor();
1
2
3

var desctor = o1.before(o2);
desctor = desctor.before(o3);
desctor();
3
2
1

var desctor = o1.after(o2);
desctor = desctor.before(o3);
desctor();
3
1
2


var desctor = o1.before(o2);
desctor = desctor.after(o3);
desctor();
2
1
3
*/



AOP的应用


1.数据上报



  • 在程序开发中,当业务代码开发完后,在结尾时需要加很多的日志上报的代码,普遍我们会去改已经之前封装好的功能函数。其实这并不是一个好的方式,那如何在不直接修改之前函数的基础上添加日志上报功能呢?

  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP日志上报</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<button class="btn" @click="handler">Button</button>
<p id="tt">{{message}}</p>
</div>
</body>
</html>
<script type="text/javascript">
// log report
const { reactive, ref, createApp } = Vue;
const app = createApp({
setup() {
const message = ref("未点击");
const count = ref(0);

Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};

function handler() {
message.value = `已点击${++count.value}`;
}

handler = handler.after(log);

function log() {
message.value = message.value + "-----> log reported";
console.log("log report");
}

return {
message,
handler,
};
},
});
app.mount("#app");
</script>


2.动态参数



  • 在日常开发中,我们需要向后台接口发送请求来获取信息,例如传参如下。业务在后续时需要添加新参数,每个接口需要把 token 值也一并传过去, 普遍我们会去改封装的请求方法,把 token 参数添加进去。但我们直接修改封装好的请求方法不是好的行为,那我们可使用上面说过的 AOP 方式来改进。


{
name: 'xxxx',
password: 'xxxx',
}



  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP动态参数</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">{{message}}</div>
</body>
</html>

<script type="text/javascript">
const { reactive, ref, createApp } = Vue;
const app = createApp({
setup() {
const message = ref("empty params");
Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
beforeFn.apply(this, arguments);
return _self.apply(this, arguments);
};
};

Function.prototype.after = function (afterFn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};

function ajax(type, url, params){
message.value = `${type} ----> ${url} -----> ${JSON.stringify(params)}`;
}

function getToken(){
// do something
return 'token';
}

ajax = ajax.before(function(type, url, params){
params.token = getToken();
})

ajax('get', 'https://www.baidu.com/userinfo', {name: 'se', password: 'xsdsd'});
return {
message,
};
},
});
app.mount("#app");
</script>


3.表单校验



  • 在日常开发中,我们经常要去做校验表单数据,通常的方式是在功能函数中进行判断处理或将判断逻辑提取为一个函数的方式。但这种方式其实是与功能性函数相混合,且校验逻辑与功能性函数有耦合关系。那我们可使用 AOP 方式来改进。

  • 如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AOP表单验证</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/vue@3.2.20/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<label>
姓名:
<input
type="text"
v-model="data.name"
placeholder="请输入姓名"
/>

</label>
<label>
密码:
<input
type="text"
v-model="data.pass"
placeholder="请输入密码"
/>

</label>
<p v-if="data.name || data.pass">{{data.name + '/' + data.pass}} ----after-----> {{data.message}}</p>
<hr>
<button @click="submitBtn">submit</button>
</div>
</body>
</html>

<script type="text/javascript">
const { reactive, ref, createApp, watchEffect } = Vue;
const app = createApp({
setup() {
const data = reactive({
name: "",
pass: "",
message: "",
});

Function.prototype.before = function (beforeFn) {
var _self = this;
return function () {
if (beforeFn.apply(this, arguments) === false) return;
return _self.apply(this, arguments);
};
};

function valid() {
if (!data.name || !data.pass) {
alert("用户名或密码不能为空");
return false;
}
}

function formSubmit() {
console.log("data ------>", data);
data.message = `${data.name} ------- ${data.pass}`;
}

formSubmit = formSubmit.before(valid);

function submitBtn() {
formSubmit();
}
return {
data,
submitBtn,
};
},
});
app.mount("#app");
</script>


装饰器模式的优缺点



  • 优点:

    1. 扩展性强:装饰器模式允许在不修改现有代码的情况下,动态地添加新功能或修改现有功能。通过使用装饰器,可以在运行时按需组合和堆叠装饰器对象,实现各种组合方式,从而实现更多的功能扩展。

    2. 遵循开闭原则:装饰器模式通过添加装饰器类来扩展功能,而不是修改现有的代码。这样可以保持原有代码的稳定性,符合开闭原则,即对扩展开放,对修改关闭。

    3. 分离关注点:装饰器模式将功能的扩展和核心功能分离开来,每个装饰器类只关注单一的额外功能。这样可以使代码更加清晰、可读性更高,并且容易维护和测试。



  • 缺点:

    1. 增加复杂性:使用装饰器模式会增加额外的类和对象,引入了更多的复杂性和层次结构。这可能使代码变得更加复杂,理解和调试起来可能更加困难。

    2. 潜在的性能影响:由于装饰器模式涉及多个对象的组合和堆叠,可能会引入额外的运行时开销,对性能产生一定的影响。尤其是当装饰器链较长时,可能会导致性能下降。




装饰器模式的适用场景



  1. 动态地扩展对象功能:当需要在运行时动态地为对象添加额外的功能或责任时,装饰器模式是一个很好的选择

  2. 遵循开闭原则:如果你希望在不修改现有代码的情况下扩展功能,而且要保持代码的稳定性,装饰器模式是一个合适的解决方案。

  3. 分离关注点:当你希望将不同的功能分离开来,使每个功能都有自己独立的装饰器类时,装饰器模式是有用的。每个装饰器只关注单一的额外功能,这样可以使代码更加清晰、可读性更高,并且容易维护和测试。

  4. 多层次的功能组合:如果你需要实现多个功能的组合,而且每个功能都可以灵活选择是否添加,装饰器模式可以很好地满足这个需求。通过堆叠多个装饰器对象,可以按照特定的顺序组合功能,实现各种组合方式。

  5. 继承关系的替代方案:当你面临类似于创建大量子类的情况时,装饰器模式可以作为继承关系的替代方案。通过使用装饰器模式,可以避免创建过多的子类,而是通过组合不同的装饰器来实现不同的功能组合。


Tip: 文章部分内容参考于曾探大佬的《JavaScript 设计模式与开发实践》。文章仅做个人学习总结和知识汇总

作者:南囝coding
来源:juejin.cn/post/7272869799960559679
收起阅读 »

还在封装 xxxForm,xxxTable 残害你的同事?试试这个工具

web
之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。 多图预警。。。 以管理后台一个列表页为例 选择对应的模板 截图查询区域,使用 OCR 初始...
继续阅读 »

之前写过一篇文章 我理想中的低代码开发工具的形态,已经吐槽了各种封装 xxxForm,xxxTable 的行为,这里就不啰嗦了。今天再来看看我的工具达到了什么程度。


多图预警。。。


以管理后台一个列表页为例



选择对应的模板



截图查询区域,使用 OCR 初始化查询表单的配置



截图表头,使用 OCR 初始化 table 的配置



使用 ChatGPT 翻译中文字段



生成代码



效果


目前我们没有写一行代码,就已经达到了如下的效果



下面是一部分生成的代码


import { reactive, ref } from 'vue'

import { IFetchTableListResult } from './api'

interface ITableListItem {
/**
* 决算单状态
*/

settlementStatus: string
/**
* 主合同编号
*/

mainContractNumber: string
/**
* 客户名称
*/

customerName: string
/**
* 客户手机号
*/

customerPhone: string
/**
* 房屋地址
*/

houseAddress: string
/**
* 工程管理
*/

projectManagement: string
/**
* 接口返回的数据,新增字段不需要改 ITableListItem 直接从这里取
*/

apiResult: IFetchTableListResult['result']['records'][0]
}

interface IFormData {
/**
* 决算单状态
*/

settlementStatus?: string
/**
* 主合同编号
*/

mainContractNumber?: string
/**
* 客户名称
*/

customerName?: string
/**
* 客户手机号
*/

customerPhone?: string
/**
* 工程管理
*/

projectManagement?: string
}

interface IOptionItem {
label: string
value: string
}

interface IOptions {
settlementStatus: IOptionItem[]
}

const defaultOptions: IOptions = {
settlementStatus: [],
}

export const defaultFormData: IFormData = {
settlementStatus: undefined,
mainContractNumber: undefined,
customerName: undefined,
customerPhone: undefined,
projectManagement: undefined,
}

export const useModel = () => {
const filterForm = reactive<IFormData>({ ...defaultFormData })

const options = reactive<IOptions>({ ...defaultOptions })

const tableList = ref<(ITableListItem & { _?: unknown })[]>([])

const pagination = reactive<{
page: number
pageSize: number
total: number
}>({
page: 1,
pageSize: 10,
total: 0,
})

const loading = reactive<{ list: boolean }>({
list: false,
})

return {
filterForm,
options,
tableList,
pagination,
loading,
}
}

export type Model = ReturnType<typeof useModel>


这就是用模板生成的好处,有规范,随时可以改,而封装 xxxForm,xxxTable 就是一个黑盒。


原理


下面大致说一下原理



首先是写好一个个模版,vscode 插件读取指定目录下模版显示到界面上



每个模版下可能包含如下内容:



选择模版后,进入动态表单配置界面



动态表单是读取 config/schema.json 里的内容进行动态渲染的,目前支持 amis、form-render、formily



配置表单是为了生成 JSON 数据,然后根据 JSON 数据生成代码。所以最终还是无法避免的使用私有的 DSL ,但是生成后的代码是没有私有 DSL 的痕迹的。生成代码本质是 JSON + EJS 模版引擎编译 src 目录下的 ejs 文件。


为了加快表单的配置,可以自定义脚本进行操作



这部分内容是读取 config/preview.json 内容进行显示的



选择对应的脚本方法后,插件会动态加载 script/index.js 脚本,并执行里面对应的方法



以 initColumnsFromImage 方法为例,这个方法是读取剪贴板里的图片,然后使用百度 OCR 解析出文本,再使用文本初始化表单


initColumnsFromImage: async (lowcodeContext) => {
context.lowcodeContext = lowcodeContext;
const res = await main.handleInitColumnsFromImage();
return res;
},

export async function handleInitColumnsFromImage() {
const { lowcodeContext } = context;
if (!lowcodeContext?.clipboardImage) {
window.showInformationMessage('剪贴板里没有截图');
return lowcodeContext?.model;
}
const ocrRes = await generalBasic({ image: lowcodeContext!.clipboardImage! });
env.clipboard.writeText(ocrRes.words_result.map((s) => s.words).join('\r\n'));
window.showInformationMessage('内容已经复制到剪贴板');
const columns = ocrRes.words_result.map((s) => ({
slot: false,
title: s.words,
dataIndex: s.words,
key: s.words,
}));
return { ...lowcodeContext.model, columns };
}

反正就是可以根据自己的需求定义各种各样的脚本。比如使用 ChatGPT 翻译 JSON 里的指定字段,可以看我的上一篇文章 TypeChat、JSONSchemaChat实战 - 让ChatGPT更听你的话


再比如要实现把中文翻译成英文,然后英文使用驼峰语法,这样就可以将中文转成英文代码变量,下面是实现的效果



选择对应的命令菜单后 vscode 插件会加载对应模版里的脚本,然后执行里面的 onSelect 方法。



main.ts 代码如下


import { env, window, Range } from 'vscode';
import { context } from './context';

export async function bootstrap() {
const clipboardText = await env.clipboard.readText();
const { selection, document } = window.activeTextEditor!;
const selectText = document.getText(selection).trim();
let content = await context.lowcodeContext!.createChatCompletion({
messages: [
{
role: 'system',
content: `你是一个翻译家,你的目标是把中文翻译成英文单词,请翻译时使用驼峰格式,小写字母开头,不要带翻译腔,而是要翻译得自然、流畅和地道,使用优美和高雅的表达方式。请翻译下面用户输入的内容`,
},
{
role: 'user',
content: selectText || clipboardText,
},
],
});
content = content.charAt(0).toLowerCase() + content.slice(1);
window.activeTextEditor?.edit((editBuilder) => {
if (window.activeTextEditor?.selection.isEmpty) {
editBuilder.insert(window.activeTextEditor.selection.start, content);
} else {
editBuilder.replace(
new Range(
window.activeTextEditor!.selection.start,
window.activeTextEditor!.selection.end,
),
content,
);
}
});
}


使用了 ChatGPT。


再来看看,之前生成管理后台 CURD 页面的时候,连 mock 也一起生成了,主要逻辑放在了 complete 方法里,这是插件的一个生命周期函数。



因为 mock 服务在另一个项目里,所以需要跨目录去生成代码,这里我在 mock 服务里加了个接口返回 mock 项目所在的目录


.get(`/mockProjectPath`, async (ctx, next) => {
ctx.body = {
status: 200,
msg: '',
result: __dirname,
};
})

生成代码的时候请求这个接口,就知道往哪个目录生成代码了


const mockProjectPathRes = await axios
.get('http://localhost:3001/mockProjectPath', { timeout: 1000 })
.catch(() => {
window.showInformationMessage(
'获取 mock 项目路径失败,跳过更新 mock 服务',
);
});
if (mockProjectPathRes?.data.result) {
const projectName = workspace.rootPath
?.replace(/\\/g, '/')
.split('/')
.pop();
const mockRouteFile = path.join(
mockProjectPathRes.data.result,
`${projectName}.js`,
);
let mockFileContent = `
import KoaRouter from 'koa-router';
import proxy from '../middleware/Proxy';
import { delay } from '../lib/util';

const Mock = require('mockjs');

const { Random } = Mock;

const router = new KoaRouter();
router{{mockScript}}
module.exports = router;
`
;

if (fs.existsSync(mockRouteFile)) {
mockFileContent = fs.readFileSync(mockRouteFile).toString().toString();
const index = mockFileContent.lastIndexOf(')') + 1;
mockFileContent = `${mockFileContent.substring(
0,
index,
)}
{{mockScript}}\n${mockFileContent.substring(index)}`
;
}
mockFileContent = mockFileContent.replace(/{{mockScript}}/g, mockScript);
fs.writeFileSync(mockRouteFile, mockFileContent);
try {
execa.sync('node', [
path.join(
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'/node_modules/eslint/bin/eslint.js',
),
mockRouteFile,
'--resolve-plugins-relative-to',
mockProjectPathRes.data.result
.replace(/\\/g, '/')
.replace('/src/routes', ''),
'--fix',
]);
} catch (err) {
console.log(err);
}

mock 项目也可以通过 vscode 插件快速创建和使用



上面展示的模版都放在了 github.com/lowcode-sca… 仓库里,照着 README 步骤做就可以使用了。


作者:若邪
来源:juejin.cn/post/7315242945454735414
收起阅读 »

精美绝伦:小程序日历组件推荐

web
前言众所周知,小程序的渲染性能一直被广大开发者诟病,2023年中旬,小程序团队正式发布了 skyline 渲染引擎,Skyline,旨在替代 WebView 作为小程序的渲染层,以提供更优秀的渲染性能和诸多增强特性,让小程序能达到原生的体验...
继续阅读 »

前言

众所周知,小程序的渲染性能一直被广大开发者诟病,2023年中旬,小程序团队正式发布了 skyline 渲染引擎,Skyline,旨在替代 WebView 作为小程序的渲染层,以提供更优秀的渲染性能和诸多增强特性,让小程序能达到原生的体验。

非常好,那么就是说我们可以在小程序上体验类原生的特性啦!这下谁敢再说小程序是屎?

尝试

在用了一段时间,主要尝试了canvas、手势组件动画等功能,惊奇的发现,小程序做的这个 skyline 渲染引擎,是一陀超大的屎。

噢!腾讯,你小子好啊,研究了这么长时间,跑我这排宿便了是吧?

image.png

image.png

自己写的样式和iconfont样式给我报了很多坨警告,能用吗?能用,但是我是屎我需要恶心你,我必须得给你点警告。

除了控制台脏了之外,还有各种各样数不清的 bug。比如,地图的bindregionchange失效,而你去论坛发,他们只会说:"未复现"、"写片段"、"你试试",发文时实测依然没有修复😅。

爱莲说

铺垫了这么多,实属无奈,我也不想说这么多,只是这口屎憋在嘴里,臭的难受。我本以为出淤泥而不染已经很难得了,没想到在这屎坑里还有大佬栽培了一朵精美绝伦的白莲花,它就是 lspriv/wx-calendar ,github链接:github.com/lspriv/wx-c…

看到这么牛逼的组件,只有区区一百来个 star。

牛逼不牛逼,直接看效果:

QQ2024422-123019.webp

它还同时支持 skyline 和 webview 下渲染。

image.png

每个场景都是丝滑过渡的,元素到元素的联合动画。看的出来,这个日历是有很重的 MIUI 风格的,如果不是右上角的小程序胶囊,我甚至以为是某手机的自带日历。

QQ2024422-14944.webp

依赖 skyline 的 worklet 动画,组件做到了跟手、丝滑,且符合直觉的动画。

lspriv/wx-calendar 使用

lspriv/wx-calendar 需要使用npm下载并构建,然后引入组件使用。

npm i @lspriv/wx-calendar

然后需要使用微信开发者工具构建 npm

{
   "usingComponents": {
       "calendar": "@lspriv/wx-calendar"
  }
}
id="calendar" bindload="handleLoad" />

生态

作者十分聪明,给 lspriv/wx-calendar 预留了插件接口,开发者可以根据自身需求,写扩展功能。

源码中 src>plugins>lunar.ts 是一个内置插件,实现了农历、节气、闰年等功能。

目前为止,还没有看到有第二个人为作者贡献插件。

展望

目前还有很多基础功能还没有开发,比如

  1. 日期标注,日期标注有是有,但是作者将几种标记方式写死了,只能用内置的日程、角标和节假日标记,开发中肯定是期望可以传入组件或自定义样式的。
  2. 选择区间,一个很常见的场景,需要选择日期区间,跨月、跨年选择,这些在不了解源码的情况下,去手写插件也是比较困难的。
  3. 自定义样式,作者将自己的样式隔离了,开发者只能通过传入指定的style字符串修改样式,这个用起来不是很方便。

结语

总的来说,这是一款不可多得的组件,即使在PC端,也是不常见的。在小程序的层层阻挠下能开发出如此的组件,实属不易。

ce2898a24a9846c59a058e07eaeea24c_tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.gif


作者:德莱厄斯
来源:juejin.cn/post/7360237771637489679

收起阅读 »

一个小小的批量插入,被面试官追问了6次

嗨,你好呀,我是哪吒。 面试经常被问到“MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?”。 首先需要明确一点,优先使用批量插入,而不是在Java中通过循环单条插入。 很多小伙伴都知道这个结论,但是,为啥?很少有人...
继续阅读 »

嗨,你好呀,我是哪吒。


面试经常被问到“MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?”。


首先需要明确一点,优先使用批量插入,而不是在Java中通过循环单条插入。


很多小伙伴都知道这个结论,但是,为啥?很少有人能说出个所以然来。


就算我不知道,你也不能反反复复问我“同一个问题”吧?


1、MyBatis批量入库时,xml的foreach和java的foreach,性能上有什么区别?


批量入库时,如果通过Java循环语句一条一条入库,每一条SQL都需要涉及到一次数据库的操作,包括网络IO以及磁盘IO,可想而知,这个效率是非常低下的。


xml中使用foreach的方式会一次性发送给数据库执行,只需要进行一次网络IO,提高了效率。


但是,xml中的foreach可能会导致内存溢出OOM问题,因为它会一次性将所有数据加载到内存中。而java中的foreach可以有效避免这个问题,因为它会分批次处理数据,每次只处理一部分数据,从而减少内存的使用。


如果操作比较复杂,例如需要进行复杂的计算或者转换,那么使用java中的foreach可能会更快,因为它可以直接利用java的强大功能,而不需要通过xml进行转换。


孰重孰轻,就需要面试官自己拿捏了~


2、在MyBatis中,对于<foreach>标签的使用,通常有几种常见的优化方法?


比如避免一次性传递过大的数据集合到foreach中,可以通过分批次处理数据或者在业务层先进行数据过滤和筛选。


预编译SQL语句、优化SQL语句,减少foreach编译的工作量。


对于重复执行的SQL语句,可以利用mybatis的缓存机制来减少数据库的访问次数。


对于关联查询,可以考虑使用mybatis的懒加载特性,延迟加载关联数据,减少一次性加载的数据量。


3、MyBatis foreach批量插入会有什么问题?


foreach在处理大量数据时会消耗大量内存。因为foreach需要将所有要插入的数据加载到内存中,如果数据量过大,可能会导致内存溢出。


有些数据库对单条SQL语句中可以插入的数据量有限制。如果超过这个限制,foreach生成的批量插入语句将无法执行。


使用foreach进行批量插入时,需要注意事务的管理。如果部分插入失败,可能需要进行回滚操作。


foreach会使SQL语句变得复杂,可能影响代码的可读性和可维护性。


4、当使用foreach进行批量插入时,如何处理可能出现的事务问题?内存不足怎么办?


本质上这两个是一个问题,就是SQL执行慢,一次性执行SQL数量大的问题。


大多数数据库都提供了事务管理功能,可以确保一组操作要么全部成功,要么全部失败。在执行批量插入操作前,开始一个数据库事务,如果所有插入操作都成功,则提交事务;如果有任何一条插入操作失败,则回滚事务。


如果一次插入大量数据,可以考虑分批插入。这样,即使某一批插入失败,也不会影响到其他批次的插入。


优化foreach生成的SQL语句,避免因SQL语句过长或过于复杂而导致的问题。


比如MySQL的INSERT INTO ... VALUES语法 通常比使用foreach进行批量插入更高效,也更可靠。


5、MyBati foreach批量插入时如何处理死锁问题?


当使用MyBatis的foreach进行批量插入时,可能会遇到死锁问题。这主要是因为多个事务同时尝试获取相同的资源(如数据库的行或表),并且每个事务都在等待其他事务释放资源,从而导致了死锁。


(1)优化SQL语句


确保SQL语句尽可能高效,避免不必要的全表扫描或复杂的联接操作,这可以减少事务持有锁的时间,从而降低死锁的可能性。


不管遇到什么问题,你就回答优化SQL,基本上都没毛病。


(2)设置锁超时


为事务设置一个合理的锁超时时间,这样即使发生死锁,也不会导致系统长时间无响应。


(3)使用乐观锁


乐观锁是一种非阻塞性锁,它假设多个事务在同一时间不会冲突,因此不会像悲观锁那样在每次访问数据时都加锁。乐观锁通常用于读取频繁、写入较少的场景。


(4)分批插入


如果一次插入大量数据,可以考虑分批插入。这样,即使某一批插入失败,也不会影响到其他批次的插入。


(5)调整事务隔离级别


较低的隔离级别(如READ UNCOMMITTED)可能会减少死锁的发生,但可能会导致其他问题,如脏读或不可重复读。


6、mybatis foreach批量插入时如果数据库连接池耗尽,如何处理?


(1)增加最大连接数


数据库连接池耗尽了,增加最大连接数,这个回答,没毛病。


(2)优化SQL语句


减少每个连接的使用时间,从而减少连接池耗尽的可能性。


万变不离其宗,优化SQL,没毛病。


(3)分批插入


避免一次性占用过多的连接,从而减少连接池耗尽的可能性。


(4)调整事务隔离级别


降低事务隔离级别可以减少每个事务持有连接的时间,从而减少连接池耗尽的可能性。但需要注意,较低的事务隔离级别可能会导致其他问题,如脏读或不可重复读。


(5)使用更高效的批量插入方法


比如MySQL的INSERT INTO ... VALUES语法。这些方法通常比使用foreach进行批量插入更高效,也更节省连接资源。


感觉每道题的答案都是一样呢?这就对喽,数据库连接池耗尽,本质问题不就是入库的速度太慢了嘛。


(6)定期检查并关闭空闲时间过长的连接,以释放连接资源。


就前面的几个问题,做一个小总结,你会发现,它们的回答大差不差。


通过现象看本质,批量插入会有什么问题?事务问题?内存不足怎么办?如何处理死锁问题?数据库连接池耗尽,如何处理?


这些问题的本质都是因为SQL执行慢,一次性SQL数据量太大,事务提交太慢导致的。


回答的核心都是:如何降低单次事务时间?



  1. 优化SQL语句

  2. 分批插入

  3. 调整事务隔离级别

  4. 使用更高效的批量插入方法


作者:哪吒编程
来源:juejin.cn/post/7359900973991362597
收起阅读 »