注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

热榜排行爬虫详解

作为一个爬虫必须摸不一样的鱼,平时大家怎么看热榜,今天爬一个热榜数据,咱就在ied中读热榜。还是一个乌龙事件听我细细道来网站地址 1,话不多说,今天图也不看了直接进入主题,打开网站首页抓个包 2,可见接口中有两个参数,第二个参数盲猜是请求时候的时间戳,第一个...
继续阅读 »

作为一个爬虫必须摸不一样的鱼,平时大家怎么看热榜,今天爬一个热榜数据,咱就在ied中读热榜。还是一个乌龙事件听我细细道来网站地址


1,话不多说,今天图也不看了直接进入主题,打开网站首页抓个包


image.png
2,可见接口中有两个参数,第二个参数盲猜是请求时候的时间戳,第一个参数有点长不像是正常的时间戳,多翻几页发现第一个参数也是一个时间戳只是后面加了三个000,咱们就去掉三个零(这里多请求了几页没有发现翻页的变化规律)


image.png
2.1,第二个参数转换发现就是请求时间没错


image.png
2.2,第一个参数转换瞬间我这充满智慧的大脑里出现了无数想法(这是随机的)(这是文章发布时间)(文章发布时间放到翻页怎么获取呢)(这是网站反爬生成一堆时间戳映射到page上做翻页)(这是通过算法和请求时间做比较生成对应的page进行翻页)(。。。)然后我就去了后台看看查一下这个参数名


image.png


image.png
只一眼,并带着对这种小网站的看不起,直接到response中一检索果然。小网站哪有什么高端反爬


image.png
3,直接上代码,翻页就不再多谢,拿到参数之后可以自己向下补充了

headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Cookie': 'deviceId=web.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqaWQiOiJhNjNkZjYxZS00ODZhLTQzNTgtODNmMy1hNDlkMjdkMmI4ZmUiLCJleHBpcmUiOiIxNjY1MjIyMzY3MDAwIn0.eQF9za4cSq8huEESJPn0nDP3PUsDiVNZ4CM_fTAeWMg; Hm_lvt_03b2668f8e8699e91d479d62bc7630f1=1662630378',
'Pragma': 'no-cache',
'Referer': 'https://dig.chouti.com/',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
}
import time
params = {
'afterTime': '1681236005077000',
'_': f'{int(time.time()*1000)}',
}

response = requests.get('https://dig.chouti.com/link/hot', params=params, headers=headers).json()['data']
next_afterTime = response[-1]['operateTime']
print(next_afterTime)
for res in response:
title = res['title']
url = res['url']
print(title)
yes = input()
if yes == '1':
print(url)

4,ok这样的话就只需要看到想看的题目就输入1返回url,就可以自行观赏了,跑一下


VeryCapture_20230412162714.gif


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

31岁,还可以拼一拼大厂么?

大家好呀,我是楼仔。 上周有一个粉丝问我:楼哥,我刚学习 Java,可以在半年内达到入门初级所有的要求么? 年前还和一位百度的前同事聊天,他目前在阿里,职级对标阿里 P7,年薪 120W,这个算是相当高的,因为 P7 一般在 80W 左右,就算是 P6 的级别...
继续阅读 »

大家好呀,我是楼仔。


上周有一个粉丝问我:楼哥,我刚学习 Java,可以在半年内达到入门初级所有的要求么?


年前还和一位百度的前同事聊天,他目前在阿里,职级对标阿里 P7,年薪 120W,这个算是相当高的,因为 P7 一般在 80W 左右,就算是 P6 的级别,年薪也有 50W。



大厂,薪资高,技术强,这也是很多同学想去大厂的原因,那 30 岁之后,还有必要去拼大厂么?


粉丝提问


半路转行的,水了五六年经验了,小公司,没大佬级别人物,瞎忙,基本就是做一些公司业务之类的,公司也不注重技术,项目短快,能挣钱就行,发现到现在是啥都不会。


本来在现在这家公司就是打算往管理岗转的,现在有个问题就是领导的一些作风和思想不是很认可,所以才想着跳了。


最近准备跳了,在看你整理的面试题,想找一个不太忙的公司,好好研究研究技术,来年拼一拼大厂,大佬给点建议呗。


楼仔回答


我觉得这个需要根据家庭情况来考虑。


如果家庭情况还好,目前的薪资能承受家庭的经济压力,就不建议再去拼大厂,因为去大厂,工资确实会高和很多,但是到了 35-40 岁之后,危机会很严重。


如果家庭情况不太好,或者正缺钱,那可以去拼一下,如果不是公司需要裁员,干到 35 岁肯定是没问题的,如果技术也打磨得还不错,即使没有混到管理层,也可以干到 40 岁。


这个回答就非常现实了,程序员本来就是吃青春饭的,如果不转管理层,或者混到专家、架构师级别,很容易就到年龄天花板。


总结一下:



  • 30岁之前,强烈建议去大厂,多挣些钱,发展更好;

  • 30岁之后,相对要计划职业的长久发展,如果专家和管理层拼不上去,建议去国企相对稳定的公司。


听了我的一番话,这位球友又说了他的其它想法,考个在职博士,完了找个能长久点的工作。


我就直接问他“你确定考了在职博士,能找到长久点的工作么?”



我给的建议是,先确定你要去哪家单位,如果这家单位需要博士学历,你再去考,而不是先去考个博士学历,然后再去看能找哪些单位,方向反了。





看得出来,我的建议是真的帮助到他了。


写在最后


其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。


比如我星球中有的球友,连一些基本的后端技术栈都没有掌握好,就去卷源码,卷各种高大尚的技术,就是典型的方向不对。


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

10 个有用的 Kotlin flow 操作符

Kotlin 拥有函数式编程的能力,运用得当,可以简化代码,层次清晰,利于阅读,用过都说好。 然而操作符数量众多,根据使用场景选择合适的操作符是一个很大的难题,网上搜索了许久只是学习了一个操作符,还要再去验证它,实在浪费时间,开发效率低下。 一种方式是转至命令...
继续阅读 »

Kotlin 拥有函数式编程的能力,运用得当,可以简化代码,层次清晰,利于阅读,用过都说好。


然而操作符数量众多,根据使用场景选择合适的操作符是一个很大的难题,网上搜索了许久只是学习了一个操作符,还要再去验证它,实在浪费时间,开发效率低下。


一种方式是转至命令式开发,将过程按步骤实现,即可完成需求;另一种方式便是一次学习多个操作符,在下次选择操作符时,增加更多选择。


本篇文章当然关注的是后者,列举一些比较常用 Kotlin 操作符,为提高效率助力,文中有图有代码有讲解,文末有参考资料和一个交互式学习的网站。


让我们开始吧。


1. reduce


reduce 操作符可以将所有数据累加(加减乘除)得到一个结果


Kotlin_操作符.002.jpeg

listOf(1, 2, 3).reduce { a, b ->
a + b
}

输出:6

如果 flow 中没有数据,将抛出异常。如不希望抛异常,可使用 reduceOrNull 方法。


reduce 操作符不能变换数据类型。比如,Int 集合的结果不能转换成 String 结果。


2. fold


fold 和 reduce 很类似,但是 fold 可以变换数据类型


Kotlin_操作符.003.jpeg


有时候,我们不需要一个结果值,而是需要继续操 flow,可使用 runningFold 。

flowOf(1, 2, 3).runningFold("a") { a, b ->
a + b
}.collect {
println(it)
}

输出:
a
a1
a12
a123

同样的,reduce 也有类似的方法 runningReduce

flowOf(1, 2, 3).runningReduce { a, b ->
a + b
}.collect {
println(it)
}

输出:
1
3
6

3. debounce


debounce 需要传递一个毫秒值参数,功能是:只有达到指定时间后才发出数据,最后一个数据一定会发出


Kotlin_操作符.004.jpeg


例如,定义 1000 毫秒,也就是 1 秒,被观察者发出数据,1秒后,观察者收到数据,如果 1 秒内多次发出数据,则重置计算时间。

flow {
emit(1)
delay(590)
emit(2)
delay(590)
emit(3)
delay(1010)
emit(4)
delay(1010)
}.debounce(
1000
).collect {
println(it)
}

输出结果:
3
4

rebounce 的应用场景是限流功能


4. sample


sample 和 debounce 很像,功能是:在规定时间内,只发送一个数据


Kotlin_操作符.005.jpeg

flow {
repeat(4) {
emit(it)
delay(50)
}
}.sample(100).collect {
println(it)
}

输出结果:
1
3

sample 的应用场景是截流功能


debounce 和 sample 的限流和截流功能已有网友实现,点击这里


5. flatmapMerge


简单的说就是获得两个 flow 的乘积或全排列,合并并且平铺,发出一个 flow。


描述的仍然不好理解,一看代码便能理解了


Kotlin_操作符.006.jpeg

flowOf(1, 3).flatMapMerge {
flowOf("$it a", "$it b")
}.collect {
println(it)
}

输出结果:
1 a
1 b
3 a
3 b

flatmapMerge 还有一个特性,在下一个操作符里提及。


6. flatmapConcat


先看代码。


Kotlin_操作符.007.jpeg

flowOf(1, 3).flatMapConcat {
flowOf("a", "b", "c")
}.collect {
println(it)
}

功能和 flatmapMerge 一致,不同的是 flatmapMerge 可以设置并发量,可以理解为 flatmapMerge 是线程安全的,而 flatmapConcat 不是线程安全的。


本质上,在 flatmapMerge 的并发参数设置为 1 时,和 flatmapConcat 基本一致,而并发参数大于 1 时,采用 channel 的方式发出数据,具体内容请参阅源码。


7. buffer


介绍 buffer 的时候,先要看这样一段代码。

flowOf("A", "B", "C", "D")
.onEach {
println("1 $it")
}
.collect { println("2 $it") }

输出结果:
1 A
2 A
1 B
2 B
1 C
2 C
1 D
2 D

注意输出的内容。


加上 buffer 的代码。

flowOf("A", "B", "C", "D")
.onEach {
println("1 $it")
}
.buffer()
.collect { println("2 $it") }

输出结果:
1 A
1 B
1 C
1 D
2 A
2 B
2 C
2 D

输出内容有所不同,buffer 操作符可以改变收发顺序,像有一个容器作为缓冲似的,在容器满了或结束时,下游开始接到数据,onEach 添加延迟,效果更明显。


8. combine


合并两个 flow,长的一方会持续接受到短的一方的最后一个数据,直到结束


Kotlin_操作符.008.jpeg

flowOf(1, 3).combine(
flowOf("a", "b", "c")
) { a, b -> b + a }
.collect {
println(it)
}

输出结果:
a1
b3
c3

9. zip


也是合并两个 flow,结果长度与短的 flow 一致,很像木桶原理。
Kotlin_操作符.009.jpeg

flowOf(1, 3).zip(
flowOf("a", "b", "c")
) { a, b -> b + a }
.collect {
println(it)
}

输出结果:
a1
b3

10. distinctUntilChanged


就像方法名写的那样,和前一个数据不同,才能收到,和前一个数据想通,会被过滤掉。


Kotlin_操作符.010.jpeg

flowOf(1, 1, 2, 2, 3, 1).distinctUntilChanged().collect {
println(it)
}

输出结果:
1
2
3
1

最后


以上就是今天要介绍的操作符,希望对大家有所帮助。


参考文章:Android — 9 Useful Kotlin Flow Operators You Need to Know


Kotlin 交互式操作符网站:交互式操作符网站


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

使用 Kotlin Compose Desktop 实现了一个简易的"手机助手"

一. adbd-connector adbd-connector 是一个实现 adb server 和 adb daemon 之间的通信协议的库,使用 Kotlin 编写。支持 PC 端直接连接 Android 设备操作 adb 相关的指令。 github 地...
继续阅读 »

一. adbd-connector


adbd-connector 是一个实现 adb server 和 adb daemon 之间的通信协议的库,使用 Kotlin 编写。支持 PC 端直接连接 Android 设备操作 adb 相关的指令。


github 地址:github.com/fengzhizi71…


二. 背景


在下图中的 adb client 和 adb server 都共存在 PC 中,PC 上安装过 adb 程序就会有。adb dameon (简称adbd)是存在于 Android 手机中的一个进程/服务。


当我们启动命令行输入 adb 命令时,实际上使用的是 adb client,它会跟 PC 本地的 adb server 进行通信(当然,有一个前提先要使用 adb-start 启动 adb server 才能调用 adb 命令)。


然后 adb server 会跟 Android 手机进行通信(手机通过 usb 口连上电脑)。最终,我们会看到 adb client 返回命令执行的结果。


一次命令的执行包含了 1-6 个步骤。其实,还要算上 adb server 内部的通信和 adb dameon 内部的通信。一次命令的执行,路径会很长。


所以,一次完整的 adb 通信会涉及如下的概念:



  • adb client:运行在 PC 上,通过在命令行执行 adb,就启动了 adb client 程序

  • adb server:运行于 PC 的后台进程,用于管理 adb client 和 daemon 之间的通信

  • adb daemon(adbd):运行在模拟器或 Android 设备上的后台服务。当 Android 系统启动时,由 init 程序启动 adbd。如果 adbd 挂了,则 adbd 会由 init 重新启动。

  • DDMS:DDMS 将 IDE 和手机设备之间建立起了一座桥梁,可以很方面的查看到目标机器上的信息。

  • JDWP:即 java debug wire protocol,Java 调试线协议,是一个为 Java 调试而设计的通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。


adb-connector.png


在 adb server 和 adbd 之间有一个 TCP 的传输协议,它定义在 Android 源码的 system/core/adb/protocol.txt 文件中。只要是能通过 adb 命令连接的手机,都会遵循这个协议,无论是 Android 或是鸿蒙系统。


因此,基于这个协议实现了一个 TCP 的客户端(adbd-connector)就可以跟手机的 adbd 服务/进程进行通信,从而实现 adb 的所有指令。


另外,我还使用 Kotlin Compose Desktop 在这个协议上做了一层 UI,实现了一个可以在 PC 上使用的简易"手机助手",且支持 Mac、Linux、Windows 等系统。



在手机连接前,先要打开手机的开发者模式。在连接过程中,手机会弹出信任框,提示是否允许 usb 调试。需要点击信任,才能完成后续的连接。
还要打开手机的 5555 端口(使用 adb 命令:adb tcpip 5555),以及获取手机连接当前 wifi 的局域网 ip 地址。有了局域网的 ip 地址和端口,才可以通过 adbd-connector 跟 adbd 进行连接。



三. adbd-connector 使用


3.1 手机的连接效果:


连上手机,获取手机的基础信息


1.png


3.2 执行 adb shell 命令的效果:


执行 adb shell 相关的命令(输入时省略 adb shell,直接输入后面的命令)


2.png


3.png


3.3 install app


目前还处在疫情期间,所以就在应用宝上找了一个跟生活相关的 App,最主要是这个 App 体积小


安装 App 时分成2步:


1.使用 push 命令将 apk 推送到手机的目录 /data/local/tmp/ 下


4.png



  1. 使用 adb shell pm install 命令安装 apk


5.png


6.png


3.4 uninstall app


adb shell pm uninstall 包名


7.png


四. 总结


这款工具 github.com/fengzhizi71… 是一个 PoC(Proof of Concept)的产物,参考了很多开源项目,特别是 github.com/sunshinex/a…


它能够实现绝大多数的 adb 命令。后续这个项目的部分代码可能会用于公司的项目。所以,这个仓库不一定会持续更新了。而且,这款工具使用起来也很繁琐,需要打开手机的 5555 端口以及输入手机局域网 ip 的地址。因此在实际业务中,还有很多东西需要改造以适合自身的业务。


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

骨灰级程序员那些年曾经告诉我们的高效学习的态度

一、背景 近日听闻某骨灰级程序员突然与世长辞,记得再2019年的时候有幸读到他的专栏,专栏中关于如何高效学习的总结让我收货颇丰,老师说: 学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,会让人感到痛苦,并随时想找理由放弃。 大部分人都认为自己爱学习...
继续阅读 »

一、背景


近日听闻某骨灰级程序员突然与世长辞,记得再2019年的时候有幸读到他的专栏,专栏中关于如何高效学习的总结让我收货颇丰,老师说:


学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,会让人感到痛苦,并随时想找理由放弃。


大部分人都认为自己爱学习,但是:



  • 他们都是只有意识没有行动,他们是动力不足的人。

  • 他们都不知道自己该学什么,他们缺乏方向和目标。

  • 他们都不具备自主学习的能力,没有正确的方法和技能。

  • 更要命的是,他们缺乏实践和坚持。


对于学习首先需要做的是端正态度,如果不能自律,不能坚持,不能举一反三,不能不断追问等,那么,无论有多好的方法,你都不可能学好。所以,有正确的态度很重要。



二、 主动学习和被动学习


老师关于主动学习和被动学习的见解也让我们技术人员收货颇丰。


1946 年,美国学者埃德加·戴尔(Edgar Dale)提出了「学习金字塔」(Cone of Learning)的理论。之后,美国缅因州国家训练实验室也做了相同的实验,并发布了「学习金字塔」报告。



人的学习分为「被动学习」和「主动学习」两个层次。



  • 被动学习:如听讲、阅读、视听、演示,学习内容的平均留存率为 5%、10%、20% 和 30%。

  • 主动学习:如通过讨论、实践、教授给他人,会将原来被动学习的内容留存率从 5% 提升到 50%、75% 和 90%。


这个模型很好地展示了不同学习深度和层次之间的对比。


我们可以看到,你听别人讲,或是自己看书,或是让别人演示给你,这些都不能让你真正获得学习能力,因为你是在被别人灌输,在听别人说。


你开始自己思考,开始自己总结和归纳,开始找人交流讨论,开始践行,并开始对外输出,你才会掌握到真正的学习能力。


所以,学习不是努力读更多的书,盲目追求阅读的速度和数量,这会让人产生低层次的勤奋和成长的感觉,这只是在使蛮力。要思辨,要践行,要总结和归纳,否则,你只是在机械地重复某件事,而不会有质的成长的。



好多书中也这么说:



这里推荐阅读下这本书:



三、深度学习和浅度学习


对于当前这个社会:



  • 大多数人的信息渠道都被微信朋友圈、微博、知乎、今日头条、抖音占据着。这些信息渠道中有营养的信息少之又少。

  • 大多数公司都是实行类似于 996 这样的加班文化,在透支和消耗着下一代年轻人,让他们成长不起来。

  • 因为国内互联网访问不通畅,加上英文水平受限,所以,大多数人根本没法获取到国外的第一手信息。

  • 快餐文化盛行,绝大多数人都急于速成,心态比较浮燥,对事物不求甚解。


所以,在这种环境下,你根本不需要努力的。你只需要踏实一点,像以前那样看书,看英文资料,你只需要正常学习,根本不用努力,就可以超过你身边的绝大多数人。


在这样一个时代下,种种迹象表明,快速、简单、轻松的方式给人带来的快感更强烈,而高层次的思考、思辨和逻辑则被这些频度高的快餐信息感所弱化。于是,商家们看到了其中的商机,看到了如何在这样的时代里怎么治愈这些人在学习上的焦虑,他们在想方设法地用一些手段推出各种代读、领读和听读类产品,让人们可以在短时间内体会到轻松获取知识的快感,并产生勤奋好学和成长的幻觉。


这些所谓的“快餐文化”可以让你有短暂的满足感,但是无法让你有更深层次的思考和把知识转换成自己的技能的有效路径,因为那些都是需要大量时间和精力的付出,不符合现代人的生活节奏。人们开始在朋友圈、公众号、得到等这样的地方进行学习,导致他们越学越焦虑,越学越浮燥,越学越不会思考。于是,他们成了“什么都懂,但依然过不好这一生”的状态。


只要你注意观察,就会发现,少数的精英人士,他们在训练自己获取知识的能力,他们到源头查看第一手的资料,然后,深度钻研,并通过自己的思考后,生产更好的内容。而绝大部分受众享受轻度学习,消费内容。你有没有发现,在知识的领域也有阶层之分,那些长期在底层知识阶层的人,需要等着高层的人来喂养,他们长期陷于各种谣言和不准确的信息环境中,于是就导致错误或幼稚的认知,并习惯于那些不费劲儿的轻度学习方式,从而一点点地丧失了深度学习的独立思考能力,从而再也没有能力打破知识阶层的限制,被困在认知底层翻不了身。


四、如何深度学习


有4点最关键:



  • 高质量的信息源和第一手的知识。

  • 把知识连成地图,将自己的理解反述出来。

  • 不断地反思和思辨,与不同年龄段的人讨论。

  • 举一反三,并践行之,把知识转换成技能。


从以上4点来说,学习会有三个步骤。


知识采集。信息源是非常重要的,获取信息源头、破解表面信息的内在本质、多方数据印证,是这个步骤的关键。


知识缝合。所谓缝合就是把信息组织起来,成为结构体的知识。这里,连接记忆,逻辑推理,知识梳理是很重要的三部分。


技能转换。通过举一反三、实践和练习,以及传授教导,把知识转化成自己的技能。这种技能可以让你进入更高的阶层。


我觉得这是任何人都是可以做到的,就是看你想不想做了。


五、学习的目的



5.1、学习是为了找到方法


学习不仅仅是为了找到答案,而更是为了找到方法。很多时候,尤其是中国的学生,他们在整个学生时代都喜欢死记硬背,因为他们只有一个 KPI,那就是在考试中取得好成绩,所以,死记硬背或题海战术成了他们的学习习惯。然而,在知识的海洋中,答案太多了,你是记不住那么多答案的。


只有掌握解题的思路和方法,你才算得上拥有解决问题的能力。所有的练习,所有的答案,其实都是在引导你去寻找一种“以不变应万变”的方法或能力。在这种能力下,你不需要知道答案,因为你可以用这种方法很快找到答案,找到解,甚至可以通过这样的方式找到最优解或最优雅的答案。


这就好像,你要去登一座山,一种方法是通过别人修好的路爬上去,一种是通过自己的技能找到路(或是自己修一条路)爬上去。也就是说,需要有路才爬得上山的人,和没有路能造路的人相比,后者的能力就会比前者大得多得多。所以,学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。


5.2、学习是为了找到原理


学习不仅仅是为了知道,而更是为了思考和理解。在学习的过程中,我们不是为了知道某个事的表面是什么,而是要通过表象去探索其内在的本质和原理。真正的学习,从来都不是很轻松的,而是那种你知道得越多,你的问题就会越多,你的问题越多,你就会思考得越多,你思考得越多,你就会越觉得自己知道得越少,于是你就会想要了解更多。如此循环,是这么一种螺旋上升上下求索的状态。


但是,这种循环,会在你理解了某个关键知识点后一下子把所有的知识全部融会贯通,让你赫然开朗,此时的那种感觉是非常美妙而难以言语的。在学习的过程中,我们要不断地问自己,这个技术出现的初衷是什么?是要解决什么样的问题?为什么那个问题要用这种方法解?为什么不能用别的方法解?为什么不能简单一些?


这些问题都会驱使你像一个侦探一样去探索背后的事实和真相,并在不断的思考中一点一点地理解整个事情的内在本质、逻辑和原理。一旦理解和掌握了这些本质的东西,你就会发现,整个复杂多变的世界在变得越来越简单。你就好像找到了所有问题的最终答案似的,一通百通了。


5.3、学习是为了了解自己


学习不仅仅是为了开拓眼界,而更是为了找到自己的未知,为了了解自己。英文中有句话叫:You do not know what you do not know,可以翻译为:你不知道你不知道的东西。也就是说,你永远不会去学习你不知道其存在的东西。就好像你永远 Google 不出来你不知道的事,因为对于你不知道的事,你不知道用什么样的关键词,你不知道关键词,你就找不到你想要的知识。


这个世界上有很多东西是你不知道的,所以,学习可以让你知道自己不知道的东西。只有当我们知道有自己不知道的东西,我们才会知道我们要学什么。所以,我们要多走出去,与不同的人交流,与比自己聪明的人共事,你才会知道自己的短板和缺失,才会反过来审视和分析自己,从而明白如何提升自己。


山外有山,楼外有楼,人活着最怕的就是坐井观天,自以为是。因为这样一来,你的大脑会封闭起来,你会开始不接受新的东西,你的发展也就到了天花板。开拓眼界的目的就是发现自己的不足和上升空间,从而才能让自己成长。


5.4、学习是为了改变自己


学习不仅仅是为了成长,而更是为了改变自己。很多时候,我们觉得学习是为了自己的成长,但是其实,学习是为了改变自己,然后才能获得成长。为什么这么说呢?我们知道,人都是有直觉的,但如果人的直觉真的靠谱,那么我们就不需要学习了。而学习就是为了告诉我们,我们的很多直觉或是思维方式是不对的,不好的,不科学的。


只有做出了改变后,我们才能够获得更好的成长。你可以回顾一下自己的成长经历,哪一次你有质的成长时,不是因为你突然间开窍了,开始用一种更有效率、更科学、更系统的方式做事,然后让你达到了更高的地方。不是吗?当你学习了乘法以后,在很多场景下,就不需要用加法来统计了,你可以使用乘法来数数,效率提升百倍。


当你有一天知道了逻辑中的充要条件或是因果关系后,你会发现使用这样的方式来思考问题时,你比以往更接近问题的真相。学习是为了改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。总之,学习让我们改变自己,行动和践行,反思和改善,从而获得成长。


六、总结


**1、首先,学习是一件“逆人性”的事,就像锻炼身体一样,需要人持续付出,但会让人痛苦,并随时可能找理由放弃。如果你不能克服自己 DNA 中的弱点,不能端正自己的态度,不能自律,不能坚持,不能举一反三,不能不断追问等,那么,无论有多好的方法,你都不可能学好。因此,有正确的态度很重要。

**


2、要拥有正确的学习观念:学习不仅仅是为了找到答案,而更是为了找到方法;学习不仅仅是为了知道,而更是为了思考和理解;学习不仅仅是为了开拓眼界,而更是为了找到自己的未知,为了了解自己;学习不仅仅是为了成长,而更是为了改变自己,改变自己的思考方式,改变自己的思维方式,改变自己与生俱来的那些垃圾和低效的算法。


喜欢的分享、转发、收藏、评论、关注哦


作者:爱海贼的无处不在
来源:juejin.cn/post/7233240426312007737
收起阅读 »

世界那么大,我并不想去看看

回家自由职业有一个半月了,这段时间确实过的挺开心的。 虽然收入不稳定,但小册和公众号广告的收入也足够生活。 我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。 一...
继续阅读 »

回家自由职业有一个半月了,这段时间确实过的挺开心的。


虽然收入不稳定,但小册和公众号广告的收入也足够生活。


我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。


一年就是 36500 元。


加上其他的支出,一年生活费差不多 5w 元。


10 年是 50w,40 年就是 200 万元。


而且还有利息,也就是说,我这辈子有 200 万就能正常活到去世了。


我不会结婚,因为我喜欢一个人的状态,看小说、打游戏、自己想一些事情,不用迁就另一个人的感受,可以完全按照自己的想法来生活。


并不是说我现在有 200 万了,我的意思是只要在二三十年内赚到这些就够了。


我在大城市打工,可能一年能赚 50 万,在家里自由职业估计一年也就 20 万,但这也足够了。


而且自由职业能赚到最宝贵的东西:时间。


一方面是我自己的时间,我早上可以晚点起、晚上可以晚点睡、下午困了可以睡个午觉,可以写会东西打一会游戏,不想干的时候可以休息几天。


而且我有大把的时间可以来做自己想做的事情,创造自己的东西。


一方面是陪家人的时间,自从长大之后,明显感觉回家时间很少了,每年和爸妈也就见几天。


前段时间我爸去世,我才发觉我和他最多的回忆还是在小时候在家里住的时候。


我回家这段时间,每天都陪着我妈,一起做饭、吃饭,一起看电视,一起散步,一起经历各种事情。


我买了个投影仪,很好用:



这段时间我们看完了《皓镧传》、《锦绣未央》、《我的前半生》等电视剧,不得不说,和家人一起看电视剧确实很快乐、很上瘾。


再就是我还养了只猫,猫的寿命就十几年,彼此陪伴的时间多一点挺好的:



这些时间值多少钱?没法比较。


回家这段时间我可能接广告多了一点,因为接一个广告能够我好多天的生活费呢。


大家不要太排斥这个,可以忽略。


其实我每次发广告总感觉对不起关注我的人,因为那些广告标题都要求起那种博人眼球的,不让改,就很难受。



小册的话最近在写 Nest.js 的,但不只是 nest。


就像学 java,我们除了学 spring 之外,还要学 mysql、redis、mongodb、rabbitmq、kafka、elasticsearch 等中间件,还有 docker、docker compose、k8s 等容器技术。


学任何别的后端语言或框架,也是这一套,Nest.js 当然也是。


所以我会在 Nest.js 小册里把各种后端中间件都讲一遍,然后会做 5 个全栈项目。


写完估计得 200 节,大概会占据我半年的时间。


这段时间也经历过不爽的事情:





这套房子是我爸还在的时候,他看邻居在青岛买的房子一周涨几十多万,而且我也提到过可能回青岛工作,然后他就非让我妈去买一套。


当时 18 年青岛限购,而即墨刚撤市划区并入青岛,不限购,于是正好赶上房价最高峰买的。


然而后来并没有去住。


这套房子亏了其实不止 100 万。


因为银行定存利息差不多 4%,200 万就是每年 8万利息,5年就是 40万。


但我也看开了,少一百万多一百万对我影响大么?


并不大,我还是每天花那些钱。


相比之下,我爸的去世对我的打击更大,这对我的影响远远大于 100 万。


我对钱没有太大的追求,对很多别人看重的东西也没啥追求。


可能有的人有了钱,有了时间会选择环游中国,环游世界,我想我不会。


我就喜欢宅在家里,写写东西、看看小说、打打游戏,这样活到去世我也不会有遗憾。


我所追求的事情,在我小时候可能是想学医,一直觉得像火影里的纲手那样很酷,或者像大蛇丸那样研究一些东西也很酷。


但近些年了解了学医其实也是按照固定的方法来治病,可能也是同样的东西重复好多年,并不是我想的那样。


人这一辈子能把一件事做好就行。


也就是要找到自己一生的使命,我觉得我找到了:我想写一辈子的技术文章。


据说最高级的快乐有三种来源:自律、爱、创造。


写文章对我来说就很快乐,我想他就是属于创造的那种快乐。


此外,我还想把我的故事写下来,我成长的故事,我和东东的故事,那些或快乐或灰暗的日子,今生我一定会把它们写下来,只是现在时机还不成熟。


世界那么大,我并不想去看看。


我只想安居一隅,照顾好家人,写一辈子的技术文章,也写下自己的故事。


这就是我的平凡之路。


作者:zxg_神说要有光
来源:juejin.cn/post/7233327509919547452
收起阅读 »

快到 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 年下半年就从腾讯课堂调到了企鹅辅导,从一组调到三组,并开始做一些团队管理的工作。整体而言我的管理经验成长的非常缓慢,这是我自己的结论。


首先,我的角色转变比较缓慢。经常看到小伙伴们做事情太“慢”,我都忍不住要自己上,或者直接告诉他们答案,我知道这很不好,但是初期的我就是忍不住,我感觉我的管理之路就是憋气之路,最后总结就是,在大方向上,我要站出来,但是具体实施层面,我要当个隐身人,这对我来说,非常难受。


其次,我是主猫头鹰次考拉的重事风格,不太擅长管理小伙伴的情绪还有激励,沟通和语言艺术真是我需要投入一生去学习锻炼的课程。另外,我有一个最大的问题就是不喜欢冲突,直接导致我不太擅长争取资源,这会让我觉得很对不起小伙伴们,这点也是我离开腾讯最大的原因吧。感觉我比较适合增量市场,在存量市场这点真的是致命的,不过专心搞好业务不挺好吗,何苦浪费时间在这些地方。

作者:潜龙在渊灬
来源:juejin.cn/post/7189612924116140092

收起阅读 »

Vue项目打包优化

web
最近做完了一个项目,但是打包之后发现太大了,记录一下优化方案 Element、Vant 等组件库按需加载 静态资源使用cdn进行引入 开启gzip压缩 路由懒加载 #首先看看啥也没做时打包的大小 可以使用 webpack-bundle-analyzer 插...
继续阅读 »

最近做完了一个项目,但是打包之后发现太大了,记录一下优化方案



  • Element、Vant 等组件库按需加载

  • 静态资源使用cdn进行引入

  • 开启gzip压缩

  • 路由懒加载


#首先看看啥也没做时打包的大小


可以使用 webpack-bundle-analyzer 插件在打包时进行分析



 


