注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

如何做一份干净的Git提交记录

背景 毕业工作有一些年头了,之前在写工作代码或者给开源项目贡献的时候,提交代码都不是很规范,甚至可以说十分的随意,想到什么就提交什么,根本没有管理提交记录的概念或者想法(当你身边的人都不怎么在意的时候,你也很难在意的)。工作了一段时间之后,对代码提交规范的要求...
继续阅读 »

背景


毕业工作有一些年头了,之前在写工作代码或者给开源项目贡献的时候,提交代码都不是很规范,甚至可以说十分的随意,想到什么就提交什么,根本没有管理提交记录的概念或者想法(当你身边的人都不怎么在意的时候,你也很难在意的)。工作了一段时间之后,对代码提交规范的要求高了不少,因为我发现,当我把这件事情做好的时候,会让工作变得顺畅一些。有一天解决倒腾git解决了一些问题,就想到把这些东西沉淀下来,分享出去。所以写下了这篇文章。倒也不是多么高大上的代码技巧或者炫技,只是分享之前遇到的一些git commit问题,和分享一些见解。


cases and thoughts


1. 处理业务开发代码和测试联调代码


想象一下这样一个场景:有一个需求开发完了,准备和其他同事联调这个需求。谨慎起见,联调期间希望可以观测到这个需求运行过程中的大部分细节。比如上下游的输入输出是什么样的,运行到某段代码他的表现应该是什么样的。如果没有测试到位,就上线了,一不小心造成了什么事故,问题可就大了。另外,如果在测试过程中遇到了肉眼看不出来的bug,这时候也需要打一些日志去分析。


这个时候问题就来了,调试日志代码一般情况下是不能上线的,毕竟一个系统在关键的地方打上日志方便线上排查bug就好,到处打日志估计服务器也很难顶得住。这个时候怎么办呢?我会通过下面的例子来演示下面解决的方法。


首先我们假设有一个仓库。叫做git-experiment.并且当前有一个提交,是一个名为Feature-1的需求的相关开发改动。通过查看git提交日志我们可以看到:


image-20230623172359302


下面让我们看看具体怎么解决这个问题。


1.1 直接在这个分支上写,merge进master之前去掉这些提交记录


第一个做法其实比较好理解,就直接在原来的分支上加嘛,测试完了把这些关于测试的提交记录去掉就好。下面这幅图模拟的情景是:



  1. 加上了打印log逻辑。

  2. 在测试中发现了问题,又添加了一个commit来修复这个bug。


image-20230623172557907


测试结束,这个时候我们看到上面这样一份提交记录,应该怎么办呢,用git revert?是不太行的喔,因为revert会生成一个新的提交记录。像下面这样子。


image-20230623175716868


这样子虽然代码没了,但是这份提交记录看起来就会很奇怪。很明显,在这个场景中,我们实际上只需要关于Feature-1代码改动和关于bug fix的代码改动。这个时候推荐用git rebase。比如在上面这个case中,可以运行:


git rebase -i HEAD~2

这时候我们将看到下面这个景象,这些你挑选好的commit会倒序排列在你面前让你操作。


image-20230623181437659


d代表要放弃这个commit,pick代表保留。确认好你的操作之后退出。可能会遇到代码冲突,解决冲突之后可以执行git rebase --continue。完事之后我们可以看到。


image-20230623181551019


Perfect,这就是我们想要的。但是,这个方法的缺点就是在原来的分支上引入了新的不确定性,并且在去除日志代码的时候,最好期待没有冲突出现,如果出现了冲突,一个不小心把业务需求的代码搞掉了,那就美滋滋了。这种做法相当于把所有的风险都集中在一个分支上面。


1.2 切换一个分支,在新分支上写测试相关的代码


这里介绍第二种方法。我们可以在原来的基础上切换出一个新的分支来,专门用来做测试相关的工作,原有的分支专注在业务开发和bug fix。


image-20230623184507882


然后给这个分支加上测试相关的日志打印代码。


image-20230623185128561


这时候可以看一下这个分支上面的提交日志。


image-20230623185158854


这时候我们就可以拿这个分支去部署,然后和别人联调测试了。如果在测试过程中发现了bug,需要修复怎么办?这个好办,在原来的分支上做bug fix,然后测试分支cherry-pick bug fix的commit就好了。让我们来看看。


image-20230623185610302


commit bug fix的代码后,切换到测试分支,cherry pick这个commit。


image-20230623190717668


有冲突解决冲突。弄完之后让我们来看看这个分支的log。


image-20230623190652166


这样你就可以继续用这个分支做bug fix之后的测试啦。这样做的好处就是,你的开发分支始终是安全且干净的。冲突都在测试分支解决。


2. 让每一个commit都有意义


相信大家都听过这样的一句话,"代码是写给人看的,其次它能在电脑上跑起来。"。无论是工作还是开源项目。如果你写的代码会给别人仔细的review,那么让别人知道你的代码代表什么,要做什么,就显得尤为关键。不要因为做这个需要时间就不去做,如果别人没看懂你的commit或者需要你修改一下commit信息,这些现在省下来的时间后面还是会还回去的。就比如上面这个例子的最终状态是这样的:


image-20230623181551019


实际上,我个人认为这两个commit应该合并成一个,其实看代码的人看到这个bug fix的commit还会好奇这个提交的上下文是什么,可能脑海里会涌现出这样一个问题:bug是什么?不要让别人想太多,如果他很困惑,他就会来问你。最好可以通过commit信息告诉他你做了什么。


说到这里你就会想了,我把所有代码都弄成一个commit,再给上比较好的commit信息是不是就好了。看情况吧,如果一个需求你写了好多代码,上千行这样子。这时候我认为应该拆分一下commit,比如这个时候你可以拆分成好几个小的commit,再分别给提供commit信息。看代码的人可以通过你的commit信息拆解你的需求,细分到每一个小需求去审视你的代码,这也节约了他人的时间。


总结


上面是在我在工作中遇到的一些关于代码提交的问题和想法, 在这里分享给到大家。希望对大家有所帮助。


作者:陪计算机走过漫长岁月
来源:juejin.cn/post/7255967761804951589
收起阅读 »

什么是护网行动?

一、什么是护网行动? 护网行动是以公安部牵头的,用以评估企事业单位的网络安全的活动。 具体实践中。公安部会组织攻防两方,进攻方会在一个月内对防守方发动网络攻击,检测出防守方(企事业单位)存在的安全漏洞。 通过与进攻方的对抗,企事业单位网络、系统以及设备等的安全...
继续阅读 »

一、什么是护网行动?


护网行动是以公安部牵头的,用以评估企事业单位的网络安全的活动。


具体实践中。公安部会组织攻防两方,进攻方会在一个月内对防守方发动网络攻击,检测出防守方(企事业单位)存在的安全漏洞。


通过与进攻方的对抗,企事业单位网络、系统以及设备等的安全能力会大大提高。


“护网行动”是国家应对网络安全问题所做的重要布局之一。“护网行动”从2016年开始,随着我国对网络安全的重视,涉及单位不断扩大,越来越多的单位都加入到护网行动中,网络安全对抗演练越来越贴近实际情况,各机构对待网络安全需求也从被动构建,升级为业务保障刚需。


二、护网分类


护网一般按照行政级别分为国家级护网、省级护网、市级护网;除此之外,还有一些行业对于网络安全的要求比较高,因此也会在行业内部展开护网行动,比如金融行业。


三、护网的时间


不同级别的护网开始时间和持续时间都不一样。以国家级护网为例,一般来说护网都是每年的7、8月左右开始,一般持续时间是2~3周。省级大概在2周左右,再低级的就是一周左右。2021年比较特殊,由于是建党100周年,所有的安全工作都要在7月之前完成,所有21年的护网在4月左右就完成了。


四、护网的影响


护网是政府组织的,会对所参与的单位进行排名,在护网中表现不佳的单位,未来评优评先等等工作都会受到影响。并且护网是和政治挂钩的,一旦参与护网的企业、单位的网络被攻击者打穿,领导都有可能被撤掉。比如去年的一个金融证券单位,网络被打穿了,该单位的二把手直接被撤职。整体付出的代价还是非常严重的。


五、护网的规则


**1、**红蓝对抗


护网一般分为红蓝两队,做红蓝对抗(网上关于红蓝攻防说法不一,这里以国内红攻蓝防为蓝本)。


红队为攻击队,红队的构成主要有“国家队”(国家的网安等专门从事网络安全的技术人员)、厂商的渗透技术人员。其中“国家队”的占比大概是60%左右,厂商的技术人员组成的攻击小队占比在40%左右。一般来说一个小队大概是3个人,分别负责信息收集、渗透、打扫战场的工作。


蓝队为防守队,一般是随机抽取一些单位参与。


2、蓝队分数


蓝队初始积分为10000分,一旦被攻击成功就会扣相应的分。每年对于蓝队的要求都更加严格。2020年以前蓝队只要能发现攻击就能加分,或者把扣掉的分补回来;但是到了2021年,蓝队必须满足及时发现、及时处置以及还原攻击链才能少扣一点分,不能再通过这个加分了。唯一的加分方式就是在护网期间发现真实的黑客攻击。


3、红队分数


每只攻击队会有一些分配好的固定的目标。除此之外,还会选取一些目标放在目标池中作为公共目标。一般来说红队都会优先攻击这些公共目标,一旦攻击成功,拿到证据后,就会在一个国家提供的平台上进行提交,认证成功即可得分。一般来说,提交平台的提交时间是9:00——21:00,但是这并不意味着过了这段时间就没人攻击了。实际上红队依然会利用21:00——9:00这段时间进行攻击,然后将攻击成果放在白天提交。所以蓝队这边需要24小时进行监守防护。


六、什么是红队?


红队是一种全范围的多层攻击模拟,旨在衡量公司的人员和网络、应用程序和物理安全控制,用以抵御现实对手的攻击。


在红队交战期间,训练有素的安全顾问会制定攻击方案,以揭示潜在的物理、硬件、软件和人为漏洞。红队的参与也为坏的行为者和恶意内部人员提供了机会来破坏公司的系统和网络,或者损坏其数据。


6.1、红队测试的意义


1. 评估客户对威胁行为的反应能力。


2. 通过实现预演(访问CEO电子邮件、访问客户数据等)来评估客户网络的安全态势。


3. 演示攻击者访问客户端资产的潜在路径。


我们认为站在红队的角度来说,任何网络安全保障任务都会通过安全检测的技术手段从寻找问题的角度出发,发现系统安全漏洞,寻找系统、网络存在的短板缺陷。红队安全检测方会通过使用多种检测与扫描工具,对蓝方目标网络展开信息收集、漏洞测试、漏洞验证。尤其是在面向规模型企业时,更会通过大规模目标侦查

等快速手段发现系统存在的安全问题,其主要流程如下:


1、大规模目标侦查


红方为了快速了解蓝方用户系统的类型、设备类型、版本、开放服务


类型、端口信息,确定系统和网络边界范围,将会通过Nmap、端口扫描与服务识别工具,甚至是使用ZMap、MASScan等大规模快速侦查工具了解用户网络规模、整体服务开放情况等基础信息,以便展开更有针对性的测试。


2、口令与常用漏洞测试


红方掌握蓝方用户网络规模、主机系统类型、服务开放情况后,将会使用Metasploit或手工等方式展开针对性的攻击与漏洞测试,其中包含:各种Web应用系统漏洞,中间件漏洞,系统、应用、组件远程代码执行漏等,同时也会使用Hydra等工具对各种服务、中间件、系统的口令进行常用弱口令测试,最终通过技术手段获得主机系统或组件权限。


3、权限获取与横向移动


红方通过系统漏洞或弱口令等方式获取到特定目标权限后,利用该主机系统权限、网络可达条件进行横向移动,扩大战果控制关键数据库、业务系统、网络设备,利用收集到的足够信息,最终控制核心系统、获取核心数据等,以证明目前系统安全保障的缺失。


红队充当真实且有动力的攻击者。大多数时候,红队攻击范围很大,整个环境都在范围内,他们的目标是渗透,维持持久性、中心性、可撤退性,以确认一个顽固的敌人能做什么。所有策略都可用,包括社会工程。最终红队会到达他们拥有整个网络的目的,否则他们的行动将被捕获,他们将被所攻击网络的安全管理员阻止,届时,他们将向管理层报告他们的调查结果,以协助提高网络的安全性。


红队的主要目标之一是即使他们进入组织内部也要保持隐身。渗透测试人员在网络上表现不好,并且可以很容易的被检测到,因为他们采用传统的方式进入组织,而红队队员是隐秘的、快速的,并且在技术上具备了规避AV、端点保护解决方案

、防火墙和组织已实施的其他安全措施的知识。


七、什么是蓝队


蓝队面临的更大挑战,是在不对用户造成太多限制的情况下,发现可被利用的漏洞,保护自己的领域。


1. 弄清控制措施


对蓝队而言最重要的,是了解自身环境中现有控制措施的能力,尤其是在网络钓鱼和电话钓鱼方面。有些公司还真就直到正式对抗了才开始找自家网络中的防护措施。


2. 确保能收集并分析数据


因为蓝队的功效基于收集和利用数据的能力,日志管理工具,比如Splunk,就特别重要了。另一块能力则是知道如何收集团队动作的所有数据,并高保真地记录下来,以便在复盘时确定哪些做对了,哪些做错了,以及如何改进。


3. 使用适合于环境的工具


蓝队所用工具取决于自身环境所需。他们得弄清“这个程序在干什么?为什么它会试图格式化硬盘?”,然后加上封锁非预期动作的技术。测试该技术是否成功的工具,则来自红队。


4. 挑有经验的人加入团队


除了工具,蓝队最有价值的东西,是队员的知识。随着经验的增长,你会开始想“我见过这个,那个也见过,他们做了这个,还做了那个,但我想知道这里是否有个漏洞。”如果你只针对已知的东西做准备,那你对未知就毫无准备。


5. 假定会有失败


提问,是通往探索未知的宝贵工具。别止步于为今天已存在的东西做准备,要假定自己的基础设施中将会有失败。


最好的思路,就是假设终将会有漏洞,没什么东

作者:网络安全在线
来源:juejin.cn/post/7255281894911606843
西是100%安全的。

收起阅读 »

培训班出来入职两个月感受分享

前言 大家好,好久不见,一晃距离上次发文章又快两个月过去了,不是我工作忙没时间写,确实是我太懒了不想写。上次发文章还是我刚入职发的心得,都是我自己真实的经历,我也偶尔来到这看看大家的评论,大家的每条评论我都看到了,因为太懒了没有一个个回复,不好意思。 关于评论...
继续阅读 »

前言


大家好,好久不见,一晃距离上次发文章又快两个月过去了,不是我工作忙没时间写,确实是我太懒了不想写。上次发文章还是我刚入职发的心得,都是我自己真实的经历,我也偶尔来到这看看大家的评论,大家的每条评论我都看到了,因为太懒了没有一个个回复,不好意思。


关于评论有很多对我上次发的文章说到的项目情况和数据库表感到怀疑,觉得我是在说谎,也有的说我就是培训机构故意瞎编文章骗人培训。针对这个我今天有空现在正好写篇文章说明下:


关于上篇文章提到的表数量真假


上篇文章是我刚入职写的,我说入职的公司数据库表将近有上千张。在这里我说声对不起,由于我那时刚入职确实不了解,老大给我在Navicat上连接了四个数据库连接,两个本地,两个线上,我当时就一个个打开大致的浏览了一下表,自己估算的这四个连接里表加起来是有将近上千张,我以为都是这个项目的表,就直接在文章里说了项目这么多表。


后来才知道本地和线上,这个项目分集团和工厂两套系统,功能大致一样也有很多不一样的,集团系统会下发指令到工厂系统,都是微服务结构,每个模块单独的服务,每个模块单独的数据库,集团和工厂数据库表加起来差不多四五百张表吧,我也不知道算不算多。下面放几张项目和数据库图片让你们看看,由于公司项目我就打码了


1689479021337.jpg


1689478827647.jpg


1689477878476.jpg


关于我入职后情况感受


在我入职这两个月时间里,我自己感觉确实有一些成长,从刚开始的恐惧,到现在的放松。刚开始给我安排的第一个业务任务就是,做一个上传Excel或PDF单子,存到minio中,然后读出单子上填写的工时和库存在页面显示出来,然后判断库存不够发邮件通知采购。我当时不知道怎么下手,领导说你这两年经验这对你来说应该不难吧?我只能自己慢慢摸索,我先chatGPT在哪搞半天怎么上传文件,领导后来看到骂了我一顿说这上传要你自己写吗?工具类都有封装的方法直接调用就存到minio了,让我想办法怎么读出单子上的数据,我百度了一天才读出单子上的数据,然后就是发邮件提醒采购,我又在那百度怎么发邮件,同事说都有的,让我看看他们代码怎么发的,我看了之后他们用kafka异步在那发邮件,我搞了好久才搞明白kafka怎么用的。总共搞了快一个星期才基本搞好,我本地用apiPost测了没问题,我就推到线上了,结果线上别人测了有问题页面数据没有显示我读出来的值,我读出来后修改了数据库值,页面刷新应该会读出来的啊,但是就是没出来,我本地测接口看数据库数据确实改了啊,为什么页面就是不变,搞了好久领导过来又骂我一顿说你看不懂代码吗?这读的缓存你看不到?让我更新数据库后清除Redis缓存,最后终于搞好了。


就这样我刚开始真的煎熬,好在领导和同事人都很好,对我帮助很大,我也慢慢的熟练写出功能了。


我现在的状况


我现在是独立负责一个模块,对这个模块也比较熟悉了,业务需求也能基本按规定时间完成了。我现在周末会在家继续学习新知识,昨天周六在家学了一天的工作流activiti7,在B站看的8个小时的视频教程搞了一天搞完了,今天准备做个小项目专门练习一下工作流。


image.png
嗯,就这样吧,我去吃饭了,吃完饭回来继续学习了。学

作者:学习编程的小江
来源:juejin.cn/post/7255968185681215525
无止境,大家一起加油

收起阅读 »

熟读代码简洁之道,为什么我还是选择屎山

web
前言 前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐; 没有严格的卡口...
继续阅读 »

前言


前几天我写了一篇Vue2屎山代码汇总,收到了很多人的关注;这说明代码简洁这仍然是一个程序员的基本素养,大家也都对屎山代码非常关注;但是关注归关注,执行起来却非常困难;我明明知道这段代码的最佳实践,但是我就是不那样写,因为我有很多难言之隐;


没有严格的卡口


没有约束就没有行动,比方说eslint,eslint只能减少很少一部分屎山,而且如果不在打包机器上配置eslint的话那么eslint都可以被绕过;对我个人而言,实现一个需求,当然是写屎山代码要来的快一些,我写屎山代码能够6点准时下班,要是写最佳实践可能就要7点甚至8点下班了,没有人愿意为了代码整洁度而晚一点下班的。


没有CodeReview,CodeReview如果不通过会被打回重新修改,直到代码符合规范才能提交到git。CodeReview是一个很好地解决团队屎山代码的工具,只可惜它只是一个理想。因为实际情况是根本不可能有时间去做CodeReview,连基本需求都做不完,如果去跟老板申请一部分时间来做CodeReview,老板很有可能会对你进行灵魂三连问:你做为什么要做CodeReivew?CodeReview的价值是什么?有没有量化的指标?对于屎山代码的优化,对于开发体验、开发效率、维护成本方面,这些指标都非常难以衡量,它们对于业务没有直接的价值,只能间接地提高业务的开发效率,提高业务的稳定性,所以老板只注重结果,只需要你去实现这个需求,至于说代码怎么样他并不关心;


没有代码规约


大厂一般都有代码规约,比如:2021最新阿里代码规范(前端篇)百度代码规范


但是在小公司,一般都没有代码规范,也就是说代码都无章可循;这种环境助长了屎山代码的增加,到头来屎山堆得非常高了,之后再想去通过重构来优化这些屎山代码,这就非常费力了;所以要想优化屎山代码光靠个人自觉,光靠多读点书那是没有用的,也执行不下去,必须在团队内形成一个规范约定,制定规约宜早不宜迟


没有思考的时间


另外一个造成屎山代码的原因就是没时间;产品经理让我半天完成一个需求,老大说某个需求很紧急,要我两天内上线;在这种极限压缩时间的需求里面,确实没有时间去思考代码怎么写,能cv尽量cv;但是一旦养成习惯,即使后面有时间也不会去动脑思考了;我个人的建议是不要总是cv,还是要留一些时间去思考代码怎么写,至少在接到需求到写代码之前哪怕留个5分钟去思考,也胜过一看到需求差不多就直接cv;


框架约束太少


越是自由度高的框架越是容易写出屎山代码,因为很多东西不约束的话,代码就会不按照既定规则去写了;比如下面这个例子:
stackblitz.com/edit/vue-4a…


这个例子中父组件调用子组件,子组件又调用父组件,完全畅通无阻,完全可以不遵守单向数据流,这样的话为了省掉一部分父子组件通信的逻辑,就直接调用父组件或者子组件,当时为了完成需求我这么做了,事后我就后悔了,极易引起bug,比如说下一次这个需求要改到这一部分逻辑,我忘记了当初这个方法还被父组件调用,直接修改了它,于是就引发线上事故;最后自己绩效不好看,但是全是因为自己当初将父子组件之间耦合太深了;


自己需要明白一件事情那就是框架自由度越高,越需要注意每个api调用的方式,不能随便滥用;框架自由不自由这个我无法改变,我只能改变自己的习惯,那就是用每一个api之前思考一下这会给未来的维护带来什么困难;


没有代码质量管理平台


没有代码质量管理平台,你说我写的屎山,我还不承认,你说我代码写的不好,逻辑不清晰,我反问你有没有数据支撑


但是当代码质量成为上线前的一个关键指标时,每个人都不敢懈怠;常见的代码质量管理平台有SonarQubeDeepScan,这些工具能够继承到CI中,成为部署的一个关键环节,为代码质量保驾护航;代码的质量成为了一个量化指标,这样的话每个人的代码质量都清晰可见


最后


其实看到屎山代码,每一个人都应该感到庆幸,这说明有很多事情要做了,有很多基建可以开展起来;推动团队制定代码规约、开发eslint插件检查代码、为框架提供API约束或者部署一个代码质量管理平台,这一顿操作起

作者:蚂小蚁
来源:juejin.cn/post/7255686239756533818
来绩效想差都差不了;

收起阅读 »

程序员提高效率的办法

最重要的-利用好工具 🔧 工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等; http://www.dooocs.com/chatgpt/REA… 1. 早上不要开会 📅 每个人一天是 24 小时,时间是均等的,但是时间...
继续阅读 »

最重要的-利用好工具 🔧


工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等;
http://www.dooocs.com/chatgpt/REA…


1. 早上不要开会 📅


每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?


因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。


2. 不要使用番茄钟 🍅


有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。


有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。


好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。


3. 休息时间不要玩手机 📱


大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。


那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:



  • 闭目养神 😪

  • 听音乐 🎶

  • 在办公室走动走动 🏃‍♂️

  • 和同事聊会天 💑

  • 扭扭脖子活动活动 💁‍♂️

  • 冥想 or 正念 🧘


4. 不要在工位上吃午饭 🥣


大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:




  • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:



    • 日光浴:外出的时候晒太阳可以促进血清素的分泌

    • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌




  • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力




  • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。




5. 睡午觉 😴


现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:



  • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。

  • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街

  • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等


睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。


6. 下午上班前运动一下 🚴


下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:



  • 1️⃣ 深蹲

  • 2️⃣ 俯卧撑

  • 3️⃣ 胯下击掌

  • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)


7. 2 分钟解决和 30 秒决断 🖖


⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成,2 分钟解决就是一个很好的辅助决策的办法。


💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。


8. 不要加班,充足睡眠 💤


作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2 、3 点。


压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。


想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。


9. 睡前 2 小时 🛌




  1. 睡前两小时不能做的事情:



    • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了

    • 🥃 喝酒

    • ⛹️ 剧烈运动

    • 💦 洗澡水过高

    • 🎮 视觉娱乐(打游戏,看电影等)

    • 📺 闪亮的东西(看手机,看电脑,看电视)

    • 💡 在灯光过于明亮的地方




  2. 适合做的事情



    • 📖 读书

    • 🎶 听音乐

    • 🎨 非视觉娱乐

    • 🧘‍♂️ 使身体放松的轻微运动




10. 周末不用刻意补觉 🚫


很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。


其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。


我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。


参考


以上大部分来源于书籍 《为什么精英都是时间

作者:dooocs
来源:juejin.cn/post/7255189463747543095
控》,作者桦泽紫苑;

收起阅读 »

如何给你的个人博客添加点赞功能

web
最近在重构博客,想要添加一些新功能。平时有看 Josh W. Comeau 的个人网站,他的每篇文章右侧都会有一个心形按钮,用户通过点击次数来表达对文章的喜爱程度。让我们来尝试实现这个有趣的点赞功能吧! 绘制点赞图标 点赞按钮的核心是 SVG 主要由两部分组...
继续阅读 »

最近在重构博客,想要添加一些新功能。平时有看 Josh W. Comeau 的个人网站,他的每篇文章右侧都会有一个心形按钮,用户通过点击次数来表达对文章的喜爱程度。让我们来尝试实现这个有趣的点赞功能吧!


image.png


绘制点赞图标


点赞按钮的核心是 SVG 主要由两部分组成:



  • 两个爱心形状 ❤️ 的 path ,一个为前景,一个为背景

  • 一个遮罩 mask ,引用 rect 作为遮罩区域


首先使用 defs 标签定义一个 id 为 heart 的爱心形状元素,在后续任何地方都可以使用 use 标签来复用这个 “组件”。


其次使用 mask 标签定义了一个 id 为 mask 的遮罩元素,通过 rect 标签设置了一个透明的矩形作为遮罩区域。


最后使用一个 use 标签引用了之前定义的 heart 图形元素作为默认的初始颜色,使用另一个 use 标签,同样引用 heart 图形元素,并使用 mask 属性引用了之前定义的遮罩元素,用于实现填充颜色的遮罩效果。


点赞动画


接下来实现随着点赞数量递增时爱心逐渐被填充的效果,我们可以借助 CSS 中 transfrom 的 translateY 属性来完成。设置最多点击次数(这里我设置为 5 次)通过 translateY 来移动遮罩的位置完成填充,也就是说,读者需要点击 5 次才能看到完整的红色爱心形状 ❤️ 的点赞按钮。


除此之外我们还可以为点赞按钮添加更有趣交互效果:



  1. 每次点击时右侧会出现『 +1 』字样

  2. 用户在点击第 3 次的时候,填充爱心形状 ❤️ 点赞按钮的同时,还会向四周随机扩散 mini 爱心 💗


这里可以用 framer-motion 来帮助我们实现动画效果。


animate([
...sparklesReset,
['button', { scale: 0.9 }, { duration: 0.1 }],
...sparklesAnimation,
['.counter-one', { y: -12, opacity: 0 }, { duration: 0.2 }],
['button', { scale: 1 }, { duration: 0.1, at: '<' }],
['.counter-one', { y: 0, opacity: 1 }, { duration: 0.2, at: '<' }],
['.counter-one', { y: -12, opacity: 0 }, { duration: 0.6 }],
...sparklesFadeOut,
])

这样就完成啦,使劲儿戳下面的代码片段试试效果:



数据持久化


想要让不同用户看到一致的点赞数据,我们需要借助数据库来保存每一个用户的点赞次数和该文章的总获赞次数。每当用户点击一次按钮,就会发送一次 POST 请求,将用户的 IP 地址和当前点赞的文章 ID (这里我使用的文章标题,可以替换为任意文章唯一标识) 存入数据库,同时返回当前的用户合计点赞次数和该文章的总获赞次数


export async function POST(req: NextRequest, { params }: ParamsProps) {
const res = await req.json()
const slug = params.slug
const count = Number(res.count)
const ip = getIP(req)
const sessionId = slug + '___' + ip

try {
const [post, user] = await Promise.all([
db.insert(pages)
.values({ slug, likes: count })
.onConflictDoUpdate({
target: pages.slug,
set: { likes: sql`pages.likes + ${count}` },
})
.returning({ likes: pages.likes }),
db.insert(users)
.values({ id: sessionId, likes: count })
.onConflictDoUpdate({
target: users.id,
set: { likes: sql`users.likes + ${count}` },
})
.returning({ likes: users.likes })
])
return NextResponse.json({
post_likes: post[0].likes || 0,
user_likes: user[0]?.likes || 0
});
} catch (error) {
return NextResponse.json({ error }, { status: 400 })
}
}

同理,当用户再次进入该页面时,发起 GET 请求,获取当前点赞状态并及时渲染到页面。


回顾总结


点赞功能在互联网应用中十分广泛,自己手动尝试实现这个功能还是挺有趣的。本文从三方面详细介绍了这一实现过程:



  • 绘制点赞图标:SVG 的各种属性应用

  • 点赞动画:framer-motion 动画库的使用

  • 数据持久化:数据库查询


如果这篇文章对你有帮助,记得点赞!


本文首发于我的个人网站 leonf

ong.me

收起阅读 »

我教你怎么在Vue3实现列表无限滚动,hook都给你写好了

web
先看成果 无限滚动列表 无限滚动列表(Infinite Scroll)是一种在网页或应用程序中加载和显示大量数据的技术。它通过在用户滚动到页面底部时动态加载更多内容,实现无缝的滚动体验,避免一次性加载所有数据而导致性能问题。供更流畅的用户体验。但需要注意在实...
继续阅读 »

先看成果


动画.gif

无限滚动列表


无限滚动列表(Infinite Scroll)是一种在网页或应用程序中加载和显示大量数据的技术。它通过在用户滚动到页面底部时动态加载更多内容,实现无缝的滚动体验,避免一次性加载所有数据而导致性能问题。供更流畅的用户体验。但需要注意在实现时,要考虑合适的加载阈值、数据加载的顺序和流畅度,以及处理加载错误或无更多数据的情况,下面我们用IntersectionObserver来实现无线滚动,并且在vue3+ts中封装成一个可用的hook


IntersectionObserver是什么



IntersectionObserver(交叉观察器)是一个Web API,用于有效地跟踪网页中元素在视口中的可见性。它提供了一种异步观察目标元素与祖先元素或视口之间交叉区域变化的方式。
IntersectionObserver的主要目的是确定一个元素何时进入或离开视口,或者与另一个元素相交。它在各种场景下非常有用,例如延迟加载图片或其他资源,实现无限滚动等。



这里用一个demo来做演示


动画.gif

demo代码如下,其实就是用IntersectionObserver来对某个元素做一个监听,通过siIntersecting属性来判断监听元素的显示和隐藏。


 const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('元素出现');
} else{
console.log('元素隐藏');
}
});
});
observer.observe(bottom);


无限滚动实现


下面我们开始动手


1.数据模拟


模拟获取数据,比如分页的数据,这里是模拟的表格滚动的数据,每次只加载十条,类似于平时的翻页效果,这里写的比较简单,
在这里给它加了一个最大限度30条,超过30条就不再继续增加了


<template>
<div ref="container" class="container">
<div v-for="item in list" class="box">{{ item.id }}</div>
</div>

</template>
<script setup lang="ts">

const list: any[] = reactive([]);
let idx = 0;

function getList() {
return new Promise((res) => {
if(idx<30){
for (let i = idx; i < idx + 10; i++) {
list.push({ id: i });
}
idx += 10
}
res(1);
});
</script>

2.hook实现


import { createVNode, render, Ref } from 'vue';
/**
接受一个列表函数、列表容器、底部样式
*/

export function useScroll() {
// 用ts定义传入的三个参数类型
async function init(fn:()=>Promise<any[] | unknown>,container:Ref) {
const res = await fn();
}
return { init }
}


执行init就相当于加载了第一次列表 后续通过滚动继续加载列表


import { useScroll } from "../hooks/useScroll.ts";
onMounted(() => {
const {init} = useScroll()
//三个参数分别是 加载分页的函数 放置数据的容器 结尾的提示dom
init(getList,container,bottom)
});

3.监听元素


export function useScroll() {
// 用ts定义传入的三个参数类型
async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 使用IntersectionObserver来监听bottom的出现
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
console.log('元素出现');
} else{
console.log('元素隐藏');

}
});
});
observer.observe(bottom);
}
return { init }
}

4.hook初始化


获取需要做无限滚动的容器 这里我们用ref的方式来直接获取到dom节点 大家也可以尝试下用getCurrentInstance这个api来获取到


整个实例,其实就是类似于vue2中的this.$refs.container来获取到dom节点容器


根据生命周期我们知道dom节点是在mounted中再挂载的,所以想要拿到dom节点,要在onMounted里面获取到,毕竟没挂载肯定是拿不到的嘛



const container = ref<HTMLElement | null>(null);
onMounted(() => {
const vnode = createVNode('div', { id: 'bottom',style:"color:#000" }, '到底了~');
render(vnode, container.value!);
const bottom = document.getElementById('bottom') as HTMLDivElement;
// 用到的是createVNode来生成虚拟节点 然后挂载到容器container中
const {init} = useScroll()
//三个参数分别是 加载分页的函数 放置数据的容器 结尾的提示dom
init(getList,container,bottom)
});

这部分代码是生成放到末尾的dom节点 封装的init方法可以自定义传入末尾的提示dom,也可以不传,封装的方法中有默认的dom


优化功能


1.自定义默认底部提示dom


async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 如果没有传入自定义的底部dom 那么就生成一个默认底部节点
if(!bottom){
const vnode = createVNode('div', { id: 'bottom',style:"color:#000" }, '已经到底啦~');
render(vnode, container.value!);
bottom = document.getElementById('bottom') as HTMLDivElement;
}
// 使用IntersectionObserver来监听bottom的出现
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
console.log('元素出现');
} else{
console.log('元素隐藏');

}
});
});
observer.observe(bottom);
}

完整代码


import { createVNode, render, Ref } from 'vue';
/**
接受一个列表函数、列表容器、底部样式
*/

export function useScroll() {
async function init(fn:()=>Promise<any[] | unknown>,container:Ref,bottom?:HTMLDivElement) {
const res = await fn();
// 生成一个默认底部节点
if(!bottom){
const vnode = createVNode('div', { id: 'bottom' }, '已经到底啦~');
render(vnode, container.value!);
bottom = document.getElementById('bottom') as HTMLDivElement;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fn();
}
});
});
observer.observe(bottom);
}
return { init }
}


<template>
<div ref="container" class="container">
<div v-for="item in list" class="box">{{ item.id }}</div>
</div>

</template>
<script setup lang="ts">
import { onMounted, createVNode, render, ref, reactive } from 'vue';
import { useScroll } from "../hooks/useScroll.ts";
const list: any[] = reactive([]);
let idx = 0;
function getList() {
return new Promise((res,rej) => {
if(idx<=30){
for (let i = idx; i < idx + 10; i++) {
list.push({ id: i });
}
idx += 10
res(1);
}
rej(0)
});
}

const container = ref<HTMLElement | null>(null);
onMounted(() => {
const vnode = createVNode('div', { id: 'bottom' }, '到底了~');
render(vnode, container.value!);
const bottom = document.getElementById('bottom') as HTMLDivElement;
const {init} = useScroll()
init(getList,container,bottom)
});

</script>
<style scoped>
.container {
border: 1px solid black;
width: 200px;
height: 100px;
overflow: overlay
}

.box {
height: 30px;
width: 100px;
background: red;
margin-bottom: 10px
}
</style>

作者:一只大加号
来源:juejin.cn/post/7255149657769066551
>
收起阅读 »

作为开发人员,如何一秒洞悉文件结构?

web
曾经在处理复杂的文件结构时感到束手无策吗?别担心,说一个真正的解决方案——JavaScript中的tree-node包。它能以一种惊人的方式展示文件和文件夹的层次结构,让你瞬间掌握复杂的项目布局。 背景 在一个新项目中,你可能会面对各种文件,包括HTML、CS...
继续阅读 »

b60632618f4042c9a5aed99a0d176157.jpeg


曾经在处理复杂的文件结构时感到束手无策吗?别担心,说一个真正的解决方案——JavaScript中的tree-node包。它能以一种惊人的方式展示文件和文件夹的层次结构,让你瞬间掌握复杂的项目布局。


背景


在一个新项目中,你可能会面对各种文件,包括HTML、CSS、JavaScript、配置文件等等。起初,你可能不清楚这些文件的具体作用和位置,感到无从下手。而随着项目的发展,文件数量可能会急剧增加,你可能会渐渐迷失在文件的迷宫中,忘记了某个文件的用途或者它们之间的关联。


正是在这样的背景下,tree-node包闪亮登场!它为你呈现出一个惊人的树状结构,展示了项目中各个文件和文件夹之间的层次关系。通过运行简单的命令,你就能立即获得一个清晰而易于理解的文件结构图。无论是文件的嵌套层级、文件之间的依赖关系,还是文件夹的组织结构,一目了然。


一键安装,瞬间拥有超能文件管理能力!


无需复杂的步骤或繁琐的设置,只需在命令提示符或终端中输入一行命令,即可全局安装tree-node包:


npm install -g tree-node-cli

震撼视觉展示


tree-node包不仅仅是文件管理工具,它能以惊人的树状结构展示方式,为你带来震撼的视觉体验。使用treee命令,它能够在屏幕上呈现令人惊叹的文件和文件夹布局。无论是开发项目还是设计项目,你都能一目了然地了解整个文件结构。


示例: 假设你的项目文件结构如下:


- src
- js
- app.js
- css
- styles.css
- theme.css
- index.html
- public
- images
- logo.png
- banner.png
- index.html
- README.md

通过执行以下命令:


treee -L 3 -I "node_modules|.idea|.git" -a --dirs-first

你将获得一个惊艳的展示结果:


.
├───src
│ ├───js
│ │ └───app.js
│ ├───css
│ │ ├───styles.css
│ │ └───theme.css
│ └───index.html
├───public
│ ├───images
│ │ ├───logo.png
│ │ └───banner.jpg
│ └───index.html
└───README.md

这个直观的展示方式帮助你迅速理解整个文件结构,无需手动遍历文件夹层级。你可以清楚地看到哪些文件和文件夹属于哪个层级,方便你快速导航和查找所需资源,你也可以在上面注释文件的作用。


自定义控制


tree-node包提供了强大的自定义功能,让你对文件结构拥有绝对掌控。只需重新执行treee命令,tree-node-cli会自动展示最新的文件结构。再通过设置参数,你可以控制显示的层级深度、忽略特定文件夹,并决定是否显示隐藏文件。


配置参数:


-V, --version             输出版本号
-a, --all-files 打印所有文件,包括隐藏文件
--dirs-first 目录在前,文件在后
-d, --dirs-only 仅列出目录
-I, --exclude [patterns] 排除与模式匹配的文件。用 | 隔开,用双引号包裹。 例如 “node_modules|.git”
-L, --max-depth <n> 目录树的最大显示深度
-r, --reverse 按反向字母顺序对输出进行排序
-F, --trailing-slash 为目录添加'/'
-h, --help 输出用法信息

