注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

2万块钱买平板:苹果新一代iPad Pro直接上M4芯片,最强也最贵

万众瞩目的苹果 M4 芯片,刚刚在新一代的 iPad Pro 上亮相了。 北京时间 5 月 7 日晚 10 点,苹果举行了春季新品发布特别活动。这次活动发布了新款 iPad(Air 和 Pro)以及新一代 Apple Pencil 和妙控键盘配件等新品。 ...
继续阅读 »

万众瞩目的苹果 M4 芯片,刚刚在新一代的 iPad Pro 上亮相了。



图片


北京时间 5 月 7 日晚 10 点,苹果举行了春季新品发布特别活动。这次活动发布了新款 iPad(Air 和 Pro)以及新一代 Apple Pencil 和妙控键盘配件等新品。


当然,新一代 iPad Pro 是此次发布会上大家关注的重点,尤其是它所搭载的芯片。


苹果没有让大家失望,为 iPad Pro 装上了自家新一代的 M4 芯片,这也是该芯片的首次亮相。


「你可能会认为我们会使用极其强大的 M3 芯片,但我们跨越到下一代 ——M4 芯片。」


也就是说全新 iPad Pro 搭载的芯片直接从 M2 跳到了 M4。


下面这张图,可以说是用一图总结了 M4 芯片的强大性能。


图片


展开来说,全新 M4 芯片由 280 亿晶体管组成,基于第二代 3nm 技术打造,并在 CPU、GPU 和 NPU 方面迎来一系列提升。


全新 10 核心 CPU


在 CPU 方面,M4 拥有 10 核心 CPU,包含 4 个性能核心和 6 个能效核心。下一代核心改进了分支预测功能,为性能核心提供更广泛的解码和执行引擎,为能效核心提供更深层次执行引擎。


此外,性能核心和能效核心还具有增强的下一代机器学习(ML)加速器。


图片


与前代 iPad Pro 搭载的 M2 相比,M4 的 CPU 性能提升了 50%


图片


因此,无论是在 Logic Pro 中处理复杂的管弦乐文件,还是在 LumaFusion 中向 4K 视频添加高要求的效果,M4 都能提高整个专业工作流程的性能。


图片


10 核心 GPU


其次,GPU 部分。M4 的全新 10 核心 GPU 建立在 M3 系列芯片的新一代图形架构之上。它具有动态缓存功能,这是苹果的一项创新,可以在硬件中实时动态分配本地内存,从而显著提高 GPU 的平均利用率。


图片


此次,M4 芯片提高了专业应用程序以及游戏方面的性能。苹果表示,这是硬件加速光线追踪首次登陆 iPad,在游戏等体验中实现更真实的阴影和反射。


图片


《暗黑破坏神:不朽》游戏。


硬件加速的网格着色也内置于 GPU 中,可以提供更强大的几何处理能力和效率。相比之下,M4 芯片专业渲染性能得到了巨大提升,是 M2 芯片速度的四倍


图片


图片专业渲染软件 Octane。


在能耗方面,M4 只需一半的功耗即可提供与 M2 相同的性能。即使与轻薄笔记本电脑中最新的 PC 芯片相比,M4 只需四分之一的功耗即可提供相同的性能。


全新显示引擎


图片


M4 采用了开创性技术加持的全新显示引擎,实现了 Ultra Retina XDR 的精度、色彩准确度和亮度均匀性,这是一种结合两个 OLED 面板的光线创建的最先进的显示屏。


图片


最强大的神经引擎


M4 的神经引擎采用 16 核心设计,使得芯片更快、性能更强。


苹果表示,M4 拥有苹果有史以来最强大的神经引擎,每秒能够执行惊人的 38 万亿次操作,是 A11 Bionic 中的第一代神经引擎速度的 60 倍


图片


图片


神经引擎与 CPU 中的下一代机器学习加速器、高性能 GPU 和更高带宽的统一内存一起,使 M4 成为一款极其强大的 AI 芯片。


借助 iPadOS 中的 AI 功能(例如用于实时音频字幕的 Live Captions 以及识别视频和照片中目标的 Visual Look Up),新款 iPad Pro 允许用户在设备上快速完成令人惊叹的 AI 任务。


在苹果的展示中,配备 M4 的 iPad Pro 只需轻按一下,即可轻松将 Final Cut Pro 中 4K 视频的主题与背景分离。


图片


苹果表示,M4 中的神经引擎比当今任何 AI PC 中的神经处理单元都更强大


自 2017 年以来,苹果所有的芯片都包含了某种版本的神经引擎,尽管到目前为止,这些芯片主要用于增强和分类照片、光学字符识别、离线听写和其他事情。但苹果可能需要更快的东西来支持端侧以大型语言模型为核心的生成式人工智能,苹果预计将在下个月的 WWDC 上在 iOS 和 iPadOS 18 上推出这种人工智能。


从往年来看,M1 和 M2 之间的等待以及 M2 和 M3 之间的等待期都是一年半左右。由于苹果公布的技术细节很少,很难知道 M3 和 M4 之间更快的转变是什么原因。可能是 M3 落后于计划,而 M4 准时或提前;也有可能 M4 只是对 M3 进行了相对温和的架构更新。这需要拿到后续测试结果才能判断。


M4 版 iPad Pro:8999 元起


除了所搭载的芯片,苹果发布会上还介绍了新款 iPad 的其他细节。


新款 iPad Air 分为 11 英寸和 13 英寸两个版本,搭载 M2 芯片,支持 Wi-Fi 6E(可以选择支持 5G 的型号),最大存储空间 1TB,比搭载 M1 芯片的 iPad Air 快 50%,但显示屏仍然是 LED 显示屏。售价方面:11 英寸机型 4799 元起,13 英寸 6499 元起。


图片


新款 iPad Pro 同样分为 11 英寸和 13 英寸两个版本,采用了双层串联 OLED 屏幕,亮度更高,色彩显示更精准。其全屏亮度可以达到 1000 尼特,峰值亮度达到 1600 尼特,苹果称其为「超精视网膜 XDR 显示屏」。


售价方面,11 英寸机型 8999 元起,13 英寸 11499 元起。但如果想要更高的配置,预算会一路飙升。13 英寸 2TB 顶配达到了 19999 元。如果选择纳米纹理玻璃,售价将达到 20799 元。这可能是 iPad 史上最贵的机型。


图片


这款新品 5 月 9 日上午 9 点接受订购,5 月 15 日发售。你准备入手吗? 


参考链接


http://www.apple.com/newsroom/20…


arstechnica.com/apple/2024/…




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

房车用了两年多,这个油耗我是没有想到的

2021年12月买了一台基于大同V80改装的B型小房车,转眼已过两年半的时间,在这两年半的时间里,总行程达四万多公里,带我们走了许许多多的地方,看了祖国的山川湖海,秀美风光,去跑山、去露营、去看海、去旅行,留下了太多太多美好的回忆 有许多小伙伴对房车感兴趣,...
继续阅读 »

2021年12月买了一台基于大同V80改装的B型小房车,转眼已过两年半的时间,在这两年半的时间里,总行程达四万多公里,带我们走了许许多多的地方,看了祖国的山川湖海,秀美风光,去跑山、去露营、去看海、去旅行,留下了太多太多美好的回忆



有许多小伙伴对房车感兴趣,会咨询关于房车各种各样的问题,所以我准备写几篇文章来详细介绍下房车使用过程中的各种细节问题。今天这篇文章主要来分享下关于这台房车的加油和油耗相关的问题,主要包含柴油、尿素和油耗三部分,看完之后相信你对房车加油及油耗会有更加清晰的认识


柴油


我买的这台房车是烧柴油的,有小伙伴对柴油不了解,有问柴油在城市里好不好加的问题,目前我去过的所有加油站,无论是城市还是乡村,都有柴油供应,柴油相比汽油来说有很多优势,例如:


1.柴油更便宜:柴油往往比最便宜的92号汽油还要便宜一点点


2.燃烧效率高:柴油引擎通常具有更高的压缩比,这意味着在燃烧燃料时能够更高效地释放能量。这使得柴油引擎在燃料效率上表现更优秀,相对于同等排量的汽油引擎,柴油引擎的燃油消耗通常更低,也就是更省油


3.扭矩输出大:由于柴油燃料的化学性质,柴油引擎在高压下能够产生更多的扭矩。这使得柴油引擎在牵引重载或需要大扭矩输出的应用中表现优异,比如卡车、拖拉机等


4.长途行驶经济性好:上边说了柴油引擎的燃油效率高和扭矩输出大,同时因为其价格本身就比汽油低,所以柴油车辆通常在长途行驶中具有更好的经济性


5.动力输出稳定:柴油燃料的燃烧过程相对稳定,这意味着柴油引擎在低转速和高负荷下能够提供更加稳定的动力输出,这对于需要长时间持续工作的应用,如发电机和工程机械,尤其重要


鉴于以上几点,除了家用轿车外的很多商用车辆很多使用柴油,尤其是那些跑长途的大货车、客车和大巴,以及对动力要求比较高的农用和工程机械等,城市内的商用车辆也很多,所以加油根本不是问题



说回房车,加柴油的优势除了以上几点外,更为关键的是加油基本不用等,尤其是在节假日的高速服务区,之前开汽油车节假日出行,在高速服务区加油站排队加油等待半小时一小时的都是常有的事,但自从开了这个柴油车,就再也没有等过了


尿素


上边说了那么多柴油相比汽油等优势,那为什么家用轿车基本上都是烧汽油而非柴油。那这就要说说柴油车相比于汽油车的劣势了


1.低温启动问题:在寒冷的气候条件下,柴油引擎的启动可能会更困难,尤其是在没有预热系统的情况下。相比之下,汽油引擎在低温下启动更为容易,家用车要的是更易用


2.振动和噪音:柴油引擎通常比汽油引擎产生更多的振动和噪音,尤其是在低速行驶时。这可能会降低驾驶舒适度,特别是对于家庭轿车这样的日常驾驶,汽油车的舒适性更好


3.排放标准:柴油引擎在排放方面通常比汽油引擎更具挑战性


家用车更看重的是易用性和舒适性,所以这也是为什么大多数的家用车都采用汽油的主要原因。同时柴油车为了达到目前最新的国六B排放标准,除了常规的技术优化升级外,往往还要通过添加尿素溶液,减少氮氧化物的排放来达标国六B标准



那有小伙伴问添加尿素频繁吗?麻烦吗?加尿素就跟加柴油差不多,加到尿素箱里即可,尿素在每个加油站里都有卖,一桶10KG的价格大概是30元,我的车子一次加一桶多不到两桶,加一次的总花费大概也就60块,我没有具体计算过加满一箱能跑多远,预估大约有五千公里左右的样子,不是很频繁,一年如果行驶两万公里的话,也就是说需要添加3至4次尿素


油耗


最后来聊聊油耗,我的房车是基于大通V80短轴中顶底盘改装的B型房车,原车重应该在2吨左右,再加上上装家具,还有水箱电器之类的,保守估计重3吨,经过两年多的实际行驶,现在整体油耗在8.5L/百公里左右


其中最高油耗10个多一点,10个多油是在非常非常堵的情况下拿到的数据,所以我姑且认为全是最高油耗吧,最低油耗大概也在8个油左右,平时开车有关注,全程高速也有接近8个油,所以算是最低油耗吧,这个车一般都是周末节假日出去玩儿开的多,上班通勤开的少,整体路况都很好,不常遇到堵车的情况,所以综合油耗也是比较低



这个油耗我是非常满意了,毕竟这么大的车,我之前上下班通勤开的CRV,综合油耗都在百公里9升多。买之前以为这么大的车油耗怎么着都要十几个,确实没想到实际会这么低,网上发了帖子,许多同样的车主都表示与我的数据相差不大,可见这是真实表现了


最后


曾经跟多个加油站的一线加油员聊过,比起汽油车,他们更愿意给柴油车加油,主要是汽油车一般都是家用小轿车为主,而柴油车都是运输/工具车为主,家用轿车油箱一般都比较小,50升左右,而柴油车油箱普遍比较大,上百升甚至几百升很正常,一次能加更多的油。更为重要的是,汽油车大多都是自己的,部分汽油车的车主能买个车就觉得自己高人一等,到加油站之类的地方就会对服务人员颐指气使,而柴油车大多都司机或者一线体力劳动者居多,跟加油站员工属于类似工种,更能理解,相对会好说话,不计较,更平和


作者:37丫37
来源:juejin.cn/post/7367275063873470502
收起阅读 »

程序媛28岁前畅游中国是什么体验?

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗。 前几年互联网飞速发展高薪招人时,大家...
继续阅读 »

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗


前几年互联网飞速发展高薪招人时,大家都有肉吃,现在遇到互联网寒冬了,有汤喝就不错了,尤其对晚入行的95 后社畜,现在回过头看,已经是互联网红利退潮的末期了。对于 80 后早一批入行的程序员, 肯定钱也挣够了,房子也早就翻几倍了,早就有抗御风险的能力了,即使裁员了也能拿着分手费找个差不多的厂子继续苟着。但是对于 95 后来说,惨不忍睹,行业内卷及其严重,刚有点工作经验就遭遇大规模裁员,重点买房都是踩在最高点接盘,现在房价跌了,车子打价格战,直接把前几年辛辛苦苦挣的首付跌没了,这几年白干了,说起来,心就抽搐的疼。不像人家00 后,直接看开了,不破三个 dai,房贷,车贷和传宗接代,直接卷老家公务员躺平,享受人生,逃离大城市的拥挤,拒绝被房子的套牢。


金融危机,经济下行,行业越来越卷,精神内耗极其严重,身体健康堪忧。我突然顿悟了,我决定,为自己而活。想看世界的心也越来越强烈,最后我坐不住了,做了个大胆的决定,畅游中国。刚好疫情快结束时,航空公司推出了自己的产品,畅游中国随心飞,我立刻入手了,入手价是三千多点,全国飞不限次数。我一边安排好自己的时间订机票,一边计划旅行路线,一个女生独自环游中国之旅开始了。没有队友,不给生活中任何糟心事打断我的计划,一人吃饱,全家不饿。下面给大家说说我去了哪些地方。


贵州-贵阳


我看好时间后立刻定机票,从上海飞到了贵阳,准备打卡黄果树瀑布。我定的酒店就在黄果树景点附近不远,一大清早7点我就起床了,呼吸着让人神清气爽的空气,吃了一些自己带的进口苹果作为早餐,特别甘甜,饱腹感足足的。8点进山了,那一刻,我别提多开心了。


回想起当社畜时,每次都是8.30起床,9.30左右到公司,每天上班心情比上坟都沉重,永远干不完的KPI,OCR,不是被PUA就是吃老板画的大饼,再丰盛的早餐一想到一堆任务要做,吃着也如同嚼蜡,更别提神清气爽,心境开拓了。


在进入黄果树后,我欢快的脚步往前走,因为我是一个女孩子独行,所以不太愿意跟陌生人说话,一路上虽然很沉默,但看到这些壮观的自然景色,闻着草木花果香,内心激动不已。爬了一个钟左右的山,终于看到了大瀑布。


下面是我实拍的景点图:


WechatIMG110.jpg
WechatIMG114.jpg
WechatIMG111.jpg
WechatIMG115.jpg

有句古诗,疑似银河落九天,一路好山好水,逛完黄果树后我出来就去吃了贵州的特色菜,价格美丽,味道很不错,超级喜欢,
当时就在感慨,上海要是能吃到这么好吃又鲜美的酸汤鱼就好了。


WechatIMG109.jpg

重庆


本来下一步去梵净山再顺路去成都自驾318路线的,但时间紧迫,我弟弟在重庆读书,说要跟我一起去自驾318,我就先去重庆跟他汇合了。


1191711609983_.pic.jpg
1211711610320_.pic.jpg
1241711610640_.pic.jpg

最后那个火锅要适度吃啊,吃两顿辣的我的陈年胃病都犯了,好几天没缓过来,哭晕在厕所,我弟跟个没事人一样,这是我深刻认识到当了多年的社畜的后果就是,经常熬夜加班点外卖,把好好的身体给造坏了。重庆的洪崖洞,解放碑也去了很多次了,这里给个图


1201711609995_.pic.jpg

成都


抵达成都,在春熙路逛了逛,宽窄巷子之前逛过就没去了,


1221711610329_.pic.jpg
1461711611961_.pic.jpg

本来想租自驾神车-坦克300的,价格是普通suv的2倍,结果路上纠结一会的功夫就被抢先租走了(自我反思:以后看准就下手吧,人生有几次这种机会,有啥好犹豫的),租了一辆1.5T的大众SUV,跟我弟一起直奔车行,然后去超市采购路上的食物,大包小包买了一堆,放车后备箱,深夜就起航了


都江堰


教科书上的都江堰,真正去看了,才深深佩服古人治水的智慧,我不是文盲,所以不用一句:卧槽,发表感叹。之前也去过洛阳的黄河小浪底水库,武汉的长江大桥,这些水利工程的智慧。


1261711610658_.pic.jpg
1251711610651_.pic.jpg

青城山


1451711611634_.pic.jpg

这里是青城山下白素贞的故事发源地。爬山是个体力活,当时穿着拖鞋就上山了,下山就傻眼了,不好意思,这里我偷懒了,坐缆车下车,嘿嘿。


泸定桥


1281711611190_.pic.jpg
1271711610783_.pic.jpg

打卡泸定桥,走上面摇摇晃晃确实需要一些勇气,特别怕手机掉下去。


海螺沟


一鼓作气,一路直行,抵达海螺沟。来之前,我觉得新能源车咋自驾318,路上看到同样是特斯拉车主,我感觉自己有点狭隘。人啊,果然要多出去看看,不能活在自己的局限认知中。


1301711611238_.pic.jpg
1481711613272_.pic.jpg
1311711611256_.pic.jpg

不过开车还是要小心,路上遇到有车盘山时发生侧翻的。还有山上偶尔会有落石下来,要当心了。


木格措


一路景色壮观,蓝天白云,川西一定要必去。到了康定情歌的原地。打个卡。


1331711611313_.pic.jpg
1471711612939_.pic.jpg

不过我路上听的歌一直都是朴树的《平凡之路》,一路循环:


我曾经跨过山和大海 也穿过人山人海

我曾经拥有着的一切 转眼都飘散如烟

我曾经失落失望 失掉所有方向

直到看见平凡 才是唯一的答案
....


不正是正值青春的我受伤了,但又奋力前行寻找答案吗。


四姑娘山


一路直行。。。抵达四姑娘山,四姑娘山有四座雪山组成,远看景色很壮观,雪已经化了很多。


1291711611214_.pic.jpg
1441711611506_.pic.jpg
1421711611464_.pic.jpg

当地信仰


遇到了一群一动不动的牦牛,还有一匹热情好客的长脸马。拿出来一个饼给它,它吃的还很香。本来开心的事现在记录起来突然感觉在暗示自己在公司当牛做马,不说了,emo了。据说那白色塔这是当地的信仰,表示尊重。


1361711611362_.pic.jpg
1351711611349_.pic.jpg
1391711611410_.pic.jpg

雅拉山口


盘山路,1.5T的车开着有点吃力,油门上不去。终于爬上山了,下车拍照时,激动过头了,开始缺氧,头疼,吸氧。。。。。。。。


1371711611374_.pic.jpg
1381711611398_.pic.jpg
1561711614555_.pic.jpg

后面走着走着身体扛不住了,我去当地买了高反的药,吃了没啥用,氧气越吸头越疼,我弟要回去上课,我身体不抗造,遗憾的半途而归了。再次强调一下,好风景要趁年轻,体力好,等老了走不动了,确实再好的风景,都没那心情和体力去欣赏了。


1551711614521_.pic.jpg
1571711614582_.pic.jpg

乐山


跟我弟散伙后,我自己开车去了乐山大佛,保佑我顺风顺水吧。还去看了东方佛群,卧佛,药师佛,看了各种佛,记不清楚了。。。


1531711614452_.pic.jpg
1601711617453_.pic.jpg
1611711617471_.pic.jpg

峨眉山


接着我自己又自驾去了峨眉山,两个地方相差不是很远,看到了峨眉山的云海,云雾缭绕,超级刺眼!


1521711614435_.pic.jpg
1511711614416_.pic.jpg

下山后当晚接着又踩着点返回成都还车。休息一晚后,又顺路打卡了锦里。感受人世间的烟火和繁华


1591711614637_.pic.jpg
1581711614623_.pic.jpg

又吃了一顿火锅后,回上海。这时,胃没有不舒服,看来,这一圈下来,肠胃好很多了。


1621711617506_.pic.jpg

又回到了我熟悉的大上海。


安徽


经过一段时间的调养后,我觉得的身体状态老好了,爬山那不是小意思,走,爬山去,什么黄山,三清山,庐山,武功山,离沪这么近,爬起来不费劲!我到了安徽省,黄山市,休息一晚准备去爬山。当晚被出租车司机拉到了老街逛逛。


1631711617723_.pic.jpg

就一个小型的徽派建筑青砖白瓦的特色,跟顾村差不多。逛完后突然下起了大雨,我猝不及防没带伞,
就记得那晚的雨,比情深深雨蒙蒙中依萍找她爸要钱被鞭子抽回去时遇到的那场大雨还大。。。。。。


黄山


不凑巧,上山时遇到了大雾,但来都来了,那就爬山下去吧。到了光明顶也啥都看不见,但幸运的时,下山时,守得云开见月明,气喘吁吁的开心拍照。


1641711617970_.pic.jpg
1651711617990_.pic.jpg
1661711618010_.pic.jpg

江西


黄山结束后,顺路就来到了江西,江西景色比较集中,一定要去上饶啊,那就先去望仙谷看看吧。


上饶-望仙谷


人工打造的经典,现实版的仙侠世界。小雨朦胧,青山傍水,景色秀丽。


1681711618049_.pic.jpg
1671711618030_.pic.jpg
1691711618070_.pic.jpg

上饶-三清山


谁说黄山归来不看山,我觉得三清山值得一去,至少我是不后悔的。每座山都有每座山的特色,爬到这时,腿开始抖了,但我可不是那么轻易就能认输的人啊,继续爬,专挑难爬的道:一线天!!!!!


1721711618536_.pic.jpg
1701711618509_.pic.jpg
1711711618524_.pic.jpg

哈哈,说这个像蟒蛇,像吗?


1731711618548_.pic.jpg

下山时腿疼的不行,扛不住了,嘴不硬了,不去庐山了,武功山了。。。。


南昌


对了,不明白为啥江西彩礼那么高?


1741711619525_.pic.jpg

广东


广州


从南昌飞到广州了,看了小蛮腰,在附近喝喝茶,遛遛弯,吃点茶点


1751711619548_.pic.jpg
1761711619562_.pic.jpg

深圳


到深圳后租了个车溜达到海边吃海鲜,还去华强北也溜一溜,吃了很多粤菜


2121711623487_.pic.jpg
2131711623507_.pic.jpg
2141711623518_.pic.jpg

香港


从深圳坐高铁到香港也就十几分钟,跟快的。香港巴士,香港茶餐厅,路过金店,想买项链的,但又怕弄丢了就没买,现在金价那么高,有点损失。


2171711623568_.pic.jpg
2181711623590_.pic.jpg
2161711623554_.pic.jpg
2151711623538_.pic.jpg

新疆


从上海飞新疆要4个多小时,一路太无聊了,下飞机后,心情就好很多


1781711619752_.pic.jpg

乌鲁木齐


去了大巴扎,吃了羊肉串和切糕,还有新疆大盘鸡


1911711619947_.pic.jpg
1921711619959_.pic.jpg
1931711619971_.pic.jpg
1951711620020_.pic.jpg

无人区


没信号,没水,荒漠一片。。。


1901711619929_.pic.jpg
1891711619916_.pic.jpg

伊犁


到了伊犁市区后,去了小吃街,吃了羊肉


1961711620038_.pic.jpg

赛里木湖


高原湖泊,非常适合自驾游玩,我这里是跟人拼车去的。看着真舒服,可惜我把单反带来,也背不动,这是人家的


1851711619856_.pic.jpg
1841711619835_.pic.jpg
1801711619776_.pic.jpg
1831711619820_.pic.jpg
1811711619793_.pic.jpg
1821711619807_.pic.jpg

边境-国门,果子沟大桥, 薰衣草


1881711619895_.pic.jpg
1871711619882_.pic.jpg
1861711619867_.pic.jpg

新疆白天长,夜里段,到了晚上9点多,天才慢慢开始变黑。


北京


这次我飞到了老北京,看了天安门,看了老城墙


1971711620074_.pic.jpg
1981711620099_.pic.jpg

内蒙古


从北京顺路来了内蒙古呼和浩特,先填饱肚了,去那个什么街买了一堆牛肉干


呼和浩特


1991711623013_.pic.jpg
2011711623052_.pic.jpg
2021711623101_.pic.jpg

青甘环线


说到去青甘,想起有个在学生时期就在玩的狐朋狗友,听说我打算去自驾就想跟我一起去。因为我的车是新能源,自驾充电比较麻烦,他打算提混动车方便些,他说让我等他提车带他一起去自驾,本来约定好了时间,到快出发时,一会又说不打算提车了,又说等他面试换好工作后,最后他自己又各种理由怂了,这种又想出去玩,又想挣钱,又不舍得花钱,这种拧巴的状态,我很无语,当然,这也是现实中大部分人的写实吧,这里我想说,做好权衡利弊和取舍就好,既然决定去追求诗和远方,就不要再去跟钱分文必争了,不可否认,旅行确实需要花钱,我们能做的就是按照自己能承担的最低的成本去看世界。人家说勇敢的人先享受世界,让他纠结犹豫去吧,我就先溜了,毕竟老祖宗给的经验是:欲买桂花同载酒,终不似,少年游。再后来,他说他提车了,问我还去不去,我说我早就已经打卡过了。我问他新工作找好了?他说还没有。。。所以他白拧巴了,车还是要提,想去的地方最终还是要去,挣不了的钱最终还是没到口袋里去。毕竟能随时说走就走的同行者只有自己。


我是从内蒙飞到了青海的西宁。


西宁市


填饱肚子先,然后出发去青海湖,远看蓝色,近看青色,全靠天气


2031711623308_.pic.jpg
2351711690717_.pic.jpg

青海湖


2091711623426_.pic.jpg
2051711623359_.pic.jpg

茶卡盐湖


天空之境,名不虚传。


2061711623379_.pic.jpg
2081711623412_.pic.jpg
2361711690772_.pic.jpg

丹霞地貌,策马奔腾


2111711623453_.pic.jpg
2101711623438_.pic.jpg

策马奔腾很潇洒,归来草原上都是马粪,有点臭。。。
仙气飘飘的牦牛,跟川西的大黑牛不一样
2231711624048_.pic.jpg
2041711623341_.pic.jpg


后面的敦煌,莫高窟去不了了,青海也是有3000多海拔的,玩嗨了,又又又高反了,不得已要回去了,哎,当了这么多年生产驴,身体熬废了。回去后多锻炼身体吧。毕竟身体是革命的成本。


武汉


于是,先飞回了武汉玩几天。回家转转,熟悉的感觉。喜欢武汉的大江大湖和历史文化。黄鹤一去不复返,白云千载空悠悠。
然后又从武汉飞到上海狗着。


2241711624590_.pic.jpg
2261711624823_.pic.jpg
2251711624807_.pic.jpg

上海市


这个城市充满了魅力。只要你有钱,就可以纸醉金迷,去和平饭店享受,去挥霍。没钱,只能继续搬砖。


2291711625200_.pic.jpg
2301711625213_.pic.jpg
2271711624846_.pic.jpg


回去后改善饮食,一边努力干活学习,一边下定决心锻炼,都有马甲线了,五公里so easy ,哈哈哈哈。每次回到上海这个繁华的国际大都市,我都深深感受到,这座城市虽然压力大,但终究是自由的,没人关心和打扰你的私人生活,你可以为自己而活,安排自己的一生,不必循规蹈矩,不必顾及世俗的眼光,这个城市包容能力很强,不妨大胆一些,追求自己的人生。去不同的城市体验不一样的生活和文化。




在买随心飞之前我也去过很多城市,比如:湖北的荆州,湖南的岳阳,张家界,广东的东莞,广西的桂林和北海,海南的三亚,云南的昆明大理丽江,江浙沪包邮一带的杭州,南京,无锡,湖州,台州,宁波,福建的厦门,河南的洛阳,开封,郑州,信阳,山东青岛,陕西西安,安徽合肥等城市。时间有限,码字不易,很抱歉这里我就不全部列出了。尤其在学生时代,那是真的快乐,没有一丝丝杂念,单纯的快乐。后面打算环游世界了,已经去了东南亚的一些国家,这里我想说我本来就是为了WLB努力的,工作生活两不误,我的旅途未完待续~


回顾这么多年,走过的国内大大小小的城市,也没具体统计过,开始逐渐让自己的眼界开阔起来,不让自己的眼光那么狭隘了,看待任何事物更具包容性吧,以前不理解的东西,现在慢慢理解了。也许人生就是这样,思想和观念一直变化。还是那句话,勇敢的人先享受人生吧,不要辜负努力写代码的自己。


作者:为了WLB努力
来源:juejin.cn/post/7351301965034586152
收起阅读 »

携手15年,语雀创始人玉伯从蚂蚁离职,选择一个人远行

转载好文:雷锋网 本文作者:何思思 2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。 他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下面...
继续阅读 »

转载好文:雷锋网 本文作者:何思思


image.png


2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。


他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下面的配图是园区风景,还有眺望远方的景色。


不愿做技术大佬,要做为产品服务的技术


“前端大牛、技术大佬”是业界给玉伯贴的标签,2008年加入淘宝后,玉伯先后做出了前端领域很火的框架 SeaJS、KISSY,之后带领团队通过开源做了很多技术产品。


但玉伯始终认为,技术只是工具,最终还是要为产品服务。所以当时在淘宝内部,玉伯一直是“折腾”的状态,加入淘宝那年,玉伯就参加了内部的赛马机制,跟团队做了几个月的创新产品,最后以失败告终,又回到了Java 团队做技术。


但这并有改变他要做创新产品的初心,于是2010 年到2011年,他一边做技术研发,一边继续摸索创新产品,但一直没做出能拿的出手的产品。直到2016年,在蚂蚁体验技术部的创新产品孵化机制策马扬鞭项目中,玉伯团队主导的语雀问世,并于2018年正式对公网提供服务。


也有内部人士称:玉伯当时和老板提了条件说,光做前端没意思,你要想留住我,就得给我一款产品做。所以当时玉伯自己要了一个团队,专门做一个闭环产品。


其实,从语雀诞生到现在经历了两次生死局:第一次是2018年,腾讯文档、钉钉文档、飞书文档相继亮相,文档产品迎来爆发期,当时阿里也想抓住这个风口,语雀最终把三分之二的人输送给了钉钉,作为钉钉文档的初始团队。在团队仅剩七八个人的时候,玉伯再次招人将团队扩充到二十人左右。


第二次是2020年,彼时,钉钉文档做了很久但并没达到预期效果,而语雀正值上升期,阿里云为了尽快把文档做起来,想把语雀、钉钉文档、阿里云笔记等内部各种文档团队聚集起来,成立一个独立的阿里文档事业部,由玉伯牵头,但却被无招反对,这也间接帮助了玉伯。


直到2021年,蚂蚁成立了智能协同事业部,其中语雀作为重点产品,以独立BU运作。


创业中的理想派,为了做好一件事而做


从2016年到现在,为了做好语雀,玉伯做了大量的工作。


玉伯曾回忆道,做语雀最大的一个感触是,啥都得做。最开始是半个PD,很快变成了客服,同时兼做运营,还需要去承担BD的工作,因为没有BD,只能逼着自己去做,一切为了产品往前跑。


也有用户在即刻分享道,自己曾经在语雀的付费用户群中提了一个文档的排序问题,当时玉伯就在群里,很快的响应了这个需求并做了优化。


image.png


此外,玉伯也背负了巨大的营收压力,尤其是近两年在阿里集团整体缩紧的状态下。雷峰网通过其他渠道了解到,集团也给语雀定了目标——“盈亏平衡”。


迫于压力,近两年语雀也调整了收费策略,2019年语雀开始尝试简单的商业化模式,即初级的团队语雀空间和语雀个人的收费版本;紧接着又重新设计了个人版价格策略,分为99元会员、199元超级会员、299元至尊会员三个档次,团队和空间版的收费则更高。


这对一个小团队来说并不容易,首先,较钉钉、飞书、腾讯文档而言,语雀强调的是知识管理的逻辑,其次,语雀服务的对象偏小众聚焦在侧重知识管理的用户,且这些目标对象比较分散,很难第一时间发掘到,这就意味着需要花很长时间去培养,没办法快速完成转化;再就是,虽然语雀团队不大,只有五六十人左右,但这部分人大都是互联网人才,成本也是一笔不小的支出。


雷峰网在之前拜访玉伯时听闻,目前语雀主要服务蚂蚁和阿里内部,在阿里内的日活已经达到了11万左右,商业化方面还比较单一,主要是通过发布会的方式宣传。由此可见,语雀的商业化路径还没完全打开。


无论选择出去创业还是集团内部创业,背负营收压力都是不可避免的。但抛开这个不谈,仅玉伯的个人角度出发,他曾谈过自己做语雀的初心,就是想把自己内心想做的事情做完,且这件事还能帮助到别人,就做了。


正是这种简单纯粹的心态,让玉伯在做语雀时只专注事情的本身以及这件事情创造的价值,而并非拼命地追求变现。


雷峰网(公众号:雷峰网)曾发表文章《留给飞书的时间》,他如此评论:



“现实主义者关注的是钱,理想主义者关注的是时间,当代这个社会,钱很重要。但更重要的,对个体来说,是如何提高时间的质量,对人类来说,不仅关注时间的质量,还关注整个人类时间的长短,是否可延续下去。赚钱是为了花钱,花钱是为了提升时间的品质甚至长度。围绕钱的现实主义者,最终会为围绕时间的理想主义者服务。”



从玉伯最新的朋友圈内容,不难看出,他的离开或许和钱权没有太大的关系,而是为了追求心目中的诗和远方。他也曾经说过自己有三个梦:“技术梦、产品梦、自由梦。”离开蚂蚁,或许是为了去实现他的“自由梦。”


作者:狗头大军之江苏分军
来源:juejin.cn/post/7299035378589040667
收起阅读 »

Vue3 新项目,没必要再用 Pinia 了!

web
最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?其实不需要,我差点忘记了 Vue3...
继续阅读 »

最近弄了一个新的 Vue3 项目,页面不多,其中有三四个页面需要共享状态,我几乎条件反射般地安装了 Pinia 来做状态管理。

后来一想,我只需要一个仓库,存放几个状态而已,有必要单独接一套 Pinia 吗?

其实不需要,我差点忘记了 Vue3 的一个重要特性,那就是 组合式函数

组合式 API 大家都知道,组合式函数可能大家没有特别留意。但是它功能强大,足矣实现全局状态管理。

组合式函数

什么是组合式函数?以下是官网介绍:

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

从这段介绍中可以看出,组合式函数要满足两个关键点:

  1. 组合式 API。
  2. 有状态逻辑的函数。

在 Vue 组件中,状态通常定义在组件内部。比如典型的选项式 API,状态定义在组件的 data() 方法下,因此这个状态只能在组件内使用。