可以看到有2.5M的大小,下面就进行优化


Element、Vant 等组件库按需加载


可以看到,在打包的文件中,占据最大比例的是这两个组件库,我们可以使用按需加载来减小 按需加载按照官方文档来就行,需要注意配置bebel.config.js


  // 在bebel.config.js的plugins选项中配置    
["component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ],
["import", { "libraryName": "vant", "libraryDirectory": "es", "style": true }, 'vant']

配置后的大小,可以明显的看到有减小



静态资源使用cdn进行引入


接下来占比最大的就是一些可以通过cdn进行引入的静态资源了


设置例外


vue.config.js文件中进行设置例外,不进行打包


设置例外的时候 key:value 中key的值为你引入的库的名字,value值为这个库引入的时候挂在window上的属性


    config.externals({
"vue":'Vue',
"vue-wordcloud":'WordCloud',
"@wangeditor/editor-for-vue":"WangEditorForVue",
})

然后在项目的 public/index.html 文件中进行cdn引入


cdn的话有挺多种的,比如bootcdnjsdelivr等,推荐使用jsdelivrnpm官方就是使用的这个


    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.runtime.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-wordcloud@1.1.1/dist/word-cloud.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@wangeditor/editor-for-vue@1/dist/index.js"></script>

完成后



gzip压缩


开启gzip压缩需要使用插件,可以用compression-webpack-plugin


vue.config.js文件中进行配置


chainWebpack: config => {
config.optimization.minimize(true)
},
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
return {
plugins: [
new CompressionPlugin({
test: /\.js$|\.css$|\.html$/,
algorithm: 'gzip',
threshold: 10240,
deleteOriginalAssets: false,
})
]
}
}
}

完成后



在配置完成后,在本地并不能开启使用,需要配置Nginx进行支持才行


路由懒加载


路由懒加载的基础是webpackMagic Comments 官方文档


    // 在路由配置中,通过下面这种方式导入对应组件
component: () => import(/* webpackChunkName: "mColumn" */ '../mviews/ColumnView.vue')

完成后


路由懒加载并不会减小文件打包后的大小,但是可以让文件分为不同的模块,当路由跳转时,才加载当前路由对应的组件


这样就可以大大减少首屏白屏的时间,不用在第一次进入的时候就加载全部的文件



如图,当第一次进入的时候只会加载到home.xx.js,当进入另一个路由的时候就会去加载对应组件,如图中的socket.xx.js


作者:羞男
来源:juejin.cn/post/7224036517139939386
收起阅读 »

Vue 必备的这些操作技巧

web
🎈 键盘事件 在 js 中我们通常通过绑定一个事件,去获取按键的编码,再通过 event 中的 keyCode 属性去获得编码 如果我们需要实现固定的键才能触发事件时就需要不断的判断,其实很麻烦 let button = document.querySel...
继续阅读 »

🎈 键盘事件



  • js 中我们通常通过绑定一个事件,去获取按键的编码,再通过 event 中的 keyCode 属性去获得编码

  • 如果我们需要实现固定的键才能触发事件时就需要不断的判断,其实很麻烦


let button = document.querySelector('button')

button.onkeyup = function (e) {
console.log(e.key)
if (e.keyCode == 13) {
console.log('我是回车键')
}
}


  • vue 中给一些常用的按键提供了别名,我们只要在事件后加上响应的别名即可

  • vue 中常见别名有:up/向上箭头down/向下箭头left/左箭头right/右箭头space/空格tab/换行esc/退出enter/回车delete/删除


// 只有按下回车键时才会执行 send 方法
<input v-on:keyup.enter="send" type="text">


  • 对于 Vue 中未提供别名的键,可以使用原始的 key 值去绑定,所谓 key 值就是 event.key 所获得的值

  • 如果 key 值是单个字母的话直接使用即可,如果是由多个单词组成的驼峰命名,就需要将其拆开,用 - 连接


// 只有按下q键时才会执行send方法
<input v-on:keyup.Q="send" type="text">

// 只有按下capslock键时才会执行send方法
<input v-on:keyup.caps-lock="send" type="text">


  • 对于系统修饰符 ctrlaltshift 这些比较复杂的键使用而言,分两种情况

  • 因为这些键可以在按住的同时,去按其他键,形成组合快捷键

  • 当触发事件为 keydown 时,我们可以直接按下修饰符即可触发

  • 当触发事件为 keyup 时,按下修饰键的同时要按下其他键,再释放其他键,事件才能被触发。


// keydown事件时按下alt键时就会执行send方法
<input v-on:keydown.Alt="send" type="text">

// keyup事件时需要同时按下组合键才会执行send方法
<input v-on:keyup.Alt.y="send" type="text">


  • 当然我们也可以自定义按键别名

  • 通过 Vue.config.keyCodes.自定义键名=键码 的方式去进行定义


// 只有按下回车键时才会执行send方法
<input v-on:keydown.autofelix="send" type="text">

// 13是回车键的键码,将他的别名定义为autofelix
Vue.config.keyCodes.autofelix=13

🎈 图片预览



  • 在项目中我们经常需要使用到图片预览,viewerjs 是一款非常炫酷的图片预览插件

  • 功能支持包括图片放大、缩小、旋转、拖拽、切换、拉伸等

  • 安装 viewerjs 扩展


npm install viewerjs --save


  • 引入并配置功能


//引入
import Vue from 'vue';
import 'viewerjs/dist/viewer.css';
import Viewer from 'v-viewer';

//按需引入
Vue.use(Viewer);

Viewer.setDefaults({
'inline': true,
'button': true, //右上角按钮
"navbar": true, //底部缩略图
"title": true, //当前图片标题
"toolbar": true, //底部工具栏
"tooltip": true, //显示缩放百分比
"movable": true, //是否可以移动
"zoomable": true, //是否可以缩放
"rotatable": true, //是否可旋转
"scalable": true, //是否可翻转
"transition": true, //使用 CSS3 过度
"fullscreen": true, //播放时是否全屏
"keyboard": true, //是否支持键盘
"url": "data-source",
ready: function (e) {
console.log(e.type, '组件以初始化');
},
show: function (e) {
console.log(e.type, '图片显示开始');
},
shown: function (e) {
console.log(e.type, '图片显示结束');
},
hide: function (e) {
console.log(e.type, '图片隐藏完成');
},
hidden: function (e) {
console.log(e.type, '图片隐藏结束');
},
view: function (e) {
console.log(e.type, '视图开始');
},
viewed: function (e) {
console.log(e.type, '视图结束');
// 索引为 1 的图片旋转20度
if (e.detail.index === 1) {
this.viewer.rotate(20);
}
},
zoom: function (e) {
console.log(e.type, '图片缩放开始');
},
zoomed: function (e) {
console.log(e.type, '图片缩放结束');
}
})


  • 使用图片预览插件

  • 单个图片使用


<template>
<div>
<viewer>
<img :src="cover" style="cursor: pointer;" height="80px">
</viewer>
</div>
</template>


<script>
export default {
data() {
return {
cover: "//www.autofelix.com/images/cover.png"
}
}
}
</script>



  • 多个图片使用


<template>
<div>
<viewer :images="imgList">
<img v-for="(imgSrc, index) in imgList" :key="index" :src="imgSrc" />
</viewer>
</div>

</template>

<script>
export default {
data() {
return {
imgList: [
"//www.autofelix.com/images/pic_1.png",
"//www.autofelix.com/images/pic_2.png",
"//www.autofelix.com/images/pic_3.png",
"//www.autofelix.com/images/pic_4.png",
"//www.autofelix.com/images/pic_5.png"
]
}
}
}
</script>


🎈 跑马灯



  • 这是一款好玩的特效技巧

  • 比如你在机场接人时,可以使用手机跑马灯特效,成为人群中最靓的仔

  • 跑马灯特效其实就是将最前面的文字删除,添加到最后一个,这样就形成了文字移动的效果


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

<head>
<meta charset="UTF-8">
<title>跑马灯</title>
<style type="text/css">
#app {
padding: 20px;
}
</style>
</head>

<body>
<div id="app">
<button @click="run">应援</button>
<button @click="stop">暂停</button>
<h3>{{ msg }}</h3>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.0/dist/vue.min.js"></script>
<script>
new Vue({
el: "#app",
data: {
msg: "飞兔小哥,飞兔小哥,我爱飞兔小哥~~~",
timer: null // 定时器
},
methods: {
run() {
// 如果timer已经赋值就返回
if (this.timer) return;

this.timer = setInterval(() => {
// msg分割为数组
var arr = this.msg.split('');
// shift删除并返回删除的那个,push添加到最后
// 把数组第一个元素放入到最后面
arr.push(arr.shift());
// arr.join('')吧数组连接为字符串复制给msg
this.msg = arr.join('');
}, 100)
},
stop() {
//清除定时器
clearInterval(this.timer);
//清除定时器之后,需要重新将定时器置为null
this.timer = null;
}
}
})
</script>

</html>

🎈 倒计时



  • 对于倒计时技巧,应用的地方很多

  • 比如很多抢购商品的时候,我们需要有一个倒计时提醒用户开抢时间

  • 其实就是每隔一秒钟,去重新计算一下时间,并赋值到 DOM


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

<head>
<meta charset="UTF-8">
<title>倒计时</title>
</head>

<body>
<div id="app">
<div>抢购开始时间:{{count}}</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.0/dist/vue.min.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: '', //倒计时
seconds: 864000 // 10天的秒数
}
},
mounted() {
this.Time() //调用定时器
},
methods: {
// 天 时 分 秒 格式化函数
countDown() {
let d = parseInt(this.seconds / (24 * 60 * 60))
d = d < 10 ? "0" + d : d
let h = parseInt(this.seconds / (60 * 60) % 24);
h = h < 10 ? "0" + h : h
let m = parseInt(this.seconds / 60 % 60);
m = m < 10 ? "0" + m : m
let s = parseInt(this.seconds % 60);
s = s < 10 ? "0" + s : s
this.count = d + '天' + h + '时' + m + '分' + s + '秒'
},
//定时器没过1秒参数减1
Time() {
setInterval(() => {
this.seconds -= 1
this.countDown()
}, 1000)
},
}
})
</script>

</html>

🎈 自定义右键菜单



  • 在项目中,我们有时候需要自定义鼠标右键出现的选项,而不是浏览器默认的右键选项

  • 对于如何实现右键菜单,在 Vue 中其实很简单,只要使用 vue-contextmenujs 插件即可

  • 安装 vue-contextmenujs 插件


npm install vue-contextmenujs


  • 引入


//引入
import Vue from 'vue';
import Contextmenu from "vue-contextmenujs"

Vue.use(Contextmenu);


  • 使用方法

  • 可以使用 <i class="icon"></i> 可以给选项添加图标

  • 可以使用 style 标签自定义选项的样式

  • 可以使用 disabled 属性禁止选项可以点击

  • 可以使用 divided:true 设置选项的下划线

  • 可以使用 children 设置子选项


<style>
.custom-class .menu_item__available:hover,
.custom-class .menu_item_expand {
background: lightblue !important;
color: #e65a65 !important;
}
</style>


<template>
<div style="width:100vw;height:100vh" @contextmenu.prevent="onContextmenu"></div>
</template>


<script>
import Vue from 'vue'
import Contextmenu from "vue-contextmenujs"
Vue.use(Contextmenu);

export default {
methods: {
onContextmenu(event) {
this.$contextmenu({
items: [
{
label: "返回",
onClick: () => {
// 添加点击事件后的自定义逻辑
}
},
{ label: "前进", disabled: true },
{ label: "重载", divided: true, icon: "el-icon-refresh" },
{ label: "打印", icon: "el-icon-printer" },
{
label: "翻译",
divided: true,
minWidth: 0,
children: [{ label: "翻译成中文" }, { label: "翻译成英文" }]
},
{
label: "截图",
minWidth: 0,
children: [
{
label: "截取部分",
onClick: () => {
// 添加点击事件后的自定义逻辑
}
},
{ label: "截取全屏" }
]
}
],
event, // 鼠标事件信息
customClass: "custom-class", // 自定义菜单 class
zIndex: 3, // 菜单样式 z-index
minWidth: 230 // 主菜单最小宽度
});
return false;
}
}
};
</script>


🎈 打印功能



  • 对于网页支持打印功能,在很多项目中也比较常见

  • 而 Vue 中使用打印功能,可以使用 vue-print-nb 插件

  • 安装 vue-print-nb 插件


npm install vue-print-nb --save


  • 引入打印服务


import Vue from 'vue'
import Print from 'vue-print-nb'
Vue.use(Print);


  • 使用

  • 使用 v-print 指令即可启动打印功能


<div id="printStart">
<p>红酥手,黄縢酒,满城春色宫墙柳。</p>
<p>东风恶,欢情薄。</p>
<p>一怀愁绪,几年离索。</p>
<p>错、错、错。</p>
<p>春如旧,人空瘦,泪痕红浥鲛绡透。</p>
<p>桃花落,闲池阁。</p>
<p>山盟虽在,锦书难托。</p>
<p>莫、莫、莫!</p>
</div>
<button v-print="'#printStart'">打印</button>

🎈 JSONP请求



  • jsonp解决跨域 的主要方式之一

  • 所以学会在 vue 中使用 jsonp 其实还是很重要的

  • 安装 jsonp 扩展


npm install vue-jsonp --save-dev


  • 注册服务


// 在vue2中注册服务
import Vue from 'vue'
import VueJsonp from 'vue-jsonp'
Vue.use(VueJsonp)

// 在vue3中注册服务
import { createApp } from 'vue'
import App from './App.vue'
import VueJsonp from 'vue-jsonp'
createApp(App).use(VueJsonp).mount('#app')


  • 使用方法

  • 需要注意的是,在使用 jsonp 请求数据后,回调并不是在 then 中执行

  • 而是在自定义的 callbackName 中执行,并且需要挂载到 window 对象上


<script>
export default {
data() {...},
created() {
this.getUserInfo()
},
mounted() {
window.jsonpCallback = (data) => {
// 返回后回调
console.log(data)
}
},
methods: {
getUserInfo() {
this.$jsonp(this.url, {
callbackQuery: "callbackParam",
callbackName: "jsonpCallback"
})
.then((json) => {
// 返回的jsonp数据不会放这里,而是在 window.jsonpCallback
console.log(json)
})
}
}
}
</script>


正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿


作者:极客飞兔
来源:juejin.cn/post/7121564385151025165

收起阅读 »

24 岁技术人不太平凡的一年

一年前,我整夜混迹于魔都淮海中路的酒吧迪厅,而白天,在外滩海景餐厅吃着四位数一顿的西餐。租住的洋房里陈列着各种奢侈品…… 一年后,我结婚了,并且买了套房子。 一、魔都 2021 年,是我爸妈非常兴奋的一年。因为那年我去了上海,找了一份年薪 60 几万的工作。他...
继续阅读 »

一年前,我整夜混迹于魔都淮海中路的酒吧迪厅,而白天,在外滩海景餐厅吃着四位数一顿的西餐。租住的洋房里陈列着各种奢侈品……


一年后,我结婚了,并且买了套房子。


一、魔都


2021 年,是我爸妈非常兴奋的一年。因为那年我去了上海,找了一份年薪 60 几万的工作。他们可以和邻居亲戚们炫耀他们的儿子了。



但我没什么感觉,对我而言,60 万只是手机上的一串数字。而且我知道,60 万并不多。因为那个整日听我汇报的、大腹便便的、看上去不怎么聪明的中年人拿着远比我高的薪水,我和他都一样,只是一条资本家的狗而已。


我一向不打算结婚,也不打算要孩子,也就意味着我不需要存钱。


我不会像我的领导那样为了在上海买一套 1200 万的房子而过得那样抠搜。我可以花一万多租一个小洋房,我可以在家里塞满那些昂贵的电器,我可以每周去国金、恒隆、新天地消费。我会买最新款的苹果全家桶,甚至连手机壳都要买 Lv 的。


但即使这样,钱还是花不完。


别人都因为没钱而烦恼,而我却因为钱花不完而烦恼。


后来,在机缘巧合之下,我开始了绚丽多彩的夜生活。在夜店,只需要花 1 万块钱,就可以当一晚上的王。这种快感是难以描述的。我成了 Myst、LolaClub、Ak、wm、pops 们的常客。开始了一段纸醉金迷的日子。


二、求职


找工作是一件再容易不过的事,因为几乎所有人都有工作。人只要降低要求,就一定能够找到工作。找工作难,是因为你渴望的工作岗位所需要的能力与你当前的能力并不匹配。


刚来到上海的时候,我非常自信,整天到处闲逛。朋友问我为什么不准备面试题?我说没有这种必要,他说不是很理解,我说那算了,不用理解。


因为技术上的知识,就那么多。你有自己的理解,有自己的经验,完全不需要胆怯,也没必要胆怯。合适就去,不合适就再看看。


能否找到好工作,更多的是你和面试官是否聊得来;你所做的事情,是否和这家公司的业务所匹配;你的经历、价值观和这家公司的愿景使命价值观是否契合。


我清楚我的能力究竟如何,我知道我应该去哪种公司。


但我需要了解这些公司的不同。于是我投了很多简历,大小公司都有。像携程、美团、得物、拼多多、小红书、微盟这些公司基本上都有去面试。跑了很多地方,徐家汇、北外滩、五角场、陆家嘴、奉贤岛、静安寺……。


全职面试,一天四五场,很忙。


但不到一周就确定好了 offer。


朋友说我是一个果断的人,我只是觉得没必要消耗时间。


明白自己的能力上限在哪,懂得在合适的环境最大化释放自己的价值。就一定可以收获一份合适的工作,或者说是一份事业。


找到自己的使命,就可战无不胜。


三、爱情


人缺什么,就会去寻找什么。


在 24 岁的年纪里,我缺少的不是梦想、金钱或者使命感,而是爱情。


我遇到了 Emma,她带给我的滋味,与我所有的想象都不一样。


我在上海,她在三亚。我们在相隔千里的城市,我坐着飞机去找她。结果因为晚了几分钟,于是我买了张全价票,硬是要在当天飞过去,因为我等这一天已经等了半个月了。


我们住在三亚的希尔顿。在三亚有两个希尔顿,结果她找错了,去了另一个希尔顿,后来我们很庆幸幸亏没有人开门。


虽然 Emma 笨笨的,但我喜欢她这样。


我们去了亚龙湾、天涯海角和南海观音。


三亚虽然是中国最南方的岛最南侧的城市,但有个外号是东北第四城。前三个是黑龙江、吉林和辽宁。一个城市的名字可能是错的,但外号绝对不会错。海南到处都是东北人。


爱情刚开始都会轰轰烈烈,我会给 Emma 精心挑选手链,去国金帮她挑选面膜,亲手给她写情书,那些情书至今还在我们的衣柜里。后来去全上海只有两个店的 Graff 买了一对情侣对戒。


我不介意给心爱的女人花钱。我觉得,一个男人把钱都花在哪儿了,就知道这个男人的心在哪儿。


当然,认识久了,特别是同居之后,这种轰轰烈烈式的浪漫就会褪去,更多的是生活中的点点滴滴。


有人说过一句话:


和喜欢的人在一起的快乐有两种,一种是一起看电影旅行一起看星星月亮分享所有的浪漫。还有一种则是分吃一盒冰淇淋一起逛超市逛宜家把浪漫落地在所有和生活有关的细节里。


但无论是前者还是后者,我都想和你一起,因为重要的是和你,而不是浪漫。


往事历历在目,虽然现在很难再有当时的那种感觉,可我更享受当下的温馨。


四、旅游


认识 Emma 之后,我们开始养成了旅游的习惯,每个月都会留出两三天时间一起出去旅游。


2 月,去了丽江。爬了玉龙雪山、逛了拉市海和木府。



玉龙雪山真的很神奇,在丽江市任何一个角落,都可以抬头看见白雪皑皑的雪山。


2 月 14 号是情人节,我在丽江的街头给 Emma 买了一束玫瑰。



3 月,我在上海,本来打算去北京,结果发生航变。然后再打算做高铁去杭州西湖,结果杭州顺丰出事了。哪都去不了,只能在上海本地旅游。去了陆家嘴的上海中心大厦。其实很早之前我就和朋友去过一次了,这次去的主要目的是和 Emma 锁同心锁。


我记得我们去的时候,已经是晚上九点五十,工作人员说十点关门,因为疫情原因,明天开始将无限期关停,具体开放时间等通知。如果现在上去,我们只能玩十分钟。没有犹豫,花了四百多买了两张票上去上了把锁。



那时我还不知道,这种繁华将在不久后彻底归于死寂。


后面几天,我们在上海很多地方溜达,外滩、豫园、迪士尼乐园。


迪士尼乐园是上海人流量最大的游乐场,几乎全年都是爆满。但这次疫情期间的迪士尼乐园的游客很少,很多项目都不需要排队。


但那天真的很冷,我们为了看烟花冻得瑟瑟发抖。夜晚的迪士尼城堡真的很美。



4-7 月上海封城,哪儿也没去。


8 月去了博鳌亚洲论坛。如果不是去开会的话,真没什么好玩的。



9 月打算坐轮船去广州徐闻、再坐绿皮火车去拉萨,因为我们认为这样会很浪漫。结果因疫情原因,没办法离开海南,只去了骑驴老街和世纪大桥旁边的云洞书店。




这座书店建在海边,进入看书要提前预约。


实际上没几个看书的,全是美女在拍照。


10 月去了济南大明湖和北京的天安门广场、八大胡同等地方。


很没意思,图我都不想放。


预约了故宫门票,并且打算去八达岭长城。但是在北京土生土长四十年的同事告诉我,他从来没去过故宫,并且不建议去八达岭长城。于是我把故宫的门票退了,并且把八达岭长城的计划也取消了。


11 月一整个月都在忙着买房子的事情,天天跟着中介跑,哪儿也没去。


五、封控、方舱、辞职与离沪


上海疫情暴发,改变了很多事。


这一部分我删掉了,涉及敏感话题。


总结来说就是:封控,挨饿,感染奥密克戎,进方舱,出舱,继续封控。


6 月 1 日,上海终于开放了。


虽然可以离开小区,但离开上海还是很难。


首先我需要收拾家里的东西,然后需要寄回我爸妈家,但当时所有的物流都处于瘫痪状态。当时我也想过直接丢掉,但我的东西真的很多,有二十多个箱子,差不多是我的全部家当了。


再之后需要购买飞机票,但所有的航班都在无限期航变。我给飞机场的工作人员打了很多次电话,没事就问。后来她们有些不耐烦了,告诉我,现在没有一架飞机能从上海飞出去,让我不要再打了。


可我不死心。


最后我选择了乘坐火车。


上海没有直达海口的火车,所以需要在南昌中转。由于我是上海来的,所以当地的政策是要隔离 7 天,除非你不出火车站。上海到南昌的火车是第一天下午到,而南昌到海口的火车是第二天的中午才出发。这也就意味着我要在南昌站睡一晚。而从上海到海口的整个路程,需要花费 3 天。这对以前的我来说是不可思议的。


但爱情赋予我的力量是难以想象的。


我在火车站旁边的超市买了一套被子就上了去南昌的火车,然后躺在按摩椅上睡了一晚。


第四天一大早,我终于到了海南。


六、求职


2022 年,是非常难找工作的。


特别是我准备搬到海南。海南是没有大型互联网公司的,连个月薪上万的工作都找不到。这样估计连自己都养不活。


所以我准备找一份远程工作。相比较于国内,国外远程工作的机会明显更多,可惜我英语不好,连一场基本的面试所需要的词汇都不够。所以我只能找一些国内的远程机会。


机缘巧合之下,终于找到了一家刚在美国融到资的创业公司。


他们需要一位带有产品属性和创造力的前端架构师。前前后后面了大概一周,我成功拿到 offer。这是我第一次加入远程团队。


印象中面试过程中技术相关的点聊了以下几点:



  • WebSocket 的弊端。

  • Nodejs 的 Stream。

  • TLV 相比 JSON 的优劣势。

  • HTTP 1.1/2 和 HTTP3(QUIC)的优劣势。

  • 对云的看法。

  • 真正影响前端性能的点。

  • 团队管理方面的一些问题。


因为公司主要业务是做云,所以大多数时间都是在聊云相关的问题。老板是一个拥有 20 年技术背景的老技术人,还是腾讯云 TVP。在一些技术上的观点非常犀利,有自己的独到之处。这一点上我的感受是非常明显的。


全球化边缘云怎么做?


现代应用的性能瓶颈往往不再是渲染性能。但各大前端框架所关注的点却一直是渲染。以至于现代主流前端框架都采用 Island 架构。


但另一个很影响性能的原因是时延。这不仅仅是前端性能的瓶颈,同时也是全球化分布式系统的性能瓶颈。


设想一下,你是做全球化的业务,你的开发团队在芝加哥,所以你在芝加哥买了个机房部署了你们的系统,但你的用户在法兰克福。无论你的系统如何优化,你的用户总是感觉卡顿。因为你们相隔 4000 多英里的物理距离,就一定要遭受 4000 多英里的网络延迟。


那该怎么办呢?很简单,在法兰克福也部署一套一模一样的系统就好了,用户连接距离自己最近的服务器就好了,这样就能避免高网络时延,这就是全球分布式。但如果你的用户也有来自印度的、来自新加坡、来自芬兰的。你该怎么办呢?难道全球 200 多个地区都逐一部署?那样明显不现实,因为成本太高了,99% 的公司都支付不起这笔费用。


所以很多云服务商都提供了全球化的云服务托管,这样就可以以低成本实现上述目标。


但大多数云服务商的做法也并不是在全球那么多地区部署那么多物理服务器。而是在全球主要的十几个地区买十几台 AWS 的裸机就够了。可以以大洲维度划分。有些做得比较好的云服务商可能已经部署了几十个节点,这样性能会更好。


这类云服务商中的一些小而美的团队有 vercel、deno deploy。这也是我们竞争目标之一。


招聘者到底在招什么人?


当时由于各种裁员,大环境不好,面试者非常多,但合适的人又非常少。后来据老板透露,他面试了 48 个人,才找到我。起初我还不信,以为这是常规 PUA,后来我开始负责三面的时候,在面试登记表中发现确实有那么多面试记录。其中竟然有熟人,也有一个前端网红。


后来我总结,很多人抱怨人难招,那他们到底在招什么人?


抛开技术不谈,他们需要有创造力的人。


爱因斯坦说:想象力比知识更重要。想象力是创造力的要素之一,也是创造力的前提。


那些活得不是很实际的人,会比那些活得踏踏实实的人有更大的机会去完成创造。


为什么要抛开技术不谈?因为职业生涯到了一定高度后,已经非唯技术论了。大家的技术都已经到了 80 分以上,单纯谈论技术,已经很难比较出明显差异,这时就要看你能用已有的技术,来做点什么,也就是创造力。这也是为什么我在文中没怎么谈技术面试的原因。


如果将马斯洛需求理论转化为工程师需求理论,那么同样可以有五级:




  1. 独立完成系统设计与实现。




  2. 具有做产品的悟性,关注易用、性能与稳定。




  3. 做极致的产品。对技术、组织能力、市场、用户心理都有很全面深入的理解。代表人物张小龙。




  4. 能够给世界带来惊喜的人。代表人物沃兹尼亚克。




  5. 推动人类文明进步,开创一个全新行业的人。代表人物马斯克。




绝大多数人都停留在前三个维度上,我也不例外。但我信奉尼采的超人主义,人是可以不断完善自我、超越自我、蜕变与进化的。既然沃兹尼亚克和马斯克能够做到的事情,我为什么不能去做呢?所以我会一直向着他们的方向前进。或许最终我并不能达到那种高度,但我相信我仍然可以做出一些不错的产品。


七、创业


搬到海南之后,时间明显多了起来。不需要上下班通勤,不去夜店酒吧,甚至连下楼都懒得去。三五天出去一次,去小区打一桶纯净水,再去京东超市买一大堆菜和食物,囤在冰箱。如果不好买到的东西,我都会选择网购。我一般会从山姆、网易严选、京东上面网购。


九月份十月份,海南暴发疫情,一共举行了十几轮全员核酸检测,我一轮都没参加。主要是用不到,出不去小区没关系,外卖可以送到小区门口,小区内的京东超市也正常营业,丝毫不影响我的日常起居。


所以我盘算着要做些事情。刚好之前手里经营着几个副业,虽然都没有赚到什么钱。但是局已经布好了。其中一个是互联网 IT 教育机构。是我和我的合伙人在 20 年就开始运营的,现在已经可以在知乎、B 站等渠道花一些营销费用进行招生。我们定价是 4200 一个月(后来涨价到 4800),一期教三个月左右。通过前几期的经验,我们预估,如果一期有十几个人到二十个人报名,就有大概将近 10 万多的收入,还是蛮可观的。所以我准备多花些精力做一下这个教育机构。


培训的内容是前端,目标是零基础就业。因为我在前端这方面有很大的优势,而且当时前端招聘异常火热。初级前端工程师需要掌握的技能也不多,HTML、CSS、JavaScript、Git、Vue,就这么几块内容而已。相比较成型慢的后端,前端是可以速成的,所以我认为这条路非常可行。


我负责的这一期,前期招了将近 20 人。



而下一期还没开课,已经招到了 5 个人。一切都向着好的方向发展。


但很快,十月、十一月左右,招生突然变难了。连知乎上咨询的人都明显变少了。我们复盘了下原因,原来是网上疯传现在是前所未有的互联网寒冬,加上各种大厂裁员的消息频发,搞得人心惶惶。甚至有人扬言,22 年进 IT 行业还不如去养猪。


因为我的合伙人是全职,他在北京生活花费很高,加上前期没有节制地投入营销费用,他已经开始负债了。但是营销费用是不能停的,一旦知+ 停掉,就很难继续招生。


后来经过再三讨论,最终还是决定先停掉。虽然我们笃定 IT 培训是红海,但感觉短期的大环境不行,不适合继续硬撑下去。



目前我带着这期学员即将毕业,而我们也会在 23 年停止招生。


培训的这个过程很累,一边要产出营销内容,一边要完善和设计教材、备课、一边还要批改作业。同时每天完成两个小时的不间断直播授课,不仅仅是对精力的考验,也是对嗓子的考验。


虽然创业未成,但失败是我们成长路上的垫脚石,而且这也谈不上什么失败。


我把经验教训总结如下,希望对你有所启发。


1. 市场和运营太重要了


中国有句谚语叫“酒香不怕巷子深”。意思是如果酒真香,不怕藏于深巷,人们会闻香而至。除了酒,这句话也隐喻好产品自己会说话,真才子自有人赏识。


但实际上,这是大错特错。


随着我阅历、知识、认知的提升,我越发觉得,往往越是这种一说就明白、人人都信的大道理,越是错的。


傅军老师讲过一句话:感受不等于经验、经验不等于知识、知识不等于定律、定律不等于真理。我深受启发。在这个快速发展的信息时代,必须保持独立思考的能力。


我思考的结果是:酒香也怕巷子深。


连可口可乐、路易威登、香奈儿这种世界顶级品牌每年都需要持续不断地花费大量的费用用于营销。由此可见营销的重要性,营销是一定有回报的。


我们的酒要香,同时也不能藏在巷子里。而且,我们的酒不需要特别香,只要不至于苦得无法下咽,就能卖出去。畅销品不一定就是质量好,某些国产品牌的手机就是一个简单的例子。


所以,只需要保证产品是 60 分以上就足够了,剩下的大部分精力都应该放在营销上面。绝大多数只懂得专注于打造 100 分产品的创业公司,基本上都死了。


正确的路径应该是:Poc、Alpha、Beta、MVP。就像下图所示,造车的过程中,产品在任何一个时间点都是可用的。



另外,决定一个公司成功的,一定是运营部或者市场部,绝对不是技术部。但是技术部有可能会让一个公司走向失败。如果一个公司要裁员,技术部门很可能首当其冲。当然这也要看公司文化,但大部分的 Boss 普遍更看重运营与市场。


2. 不要等万事俱备再开始


在创业早期,我们的主要招生渠道是 B 站,但我没有短视频经验。于是我买了暴漫 B 站 UP 主的课程。



本来以为买了课程就意味着学会了做视频,但实际上不是这样的。


做好一个 UP 主需要掌握非常多的知识,选题、文案、表达、拍摄、剪辑、互动,各个环节都有大学问。我很快发现不可能把整个课程全部研究透再去做视频。我需要保持频率地更新视频才能有更多人的关注。


随着业务的发展,后面主战场转到了知乎,工作内容也变成了软文和硬广。UP 主的很多技能还没来得及实践就被搁置了。


知乎也有一个写作训练营,学费大概也要三四千的样子。



