注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

运营:别再让你的页面一直loading 了

运营:别再让你的页面一直loading 了 第一轮 battle Q: 我想下载一个大文件,界面一直转圈,很耽误时间,我想在下载的时候还做点其他事情 A:一直转圈就一直等呗,反正还能摸会(奈何小姐姐太想做牛马了) 第二轮 battle Q: 不行,为什么别人...
继续阅读 »

运营:别再让你的页面一直loading 了


May-17-2024 15-36-38.gif


第一轮 battle


Q: 我想下载一个大文件,界面一直转圈,很耽误时间,我想在下载的时候还做点其他事情


A:一直转圈就一直等呗,反正还能摸会(奈何小姐姐太想做牛马了)


第二轮 battle


Q: 不行,为什么别人的浏览器,下载软件/文件 就能操作界面,你这就一直转圈,什么都做不了


A: 我们js 是单线程,一个时间只能做一件事,你不能在下载文件的时候,还操作界面吧...逐渐语无伦次,行,我给你试着优化优化..


image.png


最终效果


save.gif


无敌.gif


可以看到,下载文件 页面不再转圈,并且可以在界面操作,但是在点击操作1,2,到3的时候,会卡顿一下,下面会说为什么会卡这一下


开始分析



  1. 执行文件下载操作,把转圈逻辑去掉不就行了,


but: 是不转圈了,下载的时候,依然操作不了界面



  1. js 是一个单线程,一个时间只能做一件事,密集的cpu 计算,导致网站反应迟钝,就像卡了一样


resolve: 把下载文件这个耗时操作,放在其他线程操作,等到操作完毕,再通知主线程,执行完了。就像发布订阅模式一样,主线程不用执行密集的计算,也不用特意等密集计算的结果,执行完,告诉我就行了


技术使用 Web Workers


摘自 MDN developer.mozilla.org/zh-CN/docs/…


Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,它们可以使用 XMLHttpRequest(尽管 responseXML 和 channel 属性总是为空)或 fetch(没有这些限制)执行 I/O。一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。


为什么要用它:worker 的一个优势在于能够执行处理器密集型的运算



不会阻塞 UI 线程


不会阻塞 UI 线程


不会阻塞 UI 线程


不会阻塞 UI 线程


重要的事情说三遍 🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣


基本使用


主线程生成一个专用 worker


const myWorker = new Worker("worker.js"); // worker.js 是一个脚本的 URI 来执行 worker 线程

专用 worker 中消息的接收和发送


就俩主要方法 postMessage onmessage


引入脚本与库


Worker 线程能够访问一个全局函数 importScripts() 来引入脚本,该函数接受 0 个或者多个 URI 作为参数来引入资源;以下例子都是合法的:


importScripts(); /* 什么都不引入 */
importScripts("foo.js"); /* 只引入 "foo.js" */
importScripts("foo.js", "bar.js"); /* 引入两个脚本 */
importScripts("//example.com/hello.js"); /* 你可以从其他来源导入脚本 */

 ESModule 模式


const worker = new Worker('worker.js', 
{ type: 'module' // 指定 worker.js 的类型 }
);

文件下载代码



  • baseCode


import { writeFile, utils } from 'xlsx'
/**模拟生成大文件数据 */
const generateLargeFileData = () => {
const data = []
for (let i = 0; i < 10000; i++) {
data.push({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
age: Math.floor(Math.random() * 100) + 1
})
}
return data
}


  • 一只转圈的代码


/**下载大文件 */
const downloadExcel = async () => {
// 模拟生成大文件数据
const data = generateLargeFileData()
loading.value = true
// 模拟一段短暂的等待时间,确保状态更新
await delay(1000)
// 卡死的罪魁祸者
// 将数据转换为 Excel 格式
const ws = utils.json_to_sheet(data)
const wb = utils.book_new()
utils.book_append_sheet(wb, ws, 'Sheet1')
writeFile(wb, 'test.xlsx')
loading.value = false
}


  • 使用webworker,将耗时计算放到 webworker 线程,解决阻塞ui的问题


主线程



const myWorker = new Worker('downloadWorker.js')
myWorker.onmessage = (event) => {
let wb = event.data
// 这里也会占用主线程的ui渲染,所以会卡一下
writeFile(wb, 'test.xlsx')
ElMessage.success('下载任务已在后台运行,可以继续操作界面其他任务')
}

/**下载大文件 */
const downloadExcel = async () => {
const data = generateLargeFileData()
myWorker.postMessage(data)
}


worker 线程


image.png


// 非模块化文件, public 打包本身就是线上文件了
importScripts("./xlsx.js"); // 线上地址,或者本地地址

self.onmessage = (e) => {
// 将数据转换为 Excel 格式
const ws = XLSX.utils.json_to_sheet(e.data)
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
// writeFile(wb, 'test.xlsx') // 这里会操作dom, 所以将操作dom放到 主线程做
self.postMessage(wb)
self.close()
}

细节补充



  1. 本文主要介绍了专用worker,其实还有 共享 worker【主要做多页面标签通信】, ServiceWorkers 【主要做网络拦截,可以看一下之前写的pwa文章【https://juejin.cn/post/7062681470116036616】,离线缓存就是使用ServiceWorkers】

  2. 在主线程中使用时,onmessage 和 postMessage() 必须挂在 worker 对象上,而在 worker 中使用时不用这样做。原因是,在 worker 内部,worker 是有效的全局作用域(就像window.xxx ,window 一般可以不写)

  3. worker的关闭


// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.terminate(); // 关闭worker

// worker.js(worker线程) 
self.close(); // 直接执行close方法就ok了


  1. worker 错误监听 messageerror

  2. 关于主线程里的 new Worker('downloadWorker.js')


这个脚本,必须是本地/或者网络地址,这里写的是项目运行地址 匹配相应的worker。这里大家也会发现一个问题,就是这个worker是全局性的,放在public 是一个不错的选择,再者打包后,public 下本身也是会放在服务器上



  1. 用完worker, 要及时关闭,他是不会自己结束的。选择 在worker 关闭,或者主线程关闭,会有区别

  2. 其实小文件下载,用worker 有点画蛇添足,本身使用worker 也是一种消耗



详细的参考资料以及代码地址


MDN



MDN code仓库



可以下载下来直接调试,最好是起一个本地服务: http-server


image.png





代码地址


gitee.com/Big_Cat-AK-…





作者:赵小川
来源:juejin.cn/post/7369633749418934335
收起阅读 »

MybatisPlus 使用技巧与隐患

前言 MP 从出现就一直有争议 感觉一直 都存在两种声音 like: 很方便啊 通过函数自动拼接 Sql 不需要去 XML 再去使用标签 之前一分钟写好的 Sql 现在一秒钟就能写好 简直不要太方便 dislike: 侵入 Service 层 不好维护 可读性...
继续阅读 »

前言


MP 从出现就一直有争议 感觉一直 都存在两种声音


like:


很方便啊 通过函数自动拼接 Sql 不需要去 XML 再去使用标签 之前一分钟写好的 Sql 现在一秒钟就能写好 简直不要太方便


dislike:


侵入 Service 层 不好维护 可读性差 代码耦合 效率不行 sql 优化比较难


之前也有前辈说少用 MP 理由就是不好维护 但是这个东西真的是方便 只要不是强制不让用 就还是会去使用 存在集合里 最近也确实有一些体会 就从两个角度去看一下 MP


优点


操作简洁


就从我们编码中最常用的增删改查去说


按照我们之前去使用 Mybatis 的喜欢我们就要去建立一个 XML 文件 去编写 Sql 语句 算是半自动 我们可以直接去操控 Sql 语句 但是会比较麻烦 很多简单的数据查询我们都要去写一个标签 感觉这种没有意义的操作还是比较烦的 那么 MP 里面怎么实现


第一种: 最简单我们就是直接去使用提供的方法 我们非常简单就能做到这些操作 但是这个就有一个问题


nodeMapper.selectById(1);
nodeMapper.deleteById(2);
nodeMapper.updateById(new Node());
nodeMapper.insert(new Node());

维护性差 以查询为例 这个默认提供的方法都是查询所有字段我们都知道在编写 Sql 的时候第一条优化准则就是不要使用 Select * 因为这种写法是很 Low


这个就是上面selectById执行的结果


SELECT Id,name,pid FROM node WHERE Id=?

这种 Sql 肯定是不好的所以我们在使用 MP 的时候尽量不要去使用自带的快捷查询 我们可以去使用它里面的构造器


nodeMapper.selectOne(new QueryWrapper().eq("id",1).select("id"));

这汇总写法 我们可以通过后面的 select() 去指定我们需要查询的字段 算是解决上面那个问题吗 但是这个就完事了吗? 这还有一个问题


我们在开发中经常会说一个叫魔法值的东西


//这个就是魔法值 
if ("变成派大星".equals(node.getName())){
   System.out.println("魔法值");
}

之所以不要多用魔法值就是为了后期维护 我们建议使用枚举 或者建一个常量类 通过 Static final 修饰


上面那段代码是不是也有同样问题 "id"算不算魔法值呢 这种构造器产生的问题就是 不好维护


假设 我们的这Node类是高度使用的 我们到处都在写


nodeMapper.selectOne(new QueryWrapper().eq("id",1).select("id"));

刚开始没事 我们乐呵呵的 但是一旦我去修改 Id 的字段名怎么办



我修改成 test(数据库同步修改) 现在这个实体类中没有这个字段 我们再去看我们的代码



没有什么反应 没有给我提示报错 我这个时候去运行怎么办 我要一个个去找这个错误吗 这明显很费时间


这个确实是一个问题 但是也是可以解决的


Node node = nodeMapper.selectOne(new LambdaQueryWrapper().eq(Node::getId, 1).select(Node::getId));

上面这种代码就可以去解决这个问题 我们在使用的时候可以多用这个东西



一旦修改字段就会立马报错


但是 这就万事大吉了吗 NO No NO 我们要是处理稍微复杂的语句怎么办? 比如如我们字段求和 这个 LambdaQueryWrapper 还是存在限制的


如果我们想实现这种 怎么去做呢


select SUM(price_count) from  bla_order_data LIMIT 100

首先这种写法肯定是不太行的 编译不通过



除非去使用QueryWrapper



还有就是分页查询


// 条件查询
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserInfo::getAge, 20);
// 分页对象
Page queryPage = new Page<>(page, limit);
// 分页查询
IPage iPage = userInfoMapper.selectPage(queryPage , queryWrapper);
// 数据总数
Long total = iPage.getTotal();
// 集合数据
List list = iPage.getRecords();

这个还是非常简单的


简单总结


MP 在做一些简单的单表查询可以去使用但是对于一些复杂的 SQl 操作还是不要用


1、SQL 侵入 Service 的问题我们可以仿照 Mybatis 建一个专门存放 MP 查询的包


2、关于维护性 我们可以尽量去使用 LambdaQueryWrapper 去构造


3、MP 是有内置的主键生成策略


4、内置分页插件:基于 Mybatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询。


缺点


我就说一个最大的缺点就是对于复杂 Sql 的操作性很不舒服 比如我们去多表查询 你怎么去写呢


看一个例子




就是通过


@Select 注解

Mp的查询条件嵌入进去
${ew.customSqlSegment}


咱就是一整个大问号 联表老老实实去写 XML 吧 这种真的不要去用 太丑了


总结


没有过多的东西 基本都是最近看到的东西


1、复杂语句不推荐使用 MP 能用最好也别用 可读性差 难维护 使用刚开始没感觉 后期业务扩充 真的恶心的


2、可以使用 MP 中的分页 比较舒服 逐渐生成策略也舒服


3、尽量不要去使用 MP 中自带的selectById 等全表查询的方法


4、尽量使用LambdaQueryWrapper的书写形式 至少比较好维护


5、简单重复 Sql 可以用 MP。复杂 SQL 不要用




作者:臻大虾
来源:juejin.cn/post/7265624177774854204
收起阅读 »

面试官喜欢什么样的离职原因?

hi,你好,我是猿java 面试中,我们似乎总是会被问到一个敏感的话题:你从上家公司离职的原因是什么?如何机智地回答才能获得面试官的芳心呢?今天我们一起来聊一聊。 1.为什么会问离职原因? 马云曾经说过,人离职的原因主要有两种:一是钱给少了,一是心委屈了!既然...
继续阅读 »

hi,你好,我是猿java


面试中,我们似乎总是会被问到一个敏感的话题:你从上家公司离职的原因是什么?如何机智地回答才能获得面试官的芳心呢?今天我们一起来聊一聊。


1.为什么会问离职原因?


马云曾经说过,人离职的原因主要有两种:一是钱给少了,一是心委屈了!既然都已经是一个公开的问题,为什么面试官(特别是 HR)还是喜欢提起它?


其实,面试官问这个问题,绝大情况下是常规操作,担心你入职后会不会也因同样的原因离职;另一个原因是担心小部分人有违法乱纪行为被开除,所以有必要提前筛选一下。


作为打工人,对于离职原因,我们不鼓励欺骗,但绝对可以高情商回答,让那些看起来对我们不利的离职原因,可以表达得更能够被面试官接受,因此,本文总结了以下几个离职原因的表达方式:


2.常见的离职原因


这里,我们列举了几个最常见的离职理由,然后给出了低情商和高情商两种回答,假如你是面试官,也能快速作出判断,最终选择谁!


2.1 加班太严重


卷似乎已经成了国内上班的代名词,为了找到一份好工作,即便加班虐你千百次,你也要在面试官面前“违心”地表现出我待加班如初恋的态度,除非你不care这份工作或者有资本对加班NO。那么,面对加班太严重而离职,我们该如何违心地回答呢?


🚫 低情商回答:前公司加班太严重,太卷了,是头牛都受不了,我实在是卷不动,想躺平。


✅ 高情商回答:面试官,您好! 在上家公司,我能高效高质量地完成工作,但公司总会把加班时长作为一个重要的考核指标,导致很多员工为了加班而加班,效率低下,我不反对加班,但是不太认同这种低效的卷,我希望为更人性化的公司创造更大的价值。


2.2 薪资太低


雇佣关系绝大多数是建立在合理的薪酬之上,如果工作内容或者强度和薪资严重失衡却得不到调整,那么,离职的概率就会大大增加。因此,面对薪资太低离职,我们该如何回答?


🚫 低情商回答:上家公司给的工资太低了,而且一直没有调薪,做得没有动力,所以离职了。


✅ 高情商回答:面试官,您好! 我过去 3年,在公司和领导的帮助以及自身的努力下,技术能力有了质的提升,为公司做了很多降本增效的项目,领导一直很认可我,可是公司的薪资结构有一些硬指标,无法满足我的涨薪需求,所以想看看新机会,寻找一个可以长期稳步发展的平台。


2.3 领导很low


职场上,并不是每个领导都擅长管理,每个领导爬上去的原因也不尽相同,所以,如果职场上遇到优秀的领导请好好珍惜,如果遇到所谓的low领导,也不用太撕破脸,反而需要更加修炼自己的职场软技能,学会和不同的人打交道,因此,遇到low领导而离职该如何表达?


🚫 低情商回答:前领导太 low,对管理一窍不通,只会吹嘘拍马,跟着他看不到前途,所以离职。


✅ 高情商回答:面试官,您好! 因为前公司的业务过于稳定,大部分人每天工作都是在重复,我希望进入一个持续学习和提升的平台,接受更多的挑战。


2.4 被裁员


口罩事件之后,裁员弥漫在每一个公司的角落,恐惧也充斥着每个打工人的心里,曾经香饽饽的互联网,如今裁员潮不断,而这些裁员不管你能力有多突出,加班有多厉害,只是纯粹的不赚钱,所以,面对裁员,我们要如何表达自己的无辜?


🚫 低情商回答:我在之前公司的 KPI不行,被公司裁员了。


✅ 高情商回答:面试官,您好! 前公司业务有很大的调整,想让我调岗到其他业务线上,而我个人很想在xxx领域深耕,贵公司的这个岗位和我现在的很匹配,很符合我的职业规划。


2.5 无法晋升


晋升在一定程度上是对员工的认可,同时也是改善待遇的窗口,如果一直都在努力付出却得不到晋升机会,难免会打击工作的积极性,如果,当工作了几年之后,能力提升了很多,却一直得不到晋升,这种离职理由该如何来表达?


🚫 低情商回答:我在前公司做牛做马这么多年,结果晋升的都是老板的亲信,晋升无门,我只能选择离职。


✅ 高情商回答:面试官,您好! 我在前公司一直很受领导重用,负责过多个核心业务,领导多次提名我晋升,但都被突发原因夭折了,而我不想安于现状,想找一个更能发挥自己才能的平台。


2.6 和同事相处不和谐


职场上,最重要的事情就是与人相处,和同事一起成事,与人打交道绝对是一门终身的必修课,如果是因为和同事相处不和谐离职,如何才能更好地表达出不是因为自己不能融入集体?


🚫 低情商回答:前公司的同事们都很奇葩,处不到一个好朋友,太孤单了所以离职。


✅ 高情商回答:面试官,您好! 我在前公司的沟通能力一直被领导认可,但因为公司内耗较大,很多工作都花在无效的环节上,所以希望找一个氛围好,内耗低的团队长期发展。


通过上述低情商和高情商的回答对比,我们就可以很清晰的感受到语言的魅力,同样一个理由,在不说谎的前提下,只要我们可以表达的更好,面试官很多也是打工人,他们也能共情到,所以,如果你能高情商的回答,只要不是技能能力不过关,一般也很难被面试官淘汰。


3.一些现象和建议


经历过口罩事件后经济下行的这几年,领悟了很多,也看到了很多奇怪的现象:


职场上,没有谁是不可替代的


求职时,很多面试官都是曾经和自己一起奋斗的同事


入职后,发现直属领导或者更上级的领导,曾经是下属


给予援助之手的,绝大多数是曾经和你关系好或者认可你的人


给打工人的一些建议:


选择了一份工作就要全力以赴,不遗余力的提升自己,不要太计较外在因素,这些拼搏绝对是你日后无形的财富。


日常工作中,一定要和同事保持友善互助的关系,尽量交往几个能相互正向提升的同事,日后他们可能就是你的贵人。


维护好与领导的关系,不论他优秀与否,相互磨合,相互成就,说不定他日后就可以帮到你。


如果你是领导,请善待自己的下属,这个时代变化太大,三十年河东,三十年河西,与人方面就是与己方便。打工人需要相互帮扶!


最后,每个人的阅历不一样,上面 4个建议不一定每条都适合,但是绝对有一个可以受益。




作者:猿java
来源:juejin.cn/post/7375086929404837914
收起阅读 »

入职第一天,看了公司代码,牛马沉默了

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
继续阅读 »

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

打开代码发现问题不断

  1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

image.png

image.png

image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

prop_c.setProperty(key, value);

value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
  1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
  1. 日志打印居然sout和log混合双打

image.png

image.png

先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

5.随意更改生产数据库,出不出问题全靠开发的职业素养;

6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

<type>pom

来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

那有什么优点呢:

  1. 不用太怎么写文档
  2. 束缚很小
  3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

解决之道

怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


作者:小红帽的大灰狼
来源:juejin.cn/post/7371986999164928010
收起阅读 »

从江西到北京2000公里 在少年得到找到了第一份归宿 追求自己的北漂梦

2000公里来到北京,追求自己的北漂梦 哈喽哈喽,大家好,我是你们的金樽清酒。在经历了很多的面试之后,我找到了第一份实习,从江西到北京两千公里,也就是一张车票和一份努力。这篇文章可能不带太多的技术性,都是我个人到北京到公司的一些感悟。 火急火燎的入职 收到公司...
继续阅读 »

2000公里来到北京,追求自己的北漂梦


哈喽哈喽,大家好,我是你们的金樽清酒。在经历了很多的面试之后,我找到了第一份实习,从江西到北京两千公里,也就是一张车票和一份努力。这篇文章可能不带太多的技术性,都是我个人到北京到公司的一些感悟。


火急火燎的入职


收到公司的的offer之后,确定入职的时间,我最快的入职时间就是学校的考试之后,四点多考完之后,收拾收拾就开始赶高铁。从抚州到南昌,再到南昌坐地铁到火车站,说实话,这是我第一次坐地铁,蛮新奇的,后面也基本每天跟地铁打交道了,可能这是从小城市来到大城市的新奇感吧。


第一次坐上火车。怎么说呢,一言难尽,只能用彻夜难眠来解释。火车上人声的嘈杂,颠簸以极其难闻的味道,但是怎么说,内心也有点激动,毕竟火车的终点是大都市北京。到了北京,前往在网上看好的房子,一个小单间,怎么说,初到北京只能住小单间了,但是老实说我还住过更差的房子,在暑假做家教的时候,一个没有窗户的暗无天日的小房子,关灯就是黑夜,暗无天日,所以说在北京的房子我感觉还好啦。由于赶着入职,把行李给房东就赶着去公司了,一身的疲惫。


坐地铁去公司。老实说,早高峰的北京地铁,就是脸贴脸的那种。以后,每次坐地铁都是站在门口。由于不认识路,一路误打误撞来到公司办入职。各种的入职手续,领了电脑之后就被hr带到工位。老实说,在北京也就公司能给我一点安慰。我们公司没有打卡,完成自己手头上的事情就可以下班,一切都是约定俗成但是井然有序。公司里面的人也特别的随和,哈哈哈哈哈哈,第一天来做了好多个自我介绍。


然后,我就认识了我的老师,应该叫mentor吧。


mentor对我的教诲


第一天mentor交给我的任务就是配置环境。说实话,这是我第一次配置环境。mentor说自己配环境的机会不多,你想想自己配置环境的机会有多少。我就开始按照网上的教程一步步的摸索如何配置node,怎么用n来管理node的版本。
第一天,mentor给我单独开了个会,列出了我这一周要学习的东西。我都拿小本本记下来了。


WechatIMG1.jpg


WechatIMG2.jpg


从配置环境到运行项目到开发流程。然后我就开始了开发的前置准备。


第一天,我只完成了环境的配置。然后就要把项目拉去下来运行项目了。当天开了四个会,一个早会,一个周总,一个部门的技术分享,还有一个是mentor叫我留下来,总结今天的学习。然后就给我解决了项目运行不了的问题,原来是我的node版本过低,为什么要用n或者nvm进行node的版本管理呢?因为不同的项目依赖的node版本不一样。然后又交代给我一个任务,找到一个页面,看懂里面的每一行代码,讲给他听。然后mentor就会把里面我不会的讲解给我听。然后mentor拉我去开评审和复述的会,开始写项目排期了。排期是一项技术活,不能太长或太短,不要给自己留空窗期,以及不能到测试时间完不成任务,他给我盯着排期呢。以后自己排期可是一项技术活。


现在是开发的第二天了,每天都要review今天的代码,mentor给我指出了很多改进的地方。代码书写的规范,以及写注释,性能优化等。比如一些v-if可以用v-show。为什么,因为可以减少回流重绘。还好,九天的任务,我二天就完成一大半,到联调的阶段了,但不是我有多强,而是我在暑假的时候实习过一个公司写过后台管理,以及mentor叫我看懂每一行代码和element-ui组件库,没开玩笑我现在强的可怕。加油,成长的每一天。


在北京的难点


资金窘迫。去的火车票,押一付一的房租,以及提前15天交租,以及各种七七八八的,还得来回学校处理一些事情,来来回回,七七八八,说实话还没赚钱真的喘不过气。有钱男子汉,没钱汉子难啊。每天要挤的地铁,在外面的孤独感。以及不合口味的饮食。北京偏甜,我是江西萍乡的,属于很能吃辣的,到北京感觉都是甜的。mentor知道我吃辣,给我一罐辣酱,说很辣,他蘸一点点都辣的不行。我吃了,额,甜的,一点辣味都没有。唉,生活总是如此的艰难,还好公司不打卡,能学到很多东西,可以摸鱼。加油吧,开心点向前看,在这个世界上谁不难呢,身边基本都是北漂,可以说北京很包容啦。


总结


初到北京很激动,我很高兴到少年得到这家公司,我会在这家公司好好实习,提升自己的能力,完成自己的北漂路,人生艰难,但是依旧值得。给你们看看我的公司吧,我还看到了泉灵老师哦,跟我们一起上下班。


WechatIMG3.jpg


WechatIMG4.jpg


WechatIMG5.jpg


WechatIMG6.jpg


作者:jinzunqinjiu
来源:juejin.cn/post/7371633297154621494
收起阅读 »

小毛驴 40km 通勤上班:不一样的工作日!

从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。 所以通勤方案就变成了: 上班: 小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min + 步行 5min 下班: 公交 12min ----- 地铁 ...
继续阅读 »

从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。


所以通勤方案就变成了:


上班:

小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min + 步行 5min

下班:

公交 12min ----- 地铁 40min ----- 小毛驴 15min

通勤费用: 小毛驴一块钱充电可以开两天。地铁 + 公交 来回 12块。


这半年下来地铁已经坐够够了。🤦‍♂️ 有的时候实在是不想坐了。就动了开小毛驴的心思。


但是百度地图看从家到公司的距离是 34km。之前公司到家的百度距离是 18km,其实等于翻翻了。


而且之前的路况很好么有什么红绿灯而且路上的人也很少。所以基本没有什么时间浪费18km大概半个小时左右就到了。


本来是想直接买一个新电瓶车来通勤用的,但是碰到那个什么新国标要去考摩托车驾-照就耽搁了。


然后正好这两天天气还行不冷不热。我就想要买今天就开小毛驴去公司得了。正好熟悉下路况。


早上还是按照正常出门的时间 7.25 出门。然后按照百度导航直接走。因为第一次开,路况不熟悉。按照百度走的路线全是走的人多的地方。早上正好又是上班高峰期。非机动车道上全部都是人。而且路上的红绿灯贼多。基本遇到一个红绿灯就要停下来。


前半程车的电量充足速度可以很快,但是路况太差了。路上人太多,而且有占着超车道一直慢悠悠的。开的血压飙升。所以就导致速度起不来。然后到了后半程的时候全是大路。而且没有什么红绿灯也没啥人,但是电量下去了,速度又上不来。脑壳痛!


最后到公司楼下的时候是 8.42。百度地图显示 34km 需要 2 小时零五分。实际电瓶车里程显示 40km ,耗时一小时 20 分。


其实 1 小时开车的时间是感知不到的。前半程因为都是人所以精神高度集中。


另外路上的风景也是不错的。可以走之前没有走到的地方。可以愉快的画图。


下面早上的时候拍的,因为第一次。怕时间不够。就随便瞎拍了两张记录了一下。


IMG_20240428_105957.jpg


IMG_20240428_105852.jpg


IMG_20240428_110048.jpg


IMG_20240428_104954.jpg


IMG_20240428_110017.jpg


等会晚上回去的时候看看能不能走另外一条路会不会快点。


IMG_20240427_221436.jpg


IMG_20240427_221603.jpg


MVIMG_20240426_192534.jpg


IMG_20240427_205345.jpg


IMG_20240427_222136.jpg


IMG_20240427_221712.jpg


IMG_20240427_222732.jpg


IMG_20240427_221628.jpg


IMG_20240427_221326.jpg


IMG_20240427_221537.jpg


作者:执行上下文
来源:juejin.cn/post/7362729128476524563
收起阅读 »

一边敲代码一边晋升宝爸

2024年农历三月初一,我成功晋升为宝爸。 这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。” 看着这个陌生的、小小的男人,我忐忑的心变得安宁。 在这个世界,我们彼此相遇,有点不知所措,如在梦中。 他嘹亮的啼哭,让...
继续阅读 »

成品.jpg


2024年农历三月初一,我成功晋升为宝爸。


这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。”


看着这个陌生的、小小的男人,我忐忑的心变得安宁。


在这个世界,我们彼此相遇,有点不知所措,如在梦中。


他嘹亮的啼哭,让我的心弦随之颤动。


我一动不动的看着他,不敢摸他。


浓浓的血脉,注定了这一世的羁绊。


……


现在是熊宝出生的第21天了,在护理师的帮助下,我学会了抱娃、拍嗝、换尿布,算是初步顺上手了。


至此,我可以有些时间写点生娃的回忆和经验,分享给大家。


我的爱人是在2023年7月份怀孕的。


清晰记得那天早晨,我爱人跟我说她已经40多天没有来月经了,她可能怀孕了。


我赶紧在美团买了个验孕棒,一测,出现了两条红线,心中大喜。


为了万无一失,我又带她去医院检查,医生说:“恭喜你,有喜了。”


我长吁了一口气,结婚后的一年里,我爱人老担心这事。


这个时候,我还在山东老家装修房子,想着老来得子,要谨慎一些,所以就计划去北京备产。


北京消费高,以我当前的经济实力,得找份工作才行。


所以我就去boss 上看工作,看到滴滴有WebGL工程师的坑,就投了份简历,然后大大小小的面试了五六场才过。


我工作的地址是中关村壹号,所以,我就近在大牛坊租了个窗户很大的主卧,孕妈的建档就建在了上地医院。


上地医院不算三甲,但它的整体服务和环境都挺好的,产科是它的特色,看病的人不少,但也不会特别多,一般不需要排太久的队。


就这样,我开始了白天给公司当孙子,晚上给爱人当孙……,额,是守护神的生活。


孕初的几个月,我们会每两周去一次医院孕检。什么时候孕检,医生都会提前告知我们,而且孕检手册也会告诉我们整个流程。


有的孕检是需要验血的,这时就需要空腹。一般我们会很早起床,把水杯和牛肉干装进背包里,然后去医院。


孕妇饿久了、抽血多了,很容易心慌头晕,再加上孕期情绪敏感。所以,每次孕检我都会陪着她,陪她早去,避免她饿太久,验完血后,我会先给她吃点牛肉干垫垫,然后再去附近找好吃的。


在之后的时间里,一切还算顺利,熊宝在妈妈的肚子里慢慢的从一颗种子长成鹌鹑蛋、小鸡蛋、小苹果……


在他有小木瓜那么大的时候,他学会了玩脐带,并且成功的在自己的脖子上缠了一周。


那段时间,熊宝的小脑延髓池还长得有点快,都0.9了,规定的最大值是1.0,这让我们很是担心。


我上网查资料说小脑延髓池的值太大会脑子进水,变成一个傻宝。


幸亏最后都稳定住了,没有再长。


在孕期第六个月的时候,也就是2023年年底,我们开始考虑胎儿的生产和月子问题。


我们是没啥经验的,所以想着找个月嫂,或者找个月子中心,房子还得找个至少两居室的,不能再合租了。


这一堆算下来并不便宜,北京的金牌月嫂是3万,普通的也得2万。中高级的月子中心是7万到10万。两居室的房子8千/月。


这对于我在滴滴的薪资来说倒也还好,可问题是年底的时候我不想在滴滴干了。


原因是我所在的HMI部门的管理出了问题,再干下去会很浪费生命。


思虑再三,我给卡尔动力的老板提了一些建议后,就离职了。


卡尔动力是我去滴滴时,刚从滴滴脱离出来的创业公司,我在其中负责Web端的三维可视化。


在我离开公司前,老板把他的微信给我了,我们加了个好友,现在还保持着联系,希望以后会有合作。


至于我之前所在的那个HMI部门,它在我离开没多久就被打散重组了,组长也被撤了。很佩服老板的雷厉风行。


其中太具体的事情我就不再多说了,咱们继续谈生娃的事。


我离开滴滴后,就把大牛坊的房子退了,东西都寄回了老家。


接下来,我们计划在山东的潍坊老家生娃。


之所以如此,有多方面的考虑:


我们已经在北京完成了孕前和孕中的检查,胎儿很健康,所以没必要再纠结于北京。


潍坊有多个三甲医院,其医疗技术虽然比不得北京,但生娃也不是那种只有一线城市才能做的、技术难度很高的事。


北京消费高,没较高的收入就不适合再待在北京了,那时我租的房子也正好到期。而潍坊的消费是很低的,我们每天120元就可以在医院对面短租很精致的小米全屋智能公寓。潍坊中高端的月子中心一个月2万。


我刚好接到了我上上家公司的一个单子,可以在老家工作,小赚一笔。有很多时候,我离开公司,并不一定是我和公司闹掰了,其原因很多的。


我爸妈在老家自己种着蔬菜,有鸡鹅牛羊,自己家的东西吃着放心。


就这样,我们在北京待了四个月后,回到了潍坊老家。


在老家的日子里,孕妈的饮食都挺好的。我们吃东西前,都会从一个叫“孕育树”的APP上查查孕妈能不能吃。


其实,查一种食物是否适合孕妈吃是很简单的,但难的是你可能会忘了查,或者自以为不用查。


以前我爱人就有过几次,比如煮鸽子汤的时候放了某一种中药,喝了两三次后才想起查一下,结果发现那种中药容易让孕妇流产;还有一次,我对象以为自己可以吃桂圆,买了一堆后,我给她一查,发现孕妇不能吃。


孕妈在怀孕的时候,很容易傻傻的可爱,这需要我们多上点心。


我家孕妈的精神状态一直是我比较担心的,因为她有点熊,熊孩子的熊,再加上有几个词叫“产前抑郁”和“产后抑郁”,所以我会格外注意和防范她的情绪问题。


我会时刻告诉自己不能让她生气,学会换位思考,照顾好她的方方面面,她说得都对,不因小事而计较,不轴,不抬杠,努力逗她开心。


除此之外,赚钱也很重要,因为很多时候钱是可以换来快乐和舒适的。


在这期间,我给我工作过的上上家公司开发了一个三维机器人的交互展示项目,基本上能够后面的开支。


我还把去滴滴时遇到的面试题做成了一个低价的付费课-《canvas进阶-面试题》,想着以后多多少少给熊宝赚点奶粉钱。


每天我也依旧会拿出一点时间去学习,让自己保持一个持续成长的状态。


与此同时,熊宝在妈妈的肚子里也持续成长,长成了一个小西瓜。


熊宝玩脐带的能力也进步了,他成功把脐带在自己脖子上又绕了一周,成为了绕颈两周。


直到熊宝的脑袋入盆的时候,还是两周,医生说:“你们别再想让他绕回来了,当然,也不用担心他再绕更多了。”


在这期间,我们一感觉熊宝不咋蛄蛹了,就赶紧用自己买的胎心仪测测,生怕他因为绕颈两周而缺氧。


有的时候熊宝很皮,半天不动,胎心还换了位置,我们常常大半夜的测胎心测好久,都快把她妈吓哭了,直到在一个思维盲点听到强劲有力的小火车声,才放下心来。


在孕妈离预产期还有20天的时候,我们住在了潍坊阳光融合医院对面的一间环境舒适,干净卫生,可以洗衣做饭的小米全屋智能公寓里。


在这个时候,孕妈基本上就是随时可以生的了,所以我们需要住在医院旁边,以防突发情况。


我们住下来的当天,还去潍坊妇幼保健院做了孕检,检查结果并不理想,医生说:“胎心加速不及格,若一直这样,就得刨宫产。”


我爱人当时就被吓哭了,我们一直都想顺产的,我不想在她的肚子上开一道口子。


我从网上查了一下,导致胎心加速不行的原因是有很多种的,比如胎儿睡着了,或者妈妈没吃好,胎儿饿着了,不想动。


这天我爱人确实没吃好,我就跟她说:“我们去吃火锅吧,没有什么是一顿火锅解决不了的。”


于是我就带着哭得梨花带雨的孕妈吃了顿火锅。


吃完后,我们没有去潍坊妇幼保健院孕检,而是就近去了我家对面的阳光融合医院。


在阳光融合医院,医生说胎心加速及格了,虽不是那么理想,但也没有问题。


至此,我们搬来医院对面的第一天可以睡个好觉了。


一周后的上午,我们又去潍坊妇幼保健院做了孕检,胎心加速还是不理想,医生让我们下午住院,观察情况,可能要刨宫产。


我没住,我们去阳光融合医院又做了一次孕检,结果胎心加速还是合格的,所以就没去住院。


我当时的想法是,阳光融合和潍坊妇幼保健院都是三甲医院,只要有一个测着可以不刨,我们就不刨。


我们也想过为什么两个同样三甲的医院的测试结果不一样,其原因也不一定是哪个医院不行。


也可能是因为我去潍坊妇幼保健院都是上午去的,而去阳光融合医院都是下午或晚上去的。


我在滴滴的时候,回家晚,睡得晚,起得晚,熊他妈也一定要等我回来才睡。这让熊宝在娘胎里面变成了一个小夜游神。


熊宝在上午的时候总是老老实实的不咋动,等到了晚上就总在妈妈肚子里手舞足蹈。


所以现在上午去医院测胎心的时候,熊宝可能还没起床,等晚上测胎心的时候,就来了精神了。


记得离预产期还有10天左右的时候,我们还是上午去潍坊妇幼做孕检,医生直接要让我们下午就做刨宫产,她说:“原因有五:胎心加速不行,绕颈两周,产龄偏高,做过锥切,已经临近预产期。”


我没有照做,我带熊他妈又去吃了一顿火锅,然后睡了一个午觉,养足了精神,就去了潍坊人民医院。


潍坊人民医院是综合性医院,属于潍坊医院里的老大。


我们找到了经验丰富的胡明英医生做检查,胡明英医生是一位很有名望的医生,她本应退休却又被返聘回去了,只因为她当了妇产医生后,就闲不住了。


胡明英医生给我们做了全面细致的检查,得出以下结论:


● 胎心加速是合格的。


● 绕颈两周并不算太大的问题,因为小孩在顺的时候,还是可以转出来的,当然这也并非绝对。


● 脐带的血液流速和供氧都没问题,并未受绕颈两周的影响。


● 孕妇年龄35,并没有超出可以顺产的年龄范畴。


● 微小面积锥切,且并非疤痕体质,并不影响顺产。


两天后,胡明英医生又给孕妈做了骨盆检查,最终结论是:可以顺产,等瓜熟落地即可。


接下来胡医生就给孕妈开了住院单,当孕妈出现规律宫缩的时候,就可以直接来住院。


期间,我们又去阳光融合查了一次,得到的结果依旧是可以顺产,如此我们才算放心。


虽然我不懂医术,但基本逻辑我还是懂的。之前潍坊妇幼保健院在未经全面、细致分析的前提下,让孕妈当天下午就做刨宫产的行为有些武断了。


2024年4月8日的晚上,孕妈发生规律性宫缩,大约每隔半个小时一次。


我们立刻去了对面的阳光融合医院的急诊楼做检查,医生说快生了,让我们立刻住院。


我说我在胡明英医生那里挂了号,我们要去潍坊人民医院。


阳光融合的医生说她是胡明英医生的学生,让我们放心去潍坊人民医院就行,别再换其它地方了,现在的孕妈快生了,不能再折腾。


孕妈从规律宫缩到可以顺产,还会经历至少几个小时的开指时间,所以我花个十五分钟去潍坊人民医院的时间还是有的。


如果这个时候是孕妈的羊水破了,我就会选择直接在阳光融合医院顺产,当然,这个时候的医生也肯定不会再让我们走了。


我们去了潍坊人民医院后,就拿着住院单住进了候产房。


这时候的孕妈已经五六分钟宫缩一次了,而且疼感很强烈。


医生让她等着,等着开指。


这个过程孕妈很痛苦,她一直疼到第二天晚上,已经有些虚脱了。她虚弱的躺在床上,每次宫缩都会大口喘气,看着很让人揪心。


我一点点的喂她吃着晚饭,她知道后面的顺产需要体力,强忍着剧痛一点点吃掉我喂她的西蓝花、菠菜和馒头。


她每次宫缩,我都会给她按摩腰部,这样可以缓解一下她盆骨松动的痛苦。


在夜间两点的时候,她把吃过的晚饭都吐了,她有气无力跟我说:“我生不动了,我没力气了,你去跟医生说吧,让我刨吧。”


我紧紧的握着她的手说:“你再坚持一下吧,你想想我们为了顺产,经历了那么多,我们在3个医院间周折往复,你现在刨的话就前功尽弃了。”


我稳定住她的情绪后,就去了护士站,跟医生说:“11号床有点撑不住了,你可以去看看吗?”


于是医生就去给她做了检查,跟我说:“她快开到三指了,可以打无痛了。”


医生把她连床带人一起推进了产房。


我站在产房门口,不能进去。


那一夜我没有睡,这是我第一次连着两个晚上没有睡觉,却毫无睡意。


早晨6点左右,医生跟我说:“她打完无疼后,已经睡了一会,现在醒了,你去给她买点早餐,她预计中午能生。”


此时我的心算是稍微放下了一些,给她买了几个素包子和小米粥,外加一瓶脉动。医生说能量型饮料可以给她快速补充体力。


在忐忑等待的时间里,我还跟医院签了一个脐带血的储存协议,医生说以后孩子遇到了白血病、肝硬化等病,可以用脐带血治疗。


我虽不希望有那么一天,但多一份保障还是好的。


一直等到上午十点多的时候,医生终于告诉我生了,母子平安!


从此,我也是有娃的人了。


接下来,我们在医院的单间住了四天,没什么问题后,就去了月子中心,准备在月子中心住上28天。我因为没什么经验,为确保万无一失,只能花钱解决一些问题了。


在之后的日子里,我会研究一下育儿之道,看看《好妈妈胜过好老师》,同时努力赚钱养娃。


后面有啥心得,我会再分享给大家。


最后给大家总结一下我这一路走来的经验:


● 尽量让孕妈规律作息,不要像我似的把熊宝养成了小夜游神。


● 孕妈吃的每一种食物都要提前查一下能不能吃。


● 孕妈情绪很重要,一定要百般呵护,比如孕检的时候要全程陪伴,不要惹她生气。


● 孕妈吃啥和该怎么活动,网上都有,我觉得这是比较简单的。


● 当孕妈接近预产期,规律宫缩的时候,一定要立刻去医院,这很重要,千万别拖,即使这是在晚上你睡得正香的时候。


今年上海就有个宝妈晚上规律宫缩,拖到了早上才去医院,结果堵车了,还没进医院就把宝宝生车里了,但胎盘没出来,这极其危险。还好最后母子平安。


● 尽量顺产。如果有医院让你刨,除非紧急情况,不要立刻刨,尽快多换几家更好的、至少三甲的医院看看。


我尊重医生,但我并不觉得每个医生都是白衣天使。就像曾经魏则西事件,还有今年北京积水潭医院原院长田伟落马,这都说明职业和权利并不会决定人之善恶。


与此同时,我们也不要拿网上看来的知识来挑战医生的专业,网上知识仅供参考,具体怎么做要听医生的。不过,这与我多找几个更专业的医生问问并不冲突。


● 努力赚钱,有很多事都是可以用钱来解决的。


作者:李伟_Li慢慢
来源:juejin.cn/post/7367174168599150602
收起阅读 »

如何让不同Activity之间共享同一个ViewModel

问题背景 存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据 提出假设的手段 可以定义一个ViewModel,让这两个Activity去共享这个ViewModel 存在的问题 根据不同的Lifecycle...
继续阅读 »

问题背景


存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据


提出假设的手段


可以定义一个ViewModel,让这两个Activity去共享这个ViewModel


存在的问题


根据不同的LifecycleOwner创建出来的ViewModel是不同的实例,所以在两个不同的Activity之间无法创建同一个ViewModel对象


问题分析


先来梳理一下一个正常的ViewModel是怎么被构造出来的:



  1. ViewModel是由ViewModelFactoty负责构造出来

  2. 构造出来之后,存储在ViewModelStore里面
    但是问题是ViewModelStore是 和 (宿主Activity或者Fragment)是一一对应的关系
    具体代码如下


@MainThread  
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val owner by lazy(LazyThreadSafetyMode.NONE) { ownerProducer() }
return createViewModelLazy(
VM::class,
{ owner.viewModelStore },
{
(owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
?: CreationExtras.Empty
},
factoryProducer ?: {
(owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelProviderFactory
?: defaultViewModelProviderFactory
})
}

看到上面的代码第9行,viewModelStore和owner是对应关系,所以原则上根据不同LifecycleOwner无法构造出同一个ViewModel对象


解决思路



  1. 无法在不同的LifecycleOwner之间共享ViewMode对象的原因是:ViewModel的存储方ViewModelStore是和LifecycleOwner绑定,那如果可以解开这一层绑定关系,理论上就可以实现共享;

  2. 另外我们需要定义ViewModel的销毁时机:


    我们来模拟一个场景:由Activty1跳转到Activity2,然后两个Activity共享同一个ViewModel,两个activity都要拿到同一个ViewModel的实例,那这个时候ViewModel的销毁时机应该是和Acitivity1的生命周期走,也就是退出Activity1(等同于Activity1走onDestroy)的时候,去销毁这个ViewModel。



所以按照这个思路走,ViewModel需要在activity1中被创建出来,并且保存在一个特定的ViewModelStore里面,要保证这个ViewModelStore可以被这两个Activity共享;


然后等到Activity2取的时候,就直接可以从这个ViewModelStore把这个ViewModel取出来;


最后在Activity1进到destroy的时候,销毁这个ViewModel


具体实现


重写一个ViewModelProvider实现如下功能点:



  1. 把里面的ViewModelStore定义成一个单例供所有的LifecycleOwner共享

  2. 定义ViewModel的销毁时机: LifecycleOwner走到onDestroy的时机


// 需要放到lifecycle这个包,否则访问不到ViewModelStore
package androidx.lifecycle

class GlobalViewModelProvider(factory: Factory = NewInstanceFactory()) :
ViewModelProvider(globalStore, factory) {
companion object {
private val globalStore = ViewModelStore()
private val globalLifecycleMap = HashMap<String, MutableSet<Lifecycle>>()
private const val DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"
}

@MainThread
fun <T: ViewModel> get(lifecycle: Lifecycle, modelClass: Class<T>): T {
val canonicalName = modelClass.canonicalName ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
return get(lifecycle, "$DEFAULT_KEY:$canonicalName", modelClass)
}

@MainThread
fun <T: ViewModel> get(lifecycle: Lifecycle, key: String, modelClass: Class<T>): T {
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
throw IllegalStateException("Could not get viewmodel when lifecycle was destroyed")
}
val viewModel = super.get(key, modelClass)
val lifecycleList = globalLifecycleMap.getOrElse(key) { mutableSetOf() }
globalLifecycleMap[key] = lifecycleList
if (!lifecycleList.contains(lifecycle)) {
lifecycleList.add(lifecycle)
lifecycle.addObserver(ClearNegativeVMObserver(lifecycle, key, globalStore, globalLifecycleMap))
}
return viewModel
}

private class ClearNegativeVMObserver(
private val lifecycle: Lifecycle,
private val key: String,
private val store: ViewModelStore,
private val map: HashMap<String, MutableSet<Lifecycle>>,
): LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
val lifecycleList = map.getOrElse(key) { mutableSetOf() }
lifecycleList.remove(lifecycle)
if (lifecycleList.isEmpty()) {
store.put(key, null)
map.remove(key)
}
}
}
}
}

具体使用


@MainThread  
inline fun <reified VM: ViewModel> LifecycleOwner.sharedViewModel(
viewModelClass: Class<VM> = VM::class.java,
noinline keyFactory: (() -> String)? = null,
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null,
)
: Lazy<VM> {
return SharedViewModelLazy(
viewModelClass,
keyFactory,
{ this },
factoryProducer ?: { ViewModelProvider.NewInstanceFactory() }
)
}

@PublishedApi
internal class SharedViewModelLazy<VM: ViewModel>(
private val viewModelClass: Class<VM>,
private val keyFactory: (() -> String)?,
private val lifecycleProducer: () -> LifecycleOwner,
private val factoryProducer: () -> ViewModelProvider.Factory,
): Lazy<VM> {
private var cached: VM? = null
override val value: VM
get() {
return cached ?: kotlin.run {
val factory = factoryProducer()
if (keyFactory != null) {
GlobalViewModelProvider(factory).get(
lifecycleProducer().lifecycle,
keyFactory.invoke(),
viewModelClass
)
} else {
GlobalViewModelProvider(factory).get(
lifecycleProducer().lifecycle,
viewModelClass
)
}.also {
cached = it
}
}
}

override fun isInitialized() = cached != null
}

场景使用


val vm : MainViewModel by sharedViewModel()

作者:红鲤驴
来源:juejin.cn/post/7366913974624059427
收起阅读 »

用了这么久SpringBoot却还不知道的一个小技巧

前言 你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。 你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。 那么SpringBoot本身提供...
继续阅读 »

前言



你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。




你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。




那么SpringBoot本身提供了一个小技巧,很多人估计没用过。



正文


1、效果



废话不多说,先写个service和controller展示个效果最实在。




来个简单的service



@Service
public class TestService {

public String test() {

System.err.println("Hello,Java Body ~");
return "Hello,Java Body ~";
}
}


再来个简单的controller



@RestController
@RequestMapping("/api")
@AllArgsConstructor
public class TestController {

private final TestService testService;

@GetMapping("/test")
public ResponseEntity test() {
return ResponseEntity.ok().body(testService.test());
}
}


接下来是不是以为要启动调接口了,No,在SpringBoot的启动类中加这么个玩意儿



@SpringBootApplication
public class JavaAboutApplication {

public static void main(String[] args) {
SpringApplication.run(JavaAboutApplication.class, args);
}

@Bean
CommandLineRunner lookupTestService(TestService testService) {
return args -> {

// 1、test接口
testService.test();

};
}

}


启动看下效果



4.png



可以发现,SpringBoot启动后,自动加载了service的执行程序。




这个小案例是想说明什么呢,其实就是CommandLineRunner这么个东西。



2、它是什么



CommandLineRunner是一个接口,用于在Spring Boot应用程序启动后执行一些特定的任务或代码块。当应用程序启动完成后,Spring Boot会查找并执行实现了CommandLineRunner接口的Bean。




说白了,就是SpringBoot启动后,我立马想干的事,都可以往里写。



3、我用它做过什么



我的话,和很多厂家对接过接口,在前期不会直接开始写业务,而是先调通接口,再接入业务中。




比如webservice这种,我曾经使用CommandLineRunner直接调对方接口来测试,还挺舒适,也节省了IDEA资源,但要注意调试完成后注释掉,本地测试的时候再打开就行。



5.png


4、它还有哪些用途



除了可以拿来调试第三方接口,它还有什么用途吗?




其实开头已经说过,它就是SpringBoot启动后,你立马想干的事,都可以在里面写,所以你完全可以发挥想象去用。




我这里,提供几个思路作为参考。



1)、数据库初始化


你可以使用CommandLineRunner来执行应用程序启动时的数据库初始化操作,例如创建表格、插入初始数据等。



2)、缓存预热


CommandLineRunner在应用程序启动后预热缓存,加载常用的数据到缓存中,提高应用程序的响应速度。



3)、加载外部资源


加载一些外部资源,例如配置文件、静态文件或其他资源。CommandLineRunner可以帮助你在启动时读取这些资源并进行相应的处理。



4)、任务初始化


使用CommandLineRunner来初始化和配置某些定时任务,确保它们在应用程序启动后立即开始运行。



5)、日志记录


SpringBoot启动后记录一些必要的日志信息,如应用程序版本、环境配置、甚至启动时间等等,这个看具体需求。



6)、组件初始化


你可能需要按照特定的顺序初始化一些组件,CommandLineRunner可以帮助你控制初始化顺序,只需要将它们添加到不同的CommandLineRunner实现类中,并使用@Order注解指定它们的执行顺序即可。



总结



其实,能用的地方挺多,我最后再举个例子,netty启动时,往往是绑定了端口并以同步形式启动。




但如果要和SpringBoot整合,我们不可能还那么做,而是交给SpringBoot来控制netty的启动和关闭,当SpringBoot启动后,netty启动,当SpringBoot关闭时,netty自然也关闭了,这样才比较优雅。




那么,我们完全可以将netty的启动执行程序放到CommandLineRunner中,这样就可以达到目的了。




没用过的xdm,今天学会一个新知识点了不,可以自己下去试试哦。


作者:程序员济癫
来源:juejin.cn/post/7273434389404893239
收起阅读 »

程序员工作七年后的觉醒:不甘平庸,向上成长,突破桎梏

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。 看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。


看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,在约束着你。


上学时,你要好好读书,争取考上985/211,最起码上个一本。


工作后,大家都羡慕考公上岸的,上不了岸的话,你需要找一个好公司,拿到一个高工资,最好还能当上管理人员。


后来有了家庭,你要承担起男人的责任,赚钱养家。


过去20多年的时间,我都觉着这样的条条框框没有问题,在年少轻狂的时光,这些条条框框决定了你的下限,大家不都是这么过来的吗?


可是我过去的努力,都是为了符合条条框框的各项要求。我越来越觉着疑惑,我的努力,到底是为了什么啊,是为了这些世俗上的要求吗,我到底为谁而活?


压力,是自己给的


说实话,自己也给自己不少压力。


刚毕业,没有房贷车贷的情况下,我便给了自己很大的压力。压力怎么来的呢?比如一个月五千块钱的工资,买不起一个最新款的iPhone,又比如北京的朋友们,工资相比我在二线城市,竟能高出我一倍。


后来工作半年决定去北京,也是工作7年来,唯一一次的裸辞。


初生牛犊不怕虎,裸辞给我带来的毒打,至今历历在目,比如银彳亍卡余额一天天减少的焦虑,比如连面试都没有的焦虑,还有时刻担心着要是留不在北京,被迫得回老家的焦虑。


记得青旅楼下,有一家串店叫“很久以前羊肉串”,不到五点的时候门口就会有人排队,晚上下楼时看着饭店里熙熙攘攘,吃着烤串喝着扎啤的人时,心里十分羡慕,但却又不会踏进饭店一步。


毕竟一个目前找不到工作的人,每天一睁眼就是吃饭和住青旅的成本,吃个20块钱一顿的快餐就好了,怎么可能花好几百下馆子呢?


那时候心里有个愿望,就是我也想每周都可以和朋友来这里吃顿烧烤、喝喝扎啤。


嗯,我也不知道为什么,那时候对自己就是这么严苛。家庭虽不算富裕,但也绝不可能差这几顿烧烤、住几晚好的宾馆的钱,但我就是这样像苦行僧一样要求着自己,仿佛在向爸妈多要一分钱,就代表着自己输了。


后来工作稳定了,工资也比毕业时翻了几倍,恰巧又在高位上车了房子,但似乎压力只增不减,同样是不敢花钱。


现在又有了娃,这次压力也不用自己给了,别管他需要什么,一个小眼神,你只想给他买最好的。因此不敢请假,更不敢裸辞GAP一段时间了,这种感觉就像是在逃避赚钱的责任,不误正业一般。


一味的向前冲


带着压力,只能一味的向前冲,为了更高的薪资不断学习,为了更高的职级不断拼搏。


在“赚钱”这件事上,男人的基因里就像被编写好了一段代码。


    while (true){
makeMoreMoney();
}

过程中遇到困难,压力大,有难过的时候怎么办,身边有谁能去诉说呢?


中国的传统文化便是“男儿当自强”、“男儿有泪不轻弹”,怎么能去向别人诉说自己的痛苦呢?


那时候现在的老婆那时候还在上学,学生很难理解职场。结婚后,更没有人愿意在伴侣前展示自己的软弱。


和家人说?但是不开心的事,不要告诉妈妈,她帮不上忙,她只会睡不着觉。


和好朋友们一起坐下聚聚,喝几杯啤酒,少聊一些工作,压力埋在心里,让自己短暂的放松一下。



但现在的行业现状,不允许我们一味的在职场上冲了。


行业增速放缓,互联网渗透率达到瓶颈,随着而来的就是就业环境变差,裁员潮来袭。


你可以选择在职场中的高薪与光环,但也要付出相应的代价,比如变成“云老公/老婆”,“云爸爸/妈妈”。


或许我们都很想在职场中有一番作为,但是外部环境可能会让我们头破血流。


为了家庭,所以在职场中精进自己,升职加薪。我不禁在想,这看似符合逻辑的背后,我自己到底奋斗的是什么


不甘平庸,不服输


从老家裸辞去北京,是不满足于二线城市的工作环境,想接触互联网,获得更快的进步。


在北京,从小公司跳槽到大厂,是为了获得更高的薪资与大厂的光环。


再次回到老家,是不满生活只有工作,回来可以更好的平衡工作和生活。


回想起来,很多时候,自己就像一个异类。


明明工作还不满一年,技术又差,身边的朋友敢于跳槽到其他公司,涨一两千块钱的工资已经算挺好了,我却非得裸辞去北京撞撞南墙。


明明可以在中小公司里按部就班,过着按点下班喝酒打游戏的生活,却非得在在悠闲地时候,去刷算法与面经,不去大厂不死心。


明明可以在大公司有着不错的发展,负责着团队与核心系统,却时刻在思考生活中不能只有工作,还要平衡工作和家庭,最终放弃大厂工作再次回到老家。


每一阶段,我都不甘心于在当下的环境平庸下去,见识到的优秀的人越多,我便越不服输。


至此,我上面问自己的两个问题,我到底为谁而活?我自己到底奋斗的是什么,似乎有了些答案。


我做的努力,短期看是为了能够给自己、给家人更好的物质生活,但长远来看,是为了能让自己有突破桎梏与困境,不断向上的精神


仰望星空


古希腊哲学家苏格拉底有一句名言:“未经检视的人生不值得活。”那么我们为什么要检视自己的人生呢?正是因为我们有不断向上的愿望,那么我在想愿望的根源又到底是什么呢?


既然选择了不断向上,我决定思考,自己想成为什么样的人,或者说,一年后,希望自己变成什么样子,3年呢,5年呢?


当然,以后的样子,绝不是说,我要去一个什么外企稳定下来,或者说去一个大厂拿多少多少钱。


而是说,我希望的生活状态是什么,我想去做什么工作/副业,达成什么样的目标。


昨天刷到了一个抖音,这个朋友在新疆日喀则,拍下了一段延时摄影,我挺受震撼的。



生活在钢铁丛林太久了,我一直特别想去旅行,比如自驾新疆、西藏,反正越远越好。在北京租的房子,就在京藏高速入口旁,我每天上班都可以看到京藏高速的那块牌子,然后看着发会呆,畅想一下自己开着车在路上的感觉。


可好多年过去了,除了婚假的时候出去旅行,其余时间都因为工作不敢停歇,始终没有机会走出这一步,没有去看看祖国的大好河山。


我还发现自己挺喜欢琢磨,无论在做什么事情,我都会大量的学习,然后找到背后运行的规律。因为自己不断的思考,所以现实中,很少有机会和朋友交流,所以我会通过写作的方式,分享自己的思考、经历、感悟。


我写了不少文章,都是关于工作几年,我认为比较重要的经历的文章,也在持续分享我关于职业生涯的思考。


从毕业到职场,走过的弯路太多了,小到技术学习、架构方案设计,大到职业规划与公司选择,每当回忆起自己在职场这几年走过的弯路,就特别想把一些经验分享给更多的人,所以我持续的写,希望看到我文章的朋友,都能够对工作、生活有一点点帮助。


所以,我的短期目标,是希望能够帮助在职场初期、发展期,甚至一些稳定期的朋友们,在职场中少一点困惑,多一点力量


方式可能有很多,比如大家看我的文章,看我推荐的书籍、课程,甚至约我电话进行1v1沟通,都可以,帮助到一个人,我真的就会感到很满足,假设因为个人能力不足暂时帮不到,我也能根据自己的不足持续学习成长。


那么一年后,我希望自己变成什么样?
我希望自己在写作功底上,能够持续进步,写出更具有逻辑性、说服力的内容,就像明白老师、雪梅老师那样。公众号希望写出一篇10w+,当然数量越多越好,当然最希望的是有读者能够告诉我,读完这篇文章很有收获,这样比数据更能让人开心,当然最好还能够有一小部分工作之外的收入。


那么三年呢?
3年后,快要32岁了。希望那时候我已经积累了除了写作外,比如管理、销售、沟通、经营能力,能够有自己赚到工资外收入的产品、项目,最好能够和职场收入打平,最差能够和房贷打平,有随时脱离职场的底气。


五年呢?十年呢?
太久远了,想起来都很吃力的感觉。我一定还在工作,但一定不是打工,希望自己有了一份自己喜欢的事业,能够买到自己的dream car,然后能够随时带着家人看一看中国的大好河山。


你是不是想问,为什么一定要想这些?


因为当我想清楚这个问题的时候,那当下该做什么事情,该做什么选择,就有了一个清晰的标准:这件事情、这个选择,能否帮我们朝「未来的自己」更进一步?


这时候当再遇到压力、困难,我们就会变的乐观,有毅力、有勇气、自信、耐心,积极主动。


因为你自己想干成一件事,你就会迸发出120%的能量。


当然,也希望自己试试放下盔甲,允许自己撤退,允许自己躺平,允许自己怂,允许自己跟别人倾诉痛苦。


说在最后


说了很多,感谢你能看到最后。


感觉整体有点混乱,但还是总结一下:


起因是感觉自己压力很大,因为持续的大量输入导致自己有点陷入信息爆炸的焦虑,有一天下班到家时感觉头痛无比,九点就和孩子一起睡觉了,因此本来想谈谈中国男性的压力。


但不由自主的去思考自己的压力是从哪里来的,去发现压力竟都来源于传统文化、社会要求,于是越想越不服气,我为什么非得活成别人认为应该活成的样子?


于是试着思考自己想成为什么样子,其实也是一直在琢磨的一件事情,因为当开始探索个人IP的时候,我就发现自己需要更高一层的、精神层面的指导,才能让自己坚持下去。


如果你和我一样,希望你给自己的压力更小一些,环境很差,但总还有事情可以去做,愿你可以想清楚,你想成为的样子。一时想不清楚也没关系,也愿你可以允许自己撤退,允许自己软弱。


不知道你有没有想过,自己想要成为的样子呢?


作者:东东拿铁
来源:juejin.cn/post/7374337202653265961
收起阅读 »

如何优雅的将MultipartFile和File互转

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。 前言 首先来区别一下MultipartFile和File: MultipartFile是 S...
继续阅读 »

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。


前言


首先来区别一下MultipartFile和File:



  • MultipartFile是 Spring 框架的一部分,File是 Java 标准库的一部分。

  • MultipartFile主要用于接收上传的文件,File主要用于操作系统文件。


MultipartFile转换为File


使用 transferTo


这是一种最简单的方法,使用MultipartFile自带的transferTo 方法将MultipartFile转换为File,这里通过上传表单文件,将MultipartFile转换为File格式,然后输出到特定的路径,具体写法如下。


transferto.png


使用 FileOutputStream


这是最常用的一种方法,使用 FileOutputStream 可以将字节写入文件。具体写法如下。


FileOutputStream.png


使用 Java NIO


Java NIO 提供了文件复制的方法。具体写法如下。


copy.png


File装换为MultipartFile


从File转换为MultipartFile 通常在测试或模拟场景中使用,生产环境一般不这么用,这里只介绍一种最常用的方法。


使用 MockMultipartFile


在转换之前先确保引入了spring-test 依赖(以Maven举例)


<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>versionversion>
<scope>testscope>
dependency>

通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。


multi.png


作者:程序员老J
来源:juejin.cn/post/7295559402475667492
收起阅读 »

面试官问我String能存储多少个字符?

首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。 编译器源码如下,限制了字符串长度大于等于65535就会编译不通过 private void checkStringConstant(DiagnosticPosition...
继续阅读 »

  1. 首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。

  2. 编译器源码如下,限制了字符串长度大于等于65535就会编译不通过


    private void checkStringConstant(DiagnosticPosition var1, Object var2) {
    if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
    this.log.error(var1, "limit.string", new Object[0]);
    ++this.nerrs;
    }
    }

    Java中的字符常量都是使用UTF8编码的,UTF8编码使用1~4个字节来表示具体的Unicode字符。所以有的字符占用一个字节,而我们平时所用的大部分中文都需要3个字节来存储。


    //65534个字母,编译通过
    String s1 = "dd..d";

    //21845个中文”自“,编译通过
    String s2 = "自自...自";

    //一个英文字母d加上21845个中文”自“,编译失败
    String s3 = "d自自...自";

    对于s1,一个字母d的UTF8编码占用一个字节,65534字母占用65534个字节,长度是65534,长度和存储都没超过限制,所以可以编译通过。


    对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,长度和存储也都没超过限制,所以可以编译通过。


    对于s3,一个英文字母d加上21845个中文”自“占用65536个字节,超过了存储最大限制,编译失败。


  3. JVM规范对常量池有所限制。量池中的每一种数据项都有自己的类型。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANTUtf8类型表示。CONSTANTUtf8的数据结构如下:


    CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
    }

    我们重点关注下长度为 length 的那个bytes数组,这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大字节数。length 的类型是u2,u2是无符号的16位整数,因此理论上允许的的最大长度是2^16-1=65535。所以上面byte数组的最大长度可以是65535


  4. 运行时限制


    String 运行时的限制主要体现在 String 的构造函数上。下面是 String 的一个构造函数:


    public String(char value[], int offset, int count) {
    ...
    }

    上面的count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1。所以在运行时,String 的最大长度是2^31-1。


    但是这个也是理论上的长度,实际的长度还要看你JVM的内存。我们来看下,最大的字符串会占用多大的内存。


    (2^31-1)*16/8/1024/1024/1024 = 2GB

    所以在最坏的情况下,一个最大的字符串要占用 2GB的内存。如果你的虚拟机不能分配这么多内存的话,会直接报错的。





补充 JDK9以后对String的存储进行了优化。底层不再使用char数组存储字符串,而是使用byte数组。对于LATIN1字符的字符串可以节省一倍的内存空间。


作者:念念清晰
来源:juejin.cn/post/7343883765540831283
收起阅读 »

带你从0到1部署nestjs项目

web
前言 最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway 大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn) 最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲...
继续阅读 »

前言


最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway


大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn)


最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲如何部署。一直有兄弟问prisma如何部署,这篇文章就帮你扫清障碍,文章可能比较长,希望耐心看完


后端技术栈



  • nestjs

  • mysql

  • redis

  • minio

  • prisma


部署需要掌握的知识



  • docker

  • github actions

  • 服务器


实战


nestjs打包镜像


我们部署的时候用的docker,docker需要拉镜像,然后生成容器,docker的知识可以去学习下,这里就默认大家会了,我们在打包的时候要写Dockerfile文件,后端项目是需要保留node_modules的,所以打包的时候一起打进去,我的项目用的pnpm包管理工具,我的文件挂载时有点点问题,我就没有用pm2去执行多阶段打包,多阶段打包速度会比较快,还有就是比如开发环境的依赖可以不打,当然这都是优化的地方,暂时没有去做,大家可以自行尝试


# 因为我们项目使用的是pnpm安装依赖,所以找了个支持pnpm的基础镜像,如果你们使用npm,这里可以替换成node镜像
# FROM nginx:alpine
FROM gplane/pnpm:8 as builder

# 设置时区
ENV TZ=Asia/Shanghai \
DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

# 创建工作目录
RUN mkdir -p /app

# 指定工作目录
WORKDIR /app

# 复制当前代码到/app工作目录
COPY . ./

RUN npm config set registry https://registry.npm.taobao.org/
# pnpm 安装依赖
COPY package.json /app/package.json

RUN rm -rf /app/pnpm-lock.yml
RUN cd /app && rm -rf /app/node_modules && pnpm install

RUN cd /app && rm -rf /app/dist && pnpm build

EXPOSE 3000
# 启动服务
CMD pnpm run start:prod


这样后端镜像就构建好了,接下来去编写github action的文件,github actions是做ci/cd的,让我们每次的部署走自动化流程,不要每次手动去做这些工作


github actions


在我们的根目录下面创建这样一个文件,这个文件名字可以随便取


12.png


然后在里面编写逻辑


name: Docker

on:
push:
branches: ['main']

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

- name: Log int0 registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new

- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

- name: SSH Command
uses: D3rHase/ssh-command-action@v0.2.1
with:
HOST: ${{ secrets.SERVER_IP }}
PORT: 22
USER: root
PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }}
COMMAND: cd /root && ./run.sh

这里的['main']就是我们要执行哪个分支,你不是main分支,那就改成你的分支就可以,其他都是固定的模板,直接用


SSH Command 这个是我们取做ci/cd的时候,每次我们提交代码,然后配置了ssh密钥,就可以让服务器执行run.sh命令,这个shell脚本我们后面可以用到,这里就记住是让服务器去执行拉取镜像以及执行启动容器的。


当我们做到这一步之后,我们提交代码的时候,应该会出现这样的情况


13.png


因为还没有去配置ssh密钥,这个肯定跑不起来,看到我们上面ssh command里面有两个变量,就是我们要配置的,接下来我们去搞服务器。


服务器


最近双十一活动,买个服务器还是挺香的,我买的阿里云2核2g的99/年,买的时候选操作系统,随便一个都可以,我因为对ubuntu熟悉一下,就买了ubuntu操作系统的,买好之后,记得重置密码


14.png


后面我们用shell工具连接的时候需要用到密码的


之后我们去下载一个shell工具,连接服务器用的,常见的有xshell finalshell,我用的第二个。


15.png


就傻瓜式安装,下一步就可以,然后我们去连接一下服务器,去下载宝塔。


16.png


第二步那里选择ssh连接就可以了,然后主机就是你的服务器公网ip,密码就是刚刚的,用户名就是root


连接上了之后,去下载宝塔,这个是ubuntu的命令,其他的操作系统有差别,可以去搜一下就有


wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh


下载好之后输入bt default命令就可以打开了


17.png


因为宝塔是个可视化操作面板,比较方便,所以先弄好。


接下来我们去搞服务器密钥


18.png


我们在这里创建好密钥对,记得它只有一次机会,所以下载好了记得保存在你记得住的地方,然后创建好,记得要绑定,不然没效果,然后我们就要得用ssh密钥来连接服务器了


20.png


至此,我们的服务器也弄好了


github绑定密钥


21.png


这个是settings界面的,然后大家按照步骤创建就可以,到这里我们的配置就结束了。


创建shell脚本


我们上面不是说了,我们要写一个bash文件吗,现在就要来写,这个bash文件我们要执行拉镜像和跑容器


23.png


我们可以选择在宝塔中操作


docker-compose pull && docker-compose up --remove-orphans

然后我们在同目录下也就是root目录下面新建一个docker-compose.yml文件,来启动容器的,这个文件就不要展示了,也就是创建了哪些服务,挂载哪些卷,如果有需要评论区说一下就行,很简单,因为我们用了很多服务,mysql redis minio nginx 这些多镜像,就得多个容器来跑,docker-compose无疑就好


到这里后端项目就部署完了,我们还得迁移数据库对吧


数据库部署


pirsma迁移

因为我用的mysql和prisma,typeorm思路差不多,可以一样用。我们的prisma以及typeorm迁移的时候只可以同步表结构,数据不会同步过去,所以我们跑迁移命令的时候,跑完会发现没有数据,我们需要手动导入数据


另外注意点,我们docker-compose.yml里面的mysql容器名字对应我们连接的主机名,这里记得更改prisma连接,不然你的prisma还连接在localhost肯定找不到


我们来上手操作


24.png


这是我现在在跑的容器,我要找到我的后端项目对应的容器id,进去执行命令


docker exec -it <容器id> /bin/sh 跑这个我们就可以在容器内部执行命令


25.png


然后就可以把表结构同步过去了,我们也可以在生成Dockerfile的时候写迁移命令也是可以的,这样就不用手动同步了


数据库导出

我们需要将本地的数据迁移上去,需要先导出sql文件,这个就不用在这里展开说了,很简单,不会可以去找个博客教程,不到30s就完了,导出后我们需要将那个sql文件


然后我们在宝塔操作,找到你正在运行的mysql容器目录


26.png


将你的sql文件上传上去,放哪里都无所谓,记得路径就行


然后我们进入mysql容器里面,跑上面的那个命令



  1. 登录账号 mysql -u root -p

  2. 输入密码 ******* 输入你数据库连接的那个密码

  3. 进入之后 USE <database_name> 就选中了那张表

  4. 然后执行 source 刚刚的那个sql文件路径


这样操作数据就同步上去了,注意,数据同步前是一定要有表结构的,所以有先后顺序,这个地方注意。


也可以用这个命令, 将sql文件拷贝到你的容器内,然后跑上面的步骤,看个人喜好了。
docker cp /本地路径/your_file.sql 容器名称:/容器路径/your_file.sql


到这里我们的部署就结束了,等项目正式上线的时候,还有其他注意点还会再写一篇博客的


最后


项目是跟着开头提到的小付大佬学习的,主要想学下react,没想到是个全栈项目,就用nestjs写了后端,也学到了很多前端,后端,部署的知识,强烈推荐大家可以去看看。最后 觉得不错的话,可以给个点赞加关注😘


作者:西檬
来源:juejin.cn/post/7299859799780655155
收起阅读 »

webview预加载的技术原理和注意点

web
此文章介绍webview预加载的技术原理和注意点 背景 网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果 原理 即空间换时间,提前加载页面url 由于首页就有网页入口,所以需要在首页Activity进行预加载。 创建webview Web...
继续阅读 »

此文章介绍webview预加载的技术原理和注意点


背景


网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果


原理


即空间换时间,提前加载页面url


由于首页就有网页入口,所以需要在首页Activity进行预加载。


创建webview



  • WebView(MutableContextWrapper(context),使用MutableContextWrapper替换Context,,方便后续复用时替换为webview容器Activity的上下文对象

  • WebSettings、原生方法初始化,保证预加载时功能正常,因为后续不会再进行loadUrl,必须保证h5页面正常显示

  • WebViewClient、WebChromeClient监听



    • 重写onPageFinished、onReceivedTitle方法,主要为了title的接收,并且记录下来,后续webview复用时直接显示title

    • 重写onReceivedSslError方法,避免证书错误加载显示失败



  • 首页预加载容器Layout,置于最底层,宽度全屏,高度设置为全屏高度 - 顶部导航栏高度 - 状态栏高度


viewGr0up.addView(WebView(MutableContextWrapper(context)).also { web ->  // 初始化webview 
}, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


  • 刷新逻辑,绑码状态或登录信息改变时,刷新已经预加载好的webview的url


复用webview



  • webview容器整改

    • 判断是否需要使用已预加载的webview,如果需要复用,则根布局添加预加载webview进来,注意布局层级,避免覆盖了其他控件





webView?.let { web ->
(web.context as MutableContextWrapper).baseContext = activity
}

container.addView(it, 0, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


  • 原webview容器使用ViewStub代替,如果不需要复用则将ViewStub inflate,进行正常的h5页面加载

  • 添加预加载webview后,直接显示,不需要loadUrl,但是白屏分析之类的逻辑别忘了手动调用


页面关闭



  • webview跟随Activity一起进行销毁,但是需要通知首页重新生成一个webview,准备给下一次用户点击使用

  • 首页关闭,页面栈清空时,需要清空单例中webview对象,并且调用destroy

  • 不推荐回收webview继续使用,因为在实际测试中表现并不好,重建webview可以规避很多问题场景


如果用户点击比较快时,依然会看到加载过程和骨架屏


问题点和解决



  • 复用webview时,页面视觉残留上一次h5页面状态



    • 原因:页面关闭时,触发Activity的onStop方法,webview调用onPause,此时webview被暂停,webview的reload也不会立即刷新

    • 解决:回收webview时,对webview重新恢复交互状态(onResume)



  • 页面关闭,迅速点开,页面先显示上一次h5页面状态,然后开始reload



    • 原因:当Activity反复打开关闭时,Activity的回收、对象GC会滞后,导致webview已经被复用并且上屏了,webview才开始触发reload

    • 解决:webview不进行回收,每次页面关闭都进行销毁,重新创建webview



  • webview多次reload后,网络请求失败
    axios网络请求失败,response报文为空,暂未找到原因,了解的大佬麻烦解答一下,谢谢。当不回收webview后,此场景不存在了

  • h5页面正常显示后,又刷新一次页面



    • 原因:webview复用时,对webview重新进行初始化(重新创建原生能力、重置上下文对象等)时,会重新对UserAgent进行赋值,导致重新刷新了一次。

    • 排查过程
      发现网页骨架屏刚出现时点开不会重复刷新;骨架屏消失后点开也不会重复刷新;唯独骨架屏时,刚出现vConsole绿色块时点开会出现重复刷新。
      对webview的shouldOverrideUrlLoading方法进行debug,发现并没有进入断点,说明并不是调用了reload,推测有什么逻辑导致网页重新刷新了一次。
      随即用傻子办法,一段一段代码注释,发现友盟组件attach webview和通用webview容器设置userAgent的代码会导致重复刷新,难道友盟组件也有设置userAgent的代码?
      然后查看友盟组件源码,不出所料,发现友盟组件中反射了setUserAgentString方法,并且对userAgent拼接了"Umeng4Aplus/1.0.0字符串,如下图所示。


      那是否设置的userAgent有什么敏感字符串导致刷新?随即将userAgent只设置为空字符串,发现也会导致重复刷新。
      到这里水落石处,但为什么userAgent发现变化会导致网页重复刷新?
      询问前端大佬,回复没有监听userAgent,userAgent变化也不会做什么操作,故而没有得到具体答案,了解的大佬麻烦解答一下,感谢。

    • 解决:webview复用时,不进行userAgent的重复赋值




IMG20240529101049.png



  • 复用webview时,页面白屏等待一会后秒开h5页面

    • 原因:预加载时webview在1像素的layout中加载,复用到通用webview容器中,webview控件的布局已经结束,但需要时间对H5进行渲染,在重复打开关闭后或性能低下的手机表现更甚

    • 解决:首页预加载webview时,已通用webview容器同大小进行布局



  • 内存泄漏问题

    • 原因:部分原生方法对象中对Activity和Fragment有强引用,当原生方法对象被addJavascriptInterface添加进webview时,复用的webview生命周期长如Application,就会强引用到Activity,导致无法回收,内存泄漏

    • 解决:webview回收时清空Activity、Fragment的引用

    • 不复用webview后此问题不存在了




作者:聪本尊18680
来源:juejin.cn/post/7373937820179005478
收起阅读 »

面试官:为什么忘记密码要重置,而不是告诉我原密码?

Hello,大家好,我是 Sunday。 最近有个同学在面试中遇到了一个很有意思的问题,我相信大多数的同学可能都没有遇到过。 面试官提问说:“为什么很多网站忘记密码需要重置,而不是直接告诉用户原密码?” 很有意思的问题对不对。很多网站中都有“忘记密码”的功能,...
继续阅读 »

Hello,大家好,我是 Sunday。


最近有个同学在面试中遇到了一个很有意思的问题,我相信大多数的同学可能都没有遇到过。


面试官提问说:“为什么很多网站忘记密码需要重置,而不是直接告诉用户原密码?


很有意思的问题对不对。很多网站中都有“忘记密码”的功能,但是为什么当我们点击忘记密码,经过一堆验证之后,网站会让我们重置密码,而不是直接告诉我们原密码呢?


所以,今天咱们就来说一说这个问题。


防止信息泄露



2022年11月1日,Termly 更新了《98个最大的数据泄露、黑客和曝光事件》(98 Biggest Data Breaches, Hacks, and Exposures)。其中包括很多知名网站,比如:Twitter



所以,你保存在网站中的数据可能并没有那么安全。那么这样的数据泄露后会对用户产生什么影响呢?


对大多数人来说最相关的经历(网上看到的)应该是诈骗电话,他们甚至可以很清楚的告诉你你的所有个人信息。那么这些信息是怎么来的呢?


有些同学可能说是因为“网站贩卖了我的个人信息”,其实不是的。相信我 大多数的网站不会做这样的事情


出现这样事情的原因,大部分都是由于数据泄露,导致你所有的个人信息都被别人知道了。


那么,同理。既然他们可以获取到你的私人信息,那么你的账户和密码信息是不是也有可能被盗取?


而对于大多数的同学来说,为了防止密码太多忘记,所以很多时候 大家都会使用统一的密码! 也就是说你的多个账号可能都是同一个密码。所以,一旦密码泄露,那么可能会影响到你的多个账号,甚至是银彳亍卡账号。


因此,对于网站(特别是一些大网站)来说,保护用户数据安全就是至关重要的一件事情。那么他们一般会怎么做呢?


通常的处理方式就是 加密。并且这种加密可能会在多个不同的阶段进行多次。比如常见的:SHA256、加盐、md5、RSA 等等


这样看起来好像是很安全的,但是还有一个问题,开发人员知道如何解密他们。或者有些同学会认为 数据库中依然存在着正确的密码 呀?一旦出现信息泄露,不是依然会有密码泄露的问题吗?


是的,所以为了解决这个问题,网站本身也不知道你的密码是什么。


网站本身也不知道你的密码是什么


对于网站(或者其他应用)来说,它们是 不应该 存储你的原密码的。而是通过一些系列的操作来保存你加密之后的代码。并且这个加密是在前端传输到服务端时就已经进行了,并且是 不可逆 的加密操作,例如:MD5 + 加盐



我们举一个简单的例子:


比如有个用户的密码是 123456,通过 md5 加密之后是:E10ADC3949BA59ABBE56E057F20F883E


md5 理论上是不可逆的,所以从理论上来说这个加密后的代码是不可解析的。但是 md5 有个比较严重的问题就是:同样的字符串加密之后会得到同样的结果


这也就意味着:E10ADC3949BA59ABBE56E057F20F883E 代表的永远都会是 123456


所以,如果有一个很大的 md5 密码库,那么理论上就可以解析出所有的 md5 加密后的字符串。就像下图一样:




因此,在原有的 md5 加密之上,很多网站又增加了 加盐 的操作。所谓加盐指的就是:在原密码的基础上增加一些字符串,然后进行 md5 加密


比如:



  1. 原密码为 123456

  2. 在这个密码基础上增加固定字符“LGD_Sunday!”

  3. 得到的结果就是:“LGD_Sunday!123456”

  4. 然后用该字符进行 md5 加密,结果是:E1FC8CB7B54BED0FDC8711530236BA4D

  5. 此时尝试解密,会发现 解密失败



这样大家是否就可以理解,为什么很多网站在让我们输入密码的时候 ,要求包含 大小写+符号+ 字母 + 数字 了吧。本质上就是为了防止被轻松解密。


而服务端拿到的就是 “E1FC8CB7B54BED0FDC8711530236BA4D” 这样的一个加密后的结果。然后服务端再次对密码进行加密操作,从而得到的是一个 被多次加密 的数据,保存到服务端。


所以说:网站无法告知你密码,因为它也不知道原密码是什么。


目前很多网站或应用为了保证用户安全,都已经采取 扫码登录、验证码登录 等方式进行登录验证,这种无密码的方式,会更大程度的保证你的账号安全。


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

一定要考公吗,一定要上岸吗,可是我已经考出病来了!

昨天朋友圈里面看到一个好朋友发了病书,患上抑郁症了,他考公一年多了,说实话,我心中百感交集,因为我身边考公的朋友实在太多了,大家心里都在承受巨大的压力。 我觉得没必要,可能你觉得考上公务员就可以稳吃一辈子了,但是事实真的是这样吗? 当然,如果能考上一线城市的...
继续阅读 »

昨天朋友圈里面看到一个好朋友发了病书,患上抑郁症了,他考公一年多了,说实话,我心中百感交集,因为我身边考公的朋友实在太多了,大家心里都在承受巨大的压力。


fde22171b5fb0e4b6983fe643409e32.jpg


我觉得没必要,可能你觉得考上公务员就可以稳吃一辈子了,但是事实真的是这样吗?


当然,如果能考上一线城市的公务员,那么绝对是普通人最好的选择,也绝对够牛逼,但是大多数人考不上。


别说一二线,现在能考进县城里面的公务员,那也算是天之骄子,注意,这里指的是正编,如果是合同的,那么就不说了,大多数人终其一生可能都只能干到乡镇里面。


这和你有能力没能力其实关系不大,除非你能力万里挑一,特别出众。


其一,始终要明白“庙小妖风大,浅池王八多”,没有一个人不想往高处走,你进入一个乡镇单位里面,里面年龄比你大,资历比你深的大有人在,你觉得他们都想留在乡镇吗?是乡镇的饭好吃,空气清新吗?


不是,一切都是因为没机会,没机会说直白一点,就是上面没人,有人早都进城了。


前两年我在乡镇遇到一个亲戚,和她闲谈,她说租她家隔离房子的人就是乡镇的公务员,快二十年了,还是在乡镇里面,和他同一批进来的,有关系的早都进城了!


这就很现实,还有你也不要说自己家族里面谁谁谁是那个领导的,没用,如果你没有价值,或者关系不到位,人家不会真心实意帮你的!


当然,如果你够强,那么不依靠关系,你也能进城。


我朋友的哥哥就是直接从其他县城的公务员岗位辞职,然后一年不到就考回本县,而且他们还不属于县里管,是省里管,人家考试就是牛逼,面试就讨人喜欢。


但是大部分人呢,我们自己心知肚明吧,如果你真的牛逼,那么可能早都上岸了,无论是村里还是乡镇。


到现在都还没上岸,那么就证明自己能力平庸,而且没有关系,认清现实一点。


其二,考上了就真的稳了吗,就能马上躺平了?


很多人会说,考上了就稳了,直接吃一辈子,又有面子,福利又好。


别天真,这也要分单位,的确有的单位福利挺好,年终奖也不少,但是只是很小一部分。


但是近年来,公务员降薪你也看到了吧,财政那么紧张,福利肯定会大打折扣,甚至没有。


在中国,现在大多数小城市的公务员工资也就是几千块,如果家里不是县城里面的且父母不能扶持,那么生活也是挺困难的,如果家庭条件特别好,那么就当玩。


之前听朋友说,她同学香港大学硕士毕业的,家里特别有钱,但是父母就叫她考回家里,一个月虽然工资才两三千,但是父母一个月给她一万,目的就是想让她陪父母。


但是大部分人呢,别说家里给钱,不给家里就已经不错了,很多人虽然考上了公务员,但是经常在刷信用卡,因为那点工资根本做不了什么。


所以,穷人家的孩子考上公务员(小地方的公务员),不会像范进中举那样改变人生,可能会更加困难。


其实最主要的是你基本没有任何机会往上走,你考上的这个岗位,基本上就是你这辈子的终点。


上面说的这一切都是你考上的前提,无论是镇里,村里还是县里。


但是回到现实,你能考上吗?


每年身前面对的是那么多刚毕业的大学生,他们身份比你有优势,身后面对的是那么多老油条,你在夹缝中很难的。


大多考公的人,要么全职,要么随便找个两三千块钱的活干,二者都承受巨大的心理压力和经济压力。


特别是经历一次又一次的落榜,心态会越来越糟。


看着时间从自己手中按时溜走,而自己除了学会了一些考试技巧,背会了一些“八股文”,其他一无所获,如果考上了,那么就是有用的,考不上,那么这一套拿到市场上去并没有什么用。


身体上的苦尚能抗住,而情绪上的崩溃却可能影响一个人一生,你面对的是一个未知的结果,而且这个结果可能都不是你自己想要的!


有时候,我觉得当自己真的扛不住了,那么不妨直接放手了,不要觉得下一次就上岸了,如果考了三四次都考不上,那么我觉得确实是自己智商和情商不行,提前一点认识到,别去死磕。


因为考了好多次都考不上,那么后面去考,基本上就是去玩一个概率游戏,因为考这么多次最起码花了一两年的时间,对于很多知识点,实际上自己都能基本摸清。


但是为啥还考不上,这基本就和智商挂钩了。


就像我一个程序员朋友,人家复习三个月就能上岸,而且还是贵阳的一个单位,竞争也很激烈,你能说是运气吗,反正我不信,我觉得这就是智商问题。


就像我在读大学的时候,一个算法题别人很快就能学会,但是我好多遍都学不会,花费十倍时间去学都赶不上别人,这时候我深刻意识到自己智商确实不行。


那么我肯定不会去死磕算法,因为毫无卵用,智商不行就是不行,不会因为你花很多时间就能改变的,这是天生的,并且也不是啥985大学毕业的,所以没必要去死磕。


但是我在应用层面和一些底层逻辑上有一定的积累和理解,所以我就往这个方向去做,何必去死死刷算法呢?


所以我觉得,无论考公考编还是其他,和智商有很大的挂钩,和情商也有很大的挂钩。


不要去和自己的弱项较劲,而是要充分去发掘自己的优势!


我们大部分人,智商平庸,情商也平庸,所以在这条路上是十分吃亏的,就是在玩一个概率游戏!


从我个人的角度来看,我觉得与其死耗,让自己心理的压力越来越大,不如换个角度去思考人生。


大多数人无非就是求稳,一辈子没多大的风险。


这其实是一种逃避,逃避面对现实,面对人生。


有时候,放过自己未必是一件坏事,人生如此短暂,难道就只有上岸才是唯一的目标吗?


当然,尊重每一个人的选择,考公本身也需要很大的勇气。


但是我们终究要回到现实,如果拿不到结果,那么也是自我感动,因为没人会在乎你的过程,别人也没兴趣了解你的过程。


而明天的路依然要自己负责!


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

设计问能不能实现这种碎片化的效果

web
前言 某天设计发来一个 网站,问我能不能实现这种类似的效果。 不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。 F12 调试 让我们打开调试,瞅瞅别人是如何实现。 可以看到在该层级下,页面有很多个 shard-wrap ...
继续阅读 »

前言


某天设计发来一个 网站,问我能不能实现这种类似的效果。


shard-img-reverse-xs.gif

不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。


F12 调试


让我们打开调试,瞅瞅别人是如何实现。


可以看到在该层级下,页面有很多个 shard-wrap 元素,而每一个元素都是定位覆盖其父元素的。


image.png

当我们添加 display: none; 后,可以看到嘴角这里少了一块。


image.png

而继续展开其内部的元素就可以看到主要的实现原理了:clip-path: polygon();


image.png

clip-pathpolygon 更详细的解释可以看 MDN,简单来说就是在一个 div 里面绘制一个多边形。


比如上图的意思就是:选取 div 内部坐标为 (9.38%,59.35%),(13.4%,58.95%),(9.28%,61.08%) 这三个点,并将其连起来,所以就能构成一个三角形了。然后再填充 backgroundColor 该三角形就有对应颜色了。


实现过程


调试看完别人的实现后发现,好像也不是很难。但是数据又如何搞来呢?


当然我们可以直接在接口那里找别人的数据,但是我找了一会发现不太好找。


于是想到咱们可是一名前端啊,简单写段 js 扒拉下来不就好了吗,想要更多,就滑一下滚轮,切换下一个碎片图像,再执行一次即可。


function getShardDomData() {
const doms = document.getElementsByClassName('shard')
const list = []
for (let i = 0; i < doms.length; i++) {
const style = window.getComputedStyle(doms[i])
let str = style.clipPath.replace('polygon(', '').replace(')', '')
list.push({
polygon: str,
color: style.backgroundColor,
})
}
return list
}
console.log('res: ', getShardDomData());

image.png

碎片化组件


简单封装一个碎片化组件,通过 transitiondelay 增加动画效果以及延迟,即可实现切换时的碎片化动画效果。我这里是用的 tailwindcss 开发的,大家可以换成对应 css 即可。


export type ShardComItem = {
color: string
polygon: string
}

export type ShardComProps = {
items: ShardComItem[]
}

export default function ShardCom({items}: ShardComProps) {
return (
<div className="relative w-full h-full min-w-20">
{items?.map((item, index) => (
<div className="absolute w-full h-full" key={`${index}`}>
<div
className="w-full h-full transition-all duration-1000 ease-in-out"
style={{
backgroundColor: item.color,
clipPath: `polygon(${item.polygon})`,
transitionDelay: `${index * 15}ms`,
}}
>
</div>
</div>
))}
</div>

)
}

模仿实现的 demo 地址


组件的代码放码上掘金了,感兴趣可以自提。



自制画板绘画 clip-path


当然只扒拉别人的数据,这肯定是不行的,假如设计师想自己设计一份碎片化效果该怎么办呢?


解决方法也很简单:那就自己简单开发一个绘图界面,让设计师自己来拖拽生成想要的效果即可。


线上画板地址


image.png

画板的实现就放到 下篇文章 中讲述了。


最后


当然最终只是简陋的实现了一部分效果罢了,还是缺少很多动画的,和 原网站 存在不少差距的,大家看个乐就行。


作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7372013979467333643
收起阅读 »

只要条件够诱人,你比谁都自律!

​ 虽然我不知道贾玲是谁,也没看过她的电影,但是从最近刷到的视频和文章,我能感受到这人对自己确实挺狠。 很多人将其描述为励志和自律,但是我个人并不觉得有多励志和自律,因为如果不是为了拍这个电影,不是为了让自己更上一个阶梯,她大概率不会去减肥。 如果你有200斤...
继续阅读 »

图片


虽然我不知道贾玲是谁,也没看过她的电影,但是从最近刷到的视频和文章,我能感受到这人对自己确实挺狠。


很多人将其描述为励志和自律,但是我个人并不觉得有多励志和自律,因为如果不是为了拍这个电影,不是为了让自己更上一个阶梯,她大概率不会去减肥。


如果你有200斤,你朝思暮想了五六年的女孩子说如果你能减掉80斤,她就和你谈朋友,那么我相信你会拼了命去减肥。


如果你因为比较胖而患了三高,走路都气喘吁吁,医生说如果再不减肥,你最多能再活几年,那么我相信你绝对会节食,锻炼。


但是你知道即使锻炼出八块腹肌,心仪的女孩也不可能做你女朋友,你虽然肥胖,但是身体上的病还不严重。


那么你基本上也不会去刻意减肥,因为人都是喜欢有结果的事情,如果没有结果,那么我刷刷短视频,喝喝可乐,这多快乐,我干嘛去吃这个苦。


所以在朋友圈看到不少朋友被贾玲感动后直接立flag,说必须要减肥成功的人,多数也是当时被电影感动了,我相信坚持不了多久就会放弃的。


这样的事情我见得太多,自己也嘴炮过无数次,但是基本都是半途而废,因为你的感性盖过了理性,而感性本身就是很容易受环境影响。


高三时,学校总是搞一些活动,为了给学生打鸡血,会请那些演讲成功学的来感化大家,“你回头看一下你的老师,心里想一下你的父母”,种种言论敲打一颗尚未成熟的孩子心上,有一些情绪不稳定的直接抱着老师大哭,然后大声喊到,“老师,请你放心,我一定会考上重点大学,爸爸妈妈,我一定不会让你们失望”。


演讲结束后,大家都带着沉重的心情回到教室,三五天之内倒是挺努力的,也不和别人多说话,但是一个星期后,百分之九十五的人直接恢复原样,该睡觉睡觉,该摆烂摆烂。


为什么会这样?


因为当时的感性直接覆盖了理性,总觉得自己可上九天揽月,但是面对现实的时候,英语单词很难背,数学题很难解,最主要的是还面对一个未知的结果,因为没人能保证努力后就能考上重点大学,所以干脆直接摆烂。


纵使有一部分人看着在努力,但实际上不过是假自律,不过是为了麻痹自己,因为学进去多少你自己清楚,眼镜虽然盯着书,但是脑子已经到校外去了。


所以我们就能得出一个结论,因为外界的一些刺激而做的决定,发的誓,基本上都是头脑发热而已,就像喝醉了的所作所为第二天一定会后悔。


只有一种条件能让人像疯狗一样去坚持,那就是交换,并且交换的目标都是十拿九稳的,就像贾玲这样,本身就是名人,减下100斤后,团队进行运作,她的名声,收入等方面一定会发生很大的变化,所以她能坚持。


我们现实中看到的那些十年如一日都在坚持跑步的人,其实别人也是找到了交换物,比如健康的身体,清醒的头脑等等。


我之前和朋友聊天,他说写代码的时候头脑不清醒,所以坚持跑步早睡,这样每天大脑都比较清醒,工作效率就比较高,所以他跑步本身就在交换。


我们回头去想一下自己曾经放弃的那么多事,基本上都是因为没有找到交换物,所以才一次又一次的放弃。


普通的打工人的交换物无非就是身体和积累,坚持学习,坚持锻炼,坚持思考,坚持做一件能够提升自己的事,经过长时间的累积,无形的财富会变为有形的财富,即使变不成财富,也不会损失什么,唯一损失的就是本该可以挥霍的时间!


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

Mybatis-Plus的insert执行之后,id是怎么获取的?

在日常开发中,会经常使用Mybatis-Plus 当简单的插入一条记录时,使用mapper的insert是比较简洁的写法 @Data public class NoEo { Long id; String no; } NoEo noEo = ...
继续阅读 »

在日常开发中,会经常使用Mybatis-Plus


当简单的插入一条记录时,使用mapper的insert是比较简洁的写法


@Data
public class NoEo {
Long id;
String no;
}

NoEo noEo = new NoEo();
noEo.setNo("321");
noMapper.insert(noEo);
System.out.println(noEo);

这里可以注意到一个细节,就是不管我们使用的是什么类型的id,好像都不需要去setId,也能执行insert语句


不仅不需要setId,在insert语句执行完毕之后,我们还能通过实体类获取到这条insert的记录的id是什么


image.png


image.png


这背后的原理是什么呢?


自增类型ID


刚学Java的时候,插入了一条记录还要再select一次来获取这条记录的id,比较青涩


后面误打误撞才发现可以直接从insert的实体类中拿到这个id


难道框架是自己帮我查了一次嘛


先来看看自增id的情况


首先要先把yml中的mp的id类型设置为auto


mybatis-plus:
global-config:
db-config:
id-type: auto

然后从insert语句开始一直往下跟进


noMapper.insert(noEo);

后面会来到这个方法


// com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog(), false);
return stmt == null ? 0 : handler.update(stmt);
} finally {
closeStatement(stmt);
}
}

在执行了下面这个方法之后


handler.update(stmt)

实体类的id就赋值上了


继续往下跟


// org.apache.ibatis.executor.statement.PreparedStatementHandler#update
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}

image.png


最后的赋值在这一行


keyGenerator.processAfter

可以看到会有一个KeyGenerator做一个后置增强,它具体的实现类是Jdbc3KeyGenerator


// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processAfter
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, parameter);
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processBatch
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
try (ResultSet rs = stmt.getGeneratedKeys()) {
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
// Error?
} else {
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeys
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
Object parameter)
throws SQLException {
if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
// Multi-param or single param with @Param
assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
// Multi-param or single param with @Param in batch operation
assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
} else {
// Single param without @Param
// 当前case会走这里
assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeysToParam
private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
String[] keyProperties, Object parameter)
throws SQLException {
Collection<?> params = collectionize(parameter);
if (params.isEmpty()) {
return;
}
List<KeyAssigner> assignerList = new ArrayList<>();
for (int i = 0; i < keyProperties.length; i++) {
assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
}
Iterator<?> iterator = params.iterator();
while (rs.next()) {
if (!iterator.hasNext()) {
throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
}
Object param = iterator.next();
assignerList.forEach(x -> x.assign(rs, param));
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.KeyAssigner#assign
protected void assign(ResultSet rs, Object param) {
if (paramName != null) {
// If paramName is set, param is ParamMap
param = ((ParamMap<?>) param).get(paramName);
}
MetaObject metaParam = configuration.newMetaObject(param);
try {
if (typeHandler == null) {
if (metaParam.hasSetter(propertyName)) {
// 获取主键的类型
Class<?> propertyType = metaParam.getSetterType(propertyName);
// 获取主键类型处理器
typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
JdbcType.forCode(rsmd.getColumnType(columnPosition)));
} else {
throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
+ metaParam.getOriginalObject().getClass().getName() + "'.");
}
}
if (typeHandler == null) {
// Error?
} else {
// 获取主键的值
Object value = typeHandler.getResult(rs, columnPosition);
// 设置主键值
metaParam.setValue(propertyName, value);
}
} catch (SQLException e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
e);
}
}

// com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int, java.lang.Class<T>)
@Override
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
// ...
else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
checkRowPos();
checkColumnBounds(columnIndex);
return (T) this.thisRow.getValue(columnIndex - 1, this.longValueFactory);

}
// ...
}

image.png


最后可以看到这个自增id是在ResultSet的thisRow里面


然后后面的流程就是去解析这个字节数据获取这个long的id


就不往下赘述了


雪花算法ID


yml切换回雪花算法


mybatis-plus:
global-config:
db-config:
id-type: assign_id

在使用雪花算法的时候,也是会走到这个方法


// com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog(), false);
return stmt == null ? 0 : handler.update(stmt);
} finally {
closeStatement(stmt);
}
}

但是不同的是,执行完这一行之后,实体类的id字段就已经赋值上了


StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);

image.png


继续往下跟进


// org.apache.ibatis.session.Configuration#newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

// org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch (ms.getStatementType()) {
// ...
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
// ...
}

}

最后跟进到一个构造器,会有一个processParameter的方法


// com.baomidou.mybatisplus.core.MybatisParameterHandler#MybatisParameterHandler
public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.mappedStatement = mappedStatement;
this.boundSql = boundSql;
this.configuration = mappedStatement.getConfiguration();
this.sqlCommandType = mappedStatement.getSqlCommandType();
this.parameterObject = processParameter(parameter);
}

在这个方法里面会去增强参数


// com.baomidou.mybatisplus.core.MybatisParameterHandler#processParameter
public Object processParameter(Object parameter) {
/* 只处理插入或更新操作 */
if (parameter != null
&& (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
//检查 parameterObject
if (ReflectionKit.isPrimitiveOrWrapper(parameter.getClass())
|| parameter.getClass() == String.class) {
return parameter;
}
Collection<Object> parameters = getParameters(parameter);
if (null != parameters) {
parameters.forEach(this::process);
} else {
process(parameter);
}
}
return parameter;
}

// com.baomidou.mybatisplus.core.MybatisParameterHandler#process
private void process(Object parameter) {
if (parameter != null) {
TableInfo tableInfo = null;
Object entity = parameter;
if (parameter instanceof Map) {
Map<?, ?> map = (Map<?, ?>) parameter;
if (map.containsKey(Constants.ENTITY)) {
Object et = map.get(Constants.ENTITY);
if (et != null) {
entity = et;
tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
}
if (tableInfo != null) {
//到这里就应该转换到实体参数对象了,因为填充和ID处理都是争对实体对象处理的,不用传递原参数对象下去.
MetaObject metaObject = this.configuration.newMetaObject(entity);
if (SqlCommandType.INSERT == this.sqlCommandType) {
populateKeys(tableInfo, metaObject, entity);
insertFill(metaObject, tableInfo);
} else {
updateFill(metaObject, tableInfo);
}
}
}
}

最终生成id并赋值的操作是在populateKeys中


// com.baomidou.mybatisplus.core.MybatisParameterHandler#populateKeys
protected void populateKeys(TableInfo tableInfo, MetaObject metaObject, Object entity) {
final IdType idType = tableInfo.getIdType();
final String keyProperty = tableInfo.getKeyProperty();
if (StringUtils.isNotBlank(keyProperty) && null != idType && idType.getKey() >= 3) {
final IdentifierGenerator identifierGenerator = GlobalConfigUtils.getGlobalConfig(this.configuration).getIdentifierGenerator();
Object idValue = metaObject.getValue(keyProperty);
if (StringUtils.checkValNull(idValue)) {
if (idType.getKey() == IdType.ASSIGN_ID.getKey()) {
if (Number.class.isAssignableFrom(tableInfo.getKeyType())) {
metaObject.setValue(keyProperty, identifierGenerator.nextId(entity));
} else {
metaObject.setValue(keyProperty, identifierGenerator.nextId(entity).toString());
}
} else if (idType.getKey() == IdType.ASSIGN_UUID.getKey()) {
metaObject.setValue(keyProperty, identifierGenerator.nextUUID(entity));
}
}
}
}

在tableInfo中可以得知Id的类型


如果是雪花算法类型,那么生成雪花id;UUID同理


image.png


总结


insert之后,id被赋值到实体类的时机要根据具体情况具体讨论:


如果是自增类型的id,那么要在插入数据库完成之后,在ResultSet的ByteArrayRow中获取到这个id


如果是雪花算法id,那么在在插入数据库之前,会通过参数增强的方式,提前生成一个雪花id,然后赋值给实体类


作者:我爱果汁
来源:juejin.cn/post/7319541656399102002
收起阅读 »

我是如何把个人网站首屏加载时间从18秒优化到5秒的

web
起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!服务器配置CPU:1核,内存:2GiB,带宽:1Mbps这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?...
继续阅读 »

起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!

服务器配置

CPU:1核,内存:2GiB,带宽:1Mbps

这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?

换!!!

然而贫穷像是一万多条无形的枷锁束缚住了我,让我换服务器的双手动弹不得。

此路不通,只能另寻他法解决了。

优化前首屏加载测试

测试结果分析

  1. 从截图可以看到,首屏加载耗时19.15秒,主要是chunk-vendors.2daba5b2.js这个文件加载耗时最长,为17.6秒,大小为1.9M,其他文件均在4秒内加载完毕。通常页面加载的一个文件大小超过300k,已经算比较大了。第二个比较耗时的文件是chunk-vendors.62bee483.css,这个应该是样式文件。其他的文件加载耗时都不超过1秒,所以后面优化先从那两个文件下手。
  2. 重新编译项目,看下项目生成的文件

可以看到前面提到的两个文件比较大,后面列出了每个文件使用gz压缩后的大小,但是浏览器实际并没有加载压缩后的文件,而是原始文件。再打开打包文件夹,发现实际生成的js文件夹中除了js文件,还有js.map文件,js.map文件通常用于开发环境调试用,方便我们查找错误,在生成环境是不需要用到的,而且都比较大,这也是一个优化的点。

分析项目依赖情况

运行vue ui,编译查看chunk-vendors中的结构发现,主要是element-ui依赖比较大,其次是vue和mavon-editor

整个项目的情况如下

那么如何优化呢

开启nginx压缩配置

修改nginx配置,启用gzip压缩

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

测试页面加载时间缩短到5.2秒,chunk-vendors.js传输大小为556k,加载时间为4秒,其他文件加载时间基本不超过200毫秒

生产配置不生成js.map

修改项目根目录中vue.config.js配置文件,设置productionSourceMap: false

module.exports = {
runtimeCompiler: true,
productionSourceMap: false
}

打包测试文件夹大小由9.1M减小到2.26M

配置gzip压缩插件

执行npm i compression-webpack-plugin@5.0.1 -D安装插件,在vue.config.js中修改打包配置

const CompressionPlugin = require("compression-webpack-plugin");
const productionGzipExt = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
module.exports = {
runtimeCompiler: true,

productionSourceMap: false,

configureWebpack: () => {
if (process.env.NODE_ENV === "production") {
return {
plugins: [
new CompressionPlugin({
filename: "[path].gz[query]",
algorithm: "gzip",
test: productionGzipExt,
threshold: 1024, // 大于1024字节的资源才处理
minRatio: 0.8, // 压缩率要高于80%
deleteOriginalAssets: false, // 删除原始资源,如果不支持gzip压缩的浏览器无法正常加载则关闭此项
}),
],
};
}
},
};

插件需要指定版本,最新版本的会报错这个和nginx压缩配置感觉重复了,实际测试和nginx压缩配置的速度差不多,如果两个压缩都有,速度并没有提升

修改elementui组件按需引入

  1. 执行npm install babel-plugin-component -D安装 babel-plugin-component2. 修改.babelrc内容如下:
{
"presets": [["@babel/preset-env", { "modules": false}]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
  1. 在main.js中引入需要用到的组件,示例如下:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "element-ui/lib/theme-chalk/index.css";
import mavonEditor from "mavon-editor";
import "mavon-editor/dist/css/index.css";
import axios from "axios";
import {
Avatar,
Button,
Container,
DatePicker,
Dialog,
Dropdown,
DropdownItem,
DropdownMenu,
Footer,
Form,
FormItem,
Header,
Image,
Input,
Main,
Message,
MessageBox,
Notification,
Option,
Select,
Table,
TableColumn,
TabPane,
Tabs,
Timeline,
TimelineItem,
} from "element-ui";

Vue.use(Button);
Vue.use(Dialog);
Vue.use(Dropdown);
Vue.use(DropdownMenu);
Vue.use(DropdownItem);
Vue.use(Input);
Vue.use(Select);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(DatePicker);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Tabs);
Vue.use(TabPane);
Vue.use(Header);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Timeline);
Vue.use(TimelineItem);
Vue.use(Image);
Vue.use(Avatar);
Vue.use(Container);
Vue.use(Option);
Vue.use(mavonEditor);
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$axios = axios;
Vue.config.productionTip = false;

axios.interceptors.request.use(
(config) => {
config.url = "/api/" + config.url;
config.headers.token = sessionStorage.getItem("identityId");
return config;
},
(error) => {
return Promise.reject(error);
}
);

axios.interceptors.response.use(
(response) => {
if (response.data && response.data.exceptionCode) {
const exceptionType = response.data.exceptionType;
Notification({ title: response.data.exceptionMessage, type: exceptionType.toLowerCase() });
return Promise.reject(response.data);
}
return response;
},
(error) => {
return Promise.reject(error);
}
);

new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");

修改按需引入后elementui依赖大小约为1.3M

修改组件局部引入为异步组件

在一个组件中引入其他组件时使用异步的方式引入,如

export default {
components: {
register: () => import('./views/Register.vue'),
login: () => import('./views/Login.vue')
}
};

完成后此时chunk-vendors.js这个文件已经从优化前的1.9M缩小到890k

页面加载约3秒可以显示出来,其他资源在页面显示后继续后台加载,全部加载完总耗时约5秒,请求数68次

组件按组分块

使用命名chunk语法webpackChunkName: "块名"将某个路由下的组件打包在同一个异步块中,如

import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
{
path: "/",
redirect: 'home'
},
{
path: '/home',
component: () => import(/* webpackChunkName: "home-page" */ '../views/Home.vue')
},
{
path: '/documents',
component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentList.vue')
},
{
path: '/documentcontent',
component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentContent.vue')
},
{
path: '/write',
component: () => import(/* webpackChunkName: "home-page" */ '../views/WriteMarkdown.vue')
},
{
path: '/about',
component: () => import(/* webpackChunkName: "home-page" */ '../views/About.vue')
},
{
path: '/management',
component: () => import(/* webpackChunkName: "management" */ '../views/management/Management.vue'),
children: [
{ path: '', component: () => import(/* webpackChunkName: "management" */ '../views/management/ManagementOptions.vue') },
{ path: 'developplan', component: () => import(/* webpackChunkName: "management" */ '../views/management/DevelopmentPlan.vue') },
{ path: 'tags', component: () => import(/* webpackChunkName: "management" */ '../views/management/TagsManage.vue') },
{ path: 'documents', component: () => import(/* webpackChunkName: "management" */ '../views/management/DocumentsManage.vue') }
]
},
{
path: '/games',
component: () => import(/* webpackChunkName: "games" */ '../views/games/Games.vue'),
children: [
{ path: '', component: () => import(/* webpackChunkName: "games" */ '../views/games/GameList.vue') },
{ path: 'minesweeper', component: () => import(/* webpackChunkName: "games" */ '../views/games/minesweeper/MineSweeper.vue') }
]
},
{
path: '/tools',
component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsView.vue'),
children: [
{ path: '', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsList.vue') },
{ path: 'imageconverter', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ImageConverter.vue') }
]
}
];

const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});

export default router;

打包编译后文件比之前要减少了一部分,并且合并后的文件资源也不大,完全可以接受

页面加载耗时基本没变,但是请求数减少到51次

总结

  1. nginx压缩对性能的提升最大,通过压缩文件缩短资源加载时间
  2. gzip压缩插件会将文件压缩成gz格式,暂时不知道怎么用
  3. elementui按需引入会减小依赖资源的大小,chunk-vendors.js文件体积会减小
  4. 使用异步组件可以在后台加载渲染不到的资源,优先加载渲染需要的资源,缩短页面响应时间,但同时会增加打包后的文件数量,导致页面请求数量增加。
  5. 组件按路由分组,打包的时候会将相同组名的资源打包到一个文件中,可以减小请求数


作者:宠老婆的程序员
来源:juejin.cn/post/7351292656633331747
收起阅读 »

采访:中年程序猿图鉴

程序员群体曾是低调多金的代表,但最近996话题、甲骨文大裁员等事件持续发酵,让这个群体成了大众眼中的“失意中年人”。年轻时的拼命,换来的却是中年时的焦虑。收入虽高,但前途摇摆。30岁真的是程序员迈不过去的坎吗?曾经梦想着用技术改变世界的程序员们,又是如何看待自...
继续阅读 »

程序员群体曾是低调多金的代表,但最近996话题、甲骨文大裁员等事件持续发酵,让这个群体成了大众眼中的“失意中年人”。

年轻时的拼命,换来的却是中年时的焦虑。收入虽高,但前途摇摆。

30岁真的是程序员迈不过去的坎吗?曾经梦想着用技术改变世界的程序员们,又是如何看待自己的职业规划和人生价值?

穿越喧嚣,我们采访了12位中年程序员,听听他们的故事和人生。

要点速览

• 我们被固定在“敲代码”的坑里,一干就是10年,再干别的早已不会。敲代码已经成了一项流水线般的工作,就像搬砖工一样。

• 公司把有创造性的事情全部标准化,每个人负责一部分,还会安排几个人“备份”,每个人随时能被替代,我没有一点安全感。

• 这个行业根本不存在吃青春饭这一说,关键是40岁就要干40岁该干的活,35岁就要干35岁该干的活,你不能35岁还在干30岁干的活。

• 加班和掉头发是肯定的,不敢天天洗头,生怕哪天秃顶。

• 我来谷歌快三年,只有一次是真正为了赶进度加班到晚上12点。不过,硅谷的创业公司很羡慕国内的刻苦劲儿,因为对初创公司来说,真的是效率决定生死。

• 不论是什么技术,只会把低端的程序员消灭掉。同样一个东西,普通的程序员和有爱好的程序员都能实现,但是有爱好的会把它实现得更好。

• 老一代程序员喜欢亲力亲为,现在的一代多是拿来主义。我们老一辈是木工,喜欢从木头做成家具,现在的年轻人像宜家,买来现成的再自己搭。新兴程序员效率更高,我们这一辈更能追根溯源。

• 年龄越来越大,身体确实有点吃不消,上个月后背上还起了一大片带状疱疹。就算如此,我从没想过换行业,我会做一辈子程序员,这是一个有技术含量,让我愿意一直打怪升级的工作!

null

从业十年,从“工程师”变“码农”

Luke 33岁 入行10年

北京 游戏行业

10年前我入行时,整个行业一片欣欣向荣,那时候老板更喜欢称我们为“工程师”,但是现在,我们已经成为“码农”了。

之所以有这个变化,一个是因为工种越来越细化,每个程序员负责的任务越来越精细、单一,时间长了,我们只熟悉那一个模块的工作;另外一个,是因为我们自身的知识结构越来越跟不上新技术的需求。

软件行业的开发模式,是对一个框架的修改和堆砌。说得更贴切一点,就是堆积木。只要掌握了编程技能,一个程序员每天的工作几乎就是从开源网站上扒一段程序,然后根据公司需要不断在框架上添加、修改。程序是24小时不间断运行的,我们在开发和维护程序的时候,每天都需要加班到很晚,熬夜是常态,这真是一个体力活儿。

null
时间长了,我们就被固定在一个“敲代码”的坑里,一干就是10年。这时候,再干别的早已不会了,敲代码已经成了一项流水线般的工作,甚至不用动脑子就能完成,就像搬砖工一样。

出卖力气,就对身体有很高的要求。对于我们30多岁的程序员来说,已经熬不了夜,思维已经基本固定,但为了养家糊口,要求的工资却越来越高。

我们长期固定在一个岗位已经形成惯性,上不去也下不来,但这不是我们自愿的,而是对我们的一种摧残。

在这种压力下,我们也习惯了996的工作节奏。有些时候即使早早做完了工作,看着别人不下班,我们也会拖一拖,至少在老板看来这样比较敬业。

两年前我曾想过跳出这样的循环,可是当我从原单位离职后,发现再去别的岗位已经成了一个零基础的新人,而且工资比之前还低。考虑了两个月,我又回到了程序员的岗位上。

我现在最大的想法就是干好当前的工作,趁着还有机会拓展自己,延伸自己的技能。以后如果跳出这个行业,不会被技能限制。

null

有时候很羡慕年轻同事,说辞职就辞职

Bruce 34岁 入行7年

成都 手机厂商

我们团队15个人,平均年龄35岁以下,只有三个人比我年纪大。

我们公司是出了名的用人“狠”,一方面,公司每年会招很多新人,减少用人成本,让我们这些领着高薪的老员工瑟瑟发抖。另一方面,为了保持稳定,公司会把有创造性的事情全部标准化,把任务分得很细,每个人负责一部分,还会安排几个人“备份”,这就让我们没有不可替代性,自然也没有安全感。

做程序员要么是一直写代码,要么是往上走,做整体规划。如果工作几年还没有升职,确实会很有危机感,我身边合同到了不续签的情况很常见,小组的leader也是随时可以被换掉,或者整个小组被分流。

在这种压力下,我的工作时长远超996,没有时间学习新东西提升自己,更别说健身追剧。 虽然看起来我的外表和90后没什么区别,但我已经是两个孩子的爸爸了,要挣钱,只能花时间去换。

null

有时候很羡慕一些年轻的同事,觉得劳动强度大,说走就走。有的换到国企,还有的转行去做销售,听说有技术背景的销售挺吃香。我也不是没想过换工作,但这份工作离家近,收入也不错,还是先干着吧。

有人说程序员越老越不值钱,我觉得每个行业都有自己的问题,医生是越老越值钱,但要花大半辈子爬坡,也不是每个人都能忍受。

我其实还没有想清楚未来该做什么,有时候跟同事开玩笑说,成都的小吃多,大不了以后去开个店卖猪头肉。

好了,我今天讲的话算是很多了,现在要回去加班了。

null

如果35岁了,你根本get不到年轻人的点

杨光 36岁 从业14年

湖南 游戏行业

我2004年入行,毕业进入了一家在数据库这方面仅次于Oracle的外企。当时的待遇还是很好的,一个月工资有5000元,工作965,非常舒服。

在和我同一批次进入的人里,我学历很一般,他们都是清华、北大、科大的,但我技术很好,大三的时候就做过几个数据库的项目,这是我当时能进入这家外企的根本原因。

也因为这样,做了半年之后,我觉得没什么意思,也没什么增长空间了。因为公司够大,所有的程序我们只能修改,不能做大改变,在我看来这没什么技术含量。

而且,外企流程特别多。比如有一次我改一个差不多40多行的代码,对我来说非常简单,不到一天就能改完,但我花了一周的时间,大部分都用在了协调和沟通上,这跟我的性格不太相符。

于是,我选择离开外企,到联众做棋牌游戏,也是这个时候我正式步入游戏圈。2008年,做了一款知名的游戏后,我开始从单纯的技术人员向管理人员过渡,在游戏行业创过几次业,但2018年游戏行业骤冷,我开始深度思考这个行业。

我发现这个行业特别喜欢年轻人,因为你的客户是14岁到28岁的人,你如果35岁了,你根本get不到他们的点,这就跟拍电视剧一样,你让陈凯歌去拍小鲜肉,他拍不出来。

这个行业变化也挺快的,比如今年流行传奇类的,明年可能是养成类的,你怎么让一个天天打打杀杀的程序员去做温情的养成类游戏呢?他没有感觉的,而一旦年龄大了,学习能力也不如以前。

null

更重要的是,很多游戏公司的领导比较年轻,没人喜欢招一个比自己大的程序员,我 自己创业招人的时候也是更喜欢年轻人 ,我培养1个月就能上手了,而且性价比高。更何况,年轻人可以周六日在公司加班,年龄大的就得回家看孩子。

所以,去年我35岁的时候,选择离开游戏行业。我现在开始做医疗了,我的主要客户是35岁往上的,岁数越大,我对这些人的心态就越了解。

年轻的时候,我们非常崇拜求伯君、比尔盖茨、麦克戴尔这样的人,也希望可以靠技术起家,但现在已经没有这个机会了。

null

写代码谁都会,最牛的人是要做判断

田真 30岁 入行4年

北京 互联网公司程序员

在这个行业4年了,35岁的焦虑每年都会炒一次,说得好像35岁的程序员就不用活了一样。

但真实的情况不是这样。

以我个人为例,因为入行的时间有限,我目前的想法是希望自己在业务上不断锻炼,争取到了35岁有带队伍的能力,不仅仅是干一线的工作。这并不是说一线的工作不重要,是还有很多工作需要更复杂的精力去解决。退一步讲,不管是什么行业,如果你做了很多年还在一线,那基本上你有点儿不太上进。

null
刚刚参加工作的时候,我掌握了一个工具、搭建了一个框架、学习了一个语言就会觉得很满足。但是以后我肯定不会这么觉得。 就好比每次大的项目,最牛的人决定用什么框架、什么语言、什么工具,这决定整个项目的起点。  如果椅子搭好的话,后面会省很多事儿,如果没搭好,后面的麻烦就越来越多,搞不好会浪费时间和精力。

所以在一线工作时要慢慢积累这种经验,到最后慢慢具备这种能力。这是比简单写代码更重要的能力, 写代码谁都会,但不是谁都可以在重要的事情上做出重要的判断。

这个行业根本不存在吃青春饭这一说,我40岁的程序员朋友还从百度跳到阿里去了呢。关键就是40岁就要干40岁该干的活,35岁就要干35岁该干的活,你不能35岁还在干30岁干的活。

公司肯定是需要35岁以上的程序员的,但肯定不是35岁的一线工作人员。

null

硅谷的年龄危机远没有国内强烈

Joey 31岁

美国 谷歌软件工程师

在硅谷,没有公司会把年龄限制写进招聘信息里。拿谷歌来说,团队的年龄跨度很大,二十几岁的应届生和四十多岁的博士都很常见。但招人的时候,如果求职者年龄大,HR的期望值相应也会更高,除了问业务问题,还会考察这个人的格局。

在这里,程序员并不算是“高压”职业。时间灵活,看重效率,没有打卡一说,只要按时完成工作就好。我一般是上午9点半工作到晚上6点半,周末双休陪家人或是去攀岩。有时候因为想把手头的事尽快做完,我会主动加班几小时,但 来谷歌快三年,只有一次是真正为了赶进度加班到晚上12点。  和国内的996兄弟们相比,真的有点“惭愧”。

正常情况下,工程师工作七八年就会带团队了。当然也有一些人喜欢纯做技术,选择当技术大牛也没问题,一样受人尊重。总体来说, 大家的年龄危机和竞争氛围没有国内那么激烈。

null

一方面,虽然谷歌也像其他大公司一样会把任务拆解得很细,每个人负责一部分,但是谷歌的文化比较自由,老板不会事无巨细指挥你怎么做,而是给出空间发挥每个人的创造性。公司内部也鼓励流动,大家经常换组,尝试不同的方向和产品,在这个过程中能够接触新东西,不会觉得自己是在机械的重复劳动中慢慢变老。这一点可能和国内公司不太一样。

另一方面,我们的节奏没有那么快,工程师有大量时间可以学习新东西。这也是我觉得996不是一个好机制的原因,一个人的时间全部被工作填满,如果还做的是重复性的工作,很难有时间提升自己,长期来看很容易被淘汰。

我也会焦虑,但不是来自于年龄,而是关于职业成长。硅谷大部分人都拼命想学新东西,我是数学背景,如果在现在的组里不能把我的价值发挥到最大,我也会考虑换个环境。我一直提醒自己,要关注更长期的成长,个人价值才会一直上升。

都说程序员是吃青春饭,这句话暗含的意思是年纪大的程序员加班拼不过年轻人。 加不了班就没有价值?这不成立。  年纪大的程序员胜在经验,如果真的要靠熬时长,只能说明他干的活儿技术含量还不够。

我很崇尚个人奋斗,但不是砸时间才叫奋斗。 不过,硅谷的创业公司倒是很羡慕国内的刻苦劲儿,因为对初创公司来说,真的是效率决定生死。

null

特别愿意招经验丰富的大龄程序员

何建 38岁 入行16年

北京 零售电商行业

程序员大部分时间都比较忙,一忙就没有太多时间停下来思考。不深度思考,就认识不到怎么培养自己的核心竞争力。即便一个人有20年的工作经验,他的能力也是有边界的。如果本身是个安于现状的人,可能会面临中年危机。

对于被辞掉的外企员工,可能难融入国内互联网公司文化,这一点也有客观因素。国内的互联网公司做产品,可能三个月就得出一个版本,甚至再过一个月,做两个版本迭代都有可能。外企在速度上做不到,流程也极其复杂。

null

2000年-2012年这个阶段,技术的更新换代特别快,到2012年之后,技术圈大的革新已经没有了。包括现在的AI,整个技术还不是特别成熟,更多是在应用层面的落地。技术没有更新,或者没之前更新那么快,程序员们更容易陷入安逸。

我自己没有中年危机,但技术行业确实存在“年龄歧视”,包括我自己招人。我周围都是90后程序员,但我的偏见没有那么深。假如你是85年的,已经有十年工作经验,我会侧重考虑你的工作年限和实际经验是否匹配。

很多人工作了十年,还不如工作五年的,很可能是一路混过来的。 但反过来,年龄偏大,学习能力强,经验又足够丰富,我特别愿意找这种人,尤其是在工作之余兼职创业的。  这类人眼界开阔,知道创业路上可能有哪些坑,技术基础扎实的人写的代码质量也高,不需要太多额外人员为他服务。

所以说,年龄并不是决定性因素,还是要看这个人本身。

null

真正的常青树公司不会大招大裁

张军 35岁 入行10年

上海 蒹葭(嘉善)电子商务有限公司

这个行业的人才供应始终是冰火两重天的,高端人才稀缺难得,低端人才供应泛滥。 但真正有底蕴的常青树公司是不会大招大裁的,宁可提高门槛制造俱乐部效应。

我研究生毕业以后就加入了一线互联网公司,先后在百度等公司就职,现在也成了一名创业者。随之而来的节奏和眼界的变化也很清晰,在大公司工作,只能看到一个拼图的一小块,但在创业公司,每个人都要是超人,从开发到运维一肩挑,还要参与商业化,更实用主义。

大公司有严格的开发流程,从总体设计到详细设计、编码阶段、提测,然后交给运维上线,中间要花2到3周,甚至是2到3个月,初创公司没办法这么讲究,可能头天拿到需求,第二天就敢上线。

大环境一直在变,唯一不变的只有变化。所以最近几年,我必须保持学习新东西的状态,要说瓶颈的话,在于技术人转管理岗,适应起来时间会比较长,所谓“慈不掌兵,义不行贾”,打工者视角切换为leader视角,自己的性格会遇到新挑战。

我们部门平均年龄大概26岁,年龄代际必然会造成差异,但总的来说问题不大。年龄大带给我的优势就是经验的不断沉淀, 在老技术人眼里,没有多少真正的新东西,都是新瓶装老酒。

年龄本身不会给我带来危机感,带来危机感的是经济周期、行业周期、岗位需要的投入度与自身能够提供的投入度的差异。

null

干了这一行之后,基本没有上下班之分,只有醒着和睡着的区分。 坦率的讲,业界对程序员发迹线的消费是有悖科学精神的,秃不秃取决于基因。  头发掉得厉害的人,可能祖上有一些贵族基因,就像英国的查尔斯王子家族一样。很遗憾,我发际线至今还行。

我平时加班之后会去夜跑,一周三次,能够给我提供一个独立思考时机,整个公园很安静,感觉很好。

互联网人是持续学习者,持续奔跑者。目前我只实现了人生规划里的一部分,创业的野心一旦打开,就会一直在这条路上狂奔下去。

null

我们编程像木工,新兴程序员像宜家

孟誉国 41岁 从业16年

上海 ERP软件公司CEO

软件行业相对来说比较看经验,3-5年之后可能就完全换另外一套东西了。新的工具不会用,一脱节就很难跟上去。比如之前说打算盘,打得再好再快,计算器一出来经验就没用了。

与其等到人家觉得我不行,不如我自己早一点,在一个新旧替换的时代里主动选择创业,掌握主动权,然后做自己更想做也更有价值的事。

我现在自己会带小朋友,最明显的感觉是,老一代程序员喜欢亲力亲为,现在的一代多是拿来主义。 打个比方,我们老一辈是木工,喜欢自己从木头做成家具,现在的年轻人有点像宜家,买来现成的再自己搭 。新兴的程序员效率会更高,我们这一辈更能追根溯源,相辅相成吧。

年龄还有一个优势,可以让你更加冷静沉稳,不应该犯的错误会少犯一些。前几天有一个朋友让我去用黑客的方式,删掉别的管理系统里面的一个订单,早20年炫技也好要面子也好,我可能直接愿意帮忙,但现在我会跟他说:“技术上我肯定搞得定,但是我真的不能帮你。”

null

我一直比较乐天,心态比较年轻,之前上班的时候,我是公司请假最多的人。我比较喜欢旅游、摄影、做视频,玩吉他和电子音乐合成器,会参加一些公益拍摄活动,每年去海边潜水几次。

其实我也后悔,年轻的时候还是玩多了。那时候虽然也自学了不少东西,边查资料边摸索,学得特别快,但我觉得可以学得更多。我现在还在研究单片机,买了好多单片机的板和学习资料,但进度明显慢下来了。

不论是什么技术,只会把低端的程序员消灭掉。  我觉得程序员一定要热爱编程,完成了一个作品会觉得开心,而不是听说这行工资高就去临时速成甚至改行。招人的时候我也会碰到很多培训六个月就出来做程序员的人,我一般不太会去选这种。

同样一个东西,普通的程序员和有爱好的程序员都能实现,但是有爱好的会把它实现得更好。

null

程序员的职业方向也得赶风口

yanyan 31岁 从业6年

北京 手机厂商研发

35岁以上还在做一线程序员的情况其实挺多。

“程序员行业吃的是青春饭”这个说法,要分情况来看。如果在技术方面积累比较好,其实35岁以上的程序员还是比较吃香。但是如果一直写基础代码,没有比较深的技术积累,一直做到35岁非常危险。

null

甲骨文不是有个绰号叫中关村最大的外企养老机构吗? 一些被裁的年龄比较大的外企程序员虽然拿到的补偿会比较多,但他们最害怕找不到下家。  一是工作方向和工作强度跟国内程序员没法竞争,二是如果35岁以上还是纯基层的研发或者写代码的程序员,很难找到和原先岗位匹配的工作。

按照一般的职业发展路径,程序员可以发展成为某一个领域的技术专家,对标阿里的P7、P8,或者是在某一个技术方向上成为资深顾问,另一个方向就是晋升到管理层,负责项目开发或整个技术架构。这个方向不仅需要擅长技术,还要懂项目管理。

程序员行业的职业方向也是得赶风口,几年前头条、抖音发展起来,APP开发是热门方向。最近几年就是人工智能、自动驾驶最火,算法工程师和人工智能开发工程师这方面的岗位比较多。

我自己在职业方向上有时候会焦虑,但是这个行业本来就需要不断学习新东西,不然被淘汰的概率会比较大。如果再过几年,职位和收入达不到一个比较可观的情况,或者晋升的可能比较小,我可能会更焦虑,会怀疑自己是不是方向错了,或者考虑新的机会。

null

技术大佬从来不需要考虑年龄问题

Jay 32岁 入行7年

北京 电商平台后台开发

甲骨文裁员的消息,我关注过。互联网寒冬,哪个公司裁员都不稀奇,我们公司也有裁员。程序员一直是个比较容易焦虑的职业,尤其是技术没能随着年龄增长成正比的成长的话,更容易焦虑。

对大部分底层码农来说,程序员就是青春饭。HR或猎头找你,也会因为年龄问题拒绝你,我身边就有同事遇到。 但是技术大佬从来都不需要考虑年龄问题。

我们部门同事的平均年龄在30岁左右,年轻人比例不算大,年龄差异肯定是存在,毕竟大家的生活和成长经历摆在那。 我们的工作节奏比较紧凑,国内互联网公司不加班的应该不多。 掉头发也是肯定的,都不敢天天洗头,生怕哪天秃顶了。

null

如果单看收入,目前还算满意。毕竟程序员起薪就不错,不管是在什么城市,肯定都是高于当地平均工资不少。但是如果将工资和工作强度、消费水平放在一起看,那就性价比太低。

到目前为止,我还没换过工作。之前有拿过其他公司offer,后悔拒绝了。

null

庆幸当初入行早,让我现在衣食无忧

Nick 37岁 入行13年

深圳 台资公司研发助理

我已经过了35岁,但是我心态很好。

其实不是所有程序员都要累死累活的加班。  那些做产品研发的,有明确的上线时间,压力会非常大,加班也很多,如果年纪大了加不动班,当然会有危险。但像我们公司,主要是帮客户解决使用我们产品过程中遇到的问题,每年只有客户产品需要量产的5个月会忙一些,其他时间工作强度不算大。大家朝九晚六,周末双休,还能每周组织一次羽毛球比赛。

我们团队平均年龄30岁左右,但我观察,那些40岁左右的人,还是待得挺舒服。一是因为公司管理人性化,不会纯考核绩效,人员稳定。另一方面是相比于新人,老员工对公司的每一代产品更加熟悉,知道以往的产品有哪些局限、迭代的新品做了哪些更新。让新人发愁的问题,老员工可能早就经历过,能迅速能做出判断。

null

这几年,我的工作内容变化不是很大,但是圈子里的新技术是肯定要去学的。我身边甚至有朋友跨界开了处理芯片的加工厂, 我没有想要去外面折腾,这可能是因为我入行早,之前积累的收入已经足够我安居乐业,也就没有那么多动力去折腾了。

程序员的收入还是属于中等偏上,但以现在的消费水平和深圳的房价来说,入行早晚差异很大。我算是赶上了一个尾巴,所以还是要感谢当初选择这个职业,让我衣食无忧。

null

我愿意做一辈子程序员升级打怪

老铁 38岁 从业15年

北京 安全科技公司架构师

我们这行明明是“越老越吃香”, 我们有丰富的经验,是一名架构师,而不是普通的编码者。

我相信那些被甲骨文裁掉的员工也会被BAT等公司抢着要,当然,不包括少部分在大公司“养老”的人。确实有部分人在舒适区待了很多年,他们可能有十年的工作经历,却只有一年的工作经验,只是从“小白兔”熬成了“老白兔”,肯定要被时代淘汰。

这一行的更新迭代速度太快,我30岁以后,确实认真考虑过未来要如何发展,去报班考pmp(项目管理专业人士资格认证),挤出时间去上课,不断学习可以抵抗焦虑。

null

其实我们工作节奏还好,没有外界说的996那么夸张,以我们的任务量,只要在工作日每天非常专注、高效地工作6个小时就可以完成,很多人只是效率太低,拖到太晚。

但可能是年龄越来越大,最近身体确实有点吃不消了。上个月我后背上起了一大片带状疱疹,医生说是压力太大,导致免疫力低下。

就算如此,我从没想过换行业,我想我会做一辈子程序员,这是一个有技术含量,让我愿意一直打怪升级的工作!

我现在的阶段性目标是成长为一名CTO,或者是安全领域的技术专家,能够带领超过百人的团队完成项目。

这就好比登山,在你坚持爬到崖口,看到一片没有遮挡的蓝天时,你会发现一切都很值得。

*文中部分图片来源于视觉中国。应受访者要求,文中杨光、田真、张军、老铁为化名。


作者:燃财经编辑部
来源:mp.weixin.qq.com/s/5Cw-NzxjsRF2BwdSdGFzBQ
收起阅读 »

AI 搜索的价值在哪里

借鉴开源 Lepton Search 的灵感,在公司内部做了一款 AI 搜索工具,名为爱搜。这个工具目前处于带着做状态,没有投入什么人力和资源。遂想写点东西,记录下自己的一些想法和观点。不一定对,但都是吾之所悟。AI 搜索是什么AI 搜索是指利用人工智能技术,...
继续阅读 »

借鉴开源 Lepton Search 的灵感,在公司内部做了一款 AI 搜索工具,名为爱搜。这个工具目前处于带着做状态,没有投入什么人力和资源。遂想写点东西,记录下自己的一些想法和观点。不一定对,但都是吾之所悟。

AI 搜索是什么

AI 搜索是指利用人工智能技术,帮助用户更快找到需要的信息,提供更加精准和相关的搜索结果。

为什么要做 AI 搜索

  1. 现在 AI 是风口,所有产品前缀都可以加上 AI,搜索也不例外
  2. 人工智能可以帮人类承担一些搜索工作,之前人类需要在搜索上花一个小时,现在有了 AI ,只需要花 20 分钟甚至更少

怎么做 AI 搜索

从现在看,做出一个简单的 AI 搜索产品已经不存在技术难点了,有很多成熟的产品,如:

  • 国内:360AI 搜索、秘塔、天工等,还有一些内置到问答产品中,如 kimi
  • 国外:devv 、perplexity 等

下面我将从技术架构、应用层、接口层、模块层来阐述怎么做 AI 搜索产品。

技术架构

下图是我画的简单 AI 搜索产品架构示意图:

image.png

上图把架构分成了三层,分别是应用层、接口层和模块层,解释如下:

  • 应用层:可以是 web、native、桌面端、浏览器插件、sdk
  • 接口层:支持应用层的各种 api
  • 模块层:是搜索和 各种 agent 的核心实现

这应该是最简单的 AI 搜索架构了,复杂的我没有做过,就不画了。

应用层

目前一些 AI 搜索产品我都用过,直接参考秘塔、devv 和 perplexity 即可,三者页面如下图所示:

resized_image-2.png

整体布局相似,取他们精华,去他们糟粕就可以了。技术选型上,根据团队情况选择就行,如 vue 、 react。整体没有技术瓶颈,正常去开发实现即可。

接口层

基于 restful api 去和应用层对接,比如有以下接口:

  • 回答接口
  • 相关问题接口
  • 登录接口
  • 历史记录接口
  • 设置接口

这一层,也可以加上缓存功能,对于相同问题,直接返回缓存结果。也可以不加缓存,主要看业务需求。

爱搜接口层和模块层代码的目录结构如下图所示:

resized_image-4.png

使用 go 作为开发语言,整体合理。爱搜提供的接口如下图所示:

resized_image-5.png

除了自己用的接口,还给其他业务提供了一些能力支持。

模块层

这一层属于 AI 搜索的核心了,它能决定 AI 搜索的上限。模块层提供的能力越多,能力越强,产品的竞争力就越大。

上文的架构图画了两个模块:

  • 模块 1:搜索引擎 --> prompt --> 大模型
  • 模块 2:搜索引擎+爬虫 --> prompt --> 大模型

搜索引擎

搜索引擎的方案有两种,分别是付费和开源。如果用付费方案,则有百度、必应、谷歌、serper 等。如果用开源方案,则有 duckduckgo 、searxng 等。

  • 付费方案中,serper 是我认为目前最好的选择,理由是非常便宜、底层走谷歌搜索、速度很快并且国内没有被墙。
  • 开源方案中,我知道的有 searxng 和 duckduckgo ,searxng 更流行。

爬虫

  1. 在不加限制的搜索场景下,没有找到一个合适的爬虫方案,这种场景有两种方案:
  • 第一种方案:用传统的方法,拿到页面链接,然后解析页面内容,这种依赖页面 dom 结构,那么多页面,怎么去实现一个通用的解析逻辑,很难搞
  • 第二种方案:用 AI 能力,借助视觉模型,拿到页面链接,进入页面,对页面做视觉判断,需要用到什么数据,就拿什么数据,这种目前还没有尝试,感觉难度也大
  1. 如果加限制搜索场景,比如编程问题我只在 stackoverflow 、 reddit 、 github 上搜和爬取,这种是可以有合适的方案的。但是执行爬虫后,返回速度是不是会变慢,这个因素也需要考虑。

目前爱搜是没有做爬虫方案的,主要是没有想好怎么做。用过 kimi 的,都知道回答会有资料作为参考,如下图所示:

resized_image-6.png

我比较好奇的是,kimi 有没有爬取资料 url 的页面内容。还是说,只是把调搜索引擎拿到的搜索结果展示出来,或者说,会根据问题有选择的爬取资料页面。

目前用 AI 做爬虫的开源项目也有一些,但到目前为止,我还没有找到一个适合所有搜索场景的爬虫方案。

prompt

prompt 的设计有几个痛点:

大而全的 prompt 很难调

你想靠一个 prompt 解决搜索问题,是几乎不可能的,需要对 prompt 从上到下进行拆分,如下所示:

  • prompt
    • 断言 prompt:判断搜索问题是什么类型
    • 编程 prompt
      • 错误解决
      • 功能实现
      • xxx
    • 非编程 prompt
      • 新闻类
      • 医学
      • xxx

如果想让回答更加符合用户想要的,prompt 的设计就需要考虑原子化。有利于维护、适配和扩展。

很依赖大模型的能力

如果未来的大模型能力比现在强大千倍,那也许一个大而全的 prompt 就够了,但现在,还做不到这种。你设计的一个 prompt 在 X 模型上表现很好,但换到 Y 模型上,表现可能就变差了。

上文将 prompt 从上到下进行拆分,变的小而精,也是为了增加鲁棒性,让其在不同模型上都能有很好的表现效果

prompt 的设计准则太多了

据我了解,有很多提示词设计准则,像 CoT、CO-STAR、3S、微软出的 prompt 设计教程等。给我的感觉就是:到底哪个是最佳实践,估计目前没有最佳实践,这给 prompt 设计,又带来了一些困难,不同模型的 prompt 最佳实践可能不一样,如何在 prompt 上屏蔽掉这个因素,是值得思考的事情,将 prompt 拆小,在一定程度上做了屏蔽。但是也会有无法兼容的情况,这种就需要根据模型来单独设计适合它的 prompt 了。

prompt 也需要后期

有时会发现,在模型固定的情况下,不管你怎么设计 prompt ,某一个场景的输出就是有问题,这个问题大多是指输出不够稳定。

比如一个问题的回答,需要输出字符串数组,这个问题问 10 次,会偶然出现一个输出数字数组,或者直接不是数组,这种情况怎么办,从我的观点看,这种情况就需要做后期处理了,通过写程序去识别这种情况,并做相应的处理,保证返回的永远都是字符串数组。

prompt 自动化测试

prompt 本身不太可控,如何在迭代过程中,做到对 prompt 有一个稳定的监控,这就需要在 prompt 自动测试上做一些能力,比如:

  1. 自动生成各个类别的问题,每个类别生成 10 个问题,
  2. 自动去跑 prompt,每个问题,跑十遍 prompt
  3. 将相同类别的相同问题跑出的结果进行对比,分析结构和内容是否相似
  4. 将相同类别的问题跑出的结果进行对比,分析此类别的输出结果是否稳定、准确

模型

模型的重要性不言而喻,当前模型界应该是最卷的领域了,如何评估和选择模型是一个很重要的事情。就目前来说,模型选对了,产品的成本可能会降一半,效果还会更好。

模型和 prompt 配合

上文 prompt 也阐述了相关内容,模型和 prompt 工程形成良性的循环,是我们必须要去做的事情

私有化模型的挑战

如果不使用第三方模型 api,使用私有化模型,那需要做以下事情:

  1. 评估和选择模型
  2. 模型部署,要买卡,或者走托管服务
  3. 模型微调【可能需要,但如果想更好,大概率需要做】

买卡的话,成本就变大了。模型大小也要考虑,“越大”,需要的算力越多。从控制成本角度看,方向如下:

  1. 采取面向模型开发模式,用合适且性价比高的模型去解决不同的业务场景
  2. 模型倾向于选择 MOE ,在“小”的同时,获得高质量的输出结果
  3. 让 prompt 多发力,再加上后期,也可以让“小”模型的效果逼近“大”模型的效果
  4. 选择正确的微调方案,这里我没有经验,目前业界有预训练、SFT、RLHF、LORA、指令微调等
  5. 模型侧要保证性能和准度,就是输出结果要快和准,相同参数级别模型
    • a:想更快,可以尝试用 bit 更小的量化模型,测试输出效果会不会有明显差别,没有的话,就可以考虑用,这样会提高模型性能
    • b:想更准,需要根据情况做处理,比如做指令微调

AI 搜索商业价值

我先说下,目前 to c 产品的一些价值场景

  • 360:回答页面加了广告...

resized_image-7.png

  • 天工:目前没看到付费场景,但是从我的角度看,天工做的还可以,agent 很多,包括 ai ppt、数据分析等

resized_image-14.png

  • 秘塔:免费版搜索次数有限制,目前没看到上限付费版

resized_image-8.png

  • devv:按月/年付费,可获无限次 agent 使用、gpt-4o 模型等其他付费功能

resized_image-9.png

  • perplexity:按月/年付费,付费功能如下图所示:

resized_image-10.png

从我的角度看,这些 AI 搜索产品,还没有到让我付费的程度。也就说,已经 To C 的产品,我都没有付费的意愿,那在公司内部搞的 AI 搜索工具,如何去落地或者呈现价值呢?

以下有我的几点思考和看法

多在 AI Agent 上发力

AI Agent 概念:即人工智能代理,是一种利用人工智能技术来执行特定任务或服务的软件程序。AI 代理可以模拟人类智能行为,进行自主决策、学习和交互。它们可以应用于多种领域,包括但不限于客户服务、数据分析、自动化任务、个人助手等。AI 代理能够处理复杂的任务,提高效率,减少人为错误,并为用户提供更加个性化和智能化的服务体验。

这里我举一些 Agent 例子:

  1. RSS 订阅自动总结和推送 Agent 对 RSS 订阅有强依赖的用户群体,这个功能就能产生较大的价值
  2. 科技、手机、AI 等主题新闻,最新咨询日报生成和推送 通过 AI 搜索去自动搜索各主题最新新闻并进行阅读,最后输出新闻内容总结和高质量点评,对于提高用户的行业前沿资讯感知是有价值的
  3. 简历分析和评估,上传一个简历,会自动分析简历内容,给出评估报告和面试时需要问的面试问题

当前的 Agent,我更倾向于做一些小而美的 agent,太宏大的 agent,实现起来很困难,一方面受限于技术,一方面也会受限于算力

内网的搜索和总结要做好

  1. 内网的知识库:包含文档、pdf、各类分享视频
  2. 业务相关的文档

可以在搜索页面加一个搜索范围,像 perplexity 那样:

resized_image-13.png

上图显示的 内网->知识库 是我按 f12 改了下 dom 内容。

这些功能,爱搜目前都没做,看起来几句话,实际需要不少工作量。就拿 pdf 解析来说,目前业界对于复杂 pdf 的解析好像都没有太好的方案,我试过很多开源项目,都达不到我的理想需求,最近我又看到一个很不错的开源项目,叫 trieve ,其特性如下图所示:

resized_image-11.png

这个开源项目已经获得 YC 的投资了,证明其还是有技术和潜力的。目前是我看到对 pdf 分块、解析和搜索最好的开源项目了。后续多研究下这个项目。大家有什么好的开源方案也欢迎告知我。

业务相关的文档,做起来难度也大,爱搜目前也没有做,如果做的话,整体思路如下:

业务上可以根据你的登录信息,查你当前拥有的业务权限,然后允许用户选择搜索哪个业务,比如业务 A 所有的项目管理文档,包含策划文档、策划评审意见等,然后对用户选择的业务进行训练和搜索,后续用户可以在业务 A 选项中搜自己想要的内容,并获得相应的回答和索引。

多和公司内部业务联动

比如给某个业务提供联网搜索能力、提供搜索能力、提供爬虫能力等,类似这种多去和内部业务沟通交流,也能发挥落地一些价值

总结

  1. 想一下,bing 和谷歌做 AI 搜索,都被外界喷效果差,就知道要做好 AI 搜索还是很有难度的。
  2. 当然,bing 和谷歌的目标和我们不一样,我们更专注于垂直领域,我希望做小而美的 AI 搜索,它可以是一个产品矩阵,也可以是一个聚合产品
  3. 我们聚焦的是目前世界上最前沿的领域,有困难很正常

商业价值不是靠讨论出来的,而是靠试出来的。


作者:ikun日记
来源:juejin.cn/post/7373921342096080911
收起阅读 »

当程序员写代码就行了,为什么还要画图

相信很多人选择当程序员除了这个行业起步阶段薪资比其他行业高一些之外,还有一个很大的因素是觉得做研发类的工作只要代码写的好,跟电脑这个“直男”打交道就可以了。 但是还没走出校门呢,毕业这一关就得边做毕业设计边写毕业论文。写毕业论文可不是直接把自己的实现代码从ID...
继续阅读 »

相信很多人选择当程序员除了这个行业起步阶段薪资比其他行业高一些之外,还有一个很大的因素是觉得做研发类的工作只要代码写的好,跟电脑这个“直男”打交道就可以了。


但是还没走出校门呢,毕业这一关就得边做毕业设计边写毕业论文。写毕业论文可不是直接把自己的实现代码从IDE粘贴到Word里,但凡代码多一点就会被老师要求修改,老师会告诉你要把你做的毕业设计的功能、设计思路、关键部分的实现细节用绘图结合文字表达清楚。


这时很多同学就犯难了,匆匆拿出大三的教材《软件程序设计》看看上面的那些的图都怎么画的,再找找网上的例子,模仿着能画出个类似下面的流程图。



上面这个图乍一看还可以,但是网上关于流程图的语法好几个版本,每个人画出来都不一样,而且用这种形式表达大一些的流程,就完全感觉眼花缭乱。


画图的底层逻辑是沟通


其实走到工作岗位上之后我们仍然面临着这样的问题,除了专心写代码外,我们的工作构成中还有大量的需求评审、需求分析、技术评审、系统方案设计这样的需要人和人进行沟通的环节,讲逻辑、讲实现方案、讲设计思路的沟通环节。究其原因就是因为IT行业分工明细,它像其他行业的工作流水线一样涉及大量工种的合作,但又因为交付的软件并不像工业流水线一样是标准件,所以上面列举的每个环节都需要良好的沟通,让各职能人员之间建立共识、建立统一语言后才能完成高效协作交付产品。


既然需要良好的沟通建立统一语言,那么我们在需求分析、技术评审、系统方案设计文档上就不能使用太主观的语言,也不能把实现代码直接往文档上粘,那样的话且不说有的岗位不写代码,即使同岗位的其他同事也没有那个精力一行一行的仔细看完你的代码、理解你的思路。


所以这就需要我们能够简洁、高效地用人们都能看懂的专业图形描述出软件开发这些环节中需要重点沟通的需求逻辑和技术的关键细节。


我们都知道从事理工专业的人,可能对构图、色彩这些不太擅长,那么有没有一种图形不需要美术基础就能掌握,足够专业让图形能突出我们想表达的技术细节,同时还足够简洁即使是不太懂技术的人也能完全看懂呢?在IT领域还真有,那就是UML。


比如同样是表达需求的业务流程,用流程图表达的就是上面图-1的那个样子,但是用表达力更强,更注重语法的UML活动图表达流程的话就是下面这个样子。



关于怎么用活动图分析表达流程,后面会有专门的章节去给大家讲解。


程序员画图难的成因


说到UML,无论是大学里还是市面上讲解UML的书籍中对UML的讲解都太过枯燥了,它们通常都是以技术和软件设计的角度来讲述UML的,通常上来会先讲解一大堆图,哪些是结构建模,哪些是行为建模,紧接着就是各种图的一堆语法(画法),或者是给出的示例太过于技术化,完全脱离日常生活让人无法理解。


这就给我们这样一开始不太懂的人一种UML太过专业太过复杂,不好用的印象。想的那么清楚画出图来,代码早写好了。典型的例子就是如果画类图把类的各个属性和方法都想好画出来也太费时间了,况且需求多变还要经常改,还有就是那些类图表示的类的关系一会儿是箭头、一会儿是虚线、不明白他们都什么区别,看多了就头疼。


其实上面这个现象完全就是误区,UML完全不是必须那么复杂--把所有细节都表示出来才算完事,我们完全可以从需求分析阶段开始就开始使用,在分析的过程中构思业务的结构并画出来它大概的样貌。



后面随着对需求的进一步了解再去补全或者调整其中的内容。写技术文档常用的UML图除了能像上面这样使用类图分析业务的结构,还有活动图、顺序图、状态机图从不同角度分析业务的行为,而且是循序渐进的使用,不是上来把这些都用上。


早期对业务知晓不够透彻时UML图可以画的粗略些,流程分析也只先分析明白大流程即可,随着使用UML分析业务的过程对业务逐渐了解后再逐渐细化以及使用不同的图形从不同角度描述业务。 UML家族里提供的各种图,也不局限于只能用于技术分析,甚至需求用例、系统架构、IT架构方面的需求也能够使用UML进行描述。


掌握UML让自己有更多可能


无论是一线研发,还是已经转型项目经理、产品经理或者团队管理的人员或者是想要上车入行的萌新程序员,本课程都能让你收益颇多,让你掌握产品经理写需求的一些基本技能,也让你轻松应对项目经理参与竞标和项目管理时的文案编写工作。


同时还能让你管理项目质量时找到“抓手”,通过在项目团队建立技术评审、方案设计等相关机制--融合团队成员对UML的使用,让团队成员的思维性创造更容易被周知也让这些内容更容易被Review,从而达到项目开发期间高效的沟通和良好的质量保证。


职场上的“汇报困境”


除了上面讲的这些我们工作中干活需要用到的各种图形外,在职场上班和在学校上学有一个重要的区别就是我们时不时的就要被拿出来评比、通晒、述职,这些场合都会要求我们做汇报。


针对这个程序员在职场中的普遍痛点,推荐一下我用大半年的时间沉淀,汇集了我多年职场经验的画图课,解决程序员普遍只愿意埋头写代码,不会做需求分析、不会做技术评审、不会画架构图、述职汇报做不好,等等这些需要画图和表达能力的事情的时候就犯难的问题,帮助大家摆脱代码的单一维度,从多维度提升自己,建立自信,让你在工作中更游刃有余


课程最后一部分还会扩展一些互联网开发人员在职场中应对各种汇报的策略,讲述一些写汇报PPT的主旨思路,侧重点和注意事项。同时也讲一些使用堆砖块画法(我自己总结的)给汇报PPT进行配图的思路,怎么通过这些图快速抓住听众的眼球建立共识,以及怎么使用一些配图讲解规划给上级“画饼”来获得他们的支持从而进一步获得他们后续在资源上的支持,更好地开展工作,这些技巧我们在课程最后一部分都会讲到。


相关推荐


现有有两种订阅方式


方式1微信专栏:程序员的全能画图课


方式2小报童专栏:程序员的全能画图课


作者:kevinyan
来源:juejin.cn/post/7370615140242472998
收起阅读 »

我的 CEO 觉得任何技术经理都是多余的

原文 QUESTIONABLE ADVICE: “MY BOSS SAYS WE DON’T NEED ANY ENGINEERING MANAGERS. IS HE RIGHT?” 我最近加入了一家初创公司,负责管理一个约 40 名工程师的团队,担任技术...
继续阅读 »

原文 QUESTIONABLE ADVICE: “MY BOSS SAYS WE DON’T NEED ANY ENGINEERING MANAGERS. IS HE RIGHT?”



file


我最近加入了一家初创公司,负责管理一个约 40 名工程师的团队,担任技术副总裁。然而,我与 CEO(之前是工程师)在是否需要雇佣专职技术经理的问题上产生了很大的冲突。目前,工程师们被分成了 3-4 人的小团队,每个团队有一个工程师头头,负责领导团队,但他们的主要职责仍然是编写代码和交付产品。


我有 HC 在未来一年雇佣更多的工程师,但没有经理的 HC。老板认为我们是初创公司,负担不起这种奢侈品。在我看来,我们显然需要技术经理,但在他看来,经理只是多余的开销,在我们的阶段所有人都应该全力编写代码。


我不知道该如何论证。在我看来这很显然,但实际上我很难用言语表达为什么我们需要技术经理。你能帮帮我吗?


—— 真的是多余的开销吗(?!)




这里有很多问题需要解答。


你的首席执行官不理解为什么需要经理,这并不奇怪,因为他似乎不明白为什么需要组织结构。🙈 他为什么要对你如何组织团队或你可以雇佣哪些角色进行微管理?他雇用了你来做这份工作,却不让你完成。他甚至不能解释为什么不让你做。这不是个好兆头。


但这个问题确实值得思考。我们假设他不是故意要刁难你。😒


我能想到两种论证雇用技术经理的方式:一种是相当复杂的,从第一性原理 (First Principle) 出发,另一种非常简单,但可能不太令人满意。


我个人对权威有一种强烈的反感;我讨厌被告知该做什么。直到最近,我才通过系统理论的视角,找到了一种对层级制度既健康又实用的理解。


为什么组织中存在层级制度?


层级制度确实带有很多负面包袱。我们许多人都有过在层级制度下与经理或整个组织打交道的不幸经历。在这些地方,层级制度被用作压迫的工具,人们通过垄断信息和玩弄权力游戏来提升地位,决策则是通过权力压制来做出。


在那种地方工作真的是一种折磨。谁愿意将自己的创造力和生命力投入到一个感觉像《呆伯特》漫画的地方,明知道自己的价值被极少认可或回报,而且这些价值会慢慢地但确实被压制掉?


file


但层级制度本质上并非是专制的。层级制度并不是人类为控制和支配彼此而发明的一种政治结构,它实际上是自组织系统的一种属性,是为了子系统的有效运作而出现的。事实上,层级制度对复杂系统的适应性、弹性和可扩展性至关重要。


让我们从一些关于系统的基本事实开始,为可能不熟悉的人介绍一下。


层级是自组织系统的一种属性


一个系统是「由相互依赖的组件组成的网络,这些组件共同工作以实现一个共同目标」(W. Edward Deming)。一堆沙子不是一个系统,但一辆车是一个系统;如果你把油箱取下来,车就无法运作。


子系统是一个在更大系统内有较小目标的元素集合。在一个系统中可以有很多层次的子系统,它们相互依存地运行。子系统总是为了支持更大系统的需求而工作;如果子系统只为自己的最佳利益优化,整个系统可能会挂掉(这就是「次优」(suboptimal)这个术语的由来 😄)。


如果一个系统能够通过多样化、适应和改进自身使自己变得更加复杂,那么它就是自组织的。随着系统自组织并增加其复杂性,它们往往会生成层级 —— 即系统和子系统的排列。在一个稳定、有弹性和高效的系统中,子系统在很大程度上可以自我管理、自我调节,并为更大系统的需求服务,而更大系统则负责协调子系统之间的关系并帮助它们更好地发挥作用。


层级最小化了协调成本,减少了系统中任何部分需要跟踪的信息量,防止信息过载。子系统内部的信息传递和关系比子系统之间的信息传递或关系要密集得多,延迟也少得多。


(对于任何软件工程师来说,这些应该都很熟悉。模块化,对吧?😍)


按照这个定义,我们可以说,经理的工作就是在团队之间进行协调并帮助他们的团队表现得更好。


对社会技术系统的二分是伪命题


你可能听过这个谬论:「工程师搞技术,经理搞人。」我讨厌这种说法。😊 我认为这完全误解了社会技术系统的本质。社会技术系统中的「社会」和「技术」并不是截然分开的,而是相互交织、相互依存的。事实上,很少有纯粹的技术工作或纯粹的人际工作;有大量涉及两种技能的粘合工作。


看看任何一个有效运作的工程组织除了编写代码之外还要做的一部分任务:



  • 招聘、建立人脉、面试、培训面试官、汇总反馈、撰写职位描述和职业发展路径

  • 每个项目或承诺的项目管理、优先级排序、管理利益相关者和解决冲突、估算规模和范围、进行回顾会议

  • 召开团队会议、进行一对一交流、提供持续的成长反馈、撰写评审、代表团队的需求 架构设计、代码审查、重构;捕获 DORA 和生产力指标、管理警报量以防止倦怠


许多工作可以由工程师完成,而且通常也是如此。每家公司对这些任务的分配方式有所不同。这是一件好事!你不希望这些工作仅由经理来做。你希望个人贡献者共同创造组织,并参与其运行方式。几乎所有这些工作由有工程背景的人完成会更有效。


所以,你可以理解为什么有人会犹豫是否要把宝贵的人员编制花在技术经理上。为什么不希望技术部门的每个人的主要工作都是编写和交付代码呢?这不是从定义上说最大化生产力的最佳方式吗?


额……😉


技术经理是一层有用的抽象


理论上,你可以列出所有需要完成的协调任务,并让不同的人来负责每一项。但实际上,这是不切实际的,因为这样每个人都需要了解所有事情。记住,层级制度的主要好处之一是减少信息过载。团队内部的沟通应该是高效和快速的,而团队之间的沟通则可以少一些。


随着公司的扩展,你不能期望每个人都认识其他所有人;我们需要抽象的概念才能运作。经理是他们团队的联络点和代表,充当重要信息的路由器。


file


有时我把经理想象成公司的神经系统,将信息从一个部门传递到另一个部门,以协调行动。将许多或大部分功能集中到一个人身上,可以利用专业化的优势,因为经理会不断建立关系和背景知识,并在他们的角色中不断改进,这大大减少了其他人的上下文切换。


管理者 (Manager) 日程与创造者 (Maker) 日程


技术工作需要集中和专注。上下文切换的成本很高,过多的中断是挺要命的。而管理工作则是每小时左右进行一次上下文切换,并且一整天都要应对各种打断。这是两种完全不同的工作模式、思维方式和日程安排,无法很好地共存。


通常,你希望团队成员能够把大部分时间花在直接为他们负责的成果做出贡献的事情上。工程师只能做有限的粘合工作,否则他们的日程安排就会变得支离破碎,从而无法履行他们的承诺。而管理者的日程安排本身已经是支离破碎的,因此让他们承担更多的粘合工作通常不会带来太大干扰。


虽然并不是所有粘合工作都应该由管理者来完成,但管理者的职责是确保所有工作都能完成。管理者的职责是尽量让每个工程师都能从事有趣且具有挑战性的工作,但不能让他们感到过于负担重,还要确保不愉快的工作能公平分配。管理者还要确保,如果我们要求某人完成一项工作,就必须为其配备成功完成这项工作所需的资源,包括专注的时间。


管理是问责的工具


当你是工程师时,你对自己开发、部署和维护的软件负责。而作为经理,你则对团队和整个组织负责。


管理是一种让人们对特定结果(如构建具备正确技能、关系和流程的团队,以做出正确的决策并为公司创造价值)负责的方式,并为他们提供实现这些结果所需的资源(预算、工具和人员编制)。如果你不把组织建设作为某人的首要任务,那么这就不会成为任何人的首要任务,这意味着它可能不会得到很好地执行。那么,这该由谁负责呢,CEO 先生?


你对技术负责人、工程师或任何负责交付软件的人在「业余时间」能完成的任务有一个合理的上限。如果你试图让技术负责人负责构建健康的工程团队、工具和流程,那么你就是在要求他们在同一个日历里做两份时间不兼容的工作。最可能的情况是,他们会专注于自己觉得舒适的成果(技术成果),而在后台堆积组织债务。


在自然层级中,我们向上看是为了目标,向下看是为了功能。简而言之,这就是我们需要技术经理的复杂原因。


选择无趣的技术文化


更简单的论点是:大多数工程组织都有技术经理。这是默认设置。多年来,许多比你或我更聪明的人花了大量时间思考和调整组织结构,这就是我们得到的结果。


正如丹-麦金利(Dan McKinley)的名言,我们应该「选择无趣的技术」。无趣并不意味着不好,而是意味着它的能力和失败条件是众所周知的。你只能获得少数的创新点数,因此你应该明智地将这些点数用在能够成就或毁掉你业务的核心差异点上。文化也是如此。你真的想把你的点数用在组织结构上吗?为什么?


无论好坏,层级组织结构是众所周知的。市场上有很多人擅长管理或与管理者合作,你可以雇佣他们。你可以接受培训、指导,或者阅读大量的自助书籍。有各种各样的管理哲学可以围绕它们来凝聚团队或用来排除其他人。另一方面,我所知道的无经理实验(例如 Medium 和 GitHub 的全员自治,或 Linden Lab 的「选择你的工作」)都被悄然放弃或被颠覆了。在我的经验中,这并不是因为领导者疯狂追求权力,而是由于混乱、缺乏重点和执行不力。


当没有明确的结构或层级时,结果不是自由和平等,而是「非正式的、不被承认的和不负责任的领导」,正如《无结构的暴政》中详细描述的那样。事实上,这些团队往往是混乱、脆弱和令人沮丧的。我知道!我也很生气!😭


这个论点并不一定能证明你的 CEO 是错的,但我认为他的证明标准比你的要高得多。「我不想让我的任何工程师停止写代码」并不是一个有效的论点。但我也觉得我还没有完全解决生产力的核心问题,所以我们再来讨论一下这个问题。


更多代码行数 ≠ 更高生产力


简要回顾一下:我们在讨论一个有约 40 名工程师的组织,分成 10 个小组,每组有 3-4 名工程师,每组都有一个技术负责人。你的 CEO 认为,如果有人停止全职编程,这个减速将是你们无法承受的。


也许吧。但根据我的经验,由经验丰富的技术经理领导的几个较大团队,将远远优于这些小团队。这差距很明显。而且,他们可以以更高效、可持续和人性化的方式完成工作,而不是这种拼命的死命赶工。


系统思维告诉我们原因!更少的团队,但规模更大,你会有更少的整体管理开销,且大大减少了团队内慢且昂贵的协调。你可以在团队内部实现丰富、密集的知识传递,从而实现更大面积的共享。每组有7-9名工程师,你可以建立一个真正的值班轮换,这意味着更少的英雄主义和更少的倦怠。你需要进行的协调可以更具战略性,减少战术性,更具前瞻性。


五个大团队是否能比十个小团队编写更多的代码行数,即使有五名工程师成为经理并停止编写代码?可能会,但谁在乎呢?你的客户根本不关心你写了多少代码行数。他们关心的是你是否在构建正确的东西,是否在解决对他们重要的问题。关键是推动业务前进,而不是单纯地编写代码。不要忘记,单纯地编写代码会产生额外的成本和负面效应。


决定你速度的是你是否把时间花在了正确的事情上。学会正确决定构建什么是每个组织都必须自己解决的问题,而且这是一项持续不断的工作。技术经理不会做所有的工作或做出所有的决策,但根据我的经验,他们对于确保工作顺利进行并且做得很好,绝对至关重要。正如我在上篇文章中写到的,技术经理是系统用来学习和改进的反馈循环的载体。


管理人员是否会成为不必要的开销?


当然有可能。管理的核心是协调团队之间的工作并提升团队的运作效率,所以任何减少协调需求的方式也会减少对管理的需求。如果你是一家小公司,或者你的团队成员都是非常资深且习惯合作的,那么你就不需要太多的协调。另一个重要因素是变化的速度;如果你的公司在快速增长或者人员流动频繁,或者面临很多时间压力或频繁的战略调整,你对管理人员的需求就会增加。但也有许多较小的组织在没有太多正式管理的情况下运作得很好。


我不喜欢「开销」这个词,因为 a) 这有点粗鲁,b) 称管理人员为「开销」的人通常是不尊重或不重视管理这门技艺的人。


但管理实际上确实是开销😅。许多其他的粘合工作也是如此!这些工作很重要,但它们本身并不能推动业务向前发展;我们应该尽量只做那些绝对必要的工作。粘合工作的天然属性使得它很容易扩散,吞噬所有可用的时间和资源(甚至更多)。


限制是好的。感觉资源不足是好的,这应该成为常态。管理很容易变得臃肿,管理人员可能非常不愿意承认这一点,因为他们从来没有感到压力或紧张减少。(事实上,情况可能恰恰相反;臃肿的管理层可能会为管理人员带来更多工作,而精简的组织结构可能会让他们反而感到压力更小。官僚主义往往会自我发育。特别是当管理层过于关注晋升和自我时。这也是确保管理不应仅为升职或统治的又一个充分理由)




管理也很像运营工作,当它做得好的时候,是看不见的。评估管理人员的工作可能非常困难,尤其是在短期内,而决定何时创建或偿还组织债务是一个完全不同的复杂问题,远远超出了这篇文章的讨论范围。


但是,是的,管理人员绝对可以成为不必要的开销。


然而,如果你有 40 个工程师都向一个副总裁汇报,而没有其他人专门负责人员、团队和组织相关的工作,那么我可以相当肯定地说,这对你来说目前不是一个问题。




作者:Bytebase
来源:juejin.cn/post/7373226679730536458
收起阅读 »

接了个私活,分享下我是如何从 0 到 1 交付项目的

web
大家好,我是阿杆,不是阿轩。 最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。 我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。 前前后后弄了一个半月到两个月,也算是积累了一点经...
继续阅读 »

大家好,我是阿杆,不是阿轩。


最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。


我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。


前前后后弄了一个半月到两个月,也算是积累了一点经验,分享给大家,如果以后也接到私活,可以参考一下我的开展方式。


由于文中涉及到实际业务的东西不方便透露, 下面我将用图书管理系统来代替实际业务,并且称这位校友为“老板”。


image-20240421154347807


总览


我接手的这个项目是完完全全从0开始的,老板只有一个idea,然后说他的idea是来自于另一个小程序的,有部分内容可以参考那个小程序,其他什么都没有了。


先讲一下我的总体流程:



  1. 确定老板的大致需求,以及预期费用

  2. 详细梳理开发功能点,并简单画下原型图

  3. 工时评估,确定费用

  4. 出正式的UI设计稿

  5. 拟定合同、签合同

  6. 开发阶段

  7. 验收、上线


大概就是这么些步骤,也对应本文的目录,如果你有想看的部分,可以直接跳转到对应的部分。


下面我会详细讲讲每一步我都做了些什么。


确定需求


首先老板找到我,跟我说他想做一个图书管理的微信小程序,然后讲了几个小程序内的功能点。


我也向他提了几个问题:



  1. 预算有多少?



    这个肯定得问的,要是预算和工作量严重不匹配,那肯定做不了的。毕竟都是出来赚钱的,总不能让咱用爱发电吧?




  2. 预计一年内会有多少用户量?会有多少数据量?



    这个问题我主要是想知道并发量会有多少、数据量会有多少?这样方便我后续判断系统需要的配置,也便于我后续对整个系统的设计。


    好在整体用户量和数据量都不大,这对我来说也就没什么压力了,至于以后会发展到如何,这不是我该考虑的事情,我顶多把代码写好看点,他后续发展壮大了肯定是把项目接到公司里雇人做的,跟我也没什么关系。




  3. 你那边能够提供什么?



    这个主要是看对方有什么资源,是否能够对项目开发有一定的帮助。


    在我这个项目里,老板那边是什么都没有的,没有设计图、没有服务器资源、也没有辅助人员,所有内容都包揽在我这边,然后有什么问题就直接问他。




  4. 你希望多久完成?



    如果老板很急的话,那可能得多叫几个人一起做,如果时间充足的话,自己一个人做也不是不可以。





好了,第一次对话大概就是这么些内容,但仅靠一次对话肯定是无法确定需求的,只能了解个大概。


我根据老板的想法,写了一份 需求分析 出来,首先列出了几个大概的功能点:


大致功能点列举


然后根据这些功能点进行扩展,把所有功能列举出来,画成一个思维导图(打码比较多,大家将就将就😅):


延伸的思维导图


好,那么第一版的需求分析差不多就出来了,接着我打电话给老板,对着这个思维导图,一个一个的跟老板确认,需不需要这些功能。


老板一般会提出一些异议,我再继续修改思维导图,直到老板觉得这些功能他都满意了。当然这过程中我也会给一些自己的建议,有些超预算的功能,还是建议他不要做。


到这里,需求就基本确定好了。


梳理开发功能点、绘制原型图


由于我不会前端开发,只是个简单的后端仔,所以我还找了一个前端同学一起做。


我和前端两个人根据前面的需求文档,详细的梳理出了 小程序 和 后台管理系统 的功能,这个部分是比较重要的,因为后续画设计稿和开发都会以这份文档为主:


小程序功能梳理文档


还画了一些简单的原型图,这玩意丑点没事,能让人看懂就行🤣🤣:


小程序原型图-我的信息


后台管理系统原型图


这些东西弄完之后,再找老板进行一遍确认,把里面每个点都确认下来,达成共识。


工时评估,确定费用


老板觉得OK了,就到了该谈钱的时候了,前面只是聊了预算,并不是正式的确定费用。


那咱们也不能张嘴就要,要多了老板不乐意,要少了自己吃亏。


所以咱们先评估下工时,这边我分了几个部分分别进行工时评估:



  • 需求分析、功能梳理(就是前面做的那些,还没收钱的呢)

  • UI设计、交互设计

  • 前端开发

  • 后端开发

  • 系统运维(包含服务器购买、搭建、认证、配置等)

  • 后期维护


其中设计稿是找另一位朋友做的,钱单独算,然后其他部分都是我和前端同学两个人评估的,评估的粒度还是比较细的,是以小时为单位进行计算的,给大家大概看一下:


前端开发工时评估


后端开发工时评估


评估完之后汇总一下,然后根据我们自己工作的时薪,给老板一个最终的定价,正常的话还需要在这个定价上再乘一个接单系数(1.2~1.5),但是我们这个老板是校友啊,而且预算也不多,所以就没乘这个系数了(还给他打了折😂,交个朋友)。


定价报出去之后,老板觉得贵了怎么办?很简单,砍功能呗,要么你去找别人做也行。



预付订金



我觉得正常应该在梳理功能之前就要付一部分订金,也不用多少,几百块就行,算是给我们梳理功能的钱。



这里接下来就要画UI图了,我们先找老板付个订金,订金分为三部分:



  • 给前端的订金

  • 给后端的订金

  • 给UI同学画设计稿的完整费用


因为UI设计是我这边联系的,所以我肯定得先保障她的费用能完整到手,不然到时候画完图跟我说不做了,那我咋对得起画图的人。


画UI图


这部分就不用咱们操心了,把文档交给设计同学,然后等她出图就行。


这个过程中也可以时不时去看看她画的内容符不符合咱们的预期,当个小小的监工。


盯着干活


画完稿子需要跟老板、开发都对一遍,看看有没有出入,符不符合预期,有问题及时修改下,没问题就按照这份稿子进行开发了。


拟定合同、签合同



合同也是我来拟定的,其实是先到网上找了个软件开发的合同模板,然后再根据自己的想法进行合理的调整。



为什么我要到这一步才签合同呢?我觉得合同内容越细致越好,最好是能够把要开发的内容、样式都写在合同上,这样省得后面扯皮。


现在文档也出了,图也画完了,那咱们把这些东西都贴在和合同的附件上,然后附上这些条约:



  • 乙方将严格按照经过甲方审核的《软件功能设计书》的要求进行软件的开发设计。

  • 甲方托付乙方开发的软件在签订合同之后如需增加其它功能,必须以书面形式呈交给乙方,乙方做改动并酌情收取适当费用。


这样就可以保障我们在开发完后不会被恶意的增加或者修改功能了。


再改一次


这里我再列一些其他需要特别注意的点:



  1. 乙方交付日期,以及最多延期多久,如果超时怎么办?

  2. 甲方付款方式和日期(我们是用的 442 ,开工付 40%,中期验收付 40%,开发完验收付 20%)。

  3. 甲方拖欠项目款的处理方式(支付迟延履行金等)。

  4. 服务器费用是谁出?如果是乙方,需要注意包服务器的时限。

  5. 项目维护期,一般一年半年的吧。

  6. 乙方不保证项目 100% 可用,只能保障支撑 多少人 使用,支撑同时在线人数 多少人 ,如果遇到恶意攻击,不归乙方负责。

  7. 软件归属权是谁的?(如果项目款比较少的话,乙方可以要求要软件归属权,之后甲方如果想把项目接回去自己公司维护的话,需要从乙方手里买,这样乙方可以回点血)


大概就是这些吧,还有其他的东西基本都是按照模板的,没怎么改。


弄完给老板看看,没问题就签了,有问题两方再协商一下,我们这边是直接签了的。



开发阶段


开发没什么好说的,跟你在公司开发一样。


不过你接私活可不能在公司开发🚫,只能回家了自己干,不然被抓到上班干私活,你看老板裁不裁你就完事了。


微信小程序上线注意事项


微信小程序对请求的接口有三个基本要求:



  1. 必须是有备案的域名。

  2. 必须是有SSL证书(https)。

  3. 域名不得带端口号。


这个域名的问题必须要尽早解决,不然后面开发完了再去弄的话,工信部备案审核都要挺久的,不延期都难。


还有一种方式,我在逛微信开放社区看到的,使用云函数进行中转,间接请求ip接口,感觉是可行的,也比较省事,具体操作大家可以自己去探索一下。


我也是吃了没有经验的亏,买域名 + 工信部备案 + 公安备案 + 小程序备案,这一套操作下来真给我整难受死了,直接用云函数省事多了。



验收、上线


这部分也没什么好说的,大家在公司也经常经历这个步骤。


多沟通,多确认,


唯一需要提醒的是,验收的时候咱不能无条件接收老板的任何要求,毕竟价格和开发内容都是已经定好的,如果要加内容或者改内容,记得酌情要一点工时费,可不能亏待了自己。



后记


整个过程中,其实沟通是最重要的,写代码谁不会是吧?但是得让老板觉得OK才行,如果有什么疑问或者觉得不合理的地方啊,最好是尽早沟通,不然越到后面只会让问题变的越来越大。


最近刚做完这个项目,说实话没赚什么钱,甚至有点小亏😅。而且这个老板还有点拖欠工资的感觉,中期项目款拖到了项目交付才给,项目尾款到目前还没付😅😅。不过还好合同里写到了关于这块的处理方式,倒也不担心他不付这个钱。


(虽然我也不知道在哪能接到靠谱的私活🤣,但也可以先收藏本文,万一之后来活了,还能翻出来看看)


最后,希望各位都能接到 very good 的私活,祝大家早日实现财富自由!


webwxgetmsgimg (1)


作者:阿杆
来源:juejin.cn/post/7359764922727333939
收起阅读 »

仿今日头条,H5 循环播放的通知栏如何实现?

web
我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。 那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的...
继续阅读 »

我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。


toutiao.gif


那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的逻辑,并给出完整的代码。最终我实现的效果如下:


loop-notice.gif


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


布局代码


我们先看布局,如下图所示,循环播放的布局不是普通的左中右布局。可以看到,当文字向左移动时,左边的通知 Icon 和右边的留白会把文字挡住一部分。


block-out.png


为了实现这样的效果,我们给容器 box 设置一个相对定位,并把 box 中的 HTML 代码分为三部分:



  • 第一部分是 content,它包裹着需要循环播放的文字;

  • 第二部分是 left,它是左边的通知 Icon,我们给它设置绝对定位和 left: 0;

  • 第三部分是 right,它是右边的留白,我们给它设置绝对定位和 right: 0;


<div class="box">
<div class="content">

div>
<div class="left">🔔div>
<div class="right">div>
div>

.box {
position: relative;
overflow: hidden;
/* ... 省略 */
}
.left {
position: absolute;
left: 0;
/* ... 省略 */
}
.right {
position: absolute;
right: 0;
/* ... 省略 */
}

现在我们来看包裹文字的 content。content 内部包裹了三段一模一样的文字 notice,每段 notice 之间还有一个 space 元素作为间距。



<div id="content">
<div class="notice">春节期间,部分商品...div>
<div class="space">div>
<div class="notice">春节期间,部分商品...div>
<div class="space">div>
<div class="notice">春节期间,部分商品...div>
div>


为什么要放置三段一模一样的文字呢?这和循环播放的逻辑有关。


逻辑代码


我们并没有实现真正的循环播放,而是欺骗了用户的视觉。如下图所示:



  • 播放通知时,content 从 0 开始向左移动。

  • 向左移动 2 * noticeWidth + spaceWidth 时,继续向左移动便会露馅。因为第 3 段文字后不会有第 4 段文字。


    如果我们把 content 向左移动的距离强行从 2 * noticeWidth + spaceWidth 改为 noticeWidth,不难看出,用户在 box 可视区域内看到的情况基本一致的。


    然后 content 继续向左移动,向左移动的距离大于等于 2 * noticeWidth + spaceWidth 时,就把距离重新设为 noticeWidth。循环往复,就能欺骗用户视觉,让用户认为 content 能无休无止向左移动。



no-overflow-with-comment.png


欺骗视觉的代码如下:



  • 我们通过修改 translateX,让 content 不断地向左移动,每次向左移动 1.5px;

  • translateX >= noticeWidth * 2 + spaceWidth 时,我们又会把 translateX 强制设为 noticeWidth

  • 为了保证移动动画更丝滑,我们并没有采用 setInterval,而是使用 requestAnimationFrame。


const content = document.getElementById("content");
const notice = document.getElementsByClassName("notice");
const space = document.getElementsByClassName("space");
const noticeWidth = notice[0].offsetWidth;
const spaceWidth = space[0].offsetWidth;

let translateX = 0;
function move() {
translateX += 1.5;
if (translateX >= noticeWidth * 2 + spaceWidth) {
translateX = noticeWidth;
}
content.style.transform = `translateX(${-translateX}px)`;
requestAnimationFrame(move);
}

move();

完整代码


完整代码如下,你可以在 codepen 或者码上掘金上查看。



总结


本文我介绍了如何用 H5 实现循环播放的通知栏:



  • 布局方面,我们需要用绝对定位的通知 Icon、留白挡住循环文字的左侧和右侧;此外,循环播放的文字我们额外复制 2 份。

  • 逻辑方面,通知栏向左移动 2 * noticeWidth + spaceWidth 后,我们需要强制把通知栏向左移动的距离从 2 * noticeWidth + spaceWidth 变为 noticeWidth,以此来欺骗用户视觉。




作者:小霖家的混江龙
来源:juejin.cn/post/7372765277460496394
收起阅读 »

为什么很多人不推荐你用JWT?

为什么很多人不推荐你用JWT? 如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。 什么是JWT? 这个是他的官网JSON We...
继续阅读 »

为什么很多人不推荐你用JWT?


如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。


什么是JWT?


这个是他的官网JSON Web Tokens - jwt.io


这个就是JWT


img


JWT 全称JSON Web Token


如果你还不熟悉JWT,不要惊慌!它们并不那么复杂!


你可以把JWT想象成一些JSON数据,你可以验证这些数据是来自你认识的人。


当然如何实现我们在这里不讲,有兴趣的可以去自己了解。


下面我们来说一下他的流程:



  1. 当你登录到一个网站,网站会生成一个JWT并将其发送给你。

  2. 这个JWT就像是一个包裹,里面装着一些关于你身份的信息,比如你的用户名、角色、权限等。

  3. 然后,你在每次与该网站进行通信时都会携带这个JWT

  4. 每当你访问一个需要验证身份的页面时,你都会把这个JWT带给网站

  5. 网站收到JWT后,会验证它的签名以确保它是由网站签发的,并且检查其中的信息来确认你的身份和权限。

  6. 如果一切都通过了验证,你就可以继续访问受保护的页面了。


JWT Session


为什么说JWT很烂?


首先我们用JWT应该就是去做这些事情:



  • 用户注册网站

  • 用户登录网站

  • 用户点击并执行操作

  • 本网站使用用户信息进行创建、更新和删除 信息


这些事情对于数据库的操作经常是这些方面的



  • 记录用户正在执行的操作

  • 将用户的一些数据添加到数据库中

  • 检查用户的权限,看看他们是否可以执行某些操作


之后我们来逐步说出他的一些缺点


大小


这个方面毋庸置疑。


比如我们需要存储一个用户ID 为xiaou


如果存储到cookie里面,我们的总大小只有5个字节。


如果我们将 ID 存储在 一个 JWT 里。他的大小就会增加大概51倍


image-20240506200449402


这无疑就增大了我们的宽带负担。


冗余签名


JWT的主要卖点之一就是其加密签名。因为JWT被加密签名,接收方可以验证JWT是否有效且可信。


但是,在过去20年里几乎每一个网络框架都可以在使用普通的会话cookie时获得加密签名的好处。


事实上,大多数网络框架会自动为你加密签名(甚至加密!)你的cookie。这意味着你可以获得与使用JWT签名相同的好处,而无需使用JWT本身。


实际上,在大多数网络身份验证情况下,JWT数据都是存储在会话cookie中的,这意味着现在有两个级别的签名。一个在cookie本身上,一个在JWT上。


令牌撤销问题


由于令牌在到期之前一直有效,服务器没有简单的方法来撤销它。


以下是一些可能导致这种情况危险的用例。


注销并不能真正使你注销!


想象一下你在推特上发送推文后注销了登录。你可能会认为自己已经从服务器注销了,但事实并非如此。因为JWT是自包含的,将在到期之前一直有效。这可能是5分钟、30分钟或任何作为令牌一部分设置的持续时间。因此,如果有人在此期间获取了该令牌,他们可以继续访问直到它过期。


可能存在陈旧数据


想象一下用户是管理员,被降级为权限较低的普通用户。同样,这不会立即生效,用户将继续保持管理员身份,直到令牌过期。


JWT通常不加密


因此任何能够执行中间人攻击并嗅探JWT的人都拥有你的身份验证凭据。这变得更容易,因为中间人攻击只需要在服务器和客户端之间的连接上完成


安全问题


对于JWT是否安全。我们可以参考这个文章


JWT (JSON Web Token) (in)security - research.securitum.com


同时我们也可以看到是有专门的如何攻击JWT的教程的


高级漏洞篇之JWT攻击专题 - FreeBuf网络安全行业门户


总结


总的来说,JWT适合作为单次授权令牌,用于在两个实体之间传输声明信息。


但是,JWT不适合作为长期持久数据的存储机制,特别是用于管理用户会话。使用JWT作为会话机制可能会引入一系列严重的安全和实现上的问题,相反,对于长期持久数据的存储,更适合使用传统的会话机制,如会话cookie,以及建立在其上的成熟的实现。


但是写了这么多,我还是想说,如果你作为自己开发学习使用,不考虑安全,不考虑性能的情况下,用JWT是完全没有问题的,但是一旦用到生产环境中,我们就需要避免这些可能存在的问题。


作者:小u
来源:juejin.cn/post/7365533351451672612
收起阅读 »

我是没想到是还可以这样秒出答案 ...

起因 晚上在休闲游戏中,一网友发来信息求问,一道编程题。 咋一看,嘿 2023年1月浙江选考题(信息技术),挺新鲜,那就来看看吧。 聊了一下才知道,这是中考高考(6月28日晚23:05更正)选题。中考高考(6月28日晚23:05更正)就考这样的了吗? ...
继续阅读 »

起因


晚上在休闲游戏中,一网友发来信息求问,一道编程题。



咋一看,嘿 2023年1月浙江选考题(信息技术),挺新鲜,那就来看看吧。
聊了一下才知道,这是中考高考(6月28日晚23:05更正)选题。中考高考(6月28日晚23:05更正)就考这样的了吗?



image.png
image.png


一、题目



image.png



二、解析


因为题解想半天,没看明白要做的,就先直接上手代码去测试实验。通过足够多的次数去请求,就可以知道正确答案了(不符合出现的)。



后面恍然大悟会进一步讲解内容



二、代码测试


把该代码转成 java 对应的代码内容,并进行测试


public static void main(String[] args) {
// 答案记录
Map ansMap = new HashMap<>();
ansMap.put("AB##CD#", 0); // 选项A 答案
ansMap.put("#######", 0); // 选项B 答案
ansMap.put("#B##CDA", 0); // 选项C 答案
ansMap.put("###ABCD", 0); // 选项D 答案
for (int i = 0; i < 100000; i++) { // 10万次执行,看看 ABCD 答案是哪个一直没有出现
String res = runWork(); // 出现的结果
if (ansMap.get(res) == null){ // 出现和选项答案不一致的跳过
continue;
}
// 出现一致的进行+1
ansMap.put(res, ansMap.get(res) + 1);
}
// 输出结果
System.out.println(ansMap.toString());
}

public static String runWork() {
char[] a = {'A', 'B', '#', '#', 'C', 'D', '#'};
char[] stk = new char[a.length];
int top = -1;
Random random = new Random();

for (int i = 0; i < a.length; i++) {
int op = random.nextInt(2);
if (op == 1 && a[i] != '#') {
top++;
stk[top] = a[i];
a[i] = '#';
} else if (op == 0 && top != -1 && a[i] == '#') {
a[i] = stk[top];
top--;
}
}
return String.valueOf(a);
}

三、测试结果



微信图片_20230627210300.png


截图中可以看到,测试中,A、B、C 选项都出现了,不符合的是 D 选项,因此,正确答案是选项 D。

四、恍然大悟(真正解析)


仔细瞧命名, stk ,是栈(stack)的简写!可恶,这道题可以直接利用栈的知识去看选项去解了啊...



原字符数组是 'A', 'B', '#', '#', 'C', 'D', '#'

栈,就是先进后出。



选项内容解析
AAB##CD#对 a 字符数组都不进行拿出拿入,stk 字符数组就是空,
也就是不变,那么结果可以出现
B#######对 a 字符数组的ABCD都拿走,最终 stk 字符数组里就是 DCBA,
那么结果也可以出现
C#B##CDA对 a 字符数组都只拿A,并在最后一个的时候拿出最上层的。
最上层只有一个 A ,那就拿出 A ,
此时 stk 字符数组就为空了,那么结果可以出现
D###ABCD对 a 字符数组先拿A,stk 里就有 A ,但是B也需要拿,
且 A 要放在 B 拿之前的后面,不能实现,那么结果是不可以出现的!


图解:


ans.gif



那么最终,也就能明白这套代码的意思了,就是随机可能去拿去里面的字母,ABCD,放到栈里再实现放到原数组中去。对栈的理解与使用解释了一下。答案选 D ,只有 D 不符合栈的进出。




作者:南方者
来源:juejin.cn/post/7249288803532947517
收起阅读 »

【禁止血压飙升】如何拥有一个优雅的 controller

前言 见过几千行代码的 controller吗?我见过。 见过全是 try catch 的 controller 吗,我见过。 见过全是字段校验的 controller 吗,我见过。 见过全是业务代码的 controller 吗?不好意思,我们公司很多业务写在...
继续阅读 »

前言


见过几千行代码的 controller吗?我见过。


见过全是 try catch 的 controller 吗,我见过。


见过全是字段校验的 controller 吗,我见过。


见过全是业务代码的 controller 吗?不好意思,我们公司很多业务写在 controller 的。


看见这些我真的血压高。


正文


不优雅的 controller



@RestController
@RequestMapping("/user/test")
public class UserController {

private static Logger logger = LoggerFactory.getLogger(UserController.class);

@Autowired
private UserService userService;

@Autowired
private AuthService authService;

@PostMapping
public CommonResult userRegistration(@RequestBody UserVo userVo) {
if (StringUtils.isBlank(userVo.getUsername())){
return CommonResult.error("用户名不能为空");
}
if (StringUtils.isBlank(userVo.getPassword())){
return CommonResult.error("密码不能为空");
}
logger.info("注册用户:{}" , userVo.getUsername());
try {
userService.registerUser(userVo.getUsername());
return CommonResult.ok();
}catch (Exception e){
logger.error("注册用户失败:{}", userVo.getUsername(), e);
return CommonResult.error("注册失败");
}
}

@PostMapping("/login")
@PermitAll
@ApiOperation("使用账号密码登录")
public CommonResult<AuthLoginRespVO> login(@RequestBody AuthLoginReqVO reqVO) {
if (StringUtils.isBlank(reqVO.getUsername())){
return CommonResult.error("用户名不能为空");
}
if (StringUtils.isBlank(reqVO.getPassword())){
return CommonResult.error("密码不能为空");
}
try {
return success(authService.login(reqVO));
}catch (Exception e){
logger.error("注册用户失败:{}", reqVO.getUsername(), e);
return CommonResult.error("注册失败");
}
}

}


优雅的controller


@RestController
@RequestMapping("/user/test")
public class UserController1 {

private static Logger logger = LoggerFactory.getLogger(UserController1.class);

@Autowired
private UserService userService;

@Autowired
private AuthService authService;

@PostMapping("/userRegistration")
public CommonResult userRegistration(@RequestBody @Valid UserVo userVo) {
userService.registerUser(userVo.getUsername());
return CommonResult.ok();
}

@PostMapping("/login")
@PermitAll
@ApiOperation("使用账号密码登录")
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}

}


代码量直接减一半呀,这还不算上有些直接把业务逻辑写在 controller 的,看到这些我真的直接吐血



改造流程


校验方式



这个 if 校验看得我哪哪都不爽。好歹给我写一个断言吧。Assert.notNull(userVo.getUsername(), "用户名不能为空");


这不香吗?确实不香。


使用 spring 提供的@Valid




  • 在入参时使用@Valid注解,并且在 vo 中使用校验注解,如AuthLoginReqVO


@ApiModel(value = "管理后台 - 账号密码登录 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthLoginReqVO {

@ApiModelProperty(value = "账号", required = true, example = "user")
@NotEmpty(message = "登录账号不能为空")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;

@ApiModelProperty(value = "密码", required = true, example = "password")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;

}

@Valid


在SpringBoot中,@Valid是一个非常有用的注解,主要用于数据校验。以下是关于@Valid的一些详细信息:



  1. 为什么使用 @Valid 来验证参数:在编写接口时,我们经常需要验证请求参数。通常,我们可能会写大量的 if 和 if else 代码来进行判断。但这样的代码不仅不优雅,而且如果存在大量的验证逻辑,这会使代码看起来混乱,大大降低代码可读性。为了简化这个过程,我们可以使用 @Valid 注解来帮助我们简化验证逻辑。

  2. @Valid 注解的作用:@Valid 的主要作用是用于数据效验,可以在定义的实体中的属性上,添加不同的注解来完成不同的校验规则,而在接口类中的接收数据参数中添加 @valid 注解,这时你的实体将会开启一个校验的功能。

  3. @Valid 的相关注解:在实体类中不同的属性上添加不同的注解,就能实现不同数据的效验功能。

  4. 使用 @Valid 进行参数效验步骤:整个过程如下,用户访问接口,然后进行参数效验,因为 @Valid 不支持平面的参数效验(直接写在参数中字段的效验)所以基于 GET 请求的参数还是按照原先方式进行效验,而 POST 则可以以实体对象为参数,可以使用 @Valid 方式进行效验。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。

  5. @Validated与@Valid的区别@Validated@Valid 的变体。通过声明实体中属性的 groups ,再搭配使用 @Validated ,就能决定哪些属性需要校验,哪些不需要校验。


全局异常处理



  • 这个全局异常处理,可以根据自己的异常,自定义异常处理,并设置一个兜底的异常处理



@ResponseBody
@RestControllerAdvice
public class ExceptionHandlerAdvice {
protected Logger logger = LoggerFactory.getLogger(getClass());

@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
logger.error("[handleValidationExceptions]", ex);
StringBuilder sb = new StringBuilder();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((org.springframework.validation.FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
sb.append(fieldName).append(":").append(errorMessage).append(";");
});
return CommonResult.error(sb.toString());
}

/**
* 处理系统异常,兜底处理所有的一切
*/

@ExceptionHandler(value = Exception.class)
public CommonResult<?> defaultExceptionHandler(Throwable ex) {
logger.error("[defaultExceptionHandler]", ex);
// 返回 ERROR CommonResult
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}

}


就这么多,搞定,这样就拥有了漂流优雅的 controller 了



在日常开发中,还有那些血压飙升瞬间



  • 我拿出下图阁下如何面对


image-20240411185003067.png



  • 这个阁下又如何面对,我不说,你能知道这个什么吗【狗头】


image-20240411185134843.png


总结



  • 不是很明白为什么有些喜欢在 controller 写业务逻辑的,曾经有个同事问我(就是喜欢在 controller 写业务的),你这个接口写在那里,我需要调一下你这个接口。我满脸问号??不是隔壁的模块吗,为什么要调我的接口?直接引用的我的 service 去调方法就好了。

  • 这个就是痛点,各写各的,冗余代码一堆。

  • 曾经看到一个同事写一个保存的方法,虽然逻辑挺多,我滑动了好久都还没有方法还没有结束。一个方法整整几百行……

  • 看过 spring 源码都知道,spring 源码难啃,就是因为 spring 无限往下套娃,基本每个方法干每个方法的事情。比如我保存用户时,就只是保存用户,至于什么校验丢给校验的方法处理,什么发送消息丢给发送消息处理,这些就不能耦合在一起。

  • 对于看到一些 if 下面一丢逻辑,然后 if 再一丢逻辑,看代码时很多情况不需要知道这个逻辑怎么实现的,知道入参出参就大概这里做什么了。即使想知道详细情况点进去就知道了。突出这个当前方法要做的事情就好了。

  • 阿里的开发手册就推荐一个方法不能超过 80 行,超过可以根据业务具体调整一下。


作者:小塵
来源:juejin.cn/post/7357172505961578511
收起阅读 »

Android:解放自己的双手,无需手动创建shape文件

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。 现在的移动应用中为了美化界面,会给各类视图增加一些圆角、描边、渐变等等效果。当然系统也提供了对应的功能,那就是创建shape标签的XML文件,例如下图就是创建一个圆角为10dp,填充...
继续阅读 »

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。



现在的移动应用中为了美化界面,会给各类视图增加一些圆角、描边、渐变等等效果。当然系统也提供了对应的功能,那就是创建shape标签的XML文件,例如下图就是创建一个圆角为10dp,填充是白色的shape文件。再把这个文件设置给目标视图作为背景,就达到了我们想要的圆角效果。


<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="#FFFFFF" />
</shape>

//圆角效果
android:background="@drawable/shape_white_r10"

但不是所有的圆角和颜色都一样,甚至还有四个角单独一个有圆角的情况,当然还有描边、虚线描边、渐变填充色等等各类情况。随着页面效果的多样和复杂性,我们添加的shape文件也是成倍增加。


这时候不少的技术大佬出现了,大佬们各显神通打造了许多自定义View。这样我们就可以使用三方库通过在目标视图外嵌套一层视图来达到原本的圆角等效果。不得不说,这确实能够大大减少我们手动创建各类shape的情况,使用起来也是得心应手,方便了不少。


问题:


简单的布局,嵌套层级较少的页面使用起来还好。但往往随着页面的复杂程度越高,嵌套层级也越来多,这个时候再使用三方库外层嵌套视图会越来越臃肿和复杂。那么有没有一种方式可以直接在XML中当前视图中增减圆角等效果呢?


还真有,使用DataBinding可以办到!


这里就不单独介绍DataBinding的基础配置,网上一搜到处都是。咱们直接进入正题,使用**@BindingAdapter** 注解,这是用来扩展布局XML属性行为的注解。


使用DataBinding实现圆角


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = ["shape_radius""shape_solidColor"])
fun View.setViewBackground(radius: Int = 0,solidColor: Int = Color.TRANSPARENT){
val drawable = GradientDrawable()
drawable.cornerRadius = context.dp2px(radius.toFloat()).toFloat()
drawable.setColor(solidColor)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_solidColor="@{@color/white}"

其实就是对当前视图的一个扩展,有点和kotlin的扩展函数类似。既然这样我们可以通过代码配置更多自定义的属性:


各方向圆角的实现:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = ["
"shape_solidColor",//填充颜色
"shape_tl_radius",//上左圆角
"shape_tr_radius",//上右圆角
"shape_bl_radius",//下左圆角
"shape_br_radius"//下右圆角
])
fun View.setViewBackground(radius: Int = 0,solidColor: Int = Color.TRANSPARENT){
val drawable = GradientDrawable()
drawable.setColor(solidColor)
drawable.cornerRadii = floatArrayOf(
context.dp2px(shape_tl_radius.toFloat()).toFloat(),
context.dp2px(shape_tl_radius.toFloat()).toFloat(),
context.dp2px(shape_tr_radius.toFloat()).toFloat(),
context.dp2px(shape_tr_radius.toFloat()).toFloat(),
context.dp2px(shape_br_radius.toFloat()).toFloat(),
context.dp2px(shape_br_radius.toFloat()).toFloat(),
context.dp2px(shape_bl_radius.toFloat()).toFloat(),
context.dp2px(shape_bl_radius.toFloat()).toFloat(),
)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_tl_radius="@{@color/white}"//左上角
shape_tr_radius="@{@color/white}"//右上角
shape_bl_radius="@{@color/white}"//左下角
shape_br_radius="@{@color/white}"//右下角

虚线描边:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = [
"shape_radius"
"shape_solidColor"
"shape_strokeWitdh",//描边宽度
"shape_dashWith",//描边虚线单个宽度
"shape_dashGap",//描边间隔宽度
])
fun View.setViewBackground(
radius: Int = 0,
solidColor: Int = Color.TRANSPARENT,
strokeWidth: Int = 0,
shape_dashWith: Int = 0,
shape_dashGap: Int = 0
){
val drawable = GradientDrawable()
drawable.setStroke(
context.dp2px(strokeWidth.toFloat()),
strokeColor,
shape_dashWith.toFloat(),
shape_dashGap.toFloat()
)
drawable.setColor(solidColor)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_solidColor="@{@color/white}"
strokeWidth="@{1}"
shape_dashWith="@{2}"
shape_dashGap="@{3}"

渐变色的使用:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = [
"shape_startColor",//渐变开始颜色
"shape_centerColor",//渐变中间颜色
"shape_endColor",//渐变结束颜色
"shape_gradualOrientation",//渐变角度
])
fun View.setViewBackground(
shape_startColor: Int = Color.TRANSPARENT,
shape_centerColor: Int = Color.TRANSPARENT,
shape_endColor: Int = Color.TRANSPARENT,
shape_gradualOrientation: Int = 1,//TOP_BOTTOM = 1 ,TR_BL = 2,RIGHT_LEFT = 3,BR_TL = 4,BOTTOM_TOP = 5,BL_TR = 6,LEFT_RIGHT = 7,TL_BR = 8
){
val drawable = GradientDrawable()
when (shape_gradualOrientation) {
1 -> drawable.orientation = GradientDrawable.Orientation.TOP_BOTTOM
2 -> drawable.orientation = GradientDrawable.Orientation.TR_BL
3 -> drawable.orientation = GradientDrawable.Orientation.RIGHT_LEFT
4 -> drawable.orientation = GradientDrawable.Orientation.BR_TL
5 -> drawable.orientation = GradientDrawable.Orientation.BOTTOM_TOP
6 -> drawable.orientation = GradientDrawable.Orientation.BL_TR
7 -> drawable.orientation = GradientDrawable.Orientation.LEFT_RIGHT
8 -> drawable.orientation = GradientDrawable.Orientation.TL_BR
}
drawable.gradientType = GradientDrawable.LINEAR_GRADIENT//线性
drawable.shape = GradientDrawable.RECTANGLE//矩形方正
drawable.colors = if (shape_centerColor != Color.TRANSPARENT) {//有中间色
intArrayOf(
shape_startColor,
shape_centerColor,
shape_endColor
)
} else {
intArrayOf(shape_startColor, shape_endColor)
}//渐变色
background = drawable
}

//xml文件中
shape_startColor="@{@color/cl_F1E6A0}"
shape_centerColor="@{@color/cl_F8F8F8}"
shape_endColor=@{@color/cl_3CB9FF}

不止设置shape功能,只要可以通过代码设置的功能一样可以在BindingAdapter注解中自定义,使用起来是不是更加方便了。


总结:



  • 注解BindingAdapter中value数组的自定义属性一样要和方法内的参数一一对应,否则会报错。

  • 布局中使用该自定义属性时需要将布局文件最外层修改为layout标签

  • XML中使用自定义属性时一定要添加@{}


好了,以上便是解放自己的双手,无需手动创建shape文件的全部内容,希望能给大家带来帮助!


作者:似曾相识2022
来源:juejin.cn/post/7278858311596359739
收起阅读 »

Android — 实现同意条款功能

在开发App时,让用户知晓并同意隐私政策、服务协议等条款是必不可少的功能,恰逢Facebook最近对隐私政策的审核愈发严格,本文介绍如何通过TextView和ClickableSpan简单快速的实现同意条款功能。 下面是掘金(小米应用商店下载)和Github(...
继续阅读 »

在开发App时,让用户知晓并同意隐私政策、服务协议等条款是必不可少的功能,恰逢Facebook最近对隐私政策的审核愈发严格,本文介绍如何通过TextViewClickableSpan简单快速的实现同意条款功能。


下面是掘金(小米应用商店下载)和Github(Google Play下载)两个App的同意条款功能示例图:


掘金Github
image.pngimage.png

可以看见二者有所区别,这是由于国内政策不允许App自动勾选同意,必须要用户手动勾选,如果不按照要求处理甚至无法上架。


实现同意条款功能


先梳理一下实现同意条款功能的核心需求:



  1. 可以根据上架的区域不同(是否海外)决定是否显示勾选框,显示勾选框时需要提供可以获取选中状态的方法。

  2. 同意条款的提示中可能仅包含单个条款或同时包含多个条款。

  3. 条款名的颜色需要与其他文案不同,可以根据需求决定是否显示下划线,点击条款名时可以查看条款的具体内容。


上述三项需求最关键,但可以调整的细节有很多,本文仅通过较为简单的方式来实现(不使用自定义View),各位读者可以根据实际项目需求进行调整。


自定义配置类


上面的三点需求中都包含了一些配置项,可以通过配置类来管理这些参数,根据外部设定的配置进行相应处理。示例代码如下:


class ConfirmTermsConfiguration private constructor() {

// 同意提示文案
var confirmTipsContent: String = ""
private set

// 可点击的条款文案,键为条款文案,值为条款内容(链接)
var clickableTerms = ArrayMap<String, String>()
private set

// 同意条款控件距离底部的距离,默认为32dp
// 左右两侧的边距可以根据实际需求决定是否需要提供配置方法
var viewBottomMargin = DensityUtil.dp2Px(36)
private set

// 文字大小,默认14sp
var textSize = 14f
private set

// 文字颜色,默认黑色
var textColor = android.R.color.black
private set

// 可点击文字的颜色,默认为蓝色
var clickableTextColor = R.color.color_blue_229CE9
private set

// 是否显示下滑线,默认不显示
var showUnderline = false
private set

// 是否显示勾选框,默认为false
// 示例中勾选框直接使用可点击文案的颜色
// 可以根据实际需求决定是否提供相应的配置方法
var showCheckbox = false
private set

class Builder() {
private var confirmTipsContent: String = ""
private val clickableTerms = ArrayMap<String, String>()
private var viewBottomMargin = DensityUtil.dp2Px(36)
private var textSize = 14f
private var textColor = android.R.color.black
private var clickableTextColor = R.color.color_blue_229CE9
private var showUnderline = false
private var showCheckbox = false

fun setConfirmTipContent(confirmTipsContent: String): Builder {
this.confirmTipsContent = confirmTipsContent
return this
}

fun setClickableTerm(clickableTerm: String, termsLink: String): Builder {
clickableTerms.clear()
clickableTerms[clickableTerm] = termsLink
return this
}

fun addClickableTerms(clickableTerms: Map<String, String>): Builder {
this.clickableTerms.clear()
this.clickableTerms.putAll(clickableTerms)
return this
}

fun setViewBottomMargin(viewBottomMargin: Int): Builder {
this.viewBottomMargin = viewBottomMargin
return this
}

fun setTextSize(textSize: Float): Builder {
this.textSize = textSize
return this
}

fun setTextColor(textColor: Int): Builder {
this.textColor = textColor
return this
}

fun setClickableTextColor(clickableTextColor: Int): Builder {
this.clickableTextColor = clickableTextColor
return this
}

fun setShowUnderline(showUnderline: Boolean): Builder {
this.showUnderline = showUnderline
return this
}

fun setShowCheckbox(showCheckbox: Boolean): Builder {
this.showCheckbox = showCheckbox
return this
}

fun build(): ConfirmTermsConfiguration {
return ConfirmTermsConfiguration().also {
it.confirmTipsContent = confirmTipsContent
it.clickableTerms = clickableTerms
it.viewBottomMargin = viewBottomMargin
it.textSize = textSize
it.textColor = textColor
it.clickableTextColor = clickableTextColor
it.showUnderline = showUnderline
it.showCheckbox = showCheckbox
}
}
}
}

自定义ClickSpan


ClickSpan是Android中专门处理可点击文本的类,继承ClickSpan类可以实现定制可点击文本的样式以及响应事件。可以使用自定义ClickSpan来实现第三点需求,示例代码如下:


class ClickSpan(
// 默认颜色为白色
private var colorRes: Int = -1,
// 默认不显示下划线
private var isShoeUnderLine: Boolean = false,
// 点击事件监听,必须传入
private var clickListener: () -> Unit
) : ClickableSpan() {

override fun onClick(widget: View) {
// 回调点击事件监听
clickListener.invoke()
}

override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
//设置文本颜色
ds.color = colorRes
//设置是否显示下划线
ds.isUnderlineText = isShoeUnderLine
}
}


显示、隐藏同意条款控件


有了配置类和自定义ClickSpan类之后,就可以实现显示、隐藏同意条款控件了,示例代码如下:



  • 辅助类


class ConfirmTermsHelper {

private var confirmTermsView: View? = null

var confirmStatus = false
private set

fun showConfirmTermsView(activity: Activity, confirmTermsConfiguration: ConfirmTermsConfiguration) {
val confirmTipsContent = confirmTermsConfiguration.confirmTipsContent
val clickableTerms = confirmTermsConfiguration.clickableTerms
val showCheckBox = confirmTermsConfiguration.showCheckbox
// 同意条款的提示文案为空直接结束方法执行
if (confirmTipsContent.isEmpty()) {
return
}
// 先把当前的控件移除
hideConfirmTermsView()
activity.runOnUiThread {
if (showCheckBox) {
ConstraintLayout(activity).apply {
// 代码中创建CheckBox存在Padding,暂时未解决
addView(AppCompatCheckBox(activity).apply {
id = R.id.cb_confirm_terms
val checkboxSize = DensityUtil.dp2Px(30)
layoutParams = ConstraintLayout.LayoutParams(checkboxSize, checkboxSize).apply {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
startToStart = ConstraintLayout.LayoutParams.PARENT_ID
}
setButtonDrawable(R.drawable.selector_confirm_terms_chekcbox)
buttonTintList = ColorStateList.valueOf(ContextCompat.getColor(activity, confirmTermsConfiguration.clickableTextColor))
setOnCheckedChangeListener { _, isChecked ->
confirmStatus = isChecked
}
})
addView(AppCompatTextView(activity).apply {
id = R.id.tv_confirm_terms
layoutParams = ConstraintLayout.LayoutParams(0, ConstraintLayout.LayoutParams.WRAP_CONTENT).apply {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
startToEnd = R.id.cb_confirm_terms
endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
marginStart = DensityUtil.dp2Px(10)
}
textSize = confirmTermsConfiguration.textSize
setTextColor(ContextCompat.getColor(activity, confirmTermsConfiguration.textColor))
movementMethod = LinkMovementMethodCompat.getInstance()
text = SpannableStringBuilder(confirmTipsContent).apply {
clickableTerms.entries.forEach { clickableTermEntry ->
val startHighlightIndex = confirmTipsContent.indexOf(clickableTermEntry.key)
if (startHighlightIndex > 0) {
setSpan(
ClickSpan(ContextCompat.getColor(activity, confirmTermsConfiguration.clickableTextColor), confirmTermsConfiguration.showUnderline) {
// 通过CustomTab打开链接
CustomTabHelper.openSimpleCustomTab(activity, clickableTermEntry.value)
},
startHighlightIndex, startHighlightIndex + clickableTermEntry.key.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
})
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM).apply {
val defaultLeftRightSpace = DensityUtil.dp2Px(20)
marginStart = defaultLeftRightSpace
marginEnd = defaultLeftRightSpace
bottomMargin = confirmTermsConfiguration.viewBottomMargin
}
}
} else {
AppCompatTextView(activity).apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM).apply {
val defaultLeftRightSpace = DensityUtil.dp2Px(20)
marginStart = defaultLeftRightSpace
marginEnd = defaultLeftRightSpace
bottomMargin = confirmTermsConfiguration.viewBottomMargin
}
textSize = confirmTermsConfiguration.textSize
setTextColor(ContextCompat.getColor(activity, confirmTermsConfiguration.textColor))
movementMethod = LinkMovementMethodCompat.getInstance()
text = SpannableStringBuilder(confirmTipsContent).apply {
clickableTerms.entries.forEach { clickableTermEntry ->
val startHighlightIndex = confirmTipsContent.indexOf(clickableTermEntry.key)
if (startHighlightIndex > 0) {
setSpan(
ClickSpan(ContextCompat.getColor(activity, confirmTermsConfiguration.clickableTextColor), confirmTermsConfiguration.showUnderline) {
// 通过CustomTab打开链接
CustomTabHelper.openSimpleCustomTab(activity, clickableTermEntry.value)
},
startHighlightIndex, startHighlightIndex + clickableTermEntry.key.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
}
}.run {
confirmTermsView = this
removeViewInParent(this)
getRootView(activity).addView(this)
}
}
}

fun hideConfirmTermsView() {
confirmStatus = false
confirmTermsView?.run { post { removeViewInParent(this) } }
confirmTermsView = null
}

private fun getRootView(activity: Activity): FrameLayout {
return activity.findViewById(android.R.id.content)
}

private fun removeViewInParent(targetView: View) {
try {
(targetView.parent as? ViewGr0up)?.removeView(targetView)
} catch (e: Exception) {
e.printStackTrace()
}
}
}


  • 示例页面


class ConfirmTermsExampleActivity : AppCompatActivity() {

private lateinit var binding: LayoutConfirmTermsExampleActivityBinding

private val confirmTermsHelper = ConfirmTermsHelper()

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

binding.btnWithCheckBox.setOnClickListener {
confirmTermsHelper.showConfirmTermsView(this, ConfirmTermsConfiguration.Builder()
.setConfirmTipContent("已阅读并同意\"隐私政策\"")
.setClickableTerm("隐私政策", "https://lf3-cdn-tos.draftstatic.com/obj/ies-hotsoon-draft/juejin/7b28b328-1ae4-4781-8d46-430fef1b872e.html")
.setShowCheckbox(true)
.setTextColor(R.color.color_gray_999)
.setClickableTextColor(R.color.color_black_3B3946)
.build())
binding.btnGetConfirmStatus.visibility = View.VISIBLE
}
binding.btnWithoutCheckBox.setOnClickListener {
confirmTermsHelper.showConfirmTermsView(this, ConfirmTermsConfiguration.Builder()
.setConfirmTipContent("By signing in you accept out Terms of use and Privacy policy")
.addClickableTerms(
mapOf(
Pair("Terms of use", "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service"),
Pair("Privacy policy", "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement")
)
)
.setShowUnderline(true)
.setTextColor(R.color.color_gray_999)
.build())
binding.btnGetConfirmStatus.visibility = View.GONE
}
binding.btnGetConfirmStatus.setOnClickListener {
showSnackbar("Current confirm status:${confirmTermsHelper.confirmStatus}")
}
}

private fun showSnackbar(message: String) {
runOnUiThread {
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
}
}

override fun onDestroy() {
super.onDestroy()
confirmTermsHelper.hideConfirmTermsView()
}
}

效果演示与完整示例代码


最终演示效果如下:


Screen_recording_202 -original-original.gif

所有演示代码已在示例Demo中添加。


ExampleDemo github


ExampleDemo gitee


作者:ChenYhong
来源:juejin.cn/post/7372577541112872972
收起阅读 »

来,实现一下这个报表功能,速度要快,要嘎嘎快

我们有一段业务,类似一个报表,就是获取用户的订单汇总,邮费汇总,各种手续费汇总,然后拿时间噶一卡,显示在页面。 但是呢,这几个业务没啥实际关系,数据也是分开的,一个一个获取会有点慢,我开始就是这样写的,老板嫌页面太慢,让我改,可是页面反应慢,关我后端程序什么事...
继续阅读 »

我们有一段业务,类似一个报表,就是获取用户的订单汇总,邮费汇总,各种手续费汇总,然后拿时间噶一卡,显示在页面。


但是呢,这几个业务没啥实际关系,数据也是分开的,一个一个获取会有点慢,我开始就是这样写的,老板嫌页面太慢,让我改,可是页面反应慢,关我后端程序什么事,哥哥别打了,错了错了,我改,我改。那么最好的方案就是多线程分别获取然后汇总到一起返回。


在Java中获取异步线程的结果通常可以使用FutureCallableCompletableFutureFutureTask等类来实现。这些类可以用来提交任务到线程池,并在任务完成后获取结果。这就是我们想要的结果,那么这里来深入研究分析一下这三个方案。


使用FutureCallable


package com.luke.designpatterns.demo;

import java.util.concurrent.*;

public class demo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
// 获取各种汇总的代码,返回结果
return 42;
}
});
// 获取异步任务的结果
Integer result = future.get();
System.out.println("异步任务的结果是" + result);
executor.shutdown();
}
}

image.png


它们的原理是通过将任务提交到线程池执行,同时返回一个Future对象,该对象可以在未来的某个时刻获取任务的执行结果。



  1. Callable 接口Callable 是一个带泛型的接口,它允许你定义一个返回结果的任务,并且可以抛出异常。这个接口只有一个方法 call(),在该方法中编写具体的任务逻辑。

  2. Future 接口Future 接口代表一个异步计算的结果。它提供了方法来检查计算是否完成、等待计算的完成以及检索计算的结果。Future 提供了一个 get() 方法,它会阻塞当前线程直到计算完成,并返回计算的结果。



Callable 接口本身并不直接启动线程,它只是定义了一个可以返回结果的任务。要启动一个 Callable 实例的任务,通常需要将其提交给 ExecutorService 线程池来执行。



ExecutorService 中,可以使用 submit(Callable<T> task) 方法提交 Callable 任务。这个方法会返回一个 Future 对象,它可以用来获取任务的执行结果。


启动 Callable 任务的原理可以概括为以下几个步骤:



  1. 创建 Callable 实例:首先需要创建一个实现了 Callable 接口的类,并在 call() 方法中定义具体的任务逻辑,包括要执行的代码和返回的结果。

  2. 创建 ExecutorService 线程池:使用 Executors 类的工厂方法之一来创建一个 ExecutorService 线程池,例如 newFixedThreadPool(int nThreads)newCachedThreadPool() 等。

  3. 提交任务:将 Callable 实例通过 ExecutorServicesubmit(Callable<T> task) 方法提交到线程池中执行。线程池会为任务分配一个线程来执行。

  4. 异步执行ExecutorService 线程池会在后台异步执行任务,不会阻塞当前线程,使得主线程可以继续执行其他操作。

  5. 获取结果:通过 Future 对象的 get() 方法获取任务的执行结果。如果任务尚未完成,get() 方法会阻塞当前线程直到任务完成并返回结果。


总的来说,Callable 启动线程的原理是将任务提交给 ExecutorService 线程池,线程池会负责管理线程的执行,执行任务的过程是在独立的线程中进行的,从而实现了异步执行的效果。


使用CompletableFuture


import java.util.concurrent.CompletableFuture;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 获取各种汇总的代码,返回结果
return 43;
});

// 获取异步任务的结果
Integer result = future.get();

System.out.println("异步任务的结果:" + result);
}
}

image.png


CompletableFuture 是 Java 8 引入的一个类,用于实现异步编程和异步任务的组合。它的原理是基于"Completable"(可以完成的)和"Future"(未来的结果)的概念,提供了一种方便的方式来处理异步任务的执行和结果处理。


CompletableFuture 的原理可以简单概括为以下几点:



  1. 异步执行CompletableFuture 允许你以异步的方式执行任务。你可以使用 supplyAsync()runAsync() 等方法提交一个任务给 CompletableFuture 执行,任务会在一个独立的线程中执行,不会阻塞当前线程。

  2. 回调机制CompletableFuture 提供了一系列的方法来注册回调函数,这些回调函数会在任务执行完成时被调用。例如,thenApply(), thenAccept(), thenRun() 等方法可以分别处理任务的结果、完成时的操作以及任务执行异常时的处理。

  3. 组合多个任务CompletableFuture 支持多个任务的组合,可以使用 thenCombine()thenCompose()thenAcceptBoth() 等方法来组合多个任务,实现任务之间的依赖关系。

  4. 异常处理CompletableFuture 允许你对任务执行过程中抛出的异常进行处理,可以使用 exceptionally()handle() 等方法来处理异常情况。

  5. 等待任务完成:与 Future 类似,CompletableFuture 也提供了 get() 方法来等待任务的完成并获取结果。但与传统的 Future 不同,CompletableFutureget() 方法不会阻塞当前线程,因为任务的执行是异步的。


总的来说,CompletableFuture 的原理是基于回调和异步执行的机制,提供了一种方便的方式来处理异步任务的执行和结果处理,同时支持任务的组合和异常处理。


使用FutureTask


import java.util.concurrent.*;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
// 获取各种汇总的代码,返回结果
return 44;
});

Thread thread = new Thread(futureTask);
thread.start();

// 获取异步任务的结果
Integer result = futureTask.get();
System.out.println("异步任务的结果:" + result);
}
}

image.png


FutureTask 是 Java 中实现 Future 接口的一个基本实现类,同时也实现了 Runnable 接口,因此可以被用作一个可运行的任务。FutureTask 的原理是将一个可调用的任务(CallableRunnable)封装成一个异步的、可取消的任务,它提供了一个机制来获取任务的执行结果。


FutureTask 的原理可以简要概括如下:



  1. 封装任务FutureTask 接受一个 CallableRunnable 对象作为构造函数的参数,并将其封装成一个异步的任务。

  2. 执行任务FutureTask 实现了 Runnable 接口,因此可以作为一个可运行的任务提交给 Executor(通常是 ExecutorService)来执行。当 FutureTask 被提交到线程池后,线程池会在一个独立的线程中执行该任务。

  3. 获取结果:通过 Future 接口的方法,可以等待任务执行完成并获取其结果。FutureTask 实现了 Future 接口,因此可以调用 get() 方法来获取任务的执行结果。如果任务尚未完成,get() 方法会阻塞当前线程直到任务完成并返回结果。

  4. 取消任务FutureTask 提供了 cancel(boolean mayInterruptIfRunning) 方法来取消任务的执行。可以选择是否中断正在执行的任务。一旦任务被取消,get() 方法会立即抛出 CancellationException 异常。


总的来说,FutureTask 的原理是将一个可调用的任务封装成一个异步的、可取消的任务,并通过 Future 接口来提供获取任务执行结果和取消任务的机制。


这些方法中,get()方法会阻塞当前线程,直到异步任务完成并返回结果。如果任务抛出异常,get()方法会将异常重新抛出。


我们平时常用的方法就是这四种,都能实现我的需求,随便找一个哐哐干上去就好啦。


作者:奔跑的毛球
来源:juejin.cn/post/7350557995895701531
收起阅读 »

我的又一个神奇的框架——Skins换肤框架

为什么会有换肤的需求 app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。 换肤是什么 换...
继续阅读 »

为什么会有换肤的需求


app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。


换肤是什么


换肤是将app的背景色、文字颜色以及资源图片,一键进行全部切换的过程。这里就包括了图片资源和颜色资源。


Skins怎么使用


Skins就是一个解决这样一种换肤需求的框架。


// 添加以下代码到项目根目录下的build.gradle
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
// 添加以下代码到app模块的build.gradle
dependencies {
// skins依赖了dora框架,所以你也要implementation dora
implementation("com.github.dora4:dora:1.1.12")
implementation 'com.github.dora4:dview-skins:1.4'
}

我以更换皮肤颜色为例,打开res/colors.xml。


<!-- 需要换肤的颜色 -->
<color name="skin_theme_color">@color/cyan</color>
<color name="skin_theme_color_red">#d23c3e</color>
<color name="skin_theme_color_orange">#ff8400</color>
<color name="skin_theme_color_black">#161616</color>
<color name="skin_theme_color_green">#009944</color>
<color name="skin_theme_color_blue">#0284e9</color>
<color name="skin_theme_color_cyan">@color/cyan</color>
<color name="skin_theme_color_purple">#8c00d6</color>

将所有需要换肤的颜色,添加skin_前缀和_skinname后缀,不加后缀的就是默认皮肤。
然后在启动页应用预设的皮肤类型。在布局layout文件中使用默认皮肤的资源名称,像这里就是R.color.skin_theme_color,框架会自动帮你替换。要想让框架自动帮你替换,你需要让所有要换肤的Activity继承BaseSkinActivity。


private fun applySkin() {
val manager = PreferencesManager(this)
when (manager.getSkinType()) {
0 -> {
}
1 -> {
SkinManager.changeSkin("cyan")
}
2 -> {
SkinManager.changeSkin("orange")
}
3 -> {
SkinManager.changeSkin("black")
}
4 -> {
SkinManager.changeSkin("green")
}
5 -> {
SkinManager.changeSkin("red")
}
6 -> {
SkinManager.changeSkin("blue")
}
7 -> {
SkinManager.changeSkin("purple")
}
}
}

另外还有一个情况是在代码中使用换肤,那么跟布局文件中定义是有一些区别的。


val skinThemeColor = SkinManager.getLoader().getColor("skin_theme_color")

这个skinThemeColor拿到的就是当前皮肤下的真正的skin_theme_color颜色,比如R.color.skin_theme_color_orange的颜色值“#ff8400”或R.id.skin_theme_color_blue的颜色值“#0284e9”。
SkinLoader还提供了更简洁设置View颜色的方法。


override fun setImageDrawable(imageView: ImageView, resName: String) {
val drawable = getDrawable(resName) ?: return
imageView.setImageDrawable(drawable)
}

override fun setBackgroundDrawable(view: View, resName: String) {
val drawable = getDrawable(resName) ?: return
view.background = drawable
}

override fun setBackgroundColor(view: View, resName: String) {
val color = getColor(resName)
view.setBackgroundColor(color)
}

框架原理解析


先看BaseSkinActivity的源码。


package dora.skin.base

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
import android.view.View
import androidx.collection.ArrayMap
import androidx.core.view.LayoutInflaterCompat
import androidx.core.view.LayoutInflaterFactory
import androidx.databinding.ViewDataBinding
import dora.BaseActivity
import dora.skin.SkinManager
import dora.skin.attr.SkinAttr
import dora.skin.attr.SkinAttrSupport
import dora.skin.attr.SkinView
import dora.skin.listener.ISkinChangeListener
import dora.util.LogUtils
import dora.util.ReflectionUtils
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*

abstract class BaseSkinActivity<T : ViewDataBinding> : BaseActivity<T>(),
ISkinChangeListener, LayoutInflaterFactory {

private val constructorArgs = arrayOfNulls<Any>(2)

override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
if (createViewMethod == null) {
val methodOnCreateView = ReflectionUtils.findMethod(delegate.javaClass, false,
"createView", *createViewSignature)
createViewMethod = methodOnCreateView
}
var view: View? = ReflectionUtils.invokeMethod(delegate, createViewMethod, parent, name,
context, attrs) as View?
if (view == null) {
view = createViewFromTag(context, name, attrs)
}
val skinAttrList = SkinAttrSupport.getSkinAttrs(attrs, context)
if (skinAttrList.isEmpty()) {
return view
}
injectSkin(view, skinAttrList)
return view
}

private fun injectSkin(view: View?, skinAttrList: MutableList<SkinAttr>) {
if (skinAttrList.isNotEmpty()) {
var skinViews = SkinManager.getSkinViews(this)
if (skinViews == null) {
skinViews = arrayListOf()
}
skinViews.add(SkinView(view, skinAttrList))
SkinManager.addSkinView(this, skinViews)
if (SkinManager.needChangeSkin()) {
SkinManager.apply(this)
}
}
}

private fun createViewFromTag(context: Context, viewName: String, attrs: AttributeSet): View? {
var name = viewName
if (name == "view") {
name = attrs.getAttributeValue(null, "class")
}
return try {
constructorArgs[0] = context
constructorArgs[1] = attrs
if (-1 == name.indexOf('.')) {
// try the android.widget prefix first...
createView(context, name, "android.widget.")
} else {
createView(context, name, null)
}
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
} finally {
// Don't retain references on context.
constructorArgs[0] = null
constructorArgs[1] = null
}
}

@Throws(InflateException::class)
private fun createView(context: Context, name: String, prefix: String?): View? {
var constructor = constructorMap[name]
return try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
val clazz = context.classLoader.loadClass(
if (prefix != null) prefix + name else name).asSubclass(View::class.java)
constructor = clazz.getConstructor(*constructorSignature)
constructorMap[name] = constructor
}
constructor!!.isAccessible = true
constructor.newInstance(*constructorArgs)
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
val layoutInflater = LayoutInflater.from(this)
LayoutInflaterCompat.setFactory(layoutInflater, this)
super.onCreate(savedInstanceState)
SkinManager.addListener(this)
}

override fun onDestroy() {
super.onDestroy()
SkinManager.removeListener(this)
}

override fun onSkinChanged(suffix: String) {
SkinManager.apply(this)
}

companion object {
val constructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
private val constructorMap: MutableMap<String, Constructor<out View>> = ArrayMap()
private var createViewMethod: Method? = null
val createViewSignature = arrayOf(View::class.java, String::class.java,
Context::class.java, AttributeSet::class.java)
}
}

我们可以看到BaseSkinActivity继承自dora.BaseActivity,所以dora框架是必须要依赖的。有人说,那我不用dora框架的功能,可不可以不依赖dora框架?我的回答是,不建议。Skins对Dora生命周期注入特性采用的是,依赖即配置。


package dora.lifecycle.application

import android.app.Application
import android.content.Context
import dora.skin.SkinManager

class SkinsAppLifecycle : ApplicationLifecycleCallbacks {

override fun attachBaseContext(base: Context) {
}

override fun onCreate(application: Application) {
SkinManager.init(application)
}

override fun onTerminate(application: Application) {
}
}

所以你无需手动配置<meta-data android:name="dora.lifecycle.config.SkinsGlobalConfig" android:value="GlobalConfig"/>,Skins已经自动帮你配置好了。那么我顺便问个问题,BaseSkinActivity中最关键的一行代码是哪行?LayoutInflaterCompat.setFactory(layoutInflater, this)这行代码是整个换肤流程最关键的一行代码。我们来干预一下所有Activity onCreateView时的布局加载过程。我们在SkinAttrSupport.getSkinAttrs中自己解析了AttributeSet。


    /**
* 从xml的属性集合中获取皮肤相关的属性。
*/

fun getSkinAttrs(attrs: AttributeSet, context: Context): MutableList<SkinAttr> {
val skinAttrs: MutableList<SkinAttr> = ArrayList()
var skinAttr: SkinAttr
for (i in 0 until attrs.attributeCount) {
val attrName = attrs.getAttributeName(i)
val attrValue = attrs.getAttributeValue(i)
val attrType = getSupportAttrType(attrName) ?: continue
if (attrValue.startsWith("@")) {
val ref = attrValue.substring(1)
if (TextUtils.isEqualTo(ref, "null")) {
// 跳过@null
continue
}
val id = ref.toInt()
// 获取资源id的实体名称
val entryName = context.resources.getResourceEntryName(id)
if (entryName.startsWith(SkinConfig.ATTR_PREFIX)) {
skinAttr = SkinAttr(attrType, entryName)
skinAttrs.add(skinAttr)
}
}
}
return skinAttrs
}

我们只干预skin_开头的资源的加载过程,所以解析得到我们需要的属性,最后得到SkinAttr的列表返回。


package dora.skin.attr

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import dora.skin.SkinLoader
import dora.skin.SkinManager

enum class SkinAttrType(var attrType: String) {

/**
* 背景属性。
*/

BACKGROUND("background") {
override fun apply(view: View, resName: String) {
val drawable = loader.getDrawable(resName)
if (drawable != null) {
view.setBackgroundDrawable(drawable)
} else {
val color = loader.getColor(resName)
view.setBackgroundColor(color)
}
}
},

/**
* 字体颜色。
*/

TEXT_COLOR("textColor") {
override fun apply(view: View, resName: String) {
val colorStateList = loader.getColorStateList(resName) ?: return
(view as TextView).setTextColor(colorStateList)
}
},

/**
* 图片资源。
*/

SRC("src") {
override fun apply(view: View, resName: String) {
if (view is ImageView) {
val drawable = loader.getDrawable(resName) ?: return
view.setImageDrawable(drawable)
}
}
};

abstract fun apply(view: View, resName: String)

/**
* 获取资源管理器。
*/

val loader: SkinLoader
get() = SkinManager.getLoader()
}

当前skins框架只定义了几种主要的换肤属性,你理解原理后,也可以自己进行扩展,比如RadioButton的button属性等。


开源项目传送门


如果你要深入理解完整的换肤流程,请阅读skins的源代码,[github.com/dora4/dview…] 。


作者:dora
来源:juejin.cn/post/7258483700815609916
收起阅读 »

完美代替节假日API,查询中国特色日期

web
马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kb,gzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。 关于中国节假日,后面会...
继续阅读 »

马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kbgzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。


关于中国节假日,后面会跟随国务院发布进行更新,一言既出,驷马难追。


当前版本


NPM Version
GitHub License
README


项目地址:github.com/vsme/chines…


提供的功能



  1. 中国节假日(含调休日、工作日)查询,支持 2004年 至 2024年,包括 2020年 的春节延长;

  2. 24节气查询;

  3. 农历 阳历 互相转换,含有生肖和天干地支。


还需要的功能欢迎补充。


对于非 JS 语言


提供了中国节假日的 JSON 文件,通过链接 chinese-days.json 可以直接引用。


https://cdn.jsdelivr.net/npm/chinese-days/dist/chinese-days.json


快速开始


推荐方式:直接浏览器引入,会跟随国务院发布更新。


<script src="https://cdn.jsdelivr.net/npm/chinese-days/dist/index.min.js"></script>
<script>
const { isHoliday } = chineseDays
console.log(isHoliday('2024-01-01'))
</script>

其他方式安装


npm i chinese-days

使用 ESM 导入


import chineseDays from 'chinese-days'
console.log(chineseDays)

在 Node 中使用


const { isWorkday, isHoliday } = require('chinese-days');
console.log(isWorkday('2020-01-01'));
console.log(isHoliday('2020-01-01'));

节假日模块


isWorkday 检查某个日期是否为工作日


console.log(isWorkday('2023-01-01')); // false

isHoliday 检查某个日期是否为节假日


console.log(isHoliday('2023-01-01')); // true

isInLieu 检查某个日期是否为调休日(in lieu day)


在中国的节假日安排中,调休日是为了连休假期或补班而调整的工作日或休息日。例如,当某个法定假日与周末相连时,可能会将某个周末调整为工作日,或者将某个工作日调整为休息日,以便连休更多天。


// 检查 2024-05-02 返回 `true` 则表示是一个调休日。
console.log(isInLieu('2024-05-02')); // true

// 检查 2024-05-01 返回 `false` 则表示不是一个调休日。
console.log(isInLieu('2024-05-01')); // false

getDayDetail 检查指定日期是否是工作日


函数用于检查指定日期是否是工作日,并返回一个是否工作日的布尔值和日期的详情。



  1. 如果指定日期是工作日,则返回 true 和工作日名称,如果是被调休的工作日,返回 true 和节假日详情。

  2. 如果是节假日,则返回 false 和节假日详情。


// 示例用法

// 正常工作日 周五
console.log(getDayDetail('2024-02-02')); // { "date": "2024-02-02", "work":true,"name":"Friday"}
// 节假日 周末
console.log(getDayDetail('2024-02-03')); // { "date": "2024-02-03", "work":false,"name":"Saturday"}
// 调休需要上班
console.log(getDayDetail('2024-02-04')); // { "date": "2024-02-04", "work":true,"name":"Spring Festival,春节,3"}
// 节假日 春节
console.log(getDayDetail('2024-02-17')); // { "date": "2024-02-17", "work":false,"name":"Spring Festival,春节,3"}

getHolidaysInRange 获取指定日期范围内的所有节假日


接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有节假日;否则,只返回工作日的节假日。



tip: 即使不包括周末,周末的节假日仍然会被返回



// 示例用法
const start = '2024-04-26';
const end = '2024-05-06';

// 获取从 2024-05-01 到 2024-05-10 的所有节假日,包括周末
const holidaysIncludingWeekends = getHolidaysInRange(start, end, true);
console.log('Holidays including weekends:', holidaysIncludingWeekends.map(d => getDayDetail(d)));

// 获取从 2024-05-01 到 2024-05-10 的节假日,不包括周末
const holidaysExcludingWeekends = getHolidaysInRange(start, end, false);
console.log('Holidays excluding weekends:', holidaysExcludingWeekends.map(d => getDayDetail(d)));

getWorkdaysInRange 取指定日期范围内的工作日列表


接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有工作日;否则,只返回周一到周五的工作日。


// 示例用法
const start = '2024-04-26';
const end = '2024-05-06';

// 获取从 2024-05-01 到 2024-05-10 的所有工作日,包括周末
const workdaysIncludingWeekends = getWorkdaysInRange(start, end, true);
console.log('Workdays including weekends:', workdaysIncludingWeekends);

// 获取从 2024-05-01 到 2024-05-10 的工作日,不包括周末
const workdaysExcludingWeekends = getWorkdaysInRange(start, end, false);
console.log('Workdays excluding weekends:', workdaysExcludingWeekends);

findWorkday 查找工作日


查找从今天开始 未来的第 {deltaDays} 个工作日。


// 查找从今天开始 未来的第 {deltaDays} 个工作日
// 如果 deltaDays 为 0,首先检查当前日期是否为工作日。如果是,则直接返回当前日期。
// 如果当前日期不是工作日,会查找下一个工作日。
const currentWorkday = findWorkday(0);
console.log(currentWorkday);

// 查找从今天开始未来的第一个工作日
const nextWorkday = findWorkday(1);
console.log(nextWorkday);

// 查找从今天开始之前的前一个工作日
const previousWorkday = findWorkday(-1);
console.log(previousWorkday);

// 可以传第二个参数 查找具体日期的上下工作日
// 查找从 2024-05-18 开始,未来的第二个工作日
const secondNextWorkday = findWorkday(2, '2024-05-18');
console.log(secondNextWorkday);

节气模块


获取 24 节气的日期


import { getSolarTerms } from "chinese-days";

/** 获取范围内 节气日期数组 */
const solarTerms = getSolarTerms("2024-05-01", "2024-05-20");
solarTerms.forEach(({ date, term, name }) => {
console.log(`${name}: ${date}, ${term}`);
});
// 立夏: 2024-05-05, the_beginning_of_summer
// 小满: 2024-05-20, lesser_fullness_of_grain

// 没有节气 返回 []
getSolarTerms("2024-05-21", "2024-05-25");
// return []

/* 不传 end 参数, 获取某天 节气 */
getSolarTerms("2024-05-20");
// return: [{date: '2024-05-20', term: 'lesser_fullness_of_grain', name: '小满'}]

阳历农历互转


特别说明,此库中:



  1. 2057-09-28 为:农历丁丑(牛)年八月三十;

  2. 2097-08-07 为:农历丁巳(蛇)年七月初一。


阳历转换农历


// 2097-8-7
console.log(getLunarDate('2097-08-07'))

// 2057-9-28
console.log(getLunarDate('2057-09-28'))
// {
// date: "2057-09-28",
// lunarYear: 2057,
// lunarMon: 8,
// lunarDay: 30,
// isLeap: false,
// lunarDayCN: "三十",
// lunarMonCN: "八月",
// lunarYearCN: "二零五七",
// yearCyl: "丁丑",
// monCyl: "己酉",
// dayCyl: "戊子",
// zodiac: "牛"
// }

// 非闰月 和 闰月例子
console.log(getLunarDate('2001-04-27'))
console.log(getLunarDate('2001-05-27'))

根据阳历日期区间,批量获取农历日期


console.log(getLunarDatesInRange('2001-05-21', '2001-05-26'))

农历转换阳历


当为阴历闰月的时候,会出现一个农历日期对应两个阳历日期的情况,所以返回对象形式。


console.log(getSolarDateFromLunar('2001-03-05'))
// {date: '2001-03-29', leapMonthDate: undefined}

console.log(getSolarDateFromLunar('2001-04-05'))
// {date: '2001-04-27', leapMonthDate: '2001-05-27'}

欢迎贡献代码



  1. Fork + Clone 项目到本地;

  2. 节假日: 修改 节假日定义

  3. 农历定义: 修改 农历定义

  4. 其他修改需要自己查看源码;

  5. 执行命令 npm run generate 自动生成 节假日常量文件

  6. 提交PR。


致谢



  1. 农历数据来自于 Bigkoo/Android-PickerView 项目。

  2. 中国节假日数据参考了 Python 版本的 LKI/chinese-calendar 项目。


作者:Yaavi
来源:juejin.cn/post/7371815617462714402
收起阅读 »

程序员还是得明白,除了技术,你必须学会与人沟通

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 工作越久,越觉得沟通能力重要,所以今天想和大家聊聊一个被挺多程序员忽视的能力,沟通能力。 因为忽略沟通能力,自己也吃过不少亏: 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


工作越久,越觉得沟通能力重要,所以今天想和大家聊聊一个被挺多程序员忽视的能力,沟通能力


因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。


横向沟通


先说横向沟通,就是和没有汇报关系的同事或者合作方的沟通,也是我们在工作中,需要沟通场景最多的地方。
因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。
不知道横向沟通时,大家会不会遇见几种场景?



  1. 和团队内同事沟通,都把方案说的很明白了,但总感觉他不懂我的设计思路。

  2. 有问题想请教同事,但却频频被婉拒,甚至被刁难

  3. 与合作团队配合,你急的像热锅上的蚂蚁,但他们不急不慢,甚至不支持,导致项目延期。


我在刚毕业作为一个职场小透明的时候,遇到工作中不会的问题是,请教同事特别小心翼翼,生怕同事拒绝我,或者嫌弃我菜。


遇到比较和善的同事还好,愿意帮助你,但是最怕的就是最怕的就是被一些同事回复:“你能不能自己再看看代码。”


虽然绝大部分程序问题都能通过看代码解决,但是我提出了问题,那一定是我真的看不明白了,不一定是看不懂代码,可能是因为不了解一些业务的背景和历史原因而已。


我特么肯定是看完了没思路才问的,被这么一回复,下次鼓起勇气再问,不知道要到什么时候了。


所以,横向沟通,其实最关键的,就是对于合作关系的同事,如何获得他们的认可与帮助


如何获得他们的认可呢?最重要的在于你对他有多大影响力


比如你有着更好的职级,更老的资历,更广的职场人脉,在横向沟通时都会比较顺利。


但是当你是一个职场新人,或者刚换了一个岗位、一个公司,那么该如何利用自己的影响力,去横向沟通呢?


因此,我会从职业生涯不同的阶段,结合自己的实际经历,来介绍给大家。


职场初期


程序员在工作的前几年,需要提升自己的专业能力为主。


你缺乏业务经验,缺乏技术经验,在工作的沟通中,便很容易处于较低的位置。相信你也有这种感觉,你觉着一个同事更专业、更有能力,那么他和你沟通方案、寻求支持,你一定很容易被说服。


那么你要问,那职场初期,专业能力一定还不够好,那么没有更好的办法提升自己的影响力,和同事沟通了吗?


当然不是,可以通过借势,提升自己的权威度。比如技术方案讨论,如果你对方案存疑,却不能提出更好的方案,如果你拿出比如大厂的解决方案出来,通过对比优劣,你的说服力就能够大大提升。


当然,除了业务、技术经验,提升自己的逻辑能力,也是非常重要的。在寻求帮助、获得支持的时候,有准确的数据,充分的论据,都可以提升你的观点的说服力和可信度。


职场发展期


职场发展期,相信你的技术能力、业务能力会有很大的提升,或许你已经可以独当一面,你的工作范围也不限制与团队内部,可能需要与多个职能的同事一起配合完成工作。


从影响力角度分析,这时候你需要提升在同事眼中的“信任度”为主。


什么构成了你同事眼中的信任度呢?我想从几个方面来分析


你的人品如何,对待事情是否积极、公正。
职场中你更愿意相信谁呢?一定是那些积极主动,并且对所有同事一视同仁的人。


在你的职场发展期,积极主动,是一个人能否继续进步的关键。这个阶段除了快速学习技术,对于业务的学习很容易被忽略。因为遇到技术问题,这是你的工作,你不去解决,你的工作就无法完成。


但是业务知识,你不学,没有人去管你,你只需要看着产品的PRD写代码,也不会出什么错。


但如果你只关注自己的一亩三分地,工作中遇见问题不推进,什么都等着别人催,等着别人解决,那么你的影响力就无从谈起,和同事沟通也会遇到很大阻碍。


你过去的表现如何,你这个人是否靠谱?
代码讲究鲁棒性,人其实也一样。工作中面对不同的环境、条件,都需要能保证工作产出的稳定和可靠。



  1. 比如你的代码质量是否足够高,之前上线的功能,是否稳定。

  2. 对于别人提出的问题、线上的bug,你是否能快速响应不拖沓。最怕的就是别人问你一个问题,你说等一会告诉你,然后你转头就忘了这件事情。

  3. 答应别人的事情,是否能够按时完成,而不是到dead line,才告诉别人还有问题。


职场稳定期


在职场稳定期,你既有了足够的技术专业能力,也成为了一个正直、靠谱的人,这时候我认为“互惠原则”是支撑你沟通顺畅的一个小窍门。



互惠原则是社会心理学中的一个概念,指的是人与人之间天生有回馈他人恩惠的倾向。简单来说,就是“你对我好,我忍不住要报答你”。



我想通过几个例子和大家说明我是如何因为互惠原则


有一次,合作团队负责的一个需求改造很紧急,但是因为他们技术方案没有评估到我负责团队的改动,所以没有给我们的产品提需求,因此我这边没有技术排期。


如果临时提需求,那么根据排期就要排到下一个开发周期了,他们需求自然也要延期。


他们组长找到我,我和他一起评估了一下改造的复杂性,并不复杂,于是我答应他在他们上线前我也会改造完并发版。于是自己提了一个技术需求,完成了相关改造,他们的需求也顺利上线。


还有一次,产品经理考虑不周,新上线的功能校验比较严格,因为设计问题,运营同事频繁吐槽产品,影响了他们的使用效率。


正常走需求迭代,需要等到2周后了,于是产品找到我沟通,看是否有什么临时的解决方案。
为了解决运营问题,我看了下,需要临时处理一下数据,既能保证使用,也能节约不少运营的人力成本,于是我写了几个脚本,临时处理线上数据,解决了这个问题。


在这两个场景中,我发现了他们的诉求,并且发现他们对此真的很着急,于是我尽我所能,主动的为他们提供帮助。你可能会说,你这样临时多干了很多事,太亏了。


是的,短期来看,我确实牺牲了自己一部分时间去帮助他们,但是我也收获了对他们的影响力,比如承诺的一定完成,还能帮他们摆平一些问题,慢慢的我的影响力就越来越强。在以后我需要帮助的时候,他们自然也乐意去帮助我。年度的360绩效评估,我自然能够收获他们的认可与好评,长期来看,受益的就是你自己。


向上沟通


向上沟通就是和你的上级进行沟通,即使工作好多年了,我对于和领导沟通依然觉着很头大,相信这也是你非常头疼的问题。


看看一下几个场景,你有没有中招呢?



  1. 觉着领导太忙了,我目前的工作似乎也没这么重要需要汇报,找领导也不知道说什么,等领导不忙了再说。

  2. 我做好领导交给我的事情就行了,拿结果说话。

  3. 有困难需要和领导协调,但是不知道怎么说,领导会不会觉着我能力不行?

  4. 领导交给我的任务到底是什么意思?需要做到什么程度,到底是否着急。


我认为向上沟通,最重要的一个点,就是主动大胆,跨过心里的那道坎,因为绝大部分时候,我们就是内心有一个卡点,觉着无话可说,觉着没有必要。


向上沟通如果你要想影响上级,实话实说太难了,并不适用于我们每一个人。所以说预期说向上沟通,不如说我们如何才能够利用好我们的上级,帮助自己更好的发展。


展示自己


自己作出成绩的时候,觉着无人知晓,那么和上级适当的展示自己。


展示自己,你是不是觉着,这样有点显摆的意思?其实并不是,在沟通过程中,你或许了解到,这件事是否符合当下团队的发展方向,你感觉有了成绩,是否是自嗨,有没有地方需要被纠偏?


如果真正做得好,被领导认可,那么可以极大的增加你的自信心,输出你的影响力,避免“酒香也怕巷子深”。如果发现问题,那也从上级的角度发现了可以提升的地方,对你来讲也是百利而无一害。


当然,展示自己并不是直接去和领导说,我做了xxx东西,非常厉害,用了什么什么技术,而是有一种其他方式,比如协调大家,做一次技术分享,把自己的东西展示出来。


信息同步


上级安排的工作任务,无论遇到什么问题,都自己扛。


我在工作的前几年里,一直是一个低头干活的人,自己很有计划性,即使遇到问题,我也会靠着自己的力量去死磕,我一直以为,我是一个靠谱的人,领导给我安排的工作我都能自己完成,多给领导省心。


但后来有一次和领导沟通,领导说有时候一旦周期拉长,领导对我的信心就会减弱,最关键的就是我向上的反馈不够多,像是两个月的OKR,或者半年的规划时,领导很难知道具体的进度如何,最后是否能达成,因为上级需要识别风险,提前处理。


持续的做好信息同步,领导对你的信任度才能不断加深,你才能过承担更重要的工作。


困惑解答


工作久了,一定会有职业生涯的困惑,,未来怎么发展,干的不开心,甚至想离职,都可以试着和上级聊聊。


因为我们的工作内容比较单一,所以我们对于很多事情看待的角度也会单一,和领导聊聊,可以从更高的角度看一下自己当前的阶段与状态。


向下沟通


还记得开头说的那句话吗:“真正会沟通的人,不需要能说会道、口若悬河,而是懂提问、会倾听,能洞察需求、摸透人心。”



学会听,比学会讲更重要。




学会提问


我们日常生活中的提问,往往分为2种:开放式提问和限制式提问。


比如,你询问下级:最近团队比较忙,加班比较多,当然也做出了不错的成绩,不知道你怎么看待我们最近完成的这个项目呢?


这就是开放式提问,对于最近的忙碌,可能会有很多问题,或许是产品需求不合理,也可能是工程质量不高或者大家配合不够顺畅,这就是让对方做开放问答题。


如果换一个问法:最近加班比较多,你这块的工作,是否都按期完成了呢?


这种提问方式,对方只能回答是或者否。你能获得的信息就比较少。


在沟通的时候,我们要尽量多用开放式提问,要鼓励对方自由回答,多让他们讲。这样有助于你收集资料、挖掘需求,而且,还能鼓励对方对问题做出详细说明。


试着倾听


向下沟通,大家可能都会觉得重点应该在怎么说,但是向下沟通,更重要的是倾听。为什么这么说呢,因为在向下沟通的环境中,你的职级、经验通常是要比沟通对象多出一些的,如果在没有理解对方意思的情况下,很容易陷入单向输出的情况,你哇啦哇啦说了一堆方法论、公司目标与方向,但实际上沟通起不到太好的效果。


3F倾听法是一种有效的沟通技巧,它强调在倾听过程中要关注三个核心方面:Fact(事实)、Feel(感受)和Focus(意图)。这种方法可以帮助我们更全面地理解说话者的意图和需求,从而促进更有效的沟通。


倾听事实:这一步骤要求倾听者专注于对方所陈述的客观事实,避免加入自己的主观评判。倾听者需要区分事实与观点,确保理解的是对方所描述的已发生且可考证的事情。在这个过程中,倾听者应保持开放的心态,不急于作出解释或提供建议,而是先确保对事实有准确的理解。


倾听感受:在倾听事实的同时,倾听者需要注意观察对方的情绪状态,感知对方的感受。通过观察对方的肢体动作、语言、声调、表情变化等,可以更好地理解对方的情绪,与对方共情,尝试站在对方的角度去感受和理解其情绪。


倾听意图:这一步骤要求倾听者深入了解对方话语背后的真实意图和期望,而不仅仅是表面的意思。通过提问和澄清,确保准确理解对方的意图,避免误解和沟通障碍。在理解对方意图的基础上,可以更好地回应对方的需求,促进有效的沟通。


说在最后


好了,文章到这里就要结束啦,很感谢你能看到最后,经验有限,文章中如果有问题,希望你能够指正。


希望你看完之后,能够重视沟通这件事,在和代码“沟通“越来越熟练的同时,也要注重与人如何沟通。


不知道你在和同事的沟通过程中,有没有遇到什么困难或者好的经验呢?欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~


本篇文章是第36篇原创文章,2024目标进度36/100,欢迎有趣的你关注我~


作者:东东拿铁
来源:juejin.cn/post/7373474414430797863
收起阅读 »

代码和人,有一个能跑就行。为啥程序员总写dirty code

在程序员行业有一句:“代码和人,有一个能跑就行”。这句话对吗?为什么会产生这个问题?哪些代码能跑就行?有一些代码,真的就是不能动,一动就崩。里面逻辑复杂, 代码冗余,具备了一些不良代码的特征,但是它就是能跑,就是能支撑业务。有一个通俗的称谓为“屎山代码”。&n...
继续阅读 »

在程序员行业有一句:“代码和人,有一个能跑就行”。这句话对吗?为什么会产生这个问题?

哪些代码能跑就行?

有一些代码,真的就是不能动,一动就崩。里面逻辑复杂, 代码冗余,具备了一些不良代码的特征,但是它就是能跑,就是能支撑业务。有一个通俗的称谓为“屎山代码”。 image.png

哪些是dirty code

  • 缺乏注释和文档:完全没有注释或者复杂逻辑无文档
  • 命名不规范:变量、函数和类的命名不符合约定或没有约定 例如a,b,c,变量用动词,方法用名词,驼峰下划线混用等
  • 代码重复:同样的代码逻辑在多个地方重复出现,增加了维护的难度。 代码不做抽象,公共方法复制拷贝,一个方法复制多份
  • 复杂的逻辑:代码逻辑过于复杂,缺乏清晰的结构和模块化设计。
  • 硬编码值:使用硬编码的数值或字符串,而不是使用常量或配置文件

dirty code是如何产生的?

时间压力

项目不给足够时间,倒排期工程,项目经理整天催催催,老板天天问进度。预计5天,报了6天,砍到3天,1天的时候问做到哪了,2天问怎么还没做完。 你让我抽象,你让我搞架构,但是不给我时间,写出来的代码优先要进测试,提了bug再改改呗,反正缝缝补补又3年。

过于自信

自认自己的代码足够牛b。

不需要注释就可以看懂,不就是几个变量名吗?别人理一理逻辑就可以了,我的代码自己可以解释自己。 不需要抽象,这里都是一整套逻辑的。什么?你也要用这套代码,自己复制出去,别动我代码。我们要签订《代码互不侵犯条约》。

经验不足

新手小白能完成任务就不错了,什么鲁棒,什么设计模式,完全不需要考虑。一个函数500行?抱歉那是这个功能的瓶颈,不是我的瓶颈。

企业文化,标准/规范缺乏

你还记得你上一次做code review是啥时候吗?在夜深人静的时候,有没有回想每天996为啥老板还没开上大奔?

老板要的是结果,不是过程,代码写的再好,最后业务不核心,不干掉你干掉谁?

防御型编程

这个不多说了,懂的都懂。

明明知道有问题,为什么不重构呢?

重点项目,核心代码,不敢动。

在一些关键项目中,核心代码往往被视为系统的“心脏”。由于这些代码对于系统的稳定性至关重要,任何改动都可能带来巨大的风险。一旦出现问题,不仅影响系统的正常运行,还会直接影响团队的绩效和公司业务。因此,程序员往往选择维持现状,尽量避免对这些代码进行大的改动。

边缘项目,长期不迭代代码,不敢动。

对于一些边缘项目或已经很久没有进行过迭代的代码,由于缺乏持续的维护和更新,这些代码的整体质量和可读性往往较低。如果要对其进行修改,可能需要对整个系统进行全流程回归测试,这不仅费时费力,还可能导致人力资源的浪费。因此,除非遇到重大问题,否则这些代码通常也不会轻易被动。 image.png

代码能跑就行的结果是啥?

经过你一系列深思熟虑,不断优化重构,代码终于写的跟诗一样的。但是你的工期比别人多了1/2,虽然bug少了,但是研发成本大增。

尽管你的代码十分优秀,但不出意外的,在绩效评定的时候,你只拿到了及格。相反,另外一个能跑就行的同事,在每次线上出现问题的时候,都能及时化解,拿到了优秀。

因为你的项目进度慢,一些新项目和重点项目优先分配给了其他人。

慢慢的你对这家公司失去了信心,转投其他公司,但新公司的领导看到你的代码,惊为天人,于是你顺利的走上人生巅峰(给个happy end吧)。 image.png

究竟应该怎么做?

中国人讲究“中庸”,大多数情况,在非开源项目或公司无具体要求时,要求我们要掌握一个开发成本/代码质量的度。

尤其在一些并不太优秀的团队中,我们优秀的代码质量无法为我们换得足够匹配的价值回报。相反,交付效率/交付质量/线上稳定性才是优先考虑的问题。

尤其在现在降本增笑的大环境下,保护自己才是最重要的。

但优质的代码,带来的是身心的愉悦,后续维护的简单,代码的灵活性更高。建议在核心代码,工具类等优先使用高质量代码,而在一些增删改查,非核心/重点项目内容上,还是难得糊涂一下吧。


作者:天元reborn
来源:juejin.cn/post/7368397264027402275
收起阅读 »

如何选择 Android 唯一标识符

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。 标识符 IMEI 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10...
继续阅读 »

前言


大家好,我是未央歌,一个默默无闻的移动开发搬砖者~


本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。


标识符


IMEI



  • 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10+ 开始官方取消了获取 IMEI 的 API,无法获取到 IMEI 了


fun getIMEI(context: Context): String {
val telephonyManager = context
.getSystemService(TELEPHONY_SERVICE) as TelephonyManager
return telephonyManager.deviceId
}

Android ID(SSAID)



  • 无需任何权限

  • 卸载安装不会改变,除非刷机或重置系统

  • Android 8.0 之后签名不同的 APP 获取的 Android ID 是不一样的

  • 部分设备由于制造商错误实现,导致多台设备会返回相同的 Android ID

  • 可能为空


fun getAndroidID(context: Context): String {
return Settings.System.getString(context.contentResolver,Settings.Secure.ANDROID_ID)
}

MAC 地址



  • 需要申请权限,Android 12 之后 BluetoothAdapter.getDefaultAdapter().getAddress()需要动态申请 android.permission.BLUETOOTH_CONNECT 权限

  • MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会变化

  • 搭载 Android 10+ 的设备会报告不是设备所有者应用的所有应用的随机化 MAC 地址

  • 在 Android 6.0 到 Android 9 中,本地设备 MAC 地址(如 WLAN 和蓝牙)无法通过第三方 API 使用 会返回 02:00:00:00:00:00,且需要 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限


Widevine ID



  • DRM 数字版权管理 ID ,访问此 ID 无需任何权限

  • 对于搭载 Android 8.0 的设备,Widevine 客户端 ID 将为每个应用软件包名称和网络源(对于网络浏览器)返回一个不同的值

  • 可能为空


fun getWidevineID(): String {
try {
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
val mediaDrm = MediaDrm(WIDEVINE_UUID)
val widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
val sb = StringBuilder();
for (byte in widevineId) {
sb.append(String.format("x", byte))
}
return sb.toString();
} catch (e: Exception) {
} catch (e: Error) {
}
return ""
}

AAID



  • 无需任何权限

  • Google 推出的广告 ID ,可由用户重置的标识符,适用于广告用例

  • 系统需要自带 Google Play Services 才支持,且用户可以在系统设置中重置



重置后,在未获得用户明确许可的情况下,新的广告标识符不得与先前的广告标识符或由先前的广告标识符所衍生的数据相关联。




还要注意,Google Play 开发者内容政策要求广告 ID“不得与个人身份信息或任何永久性设备标识符(例如:SSAID、MAC 地址、IMEI 等)相关联。”




在支持多个用户(包括访客用户在内)的 Android 设备上,您的应用可能会在同一设备上获得不同的广告 ID。这些不同的 ID 对应于登录该设备的不同用户。



OAID



  • 无需任何权限

  • 国内移动安全联盟出台的“拯救”国内移动广告的广告跟踪标识符

  • 基本上是国内知名厂商 Android 10+ 才支持,且用户可以在系统设置中重置


UUID



  • 生成之后本地持久化保存

  • 卸载后重新安装、清除应用缓存 会改变


如何选择


同个开发商需要追踪对比旗下应用各用户的行为



  • 可以采用 Android ID(SSAID),并且不同应用需使用同一签名

  • 如果获得的 Android ID(SSAID)为空,可以用 UUID 代替【 OAID / AAID 代替也可,但需要引入第三方库】

  • 在 Android 8.0+ 中, Android ID(SSAID)提供了一个在由同一开发者签名密钥签名的应用之间通用的标识符


希望限制应用内的免费内容(如文章)



  • 可以采用 UUID ,作用域是应用范围,用户要想规避内容限制就必须重新安装应用


用户群体主要是大陆



  • 可以采用 OAID ,低版本配合采用 Android ID(SSAID)/ UUID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等


用户群体在海外



  • 可以采用 AAID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等




作者:未央歌
来源:juejin.cn/post/7262558218169008188
收起阅读 »

连公司WiFi后,无法访问外网,怎么回事,如何解决?

问题描述 从甲方项目组返回公司后,我习惯性连上公司WiFi,准备百度一个bug,突然我发现无打开百度,F5刷新了好几次也没用,浏览器报了下面的错误信息 尝试ping了一下 http://www.badu.com,好家伙,直接丢包 然后运行 ipconfig/...
继续阅读 »

问题描述


从甲方项目组返回公司后,我习惯性连上公司WiFi,准备百度一个bug,突然我发现无打开百度,F5刷新了好几次也没用,浏览器报了下面的错误信息


523d43c309e5d5b786dff74cc54894bf.png


尝试ping了一下 http://www.badu.com,好家伙,直接丢包


然后运行 ipconfig/all 命令看了一下本机的DNSF服务器信息


b75ff8dab79e97b5698daa4e060e24af.png


我的本机DNS地址是192.168.0.1


通常,本机DNS地址若为192.168.0.1,说明所连WiFi的路由器可能被设定为执行DNS转发职责,或者是期望客户端直接使用路由器作为DNS解析的入口点。而192.168.0.1一般是路由器的默认IP地址,并非一个标准的公共DNS服务器地址。在这种情况下,访问不了外网,例如百度,新浪微博等,有可能是路由器的DNS转发功能没有正常工作,或者路由器自身没有被配置正确以访问外部的DNS服务器


最简单直接的解决方法是手动设置主机的DNS地址为公共的DNS服务器地址



  • Google DNS:8.8.8.8 & 8.8.4.4

  • Cloudflare DNS: 1.1.1.1

  • 中国电信:114.114.114.114

  • 中国联通:223.5.5.5


7785e7beed4c08710c07617232dad576.png




OK,可以正常访问百度了



45db3372360d9ecab863ffb1da9e1246.png


这让我产生了非常浓烈的好奇,从浏览器上输入URL到显示页面,中间究竟发生了什么?


image.png


问题探究



这是一道面试题



1716627344750.png


从浏览器中输入URL并按下回车键后,直到网页内容完全显示在屏幕上,这个过程中发生了一系列复杂的步骤,大致可以概括如下:



  1. URL解析:浏览器首先解析输入的URL,提取出协议、域名、路径以及查询字符串等信息。

  2. 检查缓存:在发起网络请求之前,浏览器会检查本地缓存(包括浏览器缓存、系统缓存乃至路由器缓存),看看是否已经存储了该请求的资源。如果有且未过期,则直接使用缓存内容,无需继续下面的步骤。

  3. DNS解析:如果缓存中没有所需资源,浏览器会通过DNS(域名系统)将网址的域名转换为IP地址,因为网络通信是基于IP地址的。这个过程中可能涉及递归查询和迭代查询,直至找到域名对应的IP地址。

  4. TCP连接建立:获得服务器IP后,浏览器使用TCP协议与服务器建立连接。这通常涉及TCP三次握手过程,确保数据传输的可靠性和连接的双方都准备好通信。

  5. 发起HTTP/HTTPS请求:建立连接后,浏览器构造HTTP或HTTPS请求报文,包含请求方法(如GET或POST)、请求头(携带浏览器信息、请求资源的位置等)以及可能的请求体,然后发送给服务器。

  6. 服务器处理请求:服务器接收到请求后,根据请求的内容处理并准备响应,这可能涉及数据库查询、服务器端脚本执行等操作。

  7. 响应浏览器:服务器将处理好的响应数据(包括状态码、响应头、响应体等)封装成HTTP响应报文,发送回浏览器。

  8. 浏览器接收响应:浏览器接收响应数据,如果响应中有新的资源(如CSS、JavaScript、图片等),浏览器会根据需要再次发起请求获取这些资源。

  9. 渲染页面:浏览器开始解析HTML文档,构建DOM(文档对象模型)树,同时解析CSS文件构建CSSOM(CSS对象模型)树,结合这两棵树形成渲染树(Render Tree)。接着进行布局(Layout)和绘制(Painting),即确定每个节点在屏幕上的位置和外观,最终将页面内容呈现给用户。

  10. 执行JavaScript:页面中的JavaScript代码会被解析和执行,它可能修改DOM和CSSOM,导致重新布局和绘制。此外,异步请求如Ajax也可以在这个阶段发起,动态更新页面内容。

  11. 页面交互:页面加载完毕后,用户可以与页面进行交互,触发事件处理程序,进一步的JavaScript执行可能会改变页面状态。

  12. 连接关闭:当所有数据传输完毕,TCP连接会通过四次挥手的过程优雅地关闭。


上述过程中涉及到了多个层次的技术和协议,从应用层的HTTP/HTTPS、运输层的TCP、网络层的IP到链路层的以太网协议等,共同协作完成了从简单的URL输入到复杂页面展示的任务。


cbacfb95186577a2e2d92fe72fa8d0c5.png


基于上述分析,问题发生在第③步(DNS解析)上,要想回答何为DNS解析,就必须弄明白何为DNS。


何为DNS?


DNS,英文全称为Domain Name System,即域名系统。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道这个服务器对应的 IP地址,而对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址【正向解析】。以下定义概念摘自《计算机网络:自顶向下方法》:




  1. 一个由分层的 DNS 服务器( DNS server) 实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议



分布式,层次数据库


如何理解分布式?


随着互联网的快速发展,主机日益增多且数量庞大,采用单一DNS服务器上集中响应的设计并不可取,这种设计容易造成单点故障维护困难通信容量受限等问题。


为了应对上述问题和扩展性, DNS 使用了大量的 DNS 服务器并分布在全世界范围内。因为没有一台 DNS 服务器可以存放Internet上所有主机的映射数据, 相反,该映射数据被分布存储在所有的 DNS 服务器上。


如何理解层次?


DNS服务器采用层次组织,大致说来,有3种类型的 DNS 服务器:根 DNS 服务器、 顶级域 (Top- Level Domain , TLD) DNS 服务器和权威 DNS 服务器。它们的层次结构方式如下所示:


1716628019691.png


图片来源:《计算机网络:自顶向下方法》



  • 根DNS服务器


    我们首先要明确根域名是什么,它没有特定的名称,仅由一个点(.)表示。在技术层面上,它是所有域名查询的起点,负责指引域名解析过程中的查询请求到相应的顶级DNS(TLD)服务器,如.com.net.org等。而在实际的网址中,根域名通常隐含而不显示,例如com.baidu.com.,后面的点一般不会显示。


    根DNS服务器是互联网基础设施的关键部分,全球共有13组根DNS服务器,它们存储了顶级DNS服务器的地址信息,从而帮助我们将域名转换为用于网络通信的IP地址。根DNS的管理由国际互联网名称与数字地址分配机构(ICANN)负责。


  • 顶级域服务器


    这些服务器负责顶级域名,如comorgnetedugov,以及所有国家的顶级域名如uk、r、ca和jp。TLD提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。




  • 权威DNS服务器



    在因特网上具有公共可访问主机(如Wb服务器和邮件服务器)的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字映射为IP地址。



    以上内容摘自《计算机网络:自顶向下方法》,比较绕口,通俗来讲就是提供最终的主机—IP映射



本地DNS服务器


在上一节的DNS层次结构中,眼尖的小伙伴会发现,并未提及本地DNS服务器,那为什么呢?一个本地DNS服务器,从严格说来,它并不属于上述DNS服务器的层次结构,但它对DNS层次结构0是至关重要的


每个ISP(Internet Service Provider,即网络业务提供商)都有一台本地DNS服务器(也叫默认名字服务器)。当主机与某个ISP连接时,例如一个小区的ISP,一个学校的ISP等,该ISP会提供一台主机的IP地址,该主机具有一台或多台其本地DNS服务器的IP地址,通常主机的本地DNS服务器会临近主机,当主机发出DNS请求时,该请求被发往本地DNS服务器,它起着代理的作用,并将该请求转发到DNS服务器层次结构中。


迭代查询,递归查询


如下图所示,假设主机abc.net想要获取主机xyz.edu的IP地址,大致会进行如下步骤:


1716647441277.png



  1. 主机abc.net首先向它的本地DNS服务器发送一个查询报文,该报文会含有被转换的主机名xyz.edu。

  2. 本地DNS服务器会将该报文转发给根DNS服务器。

  3. 该根DNS服务器注意到其edu前缀并向本地DNS服务器返回负责edu的TLD(顶级域服务器)的IP地址列表。

  4. 该本地DNS服务器则再次向这些TLD 服务器中的其中一台发送查询报文。

  5. 该 TLD 服务器注意到 xyz. edu 前缀,并把权威DNS服务器的IP地址响应给该本地DNS服务器。

  6. 本地 DNS 服务器直接向权威DNS服务器中的其中一台重发查询报文。

  7. 该权威服务器会用xyz.edu的lP地址进行响应。

  8. 本地DNS服务器会将主机xyz.edu及其IP地址的映射数据响应给主机abc.net,主机abc.net拿到它的IP就能给主机xyz.edu发送请求。


在上图例子中,主机abc.net向本地DNS服务器发出的查询是递归查询因为该查询请求是以主机abc.net以自己的名义获得该映射。 而后继的3 个查询是迭代查询,因为所有的回答都是直接返回给本地DNS服务器。 即第①步是递归查询 ,第②,④,⑥步是迭代查询。


那所有的DNS查询都遵循迭代 + 递归的方式吗?


答案并非如此,虽然在理论上,任何DNS查询既可以是迭代的,也能是递归的。


如下图,所有的DNS查询是都是递归的,因为所有的查询请求是以主机abc.net以自己的名义获得该映射。


1716650770678.png


DNS缓存


实际上,为了改善时延性能并减少在Internet上到处传输的 DNS报文数量,DNS 广泛使用了缓存技术。 DNS 缓存的原理非常简单。 在一个请求链中,当某 DNS服务器接收一个 DNS 回答(例如,包含主机名到IP地址的映射)时,它能将该回答中的信息缓存在本地中。 下次查询时便可直接用缓存里的内容。


注意,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,通常设置为两天时间,一旦过了生存时间,这条记录就会从缓存移出。


有了缓存,本地 DNS 服务器可以立即返回所要解析主机的IP地址,而不必查询任何其他DNS服务器。 而本地 DNS服务器也能够缓存TLD服务器的地址,因而经常绕过查询链中的根 DNS服务器。


参考资料


计算机网络:自顶向下方法(原书第8版) (豆瓣) (douban.com)


作者:Jormungand581
来源:juejin.cn/post/7372456890344243215
收起阅读 »

29岁,大厂女程序员,总包六折结束北漂,聊聊换城市。

先简单描述下下我的背景。 95,双非本科,多段大厂前端背景,未婚未育,北漂快七年。 三段Gap经历,最长4个月。 回二线三个月,目前对人生很乐观。 1. 离开京东,结束北漂 昨天前同事发我消息吐槽,东子又跟兄弟们发言,不拼搏的也不再认兄弟了。 考勤新规真是十分...
继续阅读 »

先简单描述下下我的背景。
95,双非本科,多段大厂前端背景,未婚未育,北漂快七年。


三段Gap经历,最长4个月。


回二线三个月,目前对人生很乐观。


1. 离开京东,结束北漂


昨天前同事发我消息吐槽,东子又跟兄弟们发言,不拼搏的也不再认兄弟了。


考勤新规真是十分劝退,北京西南角五环外的京东总部。
研发卡到九点上班,砍掉午休。


在东子,如果是9点上班,一定不存在6点下班,东子的各大Bu潜规则都是晚上九十点左右下班,
工时每天要打满10个小时往上, 边缘部门也不例外。


在北京如果是9点到工位,我肯定是做不到。
也幸亏在2024年年初,我从京东拿到了礼包+年终奖,顺便结束了北漂。


现在在西北省会城市继续搞老本行做前端,不用再忧患9点上班带来的痛苦。


离开北京的原因和大多数人一样,


从工作发展上看,21年后,感觉“混”不再是个简单的事。
无论是晋升或是涨薪都难度比之前要大,并且这往前迈出的一步,也意味着要做更多内卷和向上管理的工作,性价比很低。


从生活体验这一面来讲,六七年的北漂体验已经让我对北京这座大城市带来的“通勤疲惫”以及“人际关系冷漠”感到麻木。挣了工资就攒着,没啥钱用来消费。 同时,我对“女前端”的职业生涯年限乐观的预期也就在三十左右,在北京的焦虑感也比其他城市会更重些,大家都是孤舟,去年因为焦虑也让我身体出了一些异常,还是快逃吧。


还有一点,我也跟大部分广大民群众一样,有个“娇夫孩子热炕头”的朴实生活愿望,“媳妇”还是回家找吧。


2. 换到二线城市之后的工作体验


回到家的这三个月,吃了很多好吃的(西北人就是西北胃!!),见了很多大学同学以及在省会的亲戚朋友。


换了个赛道,不在互联网行业,每天没那么多会议要开。
6点多就能下班,我开始下班看到夕阳,很幸福。


IMG_2629.JPG


回想在上家公司的工作内容,工作时间不是开会就是扯皮,还要保证产出和质量。
同时还有一无是处的“小组长” 要对你考核,组织架构不断的调整,职场环境糟糕,自己内耗严重。
赚那么点工资,医美钱都不够。
真的划不来。


通过互联网,也认识了很多从 北上广 回西安的前端同行,拉了群交流。
有需要的小伙伴可以后台didi我。


3. 年前找工作的感受


年前为了换工作,面试了一个多月,大概30多家公司。


包含大中小厂,面试通过大概在50%+ ,也拿了些offer。
跟周围很多同行都交流过我的面试情况,大部分都觉得我是,实力+运气。


我是业务型前端,之前从B到C,pc移动,跨端都有经验。
个人感受是面试整体的内容和之前内的差不多,前端还是 八股+项目 为主。


btw,相关面经我和其他的大厂前端朋友也沉淀了一份前端知识库,持续更新中,有需要dd.


不过已经是5年+经验 ,明显的能感觉到在问项目的时候,更加的细致和深入。细节挖得很深。
除了技术外,软实力方面也有所要求。面试中经常会被问到,如何去做一些项目管理及团队赋能的相关内容等等。


面到后期 ,跟HR谈薪才是最疲惫的环节。
京东整体研发的工资也不高,30%的涨幅都不好谈,这个跟我的面试表现也有关系。
我确实不是什么技术大佬。


回顾了下年前的面试记录。


投递了很多家公司,内推+BOSS直聘+脉脉 的简历通过率是最高的。


快手各个部门都显示不匹配,字节仍旧一轮游。
有约在晚上九点面试的,持续面到十一点多才结束。
我以为这么辛苦了,至少能给个二面,结果也是不匹配截止到一面。


北京真的把我面麻了,不qiu面了,到后期焦虑感也上来了。
直接留了两三周gap, 杭州长沙玩了一通。还是很开心的~


IMG_4655.JPG


后面西安的公司发了offer,降薪就降薪吧。


北京我实在是不想玩了,这几年跟老鼠一样,体验实在是太差,再升级苦难,我也是扛不住。


整体上看,2024年春节前,于我个人来看没有什么特别好的hc.
跟同行交流,大家也都是类似的情况。
而且一个比一个叫的惨,
我认识几个我认为技术不错的前端大佬,有从2022就开始gap的。
按照现在HR的标准,Gap真的拉黑率太高了,要打工进厂的话,还是尽量不要Gap吧,技术大神除外。


4. 其他


回西安工作后,好处就是,通勤十分钟,也不再吃外卖。
朋友家人都在身边,
总包虽然打折,也够养活自己了。
“外包之都”的名字也不是白叫的。我也能感觉到,跳槽应该是没啥地方能跳的了,基本整个西安市场上,好的前端HC是阶段性间歇出现的,机会不多。


工作上还是会有些焦虑,也逐渐意识到沉淀技术能力的重要性。
顺着最近对工作生活思考,
调整了下工作和学习上的方向,目前看来收益不小。
感兴趣的话,欢迎点赞收藏,我准备后面再写一期。


总之,目前来看体验还是很不错的,我本来物欲也不高,
希望同行们都心态向好,
努力生活。


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

关注用户隐私安全 OPPO助力开发者保护个人信息安全

为助力广大APP开发者做好个人信息保护工作,更好地维护用户合法权益,在工业和信息化部信息通信管理局的指导下,5月23日,OPPO在深圳组织开展「个保合规我参与」公益培训宣讲会——OPPO站,这也是本次系列活动的首站。近百名移动应用开发者参加了此次活动,现场多位...
继续阅读 »

为助力广大APP开发者做好个人信息保护工作,更好地维护用户合法权益,在工业和信息化部信息通信管理局的指导下,5月23日,OPPO在深圳组织开展「个保合规我参与」公益培训宣讲会——OPPO站,这也是本次系列活动的首站。近百名移动应用开发者参加了此次活动,现场多位行业专家结合工作实践进行内容分享交流,探讨保护用户个人信息安全,共同推动APP行业健康发展。

1.JPG

深圳市通信管理局副局长陈逸菁在开场致辞中表示,广大企业和应用开发者要重视并做好APP个人信息和用户权益保护工作,保障互联网行业规范健康发展,维护网络安全和公众利益,共同推进移动互联网应用产业高质量发展。

2.jpeg

随后,OPPO全球数据与网络安全总经理韩方登场致辞,他指出日新月异的信息科技为生活带来了极大便利,同时也给个人隐私和信息安全带来了诸多挑战。维护个人隐私权益,确保公众在数字生活中的安全感和舒适度,已然成为当下的一项重大课题。为此,OPPO建立了完善的隐私合规体系,从技术、制度、培训等多方面入手,全面落实隐私保护。

3.jpeg

深圳市前海互联网安全保障中心互联网管理部主任梁建翔基于APP监管合规体系和APP备案管理要求两大方面,结合当前APP个人信息保护问题及相关法律法规要求,对APP备案与合规管理政策进行了介绍。他指出,移动互联网应用程序主办者和分发平台要切实履行合规义务,落实个人信息保护和“先备案后服务”要求,充分保护用户权益,维护网络安全和公共利益,促进互联网行业规范健康发展。

4.JPG

中国信息通信研究院泰尔终端实验室信息安全部副主任王艳红对《工业和信息化部关于进一步提升移动互联网应用服务能力的通知》进行了解读。从通知出台的背景和目的、重点工作考虑、APP开发运营者、分发平台等产业链上下游主体如何落实主体责任做了详细说明。

5.jpeg

中国信息通信研究院政策与经济研究所法律研究部专家端晨希从智能时代个人信息保护法律合规问题入手,介绍了当前APP个人信息保护的问题形势、相关法律法规及政策要求,就内容推荐服务可能存在的合规难点进行了解读。

6.jpeg

中国信息通信研究院技术与标准研究所产业互联网研究部专家李鑫对SDK个人信息和权益保护技术要求及SDK管理服务平台做了介绍,并现场展示了SDK管理服务平台相关功能服务,倡议行业企业共同推动SDK生态合规,以更好地保护用户合法权益,共同促进行业健康发展。

7.jpeg

中国信息通信研究院云计算与大数据研究所审计与治理部专家甘泉围绕《个人信息保护合规审计实施方法和实务》,分享了合规审计的背景、方法等和相关事件案例,为评估个人信息保护制度的可操作性以及相关制度的有效实施提供了指引。

8.jpeg

中国信息通信研究院泰尔终端实验室信息安全部专家邓佑军从个人权利响应和终端厂商义务两大维度展开,分享了移动智能终端用户权益保护规范要点相关内容,并结合实践中发现的违规收集个人信息、过度索取权限、欺骗误导强迫用户等典型问题,介绍了专项整治行动的整改流程,助力开发运营者高效完成合规整改。

9.jpeg

中国信息通信研究院泰尔终端实验室信息安全部专家武林娜结合生成式人工智能的交互方式、产品形态、产业生态现状,指出AIGC时代移动应用对个人信息安全带来的全方位影响及生成式人工智能模型本身带来的挑战,并提出AI大模型赋能个人信息保护的路径探索。

10.jpeg

加强用户信息安全及隐私保护,一直是OPPO关注的重点之一。现场OPPO高级安全研发工程师王学成分享了OPPO保护消费者个人信息安全的实践经验,并介绍了目前OPPO已建立起安全隐私全流程全场景防护体系, 通过OPPO智能护盾、7×24小时人工、三方引擎三重扫描,全天候对应用进行严格监测,及时拦截违规应用,并已通过《白皮书》形式对外展示应用安全治理成果。

11.jpeg

据了解,OPPO智能护盾通过安全大脑实现了贯穿应用程序从上架、下载、安装、启动、运行、卸载阶段全生命周期的安全隐私治理。在应用程序上架前,OPPO通过开发者信誉管理系统, 以敏感权限检测、隐私政策合规检测、隐私自动化检测方式审查开发者资质,确保应用来源的可靠性。通过对全量应用开发者进行实名和信誉排名,确保上线应用的开发者身份真实可信;同时,严控存疑开发者、强制清退封禁恶意开发者,从源头上确保OPPO软件商店在架应用来源的安全性与真实性。

除了应用上架环节外,针对应用下载、安装环节的潜在风险OPPO也进行了保护。当用户从浏览器等非官方渠道获取应用包、进行安装流程时,系统会进入安装扫描环节,由OPPO智能护盾安全大脑提供检测能力,对恶意应用进行警示和拦截。据统计,OPPO手机终端每年拦截恶意应用安装10亿多次, 恶意应用下载13亿次,拦截72亿次风险APP行为,高效保护用户隐私和安全。

12.jpeg

此外,手机应用违规收集个新信息、超范围收集个新信息、强制过度索取权限、欺骗误导强迫行为等困扰用户的问题,OPPO也给出了应对方案。OPPO智能护盾会在应用首次启动时结合应用类型和功能场景,为用户提供合理的授权建议,减少隐私数据泄漏风险;同时,在应用使用过程中,还将通过应用行为记录展示当前过度授权的应用与风险,引导用户一键优化,进一步保障安全性。

一直以来OPPO高度重视用户隐私和数据安全, 坚持"以用户为中心"的理念,积极践行“隐私守门人”使命,为每一个OPPO用户打造更便捷、更安心的用机环境。OPPO通过建立贯穿应用全流程的安全防护体系,提高APP开发者的个人信息保护水平,保障用户的信息安全,共同推动APP行业的健康发展,与用户一起展望更安全可靠的数智化生活。

收起阅读 »

Git提交错了,于是我把锅甩给了新来的baby

又是一遭悲惨的遭遇,git提交了一连串代码之后,发现提交错了。其实是把给老婆发的消息打到了comment里,然后还提交上去了。怎么办,这被看到岂不是要社死了。 一连串的研究之后,找到了几个解决方案。接下来我们一起搞搞这种错误提交的弥补方案。其中最离谱的是第三...
继续阅读 »

又是一遭悲惨的遭遇,git提交了一连串代码之后,发现提交错了。其实是把给老婆发的消息打到了comment里,然后还提交上去了。怎么办,这被看到岂不是要社死了。


image.png


一连串的研究之后,找到了几个解决方案。接下来我们一起搞搞这种错误提交的弥补方案。其中最离谱的是第三个方案。哈哈。


赛前准备


这里模拟一下这个操作,毕竟不能直接看我们的代码记录。我们新建一个项目,新建一个文件,起名001。


image.png


然后依次改为 002 003 004 005,每次都提交一次,在005的时候,执行异常提交。


最终我们得到一个005的文件


image.png


gitee上看是这样的


image.png


对于我们来说,现在是想删除这个异常提交,不仅删除代码,还想删除记录


也就是说,期待的是,文件变为004,而且这个提交记录删除掉。


方案1 交互式 rebase


首先我们尝试一下 git rebase -i HEAD~3,这样会取出最后的三条提交记录供我们编辑。


image.png


我们可以看到顶上有三条记录,这时候,我们删除这个异常的提交5


image.png


保存之后,会返回


git rebase -i HEAD~3
Successfully rebased and updated refs/heads/master.

这时候查看记录


image.png


异常提交已经没有了。


但是若是我们直接git push 会报错


image.png


告诉我们,我们当前的分支的版本是落后于远程分支的,不能提交。


这时候就需要git push --force这个命令,强制推送!!!


需要注意的是,强制推送会覆盖远程仓库中的历史记录,因此请确保你知道这个命令是个啥,并且有必要的话,需要通知团队其他成员协调好操作。


image.png


可以看到,git push --force 是可以成功的,而且再看gitee的记录


image.png


异常提交5已经不见了。并且本地的文件已经变为了004


image.png


其实在git rebase -i HEAD~3这个命令打开的交互框里是可以更改提交的顺序的,但是不能针对同一个文件的同一行,会冲突。


方案2 git reset


git reset 其实之前写文章讲过Git reset到底该如何使用,一文读懂系列 这次我们就直接为达目的,直接使用。
我们在上边的基础上,再提交一个异常提交5,使其恢复最初的情况。


image.png


然后gitee的情况:


image.png


这时候我们执行


git reset --hard HEAD~1

这个命令将删除最近的一个提交,包括提交所做的更改。请注意,这种方法可能会导致丢失未提交的更改,也就是说,本地写的没提交的代码就没了。所以请谨慎使用。


image.png


执行之后,我们可以看到异常提交5不见了


image.png


提交的时候也需要git push --force这个命令,强制推送!!!为啥每次都使用三个!!!呢,我只想告诉你,这个命令很恐怖,一定要慎之又慎。


这时候查看gitee记录


image.png


异常提交5没有了。


使用 git revert


还有小伙伴会说,为啥不用git revert呢,这不是git专门用来回滚代码的吗?


我们恢复异常提交005,再试试


image.png


我们执行 git revert f3d8db 并且 push


image.png


可以看到,文件是从005变为004了。但是从提交记录来看,不仅没有删除记录,还多了一条。其实,除非提交的注释特别社死,不然一般用的就是git revert,因为它不仅可以保存记录,还能确保版本是往前走的。


image.png


方案3 git filter-branch(谨慎使用)


查资料的时候,还看到一个这个命令,可以来一波骚的了。那既然提错了,把这锅甩给新人不就行了,哇咔咔咔咔咔。


git filter-branch --commit-filter '
if git log --format="%B" -n 1 $GIT_COMMIT | grep -q "异常提交"; then
GIT_AUTHOR_NAME="new baby";
GIT_COMMITTER_NAME="new baby";
git commit-tree "$@";
else
git commit-tree "$@";
fi'
-- --all

然后就是这样的


image.png


image.png


可以看到名字变了。当然邮箱也是可以改的。哇咔咔,这异常不就与我没关系了么。。。但是,极其不建议这么瞎折腾哈。


这个命令会根据条件重写整个历史。操作之前备份一下吧,别折腾坏了。而且一定先和其他的小伙伴商量一下,尤其是新人哈。


在此,就研究完毕了。正常来说使用第一种或者第二种方案都是可以的。不怕挨打的话,第三种方案也行。


git rebase 和 git reset 的区别



  • git rebase 命令用于将一个分支的提交移动到另一个分支上,或者重新应用一系列的提交。它的主要作用是改变提交的基础,即重新设置提交的起点。

  • git reset 命令用于修改当前分支的 HEAD 引用,或者用于撤销之前的提交操作。


也就是说git rebase 用于重新整理提交历史,而 git reset 用于调整当前分支的位置或撤销更改。关于这两个详细的使用,git reset已经写过了,有关git rebase的我会新开一篇文章,有关将一个分支的提交移动到另一个分支上这个操作虽不常用,但总有需要用到的时候。


作者:奔跑的毛球
来源:juejin.cn/post/7365414174217355314
收起阅读 »

在滴滴开发H5一年了,我遇到了这些问题

web
IOS圆角不生效 ios中使用border-radius配合overflow:hidden出现了失效的情况: 出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效 解决方法:在使用动画效果带transform的元...
继续阅读 »

IOS圆角不生效


ios中使用border-radius配合overflow:hidden出现了失效的情况:


image.png



出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效



解决方法:在使用动画效果带transform的元素的上一级div元素的css加上下面语句:


-webkit-transform:rotate(0deg);

IOS文本省略溢出问题


在部分ios手机上会出现以下情况:


image.png


原因


在目标元素上设置font-size = line-height,并加上以下单行省略代码:


.text-overflow {
display: -webkit-box;
overflow : hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}

或者:


.text-overflow {
overflow : hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

由于不同系统包含的字体的行高不一致,即使设置了height = line-height 一样会有以上问题


解决方案


经过测试,在height = line-height = font-szie的情况下,加上padding-top: 1px可以解决这个问题,即在需要使用单行省略的地方加上:


.demo {
height: 28px;
line-height: 28px;
font-size: 28px;
padding-top: 1px;
}

如:<div class="text-overflow demo">我是需要进行单行省略的文案</div>


安卓手机按钮点击后有橙色边框


image.png


解决方案:


button:focus {
outline: none;
}

优惠券打孔效果


需求中经常需要实现一类效果:优惠券打孔,如下图所示:


image.png


通常情况下会找设计采用图片的的形式,但这个方案最大的缺陷是无法适配背景的变化。
因此,我们可以采用如下方案,左右两侧各打一个孔,且穿透背景:


image.png


具体细节可以参考这篇文章:纯 CSS 实现优惠券透明圆形镂空打孔效果


Clipboard兼容性问题


navigator.clipboard兼容性不是很好,低版本浏览器不支持


image.png


解决方案:


const copyText = (text: string) => {
return new Promise(resolve => {
if (navigator.clipboard?.writeText) {
return resolve(navigator.clipboard.writeText(text))
}
// 创建输入框
const textarea = document.createElement('textarea')
document.body.appendChild(textarea)
// 隐藏此输入框
textarea.style.position = 'absolute'
textarea.style.clip = 'rect(0 0 0 0)'
// 赋值
textarea.value = text
// 选中
textarea.select()
// 复制
document.execCommand('copy', true)
textarea.remove()
return resolve(true)
})
}

Unocss打包后样式不生效


这个问题是由webpack缓存导致的,在vue.config.js中添加以下代码:


config.module.rule('vue').uses.delete('cache-loader')

具体原因见:UnoCSS webpack插件原理


低端机型options请求不过问题


在我们的业务需求中,覆盖的人群很广,涉及到的机型也很多。于是我们发现在部分低端机型下(oppo R11、R9等),有很多请求只有options请求,没有真正的业务请求。导致用户拿不到数据,报network error错误,我们的埋点数据也记录到了这一异常。


在我们的这个项目中,我们的后台有两个,一个提供物料,一个提供别的数据。但是奇怪的是,物料后台是可以正常获取数据,但业务后台就不行!


经过仔细对比二者发送的options请求,发现了问题所在:


image.png


发现二者主要存在以下差异:



  1. Access-Control-Allow-Headers: *

  2. Access-Control-Allow-origin: *


于是我便开始排查两个响应头的兼容性,发现在这些低端机型上,Access-Control-Allow-Headers: *确实会有问题,这些旧手机无法识别这个通配符,或者直接进行了忽略,导致options请求没过,自然就没有后续真正的请求了。


image.png


解决方案:由后台枚举前端需要的headers,在Access-Control-Allow-Headers中返回。


此外,将Access-Control-Allow-Origin设置为*也有一些别的限制:



参考



作者:WeilinerL
来源:juejin.cn/post/7372396174249459750
收起阅读 »

如果你要离开,就必须头也不回地离开!

再次重温《夜航西飞》时,里面的这段话更加让我觉得有智慧,甚至会觉得是整本书里最好的句子:如果必须离开你曾经住过、爱过、深埋着所有过往的地方,无论以何种方式,都不要慢慢离开,要决绝地离开,永远不回头。不要相信过去的时光才更好,它们已经消亡了。 只是有的话需要当...
继续阅读 »

再次重温《夜航西飞》时,里面的这段话更加让我觉得有智慧,甚至会觉得是整本书里最好的句子:如果必须离开你曾经住过、爱过、深埋着所有过往的地方,无论以何种方式,都不要慢慢离开,要决绝地离开,永远不回头。不要相信过去的时光才更好,它们已经消亡了。


图片


只是有的话需要当了一定的年龄,经历了一些事情之后才能对其有更加深刻的了解,并且能付诸行动。


我的烟龄已经十年了,戒烟对我来说是一件下了无数次决心的事,但是每次基本上都是以失败而告终,里面有一个搞笑和讽刺的点,就是每次下定决心戒烟的时候,我不会把现存的烟给丢掉,反而会进行自我暗示:抽完这包就不再抽了,呵呵!


这其实就像赌徒已经输得倾家荡产了,于是还在想着再赌一次。但是只要有一次,就会有第二次,无数次。


所以说,如果没有砸碎一切的决心,那么是不可能彻底走出的。


我这次下定决心戒烟是因为喉咙确实不太舒服,其次我是想好好当一个健康的人,所以前段时间,我抽完一支后,觉得不能再抽了,于是毫无留情将剩下的烟全部用水淋湿,丢进了垃圾桶。


在戒烟一段时间后,嗓子明显舒服了许多,昨天去见了一个朋友,他说我最近的气色好多了哎。我自己也感觉好了一点。无论从身体的舒适程度还是精神状态。


所以当我再回过头来阅读《夜航西飞》的时候,里面的这句话不由让我感叹感叹其力量。


我前段时间和一个妹子聊天的时候,她说自己谈了一段时间的男朋友,后面发现其出轨了,于是自然就分手了。但是到现在依然还在想着他。现在,我也想把开头的那句话送给你。


因为一个伤害了你的人,就不要再对其有任何留恋,做事情就别再拖泥带水,而且大概率在之前的感情中,你也只是一厢情愿,别人实际上根本不care,那这样的感情注定就是你输。


此时你还深陷其中干嘛?生活的美好处处都是,何不去感受美好的生活,头也不回地离开才是最理智的做法。


后面我愈发觉得,一个总是活在过去的人,是不值得拥有更好的东西的,总觉得过去的时光是美好的,但是再美好的昨天,它已经消亡了,而此刻,才是最美好的时光碎片。


就像此刻,六点起床后,我洗漱完毕,拉开窗帘,阳光洒在我的身上,我烧了一杯温水来喝,接着打开电脑来写下心里的想法,那么我就觉得此刻比过往的任何时刻都要美好,纵使我会回忆高中六点起床,然后走在绿树成荫的校园路上,再去操场上和同学笑着跑几圈,那虽然也是美好的时光,但是它已经消亡了,而此刻的时光是我能够牢牢抓在手里的!


那么过了今天,明天当我早起后,戴着耳机去附近的公园走上几圈后,我也会觉得昨天是美好的,但是我不会活在昨天,而是牢牢把握住今天!


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

前端命令行部署:再也不用把dist包给后端部署服务了!

web
好物推荐 超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~ 这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。 这两天发现了一个别人写的一个deploy...
继续阅读 »

好物推荐


超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~


这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。
这两天发现了一个别人写的一个deploy cli。感觉蛮好用的。分享一下。


希望可以帮助更多刚入行小伙伴了解更多前端玩法。


前端命令行部署


很多公司的前端部署流程都是先打一个dist包。然后给后端同事帮忙部署。


前端:::
1714281510854.png


后端:::


529ae5c36b03377bf116bafea2e95f1.png


(开玩笑的,工作中的后端同事都没那么调皮)


本文的内容就是如何使用命令行进行前端自动部署。


我们整个网站的读取,其实就是我们上传一个静态的文件包到服务器,然后服务器上的后台服务读取我们的静态包,来进行页面的展示。所以,前端自动化部署的关键,就是,能把dist包传到服务器的指定目录下就OK了。


部署流程


推荐一个deploy cli工具(deploy-cli-service)


安装


  1. 执行 npm install deploy-cli-service -g 进行全局安装 。

  2. 执行 deploy-cli-service - v 查看版本


初始化配置文件

在项目根目录执行 deploy-cli-service init 进行初始化


deploy-cli-service init命令执行后项目目录下会出现一个名为deploy.config的文件


image.png


deploy-cli-service init初始化的内容会被默认输入到 deploy.config


修改配置文件

deploy-cli-service init初始化之后输入的内容都会默认被写入deploy.config文件中。


image.png


然后看看相关的属性有没有什么需要修改的就ok。


配置部署命令


image.png


"deploy:test": "deploy-cli-service deploy --mode test"," 写入到 package.json中的script里。


然后在命令行执行 "npm run deploy:test"


成功部署后会如下显示


image.png


image.png


注意


配置 deploy.config.js时尽量使用ssh证书登录,不要使用服务器密码,把服务器密码写在前端代码里是一件非常不好的操作。


deploy-cli-service npm地址


luck


作者:工边页字
来源:juejin.cn/post/7362924623825256463
收起阅读 »

互联网+《周易》:我在github学算卦

web
前言 《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。 像这种千古奇书,每个中国人都应该读一读,一是因...
继续阅读 »

前言


《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。


像这种千古奇书,每个中国人都应该读一读,一是因为这是老祖宗的智慧,我们不能丢弃;二是因为《周易》蕴含宇宙人文的运行规律,浅读可修身养性,熟读可明自我,深究可知未来,参透就可知天命了。


东汉著名史学家、文学家班固在《汉书•艺文志》中提出《周易》的成书是:人更三圣,世历三古


那么在哪里才可以读到呢?


其实易经的完本在网上随便就可以找到,但是都不适合在摸鱼的时候读 (!🤡),打开花花绿绿或者神神叨叨的小网站,你的 leader 肯定一眼就看出你在摸鱼。


既然没有这种网站,那干脆自己做一个。


vitePress + github pages 快速搭建


vitePress 快速开始


pnpm add -D vitepress

pnpm vitepress init

填写完 cli 里的几个问题,项目就可以直接运行了。可以看到网站直接解析了几个 示例的 md 文件,非常的神奇。


处理《周易》文本


那么哪里才可以找到《周易》的 markdown 版本呢,找了一圈也没有找到,最后找到了一个 txt 的,我觉得写个脚本转换一下。


首先,我拿 vscode 的正则给每个标题加上井号,使其成为一级标题


QQ2024511-183935.webp


此时,所有的标题都被改成了md格式的一级标题,然后直接将后缀名从 .txt 改为 .md 即可。


看过 vitepress 的文档并经过实操后发现,它的目录是一个一个的小 markdown 文件组成的,而单个 markdown 内的标题等在右侧显示


image.png


那么此时就需要把《周易》完本,按照六十四卦分为六十四个 md 文件。


我写了一个node脚本:


const fs = require('fs');

// 读取zhouyi.md文件
fs.readFile('zhouyi.md', 'utf8', (err, data) => {
 if (err) {
   console.error('读取文件出错:', err);
   return;
}

 // 按一级标题进行分割
 const sections = data.split('\n# ');

 // 循环处理每个一级标题的内容
 sections.forEach((section, index) => {
   // 提取标题和内容
   const lines = section.split('\n');
   const title = lines[0];
   const content = lines.slice(1).join('\n');

   // 写入到单独的文件中
   const fileName = `zhouyi_${index + 1}.md`;
   fs.writeFile(fileName, `# ${title}\n\n${content}`, err => {
     if (err) {
       console.error(`写入文件 ${fileName} 出错:`, err);
    } else {
       console.log(`已创建文件: ${fileName}`);
    }
  });
});
});


取名为md-slicer.js ,在控制台输入


node md-slicer.js

即可生成


image.png


然后写一个在 .vitepress/config.mtssidebar的生成函数:


let itemsLength = 64
function getSidebar() {
 let items: {}[] = [{
   text: '《周易》是什么?',
   link: '/what.md'
}]
 for (let i = 1; i <= itemsLength; i++) {
   items.push({ text: `第${numberToChinese(i)}卦`, link: `/zhouyi_${i}.md` })
}
 return items
}

numberToChinese函数用来将阿拉伯数字转为中文数字,因为周易只有六十四卦,所以不用考虑很多,够用即可


// numberToChinese
function numberToChinese(number) {
 const chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
 const chineseUnits = ['', '十', '百', '千', '万', '亿'];

 // 将数字转换为字符串,以便于处理每一位
 const numStr = String(number);

 let result = '';
 let zeroFlag = false; // 用于标记是否需要加上“零”

 for (let i = 0; i < numStr.length; i++) {
   const digit = parseInt(numStr[i]); // 当前位的数字
   const unit = chineseUnits[numStr.length - i - 1]; // 当前位的单位

   if (digit !== 0) {
     if (zeroFlag) {
       result += chineseNumbers[0]; // 如果前一位是零,则在当前位加上“零”
       zeroFlag = false;
    }
     result += chineseNumbers[digit] == "一" && unit == "十" ? unit : chineseNumbers[digit] + unit; // 加上当前位的数字和单位,当一十时,省略前面的一
  } else {
     zeroFlag = true; // 如果当前位是零,则标记为需要加上“零”
  }
}
 return result;
}

然后,设置一下vitepress基础配置和打包输出路径


export default defineConfig({
 title: "周易",
 description: "周易",
 base: "/thebookofchanges/",
 head: [
  ['link', { rel: 'icon', href: 'yi.svg' }] // 这里是你的 Logo 图片路径
],
 outDir: 'docs', // 输出到docs ,可以直接在 github pages 使用
 themeConfig: {
   // https://vitepress.dev/reference/default-theme-config
   nav: [
    { text: '首页', link: '/' },
    { text: '阅读', link: '/zhouyi_1.md' }
  ],
   logo: '/yi.svg',
   sidebar: [
    {
       text: '目录',
       items: getSidebar()
    }
  ],

   socialLinks: [
    { icon: 'github', link: 'https://github.com/LarryZhu-dev/thebookofchanges' }
  ]
}
})


然后简单给网站设计一个logo


image.png


字体是华文隶书,转化为路径后,将它拉瘦一点,再导出为 svg。


最后,用 pnpm run docs:build打包即可,打包时注意设置基本路径为 github pages 的仓库名。


发布


push到github后,在 Setting/Pages 页面发布即可。


image.png


效果预览


最后,网站运行在:larryzhu-dev.github.io/thebookofch…


image.png


image.png


仓库地址:github.com/LarryZhu-de… 来点star🤣


结语


现在只有简单的原文,如有 《周易》大佬,欢迎大佬提交注解PR。


作者:德莱厄斯
来源:juejin.cn/post/7367659849101312015
收起阅读 »

gpt-4o这些玩法真的太逆天了

OpenAI在近期发布了GPT-4系列的新模型GPT-4o。这一更新主要聚焦于多模态和端侧应用,为用户提供了全新的交互体验。 GPT-4o作为OpenAI的新模型,具有三大显著特点: 多模态:GPT-4o能够接受文本、音频、图像作为组合输入,并生成任何文本、音...
继续阅读 »

OpenAI在近期发布了GPT-4系列的新模型GPT-4o。这一更新主要聚焦于多模态和端侧应用,为用户提供了全新的交互体验。


GPT-4o作为OpenAI的新模型,具有三大显著特点:


多模态:GPT-4o能够接受文本、音频、图像作为组合输入,并生成任何文本、音频和图像的组合输出。这种多模态的理解能力让GPT-4o在处理复杂任务时更具优势,如识别人类的感情并根据感情做出“有感情的反应”。


几乎无延迟:GPT-4o对音频输入的响应时间最短为232毫秒,平均为320毫秒,这与人类在对话中的响应时间相似。这种极快的响应速度使得GPT-4o能够实时地与用户进行交互,提供流畅的用户体验。


可在电脑桌面运行:OpenAI还将与苹果合作推出了适用于macOS的ChatGPT桌面级应用。这一应用允许用户在没有网络的情况下使用ChatGPT,并且可以在本地设备上处理敏感信息,保护用户隐私。


一些逆天的视频展示


下面来一起了解一下它官网的一些视频展示的逆天操作:


第一个王炸,作业辅导


作业辅导


视频中展示的是巨佬在使用 GPT-4o 对他儿子进行作业辅导。它开始就告诉gpt-4o 说不要直接说出答案,而是帮助它一步一步解决这个几何题目,我们在视频中可以看到,的确是这样,gpt-4o 一步一步的帮助他儿子解决了这个问题,而且还是非常细致的解释,并且是非常有情感的,每当他儿子完成一步之后,gpt-4o 从语气上都会有一种更进一步的感觉,这种情感化的交互方式,让人感觉非常的亲切。


而且,所有的过程都是这个娃在拿着笔在一步一步的解决这个几何题目,gpt-4o 就是看着这个娃做的解题过程,它会判断这个娃是否步骤对了,这个交互简直太赞了!这明显得益于GPT-4o的图像理解能力的增强。


作业辅导


讲真,按照这个趋势,教培行业似乎极有可能被干掉,那些不会做奥数题的家长,有福了,因为 安特曼说,gpt-4o 是会免费的。这意味着,你不需要花费一分钱,就可以请一个专业的教培老师,帮助你的孩子解决问题。


第二个王炸,精神分裂,一个端中两个 gpt-4o 互动起来了


之前我们于gpt 的实时语音对话只能是一对一,好了,颠覆认知的时刻来了,你在一个对话窗口中,可以同时存在两个gpt-4o对话,甚至,它两还可以互相对话,这个视频中,这两 gpt-4o 相互唱起了小曲。。。
两个 gpt-4o 协调


外语学习


外语学习


在这个例子中,研究人员展示的是,它告诉 gpt-4o 它想学习西班牙语言,当然它使用英语说的,然后它使用摄像头对着苹果和香蕉,问gpt-4o这个是什么,gpt-4o 利用它图像识别的能力,认出了香蕉和苹果,然后告诉研究人员。


但是!但是!但是!它回的语言居然是英语和西班牙语的混合,也就是,gpt-4o 回答,this is manzana and plátano。差点没有惊掉我的下巴,一句回答中包含了多种语言。这中组合输出的能力,简直太强了。


参与多人对话中来


图 4


这个视频展示的是 gpt-4o 加入到了一个在线会议中,它可以看到共享的屏幕,因此它知道会议有多少个人,然后开始是每个人说了一下自己的喜欢的人和事,接着主持人发文,他们各自有哪些爱好,gpt-4o 一一都回答出来了,而且是非常的准确,最后还来了一个总结,后面腾讯会议,zoom 估计交互得更上啊,不加入一个智能记录员,这体验就得甩开好几条街了。


同声传译


同声传译


这个视频展示的是 gpt-4o 扮演的事一个翻译者的角色,画面中的两个人一个人是将英语的,一个人是将西班牙语的,gpt-4o 就负责把听到的英语转化为西班牙语,把西班牙语转换为英语,然后两个哥们就愉快的对话了,你说你的西班牙,我说我的英语,我们都听得很懂的,所以,同声传译这个行业,是不是也要凉凉了。


外婆的澎湖湾


催命曲


歪日哦,富有情感的和你对话是王炸的话,和这个对比简直小巫见大巫,它哼起了小区,而且还会偶尔和你聊天的时候爽朗的发出笑声,这种情感化的交互方式,让人感觉非常的亲切。当这个老外说它想睡觉,哼个小曲,gpt-4o 就开始哼起了外婆的澎湖湾,听得我差点给睡着了...这种情感化的交互方式,让人感觉非常的亲切。


语速控制


语速控制


在这个视频中,老外让 gpt-4o 数数,1,2,3,。。。10. gpt-4o 一口气说完了,然后老外说,你能不能慢一点,gpt-4o 就慢慢的说了一遍,然后老外说,你能不能快一点,gpt-4o 就快速的说了一遍,这种语速控制就完全可以用来训练自己的听力了,这个功能比较赞,不过,我的下巴还在。


开玩笑


开玩笑


这个视频中,老外给 gpt-4o 说它要给它老爸讲个笑话,然后他想让 gpt-4o 先听听它这个笑话是不是好笑,结果,gpt-4o 真的爽朗得笑了,笑得一点都不像机器人,听到它这个笑声的时候,我的下巴还差那么一点就掉了。。。


你是我的眼


你是我的眼


这个视频的效果也是相当的炸裂,视频中时候一个盲人,很显然他看不见,因此它所到之处,让 gpt-4o 告诉它周围都有写什么风景,gpt-4o 一一告诉他,从这个视频中,我有点小小的启发!


gpt-4o 可能更好的交互是类似 Google Glass的形式,这样的产品出来,估计全世界的盲人都要为之震撼,他们都将会重见光明,这个产品的价值,简直不可估量。


两个 gpt-4o 互相对话


两个 gpt-4o 互相对话


两个 gpt-4o 互相对话


这个视频中显示了两个gpt-4o 开始了对话,什么,永动机???实际上不是,是视频中 openai 的大佬先告诉一个 gpt-4o 说等会有个可以看见世界的 AI 会和你对话,你可以和他交流,随后它启动了另外一个 gpt-4o,然后两个 gpt-4o 开始对话了,并且大佬还可以随时打断加入他们的对话。我的脑袋已经开始疼了,这个视频太炸裂了。这意味着,我是不是可以搞 3 个手机,搞一桌四川麻将了???


着装建议


着装建议


视频中,这个大佬要准备面试了,问 gpt-4o 怎么穿着得体,然后 gpt-4o 告诉他带个帽子试试,结果带上 gpt-4o 就开始爽朗的笑了。。。,嗯,后面穿什么出门,估计可以让 gpt-4o 建议建议合不合适。。。


桌游助手


桌游助手


这个场景是两个人想玩石头剪刀布的游戏,然后让 gpt-4o 做裁判,然后就开始了,gpt-4o 说 1,2,3,亮出你们的爪子,然后判断谁输谁赢,好了,我似乎又找到了一个乐子。




作者:brzhang
来源:juejin.cn/post/7369481217030438921
收起阅读 »

7个Js async/await高级用法

web
7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
继续阅读 »

7个Js async/await高级用法


JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


1. async/await与高阶函数


当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


// 异步过滤函数
async function asyncFilter(array, predicate) {
const results = await Promise.all(array.map(predicate));

return array.filter((_value, index) => results[index]);
}

// 示例
async function isOddNumber(n) {
await delay(100); // 模拟异步操作
return n % 2 !== 0;
}

async function filterOddNumbers(numbers) {
return asyncFilter(numbers, isOddNumber);
}

filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

2. 控制并发数


在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


async function asyncPool(poolLimit, array, iteratorFn) {
const result = [];
const executing = [];

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
result.push(p);

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}

return Promise.all(result);
}

// 示例
async function uploadFile(file) {
// 文件上传逻辑
}

async function limitedFileUpload(files) {
return asyncPool(3, files, uploadFile);
}

3. 使用async/await优化递归


递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


// 异步递归函数
async function asyncRecursiveSearch(nodes) {
for (const node of nodes) {
await asyncProcess(node);
if (node.children) {
await asyncRecursiveSearch(node.children);
}
}
}

// 示例
async function asyncProcess(node) {
// 对节点进行异步处理逻辑
}

4. 异步初始化类实例


在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


class Example {
constructor(data) {
this.data = data;
}

static async create() {
const data = await fetchData(); // 异步获取数据
return new Example(data);
}
}

// 使用方式
Example.create().then((exampleInstance) => {
// 使用异步初始化的类实例
});

5. 在async函数中使用await链式调用


使用await可以直观地按顺序执行链式调用中的异步操作。


class ApiClient {
constructor() {
this.value = null;
}

async firstMethod() {
this.value = await fetch('/first-url').then(r => r.json());
return this;
}

async secondMethod() {
this.value = await fetch('/second-url').then(r => r.json());
return this;
}
}

// 使用方式
const client = new ApiClient();
const result = await client.firstMethod().then(c => c.secondMethod());

6. 结合async/await和事件循环


使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


// 异步定时器函数
async function asyncSetTimeout(fn, ms) {
await new Promise(resolve => setTimeout(resolve, ms));
fn();
}

// 示例
asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

7. 使用async/await简化错误处理


错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


async function asyncOperation() {
try {
const result = await mightFailOperation();
return result;
} catch (error) {
handleAsyncError(error);
}
}

async function mightFailOperation() {
// 有可能失败的异步操作
}

function handleAsyncError(error) {
// 错误处理逻辑
}

通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


作者:慕仲卿
来源:juejin.cn/post/7311603994928513076
收起阅读 »

Google 如果把 Go 团队给裁了会怎么样?

大家好,我是煎鱼。 节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。 据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心...
继续阅读 »

大家好,我是煎鱼。


节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。


据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心成员。


如下图所示:



此时引发了国内外社区一个较大的担忧,如果 Google 如法炮制,要放弃 Go 核心团队。会发生什么事,会不会有什么问题?



现在有什么


先知道可能会失去什么,那得先盘点一下 Go 这一门编程语言和 Go 核心团队在 Google 获得了什么。


根据我们以往对 @Lance Taylor 所澄清以及各处的描述,可以估算 Go 在 Google 大概获得了什么。


其至少包含以下内容:



  1. 工作岗位:Go 核心团队相关成员的工作岗位,包含薪资、福利等各种薪酬内容。

  2. 软硬件资源:Go 相关的软硬件资源(例如:知识产权、服务器、域名、模块管理镜像)等网上冲浪所需信息。

  3. 线下活动:Go 世界各地部分大会的开展可能会变少,或缩减规模(资金、背书等)。

  4. 大厂内部资源:因为失去 Google 内部的资源,可能逐步失去一些先进项目的熏陶和引入使用 Go 这一门编程语言的机会。

  5. 推广和反馈渠道:Go 一些显著问题和特性的发现、响应,可能会变慢。因为 Go 对于 Google 内部的问题处理和特性需要,历史上来看都是按最高优先级处理。


可能会发生什么事


如果真的一刀切,Google 把 Go 核心团队干没了,基础设施全部都不提供了。


大家普遍认为,会出现如下几种情况:



  1. 如果 Go 团队中的很多人被裁员,他们会另谋高就。各散东西。维护积极性和组织性会大幅下降。

  2. 如果 Google 决定完全停止对 Go 的投资,Go 的维护可能会变得更加复杂,因为它需要运行大量的基础设施。在这种情况下,可能会出现 Go 由 Google 转移到一个外部的基金会,会有明显的阶段性维护波动。

  3. 如果 Google 选择在内部其他团队对 Go 继续投入,较差的情况是 Google 会灵活运用他们对知识产权的所有权 --Go 很可能会更名为其他东西。


基金会方面,另外大家认为最有可能接受 Go 的基金会是:CNCF,因为 Go 项目在 CNCF 中基于数量来讲是最大的。


如下图部分所示:



同时 CNCF 和 Go 的云原生属性最为强烈,契合度非常高。


参考 Rust 发展史


@azuled 根据 Rust 的发展历史,给出了自己的一些见解。如下所表述:


1、Rust 被踢出 Mozilla 核心,成为一个独立的基金会,但它仍然存活了下来。事实上,它后来可能做得更好。


2、我认为很有可能围绕 Go 成立一个非营利组织,而且很有可能有足够多的大公司使用它来支持它,至少在一段时间内是这样。


总结


在目前这个大行情下,Go 作为 Google Cloud 团队的一员,和云原生的故事捆绑在一起。如果 Google 业绩出现波动,或者要继续降本增效。


这类没有直接营收的基础部门或团队还是比较危险的,因为其会在企业中根据利润中心、成本中心进行分摊和计算人效成本等。


如果真的强硬切割,势必会对 Go 这门编程语言产生阶段性的冲击。但未来是好是坏,就不好说了。



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

Spring Boot 3 集成 Jasypt详解

随着信息安全的日益受到重视,加密敏感数据在应用程序中变得越来越重要。Jasypt(Java Simplified Encryption)作为一个简化Java应用程序中数据加密的工具,为开发者提供了一种便捷而灵活的加密解决方案。本文将深入解析Jasypt的工作原...
继续阅读 »

随着信息安全的日益受到重视,加密敏感数据在应用程序中变得越来越重要。Jasypt(Java Simplified Encryption)作为一个简化Java应用程序中数据加密的工具,为开发者提供了一种便捷而灵活的加密解决方案。本文将深入解析Jasypt的工作原理,以及如何在Spring Boot项目中集成和使用Jasypt来保护敏感信息。


springboot-jasypt.jpg


springboot-jasypt.jpg


Jasypt简介


Jasypt(Java Simplified Encryption)是一个专注于简化Java加密操作的工具。它提供了一种简单而强大的方式来处理数据的加密和解密,使开发者能够轻松地保护应用程序中的敏感信息,如数据库密码、API密钥等。


Jasypt的设计理念是简化加密操作,使其对开发者更加友好。它采用密码学强度的加密算法,支持多种加密算法,从而平衡了性能和安全性。其中,Jasypt的核心思想之一是基于密码的加密(Password Based Encryption,PBE),通过用户提供的密码生成加密密钥,然后使用该密钥对数据进行加密和解密。


该工具还引入了盐(Salt)的概念,通过添加随机生成的盐值,提高了加密的安全性,防止相同的原始数据在不同的加密过程中产生相同的结果,有效抵御彩虹表攻击。


Jasypt与Spring Boot天然契合,可以轻松集成到Spring Boot项目中,为开发者提供了更便捷的数据安全解决方案。通过Jasypt,开发者可以在不深入了解底层加密算法的情况下,轻松实现数据的安全保护,使得应用程序更加可靠和安全。


官网地址: http://www.jasypt.org/


github地址: github.com/ulisesbocch…


Spring Boot 3 集成 Jasypt


添加依赖


在pom文件中添加一下依赖


<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot</artifactId>
<version>3.0.5</version>
</dependency>

添加配置文件


未指定前后缀的话默认格式ENC()括号里面是加密后的密文 然后实现自动解密


spring:
# 数据源配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.10.106:3306/xj_doc?characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: ENC(BLC3UQBxshlcA9tnMyJL7w==)

# 加密配置
jasypt:
encryptor:
# 指定加密密钥,生产环境请放到启动参数里面
password: 0f7b0a5d-46bc-40fd-b8ed-3181d21d644f
# 指定解密算法,需要和加密时使用的算法一致
algorithm: PBEWithMD5AndDES

iv-generator-classname: org.jasypt.iv.NoIvGenerator

# property:
# # 算法识别的前后缀,默认ENC(),包含在前后缀的加密信息,会使用指定算法解密
# prefix: ENC@[
# suffix: ]

启动类添加注解


在启动类上添加注解@EnableEncryptableProperties注解来开启自动解密


@SpringBootApplication
@MapperScan("cn.xj.xjdoc.**.mapper")
@EnableEncryptableProperties //开启自动解密功能
public class XjdocApplication {
public static void main(String[] args) {
SpringApplication.run(XjdocApplication.class, args);
}
}

测试类


public class JasyptUtil {

public static void main(String[] args){
StandardPBEStringEncryptor standardPBEStringEncryptor =new StandardPBEStringEncryptor();
/*配置文件中配置如下的算法*/
standardPBEStringEncryptor.setAlgorithm("PBEWithMD5AndDES");
/*配置文件中配置的password*/
standardPBEStringEncryptor.setPassword("0f7b0a5d-46bc-40fd-b8ed-3181d21d644f");
//加密
String jasyptPasswordEN =standardPBEStringEncryptor.encrypt("xj2022");
//解密
String jasyptPasswordDE =standardPBEStringEncryptor.decrypt(jasyptPasswordEN);
System.out.println("加密后密码:"+jasyptPasswordEN);
System.out.println("解密后密码:"+jasyptPasswordDE);
}
}

生产环境安全处理


jasypt的password值放在配置文件中在生产环境中是不安全的,我们可以将password值放到启动命令中,删除配置文件中password 的配置行,启动命令如下所示:


java -Djasypt.encryptor.password=password -jar jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar

或者


java -jar jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar --jasypt.encryptor.password=password

总结


Jasypt作为一个简单而强大的加密工具,为Java应用程序提供了便捷的数据保护方案。通过与Spring Boot的集成,开发者可以在应用程序中轻松地加密和解密敏感信息。在实际项目中,选择合适的加密方式、安全存储密码以及与Spring Security等安全框架的集成,都是保障应用程序安全的关键步骤。希望本文能够帮助读者更深入地了解Jasypt,并在实际项目中合理地运用加密技术。


作者:修己xj
来源:juejin.cn/post/7318616887415717924
收起阅读 »

Springboot3 + SpringSecurity + JWT + OpenApi3 实现认证授权

Springboot3 + SpringSecurity + JWT + OpenApi3 实现双token 目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供...
继续阅读 »

Springboot3 + SpringSecurity + JWT + OpenApi3 实现双token


目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供大家学习和项目实战使用,创作不易,转载请注明出处!


该项目使用目前最新的 Sprin Boot3 版本,采用目前市面上最主流的 JWT 认证方式,实现双token刷新。



温馨提示:SpringBoot3 版本必须要使用 JDK11 或 JDK19



SpringBoot3 新特性


Spring Boot3 是一个非常重要的版本,将会面临一个新的发展征程!Sprin Boot 3.0 包含了 12 个月以来,151 个人的 5700+ 次 commit 的贡献。这是自 4 年半前发布的 2.0 版本以来的第一次重大修订,这也是第一个支持 Spring Framework 6.0 和 GraaIVM 的 Spring Boot GA 版本。


Spring Boot 3.0 新版本的主要亮点:



  1. 最低要求为 Java 17 ,兼容 Java 19

  2. 支持用 GraalVM 生成原生镜像,代替了 Spring Native

  3. 通过 Micrometer 和 Micrometer 追踪提高应用可观察性

  4. 支持具有 EE 9 baseline 的 Jakarta EE 10


为什么采用双 Token刷新?


**场景假设:**星期四小金上班的时候摸鱼,准备在某APP 上面追剧,已经深深的陷入了角色中无法自拔,此时如果 Token 过期了 ,小金就不得不重新返回登录界面,重新进行登录,那么这样小金的一次完整的追剧体验就被打断了,这种设计带给小金的体验并不好,于是就需要使用双 Token 来解决。


**如何使用:**在小金首次登陆 APP 时,APP 会返回两个 Token 给小金,一个 accessToken,一个 refreshToken,其中 accessToken 的过期时间比较短,refreshToken 的时间比较长。当 accessToken 失效后,会通过 refreshToken 去重新获取 accessToken,这样一来就可以在不被察觉的情况下仍然使小金保持登录状态,让小金误以为自己一直是登录的状态。并且每次使用refreshToken 后会刷新,每一次刷新后的 refreshToken 都是不相同的。


**优势说明:**小金能够有一次完整的追剧体验,除非摸鱼时被老板发现了。accessToken 的存在,保证了登录的正常验证,因为 accessToken 的过期时间比较短,所以也可以保证账号的安全性。refreshToken 的存在,保证了小金无需在短时间内反复的登录来保持 Token 的有效性,同时也保证了活跃用户的登录状态可以一直延续而不需要重新登录,反复刷新也防止了某些不怀好意的人获取 refreshToken 后对用户账号进行不良操作。


一图胜千言:


image-20230604084837740


项目准备


项目采用 Spring Boot 3 + Spring Security + JWT + MyBatis-Plus + Lombok 进行搭建。


创建数据库


user 表


image-20230603220205094


token 表


在实际中应该把 token 信息保存到 redis


image-20230603220333914


创建 Spring Boot 项目


创建一个 Spring Boot 3 项目,一定要选择 Java 17 或者 Java 19


引入依赖


<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
<version>3.0.4version>
dependency>

<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.11.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.11.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.11.5version>
dependency>

编写配置文件


server:
port: 8417
spring:
application:
name: Spring Boot 3 + Spring Security + JWT + OpenAPI3
datasource:
url: jdbc:mysql://localhost:3306/w_admin
username: root
password: jcjl417
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
table-prefix: t_
id-type: auto
type-aliases-package: com.record.security.entity
mapper-locations: classpath:mapper/*.xml
application:
security:
jwt:
secret-key: VUhJT0pJT0hVWUlHRFVGVFdPSVJISVVHWUZHVkRVR0RISVVIREJZI1VJSEZTVUdZR0ZTVVk=
expiration: 86400000 # 1天
refresh-token:
expiration: 604800000 # 7 天
springdoc:
swagger-ui:
path: /docs.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs

项目实现


准备项目所需要的一系列代码,如 entity、controller 、service、mapper 等


系统角色 Role


定义一个角色(Role)枚举,详细代码参考文章结尾处的项目源码


public enum Role {

// 用户
USER(Collections.emptySet()),
// 一线人员
CHASER( ... ),
// 部门主管
SUPERVISOR( ... ),
// 系统管理员
ADMIN( ... ),
;

@Getter
private final Set permissions;

public List getAuthorities() {
var authorities = getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
}

User 实现 UserDetails


温馨提示:


由于 Spring Security 源码设计的时候 ,将用户名和密码属性定义为 username 和 password,所以我们看到的大部分教程都会遵循源码中的方式,习惯性的将用户名定义为 username,密码定义为 password。


其实我们大可不必遵守这个规则,在我的系统中使用邮箱登录,也即是将邮箱(email)作为 Security 中的用户名(username),那么我必须要将用户输入的 email 作为 username 来存放,这会使我感到非常的不适,因为我的系统中正真的 username 将会 用另外一个单词来命名。


如何避免登录时的字段必须设置为 username 和 password 呢?



重写 getter方法, 只有你的系统中登录的用户名和密码属性不是 username 和 password 的情况下 ,你进行重写才会看到下面红色框中的提示。


202306032035283

重写 username 和 password 的 getter方法


@Override
public String getUsername() {
return email;
}

@Override
public String getPassword() {
return password;
}

Security 配置文件



需要注意的是 WebSecurityConfigurerAdapter 在 Spring Security 中已经被弃用和移除


下面将采用新的配置文件



@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {

private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
private final RestAuthorizationEntryPoint restAuthorizationEntryPoint;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers(
"/api/v1/auth/**",
"/api/v1/test/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/doc.html",
"/webjars/**",
"/swagger-ui.html",
"/favicon.ico"
).permitAll()
.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())

.requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
.requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
.requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())

.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())

.requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
.requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
.requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
//添加jwt 登录授权过滤器
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout()
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())

;
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);

return http.build();
}
}

OpenApi 配置文件


OpenApi 依赖


<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-starter-webmvc-uiartifactId>
<version>2.1.0version>
dependency>

OpenApiConfig 配置


OpenApi3 生成接口文档,主要配置如下



  • Api Gr0up(分组)

  • Bearer Authorization(认证)

  • Customer(自定义请求头等)


@Configuration
public class OpenApiConfig {

@Bean
public OpenAPI customOpenAPI(){
return new OpenAPI()
.info(info())
.externalDocs(externalDocs())
.components(components())
.addSecurityItem(securityRequirement())
;
}

private Info info(){
return new Info()
.title("京茶吉鹿的 Demo")
.version("v0.0.1")
.description("Spring Boot 3 + Spring Security + JWT + OpenAPI3")
.license(new License()
.name("Apache 2.0") // The Apache License, Version 2.0
.url("https://www.apache.org/licenses/LICENSE-2.0.html"))
.contact(new Contact()
.name("京茶吉鹿")
.url("http://localost:8417")
.email("jc.top@qq.com"))
.termsOfService("http://localhost:8417")
;
}

private ExternalDocumentation externalDocs() {
return new ExternalDocumentation()
.description("京茶吉鹿的开放文档")
.url("http://localhost:8417/docs");
}

private Components components(){
return new Components()
.addSecuritySchemes("Bearer Authorization",
new SecurityScheme()
.name("Bearer 认证")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
)
.addSecuritySchemes("Basic Authorization",
new SecurityScheme()
.name("Basic 认证")
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
)
;

}

private SecurityRequirement securityRequirement() {
return new SecurityRequirement()
.addList("Bearer Authorization");
}

private List security(Components components) {
return components.getSecuritySchemes()
.keySet()
.stream()
.map(k -> new SecurityRequirement().addList(k))
.collect(Collectors.toList());
}


/**
* 通用接口
*
@return
*/

@Bean
public Gr0upedOpenApi publicApi(){
return Gr0upedOpenApi.builder()
.group("身份认证")
.pathsToMatch("/api/v1/auth/**")
// 为指定组设置请求头
// .addOperationCustomizer(operationCustomizer())
.build();
}

/**
* 一线人员
*
@return
*/

@Bean
public Gr0upedOpenApi chaserApi(){
return Gr0upedOpenApi.builder()
.group("一线人员")
.pathsToMatch("/api/v1/chaser/**",
"/api/v1/experience/search/**",
"/api/v1/log/**",
"/api/v1/contact/**",
"/api/v1/admin/user/update")
.pathsToExclude("/api/v1/experience/search/id")
.build();
}

/**
* 部门主管
*
@return
*/

@Bean
public Gr0upedOpenApi supervisorApi(){
return Gr0upedOpenApi.builder()
.group("部门主管")
.pathsToMatch("/api/v1/supervisor/**",
"/api/v1/experience/**",
"/api/v1/schedule/**",
"/api/v1/contact/**",
"/api/v1/admin/user/update")
.build();
}

/**
* 系统管理员
*
@return
*/

@Bean
public Gr0upedOpenApi adminApi(){
return Gr0upedOpenApi.builder()
.group("系统管理员")
.pathsToMatch("/api/v1/admin/**")
// .addOpenApiCustomiser(openApi -> openApi.info(new Info().title("京茶吉鹿接口—Admin")))
.build();
}
}

image-20230603224928028


Security 接口赋权的方式


hasRole及hasAuthority的区别?



hasAuthority能通过的身份必须与字符串一模一样,而hasRole能通过的身前缀必须带有ROLE_,同时可以通过两种字符串,一是带有前缀ROLE_,二是不带前缀ROLE_



通过配置文件


在配置文件中指明访问路径的权限


.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())
.requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
.requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
.requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())


.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())
.requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
.requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
.requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

通过注解


@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "系统管理员权限测试")
public class AdminController {

@GetMapping
@PreAuthorize("hasAuthority('admin:read')")
public String get() {
return "GET |==| AdminController";
}


@PostMapping
@PreAuthorize("hasAuthority('admin:create')")
public String post() {
return "POST |==| AdminController";
}
}

测试


我们登录认证成功后,系统会为我们返回 access_token 和 refresh_token。


image-20230604082145598




作者:京茶吉鹿
来源:juejin.cn/post/7241399184594993208
收起阅读 »