注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

深入理解前端缓存

web
前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存、协商缓存、cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下...
继续阅读 »

前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存协商缓存cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下来我将带你们深入理解缓存的机制以及缓存时间的判断公式,如何合理的使用缓存机制来更好的提升优化。我将会把前端缓存分成HTTP缓存和浏览器缓存两个部分来和大家一起聊聊。


HTTP 缓存


HTTP是一种超文本传输协议,它通常运行在TCP之上,从浏览器Network中可以看到,它分为Respnse Headers(响应头)Request Headers(请求头)两部分组成。


image.png


接下来介绍一下与缓存相关的头部字段:


image.png


expires


我们先来看一下MDN对于expires的介绍



响应标头包含响应应被视为过期的日期/时间。


备注:  如果响应中有指令为 max-age 或 s-maxage 的 Cache-Control 标头,则 Expires 标头会被忽略。



Expires: Wed, 24 Apr 2024 14:27:26 GMT

Cache-Control


Cache-ControlHTTP/1.1中定义的缓存字段,它可以由多种组合使用,分开列如:max-age、s-maxage、public/private、no-cache/no-store等


Cache-Control: max-age=3600, s-maxage=3600, public

max-age是相对当前时间,单位是秒,当设置max-age时则expires就会失效,max-age的优先级更高。


而 s-maxage 与 max-age 不同之处在于,其只适用于公共缓存服务器,比如资源从源服务器发出后又被中间的代理服务器接收并缓存。


public是指该资源可以被任何节点缓存,而private只能提供给客户端缓存。当设置了private之后,s-maxage则会无效。


使用no-store表示不进行资源缓存。使用no-cache表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求,而当在响应首部中被返回时,表示客户端可以缓存资源,但每次使用缓存资源前都必须先向服务器确认其有效性,这对每次访问都需要确认身份的应用来说很有用。


当然,我们也可以在代码里加入 meta 标签的方式来修改资源的请求首部:


<meta http-equiv="Cache-Control" content="no-cache" />

示例


这里我起了一个nestjs的服务,该getdata接口缓存10s的时间,Ï代码如下:


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString() }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});Ï
}

第一次请求,花费了334ms的时间。


image.png


第二次请求花费了163ms的时间,走的是磁盘缓存,快了近50%的速度


image.png


接下来我们来验证使用Cache-Control是否可以覆盖Exprie,我们将getdata接口修改如下,Cache-Control设置了1s。Ï我们刷新页面可以看到getdata接口并没有缓存,每次都会想服务器发送请求。


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString(), 'Cache-Control': 1 }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});
}

仔细的同学应该会发现一个问题,清除缓存后的第一次请求和第二次请求Size的大小不一样,这是为什么呢?


打开f12右键刷新按钮,点击清空缓存并硬性重新加载。


image.png


我们开启Big request rows更方便查看Size的大小,开启时Size显示两行,第一行就是请求内容的大小,第二行则是实际的大小。


image.png


刷新一下,可以看到Size变成了283B大小了。


image.png


带着这个问题我们来深入研究一下浏览器的压缩。HTTP2和HTTP3的压缩算法是大致相同,我们就拿HTTP2的压缩算法(HPACK)来了解一下。


HTTP2 HPACK压缩算法


HPACK压缩算法大致分为:静态Huffman(哈夫曼)压缩和动态Huffman哈夫曼压缩,所谓静态压缩是指根据HTTP提供的静态字典表来查找对应的请求头字段从而存储对应的index值,可以极大的减少内催空间。


动态压缩它是在同一个会话级的,第一个请求的响应里包含了一个比如 {list: [1, 2, 3]},那么就会把它存进表里面,后续的其它请求的响应,就可以只返回这个 header 在动态表里的索引,实现压缩的目的


需要详细了解哈夫曼算法原理的可以去这个博客看一看。


Last-Modified 与 If-Modified-Since


Last-Modified代表资源的最后修改时间,其属于响应首部字段。当浏览器第一次接收到服务器返回资源的 Last-Modified 值后,其会把这个值存储起来,并下次访问该资源时通过携带If-Modified-Since请求首部发送给服务器验证该资源是否过期。


yaml
复制代码
Last-Modified: Fri , 14 May 2021 17:23:13 GMT
If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT

如果在If-Modified-Since字段指定的时间之后资源都没有发生更新,那么服务器会返回状态码 304 Not Modified 的响应。


Etag 与 If--Match


Etag代码该资源的唯一标识,它会根据资源的变化而变化着,同样浏览器第一次收到服务器返回的Etag值后,会把它存储起来,并下次访问该资源通过携带If--Match请求首部发送给服务器验证资源是否过期


Etag: "29322-09SpAhH3nXWd8KIVqB10hSSz66" 
If--Match: "29322-09SpAhH3nXWd8KIVqB10hSSz66"

如果两者不相同则代表服务器资源已经更新,服务器会返回该资源最新的Etag值。


强缓存


强缓存的具体流程如下:


image.png


上面我们介绍了expires设置的是绝对的时间,它会根据客户端的时间来判断,所以会造成expires不准确,如果我有一个资源缓存到到期时间是2024年4月31日我将客户端时间修改成过期的时间,则在一次访问该资产会重新请求服务器获取最新的数据。


max-age则是相对的时间,它的值是以秒为单位的时间,但是max-age也会不准确。


那么到底浏览器是怎么判断该资源的缓存是否有效的呢?这里就来介绍一下资源新鲜度的公式。


我们来用生活中的食品新鲜度来举例:


食品是否新鲜 = (生产日期 + 保质期) > 当前日期

那么缓存是否新鲜也可以借助这个公式来判断


缓存是否新鲜 = (创建时间 + expire || max-age) > 缓存使用期

这里的创建时间可以理解为服务器返回资源的时间,它和expires一样是一个绝对时间。


缓存使用期 = 响应使用期 + 传输延迟时间 + 停留缓存时间

响应使用期


响应使用期有两种获取方式:



  • max(0, responseTime - dateTime)

  • age


responseTime: 是指客户端收到响应的时间

dateTime: 是指服务器创建资源的时间

age:是响应头部的字段,通常是秒为单位


传输延迟时间


传输延迟的时间 = 客户端收到响应的时间 - 请求时间

停留时间


停留时间 = 当前客户端时间 - 客户端收到响应的时间

所以max-age也会失效的问题就是它也使用到了客户端的时间


协商缓存


协商缓存的具体流程如下:


image.png


从上文可以知道,协商缓存就是通过EtagLast-Modified这两个字段来判断。那么这个Etag的标识是如何生成的呢?


我们可以看node中etag第三方库。


该库会通过isState方法来判断文件的类型,如果是文件形式的话就会使用第一种方法:通过文件内容和修改时间来生成Etag


image.png


第二种方法:通过文件内容和hash值和内容长度来生成Etag


image.png


浏览器缓存


我们访问掘金的网站,查看Network可以看到有Size列有些没有大小的,而是disk cachememory cache这样的标识。


image.png


memory cache翻译就是内存缓存,顾名思义,它是存储在内存中的,优点就是速度非常快,可以看到Time列是0ms,缺点就是当网页关闭则缓存也就清空了,而且内存大小是非常有限的,如果要存储大量的资源的话还是使用磁盘缓存。


disk cache翻译就是磁盘缓存,它是存储在计算机磁盘中的一种缓存,它的优缺点和memory cache相反,它的读取是需要时间的,可以看到上方的图片Time列用了1ms的时间。


缓存获取顺序



  1. 浏览器会先查找内存缓存,如果存在则直接获取内存缓存中的资源

  2. 内存缓存没有,就回去磁盘缓存中查找,如果存在就返回磁盘缓存中的资源

  3. 磁盘缓存没有,那么就会进行网络请求,获取最新的资源然后存入到内存缓存或磁盘缓存


缓存存储优先级


浏览器是如何判断该资源要存储在内存缓存还是磁盘缓存的呢?


打开掘金网站可以看到,发现除了base64图片会从内存中获取,其它大部分资源会从磁盘中获取。


image.png


js文件是一个需要注意的地方,可以看到下面的有些js文件会被磁盘缓存有些则会被内存缓存,这是为什么呢?


image.png


Initiator列表示资源加载的位置,我们点击从内存获取资源的该列发现资源在HTML渲染阶段就被加载了,列入一下代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>DocumentÏ</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body>
<div id="root">这样加载的js资源大概率会存储到内存中</div>
</body>
</html>

而被内存抛弃的可以发现就是异步资源,这些资源不会被缓存到内存中。


上图我们可以看到有一个Initiator列的值是(index):50但是它还是被内存缓存了,我们可以点击进去看到他的代码如下:


image.png


这个js文件还是通过动态创建script标签来动态引入的。


Preload 与 Prefetch


PreloadPrefetch也会影响浏览器缓存的资源加载。


Preload称为预加载,用在link标签中,是指哪些资源需要页面加载完成后立刻需要的,浏览器会在渲染机制介入前加载这些资源。


<link rel="preload" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js" as="script">

当使用preload预加载资源时,这些资源一直会从磁盘缓存中读取。


prefetch表示预提取,告诉浏览器下一个页面可能会用到该资源,浏览器会利用空闲时间进行下载并存储到缓存中。


<link rel="prefretch" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js"Ï>

使用 prefetch 加载的资源,刷新页面时大概率会从磁盘缓存中读取,如果跳转到使用它的页面,则直接会从磁盘中加载该资源。


作者:sorryhc
来源:juejin.cn/post/7382891974942179354
收起阅读 »

前端基建有哪些?大小公司偏重啥?🤨

前言 兄弟们可能有的感受 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。 感受二:天天写业务代码和内部工具,感觉没技术沉淀,成长太慢,然后发现架构那边挺有趣的。 感受三:对一些框架的原理、源码、工具 研究较少,无法突破评级,...
继续阅读 »

前言




兄弟们可能有的感受



  • 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。

  • 感受二:天天写业务代码和内部工具,感觉没技术沉淀,成长太慢,然后发现架构那边挺有趣的。

  • 感受三:对一些框架的原理、源码、工具 研究较少,无法突破评级,成为leader。


上面的感受都是一些兄弟们的典型感受(也包括我自己)。这时候不妨可以考虑一下,了解了解前端的基础建设,进而 搭建起一个坚实的底座和让自己得到一个提升




正文开始——关于“基建”




1.什么是基建?



  • “技术基建”,就是研发团队的技术基础设施建设,一个团队通用技术能力的沉淀。

  • 小到文档规范,脚手架工具,大到工程化、各个领域工具链,凡是能促进业务效率、沟通成本都可以称作基建。

  • 网上看到的一句话,说的很好, “业务支撑是活在当下,技术基建是活好未来”




2.基建的意义


主要是为了以下几点:



  • 业务复用,提高效率: 基建可以提高单个人的工作产出和工作效率,可以从代码层面解决一些普遍性和常用性的业务问题

  • 规范、优化流程制度: 优异的流程制度必将带来正面的、积极的、有实效的业务支撑。

  • 更好面对未来业务发展: ,像建房子一样,好的地基可以建出万丈高楼。

  • 影响力建设、开源建设:建设结果对于业务的促进,更容易获得内部合作方的认可;沉淀下来的好的经验,可以对外输出分享,也是对影响力的有力帮助。




基建搞什么




1.核心:


下手之前首先得记住总结出的核心概念:



  • 三个落地要素: 公司的团队规模、公司的业务、团队水平。

  • 四大基础特性: 技术的健全性、基建的稳定性、研发的效率性、业务的体验性


根据结合落地和基础特性,来搭建不同"重量"和"复杂度"的基建系统。(毕竟每个公司的情况都不同)




2.方向


基建开始之前,首先得确定建设的策略及步骤,主要是从 拆解研发流程 入手的:


一个基本的研发流程闭环一般是:需求导入 => 需求拆解 => 技术方案制定 => 本地编码 => 联调 => 自测优化 => 提测修复 Bug => 打包 => 部署 => 数据收集&分析复盘 => 迭代优化 。


在研发流程闭环中每一个环节的阻塞点越少,研发效率就越高。基建,就是从这些耽误研发时间的阻塞点入手,按照普遍性 + 高频的优先级标准,挨个突破。




3.搞什么


通用的公式是: 标准化 + 规范化 + 工具化 + 自动化 ,能力完备后可以进一步提升到平台化 + 产品化。在方向上,主要是从下面的 8 个主要方向进行归类和建设,供大家参考:



  • 开发规范:这一部分沉淀的是团队的标准化共识,标准化是团队有效协作的必备前提。

  • 研发流程: 标准化流程直接影响上下游的协作分工和效率,优秀的流程能带来更专业的协作。

  • 工程管理: 面向应用全生命周期的低成本管控,从应用的创建到本地环境配置到低代码搭建到打包部署。

  • 性能体验: 自动化工具化的方式发现页面性能瓶颈,提供优化建议。

  • 安全防控: 三方包依赖安全、代码合规性检查、安全风险检测等防控机制。

  • 统计监控: 埋点方案、数据采集、数据分析、线上异常监控等。

  • 质量保障: 自测 CheckList、单测、UI 自动化测试、链路自动化测试等。


如上是一般性前端基建的主要方向和分区,不论是 PC 端还是移动端,这些都是基础的建设点。业务阶段、团队能力的差异,体现在基建上,在于产出的完整性颗粒度深入度自动化的覆盖范围。




4.大小公司基建重点


小团队的现实问题:考虑到现实,毕竟大多数前端团队不像大厂那样有丰富的团队人员配置,大多数还是很小的团队,小团队在实施基建时就不可避免的遇到很现实的阻力:



  • 最大的阻力应该就是 受限于团队规模小 ,无法投入较多精力处理作用于直接业务以外的事情

  • 其次应该是团队内部 对于基建的必要性和积极性认识不够 (够用就行的思想)


大小公司基建重点:



  • 小公司: 针对一些小团队或者说偏初创期的团队,其建设,往往越偏向于基础的技术收益,如脚手架组件库打包部署工具等;优先级应该排好,推荐初创公司和小团队成立优先搭建好:规范文档、统一开发环境技术栈/方法/工具、项目模板、CI/CD流程 ,把基础的闭环优先搭建起来。

  • 大公司: 越是成熟的业务和成熟沉淀的团队,其建设会越偏向于获取更多的业务收益,如直接服务于业务的系统,技术提效的同时更能直接带来业务收益。搭建起一套坚实的项目底座,能够更好的支持上层建筑的发展,同时也能够提升团队的成长,打开在业界的知名度,获取更好的信任支持。大公司在基础建设上,会更加考虑数据一些监控以及数据的埋点分析和统计,更加的偏重于数据的安全防范,做到质量保证。对于这点,很多前端需要写许多的测试case,有些人感觉很折磨,哈哈哈哈哈哈。




基建怎么搞




下面,会针对一些大家都感兴趣的方向,结合自身过去部分的建设产出和一些文章的记录的整合(部分地方可能比较久远的,有些不记得出处。如有冒犯,深感抱歉,可与我联系!),为大家列举一些前端基建类的沉淀,以供参考。


1. 规范&文档


规范和文档是最应该先行的,规范意味着标准,是团队的共识,是沟通协作的基础。


文档:



  • 新人文档(公司、业务、团队、流程等)

  • 技术文档、

  • 业务文档、

  • 项目文档(旧的、新的)

  • 计划文档(月度、季度、年度)

  • 技术分享交流会文档


规范:



  • 项目目录规范:比如api,组件,页面,路由,hooks,store等



  • 代码书写规范:组件结构、接口(定义好参数类型和响应数据类型)、事件、工具约束代码规范、代码规范、git提交规范




2. 脚手架


开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。


省事的可能直接拥抱框架选型对应的全家桶,如 Vue 全家桶,或者用 Webpack 撸一个脚手架。能力多一些的会再为脚手架提供一些插件服务,如 Lint 或者 Mock。从简单的一个本地脚手架,到复杂的一个工程化套件系统。




3. 组件


公司项目多了会有很多公共的组件,可以抽离出来,方便自身和其他项目复用,一般可以分为以下几种组件:



  • UI组件:antd、element、vant、uview...

  • 业务组件:表单、表格、搜索...

  • 功能组件:上拉刷新,滚动到底部加载更多,虚拟滚动,拖拽排序,图片懒加载..




4. 工具 / 函数库


前端工具库,如 axios、lodash、Day.js、moment.js、big.js 等等(太多太多,记不得了)


常见的 方法 / API封装:query参数解析、device设备解析、环境区分、localStorage封装、Day日期格式封装、Thousands千分位格式化、防抖、节流、数组去重、数组扁平化、排序、判断类型等常用的方法hooks抽离出来组成函数库,方便在各个项目中使用




5. 模板


可以提前根据公司的业务需求,封装出各个端对应通用开发模版,封装好项目目录结构,接口请求,状态管理,代码规范,git规范钩子,页面适配,权限,本地存储管理等等,来减少开发新项目时前期准备工作时间,也能更好的统一公司整体的代码规范。



  1. 通用后台管理系统基础模版封装

  2. 通用小程序基础模版封装

  3. 通用h5端基础模版封装

  4. 通用node端基础模版封装

  5. 其他类型的项目默认模版封装,减少重复工作。




6. API管理 / BFF


推荐直接使用axios封装或fetch,项目中基于次做二次封装,只关注和项目有关的逻辑,不关注请求的实现逻辑。在请求异常的时候不返回 Promise.reject() ,而是返回一个对象,只是code改为异常状态的 code,这样在页面中使用时,不用用 try/catch 包裹,只用 if 判断 code 是否正确就可以。再在规定的目录结构、固定的格式导出和导入。


BFF(Backends For Frontends)主要将后端复杂的微服务,聚合成对各种不同用户端(无线/Web/H5/第三方等)友好和统一的API;




7. CI/CD 构建部署


前端具备自己的构建部署系统,便于专业化方面更好的流程控制。很多公司目前,都实现了云打包、云检测和自动化部署,每次 git commit 代码后,都会自动的为你部署项目至 测试环境、预生产环境、生产环境,不用你每次手动的去打包后 cv 到多个服务器和环境。开发新的独立系统之初,也会希望能实现一种 Flow 的流式机制,以便实现代码的合规性静态检测能力。如果可以的话,可以去实现了一套插件化机制,可以按需配置不同的检测项,如某检测项检测不通过,最终会阻塞发布流程。




8. 数据埋点与分析


前端团队可以做的是 Web 数据埋点收集和数据分析、可视化相关的全系统建设。可实现埋点规范、埋点 SDK、数据收集及分析、PV/UV、链路分析、转化分析、用户画像、可视化热图、坑位粒度数据透出等数据化能力,下面给大家细分一些这些数据:



  • 行为数据:时间、地点、人物、交互、交互的内容;

  • 质量数据:浏览器加载情况、错误异常等;

  • 环境数据:浏览器相关的元数据以及地理、运营商等;

  • 运营数据:PV、UV、转化率、留存率(很直观的数据);




9.微前端


将您的大型前端应用拆分为多个小型前端应用,这样每个小型前端应用都有自己的仓库,可以专注于单一的某个功能;也可再聚合成有各个应用组成的一个平台,而各个应用使用的技术栈可以不同,也就是可以将不同技术栈的项目给整合到一块。这点就很不错,在如今电子办公化如此细致的时代,可能许多公司工作中都不止一个平台,平台之间的切换十分的繁琐,这时候平台之间聚合的趋势想来是必然的。(个人浅显的理解)


目前成熟一点的框架有蛮多的,使用的底层思想也各有不同,目前我也在学习qiankun等框架中,期待后面能够给大家分享一篇文章,加油💪




基建之外思考




1. 从当下业务场景出发


很多时候我们的建设推不下去,往往不是因为人力的问题,而是 没想清楚/没有方向 。对于研发同学,我们更应该着重于当下,从方案出发找实际场景的问题,也就是从我们项目和团队目前的业务问题、人员问题,一步步出发。还有就是,我们得开这个头。没有一个作家是看小说看成的,同理技术专家也不会是通过看技术书籍养成的。在实践中也就是实际场景中学习,从来都是最快的方式。许多有价值的事从来都是从业务本身的问题出发。到头来你会发现:问题就是机会,问题就是长萝卜的坑




2.基建讲究循序渐进


业界大部分的研发团队,都不是阿里、腾讯、头条这样基础完备沉淀丰富的情况,起步期和快速爬坡期居多,建设滞后。体现在基建上,可能往往只有一个基于 Webpack 搞搞的脚手架,和一个第三方开源的 UI 组件库上封装下自己的业务组件库,除此之外无他。如果兄弟们现在恰好是我说的这种情况,不用焦虑,很多前端也是一样的情况。只要我们一步步建设,慢慢落地基础设施,就一定会取得好的反馈




3. 技术的价值,在于解决业务问题,并且匹配


技术的价值,在于解决业务问题;人的身价,在于解决问题的能力


基建的内容我认为首先是 和业务阶段相匹配 的。不同团队服务的业务阶段不同,基建的内容和广深度也会不同。高下之分不在于多寡,而在于对业务的理解和支持程度。


“业务支撑” 和 “基础建设” 都是同一件事的两个面,这个 “同一件事”,就是帮助业务解决问题。任何脱离解决实际场景而发起的基建,都需要重新审视甚至不应被鼓励。如果时间成本没有那么多的话,建议先搭建好基本的建设底座,想要更好的闭环的想法还是先搁置一下。




4.个人不足


总结了这么多,结果发现自己对于一些知识点还是了解的太浅显了,自身在那些方面能分享的还是不多,平时也看了一些文章记录了一些笔记,却只能描出个大概,实在是有点不好意思。但回头想想,这何尝也不算个勉励自己的方法,能够鞭策自己。后续,在我学习深入一些基建方面的知识后,会再出一些文章分享给大家,希望能够帮助到大家,共勉!!!☺(这篇文章来自于自身学习和平常看文章记录的笔记的整合,如一些地方有冒犯到,深感抱歉,请与我联系修改!)


落尾




大家好,我是 KAIHUA ,一个来自阿卡林省目前在深圳前端区Frank + ikun


计划是试试每一两月(最近有点🕊了,太忙了)复盘一次,总结出至少一个知识点,目的是尽快给自己的反馈,将自己产品一样快速迭代上升,希望可以坚持✊。


输出的文章也是个人的一些浅显理解,网上看的一些好文章记录的一些笔记,望大家包容!!!


如果有什么相关错误和冒犯之处,望大家指正,感谢感谢!!!(还在学习中,嘿嘿🤭)


下一篇文章应该会是关于 前端思考 方面的,希望早一点归纳出,和大家沟通交流...


各位 彦祖 / 祖贤,fan yin (欢迎) 关注点赞收藏,将泼天的富贵带点给我😭


一起加油!!! giao~~~🐵🙈🙉


作者:KAIHUA
来源:juejin.cn/post/7301150860825133110
收起阅读 »

Strapi让纯前端成为全栈不再是口号!🚀🚀🚀

web
序言很早以前就知道strap的存在,一直没有机会使用到。很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。strapi是什么?Strapi在国内鲜为人知,但它...
继续阅读 »

序言

很早以前就知道strap的存在,一直没有机会使用到。

很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。

如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。

image.png

strapi是什么?

Strapi在国内鲜为人知,但它在国外的使用情况真的很Nice!

image.png 其仓库也是一直在维护、更新的。

Strapi 是一个开源的 Headless CMS(无头内容管理系统)。它允许开发者通过自定义的方式快速构建、管理和分发内容。Strapi 提供了一个强大的后端 API,支持 RESTful  GraphQL 两种方式,使得开发者可以方便地将内容分发到任何设备或服务,无论是网站、移动应用还是 IoT 设备。

Strapi 的主要特点包括:

  • 灵活性和可扩展性:通过自定义模型、API、插件等,Strapi 提供了极高的灵活性,可以满足各种业务需求。
  • 易于使用的 API:Strapi 提供了一个简洁、直观的 API,使得开发者可以轻松地与数据库进行交互。
  • 内容管理界面:Strapi 提供了一个易于使用的管理界面,使得用户可以轻松地创建、编辑和发布内容。
  • 多语言支持:Strapi 支持多种语言,包括中文、英语、法语、德语等。
  • 可扩展性:Strapi 具有高度的可扩展性,可以通过插件和自定义模块、插件来扩展其功能。
  • 社区支持:Strapi 拥有一个活跃的社区,提供了大量的文档、示例和插件,使得开发人员可以轻松地解决问题和扩展功能。

主要适用场景:

  • 多平台内容分发( 将内容分发到不同web、h5等不同平台 
  • 定制化 CMS 需求( 通过插件等扩展性高度定制 
  • 快速开发api(API管理界面能够大大加快开发速度,尤其是MVP(最小可行产品)阶段 

strapi实战

光看官网界面原官网地址),还是相当漂亮的🙈:

安装Strapi

超级简单,执行下面的命令后、坐等服务启动

(安装完后,自动执行了strapi start,其mysql、语言切换、权限配置等都内置到了@strapi包中)

yarn create strapi-app my-strapi --quickstart

浏览器访问:http://localhost:1337/admin/

第一步就完成了、是不是so easy😍,不过默认是英文的,虽然英语还凑合,但使用起来还是多有不便。strapi原本就支持国际化,我们来切换成中文再继续操作。

语言切换

  1. 设置国际化

  1. 个人设置中配置语言即可:

如果看不到"中文(简体)"选项,就在项目根目录下执行build并重启:npm run build && npm start,再刷新页面应该就能看到了。注意npm start默认是生产环境的启动(只能使用表,无法创建表)、开发环境启动用"npm run develop"

strapi的基础使用

在第一步完成的时候,其实数据库就已经搭建好了,我们只管建表、增加curd的接口即可

1. 建表

设置字段、可以选择需要的类型:

在保存左边的按钮可以继续添加字段

blog字段、建模完成后,进入内容管理器给表插入数据

2. curd

上面只是可视化的查看、插入数据,怎样才能变成api来进行curd了。

  • 设置API令牌,跟进提示操作

  • 权限说明

find GET请求 /api/blogs 查找所有数据

findone GET请求 /api/blogs/:id 查找单条数据

create POST请求 /api/blogs 创建数据

update PUT请求 /api/blogs/:id 更新数据

delete DELETE请求 /api/blogs/:id 删除数据

  • postman调试

先给blog公共权限,以便调试:

  1. 查找所有数据(find)

  1. 查找单条数据(findone)

  1. 更新修改数据(update)

  1. 删除数据(delete),返回被删除的数据

再次查看:

好了,恭喜你。 到这一步,你已经掌握了strapi curd的使用方法。


strapi数据可视化、Navicat辅助数据处理

Strapi 支持多种数据库,包括 MySQL、PostgreSQL、MongoDB 和 SQLite,并且具有高度的可扩展性和自定义性,可以满足不同项目的需求。(默认使用的是SQLite数据库)

我们也可以借助Navicat等第三个工具来实现可视化数据操作:

其用户名、密码默认都是strapi

strapi数据迁移

SQLite数据库

如果你只是需要将SQLite数据库从一个环境迁移到另一个环境(比如从一个服务器迁移到另一个服务器),操作相对简单:

  1. 备份SQLite数据库文件:找到你的SQLite数据库文件(默认位置是项目根目录下的 .tmp/data.db)并将其复制到安全的位置。
  2. 迁移文件:将备份的数据库文件移动到新环境的相同位置。
  3. 更新配置(如有必要) :如果新环境中数据库文件的位置有变化,确保更新Strapi的数据库配置文件(./config/database.js)以反映新的文件路径。

SQLite到其他数据库系统

如果你需要将SQLite数据库迁移到其他类型的数据库系统,比如PostgreSQL或MySQL,流程会更复杂一些:

  1. 导出SQLite数据:首先,你需要导出SQLite数据库中的数据。这可以通过多种工具完成,例如使用sqlite3命令行工具或一个图形界面工具(如DB Browser for SQLite)来导出数据为SQL文件。
  2. 准备目标数据库:在目标数据库系统中创建一个新的数据库,为Strapi项目准备使用。
  3. 修改Strapi的数据库配置:根据目标数据库类型,修改Strapi的数据库配置文件(./config/database.js)。你需要根据目标数据库系统的要求配置连接参数。
  4. 导入数据到目标数据库:使用目标数据库系统的工具导入之前导出的数据。不同数据库系统的导入工具和命令会有所不同。例如,对于PostgreSQL,你可能会使用psql工具,对于MySQL,则可能使用mysql命令行工具。
  5. 处理数据类型和结构差异:不同的数据库系统在数据类型和结构上可能会有所差异。在导入过程中,你可能需要手动调整SQL文件或在导入后调整数据库结构,尤其是对于关系和外键约束。
  6. 测试:迁移完成后,彻底测试你的Strapi项目,确保数据正确无误,所有功能正常工作。

注意事项

  • 数据兼容性:在不同数据库系统之间迁移时,可能会遇到数据类型不兼容的问题,需要仔细处理。
  • 性能调优:迁移到新的数据库系统后,可能需要根据新的数据库特性进行调优以确保性能。
  • 备份:在进行任何迁移操作之前,总是确保已经备份了所有数据和配置。

具体步骤可能会因你的具体需求和所使用的数据库系统而异。根据你的目标数据库系统,可能有特定的迁移工具和服务可以帮助简化迁移过程。

小结

好了,梳理一下。现在我要建一套博客系统的API该怎么做了?

  1. 安装启动(已安装可忽略)yarn create strapi-app my-strapi --quickstart
  2. 在后台建表建模、设置字段
  3. 设置表的API调用权限
  4. 在需要用到的地方使用即可
    是不是超级简单了!

总结

上面我们了解了strapi的后台使用、curd操作、数据迁移等。相信大家都能快速掌握使用。我们无需基于ORM框架去搭建数据模型,也无需使用python、nestjs等后台框架去创建后台服务了。 这势必能大大提升我们的开发效率。

后续会再继续讲解strapi富文本插件的使用,有了富文本的加持、我们都能省去搭建管理后台了,如果用来做博客、高度定制化的文档系统将是非常不错的选择。


作者:tager
来源:juejin.cn/post/7340152660224819212

收起阅读 »

打脸了,rust确实很快

正文: 原标题:不要盲目迷信rust,rust或许没有你想象中的那么快 之前的文章内容是关于rust速度不快的问题。但是我遗留的问题是,计算出来的哈希值不一样。经过老哥的指点,原来是我js代码的实现有点问题,让大家见笑了。 于是我修改代码,重新测试了一遍,这...
继续阅读 »

正文:


原标题:不要盲目迷信rust,rust或许没有你想象中的那么快


之前的文章内容是关于rust速度不快的问题。但是我遗留的问题是,计算出来的哈希值不一样。经过老哥的指点,原来是我js代码的实现有点问题,让大家见笑了。


image.png


于是我修改代码,重新测试了一遍,这时哈希值就一样了,并且计算的结果确实是rust要快。刚好拿来佐证rust快的事实。


image.png


以后一定经过仔细验证,再发表文章。原文不删,留作警醒。


以下是原文:


原文:


先说结论:抛开应用场景,单说语言速度都是耍流氓。因为js调度rust,会有时间损耗。所以rust在一定应用场景下,比js还慢。


# 使用 wasm 提高前端20倍的 md5 计算速度


前两天看了一篇文件,是使用rust和wasm来加快md5的计算时间。跑了他的demo,发现只有rust的demo,而没有js的对比,于是我fork项目后,补充了一个js的对比。


image.png


测试下来,发现rust并没有比js快多少,由于浏览器限制,我只能用2GB文件来测,不知道是不是这个原因。还是我使用的js的原因。至少在2GB的边界时,js比rust要快。


他rust部分我没有动,只是添加了一个js的对比,如果大家觉得我的js写得有问题,欢迎pr重新比较。


在线对比地址:minori-ty.github.io/digest-wasm…


项目地址: github.com/Minori-ty/d…


作者:天平
来源:juejin.cn/post/7359757993732734991
收起阅读 »

手把手带你开发一套用户权限系统,精确到按钮级

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开。 如何设计一套可以精确到按钮级别的用户权限功能呢? 今天通过这篇...
继续阅读 »

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开


如何设计一套可以精确到按钮级别的用户权限功能呢?


今天通过这篇文章一起来了解一下相关的实现逻辑,不多说了,直接上案例代码!


01、数据库设计


在进入项目开发之前,首先我们需要进行相关的数据库设计,以便能存储相关的业务数据。


对于【用户权限控制】功能,通常5张表基本就可以搞定,分别是:用户表、角色表、用户角色表、菜单表、角色菜单表,相关表结构示例如下。



其中,用户和角色是多对多的关系角色与菜单也是多对多的关系用户通过角色来关联到菜单,当然也有的用户权限控制模型中,直接通过用户关联到菜单,实现用户对某个菜单权限独有控制,这都不是问题,可以自由灵活扩展。


用户、角色表的结构设计,比较简单。下面,我们重点来解读一下菜单表的设计,如下:



可以看到,整个菜单表就是一个父子表结构,关键字段如下



  • name:菜单名称

  • menu_code:菜单编码,用于后端权限控制

  • parent_id:菜单父节点ID,方便递归遍历菜单

  • node_type:菜单节点类型,可以是文件夹、页面或者按钮类型

  • link_url:菜单对应的地址,如果是文件夹或者按钮类型,可以为空

  • level:菜单树的层次,以便于查询指定层级的菜单

  • path:树id的路径,主要用于存放从根节点到当前树的父节点的路径,想要找父节点时会特别快


为了方便项目后续开发,在此我们创建一个名为menu_auth_db的数据库,SQL 初始脚本如下:


CREATE DATABASE IF NOT EXISTS `menu_auth_db` default charset utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE TABLE `menu_auth_db`.`tb_user` (
`id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '用户手机号',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '用户姓名',
`password` varchar(128) NOT NULL DEFAULT '' COMMENT '用户密码',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='用户表';

CREATE TABLE `menu_auth_db`.`tb_user_role` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='用户角色表';

CREATE TABLE `menu_auth_db`.`tb_role` (
`id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '角色名称',
`code` varchar(100) NOT NULL DEFAULT '' COMMENT '角色编码',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='角色表';


CREATE TABLE `menu_auth_db`.`tb_role_menu` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='角色菜单表';


CREATE TABLE `menu_auth_db`.`tb_menu` (
`id` bigint(20) NOT NULL COMMENT '菜单ID',
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单名称',
`menu_code` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单编码',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父节点',
`node_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '节点类型,1文件夹,2页面,3按钮',
`icon_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单图标地址',
`sort` int(11) NOT NULL DEFAULT '1' COMMENT '排序号',
`link_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单对应的地址',
`level` int(11) NOT NULL DEFAULT '0' COMMENT '菜单层次',
`path` varchar(2500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '树id的路径,主要用于存放从根节点到当前树的父节点的路径',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`) USING BTREE,
KEY idx_parent_id (`parent_id`) USING BTREE
) ENGINE=InnoDB COMMENT='菜单表';

02、项目构建


菜单权限模块的数据库设计搞定之后,就可以正式进入系统开发阶段了。


2.1、创建项目


为了快速构建项目,这里采用的是springboot+mybatisPlus框架来快速开发,借助mybatisPlus提供的生成代码器,可以一键生成所需的daoserviceweb层的服务代码,以便帮助我们剩去 CRUD 中重复编程的工作量,内容如下:



CRUD 代码生成完成之后,此时我们就可以编写业务逻辑代码了,相关示例如下!


2.2、菜单功能开发


2.2.1、菜单新增逻辑示例

@Override
public void addMenu(Menu menu) {
//如果插入的当前节点为根节点,parentId指定为0
if(menu.getParentId().longValue() == 0){
menu.setLevel(1);//默认根节点层级为1
menu.setPath(null);//默认根节点路径为空
}else{
Menu parentMenu = baseMapper.selectById(menu.getParentId());
if(parentMenu == null){
throw new CommonException("未查询到对应的父菜单节点");
}
menu.setLevel(parentMenu.getLevel().intValue() + 1);
// 重新设置菜单节点路径,多个用【,】隔开
if(StringUtils.isNotEmpty(parentMenu.getPath())){
menu.setPath(parentMenu.getPath() + "," + parentMenu.getId());
}else{
menu.setPath(parentMenu.getId().toString());
}
}
// 设置菜单ID,可以用发号器来生成
menu.setId(System.currentTimeMillis());
// 将菜单信息插入到数据库
super.save(menu);
}

2.2.2、菜单查询逻辑示例

首先,编写一个视图对象,用于数据展示。


public class MenuVo {

/**
* 主键
*/

private Long id;

/**
* 名称
*/

private String name;

/**
* 菜单编码
*/

private String menuCode;

/**
* 父节点
*/

private Long parentId;

/**
* 节点类型,1文件夹,2页面,3按钮
*/

private Integer nodeType;

/**
* 图标地址
*/

private String iconUrl;

/**
* 排序号
*/

private Integer sort;

/**
* 页面对应的地址
*/

private String linkUrl;

/**
* 层次
*/

private Integer level;

/**
* 树id的路径 整个层次上的路径id,逗号分隔,想要找父节点特别快
*/

private String path;

/**
* 子菜单集合
*/

List childMenu;

// set、get方法等...
}

接着编写菜单查询逻辑,这里需要用到递归算法来封装菜单视图。


@Override
public List queryMenuTree() {
Wrapper queryObj = new QueryWrapper<>().orderByAsc("level","sort");
List
allMenu = super.list(queryObj);
// 0L:表示根节点的父ID
List resultList = transferMenuVo(allMenu, 0L);
return resultList;
}

递归算法,方法实现逻辑如下!


/**
* 封装菜单视图
*
@param allMenu
*
@param parentId
*
@return
*/

private List transferMenuVo(List
allMenu, Long parentId){
List resultList = new ArrayList<>();
if(!CollectionUtils.isEmpty(allMenu)){
for (Menu source : allMenu) {
if(parentId.longValue() == source.getParentId().longValue()){
MenuVo menuVo = new MenuVo();
BeanUtils.copyProperties(source, menuVo);
//递归查询子菜单,并封装信息
List childList = transferMenuVo(allMenu, source.getId());
if(!CollectionUtils.isEmpty(childList)){
menuVo.setChildMenu(childList);
}
resultList.add(menuVo);
}
}
}
return resultList;
}

最后编写一个菜单查询接口,将其响应给客户端。


@RestController
@RequestMapping("/menu")
public class MenuController {

@Autowired
private MenuService menuService;

@PostMapping(value = "/queryMenuTree")
public List queryTreeMenu(){
return menuService.queryMenuTree();
}
}

为了便于演示,这里我们先在数据库中初始化几条数据,最后三条数据指的是按钮类型的菜单,用户真正请求的时候,实际上请求的是这三个功能,内容如下:



queryMenuTree接口发起请求,返回的数据结果如下图:



将返回的数据,通过页面进行渲染之后,结果类似如下图:



2.3、用户权限开发


在上文,我们提到了用户通过角色来关联菜单,因此,很容易想到,用户控制菜单的流程如下:



  • 第一步:用户登陆系统之后,查询当前用户拥有哪些角色;

  • 第二步:再通过角色查询关联的菜单权限点;

  • 第三步:最后将用户拥有的角色名下所有的菜单权限点,封装起来返回给用户;


带着这个思路,我们一起来看看具体的实现过程。


2.3.1、用户权限点查询逻辑示例

首先,编写一个通过用户ID查询菜单的服务,代码示例如下!


@Override
public List queryMenus(Long userId) {
// 第一步:先查询当前用户对应的角色
Wrapper queryUserRoleObj = new QueryWrapper<>().eq("user_id", userId);
List userRoles = userRoleService.list(queryUserRoleObj);
if(!CollectionUtils.isEmpty(userRoles)){
// 第二步:通过角色查询菜单(默认取第一个角色)
Wrapper queryRoleMenuObj = new QueryWrapper<>().eq("role_id", userRoles.get(0).getRoleId());
List roleMenus = roleMenuService.list(queryRoleMenuObj);
if(!CollectionUtils.isEmpty(roleMenus)){
Set menuIds = new HashSet<>();
for (RoleMenu roleMenu : roleMenus) {
menuIds.add(roleMenu.getMenuId());
}
//查询对应的菜单
Wrapper queryMenuObj = new QueryWrapper<>().in("id", new ArrayList<>(menuIds));
List
menus = super.list(queryMenuObj);
if(!CollectionUtils.isEmpty(menus)){
//将菜单下对应的父节点也一并全部查询出来
Set allMenuIds = new HashSet<>();
for (Menu menu : menus) {
allMenuIds.add(menu.getId());
if(StringUtils.isNotEmpty(menu.getPath())){
String[] pathIds = StringUtils.split(",", menu.getPath());
for (String pathId : pathIds) {
allMenuIds.add(Long.valueOf(pathId));
}
}
}
// 第三步:查询对应的所有菜单,并进行封装展示
List
allMenus = super.list(new QueryWrapper().in("id", new ArrayList<>(allMenuIds)));
List resultList = transferMenuVo(allMenus, 0L);
return resultList;
}
}

}
return null;
}

然后,编写一个通过用户ID查询菜单的接口,将数据结果返回给用户,代码示例如下!


@PostMapping(value = "/queryMenus")
public List queryMenus(Long userId){
//查询当前用户下的菜单权限
return menuService.queryMenus(userId);
}

2.4、用户鉴权开发


完成以上的逻辑开发之后,可以实现哪些用户拥有哪些菜单权限点的操作,比如用户【张三】,拥有【用户管理】菜单,那么他只能看到【用户管理】的界面;用户【李四】,用于【角色管理】菜单,同样的,他只能看到【角色管理】的界面,无法看到其他的界面。


但是某些技术人员发生漏洞之后,可能会绕过页面展示逻辑,直接对接口服务发起请求,依然能正常操作,例如利用用户【张三】的账户,操作【角色管理】的数据,这个时候就会发生数据安全隐患的问题。


为此,我们还需要一套用户鉴权的功能,对接口请求进行验证,只有满足要求的才能获取数据。


其中上文提到的菜单编码menuCode就是一个前、后端联系的桥梁。其实所有后端的接口,与前端对应的都是按钮操作,因此我们可以以按钮为基准,实现前后端双向权限控制


以【角色管理-查询】这个为例,前端可以通过菜单编码实现是否展示这个查询按钮,后端可以通过菜单编码来鉴权当前用户是否具备请求接口的权限,实现过程如下!


2.4.1、权限控制逻辑示例

在此,我们采用权限注解+代理拦截器的方式,来实现接口权限的安全验证。


首先,编写一个权限注解CheckPermissions


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermissions {

String value() default "";
}

然后,编写一个代理拦截器,拦截所有被@CheckPermissions注解标注的方法


@Aspect
@Component
public class CheckPermissionsAspect {

@Autowired
private MenuMapper menuMapper;

@Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)")
public void checkPermissions() {}

@Before("checkPermissions()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
Long userId = null;
// 获取请求参数
Object[] args = joinPoint.getArgs();
Object requestParam = args[0];
// 用户请求参数实体类中的用户ID
if(!Objects.isNull(requestParam)){
// 获取请求对象中属性为【userId】的值
Field field = requestParam.getClass().getDeclaredField("userId");
field.setAccessible(true);
userId = (Long) field.get(parobj);
}
if(!Objects.isNull(userId)){
// 获取方法上有CheckPermissions注解的参数
Class clazz = joinPoint.getTarget().getClass();
String methodName = joinPoint.getSignature().getName();
Class[] parameterTypes = ((MethodSignature)joinPoint.getSignature()).getMethod().getParameterTypes();
// 寻找目标方法
Method method = clazz.getMethod(methodName, parameterTypes);
if(method.getAnnotation(CheckPermissions.class) != null){
// 获取注解上的参数值
CheckPermissions annotation = method.getAnnotation(CheckPermissions.class);
String menuCode = annotation.value();
if (StringUtils.isNotBlank(menuCode)) {
// 通过用户ID、菜单编码查询是否有关联
int count = menuMapper.selectAuthByUserIdAndMenuCode(userId, menuCode);
if(count == 0){
throw new CommonException("接口无访问权限");
}
}
}
}
}
}

2.4.2、鉴权逻辑验证

我们以上文说到的【角色管理-查询】为例,编写一个服务接口来验证一下逻辑的正确性。


首先,编写一个请求实体类RoleDTO,添加userId属性


public class RoleDTO extends Role {

//添加用户ID
private Long userId;

// set、get方法等...
}

其次,编写一个角色查询接口,并在方法上添加@CheckPermissions注解,表示此方法需要鉴权,满足条件的用户才能请求通过。


@RestController
@RequestMapping("/role")
public class RoleController {

private RoleService roleService;

@CheckPermissions(value="roleMgr:list")
@PostMapping(value = "/queryRole")
public List queryRole(RoleDTO roleDTO){
return roleService.list();
}
}

最后,在数据库中初始化相关的数据。例如给用户【张三】分配一个【访客人员】角色,同时这个角色只有【系统配置】、【用户管理】菜单权限。






启动项目,在postman中传入用户【张三】的ID,查询用户具备的菜单权限,只有两个,结果如下:



同时,利用用户【张三】发起【角色管理-查询】操作,提示:接口无访问权限,结果如下:



与预期结果一致!因为没有配置角色查询接口,所以无权访问!


03、小结


最后总结一下,【用户权限控制】功能在实际的软件系统中非常常见,希望本篇的知识能帮助到大家。




作者:潘志的研发笔记
来源:juejin.cn/post/7380283378153914383
收起阅读 »

Android 复杂项目崩溃率收敛至0.01%实践

一、崩溃收敛机制 1、创建修BUG分支 在我们的项目中,每个版本发布之后,我们会创建一个opt分支,用于修复线上崩溃以及业务逻辑BUG。 开发过程中,一个APP可能同时并行开发多个需求,每个需求上线的预期时间可能会有不同。但是这个opt分支我们会保证在下个版本...
继续阅读 »

一、崩溃收敛机制


1、创建修BUG分支


在我们的项目中,每个版本发布之后,我们会创建一个opt分支,用于修复线上崩溃以及业务逻辑BUG。


开发过程中,一个APP可能同时并行开发多个需求,每个需求上线的预期时间可能会有不同。但是这个opt分支我们会保证在下个版本一定上线,QA同学也会在每个版本发布前预留测试opt分支的时间。


2、每天早晨查看Dump后台


每天上班第一件事就是查看DUMP后台,收集昨天线上发生的DUMP崩溃,具体的堆栈分配给对应的业务负责人。


业务负责人收到崩溃之后,会优先跟进排查。排查下来如果相对好修复,会第一时间直接修复掉,并提交到opt分支。如果排查下来发现,较难定位或者耗时较久,则需要给出修复预期。也可以将Bug转为技术优化,作为专项推进。因为确实有一些Bug需要通盘考虑,所有业务配合。


二、崩溃容灾机制


1、背景


我们为什么要开发一套崩溃容灾逻辑?


在对线上崩溃进行收敛时,我们发现线上有几类崩溃是我们在应用无法修复的。


例如:案例一


java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.content.ClipDescription.hasMimeType(java.lang.String)' on a null object reference
at android.widget.TextView.canPasteAsPlainText(TextView.java:15065)
at android.widget.Editor$TextActionModeCallback.populateMenuWithItems(Editor.java:4692)
at android.widget.Editor$TextActionModeCallback.onCreateActionMode(Editor.java:4627)

案例二:小米手机上出现


java.lang.NullPointerException:Attempt to invoke virtual method 'int android.text.Layout.getLineForOffset(int)' on a null object reference

案例三:集成华为推送SDK后,偶现


ava.lang.RuntimeException:Unable to start activity ComponentInfo{com.netease.popo/com.huawei.hms.activity.BridgeActivity}:
android.util.AndroidRuntimeException: requestFeature() must be called before adding content"

案例四:BadTokenException


android.view.WindowManager$BadTokenException:Unable to add window -- token android.os.BinderProxy

我们大致将以上问题划分为四类:



  • 我们认为是系统异常,应用层仅能在使用的位置try cache,有些崩溃甚至无处try cache;

  • 排查下来发现仅在某个厂商的手机上出现;

  • 集成的一些第三方SDK所引入,依赖对方修复,时间上不好掌控;

  • 由于Android系统的一些机制引发的崩溃,如弹出弹框时,恰好依赖的Actity正在销毁。业务层希望弹框可以不弹出但不要崩溃,可是系统最终是抛出来一个BadTokenException。我们可以在使用DiaLog时做判断,但是总会用有同学忘记。


基于以上我们思考是否可以开发一个框架,将这些崩溃统计进行拦截,使其不影响用户的使用。


2、技术方案


作为Android开发应该都比较清楚Handler机制。我们的崩溃容灾主要是利用了Handler机制。
具体的逻辑图如下:


popo_2022-05-15  16-16-01.jpg



  • 应用启动后,初始化崩溃白名单(应用内置,也支持服务端动态下发)

  • 通过Handler#post()方法,向主线程中发送一条消息。

  • 在Runnable#run()方法中,执行一个死循环逻辑

  • 死循环中逻辑中使用try cache将Looper.loop()防护

  • 这样只要应用的进程不结束,相当于任务一直执行在我们前面post的消息中

  • 只是我们在这个消息中,再次执行了Looper.loop()方法,执行后续消息队列中所有的消息

  • 一旦后续所有消息遇到崩溃,会先被try cache捕获。

  • 然后判断崩溃信息是否在我们的白名单中,一旦在白名单中直接捕获掉,不向外抛异常,逻辑会回到外部的死循环中,继续执行Looper.loop()方法获取后续的消息。这样就保证了逻辑的连贯,后续的事件可以继续处理。

  • 不再我们的白名单中则继续将这个异常throw出去。


3、现状


崩溃拦截框架上线至今几年的时间,积累的崩溃种类目前已经达到81种。


比较典型的除了上面介绍的几类崩溃以外,还有如我们在适配Android 12的SplasScree时,遇到的TransferSplashScreenViewStateItem 相关的错误


java.lang.IllegalArgumentException: Activity client record must not be null to execute transaction item: android.app.servertransaction.TransferSplashScreenViewStateItem@de845fa
at android.app.servertransaction.ActivityTransactionItem.getActivityClientRecord(ActivityTransactionItem.java:85)
at android.app.servertransaction.ActivityTransactionItem.getActivityClientRecord(ActivityTransactionItem.java:58)
at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:43)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:149)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:103)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2708)
at android.os.Handler.dispatchMessage(Handler.java:114)
at android.os.Looper.loopOnce(Looper.java:206)
at android.os.Looper.loop(Looper.java:296)