例如,使用以下命令可以显示三级深度的文件结构,并排除node_modules、.idea、objects和.git文件夹,同时显示所有文件,包括以点开头的隐藏文件:(这几个配置是最常见的,我基本是直接复制粘贴拿来就用


treee -L 3 -I "node_modules|.idea|objects|.git" -a --dirs-first


  • -L 3:指定路径的级别为3级。

  • -I "node_modules|.idea|objects|.git":忽略文件夹(正则表达式匹配。.git会匹配到.gitignore)。

  • -a:显示所有文件(默认前缀有"."的不会显示,例如".bin")。

  • --dirs-first:目录在前,文件在后(默认是字母排序)。


tree-node-cli的自定义控制没有繁琐的配置和操作,只需几个简单的参数设置执行命令,你就能根据自己的需求,定制化你的文件展示方式。


灵活应对文件变动


tree-node-cli不仅可以帮助你展示当前的文件结构,还可以灵活应对文件的变动。当你新增或删除了JS文件时,只需重新执行treee命令,tree-node-cli会自动更新并展示最新的文件结构。


示例:
假设在项目中新增了一个名为utils.js的JavaScript文件。只需在终端中切换到项目文件夹路径,并执行以下命令:


treee -L 3 -I "node_modules|.idea|objects|.git" -a --dirs-first

tree-node-cli将重新扫描文件结构,并在展示中包含新添加的utils.js文件:


.
├───src
│ ├───js
│ │ ├───utils.js
│ │ └───app.js
│ ├───css
│ │ ├───styles.css
│ │ └───theme.css
│ └───index.html
├───public
│ ├───images
│ │ ├───logo.png
│ │ └───banner.jpg
│ └───index.html
└───README.md

同样,如果你删除了一个文件,tree-node-cli也会自动更新并将其从展示中移除。


总结


不管你是开发者、设计师还是任何需要处理复杂文件结构的人,tree-node包都将成为你的得力助手。它简化了文件管理手动操作过程,提供了震撼的视觉展示,让你能够轻松地理解和掌握项目的文件结构。你还有更好的文件管理方法吗,欢迎在评论区分享你对文件管理的更好方法,让我们共同探讨文件管理的最佳实践。


作者:Sailing
来源:juejin.cn/post/7255189463747280951
收起阅读 »

CSS实现0.5px的边框的两种方式

web
方式一 <style> .border { width: 200px; height: 200px; position: relative; } .border::before { content: ""; position: abs...
继续阅读 »

方式一


<style>
.border {
width: 200px;
height: 200px;
position: relative;
}
.border::before {
content: "";
position: absolute;
left:0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid blue;
transform-origin: 0 0;
transform: scale(0.5);
}
</style>

<div class="border"></div>

方式二


<style>
.border {
width: 200px;
height: 200px;
position: relative;
}
.border::before {
position: absolute;
box-sizing: border-box;
content: " ";
pointer-events: none;
top: -50%;
right: -50%;
bottom: -50%;
left: -50%;
border: 1px solid blue;
transform: scale(0.5);
}
</style>

<div class="border"></div>
作者:很晚很晚了
来源:juejin.cn/post/7255147749360156730

收起阅读 »

基于 Tauri, 我写了一个 Markdown 桌面 App

web
本文视频地址 前言 大家好,我是小马。 去年,我开发了一款微信排版编辑器 MDX Editor。它可以自定义组件、样式,生成二维码,代码 Diff 高亮,并支持导出 Markdown 和 PDF 等功能。然而,作为一个微信排版编辑器,它的受众面比较有限,并不适...
继续阅读 »

本文视频地址


前言


大家好,我是小马。


去年,我开发了一款微信排版编辑器 MDX Editor。它可以自定义组件、样式,生成二维码,代码 Diff 高亮,并支持导出 Markdown 和 PDF 等功能。然而,作为一个微信排版编辑器,它的受众面比较有限,并不适用于每个人。因此,我基于该编辑器开发了 MDX Editor 桌面版,它支持 Mac、Windows 和 Linux,并且非常轻量,整个应用的大小只有 7M。现在,MDX Editor 桌面版已经成为我的创作工具。如果你对它感兴趣,可以在文末获取。


演示


技术选型


开发 MDX Editor 桌面 App,我使用了如下核心技术栈:




  • React (Next.js)




  • Tauri —— 构建跨平台桌面应用的开发框架




  • Tailwind CSS —— 原子类样式框架,支持深色皮肤




  • Ant Design v5 —— 使用"Tree"组件管理文档树




功能与实现


1. MDX 自定义组件


MDX 结合了 Markdown 和 JSX 的优点,它让你可以在 Markdown 文档中直接使用 React 组件,构建复杂的交互式文档。如果你熟悉 React,你可以在 "Config" 标签页中自定义你的组件;如果你不是一个程序员,你也可以基于现有模板进行创作。例如,模板中的 "Gallery" 组件实际上就是一个 "flex" 布局。


代码



function Gallery({children}) {

return <div className="flex gallery">

{children}

</div>


}


文档写作


预览效果


2. 深色皮肤


对于笔记软件来说,深色皮肤已经成为一个不可或缺的部分。MDX Editor 使用 Tailwind CSS 实现了深色皮肤。



3. 多主题


编辑器内置了 10+个文档主题和代码主题,你可以点击右上方的设置按钮进行切换。



4. 本地文件管理


桌面 App 还支持管理本地文件。你可以选择一个目录,或者将你的文档工作目录拖入编辑器,便能够实时地在编辑器中管理文档。



当我在开发这个功能之前,我曾担心自己不熟悉 Rust,无法完成这个功能。但是,熟悉了 Tauri 文档之后,我发现其实很简单。Tauri 提供了文件操作的 API,使得我们不需要编写 Rust 代码,只需要调用 Tauri API 就能完成文件管理。


import { readTextFile, BaseDirectory } from '@tauri-apps/api/fs';

// 读取路径为 `$APPCONFIG/app.conf` 的文本文件

const contents = await readTextFile('app.conf', { dir: BaseDirectory.AppConfig });


文档目录树采用了 Ant Design 的 Tree 组件实现,通过自定义样式使其与整体皮肤风格保持一致,这大大减少了编码工作量。


5. 文档格式化


在文档写作的过程中,格式往往会打断你的创作思路。虽然 Markdown 已经完全舍弃了格式操作,但有时你仍然需要注意中英文之间的空格、段落之间的空行等细节。MDX Editor 使用了 prettier 来格式化文档,只需按下 command+s 就能自动格式化文档。



最后


如果你对这个编辑器感兴趣,可以在 Github 下载桌面版体验。如果你对实现过程感兴趣,也可以直接查看源码。如果您有任何好的建议,可以在上面提出 Issues,或者关注微信公众号 "JS

作者:狂奔滴小马
来源:juejin.cn/post/7255189463746986039
酷" 并留言反馈。

收起阅读 »

一名4年前端打工仔的年中总结

2023年中总结 时光飞逝,感觉只是一转眼,2023年进度条已过半,相信各位掘友也会这样觉得吧。回顾过去的半年,我都做些什么事情呢?今天就给各位分享一下我的年中总结,分为两部分来总结(工作 和 个人)。 工作 2023年年初,各位应该大部分人都是新冠初愈吧,反...
继续阅读 »

2023年中总结


时光飞逝,感觉只是一转眼,2023年进度条已过半,相信各位掘友也会这样觉得吧。回顾过去的半年,我都做些什么事情呢?今天就给各位分享一下我的年中总结,分为两部分来总结(工作 和 个人)。


工作


2023年年初,各位应该大部分人都是新冠初愈吧,反正我是,也不再疫情管控了,本以为好日子来了。然而...

两极反转


网络上大厂裁员,裁线,减产...,各种消息铺天盖地,再加上AI遍地开花的冲击,有人为了博眼球,蹭热度直接搞出了什么前端已死,唯恐天下不乱的言论,本就内卷的互联网行业变得更加内卷。


个人建议有看这些混淆视听文章的时间,还不如想想怎么学点东西,想想怎么搞钱!


笔者上半年工作是这样的:



  1. 带不来成就感的项目

  2. 狭窄到看不到的上升空间

  3. 频繁的出差

  4. 1薪都没有的年终(22年还有2薪)

  5. 非常好的领导的离开

  6. 当然,没有出过意外的加薪也没有了

  7. 团队获奖——“王牌战队”


我为啥不换工作?哈哈,怪自己借口太多。


个人


在工作中,不难看出几乎不能带给我什么技术上或者其他方面能力的成长了,那只有通过自己去学习了。这半年我做了以下几件事情:


掘金专栏


✅ 完成了[我的专栏——前端需要掌握的设计模式]。(juejin.cn/column/7195…)
image.png


音乐小项目


✅ 为了熟悉Vue3 CompositionAPI以及小程序,我用业余时间完成了一个小的音乐项目,PC端


image.png


前端性能优化学习


✅ 还学习了前端性能优化,整理了一些笔记到个人的仓库
image.png
我的博客


Vue3源码学习


✅ 除了以上这些,我还在学习Vue3的源码,都在进阶Vue.js中,还在努力更新中。
image.png


参与金石计划分奖金


✅ 参与掘金的金石计划获得200+奖金,虽然不多,但是还是挺开心的。
image.png


最近参加的一次:
image.png


和对象打赌


✅ 除此之外还和对象打了一个赌,对象天天念叨减肥,我说如果在年底能够从120 -> 105斤,我就转10000🧧给她,否则明年就去领证😁,不知道会是怎么样,期待吧!反正都不亏。


总结


总的来说,2023上半年对我而言还算是充实的吧。在个人方面,我通过掘金文章专栏、个人音乐项目、前端性能优化学习以及Vue3源码学习,开拓了自己的知识与技能,养成持续学习的习惯很重要。在工作方面,虽然没有挑战、加薪。但是把出差当成公费旅游还行。


后续的规划:



  1. 对象工资已经比我高了,金9银10搏一搏,看能不能变🏍。

  2. 持续学习、多参与到开源
    作者:Lvzl
    来源:juejin.cn/post/7255189526775644215
    项目。

收起阅读 »

用Echarts打造自己的天气预报!

web
前言 最近刚刚学习了Echarts的使用,于是想做一个小案例来巩固一下。项目效果如下图所示: 话不多说,开始进入实战。 创建项目 这里我们使用vue-cli来创建脚手架: vue create app 这里的app是你要创建的项目的名称,进入界面我们选择安装...
继续阅读 »

前言


最近刚刚学习了Echarts的使用,于是想做一个小案例来巩固一下。项目效果如下图所示:


0.png


话不多说,开始进入实战。


创建项目


这里我们使用vue-cli来创建脚手架:
vue create app


这里的app是你要创建的项目的名称,进入界面我们选择安装VueRouter,然后就可以开始进行开发啦。


页面自适应实现


我们这个项目实现了一个页面自适应的处理,实现方式很简单,我利用了一个第三方的库,可以将项目中的px动态的转化为rem,首先我们要安装一个第三方的库
npm i lib-flexible
安装完成后,我们需要在 main.js中引入
import 'lib-flexible/flexible'
还要在项目中添加一个配置文件postcss.config.js,文件内容如下:


module.exports = {
plugins: {
autoprefixer: {},
"postcss-pxtorem": {
"rootValue": 37.5,
"propList": ["*"]
}
}
}

上述代码是一个 PostCSS 的配置示例,用于自动添加 CSS 属性的前缀和将像素单位转换为 rem 单位。


其中



  • autoprefixer 是一个 PostCSS 插件,用于根据配置的浏览器兼容性自动添加 CSS 属性的前缀,以确保在不同浏览器中的兼容性。

  • postcss-pxtorem 是另一个 PostCSS 插件,用于将像素单位转换为 rem 单位,以实现页面在不同设备上的自适应效果。在上述配置中,rootValue 设置为 37.5,这意味着 1rem 会被转换为 37.5px。propList 设置为 ["*"] 表示所有属性都要进行转换。


这样,我们在项目中任何一个地方写px,都会动态的转化成为rem,由于rem是一个中相对于根元素字体大小的CSS单位,可以根据根元素的字体大小进行动态的调整,达到我们一个也买你自适应的目的。


实时时间效果实现


在项目的左上角有一个实时显示的时间,我们是如何做到的呢?首先我们在数据源中定义一个loalTime字段,用来装我们的时间,然后可以通过 new Date() 函数返回当前的时间对象,但这个对象我们是无法直接使用的,需要通过toLocaleTimeString() 函数处理,将 Date 对象转换为本地时间的格式化字符串。


methods{
getLocalTime() {
return new Date().toLocaleTimeString();
},
}

仅仅是这样的话,我们获取的时间是不会动的,怎么让他动起来呢,答案是使用定时器:


created() {
setInterval(() => {
this.localTime = this.getLocalTime();
}, 1000);
},

我们使用了一个setInterval定时器函数,让他每秒钟触发一次,然后将返回的时间赋值给我们的数据源中的localTime,同时将他放在created这个生命周期中,确保一开始就能运行,这样,我们就得到了一个可以随当前时间变化的时间。


省市选择组件实现


这个功能自己实现较为麻烦,我们选择使用第三方的组件库,这里我们选择的是Vant,这是一个轻量级,可靠的移动端组件库,我们首先需要安装他


npm i vant@latest-v2 -S


由于我们使用Vue2进行开发,所以需要指定其版本,然后就是导入所以有组件:


import Vant from 'vant'; 
import 'vant/lib/index.css';
Vue.use(Vant);

由于我们只是在本地开发,所以我们选择导入所有组件,在正式开发中可以选择按需引入来达到性能优化的目的。


准备工作完毕,导入我们需要的组件:


<van-popup v-model="show" position="bottom" :style="{ height: '30%' }">
<van-area
title="标题"
:area-list="areaList"
visible-item-count="4"
@cancel="show = false"
columns-num="2"
@confirm="selectCity"
/>

</van-popup>

这里我们通过show的值来控制的组件的显示与否,点击确认按钮后,会执行selectVCity方法,该方法会将我们选择的省市返回,格式为一个包含地区编码和地区名称的一个对象数组。


天气信息的获取


我们获取天气的信息主要依靠高德地图提供的api来实现,高德地图为我们提供了很多丰富的地图功能,包括了实时天气和天气预报功能,首先我们要注册一下,成为开发者,并获取自己的密钥和key。


最后在index.html中引入:


<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: '你的密钥',
}
</script>
<script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=你的key"></script>

就可以进行开发了。我们首先需要在项目开始加载的时候显示我们当地的信息,所以需要获取我们的当前所处环境的IP地址,所以高德也为我们提供了方法:


initMap() {
let that = this;
AMap.plugin("AMap.CitySearch", function () {
var citySearch = new AMap.CitySearch();
citySearch.getLocalCity(function (status, result) {
if (status === "complete" && result.info === "OK") {
// 查询成功,result即为当前所在城市信息
// console.log(result.city);
that.getWeatherData(result.city);
}
});
});
},

通过AMap.CitySearch插件我们可以很容易的获取到我们当前的IP地址,然后将我们获取到的IP地址传入到getWeatherData() 方法中去获取天气信息,需要注意的是,因为要求项目一启动就获取信息,所以这个方法也是需要放在created这个生命周期中的。然后就是获取天气信息的方法:


getWeatherData(cityName) {
let that = this;
AMap.plugin("AMap.Weather", function () {
//创建天气查询实例
var weather = new AMap.Weather();

//执行实时天气信息查询
weather.getLive(cityName, function (err, data) {
console.log(err, data);
that.mapData = data;
});

//执行实时天气信息查询
weather.getForecast(cityName, function (err, data) {
that.futureMapData = data.forecasts;
console.log(that.futureMapData);

// 每天的温度
that.seriesData = [];
that.seriesNightData = [];
data.forecasts.forEach((item) => {
that.seriesData.push(item.dayTemp);
that.seriesNightData.push(item.nightTemp);
});

that.$nextTick(() => {
that.initEchart();
});
});
});
},

通过这个方法,我们只需要传入城市名就可以很轻松的获取到我们需要的天气信息,并同步到我们的数据源中,然后将其渲染到页面中去。


数据可视化的实现


面对一堆枯燥的数据,我们很难提起兴趣,这时候,数据可视化的重要性就体现出来了,数据可视化是指使用图表、图形、地图、仪表盘等可视化工具将大量的数据转化为具有可读性和易于理解的图像形式的过程。通过数据可视化,可以直观地呈现数据之间的关系、趋势、模式和异常,从而帮助人们更好地理解和分析数据。


而Echarts就是这样一个基于 JavaScript 的开源可视化图表库,里面有非常多的图表类型可供我们使用,这里我们使用比较简单的折线统计图来展示数据。


首先也是安装依赖


npm i echarts


然后就是在项目中引入


import * as echarts from "echarts";


然后就可以进行开发啦,现在页面中准备好一个容器,方便承载我们的图表


<div class="echart-container" ref="echartContainer"></div>


然后就是根据我们获取到的数据进行绘制:


initEchart() {
// 基于准备好的dom,初始化echarts实例
let myChart = echarts.init(this.$refs.echartContainer);

// 绘制图表
let option = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
xAxis: {
data: ["今天", "明天", "后天", "三天后"],
axisTick: {
show: false,
},
axisLine: {
lineStyle: {
color: "#fff",
},
},
},
yAxis: {
min: "-10",
max: "50",
interval: 10,
axisLine: {
show: true,
lineStyle: {
color: "#fff",
},
},
splitLine: {
show: true,
lineStyle: {
type: "dashed",
color: ["red", "green", "yellow"],
},
},
},
series: [
{
name: "白天温度",
type: "line",
data: this.seriesData,
},
{
name: "夜间温度",
type: "line",
data: this.seriesNightData,
lineStyle: {
color: "red",
},
},
],
};
myChart.setOption(option);
},

一个图表中有非常多的属性可以控制它的不同形态,具体的不过多阐述,可以查看Echarts的参考文档,然后我们就得到一个非常美观的折线统计图。同时不能忘记和省市区选择器进行联动,当我们切换省市的时候,手动触发一次绘制,并且将我们选择的城市传入,这样,我们就得到了一个可以实时获取全国各地天气的小demo。


以上就是主要功能的具体实现方法:代码地址


作者:严辰
来源:juejin.cn/post/7255161684526940220
>欢迎大家和我交流!

收起阅读 »

通过调试技术,我理清了 b 站视频播放很快的原理

web
b 站视频播放的是很快的,基本是点哪就播放到哪。 而且如果你上次看到某个位置,下次会从那个位置继续播放。 那么问题来了:如果一个很大的视频,下载下来需要很久,怎么做到点哪个位置快速播放那个位置的视频呢? 前面写过一篇 range 请求的文章,也就是不下载资源的...
继续阅读 »

b 站视频播放的是很快的,基本是点哪就播放到哪。


而且如果你上次看到某个位置,下次会从那个位置继续播放。


那么问题来了:如果一个很大的视频,下载下来需要很久,怎么做到点哪个位置快速播放那个位置的视频呢?


前面写过一篇 range 请求的文章,也就是不下载资源的全部内容,只下载 range 对应的范围的部分。


那视频的快速播放,是不是也是基于 range 来实现的呢?


我们先复习下 range 请求:



请求的时候带上 range:



服务端会返回 206 状态码,还有 Content-Range 的 header 代表当前下载的是整个资源的哪一部分:



这里的 Content-Length 是当前内容的长度,而 Content-Range 里是资源总长度和当前资源的范围。


更多关于 Range 的介绍可以看这篇文章:基于 HTTP Range 实现文件分片并发下载!


那 b 站视频是不是用 Range 来实现的快速播放呢?


我们先在知乎的视频试一下:


随便打开一个视频页面,比如这个:



然后打开 devtools,刷新页面,拖动下进度条,可以看到确实有 206 的状态码:



我们可以在搜索框输入 status-code:206 把它过滤出来:



这是一种叫过滤器的技巧:



可以根据 method、domain、mime-type 等过滤。




  • has-response-header:过滤响应包含某个 header 的请求




  • method:根据 GET、POST 等请求方式过滤请求




  • domain: 根据域名过滤




  • status-code:过滤响应码是 xxx 的请求,比如 404、500 等




  • larger-than:过滤大小超过多少的请求,比如 100k,1M




  • mime-type:过滤某种 mime 类型的请求,比如 png、mp4、json、html 等




  • resource-type:根据请求分类来过滤,比如 document 文档请求,stylesheet 样式请求、fetch 请求,xhr 请求,preflight 预检请求




  • cookie-name:过滤带有某个名字的 cookie 的请求




当然,这些不需要记,输入一个 - 就会提示所有的过滤器:



但是这个减号之后要去掉,它是非的意思:



和右边的 invert 选项功能一样。


然后点开状态码为 206 的请求看一下:




确实,这是标准的 range 请求。


我点击进度条到后面的位置,可以看到发出了新的 range 请求:



那这些 range 请求有什么关系呢?


我们需要分析下 Content-Range,但是一个个点开看不直观。


这时候可以自定义显示的列:


右键单击列名,可以勾选展示的 header,不过这里面没有我们想要的 header,需要自定义:



点击 Manage Header Columns



添加自定义的 header,输入 Content-Range:



这时候就可以直观的看出这些 range 请求的范围之间的关系:



点击 Content-Range 这一列,升序排列。


我们刷新下页面,从头来试一下:


随着视频的播放,你会看到一个个 range 请求发出:



这些 range 请求是能连起来的,也就是说边播边下载后面的部分。


视频进度条这里的灰条也在更新:



当你直接点击后面的进度条:



观察下 range,是不是新下载的片段和前面不连续了?


也就是说会根据进度来计算出 range,再去请求。


那这个 range 是完全随意的么?


并不是。


我们当前点击的是 15:22 的位置:



我刷新下页面,点击 15:31 的位置:



如果是任意的 range,下载的部分应该和之前的不同吧。


但是你观察下两次的 range,都是 2097152-3145727


也就是说,视频分成多少段是提前就确定的,你点击进度条的时候,会计算出在哪个 range,然后下载对应 range 的视频片段来播放。


那有了这些视频片段,怎么播放呢?


浏览器有一个 SourceBuffer 的 api,我们在 MDN 看一下:



大概是这样用的:



也就是说,可以一部分一部分的下载视频片段,然后 append 上去。


拖动进度条的时候,可以把之前的部分删掉,再 append 新的:



我们验证下,搜索下代码里是否有 SourceBuffer:


按住 command + f 可以搜索请求内容:



可以看到搜索出 3 个结果。


在其中搜索下 SourceBuffer:



可以看到很多用到 SourceBuffer 的方法,基本可以确认就是基于 SourceBuffer 实现的。


也就是说,知乎视频是通过 range 来请求部分视频片段,通过 SourceBuffer 来动态播放这个片段,来实现的快速播放的目的。具体的分段是提前确定好的,会根据进度条来计算出下载哪个 range 的视频。


那服务端是不是也要分段存储这些视频呢?


确实,有这样一种叫做 m3u8 的视频格式,它的存储就是一个个片段 ts 文件来存储的,这样就可以一部分一部分下载。



不过知乎没用这种格式,还是 mp4 存储的,这种就需要根据 range 来读取部分文件内容来返回了:



再来看看 b 站,它也是用的 range 请求的方式来下载视频片段:



大概 600k 一个片段:


下载 600k 在现在的网速下需要多久?这样播放能不快么?


相比之下,知乎大概是 1M 一个片段:



网速不快的时候,体验肯定是不如 b 站的。


而且 b 站用的是一种叫做 m4s 的视频格式:



它和 m3u8 类似,也是分段存储的,这样提前分成不同的小文件,然后 range 请求不同的片段文件,速度自然会很快。


然后再 command + f 搜索下代码,同样是用的 SourceBuffer:



这样,我们就知道了为什么 b 站视频播放的那么快了:


m4s 分段存储视频,通过 range 请求动态下载某个视频片段,然后通过 SourceBuffer 来动态播放这个片段。


总结


我们分析了 b 站、知乎视频播放速度很快的原因。


结论是通过 range 动态请求视频的某个片段,然后通过 SourceBuffer 来动态播放这个片段。


这个 range 是提前确定好的,会根据进度条来计算下载哪个 range 的视频。


播放的时候,会边播边下载后面的 range,而调整进度的时候,也会从对应的 range 开始下载。


服务端存储这些视频片段的方式,b 站使用的 m4s,当然也可以用 m3u8,或者像知乎那样,动态读取 mp4 文件的部分内容返回。


除了结论之外,调试过程也是很重要的:


我们通过 status-code 的过滤器来过滤除了 206 状态码的请求。



通过自定义列在列表中直接显示了 Content-Range:



通过 command + f 搜索了响应的内容:



这篇文章就是对这些调试技巧的综合运用。


以后再看 b 站和知乎视频的时候,你会不会想起它是基于 range 来实现的分段下载和播放呢?



更多调试技术可以看我的调试小册《前端调试通关秘籍》


作者:zxg_神说要有光
来源:juejin.cn/post/7255110638154072120

收起阅读 »

flutter 极简的网络请求 - Retrofit 文档记录

前言对于Retrofit插件说实话之前是不太了解的,后来偶然发现了它,感觉还是比较惊艳的。主要工作流程就是注解、生成,通过定义简化通用请求方法的繁杂工作。(ps: json_serializable、freezed 和 最新的Riverpo...
继续阅读 »

前言

对于Retrofit插件说实话之前是不太了解的,后来偶然发现了它,感觉还是比较惊艳的。主要工作流程就是注解、生成,通过定义简化通用请求方法的繁杂工作。(ps: json_serializablefreezed 和 最新的Riverpod也是类似的工作方式,但Riverpod理解要稍微复杂一些。)

优秀插件太多感觉都有点看不完了,但是一聊到能减少重()复()工()作()那高低肯定是要上车了。 (●'◡'●)

插件Git地址:retorfit.dart

一、Retrofit 文档记录

主要目的还是记录一下,方便后续使用的时候查看。

1、添加插件引用

dependencies:
dio: any
retrofit: '>=4.0.0 <5.0.0'
logger: any #for logging purpose
json_annotation: ^4.8.1

dev_dependencies:
retrofit_generator: '>=7.0.0 <8.0.0' // required dart >=2.19
build_runner: '>=2.3.0 <4.0.0'
json_serializable: ^6.6.2

2、定义请求使用

import 'package:dio/dio.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:retrofit/retrofit.dart';

part 'example.g.dart';

@RestApi(
// 请求域名
baseUrl: 'https://5d42a6e2bc64f90014a56ca0.mockapi.io/api/v1/',
// 数据解析方式,默认为json
parser: Parser.JsonSerializable,
)
abstract class RestClient {
// 标准的构建方式
// dio: 传入发起网络请求的对象
// baseUrl: 请求域名,优先级高于注解
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

// 1、添加请求方式注解,接口地址
// 2、定义返回值类型(可以是任意类型,也可以是定义好的model),请求方法名,请求参数(后面会提到)
@GET('/tasks')
Future<List<Task>> getTasks();
}

example.g.dart 是脚本生成的具体实现文件;

@RestApi(baseUrl:...) 添加注解及部分配置参数;

Future<List<Task>> getTasks(...) 定义请求的返回值、参数值、请求类型与接口地址;

这个文件请求方法配置,按规范书写就可以了。

3、执行编译脚本

# dart
dart pub run build_runner build

# flutter
flutter pub run build_runner build

// 个人更建议使用 watch 命令
// 该命令监听输入,可以实时编译最新的代码,不用每次修改之后重复使用 build 了
flutter pub run build_runner watch

4、基本使用

import 'package:dio/dio.dart';
import 'package:logger/logger.dart';
import 'package:retrofit_example/example.dart';

final logger = Logger();

void main(List<String> args) {
final dio = Dio(); // Provide a dio instance
dio.options.headers['Demo-Header'] = 'demo header'; // config your dio headers globally
final client = RestClient(dio);

client.getTasks().then((it) => logger.i(it));
}

5、更多的请求方式

  @GET('/tasks/{id}')
Future<Task> getTask(@Path('id') String id);

@GET('/demo')
Future<String> queries(@Queries() Map<String, dynamic> queries);

@GET('https://httpbin.org/get')
Future<String> namedExample(
@Query('apikey') String apiKey,
@Query('scope') String scope,
@Query('type') String type,
@Query('from') int from);

@PATCH('/tasks/{id}')
Future<Task> updateTaskPart(
@Path() String id, @Body() Map<String, dynamic> map);

@PUT('/tasks/{id}')
Future<Task> updateTask(@Path() String id, @Body() Task task);

@DELETE('/tasks/{id}')
Future<void> deleteTask(@Path() String id);

@POST('/tasks')
Future<Task> createTask(@Body() Task task);

@POST('http://httpbin.org/post')
Future<void> createNewTaskFromFile(@Part() File file);

@POST('http://httpbin.org/post')
@FormUrlEncoded()
Future<String> postUrlEncodedFormData(@Field() String hello);

6、在方法中额外添加请求头

  @GET('/tasks')
Future<Task> getTasks(@Header('Content-Type') String contentType);

-- or --

import 'package:dio/dio.dart' hide Headers;

@GET('/tasks')
@Headers(<String, dynamic>{
'Content-Type': 'application/json',
'Custom-Header': 'Your header',
})
Future<Task> getTasks();

官方后续文档就不在这里复述了,下面记录一下我自己的使用方式。

二、Retrofit 个人使用

如官方文档所述,Retrofit的使用本就十分简单,这里更多的是对请求使用的归纳。

1、创建请求体

创建文件api_client.dart,该文件主要是对请求方法的编写。

...
@RestApi()
abstract class ApiClient {
factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
// 定义请求方法
...
}

2、创建dio请求拦截

创建文件interceptor.dart,文件主要是对发起请求响应结果的通用处理,简化我们在使用过程中,重复的处理公共模块。


class NetInterceptor extends Interceptor {
NetInterceptor();

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 在这里可以配置请求头,设置公共参数
final token = UserService.to.token.token;
if (token.isNotEmpty) {
options.headers['Authorization'] = token;
}
handler.next(options);
}

@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// 预处理请求返回结果,处理通用的错误信息
// 包括不限于数据格式错误、用户登录失效、 需单独处理的额外约定错误码
Map dataMap;
if (response.data is Map) {
dataMap = response.data;
} else if (response.data is String) {
dataMap = jsonDecode(response.data);
} else {
dataMap = {'code': 200, 'data': response.data, 'message': 'success'};
}

if (dataMap['code'] != 200) {
if (dataMap['code'] == 402 || dataMap['code'] == 401) {
// _ref.read(eventBusProvider).fire(AppNeedToLogin());
}
handler.reject(
DioError(
requestOptions: response.requestOptions,
error: dataMap['message'],
),
true,
);
return;
}
response.data = dataMap['result'];
handler.next(response);
}
}

3、独立请求参数类

创建文件params.dart,文件主要目的是存放请求类型参数(毕竟官方推荐使用具体类型作为结构参数,非Map),当然也可以直接使用我们请求结果的数据模型作为请求参数(但是不是所有的方法都合适),简单的参数还是不用写这,个人感觉还是太复杂了,不够简洁。

具体没啥说的,直接使用json_serializable就可以了,当然也可以手写,这里推荐一下 VS Code插件 - Dart Data Class Generator,直接定义好属性之后直接通过提示扩展对应的方法就好了。

import 'package:json_annotation/json_annotation.dart';
part 'params.g.dart';

@JsonSerializable()
class TokenParams {
@JsonKey(name: 'client_id')
final String clientId;

TokenParams(this.clientId);
factory TokenParams.fromJson(Map<String, Object?> json) =>
_$TokenParamsFromJson(json);
Map<String, dynamic> toJson() => _$TokenParamsToJson(this);
}

4、添加请求桥接(独立基础请求配置)

创建实际请求类repository.dart,简化实际使用


class NetRepository {
/// 独立请求体
static ApiClient client = ApiClient(
Dio(BaseOptions())
..interceptors.addAll([
LogInterceptor(
requestBody: true,
responseBody: true,
),
NetInterceptor(),
]),
baseUrl: _devDomain.host,
);

/// 如果域名不一致可以独立创建,方便区分
static ApiClient user...
static ApiClient company...

}

final _devDomain = AppDomain(
host: 'https://api.apiopen.top/api',
pcHost: 'http://www.xxx.com ',
);

// 定义域名配置,用类的形式只是为了更好的管理和使用
// 当然这里也可以直接换成枚举、常量字符串等等,看个人编写习惯
class AppDomain {
/// 接口域名
final String host;

/// 电脑端地址
final String pcHost;

/// final String host1;
/// final String host2;
/// ...

AppDomain({
required this.host,
required this.pcHost,
});
}

5、使用案例

这里是搬用上一篇 一站式刷新和加载 的使用场景,其他地方放使用类似。


@override
FutureOr fetchData(int page) async {
try {
final data = await NetRepository.client.videoList(page, 20);
await Future.delayed(const Duration(seconds: 1));
if (tag) {
endLoad(data.list as List<VideoList>, maxCount: data.total);
} else {
tag = true;
endLoad([], maxCount: data.total);
}
} catch (e) {
formatError(e);
}
}

总结

如果使用这个请求库的话,可以极大的简化我们样板式代码的书写,还是值得推荐的。 一切的一切就是都是为了更简单,也算是为了尽量少写没用的代码而努力。

毕竟不想当将军的士兵不是好士兵,不想写代码的程序猿才是好猿。 ( ̄▽ ̄)"

附Demo地址: boomcx/template_getx


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

一场关于”职责边界不明确“引起的争论

起因有人的地方就会有江湖,有江湖的地方就会有人情。程序员这个职业,虽然天天面对电脑需要足够理性,但是公司技术边界问题在IT行业经常是模糊和感性的。对于大公司而言,规范和制度可能定义得比较明确,这样的争论会相对较少;对于小公司而言,部门和团队的职责定义得没有这么...
继续阅读 »

起因

有人的地方就会有江湖,有江湖的地方就会有人情。

程序员这个职业,虽然天天面对电脑需要足够理性,但是公司技术边界问题在IT行业经常是模糊和感性的。对于大公司而言,规范和制度可能定义得比较明确,这样的争论会相对较少;对于小公司而言,部门和团队的职责定义得没有这么清晰,存在很多这种模糊的边界;人情不好,此时跨部门合作就会经常出现这种扯皮的事情。

实际案例如下:

产品提出了一个需求:网约车行业,乘客预约用车时,需要对接到预约订单的司机进行履约监控,在司机忘记履约或者履约不及时的时候,系统要能够识别,并通知司机。
团队划分:派单团队和司机团队。
派单团队:从领域划分的角度,派单侧负责给乘客找司机。找到司机后,这个司机能不能去服务这个乘客,应该属于司机团队来监控,而且这个属于对司机行为的履约监控,跟派单应该没有关系。
司机团队:从领域划分的角度,司机侧只负责司机维度的管理,不负责司机和乘客双边关系的监控,司机接到订单后,属于司机和订单双边履约关系的监控,应该还是派单团队负责。

争论

对于司机的履约监控,这本身是一个非常大的方向。由哪边的团队负责开发,也意味着后续相关的维护和开发都会有较大的工作量。

从目前领域划分的角度来看,这块其实属于模糊的边界。

  • 从司机的视角来看: 就是对司机的履约行为,进行监控。
  • 从派单的视角来看: 就是对派单后,司机和乘客的关系是否合理,进行监控。

那双方各执一词,谁也无法说服谁,最后衍生到组织结构的问题。

  • 司机侧:不能只从派单的视角来看,要从整体来看,但是这个整体是什么却是模糊的。又扯到这个需求对司机团队没有价值,对派单团队价值更大。。。
  • 派单侧:产品文档已经定义的很明确,这个属于司机侧的监控,而且这个完全属于派单后的司机行为,跟派单确实毫无关系。

当两边都争执不下时,就只能请架构部门的人来进行协调。其实架构部门的人,对两边的业务都不是熟悉,他的立场就代表那个所谓的“江湖”。

最后架构部门的人站在了司机侧的立场:

  • 派单侧要对派单结果负责,订单派完后司机能不能履约,属于派单后续的闭环行为。

这个结论最终也没有完全说服我,因为司机所有的后续结果都是派单这个底层能力支持的,这么说司机侧后续的所有监控都要派单来负责。

反思

关于边界模糊的问题,有没有更好的解决思路?

  • 人情上解决,有人的地方就有江湖。多建立良好的合作关系,从日常的工作中去努力,在力所能及的范围内多给别人帮助。
  • 制度上解决,向上推动这种模糊边界的划分问题,帮助公司建立更规范的制度,这种方式其实体现了个人能力的不足。
  • 思想上解决,放大思考,模糊的边界后续对哪个团队有更大的收益;或者说后续是否可以进一步成为团队的核心能力。

总结

基于不确定性的问题,此次自己得到经验和教训。

  • 职场上永远要保持理性,争吵不会解决问题。
  • 模糊的问题,更需要你提前去思考,为自己的团队争取利益。
  • 职场上要多去思考,慎重表达,没有思考清楚,就不要表达。
  • 更多的去关注人,表达者最重要的是要关注对象的心理。

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

搞懂ThreadLocal

前言ThreadLocal可以说是面试的常客了,虽然在日常开发中用到的次数并不多,但因为其在Handler、ActivityThread中都发挥着重要的作用,使得面试官在问其他问题的时候会顺便考查一下ThreadLocal。为了彻底理清其逻辑,这里系统的整理一...
继续阅读 »

前言

ThreadLocal可以说是面试的常客了,虽然在日常开发中用到的次数并不多,但因为其在Handler、ActivityThread中都发挥着重要的作用,使得面试官在问其他问题的时候会顺便考查一下ThreadLocal。为了彻底理清其逻辑,这里系统的整理一遍ThreadLocal结构,帮助自己理解。

一、概述

在分析ThreadLocal之前先不要看源码,我们先来大致建立起关于ThreadLocal整体的认知。

TheadLocal工具涉及到的几个类:Thread、ThreadLocal、ThreadLocalMap,对于它们之间的关系我们可以这样简单理解:每个Thread对象都拥有一个独属于自己的Map容器-ThreadLocalMap,这里我们先把它理解为HashMap,该容器的作用是存储和维护独属于本线程的值,而它的key值就是TheadLocal对象,value值就是我们需要存储的Object。

这就是ThreadLocal工具的结构,所以在ThreadLocal工具中真正重要的是ThreadLocalMap,它才是存储线程独有数据的地方。

图片出处

二、ThreadLocal是什么

看了概述之后你其实已经对ThreadLocal有了一个大致的认知了,但是仅仅这些还不够,还需要更加深入的了解ThreadLocal。

ThreadLocal,即线程的本地变量,设计目的是为了让线程中拥有属于自己的变量,主要用于线程间数据隔离,是用来解决线程安全性问题的一个工具。它相当于为每一个线程都开辟了一块内存空间,用来存储共享变量的副本,每个线程访问共享变量时只能去访问和操作自己共享变量的副本,从而避免多线程竞争同一个共享数据,保证了在多线程环境下各个线程里的变量相对独立于其他线程内的变量。

在这里所谓开辟的内存空间就是 ThreadLocalMap,共享变量就是 ThreadLocal,共享变量的副本就是存储到ThreadLocalMap中的key。

//创建一个ThreadLocal共享变量
static final ThreadLocal<String> sThreadLocal = new ThreadLocal<String>();

创建一个ThreadLocal修饰的共享变量,当线程访问该共享变量时,这个线程就会在自己的成员变量ThreadLocalMap中保存一份数据副本,多个线程操作这个变量的时候,实际是在操作自身线程本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。

三、Thread源码分析

class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

