注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

幻兽帕鲁Palworld服务端最佳一键搭建教程

幻兽帕鲁Palworld最近彻底火了,忍不住也自己去搭建了一下,找了很多教程,发现还是目前找的这个最方便,结合腾讯云服务器新人优惠套餐,这里给出我搭建的详细步骤,包你轻松搞定。 此方案适合新用户,毕竟老用户购买服务器价格还是很贵的,自己如果已经是老用户了记得用...
继续阅读 »

幻兽帕鲁.jpg


幻兽帕鲁Palworld最近彻底火了,忍不住也自己去搭建了一下,找了很多教程,发现还是目前找的这个最方便,结合腾讯云服务器新人优惠套餐,这里给出我搭建的详细步骤,包你轻松搞定。


此方案适合新用户,毕竟老用户购买服务器价格还是很贵的,自己如果已经是老用户了记得用身边的人帮你买一个即可。


服务器选择


目前发现各大厂家都推出了自家的新人首单优惠,官方入场,最为致命!太便宜了,这里推荐三家主流的



image.png


腾讯云的点击进来后,可以看到很明显的一栏关于帕鲁游戏的,点击后面的前往部署就可以进入优惠的服务器了


,推荐新人使用66元这一档,我个人也是买了这档来测试。


image.png



阿里云也推出了幻兽帕鲁专属云服务器,还是针对新用户的,如果你进来看到的价格也是入下图这样,那推荐入手


image.png



华为云也推出新用户一个月的优惠价,一个比一个卷


image.png


教程推荐


我这次操作的教程脚本是参考github.com/2lifetop/Pa… 这个项目
之所以用这个教程因为足够简单,也有界面可视化来配置私服的参数,


image.png


搭建步骤详细说明


这里我用的是腾讯云服务器,所以流程介绍腾讯云上面的搭建方式,如果你买的是其他家的也类似,核心步骤都是以下2点:



  • 一键安装脚本

  • 服务端配置(可选)

  • 端口8211开放


服务器购买


因为脚本推荐的是用 Debian 12,所以我购买腾讯云服务器的时候,直接选择了 Debian12带Docker的版本。


image.png


购买后就可以进入服务器的界面了,如果找不到,可以搜索轻量应用服务器


image.png


image.png


这里你可以用第三方ssh登录或者直接直接网页登录都行。我推荐用第三方登录,我用的是FinalShell这个软件,我第一步是进入修改密码。


image.png


然后就用FinalShell登录上了,稳的一批。


一键安装脚本


以root用户登陆到服务器然后运行以下命令即可。该脚本目前只在Debian12系统上验证过。如果遇上非网络问题则请自行更换系统或者寻求其他解决方案。


非root用户请先运行 sudo su命令。


1.  wget -O PalServerInstall.sh https://www.xuehaiwu.com/wp-content/uploads/shell/Pal/PalServerInstall.sh --no-check-certificate && chmod +x PalServerInstall.sh && ./PalServerInstall.sh

出现下面这个画面了,选择1安装即可


image.png


正常等待几分钟就可以安装好了, 不过我自己安装的时候出现过问题,提示安装失败,然后我就执行11删除,然后重新执行脚本安装就成功了。


服务端配置(可选)


因为搭建的是私服嘛,所以为了体验更加,这个脚本提供了在线参数修改,步骤也很简单
先打开 http://www.xuehaiwu.com/Pal/
把你想调整的参数自行设置


image.png


其中比较重要的配置有



  • 服务器名称

  • 服务器上允许的最大玩家数(上限为 32)

  • 用于授予管理员访问权限的密码

  • 普通玩家加入所需的密码


如果要使用管理员命令需要加上管理员密码,普通玩家加入密码暂时不推荐设置,因为可能会造成玩家进不来。


服务器配置生成也挺麻烦的,所以我简单的做了个生成网页。要修改哪个直接在网页上修改就行。配备了中文介绍。


都设置好了就可以点击下面的【生成配置文件】,然后复制下生成的wget这一行命令。


image.png


然后切回到SSH那边,黏贴执行即可,这样就会生成一个叫 PalWorldSettings.ini配置文件,这个时候就重新执行下脚本命令 ./PalServerInstall.sh ,调出命令窗口,选择4 就行,这样就会覆盖配置了。


修改之后不是立即生效的,要重启帕鲁的服务端才能生效,对应数字8


端口8211开放


到此还差最后一步,就是要开放8211端口,我们进入到腾讯云网页端,点击进入详情


image.png


切换到防火墙,配置两条,TCP、UDP端口8211开放即可。


image.png


到此就算搞定了服务端的搭建了,这时候复制下公网IP,一会要用到


登录游戏


游戏也是需要大家自己购买的,打开游戏后,会看到一个【加入多人游戏(专用服务器)】选项,点击这个


8b463bab9f2b026c77afaf711f79448.png


进来后看到底部这里了没,把你服务器公网的ip去替换下 :8211前面的ip数字即可
比如我的ip是:106.54.6.86,那我输入的就是 106.54.6.86:8211


image.png


总结


ok,到此就是我搭建幻兽帕鲁Palworld服务端的全部流程,这游戏还是挺有意思的,缺点是缝合怪,优点是缝的还不错,我昨天自己搭建完玩了2个小时,大部分在搭建我的房子,盖着停不下来哈哈,感觉可以盖个10层楼。


499f598cf68efdf9486e23424e65f44.png


别人盖的比我好看多了。


image.png


这游戏其实火起来还有一个梗:帕鲁大陆最不缺的就是帕鲁,你不干有的是帕鲁干。
图片


我体验了一下也发现很真实,在游戏里面和帕鲁交朋友哈哈哈,其实是在压榨它们,让它们帮我们干活,累倒了就换一个,帕鲁多的是不缺你一个。现实中我们不也是帕鲁吗,所以大家突然找到了共鸣。


各位上班的时候就是帕鲁,下班了在游戏里面压榨帕鲁。


作者:嘟嘟MD
来源:juejin.cn/post/7328621062727122944
收起阅读 »

大厂真实 Git 开发工作流程

记得之前也写过一篇类似的文章,来到我东后感觉不够详尽,很多流程还是太局限了。大厂的开发流程还是比较规范的。如果你来不及学习长篇大论的 git 文档,这篇文章够你入职时用一段时间了。 一、开发分支模型分类 目前所在部门使用是主要是四种:dev(开发)、test(...
继续阅读 »

记得之前也写过一篇类似的文章,来到我东后感觉不够详尽,很多流程还是太局限了。大厂的开发流程还是比较规范的。如果你来不及学习长篇大论的 git 文档,这篇文章够你入职时用一段时间了。


一、开发分支模型分类


目前所在部门使用是主要是四种:dev(开发)、test(测试)、uat(预发)、release(生产)



小公司可能就一个 dev、一个 master 就搞定了,测试都是开发人员自己来🤣。



二、开发主体流程



  1. 需求评审

  2. 开发排期

  3. 编码开发

  4. 冒烟测试(自检验)

  5. 冒烟通过,提交测试,合并代码到测试分支,部署测试环境

  6. 测试环境测试,开发修 bug

  7. 测试完成,提交预发,合并代码到预发分支,部署预发环境

  8. 预发环境测试,开发修 bug(修完的 bug 要重新走测试再走预发,这个下面会解释)

  9. 测试完成,产品验收

  10. 验收完成,提交生产,合并代码到生产分支,部署生产环境

  11. 生产运营(客户)验收

  12. 验收完成,结项


三、具体操作


1. 拉取代码


一般都会在本地默认创建一个 master 分支


git clone https://code.xxx.com/xxx/xxx.git

2. 初次开发需求前,要先拉取生产/预发分支,然后基于这个分支之上,创建自己的特性分支进行开发


git fetch origin release:release

git checkout release

git checkout -b feat-0131-jie

此时,在你本地已经有了一个 release 分支对应着远程仓库的 release 分支,还有一个内容基于 release 分支的特性分支,之后便可以在这个特性分支上进行需求开发了。


注意1:分支名称是有规范和含义的,不能乱取。

推荐格式:分支责任-需求日期/需求号-开发人姓名,一般按部门规范来,常见的有以下几种。


  - feat:新功能

- fix:修补bug

- doc:文档

- refactor:重构(即不是新增功能,也不是修改bug的代码变动)

- test:测试

- chore:构建过程或辅助工具的变动

注意2:为啥拉取的是生产/预发分支

之所以要拉取 release/uat 分支而不是拉取 dev/test,是因为后者可能包含着一些其他成员还未上线或者可能有 bug 的需求代码,这些代码没有通过验证,如果被你给拉取了,然后又基于此进行新的需求开发,那当你需求开发完成,而其他成员的需求还没上线,你将会把这些未验证的代码一起发送到 uat/release 上,导致一系列问题。


3. 需求开发完成,提交&合并代码


首先先在本地把新的改动提交,提交描述的格式可以参考着分支名的格式



  • 如果是新需求的提交,可以写成 "feat: 需求0131-新增账期"

  • 如果是 bug 修复,可以写成 "fix: 禅道3387-重复请求"


git add .

git commit -m "提交描述"

此时,本地当前分支已经记录了你的提交记录,接下来进行代码合并了


在代码合并之前,我们先要梳理一下我们应该如何对分支进行管理(非常重要!)


  1. 首先,我们需要认知到的是,每一个分支应该只对应一个功能,例如当我们开发需求 01 时,那么就创建一个 feat-01-jie 分支进行开发;开发需求 02 时,就另外创建一个 feat-02-jie 分支进行开发;修改生产环境的某个 bug 时,就创建 fix-jie-3378 进行开发,等等。


    这样做的目的是,能够把不同的功能/需求/修改分离开来。想象一下这样一个场景,如果有某些紧急的需求是需要提前上线的,而此时你的分支里既包含了这些紧急的需求,又包含了其他未开发好的需求,那么这两种需求就不能拆开来分别进行提测和上线了。


  2. 其次,在合并代码时,我们要将四种分支模型(dev、test、uat、release)作为参照物,而不是把关注点放在自己的分支上。比如我们要在 dev 上调试,那就需要把自己的分支合并到 dev 分支上;如果我们需要提测,则把自己的分支合并到 test 分支上,以此类推。


    即,我们要关注到,这四个环境的分支上,会有什么内容,会新增什么内容。切记不能反过来将这四个分支合并到自己的代码上!! 如果其他成员将自己的代码也提交到 dev 分支上,但是这个代码是没有通过验证的,此时你将 dev 往自己的分支上合,那之后的提测、上预发、生产则很大概率会出问题。所以一定要保持自己的分支是干净的!



接下来介绍合并代码的方式:


第一种:线上合并,也是推荐的规范操作

git push origin feat-0131-jie

先接着上面的提交步骤,将自己的分支推送到远程仓库。


然后在线上代码仓库中,申请将自己的分支合并到 xx 分支(具体是哪个分支就根据你当前的开发进度来,如 test),然后在线上解决冲突。如果有权限就自己通过了,如果没有就得找 mt 啥的


第二种,本地合并(前提你要有对应环境分支 push 的权限)

## 先切换到你要提交的环境分支上,如果本地还没有就先拉取下来
git fetch origin test:test

git checkout test

#
# 然后将自己的分支合并到环境分支上(在编辑器解决冲突)
git merge feat-0131-jie

#
# 最后将环境分支推送到远程仓库
git push origin test

## 先切换到你要提交的环境分支上,如果本地已有该分支,则需要先拉取最新代码
git checkout test

git pull origin test

#
# 然后将自己的分支合并到环境分支上(在编辑器解决冲突)
git merge feat-0131-jie

#
# 最后将环境分支推送到远程仓库
git push origin test

两种方式有何区别?为什么推荐第一种?

这是因为在团队协作开发的过程中,将合并操作限制在线上环境有以下几个好处:



  1. 避免本地合并冲突:如果多个开发人员同时在本地进行合并操作,并且对同一段代码进行了修改,可能会导致冲突。将合并操作集中在线上环境可以减少此类冲突的发生,因为不同开发人员的修改会先在线上进行合并,然后再通过更新拉取到本地。

  2. 更好的代码审查:将合并操作放在线上环境可以方便其他开发人员进行代码审查。其他人员可以在线上查看合并请求的代码变动、注释和讨论,并提供反馈和建议。这样可以确保代码的质量和可维护性。

  3. 提高可追溯性和可回滚性:将合并操作记录在线上可以更容易地进行版本控制和管理。如果出现问题或需要回滚到之前的版本,可以更轻松地找到相关的合并记录并进行处理。


当然,并非所有情况都适用于第一种方式。在某些特定情况下,例如个人项目或小团队内部开发,允许本地合并也是可以的。但在大多数团队协作的场景中,将合并操作集中在线上环境具有更多优势。


4. 验收完成,删除分支


当我们这一版的需求完成后,本地肯定已经留有很多分支了,这些分支对于之后的开发已经意义不大了,留下来只会看着一团糟。


git branch -d <分支名>

#
# 如果要强制删除分支(即使分支上有未合并的修改)
git branch -D <分支名>

四、一些小问题


1. 前面提到,预发环境修完的 bug 要重新走测试再走预发,为什么呢?


预生产环境是介于测试和生产环境之间的一个环境,它的目的是模拟生产环境并进行更真实的测试。
它是一个重要的测试环境,需要保持稳定和可靠。通过对修复的bug再次提交到测试环境测试,可以确保预生产环境中的软件版本是经过验证的,并且没有明显的问题。


当然,也不是非要这么做不可,紧急情况下,也可以选择直接发到预生产重新测试,只要你保证你的代码 99% 没问题了。


2. 代码合并错误,并且已经推送到远程分支,如何解决?


假设是在本地合并,本来要把特性分支合并到 uat 分支,结果不小心合到了 release 分支(绝对不是我自己的案例,绝对不是。。。虽然好在最后同事本地有我提交前的版本,事情就简单很多了)


首先切换到特性分支合并到的错误分支,比如是 release


git checkout release

然后查看最近的合并信息


git log --merges

撤销合并


git revert -m 1 <merge commit ID>


  • 这里的 merge commit ID 就是上一步查询出来的 ID 或者 ID 的前几个字符


最后,撤销远程仓库的推送


git push -f origin release


  • 这个命令会强制推送本地撤销合并后的 release 分支到远程仓库,覆盖掉远程仓库上的内容。(即,得通过一个新的提交来“撤销”上一次的提交,本质上是覆盖)


3. 当前分支有未提交的修改,但是暂时不想提交,想要切换到另一个分支该怎么做?


例如:你正在开发 B 需求,突然产品说 A 需求有点问题,让你赶紧改改,但是当前 B 需求还没开发完成,你又不想留下过多无用的提交记录,此时就可以按照下面这样做:


首先,可以将当前修改暂存起来,以便之后恢复


git stash

然后切换到目标分支,例如需求 A 所在分支


git checkout feat-a-jie

修改完 A 需求后,需要先切换回之前的分支,例如需求 B 所在分支


git checkout feat-b-jie

如果你不确定之前所在的分支名,可以使用以下命令列出暂存的修改以及它们所属的分支:


git stash list

最后从暂存中恢复之前的修改


git stash pop

此时你的工作区就恢复如初了!




喜欢本文的话,可以点赞收藏呀~😘


如果有疑问,欢迎评论区留言探讨~🤔


作者:JIE
来源:juejin.cn/post/7327863960008392738
收起阅读 »

《卖炭翁》致敬河北程序员,初读已解诗中意,再读却是诗中人!

起初他们追杀共产主义者的时候, 我没有说话 ——因为我不是共产主义者; 接着他们追杀犹太人的时候, 我没有说话 ——因为我不是犹太人; 后来他们追杀工会成员的时候, 我没有说话 ——因为我不是工会成员; 此后他们追杀天主教徒的时候, 我没有说话 ——因为我是新...
继续阅读 »

起初他们追杀共产主义者的时候,


我没有说话


——因为我不是共产主义者;


接着他们追杀犹太人的时候,


我没有说话


——因为我不是犹太人;


后来他们追杀工会成员的时候,


我没有说话


——因为我不是工会成员;


此后他们追杀天主教徒的时候,


我没有说话


——因为我是新教教徒;


最后他们奔我而来,


那时已经没有人能为我说话了。



这一首著名的《我没有说话》是德国神学家马丁・尼莫拉牧师的忏悔诗,尽管他写的是自己,但这首诗却振聋发聩,发人深省,其描述忽视与表面上自己无关的团体所造成的结果。该诗后来常被引用,作为对事不关己高高挂起的人的呼吁。


这首诗被镌刻在美国马萨诸塞州波士顿的新英格兰犹太人大屠杀纪念碑石碑上。


马丁・尼莫拉曾经生活在一个黑暗无光的时代,遭受过极权统治的迫害,这一经历对他来说,有着切肤之痛。


因为自己的惨痛经历,尼莫拉牧师认识到:在这个世界上,人与人的命运往往是休戚与共的,不坚持真理,不伸张正义,不维护公平,在邪恶面前只顾及自身的利益,对他人被冤屈被欺凌被迫害漠然置之,最终受到惩罚的是我们自己。


最近的事情大家也都晓得了,这件事让我们禁不住想起初中课本里的一篇课文:唐代大诗人白居易所创作的《卖炭翁》:



卖炭翁,伐薪烧炭南山中。

满面尘灰烟火色,两鬓苍苍十指黑。

卖炭得钱何所营?身上衣裳口中食。

可怜身上衣正单,心忧炭贱愿天寒。

夜来城外一尺雪,晓驾炭车辗冰辙。

牛困人饥日已高,市南门外泥中歇。

翩翩两骑来是谁?黄衣使者白衫儿。

手把文书口称敕,回车叱牛牵向北。

一车炭,千余斤,宫使驱将惜不得。

半匹红纱一丈绫,系向牛头充炭直。




白居易在《新乐府》中每首诗的题目下面都有一个序,说明这首诗的主题。


《卖炭翁》的序是“苦宫市也”,就是要反映宫市给人民造成的痛苦。唐代皇宫里需要物品,就派人去市场上拿,随便给点钱,实际上是公开掠夺。


唐德宗时用太监专门负责掠夺老百姓。白居易写作《新乐府》是在宫市为害最深的时候,他对宫市有十分的了解,对太监极度的痛恨,对人民又有深切的同情,所以才能写出这首感人至深的《卖炭翁》。


这首诗的意义,远不止于对宫市的揭露。诗人在卖炭翁这个典型形象上,概括了唐代劳动人民的辛酸和悲苦,在卖炭这一件小事上反映出了当时社会的黑暗和不平。读着这首诗,读者所看到的决不仅仅是卖炭翁一个人,透过他,还能看到有许许多多种田的、打渔的、织布以及编程的人出现在眼前。


他们虽然不是“两鬓苍苍十指黑”,但也各自带着劳苦生活的标记;他们虽然不会因为卖炭而受到损害,但也各自在田租或赋税的重压下流着辛酸和仇恨的泪水。《卖炭翁》这首诗不但在当时有积极意义,即使对于今天的读者也有一定的教育作用。


正道是:



初读已解诗中意,再读却是诗中人!



作者:刘悦的技术博客
来源:juejin.cn/post/7284468618019143695
收起阅读 »

支付系统的心脏:简洁而精妙的状态机设计与核心代码实现

本篇主要讲清楚什么是状态机,简洁的状态机对支付系统的重要性,状态机设计常见误区,以及如何设计出简洁而精妙的状态机,核心的状态机代码实现等。 我前段时间面试一个工作过4年的同学竟然没有听过状态机。假如你没有听过状态机,或者你听过但没有写过,或者你是使用if el...
继续阅读 »



本篇主要讲清楚什么是状态机,简洁的状态机对支付系统的重要性,状态机设计常见误区,以及如何设计出简洁而精妙的状态机,核心的状态机代码实现等。


我前段时间面试一个工作过4年的同学竟然没有听过状态机。假如你没有听过状态机,或者你听过但没有写过,或者你是使用if else 或switch case来写状态机的代码实现,建议花点时间看看,一定会有不一样的收获。


1. 前言


在线支付系统作为当今数字经济的基石,每年支撑几十万亿的交易规模,其稳定性至关重要。在这背后,是一种被誉为支付系统“心脏”的技术——状态机。本文将一步步介绍状态机的概念、其在支付系统中的重要性、设计原则、常见误区、最佳实践,以及一个实际的Java代码实现。


2. 什么是状态机


状态机,也称为有限状态机(FSM, Finite State Machine),是一种行为模型,由一组定义良好的状态、状态之间的转换规则和一个初始状态组成。它根据当前的状态和输入的事件,从一个状态转移到另一个状态。


下图就是在《支付交易的三重奏:收单、结算与拒付在支付系统中的协奏曲》中提到的交易单的状态机。



从图中可以看到,一共4个状态,每个状态之间的转换由指定的事件触发。


3. 状态机对支付系统的重要性


想像一下,如果没有状态机,支付系统如何知道你的订单已经支付成功了呢?如果你的订单已经被一个线程更新为“成功”,另一个线程又更新成“失败”,你会不会跳起来?


在支付系统中,状态机管理着每笔交易的生命周期,从初始化到完成或失败。它确保交易在正确的时间点,以正确的顺序流转到正确的状态。这不仅提高了交易处理的效率和一致性,还增强了系统的鲁棒性,使其能够有效处理异常和错误,确保支付流程的顺畅。


4. 状态机设计基本原则


无论是设计支付类的系统,还是电商类的系统,在设计状态机时,都建议遵循以下原则:


明确性: 状态和转换必须清晰定义,避免含糊不清的状态。


完备性: 为所有可能的事件-状态组合定义转换逻辑。


可预测性: 系统应根据当前状态和给定事件可预测地响应。


最小化: 状态数应保持最小,避免不必要的复杂性。


5. 状态机常见设计误区


工作多年,见过很多设计得不好的状态机,导致运维特别麻烦,还容易出故障,总结出来一共有这么几条:


过度设计: 引入不必要的状态和复杂性,使系统难以理解和维护。


不完备的处理: 未能处理所有可能的状态转换,导致系统行为不确定。


硬编码逻辑: 过多的硬编码转换逻辑,使系统不具备灵活性和可扩展性。


举一个例子感受一下。下面是亲眼见过的一个交易单的状态机设计,而且一眼看过去,好像除了复杂一点,整体还是合理的,比如初始化,受理成功就到ACCEPT,然后到PAYING,如果直接成功就到PAIED,退款成功就到REFUND。



我说说这个状态机有几个不合理的地方:



  1. 过于复杂。一些不必要的状态可以去掉,比如ACCEPT没有存在的必要。

  2. 职责不明确。支付单就只管支付,到PAIED就支付成功,就是终态不再改变。REFUND应该由退款单来负责处理,否则部分退款怎么办。


我们需要的改造方案:



  1. 精简掉不必要的状态,比如ACCEPT。

  2. 把一些退款、请款等单据单独抽出去,这样状态机虽然多了,但是架构更加清晰合理。


主单:



普通支付单:



预授权单:



请款单:



退款单:



6. 状态机设计的最佳实践


在代码实现层面,需要做到以下几点:


分离状态和处理逻辑:使用状态模式,将每个状态的行为封装在各自的类中。


使用事件驱动模型:通过事件来触发状态转换,而不是直接调用状态方法。


确保可追踪性:状态转换应该能被记录和追踪,以便于故障排查和审计。


具体的实现参考第7部分的“JAVA版本状态机核心代码实现”。


7. 常见代码实现误区


经常看到工作几年的同学实现状态机时,仍然使用if else或switch case来写。这是不对的,会让实现变得复杂,且容易出现问题。


甚至直接在订单的领域模型里面使用String来定义,而不是把状态模式封装单独的类。


还有就是直接调用领域模型更新状态,而不是通过事件来驱动。


错误的代码示例:


if (status.equals("PAYING") {
status = "SUCCESS";
} else if (...) {
...
}

或者:


class OrderDomainService {
public void notify(PaymentNotifyMessage message) {
PaymentModel paymentModel = loadPaymentModel(message.getPaymentId());
// 直接设置状态
paymentModel.setStatus(PaymentStatus.valueOf(message.status);
// 其它业务处理
... ...
}
}

或者:


public void transition(Event event) {
switch (currentState) {
case INIT:
if (event == Event.PAYING) {
currentState = State.PAYING;
} else if (event == Event.SUCESS) {
currentState = State.SUCESS;
} else if (event == Event.FAIL) {
currentState = State.FAIL;
}
break;
// Add other case statements for different states and events
}
}

8. JAVA版本状态机核心代码实现


使用Java实现一个简单的状态机,我们将采用枚举来定义状态和事件,以及一个状态机类来管理状态转换。


定义状态基类


/**
* 状态基类
*/

public interface BaseStatus {
}

定义事件基类


/**
* 事件基类
*/

public interface BaseEvent {
}

定义“状态-事件对”,指定的状态只能接受指定的事件


/**
* 状态事件对,指定的状态只能接受指定的事件
*/

public class StatusEventPairextends BaseStatus, E extends BaseEvent> {
/**
* 指定的状态
*/

private final S status;
/**
* 可接受的事件
*/

private final E event;

public StatusEventPair(S status, E event) {
this.status = status;
this.event = event;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof StatusEventPair) {
StatusEventPair other = (StatusEventPair)obj;
return this.status.equals(other.status) && this.event.equals(other.event);
}
return false;
}

@Override
public int hashCode() {
// 这里使用的是google的guava包。com.google.common.base.Objects
return Objects.hashCode(status, event);
}
}

定义状态机


/**
* 状态机
*/

public class StateMachineextends BaseStatus, E extends BaseEvent> {
private final Map, S> statusEventMap = new HashMap<>();

/**
* 只接受指定的当前状态下,指定的事件触发,可以到达的指定目标状态
*/

public void accept(S sourceStatus, E event, S targetStatus) {
statusEventMap.put(new StatusEventPair<>(sourceStatus, event), targetStatus);
}

/**
* 通过源状态和事件,获取目标状态
*/

public S getTargetStatus(S sourceStatus, E event) {
return statusEventMap.get(new StatusEventPair<>(sourceStatus, event));
}
}

定义支付的状态机。注:支付、退款等不同的业务状态机是独立的


/**
* 支付状态机
*/

public enum PaymentStatus implements BaseStatus {

INIT("INIT", "初始化"),
PAYING("PAYING", "支付中"),
PAID("PAID", "支付成功"),
FAILED("FAILED", "支付失败"),
;

// 支付状态机内容
private static final StateMachine STATE_MACHINE = new StateMachine<>();
static {
// 初始状态
STATE_MACHINE.accept(null, PaymentEvent.PAY_CREATE, INIT);
// 支付中
STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);
// 支付成功
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_SUCCESS, PAID);
// 支付失败
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_FAIL, FAILED);
}

// 状态
private final String status;
// 描述
private final String description;

PaymentStatus(String status, String description) {
this.status = status;
this.description = description;
}

/**
* 通过源状态和事件类型获取目标状态
*/

public static PaymentStatus getTargetStatus(PaymentStatus sourceStatus, PaymentEvent event) {
return STATE_MACHINE.getTargetStatus(sourceStatus, event);
}
}

定义支付事件。注:支付、退款等不同业务的事件是不一样的


/**
* 支付事件
*/

public enum PaymentEvent implements BaseEvent {
// 支付创建
PAY_CREATE("PAY_CREATE", "支付创建"),
// 支付中
PAY_PROCESS("PAY_PROCESS", "支付中"),
// 支付成功
PAY_SUCCESS("PAY_SUCCESS", "支付成功"),
// 支付失败
PAY_FAIL("PAY_FAIL", "支付失败");

/**
* 事件
*/

private String event;
/**
* 事件描述
*/

private String description;

PaymentEvent(String event, String description) {
this.event = event;
this.description = description;
}
}

在支付单模型中声明状态和根据事件推进状态的方法:


/**
* 支付单模型
*/

public class PaymentModel {
/**
* 其它所有字段省略
*/


// 上次状态
private PaymentStatus lastStatus;
// 当前状态
private PaymentStatus currentStatus;


/**
* 根据事件推进状态
*/

public void transferStatusByEvent(PaymentEvent event) {
// 根据当前状态和事件,去获取目标状态
PaymentStatus targetStatus = PaymentStatus.getTargetStatus(currentStatus, event);
// 如果目标状态不为空,说明是可以推进的
if (targetStatus != null) {
lastStatus = currentStatus;
currentStatus = targetStatus;
} else {
// 目标状态为空,说明是非法推进,进入异常处理,这里只是抛出去,由调用者去具体处理
throw new StateMachineException(currentStatus, event, "状态转换失败");
}
}
}

代码注释已经写得很清楚,其中StateMachineException是自定义,不想定义的话,直接使用RuntimeException也是可以的。


在支付业务代码中的使用:只需要paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()))


/**
* 支付领域域服务
*/

public class PaymentDomainServiceImpl implements PaymentDomainService {

/**
* 支付结果通知
*/

public void notify(PaymentNotifyMessage message) {
PaymentModel paymentModel = loadPaymentModel(message.getPaymentId());
try {

// 状态推进
paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()));
savePaymentModel(paymentModel);
// 其它业务处理
... ...
} catch (StateMachineException e) {
// 异常处理
... ...
} catch (Exception e) {
// 异常处理
... ...
}
}
}

上面的代码只需要加完善异常处理,优化一下注释,就可以直接用起来。


好处:



  1. 定义了明确的状态、事件。

  2. 状态机的推进,只能通过“当前状态、事件、目标状态”来推进,不能通过if else 或case switch来直接写。比如:STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);

  3. 避免终态变更。比如线上碰到if else写状态机,渠道异步通知比同步返回还快,异步通知回来把订单更新为“PAIED”,然后同步返回的代码把单据重新推进到PAYING。


9. 并发更新问题


留言中“月朦胧”同学提到:“状态机领域模型同时被两个线程操作怎么避免状态幂等问题?”


这是一个好问题。在分布式场景下,这种情况太过于常见。同一机器有可能多个线程处理同一笔业务,不同机器也可能处理同一笔业务。


业内通常的做法是设计良好的状态机 + 数据库锁 + 数据版本号解决。



简要说明:



  1. 状态机一定要设计好,只有特定的原始状态 + 特定的事件才可以推进到指定的状态。比如 INIT + 支付成功才能推进到sucess。

  2. 更新数据库之前,先使用select for update进行锁行记录,同时在更新时判断版本号是否是之前取出来的版本号,更新成功就结束,更新失败就组成消息发到消息队列,后面再消费。

  3. 通过补偿机制兜底,比如查询补单。

  4. 通过上述三个步骤,正常情况下,最终的数据状态一定是正确的。除非是某个系统有异常,比如外部渠道开始返回支付成功,然后又返回支付失败,说明依赖的外部系统已经异常,这样只能进人工差错处理流程。


10. 结束语


状态机在支付系统中扮演着不可或缺的角色。一个专业、精妙的状态机设计能够确保支付流程的稳定性和安全性。本文提供的设计原则、常见误区警示和最佳实践,旨在帮助开发者构建出更加健壮和高效的支付系统。而随附的Java代码则为实现这一关键组件提供了一个清晰、灵活的起点。希望这些内容能够对你有用。



作者:隐墨星辰
来源:juejin.cn/post/7321569896453521419
收起阅读 »

转全栈之路,会比想象中的艰难

背景 我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。 在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了...
继续阅读 »

背景


我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。


在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了一家公司,也很想记录一下到底有什么不同。


大前端部门业务部门
组织人数近30人,纯前端方向近40人,分为不同方向,前端背景1人
工作模式由于同事都在天南海北,需要通过视频会议进行沟通纯下线沟通,所有同事都base深圳
沟通效率较低,每次沟通都需要调试设备,共享屏幕等,并且见不到面很多信息会失真高,直接面谈,肢体语言这些信息不会丢失
工作节奏有排期压力,有承诺客户交付时间。如果排期不合理会很疲惫。没有排期压力,前端工作量相比之前轻松
设计资源有专门的UED团队出图,前端不需要思考如何进行交互,这部分工作由设计师承担无设计资源,交互的好坏完全取决于研发的审美水平与自我要求
前端技术建设每个季度会有横向建设,有组件库共建等机会,前端技术相对先进部门内部无前端建设,依赖公司基建与之前经验
同事组成深圳base全员年轻化,校招生为主,因为年龄相同且技术方向相同,天然就有很多话题资深员工多,校招生占比很低,且划分不同方向,一般自己方向的人自己内部沟通较多
和+1的关系base不同,沟通频率很低。因为主要是做业务方的需求,沟通内容主要在支持工作的进展上。base相同,沟通频率比以前高5-10倍,除同步开发进展,还会针对产品迭代方向,用户体验等问题进行沟通
技术成长受限于部门性质以及绩效评价体系,员工需要在前端技术领域保持专业且高效,但工作一定年限后有挑战性的业务需求不足,容易遇到职业发展瓶颈。因为前端人数多,所以存在横向建设的空间,可以共建组件库等基建,非常自然的会接触这些需求。角色划分不明确,前后端可以相互支援彼此,大家摘掉前后端的标签,回归通用开发的角色。技术成长依赖自驱力与公司技术水平。研发人少,没有内部的横向建设机会。

纠结


为什么要转全栈?究竟有什么收益?我会在心里时不时问自己这个问题。任何一门技能,从入门到精通,都需要很多时间的学习与实践,在初期都会经历一段相当痛苦的时光。除了学习不轻松,能否创造出更大的价值也是一个问号。


但这次转全栈,有天时地利人和的作用,总结下来就是:



  1. Leader支持:和Leader沟通过,Leader觉得在我们团队多做多学对个人,对团队都有益处,欢迎我大胆尝试

  2. 后端同学支持:我们团队的细分项目多,后端工作饱和,可以分一个相对独立的活给我

  3. 全栈化背景:原先的大前端部门已经有部分前端转为全栈开发职能,部门层面鼓励全栈化发展

  4. 需求清晰:有些开发之所以忙碌是因为开会和对齐耗时太多。但是我目前拿到的prd都非常清晰,拿到就能直接开发,对齐扯皮的时间几乎不计,我只需要完成开发工作即可。这节约了我大量时间成本。想到之前经常是一天开个1-2小时会,搞得很疲惫。

  5. 工作熟练:从实习开始算起,我已经有2年多的开发经验,可以在预期时间内完成需求开发和bugfix,因此安全的预留时间精力转全栈。


其实不仅仅是我,和很多做前端的同事、朋友也都聊过,其实内心各有各的纠结。基本上大家的内心想法就是想在有限的条件下学习后端,并在有限的条件下承担一部分后端开发。


想学后端的原因:



  1. 纯属好奇心,想研究一下后端技术栈

  2. 前端作为最终的执行角色,话语权低

  3. 业务参与度低,可以半游离业务的存在,较边缘化。未来如果希望成长为管理,难以做业务管理,只能做技术管理,想象空间天花板就是成为管理一批前端的技术管理。

  4. 工作遇到天花板,想多了解一下其他的内容


想在有限条件下学习后端的原因:



  1. 工作比较忙碌,没那么多时间学习

  2. 学习一门技能要算ROI,学太多了如果既不能升职也不能加薪就没有意义

  3. 不确定市场对于全栈人才的反应,不想all in


想承担一部分后端开发的原因:



  1. 学习任何一门技能只有理论没有实践遗忘速度最快,马上就会回归到学习之前

  2. 掌握后端技能但没有企业级实战经验,说服力很弱


不想学习后端的原因:



  1. 国内市场上的全栈岗位数量稀少,如果后端岗位有10个,前端岗位有3个,那么可能就只有1个全栈岗位

  2. 普通前后端开发薪酬基本上没有区别,未来谁更好晋升在当前的经济背景也难说

  3. 大概率前端依然是自己的职业发展主线,学多一门技能可能会分摊本可以提升前端能力的时间精力

  4. 做舒适圈里面的事情很舒服,谁知道多做会不会有好处


我就是在这种纠结中一路学过来,从8月开始,痛苦且挣扎,不过到目前为止还可以接受。学到现在甚至已经有点麻木。但我也确实不知道继续在前端领域还能专精什么技能,现有的业务没有那么大的挑战性让我快速成长,所以想跳脱出来看看更大的世界。


学习路线


曲线学习


如果说做前端开发我是如鱼得水,那做后端开发就是经常呛到水。


记得我刚开始做前端实习的时候,真心感到前端知识好像黑洞,永远也学不完。由此非常佩服之前的同事,怎么把这些代码写出来的,有些代码后面一看写的还不错,甚至可能会感觉脊背发凉,是自己太弱还是自己太强?


在实习的时候,我的学习曲线可以说是一个向外扩散的圆。比如我第一次接触webpack的时候,根本不了解这是什么工具,之前一直在用jQuery写项目,所有的js都是明文写好,然后通过script引入到html中。所以一开始我会去查这个webpack到底是什么内容,但脑海中对他的印象还是非常模糊。接着我又因为webpack了解到了babel,css-loader这些概念,又去学习。又发现这需要利用到node,又去学习了《深入浅出node.js》。再后来又了解到了sourcemap等概念。直到正式加入字节半年后,我自己配了一次webpack,并且阅读了他的源码。进行了debug,进行了一次webpack插件开发的分享,才有信心说自己是真的弄明白了。不过这个弄明白,也仅限于排查bug,配项目,进行plugin和loader的开发,如果遇到更难的领域,那又将解锁一块黑洞。


怎么学


学习后端,要学的内容也一点都不少,作为新人会遇到非常多的问题。



  1. 怎么学 - 是死皮赖脸的逮住后端同学使劲问,还是多自己研究研究?遇到所有同事都不会的问题怎么处理?

  2. 学到什么程度 - 究竟要学到怎样的程度才能进入项目开发,而不犯下一些非常愚蠢的问题呢?

  3. 学习顺序 - 最简单的办法就是去看项目,看到不懂的分析一下这是什么模块的,看看要不要系统性的去了解。


我比较喜欢一开始就系统性的学,学完后再查缺补漏,再开启第二轮学习。


比如Go,官网就有很详细的文档,但未必适合新人去学。我跟着官网学了一阵子之后跑b站找视频学习了。然后又Google了一些资料,大致讲了一下反射、切片的原理,以及一些错误用法。学习Go大概用了2-3周。刚学完直接去看项目还是会觉得非常不适应,需要不断的让自己去阅读项目代码,找到Go的那种感觉。


然后需要学习很多公司内部的基建



  • 微服务架构 - 公司内部所有服务都是微服务架构,需要了解服务发现、服务治理、观测、鉴权这些基本概念以及大致的原理。为了在本地开发环境使用微服务,还需要在本地安装doas,用来获取psm的token。

  • RDS - 公司内的项目分为了各种环境,非常复杂。可以自己先创建一个MySQL服务自测,看看公司的云平台提供了哪些能力。

  • Redis - 大致了解即可,简单用不难

  • RPC - 微服务通过RPC传递,RPC协议通过IDL来定义接口传输格式,像字节会在api管理平台做封装。你定义好的IDL可以直接生成一个gopkg上传到内部镜像,然后其他用户直接go get这个库就能调用你的服务。但如果你是node服务,就可以在本地通过字节云基建的工具库自动生成代码。

  • Gorm - 所有的MySQL最终如果通过go程序调用,都需要经过gorm的封装,来避免一些安全问题。同时也可以规避一些低级错误。还需要了解gen怎么使用,将MySQL库的定义自动生成为orm代码。


还要好好学习一下MySQL的用法,这边花了一周看完了《MySQL必知必会》,然后去leetcode刷题。国庆节刷了大概80道MySQL的题目,很爽。从简单的查询,到连接、子查询、分组、聚合,再到比较复杂的窗口函数、CTE全刷了个遍,刷上瘾了。


接着就可以去看项目代码了,这一部分还是蛮折腾的,对于新人来说。本身阅读别人的代码,对于很多开发者来说就是一件痛苦的事情,何况是去阅读自己不熟悉的语言的别人的代码。


我最近接手的一个半废弃项目,就很离谱。开发者有的已经离职了,提交记录是三四年前的。PRD也找不全,到今天权限还没拿齐,明天再找人问问。这边可能是真的上下文就是要丢失的,没法找了。只能自己创建一个新的文档,把相关重点补充一下。


明天找一下这个项目的用户,演示一下怎么使用,然后根据对用法的理解进行开发……


收获


新鲜感


一直写前端真的有点腻,虽然现在技术还在迭代,但也万变不离其宗。而且真的是有点过分内卷了,像一个打包工具从webpack -> esbuild -> vite -> turbopack -> rspack。不可否认的是这些开发者的努力,为前端生态的繁荣做出了贡献。但对于很多业务来说,其实并没有太大的性能问题,对于这部分项目来说升级的收益很小。比如云服务的控制台,基本都是微前端架构,每个前端项目都非常小,就算用webpack热更新也不会慢。而且webpack使用下来是最稳定的,我现在的项目用的是vite,会存在样式引入顺序的问题,导致开发环境和生产环境的页面区别。


后端技术栈不管好还是不好,反正对我来说是很新鲜的。虽然我之前Python、Go也都用过,也用Python写出了完整的项目,但论企业级开发这算第一次~各方面都更正规


Go写起来很舒服,虽然写同样的需求代码量比TypeScript多一堆……习惯之后还是可以感受到Go的简单与安心。Go打包就没那么多事,你本地怎么跑服务器那边就怎么跑,不像前端可能碰到一堆兼容性问题。


真的有学到


我前几个月买了掘金大佬神说要有光的小课《Nest 通关秘籍》,据我了解我的几个同事也买了。不过我没坚持下来,因为工作上实在是没有使用到Nest的机会。我无法接受学了两三个月却无法在工作里做出产出的感觉。


但这一次学了可以立马去用,可以在工作中得到检验,可以接受用户的检验。我就会得到价值感与成就感。


而且字节的Go基建在我认知里很牛叉,一家以Go为主的大厂,养得起很多做基建的人。比如张金柱Gorm的作者,竟然就在字节,我前几天知道感觉牛人竟然……


Go的学习资料也非常多,还有很多实战的,真的像突然打开了新世界的大门~


与业务更近,以及更平和的心态


如果我没有学后端,会在“前端已死”的氛围里胡思乱想,忽略了前端的业务价值,前端依旧是很重要的岗位。让后端来写前端不是不行,但只有分工才能达到最高的效率。对于一个正常的业务团队来说,也完全没必要让后端去硬写前端,好几个后端配一个前端,也不是什么事。


就我目前的工作经验来看,后端可以和业务的使用者更近的对接。我们这里的后端开发会和非常多用户对接需求,了解他们的真实使用场景,思考他们背后的需求,可能还能弥补一下产品思考上的不周。和用户对齐数据传递、转换、存储、查询、以及需要不需要定时任务等等,这些后端会去负责。


而前端负责最终的交互,基本可以不用碰到使用者,基本上只需要根据后端给的接口文档,调用接口把内容渲染在表格上即可。碰到用户提反馈一般在于,加载慢(往往是数据请求很慢,但是用户会觉得是前端的问题)、交互不满意(交互美不美真的是一个很难量化的问题,按理说这属于UI的绩效)、数据请求失败(前后端接口对齐虽然体验越来越好,但是开发阶段经常改动还是免不了,最后导致前后端没有同步)。


之前开周会的时候,我基本上说不上什么话。一个是刚转岗,确实不熟。另一个是前端半游离于业务的状态,单纯的把接口内容渲染出来也很难有什么思考,导致开会比较尴尬。基本是后端在谈解决了什么oncall,解决了什么技术问题,有什么业务建设的思考等等。


这次看了别人代码之后非常期盼未来能独立owner一个方向,享受闭环一个小功能的乐趣。


职业安全感


我学的这项技能能够立马投入到工作中进行自我检验,因此我相信自己学的是“有效技能”。我理解的无效技能指学了用不上,然后忘了,花了很多时间精力最后不升职不加薪。之前看李运华大佬的网课《大厂晋升指南》里面有提到,有人花了半年把编译原理这个看似非常重要的计算机基础课学的很扎实,但因为业务不需要,不产生业务价值,也不可能获得提拔的机会。


其实内部全栈化我的理解,还有一个原因,那就是灵活调度。现在这个背景下,老板更希望用有限的人力去做更多事情。有些业务前端过剩了,但是缺后端,这个时候如果直接去招后端,一方面增加成本,再就是没有解决剩的前端,反之也是。在盘点hc的时候就容易出现调整。


多学一些有效技能,提高解决问题的深度、广度,让自己更值钱。我想不管是什么职能,最终都要回归到为业务服务的目标上。


End


写到这里,我依旧在转全栈的路上,只是想给自己一个阶段性的答案。


脱离舒适圈,进入拉伸区,需要付出,需要勇气,也需要把握机遇。给自己多一种可能,去做,去挑战自己不会的。我相信他山之石可以攻玉,越往深处走,就越能触类旁通。


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

加密的手机号,如何模糊查询?

前言 前几天,知识星球中有位小伙伴,问了我一个问题:加密的手机号如何模糊查询? 我们都知道,在做系统设计时,考虑到系统的安全性,需要对用户的一些个人隐私信息,比如:登录密码、身-份-证号、银彳亍卡号、手机号等,做加密处理,防止用户的个人信息被泄露。 很早之前,...
继续阅读 »

前言


前几天,知识星球中有位小伙伴,问了我一个问题:加密的手机号如何模糊查询?


我们都知道,在做系统设计时,考虑到系统的安全性,需要对用户的一些个人隐私信息,比如:登录密码、身-份-证号、银彳亍卡号、手机号等,做加密处理,防止用户的个人信息被泄露。


很早之前,CSDN遭遇了SQL注入,导致了600多万条明文保存的用户信息被泄。


因此,我们在做系统设计的时候,要考虑要把用户的隐私信息加密保存。


常见的对称加密算法有 AES、SM4、ChaCha20、3DES、DES、Blowfish、IDEA、RC5、RC6、Camellia等。


目前国际主流的对称加密算法是AES,国内主推的则是SM4


无论是用哪种算法,加密前的字符串,和加密后的字符串,差别还是比较大的。


比如加密前的字符串:苏三说技术,使用密钥:123,生成加密后的字符串为:U2FsdGVkX1+q7g9npbydGL1HXzaZZ6uYYtXyug83jHA=


如何对加密后的字符串做模糊查询呢?


比如:假设查询苏三关键字,加密后的字符串是:U2FsdGVkX19eCv+xt2WkQb5auYo0ckyw


上面生成的两个加密字符串差异看起来比较大,根本没办法直接通过SQL语句中的like关键字模糊查询。


那我们该怎么实现加密的手机号的模糊查询功能呢?


1 一次加载到内存


实现这个功能,我们第一个想到的办法可能是:把个人隐私数据一次性加载到内存中缓存起来,然后在内存中先解密,然后在代码中实现模糊搜索的功能。


图片这样做的好处是:实现起来比较简单,成本非常低。


但带来的问题是:如果个人隐私数据非常多的话,应用服务器的内存不一定够用,可能会出现OOM问题。


还有另外一个问题是:数据一致性问题。


如果用户修改了手机号,数据库更新成功了,需要同步更新内存中的缓存,否则用户查询的结果可能会跟实际情况不一致。


比如:数据库更新成功了,内存中的缓存更新失败了。


或者你的应用,部署了多个服务器节点,有一部分内存缓存更新成功了,另外一部分刚好在重启,导致更新失败了。


该方案不仅可能会导致应用服务器出现OOM问题,也可能会导致系统的复杂度提升许多,总体来说,有点得不偿失。


2 使用数据库函数


既然数据库中保存的是加密后的字符串,还有一种方案是使用数据库的函数解密。


我们可以使用MySQL的DES_ENCRYPT函数加密,使用DES_DECRYPT函数解密:


SELECT 
DES_DECRYPT('U2FsdGVkX1+q7g9npbydGL1HXzaZZ6uYYtXyug83jHA=''123')


应用系统重所有的用户隐私信息的加解密都在MySQL层实现,不存在加解密不一致的情况。


该方案中保存数据时,只对单个用户的数据进行操作,数据量比较小,性能还好。


但模糊查询数据时,每一次都需要通过DES_DECRYPT函数,把数据库中用户某个隐私信息字段的所有数据都解密了,然后再通过解密后的数据,做模糊查询。


如果该字段的数据量非常大,这样每次查询的性能会非常差。


3 分段保存


我们可以将一个完整的字符串,拆分成多个小的字符串。


以手机号为例:18200256007,按每3位为一组,进行拆分,拆分后的字符串为:182,820,200,002,025,256,560,600,007,这9组数据。


然后建一张表:


CREATE TABLE `encrypt_value_mapping` (
  `id` bigint NOT NULL COMMENT '系统编号',
  `ref_id` bigint NOT NULL COMMENT '关联系统编号',
  `encrypt_value` varchar(255NOT NULL COMMENT '加密后的字符串'
ENGINE=InnoDB  CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='分段加密映射表'

这张表有三个字段:



  • id:系统编号。

  • ref_id:主业务表的系统编号,比如用户表的系统编号。

  • encrypt_value:拆分后的加密字符串。


用户在写入手机号的时候,同步把拆分之后的手机号分组数据,也一起写入,可以保证在同一个事务当中,保证数据的一致性。


如果要模糊查询手机号,可以直接通过encrypt_value_mapping的encrypt_value模糊查询出用户表的ref_id,再通过ref_id查询用户信息。


具体sql如下:


select s2.id,s2.name,s2.phone 
from encrypt_value_mapping s1
inner join `user` s2 on s1.ref_id=s2.id
where s1.encrypt_value = 'U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB'
limit 0,20;

这样就能轻松的通过模糊查询,搜索出我们想要的手机号了。


注意这里的encrypt_value用的等于号,由于是等值查询,效率比较高。


注意:这里通过sql语句查询出来的手机号是加密的,在接口返回给前端之前,需要在代码中统一做解密处理。


为了安全性,还可以将加密后的明文密码,用*号增加一些干扰项,防止手机号被泄露,最后展示给用户的内容,可以显示成这样的:182***07


4 其他的模糊查询


如果除了用户手机号,还有其他的用户隐私字段需要模糊查询的场景,该怎么办?


我们可以将encrypt_value_mapping表扩展一下,增加一个type字段。


该字段表示数据的类型,比如:1.手机号 2.身-份-证 3.银彳亍卡号等。


这样如果有身-份-证和银彳亍卡号模块查询的业务场景,我们可以通过type字段做区分,也可以使用这套方案,将数据写入到encrypt_value_mapping表,最后根据不同的type查询出不同的分组数据。


如果业务表中的数据量少,这套方案是可以满足需求的。


但如果业务表中的数据量很大,一个手机号就需要保存9条数据,一个身-份-证或者银彳亍卡号也需要保存很多条数据,这样会导致encrypt_value_mapping表的数据急剧增加,可能会导致这张表非常大。


最后的后果是非常影响查询性能。


那么,这种情况该怎么办呢?
最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。


我以往的技术群里技术氛围非常不错,大佬很多。


image.png


加微信:su_san_java,备注:加群,即可加入该群。


5 增加模糊查询字段


如果数据量多的情况下,将所有用户隐私信息字段,分组之后,都集中到一张表中,确实非常影响查询的性能。


那么,该如何优化呢?


答:我们可以增加模糊查询字段。


还是以手机模糊查询为例。


我们可以在用户表中,在手机号旁边,增加一个encrypt_phone字段。


CREATE TABLE `user` (
  `id` int NOT NULL,
  `code` varchar(20)  NOT NULL,
  `age` int NOT NULL DEFAULT '0',
  `name` varchar(30NOT NULL,
  `height` int NOT NULL DEFAULT '0',
  `address` varchar(30)  DEFAULT NULL,
  `phone` varchar(11DEFAULT NULL,
  `encrypt_phone` varchar(255)  DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'

然后我们在保存数据的时候,将分组之后的数据拼接起来。


还是以手机号为例:


18200256007,按每3位为一组,进行拆分,拆分后的字符串为:182,820,200,002,025,256,560,600,007,这9组数据。


分组之后,加密之后,用逗号分割之后拼接成这样的数据:,U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB,U2FsdGVkX1+qysCDyVMm/aYXMRpCEmBD,U2FsdGVkX19oXuv8m4ZAjz+AGhfXlsQk,U2FsdGVkX19VFs60R26BLFzv5nDZX40U,U2FsdGVkX19XPO0by9pVw4GKnGI3Z5Zs,U2FsdGVkX1/FIIaYpHlIlrngIYEnuwlM,U2FsdGVkX19s6WTtqngdAM9sgo5xKvld,U2FsdGVkX19PmLyjtuOpsMYKe2pmf+XW,U2FsdGVkX1+cJ/qussMgdPQq3WGdp16Q。


以后可以直接通过sql模糊查询字段encrypt_phone了:


select id,name,phone
from user where encrypt_phone like '%U2FsdGVkX19Se8cEpSLVGTkLw/yiNhcB%'
limit 0,20;

注意这里的encrypt_value用的like


这里为什么要用逗号分割呢?


答:是为了防止直接字符串拼接,在极端情况下,两个分组的数据,原本都不满足模糊搜索条件,但拼接在一起,却有一部分满足条件的情况发生。


当然你也可以根据实际情况,将逗号改成其他的特殊字符。


此外,其他的用户隐私字段,如果要实现模糊查询功能,也可以使用类似的方案。


最后说一句,虽说本文介绍了多种加密手机号实现模糊查询功能的方案,但我们要根据实际业务场景来选择,没有最好的方案,只有最合适的。


作者:苏三说技术
来源:juejin.cn/post/7288963208408563773
收起阅读 »

为什么说程序员到国企就废了?

事实上,单纯技术上来说,确实程序员进国企就废了。虽然我也是在央企,但是这确实是一件很无奈也很真实的事情,广大想来国企的小伙伴们也做好心理准备。 倒不是因为什么技术栈老旧这种广泛传播老掉牙的原因,而是一种工作气氛、工作压力、项目规模和大家吐槽最多的”工作压力“方...
继续阅读 »

事实上,单纯技术上来说,确实程序员进国企就废了。虽然我也是在央企,但是这确实是一件很无奈也很真实的事情,广大想来国企的小伙伴们也做好心理准备。


倒不是因为什么技术栈老旧这种广泛传播老掉牙的原因,而是一种工作气氛、工作压力、项目规模和大家吐槽最多的”工作压力“方面的原因。


首先,我本身互联网和央企都呆过,对于两者的工作环境和工作模式等是比较有发言权的。


由于互联网大多是to C项目,面向的是广大用户,是非常有竞争压力的,因为市场上同类产品的数目很多,如果你的代码出现了bug,那么影响到的可就是成千上万的普通用户。


就像微博崩溃了,在整个中国至少几亿人知道,那么你产生一个bug的成本是极大的,因此整体上来说互联网对代码质量的要求就非常高。


相对应来说的就是程序员个人的压力会极大,你必须确保你写的代码的严谨性,不能出现哪怕任何一点儿线上问题,否则等待你的可能就是辞退和担责。


同样也是上面的原因,由于你写的每一行代码都是需要经历成千上万人的使用来检验的,你和你的同时在写每一行代码的时候考虑的场景和风险就会更加全面,而不是简单的curd来完成业务即可。


并且,软件的复杂程度也是与软件的用户使用数量成正比的,使用的用户量越大,你的软件复杂性越高,你所需要解决的问题就越多,技术上涉及到的深度也更深,所以你在互联网企业中编程能力提升得会更快。


但但但 。。。是,并不是呆在互联网的所有程序员能力上都会有提升的,前面我说的这些都是互联网中的那群写核心代码的程序员,在别人框架上修修改改干些搬砖的活儿的人不在其中。


因为,大多数的大厂都有着自己的技术建设团队,会设计一堆自己内部用的工具,哪怕这种工具市面上已经有了也要自己造。长期在这种环境下,干使用别人框架的活儿,但是不继续学习承担更重要工作的人也是挺惨的,而且大概率会被大厂所淘汰。


但是国企的逻辑跟上面互联网的底层逻辑完全不同,互联网的项目大多数要拿到市场上去经历残酷的厮杀,只有做得最好的产品才能够最终活下来,获得垄断地位。


而国企的软件项目一般是一些集团的内部项目,或者有一些作为乙方为其它公司开发的项目也是常年合作下来的项目。


就如同中石油、中国五矿这样的集团内部的智慧化建设项目,本质上是不存在竞争的,因为即使我的软件开发部分可以外包出去或者找外包人员来做,但是这个软件必须得是你们集团牵头来做的。


包括版权和数据什么的最终必须属于你们的集团,而且大领导也热衷于将企业整体的智慧化建设作为工作成果向上汇报 。


所以这也就决定了,国企很多的软件是不太会面对竞争问题的,比如你很难想象让一个私企来做油田的智慧化管理软件。


因此,目前来说石油大体上还是垄断的,油田建设也只有中石油、中石化集团的相关公司可以进行建设,配套的软件你让私企自己来做个产品也是空中楼阁,做出来的东西也不一定能用。


但是,正是由于软件项目没有生存压力,并且国企的正式员工只要不犯下原则性问题,如违法犯罪,泄露公司机密、造成巨大的生产事故之类的重大问题,一般也是不会被轻易开除的,但是也可能存在不续约的情况发生。


因此,呆在国企的程序员就像温室里面的花朵一样,没有动力去优化问题,去采用新的技术,因为你代码写得再好也并不会给你更多的晋升机会,所以长期下去国企程序员的技术上确实就是惨不忍睹了。


其实,我刚从互联网大厂那边跳过来的时候,心理还极度不平衡,倒不是因为国企工资给得有多么低,而是我一直耿耿于怀的是我呆的那个大厂给的钱真的太少了。


但是,当我今年和一些工作好多年的成都普通程序员交流之后,我才发现了我作为校招生进入大厂之后整个人的狂妄与无知。


前一段时间,很多人看了我分享的去5A级景区写代码的blog之后,很多人都找到我问我们公司是否还有HC,是否还能够内推,其中一个哥们令我印象深刻。


他是西南某一本毕业的本科生,从学校毕业现在已经3~4年,也是Java后台开发,一直也是在一些中小型企业里面打转转。


和他聊的时候,才发现毕业短短3~4年他已经换过4~5份工作了,而且工资才渐渐的从当初刚毕业的几千块涨到现在的1w左右。


所以,怎么说呢,对于广大普通程序员来说,近几年的主题是活下来,如果这个时候能够有个地方能让你待到春暖花开的时候,也未尝不是件好事情。


至于技术,废了就废了吧,本质上来说job和career是两回事儿,job也就是“just over break”,每一个打工人,无论在国企还是私企都必须开启自己的”career“,而不是一直打工下去,这就是我的人生信条。


作者:浣熊say
来源:juejin.cn/post/7327724945761452042
收起阅读 »

uniapp云开发--微信登录

web
前言 我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。 小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。 注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面...
继续阅读 »

前言


我们主要使用uniapp + uniCloud 实现小程序登录,并将用户数据存入云数据库。


小程序 wx.getUserProfile 调整,接口将被收回 详情,所以需要用户自己填写资料。


注:填写个人资料是一个组件,覆盖在登录之上而已,还是在同一个页面



uniCloud


创建 uniapp + uniCloud 项目,创建云数据库 数据表 uniCloud传送门


开始


创建项目


39d23acf47b440e2880f5ccadc1417f9~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


关联云服务空间




创建云数据库 数据表


不使用模版,输入名称直接创建即可。



编辑表结构,想了解更多可以去看云数据库 DB Schema 数据结构文档 传送门


{
"bsonType": "object",
"required": [],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"nickName": {
"bsonType": "string",
"label": "昵称",
"description": "用户昵称,登录获取的"
},
"avatarUrl": {
"bsonType": "string",
"label": "头像",
"description": "用户头像图片的 URL,登录获取的"
},
"gender": {
"bsonType": "number",
"label": "性别",
"description": "用户性别,1: 男;2: 女"
},
"personalize": {
"bsonType": "string",
"label": "个性签名",
"description": "个性签名,编辑资料获取"
},
"background": {
"bsonType": "object",
"label": "个人中心背景图",
"description": "个人中心背景图,编辑资料获取"
},
"mp_wx_openid": {
"bsonType": "string",
"description": "微信小程序平台openid"
},
"register_date": {
"bsonType": "timestamp",
"description": "注册时间",
"forceDefaultValue": {
"$env": "now"
}
}
}
}

创建云函数




云函数代码


云函数 将 uni.login 取得的 code 获取到用户 session, 并对 数据库进行 增加、修改、查询 操作,第一次注册必须用户主动填写用户资料。


对云数据库的相关操作 传送门


'use strict';

//小程序的AppID 和 AppSecret
const mp_wx_data = {AppID: '************', AppSecret: '***********************'}

//event为客户端上传的参数
exports.main = async (event, context) => {

//使用云数据库
const db = uniCloud.database();
// 获取 `users` 集合的引用
const pro_user = db.collection('users');
// 通过 action 判断请求对象

let result = {};
switch (event.action) {
// 通过 code 获取用户 session
case 'code2Session':
const res_session = await uniCloud.httpclient.request('https://api.weixin.qq.com/sns/jscode2session', {
method: 'GET', data: {
appid: mp_wx_data.AppID,
secret: mp_wx_data.AppSecret,
js_code: event.js_code,
grant_type: 'authorization_code'
}, dataType: 'json'
}
)
const success = res_session.status === 200 && res_session.data && res_session.data.openid
if (!success) {
return {
status: -2, msg: '从微信获取登录信息失败'
}
}

//从数据库查找是否已注册过
const res_user = await pro_user.where({
mp_wx_openid: res_session.data.openid
}).get()
// 没有用户信息,进入注册
if (res_user.data && res_user.data.length === 0) {
//event.user_info 用户信息
if (event.user_info) {
//有信息则进入注册,向数据库写入数据
const register = await uniCloud.callFunction({
name: 'user',
data: {
action: 'register',
open_id: res_session.data.openid,
user_info: event.user_info
}
}).then(res => {
result = res
})
} else {
//没有信息返回{register: true}
result = {
result: {
result: {register: true}
}
}
}
} else {
result = {
result: {
result: res_user.data[0]
}
}
}
break;
//注册 向数据库写入数据
case 'register':
const res_reg = await pro_user.add({
nickName: event.user_info.nickName,
avatarUrl: event.user_info.avatarUrl,
gender: event.user_info.gender,
mp_wx_openid: event.open_id,
register_date: new Date().getTime()
})
if (res_reg.id) {
const res_reg_val = await uniCloud.callFunction({
name: 'user', data: {
action: 'getUser', open_id: event.open_id
}
}).then(res => {
result = res
})
} else {
result = {
status: -1, msg: '微信登录'
}
}
break;
case 'update':
if (event._id && event.info) {
const res_update = await pro_user.doc(event._id).update(event.info)
if (res_update.updated >= 0) {
result = {status: 200, msg: '修改成功'}
} else {
result = {status: -1, msg: '修改失败'}
}
} else {
result = {status: -1, msg: '修改失败'}
}
break;
case 'getUser':
const res_val = await pro_user.where({
mp_wx_openid: event.open_id
}).get()
return res_val.data[0]
break;
}
return result;
};

微信登录操作


如上面所说,用户需手动上传资料,对于用户头像我们需要上传至云储存。


上传用户头像


上传图片函数参数为微信本地图片路径,我们对路径用/进行分割,取最后的图片名称进行上传


/**
* 上传图片至云存储
*/

export async function uploadImage(url) {
const fileName = url.split('/')
return new Promise(resolve => {
uniCloud.uploadFile({
filePath: url,
cloudPath: fileName[fileName.length - 1],
success(res) {
resolve(res)
},
fail() {
uni.showToast({
title: '图片上传失败!',
icon: 'none'
})
resolve(false)
}
})
})
}

登录函数


如果用户第一次上传资料,我们需要先上传头像并取得图片链接,再将用户资料写入数据库。


async wxLogin() {
if (this.userInfo && this.userInfo.avatarUrl) {
uni.showLoading({
title: '正在上传图片...',
mask: true
});
//上传头像至云储存并返回图片链接
const imageUrl = await uploadImage(this.userInfo.avatarUrl)
if (!imageUrl) {
return
}
this.userInfo = {...this.userInfo, avatarUrl: imageUrl.fileID}
}
uni.showLoading({
title: '登陆中...',
mask: true
});
const _this = this
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code) {
//取得code并调用云函数
uniCloud.callFunction({
name: 'user',
data: {
action: 'code2Session',
js_code: res.code,
user_info: _this.userInfo
},
success: (res) => {
//如register为true,用户未填写资料
if (res.result.result.result.register) {
//_this.showUserInfo 显示填写资料组件
_this.showUserInfo = true
uni.hideLoading();
return
}
if (res.result.result.result._id) {
const data = {
_id: res.result.result.result._id,
mp_wx_openid: res.result.result.result.mp_wx_openid,
register_date: res.result.result.result.register_date
}
this.loginSuccess(data)
}
},
fail: () => {
this.loginFail()
}
})
}
}
})
},

登录成功与失败


在用户登录成功后将数据存入 Storage 中,添加登录过期时间,我这里设置的是七天的登录有效期。


loginSuccess(data) {
updateTokenStorage(data)
updateIsLoginStorage(true)
uni.showToast({
title: '登陆成功!',
icon: 'none'
});
uni.navigateBack()
},

将用户数据存入 Storage,并设置过期时间 expiresTime


export function updateTokenStorage(data = null) {
if (data) {
const expiresTime = new Date().getTime() + 7 * 24 * 60 * 60 * 1000
data = {...data, expiresTime: expiresTime}
}
uni.setStorageSync('user', data)
}

isLogin 用于判断是否是否登录


export function updateIsLoginStorage(data = null) {
uni.setStorageSync('isLogin', data)
}

登录失败


loginFail() {
updateTokenStorage()
updateIsLoginStorage()
uni.showToast({
title: '登陆失败!',
icon: 'none'
});
}

判断是否登录


除了判断 isLogin 还要判断 expiresTime 是否登录过期


//判断是否登陆
export function isLogin() {
try {
const user = uni.getStorageSync('user')
const isLogin = uni.getStorageSync('isLogin')
const nowTime = new Date().getTime()
return !!(isLogin && user && user._id && user.expiresTime > nowTime);
} catch (error) {

}
}

最后


至此就实现了微信登录并将用户信息存入数据库中,我们也可以通过云函数获取用户数据,做出用户个人主页。



以上是我做个人小程序时用的登录流程,整个小程序项目已上传至 GitHub。


GitHub地址


小程序码



作者:Biao
来源:juejin.cn/post/7264592481592705076
收起阅读 »

真的不考虑下grid布局?有时候真的很方便!

web
前言 flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。 宫格类的布局 比如...
继续阅读 »

前言


flex布局大家应该已经运用的炉火纯青了,相信在日常开发中大家和我一样不管遇到什么都是flex一把搜哈。直到我遇到grid,才发现有些场景下,不是说flex实现不了而是使用grid能够更加轻松的完成任务。下面拿几个场景和大家分享一下。


宫格类的布局


比如我要实现一个布局,最外层元素的宽度是1000px,高度自适应。子元素宽度为300px,一行展示3个,从左到右排列。其中最左边与最右边的元素需要紧挨父元素的左右边框。如下图所示:



使用flex实现


这个页面布局在日常开发中非常常见,考虑下使用flex布局如何实现,横向排列元素,固定宽度300,wrap设置换行显示,设置双端对齐。看起来很简单,来实现一下。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box{
width: 1000px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
}
.item{
background: pink;
width: 300px;
height: 150px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>


实现之后发现了问题,由于我们设置了双端对齐导致,当最后一行的个数不足三个时,页面展示的效果和我们预期的效果有出入。使用flex实现这个效果就要对这个问题进行额外的处理。


处理的方式有很多种,最常见的处理方式是在元素后面添加空元素,使其成为3的倍数即可。其实这里添加空元素的个数没有限制,因为空元素不会展示到页面上,即使添加100个空元素用户也是感知不到的。个人觉得这并不是一个好办法,在实际处理的时候可能还会遇到别的问题。个人觉得还是把flex下的子元素设置成百分比好一点。


使用grid实现


面对这种布局使用grid是非常方便的,设置3列,每列300px,剩下的元素让它自己往下排即可。几行代码轻松实现该效果,不需要flex那样额外的处理。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(3, 300px);
justify-content: space-between;
gap: 10px;
width: 1000px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>

</div>
</body>

</html>


实现后台管理布局



这种后台管理的布局,使用flex实现当然也没有问题。首先需要纵向排列红色的两个div,然后再横向的排列蓝色的两个div,最后再纵向的排列绿色的两个div实现布局。达到效果是没有问题的,但是实现起来较为繁琐,而且需要很多额外的标签嵌套。



由于grid是二维的,所以它不需要额外的标签嵌套。html里面结构清晰,如果需要改变页面结构,只需要改变container的样式就可以了,不需要对html进行修改。


<!DOCTYPE html>
<html lang="en">

<head>
<style>
.container {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 100px 1fr 100px;
grid-template-areas:
'header header'
'aside main'
'aside footer';
height: 100vh;
}

.header {
grid-area: header;
background: #b3c0d1;
}

.aside {
grid-area: aside;
background: #d3dce6;
}

.main {
grid-area: main;
background: #e9eef3;
}

.footer {
grid-area: footer;
background: #b3c0d1;
}
</style>
</head>

<body>
<div class="container">
<div class="header">Header</div>
<div class="aside">Aside</div>
<div class="main">Main</div>
<div class="footer">Footer</div>
</div>
</body>

</html>

实现响应式布局


借助grid的auto-fillminmax函数可以实现类似响应式布局的效果,可以应用在后台管理的表单布局等场景。



<!DOCTYPE html>
<html lang="en">

<head>
<style>
.box {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
justify-content: space-between;
gap: 10px;
}

.item {
background: pink;
height: 100px;
}
</style>
</head>

<body>
<div class="box">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
</div>
</body>

</html>

兼容性对比


flex的兼容性


image.png


grid的兼容性


image.png


可以看到grid在兼容性上还是不如flex,grid虽然强大,但是在使用前还是需要先考虑一下项目的用户群体。


结尾


除了上述场景外肯定还有许多场景适合使用grid来完成。gridflex都是强大的布局方式,它们并没有明显的优劣之分。关键在于掌握这两种方法,并在开发中根据实际情况选择最合适的方案。


希望大家能有所收获!


作者:欲买炸鸡同载可乐
来源:juejin.cn/post/7326816030042669110
收起阅读 »

一些不被人熟知,但又很好用的HTML属性

web
HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。 下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性 contenteditable: 这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的...
继续阅读 »

HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。
下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性


contenteditable:


这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的内容。


<div contenteditable="true">
这段内容可以被编辑。
</div>

使用场景:
-可以用来创建富文本编辑器,使用户能够在网页中创建、编辑和格式化文本,


spellcheck:


该属性用于启用或禁用元素的拼写检查功能。(如果用户输入的单词拼写有误,浏览器通常会标记出来并提供纠正建议)


<textarea spellcheck="true">
这个文本区域启用了拼写检查。
</textarea>

image.png


使用场景:



  • 可以在文章创作者的富文本编辑器中使用,辅助文章创作


代码演示:


draggable:


该属性使元素可拖动。通常与 JavaScript 结合使用,实现拖放功能。


<img src="image.jpg" draggable="true" alt="可拖动的图片">

使用场景:



  • 在电子商务网站中,用户可以拖动产品图像到购物车区域,以便快速添加商品到购物清单。

  • 在可视化数据分析工具中,用户可以通过拖拽图表或数据元素来定制自己的数据可视化图形。

  • 可以创建一个可拖放的低代码平台


代码演示:


sandbox:


与 元素一起使用,sandbox 属性限制了嵌入内容的行为,如阻止执行脚本或提交表单。

<iframe src="sandboxed-page.html" sandbox="allow-same-origin allow-scripts"></iframe>

使用场景:



  • 可以在电子邮件客户端中,通过使用 sandbox 属性限制电子邮件中嵌入内容的行为,以确保安全性并防止恶意代码执行。

  • 可以在需要嵌入第三方内容(如广告、外部应用程序等)但又需要限制其行为的情况下使用。这可以防止嵌入的内容执行恶意脚本或访问敏感信息。


download:


该属性与 <a>(锚点)元素一起使用,指定用户单击链接时应下载的目标。


<a href="document.pdf" download="my-document">下载 PDF</a>

使用场景:



  • 可用于提供下载链接,例如下载文档、图像或其他文件。这使得用户可以通过单击链接直接下载相关内容而无需离开页面。


hidden:


该属性用于隐藏页面上的元素。这是最初隐藏内容的简单方法,可以通过 CSS 或 JavaScript 在后来显示。


<p hidden>这个段落最初是隐藏的。</p>

使用场景:



  • 在网页中使用弹出式模态框或折叠式面板,可以利用 hidden 属性来最初隐藏它们,并在用户点击或触发特定事件时展现。

  • 在网页表单验证中,可以将错误消息初始隐藏,只有当用户提交表单出现错误时才显示出来。


defer:



<script defer src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


async:


类似于 defer,async 属性与

<script async src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


Accept 属性:


你可以将 accept 属性与 元素(仅适用于文件类型)一起使用,以指定服务器可以接受的文件类型。


<input type="file" accept=".jpg, .jpeg, .png">

使用场景:



  • 在上传图片的社交媒体平台中,限制用户只能上传特定格式(如 JPG、PNG)的图片文件,确保图片质量和页面加载速度。

  • 在在线应用程序中,限制用户只能上传特定类型的文件,例如在云存储服务中只允许上传文档文件。


Translate:


该属性用于指定在页面本地化时,元素的内容是否应该被翻译。


<p translate="no">这段内容不应被翻译。</p>

作者:zayyo
来源:juejin.cn/post/7303789262989443083
收起阅读 »

Celeris Web,一套女生都觉得好看的Vue3模板

web
Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板 一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的? 嗯,女生总是很喜欢漂亮的东...
继续阅读 »

Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板


一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的?


嗯,女生总是很喜欢漂亮的东西,对吧?于是我决定写一款前端开发模板,让开发出来的工具她们用起来不仅方便,还得有点美美哒。Vue 3、Unocss、NaiveUI、Monorepo,这些都是我的秘密武器。我取名它为Celeris Web


这个开发框架采用了最新的技术,包括Vue 3、Vite和 TypeScript。而且,这个项目的设计初衷就用了monorepo的方法使得依赖管理和多个项目的协作变得轻松。这可是一套为开发人员提供了构建现代Web应用程序的全面解决方案哦。


不管你是老手还是新手,Celeris Web都能给你提供一个简化的前端开发流程,利用最新的工具和技术。是不是觉得很吸引人?


Snipaste_2024-01-16_14-27-03.png


Celeris Web的特点



  • ⚡ 闪电般快速:使用Vue 3,Vite和pnpm构建 🔥

  • 💪 强类型:使用TypeScript 💻

  • 📂 单库存储:易于管理依赖项和协作多个项目 🤝

  • 🔥 最新语法:使用新的< script setup >语法 🆕

  • 📦 自动导入组件:自动导入组件 🚚

  • 📥 自动导入API:使用unplugin-auto-import直接导入Composition API和其他API 📨

  • 💡 官方路由器:使用Vue Router v4 🛣️

  • 🎉 加载反馈:使用NProgress提供页面加载进度反馈 🔄

  • 🍍 状态管理:使用Pinia进行状态管理 🗃️

  • 📜 中文字体预设:包含中文字体预设 🇨🇳

  • 🌍 国际化就绪:具备使用本地化的国际化功能 🌎

  • ☁️ Netlify准备就绪:在Netlify上零配置部署 ☁️


有了Celeris Web,你的前端开发之路将更加轻松愉快!🚀


中英文双语注释


在Celeris Web的设计中,我们注重代码的可读性和学习性,为此,我们为每个函数都配备了中英文双语注释,以确保无论您的母语是中文还是英文,都能轻松理解和学习代码。


为什么选择中英文双语注释?



  1. 全球协作: 在多语言团队中,中英文双语注释能够促进更好的沟通和协作,确保团队成员都能准确理解代码的功能和实现。

  2. 学习便捷: 对于新手来说,中英文双语注释提供了更友好的学习环境,帮助他们更快速地掌握代码的逻辑和结构。

  3. 开发者友好: 我们致力于构建一个开发者友好的开发环境,中英文双语注释是我们为实现这一目标而采取的一项关键措施。

  4. 示例:


    /**
    * 打开一个新的浏览器窗口
    * Open a new browser window
    *
    * @param {string} url - 要在新窗口中打开的 URL
    * The URL to open in the new window
    *
    * @param {object} options - 打开窗口的选项
    * Options for opening the window
    * @param {string} options.target - 新窗口的名称或特殊选项,默认为 "_blank"
    * @param {string} options.features - 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    */

    export function openWindow(url: string, { target = "_blank", features = "noopener=yes,noreferrer=yes" }: {
    target?: "_blank" | "_self" | "_parent" | "_top"; // 新窗口的名称或特殊选项,默认为 "_blank"
    features?: string; // 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    } = {}
    ) {
    window.open(url, target, features);
    }

    通过这样的中英文双语注释,我们希望为开发者提供更愉悦、更高效的编码体验,让Celeris Web成为一个真正容易上手和深入学习的前端模板。



Monorepo 设计的好处


1. 依赖管理更轻松: Monorepo 将所有项目的依赖项集中管理,避免了不同项目之间版本冲突的问题,使得整体的依赖管理更加清晰和简便。


2. 代码共享与重用: 不同项目之间可以方便地共享和重用代码,减少重复开发的工作量。这对于保持代码一致性和提高开发效率非常有利。


3. 统一的构建和部署: Monorepo 可以通过统一的构建和部署流程,简化整个开发过程,减少了配置和管理的复杂性,提高了开发团队的协作效率。


4. 统一的版本控制: 所有项目都在同一个版本控制仓库中,使得版本管理更加一致和可控。这有助于团队协同开发时更好地追踪和处理版本问题。 Monorepo设计让Celeris Web不仅是一款后台管理系统模板,同时也是一个快速开发C端产品的前端Web模板。有了Celeris Web,前端开发之路将更加轻松愉快!🚀


设计理念:突破Admin管理的局限性,关注C端用户体验


在市面上,大多数前端模板都着眼于满足B端用户的需求,为企业管理系统(Admin)提供了强大的功能和灵活的界面。然而,很少有模板将C端产品的特点纳入设计考虑,这正是我们Celeris Web的创新之处。


突破Admin管理的局限性:


传统的Admin管理系统更注重数据展示和业务管理,但C端产品更加侧重用户体验和视觉吸引力。我们深知C端用户对于界面美观、交互流畅的要求,因此Celeris Web不仅提供了强大的后台管理功能,更注重让前端界面在用户层面上达到更高水平。


关注C端用户体验:


Celeris Web不仅仅是一个后台管理系统的模板,更是一个注重C端用户体验的前端Web模板。我们致力于打破传统Admin系统的束缚,通过引入崭新的设计理念,使得C端产品在前端呈现上具备更为出色的用户体验。


特色亮点:



  • 时尚美观的UI设计: 我们注重界面的美感,采用现代化设计语言,使得Celeris Web的UI不仅仅是功能的堆砌,更是一种视觉盛宴,让C端用户爱不释手。

  • 用户友好的交互体验: 考虑到C端用户的习惯和需求,Celeris Web注重交互体验的设计,通过流畅的动画效果和直观的操作,使用户感受到前所未有的愉悦和便捷。

  • 个性化定制的主题支持: 我们理解C端产品的多样性,因此提供了丰富的主题定制选项,让每个C端项目都能拥有独一无二的外观,更好地满足产品个性化的需求。


通过这一独特的设计理念,Celeris Web致力于在前端开发领域探索全新的可能性,为C端产品注入更多活力和创意。我们相信,这样的创新将带来更广泛的用户认可和更高的产品价值。在Celeris Web的世界里,前端不再局限于Admin系统,而是融入了更多关于用户体验的精彩元素。


后期发展路线:瞄准AIGC,引领互联网产品变革


随着人工智能与图形计算(AIGC)技术的崛起,我们决定将Celeris Web的发展方向更加专注于推动AIGC相关产品的研发和落地。这一战略决策旨在顺应互联网产品的变革浪潮,为未来的科技创新开辟全新的可能性。


AIGC技术引领变革:


AIGC的兴起标志着互联网产业迎来了一场技术变革,为产品带来更加智能、交互性更强的体验。Celeris Web将积极响应这一变革,致力于为开发者提供更优秀的工具,助力他们在AIGC领域创造更具前瞻性的产品。


模板的研发重心:


在后期的发展中,Celeris Web将更加重视AIGC相关产品的研发需求。我们将推出更多针对人工智能的功能模块,使开发者能够更便捷、高效地构建出色的AIGC应用。


专注产品落地:


除了技术研发,我们将加强对AIGC产品落地的支持。通过提供详实的文档、示例和定制化服务,Celeris Web旨在帮助开发者更好地将AIGC技术融入他们的实际项目中,实现技术创新与商业应用的有机结合。


开放合作生态:


为了推动AIGC技术的更广泛应用,Celeris Web将积极构建开放合作生态。与行业内优秀的AIGC技术提供商、开发者社区保持密切合作,共同推动AIGC技术的发展,携手打造更加繁荣的互联网产品生态圈。


Celeris Web未来的发展将以AIGC为核心,我们期待在这个快速发展的技术领域中,与开发者们一同探索、创新,共同引领互联网产品的未来。通过持续的努力和创新,Celeris Web将成为AIGC领域的引领者,助力开发者创造更加智能、引人入胜的互联网产品。


源码


kirklin/celeris-web (github.com)


作者:KirkLin
来源:juejin.cn/post/7324334380373688371
收起阅读 »

4天卖600万份的爆款游戏《幻兽帕鲁》,真的是AI缝合怪吗

AI一天,人间一年。我是卷福同学,一个在福报厂修过福报的程序员。现在专注AI领域整活分享 大家好啊,最近2天,Steam上出了一款非常火的游戏《幻兽帕鲁》。到今天1月24日,才过去4天。它就卖出超过600万份,最高时180万人同时在线,直接登上Steam的热...
继续阅读 »

AI一天,人间一年。我是卷福同学,一个在福报厂修过福报的程序员。现在专注AI领域整活分享



大家好啊,最近2天,Steam上出了一款非常火的游戏《幻兽帕鲁》。到今天1月24日,才过去4天。它就卖出超过600万份,最高时180万人同时在线,直接登上Steam的热销游戏和最热游戏榜首。


1.png


这个成绩,放在Steam游戏史上甚至赶超了前段时间特火的《完蛋!我被美女包围了!》


同时,在玩家近7万的评测里,高达93%的评价都是好评


2.png


到底是一款什么样的游戏能拿到这样的成绩呢?


游戏内容


0.png


游戏世界是类似《塞尔达传说 旷野之息》那样的开发探索世界,玩家可以在庞大的世界里收集各种各样的幻兽,而幻兽借鉴了任天堂《宝可梦》这个大IP中的神奇生物系统,通过AI缝合而成的。


玩家们可以在游戏里找到各种熟悉的宝可梦的影子。


4.png


5.png


在游戏里,玩家可以进行开放世界探索、宝可梦式抓幻兽、第三人称射击战斗、生存建造房屋、养成宠物等各种玩法,不同的玩家都能在里面找到属于自己的乐趣。


6.png


7.png


现在已有小学生玩家体会到了在游戏当老板,压榨帕鲁的乐趣,还总结出一套帕鲁圣经:



缝合怪,但是全缝了


让人难以想象的是,这样一款现象级的爆款游戏在项目开始时只有10人,由小作坊Pocketpair开发。而这10个人也不是专业开发游戏的,而是Pocketpair的社长在网上发掘的野生的零经验的爱好者。


相关信息可以在社长Takuro Mizobe的推特上找到,置顶是1月16日社长写给玩家们的公开信。从信中,可以找到游戏开发设计过程中的很多细节


1.游戏里的枪械动作是社长在网上找的一个爱好者做的


9.png


(PS:没事咱也上传自己做手工的视频,说不定哪天就被伯乐挖掘了)


2.帕鲁的美术师在最初在推特上应聘时被拒绝,而且出图速度惊人


美术师是个应届生,曾应聘过上百家公司,都被拒绝了。社长表示,她是一个罕见的人才,出图速度是其他原画师的四五倍(注意这句),也因为有了这位美术师的加入,现在的游戏里才有了100种帕鲁。


10.png


11.png


另外给大家说一下熟知的《怪物猎人世界》游戏里的怪物类型也才50种。


如此惊人的出图速度,以及反馈修改,一分钟内就能修改完成,很难让人不怀疑其中有AI的参与。


社长Takuro Mizobe自身就是生成式AI的拥护者,早期在推特上就有分享过用AI制作游戏的动态。几乎可以实锤AI缝合怪的传闻了!


如何玩《幻兽帕鲁》


游戏在Steam上上架的,需要先安装Steam,然后游戏售价现在有优惠,是168港币,折合人民币为152元钱。冲着这几天爆火的程度,还是值得入手玩一玩的。


12.png


但是因为游戏实在太火爆了,官方服务器已经支撑不了这么多玩家了。好在官方提供了自建游戏服务器的方法,也就是你可以在云服务器上,甚至自己电脑上搭建幻兽帕鲁的服务器,然后游戏客户端登录就行。


甚至可以搭建个局域网服务器,约着几个好朋友一起在游戏世界里探索。


13.png


小卷已经给大家整理了云服务器部署幻兽帕鲁服务端的教程。在我的公众号内发关键词幻兽帕鲁领取


作者:卷福同学
来源:juejin.cn/post/7327538517528772618
收起阅读 »

揭秘 "mitt" 源码:为什么作者钟情于 `map` 而放弃 `forEach`

web
故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach, 而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. ...
继续阅读 »

故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach


而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. (使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


咦?为什么会这样呢?


"mitt" 简介


首先,让我们认识一下 "mitt",它是一只小巧灵活的事件发射器(event emitter)库,体积仅有 200 字节,但功能强大。这个小家伙在项目中充当了事件的传播者,有点像是一个小型的邮差,把消息传递给需要它的地方。


developit/mitt: 🥊 Tiny 200 byte functional event emitter / pubsub. (github.com)


作者的选择:map vs forEach


在源码中,我们发现作者选择使用了 Array.prototype.map(),这是一个处理数组每个元素并返回新数组的函数。然而,有趣的地方在于,作者并没有在 map 中返回任何值。这和我对 map 的期望有些出入,因为我们习惯于用它生成一个新的数组。


代码的细微变化


曾经,代码片段是这样的,作者想要用 map 来执行一些操作,但却不生成新数组。


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.map((handler) => {
handler(evt!);
});
}

我希望修改成这样:


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.forEach((handler) => {
handler(evt!);
});
}

所以我很快就交了个PR:将map改成了forEach,经过了几个月的等待,PR被拒了,作者的回应是:map() is used because it is 3 bytes smaller when gzipped.(使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


code.png


pr.png


小技巧背后的逻辑


虽然 map 通常用于生成新数组,但作者在这里使用它更像是在借助压缩的优势,让代码更轻量。


大小对比


通过实验验证,使用 map 的打包大小确实稍微小一些:



  • 使用 map 时,打包大小为:


  - 190 B: mitt.js.gz
- 162 B: mitt.js.br
- 189 B: mitt.mjs.gz
- 160 B: mitt.mjs.br
- 268 B: mitt.umd.js.gz
- 228 B: mitt.umd.js.br


  • 而使用 forEach 后,打包大小为:


  - 192 B: mitt.js.gz
- 164 B: mitt.js.br
- 191 B: mitt.mjs.gz
- 162 B: mitt.mjs.br
- 270 B: mitt.umd.js.gz
- 230 B: mitt.umd.js.br

进一步实验


为了深入了解选择的影响,我又进行了一个实验。有趣的是,当我将代码中的一处使用 map 改为 forEach,而另一处保持不变时,结果居然是打包体积更大了。


experiment_results.png


总结


这个故事让我不仅仅关注于代码表面,还开始注重微小选择可能带来的影响。学到了很多平时容易忽略的点,"mitt" 作者的选择展现了在开发中面对权衡时的智慧,通过选择不同的API,以轻松的方式达到减小代码体积的目标。在编写代码时,无处不充满着权衡的乐趣。


如果你对这个故事有更多的想法或者其他技术话题感兴趣,随时和我分享哦!


作者:KirkLin
来源:juejin.cn/post/7327424955037564965
收起阅读 »

使用pixi.js开发一个智慧路口(车辆轨迹追踪)项目

web
项目效果 项目功能: 位置更新、航向角计算。 debug模式。 位置角度线性补帧。 变道、转弯、碰撞检测。 mock轨迹数据 图片效果: 视频效果: 项目启动 项目地址 github:(github.com/huoguozhang…) 线上:todo...
继续阅读 »

项目效果


项目功能:



  • 位置更新、航向角计算。

  • debug模式。

  • 位置角度线性补帧。

  • 变道、转弯、碰撞检测。

  • mock轨迹数据


图片效果:


result.gif


视频效果:



项目启动


项目地址



(如果觉得项目对你有帮助的话, 可以给我一个star 和 赞,❤️)


启动demo项目



  1. cd car-tracking-2d/demos/react-demo

  2. yarn

  3. yarn start


界面使用


debug 模式


浏览器url ?后面(search部分)加入参数debug=1


例如:http://localhost:3000/home?tunnelNo=tunnel1&debug=1


将会展示调试信息:


image.png


如图:车旁边的白色文字信息为debug模式才会展示的内容(由上到下为:里程、车id、车道id、[x,y]、旋转角度)


实现:


技术栈:


ts+pixi.js+任意前端框架


(前端框架使用vuereact或者其他框架都可以。只需要在mounted阶段,实例化我们暴露出来class即可。然后在destroyed或者unmounted阶段destory示例即可,后面会提到。)


pixi.js


官网介绍:



Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.



pixi.js是一个2D的WebGL的渲染库。但是没有three.js知名度高。一个原因是,我们2D的需求技术路线很多,可以是dom、svg、canvas draw api等,包括本项目也可以使用其他技术方案实现,希望通过本文,大家在实现这种频繁更新元素位置的功能,可以考虑一下pixi.js。



API快速讲解

这里只讲我们项目使用到的


Application

import * as PIXI from 'pixi.js';

const app = new PIXI.Application({
view: canvasDom // canvas dom 对象
});


Container

容器,功能为一个组。
当我们设置容器的scale(缩放)、rotation(旋转)、x、y(位置)时。里面的元素都会收到影响。


(ps:app.stage也是一个Container


每个 Container可以通过addChild(增加子节点)、removeChild(删除子节点),也可以设置子元素的zIndex(和css的功能一致)。子原始的scale(缩放)、rotation(旋转)、x、y(位置)是相对于Container的。


Sprite

精灵,渲染图片对象。


carObj = Sprite.from('/car.svg')

Sprite.from(url),url相同的话,只会加载一次图片。纹理对象也只会创建一次。


anchor属性其他对象也有,设置定位点,类似于csstransform-origin


执行下面代码
carObj.anchor.set(0.5, 0.5)


如果x = 10 y =10,carObj的中心点的坐标就是(10,10),旋转原点也是(10,10),缩放也是如此。


Graphics

绘制几何图形,圆弧,曲线、直线等都可以。也支持fill和stroke,canvas draw api支持的,Graphics都支持。


Text

文本,比较简单。字体、颜色、大小,都支持。



  • 值得注意的是文本内容含有换行符时(\n \r),文本会换行。

  • pixi提供测量文本的width height的方法非常好用。


Tick

this.app.ticker.add(() => {})

类似于requestAnimationFrame


具体实现


分三步,vue/react都一样:


1 获取canvas dom通过ref的方式。


2 创建我们封装Stage Road


3 组件销毁时,执行 stage.destroy(注意stage是我封装的,不是pixi的。使用方不需要使用pixi.js的api)


线性插帧

当有一个对象由坐标 点a(0,0)变换到点b(1000,1000),1秒内完成。
中间的变化值为:
dx =1000 dy=1000
记录每帧的时间差t(当前帧距离第0帧的,单位毫秒)


所以第n帧位置信息为(0+dx / 1000 * t, 0+ dy /1000 *t)


角度变换也是这个道理。


位置坐标获取

如果直线长度为1000px,对应的实际里程为100米。


当跑了50米,当前就是直线的中点坐标。
弯道呢,通过弧度可以推算出坐标。
可以把 Road.ts line 70的注释取消。


 // 方便开发观察 绘制车道线 ---begin----
// this.mount(lane.centerLine.paint())

航向角

直线简单,通过Math.atan2可以求出来。
弯道需要通过解析几何,计算出圆弧切线,然后推测出航向角。


转弯

mark.png
可以查看我们标注的一些点


以1到7的弯道举例,相当于是从新创建一次车道,车道的点是车道1和车道7的组合。
我们通过 circle属性配置,在创建Road


{
uid: '1-2',
x: 1072,
y: 1605,
circle: {
// 编号形式 车道序号-第几个点

linkId: '7-3'
}
},

这条信息表示:车道1的第2个点(uid),有圆弧链接到车道7的第3个点(circle.linkId)


碰撞检测

我们这个项目的特点是,前端展示,实际后端返回什么数据,我们就展示什么数据。(一般不需要前端处理)。
这里我们mock的数据就简单处理一下。判断是否存在相交的线段(当前对象的位置和将要到达的点),如果线段相交,车辆暂停移动。


作者:火锅小王子
来源:juejin.cn/post/7327467832866095130
收起阅读 »

微信小程序开发大坑盘点

web
微信小程序开发大坑盘点 起因 前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app...
继续阅读 »

微信小程序开发大坑盘点


起因


前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app 做,但是这玩意太难用所以不了了之了。


于是这次打算正经的用微信自己的那套东西做,结果不出意外的是入了深坑......


大坑


微信小程序云函数外部调用异常


微信小程序提供 wx.request 发起 HTTP 请求,由于微信不是浏览器,没有跨域限制,这方便了很多事,但是由于 wx.request 函数只能对 HTTPS 协议的地址发起请求,而我们学校的教务系统又是清一色的 HTTP,因此我需要一个可以用来帮助我发起 HTTP 请求的转发接口。


对于这种简单需求,云函数显然是最好的解决方案,进而我发现微信小程序自带云函数的支持,于是便兴冲冲地写了一段 NodeJS 代码,放上去跑。


结果我发现不知道为什么,请求其他网站都没问题,唯独请求我们教务系统就会原地超时。经过了几个小时的调试,最后以失败告终,转而改用腾讯云的云函数。


代码也十分简单:


const url = require('url')

const express = require('express');
const app = express()
const port = 9000

const rp = require('request-promise')

app.use(express.json());

app.post('/', async (req, res) => {
const jar = rp.jar()

try {
const response = await rp({
...req.body,
resolveWithFullResponse: true,
simple: false,
jar: jar
})
res.json(response)
} catch (e) {
res.json(e)
console.error(e)
}
})

app.listen(port, () => {
console.log("Successfully loaded")
})

其中额外引入了 request-promise 库(express 是默认引入的,腾讯云函数这里做的不错,对 npm 支持很好)。


然后做了一个模仿 wx.request 调用风格的 request 函数,这样我就可以在 wx.request 和我自己的 request 函数中无缝切换(更进阶的是,我自己写的这个还额外支持了以 Promise 风格调用。


export async function request(data) {
try {
const res = await rp({
...data,
uri: data.url,
headers: data.header,
})
let result = {
...res,
data: res.body,
header: res.headers
}
if (result.statusCode != 200) {
throw {
err_msg: "内部错误"
}
}
if (data.dataType === 'json') {
result.body = JSON.parse(result.body)
}
data.success && data.success(result);
data.complete && data.complete({})
return result;
} catch (e) {
data.fail && data.fail(e)
data.complete && data.complete({})
throw e;
}
}

function rp(data) {
return new Promise((resolve, reject) => {
wx.request({
method: 'POST',
url: 'https://service-abcdefg-123456789.gz.apigw.tencentcs.com/release/',
data: data,
success: (res) => {
resolve(res.data)
},
fail: (err) => {
reject(err)
}
})
})
}

ES6 module 和变量作用域支持差


不知道为什么,微信小程序完全不支持 ES6 module,即使它是支持 ES6 语法的。也就是说,你只能使用这种传统的 CommonJS 方式引入:


const module = require('module.js')

而不是 ES6 的 import 语法:


import module from 'module.js'

最离谱的是,微信小程序这个基于 VSCode 的编译器会给你 warn 这段代码,告知你可以转换为使用 import 导入。



于是这又引出了另外一个奇怪的问题:当你在一个界面的逻辑层文件上声明变量时,IDE 会认为这个变量是一个全局变量,因此在其他界面声明同名变量会得到一个 error,即使不会导致任何编译错误。


这导致了,现在我的模块引入必须用一种很奇怪的写法...


const sessionModule = require('../../utils/session');
const tgcModule = require('../../utils/tgc')
const cryptoModule = require('../../miniprogram_npm/crypto-js/index.js')

奇葩的 NPM 支持


在以前,微信小程序是不支持包管理器的,这也就意味着,你得手动把那些库的 JS 复制到你的项目目录里再引用,非常麻烦。但是现在好了,微信可以自动帮你做这件事了。


没错,是自动帮你复制,而不是做了包管理器支持。


怎么说呢...你需要先在你的项目源代码目录中 init 一个 package.jsonadd 你需要的包然后 install,接下来点击 IDE 顶栏的 Tools - Build npm 选项,Weixin Devtools 就会帮你生成一个 miniprogram_npm文件夹,将每个项目各自 combine 到一个 index.js 然后塞到各自名字的文件夹里,然后,你就能通过上面那种方式手动引入使用了。


很奇葩但是... 勉强能用(而且不限制使用的包管理器,比如我用的就是 yarn)。


避免使用双向绑定


微信小程序的 WXML 存在一个有限的双向绑定支持,也是类似 Vue 的那种语法糖:


<input model:value="{{value}}" />

但是这个双向绑定不知道为什么,在某些情况下会认为你没有设置一个 bindinput 事件(但实际上应该是由双向绑定自动设置的),于是不断地在后台刷警告,因此还不如手动实现来的省心。


有限的标准组件支持


如果你觉得微信小程序的开发和前端开发差不多,那就大错特错了。因为微信小程序默认情况下根本不支持任何 HTML 元素,而是套了一层他们自己的元素,比如 view 实际上是 classblock 则和 Vue 的 template 差不多(微信小程序也有 template 元素,只不过那个是给组件用的),不分 h1, h2, span, strong,只有 text 元素等。当然好在 CSS 还是那套,基本都能用。


但是... 微信小程序提供的元素依然太少了,根本没办法满足实际开发需要(比如根本没有表格元素)。于是微信小程序提供了一个 rich-text 元素,可用于渲染 HTML 元素。


但是这个 rich-text 就显得十分鸡肋,他不是通过 slot 传入 HTML 元素,而是通过 string 或者 object。这凭空增加了开发难度,导致我不得不这么写:


<rich-text nodes="{{nodes}}"></rich-text>

this.setData({
nodes: licenses.map(it => {
return `
<div style="margin: 20px 10px;"><strong>${it.projectName}</strong>
is licensed under the <code>${it.licenseName}</code>:</div>
<pre style="overflow: auto; background-color:#F5F6FA;"><code>${it.fullLicense}</code></pre>
${it.sourceRepo?`<div style="margin: 20px 10px;"><span style="color:gray; font-size: 12px;">The source code can be found at: ${it.sourceRepo}</span></div>`:""}
<br/><br/>
`

}).join("")
})

甚至这么写:



完美的回答了知乎有人“为什么不用 JSON 表达页面而是用类似 XML 一样的 HTML”的问题。


最后


虽然吐槽了这么多,但是微信小程序还是有一些不错的点的。除了上面说的宽松的跨域策略以外,微信小程序的 TypeScript 支持很完善,IDE 工具链做的也不错(除了他那个特别容易崩溃的 Simulator),加之微信开放社区的活跃度也不低(问问题一天内就有人回复),也算是能用了。


作者:HikariLan贺兰星辰
来源:juejin.cn/post/7228563544022761509
收起阅读 »

很多人是无知的,但是他们总是觉得自己是对的!

昨天在朋友圈看到了我少年时期的初恋在朋友圈晒娃,她的娃拿了省里文艺比赛的冠军,站在C位拿着话筒演讲。 同时也看到了另外的朋友晒娃,只是二者形成了很鲜明的对比。 我在动车上陷入了沉思,然后和好朋友聊了几句,他说:我们那个时代的成长和教育方式在这个时代已经不可取了...
继续阅读 »

图片


昨天在朋友圈看到了我少年时期的初恋在朋友圈晒娃,她的娃拿了省里文艺比赛的冠军,站在C位拿着话筒演讲。


同时也看到了另外的朋友晒娃,只是二者形成了很鲜明的对比。


我在动车上陷入了沉思,然后和好朋友聊了几句,他说:我们那个时代的成长和教育方式在这个时代已经不可取了!


在我小时候,农活干得很熟练,挖地,跳水,放牛,割草,整天脏兮兮的,没条件学习艺术,没条件去旅游,眼睛看到的永远是门前那一望无尽的大山。


虽然三年级就到了镇里读书,五年级就到了城里读书,但是自卑伴随了很久。不过好在父母基本上一直都在身边,虽然物质条件不充足,但是精神上并不是那么贫乏。


但是我想说的是,对于我这种农村出来的95后,这种事情发生在那个年代是正常的,是可以理解的。但是发生在今天,那就是有问题的,因为那时候大多父母的文化水平都比较低,农村人的土地思维还根深蒂固。 只要孩子能平平安安成长,至于将来有没有出息,就看命吧。


但是今天不一样了,即使是大山里面的,那也看过了外面的世界,文化水平也提高了许多,但是为什么依然还会出现很多年轻人即使物质生活多么贫乏,依然还是选择要孩子,而孩子无论从教育还是各方面都非常落后。


在我小时候那个年代,早上打着电筒去上学的事情是有,但是到了今天,我亲眼看见了七八岁的小孩子早晨六七点在乡村充满泥泞的路上打着电筒,穿着陈旧的衣服,冷得打哆嗦,走几公里的路去上学。


而在他们没见过,没听过,没想过的大中小城市里面,小孩子早上起来吃了营养餐,父母或者爷爷奶奶再送去上学,每天学习各种技能,才艺,人一说都特别有自信。


你也别说这说那,比如:干嘛要送孩子啊,你看人家日本,孩子从来都是自己去上学,从小就锻炼了独立的意识,而国内则当成老祖宗一样,上下学都接送。


但是人家和你一样吗,人家那是选择这样,而我们大部分人是只能这样,这个问题下面我们会谈。


所以我们发现一个问题,农村出来的孩子在这个时代大多混得都比较差,还比较自卑,只有极少的能稍微改命,但一定是经过脱胎换骨换来的。


我们经常在网上看到一些视频,父母在外务工,一个小孩子就在开始干家务,做饭,还要照顾比自己小的弟弟妹妹。


然后下面的人就说:这孩子以后一定能成大器,一定有一番作为。


有些人也深深认同,甚至搬出一些名人的故事来:比如董卿很小的时候父亲就让她承包家务,每天还要去酒店打扫很多房间的卫生,最后人家不也成就一番事业了。


我想说:简直荒诞得不行,就算这种事情是真实存在过,那我们也别用来乱套在所有孩子身上。


现在很多人就喜欢说,孩子要穷养,这样对他以后才好,但是穷养并不是你想的那样的,你也别乱套在孩子身上。


首先要区分真穷养还是假穷养


董卿的父母都是复旦大学毕业的高材生,这种家庭放在今天都是炸裂的存在,更何况是八九十年代,所以文化水平和经济水平都是很强的。


那么人家穷养的目的是啥,无非就是锻炼孩子的心智,让她以后的路走得更远。


所以人家是有选择性的去穷养,今天我可以让你去打扫酒店,明天就可以带你去看艺术表演,学习钢琴。


但是一般甚至过得艰难的家庭,穷养不是选择,而是没有办法,所以只能穷养,你今天干家务,明天也只能干家务,看艺术,学钢琴和你一点关系都没有,甚至你一辈子都不可能接触到。


而且孩子以后走的路大概率也是十分艰难,都是为了一次三餐,混得基本上也不会好,这是必然的。


但是奇怪的是,很多人为了所谓的人生任务,传宗接代,根本不会去思考这些问题,甚至还有一些年轻人还抱有“儿孙自有儿孙福”的落后思想,还将自己的养老任务寄托在孩子身上。


有时候真的无法想象,二十一世纪了,还抱有这种思想,实在是可恶,可悲!


我先表达自己的观点和立场:如果你没有一定的经济支撑和教育能力,而是想要孩子自己靠自己,那么就是不负责任!


那么回到文章开头,一个孩子在省里的台上演讲,一个孩子在泥泞中穿梭。


是想说什么呢?


其实无非就是想表达良好的教育和物质生活的重要性。


我亲自见过一些尖酸刻薄的人,从来不会反思自己,看到别人的孩子特别优秀,他们会说:有啥了不起的,我孩子也不差,虽然在农村玩泥巴,但是他健康啊,他快乐啊,你孩子虽然成绩优异,能歌善舞,但是你看他压力多大,没有童年。


然后转头望向旁边的几个孩子,对他们说:以后老妈就靠你们了,你们以后出来打工,一人给我买一个金戒指和金项链,直接把老妈的脖子都压弯。


上面的事情是我亲眼目睹的,他们没耐心教孩子做作业,而是直接手机上搜出来抄上去。


然后孩子的考试成绩差了,很多人就开始怪孩子了,大声呵斥:你是怎么学的,你怎么一点出息都没有,你看看人家为啥能考第一,你为啥只能考这么点分?


而这样的例子少吗?我想说,一点也不少,特别是在落后的农村和小县城,很普遍。


因为他们的目的就是怕以后自己老了没人养啊,死了没人送终啊,然后又一直给孩子灌输这种思想,最终造成了恶性循环。


而孩子从小就没有得到良好的教育和生活体验,进入社会会恐惧,也没多少竞争力。


你以为那些初入职场就特别优秀的人是进入职场才优秀的吗?


不,人家在读书的时候就已经开始崭露头角了。


国内外你只要能数得出来的优秀企业家,作家,艺术家等等,要么从小家境就不错,即使不是大富大贵,但是也是小康以上,要么文化,教育气氛特别浓厚,要么二者都兼顾,基本很难找出一个没有具备这二者的条件人。


就拿几个熟悉的人来说,人家余华当年能在家全职写作,罗永浩能在家看两年闲书,马老师能复读。


那已经是八九十年代的事情了。


试问,就算现在有多少家庭能扛得住?


并不是说经济条件和教育条件一定要多么优越才能养孩子,而是最起码要有基本的保障吧,能做到负责二字吧。


你总不可能让他以后再把你的老路走一遍吧,这和害人没有任何区别。


还有很多父母总是逼着自己的孩子结婚生子,哪怕孩子现在都自身难保。


他们会说:生了以后放孩子在农村,我们给你带啊,你们再去外面打工,几年后孩子长大了就能自己读书,就能自己做饭了。


嗯。。。。。。。。。。。。。


很离谱。


但是依旧有很多按照旨意去做了,不为别的,就是为了所谓的责任。


然后就开始赌下一代会有出息,好给自己打个漂亮的翻身仗,自己的晚年就能安稳度过了。


二三十几岁,就已经开始去担心65岁以后的日子了,把话说难听一点,如果当下都不能好好去生活,还指望65岁以后能够生活好?


反正我不信,这逻辑本来就行不通。


作者:苏格拉的底牌
来源:juejin.cn/post/7327138554756612148
收起阅读 »

面试理想汽车,给我整懵了。。。

理想汽车 今天看到一个帖子,挺有意思的。 先别急着骂草台班子。 像理想汽车这种情况,其实还挺常见的。 就是:面试官说出一个错误的结论,我们该咋办? 比较好的做法还是先沟通确认清楚,看看大家是否针对的为同一场景,对某些名词的认识是否统一,其实就是对错误结论的再...
继续阅读 »

理想汽车


今天看到一个帖子,挺有意思的。



先别急着骂草台班子。


像理想汽车这种情况,其实还挺常见的。


就是:面试官说出一个错误的结论,我们该咋办?


比较好的做法还是先沟通确认清楚,看看大家是否针对的为同一场景,对某些名词的认识是否统一,其实就是对错误结论的再次确认。


如果确定清楚是面试官的错误,仅做一次不直白的提醒后,看对方是否会陷入不确定,然后进入下一个问题,如果是的话,那就接着往下走。


如果对方还是揪着那个错误结论不放,不断追问。


此时千万不要只拿你认为正确的结论出来和对方辩论。


因为他只有一个结论,你也只有一个结论的话,场面就成了没有理据的争论,谁也说服不了谁。


我们可以从两个方向进行解释:



  • 用逻辑进行正向推导,证明你的结论的正确性

  • 用类似反证法的手段进行解释,试图从他的错误结论出发,往回推,直到推出一个对方能理解的,与常识相违背的基本知识


那么对应今天这个例子,关于「后序遍历」的属于一个定义类的认识。


我们可以用正向推导的方法,试图纠正对方。


可以从另外两种遍历方式进行入手,帮助对方理解。


比如你说:


"您看,前序遍历是「中/根 - 左 - 右」,中序遍历是「左 - 中/根 - 右」"


"所以它这个「X序遍历」的命名规则,主要是看对于一棵子树来说,根节点被何时访问。"


"所以我理解的后序遍历应该是「左 - 右 - 中/根」。"


"这几个遍历确实容易混,所以我都是这样的记忆理解的。"


大家需要搞清楚,这一段的主要目的,不是真的为了教面试官知识,因此适当舍弃一点点的严谨性,提高易懂性,十分重要。


因为我们的主要目的是:想通过有理据的解释,让他不要再在这个问题下纠缠下去


如果是单纯想争对错,就不会有前面的「先进行友好提示,对方如果进行下一问,就接着往下」的前置处理环节。


搞清楚这一段表达的实际目的之后,你大概知道用什么口吻进行解释了,包括上述的最后一句,给对方台阶下,我觉得也是必要的。


对方是错了,但是你没必要给别人落一个「得理不饶人」的印象。


还是谦逊一些,面试场上争对错,赢没赢都是候选人输。


可能会有一些刚毕业的同学,心高气傲,觉得连二叉树这么简单的问题都搞岔的面试官,不值得被尊重。


你要知道,Homebrew 作者去面谷歌的时候,也不会翻转二叉树呢。


难道你要说这世上只有那些知识面是你知识面超集的人,才值得被尊重吗?


显然不是的,大家还是要学会带着同理心的去看待世界。


...


看了一眼,底下评论点赞最高的那位:



什么高情商说法,还得是网友。


所以面试官说的后序遍历是「右 - 左 - 中」?interesting。


...


回归主线。


也别二叉树后续遍历了,直接来个 nn 叉树的后序遍历。


题目描述


平台:LeetCode


题号:590


给定一个 nn 叉树的根节点 rootroot ,返回 其节点值的后序遍历


nn 叉树在输入中按层序遍历进行序列化表示,每组子节点由空值 null 分隔(请参见示例)。


示例 1:


输入:root = [1,null,3,2,4,null,5,6]

输出:[5,6,3,2,4,1]

示例 2:


输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:[2,6,14,11,7,3,12,8,4,13,9,10,5,1]

提示:



  • 节点总数在范围 [0,104][0, 10^4]

  • 0<=Node.val<=1040 <= Node.val <= 10^4

  • nn 叉树的高度小于或等于 10001000


进阶:递归法很简单,你可以使用迭代法完成此题吗?


递归


常规做法,不再赘述。


Java 代码:


class Solution {
List ans = new ArrayList<>();
public List postorder(Node root) {
dfs(root);
return ans;
}
void dfs(Node root) {
if (root == null) return;
for (Node node : root.children) dfs(node);
ans.add(root.val);
}
}

C++ 代码:


class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> ans;
dfs(root, ans);
return ans;
}
void dfs(Node* root, vector<int>& ans) {
if (!root) return;
for (Node* child : root->children) dfs(child, ans);
ans.push_back(root->val);
}
};

Python 代码:


class Solution:
def postorder(self, root: 'Node') -> List[int]:
def dfs(root, ans):
if not root: return
for child in root.children:
dfs(child, ans)
ans.append(root.val)
ans = []
dfs(root, ans)
return ans

TypeScript 代码:


function postorder(root: Node | null): number[] {
const dfs = function(root: Node | null, ans: number[]): void {
if (!root) return ;
for (const child of root.children) dfs(child, ans);
ans.push(root.val);
};
const ans: number[] = [];
dfs(root, ans);
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:忽略递归带来的额外空间开销,复杂度为 O(1)O(1)


非递归


针对本题,使用「栈」模拟递归过程。


迭代过程中记录 (cnt = 当前节点遍历过的子节点数量, node = 当前节点) 二元组,每次取出栈顶元素,如果当前节点已经遍历完所有的子节点(当前遍历过的子节点数量为 cnt=子节点数量cnt = 子节点数量),则将当前节点的值加入答案。


否则更新当前元素遍历过的子节点数量,并重新入队,即将 (cnt+1,node)(cnt + 1, node) 入队,以及将下一子节点 (0,node.children[cnt])(0, node.children[cnt]) 进行首次入队。


Java 代码:


class Solution {
public List postorder(Node root) {
List ans = new ArrayList<>();
Deque d = new ArrayDeque<>();
d.addLast(new Object[]{0, root});
while (!d.isEmpty()) {
Object[] poll = d.pollLast();
Integer cnt = (Integer)poll[0]; Node t = (Node)poll[1];
if (t == null) continue;
if (cnt == t.children.size()) ans.add(t.val);
if (cnt < t.children.size()) {
d.addLast(new Object[]{cnt + 1, t});
d.addLast(new Object[]{0, t.children.get(cnt)});
}
}
return ans;
}
}

C++ 代码:


class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> ans;
stackint, Node*>> st;
st.push({0, root});
while (!st.empty()) {
auto [cnt, t] = st.top();
st.pop();
if (!t) continue;
if (cnt == t->children.size()) ans.push_back(t->val);
if (cnt < t->children.size()) {
st.push({cnt + 1, t});
st.push({0, t->children[cnt]});
}
}
return ans;
}
};

Python 代码:


class Solution:
def postorder(self, root: 'Node') -> List[int]:
ans = []
stack = [(0, root)]
while stack:
cnt, t = stack.pop()
if not t: continue
if cnt == len(t.children):
ans.append(t.val)
if cnt < len(t.children):
stack.append((cnt + 1, t))
stack.append((0, t.children[cnt]))
return ans

TypeScript 代码:


function postorder(root: Node | null): number[] {
const ans = [], stack = [];
stack.push([0, root]);
while (stack.length > 0) {
const [cnt, t] = stack.pop()!;
if (!t) continue;
if (cnt === t.children.length) ans.push(t.val);
if (cnt < t.children.length) {
stack.push([cnt + 1, t]);
stack.push([0, t.children[cnt]]);
}
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(n)O(n)


通用「非递归」


另外一种「递归」转「迭代」的做法,是直接模拟系统执行「递归」的过程,这是一种更为通用的做法。


由于现代编译器已经做了很多关于递归的优化,现在这种技巧已经无须掌握。


在迭代过程中记录当前栈帧位置状态 loc,在每个状态流转节点做相应操作。


Java 代码:


class Solution {
public List postorder(Node root) {
List ans = new ArrayList<>();
Deque d = new ArrayDeque<>();
d.addLast(new Object[]{0, root});
while (!d.isEmpty()) {
Object[] poll = d.pollLast();
Integer loc = (Integer)poll[0]; Node t = (Node)poll[1];
if (t == null) continue;
if (loc == 0) {
d.addLast(new Object[]{1, t});
int n = t.children.size();
for (int i = n - 1; i >= 0; i--) d.addLast(new Object[]{0, t.children.get(i)});
} else if (loc == 1) {
ans.add(t.val);
}
}
return ans;
}
}

C++ 代码:


class Solution {
public:
vector<int> postorder(Node* root) {
vector<int> ans;
stackint, Node*>> st;
st.push({0, root});
while (!st.empty()) {
int loc = st.top().first;
Node* t = st.top().second;
st.pop();
if (!t) continue;
if (loc == 0) {
st.push({1, t});
for (int i = t->children.size() - 1; i >= 0; i--) {
st.push({0, t->children[i]});
}
} else if (loc == 1) {
ans.push_back(t->val);
}
}
return ans;
}
};

Python 代码:


class Solution:
def postorder(self, root: 'Node') -> List[int]:
ans = []
stack = [(0, root)]
while stack:
loc, t = stack.pop()
if not t: continue
if loc == 0:
stack.append((1, t))
for child in reversed(t.children):
stack.append((0, child))
elif loc == 1:
ans.append(t.val)
return ans

TypeScript 代码:


function postorder(root: Node | null): number[] {
const ans: number[] = [];
const stack: [number, Node | null][] = [[0, root]];
while (stack.length > 0) {
const [loc, t] = stack.pop()!;
if (!t) continue;
if (loc === 0) {
stack.push([1, t]);
for (let i = t.children.length - 1; i >= 0; i--) {
stack.push([0, t.children[i]]);
}
} else if (loc === 1) {
ans.push(t.val);
}
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(n)O(n)

作者:宫水三叶的刷题日记
来源:juejin.cn/post/7327188195770351635
收起阅读 »

程序员为什么不能一次把功能写好,是因为他不想吗

引言 交流一下为什么他做的功能这么多Bug 大家好,最近看到一个有趣的问题: 程序员为什么要不能一次性写好,需要一直改Bug? 在我看来,程序员也是人,并非机器。 拿这个问题去质问程序员,答案无非那么几个。 1.需求的理解 有时候,在项目一开始,需求可能并...
继续阅读 »

为什么他做的功能那么多Bug


引言


交流一下为什么他做的功能这么多Bug


大家好,最近看到一个有趣的问题



程序员为什么要不能一次性写好,需要一直改Bug?



在我看来,程序员也是人,并非机器。


拿这个问题去质问程序员,答案无非那么几个


1.需求的理解


有时候,在项目一开始,需求可能并没有被完全理解清楚。


随着项目的推进,更多的细节可能浮现,需要对代码进行调整以适应新的或更清晰的需求。


首先需求的传递,通常有以下几种



  • 口头传递:程序员可能无意间听到策划的一句话,就认定为需求就是这样。

  • 需求会议:这是笔者认为比较正式的,相关人员一起,进行需求的分析和探讨。

  • 临时加的:前面提需求的时候遗漏的,后面补的。

  • 非工作日加的:在非工作日休息时,收到经理或者老板的电话需求。


这里面都涉及人与人之间交流和理解。它是极其容易受到人的状态和情绪影响的。


可能因为程序员在理解需求时较真策划无意或者有意的一句话


也可能因为程序员在会议过程中打瞌睡或者不以为然


甚至在程序员情绪不满的状态下接到了需求。


2.功能的复杂性


许多功能都涉及复杂的业务逻辑、数据处理和用户交互


理解整个功能如何运作的过程中,程序员可能会对功能的梳理不够清晰,导致一开始的实现可能考虑得不够完善


相信大家都清楚,无论是大功能还是小功能,都会有Bug


但是在相对复杂的功能下,Bug会更加容易出现甚至更多。


笔者认为这和人生的选择有点相似,越是关键的选择,越难选择


3.新的内容


项目迭代过程中,可能需要引入新的功能,他可能与项目框架或者方向完全不同。


这必然会导致程序的稳定性受到影响。


越是底层的内容,在修改时引发的内容变化就越容易,影响的面更广


这里面可能新的内容旧项目完全不搭,强行要引入这样的内容,在设计层面就不对。


也可能是因为程序员考虑不当,没有更加全面的考虑到策划或者经理的变化


4.时间的压力


项目通常有时间限制,导致程序员可能不得不在有限的时间内完成任务。


这可能导致在一开始时忽略一些潜在的问题,需要在后期修复


迫于时间的压力,程序员往往会不断地跳过遇到的问题,往更容易完成的方向去执行


那么这些卡点会被放到功能的最后处理,这和我们以前考试是相类似的。


老师教导我们,在考试遇到困难的问题时,先跳过,等到试卷做完一遍之后回来再看难题。


但往往问题也会出现在这些跳过的内容,要么难题还是难题,做不出来。要么就是给到这些难题的时间已经不多了。


5.功能的耦合


在团队协作的环境中,不同部分的代码可能同时被多个程序员修改,可能导致冲突和Bug


此外,不同模块之间的复杂交互可能在测试之前难以被完全预测


这种问题通常表现为,A程序员修改的项目的A功能,但是出乎意料的的是B程序员B功能出了问题。


这里面就涉及框架和项目的耦合情况,越是耦合严重的代码(通常被称为"屎山"),你的修改越是不能一干二净出乎意料地影响了其他功能。


6.硬件和环境变化


程序可能在不同的硬件和环境中运行,这可能导致一些未考虑到的问题。


为了适应不同的环境,可能需要进行一些修复和调整


大家知道用户的使用环境可能千奇百怪


首先设备环境就分为好几种,原生的Android,iOS,网页的H5,还有PC小程序


其次不同的网络环境,2g,3g,4g,5g和wifi


程序员在开发时以最好的网络最好的机器,去到用户的千元机,万元机和老人机的时候表现都不尽相同。


怎么解决


一把需求给你,你就那么多问题,都是不能解决的吗?


笔者认为事实并不如此,人是会进步的,通过不断的总结和优化,能逐步减少Bug的产生,但是不能杜绝



  • 需求理解:程序员与策划/经理的关系要融洽,工作时沟通和交流不要存在个人情绪和意见。认真对待每次需求会议。

  • 功能的复杂性:程序员与策划/经理要一同考虑功能的复杂性,策划与经理不能一味地提需求而不考虑复杂性,程序员不能一味地实现功能不考虑功能的变化。

  • 新的内容:程序员要仔细评估新内容对旧项目的冲击,策划/经理要认真考虑,这个功能是不是真的合适项目。

  • 时间的压力:更合理地评估功能的完成时间,拒绝不合理的降本增效。

  • 功能的耦合:不断提升代码能力,学习更加优秀的写法,应对不同需求的变化。

  • 硬件和环境变化:加强不同环境的测试,这里面要考虑的是不同环境测试的便捷性,不断优化测试环境,不要让测试困难导致了Bug的产生。


结语


不管是程序员还是策划还是经理,沟通是减少问题的关键,而不是质问。


在哪里可以看到如此清晰的思路,快跟上我的节奏!关注我,和我一起了解游戏行业最新动态,学习游戏开发技巧。


作者:亿元程序员
来源:juejin.cn/post/7320906381795672116
收起阅读 »

Linux新手村必备!这些常用操作命令你掌握了吗?

在计算机的世界里,Linux操作系统以其强大的功能和灵活性受到了广大程序员和IT爱好者的喜爱。然而,对于初学者来说,Linux的操作命令可能会显得有些复杂和难以理解。今天,我们就来一起探索一些Linux常用操作命令,让你的计算机操作更加流畅。一、目录操作首先带...
继续阅读 »

在计算机的世界里,Linux操作系统以其强大的功能和灵活性受到了广大程序员和IT爱好者的喜爱。然而,对于初学者来说,Linux的操作命令可能会显得有些复杂和难以理解。

今天,我们就来一起探索一些Linux常用操作命令,让你的计算机操作更加流畅。

一、目录操作

首先带大家了解一下Linux 系统目录:

├── bin -> usr/bin # 用于存放二进制命令
├── boot # 内核及引导系统程序所在的目录
├── dev # 所有设备文件的目录(如磁盘、光驱等)
├── etc # 配置文件默认路径、服务启动命令存放目录
├── home # 用户家目录,root用户为/root
├── lib -> usr/lib # 32位库文件存放目录
├── lib64 -> usr/lib64 # 64位库文件存放目录
├── media # 媒体文件存放目录
├── mnt # 临时挂载设备目录
├── opt # 自定义软件安装存放目录
├── proc # 进程及内核信息存放目录
├── root # Root用户家目录
├── run # 系统运行时产生临时文件,存放目录
├── sbin -> usr/sbin # 系统管理命令存放目录
├── srv # 服务启动之后需要访问的数据目录
├── sys # 系统使用目录
├── tmp # 临时文件目录
├── usr # 系统命令和帮助文件目录
└── var # 存放内容易变的文件的目录

下面我们来看目录操作命令有哪些

pwd    查看当前工作目录
clear 清除屏幕
cd ~ 当前用户目录
cd / 根目录
cd - 上一次访问的目录
cd .. 上一级目录

查看目录内信息

ll    查看当前目录下内容(LL的小写)

创建目录

  • mkdir aaa 在当前目录下创建aaa目录,相对路径;
  • mkdir ./bbb 在当前目录下创建bbb目录,相对路径;
  • mkdir /ccc 在根目录下创建ccc目录,绝对路径;

递归创建目录(会创建里面没有的目录文件夹)

mkdir -p temp/nginx

搜索命令

  • find / -name ‘b’ 查询根目录下(包括子目录),名以b的目录和文件;
  • find / -name ‘b*’ 查询根目录下(包括子目录),名以b开头的目录和文件;
  • find . -name ‘b’ 查询当前目录下(包括子目录),名以b的目录和文件;

重命名

mv 原先目录 文件的名称   mv tomcat001 tomcat

剪切命令(有目录剪切到制定目录下,没有的话剪切为指定目录)

mv /aaa /bbb      将根目录下的aaa目录,移动到bbb目录下(假如没有bbb目录,则重命名为bbb);
mv bbbb usr/bbb 将当前目录下的bbbb目录,移动到usr目录下,并且修改名称为bbb;
mv bbb usr/aaa 将当前目录下的bbbb目录,移动到usr目录下,并且修改名称为aaa;

复制目录

cp -r /aaa /bbb:将/目录下的aaa目录复制到/bbb目录下,在/bbb目录下的名称为aaa
cp -r /aaa /bbb/aaa:将/目录下的aa目录复制到/bbb目录下,且修改名为aaa;

强制式删除指定目录

rm -rf /bbb:强制删除/目录下的bbb目录。如果bbb目录中还有子目录,也会被强制删除,不会提示;

删除目录

  • rm -r /bbb:普通删除。会询问你是否删除每一个文件
  • rmdir test01:目录的删除

查看树状目录结构

tree test01/

批量操作

需要采用{}进行参数的传入了。

mkdir {dirA,dirB}  # 批量创建测试目录
touch dirA/{A1,A2,A3} # dirA创建三个文件dirA/A1,dirA/A2,dirA/A3

二、文件操作

删除

rm -r a.java  删除当前目录下的a.java文件(每次会询问是否删除y:同意)

强制删除

  • rm -rf a.java 强制删除当前目录下的a.java文件
  • rm -rf ./a* 强制删除当前目录下以a开头的所有文件;
  • rm -rf ./* 强制删除当前目录下所有文件(慎用);

创建文件

touch testFile

递归删除.pyc格式的文件

find . -name '*.pyc' -exec rm -rf {} \;

打印当前文件夹下指定大小的文件

find . -name "*" -size 145800c -print

递归删除指定大小的文件(145800)

find . -name "*" -size 145800c -exec rm -rf {} \;

递归删除指定大小的文件,并打印出来

find . -name "*" -size 145800c -print -exec rm -rf {} \;
  • “.” 表示从当前目录开始递归查找
  • “ -name ‘*.exe’ "根据名称来查找,要查找所有以.exe结尾的文件夹或者文件
  • " -type f "查找的类型为文件
  • “-print” 输出查找的文件目录名
  • -size 145800c 指定文件的大小
  • -exec rm -rf {} ; 递归删除(前面查询出来的结果)

split拆分文件

split命令:可以将一个大文件分割成很多个小文件,有时需要将文件分割成更小的片段,比如为提高可读性,生成日志等。

  1. b:值为每一输出档案的大小,单位为 byte。
  2. -C:每一输出档中,单行的最大 byte 数。
  3. -d:使用数字作为后缀。
  4. -l:值为每一输出档的行数大小。
  5. -a:指定后缀长度(默认为2)。

使用split命令将上面创建的date.file文件分割成大小为10KB的小文件:

[root@localhost split]# split -b 10k date.file
[root@localhost split]# ls
date.file xaa xab xac xad xae xaf xag xah xai xaj

文件被分割成多个带有字母的后缀文件,如果想用数字后缀可使用-d参数,同时可以使用-a length来指定后缀的长度:

[root@localhost split]# split -b 10k date.file -d -a 3
[root@localhost split]# ls
date.file x000 x001 x002 x003 x004 x005 x006 x007 x008 x009

为分割后的文件指定文件名的前缀:

[root@localhost split]# split -b 10k date.file -d -a 3 split_file
[root@localhost split]# ls
date.file split_file000 split_file001 split_file002 split_file003 split_file004 split_file005 split_file006 split_file007 split_file008 split_file009

使用-l选项根据文件的行数来分割文件,例如把文件分割成每个包含10行的小文件:

split -l 10 date.file

三、文件内容操作

修改文件内容

  • vim a.java:进入一般模式
  • i(按键):进入插入模式(编辑模式)
  • ESC(按键):退出
  • :wq:保存退出(shift+:调起输入框)
  • :q!:不保存退出(shift+:调起输入框)(内容有更改)(强制退出,不保留更改内容)
  • :q:不保存退出(shift+:调起输入框)(没有内容更改)
    文件内容的查看
cat a.java   查看a.java文件的最后一页内容;
more a.java从 第一页开始查看a.java文件内容,按回车键一行一行进行查看,按空格键一页一页进行查看,q退出;
less a.java 从第一页开始查看a.java文件内容,按回车键一行一行的看,按空格键一页一页的看,支持使用PageDown和PageUp翻页,q退出。

总结下more和less的区别

  • less可以按键盘上下方向键显示上下内容,more不能通过上下方向键控制显示。
  • less不必读整个文件,加载速度会比more更快。
  • less退出后shell不会留下刚显示的内容,而more退出后会在shell上留下刚显示的内容。

实时查看文件后几行(实时查看日志)

tail -f a.java   查看a.java文件的后10行内容;

前后几行查看

  • head a.java:查看a.java文件的前10行内容;
  • tail -f a.java:查看a.java文件的后10行内容;
  • head -n 7 a.java:查看a.java文件的前7行内容;
  • tail -n 7 a.java:查看a.java文件的后7行内容;

文件内部搜索指定的内容

  • grep under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行;
  • grep -n under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行及行号;
  • grep -v under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示没搜索到的行;
  • grep -i under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行;
  • grep -ni under 123.txt:在123.txt文件中搜索under字符串,大小写敏感,显示行及行号;

终止当前操作

Ctrl+c和Ctrl+z都是中断命令,但是作用却不一样。

Ctrl+Z就扮演了类似的角色,将任务中断,但是任务并没有结束,在进程中只是维持挂起的状态,用户可以使用fg/bg操作前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行。

Ctrl+C也扮演类似的角色,强制终端程序的执行。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里即可免费学习!

重定向功能

可以使用 > 或 < 将命令的输出的命令重定向到test.txt文件中(没有则创建一个)

echo 'Hello World' > /root/test.txt

1、grep(检索文件内容)

grep [options] pattern file
  • 全称:Global Regular Expression Print。
  • 作用:查找文件里符合条件的字符串。
// 从test开头文件中,查找含有start的行
grep "start" test*
// 查看包含https的行,并展示前1行(-A),后1行(-B)
grep -A 1 -B 1 "https" wget-log

2、awk(数据统计)

awk [options] 'cmd' file
  • 一次读取一行文本,按输入分隔符进行切片,切成多个组成部分。
  • 将切片直接保存在内建的变量中,$1,$2…($0表示行的全部)。
  • 支持对单个切片的判断,支持循环判断,默认分隔符为空格。
  • -F 指定分隔符(默认为空格)
    1)将email.out进行切分,打印出第1/3列内容
awk '{print $1,$3}' email.out

2)将email.out进行切分,当第1列为tcp,第2列为1的列,全部打印

awk '$1=="tcp" && $2==1{print $0}' email.out

3)在上面的基础上将表头进行打印(NR表头)

awk '($1=="tcp" && $2==1)|| NR==1 {print $0}' email.out

4) 以,为分隔符,切分数据,并打印第二列的内容

awk -F "," '{print $2}' test.txt

5)将日志中第1/3列进行打印,并对第1列的数据进行分类统计

awk '{print $1,$3}' email.out | awk '{count[$1]++} END {for(i in count) print i "\t" count[i]}'

6)根据逗号,切分数据,并将第一列存在文件test01.txt中

awk -F "," '{print $1 >> "test01.txt"}

3、sed(替换文件内容)

  • sed [option] ‘sed commond’ filename
  • 全名Stream Editor,流编辑器
  • 适合用于对文本行内容进行处理
  • sed commond为正则表达式
  • sed commond中为三个/,分别为源内容,替换后的内容

sed替换标记

g # 表示行内全面替换。
p # 表示打印行。
w # 表示把行写入一个文件。
x # 表示互换模板块中的文本和缓冲区中的文本。
y # 表示把一个字符翻译为另外的字符(但是不用于正则表达式)
\1 # 子串匹配标记
& # 已匹配字符串标记

1)替换解析

sed -i 's/^Str/String/' replace.java

Description

2)将末尾的.替换为;(转义.)

sed -i 's/\.$/\;/'

3)全文将Jack替换为me(g是全部替换,不加只替换首个)

sed -i 's/Jack/me/g/ replace.java

4)删除replace.java中的空格(d是删除)

sed -i '/^ *$/d' replace.java

5)删除包含Interger的行(d是删除)

sed -i '/Interger/d' replace.java

6)多命令一起执行

grep 'input' 123.txt | sed 's/\"//g; s/,/\n/g'

7)替换后将数据保存在文中

grep  123.txt | sed -n 's/\"//gw test01.txt'

4、管道操作符|

可将指令连接起来,前一个指令的输出作为后一个指令的输入

find ~ |grep "test"
find ~ //查找当前用户所有文件
grep "test" //从文件中

使用管道注意的要点

  • 只处理前一个命令正确输出,不处理错误输出。
  • 右边命令必须能够接收标准输入流,否则传递过程中数据会被抛弃
  • sed,awk,grep,cut,head,top,less,more,c,join,sort,split等

1)从email.log文件中查询包含error的行

grep 'error' email.log

2)获取到error的行,并取[]含有数字的

grep 'error' email.log | grep -o '\[0-9\]'

3)并过滤掉含有当前进程

ps -ef|grep tomcat |grep -v

4)替换后将数据保存在文中

grep  123.txt | sed -n 's/\"//gw test01.txt'

5)将文件123.txt,按,切分,去除",按:切分后,将第一列存到文件test01.txt中

grep 'input' 123.txt | awk -F ',' '{print $2}' | sed 's/\"//g; s/,/\n/g' | awk -F ":" 

5、cut(数据裁剪)

  • 从文件的每一行剪切字节、字符和字段并将这些字节、字符和字段输出。
  • 也可采用管道输入。

Description
文件截取

[root@VM-0-9-centos shell]# cut -d ":" -f 1 cut.txt

管道截取

[root@VM-0-9-centos shell]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

# 按:分割。截取第3列
[root@VM-0-9-centos shell]# echo $PATH | cut -d ":" -f 3
/usr/sbin

# 按:分割。截取第3列之后数据
[root@VM-0-9-centos shell]# echo $PATH | cut -d ":" -f 3-
/usr/sbin:/usr/bin:/root/bin
[root@VM-0-9-centos shell]#

四、系统日志位置

  • cat /etc/redhat-release:查看操作系统版本
  • /var/log/message:系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一
  • /var/log/message:系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一
  • /var/log/secure:与安全相关的日志信息
  • /var/log/maillog:与邮件相关的日志信息
  • /var/log/cron:与定时任务相关的日志信息
  • /var/log/spooler:与UUCP和news设备相关的日志信息
  • /var/log/boot.log:守护进程启动和停止相关的日志消息

查看某文件下的用户操作日志
到达操作的目录下,执行下面的程序:

cat .bash_history

五、创建与删除软连接

1、创建软连接

ln -s /usr/local/app /data

注意:创建软连接时,data目录后不加 / (加上后是查找其下一级目录);
Description

2、删除软连接

rm -rf /data

注意:取消软连接最后没有/,rm -rf 软连接。加上/是删除文件夹;
Description

六、压缩和解压缩

tar
Description
压缩(-c)

tar -cvf start.tar a.java b.java  //将当前目录下a.java、b.java打包
tar -cvf start.tar ./* //将当前目录下的所欲文件打包压缩成haha.tar文件

tar -zcvf start.tar.gz a.java b.java //将当前目录下a.java、b.java打包
tar -zcvf start.tar.gz ./* //将当前目录下的所欲文件打包压缩成start.tar.gz文件

解压缩(-x)

tar -xvf start.tar      //解压start.tar压缩包,到当前文件夹下;
tar -xvf start.tar -C usr/local //(C为大写,中间无空格)
//解压start.tar压缩包,到/usr/local目录下;
tar -zxvf start.tar.gz //解压start.tar.gz压缩包,到当前文件夹下;
tar -zxvf start.tar.gz -C usr/local //(C为大写,中间无空格)
//解压start.tar.gz压缩包,到/usr/local目录下;

解压缩tar.xz文件

tar xf node-v12.18.1-linux-x64.tar.xz

unzip/zip

压缩(zip)

zip lib.zip tomcat.jar       //将单个文件压缩(lib.zip)
zip -r lib.zip lib/ //将目录进行压缩(lib.zip)
zip -r lib.zip tomcat-embed.jar xml-aps.jar //将多个文件压缩为zip文件(lib.zip)

解压缩(unzip)

unzip file1.zip          //解压一个zip格式压缩包
unzip -d /usr/app/com.lydms.english.zip //将`english.zip`包,解压到指定目录下`/usr/app/`

七、Linux下文件的详细信息

R:Read  w:write  x: execute执行
-rw-r--r-- 1 root root 34942 Jan 19 2018 bootstrap.jar
  • 前三位代表当前用户对文件权限:可以读/可以写/不能执行
  • 中间三位代表当前组的其他用户对当前文件的操作权限:可以读/不能写/不能执行
  • 后三位其他用户对当前文件权限:可以读/不能写/不能执行图片

Description

更改文件的权限

chmod u+x web.xml (---x------)  为文件拥有者(user)添加执行权限;
chmod g+x web.xml (------x---) 为文件拥有者所在组(group)添加执行权限;
chmod 111 web.xml (---x--x--x) 为所有用户分类,添加可执行权限;
chmod 222 web.xml (--w--w--w-) 为所有用户分类,添加可写入权限;
chmod 444 web.xml (-r--r--r--) 为所有用户分类,添加可读取权限;

八、Linux终端命令格式

command [-options] [parameter]

说明:

  • command :命令名,相应功能的英文单词或单词的缩写
  • [-options] :选项,可用来对命令进行控制,也可以省略
  • parameter :传给命令的参数,可以是0个、1个或者多个

查阅命令帮助信息

-help: 显示 command 命令的帮助信息;
-man: 查阅 command 命令的使用手册,man 是 manual 的缩写,是 Linux 提供的一个手册,包含了绝大部分的命令、函数的详细使用。

使用 man 时的操作键

Description

以上就是一些Linux常用操作命令的介绍,希望对你有所帮助。

虽然这些只是Linux命令的冰山一角,但它们足以让你自如地运用Linux操作系统,记住,每一个命令都有其独特的用途和魅力。掌握了这些命令,你就能更加自如地在Linux世界中遨游。愿你在探索的道路上,发现更多的惊喜和乐趣!

收起阅读 »

北京职场50万定律:在北京不论你在任何单位工作,只要年收入大于50w,基本上都要牺牲个人生活

大家有没有注意到北京职场里的一个不成文的规则?就是,不管你在哪个单位,干什么活,只要年薪过了50万,基本上个人生活就得打个折扣。无论是教书、看病,还是在金融和互联网领域打拼,或是在央企、民企工作,年入五十万似乎成了个隐形的分水岭。当然,销售岗位除外,那里可能情...
继续阅读 »
大家有没有注意到北京职场里的一个不成文的规则?

就是,不管你在哪个单位,干什么活,只要年薪过了50万,基本上个人生活就得打个折扣。

无论是教书、看病,还是在金融和互联网领域打拼,或是在央企、民企工作,年入五十万似乎成了个隐形的分水岭。当然,销售岗位除外,那里可能情况会有点不同。

但大体上,挣得多,似乎就得在个人时间上付出更多。

北京这地儿,竞争激烈,生活成本高。这就导致了“高薪等于高投入”的默认规则。

想挣大钱,自然得付出相应的努力和时间。这里的“牺牲”,就不仅仅是晚上加个班、周末去办公室那么简单,更多的是一种持续性的、深入骨髓的工作状态。

再看看我们周围,无论是医生还是教师,这些本来应该是相对稳定的职业,现在也变得跟时代的步伐紧密相连。

医生要不断学习新技术,教师要跟上教育的最新趋势。在金融或互联网行业就更不用说了,几乎每时每刻都在发生变化,稍有不慎,就可能被淘汰。

这种压力下,不仅仅是时间的牺牲,还有心理上的压力和身体上的消耗。

那些年薪过50万的人,大多数都不是刚入门的新手,而是那些担任一定职位、肩负一定责任的中高层管理者。

他们不仅要管理好自己的工作,还要带领团队达成目标。这里面的付出,远远超过了普通员工。

作为领导者,他们需要有自我牺牲的精神,不仅要把工作做好,还要让团队成员感到鼓舞和尊重。

但这就带来了一个问题,工作和生活的平衡怎么办?在这样的工作强度下,家庭、朋友、爱好,甚至是基本的休息和锻炼时间,都可能被挤压。有的人为了工作,可能连基本的身体健康都顾不上。

长此以往,无论是身体还是心理上都可能出现问题。

这种“牺牲个人生活”的现象,在体制内也同样存在。想象一下,一个普通的公务员或企业职员,如果只是每天按时上下班,不加班不出差,他的年薪可能也就在25万到35万之间。

但如果想要年薪超过50万,那就必须得承担更多的工作,比如疯狂地做业务,或者成为领导,这几乎意味着要把全部的精力和时间都投入到工作中去,个人生活自然会受到很大影响。

这里的“50万定律”并不是一个精确的数字,可能在40万到60万之间都有类似的现象。

有些行业里,三十岁之前如果年薪没达到这个水平,可能就被认为是没什么前途;而有些行业则突然间成为热门,员工的收入在短时间内暴涨,这都是市场变化的常态

但不可否认的是,这种现象背后反映出的是一个更深层次的社会和文化问题。

在北京这样一个高度竞争的环境下,很多人为了职业成功,不得不放弃其他很多东西,比如时间、家庭甚至是自己的价值观和人格。

说实话,这个“50万定律”真是让人又爱又恨。我们都知道,钱虽然不是万能的,但没钱是万万不能的。

在北京这样的大城市里,不拼一拼,可能连基本的生活水平都难以保障。所以,能拿到高薪的人,确实值得尊敬。

他们的努力和付出是显而易见的。

但话说回来,这种高强度的工作压力真的值得吗?工作再好,钱再多,如果没有时间和精力去享受生活,那这一切又有什么意义呢?

有时候,我真的在想,我们这些在职场打拼的人,是不是都陷入了一个误区:认为只有工作成功了,人生才算成功。这种想法真的对吗?

我觉得,工作是为了更好的生活,而不是生活只为了工作。我们要追求的,应该是一种平衡。不是说不努力工作,而是在努力工作的同时,也要关注自己的身心健康,家庭和人际关系。

毕竟,当我们老了回头看这一生的时候,可能不会因为多挣了几个钱而感到自豪,反而会因为错过了孩子的成长、家人的陪伴而感到遗憾。


作者:升职笔谈
来源:mp.weixin.qq.com/s/Ku-qjNYERd2sqWuNA7IwCw

收起阅读 »

互联网大厂,开始对领导层动刀了

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。 其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。 有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。” 有次我跟...
继续阅读 »

最近,我周围有挺多互联网大厂leader级别的同事或朋友,被“降本增效”了。


其中最有意思的是,我的前同事老Z今年刚刚晋升了一级,在这个级别上还没待热乎了,然后就下来了。


有句话是这么说的:“世界上最残忍的事,莫过于让其拥有一切,然后再剥夺其所有。”


有次我跟老Z吃饭,他苦笑着跟我说:“妈的,如果不晋升,没准还能待下去呢,晋升之后反而目标变大了。”


我问他:“那你最近看新机会的结果怎么样,有没有拿到比较满意的offer呢?”


他说:“面试机会倒是不少,大厂已经面了五六个,但最后都无疾而终了。”


接下来,他又把话题聊了回来,说:“你说,如果公司对我不满意,为什么还给我晋升呢,但如果公司对我满意,又为什么还要裁我呢?”


我给他举了一个这样的例子:“就算大款给小三买奢侈品,让她住豪宅,但并不代表不会甩了她啊,对吧。”


他听了哈哈大笑,似乎释怀了。


接下来,我盘点一下,具备什么特征的管理层最容易被“降本增效”,以及在未来的日子里,我们应该如何应对这种不确定性。


“降本增效”画像


跟大家聊下,哪类用户画像的领导层最容易被“降本增效”,请大家对号入座,别心存侥幸。


(1)非嫡系


不管到哪天,大厂也都是个江湖,是江湖就有人情世故。


如果你不是老板的嫡系,那公司裁员指标下来了,你不背锅谁背锅,你不下地狱谁下地狱。


你可能会说:“我的能力比老板的嫡系强啊,公司这种操作,不成了劣币驱逐良币了吗?”


其实,这个时候对于公司来说,无论是劣币还是良币,都不如人民币来得实在。


人员冗余对于公司来讲就是负担,这个时候谁还跟你讲任人唯亲还是任人唯贤啊。


(2)老员工


可能有人会这么认为,老员工不但忠诚,而且N+1赔的钱也多,为什么会优先裁掉老员工呢。


我认为,一个员工年复一年、日复一日地待在熟悉的工作环境,就犹如温水煮青蛙一样,很容易停留在舒适区,有的甚至混成了老油子。


而老板最希望看到的是,人才要像水一样流动起来,企业要像大自然一样吐故纳新,这样才会一直保持朝气和活力。


总之,老板并不认为员工和公司一起慢慢变老,是一件最浪漫的事。


(3)高职级


对于公司来讲,职级越高的员工,薪资成本也就越高,如果能够创造价值,那自不必多说,否则的话,呵呵呵。。。


现在越来越多的公司,在制定裁员目标的时候,已经不是要裁掉百分之多少的人了,而是裁员后把人均薪资降到多少。


嗯,这就是传说中的“降均薪”,目标用户是谁,不多说也知道了吧?


(4)高龄


35+,40+,嗯,你懂的。


老夫少妻难和谐,大龄下属跟小领导不和谐的几率也很大,一个觉得年轻人不要抬气盛,另外一个觉得不气盛就不是年轻人。


不确定性——在职


恭喜你,幸存者,老天确实待你不薄,在应对不确定性这件事情上,给了你一段时间来缓冲。


如果你已经35+了,那接下来你需要把在职的每一天,都当成是最后一天来度过,然后疯狂地给自己找后路,找副业。


一定要给你自己压力,给自己紧迫感。


因为说不定哪天,曾经对你笑圃如花的HR,会忽然把你叫到一个偏僻的会议室里,面无表情地递给你一式两份的离职协议书,让你签字。


在你心乱如麻地拿起签字笔之际,她没准还得最后PUA你几句:“这次公司不是裁员,而是优化。你要反思自己过去的贡献,认识到自己的不足,这样才能持续发展。


当然,你有大厂员工的光环加持,到市场上还是非常抢手的,你要以人才输出的高度来看这次优化,为社会做贡献。”


至于找后路和副业的方式,现在网上有很多类似的星球,付费和免费的都有,加一个进去,先好好看看,主要是先把思路和视野打开。


当然,如果你周围要是有一个副业做得比较好的同事,并且他愿意言传身教你,那就更好了。


然后,找一个自己适合的方向和领域,动手去做,一定动手去做,先迈出第一步,可以给自己定一个小目标,在未来几个月内,从副业中赚到第一次钱。


从0到1最难,再接下来,应该就顺了。


不确定性——不在职


如果35+的你刚刚下来,而且手头还算殷实的话,我先劝你第一件事:放弃重返职场。


原因很简单,如果一个方向,随着你经验的积累和年龄的增长,不仅不会带来复利,而是路会越走越窄,那你坚持的意义是什么?难道仅仅是凑合活着吗?


第二件事,慢下来,别立马急急忙忙地找出路,更不要一下子拿出很多本金砸在一个项目上。据说,有的项目是专门盯着大厂员工的遣散费来割韭菜的。


有人会说,在职的人你劝要有紧迫感,离职的人你又劝慢下来,这不是“劝风尘从良,逼良家为娼”吗?


其实不是的,只是无论是在职还是离职,我们都需要在某件事情的推进上,保持一个适合且持久的节奏,不要止步不前,也不要急于求成,用力过猛。


第三件事,就是舍得把面子喂狗,不要觉得做这个不体面,做那个有辱斯文,只要在合理合法的情况下,能赚到钱才是最光荣的。


接下来,盘点周围可用资源,调研有哪些领域和方向适合你,并愿意投入下半生的精力all in去做。


这个过程可能会很痛苦,尤其对于一些悲观者来说,一上来会有一种“世界那么大,竟然再也找不到一个我能谋生的手段”的感觉,咬牙挺过去就好了。


这里说一句,人只要自己不主动崩,还是远比想象中耐操很多的。


结语


好像也没什么好说的,大家各自安好,且行且珍惜吧。


作者:托尼学长
来源:juejin.cn/post/7317859658285318170
收起阅读 »

一行代码快速实现全局模糊

web
github 仓库:github.com/astak16/blu…npm 仓库:http://www.npmjs.com/package/blu…页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理敏感数据过滤通常是由后端去做的,有时候...
继续阅读 »

github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232
收起阅读 »

备案的域名过期后没管,竟然违法了?

写在前面 去年曾经申请过一个域名玩,特别便宜,九块钱一年。放上去了一个静态页面,当时研究了一下配置https,然后给自己的网站做过一次备案。 前几天,到期后一直提醒我,但是我工作繁忙,也忘了去处理。直到前两天,突然一个云厂商电话打过来,居然说我的网站涉嫌赌博,...
继续阅读 »

写在前面


去年曾经申请过一个域名玩,特别便宜,九块钱一年。放上去了一个静态页面,当时研究了一下配置https,然后给自己的网站做过一次备案。


前几天,到期后一直提醒我,但是我工作繁忙,也忘了去处理。直到前两天,突然一个云厂商电话打过来,居然说我的网站涉嫌赌博,存在违法行为,要取消备案???


备案域名接管


我的网站就是一个静态页面啊?咋可能涉嫌赌博,而且域名也早已过期了?


电话过来之后,然后收到了一封云服务商发来的邮件



好家伙,这咋突然变成赌博网站了???


完了,域名接管算是让我碰到了,域名到期直接被抢注了,然后把备案信息也直接接管了。


因为电话里已经确认域名过期了,但是备案信息忘了取消,所以云厂商这边就自动发起了取消流程。这个赌博网站已经访问不到了。


写在后面


这危害还挺大的,通过抢注过期的备案域名,来部署违法网站,这就代表着这是一个拥有国内工信部备案的违法网站。


万万没想到,这波让我碰上了,没意识备案信息没有随域名到期一块取消,导致自己的备案信息被接管了。


大家也注意,域名到期的时候记得把自己的备案信息给取消掉。


作者:银空飞羽
来源:juejin.cn/post/7327116051438780456
收起阅读 »

2023年底被裁,分享一下面试经历

人生中第一次被裁员 2023年11月11日。 “来一下小会议室”。 领导给我私发了一条消息。 我隐约感觉到有不好的事情要发生了。在经历了一段时间的996之后,公司也开始陆续裁员了。前几天就已经连续走了好几个同事。我也是猜到被单独叫到小会议室意味着什么。虽然有所...
继续阅读 »

人生中第一次被裁员


2023年11月11日。


“来一下小会议室”。


领导给我私发了一条消息。


我隐约感觉到有不好的事情要发生了。在经历了一段时间的996之后,公司也开始陆续裁员了。前几天就已经连续走了好几个同事。我也是猜到被单独叫到小会议室意味着什么。虽然有所准备,但是还是感到忐忑。老实说做前端快5年了,我从来没有经历过裁员。基本都是发现公司情况不对,我就跳槽了。但是如今互联网it的行情已经大不如前了。我试着投过简历,根本连面试都约不到,一点机会都没。之前我有篇文章996,说明了公司和我个人的一些情况。虽然对公司的一些地方感到不满,但是年底被裁的话。找工作的压力会非常大,还是想先继续混着的。


结果事与愿违。


“你对裁员有什么看法?”


随着领导的提问,我算是明白了,轮到我被裁咯。


”我没什么看法,只要公司合法给我n+1就行“。


已经有同事被裁了,我也是懂了。被裁也没有什么办法,拿钱走人呗。领导说公司是经济性裁员,没有办法。不是我技术的问题,公司养不起这么多技术人员。产品经理、项目经理、ui、前端、后端都有被裁的。离开只是时间问题。公司是一家传统企业,现在赚不到钱了,你懂的。


我也没有过多说什么,然后人事那边的负责人就来了。跟我谈赔偿的问题,说什么公司困难。给的方案是赔偿n,这个月工资给你延几天。把东西交接一下,签字了就可以走了。我也问过之前被裁的同事,都是说公司只给赔偿n。我在公司待了快2年,赔偿我2个月的工资。我也没有挣扎,就是问了赔偿金具体多少钱,什么时候能够给我。然后就签字了,代码提交,写了一些文档。当天下午人就走了。


地铁


下午5点左右,地铁上,我拎着午睡的抱枕和键盘回家了。


我和这家公司的交集到此就结束了。当天谈完签字,人就走了。第二天产品经理还找我看问题呢,我直接说明自己被裁了。感觉就在一瞬间,后来也有一些同事联系我,都觉得我不会被裁,我也觉得自己的技术不是问题。


但是经过这件事我懂了,如果想要在一家公司长久混下去,技术不是重点,对于领导来说服从命令,按部就班就行。但是如果你想要去外面更广阔的世界,我还是推荐你要有自己的想法。不是一味得听从上级的安排。比如上家公司后端是java,但是我就是学go。上家领导想要做rn的app,我就是学flutter。当然这些都是我利用业余的时间学习的。这些额外的知识并不会影响我的基本工作,我本职还是前端开发,vue、uniapp这些会就行了。


关于赔偿的问题,听说有部分同事还在耗着,坚持要n + 1。这件事看自己的把握,我当时的想法是早点离开这个地方。因为我想要在年前找到一份新的工作,如果拖的太晚,后面离过年不久了,找工作基本就没有希望了。或者有人觉得这时候被裁,也是不好找工作。拖一天也是一天的工资,最后还能拿到n + 1那当然就最好了。一起被裁的同事感觉很多都去旅游了,回老家了什么的。


抓紧找工作


被裁第二天,我就开始改写简历了,我说一下自己的情况。


base:上海 5年前端开发经验


5年前端开发经验,其实几年经验这个没啥好说的。因为一个人多少年工作经验并不能代表一个人的实力。我学历大专,可以说是很低了。给自己的定位也不是很高,一个中高级开发。现在的市场感觉起码都是3至5年经验才好找工作。我觉得,这个年纪的开发,应该是能从0到1开发的。基本的框架搭建一下,然后熟练使用一些api就行。是的这样的开发一抓一大把,那么我们就需要有自己的加分项。接下来,我来分析一下我在2周内约到的3家面试公司的情况:


第一家公司:


主要做医疗方面的。技术栈主要是 react + taro小程序。


笔试题大概内容:



  1. flex布局的理解。

  2. 排序算法:这里推荐写快速排序,这个简单写的快,时间复杂度也ok。

  3. 递归遍历对象的属性。

  4. prototype 原型链继承 call/apply/bind

  5. 事件循环,流程控制方面的,这个前端开发面试估计都问烂了,问打印结果的顺序的。

  6. promise 的一些方法,all allsettled

  7. shadow dom 了解程度

  8. vue react 区别,如何理解虚拟dom


但是由于上家主要做的vue,所以面试官主要问的vue。当然我回答的一般,源码没看完,很多看了就忘了,不过基本用法是很熟练的。TyeScript使用情况,小程序开发的一些问题,canvas使用熟练度。最后面试官让我等一下,应该是要领导级别的来面了。结果一会儿跟我说领导今天不在,让我下次再去二面。面试失败了。


总结分析:需要熟练使用 react + taro 小程序,我只是了解taro。对于前端的问题有比较深入的了解。


第二家公司:


主要做游戏的。技术栈主要是 vue + go,需要做小程序、官网。也就是要会uniapp、nuxtjs。


是的,你没有看错。这是一个全栈开发的岗位。后端技术领导面的,说这边没有专业的前端。都是go开发、ios开发兼职前端。我理解的意思基本就是你要身兼多职。然后说要找一个主要做前端但是要会go的。不需要你搭框架,不需要写微服务。我说我会gorm、gin,面试官没有问了。后端主管估计觉得这些后端的基础都很简单,做过基本都会的。


基本都是问的前端的问题,vue 前端数据一万条卡顿怎么处理,网站需要2k 4k 手机端兼容怎么处理。游戏官网的特效活动会比较多,然后就是支付的问题。后面说考虑一下,后面给我答复。问我期望薪资,我感觉也进不去这家了。随便说了一个20+。最后凉凉啦。


总结:需要官网活动特效的制作经验,动画效果。网站兼容性的考虑,1080p 2k 4k,手机端的处理。性能优化,go开发基础。


第三家公司:


主要是供应链物流管理。技术栈 vue + uniapp, 加分项 flutter。


公司需要把老的技术vue2升级到vue3,uniapp的app开发升级成flutter。这个就是跟我上家的经历基本就一样了。vue3 + ts技术栈,小程序需要会。小程序就问了怎么获取手机号的,授权手机的流程,做过就能回答上。如何把项目从 webpack 迁移到 vite的。我之前有文章写过,vite 构建vue3项目。面试官人比较好相处,基本是熟练使用就不是问题。


主要是我展示了自己写的博客,还有一个自己开发的flutter app在我的手机上给面试官展示。面试官问我开发app的一些思路。硬件授权、状态管理、缓存、路由权限啊等等我就自己写的案例app给面试官讲解了一下。面试官比较认可,通过了,然后hr也问了一下流程问题。离职原因,之前公司的人员规模,之前的岗位,之前的直接汇报对象。距离啊,看中公司哪个方面啊。这种问题都是开放性的,积极主动回答,然后态度好一些就行。最后的结果是降薪1k元旦之后上班,我选择接受。


发展建议


最后,我根据自己的情况给大家一些跳槽和找工作的建议。开发人员还是技术为主,但是在公司上班呢要有一定的人情世故。有时候你在的业务线不行,你再厉害也不行。老板是要赚钱的,有钱的时候运气好,你就能拿到高工资。但那不一定是你真正的实力,尤其是公司效益不好的时候。需要考虑自己的下一步走向了。想要留着公司就要稳住主业务线,负责核心功能。不然就需要学习掌握一些其他公司需要的技术了。


普通学历,技术一般先不要想着跳槽了。我失业三周降薪上班的,可见大环境已经是非常的恶劣了。而且遇到的面试要求都是比较高的,大公司学历低的更进不去。还有工作的守住自己的岗位,最好是核心业务。但是如果你是边缘的业务,感觉到可能要被裁了。那么我建议你趁早准备,把自己的时间节省出来学一些技术。一个月就能学会一个小技术,增强你的职场竞争力。


如果你正在找工作。请相信机会是留给有准备的人的。就算年底被裁了,也不要直接就放松下来。因为明年找工作的人也不会少的。现在行情不好了,需要提前有个预期。需要打持久战,找工作需要一个月甚至更久。我们需要针对性的补足一些技术,比如app开发的rn、flutter、桌面端开发。webgl、threejs、canvas大屏可视化。nodejs或者其他后端语言全栈开发。seo,ssr等等。相信一两个月之后你总能掌握一两个技术点。


第一步是优化简历,把自己的优点全部展示出来。比如带过团队,从0到1开发,框架搭建。个人博客网站,github上面的项目等等。然后就要根据公司的需要,哪些是加分项的东西。针对性的学习一下,做个案例。


第二步面试。态度很重要,面试官不会喜欢高傲的面试者的。就算你真的很厉害,他也会问的你回答不上来为止。其实这些问题他也不定会。你就老实回答你所知道的,不会的不要乱扯了,不然就让人觉得你很不稳重的。基础知识稳扎稳打,不建议搜面试题。很多面试官都会问你项目中遇到的难点,怎么解决的。会看你研究问题的深度,是不是那种拿来主义的人。


然后是运气,每天学一点,慢慢积累,机会来了才能把握住得住。


新的一年,祝大家工作顺利!


作者:白筱汐
来源:juejin.cn/post/7318446300591030281
收起阅读 »

Android 当你需要读一个 47M 的 json.gz 文件

ChangeLog 2023/7/19: 修复 Python 序列化 protobuf,Koltin 反序列化,Array 长度不一致的问题。解决方案:统一使用 Koltin 进行序列化和反序列化的操作 使用数据库读取 Array 的数据 补充每种方式读取所...
继续阅读 »

ChangeLog


2023/7/19:



  1. 修复 Python 序列化 protobuf,Koltin 反序列化,Array 长度不一致的问题。解决方案:统一使用 Koltin 进行序列化和反序列化的操作

  2. 使用数据库读取 Array 的数据

  3. 补充每种方式读取所占用的磁盘空间大小


背景


事情是这样的,最近在做一个 emoji-search 的个人 Project,为了减少服务器的搭建及维护工作,我把 emoji 的 embedding 数据放到了本地,即 Android 设备上。这个文件的原始大小为 123M,使用 gzip 压缩之后,大小为 47.1M,文件每行都可以解析成一个 Json 的 Bean。文件的具体内容可以查看该 链接


// 文件行数为:3753 
// embed 向量维度为:1536
{"emoji": "\ud83e\udd47", "message": "1st place medal", "embed": [-0.018469301983714104, -0.004823130089789629, ...]}
{"emoji": "\ud83e\udd48", "message": "2nd place medal", "embed": [-0.023217657580971718, -0.0019081177888438106, ...]}


emoji 的 embedding 数据,记录了每个 emoji 的 token 向量。用来做 emoji 的搜索。将用户输入的 embedding 和 emoji 的 embedding 数据做点积,得到点积较大的 emoji,即用户的搜索结果。



Android 测试机配置如下:



hw.cpu 高通 SDM765G

hw.cpu.ncore 8

hw.device.name OPPO Reno3 Pro 5G

hw.ramSize 8G

image.androidVersion.api 33



小胆尝试


为了方便读取,我将文件放在了 raw 文件夹下,命名为 emoji_embeddings.gz。关键代码如下,这里我将 .gz 文件一次性加载到内存,然后逐行读取。


override suspend fun process(context: Context) = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.emoji_embeddings).use { inputStream ->
GZIPInputStream(inputStream).bufferedReader().use { bufferedReader ->
bufferedReader.readLines().forEachIndexed { index, line ->
val entity = gson.fromJson(line, EmojiJsonEntity::class.java)
// process entity
}
}
}
}

结果可想而知,由于文件比较大,读取文件到内存的时间大概在 13s 左右。


并且在读取的过程中,内存抖动比较严重,这非常影响用户体验。


将文件一次性加载到内存,占用的内存也比较大,大概在 260M 左右,内存紧张的情况下容易出现 OOM。



onPageScrolled


于是,接下来的工作,就是优化内存的使用和减少加载的耗时了。


优化内存使用



  • 逐行加载文件


    很显然,我们最好不要将文件一次性加载到内存中,这样内存占用比较大,容易 OOM,我们可以使用 ReaderuseLines API。类似于这样调用 bufferedReader().useLines{ } ,其原理为 Sequence + reader.readLine() 的实现。再使用 Flow 简单切一下线程,数据读取在 IO Dispatcher,数据处理在 Default Dispatcher。代码如下:


    override suspend fun process(context: Context) = withContext(Dispatchers.Default) {
    flow {
    context.resources.openRawResource(R.raw.emoji_embeddings_json).use { inputStream ->
    GZIPInputStream(inputStream).use { gzipInputStream ->
    gzipInputStream.bufferedReader().useLines { lines ->
    for (line in lines) {
    emit(line)
    }
    }
    }
    }
    }.flowOn(Dispatchers.IO)
    .collect {
    val entity = gson.fromJson(it, EmojiJsonEntity::class.java)
    // process entity
    }
    }

    但这样会导致另一个问题,那就是内存抖动。因为逐行加载到内存中,当前行使用完之后,就会等待 GC,这里暂时无法解决。


    完成之后,加载时的内存可以从 260M 减少到 140M 左右,加载时间控制在 9s 左右。




onPageScrolled



  • 减少内存抖动


    通过查看代码,并使用 Profile 进行调试,我们可以发现,其实主要的 GC 操作频繁,主要是由这行代码导致的: line.toBean<EmojiJsonEntity>() 。这里会存在 EmojiJsonEntity 对象的创建操作,但是 EmojiJsonEntity 只作为中间变量进行存在和使用,所以创建完成之后,就会进行回收。那要怎么解决这个问题呢?


    笔者暂时没找到较好的解法,这里需要保证代码逻辑不过于复杂的同时,消除中间变量的创建。暂时先这样吧😜。有时间可以使用对象池试试。



减少加载耗时



  • 找到最长耗时路径


    测试下来,IO 大概耗时 3.8s,但是总的耗时在 9s。这里我指定了 IO 使用 IO 协程调度器,数据处理使用 Default 协程调度器,IO 和数据处理是并行的。所以总的来说,是数据处理在拖后腿。数据处理主要是这部分代码 line.toBean<EmojiJsonEntity>() 的耗时,使用 Gson 库进行一次 fromJson 的操作。这里我们一步一步来,先来解决 IO 耗时的问题。


  • 加快 IO 操作


    笔者暂时想到了以下两种处理方式:



    1. 单个流分段读取


      在 GZIP 文件中,数据被压缩成连续的块,并且每个块的压缩是相对于前一个块的数据进行的。这就意味我们不能只读取文件的一部分并解压它,因为我们需要前面的数据来正确解码当前的块。所以,对于 GZIP 文件来说,实现分段读取有一些困难。这个想法,暂时先搁置吧。


    2. 多个流分段读取



      • 同一个文件开启多个流


        回到 GZIP 的讨论,同一个文件开启多个流也是徒劳的。因为即使多个线程处理各自的流,然后每个线程处理该文件的一部分,这也需要每个流从头开始对 GZIP 文件进行解压,然后跳过自己无需处理的部分。这么算下来,其实并不能加快总的 IO 速度,同时也会造成 CPU 资源的浪费。


      • 将文件拆分成多个文件之后开启多个流


        考虑这样的一种实现方式:对原有的 GZIP 文件进行拆分,拆分成多个小的 GZIP 文件,使用多线程读取,利用多核 CPU 加快 IO。听起来似乎可行,我们赶紧实现一下:


        override suspend fun process(context: Context) = withContext(Dispatchers.Default) {
        val mutex = Mutex()

        List(STREAM_SIZE) { i ->
        flow {
        val resId = getEmbeddingResId(i) // 获取当前的资源文件 Id
        context.resources.openRawResource(resId).use { inputStream ->
        GZIPInputStream(inputStream).use { gzipInputStream ->
        gzipInputStream.bufferedReader().useLines { lines ->
        for (line in lines) {
        emit(line)
        }
        }
        }
        }
        }.flowOn(Dispatchers.IO)
        }.asFlow()
        .flattenMerge(STREAM_SIZE)
        .collect { data ->
        val entity = gson.fromJson(data, EmojiJsonEntity::class.java)
        mutex.withLock {
        // process entity
        }
        }
        }

        笔者将之前的 json.gz 拆分成了 5 个文件,每个文件启动一个流去加载。之后再将这 5 个流通过 flattenMerge 合并成一个流,来进行数据处理。由于 flattenMerge 有多线程操作,所以这里我们使用协程的 Mutex 加个锁,保证数据操作的原子性。


        实际测试下来,如此操作的 IO 耗时在 2s,缩短为原来的一半,但总的耗时还是稳定在了 9s 左右,这多出来的 2s 具体花在哪里了暂时未知,咱接着优化一下数据处理吧😵‍💫。






    onPageScrolled


  • 缩短数据处理时间的方案分析


    先明确一下需求:我们需要将文件一次性加载到内存中,文件大小为 40M+,其中有每行都有一个 1536 个元素的 float 数组。了解了一圈下来,目前知道的可行的方案有两个,而且大概率需要更换数据结构和存储方式:



    1. 数据库(如 Room):在一些特定的情况下,使用数据库可能会有利,如当我们需要进行复杂查询、更新数据、或者需要随机访问数据的时候。如果需要使用数据库来缩短数据处理时间,那么我们需要在写入时就处理好数据格式,比如当前情况下,我们需要将 Float 数组使用 ByteArray 来存储。然而,在当前需求下,我们的数据相对简单,且只需要进行读操作。而且,我们的数据包含大量的浮点数数组,使用 ByteArray 来存储也会较为复杂。因此,数据库可能不是最理想的选择。但评论区大家对数据库比较看好,所以我们还是用数据库试试。

    2. Protocol Buffers (PB):PB 是一个二进制格式,比文本格式(如 JSON)更紧凑,更快,特别擅长存储和读取大量的数值数据(如 embed 数组)。我们的需求主要是读取数据,并且需要一次性将整个文件加载到内存中。因此,PB 可能是一个不错的选择。虽然 PB 数据不易于阅读和编辑,也不适合需要复杂查询或随机访问的情况。



    onPageScrolled


    如上是 PB 和 Json 序列化和反序列化的对比 ref。可以看到,在一次反序列化操作的情况下, PB 是 Json 的 5 倍。次数越多,差距越大。


    关于为什么二进制文件(PB)会比文本文件(Json) 体积更小,读写更快。这里就不过多赘述了,笔者个人理解,简单来说,是信息密度的差异,具体的大家可以去搜索,了解更多。




  • 使用 Room 存储 embedding 数据


    使用 Room 存储 embedding 数据都是进行一些常规的 CRDU 操作,这里就不赘述了,基本思路就是我们将 Json 数据存储在数据库中,在需要使用的时候,直接读取数据库即可。


    简单贴一下读取的代码:


    override suspend fun process(context: Context) = withContext(Dispatchers.IO) {
    val embeddingDao = getEmbeddingEntityDao(context)
    embeddingDao.queryAll()?.forEachIndexed { index, emojiEmbeddingEntity ->
    // process entity
    }

    Unit
    }

    实在是过于简单了,读取就完事了,多线程由数据库底层来处理。


    值得关注的是关于 Float 数组的存储和读取:


    class EmbeddingEntityConverter {
    @TypeConverter
    fun fromFloatArray(floatArray: FloatArray): ByteArray {
    val byteBuffer = ByteBuffer.allocate(floatArray.size * 4) // Float 是 4 字节
    floatArray.forEach { byteBuffer.putFloat(it) }
    return byteBuffer.array()
    }

    @TypeConverter
    fun toFloatArray(byteArray: ByteArray): FloatArray {
    val byteBuffer = ByteBuffer.wrap(byteArray)
    return FloatArray(byteArray.size / 4) { byteBuffer.float } // Float 是 4 字节
    }
    }

    笔者使用了 Room 的 @TypeConverter 注解,会在存储时,将 FloatArray 转换为 ByteArray 存储到数据库中,读取时,将 ByteArray 转换为 FloatArray 供上层使用。


    数据库读写的效果确实很惊艳,耗时 1.2s,稳定后内存占用 169MB 的样子,而且还不需要我自己处理多线程读写的问题,有点舒服。



    onPageScrolled


  • 使用 Protocol Buffers (PB) 存储 embedding 数据


    PB 文件比 Json 文件的读取要复杂不少,首先我们需要定义一下 proto 文件的格式。


    这里的 repeated float 可以理解成 float 类型的 List


    // emoji_embedding.proto
    syntax = "proto3";

    message EmojiEmbedding {
    string emoji = 1;
    string message = 2;
    repeated float embed = 3;
    }

    定义好之后,就可以进行数据的序列化操作了。值得一提的是,pb.gz 文件是 json.gz 文件的一半大小,只有 18.6M。在数据序列化的时候,笔者使用了 writeDelimitedTo API,该 API 会在写入数据时带上该条数据的长度,方便之后的数据反序列化操作。这里我们直接看一下 Android 反序列化 PB 文件的代码:


    override suspend fun process(context: Context) = withContext(Dispatchers.Default) {
    flow {
    context.resources.openRawResource(R.raw.emoji_embeddings_proto).use { inputStream ->
    GZIPInputStream(inputStream).buffered().use { gzipInputStream ->
    while (true) {
    EmojiEmbeddingOuterClass.EmojiEmbedding.parseDelimitedFrom(gzipInputStream)?.let {
    emit(it)
    } ?: break
    }
    }
    }
    }.flowOn(Dispatchers.IO)
    .buffer()
    .flatMapMerge { byteArray ->
    flow { emit(readEmojiData(byteArray)) }
    }.collect {}
    }

    private fun readEmojiData(entity: EmojiEmbeddingOuterClass.EmojiEmbedding) {
    // process entity
    }

    这里因为有生成的 EmojiEmbeddingOuterClass 代码,所以解析起来还算方便,解析完操作 entity 即可。值得注意的是,我使用 flatMapMerge 来实现多线程处理,而不是使用 launch/async ,这里的目的是减少协程的创建,减少上下文的切换,减少并发数,来提高数据处理的速度。因为实际测试下来,flatMapMerge 的速度会更快。


    那么这么做的实际效果如何呢?1.5s,和数据库读取相差不大。 (这里由于开了 build with Profile,会比实际的慢一点)。稳定下来时,内存占用 129 M。


    onPageScrolled



总结


大文件的读写,咱还是老老实实用字节码文件存储吧。小文件可以使用 Json,反序列化速度够用,可读性也可以有明显的提升。至于是用 PB 还是数据库,可以根据个人喜好及具体的业务场景分析。两者在读写速度上都是没有差别的,但是数据库在内存和磁盘空间上会占用更多。使用 PB 需要自行处理多线程相关问题,难度会较大一点。


具体的性能对比,图表如下:


json.gz + 一次性加载json.gz + 逐行加载拆分 json.gz + 逐行加载数据库加载pb.gz 加载
耗时13s9s9s1.2s1.5s
内存(加载后)260M140M148M169M129M
磁盘占用47.1M47.1M47.1M29.5M18.6M

用到的资源文件:github.com/sunnyswag/e…


源代码可查看:Github


REFERENCE


深入理解gzip原理 - 简书


Protobuf 和 JSON对比分析 - 掘金


Android Studio 配置并使用Protocol Buffer生成java文件 - CSDN博客


作者:很好奇
来源:juejin.cn/post/7253744712409071673
收起阅读 »

同学,请实现一个扫码登录

web
马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦! 即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求...
继续阅读 »

马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦!


即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求,我一看有点来劲了。一来做了多年前端,类似的需求还没有接触过,平时做的多的页面需求和改改bug对自身能力显然是无法提升的。二来扫码登录的功能很多应用都有做过,常见的微信扫码登录,也挺好奇具体如何实现。我大概看了一遍需求文档,写的挺详细的,流程图也标明了各端的交互流程。由于内网开发,产品流程图也忘记截图了,此处在网上找到的一个大概的流程图:
image.png


主要涉及到的是pc端、手机端和后台服务端。由于听产品同事说手机端由原生端(安卓和IOS)来实现,因此我这边只需要开发pc端就行,工作量直接减半有没有。做过该功能的小伙伴肯定了解,pc端的实现还是比较简单的,主要就是开启轮询查询后台扫码状态,然后做对应的提示或登录成功后跳转首页。


扫码登录的需求在前端主要难点在轮询上


0. 什么叫轮询?


所谓的轮询就是,由后端维护某个状态,或是一种连续多篇的数据(如分页、分段),由前端决定按序访问的方式将所有片段依次查询,直到后端给出终止状态的响应(结束状态、分页的最后一页等)。


1. 轮询的方案?


一般有两种解决方案:一种是使用websocket,可以让后端主动推送数据到前端;还有一种是前端主动轮询(上网查了下细分为长轮询和短轮询),通过大家熟悉的定时器(setIntervalsetTimeout)实现。


由于项目暂未用到websocket,且长轮询需要后台配合,所以直接采用短轮询(定时器)开撸了。


遇到的问题:


1、由于看需求文档上交互流程比较清晰,最开始没去网上查找实现方案,自己直接整了一版setInterval的轮询实现。在跟后台联调的过程中发现定时器每1s请求一次接口,发现很多接口没等响应就开启下一次的请求,很多请求都还在pending中,这样是不对的,对性能是很大消耗。于是想了下,可以通过setTimeout来优化,具体就是用setTimeout递归调用方式模拟setInterval的效果,达到只有上一次请求成功后才开启下一次的请求。


// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},

2、在自测了过程中又发现了另外一个问题,stopPolling方法中clearTimeout似乎无法阻止setTimeout的执行,二维码失效后请求仍在不停发出,这就很奇怪了。上网搜索了一番,发现一篇文章(很遗憾,已经找不到是哪篇文章了!)记录了这个问题:大概意思是虽然clearTimeout已经清除了定时器,但此时有请求已经在进行中,导致再次进入了循环体,重新开启了定时器。解决办法就是,需要手动声明一个标识位isStop来阻止循环体的执行。


    stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},

试了下确实达到效果了,但其实这个问题产生的具体原因我还是有些模糊的,希望遇到过相关问题的大佬能指点一下,感激不尽!


3、解决了上面提到的问题,就在以为万事大吉,只待提测的时候。后台同事发现了一个问题(点赞后台同事的尽责之心):他在反复切换登录方式(扫码登录<->账号密码登录)的过程中,发现后台日志有一段时间打印的qrcId不是最新的。然后我这边试了下,确实在切换频率过高时,此时有未完成的请求仍在进行中,导致qrcId被重新赋值了。虽然已经在beforeDestroy里调用了stopPolling清除定时器,但此时请求是未停止的。聪明的小伙伴们肯定想到axioscancelToken可以取消未完成的请求,但我实际也并没有用过,而且项目里也没有可以表演Ctrl+CCtrl+V的地方。于是百度了一番,找到一篇掘友的文章,为了表示尊敬我原封不动的搬到我的代码里了,哈哈!


import axios from "axios";
const CancelToken = axios.CancelToken;

const cancelTokenMixin = {
data() {
return {
cancelToken: null, // cancelToken实例
cancel: null, // cancel方法
};
},
created() {
this.newCancelToken();
},
beforeDestroy() {
//离开页面前清空所有请求
this.cancel("取消请求");
},
methods: {
//创建新CancelToken
newCancelToken() {
this.cancelToken = new CancelToken((c) => {
this.cancel = c;
});
},
},
};
export default cancelTokenMixin;

掘友文章[:](在vue项目中取消axios请求(单个和全局) - 掘金 (juejin.cn))


在组件里引入mixin,另外在请求时传入cancelToken实例,确实达到效果了。此时再次切换登录方式,之前的未完成的请求已被取消,也就无法再篡改qrcId。写到此处,我发现问题2也是未完成的请求导致的,那么是否可以不用isStop标识,直接在stopPolling中调用this.cancel("取消请求");不是更好吗?


完整代码如下:


import sunev from 'sunev'; // 全局公共方法库
import cancelTokenMixin from "@/utils/cancelTokenMixin"; // axios取消请求

export default {
props: {
loginType: {
type: String,
default: 'code'
}
},
mixins: [cancelTokenMixin],
data() {
return {
qrcId: '', // 二维码标识
qrcBase64: '', // 二维码base64图片
macAddr: '', // mac地址
loading: false,
isStop: false,
codeStatus: '0',
qrStatusList: [
{
status: '-1',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码生成失败\n请刷新重试',
refresh: true
},
{ status: '0', icon: '', text: '', refresh: false },
{
status: '1',
icon: 'scan',
color: '#2986ff',
svgClass: 'icon-scan-small',
text: '扫描成功\n请在移动端确认',
refresh: false
},
{
status: '2',
icon: 'confirm',
color: '#2986ff',
svgClass: 'icon-confirm-small',
text: '移动端确认登录',
refresh: false
},
{
status: '3',
icon: 'cancel',
text: '移动端已取消',
refresh: false
},
{
status: '4',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码已失效\n请刷新重试',
refresh: true
},
{
status: '5',
icon: 'success',
color: '#2986ff',
svgClass: 'icon-success-small',
text: '登录成功',
refresh: false
},
{
status: '6',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '登录失败\n请刷新重试',
refresh: true
}
],
errMsg: ''
}
},
async created() {
try {
await this.getQrCode();
this.beginPolling();
} catch(err) {
console.log(err);
}
},
computed: {
// 当前状态
curQrStatus() {
const statusObj = this.qrStatusList.find(item => item.status === this.codeStatus);
if (this.errMsg) {
statusObj.text = this.errMsg;
}
return statusObj;
}
},
methods: {
// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},
// 暂停轮询
stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},
// 获取二维码base64
async getQrCode() {
this.reset();
this.loading = true;
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCGen',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
if (res.qrcId) {
this.qrcId = res.qrcId;
this.qrcBase64 = res.qrcBase64;
} else {
this.stopPolling();
}
} catch(err) {
this.errMsg = err.message;
this.stopPolling();
}
},
// 获取二维码状态
async getQrCodeStatus() {
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCQry',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
return res.status;
} catch(err) {
this.stopPolling();
}
},
// 刷新二维码
async refresh() {
await this.getQrCode();
this.beginPolling();
},
// 切换登录类型
toggle() {
this.$emit('toggleLoginType');
},
// 重置
reset() {
this.isStop = false;
this.codeStatus = '0';
this.errMsg = '';
},
beforeDestroy() {
this.stopPolling();
}
}
}

ps:


1、由于是老项目了,登录界面逻辑较多,避免臃肿,二维码登录拆分成单独组件实现


2、由于项目组在内网开发,以下代码都是一行行重新手打的,不是很重要的html和css部分就省略了


后记:


由于此需求并不着急上线,暂未提测,所以还不知测试同事会提出怎样的bug。另外掘友们如果发现问题,也欢迎批评指正,感激不尽!


作者:wing98
来源:juejin.cn/post/7326268998490865673
收起阅读 »

Android进程间大数据通信:LocalSocket

前言 说起Android进行间通信,大家第一时间会想到AIDL,但是由于Binder机制的限制,AIDL无法传输超大数据。 那么我们如何在进程间传输大数据呢? Android中给我们提供了另外一个机制:LocalSocket 它会在本地创建一个socket通道...
继续阅读 »

前言


说起Android进行间通信,大家第一时间会想到AIDL,但是由于Binder机制的限制,AIDL无法传输超大数据。


那么我们如何在进程间传输大数据呢?


Android中给我们提供了另外一个机制:LocalSocket


它会在本地创建一个socket通道来进行数据传输。


那么它怎么使用?


首先我们需要两个应用:客户端和服务端


服务端初始化


override fun run() {
server = LocalServerSocket("xxxx")
remoteSocket = server?.accept()
...
}

先创建一个LocalServerSocket服务,参数是服务名,注意这个服务名需要唯一,这是两端连接的依据。


然后调用accept函数进行等待客户端连接,这个函数是block线程的,所以例子中另起线程。


当客户端发起连接后,accept就会返回LocalSocket对象,然后就可以进行传输数据了。


客户端初始化


var localSocket = LocalSocket()
localSocket.connect(LocalSocketAddress("xxxx"))

首先创建一个LocalSocket对象


然后创建一个LocalSocketAddress对象,参数是服务名


然后调用connect函数连接到该服务即可。就可以使用这个socket传输数据了。


数据传输


两端的socket对象是一个类,所以两端的发送和接受代码逻辑一致。


通过localSocket.inputStreamlocalSocket.outputStream可以获取到输入输出流,通过对流的读写进行数据传输。


注意,读写流的时候一定要新开线程处理。


因为socket是双向的,所以两端都可以进行收发,即读写


发送数据


var pool = Executors.newSingleThreadExecutor()
var runnable = Runnable {
try {
var out = xxxxSocket.outputStream
out.write(data)
out.flush()
} catch (e: Throwable) {
Log.e("xxx", "xxx", e)
}
}
pool.execute(runnable)

发送数据是主动动作,每次发送都需要另开线程,所以如果是多次,我们需要使用一个线程池来进行管理


如果需要多次发送数据,可以将其进行封装成一个函数


接收数据


接收数据实际上是进行while循环,循环进行读取数据,这个最好在连接成功后就开始,比如客户端


localSocket.connect(LocalSocketAddress("xxx"))
var runnable = Runnable {
while (localSocket.isConnected){
var input = localSocket.inputStream
input.read(data)
...
}
}
Thread(runnable).start()

接收数据实际上是一个while循环不停的进行读取,未读到数据就继续循环,读到数据就进行处理再循环,所以这里只另开一个线程即可,不需要线程池。


传输复杂数据


上面只是简单事例,无法传输复杂数据,如果要传输复杂数据,就需要使用DataInputStreamDataOutputStream


首先需要定义一套协议。


比如定义一个简单的协议:传输的数据分两部分,第一部分是一个int值,表示后面byte数据的长度;第二部分就是byte数据。这样就知道如何进行读写


写数据


var pool = Executors.newSingleThreadExecutor()
var out = DataOutputStream(xxxSocket.outputStream)
var runnable = Runnable {
try {
out.writeInt(data.size)
out.write(data)
out.flush()
} catch (e: Throwable) {
Log.e("xxx", "xxx", e)
}
}
pool.execute(runnable)

读数据


var runnable = Runnable {
var input = DataInputStream(xxxSocket.inputStream)
var outArray = ByteArrayOutputStream()
while (true) {
outArray.reset()
var length = input.readInt()
if(length > 0) {
var buffer = ByteArray(length)
input.read(buffer)
...
}
}

}
Thread(runnable).start()

这样就可以传输复杂数据,不会导致数据错乱。


传输超大数据


上面虽然可以传输复杂数据,但是当我们的数据过大的时候,也会出现问题。


比如传输图片或视频,假设byte数据长度达到1228800,这时我们通过


var buffer = ByteArray(1228800)
input.read(buffer)

无法读取到所有数据,只能读到一部分。而且会造成后面数据的混乱,因为读取位置错位了。


读取的长度大约是65535个字节,这是因为TCP被IP包包着,也会有包大小限制65535。


但是注意!写数据的时候如果数据过大就会自动进行分包,但是读数据的时候如果一次读取貌似无法跨包,这样就导致了上面的结果,只能读一个包,后面的就错乱了。


那么这种超大数据该如何传输呢,我们用循环将其一点点写入,也一点点读出,并根据结果不断的修正偏移。代码:


写入


var pool = Executors.newSingleThreadExecutor()
var out = DataOutputStream(xxxSocket.outputStream)
var runnable = Runnable {
try {
out.writeInt(data.size)
var offset = 0
while ((offset + 1024) <= data.size) {
out.write(data, offset, 1024)
offset += 1024
}
out.write(data, offset, data.size - offset)
out.flush()
} catch (e: Throwable) {
Log.e("xxxx", "xxxx", e)
}

}

pool.execute(runnable)

读取


var input = DataInputStream(xxxSocket.inputStream)
var runnable = Runnable {
var outArray = ByteArrayOutputStream()
while (true) {
outArray.reset()
var length = input.readInt()
if(length > 0) {
var buffer = ByteArray(1024)
var total = 0
while (total + 1024 <= length) {
var count = input.read(buffer)
outArray.write(buffer, 0, count)
total += count
}
var buffer2 = ByteArray(length - total)
input.read(buffer2)
outArray.write(buffer2)
var result = outArray.toByteArray()
...
}
}
}
Thread(runnable).start()

这样可以避免因为分包而导致读取的长度不匹配的问题


作者:BennuCTech
来源:juejin.cn/post/7215100409169625148
收起阅读 »

如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展

本文分享自天翼云开发者社区《如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展》,作者:c****w在当今的互联网时代,微服务架构已经成为许多企业选择的架构模式,它能够提高系统的灵活性、可维护性和可扩展性。然而,微服务架构下的高可用性和弹性扩展是一个复杂...
继续阅读 »

本文分享自天翼云开发者社区《如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展作者:c****w

在当今的互联网时代,微服务架构已经成为许多企业选择的架构模式,它能够提高系统的灵活性、可维护性和可扩展性。然而,微服务架构下的高可用性和弹性扩展是一个复杂的挑战。本文将介绍如何利用容器与中间件来实现微服务架构下的高可用性和弹性扩展的解决方案。

1. 理解微服务架构下的高可用性和弹性扩展需求

在微服务架构中,系统由多个微小的服务组成,每个服务都是一个独立的单元,可以独立部署和扩展。因此,要实现高可用性和弹性扩展,需要考虑以下几个方面:

· 服务的自动发现和注册

· 服务的负载均衡和容错处理

· 弹性扩展和自动伸缩

· 故障自愈和自动恢复

2. 利用容器实现微服务的高可用性

容器技术如Docker和Kubernetes可以帮助我们实现微服务的高可用性。首先,我们可以将每个微服务打包成一个独立的容器镜像,然后使用Kubernetes进行容器编排和调度。Kubernetes可以自动监控容器的健康状态,并在发生故障时自动进行容器的重启,从而保证微服务的高可用性。此外,Kubernetes还支持多种负载均衡和服务发现的机制,可以确保请求能够被正确路由到可用的服务实例上。

3. 中间件的应用实现微服务的弹性扩展

在微服务架构中,服务的请求量可能会有很大的波动,因此需要实现弹性扩展来应对高峰时期的流量。这时候,可以利用中间件来实现微服务的弹性扩展。比如,可以使用消息队列来实现异步处理,将请求发送到消息队列中,然后由多个消费者并发处理请求。这样可以有效地应对流量的波动,提高系统的弹性。

4. 实现自动化的监控和故障处理

为了保证微服务架构的高可用性和弹性扩展,需要实现自动化的监控和故障处理机制。可以利用监控系统来实时监控微服务的健康状态和性能指标,一旦发现故障,可以自动触发故障处理流程,比如自动进行容器的重启或者自动进行服务实例的扩展。这样可以大大提高系统的自愈能力,保证系统的高可用性。

结论

通过利用容器和中间件,我们可以很好地实现微服务架构下的高可用性和弹性扩展。容器技术可以帮助我们实现微服务的高可用性,而中间件可以帮助我们实现微服务的弹性扩展。通过自动化的监控和故障处理机制,可以保证系统的高可用性,从而更好地满足业务需求。

希望以上内容能够帮助您更好地理解如何利用容器与中间件实现微服务架构下的高可用性和弹性扩展。

收起阅读 »

前端如何统一开发环境

web
统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。 nodejs 首先推荐使用 fnm 管理多版本 nodejs。 对比 nvm: 支持 brew 安装,...
继续阅读 »

统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。


nodejs


首先推荐使用 fnm 管理多版本 nodejs。


对比 nvm



  • 支持 brew 安装,更新方便

  • 跨平台,windows 也能用 winget 安装


使用 fnm 一定要记得开启根据当前 .nvmrc 自动切换对应的 nodejs 版本,也就是在在 .zshrc 中加入:


eval "$(fnm env --use-on-cd)"

包管理器


尽管 npm 一直在进步,甚至 7.x 已经原生支持了 workspace。但是我钟爱 pnpm,理由:



  • 安全,避免幽灵依赖,不会将依赖的依赖平铺到 node_modules 下

  • 快,基于软/硬链接,node_modules 下是软连接,硬链接到 .pnpm 文件夹下的硬链接

  • 省磁盘,公司配的 mac 只有 256G

  • pnpm 的可配置性很强,配置不够用还可以用 .pnpmfile.js 编写 hooks

  • yarn2 node_modules 都看不到,分析依赖太麻烦了

  • 公司用的 vue,而 vue3/vite 用 pnpm(政治正确)


推荐使用 Corepack 管理用户的包管理器,其实我一开始知道有 corepack 这个 nodejs 官方的东西的时候,我就在想:为啥不叫 npmm(node package manager manager) 呢?


corepack 目前官方觉得功能没稳定,所以默认没开启,需要用户通过 corepack enable 手动开启,相关的讨论:enable corepack by default


有了 corepack 我们就可以轻松的在 npm/yarn/pnpm 中切换,安装和更新不同的版本。还有一个非常方便的特性就是通过在 package.json 中声明 packageManager 字段例如 "pnpm@8.14.1",当我们开启了 corepack,cd 到该 package.json 所在的 package 的时候,运行 pnpmcorepack 会使用 8.14.1 版本的 pnpm


corepack 是怎样做到的呢?nodejs 安装文件夹有个的 bin 目录,这个目录会被添加到 path 环境变量,其中包含了 corepack 以及 corepack 支持的包管理器的可执行文件:


❯ tree ../../Library/Caches/fnm_multishells/17992_1705553706619/bin
../../Library/Caches/fnm_multishells/17992_1705553706619/bin
├── corepack -> ../lib/node_modules/corepack/dist/corepack.js
├── node
├── npm -> ../lib/node_modules/npm/bin/npm-cli.js
├── npx -> ../lib/node_modules/npm/bin/npx-cli.js
├── pnpm -> ../lib/node_modules/corepack/dist/pnpm.js
├── pnpx -> ../lib/node_modules/corepack/dist/pnpx.js
├── yarn -> ../lib/node_modules/corepack/dist/yarn.js
└── yarnpkg -> ../lib/node_modules/corepack/dist/yarnpkg.js

可以看到 pnpm 被链接到了 corepack 的一个 js 文件,查看 corepack/dist/pnpm.js 内容:


#!/usr/bin/env node
require('./lib/corepack.cjs').runMain(['pnpm', ...process.argv.slice(2)]);

可以看到其实 corepack 相当于劫持了 pnpmyarn 命令,然后根据 packageManager 字段配置自动切换到对应的包管理器,如果已经安装过就使用缓存,没有就下载。


怎样统一 nodejs 和 包管理器


问题


虽然我在项目中配置了 .nvmrc 文件,在 package.json 中声明了 packageManager 字段,但是用户可能没有安装 fnm 以及配置根据 .nvmrc 自动切换对应的 nodejs,还有可能没有开启 corepack,所以同事的环境还是有可能和要求的不一致。我一向认为,不应该依靠人的自觉去遵守规范,通过工具强制去约束才能提前发现问题和避免争论。


解决办法


最开始是看到项目中使用了 only-allow 用于限制同事开发时只能用 pnpm,由此我引发了我一个灵感,为什么我不干脆把事情做绝一点,把 nodejs 的版本也给统一了


于是我写了一个脚本用于检查用户本地的 nodejs 的版本,包管理器的版本必须和要求一致。最近封装成了一个 cli:check-fe-env。使用方式很简单,增加一个 preinstall script:


{
"scripts": {
"preinstall": "npx check-fe-env"
}
}

工作原理



  • 用户在运行 pnpm install 之后,install 依赖之前,包管理器会执行 preinstall 脚本

  • cli 会检测:

    • 用户当前环境的 nodejs 版本和 .nvmrc 中声明的是否一样

    • 用户当前使用的包管理器种类和版本是否和 package.jsonpackageManager 字段一样




获取当前环境的 nodejs 版本很简单,可以用 process.version。想要获取执行脚本时的包管理器可以通过环境变量:process.env.npm_config_user_agent,如果一个 npm script 是通过 pnpm 运行的,那么这个环境变量会被设置为例如 pnpm/8.14.1 npm/? node/v20.11.0 darwin arm64,由此我们可以获取当前使用的包管理器和版本。


为了加快安装速度,我特意把源码和相关依赖给一起打包了,整个 bundle 大小 8k 左右。


局限性


最新的 npmpnpm 目前貌似都有一个 bug,都是安装完依赖才执行 preinstall hooks,具体看这: Preinstall script runs after installing dependencies


这个方案对于 monorepo 项目或者说不需要发包的项目是没啥问题的,但是不适用于一个要发包的项目。原因是 preinstall script 除了会在本地 pnpm install 时执行,别人安装这个包,也会执行这个 preinstall script,就和 vue-demi 用的 postinstall script 一样。主要是确实没找到一个:只会在本地运行 pnpm install 后且在安装依赖前执行的 hook。


作者:余腾靖
来源:juejin.cn/post/7325069743143878697
收起阅读 »

一万八千条线程,线程为啥释放不了?

一万八千条线程,线程为啥释放不了?大家好,我是魔性的茶叶,今天和大家带来的是我在公司里面排查的另一个性能问题的过程和结果,相当有意思,分享给大家,为大家以后有可能的排查增加一些些思路。当然,最重要的是排查出来问题,解决问题的成就感和解决问题的快乐,拽句英文,那...
继续阅读 »

一万八千条线程,线程为啥释放不了?

大家好,我是魔性的茶叶,今天和大家带来的是我在公司里面排查的另一个性能问题的过程和结果,相当有意思,分享给大家,为大家以后有可能的排查增加一些些思路。当然,最重要的是排查出来问题,解决问题的成就感和解决问题的快乐,拽句英文,那就是 its all about fun。

噢对了,谢绝没有同意的转载。

事情发生在某个艳阳高照的下午,我正在一遍打瞌睡一边写无聊的curd。坐在我身边的郑网友突然神秘一笑。 "有个你会感兴趣的东西,要不要看看",他笑着说,脸上带着自信揣测掌握我的表情。

我还以为他准备说啥点杯奶茶,最近有啥有意思的游戏,放在平时我可能确实感兴趣,可是昨天晚上我凌晨二点才睡,中午休息时间又被某个无良领导叫去加班,困得想死,现在只想赶紧码完代回家睡觉。

"没兴趣",我说。他脸上的表情就像被一只臭皮鞋梗住了喉咙,当然那只臭皮鞋大概率是我。

"可是这是之前隔壁部门那个很多线程的问题,隔壁部门来找我们了",他强调了下。

"噢!是吗,那我确实有兴趣",我一下子来了精神,趴过去看他的屏幕。屏幕上面是他和隔壁部门的聊天,隔壁部门的同事说他们看了比较久时间都找不到问题,找我们部门看看。让我臊的不行的是这货居然直接还没看问题,就开始打包票,说什么"我们部门是排查这种性能问题的行家"这种高斯林看了都会脸红的话。

"不是说没兴趣吗?"他嘿嘿一笑。我尬笑了一下,这个问题确实纠结我很久了,因为一个星期前运维同事把隔壁部门的应用告警发到了公共群,一下子就吸引到了我:

image-20230812225219774

这个实例的线程数去到差不多两万(对,就是两万,你没看错)的线程数量,1w9的线程处于runnable状态。说实话,这个确实挺吸引我的 ,我还悄悄地地去下载了线程快照,但是这是个棘手的问题,只看线程快照完全看不出来,因为gitlab的权限问题我没有隔壁部门的代码,所以只能作罢。但是这个问题就如我的眼中钉,拉起了我的好奇心,我隔一会就想起这个问题,我整天都在想怎么会导致这么多条线程,还有就是jvm真的扛得住这么多条线程?

正好这次隔壁部门找到我们,那就奉旨除bug,顺便解决我的困惑。

等待代码下拉的过程,我打开skywalking观察这个应用的状态。这次倒没到一万八千条线程,因为找不到为啥线程数量这么多的原因,每次jvm快被线程数量撑破的时候运维就重启一遍,所以这次只有接近6000条,哈哈。

image-20230812232110511

可以看到应用的线程在一天内保持增加的状态,而且是一直增加的趋势。应用没有fgc,只有ygc,配合服务的调用数量很低,tomcat几乎没有繁忙线程来看并不是突发流量。jvm的cpu居高不下,很正常,因为线程太多,僧多粥少的抢占时间片,不高才怪。

拿下线程快照导入,导入imb analyzer tool查看线程快照。

直接看最可疑的地方,有1w9千条的线程都处于runnbale线程,并且都有相同的堆栈,也就是说,大概率是同一段代码产生的线程:

image-20230817100520850

这些线程的名字都以I/O dispatcher 开头,翻译成中文就是io分配者,说实话出现在dubbo应用里面我是一点都不意外,可是我们这是springmvc应用,这个代码堆栈看上去比较像一种io多路轮询的任务,用人话说就是一种异步任务,不能找到是哪里产生的这种线程。说实话这个线程名也比较大众,网上一搜一大把,也没啥一看就能定位到的问题。

这种堆栈全是源码没有一点业务代码堆栈的问题最难找了。

我继续往下看线程,试图再找一点线索。接着我找到了大量以pool-命名开头的线程,虽然没有1w9千条这么多,也是实打实几百条:

image-20230813000451059

这两条线程的堆栈很相近,都是一个类里面的东西,直觉告诉我是同一个问题导致的。看到这个pool开头,我第一个反应是有人用了类似new fixThreadPool()这种api,这种api新建出来的线程池因为没有自定义threadFactory,导致建立出来的线程都是pool开头的名字。

于是我在代码中全局搜索pool这个单词,想检查下项目中的线程池是否设置有误:

image-20230817093407354

咦,这不是刚刚看到的堆栈里面的东西吗。虽然不能非常确定是不是这里,但是点进去看看又不会掉块肉。

这是个工具类,我直接把代码拷过来:

private static class HttpHelperAsyncClient {
private CloseableHttpAsyncClient httpClient;
private PoolingNHttpClientConnectionManager cm;
private HttpHelperAsyncClient() {}
private DefaultConnectingIOReactor ioReactor;
private static HttpHelperAsyncClient instance;
private Logger logger = LoggerFactory.getLogger(HttpHelperAsyncClient.class);
   

public static HttpHelperAsyncClient getInstance() {

instance = HttpHelperAsyncClientHolder.instance;
try {
instance.init();
} catch (Exception e) {
                   
}
return instance;
}

private void init() throws Exception {

ioReactor = new DefaultConnectingIOReactor();
ioReactor.setExceptionHandler(new IOReactorExceptionHandler() {
public boolean handle(IOException ex) {
       if (ex instanceof BindException) {
           return true;
      }
       return false;
  }
public boolean handle(RuntimeException ex) {
       if (ex instanceof UnsupportedOperationException) {
           return true;
      }
       return false;
  }
});

cm=new PoolingNHttpClientConnectionManager(ioReactor);
cm.setMaxTotal(MAX_TOTEL);
cm.setDefaultMaxPerRoute(MAX_CONNECTION_PER_ROUTE);
httpClient = HttpAsyncClients.custom()
.addInterceptorFirst(new HttpRequestInterceptor() {

                   public void process(
                           final HttpRequest request,
                           final HttpContext context)
throws HttpException, IOException {
                       if (!request.containsHeader("Accept-Encoding")) {
                           request.addHeader("Accept-Encoding", "gzip");
                      }
                  }}).addInterceptorFirst(new HttpResponseInterceptor() {

                   public void process(
                           final HttpResponse response,
                           final HttpContext context)
throws HttpException, IOException {

                       HttpEntity entity = response.getEntity();
                       if (entity != null) {
                           Header ceheader = entity.getContentEncoding();
                           if (ceheader != null) {
                               HeaderElement[] codecs = ceheader.getElements();
                               for (int i = 0; i < codecs.length; i++) {
                                   if (codecs[i].getName().equalsIgnoreCase("gzip")) {
                                       response.setEntity(
                                               new GzipDecompressingEntity(response.getEntity()));
                                       return;
                                  }
                              }
                          }
                      }
                  }
              })
              .setConnectionManager(cm)
              .build();
httpClient.start();
  }




private Response execute(HttpUriRequest request, long timeoutmillis) throws Exception {
       HttpEntity entity = null;
       Future rsp = null;
       Response respObject=new Response();
       //default error code
       respObject.setCode(400);
       if (request == null) {
      closeClient(httpClient);
      return respObject;
      }

       try{
      if(httpClient == null){
      StringBuilder sbuilder=new StringBuilder();
          sbuilder.append("\n{").append(request.getURI().toString()).append("}\nreturn error "
          + "{HttpHelperAsync.httpClient 获取异常!}");
          System.out.println(sbuilder.toString());
          respObject.setError(sbuilder.toString());
      return respObject;
      }
      rsp = httpClient.execute(request, null);
      HttpResponse resp = null;
      if(timeoutmillis > 0){
      resp = rsp.get(timeoutmillis,TimeUnit.MILLISECONDS);
      }else{
      resp = rsp.get(DEFAULT_ASYNC_TIME_OUT,TimeUnit.MILLISECONDS);
      }
      System.out.println("获取返回值的resp----->"+resp);
           entity = resp.getEntity();
           StatusLine statusLine = resp.getStatusLine();
           respObject.setCode(statusLine.getStatusCode());
           System.out.println("Response:");
           System.out.println(statusLine.toString());
           headerLog(resp);
           String result = new String();
           if (respObject.getCode() == 200) {
               String encoding = ("" + resp.getFirstHeader("Content-Encoding")).toLowerCase();
               if (encoding.indexOf("gzip") > 0) {
                   entity = new GzipDecompressingEntity(entity);
              }
               result = new String(EntityUtils.toByteArray(entity),UTF8);
               respObject.setContent(result);
          } else {
          StringBuilder sbuilder=new StringBuilder();
          sbuilder.append("\n{").append(request.getURI().toString()).append("}\nreturn error "
          + "{").append(resp.getStatusLine().getStatusCode()).append("}");
          System.out.println(sbuilder.toString());
          try {
          result = new String(EntityUtils.toByteArray(entity),UTF8);
          respObject.setError(result);
          } catch(Exception e) {
          logger.error(e.getMessage(), e);
          result = e.getMessage();
          }
          }
           System.out.println(result);

      } catch (Exception e) {
      logger.error("httpClient.execute异常", e);
} finally {
           EntityUtils.consumeQuietly(entity);
           System.out.println("执行finally中的 closeClient(httpClient)");
           closeClient(httpClient);
      }
       return respObject;
  }
   
       private static void closeClient(CloseableHttpAsyncClient httpClient) {

           if (httpClient != null) {
               try {
                   httpClient.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }
      }
}

这段代码里面用到了CloseableHttpAsyncClient的api,我大概的查了下这个玩意,这个应该是一个异步的httpClient,作用就是用于执行一些不需要立刻收到回复的http请求,CloseableHttpAsyncClient就是用来帮你管理异步化的这些http的请求的。

代码里面是这么调用这个类的:

HttpHelperAsyncClient.getInstance().execute(request, timeoutMillis)

捋一下逻辑,就是通过HttpHelperAsyncClient.getInstance()拿到HttpHelperAsyncClient的实例,然后在excute方法里面执行请求并且释放httpClient对象。按我的理解,其实就是一个httpClient的工具类

我直接把代码拷贝出来,试图复现一下,直接在mian方法进行一个无限循环的调用

while (true){
post("https://www.baidu.com",new Headers(),new HashMap<>(),0);
}

从idea直接拿一份dump:

image-20230814180513126

耶?怎么和我想的不一样,只有一条主线程,并没有复现上万线程的壮观。

就在我懵逼的时候,旁边的郑网友开口了:"你要不要试试多线程调用,这个请求很有可能从tomcat进来的"。

有道理,我迅速撸出来一个多线程调用的demo:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,100,TimeUnit.DAYS,new ArrayBlockingQueue<>(100));
       while (true) {
           Thread.sleep(100);
           threadPoolExecutor.execute(new Runnable() {
               @Override
               public void run() {

                   try {

                       post("https://www.baidu.com", new Headers(), new JSONObject(), 0);
                  } catch (Exception e) {
                       throw new RuntimeException(e);
                  }
              }
          });
      }

因为线程涨的太猛,这次idea都没办法拿下线程快照,我借助JvisualVM监控应用状态,线程数目如同脱缰的野马, 迅速的涨了起来,并且确实是I/O dispatcher线程居多

image-20230816221807385

到这里,基本能说明问题就出现在这里。我们再深究一下。

可能有的朋友已经发现了,HttpHelperAsyncClient类中的httpclient是线程不安全的,这个HttpHelperAsyncClient这个类里面有个httpclient的类对象变量,每次请求都会new一个新的httpclient赋值到类对象httpclient中,在excute方法执行完会调用closeClient()方法释放httpclient对象,但是closeClient的入参直接从类的成员对象中取,这就有可能导致并发问题。

简单的画个图解释下:

image-20230816224815066

  1. http-1-thread调用方法init()把类变量httpclient设置为自己的实例对象,http-1-client
  2. 此时紧接着http-2-thread进来,调用方法init()把类变量httpclient设置为自己的实例对象,http-2-client
  3. 接着http-1-thread执行完请求,调用closeHttpclient()方法释放httpclient,但是因为http-2线程已经设置过类变量,所以它释放的是http-2-client
  4. http-2-thread执行完请求,也去调用closeHttpClient()方法释放httpclient,但是大概率会因为http-2-client已经释放过报错

    不管http-2-client如何,http-1-client是完完全全的被忘记了,得不到释放,于是他们无止境的堆积了起来。

    如何解决呢?其实也很简单,这里httpclient对象其实是属于逃逸了,我们把它变回成局部变量,就可以解决这个问题,在不影响大部分的代码情况下,我们把生成httpclient的代码从HttpHelperAsyncClient.getInstance()移动到execute()中,并且在释放资源的地方传入局部变量而不是类变量:

    private CloseableHttpAsyncClient init() throws Exception {

    //省略部分代码
    httpClient.start();
      //现在init方法返回CloseableHttpAsyncClient
    return httpClient;
      }
    private Response execute(HttpUriRequest request, long timeoutmillis) throws Exception {
           //省略部分代码
      //改动在这里 client直接new出来
       CloseableHttpAsyncClient httpClient = init();
    //省略部分代码
       
               closeClient(httpClient);
           //省略部分代码
      }

    经过改造后的代码升级后登录skywalking查看效果:

    image-20230816230729842

可以看到线程数量恢复成了180条,并且三天内都没有增加,比之前一天内增加到6000条好多了。也就是区区一百倍的优化,哈哈。

总结

其实这个算比较低级的错误,很简单的并发问题,但是一不注意就容易写出来。但是排查难度挺高的,因为大量的线程都是没有我们一点业务代码堆栈,根本不知道线程是从哪里创建出来的,和以往的排查方法算是完全不同。这次是属于运气爆棚然后找到的代码,排查完问题我也想过,有没有其他的方法来定位这么多相同的线程是从哪里创建出来的呢?我试着用内存快照去定位,确实有一点线索,但是这属于是马后炮了,是我先读过源码才知道内存快照可以定位到问题,有点从结果来推过程的意思,没啥好说的。

总而言之,在定义这种敏感资源(文件流,各种client)时,我们一定要注意并发创建及释放资源的问题,变量能不逃逸就不逃逸,最好是局部变量。


作者:魔性的茶叶
来源:juejin.cn/post/7268049978928611347VV
收起阅读 »

我们领证啦

是的,我们领证了。在跟她经历2年时间的相处后,我们在今天2024年1月5日正式办理了结婚登记。# 我是如何找到老婆的 其实这次还是有那么一点点波折的,因为外地人无法在上海直接领结婚证,但是这个日子是我爷爷请算命先生帮我们看好的,所以我们决定回到我的老家湖北十堰...
继续阅读 »

是的,我们领证了。在跟她经历2年时间的相处后,我们在今天2024年1月5日正式办理了结婚登记。# 我是如何找到老婆的


其实这次还是有那么一点点波折的,因为外地人无法在上海直接领结婚证,但是这个日子是我爷爷请算命先生帮我们看好的,所以我们决定回到我的老家湖北十堰办理结婚登记。


今天请了一天假,考虑到怕一些突发事件,因为我们同省不同市,我怕还要什么证明,我们选择了坐飞机,预留一些时间,比如资料不齐要补资料什么的。因为6点20的飞机,我们定了4.的闹钟,但是凌晨一点半我就醒了,然后一直睡不着,可能是有点小激动的缘故吧。没等闹钟响,我们3点50分起床,煮了2个鸡蛋,带了2盒酸奶,烧了一壶开水装了一杯就匆匆出发了,昨晚预定的出租车4点20也准时到了。到了机场安检才发现不能自带水,酸奶也得喝掉,因为好几年没有坐过飞机了,竟然连这都不知道😂。6点20的飞机,因为晚点,等了一会,大概6点30就起飞了,还好还好,早晨9点就到了武当山机场,晚出发,提前达,这也是可以了。


然后我们打车到民政局,这里有一点小波折,地图一搜随便挑了个,到那发现门口竖了一个牌子,民政局换址了。


图片


我们没办法,只能坐公交去牌子上面民政局的新地址:蓝山郡。到了那里,发现那里是市政府一带,找了好一会才得知,在一个大排档旁边上去的二楼,终于找到了张湾区民政局,忘记拍了,反正非常小的一个门面,仿佛生怕别人找到似的😂。


进了大厅我们发现此时里面只有我们办理,我本来还怕排队。办理的小姐姐人很好,很细心,业务也很熟练,我们提供身-份-证、沪口本、3张照片,期间我们填了2张表,签名,按了6个手印,大概10分钟就办好了。


图片


办理期间我们全程没有表露出很兴奋的表情,以至于出民政局时,我在想当时应该面露开心一点,我甚至觉得自己没有表现好。不过这些都不重要了,此时我们很开心,我们一起走出大厅,我们觉得我们俩此刻是最幸福的人。


总的来看,此次回老家办理结婚登记,整个过程还是挺顺利的。


最后,祝天下有情人终成眷属,希望大家龙年行大运!


作者:大数据技术派
来源:juejin.cn/post/7322355350921461800
收起阅读 »

Hutool:WeakCache导致的内存泄漏

闲聊 感谢各位居然有生之年上了一次榜单。没想到一次bug定位这么火,身为电商网站的后台开发,别的不敢说,jvm调优啊,bug定位啊,sql调优啊简直是家(ri)常(chang)便(chan)饭(shi)。后续也会努力给大家带来更多文章的 就在上篇文章发了没...
继续阅读 »

闲聊



感谢各位居然有生之年上了一次榜单。没想到一次bug定位这么火,身为电商网站的后台开发,别的不敢说,jvm调优啊,bug定位啊,sql调优啊简直是家(ri)常(chang)便(chan)饭(shi)。后续也会努力给大家带来更多文章的
image.png
就在上篇文章发了没几天,生产又出问题了,一台服务cpu使用率飙到20%以上



查看gc日志发现,fullgc频繁,通过jstat排查,并没有释放多少内存【当时我再外面没有图】


通过dump出来的内存分析,是hutool的WeakCache导致的,涉及业务逻辑修改,就不透露解决方案了,下面为大家分析下为啥会内存泄漏。


问题分析


WeakHashMap


「前置知识」之前写过一篇强软弱虚分析,感兴趣的可以点击看下。


我粗略的看了下,介不是弱引用吗,怎么会内存泄漏呢


「启动参数设置」-Xms50m -Xmx50m -XX:+PrintGCDetails不嫌麻烦可以调大一点




这个是没问题的,不会发生OOM





WeakCache


下面有请下一位参赛选手WeakCache

凭借我一次次手点,发现,根本不回收,cacheMap不也是WeakHashMap咋不回收呢


搜了下issue,果然有人提过了,


「原文链接」 gitee.com/dromara/hut…




那么我们来实验下,把CacheObj拷贝出来,强制走我的



问题得到了解决,dalao牛逼


既然不会删除,那是什么时候删除的呢?


是类似于懒删。




彩蛋


那么这行代码是怎么存在这么久而不出问题的


image.png


不在那天爬的紫金山=。=
image.png


作者:山间小僧
来源:juejin.cn/post/7267445093836128314
收起阅读 »

Android MVI框架搭建与使用

前言   有一段时间没有去写过框架了,最近新的框架MVI,其实出来有一段时间了,只不过大部分项目还没有切换过去,对于公司的老项目来说,之前的MVC、MVP也能用,没有替换的必要,而对于新建的项目来说还是可以替换成功MVVM、MVI等框架的。本文完成后的效果图:...
继续阅读 »

前言


  有一段时间没有去写过框架了,最近新的框架MVI,其实出来有一段时间了,只不过大部分项目还没有切换过去,对于公司的老项目来说,之前的MVC、MVP也能用,没有替换的必要,而对于新建的项目来说还是可以替换成功MVVM、MVI等框架的。本文完成后的效果图:


在这里插入图片描述


正文


  每当一个新的框架出来,都会解决掉上一个框架所存在的问题,但同时也会产生新的问题,瑕不掩瑜,可以在实际开发中,解决掉产生的问题,就能够更好的使用框架,那么MVI解决了MVVM的什么问题呢?


  MVI同样是基于观察者模式,只不过数据通信方面是单向的,解决了MVVM双向通信所带来的问题,实际上MVVM也能做成单向通讯,但是这样就不是纯粹的MVVM,当然了,仁者见仁,智者见智。MVI框架适用于UI变化很多的项目,通过数据去驱动UI,MVI就是Model、View、Intent。



  • Model 这里的Model有所不同,里面还包含UI的状态。

  • View 还是视图,例如Activity、Fragment等。

  • Intent 意图,这个和Activity的意图要区分开,我觉得说成是行为可能更妥当,表示去做什么。


多说无益,我们还是进入实操环节吧。


一、创建项目


首先创建一个名为MviDemo的项目


在这里插入图片描述


项目创建好了,下面我们需要先进行项目的基本配置。


① 配置AndroidManifest.xml


  文章中会通过一个网络API接口,拿到数据来进行MVI框架的搭建与使用,接口地址如下:


http://service.picasso.adesk.com/v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot

通过浏览器打开可以得到很多数据,如图所示:


在这里插入图片描述


  这些数据都是JSON格式的,后面我们还会用到这些数据。因为接口使用的是http,而不是https,所以在xml文件夹下新建一个network_security_config.xml,代码如下:


<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

然后在AndroidManifest.xml中的application标签中配置它,如图所示:


在这里插入图片描述


  从Android 9.0起,默认使用https进行网络访问,如果要进行http访问则需要添加这个配置。还需要添加一个网络访问静态权限:


<uses-permission android:name="android.permission.INTERNET"/>

添加位置如下图所示:


在这里插入图片描述


项目正常搭建还需要一些依赖库和其他的一些设置,下面我们配置app模块下的build.gradle。


② 配置app的build.gradle


  请注意,这里是配置app的build.gradle,而不是项目的build.gradle,很多人会配置错误,所以我再次强调一下,将你的项目切换到Android模式,如下图所示:


在这里插入图片描述


  这里我标注了一下,你看到有两个build.gradle文件,两个文件的后面有灰色的文字说明,就很清楚的知道这两个build.gradle分别是项目和模块的。下面打开app模块下的build.gradle,在里面找到dependencies{}闭包,闭包中添加如下依赖:


    // lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
//glide
implementation 'com.github.bumptech.glide:glide:4.14.2'
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//retrofit moshi
implementation "com.squareup.retrofit2:converter-moshi:2.6.2"
//moshi used KotlinJsonAdapterFactory
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"

添加位置如下图所示:


在这里插入图片描述


然后再打开viewBinding,在android{}闭包下添加如下代码:


    buildFeatures {
viewBinding true
}

添加位置如下图所示:


在这里插入图片描述


  添加之后你会看到右上角有一个Sync Now,点击它进行依赖的载入配置,配置好之后进入下一步,为了确保你的项目没有问题,你可以现在运行一下看看。


二、网络请求


  当我们使用Kotlin时,网络访问就变得更简单了,只需要Retrofit和协程即可,首先我们在com.llw.mvidemo包下新建一个data包,然后在data包下新建一个model包,model包下我们可以通过刚才使用网页访问API拿到的JSON数据来生成一个数据类。


① 生成数据类


生成数据类,这里我们可以使用一个插件,搜索JSON To Kotlin Class,如下图所示:


在这里插入图片描述


  下载安装之后,如果需要重启,你就重启AS,重启之后,右键点击model → New → Kotlin data class File from JSON,如图所示:


在这里插入图片描述


在出现的弹窗中复制通过网页请求得到的JSON数据字符串,如图所示:


在这里插入图片描述


  这里如果觉得看起来不舒服,点击 Format 进行JSON数据格式化,然后我们需要设置数据类的名称,这里输入Wallpaper,因为我们需要使用Moshi,将JSON数据直接转成数据类,所以这里我们点击Advanced,如图所示:


在这里插入图片描述


  这里默认是,选择MoShi(Reflect),其他的不用更改,点击OK,此弹窗关闭,回到之前的弹窗,然后点击 Generate 生成数据类,你会发现有三个数据类,分别是Wallpaper、Res和Vertical,我们看一下Wallpaper的代码:


package com.llw.mvidemo.data.model

import com.squareup.moshi.Json

data class Wallpaper(
@Json(name = "code")
val code: Int,
@Json(name = "msg")
val msg: String,
@Json(name = "res")
val res: Res
)

  这里每一个字段上都有一个@Json注解,这里是MoShi依赖库的注解,主要检查一下导包的问题,这里还有一个小故事,Google 的Gson库,算是推出比较早的,从事Gson库的开发人员,后面离职去了Square,也就是OkHttp、Retrofit的开发者。Retrofit一开始是支持Gson转换的,后面增加了MoShi的转换,Moshi拥有出色的Kotlin支持以及编译时代码生成功能,可以使应用程序更快更小。这个故事我也是听说的,你可以自己去求证,下面继续。


② 接口类


  现在数据类有了,那么我们就需要根据这个数据类来写一个接口类,在com.llw.mvidemo包下新建一个network包,network包下创建一个接口类ApiService,代码如下所示:


interface ApiService {

/**
* 获取壁纸
*/

@GET("v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot")
suspend fun getWallPaper(): Wallpaper
}

这里属于Retrofit的使用方式,增加了协程的使用而已,就取代了RxJava的线程调度。


③ 网络请求工具类


现在有接口,下面我们来做网络请求,在network包下新建一个NetworkUtils类,代码如下:


package com.llw.mvidemo.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

/**
* 网络工具类
*/

object NetworkUtils {

private const val BASE_URL = "http://service.picasso.adesk.com/"

/**
* 通过Moshi 将JSON转为为 Kotlin 的Data class
*/

private val moshi: Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()

/**
* 构建Retrofit
*/

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()

/**
* 创建Api网络请求服务
*/

val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}

  由于担心你看的时候导错包,现在贴代码我会将导包的信息也贴出来,这样你总不会再导错包了吧。下面简单说明一下这个类,首先我定义了一个常量BASE_URL。作为网络接口请求的地址头,然后构建了MoShi,通过MoShi去进行JSON转Kotlin数据类的处理,之后就是构建Retrofit,将MoShi设置进去,最后就是通过Retrofit创建一个网络请求服务。


三、意图与状态


  之前我们说MVI的I 是Intent,表示意图或行为,和ViewModel一样,我们在使用Intent的时候,也是一个Intent对应一个Activity/Fragment。


① 创建意图


data包下创建一个intent包,intent包下新建一个MainIntent类,代码如下所示:


package com.llw.mvidemo.data.intent

/**
* 页面意图
*/

sealed class MainIntent {
/**
* 获取壁纸
*/

object GetWallpaper : MainIntent()
}

  这里只有一个GetWallpaper,表示获取壁纸的动作,你还可以添加其他的,例如保存图片、下载图片等,现在意图有了,下面来创建状态,一个意图有用多个状态。


② 创建状态


data包下创建一个state包,state包下新建一个MainState类,代码如下:


package com.llw.mvidemo.data.state

import com.llw.mvidemo.data.model.Wallpaper

/**
* 页面状态
*/

sealed class MainState {
/**
* 空闲
*/

object Idle : MainState()

/**
* 加载
*/

object Loading : MainState()

/**
* 获取壁纸
*/

data class Wallpapers(val wallpaper: Wallpaper) : MainState()

/**
* 错误信息
*/

data class Error(val error: String) : MainState()
}

  这里可以看到四个状态,获取壁纸属于其中的一个状态,通过状态可以去更改页面中的UI,后面我们会看到这一点,这里的状态你还可以再进行细分,例如每一个网络请求你可以增加一个请求中、请求成功、请求失败。


四、ViewModel


  在MVI模式中,ViewModel的重要性又提高了,不过我们同样要添加Repository,作为数据存储库。


① 创建存储库


data包下创建一个repository包,repository包下新建一个MainRepository类,代码如下:


package com.llw.mvidemo.data.repository

import com.llw.mvidemo.network.ApiService

/**
* 数据存储库
*/

class MainRepository(private val apiService: ApiService) {

/**
* 获取壁纸
*/

suspend fun getWallPaper() = apiService.getWallPaper()
}

  这里的代码就没什么好说的,下面我们写ViewModel,和MVVM模式中没什么两样的。


② 创建ViewModel


  下面在com.llw.mvidemo包下新建一个ui包,ui包下新建一个adapter包,adapter包下新建一个MainViewModel类,代码如下:


package com.llw.mvidemo.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.llw.mvidemo.data.repository.MainRepository
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch

/**
* @link MainActivity
*/

class MainViewModel(private val repository: MainRepository) : ViewModel() {

//创建意图管道,容量无限大
val mainIntentChannel = Channel<MainIntent>(Channel.UNLIMITED)

//可变状态数据流
private val _state = MutableStateFlow<MainState>(MainState.Idle)

//可观察状态数据流
val state: StateFlow<MainState> get() = _state

init {
viewModelScope.launch {
//收集意图
mainIntentChannel.consumeAsFlow().collect {
when (it) {
//发现意图为获取壁纸
is MainIntent.GetWallpaper -> getWallpaper()
}
}
}
}

/**
* 获取壁纸
*/

private fun getWallpaper() {
viewModelScope.launch {
//修改状态为加载中
_state.value = MainState.Loading
//网络请求状态
_state.value = try {
//请求成功
MainState.Wallpapers(repository.getWallPaper())
} catch (e: Exception) {
//请求失败
MainState.Error(e.localizedMessage ?: "UnKnown Error")
}
}
}
}

  这里首先创建一个意图管道,然后是一个可变的状态数据流和一个不可变观察状态数据流,观察者模式。在初始化的时候就进行意图的收集,你可以理解为监听,当收集到目标意图MainIntent.GetWallpaper时就进行相应的意图处理,调用getWallpaper()函数,这里面修改可变的状态_state,而当_state发生变化,state就观察到了,就会进行相应的动作,这个通过是在View中进行,也就是Activity/Fragment中进行。这里对_state首先赋值为Loading,表示加载中,然后进行一个网络请求,结果就是成功或者失败,如果成功,则赋值Wallpapers,View中收集到这个状态后就可以进行页面数据的渲染了,请求失败,也要更改状态。


③ 创建ViewModel工厂


在viewmodel包下新建一个ViewModelFactory类,代码如下:


package com.llw.mvidemo.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.llw.mvidemo.network.ApiService
import com.llw.mvidemo.data.repository.MainRepository

/**
* ViewModel工厂
*/

class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
// 判断 MainViewModel 是不是 modelClass 的父类或接口
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(MainRepository(apiService)) as T
}
throw IllegalArgumentException("UnKnown class")
}
}

五、UI


  前面我们写好基本的框架内容,下面来进行使用,简单来说,请求数据然后渲染出来,因为这里请求的是壁纸数据,所以我需要写一个适配器。


① 列表适配器


  在创建适配器之前首先我们需要创建一个适配器所对应的item布局,在layout下新建一个item_wallpaper_rv.xml,代码如下图所示:


<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.imageview.ShapeableImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/iv_wall_paper"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_margin="4dp"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/roundedImageStyle" />


这里使用了ShapeableImageView,这个控件的优势就在于可以自己设置圆角,在themes.xml中添加如下代码:


    <!-- 圆角图片 -->
<style name="roundedImageStyle">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
</style>

添加位置如下图所示:


在这里插入图片描述


下面进行我们在ui包下新建一个adapter包,adapter包下新建一个WallpaperAdapter类,里面的代码如下所示:


package com.llw.mvidemo.ui.adapter

import android.view.LayoutInflater
import android.view.ViewGr0up
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.llw.mvidemo.data.model.Vertical
import com.llw.mvidemo.databinding.ItemWallpaperRvBinding

/**
* 壁纸适配器
*/

class WallpaperAdapter(private val verticals: ArrayList<Vertical>) :
RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() {

fun addData(data: List<Vertical>) {
verticals.addAll(data)
}

class ViewHolder(itemWallPaperRvBinding: ItemWallpaperRvBinding) :
RecyclerView.ViewHolder(itemWallPaperRvBinding.root) {

var binding: ItemWallpaperRvBinding

init {
binding = itemWallPaperRvBinding
}
}

override fun onCreateViewHolder(parent: ViewGr0up, viewType: Int) =
ViewHolder(ItemWallpaperRvBinding.inflate(LayoutInflater.from(parent.context), parent, false))

override fun getItemCount() = verticals.size

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
//加载图片
verticals[position].priview.let {
Glide.with(holder.itemView.context).load(it).int0(holder.binding.ivWallPaper)
}
}
}

这里的代码相对比较简单,就不做说明了,属于适配器的基本操作了。


② 数据渲染


适配器写好之后,我们需要修改一下activity_main.xml中的内容,修改后代码如下所示:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">


<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_wallpaper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:visibility="gone" />


<ProgressBar
android:id="@+id/pb_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<Button
android:id="@+id/btn_get_wallpaper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="获取壁纸"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

下面我们进入MainActivity,修改里面的代码如下所示:


package com.llw.mvidemo.ui

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.llw.mvidemo.network.NetworkUtils
import com.llw.mvidemo.databinding.ActivityMainBinding
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import com.llw.mvidemo.ui.adapter.WallpaperAdapter
import com.llw.mvidemo.ui.viewmodel.MainViewModel
import com.llw.mvidemo.ui.viewmodel.ViewModelFactory
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

private lateinit var mainViewModel: MainViewModel

private var wallPaperAdapter = WallpaperAdapter(arrayListOf())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//使用ViewBinding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//绑定ViewModel
mainViewModel = ViewModelProvider(this, ViewModelFactory(NetworkUtils.apiService))[MainViewModel::class.java]
//初始化
initView()
//观察ViewModel
observeViewModel()
}

/**
* 观察ViewModel
*/

private fun observeViewModel() {
lifecycleScope.launch {
//状态收集
mainViewModel.state.collect {
when(it) {
is MainState.Idle -> {

}
is MainState.Loading -> {
binding.btnGetWallpaper.visibility = View.GONE
binding.pbLoading.visibility = View.VISIBLE
}
is MainState.Wallpapers -> { //数据返回
binding.btnGetWallpaper.visibility = View.GONE
binding.pbLoading.visibility = View.GONE

binding.rvWallpaper.visibility = View.VISIBLE
it.wallpaper.let { paper ->
wallPaperAdapter.addData(paper.res.vertical)
}
wallPaperAdapter.notifyDataSetChanged()
}
is MainState.Error -> {
binding.pbLoading.visibility = View.GONE
binding.btnGetWallpaper.visibility = View.VISIBLE
Log.d("TAG", "observeViewModel: $it.error")
Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()
}
}
}
}
}

/**
* 初始化
*/

private fun initView() {
//RV配置
binding.rvWallpaper.apply {
layoutManager = GridLayoutManager(this@MainActivity, 2)
adapter = wallPaperAdapter
}
//按钮点击
binding.btnGetWallpaper.setOnClickListener {
lifecycleScope.launch{
//发送意图
mainViewModel.mainIntentChannel.send(MainIntent.GetWallpaper)
}
}
}
}

  说明一下,首先声明变量并在onCreate()中进行初始化,这里绑定ViewModel采用的是ViewModelProvider(),而不是ViewModelProviders.of,这是因为这个API已经被移除了,在之前的版本中是过时弃用,在最新的版本中你都找不到这个API了,所以使用ViewModelProvider(),然后通过ViewModelFactory去创建对应的MainViewModel


  initView()函数中是控件的一些配置,比如给RecyclerView添加布局管理器和设置适配器,给按钮添加点击事件,在点击的时候发送意图,发送的意图被MainViewModel中mainIntentChannel收集到,然后执行网络请求操作,此时意图的状态为Loading


  observeViewModel()函数中是对状态的收集,在状态为Loading,隐藏按钮,显示加载条,然后网络请求会有结果,如果是成功,则在UI上隐藏按钮和加载条,显示列表控件,并添加数据到适配器中,然后刷新适配器,数据就会渲染出来;如果是失败则显示按钮,隐藏加载条,打印错误信息并提示一下。这样就完成了通过状态更新UI的环节,MVI的框架就是这样设计的。


页面UI(点击事件发送意图) → ViewModel收集意图(确定内容) →
ViewModel更新状态(修改_state) → 页面观察ViewModel状态(收集state,执行相关的UI)

这是一个环,从UI页面出发,最终回到UI页面中进行数据渲染,我们看看效果。


在这里插入图片描述


六、源码


欢迎Star 或 Fork,山高水长,后会有期~


源码地址:MviDemo


作者:初学者_Study
来源:juejin.cn/post/7223926748287254585
收起阅读 »

一个指令实现左右拖动改变布局

web
一个指令实现左右拖动改变布局 一、前言 本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了: 实现思路 总结关键技术点 完整 demo 二、实现思路 2.1 外层div布局 首先设置4个div元素,一个作为父容器,一个...
继续阅读 »

一个指令实现左右拖动改变布局


一、前言


本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了:





    1. 实现思路





    1. 总结关键技术点





    1. 完整 demo




二、实现思路


2.1 外层div布局


首先设置4个div元素,一个作为父容器,一个作为左边的容器,一个在中间作为拖动指令承载的元素,最后一个在作为右边容器的元素。


js
复制代码
<div>
<div class="left"></div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right"></div>
</div>

2.2 获取指令元素的父元素和兄弟元素


首先,接收指令传递的各元素的宽,并进行初始赋值和利用 calc 计算右边元素宽度。


js
复制代码
let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

然后,接收指令传递下来的元素 el,并根据该元素 通过 Element.previousElementSibling 获取当前元素前一个兄弟元素,即是 所在的元素。 通过
Element.nextElementSibling 获取当前元素的后一个兄弟元素,即是 所在的元素。 通过 Element.parentElement 获取当前元素的父元素。


js
复制代码
bind: function (el, binding, vnode) {
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement
}

2.3 利用浮动定位,实现浮动布局


接着,给各个容器元素设置浮动定位 float = 'left'。当然,其实其他方式也可以的,只要能达到类似“行内块”的布局即可。


可以提一下的是,设置 float = 'left' 可以创建一个独立的 BFC 区域,具有“独立隔离性”, 即 BFC 区域内部元素的布局,不会“越界”影响外部元素的布局; 外部元素的布局也不会“穿透”,影响 BFC 区域的内部布局。


js
复制代码
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
left.style.float = 'left'
resize.style.float = 'left'
right.style.float = 'left'

2.4 实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标


通过 onpointerdown 监听,实现实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标,这个特定元素即 v-resize 指令所在的元素。


这样,就可以通过获取 v-resize 指令所在的元素的位置属性,来计算出左右的元素,在拖动时需要设置的宽和位置信息。


js
复制代码
resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.setPointerCapture(e.pointerId);
return false
}

2.5 实现鼠标移动时,改变左右的宽度


通过 onpointermove 监听,实现在鼠标指针移动时,获取鼠标事件的位置信息 clientX 等,并由此计算出合适的移动距离 moveLen, resize 的左边距离,left 元素的宽,以及 right
元素的宽。


由此,就实现了每移动一步,就重新计算出新的布局位置信息,并进行了赋值。


js
复制代码
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}

2.6 鼠标抬起时,将鼠标指针从先前捕获的元素中释放


通过监听 onpointerup,实现在鼠标指针抬起时,通过 releasePointerCapture 将鼠标指针从先前捕获的元素中释放,还给鼠标自由。并将 resize 元素的 onpointermove 事件设置为
null。这样,当鼠标被抬起后,再操作就不会携带此前的绑定操作了。


js
复制代码
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}

经过上诉步骤,我们就实现了,从鼠标按下,到移动计算改变布局,然后鼠标抬起释放绑定,操作完成,改变布局的目标达成。


三、总结关键技术点


实现本需求主要的关键技术点有:


3.1 setPointerCapture 和 releasePointerCapture


Element.setPointerCapture() 用于将特定元素指定为未来指针事件的捕获目标。 指针的后续事件将以捕获元素为目标,直到捕获被释放(通过 Element.releasePointerCapture())。


Element.releasePointerCapture() 则用来将鼠标从先前通过 Element.setPointerCapture() 绑定的元素身上释放出来,还给鼠标自由。


需要注意的是,类似的功能事件还有 setCapture() 和 releaseCapture,但它们已经被标记为弃用,且是非标准的,所以不建议使用。


3.2 onpointerdown,onpointermove 和 onpointerup


与上面配套的关键事件还有,onpointerdown,onpointermove 和 onpointerup。其中 onpointermove 是实现主要改变布局的逻辑的地方。


pointerdown:全局事件处理程序,当鼠标指针按下时触发。返回 pointerdown 事件触发对象的事件处理程序。


onpointermove:全局事件处理程序,当鼠标指针移动时触发。返回 targetElement 元素的 pointermove 事件处理函数。


onpointerup:全局事件处理程序,当鼠标指针抬起时触发。返回 targetElement 元素的pointerup事件处理函数。


3.3 注意事项


① Vue.nextTick 的使用。在 vue 指令定义的 bind 中使用了 Vue.nextTick,是为了解决初次运算时,有些 dom 元素未完成渲染,设置元素属性会报警告或错误。


js
复制代码
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
}
})

② position = 'relative' 的设置。给每个元素 left 和 right 元素设置 position = 'relative',是为了解决 z-index 可能会失效的问题,我们知道有时浮动元素会导致这种情形发生。
当然这并不影响本次需求的实现,是为了其他设计考虑才这样做的。


js
复制代码
left.style.position = 'relative'
resize.style.position = 'relative'
right.style.position = 'relative'

③ cursor = 'col-resize' 的设置。为了获得更友好的体验,使得用户一眼鉴别这个功能,我们使用了 cursor 的 col-resize 属性。


js
复制代码
resize.style.cursor = 'col-resize'

四、完整 demo


// 这是定义指令的完整代码:directive.js


js
复制代码

/**
* 自定义调整宽度指令:添加指令后,可以实现拖拽边线改变页面元素的宽度。
* 指令接收两个参数,left 左边元素的宽度,中间 resize 元素的宽度。数据类型均为 number
* 使用示例:
* <div>
* <div></div>
* <div v-resize="{left: 300, resize: 10}" />
* <div></div>
* </div>
*
* 注意:由于是使用 float 布局,所以需要保证有4个元素作为浮动元素的容器,即父容器 1 个,子容器 3 个。
*
*/

import Vue from 'vue'

const resizeDirective = {}
const handler = (el, binding, vnode) => {

let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

if (binding.value?.left && Object.prototype.toString.call(binding.value?.left) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}
if (binding.value?.resize && Object.prototype.toString.call(binding.value?.resize) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}

let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
box.style.height = '100%'
box.style.width = '100%'
box.style.overflow = 'hidden'

left.style.float = 'left'
left.style.width = leftWidth + 'px'
left.style.position = 'relative'

resize.style.float = 'left'
resize.style.cursor = 'col-resize'
resize.style.width = resizeWidth + 'px'
resize.style.height = box.offsetHeight + 'px'
resize.style.position = 'relative'

right.style.float = 'left'
right.style.width = rightWidth
right.style.position = 'relative'
right.style.zIndex = 99

resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}
resize.setPointerCapture(e.pointerId);
return false
}
}
resizeDirective.install = Vue => {
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
},
update: function (el, binding) {
handler(el, binding)
},
unbind: function (el, binding) {
el.instance && el.instance.$destroy()
}
})
}

export default resizeDirective



// 在 main.js 中使用


js
复制代码
import resizeDirective from './directive'

Vue.use(resizeDirective)

// 在具体页面中使用:ResizeWidth.vue


html
复制代码

<template>
<div>
<div class="left">left</div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right">right</div>
</div>
</template>

<script>
export default {
name: 'ResizeWidth'
}
</script>

<style scoped>
.left {
background: #42b983;
height: 50vh;
}

.resize {
background: #EEEEEE;
height: 50vh;
}

.right {
background: #1e87f0;
height: 50vh;
}
</style>


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

OkDownloader,基于 OkHttp的现代化开源下载框架

OkDownloader是一款基于 OkHttp 编写的适用于Kotlin/Java/Android平台的开源下载框架,可以运行在任何JVM 机器上。 简单易用:和 OkHttp 一样简单易用的 API 功能丰富:支持同步/异步下载、网络限制、任务优先级、资...
继续阅读 »

OkDownloader是一款基于 OkHttp 编写的适用于Kotlin/Java/Android平台的开源下载框架,可以运行在任何JVM 机器上。



  • 简单易用:和 OkHttp 一样简单易用的 API

  • 功能丰富:支持同步/异步下载、网络限制、任务优先级、资源校验、多线程下载等

  • 现代化:用 Kotlin 编写的基于 OkHttp 的下载框架

  • 易扩展:支持在代码中注入自定义拦截器以及SPI声明自定义拦截器的方式扩展下载功能

  • 多平台:支持在任何 JVM 机器上运行


使用示例


创建Downloader对象


val downloader = Downloader.Builder().build()

同步下载


val request = Download.Request.Builder()
.url(url)
.int0(file)
.build()
downloader.newCall(request).execute()

异步下载


val request = Download.Request.Builder()
.url(url)
.int0(file)
.build()
downloader.newCall(request).enqueue()

取消下载


call.cancel()

更多的用法可以参考文章最后的官网


设计思路


OkDownloader 整体上模仿 OkHttp 的代码风格和模式编写,拥有和 OkHttp 一样简单易用的 API和拦截器,这种设计非常容易扩展。


代码添加拦截器


val downloader = Downloader.Builder()
.addInterceptor(CustomInterceptor())
.build()

SPI声明拦截器(可以在不同的模块中,通常会在一个扩展模块),即在扩展模块的META-INF/services/com.billbook.lib.Interceptor


com.example.CustomInterceptor1
com.example.CustomInterceptor2
com.example.CustomInterceptor3

Downloader为什么不直接设计成单例?


通常,我们在使用 OkHttp 的时候会将 OkHttpClient 包装成单例。那么为什么OkHttp 不把 OkHttpClient 直接设置成单例呢?


原因是不设计成单例会更加灵活,在需要特殊配置的时候我们调用原有的 OkHttpClient 的 newBuilder 方法重新创建一个 Builder进行特殊的参数配置(如更短的连接超时)后 build一个新的 OkHttpClient 以适应于新的网络请求场景。这样不仅可以进行资源复用(如内部的连接池)还可以特殊定制化以便适应多个网络请求场景。


资源复用


类似地,Downloader对象中有一个 ExecutorService,是内部异步下载任务调度执行的线程池。通常我们需要进行线程池的复用,所以 Downloader 也提供了 newBuilder 方法进行资源的复用。同时 Downloader 对象中会有自己的 DownloadPool,我们称它为下载池,它的职责是管理 Downloader 中的所有下载任务。Downloader 的 DownloadPool 不会进行复用,目的是为了对不同 Downloader 的下载任务隔离。


任务隔离


每个Downloader 实例有自己的DownloadPool,这样方便进行下载任务隔离,做到不同业务的下载任务互不干扰。


当然,如果你需要的是全局的Downloader统一管理App 的所有下载任务,那么你可以将 Downloader 包装成单例对象,并且设置同一个下载池,如


val downloadPool = DownloadPool()

val globalDownloader = Downloader.Builder()
.downloadPool(downloadPool)
.build()

val retry10Downloader = globalDownloader.newBuilder()
.downloadPool(downloadPool)
.defaultMaxRetry(10)
.build()

// cancelAll
globalDownloader.cancelAll()


需要说明的是,当你需要特殊配置一个 Downloader 对象,并且你需要将该 Downloader 中的任务在全局 Downloader调用 cancelAll 时也会取消它的下载任务的时候你才需要设置同一个 DownloadPool。


最后


OkDownloader提供了和 OkHttp 类似的简单易用的 API,很方便使用。同时也提供了拦截器很方便对现有的功能进行扩展,如可扩展免流 Url转换功能,4G或WIFI网络限制功能。



目前下载框架已接入线上 App 中使用,欢迎大佬吐槽点赞,如果您觉得OkDownloader好用或者该文章对你有帮助的话不妨动动你的手指给个Star~感谢您的阅读和支持!


作者:异独行
来源:juejin.cn/post/7261862616095768634
收起阅读 »

箭头函数太长了,缩短小窍门来了

web
前言 使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略: 参数括号 (param1, param2) return 关键字 甚至大括号 { }。 1. 基本语法 完整版本的箭头函数声明包括: 一对带有参数枚举的括号 (param...
继续阅读 »

前言


使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略:



  • 参数括号 (param1, param2)

  • return 关键字

  • 甚至大括号 { }


1. 基本语法


完整版本的箭头函数声明包括:



  • 一对带有参数枚举的括号 (param1, param2)

  • 后面跟随箭头 =>

  • 以函数体 {FunctionBody} 结尾


典型的箭头函数如下所示:


const sayMessage = (what, who) => {
  return `${what}${who}!`;
};

sayMessage('Hello''World'); // => 'Hello, World!'

这里有一点需要注意:你不能在参数 (param1, param2) 和箭头 => 之间放置换行符。


接下来我们看看如何缩短箭头函数,在处理回调时,使它更易于阅读。


2. 减少参数括号


以下函数 greet 只有一个参数:


const greet = (who) => {
  return `${who}, Welcome!`
};

greet('Aliens'); // => "Aliens, Welcome!"

greet 箭头函数只有一个参数 who 。该参数被包装在一对圆括号(who) 中。


当箭头函数只有一个参数时,可以省略参数括号。


可以利用这种性质来简化 greet


const greetNoParentheses = who => {
  return `${who}, Welcome!`
};

greetNoParentheses('Aliens'); // => "Aliens, Welcome!"

新版本的箭头函数 greetNoParentheses 在其单个参数 who 的两边没有括号。少两个字符:不过仍然是一个胜利。


尽管这种简化很容易掌握,但是在必须保留括号的情况下也有一些例外。让我们看看这些例外。


2.1 注意默认参数


如果箭头函数有一个带有默认值的参数,则必须保留括号。


const greetDefParam = (who = 'Martians') => {
  return `${who}, Welcome!`
};

greetDefParam(); // => "Martians, Welcome!"

参数 who 的默认值为 Martians。在这种情况下,必须将一对括号放在单个参数(who ='Martians')周围。


2.2 注意参数解构


你还必须将括号括在已解构的参数周围:


const greetDestruct = ({ who }) => {
  return `${who}, Welcome!`;
};

const race = {
  planet'Jupiter',
  who'Jupiterians'
};

greetDestruct(race); // => "Jupiterians, Welcome!"

该函数的唯一参数使用解构 {who} 来访问对象的属性 who。这时必须将解构式用括号括起来:({who {}})


2.3 无参数


当函数没有参数时,也需要括号:


const greetEveryone = () => {
  return 'Everyone, Welcome!';
}

greetEveryone(); // => "Everyone, Welcome!"

greetEveryone 没有任何参数。保留参数括号 ()


3. 减少花括号和 return


当箭头函数主体内仅包含一个表达式时,可以去掉花括号 {} 和 return 关键字。


不必担心会忽略 return,因为箭头函数会隐式返回表达式评估结果。这是我最喜欢的箭头函数语法的简化形式。


没有花括号 {} 和 return 的 greetConcise 函数:


const greetConcise = who => `${who}, Welcome!`;

greetConcise('Friends'); // => "Friends, Welcome!"

greetConcise 是箭头函数语法的最短版本。即使没有 return,也会隐式返回 $ {who},Welcome! 表达式。


3.1 注意对象文字


当使用最短的箭头函数语法并返回对象文字时,可能会遇到意外的结果。


让我们看看这时下会发生些什么事:


const greetObject = who => { message: `${who}, Welcome!` };

greetObject('Klingons'); // => undefined

期望 greetObject 返回一个对象,它实际上返回 undefined


问题在于 JavaScript 将大括号 {} 解释为函数体定界符,而不是对象文字。message: 被解释为标签标识符,而不是属性。


要使该函数返回一个对象,请将对象文字包装在一对括号中:


const greetObject = who => ({ message: `${who}, Welcome!` });

greetObject('Klingons'); // => { message: `Klingons, Welcome!` }

({ message: `${who}, Welcome!` })是一个表达式。现在 JavaScript 将其视为包含对象文字的表达式。


4.粗箭头方法


类字段提案(截至2019年8月,第3阶段)向类中引入了粗箭头方法语法。这种方法中的 this 总是绑定到类实例上。


让我们定义一个包含粗箭头方法的 Greet 类:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = (who) => {
    return `${who}${this.what}!`;
  }
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage 是 Greet 类中的一个方法,使用粗箭头语法定义。getMessage 方法中的 this 始终绑定到类实例。


你可以编写简洁的粗箭头方法吗?是的你可以!


让我们简化 getMessage 方法:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = who => `${who}${this.what}!`
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage = who => `${who}, ${this.what}! 是一个简洁的粗箭头方法定义。省略了其单个参数 who 周围的一对括号,以及大括号 {} 和 return关键字。


5. 简洁并不总是意味着可读性好


我喜欢简洁的箭头函数,可以立即展示该函数的功能。


const numbers = [145];
numbers.map(x => x * 2); // => [2, 8, 10]

x => x * 2 很容易暗示一个将数字乘以 2 的函数。


尽管需要尽可能的使用短语法,但是必须明智地使用它。否则你可能会遇到可读性问题,尤其是在多个嵌套的简洁箭头函数的情况下。


我更喜欢可读性而不是简洁,因此有时我会故意保留大括号和 return 关键字。


让我们定义一个简洁的工厂函数:


const multiplyFactory = m => x => x * m;

const double = multiplyFactory(2);
double(5); // => 10

虽然 multiplyFactory 很短,但是乍一看可能很难理解它的作用。


这时我会避免使用最短的语法,并使函数定义更长一些:


const multiplyFactory = m => { 
  return x => x * m;
};

const double = multiplyFactory(2);
double(5); // => 10

在较长的形式中,multiplyFactory 更易于理解,它返回箭头函数。


无论如何,你都可能会进行尝试。但我建议你将可读性放在简洁性之前。


6. 结论


箭头函数以提供简短定义的能力而闻名。


使用上面介绍的诀窍,可以通过删除参数括号、花括号或 return 关键字来缩短箭头函数。


你可以将这些诀窍与粗箭头方法放在一起使用。


简洁是好的,只要它能够增加可读性即可。如果你有许多嵌套的箭头函数,最好避免使用最短的形式。


作者:河马老师
来源:juejin.cn/post/7326758010523697192
收起阅读 »

爆肝手写 · 一镜到底特效· 龙年大吉 【CSS3】

web
前言 作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的...
继续阅读 »

前言


作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的家伙变成自己的热爱的事情。 龙年来临之际, 通宵写了一个全新的CSS3 一镜到底的特效案例,如下图, 希望能与大家分享这份创意与激情, 祝各位掘友们新年快乐, 龙年行大运!


Video_20240120111316[00_00_12--00_00_15].gif


上源码:



整体实现思路介绍



整个案例使用到CSS3 和 HTML技术, 案例的核心知识点 使用到了 CSS3 中的透视 、3D变换、 动画 、无缝滚动等技术要点, 下面我会逐一进行介绍




  • 知识点1: 一镜到底特效的 案例的整体布局、设计、及动画思路

  • 知识点2:CSS3中的3D坐标系

  • 知识点3:CSS3中的3D变换及案例应用

  • 知识点4:CSS3中的3D透视及案例应用

  • 知识点5:CSS3中的 透视及3d变换的异同点

  • 知识点6:CSS3中的 动画及案例应用


1、一镜到底特效 的整体布局、设计、及动画思路


如下图所示,一镜到底的案例特效 最核心的就是要 构成一套 在3D 空间中, 有多个平行的场景, 然后以摄像机的视角 从前往后 移动,在场景中穿梭, 依次穿过每一个场景的页面即可啦,自己闭上眼睛来体验一下吧;
无标题.png


对应到本案例中效果就是这样啦:


image.png


当然有朋友会说看上图,感觉不到明显的3D 立体效果, 那再来看看下面这个图吧;


消失点.png


上面这张图就是 基于人眼 看不同距离的物体呈现出的结果, 也就是透视效果, 透视效果最核心的特点就是近大远小;而影响看到透视物体大小的一个参数就是消失点距离, 比如消失点越近,最远处的物体会越小, 近大远小的效果越明显, 自己闭上眼睛来体验一下吧;


对应到本案例中效果就是这样啦:


image.png



  • 上述框架对应的HTML源码如下, 其中.sence-in 内部的子元素是素材,可以先忽略:


<div class="sence-box sence-box1">
<div class="sence-in">
<div class="text-left text-box">掘金多多</div>
<div class="text-right text-box">大展鸿图</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
</div>
</div>
<div class="sence-box sence-box2">
<div class="sence-in">
<div class="text-left text-box">步步高升</div>
<div class="text-right text-box">年年有鱼</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box3">
<div class="sence-in">
<div class="text-left text-box">心想事成</div>
<div class="text-right text-box">万事如意</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box4">
<div class="sence-in">
<div class="text-left text-box">蒸蒸日上</div>
<div class="text-right text-box">一帆风顺</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box5">
<div class="sence-in">
<div class="text-left text-box">自强不息</div>
<div class="text-right text-box">恭喜发财</div>
<div class="sence-block">龙年大吉</div>
<div class="denglong-box"></div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>

知识点一: CSS3中的坐标系


CSS3中的坐标系,是一切3D 效果的基石, 务必熟练掌握 , 如下图所示:



  • x轴坐标:左边负,右边正

  • y轴坐标:上边负,下边正

  • z轴坐标:里面负,外面正

  • 注意: 坐标系的原点在 浏览器的左上角


image.png


知识点二: 透视(perspective)


perspective属性定义了观察者和Z=0平面之间的距离,从而为3D转换元素创建透视效果。上面也说了, 透视的效果就是 近大远小, 上面的截图中也能看到 。这个属性是用来创建3D转换效果的必要属性,因为当我们进行旋转或其他3D转换时,如果透视效果设置得不正确,元素可能会显得很奇怪或不正常。 透视的语法如下:


在CSS中,我们可以通过在父元素上设置perspective属性来控制子元素的3D效果。例如:


	.container {  
perspective: 1000px;
}

在这个例子中,我们为.container元素设置了perspective属性,值为1000px。这意味着任何在这个元素内部的3D转换都会基于这个视距进行透视。


知识点三:3D 变换的核心属性: transform-style


transform-style属性决定了是否保留元素的三维空间布局。当设置为preserve-3d时,它会保留元素内部的三维空间,即使这个元素本身没有进行任何3D转换。这使得子元素可以相对于父元素进行旋转或其他3D转换,而不会影响其他元素。在我们的案例截图中 也能看出在父元素设置了 transform-style: preserve-3d;属性后, 各个场景在 Z轴方向上,已经有了前后距离上的差异了。 需要注意的点就是, transform-style属性一定要设置给发生3D变换元素的父元素


例如:


 /* 透视属性加给了 最外层的元素, 保证所有子元素的透视效果是一致的,协调的*/
.perspective-box {
transform-style: preserve-3d;
}

在这个例子中,我们为.perspective-box元素设置了transform-style属性为preserve-3d,这意味着任何在这个元素内部的3D转换都会保留其三维空间布局。



  • 小技巧:如果你希望自己做的3D场景,立体效果很真实的话, 可以尽量多的给不同的元素,设置在Z轴方向上 设置不同的偏移量, 这样的效果是 摄像机在穿梭的过程中,每一段距离都能看到不同的风景, 层次感会很强, 当然也不要太疯狂, 不然场景会变得混乱哦


知识点四、perspective和transform-style的差异和注意点(炒鸡重要!)



  • perspective属性定义了观察者和Z=0平面之间的距离,通俗的说 就是屏幕 到消失点的距离,从而影响3D元素的透视效果, 而transform-style属性决定了是否保留元素的三维空间布局

  • 当我们只使用perspective属性时,只有被明确设置为3D转换的元素才会显示透视效果。而当我们使用transform-style: preserve-3d时,即使元素本身没有进行任何3D转换,其子元素也可以进行3D转换并保留三维空间布局。


注意:perspective属性,只能带来近大远小的透视视觉效果,并不能构成真正的3D空间布局。真正的3D布局必须依赖于transform-style: preserve-3d属性来实现


知识点五、animation动画的定义和使用


CSS动画是一种使元素从一种样式逐渐改变为另一种样式的方法。这个过程是通过关键帧(keyframes)来定义的,关键帧定义了动画过程中的不同状态。 在一镜到底的案例中, 整个场景的前后移动,用的就是动画属性。


动画的使用分为两步, 具体使用方式如下:



  • 1.使用@keyframes 来定义动画

  • 2.使用animation属性来调用动画,



@keyframes rotate {
from { transform: rotateX(0deg); }
to { transform: rotateX(360deg); }
}

在这个例子中,我们定义了一个名为“rotate”的关键帧动画,使元素从X轴的0度旋转到360度。然后,我们可以通过将这个动画应用到HTML元素上来使用它:


	.perspective-content {  
animation: rotate 5s infinite linear;
}

在这个例子中,我们将“rotate”动画应用到.cube元素上,设置动画时间为5秒,无限循环,并且线性过渡;


在一镜到底的案例中, 我们定义的动画如下:



@keyframes perspective-content {

0% {
transform: translateZ(0px);
}

50% {
transform: translateZ(6000px);
}

50.1% {
transform: translateZ(-6000px);
}

100% {
transform: translateZ(0px);
}
}


上午动画 其实做了一个无线循环轮播的逻辑, 就是当 在Z轴方向上 从 0 移动到 6000距离以后, 在重置到-6000px, 这样就可以在从-6000继续向前移动, 移动到 0 ,达到一个循环, 再开始下一次的循环;



  • 小技巧: 你可以把动画 单独加给每个场景(可能有10多个子元素, 你的重复写10多遍,会很麻烦的),也可以把动画加给公共的父元素,父元素会带着里面的子元素一起动, 这样只用写一次就行哦;


结束语:


以上就是案例用到的所有知识点啦, 整个案例的代码,可以在顶部源码位置查看,我就不一一解释了, 如有疑问和建议,可以留言,一起探讨学习哦, 本人能力有限, 希望大家多多批评指导;


作者:IT大春哥
来源:juejin.cn/post/7325739662033879090
收起阅读 »

从Flutter到Compose,为什么都在推崇声明式UI?

Compose推出之初,就曾引发广泛的讨论,其中一个比较普遍的声音就是——“🤨这跟Flutter也长得太像了吧?!” 这里说的长得像,实际更多指的是UI编码的风格相似,而关于这种风格有一个专门的术语,叫做声明式UI。 对于那些已经习惯了命令式UI的Androi...
继续阅读 »

Compose推出之初,就曾引发广泛的讨论,其中一个比较普遍的声音就是——“🤨这跟Flutter也长得太像了吧?!”


这里说的长得像,实际更多指的是UI编码的风格相似,而关于这种风格有一个专门的术语,叫做声明式UI


对于那些已经习惯了命令式UI的Android或iOS开发人员来说,刚开始确实很难理解什么是声明式UI。就像当初刚踏入编程领域的我们,同样也很难理解面向过程编程面向对象编程的区别一样。


为了帮助这部分原生开发人员完成从命令式UI到声明式UI的思维转变,本文将结合示例代码编写、动画演示以及生活例子类比等形式,详细介绍声明式UI的概念、优点及其应用。


照例,先奉上思维导图一张,方便复习:





命令式UI的特点


既然命令式UI与声明式UI是相对的,那就让我们先来回顾一下,在一个常规的视图更新流程中,如果采用的是命令式UI,会是怎样的一个操作方式。


以Android为例,首先我们都知道,Android所采用的界面布局,是基于View与ViewGr0up对象、以树状结构来进行构建的视图层级。



当我们需要对某个节点的视图进行更新时,通常需要执行以下两个操作步骤:



  1. 使用findViewById()等方法遍历树节点以找到对应的视图。

  2. 通过调用视图对象公开的setter方法更新视图的UI状态


我们以一个最简单的计数器应用为例:



这个应用唯一的逻辑就是“当用户点击"+"号按钮时数字加1”。在传统的Android实现方式下,代码应该是这样子的:


class CounterActivity : AppCompatActivity() {

var count: Int = 0

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

val countTv = findViewById(R.id.count_tv)
countTv.text = count.toString()

val plusBtn = findViewById

这段代码看起来没有任何难度,也没有明显的问题。但是,假设我们在下一个版本中添加了更多的需求:




  • 当用户点击"+"号按钮,数字加1的同时在下方容器中添加一个方块。

  • 当用户点击"-"号按钮,数字减1的同时在下方容器中移除一个方块。

  • 当数字为0时,下方容器的背景色变为透明。


现在,我们的代码变成了这样:


class CounterActivity : AppCompatActivity() {

var count: Int = 0

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

// 数字
val countTv = findViewById(R.id.count_tv)
countTv.text = count.toString()

// 方块容器
val blockContainer = findViewById(R.id.block_container)

// "+"号按钮
val plusBtn = findViewById

已经开始看得有点难受了吧?这正是命令式UI的特点,侧重于描述怎么做,我们需要像下达命令一样,手动处理每一项UI的更新,如果UI的复杂度足够高的话,就会引发一系列问题,诸如:



  • 可维护性差:需要编写大量的代码逻辑来处理UI变化,这会使代码变得臃肿、复杂、难以维护。

  • 可复用性差:UI的设计与更新逻辑耦合在一起,导致只能在当前程序使用,难以复用。

  • 健壮性差:UI元素之间的关联度高,每个细微的改动都可能一系列未知的连锁反应。


声明式UI的特点


而同样的功能,假如采用的是声明式UI,则代码应该是这样子的:


class _CounterPageState extends State<CounterPage> {
int _count = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
// 数字
Text(
_count.toString(),
style: const TextStyle(fontSize: 48),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// +"号按钮
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: const Text("+")),
// "-"号按钮
ElevatedButton(
onPressed: () {
setState(() {
if (_count == 0) return;
_count--;
});
},
child: const Text("-"))
],
),
Expanded(
// 方块容器
child: Container(
width: 60,
padding: const EdgeInsets.all(10),
color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,

child: ListView.separated(
itemCount: _count,
itemBuilder: (BuildContext context, int index) {
// 方块
return Container(width: 40, height: 40, color: Colors.white);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(color: Colors.transparent, height: 10);
},
),
))
],
),
);
}
}


在这样的代码中,我们几乎看不到任何操作UI更新的代码,而这正是声明式UI的特点,它侧重于描述做什么,而不是怎么做,开发者只需要关注UI应该如何呈现,而不需要关心UI的具体实现过程。


开发者要做的,就只是提供不同UI与不同状态之间的映射关系,而无需编写如何在不同UI之间进行切换的代码。


所谓状态,指的是构建用户界面时所需要的数据,例如一个文本框要显示的内容,一个进度条要显示的进度等。Flutter框架允许我们仅描述当前状态,而转换的工作则由框架完成,当我们改变状态时,用户界面将自动重新构建


下面我们将按照通常情况下,用声明式UI实现一个Flutter应用所需要经历的几个步骤,来详细解析前面计数器应用的代码:



  1. 分析应用可能存在的各种状态


根据我们前面对于“状态”的定义,我们可以很容易地得出,在本例中,数字(_count值)本身即为计数器应用的状态,其中还包括数字为0时的一个特殊状态。



  1. 提供每个不同状态所对应要展示的UI


build方法是将状态转换为UI的方法,它可以在任何需要的时候被框架调用。我们通过重写该方法来声明UI的构造:


对于顶部的文本,只需声明每次都使用最新返回的状态(数字)即可:


Text(
_count.toString(),
...
),

对于方块容器,只需声明当_count的值为0时,容器的背景颜色为透明色,否则为特定颜色:


Container(
color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,
...
)

对于方块,只需声明返回的方块个数由_count的值决定:


ListView.separated(
itemCount: _count,
itemBuilder: (BuildContext context, int index) {
// 方块
return Container(width: 40, height: 40, color: Colors.white);
},
...
),


  1. 根据用户交互或数据查询结果更改状态


当由于用户的点击数字发生变化,而我们需要刷新页面时,就可以调用setState方法。setState方法将会驱动build方法生成新的UI:


// "+"号按钮
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: const Text("+")),
// "-"号按钮
ElevatedButton(
onPressed: () {
setState(() {
if (_count == 0) return;
_count--;
});
},
child: const Text("-"))
],

可以结合动画演示来回顾这整个过程:



最后,用一个公式来总结一下UI、状态与build方法三者的关系,那就是:



以命令式和声明式分别点一杯奶茶


现在,你能了解命令式UI与声明式UI的区别了吗?如果还是有些抽象,我们可以用一个点奶茶的例子来做个比喻:


当我们用命令式UI的思维方式去点一杯奶茶,相当于我们需要告诉制作者,冲一杯奶茶必须按照煮水、冲茶、加牛奶、加糖这几个步骤,一步步来完成,也即我们需要明确每一个步骤,从而使得我们的想法具体而可操作。


而当我们用声明式UI的思维方式去点一杯奶茶,则相当于我们只需要告诉制作者,我需要一杯“温度适中、口感浓郁、有一点点甜味”的奶茶,而不必关心具体的制作步骤和操作细节。


声明式编程的优点


综合以上内容,我们可以得出声明式UI有以下几个优点:



  • 简化开发:开发者只需要维护状态->UI的映射关系,而不需要关注具体的实现细节,大量的UI实现逻辑被转移到了框架中。

  • 可维护性强:通过函数式编程的方式构建和组合UI组件,使代码更加简洁、清晰、易懂,便于维护。

  • 可复用性强:将UI的设计和实现分离开来,使得同样的UI组件可以在不同的应用程序中使用,提高了代码的可复用性。


总结与展望


总而言之,声明式UI是一种更加高层次、更加抽象的编程方式,其最大的优点在于能极大地简化现有的开发模式,因此在现代应用程序中得到广泛的应用,随着更多框架的采用与更多开发者的加入,声明式UI必将继续发展壮大,成为以后构建用户界面的首选方式。


作者:星际码仔
来源:juejin.cn/post/7212622837063811109
收起阅读 »

我们应避免感动自己的无效学习!

Hello,大家好,我是 Sunday。 很多同学老找我沟通时,多会说:“Sunday老师,我想要学 angular,我想要学 node,我还要学 go,学 python,学 java。对了,数据库也得学! sunday 老师,你觉得 webGL 有必要学吗?...
继续阅读 »

Hello,大家好,我是 Sunday。


很多同学老找我沟通时,多会说:“Sunday老师,我想要学 angular,我想要学 node,我还要学 go,学 python,学 java。对了,数据库也得学! sunday 老师,你觉得 webGL 有必要学吗?我听说现在好多公司都在用”。


天呢!为了学而学,你学的完吗?


每次,看到这种情况,我都需要安慰他们好久,舒缓大家的紧张情绪。


我能够明显感觉到,在现在 “内卷” 日益严重的大行情之下,很多同学都会充满的焦虑,生怕自己一个不小心就会被淘汰掉。从而开始学习很多很多的内容,期望可以通过这种方式来 “安慰自己”,告诉我已经很努力了,我不会被淘汰。


可是很多时候,这种无目标,无结果的努力,其实是 毫无价值 的!



为了学而学,毫无价值


我们学习的目的只有一个,那就是:“通过最小的付出,获得最高的收入”。


所以,不要做 “感动自己的事情”。


通过无意义的折磨自己,无效的努力学习,只会为你带来痛苦。而痛苦就是痛苦,它和成功毫无关系!



那么我们应该如何做呢?


01:做减法


我做开发有 10 多年了,做技术讲师也有 5 年了。期间见过大量的、各种各样情况的同学。



  • 有同学: 学完了 Vue、React、Angular,但是哪个都不精,所以只能一边拿着不算高的薪水,一边问我:“sunday 老师,为什么我学完了这么多的东西,还没有办法拿到更高的薪资?”

  • 有同学: 不光学习前端,还学习后端。美其名曰 全栈。并期望以此来获得更高的薪资。但是往往事与愿违,老板不愿意给他涨薪,让他时常觉得自己 “怀才不遇”。

  • 有同学: 每天都会学习到晚上 12 点,永远在追逐最新的技术。有了什么新的框架、哪位大佬说了什么话、圈子里面发生了什么事 了熟于胸。日常吹牛高谈阔论,一到面试百无一能。

  • 有同学: 钻研 “技术细节”,5 种实现继承的方法、JS 打印有几种写法 摸得门清。但是一到日常开发,却 bug 百出,百思不得其解。

  • 有同学: ......


对于现在很多的同学而言,大家都已经非常的 “卷” 了。并且已经把 “卷” 当成了日常,生怕自己跑的太慢,而被 “抛弃”。


但是,漫无目的的跑,本身毫无意义,只是在 “感动自己罢了”。


所以说 适当的做做减法吧!



  • 明确自己的目标: 你到底想要什么?想要涨薪?想要在社群有更多的话语权?想要掌握一些谈资?不同的目标下,你所需要做的事情是不同的。

  • 摆脱掉所有与你的目标无关的事情:

    • 买了一堆书也不看的,就把它们收起来,听我讲就行了

    • 之前整理过的笔记,把那些不看的删了,把感觉有价值的,整理成博客发出来,以输出来反哺输入

    • 炒股的同学,把炒股软件删了吧,除了影响心态,其他的没啥用




02:学而不用,是为 null


有人多同学学习的时候习惯 记笔记。特别是我在黑马工作的时候,经常会见到有很多同学 记了满满一大本子的笔记。但是在实际工作之后,却从来没有再次翻开过。


我们总会去学习各种各样的新知识,但是因为我们的工作内容并不会发生太大的变化,所以就会导致很多的知识点因为不经常使用而被忘掉。甚至,当我们遇到一个问题的时候,去百度发现...百度到自己的文章......


所以说 学而不用,是为空


知识分为广度和深度,任何的一个人都有自己的 “能力圈”



  1. 想清楚你的能力圈是什么

  2. 然后,学习你能力圈之内做事情


学而有用,避免学习任何你用不到的知识。


03:找到你真正喜欢的事情


有很多同学学习开发只是因为 做开发可以赚更多的钱。 这本没有错,我们都是为了钱而工作。


但是,如果你本身并不喜欢开发的话,则这份工作对你而言可能并不是一个长久之计。


所以,找到你真正喜欢做的事情很重要,因为任何一个你不喜欢的职业,你都不可能一直做下去。


所以,找到你真正喜欢做的事情。在日常工作之余,开始做这个事情。在这里给大家讲一个真实的故事:



我之前工作的时候有一个同事,咱们暂且叫他老张。老张平时不争不抢,就喜欢摆弄一些多肉。


很多在我们看起来很无聊的养护工作,在他做起来却乐此不疲。


后来在 21 年的时候,他从公司被迫离开。就开始全职 抖音卖多肉


后来跟他沟通,据说卖的还不错,比他在公司的收入要高不少。并且每天都更快乐了。



所以,找到你真正喜欢的事情,在开始的时候可以把它当做副业来去做。或许,某一天它可以给你带来意想不到的收获。


总结


随笔所写,可能并不全面。先说这些吧,如果大家有兴趣,咱们后面详聊~~~~~~


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

前端实现汉堡菜单

web
如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。 单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。 在...
继续阅读 »

如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。


单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。


在这篇文章中,我们将向您展示如何在 CSS 中创建不同的汉堡菜单动画。让我们开始吧!


创建汉堡菜单


要创建汉堡菜单,我们首先需要创建 HTML 。由一个按钮元素和三个嵌套的 div 元素组成,每个元素代表汉堡图标的一行。


<button class="hamburger">
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
</button>

接下来,我们将为元素添加一些基本样式。我们将从按钮元素中删除任何默认样式,包括背景和边框颜色。


.hamburger {
background: transparent;
border: transparent;
cursor: pointer;
padding: 0;
}

然后,对于每个线元素,我们将设置背景颜色、高度、宽度和每个直线之间的间距。


.hamburger__line {
background: rgb(203 213 225);
margin: 0.25rem 0;
height: 0.25rem;
width: 2rem;
}

X


是时候使用 CSS 创建一个很酷的汉堡菜单动画了。当用户将鼠标悬停在按钮上时,我们希望线条转换为“X”形。


为了实现这一点,我们将使用  :hover  伪类和  nth-child  选择器来访问每一行。我们将使用  translate() 和  rotate() 函数将线条转换为 X 形状。


第一条线将在 y 轴上向下移动并旋转 45 度以创建一条 X 形状的线。第二行将通过将其不透明度设置为零而消失。最后一条线将在 y 轴上向上移动,并逆时针方向旋转 45 度以完成 X 形状。我们将通过在  translate()rotate()  函数中使用负值,将其转换为与第一行相反的方向。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px) rotate(45deg);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-45deg);
}

若要应用转换,我们将使用该 transition 属性。动画将使用 ease-out 计时功能运行 300 毫秒 (0.3s)。该 all 值表示将对样式更改进行动画处理,包括 transformopacity 属性。


.hamburger__line {
transition: all 0.3s ease-out;
}

通过将鼠标悬停在按钮上来尝试一下。



形成减号


在这种方法中,当按钮悬停在按钮上时,我们会将其变成减号。我们将使用与上一种方法相同的转换,但我们不会旋转第一行和最后一行。


相反,我们将在 y 轴上向下移动第一行,直到它到达第二行。第三条线将向上移动,直到到达第一行。然后,第二行将关闭可见性,就像在前面的方法中一样。


第一行和最后一行的 `transform` 属性将与前面的方法相同,只是我们将不再使用该 `rotate()` 函数。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px);
}

看看它是什么样子的!



要将按钮变成减号,我们可以使用另一种效果,将第一行和最后一行水平移出按钮。我们将使用该 translateX() 函数来指示位置仅在 x 轴上发生了变化。使用 translateX(-100%) ,可以将目标从左向右移出容器,而使用translateX(100%) ,我们可以做相反的事情。


这两种转换都将 opacity 属性设置为零,使第一行和最后一行不可见。因此,动画完成后,只有第二行仍然可见。


.hamburger:hover .hamburger__line:nth-child(1) {
opacity: 0;
transform: translateX(-100%);
}

.hamburger:hover .hamburger__line:nth-child(3) {
opacity: 0;
transform: translateX(100%);
}

看看这如何重现减号。



形成加号


在本节中,我们将向您展示另一种类型的转换。当用户将鼠标悬停在按钮上时,它会变成一个加号。为了达到这种效果,我们将第一条线向下移动,直到它与第二条线相遇,从而形成一条水平线。


然后,我们移动 y 轴上的最后一条线并将其逆时针旋转 90 度,形成加号的垂直线。最后,我们调整 opacity  第二行,使其在动画后不可见。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-90deg);
}

查看下面的演示,了解这种方法的实际应用。



形成箭头


为了在按钮上创建箭头,我们使用简单的转换技术。第一条线旋转 45 度并沿 x 轴和 y 轴移动,直到它与第二条线的第一个点相交,形成箭头的顶线。然后,我们减小第一行的宽度,使其看起来更时尚。将相同的转换应用于最后一行,以创建箭头的底线。


如果需要调整箭头的位置,请随意调整传递给 translate() 函数的值。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(-2px, 4px) rotate(-45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(-2px, -4px) rotate(45deg);
width: 16px;
}

当您将鼠标悬停在按钮上时,箭头的样子如下:



要更改箭头的方向,请调整 translate() 函数的参数。这将确保第一行和最后一行到达第二行的末尾,并且箭头将沿相反方向旋转。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(17px, 4px) rotate(45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(17px, -4px) rotate(-45deg);
width: 16px;
}


原文:phuoc.ng/collection/…


作者:关山月
来源:juejin.cn/post/7325040809698656256
收起阅读 »

龙年到~ 我做了一个龙年红包封面,一大堆人问我教程

web
前言 就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之...
继续阅读 »

前言


就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之百通过!


红包封面展示


img


后台数据


img


这是我做的快去领取吧~


制作的第一个龙年红包上线


制作红包封面


制作红包封面需要 PS 等技术,啊? 我不会啊 我就想到了在线制作海报封面的网站(会 PS 也可以自己画图随便画画都可以只要是原创即可)


我使用的是图怪兽自己在线制作完毕之后喊朋友帮我下载的他有VIP 哈哈哈,也可以进行截图懂我意思吧?


这里我就实现制作了一张海报封面图片,大概话费 30 分钟素材网站上面都有发挥你的想象好吗~


img


紧接着无水印下载,没有 VIP 的按上面说的方法或者评论说一下我帮你~


压缩图片


红包封面它的大小只能是 500kb 的大小


img


丢给熊猫压缩压缩,直接给我压缩到 4 百多 KB


img


打开红包封面平台


微信红包封面开放平台: cover.weixin.qq.com/cgi-bin/mmc…


如果没有注册就注册一个


点击定制封面,进去上传图片


img


上传红包封面进行裁剪到你自己喜欢的感觉即可


img


一些选填的我这里就没准备就没去上传了,接着我们继续上传封面故事


大小不能超过 300 kb 我们继续丢给熊猫压缩压缩可能就没作用了,这下要用到 PS 了


img


打开在线 PS


随便找一个都可以我用的是这个 http://www.tuyitu.com/ps/sources/


点击文件 files 打开你的红包封面图片


img


img


点击图像, 图像大小 我们 宽改为 750 高改为 1250 官方要求的哦



如果把握不住那就用这个裁剪图片网站 tinypng.com/



img


img


修改完毕之后我们进行导出


img


将大小调整到 300kb 如果画质不好那么就去图像修改画布的大小与图片温和即可


img


img


前往红包开放平台上传我们的封面故事,在自己写一段故事描述,我这里就使用混元大模型给我生成一个龙年的祝福语~


img


img


我也祝大家: 龙腾盛世展才华,年贺新春喜洋洋 祝福亲友事事顺,心圆梦圆福满堂!!!


最后一步


证据材料 如果不上传这个 百分之 99 会给退回


img


PSD 源文件


使用在线 PS 打开我们的红包封面图片,在进行另存为 PSD 即可


img


直接进行上传,提交之后等待审核即可,百分之百成功!!


img


img


工作日 10 分钟就审核完毕了,耐心等待~ 如果制作成功 可以贴在评论区一起领取使用呀~




作者:杨不易呀
来源:juejin.cn/post/7325647896047583273
收起阅读 »

送外卖,3年102万,先别着急破防

外卖小哥,三年百万 刚过去不久的周末,最火的一则新闻是上海,外卖小哥 3 年掙了 102 万。 能上热搜,说明这个收入,还是明显超出了群众普遍认知的。 我们知道,通常诸如「外卖/快递/网约车」这样的职业,强调一个多劳多得。 但一般内心都会给他们框定一个认知上的...
继续阅读 »

外卖小哥,三年百万


刚过去不久的周末,最火的一则新闻是上海,外卖小哥 3 年掙了 102 万


能上热搜,说明这个收入,还是明显超出了群众普遍认知的。


我们知道,通常诸如「外卖/快递/网约车」这样的职业,强调一个多劳多得。


但一般内心都会给他们框定一个认知上的大概上界,例如一个月再怎么也不会超过 2w。


毕竟再多劳多得,也是一天 24 小时,一个人一双手一双腿。


3 年 102 万,平均下来一个月 2.8 万。


乍一听,会以为是个明显存在逻辑漏洞的人造新闻。


如果再继续套用常规思路去理解,会发现即使外卖小哥 3 年来全年无休,一天 24 小时,也掙不了 102 万。


既然再用外行人思维分析无果,不然先纠正外卖小哥单月的收入上界的认识。


利用搜索引擎,我们发现好几年前就有「送外卖,月入2-3万」的新闻,且这些新闻的主角(外卖小哥)所在地也并不局限在一线城市。


因此,2.8 万,在单月收入里面,可以算作是一个在全国范围内,行业内公认的收入天花板水平,不至于是一个不可能完成的任务。


然后再来评估「月收入持续达到天花板水平」的难度,便可得知新闻本身的合理程度。


注意:这里强调是合理程度,而非真实程度,在不超出合理程度范围的事件,我们无法不依靠更多的信息去判别真伪。


接着分析,收入持续维持高水平的难度。


由于 3 年 102 万的外卖小哥,工作所在地是上海,上六休一,日均工作 18 小时


那么注定了其存在一些客观优势:



  • 相比于其他城市,所在地送餐单价更高;

  • 3 年里面包含了疫情封城的特殊时期;

  • 长期的上六休一,大概率覆盖了绝大多数的恶劣天气,恶劣天气有额外补贴;

  • 超长的日均工作时间,大概率覆盖了有补贴的送餐时间段;


这些客观条件的存在,使得「持续摸到全国级外卖行业收入天花板」的难度,相对低了一点,至少不是网友想象中的绝无可能。


有自媒体把该新闻和《买彩-票,10万中2.2亿》的事情放一起,说这是挑战网友智商年度事件中的卧龙凤雏。


说实话,这有点侮辱外卖小哥了。


是否真实,永远不会有一个准确的说法,但仅从合理程度来看,这俩压根不是一个量级。


我猜测这些自媒体,既不了解福利彩-票现有机制,说不出来为什么发生「10万2.2亿」实际是国有公证制度问题导致的结果;也没有了解外卖行业的基础现状,只会套用自己日常点外卖的配送费多少和送餐时长的错误了解,就动手写文案了。


...


分析完事件的合理程度,习惯性的,我还想了解一下新闻的报道倾向性。


毕竟再大的事件,也不都必然能够引起全国热议。


反过来说,那些能够引起全国热议的事件,背后必然有神秘力量使然。


注意,即使只是任其发酵,那也是力量的体现。


要看清新闻报道的倾向性,可以重点看原始报道(通常没有太多加工内容)发布之后的官媒内容。


于是我释怀的笑了。


我不知道这些突如其来的流量,会不会让外卖小哥转行成为演员或带货主播。


目前这些"正能量"报道/采访,看起来至少能外卖小哥带薪多休息几天。


后续怎么发展,就不多猜测了。


...


回到主线。


实在没找到送 🍚 的题目,一起送 📦 吧。


题目描述


平台:LeetCode


题号:1011


传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。


传送带上的第 i 个包裹的重量为 weights[i]weights[i]


每一天,我们都会按给出重量的顺序往传送带上装载包裹。


我们装载的重量不会超过船的最大运载重量。


返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力。


示例 1:


输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5

输出:15

解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
1 天:1, 2, 3, 4, 5
2 天:6, 7
3 天:8
4 天:9
5 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。

示例 2:


输入:weights = [3,2,2,4,1,4], D = 3

输出:6

解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
1 天:3, 2
2 天:2, 4
3 天:1, 4

示例 3:


输入:weights = [1,2,3,1,1], D = 4

输出:3

解释:
1 天:1
2 天:2
3 天:3
4 天:1, 1

提示:



  • 1<=D<=weights.length<=5×1041 <= D <= weights.length <= 5 \times 10^4

  • 1<=weights[i]<=5001 <= weights[i] <= 500


二分解法(精确边界)


假定「D 天内运送完所有包裹的最低运力」为 ans,那么在以 ans 为分割点的数轴上具有「二段性」:



  • 数值范围在 (,ans)(-\infty, ans) 的运力必然「不满足」 D 天内运送完所有包裹的要求

  • 数值范围在 [ans,+)[ans, +\infty) 的运力必然「满足」 D天内运送完所有包裹的要求


我们可以通过「二分」来找到恰好满足 D天内运送完所有包裹的分割点 ans


接下来我们要确定二分的范围,由于不存在包裹拆分的情况,考虑如下两种边界情况:



  • 理论最低运力:只确保所有包裹能够被运送,自然也包括重量最大的包裹,此时理论最低运力为 maxmax 为数组 weights 中的最大值

  • 理论最高运力:使得所有包裹在最短时间(一天)内运送完成,此时理论最高运力为 sumsum 为数组 weights 的总和


由此,我们可以确定二分的范围为 [max,sum][max, sum]


Java 代码:


class Solution {
public int shipWithinDays(int[] weights, int days) {
int max = 0, sum = 0;
for (int w : weights) {
max = Math.max(max, w);
sum += w;
}
int l = max, r = sum;
while (l < r) {
int mid = l + r >> 1;
if (check(weights, mid, days)) r = mid;
else l = mid + 1;
}
return r;
}
boolean check(int[] weights, int t, int days) {
int n = weights.length, cnt = 1;
for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
while (i < n && sum + weights[i] <= t) sum += weights[i++];
}
return cnt - 1 <= days;
}
}

C++ 代码:


class Solution {
public:
int shipWithinDays(vector<int>& weights, int days) {
int maxv = 0, sum = 0;
for (int w : weights) {
maxv = max(maxv, w);
sum += w;
}
int l = maxv, r = sum;
while (l < r) {
int mid = l + r >> 1;
if (check(weights, mid, days)) r = mid;
else l = mid + 1;
}
return r;
}
bool check(vector<int>& weights, int t, int days) {
int n = weights.size(), cnt = 1;
for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
while (i < n && sum + weights[i] <= t) sum += weights[i++];
}
return cnt - 1 <= days;
}
};

Python 代码:


class Solution:
def shipWithinDays(self, weights: List[int], days: int) -> int:
def check(weights: List[int], t: int, days: int) -> bool:
n, cnt = len(weights), 1
i, sumv = 1, weights[0]
while i < n:
while i < n and sumv + weights[i] <= t:
sumv += weights[i]
i += 1
cnt += 1
sumv = 0
return cnt - 1 <= days

maxv, sumv = max(weights), sum(weights)
l, r = maxv, sumv
while l < r:
mid = l + r >> 1
if check(weights, mid, days):
r = mid
else:
l = mid + 1
return r

TypeScript 代码:


function shipWithinDays(weights: number[], days: number): number {
const check = function(weights: number[], t: number, days: number): boolean {
let n = weights.length, cnt = 1;
for (let i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
while (i < n && sum + weights[i] <= t) sum += weights[i++];
}
return cnt - 1 <= days;
}
let maxv = 0, sumv = 0;
for (const w of weights) {
maxv = Math.max(maxv, w);
sumv += w;
}
let l = maxv, r = sumv;
while (l < r) {
const mid = l + r >> 1;
if (check(weights, mid, days)) r = mid;
else l = mid + 1;
}
return r;
};


  • 时间复杂度:二分范围为 [max,sum][max, sum]check 函数的复杂度为 O(n)O(n)。整体复杂度为 O(nlog(i=0n1ws[i]))O(n\log({\sum_{i= 0}^{n - 1}ws[i]}))

  • 空间复杂度:O(1)O(1)


二分解法(粗略边界)


当然,一个合格的「二分范围」只需要确保包含分割点 ans 即可。因此我们可以利用数据范围来确立粗略的二分范围(从而少写一些代码):



  • 利用运力必然是正整数,从而确定左边界为 11

  • 根据 1Dweights.length500001 \leqslant D \leqslant weights.length \leqslant 500001weights[i]5001 \leqslant weights[i] \leqslant 500,从而确定右边界为 1e81e8


PS. 由于二分查找具有折半效率,因此「确立粗略二分范围」不会比「通过循环取得精确二分范围」效率低。


Java 代码:


class Solution {
public int shipWithinDays(int[] weights, int days) {
int l = 1, r = (int)1e8;
while (l < r) {
int mid = l + r >> 1;
if (check(weights, mid, days)) r = mid;
else l = mid + 1;
}
return r;
}
boolean check(int[] weights, int t, int days) {
if (weights[0] > t) return false;
int n = weights.length, cnt = 1;
for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
if (weights[i] > t) return false;
while (i < n && sum + weights[i] <= t) sum += weights[i++];
}
return cnt - 1 <= days;
}
}

C++ 代码:


class Solution {
public:
int shipWithinDays(vector<int>& weights, int days) {
int l = 1, r = 1e8;
while (l < r) {
int mid = l + r >> 1;
if (check(weights, mid, days)) r = mid;
else l = mid + 1;
}
return r;
}
bool check(vector<int>& weights, int t, int days) {
if (weights[0] > t) return false;
int n = weights.size(), cnt = 1;
for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
if (weights[i] > t) return false;
while (i < n && sum + weights[i] <= t) sum += weights[i++];
}
return cnt - 1 <= days;
}
};

Python 代码:


class Solution:
def shipWithinDays(self, weights: List[int], days: int) -> int:
def check(weights: List[int], t: int, days: int) -> bool:
if weights[0] > t: return False
n, cnt = len(weights), 1
i, sumv = 1, weights[0]
while i < n:
if weights[i] > t: return False
while i < n and sumv + weights[i] <= t:
sumv += weights[i]
i += 1
cnt += 1
sumv = 0
return cnt - 1 <= days

l, r = 1, 10**8
while l < r:
mid = l + r >> 1
if check(weights, mid, days):
r = mid
else:
l = mid + 1
return r

TypeScript 代码:


function shipWithinDays(weights: number[], days: number): number {
const check = function(weights: number[], t: number, days: number): boolean {
if (weights[0] > t) return false;
let n = weights.length, cnt = 1;
for (let i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
if (weights[i] > t) return false;
while (i < n && sum + weights[i] <= t) sum += weights[i++];
}
return cnt - 1 <= days;
}
let l = 0, r = 1e8;
while (l < r) {
const mid = l + r >> 1;
if (check(weights, mid, days)) r = mid;
else l = mid + 1;
}
return r;
};


  • 时间复杂度:二分范围为 [1,1e8][1, 1e8]check 函数的复杂度为 O(n)O(n)。整体复杂度为 O(nlog1e8)O(n\log{1e8})

  • 空间复杂度:O(1)O(1)




作者:宫水三叶的刷题日记
来源:juejin.cn/post/7325132036242882586
收起阅读 »

再次吐槽鸿蒙

上次吐槽鸿蒙还是是刚刚读完官网文档。 最近尝试利用鸿神的玩安卓开放 API 写一个 WanHarmony,也是遇到了一些设计上的不合理,或者是我没有 get 到设计精髓的地方,记录一下。 没有全局 Style 在安卓中,遇到需要公共的样式,一般会抽取全局 St...
继续阅读 »

上次吐槽鸿蒙还是是刚刚读完官网文档。


最近尝试利用鸿神的玩安卓开放 API 写一个 WanHarmony,也是遇到了一些设计上的不合理,或者是我没有 get 到设计精髓的地方,记录一下。


没有全局 Style


在安卓中,遇到需要公共的样式,一般会抽取全局 Style,鸿蒙也提供了类似的能力 @Style 装饰器。例如宽高都是 100% :


@Styles function matchSize() {
.width('100%')
.height('100%')
}

文档中说是支持 组件内全局 重用。但实际测试,所谓的全局仅仅支持单个文件内的不同组件可以引用到,一旦跨文件就无法引用。


这个还挺不方便的,希望后续得到修复。


费解的 LazyForEach


LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了 LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。


显而易见,LazyForEach 是 RecyclerView 的替代品,甚至连用法都有一些类似。


LazyForEach(
dataSource: IDataSource, // 需要进行数据迭代的数据源
itemGenerator: (item: any, index?: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index?: number) => string // 键值生成函数
): void

数据源需要实现 IDataSource 接口:


interface IDataSource {
totalCount(): number; // 获得数据总数
getData(index: number): Object; // 获取索引值对应的数据
registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}

这个 Listener 也是一堆接口方法:


interface DataChangeListener {
onDataReloaded(): void; // 重新加载数据完成后调用
onDataAdded(index: number): void; // 添加数据完成后调用
onDataMoved(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
onDataDeleted(index: number): void; // 删除数据完成后调用
onDataChanged(index: number): void; // 改变数据完成后调用
onDataAdd(index: number): void; // 添加数据完成后调用
onDataMove(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
onDataDelete(index: number): void; // 删除数据完成后调用
onDataChange(index: number): void; // 改变数据完成后调用
}

乍看起来,跟 RecyclerView.Adapter 差不多。等等,ArkUI 不应该是声明式 UI 吗?为什么还要用这种写法来实现列表呢。


其实 ArkUI 也有声明式的 List 组件:


    List({ scroller: this.scroller }) {
ForEach(this.articleList, (item: ArticleEntity) => {
ListItem() {
ArticleView({ article: item })
.onClick(() => {
router.pushUrl({
url: 'pages/WebPage',
params: item
}, router.RouterMode.Single)
})
}
})
}
.height('100%')
.width('100%')

但是呢,默认会加载所有数据,不支持预加载,不支持 item 的回收复用。所以,屏蔽实现细节,直接让 List 支持回收复用会不会更好呢?


费解的 Dialog


期望的声明式 Dialog 写法:


.dialog($isShow) {
// 自定义 dialog 布局
}

鸿蒙需要通过一个神奇的 CustomDialogController 来处理。


先通过 @CustomDialog 定义自定义 Dialog,


@CustomDialog
struct CustomDialogExample {
controller: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({}),
})

build() {
Column() {
Text('自定义 Dialog')
.fontSize(20)
.margin({ top: 10, bottom: 10 })
}
}
}

然后声明一个 CustomDialogController,调用其 open() 方法来展示弹窗。


@Entry
@Component
struct CustomDialogUser {
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample(),
})

build() {
Column() {
Button('click me')
.onClick(() => {
this.dialogController.open()
})
}.width('100%').margin({ top: 5 })
}
}

官网示例中还有一个更加晦涩难懂的 一个 dialog 中弹出另一个 dialog 的场景示例。


能用,但没那么好用。


硬编码


良好的设计应该避免让程序员硬编码,以尽量减少犯错的可能性。


当我第一次看到下面这个代码,有点懵。


Grid() {
...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')


这种相比 GridLayoutManager.SpanSizeLookUp 的写法,效率确实得到了很大的提升,但可读性就降低了。


还有宽高的硬编码,


.width('100%')
.height('100%')

我一直期望可以有个类似 fillWidth/fillHeight 的装饰器可以代替一下。


最后


以上吐槽基于 API 10 版本。另外希望早日可以有 API 9 以上版本的虚拟机可以使用。


今天是鸿蒙生态千帆启航仪式,目前已经参与鸿蒙原生开发的 App 数量比我想象的还要多一些,官方也给出了 Q4 正式商用的计划。可以想象,今年肯定是鸿蒙 App 井喷的一年。



作者:路遥写代码
来源:juejin.cn/post/7325338405408555060
收起阅读 »

8年前端,那就聊聊被裁的感悟吧!

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。 另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的...
继续阅读 »

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。
另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的际遇


我的经历


第一家公司


第一家公司说来也巧,本来是准备入职一家外包的,在杭州和同学吃个饭,接到了面试通知,一看地址就在楼上,上去一共就3轮面试,不到2个小时直接给了offer。有些东西真的就是命中注定


第二家公司


第二家公司我入职以后挖了上家公司诸多墙角,我一共挖了6个前端,2个后端。拯救朋友们于水深火热之中。





我本以为我能开启美好的新生活,结果第二年就传来我父亲重病的噩耗 肺癌晚期,我学习了大量的肺癌知识什么小细胞,非小细胞,基因检测呀等等。。。





可是最后还是没有挽留住他的生命,我记得我俩在最后一次去武汉的时候,睡在一起,他给我说了很多。


他说:治不好就算了,只是没能看到自己的孙子有些可惜罢了。

他说:我这一辈碌碌无为,没给你带来多么优越的条件,结婚、买房、工作都没给到任何帮助,唯一让我感到欣慰的是你那么努力,比我强多了,家里邻居很多都眼馋你呢。
他说:你小孩的名字想好了吗?你媳妇真是个孝顺的孩子,性格也好,心地善良,你要好好对待她。

他说了很多。。。我都快忘了他说了啥了,我不想忘来着,可是可是,想起来就又好难过。


这只是我人生历程的一部分,我把这些讲出来,是为了让大家明白,你现在所经历的困苦其实没有那么严重,人在逆境之中会放大自己的困难,以博得同情。所以现在很多人给我倒苦水的时候,我总有点不屑一顾的感觉,并不是我有多强,我只是觉得都能过去。


在灰暗的时候,工作总是心不在焉,情绪莫名冲动,我和领导吵过架,和ui妹妹撕破脸,导致人家天天投诉我。我leader说我态度极其嚣张,我说你再多说一句,我干死你所以不裁我裁谁


我的人生感悟


我时常以我爸的角度换位思考,我在得知这个消息后我该咋办?是积极面对,还是放弃治疗?可是所有的都是在假设的前提之下,一切不可为真。只有在其中的才最能明白其中的感受。
那一年我看着他积极想活着的毅力,也看到了他身体日渐消瘦的无奈,无奈之余还要应付各种亲戚的嘘寒问暖


我现在很能明白《天道》中那段,丁元英说的如果为了孝顺的名声,让父亲痛苦没有尊严地活着,还不如让父亲走了。 的意思了。在他昏迷不醒的时候,大小便失禁的时候,真不如有尊严的走了。


我其实已经预感到自己要被裁,我原本是挺担心的,可是后来想想父亲的话,我总结成一句话圆滑对事,诚以待人。 这句话看上去前后矛盾,无外乎俩个观点。


圆滑对事的意思是:就是要学会嘴甜,事嘛能少干就少干,能干几分是几分,累的是别人,爽的是自己,在规则中寻求最大的自我利益化。


诚以待人的意思是:圆滑归圆滑,不能对谁都圆滑,你得有把事情办的很好的能力,你需要给真正需要的人创造价值,而不是为了给压榨者提供以自我健康为代价的价值。



用现在最流行的词来说就是「佛系」。


什么叫活明白了,通常被理解为不争不抢,得之淡然、失之泰然、顺其自然的一种心理状态。


活明白的人一般知道自己要什么样的生活,他们不世故、不圆滑,坦荡的、磊落的做自己应该做的事儿。他们与社会上潜规则里的不良之风格格不入,却不相互抵触,甚至受到局中人的青睐与欣赏。


活明白的人看着更为洒脱,得不张扬,失不气馁,心态随和、随遇而安。


不过,还有一种活明白的人,不被多数人所接受。他们玩世不恭、好吃懒做,把所有一切交给命运去背锅。这种人极度自我,没有什么可以超越他自己的利益,无法想象这种活法,简直就是在浪费六道轮回的名额。


总之,有的人活明白了,是调整自己的心态,维护社会的稳定和安宁。有的人活明白了,是以自我为中心,一边依赖着社会救济,一边责备社会龌蹉。


所以,活明白的人也分善与恶,同样是一种积极向善,另一种是消极向恶,二者同出而异名。



我对生活的态度


离职的第一个月,便独自一人去了南京,杭州,长沙,武汉,孝感。我见了很多老朋友,听听他们发发牢骚,然后找一些小众的景点完成探险。


在南京看了看中医,在杭州露营看了看日落,在长沙夜爬了岳麓山,在武汉坐了超级大摆锤,在孝感去了无名矿坑并在一个奶奶家蹭了中午饭。


我的感受极其良好,我体验了前所未有生活态度,我热情待人,嘻嘻笑笑,我站在山顶敞怀吹风,在无尽的树林中悠然自得,治愈我不少的失落情绪。我将继续为生活的不易奔波,也将继续热爱生活,还会心怀感恩对待他人,也会圆滑处事 事事佛系。


背景1.png


图层 1.png


IMG_6214.JPG


IMG_6198.JPG


IMG_6279.JPG


可能能解决你的问题


要不要和家里人说


我屏蔽了家里人,把负面情绪隐藏,避免波及母亲本就脆弱的内心世界,我还骗她说公司今年不挣钱,提前让我们放假,只给基础工资。如果你家境殷实,家庭和睦,我建议大方的说,这样你和父母又多了一个可以聊的话题,不妨和他们多多交流,耐心一些。


裁员,真不是你的问题


请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术,你得让自己变得精彩,别虚度了这如花般的时光。可能你懒,可能也没什么规划,那就想到啥就做啥好了,可能前几次需要鼓足干劲,后面就会发现轻而易举。


如何度过很丧的阶段


沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,多和朋友吹吹牛逼,别把脸面看的那么重要,死皮赖脸反而是一种讨人喜欢的性格。



不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你放平心态,积极应对

如果你也在人生的至暗时刻,也请不要彷徨,时间总会治愈一切

不妨试试大胆一点,生活给的惊喜也同样不少

我在一个冬天的夜晚写着文字,希望能对你有些帮助


作者:顾昂_
来源:juejin.cn/post/7325317404551462938
收起阅读 »

Linux操作系统简介:为何成为全球开发者热门选择?

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。那么,Linux究竟是什么...
继续阅读 »

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。

那么,Linux究竟是什么?它又是如何影响我们的生活的呢?让我们一起探索一下。

一、Linux操作系统介绍

在介绍Linux之前,先带大家了解一下什么是自由软件。自由软件的自由(free)有两个含义:第一,是可免费提供给任何用户使用;第二,是指它的源代码公开和自由修改。

所谓自由修改是指用户可以对公开的源代码进行修改,以使自由软件更加完善,还可在对自由软件进行修改的基础上开发上层软件。

Description

下面我们再来看看Linux操作系统的概念:

Linux,一般指GNU/Linux(单独的Linux内核并不可直接使用,一般搭配GNU套件,故得此称呼),是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯·本纳第克特·托瓦兹(Linus Benedict Torvalds)于1991年10月5日首次发布。

它主要受到Minix和Unix思想的启发,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。它支持32位和64位硬件,能运行主要的Unix工具软件、应用程序和网络协议。

二、Linux系统的特点

那么,Linux为什么如此重要呢?这主要得益于它的以下几个特点:

开源免费:

Linux系统是完全免费的,任何人都可以免费使用、修改和分发。这使得Linux得以迅速传播,吸引了大量的开发者参与其中,共同推动其发展。

稳定性高:

Linux系统的稳定性非常高,长时间运行不会出现死机、蓝屏等问题。这也是为什么许多大型企业和政府部门都选择Linux作为服务器操作系统的原因。

兼容性好:

Linux支持几乎所有的硬件平台,包括x86、ARM、PowerPC等。这使得Linux可以在各种不同的设备上运行如个人电脑、手机、路由器等。同时,Linux系统还支持多种编程语言,为开发者提供了广阔的发挥空间。

强大的定制性:

Linux操作系统具有很强的定制性,用户可以根据自己的需求对系统进行深度定制。这使得Linux成为了服务器、嵌入式设备、超级计算机等领域的首选操作系统。

丰富的软件资源:

由于Linux的开源特性,许多优秀的开源软件都选择在Linux平台上发布。这些软件涵盖了从办公应用、图像处理、编程语言到数据库等各种领域,为用户提供了丰富的选择。

社区支持:

Linux拥有一个庞大的开源社区,用户可以在这里寻求帮助、分享经验、讨论问题。这种社区的支持使得Linux用户能够更好地解决问题,提高自己的技能。

三、Linux的应用

Linux的影响力已经远远超出了计算机领域,在服务器、嵌入式、开发、教育等领域都有着广泛应用。

服务器领域:

在服务器领域,Linux已经成为了主流的操作系统。据统计,世界上超过70%的服务器都在运行Linux。

Description
在云计算领域,Linux也占据了主导地位。许多知名的云服务提供商,如Amazon、Google、Microsoft等,都提供了基于Linux的云服务。

嵌入式领域:

由于Linux系统具有高度的可定制性和稳定性,因此在嵌入式领域也有着广泛的应用。

Description

如智能家居设备、无人机、机器人等都使用了Linux作为其操作系统,都离不开Linux系统的支持。这是因为Linux具有高度的可定制性和稳定性,可以满足这些设备的特殊需求。

开发领域:

Linux系统是程序员们的最爱,许多知名的开源项目都是基于Linux系统开发的,如Apache、MySQL、PHP等。

Description

此外,Linux系统还是云计算、大数据等领域的重要基础。

教育领域:

Linux系统在教育领域的应用也日益普及,许多高校和培训机构都开设了Linux相关课程,培养了大量的Linux人才。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里立即免费学习!

四、Linux系统的组成

Linux系统一般有4个主要部分:内核,Shell,文件系统和应用程序。

Description

Linux内核: 内核是系统的“内脏“,是运行程序和管理像磁盘及打印机等硬件设备的核心程序。

Linux shell: shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并送入内核中执行。实际上shell是一个命令解释器,解释由用户输入命令并且把他们送到内核。

Linux 文件系统: 文件系统是文件存放在磁盘等存储设备上的组织方法。Linux能支持多种目前流行的文件系统,如XFS、EXT2/3/4、FAT、VFAT、ISO9660、NFS、CIFS等。

Linux应用程序: 标准的Linux系统都有一套称为应用程序的程序集,包括文本编辑器、编程语言、X Window、办公软件、Internet工具、数据库等。

五、总结

总的来说,Linux是一个强大、灵活、稳定和安全的操作系统,它正在改变我们的生活和工作方式。无论你是一名开发者,还是一名普通用户,都应该了解和学习Linux,因为它将会给你带来无尽的可能性和机会。

在未来的日子里,我们将会看到Linux在更多的领域发挥其强大的影响力。无论是在数据中心、云计算、物联网,还是在人工智能、机器学习等领域,Linux都将扮演着重要的角色。

收起阅读 »

Object.assign 这算是深拷贝吗

web
在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。 Object.assign...
继续阅读 »

在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。


Object.assign() 概览


首先,让我们回顾一下 Object.assign() 的基本用法。该方法用于将一个或多个源对象的属性复制到目标对象,并返回目标对象。这一过程是浅拷贝的,即对于嵌套对象或数组,只是拷贝了引用而非创建新的对象。


const obj = { a: 1, b: { c: 2 } };
const obj2 = { d: 3 };

const mergedObj = Object.assign({}, obj, obj2);

console.log(mergedObj);
// 输出: { a: 1, b: { c: 2 }, d: 3 }

浅拷贝的陷阱


浅拷贝的特性意味着如果源对象中包含对象或数组,那么它们的引用将被复制到新的对象中。这可能导致问题,尤其是在修改新对象时,原始对象也会受到影响。


const obj = { a: 1, b: { c: 2 } };
const clonedObj = Object.assign({}, obj);
clonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(clonedObj); // { a: 1, b: { c: 3 } }

在这个例子中,修改 clonedObj 的属性也会影响到原始对象 obj


因此,如果我们需要创建一个全新且独立于原始对象的拷贝,我们就需要进行深拷贝。而 Object.assign() 并不提供深拷贝的功能。


深拷贝的需求


如果你需要进行深拷贝而不仅仅是浅拷贝,就需要使用其他的方法,如使用递归或第三方库来实现深度复制。以下是几种常见的深拷贝方法:


1. 使用 JSON 序列化和反序列化


const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = JSON.parse(JSON.stringify(obj));
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这种方法利用了 JSON 的序列化反序列化过程,通过将对象转换为字符串,然后再将字符串转换回对象,实现了一个全新的深拷贝对象。


需要注意的是,这种方法有一些限制,例如无法处理包含循环引用的对象,以及一些特殊对象(如 RegExp 对象)可能在序列化和反序列化过程中失去信息。


2. 使用递归实现深拷贝


function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

const clonedObj = Array.isArray(obj) ? [] : {};

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}

return clonedObj;
}

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = deepClone(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这是一个递归实现深拷贝的方法。它会递归地遍历对象的属性,并创建它们的副本。这种方法相对灵活,可以处理各种情况。


但需要注意在处理大型对象或深度嵌套的对象时可能会导致栈溢出。


3. 使用第三方库


许多第三方库提供了强大而灵活的深拷贝功能,其中最常用的是 lodash 库中的 _.cloneDeep 方法。


const _ = require('lodash');

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = _.cloneDeep(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

使用第三方库的优势在于它们通常经过精心设计和测试,可以处理更多的边界情况,并提供更好的性能。


作者:星光漫步者
来源:juejin.cn/post/7325040809697591296
收起阅读 »

什么,你还不会调试线上 vue 组件?

web
前言 彦祖们,在日常开发中,不知道你们是否遇到过这样的场景 在本地测试开发 vue 组件的时候非常顺畅 一上生产环境,客户说数据展示错误,样式不对... 但是你在本地测试了几次,都难以复现 定位方向 这时候作为老 vuer,自然就想到了 vue devtool...
继续阅读 »

前言


彦祖们,在日常开发中,不知道你们是否遇到过这样的场景


在本地测试开发 vue 组件的时候非常顺畅


一上生产环境,客户说数据展示错误,样式不对...


但是你在本地测试了几次,都难以复现


定位方向


这时候作为老 vuer,自然就想到了 vue devtools


但是新问题又来了,线上环境我们如何开启 vue devtools 呢?


案例演示


让我们以 element-ui 官网为例


先看下此时的 chrome devtools 是没有 Vue 的选项卡的
image.png


一段神奇的代码


其实很简单,我们只需要打开控制台,运行一下以下代码


var Vue, walker, node;
walker = document.createTreeWalker(document.body,1);
while ((node = walker.nextNode())) {
if (node.__vue__) {
Vue = node.__vue__.$options._base;
if (!Vue.config.devtools) {
Vue.config.devtools = true;
if (window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit("init", Vue);
console.log("==> vue devtools now is enabled");
}
}
break;
}
}

image.png


显示 vue devtools now is enabled


证明我们已经成功开启了 vue devtools


功能验证


然后再重启一下 chrome devtool 看下效果


image.png


我们会发现此时多了一个 Vue 选项卡,功能也和我们本地调试一样使用


对于遇到 vue 线上问题调试,真的非常好用!


写在最后


本次分享虽然没有什么技术代码,重在白嫖


感谢彦祖们的阅读


个人能力有限


如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


作者:前端手术刀
来源:juejin.cn/post/7324643000700502031
收起阅读 »

background简写,真细啊!

web
背景原因 今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒: background: url('./bg.png') no-repeat center contain ; 搞定! 上面设置的依次是 背景图片 背...
继续阅读 »

背景原因


今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒:


background:  url('./bg.png') no-repeat center contain ;

搞定!


上面设置的依次是 背景图片 背景平铺模式 背景位置 背景图片是保有其原有的尺寸还是拉伸到新的尺寸。


so easy~


看我ctrl + s 保存代码,编译。


嗯? 怎么不生效? 俺的背景呢?
打开控制台一看,好家伙,压根没生效:


image.png


问题排查


第一反应是这些属性有固定顺序,但是凭我练习两年半的经验,不应该啊,之前也是这样用的啊,遂打开MDN,仔细翻阅....


发现了下面这段话:


image.png


这让我更加确信 写的没毛病啊!!


background-attachment、background-color、background-image、background-position、background-repeat、background-size
这些属性可以以任意顺序书写。


见了鬼了,待我排查两小时(摸鱼...)


原因浮现


在仔细阅读文档后发现,其实在文档的上面,还有另外一段话:


image.png


我恍然大悟,索嘎,以后看文档不能马虎了,得仔细查阅,过于经验主义了,这都是细节啊!


background使用注意事项和总结


其实,使用background时,大部分时候 属性的顺序是可以任意位置书写的,
但是有两个属性有点特殊,那就是background-size和background-position,


当background简写同时有这两个属性时,那么必须background-position在前,background-size在后,且两者只能紧挨着书写并且以 "/"分隔。
例如:


错误: background: url('./bg.png') no-repeat center  contain ; // 没有以 "/"分隔
错误: background: url('./bg.png') center no-repeat contain ; // 没有紧挨着书写
错误: background: url('./bg.png') no-repeat contain / center; //background-size写在了 background-position的前面

正确: background: url('./bg.png') no-repeat center / contain ;


写在最后


其实MDN在关于background的文档最开头的例子中就有写:


image.png


只不过没有用语言描述出来,一般没有认真看很难发现,所以有时候能够静下心来认真查阅文档,真的会发现很多细节(甩锅:这tm是谁写的文档,出来挨打).


作者:可狗可乐
来源:juejin.cn/post/7234825495333158949
收起阅读 »

在微信小程序里运行完整的 Flutter,我们是怎么做到的?

背景 小程序是一种全新的业务形态,特别是微信小程序,既结合了 Web 动态化特性,又拥有 Native 丰富的设备能力支持。 在微信这个宿主上,小程序不仅有稳定的分发渠道,更拥有完善的生命周期、数据、AI 能力支持。 在该微信上开发小程序,一般使用以下两种方法...
继续阅读 »

背景


小程序是一种全新的业务形态,特别是微信小程序,既结合了 Web 动态化特性,又拥有 Native 丰富的设备能力支持。


在微信这个宿主上,小程序不仅有稳定的分发渠道,更拥有完善的生命周期、数据、AI 能力支持。


在该微信上开发小程序,一般使用以下两种方法:



  • JavaScript + WXML + WCSS

  • Taro + React + JavaScript


本文要介绍的是使用 Flutter Framework 开发小程序的方法,以及该方法背后的技术原理。


技术挑战


尽管 Flutter 官方已经提供 Flutter Web 实现,Flutter Web 本身就是基于 dart2js 运行的,微信小程序可以运行 JavaScript,在原理上跑起 Flutter Web 是没有问题的。


但仍然存在以下技术挑战:



  • 微信小程序没有 W3C 标准的 JavaScript 对象,Flutter Web 不能直接运行。

  • 微信小程序也没有 DOM 实现,Flutter Web HTML Renderer 不能直接渲染。

  • 微信小程序对包大小的限制十分严格,主包不能超过 2M,而 Flutter Web 所编译的 main.dart.js 初始体积就有 1.3 M,必须有合理的分包机制才能上传。


我们在 MPFlutter 1.x 版本中,针对上述问题已有一定的探索,1.x 版本的解决方法如下:



  • 使用微信开源的 kbone 库,模拟 W3C 实现,并通过模拟的 DOM 对象渲染出符合 WXML 要求的视图树。

  • 通过 Shadow Element Tree 的方式,使用 JSON 在 Dart 与 JavaScript 上下文同步视图树。

  • Fork Flutter Framework,并对其进行外科手术式的裁剪,使 main.dart.js 初始体积降低到 600K。


MPFlutter 1.x 方案已经良好的运行了两年,也收到了开发者非常多的反馈,开发者常诟病于裁剪后的 Flutter Framework 不兼容 Flutter 生态上的插件,同时 material 库也无法使用,需要从头开始编写 UI。


在 MPFlutter 2.0 版本,我们重新思考在小程序上运行 Flutter 的最佳方式,并在最终使用 CanvasKit Renderer 解决以上全部问题。


技术方案


Summary


通过裁剪 Skia 生成符合微信小程序分包要求的 CanvasKit,使用 Flutter Web + W3C BOM + WebGL Canvas 跑通渲染流程。


技术选型


在介绍技术选型前,需要先介绍 Flutter Web 的两种 Renderer。


HTML Renderer


原理是 Flutter Framework 通过 dart:js 库调用 Document 对象,并基于此将各种 RenderObject 转换为对应的 Element + CSS 添加到 DOM 树中。


该方案优点在于兼容性很好,几乎没有额外的依赖;缺点是性能不佳,并且渲染内容一致性难以与 Native Flutter 对齐。


CanvasKit Renderer


原理是通过 WebGL + Skia 渲染界面,该渲染方式与 Native Flutter 是完全一致的。


该方案优点在于渲染性能非常好,一致性与 Native Flutter 几乎没有差别;缺点是内存占用大,且需要从远端加载字体。


MPFlutter 2.0 选型


我们在 1.x 版本中用的是 HTML Renderer,通过 kbone 运行的 DOM 模拟层存在很多的问题,最令人诟病的是数据更新后界面刷新慢。当然问题的并不在于 kbone,而是 MPFlutter 1.x 本身对于 Element Tree 的序列化、反序列化的处理存在天然的缺陷,尽管已经通过 Dirty 和 Diff 等手段优化。


在 2.x 版本中,我们直接抛弃 HTML Renderer 的想法,使用 CanvasKit Renderer。


使用 CanvasKit Renderer 有这几个大前提:



  • 微信小程序已支持 WebAssembly 并支持 Brotli 压缩;

  • 微信小程序 Canvas 的性能相比最初的版本有质的提升,并支持 WebGL;

  • 微信小程序全部分包限制放宽到 20M,足够使用。


Skia 裁剪


Skia 是 Google 开源的 2D 渲染库,凭借良好的跨设备能力,优秀的性能表现,在 Google 多个产品中被使用,包括 Chrome / Flutter / Android / Fuchsia 都有 Skia 的身影。


Skia 屏蔽了不同设备、平台的具体实现,对外统一以标准的 RenderObject、RenderCommand 开放。


Skia 其中一个 Render Target 是 WebGL,也就是 CanvasKit。


然而 Flutter Web 默认使用的 CanvasKit 足有 6M 之大,即使使用 Brotli 压缩后仍然不符合小程序分包要求。


我们可以通过指定编译选项的方式裁剪 CanvasKit 尺寸,以下是 MPFlutter 使用的 build 配置:


./modules/canvaskit/compile.sh release no_skottie no_sksl_trace no_alias_font no_effects_deserialization no_encode_jpeg no_encode_png no_encode_webp legacy_draw_vertices no_embedded_font no_woff2

从配置可见,我们去掉了 skottie、image encoder、内置字体等不必要的功能,这些功能我们可以使用微信小程序 API 补充回来。


Brotli 压缩后的 wasm 文件刚好符合 2M 分包要求。


CanvasKit 加载


Skia 构建完成后,会得到两个产物,canvaskit.wasmcanvaskit.js


canvaskit.js 暴露了 wasm 中的各个 c++ 方法调用,同时也提供加载 wasm 的脚手架。


但是 canvaskit.js 的实现默认是 Web 的,我们需要将其中的 fetch 以及 WebAssembly 替换为微信小程序对应的实现。


这里提供一个使用 Skia 绘制红色矩形的微信小程序工程,有兴趣的同学可以下载到本地研究。


mpflutter.feishu.cn/wiki/LWhrw3…


Flutter Web 在微信中运行


要使 Flutter Web 在微信中运行,最大难点在于 Flutter Web 要求的 Web API 如何补充完整。


特别是 Document 、Window、Navigator 这些类,这些类我已经在 GitHub 上开源了,感兴趣的可以逐个文件阅读。


github.com/mpflutter/m…


这里举一个 window 的文件节选段落讲解:


export class FlutterMiniProgramMockWindow {
// screens
get devicePixelRatio() {
return wxSystemInfo.pixelRatio;
}

get innerWidth() {
return wxSystemInfo.windowWidth;
}

get innerHeight() {
return wxSystemInfo.windowHeight;
}

// webs
navigator = {
appVersion: "",
platform: "",
userAgent: "",
vendor: "",
language: "zh",
};

// 还有更多。。。
}

Flutter Web 在运行过程中,会通过 window.innerWidth / window.innerHeight 获取当前窗口宽高,以便下一步创建合适大小的画布用于渲染。


在微信小程序中,我们需要使用 wx.getSystemInfoSync() 获取对应宽高,并在 MockWindow 中返回给 Flutter。


关于 BOM 的文件,就不详细展开,都是一些胶水代码。


而 Flutter 的 main.dart.js 也需要有一些改造才可以跑在小程序上,主要的改造是通过 export main.dart.js 中的 main 函数,使其适配 CommonJS 可暴露给 Page 调用。


字体的加载


CanvasKit 最大的问题在于字体加载,目前来看是无法复用系统本身的字体的。


我们的做法是通过裁剪 NotoSansSC 字体,只包含常用的 9000+ 汉字,内置于小程序包中优先加载它。


这样有一个好处,小程序不需要强制从 gstatic 下载字体,省流省加载时间。


后续,我们还会研究通过 Canvas 2D 的方式,从本地加载字体。


分包


关于分包,其实是最好做的,因为 Flutter Web 本身就有 defered load 编译能力。


开发者可以轻松地将 main.dart.js 切分成若干个 JS 文件,我们做的就是在 Flutter Web 编译完成后,智能地将这些 JS 文件分配到不同的分包就好了。


资源分包也同理,资源通过 brotli 压缩也可以减少包体积。


总结


整整一套下来,Flutter 已经可以在微信小程序里跑起来了,我们来总结一下做了什么?


我们通过裁剪 Skia 使得 CanvasKit 可以很好地跑在小程序上,通过 BOM 兼容的方法,使得 Flutter Web 可以在微信小程序中找到对应实现,通过字体内置、智能分包的方式很好地解决了微信包体积限制。


该方案目前已经完全跑通,并已可用,同学们可以在 v2.mpflutter.com 文档站了解到更多用法。


如果对方案有任何疑问,也欢迎添加微信交流,感谢大家的关注。


作者:PonyCui
来源:juejin.cn/post/7324923422295670834
收起阅读 »

Android:面向单Activity开发

记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Ac...
继续阅读 »


记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Acitivity之间的生命周期,现在只需关注一个,但还是需要对Fragment的生命周期进行关注。



其实早在六七年前GitHub上就有单Activity的开源库Fragmentation,后来谷歌也出了一个库Navigation。本来以为官方出品必为经典,当时跟着官方文档一步一步踩坑,最后还是放弃了该方案。理由大概如下:



  1. 需要创建XML文件,配置导航关系和跳转参数等

  2. 页面回退是重新创建,需要配合livedata使用

  3. 貌似还会存在卡顿,一些栈内跳转处理等问题


而Github上Fragmentation库已经停止维护,所幸的是再lssuse中发现了一个基于它继续维护的SFragmentation,于是正是开启了面向单Activity的开发。


提供了可滑动返回的版本


dependencies {
//请使用最新版本
implementation 'com.github.weikaiyun.SFragmentation:fragmentation:latest'
//滑动返回,可选
implementation 'com.github.weikaiyun.SFragmentation:fragmentation_swipeback:latest'
}

由于是Fragment之间的跳转,我们需要将原有的Activity跳转动画在框架初始化时设置到该框架中


Fragmentation.builder() 
//设置 栈视图 模式为 (默认)悬浮球模式 SHAKE: 摇一摇唤出 NONE:隐藏, 仅在Debug环境生效
.stackViewMode(Fragmentation.BUBBLE)
.debug(BuildConfig.DEBUG)
.animation(
R.anim.public_translate_right_to_center, //进入动画
R.anim.public_translate_center_to_left, //隐藏动画
R.anim.public_translate_left_to_center, //重新出现时的动画
R.anim.public_translate_center_to_right //退出动画
)
.install()

因为只有一个Activity,所以需要在这个Activity中装载根Fragment


loadRootFragment(int containerId, SupportFragment toFragment)

但现在的APP几乎都是一个页面多个Tab组成的怎么办呢?


loadMultipleRootFragment(int containerId, int showPosition, SupportFragment... toFragments);

有了多个Fragment的显示,我们需要切换Tab实际也很简单


showHideFragment(ISupportFragment showFragment);

是不是使用起来很简单,首页我们解决了,关于跳转和返回、参数的接受和传递呢?


//启动目标fragment
start(SupportFragment fragment)
//带返回的启动方式
startForResult(SupportFragment fragment,int requestCode)
//接收返回参数
override fun onFragmentResult(requestCode: Int, resultCode: Int, data: Bundle?) {
super.onFragmentResult(requestCode, resultCode, data)
}
//返回到上个页面,和activity的back()类似
pop()

对于单Activity而言,我们其实也可以注册一个全局的Fragment监听,这样就能掌控当前的Fragmnet


supportFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, context)
}
override fun onFragmentCreated(
fm:
FragmentManager,
f:
Fragment,
savedInstanceState:
Bundle?
)
{
super.onFragmentCreated(fm, f, savedInstanceState)
}
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
super.onFragmentStarted(fm, f)
}
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
super.onFragmentResumed(fm, f)
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
super.onFragmentDestroyed(fm, f)
}
},
true
)

接下来我们看看Pad应用。对于手机应用来说,一般不会存在局部页面跳转的情况,但是Pad上是常规操作。


image.png


如图,点击左边列表的单个item,右边需要显示详情,这时候再点左边的其他item,此时的左边页面是保持不动的,但右边的详情页需要跳转对应的页面。使用过Pad的应该经常见到这种页面,比如Pad的系统设置等页面。这时只使用Activty应该是不能实现的,必须配合Fragment,左右分为两个Fragment。


但问题又出现了,这时候点击back怎么区分局部返回和整个页面返回呢?


//整个页面回退,主要是用于当前装载了Fragment的页面回退
_mActivity.pop()
//局部回退,被装载的Fragment之间回退
pop()

如下图,这样的页面我们又应该怎么装载呢?
image.png


可以分析,页面最外面是一个Activty,要实现单Activity其内部必装载了一个根Fragment。接着这个根Fragment中使用ViewPage和tablayout完成主页框架。当前tab页要满足右边详情页的单独跳转,还得将右边页面作为主页面,以此装载子Fragment才能实现。


image.png


总结


单Activity开发在手机和平板上使用都一样,但在平板上注意的地方更多,尤其是平板一个页面可能是多个页面组成,其局部还能单独跳转的功能,其中涉及到参数回传和栈的回退问题。使用下来,我还是觉得某些页面对硬件要求很高的使用单Activity会出现体验不好的情况,有可能是优化不到位。手机应用我还是使用多Activity方式,平板应用则使用该框架实现单Activity方式。



作者:似曾相识2022
来源:juejin.cn/post/7204100079430123557
收起阅读 »