注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

普通的文本输入框无法实现文字高亮?试试这个highlightInput吧!

web
背景 前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到...
继续阅读 »

背景


前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到:没办法,这是老板提的需求,你下去研究研究吧。行吧,老板发话说啥也没用,开干吧!


实现思路


实现标红就需要给文字加上html标签和样式,但是输入框会将html都转为字符串,既然输入框无法实现,那么我们换一种思路,通过div代替输入框来显示输入的文本,那我们是不是就可以实现文本标红了?话不多说,直接上代码(文章结尾会附上demo):


<div class="main">
<div id="shadowInput" class="highlight-shadow-input"></div>
<textarea
id="textarea"
cols="30"
rows="10"
class="highlight-input"
>
</textarea>
</div>

.main {
position: relative;
}
.highlight-shadow-input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
padding: 8px;
border: 1px;
box-sizing: border-box;
font-size: 12px;
font-family: monospace;
overflow-y: auto;
word-break: break-all;
white-space: pre-wrap;
}
.highlight-input {
position: relative;
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
background: rgba(0, 0, 0, 0);
-webkit-text-fill-color: transparent;
z-index: 999;
word-break: break-all;
}

实现这个功能的精髓就在于将输入框的背景和输入的文字设置为透明,然后将其层级设置在div之上,这样用户既可以在输入框中输入,而输入的文字又不会展示出来,然后将输入的文本处理后渲染到div上。


const textarea = document.getElementById("textarea");
const shadowInput = document.getElementById("shadowInput");
const sensitive = ["敏感词", "禁用词"];
textarea.oninput = (e) => {
let value = e.target.value;
sensitive.forEach((word) => {
value = value.replaceAll(
word,
`<span style="color:#e52e2e">${word}</span>`
).replaceAll("\n", "<br>");;
});
shadowInput.innerHTML = value;
};

监听输入框oninput事件,用replaceAll匹配到敏感词并转为html后渲染到shadowInput上。此外,我们还需要对输入框的滚动进行监听,因为shadowInput是固定高度的,如果用户输入的文本出现滚动条,则需要让shadowInput也滚动到对应的位置


<div><div id="shadowInput" class="highlight-shadow-input"></div></div>

textarea.onscroll = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};
// 此处输入时也需要同步是因为输入触底换行时,div的高度不会自动滚动
textarea.onkeydown = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};

最终实现效果:


至此一个简单的文本输入框实现文字高亮的功能就完成了,上述代码只是简单示例,在实际业务场景中还需要考虑xss注入、特殊字符处理、特殊字符高亮等等复杂问题。


总结


这篇文章主要给遇到有类似业务需求的同学一个参考,以及激发大家的灵感,用这种方法是不是还可以实现一些简单的富文本功能呢?例如文字加删除线、文字斜体加粗等等。有想法或有问题的小伙伴可以在评论区留言一起探讨哦!


demo



作者:宇智波一打七
来源:juejin.cn/post/7295169886177918985
收起阅读 »

多行标签超出展开折叠功能

web
前言  记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。  今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,...
继续阅读 »

前言


 记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。
 今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,类似于多行文本展开折叠功能,如果超过最大行数则显示展开隐藏按钮,如果不超过则不显示按钮。多行文本展开与折叠功能在网上有相当多的文章了,也有许多开源的封装组件,而多行标签展开隐藏的文章却比较少,刚好最近我也遇到了这个功能,所以就单独拿出来与大家分享如何实现。


出处


 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种APP的搜索页面的历史记录这里,下面是我从拼多多(左)和腾讯学堂小程序(右)截下来的功能样式:


多行标签案列图(pdd/txxt)


其它APP一般搜索的历史记录这里都有这个小功能,比如京东、支付宝、淘宝、抖音、快手等,可能稍有点儿不一样,有的是按钮样式,有的是只有展开没有收起功能,可能我们用过了很多年平时都没有注意到这个小功能,有想了解的可以去看一看哈。如果有一天你们需要开发一个搜索页面的话产品就很有可能出这样的一个功能,接下来我们就来看看这种功能我们该如何实现。


功能实现


我们先看实现的效果图,然后再分析如何实现,效果图如下:



【样式一】:标签容器和展开隐藏按钮分开(效果图样式一)


 标签容器和按钮分开的这种样式功能实现起来的话我个人觉得难度稍微简单一些,下面我们看看如何实现这种分开的功能。


第一种方法:通过与第一个标签左偏移值对比实现

原理:遍历每个标签然后通过与第一个标签左偏移值对比,如果有几个相同偏移值则说明有几个换行


具体实现上代码:


<div class="list-con list-con-1">
<div class="label">人工智能div>
<div class="label">人工智能与应用div>
<div class="label">行业分析与市场数据div>
<div class="label">标签标签标签标签标签标签标签标签div>
<div class="label">标签div>
<div class="label">啊啊啊div>
<div class="label">宝宝贝贝div>
<div class="label">微信div>
<div class="label">吧啊啊div>
<div class="label">哦哦哦哦哦哦哦哦div>
div>
<div class="expand expand-1">展开 ∨div>



解析:HTML布局就不用多说了,是个前端都知道该怎么搞,如果不知道趁早送外卖去吧,多说无益,把机会留给其他人。其次CSS应该也是比较简单的,注意的是有个前提需要先规定容器的最大高度,然后使用overflow超出隐藏,这样展开就直接去掉该属性,让标签自己撑开即可。JavaScript部分我这里没有使用啥框架,因为这块实现就是个简单的Demo所以就用纯原生写比较方便,这里我们先获取容器,然后获取容器的孩子节点(这里我们也可以直接通过className查询出所有标签元素),返回的是一个可遍历的变签对象,然后我们记录第一个标签的offsetLeft左偏移值,接下来遍历所有的标签元素,如果有与第一个标签相同的值则累加,最终line表示有几行,如果超过我们最大行数(demo超出2行隐藏)则显示展开隐藏按钮。


第二种方法:通过计算容器高度对比

原理:通过容器底部与标签top比较,如果有top值大于容器底部bottom则表示超出容器隐藏。


具体上代码:




解析:HTMLCSS同方法一同,不同点在于这里是通过getBoundingClientRect()方法来判断,还是遍历所有标签,不同的是如果有标签的top值大于等于了容器的bottom值,则说明了标签已超出容器,则要显示展开隐藏按钮,展开隐藏还是通过容器overflow属性来实现比较简单。


【样式二】:展开隐藏按钮和标签同级(效果图样式二)


 这种样式也是绝大部分APP产品使用的风格,不信你可以打开抖音商城或汽车之家的搜索历史,十个产品九个是这样设计的,不是这样的我倒立洗头。
 这种放在同级的就相对稍微难一点,因为要把展开隐藏按钮塞到标签的最后,如果是隐藏的话就要切割标签展示数量,那下面我就带大家看看我是是如何实现的。


方法一:通过遍历高度判断

原理:同样式一的高度判断一样,通过容器底部bottom与标签top比较,如果有top值大于容器顶部bottom则表示超出容器隐藏,不同的是如何计算标签展示的长度。有个前提是按钮和标签的的宽度要做限制,最好是一行能放一个标签和按钮。


具体实现上代码:


<div id="app3">
<div class="list-con list-con-3" :class="{'list-expand': isExpand}">
<div class="label" v-for="item in labelArr.slice(0, labelLength)">{{ item }}div>
<div class="label expand-btn" v-if="showExpandBtn" @click="changeExpand">{{ !isExpand ? '展开 ▼' : '隐藏 ▲' }}div>
div>
div>


<script>
const { createApp, nextTick } = Vue
createApp({
props: {
maxLine: {
type: Number,
default: 2
}
},
data () {
return {
labelArr: [],
isExpand: false,
showExpandBtn: false,
labelLength: 0,
hideLength: 0
}
},
mounted () {
const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']

this.labelArr = labels
this.labelLength = labels.length
nextTick(() => {
this.init()
})
},
methods: {
init () {
const listCon = document.querySelector('.list-con-3')
const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
const expandBtn = listCon.querySelector('.expand-btn')

let labelIndex = 0 // 渲染到第几个
const listConBottom = listCon.getBoundingClientRect().bottom // 容器底部距视口顶部距离
for(let i = 0; i < labels.length; i++) {
const _top = labels[i].getBoundingClientRect().top
if (_top >= listConBottom ) { // 如果有标签顶部距离超过容器底部则表示超出容器隐藏
this.showExpandBtn = true
console.log('第几个索引标签停止', i)
labelIndex = i
break
} else {
this.showExpandBtn = false
}
}
if (!this.showExpandBtn) {
return
}
nextTick(() => {
const listConRect = listCon.getBoundingClientRect()
const expandBtn = listCon.querySelector('.expand-btn')
const expandBtnWidth = expandBtn.getBoundingClientRect().width
const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
for (let i = labelIndex -1; i >= 0; i--) {
const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
this.hideLength = i + 1
this.labelLength = this.hideLength
break
}
}
})
},
changeExpand () {
this.isExpand = !this.isExpand
console.log(this.labelLength)
if (this.isExpand) {
this.labelLength = this.labelArr.length
} else {
this.labelLength = this.hideLength
}
}
}
}).mount('#app3')
script>


解析:同级样式Demo我们使用vue来实现,HTML布局和CSS样式没有啥可说的,还是那就话,不行真就送外卖去比较合适,这里我们主要分析一下Javascript部分,还是先通过getBoundingClientRect()方法来获取容器的bottom和标签的top,通过遍历每个标签来对比是否超出容器,然后我们拿到第一个超出容器的标签序号,就是我们要截断的长度,这里是通过数组的slice()方法来截取标签长度,接下来最关建的如何把按钮拼接上去,因为标签的宽度是不定的,我们要把按钮显示在最后,我们并不确定按钮拼接到最后是不是会导致宽度不够超出,所以我们倒叙遍历标签,如果(最后一个标签的右边到容器的距离right值+标签的margin值+按钮的width)和小于容器宽度,则说明展示隐藏按钮可以直接拼接在后面,否则标签数组长度就要再减一位来判断是否满足。然后展开隐藏功能就通过切换原标签长度和截取的标签长度来完成即可。


方法二:通过与第一个标签左偏移值对比实现

原理:同样式一的方法原理,遍历每个标签然后通过与第一个标签左偏移值对比判断是否超出行数,然后长度截取同方法一一致。


直接上代码:




这里也无需多做解释了,直接看代码即可。


结尾


上面就是【多行标签展开隐藏】功能的基本实现原理,网上相关实现比较少,我也是只用了Javascript来实现,如果可以纯靠CSS实现,有更简单或更好的方法实现可以留言相互交流学。代码没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库。




作者:Liben
来源:juejin.cn/post/7251394142683742269
收起阅读 »

村超,淄博烧烤,哈尔滨,本质都在做情绪价值这门生意

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。 还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。 如果叫南方小豆角,南方小冬瓜,我相信大多数人就会急了,因为...
继续阅读 »

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。


还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。


如果叫南方小豆角南方小冬瓜,我相信大多数人就会急了,因为这两个词听起来都不可爱,还会有地域歧视的意思。


虽然都是蔬菜,但是南方小土豆就不一样,虽然大家都知道里面有南方人矮的意思,但是大多数人不反感,因为现在很多家长都会叫自己的孩子小土豆,养一个宠物也有叫土豆,所以说土豆,其实带有可爱的意思。


所以就南方小土豆这个称呼,就带动了很大的流量,就打破了这么多年来对东北的刻板映像。


但是就因为一个称呼就能带动那么多的人赶往东北吗?就为了去听一句南方小土豆吗?


我想显然不是,也并不是谁都愿意听的,也有一些游客去以后说反感这个称呼。


那么究其原因,其实本质还是在做情绪价值这门生意。


而情绪价值的背后是什么?


是长久积攒的急需释放的情绪和看透生活后的无能为力。


怎么理解呢?


我发现一个现象,包括我本人也是这样。


在疫情之前,我在很多地方看到有人卖唱,下面的人基本都在听,跟着唱的人不多。


但是近年来,只要有卖唱歌手的地方,基本上大家都会蜂拥上去吼上几句,有甚者直接流着眼泪大声歌唱。


因为大家都从之前的内卷中失望了,生活很大程度上并不会因为努力而发生变化,就不太和自己较真了,从而将重心移到了生活中来。


而市井,热闹就是生活的最真实写照,不需要花多少钱就能释放情绪,收获快乐。


贵州村超淄博烧烤,再到哈尔滨,都能得到很好的体现。


我们还发现一个问题,这几个城市都是比较落后的,其实并没有什么吸引人的地方,景区,经济,文化其实都没有什么突出的地方。


但是有一个特点,那就是消费便宜


你想一下,如果要在香港,澳门,上海这些城市打造这样的活动,做这样的城市IP,现实吗?


我想不现实,因为消费太高,大多数人承受不起。


你想,开一个好一点的酒店都要不少钱,还有吃和也是很贵,加上处于经济高速发展的地区,本地人比较少。


所以情感并不浓,消费并不低。


可能会像村超那样直接免费接游客去自己家里住宿,游客离开后还深情相拥吗?


可能会像哈尔滨这样一到位就一口一个南方小土豆,然后排着队接送吗?


我想基本上不会。


因为多数人的消费能力是有限的,肯定会选择热闹,便宜且好玩的地方。


所以这样的火热IP,大概只会出现在消费相对来说比较低的城市。


所以,现在的生意大多都围绕着提供情绪价值这个方向出发。


前段时间火爆全网的海底捞科目三,虽然海底捞的价格高了一点,但是在你累了,失落了的时候,突然在你面前响起了生日快乐歌,随后又跳起了科目三。


在冰冷的建筑下瞬间热泪盈眶,脑海中蹦出一句:人间值得


要知道,在外面花几百块钱是买不到这种服务的。


而这些服务本质就是提供情绪价值。


特别是在今天这样的现状下,大家兜里都没几个子,生活也都不太如意,所以这时候情绪价值对于一个人来说尤为重要。


所以以后这样火爆的城市IP还会持续出现,这是毋庸置疑的!


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

IT外传:老郑和老钱

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。 他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。 老郑刚做了一个智能识别的AI项目。这个项目快...
继续阅读 »

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



老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。


他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。


老郑刚做了一个智能识别的AI项目。这个项目快提交测试时,老郑还在别的项目组干活。这个智能识别干了没几周,又被调走干别的事情了。


即便如此,老郑开发的智能识别项目,在识别率和识别能力上,在整个业内也是领先。这得益于他长久以来的经验积累以及巧妙的算法设计。


但是,大家却不这么看。即便业内识别率只能到50%,老郑做出了85%,但是大家也会盯着那无法识别的15%。


刚刚测试工程师就在整理那15%,他们要把这15%里的100%全部汇报给领导:你看,这些一塌糊涂,这种情况能不能用?请领导定夺。


言外之意:上线后有问题跟我们无关,风险已经全部抛出来了。


其实……老郑也习惯了。


上次另一个识别项目,老郑把准确率从刚提交测试时的50%提高到97%。而测试工程师在给领导汇报时,开头说识别率很差,只有50%。领导很忙,听完这个结论就走了。剩下一些中层,又听了40分钟他是如何通过围追堵截的测试方法一步步将识别率提高的。


大家都没错,也都很辛苦,这些老郑并不关心。老郑的心情很差,因为有一个同事离职了。


老郑的这个同事,技术能力很强,强到一个人可以顶一个团队。


在体力劳动上,一个人顶一队人可能很难。比如普通的劳工一次扛3袋水泥,有个大力士可以一次扛30袋,而且速度还很快。这很罕见。


但是在科技或者软件行业,这种情况很普遍,但是很少有人被认可。


老郑的这个同事老钱,就是这样一个人。


老钱设计的代码,简洁纯净,他擅长使用中间件和编程语言的特性,代替大量的代码逻辑。其代码格式规范、文档注释清晰。也正是得益于简洁和巧妙,他的效率还很高。同样的功能,其他同事需要3个人写两周,老钱1个人一周就能搞定。


对于速度,这顶多算是多扛几袋水泥,在软件行业,这点贡献算不了什么巨大改善。


关键是老钱写的代码很少出问题。程序员写的代码出了问题(bug),会引发后续一堆人的投入。测试同事会测试,前后端要排查、修改,产品经理要做决策,市场要应对用户的投诉。这bug就像是喂鸽子时的粮食,往东边撒一把儿,一群鸽子蜂拥而至,往西边撒一把儿,西边又密密麻麻。


老钱设计的代码,很少有bug,这一点就避免了三四个部门、10多个人白忙活几周的情况。这个隐形成本的节省是巨大的。


除了代码的设计,老钱还有个优势,那就是有远见和守原则。


项目开发中,会面临很多的技术选型和方案选定。大到使用什么框架,小到一个参数选用何种数据类型。


很多的时候,在进行技术讨论时,老钱会对其他人的方案提出建议。比如一个参数不要传来传去,就要以一方为准,否则会出问题。


其他人一般会有自己的理由,比如,传来传去不用给数据库增加额外字段。但是,往往过不了多久,问题就出现了,传着传着就传乱了。于是大家又聚到一起调试:你传给我啥,我收到啥,又传给了他啥……在广场的空地撒了一把粮食,远处的鸽群放弃了旧粮,急忙朝这里飞奔而来。


老郑和老钱也合作过一个项目。老钱曾经建议老郑不要那么搞,否则会出问题。老郑没听,结果后面确实走不通了,最终老郑还是改了回去,那一个星期白干了。


然而,老钱却离职了。


他的离职半含被迫,半含自愿。首先,经济形势不好,导致公司出现了拖延工资的情况。


其次,在拖延工资的背景下,不同员工的发放情况参差不齐。有的人拖延5个月,有的人拖延3个月,有的人正常发放。而老钱的工资,拖得最久,向领导反馈也没有结果。


领导说,公司现在的回款出现延迟,前年该给的钱,去年才刚刚给。不过,每个月也都是有回款的。这点回款,首先要保证公司的日常运作,其次保证新员工,再次保证有贡献的员工。其他人,只能克服一下了。


好像意图比较明显。老钱也是个智商和情商都在线的人。


老钱提出了离职,领导立马批准,限两周内办好手续。老钱说,我原本打算能有1个月缓冲期的。


其实,老钱和老郑早就被投诉多次了。


甚至连人事都看不惯他们:凭什么这俩人工资比我们高,还不拼命加班?我不平衡……不是,他们没有大局意识!


而同为技术人员,兄弟部门的意见就更多了:再复制一份接口,随便改个字段都不配合!群里半夜@你的消息,为什么没有及时回复?我们换个对接人问你问题,你不培训他,让他看接口文档是什么意思!


老钱和老郑有个观念:用工作时间的高效率工作,换取下班后的安心休息。但是,似乎大家并不都想这样,往往是白天静悄悄,只要一下班,工作群里立刻变得活跃起来。


老郑和老钱有时候就讨论,你说领导是否知道一个员工的真实水平或者价值。


比如,A员工干的活能顶B、C、D,3个人。或者,他手下有个员工的水平在整个行业中处于上层还是下层。


“好像不知道!”


老钱说,交接工作期间,有个问题找来,领导还问他:你也参与这个项目了?


其实就上个月,老钱还在这个项目上干了半个月,日报、周报、早会、周会地定期汇报。显然领导没有关注过,因为没有发生过大的问题。一贯零失误的工作,让老钱变成了一个小透明,而且还是经常提意见的那种问题员工。


一线的员工常常辗转于项目代码之中。领导们则开会,看书,制定考核KPI指标。长期脱离一线阵地,会让领导从业务管理上浮到任务管理(从如何带领人解决问题,变成安排人去解决问题)。


软件其实是一门工程学,而非玄学。


软件工程的最佳的实践是多进行工程管理,而非思想管理。


现实很多情况都是反过来的,大家都很重视思想管理。


如果把完成一个软件项目比作攻下一座城池。那么,策略要比士气更重要。


讲策略的将军会规划好完整的攻城计划。首先,他会盘点自己有多少人员和器械,会分析对方城池有几个薄弱点。然后,部署几个分队:哪个队伍扛着云梯往城墙上驾,哪个队伍推着木车从东门撞击。其实,队伍主力要从北门水路强攻。等到把敌方守卫都引到东门时,以山坡黑烟为号,北门发起进攻。最终全面进攻,一举取得胜利。


类比到项目开发中,其实就是各个工种的配合,结构的定义和数据的流转。安排好整个项目每个端口,从上游拿到什么数据,做怎样的处理,然后给下游如何提供。最终,定好流程和时间节点,一气呵成。肯定没法想得完全周到,不过即便有问题,也都是局部问题,整体还是丝滑的。


4cb98f20-a6d4-4872-98a5-51113d85858a.jpeg


讲士气的将军则不然,他不考虑每个环节,或者技术更新太快,他已经不擅长每个环节了。他的主要精力是给士兵做思想工作。他告诫士兵们,我们又要攻打一座城池了,大家要有大局意识,不想当将军的士兵不是好士兵,士兵就是要解决问题的,不是提出问题的。他不关注粮草,不关注器械,不关注目标城池的特征,主要强调大家一定要攻下城池,这是所有人的目标和责任。然后,一声令下,众士兵蜂拥上前,去哪儿的都有。


最后没有攻下城池。将军要求手下将领做复盘,开会讨论为什么没有成功。然后,再次鼓舞大家要有建功立业的雄心。而手下的将领回去也纷纷效仿,告诉士兵们,一定要有建功立业的雄心壮志,遇到问题要解决问题而非吐槽问题,人人都是主人翁,没有粮草你要想办法去搞些粮草。第二次,士兵们又向敌方发起总攻……


这不仅仅体现在软件行业,其他行业也一样,正如一些专家、教授频频发出雷人的言论。


在国内,大家都有上级崇拜。针对如上言论,一般会有人怼你:能当领导的人,必然有过人之处,否则为什么不是你当领导?


其实这句话也没错,还真不是一个把产品做得越好就能生存得越好的环境。


老郑不知道老钱以后会不会改变,正如他不知道自己还能坚持多久。


老钱离职前,曾经问过老郑:老郑,你说那帮“埋头苦干”的年轻人,是以前的我们呢?还是我们的以后呢?


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

MyBatis实战指南(二):工作原理与基础使用详解

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。一、MyBatis的工作原理1.1 MyBatis的工作原理工作原理图示:1、读取...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。

一、MyBatis的工作原理

1.1 MyBatis的工作原理

工作原理图示:
Description

1、读取MyBatis配置文件

mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。

2、加载映射文件(SQL映射文件,一般是XXXMapper.xml)

该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。

XXXMapper.xml可以在mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3、构造会话工厂

通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。

4、创建会话对象

由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

5、Executor执行器

MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

6、MappedStatement对象

在 Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

7、输入参数映射

输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

8、输出结果映射

输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

1.2 MyBatis架构

Description

API接口层

提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

MyBatis和数据库的交互有两种方式:使用传统的MyBatis提供的API、使用Mapper接口。

1)使用传统的MyBatis提供的API

这是传统的传递Statement Id和查询参数给SqlSession对象,使用SqlSession对象完成和数据库的交互;
Description
MyBatis提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和MyBatis自身配置信息的维护操作。
示例:

SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);

上述使用MyBatis的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。

2)使用Mapper接口

MyBatis将配置文件中的每一个<mapper>节点抽象为一个Mapper接口,而这个接口中声明的方法和跟<mapper>节点中的<select|update|delete|insert>节点项对应,

即<select|update|delete|insert>节点的id值为Mapper接口中的方法名称,parameterType值表示Mapper对应方法的入参类型,而resultMap值则对应了Mapper接口表示的返回值类型或者返回结果集的元素类型。

示例:

SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}

根据MyBatis的配置规范配置后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper实例。
Description

使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject)或者SqlSession.update(“statementId”,parameterObject)等等来实现对数据库的操作。

MyBatis引用Mapper接口这种调用方式,纯粹是为了满足面向接口编程的需要。

数据处理层

负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

1)参数映射和动态SQL语句生成

动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用OGNL表达式来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。
Description
参数映射指的是对于Java数据类型和JDBC数据类型之间的转换,这里包括两个过程:

  • 查询阶段,我们要将java类型的数据,转换成JDBC类型的数据,通过preparedStatement.setXXX()来设值;

  • 另一个就是对ResultSet查询结果集的JdbcType 数据转换成Java数据类型。

2)SQL语句的执行以及封装查询结果集成List< E>

动态SQL语句生成之后,MyBatis将执行SQL语句并将可能返回的结果集转换成List<E> 。

MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。

基础支撑层

负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis层次结构

Description

1.3 Executor执行器

Executor的类别

Mybatis有三种基本的Executor执行器:SimpleExecutor、ReuseExecutor和BatchExecutor。

Description

1、SimpleExecutor

每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

2、ReuseExecutor

执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor

执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Executor的配置

指定Executor方式有两种:

1、在配置文件中指定

<settings>
<setting name="defaultExecutorType" value="BATCH" />
</settings>

2、在代码中指定

在获取SqlSession时设置,需要注意的时是,如果选择的是批量执行器时,需要手工提交事务(默认不传参就是SimpleExecutor)。

示例:

// 获取指定执行器的sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 获取批量执行器时, 需要手动提交事务
sqlSession.commit();

1.4 Mybatis是否支持延迟加载

延迟加载是什么

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。

例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。

MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

假如Clazz 类中有子对象HeadTeacher。两者的关系:

public class Clazz {
private Set<HeadTeacher> headTeacher;
//...
}

是否查出关联对象的示例:

@Test
public void testClazz() {
ClazzDao clazzDao = sqlSession.getMapper(ClazzDao.class);
Clazz clazz = clazzDao.queryClazzById(1);
//只查出主对象
System.out.println(clazz.getClassName());
//需要查出关联对象
System.out.println(clazz.getHeadTeacher().size());
}

延迟加载的设置

在Mybatis中,延迟加载可以分为两种:延迟加载属性和延迟加载集合,association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false

1)延迟加载的全局设置

延迟加载默认是关闭的。如果需要打开,需要在mybatis-config.xml中修改:

<settings>

<setting name="lazyLoadingEnabled" value="true" />

<setting name="aggressiveLazyLoading" value="false"/>
</settings>

比如class班级与student学生之间是一对多关系。在加载时,可以先加载class数据,当需要使用到student数据时,我们再加载 student 的相关数据。

  • 侵入式延迟加载:指的是只要主表的任一属性加载,就会触发延迟加载,比如:class的name被加载,student信息就会被触发加载。

  • 深度延迟加载: 指的是只有关联的从表信息被加载,延迟加载才会被触发。通常,更倾向使用深度延迟加载。

2)延迟加载的局部设置

如果设置了全局加载,但是希望在某一个sql语句查询的时候不适用延时策略,可以配置局部的加载策略。

示例:

 <association
property="dept" select="com.test.dao.DeptDao.getDeptAndEmpsBySimple"
column="deptno" fetchType="eager"/>

etchType值有2种,

  • eager:立即加载;

  • lazy:延迟加载。

由于局部的加载策略的优先级高于全局的加载策略。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用属性的值。

延迟加载的原理

MyBatis使用Java动态代理来为查询对象生成一个代理对象。当访问代理对象的属性时,MyBatis会检查该属性是否需要进行延迟加载。

如果需要延迟加载,则MyBatis将再次执行SQL查询,并将查询结果填充到代理对象中。

二、MyBatis基础使用示例

1、添加MyBatis依赖

首先,我们需要在项目中添加MyBatis的依赖。如果你使用的是Maven项目,可以在pom.xml文件中添加以下依赖:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

2、创建实体类

假设我们有一个用户表(user),我们可以创建一个对应的实体类User:

public class User {
private int id;
private String name;
private int age;
// getter和setter方法省略
}

3、创建映射文件UserMapper.xml

在MyBatis的映射文件中,我们需要定义一个与实体类对应的接口。例如,我们可以创建一个名为UserMapper的接口:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

4、创建接口UserMapper.java

接下来,我们需要创建一个与映射文件对应的接口。例如,我们可以创建一个名为UserMapper的接口:

package com.example;

import com.example.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
}

5、使用MyBatis进行数据库操作

最后,我们可以在业务代码中使用MyBatis进行数据库操作。例如,我们可以在一个名为UserService的类中调用UserMapper接口的方法:

public class UserService {
public User getUserById(int id) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(id);
sqlSession.close();
return user;
}
}

总结:

MyBatis是一个非常强大的持久层框架,它可以帮助我们简化数据库操作,提高开发效率。在实际开发中,我们还可以使用MyBatis进行更复杂的数据库操作,如插入、更新、删除等。希望这篇文章能帮助你更好地理解和使用MyBatis。

收起阅读 »

2024律师课程推荐:iCourt律师执行实务集训营(赠《执行实务大礼包》)

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?从数据来看,执行业务一定是其中之一。在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;根据...
继续阅读 »

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。


如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?


从数据来看,执行业务一定是其中之一。


在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;


根据中国执行信息公开网数据,截止到 12 月 28 日,公布的失信被执行人高达 8,618,870 位;


根据最高法公布的数据显示,得益于全国法院持续推进执行难综合治理、源头治理,仅 2023 年上半年,我国执行到位金额高达 1.2 万亿元,同比增长 23.03% ......



在业内,执行案件是公认的数量多、案件标的大、个案收费高。同时,作为诉讼案件的“最后一公里”,执行能够将司法裁判真正的“落到实处”,维护客户合法利益,实现律师职业价值。因此,一直以来,都有大量律师对执行业务“跃跃欲试”。


但,为什么现实中并没有那么多律师真正从事执行业务?


蓝海是真,“执行难”也是真:线索难找、到位率低、执行周期长、执行程序复杂多变......执行背后的难题让人望而却步。


啃下执行这块“硬骨头”,就能开拓业务范围,拓宽职业路径,在广阔的蓝海中寻找发展新机遇。


基于此,3 月 2 日 - 3 月 3 日,「执行实务集训营 07 期」将在古都西安与各位校友相见。2 天一夜的课程,将对执行实务的疑难问题进行思路点拨和深入解析,以“两个拳头” + “四大利刃”全方位切入执行案件,系统梳理相关法规案例,详细剖析执行专业知识和实务要点。


老赖隐匿财产,当事人无计可施,有哪些途径可以挖掘被执行人财产线索?

处置财产,如何形成更专业化、标准化、流程化的执行案件办理流程?

“借名买房”、“股权代持”等疑难案件该如何拆解破局、一击即中?


经过了 6 期课程的升级迭代和精心打磨,「执行实务集训营 07 期」将直击痛点,打通诉讼最后一公里。


同时,该集训营也将结合刚刚审议通过、即将在 2024 年 7 月 1 日起施行的最新修订的《公司法》内容,对执行相关的新变化、新趋势、新动向进行深入解读和思路点拨。


课程大纲


(一)查找执行财产的三大视角与方法


本次课程将会详细剖析十多种常见与非常见财产类型,并从执行法院、执行律师和当事人三个视角出发,分享查询被执行人财产的途径和方法。


(二)处置被执行人财产的标准化流程


本次课程将通过细致讲解,帮助大家形成自己、团队的执行案件办理流程,以便更好地展现代理执行业务的专业化、标准化、流程化。


(三)执行异议之诉案件的破局之道


本次课程会对案外人执行异议之诉、申请执行人执行异议之诉、追加股东为被执行人异议之诉、执行分配方案异议之诉四个板块进行逐一详细解读。此外,本次课程还将对“借名买房”能否排除强制执行、“股权代持”的隐名股东能否排除强制执行等关联问题进行拆解式分享。


(四)破解执行难的“两个拳头”与“四大利刃”


推进执行案件,常规做法是“两个拳头”:一拳打向的是被执行人财产;另一拳则打向的是被执行人。对于执行案件,除了以上两种常规打法,本次课程还提炼出了破解执行难的“四大利刃”。这些“利刃”并非会用在每一个执行案件中,但却可以成为某个具体执行案件中的“大杀器”,在关键时刻真正做到执行无阻,使命必达。



课程安排




课程讲师




往期现场:


(课程现场)


在两天一夜的课程中,除了干货满满、深入浅出的内容讲解外,校友们还将通过紧张刺激的小组比赛将课程内容串联起来,达到“融会贯通”的效果。


为了团队的荣誉,各个小组直接“卷起来了”,最终的作业展示环节简直“神仙打架”、惊喜连连。经过系统的学习和模拟实践,校友们也对破解执行难题,办理执行案件有了全新的思路和理解。


(比赛环节现场)


北京、武汉、广州...... 2023 年,校友们与「执行实务集训营」共同度过了 6 期的时光。每一期校友们都满载而归、直言不虚此行。该课程也是 2023 年 iCourt 线下集训营参与人数最多、最火爆的课程之一。

(学员合影)


课程资料


除精彩内容外,现在报名集训营,更有「执行实务干货大礼包」全部送送送!


• 1 节线上课程,业务品牌双管齐下

校友报名后均可获得《执行律师如何打造个人品牌》录播课程,方便大家随时随地进行学习提升。课程围绕写作与讲课,这两个法律人必备的技能,结合实务案例,帮助执行律师打造专业品牌,赋能业务开拓。


• 2 套项目模板,标准化办案新思路

随课程赠送《执行与执行异议》、《执行》两套项目模板,涵盖丰富的任务与任务附件,帮助校友规范办案流程,建立团队知识宝库。


• 4 本实务指引,配套学习事半功倍

《执行实务操作指引》( 4.0 版)、《执行实务 108 问》、《执行一本通法律法规汇编》( 2023 版)、《律师代理执行案件 168 个执行步骤》。4 本执行办案实务操作指引,从法律规范、常见问题出发,帮助校友解决服务中的文本困扰。


• 多份文书模板,高效应对执行难题

我们汇总了办案过程中常用的变更申请执行人、查封申请书、到期债务通知书模板等等,希望能够帮助校友提升执行办案效率,高效破解执行难题。

执行虽难,但抽丝剥茧,便能破解难题,实现职业突破。在「执行实务集训营」07 期,拥有丰富执行实务经验的韩锦超老师将带领大家探讨疑难案例,学习方法,启发思路。在前六期的基础上,iCourt 课程中心也对课程内容、课程环节、课程资料、课程体验等环节进行了全面系统的升级,结合最新法律动态,带给大家焕然一新的体验。

机遇往往与挑战并存,机遇也往往留给做足了准备的人。

眼前,是一片可待征服的蓝海。跨过这座大山,是属于我们的广阔的征途。2024 年春节“节后第一课”,「执行实务集训营」将带领大家破解执行难题,挑战业务蓝海。

突破执业困境,把握发展机遇。3 月 2 日 - 3 月 3 日,我们西安见~

了解课程详情:iCourt集训营

原文链接:https://www.icourt.cc/prac-article/728.html

收起阅读 »

探索发展,融合共生|惠州OpenHarmony城市大会圆满举行

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简...
继续阅读 »

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简称“OpenHarmony”)超高清专委会承办,惠州市电子信息产业协会协办。

(图片:惠州OpenHarmony城市大会现场)

广东省政务数据管理局局领导、一级调研员姚进,广东省工业和信息化厅信息化与软件服务业处副处长陈古典,惠州市政府副秘书长程坤,惠州市工业和信息化局局长廖巍,惠州市政务服务数据管理局局长杨伟斌,仲恺高新区管委会副主任、潼湖生态智慧区党工委副书记、管委会主任汤俊,广东九联科技股份有限公司董事长詹启军,OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见,鸿蒙生态服务有限公司总经理杜金彪,中国信通院泰尔终端实验室副主任果敢,开放原子开源基金会教育培训部部长王岩广,京东方高级副总裁荆林峰,OpenHarmony生态伙伴企业代表,惠州市OpenHarmony潜在政企客户,超高清领域企业代表,专家学者,高校代表及研究机构代表等300余人出席了本次大会。大会旨在分享及探索OpenHarmony生态发展路径,着力搭建地方产业及OpenHarmony生态领域的交流合作平台,助推OpenHarmony技术创新和生态繁荣。

广东省政务服务数据管理局局领导姚进为大会在开场致辞中强调,广东作为OpenHarmony的发源地,相关工作起步早、优势大,其在开源软件贡献、人才培养、生态应用和政策支持等方面的OpenHarmony生态体系建设上已经位居全国前列。特别指出,惠州在OpenHarmony建设方面走在全省前列,本次大会在惠州市的召开,不仅为广东省乃至全国的OpenHarmony系统发展提供了宝贵的交流平台,也为其他城市发展OpenHarmony提供了典范。未来,广东省政务服务数据管理局将继续通过成立产业协会、制定标准规范以及开展应用示范等措施,全力推动OpenHarmony产业的繁荣壮大。

(图:广东省政务服务数据管理局局领导姚进)

惠州市人民政府副秘书长程坤在致辞中强调,OpenHarmony作为构建智能终端操作系统的重要基础能力平台和安全底座,对于打造自主可控的国产操作系统和构建新的智能终端产业生态具有深远意义。惠州凭借其坚实的电子信息产业基础,积极把握OpenHarmony发展的战略机遇,并已取得了一系列实践成果。例如,支持和指导华为终端、九联科技等企业牵头成立了“OpenHarmony超高清专委会”;印发实施了加快OpenHarmony生态产业发展行动计划,成效初显。程坤表示,惠州将继续强化对OpenHarmony产业的宣传引导、主体培育、示范应用、环境优化和政策谋划等工作,为OpenHarmony生态在惠落地发展提供有力支持。

(图:惠州市人民政府副秘书长程坤)

OpenHarmony打造下一代智能终端操作系统根社区

OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见分享了OpenHarmony项目及生态进展。目前已有超过220家伙伴加入OpenHarmony生态共建,累计落地超过460款软硬件产品通过OpenHarmony兼容性测评,覆盖金融、教育、交通、医疗、公共安全、智慧城市等多个行业。随着行业标准规范的推进,OpenHarmony已成为各行各业的优选。

同时,深圳、福州、惠州、北京、重庆、南京等城市率先出台相关产业政策支持OpenHarmony发展,从供给侧和需求侧推动生态建设。为培育与产业发展契合的创新型人才,壮大OpenHarmony生态新兴力量,生态伙伴联合高校共同打造人才培养闭环生态链。柳晓见表示,希望OpenHarmony生态伙伴们聚力前行,期待更多伙伴加入OpenHarmony共建中,共筑下一代智能终端操作系统根社区。

(图:OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见)

OpenHarmony生态服务助力生态商业成功

鸿蒙生态服务公司总经理杜金彪分享了在各地政府大力支持下出台的相关OpenHarmony产业扶持政策,并介绍鸿蒙生态服务公司围绕“行业集采、政府集采、政府奖补、联盟标准”四大商业机遇,开展测评认证、市场拓展、商机对接、活动承办等服务,协助生态伙伴实现降本增效。通过搭建相关联盟平台推动产业标准建设,促进良性竞争。同时也期待更多伙伴加入,共同加速OpenHarmony生态产业健康发展。

(图:鸿蒙生态服务公司总经理杜金彪)

两位特邀嘉宾专题分享完毕后,在参会嘉宾的共同见证下,举办了OpenHarmony超高清专委会揭牌仪式、OpenHarmony人才培养战略行动启动仪式。本地政府、院校、企业等多方力量表示,将全力以赴支持OpenHarmony的发展,共同推动其在更多领域的应用和普及。此次相关仪式的成功举行,为惠州市OpenHarmony生态的发展开启了新的篇章。

OpenHarmony超高清专委会揭牌仪式

2022年,在开放原子开源基金会的指导下,在惠州市政府、惠州仲恺高新区管委会的支持下,由华为终端有限公司、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司牵头推动成立了“OpenHarmony超高清专委会”(OpenHarmony生态委员会下属负责推进OpenHarmony在超高清领域生态发展的唯一主体)。专委会将面向超高清全行业,推动 OpenHarmony的研发、装机、应用等工作,打造OpenHarmony超高清生态圈。

(图:OpenHarmony超高清专委会揭牌仪式合影)

OpenHarmony人才培养战略行动启动仪式

由开放原子开源基金会、惠州学院、惠州市技师学院、惠州城市职业学院、惠州经济职业技术学院、惠州工程职业学院、深圳技术大学、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司(OpenHarmony超高清专委会秘书处单位)共同开启OpenHarmony人才培养战略行动启动仪式,承诺合力开展OpenHarmony新型操作系统的产业及技术培训等相应的活动,为企业培养更多的实用性、复合型的软件人才。仪式结束后,大会邀请了OpenHarmony生态专家及伙伴进行主题演讲。

(图:OpenHarmony人才培养战略行动启动仪式合影)

OpenHarmony的基本设计理念和关键技术最新进展

OpenHarmony项目管理委员会(PMC)主席任革林分享了OpenHarmony的基本设计理念和关键技术最新进展,并介绍了基于OpenHarmony使能的金融、智慧教室、智能化公路建设等一系列行业场景创新解决方案。他表示,OpenHarmony技术底座能力越来越成熟,一个面向全场景、全连接、全智能时代OS的阶段目标已经达成,既可以满足生态伙伴开发丰富多彩的创新设备,也可以满足应用开发者开发复杂大型应用和极致高性能应用。

(图:OpenHarmony项目管理委员会主席任革林)

深开鸿基于OpenHarmony高校人才培养实践

深圳开鸿数字产业发展有限公司(简称“深开鸿”)OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会(PMC)成员巴延兴分享了深开鸿基于OpenHarmony高校人才培养实践。作为一家立足于OpenHarmony生态,为行业数字化、智慧化提供基础软件的生态平台型企业,深开鸿积极响应国家切实推动“深化产教融合、校企人才共育”的号召,与北京理工大学、哈尔滨工业大学、东南大学、深圳信息职业技术学院、深圳技术大学等众多高校开展合作,计划在未来几年内培养大量优秀的OpenHarmony技术人才。

(图:深开鸿OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会成员巴延兴)

基于OpenHarmony的全场景解决方案实践

江苏润开鸿数字科技有限公司(简称“润开鸿”)生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安分享了润开鸿基于OpenHarmony面向行业的HiHopeOS发行版使能千行百业的场景创新解决方案及商业落地实践,并重点介绍了基于龙芯+OpenHarmony的适配进展及工业场景探索。

(图:润开鸿生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安)

新功能,新形态,新场景

京东方科技集团股份有限公司视觉艺术事业部总经理吴坚围绕京东方集团业务新功能、新形态、新场景展开讨论,探讨京东方屏之物联与OpenHarmony结合所带来的创新与变革。

(图:京东方视觉艺术事业部总经理吴坚)

基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索

广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀分享了基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索。围绕OpenHarmony和星闪的特性在智慧空间场景中应用以及场景中特性闭环,低时延、近场联接联动让分布式空间场景无处不在。

(图:广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀)

在盛大的OpenHarmony城市大会上,我们见证了科技与生活的深度融合,感受到了开源文化与创新精神的激情碰撞。通过深入的探讨与交流,我们更加坚信,OpenHarmony所倡导的开放、共享、互联的理念,将引领我们迈向一个更加智能、高效、和谐的美好未来,共同开创一个万物智联、万物互融的新时代。让我们携手并进,以“探索发展,融合共生”的精神,为开源生态的繁荣与辉煌而努力!

(图:惠州OpenHarmony城市大会现场)

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony开源三年多来,社区快速成长,版本已迭代到OpenHarmony 4.1beta1 Release,超过 6700 名共建者、70家共建单位,贡献代码行数超过1亿行。截至2023年底,OpenHarmony开源社区已有250多家生态伙伴加入,OpenHarmony项目捐赠人达35家,通过OpenHarmony兼容性评测的伙伴达170余个,累计落地230余款商用设备,涵盖金融、教育、智能家居、交通、数字政府业、医疗等各个领域,OpenHarmony已成为下一代智能终端操作系统根社区。

收起阅读 »

2024年,现在去开发一款App需要投入多少资金?

前言 本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~ 选择大于努力 原生开发的现状 先来看下目前原生开发存在的问题以及国内的现状。 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开...
继续阅读 »

1.jpg


前言


本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~


选择大于努力


原生开发的现状


先来看下目前原生开发存在的问题以及国内的现状。



  1. 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开发人员。这也是导致很多企业不愿意选择原生开发的重要原因之一(Android、iOS)。

  2. 原生应用开发成本高,开发周期慢,如果不招人(提高用人成本)较难跟上市场节奏。

  3. 原生应用推广成本也高(与小程序相对比)。

  4. 对于我们开发人员来说,需要掌握一种语言Java或者kotlin,ios开发需要oc或者swift,难度相对于跨平台学习成本较高。


企业对于技术上的选择,目前需要的就是能节省成本、同时开发效率高的,跨平台已经是大势所趋


国内特有的小程序


小程序的优势很大,自从小程序出来后,蚕食了很大一部分手机应用的市场份额。


小程序相较于原生应用具有显著的优势,其中最大的优势在于成本的降低。相比于开发原生应用,小程序的开发成本更低,同时也更加省时省力。此外,小程序还能够充分利用微信等大型平台的庞大用户流量入口,从而降低企业在推广方面的成本。这种降低成本的好处不仅体现在企业推广方面,也使得用户在使用小程序时所需投入的成本降低(不用去下载app,也不用再走一遍注册流程)。


正因为如此,许多中小企业不愿再开发原生应用,或者说,“没能力”开发原生应用。更倾向于选择小程序。小程序的低成本开发和推广,使得中小企业能够以更少的投入获得更大的回报。此外,小程序还可以借助微信等平台的用户基础,更容易吸引和留住用户。


如何计算一款App开发的成本


本文选择跨平台技术作为开发成本的参考。那在跨平台中,从Statista(一家全球领先的统计数据平台和市场研究公司)收集的数据来看,很明显 Flutter 继续脱颖而出,成为跨平台框架中的首选。截止2023年6月,Flutter占跨平台份额的46%,在跨平台中占比第一,React Native占32%,居第二。



长话短说,开发 Flutter 应用程序的相关费用基本在 10,000 到 450,000 人民币之间,甚至更高。在这本文中,我们将分解各种成本因素,去计算 Flutter 应用程序开发成本。


那如何去计算一款应用的开发成本呢,开发一款应用一共分一下几个阶段,每个阶段都会影响总成本。



  • 第一阶段:需求分析与规划

  • 第二阶段:原型设计(UI/UX)

  • 第三阶段:正式编码(此时应用已经基本成型)

  • 第四阶段:测试

  • 第五阶段:部署与维护


Flutter技术在国内多用于外包项目,所以通常三四五(有些项目会包含二)的几个阶段都由开发者全权负责完成,应用的总体成本通常是通过将总工时乘以开发者的小时费来估算的。


影响一款应用开发成本的因素


不同的应用开发的成本可能会因多种因素而有很大差异,每个因素都会直接影响项目的预算和时间表。最终价格可能受到一系列因素的影响,例如应用程序的复杂程度、要纳入的功能总数、开发人员的每小时费以及许多其他方面。


主要的因素也是对应到开发阶段中,主要是以下这些:



  • 在需求分析时,应用程序的范围和复杂性

  • 在UI设计时,UI的动画、复杂的布局、对设计风格的要求

  • 在开发时,选择的开发方式(1.外包给自由职业者。2.外包给专业软件公司。3.自己招人干)。选择外包开发者(开发商)的地理位置,假设你在美国,找一个中国开发者,成本就会降低许多

  • 在测试时,跨平台的设备成本,功能测试的范围

  • 部署维护时,服务器的成本,bug的修复,添加新的功能


那让我们再来详细聊聊每个阶段具体要花多少费用。


需求分析设计阶段


项目的需求和范围是开发成本的主要决定因素,例如,开发一个基本的笔记应用程序比开发一个功能齐全的电商平台便宜得多。因此,在App开发的初始阶段定义项目需求和应用程序复杂性对于估算总体成本至关重要。App在刚开始需要舍弃掉一些不重要的功能。


UI设计阶段


如果有一个高质量的 UI/UX 设计,那对于App的成功是很有帮助的。但它也会影响成本,一款简单、简约的设计比具有独特图形、复杂动画动画的定制成本更低。如果需要高度定制的设计或想要实现特定的品牌元素,这将极大增加的应用程序开发成本。根据应用程序的复杂程度,设计一款完整的App平均需要 40 到 90 多个小时。设计一款App的UI,价格平均在5000-25000左右,让我们对应到每项工作中去。



  1. 前期的需求交流和沟通。此阶段涉及创建草图和线框图。所需的时间和成本取决于设计的复杂程度。创建草图和线框图可能需要 200 至 1000 的预算分配

  2. UI/UX 设计视觉效果的创建。 此阶段为整个App的内容设计,例如登录界面、注册界面等。同样,实际所需时间取决于App的复杂性。此阶段的预算范围从 5,000 到 15,000或更多

  3. logo设计。在这个阶段阶段,设计师根据之前设计的App内容和、我们的品牌配色和其他设计元素。这项共工作需要相当大的预算,大约需要 5,000 到 10,000 的预算甚至更多。当然,为了节省成本也可以放弃这一阶段,由我们自己设计


代码开发阶段


选择不同的开发人员或开发团队也会影响成本。如果选择经验丰富的专业人员团队会花费更多的前期成本,但可以带来更高的效率和更高质量的产品。如果,雇用经验不足的开发人员刚开始可能会省钱,但可能会导致开发时间更长或日后出现潜在问题。目前主流的方式为以下三种:


自由职业者(外包给程序员做私活)


这种方式可以很好的降低成本,身边也有很多朋友会接私活,确实是一个很不错的选择。但是,这种方式可能会遇到许多不确定性,例如没法按时交付。此外,如果这个项目后期需要进行维护、更新,那这个方案可能就不是最可靠选择了,因为他们可能会转移到其他项目(或者跑路),从而使持续协作变得具有挑战性。如果选择这个方案,建议是朋友推荐,或者是网上具有一定知名度的开发者。在国内,跨平台应用开发者(Flutter开发)的时薪通常在每小时150到350人民币不等。如果选择这个方式,开发成本在10000到50000之间。


外包公司


这种方法是节省开发资金而又不影响产品质量的绝佳方法,通常开发成本在50000到150000之间。如果项目需要后期的维护,迭代,那么可以优先选择这样的方式。(现在的外包公司也比较卷)


自己组团队


如果是想要真的以一种创业的方式,那么开发成本的范围是0到无上限。如果自身就是一个技术人员,那么只需要一台笔记本就可以完成对应用的开发,所花的只是时间成本。如果要招人组团队,那成本就不可估计了。


测试阶段


这部分在大多数App开发过程中,已经由开发者自己测试解决的。稍微正规些的应用可以将测试的工作外包给测试公司。成本在0~20000人民币之间。


维护与迭代


开发一款App不是短跑,而更像是一场马拉松。即使在App第一版上线后,这个旅程仍在继续。定期更新、bug修复和UI修改只是维护App的冰山一角。最好预留总成本的 15-20% 的额外费用,用来进行维护。


其他因素


——每个项目都是独特的,具体要求将决定最终成本。因此,在规划App开发预算时,必须彻底了解这些因素并加以考虑。


第三方API集成


如果项目中需要集成即时通讯等功能模块,那么第三方API集成的这部分的花销也是不可忽略。


软著申请、应用商店发布


软著申请是免费的,自行准备材料申请即可,但是通常会有2~3个月的时间,才能申请成功。如果想快速申请,可以找专门的三方申请机构,价格在500-2000左右。如果App需要上架Google Play和App Store,那么,开通Google Play 开发者账户一次性收取 25 美元费用,Apple Store 个人开发者账号每年收取 99 美元费用。此外,还会从应用内购买或订阅中扣除部分费用。申请软著和App上架的材料准备工作,通常需要10-20小时的工作。按每小时50元,此部分工作需要500-1000元的费用。


后端开发和服务器的费用


如果App只会进行一些本地操作,那么这部分的费用基本为0。如果需要后端提供服务,则需要在拿出一大笔钱进行后端的开发和服务器的购买费用。


如何降低开发成本


外包项目


这种模式允许利用全球人才库,通常以比雇用本地人才更具竞争力的价格获得服务。这点如果你在美国等发达国家可以考虑。如果在大陆,可以看看三哥他们。此外,这种方式还减少了对办公空间和设备的需求,并减少了与员工福利和津贴相关的管理费用。


明确项目要求


还是那句话,最后的成本一定与开始的需求有着很大关联。所以一定要精简需求,明确App到底要做什么。


专注于敏捷方法


如果你是个人开发者或者要带领团队开发,那一定要注重敏捷开发,确定任务优先级、经常重新评估和调整项目目标。


结论 — 关于开发一款App的成本


关于开发一款App的成本,为了让大家能更直观的感受,让我们具体数字来说明这一点。(采用Flutter跨平台)



  1. 对于简单功能的App(例如提供膳食计划App、日记App、记账App等),估计开发成本约为 10,000 — 50,000人民币之间,根据项目的复杂度来决定。

  2. 对于中等复杂度的App(例如具有即时通讯、语音通话等功能)预计成本约为 50,000 — 150,000人民币之间。

  3. 对于开发高复杂度的应用,例如抖音(简化版,真抖音现在哪个团队能从0开始做一个...),起价基本在150,000,上不封顶。


那这就是当前开发一款App的成本,以及对应的工作。


免责声明:本文中提供的数字是大致的、调研来的,可能会根据具体项目要求而有所不同!!!


作者:编程的平行世界
来源:juejin.cn/post/7312353213348347916
收起阅读 »

ThreadLocal:你不知道的优化技巧,Android开发者都在用

引言 在Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。 本文将深入探讨Andr...
继续阅读 »

引言


Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。


本文将深入探讨Android中的ThreadLocal原理及其使用技巧, 帮助你更好的理解和使用ThreadLocal


ThreadLocal的原理


public class Thread implements Runnable {

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal的原理是基于每个线程都有一个独立的ThreadLocalMap对象。ThreadLocalMap对象是一个Map,它的键是ThreadLocal对象,值是ThreadLocal对象保存的值。


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

当我们调用ThreadLocalset()方法时,会将值存储到当前线程的ThreadLocalMap对象中。当我们调用ThreadLocalget()方法时,会从当前线程的ThreadLocalMap对象中获取值。


ThreadLocal的使用


使用ThreadLocal非常简单,首先需要创建一个ThreadLocal对象,然后通过setget方法来设置和获取线程的局部变量。以下是一个简单的例子:


val threadLocal = ThreadLocal<String>()

fun setThreadName(name: String) {
threadLocal.set(name)
}

fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}

Android开发中,ThreadLocal的使用场景非常多,比如:



  • Activity中存储Fragment的状态

  • Handler中存储消息的上下文

  • RecyclerView中存储滚动位置


实际应用场景


// 在 Activity 中存储 Fragment 的状态
class MyActivity : AppCompatActivity() {

private val mFragmentState = ThreadLocal<FragmentState>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)

// 获取 Fragment 的状态
val fragmentState = mFragmentState.get()
if (fragmentState == null) {
// 初始化 Fragment 的状态
fragmentState = FragmentState()
}

// 设置 Fragment 的状态
mFragmentState.set(fragmentState)

// 创建 Fragment
val fragment = MyFragment()
fragment.arguments = fragmentState.toBundle()
supportFragmentManager.beginTransaction().add(R.id.container, fragment).commit()
}

}

class FragmentState {

var name: String? = null
var age: Int? = null

fun toBundle(): Bundle {
val bundle = Bundle()
bundle.putString("name", name)
bundle.putInt("age", age)
return bundle
}

}

这段代码在Activity中使用ThreadLocal来存储Fragment的状态。当Activity第一次启动时,会初始化Fragment的状态。当Activity重新启动时,会从ThreadLocal中获取Fragment的状态,并将其传递给Fragment


注意事项



  • 内存泄漏风险:


ThreadLocal变量的生命周期与线程的生命周期是一致的。这意味着,如果一个线程一直不结束,那么它所持有的ThreadLocal变量也不会被释放。这可能会导致内存泄漏。


为了避免内存泄漏,我们应该在不再需要ThreadLocal变量时,显式地将其移除。


threadLocal.remove()


  • 不适合全局变量: ThreadLocal适用于需要在线程间传递的局部变量,但不适合作为全局变量的替代品。


优化技巧



  • 合理使用默认值: 在获取ThreadLocal值时,可以通过提供默认值来避免返回null,确保代码的健壮性。


fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}


  • 懒加载初始化: 避免在声明ThreadLocal时就初始化,可以使用initialValue方法进行懒加载,提高性能。


val threadLocal = object : ThreadLocal<String>() {
override fun initialValue(): String {
return "DefaultValue"
}
}


  • 尽量避免在ThreadLocal中保存大对象


结论


在本文中,我们介绍了ThreadLocal的原理和使用技巧,希望这些知识能够帮助你更好地理解和使用它。


推荐


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/7317859658285858842
收起阅读 »

Android 通知文本颜色获取

前言 Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便A...
继续阅读 »

前言


Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便AlarmManager#setAlarmClock这种可以解除Dozen模式的超级工具,也无法对抗进程死亡的问题,通知到达率和及时性的效果已经大幅减弱。


Screenshot_20190403-131551.png


自定义通知是否仍有必要?

实际上,目前大多推送通知都被系统厂商代理展示了,导致实现效果雷同且没有新意。众多一样的效果,站在用户角度也产生了很多厌恶情绪,对用户的吸引点也是逐渐减弱,这其实和自定义通知的初衷是相背离的,因为自定义通知首要解决的是特色功能的展示,而通用通知却很难做到这一点。因此,在一些app中,自定义通知仍然是有必要的,但必要性没有那么强了。


当前的使用场景:



  • 前台进程常驻类型app,比如直播、音乐类等

  • 类似QQ的app浮动弹窗提醒 (这类不算通知,但是可以使用统一的方法适配)

  • 系统白名单中的app


现状


通知首要解决的是功能问题,其次是主题问题。当前,大部分app已经习惯使用系统通知栏而不使用自定义的通知,主要原因是适配难度问题。


对于自定义通知的适配,目前有两条路线:



  • 统一样式:

    是定义一套深色模式和浅色模式都能通用的色彩搭配,一些音视频app也是这么做的,巧妙的避免了因系统主题不一致造成的现实效果不同的问题,但仍然在部分手机上展示的比较突兀。

  • 读取系统通知颜色进行适配:

    遗憾的是,在Android 7.0之后,正常的通知是拿不到notification.contentView,但似乎没有看到相关的文章来解决这个问题。


两种方案可以搭配使用,但方案二目前存在无法提取颜色的问题,关键是怎么解决contentView拿不到的问题呢?接下来我们重点解决方案二的这个问题。


问题点


我们在无论使用NotificationBuilder或者NotificationCompatBuilder,其内部的build方法存在targetSdkVersion的判断,而在大于Android 7.0 的版本中,不会立即创建ContentView


protected Notification buildInternal() {
if (Build.VERSION.SDK_INT >= 26) {
return mBuilder.build();
} else if (Build.VERSION.SDK_INT >= 24) {
Notification notification = mBuilder.build();

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 21) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
if (mHeadsUpContentView != null) {
notification.headsUpContentView = mHeadsUpContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}
return notification;
} else if (Build.VERSION.SDK_INT >= 20) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 19) {
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
mExtras.putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else if (Build.VERSION.SDK_INT >= 16) {
Notification notification = mBuilder.build();
// Merge in developer provided extras, but let the values already set
// for keys take precedence.
Bundle extras = NotificationCompat.getExtras(notification);
Bundle mergeBundle = new Bundle(mExtras);
for (String key : mExtras.keySet()) {
if (extras.containsKey(key)) {
mergeBundle.remove(key);
}
}
extras.putAll(mergeBundle);
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
NotificationCompat.getExtras(notification).putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else {
return mBuilder.getNotification();
}
}

那么我们怎么解决这个问题呢?


Context Wrapper


在App 开发中,Context Wrapper是常见的事情,比如用在预加载Layout、模拟Service运行、插件加载等方面有大量使用。


本文思路是要hack targetSdkVersion,但targetSdkVersion是保存在ApplicationInfo中的,不过没关系,它是通过Context获取的,因此我们在它获取前将其修改为android 5.0的不就行了?


为什么可以修改ApplicationInfo,因为其事Parcelable的子类,看到Parcleable的子类你就能明白,该类的修改是不会触发系统服务的调度,但会影响部分功能,安全起见,我们可以拷贝一下。


public class NotificationContext extends ContextWrapper {
private Context mContextBase;
private ApplicationInfo mApplicationInfo;
private NotificationContext(Context base) {
super(base);
this.mContextBase = base;
}

@Override
public ApplicationInfo getApplicationInfo() {
if(mApplicationInfo!=null) return mApplicationInfo;
ApplicationInfo applicationInfo = super.getApplicationInfo();
mApplicationInfo = new ApplicationInfo(applicationInfo);
return mApplicationInfo;
}

public static NotificationContext from(Context context) {
return new NotificationContext(context);
}
}

targetSdkVersion hack


下一步,修改targetSdkVersion 为android 5.0版本


NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);

完整的代码


要获取的属性


class NotificationResourceInfo {
String titleResourceName;
int titleColor;
float titleTextSize;
ViewGr0up.LayoutParams titleLayoutParams;
String descResourceName;
int descColor;
float descTextSize;
ViewGr0up.LayoutParams descLayoutParams;
long updateTime;

}

获取颜色,用于判断是不是深色模式,这里其实利用的是标记查找方法,先给标题和内容设置Text,然后查找具备此Text的TextView


private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

下面是核心查找逻辑


  //遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

NotificationThemeHelper 实现


public class NotificationThemeHelper {
private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

final static String TAG = "NotificationThemeHelper";
static SoftReference<NotificationResourceInfo> notificationInfoReference = null;
private static final String CHANNEL_NOTIFICATION_ID = "CHANNEL_NOTIFICATION_ID";

public NotificationResourceInfo parseNotificationInfo(Context context) {
String channelId = createNotificationChannel(context, CHANNEL_NOTIFICATION_ID, CHANNEL_NOTIFICATION_ID);
NotificationResourceInfo notificationInfo = null;
NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

try {
applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);
//更改版本号,这样可以让builder自行创建contentview
NotificationCompat.Builder builder = new NotificationCompat.Builder(notificationContext, channelId);
builder.setContentTitle(TITLE_TEXT);
builder.setContentText(CONTENT_TEXT);
int icon = context.getApplicationInfo().icon;
builder.setSmallIcon(icon);
Notification notification = builder.build();
if (notification.contentView == null) {
return null;
}
int layoutId = notification.contentView.getLayoutId();
ViewGr0up root = (ViewGr0up) LayoutInflater.from(context).inflate(layoutId, null);
notificationInfo = getNotificationInfo(notificationContext, root);

} catch (Exception e) {
Log.d(TAG, "更新失败");
} finally {
applicationInfo.targetSdkVersion = targetSdkVersion;
}
return notificationInfo;
}

private NotificationResourceInfo getNotificationInfo(Context Context, ViewGr0up root) {
NotificationResourceInfo resourceInfo = new NotificationResourceInfo();

root.measure(0,0);
root.layout(0,0,root.getMeasuredWidth(),root.getMeasuredHeight());

Log.i(TAG,"bitmap ok");

TextView titleTextView = (TextView) root.findViewById(android.R.id.title);
if (titleTextView == null) {
titleTextView = findView(root, "android:id/title");
}
if (titleTextView != null) {
resourceInfo.titleColor = titleTextView.getCurrentTextColor();
resourceInfo.titleResourceName = getResourceIdName(Context, titleTextView.getId());
resourceInfo.titleTextSize = titleTextView.getTextSize();
resourceInfo.titleLayoutParams = titleTextView.getLayoutParams();
}

TextView contentTextView = findView(root, "android:id/text");
if (contentTextView != null) {
resourceInfo.descColor = contentTextView.getCurrentTextColor();
resourceInfo.descResourceName = getResourceIdName(Context, contentTextView.getId());
resourceInfo.descTextSize = contentTextView.getTextSize();
resourceInfo.descLayoutParams = contentTextView.getLayoutParams();
}
return resourceInfo;
}

//遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

private void saveNotificationInfoToReference(NotificationResourceInfo notificationInfo) {
if (notificationInfoReference != null) {
notificationInfoReference.clear();
}

if (notificationInfo == null) return;
notificationInfo.updateTime = SystemClock.elapsedRealtime();
notificationInfoReference = new SoftReference<NotificationResourceInfo>(notificationInfo);
}

private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

public NotificationResourceInfo getNotificationInfoFromReference() {
if (notificationInfoReference == null) {
return null;
}
NotificationResourceInfo resourceInfo = notificationInfoReference.get();
if (resourceInfo == null) {
return null;
}
long dx = SystemClock.elapsedRealtime() - resourceInfo.updateTime;
if (dx > 10 * 1000) {
return null;
}
return resourceInfo;
}

public static String getResourceIdName(Context context, int id) {

Resources r = context.getResources();
StringBuilder out = new StringBuilder();
if (id > 0 && resourceHasPackage(id) && r != null) {
try {
String pkgName;
switch (id & 0xff000000) {
case 0x7f000000:
pkgName = "app";
break;
case 0x01000000:
pkgName = "android";
break;
default:
pkgName = r.getResourcePackageName(id);
break;
}
String typeName = r.getResourceTypeName(id);
String entryName = r.getResourceEntryName(id);
out.append(pkgName);
out.append(":");
out.append(typeName);
out.append("/");
out.append(entryName);
} catch (Resources.NotFoundException e) {
}
}
return out.toString();
}

private String createNotificationChannel (Context context,String channelID, String channelNAME){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager)context. getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, NotificationManager.IMPORTANCE_LOW);
manager.createNotificationChannel(channel);
return channelID;
} else {
return null;
}
}
public static boolean resourceHasPackage(int resid) {
return (resid >>> 24) != 0;
}
}

深浅色判断其实有两种方法,第一种是305911公式,第二种是相似度。


下面是305911公式的,其实就是利用视频亮度算法YUV中的Y分量计算,Y分量表示明亮度。


private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

第二种是相似度算法,一般用于检索相似照片,一般用于优化汉明距离算法,不过这里可以用来判断是否接近黑色。
blog.csdn.net/zz_dd_yy/ar…


private boolean isSimilarColor(int colorL, int colorR) {
int red = Color.red(colorL);
int green = Color.green(colorL);
int blue = Color.blue(colorL);

int red2 = Color.red(colorR);
int green2 = Color.green(colorR);
int blue2 = Color.blue(colorR);

float vertor = red * red2 + green * green2 + blue * blue2;
// 向量1的模
double vectorMold1 = Math.sqrt(Math.pow(red, 2) + Math.pow(green, 2) + Math.pow(blue, 2));
// 向量2的模
double vectorMold2 = Math.sqrt(Math.pow(red2, 2) + Math.pow(green2, 2) + Math.pow(blue2, 2));

// 向量的夹角[0, PI],当夹角为锐角时,cosθ>0;当夹角为钝角时,cosθ<0
float cosAngle = (float) (vertor / (vectorMold1 * vectorMold2));
float radian = (float) Math.acos(cosAngle);

float degrees = (float) Math.toDegrees(radian);
if(degrees>= 0 && degrees < 30) {
return true;
}
return false;
}


用法


这种适配其实无法拿到背景色,只能拿到文字的颜色,如果文字偏亮则背景必须的是深色,反之区亮色,那么核心方法是下面的实现


public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

遗留问题


正常情况下,只能取深色和暗色,但是如果存在系统UI Mode的变化时,已经展示出来的通知,显然适配颜色无法动态变化,这也是无法避免的,解决办法是删除通知后重新发送。


总结


本篇到这里就结束了,说实在的,Android的通知的重要性大不如从前,但是必要的适配还是需要的。


作者:时光少年
来源:juejin.cn/post/7320146668476645387
收起阅读 »

PageHelper引发的“幽灵数据”,怎么回事?

前言 最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢? 大胆猜测 首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代...
继续阅读 »

前言


最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢?


大胆猜测


首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代码如下:


public  List<SdSubscription> findAll() {
return sdSubscriptionMapper.selectAll();
}

那么究竟是咋回事呢?讲道理不可能出现这种情况的啊,不要慌,我们加点日志,将日志级别调整为DEBUG,让日志飞一段时间。


public  List<SdSubscription> findAll() {
log.info("find the sub start .....");
List<SdSubscription> subs = sdSubscriptionMapper.selectAll();
log.info("find the sub end .....");
return subs;
}

果不其然,日志中出现了奇奇怪怪的分页参数,如下图所示:



果然是PageHelper这个开源框架搞的鬼,我想大家都用过吧,分页非常方便,那么究竟为什么别人都没问题,单单就我会出现问题呢?


PageHelper工作原理


为了回答上面的疑问,我们先看看PageHelper框架的工作原理吧。


PageHelper 是一个开源的 MyBatis 分页插件,它可以帮助开发者在查询数据时,快速的实现分页功能。


PageHelper 的工作原理可以简单概括为以下几个步骤:



  1. 在需要进行分页的查询方法前,调用 PageHelper 的静态方法 startPage(),设置当前页码和每页显示的记录数。它会将分页信息放到线程的ThreadLocal中,那么在线程的任何地方都可以访问了。

  2. 当查询方法执行时,PageHelper 会自动拦截查询语句,如果发现线程的ThreadLocal中有分页信息,那么就会在其前后添加分页语句,例如 MySQL 中的 LIMIT 语句。

  3. 查询结果将被包装在 Page 对象中返回,该对象包含分页信息和查询结果列表。

  4. 在查询方法执行完毕后,会在finally中清除线程ThreadLocal中的分页信息,避免分页设置对其他查询方法的影响。


PageHelper 的实现原理主要依赖于拦截器技术和反射机制,通过拦截查询语句并动态生成分页语句,实现了简单、高效、通用的分页功能。具体源码在下图的类中,非常容易看懂。



明白了PageHelper的工作原理后,反复检查代码,都没有调用过startPagedebug查看ThreadLocal中也没有分页信息啊,懵逼中。那我看看别人写的添加分页参数的代码吧,不看不知道,一看吓一跳。



原来有位“可爱”的同事竟然在查询后,加了一个分页,就是把分页信息放到线程的ThreadLocal中。


那大家是不是有疑问,丁是丁,矛是矛,你的线程关我何事?这就要说到我们的tomcat了。


Tomcat请求流程


其实这就涉及到我们的tomcat相关知识了,我们一个浏览器发一个接口请求,经过我们的tomcat的,究竟是一个什么样的流程呢?



  1. 客户端发送HTTP请求到Tomcat服务器。

  2. TomcatHTTP连接器(Connector)接收到请求,将连接请求交给线程池Executor处理,解析它,然后将请求转发给对应的Web应用程序。

  3. Tomcat的Web应用程序容器(Container)接收到请求,根据请求的URL找到对应的Servlet


关于tomcat中使用线程池提交浏览器的连接请求的源码如下:



从而得知,你的连接请求是从线程池从拿的,而拿到的这个线程恰好是一个“脏线程”,在ThreadLocal中放了分页信息,导致你这边出现问题。


总结


后来追问了同事具体原因,才发现是粗心导致的。有些bug总是出现的莫名其妙,就像生活一样。所以关键的是我们在使用一些开源框架的时候一定要掌握底层实现的原理、核心的机制,这样才能够在解决一些问题的时候有据可循。



欢迎关注个人公众号【JAVA旭阳】交流学习!



作者:JAVA旭阳
来源:juejin.cn/post/7223590232730370108
收起阅读 »

该死,这次一定要弄懂什么是时间复杂度和空间复杂度!

开始首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样: Q:那么到底这个O(n)、O(1)是什么意思呢?A:时间复杂度和空间复杂度其实是对算法执行期间的性能进行衡量的...
继续阅读 »

开始

首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样:

image.png Q:那么到底这个O(n)、O(1)是什么意思呢?

A:时间复杂度空间复杂度其实是对算法执行期间的性能进行衡量的依据。

Talk is cheap, show me the code!

下面从代码入手,来直观的理解一下这两个概念:

时间复杂度

先来看看copilot如何解释的

image.png

  • 举个🌰
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

首先来分析一下这段代码,这是一个函数,接收一个数组,然后对这个数组进行了一个遍历

  1. 第一段代码,在函数执行的时候,这段代码只会被执行1次,这里记为 1 次
let length = arr.length
  1. 循环体中的代码,循环多少次就会执行多少次,这里记为 n 次
console.log(arr[i])
  1. 循环条件部分,首先是 let i = 0,只会执行一次,记为 1 次
  2. 然后是i < length这个判断,想要退出循环,这里最后肯定要比循环次数多判断一次,所以记为 n + 1 次
  3. 最后是 i++,会执行 n 次

我们把总的执行次数记为T(n)

T(n) = 1 + n + 1 (n + 1) + n = 3n + 3
  • 再来一个🌰
// arr 是一个二维数组
function fn2(arr) {
let lenOne = arr.length
for(let i = 0; i < lenOne; i++) {
let lenTwo = arr[i].length
for(let j = 0; j < lenTwo; j++) {
console.log(arr[i][j])
}
}
}

来分析一下这段代码,这是一个针对二维数组进行遍历的操作,我们再来分析一下这段代码的执行次数

  1. 第一行赋值代码,只会执行1次
let lenOne = arr.length
  1. 第一层循环,let i = 0 1次,i < lenOne n + 1 次,i++ n 次,let len_two = arr[i].length n 次
  2. 第二层循环,let j = 0 n 次,j < lenTwo n * (n + 1) 次,j++ n * n 次
  3. console n*n 次
T(n) = 1 + n + 1 + n + n + n + n * (n + 1) + n * n + n * n = 3n^2 + 5n + 3

代码的执行次数,可以反映出代码的执行时间。但是如果每次我们都逐行去计算 T(n),事情会变得非常麻烦。算法的时间复杂度,它反映的不是算法的逻辑代码到底被执行了多少次,而是随着输入规模的增大,算法对应的执行总次数的一个变化趋势。我们可以尝试对 T(n) 做如下处理:

  • 若 T(n) 是常数,那么无脑简化为1
  • 若 T(n) 是多项式,比如 3n^2 + 5n + 3,我们只保留次数最高那一项,并且将其常数系数无脑改为1。

那么上面两个算法的时间复杂度可以简化为:

T(n) = 3n + 3
O(n) = n

T(n) = 3n^2 + 5n + 3
O(n) = n^2

实际推算时间复杂度时不用这么麻烦,像上面的两个函数,第一个是规模为n的数组的遍历,循环会执行n次,所以对应的时间幅度是O(n),第二个函数是 n*n的二维数组的遍历,对应的时间复杂度就是O(n^2) 依次类推,规模为n*m的二维数组的遍历,时间复杂度就是O(n*m)

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数时间对数时间线性时间线性对数时间二次时间三次时间指数时间
O(1)O(logn)O(n)O(nlogn)O(n^2)O(n^3)O(2^n)

空间复杂度

先看看copilot的解释:

image.png

  • 来一个🌰看看吧:
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

在函数fn中,我们创建了变量 length arr i,函数 fn 对内存的占用量是固定的,无论,arr的length如何,所以这个函数对应的空间复杂度就是 O(1)

  • 再来一个🌰:
function fn2(n) {
let arr = []
for(let i = 0; i < n; i++) {
arr[i] = i
}
}

在这个函数中,我们创建了一个数组 arr,并在循环中向 arr 中添加了 n 个元素。因此,arr 的大小与输入 n 成正比。所以,我们说这个函数的空间复杂度是 O(n)。

  • 再再来一个🌰:
function createMatrix(n) {
let matrix = [];
for (let i = 0; i < n; i++) {
matrix[i] = [];
for (let j = 0; j < n; j++) {
matrix[i][j] = 0;
}
}
return matrix;
}

在这个函数中,我们创建了一个二维数组 matrix,并在两层循环中向 matrix 中添加了 n*n 个元素。因此,matrix 的大小与输入 n 的平方成正比。所以,我们说这个函数的空间复杂度是 O(n^2)。

  • 再再再来一个🌰:
// 二分查找算法
function binarySearch(arr, target, low, high) {
if (low > high) {
return -1;
}
let mid = Math.floor((low + high) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] > target) {
return binarySearch(arr, target, low, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, high);
}
}

在二分查找中,我们每次都将问题规模减半,因此需要的额外空间与输入数据的对数成正比,我们开始时有一个大小为 n 的数组。然后,我们在每一步都将数组划分为两半,并只在其中一半中继续查找。因此,每一步都将问题的规模减半

所以,最多要划分多少次才能找到目标数据呢?答案是log2n次,但是在计算机科学中,当我们说 log n 时,底数通常默认为 2,因为许多算法(如二分查找)都涉及到将问题规模减半的操作。

2^x = n

x = log2n

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数空间线性空间平方空间对数空间
O(1)O(n)O(n^2)O(logn)

你学废了吗?


作者:爱吃零食的猫
来源:juejin.cn/post/7320288222529536038

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;

系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7230810119122190397
收起阅读 »

刷了四百道算法题,我在项目里用过哪几道呢?

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。 不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公...
继续阅读 »

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。


不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公司面试的标配,力扣现在已经超过3000题了,那么这些题目有多少进入了面试的考察呢?


以最爱考算法的字节跳动为例,看看力扣的企业题库,发现考过的题目已经有1850道——按照平均每道题花20分钟来算,刷完字节题库的算法题需要37000分钟,616.66小时,按每天刷满8小时算,需要77.08天,一周刷五天,需要15.41周,按一个月四周,需要3.85个月。也就是说,在脱产,最理想的状态下,刷完力扣的字节题库,需要差不多4个月时间。


字节题库


那么,我在项目里用过,包括在项目中见过哪些力扣上的算法呢?我目前刷了400多道题,翻来覆去盘点了一下,发现,也就这么几道。


刷题数量


1.版本比较:比较客户端版本


场景


在日常的开发中,我们很多时候可能面临这样的情况,兼容客户端的版本,尤其是Android和iPhone,有些功能是低版本不支持的,或者说有些功能到了高版本就废弃掉。


这时候就需要进行客户端的版本比较,客户端版本号通常是这种格式6.3.40,这是一个字符串,那就肯定不能用数字类型的比较方法,需要自己定义一个比较的工具方法。


某app版本


题目


165. 比较版本号


这个场景对应LeetCode: 165. 比较版本号



  • 题目:165. 比较版本号 (leetcode.cn/problems/co…)

  • 难度:中等

  • 标签:双指针 字符串

  • 描述:


    给你两个版本号 version1version2 ,请你比较它们。


    版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.330.1 都是有效的版本号。


    比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 010 < 1


    返回规则如下:



    • 如果 *version1* > *version2* 返回 1

    • 如果 *version1* < *version2* 返回 -1

    • 除此之外返回 0


    示例 1:


    输入:version1 = "1.01", version2 = "1.001"
    输出:0
    解释:忽略前导零,"01""001" 都表示相同的整数 "1"

    示例 2:


    输入:version1 = "1.0", version2 = "1.0.0"
    输出:0
    解释:version1 没有指定下标为 2 的修订号,即视为 "0"

    示例 3:


    输入:version1 = "0.1", version2 = "1.1"
    输出:-1
    解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1"0 < 1,所以 version1 < version2

    提示:



    • 1 <= version1.length, version2.length <= 500

    • version1version2 仅包含数字和 '.'

    • version1version2 都是 有效版本号

    • version1version2 的所有修订号都可以存储在 32 位整数




解法


那么这道题怎么解呢?这道题其实是一道字符串模拟题,就像标签里给出了了双指针,这道题我们可以用双指针+累加来解决。


在这里插入图片描述



  • 两个指针遍历version1version2

  • . 作为分隔符,通过累加获取每个区间代表的数字

  • 比较数字的大小,这种方式正好可以忽略前导0


来看看代码:


class Solution {
   public int compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


应用


这段代码,直接CV过来,就可以直接当做一个工具类的工具方法来使用:


public class VersionUtil {

   public static Integer compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


前面老三分享过一个规则引擎:这款轻量级规则引擎,真香!


比较版本号的方法,还可以结合规则引擎来使用:



  • 自定义函数:利用AviatorScript的自定义函数特性,自定义一个版本比较函数


        /**
        * 自定义版本比较函数
        */

       class VersionFunction extends AbstractFunction {
           @Override
           public String getName() {
               return "compareVersion";
          }

           @Override
           public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
               // 获取版本
               String version1 = FunctionUtils.getStringValue(arg1, env);
               String version2 = FunctionUtils.getStringValue(arg2, env);
               return new AviatorBigInt(VersionUtil.compareVersion(version1, version2));
          }
      }


  • 注册函数:将自定义的函数注册到AviatorEvaluatorInstance


        /**
        * 注册自定义函数
        */

       @Bean
       public AviatorEvaluatorInstance aviatorEvaluatorInstance() {
           AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
           // 默认开启缓存
           instance.setCachedExpressionByDefault(true);
           // 使用LRU缓存,最大值为100个。
           instance.useLRUExpressionCache(100);
           // 注册内置函数,版本比较函数。
           instance.addFunction(new VersionFunction());
           return instance;
      }


  • 代码传递上下文:在业务代码里传入客户端、客户端版本的上下文


        /**
        * @param device 设备
        * @param version 版本
        * @param rule   规则脚本
        * @return 是否过滤
        */

       public boolean filter(String device, String version, String rule) {
           // 执行参数
           Map<String, Object> env = new HashMap<>();
           env.put("device", device);
           env.put("version", version);
           //编译脚本
           Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
           //执行脚本
           boolean isMatch = (boolean) expression.execute(env);
           return isMatch;
      }


  • 编写脚本:接下来我们就可以编写规则脚本,规则脚本可以放在数据库,也可以放在配置中心,这样就可以灵活改动客户端的版本控制规则


    if(device==bil){
    return false;
    }

    ## 控制Android的版本
    if (device=="Android" && compareVersion(version,"1.38.1")<0){
    return false;
    }

    return true;



2.N叉数层序遍历:翻译商品类型


场景


一个跨境电商网站,现在有这么一个需求:把商品的类型进行国际化翻译。


某电商网站商品类型国际化


商品的类型是什么结构呢?一级类型下面还有子类型,字类型下面还有子类型,我们把结构一画,发现这就是一个N叉树的结构嘛。


商品树


翻译商品类型,要做的事情,就是遍历这棵树,翻译节点上的类型,这不妥妥的BFS或者DFS!


题目


429. N 叉树的层序遍历


这个场景对应LeetCode:429. N 叉树的层序遍历



  • 题目:429. N 叉树的层序遍历(leetcode.cn/problems/n-…)

  • 难度:中等

  • 标签: 广度优先搜索

  • 描述:


    给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。


    树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。


    示例 1:


    img


    输入:root = [1,null,3,2,4,null,5,6]
    输出:[[1],[3,2,4],[5,6]]

    示例 2:


    img


    输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
    输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]

    提示:



    • 树的高度不会超过 1000

    • 树的节点总数在 [0, 10^4] 之间




解法


BFS想必很多同学都很熟悉了,DFS的秘诀是,BFS的秘诀是队列


层序遍历的思路是什么呢?


使用队列,把每一层的节点存储进去,一层存储结束之后,我们把队列中的节点再取出来,孩子节点不为空,就把孩子节点放进去队列里,循环往复。


N叉树层序遍历示意图


代码如下:


class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}

//创建队列并存储根节点
Deque<Node> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
//存储每层结果
List<Integer> level = new ArrayList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
Node current = queue.poll();
level.add(current.val);
//添加孩子
if (current.children != null) {
for (Node child : current.children) {
queue.offer(child);
}
}
}
//每层遍历结束,添加结果
result.add(level);
}
return result;
}
}

应用


商品类型翻译这个场景下,基本上和这道题目大差不差,不过是两点小区别:



  • 商品类型是一个属性多一些的树节点

  • 翻译过程直接替换类型名称即可,不需要返回值


来看下代码:



  • ProductCategory:商品分类实体


    public class ProductCategory {
    /**
    * 分类id
    */

    private String id;
    /**
    * 分类名称
    */

    private String name;
    /**
    * 分类描述
    */

    private String description;
    /**
    * 子分类
    */

    private List<ProductCategory> children;

    //省略getter、setter

    }




  • translateProductCategory:翻译商品类型方法


       public void translateProductCategory(ProductCategory root) {
    if (root == null) {
    return;
    }

    Deque<ProductCategory> queue = new LinkedList<>();
    queue.offer(root);

    //遍历商品类型,翻译
    while (!queue.isEmpty()) {
    int size = queue.size();
    //遍历当前层
    for (int i = 0; i < size; i++) {
    ProductCategory current = queue.poll();
    //翻译
    String translation = translate(current.getName());
    current.setName(translation);
    //添加孩子
    if (current.getChildren() != null && !current.getChildren().isEmpty()) {
    for (ProductCategory child : current.getChildren()) {
    queue.offer(child);
    }
    }
    }
    }
    }



3.前缀和+二分查找:渠道选择


场景


在电商的交易支付中,我们可以选择一些支付方式,来进行支付,当然,这只是交易的表象。


某电商支付界面


在支付的背后,一种支付方式,可能会有很多种支付渠道,比如Stripe、Adyen、Alipay,涉及到多个渠道,那么就涉及到决策,用户的这笔交易,到底交给哪个渠道呢?


这其实是个路由问题,答案是加权随机,每个渠道有一定的权重,随机落到某个渠道,加权随机有很多种实现方式,其中一种就是前缀和+二分查找。简单说,就是先累积所有元素权重,再使用二分查找来快速查找。


题目


先来看看对应的LeetCode的题目,这里用到了两个算法:前缀和二分查找


704. 二分查找



  • 题目:704. 二分查找(leetcode.cn/problems/bi…)

  • 难度:简单

  • 标签:数组 二分查找

  • 描述:


    给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1


    示例 1:


    输入: nums = [-1,0,3,5,9,12], target = 9
    输出: 4
    解释: 9 出现在 nums 中并且下标为 4

    示例 2:


    输入: nums = [-1,0,3,5,9,12], target = 2
    输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    提示:



    1. 你可以假设 nums 中的所有元素是不重复的。

    2. n 将在 [1, 10000]之间。

    3. nums 的每个元素都将在 [-9999, 9999]之间。




解法


二分查找可以说我们都很熟了。


数组是有序的,定义三个指针,leftrightmid,其中midleftright的中间指针,每次中间指针指向的元素nums[mid]比较和target比较:


二分查找示意图



  • 如果nums[mid]等于target,找到目标

  • 如果nums[mid]小于target,目标元素在(mid,right]区间;

  • 如果nums[mid]大于target,目标元素在[left,mid)区间


代码:


class Solution {
public int search(int[] nums, int target) {
int left=0;
int right=nums.length-1;

while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
return mid;
}else if(nums[mid]<target){
//target在(mid,right]区间,右移
left=mid+1;
}else{
//target在[left,mid)区间,左移
right=mid-1;
}
}
return -1;
}
}

二分查找,有一个需要注意的细节,计算mid的时候:int mid = left + ((right - left) >> 1);,为什么要这么写呢?


因为这种写法int mid = (left + right) / 2;,可能会因为left和right数值太大导致内存溢出。同时,使用位运算,也是除以2最高效的写法。


——这里有个彩蛋,后面再说。


303. 区域和检索 - 数组不可变


不像二分查找,在LeetCode上,前缀和没有直接的题目,因为本身前缀和更多是一种思路,一种工具,其中303. 区域和检索 - 数组不可变 是一道典型的前缀和题目。



  • 题目:303. 区域和检索 - 数组不可变(leetcode.cn/problems/ra…)

  • 难度:简单

  • 标签:设计 数组 前缀和

  • 描述:


    给定一个整数数组 nums,处理以下类型的多个查询:



    1. 计算索引 leftright (包含 leftright)之间的 nums 元素的 ,其中 left <= right


    实现 NumArray 类:



    • NumArray(int[] nums) 使用数组 nums 初始化对象

    • int sumRange(int i, int j) 返回数组 nums 中索引 leftright 之间的元素的 总和 ,包含 leftright 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )


    示例 1:


    输入:
    ["NumArray", "sumRange", "sumRange", "sumRange"]
    [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
    输出:
    [null, 1, -1, -3]

    解释:
    NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
    numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
    numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
    numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

    提示:



    • 1 <= nums.length <= 104

    • -105 <= nums[i] <= 105

    • 0 <= i <= j < nums.length

    • 最多调用 104sumRange 方法




解法


这道题,我们如果不用前缀和的话,写起来也很简单:


class NumArray {
private int[] nums;

public NumArray(int[] nums) {
this.nums=nums;
}

public int sumRange(int left, int right) {
int res=0;
for(int i=left;i<=right;i++){
res+=nums[i];
}
return res;
}
}

当然时间复杂度偏高,O(n),那么怎么使用前缀和呢?



  • 构建一个前缀和数组,用来累积 (0……i-1)的和,这样一来,我们就可以直接计算[left,right]之间的累加和


前缀和数组示意图


代码如下:


class NumArray {
private int[] preSum;

public NumArray(int[] nums) {
int n=nums.length;
preSum=new int[n+1];
//计算nums的前缀和
for(int i=0;i<n;i++){
preSum[i+1]=preSum[i]+nums[i];
}
}

//直接算出区间[left,right]的累加和
public int sumRange(int left, int right) {
return preSum[right+1]-preSum[left];
}
}

可以看到,通过前缀和数组,可以直接算出区间[left,right]的累加和,时间复杂度O(1),可以说非常高效了。


应用


了解了前缀和和二分查找之后,回归我们之前的场景,使用前缀和+二分查找来实现加权随机,从而实现对渠道的分流选择。


渠道分流选择



  • 需要根据渠道和权重的配置,生成一个前缀和数组,来累积权重的值,渠道也通过一个数组进行分配映射

  • 用户的支付请求进来的时候,生成一个随机数,二分查找找到随机数载前缀和数组的位置,映射到渠道数组

  • 最后通过渠道数组的映射,找到选中的渠道


代码如下:


/**
* 支付渠道分配器
*/
public class PaymentChannelAllocator {
//渠道数组
private String[] channels;
//前缀和数组
private int[] preSum;
private ThreadLocalRandom random;

/**
* 构造方法
*
* @param channelWeights 渠道分流权重
*/
public PaymentChannelAllocator(HashMap<String, Integer> channelWeights) {
this.random = ThreadLocalRandom.current();
// 初始化channels和preSum数组
channels = new String[channelWeights.size()];
preSum = new int[channelWeights.size()];

// 计算前缀和
int index = 0;
int sum = 0;
for (String channel : channelWeights.keySet()) {
sum += channelWeights.get(channel);
channels[index] = channel;
preSum[index++] = sum;
}
}

/**
* 渠道选择
*/
public String allocate() {
// 生成一个随机数
int rand = random.nextInt(preSum[preSum.length - 1]) + 1;

// 通过二分查找在前缀和数组查找随机数所在的区间
int channelIndex = binarySearch(preSum, rand);
return channels[channelIndex];
}

/**
* 二分查找
*/
private int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;

while (left <= right) {
int mid = left + ((right - left) >> 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 当找不到确切匹配时返回大于随机数的最小前缀和的索引
return left;
}
}

测试一下:


    @Test
void allocate() {
HashMap<String, Integer> channels = new HashMap<>();
channels.put("Adyen", 50);
channels.put("Stripe", 30);
channels.put("Alipay", 20);

PaymentChannelAllocator allocator = new PaymentChannelAllocator(channels);

// 模拟100次交易分配
for (int i = 0; i < 100; i++) {
String allocatedChannel = allocator.allocate();
System.out.println("Transaction " + (i + 1) + " allocated to: " + allocatedChannel);
}
}

彩蛋


在这个渠道选择的场景里,还有两个小彩蛋。


二分查找翻车


我前面提到了一个二分查找求mid的写法:


int mid=left+((right-left)>>1);

这个写法机能防止内存溢出,用了位移运算也很高效,但是,这个简单的二分查找写出过问题,直接导致线上cpu飙升,差点给老三吓尿了。


吓惨了


int mid = (right - left) >> 2 + left;

就是这行代码,看出什么问题来了吗?


——它会导致循环结束不了!


为什么呢?因为>>运算的优先级是要低于+的,所以这个运算实际上等于:


int mid = (right - left) >> (2 + left);

在只有两个渠道的时候没有问题,三个的时候就寄了。


当然,最主要原因还是没有充分测试,所以大家知道我在上面为什么特意写了单测吧。


加权随机其它写法


这里用了前缀和+二分查找来实现加权随机,其实加权随机还有一些其它的实现方法,包括别名方法、树状数组、线段树 随机列表扩展 权重累积等等方法,大家感兴趣可以了解一下。


加权随机的实现


印象比较深刻的是,有场面试被问到了怎么实现加权随机,我回答了权重累积前缀和+二分查找,面试官还是不太满意,最后面试官给出了他的答案——随机列表扩展


什么是随机列表扩展呢?简单说,就是创建一个足够大的列表,根据权重,在相应的区间,放入对应的渠道,生成随机数的时候,就可以直接获取对应位置的渠道。


public class WeightedRandomList {
private final List<String> expandedList = new ArrayList<>();
private final Random random = new Random();

public WeightedRandomList(HashMap<String, Integer> weightMap) {
// 填充 expandedList,根据权重重复元素
for (String item : weightMap.keySet()) {
int weight = weightMap.get(item);
for (int i = 0; i < weight; i++) {
expandedList.add(item);
}
}
}

public String getRandomItem() {
// 生成随机索引并返回对应元素
int index = random.nextInt(expandedList.size());
return expandedList.get(index);
}

public static void main(String[] args) {
HashMap<String, Integer> items = new HashMap<>();
items.put("Alipay", 60);
items.put("Adyen", 20);
items.put("Stripe", 10);

WeightedRandomList wrl = new WeightedRandomList(items);

// 演示随机选择
for (int i = 0; i < 10; i++) {
System.out.println(wrl.getRandomItem());
}
}
}

这种实现方式就是典型的空间换时间,空间复杂度O(n),时间复杂度O(1)。优点是时间复杂度低,缺点是空间复杂度高,如果权重总和特别大的时候,就需要一个特别大的列表来存储元素。


当然这种写法还是很巧妙的,适合元素少、权重总和小的场景。


刷题随想


上面就是我在项目里用到过或者见到过的LeetCode算法应用,416:4,不足1%的使用率,还搞出过严重的线上问题。


……


在力扣社区里关于算法有什么的贴子里,有这样的回复:


“最好的结构是数组,最好的算法是遍历”。


“最好的算法思路是暴力。”


……


坦白说,如果不是为了面试,我是绝对不会去刷算法的,上百个小时,用在其他地方,绝对收益会高很多。


从实际应用去找刷LeetCode算法的意义,本身没有太大意义,算法题的最大意义就是面试。


刷了能过,不刷就挂,仅此而已。


这些年互联网行业红利消失,越来越多的算法题,只是内卷的产物而已。


当然,从另外一个角度来看,考察算法,对于普通的打工人,可能是个更公平的方式——学历、背景都很难卷出来,但是算法可以。


我去年面试的真实感受,“没机会”比“面试难”更令人绝望。


写到这,有点难受,刷几道题缓一下!






参考:


[1].leetcode.cn/circle/disc…


[2].36kr.com/p/121243626…


[3].leetcode.cn/circle/disc…


[4].leetcode.cn/circle/disc…







备注:涉及敏感信息,文中的代码都不是真实的投产代码,作者进行了一定的脱敏和演绎。





作者:三分恶
来源:juejin.cn/post/7321271017429712948
收起阅读 »

年终被砍、降薪、被拒,用我今年的经历给你几个忠告| 2023年终总结

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。 本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。 春节前 第一次大跌从1月20号开始,也就是春节放假前一...
继续阅读 »

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。


本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。



春节前



第一次大跌从1月20号开始,也就是春节放假前一天按照以往的经历是20号会发年终然而公司一波顶级操作一纸公告下来只有ABC绩效有年终而且与之前相比还打折,打开手机一看1000块过节费。后来才知道只给了部门几个可以拿年终的绩效名额,其他80%都是D。就这样拿着过节费过了一个年。


image.png



春节后



过完年回来后3月底要给我降薪,从组长那里得知原因是绩效评估是E,开完会后连忙去OA查询发现绩效评估为D,后来组长知道后开始和HR沟通。20号左右HR开始找我谈话开头先是道歉又说降薪不是以绩效为标准而是22年的几次线上事故影响过大原因。


一会是绩效组长沟通后又不是绩效,让我感觉是恶意降薪,就这样一直扯皮到快4月份。由于那段时间需要处理的事情太多不想和她扯皮所以选择同意降薪。


后来的小道消息得知系统录入的绩效和HR那里是两份,而系统里面高是因为有项目的加分,真不懂他们的绩效评估是怎么做的,那段时间真是可以说掉到了谷底身心俱疲。到今天想起来如果没有和别人说我系统中D绩效 HR没准也不会有其他理由降薪。


给打工人的第一个忠告:在公司里面谁也不要相信,定期收集考勤、加班证据,把证据握在自己手里,至于代码事故问题就写单元测试,留好评审会议记录,测试记录证据至少这样可以不被认定为主要责任。





9月、10月、11月裁员



之前一直听组长说23年业绩一直不好公司想要裁员到9月还是等到了,好像定了10个人将近部门人数的三分之一。因为公司砍掉了年终而且加班严重有几个小伙伴也有走的意愿,定了5个开发,还有几个转岗。10月又裁了几个开发,和被裁的小伙伴交流公司裁掉的全是年轻人30往上的一个没动,11月测试部门述职定不下名额直接两个测试全部裁掉


谈补偿HR又是神级操作先是套路员工灌输是自己想走,不是公司裁员不想给补偿金,后来又想按照实习期工资补偿,被部门几个人骂了后妥协了,年假还是不想给最后按照一倍补偿。到了发薪日又是一波操作最后几天的工资不给,听说要起诉公司又拖了一个月才发。都把人家裁了最后一天还在让别人加班太顶了。


给大家的第二个忠告:裁员的话不要慌也不要怕,一定要强硬,不要随便签字属于自己的赔偿一定要争取:赔偿金、代通知金、加班费、年假都算上,确定好最后的上班时间、社保、发薪时间。


给大家的第三个忠告:在公司不要和招惹或者和那些老员工、领导身边的红人翻脸,他们这些人就是能决定领导的想法,一边添油加醋一边对你笑嘻嘻





小插曲



8月底的一天HR突然找我说工时不够要扣工资,正常应该出勤23天184个小时,我其中一天请了假22天出勤了188个小时。按照之前公司要求加班的工时可以抵请假时长我用22天出勤了23天应出勤的时间是没问题的。HR的顶级算法是即使请假也要够应出勤工时然后多出来的才可以使用抵扣。真是这公司HR就是个大聪明数学不会算,最后还是没扣。


IMG_2392.jpg



年底



今年公司严格控制了部门支出,打车报销严查、加班也不管饭了。裁员后能干活的走了一半,现在的项目开发流程真是一言难尽,产品不设计原型、不写需求文档、不在OA提需求还说没有时间,需求没确定、没宣讲已经开始让开发这边开始了,开发按照做完初稿原型做完推倒重来。
三季度公司偷偷把社保调整到了80%,年底大言不惭的说在国家允许的情况下公积金调整到了5%,每次开会就是PUA让我们看看别的企业都在裁员应该把公司当成自己家一样。小道消息今年也没有年终。又沉闷的过了一年





出京



年终没了、也降薪了,放假后不想待在北京了端午直接去了杭州,由于接近亚运会的时间所以杭州氛围非常好,这个时候有点小梅雨,西湖边上拍的环境和氛围真的好。


IMG_1915.heic

周末和朋友们还去了承德,这个阳光和草原真绝了


trim.3A5310B6-27AB-4748-BA47-83A853A4C647.gif


11月去了南京,去南京是也为了自己的执念吧她还是没同意,这么久了也是时候放下了,第一次为了一个人跨越千里去了一个陌生的城市,鸡鸣寺的小猫都是两只。


IMG_2115.HEIC

年底和朋友几个去了威海,认识了一个辽宁的大哥开车带我们玩了一整天


2307e0a157b2162a6e595f689ed66b83.jpg

给大家的第四个忠告:工作不是你的全部,甚至不是你的生活,你要按照自己想过的方式去活着,有些事和东西得到了当然很好,你要知道得不到也不是你的问题尽人事听天命,降低期待。



2024计划



今年的计划是



  1. 继续走走到处去看看,西安、成都、武汉具体的到时候在看吧

  2. 在网上输出一些技术文章,之前的开发经历一直都没有沉淀

  3. 如果有机会可以继续搞搞副业,去年给朋友公司开发了一个APP,还有帮朋友做了一些需求

  4. 读书、读书、读书,继续学习,先试试中级软考吧,人还是不能停下来,一停下来就容易拖延

  5. 周末运动拒绝躺平,618全款拿下的公路车锻炼起来,身体才是革命的本钱

  6. 看机会,今年春节前有可能还会有一轮裁员,闲下来的时候看看机会。毕竟我们组的高级开发已经快3年没涨薪了,公司还不让人家走。


作者:旧梦呀
来源:juejin.cn/post/7320435287296032820
收起阅读 »

Android跳转系统界面_总结

1、跳转Setting应用列表(所有应用) Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS); this.startActivity(intent); ...
继续阅读 »

1、跳转Setting应用列表(所有应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS);
this.startActivity(intent);


2、跳转Setting应用列表(安装应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);


3、跳转Setting应用列表


Intent intent =  new Intent(Settings.ACTION_APPLICATION_SETTINGS);


4、开发者选项


Intent intent =  new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS);


5、允许在其它应用上层显示的应用


Intent intent =  new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);


6、无障碍设置


Intent intent =  new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);


7、添加账户


Intent intent =  new Intent(Settings.ACTION_ADD_ACCOUNT);


8、WIFI设置


Intent intent =  new Intent(Settings.ACTION_WIFI_SETTINGS);


9、蓝牙设置


Intent intent =  new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);


10、移动网络设置


Intent intent =  new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);


11、日期时间设置


Intent intent =  new Intent(Settings.ACTION_DATE_SETTINGS);


12、关于手机界面


Intent intent =  new Intent(Settings.ACTION_DEVICE_INFO_SETTINGS);


13、显示设置界面


Intent intent =  new Intent(Settings.ACTION_DISPLAY_SETTINGS);


14、声音设置


Intent intent =  new Intent(Settings.ACTION_SOUND_SETTINGS);


15、互动屏保


Intent intent =  new Intent(Settings.ACTION_DREAM_SETTINGS);


16、输入法


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS);


17、输入法_SubType


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);


18、内部存储设置界面


Intent intent =  new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);


19、存储卡设置界面


Intent intent =  new Intent(Settings.ACTION_MEMORY_CARD_SETTINGS);


20、语言选择界面


Intent intent =  new Intent(Settings.ACTION_LOCALE_SETTINGS);


21、位置服务界面


Intent intent =  new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);


22、运营商


Intent intent =  new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);


23、NFC共享界面


Intent intent =  new Intent(Settings.ACTION_NFCSHARING_SETTINGS);


24、NFC设置


Intent intent =  new Intent(Settings.ACTION_NFC_SETTINGS);


25、备份和重置


<Intent intent =  new Intent(Settings.ACTION_PRIVACY_SETTINGS);


26、快速启动


Intent intent =  new Intent(Settings.ACTION_QUICK_LAUNCH_SETTINGS);


27、搜索设置


Intent intent =  new Intent(Settings.ACTION_SEARCH_SETTINGS);


28、安全设置


Intent intent =  new Intent(Settings.ACTION_SECURITY_SETTINGS);


29、设置的主页


Intent intent =  new Intent(Settings.ACTION_SETTINGS);


30、用户同步界面


Intent intent =  new Intent(Settings.ACTION_SYNC_SETTINGS);


31、用户字典


Intent intent =  new Intent(Settings.ACTION_USER_DICTIONARY_SETTINGS);


32、IP设置


Intent intent =  new Intent(Settings.ACTION_WIFI_IP_SETTINGS);


33、App设置详情界面


public void startAppSettingDetail() {
String packageName = getPackageName();
Uri packageURI = Uri.parse("package:" + packageName);
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(packageURI);
startActivity(intent);
}


34、跳转应用市场


public void startMarket() {
Intent intent = new Intent(Intent.ACTION_VIEW);
// intent.setData(Uri.parse("market://details?id=" + "com.xxx.xxx"));
intent.setData(Uri.parse("market://search?q=App Name"));
startActivity(intent);
}


35、获取Launcherbaoming


public void getLauncherPackageName() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
final ResolveInfo res = this.getPackageManager().resolveActivity(intent, 0);
if (res.activityInfo == null) {
Log.e("TAG", "没有获取到");
return;
}

if (res.activityInfo.packageName.equals("android")) {
Log.e("TAG", "有多个Launcher,且未指定默认");
} else {
Log.e("TAG", res.activityInfo.packageName);
}
}


36、跳转图库获取图片


public void startGallery() {
Intent intent = new Intent(Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("image/*");
this.startActivityForResult(intent, 1);
}


37、跳转相机,拍照并保存


public void startCamera() {
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.jpg";
Uri headCacheUri = Uri.fromFile(new File(dir));
Intent takePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, headCacheUri);
startActivityForResult(takePicIntent, 2);
}


38、跳转文件管理器


public void startFileManager() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("file/*");
this.startActivityForResult(intent, 3);
}


39、直接拨打电话


 public void startCall() {
Intent callIntent = new Intent(Intent.ACTION_CALL);
callIntent.setData(Uri.parse("tel:" + "13843894038"));
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
return;
}
startActivity(callIntent);
}


40、跳转电话应用


public void startPhone() {
Intent intent = new Intent(Intent.ACTION_DIAL,Uri.parse("tel:" + "13843894038"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}


41、发送短信


public void startSMS() {
Uri smsToUri = Uri.parse("smsto://10086");
Intent mIntent = new Intent( android.content.Intent.ACTION_SENDTO, smsToUri );
startActivity(mIntent);
}


42、发送彩信


public void startMMS() {
Uri uri = Uri.parse("content://media/external/images/media/11");
Intent it = new Intent(Intent.ACTION_SEND);
it.putExtra("sms_body", "some text");
it.putExtra(Intent.EXTRA_STREAM, uri);
it.setType("image/png");
startActivity(it);
}


43、发送邮件


public void startEmail() {
Uri uri = Uri.parse("mailto:6666666@qq.com");
String[] email = {"12345678@qq.com"};
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra(Intent.EXTRA_CC, email); // 抄送人
intent.putExtra(Intent.EXTRA_SUBJECT, "这是邮件的主题部分"); // 主题
intent.putExtra(Intent.EXTRA_TEXT, "这是邮件的正文部分"); // 正文
startActivity(Intent.createChooser(intent, "请选择邮件类应用"));
}


44、跳转联系人


public void startContact() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Contacts.People.CONTENT_URI);
startActivity(intent);

/*Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setData(Uri.parse("content://contacts/people"));
startActivityForResult(intent, 5);*/

}


45、插入联系人


public void insertContact() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(ContactsContract.Contacts.CONTENT_URI);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, "18688888888");
startActivityForResult(intent, 1);
}


46、插入日历事件


public void startCalender() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(CalendarContract.Events.CONTENT_URI);
intent.putExtra(CalendarContract.Events.TITLE, "开会");
startActivityForResult(intent, 1);
}


47、跳转浏览器


public void startBrowser() {
Uri uri = Uri.parse("http://www.baidu.com");
Intent intent = new Intent(Intent.ACTION_VIEW,uri);
startActivity(intent);
}


48、安装应用


public void startInstall() {
String filePath="/xx/xx/abc.apk";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + filePath),
"application/vnd.android.package-archive");
startActivity(intent);
}>



49、卸载应用


public void startUnInstall() {
String packageName="cn.memedai.mas.debug";
Uri packageUri = Uri.parse("package:"+packageName);//包名,指定该应用
Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageUri);
startActivity(uninstallIntent);
}


50、回到桌面


public void startLauncherHome() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
}


51、打开任意文件(根据其MIME TYPE自动选择打开的应用)


  private void openFile(File f) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
String type = getMIMEType(f);
intent.setDataAndType(Uri.fromFile(f), type);
startActivity(intent);
}

private String getMIMEType(File f) {
String end = f.getName().substring(f.getName().lastIndexOf(".") + 1,
f.getName().length()).toLowerCase();
String type = "";
if (end.equalsIgnoreCase("mp3")
|| end.equalsIgnoreCase("aac")
|| end.equalsIgnoreCase("amr")
|| end.equalsIgnoreCase("mpeg")
|| end.equalsIgnoreCase("mp4")) {
type = "audio";
} else if(end.equalsIgnoreCase("mp4")
|| end.equalsIgnoreCase("3gp")
|| end.equalsIgnoreCase("mpeg4")
|| end.equalsIgnoreCase("3gpp")
|| end.equalsIgnoreCase("3gpp2")
|| end.equalsIgnoreCase("flv")
|| end.equalsIgnoreCase("avi")) {
type = "video";
} else if (end.equalsIgnoreCase("jpg")
|| end.equalsIgnoreCase("gif")
|| end.equalsIgnoreCase("bmp")
|| end.equalsIgnoreCase("png")
|| end.equalsIgnoreCase("jpeg")) {
type = "image";
} else {
type = "*";
}
type += "/*";
return type;
}


52、跳转录音


public void startRecord() {
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
startActivity(intent);
}



👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀



作者:派大星不吃蟹
来源:juejin.cn/post/7321551188092403764
收起阅读 »

检测自己网站是否被嵌套在iframe下并从中跳出

web
iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。 本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。 末尾放了正在使用的完整代码,想直接用的可以拉到最后。 效果 当存...
继续阅读 »

iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。

本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。


末尾放了正在使用的完整代码,想直接用的可以拉到最后。


效果


当存在嵌套时会出现一个蒙版和窗口,提示用户点击。

点击后会在新窗口打开网站页面。


嵌套展示


嵌套检测


设置响应头


响应头中有一个名为X-Frame-Options的键,可以针对嵌套操作做限制。

它有3个可选值:


DENY:拒绝所有


SAMEORIGIN:只允许同源


ALLOW-FROM origin:指定可用的嵌套域名,新浏览器已弃用


后端检测(以PHP为例)


通过获取$_SERVER中的HTTP_REFERERHTTP_SEC_FETCH_DEST值,可以判断是否正在被iframe嵌套


// 如果不是iframe,就为空的字符串
$REFERER_URL = $_SERVER['HTTP_REFERER'];

// 资源类型,如果是iframe引用的,会是iframe
$SEC_FETCH_DEST = $_SERVER['HTTP_SEC_FETCH_DEST'];

// 默认没有被嵌套
$isInIframe = false;

if (isset($_SERVER['HTTP_REFERER'])) {
$refererUrl = parse_url($_SERVER['HTTP_REFERER']);
$refererHost = isset($refererUrl['host']) ? $refererUrl['host'] : '';

if (!empty($refererHost) && $refererHost !== $_SERVER['HTTP_HOST']) {
$isInIframe = true;
}
}

// 这里通过判断$isInIframe是否为真,来处理嵌套和未嵌套执行的动作。
if($isInIframe){
....
}

前端检测(使用JavaScript)


通过比较window.self(当前窗口对象)和window.top(顶层窗口对象)可以判断是否正在被iframe嵌套


if (window.self !== window.top) {
// 检测到嵌套时该干的事
}

从嵌套中跳出


跳出只能是前端处理,如果使用了PHP等后端检测,可以直接返回前端JavaScript代码,或者HTML的A标签设置转跳。


JavaScript直接转跳(不推荐)


不推荐是因为现在大多浏览器为了防止滥用,会阻止自动弹出新窗口。


window.open(window.location.href, '_blank');

A标签点击转跳(较为推荐)


当发生了用户交互事件,浏览器就不会阻止转跳了,所以这是个不错的方法。


href="https://www.9kr.cc" target="_blank">点击进入博客

JavaScript+A标签(最佳方法)


原理是先使用JavaScript检测是否存在嵌套,

如果存在嵌套,再使用JavaScript加载蒙版和A标签,引导用户点击。


这个方法直接查看最后一节。


正在使用的方法


也就是上一节说的JavaScript+A标签。


先给待会要显示的蒙版和A标签窗口设置样式


/* 蒙版样式 */
.overlay1 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景颜色 */
z-index: 9999; /* 确保蒙版位于其他元素之上 */
display: flex;
align-items: center;
justify-content: center;
}

/* 窗口样式 */
.modal1 {
background-color: #fff;
padding: 20px;
border-radius: 5px;
}

然后是检测和加载蒙版+A标签的JavaScript代码


if (window.self !== window.top) {
// 创建蒙版元素
var overlay = document.createElement('div');
overlay.className = 'overlay1';

// 创建窗口元素
var modal = document.createElement('div');
modal.className = 'modal1';

// 创建A标签元素
var link = document.createElement('a');
link.href = 'https://www.9kr.cc';
link.target = '_blank'; // 在新窗口中打开链接
link.innerText = '点击进入博客';
//link.addEventListener('click', function(event) {
// event.preventDefault(); // 阻止默认链接行为
// alert('Test');
//});

// 将A标签添加到窗口元素中
modal.appendChild(link);

// 将窗口元素添加到蒙版元素中
overlay.appendChild(modal);

// 将蒙版元素添加到body中
document.body.appendChild(overlay);
}

博客的话,只需要在主题上设置自定义CSS自定义JavaScript即可


博客后台设置




作者:Edit
来源:juejin.cn/post/7272742720841252901
收起阅读 »

2024年突如其来的危机感和反思总结

前言 说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。 因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会...
继续阅读 »

前言


说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。


因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会被列入 优化名单。


刚刚7天内通宵两个晚上的我,听到这个消息后脑子真的嗡嗡的。因为我本能的认为失败的原因不在我,网络问题结合使用的nextJS插件,导致我们无法线下测试,所以有问题只会在生产上暴露。


就这样通宵后的我三天没有睡好,有一天晚上我梦见我在和领导解释为什么会出现这些问题,但是他们不听。我不是失落,而是害怕。有房贷和孩子没有坚强的家庭支援的人,大概会懂我这几天的无助。


起因


事情是这样的,我负责的一个项目要从公有云迁移到私有云,而私有云中有部分中间要求使用国产化,这些问题都已经解决了之前也简单记录了一下。


但是这个项目最复杂的是网络,有十几二十个防火墙要申请,还有一些白名单要配置,而且我们没有域名,使用的是别人的域名http://www.aaa.cn/path进行转发到我们web代理服务器上。就因为这个/path的原因,我们的前端后端都在代码中做了修改。关键他还不止一个域名,还有一个aaa.cn/path这个地址也可以访问。但该死的http://www.aaa.cn/path一开始只有外网可访问内网访问不了,aaa.cn/path一开始内网都可以访问。然而,今天测试的时候发现这两个https地址都可以访问了,但是我事先没有收到任何通知。这里说一下为什么要https,因为微信必须要求https域名,当然还有一些其他场景。


第一次割接


因为迁移后的环境没有割接前没有域名,更没有https的域名供我们使用测试。我们申请了公网负载IP进行测试一切顺利,于是我开始第一次割接。然后失败了,因为迁移前的obs是自带公网可访问域名的(我们的资源公网客户端可以直接访问),但是迁移后我们的obs是私有云,他们虽然提供了域名但只能内网访问,于是我们使用了nginx做了反向代理。反向代理后使用公网负载IP到访问这个私有云的obs资源是没有问题的,但上了服务器使用域名访问这些资源,就不能访问。因为当晚除了这个域名问题,还有一个程序问题,所以我在凌晨5点放弃了割接,发邮件说明失败原因。


第二次割接


第一次的使用域名无法方私有云obs问题,我领导去修改了nginx代理配置,增加了header头,将host改成了可以正常访问的公网负载IP,然后使用浏览器测试直接打开了私有云obs的图片。另外程序问题是开发忘记刷脚本了,我没有骂他,因为我觉得我骂了影响后面的工作。外包是一个团队,因为他工作的原因导致其他人无效通宵,其他人会给他压力的。当然,提还是要提的。


解决上述两个问题后,我准备了第二次割接,然后还是失败了。原因nextJS打包是需要访问后端服务器,同时nextJS中有个图片模糊加载的插件访问图片的域名和打包需要访问后端服务器域名是同一个,共用同一个参数配置。而不巧的是我们部署打包的服务器无法访问http://www.aaa.cn这个域名,而aaa.cn虽然可以访问,但是他的证书不安全nextJS的模糊加载插件直接提示安全问题,不予与加载展示。


我们蹭着线上域名割接后,做了几轮测试得出一下结论。


方案一:http://www.aaa.cn需要打包服务器能访问,运维说配置hosts就可以,但这个要提工单,无法直接协调;


方案二:aaa.cn配置上SSL安全证书,使其https合法;


方案三:如果方案一和方案二尝试后都不行,在http://www.aaa.cn打包服务器可访问的情况下或aaa.cn配置安全证书的情况,去掉nextJS的模糊图片加载问题;


再次放弃割接计划,发邮件说明原因。




然后6点睡,10点起,和领导沟通问题的时候,领导说了上面的话。我给领导回复是,主要还是网络太复杂了,但是我会尽全力的,结果怎么样我也没得选,听天命吧。


过程


领导和我聊完后,我的心情是不能平复的。


我想的最多的是,如果我失业了,我那每月1.3w的房贷怎么办?


每月的家庭支出怎么办?


我老婆一个人能不能扛得住?


现在这个环境我能快速找到工作吗?


就算找到了,我能找到心仪的工作吗?


找到新工作后,我能不能待多久?


我现在是不是该去复习一些技术了?


我应该先学哪些东西呢?


我是不是应该找个副业?


搞短视频?写小说?滴滴?外卖?


自己做几个益智的微信小程序游戏,然后靠广告赚点饭钱?


回老家问问我爷爷或者我父辈的那些山和地是否能给我种果树或者粮食?


...


第二天是个周六,我开始冷静了一点。我开始拿起手机看着一串延期的计划表发呆,我完全提不起一点兴趣,也许自己不行去做的一种借口吧。但结果是我真的没有去做,因为我不想做。


看着计划,我越看越不对劲。


第三天是个周日,快到晚上的时候,我老婆问我吃完饭不。我说不吃了,刚好适应一下失业后饿肚子的感觉,以后说不准要经常饿肚子。


第四天早上,起来把掘金、华住、学习强国签到完,学了一节多领国,然后就去完成运动计划1000跳绳+10组其他健身运动。运动完后去洗澡,然后就萌生了鼓励自己的念头。


“想想这两次失败是否完全不可测试的?”


“还有哪些我能做的?”


“领导只是说我有危险,那何不在努力试试留下来,毕竟你自己希望能在这里呆满3年+的!”


“第二次割接的问题是不是可以通过自己购买域名模拟?”


“做自己该做的,船到桥头自然直,况且你一直觉得自己能力还可以,至少是中等水品?”


反思


反思第一次失败


1、虽然自己整理了checklist清单,让项目确认了他们也确认了,但自己并没有让他们把每个环节需要执行细节落入书面;


2、自己在整个上线过程中,确实没有针对具体问题做深度的剖析,只是站在方向的引导上,过度依赖团队中的开发;


3、网络知识和nginx虽然一直在用,但自己不熟悉却没有放到学习项中,自己一直在学习其他玩意,重要紧急没有分清楚;


4、出现问题,具体的问题没有自己剖析过,觉得是网络问题自己肯定不会;


反思第二次失败


1、和第一次一样,没有亲自分析问题日志和原因,基本都是团队反馈,然后自己总结的归纳;


2、没有深思熟虑,既然上次有域名访问图片的问题,但却没有考虑https的问题和nextJS打包需要访问后端的问题;


反思个人计划


1、强化工作的部分有,但太少需要针对性增加学习工作中遇到的薄弱的技术问题;


2、整个计划中,基本除了健康就是学习,没有增加实施后可以增加收入或者增加收入机会的内容,即使列了也没有执行到位;


3、计划中应该有侧重,计划中内容太多时间太分散,应该每个阶段增加一个侧重;


调整


关于本次迁移的工作的总结:


1、上线前整理checklist,并且核对每个人负责的内容,包括细节操作和操作所需材料,并收集材料;


2、以前是团队负责人,现在是技术经理,需要下沉,表现在现场分析解决问题和增加技术知识面;


3、增对工作汇总遇到的薄弱技术知识点,针对性的寻找资料学习;


4、遇到问题,冲在一线,现在是技术经理需要关系技术细节,并且需要从细节上帮助团队解决问题;


5、没有解决不了的问题,没有复现不了的环境,无法是成本问题,不要一分不掏,因为没了工作损失的不止这点钱;


关于自身工作状态的总结:


1、这家公司自从自己将责任划分清楚后,开始有点安逸,但所有需求自己要过一遍,每个技术方案自己要把持;


2、还是要以工作为主,有一半的学习要和当下的工作相关;


3、不要过分信任团队,特别是外包团队,要将核心掌握在自己手里;


4、防御性上班,关键核心的要素信息要记笔记,但点到为止自己明白就行,不然对你下黑手时,你无力反抗和无法维护自己的权益;


结合上述总结调整2024年执行计划:


原计划


一、工作:
1)2024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;

二、学习:
1)每天保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)学习英语,多领国每天只是少一节,时间多可以多练习几个,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)通过五月份的软考高项,去年上半年没有过,下半年放弃了,每天背知识点、练习和看教学视频;

三、健康:
1)每天保持运动,常规每天1000个跳绳+10组其他运动,如俯卧撑,最次每天200个跳绳,争取将结石排除提完;
2)控制饮食,多吃粗纤维果蔬少油少盐,争取大多时候半碗饭和两素一荤,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)体重减到170以下,除了坚持以上两项,多出去走走;
4)排出肾结石,中度脂肪肝转轻度或无,降血液中的胆固醇,治好咽喉炎和鼻窦炎,以上四样至少完成两项;
5)平均睡眠提升到6小时+;
6)作为兴趣学学中医,看看倪海厦的中医视频,聊胜于无;
四、创作:
1)持续创作短视频或者小说,小说24年争取实现100w字,短视频每周一篇,不做硬性要求业余时间够就走;

以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

分解原计划


一、工作:
12024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;
1.运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划 -- 提高领导力的影响,专家权利;
2.对所有新增需求进行阅读,参与并制定需求所使用的技术方案 -- 掌握项目技术栈和架构变化,增加项目经验和能力;
3.对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
4.不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项;

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下;
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

新计划


因为之前的计划使用iphone自带的提醒事项做的,但是这东西在统计上手机和电脑不同步,而且手机电脑一起用还会重复计数。因此准备自己搞个计划清单列表小程序,至于app后续再研究,使用微信消息推送。


一、工作:
1)运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划;
-- 本月每天2小时,将程序打包编译先搞定,独立完成UAT环境的部署和安装(侧重);
2)对所有新增需求进行阅读,参与并制定需求所使用的技术方案;
-- 有就阅读,并分析需求中是否需要使用新的技术方案;
3)对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
-- 每2周学习nginx一个功能点,整理成技术文章;
4)不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;
-- 一句项目情况汇报;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项(侧重);

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下(侧重);
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;
33月底前,开发一个小程序用于记录计划清单,并使用微信提醒,后续看情况加上短信提醒(侧重);
4)模仿一个微信小游戏,

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

总结回顾


这些年我做了很多选择,但是我并没有因为我的选择变得更好。早先时候我一路走上坡的时候,我确实觉得是因为自己能力变强了我才有这样的成就,我也很自信我确实有这样的能力。但最近这4年一路下坡,让我重新认识了自己。早期我的能力可能确实在中上游,加上环境好很容易上去,而最终无论什么原因自己下来了说明自己总归有些问题的。


什么问题?自己认为比较严重的问题有如下:


1、过早且长期脱离一线,虽然有心想要重回一线,但是内心是抗拒那种艰苦的日子,虽然我不会把所有功绩揽给自己,但确实沾沾自喜;这就导致很多技术上的问题,我虽然了解但浮于表面,带着团队能解决,自己不一定能解决,最多只有思路。


2、没有认清打工人的本质,我曾在几家高端职位的公司任职,因为觉得高层领导或者直属领导太煞笔、不听劝、独断专行,而愤然离职;说到底还是太年轻,打工人就和上钟的技师一样,你要让领导爽,然后才能谈条件;他的煞笔不应该由你自己来买单,当然也和个人性格有关,城府和隐忍在职场上相当重要。


3、方向问题,我虽然做了11年,我之前的求职一直是以工资和职位头衔为目标,我基本没有规划过我的职业领域方向;等到要进入高端职位的圈子时,发现自己竟然什么都会一些,但别人要的是某个领域至少5年以上的工作经验,而我其中一个领域最多只有3.5年。


4、重心和当前迫切的问题自己没有刻意的把我,就比如很多计划看着挺好,但做起来也挺好,但是没有沉淀或者和当前的工作没有关系,就这样失去了很多巩固和提升能力的机会。


5、心里一直想要给自己留条后路,却发现前路没有走好,后路也没有留上,终日惶惶不安日。


有时候我在想,每一次的成功是不是老天给我的机会或者上辈子积德所致,每一次的失败或者落魄是不是老天觉得我朽木不可雕也。


但实际上自己也知道问题在哪?


不想做一线工作 -- 懒;


没有城府和隐忍 -- 蠢;


没有规划和防线 -- 笨;


没有重心和侧重 -- 懒;


前路没好后路成 -- 贪;


虽然明知道自己有这么多缺点,但是我还是想扛着氧气罐自救一下,说不定哪天让我踩上了风口飞起来了呢?放下氧气罐,也许我再也起不来了,但扛着虽然累,好歹我还活着。


-- 来自于35岁的自白!


作者:暗黑腐竹
来源:juejin.cn/post/7321531849850945570
收起阅读 »

一个 Kotlin 开发,对于纯函数的思考

什么是纯函数? 纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用 在数学上函数的定义为 It must work for every possible input value And it has only one ...
继续阅读 »

什么是纯函数?


纯函数是数学上的一种理想状态,即相同的输入永远会得到相同的输出,而且没有任何可观察的副作用


在数学上函数的定义为



  • It must work for every possible input value

  • And it has only one relationship for each input value



即每个在值域内的输入都能得到唯一的输出,它只可能是多对一而不是一对多的关系:



副作用



Wikipedia Side Effect: In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, which is to say if it has any observable effect other than its primary effect of returning a value to the invoker of the operation. Example side effects include modifying a non-local variable, modifying a static local variable, modifying a mutable argument passed by reference, performing I/O or calling other functions with side-effects. In the presence of side effects, a program's behaviour may depend on history; that is, the order of evaluation matters. Understanding and debugging a function with side effects requires knowledge about the context and its possible histories.



副作用的形式很多样,一切影响到外部状态、或依赖于外部状态的行为都可以称为副作用。副作用是必须的,因为程序总是不可避免的要与外界交互,如:


更改外部文件、数据库读写、用户 UI 交互、访问其他具有副作用的函数、修改外部变量


这些都可以被视为副作用,而在函数式编程中我们往往希望使副作用最小化,尽量避免副作用,对应的纯函数则是希望彻底消除副作用,因为副作用让纯函数变得不“纯”,只要一个函数还需要依赖外部状态,那么这个函数就无法始终保持同样的输入得到同样的输出。


好处是什么?



You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. — Joe Armstrong, creator of Erlang progamming




  • 可缓存性,由于唯一的输入代表唯一的输出,那么这意味着我们可以在输入不变的情况下直接返回运算过的结果。

  • 高度并行,由于纯函数不依赖外部状态,因此即便在多线程情况下外部怎么变动,纯函数始终能够返回预期的值,纯函数能够达到真正的无锁编程,高度并发。

  • 高度可测性,不需要依赖外部状态,传统的 OOP 测试我们都需要模拟一个真实的环境,比如在 Android 中将 Application 模拟出来,在执行完之后断言状态的改变。而纯函数只需要模拟输入,断言输入,这是如此的简单优雅。

  • 依赖清晰,面相对象编程总需要你将整个环境初始化出来,然后函数再依赖这些状态修改状态,函数往往伴随着大量外部的隐式依赖,而纯函数只依赖输入参数,仅此而已,也仅提供返回值。


更进一步


传统大学老师教的都是 OOP,所以大多数人最开始也不会去学习纯函数的思路,但纯函数是完全不一样的一套编程思路,下面是一个纯函数中实现循环的例子,传统的循环往往是这样的:


int sum(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}

尽管大多数语言也会提供 for...in 之类的语法,如 kotlin:


fun sum(array: IntArray): Int {
var sum = 0
for (i in array) {
sum += i
}
return sum
}

但我们注意到,在上面的例子中均引入了两个可变的变量,sum 和 i,站在 sum += i 的视角,它的外部依赖:i 是一个外部的可变状态,因此这个函数并不“纯”。


但另一方面来说,从整体函数对外的视角来看,其还是很“纯”的,因为对于传入的外部 array,始终有唯一的 int 返回值,那么我们追求完全的“纯”,完全不使用可变量的目的是什么呢?


在纯函数下要实现完全消灭不可变变量,我们可以这么做:


tailrec fun sum(array: IntArray, current: Int = 0, index: Int = 0): Int {
if (index < 0 || index >= array.size) return current
return sum(array, current + array[index], index + 1)
}

我们编写了退出条件,当 index 在不正常的情况下会,意味着没有东西可以加,直接返回 current,即当前已经算好的值;其余情况则直接返回 current 与当前 index 的值的和,再加上 index + 1 之后所有值的 sum。这个例子已经很简单了,但函数式,递归思维不免会让学传统 OOP 的人需要多加思考一下。


当然作为一个 kotlin 开发,我也毫不犹豫的使用了 tailrec 这个 kotlin 语言特性来帮助优化尾递归,否则在遇到相当长的列表的时候,这个函数会抛出 StackOverFlowError。


函数一等公民


许多面向对象语言通常会用 this 显式访问对象的属性或方法,也有一些语言会省掉编写 this,事实上在许多语言编译器的背后实现中,通常也会将“对象成员的调用”变成“额外给成员函数添加一个 this 变量”。


可见发挥重要作用的其实是函数,不如更进一步,函数是一等公民,对象只不过是个结构体;如此,在纯函数中你完全用不到 this,甚至很多情况下都用不到对象。


所谓的一等公民,就是希望函数包含对象,而不是对象包含函数,甚至可以不需要对象(暴论),下面就是一个例子,是一个常见的业务诉求:



  • UserService 接收用户 id,并提供两个函数来获取用户 token 和用户的本地储存

  • ServerService 需要服务器 ip 和 port,提供通过 secret 获取 token 和通过用户 token 获取用户数据两个能力

  • UserData 是一个用户数据类,它能够接收父布局参数来构建 UI 数据用于显示


class UserService(private  val id: Int) {
fun userToken(password: String): UserToken = TODO()
fun localDb(dbPassword: String): LocalDb = TODO()
}

class ServerService(private val ip: String, private val port: Int) {
fun serverToken(serverSecret: String): ServerToken = TODO()
fun getUser(userToken: UserToken): UserData = TODO()
}

class UserData(
val name: String, val avatarUrl: String, val description: String,
) {
fun uiData(parentLayoutParameters: LayoutParameters): UIData = TODO()
}

那么这些变成函数式会怎么样呢?会像下面这样!


typealias UserTokenService = (password: String) -> UserToken
typealias LocalDbService = (dbPassword: String) -> LocalDb

typealias UserService = (id: Int) -> Pair<UserTokenService, LocalDbService>

typealias ServerTokenService = (serverSecret: String) -> ServerToken
typealias ServerUserService = (userToken: UserToken) -> UserDataAbilities
typealias ServerService = (ip: String, port: Int) -> Pair<ServerTokenService, ServerUserService>

typealias UserUIData = (parentLayoutParameters: LayoutParameters) -> UIData
typealias UserDataAbilities = UserUIData

val userService: UserService = { userId: Int ->
val tokenService: UserTokenService = { password: String -> TODO() }
val localDbService: LocalDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService: ServerService = { ip: String, port: Int ->
val tokenService: ServerTokenService = { serverSecret: String -> TODO() }
val userService: ServerUserService = { userToken: String -> TODO() }
tokenService to userService
}

是不是看起来这些东西变得相当的复杂?但实际上真正的代码并没有写多少行,大量的代码都用来定义类型了!这也就是为什么你能看到的大多数展示函数式的例子都是用 js 去实现的,因为 js 的类型系统很弱,这样函数式写起来会很方便。


我这里用 kt 的范例则是写了大量的类型标记代码,因为我本人对显式声明类型有极高的要求,如果愿意,也可以完全将类型隐藏全靠编译器推理,就像下面这样,一切都变得简洁了,写起来和常规的 OOP 并没有太大区别。


val userService = { userId: Int ->
val tokenService = { password: String -> TODO() }
val localDbService = { dbPassword: String -> TODO() }
tokenService to localDbService
}

val serverService = { ip: String, port: Int ->
val tokenService = { serverSecret: String -> TODO() }
val userService = { userToken: String -> TODO() }
tokenService to userService
}

但不同的是,你看上面的代码,完全没有类/结构体的存在,因为变量的存储全部在函数体内储存了!


对于使用处,两种方式的用法事实上也大同小异,但可以看到我们彻底抛弃了类的存在!甚至在 kotlin 的未来版本中,如果这种代码始终在字节码中编译成 invokeDynamic,那么通过这种方式,字节码中甚至都可以避免类的存在!(当然,在 Android DEX 中会被脱糖成静态内部类)


// OOP
val userService = UserService(id = 0)
val serverService = ServerService(ip = "0.0.0.0", port = 114514)
val userToken = userService.userToken(password = "undefined")
val userData = serverService.getUser(userToken)
val uiData = userData.uiData(parentLayoutParameters)

// functional
val (userTokenService, _) = userService(0)
val (_, userDataService) = serverService("0.0.0.0", 114514)
val userToken = userTokenService("undefined")
val userData = userDataService(userToken)
val uiData = userData(parentLayoutParameters)


BTW,这里函数式 argument 没加 name 主要 kt 现在不支持。。。




柯里化


在上面的例子中,其实我们也能看到,类的存在是不必须的,类的本质其实只是预设好了一部分参数的函数,柯里化要解决的问题就是如何更轻松的实现“预设一部分参数”这样的能力。将一个函数柯里化后,允许多参数函数通过多次来进行传入,如 foo(a, b, c, d, e) 能够变成 foo(a, b)(c)(d, e) 这样的连续函数调用


在下面的例子中,我将举一个计算重量的范例:


fun weight(t: Int, kg: Int, g: Int): Int {
return t * 1000_000 + kg * 1000 + g
}

将其柯里化之后:


val weight = { t: Int ->
{ kg: Int ->
{ g: Int ->
t * 1000_000 + kg * 1000 + g
}
}
}

使用处:


// origin
weight(2, 3, 4)
// currying
weight(2)(3)(4)

在这里我们能发现,柯里化其实让实现处变复杂了,不过在 js 中通常会通过 bind 来实现,kt 也有民间大神 github 开源的柯里化库,使用这些能够从一定程度上降低编写柯里化代码的复杂度。


让我们看看 skiplang 语言吧


skiplang.com/


Skiplang 的宗旨就在其网站主页,A programming language to skip the things you have already computed,在纯函数的情况下,意味着得知输入状态,那么输出状态就是唯一确定的,这种情况就非常适合做缓存,如果输入值已经计算过,那么直接可以返回缓存的输出值。


在纯函数的情况下,意味着运算可以做到高度并行,在 skiplang 中,多个异步线程之间不允许共享可变变量,自然也不会出现异步锁等东西,从而保证了异步的绝对安全。


个人思考


纯函数的收益非常诱人,但开发者往往不喜欢使用纯函数,一些常见的原因可能是:



  1. 对性能的担忧:纯函数不允许修改变量,只允许通过 copy 等方式,创建了大量的变量;编译器需要进行激进的尾递归优化。

  2. 开发者意识淡薄:大多数学校出身的开发者只会用老师教的那一套 OOP,想培养 OOP 向函数式的转变,通常会让很多开发者感到困难,从而认为传统 OOP 简单,也是主流,没必要学新的。


尽管我对纯函数也非常的心动,但是我不是激进的纯函数派,我在日常工作中对其部分认同,具体到 kotlin 编程中,我通常坚持的理念是:



  1. 可以使用类,也可以在类中定义函数,但不允许使用可变成员。

  2. 可以使用可变的 local variable(但不推荐),但不允许在多线程之间共享

  3. 同种副作用,单一数据源。


参考



个人主页原文:一个 Kotlin 开发,对于纯函数的思考


作者:zsqw123
来源:juejin.cn/post/7321049383571046409
收起阅读 »

原来小程序分包那么简单!

web
前言 没有理论,只有实操,用最直接的方式来了解和使用小程序分包。 文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。 为什么要有小程序分包? 因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖...
继续阅读 »

前言


没有理论,只有实操,用最直接的方式来了解和使用小程序分包。


文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。


为什么要有小程序分包?


因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖或者一下静态图片,代码包难免超过2M。所以需要小程序分包功能将小程序中所有的代码分别打到不同的代码包里去,避免小程序只能上传2M的限制


目前小程序分包大小有以下限制:



  • 整个小程序所有分包大小不超过 20M(开通虚拟支付后的小游戏不超过30M)

  • 单个分包/主包大小不能超过 2M


如何对小程序进行分包?


本质上就是,配置一下app.json(小程序)或app.config.ts(Taro)中的subpackages字段。注意,分包的这个root路径和原本的pages是同级的。


如下图


image.png


这样配置好了,最基本的分包就完成了。


如何配置多个子包?


subpackages是个数组,在下面加上一样的结构就好了。


image.png
image.png


如何判断分包是否已经生效?


打开微信开发者工具,点击右上角详情 => 基本信息 => 本地代码,展开它。出现 主包,/xxxx/就是分包生效了。


如下图


image.png


所有页面都可以打到分包里面吗?


也不是,小程序规定,Tabbar页面不可以,一定需要在主包里。否则他直接报错。


分包中的依赖资源如何分配?


我们先来了解一下小程序分包资源


 引用原则
`packageA` 无法 require `packageB` JS 文件,但可以 require 主包、`packageA` 内的 JS 文件;使用 [分包异步化](https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/async.html) 时不受此条限制
`packageA` 无法 import `packageB` 的 template,但可以 require 主包、`packageA` 内的 template
`packageA` 无法使用 `packageB` 的资源,但可以使用主包、`packageA` 内的资源

原因: 分包是依赖主包运行的,所以主包是必然会被加载的,所以当分包引用主包的时候,主包的相关数据已经存在了,所以可以被引用。而分包不能引用其他分包的数据,也是因为加载顺序的问题。如果分包A引用分包B的数据,但分包B尚未被加载,则会出现引用不到数据的问题。


如果主包和分包同时使用了一个依赖,那么这个依赖会被打到哪里去?


会被打到主包


因为主包不能引用分包的资源,但是子包可以引用主包的资源,所以为了两个包都能引用到资源,只能打到主包中


比如以下情况
image.png


分包和主包同时使用了dayjs,那么这个依赖会被打入到主包中。


如果某一个依赖只在分包中使用呢?


如果某一个资源只在某一个分包中使用,那就会被打入到当前分包。


如果两个子包同时使用同一个资源呢?那资源会被打进哪里。


主包,因为两个子包的资源不能互相引用,所以与其给每一个子包都打入一个独立资源。小程序则会直接把资源打到主包中,这样,两个子包就都可以使用了。


分包需要担心低版本的兼容问题吗


不用


由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。 新客户端用分包,老客户端还是用的整包,完整包会把各个 subpackage 里面的路径放到 pages 中。


独立分包


什么是独立分包


顾名思义,独立分包就是可以独立运行的分包。


举个例子,如果你的小程序启动页面是分包(普通分包)中的一个页面,那么小程序需要优先下载主包,然后再加载普通分包,因为普通分包依赖主包运行。但是如果小程序从独立分包进入进入小程序,则不需要下载主包,独立分包自己就可以运行。


普通分包所有的限制对独立分包都有效。


为什么要有独立分包,普通分包不够吗


因为独立分包不需要依赖主包,如果有作为打开小程序的的入口的必要,加载速度会比普通分包快,给客户的体验感更好。毕竟谁也不想打开一个页面等半天。


举个例子,如果小程序启动的时候是打开一个普通分包页面。则加载顺序是:加载主包 => 再加载当前分包


但如果小程序启动的时候是打开一个独立分包页面,则加载顺序是:直接加载独立分包,无需加载主包


独立分包相对于普通分包,就是省去了加载主包的时间和消耗。


独立分包如何配置


配置和普通分包一样,加一个independent属性设为true即可。


image.png


独立分包的缺点


既然独立分包可以不依赖主包,那我把每个分包都打成独立分包可以吗。


最好别那么干


理由有四点


1.独立分包因为不依赖主包,所以他不一定能获取到小程序层面的全局状态,比如getApp().也不是完全获取不到,主包被加载的时候还是可以获取到的。概率性出问题,最好别用。


2.独立分包不支持使用插件


3.小程序的公共文件不适用独立分包。比如Taro的app.less或小程序的app.wxss


上述三个,我觉的都挺麻烦的。所以不是作为入口包这种必要的情况下,确实没有使用独立分包的需求。


PS:一个小程序里可以有多个独立分包


独立分包有版本兼容问题吗


有滴,但你不用这个兼容问题直接让你报错
在低于 6.7.2 版本的微信中运行时,独立分包视为普通分包处理,不具备独立运行的特性。


所以,即使在低版本的微信中,也只是会编译成普通分包而已。


注意!!! 这里有一个可能会遇到的,就是如果你在独立分包中使用了app.wxss或者app.less这些小程序层面的公共css文件,那么在低版本(<6.7.2)进行兼容的时候,你就会发现,独立分包的页面会被这些全局的CSS影响。因为那时候独立分包被编译成了普通分包。而普通分包是适用全局公共文件的。


分包预下载


首先我们需要了解,分包是基本功能是,在下程序打包的时候不去加载分包,然后在进入当前分包页面的时候才开始下载分包。一方面目的是为了加快小程序的响应速度。另一方面的原因是避开微信小程序本身只能上传2M的限制。


这里有一个问题,就是我在首次跳转某个分包的某个页面的时候,出现短暂的白屏怎么办?(下载分包的时间+运行接口的时间+渲染视图的时间)。


后两者没法彻底避免,只能优化代码,第一个下载分包的时间可以使用分包预下载功能解决。


我们可以通过分包预下载在进入分包页面之前就开始下载分包,来减少进入分包页面的时间。


如何配置分包预下载


当前的分包预下载只能在app.config(Taro)或者app.json(原生小程序)通过preloadRule字段去配置。


preloadRule字段是一个对象,key是页面的路径,value是进行预加载的分包name或者key,__APP__代表主包


上案例


image.png


通过preloadRule字段去配置


”packageB/pages/user/index“是key


packages:["packageA"]是value


案例上的意思是当进入packageA分包的时候,开始下载分包packageB


如果要某一个分包在加载主包的就开始下载,那么就设置packages:["APP"]即可。


总结



  1. 分包是为了解决小程序超过2m无法上传的问题

  2. 分包依赖于主包,进入分包页面,主包必然需要优先被加在

  3. 主包和分包同时引用一个依赖或资源,则当前依赖或资源会被打入到主包

  4. 两个分包使用了同一个依赖或资源,则该依赖和资源会被打入到主包

  5. 某资源或依赖只在某一个分包中使用,则该资源和依赖会被打入到该分包中

  6. 独立分包的配置相对于普通分包只是多了一个independent字段,设置为true

  7. 独立分包无需依赖主包,可独立加载。

  8. 独立分包中谨慎使用全局属性,最好别用,可能获取不到

  9. 分包可以被预加载,用于解决进入分包页面时才开始加载分包导致页面可能出现的(取决于加载速度)短暂白屏的问题。


分包官方文档


分包官方分包demo-小程序版


如果您认为对您有用的话,留个赞或收藏一下吧~


image.png


作者:工边页字
来源:juejin.cn/post/7321049399281958922
收起阅读 »

现代 CSS 解决方案:文字颜色自动适配背景色!

web
在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色。简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B。...
继续阅读 »

在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色

简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。

其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B

其功能能够涵盖:

完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

当然,今天我们不会一个一个去过这些功能,更多的时候,我们只需要知道我们能够实现这些功能。

本文,我们将从实际实用角度出发,基于实际的案例,看看 CSS 相对颜色,能够如何解决我们的一些实际问题。

快速语法入门

首先,我们通过一张图,一个案例,快速入门 CSS 相对颜色语法:

相对颜色语法的目标是允许从另一种颜色派生颜色。

上图显示了将原始颜色 green 转换为新颜色的颜色空间后,该颜色会转换为以 r、g、b 和 alpha 变量表示的各个数字,这些数字随后会直接用作新的 rgb() 颜色的值。

举个例子:

<p> CSS Relative Color p>
p {
color: rgb(255, 0, 0);
}

实现一个 color 为红色(rgb 值为 rgb(255, 0, 0))的字体:

基于上面的相对颜色语法,我如何通过一个红色生成绿色文字呢?示意如下:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}

效果如下,我们就得到绿色字体:

解释一下:

  1. 原本的红色颜色,我们把它设置为 CSS 变量 --color: rgb(255, 0, 0)
  2. 想通过红色得到绿色,对于红色的 rgb 值 rgb(255, 0, 0) 而言,需要转换成 rgb(0, 255, 0)
  3. 使用 CSS 相对颜色语法,就是 rgb(from var(--color) calc(r - 255) calc(g + 255) b)

通过这个 DEMO,我们把几个核心基础语法点学习一下:

  1. from 关键字

from 关键字,它是相对颜色的核心。它表示会将 from 关键字后的颜色定义转换为相对颜色!在 from 关键字后面,CSS 会期待一种颜色,即能够启发生成另一种颜色

  1. from 关键字 后的颜色表示,支持不同颜色表示或者是 CSS 变量

第二个关键点,from 后面通常会接一个颜色值,这个颜色值可以是任意颜色表示法,或者是一个 CSS 变量,下面的写法都是合法的:

p {
color: rgba(from #ff0000) r g b);
color: rgb(from rgb(255, 0, 0) r g b);
color: rgb(from hsl(0deg, 100%, 50%) r g b);
color: rgb(from var(--hotpink) r g b);
}
  1. 对转换后的变量使用 calc() 或其他 CSS 函数

另外一个非常重要的基础概念就是,我们可以对 (from color r g b) 后的转换变量 r g b 使用 calc() 或其他 CSS 函数。

就是我们上面的例子:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}
  1. 相对颜色语法支持,各种颜色表示函数:

相对颜色的基础的使用规则就是这样,它不仅支持 rgb 颜色表示法,它支持所有的颜色表示法:

使用 CSS 相对颜色,实现统一按钮点击背景切换

通常页面上的按钮,都会有 hover/active 的颜色变化,以增强与用户的交互。

像是这样:

最常见的写法,就是我们需要在 Normal 状态、Hover 状态、Active 状态下写 3 种颜色:

p {
color: #ffcc00;
transition: .3s all;
}
/* Hover 伪类下为 B 颜色 */
p:hover {
color: #ffd21f;
}
/** Active 伪类下为 C 颜色 **/
p:active {
color: #ab8a05;
}

在之前,我们介绍过一种利用滤镜 filter: contrast() 或者 filter: brightness() 的统一解决方案,无需写多个颜色值,可以根据 Normal 状态下的色值,通过滤镜统一实现更亮、或者更暗的伪类颜色。

在今天,我们也可以利用 CSS 相对颜色来做这个事情:

div {
--bg: #fc0;
background: var(--bg);
transition: .3s all;
}

div:hover {
background: hsl(from var(--bg) h s calc(l * 1.2));
}
div:active {
background: hsl(from var(--bg) h s calc(l * 0.8));
}

我们通过 hsl 色相、饱和度、亮度颜色表示法表示颜色。实现:

  1. 在 :hover 状态下,根据背景色,将背景亮度 l 调整为原背景色的 1.2 倍
  2. 在 :avtive 状态下,根据背景色,将背景亮度 l 调整为原背景色的 0.8 倍

在实际业务中,这是一个非常有用的用法。

完整的 DEMO,你可以戳这里:CodePen Demo -- https://codepen.io/Chokcoco/pen/KKEdOeb

使用 CSS 相对颜色,实现文字颜色自适应背景

相对颜色,还有一个非常有意思的场景 -- 让文字颜色能够自适应背景颜色进行展示。

有这么一种场景,有的时候,无法确定文案的背景颜色的最终表现值(因为背景颜色的值可能是后台配置,通过接口传给前端),但是,我们又需要能够让文字在任何背景颜色下都正常展现(譬如当底色为黑色时文字应该是白色,当背景为白色时,文字应该为黑色)。

像是这样:

在不确定背景颜色的情况下,无论什么情况,文字颜色都能够适配背景的颜色。

在之前,纯 CSS 没有特别好的方案,可以利用 mix-blend-mode: difference 进行一定程度的适配:

div {
// 不确定的背景色
}
p {
color: #fff;
mix-blend-mode: difference;
}

实操过这个方案的同学都会知道,在一定情况下,前景文字颜色还是会有一点瑕疵。并且,混合模式这个方案最大的问题是会影响清晰度

有了 CSS 相对颜色后,我们有了更多的纯 CSS 方案。

利用 CSS 相对颜色,反转颜色

我们可以利用相对颜色的能力,基于背景色颜色进行反转,赋值给 color。

一种方法是将颜色转换为 RGB,然后从 1 中减去每个通道的值。

代码非常简单:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: rgb(from var(--bg) calc(1 - r) calc(1 - g) calc(1 - b)); /** 基于背景反转颜色 **/
}

用 1 去减,而不是用 255 去,是因为此刻,会将 rgb() 表示法中的 0~255 映射到 0~1

效果如下:

配个动图,我们利用背景色的反色当 Color 颜色,适配所有背景情况:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

当然,这个方案还有两个问题:

  1. 如果颜色恰好是在 #808080 灰色附近,它的反色,其实还是它自己!会导致在灰色背景下,前景文字不可见;
  2. 绝大部分情况虽然可以正常展示,但是并不是非常美观好看

为了解决这两个问题,CSS 颜色规范在 CSS Color Module Level 6 又推出了一个新的规范 -- color-contrast()

利用 color-contrast(),选择高对比度颜色

color-contrast() 函数标记接收一个 color 值,并将其与其他的 color 值比较,从列表中选择最高对比度的颜色。

利用这个 CSS 颜色函数,可以完美的解决上述的问题。

我们只需要提供 #fff 白色和 #000 黑色两种可选颜色,将这两种颜色和提供的背景色进行比较,系统会自动选取对比度更高的颜色。

改造一下,上面的代码,它就变成了:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: color-contrast(var(--bg) vs #fff, #000); /** 基于背景色,自动选择对比度更高的颜色 **/
}

这样,上面的 DEMO 最终效果就变成了:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

此方案的优势在于:

  1. 可以限定前景 color 颜色为固定的几个色值,以保证 UI 层面的统一及美观
  2. 满足任何情况下的背景色

当然,唯一限制这个方案的最大问题在于,当前,color-contrast 还只是一个实验室功能,未大规模被兼容。

总结一下

到今天,我们可以利用 CSS 提供的各类颜色函数,对颜色有了更为强大的掌控力。

很多交互效果,不借助 JavaScript 的运算,也能计算出我们想要的最终颜色值。本文简单的借助:

  1. 使用 CSS 相对颜色,实现统一按钮点击背景切换
  2. 使用 CSS 相对颜色,实现文字颜色自适应背景

两个案例,介绍了 CSS 相对颜色的功能。但它其实还有更为广阔的应用场景,完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:Chokcoco
来源:juejin.cn/post/7321410822789742618
收起阅读 »

产品经理:“一个简单的复制功能也能写出bug?”

web
问题 刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。 我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在...
继续阅读 »

问题


刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。


我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在 hook 里没啥意义,但是乙方交付过来的代码好像特别喜欢把工具函数写成个 hook 来用),点进去查看就是简单的一个 navigator.clipboard.writeText()的方法,本地运行我又能复制成功。于是我怀疑是手机浏览器不支持这个 api 便去搜索了一下。


Clipboard


MDN 上的解释:


剪贴板 Clipboard APINavigator 接口添加了只读属性 clipboard,该属性返回一个可以读写剪切板内容的 Clipboard 对象。在 Web 应用中,剪切板 API 可用于实现剪切、复制、粘贴的功能。


只有在用户事先授予网站或应用对剪切板的访问许可之后,才能使用异步剪切板读写方法。许可操作必须通过取得权限 Permissions API (en-US)"clipboard-read" 和/或 "clipboard-write" 项获得。


浏览器兼容性


image.png


使用 document.execCommand() 降级处理


这里我也不清楚用户手机浏览器的版本是多少,那么这个 api 出现之前,是用的什么方法呢?总是可以 polyfill 降级处理的吧!于是我就查到了document.execCommand()这个方法:



  • document.execCommand("copy") : 复制;

  • document.execCommand("cut") : 剪切;

  • document.execCommand("paste") : 粘贴。


对比


Clipboard 的所有方法都是异步的,返回 Promise 对象,复制较大数据时不会造成页面卡顿。但是其支持的浏览器版本较新,且只允许 https 和 localhost 这些安全网络环境可以使用,限制较多。


document.execCommand() 限制较少,使用起来相对麻烦。但是 MDN 上提到该 api 已经废弃:


image.png


image.png


浏览器很可能在某个版本弃用该 api ,不过当前 2023/12/29 ,该复制 api 还是可以正常使用的。


具体代码修改


于是我修改了一下原来的 hook:


import Toast from "~@/components/Toast";

export const useCopy = () => {

const copy = async (text: string, toast?: string) => {

const fallbackCopyTextToClipboard = (text: string, toast?: string) => {
let textArea = document.createElement("textarea");
textArea.value = text;

// Avoid scrolling to bottom
textArea.style.top = "-200";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0"

document.body.appendChild(textArea);
// textArea.focus();
textArea.select();
let msg;
try {
let successful = document.execCommand("copy");
msg = successful ? toast ? toast : "复制成功" : "复制失败";
} catch (err) {
msg = "复制失败";
}
Toast.dispatch({
content: msg,
});
document.body.removeChild(textArea);
};

const copyTextToClipboard = (text: string, toast?: string) => {
if (!navigator.clipboard || !window.isSecureContext) {
fallbackCopyTextToClipboard(text, toast);
return;
}
navigator.clipboard
.writeText(text)
.then(() => {
Toast.dispatch({
content: toast ? toast : "复制成功",
});
})
.catch(() => {
fallbackCopyTextToClipboard(text, toast)
});
};
copyTextToClipboard(text, toast);
};

return copy;
};

上线近一年,这个复制方法没出现异常问题。


作者:HyaCinth
来源:juejin.cn/post/7317577665014448167
收起阅读 »

高管违法开除我的一些想法

昨天(1月7日),公历年初,农历临近过年。这本应大家辛苦了一年,进行年会、团聚、收款,盼望着领取薪资,与远在千里的家人团聚的日子。 然而现实是,被井某指着鼻子威胁骂到: 我等劳动局的! 我现在就违法解除! 我告诉你了,我现在就违法解除! 你试试看,你他妈两年...
继续阅读 »

昨天(1月7日),公历年初,农历临近过年。这本应大家辛苦了一年,进行年会、团聚、收款,盼望着领取薪资,与远在千里的家人团聚的日子。


然而现实是,被井某指着鼻子威胁骂到:



  • 我等劳动局的!

  • 我现在就违法解除!

  • 我告诉你了,我现在就违法解除!

  • 你试试看,你他妈两年半找不着工作!你试试看!

  • 劳动仲裁一审二审,我有的是人!我慢慢等你,你试试看。

  • 你敢上传一个试试,我现在就打110!


事件和视频


原视频很大媒体平台都有,包括百度、抖音等。



本来想知道到底是什么原因,视频里的女高管如此有恃无恐,一时不知道到底是谁违法了。难道是因为孙某有什么错误,导致她认为就算是自己根据没有违法,所以法律拿她无可奈何,或者是她认为违法了又如何?


但是随着视频的播放,她放话:劳动仲裁一审二审,我有的是人!我慢慢等你,你试试看……,从这里看来意思是说假设真的是官司打起来了,我有是办法(让你时间耗不起、工作找不起)。虽然孙某有在拍视频,她也知道有在拍,但依然如此出言不逊,甚至你敢上传一个试试


当晚,此视频在和微信群被转发,各在文章平台、短视频平台迅速跟进,次日,名为北京尼欧XX科技有限公司发表了声明。


公司声明


image.png


如果只有视频,确实也不知事情原委(万一高管只是在视频里说说玩,其实是孙某违法了呢?),所以就没继续想些什么。但次日看到这个声明之后,从声明上看:高管以停职反省处理、孙某以足额支付补偿金处理,并强调章程均合法合规。


假设此声明没有问题,我有以下疑问:



  • 员工是否真是不能通过试用期


据我了解,假设试用期为6个月,一般公司会在第二三个月就会有相关的述职会议,以评估你是否能胜任工作。也就是说,能不能胜任工作,能不能通过试用期,通常2-3个月就能知晓了。但是为什么要在最后一个月才因某能力不足裁员呢?半年的时候,普通项目都做得差不多了。



  • 视频中的日期是什么时候


视频是在1月7号流传的。如果是热点事件,通常在一天内基本大家都能知道了。


但声明上说是根据12月8日足额支付补偿,依法合规,也就是说一个月之前就已经合规处理完此事了?


个人想法


大家有知道马云说了离职不是钱不够,就是心受委屈了。虽然有比较多的人补充说还有其他自己想走之类的原因云云。


但细究的话,各有各的原因,这就不便分析问题了。


总的说来,不管是自己离职,还是公司裁员,应该都能归纳于:愿意的、不愿意的。


这东西,就像是谈恋爱一样,如果双方不喜欢,不愿意,或开始愿意,后面不愿意了,终究就会产生破窗效应,最终摆烂或分道扬镳。


如果发现员工能力不足,或公司运营困难了,需要裁员时,应早日给出处理方案,例如培训、转岗、或直言等,从双方平等的角度获得对方的理解。


如果员工认为公司有哪些地方不合理,也早点提出相关方案,为什么不喜欢?有没有建议方案?尝试过哪些努力?比如公司人员结构、工作强度、代码可维护性……提出来看看,假设表达合理,公司也重视你,自然能给你相应的说法。


如果公司的解决问题的方案是,解决提出问题的人,那早点离开又何尝不对呢?说小一点,这是为了自己洒脱一点,说大一点,这是人择良友而交,禽择良木而栖,让环境越来越好。


当然,很多时候作为人确实也是身不由己,太多羁绊。但是有没有认真考虑过,有的东西是值得的吗?当发现不值得的时候,自己还有退路吗?是健康快乐更重要还是别人的看法更重要?


如何维护自身利益


那么,作为一个员工,我们应如何保护自己?不一定解决问题,但可能解决问题。


作为员工如何维护自己的利益


搞清楚劳动法和公司规定,知道自己有什么权利和责任。了解工资、工时、福利、休假等方面的规定,确保公司别违法。留着跟工作有关的文件,合同、工资单、绩效评估之类的记录。这些东西能当证据,帮你维护自己的权益。


如果有问题或烦恼,及时跟相关人员沟通,提供明确的事实和证据。参与公司的反馈机制,提建议和意见。继续学习,提升自己的技能,增加竞争力。参加培训课程、专业发展计划,提高职业能力和知识水平。


关注职业发展机会和市场趋势,找适合自己的发展方向。积极参与职业培训、跨部门项目之类的,提高竞争力。
平衡工作和个人生活的需求,保持身心健康。合理安排工作时间和休息时间,别太累和压力太大


被违法裁员时应如何处理


搞明白劳动法和规定,尤其是关于裁员的规定。这样你就知道自己有什么权利,雇主有什么责任。收集跟裁员有关的所有证据,比如裁员通知、合同、工资单、绩效评估、公司规定之类的文件。这些东西在后面的法律行动中可能很重要。


找专业的劳动法律顾问或律师咨询,让他们给你解释权益和法律选项。他们能帮你评估情况,提供适当的建议和法律支持。跟雇主沟通,表达你对裁员决定的担忧和不满。写份申诉信或要求重新考虑决定。有时候,通过沟通和谈判,可能会找到解决问题的办法。


根据当地的法律程序,你还有机会通过调解或仲裁来解决争议。律师会给你专业的法律建议,并在法庭上代表你维护权益。


最后,为勇敢维护自身利益的人们点赞!


相关信息



作者:四叶草会开花
来源:juejin.cn/post/7320959103932989451
收起阅读 »

环信Web端IM Demo登录方式如何修改

在环信即时通讯云IM 官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的Appkey和用户去进行登录呢?👇👇👇本文以Web端为例,教大家如何更改代码来实现1、 VUE2 Demovue2 demo源码下载vue2 demo线上...
继续阅读 »

在环信即时通讯云IM 官网下载Demo,本地运行只有手机+验证码的方式登录?怎么更改为自己项目的Appkey和用户去进行登录呢?

👇👇👇本文以Web端为例,教大家如何更改代码来实现

1、 VUE2 Demo

第一步:更改appkey

webim-vue-demo===>src===>utils===>WebIMConfig.js
bcf8e92b4df988de5df647cddd0b8ce5.png

第二步:更改代码

webim-vue-demo===>src===>pages===>login===>index.vue

<template>
<a-layout>
<div class="login">
<div class="login-panel">
<div class="logo">Web IM</div>
<a-input v-model="username" :maxLength="64" placeholder="用户名" />
<a-input v-model="password" :maxLength="64" v-on:keyup.13="toLogin" type="password" placeholder="密码" />
<a-input v-model="nickname" :maxLength="64" placeholder="昵称" v-show="isRegister == true" />

<a-button type="primary" @click="toRegister" v-if="isRegister == true">注册</a-button>
<a-button type="primary" @click="toLogin" v-else>登录</a-button>
</div>
<p class="tip" v-if="isRegister == true">
已有账号?
<span class="green" v-on:click="changeType">去登录</span>
</p>
<p class="tip" v-else>
没有账号?
<span class="green" v-on:click="changeType">注册</span>
</p>

<!-- <div class="login-panel">
<div class="logo">Web IM</div>
<a-form :form="form" >
<a-form-item has-feedback>
<a-input
placeholder
="手机号码"
v
-decorator="[
'phone',
{
rules: [{ required: true, message: 'Please input your phone number!' }],
},
]"
style
="width: 100%"
>
<a-select
initialValue
="86"
slot
="addonBefore"
v
-decorator="['prefix', { initialValue: '86' }]"
style
="width: 70px"
>
<a-select-option value="86">
+86
</a-select-option>
</a-select>
</a-input>
</a-form-item>

<a-form-item>
<a-row :gutter="8">
<a-col :span="14">
<a-input
placeholder
="短信验证码"
v
-decorator="[
'captcha',
{ rules: [{ required: true, message: 'Please input the captcha you got!' }] },
]"
/>
</a-col>
<a-col :span="10">
<a-button v-on:click="getSmsCode" class="getSmsCodeBtn">{{btnTxt}}</a-button>
</a-col>
</a-row>
</a-form-item>
<a-button style="width: 100%" type="primary" @click="toLogin" class="login-rigester-btn">登录</a-button>

</a-form> -->
<!-- </div> -->
</div>
</a-layout>
</template>

<script>
import './index.less';
import { mapState, mapActions } from 'vuex';
import axios from 'axios'
import { Message } from 'ant-design-vue';
const domain = window.location.protocol+'//a1.easemob.com'
const userInfo = localStorage.getItem('userInfo') && JSON.parse(localStorage.getItem('userInfo'));
let times = 60;
let timer
export default{
data(){
return {
username: userInfo && userInfo.userId || '',
password: userInfo && userInfo.password || '',
nickname: '',
btnTxt: '获取验证码'
};
},
beforeCreate() {
this.form = this.$form.createForm(this, { name: 'register' });
},
mounted: function(){
const path = this.isRegister ? '/register' : '/login';

if(path !== location.pathname){
this.$router.push(path);
}
if(this.isRegister){
this.getImageVerification()
}
},
watch: {
isRegister(result){
if(result){
this.getImageVerification()
}
}
},
components: {},
computed: {
isRegister(){
return this.$store.state.login.isRegister;
},
imageUrl(){
return this.$store.state.login.imageUrl
},
imageId(){
return this.$store.state.login.imageId
}
},
methods: {
...mapActions(['onLogin', 'setRegisterFlag', 'onRegister', 'getImageVerification', 'registerUser', 'loginWithToken']),
toLogin(){
this.onLogin({
username: this.username.toLowerCase(),
password: this.password
});
// const form = this.form;
// form.validateFields(['phone', 'captcha'], { force: true }, (err, value) => {
// if(!err){
// const {phone, captcha} = value
// this.loginWithToken({phone, captcha})
// }
// });
},
toReset(){
this.$router.push('/resetpassword')
},
toRegister(e){
e
.preventDefault(e);
// this.form.validateFieldsAndScroll((err, values) => {
// if (!err) {
// this.registerUser({
// userId: values.username,
// userPassword: values.password,
// phoneNumber: values.phone,
// smsCode: values.captcha,
// })
// }
// });

this.onRegister({
username: this.username.toLowerCase(),
password: this.password,
nickname: this.nickname.toLowerCase(),
});
},
changeType(){
this.setRegisterFlag(!this.isRegister);
},
getSmsCode(){
if(this.$data.btnTxt != '获取验证码') return
const form = this.form;
form
.validateFields(['phone'], { force: true }, (err, value) => {
if(!err){
const {phone, imageCode} = value
this.getCaptcha({phoneNumber: phone, imageCode})
}
});
},
getCaptcha(payload){
const self = this
const imageId = this.imageId
axios
.post(domain+`/inside/app/sms/send/${payload.phoneNumber}`, {
phoneNumber: payload.phoneNumber,
})
.then(function (response) {
Message
.success('短信已发送')
self
.countDown()
})
.catch(function (error) {
if(error.response && error.response.status == 400){
if(error.response.data.errorInfo == 'Image verification code error.'){
self
.getImageVerification()
}
if(error.response.data.errorInfo == 'phone number illegal'){
Message
.error('请输入正确的手机号!')
}else if(error.response.data.errorInfo == 'Please wait a moment while trying to send.'){
Message
.error('你的操作过于频繁,请稍后再试!')
}else if(error.response.data.errorInfo.includes('exceed the limit')){
Message
.error('获取已达上限!')
}else{
Message
.error(error.response.data.errorInfo)
}
}
});
},
countDown(){
this.$data.btnTxt = times
timer
= setTimeout(() => {
this.$data.btnTxt--
times
--
if(this.$data.btnTxt === 0){
times
= 60
this.$data.btnTxt = '获取验证码'
return clearTimeout(timer)
}
this.countDown()
}, 1000)
}
}
};
</script>

webim-vue-demo===>src===>store===>login.js
只用更改actions下的onLogin,其余不用动

onLogin: function(context, payload){
context.commit('setUserName', payload.username);
let options = {
user: payload.username,
pwd: payload.password,
appKey: WebIM.config.appkey,
apiUrl: 'https://a1.easecdn.com'
};
WebIM.conn.open(options).then((res)=>{
localStorage.setItem('userInfo', JSON.stringify({ userId: payload.username, password: payload.password,accessToken:res.accessToken}));
});

},

2、VUE3 DEMO:

第一步:更改appkey

webim-vue-demo===>src===>IM===>config===>index.js

c27eca4aefd5861bb4014d86d7b080de.png

第二步:更改代码

webim-vue-demo===>src===>views===>Login===>components===>LoginInput===>index.vue

<script setup>
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { EaseChatClient } from '@/IM/initwebsdk'
import { handleSDKErrorNotifi } from '@/utils/handleSomeData'
import { fetchUserLoginSmsCode, fetchUserLoginToken } from '@/api/login'
import { useStore } from 'vuex'
import { usePlayRing } from '@/hooks'
const store = useStore()
const loginValue = reactive({
phoneNumber: '',
smsCode: ''
})
const buttonLoading = ref(false)
//根据登陆初始化一部分状态
const loginState = computed(() => store.state.loginState)
watch(loginState, (newVal) => {
if (newVal) {
buttonLoading
.value = false
loginValue
.phoneNumber = ''
loginValue
.smsCode = ''
}
})
const rules = reactive({
phoneNumber: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
smsCode: [
{
required: true,
message: '请输入短信验证码',
trigger: ['blur', 'change']
}
]
})
//登陆接口调用
const loginIM = async () => {
const { clickRing } = usePlayRing()
clickRing()
buttonLoading
.value = true
/* SDK 登陆的方式 */
try {
let { accessToken } = await EaseChatClient.open({
user: loginValue.phoneNumber.toLowerCase(),
pwd: loginValue.smsCode.toLowerCase(),
});
window
.localStorage.setItem(`EASEIM_loginUser`, JSON.stringify({ user: loginValue.phoneNumber, accessToken: accessToken }))
} catch (error) {
console
.log('>>>>登陆失败', error);
const { data: { extraInfo } } = error
handleSDKErrorNotifi(error.type, extraInfo.errDesc);
loginValue
.phoneNumber = '';
loginValue
.smsCode = '';
}
finally {
buttonLoading
.value = false;
}
/* !环信后台接口登陆(仅供环信线上demo使用!) */
// const params = {
// phoneNumber: loginValue.phoneNumber.toString(),
// smsCode: loginValue.smsCode.toString()
// }
// try {
// const res = await fetchUserLoginToken(params)
// if (res?.code === 200) {
// console.log('>>>>>>登陆token获取成功', res.token)
// EaseChatClient.open({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// window.localStorage.setItem(
// 'EASEIM_loginUser',
// JSON.stringify({
// user: res.chatUserName.toLowerCase(),
// accessToken: res.token
// })
// )
// }
// } catch (error) {
// console.log('>>>>登陆失败', error)
// if (error.response?.data) {
// const { code, errorInfo } = error.response.data
// if (errorInfo.includes('does not exist.')) {
// ElMessage({
// center: true,
// message: `用户${loginValue.username}不存在!`,
// type: 'error'
// })
// } else {
// handleSDKErrorNotifi(code, errorInfo)
// }
// }
// } finally {
// buttonLoading.value = false
// }
}
/* 短信验证码相关 */
const isSenedAuthCode = ref(false)
const authCodeNextCansendTime = ref(60)
const sendMessageAuthCode = async () => {
const phoneNumber = loginValue.phoneNumber
try {
await fetchUserLoginSmsCode(phoneNumber)
ElMessage({
type: 'success',
message: '验证码获取成功!',
center: true
})
startCountDown()
} catch (error) {
ElMessage({ type: 'error', message: '验证码获取失败!', center: true })
}
}
const startCountDown = () => {
isSenedAuthCode
.value = true
let timer = null
timer
= setInterval(() => {
if (
authCodeNextCansendTime
.value <= 60 &&
authCodeNextCansendTime
.value > 0
) {
authCodeNextCansendTime
.value--
} else {
clearInterval(timer)
timer
= null
authCodeNextCansendTime
.value = 60
isSenedAuthCode
.value = false
}
}, 1000)
}
</script>

<template>
<el-form :model="loginValue" :rules="rules">
<el-form-item prop="phoneNumber">
<el-input
class="login_input_style"
v
-model="loginValue.phoneNumber"
placeholder
="手机号"
clearable
/>
</el-form-item>
<el-form-item prop="smsCode">
<el-input
class="login_input_style"
v
-model="loginValue.smsCode"
placeholder
="请输入短信验证码"
>
<template #append>
<el-button
type
="primary"
:disabled="loginValue.phoneNumber && isSenedAuthCode"
@
click
="sendMessageAuthCode"
v
-text="
isSenedAuthCode
? `${authCodeNextCansendTime}S`
: '获取验证码'
"

></el-button>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="function_button_box">
<el-button
v
-if="loginValue.phoneNumber && loginValue.smsCode"
class="haveValueBtn"
:loading="buttonLoading"
@
click
="loginIM"
>登录</el-button
>
<el-button v-else class="notValueBtn">登录</el-button>
</div>
</el-form-item>
</el-form>
</template>

<style lang="scss" scoped>
.login_input_style {
margin: 10px 0;
width: 400px;
height: 50px;
padding: 0 16px;
}

::v-deep .el-input__inner {
padding: 0 20px;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 1.75px;
color: #3a3a3a;

&::placeholder {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
/* identical to box height */
letter-spacing: 1.75px;
color: #cccccc;
}
}

::v-deep .el-input__suffix-inner {
font-size: 20px;
margin-right: 15px;
}

::v-deep .el-form-item__error {
margin-left: 16px;
}

::v-deep .el-input-group__append {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
width: 60px;
color: #fff;
border: none;
font-weight: 400;

button {
font-weight: 300;
}
}

.login_text {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
text-align: right;

.login_text_isuserid {
display: inline-block;
// width: 100px;
color: #f9f9f9;
}

.login_text_tologin {
margin-right: 20px;
width: 80px;
color: #05b5f1;
cursor: pointer;

&:hover {
text-decoration: underline;
}
}
}

.function_button_box {
margin-top: 10px;
width: 400px;

button {
margin: 10px;
width: 380px;
height: 50px;
border-radius: 57px;
}

.haveValueBtn {
background: linear-gradient(90deg, #04aef0 0%, #5a5dd0 100%);
border: none;
font-weight: 300;
font-size: 17px;
color: #f4f4f4;

&:active {
background: linear-gradient(90deg, #0b83b2 0%, #363df4 100%);
}
}

.notValueBtn {
border: none;
font-weight: 300;
font-size: 17px;
background: #000000;
mix-blend-mode: normal;
opacity: 0.3;
color: #ffffff;
cursor: not-allowed;
}
}
</style>

3、React DEMO:

第一步:更改appkey

webim-dev===>demo===>src===>config===>WebIMConfig.js
c23c60aa28a75ddd2c743497453736d2.png

第二步:更改代码

webim-dev===>demo===>src===>config===>WebIMConfig.js
将usePassword改为true

6a1f227099bd124db2a2a4611e5bbc4b.png

4、Uniapp Demo:

第一步:更改appkey

uniapp vue2 demo
webim-uniapp-demo===>utils===>WebIMConfig.js

a1041f6e463396da3db74110f5477863.png

uniapp vue3 demo
webim-uniapp-demo===>EaseIM===>config===>index.js

7fc3d3d4077b50be4fdba689acb2f9a4.png

第二步:更改代码

webim-uniapp-demo===>pages===>login===>login.vue

c18a811eba8546dc53bebca8fa0b31da.png

5、微信小程序 Demo:

第一步:更改appkey

webim-weixin-demo===>src===>utils===>WebIMConfig.js

109f65d6690a9ace085c47e9b5e9eb49.png

第二步:更改代码

webim-weixin-demo===>src===>pages===>login===>login.wxml

<import src="../../comps/toast/toast.wxml" />
<view class="login">
<view class="login_title">
<text bindlongpress="longpress">登录</text>
</view>

<!-- 测试用 请忽略 -->
<view class="config" wx:if="{{ show_config }}">
<view>
<text>使用沙箱环境</text>
<switch class="config_swich" checked="{{isSandBox? true: false}}" color="#0873DE" bindchange="changeConfig" />
</view>
</view>

<view class="login_user {{nameFocus}}">
<input type="text" placeholder="请输入用户名" placeholder-style="color:rgb(173,185,193)" bindinput="bindUsername" bindfocus="onFocusName" bindblur="onBlurName" />
</view>
<view class="login_pwd {{psdFocus}}">
<input type="text" password placeholder="用户密码" placeholder-style="color:rgb(173,185,193)" bindinput="bindPassword" bindfocus="onFocusPsd" bindblur="onBlurPsd"/>
</view>
<view class="login_btn">
<button hover-class="btn_hover" bind:tap="login">登录</button>
</view>
<template is="toast" data="{{ ..._toast_ }}"></template>
</view>

webim-weixin-demo===>src===>pages===>login===>login.js

let WebIM = require("../../utils/WebIM")["default"];
let __test_account__, __test_psword__;
let disp = require("../../utils/broadcast");

let runAnimation = true
Page({
data: {
name: "",
psd: "",
grant_type: "password",
rtcUrl: '',
show_config: false,
isSandBox: false
},

statechange(e) {
console.log('live-player code:', e.detail.code)
},

error(e) {
console.error('live-player error:', e.detail.errMsg)
},

onLoad: function(option){
const me = this;
const app = getApp();
new app.ToastPannel.ToastPannel();

disp.on("em.xmpp.error.passwordErr", function(){
me.toastFilled('用户名或密码错误');
});
disp.on("em.xmpp.error.activatedErr", function(){
me.toastFilled('用户被封禁');
});

wx.getStorage({
key: 'isSandBox',
success (res) {
console.log(res.data)
me.setData({
isSandBox: !!res.data
})
}
})

if (option.username && option.password != '') {
this.setData({
name: option.username,
psd: option.password
})
}
},

bindUsername: function(e){
this.setData({
name: e.detail.value
});
},

bindPassword: function(e){
this.setData({
psd: e.detail.value
});
},
onFocusPsd: function(){
this.setData({
psdFocus: 'psdFocus'
})
},
onBlurPsd: function(){
this.setData({
psdFocus: ''
})
},
onFocusName: function(){
this.setData({
nameFocus: 'nameFocus'
})
},
onBlurName: function(){
this.setData({
nameFocus: ''
})
},

login: function(){
runAnimation = !runAnimation
if(!__test_account__ && this.data.name == ""){
this.toastFilled('请输入用户名!')
return;
}
else if(!__test_account__ && this.data.psd == ""){
this.toastFilled('请输入密码!')
return;
}
wx.setStorage({
key: "myUsername",
data: __test_account__ || this.data.name.toLowerCase()
});

getApp().conn.open({
user: __test_account__ || this.data.name.toLowerCase(),
pwd: __test_psword__ || this.data.psd,
grant_type: this.data.grant_type,
appKey: WebIM.config.appkey
});
},

longpress: function(){
console.log('长按')
this.setData({
show_config: !this.data.show_config
})
},

changeConfig: function(){
this.setData({
isSandBox: !this.data.isSandBox
}, ()=>{
wx.setStorage({
key: "isSandBox",
data: this.data.isSandBox
});
})

}

});


相关文档:

收起阅读 »

MyBatis实战指南(一):从概念到特点,助你快速上手,提升开发效率!

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。

大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作而感到困扰?是否曾经因为SQL语句的编写而烦恼?那么,MyBatis或许就是你的救星。

接下来,让我们一起来了解一下MyBatis的概念与特点吧!

一、MyBatis基本概念

MyBatis 是一款优秀的半自动的ORM持久层框架,它支持自定义 SQL、存储过程以及高级映射。

MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。

MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

那么,什么是ORM?

要了解ORM,先了解下面概念:

持久化

把数据(如内存中的对象)保存到可永久保存的存储设备中。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。

持久层

即专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用者和数据实体相关联。

ORM, 即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射。这样在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。

Description

总结:

  • 它是一种将内存中的对象保存到关系型数据库中的技术;

  • 主要负责实体对象的持久化,封装数据库访问细节;

  • 提供了实现持久化层的另一种模式,采用映射元数据(XML)来描述对象-关系的映射细节,使得ORM中间件能在任何一个Java应用的业务逻辑层和数据库之间充当桥梁。

Java典型的ORM框架:

  • hibernate:全自动的框架,强大、复杂、笨重、学习成本较高;

  • Mybatis:半自动的框架, 必须要自己写sql;

  • JPA:JPA全称Java Persistence API、JPA通过JDK 5.0注解或XML描述对象-表的映射关系,是Java自带的框架。

二、Mybatis的作用

Mybatis是一个Java持久层框架,它主要用于简化与数据库的交互操作。Mybatis的主要作用有以下几点:

  • 将Java对象与数据库表进行映射,通过配置XML文件实现SQL语句的定义和执行,使得开发者可以专注于业务逻辑的实现而无需编写繁琐的JDBC代码。

  • 提供了灵活的SQL映射功能,可以根据需要编写动态SQL,支持复杂的查询条件和更新操作。

  • 支持事务管理,可以确保数据的一致性和完整性。

  • 提供了缓存机制,可以提高数据库查询性能。

  • 可以与Spring、Hibernate等其他框架无缝集成,方便开发者在项目中使用。

Mybatis就是帮助程序员将数据存取到数据库里面。传统的jdbc操作,有很多重复代码块比如: 数据取出时的封装, 数据库的建立连接等等,通过框架可以减少重复代码,提高开发效率 。

MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射。

所有的事情,不用Mybatis依旧可以做到,只是用了它,会更加方便更加简单,开发更快速。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

三、MyBatis特点

1、定制化SQL

同为持久层框架的Hibernate,对操作数据库的支持方式较多,完全面向对象的、原生SQL的和HQL的方式。MyBatis只支持原生的SQL语句,这个“定制化”是相对Hibernate完全面向对象的操作方式的。

2、存储过程

储存过程是实现某个特定功能的一组sql语句集,是经过编译后存储在数据库中。当出现大量的事务回滚或经常出现某条语句时,使用存储过程的效率往往比批量操作要高得多。

MyBatis是支持存储过程的,可以看个例子。假设有一张表student:

create table student
(
id bigint not null,
name varchar(30),
sex char(1),
primary key (id)
);

有一个添加记录的存储过程:

create procedure pro_addStudent (IN id bigint, IN name varchar(30), IN sex char(1))
begin
insert into student values (id, name, sex);
end

此时就可以在mapper.xml文件中调用存储过程:

<!-- 调用存储过程 -->
<!-- 第一种方式,参数使用parameterType -->
<select id="findStudentById" parameterType="java.lang.Long" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(#{id,jdbcType=BIGINT,mode=IN})}
</select>

<parameterMap type="java.util.Map" id="studentMap">
<parameter property="id" mode="IN" jdbcType="BIGINT"/>
</parameterMap>

<!-- 调用存储过程 -->
<!-- 第二种方式,参数使用parameterMap -->
<select id="findStudentById" parameterMap="studentMap" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(?)}
</select>

3、高级映射

可以简单理解为支持关联查询。

4、避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

使用Mybatis时,数据库的连接配置信息,是在mybatis-config.xml文件中配置的。同时,获取查询结果的代码,也是尽量做到了简洁。以模糊查询为例,需要做两步工作:

1)首先在配置文件中写上SQL语句,示例:

 <mapper namespace="com.test.pojo">
<select id="listCategoryByName" parameterType="string" resultType="Category">
select * from category_ where name like concat('%',#{0},'%')
</select>
</mapper>

2)在Java代码中调用此语句,示例:

        List<Category> cs = session.selectList("listCategoryByName","cat");
for (Category c : cs) {
System.out.println(c.getName());
}

5、Mybatis中ORM的映射方式也是比较简单的

"resultType"参数的值指定了SQL语句返回对象的类型。示例代码:
<mapper namespace="com.test.pojo">
<select id="listCategory" resultType="Category">
select * from category_
</select>
</mapper>

四、Mybatis的适用场景

MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。MyBatis因其简单易用、灵活高效的特点,广泛应用于各种Java项目中。

以下是一些常见的应用场景:

  • 数据查询:MyBatis可以执行复杂的SQL查询,返回Java对象或者结果集。

  • 数据插入、更新和删除:MyBatis可以执行INSERT、UPDATE和DELETE等SQL语句。

  • 存储过程和函数调用:MyBatis可以调用数据库的存储过程和函数。

  • 高级映射:MyBatis支持一对一、一对多、多对一等复杂关系的映射。

  • 懒加载:MyBatis支持懒加载,只有在真正需要数据时才会去数据库查询。

  • 缓存机制:MyBatis内置了一级缓存和二级缓存,可以提高查询效率。

为什么说Mybatis是半自动ORM映射工具

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。

而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

MyBatis作为半自动ORM映射工具与全自动ORM工具相比,有几个主要的区别点:

1.SQL的灵活性

MyBatis作为半自动ORM映射工具,开发人员可以灵活地编写SQL语句,充分发挥数据库的特性和优势。而全自动ORM工具通常会在一定程度上限制开发人员对SQL的灵活控制。

2.映射关系的可定制性

MyBatis允许开发人员通过配置文件(或注解)自定义对象和数据库表之间的映射关系,可以满足各种复杂的映射需求。而全自动ORM工具通常根据约定和规则自动生成映射关系,对于某些特殊需求无法满足。

3.SQL的可复用性

MyBatis支持SQL的可复用性,可以将常用的SQL语句定义为独立的SQL片段,并在需要的地方进行引用。而全自动ORM工具通常将SQL语句直接与对象的属性绑定在一起,缺乏可复用性。

4.性能调优的灵活性

MyBatis作为半自动ORM映射工具,允许开发人员对SQL语句进行灵活的调优,通过手动编写SQL语句和使用高级特性进行性能优化。而全自动ORM工具通常将性能优化的控制权交给框架,开发人员无法灵活地对SQL进行调优。

MyBatis作为一种半自动ORM映射工具,相对于全自动ORM工具具有更高的灵活性和可定制性。通过灵活的SQL控制、自定义的映射关系、可复用的SQL以及灵活的性能调优,MyBatis可以满足各种复杂的映射需求和性能优化需求。

虽然MyBatis相对于全自动ORM工具需要开发人员编写更多的SQL语句,但正是由于这种半自动的特性,使得MyBatis在某些复杂场景下更加灵活和可控。

因此,我们可以说MyBatis是一种半自动ORM映射工具,与全自动的ORM工具相比,它更适用于那些对SQL灵活性和性能调优需求较高的场景。

五、Mybatis的优缺点

Mybatis有以下优点:

1.基于SQL语句编程,相当灵活

SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。

2. 代码量少

与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。

3.很好的与各种数据库兼容

4.数据库字段和对象之间可以有映射关系

提供映射标签,支持对象与数据库的ORM字段关系映射。

5.能够与Spring很好的集成

Mybatis有以下缺点:

1.SQL语句的编写工作量较大

尤其当字段多、关联表多时,SQL语句较复杂。

2.数据库移植性差

SQL语句依赖于数据库,不能随意更换数据库(可以通过在mybatis-config.xml配置databaseIdProvider来弥补)。

示例:

 <databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>

然后在xml文件中,就可以针对不同的数据库,写不同的sql语句。

3.字段映射标签和对象关系映射标签仅仅是对映射关系的描述,具体实现仍然依赖于sql。

示例:

public class Student{
String name;
List<Interest> interests;
}

public class Interest{
String studentId;
String name;
String direction;
}

<resultMap id="ResultMap" type="com.test.Student">
<result column="name" property="name" />
<collection property="interests" ofType="com.test.Interest">
<result column="name" property="name" />
<result column="direction" property="direction" />
</collection>
</resultMap>

在该例子中,如果查询sql中,没有关联Interest对应的表,则查询出数据映出的Student对象中,interests属性值就会为空。

4.DAO层过于简单,对象组装的工作量较大

即Mapper层Java代码过少,XxxMapper.xml文件中维护数据库字段和实体类字段的工作量较大。

5.不支持级联更新、级联删除

仍以上面的Student和Interest为例,当要更新/删除某个Student的信息时,需要在两个表进行手动更新/删除。

通过以上的介绍,相信大家对MyBatis已经有了更深入的了解。MyBatis是一个非常强大的持久层框架,它的灵活性、易用性、解耦性、高效性和全面性都使得它在Java开发中得到了广泛的应用。

收起阅读 »

我困在考研的这两年

我困在考研的这两年 2024考研结束了,我想对我这两年的考研之路做个总结。 2021年冬 2021年冬天的某一天,突然决定要考研。这时候已经距离我2017年大学毕业过去了四年,毕业四年之后再拿起书本准备考研,现在想想也感觉有些疯狂。 大学毕业的这四年,在北京有...
继续阅读 »

我困在考研的这两年


2024考研结束了,我想对我这两年的考研之路做个总结。


2021年冬


2021年冬天的某一天,突然决定要考研。这时候已经距离我2017年大学毕业过去了四年,毕业四年之后再拿起书本准备考研,现在想想也感觉有些疯狂。


大学毕业的这四年,在北京有了一份相对稳定的工作。从一开始干劲十足,到现在心累无力。看到过各种各样的中年领导,有时候也会想,这会不会就是自己未来三十岁、四十岁的样子。当有这种念头的时候,就会忍不住想逃离。想去改变,想把自己生活、职业的天花板再调高些。


当脑袋里冒出考研的时候,我也问过我自己,28上研究生真的有意义吗?毕业已经30+了,在这个35岁就是职业生涯末期的行业,这不是毕业就失业嘛。干嘛执着去考研呐。但是想到遇到过的那些中年领导,又害怕自己走他们的老路。不想被困死,只能改变。


2022年春夏秋冬


由于2021年末决定考研,我对考研形势也没有清晰的认知,转年三月份才开始准备。后来得知人家三月份早就已经把数学基础过完一遍了,我那时候还不知道定积分是个数吶!三、四月份还过了一遍高中数学知识,现在想来也是可笑,准备当年的研究生考试,还有时间去过高中数学知识。五月份报了班学习数学和英语,早上起来学英语,白天上班,晚上学习数学,地铁里看专业课知识。


那时候什么都不懂,就知道基础阶段、强化阶段、冲刺阶段。拼命赶进度,练习很少,学了后面忘了前面。也都顾不上了,先学了再说。也不管知识的掌握情况,就是闷头往前冲,英语大纲词汇过了一遍之后就不看了,学长难句,学语法。根本不想英语单词是不是要天天去背,没有时间去想那么多。


追进度、干糙活。中途又经历搬家。搬家之后,每天早起做英语阅读,坐地铁时看专业课知识,晚上写写数学。没有学习计划,没有复习计划。现在想那要是能学好,那可真就是天才了。可惜我不是


十二月初,我阳了。很难受,好像打摆子一样,身体抖个不停。发烧,头疼,感觉脑袋里边的脑仁疼。那几天我没怎么看书,也没看进去书。好在是考试之前阳了,没有因为这个原因缺考。


十二月二十四号,研究生考试开考。那天早上出来,天还是黑的。我有过一阵恍惚,不知道是为啥。到了考场,第一科开考了一个小时,我看空了大半的考场,缺考者很多。心里还窃喜,竞争者少了这么多,这我不上岸谁上岸。现在想来真是无知者无畏。


2023年春


考试成绩下来了,没有过线,国家线都没过,彻底失败。出分当天爸爸做手术,我正在忙着跑前跑后。看到群里有研友说可以查到分了,我急忙点开微信里保存的查分网址。没有登录、没有输入报名号,就那么直接的把我的考试成绩展示出来了。对我冲击很大,我一时间不知道怎么应对。幸亏我当时在上行的扶梯,不需要思考,它带着我向上。


和爸爸妈妈说了成绩之后,爸爸妈妈也没有责备我。反而鼓励我再考,“一年不行就两年,两年不行就三年。咱总得给它考上”。我当时很感动,我爸妈总是很坚定的支持我追寻自己的人生,真幸运遇到他们呀。那天也没时间考虑这个成绩,爸爸的手术从早上一直等到晚上七点多才做,晚上十一点才做完。很幸运,手术很成功。这是那天最好的消息


收拾挫败的心情,把不甘心化为动力。再大干一场吧。这毕竟是我自己的人生那!


2023年夏秋冬


回北京后,自己内心复盘了一下去年的学习方法,列了几条自己的问题,开始有针对的改变。



  • 英语读不懂就背单词,大纲单词四千+,每天背不了多的,那就背五十个。几个月怎么也过一遍。第二遍就每天两百个,再第三遍。第四遍...记不住具体词就记大体结构,先知道这个词的意思。作文我也用不了四千多个单词,用不上全部都背全词。再不行就写,联想着记。

  • 数学把去年没懂的地方都记录下来,先总体过一遍基础。再针对学不懂的章节。跟一个老师听不懂,就上B站、找网盘,看其他老师怎么讲的。对比验证着理解。

  • 专业课划出近几年的热点考题,着重了解对应的章节。

  • 学习方法不对就改,用艾宾浩斯曲线复习。


中午吃完饭,从办公区出来找一个阴凉地方看各科视频。夏天很热,周边饭店后厨的抽油烟机声音很响。买了个降噪耳机。每天中午要回去上班的时候,把耳机一摘,全是汗。


七月份,找了个小房子,自己搬出来单独住。每天学习、工作,时间安排的很满,很充实。每天也没有那么多时间去胡思乱想。七、八、九这三个月过的很快,没什么感觉就来到了十月份


九月底,接到了裁员通知。整个部门砍掉,人员全部辞退。“疫情的风”终于是吹到了我,我其实早就做好了心理准备。本来打算十月一假期回来提离职,十月底走人。专心十一月、十二月复习考研。接到通知后,没什么大的情绪起伏,坦然接受。通知是上午发的,赔偿是下午谈好的。emmm...说没情绪起伏是假的,这笔赔偿对我来说还是挺可观的。真香


这世界上唯一不会变的,就是一切都在变


坦然接受变化,因为迟早会有这么一天。


进入到十一月份,可能是临近考试了。突然感觉到焦躁,好多之前会做、能做的题。突然在真题这就不会做了。不能说一点思路没有吧,有点但是有限。翻开答案,看了就明白。但是让自己再做类似的题,还是和之前一样的感觉,人都麻了。抓紧复习知识点,再把强化阶段这个题相关章节的题拿出来重做。人更麻了。原来能做对的题,现在也做不对了。一点思路都没有,直接卡壳。尝试过总结题型,总结做题步骤。对我来说毫无用处,再遇到这种题,第一感觉还是大脑一片空白。多元函数积分学你在听吗?我说的就是你!


心里越来越焦虑,晚上躺床上也睡不着觉。基本每天都是满脑子乱想,然后迷迷糊糊睡着。


越到考试日期,越焦虑


我尝试开解自己“你不是不会,你是太紧张了,你是累了,你是没休息好,你是头脑不清醒”,我每天都下楼借着中午吃饭的时候吹吹风,放空下头脑。虽然效果不大,但是有效果就行。


十二月份,北京下雪了。那天中午吃完饭回来,我在小区的花园里走了好久。踩踩雪,感受下这真实的世界,这真实的生活,这真实的人生。


十二月二十二号,考试前一天,这一天我一点学习状态都没有。激动、颤抖、焦虑各种各样的情绪交杂在一起,肖四是一点也看不下去,更别提背了。为了第二天能按时起来。我手表订了五个闹钟,手机订了一个闹钟。狠怕自己起不来。


十二月二十三号,研究生考试的日子又到了。今年北京这边新增了安检门,由于不知道具体什么流程,所以考试的第一天我去的特别早。还是天蒙蒙亮的早晨。在电梯到一楼开门的那一刹那,我内心告诉自己“这是通往你波澜壮阔人生的一刻,加油去干吧”。


本来打算在考场外边背一会肖四再进去。但是因为今年新增了安检门,手机、书包什么都带不进楼里。只能放在楼外边的柜子里。外边好多人都守在柜子旁边背肖四,我本来也想趁这个时候再背背。但是我实在是有些太焦虑了,根本静不下心来。知识不进脑,外边还冷。索性就不背了,直接进考场。


该来的总会来,担忧那么多干嘛


找到考场后,发现就来了三个人。一看表,哦,才七点四十,八点半开始考试。在座位那硬坐了五十分钟。


政治、英语、数学、专业课


这四门考试之前,我坐在考场都很紧张。手心里都是汗,双手张开在桌子上摩擦一遍又一遍,做着深呼吸。告诉自己“没问题,我可以”。


每科考完试,感觉脑袋和身体都像被掏空了一样。那个时候没有太多想法,就是想吐槽一下考试题,哈哈哈。


本来我打算吐槽一下英语、数学、专业课(如果这次自己失败了,也好有一个赖的理由)。但是想想还是算了,强者从来都不抱怨环境。虽然我还不是强者,但是该有的格局咱们还是得有滴。那我就说说对这几科我感受到的优点吧。



  • 政治:中规中矩

  • 英语:英语一图画、图表作文首次结合,阅读AI模型、新题型博物馆、翻译大象都挺跟时事的。不得不说还是英语命题组会玩,很好,很新颖。

  • 数学:“60+老头”“坏滴很”,你哪里薄弱就往你哪里猛攻

  • 专业课:近十年来出的最好的一套卷子。出的题有深度,不偏不怪,不机械不套路。更注重理解而不是死记硬背。


该走的总要走,挽留也是徒劳


考完了,今年考完的感觉和去年完全不一样。今年少了无知无畏的乐观,更多的是如释重负的释然。不管最终结果如何,我已用尽了我的力气。我不想做悲情英雄,今年我上岸吧。梦中情校变母校,去到我想去的地方。


宇哥改编的这句歌词真好:“你看我多平常,困难一堆散落地上,但是我的眼中有光亮,换上坚强,气宇轩昂上战场,终将去到我想去的地方。”


青春,就是那些认为自己与众不同的日子


感谢我对象对我的支持理解,感谢我爸妈对我的包容和鼓励,也谢谢那个不放弃人生的自己


最后


我还想写好多话,写好多感受。但是现在已经凌晨一点多了。我明天还有事要早起。言尽至此


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

开发距离生活有多远

相信做开发的同学,生活中会遇到一个频率非常高的问题。通常这个问题涉及的对话是这样的: 亲朋:“你在做什么工作呀?” 本人:“我是做软件开发工作的。” 亲朋:“噢!搞电脑的呀,好高端呀,你们这个行业具体是做什么呢?” 本人:“唔......就比如手机上的 AP...
继续阅读 »

相信做开发的同学,生活中会遇到一个频率非常高的问题。通常这个问题涉及的对话是这样的:



亲朋:“你在做什么工作呀?”

本人:“我是做软件开发工作的。”

亲朋:“噢!搞电脑的呀,好高端呀,你们这个行业具体是做什么呢?”

本人:“唔......就比如手机上的 APP ,微信、淘宝你用过吧?类似这种。”

亲朋:“哇,好厉害呀!”



上面这段,是我本人过往对于这个问题的回答。其实,每次我这么回答完以后,总觉得不得劲儿。感觉好像解释了一通,却又好像没让对方理解什么是开发工作。


image.png


直到最近,我的表妹又问了我这个问题:



表妹:“我其实一直没搞明白,你们写程序到底在做什么,所以,是在做什么呢?”



开发真的距离生活有那么远吗?


直到再次思考这个问题,我似乎找到了这个问题难以回答的根源:我压根没明白程序跟生活到底有什么关系。


在这个时代,编程的产物充斥着生活的各个角落:网购、聊天、支付等。但生活和程序,好像两条相互缠绕,却又难以相交的曲线。开发的产物服务于生活,但要用生活去解释开发,却又不是那么容易的事情。程序和生活中间,难道真的相隔着一个未知的距离吗?


程序不是无中生有,而是提高效率


我们开发的程序从来不是无中生有,从来不是创造不曾存在的东西,而是有围绕某个业务做的提效工具。


例如饮品店的店员操作的机器,上面就搭载了点单、收银两大功能的程序。你说这个程序没被开发出来以前,难道店员就不点单吗,就不收银吗?当然不是,让我们回忆一下,过往饮品店收银员是怎么工作的:




  1. 询问客户要买什么饮品,客户点单后,收银员用小纸条写下饮品的名称,递给做饮品的小哥;

  2. 收银员用计算器算好价格,客户递给纸币,收银员找零;

  3. 饮品做好后,收银员思考将饮品给哪位客户;



当点单量巨大时,在这套操作中,有几个痛点出现了:




  1. 写小纸条给制作饮品的小哥,这个操作会变得很耗时;

  2. 人工计算价格、收银、找零,容易出差错;

  3. 在收银员思考将饮品交给哪位客户这件事上,需要耗费巨大的脑力;



而现在的程序的流程是这样的:




  1. 客户点单,收银员在屏幕上选择客户购买的饮品,生成价格;

  2. 客户亮出付款码进行付款,生成订单号;

  3. 客户通过订单号领取饮品;



看,这就是程序做的事情,程序只是优化了生活中繁琐的步骤,提高了生活、工作的效率。人类社会向前发展,实质上就是要提高效率,把更多的时间放在更重要的人或事情上。


作为开发工作者,我们应该是更先进的


作为开发工作者,我们应该培养解决问题的能力,应该把提升效率的思考放在日常生活中,不要做只会敲代码的程序员。这是开发工作带给我们的优势和能力,让我们在生活中,多一些思考和实践。


开发也好,程序也好,离我们的生活真的很近,近到我们随时可以触摸,近到离不开我们的生活。用开发的思维为生活插上翅膀,毕竟,各个学科、行业都是从实际生活中孕育而出,最终也应回归生活,服务生活。


作者:水果小贩
来源:juejin.cn/post/7320655446100115506
收起阅读 »

为什么mysql最好不要只用limit做分页查询?

在项目中遇到的真实问题,以及我的解决方案,部分数据做了脱敏处理。 问题 最近在做项目时需要写sql做单表查询,每次查出来的数据有几百万甚至上千万条,公司用的数据库是MySQL5.7,做了分库分表,部分数据库设置了查询超时时间,比如查询超过15s直接报超时错误,...
继续阅读 »

在项目中遇到的真实问题,以及我的解决方案,部分数据做了脱敏处理。


问题


最近在做项目时需要写sql做单表查询,每次查出来的数据有几百万甚至上千万条,公司用的数据库是MySQL5.7,做了分库分表,部分数据库设置了查询超时时间,比如查询超过15s直接报超时错误,如下图:


image.png


可以通过show variables like 'max_statement_time';命令查看数据库超时时间(单位:毫秒):


image.png


方案1


尝试使用索引加速sql,从下图可以看到该sql已经走了主键索引,但还是需要扫描150万行,无法从这方面进行优化。


image.png


方案2


尝试使用limit语句进行分页查询,语句为:


SELECT * FROM table WHERE user_id = 123456789 limit 0, 300000;

像这样每次查30万条肯定就不会超时了,但这会引出另一个问题--查询耗时与起始位置成正比,如下图:


image.png


第二条语句实际上查了60w条记录,不过把前30w条丢弃了,只返回后30w条,所以耗时会递增,最终仍会超时。


方案3


使用指定主键范围的分页查询,主要思想是将条件语句改为如下形式(其中id为自增主键):


WHERE user_id = 123456789 AND id > 0 LIMIT 300000;
WHERE user_id = 123456789 AND id > (上次查询结果中最后一条记录的id值) LIMIT 300000;

也可以将上述语句简化成如下形式(注意:带了子查询会变慢):


WHERE user_id = 123456789 AND id >= (SELECT id FROM table LIMIT 300000, 1) limit 300000;

每次查询只需要修改子查询limit语句的起始位置即可,但我发现表中并没有自增主键id这个字段,表内主键是fs_id,而且是无序的。


这个方案还是不行,组内高工都感觉无解了。


方案4


既然fs_id是无序的,那么就给它排序吧,加了个ORDER BY fs_id,最终解决方案如下:


WHERE user_id = 123456789 AND fs_id > 0 ORDER BY fs_id LIMIT 300000;
WHERE user_id = 123456789 AND fs_id > (上次查询结果中最后一条记录的id值) ORDER BY fs_id LIMIT 300000;

效果如下图:


image.png


查询时间非常稳定,每条查询的fs_id都大于上次查询结果中最后一条记录的fs_id值。正常查30w条需要3.88s,排序后查30w条需要6.48s,确实慢了许多,但总算能把问题解决了。目前代码还在线上跑着哈哈,如果有更好的解决方案可以在评论区讨论哟。


作者:我要出去乱说
来源:juejin.cn/post/7209612932366270519
收起阅读 »

啊?两个vite项目怎么共用一个端口号啊

web
问题: 最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后: ...
继续阅读 »

问题:


最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后:


image.png


该项目的端口号为5173,但是此时我再次通过vite的官方搭建一个react+ts+vite项目:npm create vite@latest react_demos -- --template react-ts,之后通过npm run dev启动项目,发现端口号并没有更新:


image.png


这是什么原因呢?


寻因:


查阅官方文档,我发现:


image.png


那么我主动在vite.config.ts中添加这个配置:


image.png


正常来说,会出现这个报错:


image.png


但是此时结果依然为:


image.png


我百思不得不得其解,于是再次查阅官方文档:


image.png
我寻思这也与文档描述不一致啊,于是我再次尝试,思考是不是vite版本号的问题,两个项目的版本号分别为:


image.png


image.png


我决定创建一个4版本的项目npm create vite@^4.1.4 react_demos3 -- --template react-ts


image.png


结果发现,还是有这个问题,跟版本号没有关系,于是我又耐心继续看官方文档,看到了这个配置:


image.png
我抱着试试的态度,在其中一个vite项目中添加这个配置:


image.png


发现,果然是这个配置的锅,当其中一个项目host配置为0.0.0.0时,vite不会自动尝试更新端口号


难道vite的端口监测机制与host也有关?


结果:


不甘心的我再次进行尝试,将两个项目的host都设置成:


image.png


image.png


vite会自动尝试更新端口号


原来如此,vite的端口号检测机制在对比端口号之前,会先对比host,由于我的微前端项目中设置了host,而新建的项目中没有设置host,新建的项目host默认值为localhost对比不成功,vite不会自动尝试下一个可用端口,而是共用一个端口


总结:


在遇到问题时,要多多去猜,去想各种可能,并且最重要的是去尝试各种可能,还要加上积极去翻阅官方文档,问题一定会得到解决的;哪怕不能解决,那也会在尝试中,学到很多东西


作者:进阶的鱼
来源:juejin.cn/post/7319699173740363802
收起阅读 »

谈谈我家的奇葩买房经历

我是 2017 年毕业的,18 年买的房。 当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。 当时的房价也是一路飙升,一周一个价那种。 我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。 那年过年回家的时候,我爸问我存了多少钱,...
继续阅读 »

我是 2017 年毕业的,18 年买的房。


当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。


当时的房价也是一路飙升,一周一个价那种。


我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。


那年过年回家的时候,我爸问我存了多少钱,我说没有存多少,不知道钱花在哪里了。


我爸嫌我花的太多了,说要不买个房吧,这样每月还房贷还能存下点。


我说北京的房子需要交 5 年公积金才能买,而且首付二百多万呢,还没那么多钱。况且以后我也不一定留在北京,可能回青岛干。


年后我就回京继续上班了。


我爸在家开了一个门店,每天坐在门口和邻居聊天。


邻居聊起他儿子读完博士在青岛工作了,在黄岛区买了个房子,两周涨了十多万呢。


然后我爸就急了,非让我妈也去买一个,说是现在买还便宜点,就算我以后不回青岛,也可以卖了去北京再买。


我爸和我妈其实关系并不好,几乎是连吵带骂的逼着我妈去买。


为什么他不自己去呢?


因为我爸有严重的晕车,一坐车就吐。


我妈其实也没出过远门,自己一个人坐车从潍坊去青岛买房确实难为她了。


我妈还有点迷信,临走之前找算卦的占了一卦,算出一个方位,说是去青岛的城阳区买。


然后我妈就去了。


我妈啥也不懂,就在一个小区门口转悠。


然后保安过来问她干啥的。


她说想来买房,但是不知道去哪里买。


保安说我给你介绍一个人,可以找他买。


然后就给我妈介绍了一个中介。


那个中介说现在青岛都限购,需要交 2 年社保,只有即墨不限购,因为它刚撤市划区,划入青岛。


然后我妈找了个出租,并且给了出租的 200 块钱,让他一起去。


之后就到了即墨观澜国际的售楼处,人家介绍说这个房子是楼王,也就是最中间的那栋楼,是最好的,而且只有几套了。


我妈还在纠结,但是那个出租不耐烦了,要走。


然后我妈就定下来了,交了 70 万首付。


之后要办理手续,我从北京回家了一趟,和我爸我妈一起打车去了青岛。


我爸一路吐了有几十次,他说把胆汁都吐出来了。


就这样,我们就在青岛买下了这套房子。


13380 一平,首付 70 万,贷款 100 万,还 15 年,总共还 150 万。


然后我又回北京上班去了,只不过开始了还房贷的日子,一个月 1 万。


之后我爸又给了我 30 万,加上我自己还的,差不多在 2021 年就把 100 多万贷款还完了。因为提前还还的少。


差不多我爸 100 万,我拿了 100 万。


其实我还挺震惊的,我爸这样一个吃喝都那么节俭的人,竟然能拿出 100 万现金来。


后来在 2022 年年中的时候,我爸浑身疼的厉害,在地上打滚那种疼,去医院查出来是淋巴癌晚期。


然后 2023 年也就是今年年初的时候,我爸去世了。


去世前交代了一些事情,这套房子给我的 100 万就是他一辈子的积蓄了。


二手房要满 5 年才能卖,正好今年可以卖了。


但是问了下房价,现在观澜国际的均价是 7000 多,我们 2018 年买的时候是 13380 呢。而且 200 万的房子现在 90 万都不一定卖出去。


那我能咋办?


怪我爸?但我爸已经没了。


怪我妈?我妈也经常犯愁,而且当年是我爸逼她去的。


而且当年那种情况,我爸做的决定并没有错,当年大多数人都会认为房价会一直涨,早上车省很多钱。


我身边有一些朋友也是为了这个刚毕业不久就买房了。


其实住的话倒也没啥问题,关键是我并不去青岛工作,而且即墨那边也找不到前端的工作,互联网公司就集中在那几个城市。


租的话,一年才 1 万 5,而且装修还要投入好几万。


所以只能卖了。


本来是我们打算 5 年后卖了,正好在北京交满了 5 年公积金,然后再去北京买。


现在这情况,估计 200 万可能一分也收不回来。


遇到这事,正常人都会难受吧,我也一样。


那天我公众号发了条卖房消息:



真的是为了卖房么?


肯定不是啊,这样能把房子卖出去就怪了。


我只是想把它讲出来,仅此而已。讲出来之后确实好多了。


这几年我这种情况的全国也有不少:



并不是为了炒房,但确实因为各种原因不去住了。结果再卖的时候腰斩都卖不出去。


后来我也释然了,我本身物欲就很低,一辈子也用不了多少钱。


而且我还年轻,赚的也不少,可以再攒。


更重要的是,我一直觉得人这一生不能只是为了赚钱,要找到自己热爱的事业,在这个方向上持续开拓,创造自己的价值。


所幸我找到了。它才是支撑起我后半生的骨架。


最后,这段经历也不是完全没价值,至少我可以把它写下来,当做故事讲给你们听。


作者:zxg_神说要有光
来源:juejin.cn/post/7281833142104948776
收起阅读 »

大环境越不好 人就越玄学

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。 问广大学子毕业后最想从事什么工作。 当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。 只有很少一部分选择了事业单位和公务员,这...
继续阅读 »

二零零几年,大环境还没像现在这么拉垮的时候,有个面向学生的网站叫校内网,里面曾有人发起了一次大范围投票。


问广大学子毕业后最想从事什么工作。


当时超过一半的人都选择了大型外企,排名第二的是大型国企民企,然后是自主创业。


只有很少一部分选择了事业单位和公务员,这部分同学还有相当比例来自对考公自古有执念的山东。


而在其他省份,多数同学都认为自己能拥有光明的未来,当然不会喜欢公务员这种工资稳定得低,日复一日枯坐案前,早早就能一眼望到头的工作。


在当时年轻人眼里,公务员属于“实在不行就只能回家考公“的备胎,地位约等于“实在不行就找个老实人嫁了“的级别。


但后来的故事我们都知道了,经济大船这几年驶入了深水区,风浪越来越大,鱼也越来越贵。


于是四平八稳旱涝保收的体制内,这几年摇身一变,一跃成为了那个最靓的仔。不得不说,人确实是时代的产物,环境的变化可以完全改变一个人的决策。


大环境好的时候,人们会不自觉地高估自身的努力,那时候人们是相信努力一定会有收获的。有时候过于相信了,但这在经济高速增长的年代并不会有太大问题,你还是会得到属于自己的那块蛋糕的。


但当经济增速换档时,付出与回报的比例开始失衡,努力就能收获的简单逻辑不攻自破。变成了努力也不一定有收获,进而发展成努力大概率不会有收获,最后演变成一命二运三风水,努力奋斗算个鬼


这种心态的转变也解释了为啥从去年以来,越来越多的年轻人开始扎堆去寺庙求签祈福,排的长队连起来能绕地球三圈,看得旁观的老大爷直摇头说,“真搞不懂这些小年轻是怎么想的,偶像粉丝见面会咋还跑到庙里来开了?!”


人在逆境迷茫时,是容易被玄学吸引。逆境意味着前路遇阻,意味着你迫切需要一些指引,而玄学恰好满足了这方面需求。


命运这个东西,有时候真蛮捉摸不透的。


我认识一小姐姐,为一场决定人生的重要考试做足了准备,结果在赶往考场的路上,书包就这么巧被扒手偷了,里面开卷考试所有的资料全部丢失,直接导致她逃汰出局,泪洒当场。


还有一大哥,在升职加薪岗位竞争的关键阶段,突然一场急病,好巧不巧失声了,一句话也说不出来,参加不了竞聘答辩,眼睁睁看着大好机会就此溜走。


等这事过去了,他一下子又能正常说话,跟被老天上了沉默debuff一样,你说他找谁说理去呢。


人活得时间越长,就越信“命“这个东西,越能意识到自己真正能把控的其实少得可怜,随便一点意外都能直接改变整个人生走向。


这种感悟放在以前,一般都是上了些年纪的人才会有的,但随着这两年经济增速换挡,年轻人频繁碰壁,被命运按在地上摩擦的次数多了,自然也就信了“命”,求签问道的也就跟着多起来了。


说句不好听的话,我觉得这样挺好的。不是说求签问道这个行为好,而是这种行为背后暗含着一个巨大的心理转变,我认为很好。


那就是放过自己。亚洲人尤其是我们特别不愿意放过自己,从出生开始就活在比较中,长辈们连夸个人都要这么夸,说哎呀,你学习真用功,比学习委员还用功;哎呀,你工资挺高,比隔壁小王还要高。


骂你的时候也一定要捎带上别人,说你看谁谁谁多厉害,你再看看你,一定是你还不够努力。


就是这种搞法很容易让人把责任全揽自己身上,对自我要求过高,最后的结果就是崩掉,就累嘛!


但现在不一样了,现代人在网络上看了太多含着金汤匙出生在罗马的人,和那些老天爷追着赏饭吃的人。


他们跟我们之间的差距大到几辈子都弥补不上,那努力万能论也就不攻自破了嘛。


于是越来越多的小伙伴开始承认自我的局限,承认努力也不一定有收获,承认人生不如意十之八九,慢慢也就承认了“命运”这个东西,开始顺其自然,没那么多执念了。


不过有些人过于放飞自我,摆烂走了另一个极端,那也是要出问题的。


即便是玄学,它也没有彻底否定个人奋斗,大富靠命没错,但小富靠勤,靠双手取得一些小成就,让日子过得舒服些还是没啥问题的。


其实我觉得一个比较合适的世界观应该是这个样子:首先咱得承认不可抗力,承认“命”与“运”这个东西是真实存在的,如果你不喜欢这两个玄乎的字,可以用“概率”代替,我们永远得做好小概率事件砸到头上的准备。


有时候拼尽一切就是没有好的结果,这咱得承认,但同时这也并不意味着从此放弃一切行动,落入虚无主义的陷阱。


人还是要去做一些什么的。比如精进某项专业技能,逐步提升自身能力,为的不是那点工资,而是一件更重要的事,抓住运气。


运气有多重要,大家都明白,它比努力重要得多。


运气这东西打比方的话,就像一个宝箱,会随机在你面前掉落,但这些宝箱自带隐形属性,你等级太低的话就看不见它,自然也就抓不住这些运气。


用现实举例,“运气”就像你在工作中遇到了某个本来还可以拉你一把的贵人,结果你的等级太低,工作能力稀碎,贵人一看,这货不值得我帮,转身走了。他这个宝箱对你而言就隐形了,消失了。


而且最讽刺的是你从头到尾都被蒙在鼓里,根本不知道自己错失了一次宝贵的机会,所以为了避免运气来了你抓不住,又溜走的这种尴尬情况出现,我们还是要去精进和磨练一下社会技能,尽量达到能在某些场合被人夸奖的程度。


把等级刷高一些,之后该吃吃该喝喝,耐心等待宝箱的出现。这可能也是以前人们常说的,“尽人事听天命”的另一种解释吧。


也希望今天聊的关于命和运的这些内容,能启发到一些小伙伴,大家一起认认真真,平平淡淡的生活。


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

别再忘了锁屏,几行代码写一个人走屏锁小工具

写在前面 之前在公司,毕竟是干安全的,部门有这么一个要求,被发现不锁屏的,请全部门喝奶茶。很不幸,我也出现过忘了锁屏然后被发现的情况。自此之后,我就形成了肌肉记忆,同时也对别人不锁屏很敏感。 为什么要强调锁屏呢?你也不想你的电脑被别人操作吧,也不想自己的信息被...
继续阅读 »

写在前面


之前在公司,毕竟是干安全的,部门有这么一个要求,被发现不锁屏的,请全部门喝奶茶。很不幸,我也出现过忘了锁屏然后被发现的情况。自此之后,我就形成了肌肉记忆,同时也对别人不锁屏很敏感。


为什么要强调锁屏呢?你也不想你的电脑被别人操作吧,也不想自己的信息被别人获取吧。毕竟防人之心不可无。


自打跳槽到新公司之后,每次去厕所的路上就看到有人电脑不锁屏,真的是令我无比的纠结。锁个屏幕有那么难吗?确实很难,有时候一忙就容易忘,于是我就想实现一个离开电脑自动锁屏的程序。


分析


这玩意实现也不难,简单思考一下,就是让电脑检测人在不在电脑前面,那就是要试试捕获摄像头了,然后设置一个间隔时间,每隔一段时间截取图片,做人脸识别,没有人脸了就锁屏就行了。


涉及到摄像头图片处理,直接让人联想到opencv,然后再用python实现上面的一套逻辑,就搞定。


代码


安装opencv的库


pip install opencv-python

直接上代码:


import cv2
import time
import os
import platform

# 检测操作系统
def detect_os():
os_name = platform.system()
if os_name == 'Windows':
return 'windows'
elif os_name == 'Darwin':
return 'mac'
else:
return 'other'

# 执行锁屏命令
def lock_screen(os_type):
if os_type == 'windows':
os.system('rundll32.exe user32.dll, LockWorkStation')
elif os_type == 'mac':
os.system('/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend')


# 初始化摄像头
cap = cv2.VideoCapture(0)

# 载入OpenCV的人脸检测模型
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

# 无人状态计时器
no_person_timer = 0
# 设定无人状态时间阈值
NO_PERSON_THRESHOLD = 3

# 检测操作系统类型
os_type = detect_os()

while True:
ret, frame = cap.read()
if not ret:
break

# 转换为灰度图像
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.1, 4)

if len(faces) == 0:
no_person_timer += 1
else:
no_person_timer = 0

# 如果超过阈值,则锁屏
if no_person_timer > NO_PERSON_THRESHOLD:
lock_screen(os_type)
no_person_timer = 0

time.sleep(1)

cap.release()

代码里都做好了注释,很简单,因为windows和macOS的锁屏指令不一样,所以做了个简单的系统平台判断。


可以完美执行,就是它得一直调用摄像头,应该也不会有人真的使用这玩意吧,hhh。


作者:银空飞羽
来源:juejin.cn/post/7317824480911458304
收起阅读 »

终于搞懂了网盘网页是怎么唤醒本地应用了

web
写在前面 用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。 自定义协议 本身单凭浏览器是没有唤醒本地应用这个能力的,...
继续阅读 »

写在前面


用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。


自定义协议


本身单凭浏览器是没有唤醒本地应用这个能力的,不然随便一个网页都能打开你的所有应用那不就乱套了吗。但是电脑系统本身又可以支持这个能力,就是通过配置自定义协议。


举个例子,当你用浏览器打开一个本地的PDF的时候,你会发现上面是file://path/xxx.pdf,这就是系统内置的一个协议,浏览器可以调用这个协议进行文件读取。


那么与之类似的,windows本身也支持用户自定义协议来进行一些操作的,而这个协议就在注册表中进行配置。


配置自定义协议


这里我用VS Code来举例子,最终我要实现通过浏览器打开我电脑上的VS Code。


我们先编写一个注册表文件


Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\vscode]
@="URL:VSCode Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\vscode\shell]

[HKEY_CLASSES_ROOT\vscode\shell\open]

[HKEY_CLASSES_ROOT\vscode\shell\open\command]
@=""D:\VScode\Microsoft VS Code\Code.exe" "%1""

这里我逐行解释



  1. Windows Registry Editor Version 5.00 这行表明该文件是一个 Windows 注册表编辑器文件,这是标准的头部,用于告诉 Windows 如何解析文件。

  2. [HKEY_CLASSES_ROOT\vscode] 这是一个注册表键的开始。在这里,\vscode 表示创建一个名为 vscode 的新键。

  3. @="URL:VSCode Protocol"vscode 键下,这行设置了默认值(表示为 @ ),通过 "URL:VSCode Protocol" 对这个键进行描述。

  4. "URL Protocol"="" 这行是设置一个名为 URL Protocol 的空字符串值。这是代表这个新键是一个 URI 协议。

  5. [HKEY_CLASSES_ROOT\vscode\shell] 创建一个名为 shell 的子键,这是一个固定键,代表GUI界面的处理。

  6. [HKEY_CLASSES_ROOT\vscode\shell\open]shell 下创建一个名为 open 的子键。这耶是一个固定键,open 是一个标准动作,用来执行打开操作。

  7. [HKEY_CLASSES_ROOT\vscode\shell\open\command]open 下创建一个名为 command 的子键。这是一个固定键,指定了当协议被触发时要执行命令。

  8. @=""D:\VScode\Microsoft VS Code\Code.exe" "%1""command 键下,设置默认值为 VSCode 的路径。 "%1" 是一个占位符,用于表示传递给协议的任何参数,这里并无实际用处。


写好了注册表文件后,我们将其保存为 vscode.reg,并双击执行,对话框选择是,相应的注册表信息就被创建出来了。



可以通过注册表中查看。


浏览器打开VS Code


这时,我们打开浏览器,输入 vscode://open



可以看到,就像百度网盘一样,浏览器弹出了询问对话框,然后就可以打开VS Code了。


如果想要在网页上进行打开,也简单


<script>
function openVSCode() {
window.location.href = 'vscode://open/';
}
</script>
<button onclick="openVSCode()">打开 VSCode</button>

写一个简单的JS代码即可。


写在最后


至此,终于是了解了这方面的知识。这就是说,在网盘安装的过程中,就写好了这个注册表文件,自定义了网盘的唤醒协议,才可以被识别。


而我也找到了这个注册表



原来叫baiduyunguanjia协议(不区分大小写),使用 baiduyunguanjia://open 可以打开。


作者:银空飞羽
来源:juejin.cn/post/7320513026188460067
收起阅读 »

都用HTTPS了,还能被查出浏览记录?

最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥? 实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如: DNS查询:通常DNS查询是不会加密的,...
继续阅读 »

最近,群里一个刚入职的小伙因为用公司电脑访问奇怪的网站,被约谈了。他很困惑 —— 访问的都是HTTPS的网站,公司咋知道他访问了啥?



实际上,由于网络通信有很多层,即使加密通信,仍有很多途径暴露你的访问地址,比如:



  • DNS查询:通常DNS查询是不会加密的,所以,能看到你DNS查询的观察者(比如运营商)是可以推断出访问的网站

  • IP地址:如果一个网站的IP地址是独一无二的,那么只需看到目标 IP地址,就能推断出用户正在访问哪个网站。当然,这种方式对于多网站共享同一个IP地址(比如CDN)的情况不好使

  • 流量分析:当访问一些网站的特定页面,可能导致特定大小和顺序的数据包,这种模式可能被用来识别访问的网站

  • cookies或其他存储:如果你的浏览器有某个网站的cookies,显然这代表你曾访问过该网站,其他存储信息(比如localStorage)同理


除此之外,还有很多方式可以直接、间接知道你的网站访问情况。


本文将聚焦在HTTPS协议本身,聊聊只考虑HTTPS协议的情况下,你的隐私是如何泄露的。


HTTPS简介


我们每天访问的网站大部分是基于HTTPS协议的,简单来说,HTTPS = HTTP + TLS,其中:



  • HTTP是一种应用层协议,用于在互联网上传输超文本(比如网页内容)。由于HTTP是明文传递,所以并不安全

  • TLS是一种安全协议。TLS在传输层对数据进行加密,确保任何敏感信息在两端(比如客户端和服务器)之间安全传输,不被第三方窃取或篡改


所以理论上,结合了HTTPTLS特性的HTTPS,在数据传输过程是被加密的。但是,TLS建立连接的过程却不一定是加密的。


TLS的握手机制


当我们通过TLS传递加密的HTTP信息之前,需要先建立TLS连接,比如:



  • 当用户首次访问一个HTTPS网站,浏览器开始查询网站服务器时,会发生TLS连接

  • 当页面请求API时,会发生TLS连接


建立连接的过程被称为TLS握手,根据TLS版本不同,握手的步骤会有所区别。



但总体来说,TLS握手是为了达到三个目的:



  1. 协商协议和加密套件:通信的两端确认接下来使用的TLS版本及加密套件

  2. 验证省份:为了防止“中间人”攻击,握手过程中,服务器会向客户端发送其证书,包含服务器公钥和证书授权中心(即CA)签名的身份信息。客户端可以使用这些信息验证服务器的身份

  3. 生成会话密钥:生成用于加密接下来数据传输的密钥


TLS握手机制的缺点


虽然TLS握手机制会建立安全的通信,但在握手初期,数据却是明文发送的,这就造成隐私泄漏的风险。


在握手初期,客户端、服务端会依次发送、接收对方的打招呼信息。首先,客户端会向服务端打招呼(发送client hello信息),该消息包含:



  • 客户端支持的TLS版本

  • 支持的加密套件

  • 一串称为客户端随机数client random)的随机字节

  • SNI等一些服务器信息


服务端接收到上述消息后,会向客户端打招呼(发送server hello消息),再回传一些信息。


其中,SNIServer Name Indication,服务器名称指示)就包含了用户访问的网站域名。


那么,握手过程为什么要包含SNI呢?


这是因为,当多个网站托管在一台服务器上并共享一个IP地址,且每个网站都有自己的SSL证书时,那就没法通过IP地址判断客户端是想和哪个网站建立TLS连接,此时就需要域名信息辅助判断。


打个比方,快递员送货上门时,如果快递单只有收货的小区地址(IP地址),没有具体的门牌号(域名),那就没法将快递送到正确的客户手上(与正确的网站建立TLS连接)。


所以,SNI作为TLS的扩展,会在TLS握手时附带上域名信息。由于打招呼的过程是明文发送的,所以在建立HTTPS连接的过程中,中间人就能知道你访问的域名信息。


企业内部防火墙的访问控制和安全策略,就是通过分析SNI信息完成的。



虽然防火墙可能已经有授信的证书,但可以先分析SNI,根据域名情况再判断要不要进行深度检查,而不是对所有流量都进行深度检查



那么,这种情况下该如何保护个人隐私呢?


Encrypted ClientHello


Encrypted ClientHelloECH)是TLS1.3的一个扩展,用于加密Client Hello消息中的SNI等信息。


当用户访问一个启用ECH的服务器时,网管无法通过观察SNI来窥探域名信息。只有目标服务器才能解密ECH中的SNI,从而保护了用户的隐私。



当然,对于授信的防火墙还是不行,但可以增加检查的成本



开启ECH需要同时满足:



  • 服务器支持TLSECH扩展

  • 客户端支持ECH


比如,cloudflare SNI测试页支持ECH扩展,当你的浏览器不支持ECH时,访问该网站sni会返回plaintext



对于chrome,在chrome://flags/#encrypted-client-hello中,配置ECH支持:



再访问上述网站,sni如果返回encrypted则代表支持ECH


总结


虽然HTTPS连接本身是加密的,但在建立HTTPS的过程中(TLS握手),是有数据明文传输的,其中SNI中包含了服务器的域名信息。


虽然SNI信息的本意是解决同一IP下部署多个网站,每个网站对应不同的SSL证书,但也会泄漏访问的网站地址


ECH通过对TLS握手过程中的敏感信息(主要是SNI)进行加密,为用户提供了更强的隐私保护。


作者:魔术师卡颂
来源:juejin.cn/post/7264753569834958908
收起阅读 »

《年会不能停》豆瓣8.2分,强烈建议所有职场人都去看!

12024年的第一部电影献给了《年会不能停》,豆瓣开分8.1分,现在保持在8.2分,这部片子真的将每个打工人都狠狠代入其中,超级推荐。作为曾经的职场新媒体人看起来,反讽效果拉满,笑点密不能停,台词里句句是职场人的嘴替,有人看着是乐子,有人是照镜子,笑完之后,蓦...
继续阅读 »

1


2024年的第一部电影献给了《年会不能停》,豆瓣开分8.1分,现在保持在8.2分,这部片子真的将每个打工人都狠狠代入其中,超级推荐。


作为曾经的职场新媒体人看起来,反讽效果拉满,笑点密不能停,台词里句句是职场人的嘴替,有人看着是乐子,有人是照镜子,笑完之后,蓦然回首小丑竟是我自己。


影片讲述的是一名厂里的“高级钳工”胡建林(大鹏饰),阴差阳错被调入了公司总部成为人事专员,从“工厂”到“大厂”经过一系列乌龙事件,反而职位越做越高。


深谙职场生存之道的打工人马杰(白客饰),与叛逆的外包员工潘妮(庄达菲饰),俩人就是踏实做事的社畜代表,勤勤恳恳却碌碌无为,甚至连正都转不了。


却与在职场最会被吐槽的胡建林成为“铁三角”组合,在最后年会揭发了公司高层的腐化,从而保住了一个厂全体员工的饭碗。


影片好看之处就在于拍出了当下社会职场的现象,年轻人在这种现状里的疑惑和挣扎。


虽然结尾的“大团圆”结局过于理想主义,但我们也只能在电影中找到这种爽感来出一口对现实的恶气,虽然梦醒之后依旧是加班熬夜低头倒茶。


2


我一直有一件疑问的事,有人真的热爱上班吗?应该有80%的人回复是不爱,但无奈吧。


从毕业之后,经历了几份工作,发现我是真不爱上班,除了拿点每个月准时的“窝囊费”,好像真没什么值得开心的了,还有无止境的加班,内部争斗,还有付出和回报不成正比的委屈。


但偏偏人就要为这几点碎银两,向生活和工作低头,就像代表职场里中年人的马杰一样。


无数个马杰都如蚂蚁一般,勤勤恳恳做事,但反而得不到升职加薪,内心有原则却难守护,就如那句“如果我失业了,家人怎么办”,直击打工人的压力痛点。


相反只有像潘妮,这个角色就代表00后的职场人群,不战队不妥协不随流,就是去整顿职场这些荒谬的规则的。


好不容公司同意转正,她却另辟蹊径,潇洒地递交“世界那么大,我要去看看”的“叛逆”辞职信。


在职场也许我们像中年的胡建林和马杰,但人生不只有工作,更多时候拥有潘妮的“叛逆”和勇敢,寻找更多的人生出口,才会更有趣更有力量一些。


3


我觉得影片很妙的一点是,把两个时代的人物结合到同一个平行空间里,将两代人的职场风格和做事方法也融入到了一起,形成了强烈对比,反差感很强。


最开始以为讲述的是90年代的事,没想到是同时代的打工场景,这种跨越也正好是我们这代和父母辈所经历过的场景,加上拍摄地点和我的职场经历相似, 更有代入感了。


90年代时我妈就曾在汽电厂工作,我小时候也在那种环境中生活过,工人们统一的工服,螺丝钉一样的工作内容,集体主义式的生活,通勤只有2分钟。


工作虽然在身体上辛苦又繁复,但下班就真的是下班,不必看公司和客户消息,电话会议,生活简单又满足。


而作为当代社畜,一定是脑力和体力的双倍付出,996的工作时间,但没有加班工资,做不完的事,没有周末,下班和放假还要守“机”待“工”,工作和生活从来不能完全分开。


时代在变,两代人的职场理想不一样,上一辈的老思想就是一份工作就是一辈子的事,在一个岗位日复一日的劳作,像胡建林一样做到一颗螺丝钉一咬就知道质量对不对。


而如今像这样心思单纯性格执拗的职员,绝对就是裁员名单上的第一批人。


4


从“工厂”跳到“大厂”,从工人到白领,胡建林宛若穿越一般的人,一切事物就像他说的“小刀割屁股,开了眼”了。


不会英语不会大厂里的专业术语,连“优化”也理解错,让裁员变成升职,在这一系列骚操作中“弄拙成巧”,连连高升。


如果按现实来说,是不会在职场存活下来的,电影里就形成强烈反差感,荒诞可笑,却也映射着在职场里靠关系进去的人,不仅不会做事,还会被像财神爷一样供着,具有讽刺意味。


作为在职场8年,5年新媒体经验的我,曾也进入过本地500人的新媒体大公司。


在原本以为进的“大厂”里人人都是专家,能力强,但当你一进去之后,会发现和影片中的体系差不多,高层的就是一帮混酒局的,中层大部分靠的是拍马屁和吹水,真正做事的可能就是底层的基础职员。


但作为底层社畜,尽管看到了职场里的bug和荒谬,就算看清了现实,也逃不过压榨和背锅,只能一边吐槽一边苦干,谁叫自己的饭碗在别人手里呢。


但我觉得不管做任何工作,也不是一味的“隐忍”,现在也不是只靠上班才能赚钱的时代,相比赚钱,我觉得人一定要记得最初的自己。


5


时代的列车呼啸向前,车轮地下总得有人增加摩擦力”,这句话很扎心却现实。


哪怕口罩问题已经结束,现在仍然有很多知名大厂在不断裁员。


打工的怕没工打,没打工的找不到工打,每天大家都活在不稳定的气氛中,就像《年会》里唱的那句:“你是不是也像我,在裁员中忐忑。”


大环境的齿轮一旦转动,谁也逃脱不了被碾压的命运,只能眼睁睁看着事情发生无力改变,才是最可悲的事情,只有在变化中才能求解。


随着知识和经验buff的叠加,我们所能做就是预测时代的节奏,在每一个车轮想要来碾压我们之前,增加自己的动力,去跑赢这辆列车。



END


作者:李猫妮
来源:mp.weixin.qq.com/s/2k6GdooHJnlUOHnckyhFZg

收起阅读 »

前端无感知刷新token & 超时自动退出

web
前端无感知刷新token&超时自动退出 一、token的作用 因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。 以oauth2.0授权码模式为例: 每次请求资...
继续阅读 »

前端无感知刷新token&超时自动退出


一、token的作用


因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。


以oauth2.0授权码模式为例:


oauth2授权码模式.png


每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。


刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。


二、无感知刷新token方案


2.1 刷新方案


当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。


如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。


2.2 原生AJAX请求


2.2.1 http工厂函数


function httpFactory({ method, url, body, headers, readAs, timeout }) {
   const xhr = new XMLHttpRequest()
   xhr.open(method, url)
   xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60

   if(headers){
       forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
  }
   
   const HTTPPromise = new Promise((resolve, reject) => {
       xhr.onload = function () {
           let response;

           if (readAs === 'json') {
               try {
                   response = JSONbig.parse(this.responseText || null);
              } catch {
                   response = this.responseText || null;
              }
          } else if (readAs === 'xml') {
               response = this.responseXML
          } else {
               response = this.responseText
          }

           resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
      }

       xhr.onerror = function () {
           reject(xhr)
      }
       xhr.ontimeout = function () {
           reject({ ...xhr, isTimeout: true })
      }

       beforeSend(xhr)

       body ? xhr.send(body) : xhr.send()

       xhr.onreadystatechange = function () {
           if (xhr.status === 502) {
               reject(xhr)
          }
      }
  })

   // 允许HTTP请求中断
   HTTPPromise.abort = () => xhr.abort()

   return HTTPPromise;
}

2.2.2 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

// 存放因token过期而失败的请求
let requests = []

function httpRequest(config) {
   let abort
   let process = new Promise(async (resolve, reject) => {
       const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
       abort = request.abort
       
       try {                            
           const { status, response, getResponseHeader } = await request

           if(status === 401) {
               try {
                   if (!isRefreshing) {
                       isRefreshing = true
                       
                       // 刷新token
                       await refreshToken()

                       // 按顺序重新发起所有失败的请求
                       const allRequests = [() => resolve(httpRequest(config)), ...requests]
                       allRequests.forEach((cb) => cb())
                  } else {
                       // 正在刷新token,将请求暂存
                       requests = [
                           ...requests,
                          () => resolve(httpRequest(config)),
                      ]
                  }
              } catch(err) {
                   reject(err)
              } finally {
                   isRefreshing = false
                   requests = []
              }
          }                        
      } catch(ex) {
           reject(ex)
      }
  })
   
   process.abort = abort
   return process
}

// 发起请求
httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })

2.3 Axios 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

let requests: ReadonlyArray<(config: any) => void> = []

// 错误响应拦截
axiosInstance.interceptors.response.use((res) => res, async (err) => {
   if (err.response && err.response.status === 401) {
       try {
           if (!isRefreshing) {
               isRefreshing = true
               // 刷新token
               const { access_token } = await refreshToken()

               if (access_token) {
                   axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;

                   requests.forEach((cb) => cb(access_token))
                   requests = []

                   return axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${access_token}`,
                      },
                  })
              }

               throw err
          }

           return new Promise((resolve) => {
               // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
               requests = [
                   ...requests,
                  (token) => resolve(axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${token}`,
                      },
                  })),
              ]
          })
      } catch (e) {
           isRefreshing = false
           throw err
      } finally {
           if (!requests.length) {
               isRefreshing = false
          }
      }
  } else {
       throw err
  }
})

三、长时间无操作超时自动退出


当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。


3.1 操作事件


操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。


特殊事件:某些耗时的功能,比如上传、下载等。


3.2 方案


用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。


在 localStorage 存入两个字段:


名称类型说明说明
lastActiveTimestring最后一次触发操作事件的时间戳
activeEventsstring[ ]特殊事件名称数组

当有操作事件时,将当前时间戳存入 lastActiveTime。


当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。


设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。


3.3 代码实现


const LastTimeKey = 'lastActiveTime'
const activeEventsKey = 'activeEvents'
const debounceWaitTime = 2 * 1000
const IntervalTimeOut = 1 * 60 * 1000

export const updateActivityStatus = debounce(() => {
   localStorage.set(LastTimeKey, new Date().getTime())
}, debounceWaitTime)

/**
* 页面超时未有操作事件退出登录
*/

export function timeout(keepTime = 60) {
   document.addEventListener('mousedown', updateActivityStatus)
   document.addEventListener('mouseover', updateActivityStatus)
   document.addEventListener('wheel', updateActivityStatus)
   document.addEventListener('keydown', updateActivityStatus)

   // 定时器
   let timer;

   const doTimeout = () => {
       timer && clearTimeout(timer)
       localStorage.remove(LastTimeKey)
       document.removeEventListener('mousedown', updateActivityStatus)
       document.removeEventListener('mouseover', updateActivityStatus)
       document.removeEventListener('wheel', updateActivityStatus)
       document.removeEventListener('keydown', updateActivityStatus)

       // 注销token,清空session,回到登录页
       logout()
  }

   /**
    * 重置定时器
    */

   function resetTimer() {
       localStorage.set(LastTimeKey, new Date().getTime())

       if (timer) {
           clearInterval(timer)
      }

       timer = setInterval(() => {
           const isSignin = document.cookie.includes('access_token')
           if (!isSignin) {
               doTimeout()
               return
          }

           const activeEvents = localStorage.get(activeEventsKey)
           if(!isEmpty(activeEvents)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }
           
           const lastTime = Number(localStorage.get(LastTimeKey))

           if (!lastTime || Number.isNaN(lastTime)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }

           const now = new Date().getTime()
           const time = now - lastTime

           if (time >= keepTime) {
               doTimeout()
          }
      }, IntervalTimeOut)
  }

   resetTimer()
}

// 上传操作
function upload() {
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, [...current, 'upload'])
   ...
   // do upload request
   ...
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
}

作者:ww_怒放
来源:juejin.cn/post/7320044522910269478
收起阅读 »

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

2024年,何去何从

如果生命只有35岁,我大抵可以活的绚烂放肆。 可是为了活到70岁,我不得不过得趋炎附势、唯唯诺诺。 2023年12月14日中午。望着公司窗外小河上清理水藻的船工,突然觉得人生好落寞。人生不知不觉已经过去了33个年头,生活过的一团糟,每每空闲就会很迷茫,工作...
继续阅读 »

如果生命只有35岁,我大抵可以活的绚烂放肆。


可是为了活到70岁,我不得不过得趋炎附势、唯唯诺诺。



89977a3eb68cfc3a82b415c9e006ec4.jpg


2023年12月14日中午。望着公司窗外小河上清理水藻的船工,突然觉得人生好落寞。人生不知不觉已经过去了33个年头,生活过的一团糟,每每空闲就会很迷茫,工作中也不知道未来方向在何方。似乎生活走到了一个十字路口,下一步的迈出千头万绪,让人举步不前。


关于读书


eade0765b64b6e958335725416a504b.jpg
最近董宇辉小作文事件在互联网上闹得沸沸扬扬。让我重新审视关于读书人这个称谓。董宇辉的出口成章,辞藻华丽,仿若腹有诗书气自华就是为他而写的一般。之前有一段时间,一直会保持每天至少抽出来半小时读书的习惯,这期间也读了很多好书,也推荐给朋友很多好书,俨然有一种自己是读者的错觉。但是,好景不长,慢慢的读书的习惯在各种乱七八糟的生活琐碎中消磨的也不多了。


2024年关于读书目标,希望自己能读完8本有意思的书籍吧。


以下推荐一些我往年读书挺有意思的书。(我读书有个特点,不会专门为了要从书中获取什么而读书,我单纯可能就是觉得这本书有趣,就会阅读,仁者见仁智者见智,推荐的不喜欢勿喷)



  • 我的二本学生

  • 焦虑的人

  • 时间的礼物

  • 牧羊少年的奇幻旅行

  • 清单革命:如何持续、正确、安全的把事情做好

  • 大雪将至

  • 无人生还

  • 古董局中局(全集)

  • 长安十二时辰

  • 罗布泊之咒


关于学习


作为程序员,最重要的事情,其实就是终身学习。


而我一直认为,一个人活在世上和其他人最大的差异变化,就是在于不断的学习。而我认为学习不光是对于书本中的知识的学习,更是对于人生百态、人情世故的学习。通过不断的学习,让自己的棱角变得圆滑,让自己的短板变的不那么明显。大白话就是通过不断的学习打磨,让自己变的装起来,活的不那么赤裸裸。


如果你觉得这个词,你认知中还是用褒贬来分辨,对于事物还是一味用对错来分辨。那么我觉得应该去学习,通过不断的书本阅读、不断的人情世故的打磨,让自己起来。


你可能不认同,但是你不得不承认,这个社会就是由人情世故组装而成的。你的不断学习是伪装也是武装,让你圈子变得不同。


学习和阅读是一辈子的事。额....我怀念单纯的我


2024学习方面,我个人计划主要是个方面。



  • Python爬虫 & js反编译深入

  • Android jetpack搞一搞

  • 单词背起来

  • 阅读习惯捡起来


关于工作


image.png


这个不重要。按部就班来~


作者:王先生技术栈
来源:juejin.cn/post/7312749480674574372
收起阅读 »

爆料 iPhone 史上最大的漏洞,你中招了吗

卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多 最近 iPhone 因为遭遇史上最复杂攻击,而登上了热搜,卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多,如果你收到了 iPh...
继续阅读 »

卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多



最近 iPhone 因为遭遇史上最复杂攻击,而登上了热搜,卡巴斯基的研究人员表示,黑客利用了 iPhone 极其隐蔽的软硬件漏洞,持续攻击了四年多,如果你收到了 iPhone 的安全补丁提示,那么赶快升级吧。


OpenAI 科学家 Andrej Karpathy 惊讶地表示:这绝对是我们迄今为止所见过的最为复杂的攻击链。从本次攻击的复杂程度来看,一次黑客攻击同时使用 4 个零日漏洞(也就是未被发现且无有效防范措施的漏洞)是 "极其罕见的",只有历史上著名的 "震网" 病毒攻击伊朗纳坦兹核工厂事件能达到这个级别(当时共利用 7 个漏洞,其中 4 个为零日漏洞)。


这次黑客的攻击手段非常复杂,攻击者只需向用户的 iPhone 发送一段恶意 iMessage 文本,无需用户点击或下载任何内容,就可以在用户不知情的情况下,获取到 iPhone 的最高级别 Root 权限,这应该是利用 Mac 系统大概 10 年都没有修复的一个字体的漏洞。



"iMessage 信息" 是苹果手机 "信息" 中的一种通信方式,可以向其他 iOS 设备、iPadOS 设备、Mac 电脑和 Apple Watch 发送文字、图片、视频和音乐等信息



当获取到 iPhone 最高级别 Root 权限,攻击者将能够在 iPhone 上安装恶意软件(间谍软件),从而收集诸如联系人、消息和位置数据等敏感信息,并传输到攻击者控制的服务器。


但是如果想成功利用这个漏洞,必须对 iPhone 最底层的机制有深入的了解,但是 iPhone 不是开源的系统,所以除了 iPhone 和 ARM 的人,几乎不会有其他人知道这个漏洞的存在。


这次这个漏洞的攻击代码,粗估高达数万行代码,写的非常的精巧复杂,这么高价值的漏洞,不会对个人进行打击,应该是针对非常重要的人物。


比如 2021 年 7 月,以色列发生了一起类似的事件,代号为 "飞马" 间谍软件攻击事件,它可以秘密安装在运行大多数版本的 iOS 和 Android 的手机(和其他设备) 上,这次的攻击持续了很多年,从 2014 年开始,一直持续到 2021 年 7 月媒体曝光之时,监听对象都是非常重要的人物。


但是如果黑客将这次的攻击代码开源,那么很多人都可以利用这个漏洞为所欲为了,造成的结果就是无差别攻击,这样对我们普通人就危险了,如果你收到了 iPhone 的安全补丁提示,那么赶快升级,转发给身边的朋友,提高警惕吧


这些年来无论在 Android 还是 iPhone, 都发现了相应的漏洞,iPhone 号称史上最安全的操作系统,都出现了这么严重的漏洞,这也再次说明了,无论多好的软件系统,都有不可避免的漏洞,一定会被人攻击。


比如在 2023 年 Android 手机上也被暴露一个漏洞,虽然这个漏洞很早被 Google 修复了,但是并不是所有人都会升级到新版本系统,所以某些大厂,利用这个被暴露出来漏洞,获取到 Android 手机上最高级别 Root 权限,攻击普通用户,控制他们的手机,获取用户大量的私人信息。而且这次攻击也持续了很多年,被曝光之时引起轩然大波,但是在其强大的财力和公关的操作下,事情很快平息了。


我一直认为技术应该服务于用户,而不是想方设法的利用公开的漏洞窃听用户的私人信息,去推送一些定制化私人广告。


全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!


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

年底喜提大礼包,分享一下日常

写在前面 元旦前喜提大礼包,因为公司的骚操作越来越多,福利越来越少,通勤时间太久等诸多原因所以主动要了裁员名额,现在给大家分享一下这几天的日常和心态吧。 找工作 看了一下最近的行情,基本是属于失业了,从离职到现在有差不多2周了,这2周里刷了一下Boss和拉钩,...
继续阅读 »

写在前面


元旦前喜提大礼包,因为公司的骚操作越来越多,福利越来越少,通勤时间太久等诸多原因所以主动要了裁员名额,现在给大家分享一下这几天的日常和心态吧。


找工作


看了一下最近的行情,基本是属于失业了,从离职到现在有差不多2周了,这2周里刷了一下Boss和拉钩,挂出来的职位倒是不少,我也投了几份简历,无一例外全都石沉大海,看来只有放个寒假过完年再说了。最近也没怎么刷面经,先好好的休息一段时间吧。


image.png


日常


这两周也没有出去玩之类的,因为老婆还没有放假,基本都是宅在家里,做做饭,玩玩游戏,写写私活,分享一下我家2只可爱的猫猫


eb4561e2e29b304bf091d2638caf153.jpg


展望


希望过完年可以找到满意的工作吧,实在不行也只能去外包了,最近也准备换换赛道,尝试一下自媒体。


写在后面


虽然失业了,但是心态上还好,有一点焦虑但是不多,可能是因为写私活占了一部分时间,没空去胡思乱想吧,希望各位待业大佬都能放平心态,好好提升自己,加油。


作者:hahayq
来源:juejin.cn/post/7320037969980702761
收起阅读 »

年会了,公司想要一个离线PC抽奖应用

web
年会了,公司想要一个离线PC抽奖应用 背景 公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC...
继续阅读 »

年会了,公司想要一个离线PC抽奖应用


封面截图.png


背景


公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC抽奖应用,所有就有了这么一个主题。


程序需求


以下是领导从其他地方复制粘贴过来的,就是想实现类似的效果而已。



  • 1、支持数字、字母、手机号、姓名部门+姓名、编号、身-份-证号等任意组合的抽奖形式。

  • 2、支持名单粘贴功能,从EXCEL、WORD、TXT等任何可以复制的地方复制名单数据内容,粘贴至抽奖软件中作为名单使用,比导入更方便。

  • 3、支持标题、副标题、奖项提示信息、奖品图片等都可以通过拖拽更改位置。

  • 4、支持内定指定中奖者。

  • 5、支持万人抽奖,非常流畅,中奖机率一致,保证公平性。

  • 6、支持中奖不重复,软件自动排除已中奖人员,每人只有一次中奖机会不会出现重复中奖。

  • 7、支持临时追加奖项、补奖等功能支持自定义公司名称、自定义标题。

  • 8、背景图片,音乐等。

  • 9、支持抽奖过程会自动备份中奖名单(不用担心断电没保存中奖名单)。

  • 10、支持任意添加奖项、标题文字奖项名额,自由设置每次抽奖人数设置不同的字体大小和列数。

  • 11、支持空格或回车键抽奖。

  • 12、支持临时增加摇号/抽奖名单,临时删掉不在场人员名单。


目前未实现的效果


有几个还没实现的



  1. 关于人员信息的任意组合抽奖形式,这边只固定了上传模板的表头,需要组合只能通过改excel的内容。

  2. 对于临时不在场名单,目前只能通过改excel表再上传才能达到效果。


技术选型


由于给的时间不多,能有现成的最好;最终选择下面的开源项目进行集成和修改。


说明:由于之前没看到有electron-vite-vue这个项目,所有自己粗略了用vue3+vite+electron开发了 抽奖程序 , 所以现在就是迁移项目的说明。


github开源项目



根据仓库的说明运行起来


动画.gif


修改web端代码并集成到electron


I 拆分页面


组件说明拼图.png


II 补充组件


​ 本人根据自己想法加了背景图片、奖品展示、操作按钮区、展示全部中奖人员名单这几个组件以及另外9个弹窗设置组件。


III 页面目录结构


目录结构.png

IV 最后就是对开源的网页抽奖项目进行大量的修改了,这里就不详细说了;因为变化太多了,一时半会想不起来改了什么。


迁移项目


I 迁移静态资源


静态资源.png


​ 关于包资源说明,这边因为要做离线的软件,所以我把固定要使用的包保存到本地了;


1. 引入到index.html中

引入资源.png


2. 引入图片静态资源

功能代码调整.png


II 迁移electron代码


说明:由于我之前写的一版代码是用js而不是ts,如果一下子全改为ts需要一些时间;所以嫌麻烦,我直接引用js文件了,后期有时间可以再优化一下。


功能代码调整.png




  1. 这时候先运行一下,看下有没有问题



​ 问题一:


问题一.png


​ 这个是因为 我之前的项目一直是用require 引入的;所以要把里面用到require都改为import引入方式;(在preload.ts里面不能用ESM导入的形式,会报语法错误,要用回require导入)


​ 问题二:


问题二.png


​ __dirname不是ESM默认的变量;改为


import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url))

III 迁移前端代码



  • 目录说明


前端功能代码.png



  • 然后一顿复制粘贴,运行,最后报错;


问题三.png
按提示来改 如下:


修改1.png



  • 问题2:资源报错
    资源报错.png
    修复:


资源变化.png



  • 接下来运行看下是否有问题;
    抽奖运行动画.gif
    ​ 运行成功

  • 下一步试一下功能


功能执行动画.gif
​ 功能报错了



  • 看后台错误打印并修复问题
    保存位置错误.png
    修改:
    路径保存源头.png

  • 再次尝试功能 - 成功
    功能执行动画2.gif


IV 一个流程下来


待使用-删除帧后-运行抽奖一个流程动画-gif.gif


打包安装运行


I 运行“npm run build”之后 报错了


打包-js报错.png
这里再次说明一下;由于本人懒得把原本js文件的代码 改为ts;要快速迁移项目 所以直接使用了js;导致打包报错了,所以需要再 tsconfig.json配置一下才行:


  "compilerOptions": {
"allowJs": true // 把这段加上去
},

II 图标和应用名称错误


default Electron icon is used  reason=application icon is not set
building block map blockMapFile=release\28.0.0\YourAppName-Windows-28.0.0-Setup.exe.blockmap

找到打包的配置文件(electron-builder.json5)进行修改:


1. 更改应用名称
"productName": "抽奖程序",

2. 添加icon图标
"win": {
"icon": "electron/controller/data/img/lottery_icon.ico", // ico保存的位置
},

III 打包后运行;资源路径报错了


打包后资源报错.png
打包后资源路径查询不到.png
由于上面的原因,需要把程序涉及读写的文件目录暴露出来;


1. 在构建配置中加入如下配置,将应用要读写的文件目录暴露出来
"extraResources": [
{
"from": "electron/assets",
"to": "assets"
}
],

剩下的就是要重新调整打包后的代码路径了,保证能够找到读写路径;
路径查找纠正.png


最后打包成功,运行项目


删除帧后-一个完整的流程-gif.gif


总结: 主打的要快速实现,所以这个离线pc抽奖程序还有很多问题,希望大家多多包容;


最后附上github地址:github.com/programbao/…
欢迎大家使用


作者:宝programbao
来源:juejin.cn/post/7319795736153210895
收起阅读 »

iOS 组件开发教程——手把手轻松实现灵动岛

1、先在项目里创建一个Widget Target2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivi...
继续阅读 »

1、先在项目里创建一个Widget Target


2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。


3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivities,并将其布尔值设置为 YES。

4、我们创建一个IMAttributes,

struct IMAttributes: ActivityAttributes {
public typealias IMStatus = ContentState

public struct ContentState: Codable, Hashable {
var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

5、灵动岛界面配置

struct IMActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: IMAttributes.self) { context in
// 创建显示在锁定屏幕上的演示,并在不支持动态岛的设备的主屏幕上作为横幅。
// 展示锁屏页面的 UI

} dynamicIsland: { context in
// 创建显示在动态岛中的内容。
DynamicIsland {
//这里创建拓展内容(长按灵动岛)
DynamicIslandExpandedRegion(.leading) {
Label(context.state.callName, systemImage: "person")
.font(.caption)
.padding()
}
DynamicIslandExpandedRegion(.trailing) {
Label {
Text(timerInterval: context.state.callingTimer, countsDown: false)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
.font(.caption2)
} icon: {
Image(systemName: "timer")
}
.font(.title2)
}
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.callName) 正在通话中...")
.lineLimit(1)
.font(.caption)
.foregroundColor(.secondary)
}

}
//下面是紧凑展示内容区(只展示一个时的视图)
compactLeading: {
Label {
Text(context.state.callName)

} icon: {
Image(systemName: "person")
}
.font(.caption2)
} compactTrailing: {
Text(timerInterval: context.state.callingTimer, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
}
//当多个Live Activities处于活动时,展示此处极小视图
minimal: {
VStack(alignment: .center) {
Image(systemName: "person")


}
}
.keylineTint(.accentColor)
}
}
}

6、在需要的地方启动的地方调用,下面是启动灵动岛的代码

        let imAttributes = IMAttributes(callName: "wqd", imageStr:"¥99", callingTimer: Date()...Date().addingTimeInterval(0))

//初始化动态数据
let initialContentState = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

do {
//启用灵动岛
//灵动岛只支持Iphone,areActivitiesEnabled用来判断设备是否支持,即便是不支持的设备,依旧可以提供不支持的样式展示
if #available(iOS 16.1, *) {
if ActivityAuthorizationInfo().areActivitiesEnabled == true{

}
} else {
// Fallback on earlier versions
}
let deliveryActivity = try Activity<IMAttributes>.request(
attributes: imAttributes,
contentState: initialContentState,
pushType: nil)
//判断启动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
//不是每次启动都会成功,当已经存在多个Live activity时会出现启动失败的情况
if deliveryActivity.activityState == .active{
_ = deliveryActivity.pushToken
}
// deliveryActivity.pushTokenUpdates //监听token变化
print("Current activity id -> \(deliveryActivity.id)")
} catch (let error) {
print("Error info -> \(error.localizedDescription)")
}
6.此处只有一个灵动岛,当一个项目有多个灵动岛时,需要判断更新对应的activity

func update(name:String) {
Task {

let updatedDeliveryStatus = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

for activity in Activity<IMAttributes>.activities{
await activity.update(using: updatedDeliveryStatus)
}
}
}

7、停止灵动岛

func stop() {
Task {
for activity in Activity<IMAttributes>.activities{
await activity.end(dismissalPolicy: .immediate)
}
}
}


收起阅读 »

被裁员后,去送外卖跑滴滴行得通吗?

一 近年来,职场的裁员和降薪已经屡见不鲜,不少同事和朋友领了“大红包”。 有的在疯狂找工作,有的暂时摆烂下来。 打开BOSS招聘,上面的岗位还是很多啊,需求量还是很大啊! 但是不好意思,其实和大部分人关系不是很大。 我们发现一个问题,大家都说找不到工作,但是企...
继续阅读 »


近年来,职场的裁员和降薪已经屡见不鲜,不少同事和朋友领了“大红包”。


有的在疯狂找工作,有的暂时摆烂下来。


打开BOSS招聘,上面的岗位还是很多啊,需求量还是很大啊!


但是不好意思,其实和大部分人关系不是很大。


我们发现一个问题,大家都说找不到工作,但是企业又在抱怨苦苦招不到人。


什么原因呢?


究其原因,中低端岗位虽然多,但是人数堪比考公务员,造成了狼多肉少的现象,如果你的牙齿不够长,不够硬,那么挺难,运气好的话可能能碰到,但是可能是生病的猎物。


高端岗位因为对薪资进行压缩,那些精英不想降低标准去,而精英只占社会群体的一小撮,大多都在寻找更好的机会,或者落差不大的机会。


所以就传出:企业招不到人,大把人找不到工作的现象。


但是很显然,我们大部分人很难突进成精英,就像码农大多很难成为CTO,架构师,领导者,运营大多无法成为总监......


我们大多数人注定就是一颗螺丝钉。


这是我们大部人的宿命,这是必须得承认的。


不少朋友说:妈的,实在不行,老子就去送外卖,去开滴滴,没什么大不了的!


貌似大家都认为这是自己职业的底线,把送外卖和开网约车作为人生的兜底方案。


但是不好意思,外卖,网约车也不是什么人都能去干的,现在门槛也高了,能赚到的越来越少。


你可能看到视频中外卖月薪超过2W,但是你不可能不知道这钱是怎么赚来的。


可以用那个梗来形容:多吗?拿命换的。


其实另外一个现实的问题是,想玩命,想卷也没机会啊,这绝非贩卖焦虑,这是铁打的事实!



在我读六年级的时候。


我的同桌是一个女同学,她父母都是出租车司机,说实话,那会儿,我可觉得出租车司机比编制牛逼多了。


所以她和我说话总是提高半个调,时不时言语中带出几个词,表明她父母是出租车司机。


那会儿,没有滴滴,没有曹操,没有T3,没有智能手机,没有跑黑车的。


所以,他们出租车司机吃得油光满面,合不拢嘴。


但是时代变了,现在你打一个出租车,和司机聊上两句,他就差点扑倒你怀里哭了起来。


打网约车也是如此,很多司机在你下车时,还客客气气对你说:可以给我个好评吗,谢谢你了。


为啥要好评呢,数据啊,数据好看,给你推的单子就多啊。


前几天看了一个视频,一个女网约车司机说:自己开了一两个月了,单子还是那么难接,再这样下去,吃不起饭了。


还别说,这些平台依旧将司机分为三六九等,等级越高,自然单子就多,等级低的,慢慢来吧。


也怪不了平台,大家都在这个城市里,单子就这么多,加入的司机越来越多,如果大家都是公平去抢单,显然不符合商业的发展。


除了各种平台的竞争,在出行方式上也是卷得一比。


刚开始是共享自行车,再到电瓶车,刚开始要押金,后面我干脆直接不要押金。


这还不够,我还送,一个月十来块钱,我可以让你把大腿肌肉练强壮,链条干起火花。


对于大城市中的打工人,距离远我选择地铁,距离近我选择自行车,小城市里面,我更愿意选择公交和共享电动车。


难啊,出租车司机哭生不逢时,网约车司机拍拍大腿:这TM就是人生!


......



外卖就好搞吗?


我一个朋友,多年前他是一个外卖资深玩家,是城市里面的蝙蝠侠,闭着眼睛都能找路,眼睛一眨就把外卖送到顾客手里。


五六年前,他在一个四五线线城市一个月都能赚取可观的收入。


2023年下半年,他又重新加入了外卖大军,但是干了四五个月,他顶不住了,直接走人,他当时还是在东莞送,东莞的人口不少哦。


我问他为啥不干了,他无奈说到:现在这个行业,狗看了都摇头。


高单价的单子抢不到,能抢到的单子价格又低。


一天跑200块钱都挺难。


可能你不信,但是这就是事实。


在东莞的对面,那是深圳,年轻人梦想的起点,无数人来到深圳,极少的人确实赚到钱了,但是更多的人都是处于深圳赚钱深圳花,一分别想带回家的状态。


这里的人多,如果肯干,加上有一定的策略,那么一个月跑万把块是可以的,但是会特别累。


更多的人其实是破不了万的。


除了行情问题,还要面对巨大的身体和心理压力,价值送外卖是一件比较危险的事。


很多人穿上黄袍不久,扛不住了,只能脱下。


外卖是有门槛的,它肯定会比你现在的工作辛苦得多,把它作为兜底方案,这是不现实的。


特别是现在就业形式的严峻,更多的人都加入这个行业,竞争大得不行,所以想从里面赚钱也是挺难得。



最后。


谈一下一个现实的问题。


有力无处使,有才无数施,干了活不重要,重要的是要有运气拿钱!


在社会劳动力过剩的形势下,个人的才能其实没多大用处,除非是大才,普才的话只能在夹缝中苟延残喘。


一网友说:躺了很久,发现996真的是福报,在这个畸形的环境里,有钱挣、有活干、有苦吃、有罪受真是一大幸事!

我们大多数人是讨厌职场中的奋斗逼和卷狗的,但是当现实当头一棒的时候,估计自己卷得比别人还厉害。


这其实和康风险能力有关,普通家庭,普通收入的工薪阶层,收入完全依赖于工资,但是要还房贷,车贷,养娃,所以基本上收入和支出持平。


那失业就是最可怕的事情。


现在市面上处于待业的人还是比较多,有力无处使。


因为市场上的业务基本处于平缓甚至下滑的状态,部分处于直线上升的业务自己又去不了。


所以难啊。


这样的形势下,我们普通人又该何去何从?


诸君怎么看?


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

前端服务框架调研:Next.js、Nuxt.js、Nest.js、Fastify

web
概述 这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。 最终选取了以下具有代表性的框架: Next.js、Nuxt.js:它们是分别与...
继续阅读 »

概述


这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。


最终选取了以下具有代表性的框架:



  • Next.js、Nuxt.js:它们是分别与特定前端技术 React、Vue 绑定的前端应用开发框架,有一定的相似性,可以放在一起进行调研对比。

  • Nest.js:是“Angular 的服务端实现”,基于装饰器。可以使用任何兼容的 http 提供程序,如 Express、Fastify 替换底层内核。可用于 http、rpc、graphql 服务,对提供更多样的服务能力有一定参考价值。

  • Fastify:一个使用插件模式组织代码且支持并基于 schema 做了运行效率提升的比较纯粹的偏底层的 web 框架。


Next.js、Nuxt.js


这两个框架的重心都在 Web 部分,对 UI 呈现部分的代码的组织方式、服务器端渲染功能等提供了完善的支持。



  • Next.js:React Web 应用框架,调研版本为 12.0.x。

  • Nuxt.js:Vue Web 应用框架,调研版本为 2.15.x。


功能


首先是路由部分:



  • 页面路由:

    • 相同的是两者都遵循文件即路由的设计。默认以 pages 文件夹为入口,生成对应的路由结构,文件夹内的所有文件都会被当做路由入口文件,支持多层级,会根据层级生成路由地址。同时如果文件名为 index 则会被省略,即 /pages/users 和 /pages/users/index 文件对应的访问地址都是 users。

    • 不同的是,根据依赖的前端框架的不同,生成的路由配置和实现不同:

      • Next.js:由于 React 没有官方的路由实现,Next.js 做了自己的路由实现。

      • Nuxt.js:基于 vue-router,在编译时会生成 vue-router 结构的路由配置,同时也支持子路由,路由文件同名的文件夹下的文件会变成子路由,如 article.js,article/a.js,article/b.js,a 和 b 就是 article 的子路由,可配合 组件进行子路由渲染。





  • api 路由:

    • Next.js:在 9.x 版本之后添加了此功能的支持,在 pages/api/ 文件夹下(为什么放在pages文件夹下有设计上的历史包袱)的文件会作为 api 生效,不会进入 React 前端路由中。命名规则相同,pages/api/article/[id].js -> /api/article/123。其文件导出模块与页面路由导出不同,但不是重点。

    • Nuxt.js:官方未提供支持,但是有其他实现途径,如使用框架的 serverMiddleware 能力。



  • 动态路由:两者都支持动态路由访问,但是命名规则不同:

    • Next.js:使用中括号命名,/pages/article/[id].js -> /pages/article/123。

    • Nuxt.js:使用下划线命名,/pages/article/_id.js -> /pages/article/123。



  • 路由加载:两者都内建提供了 link 类型组件(LinkNuxtLink),当使用这个组件替代 标签进行路由跳转时,组件会检测链接是否命中路由,如果命中,则组件出现在视口后会触发对对应路由的 js 等资源的加载,并且点击跳转时使用路由跳转,不会重新加载页面,也不需要再等待获取渲染所需 js 等资源文件。

  • 出错兜底:两者都提供了错误码响应的兜底跳转,只要 pages 文件夹下提供了 http 错误码命名的页面路由,当其他路由发生响应错误时,就会跳转到到错误码路由页面。


在根据文件结构生成路由配置之后,我们来看下在代码组织方式上的区别:



  • 路由组件:两者没有区别,都是使用默认导出组件的方式决定路由渲染内容,React 导出 React 组件,Vue 导出 Vue 组件:

    • Next.js:一个普普通通的 React 组件:
      export default function About() {
      return <div>About usdiv>
      }


    • Nuxt.js:一个普普通通的 Vue 组件:







在编译构建方面,两者都是基于 webpack 搭建的编译流程,并在配置文件中通过函数参数的方式暴露了 webpack 配置对象,未做什么限制。其他值得注意的一点是 Next.js 在 v12.x.x 版本中将代码压缩代码和与原本的 babel 转译换为了 swc,这是一个使用 Rust 开发的更快的编译工具,在前端构建方面,还有一些其他非基于 JavaScript 实现的工具,如 ESbuild。


在扩展框架能力方面,Next.js 直接提供了较丰富的服务能力,Nuxt.js 则设计了模块和插件系统来进行扩展。


Nest.js


Nest.js 是“Angular 的服务端实现”,基于装饰器。Nest.js 与其他前端服务框架或库的设计思路完全不同。我们通过查看请求生命周期中的几个节点的用法来体验下 Nest.js 的设计方式。


先来看下 Nest.js 完整的的生命周期:



  1. 收到请求

  2. 中间件

    1. 全局绑定的中间件

    2. 路径中指定的 Module 绑定的中间件



  3. 守卫

    1. 全局守卫

    2. Controller 守卫

    3. Route 守卫



  4. 拦截器(Controller 之前)

    1. 全局

    2. Controller 拦截器

    3. Route 拦截器



  5. 管道

    1. 全局管道

    2. Controller 管道

    3. Route 管道

    4. Route 参数管道



  6. Controller(方法处理器)

  7. 服务

  8. 拦截器(Controller 之后)

    1. Router 拦截器

    2. Controller 拦截器

    3. 全局拦截器



  9. 异常过滤器

    1. 路由

    2. 控制器

    3. 全局



  10. 服务器响应


可以看到根据功能特点拆分的比较细,其中拦截器在 Controller 前后都有,与 Koa 洋葱圈模型类似。


功能设计


首先看下路由部分,即最中心的 Controller:



  • 路径:使用装饰器装饰 @Controller 和 @GET 等装饰 Controller 类,来定义路由解析规则。如:
    import { Controller, Get, Post } from '@nestjs/common'

    @Controller('cats')
    export class CatsController {
    @Post()
    create(): string {
    return 'This action adds a new cat'
    }

    @Get('sub')
    findAll(): string {
    return 'This action returns all cats'
    }
    }

    定义了 /cats post 请求和 /cats/sub get 请求的处理函数。

  • 响应:状态码、响应头等都可以通过装饰器设置。当然也可以直接写。如:
    @HttpCode(204)
    @Header('Cache-Control', 'none')
    create(response: Response) {
    // 或 response.setHeader('Cache-Control', 'none')
    return 'This action adds a new cat'
    }


  • 参数解析:
    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat'
    }


  • 请求处理的其他能力方式类似。


再来看看生命周期中其中几种其他的处理能力:



  • 中间件:声明式的注册方法:
    @Module({})
    export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer
    // 应用 cors、LoggerMiddleware 于 cats 路由 GET 方法
    .apply(LoggerMiddleware)
    .forRoutes({ path: 'cats', method: RequestMethod.GET })
    }
    }


  • 异常过滤器(在特定范围捕获特定异常并处理),可作用于单个路由,整个控制器或全局:
    // 程序需要抛出特定的类型错误
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)

    // 定义
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()

    response
    .status(status)
    .json({
    statusCode: status,
    timestamp: new Date().toISOString(),
    path: request.url,
    })
    }
    }
    // 使用,此时 ForbiddenException 错误就会被 HttpExceptionFilter 捕获进入 HttpExceptionFilter 处理流程
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create() {
    throw new ForbiddenException()
    }


  • 守卫:返回 boolean 值,会根据返回值决定是否继续执行后续声明周期:
    // 声明时需要使用 @Injectable 装饰且实现 CanActivate 并返回 boolean 值
    @Injectable()
    export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
    return validateRequest(context);
    }
    }

    // 使用时装饰 controller、handler 或全局注册
    @UseGuards(new AuthGuard())
    async create() {
    return 'This action adds a new cat'
    }


  • 管道(更侧重对参数的处理,可以理解为 controller 逻辑的一部分,更声明式):

    1. 校验:参数类型校验,在使用 TypeScript 开发的程序中的运行时进行参数类型校验。

    2. 转化:参数类型的转化,或者由原始参数求取二级参数,供 controllers 使用:


    @Get(':id')
    findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
    // 使用 id param 通过 UserByIdPipe 读取到 UserEntity
    return userEntity
    }



我们再来简单的看下 Nest.js 对不同应用类型和不同 http 提供服务是怎样做适配的:



  • 不同应用类型:Nest.js 支持 Http、GraphQL、Websocket 应用,在大部分情况下,在这些类型的应用中生命周期的功能是一致的,所以 Nest.js 提供了上下文类 ArgumentsHostExecutionContext,如使用 host.switchToRpc()host.switchToHttp() 来处理这一差异,保障生命周期函数的入参一致。

  • 不同的 http 提供服务则是使用不同的适配器,Nest.js 的默认内核是 Express,但是官方提供了 FastifyAdapter 适配器用于切换到 Fastify。


Fastify


有这么一个框架依靠数据结构和类型做了不同的事情,就是 Fastify。它的官方说明的特点就是“快”,它提升速度的实现是我们关注的重点。


我们先来看看开发示例:


const routes = require('./routes')
const fastify = require('fastify')({
logger: true
})

fastify.register(tokens)

fastify.register(routes)

fastify.listen(3000, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
fastify.log.info(`server listening on ${address}`)
})

class Tokens {
constructor () {}
get (name) {
return '123'
}
}

function tokens (fastify) {
fastify.decorate('tokens', new Tokens())
}

module.exports = tokens

// routes.js
class Tokens {
constructor() { }
get(name) {
return '123'
}
}

const options = {
schema: {
querystring: {
name: { type: 'string' },
},
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
token: { type: 'string' }
}
}
}
}
}

function routes(fastify, opts, done) {
fastify.decorate('tokens', new Tokens())

fastify.get('/', options, async (request, reply) => {
reply.send({
name: request.query.name,
token: fastify.tokens.get(request.query.name)
})
})
done()
}
module.exports = routes

可以注意到的两点是:



  1. 在路由定义时,传入了一个请求的 schema,在官方文档中也说对响应的 schema 定义可以让 Fastify 的吞吐量上升 10%-20%。

  2. Fastify 使用 decorate 的方式对 Fastify 能力进行增强,也可以将 decorate 部分提取到其他文件,使用 register 的方式创建全新的上下文的方式进行封装。


没体现到的是 Fastify 请求介入的支持方式是使用生命周期 Hook,由于这是个对前端(Vue、React、Webpack)来说很常见的做法就不再介绍。


我们重点再来看一下 Fastify 的提速原理。


如何提速


有三个比较关键的包,按照重要性排分别是:



  1. fast-json-stringify

  2. find-my-way

  3. reusify



  • fast-json-stringify:
    const fastJson = require('fast-json-stringify')
    const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
    firstName: {
    type: 'string'
    },
    lastName: {
    type: 'string'
    }
    }
    })

    const result = stringify({
    firstName: 'Matteo',
    lastName: 'Collina',
    })


    • 与 JSON.stringify 功能相同,在负载较小时,速度更快。

    • 其原理是在执行阶段先根据字段类型定义提前生成取字段值的字符串拼装的函数,如:
      function stringify (obj) {
      return `{"firstName":"${obj.firstName}","lastName":"${obj.lastName}"}`
      }

      相当于省略了对字段值的类型的判断,省略了每次执行时都要进行的一些遍历、类型判断,当然真实的函数内容比这个要复杂的多。那么引申而言,只要能够知道数据的结构和类型,我们都可以将这套优化逻辑复制过去。



  • find-my-way:将注册的路由生成了压缩前缀树的结构,根据基准测试的数据显示是速度最快的路由库中功能最全的。

  • reusify:在 Fastify 官方提供的中间件机制依赖库中,使用了此库,可复用对象和函数,避免创建和回收开销,此库对于使用者有一些基于 v8 引擎优化的使用要求。在 Fastify 中主要用于上下文对象的复用。


总结



  • 在路由结构的设计上,Next.js、Nuxt.js 都采用了文件结构即路由的设计方式。Ada 也是使用文件结构约定式的方式。

  • 在渲染方面 Next.js、Nuxt.js 都没有将根组件之外的结构的渲染直接体现在路由处理的流程上,隐藏了实现细节,但是可以以更偏向配置化的方式由根组件决定组件之外的结构的渲染(head 内容)。同时渲染数据的请求由于和路由组件联系紧密也都没有分离到另外的文件,不论是 Next.js 的路由文件同时导出各种数据获取函数还是 Nuxt.js 的在组件上直接增加 Vue options 之外的配置或函数,都可以看做对组件的一种增强。Ada 的方式有所不同,路由文件夹下并没有直接导出组件,而是需要根据运行环境导出不同的处理函数和模块,如服务器端对应的 index.server.js 文件中需要导出 HTTP 请求方式同名的 GET、POST 函数,开发人员可以在函数内做一些数据预取操作、页面模板渲染等;客户端对应的 index.js 文件则需要导出组件挂载代码。

  • 在渲染性能提升方面,Next.js、Nuxt.js 也都采取了相同的策略:静态生成、提前加载匹配到的路由的资源文件、preload 等,可以参考优化。

  • 在请求介入上(即中间件):

    • Next.js、Nuxt.js 未对中间件做功能划分,采取的都是类似 Express 或 Koa 使用 next() 函数控制流程的方式,而 Nest.js 则将更直接的按照功能特征分成了几种规范化的实现。

    • 不谈应用级别整体配置的用法,Nuxt.js 是由路由来定义需要哪个中间件,Nest.js 也更像 Nuxt.js 由路由来决定的方式使用装饰器配置在路由 handler、Controller 上,而 Next.js 的中间件会对同级及下级路由产生影响,由中间件来决定影响范围,是两种完全相反的控制思路。

    • Ada 架构基于 Koa 内核,但是内部中间件实现也与 Nest.js 类似,将执行流程抽象成了几个生命周期,将中间件做成了不同生命周期内功能类型不同的任务函数。对于开发人员未暴露自定义生命周期的功能,但是基于代码复用层面,也提供了服务器端扩展、Web 模块扩展等能力,由于 Ada 可以对页面路由、API 路由、服务器端扩展、Web 模块等统称为工件的文件进行独立上线,为了稳定性和明确影响范围等方面考虑,也是由路由主动调用的方式决定自己需要启用哪些扩展能力。



  • Nest.js 官方基于装饰器提供了文档化的能力,利用类型声明( 如解析 TypeScript 语法、GraphQL 结构定义 )生成接口文档是比较普遍的做法。不过虽然 Nest.js 对 TypeScript 支持很好,也没有直接解决运行时的类型校验问题,不过可以通过管道、中间件达成。

  • Fastify 则着手于底层细节进行运行效率提升,且可谓做到了极致。同时越是基于底层的实现越能够使用在越多的场景中。其路由匹配和上下文复用的优化方式可以在之后进行进一步的落地调研。

  • 除此之外 swc、ESBuild 等提升开发体验和上线速度的工具也是需要落地调研的一个方向。




作者:智联大前端
来源:juejin.cn/post/7030995965272129567
收起阅读 »

裸辞四个月,前端仔靠着Nest绝境收下offer

经历时间轴 地点:上海 8.31做完后离职 9月份开始边复习边玩,轻松加愉快 10月中旬开始投递简历 11月月底绝望,期间仅有1次面试机会,伴随着的是各种焦虑 12月开始随遇而安,每天复习或者学习2小时,然后就是打游戏,刷剧,中旬突然开始了1波疯狂面试,1周...
继续阅读 »

经历时间轴



  • 地点:上海

  • 8.31做完后离职

  • 9月份开始边复习边玩,轻松加愉快

  • 10月中旬开始投递简历

  • 11月月底绝望,期间仅有1次面试机会,伴随着的是各种焦虑

  • 12月开始随遇而安,每天复习或者学习2小时,然后就是打游戏,刷剧,中旬突然开始了1波疯狂面试,1周8面,收到满意offer



后面开始述说离职原因、心理历程,这几个月我经历了什么?



离职原因



  1. 某独角兽 外包仔

  2. 无晋升空间,时间1年半,作为项目组第一个前端,我完成了80%工作,包括脚手架、组件库等基础建设,直到三四个月后才陆陆续续来了其他四五个前端,一块干活。一到填绩效都有我,每次表扬都有我,我都成标兵了。领导每个季度向上汇报时,都提出对我进行转正,从无名额为由到最后的不予回复。失望+1

  3. 9106式工作,阿谀奉承式的敏捷开发,从一开始还是有计划,有设计的迭代,到后面不断图快式开发,跨层汇报。后期演变为了,领导说这样做OK,做完了,领导的领导觉得不OK,重做另一种,做完后,领导的领导的领导觉得不OK,来回返工,以人力不停试错。失望+1

  4. 薪资差距,外包仔 工资每年仅有12个月工资,最低社保,最低公积金,做好做差都一样,没有任何加班工资,说好的调休,申请调休时,层层受阻,PUA不断,失望+1

  5. 僵硬、不思进取的氛围,领导最喜欢说的话,先上一版再说,并且去除了husky,eslint、tslint等提交校验,导致代码屎山不断,相似的功能、逻辑、同事一致在进行大批量复制,甚至有非常强的耦合性,不拆分组件、hook,甚至无视TS类型定义,VSCODE经常出现许多飘红文件,导致TS错误反馈链无法正常使用,维护难度急剧上涨。注定尿不到一个壶里了,失望+1


离职前对自己的认知


优势:



  • Vue3、Vue2、React函数式组件开发/class组件开发皆可

  • 近2年ts使用经验

  • 熟悉前端工程化,包括rollup、vite、webpack配置,脚手架开发,组件库搭建、monorepo式的包管理等。

  • 熟悉数据结构、设计模式,并擅长数据结构设计,保证可维护性的同时不停增强可扩展性

  • 算法方面,比不上各种大厂的前端,至少比下有余,leetcode刷过100多题,虽然都是简单中等难度,但至少强于大部分前端了吧

  • 工具方面,熟悉processon绘图工具的使用,包括绘制uml类图,脑图等


劣势:



  • 28岁,接近4年的前端开发经验

  • 大专学历

  • 外包仔


重拾技能:



  • 微信小程序开发,3年前开发过一个原生小程序,长久不用忘了

  • node方面之前还使用过Express、Koa打通mySQL,开发过几十个增删查改的接口

  • 跨段方面,以前的一段工作经历使用过UniApp开发过H5与小程序的跨段应用



重拾这部分技能后,对于寒冬中离职感觉也不是那么可怕,但后面现实还是泼了一盆凉水



离职后我是怎么做的?



  • 9月-10月我都是在整理、复习JS知识点、Vue、React框架方面的硬基础内容,期间开始了在掘金发文,也算是进行了知识点的分享,获得不少的收藏、点赞,非常有满足感

    • JS复习分享

    • Vue复习分享

    • React复习分享

    • 设计模式复习分享



  • 我的目标是中小厂自研,经过查看多家公司招聘要求后,发现中小厂对于跨端开发的执着,于是在此期间,学习了taro,感觉与uniapp一样,也能很快上手,本质也是多了一部分如同小程序般的json配置化。

  • 除此之外,每一家招聘要求上,都会有一条很显眼的要求:熟悉一种后端语言优先。招聘公司并非要求我们要真的去开发服务端,而是希望通过这种方式,降低前后端的沟通成本,并且使得双方达到一种平衡,避免前端后端间的撕逼。因此,我第一个想到了Nest,在学这个东西的时候,顺带还能复习一把以前的node开发知识,相比H5,小程序也更容易出圈。(尤其是碰到那些专注于前端开发的面试官,你能扯一部分服务端的东西,他也得一愣一愣的,因为他也不知道你对不对,你究竟有多对)


学习Nest过程中我收获了哪些东西?


通过学习,我对服务端开发套路更加清楚,前端是开发界面,接收数据,呈现数据;服务端则是负责提取或者写入数据,中间穿插着对数据的处理。当系统学习后我豁然开朗了不少。



  1. 多环境配置方案

    • 使用dotenv的简单数据配置

    • 使用js-yaml的复杂数据方式配置



  2. 数据库

    • 如何提升数据库使用效率?——ORM方案,如:typeORM、sequelize、prisma

    • 数据表间的关系有哪些?如何设计?——1对1、1对多/多对1、多对多;及三大设计范式,er图如何设计

    • 如果需要考虑数据库迁移,如何配置nest连接数据库?



  3. 日志统计

    • 为什么日志统计这么重要?

    • 常见日志方案——winston、pino

    • 日志如何分类?—— 错误日志、调试日志、请求日志

    • 日志记录位置有哪些?分别起什么作用?—— 控制台日志(方便调试)、文件日志(方便回溯与追踪)、数据库日志(敏感操作/数据 记录)



  4. 过滤器有哪些?有什么作用?—— 全局过滤器、控制器过滤器、路由过滤器;它们用来更友好地返回服务端的错误响应。

  5. 拦截器和过滤器区别是什么?拦截器主要用于在请求处理之前和之后对请求进行修改、干预或拦截。它们可以修改请求和响应的数据、转换数据格式、记录日志等,以及处理全局任务

  6. 面向对象式的开发方式,为什么老是看到JAVA里充斥着这么多的“注解”?对设计模式,模块的分类,层级的处理更上一个层次


当自我介绍时说出熟悉nest开发的变化


当有2家自研公司在对我面试时,我抛出了熟悉nest开发后。面试官感觉眼神都不一样了,这是真实的。然后这两家公司面试完后,我总结下来,1个小时有大概半小时都是在谈论服务端开发对前端的助益,更多的是关于fp开发oop开发的区别,有哪些收获?除此池外,我们还会不停探讨关于设计模式、数据库方面的话题,如:表关系、如何解耦之类的。也就是说,有面试官一直在挖掘你的深度、广度。你和面试官侃侃而谈,自然结果不会差!


另外还有两三家公司,谈到Nest或者node方面的东西时,面试官都是一句话带过,自然而然,他们不熟悉这方面的东西,你也可以反向面试出这家公司的深浅。


最后入职的公司


面试了两轮,大概4个小时不到,docker、服务端、前端、设计模式、规范、简单的算法,全都问了一遍。其实还有另外俩家备选的公司,都是到了二面三面,当我拿到这家公司offer后,婉拒了他们的面试,也少了一波问价的机会。


今天报道,朝九晚六,偶尔加班,最多8点。薪资也很满意。


感谢nest、感谢我的卷,值了!祝各位有个清晰的规划,能够快速上岸


作者:见信
来源:juejin.cn/post/7319330542100561932
收起阅读 »

2023总结:我在深圳做前端的第6年

入行前端已经6年了,一直有在掘金看技术文章的习惯。其实很早有想在掘金上写点什么,奈何个人技术水平太菜,实在不敢在各位大佬面前献丑;二来就是太懒,无法静下心来做一件事。但万事开头难,不踏出这一步,永远只能原地踏步了。今天就逼着自己记录一下这一年的经历,希望能有个...
继续阅读 »

入行前端已经6年了,一直有在掘金看技术文章的习惯。其实很早有想在掘金上写点什么,奈何个人技术水平太菜,实在不敢在各位大佬面前献丑;二来就是太懒,无法静下心来做一件事。但万事开头难,不踏出这一步,永远只能原地踏步了。今天就逼着自己记录一下这一年的经历,希望能有个好的开始!


年初找工作3个月


22年底我从干了两年的外包公司离职了。之所以在外包干了这么久,主要原因还是因为菜,另外可能就是我本人非科班入行,对外包也不是很抵触,毕竟福利方面跟第一家入职的自研公司也差不多,而且拿钱干活不丢人。


年初就过来准备找工作了,不过还是玩了大半个月,期间参加了朋友的婚礼,又去顺德玩了两天。就这样到了2月底,开始投简历面试。面试前也看了一些八股文,自己也做了总结笔记,但真正面试过程中,表达沟通能力是很重要的一方面,这方面自己是没什么优势的。面试期间一轮游的居多,有的感觉面的好的到二面了也由于没有后续而不了了之。经过了一个多月的面试,每周大概两三家的样子,人都面麻了,还是一个offer没拿到(期间其实通过了一家自研小公司的面试,但由于学历的问题,最终也黄了)。此时都有打算去其它城市看看,后来冷静想想还是打消了念头。后面又是经过了一个多月的零零星星的面试,终于在5月底拿到了一家外包的offer。


当时的面试题记



有同学可能好奇为啥找工作能这么久,大家应该都了解今年大环境的影响。另外就是学历问题,我是非统招学历,另外加上非科班,双buff加持是地狱级别也不为过。当然个人技术能力不行也占一方面。


当时是从外包裸辞的,以为可以很快找到下家,可现实给了我一记的抱拳。3个月期间没有收入,而且家里今年在装修新房,钱大部分都寄回家了,又不想让父母知道,最后只能在借呗借了2万先用着。


这里不得不说当时社会工作经验的欠缺,一般外包如果不主动离职的话,外包会给你安排面试,同时待业期间每月会给到深圳最底薪资,起码算是有个基本的生活保障。而且就算外包要裁你的话他们得给赔偿,另外自己也可以领到失业金。所以,以后在不能确保自己很快找到下家的情况下,千万不要裸辞啊。


社会给我另一记抱拳是让我真正意识到学历的重要性。在boss直聘上我沟通过的hr有上千家了,大部分了解到我的学历时都直接不回了。但现在后悔已经晚了,我已经无法拿到统招全日制学历了。后面有了解到软考,算是互联网技术人员能拿到的一个有一定含金量的职称,打算今年上半年能拿下。


新一家外包短暂的3个月


5月底入职了一家外包公司,被分派到给深圳的罗湖烟草局做项目。我们十几个项目人员在一间临时办公室里,其中4个前端5个后台外加测试和项目经理。前端项目还是比较简单,负责vue的pc端和uniapp的移动端,期间主要开发了一个电子烟的小模块,另外就是修复系统遗留bug。自己本来想着也是先干着,边工作边看看外面的机会,可没想到不到3个月就被通知说项目要撤了。只能说今年的就业环境真是堪忧!


办公楼下拍的夏天的棉花糖



前同事伸过来的橄榄枝


此时在上家还没有离职,另外也攒了大概一周的调休,也算有一点缓冲时间。后面就是准备新一轮面试考验了,投了很多家,收到的面试邀请也是寥寥,第一周面了3家的样子,其中一家面试通过但给到的薪资比期望的低很多,考虑之后还是拒绝了。


就在以为又是一场漫长的求职路时,第二周在家刷面试题时意外收到前同事的微信消息。说是看到我的简历了,想确认一下是不是我。同事是自从第一家离职后就比较少联系,平时也就偶尔朋友圈点赞之交。而且上半年由于失业的焦虑,我也没有再发朋友圈了。确认之后简单寒暄了一下,同事说他现在待的也是一家算是外包公司,加班比较严重,问我是否有意向。心想对我现在来说是一个难得机会,而且同事说可以走内推,因此一些无法预料的意外也可以避免,不多思考我便答应了。接下来就是去同事公司面试,没想到他就是前端负责人。所以也没问技术问题,就简单的聊了一下境况,然后就是等hr消息。第二天就hr就来电话了,然后就是顺利入职。


这次求职经历给我的感悟就是:平时有时间多跟朋友交流一下,增进下感情,兴许在你困难的时候,朋友可以提供一点帮助。毕竟在这个复杂的社会中,谁都不是孤独的存在,人情也就是在相互帮扶中建立的。


近况


入职新公司已有4个月了,公司实行大小周,每周1,2,4固定加班到8:30,加班强度在我看来还可以接受。给银行做的项目,每个月两次的上线节点,开发上基础设施像流水线Jenkins都是有的,就是上线流程上稍微繁琐了一点。前端技术栈pc端用的微前端,移动端是安卓和IOS内嵌H5,对我来说算是没接触过的技术了。但好在都是用的vue,上手起来也快。


年底乘着天气晴朗,去爬了一直想去的梅沙尖



2024展望


新的一年:


工作上希望能顺顺利利,业余时间持续提升技术能力,多花时间思考和总结。


生活上希望能多去接触自己喜欢的人和事,少一点迷茫,多一点开心!


flag:


1.在掘金上写5篇技术文章


2.看10本书(投资理财,个人成长,名人传记,文学小说都可以涉猎)


3.拿到软考证书(中级软件设计师)


4.谈女朋友(本人94年,快30岁了,妥妥的大龄剩男)


作者:wing98
来源:juejin.cn/post/7319700830076157988
收起阅读 »

35岁京东员工哭诉:我只是年龄大了,不是傻了残疾了,为什么不能拥有与年轻人平等的面试机会?

一位35岁的京东员工哭诉他并非因为年龄大就意味着智商下降、工作能力下降,更不是因为残疾而无法胜任工作,然而在当前经济寒冬的大环境下,为何他无法获得与那些拥有3~5年工作经验的年轻人一样的面试机会呢?这不仅仅是一个人的心声,更是一个普遍存在于社会底层的困境。大龄...
继续阅读 »

一位35岁的京东员工哭诉他并非因为年龄大就意味着智商下降、工作能力下降,更不是因为残疾而无法胜任工作,然而在当前经济寒冬的大环境下,为何他无法获得与那些拥有3~5年工作经验的年轻人一样的面试机会呢?

这不仅仅是一个人的心声,更是一个普遍存在于社会底层的困境。大龄员工在职场中所面临的困境,是一种对人才潜能的浪费,同时也反映出我们对于工作价值的认知是否真的应该被年龄所左右。这一问题不仅仅关乎一个人的个体命运,更触及到整个社会的公平和机会均等。年龄是否真的应该成为评判一个人能否胜任工作的唯一标准?年长者所积累的经验和智慧,不应该成为被忽视的财富。

有网友说:本质是体力 精力不行了,干的活都一样 肯定有限选年轻的。
这位网友说:的行业不行业没啥关系,除了师医公三个行业没啥年龄焦虑,其他还有哪个不焦虑,说白了就是中国人太多了,每年毕业生1000w,排队等着找工作
又一网友说:年龄大不好忽悠,不好pua了

网友小海豚说:还是要价太高,如果1w上下的岗位都没有的话才是真的凉了。

网友猫叔说:本质劳动力过剩,国家出台政策限制加班时长,劳动强度。增加市场劳动力需求
有网友说:因为中国企业的领导都害怕比自己年纪大的员工,不自信
网友小茄子说:主要还是卷吧。
网友小袁说:看行业,要是java就是这样,芯片硬件要好点
有网友说:因为国内老板和员工都喜欢996,35岁之后不管是身体还是家庭都要占用一部分精力。
又一网友说:卡学历都行凭什么不卡年龄呢,要做到一视同仁才公平铁子
网友榴莲说:不太能适应国内企业的工作节奏吧,每天有事没事至少10个小时起步。
网友小榛子说:你如果愿意收入打个折的话,还是能找到的
有网友说:是很无奈呀,根本没面试。行情不是一般的差
又有网友说:30都嫌弃 各种挑三拣四的 不知道在选秀还是干啥
有人认为,问题根本在于体力和精力逐渐减弱,而招聘方更愿意选择年轻人。有网友认为几乎所有行业都存在年龄焦虑,除了师医公三个行业,大部分行业都面临就业竞争激烈的问题,尤其是在中国人口众多的情况下。一些网友提到,劳动力过剩是根本原因。
也有人指出国内老板和员工对996的追求,以及领导对比自己年纪大的员工的担忧,导致了对大龄员工的排斥。有的网友认为,企业更看重的是年轻员工的卷取,而不是经验和智慧。有人指出35岁之后,身体和家庭都会占用一部分精力,不适应国内企业的工作节奏。也有网友认为,降低收入期望可能是找到工作的一种方法。


作者:Python开发
来源:mp.weixin.qq.com/s/tb3HF7Ub-7-IancG72stVA
收起阅读 »

一个优雅解决多个弹窗顺序显示方案

不是因为看到希望才坚持,而是因为坚持了才会有希望!场景  在做直播软件的时候,需要在用户打开App后,先后弹出签到,活动,提示等一系列弹窗。每个弹窗都要在前一个弹窗消失后弹出。于是就面临一个弹窗顺序问题,那时候对设计模式很陌生,不知道怎么更好的解决弹窗顺序问题...
继续阅读 »

不是因为看到希望才坚持,而是因为坚持了才会有希望!

场景

  在做直播软件的时候,需要在用户打开App后,先后弹出签到,活动,提示等一系列弹窗。每个弹窗都要在前一个弹窗消失后弹出。于是就面临一个弹窗顺序问题,那时候对设计模式很陌生,不知道怎么更好的解决弹窗顺序问题,都在下前一个弹窗取消或关闭时去加载后面一个弹窗。这样做虽然也能解决问题,但是实现并不优雅,如果在弹窗中间再添加一个其他类型的弹窗改动代价就变得很大,特别是当你是后来接手代码的新人,稍有不慎,就要背锅。怎么能简单而又优雅的解决这个问题呢?

思路

  开发者必读的23种设计模式,对于日常开发问题的解决提供了很好的思路,可以说几乎所有的优秀架构都离不开设计模式,这也是面试必问问题之一。23种设计模式中有一个责任链模式,为弹窗问题提供了解决方案,这也是我从okhttp源码中学习到的,读过okhttp的同学都知道,okhttp网络请求中的五大拦截器基于链式请求,使用起来简单高效。本篇文章同样也是基于责任链的思路来解决弹窗顺序问题。

代码

  1. 首页我们定义一个接口DialogIntercept,同时提供两个方法 intercept和show。
interface  DialogIntercept {
fun intercept(dialogIntercept: DialogChain)
fun show():Boolean
}

  所有的弹窗都需要实现DialogIntercept中的这两个方法。

  1. 自定义弹窗实现DailogIntercept接口。

● 弹窗


class FirstDialog(val context: Context) :DialogIntercept{

override fun intercept(dialogIntercept: DialogChain) {

}

override fun show():Boolean{
return true
}
}

  这里默认show()方法默认返回true,可根据业务逻辑决定弹窗是否显示。

  1. 提供一个弹窗管理类DialogChain,通过建造者模式创建管理类。根据弹窗添加的顺序弹出。
class DialogChain(private val builder: Builder) {
private var index = 0
fun proceed(){
............
...省略部分代码.....
.............
}
class Builder(){
var chainList:MutableList = mutableListOf()
fun addIntercept(dialogIntercept: DialogIntercept):Builder{
.....省略部分代码.....
return this
}
fun build():DialogChain{
return DialogChain(this)
}
}

}

效果

  为了测试效果,分别定义三个弹窗,FirstDialog,SecondDialog,ThirdDialog。按照显示顺序依次添加到DialogChain弹窗管理类中。

  1. 定义弹窗。

  由于三个弹窗代码基本相同,下面只提供FirstDialog代码。

class FirstDialog(val context: Context) :DialogIntercept{


override fun intercept(dialogIntercept: DialogChain) {
show(dialogIntercept)
}

override fun show():Boolean{
return true
}

private fun show(dialogIntercept: DialogChain){
AlertDialog.Builder(context).setTitle("FirstDialog")
.setPositiveButton("确定"
) { _, _ ->
dialogIntercept.proceed()
}.setNegativeButton("取消"
) { _, _ ->
dialogIntercept.proceed()
}.create().show()
}
}

2 . 分别将三个弹窗按照显示顺序添加到管理器中。

 DialogChain.Builder()
.addIntercept(FirstDialog(this))
.addIntercept(SecondDialog(this))
.addIntercept(ThirdDialog(this))
.build().proceed()
  1. 实现效果如下:

总结

  再优秀的架构,都离不开设计模式和设计原则。很多时候我们觉得架构师遥不可及,其实更多的时候是我们缺少一个想要进步的心。新的一年,新的起点,新的开始。


作者:IT小码哥
来源:juejin.cn/post/7319652739083108402
收起阅读 »

请给系统加个【消息中心】功能,因为真的很简单

我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面: 1)消息提示 2)消息列表 这样 这样 那,这就是我们今天要聊的【消息中心】。 1、设计 老规矩先来搞清楚消息中心的需求,再来代码实现。 我们知道在社交类项目中,有很多评论、点赞等数据...
继续阅读 »

我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面:


1)消息提示


Snipaste_2023-08-27_13-41-36.jpg


2)消息列表


这样


Snipaste_2023-08-27_13-42-25.jpg


这样


Snipaste_2023-08-27_16-41-30.jpg


那,这就是我们今天要聊的【消息中心】。


1、设计


老规矩先来搞清楚消息中心的需求,再来代码实现。


我们知道在社交类项目中,有很多评论、点赞等数据的产生,而如果这些数据的产生不能让用户感知到,那你们想想这会带来什么影响?



用户A:太鸡肋了,发布的内容被人评论点赞了,我居然看不到,下次不用了...


用户B:还好没用这个系统...



所以,看到这些结果我们是不是能够意识到一个健全的社交功能,是不是少不了这种通知用户的机制啊!而这种机制我就把他定义为【消息中心】功能。


再来拆分一下这四个字:消息中心



  1. 消息

  2. 中心


消息:这个可以是由我们自己定义,如:把帖子被用户评论当作一条消息,把评论被用户点赞也可以当作一条消息,甚至系统发布的通知也是一条消息。


中心:这个就是字面意思,将上面所提到的所有消息,归拢到一个地方进行展示。


上面我们也提到消息基本就是这两种:



  • 用户对用户:用户消息

  • 平台对用户:系统消息


针对用户消息,就类似这样,用户 A 给用户 B 的一条评论进行了点赞,那这个点赞动作就会产生一条消息,并且通知到用户 B 的一个存储消息的地方,这里通常就指用户的收件箱。这个收件箱就是专门用来存储用户发给用户的消息,而这个点对点的模式是不是就是推送模式啊!(A 推送消息给 B)


接着针对系统消息,就类似这样,平台管理人员发布了一条通知,告诉大家平台有啥 XXX 活动。那这个活动通知肯定是要让平台的所有用户都知道把,所以这个通知就要存在一个发件箱中。这个发件箱就是专门存储平台的通知,所有用户都来这个发件箱中读取消息就行,而这个一对多的模式是不是就是拉取模式啊!(所有用户都来拉取平台消息)


这样一来,我们根据不同的消息场景就抽出了一个基本的消息推拉模型,模型图如下:



Snipaste_2023-08-27_14-27-25.jpg



Snipaste_2023-08-27_14-59-50.jpg


针对这两种模式,不知道大家有没有看出区别,好像乍一看没啥区别,都是发消息,读消息,对吧!


没错,确实都是一个发,一个读,但是两者的读写频率确实有着巨大的差异。先来看推模型,一个普通用户发表了一条帖子,然后获得了寥寥无几的评论和赞,这好似也没啥特别之处,对吧!那如果这个普通用户发表的帖子成为了热门帖子呢,也即该贴子获得了上万的评论和赞。那,你们想想是不是发消息的频率非常高,而该普通用户肯定是不可能一下子读取这么多消息的,所以是不是一个写多读少的场景。再来看看拉模型,如果你的平台用户人数寥寥无几,那倒没啥特别之处,但如果用户人数几万甚至几十万。那,每个用户都过来拉取系统消息是不是就是一个读频率非常高,而发消息频率非常低(系统消息肯定不会发的很快),所以这是不是一个读多写少的场景。


1.1 推:写多读少


针对这个模式,我们肯定是要将写这个动作交给性能更高的中间件来处理,而不是 MySQL,所以此时我们的 RocketMQ 就出来了。


当系统中产生了评论、点赞类的高频消息,那就无脑的丢给 MQ 吧,让其在消息中间件中呆会,等待消费者慢慢的将消息进行消费并发到各个用户的收件箱中,就类似下面这张图的流程:


Snipaste_2023-08-27_15-45-46.jpg


2.2 拉:读多写少


那对于这个模式,所实话,我觉得不用引入啥就可以实现,因为对于读多的话无非就是一个查,MySQL 肯定是能搞定的,即使你的用户几万、几十万都是 ok 的。


但咱们是不是可以这样想一下,一个系统的官方通知肯定是不多的,或者说几天或者几个星期一次,且一旦发送就不可更改。那是不是可以考虑缓存,让用户读取官方通知的时候走缓存,如果缓存没有再走 MySQL 这样应该是可以提高查询效率,提高响应速度。


具体流程如下图:


Snipaste_2023-08-27_15-57-21.jpg


2.3 表结构设计


基本的业务流程已经分析的差不多了,现在可以把表字段抽一下了,先根据上面分析的,看看我们需要那些表:



  1. 用户收件箱表

  2. 系统发件箱表


看似好像就这两张表,但是应该还有第三张表:



  1. 用户读取系统消息记录表



我们看到页面是不是每次有一条新的消息都会有一个小标点记录新消息数量,而第三张表就是为了这个作用而设计的。


具体原理如下:



  1. 首先运营人员发布的消息都是存储在第二张表中,这肯定是没错的

  2. 那用户每次过来拉取系统消息时,将最近拉取的一条消息写入到第三种表中

  3. 这样等用户下次再来拉取的时候,就可以根据第三张表的读取记录,来确定他有几条系统消息未查看了


可能有人会发出疑问:那用户的收件箱为啥不出一个用户读取记录表呢!


这个很简单,因为收件箱中的数据已经表示这个用户需要都这些个消息了,只是不知道那些是已读的那些是未读的,我们只需要再收件箱表中加一个字段,这个字段的作用就是记录最新一次读取的消息 ID 就行,等下次要读消息时,找到上传读取读取消息的记录ID,往后读新消息即可。



好,现在来看看具体的表字段:


1)用户收件箱表(sb_user_inbox)



  • id

  • 消息数据唯一 id:MQ唯一消息凭证

  • 消息类型:评论消息或者点赞消息

  • 帖子id:业务id

  • 业务数据id:业务id

  • 内容:消息内容

  • 业务数据类型:业务数据类型(商品评论、帖子、帖子一级评论、帖子二级评论)

  • 发起方的用户ID:用户 A 对用户 B 进行点赞,那这就是用户 A 的ID

  • 接收方的用户ID:用户 B 的 ID

  • 用户最新读取位置ID:用户最近一次读取记录的 ID


SQL


CREATE TABLE `sb_user_inbox` (
`id` bigint(20) NOT NULL,
`uuid` varchar(128) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '消息数据唯一id',
`message_type` tinyint(1) NOT NULL COMMENT '消息类型',
`post_id` bigint(20) DEFAULT NULL COMMENT '帖子id',
`item_id` bigint(20) NOT NULL COMMENT '业务数据id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '内容',
`service_message_type` tinyint(1) NOT NULL COMMENT '业务数据类型',
`from_user_id` bigint(20) NOT NULL COMMENT '发起方的用户ID',
`to_user_id` bigint(20) NOT NULL COMMENT '接收方的用户ID',
`read_position_id` bigint(20) DEFAULT '0' COMMENT '用户最新读取位置ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un01` (`uuid`),
UNIQUE KEY `un02` (`item_id`,`service_message_type`,`to_user_id`),
KEY `key` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

可以看到,我加了很多业务相关的字段,这个主要是为了方便查询数据和展示数据。


2)系统发件箱表(sb_sys_outbox)



  • id

  • 内容


SQL


CREATE TABLE `sb_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`content` varchar(2000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

这个表就非常简单了,没啥业务字段冗余。


3)用户读取系统消息记录表(sb_user_read_sys_outbox)



  • id

  • 系统收件箱数据读取id

  • 读取的用户id


SQL


CREATE TABLE `sb_user_read_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`sys_outbox_id` bigint(20) NOT NULL COMMENT '系统收件箱数据读取id',
`user_id` bigint(20) NOT NULL COMMENT '读取的用户id',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

ok,这是消息中心所有分析阶段了,下面就开始实操。


2、实现


先来引入引入一下 RocketMQ 的依赖




org.apache.rocketmq
rocketmq-spring-boot-starter
2.2.1


RocketMQ 的双主双从同步刷新集群搭建教程:blog.csdn.net/qq_40399646…


MQ 配置:


Snipaste_2023-08-27_16-26-09.jpg


2.1 生产者


先来实现生产者如何发送消息。


1)消息体对象:LikeAndCommentMessageDTO


位置:cn.j3code.config.dto.mq


@Data
public class LikeAndCommentMessageDTO {

/**
* 该消息的唯一id
* 业务方可以不设置,如果为空,代码会自动填充
*/

private String uuid;

/**
* 消息类型
*/

private UserCenterMessageTypeEnum messageType;

/**
* 冗余一个帖子id进来
*/

private Long postId;

/**
* 业务数据id
*/

private Long itemId;

/**
* 如果是评论消息,这个内容就是评论的内容
*/

private String content;

/**
* 业务数据类型
*/

private UserCenterServiceMessageTypeEnum serviceMessageType;

/**
* 发起方的用户ID
*/

private Long fromUserId;

/**
* 接收方的用户ID
*/

private Long toUserId;


/*
例子:
用户 A 发表了一个帖子,B 对这个帖子进行了点赞,那这个实体如下:
messageType = UserCenterMessageTypeEnum.LIKE
itemId = 帖子ID(对评论进行点赞,就是评论id,对评论进行回复,就是刚刚评论的id)
serviceMessageType = UserCenterServiceMessageTypeEnum.POST(这个就是说明 itemId 的 ID 是归于那个业务的,方便后续查询业务数据)
fromUserId = 用户B的ID
toUserId = 用户 A 的ID
*/

}

2)发送消息代码


位置:cn.j3code.community.mq.producer


@Slf4j
@Component
@AllArgsConstructor
public class LikeAndCommentMessageProducer {

private final RocketMQTemplate rocketMQTemplate;

/**
* 单个消息发送
*
*
@param dto
*/

public void send(LikeAndCommentMessageDTO dto) {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message message = MessageBuilder
.withPayload(dto)
.build();
rocketMQTemplate.send(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, message);
}

/**
* 批量消息发送
*
*
@param dtos
*/

public void send(List dtos) {
/**
* 将 dtos 集合分割成 1MB 大小的集合
* MQ 批量推送的消息大小最大 1MB 左右
*/

ListSizeSplitUtil.split(1 * 1024 * 1024L, dtos).forEach(items -> {
List> messageList = new ArrayList<>(items.size());
items.forEach(dto -> {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message message = MessageBuilder
.withPayload(dto)
.build();
messageList.add(message);
});
rocketMQTemplate.syncSend(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, messageList);
});
}

private void checkMessageDTO(LikeAndCommentMessageDTO dto) {
AssertUtil.isTrue(Objects.isNull(dto.getMessageType()), "消息类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getItemId()), "业务数据ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getServiceMessageType()), "业务数据类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getFromUserId()), "发起方用户ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getToUserId()), "接收方用户ID不为空!");
}


/**
* 发送点赞消息
*
*
@param messageType 消息类型
*
@param serviceMessageType 业务类型
*
@param itemToUserIdMap 业务ID对应的用户id
*
@param saveLikeList 点赞数据
*/

public void sendLikeMQMessage(
UserCenterMessageTypeEnum messageType,
UserCenterServiceMessageTypeEnum serviceMessageType,
Map itemToUserIdMap, List saveLikeList)
{
if (CollectionUtils.isEmpty(saveLikeList)) {
return;
}
List dtos = new ArrayList<>();
for (Like like : saveLikeList) {
LikeAndCommentMessageDTO messageDTO = new LikeAndCommentMessageDTO();
messageDTO.setItemId(like.getItemId());
messageDTO.setMessageType(messageType);
messageDTO.setServiceMessageType(serviceMessageType);
messageDTO.setFromUserId(like.getUserId());
messageDTO.setToUserId(itemToUserIdMap.get(like.getItemId()));
dtos.add(messageDTO);
}
try {
send(dtos);
} catch (Exception e) {
//错误处理
log.error("发送MQ消息失败!", e);
}
}
}

注意:这里我用了 MQ 批量发送消息的一个功能,但是他有一个限制就是每次只能发送 1MB 大小的数据。所以我需要做一个功能工具类将业务方丢过来的批量数据进行分割。


工具类:ListSizeSplitUtil


位置:cn.j3code.config.util


public class ListSizeSplitUtil {

private static Long maxByteSize;

/**
* 根据传进来的 byte 大小限制,将 list 分割成对应大小的 list 集合数据
*
*
@param byteSize 每个 list 数据最大大小
*
@param list 待分割集合
*
@param
*
@return
*/

public static List> split(Long byteSize, List list) {
if (Objects.isNull(list) || list.size() == 0) {
return new ArrayList<>();
}

if (byteSize <= 100) {
throw new RuntimeException("参数 byteSize 值不小于 100 bytes!");
}
ListSizeSplitUtil.maxByteSize = byteSize;


if (isSurpass(List.of(list.get(0)))) {
throw new RuntimeException("List 中,单个对象都大于 byteSize 的值,分割失败");
}

List> result = new ArrayList<>();

List itemList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
itemList.add(list.get(i));

if (isSurpass(itemList)) {
i = i - 1;
itemList.remove(itemList.size() - 1);
result.add(new ArrayList<>(itemList));
itemList = new ArrayList<>();
}
}
result.add(new ArrayList<>(itemList));
return result;
}


private static Boolean isSurpass(List obj) {
// 字节(byte)
long objSize = RamUsageEstimator.sizeOfAll(obj.toArray());
return objSize >= ListSizeSplitUtil.maxByteSize;
}
}

至此呢,生产者的逻辑就算是完成了,每次有消息的时候就调用这个方法即可。


2.2 消费者


位置:cn.j3code.user.mq.consumer


@Slf4j
@Component
@AllArgsConstructor
@RocketMQMessageListener(topic = RocketMQConstants.USER_MESSAGE_CENTER_TOPIC,
consumerGr0up = RocketMQConstants.GR0UP,
messageModel = MessageModel.CLUSTERING,
consumeMode = ConsumeMode.CONCURRENTLY
)

public class LikeAndCommentMessageConsumer implements RocketMQListener {

private final UserInboxService userInboxService;

@Override
public void onMessage(LikeAndCommentMessageDTO message) {
userInboxService.saveMessage(message);
}
}

saveMessage 方法的逻辑就是将消息保存到 MySQL 中,至此消息的产生和存储就算完成了,下面来看看用户如何查看吧!


2.3 用户消息查看


对于用户查看普通的消息就是访问一下 MySQL,并且更新一下最新读取的字段值即可,我贴一下关键代码就行了,代码如下:


public IPage page(UserMessagePageRequest request) {
// 获取消息
IPage page = getBaseMapper().page(new Page(request.getCurrent(), request.getSize()), request);

if (CollectionUtils.isEmpty(page.getRecords())) {
return page;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
if (Objects.isNull(page.getRecords().get(0).getReadPositionId()) ||
page.getRecords().get(0).getReadPositionId() == 0) {
UserInbox userInbox = new UserInbox();
userInbox.setId(page.getRecords().get(0).getId());
userInbox.setReadPositionId(userInbox.getId());
updateById(userInbox);
}
}
return page;
}

2.4 系统消息查看


对于系统消息的查看也是,只贴出关键代码,查询和更新读取记录逻辑,代码如下:


@Override
public IPage lookSysPage(SysOutboxPageRequest request) {
Page page = lambdaQuery()
.orderByDesc(SysOutbox::getId)
.page(new Page<>(request.getCurrent(), request.getSize()));
IPage outboxVOIPage = page.convert(userInboxConverter::converter);
if (CollectionUtils.isEmpty(outboxVOIPage.getRecords())) {
return outboxVOIPage;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
userReadSysOutboxService.updateReadLog(page.getRecords().get(0).getId(), SecurityUtil.getUserId());
}
return outboxVOIPage;
}

这里,可能有人会发现,没有按照上面分析的那用从缓存中读,是的。这里的实现我没有用到 Redis,这里我偷了一下懒,如果有拿到我代码的同学可以试着优化一下这个逻辑。


作者:J3code
来源:juejin.cn/post/7274922643453853735
收起阅读 »

程序员真的需要双显示器吗?

我最近思考一个问题,用双显示器编程是否比单显效率高? 如果是的话那么是否显示器越多效率越高呢? 有些同学肯定会马上回答,那肯定觉得是对的! 一个屏幕用来看文档,一个屏幕用来写代码,肯定会比都挤在一个屏幕效率高呀, 如果桌子够大的话,最好再加一个屏幕专门用来聊...
继续阅读 »

我最近思考一个问题,用双显示器编程是否比单显效率高?



如果是的话那么是否显示器越多效率越高呢?


有些同学肯定会马上回答,那肯定觉得是对的!


一个屏幕用来看文档,一个屏幕用来写代码,肯定会比都挤在一个屏幕效率高呀, 如果桌子够大的话,最好再加一个屏幕专门用来聊微信,那我就不用切换窗口,那效率肯定就爆表了。


但你有没有总结一下,在你编程的时候,具体为什么单显示器会比双显示器比效率低呢?


低效的原因


以下是我总结出的2点原因:



  • 第一点就是切换窗口,因为即使在最新操作系统中,使用单显示器的情况下,无论是用鼠标,command tab ,还是四指上扫调度中心,切换窗口仍然是非常麻烦的一件事 。
    但是你如果用多显示器,就可以避免在两个窗口之间切换。两个窗口放在两个显示器里,只要晃头就行了

  • 第二点是,你会有同时显示两个窗口的需求,比如一个文档,一个ide,你需要一边看文档,一边敲代码,这用一个显示器就不太容易做到,而且有时需要调整各个窗口大小,这是非常浪费时间的。


以上两个问题就是导致你效率低下的最重要的问题,如果可以成功解决,那么使用单屏编程就不是个问题了


解决的要点:



  • 不要用调度中心,不要用command + tab 来切换窗口, 把你每个常用的软件都做成超级快捷键,肌肉记忆直接拉起

  • 学会使用窗口管理工具,配合超级快捷键,快速调整窗口,不要尝试用鼠标拉窗口,手不要离开键盘


如何定义超级快捷键



超级快捷键 = Hyper + 任意键



为了设置超级快捷键,第一步就需要定义一个 Hyper 键,也就是超级键


为了让 Hyper 键不与系统或其他软件冲突,通常会把 Shift + Control + Option + command 一起按,也就是⌃ ⌥ ⌘ ⇧ 这4个键一起按作为 Hyper 键


image.png


但是键盘上并没有一个实体的 Hyper键,我们又不可能手动按住4个键, 所以我们会用软件把大小写切换键 Capslock 改成 Hyper key


因为大小写切换键,就在小指头的旁边,是键盘上占着最好位置的按键,却是一点用都没有的一个键,改完以后当再按住 Capslock键 的时候,实际上相当于按住了4个按键,也就是按住了 Hyper键


image.png


我用到的改键软件是 Karabiner-Element


打开 Karabiner 找到 Complex Modifications 点击 Add predefined rule


image.png


点击 import more rules from the internet 这时会打开浏览器,搜索 CapsLock plus 点击 import


image.png


这里我只 enable 了两条规则



  • CapsLock to Hyper/Escape

  • Hyper Cursor navigation


当然你可以根据需要 edit -》 save 来修改


修改后的配置文件会保存在下边这个位置


~/.config/karabiner/karabiner.json


这是我最终修改后的配置文件,放在下边这里,你可以去参考


github.com/nshen/dotfi…


至此你的超级键就定义好了。


用超级快捷键切换桌面


用超级快捷键来切换桌面需要到设置中把对应的快捷键定义成 Hyper键 + 对应的键,我这里设置成了 Hyper + [Hyper + ]


image.png


具体位置在



设置 -> 键盘 -> 键盘快捷键 - 调度中心 - 调度中心 - 向左/向右移动一个空间



用超级快捷键启动应用


首先安装 Raycast,因为Raycast是目前最优秀的应用启动器,并且可以给每个应用定义快捷键,所以我们用 Raycast 来给每个常用的应用都加上超级快捷键。


打开后 command + k


image.png


打开菜单选择配置


image.png


在这里给应用设置快捷键为 Hyper + 任意键


image.png


以下是我常用的软件设置


image.png


用超级快捷键管理窗口


Raycast 本身就提供了窗口管理功能,非常好用


image.png


我们可以用和应用一样的方法,给这些功能加上快捷键


image.png


我常用的快捷键有


image.png


单屏的优势


当你熟练的使用上述技巧,你就基本上可以使用单屏达到多屏开发同样的效率。


单屏也有单屏的优势,使用单屏,你可以获得以下多屏没有的好处



  • 不用摇头晃脑了,你会更容易专注于一件事,不会被其他屏幕上的应用影响,其他应用只有在需要的时候才会被调出来

  • 不用在桌面上放一堆显示器,节省了桌面空间,桌面会更整洁

  • 便携,拿起笔记本,不用依赖外部显示器,就可以随时随地最高效编程

  • 最重要的是省钱,再也不用考虑买或升级显示器了,一个笔记本搞定!


感谢观看,喜欢类似文章,还请点个关注,谢谢


作者:nshen
来源:juejin.cn/post/7319541571279798310
收起阅读 »