每个线程都有一个成员变量-ThreadLocalMap,但是该变量并没有设置引用,也就是说内存并没有为它分配空间,它的引用实际是在ThreadLocal#set方法中设置的,这样的话,虽然每个Thread对象都会有一个ThreadLocalMap变量,但是只有在使用ThreadLocal工具实现线程数据隔离的时候才会实例化,不使用则不会实例化,避免了内存占用。

四、ThreadLocal源码分析

既然每个Thread对象都有一个属于自己的容器ThreadLocalMap,那么对于数据的管理无外乎添加、获取、删除,也就是就是set、get、remove,但是这些操作并不是线程直接对ThreadLocalMap进行,而是通过ThreadLocal来间接实现的,ThreadLocalMap是ThreadLocal的静态内部类

1、ThreadLocal#set()

    public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//this表示当前ThreadLocal对象
else
createMap(t, value);
}

//获取thread对象的成员变量ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

首先会获取当前线程的变量ThreadLocalMap,如果该变量为null,那么会调用createMap方法初始化ThreadLocalMap,如果不为null,则调用ThreadLocalMap#set方法将数据存储起来。

2、ThreadLocal#get()

    public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

private T setInitialValue() {
T value = initialValue();//initialValue方法会返回null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

与set方法同理,首先会获取ThreadLocalMap,根据ThreadLocalMap是否为null来进行操作。如果不为null,则根据key值-ThreadLocal对象直接从ThreadLocalMap中取值并返回。如果为null,则调用setInitialValue方法,该方法逻辑几乎和set方法相同,不同的是value值为null,所以最终返回的也是null。

从上面的方法中我们可以看到不管是set方法还是get方法,都会先获取当前的Thread对象,然后获取Thread对象的成员变量ThreadLocalMap,最终对Map进行操作,这样也就保证了所有操作都是作用在Thread对象的同一个ThreadLocalMap上。

五、ThreadLocalMap

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}

ThreadLocal没有直接使用HashMap而是自己重新开发了一个 map,最主要的作用是让它的key为虚引用类型,这样当ThreadLocal对象销毁时,多个持有其引用的线程不会影响它的回收。 ThreadLocalMap是一个很像HashMap的数据结构,但他并没有实现 Map接口,而且它的 Entry是继承WeakReference的,也没有 next 指针,所以不存在链表。对于hash冲突,采用的是开放地址法来进行解决。 ThreadLocaMap的扩容机制也不同于HashMap,ThreadLocalMap的扩容阈值是长度的2/3,当表中的元素数量达到阈值时,不会立即进行扩容,而是会触发一次rehash操作清除过期数据,如果清除过期数据之后元素数量大于等于总容量的3/4才会进行真正意义上的扩容。

六、ThreadLocal的内存泄漏

我们都知道内存泄漏必然和对象的引用有关,先来看一下ThreadLocal的引用关系图。

image.png

Thread中的成员变量ThreadLocalMap,它里面的key指向ThreadLocal成员变量,并且是一个弱引用。

1、为什么Entry的key使用弱引用?

如果 Entry 的key为强引用,则会导致ThreadLocal对象在被创建它的线程销毁时,由于ThreadLocalMap的持有而导致ThreadLocal对象无法被回收,进而导致严重的内存泄漏问题,因此Eetry的key被声明为弱引用来避免这种问题

2、ThreadLocal弱引用下为什么会导致内存泄漏?

所谓弱引用,是指对象允许在这种引用关系存在的情况下被GC回收。

前面也说过,ThreadLocalMap中的key是一个弱引用,当ThreadLocal变量被设置为null,即此时ThreadLocal对象仅有一个弱引用-key,而没有任何外部强引用关系。发生一次系统GC后,ThreadLocal对象会被GC回收,key的引用就变成一个null,导致这部分内存永远无法被访问,造成内存泄漏的问题。因此这些value就会一直存在一条强引用链: Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 无法回收,造成内存泄漏。

所以说,从ThreadLocal本身的设计来看,是一定存在内存泄漏的。有的朋友可能会说不会出现内存泄漏啊,如果线程被回收了,线程里面的成员变量也都会被回收,也就不存在内存泄漏了,这是不对的。首先,在线程执行期间,始终有一块无法访问的内存被占用。其次,我们在实际开发中多数情况下使用线程池,而线程池是重复利用的,线程池不会销毁线程,那么线程中会一直存在这种类型的value,导致内存泄漏。

image.png

3、如何避免内存泄漏

既然已经知道弱引用下内存泄漏的原因,那么解决方案也就很清晰了,将不再被使用的Entry及时从线程的ThreadLocalMap中删除,或者延长ThreadLocal的生命周期。

而删除不再使用的Entry有两种方式。

  • 主动清除:使用完ThreadLocal后,手动调用ThreadLocal#remove()方法,将Entry从ThreadLocalMap中删除。
  • 条件触发清除:当然,为了避免内存泄漏的问题,ThreadLocal也做了一些工作。ThreadLocalMap拥有自动清除机制去清除过期Entry,当调用ThreadLocalMapget()、set()对数据进行读写时,都会触发对Entry里面key为null的数据的清除。

我们也能看到系统自动清除是需要一定的触发条件的,不能完全避免内存泄漏,所以正确的做法是调用ThreadLocal#remove()主动清除。

还可以将ThreadLocal声名为private static,使它的生命周期与线程保持一致,保证一直存在与之关联的强引用。

总的来说,有两个方法可以避免内存泄漏

  1. 每次使用完ThreadLocal之后,主动调用remove()方法移除数据。
  2. 扩大成员变量ThreadLocal的作用域,把ThreadLocal声名为private static,使它无法被GC回收。这种方法虽然避免了key为null的情况,但是如果后续线程不再继续访问这个key,也就会导致这个内存一直占用不被释放,最后造成内存溢出的问题。

所以说来说去,最好的方式还是在使用完之后,调用remove方法去移除掉这个数据

七、总结

  • ThreadLocal为每一个线程创建一个ThreadLocalMap,用于存储独属于线程自己的数据。
  • ThreadLocal的设计并不是为了解决并发问题,而是解决变量在线程内部的共享问题,线程内部可以访问独属于自己的变量。
  • 因为每个线程都只会访问自己ThreadLocalMap 保存的变量,所以不存在线程安全问题。
  • 为了避免ThreadLocal造成的内存泄漏,最好在每次使用完ThreadLocal之后,主动调用remove()方法移除数据。

个人能力经验有限,文章如有错误,还望指正。


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

Git 回退到指定版本

Git
在 Git 中,我们可以使用多种方法回退代码到指定版本,包括使用 reset 命令、使用 revert 命令、使用 checkout 命令等。下面分别介绍这些方法。方法一: 使用 git r...
继续阅读 »

在 Git 中,我们可以使用多种方法回退代码到指定版本,包括使用 reset 命令、使用 revert 命令、使用 checkout 命令等。下面分别介绍这些方法。

方法一: 使用 git reset 命令

命令

git reset

命令可以将当前分支的 HEAD 指针指向指定的提交,从而回退代码到指定版本。

该命令有三种模式:--soft--mixed 和 --hard。它们的区别在于回退代码的程度不同。

  • --mixed (默认):将 HEAD 指针和暂存区都回退到指定提交,但不改变工作区的内容。
  • --soft 仅将 HEAD 指针回退到指定提交,不改变暂存区和工作区的内容。
  • --hard 将 HEAD 指针、暂存区和工作区都回退到指定提交,会丢失最新的代码修改,慎用。

示例

# 查看提交历史
git log

# 回退到指定提交(使用 --soft 模式)
git reset --soft <commit>

# 查看状态
git status

# 提交回退后的代码
git commit -m "回退到 <commit>"

# 推送到远程仓库
git push origin <branch>

我们首先使用 git log 命令查看提交历史,找到要回退到的提交的 SHA-1 值。

然后使用 git reset 命令回退代码到指定提交,

这里使用了 --soft 模式,这样暂存区和工作区的内容不会改变,只是 HEAD 指针指向了指定提交。

接着我们使用 git status 命令查看当前状态,确认回退操作是否正确。

最后,我们使用 git commit 命令提交回退后的代码,并使用 git push 命令将代码推送到远程仓库。

方法二:使用 git revert 命令

命令

git revert 命令可以将指定提交的修改反向应用到当前分支上,相当于撤销指定提交的修改。

这种方式比使用 git reset 命令更加安全,因为它不会改变提交历史,而是创建一个新的提交来撤销之前的修改。

示例

# 查看提交历史
git log

# 撤销指定提交
git revert <commit>

# 提交撤销操作
git commit -m "回退到版本 <commit>"

# 推送到远程仓库
git push origin <branch>

我们首先使用 git log 命令查看提交历史,找到要回退的提交的 SHA-1 值。

然后使用 git revert 命令撤销指定提交的修改,这样会创建一个新的提交来撤销之前的修改。

接着我们使用 git commit 命令提交撤销操作,

并使用 git push 命令将代码推送到远程仓库。

方法三:使用 git checkout 命令

命令

git checkout 命令可以将当前分支的 HEAD 指针指向指定的提交,并将工作区的内容替换成指定提交的内容。这种方式不改变提交历史,但会直接覆盖工作区的内容,慎用。

示例

# 查看提交历史
git log

# 切换到指定提交
git checkout <commit>

# 提交回退后的代码
git commit -m "回退到版本 <commit>"

# 切回到原来的分支
git checkout <branch>

# 推送到远程仓库
git push origin <branch>

我们首先使用 git log 命令查看提交历史,找到要回退的提交的 SHA-1 值。

然后使用 git checkout 命令切换到指定提交,这样工作区的内容就会被直接替换成指定提交的内容。

接着我们使用 git commit 命令提交回退后的代码,并使用 git checkout 命令切回到原来的分支。

最后,我们使用 git push 命令将代码推送到远程仓库。

改之后git push上去远程仓库的命令行 以及 报错的相关解决办法

当我们改完代码后,想要将代码推送到远程仓库时,可以使用以下命令:

# 推送当前分支到远程仓库
git push origin <branch>

其中,<branch> 表示当前分支的名称,例如 master。这个命令会将本地分支的提交推送到远程仓库,并将远程分支更新为与本地分支一致。

如果在推送代码时出现错误,可以根据错误提示进行相应的解决办法。

常见的错误及其解决办法如下:

  • error: failed to push some refs to 'git@github.com:<username>/<repository>.git':这个错误通常是由于本地分支和远程分支的提交历史不一致导致的。解决办法是先执行 git pull 命令将远程分支的代码拉取到本地,然后再执行 git push 命令推送代码。

  • error: src refspec <branch> does not match any:这个错误通常是由于本地分支不存在或者拼写错误导致的。解决办法是先执行 git branch 命令查看本地分支列表,确认分支名是否正确,如果不存在则需要先创建分支。

  • error: failed to push some refs to 'git@github.com:<username>/<repository>.git':这个错误通常是由于权限不足导致的。解决办法是确认当前用户是否有权限推送代码到远程仓库,如果没有则需要联系管理员进行授权。

总结

总之,回退代码和推送代码都是 Git 中非常常见的操作,掌握这些操作可以帮助我们更加高效地进行开发和协作。


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

我在数据中台建设和落地的一些经验总结

软件工程师罗小东,多年平台架构设计和落地经验,这里从智慧型项目、数字化项目进行数据治理建设的一些经验总结。概述针对于中小型团队和当前接触到的大部分项目来说,很少有非常大的数据治理需求,特别是互联网型的PB级数据。在大部分情况下,数据量在TB级或亿级级别较多。相...
继续阅读 »

软件工程师罗小东,多年平台架构设计和落地经验,这里从智慧型项目、数字化项目进行数据治理建设的一些经验总结。

概述

针对于中小型团队和当前接触到的大部分项目来说,很少有非常大的数据治理需求,特别是互联网型的PB级数据。在大部分情况下,数据量在TB级或亿级级别较多。相对于PPT级别的方法论,会更加注重于实际运用,为了应对这些场景,在以下几个方面进行了考虑和优化:

  • 是否真的需要建立一个Hadoop体系的数据仓库

  • 针对于中小型客户数据治理需求怎么建设

  • 怎么样针对当前的项目进行数据资源管理

  • 后期的数据治理和各个数据治理维护怎么做

在真正理解项目需求、精细化管理以及灵活选择数据治理工具和技术的基础上,能够更好地应对不同场景下的数据治理需求。不同项目不同架构,我有我思。

过程建设

许多客户都有数字化建设的需求,但不同的场景需要使用不同的技术方案,在具体的建设过程中,整理的一些思路:

  • 首先要充分了解客户的业务场景和需求,从而选择最适合的技术方案。

  • 在建设过程中,要注重数据质量和服务,以确保数据的准确性和能力体现。

  • 合理规划数据治理流程,包括数据采集、清洗、转换、存储等环节,并通过数据可视化手段展示数据治理效果,提高数据治理成效。

  • 对于不同的项目规模和预算成本,选择不同方案,优化算法和调整计算引擎,减少资源和成本。

针对不同的客户场景,规划合理的数据治理流程。

是否真的需要建立一个全套的数据仓库体系

针对于不同的场景,对于数据治理,需要根据具体场景来选择合__适的方案

前期的搭建方式

目前在搭建数据治理平台时,开始我们使用的是CDH做为数据仓库底座,通常使用Hadoop体系的数据仓库平台,并按照ODS/DWD/ADS等层级进行划分,通过Kettle/Filebeat/Sqoop等方式抽取数据进行离线计算,使用Hive做为数据仓库,我们的工程师在这块上也有多年的治理经验,计算引擎使用的偏向于Spark,数据建模和维护也是按通用的数据标准处理,这个有前期多个项目里面基本上都是,有一些项目会运行在K8S上。

这个过程消耗的资源较多,而且计算引擎和计算过程比较统一,特别是Spark计算的时候,消耗大量的内存资源。而对于一般中小型项目,或者一般的客户来说,这个资源的建设会成本过高,特别是在数据治理这块并不是要求特别高的时候。

客户数据治理成本高

一些客户可能并不理解数据治理的成本和价值,除了政务型项目或不缺费用的项目,很难落地,没有达到预期的数据运营效果。

比如一个智慧社区项目,在这块上的数据仓库主要存储的数据在几个方面,用户行为、IOT数据采集、还有视频流数据的存储(只存储主键祯数据),另外就是一些业务系统的数据采集存储,针对于以上数据的分析,与AI结合,提供出API服务能力,在这些数据中,超过一定生命周期的会做清理,最后评估出来10年左右180T存储,而这个过程中,大部分是冷数据。

最后建设使用的方案是云厂家的一体机来进行管理,但是这个成本是极高的,类似于这样的数据场景,遇到的比较多,最后在考虑一个问题,是否需要这么重的数据仓库。

针对于中小型客户数据治理需求怎么建设的

建立一个轻量级的数据场景,以更好地满足不同项目的需求。建设轻量级数据治理平台

建设轻量级数据治理平台,是优化数据管理和维护成本的方法之一。目前大数据套件较多,学习成本较高,对中小型团队而言,这一成本占比较大。因此,需要采取有效措施降低人员培训成本和管理维护成本。

将多个工具整合为轻量级数据中台,使用minio分布式存储、Clickhouse数据仓库、kettle抽取工具和kafka数据总线等技术统一数据治理,适配各类规模的企业需求。在数据清理和转换后,将数据存储到ODS层,非结构化和半结构化数据存储在分布式存储和ES中,并根据生命周期规划定期清理不必要的数据,只保留有价值的数据和流程相关数据。

此外,针对人员培训,设计系统化的培训课程和多种灵活的培训方式,以提高员工的数据管理和分析能力。对于团队管理和维护,可以建立数据治理的文化氛围,鼓励全员参与,同时引入自动化工具和脚本,减少人工操作和管理成本。

通过以上措施,项目可以建设出高效、灵活的数据治理平台,降低人员培训和管理成本,提高数据治理能力和业务价值体现 ,实现项目的业务需求和决策目标。

怎么样针对当前的项目进行数据资源管理

建设通用的数据治理能力组件和平台组件,以便根据具体项目需求进行选择和组合,实现对数据资源的有效管理。

针对当前的项目进行数据资源管理,可以建设一套通用的数据治理能力组件和平台组件。这些组件可用于多种场景下的数据治理工作,如:

  • 数据上报服务:供政务、个人、单位等通用型用户使用的通用数据采集上报平台,支持非技术型人员和部门进行数据入仓。

  • 数据总线服务:连接数据平台中不同组件和子系统的核心组件,实现数据的快速传输和交换,并统一集成数据主题管理。

  • 主数据管理服务:帮助企业确保数据质量、提高业务流程效率,并为数据分析和决策提供支持,促进企业内部数据的标准化、管理和共享。

  • 数据集成服务:提供在线设置ETL作业、转换任务的定时运行策略,监控任务的执行情况,查看任务执行日志的功能,强有力地支撑后续的数据开发、数据挖掘。

  • 数据开发服务:向数据开发工程师提供拖拉拽控件的方式,设计复杂的工作流有向无环图,挖掘出有商业价值的数据。

  • 数据安全网关:提供数据交换、数据共享、数据开放的平台,包含网关接口安全、接口权限认证、黑名单管理、Oauth2接口认证等功能,向组织内各个部门提供支持。

这些数据治理能力组件和平台组件可根据具体项目需求进行选择和组合,实现对数据资源的有效管理,我们采用灵活的数据治理方案,根据项目大小和需求,选择相应的数据治理工具和技术。

在提供工具的同时,针对于业务的个性化要求和业务开发需求,比如报表、大屏、还有数据服务使用等,当前是让ISV团队进行处理,而这个过程由中台团队提供技术支持和培训,而数据治理套件不对客户。

后期的数据治理和各个数据治理维护怎么做

建立一套完善的数据治理流程和规范,包括数据质量控制、数据安全保护、数据持续更新等方面的要求

实现数据治理和各个数据治理维护的目标,包括数据流程标准化、人员技术培训、数据指标采集等。在实际应用过程中,需要根据企业的具体需求和情况,适当调整和优化数据治理策略,以提高数据质量和效率,为项目的发展提供有力支撑。

  • 数据流程标准化:通过数据总线服务连接数据平台中的不同组件和子系统,以便实现数据的快速传输和交换,并统一集成数据主题管理。建立标准化的数据流程,包括数据采集、清洗、存储、转换等环节,并确保每个环节都符合相关标准和规范。

  • 人员技术培训:利用主数据管理服务对企业内部数据进行标准化、管理和共享,确保数据质量和提高业务流程效率。同时,为各个层次的员工提供有针对性、系统化的培训课程,提高他们的数据管理和分析能力。

  • 数据指标采集:使用数据集成服务在线设置ETL作业和转换任务的定时运行策略,监控任务的执行情况和查看任务执行日志的功能。确保多种数据格式和来源的数据经过清洗、转换后能够及时有效地送达组织的数据仓库,并为后续的数据开发和挖掘提供支持。

  • 数据治理目标达成:使用数据开发服务向数据开发工程师提供拖拉拽式的控件,设计复杂的工作流图,挖掘出有商业价值的数据,帮助企业实现对数据的全面管控和治理。同时,使用数据安全网关进行数据交换、共享和开放的管理,确保数据的安全性和防止潜在的风险。

同时实现对数据的全面管控和治理,确保数据的质量和安全,提高数据开发和分析的效率和准确性,从而更好地支撑企业的业务需求和决策,提供出数据服务和治理。

总结

数据治理是数字化建设中非常重要的一环。在进行数据治理时,我们需要根据不同的业务场景和需求,选择最适合的数据治理方案,包括选择不同的组件组装和数据存储方式等。对于轻量级数据管理平台和重量级数据管理平台,我们可以针对具体情况进行选择,权衡成本与效益,以满足客户实际需求。在整个数据治理过程中,我们还需要注重客户成本的管理,确保项目的落地和实际效果,并且不断优化数据治理流程,需要积极参与业务需求分析和技术选型,确保数据治理方案符合客户需求和行业标准。

过程考虑不同的场景选择不同的数据治理方案和组件组装,根据实际情况选择轻量级或重量级数据中台,注重客户成本管理和实际效果,以满足客户需求并推动数字中台建设。

以上为在大中小型项目中的数据治理经验输出,提供一些参考。


作者:软件工程师_罗小东
链接:https://juejin.cn/post/7238978524030861371
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一次查找分子级Bug的经历,过程太酸爽了

bug
在软件开发的世界里,偶尔会出现一些非常隐蔽的 Bug,这时候工程师们像探险家一样,需要深入代码的丛林,寻找隐藏在其中的“幽灵宝藏”。前段时间,我和我的团队也踏上了这样一段刺激、有趣的探险之旅。最近繁忙的工作告一段落,我总算轻松下来了,想趁这个机会,跟大家分享我...
继续阅读 »

在软件开发的世界里,偶尔会出现一些非常隐蔽的 Bug,这时候工程师们像探险家一样,需要深入代码的丛林,寻找隐藏在其中的“幽灵宝藏”。前段时间,我和我的团队也踏上了这样一段刺激、有趣的探险之旅。

最近繁忙的工作告一段落,我总算轻松下来了,想趁这个机会,跟大家分享我们的这次“旅途”。

01 引子

我是 ShowMeBug 的 CEO 李亚飞,是一个古老的 Ruby 工程师。由于 2019 年招聘工程师的噩梦经历,我立志打造一个真实模拟工作场景的 IDE,用来终结八股文、算法横行的技术招聘时代。

这个云上的 IDE 引擎,我称之为轻协同 IDE 引擎——因为它不是为了繁杂重度的工作场景准备的,而是适应于大部分人的习惯、能快速上手熟悉、加载速度快、能协同(面试用)、低延迟感,让用户感受非常友好 

多环境启动与切换 

为了达成秒级启动环境的性能要求,我们设计了一套精巧的分布式文件系统架构,其核心是一个可以瞬间复制大量小文件的写时复制 (COW) 技术。IO 吞吐能达到几万人同时在线,性能绝对是它的一大优势。

我们对此信心满满,然而没想到,很快就翻车了。

02 探险启程

2023 年 1 月,北方已经白雪皑皑,而深圳却仍难以感受到冬天的寒意。

我和我的团队在几次打开文件树的某个文件时,会显得有点慢——当时没有人在意,按照常规思路,“网速”背了这个锅。事后我们复盘才发现,这个看似微不足道的小问题,其实正是我们开始这次探险之旅的起点。

1 月底,南方的寒意缓缓侵入。这时候我们的轻协同 IDE 引擎已经开始陆续支持了 Vue2、Vue3、React、Django、Rails 等框架环境,一开始表现都很棒,加载和启动速度都很快。但是,跑了一段时间,我们开始察觉,线上环境就出现个别环境(Rails 环境)启动要 20-30s 才能完成

虽然其他环境仍然保持了极快的加载和启动速度,但敏锐的第六感告诉我,不行,这一定有什么猫腻,如果不立即行动,势必会对用户体验带来很不好的影响。于是,我开始安排团队排查眼前这个不起眼的问题,我们的探险之旅正式开始。

03 初露希望

湿冷的冬季,夜已深,我和我们的团队依旧坐在电脑前苦苦探索,瑟瑟发抖。

探险之旅的第一站,就是老大难的问题:定位Bug。目前只有某一个环境启动很慢,其他的环境都表现不错。大家想了很多办法都没有想明白为什么,甚至怀疑这个环境的模板是不是有问题——但把代码放在本地启动,最多就2秒。

哎,太诡异了。我们在这里卡了至少一周时间,不断追踪代码,分析日志文件,尝试各种方案,都没有弄清楚一个正常的程序启动为什么会慢。我们一度陷入了疲惫和焦虑的情绪中。

Debug 是种信仰,只有坚信自己能找到 Bug,才有可能找到 Bug。

软件开发界一直有一个低级 Bug 定律:所有诡异的问题都来自一个低级原因。在这“山重水复疑无路”之际,我们决定重新审视我们的探险路径:为什么只有 Rails 更慢,其他并不慢?会不会只是一个非常微小的原因而导致?

这时候,恰好有一个架构师朋友来访,向我们建议,可以用 perf 火焰图分析看看 Rails 的启动过程。

perf火焰图实例

当我们用 perf 来分析时,惊讶地发现:原来 Rails 的启动要加载更多的文件! 紧接着,我们又重新用了一个文件读写监控的工具:fatrace,通过它,我们看到 Rails 每次启动需要读写至少 5000 个文件,但其他框架并不需要。

这才让我们突然意识到,会不会是文件系统读写速度不及预期,导致了启动变慢。

04 Bug现身

为了搞清楚是不是文件系统读写速度的问题,我急需一个测试 IO 抖动的脚本。我们初步估算一下,写好这个脚本需要好几个小时的时间。

夜已深,研发同学都陆续下班了。时间紧迫!我想起了火爆全球的 ChatGPT,心想,不如让它写一个试试。

测试 IO 抖动的脚本 

Cool,几乎不需要改动就能用,把代码扔在服务器开跑,一测,果然发现问题:每一次文件读写都需要 10-20ms 才能完成 。实际上,一个优秀的磁盘 IO 读写时延应该在亚毫级,但这里至少慢了 50 倍。 

Bingo,如同“幽灵宝藏”一般的分子级 Bug 逐渐显现,问题的根因已经确认:过慢的磁盘 IO 读写引发了一系列操作变慢,进而导致启动时间变得非常慢 

更庆幸的是,它还让我们发现了偶尔打开文件树变慢的根本原因,这也是整个系统并发能力下降的罪魁祸首 

05 迷雾追因

看到这里,大家可能会问,这套分布式文件系统莫非一直这么慢,你们为什么在之前没有发现?

非也,早在项目开始的时候,这里的时延是比较良好的,大家没有特别注意这个 IOPS 性能指标,直到我们后面才留意到,系统运行超过一个月时,IO 读写时延很容易就进入到卡顿的状态,表现就是文件系统所在主机 CPU 忽高忽低,重启就会临时恢复。

此时,探险之旅还没结束。毕竟,这个“幽灵宝藏”周围依旧笼罩着一层迷雾。

我们继续用 fatrace(监控谁在读写哪个 IO)监控线上各个候选人答题目录的 IO读写情况,好家伙,我们发现了一个意外的情况:几乎每一秒都有一次全量的文件 stats 操作 (这是一个检测文件是否有属性变化的 IO 操作)!

也就是说,比如有 1000 个候选人正在各自的 IDE 中编码,每个候选人平均有 300 个文件,就会出现每秒 30 万的 IO 操作数!

我们赶紧去查资料,根据研究数据显示,一个普通的 SSD 盘的 IOPS 最高也就到 2-3 万 。于是,我们重新测试了自己分布式文件系统的 IOPS 能力,结果发现也是 2-3 万 。

那这肯定远远达不到我们理想中的能力级别。

这时,问题更加明确:某种未知的原因导致了大量的 IOPS 的需求,引发了 IO 读写时延变长,慢了大约几十倍

06 接近尾声

我和我的团队继续深究下去,问题已经变得非常明确了:

原来,早在去年 12 月,我们上线一个监听文件增删的变化来通知各端刷新的功能。

最开始我们采用事件监听 (fswatch event),因为跨了主机,所以存在 1-2s 的延迟。研发同学将其改为轮询实现的方案,进而引发了每秒扫描目录的 stats 行为。

当在百人以下访问时,IOPS 没有破万,还足够应对。但一旦访问量上千,便会引发 IO 变慢,进而导致系统出现各种异常:间歇导致某些关键接口 QPS 变低,进而引发系统抖动

随着“幽灵宝藏”显露真身,这次分子级 Bug 的探险之旅也已经接近尾声。团队大 呼:这过程实在太酸爽了!

07 技术无止境

每一个程序员在成长路上,都需要与 Bug 作充足的对抗,要么你勇于探索,深入代码的丛林,快速定位,挖到越来越丰富的“宝藏”,然后尽情汲取到顶级的知识,最终成为高手;或者被它打趴下, 花费大量时间都找不到问题的根源,成为芸芸众生中的一人。

当然,程序员的世界中,不单单是 Debug。

当我毕业 5 年之后,开始意识到技术的真正价值是解决真正的社会问题。前文中我提到,由于我发现技术招聘真是一个极其痛苦的事:特别花面试官的时间,却又无法有效分析出候选人的技术能力,所以创立 ShowMeBug 来解决这个问题:用模拟实战的编程环境,解决科学评估人才的难度

这个轻协同 IDE 技术从零开发,支持协同文件树、完全自定义的文件编辑器、协同的控制台 (Console) 与终端 (Shell),甚至直接支持 Ctrl+P 的文件树搜索,不仅易于使用,又强大有力。

但是这还不够。要知道,追求技术精进是我们技术人的毕生追求。对于这个轻协同IDE,我们追求三个零:零配置、零启动、零延迟。其中,零启动就是本文所追求的极限:以最快的速度启动环境和切换环境

因此,探险之旅结束后,我们进一步改进了此文件系统,设定 raid 的多磁盘冗余,采用高性能 SSD,同时重新制定了新磁盘架构参数,优化相关代码,最终大幅提升了分布式文件系统的稳定性与并发能力。

截止本文结尾,我们启动环境的平均速度为 1.3 秒,切换环境速度进入到亚秒级,仅需要 780ms。目前在全球范围的技术能力评估赛道 (TSA) 中,具备 1-2 年的领先性

08 后记

正当我打算结束本文时,我们内部的产品吐槽群信息闪烁,点开一看:嚯,我们又发现了新 Bug。

立夏已至,我们的探险之旅又即将开始。


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

一位双非本科大二生的前端学习求职之路

心路历程在上大学之前,对自己的未来有各种展望,想着到底该选什么专业未来做什么好呢?然后抱着志愿书没日没夜的看,看别人的专业介绍视频。最后终于确定了——数字媒体技术但是双非本科,学校教的内容是真的稀碎,学习写代码,不如软工计科这些硬专业,学艺术,又不如美院专业的...
继续阅读 »

心路历程

在上大学之前,对自己的未来有各种展望,想着到底该选什么专业未来做什么好呢?然后抱着志愿书没日没夜的看,看别人的专业介绍视频。最后终于确定了——数字媒体技术

但是双非本科,学校教的内容是真的稀碎,学习写代码,不如软工计科这些硬专业,学艺术,又不如美院专业的人,对于三维来说,又没有这种设计和创新的思维。

最后加入了学校的一个信息工程学院的工作室,决定了以后当个前端码农。

学习过程

大一上学期

在刚开始便是从htmlcssJavaScript三大件开始,大概用了一个学期,起码对这三个有了稍微初步熟练的了解,在b站上看完了黑马前端的几个教程,也看了部分js高级、es6的内容。寒假开始学习Vue2,看的是codewhy在18年的视频,案例是移动端购物街的开发,当时codewhy的课程,也是非常的折磨,可能好几集里面,改bug了就占了其中一大部分。但是总归也是在大一下学期的期中左右看完了。

大一下学期

这时候学长他们有一个uniapp的外包项目,就直接拉着我开干,然后还好,uniapp和Vue2差别不是特别大,也是第一次做实战的项目,踩了很多坑,但是也算是做的差不多,但是很炸裂的是甲方跑路了,我们做了一半最后也不了了之。

大二上学期

主要是学习微信小程序开发,也做了一个课程项目,是一个购物商城。然后接了工作室两个外包项目,一个是钉钉宜搭应用开发,这部分主要是统筹其他两个同学在做,另一个是微信小程序,配合微信小程序云开发,因为要做这个,也顺带把云开发自学了,因为没什么时间学习,所以都是看着文档一点点摸索。不过最后也是独立完成了一个小应用交差了。但是甲方又拖欠,现在已经快一年了也没到账。

然后在21年十月左右,参加了阿里巴巴终端练习生计划,跟学长组了队,做的是一个清单项目,是一个跨web端、APP、小程序的一个清单类应用,完成度还蛮高的。然后我是负责用uniapp做移动端的主要功能开发,但是毕竟是清单,真正的技术再怎么难也难不到哪里去,主要是web端他们的代码比较优雅,而且技术比较新,然后被队友带着,只以几分之差,获得了第二名,还有个奖杯和证书。

1689142578324.jpg

1689142605063.jpg

寒假

一个17的学长刚好联系到我,准备做一个代码生成器,功能大概是选择字段和类型,可以自动生成假数据,也可以进行语言的转换,比如Typescript和java的。那时候想着用最新的技术来实现,突击学习了Vue3Typescript,看的是慕课网的知乎着也项目,但是内容大部分是讲的自定义组件的开发,对于当时的我来说,是真的很难啃,更何况对Vue3也不熟悉。后面没看完便在寒假开始了这个项目的开发,因为没接触过真实的项目架构,然后代码也没有拆分,基本上是一页梭哈。所以单文件代码量来到了800+,那做起来更是很难受了。虽然代码很乱,但起码也算完成了。

做完这个项目后,刚好字节青训营也开始报名了。这次是自己单打独斗去参加,当的是队长,找了几个大三和已经实习的大四大佬们一起做,当时选的题目是使用SSR的仿掘金网站,当时在选题时,因为队友基本上是React技术栈,最后便确定用Next.js做,也恰好有大佬带着,把整个项目的结构给做好了,然后我当时主要是文章详情页的开发,还有用Strapi做一些接口数据。最后也算完整的完成了。虽然有很多需要改进的地方,但是也算是运气好,拿了四等奖,还有优秀营员。

大二下学期

在下学期重新的去系统学习Vue3,因为技术不太扎实,然后也顺带学习了Typescriptpinia等等最新的技术。直到大二下学期期中才学习完。然后同时也接了国企的一个钉钉宜搭的开发,也在低代码这里,花了不少的时间,在期末也算把这个做完了。

并且在老师和工作室同学的带领下,制作了云易学教学平台,技术栈也是用到了最新的vue3ts,最主要的功能是通过websock配合后端的接口实现了在网页通过ssh控制台的操控方式,创建docker实例,比如可以在web端真正的操作MySQL数据库,这次因为有了一次开发之后,这次就熟练多了。最后通过了省赛和国赛,省赛公费去了广外,拿了省一回来。然后国赛的答辩也在几天后。

求职之路

在大二下的期末,便想着找实习,然后开始看面经。但是因为是大二的,只能实习两个月,很多公司因为这个没给我机会,投了400份后找到了一家在北京的线上实习,主要是用uniappunicloud做全栈开发,然后便在开始恶补uniclod的内容,大概花了一星期,做完了他们的笔试便入职了。但是,里面的坑是真的非常多,一来就让我做上一个实习生遗留下来的项目,然后总共有两个实习生做过这个,最后听hr说,两个实习生都离职了。那个项目做的是真的一言难尽,很多变量用拼音命名,页面的组件也不拆分,然后unicloud明明已经可以用前端拉取所有数据了,他还是用云函数来拉取数据库的内容,然后还有各种eventbus瞎用,也有两个人做过的痕迹,因为有重复的逻辑代码,项目文件也是全是first、second、third这样的命名方式。然后很多页面类似的,也是直接复制,然后在复制后的修改,用不到的也没有删除,最后整个项目跑起来,一步一个报错。我花了整整两个星期,去试图理解这坨不可名状之物,然后在这个基础上去改bug,最后也是凭借着我花足够多的时间去想办法重构,让部分bug缓解了。

不仅如此,这个公司也是让我很难评价,我遇到一个微信登录时好时坏的问题,跟我对接的技术跟我说,是很容易解决的,看文档就可以了,文档的内容就是很基础的修改一些配置,然而这些配置我早就试过了,但是我还是根据文档操作了之后,仍然会遇到时好时坏的问题。我认为这个时好时坏的问题多半是云空间或者是appid或者小程序密钥的问题,但是技术一直打太极,说文档就可以解决啦,本来是几分钟就可以解决的,都跑过好几遍啦。确实是几分钟问题,我在我这里用自己的小程序id和云空间,直接是几分钟就好了,但是遇到时好时坏的情况我真的没办法自己去解决了,更何况这个代码不是我从头开始做的,我不知道上一个人做了哪些操作,最后在这两个星期,被折磨的压力很大。而且这个项目做完之后,也只有400,期间不断的增加新需求,增加新的bug修改。让我狠狠的体会了一波社会的险恶,最后直接一分钱没拿离职了。

未来展望

现在已经是大二暑假了,之前阿里训练营的一个队友,也进入了字节实习,另一位跟我一样的大二女生,也找到了一家北京的实习,有时候真的会有很重的无力感和迷茫,不知道该何去何从,不知道如何下手。

在剩下来的一年好好沉淀吧,最近开始算法的学习,还有准备开始React的系统性学习,在接下来的时间也会在这里更新自己的算法学习,还有一些技术内容,虽然我的实力远不如掘金的大佬们,但是如果能帮助到一些同样迷茫的大学生,那我认为就有意义了吧。


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

写"简单"而不是"容易"的代码

简单 vs 容易简单 == 容易?在大多数人的第一印象中,这两个词好像是等同的,我们在很多场景下会相互替换这两个词而表达出相同的意思,比如下面这些:“高数题很简单/容易” “开车很容易/简单” “C语言写起来很简单/容易”当然 “抗原测试阳性也很容易...
继续阅读 »

简单 vs 容易

简单 == 容易?

在大多数人的第一印象中,这两个词好像是等同的,我们在很多场景下会相互替换这两个词而表达出相同的意思,比如下面这些:

“高数题很简单/容易” “开车很容易/简单” 


image

“C语言写起来很简单/容易

当然 “抗原测试阳性也很容易/简单”...

对于以上这些句子我想大家都是认同的......吧?好吧,或许前三个问题可能有不同的意见,但在第四个问题上我们应该还是能达成一致的(如果还是有疑问,也许去医院溜达一圈可以改变你的想法,即使全程佩戴口罩;))。

正如上面的句子所示,随意更换结尾的 简单 和 *容易 *两个词,好像依然可以得出一样的含义,这并不会带来理解上的偏差,所以我们可以就此推出结论 简单 == 容易 吗?或许我们可以从另一个角度再来看下这个问题

从其他语言体系中收获一些启发:

Simple:

image

在英文单词中 Simple** **的词根是sim 和 plex, 可以理解是一层,一圈,至于plex 中所代表的折叠和扭曲的含义,当只有一层或者一圈时,实际上也就是没有折叠,不扭曲了,这个词的反义词是complex,意思是编织在一起或者折叠在一起

image

因此对应到软件开发过程中,当我们追求简单的事物时,其中最关键的一点就是,我们希望它是专注于某一方面的,我们不想看到它和其他事物交织在一起,当然,这并不意味着我们需要太过追求单一一个,相反 简单* *的关键无关乎数量,更多的是追求是更少的交织,或者是没有交织,这一点才是重中之重,正如我们上面描述的一样,事物是否是交织重叠的,只要进去看下就知道了,它是客观的,是可以深入去研究的,而这种客观也是后面我们区分于 **容易 **的核心所在

Easy:

image

来源于拉丁语动词adjacere(附近放置),其现在分词为adjacens(附近的,手边的,方便的,英语adjacent的词源,adjective的间接词源),进入古法语后有名词aise(英语ease的词源),派生了动词aisier(轻松、随意放置),其过去分词aisie进入盎格鲁-诺曼底语中为aise,进入英语为easy

而 **Easy *就有趣了,其最初是来源于拉丁词 ***adjacens(附近的,手边的,方便的)一词。***靠近是一个很有意思的概念,*我们可以从下面几个方面来理解下它

  1. 物理意义上的靠近

这个东西就在你附近,触手可得,不需要骑车,或是开车去

  1. 靠近我们已知的东西,或者换个词:熟悉