Vue3 出现之后,有了组合式 API。但对于大部分人来说,只是定义状态的方式从 data()变成了 ref(),貌似没有多大的区别。

实际上,区别大了去了。

组合式 API 提供的 ref() 等方法,不是只可以在 Vue 组件内使用,而是在任意 JS 文件中都可以使用。

这就意味着,组合式 API 可以将 组件与状态分离,状态可以定义在组件之外,并在组件中使用。当我们使用组合式 API 定义了一个有状态的函数,这就是组合式函数。

因此,组合式函数,完全可以实现全局状态管理。

举个例子:假设将用户信息状态定义在一个组合式函数中,方法如下:

// user.js
import { ref } from 'vue'

export function useUinfo() {
// 用户信息
const user_info = ref(null)
// 修改信息
const setUserInfo = (data) => {
user_info.value = data
}
return { user_info, setUserInfo }
}

代码中的 useUinfo() 就是一个组合式函数,里面使用 ref() 定义了状态,并将状态和方法抛出。

在 Vue3 组件之中,我们就可以导入并使用这个状态:


仔细看组合式函数的使用方法,像不像 React 中的 Hook?完全可以将它看作一个 Hook。

在多个组件中使用上述方法导入状态,跨组件的状态管理方式也就实现了。

模块化的使用方法

组合式函数在多个组件中调用,可能会出现重复创建状态的问题。其实我们可以用模块化的方法,更简单。

将上方 user.js 文件中的组合式函数去掉,改造如下:

import { ref } from 'vue'

// 用户信息
export const user_info = ref(null)
// 修改信息
export const setUserInfo = (data) => {
user_info.value = data
}

这样在组件中使用时,直接导入即可:


经过测试,这种方式是可以的。

使用模块化的方法,也就是一个文件定义一组状态,可以看作是 Pinia 的仓库。这样状态模块化的问题也解决了。

Pinia 中最常用的功能还有 getters,基于某个状态动态计算的另一个状态。在组合式函数中用计算属性完全可以实现。

import { ref, computed } from 'vue'

export const num1 = ref(3)

export const num2 = computed(()=> {
return num1 * num1
}

所以思考一下,对于使用 Vue3 组合式 API 开发的项目,是不是完全可以用组合式函数来替代状态管理(Pinia,Vuex)呢?

当然,以上方案仅适用于组合式 API 开发的普通项目。对于选项式 API 开发的项目,或者需要 SSR,还是乖乖用 Pinia 吧 ~

最重要的是!如果面试官问你:除了 Pinia 和 Vuex 还有没有别的状态管理方案?

你可别说不知道,记住这几个字:组合式函数!


作者:杨成功
来源:juejin.cn/post/7348680291937435682
收起阅读 »

当遇到需要在Activity间传递大量的数据怎么办?

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
继续阅读 »

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


Activity之间传递大量数据主要有如下几种方式实现:


  • LruCache

  • 持久化(sqlite、file等)

  • 匿名共享内存


使用LruCache

LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


首先我们定义好写入读出规则:


public interface IOHandler {
   //保存数据
   void put(String key, String value);
   void put(String key, int value);
   void put(String key, double value);
   void put(String key, float value);
   void put(String key, boolean value);
   void put(String key, Object value);

   //读取数据
   String getString(String key);
   double getDouble(String key);
   boolean getBoolean(String key);
   float getFloat(String key);
   int getInt(String key);
   Object getObject(String key);
}

我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.


LruCache mCache = new LruCache<>( 10 * 1024*1024);

写入数据我们使用比较简单:


@Override
public void put(String key, String value) {
   mCache.put(key, value);
}

好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


读取数据也是比较简单方便:


@Override
public String getString(String key) {
   return String.valueOf(mCache.get(key));
}

持久化数据

那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


优势:


(1)应用中全部地方均可以访问


(2)即便应用被强杀也不是问题了


缺点:


(1)操做麻烦


(2)效率低下


匿名共享内存

在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。


作者:派大星不吃蟹
来源:juejin.cn/post/7264503091116965940
收起阅读 »

不容错过的秘籍:JavaScript数组的创建和使用详解

在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。一、什么是数组数组(Array)是一种按顺序存储多个值的数据结构。你可以把它...
继续阅读 »

在编程的世界里,数据是构建一切的基础。而在JavaScript中,有一种特殊且强大的数据结构,它就是——数组。

今天,我们就来一起探索数组的奥秘,从创建到使用,一步步掌握这个重要的工具。

一、什么是数组

数组(Array)是一种按顺序存储多个值的数据结构。你可以把它想象成一个盒子,这个盒子可以存放多个物品,而且每个物品都有一个编号,我们可以通过这个编号来找到或者修改这个物品。

在JavaScript中,数组是一种特殊的对象,用于存储和操作多个值。与其他编程语言不同,JavaScript的数组可以同时存储不同类型的值,并且长度是动态的,可以根据需要随时添加或删除元素。

Description

JavaScript数组使用方括号([])来表示,其中的每个元素用逗号分隔。例如,以下是一个包含不同类型元素的数组的示例:

var myArray = [1, "two", true, [3, 4, 5]];

数组中的元素可以通过索引来访问和修改,索引从0开始。例如,要访问数组中的第一个元素,可以使用以下代码:

var firstElement = myArray[0];

JavaScript也提供了一些内置方法来操作数组,如push()、pop()、shift()、unshift()等,用于添加、删除和修改数组中的元素。

二、数组的作用

数组在编程中扮演着非常重要的角色。它可以帮助我们:

  • 存储多个值:我们可以在一个变量中存储多个值,而不需要为每个值创建单独的变量。

  • 操作数据:我们可以对数组中的元素进行添加、删除、修改和查找等操作。

  • 实现各种算法:通过数组,我们可以实现排序、搜索等常见算法。

  • 循环遍历:数组的元素是有序的,可以使用循环结构遍历数组的每个元素,从而对每个元素进行相同或类似的操作。这在处理大量数据时非常有用。

三、创建数组的方法

在JavaScript中,有多种方法可以创建数组,下面列出常见的三种:

1)字面量方式:

这是最常见的创建数组的方式,只需要在一对方括号[]中放入元素即可,如

var arr = [];

2)使用Array构造函数:

通过new Array()也可以创建数组,如

var arr = new Array();

3)使用Array.of()方法:

这个方法可以创建一个具有相同元素的新数组实例,如

var arr = Array.of(1, 2, 3);

四、使用数组的方法

创建了数组后,我们就可以对它进行各种操作了:

1、访问和修改数组元素

要访问和修改数组元素,需要使用数组的索引。数组的索引从0开始,依次递增。要访问数组元素,可以使用以下语法:

console.log(arr[0]); // 输出第一个元素
arr[1] = 4; // 修改第二个元素的值

2、向数组末尾添加元素

要向数组的末尾添加一个元素,可以使用数组的push()方法。该方法 会在数组的末尾添加指定的元素。以下是使用push()方法向数组末尾添加元素的示例:

arr.push(5);

3、从数组末尾移除元素

要从数组的末尾移除一个元素,可以使用数组的pop()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用pop()方法从数组末尾移除元素的示例:

arr.pop();

4、从数组末尾移除元素
要从数组的末尾移除一个元素,可以使用数组的unshift()方法。该方法 会移除并返回数组中的最后一个元素。以下是使用unshift()方法从数组末尾移除元素的示例:

arr.unshift(0);

5、从数组开头移除元素
要从数组的开头移除一个元素,可以使用数组的shift()方法,并将索引值设置为0。该方法 会移除并返回数组中的第一个元素。以下是使用shift()方法从数组开头移除元素的示例:

arr.shift();

6、获取数组的长度
要获取数组的长度,可以使用内置函数length()。length()函数返回数组中元素的个数。以下是获取数组长度的示例:

console.log(arr.length);

7、遍历数组

要遍历数组的所有元素,可以使用for循环。下面是遍历数组的示例:

for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

8、数组排序

要对数组进行排序,可以使用JavaScript内置的sort()方法。下面是对数组进行排序的示例:

arr.sort();

9、数组反转

要对数组进行反转,可以使用JavaScript内置的reverse()方法。下面是对数组进行反转的示例:

arr.reverse();

10、数组搜索
要在数组中搜索特定的元素,可以使用循环遍历数组,逐个比较每个元素与目标值,找到目标值后返回其索引。下面是一个示例代码:

console.log(arr.indexOf(3)); // 返回3在数组中的索引位置
console.log(arr.includes(4)); // 检查数组中是否包含4

以上就是一些常见的数组操作方法,可以根据需要使用适当的方法来操作数组中的元素。

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

五、使用数组方法的注意事项

  • 数组方法是JavaScript中针对数组对象的内置方法,可以方便地对数组进行操作和处理。

  • 使用数组方法之前,需要先创建一个数组对象。可以使用数组字面量创建一个数组,也可以使用Array()构造函数来创建一个数组。

  • 数组方法可以改变原始数组,也可以返回一个新的数组。需要根据实际需求来选择使用具体的方法。

  • 改变原始数组的方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()等。

  • 不改变原始数组的方法包括:slice()、concat()、join()、map()、filter()、reduce()、forEach()等。

  • 使用数组方法时需要注意方法的参数和返回值,不同的方法可能需要不同的参数,并且返回值类型也可能不同。

  • 数组方法的具体用法可以参考JavaScript官方文档或者其他相关教程和资源。熟练掌握数组方法可以提高代码的效率和可读性。

以上就是JavaScript数组的创建和使用方法,希望对你有所帮助。记住,数组是JavaScript中非常重要的一部分,掌握好它可以让我们的编程工作更加高效。

收起阅读 »

Chat Gpt详细教程:手把手带你Open AI 的API对接

AI
今年4月最大的一个瓜,就是Open AI全面免费开放了,所以很多人想白漂API但却不知该如何去获取Open AI的API,甚至好多小伙伴都碰壁在注册Open AI的半路上了,甚至是如何开通海外付费都变成一个难题~所以针对以上的问题,我将出一份教程为大家一一解决...
继续阅读 »

今年4月最大的一个瓜,就是Open AI全面免费开放了,所以很多人想白漂API但却不知该如何去获取Open AI的API,甚至好多小伙伴都碰壁在注册Open AI的半路上了,甚至是如何开通海外付费都变成一个难题~

所以针对以上的问题,我将出一份教程为大家一一解决。当然,本次教程全程是由本人跑过一遍的,本人亲测不封号、不踩雷、不墨迹。

Description

在整个的注册、激活Open AI账户、升级Open AI使用级别、对接API等等都会借助其他工具而产生一些费用,请各位老板慎重考虑并尝试。

在教程学习中产生的其他平台的工具费用与任何问题都与本教程无关,请各位老板悉知。

注意:所有工具的昵称全部统称为“XXX国际旅游卡”担心被优化了~ 不懂的可以问。

话不多说,开始进入正题。

一、第一步,注册谷歌邮箱

大家去下载一个谷歌浏览器,然后安装到电脑上,再去寻一个稳定“梯子”。准备工作就算完了。接下来给大家演示注册流程哈~

咳咳~ 有一些小伙伴可能不理解什么是“梯子”请自行百度搜索哈。或者可以来咨询我给你推荐一个好用的。

1、在谷歌网页输入Goolgle的地址:www.google.com,点击右上角的登录页面。
Description
2、跳转到登录页面后,点击“创建账号”,选择个人用途。下一步。
Description
3、填写基本信息,我一般会选择把名字写成英文字母,因为这样会显得很高级。Hhh~
Description
4、填写生日,随便写也可以,完全不影响后面的使用。
Description
5、创建你自己的Gmail地址,也可以使用默认生成的Gmail地址。看个人喜好了。
Description
6、设置密码,这一步重要的不是设置密码,而是保存好密码。省的到时候记不得密码了。我一般会写在我的便签里,方便后面查找使用。
Description
7、这里填写一个你的QQ邮箱即可,方便后续重要操作。多一个保障么~如果你不打算长期用的话,就直接跳过即可。
Description
8、确认信息,下一步,阅读协议,同意,注册成功!恭喜恭喜~
Description
Description
9、修改谷歌个人资料,不想改的跳过即可。
Description
10、首次登录后会出现验证登录的情况。重新验证登录一下就OK了
Description
11、手机号绑定验证,这个时候写国内的手机号码即可,短信都是秒到。输入验证码,就OK了

验证码是G- 开头的,输入后面的纯数字就OK。
Description
Description
成功登录!记得自己切换成中文模式哦~

二、第二步,注册Open AI

1、访问Open Ai 官网,点击登录。
Description
2、登录验证,正常跟着指示操作即可。
Description
配合验证就OK,
3、关键的时刻到了,使用谷歌账号登录!省去一切繁琐步骤。
Description
4、选择当前已登录的谷歌账号即可。没什么技术含量了~ 跟着步骤走就准没错。
Description
5、点击继续,(要是英文看着难受,就点下面的切换语言即可,不切换的话点击的位置都是一样的。)
Description
6、创建Open AI的基本账户信息,最好是英文,你写中文也行,就是后面会显示的很奇怪。
Description
示范的模板~ 按着下面的格式去写就OK了。
Description
7、点击前往获取API,这点毋庸置疑了。先把API拿到手,再去体验GPT吧!
Description
8、到达了主页面!点击侧边的菜单栏,选择API Keys——创建新的API,看图吧!
Description
Description
9、这一步很重要了!非常的重要。在首次获取API的时候,Open AI会要求你验证手机号的!国内手机暂时不大行,所以这个时候你需要一个海外的旅游卡,这是重点来咯!!!

三、第三步,国际SIM旅游卡

1、现在我们需要借助一个工具!国际SIM旅游卡租赁~ 我们去访问s ms-activate。短期出国、酷爱外服游戏的的朋友们应该清楚这玩意儿的好处。此处不做过多讲解。
Description
2、这里的注册流程与前面一样,选择用谷歌账号登录即可,这里不用过多的废话,里面都是中文介绍的方便很多。但是记得先充值点余额进去,方便后面使用它的短信接受功能。
Description
3、充值的话国际旅游卡支持多重支付方式,也包括了咱们国内的支付宝。充值的时候选择一个最低档就可以,够你使用了。
Description
4、充值成功后,就不用管它了。我们去租用一个国际SIM旅游卡。在首页选择租用——Whatsapp——选择一个国家的旅游卡
Description
5、但你租赁成功后,在右边会显示你的旅游卡的SIM卡 号,这个时候你就可以去激活Open ai 的API 了。(回到第二步的第7点位置,应该不需要再截图给大家看了~ )

提示:它的最低租用时效是4个小时,4个小时够你随便玩耍了。

四、第四步,创建API

1、回到openai 界面后点击创建秘钥——填写项目昵称——选择项目类——全部权限——创建秘钥。
Description
Description
2、这一步非常的关键!一定要点复制,然后保存好,API秘钥仅在首次能看见全部秘钥,等你退出这个界面的时候,就再也看不到全部秘钥了。切记切记!!!
Description
好了,这一步你已经成功的获取秘钥了!!!接下来是升级Open AI的账户级别了。

五、第五步,升级Open AI使用级别

首先点击绑定国际旅游卡信息~
Description
我们的账户默认级别是0,我们需要升级到级别1。
Description
下图为未升级级别是对接API的报错信息,不信的可以去试试~ 白漂还是有点门槛的。
Description
但是我们没有国际卡可怎么办呢?别急,教都教了怎么可能教一半呢?各位老板继续往下看!


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


六、第六步,注册+绑定

我们需要通过国际旅游卡去升级Open AI的使用级别,这一步是需要付费的哦。

首先我们要去访问bewildcard。

1、点击登录,直接用国内的号码即可登录。这里无需多说什么咯~
Description
2、我的卡片——选择一年——支付。后面的步骤就不详细展示了,基本按着提示去操作就OK了。大家选择ZFB认证会比较顺畅一些。
Description
在这个界面里按着图片步骤走即可,中英文的显示都一样。
Description
3、当你将卡片注册成功后,在“我的卡片”里会显示你的卡片信息了,如下图:
Description
4、此时你的卡片中余额是0,Wild Card 最低起步是10美元~ 所以各位老爷都懂的~ 含泪付吧…舍不得余额套不着API……
Description
5、返回Open AI官网,绑定Open AI的账户,升级使用级别。
Description
6、选择个人个体账户。
Description
7、填写详细信息。在这里要注意这个CVC,是你卡片的安全码,记得填上,是需要验证的。按着旅游卡的卡片信息去填写下面的即可。
Description
OKK,一切都大功告成!!!当你将账户“使用级别”升级成功后,可以开开心心的去对接API进行使用了。一下是使用级别1的权限了,各位老板要详细阅读哦~
Description
Description
以下是我做测试时用的真实数据,如果大家想长期使用的话,建议每个月定期给国际旅游卡(WildCaard)充上10美元,避免断粮导致的各种报错~

往往我们会最容易忽略这一点~ 然后开始在程序里疯狂找报错原因,哈哈哈~
Description
好了,本次的教程到此结束了。如果在这过程中还有什么不懂的,可以来与我交流哦。

最后祝各位老板,身体健康,工作顺利!拜拜咯~

收起阅读 »

关于“明显没有bug的代码”的一些拙见

以前听过过一个有趣的说法:不要编写没有明显bug的代码,要去编写明显没有bug的代码。这里提到的两个概念:“没有明显bug的代码”和“明显没有bug的代码”。同样的文字,只是调换了下顺序,表达的就是完全不同的概念了。前者“没有明显bug的代码”大概是最常见的代...
继续阅读 »

以前听过过一个有趣的说法:不要编写没有明显bug的代码,要去编写明显没有bug的代码。这里提到的两个概念:“没有明显bug的代码”和“明显没有bug的代码”。同样的文字,只是调换了下顺序,表达的就是完全不同的概念了。前者“没有明显bug的代码”大概是最常见的代码了,特征就是:



  1. 每段程序看起来合理,但结果就是不对

  2. 程序看起来复杂、奇怪,但就是可以正常运行

  3. 天书一般的程序

  4. 待补充


平时工作中到处缝缝补补的代码大概就是这种代码吧。背后的原因一般比较复杂,有时还不可追溯,项目工期紧,人员交接等等都有可能。因此,与其思考“如何避免没有明显bug的代码”,还不如思考“如何写出明显没有bug的代码”。本文就何为“明显没有bug的代码”总结一些个人的思的胡思乱想,阐述这类代码的几个特征。


特征1:代码简短


“明显没有xx”意味着一眼能看出来,而“一眼”这个条件就有很大的限制。如果给我一个函数,包含1000多行代码,我鼠标滚轮要滚好久,才能过完一遍代码,那么这种代码一定不是“明显没有bug的代码”。那么,反过来说,“明显没有bug的代码”一定是短小的代码。比如,Java中的Objects.equals方法:


public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

这一段代码简短到,代码跟功能定义的文字篇幅差不多,连写文档注释的必要都没有了。还有一个更极端的例子是Java的Objects.isNull方法:


public static boolean isNull(Object obj) {
return obj == null;
}

简直就是一段“废话”。


特征2:功能完整且连贯


“一眼能看出”还意味着功能不能太分散,如果一个功能,分散在十多个函数或文件中,那么看这段功能就得在很多代码片段中跳来跳去,这个就需要开发者来阅读代码时充当一个“人肉解释器”的角色,在大脑中把各个代码片段组合起来才能明白整个流程和细节,这无疑降低阅读代码的效率,bug也容易隐藏在各个代码片段的“缝隙”中。举个常见的例子:在图形界面应用中,用户登录后,弹出登录成功的提示,然后关闭登录页面。一种普通的实现是:


//1. 在登录按钮触发登录操作
loginButton.setOnClickListener(v -> controller.login(username, poassword))

//2. 在登录成功的回调中展示弹窗
public void onLoginSuccess(User user){
LoginSuccessDialog.show("login success")
}

//3. 在失败的回调中展示错误信息
public void onLoginFailed(String errorMsg){
MessageDialog.showMessage(errorMsg);
}

//4. 在LoginSuccessDialog确认后关闭页面
public void onLoginDialogConfirmed(){
loginPage.close();
}

看上去好像解耦很不错,但功能都变得七零八落。要拼凑出完整的功能大概得仔细阅读整段代码,更别提“一眼看出”了。那么一眼能看出的代码大概长啥样呢?我想,大概是这样:


loginButton.setOnClickListener(v -> {
controller.login(username, poassword)
.onSuccess(user ->
LoginSuccessDialog.总而言之,解耦需谨慎,不要因过度解耦而牺牲了内聚性和连贯性。show("login success")
.onConfirmed(() -> loginPage.close()))
.onFailed(errorMsg ->
MessageDialog.showMessage(errorMsg));
});

这是对该视图流程的一个连贯的描述,而且篇幅更短。至于获取到用户数据存储到本地数据库、通知其他页面更新等操作,跟当前视图没有关系,也就不需要放在这段代码里。总而言之,解耦需谨慎,不要因过度解耦而牺牲了内聚性和连贯性。


特征3:良好的表达


代码的篇幅得到控制后,要让人一眼看懂,还需要容易理解才行。设计心理学提出“设计传达所有必要的信息,创造一个良好的系统概念模型,引导用户理解系统状态,带来掌控感”。程序设计也是如此,代码是程序功能的文本表达,需要传达对应信息来让人产生该功能正确的概念模型。


以一个常见的上传图片的弹窗为例,思考一个菜单弹窗,包含取消和两个功能按钮:从相册选择和拍照上传,例如下图这样。


在这里插入图片描述


那么对应的代码可以表达为:


MenuDialog.create()
.withAction("拍照上传", dialog -> {
takePhoto();
})
.withAction("本地上传", dialog -> {
chooseFromGallery();
})
.onCancel(() -> {
//do something
})
.show();

或者


MenuDialog.create()
.withAction("拍照上传", controller::takePhoto)
.withAction("本地上传", controller::chooseFromGallery)
.onCancel(() -> {
//do something
})
.show();

没有多余的代码,该有的信息都表达到位,而且和实际功能有良好的对应关系。


4. 特征4:可验证正确性


代码可以让人一眼看懂之后,那么判断其有没有bug,还有一个重要前提:这个代码是有正确性可言的,可以被验证。


例如,来看下面这段随意的代码:


int type;
boolean isClosed;

void doSomething(String text) {
if (type == 0) {
if (isClosed) {
println(text);
} else {
error("something wrong");
}
} else if (type == 1) {
//do something
}
}

这段程序简短、易读,但是doSomething函数的行为依赖两个外部变量,而这两个外部变量又容易被其他地方随意改动。比如,type的定义域为1、2、3,但如果type新增类型4的时候或者被错误地赋值为-1的时候,这个doSomething函数的行为还是正确的吗?doSomething函数的正确性依赖于type变量的正确性,那么又依赖于读写type变量的程序的正确性,这样的程序是难以验证的。而且,对上下文依赖越多的程序,越难以产生明确的定义,因为这个定义也依赖上下文的定义。定义不明确,更难以验证内容的正确性。


相比之下,Objects.equalsObjects.isNull方法有着明确的定义,而且不受上下文影响,可以一眼就看出对错。而下面这段代码:


MenuDialog.create()
.withAction("拍照上传", controller::takePhoto)
.withAction("本地上传", controller::chooseFromGallery)
.onCancel(() -> {
//do something
})
.show();

表达明确,可以快速判断出程序行为是否正确、符合期望。即便MenuDialog出现异常,或者takePhotochooseFromGallery出了什么问题,也不需要来修改这段程序。


不过,程序验证是一个有点高深的科研方向,要严格验证一个程序的正确性是很困难的一件事,不过我们仍然可以试着去编写一些“看起来”正确的程序。(利用函数式编程思想写出来的代码通常容易验证一些)


作者:乐征skyline
来源:juejin.cn/post/7236010330051887164
收起阅读 »

程序员黑话之故障专辑(中英文对照版)

正好最近业内接连发生了几起影响比较大的故障,那我们就专门做一期「故障专辑」吧。 故障 故障有好几种叫法,比较正式的 故障 - Outage 事故 - Incident 不怎么严重的,时间很短的 抖了一下 - Jitter(多用于网络) Hiccup (中...
继续阅读 »

正好最近业内接连发生了几起影响比较大的故障,那我们就专门做一期「故障专辑」吧。


故障


故障有好几种叫法,比较正式的



  • 故障 - Outage

  • 事故 - Incident


不怎么严重的,时间很短的



  • 抖了一下 - Jitter(多用于网络)

  • Hiccup (中文翻译是打了个嗝,不过中文里貌似没有这个讲法)


通俗点的说法



  • 挂了/崩了- Down
    file


500


当在请求某个网络资源时,服务器内部发生错误时,返回的错误编号。扩展为系统发生内部故障。


file


变更


虽然突然的流量暴涨,或者光缆被挖断,数据中心着火,被雷劈都有可能,但绝大多数时候,故障都是变更导致的。


file


变更分为三大类:



  • 代码变更 - Code Change

  • 配置变更 - Config Change

  • 数据库变更 - Database Change


左移 (shift-left)


降低变更风险的一个方法,就是做变更前检查,问题越早发现越好。因为变更的流水线是从左往右画的,起点在左边。所以左移就是把检查尽量靠近起点。


金丝雀 (Canary)


以前矿工下井,会带一只金丝雀,如果井下空气出现状况,更敏感的金丝雀会先有异常。这个概念也带到了软件研发里。会循序渐进地做变更。另外一种叫法是灰度 (Grayscale)。


file


单元化/区域化 (Regionalization)


在互联网公司逐渐普及的架构,主要由 AWS 发扬光大,把服务进行隔离。


爆炸半径 (Blast Radius)


金丝雀和单元化都是为了降低爆炸半径,减少故障的影响面。


file


值班 (On-call)


也叫 Carry the pager。以前带着的传呼机叫做 Pager。现在传呼机被手机/软件取代了,但 Pager 这个名字沿用了下来。


file


复盘 (Postmortem)


原义是尸检报告。在软件研发领域,指详细的故障分析报告。


惊群 (Thundering Herd)


file


打雷后,动物一下子被惊醒了,到处乱窜,造成混乱。在故障恢复阶段要小心的问题,很容易刚拉起一个服务,立马又被积压的请求打挂。


结语


船停在港口是最安全的,但那不是造船的目的。软件需要持续的变更迭代,变更就有风险。但研发团队可以通过引入工具,来降低风险,针对一开始变更的三种类型,市面上也有成熟的开源方案:
代码变更 - 老牌的有 Jenkins,新兴的有 Drone CI 和 Zadig




作者:Bytebase
来源:juejin.cn/post/7301244964297670693
收起阅读 »

室友打一把王者就学会了Java多线程

大家好,我是二哥呀。 对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说: 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发...
继续阅读 »


大家好,我是二哥呀。


对于 Java 初学者来说,多线程的很多概念听起来就很难理解。比方说:



  • 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。

  • 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。


很抽象,对不对?打个比喻,你在打一把王者(其实我不会玩哈 doge):



  • 进程可以比作是你开的这一把游戏

  • 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。


带着这个比喻来理解进程和线程的一些关系,一个进程可以有多个线程就叫多线程。是不是感觉非常好理解了?


进程和线程


❤1、线程在进程下进行


(单独的英雄角色、野怪、小兵肯定不能运行)


❤2、进程之间不会相互影响,主线程结束将会导致整个进程结束


(两把游戏之间不会有联系和影响。你的水晶被推掉,你这把游戏就结束了)


❤3、不同的进程数据很难共享


(两把游戏之间很难有联系,有联系的情况比如上把的敌人这把又匹配到了)


❤4、同进程下的不同线程之间数据很容易共享


(你开的那一把游戏,你可以看到每个玩家的状态——生死,也可以看到每个玩家的出装等等)


❤5、进程使用内存地址可以限定使用量


(开的房间模式,决定了你可以设置有多少人进,当房间满了后,其他人就进不去了,除非有人退出房间,其他人才能进)


创建线程的三种方式


搞清楚上面这些概念之后,我们来看一下多线程创建的三种方式:


继承 Thread 类


♠①:创建一个类继承 Thread 类,并重写 run 方法。


public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":打了" + i + "个小兵");
}
}
}

我们来写个测试方法验证下:


//创建MyThread对象
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
//设置线程的名字
t1.setName("鲁班");
t2.setName("刘备");
t3.setName("亚瑟");
//启动线程
t1.start();
t2.start();
t3.start();

来看一下执行后的结果:



实现 Runnable 接口


♠②:创建一个类实现 Runnable 接口,并重写 run 方法。


public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "打了:" + i + "个小兵");
}
}
}

我们来写个测试方法验证下:


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
t2.start();
t3.start();

来看一下执行后的结果:



实现 Callable 接口


♠③:实现 Callable 接口,重写 call 方法,这种方式可以通过 FutureTask 获取任务执行的返回值。


public class CallerTask implements Callable<String> {
public String call() throws Exception {
return "Hello,i am running!";
}

public static void main(String[] args) {
//创建异步任务
FutureTask<String> task=new FutureTask<String>(new CallerTask());
//启动线程
new Thread(task).start();
try {
//等待执行完成,并获取返回结果
String result=task.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

关于线程的一些疑问


❤1、为什么要重写 run 方法?


这是因为默认的run()方法不会做任何事情。


为了让线程执行一些实际的任务,我们需要提供自己的run()方法实现,这就需要重写run()方法。


public class MyThread extends Thread {
public void run() {
System.out.println("MyThread running");
}
}

在这个例子中,我们重写了run()方法,使其打印出一条消息。当我们创建并启动这个线程的实例时,它就会打印出这条消息。


❤2、run 方法和 start 方法有什么区别?



  • run():封装线程执行的代码,直接调用相当于调用普通方法。

  • start():启动线程,然后由 JVM 调用此线程的 run() 方法。


❤3、通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好?


实现 Runable 接口好,原因有两个:



  • ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。

  • ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。


控制线程的其他方法


针对线程控制,大家还会遇到 3 个常见的方法,我们来一一介绍下。


1)sleep()


使当前正在执行的线程暂停指定的毫秒数,也就是进入休眠的状态。


需要注意的是,sleep 的时候要对异常进行处理。


try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}

2)join()


等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
try {
t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();

来看一下执行后的结果:



3)setDaemon()


将此线程标记为守护线程,准确来说,就是服务其他的线程,像 Java 中的垃圾回收线程,就是典型的守护线程。


//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");

t1.setDaemon(true);
t2.setDaemon(true);

//启动线程
t1.start();
t2.start();
t3.start();

如果其他线程都执行完毕,main 方法(主线程)也执行完毕,JVM 就会退出,也就是停止运行。如果 JVM 都停止运行了,守护线程自然也就停止了。


小结


本文主要介绍了 Java 多线程的创建方式,以及线程的一些常用方法。最后再来看一下线程的生命周期吧,一图胜千言。



好了,如果你想学好 Java,GitHub 上标星 10000+ 的《二哥的 Java 进阶之路》不容错过,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准二哥的 Java 进阶之路😄。


Github 仓库:github.com/itwanger/to…


码云仓库(国内访问更快):gitee.com/itwanger/to…


star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。



把二哥的座右铭送给你:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟


作者:沉默王二
来源:juejin.cn/post/7329413905028186124
收起阅读 »

你还以为前端无法操作文件吗

web
这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还...
继续阅读 »

这里面有个值得说明一点的问题是,我一直以为(可能有人跟我一样)前端是无法操作文件的,可实际上自从HTML5标准出现之后,前端虽然无法像后端那样能灵活的进行文件处理,但因为有了File System Api这套接口,前端也能够进行简单的文件处理操作(不只是读,还有写)。当然,网络环境鱼龙混杂,为防止不法网站任意获取和修改用户数据,所有本地文件操作都需要用户手动操作,不能自动保存或打开。

  1. 使用场景

    File System Api为浏览器应用增加了无限可能,比如我们经常用到的一些流程图工具,上面的保存到本地的功能,就不用再依赖后端,可以直接将数据保存到本地的文件系统中,下次打开时选中本地的指定文件,可以直接加载到浏览器中,大大提高的前端的能力边界。

  2. 功能描述

    我们就利用File Access Api搞一个简单的在线编辑器,能实现的功能如下:

    第一步,新建一个文件,命名为hello.txt,并填写初始信息 "hello world"

    第二步,打开文件,修改文件内容为“hello world,hello you!”

    第三步,保存文件

editfile.gif

  1. 实现方式概述

    直接看代码:

    <template>
     <div>
       <el-button type="primary" @click="editFile">编辑文件el-button>
       <el-button type="primary" @click="saveFile">保存文件el-button>
       <el-input
           type="textarea"
           :rows="20"
           placeholder="请输入内容"
           v-model="textarea">
    el-input>
     
     div>
    template>

    <script>
    export default {
       data() {
           return {
               textarea: ''
          }
      },
       methods: {
           editFile: async function() {
               // 选择文件
               let [fileHandle] = await window.showOpenFilePicker()
               // 复显文件内容
               fileHandle.getFile().then(blob => {
                   blob.text().then(val => {
                       this.textarea = val
                  })
              })
          },
           saveFile: async function() {
               // 新建一个文件
               const fileHandle = await window.showSaveFilePicker({
                   types: [
                      {
                           description: 'hello',
                           accept: {
                               'text/plain': ['.txt']
      // 对于一些非常用的后缀,均使用这种方式进行定义
                               // 参考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
                               // 'application/octet-stream': ['.a','.b']
                          }
                      }
                  ]
              })
               // 在文件内写入内容,写入内容用的是Stream Api,流式写入
               const writable = await fileHandle.createWritable();
               await writable.write(this.textarea);
               await writable.close();
          }
      }
    }
    script>

    可以看到,只需要短短的几行代码就可以完成本地文件的修改,需要注意的是,文件的保存不是实际意义上的修改,而是新建一个文件,进行替换,然后在新的文件里写入最新信息进行的修改。

    另:File System Api目前支持程度还不够普遍,从mdn上来看,大多数api上还有Experimental: This is an experimental technology Check the Browser compatibility table carefully before using this in production.的描述,使用前需要确认好是否满足浏览器要求。


作者:DaEar图图
来源:juejin.cn/post/7365679089811947561
收起阅读 »

眼看他搭中台,眼看他又拆了

曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之际,我们可以发现各大企业又在反其道而行,纷纷不断进行“拆中台”,那么中台对于企业而言,究竟发...
继续阅读 »

曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之际,我们可以发现各大企业又在反其道而行,纷纷不断进行“拆中台”,那么中台对于企业而言,究竟发挥了哪些作用,当前又出现了哪些问题?今天,我们特邀了高级研发管理专家、腾讯云 TVP 程超老师,他将从搭中台到拆中台的风向转变,探讨企业软件架构的底层逻辑。



中台都在忽悠吗?都被忽悠瘸了?我们都在悄悄淘汰中台,你们还在建?最近网上充斥大量文章和观点,都在说中台过时。为什么会这样说?是因为成本与复杂性?技术限制与业务变化?还是因为组织变化?为什么会这样呢?且听我一一分析。


众所周知,中台是指企业内部的中间层平台,负责连接上下游系统,提供数据和功能服务。而在过去几年中台概念曾经风靡一时,甚至被认为是企业数字化转型的关键。然而,近年来,一些企业确实出现了对中台战略的重新评估,不再像之前那样盲目地追求中台建设。其实,中台的概念兴起于企业数字化转型的浪潮中,企业开始意识到传统的前台系统(如客户端应用)与后台系统(如企业资源规划系统)之间的断层,而中台则被认为是弥合这种断层的理想方式。