我听了试听的三节课,感觉要学习的东西一样很多,所以我没有买这个课程。因为我知道我没有那么多精力。


最重要的是,我发现即使我没有什么技巧和套路,我写的软文一样有很多浏览量和转化率。知+平台的数据可以直接告诉我这篇文章或者回答写得好不好。


同时,很多相关的知识不是一成不变的,它们不是传统知识,需要从实际事物中脚踏实地地学习,我们没办法系统学习。


我们不可能做好所有的准备,但需要先去做。人生也该如此,保持渐进式。


3. 接受糟糕,不要完美主义


初创公司很多东西都是混乱的,缺乏完善的系统和结构,一切似乎都是拍脑袋决定。


报销很混乱、营销费用很混乱、内容管理很混乱、合同很混乱;甚至连招生清单都和实际上课的人对不上……


但我发现这是一个必须接受的现状,如果一切都井井有条,那就不是初创公司了。所以必须要适应这种乱糟糟、混乱的环境。


八、结婚与买房


朋友说我是一个果断的人。


因为从认识,到结婚、买房,一共用了不到一年时间。


其实起初我是强烈不建议买房的,我不看好中国的楼市,而且我们一个月花几千块租房住得非常舒服。而且我的父母在北方的城市也已经有两套房子了。但 Emma 认为没有房子不像个家,没安全感。安全感对一个女人实在太重要了,我想以她对我的了解,她怕我哪天突然悄无声息地就离开她。想了想,确实是这样,所以我就买了。


我们看了海口很多套房子,本来计划买个小两居,但看了几次,都觉得太紧凑了。我是要在家工作的,我有一张很大的双人桌,要单独留一个房间来放它。最后挑了一个三居室的房子。这种感觉和买电脑差不多,本来一台普通 Thinkpad 的预算,最终买了个 MacBookPro 顶配。



房子很漂亮,我们也很喜欢。



不过呢,同时也背负了两百万的贷款。不能再像以前那样随便花钱了。


买房只需要挑房屋布局、装修和地段,其他不需要关心。


买房贷款的注意事项:选择分期,选择还款周期最久的,不要提前还。今年由于疫情原因,房贷大概是 4.5 个点,比前几年要低。买房前保持一年流水,收入大于月供 3 倍。


还需要注意,买完房,还要考虑装修费用和家电的费用。


非刚需不建议买房,刚需的话不会考虑买房的弊端。


至于为什么结婚?


我和 Emma 都是不婚主义者。但是买房子如果不结婚,就只能写一个人的名字。但我们都不愿意写自己的名字。最后没办法,决定去办理结婚证。


但我们没有举办传统的婚礼,我们都不喜欢熙熙攘攘、吵吵闹闹。我们只想过好我们自己的生活,不希望任何人来打扰我们。


这辈子我搬了至少十次家,只有这次是搬进自己的家。


九、学习


今年在学习上主要有四个方面:



  • Web3。

  • 英语。

  • 游戏开发。

  • 自媒体。


目前对我来说,Web3 是最重要的事情。虽然我还没有 All in web3,但也差不多了。有人说 Web3 是骗局,但我认为不是骗局。


我承认 Web3 有非常多的漏洞,比如 NFT 的图片压根没上链,链上存储的只是一堆 URL。同时几乎所有用户都没有直接与区块链交互,而是与一堆中心化的平台交互。比如我们使用 MetaMask,其实数据流过了 MetaMask 和 Infura。目前的 Web3 并没有完全去中心化。


没有一个新鲜事物在一诞生就是完美的。正是由于这些不完美的地方,所以才需要我们去完善它们。


目前我已经加入了一个 Web3 团队。虽然团队不大,但是他们做的事情非常吸引我。Web3 是一个高速发展的火箭,对现在的人来说,你先上去再说,何必在乎坐在哪个位置上?


我很期待 2030 年的 Web3。


如果要进入 Web3,需要学习技术有很多,我把一些重要的技术栈列举如下:



  • Web3.js or Ethers.js or Wgami:与区块链交互的前端 SDK。

  • Solidity:智能合约编程语言。

  • Hardhat:以太坊开发环境。

  • IPFS:Web3 文件存储系统。


除了上述的技术之外,更多的是概念上的理解,比如 NFT、GameFi 相关的诸多概念等。


学习英语是因为我打算出国,同时我现在的工作环境也有很多英语,我不想让语言成为我继续提升的障碍。


至于游戏开发和自媒体,纯粹是想赚钱。


游戏开发主要是微信小游戏,靠广告费就能赚钱。微信小游戏的制作成本很低,两个人的小型团队就可以开发数款微信小游戏。而且赚钱和游戏质量并不成正比。跳一跳、羊了个羊都是成功案例。当然我的目标不是做那么大,大概只需要做到它们几百分之一的体量就非常不错了,要知道微信有将近 10 亿用户,总有人会玩你的游戏。


另外我学习自媒体和微信小游戏的原因差不多。自媒体的盘子越来越大。在 B 站、抖音这些短视频平台上,有着上千亿甚至更多的市场。这给更多人创造了创造收入的空间。


只要去做,总会有所收获。


在学习的过程中,我会记录大量笔记以及自我思考。但很少会更文。


大概在 9 月份,我参加了掘金的两次更文活动,输出了一些内容。


这个过程很累,但也有所收获。






后来考虑到时间和精力问题,所以很少更文了。


很遗憾,掘金目前只能依靠平台自身给创作者奖励,收入非常有限。如果掘金能够将变现程度发展成像 B 站、公众号或抖音那样,估计也会有更多的创作者加入。连 CSDN、博客园这些老牌产品都做不到。究其原因,还是这种性质的产品受众没有那么广泛,盘子太小了,体量自然无法涨上去。希望未来掘金能够找到属于自己的商业模式。


最后还是很感谢掘金这个平台,给了技术人们一个分享交流的空间。


十、未来


未来三年的计划是出国读硕,并全面转入 Web3 领域。


我不看好中国的经济,并且感觉在中国做生意会越来越难做。而且我更喜欢西方文化、氛围和环境。


在经历了国内疫情反复封控之后,我更加坚定了出国的打算。我不想继续生活在大陆。


我想看看外面的世界,同时系统化地提升一下自己的 CS 知识。


目前出国的思路是申请混合制研究生,F1、OPT、H1B、Green Card。目标是一所在 Chicago QS TOP500 垫底的高校。


另一条出国的路线是直接出国找一份美国的工作,但对目前的我来说是相当难,主要还是因为语言。


之所以是未来三年的计划,也是因为我的英语实在太差,目前按照 CEFR 标准,只达到了 A2。


按照 FSI 英语母语者学习外语的难度排名,中文这类语言是最难学习的。反过来,中文母语者学习英语,也是最难的。真正掌握大概需要 2200 小时,如果每天学习 3-4 小时的话,需要 88 周,将近两年。


FSI 的图片我删掉了,因为那张图上的中国版图有争议。


我的语言天赋很一般,甚至有些差。所以学习效果并不理想。我也不打算短时间内冲刺英语,因为那不太靠谱。我选择花更久的时间去磨它。


之前也想过去 Helsinki,但那边收入不高,税收太高。纠结了很久,还是觉得现在还年轻,多少还是应该有些压力的,去那种地方实在太适合养老了。


以上并不是故事,是我的亲身经历,分享出来的初衷是为大家提供更多职业和人生的思路与思考。希望对你有所帮助!



作者:代码与野兽
来源:juejin.cn/post/7178816031459115066
收起阅读 »

10个超级实用的Set、Map使用技巧

web
Set是一种类似于数组的数据结构,但是它的值是唯一的,即Set中的每个值只会出现一次。Set对象的实例可以用于存储任何类型的唯一值,从而使它们非常适用于去重。 Map是一种键值对集合,其中每个键都是唯一的,可以是任何类型,而值则可以是任何类型。Map对象的实例...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg


Set是一种类似于数组的数据结构,但是它的值是唯一的,即Set中的每个值只会出现一次。Set对象的实例可以用于存储任何类型的唯一值,从而使它们非常适用于去重。


Map是一种键值对集合,其中每个键都是唯一的,可以是任何类型,而值则可以是任何类型。Map对象的实例可以用于存储复杂的对象,并且可以根据键进行快速的查找和访问。


以下是Set和Map的一些常用方法:


Set:



  • new Set(): 创建一个新的Set对象

  • add(value): 向Set对象中添加一个新的值

  • delete(value): 从Set对象中删除一个值

  • has(value): 检查Set对象中是否存在指定的值

  • size: 获取Set对象中的值的数量

  • clear(): 从Set对象中删除所有值


Map:



  • new Map(): 创建一个新的Map对象

  • set(key, value): 向Map对象中添加一个键值对

  • get(key): 根据键获取Map对象中的值

  • delete(key): 从Map对象中删除一个键值对

  • has(key): 检查Map对象中是否存在指定的键

  • size: 获取Map对象中的键值对数量

  • clear(): 从Map对象中删除所有键值对


Set和Map是非常有用的数据结构,它们可以提高程序的性能和可读性,并且可以简化代码的编写。


Set


去重


使用 Set 可以轻松地进行数组去重操作,因为 Set 只能存储唯一的值。


const arr = [1, 2, 3, 1, 2, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]

数组转换


可以使用 Set 将数组转换为不包含重复元素的 Set 对象,再使用 Array.from() 将其转换回数组。


const arr = [1, 2, 3, 1, 2, 4, 5];
const set = new Set(arr);
const uniqueArr = Array.from(set);
console.log(uniqueArr); // [1, 2, 3, 4, 5]

优化数据查找


使用 Set 存储数据时,查找操作的时间复杂度为 O(1),比数组的 O(n) 要快得多,因此可以使用 Set 来优化数据查找的效率。


const dataSet = new Set([1, 2, 3, 4, 5]);

if (dataSet.has(3)) {
console.log('数据已经存在');
} else {
console.log('数据不存在');
}

并集、交集、差集


Set数据结构可以用于计算两个集合的并集、交集和差集。以下是一些使用Set进行集合运算的示例代码:


const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);

// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set {1, 2, 3, 4}

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set {2, 3}

// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set {1}

模糊搜索


Set 还可以通过正则表达式实现模糊搜索。可以将匹配结果保存到 Set 中,然后使用 Array.from() 方法将 Set 转换成数组。


const data = ['apple', 'banana', 'pear', 'orange'];

// 搜索以 "a" 开头的水果
const result = Array.from(new Set(data.filter(item => /^a/i.test(item))));
console.log(result); // ["apple"]

使用 Set 替代数组实现队列和栈


可以使用 Set 来模拟队列和栈的数据结构。


// 使用 Set 实现队列
const queue = new Set();
queue.add(1);
queue.add(2);
queue.add(3);
queue.delete(queue.values().next().value); // 删除第一个元素
console.log(queue); // Set(2) { 2, 3 }

// 使用 Set 实现栈
const stack = new Set();
stack.add(1);
stack.add(2);
stack.add(3);
stack.delete([...stack][stack.size - 1]); // 删除最后一个元素
console.log(stack); // Set(2) { 1, 2 }

Map


将 Map 转换为对象


const map = new Map().set('key1', 'value1').set('key2', 'value2');
const obj = Object.fromEntries(map);

将 Map 转换为数组


const map = new Map().set('key1', 'value1').set('key2', 'value2');
const array = Array.from(map);

记录数据的顺序


如果你需要记录添加元素的顺序,那么可以使用Map来解决这个问题。当你需要按照添加顺序迭代元素时,可以使用Map来保持元素的顺序。


const map = new Map();
map.set('a', 1);
map.set('b', 2);
map.set('c', 3);
map.set('d', 4);

for (const [key, value] of map) {
console.log(key, value);
}
// Output: a 1, b 2, c 3, d 4

统计数组中元素出现次数


可以使用 Map 统计数组中每个元素出现的次数。


const arr = [1, 2, 3, 1, 2, 4, 5];

const countMap = new Map();
arr.forEach(item => {
countMap.set(item, (countMap.get(item) || 0) + 1);
});

console.log(countMap.get(1)); // 2
console.log(countMap.get(2)); // 2
console.log(countMap.get(3)); // 1

统计字符出现次数


使用Map数据结构可以方便地统计字符串中每个字符出现的次数。


const str = 'hello world';
const charCountMap = new Map();
for (let char of str) {
charCountMap.set(char, (charCountMap.get(char) || 0) + 1);
}
console.log(charCountMap); // Map { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }

缓存计算结果


在处理复杂的计算时,可能需要对中间结果进行缓存以提高性能。可以使用Map数据结构缓存计算结果,以避免重复计算。


const cache = new Map();
function fibonacci(n) {
if (n === 0 || n === 1) {
return n;
}
if (cache.has(n)) {
return cache.get(n);
}
const result = fibonacci(n - 1) + fibonacci(n - 2);
cache.set(n, result);
return result;
}
console.log(fibonacci(10)); // 55

使用 Map 进行数据的分组


const students = [
{ name: "Tom", grade: "A" },
{ name: "Jerry", grade: "B" },
{ name: "Kate", grade: "A" },
{ name: "Mike", grade: "C" },
];

const gradeMap = new Map();
students.forEach((student) => {
const grade = student.grade;
if (!gradeMap.has(grade)) {
gradeMap.set(grade, [student]);
} else {
gradeMap.get(grade).push(student);
}
});

console.log(gradeMap.get("A")); // [{ name: "Tom", grade: "A" }, { name: "Kate", grade: "A" }]


使用 Map 过滤符合条件的对象


在实际开发中,我们常常需要在一个对象数组中查找符合某些条件的对象。此时,我们可以结合使用 Map 和 filter 方法来实现。比如:


const users = [
{ name: 'Alice', age: 22 },
{ name: 'Bob', age: 18 },
{ name: 'Charlie', age: 25 }
];
const userMap = new Map(users.map(user => [user.name, user]));
const result = users.filter(user => userMap.has(user.name) && user.age > 20);
console.log(result); // [{ name: 'Alice', age: 22 }, { name: 'Charlie', age: 25 }]

首先,我们将对象数组转换为 Map,以便快速查找。然后,我们使用 filter 方法来过滤符合条件的对象。


这里我们列举了一些使用SetMap的实用技巧,它们可以大大简化你的代码,并使你更有效地处理数据。SetMap是JavaScript中非常有用的数据结构,值得我们在编写代码时好好利用。


系列文章



我的更多前端资讯


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


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

《如何超过大多数人》——陈皓(左耳朵耗子)

文章原文链接为:如何超过大多数人 ps:读这篇文章前先看看下面这段话,避免误导大家。 切记,这篇文章不要过度深思(任何东西都无法经得起审视,因为这世上没有同样的成长环境,也没有同样的认知水平同时也没有适用于所有人的解决方案;也不要去急着评判里面列出的观点,只...
继续阅读 »

文章原文链接为:如何超过大多数人



ps:读这篇文章前先看看下面这段话,避免误导大家。


切记,这篇文章不要过度深思(任何东西都无法经得起审视,因为这世上没有同样的成长环境,也没有同样的认知水平同时也没有适用于所有人的解决方案;也不要去急着评判里面列出的观点,只需代入到其中适度审视一番自己即可,能跳脱出来从外人的角度看看现在的自己处在什么样的阶段就行。具体怎么想怎么做全在你自己去不断实践中寻找那个适合自己的方案



正文开始:


当你看到这篇文章的标题,你一定对这篇文章产生了巨大的兴趣,因为你的潜意识在告诉你,这是一本人生的“武林秘籍”,而且还是左耳朵写的,一定有干货满满,只要读完,一定可以练就神功并找到超过大多数人的快车道和捷径……


然而…… 当你看到我这样开篇时,你一定会觉得我马上就要有个转折,告诉你这是不可能的,一切都需要付出和努力……然而,你错了,这篇文章还真就是一篇“秘籍”,只要你把这些“秘籍”用起来,你就一定可以超过大多数人。而且,这篇文章只有我这个“人生导师”可以写得好。毕竟,我的生命过到了十六进制2B的年纪,踏入这个社会已超过20年,舍我其谁呢?!


P.S. 这篇文章借鉴于《如何写出无法维护的代码》一文的风格……嘿嘿


相关技巧和最佳实践


要超过别人其实还是比较简单的,尤其在今天的中国,更是简单。因为,你只看看中国的互联网,你就会发现,他们基本上全部都是在消费大众,让大众变得更为地愚蠢和傻瓜。所以,在今天的中国,你基本上不用做什么,只需要不使用中国互联网,你就很自然地超过大多数人了。当然,如果你还想跟他们彻底拉开,甩他们几个身位,把别人打到底层,下面的这些“技巧”你要多多了解一下。


在信息获取上,你要不断地向大众鼓吹下面的这些事:



ps:是像大众哈。不要看错用在自己身上【狗头】


自己怎么做呢?反着做不就行了吗》》》




  • 让大家都用百度搜索引擎查找信息,订阅微信公众号或是到知乎上学习知识……要做到这一步,你就需要把“百度一下”挂在嘴边,然后要经常在群或朋友圈中转发微信公众号的文章,并且转发知乎里的各种“如何看待……”这样的文章,让他们爱上八卦,爱上转发,爱上碎片

  • 让大家到微博或是知识星球上粉一些大咖,密切关注他们的言论和动向…… 是的,告诉大家,大咖的任何想法一言一行都可以在微博、朋友圈或是知识星球上获得,让大家相信,你的成长和大咖的见闻和闲扯非常有关系,你跟牛人在一个圈子里你也会变牛。

  • 把今日头条和抖音这样的APP推荐给大家……你只需要让你有朋友成功地安装这两个APP,他们就会花大量的时间在上面,而不能自拔,要让他们安装其实还是很容易的,你要不信你就装一个试玩一会看看(嘿嘿嘿)。

  • 让大家热爱八卦,八卦并不一定是明星的八卦,还可以是你身边的人,比如,公司的同事,自己的同学,职场见闻,社会热点,争议话题,……这些东西总有一些东西会让人心态有很多微妙的变化,甚至花大量的时间去搜索和阅读大量的观点,以及花大量时间与人辩论争论,这个过程会让人上瘾,让人欲罢不能,然而这些事却和自己没有半毛钱关系。你要做的事就是转发其中一些SB或是很极端的观点,造成大家的一睦讨论后,就早早离场……

  • 利用爱国主义,让大家觉得不用学英文,不要出国,不要翻墙,咱们已经是强国了……这点其实还是很容易做到的,因为学习是比较逆人性的,所以,只要你鼓吹那些英文无用论,出国活得更惨,国家和民族都变得很强大,就算自己过得很底层,也有大国人民的感觉。


然后,在知识学习和技能训练上,让他们不得要领并产生幻觉



  • 让他们混淆认识和知识,以为开阔认知就是学习,让他们有学习和成长的幻觉……

  • 培养他们要学会使用碎片时间学习。等他们习惯利用碎片时间吃快餐后,他们就会失去精读一本书的耐性……

  • 不断地给他们各种各样“有价值的学习资料”,让他们抓不住重点,成为一个微信公众号或电子书“收藏家”……

  • 让他们看一些枯燥无味的基础知识和硬核知识,这样让他们只会用“死记硬背”的方式来学习,甚至直接让他们失去信心,直接放弃……

  • 玩具手枪是易用的,重武器是难以操控的,多给他们一些玩具,这样他们就会对玩具玩地得心应手,觉得玩玩具就是自己的专业……

  • 让他们喜欢直接得到答案的工作和学习方式,成为一个伸手党,从此学习再也不思考……

  • 告诉他们东西做出来就好了,不要追求做漂亮,做优雅,这样他们就会慢慢地变成劳动密集型……

  • 让他们觉得自己已经很努力了,剩下的就是运气,并说服他们去‘及时行乐’,然后再也找不到高阶和高效率学习的感觉……

  • 让他们觉得读完书”、“读过书”就行了,不需要对书中的东西进行思考,进行总结,或是实践,只要囫囵吞枣尽快读完就等同于学好了……


最后,在认知和格局上,彻底打垮他们,让他们变成韭菜。



  • 让他们尽可能地用拼命和加班,尽可能的996,并告诉他们这就是通往成功的唯一路径。这样一来,他们必然会被永远困在低端成为最低的劳动力

  • 让他们不要看到大的形势,只看到眼前的一亩三分地,做好一个井底之蛙。其实这很简单,就是不要告诉他还有另外一种活法,不要扩大他的认识……

  • 宣扬一夜暴富以及快速挣钱的案例,最好让他们进入“赌博类”或是“传销类”的地方,比如:股市、数字货币……要让他们相信各种财富神话,相信他们就是那个幸运儿,他们也可以成为巴菲特,可以成为马云……

  • 告诉他们,一些看上去很难的事都是有捷径的,比如:21天就能学会机器学习,用区块链就能颠覆以及重构整个世界等等……

  • 多跟他们讲一些小人物的励志的故事,这样让他们相信,不需要学习高级知识,不需要掌握高级技能,只需要用低等的知识和低级的技能,再加上持续不断拼命重复现有的工作,终有一天就会成功……

  • 多让他们跟别人比较,人比人不会气死人,但是会让人变得浮躁,变得心急,变得焦虑,当一个人没有办法控制自己的情绪,没有办法让自己静下心来,人会失去耐性和坚持,开始好大喜欢功,开始装逼,开始歪门邪道剑走偏锋……

  • 让他们到体制内的一些非常稳定的地方工作,这样他们拥有不思进取、怕承担责任、害怕犯错、喜欢偷懒、得过且过的素质……

  • 让他们到体制外的那些喜欢拼命喜欢加班的地方工作,告诉他们爱拼才会赢,努力加班是一种福报,青春就是用来拼的,让他们喜欢上使蛮力的感觉……

  • 告诉他们你的行业太累太辛苦,干不到30岁。让他们早点转行,不要耽误人生和青春……

  • 当他们要做决定的时候,一定要让他们更多的关注自己会失去的东西,而不是会得到的东西。培养他们患得患失心态,让他们认识不到事物真正的价值,失去判断能力……(比如:让他们觉得跟对人拍领导的马屁忠于公司比自我的成长更有价值)

  • 告诉他们,你现有的技能和知识不用更新,就能过好一辈子,新出来的东西没有生命力的……这样他们就会像我们再也不学习的父辈一样很快就会被时代所抛弃……

  • 每个人都喜欢在一些自己做不到的事上找理由,这种能力不教就会,比如,事情太多没有时间,因为工作上没有用到,等等,你要做的就是帮他们为他们做不到的事找各种非常合理的理由,比如:没事的,一切都是最好的安排;你得不到的那个事没什么意思;你没有面好主要原因是那个面试官问的问题都是可以上网查得到的知识,而不没有问到你真正的能力上;这些东西学了不用很快会忘了,等有了环境再学也不迟……


最后友情提示一下,上述的这些“最佳实践”你要小心,是所谓,贩毒的人从来不吸毒,开赌场的人从来不赌博!所以,你要小心别自己也掉进去了!这就是“欲练神功,必先自宫”的道理。


相关原理和思维模型


对于上面的这些技巧还有很多很多,你自己也可以发明或是找到很多。所以,我来讲讲这其中的一些原理。


一般来说,超过别人一般来说就是两个维度:



  1. 在认知、知识和技能上。这是一个人赖以立足社会的能力(参看《程序员的荒谬之言还是至理名言?》和《21天教你学会C++》)

  2. 在领导力上。所谓领导力就是你跑在别人前面,你得要有比别人更好的能力更高的标准(参看《技术人员发展之路》)


首先,我们要明白,人的技能是从认识开始,然后通过学校、培训或是书本把“零碎的认知”转换成“系统的知识”,而有要把知识转换成技能,就需要训练和实践,这样才能完成从:认识 -> 知识 -> 技能 的转换。


这个转换过程是需要耗费很多时间和精力的,而且其中还需要有强大的学习能力和动手能力,这条路径上有很多的“关卡”,每道关卡都会过滤掉一大部分人。比如:对于一些比较枯燥的硬核知识来说,90%的人基本上就倒下来,不是因为他们没有智商,而是他们没有耐心。


认知


要在认知上超过别人,就要在下面几个方面上做足功夫:


1)信息渠道。试想如果别人的信息源没有你的好,那么,这些看不见信息源的人,只能接触得到二手信息甚至三手信息,只能获得被别人解读过的信息,这些信息被三传两递后必定会有错误和失真,甚至会被传递信息的中间人hack其中的信息(也就是“中间人攻击”),而这些找不出信息源的人,只能“被人喂养”,于是,他们最终会被困在信息的底层,永世不得翻身。(比如:学习C语言,放着原作者K&R的不用,硬要用错误百出谭浩强的书,能有什么好呢?)


2)信息质量。信息质量主要表现在两个方面,一个是信息中的燥音,另一个是信息中的质量等级,我们都知道,在大数据处理中有一句名言,叫 garbage in garbage out,你天天看的都是垃圾,你的思想和认识也只有垃圾。所以,如果你的信息质量并不好的话,你的认知也不会好,而且你还要花大量的时间来进行有价值信息的挖掘和处理。


3)信息密度。优质的信息,密度一般都很大,因为这种信息会逼着你去干这么几件事,


a)搜索并学习其关联的知识


b)沉思和反省


c)亲手去推理、验证和实践……


一般来说,经验性的文章会比知识性的文章会更有这样的功效。比如,类似于像 Effiective C++/Java,设计模式,Unix编程艺术,算法导论等等这样的书就是属于这种密度很大的书,而像Netflix的官方blogAWS CTO的blog等等地方也会经常有一些这样的文章。


知识


要在知识上超过别人,你就需要在下面几个方面上做足功夫:


1)知识树(图)任何知识,只在点上学习不够的,需要在面上学习,这叫系统地学习,这需要我们去总结并归纳知识树或知识图,一个知识面会有多个知识板块组成,一个板块又有各种知识点,一个知识点会导出另外的知识点,各种知识点又会交叉和依赖起来,学习就是要系统地学习整个知识树(图)。而我们都知道,对于一棵树来说,“根基”是非常重要的,所以,学好基础知识也是非常重要的,对于一个陌生的地方,有一份地图是非常重要的,没有地图的你只会乱窜,只会迷路、练路、走冤枉路!


2)知识缘由。任何知识都是有缘由的,了解一个知识的来龙去脉和前世今生,会让你对这个知识有非常强的掌握,而不再只是靠记忆去学习。靠记忆去学习是一件非常糟糕的事。而对于一些操作性的知识(不需要了解由来的),我把其叫操作知识,就像一些函数库一样,这样的知识只要学会查文档就好了。能够知其然,知其所以然的人自然会比识知识到表皮的人段位要高很多。


3)方法套路学习不是为了找到答案,而是找到方法。就像数学一样,你学的是方法,是解题思路,是套路,会用方程式解题的和不会用方程式解题的在解题效率上不可比较,而在微积分面前,其它的解题方法都变成了渣渣。你可以看到,掌握高级方法的人比别人的优势有多大,学习的目的就是为了掌握更为高级的方法和解题思路


技能

要在技能上超过别人,你就需要在下面几个方面做足功夫:


1)精益求精。如果你想拥有专业的技能,你要做不仅仅是拼命地重复一遍又一遍的训练,而是在每一次重复训练时你都要找到更好的方法,总结经验,让新的一遍能够更好,更漂亮,更有效率,否则,用相同的方法重复,那你只不过在搬砖罢了。


2)让自己犯错。犯错是有利于成长的,这是因为出错会让人反思,反思更好的方法,反思更完美的方案,总结教训,寻求更好更完美的过程,是技能升级的最好的方式。尤其是当你在出错后,被人鄙视,被人嘲笑后,你会有更大的动力提升自己,这样的动力才是进步的源动力。当然,千万不要同一个错误重复地犯!


3)找高手切磋。下过棋,打个球的人都知道,你要想提升自己的技艺,你必需找高手切磋,在和高手切磋的过程中你会感受到高手的技能和方法,有时候你会情不自禁地哇地一下,我靠,还可以这么玩!


领导力


最后一个是领导力,要有领导力或是影响力这个事并不容易,这跟你的野心有多大,好胜心有多强 ,你愿意付出多少很有关系,因为一个人的领导力跟他的标准很有关系,因为有领导力的人的标准比绝大多数人都要高。


1)识别自己的特长和天赋。首先,每个人DNA都可能或多或少都会有一些比大多数人NB的东西(当然,也可能没有),如果你有了,那么在你过去的人生中就一定会表现出来了,就是那种大家遇到这个事会来请教你的寻求你帮助的现象。那种,别人要非常努力,而且毫不费劲的事。一旦你有了这样的特长或天赋,那你就要大力地扩大你的领先优势,千万不要进到那些会限制你优势的地方。你是一条鱼,你就一定要把别人拉到水里来玩,绝对不要去陆地上跟别人拼,不断地在自己的特长和天赋上扩大自己的领先优势,彻底一骑绝尘。


2)识别自己的兴趣和事业。没有天赋也没有问题,还有兴趣点,都说兴趣是最好的老师,当年,Linus就是在学校里对minx着迷了,于是整出个Linux来,这就是兴趣驱动出的东西,一般来说,兴趣驱动的事总是会比那些被动驱动的更好。但是,这里我想说明一下什么叫“真∙兴趣”,真正的兴趣不是那种三天热度的东西,而是那种,你愿意为之付出一辈子的事,是那种无论有多大困难有多难受你都要死磕的事,这才是“真∙兴趣”,这也就是你的“野心”和“好胜心”所在,其实上升到了你的事业。相信我,绝大多数人只有职业而没有事业的。


3)建立高级的习惯和方法。没有天赋没有野心,也还是可以跟别人拼习惯拼方法的,只要你有一些比较好的习惯和方法,那么你一样可以超过大多数人。对此,在习惯上你要做到比较大多数人更自律,更有计划性,更有目标性,比如,每年学习一门新的语言或技术,并可以参与相关的顶级开源项目,每个月训练一个类算法,掌握一种算法,每周阅读一篇英文论文,并把阅读笔记整理出来……自律的是非常可怕的。除此之外,你还需要在方法上超过别人,你需要满世界的找各种高级的方法,其中包括,思考的方法,学习的方法、时间管理的方法、沟通的方法这类软实力的,还有,解决问题的方法(trouble shooting 和 problem solving),设计的方法,工程的方法,代码的方法等等硬实力的,一开始照猫画虎,时间长了就可能会自己发明或推导新的方法。


4)勤奋努力执着坚持。如果上面三件事你都没有也没有能力,那还有最后一件事了,那就是勤奋努力了,就是所谓的“一万小时定律”了(参看《21天教你学会C++》中的十年学编程一节),我见过很多不聪明的人,悟性也不够(比如我就是一个),别人学一个东西,一个月就好了,而我需要1年甚至更长,但是很多东西都是死的,只要肯花时间就有一天你会搞懂的,耐不住我坚持十年二十年,聪明的人发明个飞机飞过去了,笨一点的人愚公移山也过得去,因为更多的人是懒人,我不用拼过聪明人,我只用拼过那些懒人就好了。


好了,就这么多,如果哪天你变得消极和不自信,你要来读读我的这篇文章

作者:𝓑𝓮𝓲𝓨𝓪𝓷𝓰
来源:juejin.cn/post/7207648496978870333
,子曰:温故而知新。

收起阅读 »

Dart 3.0 语法新特性 | Records 记录类型 (元组)

1. 记录类型的声明与访问 通过 () 将若干个对象组合在一块,作为一个新的聚合类型。定义时可以直接当放入对象,也可以进行命名传入:var record = ('first', a: 2, b: true, 'last'); print(record.runt...
继续阅读 »

1. 记录类型的声明与访问


通过 () 将若干个对象组合在一块,作为一个新的聚合类型。定义时可以直接当放入对象,也可以进行命名传入:

var record = ('first', a: 2, b: true, 'last');
print(record.runtimeType);

--->[打印输出]---
(String, String, {int a, bool b})

上面的 record 对象由四个数据构成,通过 runtimeType 可以查看到其运行时类型,类型为各个非命名数据类型 + 各命名类型。



非命名类型数据可以通过 $index 进行访问:

print(record.$1);
print(record.$2);

--->[打印输出]---
first
last


命名类型数据可以通过 名称 进行访问:

print(record.a);
print(record.b);

--->[打印输出]---
2
true

注意: 一个记录对象的数据不允许被修改:


image.png




2. 记录类型声明对象


一个 Records 本质上也是一种类型,可以用该类型来声明对象,比如现在通过 (double,double,double) 的记录类型表示三个坐标,如下定义 p0 和 p1 对象:

void main() {
(double x, double y, double z) p0 = (1, 2, 3);
(double x, double y, double z) p1 = (1, 2, 6);
}

既然可以实例化为对象,那么自然也可以将其作为参数类型传入函数中,如下 distance 方法传入两个三维点,计算距离:

double distance(
(double x, double y, double z) p0,
(double x, double y, double z) p1,
) {
num result = pow(p0.$1 - p1.$1, 2) + pow(p0.$2 - p1.$2, 2) + pow(p0.$3 - p1.$3, 2);
return sqrt(result);
}

但记录类型一旦显示声明,写起来比较繁琐;和函数类型类似,也可以通过 typedef 来定义类型的别名。如下所示,定义 Point3D 作为别名,功能是等价的,但书写和可读性会更好一些:

typedef Point3D = (double, double, double);

void main() {
Point3D p0 = (1, 2, 3);
Point3D p1 = (1, 2, 6);

print(distance(p0, p1));
}

double distance(Point3D p0, Point3D p1) {
num result = pow(p0.$1 - p1.$1, 2) + pow(p0.$2 - p1.$2, 2) + pow(p0.$3 - p1.$3, 2);
return sqrt(result);
}

同理,记录类型也可以作为返回值,这样可以解决一个函数返回多值的问题。如下 addTask 方法可以计算 1 ~ count 的累加值,返回计算结果和耗时毫秒数:

({int result, int cost}) addTask2(int count) {
int start = DateTime.now().millisecondsSinceEpoch;
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}
int end = DateTime.now().millisecondsSinceEpoch;
return (
result: sum,
cost: end - start,
);
}



3. 记录类型对象的等值


记录类型会根据字段的结构自动定义 hashCode 和 == 方法。 所以两个记录对象相等,就是其中的各个数值相等。但是通过 identical 可以看出 p0 和 p1 仍是两个对象,内存地址不同:

(double, double, double) p0 = (1, 2, 3);
(double, double, double) p1 = (1, 2, 3);
print(p0 == p1);
print(identical(p0, p1));

--->[打印输出]---
true
false

如下所示,第二个数据是 List<double> 类型,两个 [2] 是两个不同的对象,所以 p2,p3 不相等:

(double, List<double>, double) p2 = (1, [2], 3);
(double, List<double>, double) p3 = (1, [2], 3);
print(p2==p3);

--->[打印输出]---
false

下面测试中, 列表使用同一对象,则 p2,p3 相等:

List<double> li = [2];
(double, List<double>, double) p2 = (1, li, 3);
(double, List<double>, double) p3 = (1, li, 3);
print(p2==p3);

--->[打印输出]---
true



4. 记录类型的价值


对于编程语言来说,Dart 的记录类型也不是什么新的东西,就是其他语言中的元组。如下所示,可以创建一个 TaskResult 类来维护数据作为返回值。但如果只是返回一些临时的数据,为此新建一个类来维护数据就会显得比较繁琐,还要定义构造函数。

class TaskResult{
final int result;
final int cost;

TaskResult(this.result, this.cost);
}

TaskResult addTask(int count) {
int start = DateTime.now().millisecondsSinceEpoch;
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}
int end = DateTime.now().millisecondsSinceEpoch;
return TaskResult(sum, end - start);
}



除此之外,一个函数返回多个数据也可以使用 Map 对象:

Map<String,dynamic> addTask(int count) {
int start = DateTime.now().millisecondsSinceEpoch;
int sum = 0;
for (int i = 0; i < count; i++) {
sum += i;
}

int end = DateTime.now().millisecondsSinceEpoch;
return {
'result' : sum,
'cost': end - start
};
}

但这种方式的弊端也很明显,返回和使用时都需要固定的字符串作为 key。如果 key 写错了,代码在运行前也不会有任何错误,这样很容易出现风险。多人协作时,而且如果函数的书写者和调用者不是一个人,那该使用什么键得到什么值就很难分辨。

Map<String,dynamic> task = addTask2(100000000);
print(task['result']);
print(task['cost']);

所以,相比于新建 class 或通过 Map 来维护多个数据,使用记录类型更加方便快捷和精确。但话说回来,如果属性数据量过多,使用记录类型看起来会非常麻烦,也不能定义成员方法来操作、修改内部数据。所以它有自己的特点使用场景,比如临时聚合多个数据来方便使用。


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

程序员的十级孤独,你体会过几级

都说天才程序员是和疯子就差一步之遥。有极致孤独与追求,有自己的精神世界, 我就是有代码洁癖的,追求极致代码要求。如何看到别人写代码不按照自己的,来我就会很抓马 就会不通过他的代码质量检查,以致于我现在有点极致的病态要求了 尤其作为项目中的leader 我感受...
继续阅读 »

都说天才程序员是和疯子就差一步之遥。有极致孤独与追求,有自己的精神世界,


我就是有代码洁癖的,追求极致代码要求。如何看到别人写代码不按照自己的,来我就会很抓马 就会不通过他的代码质量检查,以致于我现在有点极致的病态要求了


尤其作为项目中的leader 我感受更加深刻


独立思考的孤独


作为项目负责人,上面是PE时间项目成本的压力,下面是产品经理的压力,你经常会被夹在中
非常的难受,左右为难,于是你在开发项目之前,你需要纠结很久,需要在产品质量,和时间成本上左右为难。


作为一个有代码洁癖,项目契约精神的人,我经常为这种事件很崩溃。


到底是为了,赚钱而赚钱,还是为了❓。


记得我入行的热爱,是对编程感兴趣。说好要 写出改变世界的产品呢,


一个28岁程序员入行自述和感受


团队合作中的孤独



记一次线上生产事故



作为项目负责人,线上生成事故是需要负责任的,按照事故级别和影响范围时间,会扣除相应工资,绩效等。严重还可能被开除。


每次项目发版上线,你必须要在现场,不然你根本不放心,所以你变成不管是不是你的任务,你变成一定是最晚走哪个。


每次线上事故,bug报警一定最先找到你。应为留的报警错误电话,邮箱全部都是你,我记得有一次线上生产活动促销出问题了。半夜,电话被打爆了。


产品,测试,老板,各个电话第一个找到的就是你。所以那以后你几乎,电脑离不开,手机什么24小时待命。 精神会高度集中。(毕竟这是老板要求


我们项目发版,都是等到晚上10点发布版本。到了时间。测试通知发布堡垒环境,在测试走一遍,没问题了。会发布到生产环境,在看一遍,确认没问题就可以下班。


遇到问题,就炸锅,你需要协调各个部位,问题方。出现问题要抓紧修复解决,那一般没有到凌晨 是下不了班,回不了家的。



你加班看到过,凌晨红日早霞吗



失败的孤独


测试无法通过、代码错误等失败体验日常存在,尤其在项目版本环境升级时候,解决日益增长的用户,和系统负载问题,成了你最大的技术难点


这些情况增加了恐惧心理重负,会形成工作时的孤独体验


社交困境下的孤独


你们公司有每周,项目分享交流会吗,需要你不断,更新学习了解最前沿的项目技术。


作为leader 你不得不分享。


这个时候各个部门的开发都会到场,你需要分享你对一些技术看法,和了解,他怎么样的应用场景。你得作为新技术的 带头人。


可能会在一些新的项目中去使用新的技术,因为这样会有一下 绩效奖励


比如 创新奖励,专利奖励(哈哈哈哈哈)


值班的孤独


有时,孤独是一种磨练 。有时,孤独是一种思念
孤独,我们每个人都经历过


孤独让我们与众不同


孤独将我们成就,孤独令我们优秀


有人说是,过年不回家


我觉得 可能是没有时间找女朋友吧!


孤独是一种病 久了 也就习惯了!


大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题


可以关注 程序员三时公众号 进行技术交流讨论


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

内向性格的开发同学,没有合适的工作方法是不行的

一、背景 做软件开发同学的从性格上来说有两类人:外向的、内向的。 外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。 外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长...
继续阅读 »

一、背景


做软件开发同学的从性格上来说有两类人:外向的、内向的。


外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。


外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长观察,容易成为团队的定心骨,逐渐走向技术专家路线,肯研究肯花时间提高自己。



那么,在这个过程中,内向人前期的成长尤为重要,合适的工作方法和习惯也会提高在团队中的地位,而不是单纯的低头干活,本文分享下自己的经验,不一定对希望对大家有参考。


不同的性格的人,具有不同的工作方式和方法,和生活习惯,对于软件开发这个职场环境来说,内向性格不是劣势,很多人外表看着外向,其实潜意识也有很多内向性格的特征。


内向也是人的宝贵的一面,有时也是能力优势的一部分(如善于深度思考等),如果让自己掌握外向同学的行动方式,逐渐的做出改变,会更好。



二、现状


 刚毕业不久进入到职场中工作的毕业生,如果性格是外向的,那么他其实问题并不大,很多的时候,可以快速调整自己,并被其他人看到自己的工作成果,而内向性格的毕业生,如果在职场中没有主动去做某些工作和承担哪些职责,或对自己目前的工作状况没有及时调整和改变,就会造成成长缓慢,有的人会出现明明自己每天努力学习,却还是工作中那个让同时感觉能力最差的,导致经常没有分配到核心的开发工作,长此以往,消极的各种状态就出现了。


比如内向性格的毕业生在初入职场中经常会出现如下症状:


1、明知项目组的工作环境和方式存在一些不健康的因素,自己不太愿意去参与或评论


2、对开发整体流程和环节不清楚,及需求的判断有问题,需求频繁改动,代码写了被删除,自己却不敢说,或说了一次被骂以后沉默了


3、项目组缺失技术经理等全流程人员,需求自己理解和功能设计,自己却没有及时吧自己的想法与他人沟通 ,外包团队更明显


4、身边缺乏可以聊天的mentor、同事,自己感觉开发能力无法提升,却一直憋在心里,产生怀疑


5、不知道工作中如何问同事问题,才愿意帮忙解答,持续很长时间未获得同事的信任


6、有时过于逞强,不想让别人觉得自己不行,不会拒绝,实际工作量与评估有差别,导致自己延误工期。



以上的这些问题,可能不止内向性格的人会有,很多外向的人可能也会有,只是在内向性格的人身上更明显而已,如果内向性格的毕业生,明知道自己有这种情况,却不思考解决办法和改变,长时间后自我开始产生怀疑。 职场中,沟通、反馈、改变是很重要的,但是沟通不一定就是说话,反馈不一定是面对面,而改变是一直要持续去做的。 


之前看过一点得到的沟通训练营的视频教程,感觉里面有些技巧是值得大家去学习的,不仅仅是开发类型的同学。


三、经验分享


 下面我分享下,我的一些经验,可能不太对,但是希望可以帮助到看到这篇文章,深有同感的你。 


问题1:内向性格的毕业生,说的话,或者请求别人的东西,别人听不懂怎么办?


 这里先记住一件事情,在职场中,开发者要学会给不懂技术的人员,讲明白事情,要逐渐学会用生活中的事情去类比。


这个真的很重要,当你给不懂技术人讲的多以后,很多人可能都会来请教你关于某件事的理解,这个通常我们和系统的售前、需求人员、产品人员用的比较多,得学会用生活中的例子或故事去告诉他,XX能做,XX不能做的原因是什么。要坚持去练习。 


 对于请教一些人技术问题时,不管是同事也好还是网友也好,要明确自己给他的这个消息,别人是否会听懂,马上给出解决办法,还是别人看到这个问题以后,还要和我交流1小时才能知道是啥意思,这个也是很多有经验的人,不愿因帮助低级程序员的原因,这里分享下请教问题的描述模板: 


我遇到了一个问题或场景:【问题描述】,我想要实现【X功能】,但是出现了【Y现象】,我经过以下尝试:【思路细节】,但是不能解决,报错如下:【报错信息或截图】,或者我使用【关键词】百度,但是找不到答案,请问我该怎么解决或分析。


 而很多时候有经验的人,也会发现你百度的搜索词不对,这个时候,他根据你的阐述可能会告诉你怎么输入比较靠谱的搜索词来解决办法。 


问题2:评估工作计划有时过于逞强,不想让别人觉得自己不行,不会拒绝


这个真的想说,工作前期真的别逞强,没做过就是没做过,不行就是不行,别找啥接口,但是别直接和负责人说这个东西我不会(这个是很不好的,不能说不会,这是明显不相干的意思),比较合适的说法是:这个东西或概念我暂时不太清楚,没接触过过,需要一会儿或下来后我需要去研究下,然后咱们在沟通或者确定一下。 


 而很多内向性格的毕业生,缺少了这种意识,同时安排某项工作任务时,缺少对任务的分解能力和排期能力和工作后排期后的To do List梳理能力,以至于自己5天完成的任务,口头说2天就搞定了。 


 其实这种,前期mentor该给你做个示范分解的操作,或者自己主动问下,如何分解项目的需求和任务。


 而真正开发的时候,每天可能都感觉这里需要加上XXX功能,那里需要加上YYY功能,但是不知道是否需要做,这里我的建议是,把他加入到自己To do List中,然后找个时间和同事去沟通下这个想法,长此以往,同事的心里,你就是一个有想法的人,虽然不善言辞。


 主要就是这里,我们要体现自己的一个工作的对待方式,而不是一直被动接受,不拒绝,不反馈。 


问题3:明显知道产品经理、项目经理等等人员对需求的认识不足,自己闷着不反馈和说话


 很多时候,任务的返工和需求的变更,有一部分是这个原因的,在经验尚少的情况下,自己未能说出自己对这个需求的认识和怀疑,就去搞了,最后大家都不是特别的好,尤其是在产品需求设计初期,包括需求提出者也是理解不够的,这里可能有很多内容其实是你可以提供的服务,也有一些是产品在犹豫使用哪种方式实现的功能,在与你讨论后,觉得你说的又道理,而决定复用你已经有的系统。 


 很多出入职场的同学,觉得没成长也有这方面的一点原因,自己开发的功能,缺少自己设计思想和认知的影子,如果能在当前系统中体现出了自己的想法,时间久了多少成就感会有点提升的。 


要学会做自己负责的模块/功能的主人,把他们当做自己的孩子一样,主键养成主人翁的意识


问题4:项目组,当前啥都没有,文档、测试,自己也和别人一样不做改变


 这个也是目前很多公司的现状,但是不代表别人不干,你就不干,这个时候,谁主动,谁就能表现一把,同时,这也是被动让同事主动问你或咨询你的机会。


 比如没有协同的东西,那你能不能自己先装个Confluence Wiki或飞书云文档工具,自己先用起来,然后某个时机在同事眼前展示下,自己基于这个软件形成的技术思考、技术经验、技术记录等等等。


比如没有自动发布或代码质量的东西,那你能不能自己先搞个jenkins、sonarqube、checkstyle、findbug,让自己每次写完的代码,自己先搞下,然后某个时机告诉同事这个东西必须这么写怎怎么样。


 是不是有人又说了,工作没时间搞这些东西,你是不是又在扯皮呢,我只能说起码比你空闲时间自己偷偷学习公司短期内用不上的技术或长时间用不上的东西好吧,至少我能非常快速的获得1个同事的信任、2个同事的信任,从而获得团队的信任与核心工作的委派。


大部分人的想用的技术都是和公司的技术栈不搭边的,至少先把脚下的路走出来。


四、总结


 其实最近几年,发现好像很多人被卷字冲昏了头脑,每天都在想着高大尚的技术点和八股文,导致短期的这个工作没干好,还说没成长,以至于某些情况下还被认为是工作和团队中那个能力最差的,即使做了很多的努力。我想说的是,某段时间点或时期内,至少要把当前工作做好在谈论吧,这个在一些内向性格的人身上会表现的明显一些。


IT行业,很多优秀的人也是内向性格的,掌握了合适方法,会让他们成为内向性格顶端的那批优秀的人群。 


说道性格吧,即使是内向型的,可能针对十二星座还是衍生出不同的人生和结果,每个星座的也是有区别的。而在这里面最突出的我觉得是天蝎座的人群。


身为天蝎座的我,经常会想到那些和我一个星座的大佬们:


搜狐创始人张朝阳、腾讯创始人马化腾、百度创始人李彦宏、雅虎创始人杨致远、微软创始人比尔.盖茨、联想集团CEO杨元庆、推特CEO杰克.多尔西、新浪董事长曹国伟。


他们的成长也一直在激励着我。


这些经验对正在阅读文章的你有用吗,欢迎一起交流,让我们一起交流您遇到的问题。 


这篇文章是去年写的,今天增加了点内容,掘金上同步更新了一下,希望可以被更多的人看到。


如果这篇文章说道你心里了,可以点赞、分享、评论、收藏、转发哦。


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

真正的成长没有速成剂,都是风吹雨打过来的

一个人真正的成长一定是极其不容易的,如果想通过一两本书,一两个鸡汤文案,一两场培训就能够获得成长,那简直是痴人说梦,真正的成长一定不会是轻松的,一定是经过一次又一次的跌倒,然后爬起,对所做过的事,所经历的事进行一次又一次的复盘,总结,思考,最终才能慢慢认识到事...
继续阅读 »

一个人真正的成长一定是极其不容易的,如果想通过一两本书,一两个鸡汤文案,一两场培训就能够获得成长,那简直是痴人说梦,真正的成长一定不会是轻松的,一定是经过一次又一次的跌倒,然后爬起,对所做过的事,所经历的事进行一次又一次的复盘,总结,思考,最终才能慢慢认识到事物的本质,成长不是时间的堆积,也不会因为年龄递增而获得。


思考的难关


毫不夸张的说,思考是这个世界上最难的事,因为思考是要动脑的,而在现在这个信息爆炸的时代,我们想要任何资讯,任何知识,都可以找到现成的答案,所以懒惰就此滋生出来,“都有现成的答案了,我干嘛还要去动脑,我动脑得到的东西也未必有现成的好,而且动脑肚子还消耗能量,这种消耗不亚于体力劳动”,所以思考是最难的,而思考也是获取知识最快的途径。


我们在读书的时候,有些同学看似十分努力,一天感觉都是泡在书本里面的,但是成绩往往都不理想,初高中时,班上有些同学十分努力,对于我这种混子来说,我一定是扛不住的,就比如背英语单词,我发现有一些同学采用“原始人”的方式去背诵,因为我们整个初中换了三四个英语老师,而每个老师的教学方式不一样,其中一个老师就是教死记硬背,英语单词就比如“good”,她的方式是,“g o o d , g o o d”,也就是一个字母一个字母的背诵,后来因为一些原因,又换了老师,老师又教了音标,但是最后我还是发现,很多同学依旧还是“g o o d”,后来我才发现,因为学音标还需要花时间,还要动点脑子,对于一个单词,还有不同的情况,所以还是司机硬背好,这种方式就是“蛮力”,其实只要稍微花点时间去研究一下音标,然后再好好学学,背单词就会轻松很多,所以初高中英语成绩一直比较好,当然,现在很差,词汇量很少,完全是后面吃了懒惰的大亏。


所以,思考虽然是痛苦的,但是熬过痛苦期,就能够飞速成长,如果沉浸在自我感动的蛮力式努力中,那么只会离成长越来越远。


懒惰的魔咒


说到懒惰,我们可能会想到睡懒觉,不努力学习,不努力工作,但这其实并不是懒惰,每天起得很早去搬砖,日复一日地干着重复的事,却没有半点成长,这才是真正的懒惰。


没有思考的勤快是一文不值的,在现在这个社会,各种工具十分普遍,如果我们依旧保持原始人的工作方式,那么最终只会把自己累死,就像很多统计工作,如果认为自己加班到十二点,人工统计出数据来,老板就会很欣赏你,觉得你很吃苦耐劳,那么这是愚蠢的,因为有很多工具可能五分钟就能够搞出来,可偏偏固执去搞一些没用的东西,这有用吗,还有现在是人工智能时代,各种GPT工具那么爽,直接让效率翻倍,但是我就是不用,我就喜欢自己从头搞,那也没办法。


很多时候,所谓的勤快不过是为了掩饰自己的懒惰而已,懒惰得不愿意去接受新的事物,不愿意去学习新东西,总觉得“老一套“万能。


成长过程中,要不断打破自己的认知,冲破自己的心灵上的懒惰,拥抱新鲜事物,这样才不至于和主流脱节。


环境的影响



在南瓜里度日,就成圆形;在竹子里生活,就成长形。



一个人的环境同样是塑造成长的重要因素。环境不仅指物理环境,也包括人际环境和心理环境。在环境中,我们需要学会适应和改变环境,让环境成为我们成长的动力。


人以类聚,物以群分,如果我们身边的人都是不思上进,终日惶惶,那么长时间下来,我们也会受影响,读书时,如果身边的同学都好学,那么自己也绝对不会变得很烂,相反,如果同学都整天无所事事,那么自己自然也不会好到哪里去,当身边的人都是勤于思考,有想法,那么大家就会有一个良好的氛围,这样成长得就比较快,工作中,如果大家都很有热情,分享很多,学习很多,那么自己也不好意思,自然也会去学习。


但是我们每个人的能力都不一样,所以遇到的环境也不一样,所以很多时候,这并不是我们能选择的,所以说在自己没能力选择的时候,那么就要克制自己,别人混,自己不能混,要时刻提醒自己不能松懈,也不要因为别人的闲言碎语而去”同流合污“,始终记住,一切都是为了自己变得更好,不要太在意别人的看法。


保持良好的心态


在这个浮躁的社会,我们的思想和意志总是被这个社会所影响,特别现在短视频如此火爆,”脉脉上面低于百万年薪不好意思发言,抖音上面人均劳斯莱斯,自己同学朋友又买了几套房“,我们的心态时不时会受到打击,原本平稳的步伐一下变得不稳了,想一步升天了,但是当步子迈大了,可能就受伤了。


我们要时刻提醒自己自己是为自己而活,无论别人是真还是假,和自己关系不大,不要被外界过于影响,这个世界上没有一个人的成功是轻易的,都是在黑暗中努力发光的,如果相信了速成,相信快速致富,那么镰刀造已经嫁到脖子上了,而且还割坏了很多把,一茬接着一茬!


即使此刻多么的不堪,也不要放弃,积累自己,也许有一天,我们再相逢,睁开眼睛看,我才是英雄,他日若遂凌云志,敢笑黄巢不丈夫!


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

Android gradle迁移至kts

背景 在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持...
继续阅读 »

背景


在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持kotlin好久了,但是由于编译速度或者转换成本的原因,真正实现kts转换的项目很少。在笔者的mac m1 中使用最新版的AS去编译build.gradle.kts,速度已经是和用groovy写的gradle脚本不相上下了,所以就准备写了这篇文章,希望做一个记录与分享。



















groovykotlin
好处:构建速度较快,运用广泛,动态灵活好处:编译时完成所有,语法简洁,android项目中可用一套语言开发构建脚本与app编写
坏处:语法糖的作用下,很难理解gradle运行的全貌,作用单一,维护成本较高坏处:编译略慢于groovy,学习资料较少

虽然主流的gradle脚本编写依旧是groovy,但是android开发者官网也在推荐迁移到kotlin


编译前准备


这里推荐看看这篇文章,里面也涵盖了很多干货,


全局替换‘’为“”


在kotlin中,表示一个字符串用“”,不同于groovy的‘ ’,所以我们需要全局替换。可以通过快捷方式command/control+shift+R 全局替换,选中匹配正则表达式并设定file mask 为 *.gradle:

正则表达式
'(.*?[^\\])'
作用范围为
"$1"

image.png


全局替换方法调用


在groovy中,方法是可以隐藏(),举个例子

apply plugin: "com.android.application"

这里实际上是调用apply方法,然后命名参数是plugin,内容围为"com.android.application",然而在kotlin语法中,我们需要以()或者invoke的方式才能调用一个方法,所以我们要给所有的groovy函数调用添加()

正则表达式
(\w+) (([^=\{\s]+)(.*))
作用范围为
$1($2)

image.png
很遗憾的是,这个对于多行来说还是存在不足的,所以我们全局替换后还需要手动去修正部分内容即可,这里我们只要记得一个原则即可,想要调用一个kotlin函数,把参数包裹在()内即可,比如调用一个task函数,那么参数即为

task(sourcesJar(type: Jar) {
from(android.sourceSets.main.java.srcDirs)
classifier = "sources"
})

gradle kt化


接下来我们只需要把build.gradle 更改为文件名称为build.gradle.kts 即可,由于我们修改文件为了build.gradle.kts,所以当前就以kts脚本进行编译,所以很多的参数都是处于找不到状态的,即使sync也会报错,所以我们需要把报错的地方先注释掉,然后再进行sync操作,如果成功的话,AS就会帮我们进行一次编译,此时就可以有代码提示了。


开始前准备


以kotlin的方式编译,此时函数就处于可点击查看状态,区别于groovy,因为groovy是动态类型语言,所以很多做了很多语法糖,但是也给我们在debug阶段带来了很多困难,比如没有提示等等,因为groovy只需要保证在运行时找到函数即可,而kotlin却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如


image.png
对于这种动态函数,kotlin for gradle 其实也给我们内置了很多参数来对应着groovy的动态函数,下面我们来从以下方面去实践吧,tip:以下是gradle脚本编写常用


ext


我们在groovy脚本中,可以定义额外的变量在ext{}中,那么这个在kotlin中可以使用吗?嘿嘿,能用我就不会提到对吧!对的,不可以,因为ext也是一个动态函数,我们kotlin可没法用呀!那怎么办!别怕,kts中给我们定义了一个类似的变量,即extra,我们可以通过by extra去定义,然后就可以自由用我们的myNewProperty变量啦!

val myNewProperty by extra("initial value")

但是,如果我们在其他的gradle.kts脚本中用myNewProperty这个变量,那么也会找不到,因为myNewProperty这个的作用域其实只在当前文件中,确切来说是我们的build.gradle 最后会被编译生成一个Build_Init的类,这个类里面的东西能用的前提是,被先编译过!如果当前编译中的module引用了未被编译的module的变量,这当然不可行啦!当然,还是有对策的,我们可以在BuildScr这个module中定义自定义的函数,因为BuildScr这个module被定义在第一个先执行的module,所以我们后面的module就可以引用到这个“第一个module”的变量的方式去引用自定义的变量!


task



  • 新建task
groovy版本

task clean(type: Delete) {
delete rootProject.buildDir
}

比如clean就是一个我们自定义的task,转换为kotlin后其实也很简单,task是一个函数名,Delete是task的类型,clean是自定义名称

task<Delete>("clean",{
delete(rootProject.buildDir)
})

当然,我们的task类型可能在编写的由于泛型推断,隐藏了具体的类型,这个时候我们可以通过

 ./gradlew help --task task名

去查看相应的类型



  • 已有task修改
    对于有些是已经在gradle编译时存在的函数任务,比如
groovy版本

wrapper{
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN
}

这个我们kotlin版本的build.gradle能不能识别呢?其实是不可以的,因为编译器也不知道从哪里去找wrapper的定义,因为这个函数在groovy中隐藏了作用域,其实它存在于TaskContainerScope这个作用域中,所以对于所有的的task,其实都是执行在这里面的,我们可以通过tasks去找到

tasks {
named<Wrapper>("wrapper") {
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN

}
}

这种方式,去找到一个我们想要的task,并配置其内容



  • 生命周期函数
    我们可以通过函数调用的方式去配置相应的生命周期函数,比如doLast
tasks.create("greeting") {
doLast { println("Hello, World!") }
}

再比如dependOn

task<Jar>("javadocJar", {
dependsOn(tasks.findByName("javadoc"))
})

动态函数


sourceSets就是一个典型的动态函数,为什么这么说,因为很多plugin都有自己的设置,比如Groovy的sourceSets,再比如Android的SourceSets,它其实是一个接口,正在实现其实是在plugin中。如果我们需要自定义配置一些东西,比如配置jniLibs的libs目录,直接迁移到kts就会出现main找不到的情况,这里是因为main不是一个内置的函数,但是存在相应的成员,这个时候我们可以通过by getting方式去获取,只要我们的变量在作用域内是存在的(编译阶段会添加),就可以获取到。如果我们想要生成其他成员,也可以通过by creating{}方式去生成一个没有的成员

sourceSets{
val main by getting{
jniLibs.srcDirs("src/main/libs")
jni.srcDirs()
}

}

也可以通过getByName方式去获取

sourceSets.getByName("main")

plugins


在比较旧的版本中,我们AS默认创建引入一个plugin的方式是

apply plugin: 'com.android.application'

其实这也是依赖了groovy的动态编译机制,这里针对的是,比如android{}作用域,如果我们转换成了build.gradle.kts,我们会惊讶的发现,android{}这个作用域居然爆红找不到了!这个时候我们需要改写成

plugins {
id("com.android.application")
}

就能够找到了,那么这背后的原理是什么呢?我们有必要去探究一下gradle的内部实现。


说了这么多的应用层写法,了解我的小伙伴肯定知道,原理解析肯定是放在最后啦!但是gradle是一个庞大的工程,单单靠着干唠是写不完的,所以我选出了最重要的一个例子,即plugins的解析,希望能够抛砖引玉,一起学习下去吧!


Plugins解析


我们可以通过在gradle文件中设置断点,然后debug运行gradle调试来学习gradle,最终在编译时,我们会走到DefaultScriptPluginFactory中进行相应的任务生成,我们来看看


DefaultScriptPluginFactory

            final ScriptTarget initialPassScriptTarget = initialPassTarget(target);

ScriptCompiler compiler = scriptCompilerFactory.createCompiler(scriptSource);

// 第一个阶段Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else
CompileOperation<?> initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
Class<? extends BasicScript> scriptType = initialPassScriptTarget.getScriptClass();
ScriptRunner<? extends BasicScript, ?> initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
initialRunner.run(target, services);

PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);

PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);

// 第二个阶段Pass 2, compile everything except buildscript {}, pluginManagement{}, and plugin requests, then run
final ScriptTarget scriptTarget = secondPassTarget(target);
scriptType = scriptTarget.getScriptClass();

CompileOperation<BuildScriptData> operation = compileOperationFactory.getScriptCompileOperation(scriptSource, scriptTarget);

final ScriptRunner<? extends BasicScript, BuildScriptData> runner = compiler.compile(scriptType, operation, targetScope, ClosureCreationInterceptingVerifier.INSTANCE);
if (scriptTarget.getSupportsMethodInheritance() && runner.getHasMethods()) {
scriptTarget.attachScript(runner.getScript());
}
if (!runner.getRunDoesSomething()) {
return;
}

Runnable buildScriptRunner = () -> runner.run(target, services);

boolean hasImperativeStatements = runner.getData().getHasImperativeStatements();
scriptTarget.addConfiguration(buildScriptRunner, !hasImperativeStatements);
}




可以看到,源码中特别注释了,编译时的两个阶段,我们可以看到,所有的script(指函数调用),都是分别经过了阶段1和阶段2之后才真正生效的。


image.png


那么为什么android作用域在apply plugin的方式不行,plugins方式却可以呢?其实就是两个运行阶段不一致的问题。groovy可以在运行时动态找到android 这个函数,即使两者都在阶段2运行,因为groovy语法本身的特性,即使android这个函数没有定义我们也可以引用,也是在运行时阶段报错。而kotlin不一样,kotlin需要在编译的时候需要找到我们要引用的函数,即android,所以同一个阶段即plugin都没有生效(需要执行完阶段才生效),我们当然也找不到android函数,那为什么plugins又可以呢?其实很容易想到,因为plugins是在第一阶段中执行并生效的,而android引用在第二个阶段,我们接着看源码


重点关注一下compileOperationFactory.getPluginsBlockCompileOperation方法,这个方法的实现类是DefaultCompileOperationFactory,在这里我们可以看到里面定义了两个阶段

public class DefaultCompileOperationFactory implements CompileOperationFactory {
private static final StringInterner INTERNER = new StringInterner();
private static final String CLASSPATH_COMPILE_STAGE = "CLASSPATH";
private static final String BODY_COMPILE_STAGE = "BODY";

private final BuildScriptDataSerializer buildScriptDataSerializer = new BuildScriptDataSerializer();
private final DocumentationRegistry documentationRegistry;

public DefaultCompileOperationFactory(DocumentationRegistry documentationRegistry) {
this.documentationRegistry = documentationRegistry;
}

public CompileOperation<?> getPluginsBlockCompileOperation(ScriptTarget initialPassScriptTarget) {
InitialPassStatementTransformer initialPassStatementTransformer = new InitialPassStatementTransformer(initialPassScriptTarget, documentationRegistry);
SubsetScriptTransformer initialTransformer = new SubsetScriptTransformer(initialPassStatementTransformer);
String id = INTERNER.intern("cp_" + initialPassScriptTarget.getId());
return new NoDataCompileOperation(id, CLASSPATH_COMPILE_STAGE, initialTransformer);
}

public CompileOperation<BuildScriptData> getScriptCompileOperation(ScriptSource scriptSource, ScriptTarget scriptTarget) {
BuildScriptTransformer buildScriptTransformer = new BuildScriptTransformer(scriptSource, scriptTarget);
String operationId = scriptTarget.getId();
return new FactoryBackedCompileOperation<>(operationId, BODY_COMPILE_STAGE, buildScriptTransformer, buildScriptTransformer, buildScriptDataSerializer);
}
}

getPluginsBlockCompileOperation中创建了一个InitialPassStatementTransformer类对象,我们关注transform方法的内容,即如果找到了plugins,我们就进行接下来的transform操作transformPluginsBlock,这就验证了,plugins的确在第一个阶段即classpath阶段运行


@Override
public Statement transform(SourceUnit sourceUnit, Statement statement) {
...

if (scriptBlock.getName().equals(PLUGINS)) {
return transformPluginsBlock(scriptBlock, sourceUnit, statement);
}
...


总结


文章列出来了几个关键的迁移了,相信大部分的问题都可以解决了,的确在迁移到kotlin之后,还是存在一定的迁移成本的,大部分就只能生啃官网介绍,希望看完都有收获吧!


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

Android的线程和线程池

从用途上来说Android的线程主要分为主线程和子线程两类,主线程主要处理和界面相关的工作,子线程主要处理耗时操作。除Thread之外,Android中还有其他扮演线程的角色如AsyncTask、IntentService、HandleThread,其中Asy...
继续阅读 »

从用途上来说Android的线程主要分为主线程和子线程两类,主线程主要处理和界面相关的工作,子线程主要处理耗时操作。除Thread之外,Android中还有其他扮演线程的角色如AsyncTask、IntentService、HandleThread,其中AsyncTask的底层用到了线程池,IntentService和HandleThread的底层直接使用了线程。


AsyncTask内部封装了线程池和Handler主要是为了方便开发者在在线程中更新UI;HandlerThread是一个具有消息循环的线程,它的内部可以使用Handler;IntentService是一个服务,系统对其进行了封装使其可以更方便的执行后台任务,IntentService内部采用HandleThread来执行任务,当任务执行完毕后IntentService会自动退出。IntentService是一个服务但是它不容易被系统杀死因此它可以尽量的保证任务的执行。


1.主线程和子线程


主线程是指进程所拥有的的线程,在Java中默认情况下一个进程只能有一个线程,这个线程就是主线程。主线程主要处理界面交互的相关逻辑,因为界面随时都有可能更新因此在主线程不能做耗时操作,否则界面就会出现卡顿的现象。主线程之外的线程都是子线程,也叫做工作线程。


Android沿用了Java的线程模型,也有主线程和子线程之分,主线程主要工作是运行四大组件及处理他们和用户的交互,子线程的主要工作就是处理耗时任务,例如网络请求,I/O操作等。Android3.0开始系统要求网络访问必须在子线程中进行否则就会报错,NetWorkOnMainThreadException


2.Android中的线程形态


2.1 AsyncTask


AsyncTask是一个轻量级的异步任务类,它可以在线程池中执行异步任务然后把执行进度和执行结果传递给主线程并在主线程更新UI。从实现上来说AsyncTask封装了Thread和Handler,通过AsyncTask可以很方便的执行后台任务以及主线程中访问UI,但是AsyncTask不适合处理耗时任务,耗时任务还是要交给线程池执行。


AsyncTask的四个核心类如下:





    • onPreExecute():主要用于做一些准备工作,在主线程中执行异步任务执行之前

    • doInBackground(Params ... params):在线程池执行,此方法用于执行异步任务,params表示输入的参数,在此方法中可以通过publishProgress方法来更新任务进度,publishProgress会调用onProgressUpdate

    • onProgressUpdate(Progress .. value):在主线程执行,当任务执行进度发生改变时会调用这个方法

    • onPostExecute(Result result):在主线程执行,异步任务之后执行这个方法,result参数是返回值,即doInBackground的返回值。




2.2 AsyncTask的工作原理


2.3 HandleThread


HandleThread继承自Thread,它是一种可以使用Handler的Thread,它的实现在run方法中调用Looper.prepare()来创建消息队列然后通过Looper.loop()来开启消息循环,这样在实际使用中就可以在HandleThread中创建Handler了。

@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

HandleThread和Thread的区别是什么?





    • Thread的run方法中主要是用来执行一个耗时任务;

    • HandleThread在内部创建了一个消息队列需要通过Handler的消息方式来通知HandleThread执行一个具体的任务,HandlerThread的run方法是一个无限循环因此在不使用是调用quit或者quitSafely方法终止线程的执行。HandleTread的具体使用场景是IntentService。




2.4 IntentService


IntentService继承自Service并且是一个抽象的类因此使用它时就必须创建它的子类,IntentService可用于执行后台耗时的任务,当任务执行完毕后就会自动停止。IntentService是一个服务因此它的优先级要比线程高并且不容易被系统杀死,因此可以利用这个特点执行一些高优先级的后台任务,它的实现主要是HandlerThread和Handler,这点可以从onCreate方法中了解。

//IntentService#onCreate
@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

当IntentService第一次被启动时回调用onCreate方法,在onCreate方法中会创建HandlerThread,然后使用它的Looper创建一个Handler对象ServiceHandler,这样通过mServiceHandler把消息发送到HandlerThread中执行。每次启动IntentService都会调用onStartCommand,IntentService在onStartCommand中会处理每个后台任务的Intent。

//IntentService#onStartCommand
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

//IntentService#onStart
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

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

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

onStartCommand是如何处理外界的Intent的?


在onStartCommand方法中进入了onStart方法,在这个方法中IntentService通过mserviceHandler发送了一条消息,然后这个消息会在HandlerThread中被处理。mServiceHandler接收到消息后会把intent传递给onHandlerIntent(),这个intent跟启动IntentService时的startService中的intent是一样的,因此可以通过这个intent解析出启动IntentService传递的参数是什么然后通过这些参数就可以区分具体的后台任务,这样onHandleIntent就可以对不同的后台任务做处理了。当onHandleIntent方法执行结束后IntentService就会通过stopSelf(int startId)方法来尝试停止服务,这里不用stopSelf()的原因是因为这个方法被调用之后会立即停止服务但是这个时候可能还有其他消息未处理完毕,而采用stopSelf(int startId)方法则会等待所有消息都处理完毕后才会终止服务。调用stopSelf(int startId)终止服务时会根据startId判断最近启动的服务的startId是否相等,相等则立即终止服务否则不终止服务。


每执行一个后台任务就会启动一次intentService,而IntentService内部则通过消息的方式向HandlerThread请求执行任务,Handler中的Looper是顺序处理消息的,这就意味着IntentService也是顺序执行后台任务的,当有多个后台任务同时存在时这些后台任务会按照外界发起的顺序排队执行。


3.Android中的线程池


线程池的优点:





    • 线程池中的线程可重复使用,避免因为线程的创建和销毁带来的性能开销;

    • 能有效控制线程池中的最大并发数避免大量的线程之间因互相抢占系统资源导致的阻塞现象;

    • 能够对线程进行简单的管理并提供定时执行以及指定间隔循环执行等功能。




Android的线程池的概念来自于Java中的Executor,Executor是一个接口,真正的线程的实现是ThreadPoolExecutor,它提供了一些列参数来配置线程池,通过不同的参数可以创建不同的线程池。


3.1 ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

ThreadPoolExecutor是线程池的真正实现,它的构造函数中提供了一系列参数,先看一下每个参数的含义:





    • corePoolSize:线程池的核心线程数,默认情况下核心线程会在线程池中一直存活即使他们处于闲置状态。如果将ThreadPoolExecutor的allowCoreThreadTimeOut置为true那么闲置的核心线程在等待新的任务到来时会有超时策略,超时时间由keepAliveTime指定,当等待时间超过keepAliveTime设置的时间后核心线程就会被终止。

    • maxinumPoolSize:线程池中所能容纳的最大线程数,当活动线程达到做大数量时后续的新任务就会被阻塞。

    • keepAliveTime:非核心线程闲置时的超时时长,超过这个时长非核心线程就会被回收。

    • unit:用于指定超时时间的单位,常用单位有毫秒、秒、分钟等。

    • workQueue:线程池中的任务队列,通过线程池中的execute方法提交的Runnable对象会存储在这个参数中。

    • threadFactory:线程工厂,为线程池提供创建新的线程的功能。

    • handler:这个参数不常用,当线程池无法执行新的任务时,这可能是由于任务队列已满或者无法成功执行任务,这个时候ThreadPoolExecutor会调用handler的rejectExecution方法来通知调用者。




ThreadPoolExecutor执行任务时大致遵循如下规则:





    1. 如果线程池中的线程数量没有达到核心线程的数量那么会直接启动一个核心线程来执行任务;

    2. 如果线程池中线程数量已经达到或者超过核心线程的数量那么会把后续的任务插入到队列中等待执行;

    3. 如果任务队列也无法插入那么在基本可以确定是队列已满这时如果线程池中的线程数量没有达到最大值就会立刻创建非核心线程来执行任务;

    4. 如果非核心线程的创建已经达到或者超过线程池的最大数量那么就拒绝执行此任务,同时ThreadPoolExecutor会通过RejectedExecutionHandler抛出异常rejectedExecution。




3.2线程池的分类



  • FixedThreadPool:它是一种数量固定的线程池,当线程处于空闲状态时也不会被回收,除非线程池被关闭。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有空闲线程出来。FixedThreadPool只有核心线程并且不会被回收因此它可以更加快速的响应外界的请求。

  • CacheThreadPool:它是一种线程数量不定的线程池且只有非核心线程,线程的最大数量是Integer.MAX_VALUE,当线程池中的线程都处于活动状态时如果有新的任务进来就会创建一个新的线程去执行任务,同时它还有超时机制,当一个线程闲置超过60秒时就会被回收。

  • ScheduleThreadPool:它是一种拥有固定数量的核心线程和不固定数量的非核心线程的线程池,当非核心线程闲置时会立即被回收。

  • SignleThreadExecutor:它是一种只有一个核心线程的线程池,所有任务都在同一个线程中按顺序执行。

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

我从 Android 官方 App 中学到了什么?

最近 Android 官方开源了一个新的 App: Now in Android ,这个 App 主要展示了其他 App 可能没有的一些最佳实践、架构设计、以及完整的线上 App (后面会发布到 Google Play 商店中)解决方案,其次是帮助开发者及时了...
继续阅读 »

最近 Android 官方开源了一个新的 App: Now in Android ,这个 App 主要展示了其他 App 可能没有的一些最佳实践、架构设计、以及完整的线上 App (后面会发布到 Google Play 商店中)解决方案,其次是帮助开发者及时了解到自己感兴趣的 Android 开发领域。现在已经在 GitHub 中开源。


通过这篇文章你可以了解到 Now in Android 的应用架构:分层、关键类以及他们之间的交互。


目标&要求


App 的架构目标有以下几点:



  • 尽可能遵循 官方架构指南

  • 易于开发人员理解,没有什么太实验性的特性。

  • 支持多个开发人员在同一个代码库上工作。

  • 在开发人员的机器上和使用持续集成 (CI) 促进本地和仪器测试。

  • 最小化构建时间。


架构概述


App 目前包括 Data layerUI layerDomain layer 正在开发中。


Diagram showing overall app architecture


该架构遵循单向数据流的响应式编程方式。Data Layer 位于底层,主要包括:



  • UI Layer 需对 Data Layer 的变化做出反应。

  • 事件应向下流动。

  • 数据/状态应向上流动。


数据流是采用 Kotlin Flows 来实现的。


示例:在 For you 页面展示新闻信息


App 首次运行的时候,会尝试从云端加载新闻列表(选择 stagingrelease 构建变体时,debug 构建将使用本地数据)。加载后,这些内容会根据用户选择的兴趣显示给用户。


下图详细展示了事件以及数据是流转的。


Diagram showing how news resources are displayed on the For You screen


下面是每一步的详细过程。 Code 列中的内容是对应的代码,可以下载项目后在 Android Studio 查看。






























































步骤描述Code
1App 启动的时候,WorkManager 的同步任务会把所有的 Repository 添加到任务队列中。SyncInitializer.create
2初始状态会设置为 Loading,这样会在 UI 页面上展示一个旋转的动画。ForYouFeedState.Loading
3WorkManager 开始执行 OfflineFirstNewsRepository 中的同步任务,开始同步远程的数据源。SyncWorker.doWork
4OfflineFirstNewsRepository 开始调用 RetrofitNiaNetwork 开始使用 Retrofit 进行真正的网络请求。OfflineFirstNewsRepository.syncWith
5RetrofitNiaNetwork 调用云端接口。RetrofitNiaNetwork.getNewsResources
6RetrofitNiaNetwork 接收到远程服务器返回的数据。RetrofitNiaNetwork.getNewsResources
7OfflineFirstNewsRepository 通过 NewsResourceDao 将远程数据更新(增删改查)到本地的 Room 数据库中。OfflineFirstNewsRepository.syncWith
8NewsResourceDao 中的数据发生变化的时候,其会被更新到新闻的数据流(Flow)中。NewsResourceDao.getNewsResourcesStream
9OfflineFirstNewsRepository 扮演数据流中的 中间操作符, 将 PopulatedNewsResource (数据层内部数据库的一个实体类) 转换成公开的 NewsResource 实体类供其他层使用。OfflineFirstNewsRepository.getNewsResourcesStream
10ForYouViewModel 接收到 Success 成功, ForYouScreen 会使用新的 State 来渲染页面。页面将会展示最新的新闻内容。ForYouFeedState.Success

Data Layer


数据层包含 App 数据以及业务逻辑,会优先提供本地离线数据,它是 App 中所有数据的唯一信源。


Diagram showing the data layer architecture


每个 Repository 中都有自己的实体类(model/entity)。如,TopicsRepository 包含 Topic 实体类, NewsRepository 包含 NewsResource 实体类。


Repository 是其他层的公共的 API,提供了访问 App 数据的唯一途径。Repository 通常提供一种或多种数据读取和写入的方法。


读取数据


数据通过数据流提供。这意味着 Repository 的调用者都必须准备好对数据的变化做出响应。数据不会作为快照(例如 getModel )提供,因为无法确保它在使用时仍然有效。


Repository 以本地存储数据作为单一信源,因此从实例读取时不会出现错误。但是,当尝试将本地存储中的数据与云端数据进行合并时,可能会发生错误。有关错误的更多信息,请查看下面的数据同步部分。


示例:读取作者信息


可以用过订阅 AuthorsRepository::getAuthorsStream 发出的流来获得 List<Authors> 信息。每当作者列表更改时(例如,添加新作者时),更新后的 List<Author> 的内容都会发送到数据流中。如下:

class OfflineFirstTopicsRepository @Inject constructor(  
private val topicDao: TopicDao,
private val network: NiANetwork,
private val niaPreferences: NiaPreferences,
) : TopicsRepository {

// 监听 Room 数据的变化,当数据发生变化的时候,调用者就会收到对应的数据
override fun getTopicsStream(): Flow<List<Topic>> = topicDao.getTopicEntitiesStream().map {
it.map(TopicEntity::asExternalModel)
}

// ...
}

写入数据


为了写入数据,Repository 库提供了 suspend 函数。由调用者来确保它们在合适的 scope 中被执行。


示例: 关注 Topic


调用 TopicsRepository.setFollowedTopicId 将用户想要关注的 topic id 传入即可。


OfflineFirstTopicsRepository 中定义:

interface TopicsRepository : Syncable {

suspend fun setFollowedTopicIds(followedTopicIds: Set<String>)

}

ForYouViewModel 中定义:

class ForYouViewModel @Inject constructor(
private val topicsRepository: TopicsRepository,
// ...
) : ViewModel() {
// ...

fun saveFollowedInterests() {
// ...
viewModelScope.launch {
topicsRepository.setFollowedTopicIds(inProgressTopicSelection)
// ...
}
}
}

数据源(Data Sources)


Repository 可能依赖于一个或多个 DataSource。例如,OfflineFirstTopicsRepository 依赖以下数据源:



























名称使用目的
TopicsDaoRoom/SQLite持久化和 Topics 相关的关系型数据。
NiaPreferencesProto DataStore持久化和用户相关的非结构化偏好数据,主要是用户感兴趣的 Topics 内容。这里使用的是 .proto 文件。
NiANetworkRetrofit云端以 JSON 形式提供对应的 Topics 数据。

数据同步


Repository 的职责之一就是整合本地数据与云端数据。一旦从云端返回数据就会立即将其写入本地数据中。更新后的数据将会从本地数据(Room)中发送到相关的数据流中,调用者便可以监听到对应的变化。


这种方法可确保应用程序的读取和写入关注点是分开的,不会相互干扰。


在数据同步过程中出现错误的情况下,应采用对应的回退策略。App 中是经由 SyncWorker 代理给 WorkManager 的。 SyncWorkerSynchronizer 的实现类。


可以通过 OfflineFirstNewsRepository.syncWith 来查看数据同步的示例,如下:

class OfflineFirstNewsRepository @Inject constructor(
private val newsResourceDao: NewsResourceDao,
private val episodeDao: EpisodeDao,
private val authorDao: AuthorDao,
private val topicDao: TopicDao,
private val network: NiANetwork,
) : NewsRepository {

override suspend fun syncWith(synchronizer: Synchronizer) =
synchronizer.changeListSync(
versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion ->
network.getNewsResourceChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(newsResourceVersion = latestVersion)
},
modelDeleter = newsResourceDao::deleteNewsResources,
modelUpdater = { changedIds ->
val networkNewsResources = network.getNewsResources(ids = changedIds)
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
// ...
}
)
}

UI Layer


UI Layer 包含:



ViewModelRepository 接收数据流并将其转换为 UI State。UI 元素根据 UI State 进行渲染,并为用户提供了与 App 交互的方式。这些交互作为事件(UI Event)传递到对应的 ViewModel 中。


Diagram showing the UI layer architecture


构建 UI State


UI State 一般是通过接口和 data class 来组装的密封类。State 对象只能通过数据流的转换发出。这种方法可确保:



  • UI State 始终代表底层应用程序数据 - App 中的单一信源。

  • UI 元素处理所有可能的 UI State


示例:For You 页面的新闻列表


For You 页面的新闻列表数据源是 ForYouFeedState ,他是一个 sealed interface 类,包含 LoadingSuccess 两种状态:



  • Loading 表示数据正在加载。

  • Success 表示数据加载成功。Success 状态包含新闻资源列表。
sealed interface ForYouFeedState {
object Loading : ForYouFeedState
data class Success(val feed: List<SaveableNewsResource>) : ForYouFeedState
}

ForYouScreen 中会处理 feedState 的这两种状态,如下:

private fun LazyListScope.Feed(
feedState: ForYouFeedState,
//...
) {
when (feedState) {
ForYouFeedState.Loading -> {
// show loading
}
is ForYouFeedState.Success -> {
// show feed
}
}
}

将数据流转换为 UI State


ViewModel 从一个或者多个 Repository 中接收数据流当做冷 。将他们一起 组合 成单一的 UI State。然后使用 stateIn 将冷流转换成热流。转换的状态流使 UI 元素可以读取到数据流中最后的状态。


示例: 展示已关注的话题及作者


InterestsViewModel 暴露 StateFlow<FollowingUiState> 类型的 uiState 。通过组合 4 个数据流来创建热流:



  • 作者列表

  • 已关注的作者 ID 列表

  • Topics 列表

  • 已关注 Topics 列表的 IDs


Author 转换为 FollowableAuthorFollowableAuthor 是对 Author 的包装类, 添加了当前用户是否已经关注了作者。对 Topic 也做了相同转换。 如下:

    val uiState: StateFlow<InterestsUiState> = combine(
authorsRepository.getAuthorsStream(),
authorsRepository.getFollowedAuthorIdsStream(),
topicsRepository.getTopicsStream(),
topicsRepository.getFollowedTopicIdsStream(),
) { availableAuthors, followedAuthorIdsState, availableTopics, followedTopicIdsState ->

InterestsUiState.Interests(
// 将 Author 转换为 FollowableAuthor,FollowableAuthor 是对 Author 的包装类,
// 添加了当前用户是否已经关注了作者
authors = availableAuthors
.map { author ->
FollowableAuthor(
author = author,
isFollowed = author.id in followedAuthorIdsState
)
}
.sortedBy { it.author.name },
// 将 Topic 转换为 FollowableTopic,同 Author
topics = availableTopics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in followedTopicIdsState
)
}
.sortedBy { it.topic.name }
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)

两个新的列表创建了新的 FollowingUiState.Interests UiState 暴露给 UI 层。


处理用户交互


用户对 UI 元素的操作通过常规的函数调用传递给 ViewModel ,这些方法作为 lambda 表达式传递给 UI 元素。


示例:关注话题


InterestsScreen 通过 followTopic lambda 表达式传递事件,然后会调用到 InterestsViewModel.followTopic 函数。当用户点击关注话题的时候,函数将会被调用。然后 ViewModel 就会通过通知 TopicsRepository 处理对应的用户操作。


如下在 InterestsRoute 中关联 InterestsScreenInterestsViewModel

@Composable  
fun InterestsRoute(
modifier: Modifier = Modifier,
navigateToAuthor: (String) -> Unit,
navigateToTopic: (String) -> Unit,
viewModel: InterestsViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val tabState by viewModel.tabState.collectAsState()

InterestsScreen(
uiState = uiState,
tabState = tabState,
followTopic = viewModel::followTopic,
// ...
)
}

@Composable
fun InterestsScreen(
uiState: InterestsUiState,
tabState: InterestsTabState,
followTopic: (String, Boolean) -> Unit,
// ...
) {
//...
}


扩展阅读


本文主要是根据 Now in Android 中的 Architecture Learning Journey 整理而得,感兴趣的可以进一步阅读原文。除此之外,还可以进一步学习 Android 官方相关的资料:



关于架构指南部分,我之前也整理了部分对应的解读部分,大家可以移步查看



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

程序员能有什么好出路?

关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。 如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的...
继续阅读 »

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


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

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

出路耶


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


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


技术深造


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


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



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

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


项目管理


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


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



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

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

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


自由职业


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



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

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

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


跑单


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



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

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


插件独立开发者


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



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

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

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


独立开发者


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



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

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

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


团购


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



  1. 还是人际关系。

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

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


大公司养老团


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


其他


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


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


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

JS中的高阶函数

web
JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。 具体来说,高阶函数可以...
继续阅读 »

JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。



具体来说,高阶函数可以有以下几种形式:



  1. 接受函数作为参数的高阶函数


function map(array, fn) {
let result = [];
for (let i = 0; i < array.length; i++) {
result.push(fn(array[i]));
}
return result;
}

let numbers = [1, 2, 3, 4, 5];
let squaredNumbers = map(numbers, function(x) {
return x * x;
});
console.log(squaredNumbers); // [1, 4, 9, 16, 25]

在上面的例子中,map函数接受一个数组和一个函数作为参数,然后使用该函数对数组中的每个元素进行转换,并返回转换后的结果。




  1. 返回函数的高阶函数


function multiplyBy(n) {
return function(x) {
return x * n;
};
}

let double = multiplyBy(2);
let triple = multiplyBy(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30

在上面的例子中,multiplyBy函数返回一个函数,该函数可以将传入的参数乘以n。我们可以使用multiplyBy函数创建一个新的函数,然后使用该函数对不同的值进行乘法运算。




  1. 同时接受和返回函数的高阶函数


function compose(f, g) {
return function(x) {
return f(g(x));
};
}

function square(x) {
return x * x;
}

function addOne(x) {
return x + 1;
}

let addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(3)); // 16

在上面的例子中,compose函数接受两个函数作为参数,然后返回一个新的函数,该函数首先对输入值应用g函数,然后将结果传递给f函数,并返回f(g(x))的结果。我们可以使用compose函数创建一个新的函数,该函数可以将其他两个函数的功能组合在一起,以实现更复杂的操作。



其实,即使是业务代码中也会有很多用到高阶函数的地方,比如数组的迭代方法(map、filter、reduce等)、定时器(setTimeout和setInterval),还有比较典型的函数柯理化、函数组合(compose)、偏函数等,通过使用高阶函数,我们可以将常见的操作抽象出来,并将它们作为可重用的函数进行封装,从而使代码更加简洁、灵活和易于维护。





在使用高阶函数时,有时候需要注意回调函数中的上下文问题。如果回调函数中的this关键字不是指向我们期望的对象,就会导致程序出现错误。为了解决这个问题,可以使用bindapplycall等方法来明确指定回调函数的上下文。


let obj = {
value: 0,
increment: function() {
this.value++;
}
};

let arr = [1, 2, 3, 4, 5];

arr.forEach(obj.increment.bind(obj));
console.log(obj.value); // 5

在上面的例子中,obj.increment.bind(obj)会返回一个新函数,该函数会将this关键字绑定到obj对象上。我们可以使用这个新函数来作为forEach方法的回调函数,以确保increment方法的上下文指向obj对象。



其余还有诸如函数副作用问题、内存占用问题和性能问题等。为了解决这些问题,可以使用一些优化技巧,比如明确指定回调函数的上下文、使用纯函数、使用函数柯里化或函数组合等。这些技巧可以帮助我们更加灵活地使用高阶函数,并提高代

作者:施主来了
来源:juejin.cn/post/7232838211030302777
码的性能和可维护性。

收起阅读 »

函数实现单例模式

web
单例模式 一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。 class Foo { static instance; static init() { if (!this.instance) t...
继续阅读 »

wallhaven-gpqye7.jpg


单例模式


一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。


class Foo {
static instance;
static init() {
if (!this.instance) this.instance = new Foo();
return this.instance;
}
constructor() {}
}

// 将单例实例化 并暴露出去
export default Foo.init()


如此,我们就实现了简单的单例模式,并且在其他文件引入的时候已经是实例化过一次的了,或者交由用户者自行调用 init 也是可以的



函数实现


而在函数的实现上,其实本身类就是函数的某种抽象,如果去掉这个 new 的话,单纯用函数又是怎么做的呢?


let ipcMainInstance;
export default () => {
const init = () => {
return {
name: "phy",
hobby: "play games"
};
};

return () => {
if (!ipcMainInstance) {
ipcMainInstance = init();
}
return ipcMainInstance;
};
};

使用


const ipcInit = createIpc();
ipcInit();


因为我们使用的是二阶函数进行 init,所以写法上是二次调用才是 init,每个人的设计写法不一样。



然而这种写法上,每次都要写一个 init 方法进行单例实例化的包裹,这明显是一个重复工作,我们是否可以将 init 方法独立成一个函子,让他帮我们自动将我们传进去的函数进行处理,返回来的就是一个单例模式的函数呢?


抽象单例模式函子


// 非void返回值
type NonVoidReturn<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R extends void
? never
: T
: any;

/**
* 创建单例模式的函子
* @param {function} fn
* @returns {any} fn调用的返回值 必须得有return 可推断
*/

const createSgp = <T extends (...args: any) => any>(fn: NonVoidReturn<T>) => {
let _instance: undefined | ReturnType<T>;

return () => {
if (!_instance) {
_instance = fn();
}
return _instance;
};
};

export default createSgp;


使用上



import createSgp from "./createSgp";

const useAuto = () => {
let count = 0;

const setCount = (num: number) => {
count = num;
};

const getCount = () => count

return {
getCount,
setCount
};
};

// 将其处理成单例模式 并且暴露出去
export default createSgp(useAuto);


如此我们就完成了单例模式的包裹处理,并且是一个单例模式的函数。



对于hooks使用单例模式函数的问题


其实上面的操作看起来很酷,实际上很少会用到,因为你得考虑到,我用单例模式的意义是什么,如果这个函数只需要调用一次,那么就有必要用单例模式,但是hooks一般用到的时候,都属于操作性逻辑,尽量不应在hooks里面去做hooks初始化时有函数自执行调用,这个调用应该交由用户去做,我是这么理解hooks的,而这也就导致,hooks不应该用单例了,而且hooks用单例会有bug,请看下面的代码:


  let count = 0;
const useCount = {
count,
add(num){
count += num
}
}

这里我就一次简化useCount的return出来的东西,那么我们思考下,如果说,这个add在外部调用了,那么这个count会变吗?答案是不会,为什么呢?



因为当前add操作的count,是外部的count,并不是return对象的count,这句话可能很绕,但是仔细思考,一开始useCount(),他return的count是长什么样,此时,他其实就是数字0,那么,add改的count真的是这个return对象的count吗?相信说到这里,你就懂为什么了。



那我如果真的要联动到这个count,怎么做呢?


  const useCount = {
count: 0,
add(num){
this.count += num
}
}


答案是,用到this,此时这个add操作的count就是此时return 对象的count了,而这也跟类一个原理了,因为类更改的成员属性,都是实例对象本身的,而不是外部的,所以,他能更新上。这个问题,也是后面我发现的,所以以此记录一下。



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

小程序轮播图的高度如何与图片高度保持一致

web
一、存在现象 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例: 这是在iPhone Xr上的显示效果...
继续阅读 »

一、存在现象




  • 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例:




  • 这是在iPhone Xr上的显示效果:轮播图的指示点显示正常
    image.png




  • 这是在iPhone 5上的显示效果:轮播图的指示点就到图片下方去了
    image.png




二、解决方法


思路



  • 在图片加载完成后,获取到图片的高度,获取到之后进行赋值。这样的话,我们需要使用image标签的bindload属性,当图片加载完成时触发


image.png



  • 获取图片高度,可以当做获取这个轮播图组件的高度,这组件是小程序界面上的一个节点,可以使用获取界面上的节点信息APIwx.createSelectorQuery()来获取


const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec(function(res){
res[0].top // #the-id节点的上边界坐标
res[1].scrollTop // 显示区域的竖直滚动位置
})



  • 节点信息查询 API 可以用于获取节点属性、样式、在界面上的位置等信息。最常见的用法是使用这个接口来查询某个节点的当前位置,以及界面的滚动位置。如下图所示,里面有我们所需要的height,我们将这个height赋值给swiper组件,再令image标签mode="widthFix",即可自动适应轮播图高度和图片的高度保持一致



    • widthFix:缩放模式,宽度不变,高度自动变化,保持原图宽高比不变

    • HeightFix:缩放模式,高度不变,宽度自动变化,保持原图宽高比不变




  • 这是iPhone Xr上的数据,height:152.4375
    image.png




  • 这是iPhone 5上的数据,height:118.21875
    image.png




实现



  • wxml:轮播图


<swiper class="swiper" autoplay indicator-dots circular interval="{{4000}}" style="height: {{swiperHeight}}px;">
<block wx:for="{{banners}}" wx:key="bannerId">
<swiper-item class="swiper-item">
<image class="swiper-image" src="{{item.pic}}" mode="widthFix" bindload="getSwiperImageLoaded"></image>
</swiper-item>
</block>
</swiper>


  • js:只展示获取图片高度的代码,像获取轮播图数据代码已省略


Page({
data: {
swiperHeight: 0, // 轮播图组件初始高度
},

// 图片加载完成
getSwiperImageLoaded() {
// 获取图片高度
const query = wx.createSelectorQuery();
query.select(".swiper-image").boundingClientRect();
query.exec((res) => {
this.setData({ swiperHeight: rect.height });
});
},
})


  • 在上述代码中getSwiperImageLoaded方法也可以进行抽离到utils中成为一个工具函数,并用Promise进行返回,方便其他地方需要使用到


export default function (selector) {
return new Promise((resolve) => {
const query = wx.createSelectorQuery();
query.select(selector).boundingClientRect();
query.exec(resolve)
});
}


  • 所以在上述的实现代码中getSwiperImageLoaded方法可以进行如下的优化:


getSwiperImageLoaded() {
// 优化
queryRect(".swiper-image").then((res) => {
const rect = res[0];
this.setData({ swiperHeight: rect.height });
});
},


  • 如此一来,在iPhone 5上的轮播图组件展示也正常


image.png



  • 最后,因为获取的是轮播图,那么获取的数据就不止一条,按以上代码逻辑,获取到多少条数据就会执行多少遍setData赋值操作,所以可以考虑使用防抖或者节流进行进一步优化。


作者:晚风予星
来源:juejin.cn/post/7232625387296129080
收起阅读 »

CSS小技巧之圆形虚线边框

web
虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码: border-style: dotted dashed solid double; 这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的...
继续阅读 »

虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码:


border-style: dotted dashed solid double;

这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的边框样式为实线,左边的边框样式为双线。如下图所示:



border-style 除了上面所支持的样式还有 groove ridge inset outset 3D相关的样式设置,关于 border-style 的相关使用本文并不过多介绍,有兴趣的可以看官方文档。本文主要介绍使用CSS渐变实现更自定义化的虚线边框,以满足需求中的特殊场景使用。如封面图所示的6种情况足以体现足够自定义的边框样式,接下来看实现方式。


功能分析


基于封面图分析实现这类虚线边框应该满足一下几个功能配置:



  • 虚线的点数量

  • 虚线的颜色,可以纯色,多个颜色,渐变色

  • 虚线的粗细程度

  • 虚线点之间的间隔宽度


由于我们是自定义的虚线边框,所以尽可能不增加额外的元素,所以虚线的内容使用伪元素实现,然后使用定位覆盖在元素内容的上方,那么你肯定有疑问了,既然是覆盖在元素的上方,那不上遮挡了元素本身吗?



来到本文自定义圆形虚线边框的关键部分,这里我们使用CSS mask 实现,并配合使用 -webkit-mask-composite: source-in 显示元素本身的内容。



-webkit-mask-composite: 属性指定了将应用于一个元素的多个蒙版图像合成显示。当一个元素存在多重 mask 时,我们就可以运用 -webkit-mask-composite 进行效果叠加。



代码实现


首先基于上面分析的几个功能配置进行变量定义,方便后续更改变量值即可调整边框样式。


--n:20;   /* 控制虚线数量 */
--d:8deg; /* 控制虚线之间的距离 */
--t:5px; /* 控制虚线的粗细 */
--c:red; /* 控制虚线的颜色 */

对应不同的元素传入不同的值:


<div class="box" style="--n:3;--t:8px;--d:10deg;--c:linear-gradient(45deg,red,blue)">3</div>
<div class="
box" style="--n:6;--t:12px;--d:20deg;--c:green">6</div>

然后给伪元素设置基础的样式,定位,背景色,圆角等。


.box::after {
content: "";
position: absolute;
border-radius: 50%;
background: var(--c);
}

按不同的元素传入不同的背景色,最终的效果是这样的。



继续设置在mask中设置一个重复的锥形渐变 repeating-conic-gradient,代码如下:


repeating-conic-gradient(
from calc(var(--d)/2),
#000 0 calc(360deg/var(--n) - var(--d)),
#0000 0 calc(360deg/var(--n))
)



  • from calc(var(--d)/2) 定义了渐变的起点,以虚线之间的距离除以2可以让最终有对称的效果




  • #000 0 calc(360deg/var(--n) - var(--d)):定义了第一个颜色为黑色(#000),起点位置为0,终止位置为360deg/var(--n) - var(--d)度,基于虚线之间的距离和虚线的个数计算出每段虚线的渐变终止位置




  • #0000 0 calc(360deg/var(--n)):定义了第二个颜色为透明色,起点位置为0,终止位置为基于虚线的个数计算,这样与上一个颜色的差即是 --d 的距离,也就是我们控制虚线之间的距离。




基于上述代码现在的界面是如下效果:



上面有提到 -webkit-mask-composite 是应用于一个元素的多个蒙版图像合成显示,所以我们这里需要在mask中再增加一个蒙板进行合成最终的效果。


增加以下代码到mask中:


linear-gradient(#0000 0 0) content-box

注意这里使用了content-box作为背景盒模型,这意味着背景颜色只会应用到元素的内容区域,这段代码将创建一个只在元素内容区域的水平线性渐变背景,且是完全透明的背景色。


为什么是内容区域,因为这里和padding有关联,我们将定义的控制虚线的粗细 --t:5px; 应用到了伪元素的 padding 中。


padding: var(--t);

这样刚刚新增的透明背景就只会应用到下图的蓝色内容区域,再结合 -webkit-mask-composite,即``只剩下 padding 部分的内容,也就是我们的自定义边框部分。



增加以下代码:


-webkit-mask-composite: source-in;

即是最终的效果,因为这里增加的mask背景是透明色,这里 -webkit-mask-composite 的属性不限制使用 source-in, 其他的好几个都是一样的效果,有兴趣的可以了解了解。



都已经到这一步了,是不是应该再增加一些效果呢,给这个圆形的边框增加动起来的效果看看,增加一个简单的旋转动画 animation: rotate 5s linear infinite;,这样看着是不是更有感觉,适用的场景就多了。



码上掘金在线预览:



最后


到此整体代码实现就结束了,看完是不是感觉挺简单的,基于伪元素设置锥形渐变 repeating-conic-gradient并配合-webkit-mask-composite实现自定义圆形虚线边框的效果。这里是设置了 border-radius:50%; 圆角最终呈现的是圆形,有兴趣的可以更改CSS代码试试其他的形状颜色间距等。


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


参考



codepen.io/t_afif/pen/…



作者:南城FE
来源:juejin.cn/post/7233052510553522213
收起阅读 »

我竟然完美地用js实现默认的文本框粘贴事件

web
前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状 废话连篇 默认情况对一个文本框粘贴,应该会有这样的功能: 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后 将选中的文字替换成粘贴的文本 但是由于需求,我们需要拦截粘...
继续阅读 »

前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状



废话连篇


默认情况对一个文本框粘贴,应该会有这样的功能:



  1. 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后

  2. 将选中的文字替换成粘贴的文本


但是由于需求,我们需要拦截粘贴的事件,对剪贴板的文字进行过滤,这时候粘贴的功能都得自己实现了,而一旦自己实现,上面2个功能就不见了,我们就需要还原它。


面对这样的需求,我们肯定要控制移动光标,可是现在的网上环境真的是惨,千篇一律的没用代码...于是我就发表了这篇文章。


先上代码


    <textarea id="text" style="width: 996px; height: 423px;"></textarea>
<script>
// 监听输入框粘贴事件
document.getElementById('text').addEventListener('paste', function (e) {
e.preventDefault();
let clipboardData = e.clipboardData.getData('text');
// 这里写你对剪贴板的私货
let tc = document.querySelector("#text");
tc.focus();
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;
if(tc.selectionStart != tc.selectionEnd){
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionEnd)
}else{
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);
}

// 重新设置光标位置
tc.selectionEnd =tc.selectionStart = start
});
</script>


怎么理解上述两个功能?
第一个解释:
比如说现在文本框有:



染念真的很生气



如果我们现在在真的后面粘贴不要,变成



染念真的不要很生气|



拦截后的光标是在生气后面,但是我们经常使用发现,光标应该出现在不要的后面吧!
就像这样:



染念真的不要|很生气



第2个解释:



染念真的不要很生气



我们全选真的的同时粘贴求你,拦截后会变成



染念真的求你不要很生气|



但默认应该是:



染念求你|不要很生气



代码分析


针对第2个问题,我们应该先要获取默认的光标位置在何处,tc.selectionStart是获取光标开始位置,tc.selectionEnd是获取光标结束位置。
为什么这里我写了一个判断呢?因为默认时候,我们没有选中一块区域,就是把光标人为移动到某个位置(读到这里,光标在位置后面,现在人为移动到就是前面,这个例子可以理解不?),这个时候两个值是相等的。



233|333


^--- ^


1-- - 4


tc.selectionEnd=4,tc.selectionStart = 4



如果相等,说明就是简单的定位,tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart); ,tc.value.substring(0,tc.selectionStart)获取光标前的内容,tc.value.substring(tc.selectionStart)是光标后的内容。
如果不相等,说明我们选中了一个区域(光标选中一块区域说明我们选中了一个区域),代码只需要在最后获取光标后的内容这的索引改成tc.selectionEnd



|233333|


^----- ^


1----- 7


tc.selectionEnd=7,tc.selectionStart = 1



在获取光标位置之前,我们应该先使用tc.focus();聚焦,使得光标回到文本框的默认位置(最后),这样才能获得位置。
针对第1个问题,我们就要把光标移动到粘贴的文本之后,我们需要计算位置。
获得这个位置,一定要在tc.value重新赋值之前,因为这样的索引都没有改动。
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;这个代码和上面解释重复,很简单,我就不解释了。
最后处理完了,重新设置光标位置,tc.selectionEnd =tc.selectionStart = start,一定让selectionEnd和selectionStart相同,不然选中一个区域了。


如果我们在value重新赋值之后获取(tc.value.substr(0,tc.selectionStart)+clipboardData).length,大家注意到没,我们操作的是tc.value,value已经变了,这里的重新定位光标开始已经没有任何意义了!



载于我的博客

收起阅读 »

CSS新特性:让你的网页变得更加酷炫

随着互联网的发展,网页设计越来越重要。在过去,人们主要关注网站内容,而不太关注样式和布局。但是现代网页设计已经超出了这些基础层面,它们需要吸引用户、提高用户体验并增强用户对产品或服务的信心。为了实现这些目标,CSS(层叠样式表)的新特性已经变得越来越重要。在本...
继续阅读 »

随着互联网的发展,网页设计越来越重要。在过去,人们主要关注网站内容,而不太关注样式和布局。但是现代网页设计已经超出了这些基础层面,它们需要吸引用户、提高用户体验并增强用户对产品或服务的信心。为了实现这些目标,CSS(层叠样式表)的新特性已经变得越来越重要。在本文中,我们将介绍一些新的CSS特性,它们可以让你的网页变得更加酷炫。


1. 自定义属性


自定义属性是CSS最新的功能之一。使用这个功能,你可以定义你自己的CSS属性,并在整个代码库中重复使用。你只需定义一次属性,然后通过var()函数在整个代码库中使用。例如:


:root {
--primary-color: #FF0000;
}

.button {
background-color: var(--primary-color);
}

在上面的例子中,我们定义了一个名为--primary-color的CSS变量,并将其设置为红色。然后,我们将这个变量用于按钮的背景颜色。当你需要改变主色调时,只需改变--primary-color变量的值即可。


2. 网格布局


CSS网格布局是CSS3的新特性之一。使用网格布局,你可以轻松地创建复杂的网页布局。它基于行和列,而不是像传统布局那样基于盒子模型。例如:


.wrapper {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
grid-template-rows: repeat(3, 100px);
gap: 20px;
}

.box {
background-color: #000;
color: #FFF;
padding: 20px;
}

在上面的例子中,我们定义了一个名为wrapper的容器,并将其设置为网格布局。我们制定了三个列,第一列占整个空间的1/4,第二列占整个空间的一半,第三列占整个空间的1/4。我们还定义了三个行,每行高度为100像素。我们还给每个box加入了一些样式,以展示我们的布局。


3. 媒体查询


媒体查询是CSS用于响应式设计的技术。这意味着当用户改变浏览器窗口大小时,网页的布局会自动适应。例如:


@media screen and (max-width: 768px) {
.menu {
display: none;
}
.menu-toggle {
display: block;
}
}

在上面的例子中,我们定义了一个媒体查询,当浏览器窗口小于768像素时,将隐藏菜单并显示菜单切换按钮。这样,在移动设备上,网页的导航栏就会变得更加友好。


4. 动画


CSS动画是一种让你的网页更加生动和酷炫的技术。使用CSS动画,你可以使各种元素在网页上动起来,例如按钮、菜单等等。例如:


.button {
background-color: #FF0000;
color: #FFF;
padding: 20px;
border-radius: 10px;
animation: pulse 2s infinite;
}

@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}

在上面的例子中,我们定义了一个名为pulse的CSS动画,并将其应用于按钮。这个动画可以让按钮缓慢地放大和缩小,使它看起来更加醒目和有趣。


5. 变换


变换是CSS中的一个很棒的特性,可以让你改变元素的位置、大小、旋转角度等属性。使用变换,你可以创建一些非常惊人的效果,例如立体旋转、翻转等等。例如:


.box {
background-color: #FF0000;
width: 100px;
height: 100px;
transform: rotate(45deg);
}

在上面的例子中,我们定义了一个名为box的盒子,并将其旋转了45度。这可以让盒子看起来更加有趣和吸引人。


总结:


本文介绍了一些最新的CSS特性,包括自定义属性、网格布局、媒体查询、动画和变换。使用这些特性,你可以轻松地创建一个现代化、酷炫的网页设计,提高用户体验并增强用户对产品或服务的信心。当然,在实际开发中,我们还需要根据实际情况选择合适的技术和工具,以达到最佳的效果。

作者:饺子不放糖
来源:juejin.cn/post/7233057834287366203

收起阅读 »

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题


可以关注 程序员三时公众

作者:程序员三时
来源:juejin.cn/post/7232120266805526584
号 进行技术交流讨论

收起阅读 »

你管这破玩意叫缓存穿透?还是缓存击穿?

大家好,我是哪吒。 一、缓存预热 Redis缓存预热是指在服务器启动或应用程序启动之前,将一些数据先存储到Redis中,以提高Redis的性能和数据一致性。这可以减少服务器在启动或应用程序启动时的数据传输量和延迟,从而提高应用程序的性能和可靠性。 1、缓存预热...
继续阅读 »

大家好,我是哪吒。


一、缓存预热


Redis缓存预热是指在服务器启动或应用程序启动之前,将一些数据先存储到Redis中,以提高Redis的性能和数据一致性。这可以减少服务器在启动或应用程序启动时的数据传输量和延迟,从而提高应用程序的性能和可靠性。


1、缓存预热常见步骤


(1)数据准备


在应用程序启动或服务器启动之前,准备一些数据,这些数据可以是静态数据、缓存数据或其他需要预热的数据。


(2)数据存储


将数据存储到Redis中,可以使用Redis的列表(List)数据类型或集合(Set)数据类型。


(3)数据预热


在服务器启动或应用程序启动之前,将数据存储到Redis中。可以使用Redis的客户端工具或命令行工具来执行此操作。


(4)数据清洗


在服务器启动或应用程序启动之后,可能会对存储在Redis中的数据进行清洗和处理。例如,可以删除过期的数据、修改错误的数据等。


需要注意的是,Redis缓存预热可能会增加服务器的开销,因此应该在必要时进行。同时,为了减少预热的次数,可以考虑使用Redis的其他数据类型,如哈希表(Hash)或有序集合(Sorted Set)。此外,为了提高数据一致性和性能,可以使用Redis的持久化功能,将数据存储到Redis中,并在服务器重启后自动恢复数据。


2、代码实现


@Component
@Slf4j
public class BloomFilterInit
{
@Resource
private RedisTemplate redisTemplate;

//初始化白名单数据
@PostConstruct
public void init() {
//1 白名单客户加载到布隆过滤器
String key = "customer:1";
//2 计算hashValue,由于存在计算出来负数的可能,我们取绝对值
int hashValue = Math.abs(key.hashCode());
//3 通过hashValue和2的32次方后取余,获得对应的下标坑位
long index = (long)(hashValue % Math.pow(2,32));
log.info(key+" 对应的坑位index:{}",index);
//4 设置redis里面的bitmap对应类型白名单:whitelistCustomer的坑位,将该值设置为1
redisTemplate.opsForValue().setBit("whitelistCustomer",index,true);

}
}

二、缓存雪崩


Redis缓存雪崩是指在缓存系统中,由于某些原因,缓存的数据突然大量地被删除或修改,导致缓存系统的性能下降,甚至无法正常工作。


1、什么情况会发生缓存雪崩?


(1)误删除


由于误操作或故障,缓存系统可能会误删除一些正常的数据。这种情况通常会在数据库中发生。


(2)误修改


由于误操作或故障,缓存系统可能会误修改一些正常的数据。这种情况通常会在数据库中发生。


(3)负载波动


缓存系统通常会承受一定的负载波动,例如,在高峰期间,数据量可能会大幅增加,从而导致缓存系统的性能下降。


(4)数据变化频繁


如果缓存系统中的数据变化频繁,例如,每秒钟都会有大量的数据插入或删除,那么缓存系统可能会因为响应过慢而导致雪崩。


2、Redis缓存集群实现高可用


(1)主从 + 哨兵


(2)Redis集群


(3)开启Redis持久化机制aof/rdb,尽快恢复缓存集群。


3、如何避免Redis缓存雪崩?


(1)数据备份


定期备份数据,以防止误删除或误修改。


(2)数据同步


定期同步数据,以防止数据不一致。


(3)负载均衡


使用负载均衡器将请求分配到多个Redis实例上,以减轻单个实例的负载。


(4)数据优化


优化数据库结构,减少数据变化频繁的情况。


(5)监控与告警


监控Redis实例的性能指标,及时发现缓存系统的异常,并发出告警。


三、缓存穿透


Redis缓存穿透是指在Redis缓存系统中,由于某些原因,缓存的数据无法被正常访问或处理,导致缓存失去了它的作用。


1、什么情况会发生缓存穿透?


(1)数据量过大


当缓存中存储的数据量过大时,缓存的数据量可能会超过Redis的数据存储限制,从而导致缓存失去了它的作用。


(2)数据更新频繁


当缓存中存储的数据更新频繁时,缓存的数据可能会出现异步的变化,导致缓存无法被正常访问。


(3)数据过期


当缓存中存储的数据过期时,缓存的数据可能会失去它的作用,因为Redis会在一定时间后自动将过期的数据删除。


(4)数据权限限制


当缓存中存储的数据受到权限限制时,只有拥有足够权限的用户才能访问和处理这些数据,从而导致缓存失去了它的作用。


(5)Redis性能瓶颈


当Redis服务器的性能达到极限时,Redis缓存可能会因为响应过慢而导致穿透。


2、如何避免Redis缓存穿透?


(1)设置合理的缓存大小


根据实际需求设置合理的缓存大小,以避免缓存穿透。


(2)优化数据结构


根据实际需求优化数据结构,以减少数据的大小和更新频率。


(3)设置合理的过期时间


设置合理的过期时间,以避免缓存失去它的作用。


(4)增加Redis的并发处理能力


通过增加Redis的并发处理能力,以提高缓存的处理能力和响应速度。


(5)优化Redis服务器的硬件和软件配置


通过优化Redis服务器的硬件和软件配置,以提高Redis的性能和处理能力。


Redis缓存穿透


四、通过空对象缓存解决缓存穿透


如果发生了缓存穿透,可以针对要查询的数据,在Redis中插入一条数据,添加一个约定好的默认值,比如defaultNull。


比如你想通过某个id查询某某订单,Redis中没有,MySQL中也没有,此时,就可以在Redis中插入一条,存为defaultNull,下次再查询就有了,因为是提前约定好的,前端也明白是啥意思,一切OK,岁月静好。


这种方式只能解决key相同的情况,如果key都不同,则完蛋。


五、Google布隆过滤器Guava解决缓存穿透



1、引入pom


<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

2、创建布隆过滤器


BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);

(3)布隆过滤器中添加元素


bloomFilter.mightContain(1)

(4)判断布隆过滤器中是否存在


bloomFilter.mightContain(1)

3、fpp误判率


@Service
@Slf4j
public class GuavaBloomFilterService {
public static final int SIZE = 1000000;

//误判率
public static double fpp = 0.01;

//创建guava布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp);

public void guavaBloomFilter() {
for (int i = 1; i <= SIZE; i++) {
bloomFilter.put(i);
}
ArrayList<Integer> list = new ArrayList<>(10000);

for (int i = SIZE + 1; i <= SIZE + (10000); i++) {
if (bloomFilter.mightContain(i)) {
log.info("被误判了:{}", i);
list.add(i);
}
}
log.info("误判总数量:{}", list.size());
}
}

六、Redis缓存击穿


Redis缓存击穿是指在Redis缓存系统中,由于某些原因,缓存的数据无法被正常访问或处理,导致缓存失去了它的作用。


1、什么情况会发生缓存击穿?


根本原因:热点Key失效


(1)数据量过大


当缓存中存储的数据量过大时,缓存的数据量可能会超过Redis的数据存储限制,从而导致缓存失去了它的作用。


(2)数据更新频繁


当缓存中存储的数据更新频繁时,缓存的数据可能会出现异步的变化,导致缓存无法被正常访问。


(3)数据过期


当缓存中存储的数据过期时,缓存的数据可能会失去它的作用,因为Redis会在一定时间后自动将过期的数据删除。


(4)数据权限限制


当缓存中存储的数据受到权限限制时,只有拥有足够权限的用户才能访问和处理这些数据,从而导致缓存失去了它的作用。


(5)Redis性能瓶颈


当Redis服务器的性能达到极限时,Redis缓存可能会因为响应过慢而导致击穿。


2、如何避免Redis缓存击穿?


(1)设置合理的缓存大小


根据实际需求设置合理的缓存大小,以避免缓存穿透。


(2)优化数据结构


根据实际需求优化数据结构,以减少数据的大小和更新频率。


(3)设置合理的过期时间


设置合理的过期时间,以避免缓存失去它的作用。


(4)增加Redis的并发处理能力


通过增加Redis的并发处理能力,以提高缓存的处理能力和响应速度。


(5)优化Redis服务器的硬件和软件配置


通过优化Redis服务器的硬件和软件配置,以提高Redis的性能和处理能力。


七、Redis缓存击穿解决方案


1、互斥更新


通过双检加锁机制。


2、差异失效时间



先更新从缓存B,再更新主缓存A,而且让从缓存B的缓存失效时间长于A,保证A失效时,B还在。


作者:哪吒编程
来源:juejin.cn/post/7233052510553636901
收起阅读 »

聊一聊Kotlin协程"低级"api

聊一聊kotlin协程“低级”api Kotlin协程已经出来很久了,相信大家都有不同程度的用上了,由于最近处理的需求有遇到协程相关,因此今天来聊一Kotlin协程的“低级”api,首先低级api并不是它真的很“低级”,而是kotlin协程库中的基础api,我...
继续阅读 »

聊一聊kotlin协程“低级”api


Kotlin协程已经出来很久了,相信大家都有不同程度的用上了,由于最近处理的需求有遇到协程相关,因此今天来聊一Kotlin协程的“低级”api,首先低级api并不是它真的很“低级”,而是kotlin协程库中的基础api,我们一般开发用的,其实都是通过低级api进行封装的高级函数,本章会通过低级api的组合,实现一个自定义的async await 函数(下文也会介绍kotlin 高级api的async await),涉及的低级api有startCoroutineContinuationInterceptor


startCoroutine


我们知道,一个suspend关键字修饰的函数,只能在协程体中执行,伴随着suspend 关键字,kotlin coroutine common库(平台无关)也提供出来一个api,用于直接通过suspend 修饰的函数直接启动一个协程,它就是startCoroutine@SinceKotlin("1.3")

@Suppress("UNCHECKED_CAST")
public fun <R, T> (suspend R.() -> T).startCoroutine(
作为Receiver
receiver: R,
当前协程结束时的回调
completion: Continuation<T>
) {
createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)
}

可以看到,它的Receiver是(suspend R.() -> T),即是一个suspend修饰的函数,那么这个有什么作用呢?我们知道,在普通函数中无法调起suspend函数(因为普通函数没有隐含的Continuation对象,这里我们不在这章讲,可以参考kotlin协程的资料)


image.png
但是普通函数是可以调起一个以suspend函数作为Receiver的函数(本质也是一个普通函数)


image.png
其中startCoroutine就是其中一个,本质就是我们直接从外部提供了一个Continuation,同时调用了resume方法,去进入到了协程的世界

startCoroutine实现

createCoroutineUnintercepted(completion).intercepted().resume(Unit)

这个原理我们就不细讲下去原理,之前也有写过相关的文章。通过这种调用,我们其实就可以实现在普通的函数环境,开启一个协程环境(即带有了Continuation),进而调用其他的suspend函数。


ContinuationInterceptor


我们都知道拦截器的概念,那么kotlin协程也有,就是ContinuationInterceptor,它提供以AOP的方式,让外部在resume(协程恢复)前后进行自定义的拦截操作,比如高级api中的Diapatcher就是。当然什么是resume协程恢复呢,可能读者有点懵,我们还是以上图中出现的mySuspendFunc举例子mySuspendFunc是一个suspned函数

::mySuspendFunc.startCoroutine(object : Continuation<Unit> {
override val context: CoroutineContext
get() = EmptyCoroutineContext

override fun resumeWith(result: Result<Unit>) {

}

})

它其实等价于val continuation = ::mySuspendFunc.createCoroutine(object :Continuation<Unit>{

    override val context: CoroutineContext
get() = EmptyCoroutineContext

override fun resumeWith(result: Result<Unit>) {
Log.e("hello","当前协程执行完成的回调")
}

})
continuation.resume(Unit)

startCoroutine方法就相当于创建了一个Continuation对象,并调用了resume。创建Continuation可通过createCoroutine方法,返回一个Continuation,如果我们不调用resume方法,那么它其实什么也不会执行,只有调用了resume等执行方法之后,才会执行到后续的协程体(这个也是协程内部实现,感兴趣可以看看之前文章)


而我们的拦截器,就相当于在continuation.resume前后,可以添加自己的逻辑。我们可以通过继承ContinuationInterceptor,实现自己的拦截器逻辑,其中需要复写的方法是interceptContinuation方法,用于返回一个自己定义的Continuation对象,而我们可以在这个Continuation的resumeWith方法里面(当调用了resume之后,会执行到resumeWith方法),进行前后打印/其他自定义操作(比如切换线程)



class ClassInterceptor() :ContinuationInterceptor { override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =MyContinuation(continuation)

}
class MyContinuation<T>(private val continuation: Continuation<T>):Continuation<T> by continuation{
override fun resumeWith(result: Result<T>) {
Log.e("hello","MyContinuation start ${result.getOrThrow()}")
continuation.resumeWith(result)

Log.e("hello","MyContinuation end ")
}
}

其中的key是ContinuationInterceptor,协程内部会在每次协程恢复的时候,通过coroutineContext取出key为ContinuationInterceptor的拦截器,进行拦截调用,当然这也是kotlin协程内部实现,这里简单提一下。


实战


kotlin协程api中的 async await


我们来看一下kotlin Coroutine 的高级api async await用法

CoroutineScope(Dispatchers.Main).launch {
val block = async(Dispatchers.IO) {
// 阻塞的事项

}
// 处理其他主线程的事务

// 此时必须需要async的结果时,则可通过await()进行获取
val result = block.await()
}

我们可以通过async方法,在其他线程中处理其他阻塞事务,当主线程必须要用async的结果的时候,就可以通过await等待,这里如果结果返回了,则直接获取值,否则就等待async执行完成。这是Coroutine提供给我们的高级api,能够将任务简单分层而不需要过多的回调处理。


通过startCoroutine与ContinuationInterceptor实现自定义的 async await


我们可以参考其他语言的async,或者Dart的异步方法调用,都有类似这种方式进行线程调用

async {
val result = await {
suspend 函数
}
消费result
}

await在async作用域里面,同时获取到result后再进行消费,async可以直接在普通函数调用,而不需要在协程体内,下面我们来实现一下这个做法。


首先我们想要限定await函数只能在async的作用域才能使用,那么首先我们就要定义出来一个Receiver,我们可以在Receiver里面定义出自己想要暴露的方法