对于我们来说俄语很难吗?当然是的,不过对于俄国人来说他们可能并不会这么觉得,无非是他们相对来说更熟悉罢了

  1. 靠近我们的能力

手里有一把锤子,看什么都像钉子,当我们说这个东西很容易的时候,很大程度上是因为我们已经想到了一些功能相似的东西

所以,simple** == easy** 吗?不,easy 是相对的, 弹钢琴和俄语对我来说真的很难,但是对其他人来说却很容易。不像 *simple,它是客观的,*我们总是可以进去看看,寻找是否存在重叠和交叉,而 easy 总是要问一句,对谁来说容易,对谁来说很难?

为什么需要简单,而不是容易

许多时候我们说的简单,都是从自身出发的,其实更应该用词为 容易;而真正的 **简单 **,是不和别的东西耦合的,独立的东西。多数时候我们在产品开发过程中的冲突在于,产品经理会说自己的设计是简单的(Easy),但是开发同学认为复杂(不符合自己开发的 Easy),但是却忽略了,我们真正需要的是 Simple

这样说你会发现,我们做的许多事情,往往都是从 easy 开始,而不是 simple 。 easy 开始的速度很快,但是随着项目的扩展,复杂度越来越高,速度慢慢就掉下来了 —— 想想每次重构代码的痛苦吧。而 simple 则刚开始并没有太快的速度,因为需要定义许多的东西,抽象归纳许多对象,但后续推进则是越来越快 —— 因为结构清晰构件完备,只需要理解有限的上下文就可以完成模块的修改或扩展。

因为限制

任何事情都是有限制的:

  1. 我们只能让我们理解的东西变可靠

  2. 我们只能在同时思考很少的一些事情

  3. 互相纠缠的事情我们只能把它们放在一起来思考

  4. 复杂性会降低我们的理解

我们怎么可能制造出我们不了解的可靠产品,当我们在某些系统上,想在未来使事情变得更加灵活、可扩展和动态时,我们将在理解它们的行为并确保它们正确的能力上做出权衡。但是对于我们想要确保正确的事情,我们都将受到限制,受限于对它的理解。

而且我们的理解力是很有限的,举个例子,你一次能在空中保持多少个球,或者你一次能记住多少件事?数量有限,而且数量很少,对吧?所以我们只能考虑一些事情,当事情交织在一起时,我们就失去了独立对待它们的能力。

image

image

抛球讲解:http://www.matrix67.com/blog/archiv…

因此,每次我们需要理解软件的一个新部分,并且它与另一件事相关联时,我就不得不将另一件事拉入脑海,因为我们无法在没有另一件事的情况下考虑这件事。这就是他们交织在一起的本质。因此,每一次交织都会增加这种负担,而这种负担是我们可以考虑的事物数量的组合。因此,从根本上说,这种复杂性,这种将事物编织在一起,将极大的限制我们理解系统的能力

简单带来的收益

《针织城堡》 《积木城堡》

  • 容易理解

理解代码想表达什么,而不是写的是什么

  • 容易改变

你能想象在一个针织的城堡上做改动吗?

  • 容易debug

  • 灵活性

容易代码的产生

随着时间的拉长和各种各样因素的干扰,代码慢慢就脱离了我们的掌控,当回过神来,看着日积月累的代码库,我们会发现它已大到经难以撼动了,只能寄希望于它不会哪一天突然炸开,或赶在它炸开前把它扔出去。

坏味道

image

image

读侦探小说时,透过一些神秘的文字猜测故事情节是一种很棒的体验;但如果是在阅读代码,这样的体验就不怎么好了。我们也许会幻想自己是《名侦探柯南》中的柯南,但我们写下的代码应该直观明了,代码中最重要的一环就是好的名字。

然而,很遗憾,命名是编程中最难的事情之一,所以我们需要深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法,很多情况下我们不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。

重复代码

如果你在一个以上的地点看到相同的代码结构,那么可以肯定,设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。

过长函数

函数越长,就越难理解,从已有的经验中可以看出,活得最长、最好的程序,其中的函数往往都比较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,你就会明白这些小函数的价值所在。间接性带来的好处,更好的阐释力、更易于复用、更多的选择,都是由小函数来支持的。

过长参数列表

把函数所需的所有东西都以参数的形式传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据很快就会变成邪恶的东西。但过长的参数列表本身也经常令人迷惑,使用它的人必须小心再小心。尤其在参数末尾出现了布尔类型的参数时,更容易引起人的误解,传true或false会有什么不同吗?

发散式变化

我们希望软件能够更容易被修改——毕竟软件本来就该是“软”的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这一点,这可不是一个好的信号。

重复的switch

在不同的地方反复使用同样的switch 逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形 式)。重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有 的switch,并逐一更新。

“所有条件逻辑都应该用多态取代,绝大多数if语句都应该被扫进历史的垃圾桶”,这不免有些矫枉过正了

过大的类

一个类过大往往是因为想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了

注释

当然,并不是说你不该写注释,之所以要在这里提到注释是因为注释往往被当做除臭剂来使用了,大多数情况下注释的大量出现是是因为代码本身已经很糟糕了

有意义的注释应该更多关注无法通过代码本身表达的内容,除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”,这类信息可以帮助将来的修改者理解代码所没有表达出来的细节

保持简单的代码

 

什么是重构

如何保证代码不随着时间和迭代而慢慢腐坏呢? 恐怕没有比重构更有效的方法了吧,但是重构一词近些年被用的太广泛了,很多人用“重构”这个词来指代任何形式的代码清理,但实际上用“结构调整”来泛指对代码库进行的各种形式的重新组织或清理更准确一点,而重构则是特定的一类“结构调整”,整体而言,经过重构之后的代码所做的事应该与重构之前大致一样:

 

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本

重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构

也就是说,如果你在重构过程中发现了一个bug,那么重构之后它应该依然存在(当然,你可以在重构后顺手就修改掉它),同样,我们也需要把它跟性能优化也区分下开,他们两个也很像,都是在不改变程序的可观测行为下做的改动,两者的差别在于,重构的目的是为了让代码更容易理解和更容易修改,最终可能使程序更快了,也可能更慢了,而性能优化时,我们只关注让程序运行的更快,最终可能使得代码变得更难以理解和维护了,当然这点也是在我们预期之内的。

为何要重构

 

重构并不是万能药,它只是一种工具,帮助我们达到以下目的方式中的一种

▪  保持软件的设计

如果没有重构,程序的内部设计会逐渐腐败,当我们经常只为了短期目的而修改代码时,往往会忽略掉或者说没有理解整体的设计,于是代码会逐渐失去其结构,我们会越来越难以通过阅读代码来理解原来的设计,而随着代码结构的流失,我们也将越来越难以维护其设计意图,导致更快的代码腐败,所以,经常性的重构有助于我们维护代码应有的形态

▪ 使软件更容易理解

机器并不关心程序的样子,它只是按照我们的指示精确执行罢了,但别忘记了,除了我们自己和机器外,代码还有其他的读者,几个月后可能会有另一个程序员尝试读懂你的代码并做一些修改,他才是重要的那一个,相比于机器是否多消耗一个时钟周期,如果一个程序员需要花费一周时间才能读懂你的代码并修改才更要命呢。

重构可以帮我们让代码更易读。开始进行重构前,代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的意图——更清晰地说出我想要做的。

▪ 提高编程速度

归结到最后其实可以总结为一点:重构帮我们更快速地开发程序

我们很容易得出这些好处:改善设计、提升可读性、减少bug,这些都是在提高质量。但花在重构上的时 间,难道不是在降低开发速度吗?

我们在软件开发中经常能碰到这种场景,一个团队,一开始的迭代和进展都很快,但是如今想要添加一个新的功能需要的时间就长的多了,bug排查和修复也越来越慢,代码库一个补丁摞一个补丁,需要细致的“考古”工作才能搞懂系统是如何工作的,这些负担会不断的拖累我们的开发速度,直到最后恨不得重写系统。

 

有些团队则不同,他们添加新功能的速度越来越快,因为他们可以利用已有的功能快速构建新功能,两者

的主要区别就在于软件的内部质量,良好的内部质量可以让我们轻易找到在哪里以及如何修改,良好的模块划分可以使我们只需要理解代码的一小块就可以做出修改,引入bug的可能性也会变小,即使有了bug,我们也可以很快的找出来并修复掉,最终我们的代码会演变成一个平台,在其上,我们可以很容易的构造其领域相关的新功能

 

 

何时重构

▪ 添加新功能

重构的最佳时机就在添加新功能之前,当要添加一个功能时,我们一般都会先看下代码库中已有的内容,此时经常可以发现,有些函数或者代码只要稍微调整下结构,就能使我们添加新功能变得更容易,可能只是一个函数的参数不太一样或是代码中一些字面量不太一样,如果不先重构,就只能把这段代码复制过来,修改几个值,这样就产生了重复的代码,更麻烦的是,一旦后续需要调整这块逻辑,我就需要同时修改这两处(希望我还能想起来有两处需要修改)

就好像要去东边的上海,你不一定会直接向东开,而是先向北开去上高速,后者会使你更快到达目的地。

▪ 使代码更易懂

在做改动前你需要先先理解代码在做什么,然后才能修改,这段代码可能是自己写的,也可能是别人写的,一旦你需要花费很大精力思考代码在做什么时,这就是一个好的时机了,“这个变量名称代表这个意思”,“这段逻辑是这样工作的”......我们通常无法把太多的细节长时间留存在脑海里,为什么不把他转移到代码本身,使其一目了然呢?如果把对代码的理解植入代码中,这份知识会保存得更久,其他人也能获得同等的收益。

不仅如此,长远来看的话,当代码变得更易理解时,我们通常能够看到之前设计中没有看见的问题,如果没有做前面的重构,也许我们永远也看不到这些设计问题,因为我们不够聪明,无法在脑海中推演所有的变化。

▪ 有计划的重构

上面的重构时机都是夹杂在我们日常的功能开发中的,重构本身就是我们开发新功能或者修复bug的一环,但有时候,因为快速的功能迭代而忽视了代码的设计,问题就会在某些区域逐渐累积长大,最终需要专门花些时间来解决,但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。

▪ 何时不应该重构

虽然我们提倡重构,但确实有一些不值得重构的情况。

如果看见一块凌乱的代码,但并不需要修改它,那么就不需要重构它。 如果丑陋的代码能被隐藏在一个函数或者API之下,我们可以暂时容忍它继续保持丑陋。只有当我们需要理解其工作原理时,对其进行重构才有价值。

另一种情况是,如果重写比重构还容易,就别重构了,当然在这之前我们总是需要花些时间尝试的。

重构的挑战

 

▪ 延缓新功能开发

从上面的讨论中其实我们已经得到这个问题的答案了,重构的意义不在于炫技或是把代码库打磨的闪闪发光,而是纯粹从经济角度出发的考量。我们之所以重构,因为它能让我们更快,添加功能更快,修复bug更快。如果有人告诉我们“重构会拖慢进度”,我们应该坚信,他说的一定是别的东西,而不是重构,重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值

▪ 代码归属

很多重构可能不仅涉及一个模块的改动,同时也会影响其他一些模块或者系统,代码的所有权边界会妨碍我们的一些重构,例如修改某个模块暴露出去的函数命名,不仅实现方需要修改,调用方也需要修改,尤其是涉及到暴露给其他团队的api,情况可能会更复杂,很有可能根本不知道调用法有哪些

当然这并不会完全阻止我们,只是受到很多限制罢了,比如我们可以同时保留老的函数签名和api,使其内部调用重构后的实现,等到确认所有调用方都修改后,再删除老的声明,当然也可能选择永久保留老的声明。

▪ 分支合并

大多数情况下,我们都是多个人同时维护一个代码仓库的,现代便利的仓库分支管理工具很好的支持了我们的团队协作,使我们可以更快的完成产品的开发,我们通常会从主干上拉取一个功能分支进行开发,直到功能上线时才会合并会主干,以保证主干不被功能分支所影响,这存在一个问题,我们的功能分支存在的时间越久,和主干的差异就会越大,尤其是当多个人同时在进行不同的功能分支开发时情况会更加复杂,当你因为重构修改了一个函数的命名,而其他人在新加代码中又使用了它,当代码集成时,问题就来了,而且随着功能分支存在时间的增加,这种痛苦也会不断的增加。

▪ 测试

重构的一个重要特征就是--不会改变程序可观察的行为,我们不能寄希望于“只要我足够小心,就不会破坏任何东西”,但万一我们犯了个错误怎么办?或许应该把万一两个字去掉,人总是会犯错误的,关键是在于如何快速的发现错误,要做到这一点,我们就需要一套能够快速测试的代码组件,所以大多数情况下如果我们想要重构,我们就需要先有可以自测试的代码,一旦能够自测试,我们就可以使用很小的步子进行前进,一旦测试失败,我们只需要执行退回到上一次可以成功运行的状态即可。

▪ 遗留代码

大多数情况下,有一大笔遗产是件好事,但从程序员的角度来看就不同了。遗留代码往往很复杂,测试又不足,而且最关键的是,是别人写的。重构能很好的帮助我们理解系统,理顺代码的逻辑,但是关键点在于遗留系统多半没测试。如果你面对一个庞大而又缺乏测试的遗留系统,很难安全地重构清理它。

对于这个问题,显而易见的答案就是“没测试就加测试”,说起来轻松,做起来可就不轻松了,一个系统只有在一开始设计时就考虑到了测试,添加测试才会容易,可要是如此的话系统早就该有测试了,还需要现在才开始加吗?

但是,无论如何,就像《整洁代码之道》中所说的那样:“让营地比你来时更干净些”。

如何安全的重构

▪ TDD

▪ 自动化重构


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

再学http-为什么文件上传要转成Base64?

1 前言最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质...
继续阅读 »

1 前言

最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质上都是一样的,那么为什么还要先转成Base64呢?这两种方式有什么区别?带着这样的疑问我们一起来分析下。

2 multipart/form-data上传

先来看看multipart/form-data的方式,我在本地通过一个简单的例子来查看http multipart/form-data方式的文件上传,html代码如下

<!DOCTYPE html>
<html>
<head>
<title>上传文件示例</title>
<meta charset="UTF-8">
<body>
<h1>上传文件示例</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file"><br>
<label for="tx">说明:</label>
<input type="text" id="tx" name="remark"><br><br>
<input type="submit" value="上传">
</form>
</body>
</html>

页面展示也比较简单

image.png

选择文件点击上传后,通过edge浏览器f12进入调试模式查看到的请求信息。
请求头如下 image.png 在请求头里Content-Type 为 multipart/form-data; boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo,刚开始看肯定有点懵,不过其实也不复杂,可以简单理解为在请求体里要传递的参数被分为多部份,每一部分通过分解符boundary分割,就比如在这个例子,表单里有file和remark两个字段,则在请求体里就被分为两部分,每一部分通过boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo来分隔(实际上还要加上CRLF回车换行符,回车表示将光标移动到当前行的开头,换行表示一行文本的结束,也就是新文本行的开始)。需要注意下当最后一部分结尾时需要加多两个"-"结尾。
我们继续来看请求体

image.png 第一部分是file字段部分,它的Content-Type为image/png,第二部分为remark字段部分,它没有声明Content-Type,则默认为text/plain纯文本类型,也就是在例子中输入的“测试”,到这里大家肯定会有个疑问,上传的图片是放在哪里的,这里怎么没看到呢?别急,我猜测是浏览器做了特殊处理,请求体里不显示二进制流,我们通过Filder抓包工具来验证下。

image.png 可以看到在第一部分有一串乱码显示,这是因为图片是二进制文件,显示成文本格式自然就乱码了,这也证实了二进制文件也是放在请求体里。后端使用框架springboot通过MultipartFile接受文件也是解析请求体的每一部分最终拿到二进制流。

@RestController
public class FileController {
// @RequestParam可接收Content-Type 类型为:multipart/form-data 
// 或 application/x-www-form-urlencoded 请求体的内容
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
return "test";
}
}

到此multipart/form-data方式上传文件就分析完了,关于multipart/form-data官方说明可参考 RFC 7578 - Returning Values from Forms: multipart/form-data (ietf.org)

3 Base64上传

在http的请求方式中,文件上传只能通过multipart/form-data的方式上传,这样一来就会有比较大的限制,那有没其他方式可以突破这一限制,也就是说我可以通过其他的请求方式上传,比如application/json?当然有,把文件当成一个字符串,和其他普通参数没什么两样,我们可以通过其他任意请求方式上传。如果转成了字符串,那上传文件就比较简单了,但问题是我们怎么把二进制流转成字符串,因为这里面可能会有很多“坑”,业界一般的做法是通过Base64编码把二进制流转成字符串,那为什么不直接转成字符串而要先通过Base64来转呢?我们下面来分析下。

3.1 Base64编码原理

在分析原理之前,我们先来回答什么是Base64编码?首先我们要知道Base64只是一种编码方式,并不是加解密算法,因此Base64可以编码,那也可以解码,它只是按照某种编码规则把一些不可显示字符转成可显示字符。这种规则的原理是把要编码字符的二进制数每6位分为一组,每一组二进制数可对应Base64编码的可打印字符,因为一个字符要用一个字节显示,那么每一组6位Base64编码都要在前面补充两个0,因此总长度比编码前多了(2/6) = 1/3,因为6和8最小公倍数是24,所以要编码成Base64对字节数的要求是3的倍数(24/8=3字节),对于不足字节的需要在后面补充字节数,补充多少个字节就用多少个"="表示(一个或两个),这么说有点抽象,我们通过下面的例子来说明。
我们对ASCII码字符串"AB\nC"(\n和LF都代表换行)进行Base64编码,因为一共4字节,为了满足是3的倍数需要扩展到6个字节,后面补充了2个字节。

image.png

表3.1

转成二级制后每6位一组对应不同颜色,每6位前面补充两个0组成一个字节,最终Base64编码字符是QUIKQw==,Base64编码表大家可以自行网上搜索查看。

image.png 我们通过运行程序来验证下

image.png 最终得出的结果与我们上面推理的一样。

3.2 Base64编码的作用

在聊完原理之后,我们继续来探讨文件上传为什么要先通过Base64编码转成字符串而不直接转成字符串?一些系统对特殊的字符可能存在限制或者说会被当做特殊含义来处理,直接转成普通字符串可能会失真,因此上传文件要先转成Base64编码字符,不能把二进制流直接字符串。

另外,相比较multipart/form-data Base64编码文件上传比较灵活,它不受请求类型的限制,可以是任何请求类型,因为最终就是一串字符串,相当于请求的一个参数字段,它不像二进制流只能限定multipart/form-data的请求方式,日常开发中,我们用的比较多的是通过apllication/json的格式把文件字段放到请求体,这种方式提供了比较便利的可操作性。

4 总结

本文最后再来总结对比下这两种文件上传的方式优缺点。
(1)multipart/form-data可以传输二进制流,效率较高,Base64需要编码解码,会耗费一定的性能,效率较低。
(2)Base64不受请求方式的限制,灵活度高,http文件二进制流方式传输只能通过multipart/form-data的方式,灵活度低。
因为随着机器性能的提升,小文件通过二进制流传输和字符串传输,我们对这两种方式时间延迟的感知差异并不那么明显,因此大部分情况下我们更多考虑的是灵活性,所以采用Base64编码的情况也就比较多。


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

懒汉式逆向APK

通过各方神仙文档,以及多天调试,整理了这篇极简反编译apk的文档(没几个字,吧).轻轻松松对一个apk(没壳的)进行逆向分析以及调试.其实主要就是4个命令.准备下载apktool下载Android SDK Build-Tools,其中对齐和签名所需的命令都在此...
继续阅读 »

通过各方神仙文档,以及多天调试,整理了这篇极简反编译apk的文档(没几个字,吧).轻轻松松对一个apk(没壳的)进行逆向分析以及调试.其实主要就是4个命令.

准备

  1. 下载apktool
  2. 下载Android SDK Build-Tools,其中对齐和签名所需的命令都在此目录下对应的版本的目录中,比如我的在D:\sdk\build-tools\30.0.3目录下,可以将此目录加入环境变量中,后续就可以直接使用签名和对齐所需的命令了
  3. 可选,下载jadx-gui,可查看apk文件,并可导出为gralde项目供AS打开

流程

  1. 解压apk: apktool d C:\Users\CSP\Desktop\TEMP\decompile\test.apk -o C:\Users\CSP\Desktop\TEMP\decompile\test,第一个参数是要解压的apk,第二个参数(-o后面)是解压后的目录

  2. 修改: 注意寄存器的使用别错乱,特别注意,如果需要使用更多的寄存器,要在方法开头的.locals x或.registers x中对x+1

    • 插入代码:在idea上使用java2smali插件先生成smali代码,可复制整个.smali文件到包内,或者直接复制smali代码,注意插入后修改包名;
    • 修改代码:需要熟悉smali语法,可自行百度;
    • 修改so代码,需要IDA,修改完重新保存so文件,并替换掉原so文件,注意如有多个架构的so,需要都进行修改并替换;
    • 删除代码:不建议,最好逻辑理清了再删,但千万别删一半;
    • 资源:修改AndroidManifest.xml,可在application标签下加入android:debuggable="true",重新打包后方可对代码进行调试;
  3. 重打包apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk,第一个参数是要进行打包的目录文件,第二个参数(-o后面)是重新打包后的apk路径.重新打包成功,会出现Press any key to continue ...

  4. 对齐zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk,第一个参数是需要进行对齐的apk路径,第二个参数是对齐后的apk路径.对齐成功,会出现Verification succesful

  5. 签名apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk,第一个参数(--ks后面)是密钥路径,后面跟着是否开启V1、V2签名,在后面跟着签名密码,最后两个参数(--out后面)是签名后的apk路径以及需要签名的apk(注意需对齐)路径.签名成功,会出现Signed

  6. 安装adb install C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk

  7. 调试: 用jdax将apk导出为gradle项目,在AS中打开,即可通过attach debugger的方式对刚重新打包的项目进行调试.注意,调试时因为行号对不上,所以只能在方法上打上断点(菱形图标,缺点,运行速度极慢)

  8. 注意事项:

    • 上述命令中,将目录和项目'test'改成自己的目录和项目名即可;
    • apktool,zipalign,apksigner,adb命令需加入环境变量,否则在各自的目录下./xxx 去执行命令;
    • zipalign,apksigner所需的运行文件在X:XX\sdk\build-tools\30.0.3目录下;
    • 使用apksigner签名,对齐操作必须在签名之前(推荐使用此签名方式);
    • 新版本Android Studio生成的签名密钥,1.8版本JDK无法使用,我是安装了20版本的JDK(AS自带的17也行)

假懒

为了将懒进行到底,写了个bat脚本(需要在test文件目录下):

::关闭回显
@echo off
::防止中文乱码
chcp 65001
title 一键打包

start /wait apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk
start /b /wait zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
start /b /wait apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk

大家将此脚本复制进bat文件,即可一键输出.

不过目前略有瑕疵:1.重新打包需要新开窗口,并且完成后还需手动关闭;2.关闭后还要输入'N'才能进行后续的对齐和签名操作有无bat大神帮忙优化下/(ㄒoㄒ)/~~!

-------更新

真懒

对于'假懒'中的打包脚本,会有2个瑕疵,使得不能将懒进行到底.经过查找方案,便有了以下'真懒'的方案,使得整个打包可以真正一键执行:

::关闭回显
@echo off
::防止中文乱码
chcp 65001
title 一键打包

call apktool b C:\Users\CSP\Desktop\TEMP\decompile\test -o C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk
call zipalign -v 4 C:\Users\CSP\Desktop\TEMP\decompile\test_b.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
del test_b.apk
call apksigner sign -verbose --ks C:\Users\CSP\Desktop\软件开发\反编译\mykeys.jks --v1-signing-enabled true --v2-signing-enabled true --ks-key-alias key0 --ks-pass pass:mykeys --key-pass pass:mykeys --out C:\Users\CSP\Desktop\TEMP\decompile\test_b_sign.apk C:\Users\CSP\Desktop\TEMP\decompile\test_b_zipalign.apk
del test_b_zipalign.apk

echo 打包结束
echo 输出文件是-----test_b_sign.apk

pause

可以看到,把start换成了call,并且删除了重新打包和对齐后的文件,只留下最后签完名的文件

image.png

到此够了吗?不够,因为运行第一个apktool b命令时,重新打包完,会被pasue,让你按一个按键再继续.

image.png

这当然不行,这可不算一键,那么我们找到apktool的存放路径,打开apktool.bat,找到最后一行

image.png

就是这里对程序暂停了,那么就把这一行删了,当然最好是注释了就行,在最前面rem即可对命令进行注释,处理完之后,再重新运行我们的'一键打包.bat'脚本,这时候在中途重新打包后就不会出现'Press any key to continue...'了,即可一键实现打包-对齐-签名的流程了( •̀ ω •́ )y.

当然,如果想使脚本到处运行,可以给脚本增加一个变量,在运行前通过环境变量的形式把要打包的目录路径加入进来,这个大家可以自行尝试.

最后,感谢大家的阅读.这里面有对smali和so的修改,有机会和时间,我也会继续分享!!!


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

Parcelable为什么速度优于 Serializable ?

在Android开发中,我们有时需要在组件之间传递对象,比如Activity之间、Service之间以及进程之间。传递对象的方式有三种:将对象转换为Json字符串通过Serializable序列化通过Parcelable序列化 1、什么是序列化  序...
继续阅读 »

在Android开发中,我们有时需要在组件之间传递对象,比如Activity之间、Service之间以及进程之间。
传递对象的方式有三种:

  • 将对象转换为Json字符串
  • 通过Serializable序列化
  • 通过Parcelable序列化 

1、什么是序列化

微信截图_20230619105720.png

  序列化:简单来说,就是将实例的状态转化为可以存储或者传输的形式的过程
  反序列化:反过来再把这种形式还原成实例的过程就叫做反序列化

  这种可以传输或者存储的形式,可以是二进制流,也可以是字符串,可以被存储到文件,也可以通过各种协议被传输。

2、Serializable 的实现原理

   Serializable 是 Java 平台中用于对象序列化和反序列化的接口。,它是一个空接口,没有定义任何方法,它仅仅只起到了标记作用。通过实现它,Java 虚拟机可以识别该类是可以进行序列化和反序列化操作的。 

2.1 Serializable 序列化的使用

将一个对象序列化写入文件:

public class User implements Serializable {

private String name;
private String email;

public User(String name, String email) {
this.name = name;
this.email = email;
}

/***** get set方法省略 *****/
}
File file = new File("write.txt");

//序列化写入文件
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(new User("李四", "lisi@qq.com"));
objectOutputStream.flush();

//读取文件反序列化
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
User user = (User) objectInputStream.readObject();

序列化写入文件的结果:
image.png

2.2 Serializable 的关键方法ObjectOutputStream() 和 writeObject()

  那么对于一个只是实现了一个空接口的实例,ObjectOutputStream是如何做到知道这个类中都有哪些属性结构的呢?并且是如何获取它们的值的呢?
  我们来看一下 ObjectOutputStream的源码实现,在它的构造方法中主要做两件事:

  • 创建一个用于写入文件的Stream
  • 写入魔数和版本号
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);//创建Stream
...
writeStreamHeader();//写入魔数和版本号
...
}

  再来看 writeObject() 方法,writeObject的核心是调用 writeObject0()方法,在writeObject0中通过 ObjectStreamClass desc = ObjectStreamClass.lookup(cl, true) 创建出一个原始实例的描述信息的实例,即desc。desc这个描述信息中就包括原始类的属性结构和属性的值。接着再根据实例的类型调用对应的方法将实例的类名和属性信息写入到输出流;字符串、数组、枚举和一般的实例写入的逻辑也是不同的。


image.png

2.3 性能分析

  很明显在序列化的过程中,写输出流的过程肯定不存在输出瓶颈,复杂操作集中在如何去解析原始对象的结构,如何读取它的属性。所以要把重点放在ObjectStreamClass这个类是如何的被创建出来的。
  我们分析lookup方法,发现创建过程会先去读取缓存。如果发现已经解析并且加载过相同的类,那么就直接返回。在没有缓存的情况下,才会根据class去创建新的ObjectStreamClass实例。

    static ObjectStreamClass lookup(Class<?> cl, boolean all) {
...
Reference<?> ref = Caches.localDescs.get(key);//读取缓存
if (ref != null) {
entry = ref.get();
}


if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
}


...

if (entry == null) {
entry = new ObjectStreamClass(cl);//没有缓存
}

if (entry instanceof ObjectStreamClass) {
return (ObjectStreamClass) entry;
}
}

  在创建过程中,类名name是通过class实例调用反射API来获取的。再通过getDeclaredSUID 方法提取到serialVersionUID 字段信息。如果没有配置,getSerialVersionUID 方法会通过 computeDefaultSUID 生成一个默认的序列号。
  接下来就会去获取属性以及计算属性值的偏移量。

    private ObjectStreamClass(final Class<?> cl) {
name = cl.getName();//类名

suid = getDeclaredSUID(cl);//提取 serialVersionUID 字段信息


fields = getSerialFields(cl);//获取属性 即通过反射获取该类所有需要被序列化的Field
computeFieldOffsets();//计算属性值的偏移量
}

  我们再来看一下读取属性信息的代码 getSerialFields(),首先系统会判断我们是否自行实现了字段序列化 serialPersistentFields 属性,否则走默认序列化流程,既忽律 static、transient 字段。

    private static ObjectStreamField[] getSerialFields(Class<?> cl)
throws InvalidClassException
{
ObjectStreamField[] fields;
if (Serializable.class.isAssignableFrom(cl) &&
!Externalizable.class.isAssignableFrom(cl) &&
!Proxy.isProxyClass(cl) &&
!cl.isInterface())
{
if ((fields = getDeclaredSerialFields(cl)) == null) {
fields = getDefaultSerialFields(cl);//默认序列化字段规则
}
Arrays.sort(fields);
} else {
fields = NO_FIELDS;
}
return fields;
}

  然后在getDefaultSerialFields 中使用了大量的反射API,最后把属性信息构建成了ObjectStreamField的实例。

    private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();//获取当前类的所有字段
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;

for (int i = 0; i < clFields.length; i++) {
if ((clFields[i].getModifiers() & mask) == 0) {
//将其封装在ObjectStreamField中
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}

  到这里我们会发现Serializable 整个计算过程非常复杂,而且因为存在大量反射和 GC 的影响,序列化的性能会比较差。另外一方面因为序列化文件需要包含的信息非常多,导致它的大小比 Class 文件本身还要大很多,这样又会导致 I/O 读写上的性能问题。

  总结,实现Serializable接口后 ,Java运行时将使用反射来确定如何编组和解组对象。所以我们可以认定这些反射操作就是影响 Serializable 性能的一个重要的因素,同时会创建大量临时对象并导致相当多的垃圾收集。但是因为这些反射,所以Serializable的使用非常简单。

3、Parcelable的实现原理

  在Android中提供了一套机制,可以将序列化之后的数据写入到一个共享内存。其他进程就可以通过Parcel来读取这块共享内存中的字节流,并且反序列化成实例。Parcelable相对于Serializable的使用相对复杂一些。
微信截图_20230629112841.png

3.1 Parcelable 序列化的使用

public class User implements Parcelable {

private String name;

private String email;

//反序列化
protected User(Parcel in) {
name = in.readString();
email = in.readString();
}


public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}

@Override
public User[] newArray(int size) {
return new User[size];
}
};

@Override
public int describeContents() {
return 0;
}

// 用于序列化
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(email);
}
}
User user = new User();
Bundle params = new Bundle();
params.putParcelable("user", user);

Bundle arguments = getArguments();
personBean = arguments.getParcelable("user");

  实现 Parcelable 接口,需要实现writeToParcel的方法以提供序列化时,将数据写入Parcel的代码。除此之外还要提供一个Creator以及一个参数是Parcel类型的构造方法,用来反序列化。
  序列化和反序列化每个字段的代码统一由使用者自己来实现。这样一来在序列化和反序列化的过程中,就不必再去关心实例的属性结构和访问权限。这些都由开发者自己来实现。所以能够避免大面积的使用反射的情况,算是牺牲了一定的易用性来提升运行时的效率。当然了这个易用性我们也可以通过parcelize的方式来弥补。此外,Parcelable还有一个优点,就是它可以手动控制序列化和反序列化的过程。这意味着我们可以选择只序列化对象的部分字段,或者在反序列化的时候对字段进行一些额外的处理。这种灵活性使得Parcelable在某些特定的场景下更加有用。
  虽然Parcelable的设计初衷并不是像Serializable那样,基于输入流和输出流的操作,而是基于共享内存的概念。但Parcelable是支持让我们获取到序列化之后的data数组的。这样一来,我们就可以同样把序列化后的信息写入到文件中。

        //序列化写入byte[]
Parcel parcel = Parcel.obtain();
parcel.setDataPosition(0);
new User().writeToParcel(parcel, 0);
byte[] bytes = parcel.marshall();
parcel.recycle();

//从byte数组反序列化
Parcel parcel = Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
new User(parcel);

3.2 Intent、Bundle中传值对比

public class Intent implements Parcelable, Cloneable { }