按照过往的手段,我们仅能等待Google官方来修复这个错误,或者先下线掉这个错误。本框架可以直接将其进行拦截,同时用户无感知。


三、其他崩溃收敛


我们针对自身业务特点,大致梳理以下几种业务中非常常见的崩溃场景,提醒每一位同学注意。同时内部维护了一个研发质量表,每个需求提测时我们会过一遍研发质量表格,提醒同学注意相关代码质量与性能。


1、空指针问题


NPE应该是最常见的问题了。针对NPE的问题,我们的解决方式有:



  • 推荐组内所有同学习惯使用注解@NonNull和@Nullable

  • 推荐大家使用Kotlin;并且在Java调用kotlin方法时,一定要注意Kotlin方法是否要求入参不为空

  • 从List、Map中取到的对象,使用前必须判空

  • 业务代码需要将Context传给第三方工具类,传入之前必须判空

  • 对象建议声明为final或者val,防止后续其他位置置空引起NPE

  • 外接传入的对象使用前必须判空

  • 基于AS插件进行检测


2、IndexOutBoundsException


角标越界异常在平时开发中也特别常见,在我们的业务中常见于集合以及Span操作



  • 集合传入index时需要判断是否在[0, size]内

  • 操作Spannable接口setSpan方法时,需要start与end的数值不会超过长度,同时不能够为负数


3、ConcurrentModificationException 并发修改异常


并发修改异常在复杂的业务中,是非常容易遇到的。通常有两个场景容易触发,分别是foreach循环中直接调用remove方法移除元素,以及线程不安全环境下使用线程不安全集合。


针对并发修改异常:



  • 我们推荐在遍历集合时,可以new一个新的List集合,将要遍历的List集合作为参数传入,然后遍历新的集合。这样原集合在遍历时改变也不会抛异常

  • 使用线程安全的集合,如CopyOnWriteArraylist、ConcurrentHashMap等


4、系统服务(FrameWork API)



  • 调用系统服务通常需要跨进程通信,其内部很可能会抛异常,所有调用系统服务的地方都必须使用try cache。cache异常必须写入日志文件,根据业务重要性判断是否需要上报埋点数据;

  • 系统服务频繁调用时可能会引发ANR,这点也需要特别注意;


5、数据库类问题


由于我们的业务重度依赖数据库,所以数据库相关的问题占比也比较高。
主要有以下几类问题:


CursorWindowAllocationException 2048问题:


com.tencent.wcdb.CursorWindowAllocationException: 
Cursor window allocation of 2048 kb failed. total:8159,active:49
at com.tencent.wcdb.CursorWindow.<init>(SourceFile:127)

针对CursorWindowAllocationException,在我们的工程中主要是短时间内大量的内存申请。 解决方案是基于SQL监控,统计工程中SQL执行的数量,基于SQL语句针对性的优化相关逻辑,将SQL语句执行数量降低了90%以上,这个问题线上不再复现。


存储空间不足


Caused by: 
com.tencent.wcdb.database.SQLiteFullException:database or disk is full (code 13,errno 0):
at com.tencent.wcdb.database.SQLiteConnection.nativeExecute(Native Method)
at com.tencent.wcdb.database.SQLiteConnection.execute(SourceFile:728)
at com.tencent.wcdb.database.SQLiteSession.endTransactionUnchecked(SourceFile:436)
at com.tencent.wcdb.database.SQLiteSession.endTransaction(SourceFile:400)
at com.tencent.wcdb.database.SQLiteDatabase.endTransaction(SourceFile:533)
at com.tencent.wcdb.room.db.WCDBDatabase.endTransaction(SourceFile:100)

针对存储空间不足,在我们的APP中主要是添加手机存储空间检测,当空间不足时引导用户清理。


数据库损坏


com tencent wcdb database.SQLiteDatabaseCorruptException: database disk image is malformed (code 11, errno 0): 
at com.tencent.wcdb.database.SQLiteConnection.nativePrepareStatement(Native Method)
at com.tencent.wcdb.database.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:1004)
at com,tencent.wcdb.database.SQLiteConnection.executeForString(SQLiteConnection.java:807)
at com.tencent.wcdb.database.SQLiteConnection.setJournalMode(SQLiteConnection.java:424)
at com.tencent.wcdb.database.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:414)
at com.tencent.wcdb.database.SQLiteConnection.open(SQLiteConnection.java:289)
at com.tencent.wcdb.database.SQLiteConnection.open(SQLiteConnection.java:254)
at com,tencent.wcdb.database.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:603)
at com.tencent.wcdb.database.SQLiteConnectionPool.open(SQLiteConnectionPool.java:225)
at com.tencent.wcdb.database.SQLiteConnectionPool.open(SQLiteConnectionPool.java:217)
at com.tencent.wcdb.database.SQLiteDatabase.openInner(SQLiteDatabase.java:1002)

数据库损坏我们是引入了修复工具进行修复,同时对数据库损坏崩溃进行拦截。修复完成后退出到登录页面引导用户重新登录。


数据库的崩溃问题一度在我们的工程中占比超过50%,所以我们有启动数据库优化专项投入大量时间。针对数据库优化的具体介绍可以查看:Android 数据库系列三:复杂项目SQL治理与数据库的优化总结
,内部有更加详细的介绍。


四、OOM问题收敛


1、OOM介绍


OOM的问题在Android中也是非常常见了,所以这里单独拎出来说说。
OOM产生的条件:待申请的内存大于系统分配给应用的剩余内存。
OOM原因大致可以归为以下几类



  • 堆内存分配失败

    • 堆内存溢出

    • 没有足够的连续内存空间



  • 创建线程失败(pthread_create (1040KB stack) failed: Try again)

  • FD数量超出限制

  • Native虚拟内存OOM


2、内存泄露监控


线上内存泄露的监控我们是使用的快手的KOOM。
KOOM原理这里笔者就不详解了,社区内也有专门分析的文章,大家可以找找看,不过还是建议去读读源码,写的挺不错的。



指路地址:github.com/KwaiAppTeam…



将KOOM分析的报告上报到我们的后台中,有专门的同学每周会排时间跟进。


3、全局浮窗实时显示APP当前总体内存


除了线上的监控,我们也有一个自研的开发者工具。工具有一个浮窗功能,我们会在浮窗上实时显示当前应用的内存信息(每秒采集一次)。数据主要是通过获取Debug.MemoryInfo#getTotalPss()。与Android Studio Profile中Memory数据基本一致。同时在UI层面我们还会设置一个阈值,超过阈值就会将浮窗中内存的数值颜色改为红色,旨在提醒开发同学关注内存变化。


通过实时显示内存我们在开发过程中,就可以发现一些问题。如我们再进入某一个业务时,发现内存会固定涨50M+,基于此开发同学去排查,发现了很多优化点。线下发现这个问题的意义就是可以质量左移,避免到线上影响到用户。


4、线程数量监控与收敛


获取线程数量,我们可以读取文件/proc/[pid]/status 中的线程数量,代码大致如下:


public static String readThreadStatus(String pid){
RandomAccessFile reader2= null;
try {
reader2 = new RandomAccessFile("/proc/" + pid + "/status", "r");
String str;
while ((str = reader2.readLine()) != null) {
if (str.contains("Threads")) {
return str;
}
}
reader2.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader2 != null) {
reader2.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return "";
}


在前面提到的开发者工具的浮窗中,我们也有一行用来实时显示线程的数量。不过在我们的工具中,我们没有使用上面的方法,而是使用:Thread.getAllStackTraces();


使用Thread.getAllStackTraces();获取的数量比 /proc/[pid]/status 获取的少,但是在我们的工程中,我们主要关注Java线程而且通过Java线程的数量的波动也能观察到App当中线程的变化。而且Thread.getAllStackTraces()会返回Thread对象,以及堆栈数据这个对我们更加有用。


在开发者工具我们有一个单独的页面可以实时查看线程的ID、名称以及对应堆栈。在线上我们会间隔一段收集一次线程数据上报到我们的后台中。


在笔者过往的开发经历中,遇到过一次由于线程数量较多直接导致应用崩溃的情况,即某个独立业务使用OkHttp没有创建OkHttpClient单例对象,而是每次接口回调都创建一个新的client...


定位过程比较简单,在特定的场景下,可以看到浮窗中的线程数量基本处于线性增长,通过开发者工具查看线程列表可以直接看到非常多的OkHttp相关的线程。


5、FD 数量监控


获取FD数量大致可以通过以下代码


public static int getCurrentFdSize() {
int size = 0;
File dir = new File("/proc/self/fd");
try {
File[] fds = dir.listFiles();
if (fds != null) {
size = fds.length;
for (File fd : fds) {
if (Build.VERSION.SDK_INT >= 21) {
MLog.d("message", Os.readlink(fd.getAbsolutePath()));
}
}
}

} catch (Exception e) {
e.printStackTrace();
}
return size;
}


同时在KOOM中每次分析的结果中会携带所有FD句柄信息,所以我们没有单独做额外的监控了,直接查看KOOM的解析数据。


笔者仅遇到过一次由于FD句柄超限导致的异常。异常信息如下


java.lang.RuntimeException: Could not read input channel file descriptors from parcel.
at android.view.InputChannel.nativeReadFromParcel(Native Method)
at android.view.InputChannel.readFromParcel(InputChannel.java:148)
at android.view.InputChannel$1.createFromParcel(InputChannel.java:39)at android.view.InputChannel$1.createFromParcel(InputChannel.java:37)
at com.android.internal.view.InputBindResult.<init>(InputBindResult.java:68)
at com.android.internal.view.InputBindResult$1.createFromParcel(InputBindResult.java:112)at com.android.internal.view.InputBindResult$1.createFromParcel(InputBindResult.java:110)at com.android.internal.view.IInputMethodManager$Stub$Proxy.startInputOrWindowGainedFocus(IInp
at android.view.inputmethod.InputMethodManager.startInputInner(InputMethodManager.java:1361)
at android.view.inputmethod.InputMethodManager.onPostWindowFocus (InputMethodManager.java:1631)
at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:4259)
at android.os.Handler.dispatchMessage(Handler.java:109)
at android.os.Looper.loop(Looper.java:166)

后面经过排查进入内置浏览器查看某网页,FD句柄的数量会瞬间飙升。通过遍历 /proc/pid/fd 文件发现大多是都是Socket。后面排查下来是应用内某个前端页面存在Bug,疯狂new Socket...


五、总结


以上简单介绍了一下我们在工程中如何针对各类崩溃信息进行收敛,值得欣喜的是经过几年的努力基本可以将崩溃控制在万一。回过头来看很多问题还是遇到问题-解决问题的思路,这就依赖我们的开发同学本身所写的代码质量要高,否则就会陷入到写Bug-改Bug这样的循环中。


所以我们也在积极探索如何通过前期的review,工具扫描的方式尽量降低线上问题的发生概率。尽可能将问题提前暴露。不过这方面目前还没有建设的特别好,人工review实验了一段时间发现在一个业务较多的团队的实施起来很难,没有时间不说盯着代码review也不见得就能发现一些逻辑上的异常。好在公司内其他团队再研究基于AI的代码扫描,后续计划接入到当项目中看看。


作者:半山居士
来源:juejin.cn/post/7377200392059617295
收起阅读 »

未登录也能知道你是谁?浏览器指纹了解一下!

web
引言 大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢? 本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。 浏览器指纹 浏览器指纹是指通过浏览器的特征来唯一标识用户身份的...
继续阅读 »

引言


大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢?


本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。


浏览器指纹


浏览器指纹是指通过浏览器的特征来唯一标识用户身份的一种技术。


它通过记录用户浏览器的一些基本信息,包括操作系统、浏览器类型、浏览器版本、屏幕分辨率、字体、颜色深度、插件、时间戳等,通过这些信息,可以唯一标识用户身份。


应用场景


其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:



  • 资讯等网站:精准推送一些你感兴趣的资讯给你看

  • 购物网站: 精确推送一些你近期浏览量比较多的商品展示给你看

  • 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?

  • 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为

  • 网站统计: 通过浏览器指纹,网站可以统计用户的访问信息,比如用户的地理位置、访问时间、访问频率等,从而更好的为用户提供服务


如何获取浏览器指纹


指纹算法有很多,这里介绍一个网站 https://browserleaks.com/ 上面介绍了很多种指纹,可以根据自己的需要选择。



这里我们看一看canvas,可以看到光靠一个canvas的信息区分,就可以做到15万用户只有7个是重复的,如果结合其他信息,那么就可以做到更精准的识别。


canvas指纹


canvas指纹的原理就是通过 canvas 生成一张图片,然后将图片的像素点信息记录下来,作为指纹信息。


不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的。


具体步骤如下:



  1. 用canvas 绘制一个图像,在画布上渲染图像的方式可能因web浏览器、操作系统、图形卡和其他因素而异,从而生成可用于创建指纹的唯一图像。在画布上呈现文本的方式也可能因不同web浏览器和操作系统使用的字体渲染设置和抗锯齿算法而异。




  1. 要从画布生成签名,我们需要通过调用toDataURL() 函数从应用程序的内存中提取像素。此函数返回表示二进制图像文件的 base64 编码字符串。然后,我们可以计算该字符串的MD5哈希来获得画布指纹。或者,我们可以从IDAT块中提取 CRC校验和IDAT块 位于每个 PNG 文件末尾的16到12个字节处,并将其用作画布指纹。


我们来看看结果,可以知道,无论是否在无痕模式下,都可以生成相同的 canvas 指纹。
在这里插入图片描述


换台设备试试



其他浏览器指纹


除了canvas,还有很多其他的浏览器指纹,比如:


WebGL 指纹


WebGL(Web图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D2D 图形,而无需使用插件。


WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5 元素中使用。


这种一致性使 API 可以利用用户设备提供的硬件图形加速。


网站可以利用 WebGL 来识别设备指纹,一般可以用两种方式来做到指纹生产:


WebGL 报告——完整的 WebGL 浏览器报告表是可获取、可被检测的。在一些情况下,它会被转换成为哈希值以便更快地进行分析。


WebGL 图像 ——渲染和转换为哈希值的隐藏 3D 图像。由于最终结果取决于进行计算的硬件设备,因此此方法会为设备及其驱动程序的不同组合生成唯一值。这种方式为不同的设备组合和驱动程序生成了唯一值。


可以通过 Browserleaks test 检测网站来查看网站可以通过该 API 获取哪些信息。


产生 WebGL 指纹原理是首先需要用着色器(shaders)绘制一个梯度对象,并将这个图片转换为Base64 字符串。


然后枚举 WebGL 所有的拓展和功能,并将他们添加到 Base64 字符串上,从而产生一个巨大的字符串,这个字符串在每台设备上可能是非常独特的。


例如 fingerprint2js 库的 WebGL 指纹生产方式:


HTTP标头


每当浏览器向服务器发送请求时,它会附带一个HTTP标头,其中包含了诸如浏览器类型、操作系统、语言偏好等信息。


这些信息可以帮助网站优化用户体验,但同时也能用来识别和追踪用户。


屏幕分辨率


屏幕分辨率指的是浏览器窗口的大小和设备屏幕的能力,这个参数因用户设备的不同而有所差异,为浏览器指纹提供了又一个独特的数据点。


时区


用户设备的本地时间和日期设置可以透露其地理位置信息,这对于需要提供地区特定内容的服务来说是很有价值的。


浏览器插件


用户安装的插件列表是非常独特的,可以帮助形成识别个体的浏览器指纹。


音频和视频指纹


通过分析浏览器处理音频和视频的方式,网站可以获取关于用户设备音频和视频硬件的信息,这也可以用来构建用户的浏览器指纹。


webgl指纹案例


那么如何防止浏览器指纹呢?


先讲结论,成本比较高,一般人不会使用。


现在开始实践,根据上述的原理,我们知道了如何生成一个浏览器指纹,我们只需要它在获取toDataURL时,修改其中的内容,那么结果就回产生差异,从而无法通过浏览器指纹进行识别。


那么,我们如何修改toDataURL的内容呢?


我们不知道它会在哪里调用,所以我们只能通过修改它的原型链来修改。


又或者使用专门的指纹浏览,该浏览器可以随意切换js版本等信息来造成无序的随机值。


修改 toDataURL


第三方指纹库


FingerprintJS



FingerprintJS是一个源代码可用的客户端浏览器指纹库,用于查询浏览器属性并从中计算散列访问者标识符。


cookie和本地存储不同,指纹在匿名/私人模式下保持不变,即使浏览器数据被清除。


ClientJS Library



ClientJS 是另一个常用的JavaScript库,它通过检测浏览器的多个属性来生成指纹。


该库提供了易于使用的接口,适用于多种浏览器指纹应用场景。




作者:我码玄黄
来源:juejin.cn/post/7382344353069088803
收起阅读 »

h5 如何跳转微信小程序(uni-app)?

web
前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。 遇到问题点 微信内部跳转怎么实现? 外部浏览器或者app打开链接如何跳转? 微信内部跳转怎么实现 使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签...
继续阅读 »

前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。


遇到问题点



  • 微信内部跳转怎么实现?

  • 外部浏览器或者app打开链接如何跳转?


微信内部跳转怎么实现

使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签跳转小程序,需要满足3个条件



  • 首先 wx.config 授权,公众号设置 > 功能设置 > js安全域名设置

  • 域名设置,静态资源托管

  • 小程序需要关联这个公众号 设置 > 基本设置 > 相关公众号


h5上代码源码



<html>
<head>
<title>打开小程序</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<script>
window.onerror = e => {
console.error(e)
alert('发生错误' + e)
}
</script>
<!-- 引入jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<!-- weui 样式 -->
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.4.1/weui.min.css"></link>
<!-- 调试用的移动端 console -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
<!-- 公众号 JSSDK -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 云开发 Web SDK -->
<script src="https://res.wx.qq.com/open/js/cloudbase/1.1.0/cloud.js"></script>
<script>
const baseUrl ="/pages/biddingCenter/index"
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}

function docReady(fn) {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
fn()
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
var version = 'trial'
var fetchData = new Promise((resolve, reject) => {
// 获取签名,timestamp、nonceStr、signature
$.ajax({
url: "xxxxxxx",
dataType: "json",
method: 'get',
data: { xxx },

success: function (res) {
console.log("WeChatConfig", res);
if (res.data) {
var data = res.data; // 根据实际情况返还的数据进行赋值
console.log(999, data)
resolve(data);
}
},
error: function (error) {
console.error('error-->', error)
return reject(error)
}
})
});



docReady(async function() {
var ua = navigator.userAgent.toLowerCase()
var isWXWork = ua.match(/wxwork/i) == 'wxwork'
var isWeixin = !isWXWork && ua.match(/MicroMessenger/i) == 'micromessenger'
var isMobile = false
var isDesktop = false
if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|IEMobile)/i)) {
isMobile = true
} else {
isDesktop = true
}
console.warn('ua', ua)
console.warn(ua.match(/MicroMessenger/i) == 'micromessenger')
var m = ua.match(/MicroMessenger/i)
console.warn(m && m[0] === 'micromessenger')

if (isWeixin) {
var containerEl = document.getElementById('wechat-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'wechat-web-container')

var launchBtn = document.getElementById('launch-btn')
launchBtn.addEventListener('ready', function (e) {

launchBtn.setAttribute('env-version',version)
launchBtn.setAttribute('path', baseUrl)

console.log('开放标签 ready')
})
launchBtn.addEventListener('launch', function (e) {
console.log('开放标签 success')
})
launchBtn.addEventListener('error', function (e) {
console.log('开放标签 fail', e.detail)
})

await fetchData.then(res=> {
wx.config({
// debug: true, // 调试时可开启
appId: res.appId,
timestamp: res.timestamp, // 必填,填任意数字即可
nonceStr: res.nonceStr, // 必填,填任意非空字符串即可
signature: res.signature, // 必填,填任意非空字符串即可
jsApiList: ['chooseImage'], // 安卓上必填一个,随机即可
openTagList:['wx-open-launch-weapp'], // 填入打开小程序的开放标签名
})
})

} else if (isDesktop) {
// 在 pc 上则给提示引导到手机端打开
var containerEl = document.getElementById('desktop-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'desktop-web-container')
} else {

var containerEl = document.getElementById('public-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'public-web-container')
var c = new cloud.Cloud({
// 必填,表示是未登录模式
identityless: true,
// 资源方 AppID
resourceAppid: 'wxaabe7e5d652fc1d8',
// 资源方环境 ID
resourceEnv: 'cloud1-7gnlxfema33074ec',
})
await c.init()
console.log(c)
window.c = c

var buttonEl = document.getElementById('public-web-jump-button')
var buttonLoadingEl = document.getElementById('public-web-jump-button-loading')
try {
await openWeapp(() => {
console.log('dui')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
})
} catch (e) {
console.log(e,'cuo')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
throw e
}
}
})

async function openWeapp(onBeforeJump) {
var c = window.c
const res = await c.callFunction({
name: 'public',
data: {
action: 'getUrlScheme',

},
})

if (onBeforeJump) {
onBeforeJump()
}
location.href = res.result.openlink
}
</script>
<style>
.hidden {
display: none;
}

.full {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

.public-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.public-web-container p {
position: absolute;
top: 40%;
}

.public-web-container a {
position: absolute;
bottom: 40%;
}

.wechat-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.wechat-web-container p {
position: absolute;
top: 40%;
}

.wechat-web-container wx-open-launch-weapp {
position: absolute;
bottom: 40%;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container p {
position: absolute;
top: 40%;
}
</style>
</head>
<body>
<div class="page full">
<div id="public-web-container" class="hidden">
<p class="">正在打开 “ 小程序”...</p>
<a id="public-web-jump-button" href="javascript:" class="weui-btn weui-btn_primary weui-btn_loading" onclick="openWeapp()">
<span id="public-web-jump-button-loading" class="weui-primary-loading weui-primary-loading_transparent"><i class="weui-primary-loading__dot"></i></span>
小程序
</a>
</div>
<div id="wechat-web-container" class="hidden">
<p class="">点击以下按钮打开 “小程序”</p>
<!-- 跳转小程序的开放标签。文档 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html -->
<wx-open-launch-weapp id="launch-btn" username="gh_36a8dca79910" path="/pages/biddingCenter/index" env-version="trial">
<template>
<button style="width: 200px; height: 45px; text-align: center; font-size: 17px; display: block; margin: 0 auto; padding: 8px 24px; border: none; border-radius: 4px; background-color: #07c160; color:#fff;">打开小程序</button>
</template>
</wx-open-launch-weapp>
</div>
<div id="desktop-web-container" class="hidden">
<p class="">请在手机打开网页链接</p>
</div>
</div>
</body>
</html>

以上第二点后面会讲到
以上功能满足基本可以在微信内部打开小程序了,但是实际上,我们url可能是其他app,短信,或者浏览器里面打开的


外部app或者短信跳转怎么实现

首先我这边是uni-app实现的



  • 第一步在项目根目录项目新建functions这个文件夹,然后到


image.png
点击下载放入刚刚的functions下面,然后再 manifest.json 下面


 "mp-weixin" : {
"appid" : "xxxxx",
"cloudfunctionRoot": "./functions/", // 这一行就是标记云函数目录的字段
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {}
}

这个时候编译项目在编译之后的小程序项目是没有functions 这个文件夹的,我们这个时候需要在vue.config.js


const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, 'functions'),
to: path.join(__dirname, 'unpackage/dist', process.env.NODE_ENV === 'production' ? 'build' : 'dev', process.env.UNI_PLATFORM, 'functions')
}
]
})
]
}
}

在终端上面npm install 才行,需要node环境
设置以上代码重新启动编译,在微信工具里面就会出现


image.png


image.png


右击public上传并部署(云端安装依赖),成功之后打开微信开发工具云开发


image.png


image.png


会出现刚刚的云函数文件
然后再次点击云函数权限


image.png
点击所有用户访问,保存


image.png


然后再点击右上角设置>权限设置> 未登录用户访问云资源权限设置打开


image.png


以上搞定也只是函数环境搞定,然后还要配置刚刚的函数方法api


image.png


只有配置这个才能在index方法里面调用


// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()

switch (event.action) {
case 'getUrlScheme': {
return getUrlScheme(event)
}
}

return 'action not found'
}

async function getUrlScheme(event) {

return cloud.openapi.urlscheme.generate({
jumpWxa: {
path:'/pages/index/index', // <!-- replace -->
env_version:'trial',
},
// 如果想不过期则置为 false,并可以存到数据库
isExpire: false,
// 一分钟有效期
expireTime: parseInt(Date.now() / 1000 + 60),
})
}


代码讲到这边会发现之前h5里面调用的 c.callFunction 方法其实调用的就是exports.main 这个导出得方法
然后还需要把h5代码放入


image.png


image.png
以上就是整个h5跳转微信小程序的过程,需要注意的点事在h5里面 方法返回 res.result.openlink 这个参数需要跳转的url在生产上线过,要不然报错 openapi.urlscheme.generate pagepath 获取不到


作者:成书平
来源:juejin.cn/post/7264772939773526075
收起阅读 »

25k大专前端外包从深圳回武汉能拿多少?

2023 年 08 月我正式从深圳公司离职,从 7 月初开始投武汉的公司,截止 8 月底,2 个月时间有 5 个面试 3 个 offer:一个自研的 22k、两个外包一个 17k,一个 18k。最终选了一家离我比较近的 18k offer,但遗憾的是刚去第一天...
继续阅读 »

2023 年 08 月我正式从深圳公司离职,从 7 月初开始投武汉的公司,截止 8 月底,2 个月时间有 5 个面试 3 个 offer:一个自研的 22k、两个外包一个 17k,一个 18k。最终选了一家离我比较近的 18k offer,但遗憾的是刚去第一天就发现坑太深,还是决定放弃这家公司,目前失业在家,这里和大家聊一聊最近从深圳回武汉找工作的经历。

基本情况

先大致介绍下我的基本情况:大专学历,30+,从 17 年开始做全职前端开发,到现在有 6-7 年了,属于一年工作经验用 5 年的那种,Vue 一把梭,技术一般。进不了中、大厂,只能在外包混混日子。

时间回到两年前,2021 年 8 月前端行情好的时候,我凭运气找到了一家深圳二线互联网公司的前端外包岗位,offer 是 25k * 12,到今年 8 月正好呆了两年左右,为了说明真实性,下面附上我 2022 年度个税 App 收入纳税截图。

2-2022.png

声明

本人在网上冲浪时从未公开过这家我呆了两年的公司,出于薪资保密原则,如果有人认出我,知道我所在的这家公司,还请不要透露公司名字,万分感谢!

另外,为了避免纠纷,后面面试的公司,我都会进行匿名处理,如果有人猜到公司名称,评论还请使用化名,希望大家能理解。

深圳回武汉

