注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一些不被人熟知,但又很好用的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
收起阅读 »

面试官: forEach怎么停止

web
介绍 在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的...
继续阅读 »

介绍


在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的解释和实际的代码示例来消除这一概念的神秘感。


在深入探讨之前,请在我的个人网站上探索更多关于 Web 开发的深度文章:


了解 JavaScript 中的 forEach 🤔


JavaScript 的 forEach 方法是迭代数组的流行工具。它为每个数组元素执行一次提供的函数。然而,与传统的 forwhile 循环不同,forEach 旨在为每个元素执行函数,没有内置机制来提前停止或中断循环。


const fruits = ["apple", "banana", "cherry"];
fruits.forEach(function(fruit) {
console.log(fruit);
});

这段代码将输出:


apple
banana
cherry

forEach 的局限性 🚫


1. forEach 中的 break


forEach 的一个关键限制是无法使用传统的控制语句比如 breakreturn 来停止或中断循环。如果您试图在 forEach 内使用 break,将遇到语法错误,因为 break 不适用于回调函数中。


尝试中断 forEach


通常,break 语句用于在满足某个条件时提前退出循环。


const numbers = [1, 2, 3, 4, 5];
numbers.forEach(number => {
if (number > 3) {
break; // 语法错误:非法 break 语句
}
console.log(number);
});

当您试图在 forEach 循环中使用 break 时,JavaScript 抛出一个语法错误。这是因为 break 被设计为在传统循环(如 forwhiledo...while)中使用,在 forEach 的回调函数中不被识别。


2. forEach 中的 return


在其他循环或函数中,return 语句退出循环或函数,如果指定的话返回一个值。


forEach 的上下文中,return 不会跳出循环。相反,它仅仅退出回调函数的当前迭代,并继续下一个数组元素。


尝试返回 forEach


const numbers = [1, 2, 3, 4, 5]; 
numbers.forEach(number => {
if (number === 3) {
return; // 仅退出当前迭代
}
console.log(number);
});

输出


1
2
4
5

在这个例子中,return 跳过了打印 3,但是循环继续剩余的元素。


使用异常中断 forEach 循环 🆕


尽管不建议常规使用,但从技术上来说,通过抛出异常可以停止 forEach 循环。尽管这种方法非正统,一般不建议使用,因为它影响代码的可读性和错误处理,但它可以有效地停止循环。


const numbers = [1, 2, 3, 4, 5];
try {
numbers.forEach(number => {
if (number > 3) {
throw new Error('Loop stopped');
}
console.log(number);
});
} catch (e) {
console.log('Loop was stopped due to an exception.');
}
// 输出: 1, 2, 3, 循环由于异常而停止。

在这个例子中,当满足条件时,抛出一个异常,提前退出 forEach 循环。但是,重要的是要正确处理这些异常,以避免意外的副作用。


用于中断循环的 forEach 替代方法 💡


使用 for...of 循环


for...of 循环是在 ES6(ECMAScript 2015)中引入的,它提供了一种现代的、简洁的和可读的方式来迭代类似数组、字符串、映射、集合等可迭代对象。与 forEach 相比,它的关键优势在于它与 breakcontinue 等控制语句兼容,在循环控制方面提供了更大的灵活性。


for...of 的优点:



  • 灵活性:允许使用 breakcontinuereturn 语句。

  • 可读性:提供清晰简洁的语法,使代码更易读和理解。

  • 通用性:能够迭代各种可迭代对象,不仅仅是数组。


for...of 的实际示例


考虑以下场景,我们需要处理数组的元素,直到满足某个条件:


const numbers = [1, 2, 3, 4, 5];  

for (const number of numbers) {
if (number > 3) {
break; // 成功中断循环
}
console.log(number);
}

输出:


1
2
3

在这个例子中,循环迭代 numbers 数组中的每个元素。一旦遇到大于 3 的数字,它利用 break 语句退出循环。这在 forEach 中是不可能的。


其他方法



  • Array.prototype.some():可以使用它来通过返回 true 来模拟中断循环。

  • Array.prototype.every():当返回 false 值时,此方法停止迭代。


结论 🎓


尽管 JavaScript 中的 forEach 方法提供了直接的数组迭代方式,但它缺乏在循环中段中断或停止的灵活性。理解这个限制对开发人员来说至关重要。幸运的是,像 for...of 循环以及 some()every() 等方法提供了必要的控制来处理更复杂的场景。掌握这些概念不仅可以增强你的 JavaScript 技能,还可以让你为艰巨的面试问题和实际编程任务做好准备。


作者:今天正在MK代码
来源:juejin.cn/post/7324384460136611850
收起阅读 »

html中的lang起到什么作用?

web
今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果…… 在chrome上是这样的 再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……...
继续阅读 »

今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果……


在chrome上是这样的


image.png


再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……


image.png


记录一下,避坑~


C3C69257283D24A892D56FA7AD82A2B1.png


代码贴在下面,感兴趣的可以去试一下


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

<head>
<meta charset="utf-8">
<title>Document</title>
</head>

<body>
<p class="MsoNormal" style="width: 300px;background: yellow;">这一党章内容增写入宪法第一条第二款。<span lang="EN-US"></span>中国特色</p>
</body>

</html>

作者:lwlcode
来源:juejin.cn/post/7324750286329282597
收起阅读 »

原神UID300000000诞生,有人以高价购买!那么UID是怎么生成的?

原神UID有人要高价购买? 在原神的广袤世界中,每位冒险者都被赋予一个独特的身份标识——UID(User ID)。这个数字串既是你在游戏中独一无二的身-份-证明,也承载着无数冒险的记忆。然而,有一个UID格外引人注目——300000000,最近它在原神的世界中...
继续阅读 »

原神UID有人要高价购买?


在原神的广袤世界中,每位冒险者都被赋予一个独特的身份标识——UID(User ID)。这个数字串既是你在游戏中独一无二的身-份-证明,也承载着无数冒险的记忆。然而,有一个UID格外引人注目——300000000,最近它在原神的世界中诞生,成为了众人瞩目的焦点,因为有人要以高价购买。


查阅资料我们知道,UID不同开头代表不同的含义。


UID服务
uid1、2开头官服
uid5开头B服、小米服等,国内渠道服都是5开头
uid6开头美服
uid7开头欧服
uid8开头亚服
uid9开头港澳服

首先UID是固定的9位数,也就是100000000这样的,前面的1是固定的,所有玩家开头都是这个1,然后剩下的8位数才是注册顺序。比如:100000001,这个就是开服第一位玩家,100000013,这个就是第13位注册玩家。


300000000说明官服已经有2亿用户了!!!


我们先看下UID的生成的策略吧。


系统中UID需要怎么设计呢?


什么是UID?


UID是一个系统内用户的唯一标识(Unique Identifier),唯一标识成为了数字世界中不可或缺的一部分。无论是在数据库中管理记录,还是在分布式系统中追踪实体,唯一标识都是保障数据一致性和可追溯性的关键。为了满足各种需求,各种唯一标识生成方法应运而生。


UID如何设计


UUID模式


UUID (Universally Unique Identifier),通用唯一识别码的缩写。目前UUID的产生方式有5种版本,每个版本的算法不同,应用范围也不同。其中最常见的是基于时间戳的版本(Version 1)和基于随机数的版本(Version 4)。版本1的UUID包含了时间戳和节点信息,而版本4的UUID则是纯粹的随机数生成。


•基于时间的UUID:这个一般是通过当前时间,随机数,和本地Mac地址来计算出来,可以通过 org.apache.logging.log4j.core.util包中的 UuidUtil.getTimeBasedUuid()来使用或者其他包中工具。由于使用了MAC地址,因此能够确保唯一性,但是同时也暴露了MAC地址,私密性不够好。•基于随机数UUID :这种UUID产生重复的概率是可以计算出来的,但是重复的可能性可以忽略不计,因此该版本也是被经常使用的版本。JDK中使用的就是这个版本。


Java中可通过UUID uuid = UUID.randomUUID();生成。