public final class Bundle extends BaseBundle implements Cloneable, Parcelable { }

  在安卓平台最经常用到序列化的情况,是通过Intent传值。我们可以看到,无论是Intent还是Bundle,其实都是Parcelable的实现类。
  那么当Intent或者Bundle被序列化的时候,它们内部的Serializable是如何被处理的呢?
  通过代码可以看到,在Parcel的writeSerializable方法中,还是会先把Serializable转化成Byte数组。然后再通过writeByteArray去写入到Parcel中。

    public final void writeSerializable(@Nullable Serializable s) {
if (s == null) {
writeString(null);
return;
}
String name = s.getClass().getName();
writeString(name);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(s);
oos.close();

writeBytea

  所以在Intent传值的场景下,Parcelable也是存在速度优势的。因为Parcelable就是正常的基于writeToParcel的方法中的逻辑去进行序列化的。而Serializable要先通过ObjectOutputStream把对象转换成ByteArray,再通过writeByteArray写入Parcel。

  • Parcelable

  调用对象内部实现的writeToParcel 方法,通过一些write方法直接写入Parcel。

  • Serializable

  通过ObjectOutputStream把对象转换成ByteArray,再通过writeByteArray写入Parcel。

  但是有些情况下不一定Parcelable更快。
  之前在看Serializable源码的时候,我们发现ObjectStreamClass是存在缓存机制的。所以在一次序列化的过程中,如果涉及到大量相同类型的不同实例序列化,比如一个实例反复嵌套自己的类型,或者是在序列化大数组的情况下。Serializable的性能还是存在优势的。 

4、Parcelable为什么速度优于Serializable?

  • Parcelable
    1. 对象自行实现出入口方法,避免使用反射的情况。
    2. 二进制流存储在连续内存中,占用空间更小。
    3. 牺牲易用性(kotlin的Parcelize 可以弥补),换取性能。
  • Serializable
    1. 用反射获取类的结构和属性信息,过程中会产生中间信息。
    2. 有缓存结构,在解析相同类型的情况下,能复用缓存。
    3. 性能在可接受的范围内,易用性较好。

  不能抛开应用场景谈技术方案,在大多数场景下Parcelable确实存在性能优势,而Serializable的性能缺陷主要来自反射构建ObjectStreamClass类型的描述信息。在构建ObjectStreamClass类型的描述信息的过程中,是有缓存机制的。所以能够大量复用缓存的场景下,Serializable反而会存在性能优势。 Parcelable原本在易用性上是存在短板的,但是kotlin的Parcelize 很好的弥补了这个缺点。


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

从面试官角度分析:介绍一下Android中的Context?

Context是什么Context的结构Context的注意事项问题正解:一、Context是什么Context 是 Android 中用的十分常见的一种概念,常被翻译成上下文,这个概念在其他的技术中也有运用。Android 官方对它的解释,可以理解为应用程序...
继续阅读 »
  1. Context是什么
  2. Context的结构
  3. Context的注意事项

问题正解:

一、Context是什么

Context 是 Android 中用的十分常见的一种概念,常被翻译成上下文,这个概念在其他的技术中也有运用。Android 官方对它的解释,可以理解为应用程序环境中全局信息的接口,它整合了相当多系统级的服务,可以用来得到应用中的类、资源,以及可以进行应用程序级的调起操作,比如启动 Activity、Service等等,而且 Context 这个类是 抽象abstract 的,不含有具体的函数实现。

二、Context结构

Context 是维持 Android 程序中各组件能够正常工作的一个核心功能类。

Context 本身是一个抽象类,其主要实现类为 ContextImpl,另有直系子类两个:

  • ContextWrapper
  • ContextThemeWrapper

这两个子类是 Context 的代理类,它们继承关系如下:

image.png

ContextImpl类介绍

ContextImpl 是 Context API 的十分常见实现,它为 Activity 和其他应用程序组件提供基本上下文对象,说白了就是 ContextImpl 实现了抽象类的方法,我们在使用 Context 的时候的方法就是它实现的。

ContextWrapper类介绍

ContextWrapper 类代理 Context 的实现,将其所有调用简单地代理给另一个 Context 对象(ContextImpl),可以被分类为修饰行为而不更改原始 Context 的类,其实就 Context 类的修饰类。真正的实现类是 ContextImpl,ContextWrapper 里面的方法执行也是执行 ContextImpl 里面的方法。

ContextThemeWrapper

就是一个带有主题的封装类,比 ContextWrapper 多了主题,它的一个直接子类就是 Activity。

通过Context的继承关系图结合我们几个开发中比较常见的类,Activity、Service、Application,所以Context 一共有三种类型,分别是 Application、Activity 和Service,他们分别承担不同的责任,都属于 Context,而他们具有 Context 的功能则是由ContextImpl 类实现的。

三、Context的数量

其实根据上面的 Context 类型我们就已经可以得出答案了。Context 一共有 Application、Activity 和 Service 三种类型对象,因此一个应用程序中Context 数量的计算公式就可以这样写:

Context数量 = Activity数量 + Service数量 + 1

上面的1代表着 Application 的数量,因为一个应用程序中可以有多个 Activity 和多个 Service,但是只能有一个 Application。

四、Context注意事项

Context 如果使用不恰当很容易引起内存泄露问题。

最简单的例子比如说使用了 Context 的错误的单例模式:

public class Singleton {
  private static Singleton instance;
  private Context mContext;

  private Singleton(Context context) {
      this.mContext = context;
  }

  public static Synchronized Singleton getInstance(Context context) {
      if (instance == null) {
          instance = new Singleton(context);
      }
      return instance;
  }
}

上述代码中,我们使得了一个静态对象持有 Context 对象,而静态数据的生命一般是长于普通对象的,因此当 Context 被销毁(例如假设这里持有的是 Activity 的上下文对象,当 Activity 被销毁的时候),因为 instance 仍然持有 Context 的引用,导致 Context 虽然被销毁了但是却无法被GC机制回收,因为造成内存泄露问题。

而一般因为Context所造成的内存泄漏,基本上都是 Context 已经被销毁后,却因为被引用导致GC回收失败。但是 Application 的 Context 对象却会随着当前进程而一直存在,所以使用 Context 是应当注意:

  • 当 Application 的 Context 能完成需要的情况下,并且生命周期长的对象,优先使用 Application 的 Context。
  • 不要让生命周期长于 Activity 的对象持有到 Activity 的引用。
  • 尽量不在 Activity 中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。
五、如何正确回复以上面试题
  1. 面试官:Android 中有多少类型的 Context,它们有什么区别?

回答总共有 Activity 、Service、Application 这些 Context 。

共同点:它们都是 ContextWrapper 的派生类,而 ContextWrapper 的成员变量 mBase 可以用来存放系统实现的 ContextImpl,这样我们在执行如 Activity 的 Context 方法时,都是通过静态代理的方式最终执行到 ContextImpl 的方法。我们调用 ContextWrapper 的 getBaseContext 方法就能得到 ContextImpl 的实例。

不同点:它们有各自不同的生命周期;在功能上,只有 Activity 显示界面,正因为如此,Activity 继承的是 ContextThemeWrapper 提供一些关于主题、界面显示的能力,间接继承了 ContextWrapper ;而 Applicaiton 、Service 都是直接继承 ContextWrapper ,所以我们注意一点,但凡跟 UI 有关的,都应该用 Activity 作为 Context 来处理,不然要么会报错,要么 UI 会使用系统默认的主题。

  1. 面试官:一个APP应用里有几个 Context 呢?

Context 一共有 Application 、Activity 和 Service 三种类型,因此一个应用程序中 Context 数量的计算公式就可以这样写:

Context 数量 = Activity 数量 + Service 数量 + 1

上面的1代表着 Application 的数量,因为一个App中可以有多个Activity和多个 Service,但是只能有一个 Application。

  1. 面试官:Android 开发过程中,Context 有什么用?

Context 就等于 Application 的大管家,主要负责:

  • 四大组件的信息交互,包括启动 Activity、Broadcast、Service,获取 ContentResolver 等。
  • 获取系统/应用资源,包括 AssetManager、PackageManager、Resources、System Service 以及 color、string、drawable 等。
  • 文件,包括获取缓存文件夹、删除文件、SharedPreference 相关等。
  • 数据库(SQLite)相关,包括打开数据库、删除数据库、获取数据库路径等。

其它辅助功能,比如配置 ComponentCallbacks,即监听配置信息改变、内存不足等事件的发生

  1. 面试官:ContextImpl 实例是什么时候生成的,在 Activity 的 onCreate 里能拿到这个实例吗?

可以。我们开发的时候,经常会在 onCreate 里拿到 Application,如果用 getApplicationContext 取,最终调用的就是 ContextImpl 的 getApplicationContext 方法,如果调用的是 getApplication 方法,虽然没调用到 ContextImpl ,但是返回 Activity 的成员变量 mApplication 和 ContextImpl 的初始化时机是一样的。 再说下它的原理,Activity 真正开始启动是从 ActivityThread.performLaunchActivity 开始的,这个方法做了这些事:

  • 通过 ClassLoader 去加载目标 Activity 的类,从而创建 对象。
  • 从 packageInfo 里获取 Application 对象。
  • 调用 createBaseContextForActivity 方法去创建 ContextImpl。
  • 调用 activity.attach ( contextImpl , application) 这个方法就把 Activity 和 Application 以及 ContextImpl 关联起来了,就是上面结论里说的时机一样。
  • 最后调用 activity.onCreate 生命周期回调。

通过以上的分析,我们知道了 Activity 是先创建类,再初始化 Context ,最后调用 onCreate , 从而得出问题的答案。不仅 Activity 是这样, Application 、Service 里的 Context 初始化也都是这样的。

  1. 面试官:ContextImpl 、ContextWrapper、ContextThemeWrapper 有什么区别?
  • ContextWrapper、ContextThemeWrapper 都是 Context 的代理类,二者的区别在于 ContextThemeWrapper 有自己的 Theme 以及 Resource,并且 Resource 可以传入自己的配置初始化。
  • ContextImpl 是 Context 的主要实现类,Activity、Service 和 Application 的 Base Context 都是由它建立的,即 ContextWrapper 代理的就是 ContextImpl 对象本身。
  • ContextImpl 和 ContextThemeWrapper 的主要区别是, ContextThemeWrapper 有 Configuration 对象,Resource 可以根据这个对象来初始化。
  • Service 和 Application 使用同一个 Recource,和 Activity 使用的 Resource 不同。
  1. 面试官:Activity Context、Service Context、Application Context、Base Context 有什么区别?
  • Activity、Service 和 Application 的 Base Context 都是由 ContextImpl 创建的,且创建的都是 ContextImpl 对象,即它们都是 ContextImpl 的代理类 。
  • Service 和 Application 使用相同的Recource,和 Activity 使用的 Resource 不同。
  • getApplicationContext 返回的就是 Application 对象本身,一般情况下它对应的是应用本身的 Application 对象,但也可能是系统的某个 Application。
  1. 面试官:为什么不推荐使用 BaseContext?
  • 对于 Service 和 Application 来说,不推荐使用 Base Context,是担心用户修改了 Base Context 而导致出现错误。
  • 对于 Activity 而言,除了担心用户的修改之外,Base Context 和 Activity 本身对于 Reource 以及 Theme 的相关行为是不同的(如果应用了 Configuration 的话),使用 Base Context 可能出现无法预期的现象。
  1. 面试官:ContentProvider 里的 Context 是什么时候初始化的呢?

ContentProvider 不是 Context ,但是它有一个成员属性 mContext ,是通过构造函数传入的。那么这个问题就变成了,ContentProvider 什么时候创建。应用创建 Application 是通过执行 ActivityThread.handleBindApplication 方法,这个方法的相关流程有:

  • 创建 Application
  • 初始化 Application 的 Context
  • 执行 installContentProviders 并传入刚创建好的 Application 来创建 ContentProvider
  • 执行 Application.onCreate

得出结论,ContentProvider 的 Context 是在 Applicaiton 创建之后,但是 onCreate 方法调用之前初始化的。

  1. 面试官:BroadcastReceiver 里的 Context是哪来的?

广播接收器,分动态注册和静态注册。

  • 动态注册很简单,在调用 Context.registerReceiver 动态注册 BroadcastReceiver 时,会生成一个 ReceiverDispatcher 会持有这个 Context ,这样当有广播分发到它时,执行 onReceiver 方法就可以把 Context 传递过去了。当然,这也是为什么不用的时候要 unregisterReceiver 取消注册,不然这个 Context 就泄漏了哦。
  • 静态注册时,在分发的时候最终执行的是 ActivityThread.handleReceiver ,这个方法直接通过 ClassLoader 去创建一个 BroadcastReceiver 的对象,而传递给 onReceiver 方法的 Context 则是通过 context.getReceiverRestrictedContext() 生成的一个以 Application 为 mBase 的 ContextWrapper。注意这边的 Context 不是 Application 。


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

收起狭隘,换位思考,心胸放开!

当你越证明自己是对的时候,其实你已经错了一半,因为如果你真的是对的,不用你来证明,人只有经历越多,跳的坑越多,在做事时才会有更宽广的视角,在面对每一个问题时才会发现原因所在,一个人越自大,越觉得自己是对的,那么其实你就越肤浅,离正确就越来越远,做人最难的就是站...
继续阅读 »

当你越证明自己是对的时候,其实你已经错了一半,因为如果你真的是对的,不用你来证明,人只有经历越多,跳的坑越多,在做事时才会有更宽广的视角,在面对每一个问题时才会发现原因所在,一个人越自大,越觉得自己是对的,那么其实你就越肤浅,离正确就越来越远,做人最难的就是站在不同的视角去看待问题!

收起自己的狭隘

我时常在提醒自己,当你面对一件事的时候,如果你没有身入其中,那么你就不要用你有限的认知去评判,刚毕业实习的时候,公司用的后端语言是nodejs,前端用layui,虽然我自认为我JS写的还行,nodejs也不成问题,但是就是有一种抗拒,因为我主要还是写Java嘛,所以内心就有点看不上,那时候我觉得你用nodejs,他妈的连sql都要写在代码里,为啥不用Java,SpringBoot+Mybatis Plus一把梭,上去就是干,多么方便啊,还有你为啥不用Vue,Vue那么牛逼你不用你要用layui,你们都干了些什么啊!

先为自己的自以为是,自己的那狭隘而又自我感觉良好的思想说声抱歉,我们总是有一个通病,觉得要按照自己想的来才行,其实这恰恰显示出了自己的弱,自己的视野太狭隘,对于技术栈,公司有的产品和项目是和一些单位和公司有挂钩或者合作的,所以对于语言和开发框架也是有一定的讲究,还有像数据库字段,对于很多政府项目,字段命名使用拼音,有些朋友就嗷嗷直叫,就觉得这他妈什么设计啊,还不如我来干,我也曾说过,后来我学会了闭嘴。

如果你没能力制定规则,那么你就遵守规则,而不是自己只能扛100斤,非要说能媲美霸王举鼎!

站在别人的角度去看问题

在职场和生活中,很多人总是自已为是,总觉得自己来做这件事,一定比别人做得好,还有些人喜欢马后炮,别人没有很好的完成一件事,它总会说:“你看,当时我已经说是这样做,那样做,这个问题你早就应该考虑到”,这一系列话在职场和生活中,听到的太多太多了,我以前也是这样。

正式参加工作后,接手了一些代码,让人欲哭无泪,于是就和同事和朋友说:“这傻逼怎么写的代码啊,写成这个鸟样,我他妈也是服了,真sb”,后来,因为工期还有一些原因,自己也写了很多垃圾,翻出来看脸都是红的,那么,下一个人来接手的时候,自己也会被骂!

我们总是觉得自己能做得很好,作为上级,当手下工作完成得不好时,会觉得手下无能,其实何不反思一下自己,手下无能,那么证明自己的分配和对手下的一些交代一定存在很大的问题,那么作为手下,又会觉得领导不行,领导没能力,但是,何不反思一下自己做事是否认真思考过,反应过!

没有谁是正确的,也没谁是错误的,在任何事发生的时候,甩锅不仅不能解决问题,反而会使问题严重,有一句一将无能,累死三军,但是我也觉得一卒无能,拖垮三军,当然这个无能不单单指能力,还指人品,道德,担当等等,一个人是走不远的,一行人才能走得更远。

心胸宽广

“大肚能容,容天下难容之事; 开口便笑,笑世间可笑之人”,心胸宽广是一个人的魅力,总想搞谁,让谁难看,给别人扣帽子,不行君子之举,这样的人可能在暂时获取到了利益恩惠,但是也会失去很多,有很多东西是靠你的人品,道德,态度等积累起来的,在生活和工作中,要有宽广的胸怀,这样不仅能减轻你80%的心理负担,还能在无形之中给你带来你意想不到的收获!


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

一个艰难就业的23年应届生的2022年

自我介绍我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、r...
继续阅读 »

自我介绍

我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、rap... 还只擅长Vue的渣渣前端程序猿,有兴趣可以关注我的公众号程序猿青空,23年开始我会时不时分享各种优秀文章、学习资源、学习课程,探索初期,还请多多关照。这篇文章会是我公众号的第一篇文章,主要对我这一年来的经历做一个简单的流水账总结,涉及到恋爱、租房、学习、工作等各方面内容,希望这份经验对你也能有所帮助。

学习

大二下半年的时候分流,自主报名到了我们学校的产业学院——企业和学校联合创办的培养应用型人才的学院。我文科相当薄弱,埋头考研会相当痛苦,也很清楚自己做不来官僚主义那一套,公职也不是适合我的职业(没错我对公职有偏见),很坚定就业这条路。因为还没有毕业,我的身份归根结底就是一个双非下流本科的一名大学生,为了避免自己毕业即失业,看当时产业学院的宣传也不错就去了。

事实上因为产业学院刚创办不久,而且并不是所有人来到这里都是为了就业的,也有可能是为了学分、助学金等其他方面的原因,课程设计、师资力量、同学质量等各方面都良莠不齐、鱼龙混杂。每门课程的期末大作业基本都是一个小项目,大三一年里两个期末都有为了大作业通宵的几天,再加上1500💰凑活过的生活费,死贵的电费和食堂伙食费,在这里学习和生活有时候还蛮辛苦的。好在我很清楚自己应该做什么,天赋不够,努力来凑,本来起跑线就低,更应该比别人卷一点。当然我也不是那种能够没日没夜卷的人(👀),关注了鱼皮,加入了他的知识星球,在星球天天学习健身(没错我还健身💪)打卡的flag没两个礼拜就立不住了,知识付费的事咱也没少干,就是说能一直坚持下来的着实不多,咱也明白咱就是个普通人,逆袭这种事确实还是很难做到的,我这人还是比较佛系的。

大三这一年我用一年的时间从零学前端,自认为还算是没有辜负自己,这一年时间的学习也还算有成果,虽然没法和卷王们争第一,也能跟在他们后面做个万年老二(😭呜呜呜)。下半年开始实习后更别说了,新的技术栈的学习基本就停滞了。实习前我还天真的以为能有更多的时间学习,正相反,比在学校学的更少,因为下班到家七八点,生活琐事会比在学校里多得多,而且我下班后还要花一个多钟头健身,再加上忙碌一天后更无心学习,只想躺平。

下半年做过的最卷的事也就参与了字节青训营,课题选择了前端监控平台,可惜的就是没能在青训营期间完成(😭呜呜呜,队友都摆烂了),当然也就没有结营证书。但我也不甘心就这样算罢,这个项目我就自己拉出来,作为我的毕业设计去完成它。解决实习期间学习效率低的最好办法就是在公司学习一些对公司业务有关或者优化公司项目的知识,名正言顺地摸鱼。我是Vue入门的,这一年里也一直守着Vue,来年第一季度目标就是学习React和Nest,开发一个自己的数据聚合的网站,能变现就最好了(😎欸嘿)。

生活&实习

大三下,也就是今年上半年,为了冲刺暑期实习,也就没去做兼职了,感叹本就艰难的生活的同时,殊不知这是为数不多还能自己自由掌控的日子了(😥我哭死)。其实我开始准备实习还是挺晚了,再加上期末没有太多时间,准备并不是太充分,没有太多自信心,投了几家大厂,不是没回应,就是笔试挂,就有点望而却步。

在我一个大佬同学的介绍下,面试了一家南京的小厂,过程很顺利,实习薪资给的也很可观,当时就没考虑那么多,就选择接受offer了(后来在杭州实习认识了几个小伙伴,才学了没几个月,暑假就面试进了独角兽企业,我那个时候确实应该再多投一投的)。刚开始的想法是第一次出门实习,有份经验就可以,在什么城市没关系,然而事实是工作上确实没什么关系,生活上关系可大了。7月13日第一次一个人拎上行李,义无反顾地去了南京,以为自己终于能够大展拳脚,再不济也能够在公司有所贡献,然而现实总是没那么理想。

上路

因为一个人前往外地工作,第一件事情便是租房,为了省点钱就托南京实习公司的一个同事看房子,因为他的房租到期也要找房子就顺便可以租在一起,有个照应。然而实际上因为是第一次出远门工作和生活,一切和自己的理想差距显然大了许多:因为不是自己实地看的房,而且也是第一次租房,虽然房租只有850💰,但是也可能因为是夏季大家都开空调,差不多50多💰一个礼拜的电费和其他乱七八糟的费用,一个月光租房子就差不多得1200💰,并不算贵,但是性价比极低;我的房间没地方晒衣服,只能晒在那个同事的房间的阳台,作为一个社恐患者,每次去都要做很多心理斗争(他会不会睡了,他会不会在忙....🙃);桌上只能堪堪放下我的显示器和笔记本,鼠标活动范围极小;床应该是睡过好几个租客了,明显的不舒服;吃的方面因为有点水土不服不能随便乱吃,同时也是为了省钱所以选择自己做饭,因此还得购置很多厨具调味品等等,一次性的开销💰不小;回学校的频率比我想象的高,因此来回车费也成为一大负担;当时租房合同是同事代签的,他签了一年,我那时候也不懂也没问,再加上当时换工作离开的比较急,没时间找转租,违约金直接血亏1700💰。

日常挤地铁

生活的种种问题都还能接受或者解决,然而工作方面,因为进入公司的时间段比较特殊再加上疫情影响,在南京实习的三个月里,我始终没有能够在技术上得到足够的提升,再加上与公司和领导的气场不合,使得我在公司整天如坐针毡,甚至有点无所事事(总之就是过的很不开心),虽然有不低的实习薪资,但是我始终没法在那里躺平。因此在中秋决定参与秋招,开始寻找第二份实习工作。

然而今年找工作并不简单,因为频繁发作的疫情,再加上互联网行业这些年的发展,行业的形势非常的严峻,各大公司都削减了HC(head count,人头数,就是最终录用的人数,肯定有小伙伴不懂这个词,我一开始就不懂🤏),作为一个民本23年应届生,在今年的秋招着实很难找到一份理想的工作。那段时间的想法就是尽快找到下一份工作(急急急急急急,我是急急国王),找到一份离家近、工资高、平台大至少满足两个的工作。从9月10日中秋就开始投出第一份简历,到10月19日确定来到杭州的一家四五百人的SaaS公司,这期间投出过几百份简历,得到的回应却寥寥无几,这是一段非常难忘的经历。

这一个月里每一天都在为找工作烦恼,一开始专注于线上面试,却始终的得不到理想工作的认可,持续的碰壁使得开始怀疑自己这些年的学习,自己的选择是不是错了,是不是自己能力确实没法满足他们的要求(被ktv了),后来也决定不放过线下面试的机会,顶着疫情在南京、杭州、家、学校几地频繁奔波,在杭州线下面试的那一天还是顶着自己身体上的各种不适(持续拉肚子,全身酸痛,萎靡不振),仍然要拿出饱满的精神去面对面试,好在当时就获得了面试官也是现在的leader的认可,简直就是久旱逢甘霖,虽然并不是直接发的offer,但是也是十分有信心。杭州比起南京的工作,实习薪资低了很多,但是因为线下面试,对于当时感受到的公司的氛围十分的心动,也就放弃了其他小公司更高薪资的offer,决定了自己的第二份实习工作。

又上路啦

换工作又是换城市,所以又需要租房搬家,购置各种必需品,又是一大笔开销,在还没进公司前始终在担忧自己先择了薪资更低的工作,到时候会不会付出了这么多,结果又远不如预期让自己更痛苦。不过在经过了一个月左右实习后,我在杭州的公司工作的感受让我相信自己的选择没有错。

10月23日我再一次拖着一大堆行李开始了迁徙,本来打算先简单看房子,先回家住几天再自驾,拖着行李回来看房子签合同,所以我把被子等一些大件的行李都寄回家了,但是这次进入杭州后就黄🐎了(之前几地来回跑黄都没黄一下),只能多看几套房子然后就签下来,好在当天就看到一个自己满意的,10几平,押一付一,一个月算上水电差不多也就1300💰,不至于睡大街,但是我没有被子,当时杭州刚开始降温,温度也就个位数,但是买被子太亏了,之后用不上,就买了床毛毯,多盖几件衣服,凑活过了两天(真的凑活,冷的雅痞)。

杭州租的房

11月1日正式入职,正式开启了在杭州的工作生活,有条不紊的入职手续,时长1周的实习生培训,认识了许多和我一起实习的小伙伴,刚进来还赶上公司的双十一活动,让我对未来的工作生活充满希望。

双十一零食自助

第一月开始接触了一些简单的业务,重新开始了健身,第二个月就参与开发了一个简单的项目,还封装了公共组件、开发了简单的提高开发效率的脚手架工具,我终于能够继续有条不紊运转了。

在南京实习的期间除了参加了字节青训营和准备面试而巩固基础外,专业上可以说是没有丝毫提升,不过生活经验确实收获满满,坚定了自己的目标,职业生涯规划更加清晰,为了达到目标去学会自律。这几个月的开销给自己和父母都增添了不小得负担,好在现在稳定下来勉强能够在杭州自给自足,生活重新步入正轨,比起在南京,杭州的生活更加得心应手。但是并不是说南京不好,南京是一个非常优雅的城市,这里有他躺在超市里超乖的猫猫,超治愈

超乖的猫猫

离开南京前我也花时间去好好游玩了两天(去了一些免费的博物馆,景点)。

忘记叫啥地了

比起杭州,我认为南京更适合生活,我只是去到了一个不适合我的公司和因为经验不足吃了不少亏才离开了这个城市。我很珍惜在杭州的这份工作,也非常享受现在忙碌充实的生活,我也希望自己的能力能够不断得到认可,继续探索自己的人生价值。

感情

呜呜呜,鼠鼠该死啊,鼠鼠长了个恋爱脑,但是好在现在穷的雅痞,我还社恐,可以心无旁骛地工作学习(搞💰)。出来实习没几个礼拜就跟在一起一年的女孩子分手了,其实在上半年因为我们对未来规划的分歧就吵过架,她想留在慈溪,而我更向往大城市(当然不止这一点原因啦),那个时候我就很清楚这段感情肯定没法坚持很久,下半年又异地,在各自的城市实习,天天吵架,自然而然就吵分了,累觉不爱。我深知自己不是啥好男人(男人没一个好东西),还没有资本,毕业前绝对要水泥封心(做杭州第一深情)。

其实我家离学校很近,但是从念大学开始还是很少回家了,在学校里没有什么感觉,直到独自出门在外工作才知道在家真好,爸爸妈妈真好(我是妈宝男,呜呜呜😭),看这篇文章的小伙伴不要再随便跟爸爸妈妈撒气了哦。家里的老人只剩下奶奶独自在乡下了,以后一定要多打电话。

展望

在未来的一年中,希望自己能够吸收已经犯过的错误的经验,保质保量地完成未来的各项工作,作为一名程序员最重要的最重要的就是自我驱动,持续学习,通过不断学习才能够在未来的工作中创造更多的价值,以下是我23年的一些计划

学习

  • 这个月先抓紧时间把自己的毕设解决,写复盘的分享博客,之后顺利毕业
  • 上半年学习React,Nest,开发一个数据聚合分享平台,同样做分享
  • 运营自己的博客和各平台账号,不说多少粉丝,能坚持不凉就行,争取每周一个博客
  • 每季至少阅读一本书,学习一个技术栈
  • 坚持自己的每日计划和每月复盘总结(包含年中和年终总结)

工作

  • 因为现在常态化了,不知道今年的就业形势会是什么样的,着实不想再像去年那样被支配了,所以还是希望得到自己满意的薪资的前提下在这里转正,但愿不要出什么幺蛾子吧
  • 继续卷进部门更深层业务,目标负责6个项目
  • 学习更多优化开发效率和质量的技术栈,明年就简单定个两个的目标吧,要求不高

生活

  • 我真的超级想买机车的,但是杭州主城区禁摩,所以先23年下半年花时间考个D照,看情况决定买个机车还是电驴
  • 3月份房租到期了,看房肯定又要放进日程了,看看到时候有没有合租的小伙伴吧,如果有人有兴趣到时候可以分享一下杭州租房经验
  • 健身肯定是要继续的,有一说一我肉体确实没啥天赋(也可能是吃得不够多),健身更多的是一种生活态度吧
  • 我是一个很不喜欢打电话的人,尤其是和长辈,感觉没话聊,但是老人家接到自己孩子的电话,知道孩子过得不错,真的会很开心。明年定个小目标,一个月给奶奶打一通电话。

2022年好像所有人都过的很艰难,或许所有人都想离开浪浪山,但是也不要忘记看看浪浪山的风景,让我们一起加油吧。最后再打个广告,关注公众号程序猿青空,免费领取191本计算机领域黑皮书电子书,更有集赞活动免费挑选精品课程(各个领域的都有),不定期分享各种优秀文章、学习资源、学习课程,能在未来(因为现在还没啥东西)享受更多福利。


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

改行后我在做什么?(2022-9-19日晚)

闲言碎语今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。但在我这个年纪,这个阶段,看似有很多选择,但其实我...
继续阅读 »

闲言碎语

今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。

但在我这个年纪,这个阶段,看似有很多选择,但其实我没有选择。能做的也只是多挣点钱。

在这个信息爆炸的时代,我们知道更高的地方在哪里。但当你想要再往上走一步的时候,你发现你的上限,其实从出生或从你毕业的那一刻就已经注定了。可能少部分人通过自身的努力,的确能突破壁垒达到理想的高度。但这只是小概率事件罢了。在我看来整个社会的发展,其实早就已经陷入了一种怪圈。

在我,早些年刚刚进入社会的时候。那时的想法特别简单。就想着努力工作,努力提升自身的专业素养。被老板赏识,升职加薪成为一名管理者。如果,被淘汰了那应该是自己不够优秀,不够努力,专业技能不过硬,自己为人处事不够圆滑啥的。

内卷这个词语引爆网络的时候;当35岁被裁员成为常态的时候。再回头看我以前的那些想法那真的是一个笑话。(我觉得我可能是在为自己被淘汰找借口)

当前的状态

游戏工作室的项目,目前基本处于停滞的状态。我不敢加机器也不敢关机。有时候我都在想,是不是全中国那3-4亿的人都在搞这个?一个国外的游戏,金价直接拉成这个逼样。

汽配这边的话,只能说喝口稀饭。(我花了太多精力在游戏工作室上了)

梦想破灭咯

其实按照正常情况来说,游戏工作室最开始的阶段,我应该是能够稍微挣点钱的。我感觉我天时、地利、人和。我都占的。现在来看的话,其实我只占了人和。我自己可以编码,脚本还是从驱动层模拟键鼠,写的一套脚本。这样我都没赚钱,我擦勒。

接下来干嘛

接下来准备进厂打螺丝。(开玩笑的) 还是老老实实跟着我弟学着做生意吧。老老实实做汽配吧!在这个时代,好像有一技之长(尤其是IT)的人,好像并不能活得很好。除非,你这一技之长,特别特别长。(当下的中国不需要太多的这类专业技术人员吧。)

我感受到的大环境

我身边有蛮多的大牛。从他们的口中和我自己看到的。我感觉在IT这个领域,国内的环境太恶劣了。在前端,除开UI库,我用到的很多多的库全是老外的。为什么没有国人开源呢?因为,国人都忙着996了。我们可以在什么都不知道的情况下,通过复制粘贴,全局搜索解决大部分问题。 机械视觉、大数据分析、人工智能 等很多东西。这一切的基石很多年前就有了,为什么没人去研究他?为什么我们这波人,不断的在学习:这样、那样的框架。搭积木虽然很好玩。但创造一个积木,不应该也是一件更有挑战性的事情么?

在招聘网站还有一个特别奇怪的现象。看起来这家公司是在招人,但其实是培训机构。 看起来这家公司正儿八经是在招聘兼职,但其实只想骗你去办什么兼职卡。看起来是在招送快递,送外卖的,招聘司机的,但其实只是想套路你买车。我擦勒。这是怎样的一个恶劣的生存环境。这些个B人就不能干点,正经事?

卖菜的、拉车的、搞电商的、搞短视频、搞贷款的、卖保险的、这些个公司市值几百亿。很难看到一些靠创新,靠创造,靠产品质量,发展起来的公司。


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

实习到毕业一年的回忆:工作旅程

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水...
继续阅读 »

前两天和实习那会的同事一起吃饭,聊到了他们那会刚毕业两三年的工作收入,问完我之后说,“你刚毕业一年的起点太高了,税后五位数,而且还是大专学历,这在外面根本找不到薪资那么多的工作”,“这一切还是得感谢你们几个人,如果不是前年你们收留了我,估计我都不干这行跑去流水线拧螺丝了”,我点头说道。

21年六月在学校投了上百份简历,面试收到了几个offer,但是实习工资给的太少,不是2.5k或者3k,这对于那时年少轻狂的我怎么可能接受呢,果断拒绝,快月底临近毕业找不到工作的我越来越慌了,后来约了一家线上面试并且通过了,实习工资150一天,正常每个月能拿3.3k,有节假日的情况下只能拿到不到2.8k的可怜工资。但命运真的很神奇,因为这家实习公司,结识了能够在职场上帮助到我的良师益友。

实习公司所在的写字楼

21年十月认识了一位朋友介绍的女生,可能是好久没和女生接触过,我变得不怎么会和女生聊天了,只记得我和她打了两个月的王者,基本上天天玩,还都是玩的人机,后来不知道啥原因就凉凉了,当然两个月也没见过面。当然因为这个事搞的我心烦意乱,工作没法工作,21年底,22年初,也就是元旦期间,我向公司提出离职,电话裸辞,直接就不去公司了,给老板整的一脸懵逼。22年一月中旬,公司聚餐邀请了已经离职的我,晚上酒喝起兴的我,在同事的劝说下,我向老板表明了我想回到公司的意向,后来如愿以偿的回到了公司,此时,我的工资不是150一天了,而且达到了惊人的4.5k每月。

上班路上的金鸡湖大道

22年六月临近毕业,在实习公司沉淀了一年,我觉得时机已经成熟是时候走了,鼓起勇气和老板说了离职,老板同意了。这个时候我还不知道未来的一年,我还会和他们经常聚餐,一起聊行业、工作、生活。甚至今天的这份工作也得益于他们。

离职后,准备去南京发展,当时在常州的同学那暂住了几天,闲的没事干就投了几份简历玩玩,面试了两家都收到了offer,一家给政府做erp系统的公司给了7.5k,另一家是上市公司的外包给了8k,随后我就不想去南京了,选择了那家外包公司,在那前几个月基本上天天没事,过的相当的安逸,每天晚上下班后,5:30准备到球场,后来我换了个组长,我开始做MES系统了,第一个系统我身份是打杂的,给另一个同事当助手,后来做的系统,我开始当主力开发。22年底,工作干的十分不顺心,萌生了离职的想法,向外包公司的部门经理提了涨薪,他只给涨500块钱,我觉得也没必要留下了,所性直接离职,此时我还没有转正,所以我直接在一周到走人。

再次离职后,我选择回到老家休息一段时间,思考一下第二年该去往何处。在家乡待了近四十天,基本上没有碰过电脑,我到处的玩,打球,打游戏,泡澡,感觉已经废了。

过年前几天,我开始慌了,于是我重新打开我的小米笔记本,打开了熟悉又陌生的IDEA,学习了几个开源框架,背了一些面试题,准备年后去外地找工作。

CIM开源框架

大年初三,我早早的买好火车票去往常州,准备在常州找一份工作,可惜我找了近一周,一份工作也没有找到,于是我将目光看向南京和老东家所在的苏州。我联系了实习公司一个同事现在所在的公司,于是他将我内推到了现在的这个公司,他向上面的人担保我肯定没有问题,所以我直接跳过了面试,也就是在这个公司,因为我代码写的好,所以我两次加薪达到了税后五位数。

23年五月二十号,公司安排我去西安出差两周,这是我人生第一次出差,见到了网络上所谓的甲方,值得我纪念一下。

飞机上的云层 仓库

如今,那位内推我的同事,也就是我第一份实习公司的同事,他要走了,去了一家做大数据的公司,领导让我开始学习做管理,以后带新人做项目,我只能说尽力而为。

对于像我这样学历不高的人而言,个人觉得代码不是技术架构,而是人情世故,人脉是人生宝贵的一笔财富。

浪子花梦

上班摸鱼写于2023年7月12日11点。


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

环信的那些”已读“功能实现及问题解决

写在前面你在调用环信的消息回执时,是否有以下的烦恼1、发送了消息已读回执,为什么消息列表页的未读数没有发生变化?2、发送了消息已读回执,为什么消息漫游拉取不到已读状态?如果你有这些烦恼,那就继续往下看一些歧义在这之前,我们需要先来统一确定两件事情第一:消息列表...
继续阅读 »

写在前面
你在调用环信的消息回执时,是否有以下的烦恼
1、发送了消息已读回执,为什么消息列表页的未读数没有发生变化?
2、发送了消息已读回执,为什么消息漫游拉取不到已读状态?
如果你有这些烦恼,那就继续往下看

一些歧义
在这之前,我们需要先来统一确定两件事情
第一:消息列表页
第二:聊天页面
接下来以环信vuedemo为例,看一下这两者


如图所示,红色圈起来的部分为消息列表页也叫会话列表页面,可通过会话列表的api拉取。

绿色圈起来的部分为聊天页面,可通过消息漫游的api拉取

注:聊天页面的数据获取不是必须调用消息漫游api,也可以存在本地从本地进行获取,这个可根据自己项目的需求以及业务逻辑来做调整,本文以消息漫游中的数据为例
插播:会话是什么,当和一个用户或者在一个群中发消息后,就会自动把对方加到会话列表中,可以通过调用会话列表去查询。需要注意,1、此api调用有延迟,建议只有初次登录时通过此api获取到初始会话列表的数据,后续都在本地进行维护。2、登陆ID不要为大小写混用的ID,拉取会话列表大小写ID混用会出现拉取会话列表为空

解决问题一:
在明确了会话列表页和聊天页面各代指的部分之后,我们先来解决第一个问题:发送了消息已读回执,为什么会话列表的未读数没有变化
原因:对于环信来讲,消息是消息,会话是会话,这是两个概念,消息已读和会话已读并没有做联动,也就是消息已读只是对于这条消息而言并不会对会话列表的未读数产生影响,他们是两个独立的个体。会话列表的未读数是针对整个会话而言
那么如何清除会话列表的未读数呢?——需要发送会话已读回执也就是channel ack,这里还需要注意一点,sdk是只负责数据传输的,改变不了页面层的渲染逻辑。所以在发送完channel ack后页面上渲染的未读数不会无缘无故就清0了,是需要重新调用api渲染的!!!!!

channelAck() {
let option = {
chatType: "", // 会话类型,设置为单聊。
type: "channel", // 消息类型。固定参数固定值,不要动它
to: "", // 接收消息对象(用户 ID)。
};
let msg = WebIM.message.create(option);
WebIM.conn
.send(msg)
.then((res) => {
console.log("%c>>>>>>>>会话已读回执发送成功", "color:#6ad1c7", res);
})
.catch((e) => {
console.log("%c>>>>>>>>>会话已读回执发送失败", "color:#ef8784", e);
});
},


会话已读回执发送成功之后,接收方会收到onChannelMessage回调监听

conn.addEventHandler("customEvent", {
onChannelMessage: (message) => {},
});



消息已读回执是需要发送readack,是针对于某一条消息而言。这里也需要注意一点,sdk是只负责数据传输的,改变不了页面层的渲染逻辑,所以已读未读在页面上的渲染也是需要自己处理一下

readAck() {
let option = {
type: "read", // 消息是否已读。固定参数固定值,不要动它
chatType: "singleChat", // 会话类型,这里为单聊。
to: "", // 消息接收方(用户 ID)。
id: "", // 需要发送已读回执的消息 ID。
};
let msg = WebIM.message.create(option);
WebIM.conn
.send(msg)
.then((res) => {
console.log("%c>>>>>>>>消息已读回执发送成功", "color:#6ad1c7", res);
})
.catch((e) => {
console.log("%c>>>>>>>>>消息已读回执发送失败", "color:#ef8784", e);
});
},



消息已读回执发送成功之后,接收方会收到onReadMessage回调监听

conn.addEventHandler("customEvent", {
onReadMessage: (message) => {},
});




插播:会话列表未读数计算规则,简单理解,如果这个会话是单个用户在一直输出的话,这个未读数会一直累加,但是只要对方回了这条消息,那么未读数就会从这条消息之后开始再计算

 解决问题二:
再来看一下第二个问题:为什么消息漫游中拉取不到消息的已读状态
原因:环信服务器是不记录消息状态的,也就是不会记录这条消息是否已读了,所以不会返回消息已读或者未读
那么如何来实现
1、自己本地进行记录消息状态
2、可以使用环信sdk提供的reaction功能来间接是实现已读未读

reaction实现已读未读简单示例

addReaction() {
WebIM.conn
.addReaction(
{
messageId: "",//消息ID
reaction: "read" //reaction
}
)
.then((res) => {
console.log("%c>>>>>>>>reaction添加成功", "color:#6ad1c7", res);
})
.catch((e) => {
console.log("%c>>>>>>>>>reaction添加失败", "color:#ef8784", e);
});
},






总结Q&A
Q:发送了消息已读回执,为什么消息列表页的未读数没有发生变化?
A:会话和消息是两个概念,会话已读是会话已读,消息已读是消息已读,消息已读无法改变会话列表的数据
Q:发送了消息已读回执,为什么消息漫游拉取不到已读状态?
A:环信的服务器不记录消息状态,需要自己本地存储或者使用reaction功能间接实现

收起阅读 »

【环信十周年】千呼万唤始出来,环信礼物真实在!

当看到公布的信息有自己中奖的时候真的非常开心,然后就是在等待中..等待自己的大礼包。今天终于是拿到了自己,急不可耐开箱~🙃看到这包装...居然被丢成这样,但是还好里面没啥问题~拆开包装,一打开入眼就是环信十周年~😁打开包装就是大礼包全家桶了,包含了手提袋、手机...
继续阅读 »

当看到公布的信息有自己中奖的时候真的非常开心,然后就是在等待中..等待自己的大礼包。

今天终于是拿到了自己,急不可耐开箱~


🙃看到这包装...居然被丢成这样,但是还好里面没啥问题~


[舔屏]拆开包装,一打开入眼就是环信十周年~


😁打开包装就是大礼包全家桶了,包含了手提袋、手机支架、徽章、杯子、T恤、贴画,非常nice~


(全家桶)


这个手提袋外面的材质有点像纸,但是又不是纸,整体非常不错,就是有点皱巴巴的~😓


手提袋的内层是布艺的,蓝色的布也是非常的清爽~手提带上面还印有文字,非常的大气~


这个贴纸的“又不是不能用”非常的贴切,一眼就相中了~不就是说当下的工作心态嘛,又不是不能干,将就干吧~[污]


[坏笑]接下来就是这个杯子了,非常的实用,可以泡咖啡~提神醒脑~


但同时不得不吐槽一下这个杯口,有点锋利~不注意可能会导致划手~(扣厂家货款


接下来是这个徽章,质感真的不错,直接当成摆件用也非常nice~


这个手机支架实用性太高了,看电影~摸鱼都是一等一的利器~还有LOGO点缀~


最后就是这个T恤了,好看~😁


衣服背后也还有个小LOGO,这点缀炒鸡棒~


衣服洗了,看下材质~94%棉,弹性非常不错,夏天穿着凉快~

最后,感谢环信~

收起阅读 »

数仓开发沦为了取数工具人,该如何破局?

本文从成长的话题展开,主要聊了数据开发会遇到的瓶颈,以及该如何破局。其实,所谓的破局就是个人的成长,所以回归个人成长,本文也给出了一些浅薄的理解,希望对你有所帮助。 写在前面 假如你现在辞职,要加入一家新的公司,那么你最看中什么?我相信每个人都会有自己的答案...
继续阅读 »

本文从成长的话题展开,主要聊了数据开发会遇到的瓶颈,以及该如何破局。其实,所谓的破局就是个人的成长,所以回归个人成长,本文也给出了一些浅薄的理解,希望对你有所帮助。



写在前面


假如你现在辞职,要加入一家新的公司,那么你最看中什么?我相信每个人都会有自己的答案。你可能会说,[钱多、事少、离家近],你也可能会说,[能让自己有所沉淀和成长,能够学到新的东西],你还有可能会说,[想打破舒适圈,做些有挑战的事情],或者你也可能会说,[做的业务有前景,工作内容是自己喜欢的]。凡此种种,不一而足。当然,我不认为有哪些理由是正确的亦或是错误的,因为每个人的认知、工作经历会有或多或少的差异。其实,你会发现,即便是大家的理由不尽相同,但都离不开两个重要的因素:钱和个人成长。前者是工作的本质,即通过自己的劳动获取报酬,后者是实现前者最大化的条件,即个人的能力有多强。那么,如果抛开薪资报酬的因素,能让我们走得更长远的或许就是个人成长,即个人能力的增长。回头想想,当我们在学校的时候,会学习各种专业知识,来丰富自己的技能,当我们走到工作岗位时,同样也会有所积累和沉淀,可能有些东西不像在学校那样,可以用分数进行量化,但总归是有所进步,有所成长。所以本文就从个人成长的视角聊一聊作为一个数据开发,逐渐沦为了取数工具人,我们该如何进行破局。


从成长说起


正如一千个读者有一千个哈姆雷特一样,我们今天不去纠结该如何定义成长。我们先从一个大家应该能够遇到的一个场景说起——辞职面试。当我们参加一场面试的时候,面试官一般会问一个亘古不变的问题:[为什么辞职],相信大家会这样说:[上段工作技术太薄弱,自己成长的太慢了,想突破瓶颈]。现在的话,问题似乎变得有点具象了。那如果再问:[如何在技术上有所突破,在哪方面的技术上有所成长呢],似乎就很难给出答案了。为什么会这样呢?我们可以仔细复盘一下工作过程,是不是日复一日重复着,有做不完的需求,做不完的项目,长此以往就会变得焦虑了,开始怀疑做这些有什么价值,自己真正的成长了吗。其实,这种所谓的“成长”,并不是成长的全部,这种成长属于个人技能层面的,也就是说只要做到一定的时间,任何人都可以达到的水平,所以很快就会迎来上升的瓶颈。那该如何破局呢?首先我们需要明确的是,成长不只是技术和技能层面的,这些只是我们胜任某项工作的必要条件。除此之外,成长还包括一些很难被看到并且量化的东西,姑且称之为软实力吧。这种软实力可能包括但不限于:



  • 沟通表达

  • 逻辑思维

  • 组织协调

  • 执行力

  • 思考力

  • 格局视野

  • ...


我列举的这些软技能,可能很多技术人都感到不屑,内心的潜台词就是:[只做好技术不行吗,为什么要关注这些东西]。其实不然,这些很难被量化的能力恰恰是非常重要的,因为这些能力是可复用的,同时也会促进个人技能的提升。


数据开发 VS 取数工具人


作为一名数据开发,你是否有如下困惑:



  • 天天写SQL取数做报表,感觉没什么技术含量

  • 需求做不完,不停地验证数据

  • 数据结论都是运营和产品向老板汇报,但是如果数据不准确,要自己背锅

  • 数据分析有数据分析师在做,数据团队只是取数

  • 对业务不了解,只是被动的接需求,不清楚需求的业务价值是什么

  • ...


以上的这种情况在多数的公司中应该都是会存在的,长此以往,会感觉工作失去了意义,开始焦虑自己不能够成长,在技术和业务的深度上积累不够,一旦失去平台的优势,似乎就成了平凡人一个。


那么,我们该如何应对上面的问题呢?下面是我的浅薄理解。


为什么会成为取数工具人


如果平台建设完备,一般进入一家公司是不需要什么都要做的,基本上是做数据产品和数据报表。首先来看一下一张报表是怎么产出的



  • 1、业务方提需求

  • 2、数据PD整理需求,并确认口径

  • 3、数据PD与数据研发评审数据需求

  • 4、数据研发开发需求

  • 5、需求交付验收

  • 6、业务方使用报表数据


不知道你有没有发现问题,这种自上(业务方)而下(数据研发)的报表产出模式,对于一个数据研发的价值是什么,整个流程中数据研发的角色就是把数据取出来,仅此而已。这就是所谓的被动接需求,如果你的工作内容一直是这样的话,那么说明你正在沦为取数工具人。


该如何破局


还是针对上面的流程,你是否思考过一下问题:


业务层面



  • 业务方为什么会提这个需求,需求的价值是什么,背后的业务逻辑和背景是什么,不做不行吗

  • 如果业务方不提这个需求,自己是否能够洞察到业务的痛点,能否提前预判到业务想要什么

  • 数据能够给业务方带来哪些信息,是如何指导决策的

  • 能否从数据中洞察出业务问题

  • 能否沉淀出一套分析方法论,使得报表更加体系化,而不是孤零零的几个指标

  • 当前业务最关注什么,你如何提供支持

  • 业界竞对是怎么做的,有哪些可以参考

  • ...


技术层面



  • 该如何去建模,如果业务变更或者需求变更,迭代的成本有多大

  • 任务是否是优化的,是否浪费计算资源

  • 代码开发是否规范,如果交给其他人维护,能很快接手吗

  • 任务SLA如何保障,如果报错改如何监控报警

  • 使用什么样的技术方案,有哪些优缺点

  • ...


如果你认真思考了每个需求对应的上面的问题,你是否还觉得自己的定位只是取数而已。你可能又会说,需求一大堆做都做不完,哪有时间思考这些东西。是的需求是很多,但绝对不能成为不思考的理由,多去想一想需求背后的问题,你一定会有所成长的。换句话说,技术是为业务服务的,技术是成本中心,如果业务都没有了,那技术呢?皮之不存毛将焉附。


回归个人成长


我这里总结几个关键词供大家参考:



  • 视野:技术和业务

  • 体系化思考

  • 持续化输出与分享

  • 目标与规划

  • 空杯心态

  • 保持信心


总结一句话:多思考、多总结、多输出。凡事多问为什么,多思考问题背后的原理和本质。工作中多做总结,积极发现问题。另外就是多分享多输出,分享可以是多样的,比如写技术博客,比如团队内部分享等等。


最后,送给大家三句话,来结束本文的内容:


低级的欲望通过放纵就可获得;


高级的欲望通过自律方可获得;


顶级的欲望通过煎熬才可获得。


学习、思考、成长,每一件事都是反人类的,只要你坚持了,自然就会比别人有所收获,以上。


总结


本文从成长的话题展开,主要聊了数据开发会遇到的瓶颈,以及该如何破局。其实,所谓的破局就是个人的成长,所以回归个人成长,本文也给出了一些浅薄的理解

作者:飞行砖家
来源:juejin.cn/post/7254901391956508729
,希望对你有所帮助。

收起阅读 »

多端登录如何实现踢人下线

1:项目背景 或者你登录了PC端,这时候你登陆了APP或者小程序,这时候PC端的账号也会被强制下线 2:项目只有PC端 假设我们现在的项目只有PC端,没有小程序或者APP,那么这时候就是很简单了,用户的sessin(也就是所谓的Token)一般都是存储在re...
继续阅读 »

1:项目背景



或者你登录了PC端,这时候你登陆了APP或者小程序,这时候PC端的账号也会被强制下线


2:项目只有PC端


假设我们现在的项目只有PC端,没有小程序或者APP,那么这时候就是很简单了,用户的sessin(也就是所谓的Token)一般都是存储在redis中,session中包括用户ID等一些信息,当然还有一个最重要的就是登录的ip地址。


image.png


1:用户在登录的时候,从redis中获取用户session,如果没有就可以直接登录了


2:用户在另外一台电脑登录,从redis中获取到用户session,这时候用户session是有的,说明用户之前已经登录过了


3:这时候从用户session中获取IP,判断二者的ip是不是相同,如果不同,这时候就要发送一个通知给客户端,让另外一台设备登录的账号强制下线即可


3:项目有PC端和APP端和小程序端


当你的应用有PC端和APP端的时候,我们用户的session如果还是只存一个ip地址,那明显就是不够的,因为很多情况下,我们PC端和APP端是可以同时登录的,比如淘宝,京东等都是,也就是所谓的双端登录


这时候就会有多种情况


单端登录:PC端,APP端,小程序只能有一端登录
双端登录:允许其中二个端登录
三端登录:三个端都可以同时登录

对于三端可以同时登录就很简单,但是现在有个限制,就是app端只能登录一次,不能同时登录,也就是我一个手机登录了APP,另外一个手机登录的话,之前登录的APP端就要强制下线


所以我们的用户session存储的格式如下


{
userId:用户的id
clientType:PC端,小程序端,APP端
imei:就是设备的唯一编号(对于PC端这个值就是ip地址,其余的就是手机设备的一个唯一编号)
}


单端登录


首先我们要知道,用户登录不同的设备那么用户session是不一样的。对于单端登录,那么我们可以拿到用户的所有的session,然后根据clientType和imei号来强制将其它端的用户session删除掉,然后通知客户端强制下线


双端登录


同样拿到所有用户的session,然后根据自己的业务需求来判定哪一端需要强制下线,比如我们现在已经登录了PC端和APP端,这时候登录小程序,现在要让APP端的强制下线。


这时候登录之后获取用户所有的session,这时候会有二个用户session,首先拿到clientType = APP的session,然后来通知客户端这个端需要强制下线。


如果这时候我登录了PC端和一个APP端,这时候我用另外一台手机登录APP端,那么之前那台手机上登录的APP端就要被强制下线,这个时候仅通过clientType是不行的,因为我二个手机登录的clientType都是APP端。所以这时候就要根据imei号来判断了。因为不同的手机imei号是不一样的。


这时候我拿到用户所有的session



PC端的session
sessionA{
userId: 1,
clientType: PC,
imei: "123"
}

APP端的session
sessionA{
userId: 1,
clientType: APP,
imei: "12345"
}

这时候我从另外一台手机登录的时候,生成的session应该是这样的


 APP端的session
sessionA{
userId: 1,
clientType: APP,
imei: "1234567"
}

我发现同一个clientType的session已经有了,这时候我要判断imei号是否一样,imei一样说明是同一台设备,不同说明不是同一台设备,我们只需要把对应设备的账号强制下线即可了


总结


不管是单端登录,双端登录还是多端登录,我们都是根据用户session来判断。只要根据clientType和imei号来就可以满足我们大部

作者:我是小趴菜
来源:juejin.cn/post/7213598216884486204
分的踢人下线需求了。

收起阅读 »

网站“重定向次数过多”问题排查

ERR_TOO_MANY_REDIRECTS 不久前部署了一个网站,访问时却直接打不开: 当前无法使用此页面 xxx.com 重定向次数过多 若要解决此问题,请尝试清除 Cookie. ERR_TOO_MANY_REDIRECTS 我的网络架构如下: gr...
继续阅读 »

ERR_TOO_MANY_REDIRECTS


不久前部署了一个网站,访问时却直接打不开:



当前无法使用此页面 xxx.com 重定向次数过多


若要解决此问题,请尝试清除 Cookie.


ERR_TOO_MANY_REDIRECTS



我的网络架构如下:


graph LR
subgraph A["VLAN"]
subgraph B["Local Server"]
C("nginx server")
end
subgraph D["VPS"]
E("nginx server")
end
end
F["Internet"] --http/https--> E
E --http--> C


其中网站部署在 Local Server 的 Nginx 服务器上,在 VPS 上再用 Nginx 做反向代理,并不复杂。


Nginx


VPS 上的 Nginx 主要配置如下:


	upstream mycloud {
# Local server
server archlinux:5173;
}

server {
listen 443 ssl;
server_name xxx.com;
...
#证书
ssl_certificate /data/nginx/cert/xxx.com_bundle.pem;
ssl_certificate_key /data/nginx/cert/xxx.com.key;
...
location / {
proxy_pass http://mycloud;
...
}
}

server {
listen 80;
server_name xxx.com;
...
#核心转发代码
rewrite ^(.*)$ https://${server_name}$1 permanent;
}

上述配置主要做了两个事情,一是将访问的 https 流量代理到 archlinux:5173,二是将访问的 http 请求转发到 https。


怎么看都不像能导致无限重定向的样子。


再三确认配置无误后,我看了一眼浏览器 Network,在一众 301 的列表中,请求全被重定向到了一个网址:104.21.27.176,再一查,好家伙,原来是 CloudFlare Load Balancer!


这时我才想起来曾经在 Cloudflare 上开启了 DNS 服务。


Cloudflare


那么问题来了,会是 DNS 服务导致的无限重定向吗?


登录 Cloudflare 看了一下,SSL/TLS 加密模式 设置成了 flexible , 这下真相大白了!


已知 SSL/TLS加密模式 有如下选项:





  • 关闭(不安全)


    未应用加密




  • 灵活


    加密浏览器与 Cloudflare 之间的流量




  • 完全


    端到端加密,使用服务器上的自签名证书




  • 完全(严格)


    端到端加密,但服务器上需要有受信任的 CA 证书或 Cloudflare Origin CA 证书





由于应用的是 灵活 , 所以只加密了浏览器与 Cloudflare 之间的流量,并没有加密 Cloudflare 到 VPS 服务器的流量。


什么原理呢:


当域名的 DNS 记录指向 Cloudflare 后,所有的流量都将经过 Cloudflare 的代理服务器。


而在 Nginx 配置中,我使用了 rewrite 规则将 HTTP 请求重定向到 HTTPS。然而,由于 Cloudflare 代理了流量,它将请求转发给 VPS 服务器时,仍然是通过 HTTP 连接进行。这导致了一个循环:请求通过 HTTPS 到达 Cloudflare,然后被转发为 HTTP 请求到 VPS,然后 VPS 再次重定向到 HTTPS, 无限循环了。


问题清楚了,解决方案有如下选择:




  1. 在 Nginx 中取消 http 到 https 的 rewrite。




  2. 加密 Cloudflare 到 VPS 的流量。




  3. 在 Nginx 配置中使用代理服务器的原始协议(X-Forwarded-Proto)来判断是否启用 HTTPS,例如


    if ($http_x_forwarded_proto != "https") {
    rewrite ^(.*)$ https://${server_name}$1 permanent;
    }

    这将确保当请求通过 HTTPS 到达 Cloudflare 时,Cloudflare 会在转发请求时设置 X-Forwarded-Proto 头部字段为 https,然后服务器将检查该字段,并决定是否进行重定向。




毫无疑问最简单高效的方法是直接将 SSL/TLS 加密模式 设置成 完全(严格) 就行了。


点击一下选项,测试网页,问题解决。


Conclusion


从这个事情需要认识到,在使用 Cloudflare 或者其它服务时,要确保 Nginx 配置和 Cloudflare 设置之间的一致性,以避免任何不必要

作者:looko
来源:juejin.cn/post/7254572372136738853
的重定向或连接问题。

收起阅读 »

小程序自定义导航栏

web
小程序布局 谈到导航栏与自定义导航栏,就需要解释一下微信小程序的布局了。在小程序开发中使用wx.getSystemInfoAsync() 方法可以获取到系统信息。 部分获取到的信息如上图(截取自微信小程序开发者文档),对我们理解布局有用的信息是以上...
继续阅读 »

小程序布局




  • 谈到导航栏与自定义导航栏,就需要解释一下微信小程序的布局了。在小程序开发中使用wx.getSystemInfoAsync() 方法可以获取到系统信息。


    image.png


    image.png




  • 部分获取到的信息如上图(截取自微信小程序开发者文档),对我们理解布局有用的信息是以上跟宽度高度相关的属性,如当前设备的屏幕高宽,可用高宽,以及saveArea





  • 上图展示我们从systemInfo获取到的数据的实际表现,以苹果X的刘海屏为例(所有安卓刘海屏原理类似):最外层的红色框即屏幕大小,蓝色框即安全区域字面意思也就是开发者所能操纵的页面区域,上面的黄色框即手机的状态栏,绿色区域即我们要自定义的navigationBar




  • 可见,导航栏紧贴safeArea的上部,如果使用原生导航栏,导航栏下方即是真正意义的可操控范围。




  • 实际上我们自定义的导航栏也是在这个safeArea内与胶囊对齐最为和谐。很关键的原因就是微信将右上角的胶囊按钮作为了内置组件,只有黑白两种颜色,即我们无法改变它的大小位置透明度等等,所以为了配合胶囊按钮,一般自定义的导航栏位置也与上图位置一致。




自定义navigationBar怎么做?


去掉原生导航栏。



  1. 将需要自定义navigationBar页面的page.json的navigationBarTitleText去掉。

  2. 加上 "navigationStyle":"custom" ,这样原生的导航栏就已经消失,甚至后退键也不会出现需要自定义。

  3. 另外,早在2016年微信已经开始适配沉浸式状态栏,目前几乎所有的机型里微信都是沉浸式状态栏,也就是说去掉原生导航栏的同时,整个屏幕已经成为可编程区域


计算navigationBarHeight。



  • 原生的胶囊按钮当然存在,那么下一步就需要你去定位出自定义的导航栏高度以及位置。

  • 对于不同的机型,对于不同的系统,状态栏以及胶囊按钮的位置都不确定,所以需要用到一定的计算,从而面对任何机型都可以从容判定。




  1. 使用wx.getSystemInfoSync() 获取到statusBarHeight,这样就确定了导航栏最基本的距离屏幕上方的距离。




  2. 使用wx.getMenuButtonBoundingClientRect() 获取到小程序的胶囊信息(注意这个api存在各种问题,在不同端表现不一致,后面会叙述这个api调用失败的处理情况),如下图,以下坐标信息以屏幕左上角为原点。





  3. 以下图为例,上面的红色框是statusBar,高度已知;下面的红色框是正文内容,夹在中间的就是求解之一navigationBarHeight;而黄色的是原生胶囊按钮也是在垂直居中位置,高度为胶囊按钮基于左上角的坐标信息已知,不难得出,navigationBarHeight = 蓝色框高度 × 2 + 胶囊按钮.height。(蓝色框高度 = 胶囊按钮.top - statusBarHeight






  1. 最后的计算公式为:navigationBarHeight = (胶囊按钮.top - statusBarHeight) × 2 + 胶囊按钮.height。navigationBar 距屏幕上方的距离即为navigationBarHeight

  2. 这种计算方法在各种机型以及安卓ios都适用。

  3. 针对"wx.getMenuButtonBoundingClientRect() "获取错误或者获取数据为0的极少数情况,只能够去模拟,对于android,一般navigationBarHeight为48px,而对于ios一般为40px,所有机型的胶囊按钮高度是32px。



代码实现



  • 获取本机信息,写在组件的attached生命周期中。


// components/Navigation/index.js
Component({
/**
* 组件的属性列表
*/

properties: {

},

/**
* 组件的初始数据
*/

data: {
navigationBarHeight: 40,
statusBarHeight:20,
},

/**
* 组件的方法列表
*/

methods: {

},
lifetimes: {
attached: function () {
const { statusBarHeight, platform } = wx.getSystemInfoSync();
const { top, height = 32 } = wx.getMenuButtonBoundingClientRect();// 胶囊按钮高度 一般是32 如果获取不到就使用32
// 判断胶囊按钮信息是否成功获取
if (top && top !== 0 && height && height !== 0) {
//获取成功进行计算
const navigationBarHeight = (top - statusBarHeight) * 2 + height;
console.log(navigationBarHeight)
// 导航栏高度
this.setData({
navigationBarHeight,
statusBarHeight
})
} else {
//获取失败使用默认的高度
this.setData({
navigationBarHeight: platform === "android" ? 48 : 40,
statusBarHeight
})
}
}
}
})



  • 组件模板编写


<view class="custom-nav" style="height: {{navigationBarHeight}}px;margin-top:{{statusBarHeight}}px;">
<view>
<image style="width: 40rpx;height:40rpx;" src="/images/location.svg" mode="" />
</view>
</view>


 .navigationBar.wxml 样式如下:


.custom-nav{
background-color:palegoldenrod;
display: flex;
align-items: center;
}
.custom-nav__title{
margin:auto
}

外部页面引用该组件如下,


.json文件,引入组件


{
"usingComponents": {
"my-navigation":"/components/Navigation"
},
"navigationStyle": "custom"
}

注意添加属性:"navigationStyle":"custom"  代表我们要自定义组件


.wxml代码如下:


<view>
<my-navigation></my-navigation>
<view class="page-container" style="background-color: rebeccapurple;">这里是页面内容</view>
</view>


最终效果
image.png


如果想要编写更加通用的组件,可以根据需求定义传入的参数和样式


参考链接


http://www.cnblogs.com/chenwo

作者:let_code
来源:juejin.cn/post/7254812719349858361
long/…

收起阅读 »

Progress 圆形进度条 实现

web
效果图 实现过程分析 简要说明 本文主要以 TypeScript + React 为例进行讲解, 但相关知识和这个关系不大. 不会也不影响阅读 dome 中使用到了 sass, 但用法相对简单, 不影响理解 HTML DOM 元素说明 <div c...
继续阅读 »

效果图



实现过程分析


简要说明



  • 本文主要以 TypeScript + React 为例进行讲解, 但相关知识和这个关系不大. 不会也不影响阅读

  • dome 中使用到了 sass, 但用法相对简单, 不影响理解


HTML DOM 元素说明


<div className="g-progress-wrap">
<div className="g-progress"></div>
<div className="g-circle">
<span className="g-circle-before"><i/></span>
<span className="g-circle-after"><i/></span>
</div>
<div className="g-text">
20%
</div>
</div>


  • g-progress-wrap 包裹 progress, 所有的内容都在这里面

  • g-progress 主要的区域

  • 为了保证圆环有圆角效果 g-circle 内的有 2 个小圆, 放置到圆环的开始和结尾

  • g-text 放置文字区域



上面已经介绍了 html, 因为主要的处理都在css, 所以接下来只说 css



第一步, 实现一个圆


.g-progress {
width: 100px;
height: 100px;
border-radius: 50%;
background: conic-gradient(#1677ff 0, #1677ff 108deg, #eeeeee 108deg, #eeeeee 360deg);
}

image.png




  • border-radius: 50%; 实现圆形




  • 使用 background 实现背景颜色



    • conic-gradient 创建了一个由渐变组成的图像,渐变的颜色变换围绕一个中心点旋转

    • 当角度为 0 - 108deg 时, 颜色为: #1677ff; 当角度为 108deg - 360deg 时, 颜色为: #eeeeee;




第二步, 实现圆环效果


.g-progress {
/* 新增代码 */
/* mask: radial-gradient(transparent, transparent 44px, #000 44.5px, #000 100%); */
-webkit-mask: radial-gradient(transparent, transparent 44px, #000 44.5px, #000 100%);
}

image.png




  • 通过使用 mask属性, 隐藏 中间区域的显示




  • radial-gradient 创建一个图像,该图像由从原点辐射的两种或多种颜色之间的渐进过渡组成



    • 当为 0 - 44px 时, 颜色为: transparent; 当为 44px - 100% 时, 颜色为: #000;

    • 设置为 transparent 时, transparent 的区域的颜色会被隐藏




  • 为什么不使用元素覆盖, 使用中间区域的隐藏



    • 如果用元素覆盖实现的话, 如果需要显示父级的背景色时, 没办法实现




第三步, 实现圆环的圆角效果


.g-circle {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: rotate(-90deg);
&>span {
position: absolute;
top: 47px;
left: 50px;
width: 50%;
transform-origin: left;
&>i {
width: 3px;
height: 3px;
float: right;
border-radius: 50%;
background: #1677ff;
z-index: 1;
}
}
& .g-circle-after {
transform: rotate(0deg);
}
}

image.png


第四步, 文字效果处理


.g-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
color: #666666;
}

image.png


第五步, 进度变化时, 通过js更新


通过行内样式更新 rotate 的方式即可更新进度


参考文档


developer.mozilla.org/zh-CN/docs/…


developer.mozilla.org/zh-CN/docs/…


http://www.cnblogs.com/coco1s

作者:洲_
来源:juejin.cn/post/7254450297467781176
/p/15…

收起阅读 »

当你的服务挂了,该怎么排查服务挂了的原因

1. 背景 某天凌晨一点多,服务挂了,日志戛然而止,grafanar监控内存,cpu、磁盘都是正常的,该怎么去排查 2. 排查手段 观测日志、是否有程序触发关闭jvm进程,system.exit(),观测内存,cpu,磁盘,是否有因为机器资源不够分配的问题导...
继续阅读 »

1. 背景


某天凌晨一点多,服务挂了,日志戛然而止,grafanar监控内存,cpu、磁盘都是正常的,该怎么去排查


2. 排查手段



观测日志、是否有程序触发关闭jvm进程,system.exit(),观测内存,cpu,磁盘,是否有因为机器资源不够分配的问题导致进程被机器kill




观测机器的操作日志/var/log/messages,直接搜索kill 的日志



messages 日志是核心系统日志文件。它包含了系统启动时的引导消息,以及系统运行时的其他状态消息。IO 错误、网络错误和其他系统错误都会记录到这个文件中。其他信息,比如某个人的身份切换为 root,也在这里列出。如果服务正在运行,比如 DHCP 服务器,您可以在 messages 文件中观察它的活动。通常,/var/log/messages 是您在做故障诊断时首先要查看的文件。


/var/log/messages文件中存放的就是系统的日志信息,当内核程序调试时,printk语句所产生的信息显示不出来的时候,就使用cat /var/log/messages文件的方法,查看所打印出的信息.



都没问题了,直接联系运维看一下,开发也没有绝对百分百的手段判断进程挂的原因,最终判断为阿里云系统错误导致机器重启


作者:斯瓦辛武
来源:juejin.cn/post/7254542743098818621

收起阅读 »

了解短信的实现原理以及验证码短信API

前言 短信作为一种便捷、快速的通信方式,已经在我们的日常生活中得到广泛应用。无论是个人通信、企业沟通还是身份验证等场景,短信都发挥着重要的作用。而实现短信功能的核心是短信实现原理和验证码短信API。 本文将介绍短信实现的基本原理以及 验证码短信API,帮助读者...
继续阅读 »

前言


短信作为一种便捷、快速的通信方式,已经在我们的日常生活中得到广泛应用。无论是个人通信、企业沟通还是身份验证等场景,短信都发挥着重要的作用。而实现短信功能的核心是短信实现原理和验证码短信API。


本文将介绍短信实现的基本原理以及 验证码短信API,帮助读者更好地了解短信技术和应用。


实现原理(步骤)





  1. 触发事件:通知短信的实现通常是作为某种事件的响应而触发的。例如,用户完成了注册、下单、密码重置等操作,这些事件可以触发发送通知短信。




  2. 业务逻辑处理:在触发事件后,相关的业务逻辑将被执行。这可能包括生成通知内容、确定接收者等。




  3. 调用短信服务提供商的API:为了发送短信,系统将调用短信服务提供商的API。这些提供商通常是专门的短信网关或通信运营商,提供发送短信的基础设施和服务。




  4. 构建短信内容:在调用短信服务提供商的API之前,系统需要构建短信的内容。这包括编写文本消息、添加动态变量或链接等。通常,短信内容可以包含特定的占位符,用于在发送时插入动态数据,如用户名、订单号等。




  5. 调用短信服务API发送短信:使用短信服务提供商的API,系统将发送短信请求。这通常涉及向API端点发送HTTP请求,包括目标手机号码、短信内容和身份验证信息等。




  6. 短信服务商处理:短信服务提供商接收到发送短信的请求后,会进行一系列的处理步骤。这可能包括验证发送者的身份、检查短信内容的合法性、处理短信队列等。




  7. 短信传递:一旦短信服务提供商完成处理,它会将短信传递到相应的目标手机号码。这通常是通过与移动网络运营商之间的通信渠道实现的。




  8. 接收短信:目标手机号码的手机将接收到短信,并在短信应用程序中显示。用户可以查看和阅读通知短信的内容。




验证码短信API



在短信实现原理中,必不可少的一个东西就是 —— 短信API,只有调用了 短信API 我们才能把短信发送出去。


在 短信API 中最常见的就是 验证码短信API通知短信API。在之前说过了通知短信,今天就说一说 验证码短信API。短信API 我们可以去网上各个平台查看,我这里使用的是 APISpace验证码短信API~


以 JavaScript 为例的调用示例代码:


var data = "{"msg":"Eolinker】尊敬的用户{$var},欢迎联调通知短信。","params":"15800000000,张先生;13200000000,王小姐","sendtime":"","extend":"","uid":""}"

$.ajax({
"url":"https://eolink.o.apispace.com/sms-notify/notify",
"method": "POST",
"headers": {
"X-APISpace-Token":"",
"Authorization-Type":"apikey",
"Content-Type":"application/json"
},
"data": data,
"crossDomain": true
})
.done(function(response){})
.fail(function(jqXHR){})

验证码短信应用场景




  1. 注册和登录验证:许多网站、应用和服务在用户注册和登录过程中使用验证码短信来验证用户的身份。用户在提供手机号码后,会收到包含验证码的短信,然后需要输入验证码才能完成注册或登录过程。这样可以确保用户提供的手机号码是有效的,并增加账户的安全性。




  2. 密码重置和账户安全:当用户忘记密码或账户出现异常时,验证码短信可以用于重置密码或确保账户安全。通过发送验证码短信,用户可以通过验证自己的身份来重新设置密码,或者确认是否进行了某些账户操作,如更改手机号码或绑定新设备。




  3. 手机号码验证:许多平台需要验证用户提供的手机号码的真实性,以保护用户账户的安全性。验证码短信可以用于验证用户拥有指定手机号码,并通过让用户输入验证码来确认其所有权。




  4. 交易和支付安全:在电子商务和移动支付中,验证码短信被广泛用于交易和支付的安全验证。用户在进行支付或敏感操作时,会收到包含验证码的短信,需要输入正确的验证码才能完成交易或操作,以防止未经授权的访问和欺诈行为。




  5. 帐户活动通知:验证码短信也可以用于向用户发送帐户活动通知,例如当用户进行重要操作、更改账户信息、进行高风险活动等时,发送验证码短信以提醒用户并增加账户的安全性。




结束语


通过本文的介绍,我们对短信实现原理以及 验证码短信API 有了一定的了解。短信作为一种简单而高效的通信方式,在各个领域都发挥着重要的作用。验证码短信API为开发者提供了便捷的工具,使他们能够轻松地集成和使用验证码短信功能。无论是个人用户还是企业开发者,都可以利用短信技术和API来实现更安全、高效的通信和身份验证。随着移动通信技术的不断发展,我们相信短信技术将继续在各个领域发挥重要作用,并为我们的生活带

作者:爱分享的程序员
来源:juejin.cn/post/7254384497658429499
来更多便利和安全性。

收起阅读 »

我的2023年上半年总结

🍉 写在前面 春节仿佛不在昨天,端午节还是上个月的事情,转眼间2023年已经过半。分享和总结一下自己过去的这7个月吧! 现在是2023年7月12号的早上八点半,做半年总结,说真的,感觉应该大概是有很多的话要说的,毕竟都过去半年了,但是真的写起来,才发现半点文思...
继续阅读 »

🍉 写在前面


春节仿佛不在昨天,端午节还是上个月的事情,转眼间2023年已经过半。分享和总结一下自己过去的这7个月吧!


现在是2023年7月12号的早上八点半,做半年总结,说真的,感觉应该大概是有很多的话要说的,毕竟都过去半年了,但是真的写起来,才发现半点文思泉涌没有,只剩才尽思竭,根本不知道从何说起了。还好有个模板,就按着这个自动生成模板简单总结一下吧。
纯粹是一篇个人总结,不带其它讯息🍅。


🍊 一、目标达成情况总结:




  1. 到了一个暖和的地方,找到了自己认为合适的工作
    今年年初的2月27日,自己离开了工作一年半的一家公司,离开了自己待了近两年的成都市,根据自己好兄弟的推荐来到了现在的所在地:佛山,在成都待了一年多的时候就想离开了,以前服役的时候就在佛山待过,所以对这边的气候也了解一些,之所以离开成都也是感觉冬天暖和的地方呆久了,就不怎么喜欢冬天的那股寒风了,最开始想去的是海南,因为好兄弟就是海南的,但是后面他说他以后去佛山,我也先提前到佛山发展了。
    来到这边首先肯定的是先找工作,还好,因为自己技术面比普通的前端工程师全面一点,所以,在很多人都说是“互联网寒冬的时候”,我用了不到两个周,找到了自己比较满意的一份工作,当时拿到了两个offer,可能也是大家比较诟病的职业位置,一个属于外包,一个是培训机构。后面根据自己的想法,选择了现在的这家公司。公司属于中间商性质的,公司在广东,而上班的地点是在佛山,给某大厂的安全运营团队做开发,薪资上也涨幅了2k,更多的细节不方便透露,抱歉抱歉。
    以上就是自己认为完成的第一个目标

  2. 认识了更多的朋友
    来这边是好兄弟介绍过来的,因此这边也有对他很重要的人,过来后也认识了更多的朋友,感觉圈子不像以前在成都那么窄了,舒服了很多,周末也会出去跑跑步(说到这里提一句,佛山千灯湖跑步简直不要太舒服,一圈刚好十公里,是我比较喜欢的长度)另外自己偶尔也会去骑行一下,逛一下佛山,上个周骑行去了西樵山,来回70公里左右,天气太热,大热的天差点给自己送走了,太热了🥵

  3. 遇到了想守护一生的她
    这个要不后续再说吧,说起来可要说太久了。
    总的来说,来到佛山,完成了三个目标,有的还是意想不到的,属实三生有幸。



🍋 二、工作/学习成果总结:



工作以及学习成果的总结的话,从自己目前的情况来看,主要有以下几点吧:



  1. 学习了新的python Web框架(django)
    这个框架以前自己只是耳闻,根本不曾用过,用python做后端自己用的比较多的是flask,也用flask做过一些小系统(自己接的一些小外包)。django是现在这家公司开发的主要后端框架,可能很多人会说为什么不用Java,这个也不用惊讶了,别人领导说用什么就用什么,我们扩宽自己的技术面即可,不用去纠结那些。别人招人的时候也是找符合自己公司条件的人,用Java做系统开发确实是主流,但是也有人不愿意用不是。360的老大说的挺好的,来上班就是公司花钱给你学习。既然能用工作时间去学习更多的知识,何乐而不为呢,对吧

  2. 学习了Java的spring-boot框架
    以前自己也学过一些Java,但是因为用不上,所以也差不多忘完了,入职后有的时候没有事情,比较闲,就想着用Java也做一个后端demo吧,对自己的技术面也有提高。
    目前使用spring-boot写后端,使用MyBatis或者直接使用JdbcTemplate来操作数据库以及对文件上传下载方面都没什么大的问题了,因为相对来说这些都是比较基础简单的。

  3. 知道了怎么做微信小程序的消息推送
    可能有的行家看到这个就要笑了😀,以前自己用云开发了一个微信小程序,当时对小程序开发感觉基本没问题了,就没有再去学习小程序的其它功能了,在上个月的样子吧,偶然刷到一个用微信公众号去推送消息给自己女朋友的。感觉作为一个程序员来说挺有意思的,我就想着自己去弄一个,但是我没有去弄微信公众号,就换成了用小程序去推送,基本这个想法又去重新做了一个“恋爱日记”类的小程序,通过小程序发个早安晚安,记录一下认识的时间天数,双方的生日,下一个节假日有多久,感觉还是有一些意义的(相对于直男来讲是这样)。最后附上小程序的一个截图,希望对看到的人自己做设计小程序的时候有帮助。

  4. 学习了一下新的前端系统框架: vue-admin
    说真的,刚开始对公司必须要用这个框架感觉有点奇怪的,因为之前在成都的时候公司都是用的vue3,然后自己搭建前端系统框架的,感觉代码写起来很舒服,干净整洁。没有用过一些网上的成品的系统框架,来这边后这个vue-admin是自己接触的第一个系统框架,使用的是vue2,因为之前都是写vue3,现在写vue2感觉vue2写起来挺麻烦的,vue3的setup语法糖写前端简直不要太舒服。但是后面用了下,感觉不说真香吧,也还行,相比于自己手动搭建的框架,咱是不是又多学到了一个东西。对吧
    差不多就这些吧,记录一下就OK。



🍍 三、下半年规划总结:



感觉主要还是扩宽自己的技术面吧,目前想继续深入学习的是Java,毕竟Java目前还是主流的后端语言,虽然后起之秀golang已经在全力追赶Java和python了,但是真的要替代的话应该需要不少时间吧,各种语言有各种语言的优点,先从简单的学起来吧。最后记录一下之前在知乎上看到的一句话,感觉挺好
“生活要忙忙碌碌随大流,思想要偷偷摸摸求上进”
希望每一个眼里有光的人都有一个不错的未来,回首都是不负未来,抬眼都是蓝天白云(最近确实这样,太热了)。



🍓 四、最后


小程序截图,有的地方做了马赛克🤣,请不要见外,见谅见谅,个人隐私还是简单保护一下。🐬


在这里插入图片描述


就到这里,提前祝所有人中秋节快乐,半年

作者:讷言丶
来源:juejin.cn/post/7254542743098835005
总结完毕。撒花结束🌻

收起阅读 »

我的师父把 「JWT 令牌」玩到了极致

你好,我是悟空。 我的师父是唐玄奘~ 西游记的故事想必大家在暑假看过很多遍了,为了取得真经,唐玄奘历经苦难,终于达成。 在途经各国的时候,唐玄奘都会拿出一个通关文牒交给当地的国王进行盖章,方能通过。 本篇目录如下: 通关文牒 通关文牒就是唐朝官方发的一个凭证...
继续阅读 »

你好,我是悟空。


我的师父是唐玄奘~


西游记的故事想必大家在暑假看过很多遍了,为了取得真经,唐玄奘历经苦难,终于达成。


在途经各国的时候,唐玄奘都会拿出一个通关文牒交给当地的国王进行盖章,方能通过。


本篇目录如下:


图片


通关文牒


通关文牒就是唐朝官方发的一个凭证,证明持有人来自东土大唐,一般是使臣持有。


有了这个凭证后,到其他国家,比如女儿国国王看到这个凭证后,就会放行。


下面来一张西游记中通关文牒的生命周期图。


图片


长安是一个颁发凭证(通关文牒)的微服务节点,乌鸡国、女儿国和大雷音寺等都是集群中的一个微服务节点,唐玄奘拿着凭证访问各国。


那为什么别的国家认可这个凭证呢?


那是因为当时的唐朝非常强大,有很多国家都要向唐朝朝贡,与唐朝交好有很多好处的~


朝贡也有篇故事哦~唐太宗把微服务的“心跳机制”玩到了极致!


唐太宗在通关文牒上写道:“倘到西邦诸国,不灭善缘,照牒放行,须至牒者。


图片


意思就是说唐玄奘法师是我们唐朝的使臣,如果途经诸侯国,希望大家放行。


贞观之治时期的唐朝是在经济文化上都无比繁盛,国力强盛,周边国家都希望和唐朝建立友好关系,看到是唐朝使臣来了,好生招待下,然后盖章放行,给唐朝留个好印象。


在安全架构中,凭证 出现得太频繁了,比如我们在网关这一层加的校验令牌,其实就是校验凭证。


凭证是什么


凭证(Credentials)的出现就是系统保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的。


那唐太宗给唐玄奘的通关文牒就是一个凭证,上面盖着唐朝的官印、唐太宗的亲笔,这充分体现了持有者是拥有一个可信的令牌的,而且这个通关文牒上的官印是不可篡改的,如果改了,其他国家就不认了


上面这种模式其实对应的是一种普通的认证授权模式,而大名鼎鼎的 OAuth 2.0 认证授权模式虽然有五种模式,但他们殊途同归,最后的目的都是生成一个凭证给到客户端,让客户端持有这个凭证来访问资源。关于 OAuth2.0 本篇不做展开。


关于凭证的存储方案,业界的安全架构中有两种方案:



  • Cookie-Session 模式

  • JWT 方案


Cookie-Session 模式


流程图如下:


图片


用户登录认证通过后,后端会存放该客户端的身份信息,也就是存放到 session 中,session 可以用来区分不同,然后返回一个 sessionId 给到客户端。


客户端将 sessionId 缓存在客户端。当客户端下次发送 HTTP 请求时,在 header 的 cookie 字段附带着 sessionId 发送给后端服务器。


后端服务器拿到 header 中的 sessionId,然后根据 sessionId 找到 session,如果 session 存在,则从 session 中解析出用户的身份信息,然后执行业务逻辑。


我们都知道 HTTP 协议是一种无状态的传输协议,无状态表示对一个事务的处理没有上下文的记忆能力,每一个 HTTP 请求都是完全独立的。但是 Cookie-Seesion 模式却和 HTTP 无状态特性相悖,因为客户端访问资源时,是携带第一次拿到的 sessionId 的,让服务端能够顺利区分出发送请求的用户是谁。


服务端对 session 的管理,就是一种状态管理机制,该机制存储了每个在线用户的上下文状态,再加上一些超时自动清理的管理措施。Cookie-Session 也是最传统但今天依旧应用到大量系统中,由服务端与客户端联动来完成的状态管理机制。


放到西游记中,如果用这种 Cookie-Session 模式是怎么样的呢?



我们把唐朝和周边国家想想成一个分布式集群,所有国家都需要将唐玄奘这个使者信息都保存一份(分布式存储),当唐玄奘路过某个国家时,需要查询本地存储中是否有唐玄奘,如果有,则认为唐玄奘是合法的使者,可以放行。



但是这种方式就会需要每个国家都同步保存,同步的成本是非常高昂的,而且会有同步延迟的存在


Cookie-Session 模式的优势


状态信息都存储于服务器,只要依靠客户端的同源策略和 HTTPS 的传输层安全,保证 Cookie 中的键值不被窃取而出现被冒认身份的情况,就能完全规避掉上下文信息在传输过程中被泄漏和篡改的风险。Cookie-Session 方案的另一大优点是服务端有主动的状态管理能力,可根据自己的意愿随时修改、清除任意上下文信息,譬如很轻易就能实现强制某用户下线的这样功能。(来自凤凰架构)


Cookie-Session 模式的劣势


在单节点的单体服务中再适合不过,但是如果需要水平扩展要部署集群就很麻烦。


如果让 session 分配到不同的的节点上,不重复地保存着一部分用户的状态,用户的请求固定分配到对应的节点上,如果某个节点崩溃了,则里面的用户状态就会完全丢失。如果让 session 复制到所有节点上,那么同步的成本又会很高。


而为了解决分布式下的认证授权问题,并顺带解决少量状态的问题,就有了 JWT 令牌方案,但是 JWT 令牌和 Cookie-Session 并不是完全对等的解决方案,JWT 只能处理认证授权问题,且不能说 JWT 比 Cookie-Session 更加先进,也不可能全面取代 Cookie-Seesion 机制。


JWT 方案


我们上面说到 Cookie-Session 机制在分布式环境下会遇到一致性和同步成本的问题,而且如果在多方系统中,则更不能将 Session 共享存放在多方系统的服务端中,即使服务端之间能共享数据,Cookie 也没有办法跨域。


转换思路,服务端不保存任何状态信息,由客户端来存储,每次发送请求时携带这个状态信息发给后端服务。原理图如下所示:


图片


但是这种方式无法携带大量信息,而且有泄漏和篡改的安全风险。信息量大小受限没有比较好的解决方案,但是确保信息不被中间人篡改则可以借助 JWT 方案。


JWT(JSON WEB TOKEN)是一种令牌格式,经常与 OAuth2.0 配合应用于分布式、多方系统的应用系统中。


我们先来看下 JWT 的格式长什么样:


图片


以上截图来自 JWT 官网(jwt.io),数据则是悟空随意编的。


左边的字符串就是 JWT 令牌,JWT 令牌是服务端生成的,客户端会拿着这个 JWT 令牌在每次发送请求时放到 HTTP header 中。


而右边是 JWT 经过 Base64 解码后展示的明文内容,而这段明文内容的最下方,又有一个签名内容,可以防止内容篡改,但是不能解决泄漏的问题。


JWT 格式


JWT 令牌是以 JSON 结构存储,用点号分割为三个部分。


图片


第一部分是令牌头(Header),内容如下所示:


{
  "alg": "HS256",
  "typ": "JWT"
}

它描述了令牌的类型(统一为 typ:JWT)以及令牌签名的算法,示例中 HS256 为 HMAC SHA256 算法的缩写,其他各种系统支持的签名算法可以参考jwt.io/网站所列。


令牌的第二部分是负载(Payload),这是令牌真正需要向服务端传递的信息。但是服务端不会直接用这个负载,而是通过加密传过来的 Header 和 Payload 后再比对签名是否一致来判断负载是否被篡改,如果没有被篡改,才能用 Payload 中的内容。因为负载只是做了 base64 编码,并不是加密,所以是不安全的,千万别把敏感信息比如密码放到负载里面。


{
  "sub": "passjava",
  "name": "悟空聊架构",
  "iat": 1516239022
}

令牌的第三部分是签名(Signature),使用在对象头中公开的特定签名算法,通过特定的密钥(Secret,由服务器进行保密,不能公开)对前面两部分内容进行加密计算,以例子里使用的 JWT 默认的 HMAC SHA256 算法为例,将通过以下公式产生签名值:


HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)

签名的意义:确保负载中的信息是可信的、没有被篡改的,也没有在传输过程中丢失任何信息。因为被签名的内容哪怕发生了一个字节的变动,也会导致整个签名发生显著变化。此外,由于签名这件事情只能由认证授权服务器完成(只有它知道 Secret),任何人都无法在篡改后重新计算出合法的签名值,所以服务端才能够完全信任客户端传上来的 JWT 中的负载信息。


JWT 的优势



  • 无状态:不需要服务端保存 JWT 令牌,也就是说不需要服务节点保留任何一点状态信息,就能在后续的请求中完成认证功能。

  • 天然的扩容便利:服务做水平扩容不用考虑 JWT 令牌,而 Cookie-Session 是需要考虑扩容后服务节点如何存储 Session 的。

  • 不依赖 Cookie:JWT 可以存放在浏览器的 LocalStorage,不一定非要存储在 Cookie 中。


JWT 的劣势



  • 令牌难以主动失效:JWT 令牌签发后,理论上和认证的服务器就没有什么关系了,到期之前始终有效。除非服务器加些特殊的逻辑处理来缓存 JWT,并来管理 JWT 的生命周期,但是这种方式又会退化成有状态服务。而这种要求有状态的需求又很常见:譬如用户退出后,需要重新输入用户名和密码才能登录;或者用户只允许在一台设备登录,登录到另外一台设备,要求强行退出。但是这种有状态的模式,降低了 JWT 本身的价值。

  • 更容易遭受重放攻击:Cookie-Session 也有重放攻击的问题,也就是客户端可以拿着这个 cookie 不断发送大量请求,对系统性能造成影响。但是因为 Session 在服务端也有一份,服务端可以控制 session 的生命周期,应对重放攻击更加主动一些。但是 JWT 的重放攻击对于服务端来说就很被动,比如通过客户端的验证码、服务端限流或者缩短令牌有效期,应用起来都会麻烦些。

  • 存在泄漏的风险:客户端存储,很有可能泄漏出去,被其他人重复利用。

  • 信息大小有限:HTTP 协议并没有强制约束 Header 的最大长度,但是服务器、浏览器会做限制。而且如果令牌很大还会消耗传输带宽。


真假美猴王


西游记中还有一个章节,假的美猴王带着通关文牒和其他行李跑到了花果山,还想自行取经,这不就是盗用  JWT 令牌了吗?


如何使用 JWT


Java 有现成的工具类可以使用,而且校验 JWT 的工作可以统一交给网关来做,这个就是下一篇要重点讲解的实战内容了。


总结


唐玄奘就好比客户端,通关文牒就好比 JWT 令牌,经过的每个国家就好比集群中的微服务。


唐玄奘借助 JWT 令牌的认证授权模式,一路通关,最终取得真经,是不是很酷呀~


下一篇:手摸手实战 Spring Cloud Gateway + JWT 认证功能


参考资料:


《凤凰架构》


《OAuth2.0 实战》

作者:悟空聊架构
来源:juejin.cn/post/7250029300820869178

收起阅读 »

记录一次小程序开发中的各种奇葩bug

web
前段时间,跟好哥们儿商量了一下,帮他公司设计并开发一款宣传用的小程序。因为是宣传用的,所以对于后台数据几乎没什么需求,只需要用到一些接口,引导用户联系公司,且公司性质是古建筑装修,没有自己的服务器。所以我直接给他做的是个静态的小程序。 微信小程序的开发需要注意...
继续阅读 »

前段时间,跟好哥们儿商量了一下,帮他公司设计并开发一款宣传用的小程序。因为是宣传用的,所以对于后台数据几乎没什么需求,只需要用到一些接口,引导用户联系公司,且公司性质是古建筑装修,没有自己的服务器。所以我直接给他做的是个静态的小程序。


微信小程序的开发需要注意几个点:


1、主包不大于2M,分包不超过20M。
图片、视频等文件很容易占据大量空间,因此,作为没有服务器的静态页面,这些图片、视频资源,放在什么地方,然后再拿到网络链接地址,是非常关键的节省空间的方案。


2、微信小程序开发者工具,众所周知经常发神经,
莫名其妙弹出一些报错,也会有一些不兼容情况,其中的一些组件也是经常出现问题,比如媒体组件莫名其妙报“渲染层网络错误”的err。


在这次的miniProgram中,有一些功能的实现中,触发了各种奇怪bug。比如
自定义tabbar,
为了让tabbar能被自定义定制,我几乎把整个关于tabbar的开发文档读了个通透;而在定制之后又发现,
pc端模拟机上正常显示、真机预览正常显示,唯独真机调试中,tabbar不显示。
也不是不显示,我的小米8手机不显示,我两位朋友的iphone,一个显示一个不显示(过程中所有的配置是完全相同的)。


接下来就详细介绍一下我在开发中遇到的几个让我把头皮薅到锃亮的问题。


1、自定义tabbar组件


微信小程序app.json中可以直接配置tabbar。但默认的tabbar组件
不足以完全应付各类不尽相同的场景。


譬如,默认的tabbar上使用的icon
实际是png等格式的图片
而非iconfont,其大小也完全由图片本身大小决定,
无法通过css自定制。


为了解决不同业务需求,小程序也非常人性化的
允许tabbar自定义。
其方法如下:


1、在app.json的tabbar配置中,加上custom:true

2、原本的tabbar配置项必须写完整。

在custom:true之后,tabbar的所有样式皆由自定义组件控制(颜色等),但路径等需要填写正确,否则会报错路径找不到。如配置项中必须的属性不写完整,会导致报错,告诉你缺少必须的配置项属性,也不会解析出来。


    "custom": true,                                                  //自定义tabbar开启
"color": "#c7c7c7", //常态下文字颜色
"selectedColor": "#056f60", //被选中时文字颜色
"list": [
{
"iconPath": "images/tabBarIcon/index.png", //常态下icon图片的路径
"selectedIconPath": "images/tabBarIcon/index-action.png", //被选中时icon图片的路径
"text": "首页展览", //icon图片下的文字
"pagePath": "pages/index/index" //该tabbar对应的路由路径
},
{
"iconPath": "images/tabBarIcon/cases.png",
"selectedIconPath": "images/tabBarIcon/cases-action.png",
"text": "精选案例",
"pagePath": "pages/cases/cases"
},
{
"iconPath": "images/tabBarIcon/about.png",
"selectedIconPath": "images/tabBarIcon/about-action.png",
"text": "关于我们",
"pagePath": "pages/about/about"
},
{
"iconPath": "images/tabBarIcon/contact.png",
"selectedIconPath": "images/tabBarIcon/contact-action.png",
"text": "联系我们",
"pagePath": "pages/contact/contact"
}
]
},

3、创建一个自定义组件文件夹custom-tab-bar。

级别为component组件级别。里面包含一个微信小程序包必须的wxml、wxss、js、json文件。


在这里我使用了vant weapp组件库做的tabbar组件。组件上的icon用的是字节跳动的fontPark字体图标库。


<!-- components/tabBar/tabBar.wxml -->
<!-- active用于控制被选定的item -->
<van-tabbar class="tabbar"
active="{{ active }}"
inactive-color="#b5b5b5"
active-color="#056f60"
bind:change="onChange"
>
<van-tabbar-item class="tabbarItem"
wx:for="{{list}}" wx:key="id">
<view class="main">
<image class="selectedIcon"
src="{{item.selectedIconPath}}"
wx:if="{{item.id === active}}"
mode=""
/>
<image src="{{item.iconPath}}" wx:else mode="" class="icon"/>
<text class="txt">{{item.text}}</text>
</view>
</van-tabbar-item>
</van-tabbar>

/* components/tabBar/tabBar.wxss */
.main{
display: flex;
flex-direction: column;
justify-content:center;
align-items: center;

}
.tabbarItem{
background-color: #e9e9e9;
}
.selectedIcon, .icon{
width: 40rpx;
height: 40rpx;
margin-bottom: 10rpx;
}

Component({
data:{
active:0, //用来找到被选中的tabbar-Item
list:[
{
id:0,
iconPath: "/images/tabBarIcon/index.png", //iconPath这些地址换成自己的地
selectedIconPath:"/images/tabBarIcon/index-action.png", // 址,如果需要用icon图表,在
text:"首页展览", // vant中有说明如何在vant组件
pagePath:"pages/index/index" // 中集成vant以外的字体图标。
}, // 就是因为感觉太麻烦了,所以我
{ // 没有用icon图表,还是使用png
id:1,
iconPath: "/images/tabBarIcon/cases.png",
selectedIconPath:"/images/tabBarIcon/cases-action.png",
text:"精选案例",
pagePath:"pages/cases/cases"
},
{
id:2,
iconPath: "/images/tabBarIcon/about.png",
selectedIconPath:"/images/tabBarIcon/about-action.png",
text:"关于我们",
pagePath:"pages/about/about"
},
{
id:3,
iconPath: "/images/tabBarIcon/contact.png",
selectedIconPath:"/images/tabBarIcon/contact-action.png",
text:"联系我们",
pagePath:"pages/contact/contact"
}
]
},
computed:{

},
methods:{
//点击了tabbar的item后,拿到event.detail的值,根据值再进行路由跳转。
//需要注意的是,navigateTo、redirectTo的跳转方式不能跳到 tabbar 页面,
//reLaunch总是会关闭掉之前打开过的所有页,导致页面回退会直接退出小程序
//所以在此使用switchTab,跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
onChange(event){

if(event.detail===0){
wx.switchTab({
url: '/pages/index/index',
})
}else if(event.detail===1){
wx.switchTab({
url: '/pages/cases/cases',
})
}else if(event.detail===2){
wx.switchTab({
url: '/pages/about/about',
})
}else if(event.detail===3){
wx.switchTab({
url: '/pages/contact/contact',
})
}
}

},
})

到这里完成了页面跳转功能。但会发现,当我们点击其他页面的tab时,并
没有让tabbar的图表发生变化,
始终在首页被选定。
这是因为data中的active并没有发生变化,依然是active:0


那么要解决这个问题,方案是在每个tabbar路由页面的js文件中,修改active的值。比如,当点击首页时,active=0,点击第二个页面cases时,active=1......以此类推。


//pages/index/index.js
Page({
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
active: 0
})
}
}
})


