注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

开发一个简单的管理系统,前端选择 Vue 还是 React?

web
在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。 我们在低代码开发领域探索了多年...
继续阅读 »

在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。



我们在低代码开发领域探索了多年,从2014 开始研发低代码前端渲染,到 2018 年开始研发后端低代码数据模型,发布了JNPF快速开发平台。


JNPF是一个Vue2/Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可完成业务需求。


前端采用的是Vue、Element-UI…;后端采用Java(.net)、Springboot…;使用门槛低,支持分布式、k8s集群部署,适用于开发复杂的业务管理系统(ERP、MES等);采用可视化组件模式可以有效地扩展不同的业务功能,并方便实现各种业务需求,且不会导致系统臃肿,若想使用某个组件,按需引入即可,反之亦然。



低代码平台的前端框架采用Vue的优势有哪些?




  •  Vue是组件化开发,减少代码的书写,使代码易于理解。




  •  最突出的优势在于可以对数据进行双向绑定。




  •  相比较传统的用超链接进行页面的切换与跳转,Vue使用的是路由,不用刷新页面。




  •  Vue是单页应用,加载时不用获取所有的数据和dom,提高加载速度,优化了用户体验。




  •  Vue的第三方组件库丰富,低代码平台能够获得更多的支持和资源。




JNPF-Web-Vue3 的技术栈介绍


JNPF 快速开发平台的 Vue3.0 版本是基于 Vue3.x、Vue-router4.x、Vite4.x、Ant-Design-Vue3.x、TypeScript、Pinia、Less 的后台解决方案,采用 Pnpm 包管理工具,旨在为中大型项目做开发,提供开箱即用的解决方案。前端同时适配Vue2/Vue3技术栈。


以下对各项技术做简单的拓展介绍:


(1)Vue3.x

Vue3.x 作为一款领先的 JavaScript 框架,通过响应式数据绑定和组件化架构实现高效的应用开发。相较于 Vue2.x,在大规模应用场景下,Vue3.x 的渲染速度提升了近 3 倍,初始化速度提升了 10 倍以上,这不仅为我们提供了更出色的用户体验,也为企业应用的开发和维护提供了极大的便利。


此外,它所支持Composition API 可以更加灵活地实现代码复用和组件化,让我们的代码更加可读、可维护。总而言之,Vue3 在许多方面都进行了改进,包括更好的性能、更少的代码大小和更好的开发体验。


(2)Vue-router4.x

Vue-router4.x 作为 Vue.js 框架中的路由管理器,具备出色的性能和扩展性,为开发者提供了一种高效而灵活的前端路由解决方案。Vue Router 主要用于构建单页应用程序,允许创建可导航的Web 应用,使您可以轻松地构建复杂的前端应用。


(3)Vite4.x

一个基于 ES Module 的 Web 应用构建工具。作为一种全新的开发模式,Vite 相对于Webpack 更加出色,内置了许多优化手段,包括 HMR、代码分割、CSS 提取、缓存策略等,从而在保证开发速度的前提下,为应用程序的加载速度和性能提供了极致的保障。此外,它还支持快速的冷启动、模块化的打包方式以及自动化的多页面构建等特性,极大的提升了前端开发效率。


(4)Ant-Design-Vue3.x

一款基于 Vue3.x 的企业级 UI 组件库,旨在帮助开发者快速搭建出高质量、美观且易用的界面。不同于其他类似的组件库,Ant-Design-Vue3.x 更注重用户体验和可定制性,提供了一整套视觉、交互和动画设计解决方案,结合灵活的样式配置,可以满足大部分项目的UI 需求,帮助开发者事半功倍。


(5)TypeScript

TypeScript 作为一种静态类型的 JavaScript 超集,不仅完美兼容 JavaScript,还提供了强大的静态类型约束和面向对象编程特性,极大地提升了代码的可读性和重用性。TypeScript拥有强大的类型系统,可以帮助开发者在代码编写阶段发现潜在的错误,减少未知错误发生概率,并提供更好的代码补全和类型检查。这一特性让团队协作更加高效,同时也降低了维护代码的成本。


(6)Pinia

Pinia 是 Vue3.x 的状态管理库,基于 Vue3.x 的 Composition API 特性,为开发者提供了清晰、直观、可扩展和强类型化的状态管理方案,可以更好地管理应用数据和状态。无论是在小型项目还是庞大的企业级应用中,我们都可以依靠这个强大的状态管理库来迅速构建出高质量的应用。


(7)Less

一种 CSS 预处理器,能够以更便捷、灵活的方式书写和管理样式表。通过 Less,开发者可以使用变量、嵌套规则、混合、运算、函数等高级功能,使得样式表的编写更加简单、易于维护。使用 Less 不仅可以提高 CSS 开发效率,还可以生成更快、更小的 CSS 文件,从而减少网站加载时间,提升网站性能。


(8)Pnpm

Pnpm 作为一种快速、稳定、安全的包管理工具,它能够帮助我们管理 JavaScript 包的依赖关系,通过采用更为精简的数据存储结构,极大地减少冗余数据的存储,从而有效地节省磁盘空间。


其他亮点


作为一款基于SpringBoot+Vue3的全栈开发平台,满足微服务、前后端分离架构,基于可视化流程建模、表单建模、报表建模工具,快速构建业务应用,平台即可本地化部署,也支持K8S部署。


引擎式软件快速开发模式,除了上述功能,还配置了图表引擎、接口引擎、门户引擎、组织用户引擎等可视化功能引擎,基本实现页面UI的可视化搭建。内置有百种功能控件及使用模板,使得在拖拉拽的简单操作下,也能大限度满足用户个性化需求。


如果你是一名开发者,可以试试我们研发的JNPF开发平台。基于低代码充分利用传统开发模式下积累的经验,高效开发。


最后,给予一点建议


关于Vue,简单易上手,官方的文档很清晰,易于使用,同时它拥有更好的新能且占据的空间相比其他框架更少,同时vue的学习曲线是很平滑的,所以这是我为什么推荐优先学习vue的原因,对于新手来说易上手,快速帮助新手熟悉一些中小型的项目,但是对于大型的项目,这就要说到Vue响应机制上的问题了,大型项目的state(状态)是特别多的,这时watcher也会很多,进而导致卡顿。


对于React,主要是适应大型项目,由于React灵活的结构和可扩展性,相比Vue对于大型项目的适配性更高,此外其跨浏览器兼容、模块化、单项数据流等都是其优点,但是与Vue相反的就是它的学习曲线是陡峭的,由于复杂的设置过程,属性,功能和结构,它需要深入的知识来构建应用程序,这对于新手来说是不太适合作为一个入门级别的框架。


作者:冲浪中台
来源:juejin.cn/post/7295565904405790761
收起阅读 »

用1100天做一款通用的管理后台框架

web
前言 去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。 很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大...
继续阅读 »

前言


去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。


很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大家对这种非干货的文章也挺感兴趣。于是在这个三年的时间点上(没错,也就是1100天),我打算继续出来和大家唠唠,这一年我又做了些什么事,或者说,如何把一款好的后台框架变得通用?


题外话:如果你对我以前的文章感兴趣,可以点我头像进入主页查看;如果你期待我以后的文章,也可以点个关注。


痛点


因为 Fantastic-admin 是基于 Element Plus 这款 UI 组件库进行开发的,于是今年我陆陆续续被问到一些问题:



  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?

  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?

  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



类似的问题一多,我也在思考一个问题:我的这款框架是不是被 Element Plus 绑架了?如果开发者在做技术选型的时候,因为 UI 组件库不符合预期,而将我的框架筛掉,这是我不希望看到的结果。


基于这个潜在隐患,我开始计划对框架进行转型。


方案


方案一


既然开发者对 UI 组件库有各自的偏好,我又想拉拢这部分开发者,那是不是多出几套不同 UI 组件库版本的就可以了呢?没错,这是我最开始冒出来的念头。


我参考了一些同类产品的做法,尽管它们把不同 UI 组件库版本做得很像,但在使用体验过程中,还是会带来操作上的割裂感。并且因为无法抹平不同 UI 组件库在 API 上的差异,导致在框架功能上,不同版本之间也会有一些差异。



你可以分别对比左右或者上下两张图,包括左侧导航栏的样式、导航收起/展开按钮的位置、右侧项目配置中提供的功能等,都能明显发现它们的差异。


虽然这可能不是什么大问题,但我认为视觉风格上的统一是能帮助产品提高识别度的。就比如上面 4 款基于不同 UI 组件库开发的后台框架,虽然它们属于同一个产品,但如果我不告诉你,你未必能通过图片确定它们师出同门。


其次就是后台框架提供的功能不统一,这里面有一定的原因是因为 UI 组件库导致的。试想一个场景,如果你要从 Element Plus 版本的后台,迁移到 Ant Design Vue 版本的后台,框架的配置文件是否能原封不动的复制过去?如果导航(路由)数据是后端返回的,数据结构能否保持完全一致,后端无需做任何修改?因为不同 UI 组件库对菜单组件的使用方式是完全不同的,比如 Element Plus 是需要手动拼装的,而 Naive UI 则是数据驱动的,只需要传入一个树形结构的数据给组件即可。如果数据结构无法保证一致,就会增加迁移和学习的成本。


最后就是我的一点私心,因为多一个 UI 组件库的版本,势必会占据我更多的业余时间,如果同时维护 4、5 个版本,那我大概下班后的所有时间都要投入到其中,并且如果未来又有新的 UI 组件库成为流行,那就又多一个版本的维护,这并不是一个可持续发展的方案。


方案二


既然上一个方案不符合我的期望,于是我开始思考,框架本身能不能不依赖这些 UI 组件库?如果框架本身不依赖于三方的 UI 组件库,那开发者不就可以根据需要自行引入想要的组件库了么。



就如上图,主/次导航和顶栏是属于框架的部分,而这部分其实并没有用到太多 UI 组件库提供的组件,以 Element Plus 举例,我统计了一下目前 Fantastic-admin 用到的组件:



  • Menu 菜单(主/次导航)

  • Breadcrumb 面包屑(顶栏)

  • Popover 气泡卡片(顶栏右侧的工具栏)

  • Dropdown 下拉菜单(顶栏右侧的工具栏)

  • Drawer 抽屉(应用配置)

    • Message 消息提示

    • Button 按钮

    • Input 输入框

    • Radio 单选框

    • Select 选择器

    • Switch 开关

    • …(等等表单类组件)




可以看到,虽然抽屉组件里用了很多表单类的组件,但这部分组件都是在应用配置里使用的,而应用配置这个模块,主要是方便在线测试框架提供的各种功能,在实际业务开发中,是完全不需要这个模块的。



所以初步算下来,后台框架真正依赖于 Element Plus 实现的组件就只有 4 个:



  • Menu 菜单

  • Breadcrumb 面包屑

  • Popover 气泡卡片

  • Dropdown 下拉菜单


那我为什么不找一些独立的第三方插件替代呢?是的,这是我第二个方案,就是找一些独立的插件替换 UI 组件库中的组件。但问题也立马迎面而来,就是偌大一个 Github ,居然找不到符合我需求和审美的插件。


比如菜单插件,我希望它和 Element Plus 里的菜单组件在功能上没有太大差异,支持水平/垂直模式、支持折叠收起、支持设置默认激活菜单、支持默认展开等。


比如面包屑插件,或许是因为这个插件功能太简单,并且大部分 UI 组件库都有提供,在 Github 能搜到独立的面包屑插件很少,搜到的也基本上是 N 年前的上传的,既没有人维护,风格样式也很丑。


这个方案似乎也行不通……吗?


方案三


虽然方案二在实施的第一步就扑街了,但有一点思路还是正确的,就是让框架本身不依赖于三方 UI 组件库。既然网上搜不到合适的插件,那我为什么不自己写一个呢。


比如面包屑,这是一个很简单的功能,任何前端初学者应该都可以写一个面包屑组件。


而气泡卡片和下拉菜单我没有计划自己写,因为找到了一个还不错的插件 Floating Vue,它由 Vue 团队核心人员开发并维护,并且最重要的是它支持自定义样式,意味着我可以将它魔改成想要的样子,尽可能和我的框架在视觉风格上保持统一。


最后一个比较难啃的骨头就是菜单,因为找不到合适的替代品,自己写的话又比较有挑战,虽然我有一点实现思路,但不多。当然最终还是决定自己写一个,因为觉得三方 UI 组件库这么多,实在写不出来我就去读他们源码,总不能每一个源码我都读不懂吧。


这 4 个组件的替换方案确定后,剩下就是抽屉组件和它里面的一些表单组件了,这些要怎么解决呢?这会我想到了 Headless UI ,它是完全无样式的 UI 组件库,通过与 Tailwind CSS / UnoCSS 集成使用,可以快速构建出属于自己风格的组件。


但是 Headless UI 提供的组件非常有限,并不能覆盖我需要的表单组件。不过它的设计给了我启发。表单组件我并不需要非常复杂的功能,原生的表单控件其实就能满足我的使用需求,只是原生的样式比较丑,和我想要的风格不统一,那我只需要给他们定制一套统一的风格就可以了,也就写一套原子化的 CSS 样式。


于是,方案敲定,开始实操。


实操


我决定从易到难开始处理,因为这样在初期能快速看到进度推进,也避免一上来就被一个菜单功能卡住好几天,甚至十几天都没有进展,打击到自己的信心。


1. 面包屑


和预期一样,并没有什么难度,很轻松就实现了。只不过目前还是保持和 Element Plus 一样的使用方式,就是需要手动拼装,后期计划改成数据驱动的使用方式。



2. 气泡卡片 & 下拉菜单


这部分参考了 nuxt/devtoolsFloating Vue 的自定义样式,以及 nuxt/ui 中下拉菜单的样式风格,最终形成了我自己满意的风格



3. 抽屉


使用了 Headless UI 中的 Dialog 组件,因为它和抽屉组件有相同的交互方式,它们都是在遮罩层上展示内容,只不过 Dialog 更多时候是居中展示,而抽屉则是在左右两侧展示。


其次在使用过程中,发现 Headless UI 中的 Transition 组件是一个惊喜。虽然 Vue 本身就有提供 <transition> 组件用于处理过渡动画,但有一个场景会比较难处理,官方的描述是:



This technique is needed when you want to coordinate different animations for different child elements – for example, fading in a Dialog's backdrop, while at the same time sliding in the contents of the Dialog from one side of the screen.
当您要为不同的子元素协调不同的动画时,就需要使用这种技术,例如,在淡入对话框背景的同时,从屏幕的一侧滑入对话框的内容。



这说的不就是抽屉组件么?于是按照官方的示例,修改了整体风格,最终效果也就出来了。



4. 表单组件


之前的计划是修改原生表单控件的样式,但在开发过程中发现会有一定的局限性。比如 <select> 无法控制弹出选项框的样式,我的解决办法就是用 Floating Vue 封装模拟一个 select 组件。


同时也在开发过程中发现了一些被遗漏组件,于是边做边补,最终大概做了 10 多个组件。虽然看着不少,它们都秉持着最小可用的状态。什么意思呢?就是我不会给它们设计太多的 API ,因为它们的定位和三方 UI 组件库不同,它们只要满足框架本身使用即可,用不到的 API 不会进行开发。并且使用上也不会有太大负担,如果不是对框架进行二次开发,开发者是可以完全不用关注这部分组件。



5. 菜单


菜单组件确实是个难啃的骨头,我差不多用了 3 周的晚上时间去开发。


第一周,按照自己的思路徒手撸,做到一半卡壳,做不下去了;


第二周,开始看 Element Plus 、Naive UI 、Ant Design Vue 里菜单的源码;



Ant Design Vue 的没看懂,放弃;


Naive UI 的看到一半发现核心实现被作者封装到 treemate 这个独立包中了,虽然这个包是开源的,目的也是针对树形结构的一体化解决方案。但我粗略看了一遍文档,感觉有点大材小用,因为它有很多 API 我是用不到的,而我对菜单组件又有一些自己的想法,不确定是否它这个包能否满足我的需求,放弃;


最后选择看 Element Plus 的,通过在本地一点点打印数据,大概理解了实现思路,但组件递归调用,父子组件通过 provide / inject 传递数据和函数的方式,数据状态的变动也是一层层向上级组件通知,直到通知到顶层组件,在我看来有点不太优雅,如果数据能统一在顶层组件里操作就好了。其次我的计划是写一个数据驱动的菜单组件,而不是像 Element Plus 需要手动拼装的,所以虽然我大致看懂了 Element Plus 菜单组件是怎么实现的,但在我自己实现的时候,还是有很大的不同,能参考的代码并不多。


这部分的开发总结,我可能会在以后单独写一篇文章详细说说,因为这部分也是整个方案中唯一的难点。



第三周,因为实现思路大致有了,所以开发上就没有太多的卡壳,最终结果也还不错,基本达到了我的需求。


同时因为组件完全可控,顺带解决了之前使用 Element Plus 菜单组件上无法解决的 bug ,比如当菜单收起时,弹出的悬浮菜单如果数量过多,超出屏幕高度,超出的部分就无法查看了,就像这样:



但是现在则会有滚动条,使用体验上更舒服。



验证


至此,我的后台框架已经摆脱对 Element Plus 的依赖,接下来就需要验证一下是否可以方便的替换成其他 UI 组件库。


我分别用 Ant Design Vue 、Arco Design Vue 、Naive UI 、TDesign 这四款热度比较高的组件库进行了验证:















Ant Design Vue Arco Design Vue Naive UI TDesign

结果还是很满意的,都能够顺利替换,并且替换过程并没有花费很多时间,一个小时内就可以替换成功。



由于登录页这个特殊的存在,替换组件库后是需要对其用到的 Element Plus 组件进行手动修改的,这部分会比较花时间,因为会涉及到表单验证之类的东西,不同组件库的写法差异还是比较大的。



详细的替换步骤可以在 Fantastic-admin 官方文档里找到。


回顾


让我们重新看下一开始的痛点是否都解决了么:




  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?



    虽然不会有,但可以自己动手,根据教程将默认的 Element Plus 替换成你想要的 UI 组件库就可以了





  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?



    不会有冲突,现在可以彻底移除 Element Plus ,安装并使用自己的 UI 组件库





  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



    可以用 Fantastic-admin 源码先进行 UI 组件库的替换,之后再将老项目的业务代码逐部迁移





除了解决这些痛点,甚至还有新收获:




  • 帮助公司/企业打造视觉风格统一的产品,提高产品辨识度



    大公司可能有不止一个项目团队,不同项目团队的技术偏好可能无法完全统一,导致开发的后台长得也千变万化。但即使在这种情况下,使用 Fantastic-admin 依旧可以保持整体视觉风格上的统一。





  • 近乎于 0 的上手成本



    因为后台框架始终都只有一套,开发者不会因为切换 UI 组件库后,要重新了解后台框架的使用





  • 维护成本更低,产品生命周期更长



    这一点是对我自己说的,不管未来会出现多少个新的 UI 组件库,我都不需要去新增一个版本进行单独维护;或者 Element Plus 如果有一天停止维护了,我的产品也不会因此进入了死亡倒计时





总结


文章写到这里,差不多就结束了,虽然阅读一遍可能只花了不到10分钟,但为了做成这件事,我大概从今年 6 月份就开始构思了,也是花了蛮多的精力,所以很感谢你的耐心。


当一款产品做到第 4 个年头,周围大部分同类产品都进入到半停更的状态,这一年里我经常思考如何延长产品的生命周期,如何让更多人来使用,而这篇文章就是对我自己今年的一个总结,也是一份答卷,希望大家能喜欢。


另外,Fantastic-admin V4.0 已经正式发布,感兴趣的朋友可以来看看,或许你的下一个项目,就可以用上了。


作者:Hooray
来源:juejin.cn/post/7295624857432850468
收起阅读 »

终结屏幕适配这个话题

物理像素、逻辑像素、百分比适配 日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px),逻辑像素(dp, point)。 那物理像素和逻辑像素的区别是? 这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。 此时...
继续阅读 »

物理像素、逻辑像素、百分比适配


日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px)逻辑像素(dp, point)


那物理像素和逻辑像素的区别是?


这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。



此时想象这个图片放置在 `10*12px` 的屏幕中会是怎样呢。对比如下,会发现该图片放置在分辨率更高的屏幕中会变得非常狭小。

image.png
继续我们的例子,如果该屏幕想要保证图片能跟前面的低分辨率的设备显示效果一致的话,则图片的宽高应增加1倍的大小。即设备需要2倍的像素比例dpr(device percent ratio)。这样图片3*4逻辑像素的尺寸的图片在高分辨率设备中可以映射成6*8物理像素,而在低分辨率的设备(像素比例时1的设备),则3*4逻辑像素的图片映射为3*4物理像素的图片。


这是逻辑像素的大致机制。逻辑像素会根据目标设备的分辨率和尺寸计算出设备的缩放比例。逻辑像素出现是为了让不同分辨率的设备中显示相同的内容能取得大致相同的效果,当然逻辑像素并不是这样简单的百分比换算。


在Android中这个逻辑像素是dp,而ios中则是pt。在android中dp的换算公式中具体换算公式想了解的可以点击下面链接了解。
betterprogramming.pub/cracking-an…


在Android开发中将不同分辨率设备的中的物理像素比率进行如下分类。所以假设设备是230dpi的话也以hdpi1.5倍进行换算。所以这跟百分比的换算是不太一样的。以“微信”应用举例。


底部的Tab(微信、通讯录、发现,我),假设设计图中屏幕的宽度是375dp,根据tab均分,单个tab为93.75。你如果通过水平布局指定宽度为93.75逻辑像素的话则会发现出来的效果在某些手机上并不是均分的。


如下图类似微信界面运行在Iphone 14 pro。此时应该用百分比进行适配,即在不同的分辨率中基于设计图的尺寸进行等比例换算。如:设计图的分辨率为375*812,而显示设备的分辨率为1080*1920,则设计图上1像素相当于目标设备1 ✖️ “显示设备基于设计图的比例(1080/375=2.88)”像素,即 1✖️2.88=2.88像素。这就是百分比适配。对比下图可以发现逻辑像素适配的“我”是偏左的。


image.png


image.png


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


但是百分比并不是万能的。如下图逻辑像素适配和百分比像素适配的对比。在列表中,百分比布局则会出现一个问题。你会发现在大尺寸高分辨率的设备中,列表中的每一项都特别大。则如果用逻辑像素(dp、pt)则是这样。使用逻辑像素能充分发挥大屏的优势,屏幕越大显示的内容更多。


image.png


什么时候应该用逻辑像素,百分比像素。


具体什么时候应该用逻辑像素和百分比像素适配,取决于设计图UI。根据不同设计意图决定何种方案。大部分情况下使用逻辑像素不会出现什么问题,列表item必定使用逻辑像素。但是什么时候应该用百分比像素呢?


举个例子:



ps: 例子中我会以百分比像素表示将设计图像素根据不同分辨率设备等比例换算的像素。即1百分比像素= 1✖️ [(设计图分辨率)/ (目标设备分辨率)]。



下面是一个“购买成功”的UI图。中间有个票根信息。票根信息有个票根背景图片。


标注图中的屏幕分辨率为 393*852


image.png


这里票根信息UI应该用逻辑像素还是百分比像素适配呢?


通过标注图能明显看出票根信息在宽度上固定需要占用一定比例。所以这里宽度应该为 353百分比像素 。为了宽高比例正确,故高度也应为 346百分比像素 。注意这里高度的 346百分比像素 也应该是基于屏幕宽度 393 的百分比像素。即 目标设备屏幕宽度 * 346 / 393


因为整个票根的宽高都为百分比适配,则里面子部件的摆放、间距都应按照百分比的方式进行适配。不然则会出现子部件没法像标注图那样正确对齐的情况。


总结


物理像素(px)是屏幕上的实际物理点,表示屏幕上显示内容的最小单位。逻辑像素(dp、pt)是开发中使用的抽象单位,与物理像素的关系由设备的像素密度决定。


逻辑像素是开发中使用的抽象单位,它们与物理像素之间有一个映射关系。在不同的设备上,逻辑像素的布局和大小是相对统一的。使用逻辑像素可以让开发者在不同分辨率的设备上保持一致的布局和显示效果。


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


一般情况下,使用逻辑像素可以保持在不同设备上显示内容的一致性和最佳效果,特别是在涉及列表和大屏幕显示的情况下,需要根据设计图,决定使用何种方案。可以通过先分析使用逻辑像素思考是否合理,再考虑百分比适配的情况。在一些特定的设计需求下,如背景图片的铺满屏幕、比例布局等,可以考虑使用百分比适配来实现更精确的布局和显示效果。


作者:淹没
来源:juejin.cn/post/7294853623849812002
收起阅读 »

如何用Compose TV写电视桌面

写在前面 Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等. 下图为最终效果成品。 Demo源码地址 整体UI框架搭建 标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColu...
继续阅读 »

写在前面


Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等.


下图为最终效果成品。



Demo源码地址


整体UI框架搭建


标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColumn)



标题-TabRow



val tabs = listof("我的", "影视", "应用")

TabRow(
selectedTabIndex = selectedTabIndex,
indicator = { tabPositions, isActivated ->
// 移动的白色色块
TopBarMoveIndicator(...
}
) {
tabs.forEachIndexed { index, title ->
Tab(
// colors设置了 默认,上焦,选中的颜色
colors = TabDefaults.pillIndicatorTabColors(
contentColor = Color.White,
focusedContentColor = Color.Black,
selectedContentColor = Color.White,
)
...
) {
Text(...)
}
}
}

移动的白色色块,这里只是我写的Demo,都是可以自定义的.


fun TopBarMoveIndicator(
currentTabPosition: DpRect,
isFocused: Boolean
)
{
val width by animateDpAsState(targetValue = currentTabPosition.width, label = "width")
val height = if (isFocused) currentTabPosition.height else 2.dp
val leftOffset by animateDpAsState(targetValue = currentTabPosition.left, label = "leftOffset")
// 有焦点的时候,是矩形,无焦点的时候,是下划线.
val moveShape = if (isFocused) ShapeDefaults.ExtraLarge else ShapeDefaults.ExtraSmall

Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(leftOffset, currentTabPosition.top)
.width(width)
.height(height)
.background(color = Color.White, shape = moveShape)
.zIndex(-1f)
)
}

NatHost(内容切换) + 内容(TvLazyColumn)


内容切换


NatHost 功能类似 ViewPager,对 "我的","影视","应用" 几个 页面内容进行切换.


NavHost(
...
builder = {
composable(...) { // 我的
// 我的野蛮
}
composable(...) {// 影视
// 影视页面
}
composable(...) { // 应用
// 应用页面
}
}
)

内容布局


TvLazyColumn 与 LazyColumn 功能是差不多的,纵向布局,就不过多赘述,具体看谷歌的开发文档,网上相关视频教程 或 看DEMO源码.


TvLazyColumn(
...
) {
item {
ImmersiveList(...) // 沉浸式列表
}
item {
TvLazyRow(...) // 热门推荐
}
item {
TvLazyRow(...)
}
item {
TvLazyRow(...) // 豆瓣高分
TvLazyRow(...)
}
item {
TvLazyRow(...) // 预热抢先看
}
... ...
}

TvLazyColumn的相关参数,记住这个参数 pivotOffsets,它是设置滚动的位置的,比如设置滚动一直在中间位置.


fun TvLazyColumn(
modifier: Modifier = Modifier,
state: TvLazyListState = rememberTvLazyListState()
,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
userScrollEnabled: Boolean = true,
pivotOffsets: PivotOffsets = PivotOffsets(),
content: TvLazyListScope.() -> Unit
)

TvLazyRow + Item


TvLazyColumn 每行又包含了 TvLazyRow 横向布局 (如果是固定的几个,可以用 Row。


自定义的布局可以用 Surface 包含的,几个关键属性, Scale(放大),Border(边框),Glow(阴影)。



TvLazyRow(...) {
items(...) { ...
Surface(
onClick = {//点击事件}
scale = ClickableSurfaceDefaults.scale(...),
border = ClickableSurfaceDefaults.border(...),
glow = ClickableSurfaceDefaults.glow(...)
) {
// 你自定义的卡片内容,比如 图片(AsyncImage) + 文本(Text)
}
}
}

我Demo里面用的是 谷歌提供的一个包含 图片+文本的控件 StandardCardLayout


ImmersiveList 沉浸式列表

有点类似 爱奇艺,腾讯,哔哩哔哩等电视应用这种列表.


ImmersiveList(
modifier = Modifier.onGloballyPositioned { currentYCoord = it.positionInWindow().y },
background = {
// 背景图片内容
}
) {
// 布局内容
// 大标题 + 详情
// TvLazyRow
}

TV其它控件推荐


Carousel 轮播界面



TvLazyVerticalGrid/TvLazyHorizontalGrid


ModalNavigationDrawer抽屉式导航栏


ListItem


分辨率适配


TV开发涉及分辨率适配问题,Compose 也能很简单的处理此问题,无论你在1920x1080,还是1280x720等分辨率下,无缝切换,毫无压力.


val displayMetrics = LocalContext.current.resources.displayMetrics
val fontScale = LocalDensity.current.fontScale
val density = displayMetrics.density
val widthPixels = displayMetrics.widthPixels
val widthDp = widthPixels / density
val display = "density: $density\nwidthPixels: $widthPixels\nwidthDp: $widthDp"
KLog.d("display:$display")
CompositionLocalProvider(
LocalDensity provides Density(
density = widthPixels / 1920f,
fontScale = fontScale
)
) {
// 我们写的Compose主界面布局
}

参考资料


What's new with TV and intro to Compose


Android TV 上使用 Jetpack Compose


Compose TV官方设计文档


JetStreaamCompose TV demo


Compose TV demo


写在后面


近几年Android推出了很多东西,我的心尖尖是 MVI,flow(完爆Rxjava),Compose>>>


TV开发的发展,一开始是 RecycleView,要去解决焦点,优化等问题,后来是Leanback,到现在的Compose TV(开发速度提升了很多很多).


我也真的很喜欢Compose的写法,简单明了,强烈推荐Compose TV开发电视,我相信谷歌,能将Compose性能优化的越来越好.


最后一篇TV开发的文章了,以后搞车载相关去了.


作者:冰雪情缘long
来源:juejin.cn/post/7294907512444010559
收起阅读 »

Android使用Hilt依赖注入,让人看不懂你代码

前言 之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt...
继续阅读 »

前言


之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt不了解或是想了解得更多,那么接下来的内容将助力你玩转Hilt。


通过本篇文章,你将了解到:




  1. 什么是依赖注入?

  2. Hilt 的引入与基本使用

  3. Hilt 的进阶使用

  4. Hilt 原理简单分析

  5. Android到底该不该使用DI框架?



1. 什么是依赖注入?


什么是依赖?


以手机为例,要组装一台手机,我们需要哪些部件呢?

从宏观上分类:软件+硬件。

由此我们可以说:手机依赖了软件和硬件。

而反映到代码的世界:


class FishPhone(){
val software = Software()
val hardware = Hardware()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}
//软件
class Software() {
fun handle(){}
}
//硬件
class Hardware() {
fun handle(){}
}

FishPhone 依赖了两个对象:分别是Software和Hardware。

Software和Hardware是FishPhone的依赖(项)。


什么是注入?


上面的Demo,FishPhone内部自主构造了依赖项的实例,考虑到依赖的变化挺大的,每次依赖项的改变都要改动到FishPhone,容易出错,也不是那么灵活,因此考虑从外部将依赖传进来,这种方式称之为:依赖注入(Dependency Injection 简称DI)

有几种方式:




  1. 构造函数传入

  2. SetXX函数传入

  3. 从其它对象间接获取



构造函数依赖注入:


class FishPhone(val software: Software, val hardware: Hardware){
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone的功能比较纯粹就是打电话功能,而依赖项都是外部传入提升了灵活性。


为什么需要依赖注入框架?


手机制造出来后交给客户使用。


class Customer() {
fun usePhone() {
val software = Software()
val hardware = Hardware()
FishPhone(software, hardware).call()
}
}

用户想使用手机打电话,还得自己创建软件和硬件,这个手机还能卖出去吗?

而不想创建软件和硬件那得让FishPhone自己负责去创建,那不是又回到上面的场景了吗?


你可能会说:FishPhone内部就依赖了两个对象而已,自己负责创建又怎么了?


解耦


再看看如下Demo:


interface ISoftware {
fun handle()
}

//硬件
interface IHardware {
fun handle()
}

//软件
class SoftwareImpl() : ISoftware {
override fun handle() {}
}

//硬件
class HardwareImpl : IHardware {
override fun handle() {}
}

class FishPhone() {
val software: ISoftware = SoftwareImpl()
val hardware: IHardware = HardwareImpl()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone 只关注软件和硬件的接口,至于具体怎么实现它不关心,这就达到了解耦的目的。
既然要解耦,那么SoftwareImpl()、HardwareImpl()就不能出现在FishPhone里。

应该改为如下形式:


class FishPhone(val software: ISoftware, val hardware: IHardware) {
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

消除模板代码


即使我们不考虑解耦,假若HardwareImpl里又依赖了cpu、gpu、disk等模块:


//硬件
class HardwareImpl : IHardware {
val cpu = CPU(Regisgter(), Cal(), Bus())
val gpu = GPU(Image(), Video())
val disk = Disk(Block(), Flash())
//...其它模块
override fun handle() {}
}

现在仅仅只是三个模块,若是依赖更多的模块或者模块的本身也需要依赖其它子模块,比如CPU需要依赖寄存器、运算单元等等,那么我们就需要写更多的模板代码,要是我们只需要声明一下想要使用的对象而不用管它的创建就好了。


class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {
override fun handle() {}
}

可以看出,下面的代码比上面的简洁多了。




  1. 从解耦和消除模板代码的角度看,我们迫切需要一个能够自动创建依赖对象并且将依赖注入到目标代码的框架,这就是依赖注入框架

  2. 依赖注入框架能够管理依赖对象的创建,依赖对象的注入,依赖对象的生命周期

  3. 使用者仅仅只需要表明自己需要什么类型的对象,剩下的无需关心,都由框架自动完成



先想想若是我们想要实现这样的框架需要怎么做呢?

相信很多小伙伴最朴素的想法就是:使用工厂模式,你传参告诉我想要什么对象我给你构造出来。

这个想法是半自动注入,因为我们还要调用工厂方法去获取,而全自动的注入通常来说是使用注解标注实现的。


2. Hilt 的引入与基本使用


Hilt的引入


从Dagger到Dagger2再到Hilt(Android专用),配置越来越简单也比较容易上手。

前面说了依赖注入框架的必要性,我们就想迫不及待的上手,但难度可想而知,还好大神们早就造好了轮子。

以AGP 7.0 以上为例,来看看Hilt框架是如何引入的。


一:project级别的build.gradle 引入如下代码:


plugins {
//指定插件地址和版本
id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}

二:module级别的build.gradle引入如下代码:


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//使用插件
id 'com.google.dagger.hilt.android'
//kapt生成代码
id 'kotlin-kapt'
}
//引入库
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'

实时更新最新版本以及AGP7.0以下的引用请参考:Hilt最新版本配置


Hilt的简单使用


前置步骤整好了接下来看看如何使用。


一:表明该App可以使用Hilt来进行依赖注入,添加如下代码:


@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}

@HiltAndroidApp 添加到App的入口,即表示依赖注入的环境已经搭建好。


二:注入一个对象到MyApp里:

有个类定义如下:


class Software {
val name = "fish"
}

我们不想显示的构造它,想借助Hilt注入它,那得先告诉Hilt这个类你帮我注入一下,改为如下代码:


class Software @Inject constructor() {
val name = "fish"
}

在构造函数前添加了@Inject注解,表示该类可以被注入。

而在MyApp里使用Software对象:


@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: Software

override fun onCreate() {
super.onCreate()
println("inject result:${software.name}")
}
}

对引用的对象使用@Inject注解,表示期望Hilt帮我将这个对象new出来。

最后查看打印输出正确,说明Software对象被创建了。


这是最简单的Hilt应用,可以看出:




  1. 我们并没有显式地创建Software对象,而Hilt在适当的时候就帮我们创建好了

  2. @HiltAndroidApp 只用于修饰Application



如何注入接口?


一:错误示范
上面提到过,使用DI的好处之一就是解耦,而我们上面注入的是类,现在我们将Software抽象为接口,很容易就会想到如下写法:


interface ISoftware {
fun printName()
}

class SoftwareImpl @Inject constructor(): ISoftware{
override fun printName() {
println("name is fish")
}
}

@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: ISoftware

override fun onCreate() {
super.onCreate()
println("inject result:${software.printName()}")
}
}

不幸的是上述代码编译失败,Hilt提示说不能对接口使用注解,因为我们并没有告诉Hilt是谁实现了ISoftware,而接口本身不能直接实例化,因此我们需要为它指定具体的实现类。


二:正确示范

再定义一个类如下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}



  1. @Module 表示该类是一个Hilt的Module,固定写法

  2. @InstallIn 表示模块在哪个组件生命周期内生效,SingletonComponent::class指的是全局

  3. 一个抽象类,类名随意

  4. 抽象方法,方法名随意,返回值是需要被注入的对象类型(接口),而参数是该接口的实现类,使用@Binds注解标记,