从 21 年入职这家公司开始,这两年前端行情越来越差,目前我这个学历、技术水平比较难找到 25k 以上的工作。我有考虑过要不要先苟在这家公司,毕竟这个工作工资还可以,leader、同事、工作氛围都不错。但由于各方面原因,最终还是决定回武汉。

  1. 工作方面:我做的大多是技术需求,做的比较无聊,成就感较低;另外,新需求越来越少,蛋糕就这么大,僧多粥少,发挥空间较小。
  2. 生活方面:我时常在反思,我是不是一个精致的利己主义者?这些年基本就过年回家,回家也呆不了几天,在照顾父母、关心家人这方面我是做的比较差的,如果我只想着自己能不能拿高工资,自己过的是否惬意,我觉得这是很自私的,回武汉离家近可以很好的解决这个问题。

在业务需求少后,部门也有了裁员的消息,我正好在这边快两年了,也想回武汉换个环境。

为了变被动为主动,就在 7 月初开始投武汉的公司了,计划拿到 offer 就离职回去。因为在行业下行周期,越想往上挣扎越累,还不一定有好的结果,不如顺势躺平,好好享受生活。

简历投递面试数据

23 年 6 月 20 号左右,将简历开放,状态修改为在职-看机会。过了一段时间,发现没 hr 联系我,行情确实差了很多,之前简历一开放,一堆 hr 主动找你,这个时候还没主动投。

一直到 23 年 7 月 3 号,我终于修改好了简历,开始投简历。但如下图,简历比较难投出去,需要双方回复才能投。

3-boss-huifu.png

于是我又下载了拉钩、猎聘。拉钩猎聘大部分都可以直接投,但拉钩 20k+ 武汉的岗位很少,猎聘投了很多也没回复,整体还是 BOSS 上面试机会最多,下面是具体数据

App 类型沟通投递面试机会面试通过/Offer
BOSS13186142
猎聘-29411
拉钩-62(投递反馈)00

卡学历问题

我基本把武汉的 20k+ 前端岗位都投了一遍,但基本没有中、大厂都能通过简历筛选。分三种情况

  1. 没有任何回复(最多)
  2. 回复看了简历不合适(个别)
  3. 直接指出学历不符合(个别)

4-xueli.png

虽然我有自考本科+学士学位也没啥用,一般还是至少要统招本科及以上。当然也有可能会是年龄、技术菜、要的工资高等其他因素。

4-2-xueli.png

面试记录

某电商小公司 - 自研 22k(过)

来源:猎聘 App,岗位:中高级前端开发工程师(自研)(14-22k)

2023 年 7 月 10 号,在投了一个星期后,终于有了第一个面试,晚上 19:00 腾讯会议远程面,大概面了一个小时,问的问题不难,比如

  • 先自我介绍
  • 垂直居中有几种方式?
  • flex: 2 有用过吗?多列布局怎么实现?
  • 怎么判断对象为空?
  • 寻找字符串中出现最多的字符怎么实现?
  • 知不知道最新的 url 参数获取的 API?
  • 实现深拷贝
  • 实现 Promise
  • 新版本发布后,怎么用技术手段通知用户刷新页面?
  • 性能优化数据怎么上报、分析?
  • Vue 组件通信方式有哪些,各有什么特点?
  • Vue 项目怎么提高项目性能?举一些例子
  • element ui table 吸顶怎么做,滚动怎么处理等
  • 你有什么想问我的?

然后还问了一些项目问题,能不能加班,因为虽然双休,但周一到周五会有 3 天加班等。基本没有问啥原理性的问题,就是看基础怎么样,能不能干活。

面试第二天,没有消息,我以为挂了,但隔了一天,7 月 12 号,HR 电话二面,我问了我的一些基本情况后,表示可以直接发 offer,确定薪资为 22k,但其中 2.2k 要当做季度绩效发放,说的是一般不犯啥错误都可以拿到。下面是 offer 截图

5-offer-1.png

沟通入职时间定的 8 月 1 号,比较坑的是甲方都同意 7 月底可以走,外包公司这边不同意,要到 8 月中才放我走,合同确实是这样写的,我也不好说啥。

这家公司比较着急,觉得等的时间有点长了,1个月+,风险有点高。我也不能说让别人一直等,只能说,让他们可以先考虑其他候选人,这家公司过了段时间招到人了,这个 offer 就黄了。

(后面回想起来,我可能有点傻,规定是死的,人是活的,应该直接按甲方允许的 7 月底时间来,这样 offer 就没问题了。如果我们公司不让我走,我可以直接走人,就当旷工,直接被开除就行,只是没有离职证明,但工资流水是有的)

武汉某小公司 - 自研 (12-20k)x

来源:BOSS,岗位:前端开发工程师 - 自研(12-20k)14薪

在上面的 22k 这个 offer 时间有冲突的时候,我就意识到这个 offer 有风险,就开始继续投了。

到 23 年 8 月 2 号终于又有了面试机会,一面是笔试,如下图

6-hema.png

有 4 题,最后一题最简单,第 1、2 题忘记了,1、2、3 我都是用递归实现的,3、4 题如下

  1. _.flatten() 实现一个数组打平方法,支持第二个参数(可指定打平层级)
const array = [[0, 1], [2, [3, 4]], [5, 6]];
const result = _.flatten(array);
  1. 菜单数组转换为嵌套树形结构,但示例只有两级
[
{ id: 1, menu: '水果', level: 1 },
{ id: 2, menu: '橘子', level: 2, parentId: 1 }
// ...
]
// 转换为
[
{
id: 1, menu: '水果', level: 1, children: [{ id: 2, menu: '橘子', level: 2, parentId: 1 }]
},
// ...
]

笔试难度一般,主要靠思维,难度比 leetcode 算法题低,算是过了。

二面是 8 月 7 号电话面,19:00 - 20:00 一个小时左右,大部分问题都忘记了,模糊记得部分问题

  • 先自我介绍
  • 把之前的笔试题一题一题拿出来讲实现思路。
  • 对象的继承有哪几种?
  • TS 用的多吗?
  • 工作中解决的最有成就感的事?
  • vue3 在某些场景比 vue2 性能更低,为什么会这样?
  • 在团队协作时,有遇到过什么问题吗,如果有冲突你会怎么做
  • 你有什么想问我的?

另外面试小哥对我之前有两家半年左右的工作经历比较在意,问了很多之前公司的细节,因为他说之前有面试过的最后背调没通过,所以要问清楚。我的简历写的很真实,基本没有水分,是什么就是什么。

他最后透露,可能就算他可以过,但 HR 那边可能过不了,不知道是我跳槽太频繁还是啥,总之后面基本没消息了,这个算是挂了。

某上海武汉分公司 - 自研(18-23k)x

来源:BOSS,岗位:前端开发 自研(18-23k)

上次面试的挂了之后,继续投,但没面试机会,后面又忙搬家、邮寄东西,回武汉,找房子等,中间大概用了一个多星期。

在 8 月 18 号终于又有了一个自研的面试, 15:40 腾讯会议线上一面 - 技术面,上海那边的开发负责面试,问了一些问题,比较普通,我现在毫无印象。

一面过了,在 8 月 22 日,13:00 二面(现场面),公司办公地点在武昌火车站地铁口,刚开始觉得还不错,但一进去,一个开发都没有,就 1 个人,直接无语...... 武汉算是分部,那个人还不懂技术,和我吹了一下公司怎么怎么厉害,先是做了一份笔试题(比较基础)比如

  • 3 种方式实现顶部导航+左侧菜单+右侧主内容区域布局
  • jwt 鉴权逻辑
  • vue 数组下标改值,响应式丢失、为什么

7-hangshu.png

然后那个人拍了我写的笔试题,让上海那边的人看,说是做的不错。再视频连线进行面试,大致问了一些基础问题,然后坑的地方来了。我之前待过的公司,一个一个问我离职原因。。。。。。

然后就是副总面,问我有没有做过异形屏的适配,有没有写过绘制、渲染逻辑,我。。。。。。然后又问了我好几个假大空的问题,我一脸懵逼,比如一个公司呆 8 年和 8 年每年换一家公司你觉得哪种好。

后面就是回去等消息了,然后就没有然后了。。。。。

某金融公司 - 外包 17k(过)

来源:BOSS,岗位:前端开发 - 外包 17k

和上面那个公司同一时间段,在 8 月 18 号也进行了这家公司的腾讯会议一面

一面比较简单,大致为了下工作经历,重点问了下低代码、怎么动态加载渲染一个组件,底层怎么实现?面试时间比较短,有点仓促

8 月 21 号二面,大致问了一些问题后,还是追问低代码方面的问题,组件级别、可以内嵌到其他指定页面的这种低代码 sdk 封装怎么做?他们是想招个会低代码,有过 sdk 封装经验的。我之前工作中有做过组件库,封装过百万用户级别的小程序 sdk、也做过功能引导、错误上报等 sdk,还自己实现过多个 npm 包轮子,算是勉强符合他们的要求。

二面过了后,开始谈薪资,17k,基本不加班,8 月 23 号三面笔试(类似走过场),有题库,刷一下就没问题,通过就发邮件 offer 了。

8-zhengquan.png

这家公司过了,但我没有接轻易接 offer,而是让 HR 等第二天中午我的反馈,我不想接了别人 offer 又不去。这家公司的 HR 比较好、很热心积极。

主要有以下几个原因

  1. 后面还有一个 18k 的也是同一天二面,且面试体验好,大概率过了,只等确定 offer。
  2. 这家比较远,在花山,而后面一家离我比较近
  3. 这家试用期打折,下面一家不打折。

最终拒了这家 offer,因为下面要讲的这家 offer 下来了,前方高能预警,后面这家公司巨坑、后悔拒了这家。。。。

某互联网公司 - 外包 18k(过)

来源:BOSS,岗位:前端开发(外包)18k

和上面那家几乎同一时间,这家公司也进行了两轮面试

一面,腾讯会议,从 3-4 个 UI 中,选一个题来实现,30 分钟,就是平常干活画 UI,难度不大,面试官是个声音好听的妹子。

二面,腾讯会议,结对编程,面试官出题,我描述实现,面试官写代码,包括

  1. 一个简单的需要使用 Promise 应用题
  2. 运行一个 vue 项目,vue2 写法改 vue3 写法,封装一个计时器组件,组件加 props,组件加插槽等

面试体验真的很好,18k offer 下来后,果断选择了这家离我近的公司。

9-offer-3.png

但没想到的是,入职第一天发现这家公司管理问题很大

  1. 开发环境差,只能用网页版的 vscode,除了要配置 host 外,还有配置端口映射,配置稍微有问题就运行不起来,体验较差。
  2. 沟通太依赖线上,武汉这边基本是xx一线城市那边的产品、UI、开发分配任务给这边开发,沟通成本非常高。
  3. 加班问题,说的是早 9 晚 6,但他们自研一般下班这个点可能会去吃个饭, 然后回来加班,git log 看了下提交记录,不少是 20:00 之后的,还有 21 点、22 点之后的.... 如果真融入这个团队,不加班我是不信的。

从面试体验、沟通来看,这里的开发人员是优秀的,但实际入职却发现环境、氛围差的情况,我只能把这种问题归纳到管理上了。

第一天基本没干活就是配置环境,但这个氛围,我真的接受不了,后面就果断放弃这家公司了。

武汉找工作经验总结

上面我大致描述了从 7 月初到 8 月底的简历投递、面试经历。主要是面试少,实际面试通过率为 60%。下面是一些总结

  • 投递简历时段最好是周一到周三上午 8-9 点,回复、面试机会较多,周五到周天基本没反应。
  • 武汉原理性问的不多,主要还是能干活,比较需要多面手,就是什么都会的,比如 WebGL, Three.js,uni-app 等
  • 一定要问清楚、开发环境、加班问题,不要不好意思,能找自研就尽量找自研。
  • 不要听 HR 或者面试官怎么说,而是自己通过行业、所做的业务去判断是否有坑。

完结撒花,如果觉得内容对您有帮助,那就点个免费的赞吧~~

另外最近有和我一样在找工作的小伙伴吗?你们有遇到过什么坑吗?欢迎在评论区讨论~~~


作者:dev_zuo
来源:juejin.cn/post/7275225948453568552
收起阅读 »

【小程序分包】小程序包大于2M,来这教你分包啊

web
前言🍊缘由该大的不大,小程序包超出2M,无法上传发布前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,...
继续阅读 »

前言

🍊缘由

该大的不大,小程序包超出2M,无法上传发布

前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,遂将小程序分包,彻底解除封印,特来跟大家分享下如何将小程序分包,减小主包大小


🎯主要目标

实现2大重点

  1. 如何进行小程序分包
  2. 如个根据分包调整配置文件

🍈猜你想问

如何与狗哥联系进行探讨

关注公众号【JavaDog程序狗】

公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹。

此群优势:

  1. 技术交流随时沟通
  2. 任何私活资源免费分享
  3. 实时科技动态抢先知晓
  4. CSDN资源免费下载
  5. 本人一切源码均群内开源,可免费使用

2.踩踩狗哥博客

javadog.net

大家可以在里面留言,随意发挥,有问必答


🍯猜你喜欢

文章推荐

正文

🍵三个问题

  1. 为什么小程序会有2M的限制?
  1. 用户体验:小程序要求在用户进入小程序前能够快速加载,以提供良好的用户体验。限制小程序的体积可以确保小程序能够在较短的时间内下载和启动,避免用户长时间的等待。
  2. 网络条件:考虑到不同地区和网络条件的差异,限制小程序的体积可以确保在低速网络环境下也能够较快地加载和打开小程序,提供更广泛的用户覆盖。
  3. 设备存储:一些用户使用的设备可能存储空间有限,限制小程序的体积可以确保小程序可以在这些设备上正常安装和运行。
  1. 如何解决包过大问题?
  1. 优化代码,删除掉不用的代码
  2. 图片压缩或者上传服务器
  3. 分包加载
  1. 什么是分包加载?

小程序一般都是由某几个功能组成,通常这几个功能之间是独立的,但会依赖一些公共的逻辑,且这些功能一般会对应某几个独立的页面。那么小程序代码的打包,可以按照功能的划分,拆分成几个分包,当需要用到某个功能时,才加载这个功能对应的分包。


🧀实操分包步骤

1.查看项目结构

通过上方三个问题,我们开始具体分包流程,首先看一下分包前项目结构pages.json配置文件

pages.json
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/card/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/device/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/order/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/product/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

2.分析主包大小

微信开发者工具中,查看【详情】进行分析,此处本地代码只有一个主包大小399.8KB

3.参考文档

本文以uniapp为实操介绍案例

小程序官方文档:

developers.weixin.qq.com/miniprogram…

uniapp 分包文档:

uniapp.dcloud.net.cn/collocation…

4. 结构调整

将咱们项目结构按照如下图所示进行拆分

新建subPages_A 和 subPages_B,将pages下不同页面移入进新增的两个包,此处subPages_A的名字只做示例,实际要按照标准命名!

比较下之前项目结构,此处项目会报错,不用担心,稍后修改pages.json


5. 修改pages.json

根据上一步拆分的包路径,进行配置文件的调整,此处注意"subPackages" 要和 "pages" 同级

{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"subPackages": [{
"root": "pages/subPages_A",
"pages": [{
"path": "card/index",
"style": {
"navigationBarTitleText": "card"
}
},
{
"path": "device/index",
"style": {
"navigationBarTitleText": "device"
}
}
]
}, {
"root": "pages/subPages_B",
"pages": [{
"path": "order/index",
"style": {
"navigationBarTitleText": "order"
}
},
{
"path": "product/index",
"style": {
"navigationBarTitleText": "product"
}
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

这里的意思是将主包拆成subPages_A和subPages_B两个子包,对比下之前的配置


6. 启动测试

启动后查看微信开发者工具,查看【详情】可看到主包大小降为326.0kb,并且下方还有subPages_A和subPages_B两个子包

比较之前包大小,分包成功!


7. 特别注意

🎯 如果设计代码中路径问题,需要调成最新包结构路径。例如

拆包前跳转到对应设备页面

uni.navigateTo({
url:'/pages/device/index'
})

拆包后跳转到对应设备页面

uni.navigateTo({
url:'/pages/subPages_A/device/index'
})

切记如果拆包后所有路径问题需要统一修改,否则则会报错!!!


总结

本文通过实际demo进行uniapp小程序拆包,通过分析项目主包大小,查看官方文档,按照功能划分进行子包拆分,如果还有博友存在疑问或者不理解可以在上方与本狗联系,或者查看本狗发布在上方的代码,希望可以帮到大家。



作者:JavaDog程序狗
来源:juejin.cn/post/7270445774324367394

收起阅读 »

产品经理:优惠金额只入不舍,支付金额只舍不入...

web
前言 当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。 优惠券分为:折扣券(n折)、抵扣券(减x元) 需求 优惠金额、支付金额都需要保留两位小数。 优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些 支付金...
继续阅读 »

前言


当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。

优惠券分为:折扣券(n折)、抵扣券(减x元)


需求


优惠金额、支付金额都需要保留两位小数。

优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些

支付金额只舍不入,比如18.888元,显示为:18.88元。

从产品角度来讲,这个设计相当人性化。


实现


/**
* 金额计算
* @param {number} a sku原始价格
* @param {number} b 优惠券金额/折扣
* @param {string} mathFunc 四舍五入:round/ceil/floor
* @param {string} type 计算方式默认减法
* @param {digits} type 精度,默认两位小数
* */

export function numbleCalc(a, b,mathFunc='round', type = '-',digits=2) {

var getDecimalLen = num =>{
return num.toString().split('.')[1] ? num.toString().split('.')[1].length : 0;
}
//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}


var c;
//获取2个数字中,最长的小数位数
var aLen = getDecimalLen(a);
var bLen = getDecimalLen(b);
var decimalLen = aLen>bLen?aLen:bLen;
var mul = decimalLen>0?(10 ** decimalLen):1;

//转换成整数
var aInteger = floatToInt(a,aLen,decimalLen)
var bInteger = floatToInt(b,bLen,decimalLen)


if(type=='-'){
c = (aInteger - bInteger)/mul;
}else if(type=='*'){
c = aInteger * bInteger/mul/mul;
}

c = digits==0?c : Math.round(c * (10 ** digits));

if(mathFunc=='floor'){
c= Math.floor(c);
}else if(mathFunc=='ceil'){
c= Math.ceil(c);
}else {
c= Math.round(c);
}
return digits==0?c : c/(10**digits);
}



整体思路:获取两个数字之间最大的小数位,先取整再计算。
不直接进行计算,是因为存在0.1+0.2!=0.3的情况,具体原因可以看下文章下方的参考链接,写的很详细。




  • Math.ceil()  总是向上舍入,并返回大于等于给定数字的最小整数。

  • Math.floor()  函数总是返回小于等于一个给定数字的最大整数。

  • Math.round() 四舍五入


【重点】小数位取整:我之前的写法原来是错误的


image.png
我一直以来也是这种形式,预想的是直接乘100变成整数,但是出现了以下情况


19.9 * 100 = 1989.9999999999998
5.02 * 100 = 501.99999999999994

可以看到,出现了意料之外的结果!!

最后采用的方案是:将小数转成字符串,再将小数点替换成空格


//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}

总结


省流:将小数点替换成空格,变成整数,再进行相应计算。

封装的这个函数,只考虑了当前业务场景,未兼容一些边界值情况。



  • 大金额计算问题

  • 计算方式:加法、除法未做处理


参考


# 前端金额运算精度丢失问题及解决方案


作者:前端大明
来源:juejin.cn/post/7341210909069770792
收起阅读 »

Alpha系统联结大数据、GPT两大功能,助力律所管理降本增效

如何通过AI工具实现法律服务的提质增效,是每一位法律人都积极关注和学习的课题。但从AI技术火爆一下,法律人一直缺乏系统、实用的学习资料,来掌握在法律场景下AI的使用技巧。今年5月,iCourt携手贵阳律协大数据与人工智能专业委员会,联合举办了《人工智能助力律师...
继续阅读 »

如何通过AI工具实现法律服务的提质增效,是每一位法律人都积极关注和学习的课题。但从AI技术火爆一下,法律人一直缺乏系统、实用的学习资料,来掌握在法律场景下AI的使用技巧。

alphagpt

今年5月,iCourt携手贵阳律协大数据与人工智能专业委员会,联合举办了《人工智能助力律师行业高质量发展巡回讲座》,超过100家律所的律师参与活动。

讲座上, iCourt AIGC 研究员、AlphaGPT产品研发负责人兰洋,为贵州律协的律师们讲解了AI技术在法律领域的应用原理、AI技术赋能法律服务发展九大典型场景和业务实操、AI赋能法律服务的发展趋势,并强调了AI时代下,律师需要掌握的关键技能,以期帮助律师们更好地抓住技术变革带来的发展机遇。

iCourt很早就开始了法律发数据与法律AI工具的研发应用,旗下智能法律操作系统Alpha、法律人专属AI助手AlphaGPT,在法律科技日益繁荣的市场上始终走在前列。

alpha

Alpha系统的大数据功能整合了超过1.6亿的案例和超过410万的法规,通过与司法观点库、类案同判库、实务文章库等多数据库协同穿透检索,为律师办案提供精确实用的法律信息。大数据功能不仅支持最高26维度的检索,还支持标签选取、模糊检索、AI检索等多种检索方式,解决律师在任意工作场景下的检索需求。 

alphagpt

值得一提的是,Alpha系统还是仅有的一个集法律大数据、人工智能工具、数智化办公工具、律所管理与团队协同工具于一体的法律智能操作系统。Alpha系统不仅可以帮助律师实现工作效率、工作效果的双重提升,还可以助力律所推动一体化改革,真正地实现降本增效。Alpha系统中涵盖的利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理等功能模块,涵盖律所管理与发展的方方面面,是律所发展、转型、升级的的必备工具。

而法律人专属AI助手AlphaGPT,可以帮助律师在案情分析、合同审查、文书写作、法律咨询、文件阅读、法律检索六大场景实现提质增效。例如,依托Alpha系统的法律大数据,AlphaGPT可以在短时间内实现案情智能分析,像律师一样思考,输出客观的法律服务意见书,还可以分钟级时间内实现合同风险与主体风险的一建审查。此外,AlphaGPT可以在短时间内实现文本的主题概括和核心提炼,还可以提供文本校对、润色、翻译等处理操作,大大节省了时间成本。

AI时代下,法律人应当抓住科技发展带来的机遇风口,拥抱AI等新兴技术,积极应用到法律服务领域当中,以实现自身的提质增效和推动行业的快速发展。

AlphaGPT官网入口:https://icourt.cc/product/alphaGPT

收起阅读 »

律所管理OA系统推荐,Alpha法律智能操作系统荣登榜首

随着“一体化”日渐成为律所改革的重要抓手,一个好的线上一体化管理工具(律所管理OA系统)就成为了几乎所有律所改革的必需品。Alpha法律智能操作系统集律所利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理于一体,涵盖律所管理的方方面面,是律所管理O...
继续阅读 »

随着“一体化”日渐成为律所改革的重要抓手,一个好的线上一体化管理工具(律所管理OA系统)就成为了几乎所有律所改革的必需品。Alpha法律智能操作系统集律所利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理于一体,涵盖律所管理的方方面面,是律所管理OA系统中的代表性产品。目前,Alpha系统已帮助到1.5万家律所提高办案能力与质效。

经过调研与精确设计,Alpha系统以科学的工作流程为基准,以解决律所管理痛点问题为目标,进一步完善功能设置。其中,案件管理、知识管理与行政事务管理广受好评,这是律所一体化建设中不可缺少的三大环节。

alpha

据调研,律所管理的痛点之一是办案过程难管理,从而导致人员调配、案件跟进等徒增风险。对此,Alpha系统的办案件进程管理功能将律师办案过程线上化,通过项目模板将一个案件的立项、签约、办案、开庭、结案等全流程进行统追踪记录,极大地帮助管理人更好地对案件进行监察跟进。Alpha系统的律所OA打通了企业微信、钉钉、飞书等办公软件,可以自动创建工作群,助力开展团队协作,实时提醒律师待办任务,降低工作出错率。

此外,律所一体化的过程中,知识管理能否做好直接关系到律所能否长期高效地运行,决定着管理模式的成败。好的知识管理方案可以促进律所内部的优秀经验高效复制、知识成果快速共享,进而整体提升律师的业务能力。Alpha系统为律所打造了电子化、协同式的知识管理功能,提供超1.5亿案例数据,提供全面、精细的数据检索库;同时,基于多项数据库的组合应用,系统还支持5秒内自动导出客户尽调报告与法律风险分析报告,帮助律师快速了解企业客户基本信息与可能存在的法律风险,高效置顶办案策略。

alpha

针对行政事务管理,Alpha系统集成了律所OA、财务统计分析、绩效管理、审批管理等方面,促进行政统一管理,有力帮助律所降本增效。Alpha系统的自定义审批引擎支持内务外勤分条件高效审批,同时提供全自动利冲检索,做好案涉合同、发票、收款等信息的记录与审批,保障项目信息高效同步流转,实现业务数据和财务数据的统一管理,真正做到了省时省力。

Alpha是目前唯一一个将大数据,律所OA,人工智能相结合的律所管理软件,在律所一体化改革与建设上发挥巨大作用,助力律所实现整体创收提升。

Alpha系统官网入口:https://www.icourt.cc/product/alpha

收起阅读 »

如何使用Electron集成环信UIKIT

写在前面环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react--- 准备工作1.已经在环信即时...
继续阅读 »

写在前面
环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react


---
 准备工作
1.已经在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了App Key

2.了解并可以创建Electron-vite-react项目

3.了解UIkit各功能以及api调用


---

开始集成
第一步:创建一个Electron项目进度10%
Electron-vite官网有详细的教程,此处不做过多赘述,仅以当前示例项目为参考集成,更多详情指路官网

yarn create @quick-start/electron electronReact --template react

第二步:安装依赖进度15%

yarn install

第三步:启动项目进度20%

yarn run dev

到这一步我们可以得到下图


你的目录结构如下图


第四步:安装UIKit进度50%

下载easemob-chat-uikit
使用 npm 安装 easemob-chat-uikit 包

npm install easemob-chat-uikit --save

使用 yarn 安装 easemob-chat-uikit 包

yarn add easemob-chat-uikit

第五步:引入UIKit组件进度80%

1、删除App.tsx自带的内容,在App.tsx中引入UIKit组件

import {
Provider as UIKitProvider,
Chat,
ConversationList,
useClient
} from 'easemob-chat-uikit'
import 'easemob-chat-uikit/style.css'
import { useEffect } from 'react'
import './App.css'
const ChatApp = () => {
const client = useClient()
useEffect(() => {
client &&
client
.open({
user: 'userId',
pwd: 'pwd'
})
.then((res) => {
console.log('get token success', res)
})
}, [client])
return (
<div className="app_container">
<div className="conversation_container">
<ConversationList />
</div>
<div className="chat_container">
<Chat />
</div>
</div>
)
}
function App(): JSX.Element {
return (
<UIKitProvider
initConfig={{
appKey: 'your app key'
}}
>
<ChatApp />
</UIKitProvider>
)
}

export default App


2、将src/renderer/src/assets/main.css中的css样式全部替换如下
body {
display: flex;
flex-direction: column;
font-family:
Roboto,
-apple-system,
BlinkMacSystemFont,
'Helvetica Neue',
'Segoe UI',
'Oxygen',
'Ubuntu',
'Cantarell',
'Open Sans',
sans-serif;
color: #86a5b1;
background-color: #2f3241;
}

* {
padding: 0;
margin: 0;
}

ul {
list-style: none;
}

code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
background-color: #26282e;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 85%;
}

a {
color: #9feaf9;
font-weight: 600;
cursor: pointer;
text-decoration: none;
outline: none;
}

a:hover {
border-bottom: 1px solid;
}

.container {
flex: 1;
display: flex;
flex-direction: column;
max-width: 840px;
margin: 0 auto;
padding: 15px 30px 0 30px;
}

.versions {
margin: 0 auto;
float: none;
clear: both;
overflow: hidden;
font-family: 'Menlo', 'Lucida Console', monospace;
color: #c2f5ff;
line-height: 1;
transition: all 0.3s;
}

.versions li {
display: block;
float: left;
border-right: 1px solid rgba(194, 245, 255, 0.4);
padding: 0 20px;
font-size: 13px;
opacity: 0.8;
}

.versions li:last-child {
border: none;
}

.hero-logo {
margin-top: -0.4rem;
transition: all 0.3s;
}

@media (max-width: 840px) {
.versions {
display: none;
}

.hero-logo {
margin-top: -1.5rem;
}
}

.hero-text {
font-weight: 400;
color: #c2f5ff;
text-align: center;
margin-top: -0.5rem;
margin-bottom: 10px;
}

@media (max-width: 660px) {
.hero-logo {
display: none;
}

.hero-text {
margin-top: 20px;
}
}

.hero-tagline {
text-align: center;
margin-bottom: 14px;
}

.links {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
font-size: 18px;
font-weight: 500;
}

.links a {
font-weight: 500;
}

.links .link-item {
padding: 0 4px;
}

.features {
display: flex;
flex-wrap: wrap;
margin: -6px;
}

.features .feature-item {
width: 33.33%;
box-sizing: border-box;
padding: 6px;
}

.features article {
background-color: rgba(194, 245, 255, 0.1);
border-radius: 8px;
box-sizing: border-box;
padding: 12px;
height: 100%;
}

.features span {
color: #d4e8ef;
word-break: break-all;
}

.features .title {
font-size: 17px;
font-weight: 500;
color: #c2f5ff;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.features .detail {
font-size: 14px;
font-weight: 500;
line-height: 22px;
margin-top: 6px;
}

@media (max-width: 660px) {
.features .feature-item {
width: 50%;
}
}

@media (max-width: 480px) {
.links {
flex-direction: column;
line-height: 32px;
}

.links .link-dot {
display: none;
}

.features .feature-item {
width: 100%;
}
}


3、在src/renderer/src目录下添加App.css
.app_container {
width: calc(100%);
height: 100vh;
display: flex;
}
.conversation_container {
width: 30%;
}
.chat_container {
width: 70%;
}


到这一步你可以得到如下图



第六步:解决问题`进度99%`

在第五步执行完毕之后发现调试器有如下图报错


经查阅资料,发现是Electron内容安全策略在搞鬼,并提供了解决方案


接下来我们就需要在src/renderer/index.html中更改meta标签
同样out/renderer/index.html也需要更改meta标签

<meta
http-equiv="Content-Security-Policy"
content="font-src 'self' data:; img-src 'self' data:; default-src 'self'; connect-src * ws://* wss://*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
/>


接下来保存代码并运行你将得到下图


第七步:发送消息进度100%
点击好友并发送一条消息,如下图

恭喜你集成完毕~
总结:
通过以上步骤,你已经成功在Electron中集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧

收起阅读 »

Go 再次讨论 catch error 模型,官方回应现状

大家好,我是煎鱼。 最近社区的同学和 Go 官方又因为错误处理的提案屡屡被否,发生了一些小的摩擦。也非常难得的看到核心团队成员首次表达了目前的态度和情况。 基于此,我们今天进行该内容分享。紧跟 Go 官方最新进展。 快速背景 Go 的错误处理机制,主要是依赖于...
继续阅读 »

大家好,我是煎鱼。


最近社区的同学和 Go 官方又因为错误处理的提案屡屡被否,发生了一些小的摩擦。也非常难得的看到核心团队成员首次表达了目前的态度和情况。


基于此,我们今天进行该内容分享。紧跟 Go 官方最新进展。


快速背景


Go 的错误处理机制,主要是依赖于 if err != nil 的方式。因此在对函数做一定的封装后。


代码会呈现出以下样子:



jy1, err := Foo1()
if err != nil {
return err
}
jy2, err := Foo2()
if err != nil {
return err
}
err := Foo3()
if err != nil {
return err
}
...

有部分开发者会认为这比较的丑陋、混乱且难以阅读。因此 Go 错误处理的优化,也是社区里一直反复提及和提出的领域。饱受各类争议。


新提案:追求类似 try-catch


最近一位国内的同学 @xiaokentrl 提了个类似 try catch error 的新提案,试图用魔法打败魔法。



原作者给出的提案内容是:


1、新增环境变量做开关:


ERROR_SINGLE = TRUE   //error_single = true

2、使用特定标识符来做 try-catch:


Demo1 单行错误处理:


//Single-line error handling
file, err := os.Create("abc.txt") @ return nil , err
defer file.Close()

Demo2 多行错误处理:


func main() {
//Multiline error handling
:@

file, err:= os.Open("abc.txt")
defer file.Close()

buf := make([]byte, 1024)
_, err2 := file.Read(buf)

@ err | err2 return ...
}

主要的变化内容是:利用标签 @ 添加一个类似 try-catch 的代码区块,并添加运算符和相关错误处理的联动规则。


这个提案本身,其实就是以往讲到的 goto error 和 check/with 这种类似 try-catch 的模式。


当然非常快的就遭到了 Go 核心团队的反对:



@Ian Lance Taylor 表示:由于很难处理声明和应用,如果一个标签的作用域中还有其他变量,就不能使用 goto。


新的争端:官方你行你上


社区中有同学看到这一次次被否的错误处理和关联提案们,深感无奈和无语。他发出了以下的质疑:


“为什么不让 Ian Lance Taylor 和/或 Go 核心团队的其他成员提出改进的错误处理框架的初始原型,然后让 Go 社区参与进来,为其最终形式做出贡献呢?Go 中的泛型正是这样发展到现在的。


如果我们等待 Go 社区提出最初的原型,我认为我们将永远不会有改进的 Go 错误处理框架,至少在未来几年内不会。”


但其实很可惜,因为人家真干过。


Go 核心团队是有主动提出过错误处理的优化提案的,提案名为《Proposal: A built-in Go error check function, try》,快速讲一下。


以前的代码:


f, err := os.Open(filename)
if err != nil {
return …, err // zero values for other results, if any
}

应用该提案后的新代码:


f := try(os.Open(filename))

try 只能在本身返回错误结果的函数中使用,且该结果必须是外层函数的最后一个结果参数。


不过很遗憾,该官方提案,喜提有史以来被否决最多的提案 TOP1:



最终该提案也由于形形色色的意见,最终凉了。感觉也给 Go 核心团队泼了一盆凉水,因为这是继 check/handle 后的 try,到目前也没有新的官方提案了。


Go 官方回应


本次提及的新提案下,大家的交流愈演愈烈,有种认为就是 Go 核心团队故意不让错误处理得到更好的改善。


此时 Go 核心团队的元老之一 @Ian Lance Taylor 站出来发声,诠释了目前 Go 团队对此的态度。这也是首次。


具体内容如下:



“我们愿意考虑一个有良好社区支持的好的错误处理提案。


不幸的是,我很遗憾地说,基本上所有新的错误处理提案都不好,也没有社区支持。例如,这个提案有 10 个反对票,没有赞成票。我当然会鼓励人们在广泛使用这门语言之前,避免提交错误处理提案。


我还鼓励人们审查早期的提案。它们在这里:github.com/golang/go/i… 。目前已有 183 个并在不断增加。


我自己阅读了每一个。重要的是,请记住,对已被否决提案的微调的新提案也几乎肯定也会被否决。


并且请记住,我们只会接受一个与现有语言契合良好的提案。例如:这个提案中使用了一个神奇的 @ 符号,这完全不像现有语言中的任何其他东西。


Go 团队可能会在适当的时候提出一个新的错误处理提案。然而,正如其他人所说,我们最好的想法被社区认为是不可接受的。而且有大量的 Go 程序员对现状表示满意。”


总结


目前 Go 错误处理的情况和困境是比较明确的,很多社区同学会基于以往已经被否决的旧提案上进行不断的微改,再不断提交。


现阶段都是被全面否定的,因为即使做了微调,也无法改变提案本身的核心思想。


而 Go 官方自己提出的 check/handle 和 try 提案,在社区中也被广大的网友否决了。还获得了史上最多人否决的提案的位置。


现阶段来看,未来 1-3 年内在错误处理的优化上仍然会继续僵持。



作者:煎鱼eddycjy
来源:juejin.cn/post/7381741857708752905
收起阅读 »

写了一个字典hook,瞬间让组员开发效率提高20%!!!

web
1、引言 在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react ...
继续阅读 »

1、引言


在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react 开发一个小程序,所以就写一个字典 hook 方便大家开发。


2、实现过程


首先,字典接口返回的数据类型如下图所示:
image.png


其次,在没有实现字典 hook 之前,是这样使用 选择器 组件的:


  const [unitOptions, setUnitOptions] = useState([])

useEffect(() => {
dictAppGetOptionsList(['DEV_TYPE']).then((res: any) => {
let _data = res.rows.map(item => {
return {
label: item.fullName,
value: item.id
}
})
setUnitOptions(_data)
})
}, [])

const popup = (
<PickSelect
defaultValue=""
open={unitOpen}
options={unitOptions}
onCancel={() =>
setUnitOpen(false)}
onClose={() => setUnitOpen(false)}
/>

)

每次都需要在页面组件中请求到字典数据提供给 PickSelect 组件的 options 属性,如果有多个 PickSelect 组件,那就需要请求多次接口,非常麻烦!!!!!


既然字典接口返回的数据格式是一样的,那能不能写一个 hook 接收不同属性,返回不同字典数据呢,而且还能 缓存 请求过的字典数据?


当然是可以的!!!


预想一下如何使用这个字典 hook?


const { list } = useDictionary('DEV_TYPE')

const { label } = useDictionary('DEV_TYPE', 1)

const { label } = useDictionary('DEV_TYPE', 1, '、')

从上面代码中可以看到,第一个参数接收字典名称,第二个参数接收字典对应的值,第三个参数接收分隔符,而且后两个参数是可选的,因此根据上面的用法来写我们的字典 hook 代码。


interface dictOptionsProps {
label: string | number;
value: string | number | boolean | object;
disabled?: boolean;
}

interface DictResponse {
value: string;
list: dictOptionsProps[];
getDictValue: (value: string) => string
}

let timer = null;
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {}; // 字典缓存

// 因为接收不同参数,很适合用函数重载
function useDictionary(type: string): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]); // 字典数组
const [dictValue, setDictValue] = useState(""); // 字典对应值

const init = () => {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

types.push(type);

// 当多次调用hook时,获取所有参数,合成数组,再去请求,这样可以避免多次调用接口。
timer && clearTimeout(timer);
timer = setTimeout(() => {
dictAppGetOptionsList(types.slice()).then((res) => {
for (const key in dictRes.data) {
const dictList = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
dict[type] = dictList
setOptions(dictList) // 注意这里会有bug,后面有说明的
}
});
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
};

// 获取字典对应值的中文名称
const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
)

useEffect(() => init(), [])
useEffect(() => setDictValue(getLabel(value)), [options, value])

return { value: dictValue, list: options, getDictValue: getLabel };
}

初步的字典hook已经开发完成,在 Input 组件中添加 dict 属性,去试试看效果如何。


export interface IProps extends taroInputProps {
value?: any;
dict?: string; // 字典名称
}

const CnInput = ({
dict,
value,
...props
}: IProps
) => {
const { value: _value } = dict ? useDictionary(dict, value) : { value };

return <Input value={_value} {...props} />
}

添加完成,然后去调用 Input 组件


<CnInput
readonly
dict="DEV_ACCES_TYPE"
value={formData?.accesType}
/>

<CnInput
readonly
dict="DEV_SOURCE"
value={formData?.devSource}
/>


没想到,翻车了


会发现,在一个页面组件中,多次调用 Input 组件,只有最后一个 Input 组件才会回显数据


image.png


这个bug是怎么出现的呢?原来是 setTimeout 搞的鬼,在 useDictionary hook 中,当多次调用 useDictionary hook 的时候,为了能拿到全部的 type 值,请求一次接口拿到所有字典的数据,就把字典接口放在 setTimeout 里,弄成异步的逻辑。但是每次调用都会清除上一次的 setTimeout,只保存了最后一次调用 useDictionary 的 setTimeout ,所以就会出现上面的bug了。


既然知道问题所在,那就知道怎么去解决了。


解决方案: 因为只有调用 setOptions 才会引起页面刷新,为了不让 setTimeout 清除掉 setOptions,就把 setOptions 添加到一个更新队列中,等字典接口数据回来再去执行更新队列就可以了。



let timer = null;
const queue = []; // 更新队列
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {};

function useDictionary2(type: string): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]);
const [dictValue, setDictValue] = useState("");