值得一提的是,关于中台的定义,业内大佬也曾经发表过一些观点:


提炼各个业务条线的共性需求,并将这些打造成组件化的资源/能力包,然后以接口的形式提供给前台各业务部门使用,这样就可以最大限度地避免“重复造轮子”的问题,也让每一个新的前台业务创新能够真正意义上“站在巨人的肩膀上”,而不用每次开辟一个新业务都像新建一家创业公司那么艰难,甚或更为艰难。——某企业资深架构师 钟华


总结而言,中台的核心点主要有以下三个:



  • 中台是为前台而生。

  • 提炼各业务条线的共性需求。

  • 减少“重复造轮子”的时间与资源浪费。


01四大层面解读中台备受追捧原因


2015年,业界首次提出“大中台、小前台”战略,是想打造统一技术架构、产品支撑体系、数据共享平台、安全体系等等,把整个组织“横”过来,支撑多种多样的业务形态。中台似乎已经成为行业标配,稍有规模的公司都建设了自己的中台,掀起了一股强劲的中台风。


中台能够解决哪些问题呢?在我看来,主要有以下四种:



  • 项目重复造轮子严重,无法形成抽象共用


中台提供了一种在企业内部建立统一的技术平台或者服务平台的模式。这个平台可以被不同部门或者项目共享和复用,从而减少了重复开发的情况。随着新业务的不断接入,共享服务也从仅能提供单一的业务功能,不断的自我进化成更健壮更强大的服务,不断适应各种业务线的新需求。同时在数据积累方面,通过数据中台将各业务的数据都沉淀下来,不断地积累数据,发挥数据的最大威力。



  • 业务变化快,缓慢的研发流程难以迅速响应


很多企业开发响应慢,其实大部分都是因为数据问题,没有做到实时、准确和统一。比如一家公司的订单,分为 C 端订单,B 端订单,共享单车订单等等,这些订单分管在不同部门中,想要做订单统计、预测等就比较困难,各类型订单彼此割裂,而如果企业只有一个订单中心的话,数据就能够在不同场景下感知到业务的变化和联动。



  • 提高资源利用率和研发效率



说起如何提高资源利用率和研发效率,我总结为中台建设五步法:插件化、服务化、配置化、异步化和数据化。这五步环环相扣,其中插件化就是提高研发效率的关键点,我们将对核心交易流程进行抽象建模设计,并通过流程引擎的改造,实现增加多个插件和扩展点。这样,不同的业务场景可以根据需求自定义其个性化逻辑,将整个交易环节抽象为一个流程框架,并在其基础上引入一系列业务扩展。这种设计使得各业务间互不干扰,更灵活地满足各自需求。


提高资源利用率,这也是必然的,服务、数据、组件等形成统一复用,各资源也不再分散,只需通过一套服务来做支撑,并且可以通过各业务线的忙闲情况,做资源的调控、比如某个业务线使用交易中台服务,高峰时期是在早上8点到晚上12点,凌晨以后基本没有业务量,则可以考虑把针对这个业务线的资源配置降低,从而实现降本增效。



  • 提高系统稳定性和可靠性


一般来说系统的故障由三个方面引起,系统 bug、变更配置、并发流量变化。而技术中台避免了各个部门为解决自身技术问题而随意修改系统设置和配置的情况,这样做有助于防止整个系统因为随意修改而出现不稳定和安全问题。


02拆分中台并非全盘否定中台


前面我主要介绍了中台能解决哪些问题,但其实很多企业在实际引入中台的过程中,也遇到了很多问题:



  • 中台与前台的边界模糊


很多前台的业务让中台接管开发,到底是接还是不接?中台的角色和范围缺乏明确界定,导致中台与业务之间的责任划分模糊不清,引发了重复建设、资源浪费和沟通成本等问题。



  • 稳定性与灵活性的冲突


稳定与灵活一直是个矛盾体,中台接入的业务线非常多,一旦出问题影响面巨大,代码质量如何把控、上线流程如何稳定、业务如何做好隔离,都需要考虑清楚。



  • 沟通障碍与目标差异


协调中台团队和业务团队之间的沟通和合作,平衡双方的需求和利益,以及处理中台和业务之间的依赖和变更,都是一项复杂的管理任务。



  • 中台规划与业务需求之间的平衡


中台的服务需求和响应之间存在不匹配,这导致中台无法满足业务的多样化和个性化需求。有时中台过度迎合业务的短期需求,却牺牲了其长期规划和可持续发展。



  • 利益分配


距离业务近的地方,比距离业务远的地方更能得到公司增长的成果,中台看似业务,其实只是沉淀,追求的是稳定和灵活。还有业务下沉的时候,会涉及到与中台的业务交接,前台业务必定会减少。如果是部门划到中台,是否会有人员变动?当中台的服务价值和收益缺乏清晰界定,将难以有效衡量自身的贡献和影响。


综上,中台看似很美好,但很多企业在实际落地的时候却因为遇到这些问题,导致陷入困境,中台建设越建越复杂,甚至有些企业对中台也逐渐失去了信心,反而成了阻碍企业发展的瓶颈。


近两年业界开始风行“拆中台”策略——将中台变“薄”,拆分到多个独立的业务单元。这使得很多企业又开始认为中台已成明日黄花,引进中台并不是一个好选择,甚至有些企业将自身发展不顺的原因也归在了中台上面,一时间中台被全盘否定了。


我个人则认为拆分中台并非全盘否定中台,而是基于自身发展阶段和市场环境的变化进行战略调整和优化。“天下大事,合久必分,分久必合”,这就意味着在中台的管理和战略中,必须根据具体情况来做出分合的决策。有时候,将中台进行分散管理或者分解成更小的部分可能更为合适,因为这样有助于更好地满足各个业务单位的需求,提高灵活性和适应性。互联网大厂们将庞大而僵化的共享中台重新组织为灵活的业务域中台,可以更好适应具体业务场景和用户需求,既能保留中台提供通用能力和协同效率的优势,又能增加中台的灵活性和个性化。


03企业应该因地制宜选择是否需要中台


首先,我想强调的是,“中台”本身并不是一个新的架构思想,这个架构思想早在若干年以前就已经有了,很多企业已经是这么做了,就像面向对象编程语言中(Java)高内聚,低耦合,便是这种思想。


当企业处在初创期,随着业务发展产生多条业务线或产品线的时候,就会面临协同方面的挑战,如果每条业务线都要自己成立技术、运维、数据等部门,这样显然是非常浪费人力和资源的。为了适应快速发展的业务,就需要成立中台部门,来抽取、复用共性的东西,形成统一,这样既能满足“小前台,大中台”策略,让业务快跑抢占市场,中台提供稳定的炮火支援,又能提高协同和研发效率。参考示意图如下:



当企业已经渡过初创期,发展已经具有较大规模时,各条业务线人员和业务场景也比初创时更加庞大和复杂,企业了将面临更加多样化的市场,以及强大的响应能力,甚至每条业务线都要独立去创新,这样统一的中台部门就会变成瓶颈,人员、响应时间、需求变化和沟通等都会成为阻碍多样化需求的绊脚石。这时候企业就需要根据市场需要,将庞大而僵化的大共享中台,拆分到各业务单元中,将中台下沉到各业务单元中,这样既能保留中台的通用和协同能力,又能针对具体业务和场景不断增加灵活性和定制性。参考示意图如下:



总而言之,中台不是一直不变的,它需要根据市场需求不断进化,演变成能够满足当前企业市场需要的形态。中台不是万能的,它只是企业数字化转型的一种重要实现路径,我们不能对中台有过高的期望,而是应该理性地回归到企业数字化转型的价值上来。


作者简介


程超,腾讯云 TVP,高级研发管理专家,14年 Java 研发经验,8年技术管理和架构经验,曾任京东架构师,易宝支付和松果出行架构技术负责人,熟悉支付和电商领域,擅长微服务生态建设和运维监控,对 Dubbo、Spring Cloud 和 gRPC 等微服务框架有深入研究,并应用于项目,帮助过多家公司进行过微服务建设和改造,目前正在建设业务中台。 合著作品《深入分布式缓存》和《高可用可伸缩微服务架构》,极客时间每日一课讲师和出品人,CSDN 博主专家。


作者:腾讯云开发者
来源:juejin.cn/post/7366175769602932755
收起阅读 »

如果按代码量算工资,也许应该这样写

前言 假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢? 要在增加代码量的同时提高代码质量和可维护性,能否做到呢? 答案当然是可以,这可难不倒我们这种摸鱼高手。 耐心看完,你一定有所收获。 正文 1. 实现更多的...
继续阅读 »

前言


假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢?


要在增加代码量的同时提高代码质量和可维护性,能否做到呢?


答案当然是可以,这可难不倒我们这种摸鱼高手。


耐心看完,你一定有所收获。


giphy.gif


正文


1. 实现更多的接口:


给每一个方法都实现各种“无关痛痒”的接口,比如SerializableCloneable等,真正做到不影响使用的同时增加了相当数量的代码。


为了这些代码量,其中带来的性能损耗当然是可以忽略的。


public class ExampleClass implements Serializable, Comparable<ExampleClass>, Cloneable, AutoCloseable {

@Override
public int compareTo(ExampleClass other) {
// 比较逻辑
return 0;
}

// 实现 Serializable 接口的方法
private void writeObject(ObjectOutputStream out) throws IOException {
// 序列化逻辑
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 反序列化逻辑
}

// 实现 Cloneable 接口的方法
@Override
public ExampleClass clone() throws CloneNotSupportedException {
// 复制对象逻辑
return (ExampleClass) super.clone();
}

// 实现 AutoCloseable 接口的方法
@Override
public void close() throws Exception {
// 关闭资源逻辑
}

}


除了示例中的SerializableComparableCloneableAutoCloseable,还有Iterable


2. 重写 equals 和 hashcode 方法


重写 equalshashCode 方法绝对是上上策,不仅增加了代码量,还为了让对象在相等性判断和散列存储时能更完美的工作,确保代码在处理对象相等性时更准确、更符合业务逻辑。


public class ExampleClass {
private String name;
private int age;

// 重写 equals 方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ExampleClass other = (ExampleClass) obj;
return this.age == other.age && Objects.equals(this.name, other.name);
}

// 重写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}


giphy (2).gif


3. 增加配置项和参数:


不要管能不能用上,梭哈就完了,问就是为了健壮性和拓展性。


public class AppConfig {
private int maxConnections;
private String serverUrl;
private boolean enableFeatureX;

// 新增配置项
private String emailTemplate;
private int maxRetries;
private boolean enableFeatureY;

// 写上构造函数和getter/setter
}

4. 增加监听回调:


给业务代码增加监听回调,比如执行前、执行中、执行后等各种Event,这里举个完整的例子。


比如创建个 EventListener ,负责监听特定类型的事件,事件源则是产生事件的对象。通过EventListener 在代码中增加执行前、执行中和执行后的事件。


首先,我们定义一个简单的事件类 Event


public class Event {
private String name;

public Event(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

然后,我们定义一个监听器接口 EventListener


public interface EventListener {
void onEventStart(Event event);

void onEventInProgress(Event event);

void onEventEnd(Event event);
}

接下来,我们定义一个事件源类 EventSource,在执行某个业务方法时,触发事件通知:


public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void addEventListener(EventListener listener) {
listeners.add(listener);
}

public void removeEventListener(EventListener listener) {
listeners.remove(listener);
}

public void businessMethod() {
Event event = new Event("BusinessEvent");

// 通知监听器:执行前事件
for (EventListener listener : listeners) {
listener.onEventStart(event);
}

// 模拟执行业务逻辑
System.out.println("Executing business method...");

// 通知监听器:执行中事件
for (EventListener listener : listeners) {
listener.onEventInProgress(event);
}

// 模拟执行业务逻辑
System.out.println("Continuing business method...");

// 通知监听器:执行后事件
for (EventListener listener : listeners) {
listener.onEventEnd(event);
}
}
}

现在,我们可以实现具体的监听器类,比如 BusinessEventListener,并在其中定义事件处理逻辑:


public class BusinessEventListener implements EventListener {
@Override
public void onEventStart(Event event) {
System.out.println("Event Start: " + event.getName());
}

@Override
public void onEventInProgress(Event event) {
System.out.println("Event In Progress: " + event.getName());
}

@Override
public void onEventEnd(Event event) {
System.out.println("Event End: " + event.getName());
}
}

最后,我们写个main函数来演示监听事件:


public class Main {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
eventSource.addEventListener(new BusinessEventListener());

// 执行业务代码,并触发事件通知
eventSource.businessMethod();

// 移除监听器
eventSource.removeEventListener(businessEventListener);
}
}

如此这般那般,代码量猛增,还顺带实现了业务代码的流程监听。当然这只是最简陋的实现,真实环境肯定要比这个复杂的多。


5. 构建通用工具类:


同样的,甭管用不用的上,定义更多的方法,都是为了健壮性。


比如下面这个StringUtils,可以从ApacheCommons、SpringBoot的StringUtil或HuTool的StrUtil中拷贝更多的代码过来,美其名曰内部工具类。


public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}

public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}

// 新增方法:将字符串反转
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}

// 新增方法:判断字符串是否为整数
public static boolean isInteger(String str) {
try {
Integer.parseInt(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}

6. 添加新的异常类型:


添加更多异常类型,对不同的业务抛出不同的异常,每种异常都要单独去处理


public class CustomException extends RuntimeException {
// 构造函数
public CustomException(String message) {
super(message);
}

// 新增异常类型
public static class NotFoundException extends CustomException {
public NotFoundException(String message) {
super(message);
}
}

public static class ValidationException extends CustomException {
public ValidationException(String message) {
super(message);
}
}
}

// 示例:添加不同类型的异常处理
public class ExceptionHandling {
public void process(int value) {
try {
if (value < 0) {
throw new IllegalArgumentException("Value cannot be negative");
} else if (value == 0) {
throw new ArithmeticException("Value cannot be zero");
} else {
// 正常处理逻辑
}
} catch (IllegalArgumentException e) {
// 异常处理逻辑
} catch (ArithmeticException e) {
// 异常处理逻辑
}
}
}


7. 实现更多设计模式:


在项目中运用更多设计模式,也不失为一种合理的方式,比如单例模式、工厂模式、策略模式、适配器模式等各种常用的设计模式。


比如下面这个单例,大大节省了内存空间,虽然它存在线程不安全等问题。


public class SingletonPattern {
// 单例模式
private static SingletonPattern instance;

private SingletonPattern() {
// 私有构造函数
}

public static SingletonPattern getInstance() {
if (instance == null) {
instance = new SingletonPattern();
}
return instance;
}

}

还有下面这个策略模式,能避免过多的if-else条件判断,降低代码的耦合性,代码的扩展和维护也变得更加容易。


// 策略接口
interface Strategy {
void doOperation(int num1, int num2);
}

// 具体策略实现类
class AdditionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
}
}

class SubtractionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
}
}

// 上下文类
class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public void executeStrategy(int num1, int num2) {
strategy.doOperation(num1, num2);
}
}

// 测试类
public class StrategyPattern {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;

// 使用加法策略
Context context = new Context(new AdditionStrategy());
context.executeStrategy(num1, num2);

// 使用减法策略
context = new Context(new SubtractionStrategy());
context.executeStrategy(num1, num2);
}
}

对比下面这段条件判断,高下立判。


public class Calculator {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;
String operation = "addition"; // 可以根据业务需求动态设置运算方式

if (operation.equals("addition")) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
} else if (operation.equals("subtraction")) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
} else if (operation.equals("multiplication")) {
int result = num1 * num2;
System.out.println("Multiplication result: " + result);
} else if (operation.equals("division")) {
int result = num1 / num2;
System.out.println("Division result: " + result);
} else {
System.out.println("Invalid operation");
}
}
}


8. 扩展注释和文档:


如果要增加代码量,写更多更全面的注释也不失为一种方式。


/**
* 这是一个示例类,用于展示增加代码数量的技巧和示例。
* 该类包含一个示例变量 value 和示例构造函数 ExampleClass(int value)。
* 通过示例方法 getValue() 和 setValue(int newValue),可以获取和设置 value 的值。
* 这些方法用于展示如何增加代码数量,但实际上并未实现实际的业务逻辑。
*/

public class ExampleClass {

// 示例变量
private int value;

/**
* 构造函数
*/

public ExampleClass(int value) {
this.value = value;
}

/**
* 获取示例变量 value 的值。
* @return 示范变量 value 的值
*/

public int getValue() {
return value;
}

/**
* 设置示例变量 value 的值。
* @param newValue 新的值,用于设置 value 的值。
*/

public void setValue(int newValue) {
this.value = newValue;
}
}

结语


哪怕是以代码量算工资,咱也得写出高质量的代码,合理合法合情的赚票子。


giphy (1).gif


作者:一只叫煤球的猫
来源:juejin.cn/post/7263760831052906552
收起阅读 »

一个30岁前端老社畜的人生经历

前言 在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,...
继续阅读 »

前言


在掘金多年,我一直是一个读者,从事前端快8年了,每天都在看一些视频和资料以及别人的日记,零零碎碎我也做过一些笔记,但是都不成体系。这些笔记至今留存在各种应用上,写了就再也没打开过,还是没有养成习惯,我希望能坚持下去,为自己的人生添加一点历史,等以后老了,我还能证明我的青春有过一些记录,偶尔回味也会是一件比较幸福的事情。


近些年,感觉社会戾气挺重的,特别是疫情的时候,抖音里面的那些评论很让人糟心,现在年轻人也逐渐选择躺平,也是对社会的卷系妥协,随着经济的下滑,一般学校的研究生可能都很难找到一个比较ok的工作,更别提本科或者大专,作为学历真的拿不出手的我,更加焦虑。


从业前端快8年了,做过很多类型的项目,小到一般的H5展示网页,大到区块链应用、智能能源项目;其实回过头来看,没有什么大的成就感,我的从业经验只获得过一次奖杯,就是吃苦耐劳奖一个镀金的大手指,那还是我4年前在一家外包公司连续工作48小时做一个小程序上线后,老板看我确实辛苦,于是发了一个这个奖杯给我,后面被我娃摔坏了,就啥也没有了。


2023


2023年其实回头来看,收获并不是很大,归纳下来也没有几条:


  1. 今年非全研究生在读了

  2. 今年提交了入党申请

  3. 第二套房子装修完成

  4. 小孩来到了身边读书(之前在农村读幼儿园)

  5. 工作中学会了Vue3,能用Java做开发,同时更了解了业务方面的知识

  6. 开始了写作的习惯

  7. 跑了5场马拉松


2024 展望


2024年还有几天就到了,我希望每一年都能有点收获吧,立几个flag:


  1. 带妈妈旅游一次

  2. 成为党员积极分子

  3. 提高Java方面的基础知识,以及three 3D方面的能力

  4. 看不低于5本技术书籍,至少写30篇技术文章

  5. 还清自己的个人债务,当然不包含房贷

  6. 跑5场马拉松


行业展望


目前行业有些自媒体在唱衰,说前端已死,但是我觉得没这么悲观,国家多次强调往智能方向发展,各行业的智能得依托计算机才能智能,像什么智慧制造,智慧能源,智慧农村等等都需要计算机技术来运算和展示。前端只是比以前的要求会高一些了,在5,6年前对前端技术要求没有那么高,大家0基础都可以参与,但如今可能不行了,我觉得这是一个好事,要求高一点,薪资也会高一些。淘汰的,就是一开始就不适合这个行业的人。我目前在一家大型央企工作,还是算较为稳定,但同时也需要不断学习,因为或许某一天的淘汰人选就会是我,社会是残酷的,混日子终究不是一个好的方式。


个人真实经历分享


给大家分享一下我的个人真实经历,与君共勉。


我出生在一个普通的农村家庭,初中开始接触老虎机,高中接触网吧,17岁前没有出过县城,是个十足的井底之蛙,父亲几十年一直在外地工作,一年就回家两三次,从小就我妈妈一个人带我,她做装修的,每天早上7点就骑车外出上班了,下午5点回家,我在家做好饭菜等她回家吃了后,她就马上去田里土里种庄稼,喂了很多牲畜,高中毕业前一直都是这样(不过高中我住校,就我妈妈和我妹在家),我妈妈非常节约,从我记事起,每年只有过年她才会舍得给自己买一两件衣服,因为她觉得过年要穿新的衣服,寓意者新的一年有好的开始,从来没给自己买过首饰,也从来没有烫染过头发,也从来没有赌博过,但同时我父亲其实并不是有责任心的人,基本从不过问家里,以及我的学习。


我的学习打小从小就不好,学习生涯当了两个月的劳动委员,这就是我的荣耀,因为我觉得我小时就是sb,在干啥完全不知道,在学校就是为了吃那一顿饭,和同学天天玩,初中要毕业就被学校各种“”“好言相劝” 去中专学技术,学会拿高薪,实际上是为了赚中专学校的回扣。中专后面又把我们送去富士康,天天12个小时流水线,学校也是为了赚富士康的回扣,我的学业就是这样被卖来卖去,突然觉得有些可悲。这也是普遍读书不行的农村孩子的现状。


我的第一份工作是从2013年开始的,到现在已经差不多10年了,那就做一个时间线看看我的悲催往事吧,这也是我第一次对外讲


2013-2014.02


毕业季,和同学们坐着学校包的几辆大巴车,开到了成都郫县的富士康厂区,哪个时候富士康才在这里建厂,每天的工作就是搬东西,从另外一个地方往厂区里面搬,后面正式开工就开始了每天12个小时的白夜班交替,本来身体从小就弱,经常生病,在富士康就是上班,生病,加上富士康十三跳以及厂区经常出事,我和同学晚上提着东西,连夜翻墙走的,对,真是翻墙走的,后面线长给我打电话,我说我已经不在成都了。不过线长是我老乡,还是跟我没算旷工,算正常离职。 这里不得不说一句,在厂里,一个芝麻官都的官威都不得了,我实在看不惯,加上没前途,才走的。那时候天天12小时到手工资3500,自己上班赚钱还挺潇洒的,下班就去麻辣烫,一人吃饱全家不饿,和同学们啤酒小菜吃着,真是潇洒,厂区还有来自五湖四海的同龄妹子,都是中专生,还是挺快乐的,因为大家年龄相同,就是吃喝玩乐。自然半年才没存什么钱,灰溜溜回了老家,被我妈骂了一顿。


总结:富士康收获: 吃喝玩乐,此刻我的人生规划一无所知


2014.2-2015.07


回了老家,每天早上我妈6点就起来做饭、洗衣服、扫地等等,我起来烧柴,跟猪熬糠羹,喂猪,经常都是公鸡还没叫,我们都忙活一阵了,坚持了十天我就受不了了,因为我得承认,我出去工作后我变懒了,但是每天晚上很晚才睡,因为我在成都买了一个山寨的洛基亚手机,我开始在QQ聊天了,枯燥的生活我受不了,我要出去上班,我就去了重庆。就我妈和我妹两个人在家,这里我的说一下,我去重庆了,我妹才读幼儿园,我妈每天已经非常忙了,我妹从小就是邻居照看,她是位留守的老人,她每天给我妹妹吃好喝好的,比我奶奶好了太多,因为我妈性格很强势,和奶奶性格不合,我奶奶从来没照顾过我和我妹,都在伯伯家带他们的孩子,我妈妈经常晚上8.9点才从田里回家,我妹都是在邻居婆婆那儿吃饭睡觉,前几年她去世了,我妈妈哭了好久,因为她是我家的大贵人,现在每次走到她的坟前,我们都会去跪拜她。现在想起来,我妈太伟大了,她一生都是这么勤劳,吃苦。


到重庆了,上了一年多的厂,其实也是浑浑噩噩的,没有学历,只有在厂里做检验员,一个月2400的工资,入不敷出,因为当时听说主管也是中专学习,干了10多年才当主管,主管才5200一月工资,我觉得没前途,加上厂里玩的好的同事也走了,我也就走了


收获:C1驾-照, 成人高考专科录取通知书, iSO9O001,iSO14001 两个体系证书


2015.7-2016.7


这一年我就像做梦一样,2015年3月去学校报到,认识了班花我老婆,然后就开始交往,然后10月的时候,检查到怀孕了,过年就去了她的老家,因为怀孕了,也就准备结婚的事情了,同学们简直惊叹,纷纷问我怎么办到的?我才23岁,当父亲完全没概念,不过这也满足了我家人的愿望,穷人家里早当家,就在这一年,我妈妈存了一辈子的钱就被我花完了。10月检查怀孕,11月孩子她妈跟我父母說了要了买房买车的事情,我妈妈非常反对,后面我外婆对我妈说:你就这一个儿子,你都不帮他,以后他不恨你吗? 我妈妈想了几晚她咬咬牙还是同意了,过年去了女方家,她父母挺喜欢我的,我妈妈第二年年初付了房子首付26万,后面装修8万,买车8万,结婚7万,对没听错,全是我妈出的,她平时在农村做装修,有的时候包工,一个月7,8000有的时候包工一万多一个月工资,省吃俭用,全部存下的,都被我全部榨干了,好在岳父岳母没有要我一分彩礼,还给我2万块钱装修,他们也是农村人,也是吃了很多苦,2万得他们在工地干很久了,他们在老家为我们办了十多桌,请了一村的人来吃饭。


我妈后面才跟我说,这么多钱,我爸只出了一万块钱,我现在都不可思议,他在外面这么多年的钱去哪儿了? 但我也不恨他,毕竟每个人想法不同,他没有义务要给我出这些费用,不过好在之前房子一个平方8000,算是重庆比较贵的房子了,现在26000一个平方,算是赚了一些,有了一个家庭的财产保障,之前还要贵一点,现在房地产不行降了一些。


2016年7月后我出来也是误打误撞的进入了计算机这个行业,我之前压根就不了解这个行业,是看的招聘网站,招聘信息写的5000的工资,我那时候才3000多,在做销售,简直是高薪了,结果去了才知道,原来是计算机培训学校,耐不住那个美女姐姐各种软磨硬泡,我还是去学了计算机,当然,钱还是我妈跟我出的,因为也是孩子她妈跟我妈说这个行业好,比上厂强,我妈才听了她的,要是我说,那根本不管用。


收获:买房,结婚,买车,装修,好像所有的大事这段时间都基本完成了,虽然都是我妈出的钱


2016.7-2018.7


2016年10月孩子出生了,我也从培训学校出来工作了一段时间,培训机构学了4个月,时间都忙家里的事情去了,所以一毕业面试了20多家公司,都被打击了,每次都想放弃,但是回到家,看到家人,我都心里说不出的滋味,为此也哭了好多次,孩子她妈跟着我这些年,没买过一件超过300的衣服,全是淘宝的几十块一件的,我妈妈为了我在农村不管工作有多远,天气有多冷,都要去工作,我觉得我就是累赘,那时我24岁,我压力可能已经超出了我的极限,房贷3000,孩子每个月2000,车子和物业1000,还有生活费,每个月花销都要8000,有的时候孩子一生病就可能要一万以上,我后面找到一份工作4500,是切图仔,每天就jQuery,才稍微帮家里分担了一下,其实压力全部都在我妈妈哪儿,我妈妈为了我,操碎了不少心。


2018年我拿到了大专学历,然后随即开始了报名成人高考本科,孩子她妈就没有报名了,她觉得女孩子大专就够了,加上家里也没钱


收获:
1.当父亲了,压力更大了。我必须得成熟一点了,在前端行业算是正式入行了,通过自己每天工作之外,在各种QQ群里聊天拉业务,我的外快收入也逐渐多了起来,虽然很多时候工作到2点,但是总算是跟家庭减少一点压力,虽然期间换了3家工作,但是我的工资也高了一些,月薪到手9000了,加上外快时多时不多,一个月平均有个1.3的收入了;我也有一点点经济带家人去自驾游了(不过只有两次)
2.成人高考本科录取通知书


2018.7-2020.7


这一年通过我经常在QQ群聊天的好友介绍,我到了一家外包公司(他当时也拿了回扣,但是我也很感激他。因为他教我怎么面试,跟我出面试题),因为通过了客户的面试,我厚着脸皮开到了1.6一个月的工资。到手14k,我当时说我拿这么多,家里人都以为骗他们,不过等发第一个月工资的时候,他们觉得我以前选择计算机是对的,我妈妈也多了很多笑容,这个时候小孩也是大了,妈妈一个人带着孩子读幼儿园,我和孩子他妈在重庆上班,我妈也在上班,家庭算是好了起来,大家笑容也多了起来。当时加上的我的外快业务,一年也能赚个6,7万,因为大家知道我在做这块,后面一些朋友陆续的给我介绍,我也会给他们相对满足的回扣。平均一个月收入已经超过2万了,不过有点不厚道的是,我上班没事也在做外包。


收获:自己随着年龄的增加,人的心态也在发生变化,随着收入多了起来,脸上的笑容也多了,家庭矛盾也少了,日子也越来越有奔头了


2020.7-2023.10


2021年因为公司被客户从人力资源池给移除了,我们没有资格做客户的业务了,我随即也面临着失业,我28岁了,其实我还是很恐慌的,因为家庭开支这么大,加上我长期做外包,技术底子很薄弱,可能失业找不到这么高的工资,所以我很担忧,工作随便都是全日制本科起,我一个半罐子学历,能干点啥,但是后面客户对我的技术能力还有做事能力还算比较认可的,给我推荐了另外还在资源池的外包公司,但是我都不去,我觉得外包没有前途,同时他们也开不起16k的工资(虽然技术不咋的,但是现在是这工资,让我转到其他外包公司才13k,我也心有不甘啊),最后客户他们把我转进了客户内部,于是我一个中专生进入了体制内,不过在进入之前,各部长对我的学历还是有一定质疑,不过我的直系领导以自身名誉担保,我还是通过他们的几轮面试,最终成功进入。进入到体制内,身边的同事都是985的博士,研究生,还有都是留学回来的,也有一些清华北大北航的研究生,其实还是很自卑的,大家学历这么高,有的时候不得不承认,他们的专业素养,学术知识,脑回路都比较灵活,他们的英语都非常厉害,有的同事28岁都上中央台了,太强了,我妈妈非常担心我的学历,怕我一在公司一犯错就被开除,其实有的时候她还是多虑了,我也在尽力 的追赶他们,希望差距尽量小一点点。所以在2023年我拿到了非全的研究生录取通知书,继续读软件工程。在公司也申请入了党,因为他们全是党员,我在公司负责两个部门的前端管理工作,也在带一些校招的研究生,同时我也在2022年5月买了第二套房,首付42万,其他非要加上差不多47万,为此把车卖了一万五,凑点首付钱,我妈妈又出了26万,再一次吧我妈给榨干了,这次我爸也没有出一分钱。不过还在我是组合贷,每个月只出商业贷,每个月出差不多2000的房贷,第一套房贷也还有20多万就还完了,2023年我孩子读小学了,我妈妈也来重庆带孩子了,为此她没有继续在做装修了,每天接送小孩子上学放学,中间有两个小时去一家店里打扫卫生,每个月2500的收入。我在每个月给他2000多生活费,虽然她不太适应城市的生活,觉得城市的人会看不起她是农村人,走哪儿都不会用导航,但是她慢慢的还是习惯了,城市的人并没有觉得自己高人一等,她还算是过得比较快乐,现在我的收入在重庆来说还算OK,外快也有,但是我也不想太累了,我想把时间利用在学习上,因为同事们都很强,我尽量向他们看齐,


收获:本科毕-业-证,非全研究生录取通知书,稳定的工作,第二套房子首付+装修(因为旁边学校更好一点)


最后


今年30了,孩子已经7岁了,我已经开始享受十天的年假了。其实我已经算是走了很多路,深夜哭了很多次,第二天依旧怀揣着斗志,我数次回想我这30年的发展,其实都过得不是很灿烂,或许平凡的就是这样,一无所有的农村人,只能靠父母,如果父母靠不住,那自己也开心点,随着父母的年龄越来越大,我的压力也变大了,他们很多时候会征求我的意见,我也要拿钱支持他们了,也有一些感悟:


1.每年还是得有一个目标,细分到每个月,每一周去完成它,如果没有目标,那就认真的把每一件事情尽量做好,贵人每个人都会遇到,只是看能否抓住,可能会是工作中,生活中的某一个,他愿意提携你一下,真的能少走很多弯路,我的经验告诉我,我有两三次都有贵人帮助我,只是我没有把握住,就像之前一起做区块链,一起做电商的公司老板就很喜欢我,因为我比较踏实,没攻击性,人老实。但是我还是太年轻,很多时候做事不够成熟,就这样和机会擦肩而过,他们现在已经是财富自由了


2.与人交流,说话适可而止,充分尊重他人,聊天中尽量带点幽默,学习一下话题的扩展


3.没事多扩展一下人脉,我才开始培训机构出来,基本没学,全靠在QQ群聊天的人带的我,怎么学,我每次遇到问题我都会问他们,他们在远程帮我改bug,这样我才能保住工作


4.多学习,我看了下我现在的同事,他们没事不会在网上划水,而是都在学习,最敬佩的是旁边那位,一年了从小白,到一名技术骨干,技术成长太快了,他除了学习,每天还不断在看书,我只能说佩服,我很多时候都在刷抖音,我自愧不如,我有罪


5.想办法融入更好的圈子,我之前待得公司都不大,都是外包公司,大家学历都很低,没有一个是985或者211的本科生,大家上班都在聊吃喝嫖赌,主要是聊女人那点事。但是现在我发现身边的同事几年了,没有一个人说过一句脏话,说话总会特别舒服,因为你能感受到他非常尊重你,说话也非常温柔,绝不会听到SEX,tm 这种言词。


6.接受自己的平凡。我以前有很多想法,内心很浮躁,后面发现读书越少的人想法越多,到最后越来越差,债务缠身,本来都是资本的牟利工具,平凡开心更好,把家里的事情处理好,生活上逐渐改善品质,就已经很不错了,在平凡的生活多点浪漫,对未来有一点期待,但不浮夸,我觉得就已经很不错了


7.多多提高自己的综合素质吧,我是一个比较随心的人,但是后面发现,穿的邋遢,说话幼稚,身形不行,走在外面都没自信,何况别人会怎么看你呢,这一点我也在慢慢提高


8.最后我的技术其实很一般,node,vue,react,java,python,php,微信小程序,three.js 这些都有做过,有的都是为了外包业务减少点成本才去学的,但是要说哪一个比较深入,可能就前端的这几个框架,因为天天都在做,偶尔看看掘金的技术文档,但是要说特别深入的,抱歉没有,因为我从误打误撞开始进入这个行业,我的目的不是因为喜欢,而是因为工资高一点,我没有想给要为这个行业带来些什么,我只想活着,我现在觉得我没有特别喜欢做的行业,我不清楚我能在这个行业做多少年,但是只要做,我就把它做好,因为做工作的态度跟自己的喜欢没有关系,做事是做人,自己的工作做好了,下个同事才会很轻松。同时也在尽可能的弥补一些自己的软实力。希望在某一天,有更好的机会,自己能抓得住,自己不会为了自己的能力而自卑!


9.2023-12-21 17:28:53 下班了


作者:程序员xx1
来源:juejin.cn/post/7314877697996947482
收起阅读 »

为什么老家的黄瓜比北京的便宜?普通人应该去经济发达地区谋生