如此一来我们就告诉了Hilt,SoftwareImpl是ISoftware的实现类,于是Hilt注入ISoftware对象的时候就知道使用SoftwareImpl进行实例化。
其它不变运行一下:
image.png


可以看出,实际注入的是SoftwareImpl。



@Binds 适用在我们能够修改类的构造函数的场景



如何注入第三方类


上面的SoftwareImpl是我们可以修改的,因为使用了@Inject修饰其构造函数,所以可以在其它地方注入它。

在一些时候我们不想使用@Inject修饰或者说这个类我们不能修改,那该如何注入它们呢?


一:定义Provides模块


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():Hardware {
return Hardware()
}
}



  1. @Module和@InstallIn 注解是必须的

  2. 定义object类

  3. 定义函数,方法名随意,返回类型为我们需要注入的类型

  4. 函数体里通过构造或是其它方式创建具体实例

  5. 使用@Provides注解函数



二:依赖使用

而Hardware定义如下:


class Hardware {
fun printName() {
println("I'm fish")
}
}

在MyApp里引用Hardware:

在这里插入图片描述


虽然Hardware构造函数没有使用@Inject注解,但是我们依然能够使用依赖注入。


当然我们也可以注入接口:


interface IHardware {
fun printName()
}

class HardwareImpl : IHardware {
override fun printName() {
println("name is fish")
}
}

想要注入IHardware接口,需要定义provides模块:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}


@Provides适用于无法修改类的构造函数的场景,多用于注入第三方的对象



3. Hilt 的进阶使用


限定符


上述 ISoftware的实现类只有一个,假设现在有两个实现类呢?

比如说这些软件可以是美国提供,也可以是中国提供的,依据上面的经验我们很容易写出如下代码:


class SoftwareChina @Inject constructor() : ISoftware {
override fun printName() {
println("from china")
}
}

class SoftwareUS @Inject constructor() : ISoftware {
override fun printName() {
println("from US")
}
}

@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

//依赖注入:
@Inject
lateinit var software: ISoftware

兴高采烈的进行编译,然而却报错:
image.png


也就是说Hilt想要注入ISoftware,但不知道选择哪个实现类,SoftwareChina还是SoftwareUS?没人告诉它,所以它迷茫了,索性都绑定了。


这个时候我们需要借助注解:@Qualifier 限定符注解来对实现类进行限制。

改造一下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
@China
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
@US
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China

定义新的注解类,使用@Qualifier修饰。

而后在Module里,分别使用注解类修饰返回的函数,如bindSoftwareCh函数指定返回SoftwareChina来实现ISoftware接口。


最后在引用依赖注入的地方分别使用@China @US修饰。


    @Inject
@US
lateinit var software1: ISoftware

@Inject
@China
lateinit var software2: ISoftware

此时,虽然software1、software2都是ISoftware类型,但是由于我们指定了限定符@US、@China,因此最后真正的实现类分别是SoftwareChina、SoftwareUS。



@Qualifier 主要用在接口有多个实现类(抽象类有多个子类)的注入场景



预定义限定符


上面提及的限定符我们还可以扩展其使用方式。

你可能发现了,上述提及的可注入的类构造函数都是无参的,很多时候我们的构造函数是需要有参数的,比如:


class Software @Inject constructor(val context: Context) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
//注入
@Inject
lateinit var software: Software

这个时候编译会报错:

image.png
意思是Software依赖的Context没有进行注入,因此我们需要给它注入一个Context。


由上面的分析可知,Context类不是我们可以修改的,只能通过@Provides方式提供其注入实例,并且Context有很多子类,我们需要使用@Qualifier指定具体实现类,因此很容易我们就想到如下对策。

先定义Module:


@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {
@Provides
@GlobalContext
fun provideContext(): Context? {
return MyApp.myapp
}
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext

再注入Context:


class Software @Inject constructor(@GlobalContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

可以看出,借助@Provides和@Qualifier,可以实现全局的Context。

当然了,实际上我们无需如此麻烦,因为这部分工作Hilt已经预先帮我们弄了。

与我们提供的限定符注解GlobalContext类似,Hilt预先提供了:


@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}

因此我们只需要在需要的地方引用它即可:


class Software @Inject constructor(@ApplicationContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

如此一来我们无需重新定义Module。




  1. 除了提供Application级别的上下文:@ApplicationContext,Hilt还提供了Activity级别的上下文:@ActivityContext,因为是Hilt内置的限定符,因此称为预定义限定符。

  2. 如果想自己提供限定符,可以参照GlobalContext的做法。



组件作用域和生命周期


Hilt支持的注入点(类)


以上的demo都是在MyApp里进行依赖,MyApp里使用了注解:@HiltAndroidApp 修饰,表示当前App支持Hilt依赖,Application就是它支持的一个注入点,现在想要在Activity里使用Hilt呢?


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {

除了Application和Activity,Hilt内置支持的注入点如下:
image.png


除了Application和ViewModel,其它注入点都是通过使用@AndroidEntryPoint修饰。



注入点其实就是依赖注入开始的点,比如Activity里需要注入A依赖,A里又需要注入B依赖,B里又需要注入C依赖,从Activity开始我们就能构建所有的依赖



Hilt组件的生命周期


什么是组件?在Dagger时代我们需要自己写组件,而在Hilt里组件都是自动生成的,无需我们干预。
依赖注入的本质实际上就是在某个地方悄咪咪地创建对象,这个地方的就是组件,Hilt专为Android打造,因此势必适配了Android的特性,比如生命周期这个Android里的重中之重。

因此Hilt的组件有两个主要功能:




  1. 创建、注入依赖的对象

  2. 管理对象的生命周期



Hilt组件如下:
image.png


可以看出,这些组件的创建和销毁深度绑定了Android常见的生命周期。

你可能会说:上面貌似没用到组件相关的东西,看了这么久也没看懂啊。

继续看个例子:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@InstallIn(SingletonComponent::class) 表示把模块安装到SingletonComponent组件里, SingletonComponent组件顾名思义是全局的,对应的是Application级别。因此安装的这个模块可在整个App里使用。


问题来了:SingletonComponent是不是表示@Provides修饰的函数返回的实例是同一个?

答案是否定的。


这就涉及到组件的作用域。


组件的作用域


想要上一小结的代码提供全局唯一实例,则可用组件作用域注解修饰函数:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@Singleton
fun provideHardware():IHardware {
return HardwareImpl()
}
}

当我们在任何地方注入IHardware时,获取到的都是同一个实例。

除了@Singleton表示组件的作用域,还有其它对应组件的作用域:

image.png


简单解释作用域:

@Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例

@ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例

@ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例

@ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致




  1. Hilt默认不绑定任何作用域,由此带来的结果是每一次注入都是全新的对象

  2. 组件的作用域要么不指定,要指定那必须和组件的生命周期一致



以下几种写法都不符合第二种限制:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {
@Provides
@Singleton//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

除了修饰Module,作用域还可以用于修饰构造函数:


@ActivityScoped
class Hardware @Inject constructor(){
fun printName() {
println("I'm fish")
}
}

@ActivityScoped表示不管注入几个Hardware,在同一个Activity里注入的实例都是一致的。


构造函数里无法注入的字段


一个类的构造函数如果被@Inject注入,那么构造函数的其它参数都需要支持注入。


class Hardware @Inject constructor(val context: Context) {
fun printName() {
println("I'm fish")
}
}

以上代码是无法编译通过的,因为Context不支持注入,而通过上面的分析可知,我们可以使用限定符:


class Hardware @Inject constructor(@ApplicationContext val context: Context) {
fun printName() {
println("I'm fish")
}
}

这就可以成功注入了。


再看看此种场景:


class Hardware @Inject constructor(
@ApplicationContext val context: Context,
val version: String,
) {
fun printName() {
println("I'm fish")
}
}

很显然String不支持注入,当然我们可以向@ApplicationContext 一样也给String提供一个@Provides和@Qualifier注解,但可想而知很麻烦,关键是String是动态变化的,我们确实需要Hardware构造的时候传入合适的String。


由此引入新的写法:辅助注入


class Hardware @AssistedInject constructor(
@ApplicationContext val context: Context,
@Assisted
val version: String,
) {

//辅助工厂类
@AssistedFactory
interface Factory{
//不支持注入的参数都可以放这,返回值为待注入的类型
fun create(version: String):Hardware
}

fun printName() {
println("I'm fish")
}
}

在引用注入的地方不能直接使用Hardware,而是需要通过辅助工厂进行创建:


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
@Inject
lateinit var hardwareFactory : Hardware.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)

val hardware = hardwareFactory.create("3.3.2")
println("${hardware.printName()}")
}
}

如此一来,通过辅助注入,我们还是可以使用Hilt,值得一提的是辅助注入不是Hilt独有,而是从Dagger继承来的功能。


自定义注入点


Hilt仅仅内置了常用的注入点:Application、Activity、Fragment、ViewModel等。

思考一种场景:小明同学写的模块都是需要注入:


class Hardware @Inject constructor(
val gpu: GPU,
val cpu: CPU,
) {
fun printName() {
println("I'm fish")
}
}

class GPU @Inject constructor(val videoStorage: VideoStorage){}

//显存
class VideoStorage @Inject constructor() {}

class CPU @Inject constructor(val register: Register) {}

//寄存器
class Register @Inject() constructor() {}

此时小刚需要引用Hardware,他有两种选择:




  1. 使用注入方式很容易就引用了Hardware,可惜的是他没有注入点,仅仅只是工具类。

  2. 不选注入方式,则需要构造Hardware实例,而Hardware依赖GPU和CPU,它们又分别依赖VideoStorage和Register,想要成功构造Hardware实例需要将其它的依赖实例都手动构造出来,可想而知很麻烦。



这个时候适合小刚的方案是:



自定义注入点



方案实施步骤:

一:定义入口点


@InstallIn(SingletonComponent::class)
interface HardwarePoint {
//该注入点负责返回Hardware实例
fun getHardware(): Hardware
}

二:通过入口点获取实例


class XiaoGangPhone {
fun getHardware(context: Context):Hardware {
val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)
return entryPoint.getHardware()
}
}

三:使用Hardware


        val hardware = XiaoGangPhone().getHardware(this)
println("${hardware.printName()}")

注入object类


定义了object类,但在注入的时候也需要,可以做如下处理:


object MySystem {
fun getSelf():MySystem {
return this
}
fun printName() {
println("I'm fish")
}
}

@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {
@Provides
@Singleton
fun provideSystem():MySystem {
return MySystem.getSelf()
}
}
//使用注入
class Middleware @Inject constructor(
val mySystem:MySystem
) {
}

4. Hilt 原理简单分析


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}

Hilt通过apt在编译时期生成代码:


public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {

private boolean injected = false;

Hilt_SecondActivity() {
super();
//初始化注入监听
_initHiltInternal();
}

Hilt_SecondActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}

private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
//真正注入
inject();
}
});
}

protected void inject() {
if (!injected) {
injected = true;
//通过manager获取组件,再通过组件注入
((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));
}
}
}

在编译期,SecondActivity的父类由AppCompatActivity变为Hilt_SecondActivity,因此当SecondActivity构造时就会调用父类的构造器监听create()的回调,回调调用时进行注入。



由此可见,Activity.onCreate()执行后,Hilt依赖注入的字段才会有值



真正注入的过程涉及到不少的类,都是自动生成的类,有兴趣可以对着源码查找流程,此处就不展开说了。


5. Android到底该不该使用DI框架?


有人说DI比较复杂,还不如我直接构造呢?

又有人说那是你项目不复杂,用不到,在后端流行的Spring全家桶,依赖注入大行其道,Android复杂的项目也需要DI来解耦。


从个人的实践经验看,Android MVVM/MVI 模式还是比较适合引入Hilt的。
image.png


摘抄官网的:现代Android 应用架构

通常来说我们这么设计UI层到数据层的架构:


class MyViewModel @Inject constructor(
val repository: LoginRepository
) :ViewModel() {}

class LoginRepository @Inject constructor(
val rds : RemoteDataSource,
val lds : LocalDataSource
) {}

//远程来源
class RemoteDataSource @Inject constructor(
val myRetrofit: MyRetrofit
) {}

class MyRetrofit @Inject constructor(
) {}

//本地来源
class LocalDataSource @Inject constructor(
val myDataStore: MyDataStore
) {}

class MyDataStore @Inject constructor() {}

可以看出,层次比较深,使用了Hilt简洁了许多。


本文基于 Hilt 2.48.1

参考文档:

dagger.dev/hilt/gradle…

developer.android.com/topic/archi…

repo.maven.apache.org/maven2/com/…


作者:小鱼人爱编程
来源:juejin.cn/post/7294965012749320218
收起阅读 »

记一次使用babel做代码转换的经历

web
前言 前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。 这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访...
继续阅读 »

前言


前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。


这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访问不通,还得拿到本地来。


得,谁让咱们天生就是找事的好手呢,那整吧。


方案对比


既然来活了,那咱们首先得先确定下这个事怎么做?有以下几个方案:


方案一: 发挥中华民族的优良传统,勤劳,即手动将全部的静态资源引用处替换为本地引用,想想手就疼


方案二: 将偷懒运用到极致,将静态资源全部放到public/assets目录下(Vite项目中public目录下的文件会被直接复制到打包目录下),同时修改资源引用的统一前缀为 /assets,即可引用到该静态资源。目测几分钟就能完成


方案三: 写个脚本,自动完成 1 操作,瞬间手就不疼了,但是脑壳开始疼了


对比下这三个方案的优缺点,选出最优解



方案一


优点:简单


缺点:手疼且低效


方案二


优点:省时、省力


缺点:需要考虑打包后的引用路径,同时因为文件都是直接复制到包中的,并没有经过hash处理,浏览器会缓存该文件,后续如果文件修改,不能第一时间反应再客户端。


方案三


优点:高效、一劳永逸、文件会经过Vite处理,生成带有hash值的新文件,没有缓存问题


缺点:这个脚本有点难写,涉及代码转换和项目文件扫描等知识,脑壳疼



最终,本着一劳永逸的想法,我选择了方案三。


过程


整体思路:



  1. 将全部静态资源引用汇总到统一文件中,方便管理及代码分析

  2. 使用代码转换工具将上面的文件内容转换为使用 import 导入的方式


静态资源汇总


所有的静态资源引用散布在项目的各个文件中,这不利于代码分析,也不利于代码转化,所以,第一步就是将散布在项目各个文件中的静态资源引用汇总到一个文件中,方便管理、代码分析、代码转化。


这一步是纯体力活,一次劳动,收益无穷。


最终静态资源汇总文件应该是这样的:


import { ASSETS_PREFIX } from './constants';

const contactUs = `${ASSETS_PREFIX}/login/contact_us.png`;
const userAvatar = `${ASSETS_PREFIX}/login/default_avatar.png`;
const loginBg = `${ASSETS_PREFIX}/login/login_bg.jpg`;

export {
contactUs,
userAvatar,
loginBg,
}


  1. 一个静态资源对应一个变量,一个变量对应一个静态资源路径

  2. 静态资源路径必须使用模版字符串统一前缀,便于后续做替换

  3. 统一导出


代码转换


静态资源全部会送完毕后,接下来就是做代码分析及转换。


我们的目标其实就是将上面的代码转换到下面这种:


import contactUs from '@/assets/login/contact_us.png';
import userAvatar from '@/assets/login/default_avatar.png';
import loginBg from '@/assets/login/login_bg.jpg'

export {
contactUs,
userAvatar,
loginBg,
}

既然涉及代码转换,很自然的就能想到使用babel做转换。


先来简单说下babel做代码转换的过程:



  1. 使用 @babel/parser 将代码解析为抽象语法树(AST: 表示当前代码结构的js对象)

  2. 找到标识为 const 的变量,拿出该变量,并将其后对应的变量内容拿出来,将模版字符串中的变量替换为@/assets,得到新静态资源本地路径(@/assets/login/contact_us.png)

  3. 组合 import 的 AST 对象,并使用该对象替换原来的 const 相关的AST

  4. 使用 @babel/generator 将新的AST转换为代码输出到对应文件中


代码如下:


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import fs from 'fs';

// 静态资源汇总文件
let imgInfoFilePath = 'src/scripts/assets.ts';
// 要替换为的静态资源路径前缀
let replaceToCode = '@/assets';

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历const声明节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

node.declarations.forEach(decl => {
// 存储变量名
const localName = decl.id.name;
// 组装import路径
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;
// 组装import结构
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
const result = generate.default(ast, {}, code);
// 备份原文件
fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);
// 代码输出
fs.writeFileSync(imgInfoFilePath, result.code);
} catch (error: any) {
logError(error);
}
}

这样,代码就转换完成了。


这样转换完后,ts文件中相关的静态资源引用就替换完成了,但是css文件中的静态资源引用还没有被转换。


因为css文件中的静态资源路径都是完整路径,不存在其中掺杂变量的情况,所以我们只需要找到所有的css文件,并将其中的路径前缀统一替换为@/assets 即可。


import { globSync } from 'glob';
import fs from 'fs';

let replaceStr = 'https://xxxxx.xxxx.xxxxx';
let replaceToCode = '@/assets';

function replaceHttpsCode() {
try {
// 扫描文件
const files = globSync('./src/**/*.{scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 写入文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}


  1. 使用 glob 扫描当前目录下的scss、css文件。

  2. 读取文件内容,并使用replace方法替换掉静态资源路径

  3. 写入文件,完成转换


至此,代码全部转换完成。


封装成工具包


因为多个项目都会涉及静态资源转换的问题,所以我将此脚本封装为npm包,并提炼了 transform build 命令,只需执行该命令,即可完成资源转换,以下是源码分享:


cli.ts


import { Command } from 'commander';
import { version } from '../package.json';
import buildAction from './transform';
const program = new Command();

program
.command('build')
.description('transform assets and code')
.option(
'--replaceStr <path>',
'[string] 需要全局替换的字符串,默认值: https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage',
)
.option('--imgInfoFilePath <path>', '[string] 统一的静态资源文件路径 默认值: src/scripts/assets.ts')
.option('--replaceToCode <path>', '[string] 替换为的代码 默认值: @/assets')
.option('--assetsDir <path>', '[string] 静态资源文件目录 默认值: src/assets')
.action(options => {
buildAction(options);
});

program.version(version);

program.parse();

transfrom.ts


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import chalk from 'chalk';
import { globSync } from 'glob';
import fs from 'fs';

interface Options {
replaceStr?: string;
imgInfoFilePath?: string;
replaceToCode?: string;
assetsDir?: string;
}

let replaceStr = 'https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage';
let imgInfoFilePath = 'src/scripts/assets.ts';
let replaceToCode = '@/assets';
let assetsDir = './src/assets';

function checkAssetsDir() {
logInfo('检查 src/assets 目录是否存在');

if (!fs.existsSync(assetsDir)) {
logError('assets 目录不存在,请先联系相关人员下载对应项目的静态资源文件,并放置在 src/assets 目录下');
} else {
logSuccess('assets 目录存在');
}
}

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历VariableDeclarator节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

// @ts-ignore
node.declarations.forEach(decl => {
// @ts-ignore
const localName = decl.id.name;

// @ts-ignore
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;

// @ts-ignore
logInfo(`替换 ${replaceStr}${decl?.init?.quasis?.[1]?.value?.raw}${filePath}`);

// 构建导入规范
// @ts-ignore
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
// @ts-ignore
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
// @ts-ignore
const result = generate.default(ast, {}, code);

logInfo(`备份 ${imgInfoFilePath} 文件为 ${imgInfoFilePath}.bak`);

fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);

fs.writeFileSync(imgInfoFilePath, result.code);

logSuccess(`转换 ${imgInfoFilePath} 成功`);
} catch (error: any) {
logError(error);
}
}

function replaceHttpsCode() {
logInfo('开始转换 其余文件中引用https导入的静态资源');

try {
// 扫描文件
const files = globSync('./src/**/*.{vue,js,ts,scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

if (content.includes(replaceStr)) {
logInfo(`替换 ${file} 中的 ${replaceStr}${replaceToCode}`);
}

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 保存文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}

function logInfo(info: string) {
console.log(chalk.gray(`[INFO] - 🆕 ${info}`));
}

function logSuccess(info: string) {
console.log(chalk.green(`[SUCCESS] - ✅ ${info}`));
}

function logError(info: string) {
console.log(chalk.red(`[ERROR] - ❌ ${info}`));
}

export default function main(options: Options) {
replaceStr = options.replaceStr || replaceStr;
imgInfoFilePath = options.imgInfoFilePath || imgInfoFilePath;
replaceToCode = options.replaceToCode || replaceToCode;
assetsDir = options.assetsDir || assetsDir;

checkAssetsDir();
babelTransformCode();
replaceHttpsCode();
}

作者:程序员小杨v1
来源:juejin.cn/post/7295276751595798580
收起阅读 »

Flutter开发者,需要会原生吗?-- Android 篇

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。 由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flut...
继续阅读 »

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。

由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flutter开发者到底需不需要懂原生平台?



缘起


《Flutter开发者需要掌握原生Android吗?》

这个话题跟Flutter与RN对比Flutter会不会凉同属一类,都是前两年社群最喜欢争论的话题。激烈的讨论无非是观望者太多,加之Flutter不成熟,在使用过程中会遇到不少坑。


直到今年3.7.0、3.10.0相继发布,框架改进和社区的丰富,让更多人选择拥抱Flutter,关于此类型的话题才开始沉寂下来。很多招聘网站也直接出现了Flutter开发这个岗位,而且技能也不要求原生,甚至加分项前端的技能。似乎Flutter开发者在开发过程中很少用到原生的技能,然而事实绝非如此。


我专攻Flutter有3年了,期间Android、iOS、Windows应用做过不少,Web、Linux也都略有研究;这次我将直接从Android平台出发,用切身经历来论述下:Flutter开发者,真的需要懂Android。


Flutter只是个UI框架


打开一个Flutter的项目,我们可以看到整个应用其实是基于一个Activity运行的,属于单页应用。


package com.wxq.test

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}

Activity继承自FlutterActivity,FlutterActivityonCreate内会创建FlutterActivityAndFragmentDelegate


// io/flutter/embedding/android/FlutterActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
switchLaunchThemeForNormalTheme();

super.onCreate(savedInstanceState);
// 创建代理,ActivityAndFragment都支持哦
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this); // 这个方法创建引擎,并且将context吸附上去
delegate.onRestoreInstanceState(savedInstanceState);

lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);

configureWindowForTransparency();

// 设置Activity的View,createFlutterView内部也是调用代理的方法
setContentView(createFlutterView());
configureStatusBarForFullscreenFlutterExperience();
}

这个代理将会通过engineGr0up管理FlutterEngine,通过onAttach创建FlutterEngine,并且运行createAndRunEngine方法


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
void onAttach(@NonNull Context context) {
ensureAlive();

if (flutterEngine == null) {
setupFlutterEngine();
}

if (host.shouldAttachEngineToActivity()) {

Log.v(TAG, "Attaching FlutterEngine to the Activity that owns this delegate.");
flutterEngine.getActivityControlSurface().attachToActivity(this, host.getLifecycle());
}
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);

host.configureFlutterEngine(flutterEngine);
isAttached = true;
}

@VisibleForTesting
/* package */ void setupFlutterEngine() {
Log.v(TAG, "Setting up FlutterEngine.");

// 省略处理引擎缓存的代码
String cachedEngineGr0upId = host.getCachedEngineGr0upId();
if (cachedEngineGr0upId != null) {
FlutterEngineGr0up flutterEngineGr0up =
FlutterEngineGr0upCache.getInstance().get(cachedEngineGr0upId);
if (flutterEngineGr0up == null) {
throw new IllegalStateException(
"The requested cached FlutterEngineGr0up did not exist in the FlutterEngineGr0upCache: '"
+ cachedEngineGr0upId
+ "'");
}

// *** 重点 ***
flutterEngine =
flutterEngineGr0up.createAndRunEngine(
addEntrypointOptions(new FlutterEngineGr0up.Options(host.getContext())));
isFlutterEngineFromHost = false;
return;
}

// Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
// FlutterView.
Log.v(
TAG,
"No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
+ " this FlutterFragment.");

FlutterEngineGr0up group =
engineGr0up == null
? new FlutterEngineGr0up(host.getContext(), host.getFlutterShellArgs().toArray())
: engineGr0up;
flutterEngine =
group.createAndRunEngine(
addEntrypointOptions(
new FlutterEngineGr0up.Options(host.getContext())
.setAutomaticallyRegisterPlugins(false)
.setWaitForRestorationData(host.shouldRestoreAndSaveState())));
isFlutterEngineFromHost = false;
}

再调用onCreateView创建SurfaceView或者外接纹理TextureView,这个View就是Flutter的赖以绘制的画布。


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
@NonNull
View onCreateView(
LayoutInflater inflater,
@Nullable ViewGr0up container,
@Nullable Bundle savedInstanceState,
int flutterViewId,
boolean shouldDelayFirstAndroidViewDraw)
{
Log.v(TAG, "Creating FlutterView.");
ensureAlive();

if (host.getRenderMode() == RenderMode.surface) {
FlutterSurfaceView flutterSurfaceView =
new FlutterSurfaceView(
host.getContext(), host.getTransparencyMode() == TransparencyMode.transparent);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterSurfaceViewCreated(flutterSurfaceView);

// Create the FlutterView that owns the FlutterSurfaceView.
flutterView = new FlutterView(host.getContext(), flutterSurfaceView);
} else {
FlutterTextureView flutterTextureView = new FlutterTextureView(host.getContext());

flutterTextureView.setOpaque(host.getTransparencyMode() == TransparencyMode.opaque);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterTextureViewCreated(flutterTextureView);

// Create the FlutterView that owns the FlutterTextureView.
flutterView = new FlutterView(host.getContext(), flutterTextureView);
}

flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
// 忽略一些代码...
return flutterView;
}

由此可见,Flutter的引擎实际上是运行在Android提供的View上,这个View必然是设置在Android的组件上,可以是Activity、Framgent,也可以是WindowManager。

这就给我们带来了很大的可塑性,只要你能掌握这套原理,混合开发就随便玩了。


Android,是必须的能力


通过对Flutter运行机制的剖析,我们很明确它就是个单纯的UI框架,惊艳的跨端UI都离不开Android的能力,这也说明Flutter开发者不需要会原生注定走不远

下面几个例子,也可以充分论证这个观点。


一、Flutter插件从哪里来


上面讲述到的原理,Flutter项目脚手架已经帮我们做好,但这只是UI绘制层面的;实际上很多Flutter应用,业务能力都是由Pub.dev提供的,随着社区框架的增多,开发者大多时候是感知不到需要Android能力的。

然而业务的发展是迅速的,我们开始需要很多pub社区并不支持的能力,比如:getMetaDatagetMacAddressreboot/shutdownsendBroadcast等,这些能力都需要我们使用Android知识,以编写插件的形式,提供给Flutter调用。

Flutter Plugin在Dart层和Android层都实现了MethodChannel对象,同一个Engine下,只要传入一致的channelId字符串,就能建立双向的通道互相传输基本类型数据。


class FlutterNativeAbilityPlugin : FlutterPlugin, MethodCallHandler {
private var applicationContext: Context? = null

private lateinit var channel: MethodChannel

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
applicationContext = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_native_ability")
channel.setMethodCallHandler(this)
}

class MethodChannelFlutterNativeAbility extends FlutterNativeAbilityPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('flutter_native_ability');
}

发送端通过invokeMethod调用对应的methodName,传入arguments;接收端通过实现onMethodCall方法,接收发送端的invokeMethod操作,执行需要的操作后,通过Result对象返回结果。


@override
Future<String> getMacAddress() async {
final res = await methodChannel.invokeMethod<String>('getMacAddress');
return res ?? '';
}

@override
Future<void> reboot() async {
await methodChannel.invokeMethod<String>('reboot');
}

"getMacAddress" -> {
Log.i(TAG, "onMethodCall: getMacAddress")
val macAddress = CommonUtils().getDeviceMac(applicationContext)
result.success(macAddress)
}
"reboot" -> {
Log.i(TAG, "onMethodCall: reboot")
beginToReboot(applicationContext)
result.success(null)
}

ps:invokeMethod和onMethodCall双端都能实现,都能作为发送端和接收端。


二、Flutter依赖于Android机制,得以“横行霸道”


目前我们将Flutter应用于OS的开发,这需要我们不单是从某个独立应用去思考。很多应用、服务都需要从整个系统业务去设计,在以下这些需求中,我们深切感受到:Flutter跟Android配合后,能发挥更大的业务价值。



  • Android服务运行dart代码,广播接收器与Flutter通信


我们很多服务需要开机自启,这必须遵循Android的机制。通常做法是:接收开机广播,在广播接收器中启动Service,然后再去运行DartEngie,执行跨平台的代码;


class MyTestService : Service() {

private lateinit var engineGr0up: FlutterEngineGr0up

override fun onCreate() {
super.onCreate()
startForeground()

engineGr0up = FlutterEngineGr0up(this)
// initService是Flutter层的方法入口点
val dartEntrypoint = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"initService"
)
val flutterEngine = engineGr0up.createAndRunEngine(this, dartEntrypoint)
// Flutter调用Native方法的 MethodChannel 也初始化一下,调用安装接口需要
FlutterToNativeChannel(flutterEngine, this)
}
}

同时各应用之间需要通信,这时我们也会通过Broadcat广播机制,在Android的广播接收器中,通过MechodChannel发送给Flutter端。


总而言之,我们必须 遵循系统的组件规则,基于Flutter提供的通信方式,将Android的消息、事件等发回给Flutter, 带来的跨端效益是实实在在的!



  • 悬浮窗需求


悬浮窗口在视频/直播场景下用的最多,当你的应用需要开启悬浮窗的时候,Flutter将完全无法支持这个需求。

实际上我们只需要在Android中创建一个WindowManager,基于EngineGround创建一个DartEngine;然后创建flutterView,把DartEngine吸附到flutterView上,最后把flutterView Add to WindowManager即可。


