注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一边敲代码一边晋升宝爸

2024年农历三月初一,我成功晋升为宝爸。 这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。” 看着这个陌生的、小小的男人,我忐忑的心变得安宁。 在这个世界,我们彼此相遇,有点不知所措,如在梦中。 他嘹亮的啼哭,让...
继续阅读 »

成品.jpg


2024年农历三月初一,我成功晋升为宝爸。


这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。”


看着这个陌生的、小小的男人,我忐忑的心变得安宁。


在这个世界,我们彼此相遇,有点不知所措,如在梦中。


他嘹亮的啼哭,让我的心弦随之颤动。


我一动不动的看着他,不敢摸他。


浓浓的血脉,注定了这一世的羁绊。


……


现在是熊宝出生的第21天了,在护理师的帮助下,我学会了抱娃、拍嗝、换尿布,算是初步顺上手了。


至此,我可以有些时间写点生娃的回忆和经验,分享给大家。


我的爱人是在2023年7月份怀孕的。


清晰记得那天早晨,我爱人跟我说她已经40多天没有来月经了,她可能怀孕了。


我赶紧在美团买了个验孕棒,一测,出现了两条红线,心中大喜。


为了万无一失,我又带她去医院检查,医生说:“恭喜你,有喜了。”


我长吁了一口气,结婚后的一年里,我爱人老担心这事。


这个时候,我还在山东老家装修房子,想着老来得子,要谨慎一些,所以就计划去北京备产。


北京消费高,以我当前的经济实力,得找份工作才行。


所以我就去boss 上看工作,看到滴滴有WebGL工程师的坑,就投了份简历,然后大大小小的面试了五六场才过。


我工作的地址是中关村壹号,所以,我就近在大牛坊租了个窗户很大的主卧,孕妈的建档就建在了上地医院。


上地医院不算三甲,但它的整体服务和环境都挺好的,产科是它的特色,看病的人不少,但也不会特别多,一般不需要排太久的队。


就这样,我开始了白天给公司当孙子,晚上给爱人当孙……,额,是守护神的生活。


孕初的几个月,我们会每两周去一次医院孕检。什么时候孕检,医生都会提前告知我们,而且孕检手册也会告诉我们整个流程。


有的孕检是需要验血的,这时就需要空腹。一般我们会很早起床,把水杯和牛肉干装进背包里,然后去医院。


孕妇饿久了、抽血多了,很容易心慌头晕,再加上孕期情绪敏感。所以,每次孕检我都会陪着她,陪她早去,避免她饿太久,验完血后,我会先给她吃点牛肉干垫垫,然后再去附近找好吃的。


在之后的时间里,一切还算顺利,熊宝在妈妈的肚子里慢慢的从一颗种子长成鹌鹑蛋、小鸡蛋、小苹果……


在他有小木瓜那么大的时候,他学会了玩脐带,并且成功的在自己的脖子上缠了一周。


那段时间,熊宝的小脑延髓池还长得有点快,都0.9了,规定的最大值是1.0,这让我们很是担心。


我上网查资料说小脑延髓池的值太大会脑子进水,变成一个傻宝。


幸亏最后都稳定住了,没有再长。


在孕期第六个月的时候,也就是2023年年底,我们开始考虑胎儿的生产和月子问题。


我们是没啥经验的,所以想着找个月嫂,或者找个月子中心,房子还得找个至少两居室的,不能再合租了。


这一堆算下来并不便宜,北京的金牌月嫂是3万,普通的也得2万。中高级的月子中心是7万到10万。两居室的房子8千/月。


这对于我在滴滴的薪资来说倒也还好,可问题是年底的时候我不想在滴滴干了。


原因是我所在的HMI部门的管理出了问题,再干下去会很浪费生命。


思虑再三,我给卡尔动力的老板提了一些建议后,就离职了。


卡尔动力是我去滴滴时,刚从滴滴脱离出来的创业公司,我在其中负责Web端的三维可视化。


在我离开公司前,老板把他的微信给我了,我们加了个好友,现在还保持着联系,希望以后会有合作。


至于我之前所在的那个HMI部门,它在我离开没多久就被打散重组了,组长也被撤了。很佩服老板的雷厉风行。


其中太具体的事情我就不再多说了,咱们继续谈生娃的事。


我离开滴滴后,就把大牛坊的房子退了,东西都寄回了老家。


接下来,我们计划在山东的潍坊老家生娃。


之所以如此,有多方面的考虑:


我们已经在北京完成了孕前和孕中的检查,胎儿很健康,所以没必要再纠结于北京。


潍坊有多个三甲医院,其医疗技术虽然比不得北京,但生娃也不是那种只有一线城市才能做的、技术难度很高的事。


北京消费高,没较高的收入就不适合再待在北京了,那时我租的房子也正好到期。而潍坊的消费是很低的,我们每天120元就可以在医院对面短租很精致的小米全屋智能公寓。潍坊中高端的月子中心一个月2万。


我刚好接到了我上上家公司的一个单子,可以在老家工作,小赚一笔。有很多时候,我离开公司,并不一定是我和公司闹掰了,其原因很多的。


我爸妈在老家自己种着蔬菜,有鸡鹅牛羊,自己家的东西吃着放心。


就这样,我们在北京待了四个月后,回到了潍坊老家。


在老家的日子里,孕妈的饮食都挺好的。我们吃东西前,都会从一个叫“孕育树”的APP上查查孕妈能不能吃。


其实,查一种食物是否适合孕妈吃是很简单的,但难的是你可能会忘了查,或者自以为不用查。


以前我爱人就有过几次,比如煮鸽子汤的时候放了某一种中药,喝了两三次后才想起查一下,结果发现那种中药容易让孕妇流产;还有一次,我对象以为自己可以吃桂圆,买了一堆后,我给她一查,发现孕妇不能吃。


孕妈在怀孕的时候,很容易傻傻的可爱,这需要我们多上点心。


我家孕妈的精神状态一直是我比较担心的,因为她有点熊,熊孩子的熊,再加上有几个词叫“产前抑郁”和“产后抑郁”,所以我会格外注意和防范她的情绪问题。


我会时刻告诉自己不能让她生气,学会换位思考,照顾好她的方方面面,她说得都对,不因小事而计较,不轴,不抬杠,努力逗她开心。


除此之外,赚钱也很重要,因为很多时候钱是可以换来快乐和舒适的。


在这期间,我给我工作过的上上家公司开发了一个三维机器人的交互展示项目,基本上能够后面的开支。


我还把去滴滴时遇到的面试题做成了一个低价的付费课-《canvas进阶-面试题》,想着以后多多少少给熊宝赚点奶粉钱。


每天我也依旧会拿出一点时间去学习,让自己保持一个持续成长的状态。


与此同时,熊宝在妈妈的肚子里也持续成长,长成了一个小西瓜。


熊宝玩脐带的能力也进步了,他成功把脐带在自己脖子上又绕了一周,成为了绕颈两周。


直到熊宝的脑袋入盆的时候,还是两周,医生说:“你们别再想让他绕回来了,当然,也不用担心他再绕更多了。”


在这期间,我们一感觉熊宝不咋蛄蛹了,就赶紧用自己买的胎心仪测测,生怕他因为绕颈两周而缺氧。


有的时候熊宝很皮,半天不动,胎心还换了位置,我们常常大半夜的测胎心测好久,都快把她妈吓哭了,直到在一个思维盲点听到强劲有力的小火车声,才放下心来。


在孕妈离预产期还有20天的时候,我们住在了潍坊阳光融合医院对面的一间环境舒适,干净卫生,可以洗衣做饭的小米全屋智能公寓里。


在这个时候,孕妈基本上就是随时可以生的了,所以我们需要住在医院旁边,以防突发情况。


我们住下来的当天,还去潍坊妇幼保健院做了孕检,检查结果并不理想,医生说:“胎心加速不及格,若一直这样,就得刨宫产。”


我爱人当时就被吓哭了,我们一直都想顺产的,我不想在她的肚子上开一道口子。


我从网上查了一下,导致胎心加速不行的原因是有很多种的,比如胎儿睡着了,或者妈妈没吃好,胎儿饿着了,不想动。


这天我爱人确实没吃好,我就跟她说:“我们去吃火锅吧,没有什么是一顿火锅解决不了的。”


于是我就带着哭得梨花带雨的孕妈吃了顿火锅。


吃完后,我们没有去潍坊妇幼保健院孕检,而是就近去了我家对面的阳光融合医院。


在阳光融合医院,医生说胎心加速及格了,虽不是那么理想,但也没有问题。


至此,我们搬来医院对面的第一天可以睡个好觉了。


一周后的上午,我们又去潍坊妇幼保健院做了孕检,胎心加速还是不理想,医生让我们下午住院,观察情况,可能要刨宫产。


我没住,我们去阳光融合医院又做了一次孕检,结果胎心加速还是合格的,所以就没去住院。


我当时的想法是,阳光融合和潍坊妇幼保健院都是三甲医院,只要有一个测着可以不刨,我们就不刨。


我们也想过为什么两个同样三甲的医院的测试结果不一样,其原因也不一定是哪个医院不行。


也可能是因为我去潍坊妇幼保健院都是上午去的,而去阳光融合医院都是下午或晚上去的。


我在滴滴的时候,回家晚,睡得晚,起得晚,熊他妈也一定要等我回来才睡。这让熊宝在娘胎里面变成了一个小夜游神。


熊宝在上午的时候总是老老实实的不咋动,等到了晚上就总在妈妈肚子里手舞足蹈。


所以现在上午去医院测胎心的时候,熊宝可能还没起床,等晚上测胎心的时候,就来了精神了。


记得离预产期还有10天左右的时候,我们还是上午去潍坊妇幼做孕检,医生直接要让我们下午就做刨宫产,她说:“原因有五:胎心加速不行,绕颈两周,产龄偏高,做过锥切,已经临近预产期。”


我没有照做,我带熊他妈又去吃了一顿火锅,然后睡了一个午觉,养足了精神,就去了潍坊人民医院。


潍坊人民医院是综合性医院,属于潍坊医院里的老大。


我们找到了经验丰富的胡明英医生做检查,胡明英医生是一位很有名望的医生,她本应退休却又被返聘回去了,只因为她当了妇产医生后,就闲不住了。


胡明英医生给我们做了全面细致的检查,得出以下结论:


● 胎心加速是合格的。


● 绕颈两周并不算太大的问题,因为小孩在顺的时候,还是可以转出来的,当然这也并非绝对。


● 脐带的血液流速和供氧都没问题,并未受绕颈两周的影响。


● 孕妇年龄35,并没有超出可以顺产的年龄范畴。


● 微小面积锥切,且并非疤痕体质,并不影响顺产。


两天后,胡明英医生又给孕妈做了骨盆检查,最终结论是:可以顺产,等瓜熟落地即可。


接下来胡医生就给孕妈开了住院单,当孕妈出现规律宫缩的时候,就可以直接来住院。


期间,我们又去阳光融合查了一次,得到的结果依旧是可以顺产,如此我们才算放心。


虽然我不懂医术,但基本逻辑我还是懂的。之前潍坊妇幼保健院在未经全面、细致分析的前提下,让孕妈当天下午就做刨宫产的行为有些武断了。


2024年4月8日的晚上,孕妈发生规律性宫缩,大约每隔半个小时一次。


我们立刻去了对面的阳光融合医院的急诊楼做检查,医生说快生了,让我们立刻住院。


我说我在胡明英医生那里挂了号,我们要去潍坊人民医院。


阳光融合的医生说她是胡明英医生的学生,让我们放心去潍坊人民医院就行,别再换其它地方了,现在的孕妈快生了,不能再折腾。


孕妈从规律宫缩到可以顺产,还会经历至少几个小时的开指时间,所以我花个十五分钟去潍坊人民医院的时间还是有的。


如果这个时候是孕妈的羊水破了,我就会选择直接在阳光融合医院顺产,当然,这个时候的医生也肯定不会再让我们走了。


我们去了潍坊人民医院后,就拿着住院单住进了候产房。


这时候的孕妈已经五六分钟宫缩一次了,而且疼感很强烈。


医生让她等着,等着开指。


这个过程孕妈很痛苦,她一直疼到第二天晚上,已经有些虚脱了。她虚弱的躺在床上,每次宫缩都会大口喘气,看着很让人揪心。


我一点点的喂她吃着晚饭,她知道后面的顺产需要体力,强忍着剧痛一点点吃掉我喂她的西蓝花、菠菜和馒头。


她每次宫缩,我都会给她按摩腰部,这样可以缓解一下她盆骨松动的痛苦。


在夜间两点的时候,她把吃过的晚饭都吐了,她有气无力跟我说:“我生不动了,我没力气了,你去跟医生说吧,让我刨吧。”


我紧紧的握着她的手说:“你再坚持一下吧,你想想我们为了顺产,经历了那么多,我们在3个医院间周折往复,你现在刨的话就前功尽弃了。”


我稳定住她的情绪后,就去了护士站,跟医生说:“11号床有点撑不住了,你可以去看看吗?”


于是医生就去给她做了检查,跟我说:“她快开到三指了,可以打无痛了。”


医生把她连床带人一起推进了产房。


我站在产房门口,不能进去。


那一夜我没有睡,这是我第一次连着两个晚上没有睡觉,却毫无睡意。


早晨6点左右,医生跟我说:“她打完无疼后,已经睡了一会,现在醒了,你去给她买点早餐,她预计中午能生。”


此时我的心算是稍微放下了一些,给她买了几个素包子和小米粥,外加一瓶脉动。医生说能量型饮料可以给她快速补充体力。


在忐忑等待的时间里,我还跟医院签了一个脐带血的储存协议,医生说以后孩子遇到了白血病、肝硬化等病,可以用脐带血治疗。


我虽不希望有那么一天,但多一份保障还是好的。


一直等到上午十点多的时候,医生终于告诉我生了,母子平安!


从此,我也是有娃的人了。


接下来,我们在医院的单间住了四天,没什么问题后,就去了月子中心,准备在月子中心住上28天。我因为没什么经验,为确保万无一失,只能花钱解决一些问题了。


在之后的日子里,我会研究一下育儿之道,看看《好妈妈胜过好老师》,同时努力赚钱养娃。


后面有啥心得,我会再分享给大家。


最后给大家总结一下我这一路走来的经验:


● 尽量让孕妈规律作息,不要像我似的把熊宝养成了小夜游神。


● 孕妈吃的每一种食物都要提前查一下能不能吃。


● 孕妈情绪很重要,一定要百般呵护,比如孕检的时候要全程陪伴,不要惹她生气。


● 孕妈吃啥和该怎么活动,网上都有,我觉得这是比较简单的。


● 当孕妈接近预产期,规律宫缩的时候,一定要立刻去医院,这很重要,千万别拖,即使这是在晚上你睡得正香的时候。


今年上海就有个宝妈晚上规律宫缩,拖到了早上才去医院,结果堵车了,还没进医院就把宝宝生车里了,但胎盘没出来,这极其危险。还好最后母子平安。


● 尽量顺产。如果有医院让你刨,除非紧急情况,不要立刻刨,尽快多换几家更好的、至少三甲的医院看看。


我尊重医生,但我并不觉得每个医生都是白衣天使。就像曾经魏则西事件,还有今年北京积水潭医院原院长田伟落马,这都说明职业和权利并不会决定人之善恶。


与此同时,我们也不要拿网上看来的知识来挑战医生的专业,网上知识仅供参考,具体怎么做要听医生的。不过,这与我多找几个更专业的医生问问并不冲突。


● 努力赚钱,有很多事都是可以用钱来解决的。


作者:李伟_Li慢慢
来源:juejin.cn/post/7367174168599150602
收起阅读 »

如何让不同Activity之间共享同一个ViewModel

问题背景 存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据 提出假设的手段 可以定义一个ViewModel,让这两个Activity去共享这个ViewModel 存在的问题 根据不同的Lifecycle...
继续阅读 »

问题背景


存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据


提出假设的手段


可以定义一个ViewModel,让这两个Activity去共享这个ViewModel


存在的问题


根据不同的LifecycleOwner创建出来的ViewModel是不同的实例,所以在两个不同的Activity之间无法创建同一个ViewModel对象


问题分析


先来梳理一下一个正常的ViewModel是怎么被构造出来的:



  1. ViewModel是由ViewModelFactoty负责构造出来

  2. 构造出来之后,存储在ViewModelStore里面
    但是问题是ViewModelStore是 和 (宿主Activity或者Fragment)是一一对应的关系
    具体代码如下


@MainThread  
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val owner by lazy(LazyThreadSafetyMode.NONE) { ownerProducer() }
return createViewModelLazy(
VM::class,
{ owner.viewModelStore },
{
(owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
?: CreationExtras.Empty
},
factoryProducer ?: {
(owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelProviderFactory
?: defaultViewModelProviderFactory
})
}

看到上面的代码第9行,viewModelStore和owner是对应关系,所以原则上根据不同LifecycleOwner无法构造出同一个ViewModel对象


解决思路



  1. 无法在不同的LifecycleOwner之间共享ViewMode对象的原因是:ViewModel的存储方ViewModelStore是和LifecycleOwner绑定,那如果可以解开这一层绑定关系,理论上就可以实现共享;

  2. 另外我们需要定义ViewModel的销毁时机:


    我们来模拟一个场景:由Activty1跳转到Activity2,然后两个Activity共享同一个ViewModel,两个activity都要拿到同一个ViewModel的实例,那这个时候ViewModel的销毁时机应该是和Acitivity1的生命周期走,也就是退出Activity1(等同于Activity1走onDestroy)的时候,去销毁这个ViewModel。



所以按照这个思路走,ViewModel需要在activity1中被创建出来,并且保存在一个特定的ViewModelStore里面,要保证这个ViewModelStore可以被这两个Activity共享;


然后等到Activity2取的时候,就直接可以从这个ViewModelStore把这个ViewModel取出来;


最后在Activity1进到destroy的时候,销毁这个ViewModel


具体实现


重写一个ViewModelProvider实现如下功能点:



  1. 把里面的ViewModelStore定义成一个单例供所有的LifecycleOwner共享

  2. 定义ViewModel的销毁时机: LifecycleOwner走到onDestroy的时机


// 需要放到lifecycle这个包,否则访问不到ViewModelStore
package androidx.lifecycle

class GlobalViewModelProvider(factory: Factory = NewInstanceFactory()) :
ViewModelProvider(globalStore, factory) {
companion object {
private val globalStore = ViewModelStore()
private val globalLifecycleMap = HashMap<String, MutableSet<Lifecycle>>()
private const val DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"
}

@MainThread
fun <T: ViewModel> get(lifecycle: Lifecycle, modelClass: Class<T>): T {
val canonicalName = modelClass.canonicalName ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
return get(lifecycle, "$DEFAULT_KEY:$canonicalName", modelClass)
}

@MainThread
fun <T: ViewModel> get(lifecycle: Lifecycle, key: String, modelClass: Class<T>): T {
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
throw IllegalStateException("Could not get viewmodel when lifecycle was destroyed")
}
val viewModel = super.get(key, modelClass)
val lifecycleList = globalLifecycleMap.getOrElse(key) { mutableSetOf() }
globalLifecycleMap[key] = lifecycleList
if (!lifecycleList.contains(lifecycle)) {
lifecycleList.add(lifecycle)
lifecycle.addObserver(ClearNegativeVMObserver(lifecycle, key, globalStore, globalLifecycleMap))
}
return viewModel
}

private class ClearNegativeVMObserver(
private val lifecycle: Lifecycle,
private val key: String,
private val store: ViewModelStore,
private val map: HashMap<String, MutableSet<Lifecycle>>,
): LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
val lifecycleList = map.getOrElse(key) { mutableSetOf() }
lifecycleList.remove(lifecycle)
if (lifecycleList.isEmpty()) {
store.put(key, null)
map.remove(key)
}
}
}
}
}

具体使用


@MainThread  
inline fun <reified VM: ViewModel> LifecycleOwner.sharedViewModel(
viewModelClass: Class<VM> = VM::class.java,
noinline keyFactory: (() -> String)? = null,
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null,
)
: Lazy<VM> {
return SharedViewModelLazy(
viewModelClass,
keyFactory,
{ this },
factoryProducer ?: { ViewModelProvider.NewInstanceFactory() }
)
}

@PublishedApi
internal class SharedViewModelLazy<VM: ViewModel>(
private val viewModelClass: Class<VM>,
private val keyFactory: (() -> String)?,
private val lifecycleProducer: () -> LifecycleOwner,
private val factoryProducer: () -> ViewModelProvider.Factory,
): Lazy<VM> {
private var cached: VM? = null
override val value: VM
get() {
return cached ?: kotlin.run {
val factory = factoryProducer()
if (keyFactory != null) {
GlobalViewModelProvider(factory).get(
lifecycleProducer().lifecycle,
keyFactory.invoke(),
viewModelClass
)
} else {
GlobalViewModelProvider(factory).get(
lifecycleProducer().lifecycle,
viewModelClass
)
}.also {
cached = it
}
}
}

override fun isInitialized() = cached != null
}

场景使用


val vm : MainViewModel by sharedViewModel()

作者:红鲤驴
来源:juejin.cn/post/7366913974624059427
收起阅读 »

用了这么久SpringBoot却还不知道的一个小技巧

前言 你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。 你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。 那么SpringBoot本身提供...
继续阅读 »

前言



你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。




你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。




那么SpringBoot本身提供了一个小技巧,很多人估计没用过。



正文


1、效果



废话不多说,先写个service和controller展示个效果最实在。




来个简单的service



@Service
public class TestService {

public String test() {

System.err.println("Hello,Java Body ~");
return "Hello,Java Body ~";
}
}


再来个简单的controller



@RestController
@RequestMapping("/api")
@AllArgsConstructor
public class TestController {

private final TestService testService;

@GetMapping("/test")
public ResponseEntity test() {
return ResponseEntity.ok().body(testService.test());
}
}


接下来是不是以为要启动调接口了,No,在SpringBoot的启动类中加这么个玩意儿



@SpringBootApplication
public class JavaAboutApplication {

public static void main(String[] args) {
SpringApplication.run(JavaAboutApplication.class, args);
}

@Bean
CommandLineRunner lookupTestService(TestService testService) {
return args -> {

// 1、test接口
testService.test();

};
}

}


启动看下效果



4.png



可以发现,SpringBoot启动后,自动加载了service的执行程序。




这个小案例是想说明什么呢,其实就是CommandLineRunner这么个东西。



2、它是什么



CommandLineRunner是一个接口,用于在Spring Boot应用程序启动后执行一些特定的任务或代码块。当应用程序启动完成后,Spring Boot会查找并执行实现了CommandLineRunner接口的Bean。




说白了,就是SpringBoot启动后,我立马想干的事,都可以往里写。



3、我用它做过什么



我的话,和很多厂家对接过接口,在前期不会直接开始写业务,而是先调通接口,再接入业务中。




比如webservice这种,我曾经使用CommandLineRunner直接调对方接口来测试,还挺舒适,也节省了IDEA资源,但要注意调试完成后注释掉,本地测试的时候再打开就行。



5.png


4、它还有哪些用途



除了可以拿来调试第三方接口,它还有什么用途吗?




其实开头已经说过,它就是SpringBoot启动后,你立马想干的事,都可以在里面写,所以你完全可以发挥想象去用。




我这里,提供几个思路作为参考。



1)、数据库初始化


你可以使用CommandLineRunner来执行应用程序启动时的数据库初始化操作,例如创建表格、插入初始数据等。



2)、缓存预热


CommandLineRunner在应用程序启动后预热缓存,加载常用的数据到缓存中,提高应用程序的响应速度。



3)、加载外部资源


加载一些外部资源,例如配置文件、静态文件或其他资源。CommandLineRunner可以帮助你在启动时读取这些资源并进行相应的处理。



4)、任务初始化


使用CommandLineRunner来初始化和配置某些定时任务,确保它们在应用程序启动后立即开始运行。



5)、日志记录


SpringBoot启动后记录一些必要的日志信息,如应用程序版本、环境配置、甚至启动时间等等,这个看具体需求。



6)、组件初始化


你可能需要按照特定的顺序初始化一些组件,CommandLineRunner可以帮助你控制初始化顺序,只需要将它们添加到不同的CommandLineRunner实现类中,并使用@Order注解指定它们的执行顺序即可。



总结



其实,能用的地方挺多,我最后再举个例子,netty启动时,往往是绑定了端口并以同步形式启动。




但如果要和SpringBoot整合,我们不可能还那么做,而是交给SpringBoot来控制netty的启动和关闭,当SpringBoot启动后,netty启动,当SpringBoot关闭时,netty自然也关闭了,这样才比较优雅。




那么,我们完全可以将netty的启动执行程序放到CommandLineRunner中,这样就可以达到目的了。




没用过的xdm,今天学会一个新知识点了不,可以自己下去试试哦。


作者:程序员济癫
来源:juejin.cn/post/7273434389404893239
收起阅读 »

程序员工作七年后的觉醒:不甘平庸,向上成长,突破桎梏

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。 看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。


看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,在约束着你。


上学时,你要好好读书,争取考上985/211,最起码上个一本。


工作后,大家都羡慕考公上岸的,上不了岸的话,你需要找一个好公司,拿到一个高工资,最好还能当上管理人员。


后来有了家庭,你要承担起男人的责任,赚钱养家。


过去20多年的时间,我都觉着这样的条条框框没有问题,在年少轻狂的时光,这些条条框框决定了你的下限,大家不都是这么过来的吗?


可是我过去的努力,都是为了符合条条框框的各项要求。我越来越觉着疑惑,我的努力,到底是为了什么啊,是为了这些世俗上的要求吗,我到底为谁而活?


压力,是自己给的


说实话,自己也给自己不少压力。


刚毕业,没有房贷车贷的情况下,我便给了自己很大的压力。压力怎么来的呢?比如一个月五千块钱的工资,买不起一个最新款的iPhone,又比如北京的朋友们,工资相比我在二线城市,竟能高出我一倍。


后来工作半年决定去北京,也是工作7年来,唯一一次的裸辞。


初生牛犊不怕虎,裸辞给我带来的毒打,至今历历在目,比如银彳亍卡余额一天天减少的焦虑,比如连面试都没有的焦虑,还有时刻担心着要是留不在北京,被迫得回老家的焦虑。


记得青旅楼下,有一家串店叫“很久以前羊肉串”,不到五点的时候门口就会有人排队,晚上下楼时看着饭店里熙熙攘攘,吃着烤串喝着扎啤的人时,心里十分羡慕,但却又不会踏进饭店一步。


毕竟一个目前找不到工作的人,每天一睁眼就是吃饭和住青旅的成本,吃个20块钱一顿的快餐就好了,怎么可能花好几百下馆子呢?


那时候心里有个愿望,就是我也想每周都可以和朋友来这里吃顿烧烤、喝喝扎啤。


嗯,我也不知道为什么,那时候对自己就是这么严苛。家庭虽不算富裕,但也绝不可能差这几顿烧烤、住几晚好的宾馆的钱,但我就是这样像苦行僧一样要求着自己,仿佛在向爸妈多要一分钱,就代表着自己输了。


后来工作稳定了,工资也比毕业时翻了几倍,恰巧又在高位上车了房子,但似乎压力只增不减,同样是不敢花钱。


现在又有了娃,这次压力也不用自己给了,别管他需要什么,一个小眼神,你只想给他买最好的。因此不敢请假,更不敢裸辞GAP一段时间了,这种感觉就像是在逃避赚钱的责任,不误正业一般。


一味的向前冲


带着压力,只能一味的向前冲,为了更高的薪资不断学习,为了更高的职级不断拼搏。


在“赚钱”这件事上,男人的基因里就像被编写好了一段代码。


    while (true){
makeMoreMoney();
}

过程中遇到困难,压力大,有难过的时候怎么办,身边有谁能去诉说呢?


中国的传统文化便是“男儿当自强”、“男儿有泪不轻弹”,怎么能去向别人诉说自己的痛苦呢?


那时候现在的老婆那时候还在上学,学生很难理解职场。结婚后,更没有人愿意在伴侣前展示自己的软弱。


和家人说?但是不开心的事,不要告诉妈妈,她帮不上忙,她只会睡不着觉。


和好朋友们一起坐下聚聚,喝几杯啤酒,少聊一些工作,压力埋在心里,让自己短暂的放松一下。



但现在的行业现状,不允许我们一味的在职场上冲了。


行业增速放缓,互联网渗透率达到瓶颈,随着而来的就是就业环境变差,裁员潮来袭。


你可以选择在职场中的高薪与光环,但也要付出相应的代价,比如变成“云老公/老婆”,“云爸爸/妈妈”。


或许我们都很想在职场中有一番作为,但是外部环境可能会让我们头破血流。


为了家庭,所以在职场中精进自己,升职加薪。我不禁在想,这看似符合逻辑的背后,我自己到底奋斗的是什么


不甘平庸,不服输


从老家裸辞去北京,是不满足于二线城市的工作环境,想接触互联网,获得更快的进步。


在北京,从小公司跳槽到大厂,是为了获得更高的薪资与大厂的光环。


再次回到老家,是不满生活只有工作,回来可以更好的平衡工作和生活。


回想起来,很多时候,自己就像一个异类。


明明工作还不满一年,技术又差,身边的朋友敢于跳槽到其他公司,涨一两千块钱的工资已经算挺好了,我却非得裸辞去北京撞撞南墙。


明明可以在中小公司里按部就班,过着按点下班喝酒打游戏的生活,却非得在在悠闲地时候,去刷算法与面经,不去大厂不死心。


明明可以在大公司有着不错的发展,负责着团队与核心系统,却时刻在思考生活中不能只有工作,还要平衡工作和家庭,最终放弃大厂工作再次回到老家。


每一阶段,我都不甘心于在当下的环境平庸下去,见识到的优秀的人越多,我便越不服输。


至此,我上面问自己的两个问题,我到底为谁而活?我自己到底奋斗的是什么,似乎有了些答案。


我做的努力,短期看是为了能够给自己、给家人更好的物质生活,但长远来看,是为了能让自己有突破桎梏与困境,不断向上的精神


仰望星空


古希腊哲学家苏格拉底有一句名言:“未经检视的人生不值得活。”那么我们为什么要检视自己的人生呢?正是因为我们有不断向上的愿望,那么我在想愿望的根源又到底是什么呢?


既然选择了不断向上,我决定思考,自己想成为什么样的人,或者说,一年后,希望自己变成什么样子,3年呢,5年呢?


当然,以后的样子,绝不是说,我要去一个什么外企稳定下来,或者说去一个大厂拿多少多少钱。


而是说,我希望的生活状态是什么,我想去做什么工作/副业,达成什么样的目标。


昨天刷到了一个抖音,这个朋友在新疆日喀则,拍下了一段延时摄影,我挺受震撼的。



生活在钢铁丛林太久了,我一直特别想去旅行,比如自驾新疆、西藏,反正越远越好。在北京租的房子,就在京藏高速入口旁,我每天上班都可以看到京藏高速的那块牌子,然后看着发会呆,畅想一下自己开着车在路上的感觉。


可好多年过去了,除了婚假的时候出去旅行,其余时间都因为工作不敢停歇,始终没有机会走出这一步,没有去看看祖国的大好河山。


我还发现自己挺喜欢琢磨,无论在做什么事情,我都会大量的学习,然后找到背后运行的规律。因为自己不断的思考,所以现实中,很少有机会和朋友交流,所以我会通过写作的方式,分享自己的思考、经历、感悟。


我写了不少文章,都是关于工作几年,我认为比较重要的经历的文章,也在持续分享我关于职业生涯的思考。


从毕业到职场,走过的弯路太多了,小到技术学习、架构方案设计,大到职业规划与公司选择,每当回忆起自己在职场这几年走过的弯路,就特别想把一些经验分享给更多的人,所以我持续的写,希望看到我文章的朋友,都能够对工作、生活有一点点帮助。


所以,我的短期目标,是希望能够帮助在职场初期、发展期,甚至一些稳定期的朋友们,在职场中少一点困惑,多一点力量


方式可能有很多,比如大家看我的文章,看我推荐的书籍、课程,甚至约我电话进行1v1沟通,都可以,帮助到一个人,我真的就会感到很满足,假设因为个人能力不足暂时帮不到,我也能根据自己的不足持续学习成长。


那么一年后,我希望自己变成什么样?
我希望自己在写作功底上,能够持续进步,写出更具有逻辑性、说服力的内容,就像明白老师、雪梅老师那样。公众号希望写出一篇10w+,当然数量越多越好,当然最希望的是有读者能够告诉我,读完这篇文章很有收获,这样比数据更能让人开心,当然最好还能够有一小部分工作之外的收入。


那么三年呢?
3年后,快要32岁了。希望那时候我已经积累了除了写作外,比如管理、销售、沟通、经营能力,能够有自己赚到工资外收入的产品、项目,最好能够和职场收入打平,最差能够和房贷打平,有随时脱离职场的底气。


五年呢?十年呢?
太久远了,想起来都很吃力的感觉。我一定还在工作,但一定不是打工,希望自己有了一份自己喜欢的事业,能够买到自己的dream car,然后能够随时带着家人看一看中国的大好河山。


你是不是想问,为什么一定要想这些?


因为当我想清楚这个问题的时候,那当下该做什么事情,该做什么选择,就有了一个清晰的标准:这件事情、这个选择,能否帮我们朝「未来的自己」更进一步?


这时候当再遇到压力、困难,我们就会变的乐观,有毅力、有勇气、自信、耐心,积极主动。


因为你自己想干成一件事,你就会迸发出120%的能量。


当然,也希望自己试试放下盔甲,允许自己撤退,允许自己躺平,允许自己怂,允许自己跟别人倾诉痛苦。


说在最后


说了很多,感谢你能看到最后。


感觉整体有点混乱,但还是总结一下:


起因是感觉自己压力很大,因为持续的大量输入导致自己有点陷入信息爆炸的焦虑,有一天下班到家时感觉头痛无比,九点就和孩子一起睡觉了,因此本来想谈谈中国男性的压力。


但不由自主的去思考自己的压力是从哪里来的,去发现压力竟都来源于传统文化、社会要求,于是越想越不服气,我为什么非得活成别人认为应该活成的样子?


于是试着思考自己想成为什么样子,其实也是一直在琢磨的一件事情,因为当开始探索个人IP的时候,我就发现自己需要更高一层的、精神层面的指导,才能让自己坚持下去。


如果你和我一样,希望你给自己的压力更小一些,环境很差,但总还有事情可以去做,愿你可以想清楚,你想成为的样子。一时想不清楚也没关系,也愿你可以允许自己撤退,允许自己软弱。


不知道你有没有想过,自己想要成为的样子呢?


作者:东东拿铁
来源:juejin.cn/post/7374337202653265961
收起阅读 »

如何优雅的将MultipartFile和File互转

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。 前言 首先来区别一下MultipartFile和File: MultipartFile是 S...
继续阅读 »

我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。


前言


首先来区别一下MultipartFile和File:



  • MultipartFile是 Spring 框架的一部分,File是 Java 标准库的一部分。

  • MultipartFile主要用于接收上传的文件,File主要用于操作系统文件。


MultipartFile转换为File


使用 transferTo


这是一种最简单的方法,使用MultipartFile自带的transferTo 方法将MultipartFile转换为File,这里通过上传表单文件,将MultipartFile转换为File格式,然后输出到特定的路径,具体写法如下。


transferto.png


使用 FileOutputStream


这是最常用的一种方法,使用 FileOutputStream 可以将字节写入文件。具体写法如下。


FileOutputStream.png


使用 Java NIO


Java NIO 提供了文件复制的方法。具体写法如下。


copy.png


File装换为MultipartFile


从File转换为MultipartFile 通常在测试或模拟场景中使用,生产环境一般不这么用,这里只介绍一种最常用的方法。


使用 MockMultipartFile


在转换之前先确保引入了spring-test 依赖(以Maven举例)


<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>versionversion>
<scope>testscope>
dependency>

通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。


multi.png


作者:程序员老J
来源:juejin.cn/post/7295559402475667492
收起阅读 »

面试官问我String能存储多少个字符?

首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。 编译器源码如下,限制了字符串长度大于等于65535就会编译不通过 private void checkStringConstant(DiagnosticPosition...
继续阅读 »

  1. 首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。

  2. 编译器源码如下,限制了字符串长度大于等于65535就会编译不通过


    private void checkStringConstant(DiagnosticPosition var1, Object var2) {
    if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
    this.log.error(var1, "limit.string", new Object[0]);
    ++this.nerrs;
    }
    }

    Java中的字符常量都是使用UTF8编码的,UTF8编码使用1~4个字节来表示具体的Unicode字符。所以有的字符占用一个字节,而我们平时所用的大部分中文都需要3个字节来存储。


    //65534个字母,编译通过
    String s1 = "dd..d";

    //21845个中文”自“,编译通过
    String s2 = "自自...自";

    //一个英文字母d加上21845个中文”自“,编译失败
    String s3 = "d自自...自";

    对于s1,一个字母d的UTF8编码占用一个字节,65534字母占用65534个字节,长度是65534,长度和存储都没超过限制,所以可以编译通过。


    对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,长度和存储也都没超过限制,所以可以编译通过。


    对于s3,一个英文字母d加上21845个中文”自“占用65536个字节,超过了存储最大限制,编译失败。


  3. JVM规范对常量池有所限制。量池中的每一种数据项都有自己的类型。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANTUtf8类型表示。CONSTANTUtf8的数据结构如下:


    CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
    }

    我们重点关注下长度为 length 的那个bytes数组,这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大字节数。length 的类型是u2,u2是无符号的16位整数,因此理论上允许的的最大长度是2^16-1=65535。所以上面byte数组的最大长度可以是65535


  4. 运行时限制


    String 运行时的限制主要体现在 String 的构造函数上。下面是 String 的一个构造函数:


    public String(char value[], int offset, int count) {
    ...
    }

    上面的count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1。所以在运行时,String 的最大长度是2^31-1。


    但是这个也是理论上的长度,实际的长度还要看你JVM的内存。我们来看下,最大的字符串会占用多大的内存。


    (2^31-1)*16/8/1024/1024/1024 = 2GB

    所以在最坏的情况下,一个最大的字符串要占用 2GB的内存。如果你的虚拟机不能分配这么多内存的话,会直接报错的。





补充 JDK9以后对String的存储进行了优化。底层不再使用char数组存储字符串,而是使用byte数组。对于LATIN1字符的字符串可以节省一倍的内存空间。


作者:念念清晰
来源:juejin.cn/post/7343883765540831283
收起阅读 »

带你从0到1部署nestjs项目

web
前言 最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway 大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn) 最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲...
继续阅读 »

前言


最近跟着一个掘金大佬做了一个全栈项目,前端react,后端我是用的nest,大佬用的midway


大佬博客地址(前端小付 的个人主页 - 动态 - 掘金 (juejin.cn)


最近项目也是部署上线了,因为域名还没备案,地址就先不发出来了,这篇文章就讲讲如何部署。一直有兄弟问prisma如何部署,这篇文章就帮你扫清障碍,文章可能比较长,希望耐心看完


后端技术栈



  • nestjs

  • mysql

  • redis

  • minio

  • prisma


部署需要掌握的知识



  • docker

  • github actions

  • 服务器


实战


nestjs打包镜像


我们部署的时候用的docker,docker需要拉镜像,然后生成容器,docker的知识可以去学习下,这里就默认大家会了,我们在打包的时候要写Dockerfile文件,后端项目是需要保留node_modules的,所以打包的时候一起打进去,我的项目用的pnpm包管理工具,我的文件挂载时有点点问题,我就没有用pm2去执行多阶段打包,多阶段打包速度会比较快,还有就是比如开发环境的依赖可以不打,当然这都是优化的地方,暂时没有去做,大家可以自行尝试


# 因为我们项目使用的是pnpm安装依赖,所以找了个支持pnpm的基础镜像,如果你们使用npm,这里可以替换成node镜像
# FROM nginx:alpine
FROM gplane/pnpm:8 as builder

# 设置时区
ENV TZ=Asia/Shanghai \
DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

# 创建工作目录
RUN mkdir -p /app

# 指定工作目录
WORKDIR /app

# 复制当前代码到/app工作目录
COPY . ./

RUN npm config set registry https://registry.npm.taobao.org/
# pnpm 安装依赖
COPY package.json /app/package.json

RUN rm -rf /app/pnpm-lock.yml
RUN cd /app && rm -rf /app/node_modules && pnpm install

RUN cd /app && rm -rf /app/dist && pnpm build

EXPOSE 3000
# 启动服务
CMD pnpm run start:prod


这样后端镜像就构建好了,接下来去编写github action的文件,github actions是做ci/cd的,让我们每次的部署走自动化流程,不要每次手动去做这些工作


github actions


在我们的根目录下面创建这样一个文件,这个文件名字可以随便取


12.png


然后在里面编写逻辑


name: Docker

on:
push:
branches: ['main']

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf

- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

- name: Log int0 registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new

- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

- name: SSH Command
uses: D3rHase/ssh-command-action@v0.2.1
with:
HOST: ${{ secrets.SERVER_IP }}
PORT: 22
USER: root
PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }}
COMMAND: cd /root && ./run.sh

这里的['main']就是我们要执行哪个分支,你不是main分支,那就改成你的分支就可以,其他都是固定的模板,直接用


SSH Command 这个是我们取做ci/cd的时候,每次我们提交代码,然后配置了ssh密钥,就可以让服务器执行run.sh命令,这个shell脚本我们后面可以用到,这里就记住是让服务器去执行拉取镜像以及执行启动容器的。


当我们做到这一步之后,我们提交代码的时候,应该会出现这样的情况


13.png


因为还没有去配置ssh密钥,这个肯定跑不起来,看到我们上面ssh command里面有两个变量,就是我们要配置的,接下来我们去搞服务器。


服务器


最近双十一活动,买个服务器还是挺香的,我买的阿里云2核2g的99/年,买的时候选操作系统,随便一个都可以,我因为对ubuntu熟悉一下,就买了ubuntu操作系统的,买好之后,记得重置密码


14.png


后面我们用shell工具连接的时候需要用到密码的


之后我们去下载一个shell工具,连接服务器用的,常见的有xshell finalshell,我用的第二个。


15.png


就傻瓜式安装,下一步就可以,然后我们去连接一下服务器,去下载宝塔。


16.png


第二步那里选择ssh连接就可以了,然后主机就是你的服务器公网ip,密码就是刚刚的,用户名就是root


连接上了之后,去下载宝塔,这个是ubuntu的命令,其他的操作系统有差别,可以去搜一下就有


wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh


下载好之后输入bt default命令就可以打开了


17.png


因为宝塔是个可视化操作面板,比较方便,所以先弄好。


接下来我们去搞服务器密钥


18.png


我们在这里创建好密钥对,记得它只有一次机会,所以下载好了记得保存在你记得住的地方,然后创建好,记得要绑定,不然没效果,然后我们就要得用ssh密钥来连接服务器了


20.png


至此,我们的服务器也弄好了


github绑定密钥


21.png


这个是settings界面的,然后大家按照步骤创建就可以,到这里我们的配置就结束了。


创建shell脚本


我们上面不是说了,我们要写一个bash文件吗,现在就要来写,这个bash文件我们要执行拉镜像和跑容器


23.png


我们可以选择在宝塔中操作


docker-compose pull && docker-compose up --remove-orphans

然后我们在同目录下也就是root目录下面新建一个docker-compose.yml文件,来启动容器的,这个文件就不要展示了,也就是创建了哪些服务,挂载哪些卷,如果有需要评论区说一下就行,很简单,因为我们用了很多服务,mysql redis minio nginx 这些多镜像,就得多个容器来跑,docker-compose无疑就好


到这里后端项目就部署完了,我们还得迁移数据库对吧


数据库部署


pirsma迁移

因为我用的mysql和prisma,typeorm思路差不多,可以一样用。我们的prisma以及typeorm迁移的时候只可以同步表结构,数据不会同步过去,所以我们跑迁移命令的时候,跑完会发现没有数据,我们需要手动导入数据


另外注意点,我们docker-compose.yml里面的mysql容器名字对应我们连接的主机名,这里记得更改prisma连接,不然你的prisma还连接在localhost肯定找不到


我们来上手操作


24.png


这是我现在在跑的容器,我要找到我的后端项目对应的容器id,进去执行命令


docker exec -it <容器id> /bin/sh 跑这个我们就可以在容器内部执行命令


25.png


然后就可以把表结构同步过去了,我们也可以在生成Dockerfile的时候写迁移命令也是可以的,这样就不用手动同步了


数据库导出

我们需要将本地的数据迁移上去,需要先导出sql文件,这个就不用在这里展开说了,很简单,不会可以去找个博客教程,不到30s就完了,导出后我们需要将那个sql文件


然后我们在宝塔操作,找到你正在运行的mysql容器目录


26.png


将你的sql文件上传上去,放哪里都无所谓,记得路径就行


然后我们进入mysql容器里面,跑上面的那个命令



  1. 登录账号 mysql -u root -p

  2. 输入密码 ******* 输入你数据库连接的那个密码

  3. 进入之后 USE <database_name> 就选中了那张表

  4. 然后执行 source 刚刚的那个sql文件路径


这样操作数据就同步上去了,注意,数据同步前是一定要有表结构的,所以有先后顺序,这个地方注意。


也可以用这个命令, 将sql文件拷贝到你的容器内,然后跑上面的步骤,看个人喜好了。
docker cp /本地路径/your_file.sql 容器名称:/容器路径/your_file.sql


到这里我们的部署就结束了,等项目正式上线的时候,还有其他注意点还会再写一篇博客的


最后


项目是跟着开头提到的小付大佬学习的,主要想学下react,没想到是个全栈项目,就用nestjs写了后端,也学到了很多前端,后端,部署的知识,强烈推荐大家可以去看看。最后 觉得不错的话,可以给个点赞加关注😘


作者:西檬
来源:juejin.cn/post/7299859799780655155
收起阅读 »

webview预加载的技术原理和注意点

web
此文章介绍webview预加载的技术原理和注意点 背景 网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果 原理 即空间换时间,提前加载页面url 由于首页就有网页入口,所以需要在首页Activity进行预加载。 创建webview Web...
继续阅读 »

此文章介绍webview预加载的技术原理和注意点


背景


网页优化,对网页的webview进行预加载,用户点开页面达到秒开效果


原理


即空间换时间,提前加载页面url


由于首页就有网页入口,所以需要在首页Activity进行预加载。


创建webview



  • WebView(MutableContextWrapper(context),使用MutableContextWrapper替换Context,,方便后续复用时替换为webview容器Activity的上下文对象

  • WebSettings、原生方法初始化,保证预加载时功能正常,因为后续不会再进行loadUrl,必须保证h5页面正常显示

  • WebViewClient、WebChromeClient监听



    • 重写onPageFinished、onReceivedTitle方法,主要为了title的接收,并且记录下来,后续webview复用时直接显示title

    • 重写onReceivedSslError方法,避免证书错误加载显示失败



  • 首页预加载容器Layout,置于最底层,宽度全屏,高度设置为全屏高度 - 顶部导航栏高度 - 状态栏高度


viewGr0up.addView(WebView(MutableContextWrapper(context)).also { web ->  // 初始化webview 
}, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


  • 刷新逻辑,绑码状态或登录信息改变时,刷新已经预加载好的webview的url


复用webview



  • webview容器整改

    • 判断是否需要使用已预加载的webview,如果需要复用,则根布局添加预加载webview进来,注意布局层级,避免覆盖了其他控件





webView?.let { web ->
(web.context as MutableContextWrapper).baseContext = activity
}

container.addView(it, 0, ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT))


  • 原webview容器使用ViewStub代替,如果不需要复用则将ViewStub inflate,进行正常的h5页面加载

  • 添加预加载webview后,直接显示,不需要loadUrl,但是白屏分析之类的逻辑别忘了手动调用


页面关闭



  • webview跟随Activity一起进行销毁,但是需要通知首页重新生成一个webview,准备给下一次用户点击使用

  • 首页关闭,页面栈清空时,需要清空单例中webview对象,并且调用destroy

  • 不推荐回收webview继续使用,因为在实际测试中表现并不好,重建webview可以规避很多问题场景


如果用户点击比较快时,依然会看到加载过程和骨架屏


问题点和解决



  • 复用webview时,页面视觉残留上一次h5页面状态



    • 原因:页面关闭时,触发Activity的onStop方法,webview调用onPause,此时webview被暂停,webview的reload也不会立即刷新

    • 解决:回收webview时,对webview重新恢复交互状态(onResume)



  • 页面关闭,迅速点开,页面先显示上一次h5页面状态,然后开始reload



    • 原因:当Activity反复打开关闭时,Activity的回收、对象GC会滞后,导致webview已经被复用并且上屏了,webview才开始触发reload

    • 解决:webview不进行回收,每次页面关闭都进行销毁,重新创建webview



  • webview多次reload后,网络请求失败
    axios网络请求失败,response报文为空,暂未找到原因,了解的大佬麻烦解答一下,谢谢。当不回收webview后,此场景不存在了

  • h5页面正常显示后,又刷新一次页面



    • 原因:webview复用时,对webview重新进行初始化(重新创建原生能力、重置上下文对象等)时,会重新对UserAgent进行赋值,导致重新刷新了一次。

    • 排查过程
      发现网页骨架屏刚出现时点开不会重复刷新;骨架屏消失后点开也不会重复刷新;唯独骨架屏时,刚出现vConsole绿色块时点开会出现重复刷新。
      对webview的shouldOverrideUrlLoading方法进行debug,发现并没有进入断点,说明并不是调用了reload,推测有什么逻辑导致网页重新刷新了一次。
      随即用傻子办法,一段一段代码注释,发现友盟组件attach webview和通用webview容器设置userAgent的代码会导致重复刷新,难道友盟组件也有设置userAgent的代码?
      然后查看友盟组件源码,不出所料,发现友盟组件中反射了setUserAgentString方法,并且对userAgent拼接了"Umeng4Aplus/1.0.0字符串,如下图所示。


      那是否设置的userAgent有什么敏感字符串导致刷新?随即将userAgent只设置为空字符串,发现也会导致重复刷新。
      到这里水落石处,但为什么userAgent发现变化会导致网页重复刷新?
      询问前端大佬,回复没有监听userAgent,userAgent变化也不会做什么操作,故而没有得到具体答案,了解的大佬麻烦解答一下,感谢。

    • 解决:webview复用时,不进行userAgent的重复赋值




IMG20240529101049.png



  • 复用webview时,页面白屏等待一会后秒开h5页面

    • 原因:预加载时webview在1像素的layout中加载,复用到通用webview容器中,webview控件的布局已经结束,但需要时间对H5进行渲染,在重复打开关闭后或性能低下的手机表现更甚

    • 解决:首页预加载webview时,已通用webview容器同大小进行布局



  • 内存泄漏问题

    • 原因:部分原生方法对象中对Activity和Fragment有强引用,当原生方法对象被addJavascriptInterface添加进webview时,复用的webview生命周期长如Application,就会强引用到Activity,导致无法回收,内存泄漏

    • 解决:webview回收时清空Activity、Fragment的引用

    • 不复用webview后此问题不存在了




作者:聪本尊18680
来源:juejin.cn/post/7373937820179005478
收起阅读 »

面试官:为什么忘记密码要重置,而不是告诉我原密码?

Hello,大家好,我是 Sunday。 最近有个同学在面试中遇到了一个很有意思的问题,我相信大多数的同学可能都没有遇到过。 面试官提问说:“为什么很多网站忘记密码需要重置,而不是直接告诉用户原密码?” 很有意思的问题对不对。很多网站中都有“忘记密码”的功能,...
继续阅读 »

Hello,大家好,我是 Sunday。


最近有个同学在面试中遇到了一个很有意思的问题,我相信大多数的同学可能都没有遇到过。


面试官提问说:“为什么很多网站忘记密码需要重置,而不是直接告诉用户原密码?


很有意思的问题对不对。很多网站中都有“忘记密码”的功能,但是为什么当我们点击忘记密码,经过一堆验证之后,网站会让我们重置密码,而不是直接告诉我们原密码呢?


所以,今天咱们就来说一说这个问题。


防止信息泄露



2022年11月1日,Termly 更新了《98个最大的数据泄露、黑客和曝光事件》(98 Biggest Data Breaches, Hacks, and Exposures)。其中包括很多知名网站,比如:Twitter



所以,你保存在网站中的数据可能并没有那么安全。那么这样的数据泄露后会对用户产生什么影响呢?


对大多数人来说最相关的经历(网上看到的)应该是诈骗电话,他们甚至可以很清楚的告诉你你的所有个人信息。那么这些信息是怎么来的呢?


有些同学可能说是因为“网站贩卖了我的个人信息”,其实不是的。相信我 大多数的网站不会做这样的事情


出现这样事情的原因,大部分都是由于数据泄露,导致你所有的个人信息都被别人知道了。


那么,同理。既然他们可以获取到你的私人信息,那么你的账户和密码信息是不是也有可能被盗取?


而对于大多数的同学来说,为了防止密码太多忘记,所以很多时候 大家都会使用统一的密码! 也就是说你的多个账号可能都是同一个密码。所以,一旦密码泄露,那么可能会影响到你的多个账号,甚至是银彳亍卡账号。


因此,对于网站(特别是一些大网站)来说,保护用户数据安全就是至关重要的一件事情。那么他们一般会怎么做呢?


通常的处理方式就是 加密。并且这种加密可能会在多个不同的阶段进行多次。比如常见的:SHA256、加盐、md5、RSA 等等


这样看起来好像是很安全的,但是还有一个问题,开发人员知道如何解密他们。或者有些同学会认为 数据库中依然存在着正确的密码 呀?一旦出现信息泄露,不是依然会有密码泄露的问题吗?


是的,所以为了解决这个问题,网站本身也不知道你的密码是什么。


网站本身也不知道你的密码是什么


对于网站(或者其他应用)来说,它们是 不应该 存储你的原密码的。而是通过一些系列的操作来保存你加密之后的代码。并且这个加密是在前端传输到服务端时就已经进行了,并且是 不可逆 的加密操作,例如:MD5 + 加盐



我们举一个简单的例子:


比如有个用户的密码是 123456,通过 md5 加密之后是:E10ADC3949BA59ABBE56E057F20F883E


md5 理论上是不可逆的,所以从理论上来说这个加密后的代码是不可解析的。但是 md5 有个比较严重的问题就是:同样的字符串加密之后会得到同样的结果


这也就意味着:E10ADC3949BA59ABBE56E057F20F883E 代表的永远都会是 123456


所以,如果有一个很大的 md5 密码库,那么理论上就可以解析出所有的 md5 加密后的字符串。就像下图一样:




因此,在原有的 md5 加密之上,很多网站又增加了 加盐 的操作。所谓加盐指的就是:在原密码的基础上增加一些字符串,然后进行 md5 加密


比如:



  1. 原密码为 123456

  2. 在这个密码基础上增加固定字符“LGD_Sunday!”

  3. 得到的结果就是:“LGD_Sunday!123456”

  4. 然后用该字符进行 md5 加密,结果是:E1FC8CB7B54BED0FDC8711530236BA4D

  5. 此时尝试解密,会发现 解密失败



这样大家是否就可以理解,为什么很多网站在让我们输入密码的时候 ,要求包含 大小写+符号+ 字母 + 数字 了吧。本质上就是为了防止被轻松解密。


而服务端拿到的就是 “E1FC8CB7B54BED0FDC8711530236BA4D” 这样的一个加密后的结果。然后服务端再次对密码进行加密操作,从而得到的是一个 被多次加密 的数据,保存到服务端。


所以说:网站无法告知你密码,因为它也不知道原密码是什么。


目前很多网站或应用为了保证用户安全,都已经采取 扫码登录、验证码登录 等方式进行登录验证,这种无密码的方式,会更大程度的保证你的账号安全。


作者:程序员Sunday
来源:juejin.cn/post/7353580789299281961
收起阅读 »

一定要考公吗,一定要上岸吗,可是我已经考出病来了!

昨天朋友圈里面看到一个好朋友发了病书,患上抑郁症了,他考公一年多了,说实话,我心中百感交集,因为我身边考公的朋友实在太多了,大家心里都在承受巨大的压力。 我觉得没必要,可能你觉得考上公务员就可以稳吃一辈子了,但是事实真的是这样吗? 当然,如果能考上一线城市的...
继续阅读 »

昨天朋友圈里面看到一个好朋友发了病书,患上抑郁症了,他考公一年多了,说实话,我心中百感交集,因为我身边考公的朋友实在太多了,大家心里都在承受巨大的压力。


fde22171b5fb0e4b6983fe643409e32.jpg


我觉得没必要,可能你觉得考上公务员就可以稳吃一辈子了,但是事实真的是这样吗?


当然,如果能考上一线城市的公务员,那么绝对是普通人最好的选择,也绝对够牛逼,但是大多数人考不上。


别说一二线,现在能考进县城里面的公务员,那也算是天之骄子,注意,这里指的是正编,如果是合同的,那么就不说了,大多数人终其一生可能都只能干到乡镇里面。


这和你有能力没能力其实关系不大,除非你能力万里挑一,特别出众。


其一,始终要明白“庙小妖风大,浅池王八多”,没有一个人不想往高处走,你进入一个乡镇单位里面,里面年龄比你大,资历比你深的大有人在,你觉得他们都想留在乡镇吗?是乡镇的饭好吃,空气清新吗?


不是,一切都是因为没机会,没机会说直白一点,就是上面没人,有人早都进城了。


前两年我在乡镇遇到一个亲戚,和她闲谈,她说租她家隔离房子的人就是乡镇的公务员,快二十年了,还是在乡镇里面,和他同一批进来的,有关系的早都进城了!


这就很现实,还有你也不要说自己家族里面谁谁谁是那个领导的,没用,如果你没有价值,或者关系不到位,人家不会真心实意帮你的!


当然,如果你够强,那么不依靠关系,你也能进城。


我朋友的哥哥就是直接从其他县城的公务员岗位辞职,然后一年不到就考回本县,而且他们还不属于县里管,是省里管,人家考试就是牛逼,面试就讨人喜欢。


但是大部分人呢,我们自己心知肚明吧,如果你真的牛逼,那么可能早都上岸了,无论是村里还是乡镇。


到现在都还没上岸,那么就证明自己能力平庸,而且没有关系,认清现实一点。


其二,考上了就真的稳了吗,就能马上躺平了?


很多人会说,考上了就稳了,直接吃一辈子,又有面子,福利又好。


别天真,这也要分单位,的确有的单位福利挺好,年终奖也不少,但是只是很小一部分。


但是近年来,公务员降薪你也看到了吧,财政那么紧张,福利肯定会大打折扣,甚至没有。


在中国,现在大多数小城市的公务员工资也就是几千块,如果家里不是县城里面的且父母不能扶持,那么生活也是挺困难的,如果家庭条件特别好,那么就当玩。


之前听朋友说,她同学香港大学硕士毕业的,家里特别有钱,但是父母就叫她考回家里,一个月虽然工资才两三千,但是父母一个月给她一万,目的就是想让她陪父母。


但是大部分人呢,别说家里给钱,不给家里就已经不错了,很多人虽然考上了公务员,但是经常在刷信用卡,因为那点工资根本做不了什么。


所以,穷人家的孩子考上公务员(小地方的公务员),不会像范进中举那样改变人生,可能会更加困难。


其实最主要的是你基本没有任何机会往上走,你考上的这个岗位,基本上就是你这辈子的终点。


上面说的这一切都是你考上的前提,无论是镇里,村里还是县里。


但是回到现实,你能考上吗?


每年身前面对的是那么多刚毕业的大学生,他们身份比你有优势,身后面对的是那么多老油条,你在夹缝中很难的。


大多考公的人,要么全职,要么随便找个两三千块钱的活干,二者都承受巨大的心理压力和经济压力。


特别是经历一次又一次的落榜,心态会越来越糟。


看着时间从自己手中按时溜走,而自己除了学会了一些考试技巧,背会了一些“八股文”,其他一无所获,如果考上了,那么就是有用的,考不上,那么这一套拿到市场上去并没有什么用。


身体上的苦尚能抗住,而情绪上的崩溃却可能影响一个人一生,你面对的是一个未知的结果,而且这个结果可能都不是你自己想要的!


有时候,我觉得当自己真的扛不住了,那么不妨直接放手了,不要觉得下一次就上岸了,如果考了三四次都考不上,那么我觉得确实是自己智商和情商不行,提前一点认识到,别去死磕。


因为考了好多次都考不上,那么后面去考,基本上就是去玩一个概率游戏,因为考这么多次最起码花了一两年的时间,对于很多知识点,实际上自己都能基本摸清。


但是为啥还考不上,这基本就和智商挂钩了。


就像我一个程序员朋友,人家复习三个月就能上岸,而且还是贵阳的一个单位,竞争也很激烈,你能说是运气吗,反正我不信,我觉得这就是智商问题。


就像我在读大学的时候,一个算法题别人很快就能学会,但是我好多遍都学不会,花费十倍时间去学都赶不上别人,这时候我深刻意识到自己智商确实不行。


那么我肯定不会去死磕算法,因为毫无卵用,智商不行就是不行,不会因为你花很多时间就能改变的,这是天生的,并且也不是啥985大学毕业的,所以没必要去死磕。


但是我在应用层面和一些底层逻辑上有一定的积累和理解,所以我就往这个方向去做,何必去死死刷算法呢?


所以我觉得,无论考公考编还是其他,和智商有很大的挂钩,和情商也有很大的挂钩。


不要去和自己的弱项较劲,而是要充分去发掘自己的优势!


我们大部分人,智商平庸,情商也平庸,所以在这条路上是十分吃亏的,就是在玩一个概率游戏!


从我个人的角度来看,我觉得与其死耗,让自己心理的压力越来越大,不如换个角度去思考人生。


大多数人无非就是求稳,一辈子没多大的风险。


这其实是一种逃避,逃避面对现实,面对人生。


有时候,放过自己未必是一件坏事,人生如此短暂,难道就只有上岸才是唯一的目标吗?


当然,尊重每一个人的选择,考公本身也需要很大的勇气。


但是我们终究要回到现实,如果拿不到结果,那么也是自我感动,因为没人会在乎你的过程,别人也没兴趣了解你的过程。


而明天的路依然要自己负责!


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

设计问能不能实现这种碎片化的效果

web
前言 某天设计发来一个 网站,问我能不能实现这种类似的效果。 不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。 F12 调试 让我们打开调试,瞅瞅别人是如何实现。 可以看到在该层级下,页面有很多个 shard-wrap ...
继续阅读 »

前言


某天设计发来一个 网站,问我能不能实现这种类似的效果。


shard-img-reverse-xs.gif

不知你们什么感想,反正我当时第一次看到这个网站的效果时,心里第一反应就是这做个锤子啊。


F12 调试


让我们打开调试,瞅瞅别人是如何实现。


可以看到在该层级下,页面有很多个 shard-wrap 元素,而每一个元素都是定位覆盖其父元素的。


image.png

当我们添加 display: none; 后,可以看到嘴角这里少了一块。


image.png

而继续展开其内部的元素就可以看到主要的实现原理了:clip-path: polygon();


image.png

clip-pathpolygon 更详细的解释可以看 MDN,简单来说就是在一个 div 里面绘制一个多边形。


比如上图的意思就是:选取 div 内部坐标为 (9.38%,59.35%),(13.4%,58.95%),(9.28%,61.08%) 这三个点,并将其连起来,所以就能构成一个三角形了。然后再填充 backgroundColor 该三角形就有对应颜色了。


实现过程


调试看完别人的实现后发现,好像也不是很难。但是数据又如何搞来呢?


当然我们可以直接在接口那里找别人的数据,但是我找了一会发现不太好找。


于是想到咱们可是一名前端啊,简单写段 js 扒拉下来不就好了吗,想要更多,就滑一下滚轮,切换下一个碎片图像,再执行一次即可。


function getShardDomData() {
const doms = document.getElementsByClassName('shard')
const list = []
for (let i = 0; i < doms.length; i++) {
const style = window.getComputedStyle(doms[i])
let str = style.clipPath.replace('polygon(', '').replace(')', '')
list.push({
polygon: str,
color: style.backgroundColor,
})
}
return list
}
console.log('res: ', getShardDomData());

image.png

碎片化组件


简单封装一个碎片化组件,通过 transitiondelay 增加动画效果以及延迟,即可实现切换时的碎片化动画效果。我这里是用的 tailwindcss 开发的,大家可以换成对应 css 即可。


export type ShardComItem = {
color: string
polygon: string
}

export type ShardComProps = {
items: ShardComItem[]
}

export default function ShardCom({items}: ShardComProps) {
return (
<div className="relative w-full h-full min-w-20">
{items?.map((item, index) => (
<div className="absolute w-full h-full" key={`${index}`}>
<div
className="w-full h-full transition-all duration-1000 ease-in-out"
style={{
backgroundColor: item.color,
clipPath: `polygon(${item.polygon})`,
transitionDelay: `${index * 15}ms`,
}}
>
</div>
</div>
))}
</div>

)
}

模仿实现的 demo 地址


组件的代码放码上掘金了,感兴趣可以自提。



自制画板绘画 clip-path


当然只扒拉别人的数据,这肯定是不行的,假如设计师想自己设计一份碎片化效果该怎么办呢?


解决方法也很简单:那就自己简单开发一个绘图界面,让设计师自己来拖拽生成想要的效果即可。


线上画板地址


image.png

画板的实现就放到 下篇文章 中讲述了。


最后


当然最终只是简陋的实现了一部分效果罢了,还是缺少很多动画的,和 原网站 存在不少差距的,大家看个乐就行。


作者:滑动变滚动的蜗牛
来源:juejin.cn/post/7372013979467333643
收起阅读 »

只要条件够诱人,你比谁都自律!

​ 虽然我不知道贾玲是谁,也没看过她的电影,但是从最近刷到的视频和文章,我能感受到这人对自己确实挺狠。 很多人将其描述为励志和自律,但是我个人并不觉得有多励志和自律,因为如果不是为了拍这个电影,不是为了让自己更上一个阶梯,她大概率不会去减肥。 如果你有200斤...
继续阅读 »

图片


虽然我不知道贾玲是谁,也没看过她的电影,但是从最近刷到的视频和文章,我能感受到这人对自己确实挺狠。


很多人将其描述为励志和自律,但是我个人并不觉得有多励志和自律,因为如果不是为了拍这个电影,不是为了让自己更上一个阶梯,她大概率不会去减肥。


如果你有200斤,你朝思暮想了五六年的女孩子说如果你能减掉80斤,她就和你谈朋友,那么我相信你会拼了命去减肥。


如果你因为比较胖而患了三高,走路都气喘吁吁,医生说如果再不减肥,你最多能再活几年,那么我相信你绝对会节食,锻炼。


但是你知道即使锻炼出八块腹肌,心仪的女孩也不可能做你女朋友,你虽然肥胖,但是身体上的病还不严重。


那么你基本上也不会去刻意减肥,因为人都是喜欢有结果的事情,如果没有结果,那么我刷刷短视频,喝喝可乐,这多快乐,我干嘛去吃这个苦。


所以在朋友圈看到不少朋友被贾玲感动后直接立flag,说必须要减肥成功的人,多数也是当时被电影感动了,我相信坚持不了多久就会放弃的。


这样的事情我见得太多,自己也嘴炮过无数次,但是基本都是半途而废,因为你的感性盖过了理性,而感性本身就是很容易受环境影响。


高三时,学校总是搞一些活动,为了给学生打鸡血,会请那些演讲成功学的来感化大家,“你回头看一下你的老师,心里想一下你的父母”,种种言论敲打一颗尚未成熟的孩子心上,有一些情绪不稳定的直接抱着老师大哭,然后大声喊到,“老师,请你放心,我一定会考上重点大学,爸爸妈妈,我一定不会让你们失望”。


演讲结束后,大家都带着沉重的心情回到教室,三五天之内倒是挺努力的,也不和别人多说话,但是一个星期后,百分之九十五的人直接恢复原样,该睡觉睡觉,该摆烂摆烂。


为什么会这样?


因为当时的感性直接覆盖了理性,总觉得自己可上九天揽月,但是面对现实的时候,英语单词很难背,数学题很难解,最主要的是还面对一个未知的结果,因为没人能保证努力后就能考上重点大学,所以干脆直接摆烂。


纵使有一部分人看着在努力,但实际上不过是假自律,不过是为了麻痹自己,因为学进去多少你自己清楚,眼镜虽然盯着书,但是脑子已经到校外去了。


所以我们就能得出一个结论,因为外界的一些刺激而做的决定,发的誓,基本上都是头脑发热而已,就像喝醉了的所作所为第二天一定会后悔。


只有一种条件能让人像疯狗一样去坚持,那就是交换,并且交换的目标都是十拿九稳的,就像贾玲这样,本身就是名人,减下100斤后,团队进行运作,她的名声,收入等方面一定会发生很大的变化,所以她能坚持。


我们现实中看到的那些十年如一日都在坚持跑步的人,其实别人也是找到了交换物,比如健康的身体,清醒的头脑等等。


我之前和朋友聊天,他说写代码的时候头脑不清醒,所以坚持跑步早睡,这样每天大脑都比较清醒,工作效率就比较高,所以他跑步本身就在交换。


我们回头去想一下自己曾经放弃的那么多事,基本上都是因为没有找到交换物,所以才一次又一次的放弃。


普通的打工人的交换物无非就是身体和积累,坚持学习,坚持锻炼,坚持思考,坚持做一件能够提升自己的事,经过长时间的累积,无形的财富会变为有形的财富,即使变不成财富,也不会损失什么,唯一损失的就是本该可以挥霍的时间!


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

Mybatis-Plus的insert执行之后,id是怎么获取的?

在日常开发中,会经常使用Mybatis-Plus 当简单的插入一条记录时,使用mapper的insert是比较简洁的写法 @Data public class NoEo { Long id; String no; } NoEo noEo = ...
继续阅读 »

在日常开发中,会经常使用Mybatis-Plus


当简单的插入一条记录时,使用mapper的insert是比较简洁的写法


@Data
public class NoEo {
Long id;
String no;
}

NoEo noEo = new NoEo();
noEo.setNo("321");
noMapper.insert(noEo);
System.out.println(noEo);

这里可以注意到一个细节,就是不管我们使用的是什么类型的id,好像都不需要去setId,也能执行insert语句


不仅不需要setId,在insert语句执行完毕之后,我们还能通过实体类获取到这条insert的记录的id是什么


image.png


image.png


这背后的原理是什么呢?


自增类型ID


刚学Java的时候,插入了一条记录还要再select一次来获取这条记录的id,比较青涩


后面误打误撞才发现可以直接从insert的实体类中拿到这个id


难道框架是自己帮我查了一次嘛


先来看看自增id的情况


首先要先把yml中的mp的id类型设置为auto


mybatis-plus:
global-config:
db-config:
id-type: auto

然后从insert语句开始一直往下跟进


noMapper.insert(noEo);

后面会来到这个方法


// com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog(), false);
return stmt == null ? 0 : handler.update(stmt);
} finally {
closeStatement(stmt);
}
}

在执行了下面这个方法之后


handler.update(stmt)

实体类的id就赋值上了


继续往下跟


// org.apache.ibatis.executor.statement.PreparedStatementHandler#update
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}

image.png


最后的赋值在这一行


keyGenerator.processAfter

可以看到会有一个KeyGenerator做一个后置增强,它具体的实现类是Jdbc3KeyGenerator


// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processAfter
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, parameter);
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processBatch
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
try (ResultSet rs = stmt.getGeneratedKeys()) {
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
// Error?
} else {
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeys
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
Object parameter)
throws SQLException {
if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
// Multi-param or single param with @Param
assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
// Multi-param or single param with @Param in batch operation
assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
} else {
// Single param without @Param
// 当前case会走这里
assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#assignKeysToParam
private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
String[] keyProperties, Object parameter)
throws SQLException {
Collection<?> params = collectionize(parameter);
if (params.isEmpty()) {
return;
}
List<KeyAssigner> assignerList = new ArrayList<>();
for (int i = 0; i < keyProperties.length; i++) {
assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
}
Iterator<?> iterator = params.iterator();
while (rs.next()) {
if (!iterator.hasNext()) {
throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
}
Object param = iterator.next();
assignerList.forEach(x -> x.assign(rs, param));
}
}

// org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.KeyAssigner#assign
protected void assign(ResultSet rs, Object param) {
if (paramName != null) {
// If paramName is set, param is ParamMap
param = ((ParamMap<?>) param).get(paramName);
}
MetaObject metaParam = configuration.newMetaObject(param);
try {
if (typeHandler == null) {
if (metaParam.hasSetter(propertyName)) {
// 获取主键的类型
Class<?> propertyType = metaParam.getSetterType(propertyName);
// 获取主键类型处理器
typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
JdbcType.forCode(rsmd.getColumnType(columnPosition)));
} else {
throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
+ metaParam.getOriginalObject().getClass().getName() + "'.");
}
}
if (typeHandler == null) {
// Error?
} else {
// 获取主键的值
Object value = typeHandler.getResult(rs, columnPosition);
// 设置主键值
metaParam.setValue(propertyName, value);
}
} catch (SQLException e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
e);
}
}

// com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int, java.lang.Class<T>)
@Override
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
// ...
else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
checkRowPos();
checkColumnBounds(columnIndex);
return (T) this.thisRow.getValue(columnIndex - 1, this.longValueFactory);

}
// ...
}

image.png


最后可以看到这个自增id是在ResultSet的thisRow里面


然后后面的流程就是去解析这个字节数据获取这个long的id


就不往下赘述了


雪花算法ID


yml切换回雪花算法


mybatis-plus:
global-config:
db-config:
id-type: assign_id

在使用雪花算法的时候,也是会走到这个方法


// com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog(), false);
return stmt == null ? 0 : handler.update(stmt);
} finally {
closeStatement(stmt);
}
}

但是不同的是,执行完这一行之后,实体类的id字段就已经赋值上了


StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);

image.png


继续往下跟进


// org.apache.ibatis.session.Configuration#newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

// org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch (ms.getStatementType()) {
// ...
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
// ...
}

}

最后跟进到一个构造器,会有一个processParameter的方法


// com.baomidou.mybatisplus.core.MybatisParameterHandler#MybatisParameterHandler
public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.mappedStatement = mappedStatement;
this.boundSql = boundSql;
this.configuration = mappedStatement.getConfiguration();
this.sqlCommandType = mappedStatement.getSqlCommandType();
this.parameterObject = processParameter(parameter);
}

在这个方法里面会去增强参数


// com.baomidou.mybatisplus.core.MybatisParameterHandler#processParameter
public Object processParameter(Object parameter) {
/* 只处理插入或更新操作 */
if (parameter != null
&& (SqlCommandType.INSERT == this.sqlCommandType || SqlCommandType.UPDATE == this.sqlCommandType)) {
//检查 parameterObject
if (ReflectionKit.isPrimitiveOrWrapper(parameter.getClass())
|| parameter.getClass() == String.class) {
return parameter;
}
Collection<Object> parameters = getParameters(parameter);
if (null != parameters) {
parameters.forEach(this::process);
} else {
process(parameter);
}
}
return parameter;
}

// com.baomidou.mybatisplus.core.MybatisParameterHandler#process
private void process(Object parameter) {
if (parameter != null) {
TableInfo tableInfo = null;
Object entity = parameter;
if (parameter instanceof Map) {
Map<?, ?> map = (Map<?, ?>) parameter;
if (map.containsKey(Constants.ENTITY)) {
Object et = map.get(Constants.ENTITY);
if (et != null) {
entity = et;
tableInfo = TableInfoHelper.getTableInfo(entity.getClass());
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
}
if (tableInfo != null) {
//到这里就应该转换到实体参数对象了,因为填充和ID处理都是争对实体对象处理的,不用传递原参数对象下去.
MetaObject metaObject = this.configuration.newMetaObject(entity);
if (SqlCommandType.INSERT == this.sqlCommandType) {
populateKeys(tableInfo, metaObject, entity);
insertFill(metaObject, tableInfo);
} else {
updateFill(metaObject, tableInfo);
}
}
}
}

最终生成id并赋值的操作是在populateKeys中


// com.baomidou.mybatisplus.core.MybatisParameterHandler#populateKeys
protected void populateKeys(TableInfo tableInfo, MetaObject metaObject, Object entity) {
final IdType idType = tableInfo.getIdType();
final String keyProperty = tableInfo.getKeyProperty();
if (StringUtils.isNotBlank(keyProperty) && null != idType && idType.getKey() >= 3) {
final IdentifierGenerator identifierGenerator = GlobalConfigUtils.getGlobalConfig(this.configuration).getIdentifierGenerator();
Object idValue = metaObject.getValue(keyProperty);
if (StringUtils.checkValNull(idValue)) {
if (idType.getKey() == IdType.ASSIGN_ID.getKey()) {
if (Number.class.isAssignableFrom(tableInfo.getKeyType())) {
metaObject.setValue(keyProperty, identifierGenerator.nextId(entity));
} else {
metaObject.setValue(keyProperty, identifierGenerator.nextId(entity).toString());
}
} else if (idType.getKey() == IdType.ASSIGN_UUID.getKey()) {
metaObject.setValue(keyProperty, identifierGenerator.nextUUID(entity));
}
}
}
}

在tableInfo中可以得知Id的类型


如果是雪花算法类型,那么生成雪花id;UUID同理


image.png


总结


insert之后,id被赋值到实体类的时机要根据具体情况具体讨论:


如果是自增类型的id,那么要在插入数据库完成之后,在ResultSet的ByteArrayRow中获取到这个id


如果是雪花算法id,那么在在插入数据库之前,会通过参数增强的方式,提前生成一个雪花id,然后赋值给实体类


作者:我爱果汁
来源:juejin.cn/post/7319541656399102002
收起阅读 »

我是如何把个人网站首屏加载时间从18秒优化到5秒的

web
起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!服务器配置CPU:1核,内存:2GiB,带宽:1Mbps这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?...
继续阅读 »

起因是这样的,自己做了一个网站,开发的时候好好的,部署到服务器上去后,打开的时候白屏了好长时间才展示内容, 这可不能忍,必须找出原因优化掉!

服务器配置

CPU:1核,内存:2GiB,带宽:1Mbps

这上来就找到原因了啊,这配置这么低,肯定慢啊,怎么办?

换!!!

然而贫穷像是一万多条无形的枷锁束缚住了我,让我换服务器的双手动弹不得。

此路不通,只能另寻他法解决了。

优化前首屏加载测试

测试结果分析

  1. 从截图可以看到,首屏加载耗时19.15秒,主要是chunk-vendors.2daba5b2.js这个文件加载耗时最长,为17.6秒,大小为1.9M,其他文件均在4秒内加载完毕。通常页面加载的一个文件大小超过300k,已经算比较大了。第二个比较耗时的文件是chunk-vendors.62bee483.css,这个应该是样式文件。其他的文件加载耗时都不超过1秒,所以后面优化先从那两个文件下手。
  2. 重新编译项目,看下项目生成的文件

可以看到前面提到的两个文件比较大,后面列出了每个文件使用gz压缩后的大小,但是浏览器实际并没有加载压缩后的文件,而是原始文件。再打开打包文件夹,发现实际生成的js文件夹中除了js文件,还有js.map文件,js.map文件通常用于开发环境调试用,方便我们查找错误,在生成环境是不需要用到的,而且都比较大,这也是一个优化的点。

分析项目依赖情况

运行vue ui,编译查看chunk-vendors中的结构发现,主要是element-ui依赖比较大,其次是vue和mavon-editor

整个项目的情况如下

那么如何优化呢

开启nginx压缩配置

修改nginx配置,启用gzip压缩

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

测试页面加载时间缩短到5.2秒,chunk-vendors.js传输大小为556k,加载时间为4秒,其他文件加载时间基本不超过200毫秒

生产配置不生成js.map

修改项目根目录中vue.config.js配置文件,设置productionSourceMap: false

module.exports = {
runtimeCompiler: true,
productionSourceMap: false
}

打包测试文件夹大小由9.1M减小到2.26M

配置gzip压缩插件

执行npm i compression-webpack-plugin@5.0.1 -D安装插件,在vue.config.js中修改打包配置

const CompressionPlugin = require("compression-webpack-plugin");
const productionGzipExt = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
module.exports = {
runtimeCompiler: true,

productionSourceMap: false,

configureWebpack: () => {
if (process.env.NODE_ENV === "production") {
return {
plugins: [
new CompressionPlugin({
filename: "[path].gz[query]",
algorithm: "gzip",
test: productionGzipExt,
threshold: 1024, // 大于1024字节的资源才处理
minRatio: 0.8, // 压缩率要高于80%
deleteOriginalAssets: false, // 删除原始资源,如果不支持gzip压缩的浏览器无法正常加载则关闭此项
}),
],
};
}
},
};

插件需要指定版本,最新版本的会报错这个和nginx压缩配置感觉重复了,实际测试和nginx压缩配置的速度差不多,如果两个压缩都有,速度并没有提升

修改elementui组件按需引入

  1. 执行npm install babel-plugin-component -D安装 babel-plugin-component2. 修改.babelrc内容如下:
{
"presets": [["@babel/preset-env", { "modules": false}]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
  1. 在main.js中引入需要用到的组件,示例如下:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "element-ui/lib/theme-chalk/index.css";
import mavonEditor from "mavon-editor";
import "mavon-editor/dist/css/index.css";
import axios from "axios";
import {
Avatar,
Button,
Container,
DatePicker,
Dialog,
Dropdown,
DropdownItem,
DropdownMenu,
Footer,
Form,
FormItem,
Header,
Image,
Input,
Main,
Message,
MessageBox,
Notification,
Option,
Select,
Table,
TableColumn,
TabPane,
Tabs,
Timeline,
TimelineItem,
} from "element-ui";

Vue.use(Button);
Vue.use(Dialog);
Vue.use(Dropdown);
Vue.use(DropdownMenu);
Vue.use(DropdownItem);
Vue.use(Input);
Vue.use(Select);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(DatePicker);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Tabs);
Vue.use(TabPane);
Vue.use(Header);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Timeline);
Vue.use(TimelineItem);
Vue.use(Image);
Vue.use(Avatar);
Vue.use(Container);
Vue.use(Option);
Vue.use(mavonEditor);
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$axios = axios;
Vue.config.productionTip = false;

axios.interceptors.request.use(
(config) => {
config.url = "/api/" + config.url;
config.headers.token = sessionStorage.getItem("identityId");
return config;
},
(error) => {
return Promise.reject(error);
}
);

axios.interceptors.response.use(
(response) => {
if (response.data && response.data.exceptionCode) {
const exceptionType = response.data.exceptionType;
Notification({ title: response.data.exceptionMessage, type: exceptionType.toLowerCase() });
return Promise.reject(response.data);
}
return response;
},
(error) => {
return Promise.reject(error);
}
);

new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");

修改按需引入后elementui依赖大小约为1.3M

修改组件局部引入为异步组件

在一个组件中引入其他组件时使用异步的方式引入,如

export default {
components: {
register: () => import('./views/Register.vue'),
login: () => import('./views/Login.vue')
}
};

完成后此时chunk-vendors.js这个文件已经从优化前的1.9M缩小到890k

页面加载约3秒可以显示出来,其他资源在页面显示后继续后台加载,全部加载完总耗时约5秒,请求数68次

组件按组分块

使用命名chunk语法webpackChunkName: "块名"将某个路由下的组件打包在同一个异步块中,如

import Vue from "vue";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
{
path: "/",
redirect: 'home'
},
{
path: '/home',
component: () => import(/* webpackChunkName: "home-page" */ '../views/Home.vue')
},
{
path: '/documents',
component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentList.vue')
},
{
path: '/documentcontent',
component: () => import(/* webpackChunkName: "home-page" */ '../views/documents/DocumentContent.vue')
},
{
path: '/write',
component: () => import(/* webpackChunkName: "home-page" */ '../views/WriteMarkdown.vue')
},
{
path: '/about',
component: () => import(/* webpackChunkName: "home-page" */ '../views/About.vue')
},
{
path: '/management',
component: () => import(/* webpackChunkName: "management" */ '../views/management/Management.vue'),
children: [
{ path: '', component: () => import(/* webpackChunkName: "management" */ '../views/management/ManagementOptions.vue') },
{ path: 'developplan', component: () => import(/* webpackChunkName: "management" */ '../views/management/DevelopmentPlan.vue') },
{ path: 'tags', component: () => import(/* webpackChunkName: "management" */ '../views/management/TagsManage.vue') },
{ path: 'documents', component: () => import(/* webpackChunkName: "management" */ '../views/management/DocumentsManage.vue') }
]
},
{
path: '/games',
component: () => import(/* webpackChunkName: "games" */ '../views/games/Games.vue'),
children: [
{ path: '', component: () => import(/* webpackChunkName: "games" */ '../views/games/GameList.vue') },
{ path: 'minesweeper', component: () => import(/* webpackChunkName: "games" */ '../views/games/minesweeper/MineSweeper.vue') }
]
},
{
path: '/tools',
component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsView.vue'),
children: [
{ path: '', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ToolsList.vue') },
{ path: 'imageconverter', component: () => import(/* webpackChunkName: "tools" */ '../views/tools/ImageConverter.vue') }
]
}
];

const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});

export default router;

打包编译后文件比之前要减少了一部分,并且合并后的文件资源也不大,完全可以接受

页面加载耗时基本没变,但是请求数减少到51次

总结

  1. nginx压缩对性能的提升最大,通过压缩文件缩短资源加载时间
  2. gzip压缩插件会将文件压缩成gz格式,暂时不知道怎么用
  3. elementui按需引入会减小依赖资源的大小,chunk-vendors.js文件体积会减小
  4. 使用异步组件可以在后台加载渲染不到的资源,优先加载渲染需要的资源,缩短页面响应时间,但同时会增加打包后的文件数量,导致页面请求数量增加。
  5. 组件按路由分组,打包的时候会将相同组名的资源打包到一个文件中,可以减小请求数


作者:宠老婆的程序员
来源:juejin.cn/post/7351292656633331747
收起阅读 »

采访:中年程序猿图鉴

程序员群体曾是低调多金的代表,但最近996话题、甲骨文大裁员等事件持续发酵,让这个群体成了大众眼中的“失意中年人”。年轻时的拼命,换来的却是中年时的焦虑。收入虽高,但前途摇摆。30岁真的是程序员迈不过去的坎吗?曾经梦想着用技术改变世界的程序员们,又是如何看待自...
继续阅读 »

程序员群体曾是低调多金的代表,但最近996话题、甲骨文大裁员等事件持续发酵,让这个群体成了大众眼中的“失意中年人”。

年轻时的拼命,换来的却是中年时的焦虑。收入虽高,但前途摇摆。

30岁真的是程序员迈不过去的坎吗?曾经梦想着用技术改变世界的程序员们,又是如何看待自己的职业规划和人生价值?

穿越喧嚣,我们采访了12位中年程序员,听听他们的故事和人生。

要点速览

• 我们被固定在“敲代码”的坑里,一干就是10年,再干别的早已不会。敲代码已经成了一项流水线般的工作,就像搬砖工一样。

• 公司把有创造性的事情全部标准化,每个人负责一部分,还会安排几个人“备份”,每个人随时能被替代,我没有一点安全感。

• 这个行业根本不存在吃青春饭这一说,关键是40岁就要干40岁该干的活,35岁就要干35岁该干的活,你不能35岁还在干30岁干的活。

• 加班和掉头发是肯定的,不敢天天洗头,生怕哪天秃顶。

• 我来谷歌快三年,只有一次是真正为了赶进度加班到晚上12点。不过,硅谷的创业公司很羡慕国内的刻苦劲儿,因为对初创公司来说,真的是效率决定生死。

• 不论是什么技术,只会把低端的程序员消灭掉。同样一个东西,普通的程序员和有爱好的程序员都能实现,但是有爱好的会把它实现得更好。

• 老一代程序员喜欢亲力亲为,现在的一代多是拿来主义。我们老一辈是木工,喜欢从木头做成家具,现在的年轻人像宜家,买来现成的再自己搭。新兴程序员效率更高,我们这一辈更能追根溯源。

• 年龄越来越大,身体确实有点吃不消,上个月后背上还起了一大片带状疱疹。就算如此,我从没想过换行业,我会做一辈子程序员,这是一个有技术含量,让我愿意一直打怪升级的工作!

null

从业十年,从“工程师”变“码农”

Luke 33岁 入行10年

北京 游戏行业

10年前我入行时,整个行业一片欣欣向荣,那时候老板更喜欢称我们为“工程师”,但是现在,我们已经成为“码农”了。

之所以有这个变化,一个是因为工种越来越细化,每个程序员负责的任务越来越精细、单一,时间长了,我们只熟悉那一个模块的工作;另外一个,是因为我们自身的知识结构越来越跟不上新技术的需求。

软件行业的开发模式,是对一个框架的修改和堆砌。说得更贴切一点,就是堆积木。只要掌握了编程技能,一个程序员每天的工作几乎就是从开源网站上扒一段程序,然后根据公司需要不断在框架上添加、修改。程序是24小时不间断运行的,我们在开发和维护程序的时候,每天都需要加班到很晚,熬夜是常态,这真是一个体力活儿。

null
时间长了,我们就被固定在一个“敲代码”的坑里,一干就是10年。这时候,再干别的早已不会了,敲代码已经成了一项流水线般的工作,甚至不用动脑子就能完成,就像搬砖工一样。

出卖力气,就对身体有很高的要求。对于我们30多岁的程序员来说,已经熬不了夜,思维已经基本固定,但为了养家糊口,要求的工资却越来越高。

我们长期固定在一个岗位已经形成惯性,上不去也下不来,但这不是我们自愿的,而是对我们的一种摧残。

在这种压力下,我们也习惯了996的工作节奏。有些时候即使早早做完了工作,看着别人不下班,我们也会拖一拖,至少在老板看来这样比较敬业。

两年前我曾想过跳出这样的循环,可是当我从原单位离职后,发现再去别的岗位已经成了一个零基础的新人,而且工资比之前还低。考虑了两个月,我又回到了程序员的岗位上。

我现在最大的想法就是干好当前的工作,趁着还有机会拓展自己,延伸自己的技能。以后如果跳出这个行业,不会被技能限制。

null

有时候很羡慕年轻同事,说辞职就辞职

Bruce 34岁 入行7年

成都 手机厂商

我们团队15个人,平均年龄35岁以下,只有三个人比我年纪大。

我们公司是出了名的用人“狠”,一方面,公司每年会招很多新人,减少用人成本,让我们这些领着高薪的老员工瑟瑟发抖。另一方面,为了保持稳定,公司会把有创造性的事情全部标准化,把任务分得很细,每个人负责一部分,还会安排几个人“备份”,这就让我们没有不可替代性,自然也没有安全感。

做程序员要么是一直写代码,要么是往上走,做整体规划。如果工作几年还没有升职,确实会很有危机感,我身边合同到了不续签的情况很常见,小组的leader也是随时可以被换掉,或者整个小组被分流。

在这种压力下,我的工作时长远超996,没有时间学习新东西提升自己,更别说健身追剧。 虽然看起来我的外表和90后没什么区别,但我已经是两个孩子的爸爸了,要挣钱,只能花时间去换。

null

有时候很羡慕一些年轻的同事,觉得劳动强度大,说走就走。有的换到国企,还有的转行去做销售,听说有技术背景的销售挺吃香。我也不是没想过换工作,但这份工作离家近,收入也不错,还是先干着吧。

有人说程序员越老越不值钱,我觉得每个行业都有自己的问题,医生是越老越值钱,但要花大半辈子爬坡,也不是每个人都能忍受。

我其实还没有想清楚未来该做什么,有时候跟同事开玩笑说,成都的小吃多,大不了以后去开个店卖猪头肉。

好了,我今天讲的话算是很多了,现在要回去加班了。

null

如果35岁了,你根本get不到年轻人的点

杨光 36岁 从业14年

湖南 游戏行业

我2004年入行,毕业进入了一家在数据库这方面仅次于Oracle的外企。当时的待遇还是很好的,一个月工资有5000元,工作965,非常舒服。

在和我同一批次进入的人里,我学历很一般,他们都是清华、北大、科大的,但我技术很好,大三的时候就做过几个数据库的项目,这是我当时能进入这家外企的根本原因。

也因为这样,做了半年之后,我觉得没什么意思,也没什么增长空间了。因为公司够大,所有的程序我们只能修改,不能做大改变,在我看来这没什么技术含量。

而且,外企流程特别多。比如有一次我改一个差不多40多行的代码,对我来说非常简单,不到一天就能改完,但我花了一周的时间,大部分都用在了协调和沟通上,这跟我的性格不太相符。

于是,我选择离开外企,到联众做棋牌游戏,也是这个时候我正式步入游戏圈。2008年,做了一款知名的游戏后,我开始从单纯的技术人员向管理人员过渡,在游戏行业创过几次业,但2018年游戏行业骤冷,我开始深度思考这个行业。

我发现这个行业特别喜欢年轻人,因为你的客户是14岁到28岁的人,你如果35岁了,你根本get不到他们的点,这就跟拍电视剧一样,你让陈凯歌去拍小鲜肉,他拍不出来。

这个行业变化也挺快的,比如今年流行传奇类的,明年可能是养成类的,你怎么让一个天天打打杀杀的程序员去做温情的养成类游戏呢?他没有感觉的,而一旦年龄大了,学习能力也不如以前。

null

更重要的是,很多游戏公司的领导比较年轻,没人喜欢招一个比自己大的程序员,我 自己创业招人的时候也是更喜欢年轻人 ,我培养1个月就能上手了,而且性价比高。更何况,年轻人可以周六日在公司加班,年龄大的就得回家看孩子。

所以,去年我35岁的时候,选择离开游戏行业。我现在开始做医疗了,我的主要客户是35岁往上的,岁数越大,我对这些人的心态就越了解。

年轻的时候,我们非常崇拜求伯君、比尔盖茨、麦克戴尔这样的人,也希望可以靠技术起家,但现在已经没有这个机会了。

null

写代码谁都会,最牛的人是要做判断

田真 30岁 入行4年

北京 互联网公司程序员

在这个行业4年了,35岁的焦虑每年都会炒一次,说得好像35岁的程序员就不用活了一样。

但真实的情况不是这样。

以我个人为例,因为入行的时间有限,我目前的想法是希望自己在业务上不断锻炼,争取到了35岁有带队伍的能力,不仅仅是干一线的工作。这并不是说一线的工作不重要,是还有很多工作需要更复杂的精力去解决。退一步讲,不管是什么行业,如果你做了很多年还在一线,那基本上你有点儿不太上进。

null
刚刚参加工作的时候,我掌握了一个工具、搭建了一个框架、学习了一个语言就会觉得很满足。但是以后我肯定不会这么觉得。 就好比每次大的项目,最牛的人决定用什么框架、什么语言、什么工具,这决定整个项目的起点。  如果椅子搭好的话,后面会省很多事儿,如果没搭好,后面的麻烦就越来越多,搞不好会浪费时间和精力。

所以在一线工作时要慢慢积累这种经验,到最后慢慢具备这种能力。这是比简单写代码更重要的能力, 写代码谁都会,但不是谁都可以在重要的事情上做出重要的判断。

这个行业根本不存在吃青春饭这一说,我40岁的程序员朋友还从百度跳到阿里去了呢。关键就是40岁就要干40岁该干的活,35岁就要干35岁该干的活,你不能35岁还在干30岁干的活。

公司肯定是需要35岁以上的程序员的,但肯定不是35岁的一线工作人员。

null

硅谷的年龄危机远没有国内强烈

Joey 31岁

美国 谷歌软件工程师

在硅谷,没有公司会把年龄限制写进招聘信息里。拿谷歌来说,团队的年龄跨度很大,二十几岁的应届生和四十多岁的博士都很常见。但招人的时候,如果求职者年龄大,HR的期望值相应也会更高,除了问业务问题,还会考察这个人的格局。

在这里,程序员并不算是“高压”职业。时间灵活,看重效率,没有打卡一说,只要按时完成工作就好。我一般是上午9点半工作到晚上6点半,周末双休陪家人或是去攀岩。有时候因为想把手头的事尽快做完,我会主动加班几小时,但 来谷歌快三年,只有一次是真正为了赶进度加班到晚上12点。  和国内的996兄弟们相比,真的有点“惭愧”。

正常情况下,工程师工作七八年就会带团队了。当然也有一些人喜欢纯做技术,选择当技术大牛也没问题,一样受人尊重。总体来说, 大家的年龄危机和竞争氛围没有国内那么激烈。

null

一方面,虽然谷歌也像其他大公司一样会把任务拆解得很细,每个人负责一部分,但是谷歌的文化比较自由,老板不会事无巨细指挥你怎么做,而是给出空间发挥每个人的创造性。公司内部也鼓励流动,大家经常换组,尝试不同的方向和产品,在这个过程中能够接触新东西,不会觉得自己是在机械的重复劳动中慢慢变老。这一点可能和国内公司不太一样。

另一方面,我们的节奏没有那么快,工程师有大量时间可以学习新东西。这也是我觉得996不是一个好机制的原因,一个人的时间全部被工作填满,如果还做的是重复性的工作,很难有时间提升自己,长期来看很容易被淘汰。

我也会焦虑,但不是来自于年龄,而是关于职业成长。硅谷大部分人都拼命想学新东西,我是数学背景,如果在现在的组里不能把我的价值发挥到最大,我也会考虑换个环境。我一直提醒自己,要关注更长期的成长,个人价值才会一直上升。

都说程序员是吃青春饭,这句话暗含的意思是年纪大的程序员加班拼不过年轻人。 加不了班就没有价值?这不成立。  年纪大的程序员胜在经验,如果真的要靠熬时长,只能说明他干的活儿技术含量还不够。

我很崇尚个人奋斗,但不是砸时间才叫奋斗。 不过,硅谷的创业公司倒是很羡慕国内的刻苦劲儿,因为对初创公司来说,真的是效率决定生死。

null

特别愿意招经验丰富的大龄程序员

何建 38岁 入行16年

北京 零售电商行业

程序员大部分时间都比较忙,一忙就没有太多时间停下来思考。不深度思考,就认识不到怎么培养自己的核心竞争力。即便一个人有20年的工作经验,他的能力也是有边界的。如果本身是个安于现状的人,可能会面临中年危机。

对于被辞掉的外企员工,可能难融入国内互联网公司文化,这一点也有客观因素。国内的互联网公司做产品,可能三个月就得出一个版本,甚至再过一个月,做两个版本迭代都有可能。外企在速度上做不到,流程也极其复杂。

null

2000年-2012年这个阶段,技术的更新换代特别快,到2012年之后,技术圈大的革新已经没有了。包括现在的AI,整个技术还不是特别成熟,更多是在应用层面的落地。技术没有更新,或者没之前更新那么快,程序员们更容易陷入安逸。

我自己没有中年危机,但技术行业确实存在“年龄歧视”,包括我自己招人。我周围都是90后程序员,但我的偏见没有那么深。假如你是85年的,已经有十年工作经验,我会侧重考虑你的工作年限和实际经验是否匹配。

很多人工作了十年,还不如工作五年的,很可能是一路混过来的。 但反过来,年龄偏大,学习能力强,经验又足够丰富,我特别愿意找这种人,尤其是在工作之余兼职创业的。  这类人眼界开阔,知道创业路上可能有哪些坑,技术基础扎实的人写的代码质量也高,不需要太多额外人员为他服务。

所以说,年龄并不是决定性因素,还是要看这个人本身。

null

真正的常青树公司不会大招大裁

张军 35岁 入行10年

上海 蒹葭(嘉善)电子商务有限公司

这个行业的人才供应始终是冰火两重天的,高端人才稀缺难得,低端人才供应泛滥。 但真正有底蕴的常青树公司是不会大招大裁的,宁可提高门槛制造俱乐部效应。

我研究生毕业以后就加入了一线互联网公司,先后在百度等公司就职,现在也成了一名创业者。随之而来的节奏和眼界的变化也很清晰,在大公司工作,只能看到一个拼图的一小块,但在创业公司,每个人都要是超人,从开发到运维一肩挑,还要参与商业化,更实用主义。

大公司有严格的开发流程,从总体设计到详细设计、编码阶段、提测,然后交给运维上线,中间要花2到3周,甚至是2到3个月,初创公司没办法这么讲究,可能头天拿到需求,第二天就敢上线。

大环境一直在变,唯一不变的只有变化。所以最近几年,我必须保持学习新东西的状态,要说瓶颈的话,在于技术人转管理岗,适应起来时间会比较长,所谓“慈不掌兵,义不行贾”,打工者视角切换为leader视角,自己的性格会遇到新挑战。

我们部门平均年龄大概26岁,年龄代际必然会造成差异,但总的来说问题不大。年龄大带给我的优势就是经验的不断沉淀, 在老技术人眼里,没有多少真正的新东西,都是新瓶装老酒。

年龄本身不会给我带来危机感,带来危机感的是经济周期、行业周期、岗位需要的投入度与自身能够提供的投入度的差异。

null

干了这一行之后,基本没有上下班之分,只有醒着和睡着的区分。 坦率的讲,业界对程序员发迹线的消费是有悖科学精神的,秃不秃取决于基因。  头发掉得厉害的人,可能祖上有一些贵族基因,就像英国的查尔斯王子家族一样。很遗憾,我发际线至今还行。

我平时加班之后会去夜跑,一周三次,能够给我提供一个独立思考时机,整个公园很安静,感觉很好。

互联网人是持续学习者,持续奔跑者。目前我只实现了人生规划里的一部分,创业的野心一旦打开,就会一直在这条路上狂奔下去。

null

我们编程像木工,新兴程序员像宜家

孟誉国 41岁 从业16年

上海 ERP软件公司CEO

软件行业相对来说比较看经验,3-5年之后可能就完全换另外一套东西了。新的工具不会用,一脱节就很难跟上去。比如之前说打算盘,打得再好再快,计算器一出来经验就没用了。

与其等到人家觉得我不行,不如我自己早一点,在一个新旧替换的时代里主动选择创业,掌握主动权,然后做自己更想做也更有价值的事。

我现在自己会带小朋友,最明显的感觉是,老一代程序员喜欢亲力亲为,现在的一代多是拿来主义。 打个比方,我们老一辈是木工,喜欢自己从木头做成家具,现在的年轻人有点像宜家,买来现成的再自己搭 。新兴的程序员效率会更高,我们这一辈更能追根溯源,相辅相成吧。

年龄还有一个优势,可以让你更加冷静沉稳,不应该犯的错误会少犯一些。前几天有一个朋友让我去用黑客的方式,删掉别的管理系统里面的一个订单,早20年炫技也好要面子也好,我可能直接愿意帮忙,但现在我会跟他说:“技术上我肯定搞得定,但是我真的不能帮你。”

null

我一直比较乐天,心态比较年轻,之前上班的时候,我是公司请假最多的人。我比较喜欢旅游、摄影、做视频,玩吉他和电子音乐合成器,会参加一些公益拍摄活动,每年去海边潜水几次。

其实我也后悔,年轻的时候还是玩多了。那时候虽然也自学了不少东西,边查资料边摸索,学得特别快,但我觉得可以学得更多。我现在还在研究单片机,买了好多单片机的板和学习资料,但进度明显慢下来了。

不论是什么技术,只会把低端的程序员消灭掉。  我觉得程序员一定要热爱编程,完成了一个作品会觉得开心,而不是听说这行工资高就去临时速成甚至改行。招人的时候我也会碰到很多培训六个月就出来做程序员的人,我一般不太会去选这种。

同样一个东西,普通的程序员和有爱好的程序员都能实现,但是有爱好的会把它实现得更好。

null

程序员的职业方向也得赶风口

yanyan 31岁 从业6年

北京 手机厂商研发

35岁以上还在做一线程序员的情况其实挺多。

“程序员行业吃的是青春饭”这个说法,要分情况来看。如果在技术方面积累比较好,其实35岁以上的程序员还是比较吃香。但是如果一直写基础代码,没有比较深的技术积累,一直做到35岁非常危险。

null

甲骨文不是有个绰号叫中关村最大的外企养老机构吗? 一些被裁的年龄比较大的外企程序员虽然拿到的补偿会比较多,但他们最害怕找不到下家。  一是工作方向和工作强度跟国内程序员没法竞争,二是如果35岁以上还是纯基层的研发或者写代码的程序员,很难找到和原先岗位匹配的工作。

按照一般的职业发展路径,程序员可以发展成为某一个领域的技术专家,对标阿里的P7、P8,或者是在某一个技术方向上成为资深顾问,另一个方向就是晋升到管理层,负责项目开发或整个技术架构。这个方向不仅需要擅长技术,还要懂项目管理。

程序员行业的职业方向也是得赶风口,几年前头条、抖音发展起来,APP开发是热门方向。最近几年就是人工智能、自动驾驶最火,算法工程师和人工智能开发工程师这方面的岗位比较多。

我自己在职业方向上有时候会焦虑,但是这个行业本来就需要不断学习新东西,不然被淘汰的概率会比较大。如果再过几年,职位和收入达不到一个比较可观的情况,或者晋升的可能比较小,我可能会更焦虑,会怀疑自己是不是方向错了,或者考虑新的机会。

null

技术大佬从来不需要考虑年龄问题

Jay 32岁 入行7年

北京 电商平台后台开发

甲骨文裁员的消息,我关注过。互联网寒冬,哪个公司裁员都不稀奇,我们公司也有裁员。程序员一直是个比较容易焦虑的职业,尤其是技术没能随着年龄增长成正比的成长的话,更容易焦虑。

对大部分底层码农来说,程序员就是青春饭。HR或猎头找你,也会因为年龄问题拒绝你,我身边就有同事遇到。 但是技术大佬从来都不需要考虑年龄问题。

我们部门同事的平均年龄在30岁左右,年轻人比例不算大,年龄差异肯定是存在,毕竟大家的生活和成长经历摆在那。 我们的工作节奏比较紧凑,国内互联网公司不加班的应该不多。 掉头发也是肯定的,都不敢天天洗头,生怕哪天秃顶了。

null

如果单看收入,目前还算满意。毕竟程序员起薪就不错,不管是在什么城市,肯定都是高于当地平均工资不少。但是如果将工资和工作强度、消费水平放在一起看,那就性价比太低。

到目前为止,我还没换过工作。之前有拿过其他公司offer,后悔拒绝了。

null

庆幸当初入行早,让我现在衣食无忧

Nick 37岁 入行13年

深圳 台资公司研发助理

我已经过了35岁,但是我心态很好。

其实不是所有程序员都要累死累活的加班。  那些做产品研发的,有明确的上线时间,压力会非常大,加班也很多,如果年纪大了加不动班,当然会有危险。但像我们公司,主要是帮客户解决使用我们产品过程中遇到的问题,每年只有客户产品需要量产的5个月会忙一些,其他时间工作强度不算大。大家朝九晚六,周末双休,还能每周组织一次羽毛球比赛。

我们团队平均年龄30岁左右,但我观察,那些40岁左右的人,还是待得挺舒服。一是因为公司管理人性化,不会纯考核绩效,人员稳定。另一方面是相比于新人,老员工对公司的每一代产品更加熟悉,知道以往的产品有哪些局限、迭代的新品做了哪些更新。让新人发愁的问题,老员工可能早就经历过,能迅速能做出判断。

null

这几年,我的工作内容变化不是很大,但是圈子里的新技术是肯定要去学的。我身边甚至有朋友跨界开了处理芯片的加工厂, 我没有想要去外面折腾,这可能是因为我入行早,之前积累的收入已经足够我安居乐业,也就没有那么多动力去折腾了。

程序员的收入还是属于中等偏上,但以现在的消费水平和深圳的房价来说,入行早晚差异很大。我算是赶上了一个尾巴,所以还是要感谢当初选择这个职业,让我衣食无忧。

null

我愿意做一辈子程序员升级打怪

老铁 38岁 从业15年

北京 安全科技公司架构师

我们这行明明是“越老越吃香”, 我们有丰富的经验,是一名架构师,而不是普通的编码者。

我相信那些被甲骨文裁掉的员工也会被BAT等公司抢着要,当然,不包括少部分在大公司“养老”的人。确实有部分人在舒适区待了很多年,他们可能有十年的工作经历,却只有一年的工作经验,只是从“小白兔”熬成了“老白兔”,肯定要被时代淘汰。

这一行的更新迭代速度太快,我30岁以后,确实认真考虑过未来要如何发展,去报班考pmp(项目管理专业人士资格认证),挤出时间去上课,不断学习可以抵抗焦虑。

null

其实我们工作节奏还好,没有外界说的996那么夸张,以我们的任务量,只要在工作日每天非常专注、高效地工作6个小时就可以完成,很多人只是效率太低,拖到太晚。

但可能是年龄越来越大,最近身体确实有点吃不消了。上个月我后背上起了一大片带状疱疹,医生说是压力太大,导致免疫力低下。

就算如此,我从没想过换行业,我想我会做一辈子程序员,这是一个有技术含量,让我愿意一直打怪升级的工作!

我现在的阶段性目标是成长为一名CTO,或者是安全领域的技术专家,能够带领超过百人的团队完成项目。

这就好比登山,在你坚持爬到崖口,看到一片没有遮挡的蓝天时,你会发现一切都很值得。

*文中部分图片来源于视觉中国。应受访者要求,文中杨光、田真、张军、老铁为化名。


作者:燃财经编辑部
来源:mp.weixin.qq.com/s/5Cw-NzxjsRF2BwdSdGFzBQ
收起阅读 »

AI 搜索的价值在哪里

借鉴开源 Lepton Search 的灵感,在公司内部做了一款 AI 搜索工具,名为爱搜。这个工具目前处于带着做状态,没有投入什么人力和资源。遂想写点东西,记录下自己的一些想法和观点。不一定对,但都是吾之所悟。AI 搜索是什么AI 搜索是指利用人工智能技术,...
继续阅读 »

借鉴开源 Lepton Search 的灵感,在公司内部做了一款 AI 搜索工具,名为爱搜。这个工具目前处于带着做状态,没有投入什么人力和资源。遂想写点东西,记录下自己的一些想法和观点。不一定对,但都是吾之所悟。

AI 搜索是什么

AI 搜索是指利用人工智能技术,帮助用户更快找到需要的信息,提供更加精准和相关的搜索结果。

为什么要做 AI 搜索

  1. 现在 AI 是风口,所有产品前缀都可以加上 AI,搜索也不例外
  2. 人工智能可以帮人类承担一些搜索工作,之前人类需要在搜索上花一个小时,现在有了 AI ,只需要花 20 分钟甚至更少

怎么做 AI 搜索

从现在看,做出一个简单的 AI 搜索产品已经不存在技术难点了,有很多成熟的产品,如:

  • 国内:360AI 搜索、秘塔、天工等,还有一些内置到问答产品中,如 kimi
  • 国外:devv 、perplexity 等

下面我将从技术架构、应用层、接口层、模块层来阐述怎么做 AI 搜索产品。

技术架构

下图是我画的简单 AI 搜索产品架构示意图:

image.png

上图把架构分成了三层,分别是应用层、接口层和模块层,解释如下:

  • 应用层:可以是 web、native、桌面端、浏览器插件、sdk
  • 接口层:支持应用层的各种 api
  • 模块层:是搜索和 各种 agent 的核心实现

这应该是最简单的 AI 搜索架构了,复杂的我没有做过,就不画了。

应用层

目前一些 AI 搜索产品我都用过,直接参考秘塔、devv 和 perplexity 即可,三者页面如下图所示:

resized_image-2.png

整体布局相似,取他们精华,去他们糟粕就可以了。技术选型上,根据团队情况选择就行,如 vue 、 react。整体没有技术瓶颈,正常去开发实现即可。

接口层

基于 restful api 去和应用层对接,比如有以下接口:

  • 回答接口
  • 相关问题接口
  • 登录接口
  • 历史记录接口
  • 设置接口

这一层,也可以加上缓存功能,对于相同问题,直接返回缓存结果。也可以不加缓存,主要看业务需求。

爱搜接口层和模块层代码的目录结构如下图所示:

resized_image-4.png

使用 go 作为开发语言,整体合理。爱搜提供的接口如下图所示:

resized_image-5.png

除了自己用的接口,还给其他业务提供了一些能力支持。

模块层

这一层属于 AI 搜索的核心了,它能决定 AI 搜索的上限。模块层提供的能力越多,能力越强,产品的竞争力就越大。

上文的架构图画了两个模块:

  • 模块 1:搜索引擎 --> prompt --> 大模型
  • 模块 2:搜索引擎+爬虫 --> prompt --> 大模型

搜索引擎

搜索引擎的方案有两种,分别是付费和开源。如果用付费方案,则有百度、必应、谷歌、serper 等。如果用开源方案,则有 duckduckgo 、searxng 等。

  • 付费方案中,serper 是我认为目前最好的选择,理由是非常便宜、底层走谷歌搜索、速度很快并且国内没有被墙。
  • 开源方案中,我知道的有 searxng 和 duckduckgo ,searxng 更流行。

爬虫

  1. 在不加限制的搜索场景下,没有找到一个合适的爬虫方案,这种场景有两种方案:
  • 第一种方案:用传统的方法,拿到页面链接,然后解析页面内容,这种依赖页面 dom 结构,那么多页面,怎么去实现一个通用的解析逻辑,很难搞
  • 第二种方案:用 AI 能力,借助视觉模型,拿到页面链接,进入页面,对页面做视觉判断,需要用到什么数据,就拿什么数据,这种目前还没有尝试,感觉难度也大
  1. 如果加限制搜索场景,比如编程问题我只在 stackoverflow 、 reddit 、 github 上搜和爬取,这种是可以有合适的方案的。但是执行爬虫后,返回速度是不是会变慢,这个因素也需要考虑。

目前爱搜是没有做爬虫方案的,主要是没有想好怎么做。用过 kimi 的,都知道回答会有资料作为参考,如下图所示:

resized_image-6.png

我比较好奇的是,kimi 有没有爬取资料 url 的页面内容。还是说,只是把调搜索引擎拿到的搜索结果展示出来,或者说,会根据问题有选择的爬取资料页面。

目前用 AI 做爬虫的开源项目也有一些,但到目前为止,我还没有找到一个适合所有搜索场景的爬虫方案。

prompt

prompt 的设计有几个痛点:

大而全的 prompt 很难调

你想靠一个 prompt 解决搜索问题,是几乎不可能的,需要对 prompt 从上到下进行拆分,如下所示:

  • prompt
    • 断言 prompt:判断搜索问题是什么类型
    • 编程 prompt
      • 错误解决
      • 功能实现
      • xxx
    • 非编程 prompt
      • 新闻类
      • 医学
      • xxx

如果想让回答更加符合用户想要的,prompt 的设计就需要考虑原子化。有利于维护、适配和扩展。

很依赖大模型的能力

如果未来的大模型能力比现在强大千倍,那也许一个大而全的 prompt 就够了,但现在,还做不到这种。你设计的一个 prompt 在 X 模型上表现很好,但换到 Y 模型上,表现可能就变差了。

上文将 prompt 从上到下进行拆分,变的小而精,也是为了增加鲁棒性,让其在不同模型上都能有很好的表现效果

prompt 的设计准则太多了

据我了解,有很多提示词设计准则,像 CoT、CO-STAR、3S、微软出的 prompt 设计教程等。给我的感觉就是:到底哪个是最佳实践,估计目前没有最佳实践,这给 prompt 设计,又带来了一些困难,不同模型的 prompt 最佳实践可能不一样,如何在 prompt 上屏蔽掉这个因素,是值得思考的事情,将 prompt 拆小,在一定程度上做了屏蔽。但是也会有无法兼容的情况,这种就需要根据模型来单独设计适合它的 prompt 了。

prompt 也需要后期

有时会发现,在模型固定的情况下,不管你怎么设计 prompt ,某一个场景的输出就是有问题,这个问题大多是指输出不够稳定。

比如一个问题的回答,需要输出字符串数组,这个问题问 10 次,会偶然出现一个输出数字数组,或者直接不是数组,这种情况怎么办,从我的观点看,这种情况就需要做后期处理了,通过写程序去识别这种情况,并做相应的处理,保证返回的永远都是字符串数组。

prompt 自动化测试

prompt 本身不太可控,如何在迭代过程中,做到对 prompt 有一个稳定的监控,这就需要在 prompt 自动测试上做一些能力,比如:

  1. 自动生成各个类别的问题,每个类别生成 10 个问题,
  2. 自动去跑 prompt,每个问题,跑十遍 prompt
  3. 将相同类别的相同问题跑出的结果进行对比,分析结构和内容是否相似
  4. 将相同类别的问题跑出的结果进行对比,分析此类别的输出结果是否稳定、准确

模型

模型的重要性不言而喻,当前模型界应该是最卷的领域了,如何评估和选择模型是一个很重要的事情。就目前来说,模型选对了,产品的成本可能会降一半,效果还会更好。

模型和 prompt 配合

上文 prompt 也阐述了相关内容,模型和 prompt 工程形成良性的循环,是我们必须要去做的事情

私有化模型的挑战

如果不使用第三方模型 api,使用私有化模型,那需要做以下事情:

  1. 评估和选择模型
  2. 模型部署,要买卡,或者走托管服务
  3. 模型微调【可能需要,但如果想更好,大概率需要做】

买卡的话,成本就变大了。模型大小也要考虑,“越大”,需要的算力越多。从控制成本角度看,方向如下:

  1. 采取面向模型开发模式,用合适且性价比高的模型去解决不同的业务场景
  2. 模型倾向于选择 MOE ,在“小”的同时,获得高质量的输出结果
  3. 让 prompt 多发力,再加上后期,也可以让“小”模型的效果逼近“大”模型的效果
  4. 选择正确的微调方案,这里我没有经验,目前业界有预训练、SFT、RLHF、LORA、指令微调等
  5. 模型侧要保证性能和准度,就是输出结果要快和准,相同参数级别模型
    • a:想更快,可以尝试用 bit 更小的量化模型,测试输出效果会不会有明显差别,没有的话,就可以考虑用,这样会提高模型性能
    • b:想更准,需要根据情况做处理,比如做指令微调

AI 搜索商业价值

我先说下,目前 to c 产品的一些价值场景

  • 360:回答页面加了广告...

resized_image-7.png

  • 天工:目前没看到付费场景,但是从我的角度看,天工做的还可以,agent 很多,包括 ai ppt、数据分析等

resized_image-14.png

  • 秘塔:免费版搜索次数有限制,目前没看到上限付费版

resized_image-8.png

  • devv:按月/年付费,可获无限次 agent 使用、gpt-4o 模型等其他付费功能

resized_image-9.png

  • perplexity:按月/年付费,付费功能如下图所示:

resized_image-10.png

从我的角度看,这些 AI 搜索产品,还没有到让我付费的程度。也就说,已经 To C 的产品,我都没有付费的意愿,那在公司内部搞的 AI 搜索工具,如何去落地或者呈现价值呢?

以下有我的几点思考和看法

多在 AI Agent 上发力

AI Agent 概念:即人工智能代理,是一种利用人工智能技术来执行特定任务或服务的软件程序。AI 代理可以模拟人类智能行为,进行自主决策、学习和交互。它们可以应用于多种领域,包括但不限于客户服务、数据分析、自动化任务、个人助手等。AI 代理能够处理复杂的任务,提高效率,减少人为错误,并为用户提供更加个性化和智能化的服务体验。

这里我举一些 Agent 例子:

  1. RSS 订阅自动总结和推送 Agent 对 RSS 订阅有强依赖的用户群体,这个功能就能产生较大的价值
  2. 科技、手机、AI 等主题新闻,最新咨询日报生成和推送 通过 AI 搜索去自动搜索各主题最新新闻并进行阅读,最后输出新闻内容总结和高质量点评,对于提高用户的行业前沿资讯感知是有价值的
  3. 简历分析和评估,上传一个简历,会自动分析简历内容,给出评估报告和面试时需要问的面试问题

当前的 Agent,我更倾向于做一些小而美的 agent,太宏大的 agent,实现起来很困难,一方面受限于技术,一方面也会受限于算力

内网的搜索和总结要做好

  1. 内网的知识库:包含文档、pdf、各类分享视频
  2. 业务相关的文档

可以在搜索页面加一个搜索范围,像 perplexity 那样:

resized_image-13.png

上图显示的 内网->知识库 是我按 f12 改了下 dom 内容。

这些功能,爱搜目前都没做,看起来几句话,实际需要不少工作量。就拿 pdf 解析来说,目前业界对于复杂 pdf 的解析好像都没有太好的方案,我试过很多开源项目,都达不到我的理想需求,最近我又看到一个很不错的开源项目,叫 trieve ,其特性如下图所示:

resized_image-11.png

这个开源项目已经获得 YC 的投资了,证明其还是有技术和潜力的。目前是我看到对 pdf 分块、解析和搜索最好的开源项目了。后续多研究下这个项目。大家有什么好的开源方案也欢迎告知我。

业务相关的文档,做起来难度也大,爱搜目前也没有做,如果做的话,整体思路如下:

业务上可以根据你的登录信息,查你当前拥有的业务权限,然后允许用户选择搜索哪个业务,比如业务 A 所有的项目管理文档,包含策划文档、策划评审意见等,然后对用户选择的业务进行训练和搜索,后续用户可以在业务 A 选项中搜自己想要的内容,并获得相应的回答和索引。

多和公司内部业务联动

比如给某个业务提供联网搜索能力、提供搜索能力、提供爬虫能力等,类似这种多去和内部业务沟通交流,也能发挥落地一些价值

总结

  1. 想一下,bing 和谷歌做 AI 搜索,都被外界喷效果差,就知道要做好 AI 搜索还是很有难度的。
  2. 当然,bing 和谷歌的目标和我们不一样,我们更专注于垂直领域,我希望做小而美的 AI 搜索,它可以是一个产品矩阵,也可以是一个聚合产品
  3. 我们聚焦的是目前世界上最前沿的领域,有困难很正常

商业价值不是靠讨论出来的,而是靠试出来的。


作者:ikun日记
来源:juejin.cn/post/7373921342096080911
收起阅读 »

当程序员写代码就行了,为什么还要画图

相信很多人选择当程序员除了这个行业起步阶段薪资比其他行业高一些之外,还有一个很大的因素是觉得做研发类的工作只要代码写的好,跟电脑这个“直男”打交道就可以了。 但是还没走出校门呢,毕业这一关就得边做毕业设计边写毕业论文。写毕业论文可不是直接把自己的实现代码从ID...
继续阅读 »

相信很多人选择当程序员除了这个行业起步阶段薪资比其他行业高一些之外,还有一个很大的因素是觉得做研发类的工作只要代码写的好,跟电脑这个“直男”打交道就可以了。


但是还没走出校门呢,毕业这一关就得边做毕业设计边写毕业论文。写毕业论文可不是直接把自己的实现代码从IDE粘贴到Word里,但凡代码多一点就会被老师要求修改,老师会告诉你要把你做的毕业设计的功能、设计思路、关键部分的实现细节用绘图结合文字表达清楚。


这时很多同学就犯难了,匆匆拿出大三的教材《软件程序设计》看看上面的那些的图都怎么画的,再找找网上的例子,模仿着能画出个类似下面的流程图。



上面这个图乍一看还可以,但是网上关于流程图的语法好几个版本,每个人画出来都不一样,而且用这种形式表达大一些的流程,就完全感觉眼花缭乱。


画图的底层逻辑是沟通


其实走到工作岗位上之后我们仍然面临着这样的问题,除了专心写代码外,我们的工作构成中还有大量的需求评审、需求分析、技术评审、系统方案设计这样的需要人和人进行沟通的环节,讲逻辑、讲实现方案、讲设计思路的沟通环节。究其原因就是因为IT行业分工明细,它像其他行业的工作流水线一样涉及大量工种的合作,但又因为交付的软件并不像工业流水线一样是标准件,所以上面列举的每个环节都需要良好的沟通,让各职能人员之间建立共识、建立统一语言后才能完成高效协作交付产品。


既然需要良好的沟通建立统一语言,那么我们在需求分析、技术评审、系统方案设计文档上就不能使用太主观的语言,也不能把实现代码直接往文档上粘,那样的话且不说有的岗位不写代码,即使同岗位的其他同事也没有那个精力一行一行的仔细看完你的代码、理解你的思路。


所以这就需要我们能够简洁、高效地用人们都能看懂的专业图形描述出软件开发这些环节中需要重点沟通的需求逻辑和技术的关键细节。


我们都知道从事理工专业的人,可能对构图、色彩这些不太擅长,那么有没有一种图形不需要美术基础就能掌握,足够专业让图形能突出我们想表达的技术细节,同时还足够简洁即使是不太懂技术的人也能完全看懂呢?在IT领域还真有,那就是UML。


比如同样是表达需求的业务流程,用流程图表达的就是上面图-1的那个样子,但是用表达力更强,更注重语法的UML活动图表达流程的话就是下面这个样子。



关于怎么用活动图分析表达流程,后面会有专门的章节去给大家讲解。


程序员画图难的成因


说到UML,无论是大学里还是市面上讲解UML的书籍中对UML的讲解都太过枯燥了,它们通常都是以技术和软件设计的角度来讲述UML的,通常上来会先讲解一大堆图,哪些是结构建模,哪些是行为建模,紧接着就是各种图的一堆语法(画法),或者是给出的示例太过于技术化,完全脱离日常生活让人无法理解。


这就给我们这样一开始不太懂的人一种UML太过专业太过复杂,不好用的印象。想的那么清楚画出图来,代码早写好了。典型的例子就是如果画类图把类的各个属性和方法都想好画出来也太费时间了,况且需求多变还要经常改,还有就是那些类图表示的类的关系一会儿是箭头、一会儿是虚线、不明白他们都什么区别,看多了就头疼。


其实上面这个现象完全就是误区,UML完全不是必须那么复杂--把所有细节都表示出来才算完事,我们完全可以从需求分析阶段开始就开始使用,在分析的过程中构思业务的结构并画出来它大概的样貌。



后面随着对需求的进一步了解再去补全或者调整其中的内容。写技术文档常用的UML图除了能像上面这样使用类图分析业务的结构,还有活动图、顺序图、状态机图从不同角度分析业务的行为,而且是循序渐进的使用,不是上来把这些都用上。


早期对业务知晓不够透彻时UML图可以画的粗略些,流程分析也只先分析明白大流程即可,随着使用UML分析业务的过程对业务逐渐了解后再逐渐细化以及使用不同的图形从不同角度描述业务。 UML家族里提供的各种图,也不局限于只能用于技术分析,甚至需求用例、系统架构、IT架构方面的需求也能够使用UML进行描述。


掌握UML让自己有更多可能


无论是一线研发,还是已经转型项目经理、产品经理或者团队管理的人员或者是想要上车入行的萌新程序员,本课程都能让你收益颇多,让你掌握产品经理写需求的一些基本技能,也让你轻松应对项目经理参与竞标和项目管理时的文案编写工作。


同时还能让你管理项目质量时找到“抓手”,通过在项目团队建立技术评审、方案设计等相关机制--融合团队成员对UML的使用,让团队成员的思维性创造更容易被周知也让这些内容更容易被Review,从而达到项目开发期间高效的沟通和良好的质量保证。


职场上的“汇报困境”


除了上面讲的这些我们工作中干活需要用到的各种图形外,在职场上班和在学校上学有一个重要的区别就是我们时不时的就要被拿出来评比、通晒、述职,这些场合都会要求我们做汇报。


针对这个程序员在职场中的普遍痛点,推荐一下我用大半年的时间沉淀,汇集了我多年职场经验的画图课,解决程序员普遍只愿意埋头写代码,不会做需求分析、不会做技术评审、不会画架构图、述职汇报做不好,等等这些需要画图和表达能力的事情的时候就犯难的问题,帮助大家摆脱代码的单一维度,从多维度提升自己,建立自信,让你在工作中更游刃有余


课程最后一部分还会扩展一些互联网开发人员在职场中应对各种汇报的策略,讲述一些写汇报PPT的主旨思路,侧重点和注意事项。同时也讲一些使用堆砖块画法(我自己总结的)给汇报PPT进行配图的思路,怎么通过这些图快速抓住听众的眼球建立共识,以及怎么使用一些配图讲解规划给上级“画饼”来获得他们的支持从而进一步获得他们后续在资源上的支持,更好地开展工作,这些技巧我们在课程最后一部分都会讲到。


相关推荐


现有有两种订阅方式


方式1微信专栏:程序员的全能画图课


方式2小报童专栏:程序员的全能画图课


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

我的 CEO 觉得任何技术经理都是多余的

原文 QUESTIONABLE ADVICE: “MY BOSS SAYS WE DON’T NEED ANY ENGINEERING MANAGERS. IS HE RIGHT?” 我最近加入了一家初创公司,负责管理一个约 40 名工程师的团队,担任技术...
继续阅读 »

原文 QUESTIONABLE ADVICE: “MY BOSS SAYS WE DON’T NEED ANY ENGINEERING MANAGERS. IS HE RIGHT?”



file


我最近加入了一家初创公司,负责管理一个约 40 名工程师的团队,担任技术副总裁。然而,我与 CEO(之前是工程师)在是否需要雇佣专职技术经理的问题上产生了很大的冲突。目前,工程师们被分成了 3-4 人的小团队,每个团队有一个工程师头头,负责领导团队,但他们的主要职责仍然是编写代码和交付产品。


我有 HC 在未来一年雇佣更多的工程师,但没有经理的 HC。老板认为我们是初创公司,负担不起这种奢侈品。在我看来,我们显然需要技术经理,但在他看来,经理只是多余的开销,在我们的阶段所有人都应该全力编写代码。


我不知道该如何论证。在我看来这很显然,但实际上我很难用言语表达为什么我们需要技术经理。你能帮帮我吗?


—— 真的是多余的开销吗(?!)




这里有很多问题需要解答。


你的首席执行官不理解为什么需要经理,这并不奇怪,因为他似乎不明白为什么需要组织结构。🙈 他为什么要对你如何组织团队或你可以雇佣哪些角色进行微管理?他雇用了你来做这份工作,却不让你完成。他甚至不能解释为什么不让你做。这不是个好兆头。


但这个问题确实值得思考。我们假设他不是故意要刁难你。😒


我能想到两种论证雇用技术经理的方式:一种是相当复杂的,从第一性原理 (First Principle) 出发,另一种非常简单,但可能不太令人满意。


我个人对权威有一种强烈的反感;我讨厌被告知该做什么。直到最近,我才通过系统理论的视角,找到了一种对层级制度既健康又实用的理解。


为什么组织中存在层级制度?


层级制度确实带有很多负面包袱。我们许多人都有过在层级制度下与经理或整个组织打交道的不幸经历。在这些地方,层级制度被用作压迫的工具,人们通过垄断信息和玩弄权力游戏来提升地位,决策则是通过权力压制来做出。


在那种地方工作真的是一种折磨。谁愿意将自己的创造力和生命力投入到一个感觉像《呆伯特》漫画的地方,明知道自己的价值被极少认可或回报,而且这些价值会慢慢地但确实被压制掉?


file


但层级制度本质上并非是专制的。层级制度并不是人类为控制和支配彼此而发明的一种政治结构,它实际上是自组织系统的一种属性,是为了子系统的有效运作而出现的。事实上,层级制度对复杂系统的适应性、弹性和可扩展性至关重要。


让我们从一些关于系统的基本事实开始,为可能不熟悉的人介绍一下。


层级是自组织系统的一种属性


一个系统是「由相互依赖的组件组成的网络,这些组件共同工作以实现一个共同目标」(W. Edward Deming)。一堆沙子不是一个系统,但一辆车是一个系统;如果你把油箱取下来,车就无法运作。


子系统是一个在更大系统内有较小目标的元素集合。在一个系统中可以有很多层次的子系统,它们相互依存地运行。子系统总是为了支持更大系统的需求而工作;如果子系统只为自己的最佳利益优化,整个系统可能会挂掉(这就是「次优」(suboptimal)这个术语的由来 😄)。


如果一个系统能够通过多样化、适应和改进自身使自己变得更加复杂,那么它就是自组织的。随着系统自组织并增加其复杂性,它们往往会生成层级 —— 即系统和子系统的排列。在一个稳定、有弹性和高效的系统中,子系统在很大程度上可以自我管理、自我调节,并为更大系统的需求服务,而更大系统则负责协调子系统之间的关系并帮助它们更好地发挥作用。


层级最小化了协调成本,减少了系统中任何部分需要跟踪的信息量,防止信息过载。子系统内部的信息传递和关系比子系统之间的信息传递或关系要密集得多,延迟也少得多。


(对于任何软件工程师来说,这些应该都很熟悉。模块化,对吧?😍)


按照这个定义,我们可以说,经理的工作就是在团队之间进行协调并帮助他们的团队表现得更好。


对社会技术系统的二分是伪命题


你可能听过这个谬论:「工程师搞技术,经理搞人。」我讨厌这种说法。😊 我认为这完全误解了社会技术系统的本质。社会技术系统中的「社会」和「技术」并不是截然分开的,而是相互交织、相互依存的。事实上,很少有纯粹的技术工作或纯粹的人际工作;有大量涉及两种技能的粘合工作。


看看任何一个有效运作的工程组织除了编写代码之外还要做的一部分任务:



  • 招聘、建立人脉、面试、培训面试官、汇总反馈、撰写职位描述和职业发展路径

  • 每个项目或承诺的项目管理、优先级排序、管理利益相关者和解决冲突、估算规模和范围、进行回顾会议

  • 召开团队会议、进行一对一交流、提供持续的成长反馈、撰写评审、代表团队的需求 架构设计、代码审查、重构;捕获 DORA 和生产力指标、管理警报量以防止倦怠


许多工作可以由工程师完成,而且通常也是如此。每家公司对这些任务的分配方式有所不同。这是一件好事!你不希望这些工作仅由经理来做。你希望个人贡献者共同创造组织,并参与其运行方式。几乎所有这些工作由有工程背景的人完成会更有效。


所以,你可以理解为什么有人会犹豫是否要把宝贵的人员编制花在技术经理上。为什么不希望技术部门的每个人的主要工作都是编写和交付代码呢?这不是从定义上说最大化生产力的最佳方式吗?


额……😉


技术经理是一层有用的抽象


理论上,你可以列出所有需要完成的协调任务,并让不同的人来负责每一项。但实际上,这是不切实际的,因为这样每个人都需要了解所有事情。记住,层级制度的主要好处之一是减少信息过载。团队内部的沟通应该是高效和快速的,而团队之间的沟通则可以少一些。


随着公司的扩展,你不能期望每个人都认识其他所有人;我们需要抽象的概念才能运作。经理是他们团队的联络点和代表,充当重要信息的路由器。


file


有时我把经理想象成公司的神经系统,将信息从一个部门传递到另一个部门,以协调行动。将许多或大部分功能集中到一个人身上,可以利用专业化的优势,因为经理会不断建立关系和背景知识,并在他们的角色中不断改进,这大大减少了其他人的上下文切换。


管理者 (Manager) 日程与创造者 (Maker) 日程


技术工作需要集中和专注。上下文切换的成本很高,过多的中断是挺要命的。而管理工作则是每小时左右进行一次上下文切换,并且一整天都要应对各种打断。这是两种完全不同的工作模式、思维方式和日程安排,无法很好地共存。


通常,你希望团队成员能够把大部分时间花在直接为他们负责的成果做出贡献的事情上。工程师只能做有限的粘合工作,否则他们的日程安排就会变得支离破碎,从而无法履行他们的承诺。而管理者的日程安排本身已经是支离破碎的,因此让他们承担更多的粘合工作通常不会带来太大干扰。


虽然并不是所有粘合工作都应该由管理者来完成,但管理者的职责是确保所有工作都能完成。管理者的职责是尽量让每个工程师都能从事有趣且具有挑战性的工作,但不能让他们感到过于负担重,还要确保不愉快的工作能公平分配。管理者还要确保,如果我们要求某人完成一项工作,就必须为其配备成功完成这项工作所需的资源,包括专注的时间。


管理是问责的工具


当你是工程师时,你对自己开发、部署和维护的软件负责。而作为经理,你则对团队和整个组织负责。


管理是一种让人们对特定结果(如构建具备正确技能、关系和流程的团队,以做出正确的决策并为公司创造价值)负责的方式,并为他们提供实现这些结果所需的资源(预算、工具和人员编制)。如果你不把组织建设作为某人的首要任务,那么这就不会成为任何人的首要任务,这意味着它可能不会得到很好地执行。那么,这该由谁负责呢,CEO 先生?


你对技术负责人、工程师或任何负责交付软件的人在「业余时间」能完成的任务有一个合理的上限。如果你试图让技术负责人负责构建健康的工程团队、工具和流程,那么你就是在要求他们在同一个日历里做两份时间不兼容的工作。最可能的情况是,他们会专注于自己觉得舒适的成果(技术成果),而在后台堆积组织债务。


在自然层级中,我们向上看是为了目标,向下看是为了功能。简而言之,这就是我们需要技术经理的复杂原因。


选择无趣的技术文化


更简单的论点是:大多数工程组织都有技术经理。这是默认设置。多年来,许多比你或我更聪明的人花了大量时间思考和调整组织结构,这就是我们得到的结果。


正如丹-麦金利(Dan McKinley)的名言,我们应该「选择无趣的技术」。无趣并不意味着不好,而是意味着它的能力和失败条件是众所周知的。你只能获得少数的创新点数,因此你应该明智地将这些点数用在能够成就或毁掉你业务的核心差异点上。文化也是如此。你真的想把你的点数用在组织结构上吗?为什么?


无论好坏,层级组织结构是众所周知的。市场上有很多人擅长管理或与管理者合作,你可以雇佣他们。你可以接受培训、指导,或者阅读大量的自助书籍。有各种各样的管理哲学可以围绕它们来凝聚团队或用来排除其他人。另一方面,我所知道的无经理实验(例如 Medium 和 GitHub 的全员自治,或 Linden Lab 的「选择你的工作」)都被悄然放弃或被颠覆了。在我的经验中,这并不是因为领导者疯狂追求权力,而是由于混乱、缺乏重点和执行不力。


当没有明确的结构或层级时,结果不是自由和平等,而是「非正式的、不被承认的和不负责任的领导」,正如《无结构的暴政》中详细描述的那样。事实上,这些团队往往是混乱、脆弱和令人沮丧的。我知道!我也很生气!😭


这个论点并不一定能证明你的 CEO 是错的,但我认为他的证明标准比你的要高得多。「我不想让我的任何工程师停止写代码」并不是一个有效的论点。但我也觉得我还没有完全解决生产力的核心问题,所以我们再来讨论一下这个问题。


更多代码行数 ≠ 更高生产力


简要回顾一下:我们在讨论一个有约 40 名工程师的组织,分成 10 个小组,每组有 3-4 名工程师,每组都有一个技术负责人。你的 CEO 认为,如果有人停止全职编程,这个减速将是你们无法承受的。


也许吧。但根据我的经验,由经验丰富的技术经理领导的几个较大团队,将远远优于这些小团队。这差距很明显。而且,他们可以以更高效、可持续和人性化的方式完成工作,而不是这种拼命的死命赶工。


系统思维告诉我们原因!更少的团队,但规模更大,你会有更少的整体管理开销,且大大减少了团队内慢且昂贵的协调。你可以在团队内部实现丰富、密集的知识传递,从而实现更大面积的共享。每组有7-9名工程师,你可以建立一个真正的值班轮换,这意味着更少的英雄主义和更少的倦怠。你需要进行的协调可以更具战略性,减少战术性,更具前瞻性。


五个大团队是否能比十个小团队编写更多的代码行数,即使有五名工程师成为经理并停止编写代码?可能会,但谁在乎呢?你的客户根本不关心你写了多少代码行数。他们关心的是你是否在构建正确的东西,是否在解决对他们重要的问题。关键是推动业务前进,而不是单纯地编写代码。不要忘记,单纯地编写代码会产生额外的成本和负面效应。


决定你速度的是你是否把时间花在了正确的事情上。学会正确决定构建什么是每个组织都必须自己解决的问题,而且这是一项持续不断的工作。技术经理不会做所有的工作或做出所有的决策,但根据我的经验,他们对于确保工作顺利进行并且做得很好,绝对至关重要。正如我在上篇文章中写到的,技术经理是系统用来学习和改进的反馈循环的载体。


管理人员是否会成为不必要的开销?


当然有可能。管理的核心是协调团队之间的工作并提升团队的运作效率,所以任何减少协调需求的方式也会减少对管理的需求。如果你是一家小公司,或者你的团队成员都是非常资深且习惯合作的,那么你就不需要太多的协调。另一个重要因素是变化的速度;如果你的公司在快速增长或者人员流动频繁,或者面临很多时间压力或频繁的战略调整,你对管理人员的需求就会增加。但也有许多较小的组织在没有太多正式管理的情况下运作得很好。


我不喜欢「开销」这个词,因为 a) 这有点粗鲁,b) 称管理人员为「开销」的人通常是不尊重或不重视管理这门技艺的人。


但管理实际上确实是开销😅。许多其他的粘合工作也是如此!这些工作很重要,但它们本身并不能推动业务向前发展;我们应该尽量只做那些绝对必要的工作。粘合工作的天然属性使得它很容易扩散,吞噬所有可用的时间和资源(甚至更多)。


限制是好的。感觉资源不足是好的,这应该成为常态。管理很容易变得臃肿,管理人员可能非常不愿意承认这一点,因为他们从来没有感到压力或紧张减少。(事实上,情况可能恰恰相反;臃肿的管理层可能会为管理人员带来更多工作,而精简的组织结构可能会让他们反而感到压力更小。官僚主义往往会自我发育。特别是当管理层过于关注晋升和自我时。这也是确保管理不应仅为升职或统治的又一个充分理由)




管理也很像运营工作,当它做得好的时候,是看不见的。评估管理人员的工作可能非常困难,尤其是在短期内,而决定何时创建或偿还组织债务是一个完全不同的复杂问题,远远超出了这篇文章的讨论范围。


但是,是的,管理人员绝对可以成为不必要的开销。


然而,如果你有 40 个工程师都向一个副总裁汇报,而没有其他人专门负责人员、团队和组织相关的工作,那么我可以相当肯定地说,这对你来说目前不是一个问题。




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

接了个私活,分享下我是如何从 0 到 1 交付项目的

web
大家好,我是阿杆,不是阿轩。 最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。 我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。 前前后后弄了一个半月到两个月,也算是积累了一点经...
继续阅读 »

大家好,我是阿杆,不是阿轩。


最近有个校友找到我,他自己办了个公司,想做个微信小程序,于是找我帮他开发,当然不是免费的。


我一想,那挺好呀,虽然我没接过私活吧,但不代表我以后不会接私活,这不正好可以练习一下子。


前前后后弄了一个半月到两个月,也算是积累了一点经验,分享给大家,如果以后也接到私活,可以参考一下我的开展方式。


由于文中涉及到实际业务的东西不方便透露, 下面我将用图书管理系统来代替实际业务,并且称这位校友为“老板”。


image-20240421154347807


总览


我接手的这个项目是完完全全从0开始的,老板只有一个idea,然后说他的idea是来自于另一个小程序的,有部分内容可以参考那个小程序,其他什么都没有了。


先讲一下我的总体流程:



  1. 确定老板的大致需求,以及预期费用

  2. 详细梳理开发功能点,并简单画下原型图

  3. 工时评估,确定费用

  4. 出正式的UI设计稿

  5. 拟定合同、签合同

  6. 开发阶段

  7. 验收、上线


大概就是这么些步骤,也对应本文的目录,如果你有想看的部分,可以直接跳转到对应的部分。


下面我会详细讲讲每一步我都做了些什么。


确定需求


首先老板找到我,跟我说他想做一个图书管理的微信小程序,然后讲了几个小程序内的功能点。


我也向他提了几个问题:



  1. 预算有多少?



    这个肯定得问的,要是预算和工作量严重不匹配,那肯定做不了的。毕竟都是出来赚钱的,总不能让咱用爱发电吧?




  2. 预计一年内会有多少用户量?会有多少数据量?



    这个问题我主要是想知道并发量会有多少、数据量会有多少?这样方便我后续判断系统需要的配置,也便于我后续对整个系统的设计。


    好在整体用户量和数据量都不大,这对我来说也就没什么压力了,至于以后会发展到如何,这不是我该考虑的事情,我顶多把代码写好看点,他后续发展壮大了肯定是把项目接到公司里雇人做的,跟我也没什么关系。




  3. 你那边能够提供什么?



    这个主要是看对方有什么资源,是否能够对项目开发有一定的帮助。


    在我这个项目里,老板那边是什么都没有的,没有设计图、没有服务器资源、也没有辅助人员,所有内容都包揽在我这边,然后有什么问题就直接问他。




  4. 你希望多久完成?



    如果老板很急的话,那可能得多叫几个人一起做,如果时间充足的话,自己一个人做也不是不可以。





好了,第一次对话大概就是这么些内容,但仅靠一次对话肯定是无法确定需求的,只能了解个大概。


我根据老板的想法,写了一份 需求分析 出来,首先列出了几个大概的功能点:


大致功能点列举


然后根据这些功能点进行扩展,把所有功能列举出来,画成一个思维导图(打码比较多,大家将就将就😅):


延伸的思维导图


好,那么第一版的需求分析差不多就出来了,接着我打电话给老板,对着这个思维导图,一个一个的跟老板确认,需不需要这些功能。


老板一般会提出一些异议,我再继续修改思维导图,直到老板觉得这些功能他都满意了。当然这过程中我也会给一些自己的建议,有些超预算的功能,还是建议他不要做。


到这里,需求就基本确定好了。


梳理开发功能点、绘制原型图


由于我不会前端开发,只是个简单的后端仔,所以我还找了一个前端同学一起做。


我和前端两个人根据前面的需求文档,详细的梳理出了 小程序 和 后台管理系统 的功能,这个部分是比较重要的,因为后续画设计稿和开发都会以这份文档为主:


小程序功能梳理文档


还画了一些简单的原型图,这玩意丑点没事,能让人看懂就行🤣🤣:


小程序原型图-我的信息


后台管理系统原型图


这些东西弄完之后,再找老板进行一遍确认,把里面每个点都确认下来,达成共识。


工时评估,确定费用


老板觉得OK了,就到了该谈钱的时候了,前面只是聊了预算,并不是正式的确定费用。


那咱们也不能张嘴就要,要多了老板不乐意,要少了自己吃亏。


所以咱们先评估下工时,这边我分了几个部分分别进行工时评估:



  • 需求分析、功能梳理(就是前面做的那些,还没收钱的呢)

  • UI设计、交互设计

  • 前端开发

  • 后端开发

  • 系统运维(包含服务器购买、搭建、认证、配置等)

  • 后期维护


其中设计稿是找另一位朋友做的,钱单独算,然后其他部分都是我和前端同学两个人评估的,评估的粒度还是比较细的,是以小时为单位进行计算的,给大家大概看一下:


前端开发工时评估


后端开发工时评估


评估完之后汇总一下,然后根据我们自己工作的时薪,给老板一个最终的定价,正常的话还需要在这个定价上再乘一个接单系数(1.2~1.5),但是我们这个老板是校友啊,而且预算也不多,所以就没乘这个系数了(还给他打了折😂,交个朋友)。


定价报出去之后,老板觉得贵了怎么办?很简单,砍功能呗,要么你去找别人做也行。



预付订金



我觉得正常应该在梳理功能之前就要付一部分订金,也不用多少,几百块就行,算是给我们梳理功能的钱。



这里接下来就要画UI图了,我们先找老板付个订金,订金分为三部分:



  • 给前端的订金

  • 给后端的订金

  • 给UI同学画设计稿的完整费用


因为UI设计是我这边联系的,所以我肯定得先保障她的费用能完整到手,不然到时候画完图跟我说不做了,那我咋对得起画图的人。


画UI图


这部分就不用咱们操心了,把文档交给设计同学,然后等她出图就行。


这个过程中也可以时不时去看看她画的内容符不符合咱们的预期,当个小小的监工。


盯着干活


画完稿子需要跟老板、开发都对一遍,看看有没有出入,符不符合预期,有问题及时修改下,没问题就按照这份稿子进行开发了。


拟定合同、签合同



合同也是我来拟定的,其实是先到网上找了个软件开发的合同模板,然后再根据自己的想法进行合理的调整。



为什么我要到这一步才签合同呢?我觉得合同内容越细致越好,最好是能够把要开发的内容、样式都写在合同上,这样省得后面扯皮。


现在文档也出了,图也画完了,那咱们把这些东西都贴在和合同的附件上,然后附上这些条约:



  • 乙方将严格按照经过甲方审核的《软件功能设计书》的要求进行软件的开发设计。

  • 甲方托付乙方开发的软件在签订合同之后如需增加其它功能,必须以书面形式呈交给乙方,乙方做改动并酌情收取适当费用。


这样就可以保障我们在开发完后不会被恶意的增加或者修改功能了。


再改一次


这里我再列一些其他需要特别注意的点:



  1. 乙方交付日期,以及最多延期多久,如果超时怎么办?

  2. 甲方付款方式和日期(我们是用的 442 ,开工付 40%,中期验收付 40%,开发完验收付 20%)。

  3. 甲方拖欠项目款的处理方式(支付迟延履行金等)。

  4. 服务器费用是谁出?如果是乙方,需要注意包服务器的时限。

  5. 项目维护期,一般一年半年的吧。

  6. 乙方不保证项目 100% 可用,只能保障支撑 多少人 使用,支撑同时在线人数 多少人 ,如果遇到恶意攻击,不归乙方负责。

  7. 软件归属权是谁的?(如果项目款比较少的话,乙方可以要求要软件归属权,之后甲方如果想把项目接回去自己公司维护的话,需要从乙方手里买,这样乙方可以回点血)


大概就是这些吧,还有其他的东西基本都是按照模板的,没怎么改。


弄完给老板看看,没问题就签了,有问题两方再协商一下,我们这边是直接签了的。



开发阶段


开发没什么好说的,跟你在公司开发一样。


不过你接私活可不能在公司开发🚫,只能回家了自己干,不然被抓到上班干私活,你看老板裁不裁你就完事了。


微信小程序上线注意事项


微信小程序对请求的接口有三个基本要求:



  1. 必须是有备案的域名。

  2. 必须是有SSL证书(https)。

  3. 域名不得带端口号。


这个域名的问题必须要尽早解决,不然后面开发完了再去弄的话,工信部备案审核都要挺久的,不延期都难。


还有一种方式,我在逛微信开放社区看到的,使用云函数进行中转,间接请求ip接口,感觉是可行的,也比较省事,具体操作大家可以自己去探索一下。


我也是吃了没有经验的亏,买域名 + 工信部备案 + 公安备案 + 小程序备案,这一套操作下来真给我整难受死了,直接用云函数省事多了。



验收、上线


这部分也没什么好说的,大家在公司也经常经历这个步骤。


多沟通,多确认,


唯一需要提醒的是,验收的时候咱不能无条件接收老板的任何要求,毕竟价格和开发内容都是已经定好的,如果要加内容或者改内容,记得酌情要一点工时费,可不能亏待了自己。



后记


整个过程中,其实沟通是最重要的,写代码谁不会是吧?但是得让老板觉得OK才行,如果有什么疑问或者觉得不合理的地方啊,最好是尽早沟通,不然越到后面只会让问题变的越来越大。


最近刚做完这个项目,说实话没赚什么钱,甚至有点小亏😅。而且这个老板还有点拖欠工资的感觉,中期项目款拖到了项目交付才给,项目尾款到目前还没付😅😅。不过还好合同里写到了关于这块的处理方式,倒也不担心他不付这个钱。


(虽然我也不知道在哪能接到靠谱的私活🤣,但也可以先收藏本文,万一之后来活了,还能翻出来看看)


最后,希望各位都能接到 very good 的私活,祝大家早日实现财富自由!


webwxgetmsgimg (1)


作者:阿杆
来源:juejin.cn/post/7359764922727333939
收起阅读 »

仿今日头条,H5 循环播放的通知栏如何实现?

web
我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。 那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的...
继续阅读 »

我们在各大 App 活动页面,经常会看到循环播放的通知栏。比如春节期间,我就在今日头条 App 中看到了如下通知:「春节期间,部分商品受物流影响延迟发货,请耐心等待,祝你新春快乐!」。


toutiao.gif


那么,这种循环播放的通知栏如何实现呢?本文我会先介绍它的布局、再介绍它的逻辑,并给出完整的代码。最终我实现的效果如下:


loop-notice.gif


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。


布局代码


我们先看布局,如下图所示,循环播放的布局不是普通的左中右布局。可以看到,当文字向左移动时,左边的通知 Icon 和右边的留白会把文字挡住一部分。


block-out.png


为了实现这样的效果,我们给容器 box 设置一个相对定位,并把 box 中的 HTML 代码分为三部分:



  • 第一部分是 content,它包裹着需要循环播放的文字;

  • 第二部分是 left,它是左边的通知 Icon,我们给它设置绝对定位和 left: 0;

  • 第三部分是 right,它是右边的留白,我们给它设置绝对定位和 right: 0;


<div class="box">
<div class="content">

div>
<div class="left">🔔div>
<div class="right">div>
div>

.box {
position: relative;
overflow: hidden;
/* ... 省略 */
}
.left {
position: absolute;
left: 0;
/* ... 省略 */
}
.right {
position: absolute;
right: 0;
/* ... 省略 */
}

现在我们来看包裹文字的 content。content 内部包裹了三段一模一样的文字 notice,每段 notice 之间还有一个 space 元素作为间距。



<div id="content">
<div class="notice">春节期间,部分商品...div>
<div class="space">div>
<div class="notice">春节期间,部分商品...div>
<div class="space">div>
<div class="notice">春节期间,部分商品...div>
div>


为什么要放置三段一模一样的文字呢?这和循环播放的逻辑有关。


逻辑代码


我们并没有实现真正的循环播放,而是欺骗了用户的视觉。如下图所示:



  • 播放通知时,content 从 0 开始向左移动。

  • 向左移动 2 * noticeWidth + spaceWidth 时,继续向左移动便会露馅。因为第 3 段文字后不会有第 4 段文字。


    如果我们把 content 向左移动的距离强行从 2 * noticeWidth + spaceWidth 改为 noticeWidth,不难看出,用户在 box 可视区域内看到的情况基本一致的。


    然后 content 继续向左移动,向左移动的距离大于等于 2 * noticeWidth + spaceWidth 时,就把距离重新设为 noticeWidth。循环往复,就能欺骗用户视觉,让用户认为 content 能无休无止向左移动。



no-overflow-with-comment.png


欺骗视觉的代码如下:



  • 我们通过修改 translateX,让 content 不断地向左移动,每次向左移动 1.5px;

  • translateX >= noticeWidth * 2 + spaceWidth 时,我们又会把 translateX 强制设为 noticeWidth

  • 为了保证移动动画更丝滑,我们并没有采用 setInterval,而是使用 requestAnimationFrame。


const content = document.getElementById("content");
const notice = document.getElementsByClassName("notice");
const space = document.getElementsByClassName("space");
const noticeWidth = notice[0].offsetWidth;
const spaceWidth = space[0].offsetWidth;

let translateX = 0;
function move() {
translateX += 1.5;
if (translateX >= noticeWidth * 2 + spaceWidth) {
translateX = noticeWidth;
}
content.style.transform = `translateX(${-translateX}px)`;
requestAnimationFrame(move);
}

move();

完整代码


完整代码如下,你可以在 codepen 或者码上掘金上查看。



总结


本文我介绍了如何用 H5 实现循环播放的通知栏:



  • 布局方面,我们需要用绝对定位的通知 Icon、留白挡住循环文字的左侧和右侧;此外,循环播放的文字我们额外复制 2 份。

  • 逻辑方面,通知栏向左移动 2 * noticeWidth + spaceWidth 后,我们需要强制把通知栏向左移动的距离从 2 * noticeWidth + spaceWidth 变为 noticeWidth,以此来欺骗用户视觉。




作者:小霖家的混江龙
来源:juejin.cn/post/7372765277460496394
收起阅读 »

为什么很多人不推荐你用JWT?

为什么很多人不推荐你用JWT? 如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。 什么是JWT? 这个是他的官网JSON We...
继续阅读 »

为什么很多人不推荐你用JWT?


如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。


什么是JWT?


这个是他的官网JSON Web Tokens - jwt.io


这个就是JWT


img


JWT 全称JSON Web Token


如果你还不熟悉JWT,不要惊慌!它们并不那么复杂!


你可以把JWT想象成一些JSON数据,你可以验证这些数据是来自你认识的人。


当然如何实现我们在这里不讲,有兴趣的可以去自己了解。


下面我们来说一下他的流程:



  1. 当你登录到一个网站,网站会生成一个JWT并将其发送给你。

  2. 这个JWT就像是一个包裹,里面装着一些关于你身份的信息,比如你的用户名、角色、权限等。

  3. 然后,你在每次与该网站进行通信时都会携带这个JWT

  4. 每当你访问一个需要验证身份的页面时,你都会把这个JWT带给网站

  5. 网站收到JWT后,会验证它的签名以确保它是由网站签发的,并且检查其中的信息来确认你的身份和权限。

  6. 如果一切都通过了验证,你就可以继续访问受保护的页面了。


JWT Session


为什么说JWT很烂?


首先我们用JWT应该就是去做这些事情:



  • 用户注册网站

  • 用户登录网站

  • 用户点击并执行操作

  • 本网站使用用户信息进行创建、更新和删除 信息


这些事情对于数据库的操作经常是这些方面的



  • 记录用户正在执行的操作

  • 将用户的一些数据添加到数据库中

  • 检查用户的权限,看看他们是否可以执行某些操作


之后我们来逐步说出他的一些缺点


大小


这个方面毋庸置疑。


比如我们需要存储一个用户ID 为xiaou


如果存储到cookie里面,我们的总大小只有5个字节。


如果我们将 ID 存储在 一个 JWT 里。他的大小就会增加大概51倍


image-20240506200449402


这无疑就增大了我们的宽带负担。


冗余签名


JWT的主要卖点之一就是其加密签名。因为JWT被加密签名,接收方可以验证JWT是否有效且可信。


但是,在过去20年里几乎每一个网络框架都可以在使用普通的会话cookie时获得加密签名的好处。


事实上,大多数网络框架会自动为你加密签名(甚至加密!)你的cookie。这意味着你可以获得与使用JWT签名相同的好处,而无需使用JWT本身。


实际上,在大多数网络身份验证情况下,JWT数据都是存储在会话cookie中的,这意味着现在有两个级别的签名。一个在cookie本身上,一个在JWT上。


令牌撤销问题


由于令牌在到期之前一直有效,服务器没有简单的方法来撤销它。


以下是一些可能导致这种情况危险的用例。


注销并不能真正使你注销!


想象一下你在推特上发送推文后注销了登录。你可能会认为自己已经从服务器注销了,但事实并非如此。因为JWT是自包含的,将在到期之前一直有效。这可能是5分钟、30分钟或任何作为令牌一部分设置的持续时间。因此,如果有人在此期间获取了该令牌,他们可以继续访问直到它过期。


可能存在陈旧数据


想象一下用户是管理员,被降级为权限较低的普通用户。同样,这不会立即生效,用户将继续保持管理员身份,直到令牌过期。


JWT通常不加密


因此任何能够执行中间人攻击并嗅探JWT的人都拥有你的身份验证凭据。这变得更容易,因为中间人攻击只需要在服务器和客户端之间的连接上完成


安全问题


对于JWT是否安全。我们可以参考这个文章


JWT (JSON Web Token) (in)security - research.securitum.com


同时我们也可以看到是有专门的如何攻击JWT的教程的


高级漏洞篇之JWT攻击专题 - FreeBuf网络安全行业门户


总结


总的来说,JWT适合作为单次授权令牌,用于在两个实体之间传输声明信息。


但是,JWT不适合作为长期持久数据的存储机制,特别是用于管理用户会话。使用JWT作为会话机制可能会引入一系列严重的安全和实现上的问题,相反,对于长期持久数据的存储,更适合使用传统的会话机制,如会话cookie,以及建立在其上的成熟的实现。


但是写了这么多,我还是想说,如果你作为自己开发学习使用,不考虑安全,不考虑性能的情况下,用JWT是完全没有问题的,但是一旦用到生产环境中,我们就需要避免这些可能存在的问题。


作者:小u
来源:juejin.cn/post/7365533351451672612
收起阅读 »

我是没想到是还可以这样秒出答案 ...

起因 晚上在休闲游戏中,一网友发来信息求问,一道编程题。 咋一看,嘿 2023年1月浙江选考题(信息技术),挺新鲜,那就来看看吧。 聊了一下才知道,这是中考高考(6月28日晚23:05更正)选题。中考高考(6月28日晚23:05更正)就考这样的了吗? ...
继续阅读 »

起因


晚上在休闲游戏中,一网友发来信息求问,一道编程题。



咋一看,嘿 2023年1月浙江选考题(信息技术),挺新鲜,那就来看看吧。
聊了一下才知道,这是中考高考(6月28日晚23:05更正)选题。中考高考(6月28日晚23:05更正)就考这样的了吗?



image.png
image.png


一、题目



image.png



二、解析


因为题解想半天,没看明白要做的,就先直接上手代码去测试实验。通过足够多的次数去请求,就可以知道正确答案了(不符合出现的)。



后面恍然大悟会进一步讲解内容



二、代码测试


把该代码转成 java 对应的代码内容,并进行测试


public static void main(String[] args) {
// 答案记录
Map ansMap = new HashMap<>();
ansMap.put("AB##CD#", 0); // 选项A 答案
ansMap.put("#######", 0); // 选项B 答案
ansMap.put("#B##CDA", 0); // 选项C 答案
ansMap.put("###ABCD", 0); // 选项D 答案
for (int i = 0; i < 100000; i++) { // 10万次执行,看看 ABCD 答案是哪个一直没有出现
String res = runWork(); // 出现的结果
if (ansMap.get(res) == null){ // 出现和选项答案不一致的跳过
continue;
}
// 出现一致的进行+1
ansMap.put(res, ansMap.get(res) + 1);
}
// 输出结果
System.out.println(ansMap.toString());
}

public static String runWork() {
char[] a = {'A', 'B', '#', '#', 'C', 'D', '#'};
char[] stk = new char[a.length];
int top = -1;
Random random = new Random();

for (int i = 0; i < a.length; i++) {
int op = random.nextInt(2);
if (op == 1 && a[i] != '#') {
top++;
stk[top] = a[i];
a[i] = '#';
} else if (op == 0 && top != -1 && a[i] == '#') {
a[i] = stk[top];
top--;
}
}
return String.valueOf(a);
}

三、测试结果



微信图片_20230627210300.png


截图中可以看到,测试中,A、B、C 选项都出现了,不符合的是 D 选项,因此,正确答案是选项 D。

四、恍然大悟(真正解析)


仔细瞧命名, stk ,是栈(stack)的简写!可恶,这道题可以直接利用栈的知识去看选项去解了啊...



原字符数组是 'A', 'B', '#', '#', 'C', 'D', '#'

栈,就是先进后出。



选项内容解析
AAB##CD#对 a 字符数组都不进行拿出拿入,stk 字符数组就是空,
也就是不变,那么结果可以出现
B#######对 a 字符数组的ABCD都拿走,最终 stk 字符数组里就是 DCBA,
那么结果也可以出现
C#B##CDA对 a 字符数组都只拿A,并在最后一个的时候拿出最上层的。
最上层只有一个 A ,那就拿出 A ,
此时 stk 字符数组就为空了,那么结果可以出现
D###ABCD对 a 字符数组先拿A,stk 里就有 A ,但是B也需要拿,
且 A 要放在 B 拿之前的后面,不能实现,那么结果是不可以出现的!


图解:


ans.gif



那么最终,也就能明白这套代码的意思了,就是随机可能去拿去里面的字母,ABCD,放到栈里再实现放到原数组中去。对栈的理解与使用解释了一下。答案选 D ,只有 D 不符合栈的进出。




作者:南方者
来源:juejin.cn/post/7249288803532947517
收起阅读 »

【禁止血压飙升】如何拥有一个优雅的 controller

前言 见过几千行代码的 controller吗?我见过。 见过全是 try catch 的 controller 吗,我见过。 见过全是字段校验的 controller 吗,我见过。 见过全是业务代码的 controller 吗?不好意思,我们公司很多业务写在...
继续阅读 »

前言


见过几千行代码的 controller吗?我见过。


见过全是 try catch 的 controller 吗,我见过。


见过全是字段校验的 controller 吗,我见过。


见过全是业务代码的 controller 吗?不好意思,我们公司很多业务写在 controller 的。


看见这些我真的血压高。


正文


不优雅的 controller



@RestController
@RequestMapping("/user/test")
public class UserController {

private static Logger logger = LoggerFactory.getLogger(UserController.class);

@Autowired
private UserService userService;

@Autowired
private AuthService authService;

@PostMapping
public CommonResult userRegistration(@RequestBody UserVo userVo) {
if (StringUtils.isBlank(userVo.getUsername())){
return CommonResult.error("用户名不能为空");
}
if (StringUtils.isBlank(userVo.getPassword())){
return CommonResult.error("密码不能为空");
}
logger.info("注册用户:{}" , userVo.getUsername());
try {
userService.registerUser(userVo.getUsername());
return CommonResult.ok();
}catch (Exception e){
logger.error("注册用户失败:{}", userVo.getUsername(), e);
return CommonResult.error("注册失败");
}
}

@PostMapping("/login")
@PermitAll
@ApiOperation("使用账号密码登录")
public CommonResult<AuthLoginRespVO> login(@RequestBody AuthLoginReqVO reqVO) {
if (StringUtils.isBlank(reqVO.getUsername())){
return CommonResult.error("用户名不能为空");
}
if (StringUtils.isBlank(reqVO.getPassword())){
return CommonResult.error("密码不能为空");
}
try {
return success(authService.login(reqVO));
}catch (Exception e){
logger.error("注册用户失败:{}", reqVO.getUsername(), e);
return CommonResult.error("注册失败");
}
}

}


优雅的controller


@RestController
@RequestMapping("/user/test")
public class UserController1 {

private static Logger logger = LoggerFactory.getLogger(UserController1.class);

@Autowired
private UserService userService;

@Autowired
private AuthService authService;

@PostMapping("/userRegistration")
public CommonResult userRegistration(@RequestBody @Valid UserVo userVo) {
userService.registerUser(userVo.getUsername());
return CommonResult.ok();
}

@PostMapping("/login")
@PermitAll
@ApiOperation("使用账号密码登录")
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}

}


代码量直接减一半呀,这还不算上有些直接把业务逻辑写在 controller 的,看到这些我真的直接吐血



改造流程


校验方式



这个 if 校验看得我哪哪都不爽。好歹给我写一个断言吧。Assert.notNull(userVo.getUsername(), "用户名不能为空");


这不香吗?确实不香。


使用 spring 提供的@Valid




  • 在入参时使用@Valid注解,并且在 vo 中使用校验注解,如AuthLoginReqVO


@ApiModel(value = "管理后台 - 账号密码登录 Request VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthLoginReqVO {

@ApiModelProperty(value = "账号", required = true, example = "user")
@NotEmpty(message = "登录账号不能为空")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
private String username;

@ApiModelProperty(value = "密码", required = true, example = "password")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;

}

@Valid


在SpringBoot中,@Valid是一个非常有用的注解,主要用于数据校验。以下是关于@Valid的一些详细信息:



  1. 为什么使用 @Valid 来验证参数:在编写接口时,我们经常需要验证请求参数。通常,我们可能会写大量的 if 和 if else 代码来进行判断。但这样的代码不仅不优雅,而且如果存在大量的验证逻辑,这会使代码看起来混乱,大大降低代码可读性。为了简化这个过程,我们可以使用 @Valid 注解来帮助我们简化验证逻辑。

  2. @Valid 注解的作用:@Valid 的主要作用是用于数据效验,可以在定义的实体中的属性上,添加不同的注解来完成不同的校验规则,而在接口类中的接收数据参数中添加 @valid 注解,这时你的实体将会开启一个校验的功能。

  3. @Valid 的相关注解:在实体类中不同的属性上添加不同的注解,就能实现不同数据的效验功能。

  4. 使用 @Valid 进行参数效验步骤:整个过程如下,用户访问接口,然后进行参数效验,因为 @Valid 不支持平面的参数效验(直接写在参数中字段的效验)所以基于 GET 请求的参数还是按照原先方式进行效验,而 POST 则可以以实体对象为参数,可以使用 @Valid 方式进行效验。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。

  5. @Validated与@Valid的区别@Validated@Valid 的变体。通过声明实体中属性的 groups ,再搭配使用 @Validated ,就能决定哪些属性需要校验,哪些不需要校验。


全局异常处理



  • 这个全局异常处理,可以根据自己的异常,自定义异常处理,并设置一个兜底的异常处理



@ResponseBody
@RestControllerAdvice
public class ExceptionHandlerAdvice {
protected Logger logger = LoggerFactory.getLogger(getClass());

@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
logger.error("[handleValidationExceptions]", ex);
StringBuilder sb = new StringBuilder();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((org.springframework.validation.FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
sb.append(fieldName).append(":").append(errorMessage).append(";");
});
return CommonResult.error(sb.toString());
}

/**
* 处理系统异常,兜底处理所有的一切
*/

@ExceptionHandler(value = Exception.class)
public CommonResult<?> defaultExceptionHandler(Throwable ex) {
logger.error("[defaultExceptionHandler]", ex);
// 返回 ERROR CommonResult
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}

}


就这么多,搞定,这样就拥有了漂流优雅的 controller 了



在日常开发中,还有那些血压飙升瞬间



  • 我拿出下图阁下如何面对


image-20240411185003067.png



  • 这个阁下又如何面对,我不说,你能知道这个什么吗【狗头】


image-20240411185134843.png


总结



  • 不是很明白为什么有些喜欢在 controller 写业务逻辑的,曾经有个同事问我(就是喜欢在 controller 写业务的),你这个接口写在那里,我需要调一下你这个接口。我满脸问号??不是隔壁的模块吗,为什么要调我的接口?直接引用的我的 service 去调方法就好了。

  • 这个就是痛点,各写各的,冗余代码一堆。

  • 曾经看到一个同事写一个保存的方法,虽然逻辑挺多,我滑动了好久都还没有方法还没有结束。一个方法整整几百行……

  • 看过 spring 源码都知道,spring 源码难啃,就是因为 spring 无限往下套娃,基本每个方法干每个方法的事情。比如我保存用户时,就只是保存用户,至于什么校验丢给校验的方法处理,什么发送消息丢给发送消息处理,这些就不能耦合在一起。

  • 对于看到一些 if 下面一丢逻辑,然后 if 再一丢逻辑,看代码时很多情况不需要知道这个逻辑怎么实现的,知道入参出参就大概这里做什么了。即使想知道详细情况点进去就知道了。突出这个当前方法要做的事情就好了。

  • 阿里的开发手册就推荐一个方法不能超过 80 行,超过可以根据业务具体调整一下。


作者:小塵
来源:juejin.cn/post/7357172505961578511
收起阅读 »

Android:解放自己的双手,无需手动创建shape文件

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。 现在的移动应用中为了美化界面,会给各类视图增加一些圆角、描边、渐变等等效果。当然系统也提供了对应的功能,那就是创建shape标签的XML文件,例如下图就是创建一个圆角为10dp,填充...
继续阅读 »

大家好,我是似曾相识2022。不喜欢唱跳篮球,但对杰伦的Rap却情有独钟。



现在的移动应用中为了美化界面,会给各类视图增加一些圆角、描边、渐变等等效果。当然系统也提供了对应的功能,那就是创建shape标签的XML文件,例如下图就是创建一个圆角为10dp,填充是白色的shape文件。再把这个文件设置给目标视图作为背景,就达到了我们想要的圆角效果。


<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="#FFFFFF" />
</shape>

//圆角效果
android:background="@drawable/shape_white_r10"

但不是所有的圆角和颜色都一样,甚至还有四个角单独一个有圆角的情况,当然还有描边、虚线描边、渐变填充色等等各类情况。随着页面效果的多样和复杂性,我们添加的shape文件也是成倍增加。


这时候不少的技术大佬出现了,大佬们各显神通打造了许多自定义View。这样我们就可以使用三方库通过在目标视图外嵌套一层视图来达到原本的圆角等效果。不得不说,这确实能够大大减少我们手动创建各类shape的情况,使用起来也是得心应手,方便了不少。


问题:


简单的布局,嵌套层级较少的页面使用起来还好。但往往随着页面的复杂程度越高,嵌套层级也越来多,这个时候再使用三方库外层嵌套视图会越来越臃肿和复杂。那么有没有一种方式可以直接在XML中当前视图中增减圆角等效果呢?


还真有,使用DataBinding可以办到!


这里就不单独介绍DataBinding的基础配置,网上一搜到处都是。咱们直接进入正题,使用**@BindingAdapter** 注解,这是用来扩展布局XML属性行为的注解。


使用DataBinding实现圆角


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = ["shape_radius""shape_solidColor"])
fun View.setViewBackground(radius: Int = 0,solidColor: Int = Color.TRANSPARENT){
val drawable = GradientDrawable()
drawable.cornerRadius = context.dp2px(radius.toFloat()).toFloat()
drawable.setColor(solidColor)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_solidColor="@{@color/white}"

其实就是对当前视图的一个扩展,有点和kotlin的扩展函数类似。既然这样我们可以通过代码配置更多自定义的属性:


各方向圆角的实现:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = ["
"shape_solidColor",//填充颜色
"shape_tl_radius",//上左圆角
"shape_tr_radius",//上右圆角
"shape_bl_radius",//下左圆角
"shape_br_radius"//下右圆角
])
fun View.setViewBackground(radius: Int = 0,solidColor: Int = Color.TRANSPARENT){
val drawable = GradientDrawable()
drawable.setColor(solidColor)
drawable.cornerRadii = floatArrayOf(
context.dp2px(shape_tl_radius.toFloat()).toFloat(),
context.dp2px(shape_tl_radius.toFloat()).toFloat(),
context.dp2px(shape_tr_radius.toFloat()).toFloat(),
context.dp2px(shape_tr_radius.toFloat()).toFloat(),
context.dp2px(shape_br_radius.toFloat()).toFloat(),
context.dp2px(shape_br_radius.toFloat()).toFloat(),
context.dp2px(shape_bl_radius.toFloat()).toFloat(),
context.dp2px(shape_bl_radius.toFloat()).toFloat(),
)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_tl_radius="@{@color/white}"//左上角
shape_tr_radius="@{@color/white}"//右上角
shape_bl_radius="@{@color/white}"//左下角
shape_br_radius="@{@color/white}"//右下角

虚线描边:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = [
"shape_radius"
"shape_solidColor"
"shape_strokeWitdh",//描边宽度
"shape_dashWith",//描边虚线单个宽度
"shape_dashGap",//描边间隔宽度
])
fun View.setViewBackground(
radius: Int = 0,
solidColor: Int = Color.TRANSPARENT,
strokeWidth: Int = 0,
shape_dashWith: Int = 0,
shape_dashGap: Int = 0
){
val drawable = GradientDrawable()
drawable.setStroke(
context.dp2px(strokeWidth.toFloat()),
strokeColor,
shape_dashWith.toFloat(),
shape_dashGap.toFloat()
)
drawable.setColor(solidColor)
background = drawable
}

//xml文件中
shape_radius="@{10}"
shape_solidColor="@{@color/white}"
strokeWidth="@{1}"
shape_dashWith="@{2}"
shape_dashGap="@{3}"

渐变色的使用:


//自定义shape_radius、shape_solidColor字段  即圆角和填充颜色
@BindingAdapter(value = [
"shape_startColor",//渐变开始颜色
"shape_centerColor",//渐变中间颜色
"shape_endColor",//渐变结束颜色
"shape_gradualOrientation",//渐变角度
])
fun View.setViewBackground(
shape_startColor: Int = Color.TRANSPARENT,
shape_centerColor: Int = Color.TRANSPARENT,
shape_endColor: Int = Color.TRANSPARENT,
shape_gradualOrientation: Int = 1,//TOP_BOTTOM = 1 ,TR_BL = 2,RIGHT_LEFT = 3,BR_TL = 4,BOTTOM_TOP = 5,BL_TR = 6,LEFT_RIGHT = 7,TL_BR = 8
){
val drawable = GradientDrawable()
when (shape_gradualOrientation) {
1 -> drawable.orientation = GradientDrawable.Orientation.TOP_BOTTOM
2 -> drawable.orientation = GradientDrawable.Orientation.TR_BL
3 -> drawable.orientation = GradientDrawable.Orientation.RIGHT_LEFT
4 -> drawable.orientation = GradientDrawable.Orientation.BR_TL
5 -> drawable.orientation = GradientDrawable.Orientation.BOTTOM_TOP
6 -> drawable.orientation = GradientDrawable.Orientation.BL_TR
7 -> drawable.orientation = GradientDrawable.Orientation.LEFT_RIGHT
8 -> drawable.orientation = GradientDrawable.Orientation.TL_BR
}
drawable.gradientType = GradientDrawable.LINEAR_GRADIENT//线性
drawable.shape = GradientDrawable.RECTANGLE//矩形方正
drawable.colors = if (shape_centerColor != Color.TRANSPARENT) {//有中间色
intArrayOf(
shape_startColor,
shape_centerColor,
shape_endColor
)
} else {
intArrayOf(shape_startColor, shape_endColor)
}//渐变色
background = drawable
}

//xml文件中
shape_startColor="@{@color/cl_F1E6A0}"
shape_centerColor="@{@color/cl_F8F8F8}"
shape_endColor=@{@color/cl_3CB9FF}

不止设置shape功能,只要可以通过代码设置的功能一样可以在BindingAdapter注解中自定义,使用起来是不是更加方便了。


总结:



  • 注解BindingAdapter中value数组的自定义属性一样要和方法内的参数一一对应,否则会报错。

  • 布局中使用该自定义属性时需要将布局文件最外层修改为layout标签

  • XML中使用自定义属性时一定要添加@{}


好了,以上便是解放自己的双手,无需手动创建shape文件的全部内容,希望能给大家带来帮助!


作者:似曾相识2022
来源:juejin.cn/post/7278858311596359739
收起阅读 »

Android — 实现同意条款功能

在开发App时,让用户知晓并同意隐私政策、服务协议等条款是必不可少的功能,恰逢Facebook最近对隐私政策的审核愈发严格,本文介绍如何通过TextView和ClickableSpan简单快速的实现同意条款功能。 下面是掘金(小米应用商店下载)和Github(...
继续阅读 »

在开发App时,让用户知晓并同意隐私政策、服务协议等条款是必不可少的功能,恰逢Facebook最近对隐私政策的审核愈发严格,本文介绍如何通过TextViewClickableSpan简单快速的实现同意条款功能。


下面是掘金(小米应用商店下载)和Github(Google Play下载)两个App的同意条款功能示例图:


掘金Github
image.pngimage.png

可以看见二者有所区别,这是由于国内政策不允许App自动勾选同意,必须要用户手动勾选,如果不按照要求处理甚至无法上架。


实现同意条款功能


先梳理一下实现同意条款功能的核心需求:



  1. 可以根据上架的区域不同(是否海外)决定是否显示勾选框,显示勾选框时需要提供可以获取选中状态的方法。

  2. 同意条款的提示中可能仅包含单个条款或同时包含多个条款。

  3. 条款名的颜色需要与其他文案不同,可以根据需求决定是否显示下划线,点击条款名时可以查看条款的具体内容。


上述三项需求最关键,但可以调整的细节有很多,本文仅通过较为简单的方式来实现(不使用自定义View),各位读者可以根据实际项目需求进行调整。


自定义配置类


上面的三点需求中都包含了一些配置项,可以通过配置类来管理这些参数,根据外部设定的配置进行相应处理。示例代码如下:


class ConfirmTermsConfiguration private constructor() {

// 同意提示文案
var confirmTipsContent: String = ""
private set

// 可点击的条款文案,键为条款文案,值为条款内容(链接)
var clickableTerms = ArrayMap<String, String>()
private set

// 同意条款控件距离底部的距离,默认为32dp
// 左右两侧的边距可以根据实际需求决定是否需要提供配置方法
var viewBottomMargin = DensityUtil.dp2Px(36)
private set

// 文字大小,默认14sp
var textSize = 14f
private set

// 文字颜色,默认黑色
var textColor = android.R.color.black
private set

// 可点击文字的颜色,默认为蓝色
var clickableTextColor = R.color.color_blue_229CE9
private set

// 是否显示下滑线,默认不显示
var showUnderline = false
private set

// 是否显示勾选框,默认为false
// 示例中勾选框直接使用可点击文案的颜色
// 可以根据实际需求决定是否提供相应的配置方法
var showCheckbox = false
private set

class Builder() {
private var confirmTipsContent: String = ""
private val clickableTerms = ArrayMap<String, String>()
private var viewBottomMargin = DensityUtil.dp2Px(36)
private var textSize = 14f
private var textColor = android.R.color.black
private var clickableTextColor = R.color.color_blue_229CE9
private var showUnderline = false
private var showCheckbox = false

fun setConfirmTipContent(confirmTipsContent: String): Builder {
this.confirmTipsContent = confirmTipsContent
return this
}

fun setClickableTerm(clickableTerm: String, termsLink: String): Builder {
clickableTerms.clear()
clickableTerms[clickableTerm] = termsLink
return this
}

fun addClickableTerms(clickableTerms: Map<String, String>): Builder {
this.clickableTerms.clear()
this.clickableTerms.putAll(clickableTerms)
return this
}

fun setViewBottomMargin(viewBottomMargin: Int): Builder {
this.viewBottomMargin = viewBottomMargin
return this
}

fun setTextSize(textSize: Float): Builder {
this.textSize = textSize
return this
}

fun setTextColor(textColor: Int): Builder {
this.textColor = textColor
return this
}

fun setClickableTextColor(clickableTextColor: Int): Builder {
this.clickableTextColor = clickableTextColor
return this
}

fun setShowUnderline(showUnderline: Boolean): Builder {
this.showUnderline = showUnderline
return this
}

fun setShowCheckbox(showCheckbox: Boolean): Builder {
this.showCheckbox = showCheckbox
return this
}

fun build(): ConfirmTermsConfiguration {
return ConfirmTermsConfiguration().also {
it.confirmTipsContent = confirmTipsContent
it.clickableTerms = clickableTerms
it.viewBottomMargin = viewBottomMargin
it.textSize = textSize
it.textColor = textColor
it.clickableTextColor = clickableTextColor
it.showUnderline = showUnderline
it.showCheckbox = showCheckbox
}
}
}
}

自定义ClickSpan


ClickSpan是Android中专门处理可点击文本的类,继承ClickSpan类可以实现定制可点击文本的样式以及响应事件。可以使用自定义ClickSpan来实现第三点需求,示例代码如下:


class ClickSpan(
// 默认颜色为白色
private var colorRes: Int = -1,
// 默认不显示下划线
private var isShoeUnderLine: Boolean = false,
// 点击事件监听,必须传入
private var clickListener: () -> Unit
) : ClickableSpan() {

override fun onClick(widget: View) {
// 回调点击事件监听
clickListener.invoke()
}

override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
//设置文本颜色
ds.color = colorRes
//设置是否显示下划线
ds.isUnderlineText = isShoeUnderLine
}
}


显示、隐藏同意条款控件


有了配置类和自定义ClickSpan类之后,就可以实现显示、隐藏同意条款控件了,示例代码如下:



  • 辅助类


class ConfirmTermsHelper {

private var confirmTermsView: View? = null

var confirmStatus = false
private set

fun showConfirmTermsView(activity: Activity, confirmTermsConfiguration: ConfirmTermsConfiguration) {
val confirmTipsContent = confirmTermsConfiguration.confirmTipsContent
val clickableTerms = confirmTermsConfiguration.clickableTerms
val showCheckBox = confirmTermsConfiguration.showCheckbox
// 同意条款的提示文案为空直接结束方法执行
if (confirmTipsContent.isEmpty()) {
return
}
// 先把当前的控件移除
hideConfirmTermsView()
activity.runOnUiThread {
if (showCheckBox) {
ConstraintLayout(activity).apply {
// 代码中创建CheckBox存在Padding,暂时未解决
addView(AppCompatCheckBox(activity).apply {
id = R.id.cb_confirm_terms
val checkboxSize = DensityUtil.dp2Px(30)
layoutParams = ConstraintLayout.LayoutParams(checkboxSize, checkboxSize).apply {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
startToStart = ConstraintLayout.LayoutParams.PARENT_ID
}
setButtonDrawable(R.drawable.selector_confirm_terms_chekcbox)
buttonTintList = ColorStateList.valueOf(ContextCompat.getColor(activity, confirmTermsConfiguration.clickableTextColor))
setOnCheckedChangeListener { _, isChecked ->
confirmStatus = isChecked
}
})
addView(AppCompatTextView(activity).apply {
id = R.id.tv_confirm_terms
layoutParams = ConstraintLayout.LayoutParams(0, ConstraintLayout.LayoutParams.WRAP_CONTENT).apply {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
startToEnd = R.id.cb_confirm_terms
endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
marginStart = DensityUtil.dp2Px(10)
}
textSize = confirmTermsConfiguration.textSize
setTextColor(ContextCompat.getColor(activity, confirmTermsConfiguration.textColor))
movementMethod = LinkMovementMethodCompat.getInstance()
text = SpannableStringBuilder(confirmTipsContent).apply {
clickableTerms.entries.forEach { clickableTermEntry ->
val startHighlightIndex = confirmTipsContent.indexOf(clickableTermEntry.key)
if (startHighlightIndex > 0) {
setSpan(
ClickSpan(ContextCompat.getColor(activity, confirmTermsConfiguration.clickableTextColor), confirmTermsConfiguration.showUnderline) {
// 通过CustomTab打开链接
CustomTabHelper.openSimpleCustomTab(activity, clickableTermEntry.value)
},
startHighlightIndex, startHighlightIndex + clickableTermEntry.key.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
})
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM).apply {
val defaultLeftRightSpace = DensityUtil.dp2Px(20)
marginStart = defaultLeftRightSpace
marginEnd = defaultLeftRightSpace
bottomMargin = confirmTermsConfiguration.viewBottomMargin
}
}
} else {
AppCompatTextView(activity).apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM).apply {
val defaultLeftRightSpace = DensityUtil.dp2Px(20)
marginStart = defaultLeftRightSpace
marginEnd = defaultLeftRightSpace
bottomMargin = confirmTermsConfiguration.viewBottomMargin
}
textSize = confirmTermsConfiguration.textSize
setTextColor(ContextCompat.getColor(activity, confirmTermsConfiguration.textColor))
movementMethod = LinkMovementMethodCompat.getInstance()
text = SpannableStringBuilder(confirmTipsContent).apply {
clickableTerms.entries.forEach { clickableTermEntry ->
val startHighlightIndex = confirmTipsContent.indexOf(clickableTermEntry.key)
if (startHighlightIndex > 0) {
setSpan(
ClickSpan(ContextCompat.getColor(activity, confirmTermsConfiguration.clickableTextColor), confirmTermsConfiguration.showUnderline) {
// 通过CustomTab打开链接
CustomTabHelper.openSimpleCustomTab(activity, clickableTermEntry.value)
},
startHighlightIndex, startHighlightIndex + clickableTermEntry.key.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
}
}.run {
confirmTermsView = this
removeViewInParent(this)
getRootView(activity).addView(this)
}
}
}

fun hideConfirmTermsView() {
confirmStatus = false
confirmTermsView?.run { post { removeViewInParent(this) } }
confirmTermsView = null
}

private fun getRootView(activity: Activity): FrameLayout {
return activity.findViewById(android.R.id.content)
}

private fun removeViewInParent(targetView: View) {
try {
(targetView.parent as? ViewGr0up)?.removeView(targetView)
} catch (e: Exception) {
e.printStackTrace()
}
}
}


  • 示例页面


class ConfirmTermsExampleActivity : AppCompatActivity() {

private lateinit var binding: LayoutConfirmTermsExampleActivityBinding

private val confirmTermsHelper = ConfirmTermsHelper()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutConfirmTermsExampleActivityBinding.inflate(layoutInflater).apply {
setContentView(root)
}

binding.btnWithCheckBox.setOnClickListener {
confirmTermsHelper.showConfirmTermsView(this, ConfirmTermsConfiguration.Builder()
.setConfirmTipContent("已阅读并同意\"隐私政策\"")
.setClickableTerm("隐私政策", "https://lf3-cdn-tos.draftstatic.com/obj/ies-hotsoon-draft/juejin/7b28b328-1ae4-4781-8d46-430fef1b872e.html")
.setShowCheckbox(true)
.setTextColor(R.color.color_gray_999)
.setClickableTextColor(R.color.color_black_3B3946)
.build())
binding.btnGetConfirmStatus.visibility = View.VISIBLE
}
binding.btnWithoutCheckBox.setOnClickListener {
confirmTermsHelper.showConfirmTermsView(this, ConfirmTermsConfiguration.Builder()
.setConfirmTipContent("By signing in you accept out Terms of use and Privacy policy")
.addClickableTerms(
mapOf(
Pair("Terms of use", "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service"),
Pair("Privacy policy", "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement")
)
)
.setShowUnderline(true)
.setTextColor(R.color.color_gray_999)
.build())
binding.btnGetConfirmStatus.visibility = View.GONE
}
binding.btnGetConfirmStatus.setOnClickListener {
showSnackbar("Current confirm status:${confirmTermsHelper.confirmStatus}")
}
}

private fun showSnackbar(message: String) {
runOnUiThread {
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
}
}

override fun onDestroy() {
super.onDestroy()
confirmTermsHelper.hideConfirmTermsView()
}
}

效果演示与完整示例代码


最终演示效果如下:


Screen_recording_202 -original-original.gif

所有演示代码已在示例Demo中添加。


ExampleDemo github


ExampleDemo gitee


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

来,实现一下这个报表功能,速度要快,要嘎嘎快

我们有一段业务,类似一个报表,就是获取用户的订单汇总,邮费汇总,各种手续费汇总,然后拿时间噶一卡,显示在页面。 但是呢,这几个业务没啥实际关系,数据也是分开的,一个一个获取会有点慢,我开始就是这样写的,老板嫌页面太慢,让我改,可是页面反应慢,关我后端程序什么事...
继续阅读 »

我们有一段业务,类似一个报表,就是获取用户的订单汇总,邮费汇总,各种手续费汇总,然后拿时间噶一卡,显示在页面。


但是呢,这几个业务没啥实际关系,数据也是分开的,一个一个获取会有点慢,我开始就是这样写的,老板嫌页面太慢,让我改,可是页面反应慢,关我后端程序什么事,哥哥别打了,错了错了,我改,我改。那么最好的方案就是多线程分别获取然后汇总到一起返回。


在Java中获取异步线程的结果通常可以使用FutureCallableCompletableFutureFutureTask等类来实现。这些类可以用来提交任务到线程池,并在任务完成后获取结果。这就是我们想要的结果,那么这里来深入研究分析一下这三个方案。


使用FutureCallable


package com.luke.designpatterns.demo;

import java.util.concurrent.*;

public class demo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
// 获取各种汇总的代码,返回结果
return 42;
}
});
// 获取异步任务的结果
Integer result = future.get();
System.out.println("异步任务的结果是" + result);
executor.shutdown();
}
}

image.png


它们的原理是通过将任务提交到线程池执行,同时返回一个Future对象,该对象可以在未来的某个时刻获取任务的执行结果。



  1. Callable 接口Callable 是一个带泛型的接口,它允许你定义一个返回结果的任务,并且可以抛出异常。这个接口只有一个方法 call(),在该方法中编写具体的任务逻辑。

  2. Future 接口Future 接口代表一个异步计算的结果。它提供了方法来检查计算是否完成、等待计算的完成以及检索计算的结果。Future 提供了一个 get() 方法,它会阻塞当前线程直到计算完成,并返回计算的结果。



Callable 接口本身并不直接启动线程,它只是定义了一个可以返回结果的任务。要启动一个 Callable 实例的任务,通常需要将其提交给 ExecutorService 线程池来执行。



ExecutorService 中,可以使用 submit(Callable<T> task) 方法提交 Callable 任务。这个方法会返回一个 Future 对象,它可以用来获取任务的执行结果。


启动 Callable 任务的原理可以概括为以下几个步骤:



  1. 创建 Callable 实例:首先需要创建一个实现了 Callable 接口的类,并在 call() 方法中定义具体的任务逻辑,包括要执行的代码和返回的结果。

  2. 创建 ExecutorService 线程池:使用 Executors 类的工厂方法之一来创建一个 ExecutorService 线程池,例如 newFixedThreadPool(int nThreads)newCachedThreadPool() 等。

  3. 提交任务:将 Callable 实例通过 ExecutorServicesubmit(Callable<T> task) 方法提交到线程池中执行。线程池会为任务分配一个线程来执行。

  4. 异步执行ExecutorService 线程池会在后台异步执行任务,不会阻塞当前线程,使得主线程可以继续执行其他操作。

  5. 获取结果:通过 Future 对象的 get() 方法获取任务的执行结果。如果任务尚未完成,get() 方法会阻塞当前线程直到任务完成并返回结果。


总的来说,Callable 启动线程的原理是将任务提交给 ExecutorService 线程池,线程池会负责管理线程的执行,执行任务的过程是在独立的线程中进行的,从而实现了异步执行的效果。


使用CompletableFuture


import java.util.concurrent.CompletableFuture;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// 获取各种汇总的代码,返回结果
return 43;
});

// 获取异步任务的结果
Integer result = future.get();

System.out.println("异步任务的结果:" + result);
}
}

image.png


CompletableFuture 是 Java 8 引入的一个类,用于实现异步编程和异步任务的组合。它的原理是基于"Completable"(可以完成的)和"Future"(未来的结果)的概念,提供了一种方便的方式来处理异步任务的执行和结果处理。


CompletableFuture 的原理可以简单概括为以下几点:



  1. 异步执行CompletableFuture 允许你以异步的方式执行任务。你可以使用 supplyAsync()runAsync() 等方法提交一个任务给 CompletableFuture 执行,任务会在一个独立的线程中执行,不会阻塞当前线程。

  2. 回调机制CompletableFuture 提供了一系列的方法来注册回调函数,这些回调函数会在任务执行完成时被调用。例如,thenApply(), thenAccept(), thenRun() 等方法可以分别处理任务的结果、完成时的操作以及任务执行异常时的处理。

  3. 组合多个任务CompletableFuture 支持多个任务的组合,可以使用 thenCombine()thenCompose()thenAcceptBoth() 等方法来组合多个任务,实现任务之间的依赖关系。

  4. 异常处理CompletableFuture 允许你对任务执行过程中抛出的异常进行处理,可以使用 exceptionally()handle() 等方法来处理异常情况。

  5. 等待任务完成:与 Future 类似,CompletableFuture 也提供了 get() 方法来等待任务的完成并获取结果。但与传统的 Future 不同,CompletableFutureget() 方法不会阻塞当前线程,因为任务的执行是异步的。


总的来说,CompletableFuture 的原理是基于回调和异步执行的机制,提供了一种方便的方式来处理异步任务的执行和结果处理,同时支持任务的组合和异常处理。


使用FutureTask


import java.util.concurrent.*;

public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
// 获取各种汇总的代码,返回结果
return 44;
});

Thread thread = new Thread(futureTask);
thread.start();

// 获取异步任务的结果
Integer result = futureTask.get();
System.out.println("异步任务的结果:" + result);
}
}

image.png


FutureTask 是 Java 中实现 Future 接口的一个基本实现类,同时也实现了 Runnable 接口,因此可以被用作一个可运行的任务。FutureTask 的原理是将一个可调用的任务(CallableRunnable)封装成一个异步的、可取消的任务,它提供了一个机制来获取任务的执行结果。


FutureTask 的原理可以简要概括如下:



  1. 封装任务FutureTask 接受一个 CallableRunnable 对象作为构造函数的参数,并将其封装成一个异步的任务。

  2. 执行任务FutureTask 实现了 Runnable 接口,因此可以作为一个可运行的任务提交给 Executor(通常是 ExecutorService)来执行。当 FutureTask 被提交到线程池后,线程池会在一个独立的线程中执行该任务。

  3. 获取结果:通过 Future 接口的方法,可以等待任务执行完成并获取其结果。FutureTask 实现了 Future 接口,因此可以调用 get() 方法来获取任务的执行结果。如果任务尚未完成,get() 方法会阻塞当前线程直到任务完成并返回结果。

  4. 取消任务FutureTask 提供了 cancel(boolean mayInterruptIfRunning) 方法来取消任务的执行。可以选择是否中断正在执行的任务。一旦任务被取消,get() 方法会立即抛出 CancellationException 异常。


总的来说,FutureTask 的原理是将一个可调用的任务封装成一个异步的、可取消的任务,并通过 Future 接口来提供获取任务执行结果和取消任务的机制。


这些方法中,get()方法会阻塞当前线程,直到异步任务完成并返回结果。如果任务抛出异常,get()方法会将异常重新抛出。


我们平时常用的方法就是这四种,都能实现我的需求,随便找一个哐哐干上去就好啦。


作者:奔跑的毛球
来源:juejin.cn/post/7350557995895701531
收起阅读 »

我的又一个神奇的框架——Skins换肤框架

为什么会有换肤的需求 app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。 换肤是什么 换...
继续阅读 »

为什么会有换肤的需求


app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。


换肤是什么


换肤是将app的背景色、文字颜色以及资源图片,一键进行全部切换的过程。这里就包括了图片资源和颜色资源。


Skins怎么使用


Skins就是一个解决这样一种换肤需求的框架。


// 添加以下代码到项目根目录下的build.gradle
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
// 添加以下代码到app模块的build.gradle
dependencies {
// skins依赖了dora框架,所以你也要implementation dora
implementation("com.github.dora4:dora:1.1.12")
implementation 'com.github.dora4:dview-skins:1.4'
}

我以更换皮肤颜色为例,打开res/colors.xml。


<!-- 需要换肤的颜色 -->
<color name="skin_theme_color">@color/cyan</color>
<color name="skin_theme_color_red">#d23c3e</color>
<color name="skin_theme_color_orange">#ff8400</color>
<color name="skin_theme_color_black">#161616</color>
<color name="skin_theme_color_green">#009944</color>
<color name="skin_theme_color_blue">#0284e9</color>
<color name="skin_theme_color_cyan">@color/cyan</color>
<color name="skin_theme_color_purple">#8c00d6</color>

将所有需要换肤的颜色,添加skin_前缀和_skinname后缀,不加后缀的就是默认皮肤。
然后在启动页应用预设的皮肤类型。在布局layout文件中使用默认皮肤的资源名称,像这里就是R.color.skin_theme_color,框架会自动帮你替换。要想让框架自动帮你替换,你需要让所有要换肤的Activity继承BaseSkinActivity。


private fun applySkin() {
val manager = PreferencesManager(this)
when (manager.getSkinType()) {
0 -> {
}
1 -> {
SkinManager.changeSkin("cyan")
}
2 -> {
SkinManager.changeSkin("orange")
}
3 -> {
SkinManager.changeSkin("black")
}
4 -> {
SkinManager.changeSkin("green")
}
5 -> {
SkinManager.changeSkin("red")
}
6 -> {
SkinManager.changeSkin("blue")
}
7 -> {
SkinManager.changeSkin("purple")
}
}
}

另外还有一个情况是在代码中使用换肤,那么跟布局文件中定义是有一些区别的。


val skinThemeColor = SkinManager.getLoader().getColor("skin_theme_color")

这个skinThemeColor拿到的就是当前皮肤下的真正的skin_theme_color颜色,比如R.color.skin_theme_color_orange的颜色值“#ff8400”或R.id.skin_theme_color_blue的颜色值“#0284e9”。
SkinLoader还提供了更简洁设置View颜色的方法。


override fun setImageDrawable(imageView: ImageView, resName: String) {
val drawable = getDrawable(resName) ?: return
imageView.setImageDrawable(drawable)
}

override fun setBackgroundDrawable(view: View, resName: String) {
val drawable = getDrawable(resName) ?: return
view.background = drawable
}

override fun setBackgroundColor(view: View, resName: String) {
val color = getColor(resName)
view.setBackgroundColor(color)
}

框架原理解析


先看BaseSkinActivity的源码。


package dora.skin.base

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
import android.view.View
import androidx.collection.ArrayMap
import androidx.core.view.LayoutInflaterCompat
import androidx.core.view.LayoutInflaterFactory
import androidx.databinding.ViewDataBinding
import dora.BaseActivity
import dora.skin.SkinManager
import dora.skin.attr.SkinAttr
import dora.skin.attr.SkinAttrSupport
import dora.skin.attr.SkinView
import dora.skin.listener.ISkinChangeListener
import dora.util.LogUtils
import dora.util.ReflectionUtils
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*

abstract class BaseSkinActivity<T : ViewDataBinding> : BaseActivity<T>(),
ISkinChangeListener, LayoutInflaterFactory {

private val constructorArgs = arrayOfNulls<Any>(2)

override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
if (createViewMethod == null) {
val methodOnCreateView = ReflectionUtils.findMethod(delegate.javaClass, false,
"createView", *createViewSignature)
createViewMethod = methodOnCreateView
}
var view: View? = ReflectionUtils.invokeMethod(delegate, createViewMethod, parent, name,
context, attrs) as View?
if (view == null) {
view = createViewFromTag(context, name, attrs)
}
val skinAttrList = SkinAttrSupport.getSkinAttrs(attrs, context)
if (skinAttrList.isEmpty()) {
return view
}
injectSkin(view, skinAttrList)
return view
}

private fun injectSkin(view: View?, skinAttrList: MutableList<SkinAttr>) {
if (skinAttrList.isNotEmpty()) {
var skinViews = SkinManager.getSkinViews(this)
if (skinViews == null) {
skinViews = arrayListOf()
}
skinViews.add(SkinView(view, skinAttrList))
SkinManager.addSkinView(this, skinViews)
if (SkinManager.needChangeSkin()) {
SkinManager.apply(this)
}
}
}

private fun createViewFromTag(context: Context, viewName: String, attrs: AttributeSet): View? {
var name = viewName
if (name == "view") {
name = attrs.getAttributeValue(null, "class")
}
return try {
constructorArgs[0] = context
constructorArgs[1] = attrs
if (-1 == name.indexOf('.')) {
// try the android.widget prefix first...
createView(context, name, "android.widget.")
} else {
createView(context, name, null)
}
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
} finally {
// Don't retain references on context.
constructorArgs[0] = null
constructorArgs[1] = null
}
}

@Throws(InflateException::class)
private fun createView(context: Context, name: String, prefix: String?): View? {
var constructor = constructorMap[name]
return try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
val clazz = context.classLoader.loadClass(
if (prefix != null) prefix + name else name).asSubclass(View::class.java)
constructor = clazz.getConstructor(*constructorSignature)
constructorMap[name] = constructor
}
constructor!!.isAccessible = true
constructor.newInstance(*constructorArgs)
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
val layoutInflater = LayoutInflater.from(this)
LayoutInflaterCompat.setFactory(layoutInflater, this)
super.onCreate(savedInstanceState)
SkinManager.addListener(this)
}

override fun onDestroy() {
super.onDestroy()
SkinManager.removeListener(this)
}

override fun onSkinChanged(suffix: String) {
SkinManager.apply(this)
}

companion object {
val constructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
private val constructorMap: MutableMap<String, Constructor<out View>> = ArrayMap()
private var createViewMethod: Method? = null
val createViewSignature = arrayOf(View::class.java, String::class.java,
Context::class.java, AttributeSet::class.java)
}
}

我们可以看到BaseSkinActivity继承自dora.BaseActivity,所以dora框架是必须要依赖的。有人说,那我不用dora框架的功能,可不可以不依赖dora框架?我的回答是,不建议。Skins对Dora生命周期注入特性采用的是,依赖即配置。


package dora.lifecycle.application

import android.app.Application
import android.content.Context
import dora.skin.SkinManager

class SkinsAppLifecycle : ApplicationLifecycleCallbacks {

override fun attachBaseContext(base: Context) {
}

override fun onCreate(application: Application) {
SkinManager.init(application)
}

override fun onTerminate(application: Application) {
}
}

所以你无需手动配置<meta-data android:name="dora.lifecycle.config.SkinsGlobalConfig" android:value="GlobalConfig"/>,Skins已经自动帮你配置好了。那么我顺便问个问题,BaseSkinActivity中最关键的一行代码是哪行?LayoutInflaterCompat.setFactory(layoutInflater, this)这行代码是整个换肤流程最关键的一行代码。我们来干预一下所有Activity onCreateView时的布局加载过程。我们在SkinAttrSupport.getSkinAttrs中自己解析了AttributeSet。


    /**
* 从xml的属性集合中获取皮肤相关的属性。
*/

fun getSkinAttrs(attrs: AttributeSet, context: Context): MutableList<SkinAttr> {
val skinAttrs: MutableList<SkinAttr> = ArrayList()
var skinAttr: SkinAttr
for (i in 0 until attrs.attributeCount) {
val attrName = attrs.getAttributeName(i)
val attrValue = attrs.getAttributeValue(i)
val attrType = getSupportAttrType(attrName) ?: continue
if (attrValue.startsWith("@")) {
val ref = attrValue.substring(1)
if (TextUtils.isEqualTo(ref, "null")) {
// 跳过@null
continue
}
val id = ref.toInt()
// 获取资源id的实体名称
val entryName = context.resources.getResourceEntryName(id)
if (entryName.startsWith(SkinConfig.ATTR_PREFIX)) {
skinAttr = SkinAttr(attrType, entryName)
skinAttrs.add(skinAttr)
}
}
}
return skinAttrs
}

我们只干预skin_开头的资源的加载过程,所以解析得到我们需要的属性,最后得到SkinAttr的列表返回。


package dora.skin.attr

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import dora.skin.SkinLoader
import dora.skin.SkinManager

enum class SkinAttrType(var attrType: String) {

/**
* 背景属性。
*/

BACKGROUND("background") {
override fun apply(view: View, resName: String) {
val drawable = loader.getDrawable(resName)
if (drawable != null) {
view.setBackgroundDrawable(drawable)
} else {
val color = loader.getColor(resName)
view.setBackgroundColor(color)
}
}
},

/**
* 字体颜色。
*/

TEXT_COLOR("textColor") {
override fun apply(view: View, resName: String) {
val colorStateList = loader.getColorStateList(resName) ?: return
(view as TextView).setTextColor(colorStateList)
}
},

/**
* 图片资源。
*/

SRC("src") {
override fun apply(view: View, resName: String) {
if (view is ImageView) {
val drawable = loader.getDrawable(resName) ?: return
view.setImageDrawable(drawable)
}
}
};

abstract fun apply(view: View, resName: String)

/**
* 获取资源管理器。
*/

val loader: SkinLoader
get() = SkinManager.getLoader()
}

当前skins框架只定义了几种主要的换肤属性,你理解原理后,也可以自己进行扩展,比如RadioButton的button属性等。


开源项目传送门


如果你要深入理解完整的换肤流程,请阅读skins的源代码,[github.com/dora4/dview…] 。


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

完美代替节假日API,查询中国特色日期

web
马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kb,gzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。 关于中国节假日,后面会...
继续阅读 »

马上端午节到了,趁着前夕,写了个关于中国特色日期查询的库;由于中国的节假日不固定,以及阴历日期较为特殊,尽可能的做了代码上压缩,做到了 .min.js 源文件尺寸小于 16kbgzip 压缩后只有 7kb 大小,欢迎大家 PR 使用。


关于中国节假日,后面会跟随国务院发布进行更新,一言既出,驷马难追。


当前版本


NPM Version
GitHub License
README


项目地址:github.com/vsme/chines…


提供的功能



  1. 中国节假日(含调休日、工作日)查询,支持 2004年 至 2024年,包括 2020年 的春节延长;

  2. 24节气查询;

  3. 农历 阳历 互相转换,含有生肖和天干地支。


还需要的功能欢迎补充。


对于非 JS 语言


提供了中国节假日的 JSON 文件,通过链接 chinese-days.json 可以直接引用。


https://cdn.jsdelivr.net/npm/chinese-days/dist/chinese-days.json


快速开始


推荐方式:直接浏览器引入,会跟随国务院发布更新。


<script src="https://cdn.jsdelivr.net/npm/chinese-days/dist/index.min.js"></script>
<script>
const { isHoliday } = chineseDays
console.log(isHoliday('2024-01-01'))
</script>

其他方式安装


npm i chinese-days

使用 ESM 导入


import chineseDays from 'chinese-days'
console.log(chineseDays)

在 Node 中使用


const { isWorkday, isHoliday } = require('chinese-days');
console.log(isWorkday('2020-01-01'));
console.log(isHoliday('2020-01-01'));

节假日模块


isWorkday 检查某个日期是否为工作日


console.log(isWorkday('2023-01-01')); // false

isHoliday 检查某个日期是否为节假日


console.log(isHoliday('2023-01-01')); // true

isInLieu 检查某个日期是否为调休日(in lieu day)


在中国的节假日安排中,调休日是为了连休假期或补班而调整的工作日或休息日。例如,当某个法定假日与周末相连时,可能会将某个周末调整为工作日,或者将某个工作日调整为休息日,以便连休更多天。


// 检查 2024-05-02 返回 `true` 则表示是一个调休日。
console.log(isInLieu('2024-05-02')); // true

// 检查 2024-05-01 返回 `false` 则表示不是一个调休日。
console.log(isInLieu('2024-05-01')); // false

getDayDetail 检查指定日期是否是工作日


函数用于检查指定日期是否是工作日,并返回一个是否工作日的布尔值和日期的详情。



  1. 如果指定日期是工作日,则返回 true 和工作日名称,如果是被调休的工作日,返回 true 和节假日详情。

  2. 如果是节假日,则返回 false 和节假日详情。


// 示例用法

// 正常工作日 周五
console.log(getDayDetail('2024-02-02')); // { "date": "2024-02-02", "work":true,"name":"Friday"}
// 节假日 周末
console.log(getDayDetail('2024-02-03')); // { "date": "2024-02-03", "work":false,"name":"Saturday"}
// 调休需要上班
console.log(getDayDetail('2024-02-04')); // { "date": "2024-02-04", "work":true,"name":"Spring Festival,春节,3"}
// 节假日 春节
console.log(getDayDetail('2024-02-17')); // { "date": "2024-02-17", "work":false,"name":"Spring Festival,春节,3"}

getHolidaysInRange 获取指定日期范围内的所有节假日


接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有节假日;否则,只返回工作日的节假日。



tip: 即使不包括周末,周末的节假日仍然会被返回



// 示例用法
const start = '2024-04-26';
const end = '2024-05-06';

// 获取从 2024-05-01 到 2024-05-10 的所有节假日,包括周末
const holidaysIncludingWeekends = getHolidaysInRange(start, end, true);
console.log('Holidays including weekends:', holidaysIncludingWeekends.map(d => getDayDetail(d)));

// 获取从 2024-05-01 到 2024-05-10 的节假日,不包括周末
const holidaysExcludingWeekends = getHolidaysInRange(start, end, false);
console.log('Holidays excluding weekends:', holidaysExcludingWeekends.map(d => getDayDetail(d)));

getWorkdaysInRange 取指定日期范围内的工作日列表


接收起始日期和结束日期,并可选地决定是否包括周末。如果包括周末,则函数会返回包括周末在内的所有工作日;否则,只返回周一到周五的工作日。


// 示例用法
const start = '2024-04-26';
const end = '2024-05-06';

// 获取从 2024-05-01 到 2024-05-10 的所有工作日,包括周末
const workdaysIncludingWeekends = getWorkdaysInRange(start, end, true);
console.log('Workdays including weekends:', workdaysIncludingWeekends);

// 获取从 2024-05-01 到 2024-05-10 的工作日,不包括周末
const workdaysExcludingWeekends = getWorkdaysInRange(start, end, false);
console.log('Workdays excluding weekends:', workdaysExcludingWeekends);

findWorkday 查找工作日


查找从今天开始 未来的第 {deltaDays} 个工作日。


// 查找从今天开始 未来的第 {deltaDays} 个工作日
// 如果 deltaDays 为 0,首先检查当前日期是否为工作日。如果是,则直接返回当前日期。
// 如果当前日期不是工作日,会查找下一个工作日。
const currentWorkday = findWorkday(0);
console.log(currentWorkday);

// 查找从今天开始未来的第一个工作日
const nextWorkday = findWorkday(1);
console.log(nextWorkday);

// 查找从今天开始之前的前一个工作日
const previousWorkday = findWorkday(-1);
console.log(previousWorkday);

// 可以传第二个参数 查找具体日期的上下工作日
// 查找从 2024-05-18 开始,未来的第二个工作日
const secondNextWorkday = findWorkday(2, '2024-05-18');
console.log(secondNextWorkday);

节气模块


获取 24 节气的日期


import { getSolarTerms } from "chinese-days";

/** 获取范围内 节气日期数组 */
const solarTerms = getSolarTerms("2024-05-01", "2024-05-20");
solarTerms.forEach(({ date, term, name }) => {
console.log(`${name}: ${date}, ${term}`);
});
// 立夏: 2024-05-05, the_beginning_of_summer
// 小满: 2024-05-20, lesser_fullness_of_grain

// 没有节气 返回 []
getSolarTerms("2024-05-21", "2024-05-25");
// return []

/* 不传 end 参数, 获取某天 节气 */
getSolarTerms("2024-05-20");
// return: [{date: '2024-05-20', term: 'lesser_fullness_of_grain', name: '小满'}]

阳历农历互转


特别说明,此库中:



  1. 2057-09-28 为:农历丁丑(牛)年八月三十;

  2. 2097-08-07 为:农历丁巳(蛇)年七月初一。


阳历转换农历


// 2097-8-7
console.log(getLunarDate('2097-08-07'))

// 2057-9-28
console.log(getLunarDate('2057-09-28'))
// {
// date: "2057-09-28",
// lunarYear: 2057,
// lunarMon: 8,
// lunarDay: 30,
// isLeap: false,
// lunarDayCN: "三十",
// lunarMonCN: "八月",
// lunarYearCN: "二零五七",
// yearCyl: "丁丑",
// monCyl: "己酉",
// dayCyl: "戊子",
// zodiac: "牛"
// }

// 非闰月 和 闰月例子
console.log(getLunarDate('2001-04-27'))
console.log(getLunarDate('2001-05-27'))

根据阳历日期区间,批量获取农历日期


console.log(getLunarDatesInRange('2001-05-21', '2001-05-26'))

农历转换阳历


当为阴历闰月的时候,会出现一个农历日期对应两个阳历日期的情况,所以返回对象形式。


console.log(getSolarDateFromLunar('2001-03-05'))
// {date: '2001-03-29', leapMonthDate: undefined}

console.log(getSolarDateFromLunar('2001-04-05'))
// {date: '2001-04-27', leapMonthDate: '2001-05-27'}

欢迎贡献代码



  1. Fork + Clone 项目到本地;

  2. 节假日: 修改 节假日定义

  3. 农历定义: 修改 农历定义

  4. 其他修改需要自己查看源码;

  5. 执行命令 npm run generate 自动生成 节假日常量文件

  6. 提交PR。


致谢



  1. 农历数据来自于 Bigkoo/Android-PickerView 项目。

  2. 中国节假日数据参考了 Python 版本的 LKI/chinese-calendar 项目。


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

程序员还是得明白,除了技术,你必须学会与人沟通

前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 工作越久,越觉得沟通能力重要,所以今天想和大家聊聊一个被挺多程序员忽视的能力,沟通能力。 因为忽略沟通能力,自己也吃过不少亏: 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻...
继续阅读 »

前言


Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


工作越久,越觉得沟通能力重要,所以今天想和大家聊聊一个被挺多程序员忽视的能力,沟通能力


因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。


横向沟通


先说横向沟通,就是和没有汇报关系的同事或者合作方的沟通,也是我们在工作中,需要沟通场景最多的地方。
因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。因为忽略沟通能力,自己也吃过不少亏:



  1. 遇到问题不知道该请教谁,怕被别人觉着自己菜,怕麻烦别人,最后只能自己死磕。

  2. 跨部门协调时,自己的催促总是被人忽视,永远得到的是“我还有事情在忙,你先等等”。

  3. 日常只会低头写代码,甚至不知道代码为了谁而写,公司的方向、目标一概不知。

  4. 职业生涯感到困惑,想升职加薪,却又不知道如何向领导开口,到离职可能都没和领导私下说过10句话。


既然我们聊沟通,你是不是以为我要聊沟通的技巧、沟通的方式这些,当然一些方法很重要,但方式方法只能算是“术”的层面。


那些在职场中沟通顺畅,让别人愿意配合,被人信任的那些同事,仅仅靠的是方式方法吗?想要沟通更加轻松、顺畅,我觉着最终要的,还是取决于你是谁,你的表现如何,你能提供什么价值


话不多说,我们开始吧。
不知道横向沟通时,大家会不会遇见几种场景?



  1. 和团队内同事沟通,都把方案说的很明白了,但总感觉他不懂我的设计思路。

  2. 有问题想请教同事,但却频频被婉拒,甚至被刁难

  3. 与合作团队配合,你急的像热锅上的蚂蚁,但他们不急不慢,甚至不支持,导致项目延期。


我在刚毕业作为一个职场小透明的时候,遇到工作中不会的问题是,请教同事特别小心翼翼,生怕同事拒绝我,或者嫌弃我菜。


遇到比较和善的同事还好,愿意帮助你,但是最怕的就是最怕的就是被一些同事回复:“你能不能自己再看看代码。”


虽然绝大部分程序问题都能通过看代码解决,但是我提出了问题,那一定是我真的看不明白了,不一定是看不懂代码,可能是因为不了解一些业务的背景和历史原因而已。


我特么肯定是看完了没思路才问的,被这么一回复,下次鼓起勇气再问,不知道要到什么时候了。


所以,横向沟通,其实最关键的,就是对于合作关系的同事,如何获得他们的认可与帮助


如何获得他们的认可呢?最重要的在于你对他有多大影响力


比如你有着更好的职级,更老的资历,更广的职场人脉,在横向沟通时都会比较顺利。


但是当你是一个职场新人,或者刚换了一个岗位、一个公司,那么该如何利用自己的影响力,去横向沟通呢?


因此,我会从职业生涯不同的阶段,结合自己的实际经历,来介绍给大家。


职场初期


程序员在工作的前几年,需要提升自己的专业能力为主。


你缺乏业务经验,缺乏技术经验,在工作的沟通中,便很容易处于较低的位置。相信你也有这种感觉,你觉着一个同事更专业、更有能力,那么他和你沟通方案、寻求支持,你一定很容易被说服。


那么你要问,那职场初期,专业能力一定还不够好,那么没有更好的办法提升自己的影响力,和同事沟通了吗?


当然不是,可以通过借势,提升自己的权威度。比如技术方案讨论,如果你对方案存疑,却不能提出更好的方案,如果你拿出比如大厂的解决方案出来,通过对比优劣,你的说服力就能够大大提升。


当然,除了业务、技术经验,提升自己的逻辑能力,也是非常重要的。在寻求帮助、获得支持的时候,有准确的数据,充分的论据,都可以提升你的观点的说服力和可信度。


职场发展期


职场发展期,相信你的技术能力、业务能力会有很大的提升,或许你已经可以独当一面,你的工作范围也不限制与团队内部,可能需要与多个职能的同事一起配合完成工作。


从影响力角度分析,这时候你需要提升在同事眼中的“信任度”为主。


什么构成了你同事眼中的信任度呢?我想从几个方面来分析


你的人品如何,对待事情是否积极、公正。
职场中你更愿意相信谁呢?一定是那些积极主动,并且对所有同事一视同仁的人。


在你的职场发展期,积极主动,是一个人能否继续进步的关键。这个阶段除了快速学习技术,对于业务的学习很容易被忽略。因为遇到技术问题,这是你的工作,你不去解决,你的工作就无法完成。


但是业务知识,你不学,没有人去管你,你只需要看着产品的PRD写代码,也不会出什么错。


但如果你只关注自己的一亩三分地,工作中遇见问题不推进,什么都等着别人催,等着别人解决,那么你的影响力就无从谈起,和同事沟通也会遇到很大阻碍。


你过去的表现如何,你这个人是否靠谱?
代码讲究鲁棒性,人其实也一样。工作中面对不同的环境、条件,都需要能保证工作产出的稳定和可靠。



  1. 比如你的代码质量是否足够高,之前上线的功能,是否稳定。

  2. 对于别人提出的问题、线上的bug,你是否能快速响应不拖沓。最怕的就是别人问你一个问题,你说等一会告诉你,然后你转头就忘了这件事情。

  3. 答应别人的事情,是否能够按时完成,而不是到dead line,才告诉别人还有问题。


职场稳定期


在职场稳定期,你既有了足够的技术专业能力,也成为了一个正直、靠谱的人,这时候我认为“互惠原则”是支撑你沟通顺畅的一个小窍门。



互惠原则是社会心理学中的一个概念,指的是人与人之间天生有回馈他人恩惠的倾向。简单来说,就是“你对我好,我忍不住要报答你”。



我想通过几个例子和大家说明我是如何因为互惠原则


有一次,合作团队负责的一个需求改造很紧急,但是因为他们技术方案没有评估到我负责团队的改动,所以没有给我们的产品提需求,因此我这边没有技术排期。


如果临时提需求,那么根据排期就要排到下一个开发周期了,他们需求自然也要延期。


他们组长找到我,我和他一起评估了一下改造的复杂性,并不复杂,于是我答应他在他们上线前我也会改造完并发版。于是自己提了一个技术需求,完成了相关改造,他们的需求也顺利上线。


还有一次,产品经理考虑不周,新上线的功能校验比较严格,因为设计问题,运营同事频繁吐槽产品,影响了他们的使用效率。


正常走需求迭代,需要等到2周后了,于是产品找到我沟通,看是否有什么临时的解决方案。
为了解决运营问题,我看了下,需要临时处理一下数据,既能保证使用,也能节约不少运营的人力成本,于是我写了几个脚本,临时处理线上数据,解决了这个问题。


在这两个场景中,我发现了他们的诉求,并且发现他们对此真的很着急,于是我尽我所能,主动的为他们提供帮助。你可能会说,你这样临时多干了很多事,太亏了。


是的,短期来看,我确实牺牲了自己一部分时间去帮助他们,但是我也收获了对他们的影响力,比如承诺的一定完成,还能帮他们摆平一些问题,慢慢的我的影响力就越来越强。在以后我需要帮助的时候,他们自然也乐意去帮助我。年度的360绩效评估,我自然能够收获他们的认可与好评,长期来看,受益的就是你自己。


向上沟通


向上沟通就是和你的上级进行沟通,即使工作好多年了,我对于和领导沟通依然觉着很头大,相信这也是你非常头疼的问题。


看看一下几个场景,你有没有中招呢?



  1. 觉着领导太忙了,我目前的工作似乎也没这么重要需要汇报,找领导也不知道说什么,等领导不忙了再说。

  2. 我做好领导交给我的事情就行了,拿结果说话。

  3. 有困难需要和领导协调,但是不知道怎么说,领导会不会觉着我能力不行?

  4. 领导交给我的任务到底是什么意思?需要做到什么程度,到底是否着急。


我认为向上沟通,最重要的一个点,就是主动大胆,跨过心里的那道坎,因为绝大部分时候,我们就是内心有一个卡点,觉着无话可说,觉着没有必要。


向上沟通如果你要想影响上级,实话实说太难了,并不适用于我们每一个人。所以说预期说向上沟通,不如说我们如何才能够利用好我们的上级,帮助自己更好的发展。


展示自己


自己作出成绩的时候,觉着无人知晓,那么和上级适当的展示自己。


展示自己,你是不是觉着,这样有点显摆的意思?其实并不是,在沟通过程中,你或许了解到,这件事是否符合当下团队的发展方向,你感觉有了成绩,是否是自嗨,有没有地方需要被纠偏?


如果真正做得好,被领导认可,那么可以极大的增加你的自信心,输出你的影响力,避免“酒香也怕巷子深”。如果发现问题,那也从上级的角度发现了可以提升的地方,对你来讲也是百利而无一害。


当然,展示自己并不是直接去和领导说,我做了xxx东西,非常厉害,用了什么什么技术,而是有一种其他方式,比如协调大家,做一次技术分享,把自己的东西展示出来。


信息同步


上级安排的工作任务,无论遇到什么问题,都自己扛。


我在工作的前几年里,一直是一个低头干活的人,自己很有计划性,即使遇到问题,我也会靠着自己的力量去死磕,我一直以为,我是一个靠谱的人,领导给我安排的工作我都能自己完成,多给领导省心。


但后来有一次和领导沟通,领导说有时候一旦周期拉长,领导对我的信心就会减弱,最关键的就是我向上的反馈不够多,像是两个月的OKR,或者半年的规划时,领导很难知道具体的进度如何,最后是否能达成,因为上级需要识别风险,提前处理。


持续的做好信息同步,领导对你的信任度才能不断加深,你才能过承担更重要的工作。


困惑解答


工作久了,一定会有职业生涯的困惑,,未来怎么发展,干的不开心,甚至想离职,都可以试着和上级聊聊。


因为我们的工作内容比较单一,所以我们对于很多事情看待的角度也会单一,和领导聊聊,可以从更高的角度看一下自己当前的阶段与状态。


向下沟通


还记得开头说的那句话吗:“真正会沟通的人,不需要能说会道、口若悬河,而是懂提问、会倾听,能洞察需求、摸透人心。”



学会听,比学会讲更重要。




学会提问


我们日常生活中的提问,往往分为2种:开放式提问和限制式提问。


比如,你询问下级:最近团队比较忙,加班比较多,当然也做出了不错的成绩,不知道你怎么看待我们最近完成的这个项目呢?


这就是开放式提问,对于最近的忙碌,可能会有很多问题,或许是产品需求不合理,也可能是工程质量不高或者大家配合不够顺畅,这就是让对方做开放问答题。


如果换一个问法:最近加班比较多,你这块的工作,是否都按期完成了呢?


这种提问方式,对方只能回答是或者否。你能获得的信息就比较少。


在沟通的时候,我们要尽量多用开放式提问,要鼓励对方自由回答,多让他们讲。这样有助于你收集资料、挖掘需求,而且,还能鼓励对方对问题做出详细说明。


试着倾听


向下沟通,大家可能都会觉得重点应该在怎么说,但是向下沟通,更重要的是倾听。为什么这么说呢,因为在向下沟通的环境中,你的职级、经验通常是要比沟通对象多出一些的,如果在没有理解对方意思的情况下,很容易陷入单向输出的情况,你哇啦哇啦说了一堆方法论、公司目标与方向,但实际上沟通起不到太好的效果。


3F倾听法是一种有效的沟通技巧,它强调在倾听过程中要关注三个核心方面:Fact(事实)、Feel(感受)和Focus(意图)。这种方法可以帮助我们更全面地理解说话者的意图和需求,从而促进更有效的沟通。


倾听事实:这一步骤要求倾听者专注于对方所陈述的客观事实,避免加入自己的主观评判。倾听者需要区分事实与观点,确保理解的是对方所描述的已发生且可考证的事情。在这个过程中,倾听者应保持开放的心态,不急于作出解释或提供建议,而是先确保对事实有准确的理解。


倾听感受:在倾听事实的同时,倾听者需要注意观察对方的情绪状态,感知对方的感受。通过观察对方的肢体动作、语言、声调、表情变化等,可以更好地理解对方的情绪,与对方共情,尝试站在对方的角度去感受和理解其情绪。


倾听意图:这一步骤要求倾听者深入了解对方话语背后的真实意图和期望,而不仅仅是表面的意思。通过提问和澄清,确保准确理解对方的意图,避免误解和沟通障碍。在理解对方意图的基础上,可以更好地回应对方的需求,促进有效的沟通。


说在最后


好了,文章到这里就要结束啦,很感谢你能看到最后,经验有限,文章中如果有问题,希望你能够指正。


希望你看完之后,能够重视沟通这件事,在和代码“沟通“越来越熟练的同时,也要注重与人如何沟通。


不知道你在和同事的沟通过程中,有没有遇到什么困难或者好的经验呢?欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~


本篇文章是第36篇原创文章,2024目标进度36/100,欢迎有趣的你关注我~


作者:东东拿铁
来源:juejin.cn/post/7373474414430797863
收起阅读 »

代码和人,有一个能跑就行。为啥程序员总写dirty code

在程序员行业有一句:“代码和人,有一个能跑就行”。这句话对吗?为什么会产生这个问题?哪些代码能跑就行?有一些代码,真的就是不能动,一动就崩。里面逻辑复杂, 代码冗余,具备了一些不良代码的特征,但是它就是能跑,就是能支撑业务。有一个通俗的称谓为“屎山代码”。&n...
继续阅读 »

在程序员行业有一句:“代码和人,有一个能跑就行”。这句话对吗?为什么会产生这个问题?

哪些代码能跑就行?

有一些代码,真的就是不能动,一动就崩。里面逻辑复杂, 代码冗余,具备了一些不良代码的特征,但是它就是能跑,就是能支撑业务。有一个通俗的称谓为“屎山代码”。 image.png

哪些是dirty code

  • 缺乏注释和文档:完全没有注释或者复杂逻辑无文档
  • 命名不规范:变量、函数和类的命名不符合约定或没有约定 例如a,b,c,变量用动词,方法用名词,驼峰下划线混用等
  • 代码重复:同样的代码逻辑在多个地方重复出现,增加了维护的难度。 代码不做抽象,公共方法复制拷贝,一个方法复制多份
  • 复杂的逻辑:代码逻辑过于复杂,缺乏清晰的结构和模块化设计。
  • 硬编码值:使用硬编码的数值或字符串,而不是使用常量或配置文件

dirty code是如何产生的?

时间压力

项目不给足够时间,倒排期工程,项目经理整天催催催,老板天天问进度。预计5天,报了6天,砍到3天,1天的时候问做到哪了,2天问怎么还没做完。 你让我抽象,你让我搞架构,但是不给我时间,写出来的代码优先要进测试,提了bug再改改呗,反正缝缝补补又3年。

过于自信

自认自己的代码足够牛b。

不需要注释就可以看懂,不就是几个变量名吗?别人理一理逻辑就可以了,我的代码自己可以解释自己。 不需要抽象,这里都是一整套逻辑的。什么?你也要用这套代码,自己复制出去,别动我代码。我们要签订《代码互不侵犯条约》。

经验不足

新手小白能完成任务就不错了,什么鲁棒,什么设计模式,完全不需要考虑。一个函数500行?抱歉那是这个功能的瓶颈,不是我的瓶颈。

企业文化,标准/规范缺乏

你还记得你上一次做code review是啥时候吗?在夜深人静的时候,有没有回想每天996为啥老板还没开上大奔?

老板要的是结果,不是过程,代码写的再好,最后业务不核心,不干掉你干掉谁?

防御型编程

这个不多说了,懂的都懂。

明明知道有问题,为什么不重构呢?

重点项目,核心代码,不敢动。

在一些关键项目中,核心代码往往被视为系统的“心脏”。由于这些代码对于系统的稳定性至关重要,任何改动都可能带来巨大的风险。一旦出现问题,不仅影响系统的正常运行,还会直接影响团队的绩效和公司业务。因此,程序员往往选择维持现状,尽量避免对这些代码进行大的改动。

边缘项目,长期不迭代代码,不敢动。

对于一些边缘项目或已经很久没有进行过迭代的代码,由于缺乏持续的维护和更新,这些代码的整体质量和可读性往往较低。如果要对其进行修改,可能需要对整个系统进行全流程回归测试,这不仅费时费力,还可能导致人力资源的浪费。因此,除非遇到重大问题,否则这些代码通常也不会轻易被动。 image.png

代码能跑就行的结果是啥?

经过你一系列深思熟虑,不断优化重构,代码终于写的跟诗一样的。但是你的工期比别人多了1/2,虽然bug少了,但是研发成本大增。

尽管你的代码十分优秀,但不出意外的,在绩效评定的时候,你只拿到了及格。相反,另外一个能跑就行的同事,在每次线上出现问题的时候,都能及时化解,拿到了优秀。

因为你的项目进度慢,一些新项目和重点项目优先分配给了其他人。

慢慢的你对这家公司失去了信心,转投其他公司,但新公司的领导看到你的代码,惊为天人,于是你顺利的走上人生巅峰(给个happy end吧)。 image.png

究竟应该怎么做?

中国人讲究“中庸”,大多数情况,在非开源项目或公司无具体要求时,要求我们要掌握一个开发成本/代码质量的度。

尤其在一些并不太优秀的团队中,我们优秀的代码质量无法为我们换得足够匹配的价值回报。相反,交付效率/交付质量/线上稳定性才是优先考虑的问题。

尤其在现在降本增笑的大环境下,保护自己才是最重要的。

但优质的代码,带来的是身心的愉悦,后续维护的简单,代码的灵活性更高。建议在核心代码,工具类等优先使用高质量代码,而在一些增删改查,非核心/重点项目内容上,还是难得糊涂一下吧。


作者:天元reborn
来源:juejin.cn/post/7368397264027402275
收起阅读 »

如何选择 Android 唯一标识符

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。 标识符 IMEI 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10...
继续阅读 »

前言


大家好,我是未央歌,一个默默无闻的移动开发搬砖者~


本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。


标识符


IMEI



  • 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10+ 开始官方取消了获取 IMEI 的 API,无法获取到 IMEI 了


fun getIMEI(context: Context): String {
val telephonyManager = context
.getSystemService(TELEPHONY_SERVICE) as TelephonyManager
return telephonyManager.deviceId
}

Android ID(SSAID)



  • 无需任何权限

  • 卸载安装不会改变,除非刷机或重置系统

  • Android 8.0 之后签名不同的 APP 获取的 Android ID 是不一样的

  • 部分设备由于制造商错误实现,导致多台设备会返回相同的 Android ID

  • 可能为空


fun getAndroidID(context: Context): String {
return Settings.System.getString(context.contentResolver,Settings.Secure.ANDROID_ID)
}

MAC 地址



  • 需要申请权限,Android 12 之后 BluetoothAdapter.getDefaultAdapter().getAddress()需要动态申请 android.permission.BLUETOOTH_CONNECT 权限

  • MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会变化

  • 搭载 Android 10+ 的设备会报告不是设备所有者应用的所有应用的随机化 MAC 地址

  • 在 Android 6.0 到 Android 9 中,本地设备 MAC 地址(如 WLAN 和蓝牙)无法通过第三方 API 使用 会返回 02:00:00:00:00:00,且需要 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限


Widevine ID



  • DRM 数字版权管理 ID ,访问此 ID 无需任何权限

  • 对于搭载 Android 8.0 的设备,Widevine 客户端 ID 将为每个应用软件包名称和网络源(对于网络浏览器)返回一个不同的值

  • 可能为空


fun getWidevineID(): String {
try {
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
val mediaDrm = MediaDrm(WIDEVINE_UUID)
val widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
val sb = StringBuilder();
for (byte in widevineId) {
sb.append(String.format("x", byte))
}
return sb.toString();
} catch (e: Exception) {
} catch (e: Error) {
}
return ""
}

AAID



  • 无需任何权限

  • Google 推出的广告 ID ,可由用户重置的标识符,适用于广告用例

  • 系统需要自带 Google Play Services 才支持,且用户可以在系统设置中重置



重置后,在未获得用户明确许可的情况下,新的广告标识符不得与先前的广告标识符或由先前的广告标识符所衍生的数据相关联。




还要注意,Google Play 开发者内容政策要求广告 ID“不得与个人身份信息或任何永久性设备标识符(例如:SSAID、MAC 地址、IMEI 等)相关联。”




在支持多个用户(包括访客用户在内)的 Android 设备上,您的应用可能会在同一设备上获得不同的广告 ID。这些不同的 ID 对应于登录该设备的不同用户。



OAID



  • 无需任何权限

  • 国内移动安全联盟出台的“拯救”国内移动广告的广告跟踪标识符

  • 基本上是国内知名厂商 Android 10+ 才支持,且用户可以在系统设置中重置


UUID



  • 生成之后本地持久化保存

  • 卸载后重新安装、清除应用缓存 会改变


如何选择


同个开发商需要追踪对比旗下应用各用户的行为



  • 可以采用 Android ID(SSAID),并且不同应用需使用同一签名

  • 如果获得的 Android ID(SSAID)为空,可以用 UUID 代替【 OAID / AAID 代替也可,但需要引入第三方库】

  • 在 Android 8.0+ 中, Android ID(SSAID)提供了一个在由同一开发者签名密钥签名的应用之间通用的标识符


希望限制应用内的免费内容(如文章)



  • 可以采用 UUID ,作用域是应用范围,用户要想规避内容限制就必须重新安装应用


用户群体主要是大陆



  • 可以采用 OAID ,低版本配合采用 Android ID(SSAID)/ UUID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等


用户群体在海外



  • 可以采用 AAID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等




作者:未央歌
来源:juejin.cn/post/7262558218169008188
收起阅读 »

连公司WiFi后,无法访问外网,怎么回事,如何解决?

问题描述 从甲方项目组返回公司后,我习惯性连上公司WiFi,准备百度一个bug,突然我发现无打开百度,F5刷新了好几次也没用,浏览器报了下面的错误信息 尝试ping了一下 http://www.badu.com,好家伙,直接丢包 然后运行 ipconfig/...
继续阅读 »

问题描述


从甲方项目组返回公司后,我习惯性连上公司WiFi,准备百度一个bug,突然我发现无打开百度,F5刷新了好几次也没用,浏览器报了下面的错误信息


523d43c309e5d5b786dff74cc54894bf.png


尝试ping了一下 http://www.badu.com,好家伙,直接丢包


然后运行 ipconfig/all 命令看了一下本机的DNSF服务器信息


b75ff8dab79e97b5698daa4e060e24af.png


我的本机DNS地址是192.168.0.1


通常,本机DNS地址若为192.168.0.1,说明所连WiFi的路由器可能被设定为执行DNS转发职责,或者是期望客户端直接使用路由器作为DNS解析的入口点。而192.168.0.1一般是路由器的默认IP地址,并非一个标准的公共DNS服务器地址。在这种情况下,访问不了外网,例如百度,新浪微博等,有可能是路由器的DNS转发功能没有正常工作,或者路由器自身没有被配置正确以访问外部的DNS服务器


最简单直接的解决方法是手动设置主机的DNS地址为公共的DNS服务器地址



  • Google DNS:8.8.8.8 & 8.8.4.4

  • Cloudflare DNS: 1.1.1.1

  • 中国电信:114.114.114.114

  • 中国联通:223.5.5.5


7785e7beed4c08710c07617232dad576.png




OK,可以正常访问百度了



45db3372360d9ecab863ffb1da9e1246.png


这让我产生了非常浓烈的好奇,从浏览器上输入URL到显示页面,中间究竟发生了什么?


image.png


问题探究



这是一道面试题



1716627344750.png


从浏览器中输入URL并按下回车键后,直到网页内容完全显示在屏幕上,这个过程中发生了一系列复杂的步骤,大致可以概括如下:



  1. URL解析:浏览器首先解析输入的URL,提取出协议、域名、路径以及查询字符串等信息。

  2. 检查缓存:在发起网络请求之前,浏览器会检查本地缓存(包括浏览器缓存、系统缓存乃至路由器缓存),看看是否已经存储了该请求的资源。如果有且未过期,则直接使用缓存内容,无需继续下面的步骤。

  3. DNS解析:如果缓存中没有所需资源,浏览器会通过DNS(域名系统)将网址的域名转换为IP地址,因为网络通信是基于IP地址的。这个过程中可能涉及递归查询和迭代查询,直至找到域名对应的IP地址。

  4. TCP连接建立:获得服务器IP后,浏览器使用TCP协议与服务器建立连接。这通常涉及TCP三次握手过程,确保数据传输的可靠性和连接的双方都准备好通信。

  5. 发起HTTP/HTTPS请求:建立连接后,浏览器构造HTTP或HTTPS请求报文,包含请求方法(如GET或POST)、请求头(携带浏览器信息、请求资源的位置等)以及可能的请求体,然后发送给服务器。

  6. 服务器处理请求:服务器接收到请求后,根据请求的内容处理并准备响应,这可能涉及数据库查询、服务器端脚本执行等操作。

  7. 响应浏览器:服务器将处理好的响应数据(包括状态码、响应头、响应体等)封装成HTTP响应报文,发送回浏览器。

  8. 浏览器接收响应:浏览器接收响应数据,如果响应中有新的资源(如CSS、JavaScript、图片等),浏览器会根据需要再次发起请求获取这些资源。

  9. 渲染页面:浏览器开始解析HTML文档,构建DOM(文档对象模型)树,同时解析CSS文件构建CSSOM(CSS对象模型)树,结合这两棵树形成渲染树(Render Tree)。接着进行布局(Layout)和绘制(Painting),即确定每个节点在屏幕上的位置和外观,最终将页面内容呈现给用户。

  10. 执行JavaScript:页面中的JavaScript代码会被解析和执行,它可能修改DOM和CSSOM,导致重新布局和绘制。此外,异步请求如Ajax也可以在这个阶段发起,动态更新页面内容。

  11. 页面交互:页面加载完毕后,用户可以与页面进行交互,触发事件处理程序,进一步的JavaScript执行可能会改变页面状态。

  12. 连接关闭:当所有数据传输完毕,TCP连接会通过四次挥手的过程优雅地关闭。


上述过程中涉及到了多个层次的技术和协议,从应用层的HTTP/HTTPS、运输层的TCP、网络层的IP到链路层的以太网协议等,共同协作完成了从简单的URL输入到复杂页面展示的任务。


cbacfb95186577a2e2d92fe72fa8d0c5.png


基于上述分析,问题发生在第③步(DNS解析)上,要想回答何为DNS解析,就必须弄明白何为DNS。


何为DNS?


DNS,英文全称为Domain Name System,即域名系统。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道这个服务器对应的 IP地址,而对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址【正向解析】。以下定义概念摘自《计算机网络:自顶向下方法》:




  1. 一个由分层的 DNS 服务器( DNS server) 实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议



分布式,层次数据库


如何理解分布式?


随着互联网的快速发展,主机日益增多且数量庞大,采用单一DNS服务器上集中响应的设计并不可取,这种设计容易造成单点故障维护困难通信容量受限等问题。


为了应对上述问题和扩展性, DNS 使用了大量的 DNS 服务器并分布在全世界范围内。因为没有一台 DNS 服务器可以存放Internet上所有主机的映射数据, 相反,该映射数据被分布存储在所有的 DNS 服务器上。


如何理解层次?


DNS服务器采用层次组织,大致说来,有3种类型的 DNS 服务器:根 DNS 服务器、 顶级域 (Top- Level Domain , TLD) DNS 服务器和权威 DNS 服务器。它们的层次结构方式如下所示:


1716628019691.png


图片来源:《计算机网络:自顶向下方法》



  • 根DNS服务器


    我们首先要明确根域名是什么,它没有特定的名称,仅由一个点(.)表示。在技术层面上,它是所有域名查询的起点,负责指引域名解析过程中的查询请求到相应的顶级DNS(TLD)服务器,如.com.net.org等。而在实际的网址中,根域名通常隐含而不显示,例如com.baidu.com.,后面的点一般不会显示。


    根DNS服务器是互联网基础设施的关键部分,全球共有13组根DNS服务器,它们存储了顶级DNS服务器的地址信息,从而帮助我们将域名转换为用于网络通信的IP地址。根DNS的管理由国际互联网名称与数字地址分配机构(ICANN)负责。


  • 顶级域服务器


    这些服务器负责顶级域名,如comorgnetedugov,以及所有国家的顶级域名如uk、r、ca和jp。TLD提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。




  • 权威DNS服务器



    在因特网上具有公共可访问主机(如Wb服务器和邮件服务器)的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字映射为IP地址。



    以上内容摘自《计算机网络:自顶向下方法》,比较绕口,通俗来讲就是提供最终的主机—IP映射



本地DNS服务器


在上一节的DNS层次结构中,眼尖的小伙伴会发现,并未提及本地DNS服务器,那为什么呢?一个本地DNS服务器,从严格说来,它并不属于上述DNS服务器的层次结构,但它对DNS层次结构0是至关重要的


每个ISP(Internet Service Provider,即网络业务提供商)都有一台本地DNS服务器(也叫默认名字服务器)。当主机与某个ISP连接时,例如一个小区的ISP,一个学校的ISP等,该ISP会提供一台主机的IP地址,该主机具有一台或多台其本地DNS服务器的IP地址,通常主机的本地DNS服务器会临近主机,当主机发出DNS请求时,该请求被发往本地DNS服务器,它起着代理的作用,并将该请求转发到DNS服务器层次结构中。


迭代查询,递归查询


如下图所示,假设主机abc.net想要获取主机xyz.edu的IP地址,大致会进行如下步骤:


1716647441277.png



  1. 主机abc.net首先向它的本地DNS服务器发送一个查询报文,该报文会含有被转换的主机名xyz.edu。

  2. 本地DNS服务器会将该报文转发给根DNS服务器。

  3. 该根DNS服务器注意到其edu前缀并向本地DNS服务器返回负责edu的TLD(顶级域服务器)的IP地址列表。

  4. 该本地DNS服务器则再次向这些TLD 服务器中的其中一台发送查询报文。

  5. 该 TLD 服务器注意到 xyz. edu 前缀,并把权威DNS服务器的IP地址响应给该本地DNS服务器。

  6. 本地 DNS 服务器直接向权威DNS服务器中的其中一台重发查询报文。

  7. 该权威服务器会用xyz.edu的lP地址进行响应。

  8. 本地DNS服务器会将主机xyz.edu及其IP地址的映射数据响应给主机abc.net,主机abc.net拿到它的IP就能给主机xyz.edu发送请求。


在上图例子中,主机abc.net向本地DNS服务器发出的查询是递归查询因为该查询请求是以主机abc.net以自己的名义获得该映射。 而后继的3 个查询是迭代查询,因为所有的回答都是直接返回给本地DNS服务器。 即第①步是递归查询 ,第②,④,⑥步是迭代查询。


那所有的DNS查询都遵循迭代 + 递归的方式吗?


答案并非如此,虽然在理论上,任何DNS查询既可以是迭代的,也能是递归的。


如下图,所有的DNS查询是都是递归的,因为所有的查询请求是以主机abc.net以自己的名义获得该映射。


1716650770678.png


DNS缓存


实际上,为了改善时延性能并减少在Internet上到处传输的 DNS报文数量,DNS 广泛使用了缓存技术。 DNS 缓存的原理非常简单。 在一个请求链中,当某 DNS服务器接收一个 DNS 回答(例如,包含主机名到IP地址的映射)时,它能将该回答中的信息缓存在本地中。 下次查询时便可直接用缓存里的内容。


注意,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,通常设置为两天时间,一旦过了生存时间,这条记录就会从缓存移出。


有了缓存,本地 DNS 服务器可以立即返回所要解析主机的IP地址,而不必查询任何其他DNS服务器。 而本地 DNS服务器也能够缓存TLD服务器的地址,因而经常绕过查询链中的根 DNS服务器。


参考资料


计算机网络:自顶向下方法(原书第8版) (豆瓣) (douban.com)


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

29岁,大厂女程序员,总包六折结束北漂,聊聊换城市。

先简单描述下下我的背景。 95,双非本科,多段大厂前端背景,未婚未育,北漂快七年。 三段Gap经历,最长4个月。 回二线三个月,目前对人生很乐观。 1. 离开京东,结束北漂 昨天前同事发我消息吐槽,东子又跟兄弟们发言,不拼搏的也不再认兄弟了。 考勤新规真是十分...
继续阅读 »

先简单描述下下我的背景。
95,双非本科,多段大厂前端背景,未婚未育,北漂快七年。


三段Gap经历,最长4个月。


回二线三个月,目前对人生很乐观。


1. 离开京东,结束北漂


昨天前同事发我消息吐槽,东子又跟兄弟们发言,不拼搏的也不再认兄弟了。


考勤新规真是十分劝退,北京西南角五环外的京东总部。
研发卡到九点上班,砍掉午休。


在东子,如果是9点上班,一定不存在6点下班,东子的各大Bu潜规则都是晚上九十点左右下班,
工时每天要打满10个小时往上, 边缘部门也不例外。


在北京如果是9点到工位,我肯定是做不到。
也幸亏在2024年年初,我从京东拿到了礼包+年终奖,顺便结束了北漂。


现在在西北省会城市继续搞老本行做前端,不用再忧患9点上班带来的痛苦。


离开北京的原因和大多数人一样,


从工作发展上看,21年后,感觉“混”不再是个简单的事。
无论是晋升或是涨薪都难度比之前要大,并且这往前迈出的一步,也意味着要做更多内卷和向上管理的工作,性价比很低。


从生活体验这一面来讲,六七年的北漂体验已经让我对北京这座大城市带来的“通勤疲惫”以及“人际关系冷漠”感到麻木。挣了工资就攒着,没啥钱用来消费。 同时,我对“女前端”的职业生涯年限乐观的预期也就在三十左右,在北京的焦虑感也比其他城市会更重些,大家都是孤舟,去年因为焦虑也让我身体出了一些异常,还是快逃吧。


还有一点,我也跟大部分广大民群众一样,有个“娇夫孩子热炕头”的朴实生活愿望,“媳妇”还是回家找吧。


2. 换到二线城市之后的工作体验


回到家的这三个月,吃了很多好吃的(西北人就是西北胃!!),见了很多大学同学以及在省会的亲戚朋友。


换了个赛道,不在互联网行业,每天没那么多会议要开。
6点多就能下班,我开始下班看到夕阳,很幸福。


IMG_2629.JPG


回想在上家公司的工作内容,工作时间不是开会就是扯皮,还要保证产出和质量。
同时还有一无是处的“小组长” 要对你考核,组织架构不断的调整,职场环境糟糕,自己内耗严重。
赚那么点工资,医美钱都不够。
真的划不来。


通过互联网,也认识了很多从 北上广 回西安的前端同行,拉了群交流。
有需要的小伙伴可以后台didi我。


3. 年前找工作的感受


年前为了换工作,面试了一个多月,大概30多家公司。


包含大中小厂,面试通过大概在50%+ ,也拿了些offer。
跟周围很多同行都交流过我的面试情况,大部分都觉得我是,实力+运气。


我是业务型前端,之前从B到C,pc移动,跨端都有经验。
个人感受是面试整体的内容和之前内的差不多,前端还是 八股+项目 为主。


btw,相关面经我和其他的大厂前端朋友也沉淀了一份前端知识库,持续更新中,有需要dd.


不过已经是5年+经验 ,明显的能感觉到在问项目的时候,更加的细致和深入。细节挖得很深。
除了技术外,软实力方面也有所要求。面试中经常会被问到,如何去做一些项目管理及团队赋能的相关内容等等。


面到后期 ,跟HR谈薪才是最疲惫的环节。
京东整体研发的工资也不高,30%的涨幅都不好谈,这个跟我的面试表现也有关系。
我确实不是什么技术大佬。


回顾了下年前的面试记录。


投递了很多家公司,内推+BOSS直聘+脉脉 的简历通过率是最高的。


快手各个部门都显示不匹配,字节仍旧一轮游。
有约在晚上九点面试的,持续面到十一点多才结束。
我以为这么辛苦了,至少能给个二面,结果也是不匹配截止到一面。


北京真的把我面麻了,不qiu面了,到后期焦虑感也上来了。
直接留了两三周gap, 杭州长沙玩了一通。还是很开心的~


IMG_4655.JPG


后面西安的公司发了offer,降薪就降薪吧。


北京我实在是不想玩了,这几年跟老鼠一样,体验实在是太差,再升级苦难,我也是扛不住。


整体上看,2024年春节前,于我个人来看没有什么特别好的hc.
跟同行交流,大家也都是类似的情况。
而且一个比一个叫的惨,
我认识几个我认为技术不错的前端大佬,有从2022就开始gap的。
按照现在HR的标准,Gap真的拉黑率太高了,要打工进厂的话,还是尽量不要Gap吧,技术大神除外。


4. 其他


回西安工作后,好处就是,通勤十分钟,也不再吃外卖。
朋友家人都在身边,
总包虽然打折,也够养活自己了。
“外包之都”的名字也不是白叫的。我也能感觉到,跳槽应该是没啥地方能跳的了,基本整个西安市场上,好的前端HC是阶段性间歇出现的,机会不多。


工作上还是会有些焦虑,也逐渐意识到沉淀技术能力的重要性。
顺着最近对工作生活思考,
调整了下工作和学习上的方向,目前看来收益不小。
感兴趣的话,欢迎点赞收藏,我准备后面再写一期。


总之,目前来看体验还是很不错的,我本来物欲也不高,
希望同行们都心态向好,
努力生活。


作者:程序员班班
来源:juejin.cn/post/7372577541112987660
收起阅读 »

关注用户隐私安全 OPPO助力开发者保护个人信息安全

为助力广大APP开发者做好个人信息保护工作,更好地维护用户合法权益,在工业和信息化部信息通信管理局的指导下,5月23日,OPPO在深圳组织开展「个保合规我参与」公益培训宣讲会——OPPO站,这也是本次系列活动的首站。近百名移动应用开发者参加了此次活动,现场多位...
继续阅读 »

为助力广大APP开发者做好个人信息保护工作,更好地维护用户合法权益,在工业和信息化部信息通信管理局的指导下,5月23日,OPPO在深圳组织开展「个保合规我参与」公益培训宣讲会——OPPO站,这也是本次系列活动的首站。近百名移动应用开发者参加了此次活动,现场多位行业专家结合工作实践进行内容分享交流,探讨保护用户个人信息安全,共同推动APP行业健康发展。

1.JPG

深圳市通信管理局副局长陈逸菁在开场致辞中表示,广大企业和应用开发者要重视并做好APP个人信息和用户权益保护工作,保障互联网行业规范健康发展,维护网络安全和公众利益,共同推进移动互联网应用产业高质量发展。

2.jpeg

随后,OPPO全球数据与网络安全总经理韩方登场致辞,他指出日新月异的信息科技为生活带来了极大便利,同时也给个人隐私和信息安全带来了诸多挑战。维护个人隐私权益,确保公众在数字生活中的安全感和舒适度,已然成为当下的一项重大课题。为此,OPPO建立了完善的隐私合规体系,从技术、制度、培训等多方面入手,全面落实隐私保护。

3.jpeg

深圳市前海互联网安全保障中心互联网管理部主任梁建翔基于APP监管合规体系和APP备案管理要求两大方面,结合当前APP个人信息保护问题及相关法律法规要求,对APP备案与合规管理政策进行了介绍。他指出,移动互联网应用程序主办者和分发平台要切实履行合规义务,落实个人信息保护和“先备案后服务”要求,充分保护用户权益,维护网络安全和公共利益,促进互联网行业规范健康发展。

4.JPG

中国信息通信研究院泰尔终端实验室信息安全部副主任王艳红对《工业和信息化部关于进一步提升移动互联网应用服务能力的通知》进行了解读。从通知出台的背景和目的、重点工作考虑、APP开发运营者、分发平台等产业链上下游主体如何落实主体责任做了详细说明。

5.jpeg

中国信息通信研究院政策与经济研究所法律研究部专家端晨希从智能时代个人信息保护法律合规问题入手,介绍了当前APP个人信息保护的问题形势、相关法律法规及政策要求,就内容推荐服务可能存在的合规难点进行了解读。

6.jpeg

中国信息通信研究院技术与标准研究所产业互联网研究部专家李鑫对SDK个人信息和权益保护技术要求及SDK管理服务平台做了介绍,并现场展示了SDK管理服务平台相关功能服务,倡议行业企业共同推动SDK生态合规,以更好地保护用户合法权益,共同促进行业健康发展。

7.jpeg

中国信息通信研究院云计算与大数据研究所审计与治理部专家甘泉围绕《个人信息保护合规审计实施方法和实务》,分享了合规审计的背景、方法等和相关事件案例,为评估个人信息保护制度的可操作性以及相关制度的有效实施提供了指引。

8.jpeg

中国信息通信研究院泰尔终端实验室信息安全部专家邓佑军从个人权利响应和终端厂商义务两大维度展开,分享了移动智能终端用户权益保护规范要点相关内容,并结合实践中发现的违规收集个人信息、过度索取权限、欺骗误导强迫用户等典型问题,介绍了专项整治行动的整改流程,助力开发运营者高效完成合规整改。

9.jpeg

中国信息通信研究院泰尔终端实验室信息安全部专家武林娜结合生成式人工智能的交互方式、产品形态、产业生态现状,指出AIGC时代移动应用对个人信息安全带来的全方位影响及生成式人工智能模型本身带来的挑战,并提出AI大模型赋能个人信息保护的路径探索。

10.jpeg

加强用户信息安全及隐私保护,一直是OPPO关注的重点之一。现场OPPO高级安全研发工程师王学成分享了OPPO保护消费者个人信息安全的实践经验,并介绍了目前OPPO已建立起安全隐私全流程全场景防护体系, 通过OPPO智能护盾、7×24小时人工、三方引擎三重扫描,全天候对应用进行严格监测,及时拦截违规应用,并已通过《白皮书》形式对外展示应用安全治理成果。

11.jpeg

据了解,OPPO智能护盾通过安全大脑实现了贯穿应用程序从上架、下载、安装、启动、运行、卸载阶段全生命周期的安全隐私治理。在应用程序上架前,OPPO通过开发者信誉管理系统, 以敏感权限检测、隐私政策合规检测、隐私自动化检测方式审查开发者资质,确保应用来源的可靠性。通过对全量应用开发者进行实名和信誉排名,确保上线应用的开发者身份真实可信;同时,严控存疑开发者、强制清退封禁恶意开发者,从源头上确保OPPO软件商店在架应用来源的安全性与真实性。

除了应用上架环节外,针对应用下载、安装环节的潜在风险OPPO也进行了保护。当用户从浏览器等非官方渠道获取应用包、进行安装流程时,系统会进入安装扫描环节,由OPPO智能护盾安全大脑提供检测能力,对恶意应用进行警示和拦截。据统计,OPPO手机终端每年拦截恶意应用安装10亿多次, 恶意应用下载13亿次,拦截72亿次风险APP行为,高效保护用户隐私和安全。

12.jpeg

此外,手机应用违规收集个新信息、超范围收集个新信息、强制过度索取权限、欺骗误导强迫行为等困扰用户的问题,OPPO也给出了应对方案。OPPO智能护盾会在应用首次启动时结合应用类型和功能场景,为用户提供合理的授权建议,减少隐私数据泄漏风险;同时,在应用使用过程中,还将通过应用行为记录展示当前过度授权的应用与风险,引导用户一键优化,进一步保障安全性。

一直以来OPPO高度重视用户隐私和数据安全, 坚持"以用户为中心"的理念,积极践行“隐私守门人”使命,为每一个OPPO用户打造更便捷、更安心的用机环境。OPPO通过建立贯穿应用全流程的安全防护体系,提高APP开发者的个人信息保护水平,保障用户的信息安全,共同推动APP行业的健康发展,与用户一起展望更安全可靠的数智化生活。

收起阅读 »

Git提交错了,于是我把锅甩给了新来的baby

又是一遭悲惨的遭遇,git提交了一连串代码之后,发现提交错了。其实是把给老婆发的消息打到了comment里,然后还提交上去了。怎么办,这被看到岂不是要社死了。 一连串的研究之后,找到了几个解决方案。接下来我们一起搞搞这种错误提交的弥补方案。其中最离谱的是第三...
继续阅读 »

又是一遭悲惨的遭遇,git提交了一连串代码之后,发现提交错了。其实是把给老婆发的消息打到了comment里,然后还提交上去了。怎么办,这被看到岂不是要社死了。


image.png


一连串的研究之后,找到了几个解决方案。接下来我们一起搞搞这种错误提交的弥补方案。其中最离谱的是第三个方案。哈哈。


赛前准备


这里模拟一下这个操作,毕竟不能直接看我们的代码记录。我们新建一个项目,新建一个文件,起名001。


image.png


然后依次改为 002 003 004 005,每次都提交一次,在005的时候,执行异常提交。


最终我们得到一个005的文件


image.png


gitee上看是这样的


image.png


对于我们来说,现在是想删除这个异常提交,不仅删除代码,还想删除记录


也就是说,期待的是,文件变为004,而且这个提交记录删除掉。


方案1 交互式 rebase


首先我们尝试一下 git rebase -i HEAD~3,这样会取出最后的三条提交记录供我们编辑。


image.png


我们可以看到顶上有三条记录,这时候,我们删除这个异常的提交5


image.png


保存之后,会返回


git rebase -i HEAD~3
Successfully rebased and updated refs/heads/master.

这时候查看记录


image.png


异常提交已经没有了。


但是若是我们直接git push 会报错


image.png


告诉我们,我们当前的分支的版本是落后于远程分支的,不能提交。


这时候就需要git push --force这个命令,强制推送!!!


需要注意的是,强制推送会覆盖远程仓库中的历史记录,因此请确保你知道这个命令是个啥,并且有必要的话,需要通知团队其他成员协调好操作。


image.png


可以看到,git push --force 是可以成功的,而且再看gitee的记录


image.png


异常提交5已经不见了。并且本地的文件已经变为了004


image.png


其实在git rebase -i HEAD~3这个命令打开的交互框里是可以更改提交的顺序的,但是不能针对同一个文件的同一行,会冲突。


方案2 git reset


git reset 其实之前写文章讲过Git reset到底该如何使用,一文读懂系列 这次我们就直接为达目的,直接使用。
我们在上边的基础上,再提交一个异常提交5,使其恢复最初的情况。


image.png


然后gitee的情况:


image.png


这时候我们执行


git reset --hard HEAD~1

这个命令将删除最近的一个提交,包括提交所做的更改。请注意,这种方法可能会导致丢失未提交的更改,也就是说,本地写的没提交的代码就没了。所以请谨慎使用。


image.png


执行之后,我们可以看到异常提交5不见了


image.png


提交的时候也需要git push --force这个命令,强制推送!!!为啥每次都使用三个!!!呢,我只想告诉你,这个命令很恐怖,一定要慎之又慎。


这时候查看gitee记录


image.png


异常提交5没有了。


使用 git revert


还有小伙伴会说,为啥不用git revert呢,这不是git专门用来回滚代码的吗?


我们恢复异常提交005,再试试


image.png


我们执行 git revert f3d8db 并且 push


image.png


可以看到,文件是从005变为004了。但是从提交记录来看,不仅没有删除记录,还多了一条。其实,除非提交的注释特别社死,不然一般用的就是git revert,因为它不仅可以保存记录,还能确保版本是往前走的。


image.png


方案3 git filter-branch(谨慎使用)


查资料的时候,还看到一个这个命令,可以来一波骚的了。那既然提错了,把这锅甩给新人不就行了,哇咔咔咔咔咔。


git filter-branch --commit-filter '
if git log --format="%B" -n 1 $GIT_COMMIT | grep -q "异常提交"; then
GIT_AUTHOR_NAME="new baby";
GIT_COMMITTER_NAME="new baby";
git commit-tree "$@";
else
git commit-tree "$@";
fi'
-- --all

然后就是这样的


image.png


image.png


可以看到名字变了。当然邮箱也是可以改的。哇咔咔,这异常不就与我没关系了么。。。但是,极其不建议这么瞎折腾哈。


这个命令会根据条件重写整个历史。操作之前备份一下吧,别折腾坏了。而且一定先和其他的小伙伴商量一下,尤其是新人哈。


在此,就研究完毕了。正常来说使用第一种或者第二种方案都是可以的。不怕挨打的话,第三种方案也行。


git rebase 和 git reset 的区别



  • git rebase 命令用于将一个分支的提交移动到另一个分支上,或者重新应用一系列的提交。它的主要作用是改变提交的基础,即重新设置提交的起点。

  • git reset 命令用于修改当前分支的 HEAD 引用,或者用于撤销之前的提交操作。


也就是说git rebase 用于重新整理提交历史,而 git reset 用于调整当前分支的位置或撤销更改。关于这两个详细的使用,git reset已经写过了,有关git rebase的我会新开一篇文章,有关将一个分支的提交移动到另一个分支上这个操作虽不常用,但总有需要用到的时候。


作者:奔跑的毛球
来源:juejin.cn/post/7365414174217355314
收起阅读 »

在滴滴开发H5一年了,我遇到了这些问题

web
IOS圆角不生效 ios中使用border-radius配合overflow:hidden出现了失效的情况: 出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效 解决方法:在使用动画效果带transform的元...
继续阅读 »

IOS圆角不生效


ios中使用border-radius配合overflow:hidden出现了失效的情况:


image.png



出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效



解决方法:在使用动画效果带transform的元素的上一级div元素的css加上下面语句:


-webkit-transform:rotate(0deg);

IOS文本省略溢出问题


在部分ios手机上会出现以下情况:


image.png


原因


在目标元素上设置font-size = line-height,并加上以下单行省略代码:


.text-overflow {
display: -webkit-box;
overflow : hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}

或者:


.text-overflow {
overflow : hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

由于不同系统包含的字体的行高不一致,即使设置了height = line-height 一样会有以上问题


解决方案


经过测试,在height = line-height = font-szie的情况下,加上padding-top: 1px可以解决这个问题,即在需要使用单行省略的地方加上:


.demo {
height: 28px;
line-height: 28px;
font-size: 28px;
padding-top: 1px;
}

如:<div class="text-overflow demo">我是需要进行单行省略的文案</div>


安卓手机按钮点击后有橙色边框


image.png


解决方案:


button:focus {
outline: none;
}

优惠券打孔效果


需求中经常需要实现一类效果:优惠券打孔,如下图所示:


image.png


通常情况下会找设计采用图片的的形式,但这个方案最大的缺陷是无法适配背景的变化。
因此,我们可以采用如下方案,左右两侧各打一个孔,且穿透背景:


image.png


具体细节可以参考这篇文章:纯 CSS 实现优惠券透明圆形镂空打孔效果


Clipboard兼容性问题


navigator.clipboard兼容性不是很好,低版本浏览器不支持


image.png


解决方案:


const copyText = (text: string) => {
return new Promise(resolve => {
if (navigator.clipboard?.writeText) {
return resolve(navigator.clipboard.writeText(text))
}
// 创建输入框
const textarea = document.createElement('textarea')
document.body.appendChild(textarea)
// 隐藏此输入框
textarea.style.position = 'absolute'
textarea.style.clip = 'rect(0 0 0 0)'
// 赋值
textarea.value = text
// 选中
textarea.select()
// 复制
document.execCommand('copy', true)
textarea.remove()
return resolve(true)
})
}

Unocss打包后样式不生效


这个问题是由webpack缓存导致的,在vue.config.js中添加以下代码:


config.module.rule('vue').uses.delete('cache-loader')

具体原因见:UnoCSS webpack插件原理


低端机型options请求不过问题


在我们的业务需求中,覆盖的人群很广,涉及到的机型也很多。于是我们发现在部分低端机型下(oppo R11、R9等),有很多请求只有options请求,没有真正的业务请求。导致用户拿不到数据,报network error错误,我们的埋点数据也记录到了这一异常。


在我们的这个项目中,我们的后台有两个,一个提供物料,一个提供别的数据。但是奇怪的是,物料后台是可以正常获取数据,但业务后台就不行!


经过仔细对比二者发送的options请求,发现了问题所在:


image.png


发现二者主要存在以下差异:



  1. Access-Control-Allow-Headers: *

  2. Access-Control-Allow-origin: *


于是我便开始排查两个响应头的兼容性,发现在这些低端机型上,Access-Control-Allow-Headers: *确实会有问题,这些旧手机无法识别这个通配符,或者直接进行了忽略,导致options请求没过,自然就没有后续真正的请求了。


image.png


解决方案:由后台枚举前端需要的headers,在Access-Control-Allow-Headers中返回。


此外,将Access-Control-Allow-Origin设置为*也有一些别的限制:



参考



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

如果你要离开,就必须头也不回地离开!

再次重温《夜航西飞》时,里面的这段话更加让我觉得有智慧,甚至会觉得是整本书里最好的句子:如果必须离开你曾经住过、爱过、深埋着所有过往的地方,无论以何种方式,都不要慢慢离开,要决绝地离开,永远不回头。不要相信过去的时光才更好,它们已经消亡了。 只是有的话需要当...
继续阅读 »

再次重温《夜航西飞》时,里面的这段话更加让我觉得有智慧,甚至会觉得是整本书里最好的句子:如果必须离开你曾经住过、爱过、深埋着所有过往的地方,无论以何种方式,都不要慢慢离开,要决绝地离开,永远不回头。不要相信过去的时光才更好,它们已经消亡了。


图片


只是有的话需要当了一定的年龄,经历了一些事情之后才能对其有更加深刻的了解,并且能付诸行动。


我的烟龄已经十年了,戒烟对我来说是一件下了无数次决心的事,但是每次基本上都是以失败而告终,里面有一个搞笑和讽刺的点,就是每次下定决心戒烟的时候,我不会把现存的烟给丢掉,反而会进行自我暗示:抽完这包就不再抽了,呵呵!


这其实就像赌徒已经输得倾家荡产了,于是还在想着再赌一次。但是只要有一次,就会有第二次,无数次。


所以说,如果没有砸碎一切的决心,那么是不可能彻底走出的。


我这次下定决心戒烟是因为喉咙确实不太舒服,其次我是想好好当一个健康的人,所以前段时间,我抽完一支后,觉得不能再抽了,于是毫无留情将剩下的烟全部用水淋湿,丢进了垃圾桶。


在戒烟一段时间后,嗓子明显舒服了许多,昨天去见了一个朋友,他说我最近的气色好多了哎。我自己也感觉好了一点。无论从身体的舒适程度还是精神状态。


所以当我再回过头来阅读《夜航西飞》的时候,里面的这句话不由让我感叹感叹其力量。


我前段时间和一个妹子聊天的时候,她说自己谈了一段时间的男朋友,后面发现其出轨了,于是自然就分手了。但是到现在依然还在想着他。现在,我也想把开头的那句话送给你。


因为一个伤害了你的人,就不要再对其有任何留恋,做事情就别再拖泥带水,而且大概率在之前的感情中,你也只是一厢情愿,别人实际上根本不care,那这样的感情注定就是你输。


此时你还深陷其中干嘛?生活的美好处处都是,何不去感受美好的生活,头也不回地离开才是最理智的做法。


后面我愈发觉得,一个总是活在过去的人,是不值得拥有更好的东西的,总觉得过去的时光是美好的,但是再美好的昨天,它已经消亡了,而此刻,才是最美好的时光碎片。


就像此刻,六点起床后,我洗漱完毕,拉开窗帘,阳光洒在我的身上,我烧了一杯温水来喝,接着打开电脑来写下心里的想法,那么我就觉得此刻比过往的任何时刻都要美好,纵使我会回忆高中六点起床,然后走在绿树成荫的校园路上,再去操场上和同学笑着跑几圈,那虽然也是美好的时光,但是它已经消亡了,而此刻的时光是我能够牢牢抓在手里的!


那么过了今天,明天当我早起后,戴着耳机去附近的公园走上几圈后,我也会觉得昨天是美好的,但是我不会活在昨天,而是牢牢把握住今天!


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

前端命令行部署:再也不用把dist包给后端部署服务了!

web
好物推荐 超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~ 这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。 这两天发现了一个别人写的一个deploy...
继续阅读 »

好物推荐


超简单的命令行部署。给在小公司部署还是给后端dist包的萌新小伙伴们~


这边项目本身就是使用命令行部署到,不过那个命令行工具是自己写的,是嵌入到公司某一个私有npm包里,和其他依赖耦合在一起。灵活性不是很好。
这两天发现了一个别人写的一个deploy cli。感觉蛮好用的。分享一下。


希望可以帮助更多刚入行小伙伴了解更多前端玩法。


前端命令行部署


很多公司的前端部署流程都是先打一个dist包。然后给后端同事帮忙部署。


前端:::
1714281510854.png


后端:::


529ae5c36b03377bf116bafea2e95f1.png


(开玩笑的,工作中的后端同事都没那么调皮)


本文的内容就是如何使用命令行进行前端自动部署。


我们整个网站的读取,其实就是我们上传一个静态的文件包到服务器,然后服务器上的后台服务读取我们的静态包,来进行页面的展示。所以,前端自动化部署的关键,就是,能把dist包传到服务器的指定目录下就OK了。


部署流程


推荐一个deploy cli工具(deploy-cli-service)


安装


  1. 执行 npm install deploy-cli-service -g 进行全局安装 。

  2. 执行 deploy-cli-service - v 查看版本


初始化配置文件

在项目根目录执行 deploy-cli-service init 进行初始化


deploy-cli-service init命令执行后项目目录下会出现一个名为deploy.config的文件


image.png


deploy-cli-service init初始化的内容会被默认输入到 deploy.config


修改配置文件

deploy-cli-service init初始化之后输入的内容都会默认被写入deploy.config文件中。


image.png


然后看看相关的属性有没有什么需要修改的就ok。


配置部署命令


image.png


"deploy:test": "deploy-cli-service deploy --mode test"," 写入到 package.json中的script里。


然后在命令行执行 "npm run deploy:test"


成功部署后会如下显示


image.png


image.png


注意


配置 deploy.config.js时尽量使用ssh证书登录,不要使用服务器密码,把服务器密码写在前端代码里是一件非常不好的操作。


deploy-cli-service npm地址


luck


作者:工边页字
来源:juejin.cn/post/7362924623825256463
收起阅读 »

互联网+《周易》:我在github学算卦

web
前言 《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。 像这种千古奇书,每个中国人都应该读一读,一是因...
继续阅读 »

前言


《周易》乃周文王姬昌所作,是中国传统思想文化中自然哲学与人文实践的理论根源,是古代汉民族思想、智慧的结晶,被誉为“大道之源”。内容极其丰富,对中国几千年来的政治、经济、文化等各个领域都产生了极其深刻的影响。


像这种千古奇书,每个中国人都应该读一读,一是因为这是老祖宗的智慧,我们不能丢弃;二是因为《周易》蕴含宇宙人文的运行规律,浅读可修身养性,熟读可明自我,深究可知未来,参透就可知天命了。


东汉著名史学家、文学家班固在《汉书•艺文志》中提出《周易》的成书是:人更三圣,世历三古


那么在哪里才可以读到呢?


其实易经的完本在网上随便就可以找到,但是都不适合在摸鱼的时候读 (!🤡),打开花花绿绿或者神神叨叨的小网站,你的 leader 肯定一眼就看出你在摸鱼。


既然没有这种网站,那干脆自己做一个。


vitePress + github pages 快速搭建


vitePress 快速开始


pnpm add -D vitepress

pnpm vitepress init

填写完 cli 里的几个问题,项目就可以直接运行了。可以看到网站直接解析了几个 示例的 md 文件,非常的神奇。


处理《周易》文本


那么哪里才可以找到《周易》的 markdown 版本呢,找了一圈也没有找到,最后找到了一个 txt 的,我觉得写个脚本转换一下。


首先,我拿 vscode 的正则给每个标题加上井号,使其成为一级标题


QQ2024511-183935.webp


此时,所有的标题都被改成了md格式的一级标题,然后直接将后缀名从 .txt 改为 .md 即可。


看过 vitepress 的文档并经过实操后发现,它的目录是一个一个的小 markdown 文件组成的,而单个 markdown 内的标题等在右侧显示


image.png


那么此时就需要把《周易》完本,按照六十四卦分为六十四个 md 文件。


我写了一个node脚本:


const fs = require('fs');

// 读取zhouyi.md文件
fs.readFile('zhouyi.md', 'utf8', (err, data) => {
 if (err) {
   console.error('读取文件出错:', err);
   return;
}

 // 按一级标题进行分割
 const sections = data.split('\n# ');

 // 循环处理每个一级标题的内容
 sections.forEach((section, index) => {
   // 提取标题和内容
   const lines = section.split('\n');
   const title = lines[0];
   const content = lines.slice(1).join('\n');

   // 写入到单独的文件中
   const fileName = `zhouyi_${index + 1}.md`;
   fs.writeFile(fileName, `# ${title}\n\n${content}`, err => {
     if (err) {
       console.error(`写入文件 ${fileName} 出错:`, err);
    } else {
       console.log(`已创建文件: ${fileName}`);
    }
  });
});
});


取名为md-slicer.js ,在控制台输入


node md-slicer.js

即可生成


image.png


然后写一个在 .vitepress/config.mtssidebar的生成函数:


let itemsLength = 64
function getSidebar() {
 let items: {}[] = [{
   text: '《周易》是什么?',
   link: '/what.md'
}]
 for (let i = 1; i <= itemsLength; i++) {
   items.push({ text: `第${numberToChinese(i)}卦`, link: `/zhouyi_${i}.md` })
}
 return items
}

numberToChinese函数用来将阿拉伯数字转为中文数字,因为周易只有六十四卦,所以不用考虑很多,够用即可


// numberToChinese
function numberToChinese(number) {
 const chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
 const chineseUnits = ['', '十', '百', '千', '万', '亿'];

 // 将数字转换为字符串,以便于处理每一位
 const numStr = String(number);

 let result = '';
 let zeroFlag = false; // 用于标记是否需要加上“零”

 for (let i = 0; i < numStr.length; i++) {
   const digit = parseInt(numStr[i]); // 当前位的数字
   const unit = chineseUnits[numStr.length - i - 1]; // 当前位的单位

   if (digit !== 0) {
     if (zeroFlag) {
       result += chineseNumbers[0]; // 如果前一位是零,则在当前位加上“零”
       zeroFlag = false;
    }
     result += chineseNumbers[digit] == "一" && unit == "十" ? unit : chineseNumbers[digit] + unit; // 加上当前位的数字和单位,当一十时,省略前面的一
  } else {
     zeroFlag = true; // 如果当前位是零,则标记为需要加上“零”
  }
}
 return result;
}

然后,设置一下vitepress基础配置和打包输出路径


export default defineConfig({
 title: "周易",
 description: "周易",
 base: "/thebookofchanges/",
 head: [
  ['link', { rel: 'icon', href: 'yi.svg' }] // 这里是你的 Logo 图片路径
],
 outDir: 'docs', // 输出到docs ,可以直接在 github pages 使用
 themeConfig: {
   // https://vitepress.dev/reference/default-theme-config
   nav: [
    { text: '首页', link: '/' },
    { text: '阅读', link: '/zhouyi_1.md' }
  ],
   logo: '/yi.svg',
   sidebar: [
    {
       text: '目录',
       items: getSidebar()
    }
  ],

   socialLinks: [
    { icon: 'github', link: 'https://github.com/LarryZhu-dev/thebookofchanges' }
  ]
}
})


然后简单给网站设计一个logo


image.png


字体是华文隶书,转化为路径后,将它拉瘦一点,再导出为 svg。


最后,用 pnpm run docs:build打包即可,打包时注意设置基本路径为 github pages 的仓库名。


发布


push到github后,在 Setting/Pages 页面发布即可。


image.png


效果预览


最后,网站运行在:larryzhu-dev.github.io/thebookofch…


image.png


image.png


仓库地址:github.com/LarryZhu-de… 来点star🤣


结语


现在只有简单的原文,如有 《周易》大佬,欢迎大佬提交注解PR。


作者:德莱厄斯
来源:juejin.cn/post/7367659849101312015
收起阅读 »

gpt-4o这些玩法真的太逆天了

OpenAI在近期发布了GPT-4系列的新模型GPT-4o。这一更新主要聚焦于多模态和端侧应用,为用户提供了全新的交互体验。 GPT-4o作为OpenAI的新模型,具有三大显著特点: 多模态:GPT-4o能够接受文本、音频、图像作为组合输入,并生成任何文本、音...
继续阅读 »

OpenAI在近期发布了GPT-4系列的新模型GPT-4o。这一更新主要聚焦于多模态和端侧应用,为用户提供了全新的交互体验。


GPT-4o作为OpenAI的新模型,具有三大显著特点:


多模态:GPT-4o能够接受文本、音频、图像作为组合输入,并生成任何文本、音频和图像的组合输出。这种多模态的理解能力让GPT-4o在处理复杂任务时更具优势,如识别人类的感情并根据感情做出“有感情的反应”。


几乎无延迟:GPT-4o对音频输入的响应时间最短为232毫秒,平均为320毫秒,这与人类在对话中的响应时间相似。这种极快的响应速度使得GPT-4o能够实时地与用户进行交互,提供流畅的用户体验。


可在电脑桌面运行:OpenAI还将与苹果合作推出了适用于macOS的ChatGPT桌面级应用。这一应用允许用户在没有网络的情况下使用ChatGPT,并且可以在本地设备上处理敏感信息,保护用户隐私。


一些逆天的视频展示


下面来一起了解一下它官网的一些视频展示的逆天操作:


第一个王炸,作业辅导


作业辅导


视频中展示的是巨佬在使用 GPT-4o 对他儿子进行作业辅导。它开始就告诉gpt-4o 说不要直接说出答案,而是帮助它一步一步解决这个几何题目,我们在视频中可以看到,的确是这样,gpt-4o 一步一步的帮助他儿子解决了这个问题,而且还是非常细致的解释,并且是非常有情感的,每当他儿子完成一步之后,gpt-4o 从语气上都会有一种更进一步的感觉,这种情感化的交互方式,让人感觉非常的亲切。


而且,所有的过程都是这个娃在拿着笔在一步一步的解决这个几何题目,gpt-4o 就是看着这个娃做的解题过程,它会判断这个娃是否步骤对了,这个交互简直太赞了!这明显得益于GPT-4o的图像理解能力的增强。


作业辅导


讲真,按照这个趋势,教培行业似乎极有可能被干掉,那些不会做奥数题的家长,有福了,因为 安特曼说,gpt-4o 是会免费的。这意味着,你不需要花费一分钱,就可以请一个专业的教培老师,帮助你的孩子解决问题。


第二个王炸,精神分裂,一个端中两个 gpt-4o 互动起来了


之前我们于gpt 的实时语音对话只能是一对一,好了,颠覆认知的时刻来了,你在一个对话窗口中,可以同时存在两个gpt-4o对话,甚至,它两还可以互相对话,这个视频中,这两 gpt-4o 相互唱起了小曲。。。
两个 gpt-4o 协调


外语学习


外语学习


在这个例子中,研究人员展示的是,它告诉 gpt-4o 它想学习西班牙语言,当然它使用英语说的,然后它使用摄像头对着苹果和香蕉,问gpt-4o这个是什么,gpt-4o 利用它图像识别的能力,认出了香蕉和苹果,然后告诉研究人员。


但是!但是!但是!它回的语言居然是英语和西班牙语的混合,也就是,gpt-4o 回答,this is manzana and plátano。差点没有惊掉我的下巴,一句回答中包含了多种语言。这中组合输出的能力,简直太强了。


参与多人对话中来


图 4


这个视频展示的是 gpt-4o 加入到了一个在线会议中,它可以看到共享的屏幕,因此它知道会议有多少个人,然后开始是每个人说了一下自己的喜欢的人和事,接着主持人发文,他们各自有哪些爱好,gpt-4o 一一都回答出来了,而且是非常的准确,最后还来了一个总结,后面腾讯会议,zoom 估计交互得更上啊,不加入一个智能记录员,这体验就得甩开好几条街了。


同声传译


同声传译


这个视频展示的是 gpt-4o 扮演的事一个翻译者的角色,画面中的两个人一个人是将英语的,一个人是将西班牙语的,gpt-4o 就负责把听到的英语转化为西班牙语,把西班牙语转换为英语,然后两个哥们就愉快的对话了,你说你的西班牙,我说我的英语,我们都听得很懂的,所以,同声传译这个行业,是不是也要凉凉了。


外婆的澎湖湾


催命曲


歪日哦,富有情感的和你对话是王炸的话,和这个对比简直小巫见大巫,它哼起了小区,而且还会偶尔和你聊天的时候爽朗的发出笑声,这种情感化的交互方式,让人感觉非常的亲切。当这个老外说它想睡觉,哼个小曲,gpt-4o 就开始哼起了外婆的澎湖湾,听得我差点给睡着了...这种情感化的交互方式,让人感觉非常的亲切。


语速控制


语速控制


在这个视频中,老外让 gpt-4o 数数,1,2,3,。。。10. gpt-4o 一口气说完了,然后老外说,你能不能慢一点,gpt-4o 就慢慢的说了一遍,然后老外说,你能不能快一点,gpt-4o 就快速的说了一遍,这种语速控制就完全可以用来训练自己的听力了,这个功能比较赞,不过,我的下巴还在。


开玩笑


开玩笑


这个视频中,老外给 gpt-4o 说它要给它老爸讲个笑话,然后他想让 gpt-4o 先听听它这个笑话是不是好笑,结果,gpt-4o 真的爽朗得笑了,笑得一点都不像机器人,听到它这个笑声的时候,我的下巴还差那么一点就掉了。。。


你是我的眼


你是我的眼


这个视频的效果也是相当的炸裂,视频中时候一个盲人,很显然他看不见,因此它所到之处,让 gpt-4o 告诉它周围都有写什么风景,gpt-4o 一一告诉他,从这个视频中,我有点小小的启发!


gpt-4o 可能更好的交互是类似 Google Glass的形式,这样的产品出来,估计全世界的盲人都要为之震撼,他们都将会重见光明,这个产品的价值,简直不可估量。


两个 gpt-4o 互相对话


两个 gpt-4o 互相对话


两个 gpt-4o 互相对话


这个视频中显示了两个gpt-4o 开始了对话,什么,永动机???实际上不是,是视频中 openai 的大佬先告诉一个 gpt-4o 说等会有个可以看见世界的 AI 会和你对话,你可以和他交流,随后它启动了另外一个 gpt-4o,然后两个 gpt-4o 开始对话了,并且大佬还可以随时打断加入他们的对话。我的脑袋已经开始疼了,这个视频太炸裂了。这意味着,我是不是可以搞 3 个手机,搞一桌四川麻将了???


着装建议


着装建议


视频中,这个大佬要准备面试了,问 gpt-4o 怎么穿着得体,然后 gpt-4o 告诉他带个帽子试试,结果带上 gpt-4o 就开始爽朗的笑了。。。,嗯,后面穿什么出门,估计可以让 gpt-4o 建议建议合不合适。。。


桌游助手


桌游助手


这个场景是两个人想玩石头剪刀布的游戏,然后让 gpt-4o 做裁判,然后就开始了,gpt-4o 说 1,2,3,亮出你们的爪子,然后判断谁输谁赢,好了,我似乎又找到了一个乐子。




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

7个Js async/await高级用法

web
7个Js async/await高级用法 JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护...
继续阅读 »

7个Js async/await高级用法


JavaScript的异步编程已经从回调(Callback)演进到Promise,再到如今广泛使用的async/await语法。后者不仅让异步代码更加简洁,而且更贴近同步代码的逻辑与结构,大大增强了代码的可读性与可维护性。在掌握了基础用法之后,下面将介绍一些高级用法,以便充分利用async/await实现更复杂的异步流程控制。


1. async/await与高阶函数


当需要对数组中的元素执行异步操作时,可结合async/await与数组的高阶函数(如mapfilter等)。


// 异步过滤函数
async function asyncFilter(array, predicate) {
const results = await Promise.all(array.map(predicate));

return array.filter((_value, index) => results[index]);
}

// 示例
async function isOddNumber(n) {
await delay(100); // 模拟异步操作
return n % 2 !== 0;
}

async function filterOddNumbers(numbers) {
return asyncFilter(numbers, isOddNumber);
}

filterOddNumbers([1, 2, 3, 4, 5]).then(console.log); // 输出: [1, 3, 5]

2. 控制并发数


在处理诸如文件上传等场景时,可能需要限制同时进行的异步操作数量以避免系统资源耗尽。


async function asyncPool(poolLimit, array, iteratorFn) {
const result = [];
const executing = [];

for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
result.push(p);

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}

return Promise.all(result);
}

// 示例
async function uploadFile(file) {
// 文件上传逻辑
}

async function limitedFileUpload(files) {
return asyncPool(3, files, uploadFile);
}

3. 使用async/await优化递归


递归函数是编程中的一种常用技术,async/await可以很容易地使递归函数进行异步操作。


// 异步递归函数
async function asyncRecursiveSearch(nodes) {
for (const node of nodes) {
await asyncProcess(node);
if (node.children) {
await asyncRecursiveSearch(node.children);
}
}
}

// 示例
async function asyncProcess(node) {
// 对节点进行异步处理逻辑
}

4. 异步初始化类实例


在JavaScript中,类的构造器(constructor)不能是异步的。但可以通过工厂函数模式来实现类实例的异步初始化。


class Example {
constructor(data) {
this.data = data;
}

static async create() {
const data = await fetchData(); // 异步获取数据
return new Example(data);
}
}

// 使用方式
Example.create().then((exampleInstance) => {
// 使用异步初始化的类实例
});

5. 在async函数中使用await链式调用


使用await可以直观地按顺序执行链式调用中的异步操作。


class ApiClient {
constructor() {
this.value = null;
}

async firstMethod() {
this.value = await fetch('/first-url').then(r => r.json());
return this;
}

async secondMethod() {
this.value = await fetch('/second-url').then(r => r.json());
return this;
}
}

// 使用方式
const client = new ApiClient();
const result = await client.firstMethod().then(c => c.secondMethod());

6. 结合async/await和事件循环


使用async/await可以更好地控制事件循环,像处理DOM事件或定时器等场合。


// 异步定时器函数
async function asyncSetTimeout(fn, ms) {
await new Promise(resolve => setTimeout(resolve, ms));
fn();
}

// 示例
asyncSetTimeout(() => console.log('Timeout after 2 seconds'), 2000);

7. 使用async/await简化错误处理


错误处理是异步编程中的重要部分。通过async/await,可以将错误处理的逻辑更自然地集成到同步代码中。


async function asyncOperation() {
try {
const result = await mightFailOperation();
return result;
} catch (error) {
handleAsyncError(error);
}
}

async function mightFailOperation() {
// 有可能失败的异步操作
}

function handleAsyncError(error) {
// 错误处理逻辑
}

通过以上七个async/await的高级用法,开发者可以在JavaScript中以更加声明式和直观的方式处理复杂的异步逻辑,同时保持代码整洁和可维护性。在实践中不断应用和掌握这些用法,能够有效地提升编程效率和项目的质量。


作者:慕仲卿
来源:juejin.cn/post/7311603994928513076
收起阅读 »

Google 如果把 Go 团队给裁了会怎么样?

大家好,我是煎鱼。 节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。 据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心...
继续阅读 »

大家好,我是煎鱼。


节前有一则劲爆消息,Google 把 Python 基础团队和 flutter/dart 团队里相当多的开发人员给解雇了,据说可能是要换个城市重组(真是熟悉的 CY 套路)。


据悉被解雇的人中基本都是负责了 Python 重要维护的相关核心成员。


如下图所示:



此时引发了国内外社区一个较大的担忧,如果 Google 如法炮制,要放弃 Go 核心团队。会发生什么事,会不会有什么问题?



现在有什么


先知道可能会失去什么,那得先盘点一下 Go 这一门编程语言和 Go 核心团队在 Google 获得了什么。


根据我们以往对 @Lance Taylor 所澄清以及各处的描述,可以估算 Go 在 Google 大概获得了什么。


其至少包含以下内容:



  1. 工作岗位:Go 核心团队相关成员的工作岗位,包含薪资、福利等各种薪酬内容。

  2. 软硬件资源:Go 相关的软硬件资源(例如:知识产权、服务器、域名、模块管理镜像)等网上冲浪所需信息。

  3. 线下活动:Go 世界各地部分大会的开展可能会变少,或缩减规模(资金、背书等)。

  4. 大厂内部资源:因为失去 Google 内部的资源,可能逐步失去一些先进项目的熏陶和引入使用 Go 这一门编程语言的机会。

  5. 推广和反馈渠道:Go 一些显著问题和特性的发现、响应,可能会变慢。因为 Go 对于 Google 内部的问题处理和特性需要,历史上来看都是按最高优先级处理。


可能会发生什么事


如果真的一刀切,Google 把 Go 核心团队干没了,基础设施全部都不提供了。


大家普遍认为,会出现如下几种情况:



  1. 如果 Go 团队中的很多人被裁员,他们会另谋高就。各散东西。维护积极性和组织性会大幅下降。

  2. 如果 Google 决定完全停止对 Go 的投资,Go 的维护可能会变得更加复杂,因为它需要运行大量的基础设施。在这种情况下,可能会出现 Go 由 Google 转移到一个外部的基金会,会有明显的阶段性维护波动。

  3. 如果 Google 选择在内部其他团队对 Go 继续投入,较差的情况是 Google 会灵活运用他们对知识产权的所有权 --Go 很可能会更名为其他东西。


基金会方面,另外大家认为最有可能接受 Go 的基金会是:CNCF,因为 Go 项目在 CNCF 中基于数量来讲是最大的。


如下图部分所示:



同时 CNCF 和 Go 的云原生属性最为强烈,契合度非常高。


参考 Rust 发展史


@azuled 根据 Rust 的发展历史,给出了自己的一些见解。如下所表述:


1、Rust 被踢出 Mozilla 核心,成为一个独立的基金会,但它仍然存活了下来。事实上,它后来可能做得更好。


2、我认为很有可能围绕 Go 成立一个非营利组织,而且很有可能有足够多的大公司使用它来支持它,至少在一段时间内是这样。


总结


在目前这个大行情下,Go 作为 Google Cloud 团队的一员,和云原生的故事捆绑在一起。如果 Google 业绩出现波动,或者要继续降本增效。


这类没有直接营收的基础部门或团队还是比较危险的,因为其会在企业中根据利润中心、成本中心进行分摊和计算人效成本等。


如果真的强硬切割,势必会对 Go 这门编程语言产生阶段性的冲击。但未来是好是坏,就不好说了。



作者:煎鱼eddycjy
来源:juejin.cn/post/7366070642047008783
收起阅读 »

Spring Boot 3 集成 Jasypt详解

随着信息安全的日益受到重视,加密敏感数据在应用程序中变得越来越重要。Jasypt(Java Simplified Encryption)作为一个简化Java应用程序中数据加密的工具,为开发者提供了一种便捷而灵活的加密解决方案。本文将深入解析Jasypt的工作原...
继续阅读 »

随着信息安全的日益受到重视,加密敏感数据在应用程序中变得越来越重要。Jasypt(Java Simplified Encryption)作为一个简化Java应用程序中数据加密的工具,为开发者提供了一种便捷而灵活的加密解决方案。本文将深入解析Jasypt的工作原理,以及如何在Spring Boot项目中集成和使用Jasypt来保护敏感信息。


springboot-jasypt.jpg


springboot-jasypt.jpg


Jasypt简介


Jasypt(Java Simplified Encryption)是一个专注于简化Java加密操作的工具。它提供了一种简单而强大的方式来处理数据的加密和解密,使开发者能够轻松地保护应用程序中的敏感信息,如数据库密码、API密钥等。


Jasypt的设计理念是简化加密操作,使其对开发者更加友好。它采用密码学强度的加密算法,支持多种加密算法,从而平衡了性能和安全性。其中,Jasypt的核心思想之一是基于密码的加密(Password Based Encryption,PBE),通过用户提供的密码生成加密密钥,然后使用该密钥对数据进行加密和解密。


该工具还引入了盐(Salt)的概念,通过添加随机生成的盐值,提高了加密的安全性,防止相同的原始数据在不同的加密过程中产生相同的结果,有效抵御彩虹表攻击。


Jasypt与Spring Boot天然契合,可以轻松集成到Spring Boot项目中,为开发者提供了更便捷的数据安全解决方案。通过Jasypt,开发者可以在不深入了解底层加密算法的情况下,轻松实现数据的安全保护,使得应用程序更加可靠和安全。


官网地址: http://www.jasypt.org/


github地址: github.com/ulisesbocch…


Spring Boot 3 集成 Jasypt


添加依赖


在pom文件中添加一下依赖


<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot</artifactId>
<version>3.0.5</version>
</dependency>

添加配置文件


未指定前后缀的话默认格式ENC()括号里面是加密后的密文 然后实现自动解密


spring:
# 数据源配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.10.106:3306/xj_doc?characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: ENC(BLC3UQBxshlcA9tnMyJL7w==)

# 加密配置
jasypt:
encryptor:
# 指定加密密钥,生产环境请放到启动参数里面
password: 0f7b0a5d-46bc-40fd-b8ed-3181d21d644f
# 指定解密算法,需要和加密时使用的算法一致
algorithm: PBEWithMD5AndDES

iv-generator-classname: org.jasypt.iv.NoIvGenerator

# property:
# # 算法识别的前后缀,默认ENC(),包含在前后缀的加密信息,会使用指定算法解密
# prefix: ENC@[
# suffix: ]

启动类添加注解


在启动类上添加注解@EnableEncryptableProperties注解来开启自动解密


@SpringBootApplication
@MapperScan("cn.xj.xjdoc.**.mapper")
@EnableEncryptableProperties //开启自动解密功能
public class XjdocApplication {
public static void main(String[] args) {
SpringApplication.run(XjdocApplication.class, args);
}
}

测试类


public class JasyptUtil {

public static void main(String[] args){
StandardPBEStringEncryptor standardPBEStringEncryptor =new StandardPBEStringEncryptor();
/*配置文件中配置如下的算法*/
standardPBEStringEncryptor.setAlgorithm("PBEWithMD5AndDES");
/*配置文件中配置的password*/
standardPBEStringEncryptor.setPassword("0f7b0a5d-46bc-40fd-b8ed-3181d21d644f");
//加密
String jasyptPasswordEN =standardPBEStringEncryptor.encrypt("xj2022");
//解密
String jasyptPasswordDE =standardPBEStringEncryptor.decrypt(jasyptPasswordEN);
System.out.println("加密后密码:"+jasyptPasswordEN);
System.out.println("解密后密码:"+jasyptPasswordDE);
}
}

生产环境安全处理


jasypt的password值放在配置文件中在生产环境中是不安全的,我们可以将password值放到启动命令中,删除配置文件中password 的配置行,启动命令如下所示:


java -Djasypt.encryptor.password=password -jar jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar

或者


java -jar jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar --jasypt.encryptor.password=password

总结


Jasypt作为一个简单而强大的加密工具,为Java应用程序提供了便捷的数据保护方案。通过与Spring Boot的集成,开发者可以在应用程序中轻松地加密和解密敏感信息。在实际项目中,选择合适的加密方式、安全存储密码以及与Spring Security等安全框架的集成,都是保障应用程序安全的关键步骤。希望本文能够帮助读者更深入地了解Jasypt,并在实际项目中合理地运用加密技术。


作者:修己xj
来源:juejin.cn/post/7318616887415717924
收起阅读 »

Springboot3 + SpringSecurity + JWT + OpenApi3 实现认证授权

Springboot3 + SpringSecurity + JWT + OpenApi3 实现双token 目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供...
继续阅读 »

Springboot3 + SpringSecurity + JWT + OpenApi3 实现双token


目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供大家学习和项目实战使用,创作不易,转载请注明出处!


该项目使用目前最新的 Sprin Boot3 版本,采用目前市面上最主流的 JWT 认证方式,实现双token刷新。



温馨提示:SpringBoot3 版本必须要使用 JDK11 或 JDK19



SpringBoot3 新特性


Spring Boot3 是一个非常重要的版本,将会面临一个新的发展征程!Sprin Boot 3.0 包含了 12 个月以来,151 个人的 5700+ 次 commit 的贡献。这是自 4 年半前发布的 2.0 版本以来的第一次重大修订,这也是第一个支持 Spring Framework 6.0 和 GraaIVM 的 Spring Boot GA 版本。


Spring Boot 3.0 新版本的主要亮点:



  1. 最低要求为 Java 17 ,兼容 Java 19

  2. 支持用 GraalVM 生成原生镜像,代替了 Spring Native

  3. 通过 Micrometer 和 Micrometer 追踪提高应用可观察性

  4. 支持具有 EE 9 baseline 的 Jakarta EE 10


为什么采用双 Token刷新?


**场景假设:**星期四小金上班的时候摸鱼,准备在某APP 上面追剧,已经深深的陷入了角色中无法自拔,此时如果 Token 过期了 ,小金就不得不重新返回登录界面,重新进行登录,那么这样小金的一次完整的追剧体验就被打断了,这种设计带给小金的体验并不好,于是就需要使用双 Token 来解决。


**如何使用:**在小金首次登陆 APP 时,APP 会返回两个 Token 给小金,一个 accessToken,一个 refreshToken,其中 accessToken 的过期时间比较短,refreshToken 的时间比较长。当 accessToken 失效后,会通过 refreshToken 去重新获取 accessToken,这样一来就可以在不被察觉的情况下仍然使小金保持登录状态,让小金误以为自己一直是登录的状态。并且每次使用refreshToken 后会刷新,每一次刷新后的 refreshToken 都是不相同的。


**优势说明:**小金能够有一次完整的追剧体验,除非摸鱼时被老板发现了。accessToken 的存在,保证了登录的正常验证,因为 accessToken 的过期时间比较短,所以也可以保证账号的安全性。refreshToken 的存在,保证了小金无需在短时间内反复的登录来保持 Token 的有效性,同时也保证了活跃用户的登录状态可以一直延续而不需要重新登录,反复刷新也防止了某些不怀好意的人获取 refreshToken 后对用户账号进行不良操作。


一图胜千言:


image-20230604084837740


项目准备


项目采用 Spring Boot 3 + Spring Security + JWT + MyBatis-Plus + Lombok 进行搭建。


创建数据库


user 表


image-20230603220205094


token 表


在实际中应该把 token 信息保存到 redis


image-20230603220333914


创建 Spring Boot 项目


创建一个 Spring Boot 3 项目,一定要选择 Java 17 或者 Java 19


引入依赖


<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
<version>3.0.4version>
dependency>

<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.11.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.11.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.11.5version>
dependency>

编写配置文件


server:
port: 8417
spring:
application:
name: Spring Boot 3 + Spring Security + JWT + OpenAPI3
datasource:
url: jdbc:mysql://localhost:3306/w_admin
username: root
password: jcjl417
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
table-prefix: t_
id-type: auto
type-aliases-package: com.record.security.entity
mapper-locations: classpath:mapper/*.xml
application:
security:
jwt:
secret-key: VUhJT0pJT0hVWUlHRFVGVFdPSVJISVVHWUZHVkRVR0RISVVIREJZI1VJSEZTVUdZR0ZTVVk=
expiration: 86400000 # 1天
refresh-token:
expiration: 604800000 # 7 天
springdoc:
swagger-ui:
path: /docs.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs

项目实现


准备项目所需要的一系列代码,如 entity、controller 、service、mapper 等


系统角色 Role


定义一个角色(Role)枚举,详细代码参考文章结尾处的项目源码


public enum Role {

// 用户
USER(Collections.emptySet()),
// 一线人员
CHASER( ... ),
// 部门主管
SUPERVISOR( ... ),
// 系统管理员
ADMIN( ... ),
;

@Getter
private final Set permissions;

public List getAuthorities() {
var authorities = getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
}

User 实现 UserDetails


温馨提示:


由于 Spring Security 源码设计的时候 ,将用户名和密码属性定义为 username 和 password,所以我们看到的大部分教程都会遵循源码中的方式,习惯性的将用户名定义为 username,密码定义为 password。


其实我们大可不必遵守这个规则,在我的系统中使用邮箱登录,也即是将邮箱(email)作为 Security 中的用户名(username),那么我必须要将用户输入的 email 作为 username 来存放,这会使我感到非常的不适,因为我的系统中正真的 username 将会 用另外一个单词来命名。


如何避免登录时的字段必须设置为 username 和 password 呢?



重写 getter方法, 只有你的系统中登录的用户名和密码属性不是 username 和 password 的情况下 ,你进行重写才会看到下面红色框中的提示。


202306032035283

重写 username 和 password 的 getter方法


@Override
public String getUsername() {
return email;
}

@Override
public String getPassword() {
return password;
}

Security 配置文件



需要注意的是 WebSecurityConfigurerAdapter 在 Spring Security 中已经被弃用和移除


下面将采用新的配置文件



@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {

private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
private final RestAuthorizationEntryPoint restAuthorizationEntryPoint;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers(
"/api/v1/auth/**",
"/api/v1/test/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/doc.html",
"/webjars/**",
"/swagger-ui.html",
"/favicon.ico"
).permitAll()
.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())

.requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
.requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
.requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())

.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())

.requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
.requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
.requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
//添加jwt 登录授权过滤器
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout()
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())

;
//添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);

return http.build();
}
}

OpenApi 配置文件


OpenApi 依赖


<dependency>
<groupId>org.springdocgroupId>
<artifactId>springdoc-openapi-starter-webmvc-uiartifactId>
<version>2.1.0version>
dependency>

OpenApiConfig 配置


OpenApi3 生成接口文档,主要配置如下



  • Api Gr0up(分组)

  • Bearer Authorization(认证)

  • Customer(自定义请求头等)


@Configuration
public class OpenApiConfig {

@Bean
public OpenAPI customOpenAPI(){
return new OpenAPI()
.info(info())
.externalDocs(externalDocs())
.components(components())
.addSecurityItem(securityRequirement())
;
}

private Info info(){
return new Info()
.title("京茶吉鹿的 Demo")
.version("v0.0.1")
.description("Spring Boot 3 + Spring Security + JWT + OpenAPI3")
.license(new License()
.name("Apache 2.0") // The Apache License, Version 2.0
.url("https://www.apache.org/licenses/LICENSE-2.0.html"))
.contact(new Contact()
.name("京茶吉鹿")
.url("http://localost:8417")
.email("jc.top@qq.com"))
.termsOfService("http://localhost:8417")
;
}

private ExternalDocumentation externalDocs() {
return new ExternalDocumentation()
.description("京茶吉鹿的开放文档")
.url("http://localhost:8417/docs");
}

private Components components(){
return new Components()
.addSecuritySchemes("Bearer Authorization",
new SecurityScheme()
.name("Bearer 认证")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
)
.addSecuritySchemes("Basic Authorization",
new SecurityScheme()
.name("Basic 认证")
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
)
;

}

private SecurityRequirement securityRequirement() {
return new SecurityRequirement()
.addList("Bearer Authorization");
}

private List security(Components components) {
return components.getSecuritySchemes()
.keySet()
.stream()
.map(k -> new SecurityRequirement().addList(k))
.collect(Collectors.toList());
}


/**
* 通用接口
*
@return
*/

@Bean
public Gr0upedOpenApi publicApi(){
return Gr0upedOpenApi.builder()
.group("身份认证")
.pathsToMatch("/api/v1/auth/**")
// 为指定组设置请求头
// .addOperationCustomizer(operationCustomizer())
.build();
}

/**
* 一线人员
*
@return
*/

@Bean
public Gr0upedOpenApi chaserApi(){
return Gr0upedOpenApi.builder()
.group("一线人员")
.pathsToMatch("/api/v1/chaser/**",
"/api/v1/experience/search/**",
"/api/v1/log/**",
"/api/v1/contact/**",
"/api/v1/admin/user/update")
.pathsToExclude("/api/v1/experience/search/id")
.build();
}

/**
* 部门主管
*
@return
*/

@Bean
public Gr0upedOpenApi supervisorApi(){
return Gr0upedOpenApi.builder()
.group("部门主管")
.pathsToMatch("/api/v1/supervisor/**",
"/api/v1/experience/**",
"/api/v1/schedule/**",
"/api/v1/contact/**",
"/api/v1/admin/user/update")
.build();
}

/**
* 系统管理员
*
@return
*/

@Bean
public Gr0upedOpenApi adminApi(){
return Gr0upedOpenApi.builder()
.group("系统管理员")
.pathsToMatch("/api/v1/admin/**")
// .addOpenApiCustomiser(openApi -> openApi.info(new Info().title("京茶吉鹿接口—Admin")))
.build();
}
}

image-20230603224928028


Security 接口赋权的方式


hasRole及hasAuthority的区别?



hasAuthority能通过的身份必须与字符串一模一样,而hasRole能通过的身前缀必须带有ROLE_,同时可以通过两种字符串,一是带有前缀ROLE_,二是不带前缀ROLE_



通过配置文件


在配置文件中指明访问路径的权限


.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())
.requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
.requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
.requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())


.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())
.requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
.requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
.requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

通过注解


@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "系统管理员权限测试")
public class AdminController {

@GetMapping
@PreAuthorize("hasAuthority('admin:read')")
public String get() {
return "GET |==| AdminController";
}


@PostMapping
@PreAuthorize("hasAuthority('admin:create')")
public String post() {
return "POST |==| AdminController";
}
}

测试


我们登录认证成功后,系统会为我们返回 access_token 和 refresh_token。


image-20230604082145598




作者:京茶吉鹿
来源:juejin.cn/post/7241399184594993208
收起阅读 »

20个你不得不知道的Js async/await用法

web
20个你不得不知道的Js async/await用法 JavaScript的async和await关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/awai...
继续阅读 »

20个你不得不知道的Js async/await用法


JavaScript的asyncawait关键词是现代JavaScript异步编程的核心。它们让异步代码看起来和同步代码几乎一样,使得异步编程变得更加直观和易于管理。本文介绍20个关于async/await的实用技巧,将大大提升编程效率和代码的清晰度。


1. 基础用法


async函数返回一个Promise,而await关键词可以暂停async函数的执行,等待Promise解决。


async function fetchData() {
let data = await fetch('url');
data = await data.json();
return data;
}

2. 错误处理


使用try...catch结构处理async/await中的错误。


async function fetchData() {
try {
let response = await fetch('url');
response = await response.json();
return response;
} catch (error) {
console.error('Fetching data error:', error);
}
}

3. 并行执行


Promise.all()可以用来并行执行多个await操作。


async function fetchMultipleUrls(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return await Promise.all(promises);
}

4. 条件异步


根据条件执行await


async function fetchData(condition) {
if (condition) {
return await fetch('url');
}
return 'No fetch needed';
}

5. 循环中的await


在循环中使用await时,每次迭代都会等待。


async function sequentialStart(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.json());
}
}

6. 异步迭代器


对于异步迭代器(例如Node.js中的Streams),可以使用for-await-of循环。


async function processStream(stream) {
for await (const chunk of stream) {
console.log(chunk);
}
}

7. await之后立即解构


直接在await表达式后使用解构。


async function getUser() {
const { data: user } = await fetch('user-url').then(r => r.json());
return user;
}

8. 使用默认参数避免无效的await


如果await可能是不必要的,可以使用默认参数避免等待。


async function fetchData(url = 'default-url') {
const response = await fetch(url);
return response.json();
}

9. await在类的方法中


在类的方法中使用async/await


class DataFetcher {
async getData() {
const data = await fetch('url').then(r => r.json());
return data;
}
}

10. 立刻执行的async箭头函数


可以立即执行的async箭头函数。


(async () => {
const data = await fetch('url').then(r => r.json());
console.log(data);
})();

11. 使用async/await进行延时


利用async/await实现延时。


function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function delayedLog(item) {
await delay(1000);
console.log(item);
}

12. 使用async/await处理事件监听器


在事件处理函数中使用async/await


document.getElementById('button').addEventListener('click', async (event) => {
event.preventDefault();
const data = await fetch('url').then(r => r.json());
console.log(data);
});

13. 以顺序方式处理数组


使用async/await以确定的顺序处理数组。


async function processArray(array) {
for (const item of array) {
await delayedLog(item);
}
console.log('Done!');
}

14. 组合async/awaitdestructuring以及spread运算符


结合使用async/await,解构和展开操作符。


async function getConfig() {
const { data, ...rest } = await fetch('config-url').then(r => r.json());
return { config: data, ...rest };
}

15. 在对象方法中使用async/await


async方法作为对象的属性。


const dataRetriever = {
async fetchData() {
return await fetch('url').then(r => r.json());
}
};

16. 异步生成器函数


使用async生成器函数结合yield


async function* asyncGenerator(array) {
for (const item of array) {
yield await processItem(item);
}
}

17. 使用顶级await


在模块顶层使用await(需要特定的JavaScript环境支持)。


// ECMAScript 2020引入顶级await特性, 部署时注意兼容性
const config = await fetch('config-url').then(r => r.json());

18. async/await与IIFE结合


async函数与立即执行函数表达式(IIFE)结合。


(async function() {
const data = await fetch('url').then(r => r.json());
console.log(data);
})();

19. 使用async/await优化递归调用


优化递归函数。


async function asyncRecursiveFunction(items) {
if (items.length === 0) return 'done';
const currentItem = items.shift();
await delay(1000);
console.log(currentItem);
return asyncRecursiveFunction(items);
}

20. 在switch语句中使用await


switch语句的每个case中使用await


async function fetchDataBasedOnType(type) {
switch (type) {
case 'user':
return await fetch('user-url').then(r => r.json());
case 'post':
return await fetch('post-url').then(r => r.json());
default:
throw new Error('Unknown type');
}
}

作者:慕仲卿
来源:juejin.cn/post/7311603506222956559
收起阅读 »

往事-迷茫-如何抉择?

〇、往事 起初 我11年网络技术专科毕业,直到15年春节后打算开始程序员生涯,那年我25岁。那时候我只会很浅的语言基础,C、java、C#、php 都会一些,由于没有开发经验,在苏州15天,后辗转来到上海,又是5天。当初懵懵懂懂,并没有什么理想和目标,只是知道...
继续阅读 »

〇、往事


起初


我11年网络技术专科毕业,直到15年春节后打算开始程序员生涯,那年我25岁。那时候我只会很浅的语言基础,C、java、C#、php 都会一些,由于没有开发经验,在苏州15天,后辗转来到上海,又是5天。当初懵懵懂懂,并没有什么理想和目标,只是知道程序员会有一份不错的收入,仅此而已,我和众多小白一样对使用什么语言难以抉择,我跑招聘会、也投简历,无论是什么公司、什么语言,只要有人要我,我就去。


我的第一份工作


辗转来到上海的第5天,我获得了第一份开发工作,这份工作的面试,现在回想起来还是觉得不可思议,一位不知道是什么职位的A让我把页面上的一个按钮调整位置、颜色?我记不清了,只记得我好像没有弄成功,已经不记得是怎么获得的工作了。我只记得入职的第二天,公司就让我去了另外一个办公地点工作,开始打杂,都算不上搬砖,因为我能力有限……


直到很久以后,我才知道,我做的工作叫外包,我算是被卖了,起初的半年都是不缴社保的,付现金,我拿6k,公司拿9k,公司净赚3k。好吧,我没有怨天尤人,因为那时候在老家我只能拿2k,而且就凭我快半年才知道自己 “被卖” 这个事实,说明我是多笨,也许我也只配做外包,补充一句,我们做的是 .NET, asp.net。又过了半年,我稍微有些入门了,似乎也聪明一点了,知道不能再混下去,于是辞职找下家……


后来听说,这个我参与的外包项目黄了……
在后来不知道多久,曾经的公司倒闭了……
几年后我还接到银行的电话,问我这个公司的一个负责人我是否有联系,或许是追债的吧,和我又有什么关系呢?


一份不错的工作


真的不推荐新人去外包公司,因为没有人引路,都是自己摸索,如果可以的话,去个有人带的公司,或大或小,或多或少都是会有帮助的。


又是一个春节后,我开始寻找我的第二份工作,老家说七不出八不归,于是我初六就出门了,和前一年一样……
没过几天,我寻找到的第二份工作,并且还是一份甲方工作,从事汽车检测行业,IT部开发的主要是自用的产品,也是一份 .NET 开发工作。这份工作给我提供了不错的工作环境、好的同事或者也算不错的老板。2年时间,我慢慢学习,逐渐进步,可能从这之后我才算是一个合格一点的程序员。年初3月,老板找全体IT部门谈话:公司需要的产品都已经齐全,现在IT部门,软件方面也只需要一些小的功能调整,愿意留在公司的就留下来但是工资不会涨,不愿意留下来的你们就去找工作,找到下家你们随时可以离开。


你们看,我是不是说的没错,算是不错的老板吧。而且这份工作包午餐,加班有加班调休呢!


这两年我是逐渐成熟的两年,除了同事间的活动、聚餐外,我偶尔会去逛逛花鸟市场,我记得很多的日子我都是学习到10点多,对着我的笔记本,我已经不记得当初学的什么,怎么学习的,应该是没有系统的学习路线,也没人指引,这是我现在回头看的,这也导致了我现在的技术面广但是颇为杂乱,我的技术能力没有重心、飘飘的……


新入行的弟弟们,选好方向很重要,现在网上很多资源的,不能盲目学习不关联的技术栈,系统性的学习很重要,最好先有计划,按照计划路线学习。


三战


第三份工作,也就是我现在的工作,18年到现在5年半多了,其实原先没有准备在这份工作上停留这么久的,恰逢19年底-23年初的疫情,23年初我手头上又有一个不太能放下的项目,所以一直拖到现在。


找工作很难擦亮自己的眼睛,招聘的同事说的天花乱坠,可能与实际不符。进入公司后才发现,缴税是按最低的来,没有奖金,没有福利,公司不推荐、不鼓励加班(所以加班没有调休、没有加班费,工作做不完是你自己的问题),公司项目技术栈陈旧,面试说“造大炮”,进公司才发现是“做鞭炮”,而且刚进公司都算是大佬级别了,可见公司人员技术凋零,我很难想象这么些同事是怎么进入公司的。


我为什么这么说,因为现在的公司,我投过一次简历,来面试过一次没有通过,后来不小心又投了一次简历,人事应该没有注意,我又来了一次,到门口发现是这家,于是打电话,接电话的人说:既然来了就再聊聊。换了一个面试的经理,也是这样,我在 “二面” 后成功入职。


那时候我自认为我的能力介于.NET初级开发和中级开发之间,然后发现这样的水平能胜过公司的9成……,是的,我一点都不谦虚,因为项目质量太差。


5年,我有多次要离开的意图,先是疫情和大环境的原因束缚着我,我妈也劝我过了疫情再说。23年初,我负责了一个比较大的项目,也是一个大的客户,我觉得在做项目上这是一个成长,虽然现在我也不知道到底有没有长长那么1cm。我却因此放弃了一份即将拿到手的外企offer,说不后悔是假的。


快35岁了,我现在税前薪资 20k,作为一个快9年的老程序员了,这着实太低了。怪我这份工作入职工资就低,怪公司这些年涨幅太少,怪疫情耽误了我的跳槽和发展,怪自己年龄大了没有太多学习成长的精力。曾经风华正茂,怀着一个在上海发展,凭借一己之力留在上海的梦想,近乎破灭了。


如果一份工作配不上你的成长脚步,不要犹豫,换掉它。(如果薪资足够动人,当我没说)


番外


这些年里:



  1. 我学了网教,成功拿到了上交大的继续教育本科学历学位证书

  2. 我找中介,写各种项目技术文档,评了中级职称
    这算是这些年能拿出来的获得了


一、迷茫


一鼓作气,再而衰,三而截吗?


从成为程序员开始,我学过很多有关无关的技能,工作中一直算是半个全栈开发,最近两年前端稳定为了 vue,后端 .net/.net core。而.NET 在我国发展不太行,恰逢2024年春节前夕,笔者准备换工作,却难求一份前端、后端或 vue+.net 全栈开发、项目经理工作。



  • 前端我不专业,稍微欠缺了火候,我能行吗?

  • 有人说熟悉 C# 可以转游戏开发,可是我深知道语言仅仅是游戏开发以及其他开发的基础,对游戏开发一无所知的我转游戏开发稍显有些晚了。

  • 全栈开发,在上海,前端 VUE,后端 .NET 的工作屈指可数,又怎么有机会呢。

  • .NET 也少到快灭绝


下面是我从某照片网站关键词职位截图:


image.png

image.png

image.png

image.png

image.png

image.png


C#/.NET的职位真的有那么少吗?再排除名企和低薪工作,这不是快绝技(绝迹,爵迹)吗?
我没法和掘金大佬们相比,名企的职位我看都不带看的,看我多潇洒,可惜内心灰暗无比,我只是有自知之明!


我学过 Node.js、Python、java 几乎没有运用到实际项目中,我现在转 java、python 好像有些迟了吧?我相信一定会有人要的,但是薪资比 20k 低了多少我说不准,而且现在我是期望涨薪,低了能过得了自己心里的门槛吗?


如果是再整整 Node.js 开源项目,直接找 VUE+Node.js 会不会更好呢?毕竟都是 js 栈的东西,没什么门槛,深入学一个 Express.js 是不是能给自己机会呢?


我没有能力,没啥人脉,找不到私活做,我也没有 Plan B,有的都是迷茫和无奈。


二、抉择


我要如何抉择?



  1. 继续 VUE+.NET 的全栈模式,等到找到下份工作为止

  2. 深入学习 Node.js,转 VUE+Node.js 全栈模式,寻求新计划

  3. 深入学习 Python 或 Java,改变方向,置之死地而后生

  4. 转项目经理,我负责过不少项目,但是没有系统的学过项目管理,没有 PMP 证书,也不知道项目经理工作是否好找,另外,项目经理很多也是需要有对应的技术栈要求的,算是技术型项目经理吧??

  5. 换行业……没有相关工作经验谁要呢?或者把自己洗白重来?

  6. 创业……能力够吗,而且启动金从何而来?


呵呵,废物如我,活该碌碌无为,已近半生……

年龄到了,这可能也是我最后一次换工作了,或许我应该去学个 “水电工证书” 呢!


作者:程序园丁
来源:juejin.cn/post/7330442541439336448
收起阅读 »

一个巧妙的分库分表设计:异构索引表

前言 最近计划参与一个换书活动,翻到《企业IT架构转型之道阿里巴巴中台战略思想与架构实战》这本书时,回想起令我印象比较深刻的一个知识点:“异构索引表”,所以在此记录并分享,和大家共同学习交流。 异构索引表的作用 如果《一致性哈希在分库分表的应用》说的是分库分表...
继续阅读 »

前言


最近计划参与一个换书活动,翻到《企业IT架构转型之道阿里巴巴中台战略思想与架构实战》这本书时,回想起令我印象比较深刻的一个知识点:“异构索引表”,所以在此记录并分享,和大家共同学习交流。


异构索引表的作用


如果《一致性哈希在分库分表的应用》说的是分库分表的方法和策略,那么本文所探讨的“异构索引表”,则是在实施分库分表过程中一个非常巧妙的设计,可以有效的解决分库分表的查询问题。


分库分表的查询问题


问题说明


在哈希分库分表时,为了避免分布不均匀造成的“数据倾斜”,通常会选择一些数据唯一的字段进行哈希操作,比如ID。


以订单表为例,通常有(id、uid、status、amount)等字段,通过id进行哈希取模运算分库分表之后,效果如下图


哈希分库分表效果


这样分库分表的方法没有问题,但是,在后期的开发和维护过程中,可能会存在潜在的问题。


举个例子:现在要查询uid为1的记录,应该去哪个表或库去查询?


对于用户来讲,这个场景可以说是非常频繁的。


这个时候就会发现,要想查询uid为1的记录,只能去所有的库或分表上进行查询,也就是所谓的“广播查询”。


整个查询过程大概是这样的


分库分表查询


性能问题


显然,整个查询过程需要进行全库扫描,涉及到多次的网络数据传输,一定会导致查询速度的降低和延迟的增加


数据聚合问题


另外,当这个用户有成千上万条数据时,不得已要在一个节点进行排序、分页、聚合等计算操作,需要消耗大量的计算资源和内存空间。对系统造成的负担也会影响查询性能。


这是一个非常典型的“事务边界大”的案例,即“一条SQL到所有的数据库去执行”。



那么如何解决这一痛点?



解决分库分表的查询问题


本文重点:“异构索引表”是可以解决这个问题的。


引入异构索引表


简单来说,“异构索引表”是一个拿空间换时间的设计。具体如下:


添加订单数据时,除了根据订单ID进行哈希取模运算将订单数据维护到对应的表中,还要对uid进行哈希取模运算,将uid和订单id维护在另一张表中,如图所示。


异构索引表


引入“异构索引表”后,因为同一个uid经过哈希取模运算后得到的结果是一致的,所以,该uid所有的订单id也一定会被分布到同一张user_order表中。


当查询uid为1的订单记录时,就可以有效地解决数据聚合存在的计算资源消耗全库扫描的低效问题了。


接下来,通过查询过程,看看这两个问题是怎么解决的。


引入后的查询过程


引入“异构索引表”后,查询uid为1的订单记录时,具体过程分为以下几步:



  1. 应用向中间件发送select * from order where uid = 1,请求查询uid为1的订单记录。

  2. 中间件根据uid路由到“异构索引表”:user_order,获得该uid相关的订单ID列表(排序、分页可以在此sql操作)。

  3. 中间件根据返回的订单ID,再次准确路由到对应的订单表:order

  4. 中间件将分散的订单数据进行聚合返回给应用。


引入异构索引表查询


看上去引入“异构索引表”之后,多了一个查询步骤,但换来的是:



  1. 根据订单ID准确路由到订单表,避免了全库扫描。

  2. user_order表进行了排序、分页等操作,避免大量数据回到中间件去计算。


异构索引表解决不了的场景


“异构索引表”只适合简单的分库分表查询场景,如果存在复杂的查询场景,还是需要借助搜索引擎来实现。


总结


异构索引表作为一种巧妙的设计,避免了分库分表查询存在的两个问题:全库扫描不必要的计算资源消耗


但是,异构索引表并不适用所有场景,对于复杂的查询场景可能需要结合其他技术或策略来解决问题。


作者:王二蛋呀
来源:juejin.cn/post/7372070947820109851
收起阅读 »

表设计的18条军规

前言 对于后端开发同学来说,访问数据库,是代码中必不可少的一个环节。 系统中收集到用户的核心数据,为了安全性,我们一般会存储到数据库,比如:mysql,oracle等。 后端开发的日常工作,需要不断的建库和建表,来满足业务需求。 通常情况下,建库的频率比建表要...
继续阅读 »

前言


对于后端开发同学来说,访问数据库,是代码中必不可少的一个环节。


系统中收集到用户的核心数据,为了安全性,我们一般会存储到数据库,比如:mysql,oracle等。


后端开发的日常工作,需要不断的建库和建表,来满足业务需求。


通常情况下,建库的频率比建表要低很多,所以,我们这篇文章主要讨论建表相关的内容。


如果我们在建表的时候不注意细节,等后面系统上线之后,表的维护成本变得非常高,而且很容易踩坑。


今天就跟大家一起聊聊,数据库建表的18个小技巧。


文章中介绍的很多细节,我在工作中踩过坑,并且实践过的,非常有借鉴意义,希望对你会有所帮助。


图片


1.名字


建表的时候,给字段索引起个好名字,真的太重要了。


1.1 见名知意


名字就像字段索引的一张脸,可以给人留下第一印象。


好的名字,言简意赅,见名知意,让人心情愉悦,能够提高沟通和维护成本。


坏的名字,模拟两可,不知所云。而且显得杂乱无章,看得让人抓狂。


反例:


用户名称字段定义成:yong_hu_ming、用户_name、name、user_name_123456789

你看了可能会一脸懵逼,这是什么骚操作?


正例:


用户名称字段定义成:user_name


温馨提醒一下,名字也不宜过长,尽量控制在30个字符以内。



1.2 大小写


名字尽量都用小写字母,因为从视觉上,小写字母更容易让人读懂。


反例:


字段名:PRODUCT_NAME、PRODUCT_name

全部大写,看起来有点不太直观。而一部分大写,一部分小写,让人看着更不爽。


正例:


字段名:product_name

名字还是使用全小写字母,看着更舒服。


1.3 分隔符


很多时候,名字为了让人好理解,有可能会包含多个单词。


那么,多个单词间的分隔符该用什么呢?


反例:


字段名:productname、productName、product name、product@name

单词间没有分隔,或者单词间用驼峰标识,或者单词间用空格分隔,或者单词间用@分隔,这几种方式都不太建议。


正例:


字段名:product_name

强烈建议大家在单词间用_分隔。


1.4 表名


对于表名,在言简意赅,见名知意的基础之上,建议带上业务前缀


如果是订单相关的业务表,可以在表名前面加个前缀:order_


例如:order_pay、order_pay_detail等。


如果是商品相关的业务表,可以在表名前面加个前缀:product_


例如:product_spu,product_sku等。


这样做的好处是为了方便归类,把相同业务的表,可以非常快速的聚集到一起。


另外,还有有个好处是,如果哪天有非订单的业务,比如:金融业务,也需要建一个名字叫做pay的表,可以取名:finance_pay,就能非常轻松的区分。


这样就不会出现同名表的情况。


1.5 字段名称


字段名称是开发人员发挥空间最大,但也最容易发生混乱的地方。


比如有些表,使用flag表示状态,另外的表用status表示状态。


可以统一一下,使用status表示状态。


如果一个表使用了另一个表的主键,可以在另一张表的名后面,加_id_sys_no,例如:


在product_sku表中有个字段,是product_spu表的主键,这时候可以取名:product_spu_id或product_spu_sys_no。


还有创建时间,可以统一成:create_time,修改时间统一成:update_time。


删除状态固定为:delete_status。


其实还有很多公共字段,在不同的表之间,可以使用全局统一的命名规则,定义成相同的名称,以便于大家好理解。


1.6 索引名


在数据库中,索引有很多种,包括:主键、普通索引、唯一索引、联合索引等。


每张表的主键只有一个,一般使用:id或者sys_no命名。


普通索引和联合索引,其实是一类。在建立该类索引时,可以加ix_前缀,比如:ix_product_status。


唯一索引,可以加ux_前缀,比如:ux_product_code。


2.字段类型


在设计表时,我们在选择字段类型时,可发挥空间很大。


时间格式的数据有:date、datetime和timestamp等等可以选择。


字符类型的数据有:varchar、char、text等可以选择。


数字类型的数据有:int、bigint、smallint、tinyint等可以选择。


说实话,选择很多,有时候是一件好事,也可能是一件坏事。


如何选择一个合适的字段类型,变成了我们不得不面对的问题。


如果字段类型选大了,比如:原本只有1-10之间的10个数字,结果选了bigint,它占8个字节。


其实,1-10之间的10个数字,每个数字1个字节就能保存,选择tinyint更为合适。


这样会白白浪费7个字节的空间。


如果字段类型择小了,比如:一个18位的id字段,选择了int类型,最终数据会保存失败。


所以选择一个合适的字段类型,还是非常重要的一件事情。


以下原则可以参考一下:



  1. 尽可能选择占用存储空间小的字段类型,在满足正常业务需求的情况下,从小到大,往上选。

  2. 如果字符串长度固定,或者差别不大,可以选择char类型。如果字符串长度差别较大,可以选择varchar类型。

  3. 是否字段,可以选择bit类型。

  4. 枚举字段,可以选择tinyint类型。

  5. 主键字段,可以选择bigint类型。

  6. 金额字段,可以选择decimal类型。

  7. 时间字段,可以选择timestamp或datetime类型。


3.字段长度


前面我们已经定义好了字段名称,选择了合适的字段类型,接下来,需要重点关注的是字段长度了。


比如:varchar(20),biginit(20)等。


那么问题来了,varchar代表的是字节长度,还是字符长度呢?


答:在mysql中除了varcharchar是代表字符长度之外,其余的类型都是代表字节长度。


biginit(n) 这个n表示什么意思呢?


假如我们定义的字段类型和长度是:bigint(4),bigint实际长度是8个字节。


现在有个数据a=1,a显示4个字节,所以在不满4个字节时前面填充0(前提是该字段设置了zerofill属性),比如:0001。


当满了4个字节时,比如现在数据是a=123456,它会按照实际的长度显示,比如:123456。


但需要注意的是,有些mysql客户端即使满了4个字节,也可能只显示4个字节的内容,比如会显示成:1234。


所以bigint(4),这里的4表示显示的长度为4个字节,实际长度还是占8个字节。


4.字段个数


我们在建表的时候,一定要对字段个数做一些限制。


我之前见过有人创建的表,有几十个,甚至上百个字段,表中保存的数据非常大,查询效率很低。


如果真有这种情况,可以将一张大表拆成多张小表,这几张表的主键相同。


建议每表的字段个数,不要超过20个。


5. 主键


在创建表时,一定要创建主键


因为主键自带了主键索引,相比于其他索引,主键索引的查询效率最高,因为它不需要回表。


此外,主键还是天然的唯一索引,可以根据它来判重。


单个数据库中,主键可以通过AUTO_INCREMENT,设置成自动增长的。


但在分布式数据库中,特别是做了分库分表的业务库中,主键最好由外部算法(比如:雪花算法)生成,它能够保证生成的id是全局唯一的。


除此之外,主键建议保存跟业务无关的值,减少业务耦合性,方便今后的扩展。


不过我也见过,有些一对一的表关系,比如:用户表和用户扩展表,在保存数据时是一对一的关系。


这样,用户扩展表的主键,可以直接保存用户表的主键。


6.存储引擎


mysql8以前的版本,默认的存储引擎是myisam,而mysql8以后的版本,默认的存储引擎变成了innodb


之前我们还在创建表时,还一直纠结要选哪种存储引擎?


myisam的索引和数据分开存储,而有利于查询,但它不支持事务和外键等功能。


innodb虽说查询性能,稍微弱一点,但它支持事务和外键等,功能更强大一些。


以前的建议是:读多写少的表,用myisam存储引擎。而写多读多的表,用innodb。


但虽说mysql对innodb存储引擎性能的不断优化,现在myisam和innodb查询性能相差已经越来越小。


所以,建议我们在使用mysql8以后的版本时,直接使用默认的innodb存储引擎即可,无需额外修改存储引擎。


7. NOT NULL


在创建字段时,需要选择该字段是否允许为NULL


我们在定义字段时,应该尽可能明确该字段NOT NULL


为什么呢?


我们主要以innodb存储引擎为例,myisam存储引擎没啥好说的。


主要有以下原因:



  1. 在innodb中,需要额外的空间存储null值,需要占用更多的空间。

  2. null值可能会导致索引失效。

  3. null值只能用is null或者is not null判断,用=号判断永远返回false。


因此,建议我们在定义字段时,能定义成NOT NULL,就定义成NOT NULL。


但如果某个字段直接定义成NOT NULL,万一有些地方忘了给该字段写值,就会insert不了数据。


这也算合理的情况。


但有一种情况是,系统有新功能上线,新增了字段。上线时一般会先执行sql脚本,再部署代码。


由于老代码中,不会给新字段赋值,则insert数据时,也会报错。


由此,非常有必要给NOT NULL的字段设置默认值,特别是后面新增的字段。


例如:


alter table product_sku add column  brand_id int(10not null default 0;

8.外键


在mysql中,是存在外键的。


外键存在的主要作用是:保证数据的一致性完整性


例如:


create table class (
  id int(10primary key auto_increment,
  cname varchar(15)
);

有个班级表class。


然后有个student表:


create table student(
  id int(10primary key auto_increment,
  name varchar(15not null,
  gender varchar(10not null,
  cid int,
  foreign key(cid) references class(id)
);

其中student表中的cid字段,保存的class表的id,这时通过foreign key增加了一个外键。


这时,如果你直接通过student表的id删除数据,会报异常:


a foreign key constraint fails

必须要先删除class表对于的cid那条数据,再删除student表的数据才行,这样能够保证数据的一致性和完整性。



顺便说一句:只有存储引擎是innodb时,才能使用外键。



如果只有两张表的关联还好,但如果有十几张表都建了外键关联,每删除一次主表,都需要同步删除十几张子表,很显然性能会非常差。


因此,互联网系统中,一般建议不使用外键。因为这类系统更多的是为了性能考虑,宁可牺牲一点数据一致性和完整性。


除了外键之外,存储过程触发器也不太建议使用,他们都会影响性能。


9. 索引


在建表时,除了指定主键索引之外,还需要创建一些普通索引


例如:


create table product_sku(
  id int(10primary key auto_increment,
  spu_id int(10not null,
  brand_id int(10not null,
  name varchar(15not null
);

在创建商品表时,使用spu_id(商品组表)和brand_id(品牌表)的id。


像这类保存其他表id的情况,可以增加普通索引:


create table product_sku (
  id int(10primary key auto_increment,
  spu_id int(10not null,
  brand_id int(10not null,
  name varchar(15not null,
  KEY `ix_spu_id` (`spu_id`USING BTREE,
  KEY `ix_brand_id` (`brand_id`USING BTREE
);

后面查表的时候,效率更高。


但索引字段也不能建的太多,可能会影响保存数据的效率,因为索引需要额外的存储空间。


建议单表的索引个数不要超过:5个。


如果在建表时,发现索引个数超过5个了,可以删除部分普通索引,改成联合索引


顺便说一句:在创建联合索引的时候,需要使用注意最左匹配原则,不然,建的联合索引效率可能不高。


对于数据重复率非常高的字段,比如:状态,不建议单独创建普通索引。因为即使加了索引,如果mysql发现全表扫描效率更高,可能会导致索引失效。


如果你对索引失效问题比较感兴趣,可以看看我的另一篇文章《聊聊索引失效的10种场景,太坑了》,里面有非常详细的介绍。


10.时间字段


时间字段的类型,我们可以选择的范围还是比较多的,目前mysql支持:date、datetime、timestamp、varchar等。


varchar类型可能是为了跟接口保持一致,接口中的时间类型是String。


但如果哪天我们要通过时间范围查询数据,效率会非常低,因为这种情况没法走索引。


date类型主要是为了保存日期,比如:2020-08-20,不适合保存日期和时间,比如:2020-08-20 12:12:20。


datetimetimestamp类型更适合我们保存日期和时间


但它们有略微区别。



  • timestamp:用4个字节来保存数据,它的取值范围为1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07。此外,它还跟时区有关。

  • datetime:用8个字节来保存数据,它的取值范围为1000-01-01 00:00:00 ~ 9999-12-31 23:59:59。它跟时区无关。


优先推荐使用datetime类型保存日期和时间,可以保存的时间范围更大一些。



温馨提醒一下,在给时间字段设置默认值是,建议不要设置成:0000-00-00 00:00:00,不然查询表时可能会因为转换不了,而直接报错。



11.金额字段


mysql中有多个字段可以表示浮点数:float、double、decimal等。


floatdouble可能会丢失精度,因此推荐大家使用decimal类型保存金额。


一般我们是这样定义浮点数的:decimal(m,n)。


其中n是指小数的长度,而m是指整数加小数的总长度。


假如我们定义的金额类型是这样的:decimal(10,2),则表示整数长度是8位,并且保留2位小数。


12. json字段


我们在设计表结构时,经常会遇到某个字段保存的数据值不固定的需求。


举个例子,比如:做异步excel导出功能时,需要在异步任务表中加一个字段,保存用户通过前端页面选择的查询条件,每个用户的查询条件可能都不一样。


这种业务场景,使用传统的数据库字段,不太好实现。


这时候就可以使用MySQL的json字段类型了,可以保存json格式的结构化数据。


保存和查询数据都是非常方便的。


MySQL还支持按字段名称或者字段值,查询json中的数据。


13.唯一索引


唯一索引在我们实际工作中,使用频率相当高。


你可以给单个字段,加唯一索引,比如:组织机构code。


也可以给多个字段,加一个联合的唯一索引,比如:分类编号、单位、规格等。


单个的唯一索引还好,但如果是联合的唯一索引,字段值出现null时,则唯一性约束可能会失效。


关于唯一索引失效的问题,感兴趣的小伙伴可以看看我的另一篇文章《明明加了唯一索引,为什么还是产生重复数据?》。



创建唯一索引时,相关字段一定不能包含null值,否则唯一性会失效。



14.字符集


mysql中支持的字符集有很多,常用的有:latin1、utf-8、utf8mb4、GBK等。


这4种字符集情况如下:图片


latin1容易出现乱码问题,在实际项目中使用比较少。


GBK支持中文,但不支持国际通用字符,在实际项目中使用也不多。


从目前来看,mysql的字符集使用最多的还是:utf-8utf8mb4


其中utf-8占用3个字节,比utf8mb4的4个字节,占用更小的存储空间。


但utf-8有个问题:即无法存储emoji表情,因为emoji表情一般需要4个字节。


由此,使用utf-8字符集,保存emoji表情时,数据库会直接报错。


所以,建议在建表时字符集设置成:utf8mb4,会省去很多不必要的麻烦。


15. 排序规则


不知道,你关注过没,在mysql中创建表时,有个COLLATE参数可以设置。


例如:


CREATE TABLE `order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `code` varchar(20COLLATE utf8mb4_bin NOT NULL,
  `name` varchar(30COLLATE utf8mb4_bin NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_code` (`code`),
  KEY `un_code_name` (`code`,`name`USING BTREE,
  KEY `idx_name` (`name`)
ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

它是用来设置排序规则的。


字符排序规则跟字符集有关,比如:字符集如果是utf8mb4,则字符排序规则也是以:utf8mb4_开头的,常用的有:utf8mb4_general_ciutf8mb4_bin等。


其中utf8mb4_general_ci排序规则,对字母的大小写不敏感。说得更直白一点,就是不区分大小写。


而utf8mb4_bin排序规则,对字符大小写敏感,也就是区分大小写。


说实话,这一点还是非常重要的。


假如order表中现在有一条记录,name的值是大写的YOYO,但我们用小写的yoyo去查,例如:


select * from order where name='yoyo';

如果字符排序规则是utf8mb4_general_ci,则可以查出大写的YOYO的那条数据。


如果字符排序规则是utf8mb4_bin,则查不出来。


由此,字符排序规则一定要根据实际的业务场景选择,否则容易出现问题。


最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。


你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


image.png


进群方式


添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。


16.大字段


我们在创建表时,对一些特殊字段,要额外关注,比如:大字段,即占用较多存储空间的字段。


比如:用户的评论,这就属于一个大字段,但这个字段可长可短。


但一般会对评论的总长度做限制,比如:最多允许输入500个字符。


如果直接定义成text类型,可能会浪费存储空间,所以建议将这类字段定义成varchar类型的存储效率更高。


当然,我还见过更大的字段,即该字段直接保存合同数据。


一个合同可能会占几Mb


在mysql中保存这种数据,从系统设计的角度来说,本身就不太合理。


像合同这种非常大的数据,可以保存到mongodb中,然后在mysql的业务表中,保存mongodb表的id。


17.冗余字段


我们在设计表的时候,为了性能考虑,提升查询速度,有时可以冗余一些字段。


举个例子,比如:订单表中一般会有userId字段,用来记录用户的唯一标识。


但很多订单的查询页面,或者订单的明细页面,除了需要显示订单信息之外,还需要显示用户ID和用户名称。


如果订单表和用户表的数据量不多,我们可以直接用userId,将这两张表join起来,查询出用户名称。


但如果订单表和用户表的数据量都非常多,这样join是比较消耗查询性能的。


这时候我们可以通过冗余字段的方案,来解决性能问题。


我们可以在订单表中,可以再加一个userName字段,在系统创建订单时,将userId和userName同时写值。


当然订单表中历史数据的userName是空的,可以刷一下历史数据。


这样调整之后,后面只需要查询订单表,即可查询出我们所需要的数据。


不过冗余字段的方案,有利也有弊。


对查询性能有利。


但需要额外的存储空间,还可能会有数据不一致的情况,比如用户名称修改了。


我们在实际业务场景中,需要综合评估,冗余字段方案不适用于所有业务场景。


18.注释


我们在做表设计的时候,一定要把表和相关字段的注释加好。


例如下面这样的:


CREATE TABLE `sys_dept` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(30NOT NULL COMMENT '名称',
  `pid` bigint NOT NULL COMMENT '上级部门',
  `valid_status` tinyint(1NOT NULL DEFAULT 1 COMMENT '有效状态 1:有效 0:无效',
  `create_user_id` bigint NOT NULL COMMENT '创建人ID',
  `create_user_name` varchar(30NOT NULL COMMENT '创建人名称',
  `create_time` datetime(3DEFAULT NULL COMMENT '创建日期',
  `update_user_id` bigint DEFAULT NULL COMMENT '修改人ID',
  `update_user_name` varchar(30)  DEFAULT NULL COMMENT '修改人名称',
  `update_time` datetime(3DEFAULT NULL COMMENT '修改时间',
  `is_del` tinyint(1DEFAULT '0' COMMENT '是否删除 1:已删除 0:未删除',
  PRIMARY KEY (`id`USING BTREE,
  KEY `index_pid` (`pid`USING BTREE
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='部门';

表和字段的注释,都列举的非常详细。


特别是有些状态类型的字段,比如:valid_status字段,该字段表示有效状态, 1:有效 0:无效。


让人可以一目了然,表和字段是干什么用的,字段的值可能有哪些。


最怕的情况是,你在表中创建了很多status字段,每个字段都有1、2、3、4、5、6、7、8、9等多个值。


没有写什么注释。


谁都不知道1代表什么含义,2代表什么含义,3代表什么含义。


可能刚开始你还记得。


但系统上线使用一年半载之后,可能连你自己也忘记了这些status字段,每个值的具体含义了,埋下了一个巨坑。


由此,我们在做表设计时,一定要写好相关的注释,并且经常需要更新这些注释。




作者:苏三说技术
来源:juejin.cn/post/7352789840352690185
收起阅读 »

接口设计的18条军规

大家好,我是苏三,又跟大家见面了。 前言 之前写过一篇文章《表设计的18条军规》,发表之前,在全网广受好评。 今天延续设计的话题,给大家总结了接口设计的18条军规,希望对你会有所帮助。 1. 签名 为了防止API接口中的数据被篡改,很多时候我们需要对API接...
继续阅读 »

大家好,我是苏三,又跟大家见面了。


前言


之前写过一篇文章《表设计的18条军规》,发表之前,在全网广受好评。


今天延续设计的话题,给大家总结了接口设计的18条军规,希望对你会有所帮助。


图片


1. 签名


为了防止API接口中的数据被篡改,很多时候我们需要对API接口做签名


接口请求方将请求参数 + 时间戳 + 密钥拼接成一个字符串,然后通过md5等hash算法,生成一个前面sign。


然后在请求参数或者请求头中,增加sign参数,传递给API接口。


API接口的网关服务,获取到该sign值,然后用相同的请求参数 + 时间戳 + 密钥拼接成一个字符串,用相同的m5算法生成另外一个sign,对比两个sign值是否相等。


如果两个sign相等,则认为是有效请求,API接口的网关服务会将给请求转发给相应的业务系统。


如果两个sign不相等,则API接口的网关服务会直接返回签名错误。


问题来了:签名中为什么要加时间戳?


答:为了安全性考虑,防止同一次请求被反复利用,增加了密钥没破解的可能性,我们必须要对每次请求都设置一个合理的过期时间,比如:15分钟。


这样一次请求,在15分钟之内是有效的,超过15分钟,API接口的网关服务会返回超过有效期的异常提示。


目前生成签名中的密钥有两种形式:


一种是双方约定一个固定值privateKey。


另一种是API接口提供方给出AK/SK两个值,双方约定用SK作为签名中的密钥。AK接口调用方作为header中的accessKey传递给API接口提供方,这样API接口提供方可以根据AK获取到SK,而生成新的sgin。


2. 加密


有些时候,我们的API接口直接传递的非常重要的数据,比如:用户的登录密码、银彳亍卡号、转账金额、用户身-份-证等,如果将这些参数,直接明文,暴露到公网上是非常危险的事情。


由此,我们需要对数据进行加密


比如:用户注册接口,用户输入了用户名和密码之后,需要将密码加密。


我们可以使用AES对称加密算法。


在前端使用公钥对用户密码加密。


然后注册接口中,可以使用密钥解密,做一些业务需求校验。然后再换成其他的加密方式加密,保存到数据库当中。


3. ip白名单


为了进一步加强API接口的安全性,防止接口的签名或者加密被破解了,攻击者可以在自己的服务器上请求该接口。


需求限制请求ip,增加ip白名单


只有在白名单中的ip地址,才能成功请求API接口,否则直接返回无访问权限。


ip白名单也可以加在API网关服务上。


但也要防止公司的内部应用服务器被攻破,这种情况也可以从内部服务器上发起API接口的请求。


这时候就需要增加web防火墙了,比如:ModSecurity等。


4. 限流


如果你的API接口被第三方平台调用了,这就意味着着,调用频率是没法控制的。


第三方平台调用你的API接口时,如果并发量一下子太高,可能会导致你的API服务不可用,接口直接挂掉。


由此,必须要对API接口做限流


限流方法有三种:



  1. 对请求ip做限流:比如同一个ip,在一分钟内,对API接口总的请求次数,不能超过10000次。

  2. 对请求接口做限流:比如同一个ip,在一分钟内,对指定的API接口,请求次数不能超过2000次。

  3. 对请求用户做限流:比如同一个AK/SK用户,在一分钟内,对API接口总的请求次数,不能超过10000次。


我们在实际工作中,可以通过nginxredis或者gateway实现限流的功能。


5. 参数校验


我们需要对API接口做参数校验,比如:校验必填字段是否为空,校验字段类型,校验字段长度,校验枚举值等等。


这样做可以拦截一些无效的请求。


比如在新增数据时,字段长度超过了数据字段的最大长度,数据库会直接报错。


但这种异常的请求,我们完全可以在API接口的前期进行识别,没有必要走到数据库保存数据那一步,浪费系统资源。


有些金额字段,本来是正数,但如果用户传入了负数,万一接口没做校验,可能会导致一些没必要的损失。


还有些状态字段,如果不做校验,用户如果传入了系统中不存在的枚举值,就会导致保存的数据异常。


由此可见,做参数校验是非常有必要的。


在Java中校验数据使用最多的是hiberateValidator框架,它里面包含了@Null、@NotEmpty、@Size、@Max、@Min等注解。


用它们校验数据非常方便。


当然有些日期字段和枚举字段,可能需要通过自定义注解的方式实现参数校验。


6. 统一返回值


我之前调用过别人的API接口,正常返回数据是一种json格式,比如:


{
    "code":0,
    "message":null,
    "data":[{"id":123,"name":"abc"}]
},

签名错误返回的json格式:


{
    "code":1001,
    "message":"签名错误",
    "data":null
}

没有数据权限返回的json格式:


{
    "rt":10,
    "errorMgt":"没有权限",
    "result":null
}

这种是比较坑的做法,返回值中有多种不同格式的返回数据,这样会导致对接方很难理解。


出现这种情况,可能是API网关定义了一直返回值结构,业务系统定义了另外一种返回值结构。如果是网关异常,则返回网关定义的返回值结构,如果是业务系统异常,则返回业务系统的返回值结构。


但这样会导致API接口出现不同的异常时,返回不同的返回值结构,非常不利于接口的维护。


其实这个问题我们可以在设计API网关时解决。


业务系统在出现异常时,抛出业务异常的RuntimeException,其中有个message字段定义异常信息。


所有的API接口都必须经过API网关,API网关捕获该业务异常,然后转换成统一的异常结构返回,这样能统一返回值结构。


7. 统一封装异常


我们的API接口需要对异常进行统一处理。


不知道你有没有遇到过这种场景:有时候在API接口中,需要访问数据库,但表不存在,或者sql语句异常,就会直接把sql信息在API接口中直接返回。


返回值中包含了异常堆栈信息数据库信息错误代码和行数等信息。


如果直接把这些内容暴露给第三方平台,是很危险的事情。


有些不法分子,利用接口返回值中的这些信息,有可能会进行sql注入或者直接脱库,而对我们系统造成一定的损失。


因此非常有必要对API接口中的异常做统一处理,把异常转换成这样:


{
    "code":500,
    "message":"服务器内部错误",
    "data":null
}

返回码code500,返回信息message服务器内部异常


这样第三方平台就知道是API接口出现了内部问题,但不知道具体原因,他们可以找我们排查问题。


我们可以在内部的日志文件中,把堆栈信息、数据库信息、错误代码行数等信息,打印出来。


我们可以在gateway中对异常进行拦截,做统一封装,然后给第三方平台的是处理后没有敏感信息的错误信息。


8. 请求日志


在第三方平台请求你的API接口时,接口的请求日志非常重要,通过它可以快速的分析和定位问题。


我们需要把API接口的请求url、请求参数、请求头、请求方式、响应数据和响应时间等,记录到日志文件中。


最好有traceId,可以通过它串联整个请求的日志,过滤多余的日志。


当然有些时候,请求日志不光是你们公司开发人员需要查看,第三方平台的用户也需要能查看接口的请求日志。


这时就需要把日志落地到数据库,比如:mongodb或者elastic search,然后做一个UI页面,给第三方平台的用户开通查看权限。这样他们就能在外网查看请求日志了,他们自己也能定位一部分问题。


9. 幂等设计


第三方平台极有可能在极短的时间内,请求我们接口多次,比如:在1秒内请求两次。有可能是他们业务系统有bug,或者在做接口调用失败重试,因此我们的API接口需要做幂等设计


也就是说要支持在极短的时间内,第三方平台用相同的参数请求API接口多次,第一次请求数据库会新增数据,但第二次请求以后就不会新增数据,但也会返回成功。


这样做的目的是不会产生错误数据。


我们在日常工作中,可以通过在数据库中增加唯一索引,或者在redis保存requestId和请求参来保证接口幂等性。


对接口幂等性感兴趣的小伙伴,可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》,里面有非常详细的介绍。


10. 限制记录条数


对于对我提供的批量接口,一定要限制请求的记录条数


如果请求的数据太多,很容易造成API接口超时等问题,让API接口变得不稳定。


通常情况下,建议一次请求中的参数,最多支持传入500条记录。


如果用户传入多余500条记录,则接口直接给出提示。


建议这个参数做成可配置的,并且要事先跟第三方平台协商好,避免上线后产生不必要的问题。


对于一次性查询的数据太多的情况,我们需要将接口设计成分页查询返回的。


11. 压测


上线前我们务必要对API接口做一下压力测试,知道各个接口的qps情况。


以便于我们能够更好的预估,需要部署多少服务器节点,对于API接口的稳定性至关重要。


之前虽说对API接口做了限流,但是实际上API接口是否能够达到限制的阀值,这是一个问号,如果不做压力测试,是有很大风险的。


比如:你API接口限流1秒只允许50次请求,但实际API接口只能处理30次请求,这样你的API接口也会处理不过来。


我们在工作中可以用jmeter或者apache benc对API接口做压力测试。


12. 异步处理


一般的API接口的逻辑都是同步处理的,请求完之后立刻返回结果。


但有时候,我们的API接口里面的业务逻辑非常复杂,特别是有些批量接口,如果同步处理业务,耗时会非常长。


这种情况下,为了提升API接口的性能,我们可以改成异步处理


在API接口中可以发送一条mq消息,然后直接返回成功。之后,有个专门的mq消费者去异步消费该消息,做业务逻辑处理。


直接异步处理的接口,第三方平台有两种方式获取到。


第一种方式是:我们回调第三方平台的接口,告知他们API接口的处理结果,很多支付接口就是这么玩的。


第二种方式是:第三方平台通过轮询调用我们另外一个查询状态的API接口,每隔一段时间查询一次状态,传入的参数是之前的那个API接口中的id集合。


13. 数据脱敏


有时候第三方平台调用我们API接口时,获取的数据中有一部分是敏感数据,比如:用户手机号、银彳亍卡号等等。


这样信息如果通过API接口直接保留到外网,是非常不安全的,很容易造成用户隐私数据泄露的问题。


这就需要对部分数据做数据脱敏了。


我们可以在返回的数据中,部分内容用星号代替。


已用户手机号为例:182****887


这样即使数据被泄露了,也只泄露了一部分,不法分子拿到这份数据也没啥用。


14. 完整的接口文档


说实话,一份完整的API接口文档,在双方做接口对接时,可以减少很多沟通成本,让对方少走很多弯路。


接口文档中需要包含如下信息:



  1. 接口地址

  2. 请求方式,比如:post或get

  3. 请求参数和字段介绍

  4. 返回值和字段介绍

  5. 返回码和错误信息

  6. 加密或签名示例

  7. 完整的请求demo

  8. 额外的说明,比如:开通ip白名单。


接口文档中最好能够统一接口和字段名称的命名风格,比如都用驼峰标识命名。


统一字段的类型和长度,比如:id字段用Long类型,长度规定20。status字段用int类型,长度固定2等。


统一时间格式字段,比如:time用String类型,格式为:yyyy-MM-dd HH:mm:ss。


接口文档中写明AK/SK和域名,找某某单独提供等。


最近建了一些高质量的粉丝群,里面可以交流技术,有工作内推,有粉丝福利。


进群方式


添加苏三的私人微信:su_san_java,备注:掘金+粉丝,即可加入。


15. 请求方式


接口支持的请求方式有很多,比如:GET、POST、PUT、DELETE等等。


我们在设计接口的时候,要根据实际情况选择使用哪种请求方式。


实际工作中使用最多的是:GETPOST,这两种请求方式。


如果没有输入参数的接口,可以使用GET请求方式,问题不大。


如果有输入参数的接口,推荐使用POST请求方式,坑更少。


主要原因有下面两点:



  1. POST请求方式更容易扩展参数,特别是在Fegin调用接口的场景下,比如:增加一个参数,调用方可以不用修改代码。而GET请求方式,需要修改代码,否则编译会出错。

  2. GET请求方式的参数,有长度限制,最长是5000个字符,而POST请求方式对参数的长度没有做限制,可以传入更长的参数值。


16. 请求头


对于一些公共的功能,比如:接口的权限验证,或者接口的traceId参数。


我们在设计接口的时候,不用把所有的参数,都放入接口的请求参数中。


有些参数可以放到Header请求头中。


比如:我们需求记录每个请求的traceId,不用在所有接口中都加traceId字段。


而改成让用户在header中传入traceId,在服务端使用统一的拦截器解析header,即可获取该traceId了。


17. 批量


我们在设计接口的时候,无论是查询数据、添加数据、修改数据,还是删除的场景,都应该考虑一下能否设计成批量的。


很多时候,需要通过id查询数据详情,比如:通过订单id,查询订单详情。


如果你的接口只支持,通过一个id,查询一个订单的详情。


那么,后面需要通过多个id,查询多个订单详情的时候,就需要额外增加接口了。


如果你添加数据的接口,只支持一条数据一条数据的添加。


后面,有个job需要一次性添加1000条数据的时候,这时在代码中循环1000次,一个个添加,这种做法效率比较低。


为了让你的接口设计的更加通用,满足更多的业务场景,能设计成批量的,尽量别设计成单个的。


18. 职责单一


我之前见过有些小伙伴设计的接口,在入参中各种条件都支持,在Service层有N多的if...else判断。


而且返回的实体类中,包含了各种场景下的返回值字段,字段很多很全。


接口上线一年之后,自己可能都忘了,在哪些业务场景下,要传入哪些字段,返回值是哪些字段。


这类接口的维护成本非常高,而且又不敢轻易重构,怕改了A业务场景,影响B业务场景的功能,这种接口让人非常痛苦的。


好的接口设计原则是:职责单一


比如用户下单的场景,有web端和移动端。


而每个端都有普通下单和快速下单,两种不同的业务场景。


我们在设计接口的时候,可以将web端和移动端的接口在controller层完全分开。


/web/v1/order
/mobile/v1/order

并且将普通下单和快速下单也分开:


/web/v1/order/create
/web/v1/order/fastCreate
/mobile/v1/order/create
/mobile/v1/order/fastCreate

这样可以设计成4个接口。


业务逻辑更清晰一些,方便后面维护。




作者:苏三说技术
来源:juejin.cn/post/7372094258793414710
收起阅读 »

记录裁员后的半年前端求职经历

普通的人生终起波澜 去年下半年应该算是我毕业以来发生人生变故最多的一段时间。 先是 7 月份的时候发作了一次急性痛风,一个人在厦门,坐在床上路都走不了,那时候真的好想旁边能有个人能扶我去医院,真的是感受到 10 级的孤独。尝试了好几次想蹦到路边都顶不住,最后还...
继续阅读 »

普通的人生终起波澜


去年下半年应该算是我毕业以来发生人生变故最多的一段时间。


先是 7 月份的时候发作了一次急性痛风,一个人在厦门,坐在床上路都走不了,那时候真的好想旁边能有个人能扶我去医院,真的是感受到 10 级的孤独。尝试了好几次想蹦到路边都顶不住,最后还是打电话给岛外的堂哥来接我去医院。


然后开始减肥健身,每天 7 点下班就去健身房练到 10:30 健身房关门,每周末去走山海健康步道临海线或者骑行环岛东路,最终是从去年 7 月 200 斤到今年 4 月减到了 140 多。


减肥这段时间戒掉了很多坏习惯,比如奶茶,喝汤,含糖饮料,熬夜,晚起。一方面上班的时候的时候感觉自己有点营养不良,尤其是上午的时候大脑有时候会空白,但另一方面又被自己能坚持走 4,5 个小时的健康步道的毅力感到惊奇。身材也因为健身力量训练慢慢变好,洗澡的时候开始喜欢照镜子了。但是我总觉得人生不可能一帆风顺,一直向着好的方向发展。


去年 10 月国庆回家参加亲姐姐的婚礼,欢欢喜喜过了个国庆回来不久就被 leader 约到会议室通知我上了裁员名单。补偿是 n+1,last day 是下周五。


其实我当时听说我被裁了,我当时的状态是觉得很突然,但又不觉得意外,觉得有点不知所措,但又觉得有点解脱后的小确幸。不意外是因为公司很比我优秀的大佬都先我一步被裁了,在目前看不到公司起色的情况下,轮到自己是迟早的事,另一方面我一年前就和 leader 聊过我想离开的想法了。觉得不知所措是因为我还没有做好能通过 2 个月内面试快速找到并进入下一段工作(那时候还没体会到行情的恶劣)。去年年后调薪失败后我就开始认真准备面试了,后面被裁后找工作的时候刷算法题的仓库还是那时候建的,只不过后面因为要痛风之后准备花一两年好好调理身体就没怎么刷题了。


后面很多次面试面试官都问过我为什么离职,我都是直说裁员,一方面我觉得现如今行情裁员是很正常的,如果一家公司歧视被裁的,那我也会瞧不起这家公司。虽然我完全可以说是我自己裸辞的,而且公司方面态度也是完全支持或者说希望我们这么做的,但是我这人就是不喜欢作假。我可以承认我的不优秀,但是我也不愿意让自己心里有疙瘩。


坦白说,对于上家公司,我没感觉受啥委屈,leader 和同事们也非常 nice,我觉得对我挺好的,换做 35 岁,快退休的时候,我还挺乐意在那养老的。但是我毕竟 30 岁没到,还是受不了一成不变的生活,想出去折腾折腾。再加上我并不是厦门本地人,我感觉 80% 的同事都是福建人,作为来自江西的外地人的始终没有啥归属感。


但老实说,来了深圳我还是挺怀念以前厦门的生活的。怀念在环岛东路边吹海风边骑行,骑累了,就躺在路旁的草地或者石椅上;怀念五缘湾公园和陌生人一起跑步的日子,跑累了就躺在阶梯超宽的木桥上吹海风;怀念林海线的广播音乐,怀念健身房那个每次见到我都会和我打招呼教了我好多东西的练得超好的做产品的老哥,怀念每天早上都要去消费的美宜佳(对比深圳荔林这边美宜佳店面大得多,东西又全又新),怀念我常去的那家理发店爱吹牛健谈的老板(可能是因为冲的会员还有好几百没消费完^_^)。


通知被裁了以后,我就没咋干活了,一边交接工作,一遍把自己做的一些工具能开源的都给开源了。现在想来,我应该学习另外一个和我同期被裁老哥一样争取混到月底再走的。在 last day 和另外一个同事一起请客散伙饭,把部门移动端和前端同事,以及部分先我被裁的同事都叫来了,破例的喝了快 10 瓶百威,最后散场和一个先我被裁的同事一起去五缘湾散步解酒到深夜,算是正式结束了毕业后第一份工作的旅程。


通知被裁的当天我就给父母打电话告知了自己被裁的消息,本想找家人分享找点安慰缓解下自己的焦虑,但是反倒是自找烦恼了。在他们眼里就是:你被裁了肯定是你做的不够好,你做错事了,你没有打理好和领导的关系,休息个屁,赶紧找工作,明天就开始找工作。


聊不了一点,对着电话那头吼了几句直接挂了。


享受生活


被裁后我没有选择立即找工作,一方面自己完全没准备好,在上家公司工作三年从来没出去面试过(反面教材),另一方面,趁着现在有时间我想尝试一些我没试过的东西:



  • 骑行绕岛内一圈

  • 骑行往返从岛内到岛外,我在厦门的亲戚基本都在岛外同安区,顺便在亲戚那里好吃好喝了一天


离开厦门的前一晚还是去健身房健身了,和熟识的老哥道了个别,第二天就不悲不喜的离开了厦门回老家了。相比前一段伤心的北京实习经历,那肯定要好受的多。


回家之后时间来到 10 月底,那时江西老家的天气还没开始降温,但也很快就开始降温了。早在国庆节之前就买了组合哑铃和卧推凳到家里,所以在老家有坚持练胸和背,腿没怎么练了,跑步就当练腿了。国庆期间还买了很多渔具,买了汉鼎的 5.4 得螺纹钢,平时很喜欢看钓鱼短视频,小时候也经常钓鱼,在我们村我自己家有三个鱼塘,所以国庆长假钓鱼还是蛮有意思,我姐婚礼酒席上的十多条大草鱼就是我和伯父一起钓的。


当然我也没闲着啥事不干,去县城驾校报考了个 C1 驾考,回家老考驾-照 也成为了我回家以后应付村里老乡问我 为啥离过年还有 3 个月就提前回家过年 的借口。10 月 20 多号报名,最后到 12 月中旬考完,下旬拿的驾-照,分别考了 98, 100, 100, 94,科目二,三都是一把过。冬天考驾-照好处是考的人少,不怕没车子练,缺点是得早上 7 点就得起床顶着严寒去驾校练车。我发现组里 7,8 个人,总会有一两个没考过的,没考过的往往还是平时练得好的,像我这种科目二模拟考从来没过的反倒满分一把过。有事没事会去自家鱼塘钓鱼,直到后面草鱼和小鲫鱼都吃腻了就不去钓了,后面天气彻底冷下来了后也没法钓了。之前有一次大冬天去钓鱼,坐那吹了一个小时的冷风,鱼不开口就算了,还把自己整感冒了,打那就再也提不起钓鱼的兴致了。


考驾-照期间没去面试,但是有坚持每天刷 leetcode 和 ts 体操,去掘金上或者牛客上找找近两年的一些大厂面前端试题。平时有看 b 站的习惯(真不爱看擦边视频。。。),天天给我推送一些考驾-照还有前端的一些技术视频。刷八股或者看技术视频看到一些感兴趣前端知识点的就会记到便签里面,第二天在自己的 fe-demo 仓库写代码验证。刷题是为了面试,写 demos 是为了查漏补缺,学习进步。


感谢这段时间老妈每天的投喂,回想这半年我觉得一个很大的遗憾是没有带老妈出去旅游,感觉人生中这么有空的机会不多。


年前面试


12 月中旬拿到驾-照后就开始投简历面试了,一开始就投大厂:



  • wxg 视频号直播

  • 米哈游协同文档

  • 字节杭州巨量星图

  • 飞猪

  • 北京即时设计

  • MoeGo(不算大厂,但是主动约我面试的)


都一面挂了,前两个我录了面试语音,后面听的时候就发现我面试的时候表达问题很大,首先是非常紧张,说话经常结巴和重复,二是回答问题不够系统有条理。挂了我觉得不意外吧。


腾讯这个岗位应该是真招人,面完的感受是对 nodejs 要求应该蛮高。


米哈游协同文档这个岗位,我本来不想面的,因为我对这个工作方向不是很感兴趣,但是 hr 又是知乎私信又是微信加我想要约面试,就试了试,结合自己的感受和一些网上对这个岗位的评价,有较大概率不是真招人就是想骗方案。。。国内大厂貌似都喜欢自己做 IM 和 文档。


字节面挂了不知道是啥原因,我觉得可能得原因



  1. 题目没写出来,它们那个面试平台没用过,当时不知道咋测试,折腾半天就说了下思路,面试官说没问题

  2. 当时面试官问我我前公司相对其它公司竞品的优势是什么,这么基础的问题,我卡了半天,答的不好,坦白说平时很少研究竞品,自己做的产品平时都很少用

  3. 最后他问我对工作的期望,我说希望最好还是能 8 点前下班,然后问他们那工作作息,一般 9 点下班...


挂就挂了吧,但是好像面试官给的面评不好,导致后面想换个部门面 hr 不给过。我后面发现面试官好像后面还 follow 了我 github...


飞猪这个貌似是部门 leader(不确定),是 V2EX 看到我发的求职帖子直接微信视频面了下,面完他说我还没准备好面试,等我准备好了再找他,但是他给我的感觉阿里味太浓了,聊完我直接把他微信删了。


即时设计据说年底融到钱了在扩招,而且貌似很喜欢约我前公司的人面试,据说开的还挺高。好像是他们 hr 主动在 boss 上约我面试,一面结束,我在 boss 上问他面试结果咋样,告知一面通过了,马上会约二面,结果我等了两个星期都没约面试。boss 上问他后面也没回复,第三个星期当时我姐在九江医院要生孩子,我配我老妈去陪护我姐。打个电话说是前段时间忙着公司团建,没空约面试,问我明天周五约不约面试,我说我没带电脑,要不下周一,说是没问题。结果后面就没后面了,真是草了。。。说起来和一面面试官聊的还蛮开心的,最后问的一个算法题都感觉有点像是放水了,说是最后我们来考道简单的动态规划的问题。我一听是动态规划瞬间就紧张了,结果问的是青蛙跳台阶那道题,题目刚说完,我就把思路和面试官说了,面试官就说不用写了开始聊天...


MoeGo 这个公司网上风评很好,我实际感觉也确实挺好。他们 hr 我感觉就很专业很舒服,约面试之前还给我详细介绍了他们公司的业务,发了一些他们背景资料给我看,我提问的时候也都很耐心的回答。一面前做了个笔试题,我觉得题目出的也挺有水准。一面是个姓毛的面试官,最开始他们我平时在干嘛,我说平时喜欢刷类型体操,就来了几道体操题,都是那种入门级别的 Exclude 之类的,有一说一面试官平时应该是没怎么刷过 ts 体操的,用了一些很简单的技巧,感觉面试官的反应是没太看懂,例如:


type Exclude<T, U, E = U> = E extends U ? never : E;

面试官还问这个 E 是干嘛用的。题目太简单,表现欲太强以至于我面试的时候就说你出的这些题都是入门级的。面试官的于是转考手写 react 代码,他们公司貌似是 react/react native 技术栈,其中一个考的是使用 hooks 实现 useDebouncedState。要是考 debounce 怎么写,我能很快写出好几种写法并且考虑 leading 和 trailing,但是我是真的很少写自定义 hooks,要我用 hook 来实现 debounce 我还是真一下子搞不定,以前也没用过。最后在他耐心的提示下写出来了,但是我觉得可能就是写的真的很呆逼,可能是因为这个原因觉得我 react 写的太少,一面给挂了。也有可能是竞争比较激烈,虽然挂了,但是他们公司 hr 和面试官给我的印象都挺好。由于自定义 hooks 没写好,于是还专门创了个仓库练习手写一些常用的 hooks:react-hooks


总结下前面几个面试:



  1. 太紧张,在上家公司呆了三年,没怎么出来面试

  2. 技术问题没啥太大问题,项目没准备好

  3. 不应该上来就投大厂的或者想去的公司,应该先面些去的意愿不是很强的公司磨炼一下面试和表达技巧。进入下份工作,就算不是想换工作,也应该每年出去面面试,好处很多:

    • 了解行情和自己的水平

    • 拿个高于现在的薪资的 offer 聊涨薪的时候有筹码,比较有底气(这是另一个前同事的现身说法)

    • 磨炼下面试技巧,现在这行情,被裁了真不好找工作,便于被裁了快速过渡到下一份工作




面完这几家,在九江看着我外甥女出生后就回家过年了,这段时间基本上也没咋准备面试,大过年就该好好玩享受生活。不过过年的时候看到放烟花举国欢庆,喜气洋洋的时候,入睡前想到之前面试的失利和前途未卜,还是感觉心里有些阴霾。


插个题外话,有次村里一个媒人来我家,想给我牵线,问我现在哪工作,我说是在找工作,媒人就悻悻的离开了。。。所以说没有工作,相亲都没人给你介绍。


年后面试


年后我要求就放低了些,大小厂都投,包括一些小的初创公司。


大厂


我发现中大厂除了我之前面过的几家,基本上都不给面试机会。
这几家我觉得是不招人,没 hc 的:



  • 快手

  • 百度

  • 美团

  • 蚂蚁

  • 滴滴

  • 知乎,b 站,boss 直聘等等


这几家我觉得是招人但对简历背景要求比较高:



  • 携程

  • 小红书

  • shopee

  • AfterShip

  • 得物

  • 希音(内推大佬说是学历没达标)


以上公司全部没有面试机会。我发现,那些发展比较好的中厂对背景特别的看中,反倒是部分大厂只要能力出众走内推大概就会给面试机会。


年后大厂里面就只有阿里和网易约了面试,其实我也试过再换个字节的部门面试,貌似是面评的原因没 hr 那边没过。


阿里


这里点名表扬阿里,我面了五个部门,虽然没去成,但是很感谢给了这么多面试体会。



  • 飞猪

  • 淘宝(twitter 上一个老哥内推的),一面挂,貌似是在用新版 weex 重构项目,感觉是当时那个 lazyMan 的问题写的不是很好,还有后面一些 vue 的问题答的一般

  • 淘宝客服团队的,二面挂了,问的一些项目相关的问题例如你觉得你在项目里面做的好的地方,做的不好的地方,新版上线旧版咋维护的,反馈说是缺乏对项目的思考和动机,表达不是很通畅的。关于这个项目的思考我得洗一下,我当时参与那个项目的基建工作,是半路被 leader 拉过来救场的,你问我这个项目的很多规划上的问题,我是真没去了解。

  • 阿里文娱淘票票,当时晚上 8 点多,躺床上准备刚眯了一会,被阿里面试电话吵醒。脑子有点不清醒,刚好又问了一些平时没咋准备过的问题,感觉挂了也不意外吧。我还问了为啥 8 点多还来面试,他说这个点刚好工作忙完了有时间面试。挺无语的,面试难道不是工作的一部分吗?

  • AliExpress,这个是 Boos 上我投的,结果打电话给我说是外包岗位,觉得我很合适(无语),问我面不面试,我说不考虑外包,然后他好像挺纠结的说好像也有正式岗位在招,就挂电话了。过了一会同一个人又打电话过来说是确定了有正式岗位的 hc,然后就一面完了,第 3 天就二面了,二面完了就收到邮件进了内推,后面就一直没消息了。挺奇怪的,你说你二面如果不打算让我过为啥还要再进内推流程,而且面试过程中我觉得表现是很好的,聊的也挺开心的。两个猜测,1.要么是没 hc 了,或者有更好的候选人把我当备胎了 2. 骗方案的,一开始就没 hc,因为两个面试官都问了同一个问题,说是他们有一些内部遗留项目没有做 I18n,问怎么搞


拼多多


这个好像是脉脉找了个内推链接,自己投的,面到谈薪阶段,因为薪资达不到预期就不继续了。



  1. 一面的时候 hr 明确告知了 11, 11, 6

  2. 安排面试的 hr 和谈薪的 hr 不是同一个人,谈薪的 hr 很强势,给你一种爱来不来的感觉,我问到竞业之类的一些问题,她说我顾虑太多。。。

  3. 强度这么高钱没给够,我是不可能去的,我一个工作三年的前端让我背应届生倒挂我是不可能接受的呀,就因为我上份工作工资低?

  4. 确实卡涨幅的,给钱没几年前网传的大方


杭州某音乐公司


一面面试官是我这么多面试以来我觉得技术和交流最舒服的一位,为什么呢:



  1. 回答两个问题后,他就说,后面你答到关键点我们就过,直接下一道,真直接啊。。。我就喜欢直接的面试官。

  2. 问了个 babel-runtime 的作用,还夸了下我对前端工程化理解确实有点东西,感觉确实问的问题有水平,不是上来就让你背 webpack 的打包流程...

  3. 有些问题没达到关键点或者有些问题,他会直说,引导你把正确答案或者说他想要的答案说出来。比如他问了说 FCP 怎么提高,我说了很多,像是从网络协议和传输体积这些方面回答,但是他说这些不是他想要的答案,我就忽然想到说 SSR,后面就继续聊 SSR 相关的一些东西。很多时候面试问的问题我真不是不会,我感觉就是当时理解错了问题,或者和面试官已有的观念不一致。有些面试官就听到你的回答,不符合他的想法,甚至用有点嘲笑般的语气说下一道,我不知道这些人是平时工作生活不愉快来面试的时候找优越感,还是活太多想快点面完就去干活。

  4. 面完了以后他还问我离职半年都是脱产状态吗,以我的水平不至于找不到工作啊,之类关心我的话,还是头一个面试官会关心我是不是脱产。当时我说感觉可能过不了,我说我面试时间长了对自己是越来越没自信了,然后他说面试给他的感觉还是很不错的,能力还是很可以的。


面完之后第二周约了二面,这个面试就有点不愉快了:



  1. 时间约到了晚上的 7:30,面过的公司很少有约晚上面试的,我也不喜欢晚上面试,应该没和我提前沟通过(不确定)

  2. 面试官要求我开摄像头,他自己不开。面完我就问他了,我说为啥要求我开摄像头,他不开,他说没约到会议室的,灯光很暗开了摄像头我也看不到啥之类的。但我还是觉得,你不开摄像头,就不应该要求我开,我觉得候选人和面试官是平等的,我觉得我没有被尊重,面试反馈直接给了个差评。这让我会想到当初应届的时候面试腾讯 IMWeb 的经历,当时是个女面试官,也是没开摄像头,也是面试没半小时就结束面试了。

  3. 我记得他当时好像问了说是为啥用 importmap 比直接配制 splitChunks 好,我都说了可以更精细化的控制缓存,加快生产环境打包速度,但是貌似还说说服不了他,硬是逮着这个问题一直问,我就差点直接和他说了这个是我们公司当时基建部门硬推的,但是我觉得好处我也说了,说服不了就不能换个别的正常点的问题问吗?面试时间我记得比较短,可能半小时不到。

  4. 把我面挂的面试官挺多,但让我不舒服的他算一个


中小厂


中小厂真的是踩坑比较多,各有各的花活,让我印象好的不多。


x 麦物流


首先我没走进面试流程过,但是还没进到面试就让我对这家公司印象贼差:



  1. 找人内推,说是因为他们 leader 看简历感觉我是熟悉 vue 而不是 react,所以简历没过,他们只想找熟悉 react 的。我是真没想到现实中真有歧视技术栈的

  2. 后面应该是换了个各渠道投简历,hr 打了个电话过来。首先说她不是来约面试的,只是来了解情况的,面试还没开始就问我上家公司工资多少,预期是多少,我报了个预期薪资,她反问道你凭什么要求这么高的涨幅,我算了下也就 20% 的涨幅,而且是从厦门换到深圳。给我一种暴发户在施舍工作的感觉,电话聊完还是没有进面试


后来在推特上也看到有人吐槽这家公司的骚操作,还在找工作的可以留个心眼。


杭州 x 登科技


别的我先不吐槽了,我就先吐槽面试流程:



  1. 笔试

  2. 电话面试,考手写算法

  3. 要求必须现场面试,ok,我从老家江西坐高铁去现场面试

  4. 一面技术面,考前端基础

  5. 二面 leader 面,考算法

  6. 三面杭州负责人面

  7. 四面 hr 面

  8. 完了回老家之后还要再加一轮业务负责人面


8 轮面试,我和我朋友聊到我面试某家公司面了 8 轮,没一个人不是开口就说这家公司 xx 的,搞这么多轮面试。


槽点不只是这个,当时一面面试的时候,面试官端了个电脑过来,我也背了包带了电脑过去,他要考我写一个深克隆,我说我用电脑写吧,他不让,硬是要求我必须用水彩笔在白板上给他写代码,说是他们平时交流都会用到白板。我是觉得真 xx,完全是形式主义。


更过分的是,当时我手上有另外一家公司的 offer,但是已经到了回复的 deadline 了,hr 也是知道我有另一家的 offer,在我拒了另一家回复说愿意接受开出的价格的时候,hr 又回复说觉得我稳定性不够不发 offer 了。这一轮操作直接搞得我两个 offer 都没了。每次和老爸聊天他都拿有 offer 的时候不去这个来训我,每次也因为这个和老爸大吵。我的观点就是我想争取更好的工作一点问题都没有,而他的观点就说我不知足,好高骛远,有份工作就应该去干。


经验教训:没收到 offer 邮件都不稳。你对别人的真诚,别人可能觉得你是个傻逼。但凡要求必须线下面试的,基本上都有坑。


成都 xx 英


槽点:



  1. 笔试,要求写一个项目发源码,确实有套方案的嫌疑。当时写了个浏览器端的文件管理器:react-file-explorer,他们内部好像是要做一个 web 端的小组件编辑器

  2. 面前端面试官不是很懂前端

  3. 公积金确实是按最低标准交的

  4. 和脉脉上某个人遭遇是一样的,面完一轮后过了快一个月又联系我面试

  5. hr 和脉脉上说的一样回复消息超慢,你不主动问基本不会联系你是真的

  6. 由于上一点,导致当时谈 offer 的时候她发了个消息,我当时可能在忙啥,我想着就故意想着晚点回复,结果后面被我忘了,然后大概一周后我才回复她,她回复说看我没回消息就让别人先入职了。。。


还有一些其它骚操作的公司我就不说公司名了:



  • 第一轮面试全就发张表给你,让你把包括上家公司,预期薪资就填上去的。其实我觉得这些比较私密的信息应该到最后阶段才适合交流,上来就查水表,会让我觉得这家公司很不尊重人

  • 有问为啥 typeof null === 'object' 的,我说这就是设计失误没啥可解释的,面试官让我再想想。。。

  • 其它的想到再说


厦门互联网公司


其实我还是蛮喜欢在厦门工作的,待了三年有点感情了,除了工资低点,城中村好多都拆了不好租房子,饮食不辣外能吐槽的点也不多。但环境是真好,厦门政府是真有钱啊!


找同事了解了下厦门的一些不坑的互联网公司,但问了一圈基本上都没 hc,有些我还是直接找在职的前同事问的,确实不咋招人了。当时时间是 3 月份,所以金三银四已经成为一个笑话。


唯一面上的是美 X,美 X 是因为我曾经的 leader 在那,直接找他内推的,但是一开始推的不是他自己的部门,面完两轮后,反馈说是想招偏业务的而不是偏基建的。我不知道这是不是借口哈,因为我之前被问到我和内推我的人是啥关系,为啥不内推到他自己的部门,我有点怀疑是因为内部斗争有顾忌?(我自己的想法)。


好吧前 leader 把我推到他自己在的部门,结果面试的时候貌似是两个人一起面的我,像是一老带一新,这个面试官感觉很不专业,说话比我还紧张,问的问题也贼离谱,也不会引导话题,体验贼差。我还记得他们到说看我写了熟悉 typescript,就问我我平时是怎么使用函数重载和泛型的(咋不问我 if 和 while 的区别,贼尬),问我平时是咋做埋点的,我都说了我平时很少做埋点,还一直追着问。给我整烦了,问的问题全是没准备过的问题,回答的有点乱。


小型初创公司


基本上都是在 V2EX 酷工作板块看到的招聘信息。


这些小型初创公司基本上都是在一线城市,人数在 1 ~ 10 人之间,基本上都是做 AI 出海项目的,简单分为几类:



  1. 薪资开的很低,喜欢画大饼,能理解,毕业创业不易

  2. 不是想招人,只是想短期外包个前端,也能理解,但是不要拐弯抹角,想外包一开始就直说,浪费彼此时间

  3. 比较正常(指的是我现在这家)


我当时对工作的要求简单来说就是:



  1. 要么:钱给的不多,但工作轻松

  2. 或者也可以:很累,但钱给的多


对于上面第一类公司,面过两家,应该都是觉得我薪资要求高于预期,也有可能觉得我的水平高于他们预期,去了也待不久,就说不合适。其实还好,我去的意愿也不是很强。第二类碰到过一家。第三类其实说的就是我现在待的这家公司。


现在这家公司之前在 V2EX 上发招聘贴的时候,我其实看到了,但当时觉得背景挺牛逼的可能不会招我这种双非本科的就没投,但是后来团队里的人发邮件给我说狼叔推荐我给他们,问我要不要面试,然后我就去试了试。其实我感觉缘分就很奇妙,应该是诸多原因给了我面试机会的:



  1. 狼叔推荐的

  2. 组里大佬之前有面试过我前东家

  3. 组里大佬又找我前东家的原技术大佬了解我的情况


不过当时面完最后还是没发 offer,在两个候选人之中最后他们选了另一个。介绍我去了另一个搞 AI 生成广告视频的团队。当时我和他老板聊天,聊下来的感觉是觉得他有点不太尊重我,聊完直接删了微信。


至于我最后是怎么又去了这个公司,那得等到 3 月底了,也就是 1 个半月之后,据说是因为某些原因之前选择的另一个候选人离开了公司,岗位又空缺了,于是问我愿不愿意去,当时正非常焦虑的我就接下了 offer。


说来当时也是真巧了,那天我大概是午饭前接的 offer。本来打算等下午老爸从工地回来和他分享喜讯的,但是午睡完就接到老姐一个电话,说是老爸给他打电话给她骂了一顿,向她吐槽说是你弟弟什么有 offer 的时候拖着不去,后面又去不了,什么不好好找工作,搞得他干活都没力气,烦的都想要喝农药了,说她做姐姐的也不去说说弟弟。我怕老爸真喝农药去了就立马给他打了个电话说我找到工作了,他还不相信,说我是骗他的,安慰他的,再三说明下才相信是真的。诶,没工作的那段时间,前期还好,没咋吵架,越是到后面,不但我自己焦虑,每次和老爸碰面都要吵一架,搞得我更烦。


心态变化


刚被裁的时候其实就没啥想法,也没急着想找工作,想着反正拿了大礼包,先玩两个月。


年前面完失利后,就感觉自己准备不足。


对自己的实力其实还是很自信,但是挂多了后也开始慢慢对自己产生了怀疑。虽然没又明显感受到对长 gap 期的歧视,但是被问到为啥 gap 期这么长还是有点忏愧。


年前到 3 月初还能约到面试,3 月开始整个招聘市场就好像毫无波澜般的死寂,投个简历出去一个星期没约到面试。自从发生了那次被口头毁 offer 事件后就有点摆烂了,想随便面个公司上班去得了,实在是面试面的我心里憔悴,煎熬的很。


B 站和掘金开始经常给我推荐失业找工作的内容了,也让我更加焦虑。在推特上发了一些找工作的帖子,评论的特别多,确实是流量密码,发了两三个帖子后粉丝直接破千了,但这确实不是我的本意,我分享的也都是真情实意,往往也是确实无处发泄情感想找个地方发出来在网络上找点认同感和安慰。


由于多次发生了我感觉是因为我价格报高了没后续的情况,后面问预期薪资我就直接报最低预期了。我感觉很多中小厂的 hr 是真的有这种心态:你不来有的是人来,还更便宜。当时还发生了飞书裁员事件,虽然我觉得对招聘市场影响不大。但是时不时爆出来大厂裁员的新闻说明了一个问题,招聘市场有不少大厂背景出来的,竞争很激烈。


几次大厂面试挂在项目面的经历让我有点沮丧,技术面都没问题。项目这东西我也不知道咋准备,我做过的项目包括:自研的跨端框架(多数面试官听不懂),adobe 插件(技术栈太冷门),前端基建。没有那种大型的 to c 或者 to b 的项目,就显得项目没啥亮点,也想过去硬编那些不是自己做的东西,但是也许是我太真诚或者是蠢,确实没编过,做过啥就说啥。


好在得到现在老板的赏识,啥也没纠结直接就接 offer 入职了。


求职工具分享


渠道


这个我之前发过一个 twitter,这里再简单列举下:



  1. boss

  2. V2EX 酷工作板块

  3. 推特上一些大佬经常会转发一些工作机会的帖子,例如狼叔,大圣等,关注的一些老哥自己公司有 hc 也会发招聘帖子像 chenqing663, 荣顶

  4. 脉脉系统推荐

  5. 阮一峰 weekly

  6. 掘金沸点 - 内推招聘广场

  7. 各大公司官网投递(不推荐,基本上没有用)


准备面试



  1. leetcode,建议按照 tag 刷,推荐顺序二叉树,链表,数组,字符串,回溯,动态规划的,建议开个会员按出现频率顺序刷。不知道啥时候 leetcode 增加了个 javascript 分类,建议全刷,挺基础的质量还行。刷题一定要自己动手啊,光看题解是没用的。

  2. ts 体操不建议刷,基本上不考,参加了那么多面试,问的少不说,问的问题也都很简单,想刷的推荐 typeherotype-challenges。有个面试面试官发了个 typehero 某道题的链接让我做题,结果我打开后显示我已经做过了,面试官还说思路比他自己的还要好。。。

  3. bigfront 这个真是一个宝藏网站,优点很多,可以刷的题目类型很多,包括算法题,react, css 等,题目质量也不错,还有评论区可以看大佬们的题解,不但能测试算法题,react, css 也能测试,还能直接看到测试用例的代码。

  4. 刷面经,可以去牛客网,掘金,或者百度搜

  5. 刷八股建议搜索掘金的文章,这个时效性不是很重要,质量更重要。建议直接看点赞多的,一篇没整明白多看几篇,看完一定要总结。除了掘金,B 站也有很多优秀资源,例如 https 可以看看 技术蛋老师的视频

  6. 准备面试就是一个查漏补缺的过程,这个缺包括面试过程中没答好的点,自己看面经或者技术文章看到不熟悉的或者存有疑惑的知识点,还有一些实时热门知识点,例如跨标签页通信,服务器端推送。一定要自己写 demo 验证,只有这样才会印象深刻,面试的时候万一手写也没啥怕的。

  7. 可以让 chatgpt 给自己出题,可以把自己简历发给 gpt 读,结合自己的实际情况和面试的期望优化上下文,出题的时候让它只出题目不要解答,不然会影响自己思考。如果你能 10 道题 9 道都比较完美解答的化我觉得就不用刷啥八股了,准备项目吧

  8. 简历最好套个简洁的模版导出 pdf,我用的 resume-chinese,直接 Overleaf 在线编辑导出

  9. 可以整个博客网站,hr 和面试官确实是有可能会看你的博客的,所以建议把你希望被看到的博客置顶。不建议 github 没啥东西也搁简历上放个 github 链接

  10. 我写的油猴脚本:重新定义 Boss 直聘

  11. 自我介绍建议背诵道滚瓜烂熟,不要面试一开始就乱了方寸

  12. 面试前建议了解下面试的背景,它们的竞品,访问它们的官网,体验下它们的产品,可以用 lighthouse 检测下它们的网站

  13. 情绪积压多了,确实影响身心健康,挂多了我吃饭都没胃口,条件允许建议去跑步,有啥不痛快的情绪也可以在网络平台去分享,推荐推特,推特混久了你会发现前端圈子真小。。。

  14. 如果你觉得面试的时候就对这家公司有点不认可,建议千万别将就着去。例如面试的时候感觉 leader 面的 leader 喜欢 pua,掌控欲很强,阿里味很浓,建议远离。我还是对喜欢说大白话的面试官比较有好感,那些说话一套一套整那些虚的,喜欢给你画大饼的在我这是很减分的。


感谢



  1. 感觉家人对我的关心和照顾,感谢老妈每天的投喂,感谢老爸大冬天一大早开摩托送我去练车,感谢奶奶经常给我做好吃的,感谢我可爱的外甥女让我在焦虑的面试中感受到一些生活的温馨。

  2. 感谢一些前同事的关心,工作介绍,和经验分享

  3. 感谢网友的内推,耐心回答我对他们公司情况的疑问,重点感谢 柳家忍 和狼叔

  4. 感谢老板的赏识,感谢入职后可爱同事们的照顾


未来


感觉目前公司的业务充满了机遇和挑战,这还是第一次感觉自己做的东西会对公司的业务产生非常大的影响。原本想着工作之外搞搞开源玩点自己想玩的,但是我觉得我们项目目前能做的优化就很多,优化的过程就能学到很多,优化好了就会对同事的开发体验或者用户体验产生很大影响。所以工作外的时间除了享受生活外可能会花在优化公司项目上,开源项目会做的比较少了。不过我现在已经习惯了将自己做的东西开源,以及将开源的东西用到公司的项目,所以其实也是在维护开源项目。


作为一个技术洁癖者,在创业公司要做好技术和开发效率之间的平衡。


进入 ai 行业,我要学习的东西也还很多。


对于深圳熟悉又陌生的城市,也要努力的去适应,努力工作的目标是为了更好的生活,不要舍本逐末。


前段时间压力比较大没加上有点不适应这边居住环境吃的有点放纵(好吧我就是贪吃),体重有点回升,接下来要注意控制了,多出去运动。


职业生涯第二站来到了一家创业公司,相信一切都是最好的安排。煎熬的求职经历让我明白了居安思危的重要性,要更加重视职业规划。我觉得我老板和同事还都挺靠谱的,可以向他们学习怎么做更有价值的事,怎么即时正确的调整产品方向。程序员的出路我感觉也就是走管理和创业(包括独立开发),指不定以后自己 30 多岁没工作了也去创业了,这段经历肯定会有所帮助。


感兴趣可以体验下我司的产品,给我反馈 bug:openart.ai


话说这篇博客的 og 图片也是用我司的平台生成的 😀,prompt 很简单:one boy, no job,然后用了 Expand Image 功能把图片横向扩展了下。


作者:余腾靖
来源:juejin.cn/post/7368288987641774120
收起阅读 »