有时候会跟着家人去逛菜市场,有次我岳母说北京的菜真贵,原本在农村老家2、3块的黄瓜到了北京要5、6块一斤,感觉真是土气。 出于好奇,我就开始琢磨这个问题。 我问了财务专业的老婆大人,老婆大人的答案很直接,大概意思是北京的供应链比老家贵的多,比如运输成本、用于卖...
继续阅读 »

有时候会跟着家人去逛菜市场,有次我岳母说北京的菜真贵,原本在农村老家2、3块的黄瓜到了北京要5、6块一斤,感觉真是土气。


出于好奇,我就开始琢磨这个问题。


我问了财务专业的老婆大人,老婆大人的答案很直接,大概意思是北京的供应链比老家贵的多,比如运输成本、用于卖菜的房租成本、还有销售蔬菜的人力成本……然后这些成本都加到蔬菜价格上,自然就高了。


我哈哈一笑,老婆大人的回答自然是不敢出声反驳的,但我可以在心里揣测。我觉得老婆大人说的没问题,但并不深刻,于是我开始浮想联翩……


一 自产自销


在北京生活久了,我发现就蔬菜来说并非不可以自产的。我之前生活的小区,就有销售自产蔬菜的大爷大妈。老婆说是因为供应链比老家的贵,可大爷大妈自产自销不需要供应链啊,老婆的回答就没法解释了。


在我的印象里面,大爷大妈自产的蔬菜也是要比老家的贵的,为什么会如此?


我思考的结果是,大爷大妈是询价定价的。简单来说大爷大妈一开始是不知道定价多少合适的,定的太低心里不得劲,定的太高没人买,所以大爷大妈会去附近菜市场询价,然后定一个比菜市场低的价格,这样既解决了心里不得劲问题,也解决了卖不出去问题。


但这本质上还是因为北京市场上的蔬菜价格比老家的贵。


二 供应链


如果不是自产自销,那就是依靠供应链了。在老家市场销售的黄瓜和北京市场销售的黄瓜,假设有着相同的货源,但背后供应链体系肯定是不同的。


在我们老家有那种大集,就是固定一个时间周期在一个固定地点开展买卖。


大集上的菜农销售蔬菜基本可以忽略掉租房成本,然后因为菜农本身就是销售人员,也省下销售成本,这样整体的供应链成本就下来了,就能够低价销售蔬菜了。


这也就是老婆说的供应链成本不同。


三 边际


边际是经济学核心概念之一,是一种思维方式,就是永远看市场中最后一个人的行为或者最后一个产品的情况。


比如在劳动力市场上,工资不是市场中的平均水平的劳动者决定的,而是最后一个参加劳动的人决定的,要看给他多高的工资他才愿意去做这份工作,同时也要看他有多大的贡献工厂才雇用他,两者相等的时候才是市场里的均衡工资


拉长时间看蔬菜市场中的黄瓜价格会受边际影响。简单来说,蔬菜超市会因为追求最大经济利益调整蔬菜价格,直到找到均衡价格。


结果因为北京的购买者更能容忍价格的波动,导致北京的黄瓜比农村的贵。


四 可替代性


北京的购买者之所以更能容忍价格波动,原因之一是北京生活的人相对老家农村生活的人,在蔬菜这件事上,可替代性弱。


北京生活的人,少有自产自销者;而农村老家的人,多可以自产自销。这虽然不是价格上涨的直接原因,但无形当中推高了均衡价格。


五 购买力


北京的购买者之所以更能容忍价格波动,原因之二是北京的购买力强。


我之前在小区周边的永辉超市看到有机蔬菜,标记35.98/kg,我当时想什么人会买这种蔬菜呢?


蔬菜.jpg


从结果来说,北京购买者购买力强是老家的黄瓜比北京便宜的直接原因


尾声


黄瓜的价格只是一个小的不能再小的缩影,老家的黄瓜之所以比北京便宜,根本原因是老家农村和北京有着根本不同的经济结构


北京这种大城市有更高效的资源利用率,更强的生产能力和更高效的生产效率,结果就是北京创造了更多财富,即使普通人在这里也会有较高收入,高收入下基于边际思维演化的黄瓜价格,形成高的均衡价格。


多年前我曾极力劝我一个朋友来北京,他在四线城市很努力,但始终没能赚大钱。当初我说不出更具说服力的让他来北京的理由,但我认为我现在找到了:他赚钱少可能不是因为他不够努力,而是因为他本来就不在一个发达的经济体里,也就难以享受到比较高的红利。


所以我给普通奋斗者的建议是去经济发达地区谋生吧!


作者:通往自由之路pro
来源:juejin.cn/post/7353233940545323045
收起阅读 »

Activity界面路由的一种简单实现

1. 引言 平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Ac...
继续阅读 »

1. 引言


平时Android开发中,启动Activity是非常常见的操作,而打开一个新Activity可以直接使用Intent,也可以每个Activity提供一个静态的启动方法。但是有些时候使用这些方法并不那么方便,比如:一个应用内的网页需要打开一个原生Activity页面时。这种情况下,网页的调用代码可能是app.openPage("/testPage")这样,或者是用app.openPage("local://myapp.com/loginPage")这样的方式,我们需要用一种方式把路径和页面关联起来。Android可以允许我们在Manifest文件中配置<data>标签来达到类似效果,也可以使用ARouter框架来实现这样的功能。本文就用200行左右的代码实现一个类似ARouter的简易界面路由。


2. 示例


2.1 初始化


这个操作建议放在Application的onCreate方法中,在第一次调用Router来打开页面之前。


public class AppContext extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.init(this);
}
}

2.2 启动无参数Activity


这是最简单的情况,只需要提供一个路径,适合“关于我们”、“隐私协议”这种简单无参数页面。


Activity配置:


@Router.Path("/testPage")
public class TestActivity extends Activity {
//......
}

启动代码:


Router.from(mActivity).toPath("/testPage").start();
//或
Router.from(mActivity).to("local://my.app/testPage").start();

2.3 启动带参数Activity


这是比较常见的情况,需要在注解中声明需要的参数名称,这些参数都是必要参数,如果启动的时候没有提供对应参数,则发出异常。


Activity配置:


@Router.Path(value = "/testPage",args = {"id", "type"})
public class TestActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:


Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

2.4 启动带有静态启动方法的Activity


有一些Activity需要通过它提供的静态方法启动,就可以使用Path中的method属性和Entry注解来声明入口,可以提供参数。在提供了method属性时,需要用Entryargs来声明参数。


Activity配置:


@Router.Path(value = "/testPage", method = "open")
public class TestActivity extends Activity {

@Router.Entry(args = {"id", "type"})
public static void open(Activity activity, Bundle args) {
Intent intent = new Intent(activity, NestWebActivity.class);
intent.putExtras(args);
activity.startActivity(intent);
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//加载布局...

String id = getIntent().getStringExtra("id"); //获取参数
int type = getIntent().getIntExtra("type", 0);//获取参数
}
}

启动代码:


Router.from(mActivity).toPath("/testPage").with("id", "t_123").with("type", 1).start();

3. API介绍


3.1 Path注解


这个注解只能用于Activity的子类,表示这个Activity需要页面路由的功能。这个类有三个属性:



  • value:表示这个Activity的相对路径。

  • args:表示这个Activity需要的参数,都是必要参数,如果打开页面时缺少指定参数,就会发出异常。

  • method:如果这个Activity需要静态方法做为入口,就将这个属性指定为方法名,并给对应方法添加Entry注解。(注意:这个属性值不为空时,忽略这个注解中的args属性内容)


3.1 Entry注解


这个注解只能用于Activity的静态方法,表示这个方法作为打开Activity的入口。仅包含一个属性:



  • args:表示这个方法需要的参数。


3.2 Router.init方法



  • 方法签名:public static void init(Context context)

  • 方法说明:这个方法用于初始化页面路由表,必须在第一次用Router打开页面之前完成初始化。建议在Application的onCreate方法中完成初始化。


3.3 Rouater.from方法



  • 方法签名:public static Router from(Activity activity)

  • 方法说明:这个方法用于创建Router实例,传入的参数通常为当前Activity。例如,要从AActivity打开BActivity,那么传入参数为AActivity的实例。


3.4 Rouater.to和Rouater.toPath方法



  • 方法签名:




  1. public RouterBuilder to(String urlString)

  2. public RouterBuilder toPath(String path)




  • 方法说明:这个方法用于指定目标的路径,to需要执行绝对路径,而toPath需要指定相对路径。返回的RouterBuilder用于接收打开页面需要的参数。


3.4 RouterBuilder.with方法



  • 方法签名:




  1. public RouterBuilder with(String key, String value)

  2. public RouterBuilder with(String key, int value)




  • 方法说明:这个方法用于添加参数,对应Bundle的各个put方法。目前只有常用的Stringint两个类型。如有需要可自行在RouterBuilder中添加对应的方法。


3.4 RouterBuilder.start方法



  • 方法签名:public void start()

  • 方法说明:这个方法用于打开页面。如果存在路径错误、参数错误等异常情况,会发出对应运行时异常。


4. 实现


import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;

import androidx.annotation.Keep;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