const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
);

const init = () => {
if (typeof type === "string") {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

const item = {
key: type,
exeFunc: () => {
if (typeof type === "string") {
setOptions(dict[type]);
} else {
setOptions(type);
}
},
};
queue.push(item); // 把 setOptions 推到 更新队列(queue)中

types.push(type);

timer && clearTimeout(timer);
timer = setTimeout(async () => {
const params = types.slice();

types.length = 0;

try {
let dictRes = await dictAppGetOptionsList(params);
for (const key in dictRes.data) {
dict[key] = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
}

queue.forEach((item) => item.exeFunc()); // 接口回来了再执行更新队列
queue.length = 0; // 清空更新队列
} catch (error) {
queue.length = 0;
}
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
}
};

useEffect(() => init(), []);

useEffect(() => setDictValue(getLabel(value)), [options, value]);

return { value: dictValue, list: options, getDictValue: getLabel };
}

export default useDictionary;

修复完成,再去试试看~


image.png


不错不错,已经修复,嘿嘿~


这样就可以愉快的使用 字典 hook 啦,去改造一下 PickSelect 组件


export interface IProps extends PickerProps {
open: boolean;
dict?: string;
options?: dictOptionsProps[];
onClose: () => void;
}
const Base = ({
dict,
open = false,
options = [],
onClose = () => { },
...props
}: Partial<IProps>
) => {
// 如果不传 dict ,就拿 options
const { list: _options } = dict ? useDictionary(dict) : { list: options };

return <Picker.Column>
{_options.map((item) => {
return (
<Picker.Option
value={item.value}
key={item.value as string | number}
>

{item.label}
</Picker.Option>
);
})}
</Picker.Column>


在页面组件调用 PickSelect 组件


image.png


效果:


image.png


这样就只需要传入 dict 值,就可以轻轻松松获取到字典数据啦。不用再手动去调用字典接口啦,省下来的时间又可以愉快的摸鱼咯,哈哈哈


最近也在写 vue3 的项目,用 vue3 也实现一个吧。


// 定时器
let timer = 0
const timeout = 10
// 字典类型缓存
const types: string[] = []
// 响应式的字典对象
const dict: Record<string, Ref<CnPage.OptionProps[]>> = {}

// 请求字典选项
function useDictionary(type: string): Ref<CnPage.OptionProps[]>
// 解析字典选项,可以传入已有字典解析
function useDictionary(
type: string | CnPage.OptionProps[],
value: number | string | Array<number | string>,
separator?: string
): ComputedRef<string>
function useDictionary(
type: string | CnPage.OptionProps[],
value?: number | string | Array<number | string>,
separator = ','
): Ref<CnPage.OptionProps[]> | ComputedRef<string> {
// 请求接口,获取字典
if (typeof type === 'string') {
if (!dict[type]) {
dict[type] = ref<CnPage.OptionProps[]>([])

if (type === 'UNIT_LIST') {
// 单位列表调单独接口获取
getUnitListDict()
} else if (type === 'UNIT_TYPE') {
// 单位类型调单独接口获取
getUnitTypeDict()
} else {
types.push(type)
}
}

// 等一下,人齐了才发车
timer && clearTimeout(timer)
timer = setTimeout(() => {
if (types.length === 0) return
const newTypes = types.slice()
types.length = 0
getDictionary(newTypes).then((res) => {
for (const key in res.data) {
dict[key].value = res.data[key].map((v) => ({
label: v.description,
value: v.subtype
}))
}
})
}, timeout)
}

const options = typeof type === 'string' ? dict[type] : ref(type)
const label = computed(() => {
if (value === undefined || value === null) return ''
const values = Array.isArray(value) ? value : [value]
const items = values.map(
(value) => {
if (typeof value === 'number') value = value.toString()
return options.value.find((v) => v.value === value) || { label: value }
}
)
return items.map((v) => v.label).join(separator)
})

return value === undefined ? options : label
}

export default useDictionary

感觉 vue3 更简单啊!


到此结束!如果有错误,欢迎大佬指正~


作者:用户2885248830266
来源:juejin.cn/post/7377559533785022527
收起阅读 »

运维打工人,周末兼职送外卖的一天

运维打工人,周末兼职送外卖的一天 在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。 早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。...
继续阅读 »

运维打工人,周末兼职送外卖的一天


在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。


早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。


收拾好后,戴上头盔,骑上踏板车,开始了自己的第一次外卖配送之旅。


刚开始,我的心情既紧张又兴奋。手机里的订单提示声是今日的任务号角。第一份订单来自一公里外的一家外卖便利店。我快速地在地图上规划路线,开启高德导航,发动踏板车,朝着目的地出发。


123.jpg


由于便利店在园区里面,转了两圈没找到,这是就慌张了,这找不到店咋办了,没办法赶紧问下旁边的老手骑手,也就顺利找到了,便利店,进门问老板,美团104号好了嘛?老板手一指,在架子上自己看。核对没问题,点击已达到店,然后在点击已取货。


然后在导航去收获目的地,找到C栋,找到107门牌号,紧接敲门,说您好,美团外卖到了,并顺利的送达,然后点击已送达,第一单顺利完成,4.8元顺利到手。


其中的小插曲,送给一个顾客时,手机导航提示目的地,结果一看,周围都拆了。没办法给顾客打电话,加微信确认位置具体在哪里,送达时,还差三分钟,这单就要超时了。


1.jpg


配送过程中,我遇到了第一个难题:找不到店家在哪里,我的内心不禁生出些许焦虑。但很快,我调整心态,不懂不知道的地方,需要多多问人。


紧接着,第二份、第三份订单接踵而至。每一次出发和到达,每一条街道和巷弄,我开始逐渐熟悉。


7.jpg


6.jpg


日落时分,我结束了一天的工作。虽然身体有些疲惫,但内心充满了前所未有的充实感。这份工作让我体验到了不一样的人生角色,感受到了城市节奏背后的种种辛劳与甘甜


周末的兼职跑美团外卖,对我来说不仅是一份简单的工作,更是一段特别的人生经历。它教会了我坚持与责任,让我在忙碌中找到了属于自己的节奏,在逆风中学会了更加珍惜每一次到达。


最后实际周六跑了4个小时,周天跑了7个小时,一共跑了71公里,合计收获了137.80,已提现到账。


5.jpg


2.png


作者:平凡的运维之路
来源:juejin.cn/post/7341669201010425893
收起阅读 »

为了解决小程序tabbar闪烁的问题,我将小程序重构成了 SPA

web
(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))  前言 几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip...
继续阅读 »

1.jpg


(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))


 前言


几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip用户小程序下面的tabbar有五个。  

因为涉及自定义tabbar的问题,所以官方自带的tabbar肯定就不能用了,我们需要自定义tabbar。官方也提供了自定义tabbar的功能。


官网自定义tabbar


官网地址:基础能力 / 自定义 tabBar (qq.com)


{
"tabBar": {
"custom": true,
"list": []
}
}

就是需要在 app.json 中的 tabBar 项指定 custom 字段,需要注意的是 list 字段也需要存在。


然后,在代码根目录下添加入口文件:


custom-tab-bar/index.js
custom-tab-bar/index.json
custom-tab-bar/index.wxml
custom-tab-bar/index.wxss

具体代码,大家可以参考官网案例。


需要注意的是每个tabbar页面 / 组件都需要在onshow / show 函数中执行以下函数,否则就会出现tabbar按钮切换两次,才会变成选中色的问题。


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

接下来就是我的思路


2.png


我在 custom-tab-bar/index.js 中定义了一个函数,这个函数去判断当前登录人是否为vip,如果是就替换掉tabbar 的数据。


那么之前每个页面的代码就要写成这样


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().change_tabbar_list()
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

ok,我们来看一下效果。注意看视频下方的tabbar,每个页面,第一次点击的时候,有明显的闪烁bug。(大家也可以参考一下市面上的小程序,小部分的小程序有这个闪烁问题,大部分的小程序没有这个闪烁的问题(如:携程小程序))



bug产生原因


那么我们就要去思考了,为什么人家的小程序没有这个bug呢?


想这个问题前,要先去想这个bug是怎么产生的,我猜测是每个tabbar页面都有个初始化的过程,第一次渲染页面的时候要去重新渲染tabbar,每个页面的tabbar都是从0开始渲染,然后会缓存到每个页面上,所以第二次点击就没有这个bug了。


解决tabbar闪烁问题


为了解决这个问题,我想到了SPA ,也就是只留一个页面,其他的tabbar页面都弄成组件。


效果展示



已经解决,tabbar闪烁的问题。


代码思路,通过wx:if 控制组件的显示隐藏。


3.png


4.png


源码地址:gitlab.com/wechat-mini…

https克隆地址:gitlab.com/wechat-mini…


写在最后


1、我也是在网上见过别人的一些评论,说如果将小程序重构成这种单页面,会有卡顿问题,我目前没有发现这个问题,可能是我做的小程序功能比较少。


2、至于生命周期,将页面切换成组件后,页面的那些生命周期也肯定都不能使用了,只能用组件的生命周期,我之前开发使用组件的生命周期实现业务逻辑也没什么问题。 触底加载这些也只能换成组件去实现了。


3、小程序最上面的标题,也可以使用以下代码来实现。就是在每个组件初始化的时候要去执行下列代码。


            wx.setNavigationBarTitle({
title: '',
});

作者:楚留香Ex
来源:juejin.cn/post/7317281367111827475
收起阅读 »

程序员真的很死板很无聊吗?

你的生活快乐吗?如果把工作和生活10等分,你会怎么分配你的时间? 故事背景 前几天随手分享了一个关于我每天早上吃的补充剂的照片,没想到有很多小伙伴留言反馈,着实挺出乎我意料 不禁让我想起一句自黑的老梗:想结婚找程序员,钱多话少死的早,虽然是调侃,但是也反映...
继续阅读 »

你的生活快乐吗?如果把工作和生活10等分,你会怎么分配你的时间?



故事背景


前几天随手分享了一个关于我每天早上吃的补充剂的照片,没想到有很多小伙伴留言反馈,着实挺出乎我意料


截屏2024-05-28 22.00.19.png


不禁让我想起一句自黑的老梗:想结婚找程序员,钱多话少死的早,虽然是调侃,但是也反映了大众对程序员团体的刻板映象确实很糟糕。


加上我身边很多同行业的同事这个毛病那个毛病的,30出头就脂肪肝...这也让我产生了一些思考:



  • 我们这个行业的从业者,在到了25岁、30岁、35岁、40岁的生活状况,健康状况分别是怎么样的

  • 会运动吗?吃的健康吗?身体还好吗?

  • 是不是真的像网上说的一样,程序员很无聊,很宅,死板?


所以我真的很好奇,大多数开发的生活状态是怎么样的,今天我分享一下我作为一个开发的日常,想看看我的生活是不是具有普遍性。(多图预警)


自我介绍


先来自我介绍一下,我叫dev,今年32岁(中年男人了),是个从业快7年的前端,主攻 React 技术栈,目前在一家ToG的公司做架构师。现在一边写基建一边在带人,基本脱离了业务代码的编写(偶尔)。


工作之余会写写文章,发发视频,阐述一些我认知的价值观和能提升生活幸福度的方法。下面是我平时的生活状态,兴趣比价广泛,基本上喜欢什么干什么(人生苦短及时行乐):



  1. 喜欢旅游,尤其那些山山水水,它会让我的内心平静
    露营
    潜水

  2. 喜欢画画,从初中开始,课本就是我的绘画本
    画画

  3. 会弹吉他,大学玩过乐队,也受邀在很多地方表演过
    吉他

  4. 目前喜欢上了popping,因为它很酷【在学】

  5. 因为很爱健康方面的知识,2024年准备考健康管理师


养生日常


7点起床,7.30做饭


早上7点起床,洗漱完之后,喝一杯温水,7:30 准备今天的伙食。(因为公司提供早餐,周末会自己做)


午餐


早餐



我不太喜欢吃外卖,因为我感觉很多外卖的食材不新鲜,而且大多为了可以放的时间久一点,盐分含量过高了!这是我每天早上早起做饭的动力之一。



9点到公司,开始工作吃早饭


早餐一般我会搭配文章开头的补剂一起吃。上班的时间我就快速跳过了,应该大部分时间和大家一样:


9点30-12点:工作时间
12点-1点:中饭时间
1点-1点30:午休半小时
1点30-5点30:工作时间

5点30下班


一般我会在公司待到6点半,因为我习惯6点准时吃饭,吃完之后再回来打个卡下班。常规操作。


下班之后我的安排一般是:周一8点撸铁,周三周五都有 popping 课,周二周四一般就在家写点东西,或者拍点东西,看看书啥的学习一下。


周末


周六外出


周五一到,令人兴奋的周末就要来了。一般周四周五就会大概想好周六的行程。有时候三五好友,有时候一个人,找个偏僻的乡村,搭个帐篷,架个躺椅,平静的过一天~(手机关机,啥都别来烦我)


截屏2024-05-28 23.17.18.png


周日创作


一般很少会连续两天出去玩儿,所以周日会安排自己学习一些新的东西,比如最近就在备考健康管理师,周日基本上午都在复习,周日下午就灵活安排,可能刷刷剧,撸撸猫,兴致来了晚上也会去上课,因为周日晚上也有一节课。
灵感来了也会写写文章,不管技术型的或者是生活向的,想到啥写啥,我很喜欢这种自由创作的感觉。让我有对生活的掌控感。


总结


以上就是我的日常生活,最近很喜欢一个成语:向死而生。我认为人到了一定年纪之后就会从学生变为布道者。写这篇文章的目的也很简单,希望能通过自己的生活态度告诉外界,我们也懂生活,我们也有自己对于生活的态度。


当我们光着屁股来到这个世界上,死的时候除了我们这一辈子的经历也是光着屁股走。所以我希望死的那一天,回顾我的一生,能很有底气的告诉自己:这一生无憾了!


这些年不断的犯错,不断的纠正。从以前天上地下唯我独尊的摇滚少年,到现在成熟低调的中年男人。总结下来,我发现想要的生活其实很简单:



  1. 有爱我和我爱的人

  2. 有我喜欢做的事情

  3. 身体健康(能比同龄人看起来年轻10岁我就心满意足了👀)


希望这篇文章能帮到你重新认识生活,评论区也可以留下你对自己生活的态度,和你认为对自己生活的掌控感有多少?


作者:dev
来源:juejin.cn/post/7373955162127876123
收起阅读 »

“请杀死自己的学生思维”

Hello,大家好,我是 Sunday。 昨天在B站上看到了一个视频叫做 请“杀死”自己的学生思维 这不禁让我想起之前写过的一篇文章 我们应避免感动自己的无效学习! 在现在 “内卷” 日益严重的大行情之下,很多同学都会充满的焦虑,生怕自己一个不小心就会被淘...
继续阅读 »

Hello,大家好,我是 Sunday。


昨天在B站上看到了一个视频叫做 请“杀死”自己的学生思维


作者:黄一刀有毒


这不禁让我想起之前写过的一篇文章 我们应避免感动自己的无效学习!



在现在 “内卷” 日益严重的大行情之下,很多同学都会充满的焦虑,生怕自己一个不小心就会被淘汰掉。从而开始学习很多很多的内容,期望可以通过这种方式来 “安慰自己”,告诉我已经很努力了,我不会被淘汰。


可是很多时候,这种无目标,无结果的努力,其实是 毫无价值 的!



什么是学生思维


“什么是学生思维” 并没有一个明确的定义。我个人认为,所谓的学生思维指的是:期望通过 “按部就班” 的努力,来达到 “超过大多数人” 的结果


回顾下我们的学生生涯:每天的课程是固定的、学习的知识是固定的、什么时候考试是固定的、甚至 考试的内容是什么也是固定的。几乎所有的一切都是固定的


所以,我们只需要 “按部就班” 的走,学习固定的内容,掌握固定的知识,就可以了。


因为,所有的人学习的内容都一样。所以,如果想要超过大多数人怎么办呢?


那就只剩下一个办法了, 卷!!!


卷学习时长、卷补习班、卷做题,最重要的是 父母、老师也告诉你这样是对的,因为 TA们在工作中也是这么做的


从而让我们认为 卷是常态,想要超过大多数人,获得排名(注意是排名而不是成绩)的靠前,那么就 必须要比别人卷!


最可怕的是 在学生时代,这样做是对的! 只要你足够卷,那么你就可以获得更好的排名,把其他的人卷下去。


就像这张图片一样


但是,这样的方式当你进入到社会中,你会发现 它好像失效了!


我们曾经亲眼看到过很多特别努力的同事依然免不了失业的结局,就好像前几天我写的这篇文章一样 跟一位 40+ 岁的同学沟通之后,差点泪崩


所以,当你进入职场 “请杀死自己的学生思维”


建立全新的职场思维体系


“杀死自己的学生思维” 是一件非常难的事情。因为 过去几十年的教育和经历都告诉我们:好好学习、好好上班、多听老板的话、吃亏是福!


几十年的固化思维是很难在一朝一夕之间改变的。但是,只要你开始做了,那么就比不做好。


1:工作只是利益的结合,天下没有不散的宴席


最近这两年,公司裁员已经不是一个新鲜的话题了。


上班的时候,老板说:“你要把公司当家”。裁员的时候,老板说:“不努力的就不是我兄弟”


所谓的工作,本质上就是利益的结合。你需要一份工作赚钱,老板需要人帮他赚更多的钱,仅此而已。


当一份关系通过利益进行捆绑时,那么通常就不是那么稳定了!


当一方的利益无法得到保证时,离开就是一个必然的结果。跳槽如是,裁员亦如是。


所以,把工作当成一场“宴席”。虽然“天下没有不散的宴席”,但是当一场宴席结束后,你才会迎来新的机会。


2:努力适应变化,生活不是考试,不需要排名


这个社会中唯一不变的就是一直在变化。这是与学生时代的最大不同。


诚如前面所说,学生时代几乎所有的一切都是固定的。但是,在生活中一切都是 瞬息万变 的。


进入社会,我们才发现生活远不止 上学、考试 这些固定的轨道。工作环境、市场需求、个人生活状况,甚至全球的经济和政治局势,都在不断变化。我们面对的已不是一张张试卷,而是一个个现实的挑战和机遇。我们需要学会适应这些变化,在不确定性中寻找方向和机会。


同时,生活也不是考试,没有所谓的得分,也不存在所谓的正确答案。


跟大家说一个某同学的故事,这位同学我们叫他 小A:



小A 之前是某英语培训机构的老师。K12 被禁止后失业。


因为英语很好,同时身边有做开发的朋友,所以进到 私教训练营 里学习,想要做前端开发的岗位。


但是,因为之前没有编程经验,所以学习速度并不快,预计需要 5 个月的学习周期。


一次无意中,小A 了解到小区中很多小孩子都有英语学习的需求。就过来问我:“Sunday 老师,你觉得我能不能帮小朋友学习英语,这样也可以获取一些收入。”


我帮他出了一整套的方案,那一个月的时间,我们针对 小A 的培训需求进行了很多次的沟通。最后大家猜怎么着?


小A 成功的做起来了一个小范围的英语学习班,每个月的收入甚至远超之前工作的收入。



我们每个人都有很多的选择。就如 小A 一样 “做开发”,“做老师”,“做培训”。


以上的选择都可以是正确的,也可能都是错误的。生活没有标准答案,每个人都有自己的独特路径和目标。


3:不要妄图规划未来,先把当下的事情做好


很多同学喜欢规划未来,这个并没有太大的意义。


比如:很多同学还没有找到工作的时候,就在担心 35 岁之后,如果失业了怎么办?这无疑是一种杞人忧天的想法。


未来的事情不可预测。很多鸡汤书籍都在告诉大家需要思考未来的情况,从而安排未来的计划:3 年计划、5 年计划 甚至是 10 年计划。


乍一听,好像很有道理的样子。但是 现实却告诉我们,哪怕是 3 天之后的情况,我们都预测不了。


过分的思考未来,会让我们忘记当下。就像之前的躺平文中所说:“人总会死,那么为什么还要努力呢?(注意:这是不对的)”。这甚至算是一个 “百年计划” 了。


所以,先解决当下的问题 吧,这才是最重要的!当下都解决不了,谈什么未来?


总结


无论你现在处于生活中的哪一个阶段,是痛苦着,还是快乐着。我都希望你可以摆脱原有的学生思维,尝试适应变化,找到自己独特的路径和目标 从而获得自己想要的幸福。


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

程序员都在用哪些神器?

工作中,我们有时候往往需要合理利用工具为我们提高一定的工作效率。利用包罗万象的浏览器搜索想要的资源已经是司空见惯了。 既然浏览器已经成为每一位电脑工作者的首要工具,那必然会有老六的出现,在我们的浏览器上增加广告,限制资源访问的手段,逼迫我们不得不妥协或者充值满...
继续阅读 »

工作中,我们有时候往往需要合理利用工具为我们提高一定的工作效率。利用包罗万象的浏览器搜索想要的资源已经是司空见惯了。


既然浏览器已经成为每一位电脑工作者的首要工具,那必然会有老六的出现,在我们的浏览器上增加广告,限制资源访问的手段,逼迫我们不得不妥协或者充值满足他们。


一名电脑从业者,想要一台干净的办公电脑环境可以说是一份奢求。我也观察其他部门同事们的电脑,电脑时不时弹出小电影片段,一刀 9999 的弹窗,可以说是皮到家了,不解释一下都以为平时就干这些事。


所以,我这里分享几款程序员常用的神器,即可应付老六行为,也可以方便我们摸鱼以及日常的工作使用。


AdGuard 拦截器


我们工作时候难免搜索一些紧急的关键信息,搜索过程中却时不时弹出亮眼的广告,甚至出现更加过分的花屏广告。


一怒之下想点击关闭窗口,却发现关闭窗口异常的难点,甚至阴差阳错点进广告内容,选择加入他们,小紧张的和旁边观看的同事解释我并没有干这种事情。


图片


AdGuard 广告拦截器,一款无与伦比的广告拦截扩展,用以对抗各式广告与弹窗。可以拦截所有网站的广告。



  1. 拦截所有广告,包括:视频广告,各种媒体广告,例如视频广告,插播广告和浮动广告 ,令人讨厌的弹窗 横幅广告和文字广告。

  2. 加速页面载入,节省带宽,屏蔽广告和弹窗 。

  3. 拦截各种软件,广告软件和拨号安装程序(可选) 。


图片


一旦安装了这款神器,我是万万没想到多流氓的网站都能够乖巧的显示它本应该显示的内容,不夹杂花里胡哨。眼不见为净,气不过难不成咱还躲不起,还给自己一个优质的工作环境。


Global Speed


作为当代视频冲浪儿,我们在闲暇时刻总是难免观看一些视频内容来打发时间,比如哔哩哔哩,腾讯视频,百度网盘, 爱奇艺等。


图片


有些视频内容往往是冗长且枯燥的,以及万恶的资本早已经捕捉到群体的口味,通常会增加一些不知是否趣味的广告。这时我们往往万般无奈等待着播放完,或者跳过观看。


如果我们使用 Global Speed 工具,我们可以躲避那些没有充值会员时候给我们增加冗长的广告,使得视频播放速度提高至 16 倍进行躲闪。它几乎支持所有的视频平台。


也就意味着 100 秒的广告,我们 8 秒就过完了。


Imagus


在使用浏览器逛一些网站和商城时,需要查看一些视频和图片,我们往往会选择点击进去查看大图。且有时候查看大图时,需要选择登录或者 vip 才能查看大图的权限。


图片


Imagus 插件,很好的解决了多点一下的问题,他可以停留在页面上不去点击进去直接鼠标悬停之后可查看大图效果。


他的作用是鼠标指针悬停在链接或缩略图上时直接在当前页面的弹出视图上显示这些图片、HTML5 视频/音频和内容专辑。


图片


图片


Picture-in-Picture Extension


这款画中画,我只能说是摸鱼神器了。在工作中,往往会趁着老板不注意或者躲避监控范围偷偷地摸鱼。


图片


时不时看一些好玩的视频,或者看看电竞直播,但又不敢夸张全屏观看,得给足老板的面子。所以我们会选择画中画观看,蜷缩在屏幕的一个小筐里进行观看,又不引人注意,也不影响手头的工作。


图片


使用 Picture-in-Picture 工具,我们既可以一边工作一边画中画观看自己想看的小视频,还能随意所动位置以及设置画面大小,别提多惬意。


购物党自动比价工具


我所认识的程序员群体里,有 10 个程序员里有 7-8 个都是会过日子的。不管是淘淘自己喜爱的电子产品还是生活必需品里,他们往往追求极致的性价比。在购买自己喜爱的电子产品时,往往会搜罗多个电商平台进行比价,以及寻求降价机会。


图片


而且,在面对自己的女神或者妹妹赠送礼物时,会徘徊在既要给出大方又要价格合理的礼品,毕竟不能不失对方的面子也不能让自己陷入窘迫的险境。如今的环境里寸金都不是大风刮来的。


购物党自动比价工具,往往是一些程序员必备的工具。浏览商品页面时,自动查询 180 天历史价格、比较同款商品的全网最低价、提示促销和隐藏优惠券、一旦降价还能通过微信提醒你,海淘、二手房游戏平台也能比价。


图片


当我们搜索一款商品时,左上角会自动查找各个平台的价格,价格的走势,以及优惠券的信息。是能够极大得缩减我们淘尚品的时间。


SuperCopy 超级复制


我们得益于互联网包罗万象,总是能够通过检索得到我们所需要的信息,使得我们尽管是一名程序员,也容易陷入 CV(复制粘贴)工程师的身份。


图片


总是有老六的出现,好不容易搜索到关键信息,结果一复制,温馨的弹框告诉我们需要登录或者充值 VIP。


气的人直跺脚,我们还拿他没办法,老板微信里三番五次催出我们好了没有,但我们又徘徊在付费工作的僵局。


SuperCopy 超级复制,这款插件绝对能够治理那些老六。


一键破解禁止右键、破解禁止选择、破解禁止复制、破解禁止粘贴,启用复制,启用右键,启用选择,启用粘贴。


超级复制 SuperCopy ,在禁止复制、禁止右键、禁止选择的站点,一键复制,一键粘贴,一键选择,启用右键,启用复制,启用选择,启用粘贴。


主要功能:解除禁止复制、解除禁止右键、解除禁止选择、解除禁止粘贴。


图片


如何安装


方式一: 如果您有科学上网,可以直接进入谷歌商城,依次搜索插件名称即可安装。


谷歌商城地址:chromewebstore.google.com/


如您没有科学上网,您可在公众号后台会回复 【科学上网】 即可获取工具。如您有使用问题,可在后台加作者联系方式,可以进行指导和咨询。


方式二: 您可以进入插件小屋,搜索插件名称进行下载。


插件小屋:http://www.chajianxw.com/


图片


下载之后点击谷歌浏览器设置,进入扩展程序界面。


图片


您可以将刚刚下载的插件压缩包拖拽到扩展程序界面,即可使用。


图片


作者:程序员小榆
来源:juejin.cn/post/7346119032524357642
收起阅读 »

如何将微信小程序从WebView迁移到Skyline

web
什么是 Skyline 微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。 具体可以查阅官网介绍 将开发者工具切换成 Sykline 模式 调试基础库切到 2.30.4 或以上版本 确保右上角 > 详情 > 本地...
继续阅读 »

什么是 Skyline


微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。


具体可以查阅官网介绍


将开发者工具切换成 Sykline 模式



  1. 调试基础库切到 2.30.4 或以上版本

  2. 确保右上角 > 详情 > 本地设置里的 开启 Skyline 渲染调试、启用独立域进行调试 选项被勾选上

  3. 确保右上角 > 详情 > 本地设置里的 将 JS 编译成 ES5 选项被勾选上


使用 skylint 工具迁移



npx skylint


image.png


image.png


使用过程中可能会出现文件未找到错误,例如


image.png


原因就是使用绝对路径 <import src="/components/chooserList/index.wxml" />导入模块,而 skylint 无法找到该文件,需要修改为相对路径 <import src="../../components/chooserList/index.wxml" />导入模块


有几种提示不是很准确,可以评估下:



  1. @position-fixed 不支持 position: fixed:如果你根据不同 renderer 兼容,则会导致该提示一直存在

  2. @no-pseudo-element 不支持伪元素: 目前对已经支持的 ::before 和 ::after 也会进行提示


手动迁移


在 app.json 配置



{
"lazyCodeLoading": "requiredComponents", // 开启按需注入
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true // skyline 下节点默认为 flex 布局,可以在此切换为默认 block 布局
}
}
}


在 page.json 配置



{
"renderer": "skyline", // 声明为 skyline 渲染,对于已有的项目,建议渐进式迁移,对于新项目,直接全局打开,在 app.json 里进行配置
"componentFramework": "glass-easel", // 声明使用新版 glass-easel 组件框架
"disableScroll": true, // skyline 不支持页面全局滚动,为了使之与WebView保持兼容,在此禁止滚动
"navigationStyle": "custom" // skyline 不支持原生导航栏,为了使之与WebView保持兼容,并且自行实现自定义导航栏
}


skyline 不支持页面全局滚动,如果需要页面滚动,在需要滚动的区域使用 scroll-view 实现



<scroll-view type="list" scroll-y style="flex: 1; width: 100%; overflow: hidden;"></scroll-view>



page {
display: flex;
flex-direction: column;
height: 100vh;
}


skyline 渲染模式下 flex-direction 默认值是 column,为了使之与WebView保持兼容,需要在 flex 布局里将 flex-direction 默认值改为 row


在真机上调试 skyline 渲染模式


小程序菜单 > 开发调试 > Switch Render,会出现三个选项,说明如下:


Auto :跟随 AB 实验,即对齐小程序正式用户的表现


WebView :强制切为 WebView 渲染


Skyline :若当前页面已迁移到 Skyline,则强制切为 Skyline 渲染


image.png


常见问题


position: fixed 不支持


需要将



<view class="background"></view>



.background {

position: fixed;

}


修改为



<root-portal>

<view class="background"></view>

</root-portal>



.background {

position: absolute;

}


如果无法做到适配,则可以根据不同 renderer 兼容



<view class="position {{renderer}}"></view>



.position {
position: fixed;
}

.position.skyline {
position: absolute;
}



Page({
data: {
renderer: 'webview'
},

onLoad() {
this.setData({
renderer: this.renderer,
})
},
})


不支持 Canvas 旧版接口


Skyline 渲染模式下,旧版 Canvas 绘制图案无效(使用 wx.createCanvasContext 创建的上下文)


在真机中图片的 referer 丢失


测试结果如下:


使用 WebView 渲染 Image,请求的 header 是:



{
host: 'xxx',
connection: 'keep-alive',
accept: 'image/webp,image/avif,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5',
'user-agent': 'xxx',
'accept-language': 'zh-CN,zh-Hans;q=0.9',
referer: 'https://servicewechat.com/',
'accept-encoding': 'gzip, deflate'
}


使用 Skyline 渲染 Image,请求的 header 是:



{

'user-agent': 'Dart/2.16 (dart:io)',
'accept-encoding': 'gzip',
host: 'xxx'
}


官方 Demo


可以参考官方Demo学习使用 Skyline 的增强特性,比如 Worklet 动画、手势系统等,但在首次下载编译时,会遇到【交互动画】页面为空的问题,主要原因是该页面是由 TypeScript 写的,编译成 JavaScript 需要开启工具内置的 TypeScript编译插件,需要在project.config.json project.config.json 配置:



setting.useCompilerPlugins: ["typescript"]


参考



作者:不二先生不二
来源:juejin.cn/post/7262656196854644792
收起阅读 »

小程序实现无感登录+权限分配

web
djngo开发:小程序实现无感登录+权限分配 由来 最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作) 同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机...
继续阅读 »

djngo开发:小程序实现无感登录+权限分配


由来


最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作)


同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机号来区分普通用户和工作人员。这样在项目交付时,工作人员和普通用户一样可以直接登录无感登录小程序


1. 开发思路


微信的openid是一种唯一标识用户身份的字符串

用户登录小程序,通过手机号快速验证组件获取动态令牌code,后端向微信服务器发送get请求并带上code获取每个用户唯一的openid,然后记录到mysql中,并签发token。该openid就是登录小程序的唯一凭证。


2.简单实现



  • 获取openid,如果通过openid查不用户,就自动新建用户,并返回token。


#####LoginView###########

code = request.data.get("code")
appid = appid # 微信小程序的appid
appsecret = "xxxxxxxx" # 微信小程序的密钥,登录微信公众平台即可获取
# 获取openid和session_token
querystring = {"appid":appid,"js_code":code,"secret":appsecret,"grant_type":"authorization_code"}
jscode2session = requests.get('https://api.weixin.qq.com/sns/jscode2session',params=querystring)
if not jscode2session.json().get("errcode"):
data = jscode2session.json()
########拿到openid#########
openid=data.get("openid")
#######去数据库比对,如果通过openid查到用户并且未被禁用,就新建##########
try:
user = models.UserInfo.objects.get(openid)
if user.is_deleted: # 检查用户是否被禁用
return ErrorResponse(msg='用户已被禁用,无法登录',data=data,code=302)
except models.UserInfo.DoesNotExist:
models.UserInfo.objects.create()

3.更进一步:通过手机号来区别普通用户和工作人员


openid虽然做到的唯一性验证,但是当用户数量庞大时,该如何区分用户角色:



  • 一:手动在后台根据已有用户分配权限

  • 二:登录时根据某一标识区分角色


    方法一显然不靠谱,因为用户至少会超过1000人,方法二需要额外标识,显然手机号最合适。



3.1 前端获取手机号的动态令牌


小程序提供了手机号快速验证组件,方便我们获取手机号


bindgetphonenumber 事件回调中的动态令牌code传到开发者后台


  <view class="title">欢迎来到广盈预约</view>
<view class="card">
<view class="button">快捷登录</view>
<button
style="opacity: 0"
class="bottom-button"
open-type="getPhoneNumber|agreePrivacyAuthorization"
bindgetphonenumber="getrealtimephonenumber"
bindagreeprivacyauthorization="handleAgreePrivacyAuthorization"
>

同意隐私协议并授权手机号注册
</button>
</view>
</view>

Page({
getPhoneNumber (e) { console.log(e.detail.code) // 动态令牌 }
})

image.png



注意:如果你想获取用户手机号就必须添加用户授权《隐私保护协议》bindagreeprivacyauthorization,否则小程序无法上线



3.2 后端带着动态令牌去微信服务器获取手机号


简单来说就是用户登录时数据库中没有手机号对应的用户,后端就会自动建立一个账号,并分配权限为普通用户,然后直接登录。


这样的话,只需要第一次登录时获取手机号,以后登录就可以直接进入系统。


class RegisterView(APIView):  
authentication_classes = []
permission_classes = []
def getmobile(self,appid,code):
"""获取用户的手机号"""
try:
appsecret = "cxxxxxxxxx"
querystring = {"appid":appid,"secret":appsecret,"grant_type":"client_credential"}
response = requests.get('https://api.weixin.qq.com/cgi-bin/token',params=querystring)
access_token = response.json().get("access_token")
querystring = {"access_token":access_token}
headers = {"content-type": "application/json"}
payload = {"code":code}
mobile =requests.post(f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}",json=payload,headers=headers)
return mobile.json().get('phone_info').get('phoneNumber')
except Exception as e:
return


def post(self, request):
unionid =request.data.get("unionid")
nickname =request.data.get("nickname")
openid = request.data.get("openid")
code = request.data.get("code")
appid = request.data.get("appid")

mobile = self.getmobile(appid=appid,code=code)
if not mobile:
return ErrorResponse(msg="手机号获取失败")

defaults = {
"openid":openid,
"unionid":unionid,
"mobile":mobile,
"nickname":nickname,
}

"""这条语句将查找一个符合mobile=mobile条件的记录,如果找到就更新 defaults中的字段 ,否则就创建
注意: 查询的条件必须是唯一的,否则会造成多条数据返回而报错,这个逻辑同 get() 函数。
注意: 使用的字段,没有唯一的约束,并发的调用这个方法可能会导致多条相同的值插入。
"""

models.UserInfo.objects.update_or_create(mobile=mobile,defaults=defaults)
models.User_GZH.objects.get_or_create(unionid=unionid, defaults={'unionid':unionid})
return DetailResponse()

作者:大海前端
来源:juejin.cn/post/7293177336488804391
收起阅读 »

进程还在,JSF接口不干活了,这你敢信?

1、问题背景: 应用在配合R2m升级redis版本的过程中,上游反馈调用接口报错,RpcException:[Biz thread pool of provider has been exhausted],通过监控系统和日志系统定位到现象只出现在一两个节点,并...
继续阅读 »

1、问题背景:


应用在配合R2m升级redis版本的过程中,上游反馈调用接口报错,RpcException:[Biz thread pool of provider has been exhausted],通过监控系统和日志系统定位到现象只出现在一两个节点,并持续出现。第一时间通过JSF将有问题的节点下线,保留现场,业务恢复。


报错日志如下:


24-03-13 02:21:20.188 [JSF-SEV-WORKER-57-T-5] ERROR BaseServerHandler - handlerRequest error msg:[JSF-23003]Biz thread pool of provider has been exhausted, the server port is 22003
24-03-13 02:21:20.658 [JSF-SEV-WORKER-57-T-5] WARN BusinessPool - [JSF-23002]Task:com.alibaba.ttl.TtlRunnable - com.jd.jsf.gd.server.JSFTask@0 has been reject for ThreadPool exhausted! pool:80, active:80, queue:300, taskcnt: 1067777

2、排查步骤:


从现象开始推测原因,系统启动时,会给JSF线程池分配固定的大小,当线程都在工作的时,外部流量又打进来,那么会没有线程去处理请求,此时会有上述的异常。那么JSF线程在干什么呢?


1)借助SGM打印栈信息