虽然 UUID 生成方便,本地生成没有网络消耗,但是使用起来也有一些缺点,不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置。对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。


表ID自增


将user表的id设置为auto_increment,插入会自动生成ID,将表的主键ID作为UID.


这种方式的优势在于简单易实现,不需要引入额外的中心化服务。但也存在一些潜在的问题,比如数据库的性能瓶颈、数据量大需要分库分表等。


使用redis实现


Redis实现分布式唯一ID主要是通过提供像 INCR 和 INCRBY 这样的自增原子命令,由于Redis自身的单线程的特点所以能保证生成的 ID 肯定是唯一有序的。


但是单机存在性能瓶颈,无法满足高并发的业务需求,所以可以采用集群的方式来实现。集群的方式又会涉及到和数据库集群同样的问题,所以也需要设置分段和步长来实现。


为了避免长期自增后数字过大可以通过与当前时间戳组合起来使用,另外为了保证并发和业务多线程的问题可以采用 Redis + Lua的方式进行编码,保证安全。Redis 实现分布式全局唯一ID,它的性能比较高,生成的数据是有序的,对排序业务有利,但是同样它依赖于redis,需要系统引进redis组件,增加了系统的配置复杂性。当然现在Redis的使用性很普遍,所以如果其他业务已经引进了Redis集群,则可以考虑使用Redis来实现。


号段模式


号段模式是一种常见的分布式ID生成策略,也被称为Segment模式。该模式通过预先分配一段连续的ID范围(号段),并在每个节点上使用这个号段,以减少对全局资源的竞争,提高生成ID的性能。以下是一个简单的号段模式生成分布式ID的步骤:


1.预分配号段: 一个中心化的服务(通常是一个分布式协调服务,比如Zookeeper或etcd)负责为每个节点预分配一段连续的ID号段。这个号段可以是一段整数范围,如[1, 1000],[1001, 2000]等。2.本地取ID: 每个节点在本地维护一个当前可用的ID范围(号段)。节点在需要生成ID时,首先使用本地的号段,而不是向中心化的服务请求。这可以减少对中心化服务的压力和延迟。3.号段用尽时重新申请: 当本地的号段用尽时,节点会向中心化服务请求一个新的号段。中心化服务会为节点分配一个新的号段,并通知节点更新本地的号段范围。4.处理节点故障: 在节点发生故障或失效时,中心化服务会将未使用的号段重新分配给其他正常运行的节点,以确保所有的ID都被充分利用。5.定期刷新: 节点可能定期地或在某个条件下触发,向中心化服务查询是否有新的号段可用。这有助于节点及时获取新的号段,避免在用尽号段时的阻塞。


这种号段模式的优点在于降低了对中心化服务的依赖,减少了因为频繁请求中心化服务而产生的性能瓶颈。同时,由于每个节点都在本地维护一个号段,生成ID的效率相对较高。


需要注意的是,号段模式并不保证全局的递增性或绝对的唯一性,但在实际应用中,通过合理设置号段的大小和定期刷新机制,可以在性能和唯一性之间找到一个平衡点。


Snowflake模式


Snowflake是一个经典的号段生成算法,同时市面上存在大量的XXXflake算法.一般用作订单号。主要讲一下Snowflake的原理。


arch-z-id-3.png



  • 第1位占用1bit,其值始终是0,可看做是符号位不使用。

  • 第2位开始的41位是时间戳,41-bit位可表示2^41个数,每个数代表毫秒,那么雪花算法可用的时间年限是(1L<<41)/(1000L360024*365)=69 年的时间。

  • 中间的10-bit位可表示机器数,即2^10 = 1024台机器,但是一般情况下我们不会部署这么台机器。如果我们对IDC(互联网数据中心)有需求,还可以将 10-bit 分 5-bit 给 IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,具体的划分可以根据自身需求定义。

  • 最后12-bit位是自增序列,可表示2^12 = 4096个数。


不过Snowflake需要依赖于时钟,可能受到时钟回拨的影响。同时,如果并发生成ID的速度过快,可能导致序列号用尽。


总结


在选择UID生成方法时,需要根据具体的应用场景和需求权衡其优缺点。不同的场景可能需要不同的解决方案,以满足系统的唯一性要求和性能需求。那么你觉得原神的UID是如何生成的呢?如果是你该如何设计呢?


作者:半亩方塘立身
来源:juejin.cn/post/7324633501244063782
收起阅读 »

MyBatis实战指南(三):相关注解及使用

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。一、什么是注解(Annotation)首先,我们需要明...
继续阅读 »

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。

一、什么是注解(Annotation)

首先,我们需要明白什么是注解。注解 Annotation 是从JDK1.5开始引入的新技术。

Description

在Java中,注解是一种用于描述代码的元数据,它可以被编译器、库和其他工具读取和使用。MyBatis的注解就是用来简化XML配置的,它们可以让你的代码更加简洁、易读。

注解的作用:

  • 不是程序本身,对程序作出解释
  • 可以被其他程序读取到

Annotation格式:

注解是以@注解名的方式在代码中实现的,可以添加一些参数值

如:@SuppressWarnings(value=“unchecked”)

注解使用的位置:

package、class、method、field 等上面,相当于给他们添加了额外的辅助信息。

注解的分类:

1.元注解:

  • @Target:用于描述注解的使用范围

  • @Retention:用于描述注解的生命周期

  • @Documented:说明该注解将被包含在javadoc 中

  • @Inherited:说明子类可以继承父类中的该注解

  • @Repeatable:可重复注解

2.内置注解:

  • @Override: 重写检查

  • @Deprecated:过时

  • @SuppressWarnings: 压制警告

  • @FunctionalInterface: 函数式接口

3.自定义注解:

  • public @interface MyAnno{}

二、Mybatis常用注解

首先介绍一下Mybatis注解的使用方法:

第一步,在全局配置文件里的配置映射



    


第二步,在mapper接口的方法的上面添加注解

@Select("select * from user where uid = #{uid}")

    public User findUserById(int uid);

第三步,创建会话调用此方法。

接下来,我们来看看MyBatis中最常用的几个注解:

(1)@Select

作用:标记查询语句。

@Select用于标记查询语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Select注解时,需要在注解中指定SQL语句。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

User getUserById(@Param("id") Long id);

(2)@Insert

作用:标记插入语句。

@Insert用于标记插入语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Insert注解时,需要在注解中指定SQL语句。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

int addUser(User user);

(3)@Update

作用:标记更新语句。

@Update用于标记更新语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Update注解时,需要在注解中指定SQL语句。

示例:

@Update("UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}")

int updateUser(User user);

(4)@Delete

作用:标记删除语句。

@Delete用于标记删除语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Delete注解时,需要在注解中指定SQL语句。

示例:

@Delete("DELETE FROM users WHERE id = #{id}")

int deleteUserById(@Param("id") Long id);

(5)@Results

作用:用于指定多个@Result注解。

@Results用于标记结果集映射,该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@Results注解时,需要指定映射规则。

示例:


@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(6)@Result

作用:用于指定查询结果集的映射关系。

@Result用于标记单个属性与结果集中的列之间的映射关系。该注解可以用于接口方法或XML文件中,通常与@Results注解一起使用。使用@Result注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(7)@ResultMap

作用:用于指定查询结果集的映射关系。

@ResultMap用于标记结果集映射规则。该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@ResultMap注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@ResultMap("userResultMap")

User getUserById(@Param("id") Long id);

(8)@Options

作用:用于指定插入语句的选项。

@Options用于指定一些可选的配置项。该注解可以用于接口方法或XML文件中,通常与@Insert、@Update、@Delete等注解一起使用。使用@Options注解时,可以指定一些可选的配置项。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@Options(useGeneratedKeys = true, keyProperty = "id")

int insertUser(User user);

(9)@SelectKey

作用:用于指定查询语句的主键生成方式。

@SelectKey用于在执行INSERT语句后获取自动生成的主键值。该注解可以用于接口方法或XML文件中,通常与@Insert注解一起使用。使用@SelectKey注解时,需要指定生成主键的SQL语句和将主键值赋给Java对象的哪个属性。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)