@Keep
public class Router {

public static final String SCHEME = "local";
public static final String HOST = "my.app";
public static final String URL_PREFIX = SCHEME + "://" + HOST;

private static final Map<String, ActivityStarter> activityPathMap = new ConcurrentHashMap<>();

public static void init(Context context) {
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(
context.getPackageName(), PackageManager.GET_ACTIVITIES);

for (ActivityInfo activityInfo : packageInfo.activities) {
Class<?> aClass = Class.forName(activityInfo.name);
Path annotation = aClass.getAnnotation(Path.class);
if (annotation != null && !TextUtils.isEmpty(annotation.value())) {
activityPathMap.put(annotation.value(), (Activity activity, Bundle bundle) -> {
if (TextUtils.isEmpty(annotation.method())) {
for (String arg : annotation.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
Intent intent = new Intent(activity, aClass);
intent.putExtras(bundle);
activity.startActivity(intent);
} else {
try {
Method method = aClass.getMethod(annotation.method(), Activity.class, Bundle.class);
Entry entry = method.getAnnotation(Entry.class);
if (entry != null) {
for (String arg : entry.args()) {
if (!bundle.containsKey(arg)) {
throw new IllegalArgumentException(String.format("Bundle does not contains argument[%s]", arg));
}
}
method.invoke(null, activity, bundle);
} else {
throw new IllegalStateException("can not find a method with [Entry] annotation!");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static Router from(Activity activity) {
return new Router(activity);
}

private final Activity activity;

private Router(Activity activity) {
this.activity = activity;
}

public RouterBuilder to(String urlString) {
if (TextUtils.isEmpty(urlString)) {
return new ErrorRouter(new IllegalArgumentException("argument [urlString] must not be null"));
} else {
return to(Uri.parse(urlString));
}
}

public RouterBuilder toPath(String path) {
return to(Uri.parse(URL_PREFIX + path));
}

public RouterBuilder to(Uri uri) {
try {
if (SCHEME.equals(uri.getScheme())) {
if (HOST.equals(uri.getHost())) {
String path = uri.getPath();//note: 二级路径暂不考虑
ActivityStarter starter = activityPathMap.get(path);
if (starter == null) {
throw new IllegalStateException(String.format("path [%s] is not support", path));
} else {
NormalRouter router = new NormalRouter(activity, starter);
for (String key : uri.getQueryParameterNames()) {
if (!TextUtils.isEmpty(key)) {
router.with(key, uri.getQueryParameter(key));
}
}
return router;
}
} else {
throw new IllegalArgumentException(String.format("invalid host : %s", uri.getHost()));
}
} else {
throw new IllegalArgumentException(String.format("invalid scheme : %s", uri.getScheme()));
}
} catch (RuntimeException e) {
return new ErrorRouter(e);
}
}

public static abstract class RouterBuilder {
public abstract RouterBuilder with(String key, String value);

public abstract RouterBuilder with(String key, int value);

public abstract void start();
}


private static class ErrorRouter extends RouterBuilder {
private final RuntimeException exception;

private ErrorRouter(RuntimeException exception) {
this.exception = exception;
}

@Override
public RouterBuilder with(String key, String value) {
return this;
}

@Override
public RouterBuilder with(String key, int value) {
return this;
}

@Override
public void start() {
throw exception;
}
}

private static class NormalRouter extends RouterBuilder {
final Activity activity;
final Bundle bundle = new Bundle();
final ActivityStarter starter;

private NormalRouter(Activity activity, ActivityStarter starter) {
this.activity = Objects.requireNonNull(activity);
this.starter = Objects.requireNonNull(starter);
}

@Override
public RouterBuilder with(String key, String value) {
bundle.putString(key, value);
return this;
}

@Override
public RouterBuilder with(String key, int value) {
bundle.putInt(key, value);
return this;
}

@Override
public void start() {
starter.start(activity, bundle);
}
}

@FunctionalInterface
private interface ActivityStarter {
void start(Activity activity, Bundle bundle);
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Path {
String value();

String method() default "";

String[] args() default {};
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Entry {
String[] args() default {};
}
}

5. 注意



  1. 这个工具的一些功能与ARouter类似,实际项目中建议使用ARouter。如果有特殊需求,例如,页面参数的检查或定制具体打开行为,可以考虑基于这个工具进行修改。

  2. 使用了Pathmethod属性时注意添加对应的混淆设置,避免因混淆而导致找不到对应方法。


作者:乐征skyline
来源:juejin.cn/post/7235639979882463292
收起阅读 »

基于SSE的实时消息推送

背景 小盟 AI 助手项目中需要服务端把 AI 模型回调回来内容,实时推送到客户端,展示给用户;整个流程需要一个能够快速支持上线的服务端推送方案。经过对现有的一些服务端推送方案进行调研,并结合项目的周期、实现成本、用户体验等多方综合考量,最终选择了 ...
继续阅读 »



背景


小盟 AI 助手项目中需要服务端把 AI 模型回调回来内容,实时推送到客户端,展示给用户;整个流程需要一个能够快速支持上线的服务端推送方案。经过对现有的一些服务端推送方案进行调研,并结合项目的周期、实现成本、用户体验等多方综合考量,最终选择了 Server-Sent Events(SSE)方案进行实践。


首先服务端推送,是一种允许应用服务器主动将信息发送到客户端的能力,为客户端提供了实时的信息更新和通知,增强了用户体验。


服务端推送主要基于以下几个诉求:


(1)实时通知:在很多情况下,用户期望实时接收到应用的通知,如新消息提醒、活动提醒等。


(2)节省资源:如果没有服务端推送,客户端需要通过轮询的方式来获取新信息,会造成客户端、服务端的资源损耗。通过服务端推送,客户端只需要在收到通知时做出响应,大大减少了资源的消耗。


(3)增强用户体验:通过服务端推送,应用可以针对特定用户或用户群发送有针对性的内容,如优惠活动、个性化推荐等。这有助于提高用户对应用的满意度和黏性。


方案对比



轮询: 是一种较为传统的方式,客户端定时地向服务端发送请求,询问是否有新数据。服务端只需要检查数据状态,然后将结果返回给客户端。轮询的优点是实现简单,兼容性好;缺点是可能产生较大的延迟,且对服务端资源消耗较高。


长轮询(Long Polling): 轮询的改进版。客户端向服务器发送请求,服务器收到请求后,如果有新的数据,立即返回给客户端;如果没有新数据,服务器会等待一定时间(比如30秒超时时间),在这段时间内,如果有新数据,就返回给客户端,否则返回空数据。客户端处理完服务器返回的响应后,再次发起新的请求,如此反复。长轮询相较于传统的轮询方式减少了请求次数,但仍然存在一定的延迟。   


WebSocket: 一种双向通信协议,同时支持服务端和客户端之间的实时交互。WebSocket 是基于 TCP 的长连接,和HTTP 协议相比,它能实现轻量级的、低延迟的数据传输,非常适合实时通信场景,主要用于交互性强的双向通信。


SSE: 是一种基于 HTTP 协议的推送技术。服务端可以使用 SSE 来向客户端推送数据,但客户端不能通过 SSE 向服务端发送数据。相较于 WebSocket,SSE 更简单、更轻量级,但只能实现单向通信。


图片


图片


小盟 AI 助手项目需要快速上线且保证要用户较好的使用体验。鉴于 SSE 技术的轻量、实现简单、不增加额外的资源成本;当前业务场景也只需要服务端到用户端的单向的字符推送。非常适合项目需要。所以决定使用 SSE 来实现内容推送。****


深入 SSE



SSE 服务端推送,它基于 HTTP 协议,易于实现和部署,特别适合那些需要服务器主动推送信息、客户端只需接收数据的场景。有以下特点:


1、简单:基于 HTTP,无须额外的协议或者类库支持。主流浏览器都支持。


2、事件流:使用"事件流"(Event Stream)将数据从服务器发送到客户端。每个事件都可以包含一个事件标识符、事件类型和数据字段。客户端可以根据这些信息来解析和处理接收到的数据。


3、自动重连:意外断开时会自动尝试重新连接。可以确保了在网络故障或连接中断后能够及时恢复通信,为用户提供连续的数据流。重连时会在 HTTP 头中的Last_Event_ID 带上上一次的数据 ID,便于服务端返回后续数据。


4、单向推送:只能从服务端推送数据到客户端。


图片


SSE 消息体介绍:


图片


SSE消息体示例:


图片


服务端主要使用 Spring,其对 SSE 主要提供了两种支持:



  • Spring WebMVC:传统的基于 Servlet 的同步阻塞编程模型,即 同步模型 Web 框架。

  • Spring WebFlux:异步非阻塞的响应式编程模型,即 异步模型 Web 框架。          


项目基于springboot,所以选择使用前者实现。SseEmitter emitter = new SseEmitter(); 一句代码就可以建立一个 SSE 连接。


实践



后端实现


建立一个SseEmitterManger,统一管理当前服务 SSE 连接的创建、释放以及数据推送。结合 Redis 缓存可实现集群环境 SSE 连接的管理。


核心逻辑如下:



  • 连接池维护,设定一个上限,避免过大,导致内存问题。


static final Map<String, SseEmitter> sseCache =     new ConcurrentHashMap<>(300)          


  • 建立SSE连接,为每个连接建立一个唯一的MsgId,用来维护SSE连接与客户端的关系了;在 Redis 缓存中存入MsgId和当前机器节点的IP和Port,这样可以找到SSE 连接所在的服务结点,然后通过 HTTP 请求转发需要发送的数据到对应的服务节点上进行处理。


sse = new SseEmitter()sseId = "sse_xxx";redisKey= "aisse:" + bosId + "_" + wid ipPort = "10.10.10.10:8080"redis.hset(redisKey, msgId, ipPort)sseCache.put(msgId, sseEmitter);


  • 获取持有连接的 pod ipPort;根据 IP 发起请求。


ipPort = redisUtil.hashGet(redisKey, msgId)


  • 获取当前服务结点的SSE连接,发送数据。


sseEmitter = sseCache.get(msgId)sseEmitter.send(msgJson)          


  • 释放SSE连接


SseEmitter sseEmitter = sseCache.get(msgId);sseEmitter.complete();sseCache.remove(msgId);redisUtil.hashDel(redisKey, msgId);

**核心流程图如下: **  


图片


需要注意的是开启 SSE 连接接口的整个链路都要支持长连接。例如使用 Nginx 则要开启长连接的配置:



  • keepalive 用于控制可连接整个 upstream servers 的 HTTP 长连接的个数,即控制总数。

  • proxy_http_verion 用于控制代理后端链接时使用的 HTTP 版本,默认为 1.0。要想使用长连接,必须配置为 1.1。

  • proxy_set_header 需要设置为 Connection "",否则则发往 upstream servers 的请求中,Connection header 的值将为close,导致无法建立长连接。   


http {        upstream keepAliveService {            server 10.10.131.149:8080;            keepalive 20;        }            server {            listen 80;            server_name keepAliveService;            location /keep-alive/hello {                proxy_http_version 1.1;                proxy_set_header Connection "";                proxy_pass http://keepAliveService;            }        }}

**前端实现 **  


前端可以使用组件 @microsoft/fetch-event-source 来实现。


npm i @microsoft/fetch-event-source
import { fetchEventSource } from '@microsoft/fetch-event-source';let controller = new AbortController(); let eventSource = fetchEventSource('apiUrl', { method: 'POST', headers: { 'Content-Type': 'application/json', 'token': '....' }, signal: controller.signal, body: JSON.stringify({ ... // 传参 }), onopen() { // 建立连接 }, onmessage(event) { // 接收信息 // 成功之后满足某些条件可以使用AbortController关闭连接 controller.abort() eventSource?.close && eventSource.close(); }, onerror() { // 服务异常 controller.abort() eventSource?.close && eventSource.close(); }, onclose() { // 服务关闭 },})

总结



SSE 轻量级的服务端单向推送技术;具有支持跨域、使用简单、支持自动重连等特点。相对于 WebSocket 更加轻量级,如果需求场景客户端和服务端单向通信,那么 SSE 是一个不错的选择。


作者:微盟技术中心
来源:juejin.cn/post/7317325043541032970
收起阅读 »

社会现实告诉我,00后整顿职场就是个笑话

00后整顿职场,也算是我之前的关键词吧。 我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。 甚至还能在即将被开除的时候,反将一军把老板开除。 而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。 也...
继续阅读 »

00后整顿职场,也算是我之前的关键词吧。


我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。


甚至还能在即将被开除的时候,反将一军把老板开除。


而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。


也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。


1、职场宫斗,成功上位


我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。


而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。


届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。


在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。


我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。


2、内心变化的开端


我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。


他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。


我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。


我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。


我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。


因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。


加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。


所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?


3、复杂的职场生态环境


建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。


压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。


两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。


在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。


但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。


我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。


他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。


九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?


毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“


甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。


4、职场中的背锅人


他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。


但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。


我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。


但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。


面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。


5、换个思路,创造属于自己的职场


一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。


我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。


这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。


这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。


大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。


或许是因为他们心里也清楚,凭我们压根就做不到。


00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。


但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。


是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。


作者:程序员Winn
来源:juejin.cn/post/7311603432929984552
收起阅读 »

RecyclerView还能这样滚动对齐?

前言 RecyclerView要想滚动到指定position,一般有scrollToPosition()和smoothScrollToPosition()两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中...
继续阅读 »

前言


RecyclerView要想滚动到指定position,一般有scrollToPosition()smoothScrollToPosition()两种方式。滚动到指定position后,通常还会要求itemView对齐RecyclerView起始点、中心点或结束点


熟悉RecyclerView的人应该知道,使用自定义SmoothScroller可以实现平滑滚动到指定position的同时,让itemView和RecyclerView的对齐;而scrollToPosition()方法只能滚动到指定position。那有办法让scrollToPosition()也做到对齐吗?


拆解行为


分析对齐的行为后,可以分为几步



  1. 让目标itemView可见

  2. 计算itemView和目的位置的偏移量

  3. 将itemView移动到目的位置


第一步scrollToPosition()就已经可以实现了,最后一步就是调用scrollBy(),那其实只需要实现第二步计算偏移量,而这可以参考SmoothScroller的实现


平滑滚动


来看下SmoothScroller是怎么做的。通常做法都是自定义LinearSmoothScroller


RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
int preference = LinearSmoothScroller.SNAP_TO_START;// 对齐方式
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(context){
@Override
protected int getHorizontalSnapPreference() {
return preference;
}

@Override
protected int getVerticalSnapPreference() {
return preference;
}
};
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);

简单介绍下几种对齐方式



  • SNAP_TO_START:对齐RecyclerView起始位置

  • SNAP_TO_END:对齐RecyclerView结束位置

  • SNAP_TO_ANY:对齐RecyclerView任意位置,确保itemView在RecyclerView内


接下来看下getVerticalSnapPreference()或者getHorizontalSnapPreference()的返回值是怎么影响到itemView的对齐的。查看LinearSmoothScroller源码发现这两个方法会在onTargetFound()里调用


protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}

不难看出,该方法是计算targetView当前要滚动的偏移量和时长,并设置给action。而calculateDxToMakeVisible()calculateDyToMakeVisible()正是我们要找的计算偏移量的方法


由于这两个方法只依赖LayoutManager,所以我们可以将这些代码逻辑复制出来,创建一个Rangefinder类,用于计算偏移量


public class Rangefinder {
private final RecyclerView.LayoutManager mLayoutManager;

public Rangefinder(RecyclerView.LayoutManager layoutManager) {
mLayoutManager = layoutManager;
}

@Nullable
public RecyclerView.LayoutManager getLayoutManager() {
return mLayoutManager;
}

// 计算view在RecyclerView中完全可见所需的垂直偏移量
public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollVertically()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
final int start = layoutManager.getPaddingTop();
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}

// 计算view在RecyclerView中完全可见所需的水平偏移量
public int calculateDxToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
final int start = layoutManager.getPaddingLeft();
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
return calculateDtToFit(left, right, start, end, snapPreference);
}

public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd,
@SnapPreference int snapPreference)
{
switch (snapPreference) {
case LinearSmoothScroller.SNAP_TO_START:
return boxStart - viewStart;
case LinearSmoothScroller.SNAP_TO_END:
return boxEnd - viewEnd;
case LinearSmoothScroller.SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
}
return 0;
}
}

有了计算偏移量的方法,接下来就是实现itemView的对齐了


即时滚动


根据上面的拆解步骤,再分析下每一步要做的事情



  1. 调用scrollToPosition()使目标itemView可见。因为该方法最终会requestLayout(),所以要在layout后,才能通过获取到itemView。那么可以post()后调用LayoutManagerfindViewByPosition()方法获取itemView

  2. 参考LinearSmoothScrolleronTargetFound()方法,使用上面的Rangefinder计算itemView和目的位置的偏移量

  3. 调用scrollBy()将itemView移动到目的位置


RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
recyclerView.scrollToPosition(targetPosition);
recyclerView.post(new Runnable() {
@Override
public void run() {
View targetView = layoutManager.findViewByPosition(targetPosition);
if (targetView != null) {
Rangefinder rangefinder = new Rangefinder(layoutManager);
final int dx = rangefinder.calculateDxToMakeVisible(targetView, preference);
final int dy = rangefinder.calculateDyToMakeVisible(targetView, preference);
if (dx != 0 || dy != 0) {
recyclerView.scrollBy(-dx, -dy);
}
}
}
});

至此,我们就实现了即时滚动到position的同时,让itemView和RecyclerView对齐的功能。当然,这也只是测试代码,实际使用还会对上面的逻辑进行封装


测试代码 recyclerView-scroll-demo


参考


作者:benio
来源:juejin.cn/post/7364740313284444186
收起阅读 »

这么炫酷的换肤动画,看一眼你就会爱上

web
实现过程 我们先创建下 vue 项目 npm init vite-app vue3-vite-animation 进入文件夹中 cd vue3-vite-animation 安装下依赖 npm install 启动 npm run dev 重新修改 ...
继续阅读 »

动画.gif


实现过程


我们先创建下 vue 项目


npm init vite-app vue3-vite-animation

进入文件夹中


cd vue3-vite-animation

安装下依赖


npm install

启动


npm run dev

image-20240503171537954.png


重新修改 App.vue


<template>
<div class="info-box">
<div class="change-theme-btn">改变主题</div>
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

<script setup lang="ts">

</script>



<style>

.change-theme-btn {
width: 80px;
height: 40px;
background-color: #fff;
text-align: center;
line-height: 40px;
color: #282c34;
cursor: pointer;
border-radius: 8px;
border: 2px solid #282c34;
}

.info-box {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>


基本样式出来了,但是页面出现了滚动条,我们需要去掉原有样式


image-20240503175456039.png


src/index.css,里的所有样式都删除了,再到 index.html 中将 bodymargin 属性去掉


<body style="margin: 0;">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

接下来,我们来实现下换肤功能


使用 css 变量,先定义下一套黑暗主题、一套白色主题


:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}

:root.dark {
--background-color: #282c34;
--color: #fff;
}

再定义点击事件 changeColor,点击 "改变主题" 就会改变主题颜色


classList.toggle 这个方法的第一个参数是类名,第二个参数是布尔值,表示是否添加类


如果第二个参数为 true,则添加类;如果第二个参数为 false,则移除类


<div class="change-theme-btn" @click="changeColor">改变主题</div>

/* 改变颜色 */
const changeColor = () => {
document.documentElement.classList.toggle('dark')
}

image-20240503180914393.png


按钮背景颜色、边框、字体颜色都没有改变


调整下按钮样式,把背景颜色、边框、字体颜色这些都用 css 变量代替


.change-theme-btn {
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

image-20240503181138545.png


这个效果不是我们想要的,需要一个过渡动画对不对


使用 startViewTransition,这个 API 会生成一个屏幕截图,将新旧屏幕截图进行替换


截图分别对应两个伪元素 ::view-transition-new(root)::view-transition-old(root)


 // 创建一个过渡对象
document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

可以看到,一个淡入淡出的效果,但是我们需要的是一个圆向外扩散的效果


用剪切效果就可以实现,其中 circle(动画进度 at 动画初始x坐标 动画初始y坐标)


设置动画时间为 1秒,作用在新的伪元素上,也即是作用在新的截图上


const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

transition.ready.then(() => {
document.documentElement.animate({
clipPath: ['circle(0% at 50% 50%)', 'circle(100% at 100% 100%)']
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)'
})
})

动画-1714752074132-6.gif


为什么动画效果和预期的不一样


因为,默认的动画效果,把当前动画覆盖了,我们把默认动画效果去掉


/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}

动画-1714752309164-8.gif


效果出来了,但是圆的扩散不是从按钮中心扩散的


那么,通过 ref="btn" 来获取 “改变主题” 按钮的坐标位置


再获取按钮坐标减去宽高,就能得到按钮的中心坐标了


<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>

<script setup>
import { ref } from 'vue';
const btn = ref<any>(null)

/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(100% at ${x}px ${y}px)`]
}, {
duration: 1000,
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>

扩展,如果,我不要从中心扩展,要从左上角开始动画呢,右上角呢...


我们把按钮放在左上角,看看效果


修改下样式、与模板


<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

动画这个圆的半径不对,导致动画到快末尾的时候,直接就结束了


动画-1714753474905-10.gif


动画的圆的半径 = 按钮中心坐标 到 对角点的坐标


可以使用三角函数计算,两短边平方 = 斜边平方


image-20240504002759638.png


// 计算展开圆的半径
const tragetRadius = Math.hypot(
window.innerWidth - x,
innerHeight - y
)

// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})

动画-1714754131456-15.gif


如果是右上角呢


.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

动画-1714754468881-23.gif


在右边的话,使用三角函数计算,其中一个短边就不能是 屏幕宽度 - 按钮x坐标,直接是 x 坐标就对了


那要怎么实现呢,直接取 屏幕宽度 - 按钮x坐标 与 按钮x坐标 的最大值就可以了


y 也是同理


const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)

动画-1714754788538-25.gif


你可以试试其他位置,是否也是可行的


完整代码


<template>
<div ref="btn" class="change-theme-btn" @click="changeColor">改变主题</div>
<div class="info-box">
<h1>Element Plus</h1>
<p>基于 Vue 3,面向设计师和开发者的组件库</p>
</div>

</template>

<script setup lang="ts">
import { ref } from 'vue';
const btn = ref<any>(null)

/* 改变颜色 */
const changeColor = () => {
// 创建一个过渡对象
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})

const width = btn.value.getBoundingClientRect().width // 按钮的宽度
const height = btn.value.getBoundingClientRect().height // 按钮的高度
const x = btn.value.getBoundingClientRect().x + width / 2 // 按钮的中心x坐标
const y = btn.value.getBoundingClientRect().y + height / 2 // 按钮的中心y坐标

// 计算展开圆的半径
const tragetRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)

// 设置过渡的动画效果
transition.ready.then(() => {
document.documentElement.animate({
clipPath: [`circle(0% at ${x}px ${y}px)`, `circle(${tragetRadius}px at ${x}px ${y}px)`]
}, {
duration: 1000,
// pseudoElement
// 设置过渡效果的伪元素,这里设置为根元素的伪元素
// 这样过渡效果就会作用在根元素上
pseudoElement: '::view-transition-new(root)',
})
})
}
</script>


<style>

:root {
--background-color: #fff;
--color: #282c34;
background-color: var(--background-color);
color: var(--color);
}

:root.dark {
--background-color: #282c34;
--color: #fff;
}

/* 隐藏默认的过渡效果 */
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}

.change-theme-btn {
float: right;
width: 80px;
height: 40px;
background-color: var(--background-color);
text-align: center;
line-height: 40px;
color: var(--color);
cursor: pointer;
border-radius: 8px;
border: 2px solid var(--color);
}

.info-box {
width: 100vw;
height: calc(100vh - 44px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

换肤动画源码


小结


换肤功能,主要靠 css 变量 与 classList.toggle


startViewTransition 这个 API 来实现过渡动画效果,注意需要清除默认动画


圆点扩散效果,主要运用剪切的方式进行实现,计算过程运用了三角函数运算


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

从密码到无密码:账号安全进化史(科普向)

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究! 本文是一篇科普文,五一结束了,大家看点轻松的~ 不知道大家在过去半年有没有发现 Github 强制开启了 2FA,而且还不可以关闭的,每次你打开 github 都会提...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!




本文是一篇科普文,五一结束了,大家看点轻松的~


不知道大家在过去半年有没有发现 Github 强制开启了 2FA,而且还不可以关闭的,每次你打开 github 都会提醒你的验证:


Image.png


简单的说,就是打开 Github 进行验证时,只依靠密码验证已经不被允许,你必须打开你手机上的验证软件,把里面随机码输入到 Github 才能完成身份验证,类似于十年前国内 QQ 安全中心的验证。


这是一种双重验证的手段,用于更好的保证我们的账号安全,今天就以此为引,给大家讲讲账号安全相关发展的历史。


第一幕:密码的独角戏 - 脆弱的防线


在互联网的蛮荒时代,密码就像原始人手中的木棍和石头,是守护账号安全的唯一屏障。然而,这道防线却是脆弱不堪的,面对黑客的攻击,如同纸糊的老虎,一戳就破。暴-力-破-解、字-典-攻-击、社会-工程-学手段,都足以让密码这道防线形同虚设。


1. 暴-力-破-解:暴-力-破-解就是通过遍历的方式尝试出你的密码组合,比如银行的六位取款密码实际上只有 46656种组合,利用现在任何一台电脑或者手机的算力都能瞬间算出来,为了对应这种情况,现在几乎所有网站都有密码输入次数限制。


2. 字-典-攻-击:字典-攻击就是利用常用密码来攻破你的密码,比暴力破解效率更好,比如 123456 这个密码就有很多人使用。


3. 社会-工程-学:社会-工程-学说人话就是套你的话,或者调查你的信息,比如在和你沟通的过程中知道了你的手机号、身-份-证号码、生日信息等,因为有大量的人用手机号后六位、身-份-证号后六位或者生日当作自己的密码,所以这种手段的成功率一般会更高。


在当前这个时代,由于互联网各种 App 的涌入,每个人都拥有大量的账号,如何记忆他们成为了一个难题,大量的人选择对所有网站使用同一个密码,这就又造成了账号安全问题。


重复使用密码就像是使用同一把钥匙开启不同的门,一旦一把钥匙被复制,所有的门都将面临危险。


每年,全球都会发生无数起数据泄露事件,大量的用户名和密码被公开曝光。这些泄露的密码成为了黑客攻击的利器,他们可以利用这些密码进行撞库攻击,尝试登录其他网站。


会在不同的网站使用相同的密码将会导致“一损俱损”的局面。一旦一个网站发生数据泄露,黑客便可以利用泄露的密码尝试登录其他网站,从而获取更多个人信息,造成更大的损失。


第二幕:多因素认证 (MFA) 登场 - 多重关卡,层层设防


所以,为了弥补密码的不足,MFA 应运而生,为账号安全加装了多重门锁。除了密码这把“钥匙”,你还需要其他的“通关密语”才能进入:



  • 验证码: 这是国内最常用的方式,甚至几乎所有 App 都已经不需要你记忆账号密码,只需要一个手机验证码即可,国外使用手机验证码的很少,因为他们更多使用邮箱来注册账号,比如我现在在使用的编辑软件Craft 在登录时就要求你提供邮箱验证码,它甚至不能设置密码。

  • 指纹识别: 你的指纹独一无二,所以它就像是你的专属的“魔法印记”,轻轻一按,就能保证你是你。

  • 面部识别: 对着摄像头眨眨眼,你的面容信息也是你的专属印记,苹果手机上甚至使用了虹膜识别来检测你是你,而不是别人。

  • 安全令牌: 一个小巧的硬件设备,可以生成一次性密码,就像古代的“虎符”一样,只有拥有它才能调兵遣将。令牌可以有软件和硬件两种方式,软件就是 Google Auth 这种软件,而硬件则是我们早古互联网时代网上购物常用的网银 U 盾形式。


在开头的时候,我曾提到了 2FA,它和本节的 MFA 听名字非常相似,实际上说的也几乎是一个东西。


2FA 是指:需要两种验证,才能完成整个验证,一般是密码和动态安全令牌。


MFA 是指:需要两种或以上验证方式,才能完成整个验证,一般也是密码和动态安全令牌。


所以在大多数语境下,这俩说的其实是一个东西,有些验证方式将两个验证方式合而为一,比如手机/邮箱验证码。


因为多因子验证的核心是:一个你知道的凭证 和 一个你刚刚才知道的凭证。


我们一开始就知道的凭证往往是邮箱 + 密码,一个刚刚才会知道的凭证往往就是动态安全令牌码了,所以手机验证码登录的方式也是 2FA,还是属于比较方便的那种。


注:我这里说的手机验证码登录是真的发给你验证码,而不是国内的那种手机号一键登录。


第三幕:单点登录 (SSO) 崛起 - 统一管理的钥匙


其实随着 MFA 的出现,安全问题已经不需要太担心了,所以接下来账号安全开始朝着:安全 + 高效的方向开始发展,所以开始出现了 SSO。


SSO 的第一个阶段是内部互信,它的概念最早可以追溯到 1990 年代,随着企业内部网络的发展而兴起。


后来随着互联网的发展,一个公司往往同时拥有多个业务,比如十年前还是百度的天下的时候,我们会同时使用百度知道、百度贴吧、百度网盘这些产品。


你只需要在某一个百度旗下的产品登录一次,打开其他产品的时候往往也会自动识别到你的账号。


比如你在百度贴吧登录了,此时你打开网页版的百度网盘你自动就是已登录状态。


不要以为这是一个 So easy 的操作,它的原理其实是使用你存储在同一个主域名下的 cookie 实现的。


比如百度贴吧的域名是:tieba.baidu.com/,而百度网盘的域名是:pan.baidu.com/,它俩都属于主域名 baidu.com,所以通过携带同域名下cookie 的方式,让同域名下的其他服务也能正确识别当前账号。


具体识别方案一般有两种:



  1. 通过共享 session + cookie 的方式做验证。

  2. 通过获取 cookie 内部跳转到 SSO 做验证。


无论使用哪种方案,携带 cookie 这个操作必不可少,所以这一阶段的 SSO 是基于 Cookie 的。


可能还有一个词大家比较常见:SAML,SAML标准也是用于内部系统互信,做的事和基于 Cookie 的 SSO 都是一样的,所以这里我不再赘述。


第四幕:OAuth 协议的诞生 - 授权管理的桥梁


经历完 SSO 的第一个阶段之后,我们就来到了 SSO 的第二阶段:外部互信


由于 Web 互联网的兴起,这一阶段也被称为基于 Web 的 SSO,这一阶段的代表是OAuth。


你有没有想过,如果我们在所有平台都使用同一个账号多好,就不用在记忆那么多的应用账号密码,减少心智负担。


在国内互联圈地的情况下,这种情况并没有实现,也可以说通过手机号实现了。


但是在国外,Google 账号体系几乎就是事实上的一号通行,你注册一个 Google 账号之后,几乎可以通过这个账号登录所有的网站,这就是 OAtuh 的作用。


想象一下,你拥有许多宝藏,分别存放于不同的宝库中。比如,你在 Facebook 上存储着你的社交关系,在 Google 上存储着你的邮件和文件,在 Spotify 上存储着你的音乐喜好。


现在,你想要使用一个新的游戏应用,而这个应用需要访问你在 Facebook 上的好友列表,以便你能够邀请好友一起玩游戏。


这时,你面临一个两难的选择:



  • 分享密码: 将你的 Facebook 密码告诉游戏应用,让它直接访问你的好友列表。但这存在着巨大的安全风险,一旦游戏应用泄露你的密码,你的所有 Facebook 数据都将暴露无遗。

  • 放弃使用: 由于担心安全问题,你放弃使用这个游戏应用,从而错过了与好友一起游戏的乐趣。


为了解决上面这种问题,Google 等公司在 2010 年发布了 OAuth1.0,由于它存在许多问题,所以又在 2012 年发布了 OAuth2.0。


所以在现如今,几乎所有公司都接入了 Google 的 OAuth 登录,当你在第三方平台想使用 Google 账号登录时,OAuth 协议会引导你到 Google 进行授权。


平台会询问你是否同意授权第三方应用访问你的部分数据 (例如好友列表),如果你同意,平台就会发放一个临时的“通行证”给第三方应用,让它可以访问你的数据,但不会泄露你的密码。


所以 OAuth 的核心是授权而非共享。


第五幕:无密码时代的曙光 - 告别繁琐的密码


我相信当大家看到第四节的时候,大家就会觉得应该就这些了,没有别的新意了,恰恰相反,为了彻底摆脱密码的束缚,世界巨头们正在探索新的“魔法”,那就是无密码


在 2019 年,WebAuthn 标准被 W3C 以建议的形式发布,它是 FIDO 联盟下 FIDO2 的核心组件,旨在减少人们对于密码的依赖。


它带了以下三个好处:



  • 消除密码依赖: 通过使用更加安全的认证方式,例如生物识别技术 (指纹、面部识别) 或安全密钥,消除用户对密码的依赖,降低密码泄露和网络钓鱼攻击的风险。

  • 提升用户体验: 简化登录流程,无需记忆和输入复杂的密码,只需轻触指纹或插入安全密钥,即可完成身份验证。

  • 增强安全性: 使用公钥加密技术,确保用户的认证信息不会被窃取或伪造,有效抵御网络攻击。


如果大家有在 Mac 上的 Safari 浏览器登录苹果账号的经历,就会发现它不需要你输入密码,只需要一次简单的指纹验证:


Image.png


这时你通过验证你的指纹就可以顺利登录成功,这就是基于 WebAuthn 标准的 Passkeys。


目前苹果、谷歌、微软等几乎所有大厂都支持了Passkeys,,由于它也是一个 W3C 标准,所以你可以通过这个网站查看支持列表。


看起来指纹验证就像开头我们说过的 MFA,但是它比 MFA 多了一个东西就是设备,通过生物信息 + 受信设备的方式完成了它的整个认证流程,它拥有两个比较大的特点:



  • 提供了一套标准化的用户界面和用户体验,简化了无密码登录的操作流程。

  • 将用户的登录凭证 (私钥) 存储在用户的设备 (例如手机、电脑) 中,并通过云端服务进行同步,方便用户在不同设备上登录。


说回我们开头的 Github 的 2FA,其实 Github 也接入了它,如果你完成 2FA 之后,之后就可以在浏览器中通过指纹验证登录。


身份认证的未来已来,无密码的出现,为我们在登录授权流程中带来了许多方便~




好了,以上就是本篇文章的全部内容了,希望大家多多点赞支持,我将更快提供更好更优质的内容。


注:本文小标题是借助 AI 能力起的,部分描述也借助了 AI 美化,AI 美化生成内容不会超过 300 字(本文 4000 字),请大家放心食用。


作者:和耳朵
来源:juejin.cn/post/7364764922339065890
收起阅读 »

职场贩卖焦虑,不如调整心态

每当长假期来临之际,出行就变得异常困难,本来是该放松心态的阶段,因为各种各样不好的事情充斥在周围,败坏了本该愉悦的心情,五一还未出行,调补假期的事情就吵的沸沸扬扬,机票的大跌也带来背刺,这个假期注定从焦虑开始,接下来大家还会面对,路上的堵车、景区的人从众、天价...
继续阅读 »
image.png

每当长假期来临之际,出行就变得异常困难,本来是该放松心态的阶段,因为各种各样不好的事情充斥在周围,败坏了本该愉悦的心情,五一还未出行,调补假期的事情就吵的沸沸扬扬,机票的大跌也带来背刺,这个假期注定从焦虑开始,接下来大家还会面对,路上的堵车、景区的人从众、天价的消费,住店的隐私及背刺、以及可能遭遇的不好的天气等等等等;


啊~~~ 难道今天是要来贩卖焦虑吗?


贩卖焦虑吗?其实不然,我更想聊,大环境的变化,如何去调整自身?


大环境


互联网大环境,说说我自身的感受;


加班


加班,相比几年前,感受最深的是,或者说免费加班,越来越多;好像演变成了这家公司好像不搞点 996,都不是互联网公司,不搞点 996,这家公司都不赚钱,不搞点 996 ,这家公司都没有上升空间;


面试很多公司最少都是 995 起步,搞得候选人不接受,好像就吃不苦、没有奋斗精神一样,也许也是,不然为什么 79 的眉笔都会牵涉出工作不努力;但是想过没有,这可能真的会让人认知不够清晰的同学真的去怀疑自己是不是真的不够努力,也真的会让努力的人寒心;


并不是每个人都是幸存者偏差,成功真的不会降临在每个人身上,现实是大部分人不是出生在罗马,而是不是骡就是马,天赋加机遇,各种契机的组合,也许都是百万分之一的幸存率,真的不是努力了,就能有结果的;


但中国人骨子里优良的品质,从日出而作日入而息,自古以来的勤奋,就在基因里面代代传承,现实社会,别说出人头地,能够较好生存都已经不容易了,所以真的,社会多一些宽容,企业多一点人性,如果可以,真的要好好审视现状;


加班真的能带来更好的营收吗?也许可能吧,但是被禁锢的灵魂,你想要他能有多大自驱力,能有多大创造力,能看到前景吗?


内卷


人口大国,求职者甚众,听过最多也是最讽刺的话,你不干多的是有人干,是的,我们每个人都是备胎,我们每个人备胎都很多;


我不知道是从哪里看过一篇言论,大概就是说,程序员这个职业体系,其实大部分是没有什么背景的人,或者是农村人,他们没有好的出路,想要挣钱,想要快速走出原来的贫困圈,他没有太多的选择,他只能苦干;


很讽刺是不是,也许上面的话是被人断章取义的,但是我来自农村,我的感受确实大差不差,你以为干着一份体面的工作,其实在背地里都是牛马,而你没有办法去反驳,因为你没有背景,你要出圈;


所以细细想来,嘈杂的环境,加剧了内卷的诞生,一群本不该竞争的人,一群本该一起取暖的人,一群本来有着共同理想的人,逐渐变成了相互的垫脚石,kpi 的丑态,已经让很多人变成了死道友不是贫道的淡漠。


PUA


PUA,也许是自嘲的人或者不良的氛围或者玩梗,PUA 已经变成日常用语,『牛马们』 开始觉醒,原本作为管理者的手段,上位者心照不宣,突然被公之于众,大家都逐渐去理解你说的话,背后的含义是什么;


『我对你是失望的』『你在低绩效里面是靠前的』『机会已经给到你了,接下来就看你能不能把握了』『我知道你有压力,但你要扛得住压力』


是的,每个人都很珍惜自己的工作,每个人都有理想,每个人都想名利双收,利用人性的欲望去做管理,从本身来说没有什么问题,但是利用人性的弱点,去打压,去PUA,真的不病态吗?


小结


大环境确实存在问题,社会在发展,人类在进步,在快速发展的历程中,总会有一些病态的体系出现,但是在面对这些不定因素的时候,时刻保持理性的头脑,要学会去洞察,要保护自己,在做每一件事情上,不求完美,但求心安,不做别人的摆布工具,做好自我,坚守自我;


关于自我


社会立足,自我认知很重要


职业规划


和很多很多的同学聊过,经历过很多阶段的同学,实习生、初级、中级、高级、资深、专家等等,走技术架构的、走业务架构的、走技术管理的等等;


大部分的同学,其实都不具备职业规划的能力,甚至没有概念,这种真的会很盲目,没有目标、没有方向,一家一家公司换,随性,自由,看起来潇洒、有魄力;


薪资,当然是第一考虑因素,一切不聊到手薪资的都是耍流氓,但是真的不是唯一考虑因素,每个阶段,真的要知道自己需要的是什么,多去看看相关职业规划的书或者前辈的实际历程,当然不要去轻易质疑一个职业人生,理性看待每一件事;


当我们有意识的去按照自己想要走到路线的时候,薪资,我希望不是唯一的理由,当然他依然很重要,他依然是第一要素,不然工作为了什么?为了理想,丢掉面包吗?大可不必,除了自我感动,没有人会为你喝彩;


话说回来,职业规划,一定要重视,他不是今天的目标,也不是你今年的目标,他是你实现你人生目标的一个重要规划之一,每个阶段可能会变,变,很正常,不用自我怀疑,随着你见识的增长,经济的增长,你眼界会不一样,而我们要求的就是不断的是调整规划,为每个阶段的目标去准备,但是在快速奔走的过程中,也请回过头,看看自己的初心;


强大自我


本身我自己是前端,虽然现在不是纯粹的前端开发,但是从来没放弃过对前端技术的关注;


前端很乱,很快,很浮躁,『前端娱乐圈』 也不是一朝得名,很多时候,我也很焦虑,在做开发的时候,我对技术理解很纯粹,就希望用更好的方式去实现,更牛的技术去开发,但是随着了解的越多,越觉得需要了解的也越多,所以在很长的一段时间里,每天我都会查阅大量的资料,为了搞懂一个技术点,可能会去看一整本书,或者几十片技术文章;


我记得有一段时间,我甚至有点偏执,一段代码,甚至项目周期都过去一大半了,我突然发现实现的方式不是很好,很别扭,我直接把所有代码都删掉,甚至包括牵连的老代码,删掉一时爽,接下来就是无尽的痛苦,007 等着你;


换成现在,我不知道还会不会做这种偏执的事情,但是我想,不顺眼的,我照样还是会去优化,对技术的执着,我个人倒是觉得没有太大问题,但是前提别耽误项目进度,否则团队的信任会越来越低;


还有一个阶段,我觉得自己现状已经不是自己想要的了,接受降薪资,去一家可能会给我职业生涯带来有利成长的公司,人们没有办法当即去评判当时做的选择是对或者是错,事后看来,在做评价,已经没有太多意义,人生很长,也是到迟暮之年,这也许也是一段有着非同意义的经历;


我只是站在我自己的角度,去看待或者去如何强大自我,在这个过程中,有偏执,有耽误,也有背离,但是更多的是我自身对自身的调整,不能说我自己做的如何好,只能说,我是一直向着自身变好的方向努力;


关于焦虑


从我个人角度看,职业生涯中,已知和未知的事情太多,初期你会为了自己不够强大而焦虑,到后来你会发现你掌握很多东西,但是依然有很多盲区,技术很广,很难做到都了解;每个阶段都有每个阶段,或者每个环境需要有每个环境掌握的东西;有盲区,也请不要焦虑;


看过余华和罗翔的有意思一个对话:



  • 罗翔问余华老师:给女主人公起名叫林红,是取自南唐后主李煜的那个林花谢了春红,太匆匆,长恨朝来寒雨晚来风,胭脂泪,留人醉。自是人生长恨水长东吗?

  • 余华老师:我真没想这么多,我就是随便起的,现在是了。


你看,作者可能都没想那么多,如果说写的时候,每个都去考究,也许一篇小说,可以写几十年,也许余华老师就是取名字的时候,看到院子前面的林子里面的花红了,就叫林红,别去揣测太多,毕竟*『尤雨溪』*面试 vue 都不见得能通过;


我们能做的就是做好准备,拒绝内耗,理性看待,做每个阶段发力;


最后


环境、自身以及不可控因素,就像假期出行一样,你永远无法去预料即将发生的事情,如果被牵着走,你会发现很多事情,在没有开始之前,就已经寸步难行,不如调整心态,强大自我,无论是职场工作,还是生活,强大的自我,懂得适度调整,所遇之事,可能都会变得不一样,毕竟爱笑的程序员,代码肯定都不会写的很差;


作者:深谷逸风
来源:juejin.cn/post/7363193808522477607
收起阅读 »

background简写,真细啊!

web
背景原因 今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒: background: url('./bg.png') no-repeat center contain ; 搞定! 上面设置的依次是 背景图片 背...
继续阅读 »

背景原因


今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒:


background:  url('./bg.png') no-repeat center contain ;

搞定!


上面设置的依次是 背景图片 背景平铺模式 背景位置 背景图片是保有其原有的尺寸还是拉伸到新的尺寸。


so easy~


看我ctrl + s 保存代码,编译。


嗯? 怎么不生效? 俺的背景呢?
打开控制台一看,好家伙,压根没生效:


image.png


问题排查


第一反应是这些属性有固定顺序,但是凭我练习两年半的经验,不应该啊,之前也是这样用的啊,遂打开MDN,仔细翻阅....


发现了下面这段话:


image.png


这让我更加确信 写的没毛病啊!!


background-attachment、background-color、background-image、background-position、background-repeat、background-size
这些属性可以以任意顺序书写。


见了鬼了,待我排查两小时(摸鱼...)


原因浮现


在仔细阅读文档后发现,其实在文档的上面,还有另外一段话:


image.png


我恍然大悟,索嘎,以后看文档不能马虎了,得仔细查阅,过于经验主义了,这都是细节啊!


background使用注意事项和总结


其实,使用background时,大部分时候 属性的顺序是可以任意位置书写的,
但是有两个属性有点特殊,那就是background-size和background-position,


当background简写同时有这两个属性时,那么必须background-position在前,background-size在后,且两者只能紧挨着书写并且以 "/"分隔。
例如:


错误: background: url('./bg.png') no-repeat center  contain ; // 没有以 "/"分隔
错误: background: url('./bg.png') center no-repeat contain ; // 没有紧挨着书写
错误: background: url('./bg.png') no-repeat contain / center; //background-size写在了 background-position的前面

正确: background: url('./bg.png') no-repeat center / contain ;


写在最后


其实MDN在关于background的文档最开头的例子中就有写:


image.png


只不过没有用语言描述出来,一般没有认真看很难发现,所以有时候能够静下心来认真查阅文档,真的会发现很多细节(甩锅:这tm是谁写的文档,出来挨打).


作者:可狗可乐
来源:juejin.cn/post/7234825495333158949
收起阅读 »

JavaScript 流程控制语句详解:if语句、switch语句、while循环、for循环等

JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。接下来,我们将一一解析这些语句,带你走进JavaScript的世界。一、什么是流程控制语句流...
继续阅读 »

JavaScript,作为一种广泛使用的编程语言,它的流程控制语句是构建逻辑和实现功能的基础。流程控制语句包括条件语句、循环语句和转向语句,它们是编程中不可或缺的部分。

接下来,我们将一一解析这些语句,带你走进JavaScript的世界。

一、什么是流程控制语句

流程控制语句是用来控制程序中语句执行顺序的语句,它们可以影响程序的流程,从而实现不同的逻辑。流程控制语句主要分为以下三类:
Description

顺序结构: 这是最基本的流程控制,代表代码按照书写的顺序从上到下依次执行。通常程序都是从第一行代码开始顺序执行到结束的。

选择结构: 用于根据特定条件来控制代码的执行路径。常见的选择结构包括if、else、if-else if和switch等。这些语句允许程序在满足某些条件时执行特定的代码块,而在其他条件下执行另外的代码块或跳过某些代码。

循环结构: 用于重复执行某段代码直到满足退出条件为止。循环语句包括for、foreach、while和do-while等。通过这些语句,可以实现固定次数的循环或者当某个条件成立时的持续循环。

此外,还有跳转语句如break、continue和return等,它们可以改变正常的控制流程,例如跳出当前循环或者返回函数的结果。

二、条件判断语句

使用条件判断语句可以在执行某个语句之前进行判断,如果条件成立才会执行语句,条件不成立,则语句不执行。

语法一:if(条件表达式){语句…};

执行流程:
if语句在执行时,会先对条件表达式进行求值判断,

  • 如果条件表达式的值为true,则执行if后的语句,

  • 如果条件表达式的值为false,则不会执行if后的语句if语句只能控制紧随其后的那个语句。

如果希望if语句可以控制多条语句,可以将这些语句统一放在代码块中,如果就一条if语句,代码块不是必须的,但在开发中尽量写清楚。

代码演示:

<script>
if(true) console.log('好好学习,天天向上');
// 加上条件运算符 && ||
var a=20;
if(a>10&&a<=20){
alert('a在10-20之间');
alert("4567")
}
</script>

语法二:if…else…语句

语法:

if(条件表达式){
语句....
}else{
语句....
}

执行流程:
当该语句执行时,会先对if后的条件进行判断,

  • 如果该值为true,则执行if后的语句,

  • 如果该值为false,则执行else后的语句,两者选其一执行。

语法三:if…else if…else

语法:

if(条件表达式){
语句....
}else if(条件表达式){

语句....
}else{
语句....
}

执行流程:
当该语句执行时,会从上到下依次对条件表达式进行求值,

  • 如果值为true,则执行当前语句。

  • 如果值为false,则继续向下判断,如果所有的条件都不满意,就执行最后一个else或者不执行,该语句中,只会有一个代码块被执行,一旦代码块执行了, 则直接结束语句。

<script>
var age=16;
/* if(age>=60){
alert("你已经退休了~~~")
}else{
alert("你还没退休~~~")
} */


if(age>=100){
alert("您老高寿呀~~~");
}else if(age>=80){
alert("你也不小了");
} else if(age>=60){
alert("你刚退休呀~~~");
}else if(age>=30){
alert("你已经中年了");
}else if(age>=17){
alert("你刚成年呀~~~");
}else{
alert("你还是个小孩子~~")
};
</script>

三、条件分支语句

switch语句是一种多分支选择结构,它可以根据表达式的值,来选择执行不同的代码块。

语法:switch…case…

switch(条件表达式){
case 表达式:
语句....
break;
case 表达式:
语句....
break;
default:
语句...
break;
}

执行流程:

在执行时,会依次将case后的表达式的值和switch后的条件表达式的值进行全等比较。

  • 如果比较结果为true,则从当前case处开始执行代码,当前case后的所有代码都会执行;

  • 在case的后边跟着一个break关键字,这样可以确保只会执行当前case后的语句,而不会执行其他的case;

  • 如果比较结果为false,则继续向下比较;

  • 如果所有的比较结果都为false,则只执行default后的语句;

注意: switch语句和if语句的功能实际上有重复的,使用switch可以实现if的功能,同样使用if也可以实现switch的功能,所以我们使用时,可以根据自己的习惯选择。
代码演示:

<script>
var num=2;
switch(num){
case 1:
console.log("壹");
//使用break可以退出switch语句
break;
case 2:
console.log("贰");
break;
case 3:
console.log("叁")
break;

default:
console.log("非法数字~~~");
break;
}
</script>

四、循环语句

循环语句,就是让某段代码反复执行。在JavaScript中,主要有for循环、while循环、do…while循环等。

1) while循环

语法:

while(条件表达式){
语句
}

while语句执行流程:

先对条件表示式进行求值判断,如果值为true,则执行循环体,循环体执行完毕以后,继续对表达式进行判断,如果值为false,则终止循环。

2) do…while循环

语法:

do{

语句....

}while(条件表达式)

执行流程:

do…while 语句在执行时,会先执行循环体,循环体执行完毕后,再对while后的条件表示式进行判断,如果结果为true,则继续执行,执行完毕继续判断,如果结果为false,则停止执行。

注意: 实际上以上两个语句功能类似,不同的是while 是先判断后执行,而do…while会先执行后判断,do…while可以保证循环体至少执行一次,而while不行。


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

3)for语句(for循环)

在for循环中,为我们提供了专门的位置,用来放三个表达式。

  • 初始化表达式
  • 条件表达式
  • 更新表达式

for循环的语法:

for(初始化表达式;条件表达式;更新表达式){

语句....

}

for循环的执行流程:

  • 初始化表达式,初始化变量(初始化表达式,只会执行一次);

  • 条件表达式,判断是否执行循环;

  • 如果为true,则执行循环,如果为false,终止循环;

  • 执行更新表达式,更新表达式执行完毕继续重复。

<script>
//第一种写法
for(var i=0;i<10;i++){
alert(i);
}
//第二种写法 for循环中的三个部分都可以省略,也都可以写在外部
var i=0;
for(;i<10;){
alert(i++);
}

//如果在for循环中,不写任何的表达式,只写两个;
//此时循环是一个死循环,会一直执行下去,慎用
for(;;){
alert("hello");
}
</script>

五、break和continue语句

break关键字

可以用来退出switch或循环语句,不能在if语句中使用break和continue,break关键字,会立即终止离它最近的那个循环语句。

continue关键字

可以用来跳过当次循环,同样continue也是默认只会对离它最近的循环起作用。

终止指定循环

可以为循环语句创建一个label(标签),来标识当前的循环。

语法:

label(给起的标签名字):循环语句

使用break语句时,可以在break后跟着一个label,这样break可以结束指定的循环,而不是最近的。

代码演示

<script>

/* for(var i=0;i<5;i++){
console.log(i);
//break;//用来结束for的循环语句,for只会循环一次
if(i==2){
break;//这个break是对整个for循环起作用的
}
} */


/* for (var i = 0; i < 5; i++) {
console.log("@外层循环" + i);
for (var j = 0; j < 5; j++) {
break;//只会结束离他最近的内层循环
console.log("内层循环" + j);
}
} */



/* outer: for (var i = 0; i < 5; i++) {
console.log("@外层循环" + i);
for (var j = 0; j < 5; j++) {
break outer; //指定结束外层的for循环
console.log("内层循环" + j);
}
} */



for (var i = 0; i < 5; i++) {
if (i == 2) {
continue;
}
console.log(i);
}
</script>

JavaScript的流程控制语句,就像是一把魔法棒,它能让我们的代码按照我们的意愿去运行。掌握了这些语句,我们就可以在编程的世界里自由翱翔。

希望这篇文章能帮助你更好地理解和使用JavaScript的流程控制语句,让我们一起在编程的道路上,探索更多的可能性。

收起阅读 »

前端使用a链接下载内容增加loading效果

web
问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。代码如下:// utils.jsconst XLSX = require('xlsx')// 将一个sheet转成最终的exce...
继续阅读 »
  1. 问题描述:最近工作中出现一个需求,纯前端下载 Excel 数据,并且有的下载内容很多,这时需要给下载增加一个 loading 效果。
  2. 代码如下:
// utils.js
const XLSX = require('xlsx')
// 将一个sheet转成最终的excel文件的blob对象,然后利用URL.createObjectURL下载
export const sheet2blob = (sheet, sheetName) => {
sheetName = sheetName || 'sheet1'
var workbook = {
SheetNames: [sheetName],
Sheets: {}
}
workbook.Sheets[sheetName] = sheet
// 生成excel的配置项
var wopts = {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
}
var wbout = XLSX.write(workbook, wopts)
var blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
// 字符串转ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
return blob
}

/**
* 通用的打开下载对话框方法,没有测试过具体兼容性
* @param url 下载地址,也可以是一个blob对象,必选
* @param saveName 保存文件名,可选
*/
export const openDownloadDialog = (url, saveName) => {
if (typeof url === 'object' && url instanceof Blob) {
url = URL.createObjectURL(url) // 创建blob地址
}
var aLink = document.createElement('a')
aLink.href = url
aLink.download = saveName + '.xlsx' || '1.xlsx' // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
var event
if (window.MouseEvent) event = new MouseEvent('click')
else {
event = document.createEvent('MouseEvents')
event.initMouseEvent(
'click',
true,
false,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
)
}
aLink.dispatchEvent(event)
}

<el-button
@click="clickExportBtn"
>
<i class="el-icon-download"></i>下载数据
</el-button>
<div class="mongolia" v-if="loadingSummaryData">
<el-icon class="el-icon-loading loading-icon">
<Loading />
</el-icon>
<p>loading...</p>
</div>

clickExportBtn: _.throttle(async function() {
const downloadDatas = []
const summaryDataForDownloads = this.optimizeHPPCDownload(this.summaryDataForDownloads)
summaryDataForDownloads.map(summaryItem =>
downloadDatas.push(this.parseSummaryDataToBlobData(summaryItem))
)
// donwloadDatas 数组是一个三维数组,而 json2sheet 需要的数据是一个二维数组
this.loadingSummaryData = true
const downloadBlob = aoa2sheet(downloadDatas.flat(1))
openDownloadDialog(downloadBlob, `${this.testItem}报告数据`)
this.loadingSummaryData = false
}, 2000),

// css
.mongolia {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
color: #409eff;
z-index: 9999;
}
.loading-icon {
color: #409eff;
font-size: 32px;
}
  1. 解决方案探究:
  • 在尝试了使用 $nextTick、将 openDownloadDialog 改写成 Promise 异步函数,或者使用 async/await、在 openDownloadDialog 中添加 loadingSummaryData 逻辑,发现依旧无法解决问题,因此怀疑是 document 添加新元素与 vue 的 v-if 渲染产生冲突,即 document 添加新元素会阻塞 v-if 的执性。查阅资料发现,问题可能有以下几种:

    • openDownloadDialog 在执行过程中执行了较为耗时的同步操作,阻塞了主线程,导致了页面渲染的停滞。
    • openDownloadDialog 的 click 事件出发逻辑存在问题,阻塞了事件循环(Event Loop)。
    • 浏览器在执行 openDownloadDialog 时,将其脚本任务的优先级设置得较高,导致占用主线程时间片,推迟了其他渲染任务。
    • Vue 的批量更新策略导致了 v-if 内容的显示被延迟。
  • 查阅资料后找到了如下几种方案:

      1. 使用 setTimeout 使 openDownloadDialog 异步执行
      clickExport() {
      this.loadingSummaryData = true;

      setTimeout(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }


      1. 对 openDownloadDialog 内部进行优化
      • 避免大循环或递归逻辑
      • 将计算工作分批进行
      • 使用 Web Worker 隔离耗时任务
        • 在编写 downloadWorker.js 中的代码时,要明确这部分代码是运行在一个独立的 Worker 线程内部,而不是主线程中。

            1. 不要直接依赖或者访问主线程的全局对象,比如 window、document 等。这些在 Worker 内都无法直接使用。
            1. 不要依赖 DOM 操作,比如获取某个 DOM 元素。Worker 线程无法访问页面的 DOM。
            1. 代码执行的入口是 onmessage 回调函数,在其中编写业务逻辑。
            1. 和主线程的通信只能通过 postMessage 和 onmessage 发送消息事件。
            1. 代码应该是自包含的,不依赖外部变量或状态。
            1. 可以导入其他脚本依赖,比如用 import 引入工具函数等。
            1. 避免修改或依赖全局作用域,比如定义全局变量等。
        • 总之,编写 downloadWorker.js 时要注意它执行在一个独立环境中,只能通过消息事件与主线程通信。
    // 创建 Worker
    const worker = new Worker('downloadWorker.js');

    // 点击下载时向 Worker 发送消息
    function clickDownload() {

    showLoading();

    worker.postMessage({
    url: fileURL,
    filename: 'report.xlsx'
    });

    worker.onmessage = function(e) {
    // 收到下载完成的消息
    hideLoading();
    }

    }

    // 显示 loading
    function showLoading() {
    loading.style.display = 'block';
    }

    // 隐藏 loading
    function hideLoading() {
    loading.style.display = 'none';
    }

    // downloadWorker.js

    onmessage = function(e) {
    const url = e.data.url;
    const filename = e.data.filename;

    // 创建并点击链接触发下载
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();

    postMessage('下载完成');
    }

    <div id="loading" style="display:none;">
    Downloading...
    </div>

    <button onclick="clickDownload()">
    Download
    </button>

    <script src="downloadWorker.js"></script>


      1. 使用 requestIdleCallback 进行调度
      clickExport() {
      this.loadingSummaryData = true;

      requestIdleCallback(() => {
      openDownloadDialog(downloadBlob, `${this.testItem}报告数据`);

      this.loadingSummaryData = false;
      });
      }
    • 当使用上述方案时发现里面的内容变得异常的慢,于是去问GPT后,得到以下答案:

      使用 requestIdleCallback 来执行 openDownloadDialog 确实可能会需要更多时间。

      因为 requestIdleCallback 会在主线程空闲时才执行回调函数,所以不像 setTimeout 可以直接按指定时间异步执行。

      requestIdleCallback 的优先级较低,只在主线程空闲时才会执行,所以 download 的速度会比 setTimeout 慢一些。

      但使用 requestIdleCallback 的优点是它不会占用主线程时间,可以避免阻塞页面渲染。

      所以需要权衡执行速度和避免阻塞之间的关系:

      • 如果下载速度是优先考虑的因素,可以继续使用 setTimeout 异步执行。
      • 如果避免阻塞渲染更重要,则 requestIdleCallback 是更好的选择。

      偶尔下载任务被延迟不是大问题,但频繁的下载任务阻塞会严重影响页面体验。

      此外,可以结合两者试试:

      setTimeout 先异步调度,然后在回调中使用 requestIdleCallback 避免阻塞。这样就可以兼顾下载速度和避免渲染阻塞了。

    clickExport() {

    this.loadingSummaryData = true;

    setTimeout(() => {

    requestIdleCallback(() => {
    openDownloadDialog(downloadBlob);
    });

    this.loadingSummaryData = false;

    }, 200);

    }


      1. 分析线程模型,找到具体的阻塞点
      • 使用 Performance 工具分析线程
      • debugger 及 console.log 打印关键函数的执行时间
      • 检查是否有非主线程的任务阻塞了主线程
      1. 调整 vue 的批量更新策略
    new Vue({
    config: {
    // 修改批量更新的队列长度
    batchUpdateDuration: 100
    }
    })



作者:李卫泽
来源:juejin.cn/post/7268050036474609683
收起阅读 »

给迷茫的朋友一点建议吧,主要是前端方向的。

背景 前两天分享了一篇自己的个人经历,没想到引来了那么多人共鸣,很多人留下了评论,大部分评论我都认真看了,并且也回复了,有的评论建议真的很棒,对我的帮助很大,很感谢。 今天给评论给整理了一下,再加上自己有一些新的感悟,给大家分享分享,给迷茫的朋友一点建议。有很...
继续阅读 »

背景


前两天分享了一篇自己的个人经历,没想到引来了那么多人共鸣,很多人留下了评论,大部分评论我都认真看了,并且也回复了,有的评论建议真的很棒,对我的帮助很大,很感谢。


今天给评论给整理了一下,再加上自己有一些新的感悟,给大家分享分享,给迷茫的朋友一点建议。有很多东西想说,但是表达能力有限,只能写出一点。


一个30岁老前端的人生经历(学习+工作+婚姻+孩子),给迷茫的朋友一点激励。前端是不是真的不行了

有些没有毕业或者刚毕业的兄弟们,很关心这个问题,因为关乎他们是否要学前端开发。还有一些想转前端开发的兄弟,也很关心这个问题。


关于这个问题,上篇文章有评论开玩笑的说现在入前端,相当于49年入guojun,下面就这个问题我发表一下我的个人看法。


先说一下我的观点,前端确实比以前难很多,工作岗位变少,薪资变低,技术要求也变高了,还没入或准备入前端的,建议真的不要入了,有经验的都不好找工作,更别说没工作经验的了,当然天才除外。


我同学和我朋友两个前端(都是好几年工作经验的)找了几个月都没有合适的工作,面试机会还是有的,但是外包偏多,并且还要降薪,最后都拒了。


他们还发现了一个现象,有时候面试的过程中整体都非常好,hr有要发offer的意思,但是就一直拖着,然后后面突然有一天说不合适了。这种应该是有更好的选择了,以前都是求职者手握多个offer,去挑选公司,现在公司占据主动了,从多个候选人中挑员工了。


后端情况我不清楚,但是前端真的没有以前好就业了,这个时候想入前端的,我真的不太建议,除非你真的特别牛,就算你很牛,但是没有面试机会,也没办法证明啊。


有些已经入前端的新人私信我,让我给他一些提升建议。



  1. 多写多练,熟能生巧,没有别的好办法。先不用去看某某某源码,也不用花很多时间去深入的了解某个库,会用就行,然后从网上找个实战项目跟着做,或者自己从零做个小项目,在实战的过程中可以快速定位到自己薄弱的地方,然后针对自己的问题查漏补缺就行了。我当时从后端转前端时候就是这样的,不一定对所有人都适用,仅供大家参考。

  2. 多总结和复盘。这个习惯让我一直受益,比如今天的文章就是对上一篇文章评论的总结。经常复盘真的能让学习效率变高很多的,还能避免犯同样的错。我的习惯是白天学东西,晚上睡觉前复盘总结一下,这样一来印象很深刻。

  3. 学习的时候,不要怕别人说卷什么的,有些人看到别人学习就说别人卷,反过来自己偷偷的学,这种人就是不想别人追赶上自己。还有一些人自己天天下班打游戏从来不学习,看到同事学习,就在背后说别人卷,这种人就是害怕被别人甩开。为了某个目标去奋斗,我觉得是一件很有意义的事情。比如我吧,天赋一般,家庭一般,就是靠比别人付出的多,才能让一家人过上好一点的生活,只要家人开心,”卷“一点又有什么呢,随别人说去吧。

  4. 定目标。这个对于自律性不够的兄弟有帮助,相信很多人看完某个鸡汤文后,努力个几天,就坚持不下去了,这就是因为没有目标,有时候定完目标还是坚持不下去,可以找人来监督一下。比如我在上篇文章中立的flag,明年写40篇文章,有评论说写质量高的文章,一年肯定写不了那么多。是的,好文章确实不是硬挤出来的,我立这个flag的原因,是我知道自己自律不够,立出来让大家监督的,至于文章质量,我觉得只要用心去写,能帮助到人而不误导人就行了。


小结


没入前端的同学,建议别入了。刚入前端的同学想快速提升自己,多学多练,多总结和复盘,不要怕被别人说卷,给自己定目标。


30岁前端的路


我在flag里写了打算2024年认真看一下react源码,很多大佬建议说现在别搞这些没意义的东西了,搞前端死路一条。


关于大龄前端发展,从评论建议中,我总结了以下几条常用出路,欢迎大家补充。


深入技术


都说前端的天花板就是组长,结合我自身的经历和评论区的留言来看好像确实是这样,我几年前就开始做前端负责人了,其实就是组长,只是管的人多少的问题,cto永远是后端。主要工作就是需求来了,把任务给下面人分一分,就没了,最有挑战的也就是框架基建了吧。


评论里有人说,如果想要继续搞技术的话,可以往全栈方向发展,因为前端很难涉及到公司业务,所以可替代性很高,后端和公司业务结合的比较深一点,而业务才是公司的根本,换一个人很难立马上手,可替代性比前端低很多。


关于全栈我的观点是,大龄前端还是往全栈发展比较好,除了上面的原因外,全栈可以自己做个产品,还可以接私活、做自由职业者,甚至转管理都需要懂点后端技术。


想转全栈,但是又不想学别人语言,node也是可以做后端开发的,现在比较成熟的框架有midway和nest,甚至koa都可以做后端开发。关注我的人应该知道我正在做的fluxy-admin就是一个全栈项目,前端使用的是react,后端使用的是midway,前端上手midway还是很简单的,我一个星期就上手写项目了。


转管理


有一些评论提到了转管理,说实话这个是需要机会的,不是所有人想转就转的,因为公司很少愿意给员工尝试的机会,除非你开发的时候已经表现出了管理方面的天赋,让老板认可你,不然都很难转成功,一是一个萝卜一个坑,老的领导没走你就上不去,而上面领导走了,很多公司可能会从外面招一个新的人空降过来,而不是从下面提拨一个上去。


还有管理也不是什么人都可以做的,有的人搞技术可以,管理真的不太行,比如我,在某家公司,当时管着10几个前端,团队被我带的一团糟,后来我找个副组长帮我管理,我专心搞技术搞框架,团队才慢慢走上正规。


个人觉得想转管理,需要机会和自身有管理才能,这两个条件都很重要。


转产品


有些评论还提到了转产品,我感觉这个还是比较靠谱的,玉伯大佬就是一个成功转型的案例。


个人感觉前端转产品还是有一些优势的,下面是我从网上搜的:



  1. 技术背景:掌握技术知识,能更好地理解产品实现的可能性与限制,与技术团队有效沟通。

  2. 用户体验关注:作为前端开发者,通常会对用户界面和用户体验有较深的理解,有助于设计更好的产品。

  3. 敏捷与迭代理念:熟悉敏捷开发流程和迭代思维,能够快速响应市场变化并调整产品方向。

  4. 问题解决能力:编程经验锻炼了解决复杂问题的能力,这在产品管理中也非常重要。

  5. 沟通桥梁:作为曾经的开发人员,可以成为团队内部技术和非技术成员之间的沟通桥梁。


前端转产品,可以将技术理解和用户体验融入产品管理中,提高产品的竞争力。感觉门槛也不高,只需要有想法,并且现在软件行业那么发达,实在想不到好的点子,还可以借鉴别人产品设计。


甚至以后AI发展的很成熟了,可能就不需要开发了,只需要产品经理把需求描述好,产品就自动生成出来了。


我现在也在往这方面发展,最近一年在公司主导研发低代码平台,从最开始的设计到基本构架实现都是我一个人,框架成熟后,经常和交付直接交流,渐渐往产品的角色发展了。现在工作中不只是只有代码了,经常也会站在产品的角度去思考,做的东西怎么才能更有价值,怎么提高用户体验等。


平时有一些想法在公司产品里用不上,所以打算自己做一个产品,现在有了一些idea,明年应该会去做。这里说明一下,如果有想法想做个产品但找不到合适的后端合伙人,全栈做小应用或独立产品还是很有优势的,呼应了前面说的全栈优势。


搞副业


评论里也有很多人建议搞副业,这个确实也是一条出路,但是我感觉搞副业得有可靠的路子,不能盲目的去搞,不然最后可能血本无归。


我知道前几年有个程序员可以干的并且很好的副业,录视频卖课程,现在随着互联网新人越来越少,这个副业已经不行了。


现在我知道的有个副业还不错,在线帮别人修改简历,因为现在很多人找不到工作,甚至没有面试机会,他们会很焦虑,认为可能是自己简历的问题,会找一些大佬帮忙优化简历,搞这个的提前你得是个大佬,并且有一定的知名度,对于普通人不适用。


现在也有一些人趁着AI的热度,独立开发AI应用,靠这个赚了一些钱,我对这个不太了解,不做评价。


下面是一位大佬的评论,可能比较适合普通人,不过他说的那些我都没有认证,大家请谨慎对待:


image.png


转行


有很多评论认为搞技术没有前途,还是要考公,确实对于刚入行或准备入行的兄弟考公是一个比较好的选择,因为年轻还有很多机会,像我们这种年龄大的,还拖家带口的已经不适合改行了,成本太高了,真经不起折腾。


也有评论说想回老家,说实话我也想老家,但是以我的能力,回老家估计都养不活一家人,只会写代码,回老家能干啥呢。


最后


上面的内容是根据评论内容简单的整理了一下,还加了一些个人看法,大家可以结合自己的情况食用。可能还有更多好的建议因为我文笔有限,没写出来,欢迎大家评论,一起交流。


最后说一下自己吧,没写文章之前,我一直认为作为一个程序员技术最重要的,就像我的简介里说的那样,我唯一会的就是写代码,我不一定会成功,但是我会努力做的更好。看完大家的评论后,内心有过动摇,是不是不应该继续卷技术了,在写这篇文章的时候,我又坚定了,因为搞技术不只是我的工作,这也是我热爱的东西,即使有一天被迫失业不做程序员了,我可能偶尔还会写写代码。不过鸡蛋不能放在一个篮子里的道理我还是知道的,我现在已经在慢慢往产品转,用以保证未来不失业,因为还有一大家子需要我来养啊。共勉。


作者:前端小付
来源:juejin.cn/post/7311932190442635299
收起阅读 »

JSON慢地要命: 看看有啥比它快!

是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索...
继续阅读 »


是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索更快的替代方案和优化技术, 以确保你的应用程序以最佳状态运行.


JSON 是什么? 为何我要关注这个问题?



JSON 教程 | w3resource


JSON是JavaScript Object Notation的缩写, 是一种轻量级数据交换格式, 已成为Web应用中传输和存储数据的首选. 它的简洁性和人类可读格式使人类和机器都能轻松使用. 但是, 为什么要在Web开发项目中关注 JSON 呢?


JSON 是应用中数据的粘合剂. 它是服务器和客户端之间进行数据通信的语言, 也是数据库和配置文件中存储数据的格式.


JSON 的流行以及人们使用它的原因…


JSON 在Web开发领域的受欢迎程度怎么强调都不为过. 它已成为数据交换的事实标准, 这其中有几个令人信服的原因:


它易于使用!



  1. 人类可读格式: JSON 使用简单明了, 基于文本的结构, 开发人员和非开发人员都能轻松阅读和理解. 这种人类可读的格式增强了协作, 简化了调试.

  2. 语言无关性: JSON 与任何特定的编程语言无关. 它是一种通用的数据格式, 几乎所有现代编程语言都能对其进行解析和生成, 因此它具有很强的通用性.

  3. 数据结构一致性: JSON 使用键值对, 数组和嵌套对象来实现数据结构的一致性. 这种一致性使其具有可预测性, 便于在各种编程场景中使用.

  4. 支持浏览器: 网络浏览器原生支持 JSON, 允许Web应用与服务器进行无缝通信. 这种本地支持极大地促进了 JSON 在Web开发中的应用.

  5. JSON API: 许多网络服务和应用接口默认以 JSON 格式提供数据. 这进一步巩固了 JSON 在Web开发中作为数据交换首选的地位.

  6. JSON Schema: 开发人员可以使用 JSON 模式来定义和验证 JSON 数据的结构, 从而为应用增加了一层额外的清晰度和可靠性.


鉴于这些优势, 难怪全球的开发人员都依赖 JSON 来满足他们的数据交换需求. 然而, 随着我们在本博客的深入探讨, 我们将发现与 JSON 相关的潜在性能挑战, 以及如何有效解决这些挑战.


速度需求


🚀🚀🚀


应用的速度和响应的重要性


在当今快节奏的数字环境中, 应用的速度和响应能力是不可或缺的. 用户希望在Web和移动应用中即时获取信息, 快速交互和无缝体验. 对速度的这种要求是由以下几个因素驱动的:



  1. 用户期望: 用户已习惯于从数字互动中获得闪电般快速的响应. 他们不想等待网页的加载或应用的响应. 哪怕是几秒钟的延迟, 都会导致用户产生挫败感并放弃使用.

  2. 竞争优势: 速度可以成为重要的竞争优势. 反应迅速的应用往往比反应迟缓的应用更能吸引和留住用户.

  3. 搜索引擎排名: 谷歌等搜索引擎将网页速度视为排名因素. 加载速度更快的网站往往在搜索结果中排名靠前, 从而提高知名度和流量.

  4. 转化率: 电子商务网站尤其清楚速度对转化率的影响. 网站速度越快, 转换率越高, 从而增加收入.

  5. 移动性能: 随着移动设备的普及, 对速度的需求变得更加重要. 移动用户的带宽和处理能力往往有限, 因此快速的应用性能是必要的.


JSON 会拖慢我们的应用吗?


现在, 让我们来讨论核心问题: JSON 是否会拖慢我们的应用?


如前所述, JSON 是一种非常流行的数据交换格式. 它灵活, 易用, 并得到广泛支持. 然而, 这种广泛的应用并不意味着它不会面临性能挑战.


某些情况下, JSON 可能是导致应用慢的罪魁祸首. 解析 JSON 数据的过程, 尤其是在处理大型或复杂结构时, 可能会耗费宝贵的毫秒时间. 此外, 低效的序列化和反序列化也会影响应用的整体性能.


在接下来的内容中, 我们将探讨 JSON 成为应用瓶颈的具体原因, 更重要的是, 探讨如何缓解这些问题. 在深入探讨的过程中, 请记住我们的目标不是诋毁 JSON, 而是了解其局限性并发现优化其性能的策略, 以追求更快, 反应更灵敏的应用.



LinkedIn将Protocal Buffers与Rest.li集成以提高微服务性能| LinkedIn工程


JSON 为什么会变慢


尽管 JSON 被广泛使用, 但它也难逃性能挑战. 让我们来探究 JSON 可能会变慢的原因, 并理解为什么 JSON 并不总是数据交换的最佳选择.


1. 解析带来的开销


当 JSON 数据到达应用时, 它必须经过解析过程才能转换成可用的数据结构. 解析过程可能相对较慢, 尤其是在处理大量或深度嵌套的 JSON 数据时.


2. 序列化和反序列化


JSON 要求在从客户端向服务器发送数据时进行序列化(将对象编码为字符串), 并在接收数据时进行反序列化(将字符串转换回可用对象). 这些步骤会带来开销, 影响应用的整体速度.


微服务架构的世界里, JSON 通常用于在服务之间传递消息. 但是, 很关键的是, 我们必须认识到, JSON 消息需要序列化和反序列化, 这两个过程会带来巨大的开销.



在有大量微服务不断通信的场景中, 这种开销可能会增加, 并有可能使应用变慢, 以至于影响用户体验.




我们面临的第二个挑战是, 由于 JSON 的文本性质, 序列化和反序列化的延迟和吞吐量都不理想.
— LinkedIn



1_74sQfiW0SjeFfcTcgNKupw.webp
序列化和反序列化


3. 字符串操作


JSON 基于文本, 在连接和解析等操作中严重依赖字符串操作. 与处理二进制数据相比, 处理字符串的速度会慢一些.


4. 缺乏数据类型


JSON 的数据类型(如字符串, 数字, 布尔值)非常有限. 复杂的数据结构可能需要效率较低的表示法, 从而导致内存使用量增加和处理速度减慢.



5. 冗余


JSON 的人类可读性设计可能会导致冗余. 不需要的键和重复的结构增加了有效载荷的大小, 导致数据传输时间延长.



第一个挑战是 JSON 是一种文本格式, 往往比较冗余. 这导致网络带宽使用量增加, 更高的延迟, 效果并不理想.
— LinkedIn



6. 不支持二进制


JSON 缺乏对二进制数据的本地支持. 在处理二进制数据时, 开发人员通常需要将其编解码为文本, 而这可能会降低效率.


7. 深度嵌套


在某些情况下, JSON 数据可能是深嵌套的, 需要递归解析和遍历. 这种计算复杂性会降低应用的运行速度, 尤其是在没有优化的情况下.


JSON的替代方案


虽然 JSON 是一种通用的数据交换格式, 但由于其在某些情况下的性能限制, 人们开始探索更快的替代格式. 让我们深入探讨其中的一些替代方案, 了解何时以及为何选择它们:


1. Protocol Buffers(protobuf)


Protocal Buffers通常被称为protobuf, 是由谷歌开发的一种二进制序列化格式. 它的设计宗旨是高效, 紧凑和快速. Protobuf 的二进制性质使其在序列化和反序列化方面的速度明显快于 JSON.



  • 何时选择: 当你需要高性能的数据交换时, 尤其是在微服务架构, 物联网应用或网络带宽有限的情况下, 请考虑使用Protobuf.


GitHub - vaishnav-mk/protobuf-example


2. MessagePack


MessagePack 是另一种二进制序列化格式, 以速度快, 结构紧凑而著称. 它比 JSON 更有效率, 同时与各种编程语言保持兼容.



  • 何时选择: 当你需要在速度和跨语言兼容性之间取得平衡时, MessagePack 是一个不错的选择. 它适用于实时应用和对减少数据大小至关重要的情况.


3. BSON (二进制 JSON)


BSON 或二进制 JSON 是一种从 JSON 衍生出来的二进制编码格式. 它保留了 JSON 的灵活性, 同时通过二进制编码提高了性能. BSON 常用于 MongoDB 等数据库.



  • 何时选择: 如果你正在使用 MongoDB, 或者需要一种格式来弥补 JSON 和二进制效率之间的差距, 那么 BSON 是一个很有价值的选择.


4. Apache Avro


Apache Avro 是一个数据序列化框架, 专注于提供一种紧凑的二进制格式. 它基于schema, 可实现高效的数据编解码.



  • 何时选择: Avro 适用于schema演进非常重要的情况, 如数据存储, 以及需要在速度和数据结构灵活性之间取得平衡的情况.


与 JSON 相比, 这些替代方案提供了不同程度的性能改进, 具体选择取决于你的具体使用情况. 通过考虑这些替代方案, 你可以优化应用的数据交换流程, 确保将速度和效率放在开发工作的首位.



JSON, Protobufs, MessagePack, BSON 和 Avro 之间的差异


每个字节都很重要: 优化数据格式


在效率和速度至上的数据交换世界中, 数据格式的选择会产生天壤之别. 本节将探讨从简单的 JSON 数据表示到更高效的二进制格式(如 Protocol Buffers, MessagePack, BSON 和 Avro)的过程. 我们将深入探讨每种格式的细微差别, 并展示为什么每个字节都很重要.


开始: JSON 数据


我们从简单明了的 JSON 数据结构开始. 下面是我们的 JSON 数据示例片段:


{
"id": 1, // 14 bytes
"name": "John Doe", // 20 bytes
"email": "johndoe@example.com", // 31 bytes
"age": 30, // 9 bytes
"isSubscribed": true, // 13 bytes
"orders": [ // 11 bytes
{ // 2 bytes
"orderId": "A123", // 18 bytes
"totalAmount": 100.50 // 20 bytes
}, // 1 byte
{ // 2 bytes
"orderId": "B456", // 18 bytes
"totalAmount": 75.25 // 19 bytes
} // 1 byte
] // 1 byte
} // 1 byte

JSON 总大小: ~ 139 字节


虽然 JSON 用途广泛且易于使用, 但它也有一个缺点, 那就是它的文本性质. 每个字符, 每个空格和每个引号都很重要. 在数据大小和传输速度至关重要的情况下, 这些看似微不足道的字符可能会产生重大影响.


效率挑战: 使用二进制格式减小尺寸



现在, 让我们提供其他格式的数据表示并比较它们的大小:


Protocol Buffers (protobuf):


syntax = "proto3";

message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_subscribed = 5;
repeated Order orders = 6;

message Order {
string order_id = 1;
float total_amount = 2;
}
}

0A 0E 4A 6F 68 6E 20 44 6F 65 0C 4A 6F 68 6E 20 44 6F 65 65 78 61 6D 70 6C 65 2E 63 6F 6D 04 21 00 00 00 05 01 12 41 31 32 33 03 42 DC CC CC 3F 05 30 31 31 32 34 34 35 36 25 02 9A 99 99 3F 0D 31 02 42 34 35 36 25 02 9A 99 99 3F

Protocol Buffers 总大小: ~ 38 bytes


MessagePack:


(注意:MessagePack 是一种二进制格式, 此处的表示法非人工可读.)


二进制表示(十六进制):


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f


MessagePack 总大小: ~34 字节


BSON (二进制 JSON):


(注意:BSON 是一种二进制格式, 此处的表示法非人工可读.)


二进制表示法 (十六进制):


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~ 43 字节


Avro:


(注: Avro使用schema, 因此数据与schema信息一起编码.)


二进制表示法 (十六进制):


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~ 32 字节



(这些替代方案的实际字节数可能会有所不同, 提供这些数字只是为了让大家有个大致的了解.)


现在你可能会感到奇怪, 为什么我们的程序会有这么多的字节数?


现在你可能想知道为什么有些格式输出的是二进制, 但它们的大小却各不相同. Avro, MessagePack 和 BSON 等二进制格式具有不同的内部结构和编码机制, 这可能导致二进制表示法的差异, 即使它们最终表示的是相同的数据. 下面简要介绍一下这些差异是如何产生的:


1. Avro:



  • Avro 使用schema对数据进行编码, 二进制表示法中通常包含该schema.

  • Avro 基于schema的编码可提前指定数据结构, 从而实现高效的数据序列化和反序列化.

  • Avro 的二进制格式设计为自描述格式, 这意味着schema信息包含在编码数据中. 这种自描述性使 Avro 能够保持不同版本数据模式之间的兼容性.


2. MessagePack:



  • MessagePack 是一种二进制序列化格式, 直接对数据进行编码, 不包含schema信息.

  • 它使用长度可变的整数和长度可变的字符串的紧凑二进制表示法, 以尽量减少空间使用.

  • MessagePack 不包含schema信息, 因此更适用于schema已预先知道并在发送方和接收方之间共享的情况.


3. BSON:



  • BSON 是 JSON 数据的二进制编码, 包括每个值的类型信息.

  • BSON 的设计与 JSON 紧密相连, 但它增加了二进制数据类型, 如 JSON 缺乏的日期和二进制数据.

  • 与 MessagePack 一样, BSON 不包含schema信息.


这些设计和编码上的差异导致了二进制表示法的不同:



  • Avro 包含模式信息并具有自描述性, 这导致二进制大小稍大, 但提供了schema兼容性.

  • MessagePack 因其可变长度编码而高度紧凑, 但缺乏模式信息, 因此适用于已知模式的情况.

  • BSON 与 JSON 关系密切, 包含类型信息, 与 MessagePack 等纯二进制格式相比, 会增加大小.


总之, 这些差异源于每种格式的设计目标和功能. Avro 优先考虑schema兼容性, MessagePack 注重紧凑性, 而 BSON 则在保持类似 JSON 结构的同时增加了二进制类型. 格式的选择取决于具体的使用情况和要求, 如schema兼容性, 数据大小和易用性.


优化 JSON 性能


JSON 虽然用途广泛, 在Web开发中被广泛采用, 但在速度方面也存在挑战. 这种格式的人类可读性会导致数据负载较大, 处理时间较慢. 因此, 问题出现了: 我们能够怎样优化JSON以使得它更快更高效? 在本文中, 我们将探讨可用于提高 JSON 性能的实用策略和优化方法, 以确保 JSON 在提供应用所需的速度和效率的同时, 仍然是现代 Web 开发中的重要工具.


以下是一些优化 JSON 性能的实用技巧以及代码示例和最佳实践:


1. 最小化数据大小:



  • 使用简短, 描述性的键名: 选择简洁但有意义的键名, 以减小 JSON 对象的大小.


// Inefficient
{
"customer_name_with_spaces": "John Doe"
}

// Efficient
{
"customerName": "John Doe"
}


  • 尽可能缩写:  在不影响清晰度的情况下, 考虑对键或值使用缩写.


// Inefficient
{
"transaction_type": "purchase"
}

// Efficient
{
"txnType": "purchase"
}

2. 明智地使用数组:



  • 最小化嵌套: 避免深度嵌套数组, 因为它们会增加解析和遍历 JSON 的复杂性.


// Inefficient
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// Efficient
{
"orderItems": ["Product A", "Product B"]
}

3. 优化数字表示:



  • 尽可能使用整数:  如果数值可以用整数表示, 请使用整数而不是浮点数.


// Inefficient
{
"quantity": 1.0
}

// Efficient
{
"quantity": 1
}

4. 消除冗余:



  • 避免重复数据: 通过引用共享值来消除冗余数据.


// Inefficient
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

// Efficient
{
"products": [
{
"name": "Product A",
"price": 10
},
{
"name": "Product B",
"price": 15
}
]
}

5. 使用压缩:



  • 使用压缩算法:  如何可行的话, 使用压缩算法, 比如Gzip 或者Brotli, 以在传输过程中减少JSON负载大小.


// Node.js example using zlib for Gzip compression
const zlib = require('zlib');

const jsonData = {
// Your JSON data here
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// Send compressedData over the network
}
});

6. 采用服务器端缓存:



  • 缓存 JSON 响应:  实施服务器端缓存, 以便高效地存储和提供 JSON 响应, 减少重复数据处理的需要.


7. 剖析与优化:



  • 剖析性能:  使用剖析工具找出 JSON 处理代码中的瓶颈, 然后优化这些部分.



请记住, 你实施的具体优化措施应符合应用的要求和限制.



真实世界的优化: 在实践中加速


在这一部分, 我们将深入探讨现实世界中遇到 JSON 性能瓶颈并成功解决的应用和项目. 我们将探讨企业如何解决 JSON 的局限性, 以及这些优化为其应用带来的切实好处. 从 LinkedIn 和 Auth0 这样的知名平台到 Uber 这样的颠覆性科技巨头*, 这些示例为我们提供了宝贵的见解, 让我们了解在尽可能利用 JSON 的多功能性的同时提高速度和响应能力的策略.


1. LinkedIn集成Protocol Buffers:


挑战: LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加, 从而导致延迟增加.
解决方案: 他们在微服务通信中采用了二进制序列化格式 Protocol Buffers 来取代 JSON.
影响: 这一优化将延迟降低了60%, 提高了 LinkedIn 服务的速度和响应能力.


2. Uber的H3地理索引:



  • 挑战: Uber 使用 JSON 表示各种地理空间数据, 但解析大型数据集的 JSON 会降低其算法的速度.

  • 解决方法 他们引入了H3地理索引, 这是一种用于地理空间数据的高效六边形网格系统, 可减少 JSON 解析开销.

  • 影响: 这一优化大大加快了地理空间操作, 增强了 Uber 的叫车和地图服务.


3. Slack的消息格式优化:



  • 挑战: Slack 需要在实时聊天中传输和呈现大量 JSON 格式的消息, 这导致了性能瓶颈.

  • 解决方法 他们优化了 JSON 结构, 减少了不必要的数据, 只在每条信息中包含必要的信息.

  • 影响: 这一优化提高了消息渲染速度, 改善了 Slack 用户的整体聊天性能.


4. Auth0的Protocal Buffers实现:



  • 挑战: Auth0 是一个流行的身份和访问管理平台, 在处理身份验证和授权数据时面临着 JSON 的性能挑战.

  • 解决方案: 他们采用Protocal Buffers来替代 JSON, 以编解码与身份验证相关的数据.

  • 影响: 这一优化大大提高了数据序列化和反序列化的速度, 从而加快了身份验证流程, 并增强了 Auth0 服务的整体性能.


这些真实案例表明, 通过优化策略解决 JSON 的性能难题, 可对应用的速度, 响应和用户体验产生重大积极影响. 它们强调了在各种应用场景中考虑使用替代数据格式和高效数据结构来克服 JSON 相关缓慢的问题的重要性.


总结一下


在开发领域, JSON 是数据交换不可或缺的通用工具. 其人类可读格式和跨语言兼容性使其成为现代应用的基石. 然而, 正如我们在本文中所探讨的, JSON 的广泛应用并不能使其免于性能挑战.


我们在优化 JSON 性能的过程中获得的主要启示是显而易见的:



  • 性能至关重要: 在当今的数字环境中, 速度和响应速度至关重要. 用户希望应用能够快如闪电, 即使是微小的延迟也会导致不满和机会的丧失.

  • 尺寸至关重要: 数据有效载荷的大小会直接影响网络带宽的使用和响应时间. 减少数据大小通常是优化 JSON 性能的第一步.

  • 替代格式: 当效率和速度至关重要时, 探索其他数据序列化格式, 如Protocal Buffers, MessagePack, BSON 或 Avro.

  • 真实世界案例: 从企业成功解决 JSON 速度变慢问题的实际案例中学习. 这些案例表明, 优化工作可以大幅提高应用的性能.


在继续构建和增强Web应用时, 请记住要考虑 JSON 对性能的影响. 仔细设计数据结构, 选择有意义的键名, 并在必要时探索其他序列化格式. 这样, 你就能确保你的应用在速度和效率方面不仅能满足用户的期望, 而且还能超越用户的期望.


在不断变化的Web开发环境中, 优化 JSON 性能是一项宝贵的技能, 它能让你的项目与众不同, 并确保你的应用在即时数字体验时代茁壮成长.


作者:bytebeats
来源:juejin.cn/post/7299353265099423753
收起阅读 »

切!我又不是第一次没人要🤡

我和你一样都经历过 家里蹲 狗都嫌 的尴尬时期,每天早上起来拿着手机不断刷着招聘软件, 海投几百份还是杳无音讯,在BOSS直拒、前程堪忧、失联招聘、猎空之间反复横跳... 还经历了十分灰暗的阶段,焦虑导致出现躯体化反应(头痛、严重失眠、吃不下东西等) 整夜整夜...
继续阅读 »

thumb-1920-651952.jpg


我和你一样都经历过 家里蹲 狗都嫌 的尴尬时期,每天早上起来拿着手机不断刷着招聘软件,


海投几百份还是杳无音讯,在BOSS直拒前程堪忧失联招聘猎空之间反复横跳...


还经历了十分灰暗的阶段,焦虑导致出现躯体化反应(头痛、严重失眠、吃不下东西等)


整夜整夜睡不着,躺下脑子都是工作、面试、人生选择带来的压力


不想出门社交,害怕面试。


其实,我想跟你说:裸辞并不是终点。


1.裸辞/辞职并不是终点


当我扛着我的键盘收拾东西离开工位,第一次对辞职的 “人走茶凉” 有了实感,


下午六点跟对我很好的前辈们告了别,公司离地铁有点远,和往常不一样,天还没黑,有黄昏相伴


三号线还是这么挤 还有点闷。


v2-7ab9c9ebffb2d15971a4ec2a9b1b2fe3_720w.webp


算起来这是第二次辞职,但第一次辞职找了一个礼拜就顺利入职了
,这次好像有点久,今年大家都在说被裁员大环境差,同学领证成家的也不少。


我意识到人与人的节奏不同,而我好像又一次走到了岔路口,上一次这么慌张还是在高考前


即便我从来没后悔过离职这个决定,但还是会因为面试带来的压力感到局促不安


每次离职就像是一场查漏补缺的大考,对勇气,对储蓄,对知识点的大考


唯有拆迁认亲能打破这场突如其来的考验。。。啊不是。。。我想说:


唯有行动能打破僵局!!


R.png


行动!!!去吃个冰淇淋!!。。。果然有灵感了


363b10d3887191d03f7a462610206e4.jpg


短暂的欢愉后,是与台灯的昼夜相守,与简历的交织缠绵


(简历编。。不是。。写不出来呀!!!)


v2-e4d9f181546345313e5e36f15d2e3cf1_r.jpg


反复改了几版之后确实多了一些“打招呼”的机会,但是实际面试机会还是屈指可数呀,


切!又不是第一次没人要🤡,拒绝我的多的去了,得从巴黎排到广州...


继续努力,等待运气,厚积薄发


v2-4039118d48100a401ec76dd9e4d73405_r.jpg


2.当知识脱离了考试,真理和美丽才慢慢浮现


2.1 心态调整(分享一下最近对我有帮助的书)



  • 《见识》 - 吴军


这是第一年出来工作,遇上了很好的领导送我的书,每当迷茫的时候再拿出来翻翻有了不一样的感悟,很多我们看上去非做不可的事情,其实想通了并没有那么重要,无论在职场上还是在生活中,提高效率都需要从拒绝伪工作开始,有些苦是可以不用吃的,苦难并非造就人类



幸福是目的,成功是手段




  • 《意志力》 - 罗伊·鲍迈斯特


技术行业的人都知道学习是个漫长/终身的事情,跟考公考研短期爆发式集中不同,我们更需要坚持长期一点点做下去,我认识到所有人的意志力都是有限的,使用就会消耗,压力也并非与动力画等号,人也跟机器一样需要“充电”和合理分配,每个人的节奏和身体承受能力也不同。



  • 《被讨厌的勇气》 - 岸见一郎、古贺史健编著


在心情动荡的时期,这本书就像开了一盏加热灯一样在一旁无声陪伴,那会我就像婴儿一样无意识地紧紧抓着自己的头发,直到我睁开眼看见了、意识到了,放下禁锢着工作、生活、交友的课题的手,更能轻松地赶路了。



生活的方式千千万,人生的意义,由我自己决定




  • 《法律常识全知道》 - 李桥


读书的时候没有一门跟社会接轨的课程,毕业了也一直专研技术,导致一毕业不知道劳动合同/租房有什么坑,把仲裁和维权看得过于艰难,法律条例密密麻麻 一时间不知从何下手,这本书就很适合我这种来一线城市打工没什么社会经验的小白,用简单的案例植入“NPC游戏”攻略,和《影响力》这本书加一起简直就是进城防骗指南哈哈哈



免费法律援助电话:12348



2.2 前端学习路线图:


roadmap-完整路线.jpeg


各位摸鱼的小伙伴下次见,这篇便是我的2023年终总结:


裸辞不是终点,唯有行动才能打破僵局,当知识脱离了考试,真理和美丽才慢慢浮现。




参考资料:


前端学习路线


作者:慌张的葡萄
来源:juejin.cn/post/7312304122535133220
收起阅读 »

我和我的4年前端生涯

我是一个普普通通的前端开发工程师,除了我的同事,在这个圈子内没人认识我。“前端开发工程师”,老实讲,我很喜欢这个称呼,我对一些圈外朋友总是这么称呼我自己,当然我的内心和认识我的人都知道,我就是一个“写代码的”。初见在2019年,我大专毕业,那时候的学长学姐总和...
继续阅读 »

我是一个普普通通的前端开发工程师,

除了我的同事,在这个圈子内没人认识我。

“前端开发工程师”,老实讲,我很喜欢这个称呼,我对一些圈外朋友总是这么称呼我自己,当然我的内心和认识我的人都知道,我就是一个“写代码的”。

初见

在2019年,我大专毕业,那时候的学长学姐总和我开玩笑说,以后毕业的我们都会去卖保险,跑销售,那时候的我不以为意,以为只是自我调侃,后来发现人家说的是真的,根本不是开玩笑。我在某大厂做了近3个月的电销,那时候我还是我们班工资最高的人,但不得不说这份职业我根本不喜欢。后来,我大学的班花邓某(真的很漂亮)有一次和我聊天,那阵子她在某大厂做审核。她说她打算去培训前端,问我有没有兴趣。那时候的我觉得计算机啊,编程啊都是精英人群,我这等人也能从事吗?我还打趣的问她,参与前端和高考上清华哪个难?(可见那时候的我对这个想都不敢想)

学习

在19年6月毕业后,我就失业了。我根本想不到大专毕业生能从事什么好的工作,也不清楚我能做些什么。后来我一想,那不妨去培训吧,这样就能和班花做同学,有一起接触的空间和机会了!然后我就开始联系她,但是不得不说,她有强烈的拖延症,她根本对这件事没放在心上。那就自学吧!如果我先学会了,然后等她入门的时候,我就可以教她了!她一定对我刮目相看!嘿嘿!我当时是这样想的没错。我首先找到了培训机构,找他们要到了课程大纲,然后自己逐一的根据大纲上的内容,在网上找视频。不得不说,bilibili大学真的很好用。我的html,css,js都是师承的pink老师,他讲的真的很棒,后来vue就跟着180的科比老师学习,不得不说,这两位的视频对我来说有着举足轻重的作用。那时候第一次学习编程,很多东西都是死记硬背的(切记不要这么做),你很难想象到一个人会去背各种元素的作用以及api。那时候学习的过程还是很快乐的,每天都能接触到新知识,在这就是看着那些视频的进度,想象着看完后自己就能毕业的样子,还是很期待的。甚至说,根据视频模仿了一个很小的功能,然后和自己父母分享的时候,他们都在夸我,现在回想起来,那是我从事前端这么多年来最快乐的时光。

毕业

20年9月的时候,大概经历了近10个月的自学,我决定出山找工作了,我来到了广东。当时我具备的技能有html,css,js,vue2,ts。后来就进入了刷面试题的阶段,有很多面试题完全都是死记硬背的,印象最深刻的就是vue2的响应式原理和nextTick原理,完全是根据百度来的台词去阐述。虽然说现在的我让我自己手写这些都不成问题。不得不说,准备的面试题可以准备,但是项目经验,一些细节可真是苦恼了我。一些对项目,业务比较感兴趣的面试官,我通常都会挂在这里,因为没有任何的工作经验。

我还记得,我面试的第一家公司,是在一个星期天,他们需要找开发小程序的人,最后聊完了技术之后;

面试官:“我看你的简历上写着你上一份工作在武汉有着6k/月,是吗?”

我:“是的”(在这里,我和大多数人一样,包装了,美化了自己的简历)

面试官:“据我所知,不到1年工作经验的专科生,是不可能在武汉有6k/月的,当然这只是我个人的片面的看法,江东自古多才俊嘛”(这就是他的原话,我此生都忘不了)

我内心:“尴尬,脸红,我要逃!快让我走!”

我也还记得,我第一次收获的offer的时候,是一家在CBD,25层高楼的公司,在我毕业以前,我一直都以为能在那里工作的人都是精英,商业大佬。这家公司他让我试岗三天,试岗第一天有个题目是让人手写轮播图和使用element做一个分页功能。我还是通过网友,帮我远程控制我的电脑才完成的这个任务,不过很遗憾,我自己选择了离开这家公司,原因嘛,因为我每天挤不上地铁,再者就是,那家公司气氛感觉很压抑。21年的我还很年轻,有这样的果敢和勇气,如果说24年的我,肯定会忍耐这两点。

第一份工作

2021年3月,在经历过长达3个月的求职经历后,我已经被磨练成了面试机器人。终于找到了一家公司,给我的薪水也很不错,至今我都觉得那家公司是宝藏公司,人数500+,我在那里上班1年没有任何加班经历,需求也非常简单,用vue改改东西,写下静态,一周5天能摸鱼3天。就姑且叫那家公司为X公司吧,还有一个特别搞笑的经历,在X面试的时候,老大问我,“你的项目上有分页查询,这个功能是怎么做的?”,我的回答是:“这个功能是前端做的”。真的很难崩有木有!!!他居然要我了!后来在X的日子,开始过起了神仙般的日子,温饱问题总算是解决了,开始每天接触前端这个圈子,和群友聊天,早上刷各种社交软件。 那时候下班了自己也会回家学习,心里也有一个大牛梦,想着自己能不能做出一些很棒的产品开源出去,就像那些社区圈子的名人一样。还记得那时候跟着视频学习写mini-vue框架,也买了很多课程,觉得投资自己是一笔超值的买卖,然而时至今日那些视频都没有看完。在X公司混到了要转正的那一天,还是蛮忐忑的,结果老大把我单独喊出去谈话,到这里已经开始觉得事情不对了。老大说我的表现还不能满足转正,要继续观察一个月,如果说看到这里,可能很多人会说垃圾公司快跑,但我始终为我当时没有赌气离职感到自豪和开心。

ps:有趣的事,在22年的时候我和带我的女同事都离职了,我们聊天的时候说谈起来这件事,她告诉我,关于我是否能转正的问题,是老大先去问她的意见,问我技术怎么样,她把我夸了一顿。当我知道这个事情的时候,泪目了。

第二份工作

在X公司混了1年左右后,说实在的,我的任务实在是太简单了,上班大多数时间都在摸鱼和自学,没有接触到真正的业务。后来就跑路来到了第二家公司,且叫Y公司吧。在这里我接手的一个项目是从0到1实现的,这正是我想要的,能够完整的学习到项目经验,太妙了。那阵子加班的时间就开始多起来,不过也还挺满意的,能学习到真正的业务。不过这种情况持续了大概半年,新鲜感就退去了,我发现我所做的事情,就是“搬砖”。业务上需要用到什么东西,直接搜一个开源的,然后自己管理一下状态,和后端交互一下,结束了,基本上所有的我参与的业务都是这样的模式,我开始懈怠了,早上每天来了先吃半个小时早饭,然后刷新闻,甚至直到下午才开始做活,因为这些事情实在是太无聊了。在下班后,我开始探索代码的世界,开始着手实现自己刚出道的时候的梦想----实现一个开源产品。那时候的我仿佛就是为了写而去写,我根本不知道要写什么,解决什么样的问题,完全就是照着vue,react这种前端框架去抄,而且实现的非常玩具,没坚持多久就放弃了这个天真的想法。

再见

突然有一天,班花邓某突然联系我了,她告诉我她已经辞职去培训了,问我要不要一起。我得承认,收到她消息的时候小鹿乱撞了,我告诉了她我已经从事前端的工作了,她大为开心,说要我帮帮她,有什么不会的就来问我,我很开心,第一次明白被人需要是什么感受。后来她在培训班毕业了,她让我来面试她,不得不说教她的都是什么妖魔鬼怪,我问她对Vue的理解,她居然背文档说,“vue是一款渐进式框架...”。我问她什么是“渐进式”,她就不知道了。那阵子和她交流了很多,虽然不在一个城市,但还是很开心。另外确实漂亮的女生真的在我们这个行业很有优势,她不到1个月就找到工作,而且她提出期望9k,人家面试官给她10k。

WX20240327-161849@2x.png

JS为我做的三件事

  1. 工作

这第一件事就是让我有了生存的技能,找到了一份工作,首先是能生存下来了。说真的,在我参加工作之后,程序员人数膨胀之前,我一直都认为程序员是普通家庭的专科生一项比较好的出路,但现在的话的来说,我可能也会偏向于劝退专科生的萌新吧,这张图是我专科,4年工作经验,计算机专业的求职经历,如果你能承担像这样的挫折感的话,并且你比较自律,热爱编程,我也鼓励你坚持梦想!

WX20240327-132049@2x.png

其实我目前的状态也很难讲,因为这是一个离奇的经历。我们公司最近的效益不太好,可能是我们部门的产品没能带来什么收益,已经开始裁员了,而且很过分的一件事情是,我和我的部门同事,已经3个月没有发薪水了!这件事超级严重,根据过往的经验和数据的话,大概是要凉凉的节奏。

我们部门一个资历比较老的同事,就带头跑路,然后搞的其他人也就跟风,然后吧,好巧不巧,他们问我跑不跑,我深知专科生找工作无比艰难(请参考上面我的BOOS图),我只能相信公司,会好起来的。

这件事无论说到哪里,大家给我的建议都会是立马跑路,然后去仲裁,可能现在看文章的你也是这么想的,但是我很怕,我怕失去工作,我已经适应了现在的环境,我没办法面对接下来的未知挑战,这和我的个人性格有关系,我决定留下来,在赌一下自己的前程,或者等公司裁掉我,没准还有补偿不是吗?

好巧不巧,我们部门资历老的全跑了,老大让我来当老二了,我听到这个消息的时候是懵逼的,老大给我布置了一系列的任务,包括架构一下前端,招人等事宜,我提道工资呢?老大说会给你发的,公司在等机会。也就是典型的加量不加价,其实今天为什么能来更新这篇文章,也是因为我来架构前端,打算在掘金调研一套eslint规则,因为这里的大佬比较多,技术文章质量也还是很高的。

我想到这里,其实觉得我的平台,舞台也是蛮大的,发挥的机会很多,(虽然没发工资)。于是乎我提出了一系列计划上报了老大,包括技术从Vue2转到react18,前端组件库改用storybook等。除了使用react18这套方案被否决了,其余的也都通过了。

说真的,对这份工作的未来也是有极高的不确定性,如果你不敢承受10%的暴跌,那就会错过1%的大涨,不是吗?

  1. 爱情

我在2023年的时候,在某书上认识了一个江西的女生,我们聊的很投缘,我们甚至是一个大学毕业的,她在武汉上的大学。我们有着共同的爱好,经常聊天就是半天的时间就过去了。而且,我们经常分享自己的一些身边事给对方,一些实事的热点新闻的看法,我们也出奇的一致,她在朋友圈发的照片也很好看,有一次,我给她发消息,她过了一段时间没有回复我,我就开始想着,她在干嘛,为什么还没有回,开始焦急,情绪被左右了,我明白了,我确实喜欢他了。

当从朋友,也可能是网友的关系,变成了爱慕的时候,我对她的态度就变了,我开始变得很有占有欲,开始看她某书下的评论有没有其他男孩子,那阵子的我,真的很丑陋。

我开始着急了,我决定更近一步试探一下,我邀请她一起玩某某荣耀,她也答应的很爽快,那时候我还以为我成功了,有戏!没想到,当天,我上号后,她邀请我,我发现!她居然带了她闺蜜和一个男生,我纯纯无语。。。而且游戏过程中,他们三个互动比较多,基本上我就是路人甲,那晚挺失望的反正。

到了她的生日,我说我送你一个生日礼物吧!她也婉拒了,从那时候开始,我们的距离就变得远了。到今天,2024,3,27号,我们大概40多天没说过话了。

到这里,和js有什么关系呢?

我即将告诉你我人生中,做的最小丑的决定

我想的是,女生都比较幕强,我要给她展现一个技术大佬的人设!没错,在某个夜晚我是这么想的,晚上下班后,我上某里云租了一个最便宜的服务器,然后用express + react.js(没错,我个人更喜欢react,这也是我为什么尝试说服领导转react的原因,hhh)花了一晚上时间,写了一个聊天室功能,还内置了许多她喜欢的东西作为背景,icon,logo,然后我部署在了服务器上。

第二天,我立马把域名分享给她,期待她的回复!还俏皮的说,能打败微信吗?我以为这样,她会对我五体投地的佩服,结果她只回了一个 “6”,然后补充“哈哈哈哈哈哈”

那时候我觉得,她是对这种技术不感冒,她只对迪士尼和追星有兴趣,她每周都会去迪士尼和一只小狐狸合影,请注意是每周。而且她会追每一个在她城市开演唱会的人,某杰,某谦,某棋。我以为她是喜欢这些,后来我才发现,她其实是对我没兴趣而已,意识到我越界了,有意疏远我。

  1. 开源,

我用js尝试写过很多开源,也想成为一个为社区有过贡献的人,但后来发现根本没什么用,自己无论写什么,都已经有了前人的库,比我好,比我成熟,比我火。我最近一直在思考,如果我把学习这些,折腾这些的时间用来学习一门其他的事情,比如吉他,或者打游戏,我会不会更快乐?都说1-3年的前端交流群里都在聊技术,4-5年都在聊业务,5年以上的都在聊家庭和钓鱼,看得出,这就是现实,极少有人能从技术中脱颖而出,我也认清了自己。


作者:soloDog
来源:juejin.cn/post/7345760019319373864
收起阅读 »

Android渠道包自动更新

一、背景 转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题: (1)Android渠道包提交应用市场审核,工作重复&人工成本高   (2)公司目前存在多个APP、需...
继续阅读 »

一、背景


转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题:


(1)Android渠道包提交应用市场审核,工作重复&人工成本高  


(2)公司目前存在多个APP、需更多人支持,有培训成本


(3)每次发版需要人工通知项目成员渠道包审核进度 


  针对以上问题,我们设计开发了渠道包自动更新后台,用来解决渠道更新的效率问题。


二、方案调研


1、基于业务现状,做了技术调研和逻辑抽象


  不同APP支持的渠道不同,不同渠道更包api不同,如下图:


图片


针对以上调研结果,我们将通用的逻辑统一封装开发,将差异点进行配置,做到灵活配置可扩展。


2、整体的实现方案演变


初期方案,每个应用市场单独提审(需要先选择物料,选好物料后上传包文件,文件上传成功后再点击提交审核),多个应用市场需要重复该操作。


图片


上线运行了一段时间后,发现存在一些问题:单个市场提交步骤繁琐、多个应用市场需要分开多次提交。这些步骤是重复且可简化的,因此我们又对提审的过程做了封装,提供批量上传的入口,简化交互过程,做到一键提审。以下是当前运行的第二版方案:


图片


第二版方案上线后,提审同学只需要在入口处选择要更新的应用市场,然后一键上传全部物料,再点击提审按钮即可提审成功。代码内部会处理具体的逻辑,比如:根据配置规则将物料匹配到对应市场、自动匹配包文件进行提审。


三、方案设计


自动上传包含以下核心模块:



  • APP管理:支持配置多个APP信息,包括转转、找靓机、采货侠等

  • 包管理:支持下载不同渠道,不同版本的包

  • 物料管理:包括历史物料的选择,和新增物料的存储(icon、市场截图)

  • 提交审核:包括包下载、物料下载,支持按照APP配置账号密码提交审核

  • 消息提醒:对提交的结果和审核的结果进行消息通知


图片


实现效果:


提审前信息确认,选择APP,可选择单个或者多个渠道,系统自动选择包地址,用户选择物料后可一键提审多应用市场。操作简单便捷,使用成本低


图片


提审后发送消息通知,便于各方了解渠道的审核结果,对审核异常信息进行及时干预。同时自动存储不同版本的审核记录,方便后续分析。


图片


四、总结


渠道包自动更新功能,节省了大量的提交审核人力成本,打通了Android整体的持续交付过程,降低了人工学习成本。之后我们也会针对各种体验问题进行不断的改进和更新~



作者:转转技术团队
来源:juejin.cn/post/7238917620850147383
收起阅读 »

两个Kotlin优化小技巧,你绝对用的上

大家好,本篇文章仍然聊聊kotlin官方做的一些优化工作,主要包括以下三个方面: 数据对象data object的支持 @Repeatable注解的优化 接下来就带大家介绍下上面三个特性。 一. 数据对象data object的支持 该特性由kotlin1...
继续阅读 »

大家好,本篇文章仍然聊聊kotlin官方做的一些优化工作,主要包括以下三个方面:



  1. 数据对象data object的支持

  2. @Repeatable注解的优化


接下来就带大家介绍下上面三个特性。


一. 数据对象data object的支持


该特性由kotlin1.7.20插件版本提供,并处于实验阶段。



这个特性主要是和原来的object声明的单例类的toString()方法输出有关,在了解这个特性之前,我们先看下下面一个例子:


object Single1

fun main() {
println(Single1)
}

输出:



这个输出本质上就是一个类名、@、地址的拼接,有时候你想要打印输出的仅仅是类名,就得需要重写下toString()方法:


object Single1 {

override fun toString(): String {
return "Single1"
}
}

然后再看一个密封类的例子:


sealed interface Response {

data class Success(val response: String): Response

data class Fail(val error: String): Response

object Loading : Response

}

fun main() {
println(Response.Success("{code: 200}"))
println(Response.Fail("no net"))
println(Response.Loading)
}

输出:



可以看到,大家都是密封子类,但就这个Loading类的输出比较"丑陋",没有上面两个兄弟类的输出简洁清爽。


接下来我们就要介绍下主人公数据对象data object了,这个东西其实使用起来和object一模一样,核心的区别就是前者的toString() 更加简洁。


接下来从一个例子一探究竟:


data object Single2

fun main() {
println(Single2)
}

看下输出:



输出是不是比上面的object Single1更加简单明了。最重要的是在密封类中使用效果更加,我们把上面密封类Loading声明为data object


    data object Loading : Response

看下最终的输出结果:



这下子输出结果是不是清爽更多!!


讲完了应用,我们再java的角度看下其背后的实现机制,相比较于objectdata object会多了下面这三个重写方法:


public final class Single2 {

@NotNull
public String toString() {
return "Single2";
}

public int hashCode() {
return -535782198;
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (!(var1 instanceof Single2)) {
return false;
}

Single2 var2 = (Single2)var1;
}

return true;
}
}

我们需要关心的toString()方法就是直接重写返回了当前的类名。


如果想要使用这个特性,我们只需要增加如下配置即可:


compileKotlin.kotlinOptions {
languageVersion = "1.9"
}

二. @Repeatable注解优化


该特性由kotlin1.6.0插件版本提供优化。



在了解这个特性之前,我们先回忆下@Repeatable这个注解在java中的使用:


如果一个注解在某个方法、类等等上面需要重复使用,那就需要@Repeatable帮助。



  • 首先定义需要重复使用的注解


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Fruits.class)
public @interface Fruit {
String name();
String color();
}


  • 然后定义注解容器,用来指定可重复使用的注解类型


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Fruits {
Fruit[] value();
}

然后就可以在代码中这样使用:


@Fruits({
@Fruit(name = "apple", color = "red"),
@Fruit(name = "origin", color = "yellow"),
})

public class Detail {
}

大家有没有发现,可重复注解定义起来还是由一丢丢的麻烦,接下来轮到我们kotlin重磅出击了。先看下面一个例子:


@Repeatable 
annotation class Animal(val name: String)

在kotlin中我们只要声明一个需要重复使用的注解即可,kotlin编译器会自动帮助我们生成注解容器@Animal.Container,然后我们就能在代码中这样使用:


@Animal(name = "dog")
@Animal(name = "horse")
public class Detail {
}

是不是非常简单便捷了。


如果你偏要显示指明一个包含注解,也可以,通过以下方式即可实现:


@JvmRepeatable(Animals::class)
annotation class Animal(val name: String)

annotation class Animals(val value: Array)

然后除了上面的使用方式,你在kotlin中还可以这样使用:


@Animals([Animal(name = "dog"), Animal(name = "dog")])
class Detail {
}

请注意:



  1. 如果非要显示声明一个注解容器,其属性的名称一定要为value

  2. 其次,注解容器和可重复性直接不能同时声明在同一个元素上;


另外,其实这个特性kotlin早就支持了,只不过kotlin1.6.0插件版本之前,kotlin这个特性只只支持RetentionPolicy.SOURCE生命周期的注解,并且还和java的可重复注解不兼容。


总结


这两个小技巧相信在大家日常开发中还是比较实用的,希望本篇能对你有所帮助。


参考文章:


Improved string representations for singletons and sealed class hierarchies with data objects


Repeatable annotations with runtime retention for 1.8 JVM target




作者:长安皈故里
来源:juejin.cn/post/7248249730478784569
收起阅读 »

Android Room 数据库的坑

1.Android Room 数据库的坑 在用Room数据库的时候 发现有需要一个字段的条件合到一起去写这个SQL @Query("SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= :Time " + ...
继续阅读 »

1.Android Room 数据库的坑


在用Room数据库的时候 发现有需要一个字段的条件合到一起去写这个SQL


 @Query("SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= :Time " +
"
and 字段B >= :Time and 字段C <= :BTime and 字段C >= :BTime " +
"
and '(' || 字段D is null or 字段D = '' || ')'")

List
selectList(String Time, String BTime);


这里面的 “ ||” 是Room里面独特的表达方式 是替代了java里面的“+”号


正常在android中 使用 是这样的


String sql = "SELECT * FROM 表名 WHERE 字段A = '1' and 字段B <= "+传入的参数+" " +
"and 字段B >= "+传入的参数+" and 字段C <= "+传入的参数+" and 字段c >= "+传入的参数+" " +
"and '(' "+" 字段D is null or 字段D = '' "+" ')'"


cursor = db.rawQuery(sql,
null);

而在Room 中 用 “||” 代替了 “+”


2.Android Room 查询语句的坑


@Query("SELECT * FROM 表名 WHERE 字段A = '0' order by id desc")
List
selectList();

假如你正在查询一张表的面的内容,然后忽然跑出来一个异常



# [Android RoomDatabase Cruash "Cursor window allocation of 4194304 bytes failed"](https://stackoverflow.com/questions/75456123/android-roomdatabase-cruash-cursor-window-allocation-of-4194304-bytes-failed)

奔溃日志:


android.database.CursorWindowAllocationException: Could not allocate CursorWindow '/data/user/0/cn.xxx.xxx/databases/xxx.db' of size 2097152 due to error -13.
at android.database.CursorWindow.nativeCreate(Native Method)
at android.database.CursorWindow.(CursorWindow.java:139)
at android.database.CursorWindow.(CursorWindow.java:120)
at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:202)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:147)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:140)
at yd.d.m(SourceFile:21)
at cn.xxx.control.y.y0(SourceFile:1)
at e5.y.p(SourceFile:230)
at e5.y.l(SourceFile:1)
at e5.y.E(SourceFile:1)
at cn.xxx.cluster.classin.viewmodel.SessionViewModel$d.invokeSuspend(SourceFile:42)

触发原因



  • Room 对应的 Sqlite 数据库,其对 CursorWindows 分配的大小是有限制的,最大为 2M,超过之后会发生上述崩溃闪退现象(偶现且难以复现的 bug


解决方法


需要业务方梳理这块的业务,优化数据库的调用,如果明确知道在一个方法里面会调用多个数据库的方法,需要让 controller 提供新的方法,且这个 controller 层的方法需要添加 @Transaction 进行注解,从而保证在同一个事物内进行数据库操作,以此避免 CursorWindows 大小超过 2M


那么问题来了 @Transaction 这个注解是干嘛的呢


翻译 事务的意思


@Transaction
@Query("SELECT * FROM 表名 WHERE 字段A = '0' order by id desc")
List selectList();

接着 问题完美解决


作者:笨qiao先飞
来源:juejin.cn/post/7273674981959745593
收起阅读 »

告别轮询,SSE 流式传输可太香了!

今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。 对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。 接下来,我将从 SSE ...
继续阅读 »

今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。


对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。


接下来,我将从 SSE 的概念、与 Websocket 对比、SSE 应用场景多个方面介绍 SSE 流式传输,感兴趣的同学一起来了解下吧!


什么是 SSE 流式传输


SSE 全称为 Server-sent events , 是一种基于 HTTP 协议的通信技术,允许服务器主动向客户端(通常是Web浏览器)发送更新。


它是 HTML5 标准的一部分,设计初衷是用来建立一个单向的服务器到客户端连接,使得服务器可以实时地向客户端发送数据。


这种服务端实时向客户端发送数据的传输方式,其实就是流式传输。


我们在与 ChatGPT 交互时,可以发现 ChatGPT 的响应总是间断完成。细扒 ChatGPT 的网络传输模式,可以发现,用的也是流式传输。


图片


SSE 流式传输的好处


在 SSE 技术出现之前,我们习惯把需要等待服务端返回的过程称为长轮询。


长轮询的实现其实也是借助 http 请求来完成,一个完整的长轮询过程如下图所示:


图片


从图中可以发现,长轮询最大的弊端是当服务端响应请求之前,客户端发送的所有请求都不会被受理。并且服务端发送响应的前提是客户端发起请求。


前后端通信过程中,我们常采用 ajax 、axios 来异步获取结果,这个过程,其实也是长轮询的过程。


而同为采用 http 协议通信方式的 SSE 流式传输,相比于长轮询模式来说,优势在于可以在不需要客户端介入的情况下,多次向客户端发送响应,直至客户端关闭连接。


这对于需要服务端实时推送内容至客户端的场景可方便太多了!


SSE 技术原理


1. 参数设置

前文说到,SSE 本质是一个基于 http 协议的通信技术。


因此想要使用 SSE 技术构建需要服务器实时推送信息到客户端的连接,只需要将传统的 http 响应头的 contentType 设置为 text/event-stream 。


并且为了保证客户端展示的是最新数据,需要将 Cache-Control 设置为 no-cache 。


在此基础上,SSE 本质是一个 TCP 连接,因此为了保证 SSE 的持续开启,需要将 Connection 设置为 keep-alive 。


Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

完成了上述响应头的设置后,我们可以编写一个基于 SSE 流式传输的简单 Demo 。


2. SSE Demo

服务端代码:


const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.static('public'));

app.get('/events'function(req, res) {
    res.setHeader('Content-Type''text/event-stream');
    res.setHeader('Cache-Control''no-cache');
    res.setHeader('Connection''keep-alive');

    let startTime = Date.now();

    const sendEvent = () => {
        // 检查是否已经发送了10秒
        if (Date.now() - startTime >= 10000) {
            res.write('event: close\ndata: {}\n\n'); // 发送一个特殊事件通知客户端关闭
            res.end(); // 关闭连接
            return;
        }

        const data = { message'Hello World'timestampnew Date() };
        res.write(`data: ${JSON.stringify(data)}\n\n`);

        // 每隔2秒发送一次消息
        setTimeout(sendEvent, 2000);
    };

    sendEvent();
});

app.listen(PORT() => {
    console.log(`Server running on http://localhost:${PORT}`);
});

客户端代码:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>SSE Example</title>
</head>

<body>
    <h1>Server-Sent Events Example</h1>
    <div id="messages"></div>

    <script>
        const evtSource = new EventSource('/events');
        const messages = document.getElementById('messages');

        evtSource.onmessage = function(event) {
            const newElement = document.createElement("p");
            const eventObject = JSON.parse(event.data);
            newElement.textContent = "Message: " + eventObject.message + " at " + eventObject.timestamp;
            messages.appendChild(newElement);
        };
    
</script>
</body>
</html>

当我们在浏览器中访问运行在 localhost: 3000 端口的客户端页面时,页面将会以 流式模式 逐步渲染服务端返回的结果:


图片


需要注意的是,为了保证使用 SSE 通信协议传输的数据能被客户端正确的接收,服务端和客户端在发送数据和接收数据应该遵循以下规范:


服务端基本响应格式

SSE 响应主要由一系列以两个换行符分隔的事件组成。每个事件可以包含以下字段:


data:事件的数据。如果数据跨越多行,每行都应该以data:开始。
id:事件的唯一标识符。客户端可以使用这个ID来恢复事件流。
event:自定义事件类型。客户端可以根据不同的事件类型来执行不同的操作。
retry:建议的重新连接时间(毫秒)。如果连接中断,客户端将等待这段时间后尝试重新连接。

字段之间用单个换行符分隔,而事件之间用两个换行符分隔。


客户端处理格式

客户端使用 EventSource 接口监听 SSE 消息:


const evtSource = new EventSource('path/to/sse');
evtSource.onmessage = function(event) {
    console.log(event.data); // 处理收到的数据
};

SSE 应用场景


SSE 作为基于 http 协议由服务端向客户端单向推送消息的通信技术,对于需要服务端主动推送消息的场景来说,是非常适合的:


图片


SSE 兼容性


图片


可以发现,除了 IE 和低版本的主流浏览器,目前市面上绝大多数浏览器都支持 SSE 通信。


SSE 与 WebSocket 对比


看完 SSE 的使用方式后,细心的同学应该发现了:


SSE 的通信方式和 WebSocket 很像啊,而且 WebSocket 还支持双向通信,为什么不直接使用 WebSocket ?


下表展示了两者之间的对比:


特性/因素SSEWebSockets
协议基于HTTP,使用标准HTTP连接单独的协议(ws:// 或 wss://),需要握手升级
通信方式单向通信(服务器到客户端)全双工通信
数据格式文本(UTF-8编码)文本或二进制
重连机制浏览器自动重连需要手动实现重连机制
实时性高(适合频繁更新的场景)非常高(适合高度交互的实时应用)
浏览器支持良好(大多数现代浏览器支持)非常好(几乎所有现代浏览器支持)
适用场景实时通知、新闻feed、股票价格等需要从服务器推送到客户端的场景在线游戏、聊天应用、实时交互应用
复杂性较低,易于实现和维护较高,需要处理连接的建立、维护和断开
兼容性和可用性基于HTTP,更容易通过各种中间件和防火墙可能需要配置服务器和网络设备以支持WebSocket
服务器负载适合较低频率的数据更新适合高频率消息和高度交互的场景

可以发现,SSE 与 WebSocket 各有优缺点,对于需要客户端与服务端高频交互的场景,WebSocket 确实更适合;但对于只需要服务端单向数据传输的场景,SSE 确实能耗更低,且不需要客户端感知


参考文档


developer.mozilla.org/zh-CN/docs/…


作者:veneno
来源:juejin.cn/post/7355666189475954725
收起阅读 »

Android 如何统一处理登录后携带数据跳转到目标页面

需求场景 我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法: 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验...
继续阅读 »

需求场景


我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法:



  1. 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验不是很好。

  2. 点击购买跳转到登录,登录完成直接跳转到下单支付页面。


第一种我们就不谈了产品经理不同意🐶。第二种我们一般是在 onActivityResult 里面获取到登录成功,然后根据 code 跳转到目标页面。这种方式缺点就是我们要在每个页面都处理相同的逻辑还有定义各种 code,如果应用里面很多这种场景也太繁琐了。那有没有统一的方式去处理这种场景就是我们今天的主题了。


封装方式


我们的应用是组件化的,APP 的页面跳转使用了 Arouter。所以我们统一处理使用 Arouter 封装。直接上代码


fun checkLoginToTarget(postcard: Postcard) {//Postcard 是 Arouter 的类
if (User.isLogin()) {
postcard.navigation()
} else {
//不能使用 postcard 切换 path 直接跳转,因为 group 可能不同,所以重新 build
ARouter.getInstance().build(Constant.LOGIN)
.with(postcard.extras)//获取携带的参数重新转入
.withString(Constant.TAGACTIVIFY, postcard.path)//添加目标路由
.navigation()
}
}

//登录成功后在登录页面执行这个方法
fun loginSuccess() {
val intent= intent
val target = intent.getStringExtra(Constant.TAGACTIVIFY)//获取目标路由
target?.apply {
if (isNotEmpty()){
val build = ARouter.getInstance().build(this)
val extras = intent.extras//获取携带的参数
if (extras != null) {
build.with(extras)
}
build.navigation()
}
}
finish()
}

代码加了注释,使用 Kotlin 封装了顶层函数,登录页面在登录成功后跳转到目标页面,针对上面的场景直接调用 checkLoginToTarget 方法。


checkLoginToTarget(ARouter.getInstance().build(Constant.PAY_PAGE).withInt(Constant.GOOD_ID,id))

通过 Arouter 传入下单支付的路由地址,并且携带了商品的 ID,生成了 Postcard 参数。登录成功后能带着商品 ID
直接下单支付了。


最后


如果项目里没有使用路由库可以使用 Intent 封装实现,或者别的路由库也可以用上面的方式去做统一处理。


作者:shortybin
来源:juejin.cn/post/7237386183612530749
收起阅读 »

别做老实人了!这才是 HR 喜欢听的离职原因!

哈喽,大家好 我是 xy👨🏻‍💻。今天我们来聊聊一个面试时超级敏感但又不得不面对的问题——离职原因💢 找工作面试被问离职原因,千万不要直接说出你为什么从上家公司离职的真实原因!!! 当老实人要吃亏的!!! 其实,大部分真实的离职原因都是每家公司或多或少会遇到...
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。今天我们来聊聊一个面试时超级敏感但又不得不面对的问题——离职原因💢



找工作面试被问离职原因,千万不要直接说出你为什么从上家公司离职的真实原因!!!


老实人要吃亏的!!!


其实,大部分真实的离职原因都是每家公司或多或少会遇到的,但如果直接说出来,很可能会让面试官担心你入职后也会因为同样的原因离职哦!💢


😉 所以,作为一个聪明的求职者,我们应该如何回答这个问题呢?


6 个常见的离职原因


首先,让我们来看看这 6 个常见的离职原因❌,以及它们的“禁忌”回答方式吧!🚫


🎯 工资太低

🚫 禁忌回答:因为前公司给的工资太低了,我实在是受不了了。



🎯 加班严重

🚫 禁忌回答:前公司加班太严重了,我身体都垮了。



🎯 领导傻 X

🚫 禁忌回答:前领导简直就是个傻 X,根本不懂管理。



🎯 被裁员

🚫 禁忌回答:我之前的公司裁员,我被无辜波及了。



🎯 无法升职

🚫 禁忌回答:我在前公司干了几年,但一直升不上去。



🎯 同事相处不和谐

🚫 禁忌回答:前公司的同事们都太奇葩了,我实在受不了。



👀 看到了吗?这些回答都是大忌,因为它们会让 HR 觉得你不稳定、有负面情绪、甚至可能是个问题员工。那么,正确的回答方式是什么呢?别急,我们接着往下看!✅


🎯 工资太低

✅ 回答模板:HR 您好!我过去 2 年,在公司和领导的帮助下,我在技术和能力方面有了很大提升,为公司开发的各种工具和库,极大地提高了项目开发效率,领导对我很认可,但公司的薪资结构有一些硬指标,无法满足我的涨薪需求,所以想看看市场上的新机会,寻找一个可以长期稳走发展的平台。



🎯 加班严重

✅ 回答模板:HR 您好!在上家公司,我的工作效率较高,能按时保质保量地完成工作,但前司会定期统计加班时长,并以此进行考核,导致大部分人为了加班而加班,效率非常低,我个人并不反对加班,但这种低效的常态化 996,我并不认可,我希望能在更人性化管理的公司里发挥优势。



🎯 领导傻 X(这个有点难,但我们可以尽量委婉)

✅ 回答模板:HR 您好!因为前公司的工作职贵划分比较固定,每个人负责的业务比较少,对于所负责的开发工作,我已经没有什么新知识可以学的了,我希望职业生涯是一个持续的学习和提升的过程,并且在自己具备了相关业务技能经验后,也想迎接一些新的挑战。



🎯 被裁员

✅ 回答模板:HR 您好!前公司当时在进行业务调整,公司想让我调到其他业务线上,岗位职责也发生了变化,我觉得自己还是很擅长做前端工具链方向的开发,并且也在这块积累了几年经验,还是想继续在这个方向上深耕,因此出来看看机会。



🎯 无法升职

✅ 回答模板:HR 您好!前公司管理比较扁平,虽然一直被领导重用,也负责过多个核心业务,但是职级几年来来一直没有变化。领导也争取过,但由于工作年限等原因,没能通过。我不想安于现状,想找一个更能发挥自己才能的平台。



🎯 同事相处不和谐

✅ 回答模板:HR 您好!我之前在前司能力不错,取得了不错的成绩,但由于分工不太合理,出现了很多扯皮现象和低效沟通,影响到了工作效率,有时甚至会影响整个项目的进度,内耗较大,所以决定离开,希望找一个氛国好团队长期发展。



💡 记住了吗?面试时回答离职原因的关键在于:不要贬低前司和前同事不要让 HR 觉得是你的问题也不要吐槽前司的任何不足。掌握这些要领,相信你在面试中一定能够游刃有余地应对这个问题啦!




作者:前端开发爱好者
来源:juejin.cn/post/7351321275975843867
收起阅读 »

我改进了数据滚动方式!老板直接加薪

web
需求背景 前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。 我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。 我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下...
继续阅读 »

需求背景


前几天,甲方提了个需求,想让下面的数据循环展示,准备放在他们集团首页给他们领导演示用。



我们领导很重视这个事儿,拍了拍我,语重心长的说,小伙子,好好做。


我啪的一下就兴奋了,老板居然如此器重我,我必当鞠躬尽瘁,减少摸鱼,我要让老板拜倒在精湛的技术下!


于是,我搬出自己的库存代码,仅2min就实现了数据的滚动:


没错,我直接照搬了自己以前写过的文章:JS实现可滚动区域自动滚动展示 - 掘金


就在我准备告诉老板我做完了的时候,我突然想了想,这么快做完,老板一定觉得我没好好做,我以后还怎么升职加薪,赢取白富美?


于是,我连夜研究,终于改进了数据滚动方式,赢得了老板的大饼(以后涨500)。最终效果:



技术方案


技术选型


观察最终效果图,可以发现这其实就是一个数据循环滚动的效果,每条内容之间间隔1000ms,每条出现动的时间为500ms。用术语来说,这就是一个单步停顿滚动效果。


我百度了一下,社区还是有这个实现的现成方案的:vue-seamless-scroll,周下载也还行。



于是,我果断试了试,结果不知道什么原因,并不生效...


既然如此,直接手写一个吧!


实现思路


要实现上述效果其实很简单,如图



我们创造一个含有六个值的数组,每隔一段时间循环更改黄色区域的数据,当黄色区域数据变成最新的时候,红色区域整体向下移动,当有数值超出滚动区域后,在删除这个数据即可。


数据更新


如果不考虑动画,我们的代码应该这么写


<template>
<div class="item-wrap" v-for="(item, index) in animationData">
<!-- 模块内容 -->
</div>

</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// *定时器
const animationTimerMeta: any = {
timer: null,
// 这个函数负责设置轮播数据的更新逻辑。
timeFuc() {
let setTimeoutId: any = null
if (this.timer) return
this.timer = setInterval(() => {
// 取轮播数据的第一条id
let firstId = animationData.value[0].id
// 为轮播数据添加最新的第一项数据
let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
animationData.value.unshift(allCarouseData.value[addIndex])
setTimeout(() => {
// 删除数组的最后一项
animationData.value.pop()
}, 1000)

}, 1500)
}
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>

上述代码的主要功能是:



  1. 从 allCarouseData 中取出最后5个元素作为初始的轮播数据。

  2. 每1.5秒更新一次轮播数据,具体逻辑是:移除当前 animationData 的第一个元素,并从 allCarouseData 中取出前一个元素(如果已经是第一个元素,则取最后一个)添加到 animationData 的开头。

  3. 每1秒从 animationData 的末尾移除一个元素。


上述代码没有实现动画,他的效果是这样的:



动画添加


<template>
<div class="item-wrap" v-for="(item, index) in animationData"
:class="[{ moveToBottom: animationActive }, { show: animationActive && index === 0 }]"
>

<!-- 模块内容 -->
</div>

</template>
<script setup lang="ts">
// #假设这是接口请求的10条最新数据
const allCarouseData = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// #需要轮播的数据
const animationData = ref<any>([])
// #是否开启动画
const animationActive = ref(false)
// *定时器
const animationTimerMeta: any = {
timer: null,
// 这个函数负责设置轮播数据的更新逻辑。
timeFuc() {
let setTimeoutId: any = null
if (this.timer) return
this.timer = setInterval(() => {
// 取轮播数据的第一条id
let firstId = animationData.value[0].id
// 为轮播数据添加最新的第一项数据
let index = allCarouseData.value.findIndex((res: any) => res.id === firstId)
let addIndex = index - 1 < 0 ? allCarouseData.value.length - 1 : index - 1
animationData.value.unshift(allCarouseData.value[addIndex])
setTimeout(() => {
// 删除数组的最后一项
animationData.value.pop()
}, 1000)

}, 1500)
}
}
animationData.value = allCarouseData.value.slice(-5)
animationTimerMeta.timeFuc()
</script>


@keyframes moveToBottom {
0% {
transform: translateY(-47px);
}

100% {
transform: translateY(0);
}
}

.moveToBottom {
animation: moveToBottom 500ms ease-in-out forwards;
}

@keyframes fadeInFromTop {
0% {
opacity: 0;
transform: translateY(-47px);
}

100% {
opacity: 1;
transform: translateY(0);
color: #683BD6;
}
}

.show {
animation: fadeInFromTop 500ms ease-in-out forwards;
}

上述代码中,为了实现动画效果,采用了动态添加类名的技术方案。


animationData 数组中的元素会按照一定顺序进行显示和隐藏,同时伴随有动画效果。当第一个元素进入视图时,它会应用 fadeInFromTop 动画;其他元素会应用 moveToBottom 动画。通过定时器,元素会定期从 allCarouseData 中获取新的数据并更新 animationData。


代码释义:



  • moveToBottom: 当 animationActive 为真值时,此类名会被添加到 div 上。

  • show: 当 animationActive 为真值且当前元素是数组的第一个元素时,此类名会被添加到 div 上。


CSS 释义:



  • moveToBottom 动画:


定义一个名为 moveToBottom 的关键帧动画,使元素从上方移动到其原始位置。


moveToBottom 类将此动画应用到元素上。



  • fadeInFromTop 动画:


定义一个名为 fadeInFromTop 的关键帧动画,使元素从上方淡入并改变颜色。


show 类将此动画应用到元素上。


通过上述简单的实现方式,就能最终实现我们想要的效果



相比于普通滚动,这种方式看起来要好很多!


结语


要想实现这种单步停帧的效果,其实有很多实现方式,这只是笔者实现的一种,核心逻辑就是动态改变数据、增添类名。如果大家还有更好的方式,也欢迎大家指点。


作者:石小石Orz
来源:juejin.cn/post/7348433631944556555
收起阅读 »

只写后台管理的前端要怎么提升自己

web
本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。 写优雅的代码 一道面试题 大概两年以前,面试美团的时候...
继续阅读 »

本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过(虽然我现在已经心安理得的摆烂),但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。


写优雅的代码


一道面试题


大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。


原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb,而我要展示成 KBMB 等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):


function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;

while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}

return `${kb.toFixed(2)} ${units[unitIndex]}`;
}

而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:


function formatSizeUnits(kb) {
var result = '';

if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}

return result;
}

虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。


如何提升代码质量


想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。


还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。


还是上面的问题,看看 GPT 给的答案


// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。

/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/

function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);

// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}

// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);

// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}

// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB

还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)


我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。


学会封装


一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?


你说,没时间,没必要,复制粘贴反而更快。


那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。


而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。


关注业务


对于前端业务重要吗?


相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。


但是就我找工作的经验,业务非常重要!


如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。


一场面试


还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。



  • 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”

  • 我:“好嘞!”


等到面试的时候:



  • 前端ld:“你知道xxx吗?(业务名词)”

  • 我:“我……”

  • 前端ld:“那xxxx呢?(业务名词)”

  • 我:“不……”

  • 前端ld:“那xxxxx呢??(业务名词)”

  • 我:“造……”


然后我就挂了………………


如何了解业务



  1. 每次接需求的时候,都要了解需求背景,并主动去理解


    我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么 cluster controller topic broker partition…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。


  2. 每次做完一个需求,都需要了解结果


    有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?


  3. 理解需求,并主动去优化


    产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?


    产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。


    其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。



关注源码


说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。


除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。


那说什么,后台管理就这些啊?!


如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?


可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点


至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?



讲一下 Axios 源码中,拦截器是怎么实现的?


Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。


在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含 fulfilledrejected 函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。


以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:


class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}

use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}

eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}

forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}

在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过 forEach 方法将拦截器中的 fulfilledrejected 函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。


axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的 .then.catch 执行之前,插入自定义的逻辑。


请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。



前端基建


当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。


技术选型


技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表达配置化方案,比如Formily?


对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……


image.png

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)


Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。


React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。


总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。


开发规范


这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlintstylelintprettiercommitlint 等。


前端监控


干了这么多年前端,前端监控我是……一点没做过。


image.png

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。


对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。


对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerrorwindow.addEventListener('unhandledrejection', ...) 去分别捕获同步和异步错误,然后通过错误信息和 sourceMap 来定位到源码。


对于性能监控,我们可以通过 window.performancePerformanceObserver 等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。


最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon 还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。


CI/CD


持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。


场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。


这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline 、 Stage 和 Job 分别是什么,怎么配置,如何在不同环境配置不同工作流等。


了解技术动态


这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。


比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。


还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……


虽然不可能学完每一项新技术,但是可以多去了解下。


总结


写了这么多,可能有人会问,如果能回到过去,你会怎么做。


啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。


image.png

作者:我不吃饼干
来源:juejin.cn/post/7360528073631318027
收起阅读 »

三方接口不动声色将http改为了https,于是开启了我痛苦的一天

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。 原来是调的一个三方接口报错了: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorExc...
继续阅读 »

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。


原来是调的一个三方接口报错了:


javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alert.createSSLException(Alert.java:131)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:353)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:296)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:291)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:652)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:471)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:367)
at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:376)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
at sun.security.ssl.TransportContext.dispatch(TransportContext.java:183)
at sun.security.ssl.SSLTransport.decode(SSLTransport.java:154)
at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1279)
at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1188)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:401)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:373)