//pages/cases/cases.js
Page({
onShow() {
//在自定义tabbar组件的情况下,即app.json中的tabbar配置项中,custom为true时,会提供一个api接口,
//this.getTabBar(),用于获取到tabbar组件,
//可以通过this.getTabBar().setData({})修改tabbar组件内的数据。
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
active: 0
})
}
}
})

//......其他页面以此类推

直到这一步,整个自定义的tabbar组件算是完成。


出现过的BUG




  1. 因为tabbar在app.json文件中"tabbar"配置项配置过了,所以不用再在app.json中的usingComponent配置项进行引用。也无需在tabbar的路由页面的json文件中进行页面配置。




  2. 我曾在onChange(event){}方法中,添加了一行代码:this.setData({active: event.detail });
    57a5d6c616684605f393b89f49d07bf.png




这段代码在没有注释掉的时候,会导致组件在页面切换时发生跳动,处于一种混乱的状态。其原因大致是因为这行代码与page页onshow()时期的getTabBar().setData()有同样的active赋值效果,所以冲突,造成组件闪烁。



  1. 在整个项目完成后,我在使用真机调试时意外发现,模拟机上的tabbar正常显示并使用,但手机上却消失不见。


PC端:


7cb36075fc28472251777b33c085d0f.png


安卓mi8:


安卓.png


我找了很多帖子,没有发现能解决我问题的方案。然后我就问了前辈。前辈的手机是苹果系统,无论是预览、调试,都可以正常显示并使用tabbar,告知我可能是我手机问题,或许是我的手机有什么权限没开。


我又找到一位用苹果手机的同事。如果这位同事的手机也能正常使用,我就要再找一个安卓机的伙伴再测试一次,看看是否机型对代码有影响。


结果奇怪的是,我的这位朋友在进行真机调试时,也没有正常显示tabbar组件。


那么结果就不是安卓和苹果的系统问题。肯定与代码或者某种权限有关。


于是我花了两三个小时去一点点修改,一遍遍重复调试,直到终于找到问题关键所在:


1688803520185.png


这是微信小程序开发者工具中的详情界面,在本地设置中,有一个
启用条件编译
选项。把这个选项开启,tabbar就显示了;关掉这个选项,tabbar就消失了。


于是我开始搜索启用条件编译是什么意思:


2066f1267092e1b9c8c3570069f4cca.png


这是最后找到的结果。但是我并不明白为什么勾选这个会对tabbar有影响。都没有勾选的情况下,前辈的苹果手机就有显示,另一位同事的苹果手机又没有显示,而安卓机的我也一样没有显示。


如果有哪位大佬明白其中的原理,请一定要留言告诉我!!!


2、地图系统


地图系统应该是非常常见的功能,如果在公司的宣传类小程序中加入地图系统,会非常便于用户获取地址信息。


地图系统使用很简单,可以说没太大难度。只要给个map容器,然后给上必须的键值对:经(longitude)纬(latitude)度,如果需要,再给个scale,限制地图缩放的级别,其他的都可以在腾讯地图api的文档中查找需要用的属性。


如果小程序中地图没显示,就要去腾讯地图开放平台里面看看。因为这些地图系统的api都是需要密钥才能使用,所以
注册
api开放平台的账户是第一步,然后在上面的开发文档中选择微信小程序SDK中可以查阅文档。在右上角登录旁边有个控制台,里面创建一个实例,把自己的小程序appID填进去,这个时候小程序中的map应该就是可以正常显示并使用了。


如果需要在小程序的地图中加入标记点,就在map中加入markers,js中传入Obj obj格式的参数,就可以了,在腾讯地图的文档内也有。


地图系统并不难,只需要按照api规则来即可。


<map
longitude="不便展示"
latitude="不便展示"
scale="16"
markers="{{markers}}"
enable-zoom="{{false}}"
enable-scroll="{{false}}"
enable-satellite
style="width: 100%;"
/>

//以下键值对中的value,不加引号为数字类型数据,加引号为字符串类型数据。
Page({
data: {
markers: [{
id: 1, //标记点 id
longitude: 不便展示,
latitude: 不便展示,
iconPath: '/images/local.png',
height: 20,
width: 20,
title: '不便展示',
}],
},

openMap() {
//wx.openLocation()是地图功能的api,在调用该方法时,会跳转到地图
wx.openLocation({
longitude: 不便展示,
latitude: 不便展示,
scale: 18,
name: '不便展示', // 终点名称
});
}
})


3、奇奇怪怪的位置用swiper


一般而言swiper都会用在首页,用以承载轮播图。


不得不说,微信小程序自带的swiper组件虽然简单,但是好用,放上去之后加点属性和数据就可以直接用,比起bug频出的swiper插件还是舒服些。


但是swiper组件就不能用在其他地方吗?


当然可以咯,只要愿意,你就是把许多个业务员的名片用一个swiper组件去收纳,用户不嫌麻烦去一个一个翻的话,你就做呗!


这里,我在精选案例中用了两个swipwe,用来承载相册。


image.png


如图所示,这是两个swiper正在进行滚动动画。


当时在做这个时候,觉得那么多照片正好可以分成两类,一类是成品,一类是原料,让用户可以分类查看。但是我又不想让用户在看到两个相册时,觉得成品和材料就只有一张照片。一想,用swiper正好可以解决这个问题:


让用户看到轮播滚动的图片,每张图片存在时间不长,用户就会想点击放大的图片来延长查看时间,正好落入圈套,进入相册,看到所有图片。


首先是准备了两个view容器,然后在容器中放进swiper,对swiper进行for循环。这整个过程不难,循规蹈矩。但是有个难点,直到项目做完我也没能找到方案:


现在是两个view容器装了两套swiper,如果有更多的swiper,需要更多的view容器,假定数据一次性发过来,怎么样可以循环view的同时,将swiper里面的item也循环?


大概的样子就是:


<view wx:for="{{list1}}">
<swiper>
<swiper-item wx:for="{{item.list}}" wx:for-item="items">
<image src="{{items.src}}" />
<swiper-item>
</swiper>
</view>

数据结构大概是:


    data:{
list1:[
{list:[{title:"111",src:""},{title:"222",src:""},{title:"333",src:""},]},
{list:[{title:"444",src:""},{title:"555",src:""},{title:"666",src:""},]},
{list:[{title:"777",src:""},{title:"888",src:""},{title:"999",src:""},]},
{list:[{title:"aaa",src:""},{title:"bbb",src:""},{title:"ccc",src:""},]},
]
}

上面的代码在循环中肯定出现问题,但是我目前没有找到对应的方法解决。


4、总是有报错渲染层网络层出错


24ec761eb4c272a88fd96edda27bfac.png


这个问题我相信写小程序的应该都遇到过。目前我没找到什么有效解决方案。在社区看到说清除网络缓存。但是在下一次编译时又会出现。如果每次都要清除缓存,好像并不算是个解决问题的方案。


好在这个错误并不影响整体功能,我就

作者:NuLL
来源:juejin.cn/post/7254066710369763388
没有去做任何处理了。

收起阅读 »

工作三年后的胡思乱想

一眨眼工作已经三年了,前两年的总结 工作第一年、工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。 经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的各种链接都成为了可...
继续阅读 »

一眨眼工作已经三年了,前两年的总结 工作第一年工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。


经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的各种链接都成为了可能,在互联网诞生之前,人与人之间的交流就是现实生活中的圈子,而现在本来这一辈子都不会在现实中产生交集的人在互联网却会相遇。


各种写书的大佬、开源的大佬,以往可能只是从文字、代码中了解他们,但现在通过社交媒体、微信竟然就产生了互动。当然不好一面就是也会遇到和自己不相投的人,也许会影响自己的心情。


通过互联网极大的扩宽了我们的视野,看到了别人在怎么生活,也放大了自己的焦虑和欲望。我们需要认清自己的边界,知道自己想要什么,自己能做什么,不需要对本来不可能发生在自己身上的事情而焦虑。


当迷茫焦虑时,看看宇宙的纪录片,从宇宙的视角去看自己,无论从空间大小还是时间维度,其实自己什么都不是,想那么多干啥。



再想想其他动物,吃饭睡觉喵喵叫,也挺好的。



前端已死


互联网已经结束了快速扩张的时期,这是个客观事实,因此招聘的人数相对于之前减少了很多,但远没到一个已死的状态,相对于其他行业,选择互联网依旧是一个不错的选择。


前端会不会死不知道,互联网肯定会一直存在下去,现在整个社会都是基于互联网,已经变成了像电、水一样的基础设施,没有人可以离开它。因此互联网的相关的岗位一定会一直一直存在。


至于互联网中具体的职业划分,前端、后端、算法、数据库等,它们各自使用的语言、技术一定会发生变化的,当选择互联网技术行业的时候,就应该抱有持续学习的态度。


塞班操作系统被安卓、iOS 取代、.Net 岗位的减少、客户端大量岗位转前端,这些也就发生在近十几二十年。当某一个岗位减少的时候,一定又会出现新的岗位,保持开放的心态去学就可以,变化再多肯定也有不变的东西。当掌握一门技术再学习另一门技术的时候,肯定会比小白学习一门新技术快很多很多,很多经验也会迁移过去。


去年 12 月出来的 chatGPT 为代表的大模型,到现在也就半年多的时间,很多以前完全不敢想的事情就这样发生了。可以预见的是一部分岗位数量肯定也会减少,目前影响最大的应该是 UI 岗,其次一定程度上可以提高程序员的开发以及学习效率,但还没有到取代的程度,但未来会再怎么发展就不得而知了。


相对于其他行业,虽然互联网相关技术迭代确实很快,但如果是因为热爱而选择这个行业,我觉得去做一辈子是没问题的。


技术


底层技术服务于上层技术,上层技术服务于应用,真正赚钱的是应用,它可能提升了用户的效率、也可能提升了用户的生活体验,这样用户才愿意付费。上层技术的人收到了钱,进一步也愿意为底层技术的人付费。


但对于一个应用,技术并不是最重要的,更多需要的是产品和运营,一个应用在 chatGPT 和各种框架、云服务的加持下做出来变得太简单了,更多的是我们需要思考如何设计产品和如何推广运营产品,和用户产生更亲密的连接,用户才愿意付费。


极端一点,即使现在所有的应用都停止更新了,其实也并不会产生多大的影响。


在公司中亦是如此,对于技术开发,没有谁是不可取代的,公司更期望的是那些可以发现问题、分析问题、定义问题的人,至于怎么解决,问题定义清楚以后,解决方案自然可以出来,谁去解决并不重要了。


但也不用太过悲观,虽然技术不是最重要的,但一定是不可或缺的,在解决问题的过程中也会区分出能力强和能力差的:方案的设定、代码编写的好坏、线上的 bug 数、代码的扩展性等。


赚钱


赚钱很大程度又是需要运气的,比如同一个人十年前进入互联网和现在进入互联网差别就会很大,再比如开发一个应用突然爆火,例如「羊了个羊」,这些我们是很难控制的,我们只能「尽人事,听天命」。


最近几年,除了在公司工作,对于有技术的同学赚钱有下边的方式:




  • 付费课程、出书


    最近几年越来越多的人在极客时间、掘金小册写课程或者直接出书。


    对于写课的人赚到了钱,对于买课的人只要跟着看完了,多多少少都会有很多收获。付费课程会比较系统, 如果没有这些课程,去学东西肯定也是可以学的,但需要花很多时间去网上搜一些零碎的资料,由于没有经验甚至可能走很多弯路。




  • 付费社群


    市面上也会有一些付费训练的社群或者知识星球


    对于组织付费社群的人会花费很大的精力,需要持续运营并且照顾到每一个人,不然就等着挨骂吧。因此这类收益也会很高,一些人会辞去工作专职来搞。




  • 开源


    大部分开源基本上是用爱发电,更多是收获一些朋友、流量、提升技术。


    比如 core-js 作者的经历,一个 22.6k star 的项目,几乎各个网站都在用的一个项目,作者却因为钱的问题被很多人谩骂。因此如果是个人专职开源一个项目靠 GitHub Sponsor 会很难很难。


    当然,开源也是能赚到钱的,比如 Vue 开源就赚到了很多钱,但毕竟是很少很少数了。


    依赖纯开源项目赚到钱,还是需要背靠公司。比如阿里云谦的 Umi、通过开源加入 NuxtLab 的 Anthony Fu、在 AFFiNE 的雪碧等等。




  • 应用


    身为一个程序员,尤其是前端程序员,当然可以自己维护一个应用来赚钱。


    做得很成功的比如 Livid 的 V2ex 社区,Abner Lee 的 Typora(后来知道作者竟然是国内开发者)。


    也有一些没有那么出名的,比如大鹏的 mdnice,秋风的 木及简历


    当然如果要做一个很大的项目,背靠公司也是一个很好的选择,比如之前阿里玉伯的语雀、之前极客邦池建强的极客时间。


    还有一些小的创业公司会做的,冯大辉的「抽奖助手」、吴鲁加的「知识星球」等。


    做出这些应用不需要很多时间,需要我们善于发现生活中的痛点以及强大的执行力,当然想成功的话需要再加一点运气,在成功前需要不断尝试不同的东西。




  • 流量变现


    有流量就会赚钱,不管是接广告、还是带货。互联网上也会有部分人专注于怎么搞流量,知乎怎么获得更多曝光、视频号怎么获得更多流量、怎么批量注册号,各个平台规则可能是什么,怎么对抗规则,这类有技术加持也会更加顺利,很多人也在专职做。




赚钱的方式有很多,对于我来说,我会尽量选择复利的事情,这样才能产生更大的价值。比如一对一咨询,一份时间换一份收入。但如果把东西写成课程,只需要花一份的时间就能获得 N 份的收入。


另外就是需要保持分享,分享除了能帮助其他人,对自己也会有很大的帮助,写文章的过程中也会不断的有新的认知得到。虽然当下可能没有金钱方面的收入,但时间放宽到几十年,相信一定会有很大的回报。


人的欲望是无穷的,也不能陷入赚钱的极端,目标应该是关注此刻,体验生活,享受生活,而不是不停的赚钱。之前听播客,有一个恰当的比喻,钱就好比汽油,不停的赚钱相当于不停的加油,但如果汽车停着一直不动,再多的汽油也是无意义的。


健康


最近几年总是爆出程序员突然离世的新闻,前段时间耗子叔突然离世的消息听到之后真的很震惊。twitter 经常刷到耗子叔的动态,然后突然一天竟然就戛然而止了,毫无征兆。


意外是无法避免的,只能尽可能的从饮食、作息、锻炼三方面降低生病的风险。


饮食


我是工作第一年体检的时候检查出了中度脂肪肝、尿酸高,当时因为是刚毕业,体重是我的巅峰,140 多斤,脂肪都堆在了肚子上。那段时间就开始跑步加吃沙拉,少吃米饭、面条。降的也快,几个月就回到了 130 斤以下,甚至到 120 多点。


第二年体检的时候,脂肪肝基本没有了,尿酸也降了许多。


image-20230702141922024


后来就保持少吃米饭,多吃蛋白质、蔬菜的饮食了。


作息


有一次得了带状疱疹,那种非常痛的类似于痘痘的东西,后来了解了一下是因为免疫力低导致病毒入侵的。猜测因为晚上坐在电脑前,气温降低了没注意,从而导致了生病。


病好之后就决心养成早睡早起的习惯。


之前作息基本上是 1 点到 2 点睡觉,9 点前后起床。现在基本上保持在 11 点前后睡觉,6 点到 7 点间起床了。


早起的好处就是早上会有大把的时间,而且这段时间是专属于自己的,并且因为大脑刚苏醒,效率也会很高。但如果是工作一天,晚上回家再做自己的事情,此时大脑已经很疲惫了,效率会比较低。


运动


最开始是跑步,但确实很难坚持下去,跑步需要换衣服、出门,还依赖于外边的天气,成本很高。后来陆续尝试过 keep、一些付费课程,都做了但没有完全养成习惯。


后来知道了 switch 的健身环大冒险,然后就一路坚持到了现在,前段时间已经通关了。


image-20230702143219493


目前也一直在坚持,基本上一周会运动三到四次,一次大概花费 50 分钟左右。


投资


大学的时候开始接触到理财,知道了基金的概念,看了银行螺丝钉的「指数基金定投指南」,也看了「穷爸爸富爸爸」、「小狗钱钱」这类理财入门的书。当时赚到的一些钱,就跟着银行螺丝钉投了,主要是一些宽基和中概、医疗。


image-20230702153842246


一直到工作的第一年,基金收入确实不错,甚至赚了百分之四五十。当时想着原来股市这么简单,这咋还能亏钱了。


接着疫情不断发展,还有外部经济的变化,中概、医疗都大跌,当时发了年终奖还不停的补仓中概,到现在亏损也有百分之三四十了。


但我心态是可以的,一切都是浮亏和浮盈,只要不卖一切都是浮云。


经历了大起大落后吸取了一些教训,那就是一定要严格执行计划,现金流多不一定要立刻全部投入,而是按计划定投,因为没人知道会跌多久,只有有充足的现金流,才能够把亏损逐步拉平。


现在国家规定互联网基金这些必须走「投顾」,也就是主理人帮我们买入、卖出,我们只需要交一定的投顾费即可。目前我都是在雪球上投,跟投的有孟岩的「长钱账户」、alex 的「全球精选」、螺丝钉的指数增强和主动优选。


能设置自动跟投的就自动跟投了,我相信专业的事交给专业的人肯定是没问题的。


投资肯定是财富自由不了的,但一定比把钱放余额宝强一些,只要耐心持有,尤其是目前这样的熊市投入,相信到下一个牛市会有不错的回报。


(以上仅个人看法,股市有风险,入市需谨慎)


保险


如果开始接触理财,除了投资,一个绕不过去的点就是保险。


对于保险是什么的比喻,之前听薛兆丰的课时候印象深刻。



我现在还年轻力壮,将来年纪大了可能会生病,为了防止以后生病要花一大笔医药费,今天就开始存钱,每个月拿出 10% 的收入存起来,未雨绸缪。这是一种做法。


另外一种做法,是我每个月也拿出 10% 的收入去买保险。


这两种做法有什么区别呢?


区别在于,如果我是用储蓄来未雨绸缪,那么未来可能就会发生两种不同的情形。


如果我将来年纪大了也没生病,我存的钱就还是我的钱,我不需要花出去,这时候我还是很幸运的,能够保有我原来的收入,这份储蓄没有被花掉,我赚了。


但是如果我运气不好,生病了,这份储蓄就会被用掉,甚至需要借很多钱去治病,生活会发生巨大的变化。


所以通过储蓄来未雨绸缪,它的特点是未来的结局是可变的,是变动的、是带有风险的。要么高、要么低,要么能够保有原来的这份储蓄,要么这份储蓄就被用掉了甚至借更多的钱。


而对于保险来说,如果你没病,那你的生活该怎么样还是怎么样。如果你病了,那会有保险公司给你支付一大笔钱,你也不用和别人借钱,病好后继续该干啥干啥。



因此存钱去防止生病就有赌的成分了,如果没病就白赚了很多钱,如果病了生活质量可能会发生很大的变化。


而保险就可以降低风险,未来即使生病了,由于看病不需要花钱了,病好后生活质量也尽可能的维持在原来轨道 。


我期望未来肯定是尽量稳定的,所以在不影响当前生活质量的条件下我愿意拿出一部分钱来买保险。原计划我可能会 30 岁以后开始买重疾险,之前女朋友的朋友有推荐保险的,然后就跟女朋友一起配置了重疾险。


选保险一定要慎重,一些看起来很划算的保险, 到理赔的时候可能会推三阻四,甚至理赔前公司破产了,尽量要选择大公司。



当然生活没有标准答案,每个人看到世界也都是不同的,我也一直在成长,一直在认识新的东西,上边的所想的也不能保证说未来不会再变。


未来能做的就是多看看书,不限制自己,看看经济学的、哲学的、心理学的、人文的,多出去走走看看,尽可能多的增加人生体验,去认识世界,认识自己,做自己想做的事,爱自己所爱

作者:windliang
来源:juejin.cn/post/7250875810793881660
的人,走下去就好了。

收起阅读 »

搭建适用于公司内部的脚手架

web
前言 公司项目多了,且后续会增加更多项目,为了避免每次创建项目都是重复的copy,这里可以自己写一个适合公司的脚手架,就跟 vue-cli, create-react-app 类似。 简单描述下原理:首先你需要准备一个模板,这个模板可以存储在公司的git上,然...
继续阅读 »

前言


公司项目多了,且后续会增加更多项目,为了避免每次创建项目都是重复的copy,这里可以自己写一个适合公司的脚手架,就跟 vue-clicreate-react-app 类似。


简单描述下原理:首先你需要准备一个模板,这个模板可以存储在公司的git上,然后根据用户选择决定采用哪个分支。比如我们就有 h5模板web模板 两个分支。


然后这些模板会有一些我们自定义的特殊字符,让用户可以根据输入的内容替换。比如我在模板那边里有定义了 $$PROJECT_NAME$$ 这个特殊字符,通过命令行交互让用户输入创建的项目名: test-project ,最后我就通过node去遍历模板里的文件,找到这个字符,将 $$PROJECT_NAME$$ 替换成 test-project 即可。根据公司需求自己事先定义好一些特殊变量即可,主要用到的就是下面几个库。


package.json 里的 bin 字段


用于执行 可执行文件 ,当使用 npm 或 yarn 命令安装时,如果发现包里有该字段,那么会在 node_modules 目录下的 .bin 目录中复制 bin 字段链接的可执行文件,我们在调用执行文件时,可以不带路径,直接使用命令名来执行相对应的执行文件。




bin 文件里的 #! 含义


#! 符号的名称叫 Shebang,用于指定脚本的解释程序。


/usr/bin/env node 表示 系统可以在 PATH 目录中查找 node 程序


如果报错,说明没有在 PATH 中找到 node




npm link


npm link (组件库里用来在本地调试用的)是将整个目录链接到全局node_modules 中,如果有 bin 那么则会生成全局的可执行命令


npm link xxx (本地测试项目里使用), xxx 为 那个库的 package.jsonname。 是让你在本地测试项目中可以使用 xxx




  1. 库在开发迭代,不适合发布到线上进行调试。




  2. 可以帮助我们模拟包安装后的状态,它会在系统中做一个快捷方式映射,让本地的包就好像 install 过一样,可以直接使用。




  3. npm unlink 解除链接






commander —— 命令行指令配置


实现脚手架命令的配置, commander 中文文档


// 引入 program
const { program } = require('commander')

// 设置 program 可以输入的选项
// 每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。
// 长选项名称可以作为 .opts() 的对象key
program.option('-p, --port <count>') // 必选参数使用 <> 表示,可选参数使用 [] 表示

// 解析后的选项可以通过Command对象上的.opts()方法获取,同时会被传递给命令处理函数。
const options = program.opts()

program.command('create <name>').action((fileName) => {
console.log({ fileName, options })
})

program.parse(process.argv)



chalk —— 命令行美化工具


可以美化我们在命令行中输出内容的样式,例如实现多种颜色,花里胡哨的命令行提示等。chalk 文档


安装 chalk 时一定要注意安装 4.x 版本(小包使用的是 4.0.0),否则会因为版本过高,爆出错误。


const chalk = require('chalk')
console.log(`hello ${chalk.blue('world')}`)
console.log(chalk.blue.bgRed.bold('Hello world!'))



inquirer —— 命令行交互工具


支持 input, number, confirm, list, rawlist, expand, checkbox, password,editor 等多种交互方式。 inquirer 文档


const inquirer = require('inquirer')

inquirer
.prompt([
/* 输入问题 */
{
name: 'question1',
type: 'checkbox',
message: '爸爸的爸爸叫什么?',
choices: [
{
name: '爸爸',
checked: true
},
{
name: '爷爷'
}
]
},
{
name: 'question2',
type: 'list',
message: `确定要创建${fileName}的文件夹吗`,
choices: [
{
name: '确定',
checked: true
},
{
name: '否'
}
]
}
])
.then((answers) => {
// Use user feedback for... whatever!!
console.log({ answers })
})
.catch((error) => {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else went wrong
}
})