2)分析栈信息


可以用在线分析工具:spotify.github.io/threaddump-…


2.1)分析线程状态


通过工具可以定位到JSF线程大部分卡在JedisClusterInfoCache#getSlaveOfSlotFromDc方法,如图:






























2.2)分析线程夯住的方法


getSlaveOfSlotFromDc在方法入口就需要获取读锁,同时在全局变量声明了读锁和写锁:
















此时对问题有一个大体的了解,大概推测:getSlaveOfSlotFromDc是获取redis连接池,该方法入口处需要获取读锁,由于读锁之间不会互斥,所以猜测有业务获取到写锁后没有释放。同时读锁没有设置超时时间,所以导致杰夫线程处理业务时卡在获取读锁处,无法释放。


2.3)从业务的角度分析持有写锁的逻辑


向中间件研发寻求帮助,经过排查,定位到有个更新拓扑的定时任务,执行时会先获取写锁,根据该消息,定位到任务的栈信息:









代码截图:









图1









图2









图3


从日志验证:日志只打印更新拓扑的日志,没有打印更新成功的日志,且02:20分以后r2m-topo-updater就不在打印日志









2.4)深入挖掘原因


虽然现象已经可以推测出来,但是对问题的原因还是百思不得其解,难道parallelStream().forEach存在bug?难道有远程请求,没有设置超时时间?...


经过查找资料确认,如果没有指定,那么parallelStream().forEach会使用ForkJoinPool.commonPool这个默认的线程池去处理任务,该线程池默认设置(容器核心数-1)个活跃线程。同时caffeine数据过期后会异步刷新数据,如果没有指定线程池,它默认也会使用ForkJoinPool.commonPool()来执行异步线程。那么就有概率出现获取到写锁的线程无法获取执行权,获取执行权的线程无法获取到读锁。









2.5)验证


3个ForkJoinPool.commonPool-worker的确都夯在获取redis连接处,线程池的活跃线程都在等待读锁。









本地caffeine缓存没有设置自定义线程池









topo-updater夯在foreach业务处理逻辑中









3.复盘


1)此问题在特定的使用场景下才会小概率出现,非常感谢中间件团队一起协助定位问题,后续也将异步更新拓扑改为同步处理。


2)Java提供了很多异步处理的能力,但是异常处理也代表需要开启线程或者使用共用的线程池,也需要注意。


3)做好监控,能第一时间发现问题并处理问题。


作者:京东科技 田蒙


来源:京东云开发者社区


作者:京东云开发者
来源:juejin.cn/post/7379831020496715813
收起阅读 »

通过代码实现 pdf 文件自动盖章

序言在数字化时代,电子文档的安全性和真实性越来越受到重视。电子印章作为一种数字化的身份验证工具,已经成为确保文档合法性和不可篡改性的重要手段。然而,传统的电子印章往往需要人工操作,不仅效率低下,而且在处理大量文件时容易出错。为了解决这一问题,自动化地给PDF文...
继续阅读 »

序言

在数字化时代,电子文档的安全性和真实性越来越受到重视。电子印章作为一种数字化的身份验证工具,已经成为确保文档合法性和不可篡改性的重要手段。然而,传统的电子印章往往需要人工操作,不仅效率低下,而且在处理大量文件时容易出错。为了解决这一问题,自动化地给PDF文件盖电子章成为了一个迫切的需求。本文将详细介绍,如何通过 .net 程序实现这一功能,废话不多说,步入正题

Nuget 包

本文的核心包为:

  • iTextSharp,用它来操作 pdf 文件非常方便,具体的用法这里不多赘述,请参考官网
  • DynamicExpresso,一个非常好用的动态表达式解析工具包

Include="DynamicExpresso.Core" Version="2.16.1" />
Include="iTextSharp" Version="5.5.13.3" />
Include="Newtonsoft.Json" Version="13.0.3" />

素材准备

本案例用到的素材包括:用于测试的 pdf 文件一个,模拟电子章图片一张,以及盖章配置文件,文件内容如下:

[
{
"SignType" : "image",//素材类型,image表示图片素材,text 表示文本素材
"LastPage" : true,//是否仅最后一页盖章
"ImageUrl" : "https://xxxxxxxx",//图片素材的下载链接
"FileName" : "sign.png",//图片素材文件名称
"ScalePercent" : 20,//图片缩放百分比,100 表示不缩放
"Opacity" : 0.6,//图片透明度,1 表示不透明
"LocationX" : "(input.Width/10)*6",//图片素材的绝对位置表达式,(0,0) 表示左下角
"LocationY" : "input.Height/23 +20",//input.With 和 input.Height 代表 pdf 文件的宽度及高度
"Rotation" : 0//素材的旋转角度
},
{
"SignType" : "text",
"LastPage" : true,
"LocationX" : "(input.Width/10)*6+85",
"LocationY" : "input.Height/23 ",
"Rotation" : 0,
"FontSize" : 20,
"Opacity" : 0.6,
"FontColor" : {//文本素材的字体颜色值
"R" : 255,
"G" : 0,
"B" : 0
},
"Text" : "input.Date"//文本素材的表达式,也可以直接写固定文本
}
]

说明:

  1. 这里之所以设计为一个数组,是因为可能有些场景下,不仅需要盖电子章,还需要自动签上日期,比如本案例。
  2. 签署位置可以自定义,坐标(0,0)代表的是左下角,x 变大即表示横向右移,y 变大表示纵向上移。
  3. 配置文件存储,我这里是把配置文件放在了本地,当然你可以存储在任何地方,比如 MongoDB等。

代码展示

本案例采用的是 .net7.0,当然 .net6及以后都是可以的。

  1. 配置文件类,与上一步的 json 配置文件对应
namespace PdfSign;

public class SignOpt
{
public string SignType { get; set; }
public bool LastPage { get; set; }
public string ImageUrl { get; set; }
public string FileName { get; set; }
public int ScalePercent { get; set; } = 50;
public string LocationX { get; set; }
public string LocationY { get; set; }
public float LocationYf { get; set; }
public float Rotation { get; set; } = 0;
public int FontSize { get; set; }
public float Opacity { get; set; }
public RBGColor FontColor { get; set; }
public string? Text { get; set; }

public record RBGColor(int R, int G, int B);
}
  1. pdf 签署方法
using System.Dynamic;
using DynamicExpresso;
using iTextSharp.text;
using iTextSharp.text.pdf;
using Newtonsoft.Json.Linq;

namespace PdfSign;

public class SignService
{
public static string PdfSign(List signOpts, string pdfName)
{
var beforeFileName = pdfName; //签名之前文件名
var afterFileName = pdfName + "_sign"; //签名之后文件名
var idx = 0;
foreach (var opt in signOpts)
{
//创建盖章后生成pdf
var outputPdfStream =
new FileStream(afterFileName + ".pdf", FileMode.Create, FileAccess.Write, FileShare.);
//读取原有pdf
var pdfReader = new PdfReader(beforeFileName + ".pdf");
var pdfStamper = new PdfStamper(pdfReader, outputPdfStream);
//读取页数
var pdfPageSize = pdfReader.NumberOfPages;
//读取pdf文件第一页尺寸,得到 With 和 Height
var size = pdfReader.GetPageSize(1);
//通过表达式计算出签署的绝对坐标
var locationX = Eval(opt.LocationX, new { size.Width, size.Height });
var locationY = Eval(opt.LocationY, new { size.Width, size.Height });

if (opt.LastPage)
{
//盖章在最后一页
var pdfContentByte = pdfStamper.GetOverContent(pdfPageSize);
var gs = new PdfGState
{
FillOpacity = opt.Opacity
};
pdfContentByte.SetGState(gs);
switch (opt.SignType.ToLower())
{
case "image":
//获取图片
var image = Image.GetInstance(opt.FileName);
//设置图片比例
image.ScalePercent(opt.ScalePercent);
//设置图片的绝对位置,位置偏移方向为:左到右,下到上
image.SetAbsolutePosition(locationX, locationY);
//图片添加到文档
pdfContentByte.AddImage(image);
break;
case "text":
if (string.IsNullOrWhiteSpace(opt.Text))
continue;
var font = BaseFont.CreateFont();
var text = Eval(opt.Text, new { Date = DateTime.Now.ToString("yyyy-MM-dd") });
//开始写入文本
pdfContentByte.BeginText();
pdfContentByte.SetColorFill(
new BaseColor(opt.FontColor.R, opt.FontColor.G, opt.FontColor.B));
pdfContentByte.SetFontAndSize(font, opt.FontSize);
pdfContentByte.SetTextMatrix(0, 0);
pdfContentByte.ShowTextAligned(Element.ALIGN_CENTER, text,
locationX, locationY, opt.Rotation);

pdfContentByte.EndText();
break;
}
}

pdfStamper.Close();
pdfReader.Close();
idx++;
if (idx >= signOpts.Count) continue;
//文件名重新赋值
beforeFileName = afterFileName;
afterFileName += "_sign";
}

return afterFileName + ".pdf";
}

//计算动态表达式的值
public static T? Eval(string expr, object context)
{
if (string.IsNullOrWhiteSpace(expr))
return default;

var target = new Interpreter();
var input = JObject.FromObject(context);

target.SetVariable("input", input.ToObject());
return target.Eval(expr);
}
}
  1. 测试调用
using Newtonsoft.Json;
using PdfSign;

//读取签名所需配置文件
var signOpts = await GetSignOpt();

if (signOpts != null && signOpts.Any())
{
//执行 pdf 文件盖章
var signFileName= SignService.PdfSign(signOpts, "test");
}

//读取配置文件
static async Task<List<SignOpt>?> GetSignOpt()
{
var strSign = await File.ReadAllTextAsync("cfg.json");
return JsonConvert.DeserializeObject<List<SignOpt>>(strSign);
}
  1. 效果展示
    原 pdf 文件如下图: image.png 最终效果如下图: image.png

结束语

随着本文的深入探讨,我们共同经历了一个完整的旅程,从理解电子印章的重要性到实现一个自动化的.NET程序,用于在PDF文件上高效、准确地加盖电子章。我们不仅学习了.NET环境下处理PDF文件的技术细节,还掌握了如何将电子印章整合到我们的应用程序中,以实现自动化的文档认证过程。


作者:架构师小任
来源:juejin.cn/post/7377643248187080715

收起阅读 »

【已解决】uniapp小程序体积过大、隐私协议的问题

web
概述 在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserP...
继续阅读 »

概述


在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."}


定位原因


程序出现问题,首先需要把原因定位归结在第一点,这是解决问题的关键,检查了一下Git仓库的修改情况,发现引入了一个7kb大小的防抖插件,其实7kb的插件不是根本问题,问题是之前的代码写的太不规范了。


压缩资源


尽量把static下面的图片都压缩一下,这里推荐一个好用的压缩网站,图片进行压缩:tinypng.com/


我没有压缩过Js文件,但会有一种方法压缩js文件,使js文件尽量的缩小来减少js文件建立的文件体积。


uniapp官方压缩建议:


小程序工具提示vendor.js过大,已经跳过es6向es5转换。这个转换问题本身不用理会,因为vendor.js已经是es5的了。


关于体积控制,参考如下:



  • 使用运行时代码压缩
    HBuilderX创建的项目勾选运行-->运行到小程序模拟器-->运行时是否压缩代码

  • cli创建的项目可以在package.json中添加参数--minimize,示例:"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --minimize"


小程序分包处理



  • 在对应平台的配置下添加 "optimization":{"subPackages":true}开启分包优化

  • 目前只支持 mp-weixin、mp-qq、mp-baidu、mp-toutiao、mp-kuaishou的分包优化


分包优化具体逻辑:



  • 静态文件:分包下支持 static 等静态资源拷贝,即分包目录内放置的静态资源不会被打包到主包中,也不可在主包中使用

  • js文件:当某个 js 仅被一个分包引用时,该 js 会被打包到该分包内,否则仍打到主包(即被主包引用,或被超过 1 个分包引用)

  • 自定义组件:若某个自定义组件仅被一个分包引用时,且未放入到分包内,编译时会输出提示信息


分包内静态文件示例


"subPackages": [{
"root": "pages/sub",
"pages": [{
"path": "index/index"
}]
}]

网络请求


还有一个解决小程序体积过大的问题,把非必要的组件都使用Http Api接口的形式去进行交互,尽量去减少本地包中的体积,再根目录下/utils里有一个232kb的获取地址交互,可以替换成Http Api的形式来解决。


隐私协议


在开发微信小程序过程中遇到了{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."},出现这个信息的原因是微信平台更新了隐私协议,需要再后台备案更新一下,搜索了很多,都不准确,这个隐私协议没有什么特殊情况,2个小时就可以通过了。


设置路径1: 公众号平台->设置->服务内容声明,设置通过后显示的状态是已更新,状态之前的是审核中


111.png


设置路径2: 首页->管理->版本管理->提交审核 ,再这里面提审,隐私协议审核过了,就可以继续开发了。


作者:stark张宇
来源:juejin.cn/post/7296025902911897627
收起阅读 »

uniapp微信小程序授权后得到“微信用户”

web
背景 近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。 (nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4...
继续阅读 »

背景


近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。


(nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132, gender=0, country=, province=, city=, language=), code=0e1abNFa1dBwRG0lnoJa18qT0i2abNFk)

经过排查,发现官方是对微信授权的接口做出了调整。小程序用户头像昵称获取规则调整公告


WX20240206-112518@2x.png

根据上面标红的字体说明,官方的意图就是只提供openid和unionid, 不暴露用户头像昵称数据。

基于此才会在新版的接口中返回"微信用户"的信息。



  • 针对这个问题,官方提供的解决方案如下。


WX20240206-112912@2x.png
以上解决方案,表达的意思是新版用户授权的接口中, 官方只会给你提供unionid和openid.

至于用户的昵称和头像,开发者可以提供功能,以用户的意志去完成修改和更新。

tips: 建议授权接口生成用户名和昵称,采用系统默认的方式。


微信授权流程


152f3cb28a734e768381f986cec1dd26.png


uniapp代码实现


uni.login接口文档


WX20240207-221556@2x.png


后端代码


WX20240207-221641@2x.png


以上是uniapp和springboot部分代码截图展示,关注微信公众号:JeecgFlow,或微信扫描下面二维码.

回复"微信用户"可以获取完整代码。


异常分析


//如果你的接口出现如下信信息,该如何处理呢?
# {errMsg: “getUserProfile:fail api scope is not
declared in the privacy agreement“, errno: 112}

出现问题的原因: api 范围未在隐私协议中声明,建议大家更具公告,更新对应的隐私协议。

【设置-服务内容声明-用户隐私保护指引】,更新隐私协议,在第一条:开发者处理的信息中,点击【增加信息类型】,选择需要授权的信息,头像昵称我已经勾选了,所以列表中不显示了,根据需求选择和填写其他内容,最后确定并生成协议。等待隐私协议审核通过。


68b3f3f0c4ee419d9ca5dec8aa5b0a4c.png
建议按需添加,以防审核不通过。


为了分辨用户,开发者将在获取你的明示同意后,收集你的微信昵称、头像。
为了显示距离,开发者将在获取你的明示同意后,收集你的位置信息。
开发者收集你的地址,用于获取位置信息。
开发者收集你的发票信息,用于维护消费功能。
为了用户互动,开发者将在获取你的明示同意后,收集你的微信运动步数。
为了通过语音与其他用户交流互动,开发者将在获取你的明示同意后,访问你的麦克风。
开发者收集你选中的照片或视频信息,用于提前上传减少上传时间。
为了上传图片或者视频,开发者将在获取你的明示同意后,访问你的摄像头。
为了登录或者注册,开发者将在获取你的明示同意后,收集你的手机号。
开发者使用你的通讯录(仅写入)权限,用于方便用户联系信息。
开发者收集你的设备信息,用于保障你正常使用网络服务。
开发者收集你的身-份-证号码,用于实名认证后才能继续使用的相关网络服务。
开发者收集你的订单信息,用于方便获取订单信息。
开发者收集你的发布内容,用于用户互动。
开发者收集你的所关注账号,用于用户互动。
开发者收集你的操作日志,用于运营维护。
为了保存图片或者上传图片,开发者将在获取你的明示同意后,使用你的相册(仅写入)权限。
为了用户互动,开发者将在获取你的明示同意后,收集你的车牌号。
开发者访问你的蓝牙,用于设备连接。
开发者使用你的日历(仅写入)权限,用于用户日历日程提醒。
开发者收集你的邮箱,用于在必要时和用户联系。
开发者收集你选中的文件,用于提前上传减少上传时间。


当你选择所需的接口后,需要您填写使用说明。 可以参考上面的内容进行填写。

给大家看一下我申请的接口。折腾半天终于把授权登录给整好了。


WX20240208-100953@2x.png


做完上述隐私设置后,需要你重新发布自己的小程序。 并且设置成采集用户隐私。

审核通过后就可以啦。如下图, 请一定注意!!!


WX20240208-101216@2x.png


参考文档


头像昵称填写-微信官方文档

uniapp头像昵称填写

getUserProfile:fail api scope is not declared in the privacy agreement


作者:代码次位面
来源:juejin.cn/post/7332113324651610150
收起阅读 »

UniApp TabBar的巅峰之作:个性化导航的魅力

web
前言在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩...
继续阅读 »

前言

在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩转系列之微信支付也优化一下

⚠️注意 本次不是从零玩转系列需要有一定的编程能力的同学

二、介绍

UniApp的TabBar

如果应用是一个多 tab 应用,可以通过 tabBar 配置项指定一级导航栏,以及 tab 切换时显示的对应页。

在 pages.json 中提供 tabBar 配置,不仅仅是为了方便快速开发导航,更重要的是在App和小程序端提升性能。在这两个平台,底层原生引擎在启动时无需等待js引擎初始化,即可直接读取 pages.json 中配置的 tabBar 信息,渲染原生tab。

Tips

  • 当设置 position 为 top 时,将不会显示 icon
  • tabBar 中的 list 是一个数组,只能配置最少2个、最多5个 tab,tab 按数组的顺序排序。
  • tabbar 切换第一次加载时可能渲染不及时,可以在每个tabbar页面的onLoad生命周期里先弹出一个等待雪花(hello uni-app使用了此方式)
  • tabbar 的页面展现过一次后就保留在内存中,再次切换 tabbar 页面,只会触发每个页面的onShow,不会再触发onLoad。
  • 顶部的 tabbar 目前仅微信小程序上支持。需要用到顶部选项卡的话,建议不使用 tabbar 的顶部设置,而是自己做顶部选项卡

三、设计

原本的ui样式,真滴丑不好看......

我改造后的,我滴妈真漂亮pink 猛男粉

设计图如下,懂前端的大佬肯定觉得没什么,虽然但是.....我是后端