查看原因:



由于JVM默认信任证书不包含该目标网站的SSL证书,导致无法建立有效的信任链接。



奥...原来是他们把接口从http改为了https,导致我们获取数据报错了。再看看他们的证书,奥...新的。


image.png


好了,看看我们的逻辑,这其实是一个获取对方生成的PDF文件的接口


PdfReader pdfReader = new PdfReader(url);

url就是他们给的链接,是这行代码报的错。这时候,开始研究,在网上扒拉,找到了初版方案


尝试1


写一个程序专门获取安全证书,这代码有点长,全贴出来影响阅读。我给扔我hithub上了github.com/lukezhao6/I… 将这个文件贴到本地,执行javac InstallCert.java将其进行编译


image.png


编译完长这样:


image.png
然后再执行java InstallCert http://www.baidu.com (这里我们用百度举例子,实际填写的就是你想要获取证书的目标网站)


image.png
报错不用怕,因为它会去检查目标服务器的证书,如果出现了SSLException,表示证书可能存在问题,这时候会把异常信息打印出来。


在生成的时候需要输入一个1


image.png
这样,我们需要的证书文件就生成好了


image.png


这时候,将它放入我们本地的 jdk的lib\security文件夹内就行了


image.png


重启,这时候访问是没有问题了。阶段性胜利。


