注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

毕业三年,我活成了理想中的样子?

又到了毕业季,不知不觉毕业已经三年了 🤔 在这三年期间对生活有了很多感悟,俗话说“字节一年,人间三年”,感觉自己又读了两个大学 hh 对这三年做个总结吧,同时也分享下自己在杭州、在大厂的所见所得所感,也希望对你有所帮助~ 一、关于我 第一次以这种形式与大家见...
继续阅读 »

又到了毕业季,不知不觉毕业已经三年了 🤔


在这三年期间对生活有了很多感悟,俗话说“字节一年,人间三年”,感觉自己又读了两个大学 hh


对这三年做个总结吧,同时也分享下自己在杭州、在大厂的所见所得所感,也希望对你有所帮助~


一、关于我



第一次以这种形式与大家见面,还是自我介绍下吧


希望尽量用简短的文字让你了解我,同时磨平一些行文的信息差


那我们从一个小游戏开始吧



1.1 “生命年轮”游戏


2.game.jpg



  • 以七年为一个节点,写下七年中,每年对自己最重要的事情

  • 并分享给朋友,谈谈对这些事件的看法和对自己的影响,从而让彼此更加亲近


1.2 年轮线


成都人,95后,从 2015 年大一到 2022 毕业三年的 7 年


1.3 核心事件



  • 2016 年:坚持计算机行业。“软件工程”作为高考最后一个志愿,大一结束时本想转专业,但最后还是坚持下来

  • 2017 年:担任街舞协会 会长,举办《HipHop 之夜》晚会,邀请了各大高校协会参与

  • 2018 年:大三确认技术方向,开始发力,自学前端

  • 2019 年:大四实习,应届去了杭州字节跳动

  • 2020 年:重新定义了学习,面向方法论成长

  • 2021 年:遵从内心,学会勇敢,想清楚自己真正喜欢和擅长什么

  • 2022 年:再见杭州,你好魔都。去上海寻找无限可能


1.4 个人感受


性格成就现在的我


我是那种“玩和学”能够分开的人,学的时候认真学,玩的时候也会认真玩。所以大一、大二严格来说是“玩”过去的,我过的很开心。大三决定不考研后,开始自学专业课,应届能进大厂


方法论很重要


不得不说公司 2 年多的成长能够抵过大学 4 年的学习(没说不重要)。身边结识了一群优秀的人,才慢慢明白,自己学生生涯的思维是不全面的,学习方式是不高效的,由此才开始慢慢调整


选择大于努力


人生面临各种选择,无论是学生时代还是涉足社会。如果大方向是错的,再怎么努力也不会看到好结果


二、城里、城外


那么开始我的分享


3.city.jpg



苏小姐道:“法国也有这么一句话。不过,不说是鸟笼,说是被围困的城堡,城外的人想冲进去,城里的人想逃出来。鸿渐,是不是?”——《围城》



2.1 小故事


应届拿到几个大厂 offer,拥有不错的年薪,这也确实是我大学时代的梦,骄傲地跟父母报完喜讯后,马上订了台心仪已久的 Mac Pro。来到杭州报道,同事、环境等都让我格外欣喜和满足。我终于 进入 了字节


工作一年多,萌生了 国留学的想法,想再深造深造。同事D 再三劝阻我重视“沉默成本”,同时目前也是快升职之际,这种时间节点不常,有应该把握,另外出国的话以后稳定了随时都可以出去。句句在理,我被说服了,还是选择 下来搏一搏


想着暂时不出去的话,那就先 外企吧。随后去报了雅思课程,无论是之后留学还是外企,总归没有坏处。却因为疫情原因,兜兜转转花了半年终于考过了雅思。有了英语能力加成,加上本身技术底子不错,准备了一阵,拿到了苏州微软的 offer,最后却因为各种原因 拒绝


一个饭后,阳光明媚,约着同事C 和同事D 下去逛逛,吐槽 了下当下种种。C 告知了他准备 离开 的想法,我感到稍许失落,但也表示理解。很快,我们就相聚在散伙饭桌上了


2.2 感悟



关于“城里城外”故事还有很多,相信在你的生活中也萦绕着它们



站在城里:理性看待 对当下的不满


不满足于当下,本身是中性的,它既能变成追求美好生活的 动力,也可能成为负能量的源泉,让我们迷失在报怨中。应该要正确看待欲望



  • 看清改变的迫切性和必要性

  • 树立清晰的目标,制定计划

  • 按照计划推进、根据状况调整


不盲目憧憬城外:按照自己的节奏走


城外风光似乎无限美好,为什么他一年就能考上研、升职加薪、出国留学……,总是被别人牵着走,和别人比较,自己永远都不会快乐


想起一个故事,高中时听班主任讲他带过的一个学姐,她想去国外读本科,所以高中就自学雅思,2013年左右,国内考点很少,无奈只能抢国外的场次,于是她一个人在新加坡考完了雅思。而我完成雅思,是在工作 2 年之后,与高中生涯相隔近 10 年,但这是属于我的 人生节奏


朋友,认真规划未来,按照自己的轨迹前行,想去的地方总会到达的。同时也能够真心为好友们的成功而祝福


跨越城墙:构筑自己的“储蓄池”


天地悠悠,过客匆匆,潮起又潮落。人生总会面临改变,无论是主动还是被动的。要学会构建自己的认知“储蓄池”


站在《系统论》角度,系统的发展,伴随着正要素和负要素的推动,此消彼长,对应着人生的起伏。我们靠什么去扛过一次次重大改变,仅靠遇事时候的打鸡血、调整心态 是远远不够的,靠的是我们在精神和认知上的 未雨绸缪。即


有计划的提升自我,建立具有缓冲能力的储蓄池,以从容应对每次的变更


4.pool.png


三、幸存者偏差是把双刃剑


5.sword.jpg



幸存者偏差效应:指只能看到经过某种筛选而产生的结果,而没有意识到筛选的过程,因此忽略了筛选掉的关键信息



3.1 小故事


毕业三年,现在年薪也来到了 n十万,但细想身边同事好友 谁又不是呢,自己是个正常水平吧,没有什么优越;学历呢,字节背靠杭州,面向浙大招聘,同事学历也基本至一本起步,自己也没有什么异常。同事W 说如果你学历本科,月入过万,有车有房,帅气阳光,你就已经是百里挑一了。我陷入了沉思


前同事P 来到杭州阿里,相约一起喝酒。我吐槽说其实大厂也就那样,面试造火箭,进去拧螺丝,业务需求 没啥难度,写写界面,封封组件……他说:“你也不能这样说,还是有很多人想进都进不了大厂的,我觉得进大厂可以证明自己”。“证明自己”,我一惊,忽然想起这个被抛在“大明湖畔”的词。回想快毕业时,当时天天想着要进大厂。但身处大厂久了,反而被“幸存者偏差”折磨的不成样


3.2 感悟



不可否认,幸存者偏差是刻在潜意识深处的东西



身处幸存者偏差,让我们与环境拉齐


身处大厂,周围同事都很优秀,我在有意识地吸收他们的工作经验和学习方法。同时也看到自己能力上的不足,并积极改进,慢慢总结出自己的一套方法论 无论什么环境,潜意识本能会 向均线拉齐,即所谓耳濡目染:大学寝室中 5 个室友都在打游戏,你会本能想加入,想着大家不都这样;同样身处于优秀的人之间,你会本能向他们靠齐,努力克服惰性


跳出舒适圈


幸存者偏差的缺点就是长此以往,自己会对当下变得麻木,失去对整体局势变化趋势的感知力 没有什么“大家都这样,那我也就这样了”的借口,当直觉层面感到不对时,要 引起重视。及时跳出舒适圈,去感受更多的可能性,慢慢从这些可能性中筛选出适宜变化的、高效的、自己够得着的生活方式


四、所谓理想


6.dream.jpeg



兄弟B 在英国留学,兄弟L 在云南做生意,我在杭州当码农,我们都有光明的前途。——《新华字典》(玩个梗 hh)



4.1 三十而立


90 后是“悲催”的一代



  • 1982 年,计划生育政策正式写入宪法,90后一代基本都是独生子女

  • 2000 年,互联网起步,70后、80后陆续下海创业,垄断资源

  • 2010 年前后,通货膨胀,物价飞涨

  • 2014 年,房价大涨,高处不胜寒,90后初入社会

  • 2015 年,二胎政策全面开放,90后成为生育主力军,承受高额房贷

  • 2020 年,互联网增量饱和,整个行业开始内卷

  • 2030 年,中国逐步进入老龄化社会,90后延迟退休


以上列举的时间线肯定不全或说准确,但我想说的是 90后,同 80后、70后一样,也是背负沉重压力的一代,每个代有着自己的低谷和红利。90后在享受着“第三次工业革命”——互联网时代 的便利的同时,也在 承担 着意想不到的压力,很多 90后开始学会摆烂,佛系的生活渐渐流行开来,不婚、晚婚、晚育是常态


2021 年我国结婚登记数据为 763.6 万对,当我妈催我谈恋爱结婚时,我却只能说:“你看,90后 不都这样?”


在奔三途中的 90后们,我认为首先要“立”的还是“立己”。一次聚餐时,询问老会长Z,我是考研还是工作好,他脱口而出,“去考研吧,提升自己是最没有风险的投资”,这句话我会记一辈子


人们普遍惰于思考,在俗成的时间点随大流,没错,你大概率不会输的很惨。平安喜乐,踞一方天地,亦岂不快哉?所以,更多的是你与自己的博弈


4.2 精神上的富裕是最难满足的


古有“饱暖思淫欲”,当代有《马斯洛需求层次理论》金字塔。人有基本的生理、安全和归属需求,能生存下去后,开始思考价值认可和 理想抱负。感谢祖国的强大让我们处于和平的年代,能有机会去追求形而上的东西


7.tri.png


每次层需求的满足都是奔赴下一境界的激励源,相对的,需求层次越高,也 更难得到满足,取决于个人的潜力点和你的预期高度。还有一点就是,人的欲望是无穷无尽的,每个阶段有每个阶段的执念,是安于现状还是继续奋勇前行,也是你说的算。但我认为,对于精神层面的追求,你至少应该有那么一次是为自己而活的


4.3 理想的样子


“理想”本来就是一个很虚的概念,它可以杂糅进很多虚幻的东西,但“目标”不是,想清楚自己到底想要什么,勇敢地朝着这个它前进,慢慢部署自己的硬实力和软实力,直到逐渐达到那个高度


对于我来说,为未来 奋斗 过程中的自己,本就是我 理想 中的样子!


五、回归生活


在祖国广袤的土地上,在鳞次栉比的大厦里,我用一把键盘 养活 了自己


如果你问我理想的生活是什么,我会告诉你,我拼尽全力,只是为了能够 平凡 地度过这一生。正如万青唱的那样:傍晚6点下班……


8.song.JPG


感谢你的阅读


By Liam


2022.05.19 于杭州


收起阅读 »

不一样的深拷贝

web
对于深拷贝这个概念在面试中时常被提起,面试官可能让你实现深拷贝需要考虑那些因素,或者直接让你手写封装一个深拷贝,那么今天就和大家探讨一下一个让面试官感到牛逼的深拷贝, 1.思考 众所周知普通的数据类型是值存储,而复杂类型是通过开辟内存空间来存储数据的,我们通过...
继续阅读 »

对于深拷贝这个概念在面试中时常被提起,面试官可能让你实现深拷贝需要考虑那些因素,或者直接让你手写封装一个深拷贝,那么今天就和大家探讨一下一个让面试官感到牛逼的深拷贝,


1.思考


众所周知普通的数据类型是值存储,而复杂类型是通过开辟内存空间来存储数据的,我们通过内存地址从而查找数据,为了可以完全得到一个与原对象一模一样但又没有内存地址关联的深拷贝,我们需要考虑的因素其实有很多,
1.Object.create()创造的对象 Object.create()详细介绍


  let obj = Object.create(null)
obj.name = '张三'
obj.age = 22

这个对象是一个没有原型的对象,大部分对象都有自己的原型,可以使用公共的方法,但这个却不行,我们是不是应该把它考虑进去?


2.symbol作为属性名的情况 Symbol详细介绍 以及
for in 详细介绍


let obj = {
name: 'aa',
age: 22,
[Symbol('a')]: '独一无二的'
}

对于带有symbol的属性,在 for in 的迭代中是不可枚举的,我们是不是需要考虑如何解决?


3.对于修改对象的属性描述 Object.defineProperty()


let obj = { name: 'ayu', age: 22, sex: '男' }
Object.defineProperty(obj, 'age', {
enumerable: true,
configurable: true,
value: 22,
writable: false
})

这里我们改写了原对象的属性描述,age变得无法枚举,for in 也失去效果,并且很多默认的属性描述信息,我们是不是在拷贝后也应该和原对象保持一致?


4.对象的循环引用


let obj = { name: 'ayu', age: 22, sex: '男' }
obj.e = e

obj对象中有个e的属性指向obj,造成相互引用,当我们在封装深拷贝时,主要是通过递归来逐层查找属性值的情况,然后对其进行操作,如果出现这个情况,就会死循环递归造成栈内存溢出,这种情况难道也不值得考虑嘛?


5.一些特殊的对象
都说万物皆对象,对象其实有很多类型,正则,日期(Date),等都需要特殊处理
而函数和数组就比较简单


6.深拷贝的多数要点
也就是当一个对象里面嵌套了多层对象,这个大家应该都知道,我们通常一般使用递归去处理,再结合上面分析的因素就可以封装函数了


const isComplexDataType = (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date) return new Date(obj) // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp) return new RegExp(obj) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
//继承原型链
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] =
isComplexDataType(obj[key]) && typeof obj[key] !== 'function' ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}

思路
从deepclone这个函数开始说起



  1. 1.如果对象的构造器是Date构造器,则我们使用Dte构造器再构造一个Date

  2. 如果对象的构造器是正则构造器再构造一个正则

  3. WeakMap我们先不提,allDesc是拿到原对象所有的属性(可枚举以及不可枚举)以及对应的属性描述信息

  4. cloneObj是我们根据第三步拷贝的一个新的对象的信息,不过是一个浅拷贝,而且我们考虑了原型不存在的情况 Object.assin与Object.create的区别

  5. 通过for of 循环 Reflect.ownKeys(obj) Reflect.ownKeys()用法 (Reflect.ownKeys()可以遍历对象自身所有的属性(symbol,不可枚举都可以),然后重新将obj的key以及对应的值赋值给cloneObj,并且对obj[key]的值做了讨论,当它是对象并且不是函数时,我们递归处理,否则里面为普通值,直接赋给ObjClone


对于deepClone的第二个参数WeakMap来讲, 请大家想想最开始我们提到的一个问题,我们有一个对象,然后我们填了了一个属性,属性为这个对象,这是在相互引用,如果我们处理这样的对象,也使用递归处理,那么就是死循环,因此我们需要一个数据结构来解决,每次我们递归处理的时候,都把obj,以及赋值的cloneobj对应存储,当遇到死循环的时候直接return这个对象即可
WeakMap详细介绍·


(本文用到大量ES5以后的API,推荐阅读阮一峰老师的ES6,这样才能理解的透彻)

作者:当然是黑猫警长啦
来源:juejin.cn/post/7120893997718962213

收起阅读 »

简单理解Vue的data为啥只能是函数

web
前言 在学习vue的时候vue2只有在组件中严格要求data必须是一个函数,而在普通vue实例中,data可以是一个对象,但是在vue3出现后data必须一个函数,当时看着官方文档说的是好像是对象的引用问题,但是内部原理却不是很了解,今天通过一个简单的例子来说...
继续阅读 »

前言


在学习vue的时候vue2只有在组件中严格要求data必须是一个函数,而在普通vue实例中,data可以是一个对象,但是在vue3出现后data必须一个函数,当时看着官方文档说的是好像是对象的引用问题,但是内部原理却不是很了解,今天通过一个简单的例子来说明为啥data必须是一个函数


参考 (vue2data描述)


参考: (vue3data描述)


1.Vue3中的data


const { createApp } = Vue
const app = {
data: {
a: 1
},
template: `

{{a}}


`

}
createApp(app).mount('#app')

image.png
可以看到上来vue就给了警告说明data必须是一个函数 下面直接抛错


2.vue中的data


var app = new Vue({
el: '#app',
data: { a: 'hello world' }
})


这种写法是可以的,前面提过普通实例data可以是对象,但是在组件中必须是函数,
那么在vue2中难道普通实例就没有缺陷嘛?

答案:是有缺陷的,
比如这样


<div id="app1">{{ message }}div>
<div id="app2">{{ message }}div>


const data = { message: 'hello world' }
const vue1 = new Vue({
el: '#app1',
data
})

const vue2 = new Vue({
el: '#app2',
data
})


这样在页面中会显示2个内容为hello world的div标签
那么当我们通过实例去改变messag呢?


 vue1.message = 'hello Vue'

image.png


奇怪的事情发生了,我知识改变了vue1的实例中的数据,但是其他实例的数据也发生了改变,相信很简单就能看出来这应该是共用同一个对象的引用而导致的,这在开放中是非常不友好的,开发者很容易就产生连串的错误,vue2也知道这种缺陷只是没有在普通实例中去体现而已,只在组件中实现了对于data的约束


为了让大家更好的立即为啥data必须是一个函数,黑猫在此简单实现一个vue的实例然后来证明为啥data是一个函数,以及如果data不是一个函数,我们应该如何处理


3.证明data是函数以及原理实现


在实现简单原理之前,我们需要搞清楚Vue在创建实例之前,对于data到底做了什么事情简单来说就是:


vue 在创建实例的过程中调用data函数返回实例对象通过响应式包装后存储在实例的data上并且实例可以直接越过data上并且实例可以直接越过data访问属性


1.通过这句描述可以知道Vue是一个构造函数,并且传入的参数中有一个data的属性,我们可以$data去访问,也可以直接访问这个属性,并且我们需要对这个data做代理

那么简单实现如下


function Vue(options) {
this.$data = proxy(options.data())
}
function proxy(options) {
return new Proxy(options, {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
}
})
}
const data = function () {
return {
a: 'hello world'
}
}
const vue1 = new Vue({
data
})
const vue2 = new Vue({
data
})
vue1.$data.a = 'hello Vue'
console.log(vue1.$data.a) // hello Vue
console.log(vue2.$data.a) // hello world

通过简单实现可与看出来,当我们的data是一个函数的时候,在Vue的构造函数中,只有有实例创建就有执行data函数,然后返回一个特别的对象,所以当我们修改其中一个实例的时候并不会对其他实例的数据产生变化

那么当data不是一个函数呢 ,我们简单改下代码,代码如下


function Vue(options) {
this.$data = proxy(options.data)
}
function proxy(options) {
return new Proxy(options, {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
}
})
}
const data = {
a: 'hello world'
}
const vue1 = new Vue({
data
})
const vue2 = new Vue({
data
})
vue1.$data.a = 'hello Vue'
console.log(vue1.$data.a) // hello Vue
console.log(vue2.$data.a) // hello Vue

可以看出,由于共用一个对象,当代理的时候也是对同一个对象进行代理,那么当我们通过一个实例去改变数据的时候,就会影响其他实例的状态


4.如果data必须是一个对象呢?


假如有人提出如果data是一个对象,那么我们应该如何处理呢,其实也非常简单,在代理的时候我们可以将传入的data对象通过深拷贝即可,这样我们就不会使用相同引用的对象啦。

[深拷贝牛逼封装参考我以前的文章](不一样的深拷贝)


作者:当然是黑猫警长啦
来源:juejin.cn/post/7154664015333949470
收起阅读 »

javascript实现动态分页

web
之前分页都是使用框架给出的分页类来实现分页,当然,体验可能不是那么好。 这次在写YII2.0框架的后台管理系统的小例子的时候,我这也尝试了一下前后分离,用ajax来实现分页跳转。 那么前端的页码绘制及跳页等其他的样式,都是由JavaScript根据后台返回的数...
继续阅读 »

之前分页都是使用框架给出的分页类来实现分页,当然,体验可能不是那么好。


这次在写YII2.0框架的后台管理系统的小例子的时候,我这也尝试了一下前后分离,用ajax来实现分页跳转。


那么前端的页码绘制及跳页等其他的样式,都是由JavaScript根据后台返回的数据拼接而成。我的分页效果如下图所示:






 


大概就是上面的样子。


Html代码如下:对照第一张图片


<ul> 
    <li><span>1<span data-id="1"></span></span></li>
    <li><a data-id="2">2</a></li>
    <li><a data-id="3">3</a></li>
    <li><a data-id="4">4</a></li>
    <li><a data-id="5">5</a></li>
    <li><a data-id="6">6</a></li>
    <li><a data-id="7">7</a></li>
    <li><a data-id="8">8</a></li>
    <li><a data-id="false"> ... </a></li>
    <li><a data-id="11"> 11 </a></li>
    <li><a data-id="next"> &gt;&gt; </a></li>
</ul>

JavaScript代码如下:


我这里使用的是纯JavaScript代码,没有使用jquery,这个是考虑到兼容性的问题。


/**
* @name 绘制分页
* @author camellia
* @date 20200703
* @param pageOptions 这是一个json对象
* @param pageTotal 总页数
* @param curPage 当前页数
* @param paginationId  显示分页代码的上层DOM的id
*/

 function dynamicPagingFunc(pageOptions)
 {
    // 总页数
    var pageTotal = pageOptions.pageTotal || 1;
    // 当前页
    var curPage = pageOptions.curPage || 1;
    // 获取页面DOM对象
    var paginationId = document.getElementById(''+pageOptions.paginationId+'') || document.getElementById('pagination');
    // 如果当前页 大于总页数  当前页为1
    if(curPage>pageTotal)
    {
       curPage =1;
    }
    var html = "<ul>  ";
    /*总页数小于5,全部显示*/
    if(pageTotal<=5)
    {
       html = appendItem(pageTotal,curPage,html);
       paginationId.innerHTML = html;
    }
    /*总页数大于5时,要分析当前页*/
    if(pageTotal>5)
    {
       if(curPage<=4)
       {
          html = appendItem(pageTotal,curPage,html);
          paginationId.innerHTML = html;
       }
       else if(curPage>4)
       {
          html = appendItem(pageTotal,curPage,html);
          paginationId.innerHTML = html;
       }
    }
    // 显示到页面上的html字符串
    // var html = "<ul>  ";
    // html = appendItem(pageTotal,curPage,html);
    html += "</ul>";
    // 显示至页面中
    paginationId.innerHTML = html;
 }
 
 /**
  * @name 绘制分页内部调用方法,根据不同页码来分析显示样式
* @author camellia
* @date 20200703
  * @param pageTotal 总页数
  * @param curPage 当前页
  * @param html 显示在页面上的html字符串
  */

 function appendItem(pageTotal,curPage,html)
 {
    // 显示页
    var showPage = 8;
    // 总页数大于XX页的时候,中间默认...
    var maxPage = 9;
    // 开始页
    var starPage = 0;
    // 结束页
    var endPage = 0;
    // 首先当前页不为1的时候显示上一页
    if(curPage != 1)
    {
       html += "<li><a data-id = 'prev' > << </a></li> ";
    }
    // 当总页数小于或等于最大显示页数时,首页是1,结束页是最大显示页
    if(pageTotal <= maxPage)
    {
       starPage = 1;
       endPage = pageTotal;
    }
    else if(pageTotal>maxPage && curPage<= showPage)
    {
       starPage = 1;
       endPage = showPage;
       if(curPage == showPage)
       {
          endPage = maxPage;
       }
    }
    else
    {
       if(pageTotal == curPage)
       {
          starPage = curPage - 3;
          endPage = curPage;
       }
       else
       {
          starPage = curPage - 2;
          endPage = Number(curPage) + 1;
       }
 
       html += "<li><a data-id = '1'> 1 </a></li> ";
       html += "<li><a data-id='false'> ... </a></li> ";
    }
    var i = 1;
    for(let i = starPage;i <= endPage;i++)
    {
       if(i==curPage)
       {
          html += "<li ><span>"+ i +"<span data-id="+i+"></span></span></li>";
       }
       else
       {
          html += "<li ><a data-id = "+ i +">"+i+"</a></li>";
       }
    }
 
 
    if(pageTotal<=maxPage)
    {
       if(pageTotal != curPage)
       {
          html += "<li><a data-id='next' > >> </a></li> ";
       }
    }
    else
    {
       if(curPage < pageTotal-2)
       {
          html += "<li><a data-id='false'> ... </a></li> ";
       }
       if(curPage <= pageTotal-2)
       {
          html += "<li><a data-id = "+pageTotal+" > "+pageTotal+" </a></li> ";
       }
       if(pageTotal != curPage)
       {
          html += "<li><a data-id = 'next' > >> </a></li> ";
       }
    }
    return html;
 }

 调用上边的分页代码:


// 绘制分页码
 var pageOptions = {'pageTotal':result.pageNumber,'curPage':result.page,paginationId:'pages'};
 dynamicPagingFunc(pageOptions);

我这里把分页的样式是引用的公共css中的文件,这里就不展示了,将你的分页html代码把我的代码替换掉就好。


参数的聚体解释以及函数中用到的参数,备注基本都已给出。


下面这部分是点击各个页码时,请求数据及重回页码的部分


/**
 * @name 分页点击方法,因为页面html是后生成的,所以需要使用ON方法进行绑定
* @author camellia
* @date 20200703
 */

 $(document).on('click''.next'function()
 {
     layer.load(0, {shadefalse});
     // 获取当前页码
     var obj = $(this).attr('data-id');
     // 获取前一页的页码,点击上一页以及下一页的时候使用
     var curpages = $("li .sr-only").attr('data-id');
     // 点击下一页的时候
     if(obj == 'next')
     {
         obj = Number(curpages) + 1;
     }
     else if(obj == 'prev')// 点击上一页的时候
     {
         obj = curpages - 1;
     }
     $.ajax({
         //几个参数需要注意一下
         type"POST",//方法类型
         dataType"json",//预期服务器返回的数据类型
         url"?r=xxx/xxx-xxx" ,//url
         data: {'page':obj},
         successfunction (result)
         {
             // 将列表部分的html清空
             document.getElementById('tbody').innerHTML = '';
             // 重新绘制数据列表
             drawPage(result.dbbacklist);
             // 绘制分页码
             var pageOptions = {'pageTotal':result.pageNumber,'curPage':result.page,paginationId:'pages'};
             dynamicPagingFunc(pageOptions);
             layer.closeAll();
         },
         error : function() {
             alert("异常!");
         }
     });
 });

有好的建议,请在下方输入你的评论。


欢迎访问个人博客:guanchao.site


欢迎访问我的小程序:打开微信->发现->小程序->搜索“时间里的”


作者:camellia
来源:juejin.cn/post/7111487878546341919
收起阅读 »

差两个像素让我很难受,这问题绝不允许留到明年!

web
2022年8月8日,linxiang07 同学给我们的 Vue DevUI 提了一个 Issue: #1199 Button/Search/Input/Select等支持设置size的组件标准不统一,并且认真梳理了现有支持size属性的组件列表和每个组件大中小...
继续阅读 »

2022年8月8日,linxiang07 同学给我们的 Vue DevUI 提了一个 Issue:
#1199 Button/Search/Input/Select等支持设置size的组件标准不统一,并且认真梳理了现有支持size属性的组件列表和每个组件大中小尺寸的现状,整理了一个表格(可以说是提 Issue 的典范,值得学习)。



不仅如此,linxiang 同学还提供了详细的修改建议:



  1. 建议xs、 sm 、md、lg使用标准的尺寸

  2. 建议这些将组件的尺寸使用公共的sass变量

  3. 建议参考社区主流的尺寸

  4. 考虑移除xs这个尺寸、或都都支持xs


作为一名对自己有要求的前端,差两个像素不能忍


如果业务只使用单个组件,可能看不太出问题,比如 Input 组件的尺寸如下:



  • sm 24px

  • md 26px

  • lg 44px



Select 组件的尺寸如下:



  • sm 22px

  • md 26px

  • lg 42px



当 Input 和 Select 组件单独使用时,可能看不出什么问题,但是一旦把他俩放一块儿,问题就出来了。



大家仔细一看,可以看出中间这个下拉框比两边输入框和按钮的高度都要小一点。


别跟我说你没看出来!作为一名资深的前端,像素眼应该早就该练就啦!


作为一名对自己严格要求的前端,必须 100% 还原设计稿,差两个像素怎么能忍!


vaebe: 表单 size 这个 已经很久了 争取不要留到23年


这时我们的 Maintainer 成员 vaebe 主动承担了该问题的修复工作(必须为 vaebe 同学点赞)。



看着只是一个 Issue,但其实这里面涉及的组件很多。


8月12日,vaebe 同学提了第一个修复该问题的 PR:


style(input): input组件的 size 大小


直到12月13日(今天)提交最后一个 PR:


cascader组件 props size 在表单内部时应该跟随表单变化


共持续5个月,累计提交34个PR,不仅完美地修复了这个组件尺寸不统一的问题,还完善了相关组件的单元测试,非常专业,必须再次给 vaebe 同学点赞。



关于 vaebe 同学


vaebe 同学是今年4月刚加入我们的开源社区的,一直有在社区持续作出贡献,修复了大量组件的缺陷,完善了组件文档,补充了单元测试,还为我们新增了 ButtonGroup 组件,是一位非常优秀和专业的开发者。



如果你也对开源感兴趣,欢迎加入我们的开源社区,添加小助手微信:opentiny-official,拉你进我们的技术交流群!


Vue DevUI:github.com/DevCloudFE/…(欢迎点亮 Star 🌟)


--- END ---


我是 Kagol,如果你喜欢我的文章,可以给我点个赞,关注我的掘金账号和公众号 Kagol,一起交流前端技术、一起做开源!


封面图来自B站UP主亿点点不一样的视频:吃毒蘑菇真的能见小人吗?耗时六个月拍下蘑菇的生长和繁殖


2.png


作者:Kagol
来源:juejin.cn/post/7176661549115768889
收起阅读 »

vue单页面应用部署配置

web
前端 Vue是一款非常流行的JavaScript框架,它提供了一套高效、灵活、易于使用的前端开发工具。在实际开发中,我们通常会使用Vue来构建单页面应用(SPA),并将其部署到服务器上以便用户访问。本篇博客将介绍如何进行Vue单页面应用的部署配置。 构建生产版...
继续阅读 »

前端


Vue是一款非常流行的JavaScript框架,它提供了一套高效、灵活、易于使用的前端开发工具。在实际开发中,我们通常会使用Vue来构建单页面应用(SPA),并将其部署到服务器上以便用户访问。本篇博客将介绍如何进行Vue单页面应用的部署配置。


构建生产版本


首先,我们需要将Vue应用程序构建为生产版本,这可以通过运行以下命令来完成:


npm run build

该命令将生成一个dist目录,其中包含了生产版本的所有必要文件,例如HTML、CSS、JavaScript等。在部署之前,我们需要将这些文件上传到服务器上,并将其存储在合适的位置。


配置Nginx服务器


接下来,我们需要将Vue应用程序与Nginx服务器结合起来,以便处理HTTP请求和响应。下面是一个简单的配置示例:


server {
listen 80;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}
}

在上面的示例中,我们定义了一个名为“example.com”的虚拟主机,并指定了根目录即Vue应用程序所在的dist目录。同时,我们还设置了默认的index.html文件,并通过location指令来处理所有的HTTP请求。


配置HTTPS加密连接


如果需要启用HTTPS加密连接,我们可以通过以下方式来进行配置:


server {
listen 443 ssl;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

location / {
try_files $uri $uri/ /index.html;
}
}

在上面的示例中,我们使用ssl指令来启用SSL/TLS支持,并设置了证书和私钥文件的路径。同时,我们还将所有HTTP请求重定向到HTTPS连接,以确保数据传输的安全性。


配置缓存和压缩


为了提高Vue应用程序的性能和响应速度,我们可以配置缓存和压缩。下面是一个简单的配置示例:


server {
listen 80;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

location / {
try_files $uri $uri/ /index.html;

expires 1d;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
}


在上面的示例中,我们使用expires指令来定义缓存时间,并使用gzip指令来启用Gzip压缩。同时,我们还设置了需要进行压缩的文件类型,例如文本、CSS、JavaScript等。


总结


以上就是Vue单页面应用的部署配置步骤。首先,我们需要构建生产版本,并将其上传到服务器上。然后,我们需要通过Nginx服务器来处理HTTP请求和响应,以及启用HTTPS加密连接、缓存和压缩等功能。了解这些配置信息,将有助于我们更好地部署和管理

作者:爱划水de鲸鱼哥
来源:juejin.cn/post/7222651312072802359
Vue单页面应用程序

收起阅读 »

css卡片悬停

web
前言 今天分享一个简单的卡片鼠标悬停动画,初始显示一张图片,当鼠标移至卡片上方时,显示文字,先来看看预览效果: 代码实现 页面布局 <div class="view view-first"> <img src="./images...
继续阅读 »

前言


今天分享一个简单的卡片鼠标悬停动画,初始显示一张图片,当鼠标移至卡片上方时,显示文字,先来看看预览效果:


1.gif

代码实现


页面布局


<div class="view view-first">  
<img src="./images/1.webp" />
<div class="mask">
<h2>Title</h2>
<p>Your Text</p>
<a href="#" class="info">Read More</a>
</div>

</div>

这段代码了一个用于展示图片的容器 <div> 元素,其中包含了一个图片 <img> 元素和一个用于显示图片标题、文字和链接的 <div> 元素。这个容器使用了类名为 viewview-first 的 CSS 类来进行样式控制。


页面样式


.view {
width: 1080px;
height: 1430px;
margin: 10px auto;
border: 10px solid red;
overflow: hidden;
position: relative;
text-align: center;
box-shadow: 1px 1px 2px #e6e6e6;
cursor: pointer;
}
.view .mask, .view .content {
width: 1080px;
height: 1430px;
position: absolute;
overflow: hidden;
top: 0;
left: 0
}
.view h2 {
text-transform: uppercase;
color: #fff;
text-align: center;
font-size: 180px;
padding: 10px;
background: rgba(0, 0, 0, 0.6);
margin: 220px 0 0 0
}
.view p {
font-family: Georgia, serif;
font-style: italic;
font-size: 120px;
color: #fff;
padding: 10px 20px 20px;
text-align: center
}
.view a.info {
display: inline-block;
text-decoration: none;
padding: 7px 14px;
font-size: 60px;
background: #000;
color: #fff;
text-transform: uppercase;
box-shadow: 0 0 1px #000
}
.view a.info:hover {
box-shadow: 0 0 5px #000
}


.view-first img {
transition: all 0.2s linear;
}
.view-first .mask {
opacity: 0;
background-color: rgba(219,127,8, 0.7);
transition: all 0.4s ease-in-out;
}
.view-first h2 {
transform: translateY(-100px);
opacity: 0;
transition: all 0.2s ease-in-out;
}
.view-first p {
transform: translateY(100px);
opacity: 0;
transition: all 0.2s linear;
}
.view-first a.info{
opacity: 0;
transition: all 0.2s ease-in-out;
}

.view-first:hover img {
transform: scale(1.2);
}
.view-first:hover .mask {
opacity: 0.8;
}
.view-first:hover h2,
.view-first:hover p,
.view-first:hover a.info {
opacity: 1;
transform: translateY(0px);
}
.view-first:hover p {
transition-delay: 0.1s;
}
.view-first:hover a.info {
transition-delay: 0.2s;
}

这段 CSS 代码定义了 .view.view-first 这两个类的样式属性。其中,.view 类定义了容器的基本样式,包括宽度、高度、边距、背景颜色、阴影等。.view-first 类定义了容器在鼠标悬停时的效果,包括图片放大、遮罩层透明度变化、标题、文字和链接的透明度和位置变化等。这段代码通过使用伪类 :hover 来控制在鼠标悬停时的效果。同时,这段 CSS 代码中包含了一些过渡效果(transition),通过设置不同的过渡时间和延迟时间,实现了在鼠标悬停时的平滑动画效果。同时,通过使用透明度(opacity)、位移(transform: translateY())和缩放(transform: scale())等属性,实现了图片和文字的渐现和渐变效果。接下来对各个样式进行详细解释:


.view {
width: 1080px;
height: 1430px;
margin: 10px auto;
border: 10px solid red;
overflow: hidden;
position: relative;
text-align: center;
box-shadow: 1px 1px 2px #e6e6e6;
cursor: pointer;
}

设置容器元素的宽度和高度,margin: 10px auto;设置容器元素的外边距,使其在水平方向上居中,上下边距为 10 像素,text-align: center;文本的水平对齐方式为居中,box-shadow: 1px 1px 2px #e6e6e6;设置容器元素的阴影效果,水平和垂直偏移都为 1 像素,模糊半径为 2 像素,阴影颜色为 #e6e6e6。cursor: pointer;设置鼠标悬停在容器元素上时的光标样式为手型。


.view .mask, .view .content {
width: 1080px;
height: 1430px;
position: absolute;
overflow: hidden;
top: 0;
left: 0
}

选中类名为 "mask" 和 "content" 的元素,采用绝对定位,设置topleft偏移量为0。


.view h2 {
text-transform: uppercase;
color: #fff;
text-align: center;
font-size: 180px;
padding: 10px;
background: rgba(0, 0, 0, 0.6);
margin: 220px 0 0 0
}

对字体颜色和大小进行设置,文字水平居中,设置背景色等,text-transform: uppercase;设置标题文本转换为大写。


.view p {
font-family: Georgia, serif;
font-style: italic;
font-size: 120px;
color: #fff;
padding: 10px 20px 20px;
text-align: center
}
.view a.info {
display: inline-block;
text-decoration: none;
padding: 7px 14px;
font-size: 60px;
background: #000;
color: #fff;
text-transform: uppercase;
box-shadow: 0 0 1px #000
}
.view a.info:hover {
box-shadow: 0 0 5px #000
}

对子元素p标签和指定a标签进行字体样式进行设置,text-decoration: none;去除下划线,a元素在鼠标悬停状态下的添加阴影。


.view-first img { 
transition: all 0.2s linear;
}
.view-first .mask {
opacity: 0;
background-color: rgba(219,127,8, 0.7);
transition: all 0.4s ease-in-out;
}
.view-first h2 {
transform: translateY(-100px);
opacity: 0;
transition: all 0.2s ease-in-out;
}
.view-first p {
transform: translateY(100px);
opacity: 0;
transition: all 0.2s linear;
}
.view-first a.info{
opacity: 0;
transition: all 0.2s ease-in-out;
}

.view-first:hover img {
transform: scale(1.2);
}
.view-first:hover .mask {
opacity: 0.8;
}
.view-first:hover h2,
.view-first:hover p,
.view-first:hover a.info {
opacity: 1;
transform: translateY(0px);
}
.view-first:hover p {
transition-delay: 0.1s;
}
.view-first:hover a.info {
transition-delay: 0.2s;
}

对各元素在鼠标悬停状态下的样式进行设置,并添加动画效果,主要动画元素transform: scale(1.2);图片在悬停状态下缩放1.2倍,transform: translateY(0px);在y轴上偏移量,transition-delay: 0.1s;动画延迟时间,ease-in-out缓入缓出。


结语


以上便是全部代码了,总体比较简单,只需要使用一些简单的动画属性即可,喜欢的小伙伴可以拿去看看,根据自己想要的效果进行修改。


作者:codePanda
来源:juejin.cn/post/7223742591372312636
收起阅读 »

裸辞半个月的程序猿在干什么?

序 8月1日,美好的一天,我果断裸辞,在公司1年半交接不过半天时间便匆匆结束了这在里的征程。心情由忧转喜再转忧再转喜,好比是坐山车似的来回起伏。 为什么选择裸辞? 裸辞并不是对自己的不负责任,也不是任性,对于我来说可能是一次重新的洗礼。 大家都明白现在的互...
继续阅读 »


8月1日,美好的一天,我果断裸辞,在公司1年半交接不过半天时间便匆匆结束了这在里的征程。心情由忧转喜再转忧再转喜,好比是坐山车似的来回起伏


image.png


为什么选择裸辞?


image.png


裸辞并不是对自己的不负责任,也不是任性,对于我来说可能是一次重新的洗礼。
大家都明白现在的互联网形势并不是很乐观,在我裸辞的前夕每日优鲜又爆雷了。在如此悲凉的环境之下为什么还是要义务反顾的选择裸辞?我自己裸辞主要有4个原因:


1.之前的工作确实让人心累且身体累。自己需要一段时间来恢复一下身心健康。因为身体是本钱,并不能因为自己是年轻人而肆意挥霍,提前透支。心情精神则更是重中之重,心情愉悦,才能百病不轻,才能有更加积极向上的态度。


2.自己没有大多生活的负担,如家庭,车贷房贷等。这可能也是年轻人这个阶段唯一的一次特权了,因为不久之后这些东西都会接踵而至,那个时候,裸辞这个词也许永远不可能出现在我的字典中。这一次自己还是要好好享受这人生的最后一次特权。


3.对于我这个阶段的程序员来说,好好规划自己的职业生涯显得尤为重要,选择远比努力来得重要。且这个阶段的程序员找工作我始终认为都是一个应该好好准备的过程,不仅仅是查漏补缺一些知识点,更是应该定下心来为自己的将来做一些规划。


4.大环境的不好对于我来说可能有些许影响,但是对自己足够自信的我相信这并不是挡住我裸辞的拦路虎。也许这个思想后面会有转变,但绝对不会是现在!


image.png


当然这些东西某种程度上来说也许都是借口,因为健身学习、查漏补缺、人生规划可以放在平时下班之后或者是双休日。并不一定需要一大段空闲时间来做这些事情。在没辞职之前我也是一直对自己这样说的,我确实也是这样做的,但是总是感觉效果不佳,或者说没有心思,也许这也是我自身意志不够的原因吧。但我相信不少人应该也是同我一样,心理知道但是却不能高效的做到。


裸辞真的好么?


裸辞真的好么?掘金上也有很多针对于年轻人裸辞好坏的讨论,但是对于我来说我觉得效果是不错的。


裸辞之后,我为自己制定了一份所谓的计划表,也确实都的做到了,因为这份计划表实在是太简单了,除了运动就是玩,没有任何学习的计划,持续2周。


1.5点半起床空腹晨跑,我坚持了一天最后被我无情删除了这个计划。(说实话真的难)最后被我改成了早晨起床后做一些室内的有氧运动。


2.上午就是玩手机看视频。(动漫补番,玩玩手游,看看电影电视剧)


3.下午2点-4点半健身房健身游泳。


4.晚上7-9点看书,看完夜跑5km,11点睡觉。


现在的我已经经过了2周的洗礼,体重减了3.4kg,整个人精神状态都好了不少,心情愉悦,一改之前的颓废。自己的改变自己才是最清楚的,文字真的很难描述清楚。有过这种感觉的人也许会明白。


裸辞之后我做了什么


image.png


上面讲到了我前2周的玩耍健身计划,那么后2周的学习健身计划也应该开始了,这篇文章就是计划的开始。运动健身看书的时间是不会做任何修改的,主要是把之前的玩乐的时间划3分之2给学习罢了,细节就不多说了。


我这个人缺点很多,尤其是不能长时间坚持,但是短时间的坚持我从来没有失败过。所以我给自己定制的计划时间都相对来说很短。毕竟裸辞,生活还是要继续的,再次找工作的时间区间也就1个多月时间罢了。



裸辞对于每个人来说意义效果都是不同的,对于我来说裸辞只是让我能够更好的开启我的下段征程。我分享自己的裸辞一是为了给自己一个交代与监督,二是为jym提供一些短浅的建议与经验而已。


在我学习健身计划结束的时候,我同样会分享一篇文章来对自己的第二阶段做一个总结。


非常感谢能看到这里的jym!


作者:姚某人
链接:https://juejin.cn/post/7132329117722083341
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 开发中必须了解的 Context

1. 什么是 context? 作为安卓开发工程师,Context是我们经常使用的一个重要概念。 从代码的角度来看,Context是一个抽象类,它代表着应用程序环境和运行时状态的信息。Context具有许多子类,包括Activity和Service等,每个子类...
继续阅读 »

1. 什么是 context?


作为安卓开发工程师,Context是我们经常使用的一个重要概念。


从代码的角度来看,Context是一个抽象类,它代表着应用程序环境和运行时状态的信息。Context具有许多子类,包括Activity和Service等,每个子类都代表着不同的应用程序环境和状态。


从设计的角度来看,Context是一个非常重要的概念,因为它允许我们在应用程序中访问系统资源,例如数据库,共享偏好设置和系统服务等。Context还允许我们在应用程序中创建新的组件,例如Activity和Service等。


实际上,Context在安卓开发中几乎无处不在。例如,我们可以使用Context来启动一个新的Activity,获取应用程序的资源,读取和写入文件,以及访问系统服务和传感器等。Context还可以帮助我们管理应用程序的生命周期,例如在应用程序销毁时释放资源。


总之,Context是安卓开发中不可或缺的概念,它允许我们访问系统资源,管理应用程序的生命周期,并与系统交互。理解Context的概念和使用方法对于成为一名优秀的安卓开发工程师至关重要。


2. context继承关系


Context
├── ContextImpl
├── ContextWrapper
│ ├── Application
│ ├── Service
│ ├── ContextThemeWrapper
│ │ ├── Activity
│ │ │ ├── FragmentActivity
│ │ │ └── ...
│ │ └── ...
│ └── ...
└── ...

Context是一个抽象类,它有多个直接或间接的子类。


ContextImpl是Context的一个实现类,真正实现了Context中的所有函数,所调用的各种Context类的方法,其实现均来自于该类。


ContextWrapper是一个包装类,它可以包装另一个Context对象,并在其基础上添加新的功能。内部包含一个真正的Context引用,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。


ContextThemeWrapper是一个特殊的包装类,它可以为应用程序的UI组件添加主题样式。主题就是指Activity元素指定的主题。只有Activity需要主题,所以Activity继承自ContextThemeWrapper,而Application和Service直接继承自ContextWrapper。


总之,Context的继承关系非常复杂,但是理解这些关系对于在安卓开发中正确地使用Context非常重要。通过继承关系,我们可以了解每个Context子类的作用和用途,并且可以选择合适的Context对象来访问应用程序的资源和系统服务。


3.Context如何创建


在安卓应用程序中,Activity是通过调用startActivity()方法来启动的。当我们启动一个Activity时,系统会通过调用Activity的生命周期方法来创建、启动和销毁Activity对象。
而其中创建Activity的方法最终是走到ActivityThread.performLaunchActivity()方法。将其中无关方法删除后:
、、、
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
//创建 ContextImpl对象
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;


    //创建Activity对象
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
//Activity初始化
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken, r.shareableActivityToken);
//Theme设置
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

return activity;
}

、、、
可以看到Activity的创建过程十分清楚:



  1. 创建ContextImpl对象,方法最终走到静态方法ContextImpl.createActivityContext()创建。

  2. 创建Activity对象,最终instantiateActivity()通过调用Class的newInstance()方法,反射创建出来,方法注解到This method is only intended to provide a hook for instantiation. It does not provide earlier access to the Activity object. The returned object will not be initialized as a Context yet and should not be used to interact with other android APIs.,方法只创建了Activity的早期对象,并没有对它做Context的初始化,所以不能调用安卓相关api。简单来说,Activity本身继承自ContextWrapper,这个方法并没有具体实现任何Context的方法,只是将所有方法代理给了内部的baseContext,所以反射创建后,调用任何的系统的方法都是无效的。

  3. Activity初始化,调用Activity.attch(),这个方法对Activity做各种所需的初始化,Context、Thread、parent、Window、Token等等,而Context的初始化就是调用ContextWrapper.attachBaseContext()把第一步创建的ContextImpl设置到baseContext。

  4. Theme设置,前面说到Activity实现的是ContextThemeWrapper,对ContextWrapper扩展并支持了Theme的替换,调用ContextThemeWrapper.setTheme()完成Theme的初始化。


4.一些思考




  1. ContextThemeWrapper作为ContextWrapper一个扩展,它是重写了ContextImpl中的一些关于Theme的实现,也就是说ContextImpl本身也是有Theme的实现,它提供的Theme是整个APP的Theme,而这里扩展了之后,支持了Theme的替换之后,在不同的页面支持了不同的Theme设置。




  2. Context作为应用程序环境和运行时状态的信息,设计初衷上它应该是固定的,在创建成功之后就禁止改变,所以在ContextWrapper.attachBaseContext()中设置了拦截,只允许设置一次baseContext,重新设置会抛出异常。但是在一些特殊的场景中,比如跨页面使用View,或者提前创建View的时候,其实会有场景涉及替换Context。另一个坑是ContextWrapper限制baseContext只允许系统调用。不过在SDK31中,官方提供了一个特殊版本的ContextWrapper,也就是MutableContextWrapper,支持了替换baseContext。




  3. Context设计是很典型的装饰器模式,Context抽象定义了具体的接口;ContextImpl具体实现了Context定义的所有方法;ContextWrapper继承了Context接口,并包装了具体实现ContextImpl;ContextThemeWrapper继承了ContextWrapper并扩展了替换Theme的功能。




5. 附录


装饰器模式是一种结构型设计模式,它允许我们在运行时动态地为一个对象添加新的行为,而无需修改其源代码。装饰器模式通过将对象包装在一个装饰器对象中,来增加对象的功能。装饰器模式是一种非常灵活的模式,它可以在不改变原始对象的情况下,动态地添加新的行为和功能。


装饰器模式的核心思想是将对象包装在一个或多个装饰器对象中,这些装饰器对象具有与原始对象相同的接口,可以在不改变原始对象的情况下,为其添加新的行为。装饰器对象可以嵌套在一起,形成一个链式结构,从而实现更复杂的功能。


装饰器模式的结构由四个基本元素组成:




  1. 抽象组件(Component):定义了一个对象的基本接口,可以是一个抽象类或接口。




  2. 具体组件(ConcreteComponent):实现了抽象组件接口,是被装饰的对象。




  3. 抽象装饰器(Decorator):继承或实现了抽象组件接口,用于包装具体组件或其他装饰器。




  4. 具体装饰器(ConcreteDecorator):继承或实现了抽象装饰器接口,实现了具体的装饰逻辑。




装饰器模式的优点在于:




  1. 可以动态地为对象添加新的行为,无需修改其源代码。




  2. 可以嵌套多个装饰器对象,形成一个链式结构,从而实现更复杂的功能。




  3. 装饰器对象与原始对象具有相同的接口,可以完全替代原始对象。




装饰器模式的缺点在于:




  1. 可能会导致类的数量增加,增加代码的复杂度。




  2. 在装饰器链中,有些装饰器可能不被使用,但仍然需要创建和维护,浪费资源。


作者:bufan
链接:https://juejin.cn/post/7224433996137431101
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 官方项目是怎么做模块化的?快来学习下

概述 模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。 模块化的好处 模块化有以下好处: 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。 支持并行工...
继续阅读 »

概述


模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。


模块化的好处


模块化有以下好处:



  • 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。

  • 支持并行工作:模块化有助于减少代码冲突,为大型团队中的开发人员提供更高效的并行工作。

  • 所有权:一个模块可以有一个专门的 owner,负责维护代码和测试、修复错误和审查更改。

  • 封装:独立的代码更容易阅读、理解、测试和维护。

  • 减少构建时间:利用 Gradle 的并行和增量构建可以减少构建时间。

  • 动态交付:模块化是 Play 功能交付 的一项要求,它允许有条件地交付应用程序的某些功能或按需下载。

  • 可重用性:模块化为代码共享和构建多个应用程序、跨不同平台、从同一基础提供了机会。


模块化的误区


模块化也可能会被滥用,需要注意以下问题:



  • 太多模块:每个模块都有其成本,如 Gradle 配置的复杂性增加。这可能会导致 Gradle 同步及编译时间的增加,并产生持续的维护成本。此外,与单模块相比,添加更多模块会增加项目 Gradle 设置的复杂性。这可以通过使用约定插件来缓解,将可重用和可组合的构建配置提取到类型安全的 Kotlin 代码中。在 Now in Android 应用程序中,可以在 build-logic文件夹 中找到这些约定插件。

  • 没有足够的模块:相反,如果你的模块很少、很大并且紧密耦合,最终会产生另外的大模块。这将失去模块化的一些好处。如果您的模块臃肿且没有单一的、明确定义的职责,您应该考虑将其进一步拆分。

  • 太复杂了:模块化并没有灵丹妙药 -- 一种方案解决所有项目的模块化问题。事实上,模块化你的项目并不总是有意义的。这主要取决于代码库的大小和相对复杂性。如果您的项目预计不会超过某个阈值,则可扩展性和构建时间收益将不适用。


模块化策略


需要注意的是没有单一的模块化方案,可以确保其对所有项目都适用。但是,可以遵循一般准则,可以尽可能的享受其好处并规避其缺点。


这里提到的模块,是指 Android 项目中的 module,通常会包含 Gradle 构建脚本、源代码、资源等,模块可以独立构建和测试。如下:


一般来说,模块内的代码应该争取做到低耦合、高内聚。



  • 低耦合:模块应尽可能相互独立,以便对一个模块的更改对其他模块的影响为零或最小。他们不应该了解其他模块的内部工作原理。

  • 高内聚:一个模块应该包含一组充当系统的代码。它应该有明确的职责并保持在某些领域知识的范围内。例如,Now in Android 项目中的core-network模块负责发出网络请求、处理来自远程数据源的响应以及向其他模块提供数据。


Now in Android 项目中的模块类型



注:模块依赖图(如下)可以在模块化初期用于可视化各个模块之间的依赖关系。



modularization-graph.png


Now in Android 项目中有以下几种类型的模块:



  • app 模块: 包含绑定其余代码库的应用程序级和脚手架类,app例如和应用程序级受控导航。一个很好的例子是通过导航设置和底部导航栏设置。该模块依赖于所有模块和必需的模块。

  • feature- 模块: 功能特定的模块,其范围可以处理应用程序中的单一职责。这些模块可以在需要时被任何应用程序重用,包括测试或其他风格的应用程序,同时仍然保持分离和隔离。如果一个类只有一个feature模块需要,它应该保留在该模块中。如果不是,则应将其提取到适当的core模块中。一个feature模块不应依赖于其他功能模块。他们只依赖于core他们需要的模块。

  • core-模块:包含辅助代码和特定依赖项的公共库模块,需要在应用程序中的其他模块之间共享。这些模块可以依赖于其他核心模块,但它们不应依赖于功能模块或应用程序模块。

  • 其他模块 - 例如和模块syncbenchmark、 test以及 app-nia-catalog用于快速显示我们的设计系统的目录应用程序。


项目中的主要模块


基于以上模块化方案,Now in Android 应用程序包含以下模块:



































































模块名职责关键类及核心示例
app将应用程序正常运行所需的所有内容整合在一起。这包括 UI 脚手架和导航。NiaApp, MainActivity 应用级控制导航通过 NiaNavHost, NiaTopLevelNavigation
feature-1, feature-2 ...与特定功能或用户相关的功能。通常包含从其他模块读取数据的 UI 组件和 ViewModel。如:feature-author在 AuthorScreen 上显示有关作者的信息。feature-foryou它在“For You” tab 页显示用户的新闻提要和首次运行期间的入职。AuthorScreen AuthorViewModel
core-data保存多个特性模块中的数据。TopicsRepository AuthorsRepository
core-ui不同功能使用的 UI 组件、可组合项和资源,例如图标。NiaIcons NewsResourceCardExpanded
core-common模块之间共享的公共类。NiaDispatchers Result
core-network发出网络请求并处理对应的结果。RetrofitNiANetworkApi
core-testing测试依赖项、存储库和实用程序类。NiaTestRunner TestDispatcherRule
core-datastore使用 DataStore 存储持久数据。NiaPreferences UserPreferencesSerializer
core-database使用 Room 的本地数据库存储。NiADatabase DatabaseMigrations Dao classes
core-model整个应用程序中使用的模型类。Author Episode NewsResource
core-navigation导航依赖项和共享导航类。NiaNavigationDestination

Now in Android 的模块化


Now in Android 项目中的模块化方案是在综合考虑项目的 Roadmap、即将开展的工作和新功能的情况下定义的。Now in Android 项目的目标是提供一个接近生产环境的大型 App 的模块化方案,并且要让方案看起来并没有过度模块化,希望是在两者之间找到一种平衡。


这种方法与 Android 社区进行了讨论,并根据他们的反馈进行了改进。这里并没有一个绝对的正确答案。归根结底,模块化 App 有很多方法和方法,没有唯一的灵丹妙药。这就需要在模块化之前考虑清楚目标、要解决的问题已经对后续工作的影响,这些特定的情况会决定模块化的具体方案。可以绘制出模块依赖关系图,以便帮助更好地分析和规划。


这个项目就是一个示例,并不是一个需要固守不可改变固定结构,相反而是可以根据需求就行变化的。根据 Now in Android 这是我们发现最适合我们项目的一般准则,并提供了一个示例,可以在此基础上进一步修改、扩展和构建。如果您的数据层很小,则可以将其保存在单个模块中。但是一旦存储库和数据源的数量开始增长,可能值得考虑将它们拆分为单独的模块。


最后,官方对其他方式的模块化方案也是持开发态度,有更好的方案及建议也可以反馈出来。


总结


以上内容是根据 Modularization learning journey 翻译整理而得。整体上是提供了一个示例,对一些初学者有一个可以参考学习的工程,对社区中模块化开发起到的积极的作用。说实话,这部分技术在国内并不是什么新技术了。


下面讲一个我个人对这个模块化方案的理解,以下是个人观点,请批判性看待。


首先是好的点提供了通用的 Gradle 配置,简化了各个模块的配置步骤,各种方式预计会在之后的一些项目中流行开来。


不足的点就是没有明确模块化的整体策略,是应采取按照功能还是按照特性分,类似讨论还有我们平时的类文件是按照功能来分还是特性来分,如下是按照特性区分:


# DO,建议方式
- Project
- feature1
- ui
- domain
- data
- feature2
- ui
- domain
- data
- feature3

按照功能区分的方式大致如下:


# DO NOT,不建议方式
- Project
- ui
- feature1
- feature2
- feature3
- domain
- feature1
- feature2
- feature3
- data

我个人是倾向去按照特性的方式区分,而示例中看上去是偏后者,或者是一个混合体,比如有的模块是添加 feature 前缀的,但是 core-model 模块又是在统一的一个模块中集中管理。个人建议的方式应该是将各个模块中各自使用的模型放到自己的模块中,否则项目在后续进行组件化时将会遇到频繁发版的问题。当然,这种方式在模块化的阶段并没有什么大问题。


模块化之后就是组件化,组件化之后就是壳工程,每个技术阶段对应到团队发展的阶段,有机会的话后面可以展开聊聊。


作者:madroid
链接:https://juejin.cn/post/7128069998978793509
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员的坏习惯

前言 每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。 不遵循项目规范 每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、...
继续阅读 »

前言


每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。


图片.png


不遵循项目规范


每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、魔鬼数字、提交代码覆盖他人代码等问题经常发生,如果大家能够遵循相关规范,这些问题都可以避免。


用复杂SQL语句来解决问题


程序员在开发功能时,总想着是否能用一条SQL语句来完成这个功能,于是实现的SQL语句写的非常复杂,包含各种子查询嵌套,函数转换等。这样的SQL语句一旦出现了性能问题,很难进行相关优化。


缺少全局把控思维,只关注某一块业务


新增新功能只关注某一小块业务,不考虑系统整体的扩展性,其他模块已经有相关的实现了,却又重复实现,导致重复代码严重。修改功能不考虑对其他模块的影响。


函数复杂冗长,逻辑混乱


一个函数几百行,复杂函数不做拆分,导致代码变得越来月臃肿,最后谁也不敢动。函数还是要遵循设计模式的单一职责,一个函数只做一件事情。如果函数逻辑确实复杂,需要进行拆分,保证逻辑清晰。


缺乏主动思考,拿来主义


实现相关功能,先网上百度一下,拷贝相关的代码,能够运行成功认为万事大吉。到了生产却出现了各种各样的问题,因为网上的demo程序和实际项目的在场景使用上有区别,尤其是相关的参数配置,一定要弄清楚具体的含义,不同场景下,设置参数的值不同。


核心业务逻辑,缺少相关日志和注释


很多核心的业务逻辑实现,整个方法几乎没看到相关注释和日志打印,除了自己能看懂代码逻辑,其他人根本看不懂。一旦生产出了问题,找不到有效的日志输出,问题根本无法定位。


修改代码,缺少必要测试


很多人都会存在侥幸心里,认为只是改了一个变量或者只修改一行代码,不用自测了应该没有问题,殊不知就是因为改一行代码导致了严重的bug。所以修改代码一定要进行自测。


需求没理清,直接写代码


很多程序员在接到需求后,不怎么思考就开始写代码,写着写着发现自己的理解与实际的需求有偏差,造成无意义返工。所以需要多花些时间梳理需求,整理相关思路,能规避很多不合理的问题。


讨论问题,表达没有逻辑、没有重点


讨论问题不交代背景,上来就说自己的方案,别人听得云里雾里,让你从头描述你又讲不明。需要学会沟通和表达,才能进行有效的沟通和合作。


不能从错误中吸取教训


作为一位开发人员,你会犯很多错误,这不可避免也没什么大不了的。但如果你总是犯同样的错误,不能从中吸取教训,那态度就出现问题了。


总结


关于这些坏习惯,你是否中招了,大家应该尽早规避这些坏习惯,成为一名优秀的程序员。


作者:剑圣无痕
链接:https://juejin.cn/post/7136455796979662862
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

在字节跳动实习后,程序员是这样写简历的

你在每一段经历中的收获,都会变成简历上的信息。 那么,字节跳动的技术实习生们,都收获了些什么呢? 我们要来了四位技术实习生的简历,上面写着他们在字节跳动实习究竟做了什么、学了什么、有哪些方面的成长。 今天,咱们假装自己是 HR,来看看几位技术实习生们究竟有怎样...
继续阅读 »

你在每一段经历中的收获,都会变成简历上的信息。


那么,字节跳动的技术实习生们,都收获了些什么呢?


我们要来了四位技术实习生的简历,上面写着他们在字节跳动实习究竟做了什么、学了什么、有哪些方面的成长。


今天,咱们假装自己是 HR,来看看几位技术实习生们究竟有怎样的履历吧。






在字节跳动的不同业务中,


技术实习生同学都在充分被信任的情况下,


做着不输正式员工的工作。


在 Leader 指引下进步,


在 mentor 带领下学习,


不断试错,不断创新,


不断创造更有价值的技术。


大胆投递简历,


你也可以和上面四位同学一样,


用真实战,练真本事。


作者:字节跳动技术范儿
链接:https://juejin.cn/post/7221781646719418425
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Android】书客编辑器安卓Java版

书客编辑器是一款基于Markdown标记语言的开源的富文本编辑器,它以简易的操作界面和强大的功能深受广大开发者的喜爱。正如官方所说:现在的版本不一定是最好的版本,却是最好的开源版本。官方地址:editor.ibooker.cc。 下面针对书客编辑器安卓Java...
继续阅读 »

书客创作


书客编辑器是一款基于Markdown标记语言的开源的富文本编辑器,它以简易的操作界面和强大的功能深受广大开发者的喜爱。正如官方所说:现在的版本不一定是最好的版本,却是最好的开源版本。官方地址:editor.ibooker.cc


下面针对书客编辑器安卓Java版,进行详解说明。


效果图


在进行讲解之前,首先看一下书客编辑器安卓版的效果图:


书客编辑器安卓版效果图


一、引入资源


引入书客编辑器安卓Java版的方式有很多,这里主要提供两种方式:


1、在build.gradle文件中添加以下代码:


allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

dependencies {
compile 'com.github.zrunker:IbookerEditorAndroid:v1.0.1'
}

2、在maven文件中添加以下代码:


<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>

<dependency>
<groupId>com.github.zrunker</groupId>
<artifactId>IbookerEditorAndroid</artifactId>
<version>v1.0.1</version>
</dependency>

二、使用


书客编辑器安卓版简易所在就是只需要简单引入资源之后,可以直接进行使用。因为书客编辑器安卓版不仅仅提供了功能实现,还提供了界面。所以使用过程中,连界面绘制都不用了。


界面分析


书客编辑器安卓版界面大致分为三个部分,即编辑器顶部,内容区(编辑区+预览区)和底部(工具栏)。


书客编辑器安卓-布局轮廓图


首先在布局文件中引入书客编辑器安卓版控件,如布局文件为activity_main.xml,只需要在该文件内添加以下代码即可:


<?xml version="1.0" encoding="utf-8"?>
<cc.ibooker.ibookereditorlib.IbookerEditorView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ibookereditorview"
android:layout_width="match_parent"
android:layout_height="match_parent" />

实际上IbookerEditorView继承LinearLayout,所以它具备LinearLayout的一切功能。


三、功能介绍


根据轮廓图可以看出,书客编辑器安卓版布局只有三个部分,所以关于书客编辑器安卓版功能模块也就分三个部分对外提供使用,即修改哪一个布局模块就是对于哪一个功能模块。


顶部功能模块


书客编辑器安卓版顶部实际上是采用IbookerEditorTopView控件进行呈现,所以要实现顶部相关控件功能首先要获取该控件。


书客编辑器安卓版顶部


书客编辑器安卓版顶部界面图,从左到右分别对应返回(back),撤销(undo),重做(redo),编辑模式(edit),预览模式(preview),帮助(help),关于(about)。知道每个按钮对应的功能,所以就可以去修改或完善相关实现过程。


例如修改返回按钮一些属性,可以使用一下代码:


// 设置书客编辑器顶部布局相关属性
ibookerEditorView.getIbookerEditorTopView()
.setBackImgVisibility(View.VISIBLE)
.setBackImageResource(R.mipmap.ic_launcher);

当然也可以通过IbookerEditorTopView获取相关控件,然后针对该控件进行逐一处理:


ibookerEditorView.getIbookerEditorTopView()
.getBackImg()
.setVisibility(View.VISIBLE);

这里只是使用返回按钮进行举例说,其他按钮使用规则更返回按钮一样。


中间功能模块


书客编辑器安卓版中间区域又分为两个部分,分别是编辑部分和预览部分,所以要修改相关功能就要获取到相关部分的控件。其中编辑部分由IbookerEditorEditView控件进行呈现,预览部分由IbookerEditorPreView控件进行呈现。


例如修改编辑部分相关属性,可以使用如下代码:


// 设置书客编辑器中间布局相关属性
ibookerEditorView.getIbookerEditorVpView().getEditView()
.setIbookerEdHint("书客编辑器")
.setIbookerBackgroundColor(Color.parseColor("#DDDDDD"));

编辑部分并不是只有一个控件,所以也可以获取相关控件,然后针对特定控件进行逐一操作:


ibookerEditorView.getIbookerEditorVpView()
.getEditView()
.getIbookerEd()
.setText("书客编辑器");

// 执行预览功能
ibookerEditorView.getIbookerEditorVpView()
.getPreView()
.ibookerHtmlCompile("预览内容");

底部功能模块


书客编辑器安卓版,底部为工具栏,由IbookerEditorToolView进行呈现。


工具栏一共提供了30多种功能,每一个按钮对应一个功能。各个控件分别为:


boldIBtn, italicIBtn, strikeoutIBtn, underlineIBtn, capitalsIBtn, 
uppercaseIBtn, lowercaseIBtn, h1IBtn, h2IBtn,
h3IBtn, h4IBtn, h5IBtn, h6IBtn, linkIBtn, quoteIBtn,
codeIBtn, imguIBtn, olIBtn, ulIBtn, unselectedIBtn,
selectedIBtn, tableIBtn, htmlIBtn, hrIBtn, emojiIBtn;

所以要修改底部相关属性,首先要获取到IbookerEditorToolView控件,然后对该控件进行操作。


// 设置书客编辑器底部布局相关属性
ibookerEditorView.getIbookerEditorToolView()
.setEmojiIBtnVisibility(View.GONE);

当然底部一共有30多个控件,也可以直接获取到相关控件,然后该控件进行操作,如:


ibookerEditorView.getIbookerEditorToolView().getEmojiIBtn().setVisibility(View.GONE);

补充功能:按钮点击事件监听


这里的按钮点击事件监听主要是针对顶部布局按钮和底部布局按钮。


顶部部分按钮点击事件监听,需要实现IbookerEditorTopView.OnTopClickListener接口,而每个按钮点击通过对应Tag来判断,具体代码如下:


// 顶部按钮点击事件监听
@Override
public void onTopClick(Object tag) {
if (tag.equals(IMG_BACK)) {// 返回
} else if (tag.equals(IBTN_UNDO)) {// 撤销
} else if (tag.equals(IBTN_REDO)) {// 重做
} else if (tag.equals(IBTN_EDIT)) {// 编辑
} else if (tag.equals(IBTN_PREVIEW)) {// 预览
} else if (tag.equals(IBTN_HELP)) {// 帮助
} else if (tag.equals(IBTN_ABOUT)) {// 关于
}
}

其中IMG_BACK、IBTN_UNDO等变量是由IbookerEditorEnum枚举类提供。


底部部分按钮点击事件监听,需要实现IbookerEditorToolView.OnToolClickListener接口,而每个按钮点击通过对应Tag来判断,具体代码如下:


// 工具栏按钮点击事件监听
@Override
public void onToolClick(Object tag) {
if (tag.equals(IBTN_BOLD)) {// 加粗
} else if (tag.equals(IBTN_ITALIC)) {// 斜体
} else if (tag.equals(IBTN_STRIKEOUT)) {// 删除线
} else if (tag.equals(IBTN_UNDERLINE)) {// 下划线
} else if (tag.equals(IBTN_CAPITALS)) {// 单词首字母大写
} else if (tag.equals(IBTN_UPPERCASE)) {// 字母转大写
} else if (tag.equals(IBTN_LOWERCASE)) {// 字母转小写
} else if (tag.equals(IBTN_H1)) {// 一级标题
} else if (tag.equals(IBTN_H2)) {// 二级标题
} else if (tag.equals(IBTN_H3)) {// 三级标题
} else if (tag.equals(IBTN_H4)) {// 四级标题
} else if (tag.equals(IBTN_H5)) {// 五级标题
} else if (tag.equals(IBTN_H6)) {// 六级标题
} else if (tag.equals(IBTN_LINK)) {// 超链接
} else if (tag.equals(IBTN_QUOTE)) {// 引用
} else if (tag.equals(IBTN_CODE)) {// 代码
} else if (tag.equals(IBTN_IMG_U)) {// 图片
} else if (tag.equals(IBTN_OL)) {// 数字列表
} else if (tag.equals(IBTN_UL)) {// 普通列表
} else if (tag.equals(IBTN_UNSELECTED)) {// 复选框未选中
} else if (tag.equals(IBTN_SELECTED)) {// 复选框选中
} else if (tag.equals(IBTN_TABLE)) {// 表格
} else if (tag.equals(IBTN_HTML)) {// HTML
} else if (tag.equals(IBTN_HR)) {// 分割线
}
}

其中IBTN_BOLD、IBTN_ITALIC等变量是由IbookerEditorEnum枚举类提供。


Github地址
阅读原文




微信公众号:书客创作


作者:非言
链接:https://juejin.cn/post/7110187212729221156
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

常用到的几个Kotlin开发技巧,减少对业务层代码的入侵

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。 善用@get/@set: JvmName()注解并搭配setter/getter使用 假设当前存在下面三个类代码: #Opt1 public cl...
继续阅读 »

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。



善用@get/@set: JvmName()注解并搭配setter/getter使用


假设当前存在下面三个类代码:


#Opt1


public class Opt1 {

private String mContent;

public String getRealContent() {
return mContent;
}

public void setContent(String mContent) {
this.mContent = mContent;
}
}

#Opt2


public class Opt2 {

public void opt2(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

@Opt3


public class Opt3 {

public void opt3(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

这个时候我想将Opt1类重构成kotlin,我们先看下通过AS的命令Convert Java File to Kotlin File自动转换的结果:


image.png


可以看到为了兼容Opt2Opt3的调用,直接把我的属性名给改成了realContent,kotlin会自动生成getRealContent()setRealContent()方法,这样Opt2Opt3就不用进行任何调整了,kotlin这样就显得太过于智能了。


这样看起来没啥问题,但是java重构kotlin,直接把属性名给我改了,并隐式生成了属性的set和get方法,对于java而言不使用的方法会报灰提示或者只有当前类使用AS会警告可以声明成private,但是对于kotlin生成的set、get方法是隐式的,容易忽略。


所以大家在使用Convert Java File to Kotlin File命令将java重构kotlin的结果一定不能抱有百分之百的信任,即使它很智能,但还是一定要细细的看下转换后的代码逻辑,可能还有不少的优化空间。


这个地方就得需要我们手动进行修改了,比如不想对外暴露修改这个字段的set方法,调整如下:


class Opt1 {
var realContent: String? = null
private set
}

再比如保持原有的字段名mContent,不能被改为realContent,同时又要保证兼容Opt2Opt3类的调用不能报错,且尽量避免去修改里面的代码,我们就可以做如下调整:


class Opt1 {
@get: JvmName("getRealContent")
var mContent: String? = null
private set
}

善用默认参数+@JvmOverloads减少模板代码编写


假设当前Opt1有下面的方法:


public String getSqlCmd(String table) {
return "select * from " + table;
}

且被Opt2Opt3进行了调用,这个时候如果有另一个类Opt3想要调用这个函数并只想从数据库查询指定字段,如果用java实现有两种方式:



  1. 直接在getSqlCmd()方法中添加一个查询字段参数,如果传入的值为null,就查询所有的字段,否则就查询指定字段:


public String getSqlCmd(String table, String name) {
if (TextUtils.isEmpty(name)) {
return "select * from " + table;
}
return "select " + name + " from " + table;
}

这样一来,是不是原本Opt2Opt3getSqlCmd()方法调用是不是需要改动,多传一个参数给方法,而在日常的项目开发中,有可能这个getSqlCmd()被几十个地方调用,难道你一个个的改过去?不太现实且是一种非常糟糕的实现。



  1. 直接在Opt1中新增一个getSqlCmd()的重载方法,传入指定的字段去查询:


public String getSqlCmd(String table,String name) {
return "select " + name + " from " + table;
}

这样做的好处就是不用调整Opt2Opt3getSqlCmd(String table)方法调用逻辑,但是会编写很多模板代码,尤其是getSqlCmd()这个方法体可能七八十行的情况下。


如果Opt1类代码减少即200-400行且不负责的情况下,我们可以将其重构成kotlin,借助于默认参数来实现方法功能增加又不用编写模板代码的效果(如果你的Java类上千行又很复杂,请谨慎转换成kotlin使用下面这种方式)。


@JvmOverloads
fun getSqlCmd(table: String, name: String? = null): String {
return "select ${if (name.isNullOrEmpty()) "*" else name} from $table"
}

添加默认参数name时还要添加@JvmOverloads注解,这样是为了保证java只传一个table参数也能正常调用。


通过上面这种方式,我们就能保证实现了方法功能增加,又不用改动Opt2Opt3对于getSqlCmd()方法的调用逻辑,并且还不用编写额外的模板代码,一举多得。


总结


本篇文章主要介绍了在java重构成kotlin过程中比较常用到的两个技巧,最终实现效果是减少对业务逻辑代码的入侵,希望能对你有所帮助。


作者:长安皈故里
链接:https://juejin.cn/post/7148823027608715295
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用力一瞥Android渲染机制-黄油计划

一. 渲染基本概念 对于渲染来说在开始前我们先了解几个概念: CPU主要负责包括 Measure,Layout,Record,Execute 的计算操作。 GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(...
继续阅读 »

一. 渲染基本概念


对于渲染来说在开始前我们先了解几个概念:


CPU主要负责包括 MeasureLayoutRecordExecute 的计算操作。


GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(像素)以用于显示设备输出的过程,简单来说就是将我们要显示的视图,转换成用像素来表示的格式。


帧率代表了GPU在一秒内绘制操作的帧数。


刷新率代表了屏幕在一秒内刷新屏幕的次数,Android手机一般为60HZ。


二. Android黄油计划


涉及到滑动流畅,Android在谷歌4.1版本引入了黄油计划。其中有三个重要的核心元素:VSYNC、缓存区和Choreographer:


2.1 VSYNC信号


在Android4.0的时候,CPU可能会因为在忙其他的事情,导致没来得及处理UI绘制。为了解决这个问题,设计成系统在收到VSYN信号后,才会开始下一帧的渲染。也就是收到VSYN通知,CPU和GPU才开始计算然后把数据写入buffer中。


VSYN信号是由屏幕产生的,并且以60fps的固定频率发送给Android系统,在Android系统中的SurfaceFlinger接收发送的Vsync信号。当屏幕从缓存区扫描完一帧到屏幕上之后,开始扫描下一帧之前,发出的一个同步信号,该信号用来切换前缓冲区和后缓冲区。


在引入了Vsyn信号之后,绘制就变成了:


image.png


可以看到渲染的时候从第0帧开始,CPU开始准备第一帧的图形处理,好了才交给GPU进行处理,再上一帧到来之后,CPU就会开始第二帧的处理,基本上跟Vsync的信号保持同步。


有了Vsync机制,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。


2.2 三重缓存


在采用双缓冲机制的时候,也意味着有两个缓存区,分别是让绘制和显示器拥有各自的buffer,GPU使用Back Buffer进行一帧图像数据写入,显示器则是用Frame Buffer,一般来说CPU和GPU处理数据的速度视乎都能在16ms内完成,而且还有时间空余。但是一旦界面比较复杂的情况,CPU/GPU的处理时间超过了16ms,双缓冲开始失效了:


image.png


在第二个时间段内,因为GPU还是处理B帧,数据没有及时交换,导致继续系那是之前A缓存区中的内容。


在B帧完成之后,又因为缺少了Vusnc信号,只能等待一段时间。


直到下一个Vsync信号出现的时候,CPU/GPU才开始马上执行,由于执行时间仍然超过了16ms,导致下一次应该执行的缓存区交换又被推迟了,反复这种情形,就会出越来越多的jank。


为了解决这个问题,Android 4.1才引入了三缓冲机制:在双缓冲机制的基础上增加了一个Graohic Buffer缓冲区,这样就可以最大限度的利用空闲的时间。


image.png


可以看到在第二个时间段里有了区别,在第一次Vsync发生之后,CPU不用再等待了,它会使用第三个bufferC来进行下一帧的准备工作。整个过程就开始的时候卡顿了一下,后面还是很流畅的。但是GPU需要跨越两个Vsync信号才能显示,这样就还是会有一个延迟的现象。


总的来说三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了lag。


2.3 Choreographer


在了解了Vsync机制后,上层又是如何接受这个Vsync信号的?


Google为上层设计了一个Choreographer类,翻译成中文是“编舞者”,是希望通过它来控制上层的绘制(舞蹈)节奏。


可以直接从其构造函数开始看起:


private Choreographer(Looper looper, int vsyncSource) {
//创建Looper对象
mLooper = looper;
//接受处理消息
mHandler = new FrameHandler(looper);
//用来接受垂直同步脉冲,也就是Vsync信号
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
//计算下一帧的时间,Androoid手机屏幕是60Hz的刷新频率
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//初始化CallbackQueue,将在下一帧开始渲染时回调
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}

主要来看下FrameHandlerFrameDisplayEventReceiver的数据结构:


private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
//开始渲染下一帧的操作
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
//请求Vsync信号
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
//请求执行Callback
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}

FrameHandler可以看到对三种消息进行了处理,对其具体实现一会分析。


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, CONFIG_CHANGED_EVENT_SUPPRESS);
}

@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {

......
mTimestampNanos = timestampNanos;
mFrame = frame;
//将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

可以看出来这个类主要是用来接收底层的VSync信号开始处理UI过程。而Vsync信号是由SurfaceFlinger实现并定时发送,接收到之后就会调用onVsync方法,在里面进行处理消息发送到主线程处理,另外在run()方法里面执行了doFrame(),这也是接下来要关注的重点方法。


2.3.1 Choreographer执行过程



ViewRootImpl 中调用 Choreographer 的 postCallback 方法请求 Vsync 并传递一个任务(事件类型是 Choreographer.CALLBACK_TRAVERSAL)



最开始执行的是postCallBack发起回调,这个FrameCallback将会在下一帧渲染时执行。而其内部又调用了postCallbackDelayed方法,在其中又调用了postCallbackDelayedInternal方法:


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
......
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

在这里执行了时间的计算,如果立即就会调用scheduleFrameLocked方法,不然就会延迟发送一个MSG_DO_SCHEDULE_CALLBACK消息,并且在这里使用msg.setAsynchronous(true)讲消息设置成异步。、


而所对应的mHandle也就是之前的FrameHandler,根据消息类型MSG_DO_SCHEDULE_CALLBACK,最终会调用到doScheduleCallback方法:


void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}

到了这一步看到还是会调用到scheduleFrameLocked方法。


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//开启了Vsync
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}


if (isRunningOnLooperThreadLocked()) {
//申请Vsync信号
scheduleVsyncLocked();
} else {
//最终还是会调用到scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//如果没有直接使用Vsync的话,则直接通过该消息执行doFrame
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

在这里对是否使用Vsync信号进行处理,如果没有使用则直接通过消息执行doFrame。如果使用的就会先判断是否在当前Looper线程中运行,如果在的话就会请求Vsync信号,否则发送消息到 FrameHandler。直接来看下scheduleVsyncLocked方法:


 private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

可以看到调用了FrameDisplayEventReceiverscheduleVsync方法,通过查找在其父类DisplayEventReceiver中找到了scheduleVsync方法:


public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
//申请VSYNC信号,会回调onVsunc方法
nativeScheduleVsync(mReceiverPtr);
}
}

scheduleVsync()就是使用native方法nativeScheduleVsync()去申请VSYNC信号。等下一次信号接收后会调用dispatchVsync 方法:


private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) {
onVsync(timestampNanos, physicalDisplayId, frame);
}

这个onVsync方法最终实现也就是在FrameDisplayEventReceiver里。可以知道最终还是走到了doFrame方法里。


void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
......
//设置当前frame的Vsync信号到来时间
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
//时间差大于一个时钟周期,认为跳frame
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//跳frame数大于默认值,打印警告信息,默认值为30
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
//计算实际开始当前frame与时钟信号的偏差值
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}

//修正偏差值,忽略偏差,为了后续更好地同步工作
frameTimeNanos = startNanos - lastFrameOffset;
}

//若时间回溯,则不进行任何工作,等待下一个时钟信号的到来
if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync.");
}
//请求下一次时钟信号
scheduleVsyncLocked();
return;
}

......

//记录当前frame信息
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
//记录上一次frame开始时间,修正后的
mLastFrameTimeNanos = frameTimeNanos;
}

try {
//执行相关callBack
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

doFrame方法对当前帧的运行时间进行了一系列判断和修正,最终顺序执行了五种事件回调。



  1. CALLBACK_INPUT:输入

  2. CALLBACK_ANIMATION:动画

  3. CALLBACK_INSETS_ANIMATION:插入更新的动画

  4. CALLBACK_TRAVERSAL:遍历,执行measure、layout、draw

  5. CALLBACK_COMMIT:遍历完成的提交操作,用来修正动画启动时间


接着就会执行doCallbacks方法:


void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
......
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
//迭代执行所有队列任务
for (CallbackRecord c = callbacks; c != null; c = c.next) {
.....
//调用CallbackRecord内的run方法
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

主要是去遍历CallbackRecrd,执行所有任务:


private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;

@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}

最终actionrun方法会被执行,这里的action也就是我们在前面调用psetCallback传进来的,也就是 ViewRootImpl 发起的绘制任务mTraversalRunnable了。


然后这里又一次调用了doFrame方法,在啥时候token会是FRAME_CALLBACK_TOKEN呢? 可以发现在我们调用postFrameCallback内部会调用postCallbackDelayedInternal进行赋值:


 public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

ChoreographerpostFrameCallback()通常用来计算丢帧情况。


知道了Choreographer是上层用来接收VSync的角色之后,我们需要进一步了解VSync信号是如何控制上层的绘制的。而绘制UI的起点是View的requestLayout或者是invalidate方法被调用触发,好了时间不早了,这些就放在下一篇Android的屏幕刷新机制里解释吧。(刷新流程和同步屏障)


三. 小结


Android在黄油计划中引入了三个核心元素:VSYNCTriple BufferChoreographer


VSYNC 信号是由屏幕(显示设备)产生的,并且以 60fps 的固定频率发送给 Android 系统,Android 系统中的 SurfaceFlinger 接收发送的 VSYNC 信号。VSYNC 信号表明可对屏幕进行刷新而不会产生撕裂。


三重缓存机制(Triple Buffer) 利用 CPU/GPU 的空闲等待时间提前准备好数据,有效的提升了渲染性能。


又介绍了 Choreographer ,它实现了协调动画(animations)、输入(input)、绘制(drawing)三个UI相关的操作。


参考


Android 显示刷新机制、VSYNC和三重缓存机制


Android图形显示系统(一)


Android屏幕刷新机制


Android Choreographer 源码分析


“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!


作者:罗恩不带土
链接:https://juejin.cn/post/7149838503973830692
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一次查找分子级Bug的经历,过程太酸爽了

作者:李亚飞 Debugging is like trying to find a needle in a haystack, except the needle is also made of hay. Debug调试就像是在大片的干草堆中找针一样,只不...
继续阅读 »

作者:李亚飞





Debugging is like trying to find a needle in a haystack, except the needle is also made of hay.


Debug调试就像是在大片的干草堆中找针一样,只不过针也是由干草制成的。



在软件开发的世界里,偶尔会出现一些非常隐蔽的 Bug,这时候工程师们像探险家一样,需要深入代码的丛林,寻找隐藏在其中的“幽灵宝藏”。前段时间,我和我的团队也踏上了这样一段刺激、有趣的探险之旅。


最近繁忙的工作告一段落,我总算轻松下来了,想趁这个机会,跟大家分享我们的这次“旅途”。



01 引子


我是 ShowMeBug 的 CEO 李亚飞,是一个古老的 Ruby 工程师。由于 2019 年招聘工程师的噩梦经历,我立志打造一个真实模拟工作场景的 IDE,用来终结八股文、算法横行的技术招聘时代。


这个云上的 IDE 引擎,我称之为轻协同 IDE 引擎——因为它不是为了繁杂重度的工作场景准备的,而是适应于大部分人的习惯、能快速上手熟悉、加载速度快、能协同(面试用)、低延迟感,让用户感受非常友好


图片


多环境启动与切换


为了达成秒级启动环境的性能要求,我们设计了一套精巧的分布式文件系统架构,其核心是一个可以瞬间复制大量小文件的写时复制 (COW) 技术。IO 吞吐能达到几万人同时在线,性能绝对是它的一大优势。


我们对此信心满满,然而没想到,很快就翻车了。


02 探险启程


2023 年 1 月,北方已经白雪皑皑,而深圳却仍难以感受到冬天的寒意。


我和我的团队在几次打开文件树的某个文件时,会显得有点慢——当时没有人在意,按照常规思路,“网速”背了这个锅。事后我们复盘才发现,这个看似微不足道的小问题,其实正是我们开始这次探险之旅的起点。


1 月底,南方的寒意缓缓侵入。这时候我们的轻协同 IDE 引擎已经开始陆续支持了 Vue2、Vue3、React、Django、Rails 等框架环境,一开始表现都很棒,加载和启动速度都很快。但是,跑了一段时间,我们开始察觉,线上环境就出现个别环境(Rails 环境)启动要 20-30s 才能完成


虽然其他环境仍然保持了极快的加载和启动速度,但敏锐的第六感告诉我,不行,这一定有什么猫腻,如果不立即行动,势必会对用户体验带来很不好的影响。于是,我开始安排团队排查眼前这个不起眼的问题,我们的探险之旅正式开始。


03 初露希望


湿冷的冬季,夜已深,我和我们的团队依旧坐在电脑前苦苦探索,瑟瑟发抖。


探险之旅的第一站,就是老大难的问题:定位Bug。目前只有某一个环境启动很慢,其他的环境都表现不错。大家想了很多办法都没有想明白为什么,甚至怀疑这个环境的模板是不是有问题——但把代码放在本地启动,最多就2秒。


哎,太诡异了。我们在这里卡了至少一周时间,不断追踪代码,分析日志文件,尝试各种方案,都没有弄清楚一个正常的程序启动为什么会慢。我们一度陷入了疲惫和焦虑的情绪中。



Debug 是种信仰,只有坚信自己能找到 Bug,才有可能找到 Bug。



软件开发界一直有一个低级 Bug 定律:所有诡异的问题都来自一个低级原因。在这“山重水复疑无路”之际,我们决定重新审视我们的探险路径:为什么只有 Rails 更慢,其他并不慢?会不会只是一个非常微小的原因而导致?


这时候,恰好有一个架构师朋友来访,向我们建议,可以用 perf 火焰图分析看看 Rails 的启动过程。


图片


perf火焰图实例


当我们用 perf 来分析时,惊讶地发现:原来 Rails 的启动要加载更多的文件! 紧接着,我们又重新用了一个文件读写监控的工具:fatrace,通过它,我们看到 Rails 每次启动需要读写至少 5000 个文件,但其他框架并不需要。


这才让我们突然意识到,会不会是文件系统读写速度不及预期,导致了启动变慢。


04 Bug现身


为了搞清楚是不是文件系统读写速度的问题,我急需一个测试 IO 抖动的脚本。我们初步估算一下,写好这个脚本需要好几个小时的时间。


夜已深,研发同学都陆续下班了。时间紧迫!我想起了火爆全球的 ChatGPT,心想,不如让它写一个试试。


图片


测试 IO 抖动的脚本


Cool,几乎不需要改动就能用,把代码扔在服务器开跑,一测,果然发现问题:每一次文件读写都需要 10-20ms 才能完成 。实际上,一个优秀的磁盘 IO 读写时延应该在亚毫级,但这里至少慢了 50 倍。

Bingo,如同“幽灵宝藏”一般的分子级 Bug 逐渐显现,问题的根因已经确认:过慢的磁盘 IO 读写引发了一系列操作变慢,进而导致启动时间变得非常慢


更庆幸的是,它还让我们发现了偶尔打开文件树变慢的根本原因,这也是整个系统并发能力下降的罪魁祸首


05 迷雾追因


看到这里,大家可能会问,这套分布式文件系统莫非一直这么慢,你们为什么在之前没有发现?


非也,早在项目开始的时候,这里的时延是比较良好的,大家没有特别注意这个 IOPS 性能指标,直到我们后面才留意到,系统运行超过一个月时,IO 读写时延很容易就进入到卡顿的状态,表现就是文件系统所在主机 CPU 忽高忽低,重启就会临时恢复。


此时,探险之旅还没结束。毕竟,这个“幽灵宝藏”周围依旧笼罩着一层迷雾。


我们继续用 fatrace(监控谁在读写哪个 IO)监控线上各个候选人答题目录的 IO读写情况,好家伙,我们发现了一个意外的情况:几乎每一秒都有一次全量的文件 stats 操作 (这是一个检测文件是否有属性变化的 IO 操作)!


也就是说,比如有 1000 个候选人正在各自的 IDE 中编码,每个候选人平均有 300 个文件,就会出现每秒 30 万的 IO 操作数!


我们赶紧去查资料,根据研究数据显示,一个普通的 SSD 盘的 IOPS 最高也就到 2-3 万 。于是,我们重新测试了自己分布式文件系统的 IOPS 能力,结果发现也是 2-3 万 。


那这肯定远远达不到我们理想中的能力级别。


这时,问题更加明确:某种未知的原因导致了大量的 IOPS 的需求,引发了 IO 读写时延变长,慢了大约几十倍


06 接近尾声


我和我的团队继续深究下去,问题已经变得非常明确了:


原来,早在去年 12 月,我们上线一个监听文件增删的变化来通知各端刷新的功能。


最开始我们采用事件监听 (fswatch event),因为跨了主机,所以存在 1-2s 的延迟。研发同学将其改为轮询实现的方案,进而引发了每秒扫描目录的 stats 行为。


当在百人以下访问时,IOPS 没有破万,还足够应对。但一旦访问量上千,便会引发 IO 变慢,进而导致系统出现各种异常:间歇导致某些关键接口 QPS 变低,进而引发系统抖动


随着“幽灵宝藏”显露真身,这次分子级 Bug 的探险之旅也已经接近尾声。团队大
呼:这过程实在太酸爽了!


07 技术无止境


每一个程序员在成长路上,都需要与 Bug 作充足的对抗,要么你勇于探索,深入代码的丛林,快速定位,挖到越来越丰富的“宝藏”,然后尽情汲取到顶级的知识,最终成为高手;或者被它打趴下, 花费大量时间都找不到问题的根源,成为芸芸众生中的一人。


当然,程序员的世界中,不单单是 Debug。


当我毕业 5 年之后,开始意识到技术的真正价值是解决真正的社会问题。前文中我提到,由于我发现技术招聘真是一个极其痛苦的事:特别花面试官的时间,却又无法有效分析出候选人的技术能力,所以创立 ShowMeBug 来解决这个问题:用模拟实战的编程环境,解决科学评估人才的难度


这个轻协同 IDE 技术从零开发,支持协同文件树、完全自定义的文件编辑器、协同的控制台 (Console) 与终端 (Shell),甚至直接支持 Ctrl+P 的文件树搜索,不仅易于使用,又强大有力。


但是这还不够。要知道,追求技术精进是我们技术人的毕生追求。对于这个轻协同IDE,我们追求三个零:零配置、零启动、零延迟。其中,零启动就是本文所追求的极限:以最快的速度启动环境和切换环境


因此,探险之旅结束后,我们进一步改进了此文件系统,设定 raid 的多磁盘冗余,采用高性能 SSD,同时重新制定了新磁盘架构参数,优化相关代码,最终大幅提升了分布式文件系统的稳定性与并发能力。


截止本文结尾,我们启动环境的平均速度为 1.3 秒,切换环境速度进入到亚秒级,仅需要 780ms。目前在全球范围的技术能力评估赛道 (TSA) 中,具备 1-2 年的领先性


08 后记


正当我打算结束本文时,我们内部的产品吐槽群信息闪烁,点开一看:嚯,我们又发现了新 Bug。


立夏已至,我们的探险之旅又即将开始。


作者:ShowMeBug技术团队
来源:juejin.cn/post/7231429790615240764
收起阅读 »

python-实现地铁延误告警

在深圳地铁延误、临停n次之后 终于让我不得不又new了一个py文件😭😭 这次主要记录的是一个延误告警的开发过程 一、实现逻辑 使用库:requests,time,zmail,re 实现逻辑: 1、抓取深圳地铁微博的文章 2、判断是否有延误相关的内容 3、判断时...
继续阅读 »

在深圳地铁延误、临停n次之后


终于让我不得不又new了一个py文件😭😭


这次主要记录的是一个延误告警的开发过程


一、实现逻辑


使用库:requests,time,zmail,re


实现逻辑:


1、抓取深圳地铁微博的文章


2、判断是否有延误相关的内容


3、判断时间是否是今天

4、通知方式:邮件


5、定时执行任务


二、抓取深圳地铁微博(一中1~3)



def goout_report():
url ="https://weibo.com/ajax/statuses/mymblog"
# url ="https://weibo.com/szmcservice/statuses/mymblog"
data = {"uid":2311331195,"page":1,"feature":0}
headers={
"accept":"application/json, text/plain, */*",
"accept-encoding":"gzip, deflate, br",
"accept-language":"zh-CN,zh;q=0.9",
"referer":"https://weibo.com/szmcservice?tabtype=feed",
"cookie":"SUB=_2AkMV8LtUf8NxqwJRmf8XzmLgaY9wywjEieKjrEqPJRMxHRl-yT92ql0ctRB6PnCVuU8iqV308mSwZuO-G9gDVwYDBUdc; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WFpwsXV4nqgkyH.bEVfx-Xw; login_sid_t=c6bbe5dc58bf01c49b0209c29fadc800; cross_origin_proto=SSL; _s_tentry=passport.weibo.com; Apache=4724569630281.133.1655452763512; SINAGLOBAL=4724569630281.133.1655452763512; ULV=1655452763517:1:1:1:4724569630281.133.1655452763512:; wb_view_log=1920*10801; XSRF-TOKEN=1YMvL3PsAm21Y3udZWs5LeX3; WBPSESS=xvhb-0KtQV-0lVspmRtycws5Su8i9HTZ6dAejg6GXKXDqr8m6IkGO6gdtA5nN5IMNb5JZ1up7qJoFXFyoP2RSQSYXHY1uLzykpOFENQ07VthB0G9WHKwRCMWdaof42zB4mOkdTEeX_N9-m1x6Cpm3pmPsC1YhmTwqH8RGwXmYkI=",
"referer":"https://weibo.com/szmcservice",
"x-requested-with": "XMLHttpRequest",
"x-xsrf-token":"1YMvL3PsAm21Y3udZWs5LeX3",
"sec-ch-ua":'Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102',
"sec-ch-ua-platform":"Windows",
"sec-fetch-dest": "empty",
}
text = requests.get(url,headers=headers,params=data,verify=False).json()['data']['list']
today_date = time.ctime()[:10]
for i in range(1,5):
time_post = text[i]['created_at'][:10]
content = str(text[i]).split("'text': '")[1].split(", 'textLength'")[0]
tp=""
if '延误' in content and time_post == today_date:
# mail(content)
text = re.findall(">(.*?)<|>(.*?)\\",content)
for i in text:
for j in i:
if j!="":


                       tp=tp+j

        mail(tp)
break
else:
continue



三、邮件通知,代码如下


def mail(content):
mail = {
'subject': '别墨迹了!地铁又双叒叕延误啦', #邮件标题
'content_text': content, # 邮件内容
}
server = zmail.server('自己的邮箱', '密码',smtp_host="smtp.qq.com",
smtp_port=465) #此处用的qq邮箱、授权码
server.send_mail('收件人邮箱', mail)

ps:需去QQ邮箱网页版-设置-账户-开启smtp服务、获取授权码


四、定时执行任务


1、Jenkins比较合适项目的一个定时执行,


可参考如下:


jenkins环境: jenkins环境部署踩坑记


git环境:Mac-git环境搭建


2、windows-计算机管理比较合适脚本的执行,具体步骤如下,




  • windows键+R输入compmgmt.msc可进入计算机管理界面


    图片




  • 点击上图“创建任务”后如图,


    “常规”界面上输入任务名称、选项二,


    这样锁屏也会自动执行脚本


    图片




  • 点击“触发器”-新建进入新建触发器界面


    这个界面可设置任务执行时间、执行频率、任务重复间隔、延迟时间等等


    图片




  • 点击“操作”-新建跳到如图-新建操作界面


    这个界面可在“程序或脚本”输入框设置脚本运行程序,比如python.exe


    在“添加参数”输入框设置需要运行脚本路径(包含脚本名)


    在“起始于”输入框设置脚本执行路径(一般可为脚本目录)


    图片




  • 其他选项卡也可以看看,


    全部填写完可以点击“创建任务”界面上的“确定”按钮,


    然后在列表中找到新建的任务点击可查看,


    图片




  • 实时执行测试的话可以点击上图“运行”按钮


    或者右击任务-运行即可


    任务执行结果如下:




图片


作者:WAF910
来源:juejin.cn/post/7231074060788613175
收起阅读 »

正则什么的,你让我写,我会难受,你让我用,真香!

web
哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。 但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 ...
继续阅读 »



哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。


image.png


但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 CV 过来直接用~


本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!!


千分位格式化


在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。



  1. 123456789 => 123,456,789

  2. 123456789.123 => 123,456,789.123


const formatMoney = (money) => {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}

formatMoney('123456789') // '123,456,789'
formatMoney('123456789.123') // '123,456,789.123'
formatMoney('123') // '123'

想想如果不是用正则,还可以用什么更优雅的方法实现它?


解析链接参数


你一定常常遇到这样的需求,要拿到 url 的参数的值,像这样:



// url

const name = getQueryByName('name') // fatfish
const age = getQueryByName('age') // 100

通过正则,简单就能实现 getQueryByName 函数:


const getQueryByName = (name) => {
const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
const queryNameMatch = window.location.search.match(queryNameRegex)
// Generally, it will be decoded by decodeURIComponent
return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
}

const name = getQueryByName('name')
const age = getQueryByName('age')

console.log(name, age) // fatfish, 100

驼峰字符串


JS 变量最佳是驼峰风格的写法,怎样将类似以下的其它声明风格写法转化为驼峰写法?


1. foo Bar => fooBar
2. foo-bar---- => fooBar
3. foo_bar__ => fooBar

正则表达式分分钟教做人:


const camelCase = (string) => {
const camelCaseRegex = /[-_\s]+(.)?/g
return string.replace(camelCaseRegex, (match, char) => {
return char ? char.toUpperCase() : ''
})
}

console.log(camelCase('foo Bar')) // fooBar
console.log(camelCase('foo-bar--')) // fooBar
console.log(camelCase('foo_bar__')) // fooBar

小写转大写


这个需求常见,无需多言,用就完事儿啦:


const capitalize = (string) => {
const capitalizeRegex = /(?:^|\s+)\w/g
return string.toLowerCase().replace(capitalizeRegex, (match) => match.toUpperCase())
}

console.log(capitalize('hello world')) // Hello World
console.log(capitalize('hello WORLD')) // Hello World

实现 trim()


trim() 方法用于删除字符串的头尾空白符,用正则可以模拟实现 trim:


const trim1 = (str) => {
return str.replace(/^\s*|\s*$/g, '') // 或者 str.replace(/^\s*(.*?)\s*$/g, '$1')
}

const string = ' hello medium '
const noSpaceString = 'hello medium'
const trimString = trim1(string)

console.log(string)
console.log(trimString, trimString === noSpaceString) // hello medium true
console.log(string)

trim() 方法不会改变原始字符串,同样,自定义实现的 trim1 也不会改变原始字符串;


HTML 转义


防止 XSS 攻击的方法之一是进行 HTML 转义,符号对应的转义字符:


正则处理如下:


const escape = (string) => {
const escapeMaps = {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': 'quot',
"'": '#39'
}
// The effect here is the same as that of /[&<> "']/g
const escapeRegexp = new RegExp(`[${Object.keys(escapeMaps).join('')}]`, 'g')
return string.replace(escapeRegexp, (match) => `&${escapeMaps[match]};`)
}

console.log(escape(`

hello world



`
))
/*
<div>
<p>hello world</p>
</div>
*/


HTML 反转义


有了正向的转义,就有反向的逆转义,操作如下:


const unescape = (string) => {
const unescapeMaps = {
'amp': '&',
'lt': '<',
'gt': '>',
'quot': '"',
'#39': "'"
}
const unescapeRegexp = /&([^;]+);/g
return string.replace(unescapeRegexp, (match, unescapeKey) => {
return unescapeMaps[ unescapeKey ] || match
})
}

console.log(unescape(`
<div>
<p>hello world</p>
</div>
`
))
/*

hello world



*/


校验 24 小时制


处理时间,经常要用到正则,比如常见的:校验时间格式是否是合法的 24 小时制:


const check24TimeRegexp = /^(?:(?:0?|1)\d|2[0-3]):(?:0?|[1-5])\d$/
console.log(check24TimeRegexp.test('01:14')) // true
console.log(check24TimeRegexp.test('23:59')) // true
console.log(check24TimeRegexp.test('23:60')) // false
console.log(check24TimeRegexp.test('1:14')) // true
console.log(check24TimeRegexp.test('1:1')) // true

校验日期格式


常见的日期格式有:yyyy-mm-dd, yyyy.mm.dd, yyyy/mm/dd 这 3 种,如果有符号乱用的情况,比如2021.08/22,这样就不是合法的日期格式,我们可以通过正则来校验判断:


const checkDateRegexp = /^\d{4}([-\.\/])(?:0[1-9]|1[0-2])\1(?:0[1-9]|[12]\d|3[01])$/

console.log(checkDateRegexp.test('2021-08-22')) // true
console.log(checkDateRegexp.test('2021/08/22')) // true
console.log(checkDateRegexp.test('2021.08.22')) // true
console.log(checkDateRegexp.test('2021.08/22')) // false
console.log(checkDateRegexp.test('2021/08-22')) // false

匹配颜色值


在字符串内匹配出 16 进制的颜色值:


const matchColorRegex = /#(?:[\da-fA-F]{6}|[\da-fA-F]{3})/g
const colorString = '#12f3a1 #ffBabd #FFF #123 #586'

console.log(colorString.match(matchColorRegex))
// [ '#12f3a1', '#ffBabd', '#FFF', '#123', '#586' ]

判断 HTTPS/HTTP


这个需求也是很常见的,判断请求协议是否是 HTTPS/HTTP


const checkProtocol = /^https?:/

console.log(checkProtocol.test('https://medium.com/')) // true
console.log(checkProtocol.test('http://medium.com/')) // true
console.log(checkProtocol.test('//medium.com/')) // false

校验版本号


版本号必须采用 x.y.z 格式,其中 XYZ 至少为一位,我们可以用正则来校验:


// x.y.z
const versionRegexp = /^(?:\d+\.){2}\d+$/

console.log(versionRegexp.test('1.1.1'))
console.log(versionRegexp.test('1.000.1'))
console.log(versionRegexp.test('1.000.1.1'))

获取网页 img 地址


这个需求可能爬虫用的比较多,用正则获取当前网页所有图片的地址。在控制台打印试试,太好用了~~


const matchImgs = (sHtml) => {
const imgUrlRegex = /]+src="((?:https?:)?\/\/[^"]+)"[^>]*?>/gi
let matchImgUrls = []

sHtml.replace(imgUrlRegex, (match, $1) => {
$1 && matchImgUrls.push($1)
})
return matchImgUrls
}

console.log(matchImgs(document.body.innerHTML))

格式化电话号码


这个需求也是常见的一匹,用就完事了:


let mobile = '18379836654' 
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654

觉得不错的话,给个赞吧,以后继续补充~~


作者:掘金安东尼
来源:juejin.cn/post/7111857333113716750
收起阅读 »

css实现弧边选项卡

web
实现效果 实现方式 主要使用了 radial-gradient transform perspective rotateX transform-origin 等属性 思路 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变 ...
继续阅读 »

实现效果



image.png



实现方式



主要使用了



等属性



思路




  • 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变


image.png



  • 其实他是这样,如下图所示,我们只需要把黑色部分替换为透明即可,使用两个伪元素即可:


image.png



  • 通过超出隐藏和旋转得到想要的效果


image.png


image.png



  • 综上


在上述 outside-circle 的图形基础上:



  1. 设置一个适当的 perspective 值

  2. 设置一个恰当的旋转圆心 transform-origin

  3. 绕 X 轴进行旋转



  • 动图演示


3.gif



代码



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.g-container {
position: relative;
width: 300px;
height: 100px;
background: red;
border: 1px solid #277f9e;
border-radius: 10px;
overflow: hidden;
}
.g-inner {
position: absolute;
width: 150px;
height: 50px;
background: #fee6e0;
bottom: 0;
border-radius: 0 20px 0 20px;
transform: perspective(40px) scaleX(1.4) scaleY(1.5) rotateX(20deg) translate(-10px, 0);
transform-origin: 50% 100%;
}
.g-inner::before {
content: "";
position: absolute;
right: -10px;
width: 10px;
height: 10px;
top: 40px;
background: radial-gradient(circle at 100% 0, transparent, transparent 9.5px, #fee6e0 10px, #fee6e0);
}
.g-after {
position: absolute;
width: 150px;
height: 50px;
background: #6ecb15;
bottom: 49px;
right: 0;
border-radius: 20px 0 20px 0;
transform: perspective(40px) scaleX(1.4) scaleY(-1.5) rotateX(20deg) translate(14px, 0);
transform-origin: 53% 100%;
}
.g-after::before {
content: "";
position: absolute;
left: -10px;
top: 40px;
width: 10px;
height: 10px;
background: radial-gradient(circle at 0 0, transparent, transparent 9.5px, #6ecb15 10px, #6ecb15);
}
.g-inner-text,.g-after-text {
position: absolute;
width: 150px;
height: 50px;
line-height: 50px;
text-align: center;
}
.g-inner-text {
top: 50%;
left: 0;
}
.g-after-text {
top: 50%;
right: 0;
}
</style>
<body>
<div class="g-container">
<div class="g-inner"></div>
<div class="g-after"></div>
<div class="g-inner-text">选项卡1</div>
<div class="g-after-text">选项卡2</div>
</div>
</body>
</html>

参考文章:github.com/chokcoco/iC…


作者:Agony95z
来源:juejin.cn/post/7223580639710281787
收起阅读 »

极致舒适的Vue页面保活方案

web
为了让页面保活更加稳定,你们是怎么做的? 我用一行配置实现了 Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。 为什么需要页面保活? 页面保活可以提高用户...
继续阅读 »

为了让页面保活更加稳定,你们是怎么做的?


我用一行配置实现了


image.png



Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。



为什么需要页面保活?


页面保活可以提高用户的体验感。例如,当用户从一个带有分页的表格页面(【页面A】)跳转到数据详情页面(【页面B】),并查看了数据之后,当用户从【页面B】返回【页面A】时,如果没有页面保活,【页面A】会重新加载并跳转到第一页,这会让用户感到非常烦恼,因为他们需要重新选择页面和数据。因此,使用页面保活技术,当用户返回【页面A】时,可以恢复之前选择的页码和数据,让用户的体验更加流畅。


如何实现页面保活?


状态存储


这个方案最为直观,原理就是在离开【页面A】之前手动将需要保活的状态存储起来。可以将状态存储到LocalStoreSessionStoreIndexedDB。在【页面A】组件的onMounted钩子中,检测是否存在此前的状态,如果存在从外部存储中将状态恢复回来。


有什么问题?



  • 浪费心智(麻烦/操心)。这个方案存在的问题就是,需要在编写组件的时候就明确的知道跳转到某些页面时进行状态存储。

  • 无法解决子组件状态。在页面组件中还可以做到保存页面组件的状态,但是如何保存子组件呢。不可能所有的子组件状态都在页面组件中维护,因为这样的结构并不是合理。


组件缓存


利用Vue的内置组件<KeepAlive/>缓存包裹在其中的动态切换组件(也就是<Component/>组件)。<KeepAlive/>包裹动态组件时,会缓存不活跃的组件,而不是销毁它们。当一个组件在<KeepAlive/>中被切换时,activateddeactivated生命周期钩子会替换mountedunmounted钩子。最关键的是,<KeepAlive/>不仅适用于被包裹组件的根节点,也适用于其子孙节点。


<KeepAlive/>搭配vue-router即可实现页面的保活,实现代码如下:


<template>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

有什么问题?



  • 页面保活不准确。上面的方式虽然实现了页面保活,但是并不能满足生产要求,例如:【页面A】是应用首页,【页面B】是数据列表页,【页面C】是数据详情页。用户查看数据详情的动线是:【页面A】->【页面B】->【页面C】,在这条动线中【页面B】->【页面C】的时候需要缓存【页面B】,当从【页面C】->【页面B】的时候需要从换从中恢复【页面B】。但是【页面B】->【页面A】的时候又不需要缓存【页面B】,上面的这个方法并不能做到这样的配置。


最佳实践


最理想的保活方式是,不入侵组件代码的情况下,通过简单的配置实现按需的页面保活。


【不入侵组件代码】这条即可排除第一种方式的实现,第二种【组件缓存】的方式只是败在了【按需的页面保活】。那么改造第二种方式,通过在router的路由配置上进行按需保活的配置,再提供一种读取配置结合<KeepAlive/>include属性即可。


路由配置


src/router/index.ts


import useRoutersStore from '@/store/routers';

const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'index',
component: () => import('@/layout/index.vue'),
children: [
{
path: '/app',
name: 'App',
component: () => import('@/views/app/index.vue'),
},
{
path: '/data-list',
name: 'DataList',
component: () => import('@/views/data-list/index.vue'),
meta: {
// 离开【/data-list】前往【/data-detail】时缓存【/data-list】
leaveCaches: ['/data-detail'],
}
},
{
path: '/data-detail',
name: 'DataDetail',
component: () => import('@/views/data-detail/index.vue'),
}
]
}
];

router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
const { cacheRouter } = useRoutersStore();
cacheRouter(from, to);
next();
});

保活组件存储


src/stroe/router.ts


import { RouteLocationNormalized } from 'vue-router';

const useRouterStore = defineStore('router', {
state: () => ({
cacheComps: new Set<string>(),
}),
actions: {
cacheRouter(from: RouteLocationNormalized, to: RouteLocationNormalized) {
if(
Array.isArray(from.meta.leaveCaches) &&
from.meta.leaveCaches.inclued(to.path) &&
typeof from.name === 'string'
) {
this.cacheComps.add(form.name);
}
if(
Array.isArray(to.meta.leaveCaches) &&
!to.meta.leaveCaches.inclued(from.path) &&
typeof to.name === 'string'
) {
this.cacheComps.delete(to.name);
}
},
},
getters: {
keepAliveComps(state: State) {
return [...state.cacheComps];
},
},
});

页面缓存


src/layout/index.vue


<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="keepAliveComps">
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

<script lang='ts' setup>
import { storeToRefs } from 'pinia';
import useRouterStore from '@/store/router';

const { keepAliveComps } = storeToRefs(useRouterStore());
</script>

TypeScript提升配置体验


import 'vue-router';

export type LeaveCaches = string[];

declare module 'vue-router' {
interface RouteMeta {
leaveCaches?: LeaveCaches;
}
}

该方案的问题



  • 缺少通配符处理/*/**/index

  • 无法缓存/preview/:address这样的动态路由。

  • 组件名和路由名称必须保持一致。


总结


通过<RouterView v-slot="{ Component }">获取到当前路由对应的组件,在将该组件通过<component :is="Component" />渲染,渲染之前利用<KeepAlive :include="keepAliveComps">来过滤当前组件是否需要保活。
基于上述机制,通过简单的路由配置中的meta.leaveCaches = [...]来配置从当前路由出发到哪些路由时,需要缓存当前路由的内容。


如果大家有其他保活方案,欢迎留言交流哦!


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

vue 递归组件 作用域插槽

web
开头 这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。 代码 父组件 <template> <div> <Tree :data="data"> <templa...
继续阅读 »

开头


这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。


代码


父组件


<template>
<div>
<Tree :data="data">
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

理解步骤,始终知道 -> 递归就是把最里面的放到最外面来,你就当 A插槽最后会被 B 插槽替代

所以,父组件的 default 插槽用的是 B 插槽,因此 B 插槽就暴露出一个 title 给父组件使用。


删掉 A 的title :


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


由于可能只有一层,所以走不到 B 插槽,因此 A 插槽也需要暴露一个 title 给外面使用。


el-tree 的原理


父组件


<template>
<div>
<Tree :data="data">
<!-- C -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义22" }}
</div>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node,

},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if(!this.$parent.$scopedSlots.default) {
this.tree = this
}else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


这里可以看到,父组件的 C 和 子组件中的 B 都是使用到了 A 这个插槽。


这里我们只要能把 B 替换成父组件的 C 就完成了递归插槽。


子组件的代码转变


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<template #default="{ title }">
<node :title="title">
</node>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node: {
props: {
title: String,
},
render(h) {
const parent = this.$parent;
const tree = parent.tree
const title = this.title
return (tree.$scopedSlots.default({ title }))
}
}
},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if (!this.$parent.$scopedSlots.default) {
this.tree = this
} else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

这里搞了一个 node 的函数组件,node 函数组件拿到 子组件的 tree, tree也是一层层的保存着 $scopedSlots.default 其实就是 C 的那些编译节点。 然后把 title 传给了 C。


el-tree 源码贴图


image.png


tree


image.png


tree-node


image.png


image.png


image.png


image.png


总结


写的有点乱啊,这个只是辅助你理解 递归插槽,其实一开始都是懵逼了,多看下代码理解还是能看的懂的。


作者:晓欲望
来源:juejin.cn/post/7222931700438138937
收起阅读 »

不用刷新!用户无感升级,解决前端部署最后的问题

web
前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。 一、背景 网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。 二...
继续阅读 »

前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。


一、背景


网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。


二、问题分析


2.1 问题现象


网络控制台显示加载页面的资源显示404。


image.png


2.2 满足条件


发生这个现象,需要满足三个条件:



  1. 站点是SPA页面,并开启懒加载;

  2. 资源地址启用内容hash。(加载更快启用了强缓存,为了应对资源变更能及时更新内容,会对资源地址的文件名加上内容hash)。

  3. 覆盖式部署,新版本发布后旧的版本会被删除。


特别在容器部署的情况的SPA页面,很容易满足上诉三个条件。笔者在做公司的内部系统就踩过坑。


2.3 原因分析


浏览器打开页面后,会记录路由和资源路径的映射,服务器发版后,没有及时通知浏览器更新路由映射表。导致用户在发布前端打开的页面,在版本更新后,进入下一个路由加载上一个版本的资源失败,导致需要用户刷新才能正常使用。


image.png


三、解决方案


3.1 方案一:失败重试


3.1.1 思路整理:


既然加载失败了,就重试加载发版后的资源版本就行。增加一个manifest.json文件能够获取新版本对应的资源路径。


image.png


3.1.2 举例说明


以vue项目进行举例子说明:


第一步: 修改构建工具配置以生成manifest文件


使用vite构建的项目,可以在vite.config.ts增加配置build.manifest为true,用以生成manifest.json文件


export default defineConfig({
// 更多配置
build: {
//开启manifest
manifest: true,
cssCodeSplit: false //关闭单独生成css文件,方便demo演示
}
})

如果使用webpack构建的项目,可以使用webpack-manifest-plugin插件进行配置。


进行项目生产构建,生成manifest.json,内容如下:


 // 简单说明:文件内容是项目原始代码目录结构和构建生成的资源路径一一对应
{
"index.html": { // 页面入口
"dynamicImports": ["src/pages/page1.vue", "src/pages/page2.vue"],
"file": "assets/index-e170761c.js",
"isEntry": true,
"src": "index.html"
},
// page1对应单文件组件
"src/pages/page1.vue": {
"file": "assets/page1-515906ab1.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page1.vue"
},
// page2对应单文件组件
"src/pages/page2.vue": {
"file": "assets/page2-9785c68c.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page2.vue"
},
"style.css": {
"file": "assets/style-809e5baa.css",
"src": "style.css"
}
}

第二步,修改route文件,加上重试逻辑


在路由文件中,增加加载页面js失败的重试逻辑,通过新版的manifest.json来获取新版的页面js,再次加载。


import { createRouter, createWebHistory } from 'vue-router'


const router = createRouter({
history: createWebHistory('/'),
routes: [
{
path: '/page1',
// component: () => import(`../pages/page1.vue`), // 变更前
component: () => retryImport('page1'), // 变更后
},
{
path: '/page2',
// component: () => import(`../pages/page1.vue`),
component: () => retryImport('page2'),
},
]
})


async function retryImport(page) {
try {
// 加载页面资源
switch (page) {
case 'page1':
// 这里demo演示,没有使用dynamic-import-vars
return await import(`../pages/page1.vue`)
default:
return await import(`../pages/page2.vue`)
}
} catch (err: any) {
// 判断是否是资源加载错误,错误重试
if (err.toString().indexOf('Failed to fetch dynamically imported module') > -1) {
// 获取manifest资源清单
return fetch('/manifest.json').then(async (res) => {
const json = await res.json()
// 找到对应的最新版本的js
const errPage = `src/pages/${page}.vue`
// 加载新的js
return await import(`/${json[errPage].file}`)
})
}
throw err
}
}
export default router

3.1.3 总结


这个方案改造只涉及前端层,成本最低,但是无法做到多版本共存,只能适配部分发版变更,如果涉及删除页面的版本,最好增加一个容错页面。


3.2 方案二:增量部署


3.2.1 思路整理


生产环境发布改成增量发布,不再是覆盖式的发布,发版后旧版本依旧保留。


image.png


3.2.2 示例实践


需要改造构建配置,增加版本的概念,保证新旧版本不路径冲突


vite 构建工具示例:


// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const version = require('./package.json').version

// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
manifest: true,
assetsDir: `./${versionName}`, // 版本号
}
})

webpack构建工具示例:


// webpack.config.js
const path = require('path');
// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
module.exports = {
//...
output: {
path: path.resolve(__dirname, `dist/${versionName}/assets`),
},
};

3.2.3 总结


需要CI/CD发版改造,由之前的容器部署改成静态部署(即文件上传对象存储的思路一样),这种增量发部署适配全部的场景,而且,支持多版本共存,能做到版本灰度放量。


四、总结


本文通过覆盖式部署在前端版本发布,导致用户不可使用的严重体验问题,分析问题发生根因,并给出两种解决思路。笔者结合公司的云设施,最后使用增量部署,并BFF层配合使用,多版本共存,线上启用通过配置指定启用哪个版本,再也不用赶着时间点去发版。


作者:azuo
来源:juejin.cn/post/7223196531143131194
收起阅读 »

VUE中常用的4种高级方法

web
1. provide/inject provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在...
继续阅读 »

1. provide/inject


provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在子孙组件中使用 inject 来注入这个数据。


使用 provide/inject 的好处是可以让我们在父组件和子孙组件之间传递数据,而无需手动进行繁琐的 props 传递。它可以让代码更加简洁和易于维护。但需要注意的是,provide/inject 的数据是非响应式的,这是因为provide/inject是一种更加底层的 API,它是基于依赖注入的方式来传递数据,而不是通过响应式系统来实现数据的更新和同步。


具体来说,provide方法提供的数据会被注入到子组件中的inject属性中,但是这些数据不会自动触发子组件的重新渲染,如果provide提供的数据发生了变化,子组件不会自动感知到这些变化并更新。


如果需要在子组件中使用provide/inject提供的数据,并且希望这些数据能够响应式地更新,可以考虑使用Vue的响应式数据来代替provide/inject。例如,可以将数据定义在父组件中,并通过props将其传递给子组件,子组件再通过$emit来向父组件发送数据更新的事件,从而实现响应式的数据更新。


下面是一个简单的例子,展示了如何在父组件中提供数据,并在子孙组件中注入这个数据:


<!-- 父组件 -->
<template>
<div>
<ChildComponent />
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
provide: {
message: 'Hello from ParentComponent',
},
components: {
ChildComponent,
},
};
</script>

//上面provide还可以写成函数形式
export default {
provide(){
return {
message: this.message
}
}
}


<!-- 子组件 -->
<template>
<div>
<GrandchildComponent />
</div>
</template>

<script>
import GrandchildComponent from './GrandchildComponent.vue';

export default {
inject: ['message'],
components: {
GrandchildComponent,
},
};
</script>


<!-- 孙子组件 -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
inject: ['message'],
};
</script>


在上面的例子中,父组件中提供了一个名为 message 的数据,子孙组件中都可以使用 inject 来注入这个数据,并在模板中使用它。注意,子孙组件中的 inject 选项中使用了一个数组,数组中包含了需要注入的属性名。在这个例子中,我们只注入了一个 message 属性,所以数组中只有一个元素。


2. 自定义v-model


要使自定义的Vue组件支持v-model,需要实现一个名为value的prop和一个名为input的事件。在组件内部,将value prop 绑定到组件的内部状态,然后在对内部状态进行修改时触发input事件。


下面是一个简单的例子,展示如何创建一个自定义的输入框组件并支持v-model:


<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
name: 'MyInput',
props: {
value: String
}
};
</script>


在上面的组件中,我们定义了一个value prop,这是与v-model绑定的数据。我们还将内置的input事件转发为一个自定义的input事件,并在事件处理程序中更新内部状态。现在,我们可以在父组件中使用v-model来绑定这个自定义组件的值,就像使用普通的输入框一样:


<template>
<div>
<my-input v-model="message" />
<p>{{ message }}</p>
</div>
</template>

<script>
import MyInput from './MyInput.vue';

export default {
components: {
MyInput
},
data() {
return {
message: ''
};
}
};
</script>


在上面的代码中,我们通过使用v-model指令来双向绑定message数据和MyInput组件的值。当用户在输入框中输入文本时,MyInput组件会触发input事件,并将其更新的值发送给父组件,从而实现了双向绑定的效果。


3. 事件总线(EventBus)


Vue事件总线是一个事件处理机制,它可以让组件之间进行通信,以便在应用程序中共享信息。在Vue.js应用程序中,事件总线通常是一个全局实例,可以用来发送和接收事件。


以下是使用Vue事件总线的步骤:


3.1 创建一个全局Vue实例作为事件总线:


import Vue from 'vue';
export const eventBus = new Vue();

3.2 在需要发送事件的组件中,使用$emit方法触发事件并传递数据:


eventBus.$emit('eventName', data);

3.3 在需要接收事件的组件中,使用$on方法监听事件并处理数据:


eventBus.$on('eventName', (data) => {
// 处理数据
});

需要注意的是,事件总线是全局的,所以在不同的组件中,需要保证事件名称的唯一性。


另外,需要在组件销毁前使用$off方法取消事件监听:


eventBus.$off('eventName');

这样就可以在Vue.js应用程序中使用事件总线来实现组件之间的通信了。


4. render方法


Vue 的 render 方法是用来渲染组件的函数,它可以用来替代模板语法,通过代码的方式来生成 DOM 结构。相较于模板语法,render 方法具有更好的类型检查和代码提示。


下面详细介绍 Vue 的 render 方法的使用方法:


4.1 基本语法


render 方法的基本语法如下:


render: function (createElement) {
// 返回一个 VNode
}

其中 createElement 是一个函数,它用来创建 VNode(虚拟节点),并返回一个 VNode 对象。


4.2 创建 VNode


要创建 VNode,可以调用 createElement 函数,该函数接受三个参数:



  • 标签名或组件名

  • 可选的属性对象

  • 子节点数组


例如,下面的代码创建了一个包含文本节点的 div 元素:


render: function (createElement) {
return createElement('div', 'Hello, world!')
}

如果要创建一个带有子节点的元素,可以将子节点作为第三个参数传递给 createElement 函数。例如,下面的代码创建了一个包含两个子元素的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('h1', 'Hello'),
createElement('p', 'World')
])
}

如果要给元素添加属性,可以将属性对象作为第二个参数传递给 createElement 函数。例如,下面的代码创建了一个带有样式和事件处理程序的 button 元素:


render: function (createElement) {
return createElement('button', {
style: { backgroundColor: 'red' },
on: {
click: this.handleClick
}
}, 'Click me')
},
methods: {
handleClick: function () {
console.log('Button clicked')
}
}

4.3 动态数据


render 方法可以根据组件的状态动态生成内容。要在 render 方法中使用组件的数据,可以使用 this 关键字来访问组件实例的属性。例如,下面的代码根据组件的状态动态生成了一个带有计数器的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('p', 'Count: ' + this.count),
createElement('button', {
on: {
click: this.increment
}
}, 'Increment')
])
},
data: function () {
return {
count: 0
}
},
methods: {
increment: function () {
this.count++
}
}


4.4 JSX


在使用 Vue 的 render 方法时,也可以使用 JSX(JavaScript XML)语法,这样可以更方便地编写模板。要使用 JSX,需要在组件中导入 VuecreateElement 函数,并在 render 方法中使用 JSX 语法。例如,下面的代码使用了 JSX 语法来创建一个计数器组件:


import Vue from 'vue'

export default {
render() {
return (
<div>
<p>Count:{this.count}</p>
<button onClick={this.increment}>Increment</button>
</div>

)
},
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}


注意,在使用 JSX 时,需要使用 {} 包裹 JavaScript 表达式。


4.5 生成函数式组件


除了生成普通的组件,render 方法还可以生成函数式组件。函数式组件没有状态,只接收 props 作为输入,并返回一个 VNode。因为函数式组件没有状态,所以它们的性能比普通组件更高。


要生成函数式组件,可以在组件定义中将 functional 属性设置为 true。例如,下面的代码定义了一个函数式组件,用于显示列表项:


export default {
functional: true,
props: ['item'],
render: function (createElement, context) {
return createElement('li', context.props.item);
}
}

注意,在函数式组件中,props 作为第二个参数传递给 render<

作者:阿虎儿
来源:juejin.cn/post/7225921305597820985
/code> 方法。

收起阅读 »

代码重构和架构重构:你需要了解的区别

1 代码重构 定义 对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。 目的 增加可读性、增加可维护性、可扩展性 3 关键点 不影响输出 不修正错误 不增加新的功能性 代码重构时,发现有个功能实现逻辑不合理,可直接修改吗? 当然不可! 2 架构...
继续阅读 »

1 代码重构


定义


对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。


目的


增加可读性、增加可维护性、可扩展性


3 关键点



  • 不影响输出

  • 不修正错误

  • 不增加新的功能性


代码重构时,发现有个功能实现逻辑不合理,可直接修改吗?


当然不可!


2 架构重构


定义


通过整系统结构(4R)来修复系统质量问题而不影响整体系统能力。


目的


修复质量问题(性能、可用性、可扩展......)


关键点



  • 修复质量(架构,而非代码层面的质量)问题,提升架构质量

  • 不影响整体系统功能

  • 架构本质没有发生变化


把某个子系统的实现方式从硬编码改为规则引擎,是代码重构还是架构重构?


属于架构重构,架构设计方案了,实现系统可扩展性。


3 代码重构 V.S 架构重构



4 架构重构技巧


4.0 手段



架构重构是否可以修改 4R 中的 Rank?


不能!修改 rank 就不是重构,而是演进了。拆微服务不属于改 rank。外部系统协作方式都得修改了。比如将淘宝的支付方式支付宝拆出来,成为支付宝公司了。


4.1 先局部优化后架构重构


局部优化


定义:对部分业务或者功能进行优化,不影响系统架构。


常见手段:



  • 数据库添加索引,优化索引

  • 某个数据缓存更新策略采用后台更新

  • 增加负载均衡服务数量

  • 优化代码里面并发的逻辑

  • 修改Innodb buffer pool 配置,分配更多内存

  • 服务间的某个接口增加1个参数


架构重构


定义:优化系统架构,整体提升质量,架构重构会影响架构的4R定义。


常见手段:



  • 引入消息队列(增加 Role )

  • 去掉 ZooKeeper,改为内置 Raft 算法实现(删除 Role)

  • 将 Memcached 改为 Redis( 改变 Role)

  • 按照稳定性拆分微服务( 拆分 Role )

  • 将粒度太细的微服务合并(合并 Role)

  • 将服务间的通信方式由 HTTP 改为 gRPC(修改 Relation )

  • SDK从读本地配置文件改为从管理系统读取配置(修改Rule )


4.2 有的放矢



案例




  • 开发效率很慢,P业务和M系统互相影响

  • 线上问题很多,尤其是数据类问题

  • M系统性能很低


有的放矢:



重构只解决第1个问题(开发效率很慢,P业务和M系统互相影响)。其他问题咋办,架构师你不解决了吗?架构重构后了,各个业务部门再解决各自的问题,如 P业务后台优化自己的问题,M 系统优化自己的性能问题,因为这些问题本身靠重构是解决不了的,而是要靠重构拆分之后,各自再继续优化。


4.3 合纵连横


合纵


说服业务方和老板




  1. 以数据说话


    把“可扩展性”转换为“版本开发速度很慢然后给出对应的项目数据(平时注意搜集数据)。




  2. 以案例说话(其实更有效,给人的冲击力更明显) 若没有数据,就举极端案例,如某个小功能,开发测试只要5天,但是等了1个月才上线。




连横


说服其它团队。



  1. 换位思考 思考对其它团队的好处,才能让人配合。

  2. 合作双赢 汇报和总结的时候,把其它团队也带上。


案例


合纵:告诉PM和项目经理极端案例,设计2周、开发2天、一个月才上线。


连横:P业务线上问题大大减少,P业务不会被其它业务影响


4.4 运筹帷幄


① 问题分类


将问题分类,一段时间集中处理类问题。 避免对照 Excel表格,一条条解决。


② 问题排序


分类后排序,按照优先级顺序来落地。


避免见缝插针式的安排重构任务,不要搭业务的顺风车重构:



  • 避免背锅

  • 效果不明显

  • 无法安排工作量大的重构


③ 逐一攻破


每类问题里面先易后难。


把容易的问题解决掉,增强信心。


④ 案例


Before:



  • 1个100多行的Excel问题表格,一个一个的解决

  • 专挑软柿子捏

  • 见缝插针


After:



  1. 分类:性能、组件、架构、代码

  2. 分阶段: 优化-> 架构重构 -> 架构演进

  3. 专项落地: 明确时间、目标、版本



5 架构重构FAQ


架构重构是否可以引入新技术?


可以,但尽量少,架构重构要求快准


业务不给时间重构怎么办 ?


会哭的孩了有奶吃。收集数据和案例,事实说话。


其它团队不配合怎么办 ?


学会利用上级力量。上级都不支持,说明你做的这个没意义,所以领导也不在乎。那就别做了。


业务进度很紧,人力不够怎么办 ?


收集需要重构的证据,技术汇报的时候有理有据



6 测试


6.1 判断



  1. 代码重构、架构重构、架构演进都不需要去修复问题 ×

  2. 微服务拆分既可以是架构重构的手段,也可以是架构演进的手段 √

  3. 架构重构应该搭业务版本的便车,可以避免对业务版本有影响 ×

  4. 架构重构是为修复问题,因此应该将系统遗留的问题都在架构重构的时候修复 ×

  5. 架构重构应该分门别类,按照优先级逐步落地 √


6.2 思考


架构重构的时候是否可以顺手将代码重构也做了 ? 因为反正都安排版本了。No!


局部优化不属于代码/架构重构。


作者:JavaEdge在掘金
链接:https://juejin.cn/post/7222288378459848762
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Redis 使用zset做消息队列总结

1.zset为什么可以做消息队列 zset做消息队列的特性有: 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。 元素唯一性:zset的每个元素都是独一无二的...
继续阅读 »

1.zset为什么可以做消息队列


zset做消息队列的特性有:



  1. 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。

  2. 元素唯一性:zset的每个元素都是独一无二的,这对于实现某些消息需求(比如幂等性)是非常有帮助的。

  3. 成员和分数之间的映射关系:有序集合中的每个成员都有一个分数,这样就可以将相同的数据划分到不同的 queue 中,以及为每个 queue 设置不同的延时。

  4. 高效的添加删除操作:因为zset会自动维护元素之间的顺序,所以在添加或删除元素时无需进行手动排序,从而能提升操作速度。


Redis的zset天然支持按照时间顺序的消息队列,可以利用其成员唯一性的特性来保证消息不被重复消费,在实现高吞吐率等方面也有很大的优势。


2.zset实现消息队列的步骤


Redis的zset有序集合是可以用来实现消息队列的,一般是按照时间戳作为score的值,将消息内容作为value存入有序集合中。


以下是实现步骤:



  1. 客户端将消息推送到Redis的有序集合中。

  2. 有序集合中,每个成员都有一个分数(score)。在这里,我们可以设成消息的时间戳,也就是当时的时间。

  3. 当需要从消息队列中获取消息时,客户端获取有序集合前N个元素并进行操作。一般来说,N取一个适当的数值,比如10。


需要注意的是,Redis的zset是有序集合,它的元素是有序的,并且不能有重复元素。因此,如果需要处理有重复消息的情况,需要在消息体中加入某些唯一性标识来保证不会重复。


3.使用jedis实现消息队列示例


Java可以通过Redis的Java客户端包Jedis来使用Redis,Jedis提供了丰富的API来操作Redis,下面是一段实现用Redis的zset类型实现的消息队列的代码。


import redis.clients.jedis.Jedis;
import java.util.Set;

public class RedisMessageQueue {
  private Jedis jedis; //Redis连接对象
  private String queueName; //队列名字

  /**
    * 构造函数
    * @param host Redis主机地址
    * @param port Redis端口
    * @param password Redis密码
    * @param queueName 队列名字
    */
  public RedisMessageQueue(String host, int port, String password, String queueName){
      jedis = new Jedis(host, port);
      jedis.auth(password);
      this.queueName = queueName;
  }

  /**
    * 发送消息
    * @param message 消息内容
    */
  public void sendMessage(String message){
      //获取当前时间戳
      long timestamp = System.currentTimeMillis();
      //将消息添加到有序集合中
      jedis.zadd(queueName, timestamp, message);
  }

  /**
    * 接收消息
    * @param count 一次接收的消息数量
    * @return 返回接收到的消息
    */
  public String[] receiveMessage(int count){
      //设置最大轮询时间
      long timeout = 5000;
      //获取当前时间戳
      long start = System.currentTimeMillis();

      while (true) {
          //获取可用的消息数量
          long size = jedis.zcount(queueName, "-inf", "+inf");
          if (size == 0) {
              //如果无消息,休眠50ms后继续轮询
              try {
                  Thread.sleep(50);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } else {
              //计算需要获取的消息数量count与当前可用的消息数量size的最小值
              count = (int) Math.min(count, size);
              //获取消息
              Set<String> messages = jedis.zrange(queueName, 0, count - 1);
              String[] results = messages.toArray(new String[0]);
              //移除已处理的消息
              jedis.zremrangeByRank(queueName, 0, count - 1);
              return results;
          }

          //检查是否超时
          if (System.currentTimeMillis() - start > timeout) {
              return null; //超时返回空
          }
      }
  }

  /**
    * 销毁队列
    */
  public void destroy(){
      jedis.del(queueName);
      jedis.close();
  }
}


使用示例:


public static void main(String[] args) {
  //创建消息队列
  RedisMessageQueue messageQueue = new RedisMessageQueue("localhost", 6379, "password", "my_queue");

  //生产者发送消息
  messageQueue.sendMessage("message1");
  messageQueue.sendMessage("message2");

  //消费者接收消息
  String[] messages = messageQueue.receiveMessage(10);
  System.out.println(Arrays.toString(messages)); //输出:[message1, message2]

  //销毁队列
  messageQueue.destroy();
}


在实际应用中,可以结合线程池或者消息监听器等方式,将消息接收过程放置于独立的线程中,以提高消息队列的处理效率。


4.+inf与-inf


+inf 是 Redis 中用于表示正无穷大的一种特殊值,也就是无限大。在使用 Redis 的 zset 集合时,+inf 通常用作 ZREVRANGEBYSCORE 命令的上限值,表示查找 zset 集合中最大的分数值。+inf 后面的 -inf 表示 zset 中最小的分数值。这两个值一起可以用来获取 zset 集合中的所有元素或一个特定范围内的元素。例如:


# 获取 zset 集合中所有元素
ZREVRANGE queue +inf -inf WITHSCORES

# 获取 zset 集合中第1到第10个元素(分数从大到小排列)
ZREVRANGE queue +inf -inf WITHSCORES LIMIT 0 9

# 获取 zset 集合中分数在 1581095012 到当前时间之间的元素
ZREVRANGEBYSCORE queue +inf 1581095012 WITHSCORES

在这些命令中,+inf 代表了一个最大的分数值,-inf 代表了一个最小的分数值,用于确定查询的分数值范围。


5.redis使用list与zset做消息队列有什么区别


Redis 使用 List 和 ZSET 都可以实现消息队列,但是二者有以下不同之处:



  1. 数据结构不同:List 是一个有序的字符串列表,ZSET 则是一个有序集合,它们的底层实现机制不同。

  2. 存储方式不同:List 只能存储字符串类型的数据,而 ZSET 则可以存储带有权重的元素,即除了元素值外,还可以为每个元素指定一个分数。

  3. 功能不同: List 操作在元素添加、删除等方面比较方便,而 ZSET 在处理数据排序和范围查找等方面比 List 更加高效。

  4. 应用场景不同: 对于需要精细控制排序和分值的场景可以选用 ZSET,而对于只需要简单的队列操作,例如先进先出,可以直接采用 List。


综上所述,List 和 ZSET 都可以用于消息队列的实现,但如果需要更好的性能和更高级的排序功能,建议使用 ZSET。而如果只需要简单的队列操作,则 List 更加适合。


6.redis用zset做消息队列会出现大key的情况吗


在Redis中,使用zset作为消息队列,每个消息都是一个元素,元素中有一个分数代表了该消息的时间戳。如果系统中有大量消息需要入队或者大量的不同的队列,这个key的体积会越来越大,从而可能会出现大key的情况。


当Redis存储的某个键值对的大小超过实例的最大内存限制时,会触发Redis的内存回收机制,可以根据LRU算法等策略来选择需要回收的数据,并确保最热数据保持在内存中。如果内存不足,可以使用Redis的持久化机制,将数据写入磁盘。使用Redis集群,并且将数据分片到多个节点上,也是一种可以有效解决大key问题的方法。


针对大key的问题,可以考虑对消息进行切分,将一个队列切分成多个小队列,或者对消息队列集合进行分片,将消息分布到不同的Redis实例上,从而降低单个Redis实例的内存使用,并提高系统的可扩展性。


7.redis 用zset做消息队列如何处理消息积压



  1. 改变消费者的消费能力:


可以增加消费者的数量,或者优化消费者的消费能力,使其能够更快地处理消息。同时,可以根据消息队列中消息的数量,动态地调整消费者的数量、消费速率和优先级等参数。



  1. 对过期消息进行过滤:


将过期的消息移出消息队列,以减少队列的长度,从而使消费者能够及时地消费未过期的消息。可以使用Redis提供的zremrangebyscore()方法,对过期消息进行清理。



  1. 对消息进行分片:


将消息分片,分布到不同的消息队列中,使得不同的消费者可以并行地处理消息,以提高消息处理的效率。



  1. 对消息进行持久化:


使用Redis的持久化机制,将消息写入磁盘,以防止消息的丢失。同时,也可以使用多个Redis节点进行备份,以提高Redis系统的可靠性。


总的来说,在实际应用中,需要根据实际情况,综合考虑上述方法,选择适合自己的方案,以保证Redis的消息队列在处理消息积压时,能够保持高效和稳定。


8. redis使用zset做消息队列时,有多个消费者同时消费消息怎么处理


当使用 Redis 的 zset 作为消息队列时,可以通过以下方式来处理多个消费者同时消费消息:



  1. 利用Redis事务特性:zset中的元素的score会反映该元素的优先级,多个消费者可以使用Redis事务特性,采用原子性的操作将空闲的消息数据上锁,只有在被加锁的消费者消费完当前消息时,往消息队列中发送释放锁的指令,其它消费者才能够获得该消息并进行消费。

  2. 利用Redis分布式锁:使用 Redis 实现分布式锁来实现只有一个消费者消费一条消息,可以使用redis的SETNX命令(如果键已存在,则该命令不做任何事,如果密钥不存在,它将设置并返回1可以用作锁),将创建一个新的键来表示这一消息是否已经被锁定。

  3. 防止重复消费:为了防止多个消费者消费同一条消息,可以在消息队列中添加一个消息完成的标记,在消费者处理完一条消息之后,会将该消息的完成状态通知给消息队列,标记该消息已经被消费过,其它消费者再次尝试消费该消息时,发现已经被标记为完成,则不再消费该消息。


无论采用哪种方式,都需要保证消息队列的可靠性和高效性,否则会导致消息丢失或重复消费等问题。


9.redis使用zset做消息队列如何实现一个分组的功能


Redis 中的 Zset 可以用于实现一个有序集合,其中每个元素都会关联一个分数。在消息队列中,可以使用 Zset 来存储消息的优先级(即分数),并使用消息 ID 作为 Zset 中的成员,这样可以通过 Zset 的有序性来获取下一条要处理的消息。


为了实现一个分组的功能,可以使用 Redis 的命名空间来创建多个 Zset 集合。每个分组都有一个对应的 Zset 集合,消息都被添加到对应的集合中。然后,你可以从任何一个集合中获取下一条消息,这样就可以实现分组的功能。


例如,假设你的 Redis 实例有三个 Zset 集合,分别是 group1、group2 和 group3,你可以按照如下方式将消息添加到不同的分组中:


ZADD group1 1 message1
ZADD group2 2 message2
ZADD group3 3 message3

然后,你可以通过以下方式获取下一条要处理的消息:


ZRANGE group1 0 0 WITHSCORES
ZRANGE group2 0 0 WITHSCORES
ZRANGE group3 0 0 WITHSCORES

将返回结果中的第一个元素作为下一条要处理的消息。由于每个分组都是一个独立的 Zset 集合,因此它们之间是相互独立的,不会干扰彼此。


10. redis使用zset做消息队列有哪些注意事项


Redis 使用 ZSET 做消息队列时,需要注意以下几点:



  1. 消息的唯一性:使用 ZSET 作为消息队列存储的时候需要注意消息的唯一性,避免重复消息的情况出现。可以考虑使用消息 ID 或者时间戳来作为消息的唯一标识。

  2. 消息的顺序:使用 ZSET 作为消息队列存储可以保证消息的有序性,但消息的顺序可能不是按照消息 ID 或者时间戳的顺序。可以考虑在消息中增加时间戳等信息,然后在消费时根据这些信息对消息进行排序。

  3. 已消费的消息删除:在使用 ZSET 作为消息队列的时候需要注意如何删除已经消费的消息,可以使用 ZREMRANGEBYLEX 或者 ZREMRANGEBYSCORE 命令删除已经消费的消息。

  4. 消息堆积问题:ZSET 作为一种有序存储结构,有可能出现消息堆积的情况,如果消息队列里面的消息堆积过多,会影响消息队列的处理速度,甚至可能导致 Redis 宕机等问题。这个问题可以使用 Redis 定时器来解决,定期将过期的消息从队列中删除。

  5. 客户端的能力:在消费消息的时候需要考虑客户端的能力,可以考虑增加多个客户端同时消费消息,以提高消息队列的处理能力。

  6. Redis 节点的负载均衡:使用 ZSET 作为消息队列的存储结构,需要注意 Redis 节点的负载均衡,因为节点的并发连接数可能会受到限制。必要的时候可以增加 Redis 节点数量,或者采用 Redis 集群解决这个问题。


总之,使用 ZSET 作为消息队列存储需要特别注意消息的唯一性、消息的顺序、已消费消息删除、消息堆积问题、客户端的能力和节点的负载均衡等问题。


作者:香吧香
链接:https://juejin.cn/post/7221910537432825916
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不?

问题 你好,我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不? 认真考虑一下,想到了以下几点,如有不对的地方欢迎批评指正。 第一,提升学历 从现在起为升本做好准备,做好规划,然后每天去认认真真的执行,如果可能,尽量考研。因为现在互联网行业...
继续阅读 »

问题


你好,我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不?


认真考虑一下,想到了以下几点,如有不对的地方欢迎批评指正。


第一,提升学历


从现在起为升本做好准备,做好规划,然后每天去认认真真的执行,如果可能,尽量考研。因为现在互联网行业确实很卷,等你大专毕业的时候应该会更卷。现在的招聘越来越看重学历,学历不行你连面试机会都没有。你可能听说过有大专也进大厂的,那是前几年,也是心存着偏差,不能去赌这个。现在的事实是专科面试的机会很少很少。作为一个普通人,我感觉去接受更高等的教育,未来才会有更多的选择,才能更好的去掌握自己的命运。


第二,学好基础知识


在学习上,一定要想法设法学好数据结构、数据库、操作系统、计算机网络、计算机组成原理、英语、数学等等。尽量每天都抽出点儿时间来学点。你可能会说,听别人说这些东西好像在工作中也不常用呀,对,一般的工作确实不常用,但正是这些决定了你未来能走多远。这些东西是你从事这个行业的根基,你这个根基越稳固,你的未来发展就会越好。出了新东西你才能更快的掌握。假设互联网真不行了,你有这些底子在,你可以很快的去切到其他行业一些软件儿上的开发,如果说你没有这个底子,想迅速切换想都别想。


第三,锻炼合作能力


在学校多去参加一些计算机类的比赛,比如说像一些算法相关的,锻炼自己与他人合作的能力。


第四,参与写作、开源


业余时间去写写博客儿,参加一些开源项目,这些对于你毕业后找工作都是有帮助的。不得不承认,现在的大学生也挺卷的,在学校就各种源码,算法各种卷。你需要制造点不一样,想想你毕业之后有一个不错的博客儿,有一个成百上千小星星的开源项目儿,肯定是加分。


第五,学习人情世故


学习之外适当的去兼顾一些人情往来之类的东西,注意是适当。比如宿舍的一些聚餐呀,学校举办的一些比赛,建议多参加唱歌、演讲、辩论类的比赛,锻炼自己的表达、表现力、还有心里素质。不要太在意别人的看法,没那么多人在意你,这个道理越早知道越好。你迟早是要步入社会的,不是说你技术多好,你就能混的多好,人情世故这个东西,很重要,早锻炼比晚锻炼强,真的,赶早不赶晚。


第六,锻炼身体


永远记住身体是革命的本钱,没有一个好身体啥都没用,要抽时间锻炼身体哈。


作者:程序员黑黑
链接:https://juejin.cn/post/7230603857033248829
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

裁员、结婚、买房、赡养父母.....即将30岁,焦虑扑面而来

前言: 大家好,我是春风。 不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。 就算此时你还没有结婚,但面对即将不得不结婚的压力,面对已经老去的父母。时间突然就变得紧迫起来,你也...
继续阅读 »

前言:


大家好,我是春风。


不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。


就算此时你还没有结婚,但面对即将不得不结婚的压力,面对已经老去的父母。时间突然就变得紧迫起来,你也突然就不再属于你自己,我们会不自觉的扮演起家庭的依靠,而且还是唯一的依靠。这种压力完全是在自己还没准备的时候就突袭了你。


就像我这一周都是在这种压力和焦虑中度过...


192b2a6322f5c0559f59d567effb7bfb.jpeg


失眠


我不知道自己为什么会突然一下就想这么多,但年龄就像一个雷管,突然就炸开了赤裸裸的现实,或者是按下了一个倒计时。我不自觉的去想家庭,去想父母,去想我30岁40岁50|岁是什么样子。


这周的每天晚上我想着这些都失眠到三四点,当然如果这个时候你还像我一样去看下确切时间,你很大可能会失眠到五点。


尝试心理学


所以这几天上班也是一行代码都没敲,幸好需求不多。最后我迫切的觉得我应该找个办法解决一下,索性今天摸鱼一天,听了一天的心理学讲座的音频。


果然,心病还需心药医!!!


下面我给大家分享一下自己的治疗过程,希望也能对焦虑的你有所启发。
 


一、我为什么焦虑


解决焦虑的第一步就是先要弄清楚我们为什么焦虑?我们究竟在焦虑什么?可能很多人都是焦虑经济,焦虑结婚,焦虑生活中的各种琐事。


但我们也可以试着站在上帝视角,更深层次的解剖一下自己。


1. 焦虑多年努力没有换来想要的生活


比如我,我最大的焦虑也是钱,我从农村出来,没有任何背景,毕业到现在已经工作六年,20年在广州买房上车,但好巧不巧买的是恒大的房子,买完就暴雷,现在每个月有房贷,还要结婚。


所以我总是在想,这些年我算努力吗,为什么还是没有挣到钱。


三十而立近在眼前,可我这些年究竟立了什么呢?遥想刚毕业那会给自己定下的目标,虽然是天方夜谭,但对比现在,也太天方夜谭了吧。


不是说好的天道酬勤吗?不是说努力就会有收获吗?


所以我焦虑,我表面是焦虑钱,但何尝不是在焦虑自己这么多年的努力却没有得到我想要的结果呢?


2. 攀比带来的自我嫌弃


我们都知道攀比是不好的,尤其是在这个动辄年薪百万年薪的互联网世界,但也是这些网络信息的无孔不入,让我们不得不攀比,不得不怀疑自己是为什么会差这么多。


我承认自己是一个争强好胜的人,我会在读书时非常想要好的名次,因为我体验过那种胜利感一次之后,便会上瘾。所以现在工作,我也时常不自觉的攀比起来,因此,我也深深陷入了自我怀疑和自我嫌弃的枣泥。


为什么我努力学习,辛苦工作,一年下来却不如人家卖一个月炒饭,为什么那个做销售的同学两三个月就赚到了我两年的财富,为什么我工作六年攒下的钱,却还不及人家父母一个月的收租?


和我一样没背景的比我赚的多,有背景的赚的更多。这种怀疑病入膏肓的时候,我都会病态的想,那些富二代肯定都是花花公子,懒惰而不自知,毕竟电影里不都这样演吗?但现实是,别人会比你接受更好的家庭教育,环境教育。别人谈吐自如还努力学习。不仅赢在了起跑线,还比你努力。就是这种对比,越来越让我们自己嫌弃自己,厌恶自己。所以也就总是想要求自己必须去做更好的自己。


二、生命的意义


应该所有人都思考过这个问题吧,来这人间一趟,可都不想白来一趟。我们都想在这个世界留下点什么,就像战国时士人对君主,知道会被烹杀却勇于进言,只为留下一个劝谏名士的美名。人活一世,究竟为了什么呢?生前获利?死后留名?


但对于我们大多数的普通人呢?


待我们死去,我们的名字最多就被孙子辈的人知道,等到他们也故去,那这个世界还会有你来过的痕迹吗?


人生代代无穷已,江月年年望相似。


所以夜深人静的时候,我们总会在想,自己生命的意义?似乎一切都没有意义,我们注定就是会拥有一个低价值甚至无价值的人生


三、结婚的压力


我们九零后,比零零后环境是不是更好不确定,但对比八零后,肯定要差,八零后结婚,印象里还不太谈房子,车子,但我们结婚,确是一个必考题。


所以我们结婚率低,不仅有不婚族,还有现在的丁克族。


我自己来自农村,我们那里男女比例就严重失衡,村里的男孩子结婚的不超过一半。但是我爸着急,不知道你们是否有过这种催婚的经历,父母会反复的告诉你大龄剩男剩女有多丢人,你们的不婚不仅是你自己的问题,还会让家里人都抬不起头。是的,父母含辛茹苦养育了你们,现在因为你,让他们在别人面前抬不起头来,失去了自尊。


四、知道该做什么,但拖延没做后就会更加的自我嫌弃


我们擅长给自己定下很多目标,但有时候就是逃不过人性,孔子说,食色性也。我们在被创造的时候就是被设计为不断的追求多巴胺的动物。所以我们沉迷游戏,沉迷追剧。总是在周五的晚上选择放松自己。而不会因为定下了目标就去学习。


总之,我们的目标定的越美好,我们的行动往往越低效。最后,两者的差距越来越远。我们离自己期望中的那个自己判若两人。


我们又会厌恶自己,嫌弃自己。甚至痛骂自己的不自律。




以上是我分析的自己的焦虑点。相信很多也是屏幕前的你曾经或者当下也有的吧。接下来,就看看我是怎么在心理学上找到解决的办法的吧!


给自己的建议


关于攀比、努力没有想要的结果、不自律等等带来的自我嫌弃。我们或许应该这样看


1、承认自己的普通


有远大报负,有远大理想。追求自由和生命的绚丽是我们每个人都会有也应该有的念想。但当暂时还没有结果的时候。我们不应该及早否定自己。而是勇于承认自己的普通。我们都想成为这个世界上独一无二的人。事实上从某种意义上来说。我们也是独一无二的人。但从金钱,名望这些大家公认的层面来看。99.99%的人都是普通人。我们这一生很大可能就会这样平凡的过完一生。接受自己的普通,活在当下。这才是体验这趟生命之旅最应该有的态度。只要今天比昨天好。我们不就是在进步吗?


为什么一定要有个结果??


人生最大的悲哀就是跨越知道和做到的鸿沟,当一个人承认自己是个普通人的时候,他才是人格完整,真正成熟的时候


我们追求美好的事物,追求自己喜欢的东西,金钱也好,名望也罢,这都是无可厚非的。因为人就是需要被不断满足的,人因为有欲望才会想活下去。但是当暂时没有结果的时候。我们也不应该为此感到自责和焦虑。一旦我们队美好事物的追求变成了一种压力。我们就会陷入一种有负担的内缩状态,反而会跑不快


我们都害怕浪费生命,因为生命只有一次。我们想让自己的生命在这个世界留下来过的痕迹。所以我们追寻那些热爱的东西,但其实追求的过程才是最应该留下的痕迹,结果反而只是别人眼里的痕迹。


当然也有一种理解认为活在当下就是躺平。恰好现在网络上也是躺平之语频频入耳。我想说关于是努力追求理想还是躺平的一点观点。


在禅宗里有这样一句话说的非常好:身无所住而生其心


这里的住 代表的就是追求的一种执念。


身无所住而生其心,说的就是要避免有执和无执的两种极端状态。有执就是我们我都要要要。我要钱 我要名 我要豪车豪宅。无执就是觉得什么都没有意义。生命终会归于尘土。所以努力追求的再多,又有什么用呢?大多数人生命注定是无意义的。这也是很多人躺平的一部分原因吧!


但是就该这样躺平的度过一生吗?每天都陷入低价值的人生?


身无所住而生其心。我们的生命不应该陷入有执和无执这两种极端。花开了,虽然它终会化作春泥。但花开的此刻,它是真美啊!
 


2、关于结婚生子


 
关于结婚生子,为什么我要在所有人都结婚的年龄就结婚,为什么三十岁生孩子就是没出息。生育这个问题,其实是为了什么 我爸老说,你不生小孩或者很晚生小孩,到时候老了都没人照顾你,那养儿真的就是为了防老吗?其实这是一个伪命题,先还不说到时候,儿女孝不孝顺的问题,就说我爸,这么多年,他为了倾其所有,花我身上的钱不说几千万也有上百万了,如果真是要防老,那这个钱存银行,每年光吃利息就有几十万,几十万在一个农村来说晚年怎么都富足了,两三个人照顾你都够,而我到现在每年有给过我爸几十万吗?


再说养儿为了到时候不孤独,能享受天伦之乐,这算是感情上的需求吧。那既然这样,我在准备好的节奏里欣然的生育,不比我在年龄和周遭看法的压力下强行生育更加的好吗,当我想体验一下为人父的生命体验了,我顺其自然的要小孩儿,快快乐乐的养育他,而不是我已经三十岁了,别人小孩儿都打酱油了,大家都在说是不是我有问题,所以即使我现在经济,心理,精力上都没准备好,我也必须要一个小孩儿。


所以大人们说的并不是真正的理由,而人类或者动物,之所以热衷繁衍,最原始的动力是想把自己的基因流下去,是想在这个世界上留下一点记忆。


为别人而活。尤其是在农村,很多人一辈子就认识村里那些人,祖祖辈辈就只见过那些活法,在他们眼里,多少岁结婚,多少岁生孩子,这辈子就这么过去了。但是但凡有一点出格,那在其他人眼里就会抬不起头,因为,其他人出现意外的时候,自己也是这样看其他人的。所以大家都只为活在别人眼里而活,打个比方,我现在很想很想吃一个红薯,明明我吃完这个红薯,内心就会得到满足,但是我不会,因为别人会觉得我是不是穷,都只能吃红薯,这不单单是大家说的死要面子活受罪,其实是我们很多人骨子里的自卑,尤其是我们农村,经济条件都不好,没有什么值得炫耀的,所以我们就尽可能找大家能达成共识的去炫耀。很简单的一个例子。假如一个亿万富翁去到农村,他的身价已经足够自信了,即使他不结婚生子,其他人会看不起他吗?


结尾:


1、心理学是治愈,也是哲学上的思考。这种思考很多都能跳脱出现实而给到我们解决现实中问题的办法


2、再重复一遍:身无所住而生其心!


3、要爱具体的人,不要爱抽象的人,要爱生活,不要爱生活的意义。


作者:程序员春风
链接:https://juejin.cn/post/7119863033920225287
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 官方架构中的 UseCase 该怎么写?

1. UseCase 的用途 Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。 Android 最新架构:developer.android.com/topi...
继续阅读 »

1. UseCase 的用途


Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。



Android 最新架构:developer.android.com/topic/archi…



传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。



Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。



Android 架构早期的示例代码 todo-app 中曾经引入过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明确了,最新的 UseCase 示例可以从官方的 NIA 中学习。




2. UseCase 的特点


官方文档认为 UseCase 应该具有以下几个特点:


2.1 不持有状态


可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。


下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase



2.2 单一职责


严格遵守单一职责,一个 UseCase 只做一件事情,甚至其命名就是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。


下面 NIA 中所有 UseCases:



2.3 可有可无


官方文档中将 UseCase 定义为可选的角色,按需定义。简单的业务场景中允许 UI 直接访问 Repository。如果我们将 UseCase 作为 UI 与 Data 隔离的角色,那么工程中会出现很多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。


3. 如何定义 UseCase


如上所述,官方文档虽然对 UseCase 给出了一些基本定义,但是毕竟是一个新新生概念,很多人在真正去写代码的时候仍然会感觉不清晰,缺少有效指引。在究竟如何定义 UseCase 这个问题上,还有待大家更广泛的讨论,形成可参考的共识。本文也是带着这个目的而生,算是抛砖引玉吧。


3.1 Optional or Mandatory?


首先,官方文档认为 UseCase 是可选的,虽然其初衷是好的,大家都不希望出现太多 One-Liner 的 UseCase,但是作为一个架构规范切忌模棱两可,这种“可有可无”的规则其结局往往就是“无”。


业务刚起步时由于比较简单往往定义在 Repository 中,随着业务规模的扩大,应该适当得增加 UseCase 封装一些复杂的业务逻辑,但是实际项目中此时的重构成本会让开发者变得“懒惰”,UseCase 最终难产。


那放弃 UseCase 呢?这可能会造成 Repository 的职责不清和无限膨胀,而且 Repository 往往不止有一个方法, ViewModel 直接依赖 Repository 也违反了 SOLID 中的另一个重要原则 ISP ,ViewModel 会因为不相关的 Repository 改动导致重新编译。



ISP(Interface Segregation Principle,接口隔离原则) 要求将接口分离成更小的和更具体的接口,以便调用方只需知道其需要使用的方法。这可以提高代码的灵活性和可重用性,并减少代码的依赖性和耦合性。



为了降低前期判断成本和后续重构成本,如果我们有业务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需要研究如何降低 UseCase 带来的模板代码。


3.2 Class or Object?


官方建议使用 Class 定义 UseCase,每次使用都实例化一个新对象,这会做成一些重复开销,那么可否用 object 定义 UseCase 呢?


UseCase 理论上可以作为单例存在,但 Class 相对于 Object 有以下两个优势:



  • UseCase 希望像纯函数一样工作,普通 Class 可以确保每次使用时都会创建一个新的实例,从而避免状态共享和副作用等问题。

  • 普通类可以通过构造参数注入不同的 Repository,UseCase 更利于复用和单元测试


如果我们强烈希望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也可以简单的支持。例如 Dagger 中只要添加 @Singleton 注解即可


@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
private val recentSearchRepository: RecentSearchRepository,
) {
operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?


既然我们想像函数一样使用 UseCase ,那为什么不直接定义成 Function 呢?比如像下面这样


fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这确实遵循了 FP 的原则,但又丧失了 OOP 封装性的优势:



  • UseCase 往往需要依赖 Repository 对象,一个 UseCase Class 可以将 Repository 封装为成员存储。而一个 UseCase Function 则需要调用方通过参数传入,使用成本高不说,如果 UseCase 依赖的 Repository 的类型或者数量发生变化了,调用方需要跟着修改

  • 函数起不到隔离 UI 和 Data 的作用,ViewModel 仍然需要直接依赖 Repository,为 UseCase 传参

  • UseCase Class 可以定义一些 private 的方法,相对于 Function 更能胜任一些复杂逻辑的实现


可见,在 UseCase 的定义上 Function 没法取代 Class。当然 Class 也带来一些弊端:



  • 暴露多个方法,破坏 SRP 原则。所以官方推荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让职责更清晰。

  • 携带可变状态,这是大家写 OOP 的惯性思维

  • 样板代码多


3.4 Function interface ?


通过前面的分析我们知道:UseCase 的定义需要兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单方法的接口,可以低成本创建一个匿名类对象,确保对象只能有一个方法,同时具有一定封装性,可以通过“闭包”依赖 Repository。此外,Kotlin 对 SAM 提供了简化写法,一定程度也减少了样板代码。



Functional (SAM) interfaces:
kotlinlang.org/docs/fun-in…



改用 Function interface 定义 GetRecentSearchQueriesUseCase 的代码如下:


fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创建 UseCase 实例的同时,实现函数中的逻辑


val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
//...
}

我在函数实现中如何 Repository 呢?这要靠 DI 容器获取。官方示例代码中都使用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创建细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被自动注入到 SearchViewModel 中。


@HiltViewModel
class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
//...
) : ViewModel() {
//...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有构造函数,需要通过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 可以从容器中自动获取使用。


@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
@Provides
fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:


single<GetRecentSearchQueriesUseCase> {
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

4. 总结


UseCase 作为官方架构中的新概念,尚没有完全深入人心,需要不断探索合理的使用方式,本文给出一些基本思考:




  • 考虑到架构的扩展性,推荐在 ViewModel 与 Repository 之间强制引入 UseCase,即使眼下的业务逻辑并不复杂




  • UseCase 不持有可变状态但依赖 Repository,需要兼具 FP 与 OOP 的特性,更适合用 Class 定义而非 Function




  • 在引入 UseCase 之前应该先引入 DI 框架,确保 ViewModel 与 UseCase 的耦合。




  • Function Interface 是 Class 之外的另一种定义 UseCase 的方式,有利于代码更加函数式


作者:fundroid
链接:https://juejin.cn/post/7230597098847092791
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Unit 为啥还能当函数参数?面向实用的 Kotlin Unit 详解

视频先行 这是一篇视频形式的分享,如果你方便看,可以直接去看视频: 哔哩哔哩:这里 抖音:这里 YouTube:这里 下面是视频内容的脚本文案原稿分享。 视频文案原稿 很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有...
继续阅读 »

视频先行


这是一篇视频形式的分享,如果你方便看,可以直接去看视频:




下面是视频内容的脚本文案原稿分享。



视频文案原稿


很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有沿用 Java 的 void 关键字,而要引入这个叫 Unit 的新东西?


// Java
public void sayHello() {
System.out.println("Hello!");
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

不过这个问题一般也不会维持很久,因为就算你不明白,好像……也不影响写代码。


直到这两年,大家发现 Compose 的官方示例代码里竟然有把 Unit 填到函数参数里的情况:


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

我们才觉得:「啊?还能这么写?」


Unit 的本质


大家好,我是扔物线朱凯。


今天来讲一讲 Unit 这个特殊的类型。


我们在刚学 Kotlin 的时候,就知道 Java 的 void 关键字在 Kotlin 里没有了,取而代之的是一个叫做 Unit 的东西:


// Java
public void sayHello() {
System.out.println("Hello!")
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

而这个 Unit,和 Java 的 void 其实是不一样的。比如 Unit 的返回值类型,我们是可以省略掉不写的:


// Kotlin
fun sayHello() {
println("Hello!")
}

不过省略只是语法上的便利,实际上 Kotlin 还是会把它理解成 Unit


Unit 和 Java 的 void 真正的区别在于,void 是真的表示什么都不返回,而 Kotlin 的 Unit 却是一个真实存在的类型:


public object Unit {
override fun toString() = "kotlin.Unit"
}

它是一个 object,也就是 Kotlin 里的单例类型或者说单例对象。当一个函数的返回值类型是 Unit 的时候,它是需要返回一个 Unit 类型的对象的:


// Kotlin
fun sayHello() {
println("Hello!")
return Unit
}

只不过因为它是个 object ,所以唯一能返回的值就是 Unit 本身。


另外,这一行 return 我们也可以省略不写:


// Kotlin
fun sayHello() {
println("Hello!")
}

因为就像返回值类型一样,这一行 return,Kotlin 也会帮我们自动加上:


// Kotlin
fun sayHello(): Unit {
println("Hello!")
return Unit
}

这两个 Unit 是不一样的,上面的是 Unit 这个类型,下面的是 Unit 这个单例对象,它俩长得一样但是是不同的东西。注意了,这个并不是 Kotlin 给 Unit 的特权,而是 object 本来就有的语法特性。你如果有需要,也可以用同样的格式来使用别的单例对象,是不会报错的:


object Rengwuxian

fun getRengwuxian(): Rengwuxian {
return Rengwuxian
}

包括你也可以这样写:


val unit: Unit = Unit

也是一样的道理,等号左边是类型,等号右边是对象——当然这么写没什么实际作用啊,单例你就直接用就行了。


所以在结构上,Unit 并没有任何的特别之处,它就只是一个 Kotlin 的 object 而已。除了对于函数返回值类型和返回值的自动补充之外,Kotlin 对它没有再施加任何的魔法了。它的特殊之处,更多的是在于语义和用途的角度:它是个由官方规定出来的、用于「什么也不返回」的场景的返回值类型。但这只是它被规定的用法而已,而本质上它真就是个实实在在的类型。也就是在 Kotlin 里,并不存在真正没有返回值的函数,所有「没有返回值」的函数实质上的返回值类型都是 Unit,而返回值也都是 Unit 这个单例对象,这是 Unit 和 Java 的 void 在本质上的不同。


Unit 的价值所在


那么接下来的问题就是:这么做的意义在哪?


意义就在于,Unit 去掉了无返回值的函数的特殊性,消除了有返回值和无返回值的函数的本质区别,这样很多事做起来就会更简单了。


例:有返回值的函数在重写时没有返回值


比如?


比如在 Java 里面,由于 void 并不是一种真正的类型,所以任何有返回值的方法在子类里的重写方法也都必须有返回值,而不能写成 void,不管你用不用泛型都是一样的:


public abstract class Maker {
public abstract Object make();
}

public class AppleMaker extends Maker {
// 合法
@Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker {
// 非法
@Override
public void make() {
world.refresh();
}
}


public abstract class Maker<T> {
public abstract T make();
}

public class AppleMaker extends Maker<Apple> {
// 合法
Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker<void> {
// 非法
Override
public void make() {
world.refresh();
}
}


你只能去写一行 return null 来手动实现接近于「什么都不返回」的效果:


public class NewWorldMaker extends Maker {
@Override
public Object make() {
world.refresh();
return null;
}
}


而且如果你用的是泛型,可能还需要用一个专门的虚假类型来让效果达到完美:


public class NewWorldMaker extends Maker<Void> {
@Override
public Void make() {
world.refresh();
return null;
}
}


而在 Kotlin 里,Unit 是一种真实存在的类型,所以直接写就行了:


abstract class Maker {
abstract fun make(): Any
}

class AppleMaker : Maker() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker() {
override fun make() {
world.refresh()
}
}

abstract class Maker<T> {
abstract fun make(): T
}

class AppleMaker : Maker<Apple>() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker<Unit>() {
override fun make() {
world.refresh()
}
}

这就是 Unit 的去特殊性——或者说通用性——所给我们带来的便利。


例:函数类型的函数参数


同样的,这种去特殊性对于 Kotlin 的函数式编程也提供了方便。一个函数的函数类型的参数,在函数调用的时候填入的实参,只要符合声明里面的返回值类型,它是可以有返回值,也可以没有返回值的:


fun runTask(task: () -> Any) {
when (val result = task()) {
Unit -> println("result is Unit")
String -> println("result is a String: $result")
else -> println("result is an unknown type")
}
}

...

runTask { } // () -> Unit
runTask { "完成!" } // () -> String
runTask { 1 } // () -> Int

Java 不支持把方法当做对象来传递,所以我们没法跟 Java 做对比;但如果 Kotlin 不是像现在这样用了 Unit,而是照抄了 Java 的 void 关键字,我们就肯定没办法这样写。


小结:去特殊化


这就是我刚才所说的,对于无返回值的函数的「去特殊化」,是 Unit 最核心的价值。它相当于是对 Java 的 void 进行了缺陷的修复,让本来有的问题现在没有了。而对于实际开发,它的作用是属于润物细无声的,你不需要懂我说的这一大堆东西,也不影响你享受 Unit 的这些好处。


…………


那我出这期视频干嘛?


——开个玩笑。了解各种魔法背后的实质,对于我们掌握和正确地使用一门语言是很有必要的。


延伸:当做纯粹的单例对象来使用


比如,知道 Unit 是什么之后,你就能理解为什么它能作为函数的参数去被使用。


Compose 里的协程函数 LaunchedEffect() 要求我们填入至少一个 key 参数,来让协程在界面状态变化时可以自动重启:


LaunchedEffect(key) {
xxxx
xxxxxx
xxx
}

而如果我们没有自动重启的需求,就可以在参数里填上一个 Unit


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

因为 Unit 是不变的,所以把它填进参数里,这个协程就不会自动重启了。这招用着非常方便,Compose 的官方示例里也有这样的代码。不过这个和 Unit 自身的定位已经无关了,而仅仅是在使用它「单例」的性质。实际上,你在括号里把它换成任何的常量,效果都是完全一样的,比如 true、比如 false、比如 1、比如 0、比如 你好,都是可以的。所以如果你什么时候想「随便拿个对象过来」,或者「随便拿个单例对象过来」,也可以使用 Unit,它和你自己创建一个 object 然后去使用,效果是一样的。


总结


好,这就是 Kotlin 的 Unit,希望这个视频可以帮助你更好地了解和使用它。下期我会讲 Kotlin 里另一个特殊的类型:Nothing。关注我,了解更多 Android 开发的知识和技能。我是扔物线,我不和你比高低,我只助你成长。我们下期见!


作者:扔物线
链接:https://juejin.cn/post/7231345137850286138
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-EventBus修改纪实

背景 笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例: // Event.java public final class Event { private final in...
继续阅读 »

背景


笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例:


// Event.java
public final class Event {

private final int code;

public Event(int code) {
this.code = code;
}

public int getCode() {
return code;
}
}

// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

所以去查看了 EventBus 的源码,接下来我们分析下 EventBus 发送粘性事件的流程。


分析


粘性事件



以下源码基于 EventBus 3.3.1 版本



下面是发送粘性事件的源码:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

postSticky 代码比较简单,首先对 stickyEvents 进行加锁,接下来把 event 事件的 Class 对象作为 Key,event 事件本身作为 value 放进 Map 中,其中stickyEvents 是 Map 对象,实例是 ConcurrentHashMap, 其 Key 和 Value 的泛型形参分别是 ClassObject, 它的作用就是用来存储粘性事件;然后调用 post(event) 把粘性事件当作普通事件发送一下。


首先我们看下最后为什么要调用下 post(event)


虽然 post(evnet) 上面有注释,简单翻译下:"在放进 Map 后应该再发送一次,以防止订阅者想立即删除此事件",读完注释后,可能还是不太明白,这里笔者认为:在前面存储完粘性事件后,这里调用 post 把粘性事件当作普通事件发送出去,或许是因为现在已经有注册的粘性事件订阅者,此时把已经注册的粘性事件订阅者当作普通事件的订阅者,这样已经注册的粘性事件订阅者可以立即收到相应的事件,只是此时事件不再是粘性的。


postSticky 中我们并没有看到粘性事件是在哪里发送的,想一想我们使用粘性事件的目的是什么?当注册订阅者时可以收到之前发送的事件,这样来看,粘性事件的发送是在注册订阅者时,下面是注册订阅者的源码,删除了一些无关代码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

register 代码也比较简单,首先通过订阅者的 Class 对象查找订阅者所有的Event事件接收方法,然后对 EventBus 对象加锁,遍历所有的Event事件接收方法 subscriberMethods 调用 subscribe 方法,以下是 subscribe 方法源码:


// Key 为 Event Class 对象,Value 为存储 Event 的订阅者和接收 Event 方法对象的集合 
private final Map, CopyOnWriteArrayList> subscriptionsByEventType;

// Key 为订阅者对象,Value 为订阅者中的 Event Class对象集合
private final Map>> typesBySubscriber;

// Must be called in synchronized block
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// Event Class对象
Class eventType = subscriberMethod.eventType;

// 订阅者和接收 Event 方法对象
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);

// 根据 Event Class对象,获取订阅者和接收 Event 方法对象的集合
CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType);

// 判断订阅者和接收 Event 方法对象是否为空
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
// 判断是否已经包含了新的订阅者和接收 Event 方法对象,若是包含则认为是重复注册
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}

// 这里是按优先级排序插入到集合中
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}

// 这里是把 Event Class对象添加进对应订阅者的 Event Class对象集合中
List> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}

// 上面已经判断了是否重复注册,所以这里直接添加
subscribedEvents.add(eventType);

// 接下来就是粘性事件的发送逻辑了
// 判断 Event 接收方法是否可以处理粘性事件
if (subscriberMethod.sticky) {
// 这里判断是否考虑 Event 事件类的继承关系,默认为 Ture
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}

在上面的源码中,增加了不少注释有助于我们读懂源码,在源码的最后就是粘性事件的发送逻辑了,其中有两个分支,其中一个分支根据 Event 事件的继承关系发送事件,另外一个分支根据接收 Event 方法中的 Event Class 对象从 stickyEvents 中直接查找粘性事件,最后两个分支殊途同归,都调用了 checkPostStickyEventToSubscription 方法:


private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
if (stickyEvent != null) {
// If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
// --> Strange corner case, which we don't take care of here.
postToSubscription(newSubscription, stickyEvent, isMainThread());
}
}

checkPostStickyEventToSubscription 方法很简单,对粘性事件做下判空处理,继续调用 postToSubscription 方法,传入订阅者与接收 Event 方法对象,粘性事件和是否是主线程布尔值:


private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;A
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}

postToSubscription 方法比较长,但是比较好理解,就是根据接收 Event 方法上的 @Subscribe 注解中传入的线程模型进行事件的分发,具体的事件分发流程,有空再分析,本文就先不分析了,现在我们只需知道最后都会调用 invokeSubscriber(Subscription subscription, Object event) 方法即可:


void invokeSubscriber(Subscription subscription, Object event) {
try {
// 反射调用 Event 接收方法传入 Event 事件
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}

终于在 invokeSubscriber 方法中找到调用 Event 接收方法的地方了,原来 EventBus 最后是通过反射调用 Event 接收方法并传入相应 Event 事件的。


分析完 Event 事件的发送流程,好像没有发现为什么有时收不到粘性事件。


我们回过头来再看下笔者的使用示例,为了方便查看,下面贴出使用示例代码:


// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

可能细心的读者已经发现 test 方法调用了,问题应该出在 postSticky 方法中,让我们再次查看 postSticky 方法:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

根据前面分析 postSticky 方法的结果,stickyEvents 用于存储粘性事件,它是个 Map 结构,而 stickyEvents 的 Key 正是 Event 的 Class 对象,根据 Map 结构的存储原理:如果存在相同的 Key,则覆盖 Value 的值,而 stickyEvents 的 Value 正是 Event 本身。


终于真相大白,多次调用 test 方法发送粘性事件,EventBus 只会存储最后一次的粘性事件。


小结


EventBus 针对同一个粘性 Event 事件只会存储最后一次发送的粘性事件。


EventBus 的上述实现可能是因为多次发送同一个粘性事件,则认为之前的事件是过期事件应该抛弃,因此只传递最新的粘性事件。


EventBus 的这种实现无法满足笔者的业务逻辑需求,笔者希望多次发送的粘性事件,订阅者都能接收到,而不是只接收最新的粘性事件,可以理解为粘性事件必达订阅者,下面让我们修改 EventBus 的源码来满足需求吧。


修改


上一节我们分析了粘性事件的发送流程,为了满足粘性事件必达的需求,基于现有粘性事件流程,我们可以仿照粘性事件的发送来提供一个发送必达消息的方法。


Subscribe


首先我们定义 Event 接收方法可以接收粘性事件是在 @Subscribesticky = true , 所以我们可以修改 Subscribe 注解,增加粘性事件必达的方法:


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.POSTING;

/**
* If true, delivers the most recent sticky event (posted with
* {@link EventBus#postSticky(Object)}) to this subscriber (if event available).
*/

boolean sticky() default false;

// 增加消息必达的方法
boolean rendezvous() default false;

/** Subscriber priority to influence the order of event delivery.
* Within the same delivery thread ({@link ThreadMode}), higher priority subscribers will receive events before
* others with a lower priority. The default priority is 0. Note: the priority does *NOT* affect the order of
* delivery among subscribers with different {@link ThreadMode}s! */

int priority() default 0;
}

rendezvous 以为约会、约定的意思,可以理解为不见不散,在这里它有两层作用,其一是标记方法可以接收粘性事件,其二是标记方法接收的事件是必达的。


findSubscriberMethods


接下来就需要解析 rendezvous 了,我们先看看 sticky 是如何解析的,在上一节我们分析了 register 方法,方便查看,下面再贴出 register 方法源码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

上一节分析中,我们没有分析查找订阅者中所有的 Event 接收方法 findSubscriberMethods ,接下来我们分析下在 findSubscriberMethods 方法是如何查找 Event 接收方法的:


List findSubscriberMethods(Class subscriberClass) {
// 先从缓存中查找
List subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}

// 是否忽略生成索引,默认为False,所以这里走else分支
if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
// 查找Event接收方法
subscriberMethods = findUsingInfo(subscriberClass);
}

// 如果订阅者和订阅者父类中没有Event接收方法则抛出异常
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass
+ " and its super classes have no public methods with the @Subscribe annotation");
} else {
// 添加进缓存中
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}

调用 findSubscriberMethods 方法需要传入订阅者 Class 对象,通过笔者在源码中增加的注释分析发现默认调用 findUsingInfo 方法查找 Event 接收方法,我们继续跟踪 findUsingInfo 方法:


private List findUsingInfo(Class subscriberClass) {
// FindState对订阅者Class对象和Event接收方法进行了一层封装
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass); // ①
while (findState.clazz != null) {

// 查找订阅者信息,包含订阅者Class对象、 订阅者父类、Event接收方法等
findState.subscriberInfo = getSubscriberInfo(findState); // ②

// 在 ① initForSubscriber中会把subscriberInfo置为null,
// 在 ② getSubscriberInfo中没有Index对象,
// 所以第一次时这里会走else分支
if (findState.subscriberInfo != null) {
SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
for (SubscriberMethod subscriberMethod : array) {
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}
} else {
// 查找Event接收方法
findUsingReflectionInSingleClass(findState);
}

// 查找父类的Event接收方法
findState.moveToSuperclass();
}

// 通过findState返回Event接收方法,并回收findState
return getMethodsAndRelease(findState);
}

根据笔者在源码中的注释分析,在 findUsingInfo 方法中使用「享元模式」对 FindState 进行回收利用,避免创建大量临时的 FindState 对象占用内存,最后再次调用 findUsingReflectionInSingleClass 方法查找 Event 接收方法,看方法名字应该是使用反射查找,findUsingReflectionInSingleClass 源码较长,删减一些不关心的代码:


private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
// 通过反射获取当前类中声明的所有方法
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// 删减不关心的代码
}

// 遍历所有方法
for (Method method : methods) {

// 获取方法的修饰符
int modifiers = method.getModifiers();

// 判断方法是否是public的;是否是抽象方法,是否是静态方法,是否是桥接方法,是否是合成方法
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {

// 获取方法的形参Class对象数组
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {

// 获取方法上的Subscribe注解
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class eventType = parameterTypes[0];

// 检测是否已经添加了相同签名的方法,考虑子类复写父类方法的情况
if (findState.checkAdd(method, eventType)) {

// 获取注解的参数
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky(),

// 这里我们添加rendezvous参数 ①
subscribeAnnotation.rendezvous()));
}
}
}
// 删减不关心的代码
}
// 删减不关心的代码
}
}

findUsingReflectionInSingleClass 方法中通过反射获取订阅者中声明的所有方法,然后遍历所有方法:



  1. 首先判断方法的修饰符是否符合,

  2. 其次判断方法是否只有一个形参,

  3. 再次判断方法是否有 Subscribe 注解,

  4. 然后检测是否已经添加了相同签名的方法,主要是考虑子类复写父类方法这种情况,

  5. 最后获取 Subscribe 注解的参数,在这里我们解析 rendezvous,封装进 SubscriberMethod 中。


SubscriberMethod 中增加 rendezvous 字段,删除不关心的代码:


public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class eventType;
final int priority;
final boolean sticky;

// 增加 `rendezvous` 字段
final boolean rendezvous;
/** Used for efficient comparison */
String methodString;

public SubscriberMethod(Method method, Class eventType, ThreadMode threadMode,
int priority, boolean sticky,

// 增加 `rendezvous` 形参
boolean rendezvous) {
this.method = method;
this.threadMode = threadMode;
this.eventType = eventType;
this.priority = priority;
this.sticky = sticky;
this.rendezvous = rendezvous;
}
}

postRendezvous


好的,rendezvous 已经解析出来了,接下来我们对外提供发送必达事件的接口:


// 选择List存储必达事件,使用Pair封装必达事件的Key和Value
private final List, Object>> rendezvousEvents;

public void postRendezvous(Object event) {
synchronized (rendezvousEvents) {
rendezvousEvents.add(Pair.create(event.getClass(), event));
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

上面的源码,我们通过仿照 postSticky 方法实现了 postRendezvous 方法,在 postSticky 方法中使用 Map 存储粘性事件,不过我们在 postRendezvous 方法中使用 List 存储必达事件,保证必达事件不会因为 Key 相同而被覆盖丢失,最后也是调用 post 方法尝试先发送一次必达事件。


register


在上一节中我们分析了粘性事件是在 register 中调用 subscribe 方法进行发送的,这里我们仿照粘性事件的发送逻辑,实现必达事件的发送逻辑,我们可以在 subscribe 方法最后增加发送必达事件的逻辑,以下源码省略了一些不关心的代码:


private final List, Object>> rendezvousEvents;

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// 省略不关心的代码

// 粘性事件发送逻辑
if (subscriberMethod.sticky) {
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}

// 新增必达事件发送逻辑
// 判断方法是否可以接收必达事件
if (subscriberMethod.rendezvous) {
if (eventInheritance) {
for (Pair, Object> next : rendezvousEvents) {
Class candidateEventType = next.first;
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = next.second;
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object rendezvousEvent = getRendezvousEvent(eventType);
if (rendezvousEvent != null) {
checkPostStickyEventToSubscription(newSubscription, rendezvousEvent);
}
}
}
}

subscribe 方法中,我们通过仿照粘性事件的发送逻辑增加了必达事件的发送:



  1. 首先判断 Event 接收方法是否可以接收必达事件

  2. 其次考虑 Event 必达事件的继承关系,

  3. 最后两个分支都调用 checkPostStickyEventToSubscription 方法发送必达事件


happy~


总结


使用第三方库时,发现问题不要慌张,带着问题去查看源码总有一番收获,这也告诫我们在使用第三库时最好先搞明白它的实现原理,遇到问题时不至于束手无策。


通过分析 EventBus 的源码,我们有以下收获:



  1. 明白了我们注册订阅者时 EventBus 做了哪些事情

  2. 知晓了我们发送粘性事件时,EventBus 是如何处理及何时发送粘性事件的

  3. 了解到 EventBus 是通过反射调用 Event 事件的接收方法

  4. 学习了 EventBus 中的一些优化点,比如对 FindState 使用「享元模式」避免创建大量临时对象占用内存

  5. 进一步了解到对并发的处理


通过以上收获,我们成功修改 EventBus 源码实现了我们必达事件的需求。


到这里我们已经完成了必达事件的发送,不过我们还剩下获取必达事件,移除必达事件没有实现,最后 EventBus 中还有单元测试 module,我们还没有针对 rendezvous 编写单元测试,读者有兴趣的话,可以自己试着实现。


希望可以帮到你~


作者:guodongAndroid
链接:https://juejin.cn/post/7104107150678917133
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

入职东北国企做程序员一个月,感受如何?

工作环境 我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。 人 目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职...
继续阅读 »

工作环境


我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。



目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职责去负责自己的事情就好。


办公环境可以分为两个环境,分别是“职能部门”和“研发部门”:


* 职能部门比较正式,工位、装修以及员工着装都比较正规。
* 研发部门较为随意一些,无论是工位还是桌椅什么的,有些东拼西凑的感觉,但是整体还是可以接受。

另外可能是因为国企的原因,所有的工位都是大隔断那种,如果换成现在公司常见的大通桌,估计人数还能多做十好几个,毕竟我刚来的时候还没有正式工位坐呢。



相比于在其他公司上班,可能在这最大的体会就是不用考虑吃什么。公司有食堂,提供午饭,菜不能选,但是每天四菜一汤,加水果或酸奶。相比于每天纠结的选择外卖,我对这个很满意。


晚上如果加班的话,公司会统一订餐,大概一餐的费用也在20至30块之间吧,当然也没法选择吃什么,有啥吃啥被。


早餐为什么最后说,因为公司的早餐在早上八点之前供应,八点半上班。。。有点难受啊。


幸好公司提供简单的零食,面包、火腿肠、泡面等等,虽然偶尔会被大家抢空,但是总比没有强吧。



上家公司离我家只有1公里的距离,所以从回到哈尔滨也没有买车,每天不行上班,还挺惬意的。


现在不行了,新公司距离家里有十好几公里,当然我也暂时没有选择买车,地铁出行,快捷方便,还省心,唯一的缺点就是要走个1.5公里吧。


在晚上八点之后打车可以报销的,但是只能是网约车,可能是出租车的票,粘贴太过麻烦了吧。反正我是不打车,因为我嫌报销麻烦。


工具


啥是工具呢,对程序员来说就是电脑了,公司提供电脑,也可以自己买电脑进行报销,还是很人性化地。


公司的会议室设施还是不错的,各种投屏等等,比较先进,完全摒弃了传统的投影仪等等,这还让我对公司有种另眼相看的感觉。


还提供显示器什么的,自己申请就好了。


入职感受


我面试的岗位是java开发,常规的java框架使用起来都没有问题。面试过程还是比较简单的,主要是常用的一些组件,简单的实现原理等等,所以顺利通过了。


但是比较遗憾的公司给我砍了一些,定位的职级也不是很高。说实话我还是有点难受的,毕竟整个面试过程,和我对个人的能力认知还是比较清楚地。


但是当我入职后我明白了,这里毕竟是哈尔滨,收入和年龄还是有很大的关系的。部门内有好几位大哥,想想也就释然了,在其位谋其政吧,他们的工作确实我我接下来要做的繁琐。希望日后能够慢慢的升职加薪吧。


总体来说,东北人还是敞亮,有事直接提,工作也没啥拐弯抹角的,干就完了。我才刚来公司第一天,就给我把工作安排上了,一点不拿我当外人啊


工作感受


既然谈到工作了,就展开说说。


我第一天到公司,找了个临时工位,领导们各种git账号、禅道账号就给我创建好,一个项目扔给我,本月中期要求做完。。我当时内心的想法真的是:东北人果然是好相处啊,整的跟老同事似的。我能怎么办,干就完了啊。


项目还是很简单的,常规的springboot + mybatis + vue2的小项目,大概也没到月中期,一个礼拜就完事了。


比较让我惊喜的是部署的环节。居然使用的是devops工具KubeSphere。我只说这一句你们可能不理解,这是我在哈尔滨的第三家公司,从来没有一家公司说使用过k8s,甚至相关的devops工具。只能说是哈尔滨软件行业的云化程度还是太低了。唯一在上家公司的jenkins还是因为我想偷懒搭建的。


不过运维相关的内容都把握在运维人员手里,所以想要料及并且掌握先关的知识还是要自己私下去学习的。


项目其实都是小项目,以web端和app为主,基本都是前后端分离的单体架构。唯一我接触到的微服务架构应该就是公司的中台,提供统一的权限配置和登录认证,整体还是很不错的。


虽然公司的项目很多,工作看起来很忙碌,但实际还是比较轻松愉快的,我还能应付自如。每天晚上为了蹭一顿晚饭,通常会加班到七点半。用晚上这个时间更更文,也挺好的。


从体来说,是我比较喜欢的工作节奏。


个人分析


我是一个不太安定的人,长期干一件事会让我比较容易失去兴趣,还是挺享受刚换工作时,这段适应环境的感觉。也有可能更喜欢这种有一定挑战的感觉。


和上一家公司相比,这家公司在公司的时间明显多出很多,也没有那么悠闲了,但是我却觉得这更适合我,毕竟我是一个闲不住的人,安逸的环境让我感到格外的焦虑,忙碌的生活会让自己感到生活很充实。


记得之前的文章说过自己的身体健健的不太好,但是最近不知道是上班的路程变远,导致运动量的增加,之前不适的症状似乎都小时了。真闲出病来了




既来之,则安之,时刻提醒自己再努力点,阳光总在风雨后。


作者:我犟不过你
链接:https://juejin.cn/post/7125627005407592462
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一位 98 年程序员离职后

今天不写技术文了,写点轻松的。 我自己都没讨好自己,何苦要来讨好你呢? 开篇 本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、...
继续阅读 »

今天不写技术文了,写点轻松的。


img-16593196478205dc10966293f6e5e3f0be6d9ff93705f.jpg



我自己都没讨好自己,何苦要来讨好你呢?



开篇


本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、跑路的,这些消息可以说不绝于耳。但本人最终还是选择离职休整下。


得益于父辈们的努力,在房价还不到当前房价一半的时候出手了,让弱小的我在这座城市有瓦遮头,不必为三餐奔波。既没有房贷,也没有车贷,一人吃饱,全家不饿的我没有了这方面的顾虑,也有了底气做出这个选择。


img-165932129544253f44bc0a87ca13a950b2ff14f24ccd2.jpg



希望明天还能看到那朵软萌萌的云,因为它好像你



说下离职后我都干了些什么吧,给各位列一下,说不定能找到知音。



  • 刷力扣

  • 整理下之前的东西

  • 健身

  • 写作

  • 品鉴周董的新歌

  • 看综艺

  • ...


上面的这些东西不分先后,一直都在做。


刷力扣


刷力扣其实很早就开始了,每天登录力扣有一个积分,完成每日一题有 10 积分,到现在坚持了有差不多两年了,战绩如下:


QQ截图20220801104732.png



大部分是中等



光看题目量不算少,但其实大部分困难和部分中等都是 cv 之后通过的,不装了,摊牌了。刷题过程也不艰难,就一句话,简单题重拳出击,困难题唯唯诺诺。有些人会觉得算法没有必要,因为平时的工作就用不到。但我觉得算法最重要的是锻炼人的思维,思维很重要,它能够指导一个人思考问题的轨迹和方向。虽然有时刷题时会感觉自己活着就是凑数的,没必要灰心,真的,因为你的判断是对的。


整理下之前的东西


之前在工作时也积累一些东西,但没有做整理,所以趁着这段时间整理下,看下能不能发现一些新东西。个人觉得一直处于一种忙碌的状态并不一定是好事,这有点像吃东西时狼吞虎咽,容易噎着。


健身


这件事是坚持的最久的一件事,从高一一直到现在。高一时上映的速 7,被强森和郭达在办公室的那段打戏吸引,当时觉得男人就应该这样。于是从最简单的俯卧撑、引体开始,一点点的朝自己的目标努力。但这过程走了很多弯路,比如训练的方式不对,太急于求成、吃的没跟上、休息没跟上,导致很长一段时间都处于瓶颈期,一直在原地踏步。这种不上不下的感觉真的不好受,也想过放弃,但已经戒不掉了。图就不发了,担心被喷。胸、背、腿、腰、手、腹肌都有练,腹肌不多不少,正好 6 块。至于身材,我个人觉得还行,至少不差,也被人说过身材好,同性异性都有。


QQ截图20220801201459.png



被同性说



写作


这个貌似是在去年开始的,但中断了挺长一段时间,就想着在空窗期重新捡起来。至于最终能开出什么样的花,也没想过,就觉得写比不写强。读者感兴趣的可以看我之前写过的文章,主页:
鄙人的主页


img-1659357885349240b678eb24aca42039c30c16b002044.jpg



对待生活,不必说的太多,你同样可以给它一个惊喜



品鉴周董的新歌


本人虽说不是周董的粉丝,但以前总想着能在晴天里给千里之外的她带一壶爷爷泡的茶,面对面安静的坐着,她的笑容甜甜的,我也对着她傻笑。院子里是一片花海,散发着迷迭香。


她送我来到渡口,她的倒影在满是桃花的粉色海洋里若隐若现。船夫摇着桨,背对着我,哼着她唱过的红颜如霜突然来了句:这是最伟大的作品。可谁让夜晚留不住错过的烟火,活该我到现在还在流浪。


看综艺


综艺平时也是我解压的一种方式,最近把跑男第十季追完了,几位 mc 都是各有特点。不过最喜欢的还是新加入的白鹿,人美,很搞笑,魔性的笑声让人很容易记住她。


magazine-unlock-01-2.3.2022080201-7432B64DE5C9B11.jpg



你问我:我对你有多重要,我回答:太阳你知道吧



总结


可能有人看了之后会觉得有点躺平的趋势,但其实并没有。本人还是很爱折腾的,也希望能多认识点圈子以外的人,多认识点有趣的人,多认识点志同道合的人。有些人会觉得程序员很闷,不爱说话,天天就对着电脑。可能有部分人是这样的,但我不是,因为我是一个不走寻常路的程序员,而且我深知只有跳出圈子,才能打破认知。by the way,本人对数字化转型行业挺感兴趣的,有读者从事或者了解的话,可以大胆私信我啊。


作者:对方正在输入
来源:juejin.cn/post/7127653600532103198
收起阅读 »

记一次不规范使用key引发的惨案

web
前言 平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求 场景 在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的...
继续阅读 »

前言


平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求


场景


在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的排在前面,而且选好商品之后,需要在下单界面给每个商品选择发货地,发货地列表是通过商品id去接口取的,我的代码长这样:



  • 下单界面调用商品组件


// 这里每次选了商品都是从前插入:list.value = [...newList, ...list.value]
<Goods
v-for="(item, index) in list"
:key="index"
:goods="item">
</Goods>


  • 商品组件内部调用发货地组件


<SendAddress
v-model="address"
:product-no="goods.productNo"
placeholder="请选择发货地"
@update:model-value="updateValue"></SendAddress>


  • 发货地组件内部获取发货地址列表


onMounted(async () => {
getList()
})
const getList = async () => {
const postData = {
productInfo: props.productNo,
}
}

上述代码运行结果是,每次获取地址用的都是最开始选的那个商品的信息,百思不得其解啊,最后说服产品,不要倒序了,问题解决


解决过程


后来在研究前进刷新后退缓存时,关注到了组件的key,详细了解后才知其中来头


企业微信截图_16813558431830.png



重点:根据key复用或者更新,也就是key没有变化,就是复用,变化了在更新挂载,而onMounted是在挂载完成后执行,没有挂载的元素,就不会走onMounted



回到上述问题,当我们每次从前面插入数据,key的变化逻辑是这样的


结论


企业微信截图_16813564053499.png



最开始选中的商品key从1变成了2,最近选的是0。


而0和1是本来就存在的,只会更新数据,不会重新挂载,只有最开始选的那个商品key是全新的,会重新挂载,重新走onMounted。


所以每次选择数据后,拿去获取地址列表的商品信息都是第一个的



解决以上问题,把key改成item.productNo就解决了


作者:赖皮喵
来源:juejin.cn/post/7221357811287834680
收起阅读 »

1.6kB 搞定懒加载、无限滚动、精准曝光

web
上文提到有很多类库在用 IntersectionObserver 实现懒加载,但更精准的描述是,IntersectionObserver 提供了一种异步观察目标元素与根元素(窗口或指定父元素)的交叉状态的能力,这项能力不仅能用来做懒加载,还可以提供无限滚动,精...
继续阅读 »

上文提到有很多类库在用 IntersectionObserver 实现懒加载,但更精准的描述是,IntersectionObserver 提供了一种异步观察目标元素与根元素(窗口或指定父元素)的交叉状态的能力,这项能力不仅能用来做懒加载,还可以提供无限滚动,精准曝光的功能。


1. IntersectionObserver 基础介绍


不管我们使用哪个类库,都需要了解 IntersectionObserver 的基本原理,下面是一个简单的例子



import React, { useEffect } from "react";
import "./page.css";

const Page1 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;

useEffect(() => {
const io = new IntersectionObserver((entries) => {
console.log(entries[0].intersectionRatio);
});

const footer = document.querySelector(".footer");

if (footer) {
io.observe(footer);
}

return () => {
io.disconnect();
};
}, []);

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer">被观察的元素</div>
</div>

);
};

export default Page1;

如上例,可以了解到以下几点知识




  1. new 一个 IntersectionObserver 对象,下称 io,需传入一个函数,下称 callbackcallback 的入参 entries 代表了正在被观察的元素数组,数组的每一项都拥有属性 intersectionRatio ,代表了被观察的元素与根元素可视区域的交叉比例,。




  2. 使用 ioobserve 方法来添加你想观察的元素,可以多次调用添加多个,




  3. 使用 iodisconnect 方法来销毁观测




使用上方的代码,可以完成对元素最基本的观察。如上方 gif 操作,在控制台可得到以下结果 ,




  • 进入页面时,callback 被调用了一次:intersectionRatio 为 0

  • 滚动到可视区,再次调用:intersectionRatio > 0

  • 滚动出可视区,再次调用:intersectionRatio 为 0

  • 滚动到可视区,再次调用:intersectionRatio > 0


而懒加载,无限滚动,精准曝光是如何基于这个 api 去实现的呢,如果直接去写,当然也能实现,但是会有些繁琐,下面引入本篇文章的主角:react-intersection-observer 类库,先看看这个类库的基本介绍吧。


2. react-intersection-observer 基础介绍


这个类库在全局维护了一个 IntersectionObserver 实例(如果只有一个根元素,那全局仅有一个实例,实际上代码中维护了一个实例的 Map,此处简单表述),并提供了一个名为 useInViewhooks 方便我们了解到被观测的元素的观测状态。与上面相同的例子,他的写法如下:


import React, { useEffect } from "react";
import { useInView } from 'react-intersection-observer';
import "./page.css";

const Page2 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
onChange: (inView, entry) => {
console.log(entry.intersectionRatio);
}
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>被观察的元素</div>
</div>

);
};

export default Page2;

如上例,使用更少的代码,就实现了相同的功能,而且带来了一些好处



  • 不用自己维护 IntersectionObserver 实例,既不用关心创建,也不用关心销毁

  • 不用控制被观察的元素到底是 entries 内的第几个,观察事件都会在相应绑定的 onChange 中进行回调


以上仅为基本使用,实战中需求是更为复杂的,所以这个类库也提供了一系列属性,方便大家的使用:



利用上面这些配置项,我们可以实现以下功能


3. 实战用例


3.1. 懒加载


import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

interface Props {
width: number;
height: number;
src: string;
}

const LazyImage = ({ width, height, src, ...rest }: Props) => {
const { ref, inView } = useInView({
triggerOnce: true,
root: document.querySelector('.scroll-container'),
rootMargin: `0px 0px ${window.innerHeight}px 0px`,
onChange: (inView, entry) => {
console.log('info', inView, entry.intersectionRatio);
}
});

return (
<div
ref={ref}
style={{
position: "relative",
paddingBottom: `${(height / width) * 100}%`,
background: "#2a4b7a",
}}
>

{inView ? (
<img
{...rest}
src={src}
width={width}
height={height}
style={{ position: "absolute", width: "100%", height: "100%", left: 0, top: 0 }}
/>

) : null}
</div>

);
};

const Page3 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<LazyImage width={750} height={200} src={"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4acf97e7dc944bf8ad5719b2b42f026~tplv-k3u1fbpfcp-watermark.image?"} />
</div>

);
};

export default Page3;

懒加载中我们需要用到几个额外的属性:




  • triggerOnce :只触发一次




  • root:默认为文档视口(如果被观察的元素,父/祖元素中有 overflow: scroll,需要指定为该元素)




  • rootMarginrootmargin




    • 同 css 上右下左写法,需要带单位,可简写('200px 0px')




    • 正值代表观察区域增大,负值代表观察区域缩小






在图片懒加载中,因为通常不可能等到元素被滚动到了可视区域,才开始加载图片,所以需要调整 rootMargin ,可以写为,rootMargin: `0px 0px ${window.innerHeight}px 0px ,这样图片可以提前一屏进行加载。


同样懒加载不需要不可见的时候回收掉相应的 dom ,所以只需要触发一次,设置 triggerOncetrue 即可。


3.2. 无限滚动



import React, { useState } from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

const Page4 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const [datas, setDatas] = useState([1, 1, 1]);
const { ref } = useInView({
onChange: (inView, entry) => {
console.log("inView", inView);
if (inView) {
setDatas((prevDatas) => [...prevDatas, ...new Array(3).fill(1)]);
}
},
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
{datas.map((item, index) => {
return (
<div key={index + 1} className="placeholder">
第{index + 1}个元素
</div>
);
})}
<div className="load-more" ref={ref}></div>
</div>

);
};

export default Page4;

无限滚动主要依赖在 onChange 中对 inView 进行判断,我们可以添加一个高度为0的元素,名为 load-more ,当页面滚动到最下方时,该元素的 onChange 会被触发,通过对 inViewtrue 的判断后,加载后续的数据。同理,真正的无限滚动也需要提前加载(在观察内写异步请求等),也可以设置相应的 rootMargin ,让无限滚动更丝滑。


3.3. 精准曝光



import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

const Page5 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
threshold: 0.5,
delay: 500,
onChange: (inView, entry) => {
if (inView) {
console.log("元素需要上报曝光事件", entry.intersectionRatio);
}
},
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>
需要精准曝光的元素
</div>
</div>

);
};

export default Page5;

精准曝光也是很常见的业务需求,通常此类需求会要求元素的露出比例和最小停留时长。



  • 对露出比例要求的原因:因为有可能元素的有效信息并未展示,只是露出了一点点头,一般业务上会要求露出比例大于一半。

  • 对停留时长要求的原因:有可能用户快速划过,比如小说看到了很啰嗦的章节快速滑动,直接看后面结果,如果不加停留时长,中间快速滑动的区域也会曝光,与实际想要的不符。


类库恰好提供了下面两个属性方便大家的使用



  • threshold: 观察元素露出比例,取值范围 0~1,默认值 0

  • delay: 延迟通知元素露出(如果延迟后元素未达标,则不会触发onChange),取值单位毫秒,非必填。


使用上面两个属性,就可以轻松实现业务需求。


3.4. 官方示例


示例,官方示例中还有很多对属性的应用,比如 threshold 传入数组,skiptrack-visibility ,大家可自行体验。


总结


以上就是对 IntersectionObserver 以及 react-intersection-observer 的介绍了,希望能对大家有所帮助,文中录制的示例完整项目可以从此处获取。


作者:windyrain
来源:juejin.cn/post/7220309530910851130
收起阅读 »

字节都在用的代码自动生成

web
背景 如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。 字节内部的 RPC 定义主要基于 thrift 实现,thrift 定义了数据结构和函数,那么是否可以用来作为接口定义提供给前端使用呢?如果可以作...
继续阅读 »

背景


如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。


字节内部的 RPC 定义主要基于 thrift 实现,thrift 定义了数据结构和函数,那么是否可以用来作为接口定义提供给前端使用呢?如果可以作为接口定义,是不是也可以通过接口定义自动生成请求接口的代码呢?答案是肯定的,字节内部已经衍生出了多个基于 thrift 的代码生成工具,本篇文章主要介绍如何通过 thrift 生成前端接口调用的代码。


接口定义


接口定义,顾名思义就是用来定义接口的语言,由于字节内部广泛使用的 thrift 基本上满足接口定义的要求,所以我们不妨直接把 thrift 当成接口定义。


thrift 是一种跨语言的远程过程调用 (RPC) 框架,如果你对 Typescript 比较熟悉的话,那它的结构看起来应该很简单,看个例子:


namespace go namesapce

// 请求的结构体
struct GetRandomRequest {
1: optional i32 min,
2: optional i32 max,
3: optional string extra
}

// 响应的结构体
struct GetRandomResponse {
1: optional i64 random_num
}

// 定义服务
service RandomService {
GetRandomResponse GetRandom (1: GetRandomRequest req)
}

示例中的 service 可以看成是一组函数,每个函数可以看成是一个接口。我们都知道,对于 restful 接口,还需要定义接口路径(比如 /getUserInfo)和参数(query 参数、body 参数等),我们可以通过 thrift 注解来表示这些附加信息。


namespace go namesapce

struct GetRandomRequest {
1: optional i32 min (api.source = "query"),
2: optional i32 max (api.source =
"query"),
3: optional string extra (api.source = "body"),
}

struct GetRandomResponse
{
1: optional i64 random_num,
}

// Service
service RandomService {
GetRandomResponse GetRandom (1: GetRandomRequest req) (api.get = "/api/get-random"),
}

api.source 用来指定参数的位置,query 表示是 query 参数,body 表示 body 参数;api.get="/api/get-random" 表示接口路径是 /api/get-random,请求方法是 GET;


生成 Typescript


上面我们已经有了接口定义,那么对应的 Typescript 应该就呼之欲出了,一起来看代码:


interface GetRandomRequest {
min: number;
max: number;
extra: string;
}

interface GetRandomResponse {
random_num: number;
}

async function GetRandom(req: GetRandomRequest): Promise<GetRandomResponse> {
return request<GetRandomResponse>({
url: '/api/get-random',
method: 'GET',
query: {
min: req.min,
max: req.max,
},
body: {
extra: req.extra,
}
});
}


生成 Typescript 后,我们无需关心生成的代码长什么样,直接调用 GetRandom 即可。


架构设计


要实现基于 thrift 生成代码,最核心的架构如下:


image.png
因为 thrift 的内容我们不能直接拿来用,需要转化成中间代码(IR),这里的中间代码通常是 json、AST 或者自定义的 DSL。如果中间代码是 json,可能的结构如下:


{
name: 'GetRandom',
method: 'get',
path: '/api/get-random',
req_schema: {
query_params: [
{
name: 'min',
type: 'int',
optional: true,
},
{
name: 'max',
type: 'int',
optional: true,
}
],
body_params: [
{
name: 'extra',
type: 'string',
optional: true,
}
],
header_params: [],
},
resp_schema: {
header_params: [],
body_params: [],
}
}

为了保持架构的开放性,我们在核心链路上插入了 PrePlugin 和 PostPlugin,其中 PrePlugin 决定了 thrift 如何转化成 IR,PostPlugin 决定 IR 如何生成目标代码。


这里之所以是「目标代码」而不是「Typescript 代码」,是因为我希望不同的 PostPlugin 可以产生不同的目标代码,比如可以通过 TSPostPlugin 生成 Typescript 代码,通过 GoPostPlugin 生成 go 语言的代码。


总结


代码生成这块的内容还有很多可以探索的地方,比如如何解析 thrift?是找第三方功能生成 AST 还是通过 pegjs 解析成自定义的 DSL?多文件联编如何处理、字段名 case 如何转换、运行时类型校验、生成的代码如何与 useRequest 或 ReactQuery 集成等。


thrift 其实可以看成接口定义的具体实现,如果 thrift 不满足你的业务场景,也可以自己实现一套类似的接口定义语言;接口定义作为前后端的约定,可以降低前后端的沟通成本;代码生成,可以提升前端代码的质量和研发效率。


如果本文对你有启发,欢迎点赞、关注、留言交流。


作者:探险家火焱
来源:juejin.cn/post/7220054775298359351
收起阅读 »

前端怎么样限制用户截图?

web
做后台系统,或者版权比较重视的项目时,产品经常会提出这样的需求:能不能禁止用户截图?有经验的开发不会直接拒绝产品,而是进行引导。 先了解初始需求是什么?是内容数据过于敏感,严禁泄漏。还是内容泄漏后,需要溯源追责。不同的需求需要的方案也不同。来看看就限制用户截图...
继续阅读 »

做后台系统,或者版权比较重视的项目时,产品经常会提出这样的需求:能不能禁止用户截图?有经验的开发不会直接拒绝产品,而是进行引导。


先了解初始需求是什么?是内容数据过于敏感,严禁泄漏。还是内容泄漏后,需要溯源追责。不同的需求需要的方案也不同。来看看就限制用户截图,有哪些脑洞?


有哪些脑洞


v站和某乎上的大佬给出了不少脑洞,我又加了点思路。


1.基础方案,阻止右键保存和拖拽。


这个方案是最基础,当前可只能阻拦一些小白用户。如果是浏览器,分分钟调出控制台,直接找到图片url。还可以直接ctrl+p,进入打印模式,直接保存下来再裁减。


2.失焦后加遮罩层


这个方案有点意思,看敏感信息时,必须鼠标点在某个按钮上,照片才完整显示。如果失去焦点图片显示不完整或者直接遮罩盖住。


3.高速动态马赛克


这个方案是可行的,并且在一些网站已经得到了应用,在视频或者图片上随机插像素点,动态跑来跑去,对客户来说,每一时刻屏幕上显示的都是完整的图像,靠用户的视觉残留看图或者视频。即时手机拍照也拍不完全。实际应用需要优化的点还是挺多的。比如用手机录像就可以看到完整内容,只是增加了截图成本。


下面是一个知乎上的方案效果。(原地址):


image.png


正经需求vs方案


其实限制用户截图这个方案本身就不合理,除非整个设备都是定制的,在软件上阉割截图功能。为了这个需求添加更复杂的功能对于一些安全性没那么高的需求来说,有点本末倒置了。


下面聊聊正经方案:


1.对于后台系统敏感数据或者图片,主要是担心泄漏出去,可以采用斜45度七彩水印,想要完全去掉几乎不可能,就是观感比较差。


2.对于图片版权,可以使用现在主流的盲水印,之前看过腾讯云提供的服务,当然成本比较高,如果版权需求较大,使用起来效果比较好。


3.视频方案,tiktok下载下来的时候会有一个水印跑来跑去,当然这个是经过处理过的视频,非原画,画质损耗也比较高。Netflix等视频网站采用的是服务端权限控制,走的视频流,每次播放下载加密视频,同时获得短期许可,得到许可后在本地解密并播放,一旦停止播放后许可失效。


总之,除了类似于Android提供的截图API等底层功能,其他的功能实现都不完美。即使是底层控制了,一样可以拍照录像,没有完美的方案。不过还是可以做的相对安全。


你还有什么新思路吗?有的话咱评论区见,欢迎点赞收藏关注,感谢!


作者:正经程序员
来源:juejin.cn/post/7127829348689674253
收起阅读 »

Vue 实现接口进度条

web
前端在向后端请求信息时,常常需要等待一定的时间才能得到返回结果。为了提高用户体验,可以通过实现一个接口进度条函数来增加页面的交互性和视觉效果。 接口响应快 - 效果 接口响应慢 - 效果 实现思路 首先定义一个进度条组件来渲染页面展示效果,组件包含进度条背...
继续阅读 »

cover.png


前端在向后端请求信息时,常常需要等待一定的时间才能得到返回结果。为了提高用户体验,可以通过实现一个接口进度条函数来增加页面的交互性和视觉效果。



接口响应快 - 效果



接口响应慢 - 效果


实现思路


首先定义一个进度条组件来渲染页面展示效果,组件包含进度条背景、进度长度、以及进度数字,同时还要设置数据绑定相关属性,如进度条当前的百分比、动画执行状态、以及完成状态等。在请求数据的过程中,需要添加监听函数来监测数据请求的过程变化,并更新组件相应的属性和界面元素。


代码实现


下面是使用 Vue 实现一个接口进度条的栗子:


<template>
<div class="progress-bar">
<div class="bg"></div>
<div class="bar" :style="{ width: progress + '%' }"></div>
<div class="label">{{ progress }}%</div>
</div>
</template>

<script>
export default {
data() {
return {
progress: 0,
isPlaying: false,
isCompleted: false
}
},
mounted() {
this.start();
},
methods: {
start() {
this.isPlaying = true;
this.animateProgress(90)
.then(() => {
if (!this.isCompleted) {
this.animateProgress(100);
}
})
.catch((error) => {
console.error('Progress error', error);
});
},
animateProgress(target) {
return new Promise((resolve, reject) => {
let start = this.progress;
const end = target;
const duration = (target - start) * 150;

const doAnimation = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);

this.progress = start + ((end - start) * progress);

if (progress === 1) {
resolve();
} else if (this.isCompleted) {
resolve();
} else {
requestAnimationFrame(doAnimation);
}
};

const startTime = Date.now();
requestAnimationFrame(doAnimation);
});
},
finish() {
this.isCompleted = true;
this.progress = 100;
}
}
};
</script>

<style scoped>
.progress-bar {
position: relative;
height: 8px;
margin: 10px 0;
}
.bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #ccc;
border-radius: 5px;
}
.bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: 5px;
background-color: #409eff;
transition: width 0.5s;
}
.label {
position: absolute;
top: -20px;
left: calc(100% + 5px);
color: #333;
font-size: 12px;
}
</style>

首先定义了三个数据属性用于控制动画的播放和完成状态,分别是进度条当前比例 progress、动画播放状态 isPlaying、动画完成状态 isCompleted。在组件初始化的过程中,调用了 start 方法来启动进度条动画效果。在该方法内部,使用 Promise 来从 0% 到 90% 的百分比向相应位置移动,并在到达该位置时停止。


判断当前是否完成,如果没有完成则再次调用 animateProgress(100) ,并在进度加载期间检查是否有数据返回。若存在,则停止前半段动画,并使用1秒钟将进度条填充至100%。


下面讲解一下如何在请求数据的过程中添加监听函数:


import axios from 'axios';
import ProgressBar from './ProgressBar.vue';

const progressBar = new Vue(ProgressBar).$mount();
document.body.appendChild(progressBar.$el);

在这个代码片段中,使用了 Axios 拦截器来监听请求的过程。在请求开始之前,向页面添加进度条组件,之后将该组件挂载到页面中,并且将其元素追加到 HTML 的 <body> 标记尾部。


接下来,通过 onDownloadProgress 监听函数来监测下载进度的变化。如果加载完成则移除进度条组件。同时,也可以实现针对使用不同 API 的 ajax 请求设定不同的进度条,以达到更佳的用户体验效果。


axios.interceptors.request.use((config) => {    
const progressBar = new Vue(ProgressBar).$mount();
document.body.appendChild(progressBar.$el);

config.onDownloadProgress = (event) => {
if (event.lengthComputable) {
progressBar.progress = parseInt((event.loaded / event.total) * 100, 10);
if (progressBar.progress === 100) {
progressBar.finish();
setTimeout(() => {
document.body.removeChild(progressBar.$el);
}, 500);
}
}
};

return config;
}, (error) => {
return Promise.reject(error);
});

参数注入


为了能够灵活地调整接口进度条效果,可以使用参数注入来控制动画速度和完成时间的设定。在 animateProgress 函数中,使用传参来设置百分比范围和动画播放速度,从而得到不同进度条和播放时间的效果。


animateProgress(target, duration) {
return new Promise((resolve, reject) => {
let start = this.progress;
const end = target;

const doAnimation = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);

this.progress = start + ((end - start) * progress);

if (progress === 1) {
resolve();
} else if (this.isCompleted) {
resolve();
} else {
requestAnimationFrame(doAnimation);
}
};

const startTime = Date.now();
requestAnimationFrame(doAnimation);
});
}

...

this.animateProgress(90, 1000)
.then(() => {
if (!this.isCompleted) {
this.animateProgress(100, 500);
}
})
...

在这个栗子中,将 duration 参数添加到 animateProgress 函数内部,并使用该参数来设置动画速度和完成时间。在第一个调用函数的时候,将异步进度条的播放时间设为 1000ms,从而得到速度较慢、完成时间较长的进度条效果。在第二个调用函数时,将进度条完成时间缩短为 500ms,并获得由此带来的更快动画效果。


总结


实现一个接口进度条函数可以提高网站性能和用户体验效果,同时也可以让页面更加交互性和生动有趣。在栗子中,使用了 Vue 框架来构建动画组件,使用了 Axios 拦截器来监听请求进度,使用了参数注入来控制动画速度和完成时间。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7225417805855916087
收起阅读 »

Js中异步代码挂起怎么解决?

web
从下面代码引入问题 function a() { console.log('aa'); } function b() { setTimeout(() => { //异步代码 console.log('bb'); ...
继续阅读 »

从下面代码引入问题


function a() {
console.log('aa');
}

function b() {
setTimeout(() => { //异步代码
console.log('bb');
}, 1000)
}

function c() {
console.log('cc');
}

a()
b()
c()

上述代码的执行结果为先打印'aa',再打印'cc',等一秒后再打印'bb'。哎?我们是不是就有疑问了,我们明显是先调用的函数a,再调用的函数b,最后调用的函数c,为什么函数b的打印结果最后才出来呢?这里我们要清楚的是函数b中定义了一个计时器,执行此代码是需要时间的,属于异步代码,当浏览器执行到此代码时,会先将此程序挂起,继续往下执行,最后才会执行异步代码。那要怎么解决此类问题呢?一个方法是将其他函数体内也定义一个计时器,这样也就会按顺序调用了,但是这样太不优雅了;还一个方法是函数c作为参数传入函数b,在函数b中执行掉,这样也不优雅。es6出来后就可以使用promise来解决此问题了。


js是一种单线程语言


什么是单线程?


我们可以理解为一次只能完成一个任务,如果有其他任务进来,那就需要排队了,一个任务完成了接着下一个任务。



因为js是一种单线程语言,任务是按顺序执行的,但是有时我们有多个任务同时执行的需求,这就需要异步编程的思想。



什么是异步?


当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情。


什么是异步模式调用? 


前一个任务执行完,调用回调函数而不是进行后一个任务。后一个任务不等前一个任务结束就执行,任务排列顺序与执行顺序无关。


什么是回调函数?


把函数当作参数传入另一个函数中,不会立即执行,当需要用这个函数时,再回调运行()这个函数。



以前是通过回调函数实现异步的,但是回调用多了会出现回调地狱,导致爆栈。



举个用回调函数来解决异步代码挂起问题


<body>
<div class="box">
<audio src="" id="audio" controls></audio> </audio>
</div>
<script>
//ajax
let url = ''
function getSong(cb) {
$.ajax({
url: ' 数据地址',
dataType: 'json',
success(res) {
console.log(res);
url = res[0].url
cb()
}
})
}
getSong(playSong)

function playSong() {
let audio = document.getElementById('audio')
window.addEventListener('click', () => {
audio.src = url
window.onclick = function () {
audio.play()
}
})
}

</script>
</body>

代码中用ajax向后端获取数据,这是需要时间的,属于异步代码,当我们分开调用这两个函数,函数getSong中的异步代码会出现挂起状态,导致函数playSong中的url获取不到值,会出现报错的情况,运用回调函数可以很好地解决这个问题。


Promise的使用


先执行一段代码


function xq() {

setTimeout(() => {
console.log('老王');
}, 2000)

}


function marry() {
setTimeout(() => {
console.log('老王结婚了');
}, 1000)
}


function baby() {
setTimeout(() => {
console.log('小王出生了');
}, 500)
}
xq()
marry()
baby()

结果为


1.png


???这是不是有点违背了道德,只能说老王是个渣男。这时候我们就需要使用promise对象来调整一下顺序了。


function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('老王去相亲');
resolve('ok')
}, 2000)
})
}


function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('老王结婚了');
resolve('ok')
}, 1000)
})
}


function baby() {
setTimeout(() => {
console.log('小王出生了');
}, 500)
}


// xq().then(() => {
// marry().then(() => {
// baby()
// })
// })
xq()
.then(marry)
.then(baby)
// xq().then(marry)
// baby()

在这里我们可以理解为老王相亲的时候疯狂对相亲对象promise,才有了后面的步入婚姻的殿堂,结婚后想生个娃也要对妻子疯狂的promise,才有了后面的小王出生了。


老王长叹了一口气,终于通过promise挽回了形象。


小结


Js异步编程方法不只这两种,还有比如事件监听,发布/订阅,生成器函数 Generator/yield等。需要我们一起去探索研究,毕竟‘学无止境’。


作者:一拾九
来源:juejin.cn/post/7225257817345884221
收起阅读 »

😈当一个摆子前端太闲的时候会做什么

国破山河在,城春草木深。 ——杜甫·春望 今日周一,在下与诸位同道中人一起来讨论一个话题:当一个前端空闲的时候会做些什么。 🤯是独自深耕论坛,钻研学术? 👯还是三两闲聊打趣,坐而论道? 💆‍♂️亦或是闭目养神,神游天地? 作为一名优秀的(摆子、摸鱼、切图....
继续阅读 »

国破山河在,城春草木深。 ——杜甫·春望



今日周一,在下与诸位同道中人一起来讨论一个话题:当一个前端空闲的时候会做些什么


🤯是独自深耕论坛,钻研学术?


👯还是三两闲聊打趣,坐而论道?


💆‍♂️亦或是闭目养神,神游天地?




作为一名优秀的(摆子、摸鱼、切图...)前端开发者,在下在空闲时间最喜欢做的还是钻研(混)前端技术(工作量)。


新的一周,新的开始,上篇文章中有同学批评在下说不够“玩”,那么这周就“简单”画一个鼠标精灵再交予各位“玩一玩”吧。



说明一下:在下说的玩,是写一遍嗷


温馨提示:文章较长,图片较多,不耐看的同学可以先去文末玩一玩在下的“大眼”,不满足了再去创造属于各位自己的鼠标精灵



以下是这周“玩具”的简单介绍:



  • 名称:大眼

  • 生辰:发文时间的昨天(2022-08-15)

  • 性别:随意

  • 情绪:发怒/常态

  • 状态:休眠/工作中

  • 简介:没啥特别的,大眼就干一件事,就是盯着你的鼠标,以防你找不到鼠标了。不过大眼有起床气,而且非常懒散,容易犯困。


大眼生活照:


image.png


接下来请各位跟随在下的节奏,一步一步把自己的“大眼”创造出来。


👀 画“大眼”先画圆


老话说“画人先画骨”,同样画大眼也得先画它的骨,嗯......也就是个圆,没错,就是个普通的圆


在下的笔法还是老套路,先给他一个容器。


<div class="eyeSocket"></div>

给大眼容器添加一些必要的样式


body {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #111;
}
.eyeSocket {
position: absolute; // 浮动居中
left: calc(50% - 75px);
top: calc(50% - 75px);
width: 150px; // 固定宽度
aspect-ratio: 1; // 长宽比 1:1 如果浏览器不支持该属性,换成 height: 150px 也一样
border-radius: 50%;
border: 4px solid rgb(41, 104, 217);
z-index: 1;
}

效果:


image.png


然后就是另外两个圆和一些阴影效果,由于另外两个圆没有特殊的动效,所以在下使用两个伪元素来实现


.eyeSocket::before,
.eyeSocket::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); // 居中
border-radius: 50%;
box-sizing: border-box; // css3盒子模型
}
.eyeSocket::before {
width: calc(100% + 20px);
height: calc(100% + 20px);
border: 6px solid #02ffff;
}
.eyeSocket::after {
width: 100%;
height: 100%;
border: 4px solid rgb(35, 22, 140);
box-shadow: inset 0px 0px 30px rgb(35, 22, 140);
}

效果:


image.png


👀 画龙需点睛


大眼的眼球画好了,之后就需要给它点上眼睛,喜欢什么样的眼睛因人而异,在下就选择这种分割线来作为大眼的眼仁。


为了方便做一些过渡效果,在下使用echarts来完成这个眼仁。


首先在下需要各位通过任何方式引入echarts库,然后给眼仁一个容器,并初始化echarts画布。


<div class="eyeSocket">
<div id="eyeball"></div>
</div>

#eyeball {
width: 100%;
height: 100%;
}

// 画眼球
let eyeball = document.getElementById('eyeball'); // 获取eyeball元素
let eyeballChart = echarts.init(eyeball); // 初始化画布
function getEyeballChart() {
eyeballChart.setOption({
series: [
{
type: 'gauge', // 使用仪表盘类型
radius: '-20%', // 采用负数是为了让分割线从内向外延伸
clockwise: false,
startAngle: '0', // 起始角度
endAngle: '270', // 结束角度
splitNumber: 3, // 分割数量,会将270度分割为3份,所以有四根线
detail: false,
axisLine: {
show: false,
},
axisTick: false,
splitLine: {
show: true,
length: 12, // 分割线长度
lineStyle: {
shadowBlur: 20, // 阴影渐变
shadowColor: 'rgb(0, 238, 255)', // 阴影颜色
shadowOffsetY: '0',
color: 'rgb(0, 238, 255)', // 分割线颜色
width: 4, // 分割线宽度
}
},
axisLabel: false
},
{
type: 'gauge',
radius: '-20%',
clockwise: false,
startAngle: '45', // 倾斜45度
endAngle: '315',
splitNumber: 3,
detail: false,
axisLine: {
show: false,
},
axisTick: false,
splitLine: {
show: true,
length: 12,
lineStyle: {
shadowBlur: 20,
shadowColor: 'rgb(0, 238, 255)',
shadowOffsetY: '0',
color: 'rgb(0, 238, 255)',
width: 4,
}
},
axisLabel: false
}
]
})
}
getEyeballChart();

效果:


image.png


眼仁就这么轻轻松松的画好了,对于常用echarts的同学可以说是轻而易举,对吧。


同时一个静态的大眼也创建完毕,接下来就要给大眼赋予生命了。



再次提醒:长文,而且代码量多,建议抽思路看即可。



✨ 生命仪式:休眠状态


赋予生命是神圣的,她需要一个过程,所以在下从最简单的开始——休眠状态


在下给大眼设计的休眠状态,就是闭着眼睛睡觉,其实不露出眼仁同时有节奏的呼吸(缩放)罢了,相比于整个生命仪式来说,还是比较简单的,只需要修改大眼外框的大小即可。


呼吸

这里在下采用的是css转换+动画的方式


<div class="eyeSocket eyeSocketSleeping">
<div id="eyeball"></div>
</div>

/* ...其他样式 */
.eyeSocketSleeping {
animation: sleeping 6s infinite;
}

@keyframes sleeping {
0% {
transform: scale(1);
}

50% {
transform: scale(1.2);
}

100% {
transform: scale(1);
}
}

sleeping.gif


闭眼

搞定了呼吸,但是睁着眼睛怎么睡得着?


所以接下来在下要帮助大眼把眼睛闭上,这时候咱们前面给眼睛设置负数radius的好处就来了(其实是在下设计好的),因为分割线是从内向外延伸的,所以此时只需要慢慢减小分割线的高度,即可实现眼睛慢慢缩小的效果,即在下给大眼设计的闭眼效果。


实现的效果是:大眼慢慢闭上眼睛(分割线缩小至0),然后开始呼吸


直接上代码


<div class="eyeSocket" id='bigEye'> // 去掉 eyeSocketSleeping 样式,添加id
<div id="eyeball"></div>
</div>

let bigEye = document.getElementById('bigEye'); // 获取元素
// ...其他代码
let leftRotSize = 0; // 旋转角度
let ballSize = 12; // 眼睛尺寸
let rotTimer; // 定时器

function getEyeballChart() {
eyeballChart.setOption({
series: [
{
startAngle: `${0 + leftRotSize * 5}`, // 加为逆时针旋转,乘5表示速度为leftRotSize的倍
endAngle: `${270 + leftRotSize * 5}`, // 即变为每10微秒移动0.5度,1234678同理
// ...其他
splitLine: {
length: ballSize, // 分割线高度设置为眼球尺寸变量
},
},
{
startAngle: `${45 + leftRotSize * 5}`,
endAngle: `${315 + leftRotSize * 5}`,
// ...其他
splitLine: {
length: ballSize, // 同上
}
},
}
]
})
}
// 休眠
function toSleep() {
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart()
if (ballSize > 0) {
ballSize -= 0.1; // 当眼球存在时慢慢减小
} else {
bigEye.className = 'eyeSocket eyeSocketSleeping'; // 眼球消失后添加呼吸
}
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1); // 旋转,
}, 10);
}
getEyeballChart();
toSleep()


旋转实现原理:(看过在下第一篇动效的同学对旋转的实现原理应该不陌生)


修改每个圈的起始角度(startAngle)和结束角度(endAngle),并不断刷新视图,


增加度数为逆时针旋转,减去度数为顺时针旋转



如此一来就实现了眼睛缩小消失,然后开始呼吸的过程,同时咱们的大眼也进入了生命仪式之休眠状态(乱入的鼠标有点烦);


tosleep.gif


✨ 生命仪式:起床气状态


在下相信,在座(站?蹲?)的各位同僚身边或者自身都存在一些小毛病,譬如咱们的大眼,它不但懒,喜欢睡觉,甚至叫醒它还会生气,通俗讲就是有起床气


心理学上说有种说法是:情绪会让你接近生命的本真


生命不就是情绪的结合嘛,没有情绪怎么能称之为生命的呢?


在设计之前我们还有点准备工作,就是让大眼先处于休眠状态


<div class="eyeSocket eyeSocketSleeping" id='bigEye'> // 添加休眠
<div id="eyeball"></div>
</div>

// ...其他代码
let ballSize = 0; // 初始眼球尺寸为0
// ...其他代码
// getEyeballChart(); // 把这两行删掉
// toSleep() // 把这两行删掉

唤醒

然后我们需要唤醒大眼,所以首先我们需要添加唤醒动作——点击事件;


let bigEye = document.getElementById('bigEye'); // 获取元素
// ...其他代码
let leftRotSize = 0;
let ballSize = 0;
let rotTimer;
let isSleep = true; // 是否处于休眠状态
// 添加点击事件,当处于休眠状态时执行唤醒方法
bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})
// 唤醒
function clickToWeakup() {
isSleep = false; // 修改状态
bigEye.className = 'eyeSocket'; // 清除休眠状态
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart()
ballSize <= 12 && (ballSize += 0.1);
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
}, 10);
}

这样点一下大眼它就苏醒了过来:


toWeakup.gif


生气

但是!


这是一个没有情绪的大眼,而在下需要的是一个有起床气的大眼,所以这样的大眼咱们不要!


退格←...退格←...退格←...退格←...退格←...退格←......


......


慢点慢点,也不是全都不要了,咱们只需要修改一下他唤醒以后的操作,给他添加上起床气不就行了?


接着来吧:


首先我们把代表了大眼常态的蓝色系抽离出来,使用css变量代替,然后再苏醒后给他添加成代表生气的红色系


body {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #111;
perspective: 1000px;
--c-eyeSocket: rgb(41, 104, 217);
--c-eyeSocket-outer: #02ffff;
--c-eyeSocket-outer-shadow: transparent;
--c-eyeSocket-inner: rgb(35, 22, 140);
}
.eyeSocket {
/* 其他属性 */
border: 4px solid var(--c-eyeSocket);
box-shadow: 0px 0px 50px var(--c-eyeSocket-outer-shadow); /* 当生气时添加红色外发光,常态则保持透明 */
transition: border 0.5s ease-in-out, box-shadow 0.5s ease-in-out; /* 添加过渡效果 */
}
.eyeSocket::before,
.eyeSocket::after {
/* 其他属性 */
transition: all 0.5s ease-in-out; /* 添加过渡效果 */
}
.eyeSocket::before {
/* 其他属性 */
border: 6px solid var(--c-eyeSocket-outer);
}
.eyeSocket::after {
/* 其他属性 */
border: 4px solid var(--c-eyeSocket-inner);
box-shadow: inset 0px 0px 30px var(--c-eyeSocket-inner);
}

// ...其他代码
let ballColor = 'transparent'; // 默认透明,其实默认是啥都无所谓,反正看不见

function getEyeballChart() {
eyeballChart.setOption({
series: [
{
// ...其他
splitLine: {
// ...其他
lineStyle: {
// ...其他
shadowColor: ballColor, // 把眼睛的眼影颜色设为变量控制
color: ballColor,
}
},
},
{
// ...其他
splitLine: {
// ...其他
lineStyle: {
// ...其他
shadowColor: ballColor,
color: ballColor,
}
}
},
}
]
})
}
// 生气模式
function setAngry() {
// 通过js修改body的css变量
document.body.style.setProperty('--c-eyeSocket', 'rgb(255,187,255)');
document.body.style.setProperty('--c-eyeSocket-outer', 'rgb(238,85,135)');
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'rgb(255, 60, 86)');
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(208,14,74)');
ballColor = 'rgb(208,14,74)';
}
// 常态模式
function setNormal() {
document.body.style.setProperty('--c-eyeSocket', 'rgb(41, 104, 217)');
document.body.style.setProperty('--c-eyeSocket-outer', '#02ffff');
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'transparent');
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(35, 22, 140)');
ballColor = 'rgb(0,238,255)';
}
// 唤醒
function clickToWeakup() {
isSleep = false;
bigEye.className = 'eyeSocket';
setAngry(); // 设置为生气模式
clearInterval(rotTimer);
rotTimer = setInterval(() => {
getEyeballChart()
ballSize <= 50 && (ballSize += 1);
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.5);
}, 10);
}
// 点击
bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

大眼生气长这样:


angry.gif


更生气

不知道在座(站?蹲擦?)各位是如何看待,但是在下看来,大眼这样好像还不够生气。


没错还不够生气,如何让大眼起来更生气呢,生气到发火如何?


嗦干酒干!


在下这里采用的是svg滤镜的方法,svg滤镜的属性和使用方法非常繁多,在下使用得也不是很娴熟,本文中在下就不赘述了,网上冲浪有许多技术大牛讲的非常好,希望各位勉励自己。emmmm......然后来教会在下,记得给在下留言文章地址


在下使用的是feTurbulence来形成噪声,然后用feDisplacementMap替换来给大眼添加粒子效果,因为feDisplacementMap会混合掉元素,所以在下需要给大眼新增一个大眼替身来代替大眼被融合。


创建大眼替身


<div class="filter"> // 添加滤镜的元素
<div class="eyeSocket" id='eyeFilter'> // 大眼替身
</div>
</div>

.filter {
width: 100%;
height: 100%;
}
.eyeSocket,
.filter .eyeSocket { /* 给替身加上相同的样式 */
/* ...原属性 */
}

image.png


融合


<div class="filter">
<div class="eyeSocket" id='eyeFilter'>
</div>
</div>
<!-- Svg滤镜 -->
<svg width="0">
<filter id='filter'>
<feTurbulence baseFrequency="1">
<animate id="animate1" attributeName="baseFrequency" dur="1s" from="0.5" to="0.55" begin="0s;animate1.end">
</animate>
<animate id="animate2" attributeName="baseFrequency" dur="1s" from="0.55" to="0.5" begin="animate2.end">
</animate>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" scale="50" xChannelSelector="R" yChannelSelector="B" />
</filter>
</svg>

.filter {
width: 100%;
height: 100%;
filter: url('#filter'); /* 开启滤镜 */
}

copy.gif


芜湖~果然献祭了一只“大眼”出来的效果看起来确实还不错哈?确实看起来酷炫多了,不愧是**“献祭”**啊!


真眼出现


既然粒子效果已经产生,咱们的真实大眼也就不需要躲躲藏藏了,该站出来获取这粒子“光环”了!


大眼:哈!


fire.gif


额......


其实......


也挺好看的嘛,不是吗?毕竟不是献祭的真正的大眼,毕竟是个替身,效果没有本体好也是很正常的对吧。



本质上是因为feDisplacementMap设置了scale属性的原因。


feDisplacementMap其实就是一个位置替换滤镜,通过就是改变元素和图形的像素位置的进行重新映射,然后替换一个新的位置,形成一个新的图形。


scale就是替换公式计算后偏移值相乘的比例,影响着图形的偏移量和呈现的效果。



但是话虽如此,咱这个光环不能真的就这么戴着呀,咱们还需要对光环的位置进行一些微调。


.filter .eyeSocket {
left: calc(50% - 92px);
top: calc(50% - 92px);
}

goodfire.gif


看看,看看!这不就顺眼多了吗,献祭了替身,所以尺寸都是非常契合的,而且共用了样式,所以当大眼生气的时候,光环也会跟着生气。


这下光环也有了,看起来的确比之前更生气了。


但是我们还需要对大眼做一些细微的调整,因为大眼在常规状态下并不需要这个光环,睡着的时候光环在旁边“滋啦滋啦”不吵的慌么,所以我们还需要把常态下的大眼光环给消除掉。


在下采用的是不透明度opacity来控制,当大眼处于生气状态时,光环为不透明;处于常规状态时光环透明不可见。


.filter .eyeSocket {
opacity: 0; // 默认状态下不透明度为0
left: calc(50% - 92px);
top: calc(50% - 92px);
transition: all 0.5s ease-in-out; // 添加过渡效果,值得注意的是不能丢了原本的过渡效果,所以这里使用all
}

// ...其他代码
let eyeFilter = document.getElementById('eyeFilter'); // 获取元素
// 唤醒
function clickToWeakup() {
eyeFilter.style.opacity = '1'; // 不透明度设为1
// ...其他
}
deathEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

这样设置完,一个更生气的大眼就这样出现了:


moreAngry.gif


更更生气


不知看到这样发火的大眼,各位是不是已经满足于此。


但是在下认为不,在下觉得一个真正足够生气的大眼,不只局限于自己生气,还需要找人发泄!!


所以在下还给大眼添加了一些大眼找人的动效(当然是找不到的,它这么笨)。


其实就是让大眼左右旋转,通过css转换来实现。


<div class="eyeSocket eyeSocketSleeping" id='bigEye'>
<div id="eyeball"></div>
</div>
<div class="filter">
<div class="eyeSocket" id='eyeFilter'>
</div>
</div>
<!-- Svg滤镜 -->
<svg width="0">
...
</svg>

/* ...其他样式 */
body {
/* ...其他属性 */
perspective: 1000px;
}
.eyeSocketLooking {
animation: lookAround 2.5s; // 添加动画,只播放一次
}
/* 环视动画 */
@keyframes lookAround {
0% {
transform: translateX(0) rotateY(0);
}

10% {
transform: translateX(0) rotateY(0);
}

40% {
transform: translateX(-70px) rotateY(-30deg);
}

80% {
transform: translateX(70px) rotateY(30deg);
}

100% {
transform: translateX(0) rotateY(0);
}
}

// ...其他代码
let bigEye = document.getElementById('bigEye'); // 获取元素
let eyeFilter = document.getElementById('eyeFilter');

// 唤醒
function clickToWeakup() {
// ...其他代码
eyeFilter.className = bigEye.className = 'eyeSocket eyeSocketLooking'; // 同时给大眼和光环添加环视动画
}

bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

看看大眼在找什么?


lookaround.gif



向左看时,Y轴偏移量为-70px,同时按Y轴旋转-30°


向右看时,Y轴偏移量为70px,同时Y轴旋转30°



✨ 生命仪式:自我调整状态


这个状态非常好理解,大眼虽然有起床气,但是也仅限于起床对吧,总不能一直让它生气,气坏了咋办,带着情绪工作,效果也不好不是吗。


所以我们还需要给它一点时间,让它自我调整一下,恢复成正常状态。


这个自我调整状态就是一个从生气状态变回常态的过程,在这个过程中,大眼需要将生气状态的红色系切换为常态的蓝色系,同时红眼也会慢慢褪去恢复正常。


其实这个自我调整状态还是属于唤醒状态中,只是需要放在起床气状态之后。


这里在下采纳了上文中有位同学给的建议,监听动画结束事件webkitAnimationEnd,然后将自我调整放在动画结束以后。


同时这里也有两个步骤:



  1. 退出起床气状态

  2. 变回常态


为了保证两个步骤的先后顺序,可以使用Promise来实现。不懂Promise的同学可以先去学习一下,在下也讲不清楚哈哈哈哈。


// ...其他代码
bigEye.addEventListener('webkitAnimationEnd', () => { // 监听动画结束事件
new Promise(res => {
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart(); // 更新视图
ballSize > 0 && (ballSize -= 0.5); // 眼球尺寸减小
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
if (ballSize === 0) { // 当眼球尺寸为0时,将Promise标记为resolved,然后执行后面的代码
clearInterval(rotTimer);
res();
}
}, 10);
}).then(() => {
eyeFilter.style.opacity = '0'; // 清除光环
eyeFilter.className = bigEye.className = 'eyeSocket'; // 清除环视动画
setNormal(); // 设置常态样式
rotTimer = setInterval(() => {
getEyeballChart();
ballSize <= 12 && (ballSize += 0.1); // 眼球尺寸缓慢增加
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
}, 10);
})
})

添加了这样一个监听事件后,咱们的大眼就已经具备了自我调整的能力了:


back.gif


✨ 生命仪式:工作状态


接下来就到了大眼重中之重的环节,也就是大眼的工作状态


在下给大眼的工作非常简单,就是单纯的盯住在下的鼠标,如果各位想给各自的大眼一些其他的功能,可以自己发挥。


盯住鼠标,不只是说说而已,那么怎么样才能让大眼表现出他已经盯住了呢?


在下的思路是:



  1. 以大眼的位置为原点建立一个直角坐标系

  2. 然后通过监听鼠标移动事件,获取鼠标所在位置,计算出鼠标处于大眼坐标系的坐标。

  3. 将整个视口背景以X轴和Y轴进行等分成无数个旋转角度,通过鼠标坐标的数值和正负来调整大眼眼框和眼睛的Y轴和Z轴旋转,从而达到盯住鼠标的目的。


好的,咱们理清思路,接下来就是付诸于行动。


// ...其他代码
// 工作
function focusOnMouse(e) {
{
// 视口尺寸,获取到整个视口的大小
let clientWidth = document.body.clientWidth;
let clientHeight = document.body.clientHeight;
// 原点,即bigEye中心位置,页面中心
let origin = [clientWidth / 2, clientHeight / 2];
// 鼠标坐标
let mouseCoords = [e.clientX - origin[0], origin[1] - e.clientY];
// 旋转角度
let eyeXDeg = mouseCoords[1] / clientHeight * 80; // 这里的80代表的是最上下边缘大眼X轴旋转角度
let eyeYDeg = mouseCoords[0] / clientWidth * 60;
bigEye.style.transform = `rotateY(${eyeYDeg}deg) rotateX(${eyeXDeg}deg)`;
eyeball.style.transform = `translate(${eyeYDeg / 1.5}px, ${-eyeXDeg / 1.5}px)`;
}
}


注意: 如果觉得旋转角度不够大,可以调整代码中的8060,最大可以到180,也就是完全朝向,但是由于大眼终归是一个平面生物,如果旋转度数过大,就很容易穿帮,如果旋转角度为180,大眼就会在某个方向完全消失看不见(因为大眼没有厚度,这个也许是可以优化的点),所以个人喜好调整吧。



咱们来看看大眼工作时的飒爽英姿:


watching.gif


✨ 生命仪式:懒惰状态


顾名思义,懒惰状态就是......懒惰状态。


在下给大眼设计的懒惰状态就是当在下的鼠标超过30秒没有移动时,大眼就会进入休眠状态


所以生命仪式的最后收尾其实非常的轻松,没有大量的代码,只需要添加一个定时器,然后修改休眠状态的代码,将大眼的所有参数初始化即可。


// ...其他代码
let sleepTimer; // 休眠定时器

// 休眠
function toSleep() {
// ...其他操作
document.body.removeEventListener('mousemove', focusOnMouse); // 移除鼠标移动事件
bigEye.style.transform = `rotateY(0deg) rotateX(0deg)`; // 大眼归位
eyeball.style.transform = `translate(0px, 0px)`; // 眼睛归位
}
// 工作
function focusOnMouse(e) {
// ...其他操作
// 设置休眠
if (sleepTimer) clearTimeout(sleepTimer); // 如果休眠定时器已经被设置,则清除休眠定时器
sleepTimer = setTimeout(() => { // 重新计时
toSleep();
}, 30000);
}

感谢上次掘金官方的提醒,在下把线上代码贴在这,在下文笔较差,看不下去的同学可以直接过来玩一玩,感兴趣再去创建自己的大眼。(没有点运行的不要来问我为什么出不来!!!)



如果自己在码上掘金动手的同学记得不要忘记添加echarts资源


image.png


💐 结语


好家伙,原来再写一遍大眼会这么累,这次是真真正正的“玩”了一天,有功夫的各位同僚也可以去玩一玩,于在下的基础上进行完善,创造出属于各位自己的大眼。当然如果有一些比较好玩的动效也可以留言告知在下,当下次混工作量时在下可以有东西写。


就这样!


image.png


作者:Urias
来源:juejin.cn/post/7132409301380890660
收起阅读 »

IDEA用上这十大插件绝对舒服

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。 1、TabNine TabNine 是一个 IntelliJ IDEA 插...
继续阅读 »

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。


1、TabNine


TabNine 是一个 IntelliJ IDEA 插件,可以为 Java 和 JavaScript 开发人员的代码提供 AI 建议。它分析来自数百万个开源项目的代码,并提供相关且准确的代码片段,以帮助开发人员更快、更准确地编写代码。

使用 TabNine 的众多优势包括:



  1. 有效的代码提示。

  2. 支持大量编程语言。

  3. 为主流编辑器和IDE提供帮助。

  4. 使用机器学习,记住你经常写的代码,并提供极其详细的提示。


地址:plugins.jetbrains.com/plugin/1279…



2、RestfulToolkit


RestfulToolkit 提供了与 RESTful API 交互的有用工具。开发人员可以使用此插件直接从 IDE 轻松测试、调试和管理 RESTful API 请求,从而提高他们的整体效率和生产力。


该插件与 HTTP Client、REST Assured 等流行工具集成,使其成为 RESTful API 开发的完整解决方案。


地址:plugins.jetbrains.com/plugin/1029…


3、MyBatisCodeHelperPro


MyBatisCodeHelperPro 在使用 MyBatis 框架时提高了开发人员的工作效率。它包括代码生成和实时模板,使编写和管理 MyBatis 代码更加容易,节省时间和精力。



此外,该插件支持数据库架构同步和 SQL 文件生成,提高开发效率。



地址:plugins.jetbrains.com/plugin/9837…
dehelperpro


4、CodeGlance


CodeGlance 为开发人员提供了代码右侧添加了简明概览,使他们更容易浏览和理解代码。

地址:plugins.jetbrains.com/plugin/7275…



可以看到在上图右侧区域有一个代码概览区域,并且可以上下滑动。


5、GenerateAllSetter


GenerateAllSetter 有助于为类中的所有属性生成 setter 方法。这可以在编写代码时节省时间和精力,同时也降低了出错的可能性。



地址:plugins.jetbrains.com/plugin/9360…



6、Lombok


Lombok:一个自动生成样板代码的 Java 库。



Project Lombok 是一个 java 库,可自动插入您的编辑器和构建工具,为您的 java 增添趣味。永远不要再写另一个 getter 或 equals 方法,通过一个注解,您的类就有一个功能齐全的构建器,自动化您的日志变量,等等。



地址:projectlombok.org/

需要注意的就是在使用了在 IDEA 中使用 Lombok 插件记得启用 Enable annotation processing


7、Rainbow Brackets


该插件为代码的方括号和圆括号着色,从而更容易区分不同级别的代码块。


地址:plugins.jetbrains.com/plugin/1008…


可以看到添加彩色方括号后,代码可读性有所提高。


8、GitToolBox


它包括许多额外的功能和快捷方式,使开发人员更容易使用 Git。使用 GitToolBox 的众多优点包括:



  1. GitToolBox 在 IntelliJ IDEA 上下文菜单中添加了几个快速操作,允许您在不离开 IDE 的情况下执行常见的 Git 任务。

  2. Git 控制台:该插件向 IntelliJ IDEA 添加了一个 Git 控制台,允许您在 IDE 中使用 Git。

  3. GitToolBox包含了几个解决合并冲突的工具,可以更容易地解决冲突并保持你的代码库是最新的。

  4. Git stash management:该插件添加了几个用于管理Git stashes的工具,使保存和重新应用代码更改变得更加容易。


地址:plugins.jetbrains.com/plugin/7499…


9、Maven Helper


Maven Helper 提供了一种更方便的方式来处理 Maven 项目。


Maven Helper 是一个帮助开发人员完成 Maven 构建过程的工具。该插件包括用于管理依赖项、插件和配置文件的功能,例如查看、分析和解决冲突以及运行和调试特定 Maven 目标的能力。


这可以通过减少花在手动配置和故障排除任务上的时间,使开发人员有时间进行编码和创新,从而提高生产力。


地址:plugins.jetbrains.com/plugin/7179…


10、Sonarlint


Sonarlint 是一个代码质量检测工具,集成了 SonarQube 以动态检测和修复代码质量问题。


Sonarlint 提供实时反馈和建议,帮助开发人员提高代码质量。它集成了 SonarQube 代码分析平台,允许开发人员直接在他们的 IDE 中查看代码质量问题。


这通过在潜在问题到达构建和测试阶段之前检测它们来节省时间并提高效率。 Sonarlint 还可以帮助开发人员遵守最佳实践和编码标准,从而生成更易于维护和更健壮的代码。


地址:plugins.jetbrains.com/plugin/7973…



作者:wayn
链接:https://juejin.cn/post/7225989135999434789
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

初学后端,如何做好表结构设计?

前言 最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计? 大家关心的问题阳哥必须整理出来,希望对大家有帮助。 先说结论 这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从...
继续阅读 »

前言


最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计?


大家关心的问题阳哥必须整理出来,希望对大家有帮助。


先说结论


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度。


4个方面


设计数据库表结构需要考虑到以下4个方面:




  1. 数据库范式:通常情况下,我们希望表的数据符合某种范式,这可以保证数据的完整性和一致性。例如,第一范式要求表的每个属性都是原子性的,第二范式要求每个非主键属性完全依赖于主键,第三范式要求每个非主键属性不依赖于其他非主键属性。




  2. 实体关系模型(ER模型):我们需要先根据实际情况画出实体关系模型,然后再将其转化为数据库表结构。实体关系模型通常包括实体、属性、关系等要素,我们需要将它们转化为表的形式。




  3. 数据库性能:我们需要考虑到数据库的性能问题,包括表的大小、索引的使用、查询语句的优化等。




  4. 数据库安全:我们需要考虑到数据库的安全问题,包括表的权限、用户角色的设置等。




设计原则


在设计数据库表结构时,可以参考以下几个优雅的设计原则:




  1. 简单明了:表结构应该简单明了,避免过度复杂化。




  2. 一致性:表结构应该保持一致性,例如命名规范、数据类型等。




  3. 规范化:尽可能将表规范化,避免数据冗余和不一致性。




  4. 性能:表结构应该考虑到性能问题,例如使用适当的索引、避免全表扫描等。




  5. 安全:表结构应该考虑到安全问题,例如合理设置权限、避免SQL注入等。




  6. 扩展性:表结构应该具有一定的扩展性,例如预留字段、可扩展的关系等。




最后,需要提醒的是,优雅的数据库表结构需要在实践中不断迭代和优化,不断满足实际需求和新的挑战。



下面举个示例让大家更好的理解如何设计表结构,如何引入内存,有哪些优化思路:



问题描述



如上图所示,红框中的视频筛选标签,应该怎么设计数据库表结构?除了前台筛选,还想支持在管理后台灵活配置这些筛选标签。


这是一个很好的应用场景,大家可以先自己想一下。不要着急看我的方案。


需求分析



  1. 可以根据红框的标签筛选视频

  2. 其中综合标签比较特殊,和类型、地区、年份、演员等不一样



  • 综合是根据业务逻辑取值,并不需要入库

  • 类型、地区、年份、演员等需要入库



  1. 设计表结构时要考虑到:



  • 方便获取标签信息,方便把标签信息缓存处理

  • 方便根据标签筛选视频,方便我们写后续的业务逻辑


设计思路



  1. 综合标签可以写到配置文件中(或者写在前端),这些信息不需要灵活配置,所以不需要保存到数据库中

  2. 类型、地区、年份、演员都设计单独的表

  3. 视频表中设计标签表的外键,方便视频列表筛选取值

  4. 标签信息写入缓存,提高接口响应速度

  5. 类型、地区、年份、演员表也要支持对数据排序,方便后期管理维护


表结构设计


视频表































字段注释
id视频主键id
type_id类型表外键id
area_id地区表外键id
year_id年份外键id
actor_id演员外键id

其他和视频直接相关的字段(比如名称)我就省略不写了


类型表























字段注释
id类型主键id
name类型名称
sort排序字段

地区表























字段注释
id类型主键id
name类型名称
sort排序字段

年份表























字段注释
id类型主键id
name类型名称
sort排序字段

原以为年份字段不需要排序,要么是年份正序排列,要么是年份倒序排列,所以不需要sort字段。


仔细看了看需求,还有“10年代”还是需要灵活配置的呀~


演员表























字段注释
id类型主键id
name类型名称
sort排序字段

表结构设计完了,别忘了缓存


缓存策略


首先这些不会频繁更新的筛选条件建议使用缓存:




  1. 比较常用的就是redis缓存

  2. 再进阶一点,如果你使用docker,可以把这些配置信息写入docker容器所在物理机的内存中,而不用请求其他节点的redis,进一步降低网络传输带来的耗时损耗

  3. 筛选条件这类配置信息,客户端和服务端可以约定一个更新缓存的机制,客户端直接缓存配置信息,进一步提高性能


列表数据自动缓存


目前很多框架都是支持自动缓存处理的,比如goframe和go-zero


goframe


可以使用ORM链式操作-查询缓存


示例代码:


package main

import (
"time"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var (
db = g.DB()
ctx = gctx.New()
)

// 开启调试模式,以便于记录所有执行的SQL
db.SetDebug(true)

// 写入测试数据
_, err := g.Model("user").Ctx(ctx).Data(g.Map{
"name": "xxx",
"site": "https://xxx.org",
}).Insert()

// 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

// 执行更新操作,并清理指定名称的查询缓存
_, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: -1,
Name: "vip-user",
Force: false,
}).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update()
if err != nil {
g.Log().Fatal(ctx, err)
}

// 再次执行查询,启用查询缓存特性
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}
复制代码

go-zero


DB缓存机制


go-zero缓存设计之持久层缓存


官方都做了详细的介绍,不作为本文的重点。


讨论


我的方案也在我的技术交流群里引起了大家的讨论,也和大家分享一下:


Q1 冗余设计和一致性问题



提问: 一个表里做了这么多外键,如果我要查各自的名称,势必要关联4张表,对于这种存在多外键关联的这种表,要不要做冗余呢(直接在主表里冗余各自的名称字段)?要是保证一致性的话,就势必会影响性能,如果做冗余的话,又无法保证一致性



回答:


你看文章的上下文应该知道,文章想解决的是视频列表筛选问题。


你提到的这个场景是在视频详情信息中,如果要展示这些外键的名称怎么设计更好。


我的建议是这样的:



  1. 根据需求可以做适当冗余,比如你的主表信息量不大,配置信息修改后同步修改冗余字段的成本并不高。

  2. 或者像我文章中写的不做冗余设计,但是会把外键信息缓存,业务查询从缓存中取值。

  3. 或者将视频详情的查询结果整体进行缓存


还是看具体需求,如果这些筛选信息不变化或者不需要手工管理,甚至不需要设计表,直接写死在代码的配置文件中也可以。进一步降低DB压力,提高性能。


Q2 why设计外键?



提问:为什么要设计外键关联?直接写到视频表中不就行了?这么设计的意义在哪里?



回答:



  1. 关键问题是想解决管理后台灵活配置

  2. 如果没有这个需求,我们可以直接把筛选条件以配置文件的方式写死在程序中,降低复杂度。

  3. 站在我的角度:这个功能的筛选条件变化并不会很大,所以很懂你的意思。也建议像我2.中的方案去做,去和产品经理拉扯喽~


总结


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度



本文抛砖引玉,欢迎大家留言交流。


作者:王中阳Go
链接:https://juejin.cn/post/7212828749128876092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么要使用Docker和容器

1981年出版的一本书《Nailing Jelly to a Tree》描述了软件是“模糊的,难以把握的”。这在1981年是真实的,四十年后也同样如此。无论是你购买的应用程序还是自己构建的应用程序,软件部署、管理和运行仍然很困难。 Docker容器提供了一种把...
继续阅读 »


1981年出版的一本书《Nailing Jelly to a Tree》描述了软件是“模糊的,难以把握的”。这在1981年是真实的,四十年后也同样如此。无论是你购买的应用程序还是自己构建的应用程序,软件部署、管理和运行仍然很困难。


Docker容器提供了一种把握软件的方法。你可以使用Docker将应用程序封装起来,以便处理其部署和运行时的问题,如:如何在网络上公开它,如何管理其对存储、内存和I/O的使用,如何控制访问权限等等。这些问题都在应用程序本身之外处理,并以一种在所有“容器化”应用程序中保持一致的方式处理。你可以在任何安装了Docker运行时的兼容操作系统主机(Linux或Windows)上运行Docker容器。


除了这种方便的封装、隔离、可移植性和控制之外,Docker还提供了许多其他好处。Docker容器很小(几兆字节),启动速度很快,具有自己内置的版本控制和组件重用机制,可以通过公共Docker Hub或私有仓库轻松共享。


Docker容器也是不可变的,这既有安全性又有操作上的好处。对容器的任何更改都必须部署为一个全新的、版本不同的容器。


在本文中,我将探讨Docker容器如何使构建和部署软件更容易,容器解决的问题,何时容器才是正确的解决方案,何时不是。




在Docker容器之前


多年来,企业软件通常是部署在“裸机”上(即安装在具有对底层硬件完全控制的操作系统上)或虚拟机上(即安装在与其他“客户”操作系统共享底层硬件的操作系统上)。自然地,安装在裸机上使得软件难以移动和更新,这两个限制使得IT难以敏捷地响应业务需求的变化。


然后,虚拟化出现了。虚拟化平台(也称为“虚拟机管理程序”)允许多个虚拟机共享单个物理系统,每个虚拟机以隔离的方式模拟整个系统的行为,包括其自己的操作系统、存储和I/O。IT现在可以更有效地响应业务需求的变化,因为虚拟机可以克隆、复制、迁移和启动或关闭以满足需求或节约资源。



虚拟机也有助于降低成本,因为更多的虚拟机可以合并到更少的物理机器上。运行旧应用程序的遗留系统可以转换为虚拟机,并进行物理退役以节省更多的资金。


但是虚拟机仍然存在一些问题。虚拟机很大(千兆字节),每个虚拟机都包含一个完整的操作系统。只有很多虚拟化应用程序可以合并到单个系统上。分配虚拟机仍然需要相当长的时间。最后,虚拟机的可移植性有限。在某个点之后,虚拟机无法提供快速移动的企业所需的速度、敏捷性和节省成本。




Docker容器的好处


容器的工作方式有点像虚拟机,但更加具体和细粒度。它们将单个应用程序及其依赖项(应用程序运行所需的所有外部软件库)与底层操作系统和其他容器隔离开来。


所有容器化的应用程序共享一个公共操作系统(Linux或Windows),但它们彼此之间与整个系统隔离开来。操作系统提供所需的隔离机制,使这种隔离发生。Docker将这些机制包装在一个方便的接口。


Docker容器的好处在许多地方体现。以下是一些Docker和容器的主要优势:


1、Docker 可以更有效地利用系统资源


容器化应用程序的实例使用的内存比虚拟机少得多,它们启动和停止更快,并且可以在它们的主机硬件上更密集地打包。所有这些都意味着 IT 开支更少。


成本节省将根据所使用的应用程序和它们可能的资源密集程度而异,但容器无疑比虚拟机更有效率。还可以节省软件许可证的成本,因为您需要更少的操作系统实例来运行相同的工作负载。


2、Docker 可以加快软件交付周期


企业软件必须快速响应各种不断变化的情况。这意味着需要轻松扩展以满足需求,并且需要轻松更新以添加业务所需的新功能。


Docker容器可以轻松地将具有新业务功能的新版软件快速投入生产,并在需要时快速回滚到以前的版本。它们还可以更轻松地实施蓝/绿部署等策略。


3、Docker 可以实现应用程序的可移植性


在防火墙后面运行企业应用程序很重要,为了保持紧密和安全; 或者在公共云中,以便于公众访问和高弹性的资源。因为Docker容器封装了应用程序运行所需的所有内容(并且只包含那些内容),所以它们允许应用程序在环境之间轻松穿梭。任何安装了Docker运行时的主机,无论是开发人员的笔记本电脑还是公共云实例,都可以运行Docker容器。


4、Docker 在微服务架构中表现出色


Docker 容器是轻量级、可移植和自包含的,使得更容易按照前瞻性的思路构建软件,这样您就不会试图用昨天的开发方法来解决明天的问题。


容器使得实现微服务等软件模式更加容易,其中应用程序由许多松散耦合的组件构成。通过将传统的“单块式”应用程序分解为单独的服务,微服务允许业务应用程序的不同部分可以分别进行扩展、修改和维护——如果符合业务需求,可以由不同的团队在不同的时间表上进行。


容器不是实现微服务的必要条件,但它们非常适合微服务方法和敏捷开发流程。




容器并不是万能的


需要记住的是,与任何软件技术一样,容器并不是万能的。Docker 容器本身不能解决所有问题。


特别是以下几点:


1、Docker 无法解决软件的安全问题


容器中的软件默认情况下可能比在裸机上运行的软件更安全,但这就像说锁着门的房子比开着门的房子更安全一样。这并没有说明社区的状况、诱人偷盗的贵重物品的可见存在、居住在那里的人的日常生活等等。容器可以为应用程序添加一层安全性,但只能作为在上下文中保护应用程序的一般计划的一部分。


2、Docker 不能神奇地将应用程序变成微服务


如果将现有的应用程序容器化,可以减少其资源消耗并使其更容易部署。但它并不会自动更改应用程序的设计或其与其他应用程序的交互方式。这些好处只能通过开发人员的时间和努力来实现,而不仅仅是将所有内容移动到容器中的命令。


如果将传统的单块式或面向服务的应用程序放入容器中,最终得到的是一个老旧的应用程序在容器中运行。这对你的工作没有任何帮助。


容器本身没有组合微服务式应用程序的机制。需要更高级别的编排来实现这一点。Kubernetes 是这种编排系统的最常见示例。Docker swarm 模式也可以用于管理多个 Docker 主机上的许多 Docker 容器。


3、Docker 不是虚拟机的替代品


容器的一个误解是它们使虚拟机过时了。许多以前在虚拟机中运行的应用程序可以移动到容器中,但这并不意味着所有应用程序都可以或应该这样做。例如,如果你在一个有严格监管要求的行业中,可能无法将容器替换为虚拟机,因为虚拟机提供的隔离性比容器更强。


作者:Squids数据库云服务提供商
链接:https://juejin.cn/post/7226153074078416933
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一位 98 年程序员离职后

今天不写技术文了,写点轻松的。 我自己都没讨好自己,何苦要来讨好你呢? 开篇 本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、...
继续阅读 »

今天不写技术文了,写点轻松的。


img-16593196478205dc10966293f6e5e3f0be6d9ff93705f.jpg



我自己都没讨好自己,何苦要来讨好你呢?



开篇


本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、跑路的,这些消息可以说不绝于耳。但本人最终还是选择离职休整下。


得益于父辈们的努力,在房价还不到当前房价一半的时候出手了,让弱小的我在这座城市有瓦遮头,不必为三餐奔波。既没有房贷,也没有车贷,一人吃饱,全家不饿的我没有了这方面的顾虑,也有了底气做出这个选择。


img-165932129544253f44bc0a87ca13a950b2ff14f24ccd2.jpg



希望明天还能看到那朵软萌萌的云,因为它好像你



说下离职后我都干了些什么吧,给各位列一下,说不定能找到知音。



  • 刷力扣

  • 整理下之前的东西

  • 健身

  • 写作

  • 品鉴周董的新歌

  • 看综艺

  • ...


上面的这些东西不分先后,一直都在做。


刷力扣


刷力扣其实很早就开始了,每天登录力扣有一个积分,完成每日一题有 10 积分,到现在坚持了有差不多两年了,战绩如下:


QQ截图20220801104732.png



大部分是中等



光看题目量不算少,但其实大部分困难和部分中等都是 cv 之后通过的,不装了,摊牌了。刷题过程也不艰难,就一句话,简单题重拳出击,困难题唯唯诺诺。有些人会觉得算法没有必要,因为平时的工作就用不到。但我觉得算法最重要的是锻炼人的思维,思维很重要,它能够指导一个人思考问题的轨迹和方向。虽然有时刷题时会感觉自己活着就是凑数的,没必要灰心,真的,因为你的判断是对的。


整理下之前的东西


之前在工作时也积累一些东西,但没有做整理,所以趁着这段时间整理下,看下能不能发现一些新东西。个人觉得一直处于一种忙碌的状态并不一定是好事,这有点像吃东西时狼吞虎咽,容易噎着。


健身


这件事是坚持的最久的一件事,从高一一直到现在。高一时上映的速 7,被强森和郭达在办公室的那段打戏吸引,当时觉得男人就应该这样。于是从最简单的俯卧撑、引体开始,一点点的朝自己的目标努力。但这过程走了很多弯路,比如训练的方式不对,太急于求成、吃的没跟上、休息没跟上,导致很长一段时间都处于瓶颈期,一直在原地踏步。这种不上不下的感觉真的不好受,也想过放弃,但已经戒不掉了。图就不发了,担心被喷。胸、背、腿、腰、手、腹肌都有练,腹肌不多不少,正好 6 块。至于身材,我个人觉得还行,至少不差,也被人说过身材好,同性异性都有。


QQ截图20220801201459.png



被同性说



写作


这个貌似是在去年开始的,但中断了挺长一段时间,就想着在空窗期重新捡起来。至于最终能开出什么样的花,也没想过,就觉得写比不写强。读者感兴趣的可以看我之前写过的文章,主页:
鄙人的主页


img-1659357885349240b678eb24aca42039c30c16b002044.jpg



对待生活,不必说的太多,你同样可以给它一个惊喜



品鉴周董的新歌


本人虽说不是周董的粉丝,但以前总想着能在晴天里给千里之外的她带一壶爷爷泡的茶,面对面安静的坐着,她的笑容甜甜的,我也对着她傻笑。院子里是一片花海,散发着迷迭香。


她送我来到渡口,她的倒影在满是桃花的粉色海洋里若隐若现。船夫摇着桨,背对着我,哼着她唱过的红颜如霜突然来了句:这是最伟大的作品。可谁让夜晚留不住错过的烟火,活该我到现在还在流浪。


看综艺


综艺平时也是我解压的一种方式,最近把跑男第十季追完了,几位 mc 都是各有特点。不过最喜欢的还是新加入的白鹿,人美,很搞笑,魔性的笑声让人很容易记住她。


magazine-unlock-01-2.3.2022080201-7432B64DE5C9B11.jpg



你问我:我对你有多重要,我回答:太阳你知道吧



总结


可能有人看了之后会觉得有点躺平的趋势,但其实并没有。本人还是很爱折腾的,也希望能多认识点圈子以外的人,多认识点有趣的人,多认识点志同道合的人。有些人会觉得程序员很闷,不爱说话,天天就对着电脑。可能有部分人是这样的,但我不是,因为我是一个不走寻常路的程序员,而且我深知只有跳出圈子,才能打破认知。by the way,本人对数字化转型行业挺感兴趣的,有读者从事或者了解的话,可以大胆私信我啊。


作者:对方正在输入
链接:https://juejin.cn/post/7127653600532103198
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

快到 35 的"大龄"程序员

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。 我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部...
继续阅读 »

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。


我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部,做前端开发,也是 IMWeb 团队的一员,先后做过腾讯课堂和企鹅辅导业务,2020年正式任命 leader,管理差不多10人左右的前端开发团队;2022年3月,因(cheng)为(xu)某(yuan)些(dou)原(zhi)因(dao),加入虾皮金融产品部,现负责消费贷业务+催收业务的前端开发和管理工作。


我的自我介绍完了,如果大家不想浪费更多时间深入了解我的话,知道以上信息已经足够了,为了大家的脑细胞着想,提供给大家 3 个不用思考的快捷选项:



  1. 对我不感兴趣,可以左上角关闭页面(我可以对天发誓,这绝对不是相亲贴);

  2. 觉得可以交个朋友,给自己保留一个未来有惊喜的可能,可以关注我的公众号或者加我微信;

  3. 还想听我唠唠嗑的,欢迎继续看下去呀,一定满足大家的好奇心。




感谢你能够继续看下去。我想了很久,怎么样才能不至于让我的自我介绍写成流水账,但是,当我想了更久的时间之后,我发现,我想把这份流水账写出来更难,因为,很多的经历我都不记得了,我只能把我的记忆片段写下来,拼凑出我的职业生涯。好记性不如烂笔头,我觉得本文我可以永远留存并持续迭代,直到我的职业生涯结束的时候,可以用来回顾我的人生,也不失一桩美事。我也推荐大家这样做。


我的前端之路的伊始


我的第一份进入企业的工作是在2011年,大三实习,在杭州阿里,阿里妈妈广告部门(部门全称已经不记得了),后台开发,你没有看错,是的,我是后台开发,那会儿我还不知道前端,大学课程也没有一门是教前端的。


我对于阿里的印象,绝不是现在的"味儿"。我对阿里最大的印象还停留在当初那个时代,有三:



  1. 江湖气派,店小二文化,随性,直来直往,互相接受度非常高,我是非常喜欢这个文化的,当时阿里实习是不能拥有花名的,这是我职业生涯最大的遗憾之一,我还很清楚记得,当时我曾经查过,好像还没有人取名曹操,不过也是我的异想天开,因为即使我转正,我也没有那个资格取这个花名。

  2. 开放,真得非常开放,当我在新人入职欢迎聚餐中,脱到只剩裤衩的时候,我相信我那会一定是完全理解了开放这个词了。虽然直到现在回忆起来,还会有点不适,但是,当经历了那次聚餐之后,隐隐中,我会潜意识地得觉得,好像自己没什么是不可以“坦诚相待”的。

  3. 倒立文化,换个角度思考,我自认为我完全做到了,当我换个角度思考我的职业的时候,我走上了前端之路。。。


虽然在我拿到转正 offer 的时候,还是毅然决然选择保研(其实是被父母逼的)并转前端,但是我还是觉得,我在阿里的大半年实习期间,是我整个开发生涯中成长最快的时期,在那里,我学到了太多太多,以至于到现在我的开发习惯还会保留当时的一些痕迹:




  • 当我碰到需要服务运维的场景,我一定是首选 bash 脚本,然后是 python,最后才是 js,基本不会是 js 的,因为没什么是前两者做不到的。定时任务,文件锁,离线计算,文本处理等等,到现在我还记忆犹新。




  • 记不清写了多少 Map Reduce 了,但是当时,我真得被 Hadoop 的设计原理给深深的吸引到了,原来,大数据处理&分析,分布式计算和分布式文件系统有这么多的挑战,但它的解决方案又是这么的精简,直到现在,我仍然坚信,任何架构设计,一定是精简的,当我跟别人讨论架构的时候,如果他讲不清楚,或者表达非常复杂的时候,我就知道,不是设计有问题,就是表达有问题,我们还有更优的方案。天地良心,当时实习的时候,我真的是非常认真的做后台开发的,当时我还啃下了那本大象书呢,现在想想也觉得不容易,当年我是真喜欢看书呀。




  • 架构设计非常“好玩” ,在当时,阿里内部有非常多的技术分享,我常常去听我自己喜欢的分享,让我的技术视野得到了非常大的增长,比如:



    • 中文的分词要比英文的分词要难很多,最终发现,自然语言处理不是我的菜;

    • 推荐系统的结果是竞速的,当时真的有想入职百度,去学习搜索引擎的冲动;

    • 秒杀的多重降级、动态降级,各种“砍砍砍”,非常有意思。


    在当时,我学到的一个最重要的知识是,任何架构设计都是因地制宜的,不能墨守成规




在实习转正答辩的时候,最后问我的未来规划的时候,我的回答更多是偏架构设计和 UI 相关,现在回想起来都会觉得搞笑,当时我一度以为是转正失败了,但是没想到阿里开放到这都给我发了 offer,真得很感激我的老领导,但也觉得很对不起他们,因为我真的不想淹没在数据的海洋里,我更喜欢开发一些"看得着,摸得到"的东西,我会觉得做这个更有意思,所以,我选择了前端。


一波三折的腾讯梦


先说说为什么想去腾讯吧,因为我是广东人,父母都在深圳,都希望我回深圳,当时深圳不用多说,大公司就腾讯了,所以,我在实习和毕业的选择上一直都非常明确,就是深圳腾讯,但是我自己都没想到我回深圳是这么的坎坷。


研一找实习的时候,我第一次面试腾讯挂了,当时是电话面试,我记得是早上,很突然接到了面试电话,然后突然开始面试,我完全没有准备,很自然地就挂了,跟我同一个项目的做 web 服务的同学拿到腾讯的实习 offer 了,当时心理还有点不平衡,但是后面我也很快拿到新的 offer 了。


插一段题外话,当时我跟另外两个同学一起跟着导师外包项目,项目也挺有意思的,因为我们是嵌入式方向的实验室,所以我们做的是一个实时监控系统,有个同学主要负责传感器和网络编程,另外一个同学主要负责 web 后台服务,我负责前端页面(extjs),我们的项目是给一家医院做冰柜的温度实时监控系统,在冰柜中放入温度传感器,然后不断把冰柜的温度数据通过各个局域网网络节点传输器一路传到中心服务器中,然后中心服务负责存储并分析数据,落库并返回数据到前端,展示实时监控页面并添加告警功能。整个系统非常有意思,通过这个项目,我深深地感受到物联网的魅力,软硬件结合的威力。这还只是单向的,如果可以做到双向,再加上智能化,那基本就可以取代人的工作了,实际上,现在很多的无人XXX系统,他们的本质都是这个,现在互联网环境这么差,哪天干不下去了,换个行业,做物联网+虚拟+AI,做现实虚拟,实业升级事业,也是大有可为的。


回归正题,在腾讯突然面挂之后,我就开始认真复习,专门找前端的实习工作,然后很快就找到了网易的教育部门的前端开发 offer,这段经历我印象最深刻的是当时那批前端的笔试当中,我是最高分的,面试也没怎么问就拿到 offer 了,果然有笔试就是好呀,妥妥我的强项。或者是因为我有这段经历,所以后面我才会被分配到腾讯做教育吧。。。


在网易,我做的是网易云课堂和网易公开课相关的前端工作,在网易的实习过程中,我的前端基础和实践不断加强,三剑客,前端组件库,前端基础库,模块化,构建,浏览器兼容处理等等,基础技术收获很多,但是大的方面上,没什么特别的收获,就像网易的公司文化一样,没什么特别的感受,至今都没留下什么。在网易,印象最深的两个点就是:



  • 除了游戏,万般皆下品,主要靠情怀。其实这点跟在腾讯做教育也差不多;

  • 网易的伙食真的是互联网第一,不存在之一。


研二找工作的时候,我研究了腾讯的校招路演,发现有以下问题:



  • 杭州算是最后一站那种,时间很晚,到我们这边黄花菜都凉了;

  • 杭州离上海很近,过来招聘的团队应该基本都是上海的;

  • 像我这样的杭州毕业生不去阿里想去腾讯的奇葩真得不多了。


因此,我决定跑去上海参加校园招聘。当年校招我只面了百度跟腾讯,当时校园招聘都是统一笔试,面试,我记得百度是去他们上海分公司内部面试的,面了 2 轮就到 hr 了,还能留下记忆的是当时 2 面面试官对我的阿里经历很感兴趣,问了非常多,我当时就懵了,你们不是招前端的么。


然后是腾讯的面试,在一家 5 星级酒店的房间面的,当时进去就问我,能不能接受 web 服务研发岗位,我当时第一反应就是,你有无搞错呀!?但是机敏如我,肯定是立刻回答可以接受的,虽然这是一个随时都可以被废弃的万金油 api 岗位,但是它胜在可上可下,呸,是可前可后,啊呸,是可前端可后台,必须难不倒我呀,然后就是很无聊的面试,问了一些简单的前端题,了解了一下实习项目,最后做了一道智力题就结束了,相比百度的面试,有点看不过去了。最后问了我填的志愿是深圳的岗位,问我服不服从调剂,我说只想看深圳岗位,让我一度以为我又挂了,不过最后还是顺利进到 hr 的房间。。。面试,随便瞎聊,最后确认我只想回深圳,并表示可以给我争取调剂。


在回杭州的火车上,我知道百度的 offer 基本稳了,不过是上海的,腾讯的 offer 还是内心忐忑,实在是腾讯的面试有点“敷衍”了,那会儿我都在思考怎么忽悠我爸妈先在上海工作2年再回深圳了。不过没过2天,就收到了腾讯的 offer,是深圳易迅的前端开发岗位,当时在上海招聘的 90% 都是易迅(腾讯收购)的招聘,也很感谢当时帮我调剂的面试官跟 hr 了。兴奋的我在跟百度 hr 电话的时候就直接拒掉了百度 offer,现在回想起来,还真有点轻率了。


很快,我就决定提前到腾讯实习,当我坐在回深圳的火车时,看到了一则新闻:腾讯决定出售整个 ECC 给京东置换京东股份,并和京东开启战略合作。我不太记得我回家那天是什么心情,我只记得我办理入职手续的时候,窗外的天空是没有太阳的。我甚至都没认识全我的团队,因为当时所有工作都暂停了,那会儿,不是开大会,就是漫长的等待,现在想想,还挺像现在经历这场寒冬的我们一样,迷茫,忐忑,甚至有点慌张。


我加入了应届生群,在联名信上“签名”,在论坛上堆楼,终于,高层听到了我们的声音,跟京东友好协商之后,给予了我们这届应届生自主选择权 —— 是去京东还是留在腾讯,待遇不变。毫不犹豫地,我选择了腾讯。


写到这里,我还是很感慨,我的腾讯梦还真是一波三折,除了幸运还是幸运,或许因为在这件事情上花光了我前半生积攒的运气,以至于直到到现在所有的年会我都是阳光普照,深圳车牌摇号还遥遥无期,但是,我的腾讯之路还是开启了。。。


我职业生涯中最大的幸运 —— IMWeb 团队


多动动脑子


刚转来 IMWeb 团队,我接到的第一个任务是做一个爬虫,要爬淘宝教育的课程和购课数据。这不是很简单吗,之前做过呀,殊不知噩梦即将开始...


不到半天我就写好了,包括邮件模板,也自测好了,正式启动,美滋滋去喝杯茶,回来就能交差了。当我摸鱼回来一看,咦,脚本停了,接口报错,被限频了。于是我进入了疯狂调试模式,添加重试逻辑,不断调整请求频率策略,最终祭出终极策略,3分钟请求1次,这下不会被限频了吧,在稳定跑了1个小时没问题之后,我安心的下班回家了。


第二天到公司,数据跑完了,完美。于是,我做了最后的数据校对和计算调整,然后调通自动发送邮件的逻辑,再次执行。当我美滋滋地再次摸鱼回来,发现脚本又停了,这次是新的错误,没有错误信息,就是 5xx,黑人问号啊,于是各种调试各种排查,最终得出一个结论,ip 被拉进黑名单了。


好家伙,算你狠。于是我上网各种研究代理,不管免费付费,能用就是好代理,再次调整策略,申请十多个账号轮流爬,光荣牺牲了一批又一批的 ip 之后,我还是败下阵来。那个时候,我觉得我的人生都是黑暗的,我的面前立着一座大山,我怎么样都翻不过去。


当老大咨询进度的时候,我并没有得到任何安慰和建议,而是一句“多动动脑子”。


我已经忘记当时的我是什么心情,被打击成什么样了。也已经忘记了一周后是怎样完成任务的。我只记得,之后我只花了半天时间就爬了网易云课堂和慕课网的数据,他们就是毫不设防的裸......奔。


任性如我


对于我们程序员来说,碰到的最棘手的问题中,无法复现的问题肯定名列前茅。


有一次需求发布,现网验证的时候发现了一个问题,在本地和 test 环境都复现不了,live 打断点也复现不了,真是绝了,打断点没问题,不打断点有问题,我大概能猜到问题,但是需要打印一些日志来定位最终问题,可是只能在 live 才有效,先不说 live 构建会自动删掉 console.log 语句,执行一次 live 部署非常慢,如果要折腾几次来调试,那半天都解决不了问题了。


急性子的我肯定受不了这种折磨,所以我选择了直接登录现网服务器改代码调试。先把压缩文件 down 下来,本地格式化,找到对应位置添加 console.log,然后传回服务器覆盖文件,禁用 cdn 资源,直接在现网复现排查问题。几分钟不到就确定问题,然后修改代码重新部署一次过完成最终需求发布。整个过程行云流水,但是我内心慌得一比,这要是出问题被发现,那后果不敢想象。


还有好几次的 Node 服务问题,我也是直接现网调试,其实 Node 服务才是最适合这么做的场景,但是,我并不是推荐大家这样做。再到后面,我行我素的我越来越能够理解流程机制的用意和作用,现在踏上管理岗位,我更希望小伙伴们是严格遵照流程规范来工作,但我的内心深处,还是住着一个不羁的我


“万恶的” owner


“清龙,这个需求就由你来当 owner 吧。”


“owner?要做什么?”


“就是这个需求的负责人,看看需求进度有没有问题,发布别延期就行”


“好”


【需求开发中...】


“清龙,现在需求进度怎样?有没有风险?”


“我这边没问题,我问一下后台同学看看”


“你可以每天下班前收集一下大家的进展,然后在群里同步哈”


“好”


【需求测试中...】


“清龙,需求测得怎么样啦?”


“......(这不应该问测试吗)应该问题不大,我这边的 bug 都处理完了,我找测试跟进一下测试进度哈”


“可以每天下班前找测试对齐一下测试的整体进度,让测试在群里同步哈”


“好”


【需求发布中...】


“清龙,需求发得怎么样啦?”


“后台发完了,前端正在发,问题不大”


“牛呀,一定要做好现网验证,发布完成记得要在群里周知哈”


“好~”


自从团队推行 owner 机制,工作量是噌噌噌地往上涨,但是工作能力也有很大的提升。


怎么说呢,这是毁誉参半的机制,重点在于每个人怎么看待这个事情,它可以是卷、分担压力的借口;它也可以是培养新人,锻炼项目管理能力,提升沟通协调能力的最佳实践机会。


我眼中的 IMWeb 团队


它是综合的。我们团队涉猎的领域非常广,移动端,pc 端,后台均有涉猎,正因如此,我们有非常好的土壤茁壮成长,尝试各种新技术。在很早的时候,我就在数据接口低代码平台落地 GraphQL,实现了基于mysql 的 GraphQL 的 node 版本,不说业界,在公司内肯定是领先的。在公司成长的过程中,我们团队也在成长,在前端工程化上也有很多的实践和成果。后面腾讯搞 Oteam,我们团队也多有贡献。


它是着眼于业务的。 我们团队推崇做产品的主人翁,坚持不懈地以技术手段助力业务发展。我们做的所有项目都是为了业务服务,为了整个团队服务。我们团队是专业的,没有钻技术的牛角尖,更多地是扎根于业务,一切以实际出发,更多以落地与实践为主。但我们团队的业务并不是很出彩,属于半公益的教育,至今我仍然唏嘘不已,只能感叹时运不济,现在回过头来细品,再厉害的技术,没有好的业务相辅相成,也是无法一直走下去,业务是王道啊。


它是被信任与敢于信任的。作为前端团队,能够有那么大的空间来施展身手,这足以说明我们团队是受到领导的充分信任的,我们团队也非常努力来对得起这份信任。而团队也非常信任团队里的每一个人,会给予很多的试错机会和时间,就看我们有没有耐心,主动与坚持了。


在一个已经建立了一定文化的团队是幸福的,它是需要细品的,但很多人都不愿意去感受。这两年,我过得很难受,不知变通地我一直守着这份坚持,与已经被潜移默化的团队文化对抗,最终只是落得个遍体鳞伤。但是我并不后悔,反而很庆幸,因为最后我找到了自己内心的真相,一直以来,我觉得是 IMWeb 团队造就了我,其实,我所依恋的一直都是它的价值观与文化,而我愿意一直为之践行。


我的管理之路


我正式任命是在 2020年上半年,但实际上,我在 2018 年下半年就从腾讯课堂调到了企鹅辅导,从一组调到三组,并开始做一些团队管理的工作。整体而言我的管理经验成长的非常缓慢,这是我自己的结论。


首先,我的角色转变比较缓慢。经常看到小伙伴们做事情太“慢”,我都忍不住要自己上,或者直接告诉他们答案,我知道这很不好,但是初期的我就是忍不住,我感觉我的管理之路就是憋气之路,最后总结就是,在大方向上,我要站出来,但是具体实施层面,我要当个隐身人,这对我来说,非常难受。


其次,我是主猫头鹰次考拉的重事风格,不太擅长管理小伙伴的情绪还有激励,沟通和语言艺术真是我需要投入一生去学习锻炼的课程。另外,我有一个最大的问题就是不喜欢冲突,直接导致我不太擅长争取资源,这会让我觉得很对不起小伙伴们,这点也是我离开腾讯最大的原因吧。感觉我比较适合增量市场,在存量市场这点真的是致命的,不过专心搞好业务不挺好吗,何苦浪费时间在这些地方。


作者:潜龙在渊灬
链接:https://juejin.cn/post/7189612924116140092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

对话大环境严峻之下选择跨行跨岗的小姐姐

前阵子,今年3月份的时候,参加了一个线下行业交流会,里面各行各业的人士都会讲述自己的从业情况以及成果,当时状态有些不舒服加上有些拘谨没能记得很多人,人和事件都是选择性的记忆。坐在我附近的一位女孩子,笑起来很让人舒服且友好,大脑可能就选择性地记忆了。 她讲述自己...
继续阅读 »

前阵子,今年3月份的时候,参加了一个线下行业交流会,里面各行各业的人士都会讲述自己的从业情况以及成果,当时状态有些不舒服加上有些拘谨没能记得很多人,人和事件都是选择性的记忆。坐在我附近的一位女孩子,笑起来很让人舒服且友好,大脑可能就选择性地记忆了。


她讲述自己行业的时候,没感觉到丝毫的怯场,反而将自己的所处的情况以及计划都理得很清晰。为什么我会在意这些,大概是自身所不具备的看到别人具备的特质则显得格外的在意。从她描述的状况和语气来看,她已经解除了自己的困境,话里透露着对以后的走向多了些坚定和勇气。据后来了解到是咨询了行业大佬V姐,V姐将她的现状和想法做了解读和引导,大概是能如此吧。


她硕士毕业,去了一家央企工作,从事金融相关的行业,日常需要对接国内外的业务,目前该行业是天花板,自己处于事业的瓶颈期,也在离职过程中。听到这里或许觉得,今年大环境差,就业严峻,能苟着就苟着,况且还是央企,养老铁饭碗啊。是的,我也是这样想的,今年杭州上半年大肆裁员,海投简历多半已读不回,看多了it从业者在社交平台贩卖焦虑。但此刻的她,已经分析过自身的处境才能做出这一举动。毕竟相比于环境怎么样,时间其实更加宝贵,财富的积累更能决定你所做的和做想的是否值得。所以我是很支持她,结束之后和她交流了一番,并表示将来能够有更多机会联动。


一段时间后,周三的下午,正在码bug中,突然收到了她的消息,从她的话里提取出来就是她成功裸辞跳槽,还跨行跨岗,心里暗暗称强,终于能联动上了。那个下午疯狂码代码中,回了一句手头忙得晚点,觉得交流是在微信里的,可以推迟到晚上交流。当她回复预约线下当面聊的时候,我内心其实是拒绝的,像我这种比较腼腆内向的男孩子,一向没太多线下交流的机会,除非早期体验生活接外快硬着头皮上,还硬是拉上了同学陪衬,且这次还是女孩子,啊,好为难啊。


图片


当然,这个弱点一直被我重视,今年上半年都在为这个弱点做了大量的功课。每次都会和内心的自己和解,尝试和周围的人交流,小区的人,店家老板,租友,其他行业的人,再者就是周末跑出去摆摊和阿姨大叔一块聊天,聊聊他们女儿的情况......


所以,这次当然也和自己和解了,决定赴约。再次见面像极了朋友重逢的感觉,或许在职场被领导或甲方压迫久了,这次没有压迫感,久违了,唯独怕自己不知道专不专业的知识能不能够帮助到她。


见面聊了很多,发现跨行跨岗位,能够看出给她带来了很多的压力。本身专业学的就不是这个,现在从事网络安全相关的岗位,不仅要把这些计算机相关知识消化,还需要把公司提供的产品都要有所掌握,光说她工作内容下来我统计她叹了两三次气,我赶忙安慰慢慢来,一切都会好的,她遂即回我她很喜欢这份工作,累是确实是累,但是很有意义,是带着笑着说出来的。


期间也吐露过以后成熟了打算和自己的挚友出来单干,这么敏感重要的信息我当然很在意并予以支持和鼓励,这充分的表达了她对于自身情况和职业规划是清晰的。


对于自己的人生或者职业规划,有一个清晰的目标是很重要的,至少我认为很重要,“大方向不变,小动作可伸缩”,但凡你敢想敢做,就能有无限可能,不管概率大不大,有经验包就是一个值。


或许有股鸡汤的味道,但我身边接触的大佬,有互联网行业的,做自媒体的,做副业的,总结就是他们从不会把网络上频繁出现的“润”,“躺平”,“摆烂”之类慰藉心灵的词摆在嘴上,反而他们做事非常的雷厉风行,不拖拉,很明白自己的现状,以及自己想要干嘛。总比自己状况都摸不清楚,自己本职的工作都没搞明白,一边想“润”,一边好高骛远强得多。


和她交流之后,给我的感觉就是做事雷厉风行,学习能力强的那种,非但没有被跨行跨业给恐惧到,反而越发的有精神。期间问了我一些市面上主流比较热门的内容,例如云服务与本地机房,防火墙的原理,堡垒机的原理和作用,系统架构以及服务治理等等。开始是不适应的,可能内心戏比较多,我讲得很生硬岂不尴尬?或者旁边人觉得我这桌好怪,大晚上不好好吃东西讲些莫名其妙的话题?想刀一个人心是藏不住的,想帮一个人的心也是藏不住的,我尽可能地调度我那单核脑容量,同时组织人话讲述出去,专不专业我不知道,但是我知道有些比较主观,有些没能覆盖全面,讲的过程中感觉不到周围有人的存在。


结束之后,虽然打滴回去的路上还在讨论网络安全案例图,我不知道司机师傅会不会懵逼,但我觉得讨论这些很有趣,哈哈。期待下次的联动。


图片
▲图/ Ariel 拍摄提供


这一次的联动,从她身上也学习到了很多。她对于一个新环境的适应能力和学习能力是很强的,做事的风格雷厉风行,遇到难题会选择非常有效的方式。上一个做事雷厉风行,从不拖拉让我印象深刻的大佬还是去年认识,成功转型go开发,很遗憾的就是因为很多因素没能一起共事。


相反,我在往期文章多次提到的挚友鑫仔,学的建筑行业,近期询问他的状况,因为大环境恶劣,破坏了自己在学校规划好的一切,从深圳跑回到家乡二三线城市,选择了一家低于自己预期但实属无奈的设计院工作。他日常焦虑,对于过来人的经验看待他的状态和举动像是以第三视角在看他,于是对他进行了言传身教,希望对他有所帮助,能够再次恢复挥斥方遒的热血青年。


不知不觉,文章看多了,写得多了,身边各行各业的人接触多了,越发觉得什么最重要,要像什么样的人靠齐,真正做到知行合一是能带来多大的益处。


作者:桑小榆呀
链接:https://juejin.cn/post/7118992017175838750
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用Snackbar代替Toast

用Snackbar代替Toast Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Sna...
继续阅读 »

用Snackbar代替Toast



Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Snackbar。



认识Snackbar


我是在看某个三方库源码的时候发现它的,在以往的开发经验中,它出现的频率并不高。我在某个三方库中发现,在两个项目中,提示的表现却不一样,代码怎么会骗人呢。于是认真的看这个三方库的源码,直到看到下面的判断。



static {

try {

Class.forName("android.support.design.widget.Snackbar");

Class.forName("android.support.design.widget.BottomSheetDialog");

HAS_DESIGN_LIB = true;

} catch (Throwable ignore) {

HAS_DESIGN_LIB = false;

}

}

protected AbsAgentWebUIController create() {

return HAS_DESIGN_LIB ? new DefaultDesignUIController() : new DefaultUIController();

}

这里的意思是说,如果项目中有SnackbarBottomSheetDialog库的引用,则使用DefaultDesignUIController的UI,也就是Snackbar的提示;没有的话,则使用DefaultUIController的UI,也就是默认的Toast提示。


实际上其实谷歌在Android 5.0的时候就推出了Snackbar,它是Material Design中的一个控件。


实践 Snackbar


下图为ToastSnackbar的展示效果


image.png



  • 简单的提示


Snackbar的基本用法和Toast类似



Snackbar.make(findViewById(R.id.root), "这是一条提示", Snackbar.LENGTH_LONG).show();


  • 带有Action的提示



Snackbar snackbar = Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE);

snackbar.setAction("取消", new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(MainActivity.this,"点击取消",Toast.LENGTH_SHORT).show();

}

});

snackbar.show();

这里只能设置一个Action,不然旧的会被替代掉。




  1. Snackbar.LENGTH_INDEFINITE:代表无限期的显示,一直显示,点击按钮才可以隐藏




  2. Snackbar.LENGTH_LONG:长时间提示




  3. Snackbar.LENGTH_SHORT:短时间提示





  • 修改样式


更改Snackbar的背景颜色



snackbar.getView().setBackgroundColor(Color.parseColor("#0000ff"));

更改Action提示的颜色



snackbar.setActionTextColor(Color.parseColor("#ffffff"));

更改padding的距离



snackbar.getView().setPadding(50, 50, 50 , 50);

操作文字,比如添加图片、更改文字内容、更改文字颜色,更改文字大小等。


虽然Snackbar没有提供给我们直接操作文字样式的方法,但我们可以通过findViewById获取这个文字,然后就像操作TextView一样去操作它就可以了。


怎么获取TextView?



TextView textView = snackbar.getView().findViewById(R.id.snackbar_text);

snackbar_text id来自Snackbar的源码。获取ID的时候编辑器可能会报错提示,实际上是可以运行的。



@NonNull

public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {

ViewGroup parent = findSuitableParent(view);

if (parent == null) {

throw new IllegalArgumentException("No suitable parent found from the given view. Please provide a valid view.");

} else {

LayoutInflater inflater = LayoutInflater.from(parent.getContext());

SnackbarContentLayout content = (SnackbarContentLayout)inflater.inflate(hasSnackbarButtonStyleAttr(parent.getContext()) ? layout.mtrl_layout_snackbar_include : layout.design_layout_snackbar_include, parent, false);

Snackbar snackbar = new Snackbar(parent, content, content);

snackbar.setText(text);

snackbar.setDuration(duration);

return snackbar;

}

}


<view

xmlns:android="http://schemas.android.com/apk/res/android"

class="android.support.design.widget.SnackbarContentLayout"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_gravity="bottom"

android:theme="@style/ThemeOverlay.AppCompat.Dark">

<TextView

android:id="@+id/snackbar_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_weight="1"

android:layout_gravity="center_vertical|left|start"

android:paddingTop="@dimen/design_snackbar_padding_vertical"

android:paddingBottom="@dimen/design_snackbar_padding_vertical"

android:paddingLeft="@dimen/design_snackbar_padding_horizontal"

android:paddingRight="@dimen/design_snackbar_padding_horizontal"

android:ellipsize="end"

android:maxLines="@integer/design_snackbar_text_max_lines"

android:textAlignment="viewStart"

android:textAppearance="?attr/textAppearanceBody2"/>

<Button

android:id="@+id/snackbar_action"

style="?attr/snackbarButtonStyle"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_gravity="center_vertical|right|end"

android:minWidth="48dp"

android:visibility="gone"/>

</view>

作者:Android唐浮
链接:https://juejin.cn/post/7103803312910106660
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

定位都得集成第三方?Android原生定位服务LocationManager不行吗?

前言 现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。 有些同学觉得不就是获取到经纬度么,An...
继续阅读 »

前言


现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。


有些同学觉得不就是获取到经纬度么,Android 自带的就有位置服务 LocationManager ,我们无需引入第三方服务,就可以很方便的实现定位逻辑。


确实 LocationManager 的使用很简单,获取经纬度很方便,我们就无需第三方的服务了吗? 或者说 LocationManager 有没有坑呢?兼容性问题怎么样?获取不到位置有没有什么兜底策略?


一、LocationManager的使用


由于是Android的系统服务,直接 getSystemService 可以获取到


LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);

一般获取位置有两种方式 NetWork 与 GPS 。我们可以指定方式,也可以让系统自动提供最好的方式。


// 获取所有可用的位置提供器
List<String> providerList = locationManager.getProviders(true);
// 可以指定优先GPS,再次网络定位
if (providerList.contains(LocationManager.GPS_PROVIDER)) {
provider = LocationManager.GPS_PROVIDER;
} else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) {
provider = LocationManager.NETWORK_PROVIDER;
} else {
// 当没有可用的位置提供器时,弹出Toast提示用户
return;
}

当然我更推荐由系统提供,当我的设备在室内的时候就会以网络的定位提供,当设备在室外的时候就可以提供GPS定位。


 String provider = locationManager.getBestProvider(criteria, true);

我们可以实现一个定位的Service实现这个逻辑


/**
* 获取定位服务
*/
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@SuppressLint("MissingPermission")
@Override
public void onCreate() {
super.onCreate();

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

//第二个参数是间隔时间 第三个参数是间隔多少距离,这里我试过了不同的各种组合,能获取到位置就是能,不能获取就是不能
lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:"+provider +" status:"+status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}

@Override
public void onDestroy() {
super.onDestroy();
lm.removeUpdates(listener); // 停止所有的定位服务
}

}

使用:定义并动态申请权限之后即可开启服务



fun testLocation() {

extRequestPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) {

startService(Intent(mActivity, LocationService::class.java))

}

}

这样我们启动这个服务就可以获取到当前的经纬度,只是获取一次,大家如果想再后台持续定位,那么实现的方式就不同了,我们服务要设置为前台服务,并且需要额外申请后台定位权限。


话说回来,这么使用就一定能获取到经纬度吗?有没有兼容性问题


Android 5.0 Oppo



Android 6.0 Oppo海外版



Android 7.0 华为



Android 11 三星海外版



Android 12 vivo



目前测试不多,也能发现问题,特别是一些低版本,老系统的手机就可能无法获取位置,应该是系统的问题,这种服务跟网络没关系,开不开代理都是一样的。


并且随着测试系统的变高,越来越完善,提供的最好定位方式还出现混合定位 fused 的选项。


那是不是6.0的Oppo手机太老了,不支持定位了?并不是,百度定位可以获取到位置的。



既然只使用 LocationManager 有风险,有可能无法获取到位置,那怎么办?


二、混合定位


其实目前百度,高度的定位Api的服务SDK也不算大,相比地图导航等比较重的功能,定位的SDK很小了,并且目前都支持海外的定位服务。并且定位服务是免费的哦。


既然 LocationManager 有可能获取不到位置,那我们就加入第三方定位服务,比如百度定位。我们同时使用 LocationManager 和百度定位,哪个先成功就用哪一个。(如果LocationManager可用的话,它的定位比百度定位更快的)


完整代码如下:


@SuppressLint("MissingPermission")
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;
private LocationClient mBDLocationClient = null;
private MyBDLocationListener mBDLocationListener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();

createNativeLocation();

createBDLocation();
}

/**
* 第三方百度定位服务
*/
private void createBDLocation() {
mBDLocationClient = new LocationClient(UIUtils.getContext());
mBDLocationListener = new MyBDLocationListener();
//声明LocationClient类
mBDLocationClient.registerLocationListener(mBDLocationListener);
//配置百度定位的选项
LocationClientOption option = new LocationClientOption();
option.setLocationMode(LocationClientOption.LocationMode.Battery_Saving);
option.setCoorType("WGS84");
option.setScanSpan(10000);
option.setIsNeedAddress(true);
option.setOpenGps(true);
option.SetIgnoreCacheException(false);
option.setWifiCacheTimeOut(5 * 60 * 1000);
option.setEnableSimulateGps(false);
mBDLocationClient.setLocOption(option);
//开启百度定位
mBDLocationClient.start();
}

/**
* 原生的定位服务
*/
private void createNativeLocation() {

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:" + provider + " status:" + status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}


/**
* 百度定位的监听
*/
class MyBDLocationListener extends BDAbstractLocationListener {

@Override
public void onReceiveLocation(BDLocation location) {

double latitude = location.getLatitude(); //获取纬度信息
double longitude = location.getLongitude(); //获取经度信息


YYLogUtils.w("百度的监听 latitude:" + latitude);
YYLogUtils.w("百度的监听 longitude:" + longitude);

YYLogUtils.w("onBaiduLocationChanged:" + longitude + "-" + latitude);

stopSelf(); // 获取到经纬度以后,停止该service
}
}

@Override
public void onDestroy() {
super.onDestroy();
// 停止所有的定位服务
lm.removeUpdates(listener);

mBDLocationClient.stop();
mBDLocationClient.unregisterLocationListener(mBDLocationListener);
}

}

其实逻辑都是很简单的,并且省略了不少回调通信的逻辑,这里只涉及到定位的逻辑,别的逻辑我就尽量不涉及到。


百度定位服务的API申请与初始化请自行完善,这里只是简单的使用。并且坐标系统一为国际坐标,如果需要转gcj02的坐标系,可以网上找个工具类,或者看我之前的文章


获取到位置之后,如何Service与Activity通信,就由大家自由发挥了,有兴趣的可以看我之前的文章


总结


所以说Android原生定位服务 LocationManager 还是有问题啊,低版本的设备可能不行,高版本的Android系统又很行,兼容性有问题!让人又爱又恨。


很羡慕iOS的定位服务,真的好用,我们 Android 的定位服务真是拉跨,居然还有兼容性问题。


我们使用第三方定位服务和自己的 LocationManager 并发获取位置,这样可以增加容错率。是比较好用的,为什么要加上 LocationManager 呢?我直接单独用第三方的定位服务不香吗?可以是可以,但是如果设备支持 LocationManager 的话,它会更快一点,体验更好。


好了,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。


作者:newki
链接:https://juejin.cn/post/7131910145574961182
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android案例手册 - 仅一个文件的展开收缩LinearLayout

👉实践过程 Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。 前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。 仅一个文件(Java版或...
继续阅读 »

👉实践过程


Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。


前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。


仅一个文件(Java版或Kotlin版),随时复制随时用。
先看效果图


可展开的LinearLayout-仅一个文件复制即用.gif


默认展示两个子item,当点击“显示更多”的时候展开所有的子View,当点击“收起内容”的时候除了前两个其他的都隐藏。


😜使用


我们先来看看使用方式:


<cn.phototocartoonstudy.ExpandableLinearLayout
    android:id="@+id/idExpandableLinearLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="芝麻粒儿" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://juejin.cn/user/4265760844943479" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="CSDN" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://zhima.blog.csdn.net/" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="Android/Unity技术" />
</cn.phototocartoonstudy.ExpandableLinearLayout>

直接布局中用即可,或者动态代码添加:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_framelayout);
ExpandableLinearLayout idExpandableLinearLayout=findViewById(R.id.idExpandableLinearLayout);
for (int i = 0; i < 4; i++) {
TextView  txtViewTip = new TextView(this);
txtViewTip.setText("芝麻粒儿添加更多内容"+i);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
idExpandableLinearLayout.addView(txtViewTip);
}
}

😜实现


说完了使用,我们就来说说实现,前面学完LinearLayout后知道该控件使用了wrap_content,如果子View使用隐藏GONE的形式,则高度自动变化,页面布局中和该控件对其的其他控件也会自动变化。


所以,当子View的个数小于设置的默认个数,则不用添加底部,如果子View个数大于默认显示个数,则在最后动态添加一个View,当点击展开和隐藏的时候,其他多余的控件进行GONE和VISIBLE的控制即可。


我们再为控件增加点其他方法:




  1. 修改当隐藏的时候默认展示的条目




  2. 可修改展开和收起的控件文本




  3. 可修改展开和收起控件的字体大小和颜色




  4. 其他功能自己看着加吧




public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}
public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}
public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}

Java版


/**
* Created by akitaka on 2022-08-11.
*
* @author akitaka
* @filename ExpandableLinearLayout
*/

public class JavaExpandableLinearLayout extends LinearLayout implements View.OnClickListener {

private TextView txtViewTip;
/**
* 是否是展开状态,默认是隐藏
*/
private boolean isExpand = false;
private boolean boolHasBottom = false;

private int intDefaultItemCount = 2;
/**
* 待展开显示的文字
*/
private String strExpandText = "显示更多";
/**
* 待隐藏显示的文字
*/
private String strHideText = "收起内容";
private float fontTextSize;
private int intTextColor;

public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}

public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}

public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}
public JavaExpandableLinearLayout(Context context) {
this(context, null);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//设置垂直方向
setOrientation(VERTICAL);
}

@Override
public void setOrientation(int orientation) {
if (LinearLayout.HORIZONTAL == orientation) {
throw new IllegalArgumentException("ExpandableLinearLayout只支持垂直布局");
}
super.setOrientation(orientation);
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
justToAddBottom(childCount);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


/**
* 判断是否要添加底部
*/
private void justToAddBottom(int childCount) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true;
//要使用默认底部,并且还没有底部
LinearLayout linearLayoutBottom = new LinearLayout(getContext());
LinearLayout.LayoutParams layoutParamsBottom = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutBottom.setLayoutParams(layoutParamsBottom);
linearLayoutBottom.setGravity(Gravity.CENTER);
txtViewTip = new TextView(getContext());
txtViewTip.setText("展开更多");
txtViewTip.setTextSize(fontTextSize);
txtViewTip.setTextColor(intTextColor);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10);
linearLayoutBottom.addView(txtViewTip);
linearLayoutBottom.setOnClickListener(this);
//添加底部
addView(linearLayoutBottom);
hide();
Log.e("TAG", "justToAddBottom: zou l zhe ");
}
}

/**
* 刷新UI
*/
private void refreshView(View view) {
int childCount = getChildCount();
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount);
}
//大于默认数目的先隐藏
view.setVisibility(GONE);
}
}

/**
* 展开
*/
private void expand() {
for (int i = intDefaultItemCount; i < getChildCount(); i++) {
//从默认显示条目位置以下的都显示出来
View view = getChildAt(i);
view.setVisibility(VISIBLE);
}
}

/**
* 收起
*/
private void hide() {
int endIndex = getChildCount() - 1;
for (int i = intDefaultItemCount; i < endIndex; i++) {
//从默认显示条目位置以下的都隐藏
View view = getChildAt(i);
view.setVisibility(GONE);
}
}

@Override
public void onClick(View v) {
outUseMethodToggle();
}

/**
* 外部也可调用 展开或关闭
*/
public void outUseMethodToggle() {
if (isExpand) {
hide();
txtViewTip.setText(strExpandText);
} else {
expand();
txtViewTip.setText(strHideText);
}
isExpand = !isExpand;
}

/**
* 外部可随时添加子view
*/
public void outUseMethodAddItem(View view) {
int childCount = getChildCount();
//插在底部之前
addView(view, childCount - 1);
refreshView(view);
}
}

Kotlin版


/**
* Created by akitaka on 2022-08-11.
* @author akitaka
* @filename KotlinExpandableLinearLayout
*/
class KotlinExpandableLinearLayout :LinearLayout, View.OnClickListener {
private var txtViewTip: TextView? = null
constructor(context: Context?) :this(context,null)
constructor(context: Context?, attrs: AttributeSet?) :this(context,attrs,0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
//设置垂直方向
orientation = VERTICAL
}
/**
* 是否是展开状态,默认是隐藏
*/
private var isExpand = false

private var intDefaultItemCount = 2
private var boolHasBottom = false

/**
* 待展开显示的文字
*/
private var strExpandText = "显示更多"

/**
* 待隐藏显示的文字
*/
private var strHideText = "收起内容"
private var fontTextSize = 0f
private var intTextColor = 0

fun outUseMethodChangeDefaultItemCount(intDefaultItemCount: Int) {
this.intDefaultItemCount = intDefaultItemCount
}

fun outUseMethodChangeExpandText(strExpandText: String) {
this.strExpandText = strExpandText
}

fun outUseMethodChangeHideText(strHideText: String) {
this.strHideText = strHideText
}

fun outUseMethodChangeExpandHideTextSize(fontTextSize: Float) {
this.fontTextSize = fontTextSize
}

fun outUseMethodChangeExpandHideTextColor(@ColorInt intTextColor: Int) {
this.intTextColor = intTextColor
}

override fun setOrientation(orientation: Int) {
require(HORIZONTAL != orientation) { "ExpandableLinearLayout只支持垂直布局" }
super.setOrientation(orientation)
}


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val childCount = childCount
justToAddBottom(childCount)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}


/**
* 判断是否要添加底部
*/
private fun justToAddBottom(childCount: Int) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true
//要使用默认底部,并且还没有底部
val linearLayoutBottom = LinearLayout(context)
val layoutParamsBottom = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
linearLayoutBottom.layoutParams = layoutParamsBottom
linearLayoutBottom.gravity = Gravity.CENTER
txtViewTip = TextView(context)
txtViewTip!!.text = "展开更多"
txtViewTip!!.textSize = fontTextSize
txtViewTip!!.setTextColor(intTextColor)
val layoutParamsBottomTxt = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
txtViewTip!!.layoutParams = layoutParamsBottomTxt
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10)
linearLayoutBottom.addView(txtViewTip)
linearLayoutBottom.setOnClickListener(this)
//添加底部
addView(linearLayoutBottom)
hide()
}
}

/**
* 刷新UI
*/
private fun refreshView(view: View) {
val childCount = childCount
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount)
}
//大于默认数目的先隐藏
view.setVisibility(GONE)
}
}

/**
* 展开
*/
private fun expand() {
for (i in intDefaultItemCount until childCount) {
//从默认显示条目位置以下的都显示出来
val view: View = getChildAt(i)
view.setVisibility(VISIBLE)
}
}

/**
* 收起
*/
private fun hide() {
val endIndex = childCount - 1
for (i in intDefaultItemCount until endIndex) {
//从默认显示条目位置以下的都隐藏
val view: View = getChildAt(i)
view.setVisibility(GONE)
}
}

override fun onClick(v: View?) {
outUseMethodToggle()
}

/**
* 外部也可调用 展开或关闭
*/
fun outUseMethodToggle() {
if (isExpand) {
hide()
txtViewTip!!.text = strExpandText
} else {
expand()
txtViewTip!!.text = strHideText
}
isExpand = !isExpand
}

/**
* 外部可随时添加子view
*/
fun outUseMethodAddItem(view: View) {
val childCount = childCount
//插在底部之前
addView(view, childCount - 1)
refreshView(view)
}
}


📢作者:小空和小芝中的小空


作者:芝麻粒儿
链接:https://juejin.cn/post/7130639529123250213
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

JS令人头疼的类型转换

web
前言 JS中的类型转换常常被人诟病,因为javascript属于弱类型语言,它对于类型的语言没有强制的限定,这对于我们来说是头疼的。不同的类型之间的运算需要先对数据的类型进行转换,在日常开发中我们经常会用到。 数据类型 基本数据类型 Number(数字) S...
继续阅读 »

前言


JS中的类型转换常常被人诟病,因为javascript属于弱类型语言,它对于类型的语言没有强制的限定,这对于我们来说是头疼的。不同的类型之间的运算需要先对数据的类型进行转换,在日常开发中我们经常会用到。


数据类型


基本数据类型



  • Number(数字)

  • String(字符串)

  • Boolean(布尔)

  • Null

  • Undefined

  • Symbol(ES6)


引用数据类型



  • object{}

  • array[]

  • function(){}

  • date()


由于JS中拥有动态类型,在定义的时候不用指定数据类型,赋值的时候可以将任意类型赋给同一个变量,例如:let a = 1; a = '1'


类型转换


什么是类型转换?


简单来说就是将值从一种数据类型转换为另一种数据类型的过程。


分为哪几种?


根据转换的特点分为:显式类型转换(强制转换)和隐式类型转换(自动转换)。


显示类型转换(强制转换)


通过Boolean()——原始值转布尔,Number()——原始值转数字,String()——原始值转字符来进行强制类型转换。这里的转换规则可以直接查看Js官方文档:Annotated ES5


1.png


2.png


我们从文档中可以知道当我们想进行强制类型转换时,js会自动会帮我们使用ToString(value),ToNumber(value)进行转换。


//原始值转布尔
console.log(Boolean('123'));
console.log(Boolean(123));
console.log(Boolean(null));
console.log(Boolean(undefined));
console.log(Boolean(true));

//原始值转数字
console.log(Number('123'));
console.log(Number(123));
console.log(Number(null));
console.log(Number(undefined));
console.log(Number(true));

//原始值转字符串
console.log(String('123'));
console.log(String(123));
console.log(String(null));
console.log(String(undefined));
console.log(String(true));

结果为:


3.png


对象转字符串,数字


通过调用特殊的对象转换方法来完成,在js中有两个方法来执行转换,这两个方法所有的对象都具备,就是用来把对象转换为原始值的。这两个方法分别为toString(),valueOf(),这两个方法对象的构造函数原型上就有,其目的就是要有办法把对象转换为原始类型。


对象转字符串


toString()方法除了Null和Undefined其他的数据类型都具有此方法。通常情况下toString()和String()效果一样。



4.png


我们在文档中重点关注对象转字符串,上图中对象转字符串有两个步骤,先是执行自带的ToPrimitive(obj,String),再返回执行结果,分以下几步:


1.判断obj是否为基本类型,是则返回


2.调用对象自带的toString方法,如果能得到一个原始类型,则返回


3.调用对象自带的valueOf方法,如果能得到一个原始类型,则返回


4.报错


对象转数字

对象转数字的话也同样是有两个步骤:先是执行自带的ToPrimitive(obj,Number),再返回执行结果,分以下几步:


1.判断obj是否为基本类型,是则返回


2.调用对象自带的valueOf方法,如果能得到一个原始类型,则返回


3.调用对象自带的toString方法,如果能得到一个原始类型,则返回


4.报错


隐式类型转换



  • 当 + 运算作为一元操作符时,会自动调用ToNumber()处理该值。(相当于Number())


例如:console.log(+'123');结果为数字123。
console.log(+[]);结果为0,因为对象[]转换为了0。



  • 当 + 运算作为二元操作符,例(a + b)


1.lprim = ToPrimitive(v1)


2.rprim = ToPrimitive(v2)


3.如果lprim是字符串或者rprim是字符串,则返回ToString(lprim)和ToStringrprim()的拼接结果


4.返回ToNumber(lprim) + ToNumber(rprim)


结语


js类型转换规则,相当于历史事件,是已经规定好的,弄清楚它,能更好地和面试官侃侃而谈。最后感谢各位的观看。


作者:一拾九
来源:juejin.cn/post/7224518612161593402
收起阅读 »

节流 你会手写吗?

web
节流 在各大面试题中,频繁出现的老油条,节流。 啥叫节流呢? 节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器。 在间隔一段时间执行一次回调的场景有: 1...
继续阅读 »

节流


在各大面试题中,频繁出现的老油条,节流。


啥叫节流呢?


节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器。


在间隔一段时间执行一次回调的场景有:


1.滚动加载,加载更多或滚到底部监听

2.搜索框,搜索联想功能

简单来说就是,一段时间内重复触发,按一定频率(1s、3s、5s)执行,可配置一开始就执行一次。


如果还不懂,就直接上我们的例子。我们可以看到当我们滑动屏幕的时候,会频繁运行打印这个函数。


image.png
当我们进行节流后,给它设置一个时间,那么他就只会在该时间后


image.png


上代码


其中fn代表将要运行的函数,delay代表函数触发的时间间隔。


整个代码思路,
timer=null,
flag=false, 默认刚开始不运行
设置一个定时器,
等到delay时间到了,就会开始运行这个函数fn。如果在delay之前,发生了滚动等事件,因为已经
flag = true,只会return 不会运行这个函数fn。只有等带delay到了时间,才会运行函数。


定时器实现的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。


js
let count = 0;
function throttle(fn, delay) {
let timer = null // 把变量放函数里面,避免全局污染
let flag = false
let that = this
return function () {
if (flag) return
flag = true
let arg = arguments // 此处为fn函数的参数
timer = setTimeout(() => {
fn.apply(that, arg)
flag = false
}, delay)
}
}
function test(a, b) {
console.log(a, b)
}
let throttleTest = throttle(test, 1000)
// 测试函数
function test1() {
console.log('普通test:', count++)
}

window.addEventListener('scroll', (e) => {
// test1() // 不用节流的话,页面滚动一直在调用
throttleTest(1, 2) // 加上节流,即使页面一直滚动,也只会每隔一秒执行一次test函数
})

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7222984001769488443
收起阅读 »