可以分析他一个大的div包裹并且设置了边框圆形,里面有多个item元素菜单也设置了边框样式,每个菜单上面点击的时候会有背景颜色,我滴妈很简单啊,这我们在 从零玩转系列之微信支付当中讲过呀 给一个 `class样式 如果当前是谁就给谁 通过 vue 的 动态样式 so easy to happy !

四、实现思路

  • 删除TabBar配置的菜单栏:首先,需要从原始TabBar配置中移除默认的菜单栏,这将为自定义TabBar腾出空间。
  • 自定义底部菜单栏:接下来,自定义创建一个底部菜单栏,他是一个组件页面每个页面都需要引入
  • 自定义样式:使用CSS或相关样式设置,将自定义菜单栏精确地定位到底部,确保它与屏幕底部对齐,以实现预期的效果。

五、删除TabBar配置

好的我们尝试来删除 TabBar 配置 重新编译

可以看到报错了,这个错误就是我们使用的是switchTab进行菜单跳转使用别的肯定可以.但是为什么要用switchTab呢?

需求: 和原先的菜单栏功能一样不能销毁其他的菜单页面

那么我们将配置重新填上,他就不会报错了

⚠️注意: 这里有个问题,我们做的是菜单栏在uniapp当中菜单栏跳转是不会销毁其他页面的他其实是根据 switchTab 来进行路由的跳转不回销毁其他TabBar页面

菜单栏跳转的我们是不能销毁的那么这个配置就必须存在了呀,存在就存在无所谓!

遇事不要慌打开文档看看

这个时候我看到了什么?  hide 隐藏啊给我猜到了.绝壁有!!!!

uni.hideTabBar(OBJECT)

好我们知道有这个懂就行,后面我们进行创建我们的 自定义菜单栏组件 tabbar.vue

六、自定义TabBar

创建组件 tabbar.vue 这里我们使用vue3组合式Api搭建页面

<template>

<view class="tab-bar">

<view v-for="(item,index) in tabBarList" :key="index"
:class="{'tab-bar-item': true,currentTar: selected == item.id}"
@click="switchTab(item, index)">

<view class="tab_text" :style="{color: selected == index ? selectedColor : color}">
<image class="tab_img" :src="selected == index ? item.selectedIconPath : item.iconPath">image>
<view>{{ item.text }}view>
view>
view>
view>
template>

代码详细介绍

  1. : 这是一个外部的 view 元素,它用来包裹整个选项卡栏。
  1. : 这是一个 Vue.js 的循环指令 v-for,它用来遍历一个名为 tabBarList 的数据数组,并为数组中的每个元素执行一次循环。在循环过程中,item 是数组中的当前元素,index 是当前元素的索引。v-for 指令还使用 :key="index" 来确保每个循环元素都有一个唯一的标识符。
  1. :tab-bar-item': true,currentTar: selected == item.id}": 这是一个动态的 class 绑定,它根据条件为当前循环的选项卡元素添加不同的 CSS 类。如果 selected 的值等于当前循环元素的 item.id,则添加 currentTar 类,否则添加 tab-bar-item 类。
  1. @click="switchTab(item, index)": 这是一个点击事件绑定,当用户点击选项卡时,会触发名为 switchTab 的方法,并将当前选项卡的 item 对象和索引 index 作为参数传递给该方法。
  1. : 这是一个包含文本内容的 view 元素,它用来显示选项卡的文本。它还具有一个动态的样式绑定,根据条件选择文本的颜色。如果 selected 的值等于当前循环元素的 index,则使用 selectedColor,否则使用 color
  1. : 这是一个 image 元素,它用来显示选项卡的图标。它的 src 属性也是根据条件动态绑定,根据 selected 的值来选择显示不同的图标路径。
  1. {{ item.text }}: 这是一个用来显示选项卡文本内容的 view 元素,它显示了当前选项卡的文本,文本内容来自于 item.text

编写函数

代码当中的 tabBarList 函数要和 pages.json -> tabbar 配置一样哦

<script setup>
import { defineProps, ref } from 'vue'

// 子组件传递参数
const props = defineProps({
selected: {
type: Number,
default: 0
}
})

// 为选中颜色
let color = ref('#000')
// 选中的颜色
let selectedColor = ref('#ffb2b2')
// 菜单栏集合 - 与 pages.json -> tabbar 配置一样
let tabBarList = ref([
{
"id": 0,
"pagePath": "/pages/index/index",
"iconPath": "../../static/icon/icon_2.png",
"selectedIconPath": "../../static/icon/icon_2.png",
"text": "购买课程"
},
{
"id": 1,
"pagePath": "/pages/order/order",
"iconPath": "../../static/icon/gm_1.png",
"selectedIconPath": "../../static/icon/gm_1.png",
"text": "我的订单"
},
{
"id": 2,
"pagePath": "/pages/about/about",
"iconPath": "../../static/icon/about_3.png",
"selectedIconPath": "../../static/icon/about_3.png",
"text": "关于"
}
])

// 跳转tabBar菜单栏
const switchTab = (item) => {
let url = item.pagePath;
uni.switchTab({
url
})
}

script>

自定义TabBar样式


<style lang="less" scoped>
// 外部装修
.tab-bar {
position: fixed;
bottom: 25rpx;
left: 15rpx;
right: 15rpx;
height: 100rpx;
background: white;
padding: 20rpx;
border-radius: 30rpx;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 15px rgba(165, 168, 171, 0.83) !important;

// 当前点击的
.currentTar {
border-radius: 15rpx;
box-shadow: 0 0 15px #D7D7D7FF !important;
transition: all 0.5s ease-in-out;
}

// 给每个 item 设置样式
.tab-bar-item {
//flex: 0.5;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 150rpx;
padding: 15rpx;
background-color: transparent;
transition: all 0.5s ease-in-out;
margin: auto;

// 限制每个icon的大小
.tab_img {
width: 37rpx;
height: 41rpx;
}

// 限制文字大小
.tab_text {
font-size: 20rpx;
margin-top: 9rpx;
flex: 1;
}
}
}
style>

测试

我们自定义的效果出来了但是下面是什么鬼.....

可以看到我们下面也有一个菜单栏是 tabbar 配置产生出来的,我们前面不是说了隐藏吗?

修改函数新增隐藏tabbar代码

// 隐藏原生TabBar
uni.hideTabBar();

最后

本期结束咱们下次再见👋~

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗


作者:杨不易呀
来源:juejin.cn/post/7330295657167290403

收起阅读 »

优雅解决uniapp微信小程序右上角胶囊菜单覆盖问题

前言 大家好,今天聊一下在做uniapp多端适配项目,需要用到自定义导航时,如何解决状态栏塌陷及导航栏安全区域多端适配问题,下文只针对H5、APP、微信小程序三端进行适配,通过封装一个通用高阶组件包裹自定义导航栏内容,主要是通过设置padding来使内容始终保...
继续阅读 »

前言


大家好,今天聊一下在做uniapp多端适配项目,需要用到自定义导航时,如何解决状态栏塌陷及导航栏安全区域多端适配问题,下文只针对H5、APP、微信小程序三端进行适配,通过封装一个通用高阶组件包裹自定义导航栏内容,主要是通过设置padding来使内容始终保持在安全区域,达到低耦合,可复用性强的效果。


一、创建NavbarWrapper.vue组件


大致结构如下:




<script>
export default {
name: 'NavbarWrapper',
data() {
return {
// 像素单位
pxUnit: 'px',
// 默认状态栏高度
statusBarHeight: 'var(--status-bar-height)',
// 微信小程序右上角的胶囊菜单宽度
rightSafeArea: 0
}
}
}
script>


<style scoped>
.navbar-wrapper {
/**
* 元素的宽度和高度包括了内边距(padding)和边框(border),
* 而不会被它们所占据的空间所影响
* 子元素继承宽度时,只会继承内容区域的宽度
*/

box-sizing: border-box;
}
style>


目的


主要是动态计算statusBarHeight和rightSafeArea的值。


解决方案


APP端只需一行css代码即可


.navbar-wrapper {
padding-top: var(--status-bar-height);
}

下面是关于--status-bar-height变量的介绍:


image.png


从上图可以知道--status-bar-height只在APP端是手机实际状态栏高度,在微信小程序是固定的25px,并不是手机实际状态栏高度;


微信小程序时,除了状态栏高度还需要获取右上角的胶囊菜单所占宽度,保持导航栏在安全区域。


以下使用uni.getWindowInfo()uni.getMenuButtonBoundingClientRect()来分别获取状态栏高度和胶囊相关信息,api介绍如下图所示:


image.png


image.png


主要逻辑代码


在NavbarWrapper组件创建时,做相关计算


created() {
const px = this.pxUnit
// #ifndef H5
// 获取窗口信息
const windowInfo = uni.getWindowInfo()
this.statusBarHeight = windowInfo.statusBarHeight + px
// #endif

// #ifdef MP-WEIXIN
// 获取胶囊左边界坐标
const { left } = uni.getMenuButtonBoundingClientRect()
// 计算胶囊(包括右边距)占据屏幕的总宽度:屏幕宽度-胶囊左边界坐标
this.rightSafeArea = windowInfo.windowWidth - left + px
// #endif
}

用法


<NavbarWrapper>
<view class="header">headerview>
NavbarWrapper>

二、多端效果展示


微信小程序


b15a0866000c13e58259645f2459440.jpg


APP端


45ee33b12dcf082e5ac76dc12fc41de.jpg


H5端


22b1984f8b21a4cb79f30286a1e4161.jpg


三、源码


NavbarWrapper.vue




<script>
export default {
name: 'NavbarWrapper',
data() {
return {
// 像素单位
pxUnit: 'px',
// 默认状态栏高度
statusBarHeight: 'var(--status-bar-height)',
// 微信小程序右上角的胶囊菜单宽度
rightSafeArea: 0
}
},
created() {
const px = this.pxUnit
// #ifndef H5
// 获取窗口信息
const windowInfo = uni.getWindowInfo()
this.statusBarHeight = windowInfo.statusBarHeight + px
// #endif

// #ifdef MP-WEIXIN
// 获取胶囊左边界坐标
const { left } = uni.getMenuButtonBoundingClientRect()
// 计算胶囊(包括右边距)占据屏幕的总宽度:屏幕宽度-胶囊左边界坐标
this.rightSafeArea = windowInfo.windowWidth - left + px
// #endif
}
}
script>


<style scoped>
.navbar-wrapper {
/**
* 元素的宽度和高度包括了内边距(padding)和边框(border),
* 而不会被它们所占据的空间所影响
* 子元素继承宽度时,只会继承内容区域的宽度
*/

box-sizing: border-box;
background-color: deeppink;
}
style>




作者:vilan_微澜
来源:juejin.cn/post/7309361597556719679
收起阅读 »

MySQL的 where 1=1会不会影响性能?看完官方文档就悟了!

在日常业务开发中,会通过使用where 1=1来简化动态 SQL语句的拼接,有人说where 1=1会影响性能,也有人说不会,到底会不会影响性能?本文将从 MySQL的官方资料来进行分析。 动态拼接 SQL的方法 在 Mybatis中,动态拼接 SQL最常用的...
继续阅读 »

在日常业务开发中,会通过使用where 1=1来简化动态 SQL语句的拼接,有人说where 1=1会影响性能,也有人说不会,到底会不会影响性能?本文将从 MySQL的官方资料来进行分析。


动态拼接 SQL的方法


在 Mybatis中,动态拼接 SQL最常用的两种方式:使用 where 1=1 和 使用标签。


使用where 1=1


使用过 iBATIS的小伙伴应该都知道:在 iBATIS中没有标签,动态 SQL的处理相对较为原始和复杂,因此使用where 1=1这种写法的用户很大一部分是还在使用 iBATIS 或者是从 iBATIS过度到 Mybatis。


如下示例,通过where 1=1来动态拼接有效的 if语句:


<select id="" parameterType = "">
SELECT * FROM user
WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null ">
AND age = #{age }
</if>
</select>

使用标签


Mybatis提供了标签,标签只有在至少一个 if条件有值的情况下才去生成 where子句,若 AND或 OR前没有有效语句,where元素会将它们去除,也就是说,如果 Mybatis通过标签动态生成的语句为where AND name = '111',最终会被优化为where name = '111'


标签使用示例如下:


<select id="" parameterType = "">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>

标签是在 MyBatis中引入的,所以,很多一开始就使用 MyBatis的用户对这个标签使用的比较多。


性能影响


where 1=1到底会不会影响性能?我们可以先看一个具体的例子:



说明:示例基于 MySQL 8.0.30



可以使用如下指令查看 MySQL版本:


SELECT VERSION();

image.png


场景:基于一张拥有 100多万条数据的user表,根据name进行查询,


查看表结构和表的总数据,如下图:


image.png


image.png


下面,通过执行两条 SQL查询语句(一条带有 1=1):


select * from user where name = 'name-96d1b3ce-1a24-4d47-b686-6f9c6940f5f6';
select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';

image.png


对比两条 SQL执行的结果,可以发现它们消耗的时间几乎相同,因此,看起来where 1=1对整体的性能似乎并不影响。


为了排除一次查询不具有代表性,我们分别对两条 SQL语句查询 100遍,然后计算平均值:


SET PROFILING = 1;
DO SLEEP(0.001); -- 确保每次查询之间有足够时间间隔

SET @count = 0;
WHILE @count < 100 DO
select * from user where name = 'name-96d1b3ce-1a24-4d47-b686-6f9c6940f5f6';
-- or
select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';
SET @count = @count + 1;
END WHILE;

SHOW PROFILES;

两条 SQL分别执行 100次后,最终也发现它们的平均值几乎相同,因此,上述示例似乎证明了 where 1=1 对整体的性能并没有不影响。


为什么没有影响?是不是 MySQL对 1=1进行了优化?


为了证明猜想,我们借助show warnings命令来查看信息,在 MySQL中,show warnings命令用于显示最近执行的 SQL语句产生的警告、错误或通知信息。它可以帮助我们了解语句执行过程中的问题。如下示例:


explain select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';
show warnings;

image.png


将上述示例的 warnings信息摘出来如下:


/* select#1 */ select `yuanjava`.`user`.`id` AS `id`,
`yuanjava`.`user`.`name` AS `name`,
`yuanjava`.`user`.`age` AS `age`,
`yuanjava`.`user`.`sex` AS `sex`,
`yuanjava`.`user`.`created_at` AS `created_at`
from `yuanjava`.`user`
where (`yuanjava`.`user`.`name` = 'name-f692472e-40de-4053-9498-54b9800e9fb1')

从 warnings信息可以看出:1=1已经被查询优化器优化掉,因此,对整体的性能影响并不大。


那么,有没有 MySQL的官方资料可以佐证 where 1=1确实被优化了?


答案:有!MySQL有一种 Constant-Folding Optimization(常量折叠优化)的功能。


Constant-Folding Optimization


MySQL的优化器具有一项称为 Constant-Folding Optimization(常量折叠优化)的功能,可以从查询中消除重言式表达式。Constant-Folding Optimization 是一种编译器的优化技术,用于优化编译时计算表达式的常量部分,从而减少运行时的计算量,换句话说:Constant-Folding Optimization 是发生在编译期,而不是引擎执行期间。


对于上述表达的"重言式表达式"又是什么呢?


重言式


重言式(Tautology )又称为永真式,它的汉语拼音为:[Chóng yán shì],是逻辑学的名词。命题公式中有一类重言式,如果一个公式,对于它的任一解释下其真值都为真,就称为重言式(永真式)。


其实,重言式在计算机领域也具有重要应用,比如"重言式表达式"(Tautological expression),它指的是那些总是为真的表达式或逻辑条件。


在 SQL查询中,重言式表达式是指无论在什么情况下,结果永远为真,它们通常会被优化器识别并优化掉,以提高查询效率。例如,如果 where中包含 1=1 或 A=A 这种重言式表达式,它们就会被优化器移除,因为对查询结果没有实际影响。如下两个示例:


SELECT * from user where 1=1 and name = 'xxx';
-- 被优化成
SELECT * from user where name = 'xxx'

SELECT id, name, salary * (1 + 0.05 * 2) AS real_salary FROM employees;
-- 优化成(1 + 0.05 * 2 被优化成 1.1)
SELECT id, name, salary * 1.1 AS real_salary FROM employees;

另外,通过下面 MySQL架构示意图可以看出:优化器是属于 MySQL的 Server层,因此,Constant-Folding Optimization功能支持受 MySQL Server的版本影响。


image.png


查阅了 MySQL的官方资料,Constant-Folding Optimization 从 MySQL5.7版本开始引入,至于 MySQL5.7以前的版本是否具备这个功能,还有待考证。


如何选择?


where 1=1 标签 两种方案,该如何选择?



  • 如果 MySQL Server版本大于等于 5.7,两个随便选,或者根据团队的要求来选;

  • 如果 MySQL Server版本小于 5.7,假如使用的是 MyBatis,建议使用 标签,如果使用的还是比较老的 iBATIS,只能使用where 1=1

  • 如果 MySQL Server版本小于 5.7,建议升升级



信息补充:2009年5月,iBATIS从 2.0版本开始更名为 MyBatis, 标签最早出现在MyBatis 3.2.0版本中



总结


where 1=1 标签到底会不会影响性能,这个问题在网上已经出现了很多次,今天还是想从官方文档来进行说明。本文通过 MySQL的官方资料,加上百万数据的表进行真实测试,得出下面的结论:



  • 如果 MySQL Server版本大于等于 5.7,两个随便选,或者根据团队的要求来选;

  • 如果 MySQL Server版本小于 5.7,假如使用的是 MyBatis,建议使用 标签,如果使用的还是比较老的 iBATIS,只能使用where 1=1


最后,遇到问题,建议首先查找官方的一手资料,这样才能帮助自己在一条正确的技术道路上成长!


参考资料


MySQL8.0 Constant-Folding Optimization


MySQL5.7 WHERE Clause Optimization


What’s New in MySQL 5.7




作者:猿java
来源:juejin.cn/post/7374238289107648551
收起阅读 »

代码很少,却很优秀!RocketMQ的NameServer是如何做到的?

今天我们来一起深入分析 RocketMQ的注册中心 NameServer。 本文基于 RocketMQ release-5.2.0 首先,我们回顾下 RocketMQ的内核原理鸟瞰图: 从上面的鸟瞰图,我们可以看出:Nameserver即和 Broker...
继续阅读 »

今天我们来一起深入分析 RocketMQ的注册中心 NameServer。



本文基于 RocketMQ release-5.2.0



首先,我们回顾下 RocketMQ的内核原理鸟瞰图:


image.png


从上面的鸟瞰图,我们可以看出:Nameserver即和 Broker交互,也和 Producer和 Consumer交互,因此,在 RocketMQ中,Nameserver起到了一个纽带性的作用。


接着,我们再看看 NameServer的工程结构,如下图:


image.png


整个工程只有 11个类(老版本好像只有不到 10个类),为什么 RocketMQ可以用如此少的代码,设计出如此高性能且轻量的注册中心?


我觉得最核心的有 3点是:



  1. AP设计思想

  2. 简单的数据结构

  3. 心跳机制


AP设计思想


像 ZooKeeper,采用了 Zab (Zookeeper Atomic Broadcast) 这种比较重的协议,必须大多数节点(过半数)可用,才能确保了数据的一致性和高可用,大大增加了网络开销和复杂度。


而 NameServer遵守了 CAP理论中 AP,在一个 NameServer集群中,NameServer节点之间是P2P(Peer to Peer)的对等关系,并且 NameServer之间并没有通信,减少很多不必要的网络开销,即便只剩一个 NameServer节点也能继续工作,足以保证高可用。


数据结构


NameServer维护了一套比较简单的数据结构,内部维护了一个路由表,该路由表包含以下几个核心元数据,对应的源码类RouteInfoManager如下:


public class RouteInfoManager {
private final static long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; // broker失效时间 120s
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map/* topic */, Map> topicQueueTable;
private final Map/* brokerName */, BrokerData> brokerAddrTable;
private final Map/* clusterName */, Set/* brokerName */>> clusterAddrTable;
private final Map/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final Map/* brokerAddr */, List/* Filter Server */> filterServerTable;
}


  • topicQueueTable: Topic消息队列路由信息,消息发送时根据路由表进行负载均衡

  • brokerAddrTable: Broker基础信息,包括brokerName、所属集群名称、主备Broker地址

  • clusterAddrTable: Broker集群信息,存储集群中所有Broker名称

  • brokerLiveTable: Broker状态信息,NameServer每次收到心跳包是会替换该信息

  • filterServerTable: Broker上的FilterServer列表,用于过滤标签(Tag)或 SQL表达式,以减轻 Consumer的负担,提高消息消费的效率。


TopicRouteData


TopicRouteData是 NameServer中最重要的数据结构之一,它包括了 Topic对应的所有 Broker信息以及每个 Broker上的队列信息,filter服务器列表,其源码如下:


public class TopicRouteData {
private List queueDatas;
private List brokerDatas;
private HashMap> filterServerTable;
//It could be null or empty
private Map/*brokerName*/, TopicQueueMappingInfo> topicQueueMappingByBroker;
}

BrokerData


BrokerData包含了 Broker的基本属性,状态,所在集群以及 Broker服务器的 IP地址,其源码如下:


public class BrokerData {
private String cluster;//所在的集群
private String brokerName;//所在的brokerName
private HashMap brokerAddrs;//该broker对应的机器IP列表
private String zoneName; // 区域名称
}

QueueData


QueueData包含了 BrokerName,readQueue的数量,writeQueue的数量等信息,对应的源码类是QueueData,其源码如下:


public class QueueData {
private String brokerName;//所在的brokerName
private int readQueueNums;// 读队列数量
private int writeQueueNums;// 写队列数量
private int perm; // 读写权限,参考PermName 类
private int topicSysFlag; // topic同步标记,参考TopicSysFlag 类
}

元数据举例


为了更好地理解元数据,这里对每一种元数据都给出一个数据实例:


topicQueueTable:{
"topicA":[
{
"brokeName":"broker-a",
"readQueueNums":4,
"writeQueueNums":4,
"perm":6,
"topicSyncFlag":0
},
{
"brokeName":"broker-b",
"readQueueNums":4,
"writeQueueNums":4,
"perm":6,
"topicSyncFlag":0
}
],
"topicB":[]
}

brokeAddrTable:{
"broker-a":{
"cluster":"cluster-1",
"brokerName":"broker-a",
"brokerAddrs":{
0:"192.168.0.1:8000",
1:"192.168.0.2:8000"
}
},
"broker-b":{
"cluster":"cluster-1",
"brokerName":"broker-b",
"brokerAddrs":{
0:"192.168.0.3:8000",
1:"192.168.0.4:8000"
}
}
}

brokerLiveTable:{
"192.168.0.1:8000":{
"lastUpdateTimestamp":1533434434344,//long 的时间戳
"dataVersion":dataVersionObj, //参考DataVersion类
"channel":channelObj,// 参考io.netty.channel.Channel
"haServerAddr":"192.168.0.2:8000"
},
"192.168.0.2:8000":{
"lastUpdateTimestamp":1533434434344,//long 的时间戳
"dataVersion":dataVersionObj, //参考DataVersion类
"channel":channelObj,// 参考io.netty.channel.Channel
"haServerAddr":"192.168.0.1:8000"
},
"192.168.0.3:8000":{ },
"192.168.0.4:8000":{ },
}

clusterAddrTable:{
"cluster-1":[{"broker-a"},{"broker-b"}],
"cluster-2":[],
}

filterServerTable:{
"192.168.0.1:8000":[{"192.168.0.1:7000"}{"192.168.0.1:9000"}],
"192.168.0.2:8000":[{"192.168.0.2:7000"}{"192.168.0.2:9000"}],
}

心跳机制


心跳机制是 NameServer维护 Broker的路由信息最重要的一个抓手,主要分为接收心跳、处理心跳、心跳超时 3部分:


接收心跳


Broker每 30s会向所有的 NameServer发送心跳包,告诉它们自己还存活着,从而更新自己在 NameServer的状态,整体交互如下图:


image.png


处理心跳


NameServer收到心跳包时会更新 brokerLiveTable缓存中 BrokerLiveInfo的 lastUpdateTimeStamp信息,整体交互如下图:


image.png


处理逻辑可以参考源码:
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest#brokerHeartbeat:


public RemotingCommand brokerHeartbeat(ChannelHandlerContext ctx,
RemotingCommand request)
throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final BrokerHeartbeatRequestHeader requestHeader =
(BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class);

this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(requestHeader.getClusterName(), requestHeader.getBrokerAddr());

response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}

心跳超时


NameServer每隔 10s(每隔5s + 5s延迟)扫描 brokerLiveTable检查 Broker的状态,如果在 120s内未收到 Broker心跳,则认为 Broker异常,会从路由表将该 Broker摘除并关闭 Socket连接,同时还会更新路由表的其他信息,整体交互如下图:


image.png


private void startScheduleService() {
this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS);
}

源码参考:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unRegisterBroker(),核心流程:



  1. 遍历brokerAddrTable

  2. 遍历broker地址

  3. 根据 broker地址移除 brokerAddr

  4. 如果当前 Topic只包含待移除的 Broker,则移除该 Topic


其他核心源码解读


NameServer启动


NameServer的启动类为:org.apache.rocketmq.namesrv.NamesrvStartup,整个流程如下图:


image.png
图片来自:Mark_Zoe


NameServer启动最核心的 3个事情是:



  1. 加载配置:NameServerConfig、NettyServerConfig主要是映射配置文件,并创建 NamesrvController。

  2. 启动 Netty通信服务:NettyRemotingServer是 NameServer和Broker,Producer,Consumer通信的底层通道 Netty服务器。

  3. 启动定时器和钩子程序:NameServerController实例一方面处理 Netty接收到消息后,一方面内部有多个定时器和钩子程序,它是 NameServer的核心控制器。


总结


NameServer并没有采用复杂的分布式协议来保持数据的一致性,而是采用 CAP理论中的 AP,各个节点之间是Peer to Peer的对等关系,数据的一致性通过心跳机制,定时器,延时感知来完成。


NameServer最核心的 3点设计是:



  1. AP的设计思想

  2. 简单的数据结构

  3. 心跳机制




作者:猿java
来源:juejin.cn/post/7379431978814275596
收起阅读 »

uni-app新建透明页面实现全局弹窗

web
需求背景 实现一个能遮住原生 tabbar 和 navbar 的全局操作框 原理 使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbar 和 navbar 页面配置 { "path" : "pages/shootAtWill/shootAtW...
继续阅读 »

需求背景


实现一个能遮住原生 tabbarnavbar 的全局操作框


原理


使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbarnavbar


页面配置


{
"path" : "pages/shootAtWill/shootAtWill",
"style" :
{
"navigationBarTitleText" : "随手拍",
"navigationStyle": "custom",
"backgroundColor": "transparent",
"app-plus": {
"animationType": "slide-in-bottom", // 我这边需求是从底部弹出
"background": "transparent",
"popGesture": "none",
"bounce": "none",
"titleNView": false,
"animationDuration": 150
}
}
}

页面样式


<style>
page {
/* 必须的样式,这是页面背景色为透明色 */
background: transparent;
}
</style>
<style lang="scss" scoped>
// 写你页面的其他样式
</style>

这样的话就新建成功了一个透明的页面,那么这个页面上的东西都可以遮挡住原生 tabbarnavbar


我还加了遮罩:


<template>
<view>
<view class="modal" style="z-index: -1"></view>

</view>
</template>

<style lang="scss" scoped>
.modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
</style>


效果演示


在这里插入图片描述


作者:鹏北海
来源:juejin.cn/post/7317325043541639178
收起阅读 »

【干货分享】uniapp做的安卓App如何加固

2023年了,uniapp还有人用吗? 对于这个问题,只能说,2023年了,使用uniapp去开发APP的人越来越多了。 一方面在于全平台兼容确实很香,对于一些小项目或者时间要求比较高的项目来说,可以节省大量的时间与精力,也为公司节约了成本;另一方面,开发速度...
继续阅读 »

2023年了,uniapp还有人用吗?


对于这个问题,只能说,2023年了,使用uniapp去开发APP的人越来越多了。


一方面在于全平台兼容确实很香,对于一些小项目或者时间要求比较高的项目来说,可以节省大量的时间与精力,也为公司节约了成本;另一方面,开发速度非常快。就像前面说的,对于一些小项目来说,几天就可以搞定,而对于一些大项目来说,性能和原生大差不差,而且全平台兼容的特性也可以弥补这点;最后就是社区,里面有很多优质的框架和插件,节约了大量的时间(时间就是发量!!!),更重要的是,社区出人才,总能找到人和你一起吐槽(bushi)睿智的官方......


总而言之,虽然uniapp文档一般好,bug一般多,更新像拆炸弹,但是,对于很多人来说,还是很有意义的。所以用的人还是很多。


但是目前随着各种商城上架政策的严格审查,对于加固等需求也慢慢起来了,所以今天我们来讲讲uniapp开发的安卓APP要如何加固。


加固原理


先来看看一般加固会从哪几个方向进行加固


image.png


而我们如果把uniapp制作的安卓APP在加固上其实大同小异--只要是apk或者aab格式都可以,所以我们就基于这个原理来进行加固。


加固流程


01 代码混淆


按照一般的思路,先给他混淆一下子。使用代码混淆工具来混淆 JavaScript 代码,以使其难以被逆向工程和破解。常用的混淆工具包括 ProGuard 和 DexGuard。在 UniApp 中,你可以在打包安卓应用时配置 ProGuard 来进行代码混淆。示例代码如下所示,在项目根目录下的 uniapp.pro 文件中添加以下配置:


-keep class com.dcloud.** { *; }
-keep public class * extends io.dcloud.* {
*;
}

02 加固资源文件 & 防止调试和反调试


加固资源文件: 将敏感资源文件(如证书、配置文件等)进行加密或混淆,以防止被攻击者获取。可以使用第三方工具对资源文件进行加密,或者自定义加密算法来保护资源文件的安全


防止调试和反调试: 这一步可以使用第三方库或自定义代码来实现这些保护措施。比如说,可以检测应用程序是否在调试模式下运行,并在调试模式下采取相应的措施,例如关闭应用程序或隐藏敏感信息。


import android.os.Debug;

public class DebugUtils {
public static boolean isDebugMode() {
return Debug.isDebuggerConnected();
}
}

就是说,在应用程序中调用 DebugUtils.isDebugMode() 方法,可以根据返回值来判断应用程序是否在调试模式下运行,并采取相应的措施。


03 加密敏感数据


我们直接使用PBEWithMD5AndDES 算法对数据进行加密和解密。使用的时候,你可以调用 EncryptionUtils.encrypt(data) 方法来加密敏感数据,并调用 EncryptionUtils.decrypt(encryptedData) 方法来解密数据。记得将 PASSWORDSALT 替换为你自己的密码和盐值(重要!!!)。


import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.spec.KeySpec;
import java.util.Base64;

public class EncryptionUtils {
private static final String ALGORITHM = "PBEWithMD5AndDES";
private static final String PASSWORD = "your_secret_password"; // 自定义密码,请更换为自己的密码
private static final byte[] SALT = {
(byte) 0x4b, (byte) 0x6d, (byte) 0x7d, (byte) 0x15,
(byte) 0x78, (byte) 0x56, (byte) 0x34, (byte) 0x22
}; // 自定义盐值,请更换为自己的盐值

public static String encrypt(String data) {
try {
KeySpec keySpec = new PBEKeySpec(PASSWORD.toCharArray(), SALT, 65536);
SecretKey secretKey = SecretKeyFactory.getInstance(ALGORITHM).generateSecret(keySpec);
Cipher cipher = Cipher.getInstance(ALGORITHM);
PBEParameterSpec parameterSpec = new PBEParameterSpec(SALT, 100);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] encryptedBytes = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encryptedBytes);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static String decrypt(String encryptedData) {
try {
KeySpec keySpec = new PBEKeySpec(PASSWORD.toCharArray(), SALT, 65536);
SecretKey secretKey = SecretKeyFactory.getInstance(ALGORITHM).generateSecret(keySpec);
Cipher cipher = Cipher.getInstance(ALGORITHM);
PBEParameterSpec parameterSpec = new PBEParameterSpec(SALT, 100);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
byte[] decodedBytes = Base64.getDecoder().decode(encryptedData);
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
return new String(decryptedBytes, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

04 防止篡改


我们使用 SHA-256 哈希算法计算数据的哈希值。使用的时候,可以调用 IntegrityUtils.calculateHash(data) 方法来计算数据的哈希值,并将其与原始的哈希值进行比较,以验证数据的完整性。例如:


String data = "Hello, world!";
String originalHash = "2ef7bde608ce5404e97d5f042f95f89f1c232871";
String calculatedHash = IntegrityUtils.calculateHash(data);

boolean isIntegrityVerified = IntegrityUtils.verifyIntegrity(data, originalHash);
if (isIntegrityVerified) {
System.out.println("Data integrity verified.");
} else {
System.out.println("Data has been tampered with!");
}

05 签名功能


补充一个Android签名。


1)简介


本工具用于对android加固后的apk进行重新签名。


版本文件备注
Windows版apk签名工具压缩包.exe该版本包含Java运行环境,不需要额外安装。
通用版dx-signer-v1.9r.jar该版本需要Java 8+的运行环境,请依照操作系统进行安装:Adoptium

本工具依照Apache 2.0 协议开源,可以在这里查看源码github.com/dingxiangte…



使用说明



  1. 下载签名工具dx-signer.jar,双击运行。

  2. 选择输入apk、aab文件。

  3. 选择签名的key文件,并输入key密码。

  4. 选择重签后apk、aab的路径,以apk结束。如:D:\sign.apk

  5. 点击“签名”按钮,等待即可签名完成。


ps:如果有alias(证书别名)密钥的或者有多个证书的,请在高级tab中选择alias并输入alias密码


2)多渠道功能简介


多渠道工具兼容友盟和美团walle风格的多渠道包,方便客户把APP发布到不同的应用平台,进行渠道统计。



使用说明



  1. 在app中预留读取渠道信息的入口,具体见5.2.2读取渠道信息

  2. 在5.1.1的签名使用基础上,点击选择渠道清单

  3. 选择清单文件channel.txt。具体文件格式见5.2.3

  4. 点击签名,等待生成多个带签名的渠道app


读取渠道信息


顶象多渠道工具兼容友盟和美团walle风格的多渠道包,下面是两种不同风格的渠道信息读取方法。选其中之一即可


读取渠道信息:UMENG_CHANNEL

输出的Apk中将会包含UMENG_CHANNELmata-data



name="UMENG_CHANNEL"
android:value="XXX" />


可以读取这个字段。


public static String getChannel(Context ctx) {
String channel = "";
try {
ApplicationInfo appInfo = ctx.getPackageManager().getApplicationInfo(ctx.getPackageName(),
PackageManager.GET_META_DATA);
channel = appInfo.metaData.getString("UMENG_CHANNEL");
} catch (PackageManager.NameNotFoundException ignore) {
}
return channel;
}

读取渠道信息:Walle

输出的Apk也包含Walle风格的渠道信息


可以在使用Walle的方式进行读取。


implementation 'com.meituan.android.walle:library:1.1.7'


String channel = WalleChannelReader.getChannel(this.getApplicationContext());

渠道文件格式说明


请准备渠道清单文件channel.txt, 格式为每一行一个渠道, 例如:


0001_my
0003_baidu
0004_huawei
0005_oppo
0006_vivo

结语


以上就是基于uniapp制作的Android APP的加固方式,仅供参考~ 欢迎一起交流学习~




作者:昀和
来源:juejin.cn/post/7256615625882615866
收起阅读 »

集帅(美)们,别再写 :key = "index" 啦!

web
浅聊一下 灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞... 假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相...
继续阅读 »

浅聊一下


灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞...



假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!



开始


在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点东西


虚拟DOM


什么是虚拟DOM呢?虚拟DOM是一个对象,没想到吧...我们来看看Vue是如何将template模板里面的东西交给浏览器来渲染的


image.png


首先通过 compiler 将 template模板变成一个虚拟DOM,再将虚拟DOM转换成HTML,最后再交给浏览器V8引擎渲染,那么虚拟DOM是什么样的呢?


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>

<body>
<div id="app">
<ul id="item">
<li v-for="item in list" class="item">{{item}}</li>
</ul>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['vue','js','html'])
return {
list
}
}
}).mount('#app')
</script>
</body>

</html>

在这里,template模板实际上是


 <ul>
<li v-for="item in list">{{item}}</li>
</ul>

通过v-for循环,渲染出来了3个li


<ul>
<li>vue<li>
<li>js<li>
<li>html<li>
</ul>

我们的compiler会将这个模板转化成虚拟DOM


let oldDom = {
tagName = 'ul',
props:{
//存放id 和 class 等
id:'item'
},
children[
{
tagName = 'li',
props:{
class:'item'
},
children:['vue']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['js']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['html']
},
]
}

diff算法


给前面的例子来点刺激的,加上一个按钮和反转函数,点击按钮,list反转


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>

<body>
<div id="app">
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
<button @click="change">change</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['唱','跳','rap','篮球'])
const change = ()=>{
list.value.reverse()
}
const add = ()=>{
list.value.unshift('6')
}
return {
list,
change,
}
}
}).mount('#app')
</script>
</body>

</html>

点击change按钮,此时我们的DOM更改vue又是如何来更新DOM的呢?


image.png


众所周知,回流和重绘会消耗极大的性能,而当DOM发生变更的时候会触发回流重绘(可以去看我的文章(从输入4399.com到页面渲染之间的回流和重绘),那么vue3就有一个diff算法,用来优化性能


image.png


当DOM更改,compiler会生成一个新的虚拟DOM,然后通过diff算法来生成一个补丁包,用来记录旧DOM和新DOM的差异,然后再拿到html里面进行修改,最后再交给浏览器V8进行渲染


简单介绍一下diff算法的比较规则



  1. 同层比较,是不是相同的结点,不相同直接废弃老DOM

  2. 是相同结点,比较结点上的属性,产生一个补丁包

  3. 继续比较下一层的子节点,采用双端对列的方式,尽量复用,产生一个补丁包

  4. 同上


image.png


别再写 :key = "index"


要说别写 :key = "index" ,我们得先明白key是用来干什么的...如果没有key,那么在diff算法对新旧虚拟DOM进行比较的时候就没法比较了,你看这里有两个一样的vue,当反转顺序以后diff算法不知道哪个vue该对应哪个vue了


image.png


如果我们用index来充当key的话来看,当我们在头部再插入一个结点的时候,后面的index其实是改变了的,导致diff算法在比较的时候认为他们与原虚拟DM都不相同,那么diff算法就等于没有用...


image.png


可以用随机数吗?


<li v-for="item in list" :key="Math.random()">

想出这种办法的,也是一个狠人...当然是不行的,因为在template模板更新时,会产生一个新的虚拟DOM,而这个虚拟DOM里面的key也是随机值,和原虚拟DOM里的key99.99999%是不一样的...


结尾


希望你以后再也不会写 :key = "index" 了



假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!



作者:滚去睡觉
来源:juejin.cn/post/7337513012394115111
收起阅读 »

VSCode无限画布模式(可能会惊艳到你的一个小功能)

❓现存的痛点VSCode是我的主力开发工具,在实际的开发中,我经常会对编辑器进行分栏处理,组件A的tsx、css代码、工具类方法各一个窗口,组件B的tsx、css代码、工具类方法各一个窗口,组件C的......当组件拆的足够多的时候,多个分栏会把本就不大的编辑...
继续阅读 »

❓现存的痛点

image.png

VSCode是我的主力开发工具,在实际的开发中,我经常会对编辑器进行分栏处理,组件A的tsx、css代码、工具类方法各一个窗口组件B的tsx、css代码、工具类方法各一个窗口,组件C的......

small.gif

当组件拆的足够多的时候,多个分栏会把本就不大的编辑器窗口分成N份,每一份的可视区域就小的可怜,切换组件代码时,需要不小的翻找成本,而且经常忘记我之前把文件放在了那个格子里,特别是下面的场景(一个小窗口内开了N个小tab),此时更难找到想要的窗口了...

多个tab.gif

问题汇总

  1. 分栏会导致每个窗口的面积变小,开发体验差(即使可以双击放大,但效果仍不符合预期);
  2. 编辑器窗口容易被新打开的窗口替换掉,常找不到之前打开的窗口;
  3. 窗口的可操作性不强,位置不容易调整

💡解题的思路

1. 自由 & 独立的编辑器窗口

  1. 分栏会导致每个窗口的面积变小,开发体验不好。

那就别变小了!每个编辑器窗口都还是原来的大小,甚至更大!

20240531-220533.gif

2. 无限画布

  1. 编辑器窗口容易被新打开的窗口替换掉,常找不到之前打开的窗口。
  2. 窗口的可操作性不强,位置不容易调整。

那就每个窗口都拥有一个自己的位置好了!拖一下就可以找到了!

scroll.gif

3. 画布体验

好用的画布是可以较大的提升用户体验的,下面重点做了四个方面的优化:

3.1 在编辑器里可以快速缩小 & 移动

因为不可避免的会出现一些事件冲突(比如编辑器里的滚动和画布的滚动、缩放等等),通过提供快捷键的解法,可以在编辑器内快速移动、缩放画布。

command + 鼠标上下滑动 = 缩放
option + 鼠标移动 = 画布移动

注意下图,鼠标还在编辑器窗口中,依然可以拖动画布👇🏻

single-editor.gif

3.2 快速放大和缩小编辑窗口

通过快捷按钮的方式,可以快速的放大和缩小编辑器窗口。

scale.gif

3.3 一键定位到中心点

不小心把所有窗口都拖到了画布视口外找不到了?没事儿,可以通过点击快捷按钮的方式,快速回到中心点。

center.gif

3.4 窗口的合并和分解

可以在窗口下进行编辑器的合并,即可以简单的把一些常用的窗口进行合并、分解。

add-remove.gif

💬 提出的背景

作为一名前端开发同学,避免不了接触UI同学的设计稿,我司使用的就是figma,以figma平台为例,其无限画布模式可以非常方便的平铺N个稿子,并快速的看到所有稿子的全貌、找到自己想要的稿子等等,效果如下:

figma.gif

没错!我就是基于这个思路提出了第一个想法,既然图片可以无限展示,编辑器为什么不能呢?

这个想法其实去年就有了,期间大概断断续续花了半年多左右的时间在调研和阅读VSCode的源码上,年后花了大概3个月的时间进行实现,最终在上个月做到了上面的效果。

经过约一个月的试用(目前我的日常需求均是使用这种模式进行的开发),发现效果超出预期,我经常会在画布中开启约10+个窗口,并频繁的在各个窗口之间来回移动,在这个过程中,我发现以下几点很让我很是欣喜:

  1. 空间感:我个人对“空间和方向”比较敏感,恰好画布模式会给我一种真实的空间感,我仿佛在一间房子里,里面摆满了我的代码,我穿梭在代码中,修一修这个,调一调这个~
  2. 满足感:无限画布的方式,相当于我间接拥有了无限大的屏幕,我只需要动动手指找到我的编辑窗口就好了,它可以随意的放大和缩小,所以我可以在屏幕上展示足够多的代码。
  3. 更方便的看源码:我可以把源码的每个文件单独开一个窗口,然后把每个窗口按顺序铺起来,摆成一条线,这条线就是源码的思路(当然可以用截图的方式看源码 & 缕思路,但是,需要注意一点,这个编辑器是可以交互的!)

⌨️ 后续的计划

后续计划继续增强画布的能力,让它可以更好用:

  1. 小窗口支持命名,在缩小画布时,窗口缩小,但是命名不缩小,可以直观的找到想要的窗口。
  2. 增强看源码的体验:支持在画布上添加其他元素(文案、箭头、连线),试想一下,以后在看源码时,拥有一个无限的画板来展示代码和思路,关键是代码是可以交互的,这该有多么方便!
  3. 类似MacOS的台前调度功能:把有关联的一些窗口分组,画布一侧有分组的入口,点击入口可以切换画布中的组,便于用户快速的进行批量窗口切换,比如A页面的一些JS、CSS等放在一个组,B页面放在另一个组,这样可以快速的切换文件窗口。

📔 其他的补充

调研过程中发现无法使用VSCode的插件功能来实现这个功能,所以只好fork了一份VSCode的开源代码,进行了大量修改,最终需要对源码进行编译打包才能使用(一个新的VSCode),目前只打包了mac的arm64版本来供自己试用。

另外,由于VSCode并不是100%开源(微软的一些服务相关的逻辑是闭源的),所以github上的开源仓库只是它的部分代码,经过编译之后,发现缺失了远程连接相关的功能,其他的功能暂时没有发现缺失。

image.png

🦽 可以试用吗

目前还没有对外提供试用版的打算,想自己继续使用一些时间,持续打磨一下细节,等功能细节更完善了再对外进行推广,至于这次的软文~ 其实是希望可以引起阅读到这里的同学进行讨论,可以聊一下你对该功能的一些看法,以及一些其他的好点子~,thx~

🫡 小小的致敬

  • 致敬VSCode团队,在阅读和改造他们代码的过程中学习到了不少hin有用的代码技能,也正是因为有他们的开源,才能有我的这次折腾👍🏻
  • 致敬锤子科技罗永浩老师,这次实现思路也有借鉴当年发布的“无限屏”功能,本文的头图就是来自当年的发布会截图。

image.png


作者:木头就是我呀
来源:juejin.cn/post/7375586227984220169
收起阅读 »

安卓高版本HTTPS抓包:终极解决方案

虽然市面上有好多抓包工具,但是 Android 高版本都需要安装抓包工具的证书到系统目录,才能抓 https 协议的包。本文就以 Charles这个抓包工具来介绍,如何安装证书到 Android 的系统目录,实现 https 抓包。 修改证书名称 启动 Cha...
继续阅读 »

虽然市面上有好多抓包工具,但是 Android 高版本都需要安装抓包工具的证书到系统目录,才能抓 https 协议的包。本文就以 Charles这个抓包工具来介绍,如何安装证书到 Android 的系统目录,实现 https 抓包。


修改证书名称


启动 Charles,通过菜单栏中的 Help → SSL Proxying → Save Charles Root Certificate… 将 Charles 的证书导出。
使用 OpenSSL 查看证书在 Android 系统中对应的文件名,并重命名证书文件


openssl x509 -subject_hash_old -in charles-ssl-proxying-certificate.pem | head -n 1  #cdfb61bc
mv charles-ssl-proxying-certificate.pem cdfb61bc.0

将证书安装到系统证书目录下


使用 adb push 命令将我们的证书文件放到 SD 卡中


adb push cdfb61bc.0 /sdcard/Download

使用 adb 连接手机并切换到 root 用户


adb shell
su

将证书文件移动到 /system/etc/security/cacerts 目录下,由于 /system 默认是只读的,所以要先重新挂载为其添加写入权限


cat /proc/mounts  #查看挂载信息,这里我的 /system 是直接挂载到 / 的

mount -o rw,remount /
mv /sdcard/Download/cdfb61bc.0 /system/etc/security/cacerts
chmod 644 /system/etc/security/cacerts/cdfb61bc.0 #设置文件权限

如果👆的步骤你都能成功,就不用继续往下看了。


终极解决方案


我用我手上的手机都试了一下,用上面的方式安装正式,发现不能成功,一直提示 Read-only file system,但是HttpToolkit这个软件确可以通过 Android Device Via ADB来抓 https 的包。
它是怎么实现的呢?
这下又开始了漫长的谷歌之旅,最后在他们官网找到一篇文章,详细讲述了 通过有root权限的adb 来写入系统证书的神奇方案。



  1. 通过 ADB 将 HTTP Toolkit CA 证书推送到设备上。

  2. 从 /system/etc/security/cacerts/ 中复制所有系统证书到临时目录。

  3. 在 /system/etc/security/cacerts/ 上面挂载一个 tmpfs 内存文件系统。这实际上将一个可写的全新空文件系统放在了 /system 的一小部分上面。 将复制的系统证书移回到该挂载点。

  4. 将 HTTP Toolkit CA 证书也移动到该挂载点。

  5. 更新临时挂载点中所有文件的权限为 644,并将系统文件的 SELinux 标签设置为 system_file,以使其看起来像是合法的 Android 系统文件。


关键点就是挂载一个 内存文件系统, 太有才了。
具体命令如下


# 创建一个独立的临时目录,用于存储当前的证书
# 如果不这样做,在我们添加挂载后将无法再读取到当前的证书。
mkdir -m 700 /data/local/tmp/htk-ca-copy
# 复制现有的证书到临时目录
cp /system/etc/security/cacerts/* /data/local/tmp/htk-ca-copy/
# 在系统证书文件夹之上创建内存挂载点
mount -t tmpfs tmpfs /system/etc/security/cacerts
# 将之前复制的证书移回内存挂载点中,确保继续信任这些证书
mv /data/local/tmp/htk-ca-copy/* /system/etc/security/cacerts/
# 将新的证书复制进去,以便我们也信任该证书
cp /data
/local/tmp/c88f7ed0.0 /system/etc/security/cacerts/
# 更新权限和SELinux上下文标签,确保一切都和之前一样可读
chown root:root /system/etc/security/cacerts/*
chmod 644 /system/etc/security/cacerts/*
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*
# 删除临时证书目录
rm -r /data/local/tmp/htk-ca-copy

注意:由于是内存文件系统,所以重启手机后就失效了。可以将以上命令写成 shell 脚本,需要抓包的时候执行下就可以了


作者:平行绳
来源:juejin.cn/post/7360242772303577125
收起阅读 »

接口幂等和防抖还在傻傻分不清楚。。。

最近想重温下接口幂等的相关知识,正所谓温故而知新,太久不CV的东西要是哪天CV起来都生疏了,哈哈哈 先从字面意思来温习下吧,幂等的官方概念来源于数学上幂等概念的衍生,如幂等函数,即使用相同的参数重复执行,可以得到相同的结果的函数,翻译成IT行业专业术语就是一个...
继续阅读 »

最近想重温下接口幂等的相关知识,正所谓温故而知新,太久不CV的东西要是哪天CV起来都生疏了,哈哈哈


先从字面意思来温习下吧,幂等的官方概念来源于数学上幂等概念的衍生,如幂等函数,即使用相同的参数重复执行,可以得到相同的结果的函数,翻译成IT行业专业术语就是一个接口使用相同的入参,无论执行多少次,最后得到的结果且保存的数据和执行一次是完全一样的,所以,基于这个概念,分析我们的CRUD,首先,查询,它可以说是幂等的,但是如果更精细的说,它也可能不是幂等的,基于数据库数据不变的情况下,查询接口是幂等的,如果是变化的话那可能上一秒你查出来的数据,下一秒它就被人修改了也不是不可能,所以,基于这一点它不符合幂等概念


接下来是删除接口,它和查询一样,也是天然幂等的,但是如果你的接口提供范围删除,那么就破坏了幂等性原则,理论上这个删除接口就不应该存在,那如果是产品经理非要那也是可以存在滴,技术永远都是为业务服务的嘛


修改接口也是同样的道理,理论上都必须是幂等的,如果不是,那就要考虑接口幂等性了,比如你的修改积分接口里写修改积分,每次都使用i++这种操作,那么它就破坏了幂等原则,有一个好方法就是基于用户唯一性标识把积分变动通过一张表记录下来,最后统计这张表的积分数值,这里也就涉及到新增接口的知识点,其实到这里,我们会发现,所有的接口理论上都可以是幂等的,但是总是这个那个的原因导致不幂等,所以,总结起来就是,如果你的系统需求需要接口幂等,那么就实现它,现在让我们进入正题吧


刚开始温习幂等知识的时候,我百度了很多别人写的文章,发现另一个概念,叫防抖,防止用户重复点击的意思,有意思的是有些文章竟然认为防抖就是幂等,他们解决接口幂等的思路是每次调用需要实现幂等接口时,前端都需要调用后端的颁布唯一token的接口,后端颁布token后保存在缓存中,然后前端带着这个token的请求去请求我们的幂等接口,验证携带的token来实现接口幂等,按照这个思路每次请求的token都不一样,如何保证幂等中相同参数的条件呢,这显然和幂等南辕北辙了,这显然就是接口防抖或者接口加锁的思路嘛


还有一种是可以实现接口幂等性的思路,这里也可以分享一下,和上面的思路差不多,也是每次请求幂等接口的时候,先调用颁发唯一token的接口,唯一不同的是它颁发的token是基于入参生成的哈希值,后面的业务逻辑就是后端基于这个哈希值去校验,如果缓存中已经存在了,说明这个入参已经请求过了,那么直接拒绝请求并返回成功,这样,就从表面上实现了接口幂等性,因为执行100次我只执行一次,剩余的99次我都拒绝,并直接返回成功,这样,我的接口就从表面上幂等了,但是这个方案有一个很大的问题就是每次调用都需要浪费一部分资源在请求颁发token上,这对需要大量的幂等接口的系统来说就是一个累赘,所以,接下来,我们基于这个思路实现一个不需要二次调用的实现接口幂等的方法。


我的思路是这样的,业务上有些接口是实现防抖功能,有些是实现幂等功能,其实这两个功能在业务上确实是相差不大,所以,我的思路是定义一个注解,包含防抖和幂等的功能,首先基于幂等如果要是把所有入参都哈希化作为唯一标识的话有点费劲,可以基于业务上的一些唯一标识来做,如用户id或者code,还需要一个开关,用于决定是否保存这个唯一标识,还要一个时间,保存多久,还有保存时间的单位,最后,还有一个返回提醒,就是拒绝之后的友好提示,基于这些差不多了,如果你的接口功能只需要实现防抖,那么你可以设置时间段内过期,这样就实现了防抖,如果你的接口没有唯一标识,那么可以基于路由来做防抖,这个不要忘了设置过期时间,不然你的接口就永远是拒绝了,好了,思路有了,接下来就是实操了,话不多数,上代码


@Retention(RetentionPolicy.RUNTIME)
//注解用于方法
@Target({ElementType.TYPE, ElementType.METHOD})
//注解包含在JavaDoc中
@Documented
public @interface Idempotent {

/**
* 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
*
* @return Spring-EL expression
*/

String key() default "";

/**
* 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
*
* @return expireTime
*/

int expireTime() default 100;

/**
* 时间单位 默认:s
*
* @return TimeUnit
*/

TimeUnit timeUnit() default TimeUnit.SECONDS;

/**
* 提示信息,可自定义
*
* @return String
*/

String info() default "重复请求,请稍后重试";

/**
* 是否在业务完成后删除key true:删除 false:不删除
*
* @return boolean
*/

boolean delKey() default false;


基本和我们上面的思路一样,唯一key,有效期,有效期时间单位,提示信息,是否删除,注解有了,那么我们就要基于注解写我们的逻辑了,这里我们需要用到aop,引用注解应该都知道吧,这里我们直接上代码了


@Aspect
@Slf4j
public class IdempotentAspect {
@Resource
private RedisUtil redisUtil;

private static final SpelExpressionParser PARSER = new SpelExpressionParser();

private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

/**
* 线程私有map
*/

private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);

private static final String KEY = "key";

private static final String DEL_KEY = "delKey";

// 以自定义 @Idempotent 注解为切点
@Pointcut("@annotation(com.liuhui.demo_core.spring.Idempotent)")
public void idempotent() {
}

@Before("idempotent()")
public void before(JoinPoint joinPoint) throws Throwable {
//获取到当前请求的属性,进而得到HttpServletRequest对象,以便后续获取请求URL和参数信息。
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//从JoinPoint中获取方法签名,并确认该方法是否被@Idempotent注解标记。如果是,则继续执行幂等性检查逻辑;如果不是,则直接返回,不进行幂等处理。。
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (!method.isAnnotationPresent(Idempotent.class)) {
return;
}
Idempotent idempotent = method.getAnnotation(Idempotent.class);
String key;
// 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分;如果提供了key规则,则利用keyResolver根据提供的规则和切点信息生成键
if (!StringUtils.hasLength(idempotent.key())) {
String url = request.getRequestURL().toString();
String argString = Arrays.asList(joinPoint.getArgs()).toString();
key = url + argString;
} else {
// 使用jstl 规则区分
key = resolver(idempotent, joinPoint);
}
//从注解中读取并设置幂等操作的过期时间、描述信息、时间单位以及是否删除键的标志。
long expireTime = idempotent.expireTime();
String info = idempotent.info();
TimeUnit timeUnit = idempotent.timeUnit();
boolean delKey = idempotent.delKey();
String value = LocalDateTime.now().toString().replace("T", " ");
Object valueResult = redisUtil.get(key);
synchronized (this) {
if (null == valueResult) {
redisUtil.set(key, value, expireTime, timeUnit);
} else {
throw new IdempotentException(info);
}
}
Map<String, Object> map = THREAD_CACHE.get();
map.put(KEY, key);
map.put(DEL_KEY, delKey);
}


/**
* 从注解的方法的参数中解析出用于幂等性处理的键值(key)
*
* @param idempotent
* @param point
* @return
*/

private String resolver(Idempotent idempotent, JoinPoint point) {
//获取被拦截方法的所有参数
Object[] arguments = point.getArgs();
//从字节码的局部变量表中解析出参数名称
String[] params = DISCOVERER.getParameterNames(getMethod(point));
//SpEL表达式执行的上下文环境,用于存放变量
StandardEvaluationContext context = new StandardEvaluationContext();
//遍历方法参数名和对应的参数值,将它们一一绑定到StandardEvaluationContext中。
//这样SpEL表达式就可以引用这些参数值
if (params != null && params.length > 0) {
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], arguments[len]);
}
}
//使用SpelExpressionParser来解析Idempotent注解中的key属性,将其作为SpEL表达式字符串
Expression expression = PARSER.parseExpression(idempotent.key());
//转换结果为String类型返回
return expression.getValue(context, String.class);
}

/**
* 根据切点解析方法信息
*
* @param joinPoint 切点信息
* @return Method 原信息
*/

private Method getMethod(JoinPoint joinPoint) {
//将joinPoint.getSignature()转换为MethodSignature
//Signature是AOP中表示连接点签名的接口,而MethodSignature是它的具体实现,专门用于表示方法的签名。
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取到方法的声明。这将返回代理对象所持有的方法声明。
Method method = signature.getMethod();

//判断获取到的方法是否属于一个接口
//因为在Java中,当通过Spring AOP或其它代理方式调用接口的方法时,实际被执行的对象是一个代理对象,直接获取到的方法可能来自于接口声明而不是实现类。
if (method.getDeclaringClass().isInterface()) {
try {
//通过反射获取目标对象的实际类(joinPoint.getTarget().getClass())中同名且参数类型相同的方法
//这样做是因为代理类可能对方法进行了增强,直接调用实现类的方法可以确保获取到最准确的实现细节
method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
method.getParameterTypes());
} catch (SecurityException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
return method;
}


@After("idempotent()")
public void after() throws Throwable {
Map<String, Object> map = THREAD_CACHE.get();
if (CollectionUtils.isEmpty(map)) {
return;
}

String key = map.get(KEY).toString();
boolean delKey = (boolean) map.get(DEL_KEY);
if (delKey) {
redisUtil.delete(key);
log.info("[idempotent]:has removed key={}", key);
}
//无论是否移除了键,最后都会清空当前线程局部变量THREAD_CACHE中的数据,避免内存泄漏
THREAD_CACHE.remove();
}
}

上面redisUtil是基于RedisTemplate封装的工具类,可以直接替换哈,这里我们定义一个切入点,也就是我们定义的注解,然后在调用接口之前获取到接口的入参以及注解的参数,获取到这些之后,判断是否有唯一标识,没有就用路由,保存到reids当中,然后设置过期时间,最后需要把删除的标识放到线程私有变量THREAD_CACHE中在接口处理完之后判断是否需要删除redis当中保存的key,这里,我们的逻辑就写完了,接下来是使用了,使用这个就很简单,直接在你需要实现防抖和幂等的接口上打上我们的注解


/**
* 测试接口添加幂等校验
*
* @return
*/

@PostMapping("/redis")
@Idempotent(key = "#user.id", expireTime = 10, delKey = true, info = "重复请求,请稍后再试")
public Result<?> getRedis(@RequestBody User user) throws InterruptedException {
return Result.success(true);
}

这里key的定义方式我们使用了SpEL表达式,如果不指定这个表达式的话就会使用路由作为key了


到这里,接口幂等和防抖功能就顺利完成了,以后,别再防抖和幂等傻傻分不清楚了哈哈哈


最后,还是要送上一位名人曾说的一句话:手上没有剑和有剑不用是两回事!


作者:失乐园
来源:juejin.cn/post/7380274613185970195
收起阅读 »

探索副业的路上:赚钱的机会往往在你眼前,请不要视而不见

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 对于普通人来说,赚钱的机会往往是那些看不上、瞧不起的信息差。 我们的认知,对于看到的项目,可能天生把他归结于大项目、或者小项目。 但每一个赚钱机会,是从每一个小的机会,叠加了风口...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


对于普通人来说,赚钱的机会往往是那些看不上、瞧不起的信息差。


我们的认知,对于看到的项目,可能天生把他归结于大项目、或者小项目。


但每一个赚钱机会,是从每一个小的机会,叠加了风口、资金、团队、产品等杠杆,叠加成了一个大的赚钱机会。


所以说,赚钱的机会往往是那些我们看不上、瞧不起的信息差。


看不起就会错过


比如说 大学期间,我曾经对微商嗤之以鼻。卖假货、朋友圈刷屏,太low了!


几年之后,当年做微商的,做得好的赚到第一桶金,做得不好,也积累了项目经验、私域用户。


工作后,我觉着那些整理面经的,没什么意思,不就是罗列了知识,照着书本上的内容,那可差的太远了,有这时间我看看书不好吗?


但整理面经的,在技术平台持续输出的那些人,不但积累了第一波粉丝,在那个快速发展的时间,很多人靠公众号赚到了第一桶金。


看不上、瞧不起的时候,实际上是封闭了自己。


我现在也在写一些技术文章、面经,恰恰是做了自己曾经看不起的事情了。


其实时至今日,我也会有这样的毛病,互联网从来不缺少赚钱的机会。


身边有通过闲鱼赚了10w+的大佬,也有做AI公众号文章,月入过千的自媒体写手。


即使接触了这些人,我的内心也总会有些怀疑的态度,闲鱼的货源怎么办,时刻需要回复客户信息,消耗经历太大怎么办。AI写文章起号难,输出的内容自己又觉着没价值,没有什么前途怎么办。


怎么做


保持开放


保持头脑开放,让信息流入进来


持有怀疑的心态没问题,凡事有利有弊,再小的项目,也有能赚到大钱的,和赚不到钱的人。


但不能天然的排斥这些信息,很多人对于看到的赚钱信息,内心总会说出一个声音:“赚钱的事情谁会真的分享给你啊,肯定是骗人的”。


你是不也经常会有这样的想法?


其实是的,现在非常流行知识付费,市面上确实存在着通过几个赚钱项目,吸引你来买课学习、割韭菜的课程。


但同样的,一定也有优质的信息,来帮助你打平信息差、快速上手,这时候我们需要提升的是鉴别能力,这个我们有空再聊。


先干起来再说


有了充足的信息、案例,如何去做呢?


找到自己热爱或者擅长的事情。


我这里用了或,而不是且,是我最近刚刚想清楚的一个道理。


每个人都有热爱、擅长的事情,如果有一个赚钱机会,既符合你热爱,也很擅长,那是最好的。但这样的机会可遇不可求。


我到今天都还希望找到一个热爱且擅长的事情,但是我发现好难。


不到半年时间,我写了28篇文章,这是第29篇。我还是蛮喜欢写东西的,既能说说心里话,也能积累写作这个最基础的能力。


但我真的不擅长这件事情,可能在几年内,我都不敢说我自己擅长写作,因为市面上有太多输出能力很强的大佬,有太多写作课程了。


市面上真的缺少我一个内容创作者吗?那你说,我为什么还要去写,还要持续输出呢?


去尝试一下,没什么成本,先干起来再说。


保持专注


俗话说:“师傅领进门,修行在个人。”


保持专注,就是找到一个方向,在这个领域里深耕。没有在一个方向的专注,是很难赚到钱的。


写作方向的大佬,比如粥大,写了上百篇10w+阅读的文章。很多有了自己写作课程的知识型博主,都已经日更公众号几百天、上千天。



每个人的擅长都不是找到的,而是自己努力去做到的。



说在最后


在今天,我也决定列出我今年的目标,完成一百篇原创文章,持续分享我在探索个人IP道路上的思考、经验,欢迎你关注、围观。




作者:东东拿铁
来源:juejin.cn/post/7358639642412122166
收起阅读 »

面试问空窗期,HR到底想听什么?

职场中的空窗期,无论是新人还是老手,都可能会遇到,而且因人而异。因此,离职后,建议空窗期不宜过长,万一真的有空窗期,在面试中该如何回答呢?今天我们来聊一聊。 空窗期产生的原因 职场空窗期产生的原因多种多样,这里总结出主要的 2个因素: 个人因素 比如,由于工作...
继续阅读 »

职场中的空窗期,无论是新人还是老手,都可能会遇到,而且因人而异。因此,离职后,建议空窗期不宜过长,万一真的有空窗期,在面试中该如何回答呢?今天我们来聊一聊。


空窗期产生的原因


职场空窗期产生的原因多种多样,这里总结出主要的 2个因素:


个人因素


比如,由于工作压力大,想给自己一段时间休息和调整或者薪资待遇不满意等多方面因素,导致冲动“裸辞”;


又比如,很多大学生,在校招期间没有匹配到合适的工作,踏上社会后又眼高手低,导致企业和个人都不满意,也经常会出现空窗期的问题;


再比如,因为家庭原因不得不离开职场一段时间;


外在因素


比如,经济不景气、公司倒闭、行业调整、裁员等无法改变的外在因素,然后无法快速地衔接下一份工作。


HR为什么很关注空窗期?


HR关注空档期,主要有以下几个原因:


担忧求职者稳定性:HR担心求职者流动性强,可能不稳定。长期空窗期可能暗示个人适应能力不足或求职者可能频繁离职。 担忧求职者能力匹配度:HR关注求职者是否有能力与岗位匹配,长期空窗期可能暗示了求职者的能力不足,以至于长时间找不到工作。 担忧个人生活因素:HR可能担心求职者个人生活琐事过多,影响工作表现。长期空窗期可能反映个人生活稳定性问题。


如何回答空窗期?


求职者在面对空窗期时,应当诚实面对,避免隐瞒或虚假解释。对于空窗期,可用积极的态度和合理的解释来回答面试官的提问,下面根据空窗期的时间长短给出一些建议:


3个月以内


一般三个月以内的空窗期,问题不是特别大,可以先说明离职原因,比如公司组织架构调整,部门全部被优化,借此机会给自己一段时间休息和调整,现在已经调整好状态,可以更好的投入下一份工作,一般这样回答,HR也不会太纠结。


3~6个月


3~6个月时间还是有点长,这里给出给出 2个比较通用的原因:


在先前的工作中感觉到自己某方面能力的不足,所以这段时间去参加培训、给自己充电,提升一下自己某一方面的技能,为了更好的开展下一份工作;


家人生病了,急需自己的照顾,现在家人康复了,家里的事情也已经安排妥当,另外,在此期间自己也在不断地充电,我可以踏踏实实地上班;


6个月以上


6个月以上,这个时间有点长,因此,最重要的是要说明自己并没有脱离职场,比如做了一些和工作性质相关的兼职工作,空窗期时间长也可以围绕生孩子或者生病去说,因为这种理由似乎让人无法拒绝,毕竟每个家庭总有一些属于自己的小插曲。 另外,如果还可以围绕着自己在和职业相关的领域创业,现在创业失败了,经历过之后,想踏踏实实地回归职场干活。


总结


离开职场容易,回归职场难,因此,如果没有什么特殊原因,不建议离职后的空窗期太长。另外,空窗期长并不代表求职者的能力差,不管是什么原因导致空窗期我们都应该给面试官展示积极的一面,作为面试官,我们也应该从打工人的无奈出发,只要不是太离谱,就不要太计较。




作者:猿java
来源:juejin.cn/post/7380268230797901864
收起阅读 »

Echarts中国地图下钻,支持下钻到县(vue3)

web
引言 Echarts 大家都不陌生吧,时常被用于绘制各种图表,也作为大屏可视化的常驻用户,这里就不多说了,今天主要是讲述一下 Echarts 的地图下钻,支持下钻到县、返回上一级。 准备工作 地图JSON数据 DataV.GeoAtlas地理小工具系列 (al...
继续阅读 »

引言


Echarts 大家都不陌生吧,时常被用于绘制各种图表,也作为大屏可视化的常驻用户,这里就不多说了,今天主要是讲述一下 Echarts 的地图下钻,支持下钻到县、返回上一级。


准备工作


地图JSON数据


DataV.GeoAtlas地理小工具系列 (aliyun.com) 支持在线调用API和下载json资源(我这里是调用的API)



如果地图json API请求报错403,可参考这个解决办法 :地图请求阿里的geojson数据时,返回403Forbidden解决方案



技术栈



  • vue: 3.3.7

  • vue-echarts: 6.6.1 (直接使用 Echarts 也是一样的,这个只是对 Echarts 的组件封装)

  • vite: 4.5.0


地图效果


0mdtt-ameuf.gif


项目预览地址:UnusualAdmin


项目代码地址:UnusualAdmin


实现


template


这里只需要一个 Echarts 节点和一个按钮就行了


<template>
<div :style="`height: ${calcHeight('main')};`" class="wh-full pos-relative">
<v-chart :option="mapOption" :autoresize="true" @click="handleClick" />
<n-button v-show="isShowBack" class="pos-absolute top-10 left-10" @click="goBack">返回n-button>
div>
template>

获取mapJson


// 使用线上API
const getMapJson = async (mapName: string) => {
const url = `https://geo.datav.aliyun.com/areas_v3/bound/${mapName}.json`
const mapJson = await fetch(url).then(res => res.json())
return mapJson
}

// 使用本地资源
const getMapJson = async (mapName: string) => {
const url = `@/assets/mapJson/${mapName}.json`
const mapJson = await import(/* @vite-ignore */ url)
return mapJson
}


第二种方法(使用本地资源)存在问题:这个方法后续发现,vite打包不会把json文件打包到dist,线上会报错,目前没找到可靠的解决办法(如果放到public文件夹下会打包进去),故舍弃。


如果大家有什么解决这个问题的好办法,请在评论区留言,博主会一一去尝试的🙏🙏🙏



更新地图配置options


const setOptions = (mapName: string, mapData: any) => {
return {
// 鼠标悬浮提示
tooltip: {
show: true,
formatter: function (params: any) {
// 根据需要进行数据处理或格式化操作
if (params && params.data) {
const { adcode, name, data } = params.data;
// 返回自定义的tooltip内容
return `adcode: ${adcode}
name: ${name}
data: ${data}`
;
}
},
},
// 左下角的数据颜色条
visualMap: {
show: true,
min: 0,
max: 100,
left: 'left',
top: 'bottom',
text: ['高', '低'], // 文本,默认为数值文本
calculable: true,
seriesIndex: [0],
inRange: {
color: ['#00467F', '#A5CC82'] // 蓝绿
}
},
// geo地图
geo: {
map: mapName,
roam: true,
select: false,
// 图形上的文本标签,可用于说明图形的一些数据信息,比如值,名称等。
selectedMode: 'single',
label: {
show: true
},
emphasis: {
itemStyle: {
areaColor: '#389BB7',
borderColor: '#389BB7',
borderWidth: 0
},
label: {
fontSize: 14,
},
}
},
series: [
// 地图数据
{
type: 'map',
map: mapName,
roam: true,
geoIndex: 0,
select: false,
data: mapData
},
// 散点
{
name: '散点',
type: 'scatter',
coordinateSystem: 'geo',
data: mapData,
itemStyle: {
color: '#05C3F9'
}
},
// 气泡点
{
name: '点',
type: 'scatter',
coordinateSystem: 'geo',
symbol: 'pin', //气泡
symbolSize: function (val: any) {
if (val) {
return val[2] / 4 + 20;
}
},
label: {
show: true,
formatter: function (params: any) {
return params.data.data || 0;
},
color: '#fff',
fontSize: 9,
},
itemStyle: {
color: '#F62157', //标志颜色
},
zlevel: 6,
data: mapData,
},
// 地图标点
{
name: 'Top 5',
type: 'effectScatter',
coordinateSystem: 'geo',
data: mapData.map((item: { data: number }) => {
if (item.data > 60) return item
}),
symbolSize: 15,
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke'
},
label: {
formatter: '{b}',
position: 'right',
show: true
},
itemStyle: {
color: 'yellow',
shadowBlur: 10,
shadowColor: 'yellow'
},
zlevel: 1
},
]
}
}

渲染地图


const renderMapEcharts = async (mapName: string) => {
const mapJson = await getMapJson(mapName)
registerMap(mapName, mapJson); // 注册地图
// 为地图生成一些随机数据
const mapdata = mapJson.features.map((item: { properties: any }) => {
const data = (Math.random() * 80 + 20).toFixed(0) // 20-80随机数
const tempValue = item.properties.center ? [...item.properties.center, data] : item.properties.center
return {
name: item.properties.name,
value: tempValue, // 中心点经纬度
adcode: item.properties.adcode, // 区域编码
level: item.properties.level, // 层级
data // 模拟数据
}
});
// 更新地图options
mapOption.value = setOptions(mapName, mapdata)
}

实现地图点击下钻


// 点击下砖
const mapList = ref<string[]>([]) // 记录地图
const handleClick = (param: any) => {
// 只有点击地图才触发
if (param.seriesType !== 'map') return
const { adcode, level } = param.data
const mapName = level === 'district' ? adcode : adcode + '_full'
// 防止最后一个层级被重复点击,返回上一级出错
if (mapList.value[mapList.value.length - 1] === mapName) {
return notification.warning({ content: '已经是最下层了', duration: 1000 })
}
// 每次下转都记录下地图的name,在返回的时候使用
mapList.value.push(mapName)
renderMapEcharts(mapName)
}

返回上一级实现


// 点击返回上一级地图
const goBack = () => {
const mapName = mapList.value[mapList.value.length - 2] || '100000_full'
mapList.value.pop()
renderMapEcharts(mapName)
}

全部代码

<template>
<div :style="`height: ${calcHeight('main')};`" class="wh-full pos-relative">
<v-chart :option="mapOption" :autoresize="true" @click="handleClick" />
<n-button v-show="isShowBack" class="pos-absolute top-10 left-10" @click="goBack">返回</n-button>
</div>
</template>

<script setup lang="ts" name="EchartsMap">
import { use, registerMap } from 'echarts/core'
import VChart from 'vue-echarts'
import { CanvasRenderer } from 'echarts/renderers'
import { MapChart, ScatterChart, EffectScatterChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, VisualMapComponent } from 'echarts/components'
import { calcHeight } from '@/utils/help';

use([
CanvasRenderer,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
VisualMapComponent,
MapChart,
ScatterChart,
EffectScatterChart
])

const notification = useNotification()
const mapOption = ref()
const mapList = ref<string[]>([]) // 记录地图
const isShowBack = computed(() => {
return mapList.value.length !== 0
})

const getMapJson = async (mapName: string) => {
const url = `https://geo.datav.aliyun.com/areas_v3/bound/${mapName}.json`
const mapJson = await fetch(url).then(res => res.json())
return mapJson
}

const setOptions = (mapName: string, mapData: any) => {
return {
tooltip: {
show: true,
formatter: function (params: any) {
// 根据需要进行数据处理或格式化操作
if (params && params.data) {
const { adcode, name, data } = params.data;
// 返回自定义的tooltip内容
return `adcode: ${adcode}<br>name: ${name}<br>data: ${data}`;
}
},
},
visualMap: {
show: true,
min: 0,
max: 100,
left: 'left',
top: 'bottom',
text: ['高', '低'], // 文本,默认为数值文本
calculable: true,
seriesIndex: [0],
inRange: {
color: ['#00467F', '#A5CC82'] // 蓝绿
}
},
geo: {
map: mapName,
roam: true,
select: false,
// zoom: 1.6,
// layoutCenter: ['45%', '70%'],
// layoutSize: 750,
// 图形上的文本标签,可用于说明图形的一些数据信息,比如值,名称等。
selectedMode: 'single',
label: {
show: true
},
emphasis: {
itemStyle: {
areaColor: '#389BB7',
borderColor: '#389BB7',
borderWidth: 0
},
label: {
fontSize: 14,
},
}
},
series: [
// 数据
{
type: 'map',
map: mapName,
roam: true,
geoIndex: 0,
select: false,
data: mapData
},
{
name: '散点',
type: 'scatter',
coordinateSystem: 'geo',
data: mapData,
itemStyle: {
color: '#05C3F9'
}
},
{
name: '点',
type: 'scatter',
coordinateSystem: 'geo',
symbol: 'pin', //气泡
symbolSize: function (val: any) {
if (val) {
return val[2] / 4 + 20;
}
},
label: {
show: true,
formatter: function (params: any) {
return params.data.data || 0;
},
color: '#fff',
fontSize: 9,
},
itemStyle: {
color: '#F62157', //标志颜色
},
zlevel: 6,
data: mapData,
},
{
name: 'Top 5',
type: 'effectScatter',
coordinateSystem: 'geo',
data: mapData.map((item: { data: number }) => {
if (item.data > 60) return item
}),
symbolSize: 15,
showEffectOn: 'render',
rippleEffect: {
brushType: 'stroke'
},
label: {
formatter: '{b}',
position: 'right',
show: true
},
itemStyle: {
color: 'yellow',
shadowBlur: 10,
shadowColor: 'yellow'
},
zlevel: 1
},
]
}
}

const renderMapEcharts = async (mapName: string) => {
const mapJson = await getMapJson(mapName)
registerMap(mapName, mapJson);
const mapdata = mapJson.features.map((item: { properties: any }) => {
const data = (Math.random() * 80 + 20).toFixed(0) // 20-80随机数
const tempValue = item.properties.center ? [...item.properties.center, data] : item.properties.center
return {
name: item.properties.name,
value: tempValue, // 中心点经纬度
adcode: item.properties.adcode, // 区域编码
level: item.properties.level, // 层级
data // 模拟数据
}
});
mapOption.value = setOptions(mapName, mapdata)
}

renderMapEcharts('100000_full') // 初始化绘制中国地图

// 点击下砖
const handleClick = (param: any) => {
// 只有点击地图才触发
if (param.seriesType !== 'map') return
const { adcode, level } = param.data
const mapName = level === 'district' ? adcode : adcode + '_full'
// 防止最后一个层级被重复点击,返回上一级出错
if (mapList.value[mapList.value.length - 1] === mapName) {
return notification.warning({ content: '已经是最下层了', duration: 1000 })
}
mapList.value.push(mapName)
renderMapEcharts(mapName)
}

// 点击返回上一级地图
const goBack = () => {
const mapName = mapList.value[mapList.value.length - 2] || '100000_full'
mapList.value.pop()
renderMapEcharts(mapName)
}
</script>



作者:树深遇鹿
来源:juejin.cn/post/7371641968600383540
收起阅读 »

阿里也出手了!Spring CloudAlibaba AI问世了

写在前面 在之前的文章中我们有介绍过SpringAI这个项目。SpringAI 是Spring 官方社区项目,旨在简化 Java AI 应用程序开发, 让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。 而SpringAI 主要面向的...
继续阅读 »

写在前面


在之前的文章中我们有介绍过SpringAI这个项目。SpringAI 是Spring 官方社区项目,旨在简化 Java AI 应用程序开发,


让 Java 开发者像使用 Spring 开发普通应用一样开发 AI 应用。


SpringAI 主要面向的是国外的各种大模型接入,对于国内开发者可能不太友好。


于是乎,Spring Cloud Alibaba AI便问世了,Spring Cloud Alibaba AI以 Spring AI 为基础,并在此基础上提供阿里云通义系列大模型全面适配,


让用户在 5 分钟内开发基于通义大模型的 Java AI 应用。


一、Spring AI 简介


可能有些小伙伴已经忘记了SpringAI 是啥?我们这儿再来简单回顾一下。


Spring AI是一个面向AI工程的应用框架。其目标是将可移植性和模块化设计等设计原则应用于AI领域的Spring生态系统,


并将POJO作为应用程序的构建块推广到AI领域。


转换为人话来说就是:Spring出了一个AI框架,帮助我们快速调用AI,从而实现各种功能场景。


二、Spring Cloud Alibaba AI 简介


Spring Cloud Alibaba AISpring AI 为基础,并在此基础上,基于 Spring AI 0.8.1 版本 API 完成通义系列大模型的接入


实现阿里云通义系列大模型全面适配。


在当前最新版本中,Spring Cloud Alibaba AI 主要完成了几种常见生成式模型的适配,包括对话、文生图、文生语音等,


开发者可以使用 Spring Cloud Alibaba AI 开发基于通义的聊天、图片或语音生成 AI 应用,


框架还提供 OutParserPrompt TemplateStuff 等实用能力。


三、第一个Spring AI应用开发


① 新建maven 项目


注: 在创建项目的时候,jdk版本必须选择17+


新建maven项目


② 添加依赖


<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-alibaba-dependencies</artifactId>
   <version>2023.0.1.0</version>
   <type>pom</type>
   <scope>import</scope>
</dependency>

<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-ai</artifactId>
   <version>2023.0.1.0</version>
</dependency>

注: 这里我们需要配置镜像源,否则是没法下载依赖的。会报如下错误



spring-ai: 0.8.1 dependency not found



<repositories>
   <repository>
       <id>spring-milestones</id>
       <name>Spring Milestones</name>
       <url>https://repo.spring.io/milestone</url>
       <snapshots>
           <enabled>false</enabled>
       </snapshots>
   </repository>
</repositories>

③ 在 application.yml 配置文件中添加api-key


spring:
cloud:
  ai:
    tongyi:
      api-key: 你自己申请的api-key

小伙伴如果不知道在哪申请,我把申请连接也放这儿了


dashscope.console.aliyun.com/apiKey


操作步骤:help.aliyun.com/zh/dashscop…


④ 新建TongYiController 类,代码如下


@RestController
@RequestMapping("/ai")
@CrossOrigin
@Slf4j
public class TongYiController {

   @Autowired
   @Qualifier("tongYiSimpleServiceImpl")
   private TongYiService tongYiSimpleService;

   @GetMapping("/example")
   public String completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {

       return tongYiSimpleService.completion(message);
  }
   
}

⑤ 新建TongYiService 接口,代码如下


public interface TongYiService {
   String completion(String message);

}

⑥ 新建TongYiSimpleServiceImpl 实现类,代码如下


@Service
@Slf4j
public  class TongYiSimpleServiceImpl  implements TongYiService {

   private final ChatClient chatClient;

   @Autowired
   public TongYiSimpleServiceImpl(ChatClient chatClient, StreamingChatClient streamingChatClient) {
       this.chatClient = chatClient;
  }

   @Override
   public String completion(String message) {
       Prompt prompt = new Prompt(new UserMessage(message));

       return chatClient.call(prompt).getResult().getOutput().getContent();
  }


}

到这儿我们一个简单的AI应用已经开发完成了,最终项目结构如下


项目结构


四、运行AI应用


启动服务,我们只需要在浏览器中输入:http://localhost:8080/ai/example 即可与AI交互。


① 不带message参数,则message=Tell me a joke,应用随机返回一个笑话


随机讲一个笑话1


② 我们在浏览器中输入:http://localhost:8080/ai/example?message=对话内容


message带入


五、前端页面对话模式


我们只需要在resources/static 路径下添加一个index.html前端页面,即可拥有根据美观的交互体验。


index.html代码官方github仓库中已给出样例,由于代码比较长,这里就不贴代码了


github.com/alibaba/spr…


添加完静态页面之后,我们浏览器中输入:http://localhost:8080/index.html 就可以得到一个美观的交互界面


美观交互界面


接下来,我们来实际体验一下


UI交互


六、其他模型


上面章节中我们只简单体验了对话模型,阿里还有很多其他模型。由于篇幅原因这里就不一一带大家一起体验了。


应用场景:


应用场景


各个模型概述:


模型概述


七、怎么样快速接入大模型


各种应用场景阿里官方GitHub都给出了接入例子


github.com/alibaba/spr…


官方样例


感兴趣的小伙伴可以自己到上面github 仓库看代码研究


本期内容到这儿就结束了,★,°:.☆( ̄▽ ̄)/$: .°★ 。 希望对您有所帮助


我们下期再见 ヾ(•ω•`)o (●'◡'●)


作者:xiezhr
来源:juejin.cn/post/7380771735681941523
收起阅读 »

threejs渲染高级感可视化风力发电车模型

web
本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图, 视频讲解及源码见文末 技术栈 three.js 0.165.0 vite 4.3.2 nodej...
继续阅读 »

本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图,



视频讲解及源码见文末



技术栈



  • three.js 0.165.0

  • vite 4.3.2

  • nodejs v18.19.0


效果图


一镜到底动画


一镜到底 (1).gif


切割动画


切割动画.gif


线稿动画


线稿动画.gif


外壳透明度动画


外壳透明度动画.gif


展开齿轮动画


展开齿轮动画.gif


发光线条动画


发光线条.gif


代码及功能介绍


着色器


文中用到一个着色器,就是给模型增加光感的动态光影


创建顶点着色器 vertexShader:


varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

创建片元着色器 vertexShader:


varying vec2 vUv;
uniform vec2 u_center; // 添加这一行

void main() {
// 泡泡颜色
vec3 bubbleColor = vec3(0.9, 0.9, 0.9); // 乳白色
// 泡泡中心位置
vec2 center = u_center;
// 计算当前像素到泡泡中心的距离
float distanceToCenter = distance(vUv, center);
// 计算透明度,可以根据实际需要调整
float alpha = smoothstep(0.1, 0.0, distanceToCenter);

gl_FragColor = vec4(bubbleColor, alpha);

创建着色器材质 bubbleMaterial


export const bubbleMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true, // 开启透明
depthTest: true, // 开启深度测试
depthWrite: false, // 不写入深度缓冲
uniforms: {
u_center: { value: new THREE.Vector2(0.3, 0.3) } // 添加这一行
},
});


从代码中可以看到 uniform声明了一个变量u_center,目的是为了在render方法中动态修改中心位置,从而实现动态光效的效果,


具体引用 render 方法中


 // 更新中心位置(例如,每一帧都改变)  
let t = performance.now() * 0.001;
bubbleMaterial.uniforms.u_center.value.x = Math.sin(t) * 0.5 + 0.5; // x 位置基于时间变化
bubbleMaterial.uniforms.u_center.value.y = Math.cos(t) * 0.5 + 0.5; // y 位置基于时间变化

官网案例 # Uniform,详细介绍了uniform的使用方法,支持通过变量对着色器材质中的属性进行改变


光影着色器.gif


从模型上可能看不出什么,下面的图是在一个圆球上加的这个效果


光影着色器-球体.gif


着色器中有几个参数可以自定义也可以自己修改, float alpha = smoothstep(0.6, 0.0, distanceToCenter);中的smoothstep 是一个常用的函数,用于在两个值之间进行平滑插值。具体来说,smoothstep(edge0, edge1, x) 函数会计算 x 在 edge0 和 edge1 之间的平滑过渡值。当 x 小于 edge0 时,返回值为 0;当 x 大于 edge1 时,返回值为 1;而当 x 在 edge0 和 edge1 之间时,它返回一个在 0 和 1 之间的平滑过渡值。


切割动画


切割动画使用的是数学库平面THREE.Plane和属性 constant,通过修改constant值即可实现动画,从normal法向量起至constant的距离为可展示内容。



从原点到平面的有符号距离。 默认值为 0.



constant取模型的box3包围盒的min值,至max值做补间动画,以下是代码示意


const wind = windGltf.scene
const boxInfo = wind.userData.box3Info;

const max = boxInfo.worldPosition.z + boxInfo.max.z
const min = boxInfo.worldPosition.z + boxInfo.min.z

let tween = new TWEEN.Tween({ d: min - 0.2 })
.to({ d: max + 0.1 }, 1000 * 2)
.start()
.onUpdate(({ d }) => {
clippingPlane.constant = d
})

详看切割效果图


切割动画.gif


图中添加了切割线的辅助线,可以通过右侧的操作面板显示或隐藏。


模型材质需要注意的问题


由于齿轮在风车的内容部,并且风车模型开启了transparent=true,那么计算透明度深度就会出现问题,首先要设置 depthWrite = true,开启深度缓存区,renderOrder = -1



这个值将使得scene graph(场景图)中默认的的渲染顺序被覆盖, 即使不透明对象和透明对象保持独立顺序。 渲染顺序是由低到高来排序的,默认值为0



threejs的透明材质渲染和不透明材质渲染的时候,会互相影响,而调整renderOrder顺序则可以让透明对象和不透明对象相对独立的渲染。


depthWrite对比


depthwrite对比.jpeg


renderOrder 对比


renderOrder 对比.jpeg


自定义动画贝塞尔曲线


众所周知,贝塞尔曲线通常用于调整关键帧动画,创建平滑的、曲线的运动路径。本文中使用的tweenjs就内置了众多的运动曲线easing(easingFunction?: EasingFunction): this;类型,虽然有很多内置,但是毕竟需求是无限的,接下来介绍的方法就是可以自己设置动画的贝塞尔曲线,来控制动画的执行曲线。


具体使用


// 使用示例
const controlPoints = [ { x: 0 }, { x: 0.5 }, { x: 2 }, { x: 1 }];
const cubicBezier = new CubicBezier(controlPoints[0], controlPoints[1], controlPoints[2], controlPoints[3]);

let tween = new TWEEN.Tween(edgeLineGr0up.scale)
.to(windGltf.scene.scale.clone().set(1, 1, 1), 1000 * 2)
.easing((t) => {
return cubicBezier.get(t).x
})
.start()
.onComplete(() => {
lineOpacityAction(0.3)
res({ tween })
})

在tween的easing的回调中添加一个方法,方法中调用了cubicBezier,下面就介绍一下这个方法


源码


[p0] – 起点  
[p1] – 第一个控制点
[p2] – 第二个控制点
[p3] – 终点

export class CubicBezier {
private p0: { x: number; };
private p1: { x: number; };
private p2: { x: number; };
private p3: { x: number; };

constructor(p0: { x: number; }, p1: { x: number; }, p2: { x: number; }, p3: { x: number; }) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
}

get(t: number): { x: number; } {
const p0 = this.p0;
const p1 = this.p1;
const p2 = this.p2;
const p3 = this.p3;

const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const t2 = t * t;
const t3 = t2 * t;

const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;

return { x };
}
}


CubicBezier支持get方法,通过四个关键点位信息,绘制三次贝塞尔曲线,参数t在0到1之间变化,当t从0变化到1时,曲线上的点从p0平滑地过渡到p3


mt = 1 - t;:这是t的补数(1减去t)。
mt2 = mt * mt; 和 mt3 = mt2 * mt;:计算mt的平方和立方。
t2 = t * t; 和 t3 = t2 * t;:计算t的平方和立方。


这是通过取四个点的x坐标的加权和来完成的,其中权重是基于t的幂的。具体来说,p0的权重是(1-t)^3p1的权重是3 * (1-t)^2 * tp2的权重是3 * (1-t) * t^2,而p3的权重是t^3


{ x: 0 },{ x: 0.5 },{ x: 2 },{ x: 1 } 这组数据形成的曲线效果是由start参数到end的两倍参数再到end参数


具体效果如下


贝塞尔曲线.gif


齿轮


齿轮动画


模型中自带动画


齿轮动画数据.jpeg


源码中有一整套的动画播放类方法,HandleAnimation,其中功能包含播放训话动画,切换动画,播放一次动画,绘制骨骼,镜头跟随等功能。


具体使用方法:


   // 齿轮动画
/**
*
* @param model 动画模型
* @param animations 动画合集
*/

motorAnimation = new HandleAnimation(motorGltf.scene, motorGltf.animations)
// 播放动画 take 001 是默认动画名称
motorAnimation.play('Take 001')

在render中调用


motorAnimation && motorAnimation.upDate()

齿轮展开(补间动画)


补间动画在齿轮展开时调用,使用的tweenjs,这里讲一下定位运动后的模型位置,使用# 变换控制器(TransformControls),代码中有封装好的完整的使用方法,在TransformControls.ts中,包含同时存在轨道控制器时与变换控制器对场景操作冲突时的处理。


使用方法:


/**
* @param mesh 受控模型
* @param draggingChangedCallback 操控回调
*/

TransformControls(mesh, ()=>{
console.log(mesh.position)
})

齿轮展开定位.jpeg


齿轮发光


发光效果方法封装在utls/index.ts中的unreal方法,使用的是threejs提供的虚幻发光通道RenderPass,UnrealBloomPass,以及合成器EffectComposer,方法接受参数如下



// params 默认参数
const createParams = {
threshold: 0,
strength: 0.972, // 强度
radius: 0.21,// 半径
exposure: 1.55 // 扩散
};

/**
*
* @param scene 渲染场景
* @param camera 镜头
* @param renderer 渲染器
* @param width 需要发光位置的宽度
* @param height 发光位置的高度
* @param params 发光参数
* @returns
*/


调用方法如下:



const { finalComposer: F,
bloomComposer: B,
renderScene: R, bloomPass: BP } = unreal(scene, camera, renderer, width, height, params)
finalComposer = F
bloomComposer = B
renderScene = R
bloomPass = BP
bloomPass.threshold = 0


除了调用方法还有一些需要调整的地方,比如发光时模型什么材质,又或者不发光时又是什么材质,这里需要单独定义,并在render渲染函数中调用


 if (guiParams.isLight) {
if (bloomComposer) {
scene.traverse(darkenNonBloomed.bind(this));
bloomComposer.render();
}
if (finalComposer) {
scene.traverse(restoreMaterial.bind(this));
finalComposer.render();
}
}

scene.traverse的回调中,检验模型是否为发光体,再进行材质的更换,这里用的标识是 object.userData.isLighttrue时,判定该物体为发光物体。其他物体则不发光


回调方法


function darkenNonBloomed(obj: THREE.Mesh) {
if (bloomLayer) {
if (!obj.userData.isLight && bloomLayer.test(obj.layers) === false) {
materials[obj.uuid] = obj.material;
obj.material = darkMaterial;
}
}

}

function restoreMaterial(obj: THREE.Mesh) {
if (materials[obj.uuid]) {
obj.material = materials[obj.uuid];
// 用于删除没必要的渲染
delete materials[obj.uuid];
}
}


再场景的右上角我们新增了几个参数,用来调整线条的发光效果,下面通过动图看一下,图片有点大,请耐心等待加载


调试发光效果.gif


好啦,本篇文章到此,如看源码有不明白的地方,可私信~


最近正在筹备工具库,以上可视化常用的方法都将涵盖在里面


历史文章


three.js——商场楼宇室内导航系统 内附源码


three.js——可视化高级涡轮效果+警报效果 内附源码


高德地图巡航功能 内附源码


three.js——3d塔防游戏 内附源码


three.js+物理引擎——跨越障碍的汽车 可操作 可演示


百度地图——如何计算地球任意两点之间距离 内附源码


threejs——可视化地球可操作可定位


three.js 专栏


源码及讲解



源码 http://www.aspiringcode.com/content?id=…


体验地址:display.aspiringcode.com:8888/html/171422…


B站讲解地址:【threejs渲染高级感可视化风力发电车模型】 http://www.bilibili.com/video/BV1gT…


作者:孙_华鹏
来源:juejin.cn/post/7379906492038889512
收起阅读 »

前端太卷了,不玩了,写写node.js全栈涨工资,赶紧学起来吧!!!!!

web
如果你感觉到累了,卷不动了,那就来看这篇文章吧,写写全栈,涨涨工资,吹吹牛皮!人生得意须尽欢,不要只想忙搬砖!首先聊下node.js的优缺点和应用场景Node.js的优点和应用场景Node.js作为后端开发的选择具有许多优点,以下是其中一些:高性能: ...
继续阅读 »

如果你感觉到累了,卷不动了,那就来看这篇文章吧,写写全栈,涨涨工资,吹吹牛皮!人生得意须尽欢,不要只想忙搬砖!

首先聊下node.js的优缺点和应用场景

Node.js的优点和应用场景

Node.js作为后端开发的选择具有许多优点,以下是其中一些:

  1. 高性能: Node.js采用了事件驱动、非阻塞I/O模型,使得它能够处理大量并发请求而不会阻塞线程,从而具有出色的性能表现。
  2. 轻量级和高效: Node.js的设计简洁而轻量,启动速度快,内存占用低,适合构建轻量级、高效的应用程序。
  3. JavaScript全栈: 使用Node.js,开发者可以使用同一种语言(JavaScript)进行前后端开发,简化了开发人员的学习成本和代码维护成本。
  4. 丰富的生态系统: Node.js拥有丰富的第三方模块和库,可以轻松集成各种功能和服务,提高开发效率。
  5. 可扩展性: Node.js具有良好的可扩展性,可以通过添加更多的服务器实例来横向扩展应用程序,满足不断增长的用户需求。
  6. 实时应用: 由于Node.js对于事件驱动和非阻塞I/O的支持,它非常适合构建实时应用,如即时通讯、在线游戏、实时分析等。
  7. 微服务架构: Node.js可以作为微服务架构中的一个服务组件,通过轻量级的设计和快速的响应能力,使得微服务之间的通信更加高效。
  8. 数据流处理: Node.js适合处理大量的数据流,例如文件操作、网络流量分析等,可以通过流式处理来有效地管理和处理数据。

应用场景包括但不限于:

  1. Web应用程序: 开发基于Node.js的Web应用程序,如社交网络、电子商务平台、博客、内容管理系统等。
  2. API服务: 使用Node.js构建RESTful API服务,为移动应用、前端应用提供数据接口。
  3. 实时应用: 构建实时应用程序,如聊天应用、在线游戏、实时地图等,利用Node.js的事件驱动和非阻塞I/O模型实现高效的实时通讯。
  4. 数据流处理: 使用Node.js处理大量的数据流,例如日志处理、实时监控、数据分析等。
  5. 微服务架构: 将Node.js作为微服务架构中的一个服务组件,通过轻量级的设计和快速的响应能力,实现服务之间的高效通信。

总的来说,Node.js作为一种高性能、轻量级的后端开发工具,适用于各种类型的应用场景,尤其在需要处理大量并发请求和实时通讯的应用中表现突出。

node.js的局限性

尽管Node.js在许多方面都表现出色,但它也有一些局限性和适用场景的限制。以下是一些Node.js的局限性:

  1. 单线程阻塞: 虽然Node.js采用了非阻塞I/O的模型,但在处理CPU密集型任务时,单线程的特性可能导致性能瓶颈。由于Node.js是单线程的,处理阻塞操作(如大量计算或长时间的同步操作)会影响整个应用程序的响应性。
  2. 回调地狱(Callback Hell): 在复杂的异步操作中,嵌套的回调函数可能导致代码难以理解和维护,这被称为“回调地狱”问题。虽然可以使用Promise、async/await等来缓解这个问题,但在某些情况下仍可能存在。
  3. 相对较小的标准库: Node.js的标准库相对较小,相比于其他后端语言,需要依赖第三方模块来实现一些常见的功能。这可能需要花费额外的时间来选择、学习和整合这些模块。
  4. 不适合密集型计算: 由于Node.js是单线程的,不适合用于处理大量的计算密集型任务。如果应用程序主要依赖于大量的数学计算或复杂的数据处理,其他多线程的语言可能更合适。
  5. Callback错误处理: 在回调模式下,错误处理可能变得繁琐,需要在每个回调中检查错误对象。这使得开发者需要更加小心地处理错误,以确保它们不会被忽略。
  6. 相对较新的技术栈: 相较于一些传统的后端技术栈,Node.js是相对较新的技术,一些企业可能仍然更倾向于使用更成熟的技术。
  7. 不适合长连接: 对于长连接的应用场景,如传统的即时通讯(IM)系统,Node.js的单线程模型可能不是最佳选择,因为它会导致长时间占用一个线程。

尽管有这些局限性,但Node.js在许多应用场景下仍然是一个强大且高效的工具。选择使用Node.js还是其他后端技术应该根据具体项目的需求、团队的技术栈和开发者的经验来做出。

node.js常用的几种主流框架

Node.js是一个非常灵活的JavaScript运行时环境,它可以用于构建各种类型的应用程序,从简单的命令行工具到大型的网络应用程序。以下是一些常用的Node.js框架:

  1. Express.js:Express.js是Node.js最流行的Web应用程序框架之一,它提供了一组强大的功能,使得构建Web应用变得更加简单和快速。Express.js具有路由、中间件、模板引擎等功能,可以满足大多数Web应用的需求。
  2. Koa.js:Koa.js是由Express.js原班人马打造的下一代Node.js Web框架,它使用了ES6的新特性,如async/await,使得编写异步代码更加简洁。Koa.js更加轻量级和灵活,它提供了更强大的中间件功能,可以更方便地实现定制化的功能。
  3. Nest.js:Nest.js是一个用于构建高效、可扩展的服务器端应用程序的渐进式Node.js框架。它基于Express.js,但引入了许多现代化的概念,如依赖注入、模块化、类型检查等,使得构建复杂应用变得更加简单。
  4. Hapi.js:Hapi.js是一个专注于提供配置简单、可测试性强的Web服务器框架。它提供了一系列的插件,可以轻松地扩展其功能,同时具有强大的路由、验证、缓存等功能,适用于构建大型和高可靠性的Web应用程序。
  5. Meteor.js:Meteor.js是一个全栈JavaScript框架,它可以同时构建客户端和服务器端的应用程序。Meteor.js提供了一整套的工具和库,包括数据库访问、实时数据同步、用户认证等功能,使得构建实时Web应用变得更加简单和快速。
  6. Sails.js:Sails.js是一个基于Express.js的MVC框架,它提供了类似于Ruby on Rails的开发体验,使得构建数据驱动的Web应用变得更加简单。Sails.js具有自动生成API、蓝图路由、数据关联等功能,适用于构建RESTful API和实时Web应用。

Express框架:实践与技术探索

1. Express框架简介:

Express是一个轻量级且灵活的Node.js Web应用程序框架,它提供了一组简洁而强大的工具,帮助开发者快速构建Web应用。Express的核心理念是中间件,通过中间件可以处理HTTP请求、响应以及应用程序的逻辑。


2. 基础搭建与路由:

在开始实践之前,首先需要搭建Express应用程序的基础结构。通过使用express-generator工具或手动创建package.jsonapp.js文件,可以快速启动一个Express项目。接下来,我们将学习如何定义路由以及如何处理HTTP请求和响应。

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

app.get('/', (req, res) => {
res.send('Hello World!');
});

app.listen(3000, () => {
console.log('Express app listening on port 3000');
});

3. 中间件:

Express中间件是一个函数,它可以访问请求对象(req)、响应对象(res)以及应用程序的下一个中间件函数(通常命名为next)。中间件函数可以用来执行任何代码,修改请求和响应对象,以及终止请求-响应周期。

app.use((req, res, next) => {
console.log('Time:', Date.now());
next();
});

4. 模板引擎与视图:

Express框架允许使用各种模板引擎来生成动态HTML内容。常用的模板引擎包括EJS、Pug和Handlebars。通过配置模板引擎,可以将动态数据嵌入到静态模板中,以生成最终的HTML页面。

app.set('view engine', 'ejs');

5. 数据库集成与ORM:

在实际应用中,数据库是不可或缺的一部分。Express框架与各种数据库集成良好,可以通过ORM(对象关系映射)工具来简化数据库操作。常用的ORM工具包括Sequelize、Mongoose等,它们可以帮助开发者更轻松地进行数据模型定义、查询和操作。


6. RESTful API设计与实现:

Express框架非常适合构建RESTful API。通过定义不同的HTTP动词和路由,可以实现资源的创建、读取、更新和删除操作。此外,Express还提供了一系列中间件来处理请求体、响应格式等,使得构建API变得更加简单。

app.get('/api/users', (req, res) => {
// 获取所有用户信息
});

app.post('/api/users', (req, res) => {
// 创建新用户
});

7. 实践案例:

为了更好地理解Express框架的实践,我们将以一个简单的博客应用为例。在这个应用中,我们可以拓展一下用户的注册、登录、文章的创建和展示等功能,并且结合数据库和RESTful API设计。在这个示例中,我们将使用MongoDB作为数据库,并使用Mongoose作为MongoDB的对象建模工具。首先,确保您已经安装了Node.js``和MongoDB,并创建了一个名为blogApp的文件夹来存放我们的项目。

  1. 首先,在项目文件夹中初始化npm,并安装Express、Mongoose和body-parser依赖:
npm init -y
npm install express mongoose body-parser
  1. 在项目文件夹中创建app.js文件,并编写以下代码:
// 导入所需的模块
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');

// 连接MongoDB数据库
mongoose.connect('mongodb://localhost:27017/blog', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;

// 检测数据库连接状态
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', function() {
console.log('Connected to MongoDB');
});

// 创建Express应用
const app = express();

// 使用body-parser中间件解析请求体
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// 定义用户模型
const User = mongoose.model('User', new mongoose.Schema({
username: String,
password: String
}));

// 注册用户
app.post('/api/register', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.create({ username, password });
res.json({ success: true, message: 'User registered successfully', user });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 用户登录
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
if (user) {
res.json({ success: true, message: 'User logged in successfully', user });
} else {
res.status(401).json({ success: false, message: 'Invalid username or password' });
}
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 启动Express服务器
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

以上代码实现了用户注册和登录的功能,使用了MongoDB作为数据库存储用户信息,并提供了RESTful风格的API接口。

您可以通过以下命令启动服务器:

node app.js
  1. 接下来,我们添加文章模型和相关的路由来实现文章的创建和展示功能。在app.js文件中添加以下代码:
// 定义文章模型
const Article = mongoose.model('Article', new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}));

// 创建文章
app.post('/api/articles', async (req, res) => {
try {
const { title, content, author } = req.body;
const article = await Article.create({ title, content, author });
res.json({ success: true, message: 'Article created successfully', article });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

// 获取所有文章
app.get('/api/articles', async (req, res) => {
try {
const articles = await Article.find().populate('author', 'username');
res.json({ success: true, articles });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});

以上代码实现了创建文章和获取所有文章的功能,每篇文章都与特定的作者相关联。

现在,您可以使用POST请求来创建新的用户和文章,使用GET请求来获取所有文章。例如:

  • 注册新用户:发送POST请求到/api/register,传递usernamepassword字段。
  • 用户登录:发送POST请求到/api/login,传递usernamepassword字段。
  • 创建新文章:发送POST请求到/api/articles,传递titlecontentauthor字段(注意,author字段应该是已注册用户的ID)。
  • 获取所有文章:发送GET请求到/api/articles

这个示例演示了如何使用Express框架结合MongoDB实现一个简单的博客应用,并提供了RESTful API接口。可以根据需求扩展和定制这个应用,例如添加用户身份验证、文章编辑和删除功能等。

看完后是不是觉得后端(CRUD)很简单,没错!就是这么简单!喜欢的小伙伴给个点赞加收藏,码字不易!


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

反射为什么慢?

1. 背景 今天刷到一篇文章,标题是反射为什么慢,一下子懵逼了,确实没想过这个问题;盲猜了一下是由于反射实际上是做了一个代理的动作,导致执行的效率是小于直接实体类去调用方法的。 2. 文章给出的解释 文章中给出的理由是因为以下4点: 反射涉及动态解析的内容,...
继续阅读 »

1. 背景


今天刷到一篇文章,标题是反射为什么慢,一下子懵逼了,确实没想过这个问题;盲猜了一下是由于反射实际上是做了一个代理的动作,导致执行的效率是小于直接实体类去调用方法的。


2. 文章给出的解释


文章中给出的理由是因为以下4点:



  1. 反射涉及动态解析的内容,不能执行某些虚拟机优化,例如JIT优化技术

  2. 在反射时,参数需要包装成object[]类型,但是方法真正执行的时候,又使用拆包成真正的类型,这些动作不仅消耗时间,而且过程中会产生很多的对象,这就会导致gc,gc也会导致延时

  3. 反射的方法调用需要从数组中遍历,这个遍历的过程也比较消耗时间

  4. 不仅需要对方法的可见性进行检查,参数也需要做额外的检查


3. 结合实际理解


3.1 第一点分析


首先我们需要知道,java中的反射是一种机制,它可以在代码运行过程中,获取类的内部信息(变量、构造方法、成员方法);操作对象的属性、方法。
然后关于反射的原理,首先我们需要知道一个java项目在启动之后,会将class文件加载到堆中,生成一个class对象,这个class对象中有一个类的所有信息,通过这个class对象获取类相关信息的操作我们称为反射。


其次是JIT优化技术,首先我们需要知道在java虚拟机中有两个角色,解释器和编译器;这两者各有优劣,首先是解释器可以在项目启动的时候直接直接发挥作用,省去编译的时候,立即执行,但是在执行效率上有所欠缺;在项目启动之后,随着时间推移,编译器逐渐将机器码编译成本地代码执行,减少解释器的中间损耗,增加了执行效率。


我们可以知道JIT优化通常依赖于在编译时能够知道的静态信息,而反射的动态性可能会破坏这些假设,使得JIT编译器难以进行有效的优化。


3.2 第二点


关于第二点,我们直接写一段反射调用对象方法的demo:


@Test
public void methodTest() {
Class clazz = MyClass.class;

try {
//获取指定方法
//这个注释的会报错 java.lang.NoSuchMethodException
//Method back = clazz.getMethod("back");
Method back = clazz.getMethod("back", String.class);
Method say = clazz.getDeclaredMethod("say", String.class);
//私有方法需要设置
say.setAccessible(true);
MyClass myClass = new MyClass("abc", 99);
//反射调用方法
System.out.println(back.invoke(myClass, "back"));

say.invoke(myClass, "hello world");
} catch (Exception e) {
e.printStackTrace();
}
}

在上面这段代码中,我们调用了一个invoke 方法,并且传了class对象和参数,进入到invoke方法中,我们可以看到invoke方法的入参都是Object类型的,args更是一个Object 数组,这就第二点,关于反射调用过程中的拆装箱。


@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

3.3 第三点


关于调用方法需要遍历这点,还是上面那个demo,我们在获取Method 对象的时候是通过调用getMethod、getDeclaredMethod方法,点击进入这个方法的源码,我们可以看到如下代码:


private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)

{
Method res = null;
String internedName = name.intern();
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
if (m.getName() == internedName
&& arrayContentsEq(parameterTypes, m.getParameterTypes())
&& (res == null
|| res.getReturnType().isAssignableFrom(m.getReturnType())))
res = m;
}

return (res == null ? res : getReflectionFactory().copyMethod(res));
}

我们可以看到,底层实际上也是将class对象的所有method遍历了一遍,最终才拿到我们需要的方法的,这也就是第二点,执行具体方法的时候需要遍历class对象的方法。


3.4 第四点


第4点说需要对方法和参数进行检查,也就是我们在执行具体的某一个方法的时候,我们实际上是需要校验这个方法是否可见的,如果不可见,我们还需要将这个方法设置为可见,否则如果我们直接调用这个方法的话,会报错。


同时还有一个点,在我们调用invoke方法的时候,反射类会对方法和参数进行一个校验,让我们来看一下源码:


@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

我们可以看到还有quickCheckMemberAccess、checkAccess 等逻辑


4. 总结


平时在反射这块用的比较少,也没针对性的去学习一下。在工作之余,还是得保持一个学习的习惯,这样子才不会出现今天这种被一个问题难倒的情况,而且才能产出更多、更优秀的方案。


作者:喜欢小钱钱
来源:juejin.cn/post/7330115846140051496
收起阅读 »

听我一句劝,业务代码中,别用多线程。

你好呀,我是歪歪。 前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。 虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。 我只是微微一笑,这不是很正常吗? 业务代码中一般也使不上多线...
继续阅读 »

你好呀,我是歪歪。


前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。


虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。


我只是微微一笑,这不是很正常吗?


业务代码中一般也使不上多线程,或者说,业务代码中不知不觉你以及在使用线程池了,你再 duang 的一下搞一个出来,反而容易出事。


所以提到线程池的时候,我个人的观点是必须把它吃得透透的,但是在业务代码中少用或者不用多线程。


关于这个观点,我给你盘一下。


Demo


首先我们还是花五分钟搭个 Demo 出来。


我手边刚好有一个之前搭的一个关于 Dubbo 的 Demo,消费者、生产者都有,我就直接拿来用了:



这个 Demo 我也是跟着网上的 quick start 搞的:



cn.dubbo.apache.org/zh-cn/overv…




可以说写的非常详细了,你就跟着官网的步骤一步步的搞就行了。


我这个 Demo 稍微不一样的是我在消费者模块里面搞了一个 Http 接口:



在接口里面发起了 RPC 调用,模拟从前端页面发起请求的场景,更加符合我们的开发习惯。


而官方的示例中,是基于了 SpringBoot 的 CommandLineRunner 去发起调用:



只是发起调用的方式不一样而已,其他没啥大区别。


需要说明的是,我只是手边刚好有一个 Dubbo 的 Demo,随手就拿来用了,但是本文想要表达的观点,和你使不使用 Dubbo 作为 RPC 框架,没有什么关系,道理是通用的。


上面这个 Demo 启动起来之后,通过 Http 接口发起一次调用,看到控制台服务提供方和服务消费方都有对应的日志输出,准备工作就算是齐活儿了:



上菜


在上面的 Demo 中,这是消费者的代码:



这是提供者的代码:



整个调用链路非常的清晰:



来,请你告诉我这里面有线程池吗?


没有!


是的,在日常的开发中,我就是写个接口给别人调用嘛,在我的接口里面并没有线程池相关的代码,只有 CRUD 相关的业务代码。


同时,在日常的开发中,我也经常调用别人提供给我的接口,也是一把梭,撸到底,根本就不会用到线程池。


所以,站在我,一个开发人员的角度,这个里面没有线程池。


合理,非常合理。


但是,当我们换个角度,再看看,它也是可以有的。


比如这样:



反应过来没有?


我们发起一个 Http 调用,是由一个 web 容器来处理这个请求的,你甭管它是 Tomcat,还是 Jetty、Netty、Undertow 这些玩意,反正是个 web 容器在处理。


那你说,这个里面有线程池吗?


在方法入口处打个断点,这个 http-nio-8081-exec-1 不就是 Tomcat 容器线程池里面的一个线程吗:



通过 dump 堆栈信息,过滤关键字可以看到这样的线程,在服务启动起来,啥也没干的情况下,一共有 10 个:



朋友,这不就是线程池吗?


虽然不是你写的,但是你确实用了。


我写出来的这个 test 接口,就是会由 web 容器中的一个线程来进行调用。所以,站在 web 容器的角度,这里是有一个线程池的:



同理,在 RPC 框架中,不管是消费方,还是服务提供方,也都存在着线程池。


比如 Dubbo 的线程池,你可以看一下官方的文档:



cn.dubbo.apache.org/zh-cn/overv…




而对于大多数的框架来说,它绝不可能只有一个线程池,为了做资源隔离,它会启用好几个线程池,达到线程池隔离,互不干扰的效果。


比如参与 Dubbo 一次调用的其实不仅一个线程池,至少还有 IO 线程池和业务线程池,它们各司其职:



我们主要关注这个业务线程池。


反正站在 Dubbo 框架的角度,又可以补充一下这个图片了:



那么问题来了,在当前的这个情况下?


当有人反馈:哎呀,这个服务吞吐量怎么上不去啊?


你怎么办?


你会 duang 的一下在业务逻辑里面加一个线程池吗?



大哥,前面有个 web 容器的线程池,后面有个框架的线程池,两头不调整,你在中间加个线程池,加它有啥用啊?


web 容器,拿 Tomcat 来说,人家给你提供了线程池参数调整的相关配置,这么一大坨配置,你得用起来啊:



tomcat.apache.org/tomcat-9.0-…




再比如 Dubbo 框架,都给你明说了,这些参数属于性能调优的范畴,感觉不对劲了,你先动手调调啊:



你把这些参数调优弄好了,绝对比你直接怼个线程池在业务代码中,效果好的多。


甚至,你在业务代码中加入一个线程池之后,反而会被“反噬”。


比如,你 duang 的一下怼个线程池在这里,我们先只看 web 容器和业务代码对应的部分:



由于你的业务代码中有线程池的存在,所以当接受到一个 web 请求之后,立马就把请求转发到了业务线程池中,由线程池中的线程来处理本次请求,从而释放了 web 请求对应的线程,该线程又可以里面去处理其他请求。


这样来看,你的吞吐量确实上去了。


在前端来看,非常的 nice,请求立马得到了响应。


但是,你考虑过下游吗?


你的吞吐量上涨了,下游同一时间处理的请求就变多了。如果下游跟不上处理,顶不住了,直接就是崩给你看怎么办?



而且下游不只是你一个调用方,由于你调用的太猛,导致其他调用方的请求响应不过来,是会引起连锁反应的。


所以,这种场景下,为了异步怼个线程池放着,我觉得还不如用消息队列来实现异步化,顶天了也就是消息堆积嘛,总比服务崩了好,这样更加稳妥。


或者至少和下游勾兑一下,问问我们这边吞吐量上升,你们扛得住不。


有的小伙伴看到这里可能就会产生一个疑问了:歪师傅,你这个讲得怎么和我背的八股文不一样啊?


巧了,你背过的八股文我也背过,现在我们来温习一下我们背过的八股文。


什么时候使用线程池呢?


比如一个请求要经过若干个服务获取数据,且这些数据没有先后依赖,最终需要把这些数据组合起来,一并返回,这样经典的场景:



用户点商品详情,你要等半天才展示给用户,那用户肯定骂骂咧咧的久走了。


这个时候,八股文上是怎么说的:用线程池来把串行的动作改成并行。



这个场景也是增加了服务 A 的吞吐量,但是用线程池就是非常正确的,没有任何毛病。


但是你想想,我们最开始的这个案例,是这个场景吗?



我们最开始的案例是想要在业务逻辑中增加一个线程池,对着一个下游服务就是一顿猛攻,不是所谓的串行改并行,而是用更多的线程,带来更多的串行。


这已经不是一个概念了。


还有一种场景下,使用线程池也是合理的。


比如你有一个定时任务,要从数据库中捞出状态为初始化的数据,然后去调用另外一个服务的接口查询数据的最终状态。



如果你的业务代码是这样的:


//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //捕获异常以免一条数据错误导致循环结束
    try{
        //发起rpc调用
        String orderStatus = queryOrderStatus(orderInfo.getOrderId);
        //更新订单状态
        updateOrderInfo(orderInfo.getOrderId,orderStatus);  
    } catch (Exception e){
        //打印异常
    }
}

虽然你框架中使用了线程池,但是你就是在一个 for 循环中不停的去调用下游服务查询数据状态,是一条数据一条数据的进行处理,所以其实同一时间,只是使用了框架的线程池中的一个线程。


为了更加快速的处理完这批数据,这个时候,你就可以怼一个线程池放在 for 循环里面了:


//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //使用线程池
    executor.execute(() -> {
        //捕获异常以免一条数据错误导致循环结束
        try {
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId, orderStatus);
        } catch (Exception e) {
            //打印异常
        }
    });
}


需要注意的是,这个线程池的参数怎么去合理的设置,是需要考虑的事情。


同时这个线程池的定位,就类似于 web 容器线程池的定位。


或者这样对比起来看更加清晰一点:



定时任务触发的时候,在发起远程接口调用之前,没有线程池,所以我们可以启用一个线程池来加快数据的处理。


而 Http 调用或者 RPC 调用,框架中本来就已经有一个线程池了,而且也给你提供了对应的性能调优参数配置,那么首先考虑的应该是把这个线程池充分利用起来。


如果仅仅是因为异步化之后可以提升服务响应速度,没有达到串行改并行的效果,那么我更加建议使用消息队列。


好了,本文的技术部分就到这里啦。


下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。


荒腔走板



不知道你看完文章之后,有没有产生一个小疑问:最开始部分的 Demo 似乎用处并不大?


是的,我最开始构思的行文结构是是基于 Demo 在源码中找到关于线程池的部分,从而引出其实有一些我们“看不见的线程池”的存在的。


原本周六我是有一整天的时间来写这篇文章,甚至周五晚上还特意把 Demo 搞定,自己调试了一番,该打的断点全部打上,并写完 Demo 那部分之后,我才去睡觉的,想得是第二天早上起来直接就能用。


按照惯例周六睡个懒觉的,早上 11 点才起床,自己慢条斯理的做了一顿午饭,吃完饭已经是下午 1 点多了。


本来想着在沙发上躺一会,结果一躺就是一整个下午。期间也想过起来写一会文章,坐在电脑前又飞快的躺回到沙发上,就是觉得这个事情索然无味,当下的那一刻就想躺着,然后无意识的刷手机,原本是拿来写文章中关于源码的部分的时间就这样浪费了。


像极了高中时的我,周末带大量作业回家,准备来个悬梁刺股,弯道超车,结果变成了一睡一天,捏紧刹车。


高中的时候,时间浪费了是真的可惜。


现在,不一样了。


荒腔走板这张图片,就是我躺在沙发上的时候,别人问我在干什么时随手拍的一张。


我并不为躺了一下午没有干正事而感到惭愧,浪费了的时间,才是属于自己的时间。


很久以前我看到别人在做一些浪费时间的事情的时候,我心里可能会嘀咕几句,劝人惜时。


这两年我不会了,允许自己做自己,允许别人做别人。


作者:why技术
来源:juejin.cn/post/7297980721590272040
收起阅读 »

8年前端,那就聊聊被裁的感悟吧!!

web
前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。 另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的...
继续阅读 »

前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。
另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的际遇


我的经历


第一家公司


第一家公司说来也巧,本来是准备入职一家外包的,在杭州和同学吃个饭,接到了面试通知,一看地址就在楼上,上去一共就3轮面试,不到2个小时直接给了offer。有些东西真的就是命中注定


第二家公司


第二家公司我入职以后挖了上家公司诸多墙角,我一共挖了6个前端,2个后端。拯救朋友们于水深火热之中。





我本以为我能开启美好的新生活,结果第二年就传来我父亲重病的噩耗 肺癌晚期,我学习了大量的肺癌知识什么小细胞,非小细胞,基因检测呀等等。。。





可是最后还是没有挽留住他的生命,我记得我俩在最后一次去武汉的时候,睡在一起,他给我说了很多。


他说:治不好就算了,只是没能看到自己的孙子有些可惜罢了。

他说:我这一辈碌碌无为,没给你带来多么优越的条件,结婚、买房、工作都没给到任何帮助,唯一让我感到欣慰的是你那么努力,比我强多了,家里邻居很多都眼馋你呢。
他说:你小孩的名字想好了吗?你媳妇真是个孝顺的孩子,性格也好,心地善良,你要好好对待她。

他说了很多。。。我都快忘了他说了啥了,我不想忘来着,可是可是,想起来就又好难过。


这只是我人生历程的一部分,我把这些讲出来,是为了让大家明白,你现在所经历的困苦其实没有那么严重,人在逆境之中会放大自己的困难,以博得同情。所以现在很多人给我倒苦水的时候,我总有点不屑一顾的感觉,并不是我有多强,我只是觉得都能过去。


在灰暗的时候,工作总是心不在焉,情绪莫名冲动,我和领导吵过架,和ui妹妹撕破脸,导致人家天天投诉我。我leader说我态度极其嚣张,我说你再多说一句,我干死你所以不裁我裁谁


我的人生感悟


我时常以我爸的角度换位思考,我在得知这个消息后我该咋办?是积极面对,还是放弃治疗?可是所有的都是在假设的前提之下,一切不可为真。只有在其中的才最能明白其中的感受。
那一年我看着他积极想活着的毅力,也看到了他身体日渐消瘦的无奈,无奈之余还要应付各种亲戚的嘘寒问暖


我现在很能明白《天道》中那段,丁元英说的如果为了孝顺的名声,让父亲痛苦没有尊严地活着,还不如让父亲走了。 的意思了。在他昏迷不醒的时候,大小便失禁的时候,真不如有尊严的走了。


我其实已经预感到自己要被裁,我原本是挺担心的,可是后来想想父亲的话,我总结成一句话圆滑对事,诚以待人。 这句话看上去前后矛盾,无外乎俩个观点。


圆滑对事的意思是:就是要学会嘴甜,事嘛能少干就少干,能干几分是几分,累的是别人,爽的是自己,在规则中寻求最大的自我利益化。


诚以待人的意思是:圆滑归圆滑,不能对谁都圆滑,你得有把事情办的很好的能力,你需要给真正需要的人创造价值,而不是为了给压榨者提供以自我健康为代价的价值。



用现在最流行的词来说就是「佛系」。


什么叫活明白了,通常被理解为不争不抢,得之淡然、失之泰然、顺其自然的一种心理状态。


活明白的人一般知道自己要什么样的生活,他们不世故、不圆滑,坦荡的、磊落的做自己应该做的事儿。他们与社会上潜规则里的不良之风格格不入,却不相互抵触,甚至受到局中人的青睐与欣赏。


活明白的人看着更为洒脱,得不张扬,失不气馁,心态随和、随遇而安。


不过,还有一种活明白的人,不被多数人所接受。他们玩世不恭、好吃懒做,把所有一切交给命运去背锅。这种人极度自我,没有什么可以超越他自己的利益,无法想象这种活法,简直就是在浪费六道轮回的名额。


总之,有的人活明白了,是调整自己的心态,维护社会的稳定和安宁。有的人活明白了,是以自我为中心,一边依赖着社会救济,一边责备社会龌蹉。


所以,活明白的人也分善与恶,同样是一种积极向善,另一种是消极向恶,二者同出而异名。



我对生活的态度


离职的第一个月,便独自一人去了南京,杭州,长沙,武汉,孝感。我见了很多老朋友,听听他们发发牢骚,然后找一些小众的景点完成探险。


在南京看了看中医,在杭州露营看了看日落,在长沙夜爬了岳麓山,在武汉坐了超级大摆锤,在孝感去了无名矿坑并在一个奶奶家蹭了中午饭。


我的感受极其良好,我体验了前所未有生活态度,我热情待人,嘻嘻笑笑,我站在山顶敞怀吹风,在无尽的树林中悠然自得,治愈我不少的失落情绪。我将继续为生活的不易奔波,也将继续热爱生活,还会心怀感恩对待他人,也会圆滑处事 事事佛系。


背景1.png


图层 1.png


IMG_6214.JPG


IMG_6198.JPG


IMG_6279.JPG


可能能解决你的问题


要不要和家里人说


我屏蔽了家里人,把负面情绪隐藏,避免波及母亲本就脆弱的内心世界,我还骗她说公司今年不挣钱,提前让我们放假,只给基础工资。如果你家境殷实,家庭和睦,我建议大方的说,这样你和父母又多了一个可以聊的话题,不妨和他们多多交流,耐心一些。


裁员,真不是你的问题


请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术,你得让自己变得精彩,别虚度了这如花般的时光。可能你懒,可能也没什么规划,那就想到啥就做啥好了,可能前几次需要鼓足干劲,后面就会发现轻而易举。


如何度过很丧的阶段


沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

不要总在家待着,要想办法出门,多建立与社会的联系,多和朋友吹吹牛逼,别把脸面看的那么重要,死皮赖脸反而是一种讨人喜欢的性格。



不管环境怎样,希望你始终向前,披荆斩棘

如果你也正在经历这个阶段,希望你放平心态,积极应对

如果你也在人生的至暗时刻,也请不要彷徨,时间总会治愈一切

不妨试试大胆一点,生活给的惊喜也同样不少

我在一个冬天的夜晚写着文字,希望能对你有些帮助


作者:顾昂_
来源:juejin.cn/post/7331657679012380722
收起阅读 »

Electron实现静默打印小票

web
Electron实现静默打印小票 静默打印流程 1.渲染进程通知主进程打印 //渲染进程 data是打印需要的数据 window.electron.ipcRenderer.send('handlePrint', data) 2.主进程接收消息,创建打印页面...
继续阅读 »

Electron实现静默打印小票


静默打印流程


09c00eb5-f171-4090-a178-37e149d1d0f7.png


1.渲染进程通知主进程打印


//渲染进程 data是打印需要的数据
window.electron.ipcRenderer.send('handlePrint', data)

2.主进程接收消息,创建打印页面


//main.ts
/* 打印页面 */
let printWindow: BrowserWindow | undefined
/**
* @Author: yaoyaolei
* @Date: 2024-06-07 09:27:22
* @LastEditors: yaoyaolei
* @description: 创建打印页面
*/

const createPrintWindow = () => {
return new Promise<void>((resolve) => {
printWindow = new BrowserWindow({
...BASE_WINDOW_CONFIG,
title: 'printWindow',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false
}
})

printWindow.on('ready-to-show', () => {
//打印页面创建完成后不需要显示,测试时可以调用show查看页面样式(下面有我处理的样式图片)
// printWindow?.show()
resolve()
})

printWindow.webContents.setWindowOpenHandler((details: { url: string }) => {
shell.openExternal(details.url)
return { action: 'deny' }
})

if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
printWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/print.html`)
} else {
printWindow.loadFile(join(__dirname, `../renderer/print.html`))
}
})
}

ipcMain.on('handlePrint', (_, obj) => {
//主进程接受渲染进程消息,向打印页面传递数据
if (printWindow) {
printWindow!.webContents.send('data', obj)
} else {
createPrintWindow().then(() => {
printWindow!.webContents.send('data', obj)
})
}
})

3.打印页面接收消息,拿到数据渲染页面完成后通知主进程开始打印


<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>打印</title>
<style>
</style>
</head>

<body>

</body>
<script>
window.electron.ipcRenderer.on('data', (_, obj) => {
//这里是接受的消息,处理完成后将html片段放在body里面完成后就可以开始打印了
//样式可以写在style里,也可以内联
console.log('event, data: ', obj);
//这里自由发挥
document.body.innerHTML = '处理的数据'
//通知主进程开始打印
window.electron.ipcRenderer.send('startPrint')
})
</script>
</html>



这个是我处理完的数据样式,这个就是print.html
9f17ea7e-3f83-408f-a780-05d50da305de.png
微信图片_20240609102325.jpg



4,5.主进程接收消息开始打印,并且通知渲染进程打印状态


ipcMain.on('startPrint', () => {
printWindow!.webContents.print(
{
silent: true,
margins: { marginType: 'none' }
},
(success) => {
//通知渲染进程打印状态
if (success) {
mainWindow.webContents.send('printStatus', 'success')
} else {
mainWindow.webContents.send('printStatus', 'error')
}
}
)
})

aa.jpg



完毕~



作者:彷徨的耗子
来源:juejin.cn/post/7377645747448365091
收起阅读 »

几行代码,优雅的避免接口重复请求!同事都说好!

web
背景简介 我们日常开发中,经常会遇到点击一个按钮或者进行搜索时,请求接口的需求。 如果我们不做优化,连续点击按钮或者进行搜索,接口会重复请求。 首先,这会导致性能浪费!最重要的,如果接口响应比较慢,此时,我们在做其他操作会有一系列bug! 那么,我们该如...
继续阅读 »

背景简介


我们日常开发中,经常会遇到点击一个按钮或者进行搜索时,请求接口的需求。


如果我们不做优化,连续点击按钮或者进行搜索,接口会重复请求。




首先,这会导致性能浪费!最重要的,如果接口响应比较慢,此时,我们在做其他操作会有一系列bug!



那么,我们该如何规避这种问题呢?


如何避免接口重复请求


防抖节流方式(不推荐)


使用防抖节流方式避免重复操作是前端的老传统了,不多介绍了


防抖实现


<template>
<div>
<button @click="debouncedFetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const timeoutId = ref(null);

function debounce(fn, delay) {
return function(...args) {
if (timeoutId.value) clearTimeout(timeoutId.value);
timeoutId.value = setTimeout(() => {
fn(...args);
}, delay);
};
}

function fetchData() {
axios.get('http://api/gcshi) // 使用示例API
.then(response => {
console.log(response.data);
})
}

const debouncedFetchData = debounce(fetchData, 300);
</script>

防抖(Debounce)



  • 在setup函数中,定义了timeoutId用于存储定时器ID。

  • debounce函数创建了一个闭包,清除之前的定时器并设置新的定时器,只有在延迟时间内没有新调用时才执行fetchData。

  • debouncedFetchData是防抖后的函数,在按钮点击时调用。


节流实现


<template>
<div>
<button @click="throttledFetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const lastCall = ref(0);

function throttle(fn, delay) {
return function(...args) {
const now = new Date().getTime();
if (now - lastCall.value < delay) return;
lastCall.value = now;
fn(...args);
};
}

function fetchData() {
axios.get('http://api/gcshi') //
.then(response => {
console.log(response.data);
})
}

const throttledFetchData = throttle(fetchData, 1000);
</script>

节流(Throttle)



  • 在setup函数中,定义了lastCall用于存储上次调用的时间戳。

  • throttle函数创建了一个闭包,检查当前时间与上次调用时间的差值,只有大于设定的延迟时间时才执行fetchData。

  • throttledFetchData是节流后的函数,在按钮点击时调用。


节流防抖这种方式感觉用在这里不是很丝滑,代码成本也比较高,因此,很不推荐!


请求锁定(加laoding状态)


请求锁定非常好理解,设置一个laoding状态,如果第一个接口处于laoding中,那么,我们不执行任何逻辑!


<template>
<div>
<button @click="fetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const laoding = ref(false);

function fetchData() {
// 接口请求中,直接返回,避免重复请求
if(laoding.value) return
laoding.value = true
axios.get('http://api/gcshi') //
.then(response => {
laoding.value = fasle
})
}

const throttledFetchData = throttle(fetchData, 1000);
</script>


这种方式简单粗暴,十分好用!


但是也有弊端,比如我搜索A后,接口请求中;但我此时突然想搜B,就不会生效了,因为请求A还没响应



因此,请求锁定这种方式无法取消原先的请求,只能等待一个请求执行完才能继续请求。


axios.CancelToken取消重复请求


基本用法


axios其实内置了一个取消重复请求的方法:axios.CancelToken,我们可以利用axios.CancelToken来取消重复的请求,爆好用!


首先,我们要知道,aixos有一个config的配置项,取消请求就是在这里面配置的。


<template>
<div>
<button @click="fetchData">请求</button>
</div>

</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

let cancelTokenSource = null;


function fetchData() {
if (cancelTokenSource) {
cancelTokenSource.cancel('取消上次请求');
cancelTokenSource = null;
}
cancelTokenSource = axios.CancelToken.source();

axios.get('http://api/gcshi',{cancelToken: cancelTokenSource.token}) //
.then(response => {
laoding.value = fasle
})
}

</script>


我们测试下,如下图:可以看到,重复的请求会直接被终止掉!



CancelToken官网示例



官网使用方法传送门:http://www.axios-http.cn/docs/cancel…



const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

也可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建一个 cancel token:


const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// 取消请求
cancel();

注意: 可以使用同一个 cancel token 或 signal 取消多个请求。


在过渡期间,您可以使用这两种取消 API,即使是针对同一个请求:


const controller = new AbortController();

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token,
signal: controller.signal
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// 取消请求 (message 参数是可选的)
source.cancel('Operation canceled by the user.');
// 或
controller.abort(); // 不支持 message 参数