但是,但是。一顿操作下来,对于测试环境的docker,还有生产环境貌似不能这么操作。 放这个证书文件比较费事。


那就只能另辟蹊径了。


尝试2


搜到了,还有两种方案。



1.通过System.setProperty("javax.net.ssl.trustStore", "你的jssecacerts证书路径");


2.程序启动命令-Djavax.net.ssl.trustStore=你的jssecacerts证书路径 -Djavax.net.ssl.trustStorePassword=changeit



我尝试了第一种,System.setProperty可以成功,但是读不到文件,权限什么的都是ok的。
检查了蛮多地方



  • 路径格式问题

  • 文件是否存在

  • 文件权限

  • 信任库密码

  • 系统属性优先级


貌似都是没问题的,但肯定又是有问题的,因为没起作用。但是想着这样的接口有4个,万一哪天其他三个也改了,我又得来一遍。所以就算研究出来了,还是不能稳坐钓鱼台。有没有一了百了的方法嘞。


尝试3


还真找到了:这个错是因为对方网站的证书不被java信任么,那咱不校验了,直接全部信任。这样就算其他接口改了,咱也不愁。而且这个就是获取pdf,貌似安全性没那么重。那就开搞。


代码贴在了下方,上边的大概都能看懂吧,下方的我加了注释。