private lateinit var flutterView: FlutterView
private var windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
private val inflater =
context.getSystemService(Service.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private val metrics = DisplayMetrics()

@SuppressLint("InflateParams")
private var rootView = inflater.inflate(R.layout.floating, null, false) as ViewGr0up

windowManager.defaultDisplay.getMetrics(metrics)
layoutParams.gravity = Gravity.START or Gravity.TOP

windowManager.addView(rootView, layoutParams)

flutterView = FlutterView(inflater.context, FlutterSurfaceView(inflater.context, true))
flutterView.attachToFlutterEngine(engine)

engine.lifecycleChannel.appIsResumed()

rootView.findViewById<FrameLayout>(R.id.floating_window)
.addView(
flutterView,
ViewGr0up.LayoutParams(
ViewGr0up.LayoutParams.MATCH_PARENT,
ViewGr0up.LayoutParams.MATCH_PARENT
)
)
windowManager.updateViewLayout(rootView, layoutParams)


  • 不再局限单页应用


最近我们在升级应用中,遇到一个比较尴尬的需求:在原有OTA功能下,新增一个U盘插入本地升级的功能,希望升级能力和UI都能复用,且互不影响各自流程。


如果是Android项目很简单,把升级的能力抽象,通过多个Activity管理自己的业务流程,互不干扰。但是Flutter项目属于单页应用,不可能同时展示两个路由页面各自处理,所以也必须 走Android的机制,让Flutter应用同时运行多个Activity。


我们在Android端监听了U盘的插入事件,在需要本地升级的时候直接弹出Activity。Activity是继承FlutterActivity的,通过<metadata>标签指定方法入口点。与MainActivity运行main区分开,然后通过重写getDartEntrypointArgs方法,把必要的参数传给Flutter入口函数,从而独立运行本地升级的业务,而且UI和能力都能复用。


class LocalUpgradeActivity : FlutterActivity() {
}

<activity
android:name=".LocalUpgradeActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustResize">

<meta-data
android:name="io.flutter.Entrypoint"
android:value="runLocalUpgradeApp" />
<!-- 这里指定Dart层的入口点-->
</activity>

override fun getDartEntrypointArgs(): MutableList<String?> {
val filePath: String? = intent?.getStringExtra("filePath")
val tag: String? = intent?.getStringExtra("tag")
return mutableListOf(filePath, tag)
}

至此,我们的Flutter应用不再是单页应用,而且所有逻辑和UI都将在Flutter层实现!


总结


我们遵循Android平台的机制,把逻辑和UI都尽可能的交给Flutter层,让其在跨平台上发挥更大的可能性,在落地过程确实切身体会到Android的知识是何等的重要!

当然我们的应用场景可能相对复杂,一般应用也许不会有这么多的应用组合;但无论Flutter如何完善,社区更加壮大,它都离不开底层平台的支持。

作为Flutter开发者,有精力的情况下,一定要多学各个平台的框架和能力,让Flutter、更让自己走的更远!


作者:Karl_wei
来源:juejin.cn/post/7295571705689423907
收起阅读 »

正式变实习?掌趣科技这波操作太恶心

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历? 1.Offer 展示 2.面试题真题 问项目: 讲讲你最好的一个项目怎么实现的 ? 问八股: JVM 的执行...
继续阅读 »

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历?


1.Offer 展示



2.面试题真题


问项目:


讲讲你最好的一个项目怎么实现的 ?


问八股:



  1. JVM 的执行流程?

  2. JVM 是怎么找到一个类的?

  3. JVM 的内存布局?

  4. Java 虚拟机是怎么进行垃圾回收的?

  5. Java 有可能产生内存泄漏吗,举例? 内存泄露会导致什么问题?

  6. GC 的过程, 内存是怎么分配的, 是一片一片的使用呢, 还是一大块的使用? 还是想 C/C++ 那样零散的内存呢?怎么解决内存碎片问题?

  7. Java 中你都用过什么容器?HashMap 和 HashTable 的区别?ConcurrentHashMap 怎么保证线程安全问题的?

  8. 你用过 Java 的多线程锁吗?ReentrantLock 和 Synchronized 的区别?你了解什么是悲观锁?什么是乐观锁吗?

  9. Java 是怎么实现多态的? Java 中能多继承吗? Java 中的注解怎么实现的?

  10. Java 虚拟机是怎么找到一个方法,一个类,怎么根据注解找到对应的方法

  11. Java 中反射的作用,反射怎么实现的?

  12. 反射可以实现注解吗?

  13. 你了解自动拆箱装箱吗? 拆箱装箱怎么实现的?

  14. 项目中有碰见过死锁的问题吗? 什么是死锁? 只锁一个对象会产生死锁吗?

  15. Netty 用过吗?了解过吗?

  16. MySQL 有几种索引? 按字段特性分,按物理存储分呢? 索引底层有几种实现呢? MySQL 的有哪几种存储引擎? InnoDB 和 MyISAM 有什么区别?有什么优缺点?

  17. 做一个查询操作的时候,使用哪种索引?

  18. 什么叫回表查询? 如果没有创建主键,怎么进行回表查询?

  19. 什么叫索引覆盖?

  20. 一个表是索引越多越好,还是越少越好?

  21. 你了解什么叫表锁?什么叫行锁吗?什么情况下使用行锁?什么情况下使用表锁?

  22. 如果一个表的写操作比较多,是加行锁比较好,还是加表锁比较好?

  23. 你能说一下事务的隔离级别吗?

  24. Redis 数据库怎么是实现持久化的 ?Redis 里边都有哪些数据类型? ZSet 怎么用?

  25. 什么是缓存雪崩、缓存击穿、缓存穿透,分别怎么解决?

  26. 做过 Java 的网络编程吗?给我说一下 Socket 套接字的创建流程?

  27. 讲一讲三次握手,四次挥手的流程?TCP 和 UDP 的区别?

  28. 使用 UDP 来传输数据,怎么来保证他的可靠性,就像你刚刚说的后发先至问题?



PS:面试问的还挺细的,但因为做足了准备,所以这些面试题基本都拿下了。


以上问题来自学员的整理,在此感谢。



3.套路来了


拿到 Offer 本来是件开心的事,然而当聊完入职事项之后,整个人就不好了,来看看具体的经过吧:


问题1:正式工作变实习?


秋招明明投递的是正式工作,然而拿到 Offer 之后,HR 却必须先让去实习 5 个月。


投递详情如下:



明确是秋招正式岗位,而非实习。


正常的逻辑应该是拿到 Offer 之后,等明年毕业之后先去公司报道,只有 3 个月试用期,试用期没问题就转为正式员工了。



问题2:实习薪资


实习也是按照实习的工资,而非正式工资的 80% 发放的。


问题3:可能不通过&不谈正式薪资


HR 说明年四月实习期过了之后,再谈正式薪资,言外之意,如果实习期没过,那就不要你了,也就不用谈薪资了。那么请问,投递正式岗位又有什么意义呢?


问题4:实习时间超长


通常实习时间也就是 2-3 个月,而掌趣要求 11 月中旬去实习,至少实习到明年 4 月份,这个实习的时间未免要太长了。


小结


作为国内老牌上市公司,竟然以找正式工作的幌子把入选人悄悄转为实习生,这件事对有经验的人来说,一眼就知道怎么回事,然而对于涉世未深,刚步入社会的年轻人来说,却是满满的套路,还未感受生活的美好,就经历了人心的险恶。


人在做天在看,希望某些公司不要有这样的骚操作,招实习就是招实习,招正式员工就是招正式员工,不要混为一谈,更不要暗箱操作,更不要欺负那些涉世未深的年轻人。


作者:Java中文社群
来源:juejin.cn/post/7292946512585162806
收起阅读 »

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:




  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }



  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }



  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }



  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }



  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }



  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }



  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:




  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。




  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »

HarmonyOS开发:基于http开源一个网络请求库

前言 网络封装的目的,在于简洁,使用起来更加的方便,也易于我们进行相关动作的设置,如果,我们不封装,那么每次请求,就会重复大量的代码逻辑,如下代码,是官方给出的案例: // 引入包名 import http from '@ohos.net.http'; //...
继续阅读 »

前言


网络封装的目的,在于简洁,使用起来更加的方便,也易于我们进行相关动作的设置,如果,我们不封装,那么每次请求,就会重复大量的代码逻辑,如下代码,是官方给出的案例:


// 引入包名
import http from '@ohos.net.http';

// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {
console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(
// 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
"EXAMPLE_URL",
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递内容
extraData: {
"data": "data to send",
},
expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
usingCache: true, // 可选,默认为true
priority: 1, // 可选,默认为1
connectTimeout: 60000, // 可选,默认为60000ms
readTimeout: 60000, // 可选,默认为60000ms
usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
}, (err, data) => {
if (!err) {
// data.result为HTTP响应内容,可根据业务需要进行解析
console.info('Result:' + JSON.stringify(data.result));
console.info('code:' + JSON.stringify(data.responseCode));
// data.header为HTTP响应头,可根据业务需要进行解析
console.info('header:' + JSON.stringify(data.header));
console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 当该请求使用完毕时,调用destroy方法主动销毁
httpRequest.destroy();
} else {
console.info('error:' + JSON.stringify(err));
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 当该请求使用完毕时,调用destroy方法主动销毁。
httpRequest.destroy();
}
}
);

以上的案例,每次请求书写这么多代码,在实际的开发中,是无法承受的,所以基于此,封装是很有必要的,把公共的部分进行抽取包装,固定不变的参数进行初始化设置,重写基本的请求方式,这是我们封装的基本宗旨。


我们先看一下封装之后的调用方式:


异步请求


Net.get("url").requestString((data) => {
//data 为 返回的json字符串
})

同步请求


const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
//data 为 返回的json字符串

装饰器请求


@GET("url")
private getData():Promise<string> {
return null
}

封装之后,不仅使用起来更加的便捷,而且还拓展了请求类型,满足不同需求的场景。


本篇的文章内容大致如下:


1、net库主要功能点介绍


2、net库快速依赖使用


3、net库全局初始化


4、异步请求介绍


5、同步请求介绍


6、装饰器请求介绍


7、上传下载介绍


8、Dialog加载使用


9、相关总结


一、net库主要功能点介绍


目前net库一期已经开发完毕,har包使用,大家可以看第二项,截止到发文前,所支持的功能如下:


■ 支持全局初始化


■ 支持统一的BaseUrl


■ 支持全局错误拦截


■ 支持全局头参拦截


■ 支持同步方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持异步方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持装饰器方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持dialog加载


■ 支持返回Json字符串


■ 支持返回对象


■ 支持返回数组


■ 支持返回data一层数据


■ 支持上传文件


■ 支持下载文件


□ 数据缓存开发中……


二、net库快速依赖使用


私服和远程依赖,由于权限和审核问题,预计需要等到2024年第一季度面向所有开发者,所以,只能使用本地静态共享包和源码 两种使用方式,本地静态共享包类似Android中的aar依赖,直接复制到项目中即可,目前源码还在优化中,先暴露静态共享包这一使用方式。


本地静态共享包har包使用


首先,下载har包,点击下载


下载之后,把har包复制项目中,目录自己创建,如下,我创建了一个libs目录,复制进去



引入之后,进行同步项目,点击Sync Now即可,当然了你也可以,将鼠标放置在报错处会出现提示,在提示框中点击Run 'ohpm install'。


需要注意,@app/net,是用来区分目录的,可以自己定义,比如@aa/bb等,关于静态共享包的创建和使用,请查看如下我的介绍,这里就不过多介绍。


HarmonyOS开发:走进静态共享包的依赖与使用


查看是否引用成功


无论使用哪种方式进行依赖,最终都会在使用的模块中,生成一个oh_modules文件,并创建源代码文件,有则成功,无则失败,如下:



三、net库全局初始化


推荐在AbilityStage进行初始化,初始化一次即可,初始化参数可根据项目需要进行选择性使用。


Net.getInstance().init({
baseUrl: "https://www.vipandroid.cn", //设置全局baseurl
connectTimeout: 10000, //设置连接超时
readTimeout: 10000, //设置读取超时
netErrorInterceptor: new MyNetErrorInterceptor(), //设置全局错误拦截,需要自行创建,可在这里进行错误处理
netHeaderInterceptor: new MyNetHeaderInterceptor(), //设置全局头拦截器,需要自行创建
header: {}, //头参数
resultTag: []//接口返回数据参数,比如data,items等等
})

1、初始化属性介绍


初始化属性,根据自己需要选择性使用。


属性类型概述
baseUrlstring一般标记为统一的请求前缀,也就是域名
connectTimeoutnumber连接超时,默认10秒
readTimeoutnumber读取超时,默认10秒
netErrorInterceptorINetErrorInterceptor全局错误拦截器,需继承INetErrorInterceptor
netHeaderInterceptorINetHeaderInterceptor全局请求头拦截器,需继承INetHeaderInterceptor
headerObject全局统一的公共头参数
resultTagArray接口返回数据参数,比如data,items等等

2、设置请求头拦截


关于全局头参数传递,可以通过以上的header参数或者在请求头拦截里均可,如果没有同步等逻辑操作,只是固定的头参数,建议直接使用header参数。


名字自定义,实现INetHeaderInterceptor接口,可在netHeader方法里打印请求头或者追加请求头。


import { HttpHeaderOptions, NetHeaderInterceptor } from '@app/net'

class MyNetHeaderInterceptor implements NetHeaderInterceptor {
getHeader(options: HttpHeaderOptions): Promise<Object> {
//可以进行接口签名,传入头参数
return null
}
}

HttpHeaderOptions对象


返回了一些常用参数,可以用于接口签名等使用。


export class HttpHeaderOptions {
url?: string //请求地址
method?: http.RequestMethod //请求方式
header?: Object //头参数
params?: Object //请求参数
}

3、设置全局错误拦截器


名字自定义,实现INetErrorInterceptor接口,可在httpError方法里进行全局的错误处理,比如统一跳转,统一提示等。


import { NetError } from '@app/net/src/main/ets/error/NetError';
import { INetErrorInterceptor } from '@app/net/src/main/ets/interceptor/INetErrorInterceptor';

export class MyNetErrorInterceptor implements INetErrorInterceptor {
httpError(error: NetError) {
//这里进行拦截错误信息

}
}

NetError对象


可通过如下方法获取错误code和错误描述信息。


/*
* 返回code
* */

getCode():number{
return this.code
}

/*
* 返回message
* */

getMessage():string{
return this.message
}

四、异步请求介绍


1、请求说明


为了方便数据的针对性返回,目前异步请求提供了三种请求方法,在实际的 开发中,大家可以针对需要,选择性使用。


request方法


Net.get("url").request<TestModel>((data) => {
//data 就是返回的TestModel对象
})

此方法,针对性返回对应的data数据对象,如下json,则会直接返回需要的data对象,不会携带外层的code等其他参数,方便大家直接的拿到数据。


{
"code": 0,
"message": "数据返回成功",
"data": {}
}

如果你的data是一个数组,如下json:


{
"code": 0,
"message": "数据返回成功",
"data": []
}

数组获取


Net.get("url").request<TestModel[]>((data) => {
//data 就是返回的TestModel[]数组
})

//或者如下

Net.get("url").request<Array<TestModel>>((data) => {
//data 就是返回的TestModel数组
})

可能大家有疑问,如果接口返回的json字段不是data怎么办?如下:


举例一


{
"code": 0,
"message": "数据返回成功",
"items": {}
}

举例二


{
"code": 0,
"message": "数据返回成功",
"models": {}
}

虽然网络库中默认取的是json中的data字段,如果您的数据返回类型字段有多种,如上json,可以通过全局初始化resultTag进行传递或者局部setResultTag传递即可。


全局设置接口返回数据参数【推荐】


全局设置,具体设置请查看上边的全局初始化一项,只设置一次即可,不管你有多少种返回参数,都可以统一设置。


 Net.getInstance().init({
resultTag: ["data", "items", "models"]//接口返回数据参数,比如data,items等等
})

局部设置接口返回数据参数


通过setResultTag方法设置即可。


Net.get("")
.setResultTag(["items"])
.request<TestModel>((data) => {

})

requestString方法


requestString就比较简单,就是普通的返回请求回来的json字符串。


Net.get("url").requestString((data) => {
//data 为 返回的json字符串
})

requestObject方法


requestObject方法也是获取对象,和request不同的是,它不用设置返回参数,因为它是返回的整个json对应的对象, 也就是包含了code,message等字段。


Net.get("url").requestObject<TestModel>((data) => {
//data 为 返回的TestModel对象
})

为了更好的复用共有字段,你可以抽取一个基类,如下:


export class ApiResult<T> {
code: number
message: string
data: T
}

以后就可以如下请求:


Net.get("url").requestObject<ApiResult<TestModel>>((data) => {
//data 为 返回的ApiResult对象
})

回调函数

回调函数有两个,一个成功一个失败,成功回调必调用,失败可选择性调用。


只带成功


Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

成功失败都带


Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
}, (error) => {
//失败
})

2、get请求


 Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

3、post请求


Net.post("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

4、delete请求


 Net.delete("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

5、put请求


Net.put("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options。


OPTIONS
HEAD
TRACE
CONNECT

7、各个方法调用


除了正常的请求方式之外,你也可以调用如下的参数:


方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:


Net.get("url")
.setHeaders({})//单独添加请求头参数
.setBaseUrl("")//单独替换BaseUrl
.setParams({})//单独添加参数
.setConnectTimeout(10000)//单独设置连接超时
.setReadTimeout(10000)//单独设置读取超时
.setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
.setUsingCache(true)//使用缓存,默认为true
.setPriority(1)//设置优先级 默认为1
.setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
.setResultTag([""])//接口返回数据参数,比如data,items等等
.setContext(this.context)//设置上下文,用于上传文件和下载文件
.setCustomDialogController()//传递的dialog控制器,用于展示dialog
.request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

五、同步请求介绍


同步请求需要注意,需要await关键字和async关键字结合使用。


 private async getTestModel(){
const testModel = await Net.get("url").returnData<TestModel>()
}

1、请求说明


同步请求和异步请求一样,也是有三种方式,是通过参数的形式,默认直接返回data层数据。


返回data层数据


和异步种的request方法类似,只返回json种的data层对象数据,不会返回code等字段。


 private async getData(){
const data = await Net.get("url").returnData<TestModel>()
//data为 返回的 TestModel对象
}

返回Json对象


和异步种的requestObject方法类似,会返回整个json对象,包含code等字段。


 private async getData(){
const data = await Net.get("url").returnData<TestModel>(ReturnDataType.OBJECT)
//data为 返回的 TestModel对象
}

返回Json字符串


和异步种的requestString方法类似。


private async getData(){
const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
//data为 返回的 json字符串
}

返回错误


异步方式有回调错误,同步方式如果发生错误,也会直接返回错误,结构如下:


{
"code": 0,
"message": "错误信息"
}

除了以上的错误捕获之外,你也可以全局异常捕获,


2、get请求



const data = await Net.get("url").returnData<TestModel>()

3、post请求



const data = await Net.post("url").returnData<TestModel>()

4、delete请求



const data = await Net.delete("url").returnData<TestModel>()

5、put请求



const data = await Net.put("url").returnData<TestModel>()

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options


OPTIONS
HEAD
TRACE
CONNECT

7、各个方法调用


除了正常的请求方式之外,你也可以调用如下的参数:


方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:


const data = await Net.get("url")
.setHeaders({})//单独添加请求头参数
.setBaseUrl("")//单独替换BaseUrl
.setParams({})//单独添加参数
.setConnectTimeout(10000)//单独设置连接超时
.setReadTimeout(10000)//单独设置读取超时
.setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
.setUsingCache(true)//使用缓存,默认为true
.setPriority(1)//设置优先级 默认为1
.setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
.setResultTag([""])//接口返回数据参数,比如data,items等等
.setContext(this.context)//设置上下文,用于上传文件和下载文件
.setCustomDialogController()//传递的dialog控制器,用于展示dialog
.returnData<TestModel>()
//data为 返回的 TestModel对象

六、装饰器请求介绍


网络库允许使用装饰器的方式发起请求,也就是通过注解的方式,目前采取的是装饰器方法的形式。


1、请求说明


装饰器和同步异步有所区别,只返回两种数据类型,一种是json字符串,一种是json对象,暂时不提供返回data层数据。 在使用的时候,您可以单独创建工具类或者ViewModel或者直接使用,都可以。


返回json字符串


@GET("url")
private getData():Promise<string> {
return null
}

返回json对象


@GET("url")
private getData():Promise<TestModel> {
return null
}

2、get请求


@GET("url")
private getData():Promise<TestModel> {
return null
}

3、post请求


@POST("url")
private getData():Promise<TestModel> {
return null
}

4、delete请求


@DELETE("url")
private getData():Promise<TestModel> {
return null
}

5、put请求


@PUT("url")
private getData():Promise<TestModel> {
return null
}

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如@OPTIONS。


OPTIONS
HEAD
TRACE
CONNECT

当然,大家也可以使用统一的NET装饰器,只不过需要自己设置请求方法,代码如下:


@NET("url", { method: http.RequestMethod.POST })
private getData():Promise<string> {
return null
}

7、装饰器参数传递


直接参数传递


直接参数,在调用装饰器请求时,后面添加即可,一般针对固定参数。


@GET("url", {
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
private getData():Promise<string> {
return null
}

动态参数传递


动态参数适合参数可变的情况下传递,比如分页等情况。


@GET("url")
private getData(data? : HttpOptions):Promise<string> {
return null
}

调用时传递


private async doHttp(){
const data = await this.getData({
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
}

装饰器参数传递


使用DATA装饰器,DATA必须在上!


@DATA({
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
@GET("url")
private getData():Promise<string> {
return null
}

七、上传下载介绍


1、上传文件


Net.uploadFile("")//上传的地址
.setUploadFiles([])//上传的文件 [{ filename: "test", name: "test", uri: "internal://cache/test.jpg", type: "jpg" }]
.setUploadData([])//上传的参数 [{ name: "name123", value: "123" }]
.setProgress((receivedSize, totalSize) => {
//监听上传进度
})
.request((data) => {
if (data == UploadTaskState.COMPLETE) {
//上传完成
}
})

方法介绍


方法类型概述
uploadFilestring上传的地址
setUploadFilesArray上传的文件数组
setUploadDataArray上传的参数数组
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求上传,data类型为UploadTaskState,有三种状态:START(开始),COMPLETE(完成),ERROR(错误)

其他方法


删除上传进度监听

uploadRequest.removeProgressCallback()

删除上传任务

uploadRequest.deleteUploadTask((result) => {
if (result) {
//成功
} else {
//失败
}
})

2、下载文件


Net.downLoadFile("http://10.47.24.237:8888/harmony/log.har")
.setContext(EntryAbility.context)
.setFilePath(EntryAbility.filePath)
.setProgress((receivedSize, totalSize) => {
//监听下载进度
})
.request((data) => {
if (data == DownloadTaskState.COMPLETE) {
//下载完成
}
})

方法介绍


方法类型概述
downLoadFilestring下载的地址
setContextContext上下文
setFilePathstring下载后保存的路径
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求下载,data类型为DownloadTaskState,有四种状态:START(开始),COMPLETE(完成),PAUSE(暂停),REMOVE(结束)

其他方法


移除下载的任务

    downLoadRequest.deleteDownloadTask((result) => {
if (result) {
//移除成功
} else {
//移除失败
}
})

暂停下载任务

downLoadRequest.suspendDownloadTask((result) => {
if (result) {
//暂停成功
} else {
//暂停失败
}
})

重新启动下载任务

downLoadRequest.restoreDownloadTask((result) => {
if (result) {
//成功
} else {
//失败
}
})

删除监听下载进度

downLoadRequest.removeProgressCallback()

八、Dialog加载使用



1、定义dialog控制器


NetLoadingDialog是net包中自带的,菊花状弹窗,如果和实际业务不一致,可以更换。


private mCustomDialogController = new CustomDialogController({
builder: NetLoadingDialog({
loadingText: '请等待...'
}),
autoCancel: false,
customStyle: true
})

2、调用传递控制器方法


此方法会自动显示和隐藏dialog,如果觉得不合适,大家可以自己定义即可。


setCustomDialogController(this.mCustomDialogController)

九、相关总结


开发环境如下:


DevEco Studio 4.0 Beta2,Build Version: 4.0.0.400

Api版本:9

hvigorVersion:3.0.2

目前呢,暂时不支持缓存,后续会逐渐加上,大家在使用的过程中,需要任何的问题,都可以进行反馈,都会第一时间进行解决。


作者:程序员一鸣
来源:juejin.cn/post/7295397683397181450
收起阅读 »

工作六年,看到这样的代码,内心五味杂陈......

工作六年,看到这样的代码,内心五味杂陈...... 那天下午,看到了令我终生难忘的代码,那一刻破防了...... 🔊 本文记录那些年的 Java 代码轶事 ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起...... 📖一、历史背景 1.1 数据隔...
继续阅读 »

工作六年,看到这样的代码,内心五味杂陈......


那天下午,看到了令我终生难忘的代码,那一刻破防了......



🔊 本文记录那些年的 Java 代码轶事



ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起......


📖一、历史背景


1.1 数据隔离


预发,灰度,线上环境共用一个数据库。每一张表有一个 env 字段,环境不同值不同。特别说明: env 字段即环境字段。如下图所示:
image.png


1.2 隔离之前


🖌️插曲:一开始只有 1 个核心表有 env 字段,其他表均无该字段;
有一天预发环境的操作影响到客户线上的数据。 为了彻底隔离,剩余的二十几个表均要添加上环境隔离字段。


当时二十几张表已经大量生产数据,隔离需要做好兼容过渡,保障数据安全。


1.3 隔离改造


其他表历史数据很难做区分,于是新增加的字段 env 初始化 all ,表示预发线上都能访问。以此达到历史数据的兼容。


每一个环境都有一个自己独立标志;从 application.properties 中读该字段;最终到数据库执行的语句如下:


SELECT XXX FROM tableName WHERE env = ${环境字段值} and ${condition}

1.4 隔离方案


最拉胯的做法:每一张表涉及到的 DO、Mapper、XML等挨个添加 env 字段。但我指定不能这么干!!!


image.png


具体方案:自定义 mybatis 拦截器进行统一处理。 通过这个方案可以解决以下几个问题:



  • 业务代码不用修改,包括 DO、Mapper、XML等。只修改 mybatis 拦截的逻辑。

  • 挨个添加补充字段,工程量很多,出错概率极高

  • 后续扩展容易


1.5 最终落地


在 mybatis 拦截器中, 通过改写 SQL。新增时填充环境字段值,查询时添加环境字段条件。真正实现改一处即可。 考虑历史数据过渡,将 env = ${当前环境}
修改成 en 'all')


SELECT xxx FROM ${tableName} WHERE env in (${当前环境},'all') AND ${其他条件}

具体实现逻辑如下图所示:


image.png



  1. 其中 env 字段是从 application.properties 配置获取,全局唯一,只要环境不同,env 值不同

  2. 借助 JSqlParser 开源工具,改写 sql 语句,修改重新填充、查询拼接条件即可。链接JSQLParser


思路:自定义拦截器,填充环境参数,修改 sql 语句,下面是部分代码示例:


@Intercepts(
{@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)

@Component
public class EnvIsolationInterceptor implements Interceptor {
......
@Override
public Object intercept(Invocation invocation) throws Throwable {
......
if (SqlCommandType.INSERT == sqlCommandType) {
try {
// 重新 sql 执行语句,填充环境参数等
insertMethodProcess(invocation, boundSql);
} catch (Exception exception) {
log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), exception);
throw exception;
}
}

return invocation.proceed();
}
}

一气呵成,完美上线。


image.png


📚二、发展演变


2.1 业务需求


随着业务发展,出现了以下需求:



  • 上下游合作,我们的 PRC 接口在匹配环境上与他们有差异,需要改造


SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?,'all')


  • 有一些环境的数据相互相共享,比如预发和灰度等

  • 开发人员的部分后面,希望在预发能纠正线上数据等


2.2 初步沟通


这个需求的落地交给了来了快两年的小鲜肉。 在开始做之前,他也问我该怎么做;我简单说了一些想法,比如可以跳过环境字段检查,不拼接条件;或者拼接所有条件,这样都能查询;亦或者看一下能不能注解来标志特定方法,你想一想如何实现......


image.png


(●ˇ∀ˇ●)年纪大了需要给年轻人机会。


2.3 勤劳能干


小鲜肉,没多久就实现了。不过有一天下午他遇到了麻烦。他填充的环境字段取出来为 null,看来很久没找到原因,让我帮他看看。(不久前也还教过他 Arthas 如何使用呢,这种问题应该不在话下吧🤔)


2.4 具体实现


大致逻辑:在需要跳过环境条件判断的方法前后做硬编码处理,同环切面逻辑, 一加一删。填充颜色部分为小鲜肉的改造逻辑。


image.png


大概逻辑就是:将 env 字段填充所有环境。条件过滤的忽略的目的。


SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${其他条件}

2.5 错误原因


经过排查是因为 API 里面有多处对 threadLoal 进行处理的逻辑,方法之间存在调用。 简化举例: A 和 B 方法都是独立的方法, A 在调用 B 的过程,B 结束时把上下文环境字段删除, A 在获取时得到 null。具体如下:


image.png


2.6 五味杂陈


当我看到代码的一瞬间,彻底破防了......


image.png


queryProject 方法里面调用 findProjectWithOutEnv,
在两个方法中,都有填充处理 env 的代码。


2.7 遍地开花


然而,这三行代码,随处可见,在业务代码中遍地开花.......


image.png


// 1. 变量保存 oriFilterEnv
String oriFilterEnv = UserHolder.getUser().getFilterEnv();

// 2. 设置值到应用上下文
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());

//....... 业务代码 ....

// 3. 结束复原
UserHolder.getUser().setFilterEnv(oriFilterEnv);

image.png


改了个遍,很勤劳👍......


2.8 灵魂开问


image.png


难道真的就只能这么做吗,当然还有......



  • 开闭原则符合了吗

  • 改漏了应该办呢

  • 其他人遇到跳过的检查的场景也加这样的代码吗

  • 业务代码和功能代码分离了吗

  • 填充到应用上下文对象 user 合适吗

  • .......


大量魔法值,单行字符超500,方法长度拖几个屏幕也都睁一眼闭一只眼了,但整这一出,还是破防......


内心涌动😥,我觉得要重构一下。


📒三、重构一下


3.1 困难之处


在 mybatis intercept 中不能直接精准地获取到 service 层的接口调用。 只能通过栈帧查询到调用链。


3.2 问题列表



  • 尽量不要修改已有方法,保证不影响原有逻辑;

  • 尽量不要在业务方法中修改功能代码;关注点分离;

  • 尽量最小改动,修改一处即可实现逻辑;

  • 改造后复用能力,而不是依葫芦画瓢地添加这种代码


3.3 实现分析



  1. 用独立的 ThreadLocal,不与当前用户信息上下文混合使用

  2. 注解+APO,通过注解参数解析,达到目标功能

  3. 对于方法之间的调用或者循环调用,要考虑优化


同一份代码,在多个环境运行,不管如何,一定要考虑线上数据安全性。


3.4 使用案例


改造后的使用案例如下,案例说明:project 表在预发环境校验跳过


@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})


@SneakyThrows
@GetMapping("/importSignedUserData")
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void importSignedUserData(
......
HttpServletRequest request,
HttpServletResponse response)
{
......
}

在使用的调用入口处添加注解。


3.5 具体实现



  1. 方法上标记注解, 注解参数定义规则

  2. 切面读取方法上面的注解规则,并传递到应用上下文

  3. 拦截器从应用上下文读取规则进行规则判断


image.png


注解代码


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {

/**
* 是否跳过环境。 默认 true,不推荐设置 false
*
* @return
*/

boolean isKip() default true;

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipEnvList() default {};

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipTableList() default {};
}


3.6 不足之处



  1. 整个链路上的这个表操作都会跳过,颗粒度还是比较粗

  2. 注解只能在入口处使用,公共方法调用尽量避免


🤔那还要不要完善一下,还有什么没有考虑到的点呢? 拿起手机看到快12点的那一刻,我还是选择先回家了......


📝 四、总结思考


4.1 隔离总结


这是一个很好参考案例:在应用中既做了数据隔离,也做了数据共享。通过自定义拦截器做数据隔离,通过自定注解切面实现数据共享。


4.2 编码总结


同样的代码写两次就应该考虑重构了



  • 尽量修改一个地方,不要写这种边边角角的代码

  • 善用自定义注解,解决这种通用逻辑

  • 可以妥协,但是要有底线

  • ......


4.3 场景总结


简单梳理,自定义注解 + AOP 的场景


场景详细描述
分布式锁通过添加自定义注解,让调用方法实现分布式锁
合规参数校验结合 ognl 表达式,对特定的合规性入参校验校验
接口数据权限对不同的接口,做不一样的权限校验,以及不同的人员身份有不同的校验逻辑
路由策略通过不同的注解,转发到不同的 handler
......

自定义注解很灵活,应用场景广泛,可以多多挖掘。


4.4 反思总结



  • 如果一开始就做好技术方案或者直接使用不同的数据库是否可以解决这个问题

  • 是否可以拒绝那个所谓的需求

  • 先有设计再有编码


4.5 最后感想


image.png



在这个只讲业务结果,不讲技术氛围的环境里,突然有一些伤感;身体已经开始吃不消了,好像也过了那个对技术较真死抠的年纪; 突然一想,这么做的意义又有多大呢?



作者:uzong
来源:juejin.cn/post/7294844864020430902
收起阅读 »

背调,程序员入职的紧箍咒

首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。 今天跟大家分享出来,给大家在求职路上避避坑。 上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴...
继续阅读 »

首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。


今天跟大家分享出来,给大家在求职路上避避坑。


上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴,赶紧恭喜了他,现在这年头,大厂的offer没这么好拿的。


又过了两周,小张沮丧地跟我说,这家公司是先发offer后背调,结果背调之后,offer GG了,公司HR没有告知他具体原因,只是委婉地说有缘自会再相见。(手动狗头)


我听了,惋惜之余有些惊讶,问了他具体情况。


原来,小李并没有在学历上作假,也没有做合并或隐藏工作经历的事。


他犯错的点是,由于在上家公司,他跟他老板的关系不好,所以他在背调让填写上级领导信息的时候,只写了上级领导的名字,电话留的是他一个同事的。


我听后惋惜地一拍脑门儿,说:“你这么做之前,怎么也不问我一下啊?第三方背调公司进行手机号和姓名核实,都是走系统的,秒出结果。而且,这种手机号的机主姓名造假,背调结果是亮红灯的,必挂。”


小李听后,也是悔得肠子都青了,没办法,只能重新来过了。


我以前招人的时候,遇到过一次这样的情况,当时有个候选人面试通过,发起背调流程。一周后,公司HR给了我一份该候选人背调结果的pdf,上面写着:


“候选人背调信息上提供,原公司上级为郭xx,但经查手机号主为王xx,且候选人原公司并无此人。”


背调结果,红灯,不通过。


基本面


学历信息肯定不能造假,这个大家应该都清楚,学信网不是吃素的,秒出结果。


最近两份工作的入离职时间不要出问题,这个但凡是第三方背调,近两份工作是必查项,而且无论是明察还是暗访,都会查得非常仔细,很难钻空子的。


再有就是刚才说的,手机号和人名要对上,而且这个人确实是在这家公司任职的。


大家耳熟能详的大厂最好查,背调公司都有人才数据库的,而且圈子里的人也好找。再有就是,随便找个内部员工,大厂的组织结构在内部通讯软件里都能看到的。


小厂难度大一些,如果人才数据库没有的话,背调员会从网上找公司电话,然后打给前台,让前台帮忙找人。但有的前台听了会直接挂断电话。


薪资方面,不要瞒报,一般背调公司会让你打印最近半年或一年的流水,以及纳税信息。


直接上级


这应该也是大家最关心的问题之一。


马云曾经说过:离职无非两种原因,钱没给够,心委屈了。而心委屈了,绝大多数都跟自己的直接上级有关。


如果在背调的时候,担心由于自己跟直接上级关系不好,从而导致背调结果不利的话,可以尝试以下三种方式。


第一,如果你在公司里历经了好几任领导的话,可以留关系最好的那任领导的联系方式,这个是在规则允许范围内的。


第二,如果你的直接上级只是一个小组长,而你跟大领导(类似于部门负责人)关系还可以的话,可以跟大领导沟通一下,然后背调留他的信息。像这个,一般背调公司不会深究的。


就像我的那个表哥,背调公司的老板所说的:“如果一个腾讯员工,马化腾都出来给他做背调了,那我们还能说什么呢?”


第三,如果前两点走不通的话,还可以坦诚地跟HR沟通一次,说明跟上级之间确实存在一些问题,原因是什么什么。


比如:我朋友遇到了这种情况,公司由于经营不善而裁员,老板竟然无耻地威胁我朋友,如果要N+1赔偿的话,背调就不会配合。


如果你确实不是责任方的话,一般HR也能理解。毕竟都是打工人,何苦相互为难呢。


你还可以这么加上一句:“我之前工作过的公司,您背调哪家都可以,我的口碑都很好的,唯独这家有些问题。”


btw:还有一些朋友,背调的时候留平级同事的真实电话和姓名,用来冒充领导,这个是有风险的。但是遇到背调不仔细的公司,也能通过。通过概率的话,一半一半吧。


就像我那个朋友所说:“现在人力成本不便宜,如果公司想盈利的话,我的背调员一天得完成5个背调,平均不到两个小时一个。你总不能希望他们个个都是名侦探柯南吧。”


信用与诉讼


一般来讲,背调的标准套餐还包括如下内容:金融违规、商业利益冲突、个人信用风险和有限民事诉讼。其中后两个大家尽量规避。


个人信用风险包括:网贷/逾期风险、反欺诈名单和欠税报告。


网贷这块,当时我有一个同事,2021年的时候,拿了4个offer,结果不明不白地都挂在了背调上,弄得他很懵逼。


当他问这三家公司HR原因的时候,HR都告诉他不便透露。


最后,他动用身边人脉,才联系上一家公司的HR出来吃饭,HR跟他说:“以后网贷不要逾期,尤其是不同来源的网贷多次逾期。”


同事听了,这才恍然大悟。


欠税这个,就更别说了,呵呵,大家都懂,千万别心存侥幸。


再说说劳动仲裁和民事诉讼。


现在有些朋友确实法律意识比较强,受到不公正待遇了,第一想法就是“我要去仲裁”,仲裁不满意了,就去打官司。


首先我要说的是,劳动仲裁是查不到的,所以尽量在这一步谈拢解决。


但民事诉讼在网上都是公开的,而且第三方背调公司也是走系统的,一查一个准儿。如果非必要的话,尽量不要跟公司闹到这一步。


如果真遇到垃圾公司或公司里的垃圾人,第一个想法应该是远离,不要让他们往你身上倒垃圾。


尤其是你主动跟公司打官司这种,索要个加班费、年终奖什么的,难免会让新公司产生顾虑,会不会我offer的这名候选人,以后也会有对簿公堂的一天。


结语


现在这大市场行情,求职不易,遇到入职前背调更是如履薄冰,希望大家都能妥善处理好,一定要避免节外生枝的情况发生,不要在距离成功一米的距离倒下。


最后,祝大家工作顺利,纵情向前,人人都能拿到自己满意的offer,开开心心地入职。


作者:库森学长
来源:juejin.cn/post/7295160228879204378
收起阅读 »

迷茫的前端们

web
前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。 最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。 什么情况 首先...
继续阅读 »

前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。


最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。


什么情况


首先,要看看为什么会有这个观点。原因也很简单,自从去年开始,各大公司都开始裁员,然后HC也开始减少。但是因为前几年互联网工资收入高,每年毕业的应届生那是一年比一年多。前端培训班也火热的不行,每年都向社会输送大面积的前端开发。


所以,从供需角度看,前端开发找工作,尤其是刚毕业和刚培训毕业的前端开发,今年找工作就是地狱模式。所以有人说“前端已死”,自然很快就能获得共鸣


另外的声音


然后,有意思的来了。“前端已死”这个话题火了后,各大前端自媒体,前端前辈都出来发声。观点出奇的一致:前端不会死。一下子就把前端已死的观点给压下去了。这种社区声音180度的大转弯也是少见。


到底死没死


说完现象,我说下我的观点。



  1. “前端已死”的声音能获得共鸣,说明前端已经发展到了一个关键的时间点。

  2. 只要还有页面,还有小程序,前端就不会死。

  3. 不会死,并不是说能活的很好。


行业周期


互联网在行业分类中属于第三产业,也就是服务业。服务业的特点就是周期性。如果所有服务业的周期是往下走的,互联网也好不了。三年疫情,大家都知道,服务业已经被折腾的不行了,即使今年放开了,大家还敢开线下店吗?被互联网服务的行业大部分都过的不好,互联网能好吗。


当然,后面随着经济复苏,互联网也会慢慢恢复景气。但是这里还有另外一个点,由于互联网前几年工资都太高了,年年倒挂,导致选计算机专业的人越来越多。以大学四年计算,后面3年每年毕业的计算机专业的学生,还是会持续增加的。


所以呢,从供需角度,技术互联网恢复景气了,互联网招聘未必会恢复景气。

当然了,我觉得,后面几年,不会像今年这么卷,投10份简历,连个面试机会都没有。(预测这种东西,很容易打脸,大家看看就好)


另外,就前端开发而言,我感觉好多东西都很成熟了。最近看技术社区的文章,感觉很久没有看到很新的内容了(也许是推荐系统搞得信息茧房)。大家都在几个领域里面深挖。如果没有新的方向出来,真的很难容纳这么多的求职者。后面看看AI能不能创新出一波职位吧。


所以呢,从互联网行业和前端行业来看,前端已经发展到了一个关键点,后面如果没有新的方向出来,即使互联网恢复景气,求职也不会像以前那么轻松了。

但是,毕竟还有那么多业务需要页面,需要小程序,这块的需求还是在的,所以还是会有前端开发的。


技术周期


从我毕业,基本上都在做互联网,期间其实经历过好几轮技术迭代。比如最早的Flash, 到PHP, PC端的JQuery。你很难在招聘网站上找到要求这些技术的岗位了。

随着前端的发展,技术栈也一直在变化。但是,大家想想,现在流行的React 和 Vue,已经用了多久了。如果还是只会React,Vue,是不是说明已经很久没有成长了。后面会不会有新的技术栈出来,比如VR出来后,是不是还用React开发?

如果还是继续用React,Vue,那么和行业周期里面说的一样,前端不会死,但是不会像以前那么活的好。


新方向在哪里


其实聊了这么多,本质就是一个供需。供需这个角度真的太强大了,能解释很多问题。如果未来没有一个新的大方向出现,那么在岗位需求变化不大的情况下,每年大量学生毕业,也就是供应增多,求职困难是必然的。

那么会不会有新方向出现呢,目前看,有几个可能的方向:



  1. AI。从去年底开始,AI持续火热,目前虽然有退烧,但是AI的趋势已经明确。后面要看的是,AI是不是需要大量的前端工作。这块大家可以说说自己的判断。

  2. VR元宇宙。Meta搞元宇宙,差点把自己搞死,现在苹果也出VR设备,观察下能不能把这个行业带起来。大家说有没有可能。

  3. Web3。去年年初,Web3还很火了的,现在好像,嗯,一般般。后面能不能再次爆发,也要继续观察。大家也可以说说自己的开发。


扯一句


最后扯一句。我看了好多前端前辈对“前端已死”的看法,都说前端不会死,会死的都是初级前端,前端要持续学习,要让自己不可替代。说实话,我觉得这都是屁话。写代码的谁不是从初级开始的呢,现在是完全不给初级的机会,断档了!写代码的人,已经是最爱学习的那一批人了吧,永远在学习新东西。还有让自己不可替代,公司会想办法让你可替代,后面聊一聊不可替代这个话题,我也有很多想说的。


结束


后面会逐步把掌握的前端知识以及职场知识沉淀下来。 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:写代码的浩
来源:juejin.cn/post/7253437782333669434
收起阅读 »

协程-来龙去脉

首先必须声明,此文章是观看油管《KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov》的感想,如何可以,更建议您去观看这个视频而不是阅读本篇文章 代码的异步控制 举个例子,假设你要去论坛...
继续阅读 »

首先必须声明,此文章是观看油管《KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov》的感想,如何可以,更建议您去观看这个视频而不是阅读本篇文章


代码的异步控制


举个例子,假设你要去论坛上发布消息,你必须先获取token以表明你的身份,然后创建一个消息,最后去发送它


//这是一个耗时操作
fun requestToken():Token = {
... //block to wait to receive token
returen token
}

//这也是一个耗时操作
fun creatMessage() : Message ={
... //block to wait to creat Message
returen token
}

fun sendMessage(token :Token,message:Message)

fun main() {
val token = requestToken()
val meassage = creatMessage()
sendMessage(token,message)
}


这种情况显然不符合我们的现实情况,我们不可能在没有拿到token就等待在那里,我们可以开启一个线程去异步执行



fun requestToken():Token = {
... //new thread to request token
returen token
}

像这样一个任务我们就需要创建一个线程,creatMessage与requestToken可以并行进行,因此我们需要再次创建一个线程去执行creatMessage,从而创建两个线程。现在我们的手机性能很很高,我们可以创建一个,两个,甚至是一百个个线程去执行任务,但是到达一千个,一万个呢,恐怕手机的不足以支撑。
怎么解决这样的问题呢。我们只需要建立一种通知机制,在token返回后告诉我们,我们再继续完成creatMessage,进而sendMessage。这也就是callback方式



fun requestTokenCallback(callback : (Token) -> Unit) {
... //block to wait to receive token
callback.invoke(token)
}

fun creatMessageCallback : (Message) -> Unit){
... //nblock to wait to creat Message
callback.invoke(message)
}

fun sendMessage(token :Token,message:Message)

fun main() {
//创建一个线程
Thead {
requestTokenCallback { token ->
creatMessageCallback { message ->
{
sendMessage(token, message)
}
}
}

}

}


这仅仅是一个简单的案例,就产生了如此多的嵌套和连续的右括号,在实际业务中往往更为复杂,比如请求失败或者一些异常情况,甚至是一些特定的业务操作,想想这样叠加下去,简直是灾难。
如何解决这种问题呢,java中有一个CompleteFuture,正如其名,它能够异步处理任务,以期获取未来的结果去处理,我们只需要允诺我们将来在某个时间点一定会返回某种类型的数据,我们就可以以预知未来的方式使用他,


//创建一个线程去异步执行
fun requestToken() :CompletableFuture<Token> = ...

//创建一个线程去异步执行
fun creatMessage(token) : CompletableFuture<Send> = ...

fun sendMessage(send :Send)

fun main() {
requestToken()
.thenCompose{token -> creatMessage(token)}
.thenAccept{send -> sendMessage(send)}
}


令人头疼的代码已不复存在,我们可以自由组合我们任务
creatMessage的调用方式和sendMessage并不相同,kotlin为了统一这两种调用,产生了一个suspend 关键字,它能够像拥有魔法一样,让世界的时间暂停,自己又不会暂停,然后去执行自己的任务,执行完成之后,时间恢复,任务继续执行



suspend fun requestToken() :Token = ...

suspend fun creatMessage() : message = ...


fun sendMessage(token :Token,message:Message)

fun main() {
val token = requestToken()
val meassage = creatMessage()
sendMessage(token,message)
}


执行这段代码的时候,会发生编译错误,因为main函数只是一个普通的函数,并没有被suspend标记,当requestToken使时间暂停的同时,主程序也时间暂停了,那这与最开始的阻塞方法有什么不一样的呢


协程Coroutine


本文要介绍的是协程,协程是什么呢,在我看来,协程就是一个容器,让suspend标记的函数可以运行,可以开启魔法 ,让它在时间暂停的同时,并不影响主线程


public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,//创建容器的上下文
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T//拥有魔法的函数
)
: Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy) //创建协程
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block) //启动魔法开关
return coroutine
}

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext, //创建容器的上下文
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit //拥有魔法的函数
)
: Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy) //创建协程
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block) //启动魔法开关,启动协程
return coroutine
}