interface AsyncScope {
fun myFunc(){

}

}
fun async(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

注意这个类,resumeWith 只会跟startCoroutine的这个协程绑定关系,跟await的协程没有关系
class AsyncStub(override val context: CoroutineContext = EmptyCoroutineContext) :
Continuation<Unit>, AsyncScope {
override fun resumeWith(result: Result<Unit>) {

// 这个是干嘛的 == > 完成的回调
Log.e("hello","AsyncStub resumeWith ${Thread.currentThread().id} ${result.getOrThrow()}")
}
}

上面我们定义出来一个async函数,同时定义出来了一个AsyncStub的类,它有两个用处,第一个是为了充当Receiver,用于规范后续的await函数只能在这个Receiver作用域中调用,第二个作用是startCoroutine函数必须要传入一个参数completion,是为了收到当前协程结束的回调resumeWith中可以得到当前协程体结束回调的信息

await方法里面

suspend fun<T> AsyncScope.await(block:() -> T) = suspendCoroutine<T> {
// 自定义的Receiver函数
myFunc()

Thread{
切换线程执行await中的方法
it.resumeWith(Result.success(block()))
}.start()
}

在await中,其实是一个扩展函数,我们可以调用任何在AsyncScope中定义的方法,同时这里我们模拟了一下线程切换的操作(Dispatcher的实现,这里不采用Dispatcher就是想让大家知道其实Dispatcher.IO也是这样实现的),在子线程中调用it.resumeWith(Result.success(block())),用于返回所需要的信息


通过上面定的方法,我们可以实现

async {
val result = await {
suspend 函数
}
消费result
}

这种调用方式,但是这里引来了一个问题,因为我们在await函数中实际将操作切换到了子线程,我们想要将消费result的动作切换至主线程怎么办呢?又或者是加入我们希望获取结果前做一些调整怎么办呢?别急,我们这里预留了一个CoroutineContext函数,我们可以在外部传入一个CoroutineContext

public interface ContinuationInterceptor : CoroutineContext.Element
而CoroutineContext.Element又是继承于CoroutineContext
CoroutineContext.Element:CoroutineContext

而我们的拦截器,正是CoroutineContext的子类,我们把上文的ClassInterceptor修改一下


class ClassInterceptor() : ContinuationInterceptor {
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
MyContinuation(continuation)

}

class MyContinuation<T>(private val continuation: Continuation<T>) :
Continuation<T> by continuation {
private val handler = Handler(Looper.getMainLooper())
override fun resumeWith(result: Result<T>) {
Log.e("hello", "MyContinuation start ${result.getOrThrow()}")

handler.post {
continuation.resumeWith(Result.success(自定义内容))
}
Log.e("hello", "MyContinuation end ")
}
}

同时把async默认参数CoroutineContext实现一下即可

fun async(
context: CoroutineContext = ClassInterceptor(),
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

此后我们就可以直接通过,完美实现了一个类js协程的调用,同时具备了自动切换线程的能力

async {
val result = await {
test()
}
Log.e("hello", "result is $result ${Looper.myLooper() == Looper.getMainLooper()}")
}

结果

  E  start 
E MyContinuation start kotlin.Unit
E MyContinuation end
E end
E 执行阻塞函数 test 1923
E MyContinuation start 自定义内容数值
E MyContinuation end
E result is 自定义内容的数值 true
E AsyncStub resumeWith 2 kotlin.Unit

最后,这里需要注意的是,为什么拦截器回调了两次,因为我们async的时候开启了一个协程,同时await的时候也开启了一个,因此是两个。AsyncStub只回调了一次,是因为AsyncStub被当作complete参数传入了async开启的协程block.startCoroutine,因此只是async中的协程结束才会被回调。


image.png


本章代码


class ClassInterceptor() : ContinuationInterceptor {
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
MyContinuation(continuation)

}

class MyContinuation<T>(private val continuation: Continuation<T>) :
Continuation<T> by continuation {
private val handler = Handler(Looper.getMainLooper())
override fun resumeWith(result: Result<T>) {
Log.e("hello", "MyContinuation start ${result.getOrThrow()}")

handler.post {
continuation.resumeWith(Result.success(6 as T))
}
Log.e("hello", "MyContinuation end ")
}
}
interface AsyncScope {
fun myFunc(){

}

}
fun async(
context: CoroutineContext = ClassInterceptor(),
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

class AsyncStub(override val context: CoroutineContext = EmptyCoroutineContext) :
Continuation<Unit>, AsyncScope {
override fun resumeWith(result: Result<Unit>) {

// 这个是干嘛的 == > 完成的回调
Log.e("hello","AsyncStub resumeWith ${Thread.currentThread().id} ${result.getOrThrow()}")
}
}


suspend fun<T> AsyncScope.await(block:() -> T) = suspendCoroutine<T> {
myFunc()

Thread{
it.resumeWith(Result.success(block()))
}.start()
}
模拟阻塞
fun test(): Int {
Thread.sleep(5000)
Log.e("hello", "执行阻塞函数 test ${Thread.currentThread().id}")
return 5
}
async {
val result = await {
test()
}
Log.e("hello", "result is $result ${Looper.myLooper() == Looper.getMainLooper()}")
}

最后


我们通过协程的低级api,实现了一个与官方库不同版本的async await,同时也希望通过对低级api的设计,也能对Coroutine官方库的高级api的实现有一定的了解。



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

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题?

作者:程序员三时
来源:juejin.cn/post/7232120266805526584

收起阅读 »

Kotlin | 理解泛型使用

泛型类 & 泛型方法 泛型,指的是具体的类型泛化,多用在集合中(如List、Map),编码时使用符号代替,在使用时再确定具体类型。 泛型通常用于类和方法中,称为泛型类、泛型方法,使用示例:/** * 泛型类 */ abstract class Ba...
继续阅读 »

泛型类 & 泛型方法


泛型,指的是具体的类型泛化,多用在集合中(如ListMap),编码时使用符号代替,在使用时再确定具体类型。


泛型通常用于类和方法中,称为泛型类、泛型方法,使用示例:

/**
* 泛型类
*/
abstract class BaseBook<T> {
private var books: ArrayList<T> = ArrayList()

/**
* 泛型方法
*/
fun <E : T> add(item: E) {
books.add(item)
println("list:$books, size:${books.size}")
}
}

/**
* 子类继承BaseBook并传入泛型参数MathBook
*/
class BookImpl : BaseBook<MathBook>()

fun main() {
BookImpl().apply {
add(MathBook("数学"))
}
}

执行main()方法,输出:

list:[MathBook(math=数学)], size: 1

Java泛型通配符


? extends E 定义上界


Java中的泛型是不型变的,举个例子:IntegerObject的子类,但是List<Integer>并不是List<Object>的子类,因为List是不型变的。如:
错误
如果想让List<Integer>成为List<Object>的子类,可以通过上界操作符 ? extends E 来操作。


? extends E 表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身extends操作符可以限定上界通配符类型,使得通配符类型是协变的。注意,经过协变之后,数据是可读不可写的。示例:

//继承关系Child -> Parent 
class Parent{
protected String name = "Parent";
}

class Child extends Parent {
protected String name = "Child";
}

定义实体类,继承关系:Child -> Parent

class CList<E> {
//通过<? extends E>来定义上界
public void addAll(List<? extends E> list) {
//...
}
}

/**
* <? extends E>来定义上界,可以保证协变性
*/
public void GExtends() {
//1、Child是Parent的子类
Parent parent = new Child();

//2、协变,泛型参数是Parent
CList<Parent> objs = new CList<>();
List<Child> strs = new ArrayList<>(); //声明字符串List
strs.add(new Child());
objs.addAll(strs); //addAll()方法中的入参必须为List<? extends E>,从而保证了List<Child>是List<Parent>的子类。
}

addAll()方法中的入参必须为List<? extends E>,从而保证了List<Child>List<Parent>的子类。如果addAll()中的入参改为List<E>,则编译器会直接报错,因为List<Child>并不是List<Parent>的子类,如下:


错误


? super E 定义下界


? super E 可以看作一个E或者E的父类的“未知类型”,这里的父类包括直接和间接父类。super定义泛型的下界,使得通配符类型是逆变的。经过逆变之后,数据是可写不可读的,如: List<? super Child>List<Parent> 的一个超类。示例:

class CList<E> {
//通过<? super E>来定义下界
public void popAll(List<? super E> dest) {
//...
}

/**
* 逆变性
*/
public void GSuper(){
CList<Child> objs = new CList<>();
List<Parent> parents = new ArrayList<>(); //声明字符串List
parents.add(new Parent());
objs.popAll(parents); //逆变
}

可以看到popAll()的入参必须声明为List<? super E>,如果改为List<E>,编译器会直接报错:


错误


Kotlin泛型


Java 一样,Kolin 泛型本身也是不能型变的。



  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends T

  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super T


声明处型变


协变< out T>
interface GenericsP<T> {
fun get(): T //读取并返回T,可以认为只能读取T的对象是生产者
}

如上声明了GenericsP< T>接口,如果其内部只能读取并返回T,可以认为GenericsP实例对象为生产者(返回T)。

open class Book(val name: String)
data class EnglishBook(val english: String) : Book(english)
data class MathBook(val math: String) : Book(math)

已知EnglishBook、MathBookBook的子类,但是如果将Book、EnglishBook当成泛型放入GenericsP,他们之间的关系还成立吗?即:


error信息
可以看到编译器直接报错,因为虽然EnglishBookBook的子类,但是GenericsP<EnglishBook>并不是GenericsP<Book>的子类,如果想让这个关系也成立,Kotlin提供了out修饰符,out修饰符能够确保:



1、T只能用于函数返回中,不能用于参数输入中;
2、GenericsP< EnglishBook>可以安全的作为GenericsP< Book>的子类



示例如下:

interface GenericsP<out T> {
fun get(): T //读取并返回T,可以认为只能读取T的对象是生产者
// fun put(item: T) //错误,不允许在输入参数中使用
}

经过如上的改动后,可以看到GenericsP<EnglishBook>可以正确赋值给GenericsP<Book>了:
正确赋值


逆变< in T>
interface GenericsC<T> {
fun put(item: T) //写入T,可以认为只能写入T的对象是消费者
}

如上声明了GenericsC<T>接口,如果其内部只能写入T,可以认为GenericsC实例对象为消费者(消费T)。为了保证T只能出现在参数输入位置,而不能出现在函数返回位置上,Kotlin可以使用in进行控制:

interface GenericsC<in T> {
fun put(item: T) //写入T,可以认为只能写入T的对象是消费者
//fun get(): T //错误,不允许在返回中使用
}

继续编写如下函数:

    /**
* 称为GenericsC在Book上是逆变的。
* 跟系统源码中的Comparable类似
*/
private fun consume(to: GenericsC<Book>) {
//GenericsC<Book>实例赋值给了GenericsC<EnglishBook>
val target: GenericsC<EnglishBook> = to
target.put(EnglishBook("英语"))
}

可以看到GenericsC中的泛型参数声明为in后,GenericsC<Book>实例可以直接赋值给了GenericsC<EnglishBook>,称为GenericsCBook上是逆变的。在系统源码中我们经常使用的一个例子就是Comparable:

//Comparable.kt
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}

使用处型变(类型投影)


上一节中in、out都是写在类的声明处,从而控制泛型参数的使用场景,但是如果泛型参数既可能出现在函数入参中,又可能出现在函数返回中,典型的类就是Array:

class Array<T>(val size: Int) {
fun get(index: Int): T { …… }
fun set(index: Int, value: T) { …… }
}

这时候就不能在声明处做任何协变/逆变的操作了,如下函数中使用Array

 fun copy(from: Array<Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}

调用方:

val strs: Array<String> = arrayOf("1", "2")
val any = Array<Any>(2) {}
copy(strs, any) //编译器报错 strs其类型为 Array<String> 但此处期望 Array<Any>

错误原因就是因为Array<String>并不是Array<Any>的子类,即不是协变的,这里是为了保证数据的安全性。如果可以保证Array< String>传入copy()函数之后不能被写入,那么就保证了安全性,既然我们在声明Array时不能限制泛型参数,那么完全可以在使用处进行限制,如下:

 fun copy(from: Array<out Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}

可以看到对from添加了out限制,这种被称为使用处型变。即不允许from进行写入操作,那么就可以保证了from的安全性,再进行上面的调用时,copy(strs, any)就可以正确的执行了。


星投影< *>


当不使用协变、逆变时,某些场景下可以使用<*>来实现泛型,如:



  • 对于GenericsP<out T: Book>GenericsP< *>相当于GenericsP<out Book>,当T未知时,可以安全的从GenericsP<*>中读取Book值;

  • 对于GenericsC<in T>GenericsC<*>相当于 GenericsC<in Nothing>,当T未知时,没有安全方式写入GenericsC<*>

  • 对于Generics<T: Book>T为有上界Book的不型变参数,当Generics<*>读取时等价于Generics<out Book>;写入时等价于Generics<in Nothing>


泛型擦除


泛型参数会在编译期间存在,在运行期间会被擦除,例如:Generics<EnglishBook>Generics<MathBook> 的实例都会被擦除为 Generics<*>。运行时期检测一个泛型类型的实例无法通过is关键字进行判断,另外运行期间具体的泛型类型判断也无法判断,如: books as List<Book>,只会对非泛型部分进行检测,形如:books as List<*>


如果想具体化泛型参数,可以通过inline + reified的方式:

 /**
* inline + reified 使得类型参数被实化 reified:实体化的
* 注:带reified类型参数的内联函数,Java是无法直接调用的
*/
inline fun <reified T> isAny(value: Any): Boolean {
return value is T
}

参考


【1】https://www.kotlincn.net/docs/reference/generics.html
【2】https://mp.weixin.qq.com/s/vSwx7fgROJcrQwEOW7Ws8A
【3】https://juejin.cn/post/7042606952311947278


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

怎么做登录(单点登录)功能?

简单上个图(有水印。因为穷所以没开会员) 先分析下登陆要做啥 首先,搞清楚要做什么。 登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。 怎么落实? 怎么实现它?用什么实现? ...
继续阅读 »

简单上个图(有水印。因为穷所以没开会员)


怎么做登陆(单点登陆)?.png


先分析下登陆要做啥



首先,搞清楚要做什么。


登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。



怎么落实?



怎么实现它?用什么实现?




我们的项目是Springboot + Vue前后端分离类型的。


选择用token + redis 实现,权限的话用SpringSecurity来做。




前后端分离避不开的一个问题就是单点登陆,单点登陆咱们有很多实现方式:CAS中央认证、JWT、token等,咱们这种方式其实本身就是基于token的一个单点登陆的实现方案。


单点登陆我们改天整理一篇OAuth2.0的实现方式,今天不搞这个。



上代码



概念这个东西越说越玄。咱们直接上代码吧。



接口:
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
   AjaxResult ajax = AjaxResult.success();
   // 生成令牌
   //用户名、密码、验证码、uuid
   String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                     loginBody.getUuid());
   ajax.put(Constants.TOKEN, token);
   return ajax;
}


用户信息验证交给SpringSecurity

/**
* 登录验证
*/
public String login(String username, String password, String code, String uuid)
{
   // 验证码开关,顺便说一下,系统配置相关的开关之类都缓存在redis里,系统启动的时候加载进来的。这一块儿的代码就不贴出来了
   boolean captchaEnabled = configService.selectCaptchaEnabled();
   if (captchaEnabled)
  {
       //uuid是验证码的redis key,登陆页加载的时候验证码生成接口返回的
       validateCaptcha(username, code, uuid);
  }
   // 用户验证 -- SpringSecurity
   Authentication authentication = null;
   try
  {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       AuthenticationContextHolder.setContext(authenticationToken);
       // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername。
       //
       authentication = authenticationManager.authenticate(authenticationToken);
  }
   catch (Exception e)
  {
       if (e instanceof BadCredentialsException)
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
           throw new UserPasswordNotMatchException();
      }
       else
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
           throw new ServiceException(e.getMessage());
      }
  }
   finally
  {
       AuthenticationContextHolder.clearContext();
  }
   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
   LoginUser loginUser = (LoginUser) authentication.getPrincipal();
   recordLoginInfo(loginUser.getUserId());
   // 生成token
   return tokenService.createToken(loginUser);
}

把校验验证码的部分贴出来,看看大概的逻辑(这个代码封装得太碎了。。。没全整出来)
/**
* 校验验证码
*/
public void validateCaptcha(String username, String code, String uuid)
{
   //uuid是验证码的redis key
   String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //String CAPTCHA_CODE_KEY = "captcha_codes:";
   String captcha = redisCache.getCacheObject(verifyKey);
   redisCache.deleteObject(verifyKey);
   if (captcha == null)
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
       throw new CaptchaExpireException();
  }
   if (!code.equalsIgnoreCase(captcha))
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
       throw new CaptchaException();
  }
}

token生成部分


这里,token

/**
* 创建令牌
*/
public String createToken(LoginUser loginUser)
{
   String token = IdUtils.fastUUID();
   loginUser.setToken(token);
   setUserAgent(loginUser);
   refreshToken(loginUser);

   Map<String, Object> claims = new HashMap<>();
   claims.put(Constants.LOGIN_USER_KEY, token);
   return createToken(claims);
}

刷新token
/**
* 刷新令牌
*/
public void refreshToken(LoginUser loginUser)
{
   loginUser.setLoginTime(System.currentTimeMillis());
   loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
   // 根据uuid将loginUser缓存
   String userKey = getTokenKey(loginUser.getToken());
   redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

验证token
/**
* 验证令牌
*/
public void verifyToken(LoginUser loginUser)
{
   long expireTime = loginUser.getExpireTime();
   long currentTime = System.currentTimeMillis();
   if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
  {
       refreshToken(loginUser);
  }
}


注意这里返回给前端的token其实用JWT加密了一下,SpringSecurity的过滤器里有进行解析。


另外,鉴权时会刷新token有效期,看下面第二个代码块的注释。

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
   //...无关的代码删了
   httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
}
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
   @Autowired
   private TokenService tokenService;

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
       throws ServletException, IOException
  {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
      {
           //刷新token有效期
           tokenService.verifyToken(loginUser);
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
           authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
           SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      }
       chain.doFilter(request, response);
  }
}


这个登陆方案里用了token + redis,还有JWT,其实用哪一种方案都可以独立实现,并且两种方案都可以用来做单点登陆。


这里JWT只是起到个加密的作用,无它。


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

装了我这 10 个 IDEA 神级插件后,同事也开始情不自禁的嘚瑟了

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。 Vuesion Theme 颜值是生产...
继续阅读 »

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。


Vuesion Theme


颜值是生产力的第一要素,IDE 整好看了,每天对着它也是神清气爽,有木有?就 Intellij IDEA 提供的暗黑和亮白主色,虽然说已经非常清爽了,但时间久了总觉得需要再来点新鲜感?


Vuesion Theme 这个主题装上后,你会感觉整个 Intellij IDEA 更高级了。



安装完插件就立马生效了,瞧这该死的漂亮,整个代码着色,以及文件的图标,都更炫酷了:



当然了,主题这事,萝卜白菜各有所爱,就像玩 dota,我就喜欢露娜。


lombok


可能提到 lombok,多多少少有些争议,但不得不说,这玩意的确是很能省代码,并且很多开源的第三方 jar 包,以及 Intellij IDEA 2020.3 以后的版本也都默认加了 lombok。



这么多注解可以选择,在写 VO、DO、DTO 的时候是真的省心省力。



如果没有 lombok 的帮助,那整个代码就要炸了呀。对比一下,是不是感受还挺明显的?



当然了,要使用 lombok,你得在 pom.xml 文件中引入 lombok 的依赖包。


xml
复制代码
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

File Expander


这个插件不仅可以反编译,还可以打开 tar.gz,zip 等压缩文件,



如果有小伙伴反驳说自己不装插件也可以打开 jar 包里的代码,那是因为你的 jar 在 classpath。如果单独打开一个 jar 包,不装插件是看不了的。



GitToolBox


如果你经常使用 Git 提交代码的话,这款插件就非常的爽。



它能直接提示你远程版本库里有多少文件更新,你有多少文件没有提交到版本库,甚至可以显示上一次提交的时间和版本更新者。



Maven Helper


这插件几乎人手一个了吧,Java 后端开发必备啊。



依赖可视化的神器,可以很清楚地知道依赖的关系图谱,假如有冲突的话,也是一目了然。



Translation


对于英文能力差的同学来说,这个翻译插件简直神了,它支持 Google 翻译、有道翻译、百度翻译、Alibaba 翻译。



刚好写这篇内容的时候,发现最新的版本是 3.3.5,趁机升级一波。有了这款翻译插件,看源码绝对是爽歪歪。以前遇到不认识的单词,真的是好烦,还要切到翻译软件那里查,现在可好,单词翻译、文档翻译、注释翻译,都有了。



arthas idea


Arthas 应该大家都很熟悉了,阿里开源的一款强大的 java 在线诊断工具。


但如果每次都要你输入一长串命令的话,相信你也会很崩溃,尤其是很多时候我还记忆模糊,很多记不住。这款插件刚好解决了我这个烦恼,极大地提高了生产力



使用起来也非常方便,直接进入你要诊断的方法和类,右键选择对应的命令,就会自动帮你生成了。



Free Mybatis plugin


Mybatis 基本上是目前最主流的 ORM 框架了,相比于 hibernate 更加灵活,性能也更好。所以我们一般在 Spring Boot 项目中都会写对应的 mapper.java 和 mapper.xml。


那有了这款插件之后,两者就可以轻松关联起来。



比如,我这里要查看 ArticleMapper 的 xml,那么编辑器的行号右侧就会有一个向右的→,直接点击就跳转过去了。



想跳转回来的话,也是同样的道理,所以有了这款产检,mapper 和 xml 之间就可以自由切换了,丝滑。


VisualGC


这里给大家推荐一个 JVM 堆栈可视化工具,可以和 Intellij IDEA 深度集成——VisualGC。



当我们需要监控一个进程的时候,直接打开 VisualGC面板,就可以查看到堆栈和垃圾收集情况,可以说是一目了然。



CheckStyle-IDEA


如果你比较追求代码规范的话,可以安装这个插件,它会提醒你注意无用导入、注释、语法错误❎、代码冗余等等。



在 CheckStyle 面板中,你可以选择 Google 代码规范或者 sun 的代码规范,跑一遍检查,就可以看到所有的修改建议了。



最后


以上这 10 款 Intellij IDEA 插件也是我平常开发中经常用到的,如果大家有更好更效率的插件,也可以评论里留言。


参考链接:juejin.cn/post/702802…


没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。


本文已收录到 GitHub 上星标 4k+ 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路😄。


Github 仓库:github.com/itwanger/to…


star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。



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

为什么选择FastAPI?

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。 一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数...
继续阅读 »

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。



一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数据库 ORM 支持,功能完善、要素齐全:自带大量常用工具和框架(比如分页,auth,权限管理), 适合快速开发企业级网站。完善的文档:经过多年的发展和完善,Django 有广泛的实践案例和完善的在线文档,可以让开发人员有好的借鉴途径。并且使用众多,能够借鉴的例子也很多。目前 NetDevOps 的平台后端,大多数都是基于 Python 的 Django 来开发的,原因是:python 的确上手快,django 支持完善,能够迅速的建立和设备的联系并生成自带的管理页面。


但是Django太全了太重了,随着开发周期越往后,开发模块越来越多,整个程序就显得异常臃肿,代码逻辑相互耦合十分严重,整个系统需要保持极高的稳定性。所以,我们希望能够有个小而精的框架,可以逐步的将Django项目中的模块慢慢做拆分,做解耦,做程序的可插拔。FastAPI这时候进入到我们的视野。



FastAPI 顾名思义,一个字,就是快。基于(并完全兼容)OPenAPI 的相关开放标准。



  • 高性能:FastAPI 采用异步编程模式,基于 Starlette 框架和 pydantic 库进行开发。其性能相比于 Flask 和 Django 均有很大提升。

  • 简单易用:FastAPI 提供了自动生成 API 文档的功能,丰富的文档可以让开发人员更快速地了解 API 的使用方法。

  • 规范化:FastAPI 默认支持 OpenAPI(前身为 Swagger)和 JSON Schema,从而规范化 API 的设计和发布。

  • 类型检查:FastAPI 强制使用类型注解,使得代码更加严谨,同时可以使用 mypy 等类型检查工具来保证代码的质量。

  • 整合多种数据库支持:FastAPI 可以无缝进行整合多种数据库的使用,比如 SQLAlchemy、Tortoise ORM 等。


使用 FastAPI 可以提升性能、简化开发、规范 API 设计、增加代码可读性和可维护性,从而促进开发效率的提升。 FastAPI 站在以下巨人的肩膀之上:


Starlette 负责 web 部分。 Pydantic 负责数据部分。


Starlette


Starlette 是一个轻量级的 ASGI 框架和工具包,特别适合用来构建高性能的 asyncio 服务.


Starlette 的主要特性:



  1. 性能表现优异

  2. WebSocket 支持.

  3. GraphQL 支持.

  4. 进程内的后台任务执行

  5. 启动和关闭服务的事件触发

  6. 测试客户端构建于 requests.

  7. 支持 CORS, GZip, Static Files, Streaming 响应.

  8. 支持会话和 Cookie

  9. 100% 测试覆盖率

  10. 100% 类型注解

  11. 无依赖


Pydantic


pydantic 库是 python 中用于数据接口定义检查与设置管理的库。


pydantic 在运行时强制执行类型提示,并在数据无效时提供友好的错误。


它具有如下优点:



  1. 与 IDE/linter 完美搭配,不需要学习新的模式,只是使用类型注解定义类的实例

  2. 多用途,BaseSettings 既可以验证请求数据,也可以从环境变量中读取系统设置

  3. 快速

  4. 可以验证复杂结构

  5. 可扩展,可以使用 validator 装饰器装饰的模型上的方法来扩展验证

  6. 数据类集成,除了 BaseModel,pydantic 还提供了一个 dataclass 装饰器,它创建带有输入数据解析和验证的普通 Python 数据类。


FastAPI 推荐使用 uvicorn 启动服务



Uvicorn 是基于 uvloop 和 httptools 构建的非常快速的 ASGI 服务器。


uvicorn 是一个基于 asyncio 开发的一个轻量级高效的 web 服务器框架


uvicorn 设计的初衷是想要实现两个目标:


使用 uvloop 和 httptools 实现一个极速的 asyncio 服务器


实现一个基于 ASGI(异步服务器网关接口)的最小应用程序接口。


它目前支持 http, websockets, Pub/Sub 广播,并且可以扩展到其他协议和消息类型。


uvloop 用于替换标准库 asyncio 中的事件循环,使用 Cython 实现,它非常快,可以使 asyncio 的速度提高 2-4 倍。asyncio 不用我介绍吧,写异步代码离不开它。


httptools 是 nodejs HTTP 解析器的 Python 实现。


综述


综上所述,我们推荐使用 FastAPI 来进行快速开发和程序构建,尤其是功能较为简单的应用,它也支持数据结构校验和异步。提高代码执行效率和开发效率。同时具备丰富的数据支持,基于 OpenAPI 的自助交互式文档,让开发过程中的调试也十分方便。快速敏捷开发是我们的追求。



  • 快速开发:提供基础框架和默认设置,使开发人员可以快速构建 API,并自动生成文档和测试样例。

  • 高性能:基于 Python 3.6+、ASGI 和 asyncio 等技术,提供高性能特性,因此在高流量和低延迟的应用场景下比其他框架有更好的响应速度和性能。

  • 文档自动生成:内置文档生成器,自动解析函数参数和返回值来生成规范化的文档,并支持 OpenAPI 和 JSON Schema 的标准格式。

  • 强类型支持:采用类型注解,提高代码的质量和可读性,减少出现运行时错误的可能性。

  • 多种数据库支持:支持多种数据库,包括 SQLAlchemy、Tortoise ORM 等,可以方便地处理数据库操作。


FastAPI 是一款快速、高性能、安全、易用、规范化的 Web 应用框架,能够极大地提高开发效率,保证应用的高性能和可靠性。


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

Socket学习网络基础

1.OSI七层网络模型浅析 当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了! OSI七层网络模型(从下往上) : 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传...
继续阅读 »

1.OSI七层网络模型浅析


当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了!


OSI七层网络模型(从下往上)




  • 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等!在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,这一层的单位是:bit比特

  • 数据链路层(Datalink) :可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等!另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。每次通信都要经过建立通信联络和拆除通信联络两过程!这种建立起来的数据收发关系~该层的设备有:网卡,网桥,网路交换机,另外该层的单位为:

  • 网络层(Network) :主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了把任意两台终端设备数据链接起来的问题!简单点说就是:建立网络连接和为上层提供服务!该层的设备有:路由!该层的单位为:数据包,另外IP协议就在这一层!

  • 传输层(Transport) :向上面的应用层提供通信服务,面向通信部分的最高层,同时也是用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络层,并且保证这些数据段有效的到达对端!所以这层的单位是:数据段;而这层有两个很重要的协议就是:TCP传输控制协议UDP用户数据报协议,这也是本章节核心讲解的部分!

  • 会话层(Session) :负责在网络中的两节点之间建立、维持和终止通信。建立通信链接,保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!

  • 表示层(Presentation) :对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、数据格式转换和加密解密,压缩解压缩"等

  • 应用层(Application) :OSI参考模型的最高层,为用户的应用程序提供网络服务。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。



好的上面我们浅述了OSI七层网络模型,下面总结下:



OSI是一个理想的模型,一般的网络系统只涉及其中的几层,在七层模型中,每一层都提供一个特殊的网络功能,从网络功能角度观察:



  • 下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主

  • 第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;

  • 上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。


简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。




TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。TCP/IP协议簇分为四层,IP位于协议簇的第二层(对应OSI的第三层),TCP位于协议簇的第三层(对应OSI的第四层)。TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。这4层分别为:



  • 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。

  • 传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。

  • 网络互连层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。

  • 主机到网络层:对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。


3.TCP/UDP区别讲解


好吧,前两点侃侃而谈,只是给大家普及下OSI七层模型和TCP/IP四层模型的概念,接下来要讲的是和我们Socket开发相关的一些概念名词了!


1)IP地址



2)端口



1. 用于区分不同的应用程序


2. 端口号的范围为0-65535,其中0-1023未系统的保留端口,我们的程序尽可能别使用这些端口!


3. IP地址和端口号组成了我们的Socket,Socket是网络运行程序间双向通信链路的终结点,是TCP和UDP的基础!


4. 常用协议使用的端口:HTTP:80,FTP:21,TELNET:23




3)TCP协议与UDP协议的比较:


TCP协议流程详解:


首先TCP/IP是一个协议簇,里面包括很多协议的。UDP只是其中的一个。之所以命名为TCP/IP协议,因为TCP,IP协议是两个很重要的协议,就用他两命名了。


下面我们来讲解TCP协议和UDP协议的区别:


TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,即在收发数据钱,都需要与对面建立可靠的链接,这也是面试经常会问到的TCP的三次握手以及TCP的四次挥手三次握手:建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立,在Socket编程中,这一过程由客户端执行connect来触发,具体流程图如下:




  • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

  • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。


四次挥手:终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在Socket编程中,这一过程由客户端或服务端任一方执行close来触发,具体流程图如下:




  • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态

  • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

  • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

  • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。另外也可能是同时发起主动关闭的情况:



另外还可能有一个常见的问题就是:为什么建立连接是三次握手,而关闭连接却是四次挥手呢?答:因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。


UDP协议详解


UDP(User Datagram Protocol)用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。相比TCP就是无需建立链接,结构简单,无法保证正确性,容易丢包。


4.Java中对于网络提供的几个关键类


针对不同的网络通信层次,Java给我们提供的网络功能有四大类:



  • InetAddress:用于标识网络上的硬件资源

  • URL:统一资源定位符,通过URL可以直接读取或者写入网络上的数据

  • Socket和ServerSocket:使用TCP协议实现网络通信的Socket相关的类

  • Datagram:使用UDP协议,将数据保存在数据报中,通过网络进行通信


本节我们只介绍前两个类,Socket与Datagram到TCP和UDP的章节再讲解!


~InetAddress的使用例子


示例代码

    public static void main(String[] args) throws Exception{
//获取本机InetAddress的实例:
InetAddress address = InetAddress.getLocalHost();
System.out.println("本机名:" + address.getHostName());
System.out.println("IP地址:" + address.getHostAddress());
byte[] bytes = address.getAddress();
System.out.println("字节数组形式的IP地址:" + Arrays.toString(bytes));
System.out.println("直接输出InetAddress对象:" + address);
}

运行结果图



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

从进入内核态看内存管理

知乎上搜到一个比较有意思的话题:如何理解「进入内核态」,要回答好这个问题需要对内存管理及程序的运行机制有比较深刻的了解,比如你需要了解内存的分段,分页,中断等机制,信息量比较大,本文将会 Intel CPU 的发展历史讲起,循序渐近地帮助大家彻底掌握这一概念,...
继续阅读 »

知乎上搜到一个比较有意思的话题:如何理解「进入内核态」,要回答好这个问题需要对内存管理及程序的运行机制有比较深刻的了解,比如你需要了解内存的分段,分页,中断等机制,信息量比较大,本文将会 Intel CPU 的发展历史讲起,循序渐近地帮助大家彻底掌握这一概念,相信大家看了肯定有帮助,本文目录如下



  • CPU 运行机制

  • Intel CPU 历史发展史

    • 分段

    • 保护模式



  • 特权级

    • 系统调用

    • 中断



  • 分段内存的优缺点

  • 内存分页

  • 总结


CPU 运行机制


我们先简单地回顾一下 CPU 的工作机制,重新温习一下一些基本概念,因为我在查阅资料的过程发现一些网友对寻址,CPU 是几位的概念理解得有些模糊,理解了这些概念再去看 CPU 的发展史就不会再困惑


CPU 是如何工作的呢?它是根据一条条的机器指令来执行的,而机器指令= 操作码+操作数,操作数主要有三类:寄存器地址、内存地址或立即数(即常量)。


我们所熟悉的程序就是一堆指令和数据的集合,当打开程序时,装载器把程序中的指令和数据加载到内存中,然后 CPU 到内存中一条条地取指令,然后再译码,执行。


在内存中是以字节为基本单位来读写数据的,我们可以把内存看作是一个个的小格子(一般我们称其为内存单元),而每个小格子是一个字节,那么对于 B8 0123H 这条指令来说,它在内存中占三字节,如下,CPU 该怎么找到这些格子呢,我们需要给这些格子编号,这些编号也就是我们说的内存地址,根据内存地址就是可以定位指令所在位置,从而取出里面的数据



如图示:内存被分成了一个个的格子,每个格子一个字节,20000~20002 分别为对应格子的编号(即内存地址)


CPU 执行指令主要分为以下几个步骤




  1. 取指令,CPU 怎么知道要去取哪条指令呢,它里面有一个 IP 寄存器指向了对应要取的指令的内存地址, 然后这个内存地址会通过地址总线找到对应的格子,我们把这个过程称为寻址,不难发现寻址能力决定于地址总线的位宽,假设地址总线位数为 20 位,那么内存的可寻址空间为 2^20 * 1Byte = 1M,将格子(内存单元)里面的数据(指令)取出来后,再通过数据总线发往 CPU 中的指令缓存区(指令寄存器),那么一次能传多少数据呢,取决于数据总线的位宽,如果数据总线为 16 位,那么一次可以传 16 bit 也就是两个字节。




  2. 译码:指令缓冲区中的指令经过译码以确定该进行什么操作




  3. 执行:译码后会由控制单元向运算器发送控制指令进行操作(比如执行加减乘除等),执行是由运算器操纵数据也就是操作数进行计算,而操作数保存在存储单元(即片内的缓存和寄存器组)中,由于操作数有可能是内存地址,所以执行中可能需要到内存中获取数据(这个过程称为访存),执行后的结果保存在寄存器或写回内存中



    以指令 mov ax, 0123H 为例,它表示将数据 0123H 存到寄存器 AX 中,在此例中 AX 为 16 位寄存器,一次可以操作 16 位也就是 2 Byte 的数据,所以我们将其称为 16 位 CPU,CPU 是多少位取决于它一次执行指令的数据带宽,而数据带宽又取决于通用寄存器的位宽




  4. 更新 IP:执行完一条指令后,更新 IP 中的值,将其指向下一条指令的起始地址,然后重复步骤 1




由以上总结可知寻址能力与寄存器位数有关


接下来我们以执行四条指令为例再来仔细看下 CPU 是如何执行指令的,动图如下:



看到上面这个动图,细心地你可能会发现两个问题



  1. 前文说指令地址是根据 IP 来获取的吗,但上图显示指令地址却是由「CS 左移四位 + IP」计算而来的,与我们所阐述的指令保存在 IP 寄存器中似乎有些出入,这是怎么回事呢?

  2. 动图显示的地址是真实物理地址,这样进程之间可以互相访问/改写对方的物理地址,显然是不安全的,那如何才能做到安全访问或者说进程间内存的隔离呢


以上两点其实只要我们了解一下 CPU 的发展历史就明白解决方案了,有了以上的铺垫,在明白了寻址16/32/64 位 CPU 等术语的含义后,再去了解 CPU 的发展故事会更容易得多,话不多说,发车


Intel CPU 历史发展史


1971 年世界上第一块 4 位 CPU-4004 微处理器横空出世,1974 年 Intel 研发成功了 8 位 CPU-8080,这两款 CPU 都是使用的绝对物理地址来寻址的,指令地址只存在于 IP 寄存器中(即只使用 IP 寄存器即可确定内存地址)。由于是使用绝对物理地址寻址,也就意味着进程之间的内存数据可能会互相覆盖,很不安全,所以这两者只支持单进程


分段


1978 年英特尔又研究成功了第一款 16 位 CPU - 8086,这款 CPU 可以说是 x86 系列的鼻祖了,设计了 16 位的寄存器和 20 位的地址总线,所以内存地址可以达到 2^20 Byte 即 1M,极大地扩展了地址空间,但是问题来了,由于寄存器只有 16 位,那么 16 位的 IP 寄存器如何能寻址 20 位的地址呢,首先 Intel 工程师设计了一种分段的方法:1M 内存可以分为 16 个大小为 64 K 的段,那么内存地址就可以由「段的起始地址(也叫段基址) + 段内偏移(IP 寄存器中的值)」组成,对于进程说只需要关心 4 个段 ,代码段 数据段 堆栈段 附加段,这几个段的段基址分别保存在 CS,DS,SS,ES 这四个寄存器中