URL console = new URL(url);
HttpURLConnection conn = (HttpURLConnection) console.openConnection();
if (conn instanceof HttpsURLConnection) {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
((HttpsURLConnection) conn).setSSLSocketFactory(sc.getSocketFactory());
((HttpsURLConnection) conn).setHostnameVerifier(new TrustAnyHostnameVerifier());
}
conn.connect();
InputStream inputStream = conn.getInputStream();
PdfReader pdfReader = new PdfReader(inputStream);
inputStream.close();
conn.disconnect();

private static class TrustAnyTrustManager implements X509TrustManager {
//这个方法用于验证客户端的证书。在这里,方法体为空,表示不对客户端提供的证书进行任何验证。
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法用于验证服务器的证书。同样,方法体为空,表示不对服务器提供的证书进行任何验证。
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法返回一个信任的证书数组。在这里,返回空数组,表示不信任任何证书,也就是对所有证书都不做任何信任验证。
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
//这个方法用于验证主机名是否可信。在这里,无论传入的主机名是什么,方法始终返回 true,表示信任任何主机名。这就意味着对于 SSL 连接,不会对主机名进行真实的验证,而是始终接受所有主机名。
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}

解决了解决了,这样改算是个比较不错的方案了吧。


作者:奔跑的毛球
来源:juejin.cn/post/7362587412066893834
收起阅读 »

开发需求记录:实现app任意界面弹框与app置于后台时通知

前言 在产品经理提需求时候提到,app在接收到报警信息时候能不能弹出一个弹框,告诉用户报警信息,这个弹框要在app的任意界面能够弹出,并且用户点击详情时候,会跳转到报警详情界面,查看具体信息,当用户将app至于后台的时候,接收到报警信息,app发送通知,当用户...
继续阅读 »

前言


在产品经理提需求时候提到,app在接收到报警信息时候能不能弹出一个弹框,告诉用户报警信息,这个弹框要在app的任意界面能够弹出,并且用户点击详情时候,会跳转到报警详情界面,查看具体信息,当用户将app至于后台的时候,接收到报警信息,app发送通知,当用户点击通知时候,跳转到报警详情界面。
功能大体总结如上,在实现弹框与通知在跳转界面时遇到一些问题,在此记录一下。效果图如下:


开发需求 - 通知与弹框.gif


功能分析


弹框实现,使用DialogFragment。

前后台判断则是,创建一个继承自ActivityLifecycleCallbacks接口和Application的类,继承ActivityLifecycleCallbacks接口是为了前后台判断,继承Application则是方便在基类BaseActivity获取前后台相关数据。

项目原本采用单Activity多Fragment实现,后面因为添加了视频相关功能,改为了多Activity多Fragment。

原单Activity时候,实现比较容易。后面修改为多Activity,就有些头疼,最终用思路是创建基类BaseActivity,后面添加Activity时都要继承基类BaseActivity。使用基类原因是把相同的功能抽取出来,且若每个Activity都自己实现弹框和通知的话太容易出错,也太容易漏下代码了。


代码实现


弹框


在实现继承自DialogFragment的弹框时,需要在onCreateDialog方法内设置dialog的宽高模式以及背景,不然弹框会有默认的边距,导致显示效果与预期不符,未去边距与去掉边距的弹框效果如下:
image.png
关于onCreateDialog的代码如下:


override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.setContentView(R.layout.custom_dialog_layout)
dialog.window?.apply {
setLayout(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT)
setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
}
dialog.setCancelable(false)
return dialog
}

此外当弹框出现的时候,弹框背景色还会闪烁。这里采用属性值动画设置弹框背景色控件的透明度变换。完整的Dialog代码如下:


class AlarmDialogFragment: DialogFragment() {
private lateinit var binding:CustomDialogLayoutBinding
private var animator:ObjectAnimator? = null

override fun show(manager: FragmentManager, tag: String?) {
try {
super.show(manager, tag)
}catch (e:Exception){
e.printStackTrace()
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGr0up?,
savedInstanceState: Bundle?
)
: View? {
binding = CustomDialogLayoutBinding.inflate(inflater)
return binding.root
}

override fun onStart() {
super.onStart()
binding.viewAlarmDialogBg
startAnimation()
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = Dialog(requireContext())
dialog.setContentView(R.layout.custom_dialog_layout)
dialog.window?.apply {
setLayout(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT)
setBackgroundDrawable(ColorDrawable(Color.parseColor("#88000000")))//去掉DialogFragment的边距
}
dialog.setCancelable(false)
return dialog
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}

override fun onDestroy() {
super.onDestroy()
if(animator?.isStarted == true){
animator?.end()
}
}

private fun initView() {
binding.btnCloseDialog.setOnClickListener {
dismiss()
}

binding.btnDialogNav.setOnClickListener {
if(context is MainActivity){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController().navigate(R.id.alarmDetailFragment,bundle)
}else{
val intent = Intent(context,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
startActivity(intent)
}
dismiss()
}
}

private fun startAnimation() {
animator = ObjectAnimator.ofFloat(binding.viewAlarmDialogBg, "alpha", 0f, 0.6f, 0f, 0.6f, 0f)
animator?.duration = 1200
animator?.interpolator = AccelerateInterpolator()
animator?.start()
}
}

需要注意地方是,由于弹框还负责跳转,而跳转有两种情况,一种是在ActivityA内,fragmentA与fragmentB间的跳转,这种情况使用findNavController().navigate()方法进行跳转,另一种是ActivityB到另一个ActivityA内的指定FragmentB界面。这种采用startActivity(intent)方式跳转,并且在ActivityA的onStart()的方法使用下面方法。


/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
val task = intent.getStringExtra("task")
if (task == "toAlarmDetail"){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
}
}

这样从ActivityB到另一个ActivityA时候,在onStart()方法内会触发上面的initToAlarmDetail()方法,获取跳转里面的信息,在决定具体跳转到哪个Fragment。这里解释的可能不太清楚,可以在Github下载源码看看可能更好理解些。


弹框对应的xml文件代码,可以在Github内查看,可以自己写一个,这个xml比较简单,只是xml代码比较占地方这里就不粘贴了。


前后台判断


关于前后台判断,需要创建一个继承ActivityLifecycleCallbacks和Application的类,这里命名为CustomApplication,在类里面实现ActivityLifecycleCallbacks接口相关方法,此外需要创建下面三个变量,分别表示activity数量,当前activity的名称,是否处于后台,代码如下:


private var activityCount = 0
private var nowActivityName:String? = null
private var isInBackground = true

之后需要在onActivityStarted,onActivityResumed,onActivityStopped方法内进行前后台相关处理,代码如下:


override fun onActivityStarted(activity: Activity) {
activityCount++
if (isInBackground){
isInBackground = false
}
nowActivityName = activity.javaClass.name
}

override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0 && !isInBackground){
isInBackground = true
}
}

上面代码可以看出,当触发onActivityStarted方法时候,activityCount数量加一,且app处于前台。之后记录当前activity名称,这里记录activity名称是后面有个功能是app置于后台时候弹出通知,而通知相关操作,为了每个activity都能实现就放在基类执行,而弹出通知并不需要每个继承基类的activity都执行,到时候需要根据根据nowActivityName判断哪个继承了基类的activity执行通知操作。


当触发onActivityStopped方法时候,activityCount数量减一,且当activityCount数量为零时,app置于后台。
CustomApplication完整代码如下:


class CustomApplication: Application(),Application.ActivityLifecycleCallbacks {
companion object{
const val TAG = "CustomApplication"
@SuppressLint("CustomContext")
lateinit var context: Context
}

private var activityCount = 0
private var nowActivityName:String? = null
private var isInBackground = true

fun getNowActivityName(): String? {
return nowActivityName
}

fun getIsInBackground():Boolean{
return isInBackground
}

override fun onCreate() {
super.onCreate()
context = applicationContext
registerActivityLifecycleCallbacks(this)
}

override fun onActivityCreated(activity: Activity, p1: Bundle?) {

}

override fun onActivityStarted(activity: Activity) {
activityCount++
if (isInBackground){
isInBackground = false
}
nowActivityName = activity.javaClass.name
}

override fun onActivityResumed(activity: Activity) {

}

override fun onActivityPaused(activity: Activity) {

}

override fun onActivityStopped(activity: Activity) {
activityCount--
if (activityCount == 0 && !isInBackground){
isInBackground = true
}
}

override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) {

}

override fun onActivityDestroyed(activity: Activity) {

}
}

弹框与通知弹出


开发中弹框与通知弹出的触发条件是,监听Websocket若有信息过来,app处于前台弹框,处于后台弹通知。这里使用Handler来模拟,弹框弹出比较简单,若有继承了DialogFragment的AlarmDialogFragment类。代码如下:


val dialog = AlarmDialogFragment()
dialog.show(supportFragmentManager,"tag")

通知弹出也不难,若只是弹出通知示例代码如下:


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.build()
notificationManager?.notify(notificationId,notification)

弹框与通知的特殊要求是,能在界面任意地方弹出且跳转到指定界面。弹框跳转相关代码在上面'弹框'部分,下面来说下通知的跳转,点击通知跳转是通过创建PendingIntent后在设置进NotificationCompat的setContentIntent方法内,不过通知跳转与弹框跳转一样需要分两种情况考虑,第一种同一Activity内Fragment与Fragment跳转,这种情况下PendingIntent如下代码所示:


var pendingIntent:PendingIntent? = null
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()

上面代码中使用NavDeepLinkBuilder创建了一个PendingIntent,并且使用setGraph()指向使用的导航图,setDestination()则指向目标Fragment。
另一种情况则是ActivityB到另一个ActivityA内的指定FragmentB界面,这种情况下PendingIntent设置代码如下:


val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)

这种是创建一个跳转到MainActivity的Intent,并添加传递的参数task,接着设置Intent的启动方式,其中Intent.FLAG_ACTIVITY_NEW_TASK,表示启动Activity作为新任务启动,Intent.FLAG_ACTIVITY_CLEAR_TASK,表示清除任务栈中所有现有的Activity。之后调用TaskStackBuilder创建PendingIntent。
上面两种方式创建的PendingIntent可以通过NotificationCompat.setContentIntent(pendingIntent)添加进去,关于通知创建的代码如下:


/** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
private fun useNotificationPI() {
var pendingIntent:PendingIntent? = null
if(javaClass.simpleName == "MainActivity"){//主界面
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()
}else {//其他界面时候切换后台通知
val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager?.notify(notificationId,notification)
}

上面代码中if(javaClass.simpleName == "MainActivity"),及第四行代码,该代码用处是当app置于后台时候,pp界面是MainActivity时,pendingIntent使用NavDeepLinkBuilder生成,当是其他Activity时使用TaskStackBuilder生成。之所以这样是因为,在MainActivity的xml,使用了FragmentContainerView用于fragment间跳转,其他的Activity没有FragmentContainerView,因此在生成pendingIntent需要采用不同的方式生成。


这示例代码中,主要涉及到的Avtivity有MainActivity与VideoActivity,MainActivity使用FragmentContainerView,而VideoActivity没有。弹框与通知跳转的界面是AlarmDetailFragment,这个fragment在MainActivity通过Navigation实现导航。


因此在MainActivity界面进入后台时,pendingIntent使用NavDeepLinkBuilder生成,NavDeepLinkBuilder则可以使用导航图中fragment生成深度链接URI,这个URI则可以导航到指定的fragment(关于NavDeepLinkBuilder了解不深入,这里说的可能有错误地方,欢迎大佬指正)。


而VideoActivity界面进入后台时,就需要使用TaskStackBuilder生成一个启动MainActivity的Intent。而在MainActivity的onStart方法内有下面initToAlarmDetail方法,判断跳转时携带参数决定是否跳转到AlarmDetailFragment界面。


/** 跳转报警详情界面 */
private fun initToAlarmDetail() {
val task = intent.getStringExtra("task")
if (task == "toAlarmDetail"){
val bundle = Bundle()
bundle.putString("alarmId","1")
findNavController(R.id.fragment_main_table).navigate(R.id.alarmDetailFragment,bundle)
}
}

至此弹框与通知的功能基本实现,完整的BaseActivity代码如下:


open class BaseActivity: AppCompatActivity() {
companion object{
const val TAG = "BaseActivity"
}
private var alarmCount = 0
private val handler = Handler(Looper.myLooper()!!)
//为了关闭通知,manager放在外面
private val notificationId = 1
private var alarmDialogFragment: AlarmDialogFragment? = null
private var notificationManager:NotificationManager? = null
private var bgServiceIntent:Intent? = null//前台服务

private var nowClassName = ""

/** 弹框定时任务 */
private val dialogRunnable = object : Runnable {
override fun run() {
//在定时方法里面 javaClass.simpleName 不能获取当前所处Activity的名称
if (nowClassName == "VideoActivity"){ //视频界面不弹弹框
CustomLog.d(TAG,"不使用弹框 ${nowClassName}")
}else{
CustomLog.d(TAG,"使用弹框 ${nowClassName}")
useDialog()
handler.postDelayed(this, 10000)
}
}
}

/** 通知定时任务 */
private val notificationRunnable = object :Runnable{
override fun run() {
useNotificationPI()
handler.postDelayed(this,10000)
}
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
initWindow()
return super.onCreateView(name, context, attrs)
}

override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) 当前类:${javaClass.simpleName}")
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CustomLog.d(TAG,"onCreate(savedInstanceState: Bundle?) 当前类:${javaClass.simpleName}")
initData()
}

override fun onStart() {
super.onStart()
CustomLog.d(TAG,"onStart 当前类:${javaClass.simpleName}")
nowClassName = javaClass.simpleName
handler.postDelayed(dialogRunnable, 3000)
initService()
}

override fun onResume() {
super.onResume()
CustomLog.d(TAG,"onResume 当前类:${javaClass.simpleName}")
}

override fun onRestart() {
super.onRestart()
CustomLog.d(TAG,"onRestart 当前类:${javaClass.simpleName}")
}

override fun onPause() {
super.onPause()
CustomLog.d(TAG,"onPause 当前类:${javaClass.simpleName}")
}

override fun onStop() {
super.onStop()
CustomLog.d(TAG,"onStop 当前类:${javaClass.simpleName}")
val customApplication = applicationContext as CustomApplication
val nowActivityName = customApplication.getNowActivityName()
val activitySimpleName = nowActivityName?.substringAfterLast(".")
CustomLog.d(TAG,"activitySimpleName:$activitySimpleName")
val isInBackground = (this@BaseActivity.applicationContext as CustomApplication).getIsInBackground()
if (isInBackground && activitySimpleName.equals(javaClass.simpleName)){// 处于后台 且 切换至后台app的activity页面名称等于当前基类里面获取activity类名
handler.postDelayed(notificationRunnable,3000)
CustomLog.d(TAG,"使用通知 $nowClassName")
}else{
CustomLog.d(TAG,"关闭所有定时任务 $nowClassName")
closeAllTask()
}
}

override fun onDestroy() {
super.onDestroy()
CustomLog.d(TAG,"onDestroy 当前类:${javaClass.simpleName}")
closeAllTask()
this.stopService(bgServiceIntent)
}

/** 关闭所有定时任务 */
private fun closeAllTask() {
handler.removeCallbacks(dialogRunnable)
handler.removeCallbacks(notificationRunnable)
}

/** 初始化数据 - 关于弹框*/
private fun initData() {
notificationManager = notificationManager ?: this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
alarmDialogFragment = alarmDialogFragment ?: AlarmDialogFragment()
}

/** 使用通知 - 通过pendingIntent实现跳转,缺点是任意界面进入报警详情界面,点击返回键只能返回MainFragment */
private fun useNotificationPI() {
var pendingIntent:PendingIntent? = null
if(javaClass.simpleName == "MainActivity"){//主界面
CustomLog.d(TAG,">>>通知:MainActivity")
val bundle = Bundle()
bundle.putString("alarmId","1")
pendingIntent = NavDeepLinkBuilder(this)
.setGraph(R.navigation.main_navigation)
.setDestination(R.id.alarmDetailFragment)
.setArguments(bundle)
.createPendingIntent()
}else {//其他界面时候切换后台通知
CustomLog.d(TAG,">>>通知:else")
val intent = Intent(this@BaseActivity,MainActivity::class.java)
intent.putExtra("task","toAlarmDetail")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
pendingIntent = TaskStackBuilder.create(this@BaseActivity)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel("normal", "Normal", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager?.createNotificationChannel(channel)
}
val notification = NotificationCompat.Builder(this.applicationContext, "normal")
.setContentTitle("标题")
.setContentText("通知次数:${++alarmCount}")
.setSmallIcon(R.drawable.ic_launcher_background)
.setTimeoutAfter(5000)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager?.notify(notificationId,notification)
}

/** 弹框使用 - 因为此处涉及到fragment等生命周期,进入其他activity内时候,在前的activity使用useDialog会因为生命周期问题闪退*/
private fun useDialog() {
//弹出多个同种弹框
// alarmDialogFragment = AlarmDialogFragment()
// alarmDialogFragment?.show(supportFragmentManager,"testDialog")

//不弹出多个同种弹框,一次只弹一个,若弹框存在不弹新框
if (alarmDialogFragment?.isVisible == false){//如果不加这一句,当弹框存在时候在调用alarmDialogFragment.show的时候会报错,因为alarmDialogFragment已经存在
alarmDialogFragment?.show(supportFragmentManager,"testDialog")
}else{
//更新弹框内信息
}
}

/** 关闭报警弹框 */
private fun closeAlarmDialog() {
if (alarmDialogFragment?.isVisible == true) {
alarmDialogFragment?.dismiss()//要关闭的弹框
}
}

//状态栏透明,且组件占据了状态栏
private fun initWindow() {
window.statusBarColor = Color.TRANSPARENT
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}

/** 初始化服务 */
private fun initService() {
CustomLog.d(TAG,"开启前台服务")
bgServiceIntent = bgServiceIntent ?: Intent(this, BackgroundService::class.java)
this.startService(bgServiceIntent)
}
}

总结


只是弹出弹框和通知的话,实现很好实现,中间麻烦地方在于当app使用多个Activity,该怎么实现跳转到指定的界面。当然这里麻烦是,从ActivityB跳转到ActivityA的Fragment,如果是只有一个Activity应该会好办些。个人感觉fragment跳转应该有更好的方式实现希望能和大佬们交流下这种情况下,用什么技术实现。


PS:感觉原生Android在写界面和跳转方面写起来不太方便。不知道大家有便捷的方式吗。


代码地址


GitHub:github.com/SmallCrispy…


作者:卤肉拌面
来源:juejin.cn/post/7260808821659779129
收起阅读 »

无悬浮窗权限实现全局Dialog

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。 如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty...
继续阅读 »

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。


如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty的变化就容易出现这种现象。


由于applicationContext没有AppWindowToken,所以dialog无法使用applicationContext创建,要么就使用windowManager配合WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY使用创建全局悬浮窗。但是这种做法需要申请权限。那么,在没有悬浮权限情况下如何做到让dialog不受栈顶activity变化的影响?


我的想法是通过application.registerActivityLifecycleCallbacks在activity变化时,关闭原来的弹窗,并重新创建一个一样的dialog并显示。


效果演示:


1. 栈顶界面被杀


界面退出

2. 有新界面弹出


界面退出

以下是代码实现:


/**
* @Description 无需悬浮权限的全局弹窗,栈顶activity变化后通过反射重建,所以子类构造方法需无参
*/

open class BaseAppDialog<T : ViewModel>() : Dialog(topActivity!!.get()!!), ViewModelStoreOwner {

companion object {
private val TAG = BaseAppDialog::class.java.simpleName
private var topActivity: WeakReference<Activity>? = null
private val staticRestoreList = linkedMapOf<Class<*>, Boolean>() //第二个参数:是否临时关闭
private val staticViewModelStore: ViewModelStore = ViewModelStore()

@JvmStatic
fun init(application: Application) {
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
topActivity = WeakReference(activity)
}

override fun onActivityStarted(activity: Activity) {

}

override fun onActivityResumed(activity: Activity) {
topActivity = WeakReference(activity)
val tempList = arrayListOf<BaseAppDialog<*>>()
val iterator = staticRestoreList.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
val topName = (topActivity?.get() ?: "")::class.java.name
if (next.value == true) { //避免onCreate创建的弹窗重复弹出
val newInstance = Class.forName(next.key.name).getConstructor().newInstance() as BaseAppDialog<*>
tempList.add(newInstance)
Log.e(TAG, "重新创建${next.key.name},于$topName")
iterator.remove()
}

}

tempList.forEach {
it.show()
}

if (staticRestoreList.size == 0) {
staticViewModelStore.clear()
}
}

override fun onActivityPaused(activity: Activity) {
}

override fun onActivityStopped(activity: Activity) {

}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
})
}
}


var vm: T? = null

init {
val genericClass = getGenericClass()
if (vm == null) {
(genericClass as? Class<T>)?.let {
vm = ViewModelProvider(this)[it]
}
}

topActivity?.get()?.let {
(it as LifecycleOwner).lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
dismissSilent()
}
})
}
}


//用于栈顶变化时的关闭
private fun dismissSilent() {
super.dismiss()
staticRestoreList.replace(this::class.java, true)
}

override fun show() {
super.show()
staticRestoreList.put(this::class.java, false)
}

override fun dismiss() {
super.dismiss()
staticRestoreList.remove(this::class.java)
}


//获取泛型实际类型
private fun getGenericClass(): Class<*>? {
val superclass = javaClass.genericSuperclass
if (superclass is ParameterizedType) {
val actualTypeArguments: Array<Type>? = superclass.actualTypeArguments
if (!actualTypeArguments.isNullOrEmpty()) {
val type: Type = actualTypeArguments[0]
if (type is Class<*>) {
return type
}
}
}
return ViewModel::class.java
}


//自己管理viewModel以便恢复数据
override fun getViewModelStore(): ViewModelStore {
return staticViewModelStore
}
}

参数传递的话,直接通过修改dialog的viewmodel变量或调用其方法来实现。


class TipDialogVm : ViewModel() {
val content = MutableLiveData<String>("")
}


class TipDialog2 : BaseAppDialog<TipDialogVm>() {

var binding : DialogTip2Binding? = null

init {
binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog_tip2, null, false)
binding?.lifecycleOwner = context as? LifecycleOwner
binding?.vm = vm
setContentView(binding!!.root)

}
}

弹出弹窗


TipDialog2().apply {
vm?.content?.value = "嗨嗨嗨"
}.show()

作者:Abin
来源:juejin.cn/post/7295576843653087266
收起阅读 »

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
收起阅读 »