我们可以通过Deferred.await() 获取将来的值T。自此,我们有了新的代码



suspend fun requestToken() :Token = ...

suspend fun creatMessage() : message = ...


fun sendMessage(token :Token,message:Message)

fun main() {
val token = async {requestToken()}
val meassage = async{creatMessage()}
sendMessage(token.await() ,message.await() )
}


实际业务中,我们的任务并不是一直都是需要返回结果的,所以还有另一种容器,只需要去执行


public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
)
: Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}


轻量级线程


协程被视为轻量级线程,轻量在哪呢?


实际上,async与launch 并没有什么魔法,只是将封装好的任务交由线程池去执行,所以suspend标记的函数可以任意暂停


协程只是代码层级的概念,操作系统对于此是无感知的,但是线程作为cpu调度的基本单位,创建和调用是很重的,需要中断机制去进行调度,消耗很多额外的资源,所以协程被视为轻量级线程。


上面的代码中launch 与async 都是CoroutineScope 的函数,那么CoroutineScope 是什么呢
这个就是协程运行的温床,也可说是运行的基础,也就是作用域。创建处一个协程,必须有一个管理容器去管理协程的创建,分发,调度,这就是CoroutineScope。


有一种常见的需求是网络执行完成切换UI线程去执行,因此,我们需要在创建容器的时候需要一个参数,去声明协程到底在线程池中执行,UI线程池还是普通线程池,


val scope = CoroutineScope(Dispatchers.IO) //IO线程池去处理
val scope = CoroutineScope(Dispatchers.Main) // UI线程去处理

题外


阅读到这里可以知道suspend实际上就是一个callback封装, 对于其如何将标记函数转化为可挂起恢复的,可浏览# Kotlin Vocabulary | 揭秘协程中的 suspend 修饰符,个人觉得讲的不错


在推荐的这篇文章中,可以看到使用了状态机,其目的是为了节省Continuation对象的创建,可以借鉴学习


关于我


一个希望友友们能提出建议的代码下毒糕手


作者:小黑不黑
来源:juejin.cn/post/7294852698460373004
收起阅读 »

一亿数据大表,我们是如何做分页的

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。 最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功...
继续阅读 »

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。


最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功能进行优化。


对于深分页问题,一般都是采用传递主键ID的做法来解决,也就是每次查询下一页数据时,把上一页数据中最大的ID传递给下一页,这样在查询某一页时就能利用主键索引来快速查询了。


但是由于历史原因,那张一亿数据的大表没有主键ID,-_-,所以这是需要解决的问题之一。


另外,就算能利用主键索引快速查询分页数据,但是毕竟是一亿数据,最终查询比较靠后的页数时,因为数据量大就算走索引最后还是会比较慢,所以就需要分表了。


我们先使用SQL脚本的方式把一亿数据迁移到分表,我们按照时间季度进行分表,比如2022q1、2022q2、2023q1这种方式来进行分表,这样每张分表的数据就不会太多了,大概1000万左右,并且在创建分表时顺便给分表添加主键ID字段,然后在迁移数据时需要按照原始大表的交易时间进行升序排序,保证ID的自增顺序与交易时间顺序是一致的。


不过这种方案存在两个问题,第一个问题就是查询分页时跨分表了怎么办,比如我查第10页数据时,一部分数据在2023q1,一部分数据在2023q2,首先在查询时本身就需要选择年份和季度,所以直接就确定了对应的是哪张分表,查询时根据年份和季度直接定位到分表,然后再进行分页查询,所以不会出现跨表,这个问题就自然而然解决了。


另外,每张表的主键ID要不要连续,比如2022q1的主键是1-1000000,那么2022q1的主键要不要是1000001-2000000,其实我个人觉得需要这么设计,因为两个分表的数据逻辑上其实一张表,所以主键最好不冲突,但是考虑到实现难度,暂时没有采取,而是每张分表的主键ID都是从1开始的,这种方案在目前看来是没有问题的,因为每次都只会从一张分表中查询数据,不会出现跨表的情况。


另外,在设计时也想到过能不能直接用交易时间做为主键来进行分表,但是想到交易时间的精度是秒,所以很有可能出现交易时间相同的记录,这样在做分页时可能会出现下一页数据和上一页数据有重复的,所以还是需要单独设计一个主键ID。


继续思考,假如这张一亿数据的大表要做分页,但是不根据年份和季度做查询来分页,而就是直接分页,那又该如何呢?


首先,肯定还是得利用分表和主键索引,我的思路是,先给原始表添加主键ID并生成自增ID,然后再按主键ID进行分表,分表记录数也可以控制在1000万左右,前端查询时仍然按ID来查分页就可以了,但是此时就存在跨表的问题,比如每页如果是20条,查第10页数据时,如果从分表一只查出了5条,那么后端接口判断5小于20,就尝试从分表二中继续查。


当然了,如果既有复杂的查询条件,又需要进行分页,那用关系型数据库就不太好做了,就可以考虑ES之类的了。


大家有什么更好的经验或思路,欢迎不吝赐教。


我是爱分享技术的大都督周瑜,欢迎关注我的公众号:Hoeller。公众号里有更多高质量干货系列文章和精品面试题。


记得点赞、分享哦!!!


作者:爱读源码的大都督
来源:juejin.cn/post/7294823722807017499
收起阅读 »

分享下最近找工作的行情

国庆前开始陆续随便投了一点公司试了下行情,确实不很乐观,但是当时还没有特别强烈的感觉。 目前这国庆之后再投了几天的感受特别强烈,就是基本凉凉的节奏。 目前感受下来,招人的公司有限,上海这边就字节、PDD、美团、小红书、得物这些在大量招人,在招聘平台脉脉、拉勾、...
继续阅读 »

国庆前开始陆续随便投了一点公司试了下行情,确实不很乐观,但是当时还没有特别强烈的感觉。


目前这国庆之后再投了几天的感受特别强烈,就是基本凉凉的节奏。


目前感受下来,招人的公司有限,上海这边就字节、PDD、美团、小红书、得物这些在大量招人,在招聘平台脉脉、拉勾、BOSS上基本上也是这些有反馈,另外猎头基本上推的也就是得物、字节这些公司,大部分都是得物。


总结下来的情况就是,要么就是一线大厂在招人,要么就是在这个经济不好的情况下业务做得好的、没什么影响的在招人,比如得物了。


朋友问了一圈,一些公司有HC,但是在招聘平台上都是没有声音的。


华为朋友告诉我这个学历只能去OD,985都在干OD。


字节告诉我卷死来,心脏和字节只能有一个跳动。


腾讯有少量HC,但是也是一直在裁员,很不稳定,米哈游也一样。


饿了么还有一些HC,但是也是在一边裁员,一边有些部门有少量HC。


SAP目前没HC,也是裁员。


联想也没有HC,早就没了。


携程和一些其他的小一点的公司,还有一些HC,一线开发,但是好像薪资不是很高的样子,没有细问了。


诸如此类。。。


还有一些小点的公司,去年还在招人,问我要不要过去,今年普遍都在裁员,没有HC了。


还有一些在家办公的在招人,这个你懂的,但是也非常的卷。


大致情况市场就是这样,要么去卷,要么失业的节奏。


想找钱多活少的,目前看起来是非常不现实了,甚至于想找个工作都非常困难。


听说大厂现在都要卡学历、业务、大厂经历,三高匹配至少两项。


总体来看,暂时没有什么好的出路,还在继续看。。。


有好公司欢迎拉我入坑。。。


就这样。


作者:艾小仙
来源:juejin.cn/post/7288540474058457125
收起阅读 »

如何在网页中展示源代码

web
如何在网页中展示你的源代码 如下图所示: 在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码 第一步: 自定义 vite插件 首先需要一个自定义插件用来转换 vue 的自定义块 ...
继续阅读 »

如何在网页中展示你的源代码


如下图所示:


1698137911654.png


在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码


第一步: 自定义 vite插件


首先需要一个自定义插件用来转换 vue 的自定义块


在vite.config.ts文件里


import fs from 'fs'
import {baseParse} from '@vue/compiler-core'
const vueDemoPlugin = {
name: "vue-block-demo",
transform(code, path) {
if (!/vue&type=demo/.test(path)) {
return;
}
const filePath = path.split("?")[0];
//异步读取文件内容,并转为string类型
const file = fs.readFileSync(filePath).toString();
//将读取到的文件中的自定义快渲染为AST
const parsed = baseParse(file).children.find((n) => n.tag === "demo");
//读取自定义模块中的文本内容
const title = parsed.children[0].content;
//将读取文件中的自定义块切分,并转为字符串类型
const main = file.split(parsed.loc.source).join("").trim();
//以JSON数据类型返回
return `export default Comp => {
Comp.__sourceCode = ${JSON.stringify(main)}
Comp.__sourceCodeTitle = ${JSON.stringify(title)}
}`
;
},
};
export default defineConfig({
plugins: [vue(), vueDemoPlugin],
})

第二步:在要展示的源代码文件里面加上一个自定义块


比如我要展示 SwitchDemo01.vue 文件


<template> 加上 <demo> 自定义块,里面内容写上 title


<demo>常规用法</demo>
<template>
<hx-switch v-model="value1" />
<hx-switch
v-model="value2"
class="ml-2"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
/>
</template>

<script lang="ts" setup>
import { HxSwitch } from 'hx-gulu-ui';
import { ref } from 'vue'

const value1 = ref(true)
const value2 = ref(true)
</script>

第三步: 在页面展示文件里面引用


如: showSwitch.vue 文件:


<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre>{{ SwitchDemo01.__sourceCode }}</pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

// __sourceCode 这里面是源文件去除了 <demo> 外的所有代码
console.log('SwitchDemo01', SwitchDemo01.__sourceCode)

// __sourceCodeTitle 这里面是 <demo>常规用法</demo> 里面的文字
console.log('SwitchDemo01', SwitchDemo01.__sourceCodeTitle)

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

此时,已经可以在代码上显示源文件代码了,但是代码没有任何样式,很丑怎么办呢?


第四步: 引入 prismjs




  • prismjs 是代码主题的插件



    • 官网

    • 安装: npm i prismjs




  • 调用


    import 引入 好像有问题,只支持 require('prismjs'),同时在window属性下 添加了 Prismjs属性,大家可以自己试一下


    <script setup lang='ts'>
    import 'prismjs'
    import 'prismjs/themes/prism-okaidia.min.css'
    const Prism = (window as any).Prism

    const code = `var data = 1;`;
    const html = Prism.highlight(code, Prism.languages.javascript, 'javascript');
    </script>

    示例:




<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre class="language-html" v-html="html"></pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

import 'prismjs'
import 'prismjs/themes/prism-okaidia.min.css'
const Prism = (window as any).Prism

const html = computed(() => {
return Prism.highlight(SwitchDemo01.__sourceCode, Prism.languages.html, 'html')
})

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

最终效果如下图所示:


1698140953360.png


作者:Blink46
来源:juejin.cn/post/7293348981664399397
收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。


我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。


和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”


这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。


现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。


再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?


知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)


第一,认死理。


和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)


常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。


例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。


比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。


如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。


第二,喜欢拿技术套市场。


​这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。


举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。


可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。


第三,不擅长合作。


为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。


他们会搞钱。


他们会搞钱,是​因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。


大部分人,在创业路上直接卡死在这条路线上了。


投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。


那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。



--- 


我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。


只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。


最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身体,活下来才有输出。


作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
收起阅读 »

使用小程序中的 observe 实现数据监听

web
小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。 什么是 observe? observe 是小程序中的一个方法...
继续阅读 »

小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。


什么是 observe


observe 是小程序中的一个方法,用于监听数据的变化并触发相应的回调函数。它可以用于监听页面数据、组件数据以及其他数据对象。


如何使用 observe


监听页面数据


在页面的 .js 文件中,可以使用 Page 函数中的 data 中的 observe 字段来监听数据的变化。以下是一个示例:


// pages/index/index.js
Page({
data: {
count: 0,
},

// 监听 count 数据的变化
observe: {
'count': function (newVal, oldVal) {
console.log('count 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},

// 增加 count 值的函数
increaseCount() {
this.setData({
count: this.data.count + 1,
});
},
});

在上述示例中,我们在 observe 字段中定义了一个监听器,当 count 数据发生变化时,会触发相应的回调函数。


监听组件数据


在小程序的组件中,也可以使用 observe 来监听组件数据的变化。以下是一个示例:


// components/my-component/my-component.js
Component({
data: {
message: 'Hello, World!',
},

methods: {
changeMessage() {
this.setData({
message: 'New Message!',
});
},
},

// 监听 message 数据的变化
observe: {
'message': function (newVal, oldVal) {
console.log('message 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},
});

在组件的 observe 字段中同样定义了一个监听器,用于监听 message 数据的变化。


常见应用场景


1. 数据绑定


observe 可以用于在数据变化时自动更新视图,实现数据绑定。这对于构建响应式的页面和组件非常有用。


2. 数据校验


通过监听数据的变化,可以在数据变化时进行校验,确保数据的合法性。例如,监听输入框中的文本变化并验证其格式。


3. 事件通知


当某个数据变化时,可以通过 observe 触发相关的事件,通知其他部分的代码执行相应的操作。


4. 数据持久化


在某些情况下,需要将数据持久化到本地存储或服务器,可以在数据变化时触发数据保存操作。


注意事项和最佳实践


在使用 observe 进行数据监听时,有一些注意事项和最佳实践需要考虑:




  1. 数据引用类型的监听:当监听对象是引用类型(例如对象或数组)时,需要注意对象的引用是否发生变化。observe 监听的是对象的引用,而不是对象内部属性的变化。如果需要监听对象内部属性的变化,可以使用深度监听或手动触发。




  2. 避免过多监听器:不要过度使用 observe,因为过多的监听器可能会导致性能问题。只监听那些真正需要监控的数据。




  3. 监听器的性能开销:监听器的回调函数在数据变化时会被频繁调用,因此要确保回调函数的执行效率较高,以避免影响应用的性能。




  4. 避免循环引用:在监听器回调函数中不要再次修改被监听的数据,以防止循环引用和无限循环触发监听器。




  5. 生命周期管理:在页面或组件销毁时,要记得取消监听以防止内存泄漏。可以在 onUnload 生命周期中取消监听。




onUnload() {
this.setData({
observe: null, // 取消监听
});
}

通过谨慎使用 observe,你可以实现有效的数据监听和响应,提高小程序应用的可维护性和性能。


结语


observe 是小程序中非常有用的功能,它允许你监听数据的变化并执行相应的操作。无论是在页面中还是在组件中,都可以使用 observe 来实现数据的监听和响应。通过合理利用 observe,你可以构建更加动态和交互性的小程序应用。


作者:依旧_99
来源:juejin.cn/post/7295237661618438196
收起阅读 »

这个故事有点长 - 苏州

这个故事有点长 - 苏州 👉故事的开始 之前已经去过苏州了,只是当时觉得园林都一样,就只去了拙耕园、留园、山塘街、观前街、平江路、平门古城墙、金鸡湖,印象最深刻的就是,当初围绕着金鸡湖走了一半,累了打车回去了。 最近心情有点郁闷😠,决定去走走,思来想去,就发...
继续阅读 »

这个故事有点长 - 苏州



👉故事的开始


之前已经去过苏州了,只是当时觉得园林都一样,就只去了拙耕园留园山塘街观前街平江路平门古城墙金鸡湖,印象最深刻的就是,当初围绕着金鸡湖走了一半,累了打车回去了。


最近心情有点郁闷😠,决定去走走,思来想去,就发现苏州还有地方没去,于是乎就去了,提前一天定了酒店,住的花居酒店,挺不错的,停车位很多,免费停车,其他酒店都是40一天,由于姑苏区车辆限行,就在姑苏通上申请了,结果都回来了,申请才通过。


🚗第一天


由于没有那么远的距离,九点才从上海出发,大概十一点左右到的酒店,然后再楼下简单吃了顿饭,就驱车去了虎丘,在停车场还有人指导我倒车入库,然后就一直推荐旅游套餐,说虎丘+XXX好几个地方只需100元,车接车送,很划算,不能单买虎丘的,于是乎果断在美团上买了70的门票。







大概逛了三四个小时,期间卖了一块网红雪糕,宝塔形状的。五点左右就回去了,休息了一个小时,然后就打车去金鸡湖音乐喷泉,是个妹子开的车,然后在车上聊了起来,期间还为我表演了一次丝滑插车,下车时还让我给她了一个好评。七点半开始,我六点半左右就到了,结果还是没有抢到最佳观看位置,都太卷了,人很多,与外滩有一拼。




在等待期间,认真观察了苏州的“大裤衩”,期间总共变了三种颜色,期初是文字,后来变成了图案。





从金鸡湖回来, 坐地铁一号线去了观前街,第二次来了,故地重游,感觉就是不一样,又拍了两张照片😄,还是那么的热闹 。




🌅第二天


一觉睡到了十点,吃了早饭去狮子林,搜了一下,距离只有一公里多一些,果断又打车了,门票40元,中间蹭了导游的一些讲解,又看到了九狮台,能看的出来是九头狮子吗?



接下来就是假山群,里面有很多山洞,据说女儿国就在这里拍的,当初乾隆皇帝就在这里完了很久,直呼真有趣,于是就有了真趣亭




之后在附近吃了松鼠桂鱼,然后就回去了,一路都在与天气赛跑,一段下雨一段晴。




🚩起点既是终点


时间过得好快,兜兜转转了一圈,从哪来又回了哪去,晚安🌜🌛。



这个故事有点长 - 故事还在继续🏃🏃🏃.......



作者:小小愿望
来源:juejin.cn/post/7294130755840540735
收起阅读 »

用canvas画出一片星空

web
前言 由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理Canvas。Canvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。 Canvas 介绍 首先我们来介绍介...
继续阅读 »

前言


由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理CanvasCanvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。


Canvas 介绍


首先我们来介绍介绍CanvansCanvasHTML5提供的一个绘图API,它允许通过JavaScriptHTML元素创建和操作图形。Canvas提供的是一个矩形画布,我们可以在上面绘制各种图形、动画与交互效果。


功能


上面了解了Canvas是什么之后, 我们再来和大家展开说说Canvas 一些主要功能:



  1. 画布:Canvas提供了一个矩形的画布区域, 我们可以通过HTML中的<canvas>标签来创建并通过设置画布的宽高来确定绘图区域的大小。

  2. 绘画API:Canvas提供了丰富的绘图API,包括绘制路径、直线、曲线、矩形、圆形、文本等。 我们使用这些API来创建各种图形,并自定义样式、颜色、透明度等属性。

  3. 动画:Canvas可以与JavaScript的动画函数结合使用,实现动态的图形效果。通过在每一帧中更新画布上的内容,能创建平滑的动画效果。

  4. 图像处理:Canvas可以加载和绘制图像。我们可以使用Canvas的API对图像进行裁剪、缩放、旋转等操作,从而实现图像处理的功能。

  5. 事件处理:Canvas支持鼠标和触摸事件的处理,我们可以通过监听这些事件来实现交互效果,例如点击、拖拽、缩放等等。


注意:Canvas 绘制的内容是即时生成的,它不会被浏览器缓存,所以每次页面加载Canvas都需要重新绘制,所以我们在使用时需要考虑性能问题。


星空


在介绍完Canvas之后,我们再来用CanvasJS结合,实现一片星空的效果。


第一步,我们先创建好html 结构,代码如下:


  <div class="landscape">
</div>
<canvas id="canvas"></canvas>
<div class="filter"></div>
<script src="./round_item.js"></script>

现在是没有效果的,我们在给它加上css代码,将其美化,同时我们再在css上加一些动画效果,完整代码如下:


* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}

body {
background: linear-gradient(to bottom, #000 0%, #5788fe 100%);

}

.landscape {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(./star/xkbg.png);
background-repeat: repeat-x;
background-size: 1000px 250px;
background-position: center bottom;
}
.filter{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background: #fa7575;
animation: colorChange 30s ease-in infinite;
}
/* 渐变动画*/
@keyframes colorChange {
0%,100%{
opacity: 0;
}
50%{
opacity: 0.7;
}

}

这时候,我们看到的效果是这样的:


1698327282484.gif


这效果好像只有黄昏的颜色变化,少了星空。那这最后一步,就该我们Canvas上场了,我们要用Canvas画出来一片星空,并配合css的颜色变化,实现一个夜晚到清晨的感觉。


我们 js 代码这样写:


//创建星星的函数
function RoundItem(index, x, y, ctx) {
this.index = index
this.x = x
this.y = y
this.ctx = ctx
this.r = Math.random() * 2 + 1
this.color = 'rgba(255,255,255,1)'
}
// 绘制
RoundItem.prototype.draw = function () {
this.ctx.fillStyle = this.color //指定颜色
this.ctx.beginPath() // 开始绘制
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false) // 绘制圆形
this.ctx.closePath() // 结束绘制
this.ctx.fill() //填充形状
}
//移动
RoundItem.prototype.move = function () {
this.y -= 0.5
this.draw()
}


let canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
round = [],
initRoundPopulation = 200; //星星的个数

const WIDTH = document.documentElement.clientWidth,
HEIGHT = document.documentElement.clientHeight;

canvas.width = WIDTH
canvas.height = HEIGHT

init()
// setInterval(animate, 1700)
animate()
function init() {
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new RoundItem(i, Math.random() * WIDTH, Math.random() * HEIGHT, ctx)
round[i].draw()
}
}
function animate() {
ctx.clearRect(0, 0, WIDTH, HEIGHT) //清除画布
for (let star of round) {
star.move()
}
requestAnimationFrame(animate) //通过刷新帧数来调用函数
}


将上述代码添加上之后,我们也就完成星星的绘制并添加到了页面上,最终的效果如下:
1698327645625.gif


到这里,我们就用canvas 画出了一片美丽的星空。同时我们也看到了canvas的强大功能,感兴趣的小伙伴可以深入的了解它更多的用法哦。


作者:潘小七
来源:juejin.cn/post/7294103091019612212
收起阅读 »

怎么办,代码发布完出问题了

作者:蒋静 前言 回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先...
继续阅读 »

作者:蒋静



前言


回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先解决线上问题。


构建部署之“痛”


我的几段公司的工作经历:



  1. 第一段经历,是在一个传统的公司,没有运维,要我们自己登录一个跳板机,把文件部署到服务器,非常麻烦。

  2. 第二段经历,是在一个初创公司,基建几乎没有,前端的规模也很小,发布就是打个 zip 包发给运维,运维去上线。但是久而久之,运维也就不耐烦了。

  3. 后来去了稍微大些的公司,构建、部署有一套比较完善的体系,在网页上点击按钮就可以了。


那么构建部署是如何实现的呢?下面我要来介绍古茗的部署和回滚代码机制。


发布分析


我们的最终目的是发布上线,我们发布的是什么呢?是一条分支,所以我们需要先创建一条分支(更加规范的步骤应该是:基于某个需求和某个应用去拉一条分支)。在分支上开发完我们就可以进行发布的操作啦!


这个时候我们就可以操作发布,我们填写需要的配置项后就可以点击发布按钮了。但是肯定不能让所有人随随便便就发布成功,所以我们要进行一些前置校验。比如说你有没有发布的权限、代码有没有冲突、是不是节假日或非发布窗口期、这个应用有没有正在被发布。。。等等的校验,总之就是确保代码是可以被你发布的。


然后我们的发布平台就会叫 Jenkins 拿着仓库信息、分支信息,以及其他等等的配置信息去仓库拉取代码了,拉到代码之后根据不同类型的应用进行区分,进行编译打包(这个过程不同应用之间是不同的),生成对应的产物。


1. 容器化发布


image.png



注:图中Wukong是我们自研DevOps平台



容器化发布发布的是镜像,镜像 id 代表了这次发布和这个镜像的关联关系。回滚的时候只需要找到这次发布对应的 id ,运维脚本根据这个 docker 的 id 找到 docker 镜像,直接部署这次 docker 镜像,做到回滚。由于发布的是 docker 的镜像,不仅可以保证产物是相同的,发布还很快。


容器化之前的发布:先找到对应的发布,根据这次发布找到对应的 tag,然后打包发布,但是这样只能保证业务代码是相同的,不能保证机器环境、打包机的环境、依赖的版本、打包的产物等等是一样的,并且需要的时间比容器化的方式慢得多。


2. oss发布


image.png


oss 发布和容器化发布流程的区别在于不用打包镜像而是将js、css等资源传到了 oss。通过 oss 发布的应用,只需要记住版本和 oss 上面资源路径的对应关系就可以了。


例如在我们这里的实现是:每次发布完成之后会记下有 hash 的 manifest 的地址,点击回滚后会根据发布 id 找到当次的产物,通过 oss 将 manifest 内容替换为有hash 的,从而就切换了访问的资源(html 的 manifest 地址不变,改变的是 manifest 文件的内容)。


3. 小程序


image.png
钉钉小程序的回滚就比较简单了,一般在我们点击回滚之后,内部会通过 http 接口调用小程序的 api 传递需要回滚的版本好后即回滚完成。或者你也可以选择手动到开发者后台的历史版本点回滚。
例如: open.dingtalk.com/document/or…


未来展望


有了完善的部署回滚机制,我们的产研团队才能有更好的交付体验。工作中的业务价值在我们整个交付内容占比应当是比较高的,而不应当把大量的时间花费在处理部署等流程上,让我们能够更快的去完成业务交付。


更好更稳定的回滚方式,能够让我们做到出现问题时快速恢复。这样才能保证一个较低的试错成本。


对于古茗来说,我认为一个很大的优势是,我们的规模不算很大,可以更好地做好研发流程对应的工具服务的统一,打通研发流程的各个流程,每个环节之间更好地进行串联,更好的助力业务发展。


作者:古茗前端团队
来源:juejin.cn/post/7295160228878106650
收起阅读 »

2D的雪碧图已经够炫了,那么3D的呢?

web
前言 前2篇文章,分别介绍了dat.gui和纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷! 这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/...
继续阅读 »

output-16_6_11.gif


前言


前2篇文章,分别介绍了dat.gui纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷!


这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/jsm/libs/tween.module.js,使用起来也是比较简单。


初始化


老样子,场景、相机、渲染器三要素初始化,并导入需要的插件库,插件库都已在three中内置:


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;

// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);

// 场景
scene = new THREE.Scene();

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
}

// 渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera)
}

CSS3DSprite创建521个水球


// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
})
image.src = './static/img/sprite.png';