ora —— 命令行 loading 效果


现在的最新版本为 es6 模块,需要用以前的版本,例如: V5.4.1 才是 cjs 模块 : ora 文档


const ora = require('ora')

const spinner = ora('Loading unicorns').start()

setTimeout(() => {
spinner.color = 'yellow'
spinner.text = 'Loading rainbows'
}, 1000)

spinner.succeed()



fs-extra —— 更友好的文件操作


是系统 fs 模块的扩展,提供了更多便利的 API,并继承了 fs 模块的 API。比 fs 使用起来更加友好。 fs-extra 文档




download-git-repo —— 命令行下载工具


从 git 中拉取仓库,提供了 download 方法,该方法接收 4 个参数。 download-git-repo 文档


/**
* download-git-repo 源码
* Download `repo` to `dest` and callback `fn(err)`.
*
* @param {String} repo 仓库地址
* @param {String} dest 仓库下载后存放路径
* @param {Object} opts 配置参数
* @param {Function} fn 回调函数
*/


function download(repo, dest, opts, fn) {}


【注】 download-git-repo 不支持 Promise


作者:pnm学编程
来源:juejin.cn/post/7254176076082249785

收起阅读 »

今天这个 Antd 咱们是非换不可吗?

web
最近在思考一个可有可无的问题: “我们是不是要换一个组件库?” 为什么会有这个问题? 简单同步一下背景,我效力于 Lazada 商家前端团队。从接手系统以来(近 2 年) 就一直使用着 Alibaba Fusion 这套组件库。据我所知淘系都是在使用这套组件...
继续阅读 »

最近在思考一个可有可无的问题:


“我们是不是要换一个组件库?”


为什么会有这个问题?



简单同步一下背景,我效力于 Lazada 商家前端团队。从接手系统以来(近 2 年) 就一直使用着 Alibaba Fusion 这套组件库。据我所知淘系都是在使用这套组件库进行业务开发,已经有 7 ~ 10 年了吧。我们团队花了 2 年时间从 @alife/next(内部版本已经不更新) 升级到了 @alifd/next,并在此之上建立了一套前端组件库体系。将 Lazada Seller Center 改了模样,在 Fusion 的基础上建立了一套支持整个 Lazada B 端业务的设计规范和业务组件库,覆盖页面 500+。



image.pngimage.png

在这样一个可以说牵一发动全身的背景下,为何还敢有这种想法?


不美



美的反义词,不应该是丑,而是庸俗



不能说 Fusion 丑,但绝对算不上美,这点应该没有争议吧。


虽然也可以在大量的主题样式定制的情况下也可以做到下面这样看上去还行的效果:


image.png


image.png


但说实话,这不能算出众。导致不出众的原因,可以从 Ant Design 上面寻找,Ant Design 的许多细节实现细到令人发指,比如:




  • 弹出窗的追踪动效


    iShot_2023-07-10_11.57.53.gif




  • 按钮的点击动效


    iShot_2023-07-10_11.59.46.gif




  • Tooltip 的箭头追踪


    iShot_2023-07-10_12.02.04.gif




  • NumberPicker 控制按钮放大


    iShot_2023-07-10_12.11.19.gif




这些细节决定了在它上层构建出的应用品质,同样是在一个基础上进行主题和样式的调整。有 Antd 这样品质的基础,就会让在此之上构建的应用品质不会很低,自然也能够带来更好的用户体验及产品品质。


迭代


拿 Antd 的源码和 Fusion 还是有蛮大的差距的,这些差距不只是技术水平的差距,可能在 10 年前他们的代码质量是差不多的,但贵在 Antd 是一个健康的迭代状态。


Antd 已经到了 5.x,Fusion 还是 1.x。这版本后背后意味着 Fusion 从 1.x 发布后就没有大的迭代和改动。即使是 DatePicker、Overlay 这类的组件重构也是提供一个 v2 的 Props 作为差别。


这背后其实反应出的是维护者对于这个库的 Vision (愿景),或许随着 Fusion 这边不断的组织变动,早就已经失去了属于它的那份 Vision。


所以当 Antd 已经在使用 cssinjs、:where、padding-block 这种超前到我都不能接受的东西时,Fusion 里面还充斥着各种 HOC 和 Class。


可以说,Fusion 已经是一个处于缺乏活力,得过且过的维护状态。如果我们不想让这种封闭结构所带来的长期腐蚀所影响,就需要趁早谋求改变。


性能、稳定


得益于上述许多“耗散结构”的好处,Antd 的性能也比 Fusion 要好上许多。许多能够使用 Hooks、CSS 解决的问题,都不会采用组件 JS 来处理,比如 responsive、space 等。


稳定性,既体现在代码的测试质量,又体现在 UI 交互的表现稳定性。比如,Dialog、Tooltip 随着内容高度的变化而动态居中的问题( Fusion overlay v2 有通过 CSS 来控制居中,已经修复)。在很长一段时间内,我们的开发者和用户都承受着元素闪动带来的不好体验。


还有诸如 Icon 不对齐、Label 不对齐,换行 Margin 不居中等等,使用者稍微不注意打开方式,就会可能出现非预期的表现,这些都需要使用者花费额外的精力去在上层处理修复。有些不讲究的开发者就直接把这些丢了用户,又不是不能用。


“又不是不能用” , 而我们不想要这样


投入


Antd 的投入有目共睹,一个 86K star,超过 25K 次提交的库,与 Fusion 的 4.4K star、4K commits。这种投入的比例完全不在一个量级,这还没有计算围绕 Antd 周边丰富的文档、套件等投入。


都是站在巨人的肩膀上,都是借力,没有理由不去选择一个活跃的、周全的、前沿的、生态丰富的巨人。


为什么这变成了问题?


那既然我都把 Antd 吹成这样了,为什么这还需要思考,这还是个问题?无脑换不就行了?


现有生态


或许社区的 Antd 生态非常强劲。但在内部,我们所有的生态都是围绕 Fusion 在建立。包括:



  • 设计规范

  • 业务组件(50+ 常用)

  • 模板 20+

  • 发布体系

  • 业务 External

  • ... 等等许多


切换 Antd,意味着需要对所有现有生态进行升级改造,这将会是一个粗略估计 500+ 小时巨大的投入。


这将意味着我们会拦一个巨大的活到身上,做好了大家用,做不好所有人喷。


影子很重


我们都会发现一个问题,所有 Antd 来做的业务都一眼能被认出来这是 Antd。


因为它太火了,做互联网的应该没有人没见过 Antd 做的页面吧。


辩证的来看,Ant Design 它就叫 “Design”,引入 Antd 还不要它的样式,那你到底想要什么?


“想要它的好看好用,还想让他看上去跟别人不一样”


别急眼,这看上去很荒谬,但这确实是在使用 Antd 时的一个很大诉求。


我认为 Antd 应该考虑像 Daisyui 这样提供多套的主题预设。


不是说这个能力 Antd 现在没有,相反 Antd 5 提供了一整套完整的 Design Token。


但插件体系或者说开放能力,真的需要在官方自己进来做上几个,才会发现会有这么多问题 😭


这就跟 Vite 如果不自己做几个插件,只是提供了插件系统,那它的插件系统大概率是满足不了真正的使用者的。


反正虽然 Antd 5.0 提供了海量的 Design Token,但我在精细化调整样式主题时,还是发现了许多不能调整的地方(就是没有提供这样的 TOKEN 出来)。


因为 cssinjs 的方案,说实话我也不知道应该用什么样的方式进行样式改写才算是最佳实践。


CSS 方案


可以说,近一两年,随着 Vue 3、Vite、Tailwind CSS 等项目的大火🔥,又重新引起了我们对样式的思考。


Unstyled 这个词反复的被 Radix UIHeadless UI 等为首的项目提及,衍生出来的:Shadcn UIArk UI 等热门项目都让人有种醍醐灌顶的感觉。


大概是从 React、Vue 出现开始,UI 的事情就被绑定在了组件库里面,和 JS 逻辑都做好了放一起交给使用者。


但在此之前,样式和 JS 库其实分的很开的。如果你不满意当前的 UI,你大可以换一套 UI 样式库。同样是一个 <button class="btn"></button>,换上不同的 CSS,他们的样式就可以完全不一样。


但前端发展到了今天,如果我想要对我们的样式进行大范围升级,从 Element 换到 Ant Design 很可能涉及到的是技术栈的全部更替。


所以面对 cssinjs,我不敢说这是一个未来的方向,我花了很长时间去了解和体会 cssinjs,也确实它在一些场景中表现出了一些优势:



  • 按需加载,我不用再使用 babel-plugin-import 这类插件

  • 样式不在冲突,完美prefix+ :where hash样式 Scope 运行时计算,必不冲突。微前端友好!

  • ES Module,Bundless 技术不断发展,如果有一天你需要使用 ES Module,你会发现 Antd 5.x 这个组件库不需要任何适配也可以运行的很好,因为它是纯 JS

  • SSR,纯 JS 运行,也可以做 CSS 提取,InlineStyle 也变得没有那么困难


但说实话,这些方案,在原子化 CSS 中也不是无解,甚至还能做的更好。


但 Ant Design 底层其实也是采用 Unstyled 方式沉淀出了一系列的 rc-* 组件,或许有一天这又会有所变化呢,谁知道呢。


总之,我非常不喜欢使用 Props 来控制 Style这件事情。


也非常不喜欢想要用一个 Button,在移动端和 PC 端需要从不同的组件库中导入。


所以,有答案了吗?


说实话,这个问题,我思考了很久。每次思考,仿佛抓到了什么,又仿佛没有抓到什么,其实写这篇文章也是把一些思考过程罗列下来,或许能想的更清楚。



最初科举考试是选拔官僚用的,其中一个作用是:筛选出那些能够忍受每天重复做自己不喜欢事情的人



或许畏惧变化、畏惧折腾,或许就应该用 Fusion ,因为可以确定的是 Antd 5 绝对不是最后一个大版本。


选择 Antd,也意味着选择迭代更快的底层依赖,意味着拥抱了更活跃的变化,意味着要持续折腾。


如果没有准备好这种心态,那即使换了 Antd,大概率也可能会锁定某个版本,或者直接拷贝一份,这种最粗暴的方式使用。然后进入下一个循环。


今天这个 Antd 咱们是非换不可吗?


我想我已经有了我的决定,你呢?


(ps. 为什么大家对暗黑模式这么不重视...)


(ps. 如果 Fusion 相关同学看到,别自责,这不怪

作者:YeeWang
来源:juejin.cn/post/7254559214588543034
你...)

收起阅读 »

程序员更需要钝感力

程序员更需要钝感力,别让自己太疲惫 程序员每个人都是聪明的,都是反应灵敏的。 一个同事说过这么一句话:"都当的了程序员,大家都是聪明人,不过有些事不好意思点破罢了,他要是真的太过分,就真的撕破脸!"。 讲一个大多数同学都会遇到的事,之前在一家单位随着部门发...
继续阅读 »

程序员更需要钝感力,别让自己太疲惫



程序员每个人都是聪明的,都是反应灵敏的。 一个同事说过这么一句话:"都当的了程序员,大家都是聪明人,不过有些事不好意思点破罢了,他要是真的太过分,就真的撕破脸!"。



讲一个大多数同学都会遇到的事,之前在一家单位随着部门发展,空降了个老大哥(前端领导)。怎么说呢那个时候自己陷入了一个极其痛苦的时期,他呢每天都很闲不干活,活呢都交给我跟另外一个哥们干,而且功劳还是他的。我很不爽,当我跟我对象说这个事的时候,她说谁让人家是领导呢!!现在再总结的时候发现好像都释怀了,那个时候每天晚上失眠睡不着。


那个时候痛苦的点是:



  1. 身为领导不作为。

  2. 身为领导技术能力不行。

  3. 把我的产出都当作自己的汇报给领导。


其实那个时候或多或少会跟他对着干,我对象也跟我聊过这个事说不要跟领导对着干,人家是领导不干活就不干活了。好像确实没落到什么好处,最后还是人家啥活不干,痛苦的还是自己。还是要学会向上管理


都是打工的,工期是按照人日排的,就是换个领导,怎么着你都要干活的啊!谁让你不是领导呢。


到现在听到有吐槽领导会不干活、技术能力不行、功劳是自己的,锅都是我的一些吐槽。我都会笑笑不说什么,其实反过来想领导的位置不允许犯错,一般当领导的年龄都很大了(可能不是那么容易找到那么符合预期的工作了),假如再犯一些低级的错误,会被领导的领导不信任等。你如果能跟领导相处很好,就是犯错了锅是你的,最后你得绩效还是好的。


领导积极管理组织分享、一块搞些新奇的东西也好,不作为也好,干好自己的工作积极响应,自己擅长的领域就好好发挥,管他结果是啥呢,大家都聪明人,都看得明白的。有时间了就学点新东西,都是打工的,没必要搞得都心里不舒服,凡事多多人情世故下 哈哈 真不爽了离开就是了。


钝感力这个词其实跟我们常说的看开点、别往心里去、别想太多、不要太敏感等等有异曲同工之妙。


我们都需要顿感力,凡事别较真,在《高敏感是种天赋》,作者伊尔斯·桑德把高敏感比作是种天赋,是种天生具有责任感的人(任何事情都是会提前规划,反复练习不允许自己失误,努力会把事情做好),容易产生共鸣的人(不至于在工作中需要浪费太多时间沟通事情怎么做,我稍微一说你就能get到)等。


顿感力



渡边淳一再顿感力书中把顿感力解释为“迟钝的力量”,既从容面对生活中的挫折和伤痛。作者认为钝感是一种才能,一种能让人们的才华开花结果、发扬广大的力量。迟钝虽然给人一种木讷的负面印象,但钝感力确实我们赢得美好生活的手段和智慧



作者从以下几点讲了顿感力的优势:


1、从蚊子叮皮肤 展示钝感力的皮肤优势。


2、从同事被领导严格批评到我们都心疼他 到他第二天没有一点事 “白担心了”。


3、从文艺写作沙龙“石之会”结识优秀青年作家在第一次给主编投稿受打击就一蹶不振 最后销声匿迹 。


4、从“沾沾自喜”“得意忘形”到“再接再厉”。


5、从视觉过好,导致眼睛过累。


6、从听觉过好产生幻听。


7、从嗅觉过好到食物的怪味。


8、从味觉的过好到吃不到常人认为很好吃的辣度咸度。


9、从触觉的灵敏到身体感受到剧烈的疼痛。


10、从身体的关节疼痛能预知天气状况。


11、从“善睡的能人”到获得美丽 健康长寿。


一千个读者就有一千个哈姆雷特,每个人都会有不同的观点和感受, 也有一些书评如下:


读者一:


认为“钝感力”并不是什么新鲜玩意,中华五千年文化博大精深。用汉语来形容就是,一曰大智若愚,二曰难得糊涂。


大智若愚者,拥有大智慧,看起来却很笨拙,表面上给人一种木纳、不善言辞、反应迟钝的感觉,似乎不够机灵,实则心中有数,把握有度,巧藏于拙,遇事不急不躁,内心清楚明白,甚至洞若观火。


难得糊涂者,不是自我欺骗,不是装腔作势,而是表面上嘻嘻哈哈,看似糊涂,实则看透世事,看透人生,落得个与世无争,悠闲自得。


读者二:

我觉得挺好的。有了钝感力,生活会幸福一些。但是我觉得真正的钝感力,来自于自信,而自信来自于认知能力。如果我们的认知达到一定高度,钝感力自然形成。


读者三:

人生遇到的烦事50%可用四个字解决:关我屁事!

那么剩下的50%,亦可以用四个字解决:关你屁事!


如何培养顿感力呢?


1、迅速忘却不快之事;

2、认定目标,即使失败仍要继续挑战;

3、坦然面对流言蜚语;

4、对嫉妒与嘲讽心怀感激之心;

5、面对表扬,甘之如饴,但不得寸进尺,不得意忘形


敏感度检测



都读到这里了,可以做个敏感度检测摘录至《高敏感是种天赋》,作者伊尔斯·桑德书中。



主要用于高度敏感群体。每个问题有五个选项,每个数字代表问题的描述与自己的符合情况,0-4分别是:



  • 0=完全不符合

  • 1=有一点儿符合

  • 2=基本符合

  • 3=较为符合

  • 4=非常符合



  1. 美妙的音乐会让我非常激动。()

  2. 我总是花比别人更多的精力去预测未来可能出现的问题,并做好充分的准备。()

  3. 我很擅长发现新的可能和选择。()

  4. 我很容易兴奋,总是有很多主意。()

  5. 我知道生活不止我们看见和听见的一切。

  6. 我的疼痛阈限很低。()

  7. 我常常觉得对别人来说很容易的事情,对我来说却太沉重。()

  8. 每天我需要一点时间独处。()

  9. 如果我跟别人连续相处两到三个小时,中间几乎没有休息,这会让我非常疲惫。()

  10. 预感到冲突即将出现,我会提前逃跑。()

  11. 面对愤怒,即使不是针对我,也会让我倍感压力。()

  12. 别人的痛苦会深深地影响到我。()

  13. 每件事我总是竭尽全力,以避免不愉快的事情或者失误的发生。()

  14. 我富有创造力。()

  15. 艺术性的工作有时会给我带来深深的快乐。()

  16. 面对多重任务,我的阈限比别人要低。比如,我很难做到一边上网,一边与人交谈。()

  17. 我不喜欢待在刺激过多的地方,比如游乐场、大型超市、运动会。

  18. 在电视上看到的暴力图片会影响我很久。□

  19. 我会花比别人更多的时间来思考。□

  20. 我很擅长感知动物和植物的生长状态。□

  21. 身处美丽的自然环境中时,我整个身体里都洋溢着幸福。□

  22. 我触须灵敏,能轻易感知到他人的心理状态。□

  23. 我很容易感到愧疚。□

  24. 我工作的时候,如果有人看我,我会很有压力。□

  25. 我有一双敏锐的眼睛,能一眼看透别人的心理活动。□

  26. 我很容易受到惊吓。□

  27. 我能给别人提供倾情陪伴和有意义的友谊。□

  28. 那些似乎不会打扰别人的声音却给我带来很大的困扰。□

  29. 我非常直观。□

  30. 我很享受独自一人的感觉。□

  31. 多数时间我都是一个明智的决断者,但有时也会是冲动型,追求速度。□

  32. 喧闹的声音,刺激的味道和强烈的光线都会影响到我。□

  33. 我对在安静平和的环境中休息的需求比别人更大。□

  34. 我很难从饥饿和寒冷中转移注意力。□

  35. 我很容易哭泣。□



  • 1-35题总分合计()



  1. 我喜欢毫无准备地体验新事物。□

  2. 当我在某些方面比别人更聪明,我会感觉很棒。□

  3. 社交不会让我疲惫。如果气氛足够好,我可以一直在活动现场待下去,甚至不用独处休息。□

  4. 我喜欢野外生存一类的夏令营。□

  5. 我喜欢在压力下工作。□

  6. 如果别人不舒服,我倾向于认为这是他们自己的错。□

  7. 我总是充满能量,我的心情很少受到周围的事情的影响。□

  8. 我常常是最后一个离开派对的人。□

  9. 我习惯船到桥头自然直,很少担心。

  10. 我喜欢跟朋友一起在度假小屋度过周末,并不需要独自待着。□

  11. 朋友出其不意地拜访我,我会非常惊喜。□

  12. 我能应对睡眠很少的状况。□

  13. 我喜欢放鞭炮。□



  • 36-48题总分合计()


第一组题目包括1-35。将你的答案加起来求和,如果所有问题你都选1,那么合计应该是35。


第二组题目包括36-48。将你的答案加起来求和,如果所有问题你都选2,那么合计应该是26。


接着,用第一组的总分,减去第二组的总分,以上述例子为例,答案应该是9。


最后的分数则是你的敏感分数,应该是介于52-140之间的某个值。得分越高,敏感程度越高。如果你的分数超过了60,那么你可能就是一个高度敏感型的人。



对测试结果进行解释常常需要谨慎。当我们用该测试结果描述一个人时,它肯定不是非常全面的。还有许多方面未被纳入考虑。并且测试结果还会受到你测试当天的心情的影响。你可以将该测试视为一个大概的参考,而不必过分去强调它。



结语



高敏感、迟钝也罢,怎么都是自己,试着跟自己和解,适当时候不要对自己要求太高,先暂时做好眼前事。 阅己、越己、悦己、乐己八个字送给各种程序员,工作只是生活一部分而已。尤其是在现在行情不好的时候,在工作的时候做好自己的本分工作,失业的也不要想太多,就当给自己放了个假期,后面有你大展拳脚的时候。加油哦


作者:三原
来源:juejin.cn/post/7253788247065788477

收起阅读 »

看见的熄灭了,消失的忘记了

许多年前,也许是许多世纪前,我在陶盆里养了两条鱼。在此之前,陶盆里下着漫长的雨,一只蟾蜍在里面参禅,显得很寂寞。若干年后,雨下累了,月亮从桂枝上醒来,蟾蜍还困在禅意里。他俩都不知道对方是自己另外一个化身。 陶盆之外是原始的黑暗。烛龙蜷在黑暗之中想心事,有一天,...
继续阅读 »

许多年前,也许是许多世纪前,我在陶盆里养了两条鱼。在此之前,陶盆里下着漫长的雨,一只蟾蜍在里面参禅,显得很寂寞。若干年后,雨下累了,月亮从桂枝上醒来,蟾蜍还困在禅意里。他俩都不知道对方是自己另外一个化身。


陶盆之外是原始的黑暗。烛龙蜷在黑暗之中想心事,有一天,我打开他的心事,点亮烛火,开始寂静地书写。我写到一些年代和场景,陶盆哭了,而那时,我的字句里还没有它。写到后来,雨水也哭了,而那时,它正想去陶盆漫长地飘落。


那一年春天,池塘边生出青草。母亲说,鱼该上岸了,它原本就是鹿。我让自己躺在梨花和雨飘过的窗下,漫不经心地构思字句。我想到,在原始的黑暗中,陶盆是唯一的光亮。年代在陶盆里不断进化,最后演变成大大小小的裂纹。


这时,邻家的女孩汲水归来,唱起一支悠长而陈旧的民歌,悠长得不知所终,陈旧到诗经出现之前。我构思的景象开始土崩瓦解,最后只剩下两粒羞涩的字。雨水下累那天,我将它们埋进陶盆,如同把秘密埋进心里。我知道,从此那两粒字将被我反复书写和记忆。


后来桂花开了,秋香飘过窗前,母亲说鹿该下水了,它原本就是鱼。月光下,鹿还在岸边吃草,两粒羞涩的字已化作游鱼,首尾相依,你追我赶,将万物搅成巨大的漩涡。黑暗坍塌了,烛龙收起心事,将我关在年代和场景的中心。


一天午后女孩停在窗前告诉我,很久以前,烛龙来到梦里替她照亮,她看见我在烛龙紧闭的心房喂鱼,大大小小的裂纹蛛网似的从四周缓慢地向我爬来。她还说,陶盆哭的时候,她看见雨水哭了,雨水哭的时候,她醒了。


这是一个幸福而悲伤的午后。我对她说,你在我的梦里梦见我的时候,我正在构思和书写。烛龙,游鱼,裂纹,以及你和你的梦,仅是我漫长书写中的一些温暖字句。这些字句有的已经完成,有的尚未写到,最终都会与我精心构思的景象一样,除了在某些悲伤的时刻对我有所安慰外,将变得毫无意义。我还告诉她,其实陶盆并不存在,雨水也不存在,蟾蜍偶尔闪过的禅意,或者月亮久已遗忘的光线之中,包含了我所有的书写。


作者:Emanon
来源:juejin.cn/post/7254474251724947511
>2010-3-27

收起阅读 »

Android View滑动处理大法

对于触控式操作来说,滑动是一个特别重要的手势操作,如何做到让应用程序的页面滑动起来如丝般顺滑,让用户感觉到手起刀落的流畅感,是开发人猿需要重点解决的问题,这对提升用户体验是最为重要的事情。本文就将探讨一下,Android中View的滑动相关知识,以及如何做到丝...
继续阅读 »

对于触控式操作来说,滑动是一个特别重要的手势操作,如何做到让应用程序的页面滑动起来如丝般顺滑,让用户感觉到手起刀落的流畅感,是开发人猿需要重点解决的问题,这对提升用户体验是最为重要的事情。本文就将探讨一下,Android中View的滑动相关知识,以及如何做到丝般顺滑。


0.jpg


如何让View滑动起来


View的滑动是GUI支持的一项基本特性,就像触摸事件一件,这是废话,平台如果不支持,你还搞个毛线。


View滑动的基本原理


我们先来看一下Android中实现View的滑动的基本原理。其实屏幕并没有动啊,一个View的可绘制区域,对于屏幕来说,对于view tree来说都是没有变化 的。父布局给某一个View的绘制区域是在layout之后就确定好了的,当View的真实高度或者宽度超过了这块可绘制区域,那么就需要滑动才可以把整个View做到用户可见。View内部通过两个关键成员变量mScrollX和mScrollY来记录滑动之后的坐标,View本身有mLeft和mTop来标识自己相对于父布局的坐标位置,那么当有滑动的时候,在此View当中具体要绘制的区域就变成了以mLeft+mScrollX和mTop+mScrollY为起点的区域了。由此View便滚动起来了。


如何实现View的滑动


对于开发人猿来说,实现View的滑动,需要关注三个重要的方法,也即是View#scrollByView#scrollTo以及View#onScrollChanged,这是实现滑动的三个最为核心的方法。


scrollBy提供的参数是需要滑动的距离,而scrollTo则是需要传入要滑动到的目标坐标值,这两个方法都是要修改mScrollX和mScrollY的值,本质上是一样的。而onScrollChanged则是一个回调,用以通知更新了的滑动位置。


Scroll手势


要想让View滑动起来,离不开事件手势的支持。最简单也是最直接的手势就是onScroll手势,这个在GestureDetecor中可以识别出此手势,或者自己去直接处理touch event也可以得出此手势。这个并不复杂,就是直接通过touch 事件来计算滑动多少距离就好了,按照View预设计的可以滑动的方向,比如横向就计算不同时间点MotionEvent的坐标值,得到一个水平距离deltaX,然后调用scrollBy即可。垂直方向依此类推。



Scroll手势简单是因为它是直接来源于事件,且速度较慢,并不需要额外处理,所以整体逻辑处理流程并不复杂。


GestureDetector中的识别就是在ACTION_MOVE时,查看滑动过的距离,这个距离(由sqrt(dx x dx, dy x dy)如果大于touch slop,就会触发onScroll手势回调。


Fling手势


Fling也即是快速滑动,就是手指在屏幕上使劲的『挠』一下,手势的要点是手指在屏幕快速滑过一小段短距离,就像把一个小球弹出去的感觉一样。对于Fling手势来说,最重要的是速度,水平方向的速度和垂直方向的速度,可以理解为高中物理常讲到的平抛运动一样。



GestureDetector识别Fling的逻辑是,在ACTION_UP时,检查此次事件的速度,如果水平方向速度或者垂直方向速度超过了阈值,便会触发Fling手势回调。


注意:留意Scroll与Fling的区别,Scroll是慢的,不关心时间与速度,只关心滑动的距离,是在ACTION_MOVE时,手指并未有离开屏幕时就触发了,只要是ACTION_MOVE还在继续,就会继续触发onScroll,并且ACTION_UP时终止整个Scroll,而Fling只关心速度,不关心距离,是在ACTION_UP时,手指离开了屏幕了(此次事件流处理结了)才会触发。


VelocityTracker


Fling事件速度是决定性的,仔细看GestureDetector的处理过程会发现它使用了一个叫做VelocityTracker的对象,来帮忙处理一些关于速度的具体逻辑,那么有必要深入了解一下这个对象。


VelocityTracker使用起来并不复杂,获取它的一个对象后,只需要不断的把MotionEvent塞给它就可以了,然后在需要的时候让其计算两个方向上的速度,然后就没有然后了:

    velocityTracker = VelocityTracker.obtain();

onTouchEvent(MotionEvent ev) {
velocityTracker.addMovement(ev);

if (want to know velocities) {
velocityTracker.computeCurrentVelocity(100);
vx = velocityTracker.getXVelocity();
vy = veolocityTracker.getYVelocity();
be happy with vx and vy.
}
}

这个类的实现,值得仔细看一下,它主要的实现都是用JNI去实现,可能是因为计算方式较复杂,所以computeCurrentVelocity方法也说明了,让你真用的时候再调,这个不用去管细节实现。重点看一下这个类,里面有一个对象池,用以缓存对象,并且创建对象的方式并不是直接new,而是用其obtain方法。这里用的是叫享元(Flyweight Pattern)的设计模式,也就是说VelocityTracker对象其实是共享的。


顺滑如丝


前面提到了,让View滑动,只需要调用scrollBy或者scrollTo即可,但这个吧,是直接修改了mScrollX,mScrollY,然后invalidate,View下次draw时就直接在把目标区域内容绘制出来了,换句话说这两个方法滑动是瞬间跳格式的。


一般来说,这也没有问题,就像onScroll手势,ACTION_MOVE时,不断的scrollBy刚刚滑过的距离,都还okay,没有什么问题。


但是对于Fling事件就不行了,Fling事件,也即快速滑动,要求短时间内进行大距离滑动,或者像有跳转的需求时,也是短时间内要滑动大距离。如果直接scrollBy或者scrollTo一步到位了,会显得 相当的突兀,体验相当不好,卡顿感特别强。如果能像做动画那样,在一定时间内,让其平滑的滑动,就会如丝般顺滑,体验好很多。Scroller就是专门用来解决此问题的。


Scroller


Scroller是对滑动的封装,并不是View的子类,其实它跟View一点关系也没有,也不能操作View,实际上它与属性动画类似,它仅是一个滚动位置的计算器,告诉它起始位置和要滚动的距离,然后它就会告诉你位置随时间变化的值。其实这是一个中学物理题,也即给定初始位置,给定要滚动的距离,以一定的方式来计算每个时间点的位置。具体的计算方式由mInterpolater成员来控制,默认是ViscousFluid,是按自然指数为减速度来计算的,具体的可以查看Scroller的源码。如果不喜欢默认的计算方式,可以自己实现个Interpolator,然后在构造时传进去。


Scroller的作用在于实现平稳滑动,不让View的滚动出现跳跃,比如滑动一下ListView,开始滑动时的位置是x0,y0(ActionDown的位置),要向下滑动比如500个像素,不平稳的意思是,从x0,一下跳到x0+500的位置。要平稳,就要不断的一点点的改变x的值然后invalidate,这也就是Scroller的典型使用场景:

Scroller scroller = new Scroller(getContext());
scroller.startScroll(x0, y0, 500, 0);

然后在computeScroll时:

if (scroller.computeScrollOffset()) {
int currX = scroller.getCurrX();
int currY = scroller.getCurrY();
invalidate(); // with currX and currY
}

computeScrollOffset在滚动没结束时返回true,也就是说你需要继续刷新view。返回false时表明滚动结束了,当然也就没有必要再刷新view(当然如果你乐意也可以继续刷,但是位置啥的都不变了,所以刷了也白刷)。


滑动冲突处理


关于View的滑动,最难搞的问题便是手势冲突处理,特别是当页面的结构变得复杂了以后。一般来讲,滑动手势,是让某一个View沿着某一个方向『平移』一段距离,如果某一个页面中只有一个View是可以滑动的,或者页面中不同的View的可滑动方向是垂直正交的,那么就不会有冲突的问题。



所谓滑动冲突,是指父View和子View都接受滑动手势,并且方向又是一样的,这时就产生了滑动冲突,常见就是ScrollView中套着ListView(这个通常是垂直Y方向上面有滑动冲突),或者ViewPager中套着ScrollView(这个是水平X方向上有滑动冲突)。


要想解决好滑动冲突问题,需要先确实好整体的设计方案,有了大的原则后,就容易用技术方案找到解法。最理想的方案,也是目前用的最多的方案就是在子View的边界设定一个margin区域,当ACTION_DOWN在margin区域以外,认定滑动手势归父View处理,否则交由子View处理。像一些全局手势也是要用如此的方案,当点击距离屏幕一定范围内(margin区域)认定此事件归当前页面处理,否则就认定为全局手势,就好比从屏幕左边向右滑动,很多应该将此识别为BACK到上一页,但如果离左边较远时滑动,就会是页面内部的滑动事件(假如它有可滑动的组件的话,事件手势会被其滑消耗掉)。


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

VUE3基础学习(四)事件处理

监听事件 我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler" 或 @click="handler"...
继续阅读 »

监听事件


我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler" 或 @click="handler"


事件处理器 (handler) 的值可以是:



  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。

  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。


内联事件处理器


内联事件处理器通常用于简单场景,例如:

data() {
return {
count: 0
}
}
<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>

方法事件处理器


随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on 也可以接受一个方法名或对某个方法的调用。

data() {
return {
name: 'Vue.js'
}
},
methods: {
greet(event) {
// 方法中的 `this` 指向当前活跃的组件实例
alert(`Hello ${this.name}!`)
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName)
}
}
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>

方法与内联事件判断


模板编译器会通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foofoo.bar 和 foo['bar'] 会被视为方法事件处理器,而 foo() 和 count++ 会被视为内联事件处理器。


在内联处理器中调用方法


除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:

methods: {
say(message) {
alert(message)
}
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>

在内联事件处理器中访问事件参数


有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
methods: {
warn(message, event) {
// 这里可以访问 DOM 原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
}

事件修饰符


在处理事件时调用 event.preventDefault() 或 event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。


为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:



  • .stop

  • .prevent

  • .self

  • .capture

  • .once

  • .passive
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

例子说明:


stop


阻止了事件冒泡,相当于调用了event.stopPropagation方法

<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>


点击shout只输出1,不会触发shout(2)



prevent


单击“提交”按钮,阻止其提交表单
阻止了事件的默认行为,相当于调用了event.preventDefault方法
<form v-on:submit.prevent="onSubmit"></form>


self


只当在 event.target 是当前元素自身时触发处理函数


<div v-on:click.self="doThat">...</div>



使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击



once


绑定了事件以后只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>


capture


使事件触发从包含这个元素的顶层开始往下触发

<div @click.capture="shout(1)">  
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>

passive


在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
<div v-on:scroll.passive="onScroll">...</div>


native


让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件


按键修饰符


在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on 或 @ 监听按键事件时添加按键修饰符。

<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />

你可以直接使用 [KeyboardEvent.key]

<input @keyup.page-down="onPageDown" />

在上面的例子中,仅会在 $event.key 为 'PageDown' 时调用事件处理。


按键别名


Vue 为一些常用的按键提供了别名:



  • .enter

  • .tab

  • .delete (捕获“Delete”和“Backspace”两个按键)

  • .esc

  • .space

  • .up

  • .down

  • .left

  • .right


系统按键修饰符


你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。



  • .ctrl

  • .alt

  • .shift

  • .meta


例子:

<!-- Alt + Enter --> <input @keyup.alt.enter="clear" /> <!-- Ctrl + 点击 --> 
<div @click.ctrl="doSomething">Do something</div>

.exact 修饰符


.exact 修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。

<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>

鼠标按键修饰符



  • .left

  • .right

  • .middle


这些修饰符将处理程序限定为由特定鼠标按键触发的事件。


作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7254384497659707451
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

VUE3基础学习(三)Class,Style,条件渲染,列表渲染

class 绑定 数据data() { return { isActive: true, hasError: false } } 模板: <div :text-danger': hasError }" ></div> 渲染: <...
继续阅读 »

class 绑定


数据

data() { return { isActive: true, hasError: false } }

模板:


<div :text-danger': hasError }" ></div>


渲染:


<div></div>


计算属性:

computed: { 
classObject()
{
return
{
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}

绑定数组


<div :></div>


数组中嵌套对象
<div :></div>


在组件上使用

<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>

在使用时添加一些 class:

<!-- 在使用组件时 -->
<MyComponent class="baz boo" />

渲染出的 HTML 为:

<p class="foo bar baz boo">Hi!</p>

如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来实现指定:

<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<MyComponent class="baz" />

这将被渲染为:

<p>Hi!</p>
<span>This is a child component</span>

绑定内联Style

data() {
return {
activeColor: 'red',
fontSize: 30
}
}
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

直接绑定一个样式对象

data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}
<div :style="styleObject"></div>

绑定一个包含多个样式对象的数组

<div :style="[baseStyles, overridingStyles]"></div>

样式多值


你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex


条件渲染


v-if 


指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

<h1 v-if="awesome">Vue is awesome!</h1>

v-else


你也可以使用 v-else 为 v-if 添加一个“else 区块”。

<button @click="awesome = !awesome">Toggle</button>

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>


一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别。



v-else-if


顾名思义,v-else-if 提供的是相应于 v-if 的“else if 区块”。它可以连续多次重复使用:

<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>


和 v-else 类似,一个使用 v-else-if 的元素必须紧跟在一个 v-if 或一个 v-else-if 元素后面。



<template> 上的 v-if

<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>

v-else 和 v-else-if 也可以在 <template> 上使用。


v-show


另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:

<h1 v-show="ok">Hello!</h1>

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。


v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。


v-if 对比 v-show


v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。


v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。


相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。



总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适



列表渲染


v-for


我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名

data() {
return {
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
<li v-for="item in items">
{{ item.message }}
</li>

在 v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

data() {
return {
parentMessage: 'Parent',
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

v-for 变量的作用域和下面的 JavaScript 代码很类似:

const parentMessage = 'Parent'
const items = [
/* ... */
]

items.forEach((item, index) => {
// 可以访问外层的 `parentMessage`
// 而 `item` 和 `index` 只在这个作用域可用
console.log(parentMessage, item.message, index)
})

注意 v-for 是如何对应 forEach 回调的函数签名的。实际上,你也可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似:

<li v-for="{ message } in items">
{{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>

对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域:

<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
</span>
</li>

你也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法:

<div v-for="item of items"></div>

v-for 与对象


你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定。

data() {
return {
myObject: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
}
}
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>

可以通过提供第二个参数表示属性名 (例如 key):

<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>

第三个参数表示位置索引:

<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>

在 v-for 里使用范围值


v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。

<span v-for="n in 10">{{ n }}</span>

注意此处 n 的初值是从 1 开始而非 0


<template> 上的 v-for


与模板上的 v-if 类似,你也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。例如:

<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>

通过 key 管理状态


Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。


<div v-for="item in items" :key="item.id"> <!-- 内容 --> </div>



必要性
vue在patch过程中判断两个节点是否为相同节点,key是一个必要条件,渲染一组列表时key往往是唯一标识,如果不定义key,vue会认为比较的两个节点是同一个(哪怕不是),这会导致频繁更新元素,使得整个patch过程比较低效,影响性能




实际使用方式
实际使用中在渲染一组列表时key必须设置,而且是唯一标识,应避免使用数组索引作为key,因为这可能会导致一些隐藏的bug;vue在相同标签元素过渡切换的时候也会使用可以,目的也是让vue能够区分它们,否则vue只会替换其内部属性,而不会触发过渡效果



数组变化侦测


变更方法


Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:



  • push()

  • pop()

  • shift()

  • unshift()

  • splice()

  • sort()

  • reverse()



在计算属性中使用 reverse() 和 sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:

- return numbers.reverse() + return [...numbers].reverse()

作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7254169497201049660
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »