注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

IT外传:大家都满意

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑是一家IT公司的程序员,岗位是Python开发,负责处理并分析一些科学数据。 这一天,老郑在写代码,写一段如何读取.txt文件的代码。说来也奇怪,web前端从浏览器上传.txt文件给J...
继续阅读 »

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。



老郑是一家IT公司的程序员,岗位是Python开发,负责处理并分析一些科学数据。


这一天,老郑在写代码,写一段如何读取.txt文件的代码。说来也奇怪,web前端从浏览器上传.txt文件给Java后端,Java收到文件后,读取内容,然后按照规则筛选一下字符串,再返回给前端展示,这也就结束了。


但是Java同事对这个任务,先是调研了一周,然后告诉主管,这个功能很难实现。主管一听,非常重视,他担心开发流程受到影响,连忙问该怎么办?Java人员说,建议用Python来做。他不但提出问题,也给出了解决方案。他说自己百度上搜了,采用Python是可以实现的。


主管将信将疑地去问老郑,如果用Python读取.txt文件,难不难?


老郑先是一怔,他心想用什么语言读取.txt都不难啊!老郑说不难,很简单。


此时,主管心里瞬间满天晴朗:原来Java说的是真的,用Python果然不难。


主管问:做这个功能……需要几天?


老郑回答说,单纯读文件很简单,关键是读取之后做什么处理啊?


主管说就是找到几个关键词,把关键词替换成*号就行了。


老郑说,3个小时就够了。


# 打开文件并读取内容
with open('test.txt', 'r', encoding='utf-8') as file:
content = file.read()
# 替换字符串
content = content.replace('掘金', '*')
# 输出内容
print(content)

老郑怕用时太多,解释道,其实写代码用不了3个小时,主要是考虑到还需要对接和测试。


主管摇了摇头,说:不要这么着急,做工作一定要仔细,我给你1天的时间,你给我做出精品,一定要用工匠精神做出精品。


主管说,你写一个处理.txt文件的服务。Web将文件传给JavaJava再传给你,你处理完把数据告诉JavaJava再告诉前端。


老郑一听,这里面有Java。于是,他疑惑地问道:“为什么要这么麻烦?Java直接处理不行吗?”


主管笑了笑,说道:“老郑,你糊涂啊。Java要是能处理,我还来找你干啥,你说是不是呀?啊哈哈!”


说完,他背着手走了,仿佛解决了一个世纪大难题。他很满意。


后来,老郑听到Java在讨论工期。其中一项任务是Java对接老郑处理.txtPython接口,Java给出的工期是3个工作日。


老郑感觉很意外,套个壳原样转发请求,需要3天?这不合理啊!


确实不合理。老郑他们公司有一个部门,叫技术总控部。里面人员的定位都是技术大牛,专门负责审核开发人员提报的工时。这项操作,可以防止小白过于轻视任务导致延误项目时间,也可以防止老油条夸大工时造成浪费。


技术总控部的大牛注意到了Java对接Python这项3天的任务。总控告诉Java:这项3天的任务,不合理。


Java开发者淡定地问道:哪个地方不合理?!


总控说:这个3天不合理。一项任务占用3天工时,说明这项任务拆分的还不够细致。你需要将任务拆得更细致一些,我们给出的规范,最小单位是0.5小时,最大单位是1天。


于是,Java开发将这项转发接口的任务,拆分成了3个子任务。



  • 第1天:结合实际业务,完成网络请求从HTTP协议层面的组织和发送;

  • 第2天:对应逻辑场景,实现Python接口返回值的网络层面接收;

  • 第3天:根据前期对接,对齐Web前端、Python端与Java端;


技术总控看了看,满意地点了点头,好似察觉并改善了一项重大风险,又帮公司做了一件防患于未然的事情。


Java开发的工时获批了,他很满意。主管也满意,总控也满意,大家都非常满意。


董事长来视察,看到排得严丝合缝的工时链条,而且大家也都在写代码,也很满意。


老郑抬头看了看整个场面,他总感觉哪里有些不对劲,但又说不上来。这确实是一片欣欣向荣、和谐向上的景象。于是,他也满意地笑了笑:嘿嘿。


作者:TF男孩
来源:juejin.cn/post/7294072229004443663
收起阅读 »

个人创业中的全栈开发经验

web
前言 个人项目开发创业半年有余,两个项目全部扑街,一无所获。 仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。 我过去7年的工作都是在从事前端开发,从最开...
继续阅读 »

前言


个人项目开发创业半年有余,两个项目全部扑街,一无所获。


仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。


我过去7年的工作都是在从事前端开发,从最开始从事IPTV 开发,用原生JS、JQuery 开发运行在机顶盒上JSP 页面;到18年,创建了项目组的第一个Vue 项目,那时候我才算是开始“现代”前端的工作;21年到上海,在新公司开始全面使用React + TS,也就是时至创业开始,我所有的工作技能,都是前端技术,后端相关的只有自己瞎折腾的项目,没有真正应用到实际项目中的,这次也算是逼着自己进步了一把。


技术选型


前端 - 后台管理系统:React + TS,用了Antd 的组件库提供的模板直接创建项目


前端 - 微信小程序:原生微信小程序开发 + Vant Weapp


服务端:微信云开发


为什么要选用以上技术栈,只有有一个原因,就是成本极低!非常低!并且很稳定,前后端全部用JS 搞定;后台管理系统部署在腾讯云的Web 应用托管上,直接免去运维工作。说个题外话,前几年自己搞个人网站的时候,服务器是薅的阿里云的羊毛,结果就是啥活都得自己干,用Express 框架开发的后端服务,用Nginx 做代理,结果并发超过100个 服务器直接挂掉。。


现在这一套技术栈,几乎没有学习成本,腾讯云的Web 应用托管集成了CI 工具,提交代码到线上分之后,自动部署,用了半年多,网站、小程序都没有挂掉过。(我真不是腾讯的托。。)


后台管理系统


React、TS、Antd 业务开发技术不多赘述,讲讲怎么在Web 端请求微信云开发的接口吧。


微信云开发提供了可访问云服务的Web Sdk,引入sdk 后,只需要进行简单的初始化,即可访问接口。


云开发登录授权配置,打开匿名登录


image.png


示例代码

处理请求


import cloudbase from "@cloudbase/js-sdk";
...
const env = ""; // 环境id
const clientId = ""; 终端id

// 创建实例
const app: any = cloudbase.init({
env,
clientId,
});

const auth = app.auth({
persistence: "local",
});

...
// 请求方法
export const cloudFn = async (
type: string,
params?: any
): Promise<any> => {
// 判断登录态
if (!auth?.hasLoginState()) {
localStorage.clear();
await auth.signInAnonymously();
}

const res = await app.callFunction({
name: "xxxx", // 云函数名称
data: { type, data: options?. }, // 传参
parse: isDev, // 环境
});

// 根据自己的业务方式处理返回数据
...


处理接口


import { cloudFn } from "@/utils";

export const xxx = (params: API.xxx) => {
return cloudFn("name", params);
};

Web Sdk 官方文档:docs.cloudbase.net/api-referen…


部署

提交代码到部署分之后,会自动部署,访问web 应用托管,会提供一个默认访问的域名,可以直接访问,但是不推荐生产使用,只需要再配置一个域名就好了。


微信小程序


如果没有开发过微信小程序,去看一下官方文档,前端基本可以无成本上手,参照官方文档开发就好;为什么组件库选择Vant Weapp,基本补全了官方没有提供的组件,使用方式也很简单,实际使用后体验不错,值得推荐。


微信云开发


我用Java、python、node 都写过后端接口,对于一个前端来说,单论简单、好上手而言,微信云开发,我愿称之为YYDS!就两个字,简单!


官方提供了请求的方法,我对其简单的封装了一下,如果觉得不错,尽管拿去用,如果有不完善的,还请指正


示例代码

云函数入口 index.js

const user = require('./user);

exports.main = async (event, context) => {
switch (event.type) {
case "userGet":
case "userUpdate":
return await user.main(event, context);


default:
return {
code: -1,
msg: '
接口不存在'
}
}
};

user 入口

const get = require("./get");
const update = require("./update");

exports.main = async (event) => {
const apiType = event.type
const data = event.data || {};

if (apiType === 'userGet') {
return await get.main(data);
};

if (apiType === 'userUpdate') {
return await update.main(data);
};
};

user/get.js

const {
dbGet, // 通用get 方法 (见后问)
filterParams, // 清除异常参数,比如空字符串,null 等
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
// 校验参数
if (check(data)) return {
code: -1,
msg: check(data),
}

const params = {
offset: data.offset,
limit: data.limit,
name: data.name,
};


// 模糊搜索
if (params.name) {
params.name = {
$regex: ".*" + params.name,
$options: "i",
};
}

return await dbGet("user", filterParams(params));
};

user/update.js

const {
dbUpdate
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
if (check(data)) return {
code: -1,
msg: check(data),
}


const params = {
_id: data._id,
name: data.name
};

return await dbUpdate("user", params);
};

utils.js

const cloud = require("wx-server-sdk");

// 初始化云环境
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
});

const db = cloud.database();

async function dbGet(
databaseTable, // 表名
params, // 参数
orderByKey = "", // 排序参数
order = "desc" // 排序方式
) {
const pageInfo = {
offset: params.offset || 1,
limit: params.limit || 10,
};
delete params.offset;
delete params.limit;

try {
// 获取总数
const resCount = await db
.collection(databaseTable)
.where(params)
.count();

const resCountData = formatRes(resCount)
if(resCount?.code !== 0) {
return resCountData
}

// 总数是0,直接返回数据
if(resCount?.data === 0) {
return {
code: 0,
data: { data: [] },
total: 0,
}
}

// 获取数据
const res = await db
.collection(databaseTable)
.where(params)
.skip((pageInfo.offset - 1) * pageInfo.limit) // 分页
.limit(pageInfo.limit) // 最多几条
.orderBy(orderByKey, order) // 排序
.get();

// 处理返回数据
const resData = formatRes(res);
if(resData?.code === 0) {
return {
code: 0,
data: {
data: resData?.data || [],
total: resCountData?.data || 0
},
}
} else {
return resData
}
} catch (error) {
return {
code: -2,
data: null,
msg: "请求失败",
};
}
}

async function dbUpdate(databaseTable, updateData) {
let res;
let isSuccess = false;
try {
const params = updateData;

if (updateData._id) {
// 编辑
delete params._id;
res = await db.collection(databaseTable).doc(updateData._id).update({
data: params,
});

if (res.errMsg === "document.update:ok") {
isSuccess = true;
}
} else {
// 新增
res = await db.collection(databaseTable).add({
data: params,
});

if (res.errMsg === "collection.add:ok") {
isSuccess = true;
}
}
if (isSuccess) {
return {
code: 0,
_id: res._id,
msg: `${updateData._id ? "更新" : "新增"}数据成功`,
};
} else {
return {
code: -1,
data: null,
msg: `${updateData._id ? "更新" : "新增"}数据失败`,
};
}
} catch (error) {
return {
code: -1,
data: null,
msg: `请求服务器失败,${updateData._id ? "新增" : "更新"}数据失败`,
};
}
}

function formatRes(res) {
const cloudFnMsgList = ["document.get:ok", "collection.get:ok", "collection.count:ok"];

if (cloudFnMsgList.includes(res?.errMsg)) {
return {
code: 0,
data: res.data || res.total,
};
} else {
return {
code: -1,
data: null,
msg: "请求服务器失败",
};
}
}

module.exports = {
dbGet,
dbUpdate,
formatRes,
};

差不多到此就结束了,Web 端的后台管理系统,微信小程序的后端接口实现了,并且可以互通,这种方式是我实践过,在保证业务、性能、稳定的前提下,最低成本的全栈开发方案,如果有其他更好的方案,欢迎讨论。


作者:鹿林秋月
来源:juejin.cn/post/7294056563631079424
收起阅读 »

说说js代码写到html里还是单独写到js文件里哪个好?为什么?

web
"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论: 将 JavaScript 代码写入 HTML 文件的优点: 方便快捷:将 JavaScript...
继续阅读 »

"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论:


将 JavaScript 代码写入 HTML 文件的优点:



  • 方便快捷:将 JavaScript 代码直接嵌入到 HTML 文件中可以节省创建和加载额外文件的时间,特别是对于小型项目或仅需少量 JavaScript 代码的情况。

  • 直观可见:通过将 JavaScript 代码嵌入到 HTML 文件中,可以更容易地将其与相关的 HTML 元素和结构联系起来,使代码的逻辑更加清晰明了。


将 JavaScript 代码单独写入 JavaScript 文件的优点:



  • 结构清晰:将 JavaScript 代码与 HTML 分离可以使代码结构更加清晰,提高代码的可读性和可维护性。这样做有助于保持 HTML 文件的简洁和专注于内容。

  • 可重用性:将 JavaScript 代码存储在单独的文件中,可以使其在多个 HTML 文件中重复使用,提高代码的可重用性和一致性。

  • 缓存优化:当 JavaScript 代码被单独提取到外部文件中时,浏览器可以将其缓存起来,从而提高页面加载速度并节省带宽。


综上所述,将 JavaScript 代码写入 HTML 文件适合小型项目或仅需少量 JavaScript 代码的情况,以及需要快速原型设计或简单交互的情况。而将 JavaScript 代码单独写入 JavaScript 文件适合大型项目或需要复杂的逻辑和结构的情况,以及需要提高代码的可读性、可维护性和可重用性的情况。根据项目的需求和规模,我们可以灵活选择适合的方式来组织和管理 JavaScript 代码。"


作者:打野赵怀真
来源:juejin.cn/post/7294171458032336906
收起阅读 »

规范化注释你的代码,成为一名优秀程序员的必经之路!

web
前言 想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。 正文 function phoneNumber(numbers) { return"("+numbers[0]+numbers[1]+numb...
继续阅读 »

前言


想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。


正文


 function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"+' '+numbers[3]+numbers[4]+numbers[5]+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

如果我直接丢出这一串代码,你第一眼看过来,心里肯定会想“什么玩意?这一坨代码是干什么用的!居然还需要我拖动横条?!”
但是我如果在它的上方加上这样一段注释,并中途给它换行两次,它就会变成这样


/**
* @func 返回固定格式的电话号码, (123) 456-7890
* @param array [1,2,3,4,5,6,7,8,9,0]
* @return (123) 456-7890
* @author xsir
*/

函数定义
function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"
+' '+numbers[3]+numbers[4]+numbers[5]
+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

你一眼看过去就能清楚的看到,这个函数的作用是返回固定格式的电话号码,
调用函数需要输入的形参的样式是一个数组,返回值为固定格式的电话号码,函数的作者是xsir。


在大公司做程序开发的时候,一整个大的项目需要多人协作一同完成,所以代码的可读性就显得尤为重要,甚至可以说,代码的可读性高于一切,因为在这个时候你的代码不仅仅是写给你自己看和用的,而是整个开发团队的同志们都需要能快速看懂并且调用的。


如果别人看你写的代码时,仅仅只有代码而没有任何其他注释,那么他就需要整体的阅读你写的所有代码,才能知道你写的函数是干什么用的,这就会浪费很多时间。“Time is money, efficiency is life!”


顺带一提,如果你使用的是'${}'的格式


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]}) ${numbers[3]}${numbers[4]}${numbers[5]}-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

运行上述代码,输出的结果是这样的:


image.png


为了提高代码可读性,你对它进行了换行


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`


那么输出结果就会变成这样


image.png


并没有达到预期的效果。所以在实战过程中使用'${}'需谨慎。


总结


在我们开发学习的过程时就要养成良好的编程素养,每次写完一块代码就写好这块代码的注释,做到看“码”知意。同时也要避免单行代码写的过长,尽量使你的代码不需要拖动横条就能看完。


作者:阡陌206
来源:juejin.cn/post/7293789288725889078
收起阅读 »

我是这样保持精力充沛的

精力管理就好比是计算机的内存清理,你以为关掉一些程序就行了,结果你还是卡成翔。 我的现状 雷猴啊,我是一个临期程序员。打过几年工,被好几个同事问过我为什么精力这么旺盛。 这两年我大多数情况都是早上8点前就到公司*(原本9点上班,后来调成8点半,晚上要是加班...
继续阅读 »

精力管理就好比是计算机的内存清理,你以为关掉一些程序就行了,结果你还是卡成翔。



我的现状


雷猴啊,我是一个临期程序员。打过几年工,被好几个同事问过我为什么精力这么旺盛。


01.jpeg


这两年我大多数情况都是早上8点前就到公司*(原本9点上班,后来调成8点半,晚上要是加班就可以多赚半小时了)*,属于工贼类型。


晚上回到家还有精力打游戏、看书、学习。


02.JPEG






为什么要提升精力


几年前我还是一只前端程序猿,当时刚从 JQueryVue 1.0,学完 Vue 1.0 发现公司的新项目开始用 Vue 2.0 ,和外部对接的项目是用 react 。那段时间真的有点疲于奔命的感觉,很累。一天到晚都想睡觉。睡多了又容易头晕。


03.GIF


逛技术论坛时发现有些大佬真的能精通前端三大框架,写出很多原理类的文章。真让人羡慕~~(生气)~~啊。


我开始怀疑是不是我的精力出现了问题。之后尝试过借助外物的帮助,比如每天靠喝咖啡续命,吃维生素提升精力之类的方法。


但这样会进入一个死循环:


喝完咖啡可能睡不着 -> 睡不着可能就会玩手机 -> 玩手机会越玩越晚 -> 导致第二天起不到床 -> 起不到床又怕会扣工资,然后强行起床 -> 上班没精神,然后喝咖啡续命  -> ......



要破掉这个死循环,就要提升自己的精力。


04.GIF






前期准备


世界上是存在拥有 少眠基因 的人,他们每天可能睡4~5个小时就元气满满的。听说只有1%的人拥有这种基因(羡慕了),但我大概率不属于这类人。我的精力是锻炼出来的。


05.jpg


我可是受过专业训练的!




我是一个执行力很弱的人,为了让自己精力旺盛点,我就画真金白银去买课来学。买了不学就亏大了。


06.jpg


这是课程里给出的金字塔模型。完完全全跟着课程去做应该会变得很强。


但实在太难了,即使是第一层我也不能100%按照课程所说的去做(我实在做不到啊啊啊啊)。


07.gif


举个例子,课程里讲到如果你是右撇子,那早上刷牙的时候就可以用左手刷牙。我试了,左手力量控制不好,出来的结果是一嘴血。


再举个例子,课程里建议早上刷牙的时候试试单脚站着刷牙。我试了,平衡力不好,呛了口牙膏(吞肚子里了)。


由此可见,不是所有方法都适合我。




我的做法


吃了几次亏,我觉得我还是选择性训练算了。如果完完全全跟着课程做能拿100分的话,那我的目标就定在30分好了。


只要大方向没错,我总会有提升的。


大方向是啥?就是课程目录。



  • 运动



  • 情绪


08.png


在每章中调几项自己能做到的,拿张纸出来列份清单每天跟着做,假以时日~嘿嘿嘿~




运动


运动能带来什么好处我就不啰嗦了(因为我不懂其中的原理)。


先说运动,从最简单的运动做起




1个俯卧撑


如果你和我说,长跑有很多好处,可以减肥,可以产生内啡肽,获得哔哩吧啦一大堆好处。那我可以坚持1天。


但课程用了一个很巧妙的方式让学员坚持锻炼,就是每天只做1个俯卧撑


08.JPG


这招真的灵。每天只做1个,这是很容易做到的,完全没有心理负担。


而且,都趴下了,真的只做1个吗?


就这样,我每天都抱着只做1个俯卧撑的心态开启了我的训练。因为没心理负担,所以很容易就坚持下去了,同时每天都给自己带来一个积极的心理暗示:我完成了我承诺的事




间歇训练


大部分程序员工友的工作应该都是比较忙的,要每天抽一段完整时间出来运动的可能性不大。但如果能在碎片时间里稍微动一动对自己还是有好处的。


举个例子,我在公司开会的时候我不会一直坐着,我主要是站着为主。有时候还会偷偷提纲(就是你理解的那个提纲😎)。我见有些工友还买了电脑升降架,这也是不错的招式。


再来点例子,刚睡醒的时候搓脸、伸懒腰,这些需要动的操作我都归到运动里面。


日常通勤我也改成 公交地铁 + 骑共享单车,早上上班是公交或者地铁,这样不会那么容易出汗,回到公司就不会一身臭汗味。晚上下班我会选择骑共享单车,这肯定算是正式运动了,尤其是早期的共享单车骑起来真的累。





课程介绍到,吃饭不要吃太饱,尽量少吃高油高糖高碳水的食物。


但作为广东人,要我戒口?真的很难!


09.GIF




我做不到天天保持健康饮食,但工作日我还是会尽量克制。尤其是工作日的午餐,会直接影响你下午的状态。


10.gif


如果中午吃太饱,吃过多高碳水化合物(比如米饭、面条等)下午就很容易困,饭气攻心嘛。


引用课程的内容:



因为高碳水化合物的食物都容易变成糖,让血糖快速升高,从而导致胰岛素快速分泌,引发色氨酸进入大脑。


色氨酸是合成褪黑素的重要原料,褪黑素越多人就越困。



那么,如果我们想快点入睡的话是不是可以在睡前多吃米饭呢?我不建议,这样睡觉太难受了~




回归正题,应该怎么吃,可以从以下几个方面着手:



  1. 少吃多餐:不让血糖那么容易大起大落。

  2. 多吃低糖高营养食物:首选绿叶蔬菜,然后再到优质蛋白,最后才是高碳水化合物食物。

  3. 多喝水。




少吃多餐


第1点的意思两餐之间的间隔时间不要太长,不然血糖容易大起大落,这样更容易累。


除了早中晚三餐,还可以加上午茶和下午茶。


上午茶和下午茶指的不是奶茶,是指可以吃一些坚果、水果(蓝莓草莓之类的水果)。


早餐可以吃一些高纤维高蛋白食物。


午餐建议吃多点蔬菜,肉类尽量选高质量的蛋白质,比如鸡肉鱼肉。


晚餐的话可以吃点碳水化合物,稍微提高一点血糖,促进晚上睡眠。




多吃素菜,少吃碳水化合物


11.jpg


图片来自课程。





高质量睡眠是恢复精力的关键,这里说的并不是要睡很长时间才是高质量睡眠,是要睡得好才行。


怎样才能睡得好呢?什么褪黑素、深睡、浅睡、快速动眼期这些概念我就不介绍了。主要讲讲我的做法。




睡前不要玩手机


睡前不要玩手机。这句话有2个关键点:



  1. 睡前

  2. 不要玩手机




“睡前”是指多长时间呢?最好是1小时。


我手机的常用软件主要是:微信、企微、微信读书、电话、地图。


我不喜欢玩手游,也不喜欢上微博(连微博都没安装),所以我睡前很容易做到不碰手机。


听说有些工友喜欢睡前在床上打打游戏看看小说,这可能不是好习惯。


如果你实在坚持不了睡前1小时不玩手机,那可以试试睡前半小时不玩,再不行就缩减到10分钟。


还是用回运动篇提到的方法,前期先用最容易实现的标准,坚持一段时间。比如坚持半个月睡前10分钟不碰手机。之后再慢慢加长这个时间。




“不要玩手机”指的是不要在睡前有过多的娱乐项目。比如打游戏、看小说、听激昂音乐等。




冥想


冥想听上去很玄学的感觉。其实我也不知道有没有用,我只是照做了而已。


12.jpg


我所做的冥想仍然是低配版。我上过正念冥想课,确实学到很多冥想的方法,比如如何呼吸、如何思考。


但在睡前我不想搞得那么复杂(怕太复杂自己处理不来,直接睡死过去😄),睡前我只做观察呼吸这件事。


选择一个舒适的姿势躺在床上,合上眼睛,鼻子呼气时心里就想着“呼气”,鼻子吸气时心里就想着“吸气”。


这样做会确实让我平静了很多,不知不觉就睡着了。


我不清楚冥想对我的睡眠有多大帮助,但这样做让我舒服。




情绪


情绪会影响人的健康,同样会影响人的精力。


这里我需要引用一下课程里提到的3条关于情绪的定律:



  1. 人的脑子在一个时段只能主要存在一种情绪;

  2. 相比正面情绪来说,人脑更容易产生负面情绪;

  3. 人可以通过自主的训练来控制情绪。


简单解释一下:


第一条的意思是正常的人在同一时段内,很难开心和惊恐同时存在。


第二条的意思是你赚了100元又失去了100元,其实对于你来说并没什么变化,但你可能会不开心。就是对“损失”比对“获得”更敏感。


第三条的意思好心态是可以训练出来的。




我的做法:



  1. 坚持运动。运动能让我变得更积极、更正面。

  2. 感恩。每天都想想一直以来遇到过的好事。比如谢谢妈妈带我到这个世上。习惯感恩后,我心态确实变得更平和了,而且也更容易发现别人的优点。

  3. 奖励。完成一项任务后给自己小小奖励。




我在“情绪”这个段位不高,能介绍的方法并不多。我的主题是 平和。尽可能朝着这个方向去做。




其他


深呼吸


课程里讲到每天早上起床清醒后可以大喊。


这招我也做不到。在城市生活,放个屁大声点都会被邻居投诉,大叫就更加不可能做到的。


于是我改变了策略:假装自己在大叫


早上起来这么做确实能更快速清醒,最好在阳台做。


晒太阳


自然光确实比人造光舒服。我的工位比较暗,大白天都要靠灯管照明。工作日午休完我会走去窗边晒晒太阳,让自己清醒一点。






永远年轻,永远发疯。


13.JPG


作者:德育处主任
来源:juejin.cn/post/7293788137662316578
收起阅读 »

重生!入门级开源音乐播放器APP —— 波尼音乐

前言 不知道是否还有人记得,7年前的那个 「Android开源在线音乐播放器——波尼音乐」? 本来只是作为毕设项目,没想到很多人感兴趣,就断断续续的在维护,当时在网络上找到了一个百度开放的在线音乐 API,勉强实现了本地 + 网络播放能力。 可惜没过多久 AP...
继续阅读 »

前言


不知道是否还有人记得,7年前的那个 「Android开源在线音乐播放器——波尼音乐」?


本来只是作为毕设项目,没想到很多人感兴趣,就断断续续的在维护,当时在网络上找到了一个百度开放的在线音乐 API,勉强实现了本地 + 网络播放能力。


可惜没过多久 API 就被百度关闭了,从此以后便黯然失色,一度沦落为本地播放器,在这个万物互联时代显得有点落寞,我也因此没有太多更新的动力。


最近无意间发现开源社区已经有大神发布了「网易云音乐 API」,喜出望外,遂有了重整旗鼓的想法,顺便对之前的架构做一次重构,来一次脱胎换骨的升级!


经过3个多月断断续续的开发,今天,它来了!


展示


视频


截图
image.jpg


功能



后续可能会根据需要增加功能




  • 本地功能

    • 添加和播放本地音乐文件

    • 专辑封面显示

    • 歌词显示,支持拖动歌词调节播放进度

    • 通知栏控制

    • 夜间模式

    • 定时关闭



  • 在线功能

    • 登录网易云

    • 同步网易云歌单

    • 每日推荐

    • 歌单广场

    • 排行榜

    • 搜索歌曲和歌单




体验



欢迎大家体验,如果发现功能问题或兼容性问题,可以在本文评论或者 GitHub Issue



环境要求



  • Android 手机

  • 电脑(非必须)


安装步骤



  1. 搭建网易云服务器

    clone NeteaseCloudMusicApi 服务端项目到本地,根据项目说明安装并运行服务,需要确认电脑和手机处于同一局域网

  2. 安装 APP

    点击下载最新安装包

  3. 设置域名

    打开 APP,点击左上角汉堡按钮,打开抽屉,点击「域名设置」,输入步骤1中的地址(包含端口)

  4. 设置完成即可体验



没有电脑,如何体验?


其实有一些同仁已经将网易云服务部署到公网了,我们可以直接用🐶。


这里不方便直接贴地址,下面教大家如何找到可以用的服务:


用 Google 搜索「网易云音乐API」,点击结果,如果页面是下图这样(注意:非作者的 GitHub.io 页面),恭喜,你找到了可以直接使用的服务,拷贝地址栏链接,输入到步骤3即可。


screenshot-20231026-152715.png



源码


wangchenyan/ponymusic: Android online music player use okhttp&gson&material design (github.com)


欢迎感兴趣的朋友 Star、Fork、PR,有你们的支持,我会非常开心😄


开源技术



站在巨人的肩膀上




作者:王晨彦
来源:juejin.cn/post/7294072229003952143
收起阅读 »

如何写出让同事崩溃的代码

web
    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。 一、方法或变了名字随便取     首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或...
继续阅读 »

    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。


一、方法或变了名字随便取


    首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或者方法到底是干嘛用的。哎!对,就是让他去猜,哈哈哈。
来来来,空说没意思,举个栗子图片


    假设现在想要点击某个input框时,显示一个自定义的组件用于选择选择时间。


    正常的写法如下:定义一个 toggleDatePicker 方法
image.png


    这个一看就知道是时间选择器的显示切换方法。


    但是呢,我偏不,定义成下面这样:让同事摸不着头脑,哈哈哈


image.png
当看到很多这样的方法名或变量名时,同事的表情估计时这样的图片图片图片


接下来,第二招图片图片图片


二、方法体尽可能的长,长到不能在长


    这一步至关重要,将所有逻辑全部放在一个方法中写完,坚决不分步骤,不按逻辑一步步拆分方法。让同事为我的超长方法体感到叹为观止,默默流泪。


    老规矩,上栗子


    假设现在有个方法需要处理比较复杂(需要递归,而且每层数据有不同的类型)的json格式的数据回显到页面上(这是用于拼一些条件)。数据格式大概是这样的


[
{
type: "group",
relation: "or",
conditions: [
{
type: "condition",
field: {
name: "员工状态",
id: 12345678
},
logic: "=",
val: 1,
relation: "and"
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2011-07-09",
relation: "and"
}
]
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2001-07-09",
relation: "and"
}
]

    由于上面的 json 数组格式中是一个对象,对象都有 type 属性,一种是 group 类型(表示是一组条件),一种是 condition 类型(表示是真正的条件内容)。


    因此,正常情况下,我们应该是遍历数组,不同的类型分别用不同的方法去处理,并且在处理条件中各个部分的时候分别处理。如下所示:


image.png


image.png


    但是呢?咱主题是啥,让同时崩溃啊,怎么能把方法拆成这样清晰的逻辑呢。图片图片图片


    来给你看看怎么让同事对你的代码叹为观止


image.png


image.png


image.png


image.png


image.png


image.png


    怎么样,牛不牛,一个方法写完所以逻辑,从1825行一口气写到2103行,足足... 2103 - 1825 是多少来着,3减5 不够,向前借位 ,嗯。。。278 行。
****怎么样,有没有被哥的机智震惊到,如此代码。同事看到肯定心生敬佩,连连称绝。此时想到得到同事应该是这样的表情 图片图片图片


    同事还没进到方法体里面,就已经被我的 迷之方法名 和 超长方法体 所折服,接下来就让他在方法体里面快乐的遨游吧
    


    接下来,继续让同时崩溃。


三、坚决不定义统一的变量


    这个怎么说呢,就是因为有点懒,所有很多代码直接复制粘贴,多么的方便快捷。


    正常情况下,如果某个我们需要的对象在是其他对象的属性,并且层次很深,我们先定义一个变量来接收这个对象,再对这个对象操作。


例如:


let a = {
b: {
c: {
d: {
name: "我是最里面的对象"
}
}
}
}

    我们要对d对象进行很多次的操作时,一般先将d赋值给一个变量,然后对变量操作。如下:


var dOfA = a.b.c.d;
dOfA.name = "我现在被赋值给dOfA ";
dOfA.data = 1;
dOfA.other = false;

    但是呢,我就不这么干,就是要写得整整齐齐


a.b.c.d.name = "就要这么干,你打我呀";
a.b.c.d.data = 1;
a.b.c.d.other = false;

    老规矩,没有 实际的 栗子 怎么能说的形象呢,上图


    正常写法:


image.png


    我偏要这么写


image.png


    多么的整齐划一,


    全场动作必须跟我整齐划一


    来左边儿 跟我一起画个龙


    在你右边儿 画一道彩虹


    来左边儿 跟我一起画彩虹...


    咋突然哼起歌来了,不对,咱是要整同事的,怎么能偏题。


    继续,此时同事应该是这个表情图片图片图片


    然后,方法体里面只有这么点东西怎么够玩,继续 come on


四、代码能复制就复制,坚决不提成公用的方法


    代码能 CV ,干嘛费劲封装成方法,而且这样不是显得我代码行数多吗?图片图片图片


    就是玩儿,就是不封装


    来,上栗子


image.png


image.png


image.png


    看到没有,相同的代码。我在 1411行 - 1428行 写了一遍, 后面要用,在1459行-1476行复制一遍


    这还不够?那我在1504-1521行再复制一遍


    这下,爽了吧,哈哈哈


    就是不提方法,就是玩儿,哎! 有意思


    这个时候同事估计是这样的吧图片图片图片


    怎么样,是不是很绝?不不不,这算啥


    虽然以上这些会让看我代码的同事头疼,但是,只要我在公司,他们还会让我改啊。怎么能搞自己呢。


    最后一步


五、离职


    洋洋洒洒的写完代码,尽早离开。够不够绝,哈哈哈


六、申明:


    以上场景纯属个人虚构的,单纯为了给文章增加点乐趣。写这个文章的目的是让各位程序员兄弟尽量避免写这种难以维护的代码。真的太痛苦了!代码质量、代码重构真的是编程过程中很重要的一个步骤。不能抱着能用就行的心态。还是要对自己有一定的要求。只有你看得起自己的代码,别人才有可能看得起你的代码。加油吧!各位


作者:猩兵哥哥
来源:juejin.cn/post/7293888785400856628
收起阅读 »

程序员有没有必要成为业务领域专家 ?

看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。 于是,我做了如下的回答: 非常有必要。 1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。 乔布斯,你是一个聪明又有影响力的人。但是很...
继续阅读 »


看到这个知乎问题时,我的思绪纷飞,往事一幕幕闪现在脑海里,等平静下来,内心变得很笃定。


于是,我做了如下的回答:


非常有必要


1997年,乔布斯刚刚回归苹果不久,在开发者大会上,一名程序员当众质疑乔布斯不懂技术。



乔布斯,你是一个聪明又有影响力的人。但是很遗憾也很明显,很多时候你根本不知道自己在做什么。我希望你能用清楚的语言解释一下 Java 编程语言以及其变种是如何阐述 OpenDoc(开源技文档)内置的一些想法。等你说完以后,你能不能跟我们说一说你自己过去七年都干了些什么?



面对这样犀利的提问,乔布斯平静的喝了一口水,低头沉思了几秒,开口这样回答道:



有时候你能取悦一部分的人,但是当你想要作出改变的时候,最难的是某些事情别人做的是对的。我相信 OpenDoc 肯定有一些功能,没有任何其他东西能做到。我其实也不太懂,我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面,例如让你每年一个产品能够卖百八十亿美元。


我经常发现,你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去。在座的没有人比我犯过更多这样的错误,我也搞到伤痕累累,我知道这就是原因,当我们尝试去为苹果思考战略和愿景,都是从能为用户带来什么巨大利益出发,我们可以给用户带来什么,而不是先找一群工程师,大家坐下来,看看我们有什么吊炸天的技术,然后怎么把它卖出去。



我非常认同乔布斯的话。


程序员有的时候沉迷在自己的世界里,执拗的以为“代码就是全部”


但现实并非如此,编码的目的是创造产品或者提供服务,从而在这个商业社会实现更大的价值


而程序员成长为业务领域专家,能够更加深刻的理解公司的产品或者服务,从而更有优势为公司做出贡献。当个人的贡献上升时,公司的认同和利益也会随之而来。




这个回答一天内得到不少赞同,也是我意想不到的,因为我并不觉得我回答得好,看来很多同学都认可这个观点。



熟悉我的朋友都知道 ,我对技术非常有激情,曾经也认为技术意味着一切。


只是后来,工作中遇到越来越多的挫折,很多好朋友也友善的提醒我,不要太执着于技术,我也越来越认识到自己认知的局限性


我不断的去读书、听演讲、思考,依稀之间得到一个结论:"一个 IT 公司的成功 ,技术固然是重要的一环,而公司的产品、用户人群、经营模式是另一个我很少关注且非常重要的维度"。


偶然间我看了乔布斯的一个视频,视频的两句话让我醍醐灌顶。




  • 我相信你肯定能做一些样品出来,可能是一个小型的 app 来展示它的功能,最难的部分是如何将那些功能塞进更大的愿景里面




  • 你得从用户体验出发,倒推用什么技术,你不能从技术出发,然后去想如何才能卖出去




懂业务是一种认知模式,人的能力是多层次的,技术和懂业务并非互斥的关系。


亲爱的程序员朋友,技术是我们的立身之本,但是业务同样重要 , 真诚的希望你做一个既懂技术又懂业务的工程师。




如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!


作者:勇哥java实战分享
来源:juejin.cn/post/7246224746005954616
收起阅读 »

一文学会请求中断、请求重发、请求排队、请求并发

web
大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处! 以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。 阅读下文需要了解前置知识:promise、class、axios...
继续阅读 »

大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处!

以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。

阅读下文需要了解前置知识:promise、class、axios


请求中断


1.判定相同请求:请求url、请求方法、请求params参数、请求 body参数,四个值都相同,则认为是一个相同的请求。

2.判断请求中断:在上一个相同请求还没有得到响应前,再次请求,则会自动中断。


image.png


请求重发(无感刷新token)


1.当前请求返回401时,执行刷新token。

2.当同时存在多个请求返回401时,可在类中维护一个静态变量保存请求刷新接口的promise,防止多次调用刷新token。

3.RetryRequest类实例化参数:

   instance:请求实例对象

   success:刷新成功回调函数

   error:刷新失败回调函数

image.png


请求排队



  1. queue:请求等待队列。

  2. isWating:是否正在等待上个请求响应。

  3. add:向队列里加入一个等待请求的promise的resolve方法,执行该方法可立刻发送下一个请求 。

  4. next:执行下一个请求方法,在上一个请求响应后调用。

    image.png


响应处理


image.png


axios请求实例


image.png


测试


1.请求中断测试


快速点击test请求按钮多次


image.png


2.刷新token请求重发测试


(1)当用户没有登录请求接口时


image.png


(2)当用户登录后,accessToken过期,但refreshToken还没过期调用接口时


image.png
在调用刷新token接口成功后,将重发失败的test接口


(3)当refreshToken过期后调用接口时


image.png
这时已经无法刷新token了,只能乖乖跳转到登录页面了。


3.请求排队测试


(1)没有使用请求排队时


场景:当输入框输入关键字实时查找内容时,由于网络原因,可能会出现先请求的后响应的请求,导致请求错乱。

如下,模拟网络请求延迟:
image.png


当输入框依次输入1、2、3、4、5时,期望的返回结果应该是1,12,123,1234,12345。
但确得到了以下的结果:
image.png


(2)使用请求队列时


在网络请求的waterfall列可以清晰看到,当上一请求完成才会执行下一请求,直到等待队列执行完成。
image.png


源码


后端接口


var express = require('express');
var router = express.Router();

const access_token = 'access_token'
const refresh_token = 'refresh_token'
// token有效期(单位毫秒)
const tokenValidTime = 1000*2
// 刷新token有效期
const refreshTokenValidTime = 1000*5
// 登录时间,模拟token过期
let loginTime;
// 模拟判断token是否过期
const IsTokenExpired = () => {
if(new Date().getTime() > loginTime + tokenValidTime) {
return true
}
return false
}
router.post('/login', (req, res) => {
loginTime = new Date().getTime()
res.json({
access_token,
refresh_token
})
})

router.post('/refresh-token', (req, res, next) => {
const refreshToken = req.headers.authorization
console.log('refresh-token', refreshToken)
if(refreshToken !== refresh_token) {
return res.status(401).json({
msg: ' refreshToken不正确!'
})
}
if(new Date().getTime() > loginTime + refreshTokenValidTime) {
return res.status(401).json({
msg: ' refreshToken已过期,请重新登录!'
})
}
loginTime = new Date().getTime()
res.json({
access_token: 'access_token',
refresh_token: 'refresh_token'
})
})

router.get('/test', (req, res, next) => {
const token = req.headers.authorization
if(token !== access_token) {
return res.status(401).json({
msg: '没有访问权限'
})
}
if(IsTokenExpired()) {
return res.status(401).json({
msg: 'token已过期'
})
}
res.json({
name: '哈哈'
})
})

router.get('/random', (req, res) => {
const keyword = req.query.keyword
setTimeout(() => {
res.json({
value: keyword
})
// 5秒内随机返回,测试网络请求延迟效果
}, Math.random()*5000);
})

module.exports = router;

前端


Index.js


import axios from "axios"
import AbortRequest from './hooks/AbortRequest'
import ResponseHanlder from "./hooks/ResponseHanlder"
import { getAccessToken, getRefreshToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'
import { useRequestKey } from "./hooks/useRequestKey"
import RequestQueue from "./hooks/RequestQueue"
import { isAddQueue } from '@/api'

export const baseURL = '/api'
const timeout = 6000

// 创建axios实例
const instance = axios.create({
baseURL,
timeout
});

// 创建中断请求控制器
const abortRequest = new AbortRequest()
// 创建响应处理器
const responseHandler = new ResponseHanlder(instance)
// 创建请求队列排队实例
const requestQueue = new RequestQueue()

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
console.log('在发送请求之前做些什么', config)
// 在发送请求之前做些什么
if(config.url !== '/login') {
const token = config.url === refreshTokenUrl ? getRefreshToken() : getAccessToken()
config.headers.Authorization = token
}
// 刷新token接口不用创建取消请求,已经再RetryRequest类维护静态属性
if(config.url !== refreshTokenUrl) {
abortRequest.create(useRequestKey(config), config)
}
// 加入请求等待队列
if(isAddQueue(config)) {
return requestQueue.add(config.url, config)
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
const config = response.config
console.log('响应成功', response)
abortRequest.remove(useRequestKey(config))
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return responseHandler.success(response)
}, function (error) {
const config = error.config
console.log('响应错误', config)
if(config) {
abortRequest.remove(useRequestKey(config))
}
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if(axios.isCancel(error)) {
return Promise.reject('已取消重复请求!')
}
return responseHandler.error(error)
});

export default instance;

AbortRequest.js


// 重复请求中断类
class AbortRequest {
constructor() {
// 请求中断控制器集合
this.list = new Map()
}
// 创建中断请求控制器
create(key, config) {
const controller = new AbortController();
config.signal = controller.signal
// 集合中存在当前一样的请求,直接中断
if(this.list.has(key)) {
controller.abort()
} else {
this.list.set(key, controller)
}
}
// 请求完成后移除集合中的请求
remove(key) {
this.list.delete(key)
}
}
export default AbortRequest

RequestQueue.js


/**
* 相同url请求队列,排队执行维护类
*/

class RequestQueue {
constructor() {
// 请求等待队列
this.queue = {}
// 正在等待上一请求执行中
this.isWating = false
}
add(url, config) {
return new Promise((resolve) => {
const list = this.queue[url] || []
if(this.isWating) {
// 当前请求url存在等待发送的请求,则放入请求队列
list.push({ resolve: () => resolve(config) })
} else {
// 没有等待请求,直接发送
resolve(config)
this.isWating = true
}
this.queue[url] = list
console.log('list', list)
})
}
// 响应处理
next(url) {
this.isWating = false
// 拿出当前请求url的下一个请求对象
if(this.queue[url]?.length > 0) {
const nextRequest = this.queue[url].shift()
// 执行请求
nextRequest.resolve()
}
}
}

export default RequestQueue

RetryRequest.js


/**
* 无感刷新token类
*/

class RetryRequest {
// 解决存在多个并发请求时,重复调用刷新token接口问题
static refreshTokenPromise = null
constructor({
instance, // axios实例
success, // 刷新token成功执行的回调函数
error // 刷新token失败执行的回调函数
}
) {
this.instance = instance
this.success = success
this.error = error
}
/**
* @param config 当前请求对象,等待token刷新完成再重复执行
* @param refreshTokenApi 刷新token方法
*/

useRefreshToken(config, refreshTokenApi) {
if(!config.headers.Authorization) {
this.error()
return Promise.reject('token不存在!')
}
return new Promise((resolve, reject) => {
if(!RetryRequest.refreshTokenPromise) {
// refreshTokenPromise不为null,则当前正在执行刷新token方法,不再重复调用
RetryRequest.refreshTokenPromise = refreshTokenApi()
}
RetryRequest.refreshTokenPromise.then(res => {
// 刷新token成功
this.success(res)
// 重新发送请求
this.instance(config).then(data => {
resolve(data)
}).catch(err => {
// 重发失败
reject(err)
})
}).catch(err => {
// refreshToken失效或刷新token失败
this.error()
reject(err)
}).finally(() => {
// 刷新token调用完成,重置
RetryRequest.refreshTokenPromise = null
})
})
}
}

export default RetryRequest

ResponseHanlder.js


import RetryRequest from './RetryRequest'
import { refreshToken as refreshTokenApi } from '@/api/index'
import { getRefreshToken, setRefreshToken, setAccessToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'

/**
* 响应处理类
*/

class ResponseHanlder {
constructor(instance) {
this.retryRequest = new RetryRequest({
instance,
success: (res) => {
const { access_token, refresh_token } = res
setAccessToken(access_token)
setRefreshToken(refresh_token)
},
error: () => {
console.log('刷新token失败!')
// 执行失败逻辑...
}
})
}
// 请求正常响应方法
success(response) {
// 对响应数据做处理
return response.data
}
// 请求错误响应方法
error(error) {
const status = error.response?.status
// 当前返回401,且不是调用刷新token接口响应的(避免后端刷新token失败返回401导致死循环的情况)
if(status === 401 && error.config.url !== refreshTokenUrl) {
return this.retryRequest.useRefreshToken(
error.config,
() => refreshTokenApi(getRefreshToken())
)
} else {
return Promise.reject(error.response)
}
}
}

export default ResponseHanlder

request.js


import instance from './index'

class Request {
constructor() {

}
get(url, params, args) {
return instance.get(url, {
params,
...args
})
}
delete(url, params) {
return instance.get(url, {
params
})
}
post(url, data) {
return instance.post(url, data)
}
put(url, data) {
return instance.put(url, data)
}
}

export default new Request();

结语


还有一个控制请求并发数量还没进行扩展,相信大家了解了请求排队的思路后,实现请求并发控制也不是什么难事了。



无感刷新token参考文章:juejin.cn/post/728974…



作者:云上客人
来源:juejin.cn/post/7293806405650464808
收起阅读 »

判断鼠标从哪个方向进入元素

web
我们需要实现的效果图 理清需求 拿到效果图的第一步,理清下需求~ 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域? 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。 最后,最难的是如何判断鼠标进...
继续阅读 »

我们需要实现的效果图


image.png


理清需求


拿到效果图的第一步,理清下需求~



  • 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域?

  • 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。

  • 最后,最难的是如何判断鼠标进入的时候会落在我们划分好的上下左右四个区域?


思路



  • 首先我们先来划分下四个区域,一般划分的都如下图


image.png




  • 图里面有四个三角形,每个三角形代表的是一个方向,所以问题简化为如何在一个矩形里,根据对角线划分区域。由于元素存在坐标系,也就是X、Y轴,所以问题再次抽象成,如何得到两条对角线的线性函数。(初高中数学问题。)




  • 最后的问题我们就要来搞定判断鼠标落点的问题,首先我们知道我们可以在元素的鼠标事件中通过event得到鼠标的pageX和pageY,再配合元素的offsetLeft和offsetTop就可以得到鼠标在元素中的坐标。综合一下就变成了,我有一个坐标,且我知道对角线的函数表达式,请问我如何知道我这个坐标是在函数的下面还是上面?




  • 当然也许描述的比较抽象,我们可以类比一个例子,我现在有一个坐标(2,1),有一个函数y=x,值域大于0(既y>0),定义域大于0(既x>0),求该坐标在y=x的函数下面还是上面?(是不是感觉到了线性规划得到最优解的味道,对,少年,没有错,就是这样。)这里我们只要把坐标中的x值代入函数,然后判断代入的结果是否大于坐标的y值,如果大于则在函数下面,小于则在函数上面,什么?你问等于怎么办?当然是在函数上面,该坐标即在上面又在下面,所谓薛定谔的坐标是也(当然是在函数上了)。




  • 然后我们是不是可以扩展下,如果存在多个函数,再加上逻辑判断经常用的交集,并集是不是又有新的思维出现了呢?好了,这边就不再扩展了,下面直接上实现代码吧。




实现代码


注意:该demo只是简单的demo,其中有很多可以优化的地方,比如组件化,变量优化,利用发布订阅模式,实现事件联动


<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css" >
.ct{
height: 100px;
width: 100px;
border:1px red solid;
}
</style>
</head>
<body>
<div class="ct" onmouseover="fun1(event);" onmouseout="fun2(event);">

</div>
<script type="text/javascript">
//当然这样绑定事件函数是不对的
var div=document.getElementsByTagName("div")[0];
function fun1(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方进入");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方进入");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边进入");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边进入");
//todo
}

}
function fun2(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方离开");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方离开");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边离开");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边离开");
//todo
}

}
</script>
</body>
</html>

作者:洛漓
来源:juejin.cn/post/7293820517374820352
收起阅读 »

彻底搞懂闭包

web
每次面试都问,每次都背;每次都背的不错,每次都不太理解。 定义 闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。 一个简单的例子认识闭包: function init() { var...
继续阅读 »

每次面试都问,每次都背;每次都背的不错,每次都不太理解。


定义


闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。


一个简单的例子认识闭包:


function init() {
var name = 'wendZzzoo';
function getName() {
console.log(name)
}
getName()
}
init()


使用场景


那闭包有什么作用呢?但从上面这个简单的例子中,似乎很难发现这样写,也就是闭包这样的写法的用途。


数据封装和隐藏


通过使用闭包,可以创建一个作用域限定的环境,以保护变量不受外部的访问和修改。这样可以防止变量被意外修改,避免命名冲突和全局污染,提高代码的可维护性和可读性。


举个例子,定义一个计数器函数,用来某些场景下计算次数。


没有使用闭包的示例


let count = 0
function increment() {
count++
console.log(count)
}
increment()
increment()


上述代码是实现了计数器的需求,但是代码存在风险,count变量是全局定义的,在后续开发中或者是其他人维护时可以轻易修改这个变量,导致bug出现。


使用闭包的示例


function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}

const counter = createCounter();
counter();
counter();


在上述示例中,createCounter函数返回了一个内层函数increment,该函数可以访问并递增count变量。外层函数的作用域被封装在闭包中,外部无法直接访问和修改count变量。


这里可以衍生思考一下,count变量封装在闭包中只能递增,外部无法修改,那该如何重置或者递减count呢?


其实需要新增的逻辑也可以封装到闭包里,以重置count为例:


const counterModule = (function() {
let count = 0;
function increment() {
return ++count;
}
function reset() {
count = 0;
}
return {
increment,
reset
};
})();

console.log(counterModule.increment());
console.log(counterModule.increment());
counterModule.reset();
console.log(counterModule.increment());


在上面的例子中,使用立即调用函数表达式(IIFE)创建了一个闭包,内部定义了count变量和两个操作它的函数incrementreset。通过返回一个包含这些函数的对象,实现了对count变量的封装和控制。


保持数据状态


通过闭包,内层函数可以访问和持有外层函数的变量,即使外层函数执行完毕,这些变量依然存在于内层函数的词法环境中,从而实现了数据状态的保持。


这个使用场景可以算是上一个的延申,在上述示例代码中添加传参,就可以起到了数据状态保持的目的。


函数柯里化


闭包使得函数可以返回另一个函数作为结果,从而形成函数工厂的模式。通过在内层函数中访问外层函数的参数或变量,可以创建具有不同参数或上下文的函数。这种技术称为柯里化,其目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。


举个例子,定义一个求矩形面积的函数。


不使用柯里化的示例


function getArea(width, height) {
return width * height
}
const area1 = getArea(10, 20)
console.log(area1)
const area2 = getArea(10, 30)
console.log(area2)
const area3 = getArea(10, 40)
console.log(area3)


上面代码里,假设我们需要这个计算矩形面积的函数,来计算宽度总是10的多种情况,那就需要多次调用getArea函数传入相同的宽度参数,且在维护的时候,假设需要统一修改宽度为20,就需要重复修改每一次调用时宽度的传参,这样重复的工作在力求优雅的情况下看来是不合适的。


使用闭包柯里化的示例


function getArea(width) {
return height => {
return width * height
}
}

const getTenWidthArea = getArea(10)
const area1 = getTenWidthArea(20)
console.log(area1)
const area2 = getTenWidthArea(30)
console.log(area2)
const area3 = getTenWidthArea(40)
console.log(area3)


如果有需要宽度改变的情况,也可以轻松复用


const getTwentyWidthArea = getArea(20)


再举个例子,定义一个打印日志的函数。


function createLogger(prefix) {
function logger(log) {
console.log(`${prefix}: ${log}`);
}
return logger;
}

const exportWarnning = createLogger('warnning');
exportWarnning('这是一个警告日志');

const exportError = createLogger('error');
exportError('这是一个错误日志');


通过调用createLogger函数并传递不同的参数,可以创建具有不同日志前缀的logger函数。


通过上述两个柯里化的例子,可以发现柯里化是一种技术更多是一种提倡,使用这样的技术可以让你的代码更有维护性。


模拟私有化方法


私有方法是将某些函数或变量限定在一个作用域内,外部无法直接访问。


function makeCounter() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value());
Counter1.increment();
Counter1.increment();
console.log(Counter1.value());
Counter1.decrement();
console.log(Counter1.value());
console.log(Counter2.value());


上述代码通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式


两个计数器 Counter1 和 Counter2 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量


注意事项


闭包是一种强大的特性,但滥用闭包可能导致代码可读性和性能方面的问题,因此需要注意的是:



  1. 避免不必要的闭包,只有在确实需要保留状态或隐藏数据时才使用闭包。不要为了使用闭包而创建不必要的函数嵌套,盲目使用闭包并不会让你的代码看起来更高级。

  2. 要注意内存管理,闭包会持有对外部作用域的引用,可能导致内存泄漏。确保在不再需要闭包时,手动解除对外部作用域的引用,以便垃圾回收器能够正确处理。

  3. 特别要小心循环中的闭包,闭包会捕获循环变量的引用,可能导致意外结果。可以使用立即调用函数表达式(IIFE)或函数绑定来解决。

  4. 换一种解决方案,可以使用模块模式,如果需要封装私有方法和变量,考虑使用模块模式或其他模块化工具,如ES6模块。这样可以更清晰地定义私有和公共部分,并提供更好的可维护性和可测试性。


内存泄漏


闭包可以引起内存泄漏的情况,通常是涉及对外部作用域的引用。当函数形成闭包时,它会持有对其包含作用域的引用,这可能导致无法释放被闭包引用的内存。


可能导致内存泄漏的情况:



  1. 未及时释放闭包,如果闭包持有对外部作用域的引用,但不再需要使用闭包时,如果没有显式地解除对外部作用域的引用,闭包将继续存在并持有外部作用域中的变量。

  2. 当闭包和其包含作用域之间存在循环引用时,可能导致内存泄漏。例如,如果闭包中引用了一个对象,而该对象又持有对闭包的引用,这将导致它们互相引用,无法被垃圾回收。

  3. 闭包中引用了全局变量,闭包将一直存在,即使在不再需要闭包时也无法释放。这种情况下,全局变量将一直保持活动状态,无法被垃圾回收。


为避免闭包引起的内存泄漏,建议:



  1. 及时解除引用,当不再需要使用闭包时,确保手动解除对外部作用域的引用。只需要将闭包中引用的变量设置为 null 或重新分配其他值,以便垃圾回收器能够正确处理。

  2. 尽量避免闭包和其包含作用域之间的循环引用。确保在闭包中不引用外部对象,或者在外部对象中不引用闭包,以避免循环引用导致的内存泄漏。

  3. 只在确实需要保留状态或隐藏数据时使用闭包,在不需要闭包的情况下,使用适当的作用域(例如局部变量或模块作用域)来防止不必要的内存占用。


内存泄漏的发生并不一定是由闭包引起的,还可能涉及其他因素,但是,闭包在不正确使用的情况下容易导致内存泄漏问题。


作者:wendZzoo
来源:juejin.cn/post/7293805895918207026
收起阅读 »

📷纯前端也可以实现「用户无感知录屏」?

web
前言 要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。 但该方法会在浏览器弹出...
继续阅读 »

前言


要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。


但该方法会在浏览器弹出一个授权窗口,让用户选择要分享的内容,这不可实现“无感知”。


image.png


如果真正做到无感知,那我们就不能借助浏览器或者系统系统的能力了。我们能做的就只能是通过js去操作了。


要在页面内直接录制视频似乎并不容易,没有现成的开源库可以使用,也没有很好的想法。


那我们换一个思路,视频是由帧组成的,我们是否可以不断的截图,然后组合成一段视频?好像是可以的。


下载.jpeg


效果


mp4.gif


页面


先写一个简单的页面:


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

<head>
<meta charset="UTF-8">
<title>Canvas视频录制</title>
<link rel="stylesheet" href="styles.css">
</head>

<body>
<main>
<div class="buttons">
<button class="start-btn">开始录制</button>
<button class="pause-btn">暂停录制</button>
<button class="resume-btn">继续录制</button>
<button class="stop-btn">结束录制</button>
</div>
<div id="box">
<section class="content">
<h2>TODO LIST</h2>
<div class="background-div">
<button class="background-btn">切换背景颜色</button>
</div>
<div id="todo-form">
<input type="text" class="input-field" placeholder="输入待办事项">
<button type="submit" class="submit-btn">提交</button>
</div>
<div class="list"></div>
</section>
</div>
<img src="" alt="" class="hidden">
</main>

<script src="<https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.min.js>" defer></script>
<script src="canvas.js" defer></script>
</body>

</html>

截图


实现网页的截图操作,最常用的库是 html2canvas用,它可以将网页中的 HTML 元素转换为 Canvas 元素,并将其导出为图像文件。在浏览器中捕获整个页面或特定区域的截图,包括 CSS 样式和渲染效果。


const canvasFunction = () => {
html2canvas(box).then(canvas => {
const imgStr = canvas.toDataURL("image/png");
img.src = imgStr;
img.onload = function () {
ctx.drawImage(img, 0, 0, w, h);
}
});
}

合成视频


这里我们要使用到一个 API MediaRecorder ,用于在浏览器中进行音频和视频的录制。它提供了一种简单的方式来捕获来自麦克风、摄像头或屏幕的媒体数据,并将其保存为文件或进行实时流传输。


它有以下几个常用的方法:



  • isTypeSupported() 返回一个 Boolean 值,来表示设置的 MIME type 是否被当前用户的设备支持。

  • start() 开始录制媒体,这个方法调用时可以通过给 timeslice 参数设置一个毫秒值,如果设置这个毫秒值,那么录制的媒体会按照你设置的值进行分割成一个个单独的区块,而不是以默认的方式录制一个非常大的整块内容。

  • pause() 暂停媒体录制。

  • resume() 继续录制之前被暂停的录制动作。

  • stop() 停止录制。同时触发 dataavailable 事件,返回一个存储 Blob 内容的录制数据。之后不再记录。


首先创建一个 canvas 元素,用来保存 html2canvas 的截图,然后通过 captureStream 方法实时截取媒体流。


const w = boxBoundingClientRect.width;
const h = boxBoundingClientRect.height;
const canvas = document.createElement('canvas');
canvas.setAttribute('id', 'canvas');
canvas.setAttribute('width', w);
canvas.setAttribute('height', h);
canvas.style.display = 'none';
box.appendChild(canvas);

const img = document.querySelector('img');
const ctx = canvas.getContext("2d");
const allChunks = [];
const stream = canvas.captureStream(60); // 60 FPS recording 1秒60帧

通过 canvas 的流来创建一个 MediaRecorder 实例,并在 ondataavailable 事件中保存视频信息:


const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});

recorder.ondataavailable = (e) => {
allChunks.push(e.data);
};

最后,在停止录制时将帧信息创建 blob 并插入到页面上:


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

const video = document.createElement('video');
video.controls = true;
video.src = videoUrl;
video.muted = true;
video.autoplay = true;
document.body.appendChild(video);

或者可以将视频下载


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

let link = document.createElement('a');
link.style.display = 'none';
let fullBlob = new Blob(allChunks);
let downloadUrl = window.URL.createObjectURL(fullBlob);
link.href = downloadUrl;
link.download = 'canvas-video.mp4';
document.body.appendChild(link);
link.click();
link.remove();

这里,为了节省资源,只在点击按钮、输入等事件发生时才调用 html2canvas 截图 DOM。


如果实时记录屏也可以使用 requestAnimationFrame


最后


虽然实现了无感知录制屏幕,但也仅限于网页内,没有办法录制网页以外的部分。


以上的 demo 中只实现了 DOM 的录制,如果想要录制鼠标轨迹,可以增加一个跟随鼠标的元素~


作者:Mengke
来源:juejin.cn/post/7293462197386592283
收起阅读 »

被裁后的一天

距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生...
继续阅读 »

距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生不愉快的事情。我写这篇文章不是为了缅怀过去或者反思过去,而是记录我的一天,余下的内容没有华丽的词藻,也没有令人深思的道理,只是流水账。


今天我 7 点 20 就醒了,醒来的前一刻还在做梦,我梦到自己睡不着觉,可想而知那不是一个美梦。醒来的那一瞬间,我甚至没有分辨出是现实还是梦境,头昏昏沉沉的赖了一会儿床,在赖床期间我老公起床了,他洗漱结束的时候,我用手机放了一首歌,他走到床边对我巴拉巴拉说了些话,说的都是些大道理,大概是选择呀未来呀思考呀这一套,如果让我将他的话背下来,可真是在难为我。其实他一大早就讲大道理,我特别不想听,但我觉得他的话有些道理,于是赶紧起了床,洗漱完去跑步。


今天温度不高,早上很适合跑步,估摸着跑了 15 分钟,没有记录跑了多少公里,我打算明天记录一下跑步的公里数。跑完步给手机充了一会儿就出门吃早餐,需要坐两站地铁才能到那家包子铺,我住的地方距地铁站大概 1.5 公里,这段路可以坐小区的摆渡车,但我选择了走路。


那家包子铺叫什么名儿?不记得了,更准确的说,我从来没想过要记住它的名字。我买了两个豇豆包,一个卤鸡蛋还有一杯豆浆。为什么要买这些食物呢?不是没有缘由。昨天我只买了两个豇豆包和一杯豆浆,没有吃饱,于是今天加了一个卤鸡蛋。今天买的这些食物,还是没让我吃饱,明天我要再加一个豇豆包。


为什么要买豇豆包呢?因为我喜欢吃酸豇豆,但是这包子里包的不是酸豇豆,而是新鲜的豇豆,所以第一次吃它的时候它与我的预期不符合,尝了之后又发现新鲜的豇豆也好吃,现在我每次都买豇豆包。今天我看见豇豆包里包的不是豇豆,而是四季豆,四季豆也挺好吃。


这家包子店里能喝的食物除了有豆浆还有稀饭,它的稀饭不是很稀,我担心豇豆包就着稀饭吃不好下咽,于是买了豆浆。


吃了早餐我就回家了,到家的第一件事是联系物业师傅来疏通地漏,还让他们处理电动晾衣架不能升降的问题。关于电动晾衣架,我今天学到了一个新知识,有些人可能会认为那是小常识,不值一提,但我还是要写下来——电池放在遥控器里久了会腐蚀铜板,这将导致遥控器通不了电,遥控器就指挥不了晾衣架,我家的晾衣机不能升降就是这个原因的。


今天我写了两篇技术文章去参加掘金推出的金石计划征文活动,这可以瓜分奖金,这是我第 5 次参加金石计划,每一次都瓜分到了最高奖金,这次我瓜分不到最高奖金了,因为写不出 6 篇原创技术文章。


等物业师傅处理好地漏和晾衣架,发布了文章,我就出发去健身房游泳,健身房离我家有 4.8 公里左右,出发前我在想是开车还是走路,最终选了走路。


今后要做什么?在路上我不由得思考。


今年是我工作的第 6 年,6 年来我断断续续的有考研的想法。为什么想考研呢?在路上我想出了一个原因 —— 源自虚荣心。走着走着想上厕所,路过一家花店,进店问老板附近是否有厕所,他说马路对面有,到了马路对面又向烟酒铺的老板询问厕所的具体位置,如愿的上了厕所,一身轻松。


上完厕所路过一家包子铺,肚子饿了,于是进店买了 2 个包子,1 个鸡蛋还有 1 碗粥,其实我想喝带丝汤,但是卖完了,所以没喝成。


吃完午餐继续往健身房走。哦,我想起在包子铺吃午餐的时候看到了一个皮肤特别好的女生。走呀走呀,终于到了健身房,到那儿之后我没有立即去游泳池,而是去休闲区按摩,在按摩椅上躺着的时候,开始思考我是否想继续当程序员,如果当程序员,想做那方面的业务呢?


我喜欢写作。2021 年期间我写了一部小说,超过 10 万个字,2022 年写我的第一本技术书,上个月才交稿,出版社编辑说稿子问题不大,昨天我根据编辑老师的反馈做了修改,并补充了前言,现在稿件已进入 3 审。我还有一个微信公众号,每个月至少发一篇技术文章。


如果继续当程序员,我想做内容创作类的产品。


按摩完就去游泳,游泳池里只有两个人,除了我还有一个小男孩,他挺可爱的。游完 3 圈我离开游泳池去了桑拿室,那儿只有我一个人,当时我突然想到我还想当程序员,不禁眼睛里有了点泪花。


我很爱哭,被裁后我一个人在家哭了好几次。哭,不是因为被裁,因为以前没被裁的时候,我一个人在家也有哭的时候。桑拿室温度很高,泪花流出来就蒸发了。蒸完桑拿洗完澡打算回家,可是外面在下雨,于是又到按摩椅上躺了一会儿,寻思着等雨停了再回家。第二次按完摩,雨没有停的迹象,我决定淋着雨到外面去打车,16 点到的家,在家吃了一包薯片,看了一会莫言的小说《生死疲劳》,17 点的时候到床上睡觉,18 点的时候才醒,然后到小区门口买了做晚餐的蔬菜,今天晚上煮了面。


WechatIMG128.jpg


这篇文章写了 2 个小时。最后我想说今天还有一件想做的事情没有做成——开车到兴隆湖吹风。


作者:何遇er
来源:juejin.cn/post/7270831230069243961
收起阅读 »

程序员能有什么好出路?

我自己耶 从业10年了,经常在娱文中看到这种的文章,我怀疑是精准推送!! 30岁以上的程序员该何去何从? - 知乎 30岁: 程序员心中永远的痛? 过了30岁,程序员该怎么办? - 阿里云开发者社区 30岁转行程序员晚了吗?分享30岁转行的经历 - Segm...
继续阅读 »

我自己耶


从业10年了,经常在娱文中看到这种的文章,我怀疑是精准推送!!



  1. 30岁以上的程序员该何去何从? - 知乎

  2. 30岁: 程序员心中永远的痛?

  3. 过了30岁,程序员该怎么办? - 阿里云开发者社区

  4. 30岁转行程序员晚了吗?分享30岁转行的经历 - SegmentFault

  5. 30岁后“大龄程序员"应该何去何从? - 脉脉

  6. 程序员:伤不起的三十岁 - 菜鸟教程

  7. 程序员迷茫:30岁以上的“大龄程码农”出路在哪?java码 ... - 掘金

  8. 30岁老程序员迷茫| Laravel - LearnKu


关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。


如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的描述出来了:


30岁了,没啥竞争力,未来何去何从?

出路耶


我从事这个行业,其实是个人挺喜欢编程的,觉得编程是一件挺舒心的事情,所以没有考虑过换行。周围其实有一些同事,离开了这个行当,有一些赚了更多的钱,也有一些日子过的更不舒心,这里不予置评。


我简单的叙述一些可能的出路,这些出路没什么对错的区别,只是在我们人生抉择中,希望你能看到更多的选项。


技术深造


如果你在技术上有优势,这是一条可以走通的路子,未来的方向大致是“架构师”、“技术顾问”等等。这需要你有一些大型项目的经验,所以一些在大型公司就业的程序员,天然的拥有更多的机会。


通常技术深造主要是两部分:



  1. 技术视野,你需要一定的知识广度,对常用技术有深刻的理解,对部分不常用技术也要熟悉。

  2. 技术能力,有的时候,亲自动手能力、解决问题能力会很重要。


项目管理


很多程序员转行做了项目管理,其实在我们的日常工作中,项目管理一直伴随着我们,时长日久,我们对项目管理会变的更熟悉一些。这也造成了一些错觉,让我们觉得项目管理没那么难,“我去我也行”。


但是,项目管理从来不是一项普通的工作,相对于程序员,项目管理人员面临的环境会更加复杂。



  1. 面对客户。有时候,会遇见一些喜欢刁难我们的客户的。

  2. 面对团队。团队也可能不和谐。

  3. 计划乱了、工期排期、风险控制、质量管理、干系人管理等等专业知识。


自由职业


依赖于自己过硬的技术,可以承接一些外包的项目,成为一名自由的外包人员。



  1. 你的人际关系会很重要。周围有一些能打单的朋友,会让你工作的很舒服。

  2. 把事情做好,赢得信赖。

  3. 来自第三方平台的外包项目还是比较坑的,尽量做熟人生意。


跑单


当然,你在行业内可能会认识不少的朋友,他们的手里可能有些业务需要外包人员进行开发,那么拿下这些合同,找到自己朋友里面有时间做私活的人,然后我完成它。



  1. 你的人际关系更为重要。通常,这会给你带来财富。

  2. 做好自己的品牌,赢得认可,那么就有赢得钞票的机会。


插件独立开发者


一个人开发一个应用,然后上架,成功率是很低的。所以依托于平台,做一些平台内的插件,然后依托于平台推广,那么成功的几率会大一些。



  1. 你的技术能力很重要,毕竟没有专门的测试人员进行测试。

  2. 你选择的平台很重要,比如跨境电商、钉钉、微信、谷歌浏览器等等。

  3. 更加重要的是,你要对这个方向感兴趣。


独立开发者


如果你财富自由了,又喜欢编程,可以成为一名伟大的独立开发者,你脑海中的任何想法,都可以通过双手变为现实。



  1. 因为热爱,所以你会有更多的可能。

  2. 能力足够,可以参与开源的基金会,参与一些开源项目。

  3. 如果财富没自由,那也不影响我们在闲暇时间里追逐我们的梦想。


团购


IT行业是一个挺特殊的团体,他们的某些消费习惯趋于雷同,针对这些消费习惯和爱好,做一些团购,相信会赚到不少钱。



  1. 还是人际关系。

  2. 你喜欢做这些事情,从免费到收费循序渐进。

  3. 记住,双赢才能长久,IT行的聪明人是比较多的。


大公司养老团


找个大的,稳定的公司养老,但是也要留好退路,居安思危。


其他


比如炒股、搞理财的、做导游的、创业的……


每个人都会有自己的选择,有的人做好了准备,有的人还懵懵懂懂,2023年的行情如何还未可知,希望能长风破浪吧


作者:襄垣
来源:juejin.cn/post/7194295837265461305
收起阅读 »

教你面试就看出公司是否靠谱!

在⾯试过程中,应聘者可能想要从对公司环境的观察以及对⾯试官的提问当中获取公司相关的信息,以此来判断这家公司靠不靠谱,⾃⼰值不值得去。但这种信息可能会带有⼀定的⽚⾯性,毕竟 应聘者没有在公司实际体验过⼯作的内容,很难通过⼀两次⾯试就能看出公司的实际情况。这些细节...
继续阅读 »

在⾯试过程中,应聘者可能想要从对公司环境的观察以及对⾯试官的提问当中获取公司相关的信息,以此来判断这家公司靠不靠谱,⾃⼰值不值得去。但这种信息可能会带有⼀定的⽚⾯性,毕竟 应聘者没有在公司实际体验过⼯作的内容,很难通过⼀两次⾯试就能看出公司的实际情况。这些细节⼀般只能给予应聘者⼀点有效信息,虽然并不是绝对的准确,综合这些信息判断这家公司是否靠谱。


1 观⾯试流程


靠谱公司都有完善⾯试流程。公司发展到⼀定规模,不会随便找⾯试者过来沟通⼀下就决定要不要这个⾯试者。HR会主动跟应聘者 确定⾯试时间,通过招聘软件或正式邮件跟应聘者确定⾯试时间和⾯试准备内容,在⾯试当天主动跟进应聘者的到场情况,联系好⾯试官随时准备⾯试。如果你发现公司的⾯试流程⼗分混乱,HR 会突然更改⾯试时间,约好⾯试时间后⼜说⾯试官不在、让你第⼆天再过来,或者⾯试时连个会议 室都没有,说明公司招聘的HR⼀点都不专业。可能公司在招聘⼈才的能⼒和经验⼗分有限,或者公 司连⼀套完善的⾯试流程和制度都没有,这样的公司⼀般都是初创企业或⽐较⼩的公司。


2 察工作氛围


在⾯试当天,你可以提前到达公司,仔细稍微观察⼀下办公室⾥的⼯作氛围,看公司的氛围是否严 肃压抑,或者招聘信息 上写着不强制加班,实际个个⼯作压⼒都很⼤,每天被迫加班。如果⼤家 ⼯作的时候都是特别紧绷、各忙各的东⻄,员⼯或领导的神⾊表情都不是特别好,那这家公司有可 能是存在⼀定的内卷现象。加班可能特别严重。


如果不确定⾃⼰观察到的⼯作氛围是否就是真实情况,要如何去印证呢?


⼀般来说,⾯试官都会在⾯试环节询问⾯试者是否愿意接受加班。如果⾯试官主动问起你是否愿意 接受加班,你可以在回答中顺⼿推⾈反问⾯试官:


“⾸先我能理解公司在项⽬紧急的情况下是需要加班的,这个时候我肯定会⽀持公司的⼯作,以公司 的利益为先。(表示理解)我不是⼀个盲⽬加班的⼈,这样会让我的⽣活和⼯作⽐例失衡,反⽽会 影响⾃⼰的⼯作状态。(表明态度)如果是我⾃⼰⼯作效率低造成的加班,我肯定会先努⼒提升个 ⼈能⼒,改进⼯作⽅法,在规定的时间内完成⼯作任务。(提出解决⽅法)最后我想了解⼀下,贵 公司的加班频率是怎么样的,是否有对应的加班费呢?这是很多就业者都会关⼼的问题,希望你能 理解。(借机反问)”


如果⾯试官没有问起加班的问题,你就可以拿刚才在进⼊公司时观察到情况做⽂章,试探⼀下⾯试 官。例如:


(看到氛围差)刚才我进来到时候看到贵公司的同事⼯作都⼗分紧凑,⼯作时都是眉头紧锁、不说 ⼀句话,请问贵公司是对员⼯的⼯作效率有要求么?还是公司普遍存在⾼压或加班⽂化呢? (看到同事们⼯作时有说有笑)刚才进⼊公司的时候,我观察到贵公司的氛围⼗分轻松活跃,我⼗ 分喜欢贵公司的这种氛围,请问你们能做到这种氛围的原因是什么?是因为公司倡导⾼效率办公, 对员⼯是否加班没有硬性要求么? 当然,如果你时间⽐较充裕,或者⾯试的时间是在下午的时间,完全可以在公司附近停留⼀段时 间,等下班时间看看员⼯们是否有准时下班。如果下班时间过了半⼩时都没有员⼯⾛出来,那你对 这家公司的加班⽂化和⼯作压⼒就会有⼀定了解了。


3 ⾯试最后利用提问


套取信息。值不值得去⼀家公司,可以在⾯试的过程中询问公司发展的情况和业务发展阶段,以此判断你⼊职 后需要承担的⻆⾊和负责的⼯作内容。例如可以问⾯试官这些问题:




  • 公司/部⻔发展到⼀个什么样的阶段了?




  • ⽬前部⻔的业务是否有成熟的体系?




  • 这个岗位需要重点解决哪些问题?负责的业务有哪些?




  • 现在的部⻔/团队由哪些部分组成?具体是怎么分⼯的?




  • 岗位的转正/绩效考核是怎样的?有什么要求?




如果对⽅不能清晰介绍现在的业务或团队情况,或者跟你说现在的业务还处于起步阶段,需要你来 进⾏协助,那你就要警醒,这个岗位往往是需要你作为负责⼈或开荒⽜的⻆⾊,甚⾄要你帮公司重 新搭建业务体系,过程可能会⽐较⾟苦。如果薪酬也⽆法给到⽐较⾼的⽔平,说明这家公司可能也 是初创阶段,很多东⻄都不⼀定有保障,你就要慎重考虑这家公司值不值得去了。


4 爱压价?


在⾯试之前,HR⼀般会让⾯试者先填⼀张信息表,表格⾥往往有⼀个“期望薪资 ”这⼀栏。除⾮你 能⼒特别突出,否则公司⼀般会根据你期望薪资范围的下限来作为底薪的依据。假如你开出的底薪 条件并不算⾼,但公司还要继续压你的底薪下限,要么就是领导或⽼板格局太⼩,只想招⼀些廉价 劳动⼒ ,不是真的想吸引⼈才;要么是预算有限,你的要求不算⾼,但别⼈就是开不出这样的薪 酬条件。遇到那种跟你讨价还价的公司,就要学会擦亮双眼,这种公司⼊职后福利待遇多半不会太好。


5 查信息平台



  • 某眼查

  • 某查查

  • 某脉


6 总结


无论何时,求人不如求己,多谋求更多职业出路,才能无惧天下。


作者:JavaEdge在掘金
来源:juejin.cn/post/7292439225065160754
收起阅读 »

流金岁月

web
本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 小聚 “这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



小聚


“这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走了过去,在小白对面落了座。“少爷阔气,今天怎么请我来这里吃饭?”我问出心中疑虑,璇玑地中海自助旋转餐厅,位于广州塔106层,从窗户放眼望去,晚霞与珠江美景浑天然一色,无数高楼一览无遗,万家灯火如星光皆纳入眼前,最要命的是,大众点评人均525/人,还好不是我掏钱。


小白不以为然的笑了笑,“等到核污水传遍全球,你想吃都不敢吃了,人生短短几十年,要懂得及时行乐。而且,咱两的关系也非同一般啊~”。打从有记忆起,我和小白就认识了,年龄跟我差不多,性格跟我差不多,爱好也跟我差不多,好巧不巧,如今他跟我一样也是在广州做IT,所以我们经常联系,关系特别好。“还是你会享受,走吧,拿吃的。”随即我和小白分头寻宝,不一会儿的功夫,桌子上便摆满了芝士波士顿、芝士生蚝、海螺、北极贝、不知名海虾各种海鲜。


我抓起一个芝士生蚝就往里炫,甜中带嫩,入口即化,香味从味蕾刺激我的脑海,当我准备再抓一个,小白轻飘飘的说了句:“我破产了。”我顿时一僵,尴尬地把手收了回去,突然想到了什么,小心翼翼地问,“你不会找我借钱吧?我可没钱哦,这顿AA也行”。


互诉


“你想的倒是挺多,只是这芝士生蚝我才拿了两个,你吃了一个还想再拿,我只能技术性打断你了”,我沉默良久,直至小白把另一只芝士生蚝炫完,露出了他满足的嘴脸


04.png


我才询问道“是你那家自助预约舞蹈室吗?”



破产



小白点了点头,“自从前年疫情过后,收入就一直不太乐观了,且竞争愈来愈激烈,到了暑假最旺期间扣完水电和物业租金,我竟还要倒贴两百元。这样亏本坚持了大概8个月后,合伙人L总她终于决定要解散了。之前成立公司和几位合伙人签订的股份分配书感觉自己犹如走上人生巅峰,随着几家分店加盟,畅想无限美好未来,好日子越来越有,越来越甜。未曾想两年不到,昨日便签了注销公司的文件,现在搞一份副业,付出了大量心血,最后也未必落得一个美好的结局。”


“唉~”我长叹了一声,刚刚吃完这个芝士生蚝,嘴巴有点渴,顺手拿起手边的茶杯抿了一口,这铁观音茶,清淡清香、浓郁扑鼻,喝了一口后自有淡淡回甘。“大局已定,失败乃成功之母,不过这次你收获也不少吧?”之前我也听过小白的舞蹈室副业,记得他刚成立之初前几个月每月都有几万流水,扣除杂七杂八到手还能有个两三千块钱,没想到还是经受不住岁月的考验。


“的确有所收获,L总把舞蹈室的哈曼卡顿音响送我了,这音响听着贼带劲~”。或许是我安慰,小白皱着的双眉似乎舒展几分,“那你呢,你最近在搞什么?”


“我上王者了。”我漫不经心的回了一句。


“王者是谁?”,我白了他一眼,他笑了笑,“没想到你还在玩啊?”。“嗯,现在机制60星就能王者了,有时间便利用下elo机制完成下十年前的梦想,最近练了一手刀妹,真的万军丛中过,片叶不沾身,得心应手。”说罢,我便给他看了看我上王者的截图。


06.jpg


“羡慕你,你还是一如既往的追寻你想要的东西。”小白一边说一边用筷子把半截波士顿龙虾连根拔起,张开血盆大口,吞入嘴中。


“切,就算舞蹈室倒闭也不影响你现在的生活吧,你看你现在一样过的挺滋润啊。”我不屑的说到。


“本来还挺滋润的,但最近压力有点大了,现在每个月发工资后都往家里打几千。”我不解,问道“怎么了,是家里出什么事了吗?”



暴雷



“我家资产暴雷了,你之前也应该知道我家里主要收入吧”。我点了点头,“现在存放到那里的资产因为公司负责人喝酒脑溢血离世了,剩下一大堆债务没法处理,本来每月提供的利息也没有了,本金还被冻结住,现在暂时拿不回来,这些都是我妈跟我说的。”我听完,大吃一惊,不过这种依靠大资金存放赚取高额利息的盈利,本来就是极高风险的,正所谓高风险高回报。但我看着小白失落的神情,也只能安慰说,“有什么要帮忙的跟我说,还有这顿哥请你”。


就等你这句话”。小白乐呵呵的看着我,看着他这副表情,我硬了,拳头硬了。


“现在主要靠负责人的弟弟处理家中事务,他也承诺两年后慢慢还回本金,但是家里主要的收入来源没有了,我也不想家里人看不开,毕竟健康最重要。之前本来就有向家里人打钱,只不过现在翻了一倍,自己花钱也不能像之前大手大脚了,不过请你这顿饭倒是不成问题”。小白见我长舒一口气,好奇问我,“怎么请我吃顿饭好像要你命似的,最近手头很紧吗?”


“你小子,我听你这件事也不是死局,才放心下来,如果承诺两年之内将本金还清并落实到位,也已经算是万幸了。不过我最近手头的确不充裕”。“怎么?之前大礼包十几万全买皮肤去了?”。我摇了摇头,给他打开了我的小鹏APP,“我买了小鹏G9”。


05.jpg

小白看了看我的订单,便看向我,满是不解,“咦,之前你的梦中情车不是宝马5系,连在掘金写小说用的头图都是盗的百度,怎么买了小鹏G9啊,你不是看不上这种牌子的车吗?”


我笑了笑,说“所以这篇小说用的头图是小鹏G9”。“啊?”


“以前我总天真的以为,只要自己不断的存钱,总有一天能够实现自己买下宝马5系的梦想,但梦想终归是梦想,现实毕竟是现实,钱真的很难赚,我不想贷款,不靠父母支持,要自己一边打工一边存钱,凑够这五十万谈何容易。当然,毕竟我也是能力不行,能力配不上自己的野心,转眼之间便差不多到了而立之年,往后的日子还要准备结婚,生娃等等世俗制定好的人生阶梯,之前我老是看懂车帝推文,什么5系,E级,是普通人的天花板,那时候觉得天花板离我触手可及,而现在,我每天睁开眼都觉得天花板离我越来越远。慢慢的我也认清了自己,知道自己是个什么水平,也学会放下,但是,梦想永远会存在我的心中,不会灭去”。


“你爸妈不反对你?”。


“嗐,他们吵上天了,什么电车不安全呀,电车只能买特斯拉啊,小鹏都要倒闭之类的。但是又如何呢,毕竟钱是我的,他们做不了主。我也有试驾过宝马iX3,只是觉得当下,这台车更适合我”。小白听完我的赘述,点了点头,“嗯,我了解你这个人,一旦认定了某些事,别人很难去改变你的想法。”我嘿嘿一笑,“你不也是吗?”


边聊边吃时间总是过得特别快,不一会儿我们两人的桌碟上放满了残骸,堆积如山,刚好服务员经过帮我们更换了新碟子,我们异口同声的说了声谢谢。


进入了短暂的沉默,我率先问小白,“最近工作情况如何?”。


小白听完,擦了擦嘴,表情也正经了起来,“其实今天我主要目的就是来跟你分享一下我最近的经历的,看看你有啥想法。”。


“哦?细说。”



曙光



“前两周我之前的leader让我去他公司面试。”


“你去了?”


“别打岔”。小白没好气的看我一眼,继续说道“一开始我是拒绝的,你也知道,我最近都比较躺平了,拿着一份在广州过的不差的薪资,浑浑噩噩的过着日子。谁知道,那位大佬开口便以35 * 14邀约,我本是性情中人,路见不平便拔刀相助,朋友有困难,我都会鼎力相助,何况这是贵人”。随即,他给我看了看聊天内容。


02.jpg


01.jpg


03.jpg

“听到这个数,我真的是垂死病中惊坐起,手上的switch瞬间丢到一边,立刻屁颠屁颠准备简历了。他说我是人才,其实我知道,贵人的实力,才称得上真正的人才”


“因为base要去深圳,如果薪资涨幅不是太高的话,其实我心里还是不太乐意的,因为现在在公司其实也得到leader青睐了,而且临近年终了,说实话,工作了6年多,之前跳来跳去,为了眼前那点利益,年终奖都没拿过几次,实属心累。但是这次如果薪资到胃,能给到这么高,虽然脉脉都劝别人不要去,但是我还是想尝试一下,毕竟这是更大的平台,为自己以后职业规划着想,现在累点真不算啥。”


我表示理解,“毕竟脉脉都是问就是别去,劝就是不走”。


小白笑着点了点头,“所以我去了,一面二面都过了。”


“牛逼啊,面了什么,分享一下啊~”


“牛逼个啥啊,问的都比较浅,一面印象比较深主要下面几点”。



  • typescript中 interface 和 type 的区别


1. type类型使用范围更广, 接口类型只能用来声明对象
2. 在声明对象时, interface可以多次声明
3. 区别三 interface支持继承的,type不支持
4. interface可以被类实现


  • typescript中 字面量类型


这个我真忘了,回头你自己整理一下


  • redis中如何处理大key


这个我不会,面试官也很nice的讲解了一波


  • 服务器容灾方案


这个我不会,面试官也很nice的讲解了一波


  • service worker作用


这个我不会,面试官也很nice的讲解了一波

“不是,你咋啥都不会啊?”。我超大声的问。


“那你会吗?”小白反问我。


“我也不会”。


“那我就放心了,然后就是问项目细节,我稀里糊涂的答,他稀里糊涂的听,二面的话就是更深入的项目细节了。”


“然后呢然后呢?”


“然后便是昨天的HR终面,HR让我从春秋时期讲起,阐述我的工作经历,我也是乖巧的从百草园讲到三味书屋,其中声情并茂,对细节说到尽情之处,心中也不免感慨之前的辉煌。不过,往事已成云烟。待烟消云散,HR便说重点,问我期望薪资。因为期间我也听HR说现在节奏很紧,加班已是常态,10-10-6稀松平常,我见HR诚心待我,我便以真心待她”。


“你说了多少?”


“98K”


“她同意了?”


“她说你现在的薪资过低,申请不到这个数,如果降低你是否会考虑?”


“那你怎么回?”


“我怎么回,难道你不知道吗?”


“我怎么会知道?”


past lives couldn't ever hold me down, lost love is sweeter when it's finally


"i've got the strangest feelin, this isn't out first time around"


“怎么我的闹钟响了?”突然,周遭的一切突然模糊了起来,旋即进入黑暗。


梦醒


我肌肉记忆般的按停了手机的闹铃,这首歌是我最喜欢的歌,但是自从做了起床闹钟铃声后,我便没有再听过。


我机械般地刷牙洗脸漱口,坐在有点老旧的餐桌椅上,打开昨天晚上下班临时买的方块原味面包,吃了两片,肚子好受一些,看来平日加班还是要按时吃晚饭。


我出门走去,阳光落在我的脸上,小白是我,我亦是我。


作者:很饿的男朋友
来源:juejin.cn/post/7293786784127090715
收起阅读 »

一不小心真删库了

事情经过 就是今天看到web前端的页面因为某个字段是Null,就想给Prisma的某个字段加上一个默认值,就像这样: 感觉也比较常规,当我改完这个字段后,我想的就是直接跑prisma migrate然后去迁移数据库,然后自动把null全部填值了。 md,然后...
继续阅读 »

事情经过


就是今天看到web前端的页面因为某个字段是Null,就想给Prisma的某个字段加上一个默认值,就像这样:


image.png


感觉也比较常规,当我改完这个字段后,我想的就是直接跑prisma migrate然后去迁移数据库,然后自动把null全部填值了。


md,然后因为我觉得这个操作好像再正常不过了,所以我就没看prisma在说啥,直接一路y到底了。


image.png


然后我一看prisma的客户端,茫然->刷几下->看看链接->茫然->再刷几下->啊?->无语凝噎->亚历山大、血压升高、心跳加速。md我的数据那,我的表里怎么一条数据没了,几万条啊?????


所以这个事情就告诉了我们,在做一些操作的时候不要觉得烦就一路y到底,认真看看命令行的反馈,那tm红红的一段 All data will be lost.......


问题追溯


在问题追溯前我先简单介绍下Prisma是啥,以及db操作和migrate操作(就是如果完全不了解数据库Orm的已经可以直接跳过了)。


Prisma


简单的说就是Prisma通过自己创建的特定语言DSL,把自己DSL结构映射为表结构,然后可以通过一系列cli去生成client或者迁移洗数据推拉数据库之类的操作,去帮助开发者在 Node.jsTypeScript 项目中更轻松地处理数据库操作.


Prisma db


db操作pullpushseedexecute,这一系列命令用于将 Prisma schema 中的定义同步到数据库 schema 中。db push 会检查当前数据库并根据您的 Prisma schema 对其进行更改。这是一种直接将 Prisma schema 变更应用到数据库的方法,但不会创建或使用迁移文件。在使用 db类操作 时,不会记录更改的历史(也就是不会生成提交记录,记住这一点很重要)、而我理解迁移历史的意义就是在于回滚、备份、恢复和版本控制,因此适用于原型设计和开发过程中的快速迭代


它的工作流分解一下可以分为以下几步:



  1. 自检Prisma 工具检查当前连接的数据库,并收集有关现有表、列、索引和其他数据库对象的详细信息。

  2. 分析Prisma 分析收集到的数据库信息,并尝试推断当前数据库结构与 Prisma schema 之间的差异。结合推断出的信息,Prisma 将生成相应的数据库操作(例如创建、修改或删除表、列、索引等),执行后,当前已连接的数据库结构和 本地Prisma schema 应该保持一致。

  3. 生成触发器:默认情况下,在将更改应用到数据库架构后,Prisma 会触发生成器(例如,Prisma Client)。不需要手动调用 prisma generate


Prisma migrate


它帮助我们根据 Prisma schema 中的更改自动创建、处理和执行迁移,同时保持数据库 schema 与迁移历史记录同步。migrate dev 通过影子数据库(shadow database)来检测 schema 偏移和冲突,并在需要时提示我们进行数据库重置。也就是说它可以确保在团队中正确地记录和同步数据库更改。


它的工作流分解一下可以分为以下几步:



  1. 重播现有迁移历史:在影子数据库(shadow database)中重播现有迁移历史,以便检测 schema 偏移(编辑或删除的迁移文件,或者对数据库 schema 的手动更改)。

  2. 应用待执行的迁移:将待执行的迁移应用到影子数据库(例如,由同事创建的新迁移)。

  3. 从 Prisma schema 更改中生成新迁移:在运行 migrate dev 命令之前,从我们对 Prisma schema 所做的任何更改中生成新迁移。

  4. 将所有未应用的迁移应用到开发数据库并更新 _prisma_migrations:将所有未应用的迁移应用到开发数据库,并更新 _prisma_migrations 表以记录已执行的迁移。

  5. 生成触发器和迁移历史


Why?


刚刚提到了做db操作是不会生成提交记录的,那到底发生了什么呐,我旁边的老大哥给我复盘了一下,首先A同事对数据库进行了db push操作(因为他正在设计一张表),这时候数据库schema其实已经偏移了但这时候,我并不知道因为没有任何的留存,然后接下来我在没有进行db pull的情况下直接去做了migrate操作,其实在这个时候prisma通过生成的影子数据库已经检测到了有偏移了,但我并没有注意,直接y到底,导致直接进行了reset操作清空了数据。


反思



  • 在团队协作项目中优先使用数据库迁移工具(如 migrate dev)。

  • 在执行关键数据库操作之前,确保同步所有的更改并了解同事所做的修改。

  • 当迁移工具检测到偏移和冲突时,确保花时间了解问题并采取适当措施。

  • 这其实就是缺乏适当沟通和不遵循较好实践带来的后果。在执行关键操作前了解同事的更改和确认是非常重要的。遇到迁移工具检测到的偏移和冲突时,应确保彻底了解问题并采取适当措施解决,而不是忽略警告。


作者:溪饱鱼
来源:juejin.cn/post/7293765765126324259
收起阅读 »

不戴眼镜也可以看清啦!记录我的摘镜经历

大家好,我是 Gopal。好久不见,甚是想念! 本篇文章记录一下今年我做的一个比较大胆的决定——做近视手术。 首先声明一下,本篇文章不是广告,纯分享个人经历,看完这篇文章,至于要不要做视力矫正?怎么做?个人可以根据需要自行决定哈。 我为什么要摘镜 先说一下,我...
继续阅读 »

大家好,我是 Gopal。好久不见,甚是想念!


本篇文章记录一下今年我做的一个比较大胆的决定——做近视手术。


首先声明一下,本篇文章不是广告,纯分享个人经历,看完这篇文章,至于要不要做视力矫正?怎么做?个人可以根据需要自行决定哈。


我为什么要摘镜


先说一下,我的情况。我是初中毕业后开始慢慢近视的,至今的话,也有十来年了,眼镜都配了好多副。有近视的同学应该都知道,近视确确实实带给我们很多的烦恼,我举几个例子:



  1. 看 3D 电影,我得带两副眼镜,一副是自己的,一副是 3D 眼镜。

  2. 游泳的时候,脱掉眼镜,啥也看不到。

  3. 打篮球或者一些激烈的运动,眼镜经常脱落。

  4. 每天都得擦拭眼镜,对于我这种有点强迫症来讲的人,会是每天的工作。

  5. 早上起来第一件事情,找眼镜。

  6. ...


基于以上的种种,摘镜意愿,我个人是比较强烈的。当我听说现在有手术可以进行视力矫正时候,是非常兴奋的。我之后做了相关的调查,我们来看一下视力矫正有哪些方案吧。


视力矫正有哪些方案


这里我会介绍得比较粗糙一些,毕竟我不是专业的,知识都是我从网络中总结得来。


近视原因:我们大部分人的近视都是轴性近视,是由于眼球的前后长度(轴长)延长造成的,可以看以下的图片。在正常眼中,平行光线入眼后在视网膜上形成焦点。而在近视眼中,焦点位置落在视网膜之前,近视眼想看清,就得调整屈光度,使其聚焦在视网膜上。


image.png


眼镜通过镜片调节屈光度,而近视手术的大致有两种方式进行:



  • 第一种,通过角膜手术改变角膜的形态和屈光力,使物象成像到视网膜上。

  • 第二种,更换晶体或向眼内植入补偿晶体,从而达到类似的效果,这种类似于带隐形眼镜。


其中有一个特点就是晶体是有可逆性,激光手术的话,是不可逆的。以上两种并没有说哪种是最优解,需要通过医院检查才能决定。一般来说,角膜屈光手术建议近视度数小于 1000 度,散光小于 600 度。如果因为度数过高、或角膜太薄不能做角膜屈光手术,可以考虑做晶体植入手术。


现在主流的手术方案有以下四种(前面三种都属于角膜手术),具体的这里不展开了,大家有想法可以自行搜索哈。



  • Smart全激光

  • 半飞秒

  • 全飞秒Smile

  • ICL晶体植入术


而我通过检查之后,选择了全飞秒Smile。主要是我度数还好,角膜厚度也足够。


手术前中后


首先约了一家上市的机构做检查,看得出还是比较慎重,各个方面评估能不能做,以及有哪些方案,最后会有专门的人给我讲解。


做完检查之后,我当时就约了隔周去做,因为手术前几天是需要滴眼药水,而且不能佩戴隐形眼镜等。


手术当天,做术前检查,交代注意事项/术后用药、签署同意书等。说实话,我还是比较紧张的,毕竟把眼睛这么重要的部位交给医生。


然后就是手术了,真正的手术时间也就几分钟,当躺在手术室中,一开始我以为医生还在给我做前置检查,没想到几分钟后医生告诉我已经结束了,可以回家了。手术过程滴麻药,基本不会痛的。整个激光过程,眼睛是会有开睑器撑开固定,手术过程眼睛是同时睁开/闭上的。手术过程中,需要在打激光的过程中配合,眼睛保持不动不眨,听好医生指令


手术后,我和我对象就坐地铁回去了,回家的路上,我一直戴着墨镜,随着麻药的失效,会有一种较强的异物感。虽然我知道这是一个正常现象(医生提前已告知,手术后将会在4-6小时内会出现流泪、畏光、白雾感、异物感、酸胀感等刺激症状,其轻重因人而异),但是当时真的挺怕的,毕竟可不是小事。(所以最好手术当天最好还是有人一起去)。


当天下午,我就感觉好很多了。我做的事全飞秒,周六做的手术,周一的时候,我就可以正常上下班了,基本不会影响工作。唯一我感知到的一点点影响就是切换屏幕的时候,聚焦会需要一点点时间,很短,医生说是正常现象。不过中间需要注意用眼,然后注意按照医嘱滴眼药水等。这里我贴几个术后的注意事项,当然你应该根据你的主治医生来:



  1. 术后一周内清淡饮食,吃辛辣刺激食物。

  2. 术后两周内洗头洗脸时不宜将水溅入眼内,切忌揉眼。(手术室给的眼罩睡觉时带上)。

  3. 术后一个月内勿游泳,不要在眼部使用化妆品并避免异物进入眼内。

  4. 术后一个月在室外请戴太阳镜(全激光术后戴三个月),室内、晚上不需佩戴。

  5. 术后三个月内尽量避免剧烈运动,术后一周后可以适量健身运动,但须注意在活动中不要伤及手术眼。

  6. 术后视力恢复是逐步提高的过程,开始阶段看近会感到稍有模糊,雾感。此症状会逐渐消失,视力提高有快有慢,双眼视力恢复会有波动与近视度数及个人对治疗反应差异有关,六个月基本稳定。


在写本文的时候,已经有两个多月了,感觉日常生活没啥影响。在一个月复诊的时候,我的视力一边是 1.2,一边是 1.5。如果说有啥「副作用」的话,有两点。一个是晚上的视力(在光线比较差的地方)会稍微有点差,另外一个看一些光源(比如路灯),会有一点炫光。不过这些术前医生有给我说过,我是有做好心理准备的。


image.png


整体而言,目前为止,我对这次的近视手术还是很满意的。


手术价格以及副作用


这个我相信不少人比较关注,我做的全飞秒,全部下来大概 1.8w 左右(公司跟这家机构有合作,跟我说是打了折扣,我了解了一下,在深圳,这个价格其实差不多)。激光和半分秒会稍微便宜一些。晶体植入是最贵的(大概 3w 多)。具体的还需要根据不同的城市和机构看。据说虽然近视手术简单,但是它那个机器是需要给专利费用的,用一次给一次专利费用。整体上讲,这个还是可以接受的。另外,近视手术不能报医保。


比如价格,我觉得大家最关心的问题是副作用,或者说风险系数。任何手术都是有风险的,包括近视手术。网络上会有人说:



  • 近视手术这么好,为什么医生也还带眼镜?

  • 近视手术这么好,为什么马化腾还带眼镜?

  • ...


image.png


首先,先不否认大家的疑虑,毕竟眼睛这么重要的东西,要在上面动手术,想想都怕。但是,我们也需要辩证的看待这些问题。



  • 近视手术是需要满足一定条件的,不满足的话,医院是不敢给你做的。

  • 近视手术是不会导致你瞎的,从原理上来说,近视手术是眼睛前面部分的手术,不涉及眼部深层组织,比如晶状体、视网膜等,更加不可能致盲。

  • 近视手术存在发生并发症的可能性,可能性大小而已。(别人没有,并不代表自己没有)比如眼干等。虽然现在手术已经很成熟,但是在你决定要做之前,一定要先了解清楚,看你能不能承担这个风险。

  • 近视手术只是当前帮你调整好你的视力,假如你不注意用眼的话,是存在再次近视的风险的。所以一般建议成年之后,度数稳定之后再做这个手术。

  • ...


最后再强调一遍,近视手术是一种改善型的手术,不做其实本质上对于自身身体健康没有影响,如果摘镜意愿强烈的同学可以尝试去了解一下。


写在最后


目前为止,我感受到更多的是摘镜之后给我生活带来的便利,基本算是满足了我摘镜的预期!


我之前一直在想,假如哪天我落在一座荒岛上,估计是活不下去的。因为假如一旦我眼镜坏了,那么我就「看不清」这个世界了。


不过现在医学科技的进步解决了我的这个问题。


作者:Gopal
来源:juejin.cn/post/7293788137662038050
收起阅读 »

曹贼,莫要动‘我’网站 —— MutationObserver

web
前言 本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。 正文 话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子 这么好看的看的小乔,谁看谁不糊,更何...
继续阅读 »

前言


本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。


正文


话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子


image.png
这么好看的看的小乔,谁看谁不糊,更何况曹老板。这天,曹操在浏览网页的时候,无意间发现了周瑜的这个网站,看着美若天仙的小乔,曹操的眼泪止不住的从嘴角流了下来。赶紧将网站上的照片保存了下来。

这个消息最后传到了周瑜的耳朵里,他只是想展示小乔,可不是为了让别人下载的。于是在自己的网站上做了一些预防措施。

为了防止他人直接在网站上直接下载图片,周瑜将右键的默认事件给关闭了,并且为了防止有人打开控制台,并对图片保存,采取了以下方法:


禁用右键和F12键


//给整个document添加右击事件,并阻止默认行为
document.addEventListener("contextmenu", function (e) {
e.preventDefault();
return false;
});

//给整个页面禁用f12按键 keyCode即将被禁用 不再推荐使用 但仍可以使用
document.addEventListener("keydown", function (e) {
//当点了f3\f6\f10之后,即使禁用了f12键依旧可以打开控制台,所以一并禁用
if (
[115, 118, 121, 123].includes(e.keyCode) ||
["F3", "F6", "F10", "F12"].includes(e.key) ||
["F3", "F6", "F10", "F12"].includes(e.code) ||
//ctrl+f 效果和f3效果一样 点开搜索之后依旧可以点击f12 打开控制台 所以一并禁用
//缺点是此网站不再能够 **全局搜索**
(e.ctrlKey && (e.key == "f" || e.code == "KeyF" || e.keyCode == 70))||
//禁用专门用于打开控制台的组合键
(e.ctrlKey && e.shiftKey && (e.key == "i" || e.code == "KeyI" || e.keyCode == 73))
) {
e.preventDefault();
return false;
}
});

当曹操再次想保存小乔照片的时候,发现使用网页的另存了已经没用了。这能难倒曹老板吗,破解方法,在浏览器的右上角进行操作就可打开控制台,这个地方是浏览器自带的,没办法禁用


image.png
这番操作之后,曹操可以选择元素保存那个图片了。周瑜的得知了自己的禁用措施被破解后,赶忙连夜加班打补丁,于是又加了一些操作,禁止打开控制台后进行操作


禁用控制台


如何判定控制台被打开了,可以使用窗口大小来判定


function resize() {
var threshold = 100;
//窗口的外部减窗口内超过100就判定窗口被打开了
var widthThreshold = window.outerWidth - window.innerWidth > threshold;
var heightThreshold = window.outerHeight - window.innerHeight > threshold;
if (widthThreshold || heightThreshold) {
console.log("控制台打开了");
}
}
window.addEventListener("resize", resize);

但是也容易被破解,只要让控制台变成弹窗窗口就可以了


也可以使用定时器进行无限debugger,因为只有在控制台打开的时候debugger才会生效。关闭控制台的时候,并不会影响功能。当前网页内存占用比较大的时候,定时器的占用并不明显。在当前网页占用比较小的时候,一直开着定时器才会有较为明显的提升


  setInterval(() => {
(function () {})["constructor"]("debugger")();
}, 500);

破解方法一样有,在debugger的位置右键禁用调试就可以了。这样控制台就可以正常操作了


image.png
既然有方法破解,就还要做一层措施,既然是要保存图片,那就把img转成canvas,这样即使打开控制台也没办法进行对图片的保存


//获取dom
const img = document.querySelector(".img");
const canvas = document.querySelector("#canvas");
//img转成canvas
canvas.width = img.width;
canvas.height = img.height;
ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
document.body.removeChild(img);

经过一夜的努力,该加的措施都加上了。周瑜心想这下就没办法保存我的小乔了吧。

来到曹操这边,再次打开周瑜的小破站,还想故技重施时,发现已经有了各种显示,最后也没难倒曹操,那些阻碍也都被破解了。但是到保存图片的时候傻眼了,竟然已经不是图片格式了,那就没办法下载了呀。但是小乔真的很养神,曹操心有不甘,于是使用了最后一招,既然没办法下载那就截图,虽然有损画质,但是依旧能看。


得知如此情况的大都督周瑜不淡定了,从未见过如此厚颜无耻之人,竟然使用截图。


006APoFYly1g2qcclw1frg308w06ox2t.gif
话说魔高一尺,道高一丈,周瑜再次熬夜加班进行对网站的优化。于是使用了全屏水印+MutationObserver监听水印dom的方法。即使截图也让他看着不舒服。


MutationObserver


MutationObserver是一个构造函数,接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

它接收一个回调函数,每当监听的dom发生改变时,就会调用这个函数,函数传入一个参数,数组包对象的格式,里面记录着dom的变化以及dom的信息。


image.png
返回的实例是一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象。有三个方法observedisconnecttakeRecords



  • observe接收两个参数,第一个为要监听的dom元素,第二个则是一些配置对象,当调用 observe() 时,childListattributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。配置对象如下:

    • subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false

    • childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false

    • attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false

    • attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

    • attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false

    • characterDate:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

    • characterDateOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false



  • disconnect方法用来停止观察(当被观察dom节点被删除后,会自动停止对该dom的观察),不接受任何参数

  • takeRecords:方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。


该构造函数监听的dom即使在控制台中被更改属性或值,也会被监听到。


使用MutationObserver对水印dom进行监听,并限制更改。


<style>
//定义水印的样式
#watermark {
width: 100vw;
height: 100vh;
position: absolute;
left: 0;
top: 0;
font-size: 34px;
color: #32323238;
font-weight: 700;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-content: space-evenly;
z-index: 9999999;
}
#watermark span {
transform: rotate(45deg);
}
</style>

<script>
//获取水印dom
const watermark = document.querySelector("#watermark");
//克隆水印dom ,用作后备,永远不要改变
const _watermark = watermark.cloneNode(true);
//获取水印dom的父节点
const d = watermark.parentNode;
//获取水印dom的后一个节点
let referenceNode;
[...d.children].forEach((item, index) => {
if (item == watermark) referenceNode = d.children[index + 1];
});
//定义MutationObserver实例observe方法的配置对象
const prop = {
childList: true,//针对整个子树
attributes: true,//属性变化
characterData: true,//监听节点上字符变化
subtree: true,//监听以target为根节点的整个dom树
};
//定义MutationObserver
const observer = new MutationObserver(function (mutations) {
//在这里每次坚挺的dom发生改变时 都会运行,传入的参数为数组对象格式
mutations.forEach((item) => {
//这里可以只针对监听dom的样式来判断
if (item.attributeName === "style") {
//获取父节点的所有子节点,因为时伪数组,使用扩展运算符转以下
[...d.children].forEach((v) => {
//判断一下,是父节点里的那个节点被改变了,并且删除那个被改变的节点(也就是删除水印节点)
if (item.target.id && v == document.querySelector(`#${item.target.id}`)) {
v.remove();
}
});
//原水印节点被删除了,这里使用克隆的水印节点,再次克隆
const __watermark = _watermark.cloneNode(true);
//这里的this指向是MutationObserver的实例对象,所以同样可以使用observe监听dom
//监听第二次克隆的dom
this.observe(__watermark, prop);
//因为水印dom被删除了,再将克隆的水印dom添加到原来的位置 就是referenceNode节点的前面
d.insertBefore(__watermark, referenceNode);
}
});
});
在初始化的时候监听初始化的水印dom
observer.observe(watermark, prop);
</script>



这样,每当对水印dom进行更改样式的时候,就会删除该节点,并重新添加一个初始的水印dom,即使突破重重困难打开开控制台,用户也是无法对dom 进行操作。


视频转Gif_爱给网_aigei_com.gif


隔天曹操再次打开网页,发现网页上的水印,心里不足为惧,心想区区水印能难倒自己?操作到最后却发现,不论如何对水印dom进行操作,都无法改变样式。虽说只是为了保存图片,但是截图有着这样水印,任谁也不舒服呀。曹操大怒,刚吃了两口的饭啪的一下就盖在了桌子上......


20230508094549_33500.gif
然而曹操不知道的是,在控制台中,获取dom节点右键是可以只下载获取的那个节点的......


image.png


结尾


文章主要是以鬼畜恶搞的方式讲述了,如何禁止用户打开控制台(还有重写toSring,consloe.log等一些方法,但我并没有没有实现,所以这里并没有写上),并且如何使用MutationObserver构造函数来监听页面中的dom元素。其实大多情况下并没有这方面的项目需求,完全可以当扩展知识看了。


写的不好的地方可以提出意见,虚心请教!


作者:iceCode
来源:juejin.cn/post/7290862554657423396
收起阅读 »

在高德地图实现后期效果

web
介绍 最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图...
继续阅读 »

介绍


最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图层中。
Effect-PointsLayer2.gif


后期特效其实有点类似照片的后期滤镜处理,是对渲染结果的二次处理,可以实现发光、模糊、色调调整、镜头暗角、模拟环境光遮蔽等各种效果,为了方便理解,下面的讲解我将以辉光效果为例,学会了一种其他效果思路类似。


方案调研


Three官方提供了非常简单的方法实现后期特效,貌似仅需要完成以下两个步骤就可以完成我们想要的需求,代码也非常清晰简单:


import * as THREE from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing//UnrealBloomPass.js'
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'

...

// 1.在图层初始化完成后,创建效果合成器
onLayerInit(){
const { scene, camera, renderer } = this

const renderScene = new RenderPass(scene, camera)

// 后期泛光特效
bloomPass = new UnrealBloomPass(new THREE.Vector2(this.container.clientWidth, this.container.clientHeight), 1, 0, 0)
bloomPass.threshold = params.threshold
bloomPass.strength = params.strength
bloomPass.radius = params.radius

composer = new EffectComposer(renderer)
// 以下代码会遮盖地图
composer.addPass(renderScene)
composer.addPass(bloomPass)
}

// 2.更新合成器
onRender () {
if (composer) {
composer.render()
}
}

本以为这样做就可以开心收工了,燃鹅事情并没有那么简单,把这套方案移入高德的GLCustomLayer中,出现了这样的情况,后期效果直接把地图底图盖住了。
Untitled.png


出现这种情况的原因是实现辉光效果而编写的着色器,它会直接修改整个画面的alpha通道而导致透明效果丢失,因此需要单独修改UnrealBloomPass.js。


然而光是这样还不够,经过各种尝试,仍无法直接在GLCustomLayer上解决地图被遮盖的问题,后来咨询了高德地图开发团队的技术大佬,他给我的建议是后期效果层独立展示,于是就沿着这个思路进行了第二轮尝试。
Untitled 1.png


这里面有几个关键步骤是必须的:



  1. 修改UnrealBloomPass着色器代码

  2. 使用输出通道new OutputPass()置于特效通道的后面

  3. 在customLayer图层中,每次渲染就更新特效合成器EffectComposer


由于我这边是不希望之前开发的可视化图层做太多的修改去迁就这个后期效果的,也有对性能较差的终端机器优雅降级的考虑,索性把后期效果独立为EffectLayer层,以方便灵活地装载或剥离,最终实现了这个效果。


Effect-POI3dLayer1.gif


实现步骤




  1. 修改 UnrealBloomPass.js,由于这个文件在npm包中不能随意修改,我另外写了一个UnrealBloomPass1 继承并覆盖了UnrealBloomPass的方法


    import * as THREE from 'three'
    import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'

    class UnrealBloomPass1 extends UnrealBloomPass {
    constructor (resolution, strength, radius, threshold) {
    super(resolution, strength, radius, threshold)
    }

    getSeperableBlurMaterial (kernelRadius) {
    ...
    fragmentShader:
    `#include <common>
    varying vec2 vUv;
    uniform sampler2D colorTexture;
    uniform vec2 invSize;
    uniform vec2 direction;
    uniform float gaussianCoefficients[KERNEL_RADIUS];

    void main() {
    float weightSum = gaussianCoefficients[0];
    vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
    float alphaSum;
    for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
    float x = float(i);
    float w = gaussianCoefficients[i];
    vec2 uvOffset = direction * invSize * x;
    vec4 sample1 = texture2D( colorTexture, vUv + uvOffset );
    vec4 sample2 = texture2D( colorTexture, vUv - uvOffset );
    diffuseSum += (sample1.rgb + sample2.rgb) * w;
    alphaSum += (sample1.a + sample2.a) * w; //
    weightSum += 2.0 * w;
    }
    // gyrate: overwrite this line for alpha pass
    // gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
    gl_FragColor = vec4(diffuseSum/weightSum, alphaSum/weightSum);
    }`

    })
    }
    }

    export { UnrealBloomPass1 }



  2. 编写EffectLayer


    import * as THREE from 'three'
    import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
    import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
    import { UnrealBloomPass1 } from '../plugins/three/examples/jsm/postprocessing/UnrealBloomPass.js'
    import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'
    import _ from 'lodash'

    class EffectLayer {

    // 此处省去一些内部变量

    _style = {
    // 光照强度阈值
    threshold: 0.0,
    // 泛光强度
    strength: 1.0,
    // 泛光半径
    radius: 1.5
    }

    /**
    * 创建一个实例
    * @param {Object} config
    * @param {Layer} config.layer 目标图层,要求是Layer的相关子类
    * @param {Number} [config.zIndex=120] 图层的层级
    * @param {EffectStyle} [config.style] 后期特效的配置项
    */

    constructor (config) {
    const conf = _.merge(this._conf, config)
    this._style = _.merge(this._style, conf.style)

    if (!conf.layer.scene || !conf.layer.camera) {
    console.error('缺少场景和相机')
    return
    }
    this.init()
    }

    init () {
    this.createLayer()
    this.addEffect()
    }
    }



  3. 创建自定义图层customLayer


    createLayer () {
    const canvas = document.createElement('canvas')
    this._customLayer = new AMap.CustomLayer(canvas, {
    zooms: [3, 22],
    zIndex: this._conf.zIndex,
    alwaysRender: true
    })

    this._canvas = canvas
    }



  4. 创建特效合成器


    addEffect () {
    const { scene, camera, container, renderer, map } = this._conf.layer
    const { clientWidth, clientHeight } = container

    // 创建渲染器
    const effectRender = new THREE.WebGLRenderer({
    canvas: this._canvas,
    alpha: true,
    antialias: false,
    stencil: false,
    depth: false
    })
    // renderer.setClearColor(0xff0000);
    effectRender.autoClear = false
    effectRender.setSize(clientWidth, clientHeight)

    // 后期效果
    const renderScene = new RenderPass(scene, camera)

    // 后期辉光特效
    const bloomPass = new UnrealBloomPass1(new THREE.Vector2(clientWidth, clientHeight), 1, 0, 0)
    bloomPass.clear = false

    // 输出通道
    const outputPass = new OutputPass()
    outputPass.clear = false

    this.updatePass()

    const composer = new EffectComposer(effectRender)
    composer.addPass(renderScene)
    composer.addPass(bloomPass)
    composer.addPass(outputPass)

    this._composer = composer
    this._bloomPass = bloomPass

    this._customLayer.render = function () {
    if (composer) {
    // 每次渲染就更新特效合成器
    composer.render()
    }
    }

    map.add(this._customLayer)
    }

    updatePass() {
    const {_bloomPass} = this
    if (_bloomPass) {
    _bloomPass.threshold = this._style.threshold
    _bloomPass.strength = this._style.strength
    _bloomPass.radius = this._style.radius
    }
    // 添加其他特效通道...
    }



  5. 使用EffectLayer


    //之前编写的可视化图层
    const layer = new GLlayers.POI3dLayer({
    map: getMap(),
    zooms: [10, 22]
    })

    layer.on('complete', (layer) => {
    let effectLayer = new GLlayers.EffectLayer({
    layer: layer, //把图层传入effectLayer
    style:{
    threshold: 0.0,
    strength: 1.0,
    radius: 0.5,
    }
    })
    })




注意:以上方案three.js版本为0.157, 该版本对three/example/jsm/postprocessing目录中的后期效果通道相关文件做了较多调整,如果是用之前的three.js版本,修改内容可能有所不同。



至此我们就可以在之前的可视化图层基础上,加入几行代码实现辉光效果,以下是挑选一部分图层加上EffectLayer之后的效果,肉眼可见还是有很明显区别的。当然在使用过程中也发现了个别图层原有的问题需要做进一步优化。


Effect-BorderLayer1.gif


Effect-PointsLayer1.gif


Effect-SpriteLayer1.gif


Effect-cakeLayer1.gif


待解决问题


使用独立图层展示后期特效层有个明显缺点,无法关联默认基本图层的场景要素深度信息,最主要的影响是高德的建筑白模图层和自定义可视化图层的远近遮挡关系会丢失,导致可视化图层永远在最前面。比如下面这个城市主要道路的辉光效果,这个是需要后面花时间去解决的,写这篇文章的时候又找到几个方案,有时间再试一把,毕竟上面留给我的时间不多了。


Effect-FlowlineLayer2.gif


相关链接


three.js后期处理


three.js效果合成器文档和示例


实现模型材质局部辉光效果和解决辉光影响场景背景图显示的问题


Three.js带Depth实现分区辉光


作者:gyratesky
来源:juejin.cn/post/7293788726235365426
收起阅读 »

谢谢你,成都

成都,带不走的,只有你..... XXXX说,一千个人就有一千个成都 有人说成都是天府之国,有人说成都是西南柬埔寨。 我的成都,有爱有梦,谢谢你。 还记得5年前,2017年7月份,我从一个不知名的二本财经学校里的不知名专业毕业,凛冽的西北风最终没能把我留在兰州...
继续阅读 »

成都,带不走的,只有你.....


XXXX说,一千个人就有一千个成都


有人说成都是天府之国,有人说成都是西南柬埔寨。


我的成都,有爱有梦,谢谢你。


还记得5年前,2017年7月份,我从一个不知名的二本财经学校里的不知名专业毕业,凛冽的西北风最终没能把我留在兰州,大三一次偶然的寒假工,让我才意识到原来冬季并不都是白雪茫茫,原来冬季也可以绿意盎然,于是那年校招,我忽略了所有的北方校招,只参加了所有来自南方的企业。


在经历了当时看起来几次的高光时刻后,我最终通过校招加入了广东一家大型集团公司。


7月的兰州,需要穿件毛衣才可以让自己看起来非常从容,那时人生第一次坐飞机,原谅来自农村的孩子真的见识短浅,穿过白云的世界真的从来没有见识过,飞机上的2小时一刻也没有睡着,光顾着去拍下那高空中的世界。


下了飞机后,一股热气流扑面而来,让我一度怀疑广东人天天在蒸桑拿,走在路上没10分钟,我已经汗流浃背,非常羡慕走在我旁边的一个靓妹,小短袖,小短裤,甚是凉快。


IMG_1485.jpg


加入公司后,公司为每个新人分配了导师,导师带我们做常规开发任务,同时为我们能在这复杂的社会上立足而答疑解惑。


由于是传统企业,技术相对而言非常老旧,经常需要维护Jquery项目,唯独一个在当时看起来眼前一亮的项目是内部HR使用的考勤平台,在我们几个90后一再的说服下,领导同意使用React开发,但是我们必须保证进度.....


广东传统企业大部分都配备有员工餐厅,员工宿舍,因此毕业的那两年从来没有经历颠沛流离的生活,每天下班后会和当时校招进去的几个同期生先去糖水店吃一下,周末流窜于广州的大街小巷,上下九步行街.....,生活看起来无忧无虑。


直到2019年下半年,11月份的广州依然是那么炎热,桑拿生活还是一样在继续,周一正常打卡上班,上班没多久,领导给我发消息,“来下办公室呢”,进去后,领导还是和往常一样,在自己狭小略显简陋的办公室里沏了一壶茶,接下来的谈话让我一生难忘,因为这看似关心,实则裁员的谈话最终导致了我抑郁症的发作,久久没有平复.....


领导直言,公司在某个大的战略方向上已经折腾了3年,老板和投资人不愿意继续下去,开发人员只会留下部分,他可以替我申请,更换部门,去市场部就职....,一想起同期的一个市场部门同事说市场部会喝酒是一项工作技能,而我,生理本能排斥那样的生活.....


那天的广州,阳光非常热烈,但维护没有一丝丝光线照进我的工位,走出领导办公室的我,瘫坐在自己工位上,身体发软,毫无力气,喘气困难......


很多思绪开始在我脑海里出现,如果我当时选择了北方的校招,如果我曾经选择了跳槽,如果......随之而来的情绪将我击倒,没人给我说过我该怎样面对这样突如其来的变化.....


IMG_1487.jpg


之后的日子,总感觉广州的天没有了曾经那般蓝天白云的美丽,只觉得它刺眼且讨厌,每天总感觉身体加倍的疲惫,每一天都在思考的是到底做错了什么会导致现在的模样,当然也没有了当初那般谈笑风生的快乐.....,与同期生朋友的谈话聊天也变的心不在焉,乏味无趣,像那迷失的候鸟一般,找不到任何的归宿。


就这样持续了一个月,我还是鼓起勇气去了医院,果然是重度抑郁,医生建议回家修养,可我那从来走出过一亩三分地的父母又如何才能理解抑郁症的由来呢?


就这样我每天如同僵尸一样摇摇晃晃,来回走动.....


硬撑到年底,我向领导提了离职,也该离开我喜欢的南方城市了.....


回到北方,熟悉的父母,熟悉的冬日白雪,妹妹也在过年时嫁为人妻。


IMG_1484.jpg


之后的事情,想必大家都清楚,2019年年底,我们在新闻里看着新冠肺炎的消息,那么遥远,却没想到短短一个月,那么迅速就来到了我们身边。


我沉浸在父母和家乡的冬日里,实则是在逃避人生的下一段旅程,每当夜幕降临,我一遍又一遍打开Boss,脉脉,听闻着他人的故事,也探寻着自己的故事。


过年之后,到了4月,我知道,我得再次出发,就像当时刚毕业时那种对未来的美好期待一般。


最终我选择了离家不远,但又迎合了我的南方情结的成都。


在1个月内我找到了能够安身立命的前端开发工作,我开着我的小船,再次起航了......


我深深迷恋着夏日成都街头小巷的冰粉,也和很多成都朋友一样,一周必须吃一顿火锅来证明自己是个新成都人,饭桌上的折耳根永远是外地人百思不得其解的食物,川西的四季永远那么耀眼动人,令人向往。


IMG_1471.JPG


IMG_1472.JPG


IMG_1473.JPG


IMG_1474.JPG


2023年的5月,我也在这个巴适的城市和一位姑娘牵手走入了婚姻殿堂,我们都深爱着彼此,也深爱着我们几乎每周都要前往的川西。


如今的自己,也褪去了曾经的那份稚嫩,迷茫,渐渐在这片地方扎根,生长。


如今的工作也是朝九晚六,三天居家,二两办公室,wlb的日子才真正让人觉得人生值得走一遭。


平日里刷刷freecodecamp和油管上的技术视频,练习练习算法防止脑瓜壳生锈,和香港技术官探讨探讨技术,好让项目不至于那么难以维护,偶尔也用工作用的全栈技术(MERN)帮老婆写一两个小网站,谁让四川的男人都是耙耳朵呢。


作者:Aoda
来源:juejin.cn/post/7292576188017967116
收起阅读 »

有财务自由的思维,才能实现财务自由!

前两天在洋哥、竹子姐以及渡心总等大佬的带领下,第一次体验了穷爸爸富爸爸的作者研发的现金流游戏,收获颇丰! 游戏规则说明 心灵创富 现金流游戏分为三步: 一局游戏,时间两个小时;总结分享时刻;以及,最最重要的结合自己的生活,复盘自己关键时间点的选择,是否是符合财...
继续阅读 »

前两天在洋哥、竹子姐以及渡心总等大佬的带领下,第一次体验了穷爸爸富爸爸的作者研发的现金流游戏,收获颇丰!


游戏规则说明


心灵创富 现金流游戏分为三步:


一局游戏,时间两个小时;总结分享时刻;以及,最最重要的结合自己的生活,复盘自己关键时间点的选择,是否是符合财务自由的决策。


首先,说一下明面上的游戏规则:每个人都选择一张身份卡,这张身份卡决定了你的工资,还有每个月的现金流。你的身份可能是小学老师、飞机驾驶员、医生等等,他们月工资和现金流(每月结余)各不一样。


老鼠圈,所有玩家没实现财务自由之前都在老鼠圈。


游戏过程中,通过投掷骰子,可以有五种操作:



  1. 市场风云:变幻莫测的市场,之前不值钱的突然变得值钱,之前值钱的东西也会突然变得不值钱;还有金融政策可能会随时调整,借钱利率忽高忽低等。

  2. 小买卖:开小店挣小钱等。

  3. 大机会:买卖股票、房产等能带来大额现金流的操作。

  4. 意外支出:生孩子、买球拍、买游艇等。

  5. 领工资:领取一个月的现金流,比如:月工资:3300,月支出:2100,则每月现金流:3300-2100=1200。


整个游戏的目标分为两个阶段:第一个阶段,突破老鼠圈,实现财务自由;第二个阶段,实现梦想。


怎么实现财务自由?股票、房产等带来的非工作收入超过你每个月的支出,就算破圈了,实现了财务自由,游戏进入了下一个阶段。


第一次玩游戏,没有人完成第二阶段,就不说了。


二、游戏


带领银行家:竹子


玩家:海子、木川、天雨、Feli、YY、伍六七


身份:小学老师


月工资:3300 $


月支出:2190 $


月现金流:1110 $



一开始选择身份的时候,虽然想选一个工资高的,但是也没有那么强烈,只是不想选工程师了,毕竟现实中是程序员,游戏中想换个身份活一回。所以,也就比较随意选择了工资偏低的小学教师。


整场游戏,我的运气非常不好。本来月现金流就不高,结果第二轮就生了个娃,后面又陆续抽中了额外支出,让本就没钱的我雪上加霜。


唯一称得上的是机会的就是:可以以 5000 $ 购买 10 亩荒地。但是当时没有考虑到可以向银行贷款,也没有考虑到可以向其他玩家借款,最终没有购买。


没有考虑到向外部借钱的一部分原因是第一次玩这个游戏,不知道(忘记了)这个规则,另一个原因也是自己平时生活中也是这样一个人,除非迫不得已不向他人借钱。


第一次玩这个游戏,本着要恪守:10% - 20% 的钱投资高风险的产品(如股票),50% 的钱投资低风险的产品(当前最低价的股票、高收益的房产等)。


所以,我也做了两笔投资: 1000 $ 让木川代持的基金, 500 $ 让海子代持的股票。这让我在运气不好的一生中有一定的概率能破圈,虽然最后这两笔投资没有兑现,但是这两笔投资本身,我认为是没有问题的。


另外,太守规则。银行家一开始说的规则是不让给其他人提供决策建议,否则罚款。后来才知道,是可以向其他人提供付费资讯服务的,这种是不受惩罚的。


前两轮 FYY 想向我咨询决策建议,我直接就拒绝了。但是一个是给他人提供建议,可以给其他人好感,可以链接其他人。另外一个,就算有罚款,咨询的人也可以给予相应的补偿。这样就可以相互链接,相互成全。也应该思考,不破坏规则的情况下的有哪些选择。


整场游戏中,印象最深刻的一笔交易是:5000 $ 购买 10 亩荒地


我最终是没有购买,当时第一是看手里的现金,不满足购买要求。


第二是觉得这是一个可以搏一搏的机会,但是手里的现金不满足给自己制定的 10% -20% 投资高风险产品的 rule。


所以,问了在场的玩家,是否有需要这个机会的?拍卖 1000 $。最终,只有海子出价 500 $。当时,我认为这个机会价值还是挺大的,海子本质上是一个愿意花钱投资的人,所以拒绝了。


之后思考,海子是一个愿意投资机会的人,但是他当时手里有几笔房子的贷款,而且现金也不充足,所以没有购买这个机会。我只考虑了海子的性格,没有考虑他当时手里的资金,所以,错失了这笔交易。


游戏和自己现实生活中的关系


什么才算财务自由?现实中很多人说有多少多少百万,有说 500 W的,有说 1000 W的。但是这个游戏告诉我们:只要你的非工作收入超过了你的支出,你就实现了财务自由。跟你手里有多少现金是没有关系的。


唯一的目标就是,增加你的非工作收入,减少你的支出,让你的非工作收入超过你的支出。


这个游戏带给我的收获和启发


人这一生,有的人运气好,能碰到很多次大机会。有的人运气不好,可能一生也没什么机会。


不管如何,你需要在一开始就制定好你做决策的依据。这个依据就是你手里的现金流以及你能承受的风险。


你不应该拿手里的大部分现金去投资一个低收益率高风险的产品,但是也不应该守着一大笔资金不做任何投资。


这个决策依据能够让你在没机会的时候,不至于很快破产。也能让你在有机会的时候,能够快速收获第一桶金,实现财务自由。


对我现实的改变



  1. 正在整理自己的权益表和资产负债表,慢慢减少不必要的支出。

  2. 正在实践做自己的个人 IP,增加自己的非工作收入。

  3. 游戏中的小买卖、大机会,在现实中去寻找这样的信息。游戏中,大家都知道且能知道是小买卖还是大机会,但是现实中,你可能不知道什么是小买卖,什么是大机会。

  4. 在心里植入增加非工作收入,减少支出,实现财务自由这样的理念。


应该去践行的



  1. 与现实决策点结合

  2. 早日争取第一桶金

  3. 结果导向

  4. 自己反思,反思自己的不足

  5. 持续来,每次来会有不同的体验,牌面不同,选择不同

  6. 应该多做利他的事情,资源链接

  7. 玩家之间可以互相赋能

  8. 有资源的时候可以投资机会,购买机会

  9. 整理自己的资产负债表

  10. 多分享,清晰自己的认识

  11. 重复玩,玩到财务自由

  12. 映射现实中,改善自己的财务状态

  13. 运气不好,心态也要好,积极链接他人,才能保住底线的情况下,去创造更大的成功机会


作者:程序员伍六七
来源:juejin.cn/post/7293477092259201059
收起阅读 »

自研框架跻身全球 JS 框架榜单,排名紧随 React、Angular 之后!

web
前言 终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语...
继续阅读 »

前言


终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语法的全面支持,使得 Strve 在代码智能提示和代码格式化方面更加友好,进一步提高了开发效率。


介绍


相信有些小伙伴没有听说过 Strve 到底是什么,那我这里就大体介绍一下。


Strve 是一个可以将字符串转换为视图(用户界面)的 JavaScript 库。Strve 不仅易于使用,而且可以灵活地拆解不同的代码块。使用模板字符串开发用户界面,主要是利用 JavaScript 的能力,只关注 JavaScript 文件。Strve 又是一个易用性的 JavaScript 框架,它提供了很多实用的功能与生态工具。


我们可以通过一些简单的示例来了解 Strve 的使用方法。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Strve.js</title>
</head>

<body>
<script src="https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full.prod.js"></script>
<script>
const { html, setData, createApp } = Strve;
const state = {
count: 0,
};

function add() {
setData(() => {
state.count++;
});
}

function App() {
return html`<h1 onClick=${add}>${state.count}</h1>`;
}

const app = createApp(App);
app.mount('#app');
</script>
</body>
</html>

在上述代码中,我们通过引入 Strve 库,并使用 createApp 方法创建了一个 App 组件,然后通过 mount 方法挂载到页面上,这里的 App 组件就是通过模板字符串来定义的。这样就可以在 JS 代码中编写用户界面,是不是很方便呢?我们发现,在模板字符串中,我们使用 ${} 来引用数据,并且使用 onClick 方法来绑定事件。这样就可以实现一个计数器的功能。


除了这种简单的示例,Strve 还支持很多复杂的功能,我们可以使用 JSX 语法来编写组件,也可以使用函数式组件来编写组件,还可以使用组件来编写组件,甚至可以编写一些自定义的组件。


如果想了解更多关于 Strve 的信息,稍后可以到文章末尾处查阅官方文档。


性能评估


我们既然发布了 Strve,那么肯定需要对其性能进行评估,我们评估的工具就用js-framework-benchmarkjs-framework-benchmark 是什么?我们这里就简单介绍下 js-framework-benchmark,它是一个用于比较 JavaScript 框架性能的项目。它旨在通过执行一系列基准测试来评估不同框架在各种场景下的性能表现。这些基准测试包括渲染大量数据、更新数据、处理复杂的 UI 组件等。通过运行这些基准测试,可以比较不同框架在各种方面的性能优劣,并帮助开发人员选择最适合其需求的框架。js-framework-benchmark 项目提供了一个包含多个流行 JavaScript 框架的基准测试套件。这些框架包括 Angular、React、Vue 等。每个框架都会在相同的测试场景下运行,然后记录下执行时间和内存使用情况等性能指标。通过比较这些指标,可以得出不同框架的性能差异。这个项目的目标是帮助开发人员了解不同 JavaScript 框架的性能特点,以便在选择框架时能够做出更加明智的决策。同时,它也可以促进框架开发者之间的竞争,推动框架的不断改进和优化


在评估之前,我们必须要了解 js-framework-benchmark 中有两种模式。一种是 keyed,另一种是 non-keyed。在 js-framework-benchmark 中,"keyed" 模式是指通过给数据项分配一个唯一标识符作为 "key" 属性,从而实现数据项与 DOM 节点之间的一对一关系。当数据发生变化时,与之相关联的 DOM 节点也会相应更新。而 non-keyed 模式是指当数据项发生变化时,可能会修改之前与其他数据项关联的 DOM 节点。


因为 Strve 支持keyed模式,所以我们将使用此模式来评估 Strve 的性能。


对以下操作进行了基准测试:



  • 创建行:页面加载后创建 1,000 行的持续时间(无预热)。

  • 替换所有行:替换表中所有 1,000 行的持续时间(5 次预热迭代)。

  • 部分更新:对于具有 10,000 行的表,每 10 行更新一次文本(进行 5 次预热迭代)。

  • 选择行:响应单击该行而突出显示该行的持续时间。 (5 次预热迭代)。

  • 交换行:在包含 1,000 行的表中交换 2 行的时间。 (5 次预热迭代)。

  • 删除行:删除具有 1,000 行的表的行的持续时间。 (5 次预热迭代)。

  • 创建多行:创建 10,000 行的持续时间(无预热)

  • 将行追加到大型表:在包含 10,000 行的表中添加 1,000 行的持续时间(无预热)。

  • 清除行:清除填充有 10,000 行的表的持续时间。 (无热身)

  • 就绪内存:页面加载后的内存使用情况。

  • 运行内存:添加 1,000 行后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 替换内存:点击 5 次创建 1000 行后的内存使用情况。

  • 重复清除内存:创建并清除 1,000 行 5 次后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 启动时间:加载和解析 javascript 代码以及渲染页面的持续时间。

  • 持续交互:灯塔指标 TimeToConstantlyInteractive:悲观 TTI - 当 CPU 和网络都非常空闲时。 (不再有超过 50 毫秒的 CPU 任务)

  • 脚本启动时间:灯塔指标 ScriptBootUpTtime:解析/编译/评估所有页面脚本所需的总毫秒数

  • 主线程工作成本:灯塔指标 MainThreadWorkCost:在主线程上工作所花费的总时间包括样式/布局等。

  • 总字节权重:灯塔指标 TotalByteWeight:加载到页面中的所有资源的网络传输成本(压缩后)。


对于所有基准测试,都会测量持续时间,包括渲染时间。


因为js-framework-benchmark是一个自动化测试的工具,只需要符合标准的代码就可以进行测试。Strve 支持 JSX 语法,所以我们将使用 JSX 语法来编写测试代码。


import { setData, createApp } from 'strve-js';
import { buildData } from './data.js';

let selected;
let rows = [];

function setRows(update = rows.slice()) {
setData(
() => {
rows = update;
},
{
name: TbodyComponent,
}
);
}

function add() {
const data = rows.concat(buildData(1000));
setData(
() => {
rows = data;
},
{
name: TbodyComponent,
}
);
}

function remove(id) {
rows.splice(
rows.findIndex((d) => d.id === id),
1
);
setRows();
}

function select(id) {
setData(
() => {
selected = id;
},
{
name: TbodyComponent,
}
);
}

function run() {
setRows(buildData());
selected = undefined;
}

function update() {
for (let i = 0; i < rows.length; i += 10) {
rows[i].label += ' !!!';
}
setRows();
}

function runLots() {
setRows(buildData(10000));
selected = undefined;
}

function clear() {
setRows([]);
selected = undefined;
}

function swapRows() {
if (rows.length > 998) {
const d1 = rows[1];
const d998 = rows[998];
rows[1] = d998;
rows[998] = d1;
setRows();
}
}

function TbodyComponent() {
return (
<tbody>
{rows.map((item) => (
<tr class={item.id === selected ? 'danger' : ''} data-label={item.label} key={item.id}>
<td class='col-md-1'>{item.id}</td>
<td class='col-md-4'>
<a onClick={() => select(item.id)}>{item.label}</a>
</td>
<td class='col-md-1'>
<a onClick={() => remove(item.id)}>
<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>
</a>
</td>
<td class='col-md-6'></td>
</tr>
))}
</tbody>

);
}

function MainBody() {
return (
<fragment>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Strve-keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='run' onClick={run}>
Create 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='runlots'
onClick={runLots}
>

Create 10,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='add' onClick={add}>
Append 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='update'
onClick={update}
>

Update every 10th row
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='clear' onClick={clear}>
Clear
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='swaprows'
onClick={swapRows}
>

Swap Rows
</button>
</div>
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<component $name={TbodyComponent.name}>{TbodyComponent()}</component>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden='true'></span>
</fragment>

);
}

createApp(() => MainBody()).mount('#main');

以下页面就是将进行基准测试的页面:


01.png


我们大体看下测试过程,我们将使用动图来展示页面效果,这样会觉得更加直观。


02.gif


最终,Strve 通过了压力测试!


08.png


基准测试结果


既然我们通过测试,我们就需要提交到js-framework-benchmark官方项目中,进行综合评估,与全球其他框架进行比较。


我们提交的 PR 在 2023 年 9 月 18 号被作者合并了。


03.png


在接下来的时间里,作者进行了一系列的测试。最终,Chrome 118 版本于上周发布,并在 GitHub 上公布了官方的测试结果。


04.png


我们打开下面的网址,看下 Strve 的官方测试结果:


krausest.github.io/js-framewor…


经过查询,全球 JavaScript 框架榜单中共有 142 个框架。


性能测试基准分为三类:



  • 持续时间

  • 启动指标

  • 内存分配


【持续时间】


在此测试基准中,Strve 平均值 1.42,排名第 90 位。


React、Angular 和 Vue,平均值分别为1.401.381.20,分别排名第 85 位、第 83 位和第 51 位。



平均值越小,排名则越靠前。颜色越绿代表越优。



05.png


【启动指标】


在此测试基准中,Strve 平均值 1.07


React、Angular 和 Vue,平均值分别为 1.681.801.30



平均值越小,排名则越靠前。颜色越绿代表越优。



06.png


【内存分配】


在此测试基准中,Strve 平均值 1.33


React、Angular 和 Vue,平均值分别为 2.462.821.86



平均值越小,排名则越靠前。颜色越绿代表越优。



07.png


新特性


我们在上面的测试中,可以看到 Strve 性能表现非常不错。


这次我们发布的大版本号为 6.0.2,我们将这个具有里程碑意义的大版本命名为 Strve6,而 “Strve6,从芯出发!” 这个口号正是 Strve6 的核心理念。这一版本象征着我们从底层技术出发,致力于为用户提供更优质、更高效的开发体验。


此次版本我们在性能与体验之间做了权衡。在源码层面,我们将普通 Diff 算法升级为 双端 Diff 算法,大大提升了性能。另外,我们在用户体验层面也做了很大的改进。


这里,我们提到了双端 Diff 算法,我们在面试中经常提到这个概念,但是很少用到实际项目中去。那么,为了更好地理解双端 Diff 算法如何提高性能,我们来看一个关于 Strve 简单的示例。


我们来遍历一个数组,并且每次点击按钮,往数组头部中添加一个元素。


【普通 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,都会重新渲染整个列表。这样是肯定耗损浏览器性能的。


09.gif


【双端 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li key=${todo}>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,仅添加必要的元素,而不是重新渲染整个列表。这是因为我们在每个列表项中添加了 key 属性,并且这个 key 是唯一的。key 这个特殊的 attribute 主要作为 Strve 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。只要标签类型与 key 值都相等,就说明当前元素可以被复用。


10.gif


热门话题


文章接近尾声,让我们来回顾一下最近社区的几个热门话题。



  1. 为什么要开发这个框架?初衷是什么?


答:其实,我的动机特别简单,完全受 JSX 语法的影响。刚接触 JSX 语法的时候,就被它那种魔法深深地吸引住了,可以在 JS 中写 HTML。所以,我就想我自己可不可以也搞一个类似 JSX 语法的库或者框架呢!一方面可以锻炼自己的代码能力,另一方面体验开发框架的整个流程,也方便我以后更全面的学习其他框架(Vue.js、React.js 等)。


做自己喜欢的事情是特别有意义的!



  1. 为什么选择 Strve 作为框架的名字?


答:Strve 最初定位是可以将字符串转换为视图(用户界面)的 JavaScript 库,所以是由 StringView 两个单词缩减组成的新单词。



  1. 跟前端热门框架比较,是想超过它们吗?


答:不是,我主要是想学习一下前端热门框架的实现原理,然后自己实现一个框架。有一句话说得好:“只有站在巨人的肩膀上才能望得更远!”。



  1. 记得之前也写过登上框架榜单的文章,这次为什么还要写?


答:之前,Strve 测评的模式是使用"non-keyed"。现在,Strve 新的版本支持"keyed"模式,所以,我重新写了一篇文章,来介绍 Strve 的新特性。



  1. Strve 6.0.2 版本发布,普通 Diff 算法升级为双端 Diff 算法,可以简单讲下双端 Diff 算法的概念吗?


答:双端 diff 算法就是头尾指针向中间移动,分别判断头尾节点是否可以复用,如果没有找到可复用的节点再去遍历查找对应节点的下标,然后移动。全部处理完之后要对剩下的节点进行批量的新增和删除。



  1. Strve 是个 JavaScript 库还是 JavaScript 框架?


答:首先,我们来看下框架与库有什么区别?库更多是一个封装好的特定的集合,提供给开发者使用,而且是特定于某一方面的集合(方法和函数),库没有控制权,控制权在使用者手中,在库中查询需要的功能在自己的应用中使用,我们可以从封装的角度理解库;框架顾名思义就是一套架构,会基于自身的特点向用户提供一套相当于叫完整的解决方案,而且控制权的在框架本身,使用者要找框架所规定的某种规范进行开发。Strve 可以是框架,因为 Strve 提供了路由、插件等生态工具;Strve 也可以是库, 因为 Strve 可以单独作为一个渲染库。



  1. Strve 你还要继续维护下去吗?


答:是的,我还会继续维护下去,因为我也想学习下去,也希望能帮助到更多前端开发者。


关于


Strve 我是从 2021 年下半年开始开发,到现在也快两年了。在这两年中,从一个之前只会 调用 API 的码农,到现在可以独立开发一个框架,让我收获了很多。学习了如何去分析一个框架的实现原理,也学习了如何去设计一个框架。



Strve 源码仓库:github.com/maomincodin…


Strve 中文文档:maomincoding.gitee.io/strve-doc-z…



如果大家觉得 Strve 还不错,麻烦帮我点下 Star 吧,谢谢!


结语


感谢各位读者的阅读,希望本文能对你有所帮助,如果喜欢本文,欢迎点赞,欢迎关注!


最后,分享一段话给大家:



很多时候


不是有希望才去坚持


而是在坚持的过程中慢慢看到希望


我们都是在暗夜里赶路的人


纵使满身疲惫也不肯轻言放弃


愿你所坚持的东西


终有一天反过来拥抱你



作者:前端历劫之路
来源:juejin.cn/post/7293786784127025179
收起阅读 »

【JavaScript】【表达式和运算符】instanceof

web
前言 在JavaScript中,判断变量的类型,常常使用的是typeof运算符 typeof的痛点: 所有的引用类型结果都是 Object; 空值null的结果也是Object; 为此,引入 instanceof 一、instanceof 1.1 作用 ...
继续阅读 »

前言


在JavaScript中,判断变量的类型,常常使用的是typeof运算符


typeof的痛点



  • 所有的引用类型结果都是 Object

  • 空值null的结果也是Object


image.png


为此,引入 instanceof


一、instanceof


1.1 作用



  • 用于判断某个实例是否属于某构造函数

  • 在继承关系中,用来判断一个实例是否属于它的父类型或祖先类型的实例


1.2 使用




  • 语法object instanceof constructor




  • 参数



    • object:某个实例对象

    • constructor:某个构造函数




  • 示例:




// 类
class Maomi {} // 定义类
let fuLai = new Maomi() // fuLai是Maomi类的实例对象
fuLai instanceof Maomi // true

// 时间
new Date() instanceof Date // true

// 构造函数
function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi // true

// 函数
function getMaomi() {}
getMaomi instanceof Function // true

1.3 涉及的构造函数



  • 基础类型:String、Number、 Boolean、 Undefind、Null、Symbol

  • 引用类型:Object(Array、RegExp、Date、Function...)


1.3 实现原理


instanceof 的内部实现机制是:通过判断对象的原型链上是否能找到对象的 prototype,来确定 instanceof 返回值。


function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L)
return true;
L = L.__proto__;
}
}

代码释义

① L表示对象实例,R表示构造函数或者父类型实例

② 取R的显式原型,取L的隐式原型

③ 循环遍历,进行判断②中的两个值是否相等,相等返回true,不相等继续查找L的原型链


instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。



  • 示例:


function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi

观察fulai.__proto__SetMaomi.prototype的结构:

image.png


image.png



注意点fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 trueSetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false



二、instanceof产生继承关系


function Cat(name,age,type){
this.name = name;
this.age = age;
this.type = type;
}
function YingDuan(name,age,type,sex){
Cat.call(this,name,age,type);
this.sex = sex;
}
YingDuan.prototype = new Cat(); // 这里改变了原型指向,实现继承
var yd = new YingDuan("有鱼",2,"金渐层","男"); //创建了英短对象yd
console.log(yd instanceof YingDuan); // true
console.log(yd instanceof Cat); // true
console.log(yd instanceof Object); // true

下面为了直观的观察,我就不采用循环的方式,直接一个一个的打印出来:



  • yd instanceof YingDuan:

    image.png

  • yd instanceof Cat:

    image.png

  • yd instanceof Object:

    image.png


三、注意问题



  1. fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 true
    SetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false

  2. instanceof 用于判断对象类型,但以下情况的结果都为false,请注意。


console.log(Number instanceof Number)  // false
console.log(String instanceof String) // false
console.log(null instanceof Object) // false,null不具有任何对象的特性,也没有__proto__属性

参考



作者:旺仔小猪
来源:juejin.cn/post/7293348107517001739
收起阅读 »

解决Android卡顿性能瓶颈的深度探讨

在移动应用开发中,Android卡顿是一个常见但令人讨厌的问题,它可能导致用户体验下降,甚至失去用户。本文将深入探讨Android卡顿的原因,以及如何通过代码优化和性能监测来提高应用的性能。 卡顿现象 卡顿是指应用在运行时出现的明显延迟和不流畅的感觉。这可能包...
继续阅读 »

在移动应用开发中,Android卡顿是一个常见但令人讨厌的问题,它可能导致用户体验下降,甚至失去用户。本文将深入探讨Android卡顿的原因,以及如何通过代码优化和性能监测来提高应用的性能。


卡顿现象


卡顿是指应用在运行时出现的明显延迟和不流畅的感觉。这可能包括滑动不流畅、界面响应缓慢等问题。要解决卡顿问题,首先需要了解可能导致卡顿的原因。


卡顿原因


主线程阻塞


主线程负责处理用户界面操作,如果在主线程上执行耗时任务,会导致界面冻结。


public void doSomeWork() {
// 这里执行耗时操作
// ...
// 下面的代码会导致卡顿
updateUI();
}

内存泄漏


内存泄漏可能会导致内存消耗过多,最终导致应用变得缓慢。


public class MyActivity extends AppCompatActivity {
private static List<SomeObject> myList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
// 向myList添加数据,但没有清除
myList.add(new SomeObject());
}
}

过多的布局层次


复杂的布局层次会增加UI绘制的负担,导致卡顿。


<RelativeLayout>
<LinearLayout>
<ImageView />
<TextView />
<!-- 更多视图 -->
</LinearLayout>
</RelativeLayout>

大量内存分配


频繁的内存分配与回收,会导致性能下降,发生卡顿。


// 创建大量对象
List<Object> objects = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
objects.add(new Object());
}

优化策略


使用异步任务


避免在主线程上执行耗时操作,使用异步任务或线程池来处理它们。
协程提供了一种更清晰和顺序化的方式来执行异步任务,并且能够很容易地切换线程



// 创建一个协程作用域
val job = CoroutineScope(Dispatchers.IO).launch {
// 在后台线程执行后台任务
val result = performBackgroundTask()

// 切换到主线程更新UI
withContext(Dispatchers.Main) {
updateUI(result)
}
}

// 取消协程
fun cancelJob() {
job.cancel()
}

suspend fun performBackgroundTask(): String {
// 执行后台任务
return "Background task result"
}

fun updateUI(result: String) {
// 更新UI
}

在此示例中,我们首先创建一个协程作用域,并在后台线程(Dispatchers.IO)中启动一个协程(launch)。协程执行后台任务(performBackgroundTask),然后使用withContext函数切换到主线程(Dispatchers.Main)来更新UI。


内存管理


确保在不再需要的对象上及时释放引用,以避免内存泄漏。


public class MyActivity extends AppCompatActivity {
private List<SomeObject> myList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
myList.add(new SomeObject());
}

@Override
protected void onDestroy() {
super.onDestroy();
myList.clear(); // 清除引用
}
}

精简布局


减少不必要的布局嵌套,使用ConstraintLayout等优化性能的布局管理器。


<ConstraintLayout>
<ImageView />
<TextView />
<!-- 更少的视图层次 -->
</ConstraintLayout>

使用对象池


避免频繁的内存分配和回收。尽量重用对象,而不是频繁创建新对象。
使用对象池来缓存和重用对象,特别是对于复杂的数据结构。


// 使用对象池来重用对象
ObjectPool objectPool = new ObjectPool();
for (int i = 0; i < 10000; i++) {
Object obj = objectPool.acquireObject();
// 使用对象
objectPool.releaseObject(obj);
}

卡顿监测


Android提供了性能分析工具,如Android Profiler和Systrace,用于帮助您找到性能瓶颈并进行优化。


为了更深入地了解应用性能,您还可以监测主线程处理时间。通过解析Android系统内部的消息处理日志,您可以获取每条消息的实际处理时间,提供了高度准确的性能信息。


for (;;) {
Message msg = queue.next();

final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what)
;
}

msg.target.dispatchMessage(msg);

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}

当消息被取出并准备处理时,通过 logging.println(...) 记录了">>>>> Dispatching to" 日志,标志了消息的处理开始。同样,在消息处理完成后,记录了"<<<<< Finished to" 日志,标志了消息的处理结束。这些日志用于追踪消息的处理时间点。


这段代码对 Android 卡顿相关内容的分析非常重要。通过记录消息的处理起点和终点时间,开发者可以分析主线程消息处理的性能瓶颈。如果发现消息的处理时间过长,就可能导致卡顿,因为主线程被长时间占用,无法响应用户交互。


Looper.getMainLooper().setMessageLogging(new LogPrinter(new String("MyApp"), Log.DEBUG) {
@Override
public void println(String msg) {
if (msg.startsWith(">>>>> Dispatching to ")) {
// 记录消息开始处理时间
startTime = System.currentTimeMillis();
} else if (msg.startsWith("<<<<< Finished to ")) {
// 记录消息结束处理时间
long endTime = System.currentTimeMillis();
// 解析消息信息
String messageInfo = msg.substring("<<<<< Finished to ".length());
String[] parts = messageInfo.split(" ");
String handlerInfo = parts[0];
String messageInfo = parts[1];
// 计算消息处理时间
long executionTime = endTime - startTime;
// 记录消息处理时间
Log.d("DispatchTime", "Handler: " + handlerInfo + ", Message: " + messageInfo + ", Execution Time: " + executionTime + "ms");
}
}
});

这种方法适用于需要深入分析主线程性能的情况,但需要权衡性能开销和代码复杂性。


结语


Android卡顿问题可能是用户体验的重要破坏因素。通过了解卡顿的原因,采取相应的优化策略,利用性能分析工具和消息处理日志监测,您可以提高应用的性能,使用户体验更加流畅。卡顿问题的解决需要不断的监测、测试和优化,通过不断发现与解决卡顿问题,才能让应用更加流畅。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7293342627813425167
收起阅读 »

Shell 命令奇淫技巧,就是有点短

1、在任意目录之间快速移动 你发现自己要在两个或更多目录之间频繁移动,一会切换到这里,一会切换到那里,来回跳转。这些目录之间隔得还挺远,反复输入冗长的路径让人疲惫不堪。 使用内建命令 pushd 和 popd 来管理目录栈,轻松地在目录之间切换。下面是一个简单...
继续阅读 »

1、在任意目录之间快速移动


你发现自己要在两个或更多目录之间频繁移动,一会切换到这里,一会切换到那里,来回跳转。这些目录之间隔得还挺远,反复输入冗长的路径让人疲惫不堪。


使用内建命令 pushd 和 popd 来管理目录栈,轻松地在目录之间切换。下面是一个简单的示例:


$ cd /tmp/tank
$ pwd
/tmp/tank

$
pushd /var/log/cups
/var/log/cups /tmp/tank

$
pwd
/var/log/cups

$
ls
access_log error_log page_log

$
popd
/tmp/tank

$
ls
empty full

$
pushd /var/log/cups
/var/log/cups /tmp/tank

$
pushd
/tmp/tank /var/log/cups

$
pushd
/var/log/cups /tmp/tank

$
pushd
/tmp/tank /var/log/cups

$
dirs
/tmp/tank /var/log/cups

栈是一种后进先出的结构,这两个命令也正是这么做的。如果对一个新目录使用 pushd,那么它会将前一个目录压入栈中。当使用 popd时,它会弹出栈顶保存的当前位置,切换到新的栈顶目录。使用这些命令更改位置时,会从左到右输出目录栈中的值,对应于栈中自顶向下的顺序。


如果使用 pushd 时没有指定目录,那么它会交换栈顶的两个目录的位置,这样就可以重复使用 pushd 命令来实现两者之间的切换。cd命令也能够达到相同效果。


如果不记得目录栈中都有哪些目录,可以使用内建命令 dirs 按照从左到右的顺序显示。加上 -v 选项后,显示形式更形象。


$ dirs -v
0 /opt/yongheng
1 /opt/yongheng/Shell
$

数字可用来调整栈内目录的位置。pushd +1 会将编号为 1 的目录置为栈顶(并切换到该目录)并将其他目录下压。


$  pushd +1
/opt/yongheng/Shell /opt/yongheng

$
dirs -v
0 /opt/yongheng/Shell
1 /opt/yongheng

要想看到类似于栈的目录列表,但又不希望出现编号,可以使用 -p选项。


# dirs -p                                                                                              /opt/yongheng/Shell                                                                                    /opt/yongheng     

2、重复上一个命令


你刚刚输入了一个又长又麻烦的命令,其中包含了冗长的路径名和一堆复杂的参数。现在需要重新执行该命令。难道还得再输入一次?


这个问题有两种解决方法。第一种方法只需要在提示符下输入两个惊叹号,然后 bash 就会显示并重复执行上一个命令。例如:


$  cd /opt/                                                                                            $ !!                                                                                                   cd /opt/  

另一种(更现代的)方法是使用箭头键。按上箭头键会回滚到执行过的上一个命令。如果找到了需要的命令,按下 Enter 键就可以(再次)执行该命令。


3、参数重用


重用上一个命令很简单,使用 !! 就行了,但你需要的未必总是整个命令。如何只重用最后一个参数呢?


用 !$ 指明上一个命令中的最后一个参数。!:1 表示第一个参数,!:2 表示第二个参数,以此类推。


多个命令使用相同的文件名为参数是司空见惯的事情。最常见的场景之一就是程序员编辑源代码文件,然后编译、再编辑,再编译……有了 !$,事情就方便多了。如下:


$ vi /some/long/path/name/you/only/type/once
...
$ gcc !$
gcc /some/long/path/name/you/only/type/once
...
$ vi !$
vi /some/long/path/name/you/only/type/once
...
$ gcc !$
gcc /some/long/path/name/you/only/type/once
...
$

明白其中的意思了吗?这不仅省去了大量的键盘输入,还避免了错误。如果编译时输错文件名,那编译的可就不是刚刚编辑好的源代码文件了。有了 !$,就可以始终得到刚刚用过的文件名。要是想重用的参数位于命令行内部,可以使用带编号的 !: 命令来获取。


4、安全第一,命令测试


一不小心就会输错字符。 (不信你瞧!)即便是简单的 bash 命令,由此带来的后果也非常严重:你会移动错或删错文件。如果再加上模式匹配,结果更让人心跳,因为模式中的输入错误会导致南辕北辙的结果。小心谨慎的用户会怎么做?


可以使用命令历史特性和键盘便捷方式来重复参数,无须从头输入,因此能够减少输入错误。如果要用到棘手的模式来匹配文件,先用echo 测试一下模式能否正常匹配,然后再用 !$ 进行实际操作。例如:


$ ls
ab1.txt ac1.txt jb1.txt wc3.txt

$
echo *1.txt
ab1.txt ac1.txt jb1.txt

$
echo [aj]?1.txt
ab1.txt ac1.txt jb1.txt

$
echo ?b1.txt
ab1.txt jb1.txt

$
rm !$
rm ?b1.txt
$

echo 是检查模式匹配结果的一种方法。一旦确信结果符合预期,就可以将模式用于实际命令。这里我们要删除有特定名称的文件,没人愿意在这种事上犯错。


作者:博学谷_狂野架构师
来源:juejin.cn/post/7187268796753641531
收起阅读 »

压缩炸弹,Java怎么防止

一、什么是压缩炸弹,会有什么危害 1.1 什么是压缩炸弹 压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不...
继续阅读 »

一、什么是压缩炸弹,会有什么危害


1.1 什么是压缩炸弹


压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。


以下是安全测试几种经典的压缩炸弹


graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)

A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。


压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。


压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。



1.2 压缩炸弹会有什么危害


graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px

压缩炸弹可能对计算机系统造成以下具体的破坏:




  1. 资源耗尽:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。




  2. 磁盘空间耗尽:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。




  3. 系统崩溃:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。




  4. 拒绝服务攻击:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。




  5. 数据丢失:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。





重要提示:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。



二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹


2.1 个人有没有方法可以检测压缩炸弹?


有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:


graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)

A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px



  1. 安全软件和防病毒工具(推荐):使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。




  2. 文件大小限制:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。




  3. 文件类型过滤:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。




2.2 Java怎么防止压缩炸弹


在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:


graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px



  1. 解压缩算法的限制:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。




  2. 设置解压缩操作的资源限制:使用Java的java.util.zipjava.util.jar等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。




  3. 使用安全的解压缩库:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。




  4. 文件类型验证和过滤:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。




  5. 异步解压缩操作:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。




  6. 安全策略和权限控制:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。




2.2.1 使用解压算法的限制来实现防止压缩炸弹


在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制来实现防止压缩炸弹。


先来看看我们实现的思路


graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L

style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

实现流程说明如下:




  1. 首先,通过给定的 file 参数创建一个 ZipFile 对象,用于打开要解压缩的 ZIP 文件。




  2. zipFileSize 变量用于计算解压缩后的文件总大小。




  3. 使用 zipFile.entries() 方法获取 ZIP 文件中的所有条目,并通过 while 循环逐个处理每个条目。




  4. 对于每个条目,使用 entry.getSize() 获取条目的未压缩大小,并将其累加到 zipFileSize 变量中。




  5. 如果 zipFileSize 超过了给定的 size 参数,说明解压后的文件大小超过了限制,此时会调用 deleteDir() 方法删除已解压的文件夹,并抛出 IllegalArgumentException 异常,以防止压缩炸弹攻击。




  6. 创建一个 File 对象 unzipped,表示解压后的文件或目录在输出文件夹中的路径。




  7. 如果当前条目是一个目录,且 unzipped 不存在,则创建该目录。




  8. 如果当前条目不是一个目录,确保 unzipped 的父文件夹存在。




  9. 创建一个 FileOutputStream 对象 fos,用于将解压后的数据写入到 unzipped 文件中。




  10. 通过 zipFile.getInputStream(entry) 获取当前条目的输入流。




  11. 创建一个缓冲区 buffer,并使用循环从输入流中读取数据,并将其写入到 fos 中,直到读取完整个条目的数据。




  12. 最后,在 finally 块中关闭 fos 和 zipFile 对象,确保资源的释放。




实现代码工具类


import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class FileBombUtil {

/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/

public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;

/**
* 文件超限提示
*/

public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";

/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/

public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}

fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);

byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}

}

/**
* 递归删除目录文件
*
* @param dir 目录
*/

private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}

}

测试类


import java.io.File;

/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class Test {

public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}

}

三、总结


文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。


文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。


总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。


在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:





  1. 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。




  2. 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。




  3. 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。





作者:独爱竹子的功夫熊猫
来源:juejin.cn/post/7289667869557178404
收起阅读 »

那个热血澎湃的少年,他居然顶不住了!

感谢你阅读本文 那些我曾经觉得无比坚毅,勇敢,富有激情的朋友,几年后我从他们的言谈中看到了疲惫和妥协,不知是在城市的钢筋混泥土中穿梭太久而身心疲惫,还是在那午夜12点的霓虹灯下路过而感到失落。 昨天中午准备午休的时候,学弟发了个消息给我,说在深圳太累了,压力很...
继续阅读 »

感谢你阅读本文


那些我曾经觉得无比坚毅,勇敢,富有激情的朋友,几年后我从他们的言谈中看到了疲惫和妥协,不知是在城市的钢筋混泥土中穿梭太久而身心疲惫,还是在那午夜12点的霓虹灯下路过而感到失落。


昨天中午准备午休的时候,学弟发了个消息给我,说在深圳太累了,压力很大,想离开深圳了,我瞬间惊讶了,“这小伙曾经可是个卷王啊,在学校时在实验室可以从早上7点可以干到晚上12点呀,把我们这些老人卷回宿舍了他都还不回,现在怎么会累了?”



不过后面我仔细想了一想,怎么就不会累呢?


我们的同龄人中,不乏那种家境特别优越的人,有些人去上大学一个月家里可以给四五千的生活费,如果能谈上女朋友,额外还要加两千,毕业后也是一帆风顺的。


不过大部分人家境是十分普通甚至贫穷的,所以无论上学时期还是进入社会参加工作,都没有什么选择的余地,在没有资源,没有背景的情况下,生活都是过得比较艰难的。


就像网上很流行的那句话,“躺又躺不平,卷又卷不赢”。


特别是当下就业环境这么严峻,工作强度又挺强,工资还TM低,所以年轻人的积极性都被打消了,不过能有工作还好,主要是有很多人找不到工作。


可能你在网上看到统计毕业生分平均薪资过万,就业率90%等等类似的报道,但是这与实际情况大相径庭。


我在网络上也认识不少全国各地的高校学生,加上和现实中的很多朋友了解到,现在就业十分困难,无论什么专业,简历基本都石沉大海。


可能你会说,是他自己不努力,在学校没有好好学,所以出来找不到工作怪谁啊,我觉得这样想是不行的,不要以为自己所得到的都是自己努力换来的,比你努力的人多了去了,可能只是你运气稍微好了一点,站在是时代背景好一点,如果把你换到当前这个环境下,你可能还不如别人!


总是有很多成功的人说自己曾经多么难,但是自己依旧不放弃,最后走上了人生的金字塔尖,然后来呵斥现在年轻人怕吃苦,怕累!


但是实际上现在年轻人所经受的苦累并不是他们那时候所受的苦累能比拟的,身体的累尚能恢复,但是心理上的累却压得人喘不过气。


现在的年轻人毕业后好不容易进入一个公司,满怀激情去干,但是现实总是重拳出击,在繁华的都市拿着微薄的收入,加着加不完的班,熬着熬不完的夜,他们比谁都努力,比谁都上进,但是由于他们没有资源,没有阶梯,所以举步维艰。


在面对房价问题,工作压力,裁员,工资缩水等局面,那个坚毅,勇敢,富有激情的人,手心已经不知道攥出了多少汗!


现在的整个经济形势应该是最难的时刻了,那些你看似光鲜亮丽的人,实际上没有你想象的那么好,大家都比较难。


无论是大城市还是小城市,目前都是比较困难的,所以不要以为在大城市卷不动了回小城市就能轻轻松松,哪怕你回到村里都是很卷的。


在这样的局势下我们能苟住就先苟住,不要去触碰风险。


除了工作外我们应该还要注重生活,只有好好生活我们的身心才能更加健康开朗,工作永远是做不完的,而生活是越过越少的。


保持居安思危的意识,越是艰难的时候越要提起精神,积极探索新事物,别固步自封,因为可能现在看似没用的东西在关键时刻能曲线救国,实现人生的第二春!


今天的分享就到这里,感谢你的观看,我们下期见!


作者:刘牌
来源:juejin.cn/post/7293411651407904803
收起阅读 »

学弟说他面试时被问到了HashMap,差点就遭老罪了

面试官:小伙子,了解HashMap吗? 学弟:哎呦,你干嘛~ 真的问这个呀.... 面试官:呦,练习时长两年半?待会答不上来,你可就遭老罪喽! 那行吧,那开始吧...唱跳rap篮球🏀...... 一、HashMap的底层结构 说一下你理解的HashM...
继续阅读 »

面试官:小伙子,了解HashMap吗?


学弟:哎呦,你干嘛~ 真的问这个呀....


面试官:呦,练习时长两年半?待会答不上来,你可就遭老罪喽!



在这里插入图片描述



那行吧,那开始吧...唱跳rap篮球🏀......



一、HashMap的底层结构



说一下你理解的HashMap底层?



hashMap是由数值和链表组合而成的数据结构,存储为key Value形式。


在java7中叫entry,数据形式为数组+链表。java8中叫Node,数据形式为数组+链表+红黑树(当链表长度大于8时转为红黑树)。


每一个节点都会保存自身的hash、key、value、以及next属性指向下一个节点。


在这里插入图片描述


二、为什么使用数组+链表数据结构



你刚提到了使用数组+链表,可以讲讲为什么使用这个结构吗?



HashMap内部使用数组来存储键值对,这个数组就是 HashMap 的主体。


在这里插入图片描述


在数组中存储的每个位置上,可能会有多个键值对,这些键值对通过链表的形式链接在一起。


在这里插入图片描述


使用数组+链表的数据结构是为了解决散列表中的键冲突问题。在散列表中,每个键都会被映射到一个桶中,但是不同的键可能会被映射到同一个桶中,这种情况被称为键冲突。


为了解决键冲突问题,HashMap 采用了链表的形式将所有映射到同一个桶中的键值对链接在一起,这样就可以通过遍历链表来查找指定键的值当链表长度过长时,查找效率就会下降,因此在链表长度超过一定阈值(8)后,HashMap会将链表转换为红黑树,以提高查找效率


同时,数组的优势在于支持通过下标快速访问元素,因此HashMap可以将每个桶的位置映射到数组的一个元素上,通过下标访问该元素即可访问到对应的链表或红黑树


我们都知道:数组的查询效率很高,添加和删除的效率低。链表的查询效率很低,添加和删除的效率高。


因此:使用数组加链表形式,不仅可以解决散列表中的键冲突问题,且数组的查询效率高、链表的添加和删除效率高。结合在一起,增删查效率都很高


请添加图片描述



嗯,确实不错。不愧是练习时长两年半的程序员.....



三、数组+链表+红黑树



你刚说数组+链表+红黑树,什么情况下会转化红黑树?什么情况下转数组呢?



链表中元素过多,会影响查找效率,当其个数达到8的时候转换为红黑树。红黑树是平衡二叉树,在查找性能方面比链表要高


当红黑树的节点数小于等于6时,红黑树转换为链表,是为了减少内存开销


需要注意的是:将链表转换为红黑树、红黑树转换为链表的操作会影响HashMap的性能,因此需要尽可能避免这种情况的发生。同时,当HashMap中的元素数量较小时,不会出现链表转换为红黑树的情况,因此使用HashMap时,可以考虑在元素数量较少的情况下使用HashMap,以提高性能。


在这里插入图片描述


四、头插法和尾插法



说一下什么是头插法,什么是尾插法?



哇,这不是为难我胖虎吗?啥是头插法?啥是尾插法?


在这里插入图片描述


4.1、头插法


顾名思义,头插法就是新增元素时,放在最前面嘛。


举个栗子🌰,楼主画了一个简单的框框。用来表示原有存储顺序依次为1、2、3的数组。
在这里插入图片描述


假设现在加入了一个4,如果使用头插法,就会变为4123。


在这里插入图片描述


4.2、尾插法


同样道理,尾插法就是新增元素时,放在最后面。


还是原有存储顺序依次为1、2、3的数组。
在这里插入图片描述
假设现在加入了一个4,如果使用尾插法,就会变为1234。


在这里插入图片描述



头插法为什么要调整为尾插法呢?



为什么头插法要调整为尾插法?这是个好问题!!!
请添加图片描述


java7中使用头插法,新来的值会取代原有的值,原有的值就顺推到链表中。在这种情况下,引用关系可能会乱掉,严重会造成死循环。java8使用尾插法,把元素放到最后,就不会出现这种情况。


五、HashMap如何运算存储索引



向一个hashMap中存入数据时,如何知道数据放在哪个位置呢?



当向一个hashMap中存入数据时,会先根据key的哈希值决定放在数组中哪个索引位置。



Hash公式:index = HashCode(Key) & (Length - 1)



如果数组中该索引位置是空的,直接将元素放入,如果该索引位置已经存在元素了,就根据equals方法判断下已有的元素是否和我们新放入的元素是同一个,如果返回true是同一个,则覆盖掉。不是同一元素则在原有元素下面使用链表进行存储


每个元素都有一个next属性指向下一个节点(数组+链表)


    /**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
.........
}

六、HashMap初始化、扩容



嗯,你知道HashMap默认初始化大小是多少吗?还有它的扩容?



HashMap默认初始化容量大小是16,最大容量为2的30次方,负载因子是0.75


在这里插入图片描述


扩容时,会把原有数组中的值取出再次hash到新的数组中(长度扩大以后,Hash的规则也随之改变),因此性能消耗也相对较大。


当HashMap中的元素数量超过负载因子(默认为 0.75)乘以数组长度时,就会触发扩容操作,将数组长度增加一倍,并重新计算每个元素在新数组中的位置。


七、hash碰撞是什么



你听说过hash碰撞吗?



hash碰撞就是不同的Key,经过同一散列算法之后得到的hashCode值相同。


hashCode不同,key一定不同。hashCode相同,key却不一定相同。


当两个key的hashCode()返回值不同时,它们对应哈希表索引也一定不同。不同的key对象,即使它们包含相同的属性、值或状态,它们的hashCode()返回值也是不相同的。


在这里插入图片描述


当两个key的hashCode()返回值相同时,它们可能对应同一个哈希表索引,但它们并不一定相等。在哈希表中,不同的key可能会产生相同的哈希值(哈希碰撞)。


因此,当 key的hashCode相同时,还需要比较key的相等性。需要调用key的equals() 方法来判断它们是否相等。只有当hashCode相等,且equals方法返回true时。才可以认为这两个key相等


八、如何解决hash碰撞



解决hash碰撞的方法有哪些呢?



在哈希表中,哈希碰撞可能会导致性能下降或者安全问题。


常见的解决方法有:


1、开放地址法:在发生哈希碰撞时,通过一定的算法在哈希表中寻找一个空闲的位置,并将元素插入该位置。


2、链式哈希表:在每个哈希表的元素位置上,存储一个链表,哈希碰撞时,将元素插入到相应的链表中。


3、再哈希法:如果一个哈希函数产生的哈希值发生了碰撞,就再次使用另一个哈希函数计算哈希值。


4、负载因子调整:通过调整哈希表的容量、负载因子等参数,可以减少哈希碰撞的发生。


九、HashMap为什么线程不安全



HashMap线程安全吗?为什么?



HashMap是非线程安全的。在多线程环境下,如果多个线程同时修改HashMap中的数据,就可能会导致数据的不一致性。


说白了就是没加锁。


在这里插入图片描述


当多个线程同时调用HashMap的put()方法,一旦他们计算出的hash值相同,就会发生冲突,导致数据被覆盖。


所以,对于多线程并发访问的情况,建议使用线程安全的Map实现


例如ConcurrentHashMap,或者使用Collections.synchronizedMap()方法将HashMap包装成一个线程安全的Map


十、HashMap、HashTable、ConcurrentHashMap的区别



最后一个问题:说一下HashMap、HashTable、ConcurrentHashMap的区别?



麻了! 真的麻了....救救孩子吧....


在这里插入图片描述


HashMap、HashTable、ConcurrentHashMap都是Java中常用的哈希表实现。


区别主要在以下几个方面:


1、线程安全性:HashTable是线程安全的,HashMap是非线程安全的,ConcurrentHashMap通过分段锁的方式保证了线程安全。


2、是否可为空:HashTable不允许value为空,ConcurrentHashMap不允许null值作为key或value,而HashMap则允许null作为key或value。


3、迭代器:HashTable的迭代器是通过Enumeration实现的,而HashMap和ConcurrentHashMap使用的是Iterator实现的。


4、扩容:HashTable在扩容时,将容量扩大一倍加一,而HashMap和ConcurrentHashMap的扩容机制是将容量扩大一倍。


5、初始容量:HashTable的初始容量为11,而HashMap和ConcurrentHashMap的初始容量为16。


6、性能:HashMap通常比HashTable性能更好,因为它没加锁。所以弊端就是线程不安全。但后者加了锁,是线程安全的,缺点就是消耗性能。ConcurrentHashMap在多线程并发访问时,比HashTable和HashMap性能更好,因为它使用了分段锁来保证线程安全


所以,不建议使用HashTable。至于选择HashMap还是ConcurrentHashMap取决于并发访问量的大小,若并发访问量不高,则选用HashMap。若并发访问量较大,则选用ConcurrentHashMap。



ok,那今天先到这里吧。练习时长两年半的程序员.....唱跳rap篮球🏀....差点就遭老罪喽~



还有,别忘记给那个练习时长两年半的三婶儿也点个赞哈~她唱跳rap篮球也还行......


在这里插入图片描述


作者:三婶儿
来源:juejin.cn/post/7209826725365137465
收起阅读 »

领导派的活太难,除了跑路,还能怎么办?

人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中…… 这种事情一般有一些共同特点。 结果和目标极其模糊。 需要协调其他团队干活但是对方很不配合。 领导也...
继续阅读 »

人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中……


这种事情一般有一些共同特点。



  1. 结果和目标极其模糊。

  2. 需要协调其他团队干活但是对方很不配合。

  3. 领导也不知道怎么干


领导往往是拍脑袋提想法,他们也不知道具体如何执行。反过来说,如果领导明确知道怎么做,能亲自指导技术方案、亲自解决关键问题,那问题就好办了,只要跟着领导冲锋陷阵就好了,就不存在烦恼了。


遇到这种棘手的事情,如果自己被夹在中间,真的非常难受啊!


image.png


今天重点聊聊领导拍脑袋、心血来潮想做的那些大事 如果让你摊上了,你该怎么做!


1、提高警惕!逆风局翻盘难!


互联网行业目前处于稳定发展期,很少会出现突然迅猛增长的业务,也很少有公司能够迅速崛起。这是整个行业的大背景。因此,我们应该对任何不确定或模糊的目标表示怀疑,因为它们更有可能成为我们的绊脚石,而不是机遇。即使在王者荣耀这样的游戏里,要逆风翻盘也很困难,更何况在工作中呢。


当领导提出一个棘手的问题时,我们应立刻警惕,这可能不是一个好的机会,而是一个陷阱。我们不应该被领导画的饼所迷惑,而是要冷静客观地思考。哪些目标和结果是难以达到的,这些目标和结果就是领导给我们画的大饼!


领导给出任务后,我们就要努力完成。通常情况下,他们会给我们一大堆任务,需要我们确认各种事情。简而言之,他们只是有个想法,而调研报告和具体实施方案就需要我们去做。


如果领导是一位优秀而谦虚的人,通常在我们完成调研后,会根据调研结果来判断这个想法是否可行。如果不可行,他们会立即放弃,而我们也不会有什么损失。


但是,一旦领导有了一个想法,肯定是希望我们来完成的,即便我们在调研后认为不可行,大多数情况下,他们也不会接受我们的结论!因此,我们的调研工作必须极度认真,如果我们认为不可行,就要清楚地阐述不可行的理由,要非常充分。


这是我们第一次逃离的机会,我们必须重视这次机会,并抓住机会。


2、积极想办法退出


对于这种模糊不靠谱的事情,能避开就避开,不要犹豫。因为这种事情往往占用大量时间,但很难取得显著的成果。对于这种时间周期长、收益低、风险高的事情,最好保持距离。


你还需要忍受巨大的机会成本


在你长期投入这种事情的过程中,如果团队接到更好的项目和需求,那肯定不会考虑你。你只能羡慕别人的机会。


因此,如果可以撤退的话,最好离开这种费力不讨好的活远远的!



子曰:吾日三省吾身,这事能不能不干,这事能不能晚点干,这事能不能推给别人干。



如何摆脱这件事呢?


2.1 借助更高优事情插入,及时抽身


例如,突然出现了一件更为紧急的事情,这就是脱身的机会。与此同时,我们也可以为领导保留一些颜面,因为随着工作的进展,领导也会意识到这件事情的意义不大,很难取得实质成果。但是,如果我们一开始就表示不再继续做这件事,那么领导可能会觉得自己的判断出了问题,失去面子。所以,我们可以寻找一个时机,给领导下台阶。


或者,突然出现了一个需求,与我们目前的重构方案存在冲突。这是一个很好的借口。重构方案和未来产品规划产生了冲突,我们应优先满足产品规划和需求。重构方案需要后续再次评估,以找到更好的解决方案。


2.2 自己规划更重要的事情,并说服领导


当你对系统优化没有想法时,不要怪领导给你找事干。


如果领导有一个系统重构的计划和目标需要你执行,但是你不想干,或者你认为这件事不靠谱。那么你可以思考一个更可行、更有效、更能带来收益的重构方案,并与领导进行汇报。如果领导认为你的计划更加重要且更具可行性,那他可能会放弃自己的想法。


这就是主动转被动的策略。这时你的技术能力将接受考验,你能提出一个更优秀的系统重构方向吗?你能提出一个更佳的系统建设方向吗?


2.3 选择更好的时机做这件事


如果领导让你去做技术重构,而这件事的优先级不如产品需求高,上下游团队也不愿意配合你,而且领导给你的人力和时间资源也不够充裕,你应该怎么办呢?可以考虑与产品需求一起进行技术重构。也就是说,边开发需求,边进行技术重构。这样做有以下好处:可以借助于产品的力量,很自然地协调上下游团队与你一同进行重构。同时也能推动测试同事进行更全面的测试。在资源上遇到的问题,也可以让产品帮助解决。


所以,技术重构最好和产品需求结合起来进行。如果技术重构规模庞大,记得一定要分阶段进行,避免因技术重构导致产品需求延期哦。


2.4 坦诚自己能力不足,暂时无法完成这件事,以后再干行不行


可以考虑向领导坦然承认自己的能力还不足以立即执行这项任务,因此提出先缓一缓,先熟悉一下这个系统的建议。我可以多做一些需求,以此来熟悉系统,然后再进行重构。


我曾经接手一个系统,领导分配给我一个非常复杂的技术重构任务。当时我并没有足够聪明,没有拒绝,而是勉强去做,结果非常不理想,还导致了线上P0级别的事故发生!


新领导告诉我,"先想清楚如何实施,再去行动。盲目地勉强上阵只会带来糟糕的结果。当你对一个系统不熟悉的时候,绝对不能尝试对其进行重构。"


先熟悉系统至少三个月到半年。再谈重构系统!


2.5 拖字诀,拖到领导不想干这件事!


拖到领导不想干的时候,就万事大吉了。


注意这是最消极的策略,运气好,拖着拖着就不用干了。但如果运气不佳,拖延只会让任务在时间上更加紧迫,而且还会招致领导的不满。


使用拖延策略很可能得罪领导,给他们留下不良的印象。


因此,在使用此策略时应谨慎行事!


2.6 退出时毫不犹豫,不要惋惜沉默成本


如果有撤退的机会,一定不要犹豫,不要为自己付出的投入感到遗憾,不要勉强继续前进,也不必试图得到明确的结果。错误的决策只会带来错误的结果。一定要及时止损。


因为我曾经犯过类似的错误,本来有机会撤退,但是考虑到已经付出了很多,想要坚持下去。幸好有一位同事更加冷静,及时制止了我。事后我反思,庆幸及时撤退,否则后果真的不敢想象啊。


3、适当焦虑


每个人都喜欢做确定性的事情,面对不确定的事情每个人都会感到焦虑。为此可能你每天都很焦虑,甚至开始对工作和与领导见面感到厌恶。之所以这个事情让你感到不适,是因为它要求你跳出舒适区。


但是,请记住,适度的焦虑是正常的。告诉自己,这并没有什么大不了的。即使做得不好,顶多被领导责备一下而已。不值得让生活充满焦虑,最重要的是保持身心健康和快乐。


当你沉浸在焦虑中时,可能会对工作和领导感到厌烦。这样一来,你可能会对和领导沟通感到反感。这种情况是可怕的,因为你需要不断和领导沟通才能了解他真正的意图。如果失去了沟通,这个事情肯定不会有好的结果。


因此,一定要保持适度的焦虑。


3.1 沟通放在第一位


面对模糊的目标和结果,你需要反复和领导沟通,逐步确认他的意图。或者在沟通中,让领导他自己逐渐确定的自己的意图。在这方面有几个技巧~


3.2 直接去工位找他


如果在线上沟通,领导回复可能慢,可能沟通不通畅。单独约会议沟通,往往领导比较忙,没空参加。所以有问题可以直接去工位找他,随时找他沟通问题。提高效率


3.3 没听懂的话让领导说清楚


平常时候领导没说清楚,无所谓,影响不大。例如普通的产品需求,领导说的不清楚没关系,找产品问清楚就行。


面对目标不明确的项目,领导的意图就十分重要。因为你除了问领导,问其他人没用。领导就是需求的提出方,你不问领导你问谁。 在这种情况下,没听懂的事情必须要多问一嘴。把领导模糊的话问清楚。


不要怕啰嗦,也不要自己瞎揣摩领导的意图。每个人的想法都不同,瞎猜没用。


3.4 放低姿态


如果领导和你说这件事不用干了,你肯定拍手叫好。很多烦恼,领导一句话,就能帮你摆平!


放低姿态就是沟通时候,该叫苦叫苦,该求助就求助,别把自己当成超人,领导提啥要求都不打折扣的行为完全没必要。可以和领导叫叫苦,可以活跃气氛,让领导多给自己点资源,包括人和时间。


说白了,就是和 领导 “撒娇”。这方面女生比较有优势,男生可能拉不下脸。之前的公司,我真见识过,事情太多,干不完,希望领导给加人,但被领导拒绝。 然后她就哭了,最后还真管用!是个女同事。


男孩子想想其他办法撒娇吧。评论区留下你们的办法!


3.5 维护几个和领导的日常话题


平常如果有机会和领导闲聊天,一定不要社交恐惧啊! 闲聊天很能提升双方的信任关系,可以多想想几个话题。例如车、孩子、周末干啥、去哪旅游了等等。


提升了信任关系,容易在工作中和领导更加融洽。说白了就是等你需要帮忙的时候,领导会多卖你人情!


4 积极想替代方案————当领导提的想法不合理时


积极寻求替代方案,不要被领导的思路局限!引导众人朝着正确的方向前进!


不同领导的水平和对技术问题的认知不尽相同,他们注重整体大局,而员工更注重细节。这种差异导致了宏观和微观层面之间存在信息不对称,再加上个人经验、路径依赖导致的个人偏见,使得领导的想法不一定正确,也不一定能够顺利实施。


就我个人的经历来说,领导要求我进行一次技术重构。由于我对这个项目还不够熟悉,所以我完全按照领导的方案去操作,没有怀疑过。事后回顾,发现这个方案过于繁重,其实只需要调整前端接口就能解决问题,但最终我们却对底层数据库存储、业务代码和接口交互方式进行了全面改变。


最终收益并不高,反而导致了一个严重的故障。既没有获得功劳,也没有得到应有的认可。


事后反思,我意识到我不应该盲目按照领导的方案去执行,而是应该怀着质疑和批判的态度去思考他的方案。多寻求几个备选方案,进行横向比较,找到成本最低、实施最简单的方案。


4.1 汇报材料高大上,实现方案短平快


私底下,可以对老板坦诚这件事,就是没什么搞头。但是对外文章要写得高大上!


技术方案要高大上,实现方案要短平快。


面对不确定的目标、面对不好完成的任务,要适当吹牛逼和画饼。汇报文档可以和实现方案有出入。


模糊的目标,往往难以执行和完成,技术方案越复杂,越容易出问题。本来就没什么收益,还引出一堆线上问题,只能当项目失败的背锅侠,得不偿失。


一定要想办法,把实现方案做的简单。这样有3个好处;




  1. 降低实现难度,减少上线风险。




  2. 缩短开发周期,尽快摆脱这个项目。




  3. 把更多的时间放在汇报材料上。代码没人看!!!




程序员一般情况下习惯于实话实说,如果说假话,一定是被人逼得。


不会写文档?# 写文档不用发愁,1000个互联网常用词汇送给你


不会写技术方案?# 不会画图? 17 张图教你写好技术方案!


5、申请专门的团队攻克难关!


例如重构系统涉及到上下游系统,一个人搞不定的!要向领导寻求帮助,让上下游同事一起干这件事。


让熟悉系统的人跟自己一起做,拉更多的人入伙!多个人一起承担重任! 这种组织上的安排,只能由领导出面解决。


假如别的同事经常打扰你,总让你确认这件事,确认那件事,总让你帮忙梳理文档,你愿意配合吗? 每个人都很忙,没人愿意长期给你干活。


让领导帮忙成立重构小组!然后你可以给每个人都分派任务,比自己独自硬扛,成功概率大很多。


虽然重构的目标不明确,但你可以尝试明确每个人的责任,设置短期的里程碑。例如前三天梳理整理资料,每天开早会, push大家干活。(这样很招人恨!没办法,领导卷的)


5.1 寻求合作的最大公约数


重大项目往往需要多个团队同时配合,即便你申请了专门的小组跟进这件事,但是别人可能出工不出力!


他们不配合的原因在于:不光没有收益,付出还很多。成本和收益不对等,人家不愿意很正常。保持平常心!不要带着脾气看待这件事!


略微想一下就明白,既然你觉得这件事风险高、收益低,难道其他人看不出来吗?


作为项目的负责人推动事情更加困难。当别人不配合时,除了把矛盾上升到上层领导外,还有哪些更好的办法呢?




  1. 平时多和相关同学打好关系。平时奶茶咖啡多送点,吃别人嘴短,到时候求人时候很管事的。




  2. 调动对方的积极性!例如重构系统需要人家配合,但是这件事对他们又没有收益。可以和他们一起头脑风暴,想一下对方系统可以做哪些重构。当双方一拍即合,各取所需时,才能合作融洽。双赢的合作,才能顺利。




  3. 多作妥协。上下游系统的交互边界很难划分,如果交互存在争议,可以适当让步,换取对方的积极合作。完成胜于完美!




总之,涉及多个团队合作时,除了依靠上层领导的强硬干预之外,还要想一些合作共赢的方案!


6、争取更多的资源支持


没有完不成的事情,只要资源充裕,任何事情都是有希望的。当你面临棘手的问题时,除了打起12分的精气神,还要多想想和领导申请资源啊!


最重要的包括人力资源、时间资源。如果空口白牙就要人,可能比较困难。


这需要你在调研阶段深入思考,预想到系统的挑战点,把任务细分,越细越好,然后拿着排期表找领导,要人、要时间。


如果人和时间都不给!可以多试几次,软磨硬泡也是好办法!


此外还有别的办法,例如 ”偷工减料"。你可以和领导沟通,方案中哪些内容不重要,是否可以砍掉。”既然你不给人,砍掉不重要的部分,减少工作量,总可以吧"


除此之外,还可以考虑分期做。信用卡可以分期付款,技术重构当然也可以分期优化!


7、能分期就分期


对于技术重构类工作,一定要想办法分期重构,不要一次性只求大而全!




  1. 越复杂的技术方案越容易出问题!




  2. 越长的开发周期越容易出问题!




  3. 越想一次性完成,越容易忙中出错!




分期的好处自不必说,在设计方案时一定要想如何分期完成。


如果对一个系统不熟悉,建议分期方案 先易后难!先做简单的,逐渐地你对系统会有更深入的理解!


如果对一个系统很熟悉,可以考虑先难后易。先把最困难的完成!后面会轻松很多!


但是我还是建议庞大的重构工作,先易后难!先做简单的,拖着拖着,也许就不需要重构了呢!


8、即便没有功劳但是要收获苦劳


当一件事干成很难的时候,要想办法把损失降到最低。一定要想着先保护自己!别逞能!


工作几年的朋友应该知道,不是所有的项目都能成功!甚至大部分项目在商业上是失败的!做不成一件事很正常!


如果一件事很难办成,功劳就不要想了。但是可以赚一份苦劳。


这要求你能把自己的困难说给领导,例如其他团队不配合!你可以一直和领导反馈,并寻求领导的帮助。


日常工作的内容也要有文档留存。工作以周报形式单独和领导汇报!要让领导知道你每周的进展,向领导传递一个事实:“每一周你都努力地在做事,并且也都及时汇报了,日后干不成,可别只怪我一人啊!”


接到一个烫手山芋,处理起来很难~ 斗智斗勇,所以能躲开还是躲开啊!


9、转变观念:放弃责任心,领导关注的内容重点完成


出于责任心的角度,我们可能认为领导提出的方案并不正确,甚至认为领导给自己派的工作完全没有意义。


你可能认为领导的Idea 不切合实际!


出于责任心,你有你的想法,你有你的原则!你认为系统这样重构更适合!但那又怎样,除非你有足够的理由说服领导,否则改变不了什么。


站在更高的位置能看的更远,一般领导都会争取团队利益最大化。虽然看起来不切实际,但是努力拼一拼,也许能给团队带来更大的利益。这可能是领导的想法!说白了,就是领导想让团队多去冲锋陷阵,多把一些不可能变成可能!


和领导保持节奏,领导更关注哪件事,就尽力把这件事做好! 放弃自己所谓的“责任心”。


10、挑战、机遇、风险并存。


在互联网稳定期,各行各业都在内卷,公司内部更是在内卷!


在没有巨大增量的团队和公司里,靠内卷出成绩是很困难的事情。有时候真的很绝望,每一分钟都想躺平 。


像这种目标不明确、执行方案不明确、结果不明确、需要协调其他团队干活的难事越来越多!风险高、低收益的事情谁都不想干!


但是一旦能做成,对于个人也是极大地锻炼。所以大家不要一味地悲观,遇到这种棘手的事情,多和领导沟通,多想想更优的解决方案。也许能走出一条捷径,取得极大的成果~


作者:他是程序员
来源:juejin.cn/post/7290469741867565092
收起阅读 »

抓包调试工具的终极答案-whistle

web
前言 抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络...
继续阅读 »

前言


抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络数据流量的细节。


在计算机通信中,数据包是由发送端(如浏览器或应用程序)构建的,并通过互联网传输到接收端(如服务器或另一台计算机)。通常情况下,这些数据包由发送和接收的应用程序自行处理,用户往往无法直接观察这些数据包的内容。然而,抓包工具可以截取这些数据包,并将其内容以明文或加密的形式展示给用户。如果数据包是以明文形式发送的,或者我们可以推断出其加密方法,那么我们就可以对这些数据包进行深入的分析和解密。这样,我们便可以了解这些数据包的内容、用途和意义。


通过使用抓包工具,开发人员和系统管理员可以调试和分析网络应用程序和通信协议,以便更好地了解它们的性能、安全性和可靠性。此外,普通用户也可以利用抓包工具来了解他们正在使用的应用程序和网络服务的内部工作原理,并保障他们的网络安全和隐私。总之,抓包工具是计算机网络中不可或缺的一部分,它为我们提供了深入洞察和分析网络数据流量的能力。


抓包工具更多的分析网络流量以及数据包内容、检测网络问题、获取数据传输的详细信息、功能调试等。市面上常见的抓包工具有很多,比如说我们网页上常用到的浏览器开发者工具、移动端常用的vConsole以及市面上大家都在用的Charleswhistle,今天就简单给大家分享一下whistle的基本使用。


1. 简介


whistle主要是基于node来实现的一个跨平台web调试代理工具,whistle采用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能


2. 安装启动


wproxy.org/whistle/ins…


1. 环境


首先要使用whistle的话,必须要具备node环境(下载地址),下载成功后可以通过命令行来查看是否已安装


node -v // 查询当前node版本信息

2.安装whistle


后面也会提到桌面端应用LightProxy,也是基于Electron和whistle的桌面端代理软件。


npm install -g whistle // Windows
sudo npm install -g whistle // Mac(非root权限)

安装成功可以使用命令whistle help 或者w2 help查看whistle的相关帮助信息


3.启动whistle


最新版本的whistle支持三种等价的命令whistlew2wproxy


w2 start // 启动whistle
w2 restart // 重启whistle
w2 stop // 停止whistle
w2 run // 调试模式启动whistle

4.配置代理


这里就以Mac配置举例,可以在设置->Wi-Fi->详细信息->代理中选择网页代理(HTTP)填入对应的ip以及端口号;


移动端这里同样以IOS为例,在Wi-Fi->HTTP代理中打开配置代理为手动,同时填入对应ip以及端口号.


我们也可以通过chrome浏览器插件SwitchyOmega来进行网页代理



  1. 点击新建情景模式

  2. 选择选项代理服务器

  3. 配置代理协议、代理ip、代理端口

  4. 点击应用选项保存并切换到代理模式


switchyOmega


5.证书安装


最后我们只需要安装根证书即可。我们打开whistle生成的浏览器生成页,点开HTTPS选项,点击二维码下载证书,这里同样以MAC和IOS为例。



1.Mac我们打开钥匙串访问,这里要注意,当我们添加完成后依旧属于不被信任状态,我们需要双击证书,在信任的里面找到使用此证书时选中始终信任,配置证书完成后我们选中Capture TUNNEL CONNECTS即可代理成功,捕捉传输内容。


证书
2.IOS同样我们通过扫码打开证书,允许描述配置文件下载,在设置已下载描述文件中安装描述文件,安装完成后我们打开通用->关于本机->证书信任设置中选择对下载的whistle证书启用完全信任即可。



如果是windows系统出现证书无法下载的情况,进入系统设置 - 网络 - Windows防火墙 - 高级设置,设置入站规则Node.js开头的都允许连接,保存确定下载;


手机端偶尔可能会遇到无法找到证书的情况,可以连接同一个局域网,访问电脑ip代理对应ip地址,扫码HTTPS进行证书下载




3.使用


whistle官网这里详细介绍了whistle相关的命令行参数,这里我们就不过多赘述。我们只介绍几个常用的功能。


1.HTTPS请求抓取:


所有配置完成后,我们打开whistle页面,浏览器或手机发起HTTPS请求后即可看到.


image.png
那么问题来了,这么多请求同时包含了预检等众多请求,我们怎么快速找到我们需要看到的接口呢?
我们可以在下方的Type filter text来进行简单的搜索功能,默认是按照url来进行搜索的,我们也可以按照固定的分类规则来进行快速查询



  1. 默认搜索url

  2. h: {word}搜索头部

  3. c: {word}搜索内容

  4. p: {word}搜索请求协议

  5. o: {word}搜索ip

  6. m: {word}搜索方法

  7. s: {word}搜索状态码


如果我们依旧觉得不够清晰该怎么办呢,我们就可以用到whistle的Rules功能,Rules支持我们通过扩展系统host配置来进行所有操作。


// whistle将通过pattern匹配来完成对应uri的操作
pattern operatorURI

pattern的匹配规则支持域名、路径、正则、精准匹配和通配符匹配


api.juejin.cn style://color=@fff&fontStyle=italic&bgColor=red

这样我们就可以更清晰的来找到我们想捕捉的内容。


image.png


2.请求响应修改


我们可以通过固定的Rules配置来对请求或者返回来进行修改测试


{pattern} method://get 请求方式修改为get
{pattern} statusCode://500 请求状态码返回500
{pattern} log:// 日志打印
{pattern} resCors:// 跨域

以上提到的是我们部分的简单修改,如果我们需要修改请求的请求体以及相应内容,我们就需要用到whistle提供的Values板块来进行配置


{pattern} reqHeaders://filepath 修改请求头 //filepath: Values里面的key或者本地文件
// reqHeaders example
{pattern} reqHeaders://{testReqHeadersJSON}
// resBody example
{pattern} resBody://{testResBodyJSON}

Values模版中配置testReqHeadersJSON和testResBodyJSON


image.png


image.png


这样就可以添加或者修改请求头内容,修改或添加响应内容同理。


image.png


3.移动端调试


whistle不仅提供强大的web调试功能,对于移动端的调试也是十分友好的。


由于移动端需要适配众多不同的浏览器和设备,包括各种尺寸的屏幕、各种操作系统和不同的设备载体,因此相对于PC端的页面调试,移动端的调试难度更加复杂。在出现问题时,排查的过程也涉及更多因素,需要我们及时发现并修复问题。对于一个合格的开发人员来说,强大的开发能力是基础,同时还需要拥有快速解决问题的能力和精准定位问题的技能,这样才能够在面对不同的问题时应对自如、犹游刃有余。


像我们在测试环境常用到的vConsole一般是不会在生产环境以一般方式展现给用户的,但是whistle提供注入js的方法能够让我们通过js代码以及vConsole等工具来进行页面调试以及问题的快速排查。


1.接口调试

这里我们就简单的以掘金首页为例



  1. 我们在Values中配置好vConsole以及对应生成实例代码;

  2. 在Rules中通过jsPrepend进行js注入
    这样我们就可以成功生成vConsole来进行调试了



vConsole.min.js的源码我们可以去github上自行下载,或者也可以通过插件来解决。


// 集成 vConsole、eruda、mdebug 等调试H5页面工具的插件
sudo npm install -g whistle.inspect

{pattern} whistle.inspect://vConsole
{pattern} whistle.inspect://eruda
{pattern} whistle.inspect://mdebug

2. 元素样式调试

whistle同时内置了Weinre和Chii来帮助我们进行调试


{pattern} weinre://{yourName}

配置Rules后我们在Whistle下拉选项下选中对应name,重新打开页面后即可进行elment调试


image.png{pattern} 同样配置Rules后我们在Whistle选项下的Plugins选中对应Chii,点击打开后选择inspect来进行element调试


{pattern} whistle.chii://

image-20230921140713086.png


4.LightProxy


下载地址


LightProxy是基于whistle使用electron开发的一个桌面端代理软件,从操作以及证书代理配置上更加简单灵活,更好的满足开发者的需求,基本使用规则同whistle一致;同时也帮我们继承了常用的像inspectvase等插件,更加方便快捷。


5.总结


Whistle作为常用的的网络调试工具之一,它不仅具备常规的抓包功能,还在跨域代理等方面展现了多样化的应用。通过巧妙的配置和强大的功能,我们可以进行深度定制和扩展,以满足各种复杂的调试需求。


这个工具的应用场景非常广泛,从简单的HTTP/HTTPS请求拦截,到复杂的爬虫和自动化测试,都可以借助Whistle实现。同时,它还支持JavaScript、TypeScript等多种编程语言,以及各种浏览器和Node.js环境。


使用Whistle进行调试非常简单,只需要简单地设置和配置,就可以轻松地实现对网络请求的拦截和修改。无论是排查问题、测试接口还是调试前端代码,Whistle都能够帮助我们快速定位问题并解决问题。它的易用性和灵活性也使得它成为了前端开发人员的得力助手。


通过使用Whistle,我们可以更好地了解网络请求的细节,掌握API接口的调用和数据传输的规律。这有助于我们优化代码、提高程序的稳定性和性能。因此,无论是初学者还是经验丰富的开发者,都应该尝试使用Whistle来提升自己的调试技能和开发效率。


参考


1.whistle官网: wproxy.org/whistle/


作者:洞窝技术
来源:juejin.cn/post/7293180747400134706
收起阅读 »

《我当程序媛那些年(四)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!



接上文~我当程序媛那些年(三)


相遇


从H城市到S城市距离不远,高铁大概1个小时,我们相约在人广见面,虽然见面之前已经见过照片了,但没见过真人,怕有点脸盲一时半会儿认不出哈哈~



ps:等待的过程是有点期待和忐忑不安的,如果大家也有过这种经历,相信差不多都是这样的心情大差不差😂



经过十多分钟的等待,我们终于快要见面,因为是五一的原因,所以那天人广人很多,正准备上电梯扶手时,我恍然间好像感觉到有人拍了一下我的肩膀,回头侧身一看。没有看到熟悉的身影。


正当我以为是我的错觉,刚好到电梯口的时候,一个身影窜到我面前(那画面脑补一下有点搞笑😂),定睛注意一看,才发现眼前站了一个熟悉的人。背着书包,带着眼镜,斯斯文文,是典型的互联网宅男形象没跑了哈哈~(当然这里不是取笑之意,只是觉得这个形容词生动有趣😆)


因为是放假,所以我们前期就沟通好了去哪里玩。可能是因为初次见面,双方都有些拘谨,路上去聊到玩的地方话题才开始多了起来。



因为我本身就是性格外向,比较活泼的人。不熟悉我的人对我的第一印象可能是有点安静(实际正好相反哈哈~),熟悉了就是话匣子,虽然偶尔性格上会透露出女汉子的气质哈哈~



也许是有比较长的一段时间没有和身边熟悉的人聊过天,所以聊到有趣的话题时,嗯~我话会比较多,属于压根不用担心会冷场的那种🤣。


我们首先去了迪斯尼,晚上的迪斯尼烟花很美。那天虽然是尽兴而归,如果是约会或者和朋友一起的真的不建议节假日去玩哈哈~,人真的超多,排队排了一天只玩了差不多两三个项目额。。。。所以之后我们出去玩都再也不去人潮拥挤的地方(没出去玩过被坑一次就长记性了😂)。


后面两天为了缓解一下疲惫,没有去往那种要排很长的队伍景点了,陆续去参观了一下S市的动物园,以及H市的西湖等。


997898f8f3623ba14db306d04021647.jpg


7fab44935a6f99b7a4925ab96ea7a61.jpg


bd3dbd23a6626b32179791f9be57a2b.jpg


8a7388159570be6f3f53b031babc2d8.jpg


43e6f18afa89025160a70d77ac89c3d.jpg


bc099734c602575408e5f7ef198dff2.jpg


5c1ec5b2e90e2be09edbf7465bdbd9d.jpg


6ac2a47c7510db19645a79c685c1e3d.jpg


c05151ec9714b8e2abab63dbff2a8c0.jpg


a3965e9e150a42728c20bead3a15679.jpg


生病危机


继五一过后,开始回归日常的上班族生活,那次见面之后,加上假期几天的相处,之前也有过联系(虽然没见面),但是从谈吐、学识以及工作方面,我们都觉得互相很合拍,共同话题很多,所以我们后面不久就确认了关系,不过由于Z先生是在H市,所以我们前期算是属于异地恋。


上了一周班后,某天下班感觉腹部有点疼痛,一看伤口有些红肿,感觉就是被虫子咬了,至于是什么虫子,就不得而知了。于是我回家买了红霉素软膏涂了一下,感觉稍稍有些缓解。


我以为过几天就会好,当时也没多想,甚至没想着去医院看看,结果这一忽视,差点让我丢了小命`(>﹏<)′


过了几天,伤口越来越恶化,渐渐开始有脓水出现。我感觉情况有点不对,打算周末去医院看看。由于当时Z先生工作是在杭州,我们确认关系后约的都是周末见面,因此我当时就去了H市的医院。


令我没想到的是,这次H市的医院之旅,差点要了我的小命。起初找的也算是H市的三甲医院了,当时虽然就确认了是蜱虫叮咬,但是给的药物却并没有完全治好我的伤口,我不知道是不是我体质原因还是药物就没啥效果额,虽然最后花了一千多(⊙﹏⊙)。。。。


后面两天病情突然一下子就恶化的很快,腹部周围开始也全是脓,伤口开始剧烈疼痛,不能碰触的那种。


但是医生说脓水必须挤掉,我只记得当时算是痛彻心扉了,怎么形容呢,就似乎有人拿了把刀在你的腐肉上刮,还不打麻醉药的那种,挤脓的时候痛的都感觉有点意识模糊了。


当时Z先生看到治疗没效果,果断放弃继续治疗,请假带我直奔S市的专业皮肤科医院。当时伤口已经恶化到没有办法再继续上班,坐往前往S市的高铁一直到医院时,一边走伤口一边在流脓血,可想而知有多严重了。


后面到了S市的医院,医生简单的看了一下,我也把之前开的药给医生检查了一下,最终医生开了一套中药,直接热敷在伤口上,从看病->开药->敷药,整个时间大概持续了两三个小时。


在这里真的得感叹一句中药材的神奇,敷完药大概一个小时,伤口脓血已经止住,伤口疼痛也几乎缓解,红肿情况开始好转,感觉就好像在死亡的边缘跑了一趟突然能喘上气来的那种,算是毫不夸张了。


待我好些了之后,医生才开始跟我详细的说明了一下,他说我伤口之所以会恶化的这么严重,是因为蜱虫的尸体和毒素都留在了伤口里面,不清理出来只会越来越严重。


我问医生,我没有去蚊虫比较多的地方,为什么会无故染上这种虫子,医生问我是不是有去过公园或者草地呆过,像公园或者草地有的人会带猫猫狗狗啥的,动物身上最容易有沾染这种虫子,一旦沾到人体,后果不堪设想。


听到这里,我心里忽然一惊,想想放假的时候有在西湖边的草坪上坐过休息一会儿,那就有可能是那时沾染上的了。关键是这种虫子咬了还毫无感觉的那种,想想要多恐怖有多恐怖了😭。


随后医生还说,我这伤口算是恶化到最后严重的时期了,再晚来一周,小命估计都不保了。。。。。。



ps: 说到这里jym就得注意了,如果是女生或者有女朋友的男士们,千万千万去公园不要做草地,反正尽量多注意一下吧!!!!



经过这次,真是一朝被蛇咬,十年怕井绳,从此对虫子类心生恐惧,尤其是蜱虫、隐翅虫


至于隐翅虫为什么也很恐怖,额外说个小故事吧,算是亲眼见证的。


之前读初中的时候,由于是在农村,夏天晚上教室亮的时候会有很多隐翅虫飞进来,有的飞在灯泡上,有的飞在书桌上,当时班上一位女同学晚自习的时候,摁了一下隐翅虫,估计是没有洗手,然后摸了一下脸,然后就是没过两天,人就请假了,一周多才回来,整个脸脱了一层皮(一点都不带夸张,事实就是这样额)。。。。。



ps:也给各位jym提个醒,被虫子咬了如果不知道是什么虫子咬的,最好及时去医院看,千万不要拖!!!不然像我一样倒霉就得不偿失了😖,最后贴一下蜱虫和隐翅虫的照片,给大家提个醒。



image.png


image.png


image.png


文章以待后续。。。。如果觉得文章写的不错,那就给个赞或者关注一下吧,你的支持将是我写文最大的动力!


近期文章预览


我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)


作者:梦周十
来源:juejin.cn/post/7293122700687867931
收起阅读 »

《我当程序媛那些年(三)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!



接上文~我当程序媛那些年(二)


第一份工作日常


初入职B公司时,我的内心的激动而又忐忑的。毕竟是我的第一份工作,说不兴奋那都是虚言哈哈~,由于没有实际的工作经验,所以我也没有勇气报太高的薪资,我对自己的能力还是挺有自知之明的。同时心里其实也有一丢丢害怕,就算报了高薪资,最终还是会因为自己能力不足过不了试用期。


基于此原因,所以我是抱着一个谨慎和谦虚的学习态度去对待我的第一份工作,没有提太高的薪资,等到慢慢积攒经验在决定后面的出路。


先简单介绍一下我当时的B公司吧,B公司规模不大,大概20-99人左右的样子,公司大概就是做硬件机械行业的。当时进去的时候,人事部和技术部是分开的,总共两层(当然,是类似租的办公室的那种,并不是一整层的那个哈哈~),开发团队人不是很多,Java总共3个,前端2个,安卓1个,再加上还有做3D和UI的,加上项目经理和技术总监,整个团队差不多10人左右的样子。


公司整体工作环境只能算一般般吧,当时的项目经理、三个Java和1个安卓是坐在一个小办公室里,所以工作氛围不算是很活泼,整体气氛偏于沉闷。当然,由于后期生病的缘故(后续会说到~),所以第一家公司我也没有久待。


当时在公司做的项目算是接手别的公司项目,公司性质也不完全算是外包,毕竟有自营的硬件相关设施,估计只是外接一些项目多赚点钱吧。


由于那时算是互联网发展巅峰的初期,不像现在的技术栈种类繁多,当时用的是SSH(Spring+Structs2+Hibernate)框架,前端是用的Angular.js。偶尔也有Jquery的。



ps: 后面记得Structs2爆出了漏洞,所以后面改用了SpringMVC,还记得当年的Structs2可谓是风光无限,他与Spring和Hibernate堪称牛逼哄哄的Java Web三剑客,可惜一招从云端跌落,从此再也不负往日风光。还记得当时的招聘要求首要就是会SSH,后面的Structs2逐渐被SpringMVC/SpringBoot取代,Hibernate 也逐渐被 MyBatis/ Spring Data JPA 所取代,现在已经是SSM的天下了,说Struts2被淘汰一点也不为过。



因为刚入职不久,所以领导也没有给我派太多的活,先让我熟悉一下项目,当时项目管理软件用的是Redmine,前期我主要就是改改前端页面的bug,偶尔写写小功能,不是很复杂的CRUD,任务不重,工作算是比较轻松。


除此之外公司福利还算不错,记得当时五一劳动节发了三百,只是后续出去组织旅游,因为我是试用期,需要自费一半,当时手上发完工资之后,因为之前文章也提到过,借了同学的钱,所以一发工资我就把欠的钱立即还上了,再加上当时要还的助学贷款,还完房租,手上捉襟见肘,所以我当时也就没去了~


054aaf50b368fa5226a35a7845272e3.jpg


情绪转折点


虽然来上海加上面试和工作的时间才短短将近三个月,这段时间因为忙碌算是过的很充足。自从xyq离开上海之后,我的生活又恢复到了往日的安静。


我本身是一个喜欢热闹的人,太安静的环境下,我的那种焦虑感和孤独感在周末无人的环境中开始被无限放大,所以有的时候周末工作偶尔忙的时候我会去加加班,虽然加的次数不是很多。


有的时候忙碌也可能是好事,不空闲下来就不会胡思乱想那么多,工作渐渐稳定之后,我的情绪反而不像一开始找工作时的那种意气风发,整个人情绪开始断崖式跌落。


也不是工作不顺利吧,而是压力紧绷了太久,一时间突然放松下来,情绪便如洪水一样收也收不住。其实这个时候最好的方式就是出去走走,看一下新的环境或者认识新的人,转移一下注意力。


可惜当年的我没有想过这些,除了工作日上班就是周末在家里闷着,没有交际,整个人开始消极沉默寡言,至于为什么在家里闷着,主要原因是当时考虑到出去玩就得花钱,再加上刚上班不久,同事也不是特别熟的那种,也就没有想出去玩的欲望了,我也不会玩游戏,精神一下子放松下来那种糟糕的情绪就有点收不住了。


af93294cc90523736dc5d3faf16864f.jpg


与君初相识


我以为我会一直糟糕状态持续下去,到工作中期,已经有点开始影响到工作了。直至遇见那束救赎我的光——Z先生。


我们于17年相识,随后相知、相伴6年,在一起两千三百多天,直至今年国庆,我们相约结束爱情长跑,相守与共进入婚姻殿堂.


相伴期间,我们互相成长,事业上我们是互相的伙伴,生活中我们是互相的伴侣,我们算是共同进步,共同成长,因为都是做开发的,Z先生做前端,我做后端(后期才转的全栈),所以共同话题很多。


工作中我们遇到问题和挫折会相互分享,相互指导和建议,结合两人的共同想法选中最佳方案。生活中我们也会互相分享遇到的有趣的事情。



ps:虽然偶尔有点小摩擦(有时候也拌嘴吵架哈哈,但我通常都吵不过他~🤣),但都是小事,算是生活调剂吧,主要还是男女思维方式不一样,他比较偏理性,我有点偏感性😂



说起我和Z先生的缘分说着就有点绕了哈哈~,当时刚来上海找工作时,当时我和L小姐(我朋友,她当时跟我不在一个班)想找已经工作了的学长交流一下面试经验和技巧,通过老师介绍,就微信联系上了C君(Z先生他朋友),C君认识我和L小姐后,加上Z先生之前有跟C君说过和L小姐的亲戚关系(堂兄妹),得知L小姐还没有对象,所以动了追求的心思😂,因为我和L小姐是朋友,加上Z先生当时也是单身,所以C君私下就把我QQ推给了Z先生🤣。


加上QQ之后,前期我们基本一个月没有过沟通(主要还是因为没见过面不熟😂),后面因为L小姐打算去H城市(当时Z先生也在H城市),我们开始渐渐有了沟通,但也只是偶尔聊两句,前期联系比较少。


渐渐熟络起来源于Z先生带L小姐去游乐园玩发的一个QQ视频,我当时情绪正值低谷期,我自己算是感觉情绪很糟糕吧,偶然间看到了这个视频,感觉很欢乐,看完后我低落的情绪稍稍好了一些,我随即评论了一下,后面我们开始沟通真正多了起来。


我们QQ上熟络之后,对双方谈吐印象都很不错,所以我们后面准备开始见面了。由于Z先生在H城市,工作日互相没有时间,因此我们约定把时间订到了当时的五一。


af68df259a949ddd8b5a79a3ef10288.jpg


文章以待后续。。。。如果觉得文章写的不错,那就给个赞或者关注一下吧,你的支持将是我写文最大的动力!


近期文章预览


我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)


作者:梦周十
来源:juejin.cn/post/7292960995436527625
收起阅读 »

《我当程序媛那些年(二)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!



接上文~我当程序媛那些年(一)


搬家


选择了最终入职的公司之后,就开始准备搬家的事情了。由于公司距离当时住的地方比较远,当时刚来上海的时候也囊中羞涩,只能暂时住在公寓里面,公寓人很多,和学校的宿舍差不多,8张床,1张床800/月,也幸好没有久待,公寓里到处都是形形色色的人,没有自己的隐私空间。


找了距离公司附近地铁线的房子,公司在2号线徐泾东那边,早上上班坐大概6、7站,还算方便。房东也是一位老爷爷,人挺和善。新找的房子和暂住的公寓差不多的价格,房子在一楼,只有一个小窗户透气,空间狭小,仅有一张床、一个卫生间、一个可以放东西的桌子、衣柜是那种悬挂式的,整个房间大概就10-12平米左右的样子吧,虽然房间确实很小,但是我却很开心,因为我再也不需要和下班和别人挤着用卫生间,不用去公用的洗衣机间排队洗东西。


那是我从家里发生变故之后,到找到工作之前,精神一直紧绷着,我也不记得有多久没有那么发自内心开心过了,或许是因为难题都已被我自己慢慢解决,有了工作,我感觉我有了新的希望,对未来我抱有无限期许。


由于刚刚入职,年纪太小,也没有工作经验,最主要还是在试用期,想着表现好一点,所以当时没考虑请假。搬家就放在了周五下班后的时间,由于要先去住的地方拿东西(幸好东西也不是特别多,一趟能搞定),再到新的住址,当时也不知道有货拉拉这种app软件(都不知道货拉拉app啥时候发布的哈哈,只知道后面用到的时候已经有很多人在使用了~),所以到新住址整理下东西差不多就到凌晨了。


搬完家后,算是新生活正式开始了,前面有提到过是培训出来的,所以我自己独立搬出来后,就基本和原来培训同宿舍的没在怎么联系了,所谓道不同,不相为谋,加上本身我自己和她们家境差距甚大,在学校除了上课,周末就是在兼职,跟她们打交道不多,因此工作后也就跟他们基本断了联系了。


我对生活一直都算是属于那种积极向上的,属于那种性格坚韧的,虽然有的时候会经历一些坎坷,但只要咬咬牙能熬过,事后也只会赞叹自己一句我真棒勉励一下自己,瞬间觉得之前经历的事都不是事了。



ps: 只记得搬完家后的周末,那天阳光明媚,照在我的脸上,感觉恍若新生,我终于可以双手开始迎接我18岁之后崭新的人生。



69a6d3b6d5be4c867c9d7b24ec8d15e.jpg


48641b7e68e54476ec56d6dfee3ffd3.jpg


aca61df066cc55e73021ca96db0de5e.jpg


08560f208cf08a2e0547b4b0ab697dc.jpg


遇见老乡的意外惊喜


在上海偌大几千万人的人口城市,如果不是认识并且事先约定好见面的时间,遇见一个同省同城市同镇还是同村的老乡是觉得一件很让人觉得惊喜和开心的事情,就暂且用她名字的缩写吧,文中称她为xyq好了。


遇见她真的算是很有缘分,那天像往常一样准备上班,结果才出门不就,还未到地铁站,就下大雨,包里没带伞的我感觉有点手忙脚乱,周边也没有商店,纯纯的马路那种,没有遮挡物,我正准备想着索性一口气跑到地铁站算了,结果头上突然出现了一把伞,我回过头,旁边一个身高和我差不多、容貌清秀的小姐姐正举着伞打在我的头顶,我个子也不高,所以打伞还算不用过于费劲。


我先开了口谢谢她对我的帮助,她随后说看到下大雨了,我又是一个人,看着像没带伞的样子,索性她伞比较大,就一起去地铁站了。后面一路上由于不是很熟悉,路上气氛有点过于安静,我也想着跟小姐姐寒暄几句,缓解一下过于安静的气氛哈哈~,我问她是哪里人,她说也是湖南的,让我瞬间有点惊喜,后面再深入一点越聊越惊喜,结果发现是在不能近的同乡了,是我老家隔壁村的,距离我家就几百米远,由于我在家属于那种比较宅常年基本没事就不出门的,很多人都不认识,哈哈~


发现是同乡,我们话题瞬间就打开了,路上了解到小姐姐是做设计的,租的房子就离我不远,年纪虽然比我大两三岁,但我们俩却感觉很投缘。路上聊着聊着就到地铁站了,我们坐上地铁分别,那一整天心情都是开心愉悦的。


本来以为我们到这可能联系就不多了,没想到过了一周,某天下班在地铁站,看到地铁站里面旁边的娃娃机很多人在玩,感觉很有趣我停留了一会,突然听到有人叫我,回过头一看,竟然又遇见了她。我们都很开心,感觉缘分有的时候就是这么奇妙,不经意间就把两个毫无关系的人瞬间变成了朋友的缘分。


我们相约回家,之后我们联系也慢慢变多了起来,我们相互串门,做好吃的送给对方,后面我房租到期,我们索性就直接一起合租了一段时间,至于为什么是一段时间,是因为小姐姐后面工作变动,刚好也有朋友在广东那边,所以我们相处时间算是比较短暂,她后面就去广州了,虽然偶尔有联系,但终究由于工作繁忙还是聊的不是很多。


我和xyq的奇妙故事到这暂且就结束了,当然我们相遇的缘分并没有结束~,因为我去年回家过年去街上取钱遇见了她,不过此时的她已经结婚,我们又聊了很久,她已经生了一个,此时正身怀二胎,有一个体贴的丈夫,她从广州后面回到老家,在邮政工作,也算是编制人员了哈哈~,今年国庆我再次遇见了她,小孩长得玉雪可爱,灵动活泼,可爱极了。当然这些这算是后话了。。。。。


真的很高兴再次遇见她,看见她过的幸福开心我心里由衷的高兴。回想起和她的奇妙缘分,只感觉自己很幸运遇到了值得让我很惊喜的人和事。


d40567fa5a652796ef85d3b0ce15ab2.jpg


b9016b9dbfcc7d91142917b4c5fdda5.jpg


文章以待后续。。。。如果觉得文章写的不错,那就给个赞或者关注一下吧,你的支持将是我写文最大的动力!


近期文章预览

我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)


作者:梦周十
来源:juejin.cn/post/7291937010381684787
收起阅读 »

《我当程序媛那些年(一)》

序言 我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考. 如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录...
继续阅读 »

序言


我是一名全栈工程师,98年,正宗的湖南妹子,17年开始出来工作,一转眼已经工作6年了,回想起从工作至今,忙忙碌碌,没有停歇,也无法静下心来好好思考.


如今终于有时间,停下脚步,回想起过往,记忆也越来越模糊,也害怕自己最后不再记得当时的一路艰辛,想留点记录哪天等到不再做这行了闲下心来翻翻吧。


一路走来,相比同龄人还在校园读书时,由于家境窘迫,不得不早点踏入社会,尝遍酸甜苦辣。尽管一路上经历了漫长的痛苦、艰辛、泪水,但也获得了成长、温暖和最终的归宿。如果有时光回溯,可以重来一次,我还是会做出当初一样的选择,虽有遗憾,但不后悔。


文章大概脉络主要就是讲述了我是怎么踏入互联网这个行业,经历了互联网的飞速发展时期直至巅峰,又到目前经济衰条,一路磕磕绊绊,以及最后的人生规划吧。


以下描述也都是本人的真实经历,没有经历过的或许会唏嘘,因此大家就权当个乐子或者生活调剂看吧!


高考


高二时,由于我的理科实在是瘸脚的一比,因此文理分科时只能无奈选择了文科,选择文科的一大好处是,我再也不用看那种枯燥无味的物理公式,可以不在学习那些头疼的化学公式,可以逃避多了好几本选修的数学课本。


一直以来,数学是我的短板,从小数学及格的次数屈指可数,所以我一直都很羡慕那些数学逻辑思维特别好的人,哈哈,感觉他们都好聪明,数学不及格的我也注定了在高考上的失败。


高二会考过后,彻底进入最紧张的一年高三,由于数学太太太...差,差到什么程度呢?高考语文时记得当时好像是125左右吧,文综也才拿了230左右,但是数学才三十多分,英语90分,后面回想的时候就在想,要是我数学能多考几十分,也许也能和同龄人一样享受美好的大学生活了。


但也是由于数学实在太差的原因,当时甚至都想过去参加单招,都准备报名了,我妈专门赶到学校一再的阻止我,在我妈的一再劝说下,最终还是放弃单招了。


放弃单招之后,对于自己未来的出路想了很久,当时也觉得自己依照目前的数学成绩,评估了一下,考上三本啥的还有希望,二本估计够呛,所以早早的给自己规划出了明确的目标。由于家里有人接触过计算机这行业,虽然当时智能机刚普及不久,但隐约也感觉得到这是一个新兴崛起的行业,因此果断地将它纳入了我的未来规划中。


高考成绩出来后,果不其然在我的意料之后,填写完志愿,等待录取通知书,后面录取的是湖南株洲的一个铁道学院,当时高考志愿填了服从调剂,然后专业从计算机行业被调剂到电气化了,一方面是大专学校而且专业又调剂了,另一方面也考虑到家里的情况不足以支撑我能度过大学校园生活,当时想的是早点出来工作,所以后面也就没去了。


放弃去读大专的机会之后,趁着高考完那段时间,去北京打了一个月暑假工,逛了一下北京的颐和园,清华大学、北京三里屯,当时那天也赶巧,回来的时候看到有计算机的培训学校在招生,加上当时年龄也还未成年,想着培训个一年半差不多成年了就能出来工作了,因此果断选择入坑,也是从这个时候开始,算是正式踏入了互联网大门。



ps:这是我去北京当时看到的一些风景,去了北京的南锣鼓巷、清华园、水立方、鸟巢、奥林匹克公园,还看了70周年大阅兵,虽然未读大学,也算是人生一大遗憾吧,但如果重来,我还是不后悔会做同样的选择。



aa9866aafb2040e6bb67c42430751e4.jpg


6629b1df09d690515c76811722fcc34.jpg


4a0a7b7708e044fe7675c220f9191da.jpg


e26adcc29bcef9025c8f99261dc4d91.jpg


9d3209eaf6a7c3ba104114083017e57.jpg


ab0b88cad355b7ce25e689186e50e78.jpg


aac5cec72b9947cbaf17282e77728ed.jpg


ea58fd746cb6aadae270eb70d5023e1.jpg


变故


培训班的生活是漫长且枯燥无味的,总共是读三学期,刚开始去的第一学期,由于老师多教学水平也参差不齐,听的也是懵懵懂懂,加上当时也才高考完不久,因此也有点懈怠,学业上也没有那么用心,第一学期学的也是恍恍惚惚,主要学的是DIV、CSS、Jquery、JAVA基础啥的,会写一些简单的页面,第一学期结束时做了一个小项目算是对基础知识的掌握吧。


时间一晃而过,一眨眼就到了第二期下半年,也许人真的要经历磨砺才会有成长吧。天有不测风云,还记得那天下午,正是日落西山夕阳最美的时候,母亲突然打电话慌慌张张跟我说,家里出事了,具体什么事不好方便细说,只记得那一天记忆尤为深刻,那一天,我被迫成了独挡一面的大人。那一年,我18岁。


当时的我还未出去工作,还没有经济能力,当母亲跟我说家里出事,急需用到大笔钱,看着母亲为了解决家里的事情,低声下气受尽小辈言辞侮辱四处借钱的样子,尽管我还未出去工作,还是想办法问周边的同学看能不能借到点钱。


或许对人心太过敏感吧,我深知我最终的结果也不过是竹篮打水一场空,借不到什么,毕竟很少联系,突然借钱,人家也不会去借给你,很正常。但我当时已无路可走,还是硬着头皮去做了。最终借到了1800,至今为止我都很感激那给过我帮助的两位同学,我们到现在也还有联系,当然,这些都是后话了。。。。


也许看到这里会觉得疑惑,难道我身边没有什么朋友吗?其实有的,只是当时的年纪都在读书,手头基本都是父母给的,因此当时家里出事那一段时间,我第一想法没有想去找朋友,而是找已经有经济能力的同学看能不能帮助一下。


也许跟我本人性格也有点关系吧,我不大爱问别人借钱,不到万不得已借了也是想办法尽快还掉,总之,不喜欢欠钱的感觉。事后朋友得知这事说我为啥不找他们,能帮一点是一点,当时事情已经解决,也只是当闲谈后话了。


经历这一遭后,家里很长时间没有缓过来,母亲也跟我说家里没钱再供我继续读下去,但我还是不想放弃,那一刻我被迫长大,意识到很长时间内我的人生恐怕是一路艰辛了,我跟母亲让她别再操心我的事情,没有生活费的日子我周末就去做两天兼职,用于支撑下一周的生活费,依次反复,第三学期的学费后面跟学校老师沟通后,在百度申请了助学贷款,解决了学费的问题,虽然此后的两年内,一直在还着这个贷款了。。。。。。


没有钱足够支撑开支的日子分外难熬,周末两天的兼职有的时候并不足以支撑一周的生活费,为了顺利挨到周末,我只能减少吃饭的次数,只能买点便宜的零食和馒头度日,一个月下来,整个人迅速萧条,原本正常的体重也迅速掉秤到八十多斤,一个月没见到我的朋友都很惊讶,说怎么瘦的这么厉害,现在仔细想想,当时太年轻脑子也不灵活,傻的可以,也不知道去买箱泡面,至少也不至于去挨饿,当时性子也倔,吃不起饭也不想向别人求助,不过索性苦难都已过去,难熬的日子都熬过来了。


后面两学期的日子,我分外珍惜,我深知这是我最后一次机会,错过我将再也没有机会家里也没有能力在支撑我去学习的机会,那一年,埋头苦读,节假日也没有回家,一是没钱,二是也想多点时间学习。


功夫不负有心人,终于快熬到了第三学期快要结束的时候,由于出来工作需要买火车票、还有生活费,当时幸好学校有搞活动,靠着点赞群攒票的我拿到了票选的前三名,拿了最终的奖金,当时还是很开心的,因为终于体会到了第一次的得偿所愿,尽管艰辛困苦,但我永不言弃



ps: 只记得出事的那天,心情一下从清晨变天黑



216900704238292d795ff0bb62d665c.jpg


面试


最困难的事情解决后,在工作前过了一个最安稳的年,年后我怀着万分忐忑和期待离开了生活18年的小镇,终于踏上了前往上海的路程。带着对未来的无限期望,也抱着希望能快速找到工作解决身上所背负的贷款,我开始了疯狂投简历面试的过程。我深知我只有一个月的时间,也只有一次机会,只有紧紧抓住这次机会,我的未来才有无限可能。



庆幸时代的造就,也庆幸自己的直觉和眼光,庆幸一切的努力终究没有化成泡影.



17年当时的互联网还未完全发展到顶峰,不像如今很卷,那个时候互联网前景属于一片欣欣向荣的场景,很多独角兽公司处于初创阶段,还未完全崛起,面试机会很多,当时面试了大概二三十家吧,从刚开始面试紧张的磕磕巴巴到后面慢慢积攒经验,谈吐流利落落大方,最终收获了2个offer。后面选了回复最快的那家,薪资6.5K,顺利入职,我也终于不用在担心和无根的浮萍一样,最后只能落寞离去。


14f83f388ef385f2377d44fedf999ba.jpg


c94f9eaee75dc281404e15acccbdd28.jpg



ps: 当时收到入职offer,算是最开心的一天了



ff04aeb62f1cd78396cb7c8995b2cda.png


文章以待后续。。。。如果觉得文章写的不错,那就给个赞支持一下吧,你的支持将是我写文最大的动力!


最新文章预览

我当程序媛那些年(一)

我当程序媛那些年(二)

我当程序媛那些年(三)

我当程序媛那些年(四)

作者:梦周十
来源:juejin.cn/post/7291500185371623481
收起阅读 »

为啥一个 main 方法就能启动项目

在 Spring Boot 出现之前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat 或 Jetty),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。 在 IDE 中也需要对 Web 容器进行一...
继续阅读 »

在 Spring Boot 出现之前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat 或 Jetty),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。


在 IDE 中也需要对 Web 容器进行一些配置,才能够运行或者 Debug。而使用 Spring Boot 我们只需要像运行普通 JavaSE 程序一样,run 一下 main() 方法就可以启动一个 Web 应用了。这是怎么做到的呢?今天我们就一探究竟,分析一下 Spring Boot 的启动流程。


概览


回看我们写的第一个 Spring Boot 示例,我们发现,只需要下面几行代码我们就可以跑起一个 Web 服务器:


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


去掉类的声明和方法定义这些样板代码,核心代码就只有一个 @SpringBootApplication 注解和 SpringApplication.run(HelloApplication.class, args) 了。而我们知道注解相当于是一种配置,那么这个 run() 方法必然就是 Spring Boot 的启动入口了。


接下来,我们沿着 run() 方法来顺藤摸瓜。进入 SpringApplication 类,来看看 run() 方法的具体实现:


public class SpringApplication {
......
public ConfigurableApplicationContext run(String... args) {
// 1 应用启动计时开始
StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 2 声明上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;

// 3 设置 java.awt.headless 属性
configureHeadlessProperty();

// 4 启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 5 初始化默认应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

// 6 准备应用环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);

// 7 打印 Banner(Spring Boot 的 LOGO)
Banner printedBanner = printBanner(environment);

// 8 创建上下文实例
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);

// 9 构建上下文
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

// 10 刷新上下文
refreshContext(context);

// 11 刷新上下文后处理
afterRefresh(context, applicationArguments);

// 12 应用启动计时结束
stopWatch.stop();
if (this.logStartupInfo) {
// 13 打印启动时间日志
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

// 14 发布上下文启动完成事件
listeners.started(context);

// 15 调用 runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 16 应用启动发生异常后的处理
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
// 17 发布上下文就绪事件
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
......
}


Spring Boot 启动时做的所有操作都这这个方法里面,当然在调用上面这个 run() 方法之前,还创建了一个 SpringApplication 的实例对象。因为上面这个 run() 方法并不是一个静态方法,所以需要一个对象实例才能被调用。


可以看到,方法的返回值类型为 ConfigurableApplicationContext,这是一个接口,我们真正得到的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名我们可以知道,这是一个基于注解的 Servlet Web 应用上下文(我们知道上下文(context)是 Spring 中的核心概念)。


上面对于 run() 方法中的每一个步骤都做了简单的注释,接下来我们选择几个比较有代表性的来详细分析。


应用启动计时


在 Spring Boot 应用启动完成时,我们经常会看到类似下面内容的一条日志:


Started AopApplication in 2.732 seconds (JVM running for 3.734)

应用启动后,会将本次启动所花费的时间打印出来,让我们对于启动的速度有一个大致的了解,也方便我们对其进行优化。记录启动时间的工作是 run() 方法做的第一件事,在编号 1 的位置由 stopWatch.start() 开启时间统计,具体代码如下:


public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
}
// 记录启动时间
this.currentTaskName = taskName;
this.startTimeNanos = System.nanoTime();
}


然后到了 run() 方法的基本任务完成的时候,由 stopWatch.stop()(编号 12 的位置)对启动时间做了一个计算,源码也很简单:


public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {
throw new IllegalStateException("Can't stop StopWatch: it's not running");
}
// 计算启动时间
long lastTime = System.nanoTime() - this.startTimeNanos;
this.totalTimeNanos += lastTime;
......
}


最后,在 run() 中的编号 13 的位置将启动时间打印出来:


if (this.logStartupInfo) {
// 打印启动时间
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}


打印 Banner


Spring Boot 每次启动是还会打印一个自己的 LOGO,如图:


在这里插入图片描述


这种做法很常见,像 Redis、Docker 等都会在启动的时候将自己的 LOGO 打印出来。Spring Boot 默认情况下会打印那个标志性的“树叶”和 “Spring” 的字样,下面带着当前的版本。


在 run() 中编号 7 的位置调用打印 Banner 的逻辑,最终由 SpringBootBanner 类的 printBanner() 完成。这个图案定义在一个常量数组中,代码如下:


class SpringBootBanner implements Banner {

private static final String[] BANNER = {
"",
" . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\",
"( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )",
" ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/"
};
......

public void printBanner(Environment environment, Class sourceClass, PrintStream printStream) {
for (String line : BANNER) {
printStream.println(line);
}
......
}

}


手工格式化了一下 BANNER 的字符串,轮廓已经清晰可见了。真正打印的逻辑就是 printBanner() 方法里面的那个 for 循环。


记录启动时间和打印 Banner 代码都非常的简单,而且都有很明显的视觉反馈,可以清晰的看到结果。拿出来咱们做个热身,配合断点去 Debug 会有更加直观的感受,尤其是打印 Banner 的时候,可以看到整个内容被一行一行打印出来,让我想起了早些年用那些配置极低的电脑(还是 CRT 显示器)运行着 Win98,经常会看到屏幕内容一行一行加载显示。


创建上下文实例


下面我们来到 run() 方法中编号 8 的位置,这里调用了一个 createApplicationContext() 方法,该方法最终会调用 ApplicationContextFactory 接口的代码:


ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
};


这个方法就是根据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创建不同类型的应用上下文(context)。而属性 webApplicationType 的值是在前面执行构造方法的时候由 WebApplicationType.deduceFromClasspath() 获得的。通过方法名很容易看出来,就是根据 classpath 中的类来推断当前的应用类型。


我们这里是一个普通的 Web 应用,所以最终返回的类型为 SERVLET。所以会返回一个 AnnotationConfigServletWebServerApplicationContext 实例。


构建容器上下文


接着我们来到 run() 方法编号 9 的 prepareContext() 方法。通过方法名,我们也能猜到它是为 context 做上台前的准备工作的。


private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner)
{
......
// 加载资源
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}


在这个方法中,会做一些准备工作,包括初始化容器上下文、设置环境、加载资源等。


加载资源


上面的代码中,又调用了一个很关键的方法——load()。这个 load() 方法真正的作用是去调用 BeanDefinitionLoader 类的 load() 方法。源码如下:


class BeanDefinitionLoader {
......
void load() {
for (Object source : this.sources) {
load(source);
}
}

private void load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class) {
load((Class) source);
return;
}
if (source instanceof Resource) {
load((Resource) source);
return;
}
if (source instanceof Package) {
load((Package) source);
return;
}
if (source instanceof CharSequence) {
load((CharSequence) source);
return;
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
......
}


可以看到,load() 方法在加载 Spring 中各种资源。其中我们最熟悉的就是 load((Class) source) 和 load((Package) source) 了。一个用来加载类,一个用来加载扫描的包。


load((Class) source) 中会通过调用 isComponent() 方法来判断资源是否为 Spring 容器管理的组件。 isComponent() 方法通过资源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在内)来区分是否为 Spring 容器管理的组件。


而 load((Package) source) 方法则是用来加载 @ComponentScan 注解定义的包路径。


刷新上下文


run() 方法编号10 的 refreshContext() 方法是整个启动过程比较核心的地方。像我们熟悉的 BeanFactory 就是在这个阶段构建的,所有非懒加载的 Spring Bean(@Controller、@Service 等)也是在这个阶段被创建的,还有 Spring Boot 内嵌的 Web 容器要是在这个时候启动的。


跟踪源码你会发现内部调用的是 ConfigurableApplicationContext.refresh(),ConfigurableApplicationContext 是一个接口,真正实现这个方法的有三个类:AbstractApplicationContext、ReactiveWebServerApplicationContext 和 ServletWebServerApplicationContext。


AbstractApplicationContext 为后面两个的父类,两个子类的实现比较简单,主要是调用父类实现,比如 ServletWebServerApplicationContext 中的实现是这样的:


public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw ex;
}
}


主要的逻辑都在 AbstractApplicationContext 中:


@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

// 1 准备将要刷新的上下文
prepareRefresh();

// 2 (告诉子类,如:ServletWebServerApplicationContext)刷新内部 bean 工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 3 为上下文准备 bean 工厂
prepareBeanFactory(beanFactory);

try {
// 4 允许在子类中对 bean 工厂进行后处理
postProcessBeanFactory(beanFactory);

StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// 5 调用注册为 bean 的工厂处理器
invokeBeanFactoryPostProcessors(beanFactory);

// 6 注册拦截器创建的 bean 处理器
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();

// 7 初始化国际化相关资源
initMessageSource();

// 8 初始化事件广播器
initApplicationEventMulticaster();

// 9 为具体的上下文子类初始化特定的 bean
onRefresh();

// 10 注册监听器
registerListeners();

// 11 实例化所有非懒加载的单例 bean
finishBeanFactoryInitialization(beanFactory);

// 12 完成刷新发布相应的事件(Tomcat 就是在这里启动的)
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// 遇到异常销毁已经创建的单例 bean
destroyBeans();

// 充值 active 标识
cancelRefresh(ex);

// 将异常向上抛出
throw ex;
} finally {
// 重置公共缓存,结束刷新
resetCommonCaches();
contextRefresh.end();
}
}
}


简单说一下编号 9 处的 onRefresh() 方法,该方法父类未给出具体实现,需要子类自己实现,ServletWebServerApplicationContext 中的实现如下:


protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

private void createWebServer() {
......
if (webServer == null && servletContext == null) {
......

// 根据配置获取一个 web server(Tomcat、Jetty 或 Undertow)
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
......
}
......
}


factory.getWebServer(getSelfInitializer()) 会根据项目配置得到一个 Web Server 实例,这里跟下一篇将要谈到的自动配置有点关系。


作者:刘水镜
来源:juejin.cn/post/7206749400172380219
收起阅读 »

对当前就业以及自身现状的一些思考

趁着今天是1024节,写一下离职程序员的感概。自从8月份离职后,到现在已经3个月了,期间碰到很多让我精神内耗以及思考人生的事,那个谁谁谁结婚了,那个谁谁谁买车了等等等等。反观自己,还是一个浑浑噩噩的事业完无成的单身的无业游民。年龄比我大的,比我小的感觉都比我优...
继续阅读 »

趁着今天是1024节,写一下离职程序员的感概。自从8月份离职后,到现在已经3个月了,期间碰到很多让我精神内耗以及思考人生的事,那个谁谁谁结婚了,那个谁谁谁买车了等等等等。反观自己,还是一个浑浑噩噩的事业完无成的单身的无业游民。年龄比我大的,比我小的感觉都比我优秀,而我只会写两行代码。


我不禁有时候会想,这个问题是由于什么原因引起的。试着从以下几个角度来分析一下自己失败的原因。


社会


在之前的那篇《关于我工作踩大坑的事》的文章里面有说过,上一份工作是真的被套路了,具体请看主页里面的文章。


我认为,在这个社会里面人与人之间的交流应该都是真诚的,而不是尔虞我诈连说句话都是套路。就上个工作来说,主管和我约谈的时候,我能感觉到他说话都在绕着圈子,变个法子和我说这个问题怎么怎么样。虽然我不是很懂这些管理话术,但是我还是能察觉到里面的一些端倪。


这个是基于我试用期超过了6个月而且没有签合同的情况下,我主动找上司约谈合同的情景下的对话


“如果你没有意见的话,那我们就继续保持这个关系”


话里意思就是“转正?转什么正,转正是不可能转正的,你爱干干,不干滚”


他问我有没有意见,那我肯定有意见的阿,社保不买,福利没有,合同没有,只有干活。到头来连个正式员工都不是了,不需要我的时候就爱干干,不干滚。不带这样玩的吧,入职之前说好的试用期三个月,三个月又三个月,最后连员工都不是。


image.png


“那边的部门刚刚走了个产品经理,你要是愿意的话我就推荐你去,然后那边会约你做一个面谈,如果可以的话那就可以正式签约”


虽然当时就觉得是大饼,但是我还是抱着一点点点点点希望等啊等,一个半月过去了,杳无音信。现在想想我真的很单纯。


下一份工作,入职不签合同的话我都要仔细考虑考虑要不要入职了,求职路上,全是套路!!


就业


自从去年12月底开始,整个互联网甚至全国经济都萎靡不振,各行各业都在裁员,包括阿里,微软这种互联网巨头。被裁的人分散到全国各地,竞争力一下子上来了,就比如我是个普通本科生,而我的竞争者都是985/211甚至哈佛等等这种世界一流名校毕业的。抢不过,根本抢不过,臣妾做不到啊。


image.png


然后吧,随着恒大的暴雷到许老板上头条,这些年楼市都一般般,没有之前那样的活跃了。


楼市的活跃度下降带来的资金资金问题影响着企业,而企业的资金问题就影响着企业的发展,谁也不想自己的钱就这么打水漂了。谨慎的投资带来了谨慎的招聘,前几年的金三银四,金九银十,在今年好像都没怎么听过了,而且各种电商平台的大活动也不公布订单数据了,可想而知。


image.png


这个薪资你认真的吗?租个房就没了还要倒贴。我不理解,但我大受震撼。


身边


打开朋友圈,那个小学同学生娃了,那个大学同学办婚礼了,隔壁屋买车了,反观自己,好像前面三样一个没有阿。这些人里面有年龄比我大的,也有比我小的,别人都家庭美满儿孙满堂了,而我只能在电脑桌面前写写文章。


想到这些我不禁在想,以前不是说读书越多就越成功吗?我寻思我也是个本科毕业吧,也没差到哪里去,为什么我三不沾呢(没车没孩子没女友),而我身边的早早就出来打拼现在幸福美满。


我依稀记得我妈2年前的一句话“哎,你们都出来赚大钱了,我儿子还背着个书包负资产”,现在想想也是,读那么多书,出来工作也是月薪3000,高中毕业也是月薪3000。那我多花的这几年时间到底是为了什么呢,可能是3000的岗位的舒适性吧大概。


可能有人会说,那些早早出来打拼的,别人家庭环境比我好,这个我不反驳,这也确实,谁不想有个富老爸老妈呢,谁不想做个富二代呢?


抛开别人富裕家庭不说,拼搏得来的幸福生活那是别人应得的,还有就是能抓住机遇被幸运女神眷顾的,运气也是实力的一部分,不是吗?我也想被幸运女神眷顾啊,我也想当一把幸运儿,上一份工作已经够倒霉了。


鸡汤


Dont worry, be happy! 反正开心不开心也要过一天,为什么不开开心心的过呢?俗话说,笑一笑十年少,人啊也就2w天,能过一天是一天,开心最重要。这段时间精神内耗很严重,要不是有个好基友,我可能就玉玉了。


不开心的时候看点开心的事物,比如猫猫!!这个世界真的不能没有猫猫吧。


image.pngimage.png


我想这段时间好好改改熬夜的坏习惯(最近11点睡觉,早起真的很爽),还有去锻炼一下瘦一下肚子,都快变成米其林先生了。当然了,自我增值也非常的重要,打算和朋友们讨论交流一下技术以及学习提升自己的技能,不然竞争者个个都是985/211真的抢不过啊。


欢迎大家在评论区留言,有不足的地方请指出。


作者:Avalon2353
来源:juejin.cn/post/7293304986867679283
收起阅读 »

手把手教你打造一个“蚊香”式加载

web
前言 这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。 效果预览 从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。 HTML布局 首先我们通过1...
继续阅读 »

前言


这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。


效果预览



从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。


HTML布局


首先我们通过15span子元素来实现金钟罩的每一层,用于创建基本结构。从专业术语上讲,每个span元素都代表加载动画中的一个旋转的小点。通过添加多个span元素,可以创建出一串连续旋转的小点,形成一个加载动画的效果。


<div class="loader">
<span></span>
// 以下省略15span元素
</div>

CSS设计


完成了基本的结构布局,接下来就是为它设计CSS样式了。我们一步一步来分析:


首先是类名为loaderCSS类,相关代码如下。


.loader{
position: relative;
width: 300px;
height: 300px;
transform-style: preserve-3d;
transform: perspective(500px) rotateX(60deg);
}

我们将元素的定位方式设置为相对定位,使其相对于其正常位置进行定位。然后定义好宽度和高度之后,设置元素的变换样式为preserve-3d,这样可以元素的子元素也会受到3D变换的影响。除此之外,还需要transform属性来设置元素的变换效果。这里的perspective(500px)表示以500像素的视角来观察元素,rotateX(60deg)则表示绕X轴顺时针旋转60度。


这样就将一个宽高都定义好的元素进行了透视效果的3D旋转,使其以60度角度绕X轴旋转。


loader类可以理解为父容器,接下来就是loader类中的子元素span


.loader span{
position: absolute;
display: block;
border: 5px solid #fff;
box-shadow: 0 5px 0 #ccc,
inset 0 5px 0 #ccc;
box-sizing: border-box;
border-radius: 50%;
animation: animate 3s ease-in-out infinite;
}

通过以上样式,我们可以创建一个圆形的动画效果,边框有阴影效果,并且以动画的方式不断旋转。关于CSS部分大部分都是一样的,这里主要介绍一下这里定义的动画效果。名称为animate,持续时间为3秒,缓动函数为ease-in-out,并且动画无限循环播放。


@keyframes animate {
0%,100%{
transform: translateZ(-100px);
}
50%{
transform: translateZ(100px);
}
}

这是一个关键帧动画。关键帧是指动画在不同时间点上的状态或样式。首先该动画名为animate,它包含了三个时间点的样式变化:


0%100% 的时间点,元素通过transform: translateZ(-100px)样式将在Z轴上向后移动100像素,这将使元素远离视图。


50% 的时间点,元素通过transform: translateZ(100px)样式将在Z轴上向前移动100像素。这将使元素靠近视图。


通过应用这个动画,span元素将在动画的持续时间内以一定的速率来回移动,从而产生一个视觉上的动态效果。


最后就是单独为每个子元素span赋予样式了。


.loader span:nth-child(1){
top: 0;
left: 0;
bottom: 0;
right: 0;
animation-delay: 1.4s;
}
.loader span:nth-child(2){
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
animation-delay: 1.3s;
}
......
以下省略到第15span元素

第一个span元素的样式设置了top、left、bottom和right属性为0,这意味着它会填充父元素的整个空间。它还设置了animation-delay属性为1.4秒,表示在加载动画开始之后1.4秒才开始播放动画。


后面14span元素都是按照这个道理,以此类推即可。通过给span元素的动画延迟属性的不同设置,可以实现加载动画的错落感和流畅的过渡效果。


总结


以上就是整个效果的实现过程了,通过设计的动画来实现这个蚊香式加载,整体还是比较简单的。大家可以去码上掘金看看完整代码,然后自己去尝试一下,如果有什么创新的地方或者遇到了什么问题欢迎在评论区告诉我~


作者:一条会coding的Shark
来源:juejin.cn/post/7291951762948259851
收起阅读 »

语雀,这波故障,放眼整个互联网也是炸裂般的存在。

你好呀,我是歪歪。 昨天语雀凉了一下午,哦,不对,下午一般是指 12 点到 18 点的这 6 个小时。 语雀是从下午 14 点到晚上 22 点多,凉了 8 小时有余。 这故障时长,放眼整个互联网也是炸裂般的存在。 我掐指一算,要是再晚个半小时修复,差点连 ...
继续阅读 »

你好呀,我是歪歪。




昨天语雀凉了一下午,哦,不对,下午一般是指 12 点到 18 点的这 6 个小时。


语雀是从下午 14 点到晚上 22 点多,凉了 8 小时有余。


这故障时长,放眼整个互联网也是炸裂般的存在。


我掐指一算,要是再晚个半小时修复,差点连 3 个 9 的(99.9%)可用性都保证不了。


如果你不知道语雀的话,我先用一句话给你铺垫一下:语雀是孵化自蚂蚁集团的。


这是它官网的自我介绍:



背靠蚂蚁,这样你再想想长达 8 小时的宕机,是不是就更加的有点匪夷所思了。


说好的高可用呢?八股文拿出来翻翻啊。


作为程序员,大家聊到这里的时候,一遍都会谈到高可用、容灾备份、两地三中心、异地多活、同城双活这些玩意..


这些东西大家聊起来都不算陌生,常常也出现于面试环节。


但是真的要做起来,是很困难的,是要以年度为时间单位进行推进的。


歪师傅没有搞过两地三中心,但是我见证过两地三中心从零开始搭建的全过程,可以说是举公司全体科技之力,耗费了巨大的人力物力,研发成本,燃烧了一个又一个老运维,才把这玩意推上去。


然而这玩意搭建起来之后,从来没有正式使用过。


没有使用,就是最好的结果。


虽然一次都没有正式使用,但是每年的灾备演练是必不可少的,演练一次,至少准备一个月,而且每年演练两次。


当场拔网线,模拟火灾报警,地震来袭,光纤挖断啥的,你以为这些是开玩笑的?


都是有预案的。宕机时间长了,


但是经过这次这个事情,我准备下次在演练的时候提一个意见:演练操作手册,电子版搞一份,打印版搞一份。万一真遇到的事情的时候,电子版打不开了,岂不是尴尬。



这件事儿也给大家提了个醒,自己写的文档,记得还是在本地留存一份。


本地化的文档,然后上传到各个云上,才是相对稳妥的方案。


比如歪师傅,写了这么多文档,在本地都有一份 md 格式的文件。我都是先用 markdown 格式在有道云上写好,没有花里胡哨的东西,写完之后只需要一键 CV 保存到本地:



不要太相信云端,我用有道云之前也丢过数据,写了一篇文章,莫名其妙的少了一大把,还找不回来。


你说气不气人嘛。


现在还能卡我脖子的就是腾讯云了,因为我的图床用的是腾讯云的图床:



要是腾讯云的对象存储挂了,那我文章中的图片也就挂了。


图片挂了,表情包也就没有了。


表情包都没有了,看文章还有啥意思啊。



这次事件之后,语雀如果不尽快给出补偿方案和离线功能,应该会流失一部分用户吧。


其他的各类笔记应用,也蠢蠢欲动,在相关的帖子下面进行宣传,想把这部分用户引流到自家产品之上。


笔记类软件现在是很卷的,除了大家耳熟能详的有道云、印象笔记、网易、为知,现在的 Notion、obsidian、Logseq......以及越来越多的本地软件,几乎可以说,每个大厂都有各自的笔记类产品,有的是偏向于在线 文档,比如金山文档、腾讯文档。有的是夹在办公软件里面,以协同为主,比如飞书文档。


这是语雀面临的外部竞争。


而语雀作为阿里系,内部还有一个钉钉文档与之赛马,而语雀的创始人玉伯于今年 4 月底离开蚂蚁,传言入职了飞书。


这就很巧了,飞书文档也很厉害。


语雀,这波属实焦灼,内忧外患啊。


虽然破局很难,但是互联网从来不缺少绝地反击,绝处逢生的故事。


语雀这次的事件确实处理的非常不好,但是这并不妨碍它是一个优秀的文档记录、协作类的产品。


然而互联网也是残酷和逐利的,当前的内忧外患之下,任何一个错误都有可能被放大,然后变成致命一击。


面对来势汹汹的这一招致命一击,语雀怎么去“接化发”。



接下来的故事,就看语雀怎么去写了。



我很期待,写出一个蜿蜒曲折、绝地反击的故事。


最后,我单方面的为“网络故障”发声,每次任何一个厂出任何一个问题要写对外公告的时候,第一个出来顶锅的,一定是“网络故障”。


惨,实在是太惨了。



作者:why技术
来源:juejin.cn/post/7293168604614377507
收起阅读 »

【Java集合】来了两个“插班生”如何打印花名册,以数组案例带你搞懂Collection集合概念

嗨~ 今天的你过得还好吗?到那时风变得软绵绵的🌞1.1 数组的特点步骤:有三个学生,放在一个长度为3的数组花名册打印学生突然来了两个插班生,请放在数组花名册中无法插入,通过重新定义一个新的数组,组成新的花名册下面我们来实现这个案例:2.输入...
继续阅读 »


嗨~ 今天的你过得还好吗?

每件事情都会好起来的

到那时风变得软绵绵的

阳光也会为你而温暖

🌞


这个系列是我在学习Java集合这部分时候,结合书籍和Java提供的api整理的部分知识,也参考了一些网络上的文章,如果错误,望大家指出。希望本系列文章对大家学习Java有所帮助,也可以回顾下这部分的基础知识,温故而知新。


集合概述

1.1 数组的特点

Java是一种面向对象语言,对一个事物的描述都是以对象的形式存在,为了方便操作这些对象,就需要把这些对象存储起来。为容纳一组对象,我们最适宜的选择就是Array数组;而且容纳一系列的基础数据类型的话,更是必须采用数组。


我们通过一个小案例来回顾一下之前的数组知识。数组不仅可以存放基本数据类型也可以容纳属于同一种类型的对象。


数组案例:一个小班有三个学生,请打印学生的姓名和年龄?

步骤:

  • 有三个学生,放在一个长度为3的数组花名册

  • 打印学生

  • 突然来了两个插班生,请放在数组花名册中

  • 无法插入,通过重新定义一个新的数组,组成新的花名册


下面我们来实现这个案例:

1.首页创建一个 javaee 的项目

Description


2.输入名称 collectPractice,选择对应的 JDK 版本 1.8

Description


3.新增 Class  student

Description

4.文件内容如下:

public class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里可以查看

5.在 Main.java 中,我们将要完成的需求写到注释中,逐行去打印结果

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

结果:

Description

6.来了两个新学生,也要加入到花名册中,直接使用数组添加,打印花名册,发现报错

import java.util.Arrays;

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

students[3] = student4;
students[4] = student5;

System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

打印结果

Description


7.所以在此时,我们需要重新 new 一个 长度为 5 的数组,重新设置新的花名册

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

// students[3] = student4;
// students[4] = student5;
//
// System.out.println("学生花名册---"+ Arrays.toString(students));

Student[] studentsNew = new Student[5];
for (int i = 0; i < students.length; i++) {
studentsNew[i] = students[i];
}
studentsNew[3] = student4;
studentsNew[4] = student5;


System.out.println("学生花名册---"+Arrays.toString(students));
System.out.println("学生新的花名册---"+Arrays.toString(studentsNew));
}
}

打印结果:

Description


分析结论

  • 数组长度在初始化后,就确定了,不能更改,不便于存储数量的扩展。比如我们再来了两个插班生,直接往元素组添加元素,会报错误信息。

  • 数组提供的属性和方法少,不便于操作。比如我们在打印花名册时,需要借助工具类的toString方法。

  • 存储的类型可以是基本类型,也可以是对象,但是必须是同一类型。


因为数组存在的这些缺点,Java语言又为我们提供了一种新的存储数据并且存储空间可变的容器,这就是我们Java集合的概念。它和数组一样,都是可以存储数据的容器,一种存储空间可变的存储模型,并且存储的数据容量可以随时发生改变。


1.2 集合的特点

最后我们来总结一下集合的特点:

  • 可以动态保存任意多的对象,使用方便;

  • 集合提供了一系列操作元素的方法,使集合元素的添加和修改等操作变得简单;

  • 集合还可以保存具有映射关系的关联数据;

  • 集合只能保存对象,实际上保存的是对象的引用地址。


文章就写到这里了,觉得不错的话点个赞支持一下吧!


收起阅读 »

他们在学校里不会教你的编程原则

前言 在大学的时候,学校一般只会教你你写编程语言,比如C、C++、JAVA等编程语言。但是当你离开大学进入这个行业开始工作时,才知道编程不只是知道编程语言、语法等,要想写好代码,必须还要了解一些编程原则才行。本文主要讨论KISS、DRY和SOLID这些常见的编...
继续阅读 »

前言


在大学的时候,学校一般只会教你你写编程语言,比如C、C++、JAVA等编程语言。但是当你离开大学进入这个行业开始工作时,才知道编程不只是知道编程语言、语法等,要想写好代码,必须还要了解一些编程原则才行。本文主要讨论KISSDRYSOLID这些常见的编程原则,而且你会发现随着工作时间越久,越能感受这些编程原则的精妙之处,历久弥香。


KISS原则



Keep It Simple, Stupid!



你是不是有过接手同事的代码感到十分头疼的经历,明明可以有更加简单、明白的写法,非要绕来绕去,看不明白?


其实,我们在写代码的时候应该要遵守KISS原则,核心思想就是尽量保持简单。代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。


我们写代码的的时候要站在别人的角度出发,就像马丁·福勒说的,我们写的代码不是给机器看的,而是给人看的。


“任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写出人类可以理解的代码。” — 马丁·福勒


那么如何才能写出满足KISS原则的代码呢?


如何写出KISS原则的代码?


我们直接上例子,下面的校验IP是否合法的3种实现方式,大家觉得哪个最KISS?



  1. 写法一




  1. 写法二




  1. 写法三




  • 写法一代码量最少,正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则的设计初衷上来讲,这种实现方式并不符合 KISS 原则。

  • 写法二使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP地址字符串,逻辑清晰,可读性好。

  • 写法三不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法,容易出bug,不好理解。


所以说,符合KISS原则的代码并不是代码越少越好,还要考虑代码是否逻辑清晰、是否容易理解、是否够稳定。


总结以下如何写出KISS原则的代码:



  1. 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。

  2. 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出bug 的概率会更高,维护的成本也比较高。

  3. 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

  4. 主观站在别人的角度上编写代码。你在编写代码的时候就要思考我这个同事看这段代码是不是很快就能够明白理解。


DRY原则



Don't Repeat Yourself



你是不是有过这样的经历,项目中很多重复逻辑的代码,然后修改一个地方,另外一个地方忘记修改,导致测试给你提了很多bug?


DRY原则,英文全称Don’t Repeat Yourself,直译过来就是不要重复你自己。这里的重复不仅仅是代码一模一样,还包括实现逻辑重复、功能语义重复、代码执行重复等。我们不要偷懒,有责任把这些存在重复的地方识别出来,然后优化它们。


如何写出DRY原则的代码呢?


我们直接上例子,代码重复的我就不讲了,很好理解,关于实现逻辑或者功能语义重复的我觉个例子。


还是上面校验IP的例子,团队中两个同事由于不知道就有了两种写法。



  • 同事A写法




  • 同事B写法



尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。不然哪天校验规则变了,很容易只改了其中一个,另外一个漏改,就会出现莫名其妙的bug


其他的比如逻辑重复的意思是虽然功能是不一致的,但是里面的逻辑都是一模一样的。举个例子,比如校验用户名和校验密码,虽然功能不一致,但是校验逻辑都是相似,判空、字符长度等等,这种情况我们就需要把相似的逻辑抽取到一个方法中,不然也是不符合DRY原则。


那么我们平时写代码注意些什么才是符合DRY原则呢?



  • 使用现成的轮子,不轻易造轮子


其实最关键的就是写代码带脑子,用到一个方法先看看有没有现成的,不要看看不看,就动手在那里造轮子。



  • 减少代码耦合


对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。



  • 满足单一职责原则


我们前面讲过,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。



  • 模块化


这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。



  • 业务与非业务逻辑分离


越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。



  • 通用代码下沉


从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。



  • 继承、多态、抽象、封装


在讲面向对象特性的时候,我们讲到,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。



  • 应用模板等设计模式


一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。


SOLID原则


SOLID原则不是一个单一的原则,而是对软件开发至关重要的 5 条原则,遵循这些原则有助于我们写出高内聚、低耦合、可扩展、可维护性好的代码。


S—单一职责原则



一个类应该有一个,而且只有一个改变它的理由。



单一职责原则在我看来是最容易理解也是最重要的一个原则。它的核心思想就是一个模块、类或者方法只做一件事,只有一个职责,千万不要越俎代庖。它可以带来下面的好处:



  • 可以让代码耦合度更低

  • 使代码更容易理解和维护

  • 使代码更易于测试和维护,使软件更易于实施,并有助于避免未来更改的意外副作用


举个例子,我们有两个类PersonAccount。 两者都负有存储其特定信息的单一责任。 如果要更改Person的状态,则无需修改类Account,反之亦然, 不要把账户的行为比如修改账户名changeAcctName写在Person类中。


    public class Person {
private Long personId;
private String firstName;
private String lastName;
private String age;
private List accounts;

// 错误做法
public void changeAcctName(Account account, String acctName) {
acccount.setAccountName(acctName);
// 更新到数据库
}
}

public class Account {
private Long guid;
private String accountNumber;
private String accountName;
private String status;
private String type;

}

所以大家在编写代码的时候,一定要停顿思考下这个段代码真的写在这里吗?另外很关键的一点是如果发现一个类或者一个方法十分庞大,那么很有可能已经违背单一职责原则了,后续维护可想而知十分痛苦。


O—开闭原则



软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。



对扩展开放,对修改关闭,什么意思?很简单,其实就是我们要尽量通过新增类实现功能,而不是修改原有的类或者逻辑。因为修改已有代码很有可能对已有功能引入bug。


让我们通过一个例子来理解这个原则,比如一个通知服务。


    public class NotificationService {
public void sendOTP(String medium) {
if (medium.equals("email")) {
//email 发送
} else if (medium.equals("mobile")) {
// 手机发送
}
}

现在需要新增微信的方式通知,你要怎么做呢? 是在加一个if else吗? 这样就不符合开闭原则了,我们看下开闭原则该怎么写。



  • 定义一个通知服务接口



public interface NotificationService {
public void sendOTP();
}


  • E-mail方式通知类EmailNotification



public class EmailNotification implements NotificationService{
public void sendOTP(){
// write Logic using JavaEmail api
}
}


  • 手机方式通知类MobileNotification



public class MobileNotification implements NotificationService{
public void sendOTP(){
// write Logic using Twilio SMS API
}
}


  • 同样可以添加微信通知服务的实现WechatNotification



public class WechatNotification implements NotificationService{
public void sendOTP(String medium){
// write Logic using wechat API
}
}

这样的方式就是遵循开闭原则的,你不用修改核心的业务逻辑,这样可能带来意向不到的后果,而是扩展实现方式,由调用方根据他们的实际情况调用。


是不是想到了设计模式中的策略模式,其实设计模式就是指导我们写出高内聚、低耦合的代码。


L—里氏替换原则



派生类或子类必须可替代其基类或父类



这个原则稍微有点难以理解,它的核心思想是每个子类或派生类都应该可以替代/等效于它们的基类或父类。这样有一个好处,就是无论子类是什么类型,客户端通过父类调用都不会产生意外的后果。


理解不了?那我我们通过一个例子来理解一下。


让我们考虑一下我有一个名为 SocialMedia 的抽象类,它支持所有社交媒体活动供用户娱乐,如下所示:


    package com.alvin.solid.lsp;

public abstract class SocialMedia {

public abstract void chatWithFriend();

public abstract void publishPost(Object post);

public abstract void sendPhotosAndVideos();

public abstract void groupVideoCall(String... users);
}

社交媒体可以有多个实现或可以有多个子类,如 FacebookWechatWeiboTwitter 等。


现在让我们假设 Facebook 想要使用这个特性或功能。


    package com.alvin.solid.lsp;

public class Wechat extends SocialMedia {

public void chatWithFriend() {
//logic
}

public void publishPost(Object post) {
//logic
}

public void sendPhotosAndVideos() {
//logic
}

public void groupVideoCall(String... users) {
//logic
}
}

我们都知道Facebook都提供了所有上述的功能,所以这里我们可以认为FacebookSocialMedia类的完全替代品,两者都可以无中断地替代。


现在让我们讨论 Weibo


    package com.alvin.solid.lsp;

public class Weibo extends SocialMedia {
public void chatWithFriend() {
//logic
}

public void publishPost(Object post) {
//logic
}

public void sendPhotosAndVideos() {
//logic
}

public void groupVideoCall(String... users) {
//不适用
}
}

我们都知道Weibo微博这个产品是没有群视频功能的,所以对于 groupVideoCall方法来说 Weibo 子类不能替代父类 SocialMedia。所以我们认为它是不符合里式替换原则。


如果强行这么做的话,会导致客户端用父类SocialMedia调用,但是实现类注入的可能是个Weibo的实现,调用groupVideoCall行为,产生意想不到的后果。


那有什么解决方案吗?


那就把功能拆开呗。


    public interface SocialMedia {   
public void chatWithFriend();
public void sendPhotosAndVideos()
}


public interface SocialPostAndMediaManager {
public void publishPost(Object post);
}



public interface VideoCallManager{
public void groupVideoCall(String... users);
}

现在,如果您观察到我们将特定功能隔离到单独的类以遵循LSP。


现在由实现类决定支持功能,根据他们所需的功能,他们可以使用各自的接口,例如 Weibo 不支持视频通话功能,因此 Weibo 实现可以设计成这样:


    public class Instagram implements SocialMedia,SocialPostAndMediaManager{
public void chatWithFriend(){
//logic
}
public void sendPhotosAndVideos(){
//logic
}
public void publishPost(Object post){
//logic
}
}

这样子就是符合里式替换原则LSP。


I—接口隔离原则



接口不应该强迫他们的客户依赖它不使用的方法。



大家可以看看自己的工程,是不是一个接口类中有很多很多的接口,每次调用API方法的时候IDE工具给你弹出一大堆,十分的"臃肿肥胖"。所以该原则的核心思想要将你的接口拆小,拆细,打破”胖接口“,不用强迫客户端实现他们不需要的接口。是不是和单一职责原则有点像?


例如,假设有一个名为 UPIPayment 的接口,如下所示


    public interface UPIPayments {

public void payMoney();

public void getScratchCard();

public void getCashBackAsCreditBalance();
}

现在让我们谈谈 UPIPayments 的一些实现,比如 Google PayAliPay


Google Pay 支持这些功能所以他可以直接实现这个 UPIPaymentsAliPay 不支持 getCashBackAsCreditBalance() 功能所以这里我们不应该强制客户端 AliPay 通过实现 UPIPayments 来覆盖这个方法。


我们需要根据客户需要分离接口,所以为了满足接口隔离原则,我们可以如下设计:



  • 创建一个单独的接口来处理现金返还。



public interface CashbackManager{
public void getCashBackAsCreditBalance();
}

现在我们可以从 UPIPayments 接口中删除getCashBackAsCreditBalanceAliPay也不需要实现getCashBackAsCreditBalance()这个它没有的方法了。


D—依赖倒置原则



高层模块不应该依赖低层模块,两者都应该依赖于抽象(接口)。抽象不应该依赖于细节(具体实现),细节应该取决于抽象。



这个原则我觉得也不是很好理解,所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。比如大家都知道的MVC模式,controller是调用service层接口这个抽象,而不是实现类。这也是我们经常说的要面向接口编程,而非细节或者具体实现,因为接口意味着契约,更加稳定。


我们通过一个例子加深一下理解。



  • 借记卡



public class DebitCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}


  • 信用卡



public class CreditCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}

现在用这两张卡你去购物中心购买了一些订单并决定使用信用卡支付


    public class ShoppingMall {
private DebitCard debitCard;
public ShoppingMall(DebitCard debitCard) {
this.debitCard = debitCard;
}
public void doPayment(Object order, int amount){
debitCard.doTransaction(amount);
}
public static void main(String[] args) {
DebitCard debitCard=new DebitCard();
ShoppingMall shoppingMall=new ShoppingMall(debitCard);
shoppingMall.doPayment("some order",5000);
}
}

上面的做法是一个错误的方式,因为 ShoppingMall 类与 DebitCard 紧密耦合。


现在你的借记卡余额不足,想使用信用卡,那么这是不可能的,因为 ShoppingMall 与借记卡紧密结合。


当然你也可以这样做,从构造函数中删除借记卡并注入信用卡。但这不是一个好的方式,它不符合依赖倒置原则。


那该如何正确设计呢?



  • 定义依赖的抽象接口BankCard



public interface BankCard {
public void doTransaction(int amount);
}


  • 现在 DebitCardCreditCard 都实现BankCard



public class CreditCard implements BankCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}


public class DebitCard implements BankCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}


  • 现在重新设计购物中心这个高级类,他也是去依赖这个抽象,而不是直接低级模块的实现类



public class ShoppingMall {
private BankCard bankCard;
public ShoppingMall(BankCard bankCard) {
this.bankCard = bankCard;
}
public void doPayment(Object order, int amount){
bankCard.doTransaction(amount);
}
public static void main(String[] args) {
BankCard bankCard=new CreditCard();
ShoppingMall shoppingMall1=new ShoppingMall(bankCard);
shoppingMall1.doPayment("do some order", 10000);
}
}

我们还可以拿 Tomcat这个 Servlet 容器作为例子来解释一下。


Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet规范。


总结


本文总结了软件编程中的黄金原则,KISS原则,DRY原则,SOLID原则。这些原则不仅仅适用于编程,也可以指导我们在架构设计上。虽然其中有些原则很抽象,但是大家多多实践和思考,会体会到这些原则的精妙。


作者:JAVA旭阳
来源:juejin.cn/post/7237037029570641979
收起阅读 »

🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

🎖️职业水平怎么样才算达到平均标准?我来告诉你 嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 毕业后进入社会,我像大家一样感到恐惧和不安。在这个新的阶段,我们都投入了大量时间和精力来从事各种社会劳动,同时也努力满足自己的经济需求。我们每个人在这个过程中都会去思考...
继续阅读 »

🎖️职业水平怎么样才算达到平均标准?我来告诉你


嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


毕业后进入社会,我像大家一样感到恐惧和不安。在这个新的阶段,我们都投入了大量时间和精力来从事各种社会劳动,同时也努力满足自己的经济需求。我们每个人在这个过程中都会去思考如何实现自己的人生价值,追求小时候美好的憧憬和期盼。💼


然而,在这个思考的过程中,没有人能为我提供确切答案。离开了学校的庇护和老师的指导,我感到比学校学习时更加困惑。未来的方向不太清晰,这使我在面对职业选择、个人发展和人生道路时遇到了许多挑战和困惑。🤔


有没有想过你职业生涯的下一步应该是什么呢?🤔


你怎么知道接下来要学习什么工具、原则或编程语言呢?📚


我想和大家分享一个超级简单的程序员分级定义思路,也许它可以帮助你这个处于职业生涯各个阶段的开发人员找到下一个目标并迈向更高的境界!✨


🚩声明:不一定正确,只是一组思路


以下的内容可能不一定正确,因为不同企业对员工能力的定义可能会有所不同。甚至每个人对这些级别的定义也会有很大的差异。🚫


然而,排除了内卷化的分级标准后,我接下来要介绍的每个阶段都代表了职业生涯中大多数人可能达到的“位置”。🎯


在每个等级之间,都存在一些过渡,可能需要在特定领域获得更多的知识和经验,也可能需要提升社交方面的知识和经验。🔀


每个等级都是在上一个等级的基础上进一步发展而设立的,我对此有着自己的职场经验启发。💡


然而,请注意:我所说的这些并不一定与你目前所处的职位相对应。 🚫


在某些公司,拥有“高级开发工程师”职称的人,实际上在技能和专业知识能力方面可能只是初级开发工程师!👨‍💻🏢


在职场中,许多人之所以被晋升,仅仅是因为他们在该领域(无论是前端、后端还是运维)有几年的经验,并非因为他们具备胜任所需的技能和知识。📚


同时,很多情况下,他们之所以成为公司中业务经验最丰富的“高级开发工程师”,仅仅是因为他们在同一家公司工作了很长时间,从而“熬掉”了许多老员工。⏳


这个世界并不公平,我相信大多数人都已经看到并经历了这种情况。🌍


当然,我还想补充一点,我所描述的这些等级并不是一成不变的标准。在你所在的领域中,有些地方对这些要求可能并不那么严格,所以你不需要过于关注我所提到的要求。🤔


以下内容仅供参考,希望能够帮助你更好地管理和掌握你未来的职业规划。说到底这仅仅是一种思路,我不是行业领袖,它仅仅是一组思路。🔍


1️⃣编程爱好者



“我有点不知道该怎么给这个阶段的 coder 定个级,算了,咱们姑且称他们为"编程爱好者"吧,但其实我还是觉得这个说法不太准确。😕”



我这里所指的“编程爱好者”是指广义上的 coder ,也就是那些会写代码或者热衷于写代码的人。💻


这些人可能有以下特征:



  1. 他们并非以“编程”为主业,而只是因为兴趣或者作为该专业的学生而加入到我们这个圈子中。对于那些以编程为职业的开发人员来说,他们算是“业余”的。🔍

  2. 这些开发爱好者了解编程语言的语法,并且能够熟练运用他们擅长的编程语言,甚至有时候比一些专业开发人员表现得更出色!📚

  3. 他们有能力独立开发一些小型项目,例如脚本、网页、游戏或应用程序。🚀

  4. 他们擅长使用搜索引擎自发解决问题。🔎

  5. 然而,在这个阶段,他们的编程能力并不能直接转化为经济利益,也就是说他们并不能通过技能获得收入。🚫


2️⃣初级开发工程师


"初级开发工程师"代表着那些已经以专业人士的身份进入IT领域的人,他们需要与其他专业人士合作,一起完成工作任务。👩‍💻


他们可能有以下特征:



  1. 他们是以编程为主要职业的专业人士,企业需要支付报酬雇佣他们加入生产。💼

  2. "初级开发工程师"会被分配到一个或多个项目中工作,但他们可能无法完全理解整个项目的结构,因为对于他们来说,项目可能还是“太大”了。🔨 在这个阶段,他们更多地承担一些被拆分成小模块的任务,对于项目的整体认识,他们并不清晰。🔎

  3. 他们可能只对自己专业领域有了解,在工作中需要继续学习前后端通信和数据库连接等跨系统的知识。📚

  4. 他们需要在中级开发工程师或高级开发工程师的指导下完成工作。🤝



“这些特征是一般情况下的描述,具体的职位要求和工作内容可能因公司和行业而异。📋💼”



3️⃣中级开发工程师


到了"中级开发工程师"阶段,他们已经适应了业内的整体开发节奏,成为了一名合格的开发团队成员和代码贡献者。🚀


在这个阶段,他们具备以下特征:



  1. 能够独立构建业务模块,并熟悉最佳实践。例如,在Web应用中开发单点登录模块。🏗️

  2. 开始了解项目的基本系统架构,对领域内的架构、性能和安全性有一定的了解。🏢

  3. 能够熟练使用专业工具来提高工作效率。🛠️

  4. 对设计模式和良好的编码习惯有基本的了解。🎨

  5. 能够在常规工作中独立操作,无需过多监督。💪

  6. 对于高级开发工程师来说,他们可能缺乏经验,需要经历几次完整的开发周期和遇到很多“坑”之后,才能学会如何在下次避免它们。🔍



“这个阶段的开发工程师最缺乏的就是项目实践经验。只要有不断地项目经历,通过实践和经验积累,他们就会不断成长。🌱”



4️⃣高级开发工程师


遗憾的是我们中大多数人在职业生涯中大部分时间都在面临从“中级开发工程师”到“高级开发工程师”的门槛。


有些“开发工程师”可能在整个职业生涯中一直停留在中级水平。


“高级开发工程师”之所以与众不同,是因为他们知道什么可以做,什么不可以做。这种洞察力是通过过去犯过的错误和经验教训获得的。


开发经验对于成为“高级开发工程师”至关重要。


根据我的理解,“高级开发工程师”应该具备以下特征:



  1. 精通团队所使用的核心技术,对其应用得非常熟练。💪

  2. 熟悉系统架构设计和设计模式,并能够在团队项目中应用这些概念,构建更复杂的系统。🏢

  3. 拥有构建“完整”解决方案的经验,能够考虑到项目的各个方面并提供全面的解决方案。🔍

  4. 在服务器部署和维护方面有一定的经验,了解负载平衡、连接池等跨领域知识。🖥️

  5. 作为团队的核心成员,能够担任导师的角色,积极指导中级和初级开发工程师。👥


其中最后一条是最最重要的。如果不能把你的经验、专业知识和知识传授给你的团队成员,我认为这就不是一个合格的“高级开发工程师”。


成为“高级开发工程师”的一个重要指标:一定是团队的其他成员经常向你寻求建议和帮助



“如果你还在沮丧为什么同事老是问我问题,也许现在可以改变一下想法了。💼


因为你是你们团队最重要的百科全书呢!也许现在是时候考虑向老板提出加薪的要求了呢?💰”



5️⃣开发领袖



这个阶段我也有点困惑,不知道要给他们这个等级取一个准确的称号。我想了两个名字:“高级架构师”和“团队领导者”,但是我又想,其实高级工程师也可以领导团队,也有架构能力啊。那就还是加“领袖”两个字,突出在技术领域的高级能力、团队领导能力和架构能力。这样看起来就更厉害了!👨‍💼



在这个阶段,程序员们已经不再仅仅为一个团队服务。他们可能同时为多个团队提供支持,并向下属团队提供更底层的指导,特别是在设计和早期产品开发阶段。💪


在国内,由于很难找到同时在业务领域和专业领域都深耕的人才,这类职位可能被企业分拆为不同的职能,更加注重管理能力而非专业能力。 🤔最终可能招聘了一个“高级监工”(毕竟,同时在业务领域和专业领域同时深耕的人真的少之又少,而且一般企业也不愿意花费与之对等的报酬)。


因此,大部分人可能会不同意我这个阶段的观点。 😕开发领袖的职能范围可能涵盖“敏捷教练(scrum master)”、“DevOps”、“项目经理(PM)”、“CTO”等管理职务。


因此,开发领袖最重要的特征是:



  1. 对业务领域有深刻的理解,能够消除开发团队与企业其他业务部门之间的沟通障碍。🌐

  2. 发挥"PM"职能: 协助规划产品开发和时间表,向营销或销售团队提供反馈。📈

  3. 发挥"CTO"职能: 协助高层管理,实现企业愿景,领导开发团队实现企业的业务目标。📊


因此,开发领袖必须对所处的业务领域(如医疗、金融、人力资源等)的产品有深入的了解。🏥 基于这些了解,他们能够理解软件所解决的业务问题,并且必须了解其他学科,如管理、产品开发、营销等,以消除各部门合作之间的沟通障碍。


简而言之,高级开发工程师和开发领袖的区别在于:



  1. 高级开发工程师也担任团队领导的角色,但主要面向开发团队的“内部”。👥

  2. 开发领袖则超越团队内部管理,他们的管理职能是面向“外部”的,致力于消除开发团队与公司其他部门之间的沟通障碍。🌍


因此,成为开发领袖需要具备高层领导的全局视野,并能够将业务术语和技术术语相互转化。🔑


如果你能够在公司内很好地与业务同事交流技术解决方案,并让其理解,那么你已经拥有了“开发领袖”其一的核心能力。💡


6️⃣领域专家


这个阶段的他们已经跳出了企业的限制,在一些特定领域也颇负盛名。他们的解决方案不再是只为一家企业服务,他们擅长的领域也不是一般的学科分类,而是一个非常有针对性地细分领域。🚀


可惜的是,一般的开发者们很难接触到这些领域,你想要了解他们的知识都不知道从哪儿下手,因为他们的知识分享大多是封闭的,只在内部共享,不对外传播。🔒



“可能你会觉得这与你对开源软件行业的理解不太一样,开源难道不是互联网发展的第一推动力吗?是啊,我同意你的观点,但你不了解不代表它不存在。其实大部分的技术分享都是在内部进行的,许多讲座和峰会也只限邀请制🔐。”



他们可能是某种编程语言的奠基人,可能是Web安全领域的重要任务驱动者,也可能是教导其他前端开发者如何使用React的大师,甚至还有那些在特定行业中扮演技术导师角色的人!👨‍💻


他们还可能是某个社区的建设者,在互联网和社会上有一群人将他们视为直接或间接的导师。🏢


他们也可能是支持特定事业或理念,并为之做出显著贡献的思想领袖。💡


他们会公开地讨论自己的专业领域和他们所推崇的理念。🗣️



“如果你也有自己的小圈子。比如在掘金社区;比如在GITHUB,拥有自己的互联网开源项目,并且有一大群粉丝用户支持和拥护你的产品和理念。那你也可以算是某一细分领域的专家了。👥”



总而言之,他们的一举一动都可能对互联网技术的发展产生重大影响。😄




🎉 你觉得怎么样?你认为自己处于哪个阶段?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


作者:道长王jj
来源:juejin.cn/post/7240838046789353530
收起阅读 »

消息推送的实现方式

短轮询(Long Polling) 网络资源:短轮询会产生大量的网络请求,尤其是当客户端轮询间隔很短时。这可能会导致大量的网络开销。 服务器处理:对于每个轮询请求,服务器需要处理该请求并发送响应,即使没有新的数据。这会导致服务器频繁地处理请求,可能增加CPU...
继续阅读 »

短轮询(Long Polling)



  1. 网络资源:短轮询会产生大量的网络请求,尤其是当客户端轮询间隔很短时。这可能会导致大量的网络开销。

  2. 服务器处理:对于每个轮询请求,服务器需要处理该请求并发送响应,即使没有新的数据。这会导致服务器频繁地处理请求,可能增加CPU和内存的使用。

  3. 总结:如果更新频率很低,但客户端仍然频繁地发送请求,短轮询可能会造成资源浪费,因为大多数响应可能只是告知“无新数据”


长轮询(Long Polling)



  • 客户端发送请求到服务器,服务器如果没有准备好的数据,就保持连接开放,直到有数据可以发送。一旦数据被发送,客户端处理数据后再次发送新的请求,如此循环。

  • 长轮询通常用于实时或近实时的通知和更新,比如活动通知。



  1. 网络资源:相比短轮询,长轮询减少了无效的网络请求。服务器只在有新数据时才发送响应,从而减少了网络流量。

  2. 服务器处理:长轮询可能导致服务器需要维护更多的打开连接,因为它会为每个客户端请求保持一个打开的连接,直到有新数据或超时。这可能会增加服务器的内存使用,并可能达到服务器的并发连接限制。

  3. 总结:长轮询在某些场景下可以提供更高效的资源使用,尤其是当数据更新不频繁但需要快速传递给客户端时。但如果有大量的客户端同时进行长轮询,服务器可能需要处理大量的并发打开连接。


WebSocket:



  • WebSocket提供了一个全双工通信通道,使得服务器和客户端可以在任何时刻发送数据给对方。这是一个非常实时且高效的解决方案。适合实时聊天



  1. 网络资源:WebSocket 在建立连接后只需要一个握手过程,之后数据可以在此连接上双向传输,不需要为每条消息进行新的请求和响应。这极大地减少了网络开销。

  2. 服务器处理:一旦 WebSocket 连接被建立,它将保持打开状态,直到客户端或服务器决定关闭它。这意味着服务器必须维护所有活动的 WebSocket 连接,这可能会消耗内存和其他资源。

  3. 总结:WebSocket 在数据频繁更新并且需要实时传递给客户端的场景中非常有效。尽管需要维护持久连接,但由于减少了网络开销,通常更为高效。


服务器发送事件(Server-Sent Events, SSE) :



  • 服务器发送事件是一种使服务器能够发送新数据到客户端的简单方法。它比WebSocket简单,但只允许服务器向客户端发送数据。活动通知和提醒



  1. 网络资源:与 WebSocket 类似,SSE 也只需要一次握手来建立持久连接。一旦连接建立,服务器可以持续地向客户端推送消息。

  2. 服务器处理:SSE 需要维护持久连接以发送数据,但与 WebSocket 相比,SSE 只是单向的。这意味着服务器不需要处理从客户端发来的消息。

  3. 总结:SSE 是一种高效的技术,适用于只需要服务器向客户端推送数据的场景,例如实时消息通知。


HTTP/2 Server Push:




  • HTTP/2协议支持服务器推送,允许服务器在客户端需要之前预先发送数据。这可以减少延迟,但通常只用于发送关联的资源,如CSS或JavaScript文件,而不是用于通用的消息推送。




  • 主要用于提前发送关联资源如CSS、JavaScript文件,以减少加载时间,提高网页性能。




  • 可以减少网页加载时间,提高用户体验。




  • 不适用于通用的消息推送,且需要HTTP/2协议支持,实现可能需要特定的服务器配置。




MQTT协议


MQTT 全称(Message Queue Telemetry Transport):一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。


该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的MQ有点类似。


TCP协议位于传输层,MQTT 协议位于应用层,MQTT 协议构建于TCP/IP协议上,也就是说只要支持TCP/IP协议栈的地方,都可以使用MQTT协议。


为什么要用 MQTT协议?


MQTT协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP协议呢?



  • 首先HTTP协议它是一种同步协议,客户端请求后需要等待服务器的响应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合IOT应用程序。

  • HTTP是单向的,如果要获取消息客户端必须发起连接,而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。

  • 通常需要将一条命令或者消息,发送到网络上的所有设备上。HTTP要实现这样的功能不但很困难,而且成本极高。


第三方推送服务:



  • 使用如Firebase Cloud Messaging (FCM), Apple Push Notification Service (APNs)等第三方推送服务来处理消息推送。


对比


WebSocket和Server-Sent Events提供了较低的延迟和较高的实时性,但可能需要更多的服务器资源。长轮询可能会有更高的延迟,并且可能不是最高效的解决方案。HTTP/2 Server Push和第三方推送服务可能更适合于不需要高度实时性的应用。消息队列和发布/订阅模型提供了一种解耦服务器和客户端的方式,但可能会增加系统的复杂性。


在选择实现方法时,需要考虑应用的具体需求,例如实时性的要求、服务器资源、网络条件以及开发和维护的复杂性。同时,也可以考虑将几种方法结合使用,以满足不同的需求。



  • 如果有大量的客户端并且数据更新不频繁,长轮询可能比短轮询更为有效,因为它减少了无效的网络请求。

  • 如果服务器有并发连接的限制或资源有限,大量的长轮询请求可能会耗尽资源,导致服务器不稳定。

  • 如果数据更新非常频繁,短轮询可能会比较合适,因为它可以更简单地处理频繁的请求。

  • WebSocket 通常在需要实时通信的应用中更为有效和资源高效。它减少了网络开销,并提供了持续的、低延迟的双向通信。

  • 短轮询长轮询 可能更适合不需要持续连接的场景或当 WebSocket 不可用或不适用时的备选方案。

  • WebSocket:提供双向通信,适用于需要实时双向交互的应用,如在线聊天。由于它是全双工的,可能需要更多的资源来处理双向的消息传输。

  • SSE:提供单向通信,适用于只需要服务器推送数据的应用,如股票行情更新。通常,SSE 比 WebSocket 更轻量,因为它只处理单向通信。

  • 短轮询:可能会产生大量网络开销,特别是在数据更新频繁的场景中。

  • 长轮询:减少了网络开销,但可能需要服务器维护大量的打开连接,直到有新数据或超时。


从资源消耗的角度看:



  • WebSocketSSE 都需要维护持久连接,但通常比短轮询和长轮询更高效,因为它们减少了网络开销。

  • SSE 可能比 WebSocket 更轻量,因为它是单向的。

  • 短轮询 可能是最耗资源的,尤其是在频繁请求且数据更新不频繁的场景中。

  • 长轮询 在某些情况下可能比短轮询更高效,但仍然不如 WebSocket 或 SSE。


作者:Pomelo_刘金
来源:juejin.cn/post/7291464815658172471
收起阅读 »

我本可以忍受黑暗,如果我未曾见过光明

【随想录】我本可以忍受黑暗,如果我未曾见过光明 随想录 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 老文章? 这篇文章大体结构早已在我语...
继续阅读 »

【随想录】我本可以忍受黑暗,如果我未曾见过光明



随想录


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



老文章?


这篇文章大体结构早已在我语雀里写完了很久很久~~~


假期就有构思了,现在埋坑


因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学一起上课,而且下一届还是我带的班级,想想那种感觉“咦,武哥你怎么在这上课”而我,内心qs:杀了我把,太羞辱了,脚指头已经扣除一套四合院了)


朋友问我成绩,当时孩子都傻了


所以这段时间我正在经历自我内耗,就向是欠了谁东西,到了deadline,到了审判的日子才能释怀!也至于最近心理一直在想着这个事情,导致最近焦虑的一批,最近几天自己都不正常了,但是终于结束了~~~(非常感谢老师)



言归正传


好了好了,又跑题了,书归正题,你可能会疑惑我为什么用这个标题,难道我经历了什么涩会黑暗,被潜规则,被PUA......(给你个大逼斗子,停止瞎想,继续向下看)



这篇文章灵感来源于我很喜欢的B站一位高中语文老师讲解《琵琶行》,突然我被这个短短 3分51秒的视频搞得愣住了,直接神游五行外,大脑开始快速的回顾自己最近的生活~~~(再次表白真的很爱这摸温柔的语文老师,他的课真的让我感觉到什么叫“腹有诗书气自华”)



视频链接:https://www.bilibili.com/video/BV1bW4y1j7Un/

最爱的语文老师


其实人生当中很残忍的一个事儿是什么呢?就是你一直以为未来有无限可能的时候,就像琵琶女觉得她能够过上那样的生活一直下去。一直被“五陵年少争缠头”,一直被簇拥着的时候,突然有一天你意识到好像这辈子就只能这样,就只能去来江头守空船,守着这一这艘空船,默默的度过慢慢的长夜。
就是如果如果你不曾体验过那样的生活,你会觉得好像“我”最终嫁给了一个商人,然后至少衣食不愁,至少也能活得下去,好像也还算幸福。但是如果我曾经经历过那样的生活,我此刻内心多多少少是有些不甘的。


很喜欢的一幅油画


亦或者是像白居易,如果他是从平民起身,然后一直一步一步做到了江州司马可能觉得也还是不错,但是你要知道他在起点就是在京城为官,所以这里其实是有很明显的,一种落差。那也同样,如果此刻你回到我们说所有的文学都是在读自己,你想想看你自己,此刻你可能没有这种感觉。


30公里鲜啤



哈哈哈,兄弟们不要emo啊,让我们珍惜当下,还是那句话,我们还年轻,谁都不怕。(但是遇到刀枪棍棒还是躲一躲呀,毕竟还是血肉之躯)



其实反思反思人生中最大的挑战,就是接受自己生来平凡。自己没有出色的外表,我也没有过人的才华,我可能也少了些许少年时的锐意。但是这个emo点我并不care,因为我还在拥有选择的阶段,我也在尝试探索不一样的人生,这也许就是喜欢记录生活和写下灵机一动时候想法的意义。但是也就向UP主@peach味的桃子记录自己第44次开学,也是最后一次开学表达自己点点滴滴,也同样是不同的感受;我们同样有应届生的迷茫,但是想想也没什么可怕,还在学习,还在向目标奔跑,也还在享受校园生活~~~


打卡老馆子-群乐饭店


啊呀,好像又唠跑偏了,就是说我对这个视频那么的不一样,尤其是这个主题,因为自己的寒假的实习给我带来了新的视野,哦不,应该是旷野,很有幸能去华为在我们省份的办事处,又被出差派往华为在一个某市分部工作了半个月。这短短的实习经历,让我在大三这个迷茫的时期多了份坚定,在这个期间和大佬们一起工作,真的看到了人家的企业文化和那种行动力,最主要被军团的大佬们很牛掰技术折服,在相处这段时间真的知道了什么是向往的生活,这个学历门槛迈过去,你将会迎来什么样的明天~~~


(谁说我去卖手机去了,我揍他啊[凶狠])


游客打卡照


所以我可能对之前年终总结看法有了些改变,我之前年终总结写到,薪资又不会增加多少,浪费三年那不纯属XX嘛,没错,今天我被打脸了,为我之前的幼稚想法感到可笑;写到这里脑子已经开始疼了,最近甲流,朋友圈注意身体,这个东西真的会影响我们的战斗力,好吧,这也只是一个随想录,留点内容给年中总结,要不到时候就词穷了,哈哈~~


很nice的江景房


近期反思


其实每个人的出发点不一样不能一概而论,就向我自己出发,一个来自十八线农村的孩子,父母通过自己一代人的努力从农村到乡镇,而我就通过自己的求学之路一直到,貌似能够在这个省份的省会立足,这也就是我能做的进步,不管怎么说,我们都是从自身出发,其实谈到这个问题,我自身也很矛盾,小城市就真的不好吗,人的一生除了衣食无忧,在向下追求的不就是快乐,如果真的能和一个爱的人,在做一些自己喜欢做的事情,难道不就是“人生赢家”,城市在这种维度下考虑貌似也不重要~~(如果你想diss这种想法,没有考虑子女的教育问题,其实我想到了,但是我目前的年龄和所处的位置吧,感觉很片面,所以就不对这个点展开讨论了)


过度劳累,小酌一杯


回复问题


有人怕别人看到自己以往的文章写的很幼稚,就不想写了,我有不同的看法,只有看到曾经的对事情的看法和处理方式幼稚了,才能证明自己的成长呀,谁能一下子从孩子成为一个大人!(但是某些时候谁还是不是一个孩子[挑眉])



作者:武师叔
来源:juejin.cn/post/7208476031136792631
收起阅读 »

"我的领导只任用关系户,我的团队里全是嫡系。"

说这句话的同学,跟我一起来认真的思考一下,你认为的那个关系户跟领导最初也是陌生人。既然关系户能跟领导搞好关系,你是不是也能跟领导搞好关系?现在给你个任务:跟领导搞好关系,请问你准备怎么做呢? 拍马屁吗?跟领导睡觉吗?你去试一试,看看管不管用。我相信你自己也知道...
继续阅读 »

说这句话的同学,跟我一起来认真的思考一下,你认为的那个关系户跟领导最初也是陌生人。既然关系户能跟领导搞好关系,你是不是也能跟领导搞好关系?现在给你个任务:跟领导搞好关系,请问你准备怎么做呢?


拍马屁吗?跟领导睡觉吗?你去试一试,看看管不管用。我相信你自己也知道,你每天吹捧领导"您真英明",领导只会觉得你是个傻逼。


一个部门有两个同事小A和小B,小A是一个身材曼妙的美女,谁见了都把持不住。但是业务能力一般,一个月只能给公司赚5万块;小B是书呆子理工男,业务能力比较好,一个月能给公司赚10万。


假设你是一个极端好色的男领导。


现在有两种情况,第1种情况,季度销售目标8个亿,两个月过去了,你只实现1个亿,再这样下去,你非得被开掉不可。这样你会把资源偏向哪个同事?


第2种情况,季度目标1个亿,你已经实现了8个亿,你会偏向哪个同事?


你跟漂亮女同事睡了觉,账上的钱会自动多出来吗?7个亿的销售额压力估计压得你都会不举吧?就算领导再好色,只要他心智还正常,肯定也会先偏向小B,把自己的位置稳固住,然后再用自己的位置去泡妞。


在巨大的压力下,人们会克制自己任性的偏好。反过来说,在巨大的压力下,人们做出的抉择,往往不是任性的偏好。


再来讲讲逻辑学


相关性不等于因果性, A事件和b事件总是同时发生,有可能是a事件导致了b事件,也有可能是b事件导致了a事件,也有可能是a和b由共同的一个看不见的c事件导致。


你的领导跟小a关系很好,小a晋升了,所以你得出一个结论。你的领导偏爱关系户。


有没有可能是另外一种情况?小a能力很强,表现很好,所以他晋升了。同时因为他表现好所以领导跟他关系很好?


请用严谨的逻辑来做一个排他性证明,证明我说的这种情况是错的。


还要说说利益


职场是赤裸裸的利益场,除了利益以外一无所有。


在职场里a跟b关系好,只有一种可能,就是b能满足a的利益;如果两个人互相关系好,那就是他们两个是利益共同体。


你去拍领导的马屁,领导完全不会屌你。如果你去分析领导正在焦虑的事情,跟他一起出谋划策,帮他解决,他肯定立刻跟你关系就上升了。不仅如此,如果他下次有了新的焦虑的事情,一筹莫展,做不了决定,他也会第一时间来问问你的意见。在这种时候,领导对你的需要比你对领导的需要更迫切,他就算不喜欢你,他也得硬着头皮跟你点头哈腰。


旁人看了就会觉得这个人是领导的嫡系,他俩关系真好。这种不合逻辑的论调,只不过是自己把握核心利益走向能力不足的无能狂怒,是吃不着葡萄说葡萄酸的低级防御机制。


这么多年来我在每个团队都很受领导器重,但我从来不点头哈腰,我从来都是该吵架就吵架,一点儿都不客气。因为我准确的知道他的利益点,也就可以准确的评估领导对我的需要程度,他不爽也得忍着。(他不需要我的时候我就乖乖的,大丈夫能屈能伸)。不懂门道的外人看来,我就是关系户。


"关系户"这个词特别好,道德上是不正当的,同时也不是谁能都能做到,所以它是一个非常完美的心理防御的盾牌。信奉"关系户"的人,往往既不会搞关系,也不会搞工作。认真搞一次关系就知道,搞关系比搞工作难多了。


要是你真的觉得你的团队任用关系户,那也请你认真的去搞关系。不过我猜到时候你会抱怨公司不好好"搞关系"净"搞工作"了,那些上位的人全都是"工作狂"。


拍马屁这种门槛这么低的事情,要是真的管用的话,竞争的激烈程度会超乎你的想象,你可能连领导的脚后跟都见不到。


职场是赤裸裸的利益场,除了利益以外一无所有。反过来说,那个你认为的"关系户",跟你的领导之间也是赤裸裸的利益关系。


你买东西的时候是希望卖东西的人越多越好还是越少越好?你卖东西的时候是希望买东西的人越多越好还是越少越好?所以你的领导是希望自己的团队里,牛逼的人越多越好还是越少越好?


所以哪个领导会任由自己团队的某个成员一家独大?


"我能力强,我的领导却不任用我"


人这种生物天生就有高看自己一眼的趋势的,人很难公平的对待自己和别人。所以当一个人跟我说他牛逼的时候,我心里第一时间的反应就是"你是真牛逼还是吹牛逼?"。


我平常在日常需求任务分配的时候,经常会遇到一个现象,一个PM提需求的时候指名道姓希望某研发同学来开发。此时另外一个需求的PM也立刻跳起来说他的需求也希望这个研发同学来开发。两个PM原地开始互相PK起了需求价值,我打圆场说团队还有别的空余人力,两个PM实在不好意思继续掰扯了才作罢。


两个PM不仅在人力的维度展开竞争,而且在某个具体的研发同学身上展开了竞争。



职场是赤裸裸的利益场,你的工作表现直接影响着别人的利益;反过来别人的表现也影响着你的利益。研发开发需求快质量高,可以直接的换算成需求在绩效周期内能给客户稳定使用的可能性,这个又可以直接换算成绩效,绩效又可以直接换算成年终奖。所以你的工作能力会被你的同事赤裸裸换算成真金白银,没有人会跟白花花的银子过不去,所以你真的优秀的话,别人是可以敏锐的感觉到的。


在高压的环境下,真实的你自己活在别人的眼睛里。


"我们团队确实是有嫡系的呀!"


团队里的成员加入团队的顺序有先有后,小A小B小C在团队一开始最艰难的时候跟领导一起搭建起了这条业务线。所以他们了解这个团队发展过程中的诸多细节,甚至可以说这个团队的文化是他们共同创造的。


你上半年刚加入团队,总共也没做几个需求。你要是领导,你更信任谁?有一个容易出成绩又很有挑战的项目,你更愿意交给谁?


你比别人晚来,不代表你能力比别人差,但你是不是一定就能力比别人好?还不确定。而前几个人的能力好坏领导经过多年磨合已经一清二楚了,对他来说,这个项目交给 ta 确定性就更大一些,风险更小一些。


容易出成绩的工作总是很稀缺的,所以这些工作一定要集中给更有希望晋升的人,领导的心态就是扶上去一个算一个。如果这样的工作分摊给好几个人,那好几个人都没有足够的业绩晋升,最后大家都不满意。


所有人都不满意和至少有一个人满意,是你的话你选哪一种?


这个内容甚至是写在教人当领导的书里,作为标准教程的。


再说说另外一种情况


公司要成立一个新业务线,需要迅速搭建一个20人的团队。老板认识一个别的公司的领导,那个领导表示要带着自己目前的团队一起来这个公司。老板听了大喜过望,恨不能跪在地上给对方磕俩响头。要知道想要搭建一个20人的团队,在招聘上要花费多少人力资源成本。而且招聘一个20人团队,至少需要半年的时间。人找齐了还不能立刻投入工作,彼此还得磨合。


现在好了,有人带着一个团队直接过来。成本的问题时间的问题磨合的问题一口气全部解决。


对方带了15个人过来,离20个人还差5个。于是小G被调配进入了这个团队。这个时候虽然名义上是对方几个新人加入了小G的公司,但实际上真实发生的事情是小G加入了对方的团队。所以贡献要沿着团队的历史记忆延续和排列。


小G可能会感觉到巨大的不公平,但是你尝试从老板的视角看,小G的这种"不公平"的感受,能值得起组建一个20人团队的费用吗?你的"公平"值几十万吗?


给大家几点建议。


第一,对于打工人来说,持续在同一个公司同一个团队长期工作才是最大化自己利益的最好方式,频繁跳槽的人是非常短视的。如果你加入了一个新团队,要保持耐心,耐心是一个良好的品格。


第二,机遇真的很重要,我相信这句话你已经听腻了。但是之前你听这句话只有一个宏观而含混的理解,我相信你读完这篇文章就对这句话有一个形象而具体的理解。


第三,这个世界不是公平的,如果你觉得世界是公平的,那只能说明你自己幼稚。你假设了一个"公平"的世界,但却发现真实的世界不是公平的,于是内心世界陷入了巨大的冲突,每日活在精神内耗里。请问谁许诺过你这个世界是公平的了吗?许诺给你的那个人有足够的权威保证世界是公平的吗?


要心平气和的接受世界不是公平的,然后在"世界不不公平"的基础上组织自己的想法和行动,以达到自己利益的最大化。这样就能避免很多无意义的内耗。


上面这段话好像我在PUA你,但是别忘了我只是在脉脉匿名圈顶着一个马甲的陌生人,我PUA你对我没有任何好处。然而我发自肺腑地说,我自己就是这么想的,从我是一个小兵开始就是这个想的,也是这么做的,我用这套想法获得了今天的一切,我善意的把这些想法分享给你,希望避免你的困扰。你爱信不信。


最后的最后想说一句,如果有一天你掌握了权力,我希望你能公平的对待他人,咱们就别再为这个世界的操蛋添砖加瓦了,这是一种修养。


作者:马可奥勒留
来源:juejin.cn/post/7288178532861378600
收起阅读 »

SQL为什么动不动就N百行以K计?

发明SQL的初衷之一显然是为了降低人们实施数据查询计算的难度。SQL中用了不少类英语的词汇和语法,这是希望非技术人员也能掌握。确实,简单的SQL可以当作英语阅读,即使没有程序设计经验的人也能运用。 然而,面对稍稍复杂的查询计算需求,SQL就会显得力不从心,经常...
继续阅读 »

发明SQL的初衷之一显然是为了降低人们实施数据查询计算的难度。SQL中用了不少类英语的词汇和语法,这是希望非技术人员也能掌握。确实,简单的SQL可以当作英语阅读,即使没有程序设计经验的人也能运用。


然而,面对稍稍复杂的查询计算需求,SQL就会显得力不从心,经常写出几百行有多层嵌套的语句。这种SQL,不要说非技术人员难以完成,即使对于专业程序员也不是件容易的事,常常成为很多软件企业应聘考试的重头戏。三行五行的SQL仅存在教科书和培训班,现实中用于报表查询的SQL通常是以“K”计的。


SQL困难的分析探讨


这是为什么呢?我们通过一个很简单的例子来考察SQL在计算方面的缺点。


设有一个由三个字段构成的销售业绩表(为了简化问题,省去日期信息):


sales_amount销售业绩表
sales销售员姓名,假定无重名
product销售的产品
amount该销售员在该产品上的销售额

现在我们想知道出空调和电视销售额都在前10名的销售员名单。


这个问题并不难,人们会很自然地设计出如下计算过程:


1. 按空调销售额排序,找出前10名;


2. 按电视销售额排序,找出前10名;


3. 对1、2的结果取交集,得到答案;


我们现在来用SQL做。


1. 找出空调销售额前10名,还算简单:


select top 10 sales from sales_amount where product='AC' order by amount desc

2. 找出电视销售额前10名。动作一样:


select top 10 sales from sales_amount where product='TV' order by amount desc

3. 求1、2的交集。这有点麻烦,SQL不支持步骤化,上两步的计算结果无法保存,只能再重抄一遍了:


select * from
( select top 10 sales from sales_amount where product='AC' order by amount desc )
intersect
( select top 10 sales from sales_amount where product='TV' order by amount desc )

一个只三步的简单计算用SQL要写成这样,而日常计算中多达十几步的比比皆是,这显然超出来许多人的可接受能力。


我们知道了SQL的第一个重要缺点:不支持步骤化。把复杂的计算分步可以在很大程度地降低问题的难度,反过来,把多步计算汇成一步则很大程度地提高了问题的难度。


可以想象,如果老师要求小学生做应用题时只能列一个算式完成,小朋友们会多么苦恼(当然,不乏一些聪明孩子搞得定)。


SQL查询不能分步,但用SQL写出的存储过程可以分步,那么用存储过程是否可以方便地解决这个问题呢?


暂先不管使用存储过程的技术环境有多麻烦和数据库的差异性造成的不兼容,我们只从理论上来看用分步SQL是否能让这个计算更简单捷些。


1. 计算空调销售额前10名。语句还是那样,但我们需要把结果存起来供第3步用,而SQL中只能用表存储集合数据,这样我们要建一个临时表:


create temporary table x1 as
select top 10 sales from sales_amount where product='AC' order by amount desc

2. 计算电视销售额前10名。类似地


create temporary table x2 as
select top 10 sales from sales_amount where product='TV' order by amount desc

3. 求交集,前面麻烦了,这步就简单些


select * from x1 intersect x2

分步后思路变清晰了,但临时表的使用仍然繁琐。在批量结构化数据计算中,作为中间结果的临时集合是相当普遍的,如果都建立临时表来存储,运算效率低,代码也不直观。


而且,SQL不允许某个字段取值是集合(即临时表),这样,有些计算即使容忍了繁琐也做不到。


如果我们把问题改为计算所有产品销售额都在前10名的销售员,试想一下应当如何计算,延用上述的思路很容易想到:


1. 将数据按产品分组,将每组排序,取出前10名;


2. 将所有的前10名取交集;


由于我们事先不知道会有多个产品,这样需要把分组结果也存储在一个临时表中,而这个表有个字段要存储对应的分组成员,这是SQL不支持的,办法就行不通了。


如果有窗口函数的支持,可以转换思路,按产品分组后,计算每个销售员在所有分组的前10名中出现的次数,若与产品总数相同,则表示该销售员在所有产品销售额中均在前10名内。


select sales
from ( select sales,
from ( select sales,
rank() over (partition by product order by amount desc ) ranking
from sales_amount)
where ranking <=10 )
group by sales
having count(*)=(select count(distinct product) from sales_amount)

这样的SQL,有多少人会写呢?


况且,窗口函数在有些数据库中还不支持。那么,就只能用存储过程写循环依次计算每个产品的前10名,与上一次结果做交集。这个过程比用高级语言编写程序并不简单多少,而且仍然要面对临时表的繁琐。


现在,我们知道了SQL的第二个重要缺点:集合化不彻底。虽然SQL有集合概念,但并未把集合作为一种基础数据类型提供,这使得大量集合运算在思维和书写时都需要绕路。


我们在上面的计算中使用了关键字top,事实上关系代数理论中没有这个东西(它可以被别的计算组合出来),这不是SQL的标准写法。


我们来看一下没有top时找前10名会有多困难?


大体思路是这样:找出比自己大的成员个数作为是名次,然后取出名次不超过10的成员,写出的SQL如下:


select sales
from ( select A.sales sales, A.product product,
(select count(*)+1 from sales_amount
where A.product=product AND A.amount<=amount) ranking
from sales_amount A )
where product='AC' AND ranking<=10


select sales
from ( select A.sales sales, A.product product, count(*)+1 ranking
from sales_amount A, sales_amount B
where A.sales=B.sales and A.product=B.product AND A.amount<=B.amount
group by A.sales,A.product )
where product='AC' AND ranking<=10

这样的SQL语句,专业程序员写出来也未必容易吧!而仅仅是计算了一个前10名。


退一步讲,即使有top,那也只是使取出前一部分轻松了。如果我们把问题改成取第6至10名,或者找比下一名销售额超过10%的销售员,困难仍然存在。


造成这个现象的原因就是SQL的第三个重要缺点:缺乏有序支持。SQL继承了数学上的无序集合,这直接导致与次序有关的计算相当困难,而可想而知,与次序有关的计算会有多么普遍(诸如比上月、比去年同期、前20%、排名等)。


SQL2003标准中增加的窗口函数提供了一些与次序有关的计算能力,这使得上述某些问题可以有较简单的解法,在一定程度上缓解SQL的这个问题。但窗口函数的使用经常伴随着子查询,而不能让用户直接使用次序访问集合成员,还是会有许多有序运算难以解决。


我们现在想关注一下上面计算出来的“好”销售员的性别比例,即男女各有多少。一般情况下,销售员的性别信息会记在花名册上而不是业绩表上,简化如下:


employee员工表
name员工姓名,假定无重名
gender员工性别

我们已经计算出“好”销售员的名单,比较自然的想法,是用名单到花名册时找出其性别,再计一下数。但在SQL中要跨表获得信息需要用表间连接,这样,接着最初的结果,SQL就会写成:


select employee.gender,count(*)
from employee,
( ( select top 10 sales from sales_amount where product='AC' order by amount desc )
intersect
( select top 10 sales from sales_amount where product='TV' order by amount desc ) ) A
where A.sales=employee.name
group by employee.gender

仅仅多了一个关联表就会导致如此繁琐,而现实中信息跨表存储的情况相当多,且经常有多层。比如销售员有所在部门,部门有经理,现在我们想知道“好”销售员归哪些经理管,那就要有三个表连接了,想把这个计算中的where和group写清楚实在不是个轻松的活儿了。


这就是我们要说的SQL的第四个重要困难:缺乏对象引用机制,关系代数中对象之间的关系完全靠相同的外键值来维持,这不仅在寻找时效率很低,而且无法将外键指向的记录成员直接当作本记录的属性对待,试想,上面的句子可否被写成这样:


select sales.gender,count(*)
from (…) // …是前面计算“好”销售员的SQL
group by sales.gender

显然,这个句子不仅更清晰,同时计算效率也会更高(没有连接计算)。


我们通过一个简单的例子分析了SQL的四个重要困难,这也是SQL难写或要写得很长的主要原因。基于一种计算体系解决业务问题的过程,也就是将业务问题的解法翻译成形式化计算语法的过程(类似小学生解应用题,将题目翻译成形式化的四则运算)。SQL的上述困难会造成问题解法翻译的极大障碍,极端情况就会发生这样一种怪现象:将问题解法形式化成计算语法的难度要远远大于解决问题本身


再打个程序员易于理解的比方,用SQL做数据计算,类似于用汇编语言完成四则运算。我们很容易写出3+5*7这样的算式,但如果用汇编语言(以X86为例),就要写成


    mov ax,3
mov bx,5
mul bx,7
add ax,bx

这样的代码无论书写还是阅读都远不如3+5*7了(要是碰到小数就更要命了)。虽然对于熟练的程序员也算不了太大的麻烦,但对于大多数人而言,这种写法还是过于晦涩难懂了,从这个意义上讲,FORTRAN确实是个伟大的发明。


为了理解方便,我们举的例子还是非常简单的任务。现实中的任务要远远比这些例子复杂,过程中会面临诸多大大小小的困难。这个问题多写几行,那个问题多写几行,一个稍复杂的任务写出几百行多层嵌套的SQL也就不奇怪了。而且这个几百行常常是一个语句,由于工程上的原因,SQL又很难调试,这又进一步加剧了复杂查询分析的难度。


更多例子


我们再举几个例子来分别说明这几个方面的问题。


为了让例子中的SQL尽量简捷,这里大量使用了窗口函数,故而采用了对窗口函数支持较好的ORACLE数据库语法,采用其它数据库的语法编写这些SQL一般将会更复杂。
这些问题本身应该也算不上很复杂,都是在日常数据分析中经常会出现的,但已经很难为SQL了。


计算不分步


把复杂的计算分步可以在很大程度地降低问题的难度,反过来,把多步计算汇成一步完成则会提高问题的复杂度。



任务1 销售部的人数,其中北京籍人数,再其中女员工人数?



销售部的人数


select count(*) from employee where department='sales'

其中北京籍的人数


select count(*) from employee where department='sales' and native_place='Beijing'

再其中的女员工人数


select count (*) from employee
where department='sales' and native_place='Beijing' and gender='female'

常规想法:选出销售部人员计数,再在其中找出其中北京籍人员计数,然后再递进地找出女员工计数。每次查询都基于上次已有的结果,不仅书写简单而且效率更高。


但是,SQL的计算不分步,回答下一个问题时无法引用前面的成果,只能把相应的查询条件再抄一遍。



任务2 每个部门挑选一对男女员工组成游戏小组



with A as
(select name, department,
row_number() over (partition by department order by 1) seq
from employee where gender=‘male’)
B as
(select name, department,
row_number() over(partition by department order by 1) seq
from employee where gender=‘female’)
select name, department from A
where department in ( select distinct department from B ) and seq=1
union all
select name, department from B
where department in (select distinct department from A ) and seq=1

计算不分步有时不仅造成书写麻烦和计算低效,甚至可能导致思路严重变形。


这个任务的直观想法:针对每个部门循环,如果该部门有男女员工则各取一名添进结果集中。但SQL不支持这种逐步完成结果集的写法(要用存储过程才能实现此方案),这时必须转变思路为:从每个部门中选出男员工,从每个部门选出女员工,对两个结果集分别选出部门出现在另一个结果集的成员,最后再做并集。


好在还有with子句和窗口函数,否则这个SQL语句简直无法看了。


集合无序


有序计算在批量数据计算中非常普遍(取前3名/第3名、比上期等),但SQL延用了数学上的无序集合概念,有序计算无法直接进行,只能调整思路变换方法。



任务3 公司中年龄居中的员工



select name, birthday
from (select name, birthday, row_number() over (order by birthday) ranking
from employee )
where ranking=(select floor((count(*)+1)/2) from employee)

中位数是个常见的计算,本来只要很简单地在排序后的集合中取出位置居中的成员。但SQL的无序集合机制不提供直接用位置访问成员的机制,必须人为造出一个序号字段,再用条件查询方法将其选出,导致必须采用子查询才能完成。



任务4 某支股票最长连续涨了多少交易日



select max (consecutive_day)
from (select count(*) (consecutive_day
from (select sum(rise_mark) over(order by trade_date) days_no_gain
from (select trade_date,
case when
closing_price>lag(closing_price) over(order by trade_date)
then 0 else 1 END rise_mark
from stock_price) )
group by days_no_gain)

无序的集合也会导致思路变形。


常规的计算连涨日数思路:设定一初始为0的临时变量记录连涨日期,然后和上一日比较,如果未涨则将其清0,涨了再加1,循环结束看该值出现的最大值。


使用SQL时无法描述此过程,需要转换思路,计算从初始日期到当日的累计不涨日数,不涨日数相同者即是连续上涨的交易日,针对其分组即可拆出连续上涨的区间,再求其最大计数。这句SQL读懂已经不易,写出来则更困难了。


集合化不彻底


毫无疑问,集合是批量数据计算的基础。SQL虽然有集合概念,但只限于描述简单的结果集,没有将集合作为一种基本的数据类型以扩大其应用范围。



任务5 公司中与其他人生日相同的员工



select * from employee
where to_char (birthday, ‘MMDD’) in
( select to_char(birthday, 'MMDD') from employee
group by to_char(birthday, 'MMDD')
having count(*)>1 )

分组的本意是将源集合分拆成的多个子集合,其返回值也应当是这些子集。但SQL无法表示这种“由集合构成的集合”,因而强迫进行下一步针对这些子集的汇总计算而形成常规的结果集。


但有时我们想得到的并非针对子集的汇总值而是子集本身。这时就必须从源集合中使用分组得到的条件再次查询,子查询又不可避免地出现。



任务6 找出各科成绩都在前10名的学生



select name
from (select name
from (select name,
rank() over(partition by subject order by score DESC) ranking
from score_table)
where ranking<=10)
group by name
having count(*)=(select count(distinct subject) from score_table)

用集合化的思路,针对科目分组后的子集进行排序和过滤选出各个科目的前10名,然后再将这些子集做交集即可完成任务。但SQL无法表达“集合的集合”,也没有针对不定数量集合的交运算,这时需要改变思路,利用窗口函数找出各科目前10名后再按学生分组找出出现次数等于科目数量的学生,造成理解困难。


缺乏对象引用


在SQL中,数据表之间的引用关系依靠同值外键来维系,无法将外键指向的记录直接用作本记录的属性,在查询时需要借助多表连接或子查询才能完成,不仅书写繁琐而且运算效率低下。



任务7 女经理的男员工们



用多表连接


select A.*
from employee A, department B, employee C
where A.department=B.department and B.manager=C.name and
A.gender='male' and C.gender='female'

用子查询


select * from employee
where gender='male' and department in
(select department from department
where manager in
(select name from employee where gender='female'
))

如果员工表中的部门字段是指向部门表中的记录,而部门表中的经理字段是指向员工表的记录,那么这个查询条件只要简单地写成这种直观高效的形式:


where gender='male' and department.manager.gender='female'

但在SQL中则只能使用多表连接或子查询,写出上面那两种明显晦涩的语句。



任务8 员工的首份工作公司



用多表连接


select name, company, first_company
from (select employee.name name, resume.company company,
row_number() over(partition by resume. name
order by resume.start_date) work_seq
from employee, resume where employee.name = resume.name)
where work_seq=1

用子查询


select name,
(select company from resume
where name=A.name and
start date=(select min(start_date) from resume
where name=A.name)) first_company
from employee A

没有对象引用机制和彻底集合化的SQL,也不能将子表作主表的属性(字段值)处理。针对子表的查询要么使用多表连接,增加语句的复杂度,还要将结果集用过滤或分组转成与主表记录一一对应的情况(连接后的记录与子表一一对应);要么采用子查询,每次临时计算出与主表记录相关的子表记录子集,增加整体计算量(子查询不能用with子句了)和书写繁琐度。


SPL的引入


问题说完,该说解决方案了。


其实在分析问题时也就一定程度地指明了解决方案,重新设计计算语言,克服掉SQL的这几个难点,问题也就解决了。


这就是发明SPL的初衷!


SPL是个开源的程序语言,其全名是Structured Process Language,和SQL只差一个词。目的在于更好的解决结构化数据的运算。SPL中强调了步骤化、支持有序集合和对象引用机制、从而得到彻底的集合化,这些都会大幅降低前面说的“解法翻译”难度。


这里的篇幅不合适详细介绍SPL了,我们只把上一节中的8个例子的SPL代码罗列出来感受一下:



任务1



AB
1=employee.select(department=="sales")=A1.len()
2=A1.select(native_place=="Beijing")=A2.len()
3=A2.select(gender=="female")=A3.len()

SPL可以保持记录集合用作中间变量,可逐步执行递进查询。



任务2



ABC
1for employee.group(department)=A1.group@1(gender)
2>if B1.len()>1=@|B1

有步骤和程序逻辑支持的SPL能很自然地逐步完成结果。



任务3



A
1=employee.sort(birthday)
2=A1((A1.len()+1)/2)

对于以有序集合为基础的SPL来说,按位置取值是个很简单的任务。



任务4



A
1=stock_price.sort(trade_date)
2=0
3=A1.max(A2=if(close_price>close_price[-1],A2+1,0))

SPL按自然的思路过程编写计算代码即可。



任务5



A
1=employee.group(month(birthday),day(birthday))
2=A1.select(~.len()>1).conj()

SPL可以保存分组结果集,继续处理就和常规集合一样。



任务6



A
1=score_table.group(subject)
2=A1.(~.rank(score).pselect@a(~<=10))
3=A1.(~(A2(#)).(name)).isect()

使用SPL只要按思路过程写出计算代码即可。



任务7



A
1=employee.select(gender=="male" && department.manager.gender=="female")

支持对象引用的SPL可以简单地将外键指向记录的字段当作自己的属性访问。



任务8



A
1=employee.new(name,resume.minp(start_date).company:first_company)

SPL支持将子表集合作为主表字段,就如同访问其它字段一样,子表无需重复计算。


SPL有直观的IDE,提供了方便的调试功能,可以单步跟踪代码,进一步降低代码的编写复杂度。


imagepng


对于应用程序中的计算,SPL提供了标准的JDBC驱动,可以像SQL一样集成到Java应用程序中:



Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
Statement st = connection.();
CallableStatement st = conn.prepareCall("{call xxxx(?,?)}");
st.setObject(1, 3000);
st.setObject(2, 5000);
ResultSet result=st.execute();
...


SPL资料



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

为了方便写文章,我开发了一个目录树🌲生成器

web
这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。 您可以通过以下链接访问:目录树生成器 - 在线使用 Next.js 是一个React全栈框架,它不仅可以用于...
继续阅读 »

这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。


image.png


您可以通过以下链接访问:目录树生成器 - 在线使用


Next.js


是一个React全栈框架,它不仅可以用于构建服务器端渲染(SSR),也支持支持静态渲染。


webkitdirectory



HTMLInputElement.webkitdirectory 是一个反应了 HTML 属性 webkitdirectory 的属性,其指示 <input> 元素应该让用户选择文件目录而非文件。在选择文件目录后,该目录及其整个内容层次结构将包含在所选项目集内。可以使用 webkitEntries (en-US) 属性获取选定的文件系统条目。

———————MDN



简而言之 利用这属性,我们可以在浏览器中上传文件夹,并获取到文件的目录结构。


可以看一个简单的栗子🌰


这个功能,也有一个兼容问题,具体参考这个:


image.png


有一些老版本的浏览器和安卓端火狐浏览器不支持的无法使用该功能。


数据转换


我们要将原数据转换一下


-   Java/main/main.java
- Java/main/main.class
- Java/hello/HelloWorld.class
- Java/hello/HelloWorld.java
- Java/OOP/xx.js
- Java/OOP/Person.class
- Java/OOP/oop.class
- Java/OOP/oop.java

转换为:


{
"name": "Java",
"type": "folder",
"contents": [
{
"name": "main",
"type": "folder",
"contents": [
{
"name": "main.java",
"type": "file"
},
{
"name": "main.class",
"type": "file"
}
]
},
{
"name": "hello",
"type": "folder",
"contents": [
{
"name": "HelloWorld.class",
"type": "file"
},
{
"name": "HelloWorld.java",
"type": "file"
}
]
},
{
"name": "OOP",
"type": "folder",
"contents": [
{
"name": "xx.js",
"type": "file"
},
{
"name": "Person.class",
"type": "file"
},
{
"name": "oop.class",
"type": "file"
},
{
"name": "oop.java",
"type": "file"
}
]
}
]
}

将路径结构转化为对象结构,方便我们的后续逻辑处理,转化方法是:



function convertToDirectoryStructure(fileList) {
const directory = {
name: "App",
type: "folder",
contents: [],
};

for (let i = 0; i < fileList.length; i++) {
const pathSegments = fileList[i].webkitRelativePath.split("/");
let currentDirectory = directory;

for (let j = 0; j < pathSegments.length; j++) {
const segment = pathSegments[j];
const isDirectory = j < pathSegments.length - 1;

let existingEntry = currentDirectory.contents.find((entry) => entry.name === segment);
if (!existingEntry) {
existingEntry = { name: segment };
if (isDirectory) {
existingEntry.type = "folder";
existingEntry.contents = [];
} else {
existingEntry.type = "file";
}
currentDirectory.contents.push(existingEntry);
}

currentDirectory = existingEntry;
}
}

return directory.contents[0];
}

最终效果


最后我们再加上一个一键复制的功能,就完成了。



最后我是将功能优化后部署到了GitHub Pagas上,如何将Next.js部署到GitHub Pages,可以看看我的这篇 如何将Next.js部署到Github Pages


最后希望大家多多使用,给个star,GitHub地址:dir-tree


作者:九旬
来源:juejin.cn/post/7292955000454692875
收起阅读 »

离职原因千万不要这样说!

HR 面作为面试的最后一关,非常重要,因为走到这一步,你已经和 Offer 只有一步之遥了。有人会认为:只要进入 HR 面就稳了,其实并不是! 在一个公司里,HR 拥有最终的人事任命权,部门主管只能提供用人建议,所以这一关千万不要大意,我每年都有学生挂在 HR...
继续阅读 »

HR 面作为面试的最后一关,非常重要,因为走到这一步,你已经和 Offer 只有一步之遥了。有人会认为:只要进入 HR 面就稳了,其实并不是!


在一个公司里,HR 拥有最终的人事任命权,部门主管只能提供用人建议,所以这一关千万不要大意,我每年都有学生挂在 HR 面。那么,今天我们就来聊聊“离职原因”这个话题。


1.同事或领导不行


虽然你在工作中,可能会遇到与同事或上司之间的不和谐关系,但直接将其作为离职原因,会对你的职业形象造成负面影响。


你应该这样回答



前公司的办公室政治比较严重,同事和领导之间相互推诿、扯皮的想象比较常见,工作效率也被无限拉低。我希望找一家能把精力放在高效工作的公司,同事之间分工明确、协同合作,同事之间氛围能好一点的公司,因为一个人的大部分时间都在公司,心情愉快的高效工作是非常重要的。



2.被裁员


这两年被裁员是比较常见的事,但直接说被裁了,可能会让 HR 觉得你的价值不高,因为那么多人,为什么偏偏把你裁了?是不是你的能力不如其他人?


你应该这样回答



原业务线被砍了,领导安排我转岗到其他业务线。但是新岗位和我的职业规划不相符,我还是想继续在XX方向深耕,所以我离职了。



3.学不到东西


如果直接说“学不到东西”,会让用人单位觉得你就是来学东西的,过段时间把东西学会了,也会从这离职的。


你应该这样回答



在原公司所负责的工作内容比较单一且重复性比较高,无法满足我个人的职业发展需求。我想找一个更有挑战性、并且更有成长空间的工作。



4.工资低


抱怨工资低,虽然工资低可能是客观原因,但是工资低也可能是因为你能力或经验的问题,所以因为工资低而离职,会让用人单位怀疑你的专业性和稳定性。


你应该这样回答



这几年我的技术以及业务能力已经得到了显著的提升,为公司创造XXX的业绩,领导对我也很认可,但是原公司的工资的涨幅非常有限,而现在需要用钱的地方很多,所以我想看看新机会。



5.加班多


互联网是典型的“薪资高、加班多”的公司,直接在离职原因中说加班多,可能会错失很多机会。


你应该这样回答



前公司常态化 996,但实际的工作量并不大,导致大部分人为了加班而加班,效率非常低,我个人并不反对加班,但这种低效的 996,我并不认可。



6.其他不能说的事



  1. 说上一家领导和公司的坏话,会给人留下一个不能融入环境或者缺乏团队意识的印象。

  2. 我男朋友/女朋友在这边工作,感情用事是职场大忌。

  3. 我爸妈不喜欢我原来的那份工作,没有主见也是职场大忌。


7.离职原因这样说


在原公司所负责的工作内容比较单一且重复性比较高,无法满足我个人的职业发展需求。我想找一个更有挑战性、并且有更大成长空间的工作。而贵公司的情况非常符合我的预期:



  • 首先,我很认可贵公司所推崇的人性化管理,非常符合我对工作环境的预期,我也相信在这样的环境中,我能发挥更大的主观能动性。

  • 并且我非常看好贵公司所处的行业和所做的事,如果我能有幸加入贵公司,相信一定能和贵公司一起发展、共同进步。


小结


HR 面作为面试的最后一关,非常重要,因为走到这一步,你已经和 Offer 只有一步之遥了。所以在回答离职原因时,不能太刻薄、不能太过于“现实”,要把刻薄的话委婉的说,要把个人的事儿往“大”的说。


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

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
收起阅读 »

通过问题透析 IDEA Debug

引言 本来通过问题引入,一步一步对问题进行分析,重点学习 IDEA Debug 的能力解决问题。阅读本文可以学习如何通过 IDEA 的 Debug 功能解决实际问题。本文适合学生和刚工作的朋友,把 IDEA 作为开发工具,并且有 Spring 和 JPA 的使...
继续阅读 »

引言


本来通过问题引入,一步一步对问题进行分析,重点学习 IDEA Debug 的能力解决问题。阅读本文可以学习如何通过 IDEA 的 Debug 功能解决实际问题。本文适合学生和刚工作的朋友,把 IDEA 作为开发工具,并且有 Spring 和 JPA 的使用经验。


问题引入


最近看了 eclipse 开源的集合 Eclipse Collections,觉得它的使用相比 JDK 集合更加简洁,想在实际项目中使用。部分 API 对比如下。


JDK API


 //users is List<User> 
users.stream.map(user -> user.getCity()).collect(Collectors.toList());

Eclipse Collections API


 //users is MutableList<User>
users.collect(user -> user.getCity);

可以看到后者比前者要简洁不少。实际开发中集合数据大多还是来自数据库查询,使用 JPA 查询如下。


JDK API


List<User> findByCity(String city);

我想改成 Eclipse Collections API


MutableList<User> findByCity(String city);

然而报错了


org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.util.ArrayList<?>] to type [org.eclipse.collections.api.list.MutableList<?>] for value '[]'; nested exception is java.lang.IllegalArgumentException: Unsupported Collection interface: org.eclipse.collections.api.list.MutableList
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175)

如果不想看过过程,结论是改成如下代码或者升级 sping boot 到 2.7.0 及以上版本。


FastList<User> findByCity(String city);

Debug


对代码简单分析



  • 查看类名称,方法名称。 有 convert.ConversionFailedException/ConversionUtils.invokeConverter/convert.support.GenericConversionService.convert等等,关键词 convert。我们应该联想到这段代码的功能是把某一个类型 convert 到某一个类型。

  • 再看一眼报错信息,Failed to convert from type [java.util.ArrayList<?>] to type [org.eclipse.collections.api.list.MutableList<?>],无法将 ArrayList 转换成 MutableList

  • 再分析报错的那一行return converter.convert(source, sourceType, targetType),我们会更清晰一点。

    • result 是转换的结果,应该是 MutableList 的一个实例。

    • convert 方法是执行转换的核心逻辑,我们要的核心转换逻辑代码肯定在这里,如果你直接去看的话,它肯定是一个接口,这是面向接口编程。

    • sourceType 源类型,是 ArrayList 类型。

    • targetType 目标类型,是 MutableList 类型。




打断点


在 IDEA 控制台可以直接点击报错 class 定位到源文件,我们先点击 ConversionFailedException ,再点击 ConversionUtils.java:47,发现都是报错的异常,对我们没有帮助。最后我们点击 GenericConversionService.java:192,终于看到一行代码了。


Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);

断点分析


执行过程会停留在断点处,我们可以查看上下文变量类的实例。这里我们以 converter 为例。按照数字步骤点击,如下。


image.png


图中显示是 ConvertertoString 方法的结果。
可能的 converter 如下:


1. java.lang.String -> java.lang.Enum
2. NO_OP
3. java.lang.Boolean -> java.lang.String
// 等等。。。。。

由于是底层方法,被调用的次数很多,在这个断点停留的次数也很多。很多次不是我们想要的 Converter


条件断点


顾名思义 IDEA 会通过我们添加的条件来判断这个断点是否需要被处理。


我们想要的 Converter 是什么呢?回到代码分析阶段,我们想要的 ConvertersourceTypetargetType,通过上面分析 targetType 类型是 MutableList 类型。


下面添加条件断点:


image.png

完整的条件如下:


MutableList.class.isAssignableFrom(targetType.getType());

添加成功的标志如下,会在断点处显示问号。


image.png


单步调试


Debug 模式启动程序,可以看到 IDEA 停留在我们的条件断点上,并且targetType 的类型正是 MutableList


image.png


单步调试代码,来到 org.springframework.core.CollectionFactory#createCollection 方法。部分代码如下:


//省略的其他代码

// 判断集合类型是不是 ArrayList 或者 List,显然这里不是
else if (ArrayList.class == collectionType || List.class == collectionType) {
return new ArrayList<>(capacity);
}
//省略的其他代码

else {
//如果是集合类型的接口 或者 不是集合类型抛出异常
if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
}
try {
//如果是集合类型的类,直接通过反射实例化。
return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();
}
}

重回代码分析


上面的 collectionTypeMutableList,而 MutableList 是接口,走读代码可以发现最终会执行下面的代码,最终导致抛出异常。


if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
}

所以只需要我们的目标集合不是接口就行了,FastListMutableList 的实现类。 修改代码为如下:


FastList<User> findByCity(String city);

翻看控制台找到了下面的异常信息,这也侧面反映我们之前找的报错位置不是很精确。我们寻找异常时应该选择最原始的异常信息。


Caused by: java.lang.IllegalArgumentException: Unsupported Collection type: org.eclipse.collections.api.list.MutableList
at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:205)
at org.springframework.core.convert.support.CollectionToCollectionConverter.convert(CollectionToCollectionConverter.java:81)

继续分析源码可以发现,如果我们定义的类型不是接口,JPA 就会通过反射创建集合,即如下代码:


return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();

总结


本来通过解决实际问题介绍了 IDEA Debug 功能的使用。还有以下几点需要注意。



  • 查找异常时要定位到最初始的异常,这样往往能迅速处理问题。

  • 本文的问题只有在 sping boot 2.7.0 以下才会出现,高版本已经修复此问题。参见提交 spring data common

  • 使用非 Java 官方集合需要进行转换,有微小的性能损耗,对于常规内存操作来说影响很小,而且高版本中有优化。如果查询数据上千上万条时,应该避免转换,当然也要使用分页避免一次性查询成千上万的数据。


本文源码


作者:郁乎文
来源:juejin.cn/post/7185569129024192568
收起阅读 »