这四个寄存器也是 16 位,那怎么访问 20 位的内存地址呢,实现也很简单,将每个寄存器的值左移四位,然后再加上段内偏移即为寻址地址,CPU 都是取代码段 中的指令来执行的,我们以代码段内的寻址为例来计算内存地址,指令的地址 = CS << 4 + IP ,这种方式做到了 20 位的寻址,只要改变 CS,IP 的值,即可实现在 0 到最大地址 0xFFFFF 全部 20 位地址的寻址


举个例子:假设 CS 存的数据为 0x2000,IP 为 0x0003,那么对应的指令地址为



图示为真实的物理地址计算方式,从中可知, CS 其实保存的是真实物理地址的高 16 位


分段的初衷是为了解决寻址问题,但本质上CS:IP 计算得到的还是真实物理地址,所以它也无法支持多进程,因为使用绝对物理地址寻址意味着进程可以随意修改 CS:IP,将其指向任意地址,很可能会覆盖正在运行的其他进程的内存,造成灾难性后果。


我们把这种使用真实物理地址且未加任何限制的寻址方式称为实模式(real mode,即实际地址模式)


保护模式


实模式上的物理地址由 段寄存器中的段基址:IP 计算而来,而段基址可由用户随意指定,显然非常不安全,于是 Intel 在之后推出了 80286 中启用了保护模式,这个保护是怎么做的呢


首先段寄存器保存的不再是段基址了,而是段选择子(Selector),其结构如下



其中第 3 到 15 位保存的是描述符索引,此索引会根据 TI 的值是 0 还是 1 来选择是到 GDT(全局描述符表,一般也称为段表)还是 LDT 来找段描述符,段描述符保存的是段基址和段长度,找到段基址后再加上保存在 IP 寄存器中的段偏移量即为物理地址,段描述符的长度统一为 8 个字节,而 GDT/LDT 表的基地址保存在 gdtr/ldtr 寄存器中,以 GDT (此时 TI 值为 0)为例来看看此时 CPU 是如何寻址的



可以看到程序中的地址是由段选择子:段内偏移量组成的,也叫逻辑地址,在只有分段内存管理的情况下它也被称为虚拟内存


GDT 及段描述符的分配都是由操作系统管理的,进程也无法更新 CS 等寄存器中值,这样就避免了直接操作其他进程以及自身的物理地址,达到了保护内存的效果,从而为多进程运行提供了可能,我们把这种寻址方式称为保护模式


那么保护模式是如何实现的呢,细心的你可能发现了上图中在段选择子和段描述符中里出现了 RPLDPL 这两个新名词,这两个表示啥意思呢?这就涉及到一个概念:特权级


特权级


我们知道 CPU 是根据机器指令来执行的,但这些指令有些是非常危险的,比如清内存置时钟分配系统资源等,这些指令显然不能让普通的进程随意执行,应该始终控制在操作系统中执行,所以要把操作系统和普通的用户进程区分开来


我们把一个进程的虚拟地址划分为两个空间,用户空间内核空间,用户空间即普通进程所处空间,内核空间即操作系统所处空间



当 CPU 运行于用户空间(执行用户空间的指令)时,它处于用户态,只能执行普通的 CPU 指令 ,当 CPU 运行于内核空间(执行内核空间的指令)时,它处于内核态,可以执行清内存,置时钟,读写文件等特权指令,那怎么区分 CPU 是在用户态还是内核态呢,CPU 定义了四个特权等级,如下,从 0 到 3,特权等级依次递减,当特权级为 0 时,CPU 处于内核态,可以执行任何指令,当特权级为 3 时,CPU 处于用户态,在 Linux 中只用了 Ring 0,Ring 3 两个特权等级



那么问题来了,怎么知道 CPU 处于哪一个特权等级呢,还记得上文中我们提到的段选择子吗



其中的 RPL 表示请求特权((Requested privilege level))我们把当前保存于 CS 段寄存器的段选择子中的 RPL 称为 CPL(current priviledge level),即当前特权等级,可以看到 RPL 有两位,刚好对应着 0,1,2,3 四个特权级,而上文提到的 DPL 表示段描述符中的特权等级(Descriptor privilege level)知道了这两个概念也就知道保护模式的实现原理了,CPU 会在两个关键点上对内存进行保护




  1. 目标段选择子被加载时




  2. 当通过线性地址(在只有段式内存情况下,线性地址为物理地址)访问一个内存页时。由此可见,保护也反映在内存地址转换的过程之中,既包括分段又包括分页(后文分提到分页)




CPU 是怎么保护内存的呢,它会对 CPL,RPL,DPL 进行如下检查



只有 CPL <= DPL 且 RPL <= DPL(申请特权等级待以及当前特权等级必须比调用的目标代码段的特权级更高,以防普通程序直接调用目标代码段)时,才会加载目标代码段执行,否则会报一般保护异常 (General-protection exception)


那么特权等级(也就是 CPL)是怎么变化的呢,我们之前说了 CPU 运行于用户空间时,处于用户态,特权等级为 3,运行于内核空间时,处于内核态,特权等级为 0,所以也可以换个问法 CPU 是如何从用户空间切换到内核空间或者从内核空间切换到用户空间的,这就涉及到一个概念:系统调用


系统调用


我们知道用户进程虽然不能执行特权指令,但有时候也需要执行一些读写文件,发送网络包等操作,而这些操作又只能让操作系统来执行,那该怎么办呢,可以让操作系统提供接口,让用户进程来调用即可,我们把这种方式叫做系统调用,系统调用可以直接由应用程序调用,或者通过调用一些公用函数库或 shell(这些函数库或 shell 都封装了系统调用接口)等也可以达到间接调用系统调用的目的。通过系统调用,应用程序实现了陷入(trap)内核态的目的,这样就从用户态切换到了内核态中,如下


应用程序通过系统调用陷入内核态


那么系统调用又是怎么实现的呢,主要是靠中断实现的,接下来我们就来了解一下什么是中断


中断


陷入内核态的系统调用主要是通过一种 trap gate(陷阱门)来实现的,它其实是软件中断的一种,由 CPU 主动触发给自己一个中断向量号,然后 CPU 根据此中断向量号就可以去中断向量表找到对应的门描述符,门描述符与 GDT 中的段描述符相似,也是 8 个字节,门描述符中包含段选择子,段内偏移,DPL 等字段 ,然后再根据段选择子去 GDT(或者 LDT,下图以 GDT 为例) 中查找对应的段描述符,再找到段基地址,然后根据中断描述符表的段内偏移即可找到中断处理例程的入口点,整个中断处理流程如下



画外音:上图中门描述符和段描述符只画出了关键的几个字段,省略了其它次要字段


当然了,不是随便发一个中断向量都能被执行,只有满足一定条件的中断才允许被普通的应用程序调用,从发出软件中断再到执行中断对应的代码段会做如下的检查



一般应用程序发出软件中断对应的向量号是大家熟悉的 int 0x80(int 代表 interrupt),它的门描述符中的 DPL 为 3,所以能被所有的用户程序调用,而它对应的目标代码段描述符中的 DPL 为 0,所以当通过中断门检查后(即 CPL <= 门描述符中的 DPL 成立),CPU 就会将 CS 寄存器中的 RPL(3) 替换为目标代码段描述符的 DPL(0),替换后的 CPL 也就变成了 0,通过这种方式完成了从用户态到内核态的替换,当中断代码执行后执行 iret 指令又会切换回用户态


另外当执行中断程序时,还需要首先把当前用户进程中对应的堆栈,返回地址等信息,以便切回到用户态时能恢复现场


可以看到 int 80h 这种软件中断的执行又是检查特权级,又是从用户态切换到内核态,又是保存寄存器的值,可谓是非常的耗时,光看一下以下图示就知道像 int 0x80 这样的软件中断开销是有多大了


系统调用


所以后来又开发出了 SYSENTER/SYSCALL 这样快速系统调用的指令,它们取消了权限检查,也不需要在中断描述表(Interrupt Descriptor Table、IDT)中查找系统调用对应的执行过程,也不需要保存堆栈和返回地址等信息,而是直接进入CPL 0,并将新值加载到与代码和堆栈有关的寄存器当中(cs,eip,ss 和 esp),所以极大地提升了性能


分段内存的优缺点


使用了保护模式后,程序员就可以在代码中使用了段选择子:段偏移量的方式来寻址,这不仅让多进程运行成为了可能,而且也解放了程序员的生产力,我们完全可以认为程序拥有所有的内存空间(虚拟空间),因为段选择子是由操作系统分配的,只要操作系统保证不同进程的段的虚拟空间映射到不同的物理空间上,不要重叠即可,也就是说虽然各个程序的虚拟空间是一样的,但由于它们映射的物理地址是不同且不重叠的,所以是能正常工作的,但是为了方便映射,一般要求在物理空间中分配的段是连续的(这样只要维护映射关系的起始地址和对应的空间大小即可)


段式内存管理-虚拟空间与实际物理内存的映射


但段式内存管理缺点也很明显:内存碎片可能很大,举个例子



如上图示,连续加载了三个程序到内存中,如果把 Chrome 关闭了,此时内存中有两段 128 M的空闲内存,但如果此时要加载一个 192 M 的程序 X 却有心无力了 ,因为段式内存需要划分出一块连续的内存空间,此时你可以选择把占 256 M 的 Python 程序先 swap 到磁盘中,然后紧跟着 512 M 内存的后面划分出 256 M 内存,再给 Python 程序 swap 到这块物理内存中,这样就腾出了连续的 256 M 内存,从而可以加载程序 X 了,但这种频繁地将几十上百兆内存与硬盘进行 swap 显然会对性能造成严重的影响,毕竟谁都知道内存和硬盘的读写速度可是一个天上一个地上,如果一定要交换,能否每次 swap 得能少一点,比如只有几 K,这样就能满足我们的需求,分页内存管理就诞生了


内存分页


1985 年 intel 推出了 32 位处理器 80386,也是首款支持分页内存的 CPU


和分段这样连续分配一整段的空间给程序相比,分页是把整个物理空间切成一段段固定尺寸的大小,当然为了映射,虚拟地址也需要切成一段段固定尺寸的大小,这种固定尺寸的大小我们一般称其为页,在 LInux 中一般每页的大小为 4KB,这样虚拟地址和物理地址就通过页来映射起来了



当然了这种映射关系是需要一个映射表来记录的,这样才能把虚拟地址映射到物理内存中,给定一个虚拟地址,它最终肯定在某个物理页内,所以虚拟地址一般由「页号+页内偏移」组成,而映射表项需要包含物理内存的页号,这样只要将页号对应起来,再加上页内偏移,即可获取最终的物理内存



于是问题来了,映射表(也称页表)该怎么设计呢,我们以 32 位虚拟地址位置来看看,假设页大小为 4K(2^12),那么至少需要 2^20 也就是 100 多万个页表项才能完全覆盖所有的虚拟地址,假设每一个页表项 4 个字节,那就意味着为一个进程的虚拟地址就需要准备 2^20 * 4 B = 4 M 的页表大小,如果有 100 个进程,就意味着光是页表就要占用 400M 的空间了,这显然是非常巨大的开销,那该怎么解决这个页表空间占用巨大的问题呢


我们注意到现在的做法是一次性为进程分配了占用其所有虚拟空间的页表项,但实际上一个进程根本用不到这么巨大的虚拟空间,所以这种分配方式无疑导致很多分配的页表白白浪费了,那该怎么办,答案是分级管理,等真正需要分配物理空间的时候再分配,其实大家可以想想我们熟悉的 windows 是怎么分配的,是不是一开始只分配了 C 盘,D盘,E盘,等要存储的时候,先确定是哪个盘,再在这个盘下分配目录,然后再把文件存到这个目录下,并不会一开始就把所有盘的空间给分配完的



同样的道理,以 32 位虚拟地址为例,我们也可以对页表进行分级管理, 页表项 2^20 = 2^10 * 2^10 = 1024 * 1024,我们把一个页表分成两级页表,第一级页表 1024 项,每一项都指向一个包含有 1024 个页表项的二级页表


图片来自《图解系统》


这样只有在一级页表中的页表项被分配的时候才会分配二级页表,极大的节省了空间,我们简单算下,假设 4G 的虚拟空间进程只用了 20%(已经很大了,大部分用不到这么多),那么由于一级页表空间为 1024 *4 = 4K,总的页表空间为 4K+ 0.2 * 4M = 0.804M,相比于原来的 4M 是个巨大的提升!


那么对于分页保护模式又是如何起作用的呢,同样以 32 位为例,它的二级页表项(也称 page table entry)其实是以下结构



注意第三位(也就是 2 对应的位置)有个 U/S,它其实就是代表特权级,表示的是用户/超级用户标志。为 1 时,允许所有特权级别的程序访问;为 0 时,仅允许特权级为0、1、2(Linux 中没有 1,2)的程序(也就是内核)访问。页目录中的这个位对其所映射的所有页面起作用


既然分页这么好,那么分段是不是可以去掉了呢,理论上确实可以,但 Intel 的 CPU 严格执行了 backward compatibility(回溯兼容),也就是说最新的 CPU 永远可以运行针对早期 CPU 开发的程序,否则早期的程序就得针对新 CPU 架构重新开发了(早期程序针对的是 CPU 的段式管理进行开发),这无论对用户还是开发者都是不能接受的(别忘了安腾死亡的一大原因就是由于不兼容之前版本的指令),兼容性虽然意味着每款新的 CPU 都得兼容老的指令,所背的历史包袱越来越重,但对程序来说能运行肯定比重新开发好,所以既然早期的 CPU 支持段,那么自从 80386 开始的所有 CPU 也都得支持段,而分页反而是可选的,也就意味着这些 CPU 的内存管理都是段页式管理,逻辑地址要先经过段式管理单元转成线性地址(也称虚拟地址),然后再经过页式管理单元转成物理内存,如下


分页是可选项


在 Linux 中,虽然也是段页式内存管理,但它统一把 CS,DS,SS,ES 的段基址设置为了 0,段界限也设置为了整个虚拟内存的长度,所有段都分布在同一个地址空间,这种内存模式也叫平坦内存模型(flat memory model)


平坦内存模型


我们知道逻辑地址由段选择子:段内偏移地址组成,既然段选择子指向的段基地址为 0,那也就意味着段内偏移地址即为即为线性地址(也就是虚拟地址),由此可知 Linux 中所有程序的代码都使用了虚拟地址,通过这种方式巧妙地绕开了分段管理,分段只起到了访问控制和权限的作用(别忘了各种权限检查依赖 DPL,RPL 等特权字段,特权极转移也依赖于段选择子中的 DPL 来切换的)


总结


看完本文相信大家对实模式,保护模式,特权级转换,分段,分页等概念应该有了比较清晰的认识。


我们简单总结一下,CPU 诞生之间,使用的绝对物理内存来寻址(也就是实模式),随后随着 8086 的诞生,由于工艺的原因,虽然地址总线是 20 位,但寄存器却只有 16 位,一个难题出现了,16 位的寄存器该怎么寻址 20 位的内存地址呢,于是段的概念被提出了,段的出现虽然解决了寻址问题,但本质上 CS << 4 + IP 的寻址方式依然还是绝对物理地址,这样的话由于地址会互相覆盖,显然无法做到多进程运行,于是保护模式被提出了,保护就是为了物理内存免受非法访问,于是用户空间,内核空间,特权级也被提出来了,段寄存器里保存的不再是段基址,而是段选择子,由操作系统分配,用户也无法随意修改段选择子,必须通过中断的形式才能从用户态陷入内核态,中断执行的过程也需要经历特权级的检查,检查通过之后特权级从 3 切换到了 0,于是就可以放心合法的执行特权指令了。可以看到,通过操作系统分配段选择子+中断的方式内存得到了有效保护,但是分段可能造成内存碎片过大以倒频繁 swap 会影响性能的问题,于是分页出现了,保护模式+分页终于可以让多进程,高效调度成为了可能


参考



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

计算机网络 传输层

计算机网络 传输层 传输层服务 传输层在网络层的基础上,实现了进程到进程的服务。 传输层通过引入端口号来区分进程,在 UDP/TCP 的首部中,一个端口占用 2B, 即 16 bit。 复用与分用 一台主机上的多个进程,利用一个传输层实体,将数据发送出去,这...
继续阅读 »

计算机网络 传输层


传输层服务


传输层在网络层的基础上,实现了进程到进程的服务。


传输层通过引入端口号来区分进程,在 UDP/TCP 的首部中,一个端口占用 2B, 即 16 bit。



复用与分用


一台主机上的多个进程,利用一个传输层实体,将数据发送出去,这就叫复用。而远端主机的传输层实体,收到了很多数据,然后通过识别端口号,将数据交给具体的进程,这就叫分用(或解复用)。


UDP(无连接传输协议 / 用户数据报协议)


面向报文协议,即一个 UDP 数据报就是应用层交下来一个完整报文。


是一种尽力而为的,非有序的、无连接的一种协议,通常用于流媒体应用、事务性应用,如 DNS 等。


UDP 的报文结构



UDP 会使用校验和对数据做差错控制编码。


将数据按每16位一组相加,最高位相加产生的进位,回卷到最低位去,如此,将最后的和取反码就是最终的校验和,在接收端,按同样的方法求校验和,与接收端的校验和相加,如果结果不是 16 个 1,则肯定出错,如果是 16 个 1 ,说明大概率没有出错


可靠数据传输的基本原理


可靠的传输通常需要实现数据的不丢失、不乱序、无差错等核心功能。


我所看过的传输层相关的书,都是通过递进式的讲解实现可靠的网络传输,这里也已这种方式来记录。但我这里 rdt1.0、rdt2.0、rdt3.0跟书本上的可能不一致,纯粹是按自己理解的来,我觉得这里能够理解其逐渐完善可靠性的过程就OK了。


rdt1.0 停等协议


先假设传输层以下的实现都是可靠的,不会出现数据差错的情况,**那如何实现不乱序呢?**很简单,我们只要保证按顺序传就可以,发送端先发一个,等接收端收到了再发下一个,如此往复直到数据全部发送完。这样的协议叫做停等协议。



rdt2.1 ACK 和 NAK


这里看上去确实实现了按序到达,但这是建立在底层不会出现差错的情况下,一旦出现了一些异常情况,比如**分组在传输过程中出现了比特反转(0被识别为1,1识别为0)**该如何处理?


那需要接收端给发送端一个答复,接收端是否收到了合法的分组。如果收到了正确的分组,接收端回复一个确定(ACK),发送端识别到时ACK,就可以继续发送下一个分组,若收到了错误的分组,则回复一个否定(NAK),那接收端就需要重发当前分组。



如图这里主要有两种异常情况:




  1. 分组在传输过程中出错:


    接收端检测到错误之后,回复 NAK , 发送端收到 NAK 后,需要重传上一个分组。




  2. ACK 或 NAK 在传输过程中出错:


    发送端不能识别这到底是 ACK 还是 NAK,于是就统一按照这是 NAK 来处理,于是就重发上一个分组。而这里重发后,会导致接收端收到重复的分组,接收端需要丢弃这个分组,然后回复 ACK。




当然,如果过程中没有出错,那就一步一步的正常的将数据发送给接收端就OK了。


rdt2.2 序号


在rdt2.2中,我们开始使用序号,给分组进行编号。并且使用上一个分组的编号来替代 NAK ,什么意思呢?如果用户在收到一个分组 P3 时,发现数据出错了,那这时它就不再回复 NAK 了,而是回复 ACK = P2,表示接收端才收到 P2,于是发送端需要重传 P3,这样实现了跟 NAK 一样的效果。这么做为后面的流水协议奠定了基础。



可以看到,这里用分组的序号替代单纯的 ACK/NAK,异常情况跟 rdt 2.1 非常相似。


rdt3.0 超时重传机制


如果分组在传输过程中丢失了,接收端等待下一个分组,发送端等待 ACK,双方如果都这么默默的等待下去,那不就出现死锁了?


我们可以让发送端发送一个分组后,设置一个超时定时器,如果在一段时间后还没有收到 ACK, 触发超时重传机制,重发上一个分组,这里不论是分组丢失还是 ACK 丢失,都可以重发。如果是 ACK 丢失,接收端会收到重复的分组,跟 rdt2.0 的方案类似,将重复的分组丢弃就可以了,并且对当前的分组发送 ACK 。



由于定时器的设置,不可能百分百的确定分组或 ACK 丢失,可能在超时重传后好一段时间,对前面某个分组的 ACK 慢悠悠的过来了,这是发送端可以什么都不做,无视它就可以了,因为前面已经收到过对这个分组的确认。


超时计时器的时间需要根据RTT的时间来动态设置,如果超时时间设置得过短,会引起不必要的重传,如果设置得过长,会使重传效率变低。


到目前为止,停等协议已经实现了可靠传输,但是它的信道利用率很低


滑动窗口协议(slide window)


停等协议的信道利用率很低,是因为它一次只发一个分组,确认发送完成后才能发另一个,这中间会有很长的等待时间,如何提高信道利用率呢?显然,我们在发完一个分组后,不等 ACK 就在此下一个,看上去就能提高效率。因此,这种连续发送多个未经确认的分组的协议叫做流水线协议


在发送端会有一段缓冲区,可以存储已经发送出去但还没有收到 ack 的分组,用于后面检错重发、超时重发等。而在接收端,也有一段缓存区,主要是为了适应发送端发送速率和接收端接收速率不一致的情况,当发送端速度过快时,可以用缓冲区暂存一下。


发送端缓冲区的发送窗口长度叫做 sw , 接收端缓冲区接收窗口长度叫做 rw 。窗口是缓冲区的一个子集。



  • 当 sw = 1,rw = 1 时,这就是停等协议

  • 当 sw > 1,rw = 1 时,这就是 GBN 协议

  • 当 sw > 1,rw > 1时,这就是 SR (selective repeat)协议



通常发送窗口的后沿指针指向低位的首个已发送但没有确认的分组,前沿指针指向高位第一个已发送的分组,如果发送的分组数量比最大发送窗口长度小,那表明发送端可以继续发送分组,也就是前沿指针可以向前移动。如果低位的未确认的分组收到了确认,则后沿指针可以向前移动,移动到首个未确认的分组。这时,发送窗口被使用的长度就变小了,发送端就可以继续发送分组。


而接收端收到的分组在接收窗口范围内时,可以接收并对其作出确认,若没有比它序号更低的分组等待被确认,则接收窗口的后沿指针可以向前移动,移动到首个未确认的分组。由于窗口指针的移动,接收端又有了更多的空间来接收后续的分组。如果接收的分组超出接收窗口范围,则将其丢弃,因为接收窗口暂时没有足够空间去保存更多的分组。


GBN

GBN 协议特点:rw = 1, 顺序接收,累积确认。


双方正常的流程:发送方可以按序发送多个分组,这些分组有序的到达接收端,接收端对其一一作出确认,接收端每接收一个分组,窗口就向前移动一位,然后接收到下一个分组,再对下一个分组进行确认,然后再往前移动。


双方的异常流程:



  1. 如果接收端收到乱序的分组,比如在等待 P1 的过程中,结果收到一个 P2,那接收端会将其丢弃,并发送其上一个分组,也就是 P0 的确认。发送端收到 P0 的 ACK 后,就会将 P0 之后的所有的分组重发一遍,这也就是 go back N 的意义。总结起来就是,如果低位的分组出错(失序或超时)了,需要从低位开始重传后续所有的分组



SR

SR 协议特点:rw > 1, 可以乱序接收非累积确认,或者叫单独确认,收到哪个分组,就给哪个确认。窗口向前滑动时,滑到从低位开始第一个未确认的分组


双方的正常流程:发送方依次发送多个分组,每个分组要设置一个独立的超时计时器,由于 SR 的接收窗口长度大于 1,因此只要是在窗口范围内的分组,它都可以接收,并单独对其作出确认。发送方收到确认后,取消超时计时器,如果没有比当前更小的需要被确认的分组,那后沿指针就向前移动,发送端继续发送后续的分组。


双方的异常情况:




  1. 发送方的某个分组丢失了,那接收方就无法对其作出确认,接收窗口的后沿指针无法向前移动,一段时间后,发送端的这个分组的超时计时器会触发重传,然后重新设置超时计时器。接收方收到这个分组后,对其作出确认,然后接收窗口的前沿指针得以向前移动,从而可以接收更多的分组。而发送方收到确认后,发送窗口的后沿指针也会向前移动,发送端就可以发送后续的分组。




  2. 接收端的某个分组的ACK 丢失了,这里发送端对应的分组超时计时器会进行重发,重发后接收端收到了重复的分组,而接收端发现这个分组已经被确认过,直接将其忽略即可。如果接收端的某个分组的 ACK 是因为网络拥塞导致超时之后才到达发送端,那发送端发现自己已经重发了这个分组,正在等待确认中,那发送端直接忽略这个ACK就可以了,同理,如果某个分组已经通过超时重传后被确认了,而首次发送的 ACK 才缓缓到达,发送端同样可以忽略这次的确认,因为这个分组已经被确认过了。




GBN & SR 的差别

GBN 使用起来简单,适合在低差错率的网络中使用,使用累积确认(比如,对某个分组进行确认,则表明这个分组及以前所有的分组,都被正确接收了,而之后的分组没有被收到),一旦出错,需要重发之后所有已经发送过的分组,会有比较大的代价。


SR 实现起来复杂些,因为每个分组都有单独的超时计时器,只需要对出现差错的分组单独重传,适合在差错率较高的网络中使用。使用非累积确认。


TCP(面向连接传输协议)


在可靠传输原理中,我们为了方便,是给分组设置了编号,但实际在 TCP 中是对字节流进行编号。


由于我们到了具体的 TCP 协议,通常需要将分组描述为报文段


TCP 采用了前面可靠传输原理中的流水线协议、累积确认、单超时计时器等特点,是 SR 和 GBN 的混合,同时还支持流量控制,拥塞控制。


段结构



TCP 在载荷部分前,会加上自己的控制信息,也就是 TCP 的首部。如果不包含选项部分,首部的长度为 20个字节。


介绍几个主要的字段。


序号:由发送端设置,表示发送端发送报文段的首字节编号。


确认号:由接收端设置,当 ACK = 1时,确认号才有效,表示接收端期望收到字节编号。同时,在累积确认中表示这个编号之前的分组已经收到。


SYN:表示要建立连接。


FIN:表示要拆除连接。


可靠数据传输


TCP 的可靠传输原理大致可以理解为是 SR + 累积确认 + 快速重传。TCP 的发送端不会为每个发出去的报文段设置超时计时器,而是在发送窗口滑动到首个未确认的报文段时启动一个超时计时器。


TCP 对于乱序到达的分组,没有规定该存储还是丢弃,可以自由实现。


快速重传


低位的分组,在超时计时器触发超时之前,如果收到了连续 3 次重复的对当前报文段的确认,可以认为当前报文段已经出现了差错,发送端就可以提前重发这个报文段,而不用等到超时计时器生效。


TCP 的实际实现中,在收到某个分组后,会短暂的等待一会,等收到下一个分组后,再发送确认,这样就可以只发送 1 次确认,这也是利用了累积确认的优点。当然,它不能等待太久,并且等待的分组只能有 1 个,如果下一个分组到来,立刻发送确认。


流量控制


流量控制时为了解决发送端的发送速率和接收端的读取数据不一致的问题,通常是发送端发送的太快,超出了接收端的缓冲区,超出的这些报文段就会被丢弃,那发送端发这么多也就没有了意义。


接收方在发送确认时,可以将自己可用的缓冲区大小放在 TCP 首部的接收窗口字段中,发送端收到这个 ACK 后,就知道是否还能够继续发送数据,可以根据接收端的窗口,调节自己的窗口大小。


连接管理


TCP 是面向连接的,在正式传送数据前,需要做一些准备工作,如:协商起始序号,设置发送窗口、接收窗口等。



连接建立被称为“三次握手”,TCP 的连接建立过程如下:


连接建立前,客户端处于连接关闭状态,服务端处于监听状态。



  1. 客户端发送连接建立请求,SYN=1,同时设置一个随机的初始字节序号x,即 seq=x。

  2. 服务端收到了连接建立请求,设置 ACK=1,表示同意建立请求,同时设置请求号 ack=x+1,TCP 规定 SYN 报文段不能携带数据,但需要消耗 1 个序号,因此这里的 ack=x+1。这部分操作表示服务端同意建立请求。同时,第二次握手还有服务端作为发送方希望与客户端建立连接的请求,所以,这里有跟第一次握手相似的数据,SYN=1, seq=y,y 是服务端随机生成的序号。

  3. 客户端收到了服务端发来的 ack,那客户端作为发送方与服务端的连接就已经建立,这时客户端就已经可以向服务端发送数据了。同时也收到了服务端发来的连接建立请求,因而这次客户端需要回复 ACK=1,ack=y+1, 表示同意建立请求,然后这次不论是否传送数据 seq都是 x+1。


经过上面三次握手,客户端与服务端的双向的连接就建立起来了。


当双方通信完毕后,TCP 需要拆除连接。连接拆除的过程通常叫做“四次挥手”,过程如下:




  1. 客户端发送 FIN=1,表示希望拆除连接。

  2. 服务端收到后发送 ACK=1。(客户端收到ACK后,客户端到服务端的连接就拆除了,但服务端到客户端的连接仍然可用,服务端仍然可以向客户端发送数据)

  3. 服务端发送 FIN=1,表示希望拆除连接。

  4. 客户端收到后发送 ACK=1。服务端收到后确认后,连接即拆除。但站在客户端的角度,作为最后一个发送消息的角色,它还不知道自己发送的确认有没有准确的到达服务器,所以在发送完 ACK 后,会等待一段时间,如果服务端没有重传FIN,那客户端作为接收方到服务端的接收连接也就断开了,那到目前为止,所有的连接就都拆除了。


上面第四步客户端会有一个等待时间来防止 ack 出现差错,但这个方案也不是完美的,如果服务端发现这个ack 有差错,重发了一个连接拆除的请求,但这个请求慢悠悠过了好一会才到达客户端(时间大于客户端的等待时间),可是客户端已经关闭了连接,那服务端短时间内就没法收到拆除连接的 ack 了。


因此,作为拆除连接的最后一环,第四次挥手无论怎样都不会完美,只能尽可能的降低出现问题的概率。


为什么要设置 SequenceNum?


防止滞留在网络中的上一次连接中的一些段,被识别位本次连接中的段。SequenceNum 的长度是 32 位,可以编址 4G 的字节,这么大的范围在两次连接中很难出现相近的序号段,一旦序号段差别很大,这个“野“报文段也就很难出现接收窗口中,就会被接收方丢弃。


为什么2次握手不行?



  1. TCP 是全双工的连接,建立连接时,既要有客户端到服务端的连接,也要有服务端到客户端的连接。建立一个方向的连接需要 2 次握手,两个方向就是 4 次,但是在服务端向客户端确认连接时,可以捎带上服务端对客户端的连接建立请求,这样就合并了一次握手,所以有三次。

  2. 如果只有 2 次握手,且服务端的确认出现差错的情况下,客户端就收不到建立连接的确认,这个方向的连接就会失败。而服务端不知道自己的连接确认丢失了,导致服务端到客户端的连接建立了,这样就出现了“半连接”的情况。如果服务端有大量的这种“半连接”,会极大的消耗服务端的性能。如果上一个连接滞留在网络中的报文段到达了接收方,还有可能阴差阳错的被服务端接收。



TCP的拥塞控制


拥塞时指过多的分组被注入到网络中,超出了网络的处理能力,导致大量的分组 ”拥挤“ 在网络中间设备队列中等待转发,网络性能显著下降的现象。


拥塞的直接后果:



  1. 数据分组通过网络的延时显著增加;

  2. 由于中间网络设备的队列满导致大量的分组被丢弃。



拥塞控制就是通过合理调度、规范、调整向网络中发送数据的主机数量、发送速率或数据量,以避免拥塞或尽快消除已发生的拥塞。拥塞控制可以在不同层实现,比较典型的是在网络层和传输层进行拥塞控制。


拥塞控制的基本思想是 AIMD。拥塞窗口的调整主要分为慢启动阶段和拥塞避免阶段。在慢启动阶段,每收到 1 个确认段,拥塞窗口增加 1 个 MSS,每经过 1 个 RTT,拥塞窗口增长 1 倍;在拥塞避免阶段,每经过 1 个 RTT,拥塞窗口才增长 1 个 MSS。



如何检测网络中是否发生了拥塞?



  1. 分组发生了超时

  2. 连续收到了多个对同一分组的重复确认。

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

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

又到了毕业季,不知不觉毕业已经三年了 🤔 在这三年期间对生活有了很多感悟,俗话说“字节一年,人间三年”,感觉自己又读了两个大学 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »