注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

工作了5年你居然不知道版本号有这些规范?

前言 所谓语义化版本控制,就是要求你的版本号能按照特定规则及条件来进行约束,以期达到见到版本号即能了解其修改内容的信息或相邻版本间的更新迭代关系。通过阅读本文,你将能够对语义化版本控制规范能够有一个全面的了解,同时也对各平台上依赖版本时的语法有个大体的了解。 ...
继续阅读 »



前言


所谓语义化版本控制,就是要求你的版本号能按照特定规则及条件来进行约束,以期达到见到版本号即能了解其修改内容的信息或相邻版本间的更新迭代关系。通过阅读本文,你将能够对语义化版本控制规范能够有一个全面的了解,同时也对各平台上依赖版本时的语法有个大体的了解。


背景


在正式开始之前,先问大家几个问题:


我们经常在类似 Github、npm、或者 pub.dev 上看到一些软件或者库的版本号包含如下信息,你是否会疑惑他们之间的区别是什么?分别适用什么场景?



  • alpha

  • beta

  • rc

  • release


再看看下面几组版本号,你是否能弄清楚各个版本号之间谁更新更大?



  • 1.0.0 1.0.1 1.1.0

  • 1.0.0-beta 1.0.0

  • 1.0.0-release 1.0.0

  • 1.0.0-alpha 1.0.0-alpha.1

  • 1.0.0-alpha 1.0.0-rc.1


这次将借着我们在做组件管理平台的机会,像大家介绍一下日常软件开发中的语义化版本控制规范。相信通过下面的学习,上述的问题也能够迎刃而解。


常见先行版本号标识


上面说到 alpha、beta、rc、release 等版本号中常见的一些标识,有一个正式的名称叫做:先行版本号标识。我们可以通过一个生活中的例子来通俗易懂的说明它们之间的差异和联系。


现在假设你是一个蛋糕店的老板,你打算给你的蛋糕店推出一个新品,那么上述所谓的先行版本号就是如下几个阶段的蛋糕:


Alpha 版就是你对于你蛋糕的最终形式还在脑海当中,只有一个蛋糕的基本样子,口味应该是什么味道你心里还没谱,对于装饰如奶油、水果还有蜡烛这些甚至都还没有放在一起(你的软件各功能甚至都没有打通)。由于过于简陋,并且口味还没固定,你还不能将其给你的顾客品尝。你只能自己反复摸索尝试,或者让自己店里的员工对口味、外观以及一些缺陷进行点评。


Alpha版蛋糕


Beta 版就是你的蛋糕已经开始尝试将部分奶油涂抹在蛋糕上,你已经尝试将所有的元素组装起来,这时候的蛋糕还处于不能拿出去卖的阶段,但口味和后续方向已经基本固定。你甚至可以邀请你店里的熟客来参加小规模的试吃活动,并让他们针对你的这款蛋糕进行全方面的点评。


Beta版蛋糕


RC 版就是你的蛋糕已经基本做完了,其最核心的口味和外观已经确定下来,你可以再检查一下蛋糕是否有裂缝、哪些地方需要针对性的进行一些美化或修补。


RC版蛋糕


release 版就是你已经把蛋糕装饰好了,插上蜡烛,撒上曲奇,进行裱花。这时候蛋糕已经完成了,你可以正式的将这块蛋糕摆上橱窗,向大家兜售你的艺术品了。


release版蛋糕


通过上述的蛋糕制作过程,你应该对这些先行版本号标识有了自己的认知。接下来我们再总结下这些先行版本号标识的常见含义:


标识常见含义
alpha(α)内部测试版(有些也叫 A 测版)。α 是希腊字母的第一个,表示最早的版本,一般此类版本包含很多 BUG ,功能也不全,主要面向的是开发人员和测试人员。
beta(β)公开测试版(有些也叫 B 测版)。 β 是希腊字母的第二个,因此这个版本比alpha版发布较晚一些,主要是给参与内部体验的用户测试用,该版本仍然存在很多 BUG ,但是相对 alpha 版要稳定一些。此时,基本功能已经固定,但仍然可能增加新功能。
rc(Release Candidate)rc (候选版本),该版本又较 beta 版更进一步了,该版本功能不再增加,和最终发布版功能一样。
这个版本的作用是提前预览即将发行版本的内容,并且在该版本后,最终发行版的发布也迫在眉睫了。
release稳定版(有些也叫做 stable、GA 版)。在开源软件中,都有正式版,这个就是开源软件的最终发行版,用户可以放心大胆的用了。

相信阅读到这里,上面的第一个问题你已经有了答案。那么明白这些标识的具体含义之后,它到底应该怎么用呢?具体要放在版本号里的哪个位置上呢?接下来我们将通过对语义化版本控制规范的详细介绍,来帮助你解答这些疑惑。


何为语义化版本控制规范


在介绍什么是语义化版本控制规范之前,我们先需要了解为什么需要语义化版本控制规范。


大家先设身处地的设想这样一个开发场景:


你现在的项目现在分别依赖了 foo : 1.0.0bar : 2.0.0baz : 3.0.0


项目example_app的依赖项


同时 foo 组件也依赖了 bar : 2.0.0baz : 3.0.0


组件foo的依赖项


同时 bar 组件也依赖了 baz : 3.0.0


组件bar的依赖项


现在你很幸运,项目可以跑起来。


突然有一天因为要修改一个问题,需要升级你项目中 baz 组件的版本号,需要将它从 3.0.0 升级到 3.0.1。但很不幸的是,baz 组件这个小小的版本升级却发生了破坏性的 API 改动。然后你发现你不仅需要修改主工程 example_app 的版本号,还需要升级 foo 组件的版本号以及 bar 组件的版本号。而在你做完这些之后,发现 foo 依赖的其他组件的版本又和你主工程 example_app 项目中依赖的组件的版本冲突了,于是你崩溃了。


这就是软件管理领域中被称作“依赖地狱”的死亡之谷。即当你的系统规模越大,引入的包越多,你就越可能遇到由于依赖导致的问题:



  • 如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)

  • 而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量) 当你项目的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。


通过上述的场景我们可以看到,版本号的管理(包括依赖关系的控制以及版本号的命名)并不是一个随心所欲的事情:管理好了,能给你带来极大便利,反之则会给你的开发带来很多没必要的麻烦。那么我们应该如何解决这些事情呢?


基于上述的一些问题,Gravatars 及 Github 的创始人之一的 Tom Preston-Werner 提出了一个名为语义化版本控制规范(Semantic Versioning)的解决方案,它期望用一组简单的规则及条件来约束版本号的配置和增长。这套规则是根据现在各种封闭、开源软件所广泛使用的版本号命名规则所设计。为了让这套理论运作,必须要定义好你的公共 API。一旦定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:



版本格式:主版本号.次版本号.修订号,版本号递增规则如下:



  1. 主版本号:当你做了不兼容的 API 修改

  2. 次版本号:当你做了向下兼容的功能性新增

  3. 修订号:当你做了向下兼容的问题修正


先行版本号版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。



以上这套规则或者说系统,就是所谓的”语义化版本控制”,在这套规则之下,版本号以及版本号的更新都潜在包含了代码的修改信息,而这套规则被简称为 semver (Semantic Versioning 简写)。


接下来我将基于 semver 2.01 向大家介绍 这套规则的一些细则。


语义化版本控制规范细则


语义化版本控制规范的细则有很多,有兴趣的同学可以直接到semver 2.01 的官方文档 查看即可,我们这里将其主要内容总结给大家。


X.Y.Z(主版本号.次版本号.修订号)修复问题但不影响 API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。


一个标准的语义化版本号各部分的含义


其实所谓语义化版本控制规范,本来也只是一种约定,它并不能完美适合每一个团队,我们也没必要完全生搬硬套,这里以 Google 官方推出的 mockito2 的版本号为例,可以看到其也没有严格按照细则进行遵守。


组件mockito的一些非正式版本号


所以如果你团队内已经统一认知,了解版本号中每个地方代表的含义,做到“见号知意”:看到 1.0.0-npeFixed.8 就知道这个组件是从 1.0.0 拉出来 为了修复 NPE 的;看到 2.3.0-addFaceIdSupport.1 就知道这个组件是基于 2.3.0 来做 FaceId 支持的;见到 5.0.0-nullSafety.6 就知道这个版本是为了空安全的。那么我们的语义化版本控制规范的目的也就达到了,不是吗?


版本语法


就像人类的烹饪方式从最开始的单纯用火烤到发明陶器之后的烹煮,再到现代社会中基于烤、煮、蒸而演化出来的各类五花八门的烹饪方式一样,语义化版本控制规范在各个平台上也衍生出不同的版本规范和版本语法(Version Syntax),但万变不离其宗。接下来我将大致介绍下常见平台版本语法的异同,期望能对你有所帮助。



由于 PyPI上的版本规范及版本说明符比较特殊且繁琐,这里就不进行比对,有兴趣的同学可以查看PEP 440 – Version Identification and Dependency Specification3 了解更多细节。



和烹饪方式的的演化过程一样, 语义化版本控制规范在不同平台、不同时期也有不同的表现


定义平台格式示例描述
完全匹配目标版本gradleversion

version!!
com.example:foo:5.0.2

com.example:foo:5.0.2!!
这里用5.0.2 和 5.0.2!! 是有区别的。
5.0.2 这种写法是对此版本最低要求,但是可以被引擎升级,这里的5.0.2是被称作必须版本(Required version4), 也就是说它最小版本是5.0.2,并且相信未来的任何升级都是可以正常工作的。

而5.0.2!! 则是严格版本(Strict version5), 即只能使用5.0.2的版本,传递依赖项过来的版本如果没有更高约束或者别的严格版本,会被覆写为此版本,否则会失败。
mavenversion

[version]
5.0.2

[5.0.2]
和gradle类似,这里用5.0.2 和 [5.0.2] 是有区别的。
5.0.2 这种写法是对此版本的软要求(Soft requirement6),如果依赖关系树中较早没有出现其他版本,则使用 5.0.2。

而 [5.0.2] 这种写法是对此版本的硬性要求(Hard requirement6)。使用 5.0.2并且仅使用 5.0.2。
pubversionfoo: 5.0.2
npmversionfoo: 5.0.2
podversion

= version
pod 'foo', '5.0.2'

pod 'foo', '=5.0.2'
兼容版本gradleversion.+com.example:foo:1.+>= 1.0.0 < 2.0.0
maven[version, version+1)[1.0.0, 2.0.0)同上
pub^version

~version
foo: ^1.0.0

foo: ~1.0.0
>= 1.0.0 < 2.0.0

>=1.0.0 < 1.1.0

^version 和 ~version 分别被称作 插入符语法(Caret Syntax7) 和 波形语法(Tilde Syntax8),他们的主要区别在于前者兼容当前版本后及后续所有的 次版本号及修订号,即 ^X.Y.Z 等价于 >=X.Y.Z
<(X+1).0.0;

而后着只兼容当前版本号及后续所有的修订号,即
~X.Y.Z 等价于 >=X.Y.Z
npm^versionfoo: ^1.0.0同上
pod~> versionpod 'foo', '~> 1'同上
匹配任意版本gradlecom.example:foo任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
maven[firstVersion,)[0.0.1,)>=0.0.1
pubanyfoo: any任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
npm*foo: *同上
podpod 'foo'同上
已发布的最新版本gradle+com.example:foo:+任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
mavenLATEST

LATESTLATEST 在maven 3.x版本被废弃
pubanyfoo: any任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
npm*foo: *同上
pod> 0.0.1pod 'foo', '>0.0.1'同上
大于当前版本gradle(version, )com.example:foo: (0.0.1, )
maven(version, )(0.0.1, )
pub>versionfoo: >0.0.1
npm> versionfoo: > 0.0.1
pod> versionpod 'foo', '> 0.0.1'
大于等于当前版本gradle[version, )com.example:foo: [0.0.1, )
maven[version, )[0.0.1, )
pub>=versionfoo: >=0.0.1
npm>= versionfoo: >= 0.0.1
pod>= versionpod 'foo', '>= 0.0.1'
小于当前版本gradle(, version)com.example:foo: (, 2.0.0)
maven(, version)(, 2.0.0)
pubfoo: <2.0.0
npm< versionfoo: < 2.0.0
pod< versionpod 'foo', '< 2.0.0'
小于等于当前版本gradle(, version]com.example:foo: (, 2.0.0]
maven(, version](, 2.0.0]
pub<=versionfoo: <=2.0.0
npm<= versionfoo: <= 2.0.0
pod<= versionpod 'foo', '<= 2.0.0'
范围区间gradle[version1, version2]com.example:foo: [1.0.0, 2.0.0]
maven[version1, version2][1.0.0, 2.0.0]
pub'>=version1 <=version2 'foo: '>=1.0.0 <=3.0.0'当存在区间约束的时候,版本号需要通过单引号进行包裹
npmversion1-version2

>=version1 <=version2
foo: 1.0.0-3.0.0


foo: >=1.0.0 <=3.0.0
version1 到 version的任意版本号,包含自身
pod>=version1, <=version2pod 'foo', '>= 1.0.0' , '<= 3.0.0'
范围集合gradle(,version1), [version2,)com.example:foo:
(,1.0.0),[3.0.0,)
< 1.0.0 或者 >= 3.0.0
maven(,version1), [version2,)(,1.0.0),[3.0.0,)同上
pub不支持不支持不支持
npmversion1version2foo: <1.0.0>= 3.0.0< 1.0.0 或者 >= 3.0.0
pod=version2pod 'foo', '< 1.0.0' , '>= 3.0.0'同上
排除制定版本gradle(,version), (version,)com.example:foo:
(,1.0.5),(1.0.5,)
不等于 1.0.5
maven(,version), (version,)(,1.0.5),(1.0.5,)同上
pub不支持不支持不支持
npm>versionfoo: <1.0.5>1.0.5不等于 1.0.5
pod!= versionpod 'foo', '!= 1.0.5'不等于 1.0.5
特有gradlemaven特殊版本标识: -SNAPSHOTcom.example:foo: 1.0.0-SNAPSHOT这个其实是maven的特殊版本标识,当你发布此带-SNAPSHOT标识版本后,maven自己会根据你的发布时间将版本展开为类似于1.0-yyyyMMdd-HHmmss-1 的格式,所以如果你带了此标识,你可以重复发布此版本,当前前提是你的maven开启了对应的配置。
其他特殊标识
dev

rc\snapshot\final\ga\release\sp
1. dev会被判定低于任何其他非数字部分,如:
1.0.0-dev < 1.0.0-ALPHA < 1.0.0-alpha < 1.0-rc
2. 字符串rc,snapshot,final,ga,release和 sp 被认为高于其他字符串部分(按此顺序排序),如:
1.0-zeta < 1.0-rc < 1.0-snapshot < 1.0-final < 1.0-ga < 1.0-release < 1.0-sp < 1.0


有些平台中还有一些特定的其他语法和规则,如果感兴趣,可以点击平台名称的超链接进入对应平台的官方文档自行查看。



相信你读到了这里,对语义化版本控制规范已经了然于胸。那么开篇的两个问题你是否也有了答案,欢迎在评论区留言。


Q&A 环节


经过上面的分享,相信大家对语义化版本已经有了一个整体的了解,那么我们来检验一下你的学习效果,请尝试回答下面几个问题:


Q:“v1.2.3” 是一个语义化版本号吗?


首先,“v1.2.3” 并不是的一个语义化的版本号。但是,在语义化版本号之前增加前缀 “v” 是用来表示版本号的常用做法。在版本控制系统中,将 “version” 缩写为 “v” 是很常见的。但是我们可以通过 npm-semver 来进行处理并转化成语义化的版本。


npm-semver可以帮你处理和转化语义化版本


Q:这么多规则及要求,我该如何验证我的语义化版本是否符合规范或者比较他们之间的大小关系呢?


这里就推荐 npm 的 github.com/npm/node-se…


node-semver 也可以帮你做到


对于脚本上对版本是否符合要求进行验证,可以使用 semver 2.0 文档中推荐的如下两个正则表达式。


第一个用于支持按组名称提取的语言,PCRE(Perl 兼容正则表达式,比如 Perl、PHP 和 R)、Python 和 Go。参见: regex101.com/r/Ly7O1x/3/



/^(?P0|[1-9]\d*).(?P0|[1-9]\d*).(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:+(?P[0-9a-zA-Z-]+(?:.[0-9a-zA-Z-]+)*))?$/gm



第二个用于支持按编号提取的语言(与第一个对应的提取项按顺序分别为:major、minor、patch、prerelease、buildmetadata)。主要包括 ECMA Script(JavaScript)、PCRE(Perl 兼容正则表达式,比如 Perl、PHP 和 R)、Python 和 Go。 参见: regex101.com/r/vkijKf/1/



/^(0|[1-9]\d ).(0|[1-9]\d).(0|[1-9]\d )(?:-((?:0|[1-9]\d|\d*a-zA-Z-)(?:.(?:0|[1-9]\d |\da-zA-Z- )) ))?(?:+([0-9a-zA-Z-]+(?:.[0-9a-zA-Z-]+)))?$/gm



Q:万一不小心把一个不兼容的改版当成了次版本号发行了,或者在修订等级的发布中,误将重大且不兼容的改变加到代码之中,我能通过重复发布当前版本来解决问题吗?


首先必须强调一点,不管如何都不能去修改已发行的版本(这点在部分平台已经帮你处理掉了,例如 pub 本身已经做了这种限制)。然后最好根据场景升级一个对应级别的版本来回滚逻辑,最后再将你的重大且不兼容的改版升一个主版本号进行发布。记住,语义化的版本控制就是透过版本号的改变来传达意义。


尾声


至此,我们已经了解了语义化版本控制规范的具体细则,常用的先行版本号标识的含义及应用场景,希望能在大家日后的工作生活当中所有帮助。你还见过哪些常见的先行版本号,你们团队又是如何避免包依赖地狱的,欢迎在评论区补充。感谢大家的观看,再见。


作者:政采云技术
来源:juejin.cn/post/7278238875456684090
收起阅读 »

某研究生不写论文竟研究起了算命?

起因 大约一个月前,在学校大病一场(不知道是不是🐑了,反正在学校每天核酸没检测出来)在宿舍休息了整整一周。当时因为发烧全身疼所以基本一直躺着刷刷视频。看了一周倪海厦老师讲的天纪,人纪感悟颇多,中华传统中一些优秀的东西竟然在现代教育下被丢失了而现在的人也只有在身...
继续阅读 »

起因


大约一个月前,在学校大病一场(不知道是不是🐑了,反正在学校每天核酸没检测出来)在宿舍休息了整整一周。当时因为发烧全身疼所以基本一直躺着刷刷视频。看了一周倪海厦老师讲的天纪,人纪感悟颇多,中华传统中一些优秀的东西竟然在现代教育下被丢失了而现在的人也只有在身体不得不休息的情况下才会停止内卷慢下来好好思考。


当然有人会说算命啥的都是封建迷信,作为接受了科学思想洗礼的新时代人慢慢也不再去接受那一套,在这里呢我不对这些想法做任何评价。信则有不信则无,存在即合理嘛。


因为疫情原因呢学校早早就给我们放了假,有了更多的空闲时间可以思考在学校没时间想的事情,做一些除开看论文、做项目之外的事


开始


前几天买的服务器刚好到家了,花了几天配置好环境(最重要的是花了大半天解决todesk、向日葵远程黑屏的问题)。今天下午写好代码丢到阳台让它自己慢慢训练去,剩下的时间就开始写今天关于算命的小程序。


六壬法


六壬法


留连速喜赤口
大安空亡小吉

根据倪师讲的,计算现在的农历日期加上时辰就可以推算当前某个想法适不适合去做。以大安开始每次从当宫开始顺时针数,下面是例子。



  • 假如今天是农历十二月十号 子时




  1. 首先从大安开始数1到12,结果是空亡




  2. 再从空亡开始数1到10,结果是速喜




  3. 然后从速喜开始子丑寅卯这样数,子是1




  4. 所以上面例子最后的结果就是速喜




计算过程都理解了怎么写成代码呢。这里需要知道三个条件,农历的月日以及当前的时辰,农历的月日这个地方我借鉴了这篇博客【C/C++】:用C实现输出日期的阴历日子直接复制了主要内容,打表直接计算农历日期,只需要输入当前阳历的年月日。


然后当前时间转为十二时辰也很简单,首先得到当前的小时时间hour,那么十二时辰就是(hour+1)/2<12?(hour+1)/2:0;这里子时记为0。


整个程序如下:


#include<iostream>
#include<string>
#include<ctime>
#include<vector>

std::vector<std::string> MAP{"大安","留连","速喜","赤口","小吉","空亡"};

unsigned int LunarCalendarDay;
unsigned int LunarCalendarTable[199] =
{
0x04AE53,0x0A5748,0x5526BD,0x0D2650,0x0D9544,0x46AAB9,0x056A4D,0x09AD42,0x24AEB6,0x04AE4A

,/*1901-1910*/


0x6A4DBE,0x0A4D52,0x0D2546,0x5D52BA,0x0B544E,0x0D6A43,0x296D37,0x095B4B,0x749BC1,0x049754

,/*1911-1920*/


0x0A4B48,0x5B25BC,0x06A550,0x06D445,0x4ADAB8,0x02B64D,0x095742,0x2497B7,0x04974A,0x664B3E

,/*1921-1930*/


0x0D4A51,0x0EA546,0x56D4BA,0x05AD4E,0x02B644,0x393738,0x092E4B,0x7C96BF,0x0C9553,0x0D4A48

,/*1931-1940*/


0x6DA53B,0x0B554F,0x056A45,0x4AADB9,0x025D4D,0x092D42,0x2C95B6,0x0A954A,0x7B4ABD,0x06CA51

,/*1941-1950*/


0x0B5546,0x555ABB,0x04DA4E,0x0A5B43,0x352BB8,0x052B4C,0x8A953F,0x0E9552,0x06AA48,0x6AD53C

,/*1951-1960*/


0x0AB54F,0x04B645,0x4A5739,0x0A574D,0x052642,0x3E9335,0x0D9549,0x75AABE,0x056A51,0x096D46

,/*1961-1970*/


0x54AEBB,0x04AD4F,0x0A4D43,0x4D26B7,0x0D254B,0x8D52BF,0x0B5452,0x0B6A47,0x696D3C,0x095B50

,/*1971-1980*/


0x049B45,0x4A4BB9,0x0A4B4D,0xAB25C2,0x06A554,0x06D449,0x6ADA3D,0x0AB651,0x093746,0x5497BB

,/*1981-1990*/


0x04974F,0x064B44,0x36A537,0x0EA54A,0x86B2BF,0x05AC53,0x0AB647,0x5936BC,0x092E50,0x0C9645

,/*1991-2000*/


0x4D4AB8,0x0D4A4C,0x0DA541,0x25AAB6,0x056A49,0x7AADBD,0x025D52,0x092D47,0x5C95BA,0x0A954E

,/*2001-2010*/


0x0B4A43,0x4B5537,0x0AD54A,0x955ABF,0x04BA53,0x0A5B48,0x652BBC,0x052B50,0x0A9345,0x474AB9

,/*2011-2020*/


0x06AA4C,0x0AD541,0x24DAB6,0x04B64A,0x69573D,0x0A4E51,0x0D2646,0x5E933A,0x0D534D,0x05AA43

,/*2021-2030*/


0x36B537,0x096D4B,0xB4AEBF,0x04AD53,0x0A4D48,0x6D25BC,0x0D254F,0x0D5244,0x5DAA38,0x0B5A4C

,/*2031-2040*/


0x056D41,0x24ADB6,0x049B4A,0x7A4BBE,0x0A4B51,0x0AA546,0x5B52BA,0x06D24E,0x0ADA42,0x355B37

,/*2041-2050*/


0x09374B,0x8497C1,0x049753,0x064B48,0x66A53C,0x0EA54F,0x06B244,0x4AB638,0x0AAE4C,0x092E42

,/*2051-2060*/


0x3C9735,0x0C9649,0x7D4ABD,0x0D4A51,0x0DA545,0x55AABA,0x056A4E,0x0A6D43,0x452EB7,0x052D4B

,/*2061-2070*/


0x8A95BF,0x0A9553,0x0B4A47,0x6B553B,0x0AD54F,0x055A45,0x4A5D38,0x0A5B4C,0x052B42,0x3A93B6

,/*2071-2080*/


0x069349,0x7729BD,0x06AA51,0x0AD546,0x54DABA,0x04B64E,0x0A5743,0x452738,0x0D264A,0x8E933E

,/*2081-2090*/
0x0D5252,0x0DAA47,0x66B53B,0x056D4F,0x04AE45,0x4A4EB9,0x0A4D4C,0x0D1541,0x2D92B5

/*2091-2099*/
};

int MonthAdd[12] = {0,31,59,90,120,151,181,212,243,273,304,334};
int LunarCalendar(int year,int month,int day)
{
int Spring_NY,Sun_NY,StaticDayCount;
int index,flag;
//Spring_NY 记录春节离当年元旦的天数。
//Sun_NY 记录阳历日离当年元旦的天数。
if ( ((LunarCalendarTable[year-1901] & 0x0060) >> 5) == 1)
Spring_NY = (LunarCalendarTable[year-1901] & 0x001F) - 1;
else
Spring_NY = (LunarCalendarTable[year-1901] & 0x001F) - 1 + 31;
Sun_NY = MonthAdd[month-1] + day - 1;
if ( (!(year % 4)) && (month > 2))
Sun_NY++;
//StaticDayCount记录大小月的天数 29 或30
//index 记录从哪个月开始来计算。
//flag 是用来对闰月的特殊处理。
//判断阳历日在春节前还是春节后
if (Sun_NY >= Spring_NY)//阳历日在春节后(含春节那天)
{
Sun_NY -= Spring_NY;
month = 1;
index = 1;
flag = 0;
if ( ( LunarCalendarTable[year - 1901] & (0x80000 >> (index-1)) ) ==0)
StaticDayCount = 29;
else
StaticDayCount = 30;
while (Sun_NY >= StaticDayCount)
{
Sun_NY -= StaticDayCount;
index++;
if (month == ((LunarCalendarTable[year - 1901] & 0xF00000) >> 20) )
{
flag = ~flag;
if (flag == 0)
month++;
}
else
month++;
if ( ( LunarCalendarTable[year - 1901] & (0x80000 >> (index-1)) ) ==0)
StaticDayCount=29;
else
StaticDayCount=30;
}
day = Sun_NY + 1;
}
else //阳历日在春节前
{
Spring_NY -= Sun_NY;
year--;
month = 12;
if ( ((LunarCalendarTable[year - 1901] & 0xF00000) >> 20) == 0)
index = 12;
else
index = 13;
flag = 0;
if ( ( LunarCalendarTable[year - 1901] & (0x80000 >> (index-1)) ) ==0)
StaticDayCount = 29;
else
StaticDayCount = 30;
while (Spring_NY > StaticDayCount)
{
Spring_NY -= StaticDayCount;
index--;
if (flag == 0)
month--;
if (month == ((LunarCalendarTable[year - 1901] & 0xF00000) >> 20))
flag = ~flag;
if ( ( LunarCalendarTable[year - 1901] & (0x80000 >> (index-1)) ) ==0)
StaticDayCount = 29;
else
StaticDayCount = 30;
}
day = StaticDayCount - Spring_NY + 1;
}
LunarCalendarDay |= day;
LunarCalendarDay |= (month << 6);
if (month == ((LunarCalendarTable[year - 1901] & 0xF00000) >> 20))
return 1;
else
return 0;
}

void output(int year,int month,int day,int &d_month,int &d_day)
{
const char *ChDay[] = {"*","初一","初二","初三","初四","初五",
"初六","初七","初八","初九","初十",
"十一","十二","十三","十四","十五",
"十六","十七","十八","十九","二十",
"廿一","廿二","廿三","廿四","廿五",
"廿六","廿七","廿八","廿九","三十"
};
const char *ChMonth[] = {"*","正","二","三","四","五","六","七","八","九","十","十一","腊"};
char str[13] = "";
strcat(str,"农历");
if (LunarCalendar(year,month,day)){
strcat(str,"闰");
d_month=(LunarCalendarDay & 0x3C0) >> 6;
strcat(str,ChMonth[(LunarCalendarDay & 0x3C0) >> 6]);
}
else{
d_month=(LunarCalendarDay & 0x3C0) >> 6;
strcat(str,ChMonth[(LunarCalendarDay & 0x3C0) >> 6]);
}
strcat(str,"月");
d_day=LunarCalendarDay & 0x3F;
strcat(str,ChDay[LunarCalendarDay & 0x3F]);
puts(str);
}


int ChangeHourToPeriods(){
time_t now=time(0);
int hour;
std::tm* t=std::localtime(&now);
hour=(int)t->tm_hour;
hour=(hour+1)/2<12?(hour+1)/2:0;
return hour;
}


int main(){
int year,month,day;
std::cout<<"Please input the number of year,month,day:"<<std::endl;
std::cin>>year>>month>>day;
int d_month=0,d_day=0;
output(year,month,day,d_month,d_day);
int hour=ChangeHourToPeriods();
int SumAll=d_month+d_day+hour-2;
std::cout<<"The result is : "<<MAP[(SumAll%6)]<<std::endl;
return 0;
}

因为每次从当宫开始数也就每次都少数了1,但是由于最后子时以0开始所以只减2。


测试结果(测试的时间是21点亥时)
在这里插入图片描述


占卦法


占卦法


还有傅佩荣教授的三组三位数占卦法,这个更多就是易经中的内容太过于深奥,所以直接说计算方法。


三组三位数分别代表下卦,上卦和爻,卦象的三位数对8取余爻对6取余,如果整除就记为除数本身。


然后再去六十四卦中复制打表,最后得到卦象以及爻


整体实现如下


#include<iostream>
#include<string>
#include<vector>


std::vector<std::string> TRIGRAM=std::vector<std::string> {"乾","兑","离","震","巽","坎","艮","坤"};
std::vector<std::string> TRIGRAMMAP {
"乾为天","泽天夬","火天大有","雷天大壮","风天小畜","水天需","山天大畜","地天泰",
"天泽履","兑为泽","火泽睽","雷泽归妹","风泽中孚","水泽节","山泽损","地泽临",
"天火同人","泽火革","离为火","雷火丰","风火家人","水火既济","山火贲","地火明夷",
"天雷无妄","泽雷随","火雷噬嗑","震为雷","风雷益","水雷屯","山雷颐","地雷复",
"天风姤","泽风大过","火风鼎","雷风恒","巽为风","水风井","山风蛊","地风升",
"天水讼","泽水困","火水未济","雷水解","风水涣","坎为水","山水蒙","地水师",
"天山遁","泽山咸","火山旅","雷山小过","风山渐","水山蹇","艮为山","地山谦",
"天地否","泽地萃","火地晋","雷地豫","风地观","水地比","山地剥","坤为地"

};

int main(){
std::vector<int> LowToHigh(3);
std::vector<std::string> results(3);
std::cout<<"Input 3*xxx like(111,222,333),means from up to high"<<std::endl;

for(int i=0;i<3;++i){
std::cin>>LowToHigh[i];
}

LowToHigh[0]=LowToHigh[0]%8==0?8:LowToHigh[0]%8;
LowToHigh[1]=LowToHigh[1]%8==0?8:LowToHigh[1]%8;
LowToHigh[2]=LowToHigh[2]%6==0?6:LowToHigh[2]%6;



for(int i=0;i<2;++i){
results[i]=TRIGRAM[LowToHigh[i]-1];
}
results[2]=std::to_string(LowToHigh[2]);

std::cout<<"下卦:"<<results[0]<<" 上卦:"<<results[1]<<" "<<results[2]<<std::endl;
std::cout<<"卦象: "<<TRIGRAMMAP[(LowToHigh[0]-1)*8+LowToHigh[1]-1]<<std::endl;
}

这里测试的例子是视频中的数字
在这里插入图片描述


结尾


有的时候真的需要一些自己的沉淀时间,不能忘了最初的目标。写这篇Blog也并不是为了宣传封建迷信,而是希望让更多程序圈的人想想当初我们喜欢敲代码到底是为了什么?我们喜欢的是将代码作为工具实现一些有趣的功能,并不是为了水论文或者工作被迫的去东拼西凑弄出一些用处不大的东西。


好多想吐槽的话最终还是打了又删,无法改变现状的时候只能屈服于现状,我相信不止我一人如此。但是请不要忘了 潜龙勿用见龙在田,终日乾乾飞龙在天


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

使用后端代码生成器,提高开发效率

如果你是一名后端开发者,那么大多数的工作一定是重复编写各种 CRUD(增删改查)代码。时间长了你会发现,这些工作不仅无趣,还会浪费你的很多时间,没有机会去做更有创造力和挑战的工作。 作为一名程序员,一定要学会偷懒!学会利用工具来解放人力。 其实现在有很多现成的...
继续阅读 »

如果你是一名后端开发者,那么大多数的工作一定是重复编写各种 CRUD(增删改查)代码。时间长了你会发现,这些工作不仅无趣,还会浪费你的很多时间,没有机会去做更有创造力和挑战的工作。


作为一名程序员,一定要学会偷懒!学会利用工具来解放人力。


其实现在有很多现成的代码生成器,可以帮助我们自动生成常用的增删改查代码,而不用自己重复编写,从而大幅提高开发效率,所以大家一定要掌握。


对应到 Java 后端开发,主流技术是 Spring Boot + Spring MVC + MyBatis 框架,使用这些技术来开发项目时,通常需要编写数据访问层 (DAO / Mapper) 和数据库表的 XML 映射代码、实体类、Service 业务逻辑代码、以及 Controller 接口代码。


本文就以使用 IDEA 开发工具中我认为非常好用的免费代码生成插件 MyBatisX 为例,带大家学习如何使用工具自动生成后端代码,节省时间和精力。


MyBatisX 自动生成代码教程


1、安装 MyBatisX 插件


首先,确保你已经安装了 IntelliJ IDEA 开发工具。


打开你的项目工程,然后进入 Settings 设置页搜索 MyBatisX 插件并安装,步骤如图:



2、配置数据库连接


MyBatisX 插件的核心功能是根据数据库表的结构来生成对应的实体类、数据访问层 Mapper、Service 等代码,所以在使用前,我们需要在 IDEA 中配置一个数据库连接。


先在 IDEA 右侧的 Database 中创建一个 MySQL 数据源配置:



然后根据自己的数据库信息填写配置,并测试能否连接成功:



连接成功后,就可以在 IDEA 中管理数据库了,不需要 Navicat 之类的第三方工具:



3、使用 MyBatisX 生成代码


右键要生成代码的数据表,进入 MyBatisX 生成器:



然后进入生成配置页面,可以根据你的需求来自定义代码生成规则:



上述配置中,我个人建议 base package (生成代码的包名和位置)尽量不要和已有的项目包名重叠,先把代码生成到一个完全不影响业务的位置,确认生成的代码没问题后,再移动代码会更保险一些。


进入下一步,填写更多的配置,可以选择生成代码的模板(一般是 MyBatis-Plus 模板),以及自定义实体类的生成规则(一般建议用 Lombok)。


以下是我常用的推荐配置:



改完配置后,直接点击生成即可,然后可以在包目录中看到生成的代码:



4、定制修改


通过以上方法,就已经能够完成基础增删改查代码的生成了,但一般情况下,我们得到生成的代码后,还要再根据自己的需求进行微调。


比如把主键 ID 的生成规则从自动递增改为雪花算法生成,防止数据 id 连续被别人轻松爬走:



最后你就可以使用现成的代码来操作数据库啦~


其他


如开头所说,现在的代码生成器非常多,比如 MyBatis Plus 框架也提供了灵活的代码生成器:



指路:baomidou.com/pages/98140…




再比如可以直接在浏览器使用的代码生成器,鱼皮自己也开发过并且开源了:



指路:sqlfather.yupi.icu/


开源:github.com/liyupi/sql-…




感兴趣的话,大家也可以尝试使用 FreeMarker 技术做一个属于自己的代码生成器。


实践


编程导航星球的用户中心项目使用了 MyBatisX 插件来生成代码,非常简单,大家一定要学会运用!


作者:程序员鱼皮
来源:juejin.cn/post/7300949817378308123
收起阅读 »

中介思想背后的三大技术意识,你具备了哪些?

不论在天上,在自然界,在精神中,不论在哪个地方,没有什么东西不是同时包含着直接性和间接性的。——黑格尔 代码既是一种艺术创作,也是一种工艺制造,其中也包含诸多中介。 以下是一个简单认识中介的例子: 中介活动:中介活动系指中介人居间帮助 委托方、委托方合作者(...
继续阅读 »

不论在天上,在自然界,在精神中,不论在哪个地方,没有什么东西不是同时包含着直接性和间接性的。——黑格尔



代码既是一种艺术创作,也是一种工艺制造,其中也包含诸多中介。


以下是一个简单认识中介的例子:


中介活动:中介活动系指中介人居间帮助

委托方、委托方合作者(下简称合作者)双方达成某项协议/契约/合同的活动。


嗯,读到这儿好像还能理解,以买房为例,大概就如下图所示:


中介活动图.jpg


但还得再加上这么一句话,但在中介过程中,牵涉到中介人与委托方(甲、乙方或双方)

签约、发布、寻找委托方合作者、协调甲、乙方签约、帮助完成签约和追索并获得报酬等活动。


中介活动图2.jpg


相比上一张图,这张图更能体现出中介具体做了哪些工作



  • 积极收集符合委托方条件的合作者;

  • 和合作者讨价还价;

  • ······


当然,中介并不是白干这些活,他最终也是需要中介费的,这也就是委托方的成本。


看完只想说,啊啊啊啊可恶要长脑子了,不过没有关系,在代码意识中,成本的耗费相比模块和架构意识少。




代码意识


言归正传,我们直入主题,看以下两个案例:




  • 有代码块 A、B、C... 想使用变量 “str”,我们往往会通过静态常数描述它,

    如:static String CONSTANT = "str"




  • 委托方:代码块 A、B、C...




  • 中介人:CONSTANT




  • 合作者:"str"




  • 有类 A、B、C... 想使用类 X,在 Spring 框架中,我们常常会通过注入的方式让 X 被其他类依赖.




  • 委托方:类 A、B、C...




  • 中介人:Spring 容器




  • 合作者:类 X




上述只是两个简单的中介行为案例,现实生活中委托方和合作者往往都会向中介人提供自己的需求,

而中介人则需要根据需求推荐委托方或合作者。


衍生出下一个案例:



  • 在一个方法中,入参为 code,而这个方法动作则需要根据不同的 code 执行不同的逻辑。

    如:


void performLogic(String code) {  

}

如果 code 的变化是固定的,例如像英文字母,无论如何都是 26 个,那我们穷举出来,其实也不耽误代码的扩张性,

只是过于冗长有点丑陋,但这种情况在实际中偏少。


void performLogic(String code) {  
if (code == "A") {

} else if(code == "B") {

} ... {

} else if (code == "Y") {

} else {

}
}

实际开发过程中,code 的变化往往是动态的,考虑维护成本和扩张性的功能,所以在方法performLogic()

显然不能因为 code 的动态变化而变化。


那可不可以找一个中介,让其提前知晓 code 对应的逻辑,每当 code 投来,我们让它把对应的逻辑给到委托者,

这样,委托者只需要关心自己在什么场景下传递什么 code,而不需要关心具体的逻辑怎么做。


这种情景 Map 结构再合适不过,因此可以这么写:


  
@Value // get
private final Map<String, Logic> knownLogics;

void performLogic(String code) {
Logic logic = knownLogics.getOrDefault(code, defaultLogic);
logic.performed();
}

那以上的身份可以确定如下:



  • 委托者:执行 performLogic() 的业务

  • 中介者:knownLogics

  • 合作者:对应的逻辑


中介活动也显然易见:



  1. 中介提前知晓委托者(上层业务)的需求(code)对应哪些合作者(逻辑)

  2. 委托者将需求给中介(knownLogics)

  3. 中介将对应的合作者告知委托者

  4. 委托者与合作者完成合作


中介者的身份有效地将委托者与合作者进行了解藕,彼此各尽其职。


相反,如果在此处采取 “字母”的做法,那么每当出现新的 code ,那么都需要在方法 performLogic() 中修改。


综上,使用中介思想可以让委托者和合作者在遵循单一职责和开闭原则的同时,还能保证委托者与合作者合作的代码不变,

是符合面向对象设计原则的。因此,使用中介思想可以促进开发人员理解面向对象设计原则和灵活使用设计模式,进而提高代码质量。




模块意识


以规则引擎在众安无界山理赔中心的应用为例:


在理赔业务中,主要有报案、立案、定损、理算和核赔五大流程,每一个流程进行至下一步时都需要进行规则校验,

例如有黑名单客户校验、反洗钱校验等校验规则。


如果将这些规则嵌套在每一步流程的代码中,那么一旦面对规则逻辑需要修改时,就不得不在原有代码上进行修改,

这导致规则与代码逻辑强耦合,并且每一次修改规则时,都需要重新编译代码。


我们可以通过中介意识将这个问题解决,我们将每个被校验的对象当作变量 x

在经过一个函数:


Fn(x1x2...,xn){0,1}F_n(x_1,x_2,..., x_n) \in
\begin{Bmatrix}
0, 1
\end{Bmatrix}

后得到通过或不通过。


理赔传参至函数.png


到这一步,我们也只是知道了委托方(业务代码)、委托方的需求(变量 X)及合作者(函数)


那中介是谁呢?没错,这个中介就是要新增的模块


言归正传,我们回到规则引擎在无界山理赔中心的应用中,那么我们可以确定以下身份:



  • 委托方:业务代码

  • 中介:规则引擎

  • 合作者:规则组(一簇规则;函数)


中介活动如下:



  1. 委托方(业务代码)提供需求(被校验的对象)给中介(规则引擎)

  2. 中介寻找合作者(规则组)

  3. 合作者按照委托方的需求签订协议(校验结果)


有了如上意识后,我们可以将规则引擎单独做一个模块去开发,然后使业务模块依赖,最后通过“创造(配置)”合作者(规则)。


这样,所有要使用到规则校验的业务代码都只需要通过规则引擎的入口,传递指定的需求和规则组 code 即可完成校验,如下图:


理赔传参至函数2.png


事实上,从模块意识开始,成本的问题就略有呈现了,例如:



  • 创建出中介;

  • 编写中介找到合作者逻辑;

  • 合作者创造出来,应该存储在何处?又如何管理?


通常来讲,都是存储到数据库,又通过接口调用进行增删改查,虽然与业务代码进行了解耦,但需要另取资源存储和管理,

那这样是否是拆东墙补西墙呢?


回答这个问题之前,反过来问一个问题,如果保持原来的做法,没有中介,那又会怎么样呢?因此这就成了对比,需要在权衡之下做选择。


显然,有了中介能够拆更少的东墙补更多的西墙。




架构意识



以前车马很慢,书信很远,一生只够爱一个人。



为了能让信封能够抵达心上人的手上,往往会将信封塞到信箱中,或是托信使帮忙托送,尔后忙于其他。


架构亦是如此,服务与服务之间难免存在沟通的情况,例如:



  1. 如果服务 A 需要且满足某接口,那么通常会让服务 A 寻找实现了该接口的服务;

  2. 如果服务 A 只是需要某服务的处理,并不关心处理的细节,那么通常会让 A 传递给信使,信使再告诉能帮助 A 的服务 X。


从中介思想的角度看上述两个案例,需要解决两个问题:



  1. 案例 1 中 A 是如何找到实现了该接口的服务?

  2. 案例 2 中的信使是谁?又如何找到他?


问题显而易见:



  1. 案例 1 中 A 肯定是通过中介才找到实现了该接口的服务;

  2. 案例 2 中 A 肯定也是通过中介才找到信使;

  3. 案例 2 中 信使自己本身也是一个中介人,负责存储 A 的需求和寻找帮助 A 的服务 X。


事实上,上述的案例就是现在的远程过程调用(Remote Procedure Call, 下简称 RPC)

和消息队列(Message Queue, 下简称 MQ)。


案例 1 (以 Dubbo 框架作为 RPC 框架为例)的身份确定如下:



  • 委托方:服务 A

  • 中介:注册中心

  • 合作者:实现了该接口的服务





案例 1 的中介活动如下:



  1. 委托方(服务 A)已知合作者(实现了该接口的服务)的要求(接口信息)

  2. 委托方按要求提供信息给中介

  3. 中介根据委托方提供的信息寻找合适(时间、天气等外部因素)的合作者

  4. 委托方得到合作者的答复(响应结果)


案例 2 的身份确定如下:



  • 委托方:服务 A

  • 中介 C:配置中心

  • 合作者 M 兼中介 M:消息队列

  • 合作者 X:帮助服务 A 的服务 X


生产者-消费者.jpg


案例 2 的中介活动如下:



  • 委托方(服务 A)从中介 C (配置中心)得知有中介 M (消息队列)可以帮助他

  • 委托方找到中介 M 拖信(消息体)

  • 中介 M 将信传给委托方指定的合作者 X (帮助服务 A 的服务)


中介意识在当前分布式架构中非常常用,除了 RPC 和 MQ 用于服务之间的交流之外,

还诞生了许多中介人用于不同的场景,例如:



  • 充当中介的事务协调者,用于分布式事务场景;

  • 充当中介的分布式锁,用于多服务对共享资源的访问;

  • 充当中介的负载均衡器,用于多服务时的负载均衡;

  • 充当中介的服务监控,用于监视多服务沟通链路;

  • ······


到这儿,作为造物主的你此时会发现,除了在完成功能的服务之外,令你头疼的不仅仅是功能逻辑,

还涉及到了如何让功能在各种场景下运行,因此你做了不少非原有功能的事情。


这就是成本,它不再是“多写几行代码”这种简单的成本,此时的它显然变得不可忽视。


那是不是就可以随意妄为呢?什么中介,这些成本才不想考虑。


那更有趣,如果我们的生活中没有像招标、房屋、拍卖和招聘这种中介,似乎好像也没啥影响,无疑是变得不方便。

但中介有没有可能是物,是思想呢?

比如一个人想让另一个人消失,是什么在约束他呢......


总结


艺术来源于生活,代码也如此,程序员也是一种艺术家,如今在现实生活中已有很多中介案例,只需要模仿或照搬。

特别地是,程序员有着与生俱来的逻辑感,他们热衷于为什么,于是在代码世界中将那些曾经的工匠精神复燃,

在那里,重现了将动力和转矩传递到所需处的齿轮、解决远距离沟通的电话乃至能量转换的发动机的发明。


参考文献


[1] Dubbo 官方文档


[2] 黑格尔. 逻辑学[M]. 北京: 商务印书馆, 1976.


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

防抖是回城,节流是攻击

web
前言 防抖和节流是前端开发中常用的函数优化手段,它们可以限制函数的执行频率,提升性能和用户体验。在我们的日常开发中,经常会遇到一些需要对函数进行优化的场景,比如防止表单的重复提交。 一 防抖与节流的区别 我们简单描述下它们的作用 防抖:它限制函数在一段连续的时...
继续阅读 »

前言


防抖节流是前端开发中常用的函数优化手段,它们可以限制函数的执行频率,提升性能和用户体验。在我们的日常开发中,经常会遇到一些需要对函数进行优化的场景,比如防止表单的重复提交。


一 防抖与节流的区别


我们简单描述下它们的作用


防抖:它限制函数在一段连续的时间内只执行一次。当连续触发某个事件时,只有在事件停止触发一段时间后,才会执行函数。


节流:它按照固定的时间间隔执行函数。当连续触发某个事件时,每隔一段时间执行一次函数。


简而言之,防抖是在事件停止触发后延迟执行函数,而节流是按照固定的时间间隔执行函数。


因为防抖节流的作用和应用场景基本相同,也就导致它们容易被人混淆,不好记忆。


之前在网上看到了一个例子非常的有趣形象,和大家分享下。


王者荣耀大家都玩过吧,里面的英雄都有一个攻击间隔,当我们连续的点击普通攻击的时候,英雄的攻速并不会随着我们点击的越快而更快的攻击。这个其实就是节流,英雄会按照自身攻速的系数执行攻击,我们点的再快也没用。


而防抖在王者荣耀中就是回城,在游戏中经常会遇到连续回城嘲讽对手的玩家,它们每点击一次回城,后一次的回城都会打断前一次的回城,只有最后一次点击的回城会被触发,从而保证回城只执行一次,这就是防抖的概念。


自从我看到这个例子后,节流和防抖就再也没记混过了。作为一个8年王者老玩家。


下面是防抖和节流的实现


防抖的实现与使用


防抖的应用场景:



  1. 输入框搜索:当用户在搜索框中输入关键字时,使用防抖可以避免频繁发送搜索请求,而是在用户停止输入一段时间后才发送请求,减轻服务器压力。

  2. 窗口调整:当窗口大小调整时,使用防抖可以避免频繁地触发重排和重绘操作,提高页面性能。

  3. 按钮点击:当用户点击按钮时,使用防抖可以避免用户多次点击造成的多次提交或重复操作。


immediate参数用于控制防抖函数是否立即触发,true立即触发,false过delay时间后触发。


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按钮</button>
<script>
function debounce(func, delay, immediate) {
let timer;
return function () {
let context = this;
let args = arguments;

if (timer) {
clearTimeout(timer)
}

if (immediate && !timer) {
func.apply(context, args)
}

timer = setTimeout(() => {
timer = null
if (!immediate) {
func.apply(context, args)
}
}, delay);
}
}

// 创建一个被防抖的函数
const debouncedFunction = debounce(() => {
console.log("Debounced function executed.");
}, 1000, false);

document.getElementById('btn').addEventListener('click', debouncedFunction)

</script>
</body>

</html>

节流的实现与使用


节流的应用场景:



  1. 页面滚动:当页面滚动时,使用节流可以限制滚动事件的触发频率,减少事件处理的次数,提高页面的响应性能。

  2. 鼠标移动:当鼠标在某个元素上移动时,使用节流可以减少事件处理的次数,避免过于频繁的操作。


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按钮</button>
<script>

function throttle(func, delay, immediate) {
let timer;
return function () {
const context = this
const args = arguments

if (timer) {
return
}

if (!timer && immediate) {
func.apply(context, args)
}

timer = setTimeout(() => {
timer = null

if (!immediate) {
func.apply(context, args)
}
}, delay);
}
}

// 创建一个被节流的函数
const throttledFunction = throttle(() => {
console.log("throttled function executed.");
}, 1000, false);

document.getElementById('btn').addEventListener('click',throttledFunction)
</script>
</body>

</html>

结尾


看完本文章后,希望能够加深大家对防抖和节流的印象,分清二者的区别。


作者:欲买炸鸡同载可乐
来源:juejin.cn/post/7301244391153467431
收起阅读 »

买房后,害怕失业,更不敢裸辞,心情不好就提前还房贷,缓解焦虑

自从买房后,心态有很大变化。虽然住自己的房子,心情和体验都很好,但是一把掏空钱包,很焦虑。买房后现金流一直吃紧,再加上每年16万的房贷,我很焦虑会失业。之前我喜欢裸辞,现在不敢想裸辞这个话题。尤其是在行业下行期,找工作很艰难,背着房贷裸辞,简直是头孢就酒,嫌命...
继续阅读 »

自从买房后,心态有很大变化。虽然住自己的房子,心情和体验都很好,但是一把掏空钱包,很焦虑。买房后现金流一直吃紧,再加上每年16万的房贷,我很焦虑会失业。之前我喜欢裸辞,现在不敢想裸辞这个话题。尤其是在行业下行期,找工作很艰难,背着房贷裸辞,简直是头孢就酒,嫌命太久。


焦虑的根源是背负房贷,金额巨大,而且担心40岁以后失业,还不上房贷。


一次偶然的沟通


"你的贷款利率调整了吗",同事问我。


同事比我早两年在北京买房,他在顺义买的,我在昌平买的,我俩一直有沟通房贷的问题。但我没听说利率有调整,银行好像也没通知我,于是我问道:”我不知道啊,你调整了?调到多少了?“。


”调的挺多的,已经降到了 4.3%“。同事兴高采烈的回复我。


”这么牛逼,之前我记得一直是4.85%,我去看看我的利率“,我听说房贷利率下降那么多,很是兴奋。


然而我的房贷利率没有调整,我尝试给银行打电话,沟通的过程很坎坷。工商银行客服说了很多,大概意思是:利率会自动调整,无需申请,但是要等到利率调整日才会调整”。我开始很不理解,很生气,利率都调整了,别人也都调整了,凭什么不给我调整呢?


我想到同事有尝试提前还贷,生气的时候,我就萌发了提前还贷的想法。


开始尝试提前还贷,真香


我在22年初贷款买房,其中商业贷款 174 万,贷款25年,等额本息,每个月要还 1 万的房贷。公积金贷款每个月大概需要还 2500。每个月一万二的房贷还是很有压力的,尤其是刚买房的这一两年,兜里比脸都干净,没存款,不敢失业,更不敢裸辞。


即便兜里存款不多,也要提前还贷,因为实在太香了。


我在工行App上,申请 提前还贷,选择缩短 18个月的房贷,只需要 6万2,而我每个月房贷才1万,相当于是用 6 万 顶 18 万的房贷。还有比这更划算的事情吗?


image.png
预约提前还款后,银行会安排一个时间,在这个时间前,把钱存进去。到时候银行就会扣除,如果扣除金额不足,那么提前还款计划则自动终止,需要重新预约!


工行的预约还款时间大概是1个月以后,我是10-15号申请提前还款,银行给的预约日期是 11-14号,大概是1个月。


提前还款,比理财强多了


这次还贷以后,我又申请了提前还款, 提前还 24 期,只需要 9 万,也就是 9 万顶 24 万;提前还 60 期,只需要 24 万,相当于 24 万顶 60 万。


image.png


image.png


还有比提前还贷收益更高,风险更低的理财方式吗?没有! 除了存款外,任何理财都是有风险的。债券和基金收益和风险挂钩,想找到收益5%的债券基金,要承担亏本风险。你惦记人家的利息,人家惦记你的本金!


股票的风险更不必说,我买白酒股票已经被套的死死,只能躺平装死。(劝大家不要入 A 股)


提前还贷划算吗?


我目前的贷款利息是 4.85%,而存到银行的利息不会超过 3% ,很多货币基金只有 2%了。两者利息差高达 3%,肯定是提前还贷款更加合适。


要明白,一年两年短期的利息差还好,但是房贷可是高达 25 年。25年 170 万贷款 3% 的利息差,这个金额太大了。提前还了,省下来的钱还是很多的。例如刚才截图里展示的 提前还 24 万顶了 60 万的房贷。


网上很多砖家说,“要考虑通货膨胀因素,4.85% 的贷款利率和实际通货膨胀比起来不高,提前还款不划算。”


砖家说话都是昧良心的。提前还贷款是否划算,只需要和存款利率比就行了,不需要和通货膨胀比。因为把钱存在银行也会因为通货膨胀贬值。只有把钱 全都消费,全部花光才不会受通货膨胀的困扰,建议砖家,多消费,把家底败光,这样最划算!


砖家们一定是害怕太多人提前还贷,影响了银行的放贷生意。今年上半年,提前还贷已经成潮流,有些银行坐不住,甚至关闭了提前还贷的入口…… 所以要抓紧,没准哪天就提高了还贷门槛,或者直接禁止。


程序员群体收入高,手里闲钱多,可以考虑提前还贷款,比存银行划算多了,别再给银行打工了!


作者:五阳神功
来源:juejin.cn/post/7301530293378727971
收起阅读 »

刚入职因为粗心大意,把事情办砸了,十分后悔

刚入职,就踩大坑,相信有很多朋友有我类似的经历。 5年前,我入职一家在线教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。 在熟悉环境之后,我趁着上厕所的时候,顺便去旁边的零食摊挑...
继续阅读 »

刚入职,就踩大坑,相信有很多朋友有我类似的经历。


5年前,我入职一家在线教育公司,新的公司福利非常好,各种零食随便吃,据说还能正点下班,一切都超出我的期望,“可算让我找着神仙公司了”,我的心里一阵窃喜。


在熟悉环境之后,我趁着上厕所的时候,顺便去旁边的零食摊挑了点零食。接下来的一天里,我专注地配置开发环境、阅读新人文档,当然我也不忘兼顾手边的零食。


初出茅庐,功败垂成


"好景不长",第三天上午,刚到公司,屁股还没坐热。新组长立刻给我安排了任务。他决定让我将配置端的课程搜索,从使用现有的Lucene搜索切换到ElasticSearch搜索。这个任务并不算复杂,然而我却办砸了。


先说为什么不复杂?



  1. ElasticSearch的搜索功能 基于Lucene工具库实现的,两者在搜索请求构造方式上几乎一致,在客户端使用上差异很小。


image.png



  1. 切换方案无需顾虑太多稳定性问题。由于是配置端课程搜索,并非是用户端搜索,所以平稳切换的压力较小、性能压力也比较小。


总的来说,领导认为这个事情并不紧急,重要性也不算高,而且业务逻辑相对简单,难度能够把握,因此安排我去探索一下。可是,我却犯了两个错误,把入职的第一件事办砸了。现在回过头来看,十分遗憾!


image.png


难以解决的bug让我陷入困境


将搜索方式从Lucene切换为ElasticSearch后,如何评估切换后搜索结果的准确度呢?


除了通过不断地回归测试,还有一个更好的方案。


我的方案是,在调用搜索时同时并发调用Lucene搜索和ElasticSearch搜索。在汇总搜索结果时,比对两者的搜索结果是否完全一致。如果在切换搜索引擎的过程中,两个方案的搜索结果不一致,就打印异常搜索条件和搜索结果,并进行人工排查原因。


image.png


在实际切换过程中,我经常遇到搜索数据不一致的情况,这让我感到十分苦恼。我花了一周的时间编写代码,然后又用了两周多的时间来排查问题,这超出了预估的时间。在这个过程中,我感到非常焦虑和沮丧。作为一个新来的员工,我希望能够表现出色,给领导留下好印象。然而事与愿违,难以解决的bug让我陷入困境。


经过无数次的怀疑和尝试,我终于找到了问题的根源。原来,我忘记了添加排序方式。


因为存在很多课程数据,所以配置端搜索需要分页搜索。在之前的Lucene搜索方式中,我们使用课程Id来进行排序。然而在切换到新的ElasticSearch方案中时,我忘记了添加排序方式。这个错误的后果是,虽然整体上结果是一致的,但由于新方案没有排序方式,每一页的搜索结果是随机的,无法预测,所以与原方案的结果不一致。


image.png
新方案加上课程Id排序方式以后,搜索结果和原方案一致。


为此,我总结了分页查询的设计要点!希望大家不要重复踩坑!# 四选一,如何选择适合你的分页方案?


千万不要粗心大意


实际上,在解决以上分页搜索没有添加排序方式的问题之后,还存在着许多小问题。而这些小问题都反映了我的另一个不足:粗心大意。


正是这些小问题,导致线上环境总会出现个别搜索结果不一致的情况,导致这项工作被拖延很久。


课程模型是在线教育公司非常核心的数据模型,业务逻辑非常复杂,当然字段也非常多。在我入职时,该模型已经有120 个字段,并且有近 50 个字段可以进行检索。


在切换搜索方式时,我需要重新定义各种DO、DTO、Request等类型,还需新增多个类,并重新定义这些字段。在这个过程中,我必须确保不遗漏任何字段,也不能多加字段。当字段数量在20个以内时,这项工作出错的可能性非常低。然而,班课模型却有多达120个字段,因此出错的风险极大。当时我需要大量搬运这些字段,然而我只把这项工作看作是枯燥乏味的任务,未能深刻意识到出错的可能性极大,所以工作起来散漫随意,没有特别仔细校验重构前后代码的准确性。


image.png
墨菲定律:一件事可能出错时就一定会出错



墨菲定律是一种普遍被接受的观念,指出如果某件事情可能出错,那么它将以最不利的方式出错。这个定律起源于美国航天局的项目工程师爱德华·墨菲,在1950年代发现了这一规律。


墨菲定律还强调了人类的倾向,即将事情弄糟或让事情朝着最坏的方向发展。它提醒人们在计划和决策时要考虑可能出错的因素,并准备应对不利的情况。



墨菲定律实在是太准了,当你感觉某个事情可能会出错的时候,那它真的就会出错。而且我犯错不止一次,因为有120个字段,很多字段的命名非常相似,最终我遗漏了2个字段,拼写错误了一个字段,总共有三个字段出了问题。


不巧的是,这三个字段也参与检索。当用户在课程搜索页面选择这三个字段来进行检索时,因为字段的拼写错误和遗漏,这三个字段没有被包含在检索条件中,导致搜索结果出错……


导致这个问题的原因有很多,其中包括字段数量太多,我的工作不够细致,做事粗心大意,而且没有进行充分的测试……


为什么没有测试


小公司的测试人员相对较少,尤其是在面对课程管理后台的技术重构需求时,更加无法获取所需的测试资源!


组长对我说:“ 要人没有,要测试更没有!”


image.png


事情办砸了,十分遗憾


首先,从各个方面来看,切换搜索引擎这件事的复杂度和难度是可控的,而且目标也非常明确。作为入职后第一项任务,我应该准确快速地完成它,以留下一个良好印象。当然,领导也期望我能够做到这一点,然而事实与期望相去甚远。


虽然在线上环境没有出现问题,但在上线后,问题排查的时间却远远超出了预期,让领导对结果不太满意。


总的来说,从这件事中,我获得的最重要教训就是:对于可能出错的事情,要保持警惕。时刻用墨菲定律提醒自己,要仔细关注那些可能发生小概率错误的细节问题。


对于一些具有挑战性的工作,我们通常都非常重视,且在工作中也非常认真谨慎,往往不会出错。


然而,像大量搬运代码、搬运大量字段等这类乏味又枯燥的工作确实容易使人麻痹大意,因此我们必须提高警惕。要么我们远离这些乏味的工作,要么就要认真仔细地对待它们。


否则,如果对这些乏味工作粗心大意,墨菲定律一定会找上你,让你在线上翻车!


作者:他是程序员
来源:juejin.cn/post/7295576148364787751
收起阅读 »

35岁遭遇父亲肺癌、失业、失恋. . . . . .

写在前面 目前已经上班快两个月了,对现在的工作很满意,甚至说更喜欢这的氛围吧。 如题所示,从今年5月开始,发生的所有事,都完全超出了我自己可以承受的范围,好在这一切都过去了,真的感谢上天安排,让我能更加确信自己要的是什么,以后该怎么生活。 爸爸被诊断为肺癌 我...
继续阅读 »

写在前面


目前已经上班快两个月了,对现在的工作很满意,甚至说更喜欢这的氛围吧。


如题所示,从今年5月开始,发生的所有事,都完全超出了我自己可以承受的范围,好在这一切都过去了,真的感谢上天安排,让我能更加确信自己要的是什么,以后该怎么生活。


爸爸被诊断为肺癌


我每年都会带父母去做体检,因为去年疫情全面放开后,担心被传染。寻思稳定稳定再去。


后爸爸因为走路崴脚在家养了三个月,就一直没去上体检。


有一天下班爸爸跟我说,心脏不得劲,心总疼,而且还上不来气,我说那明天去医院看看吧。


由于我工作项目忙还总加班,就让姐姐陪爸爸一起去检查了。


由于比较有名的医院,都没号了,姐姐就去了某国际医院(私立医院),做了全面检查,通过各种CT的检查结果汇总,医院给出的答案是小细胞癌晚期,建议转院。


当我姐哭着给我打电话告诉我这个消息时,我整个人都楞了几秒。


我跟姐姐说,你先别慌,我们再去其他医院看看,小医院技术不行,也许查错了呢!


你就跟我爸说,可能是上火引起的,CT上查出来有个黑点具体什么没看出来,建议我们去大医院看,那设备好一些,能看出来


接下来,我和姐姐去约各大医院的专家号,某军区总院、某四院、某二院、某市中医院、某省总医院、某肿瘤医院等等。


最后,以上所有医院的结果,给出的答案都是小细胞癌晚期


据我同学给介绍的医生说,我爸这样的情况,最多可能半年或1年,即使是化疗也意义不大,当我和姐姐知道这个消息的时候,一时我也接受不了这个消息,看到姐姐伤心痛哭的样子,我心里也难受极了.....


我强忍着跟姐姐说,咱们再看看,肯定可以治疗。


爸爸得知自己肺癌


对我们而言,怕爸爸知道自己肺癌,会因为舍不得钱而轻生的想法,不配合治疗,所以刚开始在没完全确诊之前,就一直瞒着他。


后来,随着去的医院越来越多,爸爸也逐渐开始起了疑心!


直到我们去某四院,做完肺活检,等结果。并告诉爸爸一周后才能出来(那时候我都佩服我自己撒谎的本事!),其实结果早就出了,只是 我和姐姐不死心,想拿着结果去其他医院再看看,总觉得是医院给看错了。


爸爸也是一直在关注着检查的结果,每天都会问结果到底什么时候能出来!


本来和妈妈、姐姐一直打算瞒着爸爸,让他开心的过完后面的时间。


当然,肯定这肯定是瞒不住的,只是能瞒着一天算一天。


后来,经商量后决定,还是跟爸爸说,觉得他也有知道的权利,而且我们相信爸爸可以接受,并且会积极配合治疗。


当爸爸知道自己得了肺癌后,刚开始那几天,每天都在那发呆,一句话也不说。


于是,我们就决定每天家里必须有个人在家,怕爸爸有轻生的想法,但怕他待在家比较闷,我们决定就带爸爸去旅游。


当然这期间,我们一直没有放弃,又通过关系找到某肿瘤医院主任医生,抱着试试看的态度就去了,听他说完,我们觉得还算靠谱,于是,我们决定就在这家医院治疗了。


随着时间的流逝,爸爸也开始慢慢接受了自己肺癌的事实,而且也选择积极配合治疗,最后也去了医院,这真的让我很开心。


千万不要去化疗


刚开始化疗的时候,我们都是早上7点多就到医院,晚上挂完点滴,到晚上9点-10点才能到家,第一次化疗大约5天左右。


和每个化疗患者一样,刚开始,爸爸也是开始掉头发、厌食。


爸爸最爱吃猪蹄,我每天都会买猪蹄,都后来干脆都不吃,说是没胃口,再到后面脸上也逐渐出现一些症状,有点发黑。


那一刻,我真的感觉化疗就和慢性自杀一样,看着一天天日渐消瘦的爸爸,我的心里真的很不是滋味。


就这样顺利的完成第一阶段,22天后,我们又开始进行了第二次化疗。


化疗的第一天开始,爸爸就开始感觉不舒服,说心脏不得劲,后来医生说有个药不给用了,再看看。


然后,到了第二天,爸爸又开始血压不稳定、心慌,开始不怎么吃饭了,也不怎么说话了,把我吓坏了,脸色也不好。


我一也没睡,就这样守着看到了第三天,抢矿更不好,爸爸开始吐,恶心。


后来和医生说我们要强制出院,不打了,化疗的反应真的太大了,医生也同意我们出院了。


回家后,过了两天稍微好些,第三天,爸爸开始高烧不退、拉肚子不止,脸色苍白,后来我就带爸爸去急诊,输液后好了一些,到急诊那医生,听医生说爸爸是化疗后引起的,没有血小板和白细胞了,建议我们回原医院好些。


然后,我们又去商量主治医师,跟她说了下我爸爸现在的状况,只想恢复正常,表示先不化疗了,并询问能再次接收我们住院治疗,医生最后同意了。


住院后,开始做各种检查,住院当天下午稍微稳定了一些,然后连续两天又开始连续高烧、血压不稳,查完指标说还是是血小板太低,白细胞是0,几乎没有免疫力,需要补


这时,通过食补根本来不及,主要因为爸爸基本不怎么吃东西了,而且高烧起来,抽起来吓人,筷子都咬折了,真太吓人了,我当时真的希望我要是能替爸爸受这个罪该多好,当时真的强忍着眼泪,心里老难受了 !


最后,在我们的一再坚持下,请到了某二院的专家来帮忙会诊,老专家先让爸爸做了几项检查,并给换了药输液,大约也就一周左右,爸爸就好了,出院前,也去查了CT,发现肺部的肿瘤竟然没了!


医生说建议,过一阵再来接着化疗,后面再放疗会更好。其实我们也明白,因为小细胞癌的扩散速度很快,所以建议多观察治疗。


但是我们都坚持不会再去化疗了!


求医之路


爸爸出院回家后,开始进行食补,大约一周后,爸爸能正常走了,而且起色也好了许多。


我们开始四处求医,开启寻医之路,也是去了好多地方吧!


这期间遇到的,有一些老中医有个习惯,说是凡是化疗过的都不给看, 这让我表示很苦恼而且不理解。


再后来,爸爸的一位病友给推荐了一个老中医,我们还是照常打个电话过去,说明了情况,查看是否能治疗,结果开心的是能治疗。


刚开始,我们也是不相信的,抱着试试看的态度,我们去看了下,医生给开了半个月的药,结果爸爸喝中药俩礼拜后,就感觉明显走路有劲了,比以前强很多。


直到现在还在喝中药,之前化疗的副作用慢慢都好了,比如厌食、牙齿松动,吃甜的东西牙疼,头发也长出来了,而且每天还和之前一样,早晚去散步,连之前楼下的老太太,都说完全看不出来像生病一样,真的是好开心,也算是好事多磨吧,也可以说是遇到贵人了。


我失业了


2022年开始年底就开始裁员,公司也是组织架构调整,人员变动也比较频繁。


我当时也是负责性能测试、自动化测试这两块,并行着三个项目,也是真的加班加点的干。


因为爸爸生病,跑医院检查,我总请假,赶上公司组织架构调整。检查、陪护一个人根本忙不过来,妈妈年龄大了不太方便,所以我辞职了,当然也失业了,"成功"地走进2023失业大军中。


好处就是,我终于可以全身心的去忙家里的事了。


找工作


出院后的一个月,爸爸这边病情算是稳定了,我便开始了积极找工作。


因为好久没找工作了,首先,我用了大约5天的时间去搞简历,搞完简历便开始找工作。


通过找工作才发现,真TM卷呀,都是统招本科起步,更过分的是有的公司还要求必须是计算机专业!


我一看我自己,大专自考本科,完全没竞争力,BOSS上、智联、脉脉上、拉钩、内推,基本都是被学历卡掉了!


而且,那会特别焦虑,除了这行还能干嘛,我还会做啥,离了这行,是不是完了?


那会女友找到了工作,而且收入也不错,我待业,顶着巨大压力,我还是继续努力的寻找的工作。


还好感谢上天眷顾图片,还有几个面试,让我有的选择,最终我选择了一家成功上岸工作了。


分享两个面试题:


StringBuilderStringBuffer的区别?(我只用过他们拼接字符,结果凉了)


你性能测试中最大的QPS是多少?(这题我跟面试官,开始了杠精模式,我真没法回答)


我失恋了


每次写到这块,就感觉我像个怨妇一样呢,哈哈,真的不爱写。


我和女友是相亲认识的,3月相识直到今年10月长假彻底分手,分手是在吃完定亲饭开始黄的,听起来是不是很奇怪。


那会我已经上班一周,也是临近十一的一周,爸爸那会身体恢复的已经很好了,开始正常上班了。


因为在这之前她总跟我说,我们结婚把之类的话,我刚开始没往心里去,但是也就在那周,我接了话拆,并也耐心的跟他说了个初步沟通,寻问过彩礼和三金等等,当时,我看她也很开心的。


然后,回家我就跟家长说了这事,我爸说,那也行我这边身体现在也恢复的挺好,那就吃个定亲饭,把你俩的事定下来。


之后,爸爸给介绍人打了电话给介绍人,介绍人询问她家什么时候有时间,然后他妈妈给了个时间,当天晚上下班,女孩就跟我生气说,没正式通知她(也许差个求婚仪式吗?我可能直男了吧),完事我俩吵了一架。


然后呢,她告诉他妈说不行,然后又跟他妈妈吵架了,那意思说没告诉她 怎么就定了呢!


后来,我妥协了,我说那这样吧,时间你来定什么时候都行,不行怎么就先处着,你感觉行再订婚,你别有压力呢。


结果,也不到怎么她妈妈又给介绍人打电话定了个时间,完事又不行,来回会改了三次,我爸妈当时也说,要是人家不愿意就算了,再等等吧。


完事我还是跟女孩说,等你们定好,提前一天告诉我就行,你先和家长商量好就行的。


就因为吃定亲饭时间,他们家来回变定不下来,搞的我父母心情很复杂,但也是为了自己的儿子幸福,就等等了。 好在最后,定下来最后的时间了。


在吃定亲饭那天,给了定金,我感觉啥也没谈,介绍人问了女孩家有没有陪嫁,男方这边有房子,可以出装修等等,其他的我忘记了,当时他父母不吱声也不表态,我就感觉很奇怪吧。


当然,我作为晚辈在桌上没法发表意见。


于是,第二天,我们去自驾游回来的时候,我就说吃完定亲饭,介绍人问你父母你家有没有陪嫁,你爸妈没表态,我爸爸得了肺癌手头可能也不宽裕,我寻思问下你家有没有陪嫁,可以出点家电或者装修?因为都我家的话,可能会负债。


女孩问我是我的意思,还是我爸妈的意思,我说昨晚我们到家都很晚了,我晚上到家父母都睡觉了,一早我俩5点多出门去旅游,基本没说上话,我俩就是商量下。


女孩没说话,直接下车,并说 我现在就去找我爸妈,我去问问(当时生气的哭了,或许我不该问?


之后,我回家和我爸妈说,我又把对象惹生气了,并说了事情的缘由,父母本着保全大局的原则,让我先去道歉。


我先去找女孩的妈妈,结果,到那后,他妈妈基本都不让我说话,大概那意思,就是她们不是卖女儿,娶媳妇还想让她们拿钱,能娶得起就娶,娶不起就不娶,要是有饥荒,女儿肯定不嫁。


我看这样,我就又去找女孩各种道歉努力,女孩最后说不处了,直接退定金,走介绍人流程


之后,我回家跟父母学了一下,父母还是为了我,为了他们儿子,直接去找到女孩的父母,带着我去赔礼道歉。


到了后,我虽然离的很远都能听到,他妈妈跟我说的那些话,又跟我妈妈说了几遍,好在他爸爸是明事理的人,说也会帮着劝劝,但是他爸爸说了不算,尴尬。


随后,妈妈和我又去找到女孩去赔礼道歉,妈妈说了很多话,大概意思是,给阿姨个面子,我们家小孩不太会说话,别忘心里去


那种乞求被原谅的感觉,我那一刻,眼泪唰就出来了,心里很不是滋味,完事我偷偷的快速擦掉眼泪。妈妈临出门时说,大概意思是,你俩好好聊聊,好好相处,我家小孩嘴笨,你多担待点!结果我说啥,她也不说话,我说,那你先忙。


那一刻的冷漠,让我感觉到这份感情真的太脆弱、卑微了。


接着到了第二天,我早上买完早点给他送去,从8点多一直说到11点多,她的态度依旧是那么坚决,说不想处了,我当时回家后,我就在那想,处了半年多,一点感情也没有吗?说分就分,真的就能放下?


虽然每次吵架她都说分手,我都去哄,去赔礼道歉,也习惯了,但是这次不一样,我感觉好像是真的无法挽回。


但是我真的觉得,我和女孩都相处了快7个月了,有啥不能谈呢,至于上升到家长层面吗?


回家后,我跟父母说了下,后来大家讨论后,得出的结论:


可能是因为得知我爸爸是肺癌,每个月都要喝中药治病,觉得可能会是个累赘,再一个就是为了给爸爸治病花了不少钱,可能我家没多少钱了,可能怕以后的日子不好过,受拖累!


接着又过了一天,介绍人把我叫去,聊了一下,问我还有多少钱,还有一些别的,然后,又说跟她妈妈约定了好了,具体啥我忘记了,和我聊完,又给女孩妈妈打了个电话。


又过了一个多小时,我接到女孩的电话,她没说话,说打错了,我寻思给了台阶我就下了,我说你在哪,等你到家,你告诉我我去找你吧。


当天晚上,我收到女孩微信,说让我一起去介绍人那再聊一下,他们聊了啥,我也不到,我被支开了,他们聊了很久吧,聊的啥我也不知道,完事我送她回家。


第二天,早上我去找他,我打算跟她好好聊一下,我知道她肯定是想和好,我就问了他几个问题:



  1. 我不能保证一直都有工作,或者说我以后可能会有待业期,或者赚的不多,或者说不能给你更好的物质生活,但是我肯定会努力赚钱,不会摆烂(大概意思),你愿意跟我结婚吗?(没想过,而且现在不想结婚,想两年后吧,或者明年?)

  2. 比如我上班要早起你上班比较晚,偶尔愿意帮我早起做个早饭吗?(不想起来)

  3. 以后相处模式,能跟我先沟通下,再到父母层面吗?(不说话)

  4. 我爸爸要喝中药,我不能不管我爸爸(她的意思是我把父母看的比她重)


其他我忘记了,总之基本不说话,虽然来抱我,表示和好,但是那一刻我发现,感觉真的变了!


然后跟我说,她会跟她妈妈说,我们和好了,但是不让我和我爸妈说,让我很吃惊!


难道我家长是因为好说话?就该不被重视?


“和好后”的两天,和之前一样,每天正常见面,都是我在找话题,她还是不说话,去她家,她基本不说话,在那工作,不说话,我就在那刷手机看视频。


然后又过去两天,我们出去散步,她还是不说话,甚至不牵我手。


想到带我去道歉的妈妈她的冷漠态度, 突然,我好像想明白了。


又过了一天,晚上下班到家,我提出了分手,删除了她的微信,彻底分手了


我知道,如果我在跟她谈结婚,还是会遇到这样的问题,而且她也说过,



不想结婚,想再玩两年!



而我真玩不起了,从始至终,每次吵架,我都去道歉挽留,因为害怕失去,感觉在感情里,我被养成了讨好型的人格。


跳出来看,换位思考,如果我是女孩家长,可能也会让她嫁的更好,或者未来会有更好的物质生活,感情里,没有谁对谁错,只有愿不愿意吧,所以真的就是和平分手了!


但无论怎样,我也不会动父母养老的钱,没钱我就不娶!


可能有同学会说了,那你是娶不起吧。


没错,要是结个婚,要贷款梭哈的话,还是算了吧。


你有没有想过把父母老本都拿来,完事还让他们去借钱,他们都那么大岁数了,没有劳动力了,怎么还?


值得一说的是,她家并不是家境不好,只能说和我家差不多吧,后面我了解到,真的不是他们家没钱,只是他姐姐陪嫁还有个房子,到我这就一个人,让我们自己白手起家?还是瞧不起我?亦或许就不想跟我结婚?


写在最后


刚失恋那一个月,真的我天天失眠睡不着,而且担心光棍一辈子,去求助很多朋友帮忙介绍对象。


而现在呢,失眠是因为搞不到钱。真的才发现搞钱,才是这个世界上最有意义的事了吧。


图片


作者:软件测试君
来源:juejin.cn/post/7300099522202009637
收起阅读 »

老黄深夜炸场,世界最强AI芯片H200震撼发布!性能飙升90%,Llama 2推理速度翻倍,大批超算中心来袭

【新智元导读】 刚刚,英伟达发布了目前世界最强的AI芯片H200,性能较H100提升了60%到90%,还能和H100兼容。算力荒下,大科技公司们又要开始疯狂囤货了。 英伟达的节奏,越来越可怕了。 就在刚刚,老黄又一次在深夜炸场——发布目前世界最强的AI芯片H2...
继续阅读 »
【新智元导读】 刚刚,英伟达发布了目前世界最强的AI芯片H200,性能较H100提升了60%到90%,还能和H100兼容。算力荒下,大科技公司们又要开始疯狂囤货了。

英伟达的节奏,越来越可怕了。


就在刚刚,老黄又一次在深夜炸场——发布目前世界最强的AI芯片H200!


较前任霸主H100,H200的性能直接提升了60%到90%。


不仅如此,这两款芯片还是互相兼容的。这意味着,使用H100训练/推理模型的企业,可以无缝更换成最新的H200。


图片


全世界的AI公司都陷入算力荒,英伟达的GPU已经千金难求。英伟达此前也表示,两年一发布的架构节奏将转变为一年一发布。


就在英伟达宣布这一消息之际,AI公司们正为寻找更多H100而焦头烂额。


英伟达的高端芯片价值连城,已经成为贷款的抵押品。


图片


谁拥有H100,是硅谷最引人注目的顶级八卦


至于H200系统,英伟达表示预计将于明年二季度上市。


同在明年,英伟达还会发布基于Blackwell架构的B100,并计划在2024年将H100的产量增加两倍,目标是生产200多万块H100。


而在发布会上,英伟达甚至全程没有提任何竞争对手,只是不断强调「英伟达的AI超级计算平台,能够更快地解决世界上一些最重要的挑战。」


随着生成式AI的大爆炸,需求只会更大,而且,这还没算上H200呢。赢麻了,老黄真的赢麻了!


图片


141GB超大显存,性能直接翻倍!


H200,将为全球领先的AI计算平台增添动力。


它基于Hopper架构,配备英伟达H200 Tensor Core GPU和先进的显存,因此可以为生成式AI和高性能计算工作负载处理海量数据。


英伟达H200是首款采用HBM3e的GPU,拥有高达141GB的显存。


图片


与A100相比,H200的容量几乎翻了一番,带宽也增加了2.4倍。与H100相比,H200的带宽则从3.35TB/s增加到了4.8TB/s。


英伟达大规模与高性能计算副总裁Ian Buck表示——



要利用生成式人工智能和高性能计算应用创造智能,必须使用大型、快速的GPU显存,来高速高效地处理海量数据。借助H200,业界领先的端到端人工智能超算平台的速度会变得更快,一些世界上最重要的挑战,都可以被解决。



图片


Llama 2推理速度提升近100%


跟前代架构相比,Hopper架构已经实现了前所未有的性能飞跃,而H100持续的升级,和TensorRT-LLM强大的开源库,都在不断提高性能标准。


H200的发布,让性能飞跃又升了一级,直接让Llama2 70B模型的推理速度比H100提高近一倍!


H200基于与H100相同的Hopper架构。这就意味着,除了新的显存功能外,H200还具有与H100相同的功能,例如Transformer Engine,它可以加速基于Transformer架构的LLM和其他深度学习模型。


图片


HGX H200采用英伟达NVLink和NVSwitch高速互连技术,8路HGX H200可提供超过32 Petaflops的FP8深度学习计算能力和1.1TB的超高显存带宽。


当用H200代替H100,与英伟达Grace CPU搭配使用时,就组成了性能更加强劲的GH200 Grace Hopper超级芯片——专为大型HPC和AI应用而设计的计算模块。


图片


下面我们就来具体看看,相较于H100,H200的性能提升到底体现在哪些地方。


首先,H200的性能提升最主要体现在大模型的推理性能表现上。


如上所说,在处理Llama 2等大语言模型时,H200的推理速度比H100提高了接近1倍。


图片


因为计算核心更新幅度不大,如果以训练175B大小的GPT-3为例,性能提升大概在10%左右。


图片


显存带宽对于高性能计算(HPC)应用程序至关重要,因为它可以实现更快的数据传输,减少复杂任务的处理瓶颈。


对于模拟、科学研究和人工智能等显存密集型HPC应用,H200更高的显存带宽可确保高效地访问和操作数据,与CPU相比,获得结果的时间最多可加快110倍。


相较于H100,H200在处理高性能计算的应用程序上也有20%以上的提升。


图片


而对于用户来说非常重要的推理能耗,H200相比H100直接腰斩。这样,H200能大幅降低用户的使用成本,继续让用户「买的越多,省的越多」!


图片


上个月,外媒SemiAnalysis曾曝出一份英伟达未来几年的硬件路线图,包括万众瞩目的H200、B100和「X100」GPU。


图片


而英伟达官方,也公布了官方的产品路线图,将使用同一构架设计三款芯片,在明年和后年会继续推出B100和X100。


图片


B100,性能已经望不到头了


这次,英伟达更是在官方公告中宣布了全新的H200和B100,将过去数据中心芯片两年一更新的速率直接翻倍。


以推理1750亿参数的GPT-3为例,今年刚发布的H100是前代A100性能的11倍,明年即将上市的H200相对于H100则有超过60%的提升,而再之后的B100,性能更是望不到头。


图片


至此,H100也成为了目前在位最短的「旗舰级」GPU。如果说H100现在就是科技行业的「黄金」,那么英伟达又成功制造了「铂金」和「钻石」。


图片


H200加持,新一代AI超算中心大批来袭


云服务方面,除了英伟达自己投资的CoreWeave、Lambda和Vultr之外,亚马逊云科技、谷歌云、微软Azure和甲骨文云基础设施,都将成为首批部署基于H200实例的供应商。


图片


此外,在新的H200加持之下,GH200超级芯片也将为全球各地的超级计算中心提供总计约200 Exaflops的AI算力,用以推动科学创新。


图片


在SC23大会上,多家顶级超算中心纷纷宣布,即将使用GH200系统构建自己的超级计算机。


德国尤里希超级计算中心将在超算JUPITER中使用GH200超级芯片。


这台超级计算机将成为欧洲第一台超大规模超级计算机,是欧洲高性能计算联合项目(EuroHPC Joint Undertaking)的一部分。


图片


Jupiter超级计算机基于Eviden的BullSequana XH3000,采用全液冷架构。


它总共拥有24000个英伟达GH200 Grace Hopper超级芯片,通过Quantum-2 Infiniband互联。


每个Grace CPU包含288个Neoverse内核, Jupiter的CPU就有近700万个ARM核心。


它能提供93 Exaflops的低精度AI算力和1 Exaflop的高精度(FP64)算力。这台超级计算机预计将于2024年安装完毕。


图片


由筑波大学和东京大学共同成立的日本先进高性能计算联合中心,将在下一代超级计算机中采用英伟达GH200 Grace Hopper超级芯片构建。


作为世界最大超算中心之一的德克萨斯高级计算中心,也将采用英伟达的GH200构建超级计算机Vista。


图片


伊利诺伊大学香槟分校的美国国家超级计算应用中心,将利用英伟达GH200超级芯片来构建他们的超算DeltaAI,把AI计算能力提高两倍。


此外,布里斯托大学将在英国政府的资助下,负责建造英国最强大的超级计算机Isambard-AI——将配备5000多颗英伟达GH200超级芯片,提供21 Exaflops的AI计算能力。


图片


英伟达、AMD、英特尔:三巨头决战AI芯片


GPU竞赛,也进入了白热化。


图片


面对H200,而老对手AMD的计划是,利用即将推出的大杀器——Instinct MI300X来提升显存性能。


MI300X将配备192GB的HBM3和5.2TB/s的显存带宽,这将使其在容量和带宽上远超H200。


而英特尔也摩拳擦掌,计划提升Gaudi AI芯片的HBM容量,并表示明年推出的第三代Gaudi AI芯片将从上一代的 96GB HBM2e增加到144GB。


图片


英特尔Max系列目前的HBM2容量最高为128GB,英特尔计划在未来几代产品中,还要增加Max系列芯片的容量。


H200价格未知


所以,H200卖多少钱?英伟达暂时还未公布。要知道,一块H100的售价,在25000美元到40000美元之间。训练AI模型,至少需要数千块。此前,AI社区曾广为流传这张图片《我们需要多少个GPU》。


图片


GPT-4大约是在10000-25000块A100上训练的;Meta需要大约21000块A100;Stability AI用了大概5000块A100;Falcon-40B的训练,用了384块A100。


根据马斯克的说法,GPT-5可能需要30000-50000块H100。摩根士丹利的说法是25000个GPU。


Sam Altman否认了在训练GPT-5,但却提过「OpenAI的GPU严重短缺,使用我们产品的人越少越好」。


图片


我们能知道的是,等到明年第二季度H200上市,届时必将引发新的风暴。


参考资料:nvidianews.nvidia.com/news/nvidia…


作者:新智元
来源:juejin.cn/post/7300860696685363219
收起阅读 »

iOS 判断系统版本

iOS
方案一 double systemVersion = [UIDevice currentDevice].systemVersion.boolValue; if (systemVersion >= 7.0) { // >= iOS 7.0 ...
继续阅读 »

方案一


double systemVersion = [UIDevice currentDevice].systemVersion.boolValue;

if (systemVersion >= 7.0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

if (systemVersion >= 10.0) {
// >= iOS 10.0
} else {
// < iOS 10.0
}

如果只是大致判断是哪个系统版本,上面的方法是可行的,如果具体到某个版本,如 10.0.1,那就会有偏差。我们知道 systemVersion 依旧是10.0。


方案二


NSString *systemVersion = [UIDevice currentDevice].systemVersion;
NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1" options:NSNumericSearch];

if (comparisonResult == NSOrderedAscending) {
// < iOS 10.0.1
} else if (comparisonResult == NSOrderedSame) {
// = iOS 10.0.1
} else if (comparisonResult == NSOrderedDescending) {
// > iOS 10.0.1
}

// 或者

if (comparisonResult != NSOrderedAscending) {
// >= iOS 10.0.1
} else {
// < iOS 10.0.1
}

有篇博客提到这种方法不靠谱。比如系统版本是 10.1.1,而我们提供的版本是 8.2,会返回NSOrderedAscending,即认为 10.1.1 < 8.2 。


其实,用这样的比较方式 NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1"],的确会出现这种情况,因为默认是每个字符逐个比较,即 1(0.1.1) < 8(.2),结果可想而知。但我是用 NSNumericSearch 方式比较的,即数值的比较,不是字符比较,也不需要转化成NSValue(NSNumber) 再去比较。


方案三


if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

// 或者

if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

这些宏定义是 Apple 预先定义好的,如下:


#if TARGET_OS_IPHONE
...
#define NSFoundationVersionNumber_iOS_9_4 1280.25
#define NSFoundationVersionNumber_iOS_9_x_Max 1299
#endif


细心的童靴可能已经发现问题了。Apple 没有提供 iOS 10 以后的宏?,我们要判断iOS10.0以后的版本该怎么做呢?
有篇博客中提到,iOS10.0以后版本号提供了,并且逐次降低了,并提供了依据。


#if TARGET_OS_MAC
#define NSFoundationVersionNumber10_1_1 425.00
#define NSFoundationVersionNumber10_1_2 425.00
#define NSFoundationVersionNumber10_1_3 425.00
#define NSFoundationVersionNumber10_1_4 425.00
...
#endif


我想这位童鞋可能没仔细看, 这两组宏是分别针对iPhone和macOS的,不能混为一谈的。


所以也只能像下面的方式来大致判断iOS 10.0, 但之前的iOS版本是可以准确判断的。


if (NSFoundationVersionNumber > floor(NSFoundationVersionNumber_iOS_9_x_Max)) {
// > iOS 10.0
} else {
// <= iOS 10.0
}

方案四


在iOS8.0中,Apple也提供了NSProcessInfo 这个类来检测版本问题。


@property (readonly) NSOperatingSystemVersion operatingSystemVersion NS_AVAILABLE(10_10, 8_0);
- (BOOL) isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version NS_AVAILABLE(10_10, 8_0);

所以这样检测:


if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 8, .minorVersion = 3, .patchVersion = 0}]) {
// >= iOS 8.3
} else {
// < iOS 8.3
}

用来判断iOS 10.0以上的各个版本也是没有问题的,唯一的缺点就是不能准确版本是哪个版本,当然这种情况很少。如果是这种情况,可以通过字符串的比较判断。


方案五


通过判断某种特定的类有没有被定义,或者类能不能响应哪个特定版本才有的方法。
比如,UIAlertController 是在iOS 8.0才被引进来的一个类,我们这个依据来判断版本


if (NSClassFromString(@"UIAlertController")) {
// >= iOS 8.0
} else {
// < iOS 8.0
}

说到这里,就顺便提一下在编译期间如何进行版本控制,依然用UIAlertController 来说明。


NS_CLASS_AVAILABLE_IOS(8_0) @interface UIAlertController : UIViewController

NS_CLASS_AVAILABLE_IOS(8_0) 这个宏说明,UIAlertController 是在iOS8.0才被引进来的API,那如果我们在iOS7.0上使用,应用程序就会挂掉,那么如何在iOS8.0及以后的版本使用UIAlertController ,而在iOS8.0以前的版本中仍然使用UIAlertView 呢?


这里我们会介绍一下在#import <AvailabilityInternal.h> 中的两个宏定义:


*__IPHONE_OS_VERSION_MIN_REQUIRED


*__IPHONE_OS_VERSION_MAX_ALLOWED


从字面意思就可以直到,__IPHONE_OS_VERSION_MIN_REQUIRED 表示iPhone支持最低的版本系统,__IPHONE_OS_VERSION_MAX_ALLOWED 表示iPhone允许最高的系统版本。


__IPHONE_OS_VERSION_MAX_ALLOWED 的取值来自iOS SDK的版本,比如我现在使用的是Xcode Version 8.2.1(8C1002),SDK版本是iOS 10.2,怎么看Xcode里SDK的iOS版本呢?



进入PROJECT,选择Build Setting,在Architectures中的Base SDK中可以查看当前的iOS SDK版本。



打印这个宏,可以看到它一直输出100200。


__IPHONE_OS_VERSION_MIN_REQUIRED 的取值来自项目TARGETS的Deployment Target,即APP愿意支持的最低版本。如果我们修改它为8.2,打印这个宏,会发现输出80200,默认为10.2。


通常,__IPHONE_OS_VERSION_MAX_ALLOWED 可以代表当前的SDK的版本,用来判断当前版本是否开始支持或具有某些功能。而__IPHONE_OS_VERSION_MIN_REQUIRED 则是当前SDK支持的最低版本,用来判断当前版本是否仍然支持或具有某些功能。


回到UIAlertController 使用的问题,我们就可以使用这些宏,添加版本检测判断,从而使我们的代码更健壮。


 - (void)showAlertView {
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Title" message:@"message" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
[alertView show];
#else
if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_0) {
UIAlertController *alertViewController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *otherAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];

[alertViewController addAction:cancelAction];
[alertViewController addAction:otherAction];

[self presentViewController:alertViewController animated:YES completion:NULL];
}
#endif
}

方案六


iOS 11.0 以后,Apple加入了新的API,以后我们就可以像在Swift中的那样,很方便的判断系统版本了。


if (@available(iOS 11.0, *)) {
// iOS 11.0 及以后的版本
} else {
// iOS 11.0 之前
}

参考链接



作者:蒙哥卡恩就是我
来源:juejin.cn/post/7277111344003399734
收起阅读 »

货拉拉用户 iOS 端灵动岛实践总结

iOS
1. 前言 实时活动是iOS 16.1及以上版本中新增的功能,它允许应用在锁屏界面显示实时数据,能够帮助用户实时查看当前订单的进展,而无需解锁手机。用户在货拉拉APP上下单后,可以将手机放置一旁,开始其他工作。当用户想要查询订单状态时,只需从锁定屏幕或灵动岛...
继续阅读 »

1. 前言


实时活动是iOS 16.1及以上版本中新增的功能,它允许应用在锁屏界面显示实时数据,能够帮助用户实时查看当前订单的进展,而无需解锁手机。用户在货拉拉APP上下单后,可以将手机放置一旁,开始其他工作。当用户想要查询订单状态时,只需从锁定屏幕或灵动岛上轻松操作即可。实时活动的出现不仅省去了用户解锁手机的步骤,更为用户节省了时间和精力。目前货拉拉APP适配“灵动岛”的最新6.7.68版本已正式上线,欢迎大家升级体验。在适配过程中,货拉拉App也踩过很多“坑”,在此汇总为实战经验分享给大家。


2. Live Activity&灵动岛的介绍


Live Activity的实现需要使用Apple的ActivityKit框架。通过使用ActivityKit,开发者可以轻松地创建一个Live Activity,这是一个动态的、实时更新的活动,可以在用户的设备上显示各种信息。此外,ActivityKit还提供了推送通知的功能,开发者可以通过服务器向用户的设备发送更新;这样,即使应用程序没有运行,用户也可以接收到最新的信息。


灵动岛是Live Activity的一种展示形式,灵动岛有三种展示形式:Compact紧凑、Minimal最小化,Expanded扩展。开发时必须实现这三种形式,以确保灵动岛在不同的场景下都能正常展示。



同时还需要实现锁屏下的实时活动UI,设备处于锁屏状态下,也能查看实时更新的内容。以上功能的实现,都是使用WidgetKit和SwiftUI完成开发。


2.1 技术难点及策略


实时活动,主要是APP在后台时,主动更新通知栏和灵动岛的数据,为用户展示最新实时订单状态。如何及时刷新实时活动的数据,是一个重点、难点。


更新方式有3种:



  1. 通过APP内订单状态的变化刷新实时活动和灵动岛。此方法开发量小,但是APP退到后台30s后或者进程杀掉,会停止数据的更新。

  2. 让APP配置支持后台运行模式,通过本地现有的订单状态变化逻辑,在后台发起网络请求,获取订单的数据后刷新实时活动。此方法开发量小,但求主App进程必须存在,进程一旦杀掉就无法更新。

  3. 通过接受远程推送通知来更新实时活动。此方法需要后端配合,此方式比较灵活,无需App进程存在,数据更新及时。也是业界常见的方案。


通过对数据刷新的三种方案进行评估后,选择了用户体验最佳的第三种方式。通过后端发生push,端上接受push数据来更新实时活动。


3. Live Activity&灵动岛的实践


3.1 实现方案流程图


实现流程图:


image.png


3.2 实现代码


创建Live Activities的准备:



  • Xcode需要14.1以上版本

  • 在主工程的 Info.plist 文件中添加一个键值对,key 为 NSSupportsLiveActivities,value 为 YES

  • 使用ActivityKit在Widget Extension 中创建一个Live Activity


需要实现锁屏状态下UI、灵动岛长按展开的UI、灵动岛单个UI、多个实时活动时的minimalUI


import SwiftUI
import WidgetKit

@main
struct TestWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TestAttributes.self) { context in
// 锁屏状态下的UI
} dynamicIsland: { context in
DynamicIsland {
//灵动岛展开后的UI
} compactLeading: {
// 未被展开左边UI
} compactTrailing: {
// 未被展开右边UI
} minimal: {
// 多任务时,右边的一个圆圈区域
}
.keylineTint(.cyan)
}
}
}

灵动岛主要分为StartUpdateEnd三种状态,可由ActivityKit远程推送控制其状态。


开启Live Activity


        let state = TestAttributes.ContentState()
let attri = TestAttributes(value: 100)
do {
let current = try Activity.request(attributes: attri, contentState: state, pushType: .token)
Task {
for await state in current.contentStateUpdates {
//监听state状态
}
}
Task {
for await state in current.activityStateUpdates {
//监听activity状态
}
}
} catch(let error) {
}

更新Live Activity


   Task {
guard let current = Activity<TestAttributes>.activities.first else {
return
}
let state = TestAttributes.ContentState(value: 88)
await current.update(using: state)
}

结束Live Activity


    Task {
for activity in Activity<TestAttributes>.activities {
await activity.end(dismissalPolicy: .immediate)
}
}

4. 使用ActivityKit推送通知


ActivityKit提供了接收推送令牌的功能,我们可以使用这个令牌来通过ActivityKit推送通知从我们的服务器向Apple Push Notification service (APNs)发送更新。


推送更新Live Activity的准备:




  • 在开发者后台配置生成p8证书,替换原来的p12证书




  • 通过pushTokenUpdates获取推送令牌PushToken




  • 向后端注册PushToken




代码展示:


//取得PushToken
for await tokenData in current.pushTokenUpdates {
let mytoken = tokenData.map { String(format: "x", $0) }.joined()
//向后端注册
registerActivityToken(mytoken)
}

4.1 模拟器push验证测试


环境要求:


Xcode >= 14.1 MacOS >= 13.0


准备工作:



  1. 通过pushTokenUpdates获取推送需要的token

  2. 根据开发者TeamID、p8证书本地路径、BuidleID等进行脚本配置


脚本示例:


export TEAM_ID=YOUR_TEAM_ID
export TOKEN_KEY_FILE_NAME=YOUR_AUTHKEY_FILE.p8
export AUTH_KEY_ID=YOUR_AUTHKEY_ID
export DEVICE_TOKEN=YOUR_PUSH_TOKEN
export APNS_HOST_NAME=api.sandbox.push.apple.com

export JWT_ISSUE_TIME=$(date +%s)
export JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
export JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"

curl -v \
--header "apns-topic:YOUR_BUNDLE_ID.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data \
'{"Simulator Target Bundle": "YOUR_BUNDLE_ID",
"aps": {
"timestamp":1689648272,
"dismissal-date":0,
"event": "update",
"sound":"default",
"content-state": {
"title": "等待付款",
"content": "请尽快完成下单"
}
}}'
\
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN

其中:


apns-topic:固定为{BundleId}.push-type.liveactivity


apns-push-type:固定为liveactivity


Simulator Target Bundle:模拟器推送,设置为对应APP的BundleId


timestamp:表示推送通知的发送时间,如果timestamp字段的值与当前时间相差太大,可能会收不到推送。


event:可填入update、end,对应Live Activity的更新与结束。


dismissal-date:当event为end时有效,表示结束后从锁屏上移除Live Activity的时间。如果推送内容不包含"dismissal-date",默认结束后4小时后消失,但内容不会再发生更新。如果期望Live Activity结束后立即从锁屏上移除它,可为"dismissal-date"提供一个过去的日期。


content-state:对应灵动岛的Activity.ContentState;如果push中content-state的字段和Attributes比较:




  • 字段过多,多余的字段可能会被忽略,不会导致解析失败




  • 字段缺少,会在解析push通知时出现问题错误。错误表现为:实时活动会有蒙层,并展示loading菊花UI。




示范:


image.png


image.png


5. 踩坑记录




  • 在模拟器上无法获取到pushToken,无法进行推送模拟?


    检查电脑的系统版本号,需要13.0以上




  • 更新实时活动时,页面显示加载loadingUI,为什么?


    核对push字段和Activity.ContentState的字段是否完全一致,字段少了会解析失败




  • 在16.1系统上,无法展示实时活动,其他更高系统能展示?


    检查Widget里面iOS系统版本号的配置,设置为想要支持的最低版本




  • dismissal-date设置为10分钟后才消失,为什么Dynamic Island灵动岛立即消失了?


    Dynamic Island的显示逻辑可能会更加复杂,如果push的event=end,Dynamic Island灵动岛会立即消失。期望同时消失,可以在指定时间再发end,dismissal-date设置为过去时间,锁屏UI和Dynamic Island灵动岛会同时消失。




  • 推送不希望打扰用户,静默推送,不需要震动和主动弹出,如何设置?


    将"content-available"设置为1,"sound" 设置为: ""




"aps" = {
"content-available" : 1,
"sound" : ""
}



  • 用户系统是深色模式时,如何适配?


    可以使用@Environment(.colorScheme)属性包装器来获取当前设备的颜色模式。会返回一个ColorScheme枚举,它可以是.light.dark。在根据具体的场景进行UI适配




struct ContentView: View {
@Environment(.colorScheme) var colorScheme

var body: some View {
VStack {
if colorScheme == .dark {
Text("深夜模式")
.foregroundColor(.white)
.background(Color.black)
} else {
Text("日间模式")
.foregroundColor(.(.black)
.background(Color.white)
}
}
}
}

5.1 场景限制及建议



  1. 官方文档提示实时活动最多持续8小时,8小时后数据无法刷新,12小时后会强制消失。因此8小时后的数据不准确

  2. 实时活动的卡片上禁止定位以及网络请求,数据需要小于4KB,不能展示特别负责庞大的数据

  3. 同场景多卡片由于样式趋同且折叠,不建议同时创建多卡片。用户多次下单时,建议只处理第一个订单


6. 用户APP上线效果


用户端iOS APP灵动岛上线后的部分场景截图:







7. 总结


灵动岛功能自上线以来,经过我们的数据统计,用户实时活动使用率高达75%以上。这一数据的背后,是灵动岛强大的功能和优秀的用户体验。用户可以在锁屏页直接查看订单状态,无需繁琐的操作步骤,大大提升了用户体验。这种便捷性,使得灵动岛在用户中的接受度较高。


我们的方案不仅可以应用于当前的业务场景,后续还计划扩展到营销活动,定制化通知消息等多种业务场景。这种扩展性,使得灵动岛可以更好地满足不同用户的需求,丰富产品运营策略。


我们希望通过分享开发过程中遇到的问题和解决方案,可以帮助到更多的人。如果你有任何问题或者想法,欢迎在评论区留言。期待我们在技术的道路上再次相遇。


总的来说,灵动岛以其高效、便捷、灵活的特性,赢得了用户的广泛好评。我们将继续努力,为用户提供更优质的服务,为产品的发展注入更多的活力。


作者:货拉拉技术
来源:juejin.cn/post/7300779071390335030
收起阅读 »

iOS如何通过在线状态来监听其他设备登录的状态

前提条件1、完成 3.9.1 或以上版本 SDK 初始化2、了解环信即时通讯 IM API 的 使用限制。3、已联系商务开通在线状态订阅功能实现方法你可以通过调用 subscribe 方法订阅自己的在线状态,从而可以监听到其他设备在登录和离线时的回调,示例代码...
继续阅读 »

前提条件

1、完成 3.9.1 或以上版本 SDK 初始化
2、了解环信即时通讯 IM API 的 使用限制。
3、已联系商务开通在线状态订阅功能

实现方法

你可以通过调用 subscribe 方法订阅自己的在线状态,从而可以监听到其他设备在登录和离线时的回调,示例代码如下:

先在EMConversationsViewController.m文件上加代理

EMPresenceManagerDelegate
[[[EMClient sharedClient] presenceManager] addDelegate:self delegateQueue:nil];

别的设备在发送状态变化的时候代理方法会接收到响应

- (void) presenceStatusDidChanged:(NSArray<EMPresence*>*)presences
{

NSLog(@"presenceStatusDidChanged:%@",presences);
}


红框中的device是发布者的当前在线设备使用的平台,包括iOSAndroidLinuxwindowswebim

status 是当前在线状态,0为离线,1为在线。

通过上述的方式可以在监听到变化时可以让自己的设备做些业务。

相关文档:

收起阅读 »

【Java集合】想成为Java编程高手?先来了解一下List集合的特性和常用方法!

嗨~ 今天的你过得还好吗?生命如同寓言其价值不在于长短而在于内容通过前面文章的介绍,相信大家对Java集合框架有了简单的理解,接下来说说集合中最常使用的一个集合类的父类,List 集合。那么,List到底是什么?它有哪些特性?又该如何使用呢?让我们一...
继续阅读 »


嗨~ 今天的你过得还好吗?

生命如同寓言

其价值不在于长短

而在于内容


通过前面文章的介绍,相信大家对Java集合框架有了简单的理解,接下来说说集合中最常使用的一个集合类的父类,List 集合。那么,List到底是什么?它有哪些特性?又该如何使用呢?让我们一起来揭开List的神秘面纱。

List,顾名思义,就是列表的意思。在Java中,List是一个接口,它继承了Collection接口,表示一个有序的、可重复的元素集合。下面我们从List 接口的概念、特点和常用方法等方面来介绍List。


一、接口介绍


java.util.List 接口,继承自 Collection 接口(可以回看咱们第二篇中的框架体系),List 接口是单列集合的一个重要分支,习惯性地将实现了List 接口的对象称为List集合。


在list 集合中允许出现重复的元素,所有的元素对应一个整数型的序号记载其在容器中的位置进行存储,在程序中可以通过索引来访问集合中的指定元素。另外,List集合还是 有序的,即元素的存入和取出顺序一致。

List 接口的特点:

  • 它是一个元素存取有序的集合。例如,存元素的顺序是3,45,6。那么集合中,元素的存储就是按照3,45,6的顺序完成的)。

  • 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。

  • 可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。


List接口中常用方法:

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:

  • public void add(int index, E element):将指定的元素,添加到该集合中的指定位置上。

  • public E get(int index):返回集合中指定位置的元素。

  • public E remove(int index):移除列表中指定位置的元素, 返回的是被移除的元素。

  • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。


通过代码来体验一下:


二、List集合子类

List接口有很多实现类,如ArrayListLinkedList等,它们各自有着不同的特点和应用场景。下面分别来介绍一下常用的ArrayList 集合和LinkedList集合。


ArrayList 集合

通过 javaApi 帮助文档 ,可以看到 List的实现类其实挺多,在此选择比较常见的 ArrayList 和 LinkedList 简单介绍。


ArrayList 有以下两个特点:

  • 底层的数据结构是一个数组;

  • 这个数组会自动扩容,看起来像一个长度可变的数组。

通过阅读源码的方式,简单分析下这两个特点的实现:



在实例化ArrayList时,调用了对象的无参构造器,在无参构造器中,首先看到变量 elementData 的定义就是一个数组类型,它存储的就是集合中的元素,其次在初始化对象时,把一个长度为0的Object[] 数组,赋值给了 elementData 。这就是刚刚所说的 ArrayList 底层是一个数组


下面再来看自动扩容这个特点又是怎么实现的。


在向集合中添加一个元素之前,会计算集合中数组的长度是否满足,可以通过代码追踪,通过一系列方法的调用,会使用 arrays 工具类的复制方法 (根据文档,介绍复制方法)创建一个新的长度的数组,将添加的元素保存进去,这就是说的数组可变,自动扩容


ArrayList的两个特点就介绍到这里了,大家有兴趣的可以去读读源码,挺有意思。


重点说明:

之前讲过,数组结构的特点是元素增删慢,查找快。由于java.util.ArrayList 集合数据存储的结构是数组结构,所以它的特点也是元素增删慢,但是查询快


由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList 也是最常使用的集合。

而因着这些特点呢,在日常开发中,有些开发人员就非常随意地使用ArrayList完成任何需求,这是不严谨,这种编码方式也是不提倡的。

LinkedList是一个双向链表,那么双向链表是什么样子的呢,我上篇文章说过的结构图:


LinkedList 集合

接着来看看下面这个实现类:java.util.LinkedList 集合数据存储的结构是链表结构。方便元素添加、删除的集合。

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


inkedList 是由链表来说实现的,并且它实现了List接口的所有方法,还增加了一些自己特有的方法。


api 文档上提到 LinkedList 所有的操作都是按照双重链接列表来执行,那就说明 LinkedList 的底层数据结构的实现是 一个双向链表。


那么之前介绍过双向链表的特点,所以LinkedList的特点就是:元素添加,删除速度快,而查询速度慢。


常用方法

LinkedList 作为 List的实现类,List中的方法LinkedList都是可以使用,所以这些方法就不做详细介绍;而特别练习一下 linkedList 提供的特有方法,因为在实际开发中对一个集合元素的添加与删除也经常涉及到首尾操作。



下面看下演示代码:



三、总结


虽然List功能强大,但我们也不能滥用。在使用时,我们需要注意以下几点:

  • 尽量避免频繁的插入和删除操作,因为这会影响List的性能。在这种情况下,我们可以考虑使用LinkedList。

  • List的大小是有限的,当元素超过List的最大容量时,会抛出OutOfMemoryError异常。因此,我们需要合理地设置List的初始容量和最大容量。


总的来说,Java单列集合List是一个非常强大的工具,它可以帮助我们解决很多编程问题。只要我们能够正确地使用它,就能够在编程的世界中找到无尽的乐趣。



收起阅读 »

一文带你如何优雅地封装小程序蓝牙模块

web
一. 前言。 蓝牙功能在我们日常软件中的使用率还是蛮高的----譬如各类共享单车/电单车。正因此,我们开发中接触蓝牙功能也是日渐增长。对于很多从未开发过蓝牙功能的童鞋来说,当PM小姐姐扔过来一个蓝牙协议要你接入时,简直一头雾水(我是谁?我在哪?)。只能一翻度娘...
继续阅读 »

一. 前言。


蓝牙功能在我们日常软件中的使用率还是蛮高的----譬如各类共享单车/电单车。正因此,我们开发中接触蓝牙功能也是日渐增长。对于很多从未开发过蓝牙功能的童鞋来说,当PM小姐姐扔过来一个蓝牙协议要你接入时,简直一头雾水(我是谁?我在哪?)。只能一翻度娘和AI,可是网上文章大多水准参差不齐,技术五花八门,没法真正地让你从无到有掌握蓝牙功能/协议对接。


二. 说明。


本文就基于uni-app框架结合微信和支付宝小程序为例,来讲述蓝牙功能在各类型小程序中的整体开发流程和如何“优雅”高效的封装蓝牙功能模块。本文使用到的主要技术栈和环境有:



  • uni-app

  • JavaScript

  • AES加解密

  • 微信小程序

  • 支付宝小程序


三. 蓝牙流程图。


正所谓“知己知彼,百战不殆”,所以在讲述蓝牙模块如何在小程序中开发和封装之前,我们先要了解蓝牙功能模块是如何在小程序中“走向”的,各API是如何交互通讯的。为了让大家看得清楚,学的明白----这里简明扼要地梳理了一份蓝牙核心API流程图(去除了非必要的逻辑走向,只展示了实际开发中最重要的步骤和交互)。



  • uni-app: 蓝牙API

  • 微信小程序:蓝牙API

  • 支付宝小程序:蓝牙API

  • 核心API流程图(注:每家厂商的小程序API大同小异,uni-app的基本通用,具体明细详见各厂商开发文档):


小程序蓝牙流程.png


四. 蓝牙协议。


了解完开发所需的API后,就需要根据实际开发场景中所对接的硬件和其厂家提供的蓝牙对接协议来结合上述的API来编写代码了。每家厂商的蓝牙协议是不一样的,不过“万变不离其宗”。只要知道其中的规则,真正看懂一家,那换其他家的也是可以看懂的。本文以下述协议(蓝牙寻车+蓝牙开锁)为例解释下。


1. 寻车:



  • 协议内容:


image.png



  • 解读:


根据上述图文的描述,我们可以知道想要开启蓝牙锁,那么必须先通过寻车蓝牙指令(7B5B01610060 或 7B5B01610160)写入,然后根据蓝牙响应的信息功能体和错误码判断响应是否正确,如正确,那么就拿到此时的随机数,后根据协议规定对该随机数做相应的处理,最后将处理后得到的结果用于组装开锁的蓝牙写入指令。



  • 案例代码:


image.png
image.png


2. 开锁:



  • 协议内容:


image.png



  • 解读:


根据上述图文的描述,我们可以知道开锁的写入指令是需要自己组装的,组装规则为:7B5B(数据头) 1B(信息体长度) 62(信息功能) 00(秘钥索引)018106053735(补1位0的电话号码)4B大端的时间戳 寻车拿到的随机码补8位0后经AES加密组合得到的16B数据 00(校验码);所以开锁写入的数据就是这种(案例:7B5B1B6200018106053735XXXXXXXXXXXXXXXXXXXX)。响应的话,也是根据信息功能体和错误码来判断开锁失败(9201)还是成功(9200)。



  • 案例代码:


image.png


五.代码编写。


这里为了提高蓝牙模块的代码耦合度,我们会把业务层和蓝牙模块层分离出来----也就是会把蓝牙整体流程交互封装成一个蓝牙模块js,然后根据业务形态,在各个业务层面上通过传参的形式来区分每个组件的蓝牙功能。


1. 业务层:



  • 核心代码:


//引入封装好的蓝牙功能JS模块核心方法函数
import { operateBluetoothYws } from '@/utils/bluetoothYws.js';

//调用蓝牙功能
blueTooth() {
//初始化蓝牙模块,所有的蓝牙API都需要在此步成功后才能调用
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功res', res);
let mac = 'FF8956DEDA29';
let key = 'oYQMt8LFavXZR6sB';
operateBluetoothYws('open', mac, key, flag => {
if (flag) {
console.log('flag存在回调函数--蓝牙成功,可以执行后续步骤了', flag);
} else {
console.log('flag不存在回调函数--蓝牙成功,可以执行后续步骤了', flag);
}
})
},
fail(err) {
console.log('初始化蓝牙失败err', err);
}
})
},


  • 解读:


这里是我们具体业务层需要的写法,一开始就是引入我们封装好的蓝牙JS模块核心方法函数(operateBluetoothYws),然后启用uni.openBluetoothAdapter这个蓝牙功能启动前提,成功后在其success内执行operateBluetoothYws方法,此时的参数根据实际开发业务和相对应的蓝牙协议而定(这里以指令参数、设备编号和AES加密秘钥为例),实际中每个mac和key是数据库一一匹配的,我们按后端童鞋提供的接口获取即可(这里为了直观直接写死)。


2. 蓝牙模块层:



  • 核心代码:


let CryptoJS = require('./crypto-js.min.js'); //引入AES加密
let callBack = null; //回调函数,用于与业务层交互
let curOrder; //指令(开锁还是关锁后取锁的状态)
let curMac; //当前扫码的车辆编码对应的设备mac
let curKey; //当前扫码的车辆编码对应的秘钥secret(用于AES加密)
let curDeviceId; //当前扫码的车辆编码对应的设备的 id
let curServiceId; //蓝牙服务 uuid,需要使用 getBLEDeviceServices 获取
let curCharacteristicRead; //当前设备读的uuid值
let curCharacteristicWrite; //当前设备写的uuid值


//蓝牙调用核心方法(order: 指令;mac:车辆编码;key:秘钥secret;cb:回调)
function operateBluetoothYws(order,mac, key, cb) {
curOrder = order;
curMac = mac;
curKey = key;
callBack = cb
searchBluetooth();
}

//第一步(uni.startBluetoothDevicesDiscovery(OBJECT),开始搜寻附近的蓝牙外围设备。)
function searchBluetooth() {
uni.startBluetoothDevicesDiscovery({
services: ['00000001-0000-1000-8000-00805F9B34FB', '00000002-0000-1000-8000-00805F9B34FB'],
success(res) {
console.log('第一步蓝牙startBluetoothDevicesDiscovery搜索成功res', res)
watchBluetoothFound();
},
fail(err) {
console.log('第一步蓝牙startBluetoothDevicesDiscovery搜索失败err', err)
callBack && callBack(false)
}
})
}

//第二步(uni.onBluetoothDeviceFound(CALLBACK),监听寻找到新设备的事件。)
function watchBluetoothFound() {
uni.onBluetoothDeviceFound(function(res) {
curDeviceId = res.devices.filter(i => i.localName.includes(curMac))[0].deviceId;
stopSearchBluetooth()
connectBluetooth()
})
}

//第三步(uni.createBLEConnection(OBJECT),连接低功耗蓝牙设备。)
function connectBluetooth() {
if (curDeviceId.length > 0) {
// #ifdef MP-WEIXIN
uni.createBLEConnection({
deviceId: curDeviceId,
timeout: 5000,
success: (res) => {
console.log('第三步通过deviceId连接蓝牙设备成功res', res);
getBluetoothServers()
},
fail: (err) => {
console.log('第三步通过deviceId连接蓝牙设备失败err', err);
callBack && callBack(false)
}
});
// #endif
// #ifdef MP-ALIPAY
my.connectBLEDevice({
deviceId: curDeviceId,
timeout: 5000,
success: (res) => {
console.log('第三步通过deviceId连接蓝牙设备成功res', res);
getBluetoothServers()
},
fail: (err) => {
console.log('第三步通过deviceId连接蓝牙设备失败err', err);
callBack && callBack(false)
}
});
// #endif
}
}

//第四步(uni.stopBluetoothDevicesDiscovery(OBJECT),停止搜寻附近的蓝牙外围设备。)
function stopSearchBluetooth() {
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log('第四步停止搜寻附近的蓝牙外围设备成功res', res);
},
fail: (err) => {
console.log('第四步停止搜寻附近的蓝牙外围设备失败err', err);
}
})
}

//第五步(uni.getBLEDeviceServices(OBJECT),获取蓝牙设备所有服务(service)。)
function getBluetoothServers() {
uni.getBLEDeviceServices({
deviceId: curDeviceId,
success(res) {
console.log('第五步获取蓝牙设备所有服务成功res', res);
//这里取res.services中的哪个,这是硬件产商配置好的,不同产商不同,具体看对接协议
if (res.services && res.services.length > 1) {
curServiceId = res.services[1].uuid
getBluetoothCharacteristics()
}
},
fail(err) {
console.log('第五步获取蓝牙设备所有服务失败err', err);
callBack && callBack(false)
}
})
}

//第六步(uni.getBLEDeviceCharacteristics(OBJECT),获取蓝牙设备某个服务中所有特征值(characteristic)。)
function getBluetoothCharacteristics() {
// #ifdef MP-WEIXIN
uni.getBLEDeviceCharacteristics({
deviceId: curDeviceId,
serviceId: curServiceId,
success: (res) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值成功res', res);
curCharacteristicWrite = res.characteristics.filter(item => item && item.uuid.includes('0002'))[
0].uuid
curCharacteristicRead = res.characteristics.filter(item => item && item.uuid.includes('0003'))[
0].uuid
notifyBluetoothCharacteristicValueChange()
},
fail: (err) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值失败err', err);
callBack && callBack(false)
}
});
// #endif
// #ifdef MP-ALIPAY
my.getBLEDeviceCharacteristics({
deviceId: curDeviceId,
serviceId: curServiceId,
success: (res) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值成功res', res);
curCharacteristicWrite = res.characteristics.filter(item => item && item.characteristicId.includes('0002'))[
0].characteristicId
curCharacteristicRead = res.characteristics.filter(item => item && item.characteristicId.includes('0003'))[
0].characteristicId
notifyBluetoothCharacteristicValueChange()
},
fail: (err) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值失败err', err);
callBack && callBack(false)
}
});
// #endif
}

//第七步(uni.notifyBLECharacteristicValueChange(OBJECT),启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。)
function notifyBluetoothCharacteristicValueChange() {
uni.notifyBLECharacteristicValueChange({
deviceId: curDeviceId,
serviceId: curServiceId,
characteristicId: curCharacteristicRead,
state: true,
success(res) {
console.log('第七步启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值成功res', res);
if(curOrder == 'open'){
//寻车指令
getRandomCode();
}else if(curOrder == 'close'){
//查看锁状态指令
getLockStatus();
}else{

}
//第八步(监听)(uni.onBLECharacteristicValueChange(CALLBACK),监听低功耗蓝牙设备的特征值变化事件。),含下发指令后的上行回应接受
//这里会一直监听设备上行,所以日志等需清除
uni.onBLECharacteristicValueChange((characteristic) => {
// #ifdef MP-WEIXIN
//完整的蓝牙回应数据
let ciphertext = ab2hex(characteristic.value);
//蓝牙回应数据的信息功能体和错误码
let curFeature = ab2hex(characteristic.value).slice(6, 10);
//蓝牙回应数据的错误码
let errCode = ab2hex(characteristic.value).slice(8, 10);
// #endif

// #ifdef MP-ALIPAY
//完整的蓝牙回应数据
let ciphertext = characteristic.value;
//蓝牙回应数据的信息功能体和错误码
let curFeature = characteristic.value.slice(6, 10);
//蓝牙回应数据的错误码
let errCode = characteristic.value.slice(8, 10);
// #endif
if (curFeature.startsWith('91')) { //寻车响应,拿到随机码
//用于给开锁的随机码
getUnlockData(ciphertext)
} else if (curFeature.startsWith('9200')) { //开锁响应(成功)
callBack && callBack(true)
} else if (curFeature.startsWith('98')) { //关锁后APP主动读取后的响应,查看是否已关锁
if (curFeature == '9801') { //关锁成功
callBack && callBack(true)
} else { //关锁失败
callBack && callBack(false)
}
} else {

}
})
},
fail(err) {
console.log('第七步启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值失败err', err);
callBack && callBack(false)
}
})
}

// ArrayBuffer转16进度字符串示例
function ab2hex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function (bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('')
}

//寻车指令,用于拿到开锁所需的随机码
function getRandomCode() {
let str = '7B5B01610060';
writeBLE(str)
}

//开锁指令,获取到开锁所需的数据
function getUnlockData(ciphertext) {
if (ciphertext.length > 16) { //确保寻车后蓝牙响应内容有用于开锁的随机码
//开锁头(固定值)
let headData = '7B5B1B6200';
//用户手机号
let userPhone = '018106053735';
//4B大端秒级时间戳
let timestamp = convertLettersToUpperCase(decimalToHex(getSecondsTimestamp()));
//随机码 + 8个‘0’
let randomVal = convertToLower(ciphertext.slice(16, 24)) + '00000000';
//AES加密后的前32位密文
let aesResult = aesEncrypt(randomVal,curKey).slice(0,32)
//校验码
let checkCode = '00';
//最后用于发指令的内容
let result = headData + userPhone + timestamp + aesResult + checkCode;
writeBLE(result)
} else {
getRandomCode();
}
}

//查看锁状态指令,用于验证用户手工关锁后查询是否真的已关锁
function getLockStatus() {
let str = '7B5B006868';
writeBLE(str)
}

//AES的ECB方式加密,以hex格式(转大写)输出;参数一:明文数据,参数二:秘钥
function aesEncrypt(encryptString, key) {
let aeskey = CryptoJS.enc.Utf8.parse(key);
let aesData = CryptoJS.enc.Utf8.parse(encryptString);
let encrypted = CryptoJS.AES.encrypt(aesData, aeskey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
//将base64格式转为hex格式并转换成大写
let password = encrypted.ciphertext.toString().toUpperCase()
return password;
}

//处理写入数据
function writeBLE(str) {
//如果大于20个字节则分包发送
if (str.length > 20) {
let curArr = splitString(str,20);
// #ifdef MP-WEIXIN
curArr.map(i => writeBLECharacter(hexStringToArrayBuffer(i)))
// #endif

// #ifdef MP-ALIPAY
curArr.map(i => writeBLECharacter(i))
// #endif
} else {
// #ifdef MP-WEIXIN
writeBLECharacter(hexStringToArrayBuffer(str));
// #endif

// #ifdef MP-ALIPAY
writeBLECharacter(str);
// #endif
}
}

//第八步(写入)(uni.writeBLECharacteristicValue(OBJECT),向低功耗蓝牙设备特征值中写入二进制数据。)
function writeBLECharacter(bufferValue){
uni.writeBLECharacteristicValue({
deviceId: curDeviceId,
serviceId: curServiceId,
characteristicId: curCharacteristicWrite,
value: bufferValue,
success(res) {
console.log('第八步(写入)向低功耗蓝牙设备特征值中写入二进制数据成功res', res);
},
fail(err) {
console.log('第八步(写入)向低功耗蓝牙设备特征值中写入二进制数据失败err', err);
callBack && callBack(false)
}
})
}

//将字符串以每length位分割为数组
function splitString(str, length) {
var result = [];
var index = 0;
while (index < str.length) {
result.push(str.substring(index, index + length));
index += length;
}
return result;
}

//字符转ArrayBuffer
function hexStringToArrayBuffer(str) {
// 将16进制转化为ArrayBuffer
return new Uint8Array(str.match(/[\da-f]{2}/gi).map(function(h) {
return parseInt(h, 16)
})).buffer
}

//对字符串中的英文大写转小写
function convertToLower(str) {
var result = '';
for (var i = 0; i < str.length; i++) {
if (/[a-zA-Z]/.test(str[i])) {
result += str[i].toLowerCase();
} else {
result += str[i];
}
}
return result;
}

//对字符串中的英文小写转大写
function convertLettersToUpperCase(str) {
var result = str.toUpperCase(); // 将字符串中的字母转换为大写
return result;
}

//获取秒级时间戳(十进制)
function getSecondsTimestamp() {
var timestamp = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位为秒)
return timestamp;
}

//将十进制时间戳转成十六进制
function decimalToHex(timestamp) {
var hex = timestamp.toString(16); // 将十进制时间戳转换为十六进制字符串
return hex;
}


//抛出蓝牙核心方法
module.exports = {
operateBluetoothYws
};


  • 解读:


这里的步骤和上面流程图中的步骤走向是一样的,不过里面的详情,笔者还是想每一步都拆开来对着实际案例讲述为好,详见下文(这里主要是为了照顾小白,大佬勿怪)。


六. 蓝牙模块层各步骤详解。



  1. 蓝牙功能调用核心方法的定义和导出(operateBluetoothYws)


operateBluetoothYws 这里没啥好特别的,就是将业务层传进来的参数做个中转处理,为后续步骤的api所调用,详见上文代码及其注释。



  1. 第一步(uni.startBluetoothDevicesDiscovery(OBJECT))


uni.startBluetoothDevicesDiscovery 这里主要注意的是services这个参数,这个参数会由硬件厂家提供,一般在其提供的蓝牙协议文档中会标注,作用是要搜索的蓝牙设备主 service 的 uuid 列表。某些蓝牙设备会广播自己的主 service 的 uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉周边不需要处理的其他蓝牙设备。


image.png



  1. 第二步(uni.onBluetoothDeviceFound(CALLBACK))


uni.onBluetoothDeviceFound 这一步用来确定目标设备id,即后续步骤所需的参数deviceId。 这里主要注意的是其回调函数的devices结果,我们要根据厂家或其提供的蓝牙对接协议规定和我们业务层传进来的mac来匹配筛选目标设备(因为这里会监听到第一步同样的uuid的每一台设备)(这里我就一台设备测试,所以回调函数的devices结果数组中内容就一个;然后之所以用localName.includes(curMac) 来匹配目标设备,这是根据厂商协议文档来做的,每家厂商和每种设备不一样,这里要按实际情况处理,不过万变不离其宗)。


image.png



  1. 第三步(uni.createBLEConnection(OBJECT))


uni.createBLEConnection 这里没啥特别的,主要就是用到第二步中得到的deviceId去连接低功耗蓝牙目标设备。需要注意的是这里支付宝小程序的API不一致,为my.connectBLEDevice


image.png



  1. 第四步(uni.stopBluetoothDevicesDiscovery(OBJECT))


uni.stopBluetoothDevicesDiscovery 这一步主要是为了节省电量和资源,在第三步连接目标设备成功后给停止搜寻附近的蓝牙外围设备。


image.png



  1. 第五步(uni.getBLEDeviceServices(OBJECT))


uni.getBLEDeviceServices 这里通过第二步中得到的deviceId用来获取蓝牙目标设备的所有服务并确定后续步骤所需用的蓝牙服务uuid(serviceId)。这里取res.services中的哪个,这是硬件厂商定好的,不同厂商不同,具体看对接协议(案例中的是固定放在第2个,所以是通过curServiceId = res.services[1].uuid得到)。


image.png



  1. 第六步(uni.getBLEDeviceCharacteristics(OBJECT))


uni.getBLEDeviceCharacteristics 这里通过第二步获取的目标设备IddeviceId和第五步获取的蓝牙服务IdserviceId来得到目标设备的写的uuid读的uuid。这里取characteristics的哪一个也是要根据厂商和其提供的蓝牙协议文档来决定的(案例以笔者这的协议文档为主,所以是这样获取的:curCharacteristicWrite = res.characteristics.filter(item => item && item.uuid.includes('0002'))[0].uuid 和 curCharacteristicRead = res.characteristics.filter(item => item && item.uuid.includes('0003'))[0].uuid)。需要注意的是这里支付宝小程序的API不一致,为my.getBLEDeviceCharacteristics,其res返回值也不一样,curCharacteristicWrite = res.characteristics.filter(item => item && item.characteristicId.includes('0002'))[0].characteristicId 和 curCharacteristicRead = res.characteristics.filter(item => item && item.characteristicId.includes('0003'))[0].characteristicId。


image.png



  1. 第七步(uni.notifyBLECharacteristicValueChange(OBJECT))


uni.notifyBLECharacteristicValueChange 这里就是开启低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。可以在其的success内执行一些写入操作执行uni.onBLECharacteristicValueChange(CALLBACK)来监听低功耗蓝牙设备的特征值变化事件了。


image.png



  1. 第八步(写入)(uni.writeBLECharacteristicValue(OBJECT))


uni.writeBLECharacteristicValue 这里特别要注意的是参数value必须为二进制值(这里需用注意的是支付宝小程序的参数value可以不为二进制值,可直接传入,详见支付宝小程序开发文档);并且单次写入不得超过20字节,超过了需分段写入


image.png


image.png



  1. 第八步(监听)(uni.onBLECharacteristicValueChange(CALLBACK))


uni.onBLECharacteristicValueChange 这里需根据实际开发的业务场景对CALLBACK 返回参数转16进度字符串后自行处理(支付宝小程序如果写入时未转换,那么这里读取时也不需要转换)(本文以寻车--开锁--检测锁状态为例)。


image.png


七. 总结。


以上就是本文的所有内容,主要分为2部分----业务层蓝牙模块层(封装)。业务层只需要关注目标设备和其对应的密钥(不同厂家和设备不同);蓝牙模块层主要是按蓝牙各API拿到以下四要素并按流程图一步步执行即可。



  1. 蓝牙设备Id:deviceId

  2. 蓝牙服务uuid:serviceId

  3. 蓝牙写操作的uuid

  4. 蓝牙读操作的uuid


至此,如何在小程序中优雅地封装蓝牙模块并高效使用就已经完结了,当然本文只是以最简而易学的案例来讲述蓝牙模块开发,大多只处理了success的后续,至于fail后续可以根据大家实际业务处理。相信看到这,你已经对小程序开发蓝牙功能,对接各种蓝牙协议已经有一定的认识了,再也不虚PM小姐姐的蓝牙需求了。完结撒花~ 码文不易,还请各位大佬三连鼓励(如发现错别之处,还请联系笔者修正)。


作者:三月暖阳
来源:juejin.cn/post/7300929241948422179
收起阅读 »

流量思维的觉醒,互联网原来是这么玩的

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。 微创业,认知很低 大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。 没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机...
继续阅读 »

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。


微创业,认知很低


大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。


没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机,做起了点小买卖。所以就发现如果我手动给同学处理订单会非常麻烦。他们把文件通过qq发给我,我这边打开,排版,确认格式没有问题之后算一个价格,然后打印。


所以根据痛点,我打算开发一个线上自助下单,商户自动打印的一整套系统。


百折不挠,项目终于上线


21年年中克服各种困难终于实现整套系统,提供了小程序端,商户客户端,web端。


用户在手机或网页上上传文件后会自动转换为pdf,还提供了在线预览,避免因为格式与用户本地不同的纠纷。可以自由调节单双面、打印范围、打印分数、色彩等参数。实时算出价格,自助下单。下单后服务器会通知商户客户端拉取新任务,拉取成功后将文件丢入打印队列中。打印完成后商户客户端发送信息,并由服务器转发,告知用户取件。


image.png


image.png


大三下学期,宿舍里通过线上平台,在期末考试最忙那段期间经过了“订单高峰”的考验,成交金额上千块钱。看着我商户端里面一个个跳动的文件,就像流入口袋里的💰,开心。


商业化的很失败


没想到,我自己就是我最大的客户。


期末考完,其实想拉上我的同学大干一场,让校里校外的所有的商户,都用上我们的软件,多好的东西啊。对于盈利模式的概念非常模糊,同时也有很强的竞品。我的同学并不看好我。


我对商业化的理解也源自美团模式,美团是外卖的流量入口,所以对商户抽佣很高。滴滴是打车的流量入口,对司机的抽佣也很高。所以我认为,假设我未来成为了自助打印的流量入口,那应该也可以试试抽佣模式。


而且就算我不能为商户引流,也能解放他们的双手。


当时的我,一个人做技术,做UI,还要做商业计划,去地推,真的搞得我精疲力尽。反正后面觉得短期内变现无望,就去腾讯实习了。


其实也推广了2个商户,但是他们因为各种原因不愿意用。一个是出于隐私合规风险的考虑,一个是订单量少,不需要。


所以基本这个自助打印只能框死在高校。大学生打印的文件私密性很低,但是单价低,量多,有自助打印的需求。还有一部分自助打印的场景是在行政办事大厅,这种估计没点门门道道是开不进去的。


看不懂的竞品玩法


商户通过我的平台走,我这边并不无本万利。


因为开通了微信支付、支付宝支付,做过的小伙伴应该都知道办这些手续也会花一些钱,公司还要每年花钱养。还有需要给用户的文档成转换成pdf,提供在线预览,这很消耗算力和带宽,如果用户的成交单价非常低,哪怕抽佣5%都是亏的。比如用户打印了100份1页的内容,和打印了1份100页的内容,对我来说成本差别很大,前者很低,后者很高。


当时学校里已经有一部分商户用上自助打印了。一共有3个竞品。


竞品A:不抽佣,但是每笔订单对用户收取固定的服务费,界面简陋,有广告。


竞品B:不抽佣,不收用户的服务费,界面清爽无广告。


竞品C:彻彻底底走无人模式,店铺内基本没有老板,店铺是自营或加盟的。


前期缺乏市场调研,后期缺乏商业认知


当时我在没有摸清自己商业模式,市场调研也没怎么做好的情况下。一心想的就是先把东西做出来再说,卖不成自己还能学到技术。毕竟技术这个玩意不在项目里历练,永远都是纸上谈兵。所以对于商业化的设想就是搞不成就不搞了。


我当时的想法就是要“轻”运营,就是最好我的利润是稳定的,不会亏损的。商户如果要用就得每笔订单都给我一笔钱。


后面为了补齐和竞品的功能差距,也耗费了大量心力。让我把项目从一个大学课程设计,变成了一个有商业化潜力的产品。


竞品玩法的底层逻辑


商业化的时候,就发现这个市场还是蛮卷的,不可能直接和商户收钱。竞品B不仅免费,还想着帮商户创造额外收入,做“增益”。那我确实是没有精力去对抗的。


我当时也没搞懂自己的定位,我究竟是tob还是toc。当时想着我精心设计的界面,怎么可以被广告侵蚀?那可是我的心血。所以一心想把产品体验做的比竞品好,就会有人用。但这个定位也很模糊,因为如果商户不用你的,用户怎么可能用你的下单呢。


其实应该to rmb。面向利润开发。美,是奢侈品,那是属于我内心的一种追求,但他很难具有说服力让商户使用。在国内的各种互联网产品,不盈利的产品最后都是越来越粗糙,越来越丑的,都要降本增效。而rmb是必需品,如果不能为各方创造价值,那就没有竞争力。


所以后续分析了一下各家的玩法:


竞品A:传统商业模式,依靠用户强制付费和广告,市占率一般,和第一差了10倍数量级。


竞品B:烧钱模式,免费给商户用,免费给用户用,自己想办法别的渠道做增益,还要补贴商户。市占率第一。先圈地,再养鱼,变现的事之后再说。


竞品C:不单单做打印软件,卖的是项目。一整套自助打印店的解决方案,不知道店铺能不能赚钱,但是可以先赚加盟商的钱。这个对商业运作的要求会很高,我一时半会做不了。


大佬指点了一下我


他说,你看现在什么自助贩卖机,其实就是一个流量入口。至于别的盈利不盈利再说,但是流量是值钱的。


我最近去查阿拉丁指数,了解到了买量和卖量的观念,重新认识了流量,因为知道价格了。


买量和卖量是什么?


买量说的就是你做了一个app,花钱让别人给你引流。


卖量就是你有一个日活很高的平台,可以为别人引流。


买量和卖量如何结算?


一般分为cpc和cpa两种计价方式。前者是只要用户点击了我的引流广告,广告主就得掏钱。后者是用户可能还需要注册并激活账号,完成一系列操作才掏钱。


一般价格在0.1-0.3元,每次引流。


后面我查了一下竞品B在卖量,每天可以提供10-30w的uv,单次引流报价0.1元。也就是理想情况下,每天可以有1-3w的广告费收入。


侧面说明了竞品B的市占率啊,在这个细分市场做到这个DAU……


关于流量,逆向思维的建立


流量是实现商业利益的工具。


工具类应用通过为别人引流将流量变现,内容类应用通过电商将流量变现的更贵。


依靠流量赚钱有两种姿势,主动迎合需求,和培养需求。前者就是你可以做一些大家必须要用的东西,来获得流量。比如自助打印小程序,只要商户接入了,那么他的所有顾客都会为这个小程序贡献流量。比如地铁乘车码,所有坐地铁的人都会用到,比如广州地铁就在卖量,每天有几百万的日活。


培养需求就是做自己看好的东西,但是当下不明朗,尝试发掘用户潜在的需求。


流量,如果不能利用好,那就是无效流量。所以正确的姿势是,发掘目标人群 -> 设计变现方案 -> 针对性的开发他们喜欢的内容或工具 -> 完成变现。而不是 自己发现有个东西不错 -> 开发出来 -> 测试一下市场反应 -> 期盼突然爆红,躺着收钱。


研究报告也蛮有意思,主打的就是一个研究如何将用户口袋里的钱转移到自己口袋里。做什么产品和个人喜好无关,和有没有市场前景相关。


互联网是基于实体的


互联网并不和实体脱钩,大部分平台依赖广告收入,但广告基本都是实体企业来掏钱。还有电商也是,消费不好,企业赚不到钱,就不愿意投更多推广费。


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

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

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

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


思考的难关


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


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


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


懒惰的魔咒


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


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


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


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


环境的影响



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



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


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


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


保持良好的心态


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


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


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



今天的分享就到这里,感谢你的观看,我们下期见!



作者:刘牌
来源:juejin.cn/post/7233052510554423333
收起阅读 »

【一点点税务知识】我的工资原来是这样少的

起因是这样的,我发现我的工资代扣个税,相较以前翻了三、四倍,工资也没给我涨呀,怎么交税还多了。怀疑给我算错了,于是我翻了翻资料找到一张税务总局的个人所得税税率表。 它是这样计算的: 1. 一年分成12个月,交纳税也分为12期 2. 本期应预扣预缴税额 = ...
继续阅读 »

起因是这样的,我发现我的工资代扣个税,相较以前翻了三、四倍,工资也没给我涨呀,怎么交税还多了。怀疑给我算错了,于是我翻了翻资料找到一张税务总局的个人所得税税率表



它是这样计算的:


1. 一年分成12个月,交纳税也分为12期

2.
本期应预扣预缴税额 = (累计预扣预缴应纳税所得额 * 税率 - 速算扣除数)- 累计已预扣预缴税额

3.
累计预扣预缴应纳税所得额 = 累计收入 - 累计免税收入 - 累计减除费用 - 累计专项扣除 - 累计专项附加扣除 - 累计依法确定的其他扣除

4.
其中,累计减除费用,按照5000元/月乘以纳税人当年截至本月在本单位的任职受雇月份数计算


举个例子,假设张三每月工资收入20000,各项社会保险金(五险一金)扣除为1000。


在八月份:



  • 张三累计减除费用是5000*8=40000

  • 累计专项扣除是1000*8=8000

  • 排除张三有免税收入等情况,他的累计预扣预缴应纳税所得额为20000*8-40000-8000=112000

  • 累计预扣预缴应纳税所得额112000对应税率表的2级数,所以第八期应预扣预缴税额为(112000*0.1-2520)-累计已预扣预缴税额

  • 累计已预扣预缴税额是前7个月的纳税总和。这样计算,20000*7-5000*7-1000*7=98000 对应税率表的2级数,前7期累计已预扣预缴税额为98000*0.1-2520 = 7280

  • 最后,张三在八月份,他要纳税为(112000*0.1-2520)-7280=1400


等等,文章还没完呢,不然又有人怼我纯水了。



我发现网络上像这类纳税计算器参差不齐,计算公式差得离谱,所以决定自己动手撸一个。



个税计算器


由于html、css、js代码内容长,所以我把这部分内容拼接成一张大图,也方便读源码。css布局大量使用Flex弹性布局,不了解的同学先学习一波《和我女神王冰冰一起学display: flex布局》



描述下js逻辑层:



  • 本月工资、社保(五险一金)、专项附加扣除都要乘以纳税期数,分别计算出各自的累计数

  • 本月工资、社保(五险一金)、专项附加扣除、累计减除费用累计数相减计算后,就是累计预扣预缴应纳税所得额(累计应缴税款)

  • 个人所得税税率表转化成taxRates数据结构,累计预扣预缴应纳税所得额作为参数调用getTaxRate方法返回税率、速算扣除数

  • 累计已预扣预缴税额(已缴税款)计算为纳税期数减1,然后以减后的纳税期数再重复一遍上述计算过程

  • 本期应预扣预缴税额(应交税额)= 累计预扣预缴应纳税*税率-速算扣除数-累计已预扣预缴税额(已缴税款)


布局兼容到了PC端、移动端,它们分别是这样的:



想要源码的同学,可以访问下面👇链接保存页面即可。


个税计算器在线链接:http://www.linglan01.cn/c/salary/


最后的话


文章中一类的个税计算器,一般计算出来的结果是有偏差的,原因如下:



  • 每月工资不是固定的,受KPI影响工资会有一定起浮

  • 奖金类的收入也要计算进去,如果有奖金没有计算进累计预扣预缴应纳税所得额,那计算的结果就是会偏差


所以说,个税计算器只能计算出大概的税。


想要准确的计算自己纳税情况,建议下载个人所得税APP。



当工资收入越高,应纳税所得额比重也会增大,比重在到一定程度后,我想我们应该要考虑如何合法避税。


每年年未都会有一次在个人所得税APP提交专项附加扣除,它能一定程度上补返回税额给我们。



另外,开通个人养老金帐户也可以进行一定额度的避税,将来养老滋不滋润重点看这个帐户。我收入还不足以供个人养老帐户,有条件、有需要的同学可以去了解一下。


如果我的文章对你有帮助,您的👍就是对我的最大支持^_^。


作者:凌览
来源:juejin.cn/post/7270395503821160506
收起阅读 »

浏览器的秘密

web
 浏览器架构 1 浏览器的历史 单进程与多进程浏览器 在2007年之前,市面上浏览器都是单进程的,所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。 最新的Chrome浏览器包括:1个浏览器(B...
继续阅读 »

 

浏览器架构


1 浏览器的历史


单进程与多进程浏览器



在2007年之前,市面上浏览器都是单进程的,所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。20210415092040.png




最新的Chrome浏览器包括:1个浏览器(Browser)主进程1个 GPU 进程1个网络(NetWork)进程多个渲染进程多个插件进程
20210415092356.png




  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。

  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响


2 JavaScript的单线程模型



  • 因为JS是与用户互动,以及操作DOM。如果JavaScript是多线程的,会带来很多复杂的问题,假如 JavaScript有A和B两个线程,A线程在DOM节点上添加了内容,B线程删除了这个节点,应该是哪个为准呢? 所以,为了避免复杂性,所以设计成了单线程。

  • H5提供了多线程的方案:Web Worker, 他允许主线程创建worker线程,分配任务给worker进程处理,但是worker线程完全受到主线程控制,也不能操作DOM,没有改变JS的单线程本质。


3 Chrome 打开一个页面需要启动多少进程?分别有哪些进程?


打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。



  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。


4 渲染机制


1. 浏览器如何渲染网页


概述:浏览器渲染一共有五步



  1. 处理 HTML 并构建 DOM 树。

  2. 处理 CSS构建 CSSOM 树。

  3. 将 DOM 与 CSSOM 合并成一个渲染树。

  4. 根据渲染树来布局,计算每个节点的位置。

  5. 调用 GPU 绘制,合成图层,显示在屏幕上



第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染



具体如下图过程如下图所示


img


img


渲染



  • 网页生成的时候,至少会渲染一次

  • 在用户访问的过程中,还会不断重新渲染



重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)




  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢

  • 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且CSS也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM


知识点1



  • 由于<script>标签是阻塞解析的,将脚本放在网页尾部会加速代码渲染。

  • deferasync属性也能有助于加载外部脚本。

  • defer使得脚本会在dom完整构建之后执行;

  • async标签使得脚本只有在完全available才执行,并且是以非阻塞的方式进行的


知识点2: 重绘(Repaint)和回流(Reflow)



重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大




  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

  • 回流是布局或者几何属性需要改变就称为回流。



回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流



以下几个动作可能会导致性能问题



  • 改变 window 大小

  • 改变字体

  • 添加或删除样式

  • 文字改变

  • 定位或者浮动

  • 盒模型


很多人不知道的是,重绘和回流其实和 Event loop 有关



  • 当 Event loop 执行完Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。

  • 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms才会触发一次,并且自带节流功能。

  • 判断是否触发了 media query

  • 更新动画并且发送事件

  • 判断是否有全屏操作事件

  • 执行 requestAnimationFrame 回调

  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好

  • 更新界面

  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调


常见的引起重绘的属性



  • color

  • border-style

  • visibility

  • background

  • text-decoration

  • background-image

  • background-position

  • background-repeat

  • outline-color

  • outline

  • outline-style

  • border-radius

  • outline-width

  • box-shadow

  • background-size


3.4 常见引起回流属性和方法



任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子




  • 添加或者删除可见的DOM元素;

  • 元素尺寸改变——边距、填充、边框、宽度和高度

  • 内容变化,比如用户在input框中输入文字

  • 浏览器窗口尺寸改变——resize事件发生时

  • 计算 offsetWidth 和 offsetHeight 属性

  • 设置 style 属性的值


回流影响的范围



由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种




  • 全局范围:从根节点html开始对整个渲染树进行重新布局。

  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局


全局范围回流


<body>
<div class="hello">
<h4>hello</h4>
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>coding</li>
<li>loving</li>
</ol>
</div>
</body>


p节点上发生reflow时,hellobody也会重新渲染,甚至h5ol都会收到影响



局部范围回流



用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界



3.5 减少重绘和回流



使用 translate 替代 top



<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>


@程序员poetry: 代码已经复制到剪贴板



  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量


for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}


@程序员poetry: 代码已经复制到剪贴板



  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • CSS选择符从右往左匹配查找,避免 DOM深度过深

  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层。


img


img


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

谈谈SSO单点登录的设计实现

谈谈SSO单点登录的设计实现 本篇将会讲讲单点登录的具体实现。 实现思路 其实单点登录在我们生活中很常见,比如学校的网站,有很多个系统,迎新系统,教务系统,网课系统。我们往往只需要登录一次就能在各个系统中被认定为登录状态。 这是怎么实现的?我们需要一个认证中心...
继续阅读 »

谈谈SSO单点登录的设计实现


本篇将会讲讲单点登录的具体实现。


实现思路


其实单点登录在我们生活中很常见,比如学校的网站,有很多个系统,迎新系统,教务系统,网课系统。我们往往只需要登录一次就能在各个系统中被认定为登录状态。


这是怎么实现的?我们需要一个认证中心,一如学校网站也有一个统一认证中心,也就是我们的SSO的Server端。在每个系统也就是Client端,我们只要判断已经在这个认证中心中登录,那我们就会被设置为登录状态。


再来就是最后一个问题了,我们判断在认证中心登录后,怎么在其他系统中也登录?


这个问题其实就是单点登录中最麻烦的问题了,也就是如何传播我们的登录状态。


我们可以分为两个情况Cookie共享传播状态,url参数传播状态。


Cookie共享传播状态


第一种情况:我们的认证中心和其他系统是在一个域名下的,认证中心为父域名(jwxt.com),其他系统是子域名(yx.jwxt.com),或者是同一IP不同端口的情况,我们的服务端通过cookie去判断是否登录。


在这种情况下我们只要在认证中心登录成功的时候设置Cookie,当然设置Cookie的时候也要注意设置好你的Cookie参数。


要注意设置的参数是dominpath。这两个参数值决定了Cookie的作用域。domin要设置为父域名(.jwxt.com)。当然还要注意一个SameSite参数,不能设置为。(如果为,你在baidu.com登录,在example.com网站如果你点击了 baidu.com/delete 链接,会带着你在baidu.com的Cookie访问。)


设置完Cookie,子域名的系统也有了Cookie,自然就会被服务端判断为登录状态。


简而言之,就是利用Cookie共享来实现登录状态的传播。


url参数传播状态


第二种我们的认证中心和其他系统不在一个域名下的,或者是不同IP的情况。


为了安全浏览器限制cookie跨域,也就是说第一种方法就不管用了。


这种情况可以通过传播参数来实现,也就是在认证中心登录后带着 登录凭证(token) 重定向到对应的Client页面,然后我们的前端就可以用js获取到url中的token进行存储(设置到Cookie或者localstorage等方式),之后我们的服务端只需要通过这个token就可以判断为登录状态了。


当然,为了安全我们往往不会直接传递凭证,而是传递一个校验码ticket,然后前端发送ticket到服务端校验ticket,校验成功,就进行登录,设置Cookie或者存储token。


流程


接下来我们梳理一下流程,一下Client为需要单点登录的系统,Server为统一认证中心。


Cookie共享传播状态



  1. 用户在Client1,如果没有登录,跳转到Server,判断在Server是否登录,如果判断没有登录,要求登录,登录成功后设置Cookie,跳转Client

  2. Client1登录成功


如果之后在Client2页面,由于共享Cookie,当然也是登录状态。


url参数传播状态



  1. 用户在Client1,判断没有登录,跳转到Server,判断在Server是否登录,如果没有登录,要求登录,登录成功后设置Cookie,带着ticket跳转Client。

  2. 到了Client1,前端通过参数获取到ticket,发送到服务端,服务端校验ticket获取登录id,设置Cookie进行登录。


之后在Client2页面



  1. 用户在Client2,判断没有登录,跳转到Server,判断在Server是否登录,这时候判断为登录,带着ticket(或者token)跳转Client。

  2. 到了Client2,前端通过参数获取到ticket,发送到服务端,服务端校验ticket获取登录id,设置Cookie进行登录。


如果不使用ticket校验就直接存储传播过来的登录凭证即可,当然如果你不存储到Cookie,记得在请求后端服务的时候带上token。


ticket校验


再说说ticket校验


ticket校验根据情况也可以分为两种,一种情况是Server和Client的后端共用的同一个Redis或者Redis集群,可以直接向Redis请求校验。如果后端用的Redis不同,可以发送http请求到Server端在Server端校验。


到此,单点登录就完成了。


当然在以上描述中的Cookie你也可以不使用,使用Cookie主要是方便,在请求后端时会自动发送。你只需要存储到localstorage/sessionstorage等地方,请求后端的时候记得get然后带上即可。


作者:秋玻
来源:juejin.cn/post/7297782151046266890
收起阅读 »

前端基建有哪些?大小公司的偏重啥?🤨

web
前言 兄弟们可能有的感受 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。 感受一:天天写业务代码,感觉没啥技术含量,没沉淀,成长太慢,架构那边挺有趣的。 感受二:天天写内部工具,觉得没啥提升,感觉要废。 感受三:对一些框架的...
继续阅读 »

前言




兄弟们可能有的感受



  • 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。

  • 感受一:天天写业务代码,感觉没啥技术含量,没沉淀,成长太慢,架构那边挺有趣的。

  • 感受二:天天写内部工具,觉得没啥提升,感觉要废。

  • 感受三:对一些框架的原理、源码、工具 研究较少,无法突破评级,成为leader。


上面的感受都是一些兄弟们的典型感受(也包括我自己)。这时候不妨可以考虑一下,了解了解前端的基础建设,进而 搭建起一个坚实的底座和让自己得到一个提升




正文开始——关于“基建”




1.什么是基建?



  • “技术基建”,就是研发团队的技术基础设施建设,一个团队通用技术能力的沉淀。

  • 小到文档规范,脚手架工具,大到工程化、各个领域工具链,凡是能促进业务效率、沟通成本都可以称作基建。

  • 网上看到的一句话,说的很好, “业务支撑是活在当下,技术基建是活好未来”




2.基建的意义


主要是为了以下几点:



  • 业务复用,提高效率: 基建可以提高单个人的工作产出和工作效率,可以从代码层面解决一些普遍性和常用性的业务问题

  • 规范、优化流程制度: 优异的流程制度必将带来正面的、积极的、有实效的业务支撑。

  • 更好面对未来业务发展: ,像建房子一样,好的地基可以建出万丈高楼。

  • 影响力建设、开源建设:建设结果对于业务的促进,更容易获得内部合作方的认可;沉淀下来的好的经验,可以对外输出分享,也是对影响力的有力帮助。




基建搞什么




1.核心:


下手之前首先得记住总结出的核心概念:



  • 三个落地要素: 公司的团队规模、公司的业务、团队水平。

  • 四大基础特性: 技术的健全性、基建的稳定性、研发的效率性、业务的体验性


根据结合落地和基础特性,来搭建不同"重量"和"复杂度"的基建系统。(毕竟每个公司的情况都不同)




2.方向


基建开始之前,首先得确定建设的策略及步骤,主要是从 拆解研发流程 入手的:


一个基本的研发流程闭环一般是:需求导入 => 需求拆解 => 技术方案制定 => 本地编码 => 联调 => 自测优化 => 提测修复 Bug => 打包 => 部署 => 数据收集&分析复盘 => 迭代优化 。


在研发流程闭环中每一个环节的阻塞点越少,研发效率就越高。基建,就是从这些耽误研发时间的阻塞点入手,按照普遍性 + 高频的优先级标准,挨个突破。




3.搞什么


通用的公式是: 标准化 + 规范化 + 工具化 + 自动化 ,能力完备后可以进一步提升到平台化 + 产品化。在方向上,主要是从下面的 8 个主要方向进行归类和建设,供大家参考:



  • 开发规范:这一部分沉淀的是团队的标准化共识,标准化是团队有效协作的必备前提。

  • 研发流程: 标准化流程直接影响上下游的协作分工和效率,优秀的流程能带来更专业的协作。

  • 工程管理: 面向应用全生命周期的低成本管控,从应用的创建到本地环境配置到低代码搭建到打包部署。

  • 性能体验: 自动化工具化的方式发现页面性能瓶颈,提供优化建议。

  • 安全防控: 三方包依赖安全、代码合规性检查、安全风险检测等防控机制。

  • 统计监控: 埋点方案、数据采集、数据分析、线上异常监控等。

  • 质量保障: 自测 CheckList、单测、UI 自动化测试、链路自动化测试等。


如上是一般性前端基建的主要方向和分区,不论是 PC 端还是移动端,这些都是基础的建设点。业务阶段、团队能力的差异,体现在基建上,在于产出的完整性颗粒度深入度自动化的覆盖范围。




4.大小公司基建重点


小团队的现实问题:考虑到现实,毕竟大多数前端团队不像大厂那样有丰富的团队人员配置,大多数还是很小的团队,小团队在实施基建时就不可避免的遇到很现实的阻力:



  • 最大的阻力应该就是 受限于团队规模小 ,无法投入较多精力处理作用于直接业务以外的事情

  • 其次应该是团队内部 对于基建的必要性和积极性认识不够 (够用就行的思想)


大小公司基建重点:




  • 小公司: 针对一些小团队或者说偏初创期的团队,其建设,往往越偏向于基础的技术收益,如脚手架组件库打包部署工具等;优先级应该排好,推荐初创公司和小团队成立优先搭建好:规范文档、统一开发环境技术栈/方法/工具、项目模板、CI/CD流程 ,把基础的闭环优先搭建起来。




  • 大公司: 越是成熟的业务和成熟沉淀的团队,其建设会越偏向于获取更多的业务收益,如直接服务于业务的系统,技术提效的同时更能直接带来业务收益。搭建起一套坚实的项目底座,能够更好的支持上层建筑的发展,同时也能够提升团队的成长,打开在业界的知名度,获取更好的信任支持。大公司在基础建设上,会更加考虑数据一些监控以及数据的埋点分析和统计,更加的偏重于数据的安全防范,做到质量保证。对于这点,很多前端需要写许多的测试case,有些人感觉很折磨,哈哈哈哈哈哈。






基建怎么搞




下面,会针对一些大家都感兴趣的方向,结合我们团队过去部分的建设产出,为大家列举一些前端基建类的沉淀,以供参考。


1. 规范&文档


规范和文档是最应该先行的,规范意味着标准,是团队的共识,是沟通协作的基础。


文档:



  • 新人文档(公司、业务、团队、流程等)

  • 技术文档、

  • 业务文档、

  • 项目文档(旧的、新的)

  • 计划文档(月度、季度、年度)

  • 技术分享交流会文档


规范:



  • 项目目录规范:比如api,组件,页面,路由,hooks,store等



  • 代码书写规范:组件结构、接口(定义好参数类型和响应数据类型)、事件、工具约束代码规范、代码规范、git提交规范




2. 脚手架


开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。


省事的可能直接拥抱框架选型对应的全家桶,如 Vue 全家桶,或者用 Webpack 撸一个脚手架。能力多一些的会再为脚手架提供一些插件服务,如 Lint 或者 Mock。从简单的一个本地脚手架,到复杂的一个工程化套件系统。




3. 组件


公司项目多了会有很多公共的组件,可以抽离出来,方便自身和其他项目复用,一般可以分为以下几种组件:



  • UI组件:antd、element、vant、uview...

  • 业务组件:表单、表格、搜索...

  • 功能组件:上拉刷新,滚动到底部加载更多,虚拟滚动,拖拽排序,图片懒加载..




4. 工具 / 函数库


前端工具库,如 axios、loadsh、Day.js、moment.js、big.js 等等(太多太多,记不得了)


常见的 方法 / API封装:query参数解析、device设备解析、环境区分、localStorage封装、Day日期格式封装、Thousands千分位格式化、防抖、节流、数组去重、数组扁平化、排序、判断类型等常用的方法hooks抽离出来组成函数库,方便在各个项目中使用




5. 模板


可以提前根据公司的业务需求,封装出各个端对应通用开发模版,封装好项目目录结构,接口请求,状态管理,代码规范,git规范钩子,页面适配,权限,本地存储管理等等,来减少开发新项目时前期准备工作时间,也能更好的统一公司整体的代码规范。



  1. 通用后台管理系统基础模版封装

  2. 通用小程序基础模版封装

  3. 通用h5端基础模版封装

  4. 通用node端基础模版封装

  5. 其他类型的项目默认模版封装,减少重复工作。




6. API管理 / BFF


推荐直接使用axios封装或fetch,项目中基于次做二次封装,只关注和项目有关的逻辑,不关注请求的实现逻辑。在请求异常的时候不返回 Promise.reject() ,而是返回一个对象,只是code改为异常状态的 code,这样在页面中使用时,不用用 try/catch 包裹,只用 if 判断 code 是否正确就可以。再在规定的目录结构、固定的格式导出和导入。


BFF(Backends For Frontends)主要将后端复杂的微服务,聚合成对各种不同用户端(无线/Web/H5/第三方等)友好和统一的API;




7. CI/CD 构建部署


前端具备自己的构建部署系统,便于专业化方面更好的流程控制。很多公司目前,都实现了云打包、云检测和自动化部署,每次 git commit 代码后,都会自动的为你部署项目至 测试环境、预生产环境、生产环境,不用你每次手动的去打包后 cv 到多个服务器和环境。开发新的独立系统之初,也会希望能实现一种 Flow 的流式机制,以便实现代码的合规性静态检测能力。如果可以的话,可以去实现了一套插件化机制,可以按需配置不同的检测项,如某检测项检测不通过,最终会阻塞发布流程。




8. 数据埋点与分析


前端团队可以做的是 Web 数据埋点收集和数据分析、可视化相关的全系统建设。可实现埋点规范、埋点 SDK、数据收集及分析、PV/UV、链路分析、转化分析、用户画像、可视化热图、坑位粒度数据透出等数据化能力,下面给大家细分一些这些数据:



  • 行为数据:时间、地点、人物、交互、交互的内容;

  • 质量数据:浏览器加载情况、错误异常等;

  • 环境数据:浏览器相关的元数据以及地理、运营商等;

  • 运营数据:PV、UV、转化率、留存率(很直观的数据);




9.微前端


将您的大型前端应用拆分为多个小型前端应用,这样每个小型前端应用都有自己的仓库,可以专注于单一的某个功能;也可再聚合成有各个应用组成的一个平台,而各个应用使用的技术栈可以不同,也就是可以将不同技术栈的项目给整合到一块。这点就很不错,在如今电子办公化如此细致的时代,可能许多公司工作中都不止一个平台,平台之间的切换十分的繁琐,这时候平台之间聚合的趋势想来是必然的。(个人浅显的理解)


目前成熟一点的框架有蛮多的,使用的底层思想也各有不同,目前我也在学习qiankun等框架中,期待后面能够给大家分享一篇文章,加油💪




基建之外思考




1. 从当下业务场景出发开始


很多时候我们的建设推不下去,往往不是因为人力的问题,而是 没想清楚/没有方向 。对于研发同学,我们更应该着重于当下,从方案出发找实际场景的问题,也就是从我们项目和团队目前的业务问题、人员问题,一步步出发。还有就是,我们得开这个头。没有一个作家是看小说看成的,同理技术专家也不会是通过看技术书籍养成的。在实践中也就是实际场景中学习,从来都是最快的方式。许多有价值的事从来都是从业务本身的问题出发。到头来你会发现:问题就是机会,问题就是长萝卜的坑




2.基建讲究循序渐进


业界大部分的研发团队,都不是阿里、腾讯、头条这样基础完备沉淀丰富的情况,起步期和快速爬坡期居多,建设滞后。体现在基建上,可能往往只有一个基于 Webpack 搞搞的脚手架,和一个第三方开源的 UI 组件库上封装下自己的业务组件库,除此之外无他。如果兄弟们现在恰好是我说的这种情况,不用焦虑,很多前端也是一样的情况。只要我们一步步建设,慢慢落地基础设施,就一定会取得好的反馈




3. 技术的价值,在于解决业务问题,并且匹配


技术的价值,在于解决业务问题;人的身价,在于解决问题的能力


基建的内容我认为首先是 和业务阶段相匹配 的。不同团队服务的业务阶段不同,基建的内容和广深度也会不同。高下之分不在于多寡,而在于对业务的理解和支持程度。


“业务支撑” 和 “基础建设” 都是同一件事的两个面,这个 “同一件事”,就是帮助业务解决问题。任何脱离解决实际场景而发起的基建,都需要重新审视甚至不应被鼓励。如果时间成本没有那么多的话,建议先搭建好基本的建设底座,想要更好的闭环的想法还是先搁置一下。




4.个人不足


总结了这么多,结果发现自己对于一些知识点还是了解的太浅显了,自身在那些方面能分享的还是不多,也看了一些文章,只能描出个大概,实在是有点不好意思。但回头想想,这何尝也不算个勉励自己的方法,能够鞭策自己。后续,在我学习深入一些基建方面的知识后,会再出一些文章分享给大家,希望能够帮助到大家,共勉!!!☺(发现问题会及时补充)




落尾




大家好,我是 KAIHUA ,一个来自阿卡林省目前在深圳前端区Frank + ikun


从这周开始,我想试试每一两周复盘一次,总结出至少一个知识点,目的是尽快给自己的反馈,将自己产品一样快速迭代上升,希望可以坚持✊。


如果有什么相关错误,望大家指正,感谢感谢!!!(还在学习中,嘿嘿🤭)


下一篇文章应该会是关于 前端思考 方面的,希望早一点归纳出,和大家沟通交流...


各位 彦祖 / 祖贤,fan yin (欢迎) 关注点赞收藏,将泼天的富贵带点给我😭


一起加油!!! giao~~~🐵🙈🙉


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

有些程序员表面老实,背地里不知道玩得有多花

作者:CODING来源:juejin.cn/post/7259258539164205115

img


img


img


img


img


img


img


img


img


img


img


img


img


img


img


img


img


img


作者:CODING
来源:juejin.cn/post/7259258539164205115

关于我坚持 2 年的学习打卡心得

学习的心得 记住两个概念,终值和峰值。这是一个心理学专家提出来的。 峰值是指这段体验中的最高峰。终值是指这段体验结束后的感觉。它们都分为两个方向,正向和负向。 在学习的过程中,想体验到正向的终值和峰值,是比较困难的。我如何让学习变得相对愉悦一点呢?穿插自己比较...
继续阅读 »

学习的心得


记住两个概念,终值和峰值。这是一个心理学专家提出来的。


峰值是指这段体验中的最高峰。终值是指这段体验结束后的感觉。它们都分为两个方向,正向和负向。


在学习的过程中,想体验到正向的终值和峰值,是比较困难的。我如何让学习变得相对愉悦一点呢?穿插自己比较喜欢的,即自己比较擅长的。例如,我每次学习时,都会划分时间片,因为目标越小,压力越小。学习计算机网络,我每次强制自己学 25 分钟,时间一到就立刻停止,即使我还没学完整,此时去做一点自己喜欢的输入,例如去吃点水果零食,看会儿朋友圈。之后就是两种任务来回切换。如果让我连续学习几个小时的计算机网络,我可能很难坚持到最后。


为了避免终值是负向的,我们制定计划的时候,要考虑量力而行,以天为最小单位,不要给自己一天安排太多任务,根据自己的情况灵活决定,每天我们都能总结自己的收获,一目了然。人的大脑,都喜欢看到眼前的利益,我们的远大理想和目标,很难满足大脑。让自己能从每天的学习上得到正反馈。学东西,制定计划,不是为了用某种标准框架自己,我们是为了成长,而不是为了满足框架,专注做事本身。



  • 戒掉手机。物理隔绝。

  • 收集素材,整理输出。

  • 不搞形式主义,直接开始。

  • 带着问题去探索,做到记少忆多。

  • 要允许自己写出来垃圾,否则连垃圾都写不出来。

  • 检测自己获得了什么,也就是做题,实践应用。这样才能实现闭环。

  • 加工自己的知识,即仔细的思考、精细化的提问,多问自己“为什么”。


坚持的秘诀



  • 要么此时做,要么不再做。

  • 不要花大量的时间做容易的环节。

    • 例如学习数据结构与算法,长时间都去学习最基础的数组、链表、队列,这就是伪勤奋。相对的真勤奋,是真的那些让你需要感到思考、克服困难的任务。



  • 番茄模式。轮换式工作,投资式休息。

  • 如果不去做完成这个任务,就要去做更难得任务。

    • 例如学习计算机网络,我不想学习的时候,就告诉自己要去看 CSAPP。



  • 领先自己的计划。如有偷懒,也可接受,不至于一日崩盘。

  • 完成每天的目标后,其余时间,自由安排,一切感兴趣之事。

  • 要么自律,要么他律。

    • 什么是他律?大声的告诉你在意的人,你在学什么。



  • 不想学的时候,也先打开看看。根据惯性定律,改变状态往往是最难的,但维持状态却是相对简单的。


做笔记


笔记有两个方向,四个作用。
关于方向,一个方向记录别人说的话,另一个方向是记录看过的书、视频。关于作用,请看下图。


WechatIMG160.jpeg


总结


没有完美的方法论,只有完美的行动,祝愿看完的同学们,都能有完美的人生。


有修养的程序员才可能成长为真正的工程师和架构师,而没有修养的程序员只能沦为码农,这是码农和工程师的关键区分点。


修养指的是:英文能力、提问的能力、写代码的修养、安全防范意识、软件工程和上线规范、编程规范等。这些能力的训练和培养将为后续的学习和发展夯实基础。



作者:龚国玮
来源:juejin.cn/post/7204349756620750908
收起阅读 »

既当产品又当研发,字节大哥手把手带我追求极致

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做...
继续阅读 »

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做的东西,上面说的特点至少有一个不具备,甚至通通不具备。


而我在字节实习的过程中,所经手的恰恰就是这么一个需求不明确、解决方案不明确、最终产品效果不明确的项目。整个过程中有过焦头烂额毫无进展的时刻也有过欲哭无泪的时刻,还好有我的mentor带着我一路披荆斩棘、过关斩将。


首先和大家讲一下项目背景,当时我在的组是视频会议移动端,经历了近三年大流感的洗礼,相信大家对于视频会议中可能遇到的各种问题如数家珍,包括但不限于没声了、没音了、没画面了、画面卡顿、画面不清晰、画面和语音不同步、同步屏幕时闪退等等等等。作为一个服务企业级的B端产品,出现以上问题时就可能会投诉,然后经过客户成功部门转手到运营再转手到研发这里,研发就需要判断分析一下究竟是我们产品的原因、还是客户本身设备的问题、或者是第三方环境的因素,当用户的量级上来后,这样的客诉就会很多,会严重占用oncall的研发人员的时间以及精力。


我的mentor,一个专注于解决问题、避免重复劳动的人,一个字节范我觉得有E+的人,一个虽然身处移动端但是前后端甚至网络也都会的人,觉得这样很不OK,应该有个工具,能够自动的分析出来客户究竟遇到了什么问题,分析不出来的再找研发进行排查。没有这个工具也不影响业务开发的进展,所以整个项目并不存在时间上的紧迫性,但是呢,有这个工具做出来后肯定会大大降低研发的开发时间,所以项目的必要性还是有的。于是,我作为刚入职的实习新人,这个项目就交给我来做了。


而我,一个还没有从校园中完全出来的新兵蛋子,说实话面对这样的场面是一脸懵逼的,对于要做啥、要怎么做可以说是一无所知,我的mentor在我入职后,让我先了解了解背景,第一周就带着我oncall了,让我知道都可能有样的客诉,手把手给我演示他们正常的排查问题的方式。先了解客户反馈的情况,然后捞出来客户对应时间的设备信息以及设备日志。


说实话,作为一个新人,或者说我本身对于项目有一种畏难心理,碰到一点难题就总是想着往后拖,或者摆烂先不管然后就搁置在一边不想再问津了,但是我的mentor是一个有着坚定信念的人,差不多就是见山开山,见水架桥这种,遇到问题会主动找到相关人员一起解决,可以说就是有那种主人翁,项目owner的意识。于是,我就跟在他的后面,和整个团队的不同角色沟通他们遇到问题时排查的思路,试图总结出来一种通用的流程。在过程中,难免有许多困难,我的第一反应是退缩,但是导师的第一反应是拉会拉上相关人员一起讨论,看看用什么方式可以解决。比如在如何确定设备日志和故障表现的映射关系时,先后调研了多种方式看看相关团队有没有类似的做法以及他们最后实现的效果,包括大数据机器学习、代码状态流转图、自定义规则引擎等多种方式,最后调研后决定采用自定义规则引擎的方式。在实现需求的时候,需要其他团队协作时,他总是直接向前提出自己的需求,而我向一个陌生人发消息之前总要做一些心理建设,总是在担心些什么、害怕些什么,而事实上大家都是打工人,谁也不比谁厉害多少,对方不配合那就拉+1进群一起看看,解决不了就向上暴露问题。


于是,导师披荆斩棘在前,我在后面跟着实现他的设想。我们很快就做出来了第一个版本。通过Python自动化下载设备日志,然后正则匹配筛选出含有特定标记的日志,并对他们的出现频率次数做出判断。因为Python是解释型的语言,所以可以把规则直接写成python语言,用eval或者exec函数进行执行。第一个版本做出来后,导师又积极的带着我去给其他人宣传我们的这个工具。然后根据他们的反馈继续进行相关改进,最后我离职前实现的效果就是@ 一个群里的机器人,告诉他出现问题的ID,他就能自动化的拉下来日志进行排查,然后告诉你他分析的结果,整个交互非常的方便。


一个成功的项目必须要有一个负责的owner,我的导师就向我展示了一个优秀的owner是如何一步步解决问题、排除项目中的难关,如今我也正式成为一名打工人,希望我也能早日如他一般自如的面对工作。


我是日暮与星辰之间,出道两年半的Java选手,相信时间的力量,一起成为更好的自己!


作者:日暮与星辰之间
来源:juejin.cn/post/7211801284709138493
收起阅读 »

面试多起来了

就在昨天 10.17 号,同时收到了三个同学面试的消息。他们的基本情况都是双非院校本科、没有实习经历、不会消息中间件和 Spring Cloud 微服务,做的都是单体项目。但他们投递简历还算积极,从今年 9 月初就开始投递简历了,到现在也有一个多月了。 来看看...
继续阅读 »

就在昨天 10.17 号,同时收到了三个同学面试的消息。他们的基本情况都是双非院校本科、没有实习经历、不会消息中间件和 Spring Cloud 微服务,做的都是单体项目。但他们投递简历还算积极,从今年 9 月初就开始投递简历了,到现在也有一个多月了。


来看看,这些消息。
73cef56826e05f5c6076d35e6bcf442.jpg
7ceca957a2af480d4ae4fe345698b35.jpg
674493f3926c19f65f16c529e1c231b.jpg


为什么会这样?


9 月中旬就开始正式批校招了,而且从往年的数据来看,每年参加秋招的公司大概有 1700 多家,为什么大多数双非本科,到了 10 月中旬才有面试机会呢?


主要原因是软件行业这两年的情况是“供大于求”,一方面是软件公司的业务趋向平稳,招聘需求量不大;而另一方面是应届生逐年增多,而且涌向软件行业的人也越来越多。


a.毕业生增多


image.png
从图片可以看出,每年本硕毕业生都在增多,而 2022 年本硕毕业生已经达到了惊人的 1076 万人了,请问咱们国家的就业缺口有这么大吗?


b.业务平稳期


软件的生命周期中有两个大的阶段:



  1. 程序开发期:需要大量的人力协同开发一款程序,这时候需要大量的研发人员。

  2. 程序维护期:程序开发完了,只有一些小的需求和功能维护等工作,这个时候只需要少量的开发人员就可以搞定了。


而目前大部分互联网公司都已经进入了第二个阶段“程序维护期”,如果程序开发期需要 1000 人的话,那么维护期 50 个人就够了,因为没有很多的功能要做,只是少量修修补补的工作。


c.越来越多人的涌入


2018 年之后,计算机成为中国薪资最高的行业,一举超过了多年霸榜的金融行业,所以大家慢慢全部都明白了“计算机行业赚钱啊”。


所以,综合上述情况大家可以看出,目前的供需关系是:供给方(毕业的学生)远远大于需求方(用人单位),所以目前计算机行业就业严峻是一个必然事件。


如何获得Offer?


既然想拿高薪、既然没有其他的出路、既然已经上了贼船,那么怎么才能在竞争激烈的校招中找到满意的 Offer 呢?


你需要做以下四件事:



  1. 海投简历

  2. 面试前充分准备

  3. 准备好自我介绍

  4. 调整好心态&积极面试


1.海投简历


海投简历是指,你要把你能找到的、你能看到的所有和你岗位相关的职位都投递一遍(简历)。


举个例子,例如你在 Boss 上投递 Java 研发工程师的工作,那么就搜索“Java”,然后把你能找到的(看到的)所有公司,且没投递的公司(投递的公司用 Excel 记录下来),全部(打招呼)投递一遍简历。


注意:不用去看 HR 发布的职位要求,很多公司发布的职位要求是比较高的,但大部分情况下,她们都会减低标准,给更多应聘者笔试和面试的机会。所以说,不要看到很高的职位要求就退缩了,任何机会都不要放过,海投就是投递所有和你职位相关的所有公司,一家都不放过,因为他的失败影响不大,但万一成功了就有工作了。



海投简历什么时候结束?
答:海投简历通常是到 11 月中下旬,或拿到第一个保底 Offer 之后,才会逐渐停止,所以做好打持久战的准备,没有任何事是一蹴而就的。



2.面试前充分准备


面试之前,一定要把该公司的岗位技能要求,以及该公司的往年历史真题全部过一遍。


对于自己不会的技能一定要提前学习,还有往年的历史真题也要仔细过一遍,把不会的问题在面试前一定要搞定,防止面试时再次被面试官问到。


3.准备好自我介绍


细节决定成败,面试本质上是“自我推销”的过程。如何在短短的几十分钟内打动面试官,从来都不是一个简单的问题。


所以怎么开场?怎么让面试官对我产生兴趣?非常关键。


好的自我介绍,一定要讲明白以下 4 点:



  1. 你是谁?

  2. 你会啥?

  3. 你有什么成就?

  4. 为什么是你?


a.你是谁?


自我介绍的第一步一定是自报家门,例如,我是张三,2015 年毕业于西安电子科技大学,毕业之后一直从事 Java 开发的工作,做过 XXX 公司的高级研发工程师,也很高兴参加贵公司的面试。



校招版本:我是李四,24 届学生,目前就读于西安电子科技大学,硕士学历,就读的专业是软件工程(非软件相关专业就不要介绍你的专业了),很荣幸参加贵公司的面试。



b.你会啥?


技术岗位,最看重的就是你的技术能力,所以这一步一定要好好准备,并详细的介绍你会的技能。


要做好这一步,在面试前一定要查阅应聘公司的岗位要求和使用的技术栈,这样你就能针对性的进行技能介绍了。而当面试官看到一个应聘者的技术栈,和自己公司的技术栈完全匹配的时候,你的面试成功率就大幅提升了。


例如,你可以这样介绍。
我会的技能是并发编程、MySQL、Redis、Spring、Spring MVC、Spring Boot、Spring Cloud Alibaba Nacos、Sentinel、Seata、Spring Cloud Gateway、Skywalking、RabbitMQ 等技术栈。


c.你有什么成就?


学以致用很重要,尤其是校招,你上面说你会,那么怎么证明你真的会你说的哪些技术呢?你使用上述技能获得过什么成就?或做过什么项目呢?


如果你参加过 ACM、蓝桥杯等编程竞技大赛,可以在自我介绍的时候详细的说一下,参赛情况和获奖经历。


如果你没有参赛经历和获奖经历,那么你可以介绍你用上面的技能做过什么项目?


例如,我使用 Spring Cloud Alibaba 全家桶 + Spring Cloud Gateway + MySQL + Redis + RabbitMQ 总共做过 3 个项目,其中有两个项目我已经写在简历上了,等会您有任何关于项目或技能点的问题都可以问我。


d.为什么是你?


前面三点是陈述,而最后这点就要升华了,这也是你进行“自我吹嘘”最后的机会,也是打动面试官最关键的时刻,“峰终定律”就是讲这个事。


为什么要你?就是你要介绍自己的优点了,例如(但不限)以下这些:



  1. 我的技术栈和公司非常匹配:因为我的技术栈和公司的技术栈非常匹配,所以来了之后就能直接干活,大大节省了新人培养成本。

  2. 我对公司的业务比较熟悉:我之前从事过,或者详细的了解过公司的相关业务,所以来了之后直接能干活,大大节省了业务培训成本。

  3. 我做事比较专注:例如,去图书馆看书,经常忘记吃中午饭,等到肚子饿的不行了,抬头一看表已经下午 3 点了。

  4. 我自学能力比较强:例如,整个微服务,像 Spring Cloud Alibaba 整个技术栈,我只用了 2 周的时间就全部学会了,并且能用它开发一个 Java 项目,期间遇到的所有问题,我都能自行解决。

  5. 我喜欢编程:例如,您可以看我的 GitHub 我每天都有练习和提交代码。


4.调整心态&积极面试


a.避免过度紧张


学的好也要面的好,尤其是第一次面试,紧张是不可避免的事情,所以你要告诉自己“允许自己适当紧张”这是正常的表现。


你越在意什么就越容易失去什么,所以不要过度的在意“自己比较紧张”这件事,它是正常的情况,我工作 13 年了,现在出去面试依然会紧张,所以“紧张”这些事,本身就是人类正常的情绪。


如何缓解紧张?


答:把注意力和精力放在面试官问的问题上,而不是过度的关照自我,面试前深呼吸,面试时把注意力放在自身以外的其他事情上,这样就能大大的减少紧张的情绪。


b.不要害怕失败


越害怕什么就越容易失去什么,所以不要害怕失败,失败乃成功之母,任何事情都是有意义的,即使失败也不例外,它能让你变成更好的自己。


你把每次面试都当成是自我检验和自我提升的机会,无论结果如何,你都能收获成长,越不在意结果,可能结果越理想。


c.不要太在意薪资


万事开头难,尤其是校招第一份工作,不要太在意薪资,你真正赚钱是 3-5 年工作经验之后,所以事情不可能一蹴而就,也不可能一口气吃成一个大胖子。所以先入行比什么都重要,熟练之后才能真正的赚到钱。每个人都是一样,所以不要太在意入行薪资。


小结


软件行业业务趋于平稳,涌入的人越来越多,所以也会越来越卷,每年都是当下最好的一年,所以你只有做到最好,才有可能拿到满意的 Offer。多投简历、面试前做好充足准备、准备好自我介绍、调整好心态、积极去面试,相信做好这些,结果就不会太差。加油,少年。


作者:Java中文社群
来源:juejin.cn/post/7291134345920643107
收起阅读 »

用剥蒜的方式去学习闭包,原来这么简单!!!

web
对于很多学习js的码友们来说,闭包无疑是最难啃的模块之一,今天我们就用剥蒜的方式一层一层的剥开它。 我们先用一个案例来引入它 大多数肯定会觉得它输出的结果是0到9,那么大多数人都错了,它输出的结果是十个10 那么要怎么让它输出0到9呢?这里我们要先引入一个新...
继续阅读 »

对于很多学习js的码友们来说,闭包无疑是最难啃的模块之一,今天我们就用剥蒜的方式一层一层的剥开它。


我们先用一个案例来引入它



大多数肯定会觉得它输出的结果是0到9,那么大多数人都错了,它输出的结果是十个10
那么要怎么让它输出0到9呢?这里我们要先引入一个新的东西,叫调用栈


调用栈


调用栈是用来管理函数调用关系的一种数据结构


在代码预编译的时候就会产生一个生成一个调用栈,它会先去找到代码中的全局变量、函数声明,形成一个变量环境,这个变量环境和词法环境(这里我们不去探讨)一起放在全局执行上下文中。然后再从上到下去执行代码,每调用一个函数,就会生成一个该函数的执行上下文,然后入栈,函数执行的时候先去它的词法环境去找对应的变量名,找不到再去变量环境中找,再找不到就去它的下一级去寻找直到找到为止,然后函数执行完成后再将其执行上下文销毁,避免栈溢出,我们用一个代码来举例:


iwEcAqNwbmcDAQTRB4AF0QQ4BrAXVDtt0dImQAU9UGjZw-8AB9IZAt15CAAJomltCgAL0gAC84A.png_720x720q90.jpg


执行函数add时,先去add的执行上下文中寻找b,b在add的变量环境中,但是并没有a,于是再去全局执行上下文中按照词法环境和变量环境的顺序去找,找到了a,最终返回a+b=12。


作用域链


调用栈在生成执行上下文时会默认在变量环境中产生一个outer,它指向该函数的外层作用域,函数声明在哪里,哪里就是函数的外层作用域,然后形成一个作用域链。


我们再来看下一个案例



调用foo的时候生成了foo的执行上下文,foo的函数体中有bar的调用,所以又生成了一个bar的执行上下文,bar声明在最外面,所以它的outer指向全局执行上下文,因此当bar在寻找myName这个变量的时候直接跳过foo去了全局执行上下文,所以最终输出的结果是万总


iwEcAqNwbmcDAQTRB4AF0QQ4BrDMeEmBKzv5FAU9V3BEkJoAB9IZAt15CAAJomltCgAL0gADbJY.png_720x720q90.jpg


闭包


了解完调用栈和作用域链之后,就可以进入我们今天的主题闭包了,还是用一个案例来说明



函数a的函数体中声明了一个函数b,并且函数a的结果是返回了函数b


var c = a() 先调用a,并且把a的返回值赋给c,因此c就是一个函数,然后再调用c,这就是整个的执行过程。在调用完a后,a的函数体已经全部执行完毕,应该被销毁,但是在调用c的时候(c就是函数b),需要用到a中的变量,因此在销毁掉a的执行上下文的同时会分出一个区域用来存储b中所需要用到a的变量,这个存储了count的地方就叫做闭包。


iwEcAqNwbmcDAQTRB4AF0QQ4BrCKgRsdcT9JIgU9Y1plkyAAB9IZAt15CAAJomltCgAL0gACzhQ.png_720x720q90.jpg


因此闭包的概念就是:


即使外部函数已经执行完毕,但是内部函数引用了外部函数中的变量依然会保存在内存中,我们把这些变量的集合,叫做闭包


现在我们再回到第一个问题,如何让它输出0到9,很显然,就是在for的内部形成一个闭包,让i每次可以叠加存在内存中,因此代码如下:



这样一层一层把从外到内的去了解闭包,是不是就更容易了呢,你学会了吗?


作者:欣之所向
来源:juejin.cn/post/7300063572074201125
收起阅读 »

自定义注解实现服务动态开关

🧑‍💻🧑‍💻🧑‍💻Make things different and more efficient 接近凌晨了,今天的稿子还没来得及写,甚是焦虑,于是熬了一个夜也的给它写完。正如我的题目所说:《自定义注解实现服务动态开关》,接下来和shigen一起来揭秘吧。 ...
继续阅读 »

🧑‍💻🧑‍💻🧑‍💻Make things different and more efficient


接近凌晨了,今天的稿子还没来得及写,甚是焦虑,于是熬了一个夜也的给它写完。正如我的题目所说:《自定义注解实现服务动态开关》,接下来和shigen一起来揭秘吧。




前言


shigen实习的时候,遇到了业务场景:实现服务的动态开关,避免redis的内存被打爆了。 当时的第一感受就是这个用nacos配置一下不就可以了,nacos不就是有一个注解refreshScope,配置中心的配置文件更新了,服务动态的更新。当时实现是这样的:


在我的nacos上这样配置的:


 service:
  enable: true

那对应的java部分的代码就是这样的:


 class Service {
   @Value("service.enable")
   private boolean serviceEnable;
   
   public void method() {
     if (!serviceEnable) {
       return;
    }
     // 业务逻辑
  }
 }

貌似这样是可以的,因为我们只需要动态的观察数据的各项指标,遇到了快要打挂的情况,直接把布尔值换成false即可。




但是不优雅,我们来看看有什么不优雅的:



  1. 配置的动态刷新是有延迟的。nacos的延迟是依赖于网络的;

  2. 不亲民。万一哪个开发改坏了配置,服务就是彻底的玩坏了;而且,如果业务想做一个动态的配置,任何人都可以在系统上点击开关,类似于下边的操作:


服务开关操作


element-UI的动态开关


nacos配置的方式直接不可行了!


那给予以上的问题,相信部分的伙伴已经思考到了:那我把配置放在redis中呗,内存数据库,直接用外部接口控制数据。


很好,这种想法打开了今天的设计思路。我们先协一点伪代码:


 @getMapping(value="switch") 
 public Integer switch() {
     Integer status = redisTemplate.get("key");
     if (status == 1) {
       status = 0;
    } else {
       status = 1;
    }
     redisTemplate.set("key", status);
     return status;
 }
 
 
 @getMapping(value= "pay")
 public Result pay() {
   Integer status = redisTemplate.get("key");
   if (status ==0) {
     throw new Bizexception("服务不可用");
  } else {
     doSometing();
  }
 }

貌似超级完美了,但是想过没有,业务的侵入很大呢。而且,万一我的业务拓展了,别的地方也需要这样的配置,岂不是直接复制粘贴?那就到此为止吧。




我觉得任何业务的设计都是需要去思考的,一味的写代码,做着CRUD的各种操作,简直是等着被AI取代吧。


那接下来分享shigen的设计,带着大家从我的视角分析我的思考和设计点、关注点。


代码设计


注解设计


 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 public @interface ServiceSwitch {
 
     String switchKey();
 
     String message() default "当前业务已关闭,请稍后再试!";
 
 }

我在设计的时候,考虑到了不同的业务模块和失败的信息,这些都可以抽取出来,在使用的时候,直接加上注解即可。具体的方法和拦截,我们采用spring的AOP来做。


常量类


 public class Constants {
 
     public static final String ON = "1";
     public static final String OFF = "0";
 
     public static class Service {
 
         public static final String ORDER = "service-order";
         public static final String PAY = "service-pay";
    }
 
 }

既然涉及到了业务模块和状态值,那配置一个常量类是再合适不过了。


业务代码


   @ServiceSwitch(switchKey = Constants.Service.PAY)
   public Result pay() {
       log.info("paying now");
       return Result.success();
  }

业务代码上,我们肯定喜欢这样的设计,直接加上一个注解标注我们想要控制的模块。


请注意,核心点来了,我们注解的AOP怎么设计?


AOP设计


老方式,我们先看一下代码:


 @Aspect
 @Component
 @Slf4j
 public class ServiceSwitchAOP {
 
     @Resource
     private RedisTemplate redisTemplate;
 
     /**
      * 定义切点,使用了@ServiceSwitch注解的类或方法都拦截 需要用注解的全路径
      */

     @Pointcut("@annotation(main.java.com.shigen.redis.annotation.ServiceSwitch)")
     public void pointcut() {
    }
 
     @Around("pointcut()")
     public Object around(ProceedingJoinPoint point) {
 
         // 获取被代理的方法的参数
         Object[] args = point.getArgs();
         // 获取被代理的对象
         Object target = point.getTarget();
         // 获取通知签名
         MethodSignature signature = (MethodSignature) point.getSignature();
 
         try {
 
             // 获取被代理的方法
             Method method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
             // 获取方法上的注解
             ServiceSwitch annotation = method.getAnnotation(ServiceSwitch.class);
 
             // 核心业务逻辑
             if (annotation != null) {
 
                 String switchKey = annotation.switchKey();
                 String message = annotation.message();
                 /**
                  * 配置项: 可以存储在mysql、redis 数据字典
                  */

                 String configVal = redisTemplate.opsForValue().get(switchKey);
                 if (Constants.OFF.equals(configVal)) {
                     // 开关关闭,则返回提示。
                     return new Result(HttpStatus.FORBIDDEN.value(), message);
                }
            }
 
             // 放行
             return point.proceed(args);
        } catch (Throwable e) {
             throw new RuntimeException(e.getMessage(), e);
        }
    }
 }

拦截我的注解,实现一个切点,之后通知切面进行操作。在切面的操作上,我们读取注解的配置,然后从redis中拿取对应的服务状态。如果服务的状态是关闭的,直接返回我们自定义的异常类型;服务正常的话,继续进行操作。


接口测试


最后,我写了两个接口实现了服务的调用和服务模块状态值的切换。


 @RestController
 @RequestMapping(value = "serviceSwitch")
 public class ServiceSwitchTestController {
 
     @Resource
     private ServiceSwitchService serviceSwitchService;
 
     @GetMapping(value = "pay")
     public Result pay() {
         return serviceSwitchService.pay();
    }
 
     @GetMapping(value = "switch")
     public Result serviceSwitch(@RequestParam(value = "status", required = false) String status) {
         serviceSwitchService.switchService(status);
         return Result.success();
    }
 }

代码测试


测试服务正常


服务状态正常情况下的测试


此时,redis中服务的状态值是1,服务也可以正常的调用。


测试服务不正常


我们先调用接口,改变服务的状态:


调用接口,切换服务的状态


再次调用服务:


服务模块关闭


发现服务403错误,已经不能调用了。我们改变一下状态,服务又可以用了,这里就不做展示了。


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

程序员什么时候感觉到编程能力突飞猛进?

我是一个41岁老程序员,从 2007年硕士毕业参加工作以来,已经在编程一线工作16年了。平时一直有逛技术社区的习惯。最近在社区看到一个讨论,题目是《你什么时候感觉到编程能力突飞猛进》。我觉得这个题目挺有意思,于是借这篇文章,也来聊聊自己的程序人生。 我觉得自己...
继续阅读 »

我是一个41岁老程序员,从 2007年硕士毕业参加工作以来,已经在编程一线工作16年了。平时一直有逛技术社区的习惯。最近在社区看到一个讨论,题目是《你什么时候感觉到编程能力突飞猛进》。我觉得这个题目挺有意思,于是借这篇文章,也来聊聊自己的程序人生。


我觉得自己编程能力突飞猛进,应该是分了几个阶段吧。


第一阶段:研究生时期封闭式的项目开发阶段


我 2004 年电子科技大学计算机科学与技术本科毕业,同年留在母校,继续攻读计算机系统结构的硕士学位。研一时上了很多专业必修课,比如《Unix 系统设计》,《Unix 环境下的高级编程》,《Unix 网络编程》等等。但是这些书本上的知识,我也只是课堂上跟着老师过了一遍,缺乏动手实战。研一在校园里,也只是完成这些教材的课后作业而已。我当时具体有多菜?连 GDB 调试器都不熟,代码执行出错,我只会用 printf 大法来定位问题。


研二时我们教研室的同学们,在当时段翰聪段博的带领下,到北京声学所高性能网络实验室做项目。白天在实验室,晚上九点之后才返回海淀区知春路的学生宿舍休息,算是一年的封闭式开发吧。当时教研室承接的项目是国家发改委 CNGI 专项基金子课题——基于 IPv6 的 P2P 弹性重叠网络智能节点的研制。段博当时还在攻读他的博士学位,他现在已经是电子科技大学计算机科学与工程学院的教授和博士生导师了。


这一年的封闭式开发,我最先的任务是负责实现一款基于 P2SP 协议的负载发生器,作为测试工具,来验证项目研制的网络智能节点的各项性能指标和参数。刚开始动手编写项目代码时,我就立即感受到这种大型项目和校园里编写的那些课后作业代码的具体差异。我的程序代码量稍稍一上去,一运行就 Segmentation Fault,然后我就不知所措了。还好项目组里有段博这种大神,还有教研室其他优秀的同学帮助我。这一年我们可以说是心无旁骛,每天除了项目编码,技术讨论,然后就是找一些项目相关的论文来阅读,再有就是网上逛技术社区。慢慢的我也熟练掌握了 GDB 的用法,遇到问题也知道如何通过单步调试去定位问题,根据程序运行出错消息用搜索引擎去寻找解决方案,向身边伙伴求助的次数也明显减少了。



第二阶段:工作时大量研读其他高手同事的代码


2007 年研究生毕业后,我进入了 SAP 成都研究院工作。我本科和研究生生涯,使用的都是 C/C++, 工作中换成了 SAP 独树一帜的 ABAP. 面对技术栈的切换,编程经验尚浅的我可谓是苦不堪言,ABAP 在我手中用起来觉得各种别扭。有一次,ABAP 一个浅拷贝和深拷贝的问题困扰了我几天,我居然还写邮件,给 SAP 成都研究院我熟识的几位 ABAP 高手同事吐槽,说某某需求如果用 C/C++ 实现那是分分钟的事,可现在换成 ABAP,得绕来绕去。



现在回忆起来,我当时的行为挺可笑。那几位高手同事收到邮件后没有回复,只有一位当时外号薛老板的同事,不仅认真给我指出我邮件里关于 ABAP 实现深拷贝的错误,邮件末尾还点评到:“从 ABAP 转 C++/Java 很难,但是从 C++/Java 转 ABAP 开发很简单。”


虽然当时 SAP 开发社区尚不如现在完善,2007 年的时候,网络上 ABAP 开发资源也没有现在丰富。不过 ABAP 的开发编程环境在服务器端,这使得我能轻松阅读到服务器上其他 ABAP 高手的代码。


抱着熟读唐诗三百首,不会作诗也会吟的心态,我把我认识的很多 ABAP 高手的工作代码看了一个遍。不光看,还把他们的代码拷贝出来,自己修改,然后单步调试,边调试边学习。遇到不懂的知识点,直接按 F1 召唤出帮助文档学习。



通过大量的阅读,我发现程序员与程序员之间还是存在细微的编码风格差异。比如 William 是当时在 SAP 成都研究院工作过的一位天才程序员,他的很多非生产代码,都是用面向过程的编程方式编写,并且变量命名风格颇有谭浩强 C 语言程序设计那本书里配套源代码的风采。Willian 程序里数量众多的 include 和神秘的变量命名规则,庞大的代码量,但是最后程序仍然能够极其精巧地运行,成功实现极其繁复的需求,这一切让我佩服不已。


Annie 是另一个让我极其佩服的 ABAP 程序媛。她的代码生产速度让我惊叹,而且代码规范工整,犹如教科书一般。当时 SAP 成都研究院 On Demand 交付项目使用的 CPMS(Content Production Management System) ,前身是 CR(Content Repository) 系统,这个系统的主要开发人员就是 Annie. 她对 CR 系统的贡献,给我提供了大量可供学习和模仿的素材。当时在佩服之余,我心中也有一个疑问:Annie 是如何做到短时间内写出海量高质量代码的?要知道那可是在 2007 年,那时既没有 ABAP 代码生成向导,也没有 ChatGPT. 我当时性格腼腆,也没好意思去问她,这个遗憾就一直留到现在了。


就这样一头扎进 ABAP 代码海洋之后,慢慢的我工作中对 ABAP 的运用也得心应手起来,和自己刚进 SAP 成都研究院时相比,ABAP 编程能力可以说是突飞猛进,这可能就是量变到质变吧。



第三阶段:2014 年接触 JavaScript/HTML/CSS


2007~2014年,我做了七八年的 ABAP 开发,2014 年底,我觉得自己算是这个领域的专家了,此时工作岗位变动,需要接收 SAP UI5 应用开发,使用的技术栈从 ABAP 转成了 JavaScript 和 HTML/CSS. 此前我从未在工作中接触过基于 JavaScript 的 Web 前端开发。和之前刚毕业工作时从 C++ 转 ABAP 一样,我在刚接触 SAP UI5 开发时,又失去了对 ABAP 得心应手那种感觉。


我当时的做法是,从源代码实现的层面研究 SAP UI5 这个前端框架,研究它的工作原理,顺便把 JavaScript 也学习了。我也在技术社区上发布了《深入学习 SAP UI5 框架代码系列》,从 SAP UI5 Module 的懒加载机制,控件渲染机制,事件处理机制,元数据实现机制,实例数据的读写实现原理,数据绑定的实现原理等方面,通过分析 SAP UI5 框架的 JavaScript 源代码实现,介绍了我对这个前端开发框架的理解。



从框架一行行代码的研读,我也领略了从事应用开发和框架开发的不同侧重点和编程技巧,我这个系列总共写了 14 篇文章。漫长的框架源代码研读和文章写作完成之后,我感到自己 Web 应用的编程能力再次突飞猛进。


展望未来


以 ChatGPT 为代码的 AIGC 工具的流行,为程序员再次提供了编程能力突飞猛进的机会。善用 AIGC 工具,我们可以提高自己对陌生编程领域的学习速度,在学习遇到障碍时,善用这些工具,能够帮助我们克服学习过程中遇到的各种困难。


愿每一位程序员同行都能在编程中找到快乐。



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

我被这奇葩的答辩评价给惊呆了

最近组里有个小伙伴晋升,我司职级跟腾讯的不一样,可以理解为大概是要晋升高工(T9)吧。 据我了解,我司的晋升答辩还不成熟,没有统一规范和套路,那我就以腾讯的经验来辅导我的小伙伴吧。我想,万变不离其宗,只要能论证能力达标就可以了吧,结果,我着着实实地被这个奇葩的...
继续阅读 »

最近组里有个小伙伴晋升,我司职级跟腾讯的不一样,可以理解为大概是要晋升高工(T9)吧。


据我了解,我司的晋升答辩还不成熟,没有统一规范和套路,那我就以腾讯的经验来辅导我的小伙伴吧。我想,万变不离其宗,只要能论证能力达标就可以了吧,结果,我着着实实地被这个奇葩的答辩评价给雷到了。


插图1.jpeg


上周答辩,我跟我领导全程旁听。先不管我的小伙伴的答辩内容、评委提问对答表现、临场发挥怎么样,直接快进,跳到最后评委合议之后的答辩总结环节。



原话我肯定不记得了,就按照大概意思来描述



“先说说 XX 的优点。XX 的答辩有两个很突出的优点,”


“第一个是体现出来很好的产品化思维,整体的架构设计把握得比较好,能够解释地比较清楚;”


“第二个是问题解决能力比较好,能够结合业务的情况去思考方案的优缺点,进行合理的决策。”


“但 XX 的答辩有个很明显的问题。”


“这个项目没有很好地体现前端的技术(深度)。”【我当时的反应:what?评委,麻烦你再说一次?】


“这么说吧,这是前端的通道答辩,这个项目放在前端的通道答辩是不太合适的这个项目对前端岗位的挑战是不太够的。”【我当时的反应:!?我不是很懂你在说什么,我进错会议室了?】


换种说法,这个项目拿去给 QA 也是能做出来的。”【这一句是原话,因为这句话是印象最深刻的。】


这句话真得把我给整无语了。


首先,3 个评委都是前端,之前还会有后台的评委,今年终于“规范”了,全部前端了。照我说,还不如来后台评委呢,不至于说这种话,真给整破防了。


其次,这些评委全部是比我高 1、2 级的,这职级居然能说出这种话。


所以,我挺想知道,什么是前端技术?什么是前端专属的技术?


按照评委们的说法,


那不要拿后台全栈项目去答辩了,后台也是能做出来的。


那不要拿 app 跨端项目去答辩了,app 客户端也是能做出来的。


那不要拿 pc 跨端项目去答辩了,pc 客户端也是能做出来的。


那不要拿 devops 项目去答辩了,谁都能做出来的。


那不要拿基建项目去答辩了,infra 也是能做出来的。


所以,前端技术就只剩切图仔了呗????搞搞组件库?搞搞页面性能优化?


问题是这玩意儿能搞出花儿来?真想搞出花儿来不得再造一个 React?真要搞出个 React 出来能呆在这儿?


哎,真要被憋出内伤来了,还是得写出来释放一下。


我司答辩还没到卷部门影响力的地步,那全凭答辩评委个人喜好我也忍了,但这样的认知我实在是接不住呀。


哎,止血止血,打住打住,给我一周时间缓一缓,缓一缓......


作者:潜龙在渊灬
来源:juejin.cn/post/7300918873904824331
收起阅读 »

那个年薪 201万 的华为 “天才少年” 真面目被曝光,醒醒吧,他根本就不是天才!

综合自网络有的人为了买房掏空家里6个钱袋,此后几十年被捆绑在房贷上,有的人自己奋斗几年,甚至是一两年就可以全款入手。今天介绍一个高考落榜,复读一年才进一所三本院校,最后成功逆袭成为年薪201万的华为“天才少年”的故事。你以为201万已经是他的极限了,其实更牛的...
继续阅读 »
综合自网络
有的人为了买房掏空家里6个钱袋,此后几十年被捆绑在房贷上,有的人自己奋斗几年,甚至是一两年就可以全款入手。
今天介绍一个高考落榜,复读一年才进一所三本院校,最后成功逆袭成为年薪201万的华为“天才少年”的故事。
你以为201万已经是他的极限了,其实更牛的还在后面。
他曾拒绝腾讯和阿里,甚至是世界巨头IBM的offer,有的大厂甚至开出了360万年薪。
看到的这样的消息,网友的态度很统一:
这位“天才少年”叫张霁,彼时的他刚博士毕业,不过如今他的过往经历被曝光,人们震惊地发现他的真面目:根本不是什么天才!
复读才考上三本,最后成功逆袭的原因
张霁最高学历是985高校华中科技大学计算机专业博士学位,但退回几年前,他还是别人眼中的“失败者”。

他的父母职业都是教师,对他的学习成绩没有作过多要求,重点培养他独立思考的能力,比如儿时他看上一个价值50块钱的玩具,父母只会给他45块钱,剩下的5块要他自己想办法,小张霁会通过卖废品的方式攒到钱去购买心爱的玩具。

通过这样的教育,培养了他善于思考和动手解决问题的能力,但是张霁并没有重视成绩,因此平时成绩不温不火,直到高考那年,连大学都没考上。

在之后伴随的“这个孩子完了”、“当个技工”评价声中,痛定思痛,决定复读。努力追赶了一年,奇迹并没有发生,他考上了一所当地人都很少知道的三本民办院校——武昌理工学院。

谁能想到,三本只是他的起点,他选择了并不热门的专业——计算机,在往后的几年里,他会继续完成他的人生逆袭,
当时学校氛围不必多说,大家基本每天都在吃喝玩乐,努力学习不仅是异类,甚至会被取笑。
张霁就在这样的环境中定下了考研考博的目标,并坚持不懈的在图书馆啃难嚼的书本,最终功夫不负有心人的考上了武汉邮电科学研究院的研究生。
3年后又考上了自己的理想大学华中科技大学博士——这里是武汉光电国家研究中心,更是整个湖北省唯一的国家级实验室。
读博期间他取得了许多突破,并在众多国际一流刊物发表相关学术论文,包括ATC, DAC,ICPP等行业顶级会议和期刊。
每篇都是国际top级别,刊登难如登天,是无数科研人毕生的追求,重点是许多成果可以直接落地。
此外,他还在腾讯实习期间拿到了腾讯2016、2017年度杰出贡献奖,2019年度最佳卓越运营奖。
最后选择了华为,是因为华为正在受到“制裁”,他知道可以在华为找到志同道合的人,一起努力渡过难关。
他有一句座右铭:“很多人比你还要努力,你有什么理由不上进”。
所有的成功都有努力后的水到渠成
入选华为“天才少年”并不容易。
“众所周知,华为有资源池。”知乎上一名匿名的华为员工介绍称,例如,某产品线申报人力缺口200人,那么一般会从简历投递者中按学校、专业等筛选出大约2000份简历。
笔试、面试通过大概500人,放进资源池。
然后再按笔试、面试综合成绩发放200份offer,最终有100人签约,然后再按顺序发放100份offer……直到offer签完。
哪有什么天才少年,不过是一群怀揣梦想、坚韧不拔的人在苦苦地熬。
吃够了苦,熬到了头,生活才有可能对你网开一面。
张霁的逆袭故事并不是“天才少年”里的个例。
沈树忠,家境贫寒,世代都是农民出身。第一次高考成绩化学只有5分,物理0分。第二次高考仅仅读了中专,毕业后当了个“挖煤工人”,20岁自学考研,历时6年拿到硕士博士学位,如今是中国科学院院士,并拿下了地层学国际最高金奖,成为中国获此奖项第一人。
还有物理5分、化学5分,数学15分,却要挑战物理系,让爱因斯坦赞叹过的世界火箭、宇航工程的开拓者,“中国力学之父”,他叫钱伟长。
同样有201万年薪的左鹏飞,也不是大家眼中的“天选之子”。
他说:“我只是把别人打游戏的时间,花在了实验室里。”
......
说实话,我觉得“天才少年”这个项目名字虽然吸引人,但它给很多日以继夜的努力才得到的结果加上了一层光芒四射的滤镜,让普通人可望而不可即。
它容易让人们忽略那些努力的重要性。
但其实,哪有那么玄乎。这世界上99.9%的人远没有到拼智商的时候,有的不过是找到自己为之努力的方向、去坚持然后得到。
于是,年龄大的叫大器晚成,年龄小的叫天才,我更愿意称其为逆袭。
薛兆丰:“我们每个人,都在为自己的简历打工。”
知乎上有这样一个提问:
“为什么大多数人宁愿吃生活的苦,也不愿吃学习的苦?”
点赞最高的答主@特雷西亚是这样说的:
“生活的苦难可以被疲劳麻痹,被娱乐转移,无论如何只要还生存着,行尸走肉也可以得过且过,最终习以为常,可以称之为钝化。 
学习的痛苦在于,你始终要保持敏锐的触感,保持清醒的认知和丰沛的感情,这不妨叫锐化。”
生活的苦,会让人麻木,习以为常;学习的苦,让人保持尖锐的疼痛感。
人生是一条漫长的旅程,但关键的就那么几年。
生活其实并不算太苦,苦的是在该努力的年纪,你却选择了放纵和逃避。
真正的苦,是被命运扼住了咽喉,无法动弹,没有出路。
《奇葩说》里辩手肖骁有一句辩词我特别喜欢:
往往最诱惑的选择,不是上帝给你的机会,而是恶魔给你的考题。
在向上攀登的路上,我们会遇到无数选择题:
安逸还是改变?主动出击还是随波逐流?
我只有一个建议:在人生的十字路口,永远选择正确但困难模式。
因为好走的路,都是下坡路。
所以现在开始学习起来吧,定好目标,规划好人生的每一步,低级的快乐靠放纵,顶级的快乐靠自律。
---END---

作者:程序员直聘
来源:mp.weixin.qq.com/s/0TKWfX7-HvrgFs8xowr5sw
e>

收起阅读 »

谈传统艺术的没落

最近想学快板。 现在这玩意儿竟然也算个冷门了,周边找不到人教。 怎么整?上网看视频,找帖子。 有个帖子就说有一个QQ群,里面有很多教程啊,板谱之类的。 我就加群了,等了三天,终于给通过了。 看资料,QQ群的标签是【60后】、【70后】。我想,这些半大老头是怎么...
继续阅读 »

最近想学快板。


现在这玩意儿竟然也算个冷门了,周边找不到人教。


怎么整?上网看视频,找帖子。


有个帖子就说有一个QQ群,里面有很多教程啊,板谱之类的。


我就加群了,等了三天,终于给通过了。


看资料,QQ群的标签是【60后】、【70后】。我想,这些半大老头是怎么成功建群的(群主已经是古稀之年),同时感慨我国互联网的普及程度之高。


待了一周,群里1000人,没有人发言,除了一些某某入群,不要发广告的公告等等这类通知。


忽然有一天,一个刚入群的小弟打破了宁静,吓得我都不敢登QQ了。



【吐槽】小弟


今年公司年会让我出节目~请各位老师指点一下~没有多长时间了~怎么样短时间内能练出来

我这着急呢


 


【管理员】大佬


临时抱佛脚、为了什么年会“应景”,零基础就想短期之内学会快板的,这些朋友,咱能换个别的练么?放过快板行么?知道这里有多不容易么?眼高手低会害你一辈子


我说的这些,肯定会招惹一部分人不高兴


这门艺术不简单


但凡快板打的、说的达到一定水平的,都是有毅力、坚韧之人


这门艺术需要用时间来不断积累


不断学习不断修正不断钻研,然后,一辈子


单点、双点、基本点、混合点,最基础的东西,有多少人还打不利索呢


更别说吐字归音、用气发声,上板演唱,表演表现,手眼身法步,舞台掌控以及改编、创作了


手眼身法步

每一样都不容易


【活跃】二哥 

表演是最难的


【管理员】大佬 

其实最难的到不是表演

最难的是思想、情感


简单说就是,理解力

任何想表达、表演出来的东西

都是先有一个想法

举个例子

大家很喜欢武松打店


前头对话

武松他闻听一瞪眼……


贤弟你们说话理不合

这是武松的开脸,如何表演表现?

他是什么内心情感?

他的情绪是高是低?


穿的什么?

戴的什么?


跟董平薛霸是什么关系?

用什么语气?

什么眼神?

手放哪儿?

脖子上可是带着枷锁呢


怎么塑造

等等等等

这些只有你先理解到、想到

才可能去表现、表演


当然即便想到了,也需要大量的练习


给自己的“定位”很关键,自己要达到什么艺术水准?如果你的定位是“成为一名快板爱好者”,那么好,你的水平一定是业余中比较“业余”的……如果你的定位是“具有专业水平”的爱好者,那么你的水平一定是业余爱好者中的高水平。所以说,也别埋怨没人教啊、别人教的不行啊,什么什么的,第一,快板并不适合所有人学习(这是说要达到一定的程度,起码有模有样);第二,多找自己的原因


【话唠】三哥


不要埋怨别人不教,别人没有义务去教



这是一个90后沉入70后的世界,进行放肆的请教,结果被众人教育了的事件。


如果某个70后,不慎落入90后的世界,进行无端的说教,估计下场也不会太乐观。起码会收到几百兆的斗图。


回过头来看,一个新人想在公司年会上表演快板,他认为呱嗒呱嗒很好学,又很别致,于是找了个交流群去问问。结果群里反馈你别糟蹋快板了,这是一辈子的艺术,不要包含功利心,它这么的高深,别人也没有义务教你。


再回过头来看,“老先生”们说的确实没错。快板确实不好学,里面有很多的表演技巧和艺术素养,想要有模有样地表演确实很难。



新人的需求是:我如何以最短的时间学会快板表演?


对方的回复是:呸!白日做梦!



这件事情,让我想起来很多人找我做软件。


我老是感觉对方的诉求,达不到开发的标准,以至于解决不了他的问题。


比如,对方想做个馒头,要求里面要有馅儿,馅儿可以是肉的,可以是豆沙的。我说,那你是要馒头还是要包子,或者豆沙包。


对方说,就是个馒头,可以放馅儿,不用点菜了。


我说,算了吧,做不了,我会做馒头,会做包子,但是不会做带有包子特征的馒头。


结果,对方最后找别人做出来了,跟我说你看就是这样子。我一看,就是一个包子。


你会感觉,自己是不是在坚守一些什么,比如《新华字典》里的字词释义之类的东西。


所以,那个新人问,怎么短时间内,在公司年会表演快板。我觉得,不一定要打成李润杰大师那样,也不用单点、双点、混合点纵横交错。没有开场板也无所谓,找一段趣味性强的文本,掌握好节奏,嘀嗒,嘀嗒,一个节奏一句话,在非商演的情况下,也不算是糊弄人,年会的场合不就图个乐呵嘛,演好了演砸了都是个喜剧。


老先生又说了,你那个不叫正宗的快板,快板一定要先有开场板,开场板一定要是“哒嘀咯嘀哒嘀嘀哒”。


老先生息怒,您那正宗快板,能帮小弟解决,五天后,年会表演的问题吗?


为什么非要帮他解决问题?


问得好。


《中国有嘻哈》rap火了,为什么快板没有火?


《一人我饮酒醉》喊麦火了,为什么快板没有火?


仔细看看rap,喊麦,快板这三者,其实从表演形式上是比较接近,是很容易混淆的。


现在人们精神压力都很大,你需要情感发泄释放。


看一个rap,点头哈腰,观众跟着节奏也能点点头,代入感很强。看完了,心情大好。


来一段喊麦,一人我饮酒醉,醉把佳人成双对……是不是跟着喊出来了。


那天一个小孩从喷泉池子上跳下来,喊着:败帝王,斗苍天,夺得皇位已成仙。吓了我一跳,我以为我遇到神仙了。


这个调子是标准的123、123、1234567。数来宝的调子:打竹板,进街来,住户买卖两边排。也有买,也有卖,也有幌子和招牌。


为什么人家火了,你没有火。


我觉得就是坚守的东西太多了。



  1. 形式上,太固定。“哒嘀咯嘀哒嘀嘀哒”对了。“哒咯咯嘀哒嘀嘀哒”错了。全国一个样。不这样,就不是快板。

  2. 内容上,太陈旧。“华蓥山,巍峨耸立万丈多,嘉陵江水,滚滚地东流像开锅。”当年听,可能有共鸣。但是现在人听着没有啥感觉了。另外,第二字念什么?

  3. 思想上,太封闭。自认为这门艺术造诣很高,自己把终身奉献给它,别人也得这样,不允许其他人有丝毫地轻视,并且建立起一道壁垒,阻挡想来尝试的新人。对于传承,坚持宁缺勿滥。就跟谈对象一样,两个人一见面,女的说,除非你忠于我一辈子,我才和你交往。小伙子:你呀?我呀?


我的父辈们,他们小时候边跑边唱的是:闲言碎语不多讲,表一表山东好汉武二郎。因为,他们别无选择。


我的子辈们,他们边跑边唱的千差万别,因为他们不知道怎么选择。谁能解决自己问题,比如好记又能装逼,他们选择谁。


曾经一个深入民间的艺术,随着时代的发展变得鲜为人知,然而它却丝毫没有一点改变。


或许也正是这种坚持,才保持了它的原汁原味。同时,伴随着的,也是它变成了一片标本,靠着国家的文化扶持资金传承。


其实,它是可以自己繁殖的。


原来觉得穿着大褂说单口相声受约束的人,换成西装,说了脱口秀。


原来打不好快板,但是节奏掌握的还挺好的人,带上帽子,唱起了rap。


为什么传统艺术发展不起来?


做煤油灯的,坚持用煤油当燃料,用柴油都不行,电灯更谓之不伦不类。等到这个世界不再生产煤油,他才放弃做灯。他依然还在坚持煤油灯正宗,而大家都在用LED。


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

为何在中国MySQL远比PostgreSQL流行

首先在全球范围内,MySQL 一直是领先于 PostgreSQL (下文简称 PG) 的。下图是 DB-Engines 的趋势图,虽然 PG 是近 10 年增长最快的数据库,但 MySQL 依然保持着优势。再来看一下 Google Trends 过去一年的对比...
继续阅读 »

首先在全球范围内,MySQL 一直是领先于 PostgreSQL (下文简称 PG) 的。下图是 DB-Engines 的趋势图,虽然 PG 是近 10 年增长最快的数据库,但 MySQL 依然保持着优势。

再来看一下 Google Trends 过去一年的对比
MySQL 也依然是明显领先的。而进一步看一下地域分布的话
绝大多数地区依然是 MySQL 领先,份额对比在 60:40 ~ 70:30 之间;少数几个国家如俄罗斯不分伯仲;印度的对比是 85:15;而中国则是达到了 96:4,也是 Google Trends 上差异最明显的国家。
笔者从 2009 年左右开始学习数据库相关知识,接触到了 MySQL 5.1 和 PG 8.x。而深度在工作中使用则是 2013 年,那时加入 Google Cloud SQL 开始维护数据库,MySQL 从 5.5 开始,到之后 2017 年 Cloud SQL 推出了 PG 服务,从 9.6 开始,后来一直同时维护 Google 内部的 MySQL 和 PG 分支,也就一直关注着两边的发展。18 年回国后,进一步熟悉了国内的生态。
下面就来尝试分析一下 MySQL 在中国流行度遥遥领先于 PG 的原因。

Windows

MySQL 在 1998 年就提供了 Windows 版本,而 PostgreSQL 则到了 2005 年才正式推出。之前读到的原因是 Windows 早期的版本一直无法很好支持 PostgreSQL 的进程模型。

上手门槛

MySQL 上手更简单,举几个例子:
  1. 连 PG,一定需要指定数据库,而 MySQL 就不需要。psql 大家碰到的问题是尝试连接时报错 FATAL Database xxx does not exist。而 mysql 碰到的问题是连接上去后,执行查询再提示 no database selected。

  2. 访问控制的配置,首先 PG 和 MySQL 都有用户系统,但 PG 还要配置一个额外的 pg_hba (host-based authentication) 文件。

  3. MySQL 的层级关系是:实例 -> 数据库 -> 表,而 PG 的关系是:实例(也叫集群)> 数据库 > Schema > 表。PG 多了一层,而且从行为表现上,PG 的 schema 类似于 MySQL 数据库,而 PG 的数据库类似于 MySQL 的实例。PG 的这个额外层级在绝大多数场景是用不到的,大家从习惯上还是喜欢用数据库作为分割边界,而不是 schema。所以往往 PG 数据库下,也就一个 public schema,这多出来的一层 schema 就是额外的负担。

  4. 因为上面机制的不同,PG 是无法直接做跨库查询的,早年要通过 dblink 插件,后来被 FDW (foreign data wrapper) 取代。

  5. PG 有更加全面的权限体系,数据库对象都有明确的所有者,但这也导致在做测试时,更经常碰到权限问题。
虽然 PostgreSQL 的设计更加严谨,但也更容易把人劝退。就像问卷设计的一个技巧是第一题放一个无脑就能答上来的二选一,这个的目的在于让对方开始答题。

性能

最早 Google 搜索和广告业务都是跑在 MySQL 上的,我读到过当时选型的备忘。其实一开始团队是倾向于 PG 的(我猜测是 PG 的工程质量更加符合团队的技术品味),但后来测试发现 MySQL 的性能要好不少,所以就选型了 MySQL。
现在两者的性能对比已经完全不一样了,而且性能和业务关联性很强,取决于 SQL 复杂度,并发,延迟这些不同的组合。目前在大部分场景下,MySQL 和 PG 的性能是相当的。有兴趣可以阅读 Mark Callaghan (https://smalldatum.blogspot.com/) 的文章。


互联网

最重要的是 LAMP 技术栈,Linux + Apache + MySQL + PHP,诞生于 1998 年,和互联网崛起同步,LAMP 技术栈的普及也带火了 MySQL。这个技术栈的绑定是如此之深,所以时至今日,MySQL 官方客户端 MySQL Workbench 也还是不及 phpMyAdmin 流行。


大厂的号召力

前面提到的 Mark Callaghan 一开始在 Google 的 MySQL 团队,他们给生态做了很多贡献,后来 Google 内部开始用 Spanner 替换 MySQL,Mark 他们就跑到了 Facebook 继续做,又进一步发展了 MySQL 的生态,像当时互联网公司都需要的高可用方案 MHA (Master High Availability) 就是 Mark 在 FB 时期打磨成熟的。当时整个互联网技术以 Google 为瞻,传播链差不多是 Google > Facebook / Twitter > 国内互联网大厂 > 其他中小厂。MySQL 在互联网公司的垄断就这样形成了。
相对的,那段时间 PG 有影响力的文章不多,我唯一有印象的是 Instagram 分享他们 sharding 的方案,提到用的是 PostgreSQL (https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c)。


生态

有了大量使用后,自然就有人去解决碰到的各种问题。先是 InnoDB 横空出世,解决了事务和性能问题。主从,中间件分库分表方案解决了海量服务的扩展和高可用问题。各种 MySQL 相关书籍,培训资料也冒了出来,应该不少人都读过高性能 MySQL (High Performance MySQL) 这本书。
业界有 Percona 这样专注于做 MySQL 技术咨询的公司,他们还研发了一系列工具,比如做大表变更的 pt-online-schema-change(后来 GitHub 还发布了改良版 gh-ost),做备份的 xtrabackup。
国内也做了不少的贡献,阿里给上游贡献了许多 replication 的改进。SQL 审核优化这块,有去哪儿研发的 Inception,小米团队的 SOAR。Parser 有 PingCAP 的 MySQL Parser。
相对而言 PG 在工具链的生态还是差不少,比如 PG 生态里没有开箱即用的 Parser,没有 Parser 也就无法做 SQL 审核。Bytebase 在实现相关功能时,就只能从头开始做。当然这也成为了 Bytebase 产品的核心竞争力,我们是市面上对 PG 变更审核,查询脱敏支持最好的工具,除了大表变更外,功能完全对标 MySQL。


总结和展望

回到中国 MySQL 远比 PostgreSQL 流行的原因,在上面所有列出的要素里,我觉得最核心的还是第一条,MySQL 很早就能跑在 Windows 上,而 PG 不能。因为有了能跑 Windows 这个点,MySQL 成为了 LAMP 的一部分,到后来成为了支撑整个互联网的基石。当时国内大家手头装的都是 windows 操作系统,要开发 web 应用,都用 LAMP 架构,就顺便把 MySQL 带上了。
此外国内还有更明显的头部效应。国内所有互联网公司的技术体系都源自阿里,比如拿研发环境来说,SIT (System Integration Test) 是我回国加入蚂蚁后才接触到的名词,但后来在其他各个地方又都反复遇到。数据库方案也是如此,全套照搬了阿里的 MySQL 方案。就连技术职级也是,找工作先确认对标 P 几。
就在上月,MySQL 5.7 宣布了 EOL,算是给 MySQL 5 系,这个支撑了过去 15 年中国互联网的功勋做了一个告别。
随着 MySQL 的辞旧,PG 的崛起,在这 AI 的黎明,VR 的前夜,下一个 15 年,MySQL 和 PG 之间相爱相杀的故事又该会如何演绎呢。


作者:OSC开源社区
来源:mp.weixin.qq.com/s/i08gzmCtJwpUfuf_sVUTWw

收起阅读 »

有这3个迹象,你就该离职了!

我的前下属小P,在当前这样的环境下,裸辞了。小P在这家公司呆了3年,平时任劳任怨,努力踏实。对一些“007”加班、背黑锅等“非常规”任务,也来者不拒,积极配合。本以为这样就可以获得领导的青睐,能让自己的职场之路更顺畅。但现实却没如他所愿,前段时间公司有个主管空...
继续阅读 »

我的前下属小P,在当前这样的环境下,裸辞了。

小P在这家公司呆了3年,平时任劳任怨,努力踏实。对一些“007”加班、背黑锅等“非常规”任务,也来者不拒,积极配合。

本以为这样就可以获得领导的青睐,能让自己的职场之路更顺畅。但现实却没如他所愿,前段时间公司有个主管空缺,无论从哪个角度看,小P都是最佳人选,小P自己也以为十拿九稳,但没想到领导却提拔了一个刚入职不到半年的新同事,真实的原因并不是那个人能力有多强,而是因为他是领导以前的旧下属,是嫡系“子弟兵”。

小P输给一个寸功未立的“关系户”,心里憋屈,找领导理论。但领导就是领导,一下子指出N条小P在工作中的不足,说他工作效率低、有时候还犯错、而且还缺少大局观。小P听了更感到委屈,自己平时干的都是其他同事不愿接的“脏乱差”的活,有时候一整个项目都靠自己一个人顶着,现在反而成了多做多错,而那位被领导提拔的“嫡系”,手里没啥正事,整天和领导一起吹牛抽烟,反倒成了有大局观。

小P情绪非常低落,天天上班如上坟,对工作提不起丝毫兴趣,而且身体和心理都出现明显不适,于是就下定决心,裸辞了。

小P的这个决定,我是支持的。如果公司的环境对你的发展不利,你也看不到任何改观的迹象,那么止损可能是当下最好的选择。下面跟你聊聊,遇到哪类情况,你应该果断离开。



01

领导故意打压你

脏活累活想着你,升职加薪没你份


人在职场,要能分辨两个最起码的事实,领导是“总用你”还是“重用你”;是喜欢 “你能干活”,还是喜欢“能干活的你”。

领导让你做事的时候,简直把你当成公司“继承人”,技术需要你攻关、客户需要你维护、团队需要你协调、项目需要你加班,好像哪里都需要你,什么活儿你都要负责。

直到每次升职加薪的机会都和自己无缘时,上面说的小P就是典型的例子,脏活累活都丢给他,升职加薪全留给亲信,明显厚此薄彼,不公平、不厚道。出现这种情况,通常有以下几种原因:

1、你是个不会说“不”的软柿子老实人

很多单位都一些老实本分的打工人,他们在基层踏实工作,与人为善,任何人都不想得罪,对领导更是言听计从,从不讨价还价。

这种人确实是大家眼中的“好人”,但这样的人不懂得说“不”,不会向别人展示自己的“边界”,以至于领导或其他人会不断对他试探,把脏活累活、不该他干的活全都丢给他。

最后,所有人都对此习以为常,而他却慢慢变成了多做多错、出力还可能不讨好的“杨白劳”。至于升职加薪,是不太可能轮到他的,他真上去了,这些脏活累活以后丢给谁干?

2、你不是领导的“自己人”

有人的地方就有江湖,没办法,谁让中国是个人情社会呢。不管在哪个团队、哪个群体,人与人之间都会有个亲疏远近,最大的差别只不过是表现的是否明显而已。团队的各种资源、机会都是有限的,拥有权力且怀有私心的某些领导,在分配这些资源的时候,就极有可能向所谓的“自己人”倾斜。

上文中的小P不就是败给了领导的前下属了吗?当然,不是说所有的公司领导,都像小P的领导一样不公平公正,但如果真遇到这种明显不讲规则的人,就算你干的再多,也很难分到肉吃,有时能不能喝口汤,都要看他心情。

3、你是个“能力有限”的人

这里说的“能力有限”,不是说工作能力有限,而是指你只会“工作”,而没有背景、资源、人脉,等其他方面的可交换价值。我曾见过一个行政部前台,半年时间被提拔为部门经理,不是说她工作能力有多强,而是她的亲叔叔是某重点中学校长,她利用这个关系为公司老总的孩子解决了上重点的问题。

我还听过很多带“资”入职的例子,这些人到了公司,就能利用自己的资源,为公司或领导个人,解决很多棘手的问题;而你,只擅长本本分分在自己工位上按部就班的工作(而且能干这种活的人一抓一大把),即使你做再多的工作,和前一类相比,稀缺性和竞争力都是明显不能比的。有了好机会,自然会被那些有“特殊贡献”的人先抢走。



02

部门集体摸鱼,人浮于事


在当前的职场生态中,不少企业人浮于事,充斥着“坐班不做事”、“领导不走我不走”等伪加班、伪忙碌的形式主义。出现这种情况,通常因为两种原因:

1、员工“形式主义”严重

我曾听一位年轻朋友讲过他经历的一件奇葩事。他曾和另一个同事一起负责公司的某个项目,某次两人加班到9点回家,但第二天朋友却发现那位同事发了这样一条朋友圈“一不小心搞到现在,终于忙完了”,配图是办公桌上开着的电脑,电脑上显示的是一份打开的文档,而发布时间则显示“某年某月某日凌晨2点”。而且在这条朋友圈下,公司领导不仅点了赞,还写下“辛苦了”三个字的留言。

这让朋友心里非常不爽,活儿明明是两人一起干的,功劳就全成了同事的了。除了想给领导留下勤奋努力的好印象外,还有人有其他一些目的。比如,没结婚成家的想蹭蹭公司的空调,反正回去也是打游戏,在哪玩不是玩;还有一些“勤俭持家”的是为了蹭公司的班车、食堂,甚至是为了熬点加班补贴,生活不易,能省点就省点,能捞点就捞点。总之,大家就这样互相耗着,早走你就输了。

2、公司领导的“官僚主义”

曾有一位粉丝朋友向我分享过他辞职的故事。他的领导就是一位见不得别人比他早下班的人。只要别人比他早离开公司,他就觉得为别人的工作不饱和,就会在开会的时候含沙射影批评几句,或者给那个“早走”的人多安排些工作。

在一些企业中出现这种集体摸鱼、人浮于事的情况,虽然并不是每个人都故意为之,但就像在电影院里看电影,一旦前排的人站起来,后面的人就必须跟着站起来一样,很多人被周围的环境裹挟,不得不一边讨厌着,一边照样干着。



03

长期情绪内耗,个人成长为零


耐克创始人菲尔·奈特在他的自传中有句名言:人生即是成长,不成长即死亡。我们进入一家公司,除了挣钱以外,还有另外一个非常重要的诉求,即积累经验、学习技能,实现个人成长。但如果我们发现,自己的工作除了赚几两并不值得激动的碎银外,其余全是资源的消耗和精神的内耗,而不能给自己带来任何成长,这样的公司肯定也不是久留之地。

我的一个读者,原来是某互联网公司的技术骨干,后来被一家传统企业挖走,但做了不到一年,就觉得坚持不下去了。老板请他的时候,饼画的很好,说自己认清了趋势,要大力发展搞数字化,一定会给侄子最大的工作支持。

但事实远非如此,他过去之后才发现,自己基本是一个光杆司令,公司里里技术好还有追求只有他一个。老板的想法天天变,又不给资源,就只会让他自己想办法,KPI完不成就各种PUA他。

在这种工作环境下,侄子上班度日如年,每天都充满焦虑,并开始变得越来越不自信。

我知道后,劝他立刻离开这家公司。再呆下去只会增加内耗,没有任何成长可言。

后来,我通过朋友把他推荐到另一家业内知名的公司,小伙子很快又找回了状态,在那里他每天都能从同事、团队那里汲取新的养分,而不是一味的输出、做大量纯消耗的无用功。



04

离职前,给你3个建议


1、不要因为暂时性的困难而离职

王阳明先生有句名言:积之不久,发之必不宏,得之不难,失之必易。意思是说,如果你积累的不深,发挥的时候就不会太持续;一样东西如果得来不费劲,那么失去它也会很容易。

但很多职场人,往往没有这个觉悟,但凡遇到点不称心的事,就会在心里打退堂鼓。每当这个时候,你不妨问自己两个问题:

一是这个问题是不是暂时的,是不是真的不能克服?

二是,这个问题是不是这家公司独有的,当你换另一个公司时,是不是能确保不再出现?这些问题想清楚了,相信答案也就有了。

千万不要轻言放弃,越能够经得起挑战,越能够真正拥有。

2、离职是因为有更好的机会,而不是逃避困难

马克思曾经说过:“人不是出于逃避某种消极力量,而是出于展现自身积极个性,才能获得真正的自由。”离职跳槽从来都不应该出于简单的消极逃避,而应该是因为有了更积极的选择。

什么是更好的选择?比如,有更大更有影响力的平台,愿意以30%的薪水涨幅,以更高的title邀请你加盟,这就非常值得考虑。而不能仅仅情绪上不喜欢现在公司,而慌不择路,毫无规划的随便选一家新公司做“避风港”,甚至不惜平跳、打折跳,这样做既草率又不负责。

3、要有随时离开公司的能力

职场上的成功,不是永远不被解雇,而是永远拥有主动选择职业、随时可以离开公司的能力。这就要求我们在职场中,要注意学习新的技能和认知,要能清晰客观的认识自己,知道哪些是自己真正具备的可复制、可转移、可嫁接的个人能力,而不至于出现“错把平台当能力”的“误会”。

同时,还要在职场中为自己积累一些能拿得出手、更有说服力的“硬通货”,比如过往的出色业绩,良好的业内口碑,这些都能作为你的背书,让你在新的舞台上,更受器重和尊重。良好的开始就是成功的一半,真正有实力的你,搞定成功的另外“一半”,想必也不是什么难事。

大哲学家康德说过:真正的自由,不是你想做什么;而是当你不想做什么时,可以不做什么。有底气、负责任的离职,就是这句话最好的诠释,希望小伙伴们在职场中,都能从事自己喜欢的工作,受到良好的尊重和对待,有丰厚的薪水收入,还能有让人满意的个人成长。加油!

作者:军哥手记
来源:mp.weixin.qq.com/s/BR_uzYx6sEFwMsBRRgsAiw

收起阅读 »

前端小练:kiss小动画分享

web
最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画 人狠话不多,哈士奇给大家献上代码先 <!DOCTYPE html> <html lang="en"> ...
继续阅读 »

最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画


image.png


人狠话不多,哈士奇给大家献上代码先


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="container">
<div class="ball" id="l-ball">
<div class="face face-l">
<div class="eye-l eye-ll"></div>
<div class="eye-l eye-lr"></div>
<div class="mouth"></div>
</div>
</div>
<div class="ball" id="r-ball">
<div class="face face-r">
<div class="eye-r eye-rl"></div>
<div class="eye-r eye-rr"></div>
<div class="mouth-r"></div>
<div class="kiss-r">
<div class="kiss"></div>
<div class="kiss"></div>
</div>
</div>
</div>
</div>
</body>
</html>

body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}
.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}
.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}
.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}
.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}
.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}
#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}
.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}
#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

接下来哈士奇为大家依次聊聊这段代码
首先是html的部分:


html主要是使用div对整个页面做出一个布局,哈士奇此次的小人主要是小人的两张脸和五官,因此我们在html的代码创建的过程中需要留出脸 眼睛 嘴巴的部分进行后面的css代码的操作。


在这里有些同学可能会问到,这里的kiss和mouth怎么回事,稍后我们就知道了!


那么再给大家讲讲css的部分:


首先我们通过整个页面的设置,将整个页面背景设置,也就是body部分,去除之前的默认值。


body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}

接下来就是容器container的设置,我们的设置一个大容器用于放下两个小人,通过position中的absolute对于父容器(此处的是body)进行定位,使用translate函数将容器移到页面的正中心的位置


.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}

将大致的位置确定好了以后,我们就可以开始对于两个kiss小人进行操作了


首先确定两个小球的设置


.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}

通过border将外形线条确定,这样一来就可以制造出小人外面一圈的线,通过border-radius确定弧度,最后通过relative的相对定位,针对元素的原本位置进行定位。那么为什么要使用display呢?我们都知道,inlie-block可以使得块级元素div转化为具有行内块的特点的元素,因此div中的两个ball小球就能处于同一行了


确定两个小球的位置以后我们开始确定小球的脸


.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}

通过左右两脸的设置确定他们相对于他们父容器l-ball 和r-ball的位置


接下来设置眼睛的相同元素的设置


.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}

对于我们来说,这里的两个眼睛其实就是两个弧线,所以我们只需要确定两根线,然后使用boder-radius进行弯曲,就能把眼睛制造出来了,再通过absolute对于自己的父容器进行定位


.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}

微调设置眼睛的具体位置


再进行嘴巴的设计


.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}

也是进行一个弯曲的曲线的设计


接下来难度要升级了,两个脸颊红红的部分应该如何实现呢?
这里我们使用到了伪元素进行创建


.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}

给大家复习一下伪元素的用法,在哈士奇的这个代码中,before和after分别是对于父容器的第一个子元素进行操作,也就是face里面的左眼睛进行操作,针对左眼定位脸颊的位置(记住哦,如果没有给出伪元素的定位,也就是父容器的话,是无法显示伪元素的


这样一来,我们的脸颊也做好了
最后就是属于亲脸颊时的嘴巴部分


.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}

哈士奇在设计中,想要右边的小球能够在最后亲到左边小球,那么光移动嘴巴是不行的,还需要让嘴巴变形成为嘟嘴的样子,因此哈士奇在mouth-r中设置了嘴巴的样式,接下来又在kiss中设置了嘴巴转变后的样式。这就是为什么右边的小球要设置kiss和mouth的原因了!


在kiss-r中大家看到opacity,如果opacity:0; 那么代表着这个块是隐藏状态,如果opacity:1; 那么就是显示的状态


最后就是动画设置的部分了,有些小伙伴已经看出来了哈士奇已经写过的
animation: mouth-r 4s ease infinite;


那么在这给大家讲讲这是个啥意思


animation(声明动画): mouth-r(动画的名字) 4s(时间) ease(规定慢速开始,然后变快,然后慢速结束的过渡效果) infinite(永久执行:动画会循环播放)


先聊聊左脸的动画,哈士奇希望它平移过去,然后做出小鸟依人的蹭一蹭的动作,于是就有了


#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}

首先就是在元素中声明需要准备动画,然后在下方使用@keyframes 动画名 写出一个动画的具体内容(我们需要写出什么时候动画要做什么)


比如4s中0%的时候我希望动画开始平移,就写transform:translate()写出平移的位置是多少像素,那么在下一个%出现前,浏览器就会执行你的操作,表示在%~%之间执行动画的操作


rotate()则是进行旋转,使用以后动画将会根据一定的比例进行旋转


最后就是右脸小球的亲亲操作了


#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

前面还是进行平移操作,到了后面,小球需要进行亲亲,那么哈士奇通过opacity实时操作嘴巴的出现时间,最后在亲的前面的时间把嘟嘴展现出来。


你学会了吗?快去给你的女友写个亲亲动画吧!!


总结与联想


总结


今天哈士奇给大家分享了一个前端小动画的展现,并且逐步为大家解释了一个前端小动画应该如何写出来,在这其中涉及到了transform opacity animation z-index的使用,大家可以简单上手做做哦


联想


那么动画是否还有其他的关键词呢?ease就能解决所有的平移问题吗?我们是否可以通过其他方式展示不同效果呢?


作者:疯犬丨哈士奇
来源:juejin.cn/post/7300460850010734646
收起阅读 »

周爱民:告前端同学书

web
一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成...
继续阅读 »

一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成文,是为《告前端同学书》。



作者:周爱民 / aimingoo


各位前端同学,就我的所知来看,每⼀个具体的技术,在其⽅向上都有着不同的标志事件,也因此有着不同的阶段划分。但是我想,如果从我们这个领域对“前端”的认识来观察这件事,⼤概会对不同的前端阶段能有更清晰的认识。


早期前端的从业⼈员⼤多来⾃后端开发者、应⽤软件开发者,或者⽹⻚设计师,⽽并没有专职的前端开发。例如说阿⾥巴巴在 2010 年之前都把前端归在产品部⻔,可⻅前端⼯程师的来源和定位⼀直都很模糊。这个时代,ECMAScript 还陷在 Ed4 的泥坑中没有⾛出来,IE 浏览器带来的标准分裂还没有得到全⾯的修补,源于对这个领域的漠视,⼤⼚优势也没有体现出来,前端开发者们基本上各⾃为战,框架和中间件层出不穷⽽⼜良莠难分,开发⼯具和环境却荒草凄凄以⾄于乏善可陈。但是也正是在这个时代,ES6、CSS3、HTML5 等等都在筑基、渗透与蓄势。


随着专⽤⼯具链和开发流程的成熟,前后端分离的运动从项⽬内部开始蔓延到整个领域,出现了专⻔的前端开发⼯程师、团队,以及随之⽽来的⻆⾊细分,很多独⽴的技术社区就是在这个时代出现的。前后端分离不仅仅是⼀种技术表现,更是⼀种⾏业协作的模式与规范,并且反过来推动了⼯具和框架的⼤发展。信⼼满满的前端不拘于⼀城⼀地,⼀⽅⾯向前、向专业领域推进,从⽽影响到交互与接触层。因此更丰富的界⾯表现,以及从移动设备到⼈机交互界⾯等领域都成了前端的研究⽅向,是所谓“⼤前端”。⽽另⼀⽅⾯则向后、向系统领域渗透,有了所谓⼯程师“全栈化”运动。这个时候的“全栈”,在⼯程上正好符合敏捷团队的需求,在实践上正好⼜叠加上DevOPS、云端开发和⼩应⽤的⼏阵助⼒,前端因此⼀⽚繁华景象。


所以 2008 年左右开始的前后端分离是整个前端第⼆阶段的起点,这场运动改变了软件开发的体系与格局,为随后⼗年的前端成熟期拓开了局⾯。那⼀年的 SD2C 我谈了《VCL 已死、RAD 已死》,⽽⼗年后阿⾥的圆⼼在GMTC 上讲了《前端路上的思考》,可算作对这个时代的预⾔和反思。


相对于之前所说的第⼀、第⼆阶段,我认为如今我们正⾏进在⼀个全新阶段中。这个阶段初起的主要表现是:前端分离为独⽴领域,并向前、后两个⽅向并进之举已然势微。其关键在于,前端这个领域中的内容已经逐渐复杂,⽽其应⽤的体量也将愈加庞⼤,因此再向任何⽅向发展都难尽全⼒、难得全功。


摊⼦铺得⼤了,就需要再分家。所以下⼀个阶段中,将再次发⽣横向的领域分层,⼀些弥合层间差异的技术、⽅法与⼯具也将出现,类似于 Babel 这样的“嵌缝膏”产品将会再次成为⼀时热⻔。但⻓期来说,领域分层带来的是更专精的职业与技能,跨域协作是规约性的、流程化的,以及⼯具适配的。从 ECMAScript 的实践来看,规范的快速更新和迭代已经成为现实,因此围绕规范与接⼝的新的开发技术与⼯程模型,将会在这个阶段中成为主要⼒量,并成为维持系统稳定性的主要⼿段。


这是在⼀个新阶段的前夜。故此,有很多信息并不那么明朗,⽐如说像前后端分离这样的标志性事件并没有出现,亦或者出现了也还没有形成典型影响。我倾向于认为引领新时代的,或者说开启下⼀个阶段的运动将会发⽣在交互领域,也就是说新的交互⽅式决定了前端的未来。之前⾏业⾥在讲的 VR 和 AR(虚拟现实和增强实境)是在这个⽅向上的典型技术,但不唯于此。⼏乎所有在交互⽅式上的变⾰,都会成为⼈们认识与改变这个世界的全新动⼒,像语⾳识别、视觉捕捉、脑机接⼝等等,这些半成熟的或者实验性的技术都在影响着我们对“交互”的理解,从⽽也重新定义了前端。


⾏业⽣态也会重构,如同今天的前端⼤会已经从“XX技术⼤会”中分离出来⼀样,不久之后“交互”也会从前端分化出来,设计、组件化、框架与平台等等也会成体系地分化出来。前端会变得⽐后端更复杂、更多元,以及更加的⽣机勃勃。这样的⽣态起来了,⼀个新的时代也就来临了。简单地说,1、要注重领域与规范,2、要跟进交互与体验,3、要在⽣态中看到机会。


然而,前端的同学们,我们也不要忘记在这背景中回望自身,正视我们前端自己的问题。


其⼀,底⼦还是薄,前端在技术团队与社区的积累上仍然不够。看起来摊⼦是铺开了,但是每每只在“如何应⽤”上下功夫,真正在⽹络、系统、语⾔、编译、机器学习等等⽅⾯有深⼊研究的并不多。⼀直以来,真正有创建性或预⻅性的思想、⽅法与理论鲜⻅于前端,根底薄是⾸要原因。


其⼆,思维转换慢,有些技术与思想抛弃得不够快,不够彻底。不能总是把核⼼放在“三⼤件(JS+CSS+HTML)”上⾯,核⼼要是不变,前端的⾰命也就不会真正开始。要把“Web 前端”前⾯的“Web”去掉,就现实来说,很多⼈连“观望”都没有开始。


其三,还没有找到跟“交互”结合起来的有效⽅法与机制。前端过去⼗年,在 IoT、机器学习、云平台等等每⼀次潮流都卡上了点⼉,但是如果前端的下⼀次转型起于“交互”,那么我们⽬前还没有能⼒适应这样的变化。当然,契机也可能不在于“交互”,但如果这样,我们的准备就更不充分了。


其四,向更多的应⽤领域渗透的动机与动⼒不明确。⻓期以来,前端在各个领域上都只是陪跑,缺乏真正推动这些领域的动机与动⼒。往将来看,这些因素在前端也将持续缺乏。寻求让前端持续发展,甚⾄领跑某些领域的内驱⼒量,任重⽽道远。


同学们,我想我们必须有一种共同的、清醒的认识与认知:浏览器是未来。去操作系统和云化是两个⼤的⽅向,当它们达成⽬标时,浏览器将成为与⽤户接触的唯⼀渠道。研究浏览器,其本质就是研究交互和表现,是前端的“终极私活”。但不要局限于“Web 浏览器”,它必将成为历史,如同操作系统的“⽂件浏览器”⼀样。


要极其关注 JavaScript 的类型化,弱类型是这⻔语⾔在先天条件上的劣势,是它在⼤型化和系统化应⽤中的明显短板。这个问题⼀旦改善,JavaScript 将有⼒量从其它各种语⾔中汲取营养,并得以⾯向更多的开发领域,这是 JavaScript 的未来。


AI 和 WASM 在前端可以成为⻬头并进的技术,⼀个算法,⼀个实现。对于前端来说,性能问题⼀直是核⼼问题,⽽交互与表现必将“⼤型与复杂化”,例如虚拟现实交互,以及模拟反馈等等,⽽ WASM 是应对这些问题的有效⼿段。


所谓交互与表现,本质上都是“空间问题”。亦即是说,前端表现中的所谓布局、块、位置、流等等传统模式与技术,与将来的交互技术在问题上是同源的。就像“盒模型”确定了 CSS 在前端的核⼼地位⼀样,新的空间定位技术,以及与之匹配的表现与交互⽅法是值得关注和跟进的。


前端要有更强的组织⼒,才能应付更⼤规模的系统。这⾥的组织⼒主要是针对⼯程化⽽⾔,所有⼯程化⼯具,其最终的落脚点都在快速、可靠,并以体系化的⽅式来组织⼯程项⽬。这包括⼈、资源、信息、时间、能⼒与关系等等⼯程因素,每个⽅⾯都有问题,都值得投⼊技术⼒量。


相较于新入行的前端的同学们,我能从没有前端走到如今前端的⼤发展,何其幸也。以我⼀路之所⻅,前端真正让我钦佩的是持久的活⼒。前端开发者⼏乎总是⼀个团队中“新鲜⾎液”的代名词,因此前端在业界的每个阶段都⾛在时代的前列。如今看 C 语⾔的⽼迈,操作系统的封闭,后台的保守,以及业务应⽤、产品市场等等各个领域都在筑城⾃守,再看前端种种,便总觉得开放与探索的信念犹在。


曾经与我⼀道的那些早期的前端开发者们,如今有做了主管的,有搞了标准的,有带了团队的,有转了后端的,有做架构做产品做运维等等⼀肩担之,也有开了公司做了顾问从商⼊政的,但也仍然还有在前端⼀线上做着努⼒,仍看好于这⼀个⽅向并在具体事务上勉⼒前⾏的。我曾经说,“任何事情做个⼗年,总会有所成绩的”,如今看来,这个时间还是说少了,得说是:⼏个⼗年地做下去,前端总能做到第⼀。


惟只提醒⼤家,领域分层的潮流之下,层间技术的核⼼不是功能(functional),⽽是能⼒(capabilities)。向应⽤者交付能⼒,需要有体系性的思维,要看向系统的全貌。我们专精于细节没错,专注于⼀城⼀地也没错,然而眼光⾼远⽽脚踏实地,是前端朋友们当有之势。


亦是这个时代予我们的当为之事!


周爱民/aimingoo


初稿于2022.06


此稿于2023.10


作者:裕波
来源:juejin.cn/post/7290751135903236137
收起阅读 »

写给想入门单元测试的你

✨这里是第七人格的博客✨小七,欢迎您的到来~✨ 🍅系列专栏:【架构思想】🍅 ✈️本篇内容: 写给想入门单元测试的你✈️ 🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱 一、为什么要进行单元测试 首先我们来看一下标准的软件开发流程是什么样...
继续阅读 »

✨这里是第七人格的博客✨小七,欢迎您的到来~✨


🍅系列专栏:【架构思想】🍅


✈️本篇内容: 写给想入门单元测试的你✈️


🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱


一、为什么要进行单元测试


首先我们来看一下标准的软件开发流程是什么样的


01_开发流程规范.png
从图中我们可以看到,单元测试作为开发流程中的重要一环,其实是保证代码健壮性的重要一环,但是因为各种各样的原因,在日常开发中,我们往往不重视这一步,不写或者写的不太规范。那为什么要进行单元测试呢?小七觉得有以下几点:



  • 便于后期重构。单元测试可以为代码的重构提供保障,只要重构代码之后单元测试全部运行通过,那么在很大程度上表示这次重构没有引入新的BUG,当然这是建立在完整、有效的单元测试覆盖率的基础上。

  • 优化设计。编写单元测试将使用户从调用者的角度观察、思考,特别是使用TDD驱动开发的开发方式,会让使用者把程序设计成易于调用和可测试,并且解除软件中的耦合。

  • 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。

  • 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。


不少同学,写单元测试,就是直接调用的接口方法,就跟跑swagger和postMan一样,这样只是对当前方法有无错误做了一个验证,无法构成单元测试网络。


比如下面这种代码


@Test
public void Test1(){
xxxService.doSomeThing();
}

接下来小七就和大家探讨一下如何写好一个简单的单元测试。


小七觉得写好一个单元测试应该要注意以下几点:


1、单元测试是主要是关注测试方法的逻辑,而不仅仅是结果。


2、需要测试的方法,不应该依赖于其他的方法,也就是说每一个单元各自独立。


3、无论执行多少次,其结果是一定的不变的,也就是单元测试需要有幂等性。


4、单元测试也应该迭代维护。


二、单元测试需要引用的jar包


针对springboot项目,咱们只需要引用他的starter即可


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

下面贴出这个start包含的依赖


<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.0.RELEASE</version>
<name>Spring Boot Test Starter</name>
<description>Starter for testing Spring Boot applications with libraries including
JUnit, Hamcrest and Mockito</description>
<url>https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-test</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>http://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</connection>
<developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</developerConnection>
<url>http://github.com/spring-projects/spring-boot/spring-boot-starters/spring-boot-starter-test</url>
</scm>
<issueManagement>
<system>Github</system>
<url>https://github.com/spring-projects/spring-boot/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.11.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.6.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

三、单元测试解析与技巧


1、单元测试类注解解析


下面是出现频率极高的注解:


/*
* 这个注解的作用是,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
* 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个spring容器的
* 所以得找这个SpringRunner这个类,来先准备好spring容器,再执行各个测试方法
*/

@RunWith(SpringRunner.class)
/*
* 这个注解的作用是,去寻找一个标注了@SpringBootApplication注解的一个类,也就是启动类
* 然后会执行这个启动类的main方法,就可以创建spring容器,给后面的单元测试提供完整的这个环境
*/

@SpringBootTest
/*
* 这个注解的作用是,可以让每个方法都是放在一个事务里面
* 让单元测试方法执行的这些增删改的操作,都是一次性的
*/

@Transactional
/*
* 这个注解的作用是,如果产生异常那么会回滚,保证数据库数据的纯净
* 默认就是true
*/

@Rollback(true)

2、常用断言


Junit所有的断言都包含在 Assert 类中。


void assertEquals(boolean expected, boolean actual)检查两个变量或者等式是否平衡
void assertTrue(boolean expected, boolean actual)检查条件为真
void assertFalse(boolean condition)检查条件为假
void assertNotNull(Object object)检查对象不为空
void assertNull(Object object)检查对象为空
void assertArrayEquals(expectedArray, resultArray)检查两个数组是否相等
void assertSame(expected, actual)查看两个对象的引用是否相等。类似于使用“==”比较两个对象
assertNotSame(unexpected, actual)查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象
fail()让测试失败
static T verify(T mock, VerificationMode mode)验证调用次数,一般用于void方法

3、有返回值方法的测试


@Test
public void haveReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、断言
}

4、无返回值方法的测试


@Test
public void noReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、验证执行次数
}

四、单元测试小例


以常见的SpringMVC3层架构为例,咱们分别展示3层架构如何做简单的单元测试。业务场景为用户user的增删改查。


(1)dao层的单元测试


dao层一般是持久化层,也就是与数据库打交道的一层,单元测试尽量不要依赖外部,但是直到最后一层的时候,DAO层的时候,还是要依靠开发环境里的基础设施,来进行单元测试。


@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback
public class UserMapperTest {

/**
* 持久层,不需要使用模拟对象
*/

@Autowired
private UserMapper userMapper;

/**
* 测试用例:查询所有用户信息
*/

@Test
public void testListUsers() {
// 初始化数据
initUser(20);
// 调用方法
List<User> resultUsers = userMapper.listUsers();
// 断言不为空
assertNotNull(resultUsers);
// 断言size大于0
Assert.assertThat(resultUsers.size(), is(greaterThanOrEqualTo(0)));
}

/**
* 测试用例:根据ID查询一个用户
*/

@Test
public void testGetUserById() {
// 初始化数据
User user = initUser(20);
Long userId = user.getId();
// 调用方法
User resultUser = userMapper.getUserById(userId);
// 断言对象相等
assertEquals(user.toString(), resultUser.toString());
}

/**
* 测试用例:新增用户
*/

@Test
public void testSaveUser() {
initUser(20);
}

/**
* 测试用例:修改用户
*/

@Test
public void testUpdateUser() {
// 初始化数据
Integer oldAge = 20;
Integer newAge = 21;
User user = initUser(oldAge);
user.setAge(newAge);
// 调用方法
Boolean updateResult = userMapper.updateUser(user);
// 断言是否为真
assertTrue(updateResult);
// 调用方法
User updatedUser = userMapper.getUserById(user.getId());
// 断言是否相等
assertEquals(newAge, updatedUser.getAge());
}

/**
* 测试用例:删除用户
*/

@Test
public void testRemoveUser() {
// 初始化数据
User user = initUser(20);
// 调用方法
Boolean removeResult = userMapper.removeUser(user.getId());
// 断言是否为真
assertTrue(removeResult);
}

private User initUser(int i) {
// 初始化数据
User user = new User();
user.setName("测试用户");
user.setAge(i);
// 调用方法
userMapper.saveUser(user);
// 断言id不为空
assertNotNull(user.getId());
return user;
}
}

(2)service层的单元测试


@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {

@Autowired
private UserService userService;

/**
* 这个注解表名,该对象是个mock对象,他将替换掉你@Autowired标记的对象
*/

@MockBean
private UserMapper userMapper;

/**
* 测试用例:查询所有用户信息
*/

@Test
public void testListUsers() {
// 初始化数据
List<User> users = new ArrayList<>();

User user = initUser(1L);

users.add(user);
// mock行为
when(userMapper.listUsers()).thenReturn(users);
// 调用方法
List<User> resultUsers = userService.listUsers();
// 断言是否相等
assertEquals(users, resultUsers);
}

/**
* 测试用例:根据ID查询一个用户
*/

@Test
public void testGetUserById() {
// 初始化数据
Long userId = 1L;

User user = initUser(userId);
// mock行为
when(userMapper.getUserById(userId)).thenReturn(user);
// 调用方法
User resultUser = userService.getUserById(userId);
// 断言是否相等
assertEquals(user, resultUser);

}

/**
* 测试用例:新增用户
*/

@Test
public void testSaveUser() {
// 初始化数据
User user = initUser(1L);
// 默认的行为(这一行可以不写)
doNothing().when(userMapper).saveUser(any());
// 调用方法
userService.saveUser(user);
// 验证执行次数
verify(userMapper, times(1)).saveUser(user);

}

/**
* 测试用例:修改用户
*/

@Test
public void testUpdateUser() {
// 初始化数据
User user = initUser(1L);
// 模拟行为
when(userMapper.updateUser(user)).thenReturn(true);
// 调用方法
Boolean updateResult = userService.updateUser(user);
// 断言是否为真
assertTrue(updateResult);
}

/**
* 测试用例:删除用户
*/

@Test
public void testRemoveUser() {
Long userId = 1L;
// 模拟行为
when(userMapper.removeUser(userId)).thenReturn(true);
// 调用方法
Boolean removeResult = userService.removeUser(userId);
// 断言是否为真
assertTrue(removeResult);
}

private User initUser(Long userId) {
User user = new User();
user.setName("测试用户");
user.setAge(20);
user.setId(userId);
return user;
}

}

(3)controller层的单元测试


@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class UserControllerTest {

private MockMvc mockMvc;

@InjectMocks
private UserController userController;

@MockBean
private UserService userService;

/**
* 前置方法,一般执行初始化代码
*/

@Before
public void setup() {

MockitoAnnotations.initMocks(this);

this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}

/**
* 测试用例:查询所有用户信息
*/

@Test
public void testListUsers() {
try {
List<User> users = new ArrayList<User>();

User user = new User();
user.setId(1L);
user.setName("测试用户");
user.setAge(20);

users.add(user);

when(userService.listUsers()).thenReturn(users);

mockMvc.perform(get("/user/"))
.andExpect(content().json(JSONArray.toJSONString(users)));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:根据ID查询一个用户
*/

@Test
public void testGetUserById() {
try {
Long userId = 1L;

User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);

when(userService.getUserById(userId)).thenReturn(user);

mockMvc.perform(get("/user/{id}", userId))
.andExpect(content().json(JSONObject.toJSONString(user)));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:新增用户
*/

@Test
public void testSaveUser() {
Long userId = 1L;

User user = new User();
user.setName("测试用户");
user.setAge(20);

when(userService.saveUser(user)).thenReturn(userId);

try {
mockMvc.perform(post("/user/").contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:修改用户
*/

@Test
public void testUpdateUser() {
Long userId = 1L;

User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);

when(userService.updateUser(user)).thenReturn(true);

try {
mockMvc.perform(put("/user/{id}", userId).contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:删除用户
*/

@Test
public void testRemoveUser() {
Long userId = 1L;

when(userService.removeUser(userId)).thenReturn(true);

try {
mockMvc.perform(delete("/user/{id}", userId))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}

}


五、其他


1、小七认为不需要对私有方法进行单元测试。


2、dubbo的接口,在初始化的时候会被dubbo的类代理,和单测的mock是两个类,会导致mock失效,目前还没有找到好的解决方案。


3、单元测试覆盖率报告


(1)添加依赖


<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
</dependency>


(2)添加插件


<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<id>pre-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>post-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>


(3)执行mvn test命令


报告生成位置


image.png


4、异常测试


本次分享主要是针对正向流程,异常情况未做处理。感兴趣的同学可以查看附录相关文档自己学习。


六、附录


1、user建表语句:


CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名',
`age` INT(3) NOT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='user示例表';

2、文章小例源码地址:gitee.com/diqirenge/s…


3、mockito官网:site.mockito.org/


4、mockito中文文档:github.com/hehonghui/m…


作者:第七人格
来源:juejin.cn/post/7297608084306821132
收起阅读 »

时光匆匆独白

  每天在固定的时间醒来,不用闹铃,叫醒我的不再是闹铃,可能真的只是年纪大了,习惯了早睡,也习惯了早起。今天不写技术,只想随便写一写,写我平时所想所念。其实想的最多的一个问题就是:生活的意义是什么?的确,这是一个挺傻的问题,但是又情不自禁的去问,或许这正如有些...
继续阅读 »

  每天在固定的时间醒来,不用闹铃,叫醒我的不再是闹铃,可能真的只是年纪大了,习惯了早睡,也习惯了早起。今天不写技术,只想随便写一写,写我平时所想所念。其实想的最多的一个问题就是:生活的意义是什么?的确,这是一个挺傻的问题,但是又情不自禁的去问,或许这正如有些人所说,生活哪有什么意义?不过只是苟且。



0099.png


  或许真的是这样吧,大家怎么想都无所谓,因为这说到底也只是一种感受而已。意义也好,幸福感也罢,每个人对其理解不同,无论怎么想我觉得都没有错。我自认是一个对待生活蛮透亮的一个人,胜不骄败不馁,面对得失也没有太大的情绪波动,但是在外人看来是活的比较傻。


  记得上高中那会儿,面对每天重复的生活,我就会想起韩寒的小说《三重门》,感觉未来的路和现在的路真的也就这样了,固定的套路,每天游走在教室,宿舍,食堂,想起这些内心未免心生波澜,感觉心塞不已,后来想的多了也就不再想了,因为我知道自己跳不出去,作为局内人只能老老实实的在这个圈子里待着,想多了也只是徒增烦恼罢了。


  后来上了大学,第一次出那么远的门,跨越几千公里来到了陌生的城市,还记得刚下火车的那一刻怀揣着对大学的期待,内心激动不已,那时候傻傻的想终于跳出来了,不再只是重复的生活了。现在想想那时候之所以有这样的想法,一方面是对现实的逃避,另一方面还是因为自己too young too simple !后来准备考研,仿佛又回到了高中时候的生活,但是内心却没有了波澜,或许是自己成长了吧,对于学习和生活而言,有了一点点理解,这是一种好听的说法,说的不好听的话,其实自己是对生活有些妥协了。


  再后来呢,自己工作了,却发现自己所面对的生活更加复杂了,那时候为了所谓的户口,拿着现在看来只能勉强度日的薪水,说实话挺累的,这种累不是说工作累,早九晚六的工作并没有多累,其实最累的是心。那时候就在想这就是我所期待的生活吗?难道真的要一条路走到黑,对于普通家庭出身的自己而言,我深知有些东西及时自己取得了,那也将是遍体鳞伤,搭上父母,哎,何必呢。


  于是我选择跳了出来,做了一个外人看来很傻的决定,其实随着后面收入的日渐增加,也并没有为当初的决定感到后悔,因为我知道这就是自己要走的路,而那条在外人看来繁花似锦的大道真的不适合我。生活,是一幅多彩的绘画,细看其中的每个色彩和纹理,都是自己所经历的不同阶段所留下的痕迹。我们走过岁月的长河,感受着生活中的喜怒哀乐,也在不断思考。对也好,错也罢,一切都终将在时间的冲刷之下变成过眼云烟,所以呢,不要为过去的决定感到悔恨。


  在到现在,女儿马上就要上小学了,父母也要退休了,每周末自己会驱车回到老家,这样的生活真的很平淡,少了社交,多了一些陪伴,说实话:这样挺好。现在想生活的意义是什么?我觉得是让他如何再变的充实起来,我想带着家人在闲暇的日子里出去转转,我想能第一时间满足他们的需要。面对未来诸多的不确定性,活在当下,或许生活才能变得更加“骚气”一点。


  好了,又要准备睡觉了!说些题外话,最近刚刚把战神5的主线打完,不得不感叹游戏的制作真的很精良,剧情也很饱满。恰逢最近黑神话线下试玩,大部分反馈都很好,但是也有一部分人言辞激烈,什么抄袭魂类游戏,什么看视频动作坚硬等等,对于这样的声音,我们没必要较真,每个人的喜恶都是他的权利,但是能看国产游戏的点滴进步,希望大家多多给予鼓励吧。


  明天又是新的一天,加油,铁子们!


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

风格是一种神秘的力量

看马未都先生讲古董鉴定: 这个落款虽然写的是明朝,其实是现代仿的。这个没落款,一打眼看上去像清康熙年间的,其实是民国仿康熙。民国的也有些价值,建议继续收藏。 我感觉不可思议。 他一看就知道是什么年代的,而且还知道是从那个年代仿了另一个年代。 这专家是不是胡...
继续阅读 »

看马未都先生讲古董鉴定:



这个落款虽然写的是明朝,其实是现代仿的。这个没落款,一打眼看上去像清康熙年间的,其实是民国仿康熙。民国的也有些价值,建议继续收藏。



我感觉不可思议


他一看就知道是什么年代的,而且还知道是从那个年代仿了另一个年代。


这专家是不是胡说八道?


马先生接着讲,每个年代都有自己的风格,虽然每个时代都极力模仿古代,但风格却模仿不了。我看它第一眼立马就知道这东西不真。


对于这个风格,我理解不了。现在科技都这么发达了,我查好了明朝的风格,照着去做,你还能分辨出来?


另外,你不仔细看看色泽,纹理,胎质,一瞅就知道是假的。就靠风格?这是一种感觉啊……


后来,有一件事,让我稍微理解这种感觉了。


朋友圈里出现一些派发红包之类的分享信息。


比如某集团上市,派发红包。当看到这个logo+标题的时候,我立马就感觉这活动是假的。


为了验证猜想,我决定点进去一探究竟。


图片


链接转到浏览器打开,按F12调试发现:域名,是个野域名,一串随机数那种。网页是静态页面,根本没有数据的上传和下载。活动说明上有繁体字,这是规避敏感词审查用的吧。链接最终指向了小广告页面。


假活动。


第一眼看到的时候,我就觉得是假的。


哎,奇怪,我又是靠什么判断的呢?我想,好像我也是靠风格进行的评判。因为有多年互联网从业经验,会有一种行业嗅觉,感觉这不是正常互联网产品的风格。


此时,到自己的行业中,我有些理解马先生所讲的了。


看下面一个风格。


图片


这是杀马特风格,形成于2008年。我小时候,大街小巷很多这样的青年,我当时很羡慕,但是没钱搞。因为当时青年片面模仿日本动漫和欧美摇滚,于是就形成了一种独特的风格。


那么,杀马特在唐朝有没有?肯定没有,因为没有相关的材料和烫染工具。


那么以后会不会有杀马特?可能会有,但是再出现时,必定会结合那时的审美和工艺,不会和2008年那时完全一样。我现在看他都已经感觉丑了。


所以,你看,风格居然是一种独一无二的神秘力量,它结合了特定的时代审美和特定的生产力水平。


其实,就像看一个人,他一开口,你就知道他是一个什么水平的人。


看似是一种感觉,实际上却有很多的客观标准。这些标准都隐藏在了你的判断指标里,最后你对外说那是一种感觉。


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

【微信小程序】 token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送


二、【流程图】🚩


图中为大致流程,为了看起来简洁直观,略去了与本文内容不相关的步骤


image.png


三、【后端代码】🚩



1. ⌈签发和验证 token⌋🍥




  • 签发 accessToken 时,设置的过期时间是4个小时

  • 签发 refreshToken 时,设置的过期时间是7天

  • 自己在测试的时候,可以把时间改短一点,例如30s

  • 正常实现的效果是:登录时间超过4个小时之后,再次发送需要身份验证的请求,会使用 refreshToken 去请求刷新 accessToken;如果距离上次登录已经超过了7天,则会提示重新登录

  • 这样的话,实现了一定的安全性(因为 accessToken 每隔4个小时就会更新),同时又没有让用户频繁地重新登录



2. ⌈登录⌋🍥




  • 拿到请求参数中的登录凭证(code),以及保存的 appId 和 appSecret

  • 基于上述三个参数发送请求到微信官方指定的服务器地址,获取 openid

  • openid 是小程序登录用户的唯一标识,每次登录时的登录凭证(code)会变,但是获取到的 openid 是不变的

  • 根据 openid 在数据库中查找用户,如果没有查找到,说明本次登录是当前用户的首次登录,需要创建一个新用户,存入数据库中

  • 然后根据用户 id 以及设置的签发密钥进行 accessToken 和 refreshToken 的签发

  • 签发密钥可以是自己随意设置的一段字符串,两个 token 要设置各自对应的签发密钥

  • 这个签发密钥,在进行 token 验证的时候会使用到


四、【前端代码】🚩



1. ⌈封装的登录方法⌋🍥




  • 在创建微信小程序项目时,默认是在根目录下 app.js 的 onLaunch 生命周期函数中进行了登录

  • 也就是说每次在小程序初始化的时候都会进行登录操作

  • 作者这里是把登录操作单独提取出来了,这样可以在个人主页界面专门设置一个登录按钮

  • 当本地存储的用户信息被清除,或者上面提到的 refereshToken 也过期的情况下,我们点击登录按钮进行登录操作


import { loginApi } from '@/api/v1/index'

const login = async () => {
try {
// 登录获取 code
const {code} = await wx.login()
// 调用后端接口,获取用户信息
const {user, accessToken, refreshToken} = await loginApi(code)
wx.setStorageSync('profile', user)
wx.setStorageSync('accessToken', `Bearer ${accessToken}`)
wx.setStorageSync('refreshToken', refreshToken)
} catch (error) {
wx.showToast({
title: '登录失败,请稍后重试',
icon: 'error',
duration: 2000
})
}
}

export default login



2. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。在上面的验证 token 代码中,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 调用 wx.request 发送请求,在 success 回调函数中判断请求返回信息中的状态码,根据状态码的不同做对应的操作,这里只讨论401 token 过期的情况





  • 当 token 过期时,从本地存储中获取到 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中





【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


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

【Taro】【微信小程序】token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送



👉具体实现可以翻阅作者的上一篇文章:【微信小程序】token 无感刷新



👉实现思路中的后三步,微信小程序中是在请求的 success 回调函数中做的处理,Taro 中则是设置了响应拦截器,在拦截器中做的对应处理,本文仅讨论有区别的这部分



二、【前端代码】🚩



1. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。后端接口在验证 token 时,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 先通过 Taro.addInterceptor 设置拦截器,然后调用 Taro.request 发送请求。这样的话,当请求真正发送之前以及获取到响应信息时,都会先进入到拦截器中,我们就是在这里进行的 token 刷新操作





  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { baseUrl } from '../config'
    import responseInterceptor from '../http/interceptors'

    // 添加拦截器
    Taro.addInterceptor(responseInterceptor)

    // 封装的请求方法
    const request = (url, method = 'GET', params = {}, needToken = false, header = null) => {
    const {contentType = 'application/json'} = header || {}
    if (url.indexOf(baseUrl) === -1) url = baseUrl + url

    const option = {
    url,
    method,
    data: method === 'GET' ? {} : params,
    header: {'Content-Type': contentType}
    }

    // 处理 token
    if (needToken) {
    const token = Taro.getStorageSync('accessToken')

    if (token) {
    option['header']['Authorization'] = token
    } else {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })

    return
    }
    }

    // 发起请求
    return Taro.request(option)
    }

    export default request





2. ⌈拦截器⌋🍥



拦截器是一个函数,接受 chain 对象作为参数。chain 对象中含有 requestParams 属性,代表请求参数。拦截器最后需要调用 chain.proceed(requestParams) 以调用下一个拦截器或者发起请求。Taro 中的这个拦截器没有请求拦截器和响应拦截器之分,具体看你是在调用 chain.proceed(requestParams) 之前还是之后做的操作。具体说明可查阅官方文档




  • 拦截器中先调用 chain.proceed(requestParams) 发送请求,其返回的是一个 promise 对象,所以可以在 .then 中做响应处理




  • .then 中先判断响应状态码,这里我们只讨论 401 token 过期的情况





  • 当 token 过期时,获取本地存储的 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中







  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { statusCode } from '../config'
    import request from './request'

    // 标识 token 刷新状态
    let isTokenRefreshing = false

    // 存储因为等待 token 刷新而挂起的请求
    let failedRequests = []

    // 设置响应拦截器
    const responseInterceptor = chain => {
    // 先获取到本次请求的参数,后面会使用到
    let {requestParams} = chain

    // 发起请求,然后进行响应处理
    return chain.proceed(requestParams)
    .then(res => {
    switch (res.statusCode) {
    // 404
    case statusCode.NOT_FOUND:
    return Promise.reject({message: '请求资源不存在'})
    // 502
    case statusCode.BAD_GATEWAY:
    return Promise.reject({message: '服务端出现了问题'})
    // 403
    case statusCode.FORBIDDEN:
    return Promise.reject({message: '没有权限访问'})
    // 401
    case statusCode.AUTHENTICATE:
    // 获取 refreshToken 发送请求刷新 token
    // 刷新请求发送前,先判断是否有已发送的请求,如果有就挂起,如果没有就发送请求
    if (isTokenRefreshing) {
    const {url: u, method, params, header} = requestParams
    return failedRequests.push(() => request(u, method, params, true, header))
    }

    isTokenRefreshing = true
    const url = '/auth/refresh-token'
    const refreshToken = Taro.getStorageSync('refreshToken')
    return request(url, 'POST', {refreshToken}, false)
    .then(response => {
    // 刷新成功,将新的 accesToken 和 refreshToken 存储到本地
    Taro.setStorageSync('accessToken', `Bearer ${response.accessToken}`)
    Taro.setStorageSync('refreshToken', response.refreshToken)

    // 将 failedRequests 中的请求使用刷新后的 accessToken 重新发送
    failedRequests.forEach(callback => callback())
    failedRequests = []

    // 再将之前报 401 错误的请求重新发送
    const {url: u, method, params, header} = requestParams
    return request(u, method, params, true, header)
    })
    .catch(err => Promise.reject(err))
    .finally(() => {
    // 无论刷新是否成功,都需要将 isTokenRefreshing 重置为 false
    isTokenRefreshing = false
    })
    // 500
    case statusCode.SERVER_ERROR:
    // 刷新 token 失败
    if (res.data.message === 'Failed to refresh token') {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })
    return Promise.reject({message: '请登录'})
    }

    // 其他问题导致失败
    return Promise.reject({message: '服务器错误'})
    // 200
    case statusCode.SUCCESS:
    return res.data
    // default
    default:
    return Promise.reject({message: ''})
    }
    })
    .catch(error => {
    console.log('网络请求异常', error, requestParams)
    return Promise.reject(error)
    })
    }

    export default responseInterceptor




【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


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

那些阻碍我们前进的学生思维!

每个人都是从学生时代过来的,但是有人在短暂的时间里面脱胎换骨,而有人却保持学生思维五年,十年甚至更久。 说点心里话! 一、务虚与务实 埋头苦干,只能感动自己! 遗憾的事,我上大学的时候并没有参加过任何比赛,倒不是因为我的能力不如别的同学强,而是过于务实,觉得这...
继续阅读 »

每个人都是从学生时代过来的,但是有人在短暂的时间里面脱胎换骨,而有人却保持学生思维五年,十年甚至更久。


说点心里话!


一、务虚与务实


埋头苦干,只能感动自己!


遗憾的事,我上大学的时候并没有参加过任何比赛,倒不是因为我的能力不如别的同学强,而是过于务实,觉得这些太虚了,相反,很多能力不如我的同学,甚至啥也不懂的,他们也参加了不少比赛,也拿了不少奖,这在他们后续的求职中带了不少亮光!


因为大学生参加的比赛,特别是普通大学,说白了,都没什么难度,学校也不会要求做出什么牛逼的东西,更多的是让大学生能够积极参与,扩展自己的视野。


所以,这时候就别去在乎什么技术深浅,因为你做的东西不会有人拿去用的,而抓住每一次演讲,每一次表演,那才是真的锻炼你的能力的时候。


现在,曾经那些我觉得太务虚的人,有的人已经开始创业并且做得不错了,他们的技术虽然不强,但是他们的思维已经发生了改变,说白了,吹牛逼的时候都要硬气一点,这是实话。


职场也是,并不是你埋头苦干就能得到老板的赏识,老板一天那么忙,你不找机会展示自己,他怎么记住你?


换句话来说,PPT的好坏程度大于埋头苦干,无声的付出不如技巧性的展示。


不过话又说回来,务实是一种品质,也是我们应该坚守的,但是更多的时候要做到有策略的务实,而不是埋头的务实,因为我们大多人搞不了科研,做不了什么轰动的学术。


二、人与人的本质是价值的交换


人与人之间要么有情绪价值的交换,要么有经济价值的交换,如果因为情怀,因为理想主义,那么是毫无意义的。


起初不少同学找我解决问题的时候,我都会花费大量的时间去帮他们解决问题,我免费服务的同学个数不下于100个,那段时间我也才毕业参加工作,所以下班时候都花费了很多时间去无偿服务。


一开始我觉得挺开心,毕竟那时候觉得帮助他人,快乐自己嘛!


但是我却花费了这个世界上最宝贵的东西,那就是时间,我本可以去学习,去提升自己,但是却为了自己所谓的情怀,白白浪费时间,可笑的是免费帮助很多人,却没有得到一句感谢,那么这样的情怀有何意义!


后面我改为付费服务,用时间来换取金钱,这才是成年人该做的事。


这一点对于我的改变很大,我觉得最重要的是我有了付费的习惯,告别白嫖思维,我也会花费金钱去购买服务,购买知识,因为我明白免费的大多都不值钱。


三、以弱势来当挡箭牌


之前一个学弟说,他到了新公司,就给他安排了很有难度的工作,他抱怨道,为什么不能对毕业生友好一点,我回了他一句,“去你妈的,想做简单的,那就滚蛋”,拿毕业生来当挡箭牌,首先就是对自己极度不肯定,不相信自己。


也遇到一些同事和朋友总是抱怨道,“我他妈就这点工资,干嘛给我安排这么多,干嘛让我总是出差,干嘛让我做脏活累活”。


如果总是以工资低,自己工作年限短这些来抱怨,那么就别加入职场,职场本来就是残酷的,并不是请你来过好日子的,如果觉得不公,那么就换,没能力换,那么就要硬着头皮干。


四、别特立独行


“同事都是一群傻逼,一群卷狗,我才不想和他们有任何交集,不想同流合污!”


最近看到朋友圈有人骂同事,领导,但是何不自己回想一下,难道自己就那么清高,那么正确吗?


当你在骂别人的时候,其实大多时候骂的是自己。


在职场中如果太特立独行,总觉得谁不对,谁不行,那么就容易树敌,对于自己的职业发展很不好。


可能你离开了这个公司就觉得逃离了,但是如果把矛盾搞大,对于自己后面的职业都或多或少有一定的影响,试想,如果背调比较严格,那么对自己有利吗?


就算我的同事是傻逼,我也依然要和他做“好朋友”,这是一句很有智慧的话!


圆滑当道的世界,太过于尖锐未必是一件好事!


作者:追梦人刘牌
来源:juejin.cn/post/7300781073376509987
收起阅读 »

如何提升人的思考能力和深度?

除非是天才型如爱因斯坦、图灵等可以推动一个时代的人,否则绝大多数人和人之间的差异并不是很大,大家都是聪明的、有能力的。唯一影响人的是思维方式、自律能力和执行能力。而思维方式是其中最影响人的。那么如何提升人的思考能力和深度呢? 如果我们给到两个看起来同等聪明的人...
继续阅读 »

除非是天才型如爱因斯坦、图灵等可以推动一个时代的人,否则绝大多数人和人之间的差异并不是很大,大家都是聪明的、有能力的。唯一影响人的是思维方式、自律能力和执行能力。而思维方式是其中最影响人的。那么如何提升人的思考能力和深度呢?


如果我们给到两个看起来同等聪明的人同样的时间,那么思维方式更高效的人一定是成长更快的。我们在考虑一个问题的时候,我们应当是思考用什么的方式去思考这个问题更好。考虑思考方式,就是思考用什么思路、思维模式才能得到结果。也就是说先思考工作的方法然后再实施。


举个例子,我们想要将工作安排进一个日程表中,那么什么样的工作安排方式才能保证做出来的日程表上没有疏漏呢?那在安排之前我们就需要先确认工作顺序。如果这个日程表还涉及到了其它人,我们可能要先有大致的计划找到其它相关人确认和同意后再去安排时间更好。


比如一个简单的例子:“三个人都是朋友打算一起去海外旅行,应该选择哪个国家作为目的地比较好?”



  • 首先,对照大家的日程,看看三个人的假期的交集是哪些天数。

  • 列出 10 个在交集天数能去的国家,并且通过搜索等方式提取出这些国家的三个特色的关键词(如海边、法餐、卢浮宫等等具有特色的词语。)

  • 三人分别从观光、美食、娱乐、费用等作出预估和评价、一起讨论。

  • 将三人评价最高的国家作为最终去的地方。


从上述的例子中我们可以看出来,即使是一个复杂的、庞大的问题我们都可以将其分解为小问题,针对每个小问题设计论点,分析每个论点就能得到答案。


无论是什么课题,它需要的方法和流程都一样。首先整理和分解;然后对每个方法做数据分析;最后在每一种方法中找出重点,把重点落实到方案上。


那么我们要如何去训练呢?你可以在看到广告啊、新闻的时候立马设计一个课题,比如“香飘飘奶茶_一年卖出三亿多杯,能_环绕地球一圈”,那它是怎么计算出来的呢?是将销售额 x 杯子大小算出来的长度吗?绕地球一圈是指赤道吗?等等。可以哪个小本子记下来所有想到的问题。我们在看一个新闻时可以先对着标题思考几分钟,再去看内容,如此反复你就会越来越厉害。


为了帮助思考,我们可以利用脑图工具,将问题逐步的梳理到更细节的地方。当然我们是需要反馈的,我们可以找到这些领域的专家、朋友帮忙看看有没有什么建议或者存在有没有什么问题。


如果是一个比较大的课题,我们可以把很多现象(事实)收集起来,然后对他们进行分析,得到结论后给出行动建议。在《靠谱》中有提到“天上出现乌云,眼看就要下雨,带上伞比较好。”,这其实是对事实、分析和行动三者的比喻。


其次我们要特别区分现状事实和意见建议。这个意见是基于客观的数据得出来的,还是你个人的推测?还是最近的普遍趋势?谁都不知道。这样的话也就不能进行严密的讨论。区分事实现状、分析研究、行动方案,明确回答“结论”和“依据”。


《简单聊聊因果推断》


但就像我在因果推断里聊的一样,“厘清影响问题的各个元素并理解其间的关系 , 我们才能更好找出所谓问题的杠杆解。” 。我们在解决问题时需要关注问题所处的上下文、系统关系,以及各个元素之间的内部因果与联系。复杂问题之间互为因果、环环嵌套,因而有时难以分析和筛选、聚焦于某个单一问题而创造的方案,往往只是局部优化。为此我们需要分析每个问题的产生原因和影响结果,找到某个问题所关联的上下游问题。我们只能判断发生事件在时间上的先后是否有统计显著性,并不能判断因果。相关性并不能准确的说明因果关系。


我们人工推论出来的是一个在我们收集到的数据上的局部的(我们不是上帝视角,无法全知全能)相关性分析成果,它不一定是事实,可能只是一个猜测、预测。


我们做个复杂一点的课题的训练吧:“Louis Vuitton 将于 2 月 18 日起上调全球产品售价,幅度将在 8% 至 20% 之间。”


我们可以把这个课题拆解为下面几个问题:



  • 为什么 LV 要涨价?

  • 涨价之后会有什么影响?

  • 是否会影响未来销量?


对于第一个问题来说,无论 LV 为什么要涨价,它一定是基于未来奢侈品市场的销售增长预计与成本之间的博弈决定的,所以我们要先分析出未来的销售额预计。这里引申一个概念叫需求弹性。



一个人对某种商品的需求的唯一普遍规律就是:如果其他情况不变,他对此商品的需求会随着对其拥有量的增加而递减。这种递减也许缓慢也许迅速。如果缓慢,那么他对此商品所出的价格,就不会因为他对此商品的拥有量的大量增加而大幅度下降;而且价格的小幅度下降会使他的购买量大幅度增加。



在下方的问题下,我会补上一些来自于权威新闻网站的 “证据”,大家可以自行看看问题的答案是什么。


为什么 LV 要涨价?



  • 未来奢侈品的销售额预测是怎么样的?


欧洲时报11月16日,咨询公司贝恩(Bain)15 日发布的一项研究显示,到 2022 年,全球奢侈品行业的收入将达到 1.4 万亿欧元,按固定汇率计算,比 2021 年增长13%,这一增长应该使 95% 的奢侈品品牌受益。在美国和欧洲市场的推动下,这是“相当可观”的一年。研究预测,2030 年之前,奢侈品行业增长将保持在 3% 到 8% 。



  • 同类品牌是否涨价?


2022 年1月,Chanel 在去年十一月已经提价的基础上,部分款式将再度涨价。其中,Coco Handle 涨价 2000 元,BusinessAffinity 与 Le Boy 等包型的价格会有8%-12% 的涨幅‍‍。过去三年,Chanel 的涨幅已经高达 60%。


Hermès 的涨价基本固定在每年的1月进行,本轮调价每款涨价范围从 500 元至 4300 元不等。Lindy mini 从原来的 46500 元涨到 48700 元,升幅为 4.7%;


Dior 于 1 月 18 日,小号 Lady Dior 手袋售价从 3.6 万涨至 4.1 万,增幅达 13.9%。



  • 官方的理由是什么?


价格调整考虑了生产成本、原材料、运输以及通货膨胀的变化。



  • 官方的理由是否成立?


根据 LVMH 2022 年的财报来看,经常性业务的利润方面,除了下降 3% 的香水和化妆品外,其他业务的利润均有两位数增长,其中精品零售 2022 年的利润较 2021 年激增 48%,时装与皮具的利润增长 22%,手表和珠宝增长 20%,酒类业务的增速 16% 垫底,但酒类的营收和盈利都创自身年度新高。


管全年总销售额增长 17%,但 LVMH 的利润率却持平。该公司决定将其数十亿美元的营销预算增加三分之一,这已经成为奢侈品行业的普遍趋势。品牌需要在广告上投入巨资,以继续证明价格大幅上涨的合理性。瑞银(UBS)分析师估计,去年奢侈品品牌平均涨价 8%,与全球通胀水平大致一致。



  • 中国放开疫情管控是否有影响?


中国在全球个人奢侈品消费中的份额激增,从 2019 年的 38% 至 39% 猛增到 80%。


在12月26日中国内地宣布将于1月8日取消将近三年的防疫政策后,据《华尔街日报》报导,这不仅让世界各国迎来全球最大的旅游消费来源群体,放鬆政策的消息还提振了全球股市,奢侈品股尤其受益,因这意味佔奢侈品市场约很大部份的中国消费者正式归来。


著名奢侈品巨头 LVMH 集团(LVMH Moet Hennessy Louis Vuitton)早前表示,其亚洲(不包括日本)第一季度的收入增长放缓至8%,但仍是其最大的单一市场地区,佔全球总收入的 37%。


为了迎合日益增加的中国消费者,许多欧洲奢侈品牌自疫情前几年已增聘了会中文的员工,并开始专注开发中国游客喜爱的产品。国际管理谘询公司贝恩公司(Bain & Company)的数字显示,虽然中国消费者受疫情限制而有所放缓,今年只佔全球奢侈品支出最多19%,但预计到2030年他们将佔多达四成的市场份额。


预计中国游客要到2024年才会大规模重返欧洲,进一步提振公司销售业绩。


涨价之后有什么影响?


我们拿它在 2022 年 2 月涨价后的表现来看看:



  • 2022 年四季度营业收入 226.99 亿欧元,同比增长 13.3 %,分析师预期 223.5 亿欧元,全年营收 791.84 亿欧元,较 2021 年增长 23%,分析师预期 787.2 亿欧元。

  • 2022年净利润140.84亿欧元,同比增长17%;来自经常性业务的利润为210.55亿欧元,同比增长23%。


是否会影响未来销量?


2022 财年 LVMH 集团在地缘政治和经济形势的不利影响下,仍然创下历史业绩新高,集团销售收入达 792 亿欧元,营业利润达 211 亿欧元,皆增长 23%。


尽管全年成绩单亮眼,但相较第三季度数据,第四季度所有地区的增长有所放缓。去年第四季度,除日本外,亚洲地区的内生性营收同比下降了8%(该公司只报告每季度的内生性变化)。奢侈品研究所的 Milton Pedraza 分享说:“对于所有顶级奢侈品牌和集团来说,2022年的增长主要来自强劲的定价,而来自同店销量增长的比例则要小得多。”


随着中国政府计划逐步重新开放国际旅游,可能还要有几个月时间,中国游客才能重新大量出现在欧洲。这应当会释放消费,因为考虑到价差和退税,巴黎或米兰等城市的奢侈品价格要比中国便宜多达 40%。


按照 LVMH 的预估,中国要到 2024 年才会完全恢复对奢侈品的消费。


英国央行周四也将关键利率上调 0.5 个百分点,连续第 10 次上调关键利率,但释出信号可能很快就会暂停这一系列加息行动,因同比通胀率下降,经济步履蹒跚。


欧洲央行此举是连续第五次大幅加息,使关键利率达到2.5%,创下2008年以来的最高水平。不过这一水平仍然低于美联储和英国央行的利率,前者在周三将利率提高到4.5%至4.75%,后者则在周四早些时候将利率提高到4%。


天达(Investec) 经济学家在给客户的一份报告中写道,中国放松严格的动态清零政策提振了增长前景,而欧洲天气转暖已帮助缓和能源危机的严重程度。他们把对今年全球经济增长的预测从 2.2% 提高到 2.4%。


总结


LVMH 的涨价本质上是希望在通货膨胀下保持增长,如果希望保持增长就需要投放更多的广告和营销,如果要有钱投入就必须要涨价,而要证明涨价是对的那就得继续投入更多的钱。好在是奢侈品集团们对价格的调控都比较“自由”。


其次考虑到欧洲加息和天气转暖,欧洲的整体区域市场份额也会获得提升。按照现在的预测来看全球的经济似乎是在往好的方向增长,中国放开了疫情管控后以及全球旅游业的逐渐恢复可以有效的给 LVMH 带来一个更大的前景,那么在市场仍然是有很大的需要的情况下,即使维持目前的销售量不变,当价格提升时盈利就会上涨。综合上述在经济回暖的情况下、中国疫情管控放开销售量可能会增加,各大奢侈品集团自然而然就会开始进一步涨价追求更高的利润。LVMH 公司表示,即使面对不确定的地缘政治和宏观经济环境,该公司也有信心通过成本控制和选择性投资的政策,保持目前的增长水平。


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

创业一年 | 一名普通前端的血泪史

前言 年初我裸辞创业了,跟一个朋友一起合伙做项目,我主要还是做技术部分,开发一个回收类的项目 也是第一次创业,虽然听过很多道理,自己经历过又是另外一回事 我们的项目经历过高峰,现在算是谷底,基本的情况基本就是在苦苦挣扎 这篇文章我会把我所经历的过程讲述出来,在...
继续阅读 »

前言


年初我裸辞创业了,跟一个朋友一起合伙做项目,我主要还是做技术部分,开发一个回收类的项目


也是第一次创业,虽然听过很多道理,自己经历过又是另外一回事


我们的项目经历过高峰,现在算是谷底,基本的情况基本就是在苦苦挣扎


这篇文章我会把我所经历的过程讲述出来,在最后也会总结一些创业过程的一些经验和避坑指南


希望对你有所帮助


自我介绍 & 背景


我是一名快35岁的前端码农,离职前是在银行做的外包开发。2014年开始从事开发,不过刚开始是做的iOS开发,后来又转了web前端开发


眼看35岁大限快到,内心比较着急,着急的去探索一条以后的出路,我想这应该是码农们以后必须要面对的问题


只是年初的时候,正好有一个契机,我就想趁着还没到35岁之前,主动出击,做一番尝试


当前也是做好了充分的准备


契机


这件事的契机就是我给朋友开发一个小程序,业务量在慢慢的起来;一商量就决定去做这件事了,当然也跟家里商量一下,用一年时间来试错


我的理由是:


对于码农来说,年龄是个很难跨过去的坎,这一步迟早是要来的,我不太想被动的等到那一天,我还是想主动的寻找出路


离开上海


我走之前跟一些朋友做了道别,把自己的行李全部塞进来车里,开着车又去了一个陌生的城市(去朋友的城市),离开的时候真的是感慨万千


有家人的地方才有温暖,老婆小孩都走了,一个人呆在上海特不是滋味


业务的高速发展


我们的业务在上半年还是发展很顺利的,通过抖音直播等方式,吸引用户,用户越来越多,业务也慢慢放量了


我们的注册用户有28万多,高峰时期Api调用量有250万


我们的收入来源有以下几部分


会员费


我们的利润来源之一是 会员费


截止目前为止,合伙人有350个左右,早期的话一个合伙人大概赚1000块,后期一个会员赚2000块,代理商有1700个,前期一个赚100,后来涨价一个赚288元,具体前期多少个,已经不清楚了


细算下来,我们收的会员费都大几十万了,应该是赚钱的


产品的差价


我们利润来源之二是产品的差价


我们回收的产品也是有一点利润的,只是不多,一个才几毛钱,甚至几分钱,但也都是有利润的,需要走量


最高峰的时候,一天的订单有10万单,目前已经萎缩到了5万单左右


这其实也是一部分利润,到目前为止,累计应该有1000万单


广告费


我们利润来源之三就是小程序广告费


想必大家都开发过小程序,小程序后台是有广告费的,我们的广告费也不少


但是很遗憾我刚开始开发的时候用的是 别人的 serverless 的框架,对的,就是 LeanCould,这玩意每天有3W的免费的Api调用量,而我们的量非常大,所以被收取的服务器也是不少的


小程序的广告费基本覆盖了服务器的费用,还略微有些结余


最高的一天广告费有1000多,后期慢慢稳定了就是300左右,而服务器的费用大概在200左右


关于小程的广告费,如果没有新人进入的话。他的广告费会下降的很厉害,做过的都知道


下面贴几张图,下面的小程序的广告收入,大部分就在这里了


image.png


image.png


前期一直用的LeanCould也是有对应服务器的 5W多


image.png


后来我自己买了服务器,写了后端,就迁移了平台,也就是目前在用的平台


image.png


隐患


随着时间的不断的推移,慢慢的发现平台有点入不敷出了,太奇怪了


现在才知道,其实出货不太好出了,我们负责出货的朋友,没有及时沟通,导致我们的囤货大量积压,卖不出去


其实这个现象提前就已经有了,但是负责推广和运营的朋友并不知情,一直都是在推广,提现秒到,风控没有做好


等发现问题的时候,其实是有点晚了,我们的货囤了很多了


现在我们的现金流已经非常的吃紧了


最可怕的是我们囤货的东西市场价值越来越低,很多甚至下游都不收了


失败总结


失败的原因肯是很多的,但总结下来有以下几点


1.公司的流水一定要清晰透明


因为很多小团队,甚至就2-3个人,不管有没有盈利,都要做到财务公开和透明,这样有两个好处


第一 防止钱进入私人口袋


第二 每日的现金流可以让随时有危机感,并且可以随时意识到危机


每日公开现金流真的很重要,重要的事情说三遍


每日公开现金流真的很重要,重要的事情说三遍


每日公开现金流真的很重要,重要的事情说三遍


如果实在做不到每日对账,最少一个礼拜一次


2.每个人的股份也要分清楚


很多时候都是好朋友,熟人,就没有太谈这个方面的事情。但是确切的告诉你一定要大方的谈,因为这个影响到后面的利润分成,很容易出现矛盾


关于技术方面


1. 先用LeanClund 开发


为什么一开始是使用serverLess开发



  • 因为速度快,前期产品从0到1,全部都一个人写,功能还是蛮多的如果又去写前端,又去写后端,还要运维,确实开发速度很慢

  • 自己做前端出生,后端的不太会,虽说增删改查比较简单,但是那只是最基础的功能,这是真实的线上项目,从没有弄过,还是不太会

  • serverLess 我比较熟,之前也开发过一个线上项目,只是 调用量比较小


2. 自己写买服务器,全部自己开发


为什么要自己开发全部代码



  • Api调用量大了之后,LeanCould的费用太高了,我们需要节省成本

  • 我们要做自己的APP,因为小程序很多时候会被投诉被封,APP可以规避风险(当然我们运营的好,一直都比较稳定,没有被封过,虽然也开发了APP,后来还是主要用小程序,而当时用LeanCould里面有一些代码与小程序耦合了一点)

  • LeanCould 无法没有实现事务等,随着使用的加深,发现它在底层上有些不足,自己写云端写很多逻辑也是比较麻烦(比如微信里面的很多都是要 服务器去调用腾讯的接口,不得不在云端写代码)、


主要技术栈



  • 前端小程序用 uni-app

  • 后台管理系统 是用 Vue

  • 后端使用Koa

  • 数据库使用 MySqlRedies


系统的一些数据



  • 目前系统有 28W用户

  • 目前300W的订单量,每日月5W的新增订单(以前的平台有1000多W的订单量,暂停了)


后面的打算


虽然还有2个月时候才过年,但是 今天的尝试 基本算失败了,正在思考明年干什么


看看还有什么项目做,或者有什么生意做(前提是有一些资源,否则就不做)


实在不行,可能还得去先找个工作


希望我的经历可以帮助到你,大家有什么问题可以评论区交流


作者:三年三月
来源:juejin.cn/post/7295353579001806902
收起阅读 »

完爆90%的性能毛病,数据库优化八大通用绝招!

毫不夸张的说咱们后端工程师,无论在哪家公司,呆在哪个团队,做哪个系统,遇到的第一个让人头疼的问题绝对是数据库性能问题。如果我们有一套成熟的方法论,能让大家快速、准确的去选择出合适的优化方案,我相信能够快速准备解决咱么日常遇到的80%甚至90%的性能问题。 从解...
继续阅读 »

毫不夸张的说咱们后端工程师,无论在哪家公司,呆在哪个团队,做哪个系统,遇到的第一个让人头疼的问题绝对是数据库性能问题。如果我们有一套成熟的方法论,能让大家快速、准确的去选择出合适的优化方案,我相信能够快速准备解决咱么日常遇到的80%甚至90%的性能问题。


从解决问题的角度出发,我们得先了解到**问题的原因;其次我们得有一套思考、判断问题的流程方式,**让我们合理的站在哪个层面选择方案;最后从众多的方案里面选择一个适合的方案进行解决问题,找到一个合适的方案的前提是我们自己对各种方案之间的优缺点、场景有足够的了解,没有一个方案是完全可以通吃通用的,软件工程没有银弹。


下文的我工作多年以来,曾经使用过的八大方案,结合了平常自己学习收集的一些资料,以系统、全面的方式整理成了这篇博文,也希望能让一些有需要的同行在工作上、成长上提供一定的帮助。



文章首发公众号:码猿技术专栏



为什么数据库会慢?


慢的本质:


慢的本质
查找的时间复杂度查找算法
存储数据结构存储数据结构
数据总量数据拆分
高负载CPU、磁盘繁忙

无论是关系型数据库还是NoSQL,任何存储系统决定于其查询性能的主要有三种:



  • 查找的时间复杂度

  • 数据总量

  • 高负载


而决定于查找时间复杂度主要有两个因素:



  • 查找算法

  • 存储数据结构


无论是哪种存储,数据量越少,自然查询性能就越高,随着数据量增多,资源的消耗(CPU、磁盘读写繁忙)、耗时也会越来越高。


从关系型数据库角度出发,索引结构基本固定是B+Tree,时间复杂度是O(log n),存储结构是行式存储。因此咱们对于关系数据库能优化的一般只有数据量。


而高负载造成原因有高并发请求、复杂查询等,导致CPU、磁盘繁忙等,而服务器资源不足则会导致慢查询等问题。该类型问题一般会选择集群、数据冗余的方式分担压力。



应该站在哪个层面思考优化?



从上图可见,自顶向下的一共有四层,分别是硬件、存储系统、存储结构、具体实现。层与层之间是紧密联系的,每一层的上层是该层的载体;因此越往顶层越能决定性能的上限,同时优化的成本也相对会比较高,性价比也随之越低。以最底层的具体实现为例,那么索引的优化的成本应该是最小的,可以说加了索引后无论是CPU消耗还是响应时间都是立竿见影降低;然而一个简单的语句,无论如何优化加索引也是有局限的,当在具体实现这层没有任何优化空间的时候就得往上一层【存储结构】思考,思考是否从物理表设计的层面出发优化(如分库分表、压缩数据量等),如果是文档型数据库得思考下文档聚合的结果;如果在存储结构这层优化得没效果,得继续往再上一次进行考虑,是否关系型数据库应该不适合用在现在得业务场景?如果要换存储,那么得换怎样得NoSQL?


所以咱们优化的思路,出于性价比的优先考虑具体实现,实在没有优化空间了再往上一层考虑。当然如果公司有钱,直接使用钞能力,绕过了前面三层,这也是一种便捷的应急处理方式。


该篇文章不讨论顶与底的两个层面的优化,主要从存储结构、存储系统中间两层的角度出发进行探讨


八大方案总结



 数据库的优化方案核心本质有三种:减少数据量用空间换性能选择合适的存储系统,这也对应了开篇讲解的慢的三个原因:数据总量、高负载、*查找的时间复杂度。*


  这里大概解释下收益类型:短期收益,处理成本低,能紧急应对,久了则会有技术债务;长期收益则跟短期收益相反,短期内处理成本高,但是效果能长久使用,扩展性会更好。


  静态数据意思是,相对改动频率比较低的,也无需过多联表的,where过滤比较少。动态数据与之相反,更新频率高,通过动态条件筛选过滤。


减少数据量


减少数据量类型共有四种方案:数据序列化存储、数据归档、中间表生成、分库分表。


就如上面所说的,无论是哪种存储,数据量越少,自然查询性能就越高,随着数据量增多,资源的消耗(CPU、磁盘读写繁忙)、耗时也会越来越高。目前市面上的NoSQL基本上都支持分片存储,所以其天然分布式写的能力从数据量上能得到非常的解决方案。而关系型数据库,查找算法与存储结构是可以优化的空间比较少,因此咱们一般思考出发点只有从如何减少数据量的这个角度进行选择优化,因此本类型的优化方案主要针对关系型数据库进行处理。



数据归档



注意点:别一次性迁移数量过多,建议低频率多次限量迁移。像MySQL由于删除数据后是不会释放空间的,可以执行命令OPTIMIZE TABLE释放存储空间,但是会锁表,如果存储空间还满足,可以不执行。



关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部的java性能调优手册



建议优先考虑该方案,主要通过数据库作业把非热点数据迁移到历史表,如果需要查历史数据,可新增业务入口路由到对应的历史表(库)。



中间表(结果表)



中间表(结果表)其实就是利用调度任务把复杂查询的结果跑出来存储到一张额外的物理表,因为这张物理表存放的是通过跑批汇总后的数据,因此可以理解成根据原有的业务进行了高度的数据压缩。以报表为例,如果一个月的源数据有数十万,我们通过调度任务以月的维度生成,那么等于把原有的数据压缩了几十万分之一;接下来的季报和年报可以根据月报*N来进行统计,以这种方式处理的数据,就算三年、五年甚至十年数据量都可以在接受范围之内,而且可以精确计算得到。


那么数据的压缩比率是否越低越好?下面有一段口诀:



  • 字段越多,粒度越细,灵活性越高,可以以中间表进行不同业务联表处理。

  • 字段越少,粒度越粗,灵活性越低,一般作为结果表查询出来。


数据序列化存储




在数据库以序列化存储的方式,对于一些不需要结构化存储的业务来说是一种很好减少数据量的方式,特别是对于一些M*N的数据量的业务场景,如果以M作为主表优化,那么就可以把数据量维持最多是M的量级。另外像订单的地址信息,这种业务一般是不需要根据里面的字段检索出来,也比较适合。


这种方案我认为属于一种临时性的优化方案,无论是从序列化后丢失了部份字段的查询能力,还是这方案的可优化性都是有限的。


分库分表


分库分表作为数据库优化的一种非常经典的优化方案,特别是在以前NoSQL还不是很成熟的年代,这个方案就如救命草一般的存在。


如今也有不少同行也会选择这种优化方式,但是从我角度来看,分库分表是一种优化成本很大的方案。这里我有几个建议:



  1. 分库分表是实在没有办法的办法,应放到最后选择。

  2. 优先选择NoSQL代替,因为NoSQL诞生基本上为了扩展性与高性能。

  3. 究竟分库还是分表?量大则分表,并发高则分库

  4. 不考虑扩容,一部做到位。因为技术更新太快了,每3-5年一大变。


拆分方式



只要涉及到这个拆,那么无论是微服务也好,分库分表也好,拆分的方式主要分两种:垂直拆分、水平拆分


垂直拆分更多是从业务角度进行拆分,主要是为了**降低业务耦合度;**此外以SQL Server为例,一页是8KB存储,如果在一张表里字段越多,一行数据自然占的空间就越大,那么一页数据所存储的行数就自然越少,那么每次查询所需要IO则越高因此性能自然也越慢;因此反之,减少字段也能很好提高性能。之前我听说某些同行的表有80个字段,几百万的数据就开始慢了。


水平拆分更多是从技术角度进行拆分,拆分后每张表的结构是一模一样的,简而言之就是把原有一张表的数据,通过技术手段进行分片到多张表存储,从根本上解决了数据量的问题。




路由方式



进行水平拆分后,根据分区键(sharding key)原来应该在同一张表的数据拆解写到不同的物理表里,那么查询也得根据分区键进行定位到对应的物理表从而把数据给查询出来。


路由方式一般有三种区间范围、Hash、分片映射表,每种路由方式都有自己的优点和缺点,可以根据对应的业务场景进行选择。


区间范围根据某个元素的区间的进行拆分,以时间为例子,假如有个业务我们希望以月为单位拆分那么表就会拆分像 table_2022-04,这种对于文档型、ElasticSearch这类型的NoSQL也适用,无论是定位查询,还是日后清理维护都是非常的方便的。那么缺点也明显,会因为业务独特性导致数据不平均,甚至不同区间范围之间的数据量差异很大。


Hash也是一种常用的路由方式,根据Hash算法取模以数据量均匀分别存储在物理表里,缺点是对于带分区键的查询依赖特别强,如果不带分区键就无法定位到具体的物理表导致相关所有表都查询一次,而且在分库的情况下对于Join、聚合计算、分页等一些RDBMS的特性功能还无法使用。



一般分区键就一个,假如有时候业务场景得用不是分区键的字段进行查询,那么难道就必须得全部扫描一遍?其实可以使用分片映射表的方式,简单来说就是额外有一张表记录额外字段与分区键的映射关系。举个例子,有张订单表,原本是以UserID作为分区键拆分的,现在希望用OrderID进行查询,那么得有额外得一张物理表记录了OrderID与UserID的映射关系。因此得先查询一次映射表拿到分区键,再根据分区键的值路由到对应的物理表查询出来。可能有些朋友会问,那这映射表是否多一个映射关系就多一张表,还是多个映射关系在同一张表。我优先建议单独处理,如果说映射表字段过多,那跟不进行水平拆分时的状态其实就是一致的,这又跑回去的老问题。


用空间换性能


该类型的两个方案都是用来应对高负载的场景,方案有以下两种:分布式缓存、一主多从。


与其说这个方案叫用空间换性能,我认为用空间换资源更加贴切一些。因此两个方案的本质主要通数据冗余、集群等方式分担负载压力。


对于关系型数据库而言,因为他的ACID特性让它天生不支持写的分布式存储,但是它依然天然的支持分布式读



分布式缓存



缓存层级可以分好几种:客户端缓存API服务本地缓存分布式缓存,咱们这次只聊分布式缓存。一般我们选择分布式缓存系统都会优先选择NoSQL的键值型数据库,例如Memcached、Redis,如今Redis的数据结构多样性,高性能,易扩展性也逐渐占据了分布式缓存的主导地位。


缓存策略也主要有很多种:Cache-AsideRead/Wirte-ThroughWrite-Back,咱们用得比较多的方式主要**Cache-Aside,**具体流程可看下图:



我相信大家对分布式缓存相对都比较熟悉了,但是我在这里还是有几个注意点希望提醒一下大家:



关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部的java性能调优手册



避免滥用缓存


缓存应该是按需使用,从28法则来看,80%的性能问题由主要的20%的功能引起。滥用缓存的后果会导致维护成本增大,而且有一些数据一致性的问题也不好定位。特别像一些动态条件的查询或者分页,key的组装是多样化的,量大又不好用keys指令去处理,当然我们可以用额外的一个key把记录数据的key以集合方式存储,删除时候做两次查询,先查Key的集合,然后再遍历Key集合把对应的内容删除。这一顿操作下来无疑是非常废功夫的,谁弄谁知道。



避免缓存击穿


当缓存没有数据,就得跑去数据库查询出来,这就是缓存穿透。假如某个时间临界点数据是空的例如周排行榜,穿透过去的无论查找多少次数据库仍然是空,而且该查询消耗CPU相对比较高,并发一进来因为缺少了缓存层的对高并发的应对,这个时候就会因为并发导致数据库资源消耗过高,这就是缓存击穿。数据库资源消耗过高就会导致其他查询超时等问题。


该问题的解决方案也简单,对于查询到数据库的空结果也缓存起来,但是给一个相对快过期的时间。有些同行可能又会问,这样不就会造成了数据不一致了么?一般有数据同步的方案像分布式缓存、后续会说的一主多从、CQRS,只要存在数据同步这几个字,那就意味着会存在数据一致性的问题,因此如果使用上述方案,对应的业务场景应允许容忍一定的数据不一致。


不是所有慢查询都适用


一般来说,慢的查询都意味着比较吃资源的(CPU、磁盘I/O)。举个例子,假如某个查询功能需要3秒时间,串行查询的时候并没什么问题,我们继续假设这功能每秒大概QPS为100,那么在第一次查询结果返回之前,接下来的所有查询都应该穿透到数据库,也就意味着这几秒时间有300个请求到数据库,如果这个时候数据库CPU达到了100%,那么接下来的所有查询都会超时,也就是无法有第一个查询结果缓存起来,从而还是形成了缓存击穿。


一主多从



常用的分担数据库压力还有一种常用做法,就是读写分离、一主多从。咱们都是知道关系型数据库天生是不具备分布式分片存储的,也就是不支持分布式写,但是它天然的支持分布式读。一主多从是部署多台从库只读实例,通过冗余主库的数据来分担读请求的压力,路由算法可有代码实现或者中间件解决,具体可以根据团队的运维能力与代码组件支持视情况选择。


一主多从在还没找到根治方案前是一个非常好的应急解决方案,特别是在现在云服务的年代,扩展从库是一件非常方便的事情,而且一般情况只需要运维或者DBA解决就行,无需开发人员接入。当然这方案也有缺点,因为数据无法分片,所以主从的数据量完全冗余过去,也会导致高的硬件成本。从库也有其上限,从库过多了会主库的多线程同步数据的压力。



选择合适的存储系统


NoSQL主要以下五种类型:键值型、文档型、列型、图型、搜素引擎,不同的存储系统直接决定了查找算法存储数据结构,也应对了需要解决的不同的业务场景。NoSQL的出现也解决了关系型数据库之前面临的难题(性能、高并发、扩展性等)。


例如,ElasticSearch的查找算法是倒排索引,可以用来代替关系型数据库的低性能、高消耗的Like搜索(全表扫描)。而Redis的Hash结构决定了时间复杂度为O(1),还有它的内存存储,结合分片集群存储方式以至于可以支撑数十万QPS。


因此本类型的方案主要有两种:**CQRS、替换(选择)存储,**这两种方案的最终本质基本是一样的主要使用合适存储来弥补关系型数据库的缺点,只不过切换过渡的方式会有点不一样。



CQRS


CQS(命令查询分离)指同一个对象中作为查询或者命令的方法,每个方法或者返回的状态,要么改变状态,但不能两者兼备 



讲解CQRS前得了解CQS,有些小伙伴看了估计还没不是很清晰,我这里用通俗的话解释:某个对象的数据访问的方法里,要么只是查询,要么只是写入(更新)。而CQRS(命令查询职责分离)基于CQS的基础上,用物理数据库来写入(更新),而用另外的存储系统来查询数据。因此我们在某些业务场景进行存储架构设计时,可以通过关系型数据库的ACID特性进行数据的更新与写入,用NoSQL的高性能与扩展性进行数据的查询处理,这样的好处就是关系型数据库和NoSQL的优点都可以兼得,同时对于某些业务不适于一刀切的替换存储的也可以有一个平滑的过渡。


从代码实现角度来看,不同的存储系统只是调用对应的接口API,因此CQRS的难点主要在于如何进行数据同步。


数据同步方式



一般讨论到数据同步的方式主要是分拉:


推指的是由数据变更端通过直接或者间接的方式把数据变更的记录发送到接收端,从而进行数据的一致性处理,这种主动的方式优点是实时性高。


拉指的是接收端定时的轮询数据库检查是否有数据需要进行同步,这种被动的方式从实现角度来看比推简单,因为推是需要数据变更端支持变更日志的推送的。


而推的方式又分两种:CDC(变更数据捕获)和领域事件。对于一些旧的项目来说,某些业务的数据入口非常多,无法完整清晰的梳理清楚,这个时候CDC就是一种非常好的方式,只要从最底层数据库层面把变更记录取到就可。


对于已经服务化的项目来说领域事件是一种比较舒服的方式,因为CDC是需要数据库额外开启功能或者部署额外的中间件,而领域事件则不需要,从代码可读性来看会更高,也比较开发人员的维护思维模式。



替换(选择)存储系统


因为从本质来看该模式与CQRS的核心本质是一样的,主要是要对NoSQL的优缺点有一个全面认识,这样才能在对应业务场景选择与判断出一个合适的存储系统。这里我像大家介绍一本书马丁.福勒《NoSQL精粹》,这本书我重复看了好几遍,也很好全面介绍各种NoSQL优缺点和使用场景。


当然替换存储的时候,我这里也有个建议:加入一个中间版本,该版本做好数据同步与业务开关,数据同步要保证全量与增加的处理,随时可以重来,业务开关主要是为了后续版本的更新做的一个临时型的功能,主要避免后续版本更新不顺利或者因为版本更新时导致的数据不一致的情况出现。在跑了一段时间后,验证了两个不同的存储系统数据是一致的后,接下来就可以把数据访问层的底层调用替换了。如此一来就可以平滑的更新切换。


结束


本文到这里就把八大方案介绍完了,在这里再次提醒一句,每个方案都有属于它的应对场景,咱们只能根据业务场景选择对应的解决方案,没有通吃,没有银弹。


这八个方案里,大部分都存在数据同步的情况,只要存在数据同步,无论是一主多从、分布式缓存、CQRS都好,都会有数据一致性的问题导致,因此这些方案更多适合一些只读的业务场景。当然有些写后既查的场景,可以通过过渡页或者广告页通过用户点击关闭切换页面的方式来缓解数据不一致性的情况。


作者:码猿技术专栏
来源:juejin.cn/post/7185338369860173880
收起阅读 »

早起、冥想、阅读、写作、运动

周岭在《认知觉醒》一书中提出了快速改变人生的五件事,即:「早起」、「冥想」、「阅读」、「写作」、「运动」。低调务实优秀中国好青年交流群也正是从这 5 件事入手,帮你养成好习惯。我也试着实践了有将近一年的时间,今谈谈收获与心得。 早起 一日之计在于晨,一年之计在...
继续阅读 »

周岭在《认知觉醒》一书中提出了快速改变人生的五件事,即:「早起」、「冥想」、「阅读」、「写作」、「运动」。低调务实优秀中国好青年交流群也正是从这 5 件事入手,帮你养成好习惯。我也试着实践了有将近一年的时间,今谈谈收获与心得。


早起


一日之计在于晨,一年之计在于春;早起是个老生常谈的话题了,鲁迅先生小时候为了上课不迟到,还把「早」刻在桌上告诫自己。我小时候每天晚上吃完饭,没什么事早早地就睡了,甚至觉得十点睡觉都是一件很可怕的事。如今呢,自从步入互联网时代,十点?不好意思,十点夜生活才刚刚开始。


秉承着先僵化、后优化、再固化的原则,我决定尝试一段时间。起初几天是真的很难受,白天浑浑噩噩的完全提不起精神。不过慢慢的,晚上倒是越来越早的睡了。差不多半个月时间几乎都习惯了 10 点左右睡觉,6 点前起床。正常早上六点起床后,稍微锻炼一会回来坐那下下汗,冲个凉水澡,然后吃个早饭就去工作了。


持续了有半年时间,直观感受就是身体越来越好,精神头越来越棒;但我并不认为这是早起带来的,潜移默化改变了我的是生活规律。毕竟美国人时差和咱们完全反着来,也没见几个英年嗝屁的。现在为止,我想早起也许就真的只是早点起来罢了。


但有一天,我翻看着旧日的朋友圈:星光不问赶路人,豁然开朗。也深刻地认识到了自己的肤浅,早起其实并不只意味着早点起来罢了。想象一下,如果明天要和女神约会?或者新工作的第一天?不用考虑肯定早早的就起来收拾了,因为你开心,快乐,幸福;甚至要迎来人生新阶段了。所以早起真谛可能不仅仅是早点起来,更重要的是进一步寻找人生的意义,创造生命的价值,为我所热爱奋斗终生!


冥想


关于冥想,老实说太高端了,高端到有点不接地气,反正 100 个人有 100 个见解。刚开始还看了各种视频、翻了有关的书、试了各种动作体验冥想、有没有效果我不清楚,不过睡得倒很快。


感受呼吸、扫描身体、提升专注力,但越努力就越凌乱……由于不能形成持续的正反馈,所以我有点消极。去你的冥想,浪费生命。后续冥想也是断断续续的持续了好久,那天想起来就尝试一下,想不起来就算了。


直到有阵子,忘记具体在做什么,总之就是在写代码。从上班来坐那,要不是同事喊我,还真没感觉一个上午都过去了……也是瞬间明白了《十分钟冥想》中:心流。


我把冥想定义为心无杂念、极致专注。但是早期的努力只是停留在表面上而没有透彻地理解。我认为冥想最重要的一点:感知力、尝试学会深入感受身体各个部位,体会情绪在大脑波动,品尝茶水在身体流淌,体会世间万物。


一个小和尚问得道的师父:“您得道之前做什么?”

老和尚说:“砍柴、挑水、做饭。”

“那得道之后呢?”小和尚继续问道。

老和尚回答:“还是砍柴、挑水、做饭。”

小和尚一脸疑惑:“师父,这不是一样吗?”

老和尚说:“当然不一样。得道前,我砍柴时惦记着挑水,挑水时惦记着做饭;得道后,砍柴即砍柴,挑水即挑水,做饭即做饭。”

阅读


生命是有限的,但书籍却是无限的,怎么用有限的生命阅读无限的书籍呢?根据不科学统计,人的一生最多只能阅读 15000 本书籍,那估计是没有一个人可以活着读完。所以我们应该要追求精读细阅和高质量的阅读。


首先要会读书,读好书。《如何阅读一本书》就非常详细的讨论了怎么样阅读一本书,尽管有关读书的方法论确实很好,但我觉得阐述得太过重复啰嗦。其实读书喜欢什么就读什么,不要拘泥于阅读世界名著,人文哲理。但我建议少读都市言情,穿越爽文,其可吸收的营养价值较少。具体想怎么读就怎么读,咬文嚼字、一目十行都无所谓,但是这一种读法仅限于是一本好书的情况下。可是究竟什么是好书呢?追随那些走的快的人,阅读其推荐的书单。


假如面临的是一本新书,那么你可以尝试:



  1. 深入了解书的作者、写作的背景。

  2. 详细阅读书的自序、引言、大纲、目录等重要信息。

  3. 快速翻阅书中的部分章节。如果感觉这本书很有价值,那就接着继续。

  4. 带着疑问追随作者的步伐,选择最适合的方式阅读。

    1. 这本书讲了什么?

    2. 作者细说了什么?

    3. 作者的观点是否正确?

    4. 作者讲的和我有什么关系?



  5. 收获体会,记录笔记。


再分享一种进一步的阅读方法:主题阅读。在某个类目中挑选同方向的若干本书,然后确认自己研究的主题和方向。



  1. 依次阅读每本书。

  2. 理清问题、界定主题。

  3. 与不同作者达成共识。

  4. 分析讨论。


写作



我学生时期其实最厌恶写作了……为什么会是你给我段话,让我来研究一下它怎么想的,然后再为你阐述一下自己的观点。我 TM 怎么知道他想什么,爱想什么想什么。



写作实际上可以和阅读相结合,从而构成完美的闭环。


不知道是不是只有自己写作效率低,感觉自己就像间歇泉,总是时不时的迸发灵感。但有时候喷多了,我还写不下来。所以我一般阅读书籍的时候总是会主动掺杂一些技术类目书籍,这样既有助于提高专业技能,又能留足思考时间。


写作我倒没啥可分享心得的,随心所欲,不必整的很累。但必须重视以下三点:



  1. 务必不要出现错字。

  2. 一定要正确地运用标点符号和合理地分段。

  3. 确保文章整体阅读流畅性。


运动


生命在于运动,如只老乌龟一样冲击活 100 年!


运动锻炼不局限于任何形式,爬楼梯也可以,最重要的是生活态度。千万不要眼高手低,今天运动明天就想超越博尔特,持续保持正反馈,日拱一卒,冲吧骚年!


如果不知道如何下手,可以参考我的 wiki 手册:健身手册




其实吧,哪怕你尝试了「早起」、「冥想」、「阅读」、「写作」、「运动」,也不可能立刻获得收获。过去既然无法改变,未来更不知道何去。


那么请尝试着慢一点,慢一点,再慢一点,也许当你回头那刻,轻舟已过万重山。



来源:早起、冥想、阅读、写作、运动 - 7Wate‘s blog



作者:7Wate
来源:juejin.cn/post/7210298403070722105
收起阅读 »

低p程序员互联网求生指南

大家好,我小j。 先做个自我介绍,我在国内大厂担任数年的开发工作,但是回顾我的职业生涯,我认为还是充满遗憾和失望的,中间做过几次错误的选择。在此,刚好借助这个文章,我想回顾下之这数年的职业生涯,点出失败的教训,描述下在我认为的互联网公司能安全度日,谋求晋升的要...
继续阅读 »

大家好,我小j。


先做个自我介绍,我在国内大厂担任数年的开发工作,但是回顾我的职业生涯,我认为还是充满遗憾和失望的,中间做过几次错误的选择。在此,刚好借助这个文章,我想回顾下之这数年的职业生涯,点出失败的教训,描述下在我认为的互联网公司能安全度日,谋求晋升的要点以及说点大实话,希望能给各位读者学习的地方。想法很跳跃,大家根据章节观看。如果你有不认可的地方,都是你对,是我太失败。


观点


要对大厂祛魅


我一定要第一个提这个点。

起因是最近在很多技术群看到大家都对大厂开发的身份非常崇拜,觉得大厂的人一定比小厂中厂优秀,大厂的人说话一定是对的云云,大厂的技术一定更好,而且在国内论坛上和学生辩论过大厂的观念,让我这个前员工深受震撼。所以一定要找机会聊一下这个话题,可能会伤害朋友们的感情,但是还是想聊一下。



  1. 大厂人是不是一定非常优秀

    不是,国内大厂在黄金时期大部分是冗余性招人,以应对每年的绩效考核和未来可能的业务拓展,一个管理一定要懂怎么要新财年的hc,这样才能保住完成手下大部分的开发任务。

    大厂面试的默认逻辑会导致大量学校好、学历好、掌握面试技巧的人进入大厂,这也是为什么很多知识星球,面试指南,面经,小册盛行的原因。

  2. 剖析大厂开发

    在我们了解了这个前提下,解析下大厂开发,其实也符合二八原则,大厂20%左右的员工是真的有经验有天赋的超能力者,他们去实现架构,完成整个开发流程套件、开发系统的开发。而大量的员工实际上是在其他大佬规划好的线路上填api糊业务罢了,完成基础职务,之后再开发各种okr项目来满足绩效的要求。从技术的角度来看,大部分大厂开发实际平均水平也没有那么高,也是业务仔罢了。

  3. 大厂真正优秀的是什么


    • 大厂真正优秀的是有一些内部架构大佬完成一套完善的开发套件以及设置开发流程,让每个参与大厂的开发都有相对不难受的开发体验以及完整的上线监控流程。让不同水平的开发都足以完成他们被要求完成的任务。




    • 大厂优秀的是有远超中小厂的业务体量、薪资福利。




    • 大厂优秀的是身边同事基本都很优秀,都有自己能学习的点,也是未来的社交圈子。




    • 大厂优秀的是能让你熟悉一整套成熟的开发流程,需求评审-开发评审-业务开发-发布提测-正式上线-日志监控-热修回滚。让你了解一整个应用的开发方式。




    • 大厂优秀的是能给你简历加分,带来未来可能的发展机会。




    • 大厂优秀的还有像ata、学城这种前人留下的各种资料可供学习,虽然很多水文但是也远比简中外面的文章靠谱。




    • 大厂优秀的是有更多owner项目的机会,让你能有机会发挥自己的能力大展拳脚。






身为大厂人,应该清楚现在的成就是自己的能力还是平台给的机会,戒骄戒躁。

身为非大厂人,也不要太神话大厂,其实屎山一样很多,大家还是理性讨论。


说句政治不正确的,很多大厂的成功除了依托本身人才和领导层的慧眼以外,更多还是依托时代背景,时势造英雄。 为什么目前环境小厂发育艰难,因为一旦你业务达到一定水平足以引起大厂注意以后,大部分大厂都会提出收购,如果你统一收购就会并入大厂之中。如果你不同意收购,他们会利用自己的雄厚财力定点爆破你的员工,抄袭你的创意,诋毁你的业务,抢走你的客户。当前创业不仅要选对市场,还要顶得住大厂的摧残。


高考很重要,学历很重要,专业很重要


虽然可能看到文章的人大多数已经就业或者在大学学习,但是我还是想提这个点。

诚然,互联网开发已经算是不看学历,不看出身的几个职业之一,但是在相同水平的一群求职者中,面试官还是更愿意招自己的校友、学历好看的人、专业对口的人。这个也算是一个潜规则,从好学校毕业中得到一个好员工的概率确实比从一般的学校中挑到前几名的概率大。虽然我们说宁做宁做鸡头不做凤尾,但是现实生活往往是凤尾的平均境遇比鸡头被伯乐适中的概率高,不要去赌自己能被人发掘,要尽量凑到高水平人群中,往往被人选择的机会更大。


选择高校的排名大概就是综合排名>行业内专业知名度>高校所在城市(影响你的实习选择)。


要承认和接受互联网里的圈子


首先叠个甲,这块并不是说圈子一定是好事,但是目前的环境圈子确实能在职业发展中帮助你迅速提高,这个圈子包括老乡圈、学校圈、公司圈(比如bidu帮、ali帮)、技术圈、老同事圈(etc.),大家在一个陌生环境中还是会倾向去找自己有关系的人,结成圈子,铁打一块,在一个圈子里,对你面试过关,绩效提高,晋升都有帮助。


互联网也需要情商,也有嫡系


很多人包括我之前对程序员的理解也是不用跟人打交道,只需要在电脑上完成开发任务即可,但是实际的工作生涯中,因为你的绩效是人评,你的晋升是人评。不可避免还是要多跟人打交道。跟+1(组长)的关系,跟+2(部门老板)的关系或多或少还是对你的结果有一些影响,我并不是说让大家去舔,但是起码要有一些眼力见,做该做的事情。


聊完了前面几个很油的话题之后,我们回归到实际开发生活中


尽量选择大厂,注意识别子公司和外包


虽然我们之前想让大家对大厂祛魅,但是目前来看进入大厂还是能带来更多的收入和晋升机会以及未来的跳槽机会,而且你未来的同事圈层也会更为优秀,要知道这些人就是你未来的内推池,在互联网,互帮互助永远比单打独斗更好。在同等情况下,我们肯定推荐大厂offer>小厂offer,核心offer>边缘bu offer。大厂的卷虽然不一定能一定让你收益,但是很多小厂卷都卷不出收益,从roi来看,大厂核心部门是我们的就职首选。

但是也要分清大厂、大厂子公司和外包。有些公司虽然名义上是大厂子公司,但是无法享受大厂的权益,属于是披着羊皮卖狗肉,环境不好的时候选择先去子公司呆着无可厚非,但是如果你一心想参加大厂,却选错了bu,可能会浪费一段时间。

尽量不要选择外包,国内目前对外包的开发经历还是或多或少有一些歧视的,这个歧视不会表现在jd里,而是hr简历关、面试中可能因为你的背景一键否定。


领导的意义远大于你的想象


一定要珍惜一个好的领导
在相同水平的公司选择下,重要性上我认为有资源的部门>领导nice程度>>>>>>其他因素。

有潜力/大老板亲自抓的业务能带来更多的晋升机会,而且窗口期进入也很容易,一旦做大了容易成为骨干,后续owner项目机会大(前提不被后续老板亲信空降摘桃子)。
不好但是有资源的部门也能分一杯羹。
但是领导作为你天天见面的人,对你的影响比任何都大,一个理想中的领导不一定技术非常牛逼,但是一定是懂得对外抢肉抢功劳,对内帮助内部成员成长,懂得规划成员的晋升路径,及时跟进组员问题,适当提携帮助的人。由此可以看出来,跟着一个好的领导,不仅有利于工作情绪,也会让你一路顺利的走上晋升之路。

相反,遇见一个不合适的领导,不仅经常pua,不下班,还经常让你背c,没有晋升机会,不如趁早活水骑驴找马。离职原因无非钱给少了,心受委屈了,坏领导能让你同时体会两种感受。


学会和领导汇报工作


新人经常做错的一个事情就是闷头干活,不会汇报,不会报功。要知道领导不可能了解每个人的进度和开发内容,每周的周报是唯一的汇报途径。如果你所做的内容不被领导知道,那么又怎么表现你的价值呢?所以,要学会跟领导汇报进度,可以是每次做完一个阶段后发一个简略的阶段报告,亦或是遇到问题时及时和老板沟通目前的困难以及可能能解决的方法。让老板充分了解你的工作,才能帮你去谋求进一步向+2的汇报,不要做一个只会闷头干活的老黄牛。


学会跟领导提出想法


承接上个话题,举一个例子。如果我们想晋升涨薪,完全可以oneone的时候跟老板提出想法:老板你看我如果想晋升/涨薪,我应该去做哪些内容/完成哪些目标呢。从领导的回答也可以看得出他对你的态度



  1. 如果他认真回答,给你列好路径,那么说明晋升/涨薪还是很有希望的,这也是身为领导应该去做的事——规划自己小弟晋升,那么就按着他的路子付出努力实现

  2. 如果他给你画饼,打哈哈。说明你不是嫡系,可能需要在多做一些事情引起他的注意。

  3. 如果他完全无视这个话题,说明他完全没考虑你的晋升情况,那么这个时候就该考虑后路了


要学会onoene找老板沟通,不仅是让老板知道你最近的情况,也是了解老板对你的态度的时候,要学会双向沟通。


不要过多的嫌弃分配的业务


大部分大厂的业务并没有太高的技术含量,尤其像业务部门的活动业务和基建部门的客服业务,我们要清楚的认识到工作就是给自己赚窝囊费的,只要钱给足,业务什么样都是可以接受的。但是在完成日常业务的时候,我们可以考虑如何优化自己手里的活,怎么让自己手里的活效果更好,这方面的助力是有助于老板看到你的亮点,理解到你的能力的。而不是经常抱怨任务烂,不想做。


学会与人为善,维护自己的朋友圈


要知道,你身边的朋友大多技术不弱于你,未来这些人都是你可能的内推对象和被内推对象,要学会与人为善,尽量不要和同事闹冲突,最好之后也经常保持联系,万一之后有内推的机会这些都是潜在的大腿,要知道无论是国内还是国外,招人的第一选择永远是内推,维护好自己的朋友圈,早晚会得到助力。


多贴近业务,了解业务流程


不要只会做一个执行者,在日常的业务开发中要尽量的去学习业务的流程,了解整个bu的运转方法,盈利方法,这样在需求会上你也能提出自己的意见。多和产品和运营聊天,了解业务数据。这样你也能对bu下一步是进一步发展还是收缩有一定预期,提前规划下一步自己的努力方向。


要学会投巧的发展


首先感谢群友大编制的提出。人不可能是全能的,一定要有一定的不可替代性和独特性,如何在一个团队中脱颖而出,除了本身真的足够优秀以外,还可以投巧的发展,举个例子,在一个前端业务团队,普遍大家都会脚手架配置和组件化,拥有这些技能不稀奇,但是如果A会可视化开发,B会nodejs,那么这两个同学在这个团队中就容易显得更亮眼,如果大家的业务都是糊业务,这两个同学在所学技能上稍微多点优化就容易获得更好的绩效。


要有技术追求,但不要太沉迷在公司成长


虽然现在大家也基本上认识到成长不能靠公司了,大部分公司的日常业务开发技术含量并没有那么高,不要妄想在日常日复一日的业务中提高自己的技术水平,那只能提高你糊业务的熟练度和经验。如果想追求技术的提高,还是要靠工作之余的okr项目或者是自己的私下的学习。但是,想在目前国内这个环境中稳定成长厚积薄发,还是不能放弃技术追求,技术经验在身,就算面对裁员风险也不慌。我们要卷,要以提高技术、增加晋升机会,有目的的卷,无效的卷不仅带不来收益,还能带来同事的鄙夷和icu的风险。


承认天赋的差距,在团队中不掉队


程序开发确实是一个天赋的职业,要承认天赋和能力的差距,达到日常业务线上0bug、0delay的60分目标不难,但是想更进一步确实需要正确的方向和努力,我们做不到比所有人都优秀,只需要做到在团队中不掉队,不是最差的几个,就能尽可能的保证在裁员大潮幸存。


在开发之余,考虑自己的长处


目前国内环境比较恶劣,35的达摩克利斯之剑悬在每个人头上,一方面大厂hc在缩小,创新项目在关停,就算你再自信,一但没有hc,也没有换岗位的机会。另一方面随着年龄的增长,初级中级开发的大门也随之关闭,一但你在某个年龄段没有达到对应的职级,就容易被视作失败。而跳槽更要看目标公司有没有对应职级的坑位,职级越高坑位越少。目前高龄开发的环境还是比较恶劣的。

在这之上,我们要考虑是否找到另一个赛道,发挥自己的长处。能有效延缓焦虑,降低未来的风险


总结


说了这么多,也是我目前的一些浅薄经验纸上谈兵,至少从我的职业经验来看,并没有做到以上的内容,还是一个owner项目的大头兵,写了这些内容,也是希望新人不撞我的南墙,老人提前规划后路。欢迎大家多多交流,让国内有一个更好的程序员成长环境。

永远不要忘了,家人爱人和朋友是你永远的后盾,在你坚持不住,想投降的时候,记得你永远有一个避风港存在,一个成功的人永远离不开背后默默支持的人,工作只是我们生活中的一小方面,善待朋友,珍爱亲人,才是我们一直要做的事情。不要因为工作伤害爱你的人。


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

开发企业微信群机器人,实现定时提醒

大家好,我是鱼皮,今天分享一个用程序解决生活工作问题的真实案例。 说来惭愧,事情是这样的,在我们公司,每天都要轮流安排一名员工(当然也包括我)去楼层中间一个很牛的饮水机那里接水。但由于大家每天都有自己的工作,经常出现忘记接水的情况,导致大家口渴难耐。 怎么解决...
继续阅读 »

大家好,我是鱼皮,今天分享一个用程序解决生活工作问题的真实案例。


说来惭愧,事情是这样的,在我们公司,每天都要轮流安排一名员工(当然也包括我)去楼层中间一个很牛的饮水机那里接水。但由于大家每天都有自己的工作,经常出现忘记接水的情况,导致大家口渴难耐。


怎么解决这个问题呢?


我想到了几种方法:


1)每天大家轮流提醒。但是别说提醒别人了,自己都不记得什么时候轮到自己接水。


2)由一个员工负责提醒大家接水,必要时招募一个 “接水提醒员”。


3)在企业微信的日历功能给员工安排接水日程,就像下面这样:



但问题是我们的人数和天数不是完全对应的、反复安排日程也很麻烦。


你觉得上面哪种方案好呢?其实我觉得第二个方案是最好的 —— 招募一个 “接水提醒员”。


别笑,我认真的!


只不过这个 “接水提醒员” 何必是人?


没错,作为一名程序员,我们可以搞一个机器人,让它在企业微信群聊中每天提醒不同的员工去接水即可。


其实这个功能和员工排班打卡系统是很类似的,只不过更轻量一些。我也调研了很多排班系统,但是都要收费,索性自己开发一个好了。


在企业微信中接入机器人其实非常简单,因为企业微信官方就支持群聊机器人功能,所以这次的任务我就安排给了实习生,他很快就完成了,所以我相信大家应该也都能学会~


企微群聊机器人开发


学习开发第三方应用时,一定要先完整阅读官方文档,比如企业微信群机器人配置文档。



指路:developer.work.weixin.qq.com/document/pa…




设计 SDK 结构


虽然我们的目标是做一个提醒接水机器人,但是企业微信群聊机器人其实是一个通用的功能,所以我们决定开发一个企微机器人 SDK,以后公司其他业务需要时都能够快速复用。(比如开发一个定时喝水提醒机器人)


设计好 SDK 是需要一定技巧的,之前给大家分享过:如何设计一个优秀的 SDK ,可以阅读参考。


在查阅企微机器人文档后,了解到企业微信机器人支持发送多种类型的消息,包括文本、 Markdown 、图片、图文、文件、语音和模块卡片等,文档中对每一种类型的请求参数和字段含义都做了详尽的解释。



吐槽一下,跟微信开发者文档比起来,企微机器人的文档写得清晰多了!



企微文本消息格式


企微文本消息格式


由于每种消息最终都是要转换成 JSON 格式作为 HTTP 请求的参数的,所以我们可以设计一个基础的消息类(Message)来存放公共参数,然后定义各种不同的具体消息类来集成它(比如文本消息 TextMessage、Markdown 消息 MarkdownMessage 等)。


为了简化开发者使用 SDK 来发送消息,定义统一的 MessageSender 类,在类中提供发送消息的方法(比如发送文本消息 sendText),可以接受 Message 并发送到企业微信服务器。


最终,客户端只需调用统一的消息发送方法即可。SDK 的整体结构如下图所示:



值得一提的是,如果要制作更通用的消息发送 SDK。可以将 MessageSender 定义成接口,编写不同的子类比如飞书 MessageSender、短信 MessageSender 等。


开发 SDK


做好设计之后,接下来就可以开始开发 SDK 了。


步骤如下:



  1. 获取 webhook

  2. 创建 SDK 项目

  3. 编写代码

  4. SDK 打包

  5. 调用 SDK


1、获取 webhook


首先,必须在企业微信群聊中创建一个企业微信机器人,并获取机器人的 webhook。


webhook 是一个 url 地址,用于接受我们开发者自己服务器的请求,从而控制企业微信机器人。后续所有的开发过程,都需要通过 webhook 才可以实现。



复制并保存好这个 Webhook 地址,注意不要泄露该地址!



2、创建 SDK 项目


SDK 通常是一个很干净的项目,此处我们使用 Maven 来构建一个空的项目,并在 pom.xml 文件中配置项目信息。


需要特别注意的是,既然我们正在创建一个 SDK,这意味着它将被更多的开发者使用。因此,在配置 groupId 和 artifactId 时,我们应当遵循以下规范:



  • groupId:它是项目组织或项目开发者的唯一标识符,其实际对应的是 main 目录下的 Java 目录结构。

  • artifactId:它是项目的唯一标识符,对应的是项目名称,即项目的根目录名称。通常,它应当为纯小写,并且多个词之间使用中划线(-)隔开。

  • version:它指定了项目的当前版本。其中,SNAPSHOT 表示该项目仍在开发中,是一个不稳定的版本。


以下是我们配置好的项目信息:


<groupId>com.yupi</groupId>
<artifactId>rtx-robot</artifactId>
<version>1.0-SNAPSHOT</version>

为了让我们的项目更加易用,我们还要能做到让开发者通过配置文件来传入配置(比如 webhook),而不是通过硬编码重复配置各种信息。


所以此处我们把项目只作为 Spring Boot 的 starter,需要在 pom.xml 文件中引入依赖:


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

最后,我们还需要添加一个配置,配置项 <skip>true</skip> 表示跳过执行该插件的默认行为:


<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <skip>true</skip>
            </configuration>
        </plugin>
    </plugins>
</build>

这样,一个 SDK 项目的初始依赖就配置好了。


3、编写配置类


现在我们就可以按照之前设计的结构开发了。


首先,我们要写一个配置类,用来接受开发者在配置文件中写入的 webhook。


同时,我们可以在配置类中,将需要被调用的 MessageSender 对象 Bean 自动注入到 IOC 容器中,不用让开发者自己 new 对象了。


示例代码如下:


@Configuration
@ConfigurationProperties(prefix = "wechatwork-bot")
@ComponentScan
@Data
public class WebhookConfig {

    private String webhook;

    @Bean
    public RtxRobotMessageSender rtxRobotMessageSender() {
        return new RtxRobotMessageSender(webhook);
    }
}

接下来,为了让 Spring Boot 项目在启动时能自动识别并应用配置类,需要把配置类写入到 resources/META-INF/spring.factories 文件中,示例代码如下:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yupi.rtxrobot.config.WebhookConfig

4、编写消息类


接下来,我们要按照官方文档的请求参数把几种类型的消息对象编写好。


由于每个消息类都有一个固定的字段 msgtype,所以我们定义一个基类 Message,方便后续将不同类型的消息传入统一的方法:


public class Message {

    /**
     * 消息类型
     **/

    String msgtype;
}

接下来编写具体的消息类,比如纯文本类型消息 TextMessage,示例代码如下:


@Data
public class TextMessage extends Message {

    /**
     * 消息内容
     */

    private String content;

    /**
     * 被提及者userId列表
     */

    private List<String> mentionedList;

    /**
     * 被提及者电话号码列表
     */

    private List<String> mentionedMobileList;
  
    /**
     * 提及全体
     */

    private Boolean mentionAll = false;

    public TextMessage(String content, List<String> mentionedList, List<String> mentionedMobileList, Boolean mentionAll) {
        this.content = content;
        this.mentionedList = mentionedList;
        this.mentionedMobileList = mentionedMobileList;
        this.mentionAll = mentionAll;

        if (mentionAll) {
            if (CollUtil.isNotEmpty(this.mentionedList) || CollUtil.isNotEmpty(this.mentionedMobileList)) {
                if (CollUtil.isNotEmpty(mentionedList)) {
                    this.mentionedList.add("@all");
                } else {
                    this.mentionedList = CollUtil.newArrayList("@all");
                }
            } else {
                this.mentionedList = CollUtil.newArrayList("@all");
            }
        }
    }

    public TextMessage(String content) {
        this(content, nullnullfalse);
    }
}

上面的代码中,有个代码优化小细节,官方文档是使用 “@all” 字符串来表示 @全体成员的,但 “@all” 是一个魔法值,为了简化调用,我们将其封装为 mentionAll 布尔类型字段,并且在构造函数中自动转换为实际请求需要的字段。


5、编写消息发送类


接下来,我们将编写一个消息发送类。在这个类中,定义了用于发送各种类型消息的方法,并且所有的方法都会依赖调用底层的 send 方法。send 方法的作用是通过向企微机器人的 webhook 地址发送请求,从而驱动企微机器人发送消息。


以下是示例代码,有很多编码细节:


/**
 * 微信机器人消息发送器
 * @author yuyuanweb
 */

@Slf4j
@Data
public class RtxRobotMessageSender {

    private final String webhook;
  
    public WebhookConfig webhookConfig;

    public RtxRobotMessageSender(String webhook) {
        this.webhook = webhook;
    }

    /**
     * 支持自定义消息发送
     */

    public void sendMessage(Message message) throws Exception {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            send(textMessage);
        } else if (message instanceof MarkdownMessage) {
            MarkdownMessage markdownMessage = (MarkdownMessage) message;
            send(markdownMessage);
        } else {
            throw new RuntimeException("Unsupported message type");
        }
    }

    /**
     * 发送文本(简化调用)
     */
 
    public void sendText(String content) throws Exception {
        sendText(content, nullnullfalse);
    }
  
    public void sendText(String content, List<String> mentionedList, List<String> mentionedMobileList) throws Exception {
        TextMessage textMessage = new TextMessage(content, mentionedList, mentionedMobileList, false);
        send(textMessage);
    }
    
    /**
     * 发送消息的公共依赖底层代码
     */

    private void send(Message message) throws Exception {
        String webhook = this.webhook;
        String messageJsonObject = JSONUtil.toJsonStr(message);
       // 未传入配置,降级为从配置文件中寻找
        if (StrUtil.isBlank(this.webhook)) {
            try {
                webhook = webhookConfig.getWebhook();
            } catch (Exception e) {
                log.error("没有找到配置项中的webhook,请检查:1.是否在application.yml中填写webhook 2.是否在spring环境下运行");
                throw new RuntimeException(e);
            }
        }
        OkHttpClient client = new OkHttpClient();
        RequestBody body = RequestBody.create(
                MediaType.get("application/json; charset=utf-8"),
                messageJsonObject);
        Request request = new Request.Builder()
                .url(webhook)
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                log.info("消息发送成功");
            } else {
                log.error("消息发送失败,响应码:{}", response.code());
                throw new Exception("消息发送失败,响应码:" + response.code());
            }
        } catch (IOException e) {
            log.error("发送消息时发生错误:" + e);
            throw new Exception("发送消息时发生错误", e);
        }
    }
}

代码部分就到这里,是不是也没有很复杂?


6、SDK 打包


接下来就可以对 SDK 进行打包,然后本地使用或者上传到远程仓库了。


SDK 的打包非常简单,通过 Maven 的 install 命令即可,SDK 的 jar 包就会被导入到你的本地仓库中。



在打包前建议先执行 clean 来清理垃圾文件。




7、调用 SDK


最后我们来调用自己写的 SDK,首先将你的 SDK 作为依赖引入到项目中,比如我们的接水提醒应用。


引入代码如下:


<dependency>
  <groupId>com.yupi</groupId>
  <artifactId>rtx-robot</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

然后将之前复制的 webhook 写入到 Spring Boot 的配置文件中:


wechatwork-bot:
  webhook: 你的webhook地址

随后你就可以用依赖注入的方式得到一个消息发送者对象了:


@Resource
public RtxRobotMessageSender rtxRobotMessageSender;

当然你也可以选择在一个非 Spring 环境中手动创建对象,自己传入 webhook:


String webhook = "你的webhook地址";
RtxRobotMessageSender rtxRobotMessageSender = new RtxRobotMessageSender(webhook);

现在,就可以轻松实现我们之前提到的提醒接水工具了。


这里我们就用最简单的方式,定义一个员工数组,分别对应到每周 X,然后用定时任务每日执行消息发送。


示例代码如下:


@Component
public class WaterReminderTask {

    @Resource
    public RtxRobotMessageSender rtxRobotMessageSender;

    private String[] names = {"员工a""员工b""员工c""员工d""员工e"};

    @Scheduled(cron = "0 55 9 * * MON-FRI")
    public void remindToGetWater() {
        LocalDate today = LocalDate.now();
        DayOfWeek dayOfWeek = today.getDayOfWeek();
        String nameToRemind;
        switch (dayOfWeek) {
            case MONDAY:
                nameToRemind = names[0];
                break;
            case TUESDAY:
                nameToRemind = names[1];
                break;
            case WEDNESDAY:
                nameToRemind = names[2];
                break;
            case THURSDAY:
                nameToRemind = names[3];
                break;
            case FRIDAY:
                nameToRemind = names[4];
                break;
            default:
                return;
        }
      
        String message = "提醒:" + nameToRemind + ",是你接水的时间了!";
        rtxRobotMessageSender.sendText(message);
    }
}

好了,现在大家每天都有水喝了,真不错 👍🏻



最后


虽然开发企微机器人 SDK 并不难,但想做一个完善的、易用的 SDK 还是需要两把刷子的,而且沉淀 SDK 对自己未来做项目帮助会非常大。


希望本文对大家有帮助,学会的话 点个赞在看 吧,谢谢大家~


作者:程序员鱼皮
来源:juejin.cn/post/7300611640017813513
收起阅读 »

听我一句劝,业务代码中,别用多线程。

你好呀,我是歪歪。 前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。 虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。 我只是微微一笑,这不是很正常吗? 业务代码中一般也使不上多线...
继续阅读 »

你好呀,我是歪歪。


前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。


虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。


我只是微微一笑,这不是很正常吗?


业务代码中一般也使不上多线程,或者说,业务代码中不知不觉你以及在使用线程池了,你再 duang 的一下搞一个出来,反而容易出事。


所以提到线程池的时候,我个人的观点是必须把它吃得透透的,但是在业务代码中少用或者不用多线程。


关于这个观点,我给你盘一下。


Demo


首先我们还是花五分钟搭个 Demo 出来。


我手边刚好有一个之前搭的一个关于 Dubbo 的 Demo,消费者、生产者都有,我就直接拿来用了:



这个 Demo 我也是跟着网上的 quick start 搞的:



cn.dubbo.apache.org/zh-cn/overv…




可以说写的非常详细了,你就跟着官网的步骤一步步的搞就行了。


我这个 Demo 稍微不一样的是我在消费者模块里面搞了一个 Http 接口:



在接口里面发起了 RPC 调用,模拟从前端页面发起请求的场景,更加符合我们的开发习惯。


而官方的示例中,是基于了 SpringBoot 的 CommandLineRunner 去发起调用:



只是发起调用的方式不一样而已,其他没啥大区别。


需要说明的是,我只是手边刚好有一个 Dubbo 的 Demo,随手就拿来用了,但是本文想要表达的观点,和你使不使用 Dubbo 作为 RPC 框架,没有什么关系,道理是通用的。


上面这个 Demo 启动起来之后,通过 Http 接口发起一次调用,看到控制台服务提供方和服务消费方都有对应的日志输出,准备工作就算是齐活儿了:



上菜


在上面的 Demo 中,这是消费者的代码:



这是提供者的代码:



整个调用链路非常的清晰:



来,请你告诉我这里面有线程池吗?


没有!


是的,在日常的开发中,我就是写个接口给别人调用嘛,在我的接口里面并没有线程池相关的代码,只有 CRUD 相关的业务代码。


同时,在日常的开发中,我也经常调用别人提供给我的接口,也是一把梭,撸到底,根本就不会用到线程池。


所以,站在我,一个开发人员的角度,这个里面没有线程池。


合理,非常合理。


但是,当我们换个角度,再看看,它也是可以有的。


比如这样:



反应过来没有?


我们发起一个 Http 调用,是由一个 web 容器来处理这个请求的,你甭管它是 Tomcat,还是 Jetty、Netty、Undertow 这些玩意,反正是个 web 容器在处理。


那你说,这个里面有线程池吗?


在方法入口处打个断点,这个 http-nio-8081-exec-1 不就是 Tomcat 容器线程池里面的一个线程吗:



通过 dump 堆栈信息,过滤关键字可以看到这样的线程,在服务启动起来,啥也没干的情况下,一共有 10 个:



朋友,这不就是线程池吗?


虽然不是你写的,但是你确实用了。


我写出来的这个 test 接口,就是会由 web 容器中的一个线程来进行调用。所以,站在 web 容器的角度,这里是有一个线程池的:



同理,在 RPC 框架中,不管是消费方,还是服务提供方,也都存在着线程池。


比如 Dubbo 的线程池,你可以看一下官方的文档:



cn.dubbo.apache.org/zh-cn/overv…




而对于大多数的框架来说,它绝不可能只有一个线程池,为了做资源隔离,它会启用好几个线程池,达到线程池隔离,互不干扰的效果。


比如参与 Dubbo 一次调用的其实不仅一个线程池,至少还有 IO 线程池和业务线程池,它们各司其职:



我们主要关注这个业务线程池。


反正站在 Dubbo 框架的角度,又可以补充一下这个图片了:



那么问题来了,在当前的这个情况下?


当有人反馈:哎呀,这个服务吞吐量怎么上不去啊?


你怎么办?


你会 duang 的一下在业务逻辑里面加一个线程池吗?



大哥,前面有个 web 容器的线程池,后面有个框架的线程池,两头不调整,你在中间加个线程池,加它有啥用啊?


web 容器,拿 Tomcat 来说,人家给你提供了线程池参数调整的相关配置,这么一大坨配置,你得用起来啊:



tomcat.apache.org/tomcat-9.0-…




再比如 Dubbo 框架,都给你明说了,这些参数属于性能调优的范畴,感觉不对劲了,你先动手调调啊:



你把这些参数调优弄好了,绝对比你直接怼个线程池在业务代码中,效果好的多。


甚至,你在业务代码中加入一个线程池之后,反而会被“反噬”。


比如,你 duang 的一下怼个线程池在这里,我们先只看 web 容器和业务代码对应的部分:



由于你的业务代码中有线程池的存在,所以当接受到一个 web 请求之后,立马就把请求转发到了业务线程池中,由线程池中的线程来处理本次请求,从而释放了 web 请求对应的线程,该线程又可以里面去处理其他请求。


这样来看,你的吞吐量确实上去了。


在前端来看,非常的 nice,请求立马得到了响应。


但是,你考虑过下游吗?


你的吞吐量上涨了,下游同一时间处理的请求就变多了。如果下游跟不上处理,顶不住了,直接就是崩给你看怎么办?



而且下游不只是你一个调用方,由于你调用的太猛,导致其他调用方的请求响应不过来,是会引起连锁反应的。


所以,这种场景下,为了异步怼个线程池放着,我觉得还不如用消息队列来实现异步化,顶天了也就是消息堆积嘛,总比服务崩了好,这样更加稳妥。


或者至少和下游勾兑一下,问问我们这边吞吐量上升,你们扛得住不。


有的小伙伴看到这里可能就会产生一个疑问了:歪师傅,你这个讲得怎么和我背的八股文不一样啊?


巧了,你背过的八股文我也背过,现在我们来温习一下我们背过的八股文。


什么时候使用线程池呢?


比如一个请求要经过若干个服务获取数据,且这些数据没有先后依赖,最终需要把这些数据组合起来,一并返回,这样经典的场景:



用户点商品详情,你要等半天才展示给用户,那用户肯定骂骂咧咧的久走了。


这个时候,八股文上是怎么说的:用线程池来把串行的动作改成并行。



这个场景也是增加了服务 A 的吞吐量,但是用线程池就是非常正确的,没有任何毛病。


但是你想想,我们最开始的这个案例,是这个场景吗?



我们最开始的案例是想要在业务逻辑中增加一个线程池,对着一个下游服务就是一顿猛攻,不是所谓的串行改并行,而是用更多的线程,带来更多的串行。


这已经不是一个概念了。


还有一种场景下,使用线程池也是合理的。


比如你有一个定时任务,要从数据库中捞出状态为初始化的数据,然后去调用另外一个服务的接口查询数据的最终状态。



如果你的业务代码是这样的:


//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //捕获异常以免一条数据错误导致循环结束
    try{
        //发起rpc调用
        String orderStatus = queryOrderStatus(orderInfo.getOrderId);
        //更新订单状态
        updateOrderInfo(orderInfo.getOrderId,orderStatus);  
    } catch (Exception e){
        //打印异常
    }
}

虽然你框架中使用了线程池,但是你就是在一个 for 循环中不停的去调用下游服务查询数据状态,是一条数据一条数据的进行处理,所以其实同一时间,只是使用了框架的线程池中的一个线程。


为了更加快速的处理完这批数据,这个时候,你就可以怼一个线程池放在 for 循环里面了:


//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //使用线程池
    executor.execute(() -> {
        //捕获异常以免一条数据错误导致循环结束
        try {
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId, orderStatus);
        } catch (Exception e) {
            //打印异常
        }
    });
}


需要注意的是,这个线程池的参数怎么去合理的设置,是需要考虑的事情。


同时这个线程池的定位,就类似于 web 容器线程池的定位。


或者这样对比起来看更加清晰一点:



定时任务触发的时候,在发起远程接口调用之前,没有线程池,所以我们可以启用一个线程池来加快数据的处理。


而 Http 调用或者 RPC 调用,框架中本来就已经有一个线程池了,而且也给你提供了对应的性能调优参数配置,那么首先考虑的应该是把这个线程池充分利用起来。


如果仅仅是因为异步化之后可以提升服务响应速度,没有达到串行改并行的效果,那么我更加建议使用消息队列。


好了,本文的技术部分就到这里啦。


下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。


荒腔走板



不知道你看完文章之后,有没有产生一个小疑问:最开始部分的 Demo 似乎用处并不大?


是的,我最开始构思的行文结构是是基于 Demo 在源码中找到关于线程池的部分,从而引出其实有一些我们“看不见的线程池”的存在的。


原本周六我是有一整天的时间来写这篇文章,甚至周五晚上还特意把 Demo 搞定,自己调试了一番,该打的断点全部打上,并写完 Demo 那部分之后,我才去睡觉的,想得是第二天早上起来直接就能用。


按照惯例周六睡个懒觉的,早上 11 点才起床,自己慢条斯理的做了一顿午饭,吃完饭已经是下午 1 点多了。


本来想着在沙发上躺一会,结果一躺就是一整个下午。期间也想过起来写一会文章,坐在电脑前又飞快的躺回到沙发上,就是觉得这个事情索然无味,当下的那一刻就想躺着,然后无意识的刷手机,原本是拿来写文章中关于源码的部分的时间就这样浪费了。


像极了高中时的我,周末带大量作业回家,准备来个悬梁刺股,弯道超车,结果变成了一睡一天,捏紧刹车。


高中的时候,时间浪费了是真的可惜。


现在,不一样了。


荒腔走板这张图片,就是我躺在沙发上的时候,别人问我在干什么时随手拍的一张。


我并不为躺了一下午没有干正事而感到惭愧,浪费了的时间,才是属于自己的时间。


很久以前我看到别人在做一些浪费时间的事情的时候,我心里可能会嘀咕几句,劝人惜时。


这两年我不会了,允许自己做自己,允许别人做别人。


作者:why技术
来源:juejin.cn/post/7297980721590272040
收起阅读 »

2023行情不好,大龄员工如何跳槽

每一次找工作,都像一次职场大考,既是一次好的整理机会,也是一场对过去工作全方位的检验。--2023年换工作记。 来新团队快两周了,从年初二月份开始准备,到现在近9个月时间,换工作这事才算告一段落。 我今年35了,本科毕业十多年了,级别也不高,一线大兵,职场人的...
继续阅读 »

每一次找工作,都像一次职场大考,既是一次好的整理机会,也是一场对过去工作全方位的检验。--2023年换工作记。


来新团队快两周了,从年初二月份开始准备,到现在近9个月时间,换工作这事才算告一段落。


我今年35了,本科毕业十多年了,级别也不高,一线大兵,职场人的窘境在我身上齐活了--大龄Low T。


这篇文章总结了此次跳槽前后的一些思考与实践,供朋友们参考。


充分的准备


因为这次跳槽中间穿插了内部转岗、换城市、搬家,所以时间比较长。实际有效的求职时间大概5个月。


内容时长
复习专业技术1个月
刷题1个月
写简历3天
看机会&面试3个月


客观上行情确实不好,机会少,所以要留更长的时间等待机会,注意!是等!并不是你刚好投简历,就能遇到好的机会,你要留足够的时间等市面上合适的机会放出来。




公司一般年初会做好招聘预算,所以有金三银四,有的团队是下半年做完调研定好战略,启动招聘为明年做准备,这大概在金九银十。除了这两个窗口,其他时间大部分是淡季,除了少数公司遇到风口了会临时增加预算,或者业绩很好继续追加人,比如OpenAI的窗口,国内今年很多公司在补大模型和算法相关的人才。



准备-复习专业技术


我在百度呆了8年多,培养了一个习惯,重要的事情,全力以赴。



人和人的差距,其实就是几个关键点的差距。冲上去了就上去了。把每一个能做好的点都做到极致,你大概率就能超过别人。



时间有限,复习要有重点。


复习之前,你要明确几个简单的问题:



  1. 你出去要应聘的岗位和级别是什么?

  2. 这个岗位的要求是什么?

  3. 和别人比,你有什么优势?


上常用的招聘app(我主要用的是boss直聘)上搜一下相关的岗位是否有招聘,看看都有哪些公司、哪些岗位在招人,有什么要求。


如果你不介意换城市,多看几个城市,可以增加面试成功的几率。


然后,明确你要打造自己的"人设",不管你过去在公司干的怎么样,通过这几个月的复习你想成为什么样的人?成为哪个领域的专家,精通哪些技术、熟悉哪些技术,列出来,挑两三样重点复习,没有的话,就现整一个,临阵磨枪不快也光!B站上大把的免费教程教你如何快速成为专家。



求职中,最忌讳样样都会、啥都一知半解,好的简历和面试通常是精通一两门,其他的能横向拓展、融会贯通。




能精通一点意味着候选人能长期有耐心的钻研技术、深入解决问题、追求卓越,面试官有理由相信其他的问题他也一定能做的好。



我从事的是图形领域的开发工作,我花了一个月的时间,整理了OpenGL的常见API及用法、常见的渲染算法、C++高频问题,以及游戏引擎的架构和渲染管线,算是准备的比较充分了。


图形C++
OpenGL知识点整理(1)c++进阶知识点复习(1)
OpenGL知识点整理(2)c++进阶知识点复习(2)
深入理解opengl坐标系统c++3
游戏引擎(1)-ECS模式/
PBR(Physically Based Rendering)-理论/
PBR(Physically Based Rendering)-IBL/
图形学自学之路/

另外还有实时渲染算法、业务工程架构、引擎分析之类的整理,涉及到公司工作就没有发到公众平台上。


刷算法题


平时工作用到算法的地方并不多,想趁这个机会,把算法再熟悉一遍,温故知新。算法赋予了计算机灵魂,大厂考算法是有道理的。


提前给自己打了预防针,做好了打持久战的准备,所以直接买了力扣一年的vip会员,方便刷题和看题解。


另外专门读了一本系统的讲算法的书--《计算之魂-吴军》,从认知上提升对计算机、算法的理解。差不多刷了170多道题,基本上够用了。


关于刷题,之前写过一篇文章:重学数据结构算法(刷题心得)


投简历-找到有效的工作机会


节省时间,在网上找了个付费的简历网站,把履历填进去,能很方便的生成整洁的简历。也是直接买一年的,哥我就打算死磕了。


我已经工作十多年了,不能像刚毕业那样海投,那时是广撒网,有机会就去。现在有非常明确的目标,能接得住总包的工作机会就那么几家,一只手都数得过来,连预演练手的机会都没有,面一家就少一家。


锚定了意向中的那几家公司和岗位,有熟人的找熟人推一推,没熟人的硬投。实际发现,这年头HR都不靠谱了,更不用说猎头了。BOSS直聘上,研发自己跑出来找简历的一般都是真实的,那是真的着急招人,几率也更大,至少他要了你的简历,简历筛选这一关是过了,比HR效率高。


我最后投的几个岗位,都是字节研发侧主动来要简历的,加微信问了下,都是有大量真实的HC。


多说两句,其他的公司我就不黑了,字节我问了招聘方,他们明确表示不太在意年龄,冲这个人才观,我也更笃定了去字节。


面试-全力以赴每一场面试


我当了很多年面试官,也参加了很多次校招。面试其实很看眼缘,很难做到完全公平,除非你特别牛逼,有绝对的优势过面试,或者特别垃圾狗都嫌弃你,大多数候选人都在中间徘徊。


面试你的人大概率以后是你的leader,或者peer,他看你气质顺眼很重要,他认可你了,只要你不太差,也会给你过的。不喜欢你的气质,就无意中会有些刁难,过和不过都是一念之差。


这里说几个面试相关的细节。


不会的问题怎么办


说不知道、不会、没做过,是最差的回答。我面试中,会坦言自己没遇到过,请给我几分钟思考下,尝试找到合理的答案。


没有思路怎么办?


我会和面试官沟通,能否给一些提示,或者换一道题。


感觉自己没面好,直接放弃吗?


人生不要轻易言弃!!举两个我这次面试中的案例:



  • 案例1.


我一面的算法题写的有点问题,面试完回到家9点多。回忆代码逻辑,重新写了一遍,调试没问题了微信上和面试官沟通了下。


我表达的意思是:我不会放过任何一个有问题的代码,永不放弃!



  • 案例2.


二面的面试官问的很细,问了几个游戏引擎中很深入的问题,没回答好,我感觉自己应该是跪了。回到家我找了之前自己学习和整理的相关笔记发给面试官,告诉他,这些问题我之前真的有认真研究过,只是这次面试没回答好。


另外,我把在当前这家公司的历史绩效也截图发给了面试官,连着几年都拿了团队最高绩效,告诉他我真的很靠谱,恳求再给一次面试机会。


大概是我的真诚和坚持打动了面试官,第二天电话聊了下,还真给过了,推到了第三面。后面的面试就都比较顺利了。


涨幅


今年的行情,我了解到的,大部分公司都是卡30%的涨幅。HR问我期望薪资时,我很坚决的说,我看中的是这个机会,我热爱这个领域,薪酬差不多就行。


我心里能接受的最差的结果是降薪20%。这个年纪了,还能去一个往上走、充满机会的团队,持续成长,对职业发展来说是莫大的幸运,单纯的追求薪资是在杀鸡取卵、饮鸩止渴。


题外话-天赋是什么?


整个求职过程有点坎坷,有一些机会面的很好没有后话也很费解,后面也释怀了,大概是用人团队没有HC、或者给不上价,给内推人一个面子、走走过场,然后随便找个借口fail掉。


一个好朋友,也是前同事,一直关注我的面试进展,比我还紧张,他说,如果我这么努力这么牛逼都找不到工作,他的职业发展该何去何从。


第二天早上,我给他发了一个易建联退役的演讲视频[1分钟]:

v.douyin.com/iRSfCUAe/


真正的天赋,是你有多少热爱和付出-易建联


有多少朋友抱怨职场不公、运气不好,请问,你对自己的热爱有多少坚持,你对你的职业又有多少热爱,你又有多少勇气和毅力去改变这一切。



"没有人能随随便便成功,我也不例外"。



这是那天早上,我还在等offer、去上家公司上班的地铁上,在微信上打给这位朋友的最后一句。


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

一个小公司的技术开发心酸事

背景 长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。 自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给...
继续阅读 »

背景


长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。


自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给发。


当时老板的要求就是尽力降低人力成本,尽快的开发出来App(Android+IOS),老板需要尽快的运营起来。


初期的技术选型


当时就自己加上一个刚毕业的纯前端开发以及一个前面招聘的ui,连个人事、测试都没有。


结合公司的需求与自己的技术经验(主要是前端和nodejs的经验),选择使用如下的方案:



  1. 使用uni-app进行App的开发,兼容多端,也可以为以后开发小程序什么的做方案预留,主要考虑到的点是比较快,先要解决有和无的问题;

  2. 使用egg.js + MySQL来开发后端,开发速度会快一点,行业比较小众,不太可能会遇到一些较大的性能问题,暂时看也是够用了的,后期过渡到midway.js也方便;

  3. 使用antd-vue开发运营后台,主要考虑到与uni-app技术栈的统一,节省转换成本;


也就是初期选择使用egg.js + MySQL + uni-app + antd-vue,来开发两个App和一个运营后台,快速解决0到1的问题。


关于App开发技术方案的选择


App的开发方案有很多,比如纯原生、flutter、uniapp、react-native/taro等,这里就当是的情况做一下选择。



  1. IOS与Android纯原生开发方案,需要新招人,两端同时开发,两端分别测试,这个资金及时间成本老板是不能接受的;

  2. flutter,这个要么自己从头开始学习,要么招人,相对于纯原生的方案好一点,但是也不是最好的选择;

  3. react-native/taro与uni-app是比较类似的选择,不过考虑到熟练程度、难易程度以及开发效率,最终还是选择了uni-app。


为什么选择egg.js做后端


很多时候方案的选择并不能只从技术方面考虑,当是只能选择成本最低的,当时的情况是egg.js完全能满足。



  1. 使用一些成熟的后端开发方案,如Java、、php、go之类的应该是比较好的技术方案,但对于老板来说不是好的经济方案;

  2. egg.js开发比较简单、快捷,个人也比较熟悉,对于新成员的学习成本也很低,对于JS有一定水平的也能很快掌握egg.js后端的开发


中间的各种折腾


前期开发还算顺利,在规定的时间内,完成了开发、测试、上线。但是,老板并没有如前面说的,很快运营,很快就盈利,运营的开展非常缓慢。中间还经历了各种折腾的事情。



  1. 老板运营遇到困难,就到处找一些专家(基本跟我们这事情没半毛钱关系的专家),不断的提一些业务和ui上的意见,不断的修改;

  2. 期间新来的产品还要全部推翻原有设计,重新开发;

  3. 还有个兼职的领导非要说要招聘原生开发和Java开发重新进行开发,问为什么,也说不出什么所以然,也是道听途说。


反正就是不断提出要修改产品、设计、和代码。中间经过不断的讨论,摆出自己的意见,好在最终技术方案没修改,前期的工作成果还在。后边加了一些新的需求:系统升级1.1、ui升级2.0、开发小程序版本、开发新的配套系统(小程序版本)以及开发相关的后台、添加即时通信服务、以及各种小的功能开发与升级;


中间老板要加快进度了就让招人,然后又无缘无故的要开人,就让人很无奈。最大的运营问题,始终没什么进展,明显的问题并不在产品这块,但是在这里不断的折腾这群开发,也真是难受。


明明你已经很努力的协调各种事情、站在公司的角度考虑、努力写代码,却仍然无济于事。


后期技术方案的调整



  1. 后期调整了App的打包方案;

  2. 在新的配套系统中,使用midway.js来开发新的业务,这都是基于前面的egg.js的团队掌握程度,为了后续的开发规范,做此升级;

  3. 内网管理公用npm包,开发业务组件库;

  4. 规范代码、规范开发流程;


人员招聘,团队的管理


人员招聘


如下是对于当时的人员招聘的一些感受:



  1. 小公司的人员招聘是相对比较难的,特别是还给不了多少钱的;

  2. 好在我们选择的技术方案,只要对于JS掌握的比较好就可以了,前后端都要开发一点,也方便人员工作调整,避免开发资源的浪费。


团队管理


对于小团队的管理的一些个人理解:



  1. 小公司刚起步,就应该实事求是,以业务为导向;

  2. 小公司最好采取全栈的开发方式,避免任务的不协调,造成开发资源的浪费;

  3. 设置推荐的代码规范,参照大家日常的代码习惯来制定,目标就是让大家的代码相对规范;

  4. 要求按照规范的流程设计与开发、避免一些流程的问题造成管理的混乱和公司的损失;

    1. 如按照常规的业务开发流程,产品评估 => 任务分配 => 技术评估 => 开发 => 测试 => cr => 上线 => 线上问题跟踪处理;



  5. 行之有效可量化的考核规范,如开发任务的截止日期完成、核心流程开发文档的书写、是否有线上bug、严谨手动修改数据库等;

  6. 鼓励分享,相互学习,一段工作经历总要有所提升,有所收获才是有意义的;

  7. 及时沟通反馈、团队成员的个人想法、掌握开发进度、工作难点等;


最后总结及选择创业公司避坑建议!important



  1. 选择创业公司,一定要确认老板是一个靠谱的人,别是一个总是画饼的油腻老司机,或者一个优柔寡断,没有主见的人,这样的情况下,大概率事情是干不成的;

    1. 老板靠谱,即使当前的项目搞不成,也可能未来在别的地方做出一番事情;



  2. 初了上边这个,最核心的就是,怎么样赚钱,现在这种融资环境,如果自己不能赚钱,大概率是活不下去的@自己;

  3. 抓住核心矛盾,解决主要问题,业务永远是最重要的。至于说选择的开发技术、代码规范等等这些都可以往后放;

  4. 对上要及时反馈自己的工作进度,保持好沟通,老板总是站在更高一层考虑问题,肯定会有一些不一样的想法,别总自以为什么什么的;

  5. 每段经历最好都能有所收获,人生的每一步都有意义。


以上只是个人见解,请指教。


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

语雀又崩了?今天咱们玩点花的,手把手教你写出令人窒息的“烂代码”

web
Hello,大家好,我是Sunday。 10月23日 2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。 不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在...
继续阅读 »

Hello,大家好,我是Sunday。


10月23日


2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。


不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在问题解决之后,给大家 六个月的会员补偿 也可以说是诚意满满(以下为10月24日语雀团队公告)。




毕竟大家都是程序员嘛,这种事也不是不能接受。毕竟:谁还没搞崩过系统呢?😂



本以为这件事就这么过去了,哪知道昨天的一个故障,再次让语雀登上了“风口浪尖”......


11月12日


昨天下午,我在正常使用语雀记录同学学习情况的时候,突然出现了无法保存的情况。心想:“这不会是又崩了吧~~”


看了眼语雀群的微信,果然......




说实话,当时我的第一反应是:“又有瓜可以吃了~~~~~,开心😂”



反正也写不成了,坐等吃瓜就行了。正好恰逢双十一,看看买的硬盘到哪了。


结果打开淘宝才发现,这次不对劲啊,淘宝也崩了!!!




最终我们了解了事情的全貌:



本次事故是由于阿里云 OSS 的故障导致的。钉钉、咸鱼、淘宝、语雀都崩了....



从语雀的公告也体现出了这点:



公告内容如下:



尊敬的客户:您好!北京时间2023年11月12日 17:44起,阿里云监控云产品控制台访问及API调用出现出现使用异常,阿里云工程师正在紧急介入排查。非常抱歉给您的使用带来不便,若有任何问题,请随时联系我们。



可以说,语雀这次有点躺枪了(谁让你刚崩过呢~~~)。


玩点花的!教你写出令人窒息的“烂代码”


好啦,瓜吃完啦。



关于语雀崩溃的反思,网上有很多文章,我就不凑这个热闹了,想要看的同学可以自行搜索~~



“回归正题”,接下来咱们就来看看咱们的文章正题:“如何写出烂代码”。



以下共有十三条烂代码书写准则,可能并没有面面俱到,如果大家发现有一些难以忍受的烂代码习惯,也可以留言发表意见~~



第一条:打字越少越好


  // Good 👍🏻
const a = 18

// Bad 👎
const age = 18

第二条:变量/函数混合命名风格


  // Good 👍🏻
const my_name = 'Suday'
const mName = 'Sunday'
const MnAme = 'Sunday'

// Bad 👎
const myName = 'Sunday'

第三条:不要写注释


  // Good 👍🏻
const cbdr = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第四条:使用母语写注释


  // Good 👍🏻
// 666 мс было эмпірычна вылічана на аснове вынікаў UX A/B.
const callbackDebounceRate = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第五条:尽可能混合不同的格式


  // Good 👍🏻
const n = 'Sunday';
const a = "18"
const g = "MAN"

// Bad 👎
const name = 'sunday'
const age = '18'
const gender = 'man'

第六条:尽可能把代码写成一行


  // Good 👍🏻
document.location.search.replace(/(^\?)/, '').split('&').reduce(function (o, n) { n = n.split('=') })

// Bad 👎
document.location.search
.replace(/(^\?)/, '')
.split('&')
.reduce((searchParams, keyValuePair) => {
keyValuePair = keyValuePair.split('=')
searchParams[keyValuePair[0]] = keyValuePair[1]
return searchParams
})

第七条:发现错误要保持静默


   // Good 👍🏻
try {
...
} catch () {🤐}

// Bad 👎
try {
...
} catch (error) {
setErrorMessage(error.message)
logError(error)
}

第八条:广泛使用全局变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第九条:构建备用变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第十条:Type 使用需谨慎


  // Good 👍🏻
function sum(a, b) {
return a + b
}

// Bad 👎
function sum(a: number, b: number) {
return a + b
}

第十一条:准备「Plan B」


  // Good 👍🏻
function square(num) {
if (typeof num === 'undefined') {
return undefined
} else {
return num ** 2
}
return null
}

// Bad 👎
function square(num) {
if (typeof num === 'undefined') {
return undefined
}
return num ** 2
}

第十二条:嵌套的三角法则


    // Good 👍🏻
function somFun(num) {
if (condition1) {
if (condition2) {
asyncFunction(param, (result) => {
if (result) {
for (; ;) {
if (condition3) {

}
}
}
})
}
}
}

// Bad 👎
async function somFun(num) {
if (!condition1 || !condition2) {
return;
}
const result = await asyncFunction(params);
if (!result) {
return;
}
for (; ;) {
if (condition3) {

}
}
}

第十三条:混合缩进


      // Good 👍🏻
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

// Bad 👎
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

总结


所谓的“烂代码”,是大家一定 不要 照着写的哈。


“教你写出令人窒息的“烂代码”“ 是一个反义,这个大家肯定是可以明白的哈~~~~。



”烂代码“内容参考自:zhuanlan.zhihu.com/p/516564022



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