上面的代码中,我们创建了img标签,并使用CSS3DSpriteHTML元素转化为threejs的CSS3精灵模型,类似与转换成了three中的网格,并随机分布在-2000,2000的立方体中。


看下效果:


three04-1.jpg


添加控制器


关于控制器,前面也已经介绍过啦,通过控制器,我们就可以改变相机的位置,观察不同角度的物体。


// 定义控制器
let controls;
controls = new TrackballControls( camera, renderer.domElement );

// 渲染
function animate() {
...
controls.update();
...
}

注意哦,controls.update需要防止在animate中,每帧执行。


有了控制器。我们就可以实现交互啦:


output-15_37_12.gif


让小球按规律放大缩小


让小球按照正弦时间,放大缩小,即有一种闪烁的效果:


const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {
const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);
}

output-15_37_59.gif


让小球生成特定图形


生成矩形,对应的每个小球坐标:


const amount = 8;
const separationCube = 150;
const offset = ( ( amount - 1 ) * separationCube ) / 2;

for ( let i = 0; i < particlesTotal; i ++ ) {

const x = ( i % amount ) * separationCube;
const y = Math.floor( ( i / amount ) % amount ) * separationCube;
const z = Math.floor( i / ( amount * amount ) ) * separationCube;

positions.push( x - offset, y - offset, z - offset );

}

tween.js使用


const position = {x: 0,y: 0};
;//创建一段tween动画
const tween = new TWEEN.Tween(position)
//经过2秒,position对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start();
// 动画效果 类似annimation
tween.easing()
// 完成时执行的钩子,里面可以继续执行下一个操作
tween.onComplete()

使杂乱的小球变成矩形


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;

for (let i = 0; i < particlesTotal; i++) {

const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;

positions.push(x - offset, y - offset, z - offset);

}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

controls = new TrackballControls(camera, renderer.domElement);

}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

杂乱的小球变成多种形态完整代码


限制文件大小啦,没法完全展示,大家自行运行看看吧!


output-15_49_5.gif


<div id="container"></div>
<script type="module">
import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// Plane
const amountX = 16;
const amountZ = 32;
const separationPlane = 150;
const offsetX = ((amountX - 1) * separationPlane) / 2;
const offsetZ = ((amountZ - 1) * separationPlane) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amountX) * separationPlane;
const z = Math.floor(i / amountX) * separationPlane;
const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200;
positions.push(x - offsetX, y, z - offsetZ);
}

// Cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;
positions.push(x - offset, y - offset, z - offset);
}

// Random
for (let i = 0; i < particlesTotal; i++) {
positions.push(
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000
);
}

// Sphere
const radius = 750;
for (let i = 0; i < particlesTotal; i++) {
const phi = Math.acos(- 1 + (2 * i) / particlesTotal);
const theta = Math.sqrt(particlesTotal * Math.PI) * phi;
positions.push(
radius * Math.cos(theta) * Math.sin(phi),
radius * Math.sin(theta) * Math.sin(phi),
radius * Math.cos(phi)
);
}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
controls = new TrackballControls(camera, renderer.domElement);
}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

</script>

作者:八月十八
来源:juejin.cn/post/7294301361835147290
收起阅读 »

你眼中的失业潮是怎样的?

逛 V 站刷到一个北漂的码农,他的生存指南直接让我破防,但也不得不佩服他的生存能力和心态。 先介绍下这位老哥的情况,普通本科非科班,9 年 IOS 开发经验,其中 1 年 RN,一年 flutter。北漂,名下无车,老婆和孩子都在北京,目前孩子在读幼儿园。 上...
继续阅读 »

图片


逛 V 站刷到一个北漂的码农,他的生存指南直接让我破防,但也不得不佩服他的生存能力和心态。


先介绍下这位老哥的情况,普通本科非科班,9 年 IOS 开发经验,其中 1 年 RN,一年 flutter。北漂,名下无车,老婆和孩子都在北京,目前孩子在读幼儿园。


上半年已经被优化裁员了,更新简历重新找工作,整体情况就是一周打招呼几十次,只成功投了 3 份简历,0 次面试。


实在不敢想象几个月没工作心态会崩成啥样。虽然没有做好心理准备,但也没有别的办法了。


社保代缴违法了也断缴了,每个月剩下 3k 左右,够孩子上两个月的幼儿园。


失业一段时间之后,注册了滴滴代驾,等待审核通过之后打算买一个折叠自行车。


发现北京的滴滴代驾已经没有名额了,只能等缺人的时候才能继续走下一步操作。


图片


然后注册了美团众包,新手第一天还得培训,培训完才能接单。


之后便开始做起了外卖员,第一次跑从早上 5 点到 7 点 50 分,跑了 6 单 ,15km 的路程,一共赚了 44 元,如果扣除保险的就是 41 元,时薪则是 16.4 元。


由于刚开始,并不累,只是觉得有点意思,目前心态还算好。有一个好处就是,不会觉得挣钱很容易了,坐办公室一天就是几百上千。


之所以会优先选择代驾和外卖的原因,是因为今年的行情的确很糟糕,找了几个月的工作还没找到,感觉这次的找工作周期并不短。


另一个原因是时间比较自由,不会影响我学习和面试。开发这行我也比较热爱,不愿意丢弃这份工作。


图片


并不是说找工作期间完全找不到工作。而是找的工作降薪太多,从企业的角度来说不稳定的因素也有。自己也不甘心,一旦降下去很难说下次按上次的高薪去涨。


由于北京代驾排队人数较多,暂时没有开放名额,所以入职了美团和小鸟。


从 5 月份 12 号开始跑,一开始基本上早上 5 点到 7 点 20 这段时间,然后回家接孩子上幼儿园。


只跑早上,是因为白天还得学习,后来发现没一个面试,索性白天时间段也开始跑了。


5 月份跑单统计下来,美团 275 单,一共 1900 元,扣了 180 元,有一次上午的单子已经餐损了,结果下午又流转到我这新手的头上了,由于没有经验不会操作导致餐损和违规算到我头上了,反馈给客服也不顶用。


蜂鸟 44 单,一共 280 元,扣除 20 元的保险和超时扣费。


每小时基本 20 元左右,叠加活动奖励差不多能到 26.7 元一小时,基本上 1元/km。这个 km 不止是单纯商家到顾客的距离,还要加上骑手接单时的位置到商家的距离。


所用的工具就是一辆最高 25km/h 的国标电动车。所以同时最多挂3单,那些车速快的我看到能接 9 单。没解速是因为觉得外卖只是暂时的,解速以后带孩子骑行就不安全了。


跑外卖整体的收获就是,觉得挣钱不容易,远不如之前坐办公室一天就好几百。甚至觉得跑外卖的时候还期盼下雨,因为会有天气补贴,1 单有 0.5-1 元的补贴,自己想想都觉得心酸。


但也没有那么焦虑了,虽然不够房租和日常开销,但好歹能够也算有收入。其实内心也是有点焦虑,不过在家里就是不能表现得太明显。


我比较喜欢开车,所以还是会去跑代驾。由于滴滴排队太慢了,到现在还在排队中,就先加入了别的代驾。


做代驾前,我做了一些功课。在抖音上搜各种档式,包括鸡腿挡、怀挡、旋钮挡等。还有各种车的启动方式,比如宝马、奔驰、路虎、特斯拉等。


心理建设还是有的,毕竟是开别人的车,怕刮蹭出事故。自己第一次上线接单前足足鼓励了半小时。


我没有买电动车和自行车,靠地铁和共享单车。因此接单我都跑市里,从 2 环到 4 环,跑偏僻地方的概率很小,不至于给我干到廊坊回不来。


刚开始的时候  14 点就出门,但是基本上 19 点开始才可能有单,再加上后来回家晚,第二天可能要补觉,所以基本上就是晚上 20 点到 凌晨。


图片


且家里住的比较远,基本上到家都在凌晨1点半以上,最晚曾到凌晨4点。


整体收入,没接过 200 元以上得大单,所以时薪在 0-120 元左右。由于单量不稳定,有时候要等个把小时才来单,现在每天差不多几十到 100元 不等。


有一次接了一个 300+ 的大单,预计开车 1 小时多点,但是被取消了,有点心痛!


与外卖相比大概率好点,除非代驾没单或者外卖有活动。


近两个月找工作情况,一共3个面试,其中2 个内推,1 个网投。一个非大厂,终面挂,2 个大厂,分别一面挂和二面挂。


目前已经降薪 30% 在找了,但是还没有找到,笑哭。


同时也在继续开发自己的 APP,由于苹果账号到期了所以现在还没搜到。目前正在做得内容有 商品码识别、小组件和内购,希望掌握技能的同时增加收入。


看到这里,我默默的问一句,大家眼中的失业潮是怎样的?


看了不少的消息,基本上有一个共同的意识就是,环境不太好,都在挣扎做一些副业之类挣额外的收入来维持。


另外代驾和外卖数量逐渐开始到达红线的消息传来,也不是空穴来潮。


我所在的开发团队,也都有一个共同意识,就是做好留一手的准备,该考驾-照的考驾-照,该练体力的练体力,该做其他准备的做其他准备。


公司个月都在汇报信息化建设程度,我们疯狂给他们开发系统,开发工具,省人省力省成本。


做到了一定程度竟然听到在清算人力,听到之后不禁打了个寒颤,至少我也是帮凶了吧。


文中主人公 V 站地址👉:http://www.v2ex.com/t/956789


作者:桑小榆呀
来源:juejin.cn/post/7293804880707059750
收起阅读 »

重复请求优化

web
设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:...
继续阅读 »

设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:两个组件必须有一个共同的祖先组件,如果这两个组件是同级的兄弟组件倒也还好,如果非同级,那么数据的传参就会有些麻烦了。那么还有其他办法吗?当然是有的。


我们可以换个思路,每个组件还是保持原有的业务逻辑不变,从请求接口处做文章。既然是同一个接口调用了两次,而且还是返回了相同的请求结果,那么不妨在第一次时调用成功时,就把请求结果缓存起来,等到第二次再调用时,直接返回缓存的数据。按照这个思路可以写出第一版的代码(这里用了 TS 方便查看参数的类型):


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* @param request 请求函数
*/

function cacheRequest<T>(request: (...args: any[]) => Promise<T>) {
const cache = Symbol("cache")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}


  • 首先 cacheRequest 函数,需要接收一个参数 requestrequest 是一个返回结果为 Promise 对象的函数。cacheRequest 执行完后返回一个新的匿名函数。

  • 然后,在匿名函数的内部,先判断 request 的原型对象上是否有 cache(这里的 cache 使用了 Symbol 类型,确保键名唯一)。也即,是否有缓存过的请求结果,如果没有,说明是第一次调用,则将 request 的执行结果存到缓存里。如果有缓存,则直接返回缓存。

  • 可以看到,缓存也是一个 Promise 类型。在同时调用多次请求时,只要在第一次调用执行后,已经把 Promise 存到缓存里了,后续的请求返回的也是缓存里的 Promise,从而保证多个请求都指向同一个 Promise ,也即只会调用一次 API 接口。

  • 这里需要注意一点,由于需要往 request 的原型对象上挂载缓存,所以 request 不能是箭头函数。因为箭头函数没有 this,也就意味着没有原型对象。


小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
return function (...args) {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
newRequest()

version1.png


可以看到虽然 newRequest 调用了三次,但是 fetch request 只打印了一次,也就是说 request 只调用了一次,符合预期!但是,最后一次 newRequest 的调用,是在 3 秒后调用的,也是走的缓存,没有重新执行。仔细思考一下,后续无论什么时候调用 newRequest 都会使用缓存里的数据,不会重新调用请求了,这显然是不合理的。我们还需要加个缓存的过期时间,超过这个时间,就重新发起新的请求。第二版如下:


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* 保证在 cacheTime 时间间隔内的多次请求,只会调用一次
* @param request 请求函数
* @param cacheTime 最大缓存时间(单位毫秒)
*/

export function cacheRequest<T>(request: (...args: any[]) => Promise<T>, cacheTime = 1000) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}


  • 首先,cacheRequest 新增一个入参 cacheTime,用于设置过期时间,默认为 1 秒。

  • 其次,在原型对象上新增了一个 lastTime 属性,用来记录最后一次调用的时间。

  • 当缓存为空,或者当前时间距离上一次调用时间超过缓存过期时间时,更新 cachelastTime


再来小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args) {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
setTimeout(newRequest, 3000)

version2.png


这一次,fetch request 打印了两次,符合预期,完美!


作者:showlotus
来源:juejin.cn/post/7294597695478333476
收起阅读 »

接手了一个外包开发的项目,我感觉我的头快要裂开了~

嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,...
继续阅读 »

嗨,大家好,我是飘渺。


最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,在这段时间里,我受到了来自"外包"和"微服务"这双重debuff的折磨。


image-20231016162237399


今天,我想和大家分享一下我在这几天中遇到的问题。希望这几个问题能引起大家的共鸣,以便在未来的微服务开发中避免再次陷入相似的困境。


1、服务模块拆分不合理


绝大部分网上的微服务开源框架都是基于后台管理进行模块拆分的。然而在实际业务开发中,应该以领域建模为基础来划分子服务。


目前的服务拆分方式往往是按照团队或功能来拆分,这种不合理的拆分方式导致了服务调用的混乱,同时增加了分布式事务的风险。


2、微服务拆分后数据库并没拆分


所有服务都共用同一个数据库,这在物理层面无法对数据进行隔离,也导致一些团队为了赶进度,直接读取其他服务的数据表。


这里不禁要问:如果不拆分数据库,那拆分微服务还有何意义?


3、功能复制,不是双倍快乐


在项目中存在一个基础设施模块,其中包括文件上传、数据字典、日志等基础功能。然而,文件上传功能居然在其他模块中重复实现了一遍。就像这样:


image-20231017185809403


4、到处都是无用组件堆彻


在项目的基础模块中,自定义了许多公共的Starter,并且这些组件在各个微服务中被全都引入。比如第三方登录组件、微信支付组件、不明所以的流程引擎组件、验证码组件等等……


image.png


拜托,我们已经有自己的SSO登录,不需要微信支付,还有自己的流程引擎。那些根本用不到的东西,干嘛要引入呢?


5、明显的错误没人解决


这个问题是由上面的问题所导致的,由于引入了一个根本不需要的消息中间件,项目运行时不断出现如下所示的连接异常。


image-20231013223714103


项目开发了这么久,出错了这么久,居然没有一个人去解决,真的让人不得不佩服他们的忍受力。


6、配置文件一团乱麻


你看到服务中这一堆配置文件,是不是心里咯噔了一下?


image-20231017190214587


或许有人会说:"没什么问题呀,按照不同环境划分不同的配置文件”。可是在微服务架构下,已经有了配置中心,为什么还要这么做呢?这不是画蛇添足吗?


7、乱用配置中心


项目一开始就明确要使用Apollo配置中心,一个微服务对应一个appid,appid一般与application.name一致。


但实际上,多个服务却使用了相同的appid,多个服务的配置文件还塞在了同一个appid下。


更让人费解的是,有些微服务又不使用配置中心。


8、Nacos注册中心混乱


由于项目有众多参与的团队,为了联调代码,开发人员在启动服务时不得不修改配置文件中Nacos的spring.cloud.nacos.discovery.group属性,同时需要启动所有相关服务。


这导致了两个问题:一是某个用户提交了自己的配置文件,导致其他人的服务注册到了别的group,影响他人的联调;二是Nacos注册中心会存在一大堆不同的Gr0up,查找服务变得相当麻烦。


其实要解决这个问题只需要重写一下网关的负载均衡策略,让流量调度到指定的服务即可。据我所知,他们使用的开源框架应该支持这个功能,只是他们不知道怎么使用。


9、接口协议混乱


使用的开源脚手架支持Dubbo协议和OpenFeign调用,然而在我们的项目中并不会使用Dubbo协议,微服务之间只使用OpenFeign进行调用。然而,在对外提供接口时,却暴露了一堆支持Dubbo协议的接口。


10、部署方式混乱


项目部署到Kubernetes云环境,一般来说,服务部署到云上的内部服务应该使用ClusterIP的方式进行部署,只有网关服务需要对外访问,网关可以通过NodePort或Ingress进行访问。


这样做可以避免其他人或服务绕过网关直接访问后端微服务。


然而,他们的部署方式是所有服务都开启了NodePort访问,然后在云主机上还要部署一套Nginx来反向代理网关服务的NodePort端口。


image-20231016162150035


结语


网络上涌现着众多微服务开源脚手架,它们吸引用户的方式是将各种功能一股脑地集成进去。然而,它们往往只是告诉你“如何集成”却忽略了“为什么要集成”。


尽管这些开源项目能够在学习微服务方面事半功倍,但在实际微服务项目中,我们不能盲目照搬,而应该根据项目的实际情况来有选择地裁剪或扩展功能。这样,我们才能更好地应对项目的需求,避免陷入不必要的复杂性,从而更加成功地实施微服务架构。


最后,这个开源项目你们认识吗?


image-20231017190633190



关注公众号 Java日知录 获取更多精彩文章



作者:飘渺Jam
来源:juejin.cn/post/7291480666087964732
收起阅读 »

某月薪5万的朋友关于处理BUG的心得

引言 大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。 今天我想和大家分享一个有趣的故事。这个故事的主角是我的一个朋友,他是一位资深的软件工程师,月薪高达5万。你可能会觉得这个收入水平的人应该过着无忧无虑的生活,但是他告诉我,他的工作压力非常大,尤其是...
继续阅读 »

引言


大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。


今天我想和大家分享一个有趣的故事。这个故事的主角是我的一个朋友,他是一位资深的软件工程师,月薪高达5万。你可能会觉得这个收入水平的人应该过着无忧无虑的生活,但是他告诉我,他的工作压力非常大,尤其是在处理BUG的时候。


BUG是什么


手下留情,我不是你们要找的BUG


BUG,这个词在软件开发中是非常常见的。它是计算机程序中的一个错误或问题,可能会导致程序无法正常运行。对于软件工程师来说,找到并修复这些BUG是一项非常重要的任务


心得


高手支招


这位朋友告诉我,他在处理BUG的过程中,有以下几点心得





  1. 耐心:找到并修复BUG需要非常强的耐心。有时候,你可能需要花费几个小时,甚至几天的时间来追踪一个BUG的来源。这需要你有足够的耐心去面对这个问题。




  2. 细心:在处理BUG的过程中,你需要非常细心。因为BUG可能隐藏在程序的任何一个地方,只有通过仔细的检查,才能找到它。




  3. 创新:有时候,传统的解决方法可能无法解决某个BUG。这时候,你需要有创新的思维,去寻找新的解决方案。




  4. 团队协作:处理BUG并不是一个人的事情,而是需要整个团队的协作。每个人都有自己的专长,只有通过团队的协作,才能更有效地解决问题。




  5. 不断学习:技术是不断发展的,新的编程语言、新的开发工具、新的开发方法会不断出现。作为软件工程师,你需要不断学习,才能跟上技术的发展。





我这位朋友的心得让我深受启发。他告诉我,虽然处理BUG的过程可能会很艰难,但是当你成功解决一个问题的时候,那种成就感是无法用言语表达的


我的看法


patience is key in life


首先,让我们来谈谈耐心。在游戏开发中,找出BUG的来源是一项非常复杂的任务。你可能需要查看大量的代码,甚至需要反复测试多次,才能找到问题的根源。这个过程可能会非常耗时,但是没有耐心的话,你可能会错过一些重要的线索。因此,耐心是处理BUG的第一步


细心


其次细心也是非常重要的。在编写代码的过程中,程序员可能会犯一些错误,这些错误可能会导致程序无法正常运行。这些错误可能是语法错误,也可能是逻辑错误。只有通过仔细的检查,才能找到这些错误。因此,细心是处理BUG的第二步


创新


接下来,我们来谈谈创新。在游戏开发中,传统的解决方法可能无法解决某些复杂的问题。这时候,你需要有创新的思维,去寻找新的解决方案。例如,你可以尝试使用新的编程语言或者新的开发工具来解决问题。因此,创新是处理BUG的第三步


团队协作


然后,我们来谈谈团队协作。在游戏开发中,处理BUG并不是一个人的事情,而是需要整个团队的协作。每个人都有自己的专长,只有通过团队的协作,才能更有效地解决问题。例如,设计师可以帮助程序员理解用户的需求,测试人员可以帮助程序员发现程序的问题。因此,团队协作是处理BUG的第四步


不断学习


最后,我们来谈谈不断学习。技术是不断发展的,新的编程语言、新的开发工具、新的开发方法会不断出现。作为游戏开发者,你需要不断学习,才能跟上技术的发展。只有这样,你才能更好地处理BUG。因此,不断学习是处理BUG的最后一步


结语


总的来说,处理BUG是一项需要耐心、细心、创新、团队协作和不断学习的任务。虽然这个过程可能会很艰难,但是当你成功解决一个问题的时候,那种成就感是无法用言语表达的。


希望这些心得能对你们有所帮助


在哪里可以看到如此清晰的思路,快跟上我的节奏!关注我,和我一起了解游戏行业最新动态,学习游戏开发技巧。


我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。


AD:笔者线上的小游戏《重力迷宫球》《贪吃蛇掌机经典》《填色之旅》大家可以自行点击搜索体验。


作者:亿元程序员
来源:juejin.cn/post/7294563074936356875
收起阅读 »

废掉一个程序员的十大铁律,你中招了吗?

写在前面 废掉一个人,其实还是挺简单的。最简单的两种方式就是:让他忙,一直忙;让他闲,一直闲。 一个人开始废掉的标志 终日懒散,无所事事;没有目标,没有规划;不想上班,只想打游戏;除了这些,可能下一个将要废掉的人,就是看似每天都很忙碌的你! 冰河在多年的从业...
继续阅读 »

写在前面


废掉一个人,其实还是挺简单的。最简单的两种方式就是:让他忙,一直忙;让他闲,一直闲。


一个人开始废掉的标志


终日懒散,无所事事;没有目标,没有规划;不想上班,只想打游戏;除了这些,可能下一个将要废掉的人,就是看似每天都很忙碌的你!


图片


冰河在多年的从业生涯中,总结出10条序员如何让自己废掉的铁律。大家对号入座。


如何让自己更快的废掉?


图片


忙得要死


有人说,废掉一个程序员最隐蔽的方式,就是让他忙到没有时间成长。每天定的计划特别满,正常工作时间完不成的,必须留下来加班。程序员每天都要很努力的工作,不停的coding,写业务代码。每天都非常忙碌,甚至周末都不能休息。有些公司“996”还不行,还要弄个“007”出来,程序员完全无法留出时间自我总结和成长。


冰河见过太多这样的人了,工作几年了,只会写CRUD。为哈?因为他太忙了,忙到没有时间提升自己。很多工作三年的程序员就只会CRUD。跳槽面试时,一旦被问到性能调优、高并发、Dubbo、SpringCloud等底层的原理和技术时,基本就歇菜了。


闲得要命


有人说,废掉一个程序员最快的方式,是让他闲着,让他没有方向感,不知道干啥。这类程序员也不会规划自己的时间,只是被动的接受别人的安排。时间久了,忘记了自己是干啥的,成功的废掉了自己。


拖延症


这类程序员就是自己把自己拖死的。工作任务不重,但是每天会加班到很晚,开始觉得很勤奋,但是细想,就这点工作不至于加班这么久吧?一问才知道,这是拖延症的表现。有些程序员做事情喜欢拖延,工作前要先聊会QQ,聊会微信,看看微博,刷刷朋友圈。总觉得时间还早,晚点再干吧。久而久之,养成了难以戒掉的拖延症。


吃老本


不及时更新自己的技术栈,拒绝接受新技术。总觉得自己的技术很牛X,殊不知,你的技术已经赶不上时代的潮流了,刚毕业的小学生掌握的技术都比你先进。


不会规划


对自己的职业生涯没有规划,未来朝着哪些方向努力,完全不知道。当一天和尚,撞一天钟。一旦公司或者企业出现变故,这类程序员基本没戏。


没有目标


这点与不会规划有些雷同,但是更加强调的是目标感。没有目标感,无法更好的驱动自己去完成相应的事项,无法让自己更聚焦到优先级高的事项中,即使再忙,也是眉毛胡子一把抓,最后累苦了自己,还拖累了别人。


自我感觉良好


这类程序员就是自我感觉啥都会,将自己封闭在自身的世界中,不愿意与他人沟通和交流,觉得自己都是对的,就这么干!美其名曰:自我良好!实际上,啥也不是。久而久之,不废才怪!


专注度不够


做事情注意力不集中,写两行代码,看看手机、刷刷微博和朋友圈,无法让自己专注的完成手上的任务。时间久了,养成了三心二意的习惯。


知识面欠缺


不愿拓宽自己的技术栈,发自内心愿意当一颗小小的螺丝钉,不愿意尝试新技术和新业务,守着自己的一亩三分地,反反复复就那些CRUD操作。


啥都想学


这类程序员本质上就是没有一个方向,对自己的职业生涯没有规划,啥都想学,一会想学Java,一会想学Python,一会又想学Go,最后又想学大数据。到头来,却是啥也没学会。


以上这些,希望小伙伴们有则改之,无则加勉~~


最后,除上面10条铁律外,还有一点就是 改需求 也能让程序员死得快些!


好了,今天就到这儿吧,小伙伴们,你们知道哪些能够让程序员更快废掉的方式呢?请在文末留言,说出你的观点。我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7294853623849435170
收起阅读 »

【Java集合】了解集合的框架体系结构及常用实现类,从入门到精通!

嗨~ 今天的你过得还好吗?以不同的方式长大谁都没有轻轻松松🌞- 2023.10.27 -通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构...
继续阅读 »

640 (13).jpg

嗨~ 今天的你过得还好吗?

以不同的方式长大

谁都没有轻轻松松

🌞

- 2023.10.27 -

通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构

从集合框架开始,也就是进入了java这些基础知识及面向对象思想进入实际应用编码的过程,通过jdk中集合这部分代码的阅读学习,就能发现这一点。


5bbd4f4683e25cfda6c3946e9925c48f.gif


本计划在这篇中把框架体系和一些集合的常用方法一起编写。仔细考虑之后,本着突出重点,结构清晰的思路,所以把框架体系单独拉出来,为让各位看官对java的集合框架有个清晰的认识,最起码记住常用的几种常用实现类!


好的,下面我们进入正题。


集合的框架体系结构

可以在很多JAVAEE进阶知识的学习书籍或者教程中看到,JDK中提供了满足各种需求的API,主要是让我们去学习和了解它提供的各种API。

54f18513580ae2f9b13d1b648e6c2380.jpeg

在使用这些API之前,我们往往需要先了解其继承与接口架构,才能了解何时采用哪个实现类,以及类之间如何彼此合作,从而达到灵活应用。


查看api文档,集合按照存储结构可以分为两大类,

  • 单列集合 java.util.Collection

  • 双列集合 java.util.Map


通过jdk api 来看在 JDK中 提供了丰富的集合类库,为了便于初学者进行系统地学习,我们通过结构图来分别描述集合类的继承体系。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


640 (27).gif


Collection

Description

Collection: 单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口:

  • java.util.List List的特点是 元素有序,元素可重复。

  • java.util.Set Set的特点是 元素无序(不全是),而且不可重复


List 接口主要的实现类有 java.util.ArrayList 和 java.util.LinkedList,Set 接口的主要实现类有 java.util.HashSet 和 java.util.TreeSet。



640 (27).gif

Map

Description

Map: 双列集合,用于存储具有映射关系的对象。常用的实现类有

  • java.util.HashMap

  • java.util.LinkedHashMap

注:图片中 小标中有 I的都是接口类型,而 C 的都是具体的实现类。


好的,框架的介绍就到这里了。本文中主要介绍了框架的两大类,以及我们在开发工作中使用的几种常见的接口和实现类,在后面的文章中,一一介绍吧。



收起阅读 »

宽度 or 深度?

😰服了,随便起的标题也太擦边了🙈(肯定是我的工作伙伴带坏了我~) 在阿里和京东,会把人才分为“L 型人才”和“T型人才”,其实这个很容易理解,L型 —— 先保证技术深度再保证技术宽度;T型 —— 先保证技术宽度再保证技术深度 。恰好本人对于这个问题也没有明确...
继续阅读 »

😰服了,随便起的标题也太擦边了🙈(肯定是我的工作伙伴带坏了我~)



在阿里和京东,会把人才分为“L 型人才”和“T型人才”,其实这个很容易理解,L型 —— 先保证技术深度再保证技术宽度;T型 —— 先保证技术宽度再保证技术深度 。恰好本人对于这个问题也没有明确的答案,所以在这里想跟大家讨论一下:对于一个职业生涯初期的程序猿来说,在职业生涯初期应该以技术宽度为重还是以技术深度为重?


前辈的看法


先po一下几位前辈的观点。


一位掘金朋友的意见



单向发展局限性比较大,不管是做技术管理还是技术专家,都需要一定的广度,但是这个广度我觉得是建立在已经有深度的基础上,单纯为了扩展广度而去学很多基础的东西,意义不大,毕竟不是为了做外包,所以还是看你处于哪个阶段的瓶颈期吧,如果是1-3年,可能花时间搞清楚原理性的东西带来价值更大,而如果是3-5年,应该是要培养考虑问题的思维能力上吧,这是我的个人看法~



一位字节大佬的意见:



突然发现,前端好像没几个做到 CTO 的……仔细想想,国内的前端界比较出名的前端出身做到很高职位的,玉伯算是一个代表,后期他基本上已经成为一个产品设计方面的负责人了,脱离了单纯前端的范畴。而普遍对于前端天花板的看法都差不多,确实是认为有后端工程背景的人升为 VP/CTO 级别的概率比较高,而前端更倾向于在框架中日复一日的迷失。而整体来说,后端天花板的高度普遍远远高于前端天花板的高度。



职业背景


本人是处在一个制造业公司的前端开发(也是唯一一个前端),公司主要技术栈为sql,公司主要通过sql解决公司业务问题,公司有雇佣外包公司(可以理解为公司大部分业务通过外包实现,公司没有固定的IT产品,也理所当然的没有技术支持,每个人能实现日常业务即可,因此可能对于个人的技术深度发展帮助有限?),其实公司对于职业定位真的区分不大,推崇的理念是你能干活即可,因此对于业务没有特定的划分,因此虽然我是前端,但是我也能碰后端(java)、sql、运维(部署)的活。


我的看法


刚想搜一下资讯,结果就看到了这个沸点。



😅在一个内卷如此严重、形势如此悲惨的状况下,还真的有必要去思考选择走哪条路吗?


我的看法是,其实无论是选择技术宽度还是选择技术深度,如果真的不知道选择什么,与其迷茫停止前进不如跟着环境走?我相信只要持续学习,学到的终归是自己的,到最后肯定是会有所收获的💪。至于怎么选择,好像得以后考虑了...


👀最后


其实这个问题我真的没有答案,如果有大佬有其他看法,希望能为我答疑解惑吧😭


作者:码外生活
来源:juejin.cn/post/7287020579831988258
收起阅读 »

俞敏洪:我曾走在崩溃的边缘

web
大家在人生的经历中遇到过很崩溃的事情吗? 我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。 长时间下来,这个习惯让我对名人的苦难经历...
继续阅读 »

大家在人生的经历中遇到过很崩溃的事情吗?


我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。


长时间下来,这个习惯让我对名人的苦难经历或者处理棘手问题的经历有强烈的好奇心。最近,读了俞敏洪的自传《我曾走在崩溃的边缘》,感觉挺有意思。


俞敏洪是新东方的老板,在“双减”政策之后,新东方转型做了直播,也就是大家熟知的东方甄选,可能很多人还买过他们直播间的大米。当然,我没有买过,因为理智促使我很少为情怀买单。


离开北大


俞敏洪曾经是北大的老师,他的梦想是出国留学。但老师的工资低,很难赚够出国的学费。作为南方人的他,天生的商人基因让他找到了赚钱的路子——开英语培训班。这条路子获得的收入比工资高十几倍,利润十分丰厚。


于是,他打着北大的招牌私下招生,这意味着和北大“官方”的托福培训班形成了竞争关系。学校当然不会允许北大老师和北大抢生意,况且学校禁止老师私下办培训班。俞老师无法避免地和校领导发生了冲突,并因此被处分。


图片


处分的通告在学校的高音喇叭上足足播了一个礼拜,这件事情闹得人尽皆知,对俞敏洪名声的伤害极大。后来,学校分房自然没有俞老师的份了。在中国的社会体系下,名声对一个人来说极其重要。这种“德治”社会虽然在人口大国里对秩序起着巨大的作用,但也给一些人带来了巨大伤害。一遭名声败坏,要背一辈子,这对当事人是多大的打击。


那时俞敏洪已经结婚,本可以在大学教书过安稳的生活,但这一纸处分,让他决定从北大离职。最后,他骑着三轮车拉着家当离开了北大,开启了新东方的事业。


图片


死磕办学证


办培训班需要办学证,类似于现在的牌照。如果没有就无法公开招生,这意味着无法扩大规模。俞敏洪没办法,找了当时一个叫东方大学的机构联合办培训班,条件是支付总收入的25%给东方大学。


东方大学不参与招生、培训等所有事情,却要分掉一大笔钱。随着培训班的规模越来越大,俞敏洪意识到这不是长久之计,他决定就算再难,死磕也要把办学证拿到手。


要拿到办学证要符合两个条件:一是必须有大学副教授以上职称,二是要经原单位同意。


俞敏洪在北大只是讲师,没有副教授职称,而且北京大学处分了他,不可能同意他办学。两个条件都不符合,教育局直接拒绝,并叫他不要来了。


不得不说,俞老师的脸皮是够厚的,每隔一两星期就去教育局和办事的人聊天,久了大家就混熟了。


大概耗了半年,教育局放低了办学的要求,只要他能够在人才交流中心拿到允许办学证明就放行。可是人才交流中心的工作人员根本不给他开证明。直到遇见他一个在这里工作的学生,在她的帮助下才拿到证明。


办学证到手后,俞敏洪离开东方大学,开始独立办培训班。原来的“东方大学外语培训部”这块招牌积累了相当的名气,新东方成立后,大量学生还去那边报名。为了顺利切换品牌,新的培训机构起名叫新东方,而且从东方大学买断了“东方大学外语培训部”三年的使用权,每年支付20万。


这一系列的操作,可见俞敏洪有相当不错的商业头脑。


被赶下董事长的位置


中国是一个人情社会,比如亲情、友情、同学情。在这种社会成长起来的人,自然会想到找自己熟悉的人一起做事业。俞敏洪也不例外。新东方的培训班办得风生水起,俞敏洪开始寻找人才。


除了拉亲人朋友入伙,他还出国把大学同学王强、徐小平拉回来一起跟他干事业。这三人被称为“东方三驾马车”,也就是电影《中国合伙人》的原型。


image.png


亲人、同学、朋友之间,天然有信任感,在事业的初创阶段一起工作沟通效率非常高,而且为了共同的目标,凝聚力非常强。


当公司到了一定的规模,这种人情关系构建起来的团队,会使公司的人事关系变得非常复杂。


一是,团队没有组织架构,决策效率低下;二是,老板没有话语权,下面的人不知道该听谁的,却谁都不敢得罪。


后来,在新东方改革期间,创始团队出现各种矛盾,俞敏洪无法短期内处理好这些矛盾,被管理层认为是不合格的董事长。于是,俞敏洪从位置上退了下来。


退位期间,其他几个领导轮流做主,也无法处理好团队的矛盾。俞敏洪开始大量阅读公司管理、股权管理的书籍,积累比其他领导更丰富的管理知识。两三年后,他重新回到董事长的位置上。


他能回到位置上,管理知识是一方面,我斗胆猜测,运气的成分占比很大。毕竟被自己的公司赶走的大有人在。


结尾


除了上面3个故事,俞敏洪还有很多非常精彩的故事,比如“被抢劫险些丧命”、“知识产权侵权风波”、“新东方上市”、“遭遇浑水公司做空”等等。


语言是思想的外衣。他来自农村,《我曾走在崩溃的边缘》这本书语言坦诚,像他本人一样。他的人生非常精彩,展现了他强大的韧性。


他的成功,有时代的机遇,也有个人的努力。我们可能无法准确把握时代的机遇,但可以学习他的努力和韧性,在崩溃之时屹立不倒。


作者:华仔很忙
来源:juejin.cn/post/7218487123212091450
收起阅读 »

JS小白请看!一招让你的面试成功率大大提高——规范代码

web
前言 规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如...
继续阅读 »

前言


规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如何使我们的代码可读性提高。


正文



  • 这里我们先放出一道面试题


输入一个数组,例如:array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
返回一个固定格式的电话号码 例如:(123456-789

function phoneNumber(numbers){

}


  • 注释


当我们拿到这道题时,我们需要先做什么呢?是一上来直接实现这个函数的功能吗?一般人可能就这样上了,但是,如果面试官看到你这样去写代码,这样会显现你的编程素养特别差,面试官对你的好感度肯定也会随之下降。那我们应该怎么做呢?我们需要先写一个良好的注释


/**
* @func 返回固定格式的电话号码 函数功能
* @params array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
* @return (123) 456-7890
* @author jser
*/

// 函数定义
function phoneNumber(numbers) {

}

良好的注释可以提高代码的可读性。小伙伴们要记住,代码的可读性高于一切!


在大公司中,一份代码可能会经过许多的程序员阅读或修改。如果你写了良好的注释,当你的代码被他人阅读时,其他程序员可以快速读懂这份代码,或者根据自己的需要去修改这份代码,这样大大节省了时间,也提高了团队的效率。



  • 换行


当我们把良好的注释写好之后,就可以写代码去实现功能了。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]+ ")" + " " + numbers[3]+numbers[4]+numbers[5] + "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9]+""
}

“什么?这是什么代码,还需要我拖动去查看后面的,你可以走了!”


相信很多小伙伴看到这串代码时,也跟小编一样的头疼,这代码也看的太费劲了吧,怎么全部都挤在一行里去了。运行了一下之后,发现运行结果是对的,但是小编是不建议大家这样写代码的,我们要适当的进行换行操作,这样同样提高了代码的可读性。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""
}

ES6


ES5和ES6都是JavaScript语言的版本。ES5是ECMAScript 5的简称。自ES6(ECMAScript2015)出来后,ES6引入了一些新的语法和关键字,使得代码更加易读、易写,提高了可维护性,例如解构赋值、箭头函数、模板字符串等。



  • 箭头函数


在ES5旧版本中,很多小伙伴会觉得function很繁琐,而且到处都是function。而箭头函数可以算是函数的简版,它的结构变得比函数简单。那我们如何使用箭头函数呢?



  1. 去掉function,在()和{}之间加=>

  2. 如果参数列表只有一个形参,可省略()

  3. 如果函数体只有一句话,可省略{},如果仅有的一句话函数体是return xxx,就必须省略return


在上面那个例子里,我们可以这样写箭头函数


phoneNumber = (numbers) =>"(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""


可以看出,箭头函数的结构比我们使用旧版的函数会简单许多,小伙伴们可以选择使用啊。



  • 模板字符串


模板字符串,也称为模板字面量,是 ECMAScript 6(ES6)引入的一种新的字符串表示法。它允许在字符串中嵌入变量和表达式,使用反引号(``)包围字符串内容。与传统字符串拼接相比,模板字符串具有以下优势:


模板字符串允许在字符串中插入变量值或表达式,使用 ${} 语法。这使得代码更加清晰和可读,不需要繁琐的字符串拼接。


const name = 'junjun'; 
const greeting = `Hello, ${name}!`; // 使用模板字符串插入变量
console.log(greeting);// 输出:Hello, junjun!

那机灵的小伙伴就问了,电话号码那个例子是不是也可以使用模板字符串,我们直接上代码


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

//输出 (123)
// 456
// -7890

根据结果可以看到,使用模板字符串时,当我们进行换行操作时,模板字符串也会换行。模板字符串虽然方便,但小伙伴们要记住,不是在所有情况下,都可以使用模板字符串的。


总结


代码的可读性高于一切。我们作为小白,在慢慢成长的过程中,一定要尽早的规范自己的代码,提高代码的可读性,学习ES6的语法。当我们以后进入企业工作之后,公司会统一我们的代码风格,并且规定使用哪些语句。


作者:来颗奇趣蛋
来源:juejin.cn/post/7294080876827901993
收起阅读 »

Echarts添加水印