int insertUser(User user);

(10)@Param

作用:用于指定方法参数名称。

@Param用于为SQL语句中的参数指定参数名称。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Param注解时,需要指定参数名称。

示例:

@Select("SELECT * FROM users WHERE name = #{name} AND age = #{age}")

List getUsersByNameAndAge(@Param("name") String name, @Param("age") Integer age);

(11)@One

作用:用于指定一对一关联关系。

@One用于在一对一关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@One注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。



  

  

  

  







  

  

  

  


上述代码中,@One注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对一关联查询。在departmentResultMap中,使用@One注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为manager,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@One注解之外,还可以使用@Many注解来指定一对多关联查询的映射方式。

总之,@One注解是MyBatis中用于在一对一关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对一关联查询的结果映射。

(12)@Many

作用:用于指定一对多关联关系。

@Many用于在一对多关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@Many注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。

示例:



  

  

  

  







  

  

  


上述代码中,@Many注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对多关联查询。在departmentResultMap中,使用@Many注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为members,ofType参数指定了集合中元素的类型为User,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@Many注解之外,还可以使用@One注解来指定一对一关联查询的映射方式。

总之,@Many注解是MyBatis中用于在一对多关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对多关联查询的结果映射。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

(13)@ResultType

作用:用于指定查询结果集的类型。

@ResultType用于指定查询结果的类型。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@ResultType注解时,需要指定查询结果的类型。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

@ResultType(User.class)

User getUserById(Long id);

(14)@TypeDiscriminator

作用:用于指定类型鉴别器,用于根据查询结果集的不同类型映射到不同的Java对象。

@TypeDiscriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@TypeDiscriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:



  

  

  

  

    

    

    

  








  

  







  








  


上述代码中,@TypeDiscriminator注解用于指定不同子类型的映射方式。在vehicleResultMap中,使用@TypeDiscriminator注解指定了类型列的名称为type,javaType参数指定了类型列的Java类型为String,标签中的value属性分别对应不同的子类型(car、truck、bus),resultMap属性用于指定不同子类型的结果集映射规则。

除了使用@TypeDiscriminator注解之外,还可以使用标签来指定不同子类型的映射方式。

总之,@TypeDiscriminator注解是MyBatis中用于在自动映射时指定不同子类型的映射方式的注解之一,可以方便地实现自动映射不同子类型的结果集映射规则。

(15)@ConstructorArgs

作用:用于指定Java对象的构造方法参数。

@ConstructorArgs用于指定查询结果映射到Java对象时使用的构造函数和构造函数参数。该注解可以用于XML文件中,通常与标签一起使用。使用@ConstructorArgs注解时,需要指定构造函数参数的映射关系。

示例:



  

  

    

    

  



(16)@Arg

作用:用于指定Java对象的构造方法参数。

@Arg用于指定查询结果映射到Java对象时构造函数或工厂方法的参数映射关系。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Arg注解时,需要指定参数的映射关系。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

User getUserById(@Arg("name") String name, @Arg("age") int age);

(17)@Discriminator

作用:用于指定类型鉴别器的查询结果。

@Discriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Discriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:


@Select("SELECT * FROM vehicle WHERE type = #{type}")

@Discriminator(column = "type", javaType = String.class, cases = {

  @Case(value = "car", type = Car.class),

  @Case(value = "truck", type = Truck.class),

  @Case(value = "bus", type = Bus.class)

})


List getVehiclesByType(String type);

(18)@CacheNamespace

作用:用于指定缓存的命名空间。

@CacheNamespace用于指定Mapper接口中的查询结果是否进行缓存。该注解可以用于Mapper接口上,用于指定Mapper接口中所有方法默认的缓存配置。使用@CacheNamespace注解时,需要指定缓存配置的属性。

示例:

@CacheNamespace(

  implementation = MyBatisRedisCache.class,

  eviction = MyBatisRedisCache.Eviction.LRU,

  flushInterval = 60000,

  size = 10000,

  readWrite = true,

  blocking = true

)


public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(19)@Flush

作用:用于在插入、更新或删除操作之后自动清空缓存。

@Flush是用于在Mapper接口中指定在执行方法前或方法后刷新缓存。该注解可以用于Mapper接口方法上,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Flush注解时,需要指定刷新缓存的时机。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Flush(flushCache = FetchType.AFTER)

User getUserById(Long id);

(20)@MappedJdbcTypes

作用:用于指定Java对象属性与数据库列的映射关系。

@MappedJdbcTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedJdbcTypes注解时,需要指定Java类型和对应的JDBC类型。

示例:

public class User {

  private Long id;

  @MappedJdbcTypes(JdbcType.VARCHAR)

  private String name;

  private Integer age;

  // ...

}

(21)@MappedTypes

作用:用于指定Java对象与数据库类型的映射关系。

@MappedTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedTypes注解时,需要指定Java类型。

示例:

@MappedTypes(User.class)

public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(22)@SelectProvider

作用:用于指定动态生成SQL语句的提供者。

@SelectProvider是用于在Mapper接口中动态生成查询SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@SelectProvider注解时,需要指定Provider类和Provider方法。

示例:

@SelectProvider(type = UserSqlProvider.class, method = "getUserByIdSql")

User getUserById(Long id);

(23)@InsertProvider

作用:用于指定动态生成SQL语句的提供者。

@InsertProvider用于在Mapper接口中动态生成插入SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@InsertProvider注解时,需要指定Provider类和Provider方法。

示例:

@InsertProvider(type = UserSqlProvider.class, method = "insertUserSql")

int insertUser(User user);

(24)@UpdateProvider

作用:用于指定动态生成SQL语句的提供者。

@UpdateProvider用于在Mapper接口中动态生成更新SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@UpdateProvider注解时,需要指定Provider类和Provider方法。

示例:

@UpdateProvider(type = UserSqlProvider.class, method = "updateUserSql")

int updateUser(User user);

(25)@DeleteProvider

作用:用于指定动态生成SQL语句的提供者。

@DeleteProvider用于在Mapper接口中动态生成删除SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@DeleteProvider注解时,需要指定Provider类和Provider方法。

示例:

@DeleteProvider(type = UserSqlProvider.class, method = "deleteUserSql")

int deleteUser(Long id);

以上就是MyBatis的相关注解及使用示例了,实际开发中不一定每个都能用到,但是可以收藏起来,有备无患嘛!

总的来说,MyBatis的注解是一个非常强大的工具,它可以帮助你减少XML配置的工作量,让你的代码更加简洁、易读。但是,它也有一定的学习成本,你需要花一些时间去理解和掌握它。希望这篇文章能帮助你更好地理解和使用MyBatis的注解。

收起阅读 »

300块成本从零开始搭建自己的家庭版NAS还可以自动备份,懂点代码有手就行!

前言 300块成本从零开始搭建自己的家庭版NAS,还可以手机上文件照片音乐自动备份,完全实现了自己的网盘效果,可以设置用户权限分配,目录上传、断点续传、并行上传、拖拽文件上传等日常操作。 为什么要搭建NAS? 现在的手机性能比以前强多了,所以每次换手机的...
继续阅读 »

前言



300块成本从零开始搭建自己的家庭版NAS,还可以手机上文件照片音乐自动备份,完全实现了自己的网盘效果,可以设置用户权限分配,目录上传、断点续传、并行上传、拖拽文件上传等日常操作。



PixPin_2024-01-14_21-24-12.png


为什么要搭建NAS?


现在的手机性能比以前强多了,所以每次换手机的原因居然是存储空间满了,不得不更换一个存储空间更大的手机,加上手机拍照,摄影,工作,生活,有娃的视频等,数据越来越多,我们需要一个性价比高的安全的存储介质。


目前市场上可选的方式很多,在线网盘,移动硬盘,U盘,私人NAS等。这些优缺点很明显,在线网盘,优点是最方便,下载个app完事,但缺点更多,大家懂的,空间大小要充值会员,下载速度要充值会员,一旦数据放上去了将会被收割个不停,更惨的是,完全没有个人隐私,想想都可怕,别人用你的数据去训练AI,你还在给他充值会员。移动硬盘和U盘,用起来最不方便,最后只能是选择NAS。


市面上的NAS分析


某宝一搜,市面上的NAS琳琅满目,经过我花了一个星期仔细筛查,主要分3种,群晖NAS(黑群晖),网络盒子,第三方公司销售的NAS云盘。大致如下:


image.png


(非广告,打码处理)



  • 群晖NAS,专业级别的NAS,性能高,效果好,价格也很感人,非公司级别也用不着,大炮打鸟的感觉

  • 网络盒子,看起来价格低廉,充值会员,流量,账号,空间,全都会卡着你

  • 第三方NAS云盘,经过研究,其所谓的外网链接都必须走他们公司的服务器转发,这意味着,你所有的数据都被别人看光光,这种还要看公司运营,还会有小公司倒闭等风险


我的私人NAS实现方式




  1. 购买一台微型服务器,接入到家庭路由

  2. 买几块硬盘挂载到服务器

  3. 部署开源的网盘系统,经过多种实验和研究,作者推荐Cloudreve社区开源网盘

  4. 通过内网穿透方式,把服务暴露出去

  5. 通过安装配置WebDAV协议访问的第三方文件管理器管理手机,通过web服务管理网盘所有数据



image.png


image.png


详细实现步骤


第一步:


购买一个微服务器,这里仅展示作者买的微服务器,不做广告和推荐,个人根据实际情况购买(如有需要可以和作者私下沟通)。大概100多即可购买一台,配置不同价格不同。买回来让商家预先安装了centos操作系统,买回来后插上路由器,连上家里的内网,在电脑上通过ssh连接上去。
PS:初始化系统相关信息可以问商家要。



image.png


第二步:


买一块硬盘通过USB接口接上去,这个完全有个人喜好,推荐机械硬盘,买个可插入多个盘位的硬盘外接盒子,安全又高效,这里可以参考之前的图,有示例,作者就买了个便宜货先用着。大约1个T,临时够用。



第三步:


部署开源网盘,我这里选择的是Cloudreve,原因如下:



  1. 开源系统,截止今日Star20.1K

  2. 中文支持的好,国产,Go语言架构,效率还行

  3. 支持WebDAV协议,可以用第三方app对接,研究了ES文件管理器,可以自动备份资料到服务器上去,IOS有专用app

  4. 前端UI做的不错,基础功能齐全

  5. 可以多用户权限管理,存储管理



image.png


部署文档参见官网,下期将会描述技术细节


第四步:


内网穿透,这里用的FRP,这个配置也折腾了我好久,要求我们要有一个服务器和域名,这个作者之前有几台非常便宜的服务器和域名在手,顺便做个部署即可,一般用户可以购买下各个云服务商的优惠版本,几百块1年非常便宜。



  • 第一个是要配置好服务端即我们的云服务器,开通ssh隧道,一个是开通转接http和https的接口,私人用无需https

  • 第二个是要配置客户端我们要放开的服务,即ssh和Cloudreve部署地址。



FRP部署技术将新开一个专题介绍


第五步:


WebDAV配置手机,我们先配置一个内网版本的网盘,然后根据内网穿透映射到外面的地址再配置一个外网的网盘,这样在家的时候我们通过连上路由器,用内网访问,速度快,建议备份都在内网时候传输,平时不在家的时候用外网来查看。



image.png


基于这个服务打通,我们可以干更多事情了,建个网站如何?



内外网打通,服务器有了,我们甚至可以做更多事情,建个网站,把家里的设备全部用服务器来管理,如果你家有视频监控,也可以备份到服务器!



更多部署软件部分细节,将在下期分享,



  • Cloudreve部署

  • FRP部署

  • WebDAV配置

  • 等等...

作者:天问cc
来源:juejin.cn/post/7323599971214802956
收起阅读 »

2023:情若能自控,要心有何用。。。。

情若能自控,要心有何用。。。。 一、开篇   岁末将至,人心渐老,百般滋味涌上心头,话到嘴边不值得一提。词穷不是沉默,而是一言难尽。该接受的不该接受的,都接受了,没啥不公平的,习惯了。看错人,不是瞎,是心软;信错人,不是傻,是重情义;爱错人,不是愚蠢,而是你的...
继续阅读 »

情若能自控,要心有何用。。。。


一、开篇


  岁末将至,人心渐老,百般滋味涌上心头,话到嘴边不值得一提。词穷不是沉默,而是一言难尽。该接受的不该接受的,都接受了,没啥不公平的,习惯了。看错人,不是瞎,是心软;信错人,不是傻,是重情义;爱错人,不是愚蠢,而是你的劫。什么事情都要自身找原因,不要苟且他人。鞋子脏了,是因为你走的路不干净。该反省的是自己的眼光和见识,永远不要怀疑自己的真诚和善良……好了,时间到了,该走了……



  • 我本两袖一清风,赤心可抵岁月长。

  • 孤身何惧人生苦,独行敢试不平路。

  • 红尘本是无情道,偏偏痴心博君笑。

  • 红杏枝头春意闹,不过岁月风中萧。

  • 梦逆光阴初到时,强船何惧风浪涌。

  • 奈何竟遇遭人欺,一人把这悲凉谱。

  • 惊鸿一瞥忘不了,只见得炊烟袅袅。

  • 此生恐难再相逢,坠落片片葬夙梦。

  • 无人问津又何妨,逍遥自在人心好。

  • 浮云千载悠悠过,何曾片缕下中州。


二、我与职场


2.1 追风赶月莫停留


  我依然在北京这座城市漂泊,我没有勇气或者说没有足够的能力与底气回到老家扎根,我依然过着普普通通的周中上班周末摆烂的人生,两点一线的在舒适圈中挣扎,不愿逃脱。在我将近九年多的工作经历中,共经历了4家公司。在工作经验不断积累的过程中,公司各种乱象或不公,几乎都经历过。也因为长期的隐忍最终爆发,开始排斥人在公司,还要平衡工作情绪+奇葩管理,但不排斥工作。我更倾向于居家办公,你给我钱,我给你成果,不需要乌烟瘴气的办公氛围,不需要能者多劳的pua,更不需要尔虞我诈的利用。

经历了公司大规模裁员,同事有被迫离职的,也有自己跳槽走人的,导致对自己的职业生涯产生了迷茫,跳槽 or 副业,一时不知道该如何选择。对于我来说,这一年的工作情况可以用四个字来形容,那就是"平平无奇",工资也是"纹丝未动"。我好似一只大蛤蟆,公司则如一锅正在加热的温水。这一年唯一的收获就是工作越来越顺手了,然后工作也变得一成不变,接需求、分析、设计、开发、测试、上线,每天好像在坐牢一样,没有一点技术含量。感觉如果继续呆下去,再过几年我就可以回家烤红薯了。


2.2 平芜尽处是春山


  说真的,今年可能是个人技术能力提升最小的一年,我竟然没有任何值得拿出手的东西,我的时间就这样白白流逝了,好像已经很努力了,但是依然很普通,导致想跳槽都没信心。一方面是因为其他事情耽搁了,另一方面的确是有点懈怠了,在工作中用不到的新技术就很少像以前那样去学习了,对已掌握的知识点也缺少动力去继续深挖了。这点的确不太好,只要还在这个行业,就如逆水行舟,不进则退。

这一年的我,可以说是从迷茫到醒悟。现在的技术层出不穷,似乎大家都在卷各种技术,例如 Flutter、Framework、Docker等等。或许大家都有跟我一样的感受,面对不断涌现的新技术,难免会让人感到迷茫,不知所措,应该躺平呢?还是盲目跟风卷呢?我真的能选择躺平吗?拼爹不行,拼存款没有,夹杂着公司裁员、经济形势不好的情况下,我决心改变自己。虽然在工作中不能提升技术,但是自己不能放弃自己,不然辞职就等于失业。首先要改变手机占用我的时间,虽然这很困难,但我不能倒在刷剧、刷短视频的魔爪之下,以学习、编写技术文章为重要事项,逼迫自己学习。为防止自己因为太难而打退堂鼓,前期制定些简单任务:一周一个核心的技术知识点,两周一篇技术文章。随着学的东西越来越多,写的文章也被更多人阅读和关注时,任务适当加大难度。所以今年在闲暇时间学习了很多东西,如 Vue 组件、Docker容器等,立志成为一名全栈工程师。截止年底,不知不觉中竟然写了160多篇随记、40多篇技术文章。当然有的是没有发表在博客上,至于为啥就不用说了,懂得都懂。

虽然我不知道 35 岁后(如果我能活到那个时候)程序员何去何从,在中国35岁是一个比较尴尬的年龄,35岁嫌老、65嫌年轻。如果一旦失业,很有可能会受到其余公司HR的歧视。做技术的学的技术一定要顺应时代的发展,社会需要什么黑科技,就要花时间去钻研。我知道现在不努力积累自己的专业知识,未来只会如逆水行舟,一步步将我推回起点。疫情三年真的是大浪淘沙,淘汰只会是那些不脚踏实地学习和工作的人,出来混迟早要还的。只有现在奋力前行,未来才有更多的选择机会。


2.3 人生苦短,帮我倒满


  这一年我遇到让我心动的那人,其实,我现在也没想好该怎么描述这段不太好的经历,怎么说呢,那种感觉就好像开局就被针对了一样,完全发育不起来!
  这段我写下她身上我喜欢的点吧,淡妆、穿着很朴素、自然,不做作。再一个就是我很喜欢她努力学习的样子,真的安静的像一道风景,我总会在旁边偷偷的看她,一边看一边傻笑。有的时候,她还有些小小的笨拙,让我觉得很喜欢,这个姑娘我不是凑合,是真的喜欢。

虽然在一起的时间不到一个月就分开了,之后那段时间我整个人精神恍惚,开始就剧烈的呕吐,整晚头疼的睡不着,去了趟医院,诊断结果是脑内伤,可能伴有中度抑郁,情况有些麻烦,给我开了一堆又一堆的治疗抑郁的药,又建议一个月再复诊。这一点也是吓到了我,也让我意识到生活和工作应该分开的道理,工作是我们赚钱的工具,不应该成为毁坏我们身体的元凶。趁着十一放假我跟一个小伙伴一起去了拉萨(遗憾的是我手机落在了小伙伴车上),这期间我遇到一个喇嘛,姑且算是算命吧,他说木性温暖,火伏其中,钻灼而出,故木生火。而我乃火命,故而需要木属性之物常伴身旁,恰好我带着一对儿核桃,此物可助我驱祸免灾。虽然我不怎么相信这些,但为了心安勉强接受。医院复诊的结果还是不出意外的坏,又恰逢不到半年间两位友人的离世,时日不多的我不得不把这些年来开发项目(纯属个人)卖掉,再加上我工作以来积攒的钱,一部分用来做父母的养老之用,一部分给父母买了养老保险,剩下的留作我半年出行之用,毕竟有些地方我一直想去,但总因种种原因不得行,这次终于可以出动了。

人生中,多的是身不由己的时刻,得也好,失也罢,都要坦然面对。喝酒不问度数,酒后不问去处。人生苦短,帮我倒满倒满……


be0b6d6e703610cffce19cc234066e56.jpeg


三、关于个人


3.1 漫天神佛不识君,幽冥可曾有知心


  2023 是疫情恢复的第一年,褪去口罩的滤镜,我们更真切的看懂了这个世界,大家都活明白了,房子不买可以租,车子能开就行。所谓的财富,在生命和健康面前微不足道;个人的努力,在时代面前微不足道,你不涨工资、买不起眉笔,也不是能力不行,降低欲望、降低消费也可以过的很好。做饭的尽头是大铁锅,衣服的尽头是保暖舒适,消费主义的尽头是断舍离,万事的尽头是尽人事知天命,幸福的尽头是平安、健康。我们是失去了很多人,但就算公交车上空无一人,司机师傅还是会把车开到终点站。战乱也让我们明白,原来生在一个和平的国家,是那么的幸福。很多人的生日愿望,也从财富、爱情变成了希望世界和平。

2023年是割裂的一年,朋友圈好像所有的人都在旅游,但携程用户从2600万跌到了600万;外卖员、网约车司机变多了,可滴滴用户却从4500万跌到了1000万;摆地摊的越来越多了,但怪兽充电宝却从300万跌到了100万;2023年失信被执行人数突破了800万,上半年有46万多家公司倒闭,boss直聘月活用户却突破了一个亿;考公的人越来越多,创业的越来越少;药店越来越多,孩子越来越少;房子越盖越多,股民越炒越少……口罩是我们最后的遮羞布,以后再赚不到钱就没有借口了。今年甚至连除夕回家都成了奢望,这一年世界也很混乱,很多生命都定格在了2023,我最好的朋友也留在了这一年,我经常梦见他……我觉得人生就像下面这张画一样:虽前路依旧光明、未来就在彼岸,可我却深处黑暗独帆前行……


20231209201408.png


3.2 但饮孟婆解千忧,余后共赴忘川流


  日月蹉跎,人已将老而功业未建。我这等人,真的能成大业吗?我没有变,只是心情变了。我还是我,只是面对现实,多了点无奈、多了点沉默。我曾享受过一天晚上花几千,也体验100块钱都借不到。人嘛,享受过不该拥有的风光,就要承受随之而来的报应。所有的事情都抵不过时间和现实,让人成熟的从来不是年龄,而是经历。我觉得今年本该幸福的,可这烂透了的生活,却耗尽我所有的精力。样样都不顺心,事事都不如意,都说先苦后甜,可是我连最基本的快乐都给不了自己,却又无能为力!好像有迹可循,又好像无路可走。我原本以为今年我会很幸福的,可是记不清了,只记得今年心态崩多少次!我真的不喜欢今年,今年让我太难过了。我讨厌现在的自己,一边压抑着自己的情绪,一边装作什么都没事的样子,一到深夜,就彻底崩溃,天亮后还要微笑的去面对一切。

曾有人问过我,2023年我们到底收获了什么?也许,还活着,算是我今年最大的收获吧!好好活下去,朋友们,哪怕凑合的活下去。的确,不是所有的坚持都会有收获,但总有一些坚持,能在一寸冰封的土地里培育出香甜的果实……不是有所成就才算活着,梦想也不是多么了不起的东西,只喜欢看天走路、吃烧烤的人生,也很好!


9af4a8316e3a15fd71219e90a0f05b82.jpeg



日落归山海,山海藏深意,没有人能不遗憾!



四、小结



把今天最好的表现当作明天最新的起点..~



  投身于天地这熔炉,一个人可以被毁灭,但绝不会被打败!一旦决定了心中所想,便绝无动摇。迈向光明之路,注定荆棘丛生,自己选择的路,即使再荒谬、再艰难,跪着也要走下去!放弃,曾令人想要逃离,但绝境重生方为宿命。若结果并非所愿,那就在尘埃落定前奋力一搏!


划重点.gif


作者:独泪了无痕
来源:juejin.cn/post/7324165965205225522
收起阅读 »

原来我们是这样对工作失去兴趣的

大家好,我是「云舒编程」,今天我们来聊聊那些让我们对工作失去兴趣的原因。 一、前言    相信很多人有过接手别人的系统,也有将自己负责的系统交接给别人的经历。既有交接出去不用在费力治理维护技术债务的喜悦,也有接手对方系统面对一系列维护问题的愁容满面。    但...
继续阅读 »

大家好,我是「云舒编程」,今天我们来聊聊那些让我们对工作失去兴趣的原因。


一、前言


   相信很多人有过接手别人的系统,也有将自己负责的系统交接给别人的经历。既有交接出去不用在费力治理维护技术债务的喜悦,也有接手对方系统面对一系列维护问题的愁容满面。

   但是被人嫌弃的系统曾经也是「创建人」心中的白月光啊,是什么导致了「白月光」变成了「牛夫人」呢?是996,工期倒排、先上再优化,还是随时变动的需求?

   让我们来复盘系统是怎么一步一步腐化的,让我们丢失了最初的兴趣,同时总结一些经验教训以及破局之策。


二、白月光到牛夫人的经历


一般当我们设计一个系统时,总是会抱着要把该项目打造为「干净整洁」的项目的想法,


图片


但是随着时间的推移,最后总是不可避免的变成了这样:


图片


2.1、从0到1


   我们发现大多数人对于创建新项目总是会抱有极大的激情兴趣,会充分的考虑架构设计。但是对于接手的项目就会缺乏耐心。

   这种心理在《人月神话》一书中被说为编程职业的乐趣:

“首先,这种快乐是一种创建事物的纯粹快乐。如同小孩在玩泥巴时 感到快乐一样,成年人喜欢创建事物,特别是自己进行设计 。我想这种 快乐是上帝创造世界的折射,一种呈现在每片独特的、崭新的树叶和雪 花上的喜悦。”

“第四,这种快乐是持续学习的快乐,它来自于这项工作的非重复特性。人们所面临的问题总有这样那样的不同,因而解决问题的人可以从 中学习新的事物,有时是实践上的,有时是理论上的,或者兼而有之。”

图片

正是由于这样的心理,人们在面对新系统时,可以实践自身所学,主动思考如何避开曾经遇到的坑。满足了内心深处的对于创造渴望。
   当一个项目是从0到1开始设计的,并且前期是由少数「高手」成员主导开发的话,一般不会有债务体现。当然明面上没有债务,不代表没有埋下债务的种子。


2.2、抢占市场、快速迭代


   系统投入市场得到验证后,如果顺利,短期会收获大量用户。伴随着用户指数增长的同时,各种产品需求也会随着而来。一般在这个阶段将会是「工期倒排、先上再优化,需求随时变动」的高发期。

   同时由于需求的爆发,为了提高团队的交付率,在这个阶段会引入大量的“新人”。随着带来的就是新老思想的碰撞,新的同学不一定认同之前的架构设计。

   在这个阶段,如果团队存在主心骨,可以“游说”多方势力,平衡技术产品、新老开发之间的矛盾,那么在这个阶段引入的债务将会还好。但是如果团队缺乏这样的角色,就会导致公说公有理婆说婆有理,最后的结果就是架构会朝着多个方向发展,一个项目里充斥着多种不同思路的设计。有些甚至是矛盾的。如果还有【又不是不能用】的想法出现,那将是灭顶之灾。

图片

但是在这个阶段对于参与者又是幸福的,一份【有市场、有用户、有技术、有价值】的项目,无论是对未来的晋升还是跳槽都是极大的谈资。


2.3、维护治理


   褪去了前期“曾经沧海难为水,除却巫山不是云”的热恋后,剩下的就是生活的柴米油盐。系统的最终结局也是“维护治理”。

   在这个阶段,需求的数量将大大减少,但是由于前期的“快速建设”,一个小小的需求,我们可能需要耗费数周的时间去迭代。而且系统越来越复杂,迭代越来越困难。
   
同时每天需要花费大量的时间处理客诉、定位bug,精力被完全分散。系统的设计和技术慢慢的变得僵化。并且由于用户量巨大,每次改动都要承担很大的线上风险。这样的情况对于程序员的精力、体力都是一场内耗,并且如果领导的重心也不在此项目时,更会加重这种内耗。于是该系统就会显得“人老色衰”,曾经的「白月光」也就变成了「牛夫人」。

图片


三、牛夫人不好吗?


3.1、缺乏成就感


《人月神话》中关于程序员职业的苦恼曾说过以下几点:



  1. 对于系统编程人员而言,对其他人的依赖是一件非常痛苦的事情。他依靠其他人的程序,而这些程序往往设计得并不合理、实现拙劣、发布不完整(没有源代码或测试用例)或者文档记录得很糟。所以,系统编程人员不得不花费时间去研究和修改,而它们在理想情况下本应该是可靠的、完整的。

  2. 下一个苦恼---概念性设计是有趣的,但寻找琐碎的bug却是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的 劳动。程序编制工作也不例外。

  3. 最后一个苦恼 ,有时也是一种无奈—当投入了大量辛苦的劳动 ,***产品在即将完成或者终于完成的时候,却已显得陈旧过时。***可能是同事或竞争对手已在追逐新的、更好的构思; 也许替代方案不仅仅是在构思 ,而且己经在安排了。


随着业务趋于稳定,能够发挥创造性的地方越来越少,剩下的更多是沉闷、枯燥的维护工作。并且公司的资源会更聚焦在新业务、新方向,旧系统获得的关注更少,自然而然就缺乏成就感。也就形成了【只见新人笑,不见旧人哭


3.2、旧系统复杂、难以维护


《A Philosophy of Software Design》一书中对复杂性进行了如下定义:“Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.”,即任何使得软件难于理解和修改的因素都是复杂性。

作者John 教授又分别从三个角度进行了解释复杂性的来源:


3.2.1、变更放大


   复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。对于复杂的系统来说,如果代码没有聚敛,那么一次小小的需求可能会导致多处修改。同时为了保证不出故障,需要对涉及的修改点、功能点进行完整的覆盖测试。这种隐藏在背后的工作量是巨大的。


3.2.2、认知负荷


   复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。当一个系统经过多年的迭代开发,其复杂度将是指数级别的。并且会充斥这很多只有“当事人”才能理解的“离谱”功能。这就对维护者提出了极高的要求,需要掌握很多“冰山下”的知识才能做到手到擒来。而这对维护者的耐心、能力又是一次挑战。

图片


3.2.3、未知的未知


   未知的未知是指必须修改哪些代码才能完成任务,或者说开发人员必须获得哪些信息才能成功地执行任务。这一项也是John Ousterhout教授认为复杂性中最糟糕的一个表现形式。
     这句话看起来比较抽象,如果映射到我们日常的工作中就是“我也不知道为啥这么改就好了”、“在我这是好的呀”、“刚刚还能运行啊,不知道为啥现在突然不行了”。

   这种情况就是我们不知道改动的这行代码是否能让程序正常运转,也不知道这行代码的改动是否又会引发新的问题。这时候我们发现,那些“上帝类”真的就只有上帝能拯救了。

   我曾经维护一个老系统,源码是通过文件相互传递的,没有仓库,里面的框架是自己撸的轮子,没有任何说明文档。服务部署是先在本地编译成二进制文件,然后在上传到服务器启动。每次改动上线都是就是一次生死劫,幸好没过多久,这个系统就被放弃了。


四、为何变成了牛夫人


4.1、伪敏捷


   “敏捷”已经成为了国内公司的银弹了。
   需求不做市场分析、不考虑用户体验、不做设计分析、不考虑前因后果,美其名曰“敏捷”。
   工期倒排、先上再说、明天能不能上、这个问题上了再优化,美其名曰“敏捷”。

   我曾经参与过一个项目,最开始是给了三个月的时间完成产品规划+开发,但是项目立项后领导层迟迟无法达成统一,一直持续进行了两个月的讨论后,终于确定了产品模型,进入开发。到这里留给开发的时间还剩一个月,咬咬牙也能搞定。但是开发真正进场,上报了需要开发的周期后,上面觉得太慢了,要求3周搞定,过了一天还是觉得太慢,要求2周,最后变成了要求4天搞定。遇到这种情况能怎么办,只能加人,只能怎么快怎么来。

   之前阿里程序员也开玩笑式的说出了类似的场景:“2月13号上午省领导问逍遥子全省的健康码今天上线行不行,逍遥子说可以。等消息传达到产研团队的时候已经是中午了,然后团队在下午写了第一行代码。”


4.2、人的认知局限


   《人月神话》一书中提到了一种组建团队的方式「外科手术团队」:“十个人,其中七个专业人士在解决问题,而系统是一个人或者最多两个人思考的产物,因此客观上达到了概念的一致性。”
   也就是团队只需要一个或者两个掌舵人,负责规划团队的方向和系统架构,其余人配合他完成任务。目前我所待过的团队也基本是按照这个模式组成的,领导会负责重要事情的决策和团队分歧时的拍板,其余人则相互配合完成目标任务。但是这样的模式也导致了一个问题:掌舵人的认知上限就决定了团队的上限,而这种认知上限天然就会导致系统架构设计存在局限性,而这种局限性又会随着“伪敏捷”放大