web
如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。 Echarts-backgroundColor backgroundColor 支持使用rgb(255,255,255),rgba(255,255,255,...
继续阅读 »

如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。



Echarts-backgroundColor


backgroundColor



支持使用rgb(255,255,255)rgba(255,255,255,1)#fff等方式设置为纯色,也支持设置为渐变色和纹理填充,具体见option.color



color


支持的颜色格式:




  • 使用 RGB 表示颜色,比如 'rgb(128, 128, 128)',如果想要加上 alpha 通道表示不透明度,可以使用 RGBA,比如 'rgba(128, 128, 128, 0.5)',也可以使用十六进制格式,比如 '#ccc'




  • 渐变色或者纹理填充


    // 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比,如果 globalCoord  `true`,则该四个值是绝对的像素位置
    {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 0,
    y2: 1,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 径向渐变,前三个参数分别是圆心 x, y 和半径,取值同线性渐变
    {
    type: 'radial',
    x: 0.5,
    y: 0.5,
    r: 0.5,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 纹理填充
    {
    image: imageDom, // 支持为 HTMLImageElement, HTMLCanvasElement,不支持路径字符串
    repeat: 'repeat' // 是否平铺,可以是 'repeat-x', 'repeat-y', 'no-repeat'
    }



水印


通过一个新的canvas绘制水印,然后在backgroundColor中添加


const waterMarkText = 'YJFicon'; // 水印
const canvas = document.createElement('canvas'); // 绘制水印的canvas
const ctx = canvas.getContext('2d');
canvas.width = canvas.height = 100; // canvas大小 - 控制水印间距
ctx.textAlign = 'center'; // 文字水平对齐
ctx.textBaseline = 'middle'; // 文字对齐方式
ctx.globalAlpha = 0.08; // 透明度
ctx.font = '20px Microsoft Yahei'; // 文字格式 style size family
ctx.translate(50, 50); // 偏移
ctx.rotate(-Math.PI / 4); // 旋转
ctx.fillText(waterMarkText, 0, 0); // 绘制水印

option = {
//...
backgroundColor: {//在背景属性中添加
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
...
}

image.png


如果只想在 toolbox.saveAsImage 下载的图片才展示水印,toolbox.feature.saveAsImage 支持配置backgroundColor,将其设置为水印【纹理】即可


option = {
//...
toolbox: {
show: true,
feature: {
...
saveAsImage: {
type: 'png',
backgroundColor: {
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
}
}
}
//...
}

graphic


除了使用纹理背景,还可以 graphic 添加图形元素,其中包括 text,可用于绘制水印。


option = {
//...
graphic: [
{
type: 'group',
rotation: Math.PI / 4,
bounding: 'raw',
top: 100,
left: 100,
z: 100,
children: [
{
type: 'text',
left: 0,
top: 0,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
},
{
type: 'text',
left: 40,
top: 40,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
}
]
},
],
//...
}

比较繁琐,需要自己设置平铺规律,建议封装一个平铺方法,不能控制区分 saveAsImage。


最初想象


预研


通过源码可以知道 saveAsImage 的实现也是通过内置 API getConnectedDataURL 获取url(base64格式),然后赋值到 a 标签上(带download属性)实现下载。


class SaveAsImage extends ToolboxFeature<ToolboxSaveAsImageFeatureOption> {

onclick(ecModel: GlobalModel, api: ExtensionAPI) {
const model = this.model;
const title = model.get('name') || ecModel.get('title.0.text') || 'echarts';
const isSvg = api.getZr().painter.getType() === 'svg';
const type = isSvg ? 'svg' : model.get('type', true) || 'png';
const url = api.getConnectedDataURL({
type: type,
backgroundColor: model.get('backgroundColor', true)
|| ecModel.get('backgroundColor') || '#fff',
connectedBackgroundColor: model.get('connectedBackgroundColor'),
excludeComponents: model.get('excludeComponents'),
pixelRatio: model.get('pixelRatio')
});
const browser = env.browser;
// Chrome, Firefox, New Edge
if (isFunction(MouseEvent) && (browser.newEdge || (!browser.ie && !browser.edge))) {
const $a = document.createElement('a');
$a.download = title + '.' + type;
$a.target = '_blank';
$a.href = url;
const evt = new MouseEvent('click', {
// some micro front-end framework, window maybe is a Proxy
view: document.defaultView,
bubbles: true,
cancelable: false
});
$a.dispatchEvent(evt);
}
// IE or old Edge
else {
// ...
}
}
// ...
}

思路


可以通过扩展此方法,先获取原始的图片url,基于这个图片重新绘制一个canvas,然后在这个基础上覆盖水印,最后将canvas再次转成url返回。



  • echartsInstance._api.getConnectedDataURL

  • echartsInstance.__proto__.getConnectedDataURL


含有两处实例方法需要处理。


结果



  • ✅可以拦截默认行为获取到添加水印后的图片url

  • ❌通过原api方法获取到url后绘制到新的canvas,涉及到异步处理(img标签需要等待load),由于 saveAsImage 调用 getConnectedDataURL 获取url是同步过程,因此无法正确读取到异步处理完的最终url,导致下载失败;不过,手动调用实例getConnectedDataURL 可以使用,需要配置Promise语法使用。


伪代码:


const originGetConnectedDataURL = echartsInstance._api.getConnectedDataURL
echartsInstance._api.getConnectedDataURL = async function () {
const origin = originGetConnectedDataURL.call(echartsInstance, ...arguments)
const result = await toWaterUrl(origin)
return result
}

function toWaterUrl (url) {
return new Promise(resolve => {
const img = new Image()
img.src = url + '?v=' + Math.random();
img.setAttribute('crossOrigin', 'Anonymous');
img.onload = function() {
// drawCanvas img 转 canvas
// afterWater canvas 绘制水印
resolve(afterWater(drawCanvas(img))
}
})
}

附带drawCanvas/afterWater


function drawCanvas(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
return canvas;
}

function afterWater(canvas, { text = 'WangSu', font = 'italic 10px', fillStyle = 'red', rotate = -30 }, type = 'png') {
return new Promise((resolve, reject) => {
let context = canvas.getContext('2d');
context.font = font;
context.fillStyle = fillStyle;
context.rotate(rotate * Math.PI / 180);
context.textAlign = 'center';
context.textBaseline = 'Middle';
const textWidth = context.measureText(text).width;
for (let i = (canvas.height * 0.5) * -1; i < 800; i += (textWidth + (textWidth / 5))) {
for (let j = 0; j < canvas.height * 1.5; j += 128) {
// 填充文字,i 间距, j 间距
context.fillText(text, i, j);
}
}
resolve(canvas.toDataURL('image/' + type))
})
}

作者:wangsd
来源:juejin.cn/post/7290417906840322106
收起阅读 »

Python枚举类:定义、使用和最佳实践

枚举(Enum)是一种有助于提高代码可读性和可维护性的数据类型,允许我们为一组相关的常量赋予有意义的名字。 在Python中,枚举类(Enum)提供了一种简洁而强大的方式来定义和使用枚举。 一、枚举类 1.1 什么是枚举类? 枚举类是一种特殊的数据类型,用于表...
继续阅读 »


枚举(Enum)是一种有助于提高代码可读性和可维护性的数据类型,允许我们为一组相关的常量赋予有意义的名字。


在Python中,枚举类(Enum)提供了一种简洁而强大的方式来定义和使用枚举。


一、枚举类


1.1 什么是枚举类?


枚举类是一种特殊的数据类型,用于表示一组具有离散取值的常量。它将常量与有意义的名字关联起来,使得代码更易读、更易维护。枚举类的每个成员都有一个唯一的名称和一个关联的值。


枚举类的典型用例包括表示颜色、方向、状态、星期几等常量值。使用枚举可以增强代码的可读性,减少硬编码的风险。


1.2 Python中的枚举类


在Python中,使用内置模块enum来创建和使用枚举类。


enum模块提供了Enum类,允许定义自己的枚举类型。


二、定义和使用枚举类


2.1 定义枚举类


要定义一个枚举类,需要导入Enum类并创建一个继承自它的子类。在子类中,我们定义枚举成员,并为每个成员分配一个名称和一个关联的值。


示例代码:


from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

在这个示例中,定义一个名为Color的枚举类,它有三个成员:REDGREENBLUE,每个成员都有一个整数值与之关联。


2.2 访问枚举成员


定义枚举类,可以通过成员名来访问枚举成员。例如:


print(Color.RED)    # 输出:Color.RED
print(Color.GREEN)  # 输出:Color.GREEN

2.3 获取枚举成员的值


要获取枚举成员的关联值,可以使用成员的value属性。例如:


print(Color.RED.value)    # 输出:1
print(Color.GREEN.value)  # 输出:2

2.4 比较枚举成员


枚举成员可以使用相等运算符进行比较。可以直接比较枚举成员,而不必比较它们的值。例如:


color1 = Color.RED
color2 = Color.GREEN

print(color1 == color2)  # 输出:False

2.5 迭代枚举成员


使用for循环来迭代枚举类的所有成员。例如,要打印所有颜色的名称和值:


for color in Color:
    print(f"{color.name}: {color.value}")

2.6 将值映射到枚举成员


根据枚举成员的值来获取成员本身,可以通过枚举类的__members__属性来实现。


例如,要根据值获取Color枚举成员:


value = 2
color = Color(value)
print(color)  # 输出:Color.GREEN

三、枚举的最佳实践


第三部分:枚举的最佳实践


枚举是一种有用的数据类型,但在使用时需要遵循一些最佳实践,以确保代码的可读性和可维护性。


3.1 使用枚举代替魔术数字


在代码中使用枚举来代替魔术数字(不明确的常量值)可以增加代码的可读性。枚举为常量提供了有意义的名字,使得代码更容易理解。


例如,使用枚举来表示星期几:


from enum import Enum

class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 5
    SATURDAY = 6
    SUNDAY = 7

3.2 避免硬编码


尽量避免在代码中硬编码枚举成员的值。如果需要使用枚举成员的值,最好使用枚举成员本身而不是其值。这可以提高代码的可读性,使得代码更容易维护。


例如,避免这样的写法:


if day == 1:  # 避免硬编码
    print("Today is Monday")

而使用枚举成员:


if day == Weekday.MONDAY:  # 更具表现力
    print("Today is Monday")

3.3 使用枚举成员的名称


枚举成员的名称通常应该使用大写字母,以便与常规变量和函数名称区分开。这是一种约定,有助于提高代码的可读性。例如,使用RED而不是red


3.4 考虑枚举成员的值类型


枚举成员的值通常是整数,但根据上下文和需求,可以选择不同的值类型,如字符串。选择适当的值类型可以使代码更具表现力。


3.5 考虑用法和上下文


在定义枚举时,考虑其用法和上下文。命名枚举成员和选择合适的值应该反映其在应用程序中的含义和用途。这有助于其他开发人员更容易理解和使用枚举。


3.6 枚举的不可变性


枚举成员是不可变的,一旦创建就不能更改其值。这有助于确保枚举成员的稳定性,并防止意外的修改。


遵循这些最佳实践可以帮助你有效地使用枚举,提高代码的可读性和可维护性。枚举是一种强大的工具,可以在代码中代替魔术数字,并提供有意义的常量名称。


总结


Python的枚举类是一种强大的工具,用于表示一组相关的常量,并提高代码的可读性和可维护性。通过枚举,我们可以为常量赋予有意义的名称,避免硬编码的值,以及更容易进行比较和迭代。


在实际编程中,枚举类可以提供一种清晰、可维护且更具表现力的方式来处理常量值。


作者:涛哥聊Python
来源:juejin.cn/post/7294150742112895002
收起阅读 »

手把手教你压测

前言 身为后端程序员怎么也要会一点压力测试相关的技术吧, 不然无脑上线项目万一项目火了进来大量请求时出现程序执行缓慢, 宕机等情况你肯定稳稳背锅, 而且这个时候短时间内还没办法解决, 只能使用物理扩容CPU, 内存, 更换网络等几种方式来解决问题, 妥妥的为公...
继续阅读 »

前言


身为后端程序员怎么也要会一点压力测试相关的技术吧, 不然无脑上线项目万一项目火了进来大量请求时出现程序执行缓慢, 宕机等情况你肯定稳稳背锅, 而且这个时候短时间内还没办法解决, 只能使用物理扩容CPU, 内存, 更换网络等几种方式来解决问题, 妥妥的为公司增加支出好吧, 下一个被开的就是你


都是想跑路拿高薪的打工仔, 身上怎么可以背负污点, 赶紧学一手压力测试进行保命, 我先学为敬


本篇文章主打一个学完就会, 奥利给



文中出现软件的版本



  • JMeter: 5.5

  • ServerAgent: 2.2.3



性能调优对各个开发岗位的区别


各个岗位对性能调优的关键节点



  • 前端工程师:

    • 首屏时间: 初次访问项目等待加载时间

    • 白屏时间: 刷新页面到数据全部展示时间

    • 可交互时间

    • 完全加载时间



  • 后端工程师

    • RT: 响应时间

    • TRS: 每秒事务数

    • 并发数: 这应该不会解释了吧



  • 移动端工程师

    • 端到端相应时间

    • Crash率

    • 内存使用率

    • FPS





主要讲一下后端工程师(Java), 毕竟这是吃饭的家伙

对于后端工程师来说, 影响性能的地方主要有两个



  • 数据库读写, RPC, 网络IO, 代码逻辑复杂度, 缓存

  • JVM(Throughput)
    - JVM(Throughput)



影响性能的关键要素



  • 产品设计

    • 产品逻辑

    • 功能交互

    • 动态效果

    • 页面元素



  • 基础网络

  • 代码质量&架构

    • 架构不合理

    • 研发功底和经验不足

    • 没有性能意识: 只实现功能不注重代码性能, 当业务上量后系统出现连锁反应, 导致性能问题增加

    • 数据库: 慢查询, 过多查询, 索引使用不当, 数据库服务器瓶颈



  • 用户移动端环境

    • 设备类型&性能

    • 系统版本

    • 网络(WiFi, 2G, 3G, 4G, 5G)

    • 硬件及云服务(服务器硬件, CPU, 内存..)




1. 初步了解压力测试


1.1压力测试是什么


压力测试是针对特定系统或组件, 为要确定其稳定性而特意进行的严格测试. 会让系统在超过正常使用条件下运作, 然后再确认其结果
对系统不断施加压力, 来预估系统`负载能力`的一种测试

一般而言, 只有在系统基础功能测试验证完成, 系统趋于稳定的情况下, 才会进行压力测试

1.2压力测试的目的


当负载主键增加时, 观察系统各项性能指标的变化情况是否有异常
发现系统的性能短板, 进行针对性的性能优化
判断系统在**高并发情况下是否会报错**, 进行是否会挂掉
测试在系统某个方面达到瓶颈时, 粗略估计系统性能上限

1.3 压力测试的指标


指标含义
相应时间(RT)是指系统对请求作出响应的平均时间, 对于单用户的系统, 响应时间可以很好地度量系统的性能
吞吐量(Throughput)是指系统在单位时间内处理的数量, 每秒事务数TPS 也算是吞吐量的一种
资源利用率CPU占用率, 内存使用率, 系统负载, 网络IO
并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量, 用户不同的使用模式会导致不同用户在单位时间发出不同数量的请求
错误率失败请求占比, 在测试时添加响应断言, 验证不通过即标记为错误, 若不添加, 响应码非200则为错误


评判系统性能, 主要考虑三个性能指标 RT, TPS, 资源利用率



image.png


上图充分的展示了响应时间, 吞吐量, 利用率和并发用户数之间的关系


随着并发用户的增加经过轻负载区, 达到最优并发数, 此时利用率高,吞吐量高, 响应时间短


但是如果用户数继续增加, 就会到达重负载区, 此时性能最大化, 但是当超过某一临界值(最大并发数)之后, 响应时间会急剧增加, 利用率平缓, 吞吐量急速下降


我们进行压测的目的主要就是测试出这个临界值的大小, 或者说, 我们系统当前能承受住的最大并发数


2. 压力测试工具 JMeter


老规矩, 先来一波软件介绍
JMeter是 Apache组织开发的基于 Java的开源压力测试工具, 具有体积小, 功能全, 使用方便等特点. 最初被设计用于 Web应用测试, 后来被扩展到其他测试领域.


常用压测工具:



  • Apache JMeter可视化的测试工具

  • LoadRunner 预测系统行为和性能的负载测试工具

  • Apache的 ab压力测试

  • nGrinder韩国研发的一款性能测试工具

  • PAS阿里测试工具


压测目标:



  • 负载上升各项指标是否正常

  • 发现性能短板

  • 高并发下系统是否稳定

  • 预估系统最大负载


2.1 安装 JMeter



写在前面, 需要 Java8环境, 没有的话需要去安装, 教程百度上一大堆



官网地址: jmeter.apache.org/


image.png


熟悉的download, 点他


image.png


开始下载(是真的慢)


image.png


解压之后进入 bin目录下, 双击 jmeter.bat, 就可以启动 JMeter了


image.png


上图可以看出, 在我们第一次打开界面时是英文的, 多少有点不友好, 接下来讲解一下怎么将语言更改为中文


2.2 设置 JMeter界面为中文


还是我们的 bin目录下, 有一个 jmeter.properties文件


image.png


双击打开, 搜索 language


image.png


去除 #号, 值更改为 zh_CN, 保存文件然后重启软件(双击jmeter.bat)


image.png


可以看到, 我们的 jmeter成功更改为了中文界面, 这对于我这种英语白痴来说是很舒服的


image.png


2.3 初步使用 JMeter


我们先随便创建一个测试用例, 就是简单测试, 同时讲解一下常用的参数


本次测试采用 20线程, 1秒启动时间, 循环100次, Get请求


2.3.1 创建线程组


image.png


image.png



  • 线程数: 虚拟的用户数, 一个用户占一个线程

  • Ramp-Up: 等待时间, 设置的虚拟用户(线程数)需要多长时间全部启动

  • 循环次数: 单个线程发送请求的次数

  • 调度器:

    • 持续时间: 该任务执行的时间

    • 启动延迟: 等待多少秒开始执行




2.3.2 创建 http请求


右键线程组-添加HTTP请求


image.png


这个中文讲解的很明白, 应该都看得懂的, 有疑问的评论区留言


image.png


2.3.3 结果树



结果树, 聚合报告, 图形结果只有新增, 解释在测试



线程组右键-添加-监听器-查看结果树


image.png


image.png


执行结果分析(启动之后显示界面)


image.png


列表列出了每一次的HTTP请求, 绿色的是成功, 红色的话就是失败



  • 取样器结果参数详解

    • Thread Name:线程组名称

    • Sample Start: 启动开始时间

    • Load time:加载时长

    • Latency:等待时长

    • Size in bytes:发送的数据总大小

    • Headers size in bytes:发送数据的其余部分大小

    • Sample Count:发送统计

    • Error Count:交互错误统计

    • Response code:返回码

    • Response message:返回信息

    • Response headers:返回的头部信息



  • 请求

    • 基本数据

    • 入参

    • 请求头



  • 相应数据

    • 响应码

    • 响应头




2.3.4 聚合报告


线程组右键-添加-监听器-聚合报告


image.png


执行结果分析(启动之后界面)


image.png


参数解释



  • 样本: 并发量

  • 平均值: 接口请求用时(单位毫秒)

  • 中位数: 请求用时中位数(单位毫秒), 例如2000请求以请求时间排序, 排名1000的用时时长

  • 90%百分位, 95%百分位, 99%百分位和中位数同理

  • 最小, 最大值: 请求用时最小和最大

  • 异常% : 请求中异常的百分比

  • 吞吐量: 单位时间内请求次数


2.3.5 图形结果


线程组右键-添加-监听器-图形结果


image.png


执行结果分析(启动之后显示界面)


image.png



  • 样本数目:总共发送到服务器的请求数。

  • 最新样本:代表时间的数字,是服务器响应最后一个请求的时间。

  • 吞吐量:服务器每分钟处理的请求数。

  • 平均值:总运行时间除以发送到服务器的请求数。

  • 中间值:有一半的服务器响应时间低于该值而另一半高于该值。

  • 偏离:表示服务器响应时间变化、离散程度测量值的大小。


2.3.6 断言


断言主要用来判断结果返回是否符合预期


线程组右键-添加-断言-响应断言


image.png


image.png


假设我们接口的返回状态码字段为code, 200为成功, 那么就可以在断言这里进行配置, 来判断请求是否成功


image.png


3. JMeter插件


3.1 插件安装


首先说明 JMeter是不支持插件的, 所以我们先要 JMeter的插件允许插件下载, 这句话多少有点拗口


网址: Install :: JMeter-Plugins.org


image.png


点击上图红框即可下载插件, 前面说过了 JMeter是 Java8开发的, 插件对应的也是一个 jar包


image.png


下好之后就可以放在 JMeter安装目录下的 lib/ext/ 下了, 具体下载页面也有说明


image.png


上述操作结束之后, 在选项里面就可以看到插件中心Plugins Manager


image.png


弹出以下界面, 点击 Available Plugins搜索我们需要的插件Basic GraphsAdditional Graphs, 勾选上, 然后安装


image.png


Basic Graphs主要显示显示平均响应时间,活动线程数,成功/失败交易数等


image.png


Additional Graphs主要显示吞吐量,连接时间,每秒的点击数等


image.png


在安装成功之后, 在监听器会相应的多出很多的 jc开头的, 这就代表安装成功了



我使用的是 5.5版本的, 之前版本安装之后好像要手动重启, 5.5安装完会自动重启



image.png


4. Linux硬件监控


在压测过程中, 我们需要实时了解服务器的CPU, 内存, 网络, 服务器负载等情况的变化, 这个时候我们就需要对我们的 Linux系统进行监控, 通常来讲, 我们查询 Linux系统的资源占用情况可以使用以下几种方法



  • 使用命令: top, iostat, iotop等

  • 使用 Linux远程连接工具 FinalShell等

  • 宝塔

  • JMeter压测工具 PerfMon


在 JMeter中, 如果需要监控服务器硬件, 那么我们还需要安装 PerfMon插件


image.png
PerfMon监控服务器硬件,如CPU,内存,硬盘读写速度等


进入下述地址开始下载监控包: github.com/undera/perf…


image.png


下载好之后我们可以直接解压放到服务器上, 会看到有两个startAgent文件, 分别是Windows系统和Linux系统的启动脚本


image.png


我们直接启动就可以了, 如果脚本启动连接不上的话可以考虑更改脚本内容


例: Linux系统脚本更改为以下内容


## 默认启动运行 startAgent.sh 脚本即可

## 服务启动默认4444端口,根本连接不上,因此自己创建一个部署脚本文件对此进行部署,且把端口修改为7879

nohup java -jar ./CMDRunner.jar --tool PerfMonAgent --udp-port 7879 --tcp-port 7879 > log.log 2>&1 &

## 赋予可执行权限

chmod 755 startAgent.sh

启动成功之后, 脚本同级路径下会多出 log.log的日志文件


image.png


然后我们就可以配置 JMeter了, 线程组-监听器-jp@gc - PerfMon Metrics Collector


image.png


image.png


我是在本地启动了ServerAgent.bat进行测试, 执行结果如下所示:


image.png



注: 文件必须配置, 不然没有图像



具体的配置指标信息建议看官方文档, 太多了....
github.com/undera/perf…


image.png


image.png


ServerAgent闪退问题


Windows系统配置好ServerAgent启动之后窗口闪退可能是 jre版本问题, 可以从下面的链接下载老版的 jre


http://www.aliyundrive.com/s/Yzw3DZ74w…


下载好之后, 建议安装目录设置在ServerAgent/jre


image.png


并更改startAgent.bat脚本, cd 到老版本 jre路径


image.png


作者:宁轩
来源:juejin.cn/post/7248511603883638844
收起阅读 »

最近的一些小事

唉,感觉我这个中年程序员叹一口气,都散发着浓郁苦咖啡蒸气的味道。 最近身体有些失调,拉肚子、口臭、眼屎、长痘、口角炎、尿黄、腰疼,哪儿哪儿都不好。在城市里生活,即便一点辣都不碰,依然火气很大,烦躁得很。 国庆假期回了趟老家,天天不忌口,这些问题却奇迹般地消失了...
继续阅读 »

唉,感觉我这个中年程序员叹一口气,都散发着浓郁苦咖啡蒸气的味道。


最近身体有些失调,拉肚子、口臭、眼屎、长痘、口角炎、尿黄、腰疼,哪儿哪儿都不好。在城市里生活,即便一点辣都不碰,依然火气很大,烦躁得很。


国庆假期回了趟老家,天天不忌口,这些问题却奇迹般地消失了。一回来,又反复了。我感觉是喝的水有问题,以至于换了一套净水器的滤芯。结果,商家还少发了配件,没法安装。


烦啊,莫名地烦躁。在老家时,种了2天的地。遇到邻地的村民和父母起了纷争,他说他家的地少了。刚分地时,我家是8米,他家是6米,现在两家各是7米。他不但抢占,还说他家的本来就比我家多。父母都是老实人,我也早读诗书,少小离家,我们都整不了无赖。农村土地管理很乱,没有记录,谁横谁有理,官方也无奈。我倍感无力。


回到城里后,越想越憋屈,又想起他的一句话,他反复朝我说“你给评评理”。我突然意识到,他这是示威,就算儿子回来了,也拿他没辙。突然感觉,人家的城府之深、心机之重,已经渗入骨髓,可随机挥发,而我久久才能参透。自责之感,愈发严重。


久对代码,我不擅长与人有关的一切社会事务。处理事情,害怕冲突,多秉持儒家的“温良恭让”,长久以来都是如此,家风亦然。此事,媳妇诟病久矣,因在底层社会,高道德意味着低资源。


几日借酒浇愁,思考破圈之事,悬而又悬,心慌不已。这日酒多,遂向媳妇诉说心中之不悦。我有红线,不同媳妇讲烦闷之事,因每次皆生矛盾。这日破戒,也未出所料。媳妇说,不是我说你,你就是xxx。你每次都xxx。你看人家xxx,你为什么就不能xxx。


我可精准预测她下一句会说什么。我本内心愧疚求安慰,她却常让这份自责升级到自裁的境地。我遂低头不语,后悔破戒。媳妇眼泪噼啪掉落:你每次都这样,是你找我谈心,我在这里噼里啪啦说个不停,你却跟个闷葫芦一样,连个回应也没有。


我连忙去哄,赶紧说上几句话,她却像一条掉到岸上的大鲤鱼,一碰就扑棱。随后,一周没有好脸色。


我又深深地叹了一口气,暗誓以后绝不再谈。同时,心里挤下一滴泪,孤独之感,油然而生,此事更与何人说。


拖着身体去上班,职场万象,意也难平。5分钟随手就能干完的事情,需要讨论半天要不要干,再辩论1天该谁去干,然后再进行1周的技术调研,干多久再排期。浪费或低效,与我无关,但我长期在这种环境下,我该变成一个什么样的人呢?与众不同会被压制,同流虽能发达但心有不甘。


写文已久,稍有风格,也出过多篇10W+的技术文,约有十几名出版社编辑,询问我是否想出书。遂又开始精神内耗。写书是好事,但必定要投入大量精力,而后续是否能出版,能否赚回机会成本,都未可知。


父母慢慢年迈,小儿正在成长,自己身体愈乏,依然没有找到事业的方向。与同行交流,各家中小型互联网公司,多在萎靡。大厂咱又进不去。


奋斗是终生的事情,但留给自己奋斗的黄金时间不多了。这时间该怎么用?把宝押到哪里,这又是难题。


目前看,啥也不多想,仅仅上班,生活不成问题,也没有烦心事。但是,以后呢?我也分不清这是内卷,还是内耗。只是隐约感觉,以后会越来越难,老的、小的都会越来越依靠自己,而自己还不够强大。


有句古话叫:比上不足,比下有余。此理论可以判我个“身在福中,无病呻吟”或者“安于现状,不够努力”。


抖音上看到一句话,史铁生说的,大意是:我四肢健全时,常抱怨环境不好。瘫痪后,开始怀念可以行走的日子。长了褥疮,怀念坐在轮椅上。后来得了尿毒症,又怀念当初长褥疮。


他没有和撕葱去比,而是和以前的自己对比。


作者:TF男孩
来源:juejin.cn/post/7290842524746022972
收起阅读 »

前端开发,微信公众号静默网页授权,本地调试及上线

web
1、前言 基于今天有个朋友问了我相关公众号授权的一些流程问题,今日来记录记录以及讲解一下如果是前端,没怎么了解后端,一些情况下前端自己调试静默授权一些东西,或者想自己试试自己公众号来玩一玩授权。 2、工具 既然在本地调试,那自然少不了微信开发者工具了,对,...
继续阅读 »

1、前言



基于今天有个朋友问了我相关公众号授权的一些流程问题,今日来记录记录以及讲解一下如果是前端,没怎么了解后端,一些情况下前端自己调试静默授权一些东西,或者想自己试试自己公众号来玩一玩授权。



2、工具


既然在本地调试,那自然少不了微信开发者工具了,对,没错,就是那个微信开发者工具。这玩意是微信公众号官方推出来的一款专门给用户制作微信小程序小游戏和公众号的软件。


image.png






概念: 用户在微信客户端访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。



题外话: 每个用户在不同的公众号openid不同。如果需要在多个公众号统一用户的账号的话,就需要UnionID在这里配同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。


3、流程




🔴 微信开发者工具


前期我们需要在本地进行调试,也就是在微信开发者工具里面调试。下载完成之后,打开软件出现二维码:


image.png


微信扫码进去。


手机端出现是否确定登录微信开发者工具,点击确认登录进去。


image.png


进来,点击公众号网页项目进去,


image.png




🔴 网页授权解剖


我们来看一下文档网页授权这块,文档当中提及两种授权,


一种是不弹出授权页面,直接跳转,就能获取用户openid(scope为snsapi_base),


另外一种是弹出授权页面,可通过openid拿到昵称、性别、所在地。并且呢,即使在未关注的情况下,只要用户授权,也能获取这个用户相关信息


1、scope=snsapi_base(静默,无感知):


snsapi_basescope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页面。[用户感知的就是直接进入回调页(往往是业务页面)]


2、snsapi_userinfo(弹框,需要用户手动同意):


snsapi_userinfoscope发起的网页授权,是用来获取用户的基本信息的。[需要用户手动同意,无需关注,用户同意授权给我们去拿他们的相关基本信息]


至于第3种就是需要用户关注了公众号之后,才能得到用户的openid去得到用户的一些基本信息的,这里只讲前面两种。


无论是上面1还是2,都需要条件为已认证服务号,需要引导用户打开地址



注意:



如果在地址栏中输入https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect这个地址,直接提示改链接无法访问的话,解决(1、看参数正确与否,2、看认证没认证,也就是scope参数授权作用域权限有没有)。


🔴 链接格式


1、静默授权snsapi_base


https://open.weixin.qq.com/connect/oauth2/authorize?appid=这个公众号的appid&redirect_uri=后端跳转拿code接口地址&response_type=code&scope=snsapi_base&state=123#wechat_redirect


2、弹窗用户同意授权 snsapi_userinfo:


https://open.weixin.qq.com/connect/oauth2/authorize?appid=这个公众号的appid&redirect_uri=后端跳转拿code接口地址&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect


参数说明
appid公众号的appid
redirect_uri授权后重定向的回调链接地址,使用urlEncode处理
response_type返回类型,就填code就可以了
scope两个,上面👆讲过,一个静默snsapi_base,一个需同意snsapi_userinfo
state重定向会带上state参数,可填a-zA-Z0-9
#wechat_redirect必带


到最后会跳转到redirect_uri/?code=CODE&state=STATE


我们做这些授权的目的就是为了得到code,code这个玩意就是得到access_token的敲门砖,code每次授权都不一样,每次的code只能使用一次,5分钟过期



4、沙盒测试(本地调试)


🔴 配置公众号平台测试账号


image.png


在【设置与开发】-【开发者工具】-【公众平台测试账号】,点进去。


这里呢,微信官方为我们提供了一个测试号,我们本地调试的话,先这个测试号来调网页授权功能。后期部署到线上,再换成我们自己这个公众号的appid和配置线上后端的域名,这是后面本地调试没问题了,再放到线上到这一步。


测试号的appid和appsecret,到后面有用:


image.png


你就看到这里就行了,其他的不用管,看到网页服务-网页账号那里,去授权网页授权获取用户基本信息


image.png


image.png


点击修改,进去网页授权域名填写,就是你希望跳转的地址的域名,这里本地调试可以填ip:port (ip:端口)这样。本地开发不用域名,当然如果你host映射重定向到一个域名(这种就是简单东西复杂化)也不是不行,就是没必要。


做完这些配置,就到代码部分了。


🔴 代码


前端,写一个页面:


代码如下:


auth.html js部分 (直接跳)



<script>
window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=测试号的appid&redirect_uri=" +
"http://192.168.0.57:8001/(注释:本地的调试获取code的地址)" +
"&response_type=code&scope=snsapi_base&state=1#wechat_redirect";
</script>

这里我用vscode的插件live_server跑起来的,地址是http:127.0.0.1:5501/auth.html


接收的话也是这个页面来接收code,得到code之后,就可以为所欲为了(bushi),就可以传给后端就获取token了,前端也可以。


获取token的过程:



获取code后,请求以下链接获取access_token:




请求这个接口 https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code



传参是 appidsecretcodegrant_type写authorization_code。


最后得到的json数据就是下面👇这个:


image.png


具体页面代码是这样的:


image.png


🔴 效果


Kapture 2023-10-25 at 17.55.46.gif


5、上线



此文讲的是本地调试怎么调,上线之后呢,我们就换,appid要换,把测试appid换成线上公众号appid(后台拿),网页授权域名要加(在【设置与开发】-【公众号设置】-【功能设置】-【网页授权域名】那里改成我们线上的域名,一个月只能改5次,可加两个域名)。



6、总结


没有太多花里胡哨的东西,就是通过code拿token,code怎么来,code通过跳转得来,这就完事了。



☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学习。感谢阅读,祝您开发有乐趣。



作者:盏灯
来源:juejin.cn/post/7293804736754106377
收起阅读 »

实采6位产品经理,他们转行都去了哪里?

作为互联网最炙手可热的优质岗位,产品经理以其“门槛低”、“工资高“的就业优势深受职场新人青睐,招聘需求榜常年稳居Top3,随着压力和内卷与日俱增,从业多年的产品经理陆续呈现出走的迹象,或自主创业,或退居二线…… 选择,各有不同;生活,依旧精彩。本期,小摹采访...
继续阅读 »

作为互联网最炙手可热的优质岗位,产品经理以其“门槛低”、“工资高“的就业优势深受职场新人青睐,招聘需求榜常年稳居Top3,随着压力和内卷与日俱增,从业多年的产品经理陆续呈现出走的迹象,或自主创业,或退居二线……


图片


选择,各有不同;生活,依旧精彩。本期,小摹采访了6位从事新领域的产品经理,一起谈谈转行,这或许也是你以后的故事。


@ 忧桑的大叔


工龄:10年


曾任职位:P2P产品专家


当前职位:某银行项目负责人


转行原因:前几年的P2P真的太香了,年终分红都有20W+,但风口来的快走的也快,公司2年没有拿到新一轮融资,现金流非常紧,自己也快40了,时间和精力拼不过年轻人,一直在寻找新的机会,好在之前和不少银行都有合作,深得对方领导赏识,经猎头介绍进入国企银行做起了项目管理,工作环境轻松,薪资待遇非常满意。


@ 有妖气


工龄:8年


曾任职位:上市公司产品总监


当前职位:创业公司CEO


转行原因:部门沟通时发现新的产品机遇并在内部有了雏形,可惜高层并不愿意投入成本,为了不让这款产品被扼杀在摇篮里,最终鼓起勇气离职创业,第一年真的非常坎坷,年初招了20个人,10月份不到走了20个,所幸投资人给予足够的资源、现金支持,推荐了技术、运营类的人才,目前已经是30人的成熟团队,虽然离商业变现还很长,但我相信我们能度过这个资本寒冬。


@ 小田阿姨


工龄:6年


曾任职位:CRM系统产品经理


当前职位:客服主管


转行原因:元旦的时候有了意外惊喜,10月怀胎有了小虎崽,我琢磨着互联网行业的加班时间太长,很难能有时间陪陪孩子,所以连续2次和COO提了离职,打算找个加班不多的公司上班。出乎意料,COO将我调任到客服部门担任主管一职,这个岗位和我的契合度很高,加之产品本身是自己设计的,不存在多余的学习成本,就选择继续呆在这里。


@ 写书的海哥


工龄:5年


曾任职位:BAT产品经理


当前职位:自由作家


转行原因:抓住了偶然的机会,有属于自己的公众号,每周三、五会更新推文,分享产品设计的方法和经验,也会鉴赏优质作品的亮点,2年下来累计了3W的粉丝,国庆的时候有个出版商邀请我把5年的经验沉淀一本书,供更多的产品经理学习,也是一个发展副业的机会,看时间很充裕就答应了下来,没成想赶上了双11,一边设计一边写作的压力很大,睡眠时间不足3小时,但已经喜欢上了写书这个过程,拿到年终奖后选择了离职,转行自由作家,现在能更舒服地做自己喜欢的事情。


@ 蒲公英有话说


工龄:3年


曾任职位:某修图公司产品经理


当前职位:网络主播


转行原因:兴趣爱好使然,在美学公司上班,擅长修图、拍照,平常也喜欢在社交平台分享生活记录。这几年公司扩展了直播带货的业务,有一次外聘的主播身体不舒服,出于对自己的颜值和气质的自信,主动客串了一波,讲解过程中发现自己对商品的理解远比主播更加深入,开始去不同的直播间露脸,收入慢慢多起来,半年后就专职做起了主播,打算先吃几年的青春饭,好好搞钱再重新规划自己。


@ 赵三岁


工龄:2年


曾任职位:游戏公司产品经理


当前职位:某五线城市公务员


转行原因:这几年版权号、政策收缩,游戏行业受到了很大的波及,隔壁的项目线直接被叫停,20多号人当天就被裁,那种恐惧深有体会,几番思考,我觉得既然是打工人,那还是找个稳定一些的工作更加靠谱,每天被裁员的环境而支配,真的不是我想要的,想着考公,家里人也在城里托了些关系,翻年后就回老家当起了公务员,朝九晚五,无KPI考核,每天都能睡好觉,这在以前根本不敢想。


在小摹看来,是蝴蝶总会遇到春天,而机会偏爱有准备的头脑,即使投身新行业,产品经理也并有没因岗位不同而从零开始,反而顺风顺水,扶摇直上。究其原因,无外乎是PM的规划、执行、沟通、管理等核心竞争力适用于绝大多数领域,能够高效帮助企业解决问题,实现自身价值,转行也有足够的底气。所以,产品经理只有不断加强核心竞争力,才能在工作中出类拔萃,获得彼此满意的薪资待遇。


作者:摹客
来源:juejin.cn/post/7164661775475376159
收起阅读 »

IT外传:大家都满意

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑是一家IT公司的程序员,岗位是Python开发,负责处理并分析一些科学数据。 这一天,老郑在写代码,写一段如何读取.txt文件的代码。说来也奇怪,web前端从浏览器上传.txt文件给J...
继续阅读 »

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。



老郑是一家IT公司的程序员,岗位是Python开发,负责处理并分析一些科学数据。


这一天,老郑在写代码,写一段如何读取.txt文件的代码。说来也奇怪,web前端从浏览器上传.txt文件给Java后端,Java收到文件后,读取内容,然后按照规则筛选一下字符串,再返回给前端展示,这也就结束了。


但是Java同事对这个任务,先是调研了一周,然后告诉主管,这个功能很难实现。主管一听,非常重视,他担心开发流程受到影响,连忙问该怎么办?Java人员说,建议用Python来做。他不但提出问题,也给出了解决方案。他说自己百度上搜了,采用Python是可以实现的。


主管将信将疑地去问老郑,如果用Python读取.txt文件,难不难?


老郑先是一怔,他心想用什么语言读取.txt都不难啊!老郑说不难,很简单。


此时,主管心里瞬间满天晴朗:原来Java说的是真的,用Python果然不难。


主管问:做这个功能……需要几天?


老郑回答说,单纯读文件很简单,关键是读取之后做什么处理啊?


主管说就是找到几个关键词,把关键词替换成*号就行了。


老郑说,3个小时就够了。


# 打开文件并读取内容
with open('test.txt', 'r', encoding='utf-8') as file:
content = file.read()
# 替换字符串
content = content.replace('掘金', '*')
# 输出内容
print(content)

老郑怕用时太多,解释道,其实写代码用不了3个小时,主要是考虑到还需要对接和测试。


主管摇了摇头,说:不要这么着急,做工作一定要仔细,我给你1天的时间,你给我做出精品,一定要用工匠精神做出精品。


主管说,你写一个处理.txt文件的服务。Web将文件传给JavaJava再传给你,你处理完把数据告诉JavaJava再告诉前端。


老郑一听,这里面有Java。于是,他疑惑地问道:“为什么要这么麻烦?Java直接处理不行吗?”


主管笑了笑,说道:“老郑,你糊涂啊。Java要是能处理,我还来找你干啥,你说是不是呀?啊哈哈!”


说完,他背着手走了,仿佛解决了一个世纪大难题。他很满意。


后来,老郑听到Java在讨论工期。其中一项任务是Java对接老郑处理.txtPython接口,Java给出的工期是3个工作日。


老郑感觉很意外,套个壳原样转发请求,需要3天?这不合理啊!


确实不合理。老郑他们公司有一个部门,叫技术总控部。里面人员的定位都是技术大牛,专门负责审核开发人员提报的工时。这项操作,可以防止小白过于轻视任务导致延误项目时间,也可以防止老油条夸大工时造成浪费。


技术总控部的大牛注意到了Java对接Python这项3天的任务。总控告诉Java:这项3天的任务,不合理。


Java开发者淡定地问道:哪个地方不合理?!


总控说:这个3天不合理。一项任务占用3天工时,说明这项任务拆分的还不够细致。你需要将任务拆得更细致一些,我们给出的规范,最小单位是0.5小时,最大单位是1天。


于是,Java开发将这项转发接口的任务,拆分成了3个子任务。



  • 第1天:结合实际业务,完成网络请求从HTTP协议层面的组织和发送;

  • 第2天:对应逻辑场景,实现Python接口返回值的网络层面接收;

  • 第3天:根据前期对接,对齐Web前端、Python端与Java端;


技术总控看了看,满意地点了点头,好似察觉并改善了一项重大风险,又帮公司做了一件防患于未然的事情。


Java开发的工时获批了,他很满意。主管也满意,总控也满意,大家都非常满意。


董事长来视察,看到排得严丝合缝的工时链条,而且大家也都在写代码,也很满意。


老郑抬头看了看整个场面,他总感觉哪里有些不对劲,但又说不上来。这确实是一片欣欣向荣、和谐向上的景象。于是,他也满意地笑了笑:嘿嘿。


作者:TF男孩
来源:juejin.cn/post/7294072229004443663
收起阅读 »

个人创业中的全栈开发经验

web
前言 个人项目开发创业半年有余,两个项目全部扑街,一无所获。 仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。 我过去7年的工作都是在从事前端开发,从最开...
继续阅读 »

前言


个人项目开发创业半年有余,两个项目全部扑街,一无所获。


仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。


我过去7年的工作都是在从事前端开发,从最开始从事IPTV 开发,用原生JS、JQuery 开发运行在机顶盒上JSP 页面;到18年,创建了项目组的第一个Vue 项目,那时候我才算是开始“现代”前端的工作;21年到上海,在新公司开始全面使用React + TS,也就是时至创业开始,我所有的工作技能,都是前端技术,后端相关的只有自己瞎折腾的项目,没有真正应用到实际项目中的,这次也算是逼着自己进步了一把。


技术选型


前端 - 后台管理系统:React + TS,用了Antd 的组件库提供的模板直接创建项目


前端 - 微信小程序:原生微信小程序开发 + Vant Weapp


服务端:微信云开发


为什么要选用以上技术栈,只有有一个原因,就是成本极低!非常低!并且很稳定,前后端全部用JS 搞定;后台管理系统部署在腾讯云的Web 应用托管上,直接免去运维工作。说个题外话,前几年自己搞个人网站的时候,服务器是薅的阿里云的羊毛,结果就是啥活都得自己干,用Express 框架开发的后端服务,用Nginx 做代理,结果并发超过100个 服务器直接挂掉。。


现在这一套技术栈,几乎没有学习成本,腾讯云的Web 应用托管集成了CI 工具,提交代码到线上分之后,自动部署,用了半年多,网站、小程序都没有挂掉过。(我真不是腾讯的托。。)


后台管理系统


React、TS、Antd 业务开发技术不多赘述,讲讲怎么在Web 端请求微信云开发的接口吧。


微信云开发提供了可访问云服务的Web Sdk,引入sdk 后,只需要进行简单的初始化,即可访问接口。


云开发登录授权配置,打开匿名登录


image.png


示例代码

处理请求


import cloudbase from "@cloudbase/js-sdk";
...
const env = ""; // 环境id
const clientId = ""; 终端id

// 创建实例
const app: any = cloudbase.init({
env,
clientId,
});

const auth = app.auth({
persistence: "local",
});

...
// 请求方法
export const cloudFn = async (
type: string,
params?: any
): Promise<any> => {
// 判断登录态
if (!auth?.hasLoginState()) {
localStorage.clear();
await auth.signInAnonymously();
}

const res = await app.callFunction({
name: "xxxx", // 云函数名称
data: { type, data: options?. }, // 传参
parse: isDev, // 环境
});

// 根据自己的业务方式处理返回数据
...


处理接口


import { cloudFn } from "@/utils";

export const xxx = (params: API.xxx) => {
return cloudFn("name", params);
};

Web Sdk 官方文档:docs.cloudbase.net/api-referen…


部署

提交代码到部署分之后,会自动部署,访问web 应用托管,会提供一个默认访问的域名,可以直接访问,但是不推荐生产使用,只需要再配置一个域名就好了。


微信小程序


如果没有开发过微信小程序,去看一下官方文档,前端基本可以无成本上手,参照官方文档开发就好;为什么组件库选择Vant Weapp,基本补全了官方没有提供的组件,使用方式也很简单,实际使用后体验不错,值得推荐。


微信云开发


我用Java、python、node 都写过后端接口,对于一个前端来说,单论简单、好上手而言,微信云开发,我愿称之为YYDS!就两个字,简单!


官方提供了请求的方法,我对其简单的封装了一下,如果觉得不错,尽管拿去用,如果有不完善的,还请指正


示例代码

云函数入口 index.js

const user = require('./user);

exports.main = async (event, context) => {
switch (event.type) {
case "userGet":
case "userUpdate":
return await user.main(event, context);


default:
return {
code: -1,
msg: '
接口不存在'
}
}
};

user 入口

const get = require("./get");
const update = require("./update");

exports.main = async (event) => {
const apiType = event.type
const data = event.data || {};

if (apiType === 'userGet') {
return await get.main(data);
};

if (apiType === 'userUpdate') {
return await update.main(data);
};
};

user/get.js

const {
dbGet, // 通用get 方法 (见后问)
filterParams, // 清除异常参数,比如空字符串,null 等
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
// 校验参数
if (check(data)) return {
code: -1,
msg: check(data),
}

const params = {
offset: data.offset,
limit: data.limit,
name: data.name,
};


// 模糊搜索
if (params.name) {
params.name = {
$regex: ".*" + params.name,
$options: "i",
};
}

return await dbGet("user", filterParams(params));
};

user/update.js

const {
dbUpdate
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
if (check(data)) return {
code: -1,
msg: check(data),
}


const params = {
_id: data._id,
name: data.name
};

return await dbUpdate("user", params);
};

utils.js

const cloud = require("wx-server-sdk");

// 初始化云环境
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
});

const db = cloud.database();

async function dbGet(
databaseTable, // 表名
params, // 参数
orderByKey = "", // 排序参数
order = "desc" // 排序方式
) {
const pageInfo = {
offset: params.offset || 1,
limit: params.limit || 10,
};
delete params.offset;
delete params.limit;

try {
// 获取总数
const resCount = await db
.collection(databaseTable)
.where(params)
.count();

const resCountData = formatRes(resCount)
if(resCount?.code !== 0) {
return resCountData
}

// 总数是0,直接返回数据
if(resCount?.data === 0) {
return {
code: 0,
data: { data: [] },
total: 0,
}
}

// 获取数据
const res = await db
.collection(databaseTable)
.where(params)
.skip((pageInfo.offset - 1) * pageInfo.limit) // 分页
.limit(pageInfo.limit) // 最多几条
.orderBy(orderByKey, order) // 排序
.get();

// 处理返回数据
const resData = formatRes(res);
if(resData?.code === 0) {
return {
code: 0,
data: {
data: resData?.data || [],
total: resCountData?.data || 0
},
}
} else {
return resData
}
} catch (error) {
return {
code: -2,
data: null,
msg: "请求失败",
};
}
}

async function dbUpdate(databaseTable, updateData) {
let res;
let isSuccess = false;
try {
const params = updateData;

if (updateData._id) {
// 编辑
delete params._id;
res = await db.collection(databaseTable).doc(updateData._id).update({
data: params,
});

if (res.errMsg === "document.update:ok") {
isSuccess = true;
}
} else {
// 新增
res = await db.collection(databaseTable).add({
data: params,
});

if (res.errMsg === "collection.add:ok") {
isSuccess = true;
}
}
if (isSuccess) {
return {
code: 0,
_id: res._id,
msg: `${updateData._id ? "更新" : "新增"}数据成功`,
};
} else {
return {
code: -1,
data: null,
msg: `${updateData._id ? "更新" : "新增"}数据失败`,
};
}
} catch (error) {
return {
code: -1,
data: null,
msg: `请求服务器失败,${updateData._id ? "新增" : "更新"}数据失败`,
};
}
}

function formatRes(res) {
const cloudFnMsgList = ["document.get:ok", "collection.get:ok", "collection.count:ok"];

if (cloudFnMsgList.includes(res?.errMsg)) {
return {
code: 0,
data: res.data || res.total,
};
} else {
return {
code: -1,
data: null,
msg: "请求服务器失败",
};
}
}

module.exports = {
dbGet,
dbUpdate,
formatRes,
};

差不多到此就结束了,Web 端的后台管理系统,微信小程序的后端接口实现了,并且可以互通,这种方式是我实践过,在保证业务、性能、稳定的前提下,最低成本的全栈开发方案,如果有其他更好的方案,欢迎讨论。


作者:鹿林秋月
来源:juejin.cn/post/7294056563631079424
收起阅读 »

说说js代码写到html里还是单独写到js文件里哪个好?为什么?

web
"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论: 将 JavaScript 代码写入 HTML 文件的优点: 方便快捷:将 JavaScript...
继续阅读 »

"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论:


将 JavaScript 代码写入 HTML 文件的优点:



  • 方便快捷:将 JavaScript 代码直接嵌入到 HTML 文件中可以节省创建和加载额外文件的时间,特别是对于小型项目或仅需少量 JavaScript 代码的情况。

  • 直观可见:通过将 JavaScript 代码嵌入到 HTML 文件中,可以更容易地将其与相关的 HTML 元素和结构联系起来,使代码的逻辑更加清晰明了。


将 JavaScript 代码单独写入 JavaScript 文件的优点:



  • 结构清晰:将 JavaScript 代码与 HTML 分离可以使代码结构更加清晰,提高代码的可读性和可维护性。这样做有助于保持 HTML 文件的简洁和专注于内容。

  • 可重用性:将 JavaScript 代码存储在单独的文件中,可以使其在多个 HTML 文件中重复使用,提高代码的可重用性和一致性。

  • 缓存优化:当 JavaScript 代码被单独提取到外部文件中时,浏览器可以将其缓存起来,从而提高页面加载速度并节省带宽。


综上所述,将 JavaScript 代码写入 HTML 文件适合小型项目或仅需少量 JavaScript 代码的情况,以及需要快速原型设计或简单交互的情况。而将 JavaScript 代码单独写入 JavaScript 文件适合大型项目或需要复杂的逻辑和结构的情况,以及需要提高代码的可读性、可维护性和可重用性的情况。根据项目的需求和规模,我们可以灵活选择适合的方式来组织和管理 JavaScript 代码。"


作者:打野赵怀真
来源:juejin.cn/post/7294171458032336906
收起阅读 »

规范化注释你的代码,成为一名优秀程序员的必经之路!

web
前言 想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。 正文 function phoneNumber(numbers) { return"("+numbers[0]+numbers[1]+numb...
继续阅读 »

前言


想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。


正文


 function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"+' '+numbers[3]+numbers[4]+numbers[5]+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

如果我直接丢出这一串代码,你第一眼看过来,心里肯定会想“什么玩意?这一坨代码是干什么用的!居然还需要我拖动横条?!”
但是我如果在它的上方加上这样一段注释,并中途给它换行两次,它就会变成这样


/**
* @func 返回固定格式的电话号码, (123) 456-7890
* @param array [1,2,3,4,5,6,7,8,9,0]
* @return (123) 456-7890
* @author xsir
*/

函数定义
function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"
+' '+numbers[3]+numbers[4]+numbers[5]
+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

你一眼看过去就能清楚的看到,这个函数的作用是返回固定格式的电话号码,
调用函数需要输入的形参的样式是一个数组,返回值为固定格式的电话号码,函数的作者是xsir。


在大公司做程序开发的时候,一整个大的项目需要多人协作一同完成,所以代码的可读性就显得尤为重要,甚至可以说,代码的可读性高于一切,因为在这个时候你的代码不仅仅是写给你自己看和用的,而是整个开发团队的同志们都需要能快速看懂并且调用的。


如果别人看你写的代码时,仅仅只有代码而没有任何其他注释,那么他就需要整体的阅读你写的所有代码,才能知道你写的函数是干什么用的,这就会浪费很多时间。“Time is money, efficiency is life!”


顺带一提,如果你使用的是'${}'的格式


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]}) ${numbers[3]}${numbers[4]}${numbers[5]}-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

运行上述代码,输出的结果是这样的:


image.png


为了提高代码可读性,你对它进行了换行


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`


那么输出结果就会变成这样


image.png


并没有达到预期的效果。所以在实战过程中使用'${}'需谨慎。


总结


在我们开发学习的过程时就要养成良好的编程素养,每次写完一块代码就写好这块代码的注释,做到看“码”知意。同时也要避免单行代码写的过长,尽量使你的代码不需要拖动横条就能看完。


作者:阡陌206
来源:juejin.cn/post/7293789288725889078
收起阅读 »

我是这样保持精力充沛的

精力管理就好比是计算机的内存清理,你以为关掉一些程序就行了,结果你还是卡成翔。 我的现状 雷猴啊,我是一个临期程序员。打过几年工,被好几个同事问过我为什么精力这么旺盛。 这两年我大多数情况都是早上8点前就到公司*(原本9点上班,后来调成8点半,晚上要是加班...
继续阅读 »

精力管理就好比是计算机的内存清理,你以为关掉一些程序就行了,结果你还是卡成翔。



我的现状


雷猴啊,我是一个临期程序员。打过几年工,被好几个同事问过我为什么精力这么旺盛。


01.jpeg


这两年我大多数情况都是早上8点前就到公司*(原本9点上班,后来调成8点半,晚上要是加班就可以多赚半小时了)*,属于工贼类型。


晚上回到家还有精力打游戏、看书、学习。


02.JPEG






为什么要提升精力


几年前我还是一只前端程序猿,当时刚从 JQueryVue 1.0,学完 Vue 1.0 发现公司的新项目开始用 Vue 2.0 ,和外部对接的项目是用 react 。那段时间真的有点疲于奔命的感觉,很累。一天到晚都想睡觉。睡多了又容易头晕。


03.GIF


逛技术论坛时发现有些大佬真的能精通前端三大框架,写出很多原理类的文章。真让人羡慕~~(生气)~~啊。


我开始怀疑是不是我的精力出现了问题。之后尝试过借助外物的帮助,比如每天靠喝咖啡续命,吃维生素提升精力之类的方法。


但这样会进入一个死循环:


喝完咖啡可能睡不着 -> 睡不着可能就会玩手机 -> 玩手机会越玩越晚 -> 导致第二天起不到床 -> 起不到床又怕会扣工资,然后强行起床 -> 上班没精神,然后喝咖啡续命  -> ......



要破掉这个死循环,就要提升自己的精力。


04.GIF






前期准备


世界上是存在拥有 少眠基因 的人,他们每天可能睡4~5个小时就元气满满的。听说只有1%的人拥有这种基因(羡慕了),但我大概率不属于这类人。我的精力是锻炼出来的。


05.jpg


我可是受过专业训练的!




我是一个执行力很弱的人,为了让自己精力旺盛点,我就画真金白银去买课来学。买了不学就亏大了。


06.jpg


这是课程里给出的金字塔模型。完完全全跟着课程去做应该会变得很强。


但实在太难了,即使是第一层我也不能100%按照课程所说的去做(我实在做不到啊啊啊啊)。


07.gif


举个例子,课程里讲到如果你是右撇子,那早上刷牙的时候就可以用左手刷牙。我试了,左手力量控制不好,出来的结果是一嘴血。


再举个例子,课程里建议早上刷牙的时候试试单脚站着刷牙。我试了,平衡力不好,呛了口牙膏(吞肚子里了)。


由此可见,不是所有方法都适合我。




我的做法


吃了几次亏,我觉得我还是选择性训练算了。如果完完全全跟着课程做能拿100分的话,那我的目标就定在30分好了。


只要大方向没错,我总会有提升的。


大方向是啥?就是课程目录。



  • 运动



  • 情绪


08.png


在每章中调几项自己能做到的,拿张纸出来列份清单每天跟着做,假以时日~嘿嘿嘿~




运动


运动能带来什么好处我就不啰嗦了(因为我不懂其中的原理)。


先说运动,从最简单的运动做起




1个俯卧撑


如果你和我说,长跑有很多好处,可以减肥,可以产生内啡肽,获得哔哩吧啦一大堆好处。那我可以坚持1天。


但课程用了一个很巧妙的方式让学员坚持锻炼,就是每天只做1个俯卧撑


08.JPG


这招真的灵。每天只做1个,这是很容易做到的,完全没有心理负担。


而且,都趴下了,真的只做1个吗?


就这样,我每天都抱着只做1个俯卧撑的心态开启了我的训练。因为没心理负担,所以很容易就坚持下去了,同时每天都给自己带来一个积极的心理暗示:我完成了我承诺的事




间歇训练


大部分程序员工友的工作应该都是比较忙的,要每天抽一段完整时间出来运动的可能性不大。但如果能在碎片时间里稍微动一动对自己还是有好处的。


举个例子,我在公司开会的时候我不会一直坐着,我主要是站着为主。有时候还会偷偷提纲(就是你理解的那个提纲😎)。我见有些工友还买了电脑升降架,这也是不错的招式。


再来点例子,刚睡醒的时候搓脸、伸懒腰,这些需要动的操作我都归到运动里面。


日常通勤我也改成 公交地铁 + 骑共享单车,早上上班是公交或者地铁,这样不会那么容易出汗,回到公司就不会一身臭汗味。晚上下班我会选择骑共享单车,这肯定算是正式运动了,尤其是早期的共享单车骑起来真的累。





课程介绍到,吃饭不要吃太饱,尽量少吃高油高糖高碳水的食物。


但作为广东人,要我戒口?真的很难!


09.GIF




我做不到天天保持健康饮食,但工作日我还是会尽量克制。尤其是工作日的午餐,会直接影响你下午的状态。


10.gif


如果中午吃太饱,吃过多高碳水化合物(比如米饭、面条等)下午就很容易困,饭气攻心嘛。


引用课程的内容:



因为高碳水化合物的食物都容易变成糖,让血糖快速升高,从而导致胰岛素快速分泌,引发色氨酸进入大脑。


色氨酸是合成褪黑素的重要原料,褪黑素越多人就越困。



那么,如果我们想快点入睡的话是不是可以在睡前多吃米饭呢?我不建议,这样睡觉太难受了~




回归正题,应该怎么吃,可以从以下几个方面着手:



  1. 少吃多餐:不让血糖那么容易大起大落。

  2. 多吃低糖高营养食物:首选绿叶蔬菜,然后再到优质蛋白,最后才是高碳水化合物食物。

  3. 多喝水。




少吃多餐


第1点的意思两餐之间的间隔时间不要太长,不然血糖容易大起大落,这样更容易累。


除了早中晚三餐,还可以加上午茶和下午茶。


上午茶和下午茶指的不是奶茶,是指可以吃一些坚果、水果(蓝莓草莓之类的水果)。


早餐可以吃一些高纤维高蛋白食物。


午餐建议吃多点蔬菜,肉类尽量选高质量的蛋白质,比如鸡肉鱼肉。


晚餐的话可以吃点碳水化合物,稍微提高一点血糖,促进晚上睡眠。




多吃素菜,少吃碳水化合物


11.jpg


图片来自课程。





高质量睡眠是恢复精力的关键,这里说的并不是要睡很长时间才是高质量睡眠,是要睡得好才行。


怎样才能睡得好呢?什么褪黑素、深睡、浅睡、快速动眼期这些概念我就不介绍了。主要讲讲我的做法。




睡前不要玩手机


睡前不要玩手机。这句话有2个关键点:



  1. 睡前

  2. 不要玩手机




“睡前”是指多长时间呢?最好是1小时。


我手机的常用软件主要是:微信、企微、微信读书、电话、地图。


我不喜欢玩手游,也不喜欢上微博(连微博都没安装),所以我睡前很容易做到不碰手机。


听说有些工友喜欢睡前在床上打打游戏看看小说,这可能不是好习惯。


如果你实在坚持不了睡前1小时不玩手机,那可以试试睡前半小时不玩,再不行就缩减到10分钟。


还是用回运动篇提到的方法,前期先用最容易实现的标准,坚持一段时间。比如坚持半个月睡前10分钟不碰手机。之后再慢慢加长这个时间。




“不要玩手机”指的是不要在睡前有过多的娱乐项目。比如打游戏、看小说、听激昂音乐等。




冥想


冥想听上去很玄学的感觉。其实我也不知道有没有用,我只是照做了而已。


12.jpg


我所做的冥想仍然是低配版。我上过正念冥想课,确实学到很多冥想的方法,比如如何呼吸、如何思考。


但在睡前我不想搞得那么复杂(怕太复杂自己处理不来,直接睡死过去😄),睡前我只做观察呼吸这件事。


选择一个舒适的姿势躺在床上,合上眼睛,鼻子呼气时心里就想着“呼气”,鼻子吸气时心里就想着“吸气”。


这样做会确实让我平静了很多,不知不觉就睡着了。


我不清楚冥想对我的睡眠有多大帮助,但这样做让我舒服。




情绪


情绪会影响人的健康,同样会影响人的精力。


这里我需要引用一下课程里提到的3条关于情绪的定律:



  1. 人的脑子在一个时段只能主要存在一种情绪;

  2. 相比正面情绪来说,人脑更容易产生负面情绪;

  3. 人可以通过自主的训练来控制情绪。


简单解释一下:


第一条的意思是正常的人在同一时段内,很难开心和惊恐同时存在。


第二条的意思是你赚了100元又失去了100元,其实对于你来说并没什么变化,但你可能会不开心。就是对“损失”比对“获得”更敏感。


第三条的意思好心态是可以训练出来的。




我的做法:



  1. 坚持运动。运动能让我变得更积极、更正面。

  2. 感恩。每天都想想一直以来遇到过的好事。比如谢谢妈妈带我到这个世上。习惯感恩后,我心态确实变得更平和了,而且也更容易发现别人的优点。

  3. 奖励。完成一项任务后给自己小小奖励。




我在“情绪”这个段位不高,能介绍的方法并不多。我的主题是 平和。尽可能朝着这个方向去做。




其他


深呼吸


课程里讲到每天早上起床清醒后可以大喊。


这招我也做不到。在城市生活,放个屁大声点都会被邻居投诉,大叫就更加不可能做到的。


于是我改变了策略:假装自己在大叫


早上起来这么做确实能更快速清醒,最好在阳台做。


晒太阳


自然光确实比人造光舒服。我的工位比较暗,大白天都要靠灯管照明。工作日午休完我会走去窗边晒晒太阳,让自己清醒一点。






永远年轻,永远发疯。


13.JPG


作者:德育处主任
来源:juejin.cn/post/7293788137662316578
收起阅读 »

重生!入门级开源音乐播放器APP —— 波尼音乐

前言 不知道是否还有人记得,7年前的那个 「Android开源在线音乐播放器——波尼音乐」? 本来只是作为毕设项目,没想到很多人感兴趣,就断断续续的在维护,当时在网络上找到了一个百度开放的在线音乐 API,勉强实现了本地 + 网络播放能力。 可惜没过多久 AP...
继续阅读 »

前言


不知道是否还有人记得,7年前的那个 「Android开源在线音乐播放器——波尼音乐」?


本来只是作为毕设项目,没想到很多人感兴趣,就断断续续的在维护,当时在网络上找到了一个百度开放的在线音乐 API,勉强实现了本地 + 网络播放能力。


可惜没过多久 API 就被百度关闭了,从此以后便黯然失色,一度沦落为本地播放器,在这个万物互联时代显得有点落寞,我也因此没有太多更新的动力。


最近无意间发现开源社区已经有大神发布了「网易云音乐 API」,喜出望外,遂有了重整旗鼓的想法,顺便对之前的架构做一次重构,来一次脱胎换骨的升级!


经过3个多月断断续续的开发,今天,它来了!


展示


视频


截图
image.jpg


功能



后续可能会根据需要增加功能




  • 本地功能

    • 添加和播放本地音乐文件

    • 专辑封面显示

    • 歌词显示,支持拖动歌词调节播放进度

    • 通知栏控制

    • 夜间模式

    • 定时关闭



  • 在线功能

    • 登录网易云

    • 同步网易云歌单

    • 每日推荐

    • 歌单广场

    • 排行榜

    • 搜索歌曲和歌单




体验



欢迎大家体验,如果发现功能问题或兼容性问题,可以在本文评论或者 GitHub Issue



环境要求



  • Android 手机

  • 电脑(非必须)


安装步骤



  1. 搭建网易云服务器

    clone NeteaseCloudMusicApi 服务端项目到本地,根据项目说明安装并运行服务,需要确认电脑和手机处于同一局域网

  2. 安装 APP

    点击下载最新安装包

  3. 设置域名

    打开 APP,点击左上角汉堡按钮,打开抽屉,点击「域名设置」,输入步骤1中的地址(包含端口)

  4. 设置完成即可体验



没有电脑,如何体验?


其实有一些同仁已经将网易云服务部署到公网了,我们可以直接用🐶。


这里不方便直接贴地址,下面教大家如何找到可以用的服务:


用 Google 搜索「网易云音乐API」,点击结果,如果页面是下图这样(注意:非作者的 GitHub.io 页面),恭喜,你找到了可以直接使用的服务,拷贝地址栏链接,输入到步骤3即可。


screenshot-20231026-152715.png



源码


wangchenyan/ponymusic: Android online music player use okhttp&gson&material design (github.com)


欢迎感兴趣的朋友 Star、Fork、PR,有你们的支持,我会非常开心😄


开源技术



站在巨人的肩膀上




作者:王晨彦
来源:juejin.cn/post/7294072229003952143
收起阅读 »

如何写出让同事崩溃的代码

web
    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。 一、方法或变了名字随便取     首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或...
继续阅读 »

    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。


一、方法或变了名字随便取


    首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或者方法到底是干嘛用的。哎!对,就是让他去猜,哈哈哈。
来来来,空说没意思,举个栗子图片


    假设现在想要点击某个input框时,显示一个自定义的组件用于选择选择时间。


    正常的写法如下:定义一个 toggleDatePicker 方法
image.png


    这个一看就知道是时间选择器的显示切换方法。


    但是呢,我偏不,定义成下面这样:让同事摸不着头脑,哈哈哈


image.png
当看到很多这样的方法名或变量名时,同事的表情估计时这样的图片图片图片


接下来,第二招图片图片图片


二、方法体尽可能的长,长到不能在长


    这一步至关重要,将所有逻辑全部放在一个方法中写完,坚决不分步骤,不按逻辑一步步拆分方法。让同事为我的超长方法体感到叹为观止,默默流泪。


    老规矩,上栗子


    假设现在有个方法需要处理比较复杂(需要递归,而且每层数据有不同的类型)的json格式的数据回显到页面上(这是用于拼一些条件)。数据格式大概是这样的


[
{
type: "group",
relation: "or",
conditions: [
{
type: "condition",
field: {
name: "员工状态",
id: 12345678
},
logic: "=",
val: 1,
relation: "and"
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2011-07-09",
relation: "and"
}
]
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2001-07-09",
relation: "and"
}
]

    由于上面的 json 数组格式中是一个对象,对象都有 type 属性,一种是 group 类型(表示是一组条件),一种是 condition 类型(表示是真正的条件内容)。


    因此,正常情况下,我们应该是遍历数组,不同的类型分别用不同的方法去处理,并且在处理条件中各个部分的时候分别处理。如下所示:


image.png


image.png


    但是呢?咱主题是啥,让同时崩溃啊,怎么能把方法拆成这样清晰的逻辑呢。图片图片图片


    来给你看看怎么让同事对你的代码叹为观止


image.png


image.png


image.png


image.png


image.png


image.png


    怎么样,牛不牛,一个方法写完所以逻辑,从1825行一口气写到2103行,足足... 2103 - 1825 是多少来着,3减5 不够,向前借位 ,嗯。。。278 行。
****怎么样,有没有被哥的机智震惊到,如此代码。同事看到肯定心生敬佩,连连称绝。此时想到得到同事应该是这样的表情 图片图片图片


    同事还没进到方法体里面,就已经被我的 迷之方法名 和 超长方法体 所折服,接下来就让他在方法体里面快乐的遨游吧
    


    接下来,继续让同时崩溃。


三、坚决不定义统一的变量


    这个怎么说呢,就是因为有点懒,所有很多代码直接复制粘贴,多么的方便快捷。


    正常情况下,如果某个我们需要的对象在是其他对象的属性,并且层次很深,我们先定义一个变量来接收这个对象,再对这个对象操作。


例如:


let a = {
b: {
c: {
d: {
name: "我是最里面的对象"
}
}
}
}

    我们要对d对象进行很多次的操作时,一般先将d赋值给一个变量,然后对变量操作。如下:


var dOfA = a.b.c.d;
dOfA.name = "我现在被赋值给dOfA ";
dOfA.data = 1;
dOfA.other = false;

    但是呢,我就不这么干,就是要写得整整齐齐


a.b.c.d.name = "就要这么干,你打我呀";
a.b.c.d.data = 1;
a.b.c.d.other = false;

    老规矩,没有 实际的 栗子 怎么能说的形象呢,上图


    正常写法:


image.png


    我偏要这么写


image.png


    多么的整齐划一,


    全场动作必须跟我整齐划一


    来左边儿 跟我一起画个龙


    在你右边儿 画一道彩虹


    来左边儿 跟我一起画彩虹...


    咋突然哼起歌来了,不对,咱是要整同事的,怎么能偏题。


    继续,此时同事应该是这个表情图片图片图片


    然后,方法体里面只有这么点东西怎么够玩,继续 come on


四、代码能复制就复制,坚决不提成公用的方法


    代码能 CV ,干嘛费劲封装成方法,而且这样不是显得我代码行数多吗?图片图片图片


    就是玩儿,就是不封装


    来,上栗子


image.png


image.png


image.png


    看到没有,相同的代码。我在 1411行 - 1428行 写了一遍, 后面要用,在1459行-1476行复制一遍


    这还不够?那我在1504-1521行再复制一遍


    这下,爽了吧,哈哈哈


    就是不提方法,就是玩儿,哎! 有意思


    这个时候同事估计是这样的吧图片图片图片


    怎么样,是不是很绝?不不不,这算啥


    虽然以上这些会让看我代码的同事头疼,但是,只要我在公司,他们还会让我改啊。怎么能搞自己呢。


    最后一步


五、离职


    洋洋洒洒的写完代码,尽早离开。够不够绝,哈哈哈


六、申明:


    以上场景纯属个人虚构的,单纯为了给文章增加点乐趣。写这个文章的目的是让各位程序员兄弟尽量避免写这种难以维护的代码。真的太痛苦了!代码质量、代码重构真的是编程过程中很重要的一个步骤。不能抱着能用就行的心态。还是要对自己有一定的要求。只有你看得起自己的代码,别人才有可能看得起你的代码。加油吧!各位


作者:猩兵哥哥
来源:juejin.cn/post/7293888785400856628
收起阅读 »

程序员有没有必要成为业务领域专家 ?

看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。 于是,我做了如下的回答: 非常有必要。 1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。 乔布斯,你是一个聪明又有影响力的人。但是很...
继续阅读 »


看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。


于是,我做了如下的回答:


非常有必要


1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。



乔布斯,你是一个聪明又有影响力的人。但是很遗憾也很明显,很多时候你根本不知道自己在做什么。我希望你能用清楚的语言解释一下 Java 编程语言以及其变种是如何阐述 OpenDoc(开源技文档)内置的一些想法。等你说完以后,你能不能跟我们说一说你自己过去七年都干了些什么?



面对这样犀利的提问,乔布斯平静的喝了一口水,低头沉思了几秒,开口这样回答道:



有时候你能取悦一部分的人,但是当你想要作出改变的时候,最难的是某些事情别人做的是对的。我相信 OpenDoc 肯定有一些功能,没有任何其他东西能做到。我其实也不太懂,我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面,例如让你每年一个产品能够卖百八十亿美元。


我经常发现,你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去。在座的没有人比我犯过更多这样的错误,我也搞到伤痕累累,我知道这就是原因,当我们尝试去为苹果思考战略和愿景,都是从能为用户带来什么巨大利益出发,我们可以给用户带来什么,而不是先找一群工程师,大家坐下来,看看我们有什么吊炸天的技术,然后怎么把它卖出去。



我非常认同乔布斯的话。


程序员有的时候沉迷在自己的世界里,执拗的以为“代码就是全部”


但现实并非如此,编码的目的是创造产品或者提供服务,从而在这个商业社会实现更大的价值


而程序员成长为业务领域专家,能够更加深刻的理解公司的产品或者服务,从而更有优势为公司做出贡献。当个人的贡献上升时,公司的认同和利益也会随之而来。




这个回答一天内得到不少赞同,也是我意想不到的,因为我并不觉得我回答得好,看来很多同学都认可这个观点。



熟悉我的朋友都知道 ,我对技术非常有激情,曾经也认为技术意味着一切。


只是后来,工作中遇到越来越多的挫折,很多好朋友也友善的提醒我,不要太执着于技术,我也越来越认识到自己认知的局限性


我不断的去读书、听演讲、思考,依稀之间得到一个结论:"一个 IT 公司的成功 ,技术固然是重要的一环,而公司的产品、用户人群、经营模式是另一个我很少关注且非常重要的维度"。


偶然间我看了乔布斯的一个视频,视频的两句话让我醍醐灌顶。




  • 我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面




  • 你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去




懂业务是一种认知模式,人的能力是多层次的,技术和懂业务并非互斥的关系。


亲爱的程序员朋友,技术是我们的立身之本,但是业务同样重要 , 真诚的希望你做一个既懂技术又懂业务的工程师。




如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!


作者:勇哥java实战分享
来源:juejin.cn/post/7246224746005954616
收起阅读 »

一文学会请求中断、请求重发、请求排队、请求并发

web
大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处! 以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。 阅读下文需要了解前置知识:promise、class、axios...
继续阅读 »

大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处!

以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。

阅读下文需要了解前置知识:promise、class、axios


请求中断


1.判定相同请求:请求url、请求方法、请求params参数、请求 body参数,四个值都相同,则认为是一个相同的请求。

2.判断请求中断:在上一个相同请求还没有得到响应前,再次请求,则会自动中断。


image.png


请求重发(无感刷新token)


1.当前请求返回401时,执行刷新token。

2.当同时存在多个请求返回401时,可在类中维护一个静态变量保存请求刷新接口的promise,防止多次调用刷新token。

3.RetryRequest类实例化参数:

   instance:请求实例对象

   success:刷新成功回调函数

   error:刷新失败回调函数

image.png


请求排队



  1. queue:请求等待队列。

  2. isWating:是否正在等待上个请求响应。

  3. add:向队列里加入一个等待请求的promise的resolve方法,执行该方法可立刻发送下一个请求 。

  4. next:执行下一个请求方法,在上一个请求响应后调用。

    image.png


响应处理


image.png


axios请求实例


image.png


测试


1.请求中断测试


快速点击test请求按钮多次


image.png


2.刷新token请求重发测试


(1)当用户没有登录请求接口时


image.png


(2)当用户登录后,accessToken过期,但refreshToken还没过期调用接口时


image.png
在调用刷新token接口成功后,将重发失败的test接口


(3)当refreshToken过期后调用接口时


image.png
这时已经无法刷新token了,只能乖乖跳转到登录页面了。


3.请求排队测试


(1)没有使用请求排队时


场景:当输入框输入关键字实时查找内容时,由于网络原因,可能会出现先请求的后响应的请求,导致请求错乱。

如下,模拟网络请求延迟:
image.png


当输入框依次输入1、2、3、4、5时,期望的返回结果应该是1,12,123,1234,12345。
但确得到了以下的结果:
image.png


(2)使用请求队列时


在网络请求的waterfall列可以清晰看到,当上一请求完成才会执行下一请求,直到等待队列执行完成。
image.png


源码


后端接口


var express = require('express');
var router = express.Router();

const access_token = 'access_token'
const refresh_token = 'refresh_token'
// token有效期(单位毫秒)
const tokenValidTime = 1000*2
// 刷新token有效期
const refreshTokenValidTime = 1000*5
// 登录时间,模拟token过期
let loginTime;
// 模拟判断token是否过期
const IsTokenExpired = () => {
if(new Date().getTime() > loginTime + tokenValidTime) {
return true
}
return false
}
router.post('/login', (req, res) => {
loginTime = new Date().getTime()
res.json({
access_token,
refresh_token
})
})

router.post('/refresh-token', (req, res, next) => {
const refreshToken = req.headers.authorization
console.log('refresh-token', refreshToken)
if(refreshToken !== refresh_token) {
return res.status(401).json({
msg: ' refreshToken不正确!'
})
}
if(new Date().getTime() > loginTime + refreshTokenValidTime) {
return res.status(401).json({
msg: ' refreshToken已过期,请重新登录!'
})
}
loginTime = new Date().getTime()
res.json({
access_token: 'access_token',
refresh_token: 'refresh_token'
})
})

router.get('/test', (req, res, next) => {
const token = req.headers.authorization
if(token !== access_token) {
return res.status(401).json({
msg: '没有访问权限'
})
}
if(IsTokenExpired()) {
return res.status(401).json({
msg: 'token已过期'
})
}
res.json({
name: '哈哈'
})
})

router.get('/random', (req, res) => {
const keyword = req.query.keyword
setTimeout(() => {
res.json({
value: keyword
})
// 5秒内随机返回,测试网络请求延迟效果
}, Math.random()*5000);
})

module.exports = router;

前端


Index.js


import axios from "axios"
import AbortRequest from './hooks/AbortRequest'
import ResponseHanlder from "./hooks/ResponseHanlder"
import { getAccessToken, getRefreshToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'
import { useRequestKey } from "./hooks/useRequestKey"
import RequestQueue from "./hooks/RequestQueue"
import { isAddQueue } from '@/api'

export const baseURL = '/api'
const timeout = 6000

// 创建axios实例
const instance = axios.create({
baseURL,
timeout
});

// 创建中断请求控制器
const abortRequest = new AbortRequest()
// 创建响应处理器
const responseHandler = new ResponseHanlder(instance)
// 创建请求队列排队实例
const requestQueue = new RequestQueue()

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
console.log('在发送请求之前做些什么', config)
// 在发送请求之前做些什么
if(config.url !== '/login') {
const token = config.url === refreshTokenUrl ? getRefreshToken() : getAccessToken()
config.headers.Authorization = token
}
// 刷新token接口不用创建取消请求,已经再RetryRequest类维护静态属性
if(config.url !== refreshTokenUrl) {
abortRequest.create(useRequestKey(config), config)
}
// 加入请求等待队列
if(isAddQueue(config)) {
return requestQueue.add(config.url, config)
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
const config = response.config
console.log('响应成功', response)
abortRequest.remove(useRequestKey(config))
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return responseHandler.success(response)
}, function (error) {
const config = error.config
console.log('响应错误', config)
if(config) {
abortRequest.remove(useRequestKey(config))
}
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if(axios.isCancel(error)) {
return Promise.reject('已取消重复请求!')
}
return responseHandler.error(error)
});

export default instance;

AbortRequest.js


// 重复请求中断类
class AbortRequest {
constructor() {
// 请求中断控制器集合
this.list = new Map()
}
// 创建中断请求控制器
create(key, config) {
const controller = new AbortController();
config.signal = controller.signal
// 集合中存在当前一样的请求,直接中断
if(this.list.has(key)) {
controller.abort()
} else {
this.list.set(key, controller)
}
}
// 请求完成后移除集合中的请求
remove(key) {
this.list.delete(key)
}
}
export default AbortRequest

RequestQueue.js


/**
* 相同url请求队列,排队执行维护类
*/

class RequestQueue {
constructor() {
// 请求等待队列
this.queue = {}
// 正在等待上一请求执行中
this.isWating = false
}
add(url, config) {
return new Promise((resolve) => {
const list = this.queue[url] || []
if(this.isWating) {
// 当前请求url存在等待发送的请求,则放入请求队列
list.push({ resolve: () => resolve(config) })
} else {
// 没有等待请求,直接发送
resolve(config)
this.isWating = true
}
this.queue[url] = list
console.log('list', list)
})
}
// 响应处理
next(url) {
this.isWating = false
// 拿出当前请求url的下一个请求对象
if(this.queue[url]?.length > 0) {
const nextRequest = this.queue[url].shift()
// 执行请求
nextRequest.resolve()
}
}
}

export default RequestQueue

RetryRequest.js


/**
* 无感刷新token类
*/

class RetryRequest {
// 解决存在多个并发请求时,重复调用刷新token接口问题
static refreshTokenPromise = null
constructor({
instance, // axios实例
success, // 刷新token成功执行的回调函数
error // 刷新token失败执行的回调函数
}
) {
this.instance = instance
this.success = success
this.error = error
}
/**
* @param config 当前请求对象,等待token刷新完成再重复执行
* @param refreshTokenApi 刷新token方法
*/

useRefreshToken(config, refreshTokenApi) {
if(!config.headers.Authorization) {
this.error()
return Promise.reject('token不存在!')
}
return new Promise((resolve, reject) => {
if(!RetryRequest.refreshTokenPromise) {
// refreshTokenPromise不为null,则当前正在执行刷新token方法,不再重复调用
RetryRequest.refreshTokenPromise = refreshTokenApi()
}
RetryRequest.refreshTokenPromise.then(res => {
// 刷新token成功
this.success(res)
// 重新发送请求
this.instance(config).then(data => {
resolve(data)
}).catch(err => {
// 重发失败
reject(err)
})
}).catch(err => {
// refreshToken失效或刷新token失败
this.error()
reject(err)
}).finally(() => {
// 刷新token调用完成,重置
RetryRequest.refreshTokenPromise = null
})
})
}
}

export default RetryRequest

ResponseHanlder.js


import RetryRequest from './RetryRequest'
import { refreshToken as refreshTokenApi } from '@/api/index'
import { getRefreshToken, setRefreshToken, setAccessToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'

/**
* 响应处理类
*/

class ResponseHanlder {
constructor(instance) {
this.retryRequest = new RetryRequest({
instance,
success: (res) => {
const { access_token, refresh_token } = res
setAccessToken(access_token)
setRefreshToken(refresh_token)
},
error: () => {
console.log('刷新token失败!')
// 执行失败逻辑...
}
})
}
// 请求正常响应方法
success(response) {
// 对响应数据做处理
return response.data
}
// 请求错误响应方法
error(error) {
const status = error.response?.status
// 当前返回401,且不是调用刷新token接口响应的(避免后端刷新token失败返回401导致死循环的情况)
if(status === 401 && error.config.url !== refreshTokenUrl) {
return this.retryRequest.useRefreshToken(
error.config,
() => refreshTokenApi(getRefreshToken())
)
} else {
return Promise.reject(error.response)
}
}
}

export default ResponseHanlder

request.js


import instance from './index'

class Request {
constructor() {

}
get(url, params, args) {
return instance.get(url, {
params,
...args
})
}
delete(url, params) {
return instance.get(url, {
params
})
}
post(url, data) {
return instance.post(url, data)
}
put(url, data) {
return instance.put(url, data)
}
}

export default new Request();

结语


还有一个控制请求并发数量还没进行扩展,相信大家了解了请求排队的思路后,实现请求并发控制也不是什么难事了。



无感刷新token参考文章:juejin.cn/post/728974…



作者:云上客人
来源:juejin.cn/post/7293806405650464808
收起阅读 »

判断鼠标从哪个方向进入元素

web
我们需要实现的效果图 理清需求 拿到效果图的第一步,理清下需求~ 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域? 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。 最后,最难的是如何判断鼠标进...
继续阅读 »

我们需要实现的效果图


image.png


理清需求


拿到效果图的第一步,理清下需求~



  • 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域?

  • 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。

  • 最后,最难的是如何判断鼠标进入的时候会落在我们划分好的上下左右四个区域?


思路



  • 首先我们先来划分下四个区域,一般划分的都如下图


image.png




  • 图里面有四个三角形,每个三角形代表的是一个方向,所以问题简化为如何在一个矩形里,根据对角线划分区域。由于元素存在坐标系,也就是X、Y轴,所以问题再次抽象成,如何得到两条对角线的线性函数。(初高中数学问题。)




  • 最后的问题我们就要来搞定判断鼠标落点的问题,首先我们知道我们可以在元素的鼠标事件中通过event得到鼠标的pageX和pageY,再配合元素的offsetLeft和offsetTop就可以得到鼠标在元素中的坐标。综合一下就变成了,我有一个坐标,且我知道对角线的函数表达式,请问我如何知道我这个坐标是在函数的下面还是上面?




  • 当然也许描述的比较抽象,我们可以类比一个例子,我现在有一个坐标(2,1),有一个函数y=x,值域大于0(既y>0),定义域大于0(既x>0),求该坐标在y=x的函数下面还是上面?(是不是感觉到了线性规划得到最优解的味道,对,少年,没有错,就是这样。)这里我们只要把坐标中的x值代入函数,然后判断代入的结果是否大于坐标的y值,如果大于则在函数下面,小于则在函数上面,什么?你问等于怎么办?当然是在函数上面,该坐标即在上面又在下面,所谓薛定谔的坐标是也(当然是在函数上了)。




  • 然后我们是不是可以扩展下,如果存在多个函数,再加上逻辑判断经常用的交集,并集是不是又有新的思维出现了呢?好了,这边就不再扩展了,下面直接上实现代码吧。




实现代码


注意:该demo只是简单的demo,其中有很多可以优化的地方,比如组件化,变量优化,利用发布订阅模式,实现事件联动


<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css" >
.ct{
height: 100px;
width: 100px;
border:1px red solid;
}
</style>
</head>
<body>
<div class="ct" onmouseover="fun1(event);" onmouseout="fun2(event);">

</div>
<script type="text/javascript">
//当然这样绑定事件函数是不对的
var div=document.getElementsByTagName("div")[0];
function fun1(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方进入");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方进入");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边进入");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边进入");
//todo
}

}
function fun2(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方离开");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方离开");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边离开");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边离开");
//todo
}

}
</script>
</body>
</html>

作者:洛漓
来源:juejin.cn/post/7293820517374820352
收起阅读 »

彻底搞懂闭包

web
每次面试都问,每次都背;每次都背的不错,每次都不太理解。 定义 闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。 一个简单的例子认识闭包: function init() { var...
继续阅读 »

每次面试都问,每次都背;每次都背的不错,每次都不太理解。


定义


闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。


一个简单的例子认识闭包:


function init() {
var name = 'wendZzzoo';
function getName() {
console.log(name)
}
getName()
}
init()


使用场景


那闭包有什么作用呢?但从上面这个简单的例子中,似乎很难发现这样写,也就是闭包这样的写法的用途。


数据封装和隐藏


通过使用闭包,可以创建一个作用域限定的环境,以保护变量不受外部的访问和修改。这样可以防止变量被意外修改,避免命名冲突和全局污染,提高代码的可维护性和可读性。


举个例子,定义一个计数器函数,用来某些场景下计算次数。


没有使用闭包的示例


let count = 0
function increment() {
count++
console.log(count)
}
increment()
increment()


上述代码是实现了计数器的需求,但是代码存在风险,count变量是全局定义的,在后续开发中或者是其他人维护时可以轻易修改这个变量,导致bug出现。


使用闭包的示例


function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}

const counter = createCounter();
counter();
counter();


在上述示例中,createCounter函数返回了一个内层函数increment,该函数可以访问并递增count变量。外层函数的作用域被封装在闭包中,外部无法直接访问和修改count变量。


这里可以衍生思考一下,count变量封装在闭包中只能递增,外部无法修改,那该如何重置或者递减count呢?


其实需要新增的逻辑也可以封装到闭包里,以重置count为例:


const counterModule = (function() {
let count = 0;
function increment() {
return ++count;
}
function reset() {
count = 0;
}
return {
increment,
reset
};
})();

console.log(counterModule.increment());
console.log(counterModule.increment());
counterModule.reset();
console.log(counterModule.increment());


在上面的例子中,使用立即调用函数表达式(IIFE)创建了一个闭包,内部定义了count变量和两个操作它的函数incrementreset。通过返回一个包含这些函数的对象,实现了对count变量的封装和控制。


保持数据状态


通过闭包,内层函数可以访问和持有外层函数的变量,即使外层函数执行完毕,这些变量依然存在于内层函数的词法环境中,从而实现了数据状态的保持。


这个使用场景可以算是上一个的延申,在上述示例代码中添加传参,就可以起到了数据状态保持的目的。


函数柯里化


闭包使得函数可以返回另一个函数作为结果,从而形成函数工厂的模式。通过在内层函数中访问外层函数的参数或变量,可以创建具有不同参数或上下文的函数。这种技术称为柯里化,其目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。


举个例子,定义一个求矩形面积的函数。


不使用柯里化的示例


function getArea(width, height) {
return width * height
}
const area1 = getArea(10, 20)
console.log(area1)
const area2 = getArea(10, 30)
console.log(area2)
const area3 = getArea(10, 40)
console.log(area3)


上面代码里,假设我们需要这个计算矩形面积的函数,来计算宽度总是10的多种情况,那就需要多次调用getArea函数传入相同的宽度参数,且在维护的时候,假设需要统一修改宽度为20,就需要重复修改每一次调用时宽度的传参,这样重复的工作在力求优雅的情况下看来是不合适的。


使用闭包柯里化的示例


function getArea(width) {
return height => {
return width * height
}
}

const getTenWidthArea = getArea(10)
const area1 = getTenWidthArea(20)
console.log(area1)
const area2 = getTenWidthArea(30)
console.log(area2)
const area3 = getTenWidthArea(40)
console.log(area3)


如果有需要宽度改变的情况,也可以轻松复用


const getTwentyWidthArea = getArea(20)


再举个例子,定义一个打印日志的函数。


function createLogger(prefix) {
function logger(log) {
console.log(`${prefix}: ${log}`);
}
return logger;
}

const exportWarnning = createLogger('warnning');
exportWarnning('这是一个警告日志');

const exportError = createLogger('error');
exportError('这是一个错误日志');


通过调用createLogger函数并传递不同的参数,可以创建具有不同日志前缀的logger函数。


通过上述两个柯里化的例子,可以发现柯里化是一种技术更多是一种提倡,使用这样的技术可以让你的代码更有维护性。


模拟私有化方法


私有方法是将某些函数或变量限定在一个作用域内,外部无法直接访问。


function makeCounter() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value());
Counter1.increment();
Counter1.increment();
console.log(Counter1.value());
Counter1.decrement();
console.log(Counter1.value());
console.log(Counter2.value());


上述代码通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式


两个计数器 Counter1 和 Counter2 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量


注意事项


闭包是一种强大的特性,但滥用闭包可能导致代码可读性和性能方面的问题,因此需要注意的是:



  1. 避免不必要的闭包,只有在确实需要保留状态或隐藏数据时才使用闭包。不要为了使用闭包而创建不必要的函数嵌套,盲目使用闭包并不会让你的代码看起来更高级。

  2. 要注意内存管理,闭包会持有对外部作用域的引用,可能导致内存泄漏。确保在不再需要闭包时,手动解除对外部作用域的引用,以便垃圾回收器能够正确处理。

  3. 特别要小心循环中的闭包,闭包会捕获循环变量的引用,可能导致意外结果。可以使用立即调用函数表达式(IIFE)或函数绑定来解决。

  4. 换一种解决方案,可以使用模块模式,如果需要封装私有方法和变量,考虑使用模块模式或其他模块化工具,如ES6模块。这样可以更清晰地定义私有和公共部分,并提供更好的可维护性和可测试性。


内存泄漏


闭包可以引起内存泄漏的情况,通常是涉及对外部作用域的引用。当函数形成闭包时,它会持有对其包含作用域的引用,这可能导致无法释放被闭包引用的内存。


可能导致内存泄漏的情况:



  1. 未及时释放闭包,如果闭包持有对外部作用域的引用,但不再需要使用闭包时,如果没有显式地解除对外部作用域的引用,闭包将继续存在并持有外部作用域中的变量。

  2. 当闭包和其包含作用域之间存在循环引用时,可能导致内存泄漏。例如,如果闭包中引用了一个对象,而该对象又持有对闭包的引用,这将导致它们互相引用,无法被垃圾回收。

  3. 闭包中引用了全局变量,闭包将一直存在,即使在不再需要闭包时也无法释放。这种情况下,全局变量将一直保持活动状态,无法被垃圾回收。


为避免闭包引起的内存泄漏,建议:



  1. 及时解除引用,当不再需要使用闭包时,确保手动解除对外部作用域的引用。只需要将闭包中引用的变量设置为 null 或重新分配其他值,以便垃圾回收器能够正确处理。

  2. 尽量避免闭包和其包含作用域之间的循环引用。确保在闭包中不引用外部对象,或者在外部对象中不引用闭包,以避免循环引用导致的内存泄漏。

  3. 只在确实需要保留状态或隐藏数据时使用闭包,在不需要闭包的情况下,使用适当的作用域(例如局部变量或模块作用域)来防止不必要的内存占用。


内存泄漏的发生并不一定是由闭包引起的,还可能涉及其他因素,但是,闭包在不正确使用的情况下容易导致内存泄漏问题。


作者:wendZzoo
来源:juejin.cn/post/7293805895918207026
收起阅读 »

📷纯前端也可以实现「用户无感知录屏」?

web
前言 要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。 但该方法会在浏览器弹出...
继续阅读 »

前言


要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。


但该方法会在浏览器弹出一个授权窗口,让用户选择要分享的内容,这不可实现“无感知”。


image.png


如果真正做到无感知,那我们就不能借助浏览器或者系统系统的能力了。我们能做的就只能是通过js去操作了。


要在页面内直接录制视频似乎并不容易,没有现成的开源库可以使用,也没有很好的想法。


那我们换一个思路,视频是由帧组成的,我们是否可以不断的截图,然后组合成一段视频?好像是可以的。


下载.jpeg


效果


mp4.gif


页面


先写一个简单的页面:


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

<head>
<meta charset="UTF-8">
<title>Canvas视频录制</title>
<link rel="stylesheet" href="styles.css">
</head>

<body>
<main>
<div class="buttons">
<button class="start-btn">开始录制</button>
<button class="pause-btn">暂停录制</button>
<button class="resume-btn">继续录制</button>
<button class="stop-btn">结束录制</button>
</div>
<div id="box">
<section class="content">
<h2>TODO LIST</h2>
<div class="background-div">
<button class="background-btn">切换背景颜色</button>
</div>
<div id="todo-form">
<input type="text" class="input-field" placeholder="输入待办事项">
<button type="submit" class="submit-btn">提交</button>
</div>
<div class="list"></div>
</section>
</div>
<img src="" alt="" class="hidden">
</main>

<script src="<https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.min.js>" defer></script>
<script src="canvas.js" defer></script>
</body>

</html>

截图


实现网页的截图操作,最常用的库是 html2canvas用,它可以将网页中的 HTML 元素转换为 Canvas 元素,并将其导出为图像文件。在浏览器中捕获整个页面或特定区域的截图,包括 CSS 样式和渲染效果。


const canvasFunction = () => {
html2canvas(box).then(canvas => {
const imgStr = canvas.toDataURL("image/png");
img.src = imgStr;
img.onload = function () {
ctx.drawImage(img, 0, 0, w, h);
}
});
}

合成视频


这里我们要使用到一个 API MediaRecorder ,用于在浏览器中进行音频和视频的录制。它提供了一种简单的方式来捕获来自麦克风、摄像头或屏幕的媒体数据,并将其保存为文件或进行实时流传输。


它有以下几个常用的方法:



  • isTypeSupported() 返回一个 Boolean 值,来表示设置的 MIME type 是否被当前用户的设备支持。

  • start() 开始录制媒体,这个方法调用时可以通过给 timeslice 参数设置一个毫秒值,如果设置这个毫秒值,那么录制的媒体会按照你设置的值进行分割成一个个单独的区块,而不是以默认的方式录制一个非常大的整块内容。

  • pause() 暂停媒体录制。

  • resume() 继续录制之前被暂停的录制动作。

  • stop() 停止录制。同时触发 dataavailable 事件,返回一个存储 Blob 内容的录制数据。之后不再记录。


首先创建一个 canvas 元素,用来保存 html2canvas 的截图,然后通过 captureStream 方法实时截取媒体流。


const w = boxBoundingClientRect.width;
const h = boxBoundingClientRect.height;
const canvas = document.createElement('canvas');
canvas.setAttribute('id', 'canvas');
canvas.setAttribute('width', w);
canvas.setAttribute('height', h);
canvas.style.display = 'none';
box.appendChild(canvas);

const img = document.querySelector('img');
const ctx = canvas.getContext("2d");
const allChunks = [];
const stream = canvas.captureStream(60); // 60 FPS recording 1秒60帧

通过 canvas 的流来创建一个 MediaRecorder 实例,并在 ondataavailable 事件中保存视频信息:


const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});

recorder.ondataavailable = (e) => {
allChunks.push(e.data);
};

最后,在停止录制时将帧信息创建 blob 并插入到页面上:


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

const video = document.createElement('video');
video.controls = true;
video.src = videoUrl;
video.muted = true;
video.autoplay = true;
document.body.appendChild(video);

或者可以将视频下载


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

let link = document.createElement('a');
link.style.display = 'none';
let fullBlob = new Blob(allChunks);
let downloadUrl = window.URL.createObjectURL(fullBlob);
link.href = downloadUrl;
link.download = 'canvas-video.mp4';
document.body.appendChild(link);
link.click();
link.remove();

这里,为了节省资源,只在点击按钮、输入等事件发生时才调用 html2canvas 截图 DOM。


如果实时记录屏也可以使用 requestAnimationFrame


最后


虽然实现了无感知录制屏幕,但也仅限于网页内,没有办法录制网页以外的部分。


以上的 demo 中只实现了 DOM 的录制,如果想要录制鼠标轨迹,可以增加一个跟随鼠标的元素~


作者:Mengke
来源:juejin.cn/post/7293462197386592283
收起阅读 »