4.3、人员流动


   经历过这种离职交接、活水交接的打工人应该深有体会,很多项目一旦步入这个阶段,大多数负责人就会开始放飞自我,怎么快怎么来,只想快点结束这段工作,快速奔赴下一段旅程。
   从人性的角度是很难评价这种情况的,毕竟打工人和老板天然就不是一个战线的,甚至可能是对立面的。而我们大多数人都不可能是圣人,从自身角度从发这种行为是无可厚非的。


五、如何保持白月光


   这里想首先抛个结论,系统变腐化是不可避免的。就类似人一样,随着时间的流逝,也会从以前一个连续熬夜打游戏看小说第二天依旧生龙活虎的青年变为一个在工位坐半小时都腰酸背痛,快走几步都喘的中年人。而这都来源于生活的压力、家庭的压力、工作的压力。同样的,面对业务的压力、抢占市场的压力、盈利的压力,系统也不可避免会变成“中年人”。
   就像人一样会使用护肤品、健身等手段延缓自己的衰老一样,我们也可以使用一些手段延缓系统“衰老”。
   在网上,已经有无数的文章教怎么避免代码腐化了,例如“DDD领域驱动设计”、“业务建模”、“重构”等等。
   今天我想从别的角度聊聊怎么延缓代码腐化。


5.1、避免通用


   软件领域有个特点,那就是复用。程序员们总是在思考怎么样写一段到处通用的代码,以不变应万变。特别是当国内提出中台战略后,这种情况就如脱缰的野马一般,不可阻挡。要是你做的业务、架构不带上xx中台,赋能xx,你都觉得你低人一等。
   但是其实我们大部分人做的都是业务系统,本身就是面向某块特定市场的、特定用户的。这就天然决定了其局限性。
   很多时候你会发现你用了100%的力气,设计了一个80%你认为有用的通用中台,最后只有20%产生了作用,剩下60%要么再也没有动过,要么就是被后来参与者喷的体无完肤。
   当然这里也不说,设计时就按照当前产品提出的需求设计就行,一点扩展的余地都不留,而是在「通用」与「业务需求」之间取一个平衡。而这种平衡就取决于经验了。如果你没有这方面经验,那你就去找产品要「抄袭」的是哪个产品,看看他们有哪些功能,可以预留这些功能的设计点。


5.2、Clean Code


说实话,国内的业务系统80%都没有到需要谈论架构设计的地步。能够做到以下几点已经赢麻了:



  1. 良好的代码注释和相关文档存档【重中之重】

  2. 避免过长参数

  3. 避免过长方法和类

  4. 少量的设计模式

  5. 清晰的命名

  6. 有效的Code Review【不是那种帮我CR下,对方1秒后回复你一个done】


5.3、学会拒绝


   自从国内开始掀起敏捷开发的浪潮后,在项目管理方面就出现了一个莫名其妙的指标:每次迭代的需求数都有会有一个数值,而且还不能比上一次迭代的少。
   这种情况出现的原因是需求提出者无法确定这个需求可以带来多大的收益,这个收益是否满足老板的要求。那么他只能一股脑上一堆,这样即使最后效果不及预期,也可以怪罪于用户不买账。不是他不努力。
   在这种时候,就需要开发学会识别哪些是真需求,哪些是伪需求,对于伪需求要学会说不。当然说不,不是让你上来就是开喷,而是你可以提出更加合理的理由,甚至你可以提出其他需求代替伪需求。这一般需要你对这块业务有非常深入的研究,同时对系统有上帝视角。
   基本上在每个公司的迭代周期都是有时间要求的,比如我们就是两周一迭代,如果需求是你可控的,那么你就有更多的时间和心思在维护系统上,延缓他的衰老。


结尾


       分享一些我摸鱼时喜欢看的书,除了本文总是提到的《人月神话》《A Philosophy of Software Design》外,还有《黑客与画家》、《演进式架构》。有需要的可以关注 公众号「云舒编程」,回复"书籍"即可免费获取:跳转地址


图片


作者:云舒编程
来源:juejin.cn/post/7312724606605918249
收起阅读 »

Camera2 同时预览多个摄像头,CameraX不行?

本来是想通过CameraX实现同时预览多个摄像头,通过官网文档介绍,在CameraX 1.3 后通过ConcurrentCamera运行多个摄像头,但实际在小米10(Android 13)运行,报错当前设备不支持ConcurrentCamera,代码Camer...
继续阅读 »

本来是想通过CameraX实现同时预览多个摄像头,通过官网文档介绍,在CameraX 1.3 后通过ConcurrentCamera运行多个摄像头,但实际在小米10(Android 13)运行,报错当前设备不支持ConcurrentCamera,代码CameraProvider.availableConcurrentCameraInfos查询也是返回数量0,表示设备不支持。


请教ChatGPT回答,来进行编写,回答可以通过代码创建多个previewrequireLensFacing,但是实际运行时不可行的。程序会报下面代码问题,选择摄像头设备异常。


val cameraSelector =builder
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build()

因此个人下定义是在cameraX 1.3.0-alpha07前应该是不支持预览多摄像头的。如果有小伙伴验证OK,希望可以告知,多谢。


故采用Camera2来实现多摄像头同时预览。


Camera2 同时预览摄像头


记得先申请权限,以及动态申请!!


    <uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

记得先申请权限,以及动态申请!!


1、判断设备是否支持摄像头


fun isSupportCamera(): Boolean {
initCameraManager()
return cameraManager!!.cameraIdList.isNotEmpty()
}

initCameraManager主要是初始化CameraManager对象cameraManager。我们通过cameraIdList列表是否空来判断是否有摄像头。


private fun initCameraManager() {
if (cameraManager == null) {
cameraManager = getApplication<Application>().getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
}
}

2、获取摄像头列表


我们遍历第1步获取到的摄像头ID列表,然后通过getCameraCharacteristics查询该摄像头相关的数据,封装到NCameraInfo对象中。这里我们只查询几个简单的信息。


fun getCameraListInfo() {

initCameraManager()

if (cameraManager.cameraIdList.isNotEmpty()) {
for (cameraId in cameraManager.cameraIdList) {
val cameInfo = NCameraInfo()
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)

cameInfo.id = cameraId
cameInfo.face ="${ getFaceStr(facing)},CameraId:${cameraId}"
cameraMap[cameraId] = cameInfo
}
cameraInfo.value = cameraMap.values.toList()
}
}

3、打开摄像头


打开摄像头非常简单,只需要调用openCamera函数即可,主要是stateCallback函数的实现。其中handler,是用来切换到主线程var handler = Handler(Looper.getMainLooper())


fun openCamera(cameraId: String) {
initCameraManager()
cameraManager?.openCamera(cameraId, stateCallback, handler)
}

我们一起看看stateCallback函数的实现。也就是当我们打开摄像头,摄像头相关状态会通过下面三个函数进行回调,因为这里采用ViewModel方式,所以会多一份回调到Activity。不用着急,最后有完整代码。


   private val stateCallback=object : StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraMap[camera.id]?.apply {
cameraDevice = camera
state = 1
cameraCallback?.onCameraOpen(this)
}

}

override fun onDisconnected(camera: CameraDevice) {
cameraMap[camera.id]?.apply {
cameraDevice = camera
state = 0
cameraCallback?.onCameraClose(this)
}
}

override fun onError(camera: CameraDevice, error: Int) {
Log.e(TAG, "camera ${camera.id} error code:${error}")
cameraMap[camera.id]?.apply {
cameraDevice = camera
state = 3
cameraCallback?.onCameraError(this,error)
}
}
}

我们查看Activity中的实现。onCameraOpen函数主要动态创建TextureView对象,添加到界面中,用于预览摄像头内容。


	 override fun onCameraOpen(camera: NCameraInfo) {
adapter.notifyItemChanged(adapter.items.indexOf(camera))

//创建TextureView
val textureView = TextureView(this)
textureView.id = View.generateViewId()
camera.previewId=textureView.id
val layoutParams = LinearLayout.LayoutParams(previewWidth, LayoutParams.MATCH_PARENT)
viewBinding.llCameraPreview.addView(textureView, layoutParams)

//textureview 与摄像头绑定
textureView.surfaceTextureListener=object:SurfaceTextureListener{
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
//创建Surface并用于摄像头渲染
val surface = Surface(textureView.surfaceTexture)
val builder = camera.cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)!!
builder.addTarget(surface)

camera.cameraDevice?.createCaptureSession(listOf(surface), object : StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
session.setRepeatingRequest(builder.build(),null,model.handler)
}

override fun onConfigureFailed(session: CameraCaptureSession) {

}
}, model.handler)
}

override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
Log.d(TAG,"onSurfaceTextureSizeChanged")
}

override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.d(TAG,"onSurfaceTextureDestroyed")
return true
}

override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
//Log.d(TAG,"onSurfaceTextureUpdated")
}
}


}

override fun onCameraClose(camera: NCameraInfo) {
Log.d(TAG,"onCameraClose:${camera}")
adapter.notifyItemChanged(adapter.items.indexOf(camera))
camera.cameraDevice?.close()
val view=viewBinding.llCameraPreview.findViewById<TextureView>(camera.previewId)
viewBinding.llCameraPreview.removeView(view)
}

override fun onCameraError(camera: NCameraInfo, error: Int) {
Log.e(TAG,"onCameraError:${camera}${error}")
adapter.notifyItemChanged(adapter.items.indexOf(camera))
camera.cameraDevice?.close()
}

4、效果


image-20230615220353385


5、小坑



  • 实测在小米10手机,先开启后摄,再开启前摄,前摄无法打开=》异常。先开前摄,再开后摄正常。

  • 小米11、诺基亚x7实测正常。


项目地址,点我跳战,关键类:Camera2Activity


作者:新小梦
来源:juejin.cn/post/7244783947821236285
收起阅读 »

总是听说 Vue3 选择 Proxy 的原因是性能更好,不如直接上代码对比对比

web
逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.definePropert...
继续阅读 »

逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.defineProperty 性能更好,因此自己创建了一个小 demo 来对比二者在不同场景下的性能。



以下测试仅在 谷歌浏览器 中进行,不同浏览器内核不同,结果可能有差异。可以访问此 在线地址 测试其他环境下的性能。



封装响应式


本文不会详细解析基于 Object.definePropertyProxy 的封装代码,这些内容在多数文章中已有介绍。Vue3 对嵌套对象的响应式处理进行了优化,采用了一种惰性添加的方式,仅在对象被访问时才添加响应式。相比之下,Vue2 采用了一次性递归处理整个对象的方式添加响应式。为了确保比较的公平性,本文下面的 Object.defineProperty 代码也采用了相同的惰性添加策略。


Object.defineProperty


/** Object.defineProperty 深度监听 */
export function deepDefObserve(obj, week) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
let value = obj[key]

Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
if (
typeof value === "object" &&
value !== null &&
week &&
!week.has(value)
) {
week.set(value, true)
deepDefObserve(value)
}
return value
},
set(newValue) {
value = newValue
},
})
}
return obj
}

Proxy


/** Proxy 深度监听 */
export function deepProxy(obj, proxyWeek) {
const myProxy = new Proxy(obj, {
get(target, property) {
let res = Reflect.get(target, property)
if (
typeof res === "object" &&
res !== null &&
proxyWeek &&
!proxyWeek.has(res)
) {
proxyWeek.set(res, true)
return deepProxy(res)
}
return res
},
set(target, property, value) {
return Reflect.set(target, property, value)
},
})
return myProxy
}

测试性能


测试场景有五个:



  1. 使用两个 API 创建响应式对象的耗时,即 const obj = reactive({}) 的耗时

  2. 测量对已创建的响应式对象的属性进行访问的速度,即 obj.a 的读取时间。

  3. 测量修改响应式对象属性值的耗时,即执行 obj.a = 1 所需的时间。

  4. 创建多个响应式对象,并模拟访问和修改它们属性的操作,以评估在多对象场景下的性能表现。

  5. 针对嵌套对象进行响应式性能测试,以评估在复杂数据结构下的性能表现。


初始化性能


const _0_calling = {
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(data, keys[i], {
get() {},
set() {},
})
}
},
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = new Proxy(data, {
get() {},
set() {},
})
},
}

image.png


很明显,Proxy 的性能优于 Object.defineProperty


读取性能


const readDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const readProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _1_read = {
useObjectDefineProperty() {
readDefData.a
readDefData.b
readDefData.e
},
useProxy() {
readProxyData.a
readProxyData.b
readProxyData.e
},
}

image.png


Object.defineProperty 明显优于 Proxy


写入性能


const writeDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const writeProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _2_write = {
count: 2,
useObjectDefineProperty() {
writeDefData.a = _2_write.count++
writeDefData.b = _2_write.count++
},
useProxy() {
writeProxyData.a = _2_write.count++
writeProxyData.b = _2_write.count++
},
}

image.png


Object.defineProperty 优于 Proxy,不过差距不大。


多次创建及读写


export const _4_create_read_write = {
count: 2,
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
deepDefObserve(data)
data.a = _4_create_read_write.count++
data.b = _4_create_read_write.count++
data.a
data.c
},
proxyWeek: new WeakMap(),
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = deepProxy(data, _4_create_read_write.proxyWeek)
proxy.a = _4_create_read_write.count++
proxy.b = _4_create_read_write.count++
proxy.a
proxy.c
},
}

image.png


Proxy 优势更大,但这个场景并不多见,很少会出现一次性创建大量响应式对象的情况,对属性的读写场景更多。


对嵌套对象的性能


对内部的每个属性都进行读或写操作


const deepProxyWeek = new WeakMap()
const defWeek = new WeakMap()
export const _5_deep_read_write = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
defWeek
),
useObjectDefineProperty() {
_5_deep_read_write.defData.res.code = _5_deep_read_write.count++
_5_deep_read_write.defData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.defData.res.message.error
_5_deep_read_write.defData.res.data[0].id
_5_deep_read_write.defData.res.data[0].name
_5_deep_read_write.defData.res.data[1].id
_5_deep_read_write.defData.res.data[1].name
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
deepProxyWeek
),
useProxy() {
_5_deep_read_write.proxyData.res.code = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.message.error
_5_deep_read_write.proxyData.res.data[0].id
_5_deep_read_write.proxyData.res.data[0].name
_5_deep_read_write.proxyData.res.data[1].id
_5_deep_read_write.proxyData.res.data[1].name
},
}

image.png


Object.defineProperty 会稍好一些,但两者的差距不大。


只读取修改嵌套对象的浅层属性


const _6_deepProxyWeek = new WeakMap()
const _6_defWeek = new WeakMap()
export const _6_update_top_level = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_deepProxyWeek
),
useObjectDefineProperty() {
_6_update_top_level.defData.res.code = _6_update_top_level.count++
_6_update_top_level.defData.res.message.error
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_defWeek
),
useProxy() {
_6_update_top_level.proxyData.res.code = _6_update_top_level.count++
_6_update_top_level.proxyData.res.message.error
},
}

image.png


这个场景 Proxy 略优于 Object.defineProperty


总结


Proxy 在对象创建时的性能明显优于Object.defineProperty。而在浅层对象的读写性能方面,Object.defineProperty 表现更好。但是当对象的嵌套深度增加时,Object.defineProperty 的优势会逐渐减弱。尽管在性能测试中,Object.defineProperty 的读写优势可能更适合实际开发场景,但在 谷歌浏览器 中,Proxy 的性能与 Object.defineProperty 并没有拉开太大差距。因此,Vue3 选择 Proxy 不仅仅基于性能考量,还因为 Proxy 提供了更为友好、现代且强大的 API ,使得操作更加灵活。


作者:clench
来源:juejin.cn/post/7324141201802821672
收起阅读 »