注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

ThreadLocal使用不规范,上线两行泪

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜 ThreadLocal是Java中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点...
继续阅读 »

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜





ThreadLocalJava中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点,对于此网上已经有很多经典文章来进行分析,但今天我们主要分析笔者在项目中遇到的一个错误使用ThreadLocal的示例,并针对错误原因进行深入剖析,理论结合实践让你更加透彻的理解ThreadLocal的使用。


前言


Java中的ThreadLocal是一种用于在多线程环境中存储线程局部变量的机制,它为每个线程都提供了独立的变量副本,从而避免了线程之间的竞争条件。事实上,ThreadLocal的工作原理是在每个线程中创建一个独立的变量副本,并且每个线程只能访问自己的副本。


进一步,ThreaLocal可以在当前线程中独立的保存信息,这样就方便同一个线程的其他方法获取到该信息。 因此,ThreaLocal的一个最广泛的使用场景就是将信息保存,从而方便后续方法直接从线程中获取。


使用ThreadLocal出现的问题


明白了ThreaLocal的应应用场景后,我们来看一段如下代码:



控制层



@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {

@Autowire
private UserService userService;

@GetMapping("get-userdata-byId")
public CommonResult<Object> getUserData(Integer uid) {

return userService.getUserInfoById(uid);

}


服务层



@Service
public class UserService {

ThreadLocal<UserInfo> locals = new ThreadLocal<>();

public CommonResult<UserInfo> getUserInfoById ( String uid) {
UserInfo info = locals.get();

if (info == null) {
// 调用uid查询用户
UserInfo userInfo = UserMapper.queryUserInfoById(uid);
locals.set(userInfo);
}
// ....省略后续会利用UserInfo完成某些操作

return CommonResult.success(info);
}
}

(注:此处为了方便复现项目代码进行了简化,重点在于理解ThreaLocal的使用)


先来简单介绍一下业务逻辑,前台通过url访问/user/get-userdata-byId后,后端会根据传入的uid信息查询用户信息,以避免进而根据用户信息执行相应的处理逻辑。进一步,在服务层中会缓存当前id对应的用户信息,避免频繁的查询数据库。


直观来看,上述代码似乎没问题。但最近用户反馈会出现这样一个问题,就是用户A登录系统后,查询到的可能是用户B的信息,这个问题就很诡异。遇到问题不要慌,不妨来看看笔者是如何进行思考,来定位,解决问题的。


首先,用户A登录系统后,前端访问/user/get-userdata-byId时携带的uid信息肯定是用户Auid信息;进一步,传到控制层getUserData处的uid信息肯定是用户Auid。所以,发生问题一定发生在UserService中的getUserInfoById方法。


进一步,由于用户传入的uid信息没有问题,那么传入getUserInfoById方法也肯定没有问题,所以问题发生地一定在getUserInfoById中获取用户信息的位置。所以不难得出这样的猜测,即问题大概率在 UserInfo info = locals.get()这行代码。


为了加深理解,我们再来回顾一下问题。"即用户A登录,最终却查询到用户B相关的信息"。 其实,这个问题本质其实在于数据不一致。众所周知,造成数据不一致的原因有很多,但归根到底其实无非就是:“存在多线程访问的资源信息,进一步,多线程的存在导致数据状态的改变原因不唯一”


Spring中的Bean都是单例的,也就是说Bean中成员信息是共享的。换句话说, 如果Bean中会操纵类的成员变量,那么每次服务请求时,都会对该变量状态进行改变,也就会导致该变量成员那状态不断发生改变。


具体到上述例子,UserService中的被方法操纵的成员是什么?当然是locals这个成员变量啦! 至此,问题其实已经被我们定位到了,导致问题发生的原因在于locals变量。


说到此,你可能你会疑惑ThreadLocal不是可以保证线程安全吗?怎么使用了线程安全的工具包还会导致线程安全问题?


问题复现


况且你说是ThreadLocal出问题那就是ThreadLocal出问题吗?你有证据吗?所以,接下来我们将通过几行简单的代码,复现这个问题。



@RestController
@RequestMapping("/th")
public class UserController {

ThreadLocal<Integer> uids = new ThreadLocal<>();

@GetMapping("/u")
public CommonResult getUserInfo(Integer uid) {
Integer firstId = uids.get();
String firstMsg = Thread.currentThread().getName() + " id is " + firstId;
if (firstId == null) {
uids.set(uid);
}

Integer secondId = uids.get();
String secondMsg = Thread.currentThread().getName() + " id is " + secondId;

List<String> msgs = Arrays.asList(firstMsg,secondMsg);
return CommonResult.success(msgs);


}
}


  1. 第一次访问:uid=1


image.png



  1. 第二次访问:uid=2
    image.png


可以看到,对于第二次uid=2的访问,这次就出现了 Bug,显然第二次获取到了用户1的信息。其实,从这里就可以看出,我们最开始的猜测没有任何问题。


拆解问题发生原因


既然知道了发生问题的原因在于ThreadLocal的使用,那究竟是什么导致了这个问题呢?事实上,我们在使用ThreadLocal时主要就是使用了其的get/set方法,这就是我们分析的切入口。先来看下ThreadLocalset方法。


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

可以看到,ThreadLocalset方法逻辑大致如下:



  1. 首先,通过Thread.currentThread获取到当前的线程

  2. 然后,获取到线程当中的属性ThreadLocalMap。接着,对ThreadLocalMap进行判断,如果不为空,就直接更新要保存的变量值;否则,创建一个threadLocalMap,并且完成赋值。


进一步,下图展示了Thrad,ThreadLocal,ThredLocalMap三者间的关系。


image.png


回到我们例子,那导致出现访问错乱的原因是什么呢?其实很简单,原因就是 Tomcat 内部会维护一个线程池,从而使得线程被重用。从图中可以看到两次请求的线程都是同一个线程: http-nio-8080-exec-1,所以导致数据访问出现错乱。


image.png


那有什么解决办法吗?其实很简单,每次使用完记得执行remove方法即可。因为如果不调用remove方法,当面临线程池或其他线程重用机制可能会导致不同任务之间共享ThreadLocal数据,这可能导致意外的数据污染或不一致性。就如我们的例子那样。


总结


至此,我们以一个实际生产中遇到的一个问题为例由浅入深的分析了ThreadLocal使用不规范所带来的线程不安全问题。可以看到排查问题时,我们用到的不仅仅只有ThreadLocal的知识,更有多线程相关的知识。


可能平时我们也会抱怨学了很多线程知识,但工作中却很少使用。因为日常代码中基本写不到多线程相关的功能。但事实却是,很多时候只是我们没有意识到多线程的使用。例如,在Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境,否则接口也不可能支持这么高的并发,并不能单纯认为没有显式开启多线程就不会有线程安全问题。此外,虽然jdk提供很多线程安全的工具类,但其也有特定的使用规范,如果不遵循规范依旧会导致线程安全问题, 并不是使用了线程安全的工具类就一定不会出问题!


最后,再多提一嘴,学了的知识一定要用起来,可能你为了应付面试也曾看过ThreadLocal相关的面经,也知道使用ThreadLocal要执行remove,否则可能会导致内存泄露但编程的很多东西,确实需要自己实际操作,否则知识并不会凭空进入你的脑海。


选择了程序员这条路,注定只能不断的学习,大家一起共勉啦!另外,祝大家双节快乐!


作者:毅航
来源:juejin.cn/post/7283692887573184547
收起阅读 »

大专还有机会进大厂吗??

好多同学问我,月哥,大专还有机会进大厂吗?? 我虽然是做培训的,,,但是每当遇到这样的问题,,我总是不知道该如何的回答,,,我很想安慰你,但是说出去的话,只能打击你!!我不是神呢!以往有大专进大厂的案例,现在基本为0了。虽然我很想挣你的钱,但是我知道,我不可...
继续阅读 »

好多同学问我,月哥,大专还有机会进大厂吗??



  • 我虽然是做培训的,,,但是每当遇到这样的问题,,我总是不知道该如何的回答,,,我很想安慰你,但是说出去的话,只能打击你!!我不是神呢!以往有大专进大厂的案例,现在基本为0了。虽然我很想挣你的钱,但是我知道,我不可能给你画饼的,因为这个饼硌🦷

  • 如果你以前没有进过大厂,没有一线大厂的履历,以后应该也不会了,至少今年这个行情下,985进大厂的都不多,你99.9999%进不去了,留下0.0001%给你幻想下。


他们都说不卡学历



  • 我们不卡学历的,你只要足够优秀,,,然后简历给我看下,,,哦,不好意思,你不符合我们的招聘要求,,,问其原因,,拜拜了您!

  • 然后找内部的leader问,今年人太多了,985,211的都一堆简历,,,学历低的基本不看的,浪费时间,,,,,就尼玛现实,,,,!

  • 就算你很牛,但是,我就不给你面试机会,你怎么地。

  • 内推有用吗??基本没用,除非你简历本身就有类似上千star的开源项目的核心作者这种经验,不然很难让他们相信,你很牛批,,,而且面试的难度也是巨大的,,,如果面试官不是你二大爷的话!手动滑稽一波。


我考在职的计算机研究生,,,,



  • 你要说他毫无用处吧,也不合适,那么含金量基本为0.000001,,,我就不展开说了,懂的都懂!


所以,,,,铁子们



  • 疯狂刷题,进不了大厂,但是涨薪才是王道,进不进大厂不是那么重要,,,,围城而已,只是一份工作而已,虽然钱多些,但是这个钱不是很好拿!


想去大厂的路



  • 写好的文章,打造社区影响力

  • 参与开源,有好的开源项目

  • 学好算法和英语,远程国外


以上都是长线作战的事,很多人都坚持不下来的。但是坚持下来就会有很大的提升,光说不练假把式,想要得到,就得先做到,参考月哥的经历,得非常非常的长线的努力才行!


志不达,为人生常态




  • 我很想告诉你努力有用,但是,确实用处不大,因为你做不到,很多同学都是口嗨,连安静的看书一个小时都做不到,何谈进阶!




  • 调整好自己的心态,,,莫焦虑!得刷题!不然你连工作都不好找呢!




  • 努力提升自己,不必要一定去大厂,很多小厂也不错,涨薪水才是王道!



作者:前端要努力
来源:juejin.cn/post/7277912168493465640
收起阅读 »

放弃熬夜,做清晨的霸主🔥

☀️ 前言 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)。 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。 在尝试早起将近一个月的时间后,我...
继续阅读 »

☀️ 前言



  • 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)

  • 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。

  • 在尝试早起将近一个月的时间后,我发现,我的效率确实是有了质的提升,接下来我会根据HeyMatt老师提到的方法和我一些实践来进行说明,感兴趣的小伙伴欢迎收藏慢慢看。


🕐 极致利用晚上时间的错觉



  • 会不会有很多小伙伴会有这种情况,每天辛勤劳作后,到了11点半大脑就会提示你:累了一天了,要不要放松一下呢?视频里说到,这种大脑暗示的放松大体分为三种:

    • 开始刷视频,打个游戏,借助浅层的刺激感来放松

    • 点个宵夜,搞个小烧烤吃吃,借助食物换取特定心境

    • 想一些过往能够牵动情绪的往事,沉浸在起伏连绵的情绪中



  • 绝了,以上三种我都尝试过,全中,但是作为程序员我还会有其他的几种:

    • 学习知识📖

    • 优化代码💼

    • 加快需求进度,赶需求🏃



  • 我经常会有这种想法,如果晚上11点半到1点半我可以把这些事情做完或者做多一点,那么我的时间就会被延长🕐。

  • 错❌,看了这个视频后我真的悟了,我花掉了N个晚上的两个小时,但是换不回来人生相应的发展,甚至很多质量很差的决策、代码都是在这个时间段产出的。

  • 可能你确实在这晚上获得了很多愉悦感,但是这个愉悦感是没有办法持续的第二天又赖床又想逃避,你会去想我白白浪费了晚上两个小时刷剧,过了一个晚上这个愉悦感在你早上醒来的时候会忽然转化为你的焦虑感

  • 确实是这样的,特别是在周末熬夜的时候,你会潜意识的特别晚睡,第二天让睡眠拉满,直接到中午才起床,但其实这样不是浪费了更多的时间吗?


🤔 三个风险



  • HeyMatt老师提到在熬夜的这些时间,面临了至少三个风险。


时间的消耗不可控



  • 就拿我来举例,我前段时间老是想着公司需求怎么做,需求的方案是不是不完整,是不是有可以优化的点,要修复的Bug怎么定位,怎么解决。

  • 我不自觉的就会想,噢我晚上把它给搞定,那么第二天就可以放下心去陪家人出去走走。

  • 可是事实呢?运气好一点或许可以在2个小时解决1点准时睡觉,但是运气不好时,时间会损耗越来越多,2个半小时,3个小时,4个小时,随着时间的消逝,问题没有解决就会越发焦虑,不禁查看时间已经凌晨3-4点了。

  • 就更不用说以前大学的时候玩游戏,想着赢一局就睡觉,结果一晚上都没赢过...😓


精神方面的损耗



  • 当我们消耗了晚上睡眠时间来工作、来学习、来游戏,那么代价就是你第二天会翻倍的疲惫。

  • 你会不自觉的想要睡久一点,因为这样才能弥补你精神的损耗,久而久之你就会养成晚睡晚起的习惯,试问一下自己有多久没有在周末看过清晨的阳光了?

  • 再说回我,当我前一个晚上没有解决问题带着焦虑躺在床上时,我脑子会不自觉全是需求、Bug,这真的不夸张,我真的睡着了都会梦到我在敲代码。这其实就是一种极度焦虑而缺乏休息的大脑能干出来的事情。

  • 我第二天闹钟响了想起我还有事情没做完,就会强迫自己起床,让自己跟**“想休息的大脑”**打架,久而久之这危害可想而知。


健康维度的损耗



  • 随着熬夜次数的增多,年龄的增长,很多可见或不可见的身体预警就会越来越多,具体有什么危害,去问AI吧,它是懂熬夜的。



🔥 做清晨的霸主



  • 那么怎么解决这些问题呢,其实很简单,把晚上11.30后熬夜的时间同等转化到早上即可,比如11.30-1.30,那么就转化到6.30-8.30,这时候就会有同学问了:哎呀小卢,你说的这么简单,就是起不来呀!!

  • 别急,我们都是程序员,最喜欢讲原理了,HeyMatt老师也将原理告诉了我们。


赖床原理



  • 其实我们赖床起不来的很大一部分原因是自己想太多了。

  • 闹钟一响,你会情不自禁去思考,“我真的要现在起床吗?” “我真的需要这一份需要早起的工作吗?” “我起床之后我需要干什么?” “这么起来会不会很累,要不还是再睡一会,反正今天不用上班?”

  • 这时候咱们大脑就处于一种**“睡眠”“清醒”**的重叠状态,就跟叠buffer一样,大脑没有明确的收到指令是要起床还是继续睡。

  • 当我们想得越多,意识就变得越模糊,但是大脑不愿意去思考,大脑无法清晰地识别并执行指令,导致我们又重新躺下了。


练就早起



  • 在一次采访中,美国作家 Jocko Willink 老师提出了一种早起方法::闹钟一响,你的大脑什么都不要想,也不需要去想,更不用去思考,让大脑一片空白,你只需执行动作即可。

  • 而这个动作其实特别简单,就是坐起来--->站起来--->去洗漱,什么都不用想,只用去做就好。

  • 抱着试一试的心态,我尝试了一下这种方法,并在第二天调整了闹钟到 6:30。第二天闹钟一响,直接走进卫生间刷个牙洗个脸,瞬间清醒了,而且我深刻的感觉到我的专注力精神力有着极大的提升,大脑天然的认为现在是正常起床,你是需要去工作和学习👍。

  • 绝了,这个方法真的很牛*,这种方法非常有效,让我觉得起床变得更容易了,推荐大家都去试试,你会回来点赞的。


克服痛苦



  • 是的没错,上面这种办法是会给人带来痛苦的,在起床的那一瞬间你会感觉仿佛整个房间的温度都骤降了下来,然后,你使劲从被窝里钻出来,脚底下着地的瞬间,你感到冰凉刺骨,就像是被一桶冰水泼醒一样。你感到全身的毛孔都瞬间闭合,肌肉僵硬,瑟瑟发抖,好像一股冰冷的气流刺痛着你的皮肤。

  • 但是这种痛苦是锐减的,在三分钟之后你的痛苦指数会从100%锐减到2%

  • 带着这种征服痛苦的快感,会更容易进入清晨的这两小时的写作和工作中。


✌️ 我得到了什么



  • 那么早起后,我收获了什么呢❓❓


更高效的工作时间



  • 早起可以让我在开始工作前有更多的时间来做自己想做的事情,比如锻炼、读书、学习新技能或者提升自己的专业知识等,这些事情可以提高我的效率专注力,让我在工作时间更加高效。

  • 早起可以让我更容易集中精力,因为此时还没有太多事情干扰我的注意力。这意味着我可以更快地完成任务,更少地分心更少地出错


更清晰的思维



  • 早上大脑比较清醒,思维更加清晰,这有助于我更好地思考解决问题,我不用担心我在早上写的需求方案是否模糊,也能更好的做一些决策

  • 此外,早起还可以让我避免上班前匆忙赶路的情况,减少心理上的紧张压力


更多可支配的时间



  • 早起了意味着早上两个最清醒的时间随便我来支配,我可以用半小时运动,再用10分钟喝个咖啡,然后可以做我喜欢做的事情。

  • 可以用来写代码,可以用来写文章,也可以用来运营个人账号

  • 可以让我有更多的时间规划安排工作,制定更好的工作计划时间管理策略,从而提高工作效率减少压力


更好的身体健康



  • 空腹运动对我来说是必须要坚持的一件事情,早起可以让我有更多的时间来锻炼身体,这对程序员来说非常重要,因为长时间的坐着工作容易导致身体不健康

  • 用来爬楼,用来跑步,用来健身环等等等等,随便我支配,根本不用担心下班完了后缺乏运动量。


👋 写在最后



  • 我相信,我坚持了一年后,我绝对可以成为清晨的霸主,你当然也可以。

  • 而且通过早起不思考这个方法,很多在生活有关于拖延的问题都可以用同样的方式解决,学会克服拖延直接去做,在之后就会庆幸自己做出了正确的决定

  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

作者:快跑啊小卢_
来源:juejin.cn/post/7210762743310417977
收起阅读 »

IT外传:会议室里的技术副主管

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑,单名一个“常”字,是一名资深程序员。最近,他新入职了一家IT公司,岗位是后端开发。 入职后,他的领导周主管,给他安排了一项任务:对一类表单图片的特定区域进行处理。 这个表单图片,是...
继续阅读 »

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



老郑,单名一个“常”字,是一名资深程序员。最近,他新入职了一家IT公司,岗位是后端开发。


入职后,他的领导周主管,给他安排了一项任务:对一类表单图片的特定区域进行处理。


pic.png


这个表单图片,是由前端动态生成的,主要做信息收集用。现在要求前端生成时,附带一份内容项与坐标区域的配套信息。比如图片的1/4到1/2的区域范围是教育经历模块,1/2到2/3的区间是工作经历部分。而老郑要做的,就是将这些区域裁剪下来。


代码很简单,用opencv就行。从原图中裁出一个区域,python就一句话crop_img=img[y1:y2, x1:x2]


pic2.png


为了验证用百分比标注二次还原的效果,老郑还专门用js写了一个制作表单的前端页面。他在生成的同时,也记录一份同名标注的json文件。一试,效果很好。


老郑等着项目启动,因为他要对接生成表单的项目组。这天他正在工作,产品经理叫他赶紧到大会议室来一趟,请求支援。


老郑不明白什么事情,就去了。


会议室很大很气派,里面已经聚集了十多个人。大家鸦雀无声,气氛凌冽,似乎会议被中止了。这里面,老郑只认识一个人,就是产品经理董非凡。这个方案就是董非凡和老郑一起讨论出来的。现在董非凡对他们组里的技术进行宣讲时,出现了问题。


“老郑,你给大家说说你的想法!”


很明显,董非凡已经给老郑留出了C位。老郑拉开豪华转椅坐下,说道:“就是咱们前端在生成的时候,将几个关键点的坐标记录一下……”


巨大的方形会议桌的对角线,斜躺着一个黑衣人。黑衣人说:“你说的这个,实现不了!”


老郑瞬间一怔:“实现……不了,为什么实现不了?”


董非凡拉扯了老郑一下:“郑工,你把你实现的给大家看看!”


老郑用浏览器访问他写的表格制作页面,按下F12,调到Console选项,操作了一番,控制台输出一串串坐标信息。


老郑说:“这可以的呀!我不是前端,就会几句js,但是我觉得能实现!”


黑衣人说:“你能实现,并不代表我们能实现。我们和你用的不是一种实现方式!”


老郑被这种傲慢刺激到了,他回怼说:“那你们就换我这种方式”。


老郑感觉自己是新人,而且不清楚黑衣人是谁,压了压情绪。他咧着嘴笑了笑,说:“哎呀,我不干涉你们用哪种方式,我又不懂,只要能给我提供数据就行呗!”


黑衣人问对面的一个小伙子:“咱们能实现吗?”


小伙子点了点头说可以的,他说可以通过计算div的偏移量来获取指定区域的坐标。


黑衣人打断了小伙子,让他不要说了。


黑衣人对老郑说:“做是可以做,但是我需要抛一个风险,这种方式可能会出错!”


“出错?”老郑又是一怔,“为什么会出错?!”


黑衣人说:“这个世上没有绝对不出错的事情。”


老郑压了压情绪,和颜悦色地说:“哎呀,这个你也出个错,我这也出个错,一串起来,我们这个活可没法干喽”


黑衣人解释道:“任何事情都没法保证全对,你不这样觉得吗?你能保证你的代码100%没问题?”


董非凡插话说:“那个……出错没关系,我们可以调嘛!我们保证在理想条件下能走通,然后到实际场景中,我们再去做容错嘛!后面还有对接,自测,测试。”


会议结束了。


老郑问董非凡,会上那个黑衣人是谁啊?


董非凡说,他是负责生成模板业务的技术副主管。


“副主管?那正主管呢?”


“正主管就是做你对面那个!他是做Java的,他管后端。前端的讨论,他不参与”


后来,需求有所细化。不但要裁切大区域模块,而且还要把里面更细致的信息也裁出来,就比如教育经历中的学校名称区域。


pic3.png


需求是这个需求。但是,谁来推动呢?周主管跟老郑说,你去组织一个会议,跟兄弟部门说说需求,然后要个工期。


老郑问:“有必要开会吗?我去前端小伙子的工位旁跟他说一下”


周主管说:“得开会。拉上我,叫上对方的前端小伙子,以及小伙子的主管,还有产品经理。我们要在会上正式提出需求,然后讨论技术可行性,确定什么时间能提供给我们。会后写一个会议纪要,通知相关领导。”


老郑立马约了一个15分钟的会,他觉得是随手返回数据的事情,说完的功夫就做完了。约会议,只不过是把问工期搬到会上有一个仪式感。


周主管感觉15分钟时间太短了,要约长一点,至少30分钟。老郑协调了半天,这几个人的会议日程都有冲突。周主管表示,大家忙的话,会议可以延期。上午没空就下午开,下午没空就明天开。实在不行,可以加班开这个会。


老郑觉得还是算了,赶紧开吧。于是就约了下午的会议。结果开会时,就老郑和小伙子去了,其他人都被叫去开各种临时会了。


老郑和小伙子面对面。老郑说,这个会是领导要求开的。


“我发你的需求看过了吧?我们也在线沟通过细节,应该没啥问题”


小伙子说没问题。


这个会议好像1分钟就结束了。


老郑想,还有没到会的领导,要不要等等他们?否则,我们这一结束,他们再过来,会指责这会议没开。


老张和小伙子先是聊了聊技术,后又聊了聊技术。


大约二十分钟后,差不多了。老张想问问小伙子,多返回那一个位置,大概需要做多久。


此时,上次那个黑衣人,也就是主管前端的副主管,急匆匆地过来了。


“哎呀,幸好赶上了。那个需求看了吗?”黑衣人问小伙子。


小伙子还没来得及搭茬,副主管说:“我看过了,哎呦,我反正是没想到实现思路”


小伙子不说话了。


老郑不愿意和他多聊,老郑说:“需求我俩讨论清楚了,现在需要定一下开发周期。这可不是我要啊,是我领导要,最后还得形成会议纪要。不知道是现在能给呀,还是得回去研究研究……”


小伙子仍然不说话。副主管说:“这个时间啊,还真不好说。咱们都是干技术的,我不说你也懂。这种研究性的工作,没有试过谁知道呢?顺利的话,可能五分钟就出来了。当然,也可能一周才能给你。从我的角度来看,现在仍然没有思路,不知道该怎么去实现。但是,我们保证,努努力,不管克服多大的困难,最后肯定是要搞出来的。这样吧,给你一个最大时间,一周内做完。不是说从今天起一周之后,也可能这周三、周四就做完了,提前做完了就当是给你一个惊喜”。


老郑说了句好的,就结束了会议。他回去写下会议纪要:第一,双方已明确对接需求;第二,一周内完成交付。然后他就开始写代码了。项目没有负责人,这意味着谁都能管,同时谁也没法管。就算他明知道半天能干完,又能怎样呢?和对方领导去讨价还价?说我不行,你行你上啊!这除了树敌,没有任何好处。这可能就是环境、氛围,或者称之为“文化”。


此时,老郑的内心波澜不惊。唯一让他思绪泛起一点波纹的,是他从黑衣副主管身上,看到了以前的自己。


大约7年前,老郑还是一家上市公司的中层干部、小股东。公司为了加强技术体系的横向建设,从所有业务线中,每个工种抽离出一个人,这些人合伙组成了一个叫技术研究院的组织。老郑当时被选中,负责整个公司有关移动端(Android、iOS)的技术攻关、工期评估、框架管理。


起初还好。移动端的开发者多是老郑面试并招进来的,而且很多业务也是老郑的一手项目。但是到后来,随着人员流动,加上老郑开始脱离了具体业务,将更多精力投入到了写文档和申报材料当中。他再也不知道每个业务的具体功能,如何实现。渐渐地,他提出的一些思路,大家不再支持,他说话也没有人听从。


有一次,老郑发现会议室在开会。他从缝里看到了一个事业部的产品、技术在讨论问题。老郑推门进去。他依然清楚地记得那个iOS兄弟姓宋,产品经理姓李。iOS兄弟的实现思路和报工期的方式,明显违背了老郑定的策略。老郑当场发飙了。老郑说,你们今天提需求明天就上线,这样制定计划是有风险的。我是由公司任命的研究院副院长,也是股东,我要对公司负责。吧啦吧啦他说了一通。


小宋和小李并没有理会老郑,反而是事业部的罗总闻讯过来,连忙给老郑道歉。后来,事业部越来越独立。老郑也慢慢没有了存在的意义。临走前,老郑专门找到产品经理小李,跟他道了歉。老郑说他们事业部的开发效率越来越高了,紧跟市场的脚步,蒸蒸日上,我以前的想法是错的。其实,到现在为止,老郑也没有搞清楚,到底是自己玩死了自己,还是公司玩死了自己。而小李也只是客套几句就去忙了。


而今的黑衣副主管,多少也有点这个意思。他们并不关注事情本身(没有精力),只关注通用的流程。不管是1分钟的活,还是一个月的任务(很难分辨),都要开满各项会议(总是没错),要显得很艰难,要留出足够多的抵抗风险的时间。


“咚咚~”小伙发来了一条消息。他说,我先给你个测试版的对接着。过两天我再从群里发布个正式版。


老郑望向窗外,笑了笑。


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

劝你放弃纸上谈兵

引言 纸上谈兵,汉语成语,拼音是zhǐ shàng tán bīng,意思是指在纸面上谈论打仗。比喻空谈理论,不能解决实际问题。也比喻空谈不能成为现实。 WEB 开发发展到现在,各种优秀的框架以及丰富的网络资源,让 WEB 开发入门门槛降到了很低很低,但是...
继续阅读 »

引言



纸上谈兵,汉语成语,拼音是zhǐ shàng tán bīng,意思是指在纸面上谈论打仗。比喻空谈理论,不能解决实际问题。也比喻空谈不能成为现实。



WEB 开发发展到现在,各种优秀的框架以及丰富的网络资源,让 WEB 开发入门门槛降到了很低很低,但是并不是证明 WEB 开发没有门槛了,也不能证明 WEB 开发就没有难度了


最近在学校做项目,依稀能听到这种事情:老师给了一个XXX题目,甲同学看了看题目,思考了一下,觉得这个项目挺简单的,甚至有可能看不上这个简单的项目……说实话,以前我也是抱着这种心态,觉得老师给的那些项目又简单又 low,做出来也是浪费时间,没有什么太多意义


不过,后来在我做了一个“秒杀商城”的项目以后,我就开始认真对待每一个看似简单的项目了。为什么呢?


秒杀商城


讲实话,这个项目就是纯拿来练手,为了往简历上写项目经历的项目。但是我也是在这个项目里面遇到了很多很多的问题......


期初想到这个项目的时候,我就在想,网上一大堆电商项目,也有很多高并发的商城秒杀项目供我参考,那我做这个还不是简简单单么


然后,在我真正开始做这个项目的时候,我遇到了很多很多的没有预料的甚至见都没见过的问题,我想,假如我是面试官,我想要判断一个人是否真的做过这个项目,下面这些问题就够了


在使用消息队列的时候,前端如何判断消息是否正确消费


第一次接触消息队列的时候,因为消息队列没有返回值让我挺难受的。但是在做高并发,尤其是秒杀项目的时候,消息队列仍然是首选


那么如何解决前端判断消息是否消费呢(比如订单是否已经支付完成)?


其实这个问题的答案很简单,简单到我在第一次听到这个答案的时候都有点不敢相信:轮询(即设置一个周期定时器,一直调用接口进行查询)。当然,也可以用其他的方式,这种方式只是最简单最好理解的一种


前端JavaScript的Number型与Java的long型最大长度问题


遇到这个问题的时候是因为项目里面生成全局唯一 id 使用到了雪花算法,雪花算法生成的就是一个64位的唯一 id,正好就是 Java 的 long 型最大位数。也正是因为这个算法用到了64位数字,所以就会遇到与 JavaScript 的 Number 类型数字的最大长度问题了


我们先来输出一下 Java 和 JavaScript 的最大值


public class Main{
public static void main(String[] args) {
System.out.println(Long.MAX_VALUE);
}
}
// 9223372036854775807
// 0x7fffffffffffffffL

console.log(Number.MAX_VALUE)
// 1.7976931348623157e+308

通过上面两段代码可以最直观的看出来一个问题,Java 的 long 型最大值与 JavaScript 的 Number 最大值不一样


然后我们再看一下下面这个代码:


var x = 9223372036854775807;
console.log(x)
// 9223372036854776000

这里只是把 Java 的 long 型最大值在 JavaScript 中输出出来,结果是最后三位数字变为0,倒数第四位数字进行四舍五入了。那么在开发的时候肯定就会遇到问题


这个问题解决办法其实也很好理解,做过大数相加的那种算法题的应该都知道,那就是用字符串表示数字。这个问题的解决办法就是后端传这种 long 型数据的时候使用字符串就好了


feign远程调用问题


这个问题只会让习惯不好的同学遇到(比如我),因为我在传统的 spring boot 开发的时候,一般不会在RequestMapping接口处写@RequestParam注解,然后在我第一次使用 feign 的时候,我仍旧在服务生产者处不写@RequestParam注解,结果导致 feign 远程调用失败


当然,这个问题的解决过程也是很顺畅,因为网上很多人(估计大部分都是新手)都遇到了这个坑


小结


实际上,上面的这些问题都不是特别难的问题,都是我们平时开发遇到的一些遇到了随便查一下就知道的问题。但是这些问题足以证明自己是否真真正正的做过这些项目


回到正题,如果仍旧是纸上谈兵,不去切实的自己动手尝试一遍,那么这个项目又怎么愿意写在简历上呢?我们常说见微知著,其实我认为往往就是这些地方就可以见微知著


作者:大爆米花
来源:juejin.cn/post/7283438991473426467
收起阅读 »

我的发!地表最强扫一扫

web
在很久很久以前,我亲爱的同事们在对接二维码扫描业务的时候,都是使用的微信官方自带的扫一扫,比如这样 wx.scanQRCode({ needResult: 0, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果, scanType: ["qrC...
继续阅读 »

在很久很久以前,我亲爱的同事们在对接二维码扫描业务的时候,都是使用的微信官方自带的扫一扫,比如这样


wx.scanQRCode({
needResult: 0, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType: ["qrCode","barCode"], // 可以指定扫二维码还是一维码,默认二者都有
success: function (res) {
var result = res.resultStr; // 当needResult 为 1 时,扫码返回的结果
}
});

所以我扫码就一定得依赖微信,在普通的浏览器中打开就GG,并且还要绑定公众号,烦的一批。


然后我就在想,扫码不就是靠摄像头捕捉图像进行解码出内容嘛,那肯定会有原生的解决方案。


Google Google Google Google ......


果然是有的,Web API中也提供了一个实验性的功能,Barcode Detection API


image.png


它提供了一个detect方法,可以接收图片元素、图片二进制数据或者是ImageData,最终返回一个包含码信息的Promise对象。


但是呢,这个功能的浏览器兼容性比较差,看了caniuse,心凉了一半。


image.png


但我相信大神们肯定有自己的解决方案,继续Google呗。


Google Google Google Google ......


还真有这么一个库,html5-qrcode,它在zxing-js的基础之上,又增加了对多种码制的解码支持,站在巨人的肩膀上又跟高了一层。


html5-qrcode支持的码有:


CodeExample
QR Codeimage.png
AZTECimage.png
CODE_39image.png
CODE_93image.png
CODE_128image.png
ITFimage.png
EAN_13image.png
EAN_8image.png
PDF_417image.png
UPC_Aimage.png
UPC_Eimage.png
DATA_MATRIXimage.png
MAXICODE*
RSS_14*
RSS_EXPANDED*image.png

我个人觉得非常够用了,平时用的最多的还是二维码、条形码,其他的码也都少见。


关键是人家还支持了各种浏览器,可以说已经是很良心了(什么UC浏览器的,其实我都瞧不上,不支持就不支持,无所吊谓)


image.png


来看看官方提供的demo效果


chrome-capture-2023-8-27.gif


好好好,很棒。但是他们没有提供框架支持,那么我又可以站在巨人的肩膀上的巨人的肩膀上造轮子了。


先来看看我自己封装的React组件


demo.gif


使用方法也简单


function App() {
const scanCodeRef = useRef();
const [scanResult, setScanResult] = useState('');

function startScan() {
scanCodeRef.current?.initScan();
}

return (
<div>
<button onClick={startScan}>扫一扫</button>
<p>扫描结果: {scanResult}</p>
<ScanQrCodeH5
ref={scanCodeRef}
scanTips="请一定要对准二维码哦~"
onScanSuccess={(text) =>
{
setScanResult(text);
}}
// onScanError={(err) => {
// console.log(err);
// }}
/>
</div>

);
}

三二一,上链接,rc-qrcode-scan


这次的版本没有加入从相册选择图片进行解码,下个版本将会加入,希望能帮到掘友们。


2023-09-28更新,掘友们我把从相册选择加进去了。


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

我转产品了-前端转产品是一种什么样的体验

程序之路 入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、 webapp 。然后是 node/express/koa ,开始涉及全栈。 代码...
继续阅读 »

程序之路


入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、
webapp 。然后是 node/express/koa ,开始涉及全栈。
代码管理工具也从 svn 到 git ,然后制定提交规范,分支管理规范,结合 gitflow/githook 以及各种 lint 保证团队开发风格及可维护
性。
产品发布的方式从 ftp 上传,到 npm/nodejs/shell 脚本,然后再到 jenkins/docker/git 多分支多环境部署。


从第 3 年之后就感觉技术没什么提升了 ,后面都是在各个小作坊担任前端组长角色(其实感觉就是救火队长),哪里项目急去哪里,哪里有难题去哪里。实际比 UI、比测试、比实习产品的地位还低,基本没有话语权。



为什么转产品


严格来说,并不是专门的喜欢产品这个职位,而是希望了解产品经理所做的事。因为在软件开发的工作里,工作的内容和返工程序大大取决与产品对用户需求的理解能力,业务熟悉能力。而作为前端,经常只集中精力在处理页面还原、交互实现、数据对接、浏览器兼容等工作上面。对整个系统的业务逻辑是比较片面的。


如果对用户需求和产品业务有所了解,那可能在开发之前就能发现需求上的不必要性,发现设计上的错误,而减少程序开发的返工率。


总的说来,是期望:



  • 拒绝无效编程

  • 深入理解业务

  • 培养跨部门沟通能力

  • 培养产品设计能力


是否适合转产品


根据上面所说的几点理解,我自身而言并不拒绝,这是在心理方面。


在能力方面,我认为我是可以去学习和培养得到这份能力的。因为自己做的一个程序库 demo,得到了第一份前端工作。前端工作 2 年后,老板尝试让我做产品,并在过程中得到老板的一些建议。1、做产品就不要去考虑程序实现;2、如果自己是对的,就要去坚持,争执得面红耳赤也没有关系。对于这两点建议现在我是如何理解的,后面我会讲。


在习惯方面,我经常会吐槽 xx 产品应如何实现,经常觉得 xx 产品很难用,也经常自己开发 xx 小工具。当然这里我想说:人人都是产品经理,我是认可这句话的。因为产品的受众就是大众,而大众的感受就是产品。至于我自己的 xx 小工具,当然也会被吐槽,不过我觉得这并不影响“喜欢做产品”这个习惯,而做出好产品,是在做产品的过程中去获得的能力。


如何得到这份工作


严格意义上的这份工作,大家都知道一般而言薪资是比开发要低一些的。我说下我能给到的:



  • 接受作为入门岗产品的薪资,不考虑自己的开发经验的工资

  • 能陪开发一起加班,一起赶项目

  • 能在与客户的需求讨论阶段,通过自己的开发经验给出符合客户所需和较低开发成本的解决方案

  • 能处理好产品核心的工作,例如需求文档、原型设计等(仅限于我当前对产品职务的了解)

  • 必要时可为前端团队提供技术方案


真正意义的产品岗的入门工作


我入职这家公司,公司管理层有征求我的意见。问公司现在缺产品,把我拉来填这个位置,问我的想法之类。我接受之后,在这个公司的职位就正式为产品了。


前期的工作,是与另一个兼职的产品去客户现场去了解需求,我做会议纪要,每场会我都在。


领导的意思是,因为兼职的那个产品可能会照顾不到。所以期望我今后能全权接受他手上的项目和往后的产品项目。


另一个公司的项目是两个团队开发的,公司一个团队,公司外部一个团队。这个项目有二期,计划我来接手二期。因为一期临近上线,把我接去做测试,说是我也刚好可以熟悉一下这个项目。虽然在之前我的理解中,产品就是产品,测试就是测试,心里多少有一点抵触。但想到确实在测试过程中多少可以增加对系统的了解,也坦然接受了。


然后在临近上线时,客户认为当前的产品流程不符合需要。需要修改流程,还要增加一个额外的流程。本来项目时间有所有延后,又加上客户添加需求,所以双方决定延后半月上线,但要添加新的流程以及再加一个二期功能。


这个二期功能中有一个拓客功能就是我将来要设计的模块。现在相当于我要提前介入。


不过好在这个系统的客户都还比较好相处,在客户现场做测试、改 BUG、讨论需求的这几天里,经常各种好吃好喝的东西都拿过来。饭点也问大家的口味情况,不重样的给大家点餐。系统有不少的问题,客户也没发脾气(这个我至今没理解)。


一个拓客模块原型


在我的构思中,是打算把整个拓客功能高度抽象化,尽量减少与原系统的耦合度。希望将来其他系统能便于复用这个模块,因为拓客功能是面向 C 端程序常见需求,并且流程也容易标准化。


所以构思了很多东西。


当与客户讲了这些东西之后,客户表示很多东西都有考虑到位。当然也有客户的自己侧重点的东西和必要上的东西的考量,这些东西在前期可能作为产品是比较难感知到的。



与客户讨论需求的部分心得


心得来源于分歧。


虽然这次需求沟通总的来说达到了自己的预取。但这边负责人后面批评了我,所为什么我要给客户讲这么多东西?为什么要答应他们?我们做不完!


我说我没有答应他们什么,我只是尽可能的去了解客户想做的,和让客户知道我想做的。后面我有意识到,由于这个模块是在这半月之类要临时加上去的,负责人害怕客户会认为我给他讲的那些功能就是这半月之类要上的功能。


所以,在这种情况下,在与客户表达功能的同时,要避免客户对功能产生错误的预期。


所以我后面单独找客户聊了,由于时间紧迫,之前给他讲的那些功能并不能完全实现。然后给他展示我这边能给到的一个满足他拓客条件的简化版本。客户表示理解,欣然接受,这个简化版本也与团队进行了同步,没什么问题。


另外,对于一个功能的实现,有很多做法和分支。我们不用一开始做得很细,当与客户沟通,得到客户想做的方向之后(当然客户想做的方向不一定正确,而如何能提前知道客户的方向不正确,这可能是更上一层的能力,比客户更了解客户所面临的问题)。


一个需求文档


拓客所处的项目第一期进行了近一年左右,神奇的是居然没有还没有需求文档。现在项目要上线了,负责人要去找客户结账了才想到要这文档。然后这文档让我来写,对于半路介入这个项目并且刚试岗这个职位的我来说简直头皮发麻。因为据我了解需求文档这东西巨细无遗,需要深入到系统的每个流程和细节。


谁让我现在是这个角色,我不入地狱谁入地狱?随后我反手就找公司把公司的需求文档模板发我一下。模板发了,但我一眼看过去,只知道需要填些什么内容,像是一个骷髅,却想不有内容的样子应会是怎样的,不知道一个有血肉甚至是有灵魂的样子是怎样的。


然后又让公司把以前的其他项目发我一份。然后公司随手发我一份,我打开一看,好家伙,161 页,部分内容如下:



以我之前的了解,需求文档这东西主要是用于验收的(实际开发中需求文档根本来不及跟上需求的变化)。而验收时为了表达工作量,需求文档通常都是内容越多越好。


所以这真也是个体力活。


为了让需求文本能与现有的实现相符合,我打开了现在的系统,现在的系统有些流程还跑不通,然后又根据我的之前的测试结果和现有原型的理解,进行梳理,先把页面和功能拉出来,大概如下:


# 后台管理系统
- 登录
- 用户名
- 密码
- 验证码
- 记住密码
- 系统管理
- 区域架构
- 展开和折叠
- 上级区域
- 名称
- 排序
- 状态是启用还是停用
- 区域层级
- 搜索 -- 名称、层级、状态
......
......
......

# 小程序
- 推广中心
- 统计面板
- 奖励总金额 -- 考虑隐私问题暂不展示应邀人员的细目
- 注册人数 -- 考虑隐私问题暂不展示应邀人员的细目
- 去提现 -- 跳转到体现页面
- 去提现
- 展示总的可提现金额
- 输入想提现的金额发起提现申请
- 展示提现申请记录

- 登录
- 有手机号时授权登录
- 无需要号时通过验证码登录,并进行实名认证
- 设置安全密码
......
......
......


然后根据页面和功能点去展开描述。具了解,需求文档需要包含以下内容:


- 产品概述
- 功能概述
- 用户需求
- 功能分析
- 非功能性需求
- 界面设计
- 数据需求
- 约束和假设

而在功能需求中,有几点是常见的:


- 功能概述
- 功能分析
- 界面设计
- 数据需求

看起来就是功能概括是怎样的?功能具体是怎样的?界面怎样的?数据库设计是怎样的?


很明显,数据库设计这个我暂时细致不了,而且我看现有的需求文档中也不是每个功能都把数据库设计放上去的。总之我认为,能基本把功能描述清楚,看起来够分量就行啦。


那么基于上面我列出的功能结构,例如:


- 登录
- 用户名
- 密码
- 验证码
- 记住密码

是很容易能推导出来:


- 功能概述
- 功能分析
- 界面设计

这东西的:


### 功能概述
本功能旨在提供用户登录系统的功能,包括输入用户名、密码和验证码,并提供记住密码的选项。

### 功能分析
用户登录功能主要涉及以下几个要素:

1. 用户名:用户需要输入其注册时使用的用户名。
2. 密码:用户需要输入与用户名对应的密码。密码应该以安全的方式进行存储和传输,例如使用哈希算法进行加密。
3. 验证码:为了增加登录的安全性,可以添加验证码功能,要求用户输入验证码。验证码通常是由字母和数字组成的随机字符串,用于验证用户的真实性。
4. 记住密码:提供一个选项,让用户选择是否记住密码。如果用户选择记住密码,下次登录时系统会自动填充用户名和密码。

### 界面设计
用户登录界面应包含以下元素:

- 用户名输入框:用于输入用户名。
- 密码输入框:用于输入密码。密码应以隐藏或替代字符的形式显示。
- 验证码输入框:用于输入显示的验证码。
- 验证码图片:用于显示验证码的图像,以便用户看到并输入。
- 记住密码复选框:用于让用户选择是否记住密码。
- 登录按钮:用户点击此按钮以提交登录表单并尝试登录系统。


然后我就以这种方式完成了 98 页的需求文档,这样应该能先交差一版了。


image.png


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7283766477802864675
收起阅读 »

降低代码可读性的 12 个技巧

工作六七年以来,接手过无数个烂摊子,屎山雕花、开关编程已经成为常态。 下面细数一下 降低代码可读性,增加维护难度的 13 个编码“技巧”。 假设一个叫”二狗“ 的程序员,喜欢做以下事情。 1. 二狗积极拆分微服务,一个表对应一个微服务 二狗十分认可微服务的设计...
继续阅读 »

工作六七年以来,接手过无数个烂摊子,屎山雕花、开关编程已经成为常态。 下面细数一下 降低代码可读性,增加维护难度的 13 个编码“技巧”。


假设一个叫”二狗“ 的程序员,喜欢做以下事情。


1. 二狗积极拆分微服务,一个表对应一个微服务


二狗十分认可微服务的设计思想。认为微服务可以独立开发和发布,每次改动不会影响其他系统。大大提高了开发人员的效率和线上稳定性。还可以在新服务里使用新的技术,例如JDK 21


于是狗哥把微服务的思想发挥到极致,每一张表都是一个服务。系统的应用架构图十分壮观。狗哥自豪的跟新同学讲解自己设计的系统。新同学看着十几个服务陷入了思考,不停地问着每个服务的作用,干了什么。狗哥很满足。


新同学第一次开发需求,表现很差。虽然他要改10个服务,但是每个服务只改动了一点点。并且由于服务之间都是Rpc调用,需要定义大量的接口,他需要发布好多的 jar,定义版本号,解决测试环境版本冲突,测试和上线阶段可把他忙坏了。


光是梳理上线顺序,新同学就请教了狗哥 三次。 最后还是狗哥帮他上线了3 个服务,新同学才赶在 凌晨 3 点前把所有的服务发完。看着新同学买了奶茶的份上,狗哥这次才没有和领导吐槽,“这个同学不行啊,上个线都这么费劲”


微服务过多,也困扰着狗哥。虽然线上流量不高,但是由于 “微服务太多,系统架构复杂",接口性能不行。


于是狗哥开始进行重构,他重新加了一个开关,新逻辑可以减少Rpc,调用提高性能。狗哥在代码中加了注释 "新逻辑"。


狗哥把代码上线了,但是在线上环境不敢放开,只在测试环境打开了开关。


2. 二狗积极重构代码,但是线上不放量


狗哥喜欢对代码进行重构,狗哥和领导吹牛,说“ 重构后的代码性能更强,更稳定”。 狗哥还添加了注释 ”这是新逻辑“。


但是狗哥在线上比较谨慎,并没有进行放量。只是在测试环境,放开了全量。


新接手的同学不知道线上还没放量,看到“这是新逻辑” ,他就在狗哥的“新逻辑”上改代码。测试环境验证一切正常,到了线上阶段却怎么也跑不通。


此时新同学才发现 ”新逻辑“ 的开关没有打开,你猜,他敢打开这个开关吗? 于是他只能删代码,在旧逻辑上重新开发。 等到改完代码,再上线时,已经天亮了。


由于这次上线问题,大家一起熬夜加班,需求上线被推迟。新同学被产品和测试一顿骑脸输出。新同学委屈的想要离职。


3. 二狗喜欢挑战自我,方法长度一定要超过1000行


二狗写代码天马行空。二狗认为提炼新方法会打断自己的编码思路,代码越长,逻辑越连贯,可读性越高。二狗还认为 优秀的程序员写的方法都是 非常长的。这能体现个人的能力。


二狗不光自己写超长的方法,在改别人的代码时,也从不提炼新的方法。二狗总是在原来的方法中添加更长的一段代码。


新同学接手代码时速度很慢,即使加班到凌晨,也不理解狗哥代码设计的艺术。狗哥还向领导抱怨,”你最近招的人不行啊,一个小需求开发这么久,上线还出了bug。“


4. 二狗喜欢挑战自我,一个方法 if/try/else 要嵌套10层以上


二狗写代码十分认真,想到哪里就写哪里。 if/else/try catch 层层嵌套。 狗哥的思路很快,并且思考全面,
嵌套十几层的代码一点bug都没有,测试同学都夸赞狗哥 ”代码质量真高啊“,一个bug都没有。


新同学接手新代码时,看到嵌套十几层的代码,大脑瞬间就要爆炸。想要骂人,但是看到代码作者是狗哥……


无奈之下,自己实在看不懂这段代码,于是点了一杯奶茶,走到了狗哥工位旁,”狗哥,多喝点水,给你点了一杯奶茶。…………这段代码能给我讲讲吗?“


狗哥过几天和领导闲聊天,“新来的同学人不错,还给我点奶茶喝”


5. 二狗认为变量命名是艺术,要随机命名,不要和业务逻辑有关系


二狗觉得写代码是艺术,就好像画画一样。”你见过几个人能看懂 梵高的画?” 狗哥曾经和旁边人吹牛。


二狗写代码思路十分奇特,有时候来不及想变量如何命名,有时候是懒得想变量命名。狗哥经常随便就命名了,例如 str1,str2,list1,list2等等。不得不说,狗哥的思维还是敏捷的,这么多变量命名都能记住,还不出bug。


但是狗哥记性不大行,过一两个月就不太记得这些变量的意义了。


6. 二狗积极写注释,但是写了错误的注释


一个成熟稳重的程序员改别人代码时会十分慎重,如果有代码注释,他们一定会十分认真阅读并尝试理解它。


二狗喜欢把注释引入错误的方向,例如 “是” 改成 “不是”,“更好”改成”更差“,把两处不相干的注释交换一下位置 等。


新接手的同学点了一杯奶茶,虚心求助二狗,“狗哥,你写的这段注释有什么深意啊,我看了三天,也不理解啊”。


到时候狗哥就可以给新同学一边装B,一边讲代码了。当然还要看心情,要是不口渴,可以讲讲。


7. 二狗改代码很认真,但是注释从来不改


二狗改代码真的非常认真,但是他不喜欢改注释。最终代码大改特改,注释纹丝不动。最终代码和注释不相干,部分正确,部分错误。


新接手的同学研究了两天也没搞明白。于是求助了狗哥


到时候狗哥就可以大展神威了 。”那段注释是错的,你别管,就当没有!“


狗哥顺便还说了一句,”优秀的代码不需要写注释,也不知道是哪个XX 写的注释“,成功收割新同学的"钦佩"之情。


8. 二狗喜欢复制代码


狗哥写代码十分着急,根本来不及重构。他总是想到一段代码,就复制过来。神奇的是,狗哥经常这么写,但是也没出什么问题。


但新同学就惨了,在改完狗哥的代码后,总被测试同学背地里吐槽,“一点小需求咋这么多bug,跟狗哥比差远了”。原来新同学改了一处,忘了改另外几处,代码被复制了好多遍,他实在无法全面梳理。


于是每次代码写完,新同学都要不停的研究代码,总是害怕自己少改了哪些地方,下班时间越来越晚。并且新同学也不敢把雷同的代码重构到一起。(“你们猜猜他为什么不敢?)


慢慢的,组里的人都被迫向狗哥学习,狗哥成功输出了自己的编码习惯。


9. 二狗积极写技术方案,但是最终代码实现不按照技术方案来


二狗非常喜欢写技术方案,大部分时间都花在技术方案上,总是把技术方案打磨的 滑不留手。 但是在写代码时,狗哥总觉得按照方案设计写代码,时间上根本来不及啊,还是简单来吧,凑活实现吧。


例如狗哥曾经设计了一套复杂的Redis秒杀库存系统,但是实现时选择了最Low的 数据库同步扣减方案。


狗哥写的流程图和实际代码也没什么关系。 但是流程图旁边加满了注释和说明,让人觉得 ”这个技术方案很权威“。


新同学熟悉项目时,从公司文档中搜到了很多技术方案,本以为可以很快熟悉系统,但是发现技术方案和代码不太一样。越看越迷惑。


于是点了奶茶再次走向了狗哥,狗哥告诉他,“那个技术方案太复杂,排期紧张,开发来不及。你就当没那个技术方案。”


10. 二狗十分自信,从不打日志。


二狗对自己的代码十分自信,认为不会出现任何问题,所以他从来不打日志。每次开发代码时,狗哥的思维天马行空,但是从来不想加个日志会有助于排查问题。


直到有一天,线上真的出问题了,除了异常堆栈,找不到其他有效的日志。大家面面相觑,不知道怎么办。狗哥挺身而出,重新加了日志,上线。 故障持续了不知道有多久……,看着狗哥忙碌,领导不停地询问还需要多久才能上线。


复盘会上,有人对狗哥不写日志的行为进行批判,狗哥却在 狡辩 “加了日志,就能避免这次故障吗? 出问题还不是因为你们系统出了bug,跟我不打日志有啥关系。” 双方陷入了无限的扯皮之中……


11. 二狗积极学习,引用一个高大上的框架 解决一个小问题


二狗非常喜欢学习,学习了很多高大上的框架。最近二狗学习了规则引擎,觉得这是个好东西,恰好最近在进行重构。于是二狗把 drools、avatior、SPEL等规则引擎、表达式求值 等框架引入系统。只是为了解决策略模式的问题。即何种条件下使用哪种策略。 狗哥在系统架构图里,着重讲了规则引擎部分,十分自豪。


新同学熟悉系统后,光是规则引擎部分就看了足足一周。但是还是不知道怎么修改代码。于是向狗哥请教。狗哥告诉他说," 你在这个地方 加一行代码 rule.type == 12 ,走这个 CommonStrategy 策略类就可以了。“


新同学恍然大悟,原来这就是规则引擎啊。但是为什么不用策略模式呢?好像策略模式不费事啊! 狗哥技术就是强啊,杀鸡用核弹。


12. 二狗积极造轮子,能造轮子的程序员才是牛掰的程序员


二狗非常喜欢造轮子,他对开源软件的大神们心向往之,觉得自己应该向他们学习。狗哥认为 造轮子才能更快地成长。


于是在狗哥的积极学习下,组里的 分布式锁 没有使用 redission,而是自己用setnx搞的。虽然后面出了问题,但是狗哥的技术得到了锻炼。# 不用Redssion硬造轮子,结果翻车了…


总结


降低代码可读性的方式方法 包括但不限于以上12种;


像二狗这样的程序员包括但不限于二狗。


大家不要向二狗学习,因为他是真的。


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

突如其来的秋季反思

反思来的很突然,人随运走,兴由事发。 一切很突然,一切又有迹可循。 五月份时,Boss让我停掉一切研发事项,开始统筹变更管理;九月初,我从研发转为项目管理; 通俗来说,某些原因导致的医疗器械中的DMR变化,这些变化及其追溯即所谓变更管理 巨变之下,回顾了近两年...
继续阅读 »

反思来的很突然,人随运走,兴由事发。


一切很突然,一切又有迹可循。


五月份时,Boss让我停掉一切研发事项,开始统筹变更管理;九月初,我从研发转为项目管理;


通俗来说,某些原因导致的医疗器械中的DMR变化,这些变化及其追溯即所谓变更管理


巨变之下,回顾了近两年的历程,所思所想,记于下文。


养性与养气



20年冬季,身体不适,去看中医。诊断脉弦数,热邪亢盛,肝风内动之象。开了些药,听了一堆医嘱。


在狂奔的途中撞上了墙,一个踉跄,转身后,竟看到了歇斯底里的自己。



那时,我突然意识到,我在工作中走了歪路,并且已产生了很多不好的影响。用时下的词描述为 "极度精神内耗"



理想的书籍是智慧的钥匙 -- 托尔斯泰



与其这样内耗,不如先把个人技术提升的事情先放一放,将业余时间用来读一读书。于是买了一本想读很久的书《管子》。


我仍然记得,小学的苏教版课本上,有一句:仓廪实则知礼节,衣食足则知荣辱 语出 《管子-牧民》,老师给我们讲了管仲帮助齐桓公称霸的历史,并诵读了部分章句,并告诫我们以后有机会一定要读一读这部鸿篇巨著。


买这本书的理由很片面:找一本感兴趣又难读的书来磨性子。先秦文章,词句远比唐宋时期晦涩。理念振聋发聩,章句浩然磅礴,对我而言是不二之选。



读这本书的过程中,我开始思考公司的管理,团队的做事方式。并且真正理解一个道理:“不要陷在自己的世界中钻牛角尖,要去和高人探讨,如果不能和真人讨论,就去读高人的书籍”。



从这时起,开始了养性、养气。


作者按:方法上不必强求一致,如果读者诸君能够旁证自身,发现也应该做出自我调整,养性、养气,那么本章节就真正触达有缘人了



中庸中提到: 天命之谓性,率性之谓道,修道之谓教。喜怒哀乐之未发谓之中,发而皆中节谓之和。中也者,天下之大本也;和也者,天下之达道也。致中和,天地位焉,万物育焉。



通俗地讲,养性就是控制情绪,适度地释放,有节制,达到很平和的状态。而养气是养浩然气,知善恶辩是非明黑白,不可一味和稀泥。



居天下之广居,立天下之正位,行天下之大道;得志与民由之,不得志独行其道;富贵不能淫,贫贱不能移,威武不能屈,此之谓大丈夫。 -- 孟子 滕文公



价值证明的陷阱


再后来,我入职了新公司。此时我一直在思考一个问题:



如果在工作中花了很多心血和精力,但如何体现出价值 -- 价值证明问题



可能在大部分公司,都有这样的不利因素:需要打工人自己举证自己的价值


一旦陷入到这样的怪圈中,永远是吃亏的。



你如何证明自己本职工作做得很出色?


你如何证明你做了本质工作之外的内容,并对公司产生了价值?


你如何证明……



上面的BOSS无非是想用这种方式逼底层人内卷罢了,只要你去想了,你就输了。



公司的核心是商业化,不要奢求他人能管理好贪欲



目标契合与捆绑


而解法也不难,让上级无法否认你的价值即可!如果你所在的公司,上级很轻易就可以否定别人的价值,那么就可以考虑换工作了。


可以将目标分成两部分:



  • 一部分是明面上的,紧扣上级的考核点,对齐公司的核心价值,如果公司的核心价值很低,那么也可以考虑换工作了。

  • 另一部分是私下里的,用于个人成长。时下难以在一家公司干到退休,人总要成长。这部分目标是朝下一个职位的模板对齐的

    • 能融合进当前工作的,就将其打造为超预期

    • 不能融合的,就需要付出个人时间了




以这种方式切入,上级难以否定你所创造的价值(否则是自我否定),公司也难以否定全员价值。与此同时,自己也可以借机成长(达成自我目标)。不可否认,这一方式可以避免自己浪费精力,好钢永远用在刀刃上。


日常需要留意:



  • 商业画布,但一般难以接触,甚至没有明确

  • 业务布局、产品规划,用于分清主次

  • 市场分析、一般也难以接触,留心Boss们的分享

  • 各种大会,先听出基调和逻辑,用正说反说折中说去拆解话术,还原真实想法


结语


近半年,也常和朋友聊中年危机之类的话题,时常感慨万千,虽然还有几年才到年纪,但总要先做好准备。


这次的思考比较随性,并未仔细提炼主题并围绕行文,个人观点大体如下:



  • 大部分公司管理者认为程序员是"生产工具",并且利用各种方式让人成为高产的工具

  • 我们需要认识到这一点,并打破这一点。关键在于形成自我核心价值观、逻辑体系自洽。就可以免疫PUA等手段,并且不露于形色

  • 读书、读好书是一种有效方式

  • 规避自我证明价值这类陷阱

  • 用"目标契合与捆绑" 这一方式,在工作中不浪费精力


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

Web 版 PS 用了哪些前端技术?

web
经过 Adobe 工程师多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly + Emscripten、Web Components + Lit、Service Workers + Workbox 和新的 Web API 的支...
继续阅读 »

经过 Adobe 工程师多年来的努力,并与 Chrome 等浏览器供应商密切合作,通过 WebAssembly + Emscripten、Web Components + Lit、Service Workers + Workbox 和新的 Web API 的支持,终于在近期推出了 Web 版 Photoshop(photoshop.adobe.com),这在实现高度复杂和图形密集型软件在浏览器中运行方面具有重大意义!


图片


本文就来看看 Photoshop 所使用的 Web 能力、进行的性能优化以及未来可能的发展方向。


愿景:在浏览器中使用 Photoshop


Adobe 的愿景就是将 Photoshop 带到浏览器中,让更多的用户能够方便地使用它进行图像编辑和平面设计。过去几十年,Photoshop一直是图像编辑和平面设计的黄金标准,但它只能在桌面上运行。现在,通过将它移植到浏览器中,就打开一个全新的世界。


Web 版 Photoshop 承诺了无处不在、无摩擦的访问体验。用户只需打开浏览器,就能即时开始使用 Photoshop 进行编辑和协作,而不需要安装任何软件。而且,由于Web是一个跨平台的运行环境,它可以屏蔽底层操作系统的差异,使Photoshop 能够在不同的平台上与用户进行互动。


图片


另外,通过链接的功能,共享工作流变得更加方便。Photoshop文档可以通过URL直接访问。这样,创作者可以轻松地将链接发送给协作者,实现更加便捷的合作。


但是,实现这个愿景面临着重大的技术挑战,要求重新思考像Photoshop这样强度大的应用如何在Web上运行。


使用新的 Web 能力


最近几年出现了一些新的 Web 平台能力,可以通过标准化和实现最终使类似于Photoshop这样的应用成为可能。Adobe工程师们创新地利用了几个关键的下一代API。


使用 OPFS 实现高性能本地文件访问


Photoshop 操作涉及读写可能非常大的PSD文件。这要求有效访问本地文件系统,新的Origin Private File System API (OPFS) 提供了一个快速、特定于源的虚拟文件系统。



Origin Private File System (OPFS) 是一个提供了快速、安全的本地文件系统访问能力的 Web API。它允许Web应用以原生的方式读取和写入本地文件,而无需将文件直接暴露给Web环境。OPFS通过在浏览器中运行一个本地代理和使用特定的文件系统路径来实现文件的安全访问。



 
const opfsRoot = await navigator.storage.getDirectory();

使用 OPFS 可以快速创建、读取、写入和删除文件。例如:


 
// 创建文件
const file = await opfsRoot.getFileHandle('image.psd', { create: true });

// 获取读写句柄
const handle = await file.createSyncAccessHandle();

// 写入内容

handle.write(buffer);

// 读取内容
handle.read(buffer);

// 删除文件
await file.remove();

为了实现绝对快的同步操作,可以利用Web Workers获取 FileSystemSyncAccessHandle


这个本地高性能文件系统在浏览器中实现Photoshop所需的高要求文件工作流程非常关键。它能够提供快速而可靠的文件读写能力,使得Photoshop能够更高效地处理大型文件。这种优化的文件系统为用户带来更流畅的图像编辑和处理体验。


释放WebAssembly的强大潜力


WebAssembly是重新在JavaScript中实现Photoshop计算密集型图形处理的关键因素之一。为了将现有的 C/C++ 代码库移植到 JavaScript 中,Adobe使用了Emscripten编译器生成WebAssembly模块代码。


在此过程中,WebAssembly具备了几个至关重要的能力:



  • SIMD:使用SIMD向量指令可以加速像素操作和滤波。

  • 异常处理:Photoshop的代码库中广泛使用C++异常。

  • 流式实例化:由于Photoshop的WASM模块大小超过80MB,因此需要进行流式编译。

  • 调试:Chrome浏览器在DevTools中提供的WebAssembly调试支持是非常有用的

  • 线程:Photoshop使用工作线程进行并行执行任务,例如处理图像块:


 
// 线程函数
void* tileProcessor(void* data) {
// 处理图像块数据
return NULL;
}

// 启动工作线程
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, tileProcessor, NULL);
pthread_create(&thread2, NULL, tileProcessor, NULL);

// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

利用 P3 广色域


P3色域比sRGB色域更广阔,能够显示更多的颜色范围。然而长时间以来,在 Web 上sRGB一直是唯一的色域标准,其他更宽广的色域如P3并没有被广泛采用。


图片


Photoshop利用新的color()函数和Canvas API来充分发挥P3色域的鲜艳度,从而实现更准确的颜色呈现。通过使用这些功能,Photoshop能够更好地展示P3色域所包含的更丰富、更生动的颜色。


 
color: color(display-p3 1 0.5 0)

Web Components 提供UI的灵活性


Photoshop是 Adobe Creative Cloud 生态系统中的一部分。通过使用基于 Lit[1] 构建的标准化 Web Components 策略,可以实现应用之间 UI 的一致性。



Lit 是一个构建快速、轻量级 Web Components 库。它的核心是一个消除样板代码的组件基础类,它提供了响应式状态、作用域样式和声明性模板系统,这些系统都非常小、快速且具有表现力。



图片


Photoshop 的 UI 元素来自于Adobe 的 Web Components 库:Spectrum[2],该库实现了Adobe的设计系统。


Spectrum Web Components 具有以下特点:



  • 默认支持无障碍访问:开发时考虑到现有和新兴浏览器规范,以支持辅助技术。

  • 轻量级:使用 Lit Element 实现,开销最小。

  • 基于标准:基于 Web Components 标准,如自定义元素和 Shadow DOM 构建。

  • 框架无关:由于浏览器级别的支持,可以与任何框架一起使用。


此外,整个 Photoshop 应用都是使用基于 Lit 的 Web Components 构建的。Lit的模板和虚拟DOM差异化使得UI更新效率高。当需要时,Web Components 的封装性也使得轻松地集成其他团队的 React 代码成为可能。


总体而言,Web Components 的浏览器原生自定义元素结合Lit的性能,为Adobe构建复杂的 Photoshop UI 提供了所需的灵活性,同时保持了高效性。


优化 Photoshop 在浏览器中的性能


尽管新的 Web Components 提供了基础,但像Photoshop这样的密集型桌面应用仍然需要进行广泛的跟踪和性能优化工作,以提供一流的在线体验。


图片


使用 Service Workers 缓存资源和代码


Service Workers 可以让 Web 应用在用户首次访问后将其代码和资源等缓存到本地,以便在后续加载时可以更快地呈现。尽管 Photoshop 目前还不支持完全离线使用,但它已经利用了 Service Workers 来缓存其 WebAssembly 模块、脚本和其他资源,以提高加载速度。


图片


Chrome DevTools Application 面板 > Cache storage 展示了 Photoshop 预缓存的不同类型资源,包括在Web上进行代码拆分后本地缓存的许多JavaScript代码块。这些被本地缓存的JavaScript代码块使得后续的加载非常快速。这种缓存机制对于加载性能有着巨大的影响。在第一次访问之后,后续的加载通常非常快速。


Adobe 使用了 Workbox[3] 库,以更轻松地将 Service Worker 缓存集成到构建过程中。


当资源从Service Worker缓存中返回时,V8引擎使用一些优化策略:



  • 安装期间缓存的资源会被立即进行编译,并立即进行代码缓存,以实现一致且快速的性能表现。

  • 通过Cache API 进行缓存的资源,在第二次加载时会经过优化的缓存处理,比普通缓存更快速。

  • V8能够根据资源的缓存重要性进行更积极的编译优化。


这些优化措施使得 Photoshop 庞大的缓存 WebAssembly 模块能够获得更高的性能。


图片


流式编译和缓存大型WebAssembly模块


Photoshop的代码库需要多个大型的WebAssembly模块,其中一些大小超过80MB。V8和Chrome中的流式编译支持高效处理这些庞大的模块。


此外,当第一次从 Service Worker 请求 WebAssembly 模块时,V8会生成并存储一个优化版本以供缓存使用,这对于 Photoshop 庞大的代码尺寸至关重要。


并行图形操作的多线程支持


在 Photoshop 中,许多核心图像处理操作(如像素变换)可以通过在多个线程上进行并行执行来大幅提速。WebAssembly 的线程支持能够利用多核设备进行计算密集型图形任务。


这使得 Photoshop 可以将性能关键的图像处理函数移植到 WebAssembly,并使用与桌面端相同的多线程方法来实现并行处理。


通过 WebAssembly 调试优化


对于开发过程中的诊断和解决性能瓶颈来说,WebAssembly 调试支持非常重要。Chrome DevTools 具备分析 WASM 代码、设置断点和检查变量等一系列功能,这使得WASM的调试与JavaScript有着相同的可调试性。


图片


将设备端机器学习与 TensorFlow.js 集成


Photoshop 最近的 Web 版本包括了使用 TensorFlow.js[4] 提供 AI 功能的能力。在设备上运行模型而不是在云端运行,可以提高隐私、延迟和成本效益。



TensorFlow.js 是一款面向JavaScript开发者的开源机器学习库,能够在浏览器客户端中运行。它是 Web 机器学习方案中最成熟的选项,支持全面的 WebGL 和 WebAssembly 后端算子,并且未来还将可选用WebGPU后端以实现更快的性能,以适应新的Web标准。



“选择主题”功能利用机器学习技术,在图像中自动提取主要前景对象,大大加快了复杂选区的速度。


下面是一幅日落的插图,想将它改成夜晚的场景。使用了"选择主题"和 AI prompt 来尝试选择最感兴趣的区域以进行更新。


图片


Photoshop 能够根据 AI prompt 生成一幅更新后的插图:


图片


根据 AI prompt,Photoshop 生成了一幅基于此的更新插图:


图片


该模型已从 TensorFlow 转换为 TensorFlow.js 以启用本地执行:


 
// 加载选择主题模型
const model = wait tf.loadGraphModel('select_subject.json');

// 对图像张量运行推理
const {mask, background} = model.execute(imgTensor);

// 从掩码中细化选择

Adobe 和 Google 合作通过为 Emscripten 开发代理 API 来解决 Photoshop 的 WebAssembly 代码和 TensorFlow.js 之间的同步问题。这使的框架之间可以无缝集成。



由于Google团队通过其各种支持的后端(WebGL,WASM,Web GPU)改进了 TensorFlow.js 的硬件执行性能,这使模型的性能提高了30%到200%,在浏览器中能够实现接近实时的性能。



关键模型针对性能关键的操作进行了优化,例如Conv2D。Photoshop 可以根据性能需求选择在设备上还是在云端运行模型。


Photoshop 未来在 Web 上的发展


Photoshop 在 Web 上的普遍应用是一个巨大的里程碑,但这只是可能性的冰山一角。


随着浏览器厂商不断发展和完善标准和性能,Photoshop 将继续在 Web 上扩展,通过渐进增强来上线更多功能。而且,Photoshop 只是一个开始。Adobe计划在网络上积极构建其整个 Creative Cloud 套件,在浏览器中解锁更多复杂的设计应用。


Adobe 与浏览器工程师的合作将持续推动 Web 平台的进步,通过提升标准和改进性能,开发出更具雄心的应用。前方等待着我们的,是充满无限可能性的未来!



Photoshop 网页版目前可以在以下桌面版浏览器上使用:



  • Chrome 102+

  • Edge 102+

  • Firefox 111+



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

经济持续低迷环境下,女全栈程序员决定转行了

引言 疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。 近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据...
继续阅读 »

引言


疫情这几年,社会问题层出不穷,而在疫情放开之后,最头疼的就是民生就业问题,大厂裁员,小厂倒闭,每年大批量的应届毕业生也涌入就业市场。


近几日,统计局也发布了就业相关数据,全国失业青年达600多万,面对此数据,我们能想到的是实际的失业人数肯定会比公布的数据要多很多,尤其是表示 “一周工作一小时以上” 也纳入了就业范围。


image.png


而从我自己的判断来说,记得我自己在去年8月份被裁之后就在xhs发布了一篇关于个人如何交社保的教程,去年年底,观看浏览量不是特别多,而在今年(从年初至今)浏览量以及收藏量蹭蹭往上涨,几乎是每天都有人浏览和收藏我的帖子,抛去网上数据到底如何,光从我自己的感受来看,今年失业人数比去年更多!


image.png


个人只是随手发了一个帖子,将自己如何交社保的步骤记录下来,就有持续的搜索流量,这绝不是一件好事!说明了哀鸿遍野。


一面广大青少年正值青春鼎盛却面临着就业危机,另一方面还要忍受各种开支的骤增,比如深圳统租房的出现,大批人发声:微棠gun出深圳!



曾经破旧拥挤的城中村,为每一位打工人开启了大城市的入口,虽然这个入口短暂,且在关上门的时候,会毫不犹豫抹去你所有的痕迹。

而今这个入口,它不会再破旧拥挤,但会吸取你身上的最后一滴血。



个人经历


1.行政岗转前端


自己曾经拿着一个一本工科学历,因为厌倦行政岗位的勾心斗角,从而挑灯夜战每天在公司加班学习前端到11点,半路出家转行做了前端程序员。


2.刚转行遇吸血领导


而刚转行,又遇到了极其吸血的创业公司(大小周、从0到1项目,双周迭代迭代加班到2点)。


当时不敢辞职,不外乎有几个原因:



  • 刚转行,自己认为技术还比较菜,不敢辞职,被裁了之后才发现外面一大片天地

  • 真的很忙,根本没有时间提升自我与准备面试。因为呆了两年,我自己上了一次救护车,后来离职之后也发现自己因此得了疲劳综合症

  • 比较会吃苦,当时看来觉得可以忍一忍


关于这家公司呢,我想说,我这领导是真的狗,领导是我大一届的学长,曾经担任了大厂某知名项目的组长,号称协同领域的专家,关于此人是我生活中见过最资本的一个人:



  • 针对刚毕业的新人,不培养下属却对下属有着超乎大厂的要求(毫不夸张,你没经历过就不要觉得我是在夸张)

  • 技术部的同事都是很年轻的,做事都兢兢业业,不甩锅,不摸鱼,很多事都是自发的去解决,关于技术水平,我很客观的评价,不菜

  • 在裁我的时候,我呆的时间是13个月,也就是差一个月满2年,但他忽悠我说法律都规定只能给我1+1,我还不满2年,当时对方忽悠毫不脸红,又本着学长+平时看起来正人君子的偏见,在当时就签署了合同,失去的1个月补偿金还好,最伤人的是利用了你的信任,杀人诛心。


3.持续学习


从吸血公司出来之后,进入了相对比较wlb的公司,也清楚认识到自己在程序员领域,女性并不吃香,因此自己也是一直在学习前端技术。



  • 比如自己也曾在掘金发布了上百的技术文章

  • 买教学课程

  • 从零学算法,刷Leetcode

  • github持续输出代码

  • 建立自己的技术博客


image.png


image.png


4.努力不代表有收获


曾经相信自己勤能补拙,后来发现,比你拙的一大批还比你工资高;

曾经熟悉React技术栈,却在失业时找前端兼职时因不会vue而被刷;

曾经将网上的八股文背了再背,面试一二面对答如流,却倒在了三面面试官深问你项目经验;
曾经以为深耕项目经验,学性能优化、前端工程化、架构,却因为面试不会吹牛且遇上近几年经济低迷环境,工资还是那样。

曾经以为,自己努力点,自己性格好点,不断提升,会迎来比较好的人生。

曾经以为,男女平等,男生不应该一人承担经济压力,所以放弃了沉迷貌美如花,选择了与男生一样扛水桶,挑重活,但事实是,那些每天开开心心负责貌美如花的女生比我这种埋头搞钱的女生要幸福很多,对于像花一样的女生,谁不怜爱宽容呢,谁会去宽容一个扎在程序员堆里放弃自己容貌的黄脸婆呢。(看到这里,也许有人觉得我是因为自己长的太丑了,所以才选择搞钱,然而客观来分析,我自己并不丑,虽然说不是校花班花级别,但也可以在普通人群里说的是中上,颜控党眼里也能过得去,不是普信)



然后事实是,有些人,不用长得漂亮,不用能力强,不用对外提供情绪价值或其他价值,他站在那里,就有好的收获,就有人包容就有人爱。



在经历过上述的心理历程之后,明白了职场规则,以及社会运作规律,在大环境下,每个人都在尽自己的努力维持着公平,这个世界,因为有些人经历坎坷,未能坚守住自己底线,从而世界才会有坏人的存在。但大部分情况是,没有绝对的坏人,比如你觉得领导对自己很吸血,但可能领导背后的压力是整个公司的生存(虽然我的领导真的就是单纯的吸血),比如你觉得有些人对自己戾气重,可能当时人家真的内心极其痛苦,而你刚好撞到了枪口上,比如有些人因为诸多原因对你坏,但可能对别人好。


So,个人而言,还是做好自己,看淡所有的行为,同时能有自己的盾和矛。


决定转行


明白自己确实不适合长久做程序员,因此跟大家一样,网上搜了很多搞副业赚钱的路子,排除了偏门以及刑法上的路子,结合我自己的情况,目前已经开始正式着手Vlog自媒体之路了。



  • 买拍摄工具

  • 打造自己的IP

  • 整理自己的衣着、居住环境

  • 学习自媒体知识、拍摄技巧


总的而言,作为一个硬件工科出身的妹子,一直觉得自己更喜欢软件,比如硬件我要调试半天的电路我才能把一个灯泡💡点亮,而计算机,我写一行代码就可以得到反馈,即使是错误的,也能快速做出调整。


但也不可否认,女生在敲代码方面确实跟男生比没有那么大的天赋,就好比玩游戏,大部分女生会玩游戏,但是如果说要打的特别好,男生还是居多。


所以自己也很佩服那些在代码这条路上走的很坚定的女程序员。一起加油吧。


最后,我给各位女程序猿一个小建议,如果没有很高的学历背景或比较好的人脉资源运气,我觉得趁早搞一个副业,但是绝对不要裸辞去搞副业。程序员这个岗位虽然目前已经卷的不行,但瘦死的骆驼比马大,比某些天坑行业还是好很多,我觉得我们还是很幸运的。


掘金还没有评论置顶功能,就只能编辑在文章尾部了,更新:


这个文章呢,其实是在我自己很痛苦情况下写的,头痛+抑郁+想自杀,敲不进代码但不得不上班写的,另外,文章也只是阐述了我职场的不顺,还有其他很多方面都很痛苦,真实情况文章阐述不到十分之一,所以我真的劝各位键盘侠,别站在道德制高点来欺负一个跟你无冤无仇的人了,如今社会压力那么大,本身就导致抑郁症自杀的人那么多,戾气重可以理解,但别伤害别人,你只看到别人的一部分,不要成为压死骆驼的最后一颗稻草。真的,做个人吧。


另外,文章因用词不当引起的一些男女对立问题(这些不属于键盘侠,内容比较客观属于良好讨论,我也虚心接受),就当我是在放屁,我在评论区也虚心接受了这点,之后关于此类问题我就不回复了。


希望大家将焦点放在程序员职业发展方向上,一起谈谈中年之后的就业方向、副业等。


至于那些一上来就说玩流量、故意挑起战争、花瓶、网红的键盘侠们,在我没骂你之前就gun,真的,我脾气很爆...


作者:傲娇的萌
来源:juejin.cn/post/7246304095375097915
收起阅读 »

千里之行,始于发心

“千里之行,始于足下”,这两句话出自《道德经·第 64 章》,每个人小时候都会被问及:长大了想做什么?想成为什么样的人?我记得喜之郎之前有一则广告:长大后我要当太空人,爷爷奶奶可高兴了... 每个孩子都梦想着自己长大能够成为警察、科学家、作家、医生.........
继续阅读 »

“千里之行,始于足下”,这两句话出自《道德经·第 64 章》,每个人小时候都会被问及:长大了想做什么?想成为什么样的人?我记得喜之郎之前有一则广告:长大后我要当太空人,爷爷奶奶可高兴了...


每个孩子都梦想着自己长大能够成为警察、科学家、作家、医生......然而,当我们长大后,又有多少人能够实现自己的愿望呢?老子在道德经中点明了踏上成功之路的方法:千里之行始于足下。再类比到学习上来,难道不是这样吗?有了学习的目标还要有行动,立即开始就是迈向成功的第一步,也就是说要 “始于足下”。


老子说的“千里之行始于足下”,的确没错,但是我认为真正的千里之行,应该始于发心,只有我们拥有做好这件事的心,即便千里之行遇到各种困难,最终我们会坚持下去,直到成功的那一刻。


在开始分享学习方法之前,我们先思考一个问题:什么是学习?大家可以在脑海中过一遍,从上学到现在工作,我们基本都在不停地学习新的知识,看起来学习不就是一种行为嘛,那到底是怎样的一种行为?


我先给学习下个定义,它分为三个过程,第一个过程是理解,第二个过程叫记忆,第三个过程叫应用。一个事情你理解了,并且过了一段时间之后你记住了,再过一段时间后能熟练应用了,这才是一个完整的学习过程。不管少了哪个环节,学习都不能持久,最后的结果就是没学习到,这就是对学习的定义。


之前也看到不少小伙伴们在群里问 :编程怎么学啊?学完后又忘了!买了直播课听老师讲的时候都能听懂,自己写就不会了!我怎么这么笨啊!是不是脑子有什么问题啊等等。其实最主要的原因就是你以为自己学明白了,理解了,但其实你并没有真正的理解,你所谓的学习只停留在学习过程中第一个环节,只有当你理解了,记住了,并且能熟练应用了,这才能称你学会了。接下来给大家分享一个我用了两年的学习方法,个人觉得挺不错的,这也是世界公认的一个高效学习方法。


费曼学习法


这个方法我还是从我老师那里听来的,费曼学习法是由理查德·费曼提出来的,1965 年的时候也获得诺贝尔物理学奖,爱因斯坦曾说过:“If you can't explain it simply, you don't understand it well enough.” 意思就是说如果不能向他人简单解释一件事,就还没有真正弄懂它,如果你想弄清楚某个知识点,那就把它解释清楚!实际上,把自己正在学的知识教给他人,也正是费曼学习法的核心理念!费曼学习法是一种以教代学的学习方式。


假如我们通过直播课或者技术书学习某个前端技术,当学完某个知识点后,我们的大脑有可能会给自己一个错觉,就是自认为我学会了,学懂了,因为你还处在学习的一个过程理解阶段,真正的学习是分为三个阶段的理解、记忆、应用。检测自己是否真正学会了的方式就是利用费曼学习法,当你在给对方阐述的过程中,如果你对其中的知识点有不理解的地方,你会产生断层,会讲不下去,会给自己讲蒙了,当你发现讲不下去的时候,就是你对知识点理解不透彻的时候,然后回去接着学,学完之后再重新整理一遍继续给别人讲,直到可以重头到尾能给被人讲的明明白白了,甚至讲到被人听明白的时候,就证明你对这个知识点学习没有问题了,这就起到了查漏补缺的作用。为了让别人听得懂,首先自己得懂。在分享前,你会在大脑重新过一遍知识点,这就加深了对知识的理解,分享时,我们需要充分调动和提取大脑中的知识,这能够加强对知识的记忆和理解。


别着急记笔记


请大家先闭眼,在脑海中回想一个情景:语文老师正在讲课,讲台下的学生们握着笔杆子不停地写来写去,生怕错过一个知识点,仿佛只有全都记下来心里才会踏实,内心会有一丝欣慰......


接下来分享的学习方法也就一句话:第一次学习的时候不要记笔记,第一次学的时候,认真听,认真看,能记住多少算多少,就这么简单。


这是为什么呢?首先第一遍记的东西可能不是重点,会记很多笔记,第二个,我们需要了解一下我们的大脑,其实我们的大脑是非常喜欢省事的,当我们把知识点记到笔记本上,此时我们的大脑会记住,这些知识都记在本子上,就不会帮我们记在大脑里,我们如何将一个东西记得深刻,只有一个办法,就是让你的大脑不安全,当大脑没有安全感的时候,让你的内心无处安放的时候,反复记两遍准能记住,所以不要事事都给自己充分的安全感,尤其是记忆这个东西,当你记在本子上,笔记特别全,等你回顾的时候,什么也想不起来,而你记得最多就是这些知识点我记在本子上了,那么这些东西什么时候才能成为自己的呢?记都记不住,等到应用的时候怎么可能灵活呢!说这些不是说不让大家记笔记,只是第一遍学习不要记笔记,一些重点知识,容易忘的东西还是需要记下来的,以便之后的复习浪费时间。


我们应该放好自己的心态,也要认清一个事实:不管学任何东西都不要指望一遍就能学会学通,至少要抱着学两遍的心态去学习,所以第一遍的时候不要记笔记,能理解多少算多少,记不住没关系,等第二遍学的时候,看自己能想起哪些知识点,什么是清晰的?什么是模糊的?什么是根本想不起来的?然后把清晰的东西验证一遍,模糊的东西再学一遍,记不住的东西标记一下,这时就需要记笔记了,对于那些模糊记不住的重点记下来,这样的话,学习就能把握一个很好的节奏,知识重点拿下了,记不住的也拿下了,那以后基本上就是忘记这些东西了,以后忘了再看看就行了,这就要比你第一次记笔记牢靠得多,深刻得多,第一次记笔记觉得哪哪都是重点,内心也不放心,恨不得全都记下来。


我们需要经常这么锻炼,你越让自己内心不安全,战战兢兢,大脑越让你记忆深刻,不断这么训练自己,你的记忆力以后会非常强大。


保持独立思考


什么才是独立思考?比如说:当我们面对同样的信息时,有些人就能产出独到的见解,令人印象深刻。在做项目开发时,有的人就能给出新颖可行的方案,我们通常将这些人归结为懂得如何思考的人。


有这样一句话:“当我们一旦融入某个群体中,那么你就会传染上他们习惯以及思维方式,做出一些荒谬绝伦却毫不自知的事情”。


我们每天接收的信息量非常多,但大多数人在看到这些信息之前就已经停止了思考,无论是看新闻,还是刷抖音短视频,获得的知识都只是碎片化信息,但是很多人都把这些碎片化信息当成了知识的全部,缺乏思考的能力,某个博主或者专家说什么就是什么。在思考面前,我们停下了前进的脚步。孰不知,我们接收到的信息都是经过加工的,甚至我们看到那些到处炫耀生活的短视频,都是其他人经过包装后,潜移默化间灌输给我们的。当我们逐渐依赖其他人给出的“答案”时,我们的独立思考能力,就会在这种思维影响下一点一点地消失了。如果我们任其发展不反抗,无异于是对自我的扼杀。一个真正有思想的人,一定是懂得如何独立思考且拥有独立人格的人,他们看待事物不会透过有色眼镜歪曲揣测。而我们应该抱着敞开心扉,拥抱多元化的态度,以一种客观的方式分析遇到的每一件事,在反复的锤炼中,也许我们会发现自己已经不知不觉拥有了独立思考的能力。


坚持不懈


坚持这件事,归根结底就是意志力的较量,谁能挺到最后,谁就是胜利者。而常常放弃,或许不是因为我们无法坚持,而是给自己的退路太多。我觉得这对于 21 世纪的年轻人来说这是世界性的一个难题,我们大多很难去坚持一件事情,但是反观父母年代的人,他们貌似跟我们这个年代的人不太一样,他们坚持一件事情好像比我们容易点。这其实是有一个深刻的原因的,说简单点就是时代变化太快了,在父母那个年代生活条件有限,做什么都不方便,过节走个亲戚,一走就是几十里路,买个东西要走很远才能到镇上,收割庄稼基本都是人工,在父母那个年代里,生活节奏很慢,看起来干什么都像似在浪费时间,但是恰恰他们很勤奋,正是因为他们的生活节奏慢,所以他们的忍耐能力增加了,所以他们成长的快,他们可以接受一切比较慢的事情,需要坚持的事情,在父母那个年代,能上学,能读书,他们会觉得特别好,他们会珍惜这样的机会。


再看看我们现在的生活,一切都变得非常快,做什么都很方便,出门有滴滴打车,饿了有美团送上门,网速提升到 5G 了,但是像学习、减肥还像父母年代那样原始,依然需要我们付出时间和精力,它并没有随着时代的发展而被赋能,也没有随着时代的进步而被简洁化。但是请看看现在的我们,现在的我们变得浮躁了,没有办法接受一切慢的事物,在这种快节奏的时代下,那些需要花时间才能厚积薄发的事情我们如何坚持下去?那这是不是我们这个时代面临最严峻的问题?


世界上一切修行的方法都可以用金刚经里的一句话来总结,叫作善护念。就是说你想做任何事情,想在任何方面取得成就,无论是事业,还是爱情,都可以用善护念来直达最后的高度。什么是善护念呢?用一句话来说就是保护好我们的初心,一个人是否能够坚持下去,取决于自己的发心,他是真正热爱并且发自内心真正想做好这件事情,他坚持的动力都是从内心中生出来的,而不是说外在强加在身上的,更不是说我们喊着去坚持,但内心不想做的事情。


华严经里释迦摩尼也说过一句话:“众生皆具如来智慧德相”。每个人心中都有一座佛,每个人都想成功,那为什么就不能坚持做好一件事情?只因妄想执著,不能证得,各种各样的诱惑蚕食着本心,以至于走着走着却忘了自己的初心。六祖慧能受五祖弘忍大师点拨的时候恍然大悟,说了五句感慨了一下:“何期自性,本自清净;何期自性,本不生灭;何期自性,本自具足;何期自性,本无动摇;何期自性,能生万法”。其中第三句“何期自性,本自具足”跟王阳明的“圣人之道,悟性自足”其实是一样的意思,虽然说一个修佛,一个修儒家,但他们最终修炼到正果的时候,真正取得成果的时候,得出的结论几乎是一样的。只要能守护好自己的初心,坚持是件很简单的事情,最终的成功自然水到渠成。


最后的话


有一首诗叫做《纽约比加州时间早三小时》,它结尾处写道:


其实每个人在自己的时区有自己的步程。不用嫉妒或嘲笑他们。


他们都在自己的时区里,你也是!


生命就是等待正确的行动时机。


所以,放轻松。


你没有落后。


你没有领先。


在命运为你安排的属于自己的时区里,一切都准时。


我很喜欢这首诗,它时刻提醒着我,每个人都有自己的时区,不必着急,未来之路,愿与君共勉!🤝


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

《最近解决的一个bug与最近蹦出的一些想法》

0、一句话概括bug的原因 项目更换了邮箱服务器,原服务器支持的账号格式在新服务器上不被支持;即发送给新服务器的账号错误。 1、最近解决的一个bug (1)bug: java程序通知阿里云邮箱服务器发送邮件失败。 异常报错信息:AuthenticationFa...
继续阅读 »

0、一句话概括bug的原因


项目更换了邮箱服务器,原服务器支持的账号格式在新服务器上不被支持;即发送给新服务器的账号错误。


1、最近解决的一个bug


(1)bug:


java程序通知阿里云邮箱服务器发送邮件失败。

异常报错信息:AuthenticationFailedException: 526 Authentication failure[0]。


(2)背景:


弃用原邮箱服务器、更换为阿里云邮箱服务器后,所有版本的项目向服务器发出的邮件请求均无响应。


(3)排错:


AuthenticationFailedException,翻译过来就是认证不通过异常;认证不通过的原因一般是:服务器错误、用户名错误、用户名密码不匹配。

阿里云官方排错参考连接:阿里邮箱如何通过SMTP程序发信

使用参数在Foxmail中配置,可成功进行SMTP发信;这一步,确定了服务器无错、用户名无错、用户名与密码匹配。

那么,哪里出了问题?

翻阅官网原文: 



经排查,SMTP服务器配置、端口没有错误;那么问题就藏在代码逻辑和参数中。

当时对代码逻辑和参数并未产生质疑:代码延用的是之前对接服务器的部分;需要变动的参数都存在了数据库,并且这些参数在Foxmail上已被验证通过。把问题甩给阿里云人工,工程师查看操作日志后确定服务器接收的账号密码出错。基于出错点,重新复盘:服务器没问题,数据库的帐号密码没问题,那就是java程序处理后并向服务器发送的账号密码出了问题!

程序拿到了正确的帐号密码,却向服务器发送了错误的。在可能出错的代码块内排查:从src文件夹代码到hutool工具类库源码一路debug,发现阿里云邮箱服务器识别不了邮件账号;同样的代码逻辑,发送给原服务器的有效账号是“tairui”,而阿里云服务器需要的是“tairui@aliyun.com”。

最终重新拼接邮件账号字符串,问题解决。


2、最近蹦出的一些想法


(1)软件工程师,是一个什么样的职业?


软件工程师,听上去就是一群建库删库、增删改查数据、开发软件的哥们。

程序员可以像创造了一个又一个世界的操盘手。这个世界的规则都由他说了算:每个对象都是这个社会中的个体,每名个体通过传递消息建立他们的父子、兄弟、恋爱关系;每名个体的本质在于其所处的社会关系,整个社会的本质又是个体间关系的总和。

程序员也仅仅是社会分工的一个角色。他是一名与一个挖水沟的工人并没有太大区别的工人,同样从事着机械性的造轮子工作,同样为社会分工的目的而劳动。


(2)如何从事这样的职业?


跳过基础入门、背八股、刷面经的步骤,假设X已经顺利入职并从事着软件开发的工作,问初入职场的X如何在这个岗位上发热?

得意识到学习能力才是终身竞争力。剔除天赋、运气的因素,剩下的能让X在职场里披荆斩棘的可控因素中,主要因素就是学习能力。

得想明白程序员需要学习的到底是什么。语言是一个工具,框架更是;框架每年都在变,语言的核心思想却贯穿始终。X至少得吃透一门编程语言的教材,形成一个系统的编程思维,以便将来使用其他语言工具时能够一通百通。


(3)不断解决bug的感觉,就像精神鸦片,给平平无奇的工作添加了欢乐。


在毕业后工作满一年的时间跨度里,常常因为解决了一个问题而兴奋,不断地收获工作中的小确幸。

希望每一名劳动者能够在岗位上找到兴趣点,这就像是:在一个六年级毕业的暑假,午后阳光炙热,你怀揣着印着周杰伦半身像的雪碧,一路小跑到大伯家,按下乳白色主机和大屁股显示屏的开关键,伴随着XP系统的开机声急促地呼吸,在IE浏览器上输入www.4399.com;此刻,渴求的眼神、激动的指关节和涌上脸颊的绯红,让你忘记阳光的毒辣、酸胀的肌肉和在气管上切割的空气。

.

.

.

工作满一周年记

20230610 19:10


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

在 SwiftUI 中创建一个环形 Slider

iOS
前言 Slider 控件是一种允许用户从一系列值中选择一个值的 UI 控件。在 SwiftUI 中,它通常呈现为直线上的拇指选择器。有时将这种类型的选择器呈现为一个圆圈,拇指绕着圆周移动可能会更好。本文介绍如何在 SwiftUI 中定义一个环形的 Slider...
继续阅读 »


前言


Slider 控件是一种允许用户从一系列值中选择一个值的 UI 控件。在 SwiftUI 中,它通常呈现为直线上的拇指选择器。有时将这种类型的选择器呈现为一个圆圈,拇指绕着圆周移动可能会更好。本文介绍如何在 SwiftUI 中定义一个环形的 Slider。


初始化环形轮廓


ZStack中的三个圆环开始。一个灰色的圆环代表滑块的路径轮廓,一个淡红色的圆弧代表沿着圆环的进度,一个圆圈代表当前光标或拇指的位置。将滑块的范围设置为0.0到1.0,并硬编码一个直径和一个的当前位置进度 - 0.33。

struct CircularSliderView1: View {
let progress = 0.33
let ringDiameter = 300.0

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

var body: some View {
VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
}
.frame(width: ringDiameter, height: ringDiameter)

Spacer()
}
.padding(80)
}
}



将进度值和拇指位置绑定


将进度变量更改为状态变量并添加默认 Slider。这个 Slider 用于修改进度值,并在圆形滑块上实现足够的代码以使拇指和进度弧响应。当前值显示在环形 Slider 的中心。

struct CircularSliderView2: View {
@State var progress = 0.33
let ringDiameter = 300.0

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: 78, weight: .bold, design:.rounded))
}
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.shadow(radius: 3)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
}
.frame(width: ringDiameter, height: ringDiameter)


VStack {
Text("Progress: \(progress, specifier: "%.1f")")
Slider(value: $progress,
in: 0...1,
minimumValueLabel: Text("0.0"),
maximumValueLabel: Text("1.0")
) {}
}
.padding(.vertical, 40)

Spacer()
}
.padding(.vertical, 40)
.padding()
}
}
}


添加触摸手势


DragGesture 被添加到滑块圆圈,并且使用临时文本视图显示拖动手势的当前位置。可以看到 x 和 y 坐标围绕包含环形 Slider 的位置中心的变化情况。

struct CircularSliderView3: View {
@State var progress = 0.33
let ringDiameter = 300.0

@State var loc = CGPoint(x: 0, y: 0)

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

private func changeAngle(location: CGPoint) {
loc = location
}

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: 78, weight: .bold, design:.rounded))
}
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.blue)
.shadow(radius: 3)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
.gesture(
DragGesture(minimumDistance: 0.0)
.onChanged() { value in
changeAngle(location: value.location)
}
)
}
.frame(width: ringDiameter, height: ringDiameter)

Spacer().frame(height:50)

Text("Location = (\(loc.x, specifier: "%.1f"), \(loc.y, specifier: "%.1f"))")

Spacer()
}
.padding(.vertical, 40)
.padding()
}
}
}


为不同的坐标值设置滑块位置


圆形滑块上有两个表示进度的值,用于显示进度弧度的progress值和用于显示滑块光标的rotationAngle。应该只有一个属性来保存滑块进度。视图被提取到一个单独的结构中,该结构具有圆形滑块上进度的一个绑定值。


滑块的range的可选参数也是可用的。这需要对进度进行一些调整,以计算已设置的角度以及拇指在圆形滑块上位置的旋转角度。另外调用onAppear根据View出现前的进度值计算旋转角度。

struct CircularSliderView: View {
@Binding var progress: Double

@State private var rotationAngle = Angle(degrees: 0)
private var minValue = 0.0
private var maxValue = 1.0

init(value progress: Binding<Double>, in bounds: ClosedRange<Int> = 0...1) {
self._progress = progress

self.minValue = Double(bounds.first ?? 0)
self.maxValue = Double(bounds.last ?? 1)
self.rotationAngle = Angle(degrees: progressFraction * 360.0)
}

private var progressFraction: Double {
return ((progress - minValue) / (maxValue - minValue))
}

private func changeAngle(location: CGPoint) {
// 为位置创建一个向量(在 iOS 上反转 y 坐标系统)
let vector = CGVector(dx: location.x, dy: -location.y)

// 计算向量的角度
let angleRadians = atan2(vector.dx, vector.dy)

// 将角度转换为 0 到 360 的范围(而不是负角度)
let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians

// 根据角度更新滑块进度值
progress = ((positiveAngle / (2.0 * .pi)) * (maxValue - minValue)) + minValue
rotationAngle = Angle(radians: positiveAngle)
}

var body: some View {
GeometryReader { gr in
let radius = (min(gr.size.width, gr.size.height) / 2.0) * 0.9
let sliderWidth = radius * 0.1

VStack(spacing:0) {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9),
style: StrokeStyle(lineWidth: sliderWidth))
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: radius * 0.7, weight: .bold, design:.rounded))
}
// 取消注释以显示刻度线
//Circle()
// .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.6),
// style: StrokeStyle(lineWidth: sliderWidth * 0.75,
// dash: [2, (2 * .pi * radius)/24 - 2]))
// .rotationEffect(Angle(degrees: -90))
Circle()
.trim(from: 0, to: progressFraction)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: sliderWidth, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.shadow(radius: (sliderWidth * 0.3))
.frame(width: sliderWidth, height: sliderWidth)
.offset(y: -radius)
.rotationEffect(rotationAngle)
.gesture(
DragGesture(minimumDistance: 0.0)
.onChanged() { value in
changeAngle(location: value.location)
}
)
}
.frame(width: radius * 2.0, height: radius * 2.0, alignment: .center)
.padding(radius * 0.1)
}

.onAppear {
self.rotationAngle = Angle(degrees: progressFraction * 360.0)
}
}
}
}


CircularSliderView 的三种不同视图被添加到View中以测试和演示 Circular Slider 视图的不同功能。

struct CircularSliderView5: View {
@State var progress1 = 0.75
@State var progress2 = 37.5
@State var progress3 = 7.5

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.06, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
CircularSliderView(value: $progress1)
.frame(width:250, height: 250)

HStack {
CircularSliderView(value: $progress2, in: 1...10)

CircularSliderView(value: $progress3, in: 0...100)
}

Spacer()
}
.padding()
}
}
}


总结


本文展示了如何定义响应拖动手势的圆环滑块控件。可以设置滑块视图的大小,并且滑块按预期工作。可以向控件添加更多参数以设置颜色或圆环内显示的值的格式。


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

DNS

DNS DNS:Domain Name System 域名系统,应用层协议,是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网,基于C/S架构,服务器端:53/udp, 53/tcp实际上,每一台 DNS 服务器都...
继续阅读 »

DNS


DNS:Domain Name System 域名系统,应用层协议,是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网,基于C/S架构,服务器端:53/udp, 53/tcp实际上,每一台 DNS 服务器都只负责管理一个有限范围(一个或几个域)内的主机域 名和 IP 地址的对应关系,这些特定的 DNS 域或 IP 地址段称为 zone(区域)。根据地址解 析的方向不同,DNS 区域相应地分为正向区域(包含域名到 IP 地址的解析记录)和反向区 域(包含 IP 地址到域名的解析记录)

根域: 全球根服务器节点只有13个,10个在美国,1个荷兰,1个瑞典,1个日本


  • 一级域名:Top Level Domain: tld
  • 三类:组织域、国家域(.cn, .ca, .hk, .tw)、反向域
  • com, edu, mil, gov, net, org, int,arpa
  • 二级域名:magedu.com
  • 三级域名:study.magedu.com
  • 最多可达到127级域名

ICANN(The Internet Corporation for Assigned Names and Numbers)互联网名称与数字地址分配机构,负责在全球范围内对互联网通用顶级域名(gTLD)以及国家和地区顶级域名(ccTLD)系统的管理、以及根服务器系统的管理


DNS服务器类型


  • 缓存域名服务器:只提供域名解析结果的缓存功能,目的在于提高查询速度和效率, 但是没有自己控制的区域地址数据。构建缓存域名服务器时,必须设置根域或指定其他 DNS 服务器作为解析来源。
  • 主域名服务器:管理和维护所负责解析的域内解析库的服务器
  • 从域名服务器 从主服务器或从服务器"复制"(区域传输)解析库副本

序列号:解析库版本号,主服务器解析库变化时,其序列递增

刷新时间间隔:从服务器从主服务器请求同步解析的时间间隔

重试时间间隔:从服务器请求同步失败时,再次尝试时间间隔

过期时长:从服务器联系不到主服务器时,多久后停止服务

通知机制:主服务器解析库发生变化时,会主动通知从服务器


DNS查询类型及原理


查询方式

  • 递归查询:一般客户机和本地DNS服务器之间属于递归查询,即当客户机向DNS服务器发出请求后,若DNS服务器本身不能解析,则会向另外的DNS服务器发出查询请求,得到最终的肯定或否定的结果后转交给客户机。此查询的源和目标保持不变,为了查询结果只需要发起一次查询。(不需要自己动手)

  • 迭代查询:一般情况下(有例外)本地的DNS服务器向其它DNS服务器的查询属于迭代查询,如:若对方不能返回权威的结果,则它会向下一个DNS服务器(参考前一个DNS服务器返回的结果)再次发起进行查询,直到返回查询的结果为止。此查询的源不变,但查询的目标不断变化,为查询结果一般需要发起多次查询。(需要自己动手)


查询原理过程


正向解析查询过程:

1 先查本机的缓存记录

2 查询hosts文件

3 查询dns域名服务器,交给dns域名服务器处理 以上过程称为递归查询:我要一个答案你直接会给我结果

4 这个dns服务器可能是本地域名服务器,也有个缓存,如果有直接返回结果,如果没有则进行下一步

5 求助根域服务器,根域服务器返回可能会知道结果的一级域服务器,让他去找一级域服务器

6 求助一级域服务器,一级域服务器返回可能会知道结果的二级域服务器让他去找二级域服务器

7 求助二级域服务器,二级域服务器查询发现是我的主机,把查询到的ip地址返回给本地域名服务器

8 本地域名服务器将结果记录到缓存,然后把域名和ip的对应关系返回给客户端


DNS的分布式互联网解析库 



正向解析


各种资源记录


区域解析库:由众多资源记录RR(Resource Record)组成

记录类型:A, AAAA, PTR, SOA, NS, CNAME, MX


  • SOA:Start Of Authority,起始授权记录;一个区域解析库有且仅能有一个SOA记录,必须位于解析库的第一条记录SOA,是起始授权机构记录,说明了在众多 NS 记录里哪一台才是主要的服务器。在任何DNS记录文件中,都是以SOA ( Startof Authority )记录开始。SOA资源记录表明此DNS名称服务器是该DNS域中数据信息的最佳来源。
  • A(internet Address):作用,域名解析成IP地址
  • AAAA(FQDN): --> IPV6
  • PTR(PoinTeR):反向解析,ip地址解析成域名
  • NS(Name Server):,专用于标明当前区域的DNS服务器,服务器类型为域名服务器
  • CNAME : Canonical Name,别名记录
  • MX(Mail eXchanger)邮件交换器
  • TXT:对域名进行标识和说明的一种方式,一般做验证记录时会使用此项,如:SPF(反垃圾邮件)记录,https验证等

安装配置实操


下载安装bind文件,关闭防火墙进行后续操作

cd到etc下的文件夹查询对应软件,编辑named.conf文件




wq保存退出,接下来修改named.rfc1912.zones文件




wq保存退出后cd到var/named的文件下,复制local的文件作为自定义网站的文件模板进行编辑



wq保存退出之后检查有效性,首先修改网卡DNS为当前主机地址然后重启



 接下来开启程序systemctl start named




反向解析


和正向相似,在named.rfc1912文件下添加一段命令之后重新创建一个zones文件,将类型A换成PTR




主从复制


首先需要两台服务器,以我自己的192.168.222.100和192.168.222.200为例

进入etc下的named.conf文件修改两个any




修改etc下的named.rfc1912文件


 

 复制一份named.localhost作为模板进行修改
 

对网卡配置进行修改,然后重启网卡和启动named程序 


 接下来对第二台从服务器进行修改
同上,对named.conf文件进行修改两个any



 接下来修改从服务器的rfc文件




此时自动在slave文件夹下生成主服务器的文件



 修改主服务器的配置,双方重启后从也会变更









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

在前端领域摸爬滚打7年,我终于掌握了这些沉淀技巧

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。 所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序...
继续阅读 »

我做开发多年,常常有人问我「软件开发难学吗?」「前端和后端哪个比较简单?」「培训后是否好找工作呢?」这些问题单拎出来比较棘手,三言两语说不清楚,需要你对开发有一个系统了解,问题才能迎刃而解。


所以,我想和你分享我的学习和工作经历,希望这对于正在准备成为一名程序员的你有所帮助。


我的经历可能会为新手提供一些有用的建议和思路。


01 萌芽之初,点燃编程学习的梦想


对于一些90后的朋友来说,网游填满了他们的高中时期,甚至是初中。


他们经常因为不走寻常路去打游戏,在回来时被门卫大爷逮个正着。尽管我没有沉迷于游戏,但我仍然被游戏所吸引。


在游戏中,我一直认为只有玩家和 NPC 的存在,但是,玩得越多,你会发现还有一些不寻常的角色,那就是“工作室”。部分“工作室”利用一些技术手段批量、自动地在游戏中完成任务以赚取游戏产出。


虽然这种行为不可取,但是他们使用的技术确实让我感兴趣。


这时候,代码的种子已经悄悄埋藏在我的内心深处,等待发芽。


高中毕业后,卸下学业负担,我开始利用暑期学习了一些脚本精灵、Tc 简单编程和易语言编程,这也是我第一次接触编程基础语法,如条件判断、循环、遍历和条件选择,再加上社区提供的一些识图插件,我就像一个蹩脚的裁缝,东拼西凑,左缝右补,费劲巴拉缝制成一件衣服,却不合身。


虽然实现了自动登录游戏的功能,但很不幸运的是,这样的小功能也还是过不去游戏的自检程序,万物皆有裨益,万事皆可为师,正是这一次编程体验促使了我后来的专业选择。


02 踏上编程学习之路,从安卓到前端,每一步都算数


英语是我成长路上的一块绊脚石,在选择专业时,我想躲开英语,于是选择了同为计算机系下的软件外包服务专业,结果发现,只要是技术,英语的要求都是一样的。


当然,我选择这个专业还有另外一个动机 -- 它开设了Android课程。毕竟,那时我刚拿到一款安卓手机,能在手机上开发自己的App是何等酷炫的体验啊!


那时,有一本厚重的《疯狂 Android 讲义》成了我的启蒙之书,我翻过无数遍,上课、参加编程比赛、实习工作、这本书我一直在用,为我第一份工作立下了汗马功劳。


临近毕业,是先就业还是先培训,许多软件相关专业的毕业生都面临着这样的选择。


所以,你要想明白,你到底需要的是什么?


我选择参加培训是出于两个原因:第一是为了将平时自学的知识整合起来,第二是希望能够认识更多的小伙伴,以便进行技术交流。编程最忌讳的就是闭门造车,不进行沟通交流。


然而,选择参加培训并不是每个人的选择。


如果你有能力自己阅读技术书籍,并且知道如何获取最新的技术信息,那么参加培训完全没有必要。


只有当你需要别人的指点和帮助来梳理技能,或者需要更好的机会来进行技术交流时,参加培训才是一个好的选择。


但是,如果你仅仅因为听说培训完就能很赚钱而选择花钱加入,那么你就要好好思考一下了,周围打水漂的人确实不在少数。


培训结束后,2015 年 12 月 7 号,我入职了第一家公司,担任 Android 开发工程师。


人生有时候做一个决策,一个行动,当时只道是寻常,当它的价值在未来某一刻兑现时,你会感谢当时努力的自己。


如果没有大学时翻过无数遍的《疯狂 Android 讲义》,我不可能找到这份工作。


03 学前端到底在学什么


工作后,我第一次真正进入团队开发模式(我是不会告诉你我当初使用百度云盘定时同步代码的,炸过一次硬盘),由于业务需要一定的前端支持(合同模板),所以在一次小组会议上,组长建议我们要着手学习前端技术(Angular1.x)。


到了17年左右,公司的业务开始由原 Pad 端转移到手机端。我和其他几个新入职的小伙伴经过一上午的 Vuejs2.x 培训后,就开始上手开发了。


也是在这次前端项目开发中,我第一次接触到了闭包导致循环失灵的问题,第一次把一个页面写到 3 千多行(烂,不懂拆分)。


由于这次前端项目开发的经验不足,导致迭代两年后,项目能编译出 200MB 的内容。我只能通过各种查找和大量的 webpack 参数调试,将产物压缩回了20MB 左右。对于我来说,这也是一次很大的成长。


我非常推荐各位小伙伴在工作中多承担,因为开发经验绝非是你熟背八股题得到的,开发经验只能是来自大量的项目实战。


多做练习,多遇困难,多做总结,得到的才是自己的。开发经验决定了你的下一个项目能否走得更顺利。


选择成为前端程序员是一件比较苦的事情,因为这个领域的技术更新非常频繁,如果你不持续学习,那么你就会落后,这也是“前端很累”的一个根本原因。


实际上,现在还有一些人对前端存在偏见,因为他们认为不就一个 JavaScript,能有多难?


但是事实上,很多前端构建技术的底层实现并不是用 JavaScript 语言编写的,而是基于了其它编程语言如 Golang(代:ESBuild)和Rust(代表:SWC)“包装”起来的,利用这些语言的特点来弥补 JavaScript 的不足。


前端学习的基础是 JavaScript,但不仅仅是 JavaScript,如果你认为学习 JavaScript 就是学习前端,那么你可能会走进死胡同。


04 正确的学习编程方式一定是这样的


在学校里,老师一定告诉过你两个正确的学习方式,其中一个是要做笔记,另一个是要能够向同学清晰地讲解。


繁多的技术是不可能靠记忆实现的,因此做笔记和写博客是记录学习过程和分享学习成果的捷径。


现在,我也发现很多在校的同学积极在各大技术社区分享自己的学习经验,这也印证了这条成长途径的正确,同时也激励我们这些已经做了多年程序员的伙伴要更加努力。


不论你是学习新的编程语言还是新的框架,都需要为其配置对应环境,但有很多框架的环境配置其实对于第一次接触的小伙伴来说并不友好,就比如我最初在从Android转前端的时候就因为安装NodeJsNpm这些东西而烦恼,因为当时莫名其妙就提示你Python2的模块找不到了,要不就是安装依赖超时了,在环境搭建问题上花费太长时间真的不划算。


为了避免环境搭建影响学习进度,我们可以使用一些在线的 IDE 环境,例如 CodePen、CodeSandBox、Stackblitz、JSRun 等。


但是,它们在依赖安装、操作习惯和响应速度上仍然有一些上手难度。


我最近一段时间一直在使用 1024Code  社区提供的在线 IDE,它提供了很多热门语言和框架的代码空间模板,免配置环境,即开即用随时学习新技术。


它支持多人开发和在线分享,无论是和朋友一起开发项目还是找大佬请教问题,都非常轻松。


05 学习编程,高效沉淀需要技巧


我发现之前写博客时做的案例很难沉淀下来。往往只是写完一遍,很少再打开运行。


但是在 1024Code 中,可以以卡片的形式记录每一个案例,也可以将一系列案例放到一个集合中归类。


此外,1024Code 还支持在个人主页中渲染 Markdown,为小伙伴打造炫酷的个人主页提供了便利。


最令人赞叹的是,1024Code 紧跟最近比较火的 ChatGPT,将其接入到了 IDE 中,让你在编码的同时可以更快速地查找解决方案。下面我给大家简单地展示一下:


在社区主页中,案例以卡片的形式展示。你可以点击你感兴趣的案例,一键运行。边浏览源码,边跟着作者提供的 README 进行学习。


如果你想在此基础上练习或二次开发,还可以 fork 一份到自己的工作空间。如果你发现作者的代码有不合理的地方,还可以在评论区大胆地给他留言,大家可以共同成长。



1024Code 提供了众多空间模板,涵盖了多种编程语言和框架,例如针对数据统计和 AI 模型训练的 Python,以及让许多程序员感到头疼的 C++。


此外,它还支持其它主流的热门编程语言和框架。



Markdown 是编程小伙伴们最常用的笔记格式之一,因此无需专门学习其语法。只需要多看几遍,就可以自然而然地掌握。


此外,你还可以参考社区中其他小伙伴的主页,来打造自己独特的个人主页。



接下来,我要展示一段时间以来我制作的合集。


最初,这个合集是为了帮助那些不熟悉滴滴 LF 框架如何使用 Vue3+TS 编写的小伙伴们而制作的。


我还将合集地址提交到了 LF 仓库,希望能够帮助那些正在转向 Vue3+TS 的小伙伴们。



最重磅的就是 ChatGPT 了。


在使用 1024Code 的 IDE 进行开发过程中,如果遇到问题,你可以快速打开 ChatGPT 来协助你查找答案,而不需要离开当前页面。


ChatGPT 支持上下文连续问答模式,虽然它不能解决你所有的问题,甚至会给出错误的答案,但对于一些常规类编程问题或正在做毕业设计的小伙伴们,它还是能够显著提升效率的。



总结


最后,我再为你做一些总结、建议和对未来的期待:

  • 我建议你要有很强的动力来学习编程,因为坚持并不是易事;

  • 我建议你坚守自己慎重选择的专业,因为不忘初心方得始终;

  • 我建议你在面对技术培训时要清醒认知,因为明确目标的选择才适合自己;

  • 我建议你在工作中抓住一切学习的机会,因为努力的人很多,只有不断学习才能跟上技术的发展;

  • 我建议你在编程学习时要善用工具、做好笔记、写博客,不断沉淀自己的知识和经验;


最后的最后,愿我们所有付出都将是沉淀,所有美好终会如期而至。


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

再聊聊秋招焦虑

我想,关于秋招,大家是真的焦虑的。 作为一个刚起了两个月的公众号,平时写写技术相关的内容,阅读量少则几十,多则也不超过500。结果一聊到秋招这个话题,阅读量直接干到27000去了,让我硬生生地过了一把大V的瘾。(手动狗头) 现代版的“国家不幸诗家幸,赋到沧桑...
继续阅读 »

我想,关于秋招,大家是真的焦虑的。


作为一个刚起了两个月的公众号,平时写写技术相关的内容,阅读量少则几十,多则也不超过500。结果一聊到秋招这个话题,阅读量直接干到27000去了,让我硬生生地过了一把大V的瘾。(手动狗头)



现代版的“国家不幸诗家幸,赋到沧桑句便工”有没有?


所以,我决定再聊十块钱的“秋招焦虑”这个话题,以一个过来人的身份,开导开导同学们。如果同学们真的干了这碗鸡汤后,觉得暖心暖胃,精神上舒服一些,那也不枉我敲这么多字了。


首先说下,产生焦虑的原因是什么?


参照了知乎上的众多答案,我觉得最精辟的一个答案是:担心某件不好的事情,在未来的某个时间点将会发生,但主观上又拒绝接纳。


而往往焦虑的人的认知特点是:


  • 高估不好的事情的发生概率;
  • 高估不好的事情所带来的后果;
  • 低估自己应对挫折的能力;

我们把这种认知特点带入到秋招中,同学们的想法大概是这样:


卧槽,今年校招这么卷,完全就是一片哀鸿遍野的景象,那我肯定找不到工作了。一旦形成毕业即失业的情况,那我前面二十多年爬冰卧雪、寒窗苦读就完全看不到价值了。试问读书的意义是什么呢?完了,生不逢时啊,我这辈子基本上也就交待了。


而在这种焦虑的情绪下,最常见的应激反应就是回避行为。即:有条件的打算润到国外,没条件的誓死考公。


下面,开始灌鸡汤,各位同学听好。


超过80%的人,你不必焦虑


其实各行各业都一样,你只要能超过行业内80%的人,基本上就没什么好担心的,降低一些预期,找到一份差不多的工作肯定没问题。


逻辑是这样的,如果一个行业的top 20%都凉凉了,那证明这个行业不仅仅是不景气,而是被这个时代所淘汰了。如果真的到了这个地步,那最焦虑的肯定不是作为个体的你了。


按照top 20%这个人物画像进行圈选的话,很多专业能力不错,有实习经历的211 985同学,其实是都在其内的。


你如果还在焦虑的话,那就是世上本无事,庸人自扰之了。



当然,再说一句,如果你不在这20%的池子里面,那你还轮不到焦虑,你最先做好的的是四个字:反求诸己。


反求诸己,很多事情上都是这么一个道理,就是苦练基本功。


正常周期变化,你无须焦虑


一般情况下,经济和行业周期大概在8到10年左右。任何人漫长的职业生涯中,都会经历几个起起伏伏的过程,有时候早经历一次,未必是坏事。


郭德纲的原话是这样说的:


吃亏要趁早,一帆风顺不是什么好事。从小大伙娇生惯养,没有人跟他说过什么话,65岁走在街上谁瞪他眼就会猝死。从出生开始一天打八个嘴巴,这样的到25岁就是铁罗汉、活金刚一样什么都不在乎,吃亏要趁早。


其实话糙理不糙,当你经历了萧条的经济周期,再遇到经济转好,行业内一片欣欣向荣的时候,你会怀着更加感恩的心态,拥有更成熟的理性思考,再也不会把风口机遇和平台资源当做个人能力。


说得粗鄙一点儿就是,变得更加有逼数儿了,不好高骛远,不过度消费,这样反而让你以后的路走得更平稳一些。



不就是一次秋招吗,你焦虑个毛


有的同学性格过于敏感脆弱,总是会把一件不好的事情的后果,弄得跟世界末日一样。


其推理过程大概这样:


少了一枚铁钉,掉了一只马掌。掉了一只马掌,失去一匹战马。失去一匹战马,失去一场战役。败了一场战役,毁了一个王朝。



等于“千里之堤,毁于蚁穴”的道理,让他给用到这里了。


其实真的不用这么想,就算秋招不太理想,还有春招,春招就算也不理想,直接走社招投简历不就完了。退一万步说,大不了转行,只要你不懒不傻不笨,照样能在别的行业混得风生水起。


真的真的没必要,认为秋招GG了,整个人生都黯淡了。人这一辈子很长,有很多翻盘的机会,也有很多盛极而衰的事例,没必要计较一时之成败。


像男人一样,扛过焦虑期


永远不要低估人类应对挫折和低谷的能力。


想想我们的父母辈,正好赶上了国企的下岗潮,捧了半辈子的铁饭碗丢了。就像刘欢的那首《重头再来》所唱到的那样,”辛辛苦苦已度过半生,今夜重又走进风雨“。


但是,基本上没看到下岗的哪家哪户断粮饿死,最不济的也是出去打工或者做点儿小生意,还有的因此而混出一些名堂出来呢。


就像曾国藩的那句名言所说的一样:生平长进,全在受挫受辱之时,需咬牙立志,受不得穷,立不得品,受不得屈,必做不得事。


我们再来看看褚时健老爷子,以前是云南红塔集团的董事长,后来由于贪污受贿被判处无期徒刑,和妻子双双入狱,女儿自杀,他的低谷堪称绝望之谷。


2002年,74岁出狱二次创业,开始做褚橙。到了2014年,”褚橙“销售额达到了一亿多元,纯利润7000多万。因此,人们又称它为”励志橙“。



难怪见过大世面的王石都这样感慨道:


橙子挂果要6年,他那时已经75岁高龄了。试想一下,一个75岁的老人,戴一个大墨镜,穿着颇圆领衫,兴致勃勃地跟我谈论橙子挂果是什么场景。我当时就想,如果我遇到他那样的挫折,到了他那个年纪,我会想什么?我知道,我一定不会像他那样勇敢。


所以,像个男人一样,顶住压力,行动起来吧。扛过这段焦虑期吧,阴霾只是暂时的,未来终将是美好的。


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

Xcode 升级到14.3以后 调试与打包遇到的坑

iOS
前言 是苹果逼的,通知说2023年4月25日之后,所有的App都要在iOS16的SDK上打包。不然也不会有那么多事情(呜呜呜🥹)。 1.Xcode 14.3版本运行项目报错 问题如下:ld: file not found: /Applications/Xcod...
继续阅读 »

前言


是苹果逼的,通知说2023年4月25日之后,所有的App都要在iOS16的SDK上打包。不然也不会有那么多事情(呜呜呜🥹)。


1.Xcode 14.3版本运行项目报错


问题如下:

ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a
clang: error: linker command failed with exit code 1 (use -v to see invocation)

报错信息看,都是在链接库的时候因为找不到静态库(libarclite_iphonesimulator.a/libarclite_iphoneos.a)而报错。利用访达的前往文件夹功能快速来到报错信息中的目录,发现连 arc目录都不存在,更不用说静态库文件。


开发人员解释说,因为系统已经内置有 ARC相关的库,所以没必要再额外链接,至少Xcode 14支持的最低部署目标iOS 11及以上版本的系统肯定是没问题的。如果应用部署目标不低于iOS 11还出现问题,那么应该是第三方库的部署目标有问题。


所以解决方案也很清晰了,将所有依赖库和应用最低部署版本都限制在iOS11以上即可。


解决方案:

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end

2. 升级Xcode14以后项目报错 Stored properties cannot be marked potentially unavailable with '@available'


这是依赖库报错,把其中一个库升级到了最新的版本,不报错了。但是还有一个库没办法升级,因为我们的项目是Flutter项目,不知道是哪个三方库的依赖库,百度了好久没找到办法,最后还是强大的Google到方法:


在iOS目录下:

执行pod install
然后再执行pod update

最终可以了


3. Xcode升级到14.3 archieve打包失败

mkdir -p /Users/hsf/Library/Developer/Xcode/DerivedData/Ehospital-crirdmppgluxkodauexhkenjuxet/Build/Intermediates.noindex/ArchiveIntermediates/Ehospital/BuildProductsPath/Release-iphoneos/复旦云病理.app/Frameworks
Symlinked...
rsync --delete -av --filter P .*.?????? --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/AliyunOSSiOS.framework" "/Users/hsf/Library/Developer/Xcode/DerivedData/Ehospital-crirdmppgluxkodauexhkenjuxet/Build/Intermediates.noindex/ArchiveIntermediates/Ehospital/InstallationBuildProductsLocation/Applications/复旦云病理.app/Frameworks"
building file list ... rsync: link_stat "/Users/hsf/Desktop/medical/app/iOS/Ehospital/../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/AliyunOSSiOS.framework" failed: No such file or directory (2)
done

sent 29 bytes received 20 bytes 98.00 bytes/sec
total size is 0 speedup is 0.00
rsync error: some files could not be transferred (code 23) at /AppleInternal/Library/BuildRoots/97f6331a-ba75-11ed-a4bc-863efbbaf80d/Library/Caches/com.apple.xbs/Sources/rsync/rsync/main.c(996) [sender=2.6.9]
Command PhaseScriptExecution failed with a nonzero exit code

找到...-frameworks.sh 文件,替换

source="$(readlink "${source}")"

source="$(readlink -f "${source}")"

4. The version of CocoaPods used to generate the lockfile (1.3.1) is higher than the version of the current executable (1.1.0.beta.1). Incompatibility issues may arise.


这个比较简单,更新cocoapods就行。

sudo gem install cocoapods

5. Warning: CocoaPods minimum required version 1.6.0 or greater not installed…

sudo gem install cocoapods

6. Cocoapods 更新卡死在1.5.3,但控制台一直提示说有新版本


主要就是ruby的问题了。别问我怎么知道的,花了一天的时间。

ruby -v 查看版本

若比较低,现在一般都3.x了,所以要升级


用以下命令就可以升级了,可能需要科学上网。

brew update
brew install ruby

升级完成以后,ruby -v后其实还是原来的版本👌,这是因为环境变量没有配置。因此,还有一个步骤就是配置环境变量。

vi ~/.zshrc 

拷贝 export PATH="/usr/local/opt/ruby/bin:$PATH" 放进去


英文输入法下 按下esc键 输入 :wq


最后再执行

source ~/.bash_profile

然后更新gem

gem update #更新所有包
gem update --system #更新RubyGems软件

最后再更新pod

sudo gem install cocoapods

注意现在可能会提示说更新到了1.12.1了,但实际上还是1.5.3,所以还要执行另外一个命令。

sudo gem install -n /usr/local/bin cocoapods

这个就可以有效升级了。


7. gem常用命令

gem -v #gem版本
gem update #更新所有包
gem update --system #更新RubyGems软件
gem install rake #安装rake,从本地或远程服务器
gem install rake --remote #安装rake,从远程服务器
gem install watir -v(或者--version) 1.6.2#指定安装版本的
gem uninstall rake #卸载rake包
gem list d #列出本地以d打头的包
gem query -n ''[0-9]'' --local #查找本地含有数字的包
gem search log --both #从本地和远程服务器上查找含有log字符串的包
gem search log --remoter #只从远程服务器上查找含有log字符串的包
gem search -r log #只从远程服务器上查找含有log字符串的包
gem help #提醒式的帮助
gem help install #列出install命令 帮助
gem help examples #列出gem命令使用一些例子
gem build rake.gemspec #把rake.gemspec编译成rake.gem
gem check -v pkg/rake-0.4.0.gem #检测rake是否有效
gem cleanup #清除所有包旧版本,保留最新版本
gem contents rake #显示rake包中所包含的文件
gem dependency rails -v 0.10.1 #列出与rails相互依赖的包
gem environment #查看gem的环境

结语


有些坑现在只是知道这样做就行,还不知道为什么。后面再补补吧。


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

iOS-解决定位权限卡顿问题

iOS
一、简介 在iOS系统中,定位权限获取是一个涉及进程间同步通信的方法,如果频繁访问可能会导致卡顿或者卡死。在一些打车或者地图类的APP中,定位权限的卡顿报错可能是大头,亟需解决! 下面是系统类提供的访问定位权限的方法:// CLLocationManager是...
继续阅读 »

一、简介


在iOS系统中,定位权限获取是一个涉及进程间同步通信的方法,如果频繁访问可能会导致卡顿或者卡死。在一些打车或者地图类的APP中,定位权限的卡顿报错可能是大头,亟需解决!
下面是系统类提供的访问定位权限的方法:

// CLLocationManager是系统的定位服务管理类
open class CLLocationManager : NSObject {
// 1.下面方法是访问系统设置中定位是否打开
@available(iOS 4.0, *)
open class func locationServicesEnabled() -> Bool

// 2.1 iOS 14.0之后,访问定位的授权状态
@available(iOS 14.0, *)
open var authorizationStatus: CLAuthorizationStatus { get }

// 2.2 iOS 14.0之后,访问定位的授权状态
@available(iOS, introduced: 4.2, deprecated: 14.0)
open class func authorizationStatus() -> CLAuthorizationStatus
}

二、从卡顿堆栈例子中分析问题


为了解决这个卡顿,首先要分析卡顿报错堆栈。接下来举一个定位权限频繁获取导致的卡顿的堆栈:

0 libsystem_kernel.dylib _mach_msg2_trap + 8
1 libsystem_kernel.dylib _mach_msg2_internal + 80
2 libsystem_kernel.dylib _mach_msg_overwrite + 388
3 libsystem_kernel.dylib _mach_msg + 24
4 libdispatch.dylib __dispatch_mach_send_and_wait_for_reply + 540
5 libdispatch.dylib _dispatch_mach_send_with_result_and_wait_for_reply + 60
6 libxpc.dylib _xpc_connection_send_message_with_reply_sync + 240
7 Foundation ___NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__ + 16
8 Foundation -[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:] + 2236
9 Foundation -[NSXPCConnection _sendSelector:withProxy:arg1:arg2:arg3:] + 136
10 Foundation __NSXPCDistantObjectSimpleMessageSend3 + 76
11 CoreLocation _CLCopyTechnologiesInUse + 30852
12 CoreLocation _CLCopyTechnologiesInUse + 25724
13 CoreLocation _CLClientStopVehicleHeadingUpdates + 104440
14 MyAPPName +[ZLLocationRecorder locationAuthorised] + 40
15 ... // 以下略
  • 首先从第14行找到是ZLLocationRecorder类的locationAuthorised方法调用后,执行到了系统库函数,最终导致了卡死、卡顿。

  • 对堆栈中第0-13行中的方法做一番了解,初步发现xpc_connection_send_message_with_reply_sync函数涉及进程间同步通信,可能会阻塞当前线程点击查看官方方法说明



该函数说明:Sends a message over the connection and blocks the caller until it receives a reply.

  • 接下来添加符号断点xpc_connection_send_message_with_reply_sync, 注意如果是系统库中的带下划线的函数,我们添加符号断点的时候一般需要少一个下划线_. 执行后,从Xcode的方法调用栈视图中查看,可以发现ZLLocationRecorder类的locationAuthorised方法内部中调用CLLocationManager类的locationServicesEnabledauthorizationStatus方法都会来到这个符号断点.所以确定了是这两个方法导致的卡顿。(调试时并未发现卡顿,只是线上用户的使用环境更加复杂,卡顿时间长一点就被监控到了,我们目前卡顿监控是3秒,卡死监控是10s+)。

  • 然后通过CLLocationManager类的authorizationStatus方法说明,发现也是说在权限发生改变后,系统会保证调用代理方法locationManagerDidChangeAuthorization(_:),所以就产生了我们的解决方案,最终上线后也是直接解决了这个卡顿,并且APP启动耗时监控数据也因此变好了一些。


三、具体的解决方案



 注意点:设置代理必须在有runloop的线程,如果业务量不多的话,就在主线程设置就可以。


四、Demo类,可以直接用

import CoreLocation

public class XLLocationAuthMonitor: NSObject, CLLocationManagerDelegate {
// 单例类
@objc public static let shared = XLLocationAuthMonitor()

/// 定位服务是否可用, 这里设置成变量避免过于频繁调用系统方法时产生卡顿,系统方法涉及进程间通信
@objc public private(set) var serviceEnabled: Bool {
set {
threadSafe { _serviceEnabled = newValue }
}

get {
threadSafe { _serviceEnabled ?? CLLocationManager.locationServicesEnabled() }
}
}

/// 定位服务授权状态
@objc public private(set) var authStatus: CLAuthorizationStatus {
set {
threadSafe { _authStatus = newValue }
}

get {
threadSafe {
if let auth = _authStatus {
return auth
}
if #available(iOS 14.0, *) {
return locationManager.authorizationStatus
} else {
return CLLocationManager.authorizationStatus()
}
}
}
}

/// 计算属性,这里返回当前定位是否可用
@objc public var isLocationEnable: Bool {
guard serviceEnabled else {
return false
}

switch authStatus {
case .authorizedAlways, .authorizedWhenInUse:
return true
case .denied, .notDetermined, .restricted:
return false
default: return false
}
}

// MARK: - 内部使用的私有属性
private lazy var locationManager: CLLocationManager = CLLocationManager()
private let _lock = NSLock()
private var _serviceEnabled: Bool?
private var _authStatus: CLAuthorizationStatus?

private override init() {
super.init()
// 如果是主线程则直接设置,不是则在mainQueue中设置
DispatchQueue.main.safeAsync {
self.locationManager.delegate = self
}
}

private func threadSafe<T>(task: () -> T) -> T {
_lock.lock()
defer { _lock.unlock() }
return task()
}

// MARK: - CLLocationManagerDelegate
/// iOS 14以上调用
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if #available(iOS 14.0, *) {
authStatus = locationManager.authorizationStatus
serviceEnabled = CLLocationManager.locationServicesEnabled()
}
}

/// iOS 14以下调用
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
authStatus = status
serviceEnabled = CLLocationManager.locationServicesEnabled()
}
}

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

小米:阳了,被裁了

hi 大家好,我是 DHL。公众号:ByteCode ,专注有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、大厂面经。 随着防疫政策的放开,小阳人越来越多了,身边很多小伙伴都在朋友圈晒自己阳了之后的各种状态,基本上...
继续阅读 »

hi 大家好,我是 DHL。公众号:ByteCode ,专注有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、大厂面经。



随着防疫政策的放开,小阳人越来越多了,身边很多小伙伴都在朋友圈晒自己阳了之后的各种状态,基本上都处于一边发烧,一边坚持工作的状态,症状严重的小伙伴忍着疼痛还要处理公司的任务,把自己奉献给公司,然后收到了却是公司无情的裁员的消息。


年末将至,知乎和小米也登上了热搜。


裁员


我之前在小米的同事陆陆续续收到了通知,阳着还在工作,然后收到了裁员的消息。国内公司裁员的吃相都不怎么好看,基本上在发年终奖前会进行大比例的裁员。


2021 年的时候小米有 32000 名员工,据传 2022 年底小米要裁 6000 名员工,裁员幅度接近 20%,无论消息是否真实,但是这次裁员规模影响范围应该不小。



小米为什么要裁员


小米连续 3 个季度开始下滑,前 3 个季度,每个季度利润 20 亿,相比于去年同期的 50 亿下跌了很多,那为什么利润下跌这么多呢,主要有以下原因:


  1. 公司不赚钱,意味着主营业务开始萎缩,小米的主营业务,手机前 3 个季度卖了 4020 万部,销售额大概 425 亿,平均每部手机 1000 元,原本指望华为被制裁之后,小米能拿下这部分用户,但是最后也放弃了,这部分用户基本上都归苹果了
  2. 据调查中国的手机市场已经处于饱和状态,每年换手机的发烧友越来越少了
  3. 小米赌上全部身价大踏步地进入汽车领域,汽车是个周期长、投资大的业务,没有上百个亿,基本上不可能会有结果的
  4. 小米的股价也跌了很多,投资人很失望,我也买了很多小米的股票,基本上都是血亏

所以不得不开始降本增效,在老板的眼里,业务上升期的时候,开始疯狂砸钱招人,到达了瓶颈,业务不再增长的时候,老板就会冷静下来盘算,到底需不需要这么多人,然后开始降本增效,而裁员就是最有效的控制成本的手段。


曾经有小伙伴问过,小米的年终奖能拿多少


我在这里也只是顺口一说,大家当做饭后余兴看看就好了,小米的年终奖是 2 个月,而个人绩效是跟部门和所在事业部挂钩的,如果部门的绩效好的话,大部分人都能拿满,但是如果部门绩效不好的话,只有少数人能拿满,平均下来一个部门能拿满 2 个月的人数非常少,如果你非常的优秀,拿 3~4 个月也是有的,但是这个比例极其少,如果你和领导关系好的话,那么就另当别论了。


小米这次裁员赔偿虽然给了 N+2,但是这次裁员的吃相也比较难看,引来了小米员工的吐槽。以下图片来自网络。




而每次裁员,应届生都是最惨的,在大裁员的环境下,能不能找到工作是最大的问题,应届生和有工作经验的社招生是不一样的,无论是赔偿还是找工作的机会,相比于应届生更愿意招社招生,当然特别优秀的除外。



我之前很多在小米的同事,赔偿都给了 N + 2,但是年底被裁员时间点非常的不好,短时间内,想找到工作是非常困难的,但是先不要着急,如果你的身体还没恢复,建议先等身体恢复,在恢复期间,整理一下你的工作项目,网上搜索一下面试题,整理和回顾这些面试题,记住一定要多花时间刷算法题。


等到年后找工作会容易些,面试的成功的率也会很高,你的溢价空间也会很大,在选择公司的时候,这个阶段还是以稳为主,避开那些风险高的公司和部门。


文章的最后


遍地小阳人的冬天比以往更冷,在公司非常艰难,业务不再增长的时候,都会断臂求生,我们都要去面对被裁的风险。


站在打工者的角度,当一个人在某个环境待久了,会被表象的舒适所蒙蔽,时间久了会变得很迷茫,所以我们要想办法跳出舒适圈,保持学习的热情。

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

鸿蒙原生应用,全面启动,开发者需要抓住风口的浪尖

iOS
前言 老铁们,就在前天,9月25日,在华为秋季全场景新品发布会上,华为常务董事、终端BG CEO、智能汽车解决方案BU董事长余承东介绍了鸿蒙系统的最新进展:HarmonyOS 4发布后,短短一个多月升级用户已经超过6000万,成为史上升级速...
继续阅读 »

前言


老铁们,就在前天,9月25日,在华为秋季全场景新品发布会上,华为常务董事、终端BG CEO、智能汽车解决方案BU董事长余承东介绍了鸿蒙系统的最新进展:HarmonyOS 4发布后,短短一个多月升级用户已经超过6000万,成为史上升级速度最快的HarmonyOS版本;余承东宣布,鸿蒙原生应用全面启动,HarmonyOS NEXT开发者预览版将在2024年第一季度面向开发者开放。



我们知道,在8月4日的华为开发者大会,华为才刚刚推出了面向开发者的 HarmonyOS NEXT 开发者预览版,如果说当时只是一个概念,那么这次,绝对是正式官宣,打响移动端第三系统的第一枪!我们有理由且必须相信,HarmonyOS NEXT开发者预览版正在急速到来,不仅仅是对手机系统的冲击、移动端的开发者也有着不小的冲击。


如果说当时刚推出,你踌躇徘徊,犹豫不定,对待HarmonyOS犹如对待外来物一样,极度的排斥,那么这次你绝对忽视不得,否则,你将错过一个时代的步伐。


短短一个多月升级用户已经超过6千万,足以打脸那些看弱HarmonyOS的人,也从另一方面说明,HarmonyOS已经得到越来越多人的喜爱,或许是有了不断攀升的用户量,才让华为手机有了信心去发展原生系统应用,并且此前有爆料数据显示鸿蒙OS5.0会取消支持安卓软件,这种爆料绝非空穴来风,可能很多人包括我在内,会觉得取消支持Android软件,是一件非常冒险的行为,但是随着华为手机的体量越来越大,生态越来越好,这个事情是必须且迟早的要做的。


鸿蒙是否有必要学习


可能鸿蒙从一诞生,就背着一个”套壳“的骂名,毕竟一直都兼容AOSP(Android 开放源代码项目),很难不令人怀疑,当然了,曾经我也有所怀疑,以至于,对于HarmonyOS保持的态度,始终都是,冷漠,不感冒,毕竟Android开发的包,在HarmonyOS上也能用,我们何必再去研究它呢?费力又费时间,还不如刷刷短视频,对吧。


但是,一旦HarmonyOS剥离AOSP,Android开发的包无法在其运行,这种情况下,身为移动端的开发者,特别是Android端的开发者,你觉得有没有必要学习?


试想这样的一个场景,当其他的应用都能在HarmonyOS上运行,而你的应用确不支持,你是什么感觉?当然了,也得问一句,你们公司是什么感觉?虽然说目前HarmonyOS国内市场占有率为8%,占有率并不是很多,但止不住它发展迅速啊,未来,20%,50%都有可能,即便是8%,这样的一个市场,你和你的公司难道会果断的放弃?如果放弃的话,确实没必要学,但是,能放弃吗?


再试想一个场景,随着HarmonyOS不断的发展,移动端三分天下,而企业考虑到成本问题,在招聘的时候,要求了必须要会HarmonyOS开发,你如何破解这个问题?


无论是自身发展还是当下的企业布局,HarmonyOS都是你躲不过的一道屏障,无非就是什么时间入手的问题,当然了,如果一个企业或者个人,对HarmonyOS,没什么业务发展,也不在乎这些市场份额,那就没必要学习,反过来,真的要静下心来,好好研究研究了,否则影响的不仅仅是一个应用,更是大量的用户流失。


可能很多人都会觉得,HarmonyOS剥离AOSP,这么冒险的事,华为大概率不会那么武断,即便升级,可能也会采取双系统并行,也就是HarmonyOS4.0 和HarmonyOS Next,继续兼容Android一段时间,当然了,不排除这种做法,我想说的是,这也只是一个广大的猜测,在其他大厂APP都跟进的情况下,如果它升级了,怎么办?哪怕概率为1%,对企业和个人的影响绝对是100%,话又说回来,它采取了双系统并行或者有其他的兼容方案,你觉得华为会一直兼容吗,所以啊,如果你想继续从事这个行业,学只不过是早晚的问题


所以啊,HarmonyOS,肯定是要学的,除非你要告别当前从事的移动端开发,如果再做一层针对性的,那就是告别Android端开发,毕竟和iOS端的冲突目前还没那么大。


不仅要学,而且还要提前进行技术储备,目的防患于未然;毕竟来年的事,谁也说不定,有条件的公司,技术储备之后,就可以复刻鸿蒙版App了,尽量赶上升级后的第一批App,这样就可以做到无缝衔接,不至于鸿蒙系统流失用户,当然了,也可以只做技术储备,隔岸观火,进一步观察HarmonyOS的下一步动作,但是,技术储备一定要做,无论来年华为升级与否,因为复刻鸿蒙版App,不是一朝一夕能够完成的,起码目前来看,还没有一件转化的功能,只能从0到1的进行开发,小体量的App还好说,大体量的App,从0到1没个半年以上还真完成不了,所以啊,哪怕华为宣布来年不强制升级,到2025年升级,留给开发者的时间还多吗?


HarmonyOS的学习路径有很多,官网也给出了详细的视频以及文档教程,大家可以直接学习即可,当然了大家也可以关注我,哈哈,我也会定时分享HarmonyOS相关的技术,目前在有序的输出。


鸿蒙未来的发展


根据华为最新公布的数据:目前鸿蒙生态设备已达7亿台,早就跨过了“生死线”;鸿蒙品牌知名度从2021年的50%升级至今年6月的85%,越来越多的用户知晓和主动拥抱HarmonyOS;HarmonyOS 3用户升级率达到85%,超过了iOS(81%)成为最新版本设备升级率最高的操作系统,而HarmonyOS 4发布后,短短一个多月升级用户已经超过6000万,可以说是,恐怖如斯,遥遥领先!


目前华为已与合作伙伴和开发者在社交、影音、游戏、资讯、金融等18个领域全面展开合作,在HarmonyOS独特的全场景分布式体验、原生智能、纯净安全、大模型AI交互等方面,HarmonyOS NEXT构筑了差异化优势,全面领先于行业。


为了更好帮助合作伙伴成长,在HDC 2023期间,华为正式发布鸿蒙生态伙伴发展计划——“鸿飞计划”,宣布未来三年将投入百亿人民币,向伙伴提供全方位的资源扶持,包括技术支持、市场推广、商业合作等,让每一位伙伴都成为鸿蒙生态的主角。


无论是企业的绝对支持,还是政府的大力推进,HarmonyOS的发展,可以说势如破竹,三分天下,也就是时间的问题。


我们都知道,操作系统生态的发展,人才是重中之重。随着鸿蒙生态的发展,专业人才需求正在呈现井喷式增长,为此,在鸿蒙人才培养方面,华为也做了全面投入,今年以来已有超过170万人参加了鸿蒙学堂的课程学习、线下活动,华为还和全国300多所高校展开了合作,鸿蒙产学合作项目超过140个,已经颁发鸿蒙学堂证书超过7万,各类开发者活动累计参加人次超过350万。


可以告诉大家的是,俺也是其中一员,哈哈~,当然了,证书并没有含金量,只是一个阶段学习的测试而已。



除此之外,近期教育部-华为“智能基座”产教融合协同育人基地2.0启动,未来双方将与72所高校合作培养鸿蒙人才,一起促进鸿蒙生态的繁荣发展。


我们总担忧鸿蒙的生态,对它不屑一顾,说它“套壳”,说它抄袭,说它迟早会死,可是,人家不吭不响,不反驳,只会默默的耕耘,以至于发展的越来越好,越来越完善,为什么鸿蒙这么自信,我们却不自信呢?我们在担忧什么?


鸿蒙的生态离不开每一个的开发者,我们有理由相信,未来的时刻,它肯定会剑指Android和iOS,我们更有理由相信,国产系统的繁荣富强,一定会到来,民族的自信心也必定到来!


鸿蒙不仅仅是一个系统,它是更长远的国家战略


国家战略说的有点大了,但是肯定是在计划之内和大力支持的,为什么这么说,从18年的中美贸易战,到22年的俄乌冲突,卡脖子的事,发生的还少吗?动不动进行制裁,动不动限制出口,美国佬龌龊的事做的还少吗?如果说一直没有自研,那么话语权始终掌握在别人手里,不仅仅是一个系统,像芯片等等,我们始终很难强大。


俄乌冲突期间,谷歌公司停止认证运行安卓操作系统的俄罗斯BQ公司的智能手机,微软宣布禁止在俄罗斯使用Windows系统,也许对于我们个人而言,觉得没什么影响,但是站在国家层面,绝对是致命的打击,如果未来,收复TW时,也来这么一下,你觉得,国家能承受的住吗?


除了各种限制和制裁之外,俄乌冲突期间最恐怖的是,谷歌地图服务提供俄罗斯所有军事和战略设施的最高分辨率卫星图像,这不就等于明牌了,你在明处,人家在暗处,所以,无论是系统,还是芯片,还是其他的技术方向,站在国家层面上,能够自研,无论是摆脱外部限制,还是自身科技发展,绝对都是划时代的意义。


所以,老铁们,对于鸿蒙,于国于人,我们都应该有充足的自信,不仅仅关系着手机系统的三分天下,更是国家安全的未来措施,政策,一定是某项事物发展的导向,跟着国家走,准没错。


开发者如何提前布局


我觉得应该从三方面入手,第一,就是技术储备,学习HarmonyOS,能够达到独立的完成项目开发;第二,就是,技术架构,组件,基础库的梳理和开发,这么做的目的,是便于日后项目的快速开发;第三,就是着手自己项目HarmonyOS版的开发了,以应对未来HarmonyOS升级。


未来是否有一键转化HarmonyOS版App的功能,这个一切未知,有的话,就太方便了,没有的话,只能从0到1进行开发了,当然了,跨平台语言的支持,也是一个突破点,比如Flutter支持HarmonyOS,那么对于原来Flutter语言的App而言,就无比轻松了,而目前来说,这些都没有一个实质性的进展,所以还是一步一步的先学习HarmonyOS开发吧。


还好,HarmonyOS主推的是ArkTs语言,其中也定义了声明式UI,和Flutter,Compose,Swift等有着异曲同工之妙,如果你有着声明式开发的经验,那么掌握HarmonyOS简直是易如反掌。


当然了,为了更好的提高开发效率,HarmonyOS采用了反推的做法,推出了自己的跨平台框架ArkUI-X,成熟之后,我们可以作为开发框架,进而兼容Android和iOS。


ArkUI-X 是 ArkUI 的跨平台框架,采用 ArkUI 开发的应用能在 HarmonyOS 上原生运行,获得极佳的性能,通过 ArkUI-X 能够在 Android 和 IOS 上跨平台运行,获得强于 Flutter、React Native 等同类竞品的性能。



总结


该说的也都说了,不该说的也说了,至于HarmonyOS,您是学习还是放弃,只能由自己决断了,可以肯定得是,您的放弃,一定是未来的错误决定。


番外


写文章的时候,电脑上老是有一种刺啦刺啦声音,这个声音很小,听的不是很清楚,一开始我总以为是敲击键盘的声音,当我凑近一听,一种熟悉的声音扑面而来:遥遥领先,遥遥领先~


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

对不起 localStorage,现在我爱上 localForage了!

web
前言 前端本地化存储算是一个老生常谈的话题了,我们对于 cookies、Web Storage(sessionStorage、localStorage)的使用已经非常熟悉,在面试与实际操作之中也会经常遇到相关的问题,但这些本地化存储的方式还存在一些缺陷,比较明...
继续阅读 »

前言


前端本地化存储算是一个老生常谈的话题了,我们对于 cookies、Web Storage(sessionStorage、localStorage)的使用已经非常熟悉,在面试与实际操作之中也会经常遇到相关的问题,但这些本地化存储的方式还存在一些缺陷,比较明显的缺点如下:



  1. 存储量小:即使是web storage的存储量最大也只有 5M

  2. 存取不方便:存入的内容会经过序列化,当存入非字符串的时候,取值的时候需要通过反序列化。


当我们的存储量比较大的时候,我们一定会想到我们的 indexedDB,让我们在浏览器中也可以使用数据库这种形式来玩转本地化存储,然而 indexedDB 的使用是比较繁琐而复杂的,有一定的学习成本,但第三方库 localForage 的出现几乎抹平了这个缺陷,让我们轻松无负担的在浏览器中使用 indexedDB


截止今天,localForage 在 github 的 star 已经22.8k了,可以说 localForageindexedDB 算是相互成就了。


什么是 indexedDB


IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象)。


存取方便


IndexedDB 是一个基于 JavaScript 的面向对象数据库。IndexedDB 允许你存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。


之前我们使用 webStorage 存储对象或数组的时候,还需要先经过先序列化为字符串,取值的时候需要经过反序列化,那indexedDB就比较完美的解决了这个问题,可以轻松存取对象或数组等结构化克隆算法支持的任何对象。


stackblitz.com/ 网站为例,我们来看看对象存到 indexedDB 的表现



异步存取


我相信你肯定会思考一个问题:localStorage如果存储内容多的话会消耗内存空间,会导致页面变卡。那么 IndexedDB 存储量过多的话会导致页面变卡吗?


不会有太大影响,因为 IndexedDB 的读取和存储都是异步的,不会阻塞浏览器进程。


庞大的存储量


IndexedDB 的储存空间比LocalStorage 大得多,一般可达到500M,甚至没有上限。


But.....关于 indexedDB 的介绍就到此为止,详细使用在此不再赘述,因为本篇文章我重点想介绍的是 localForage!


什么是 localForage


localForage 是基于 indexedDB 封装的库,通过它我们可以简化 IndexedDB 的使用。



兼容性


想必你一定很关注兼容性问题吧,我们可以看下 localStorage 与 indexedDB 兼容性比对,两者之间还是有一些小差距。


image.png


但是你也不必太过担心,因为 localforage 已经帮你消除了这个心智负担,它有一个优雅降级策略,若浏览器不支持 IndexedDB 则使用 WebSQL ,如果不支持 WebSQL 则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。


localForage 的使用



  1. 下载


import localforage from 'localforage'




  1. 创建一个 indexedDB


const myIndexedDB = localforage.createInstance({
name: 'myIndexedDB',
})


  1. 存值


myIndexedDB.setItem(key, value)


  1. 取值


由于indexedDB的存取都是异步的,建议使用 promise.then() 或 async/await 去读值


myIndexedDB.getItem('somekey').then(function (value) {
// we got our value
}).catch(function (err) {
// we got an error
});

or


try {
const value = await myIndexedDB.getItem('somekey');
// This code runs once the value has been loaded
// from the offline store.
console.log(value);
} catch (err) {
// This code runs if there were any errors.
console.log(err);
}


  1. 删除某项


myIndexedDB.removeItem('somekey')


  1. 重置数据库


myIndexedDB.clear()


以上是本人比较常用的方式,细节及其他使用方式请参考官方中文文档localforage.docschina.org/#localforag…



VUE 推荐使用 Pinia 管理 localForage


如果你想使用多个数据库,建议通过 pinia 统一管理所有的数据库,这样数据的流向会更明晰,数据库相关的操作都写在 store 中,让你的数据库更规范化。


// store/indexedDB.ts
import { defineStore } from 'pinia'
import localforage from 'localforage'

export const useIndexedDBStore = defineStore('indexedDB', {
state: () => ({
filesDB: localforage.createInstance({
name: 'filesDB',
}),
usersDB: localforage.createInstance({
name: 'usersDB',
}),
responseDB: localforage.createInstance({
name: 'responseDB',
}),
}),
actions: {
async setfilesDB(key: string, value: any) {
this.filesDB.setItem(key, value)
},
}
})

我们使用的时候,就直接调用 store 中的方法


import { useIndexedDBStore } from '@/store/indexedDB'
const indexedDBStore = useIndexedDBStore()
const file1 = {a: 'hello'}
indexedDBStore.setfilesDB('file1', file1)

后记


以上就是本篇文章的所有内容,感谢观看,欢迎留言讨论。


作者:阿李贝斯
来源:juejin.cn/post/7275943591410483258
收起阅读 »

解决Android13上读取本地文件权限错误记录

Android13 WRITE_EXTERNAL_STORAGE 权限失效 1. 需求及问题 需求是读取sdcard上txt文件 Android13(targetSDK = 33)上取消了WRITE_EXTERNAL_STORAGE,READ_EXTERN...
继续阅读 »

Android13 WRITE_EXTERNAL_STORAGE 权限失效


1. 需求及问题



  1. 需求是读取sdcard上txt文件

  2. Android13(targetSDK = 33)上取消了WRITE_EXTERNAL_STORAGEREAD_EXTERNAL_STORAGE权限。

  3. 取而代之的是READ_MEDIA_VIDEOREAD_MEDIA_AUDIOREAD_MEDIA_IMAGES权限

  4. 测试发现,即便动态申请上面三个权限,仍旧无法读取本地txt文件


image.png


2. 解决方案



  1. AndroidManifest.xml中增加


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LocationDemo"
tools:targetApi="31">


<activity
android:name=".MainActivity"
android:exported="true">

<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>


  1. Activity中新增代码


// 方案一:跳转到系统文件访问页面,手动赋予
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + this.getPackageName()));
startActivity(intent);

Screenshot_20230927-131444[1].png


// 方案二:跳转到系统所有需要文件访问页面,选择你的APP,手动赋予权限
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);

image.png


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

傻吗?谈男人们饭桌的拼酒现象

过年了,亲朋好友们聚在一起,免不了会喝酒。对于喝酒,尤其是人多的时候,更尤其是多数人都喝的时候,男性朋友们近乎是往“死”里喝。 这一点,女性朋友们很难理解。 首先,酒喝多了肯定是伤害身体的。它会危害肝、胃、心脑血管。并且还让人神志不清,容易出错,增大发生危险的...
继续阅读 »

过年了,亲朋好友们聚在一起,免不了会喝酒。对于喝酒,尤其是人多的时候,更尤其是多数人都喝的时候,男性朋友们近乎是往“死”里喝。


这一点,女性朋友们很难理解


首先,酒喝多了肯定是伤害身体的。它会危害肝、胃、心脑血管。并且还让人神志不清,容易出错,增大发生危险的概率。酗酒伤身体,这已经是没有什么争议的事情了。那些说接触烟酒能长寿的人和例子,也并非是假的。只是长寿和影响健康,是可以同时发生的。


但是,少喝一点,从缓解焦虑情绪和分散注意力来讲,对心理是有帮助的。这作用等同于听歌和看电影。


我们在生活中,经常会发现这样一个现象。那就是饭局中会有拼酒现象。尤其是同平级伙伴吃饭,一定要分出个高下。仿佛谁喝的多,谁就厉害,谁就是王。这个现象在男性群体中尤为明显。


可能你也不知道为什么要拼酒,但还是不自主地加入了这个队伍


其实,这可能是个高端局。下面咱从三个方面,分析这个事情。


第一:争强斗胜是人的本性。人在还是动物的时候,就用尽各种方式相互斗争、比赛,目的就是脱颖而出,获得好的资源。野蛮的时候,主要途径是肢体上的搏斗和厮杀。这也是很多体育竞技项目产生的原因。


然而现在的文明社会,很难再体现上面的冲突了。尤其是饭局上,你肯定不能打一架。现代文明,需要一种有难度又印象深刻的表现形式。看谁吃得多,肯定不行。然而喝酒,就是一个很好的表现形式。


第二:反映一个人的自控能力。人都有想干的事情和不想干的事情。同样,酒喝到一定程度,也会不想喝。不想喝的时候就不喝吗?那么不想加班的时候就不加班吗?不想早起的时候就不早起吗?这是一种承受和应对压力的能力。同时,喝完酒会意识模糊,在这种情况下需要控制自己的言行举止,稍有不当,将会贻笑大方。所以,这也是一个体现个人对自身控制能力的筛选项。


第三:快速拉近彼此间的距离。一群陌生人吃饭,即使有一个好的话题讨论,彼此之间也会有一个陌生的安全距离。然而很多时候,饭桌的上的人,不是有经常相聚的机会的。但是这时还带着各自的目的和任务。因此,在短时间内搞好关系,变得尤为重要。通过酒,可以让相敬如宾变为勾肩搭背,要个号码,打听个事情,变得简单起来。


拼酒,是一种肢体搏斗在如今文明世界的延续和替代。因此,它又是另一种你死我活。它具备了通过非暴力手段就可区分出层次的指标。以上三点看似合理,也不排除有曲解之嫌。不可否认,拼酒这种杀敌一百,自损三千的方式,几千年了依然存在,也有它存在的道理。


你愿意拼就拼,不愿意就撤。这个没啥,全看个人的选择。


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

2023年:我成了半个外包

边线业务与主线角色被困外包; 01 2022年,最后一个工作日,裁员的小刀再次挥下; 商务区楼下又多了几个落寞的身影,办公室内又多了几头暴躁的灵魂; 随着裁员的结束,部门的人员结构简化到了极致,至少剩下的人是这么认为的; 说实话,对于当下的互联网行业来说,个...
继续阅读 »

边线业务与主线角色被困外包;




01



2022年,最后一个工作日,裁员的小刀再次挥下;


商务区楼下又多了几个落寞的身影,办公室内又多了几头暴躁的灵魂;


随着裁员的结束,部门的人员结构简化到了极致,至少剩下的人是这么认为的;


说实话,对于当下的互联网行业来说,个人感觉两极分化的有点严重;


卷的,卷到鼻青脸肿,不知道BUG和需求哪个会先来;


不卷,感觉随时失业,不知道明天和裁员哪个会先来;


最近这几年,裁员的故事已经不新奇了;


比较热的话题反而是留下的那些人,如何应对各种此起彼伏的事情;


裁员,对于走的人和留的人来说,都是正面暴击;


走的人,虽然拿着赔偿礼包,但是要面对未来工作的不确定性,尤其是在当下的环境中;


留的人,要兜底很多闪现过来的事项,未来一段时间会陷入混乱的节奏中;


对于公司来说;


裁员之后如何应对业务,没有一丝丝迟疑,会做出了完全出于本能的决定;


内部团队能应对的就自己解决,解决不了就交给外包方处理;


整体的策略就是:核心业务领域之外的需求,选择更低成本的解决手段;



02



公司裁员之后,本意还是想专注自己的核心业务;


至于为何要接其他公司的需求,这里就涉及很多社会上的人情世故了;


比如一些重要关系或者流水大的客户;


缺乏互联网方面的专业团队,合作时会偶尔抛出研发或其他方面的需求;


对于公司来说,接手吃力不讨好,不接手又怕影响客户关系维护;


最好的选择就是寻求外包解决;


基于公司的研发团队,替客户进行相关需求的落地把控;


虽然接收外包需求流水抽成不高,但是可以更加紧密的维持客户合作关系;


允许质疑外包的质量和效率,但是不能否认长期的整体成本;


在裁员之后,团队介入的外包项目越来越多,形成主线和外包业务五五开的魔幻局面;


外包项目的合作形式大致分为两种;




  • 甲乙双方:甲方的业务与公司主线业务相关联,通常由团队自己开发;

  • 甲乙丙三方:甲方的业务比较独立,乙方接手之后再转交给丙方开发;


在这种合作中,如果只涉及甲乙两方,流程还是顺畅的;


但是对于甲乙丙三方的合作模式,如果再关联其他对接方,简直就是离谱踹门而入,离谱想拆家;


在经历几次甲乙丙三方的合作过程中,对于夹板气的体会已经是铭刻在心了;


甲乙双方对于丙方来说,是提供需求单的甲方;乙丙双方对于甲方来说,是落地需求单的外包方;


合作过程中拉扯出个精分现象,都习以为常了;


下面基于甲乙丙三方合作的模式,来聊一聊外包所踩到的坑坑洼洼;



03



【如何选择外包公司】


在甲乙丙三方合作中,甲方交给乙方的业务,可能是基于信任关系,或者成本原因;


但是乙方想再找一个靠谱的外包团队,难度就会大很多;


乙方既然承接需求,最终都是想交付高质量的结果,从而加强双方的合作关系;


如果没有一个靠谱的外包团队介入,所谓高质量的结果根本无从谈起;


通常会先从过往的合作过且靠谱的外包团队中寻找,但是能找到的概率其实并不大,这里的影响因素有很多;


需求本身的复杂度,外包团队能不能承接,是一方面;


甲方对于需求落地的预期时间,与外包团队的介入时间是否符合,也是一方面;


乙方对于外包团队的报价能否接受,又是一方面;


如果合作过的团队中没有,则会优先从公司内部寻求推荐,比盲寻一个不知底的团队要靠谱很多;


这里存在一个关键的卡点因素;


虽然研发团队接触的外包人员多,但是碍于怕麻烦的心理,乐意介入的人很少;


所以需求最终交给一个新的外包团队的概率很大,也为后续的诸多问题埋下隐患;



04



【三方合作的流程机制】


首先还是先说一个基本原则,在复杂的协作中,明确流程是最基础的事项;


三方合作,实现需求,获取利益回报;


流程上看可能并不复杂,然而在实际协作过程中,又十分的曲折;


在明确协作的流程时,需要把握需求的三个关键阶段:排期、研发、交付;


这里阶段划分是站在研发的角度拆解,从项目经理或者决策层看又是另一个说法了;



在研发视角下,虽然依旧是围绕排期、研发、交付三个阶段;


但由于涉及三方协同,各个阶段的事项都会变的繁杂;


流程的推进和问题解决,都要进行三方的统筹协调,麻烦事也从不缺席;


排期阶段



  • 乙方接受甲方的需求单和报价,并寻求丙方做需求实现;

  • 丙方围绕需求单进行拆解,输出项目计划书以及报价,乙方认同后达成初步合作意向;

  • 乙丙双方就排期与甲方达成共识后,三方就各自的合作签订外包合同;


研发阶段



  • 丙方就需求完成设计,在甲乙双方评审通过后,正式进入开发阶段;

  • 丙方需要定期将开发进度同步给乙方,乙方确认后也需要定期汇报给甲方;


交付阶段



  • 理论上丙方在自测完成后,再交付给乙方进行验收;

  • 乙方在验收阶段承担的压力比较大,本着对客户关系负责的态度,需要实现高质量的交付;

  • 甲方验收通过后,进行线上部署并交付项目材料,最终完成合同的结算流程;


流程终究只是对协作的预期设定;


在实际的执行中,会有各种问题层出不穷;


很容易把各方都推到情绪的边缘,进而导致系列关联的效应问题;



05



【三方合作的沟通问题】


如果从三方合作的问题中,选一个最大的出来,不用证明都确定是沟通问题;


沟通不到位,问题容易说不清楚,解决问题的很多动作可能都是抓瞎;


由于三方的合作是远程在线模式,不是当面表达;


沟通频率本来就低,等到发现问题解决思路不对时,耽误的时间已经久了;


如果返工;


那排期又需要重新协商,又会引起一系列必要的麻烦问题;


这种情况,对于乙方的项目经理来说;


身处甲丙两方的极限拉扯之中,会经常在离职和跳槽的情绪中不断徘徊;


然而也不乏一些花哨的操作,将甲乙丙三方拉扯到一个协作群中;


如果甲方不介意乙方寻找外包实现需求,那么三方在群里及时沟通和解决问题的效率也会高很多;


但是大部分的甲方还是介意的,很多沟通都是由丙方到乙方,乙方再转述给甲方;


传话游戏玩到最后,驴头不对驴嘴的现象十有八九;


所以,很多的外包合作群中;


可能都是存在着甲乙丙三方人员,只是乙丙对甲方语调统一,以此避免信息传递的问题;



06



【需求落地的质量问题】


对于三方合作实现的需求,质量高不高?


比较肯定的回答;


可能有一定的质量,但是高质量的期望建议打消,说不定还有一丝惊喜;


质量依赖靠谱的外包合作方,这本身就是一件有难度的事,看脸和运气都没用;


专业负责的外包团队少有,所以其团队的业务有持续性;


在实际协作过程中出现的问题少,才可能更加专注于需求本身的落地实现上;


然而真实的现状是;


外包团队会在需求排期内尽快完成,投入越少,收益越大;


比如:实现一个需求,估时30天,费用10W;


如果在15天内完成需求,相当于成本投入缩减一半,这样在30天内可能实现多个需求;


鉴于这种策略之下,很多需求的实现可能都是仓促的,质量上自然很难保证;


所以对于质量问题的把关,压力会给到乙方,在交付验收时做好时间差管理;



乙方预留一部分时间段,对丙方交付进行验收,如果出现问题及时修改,避免传递到甲方;


当然了,混乱验收和测试也是常见的骚操作;


不乏一些丙方拿乙方的验收当测试,乙方拿甲方的验收当测试,以此来降低自己的时间成本;


由此导致三方合作裂开,尾款结算的问题,甚至对簿公堂也不少见;


虽然不是三方负责人乐意见到的,但又是三方都很难把控的事;


最终结果就是,不但成本没少,事情还更多了;



07



业务需求外包,是比较常见的一种手段,只是过程与结果的把控难度较大;


对于甲乙两方来说;


可能是利益驱动,可能是社会的人情世故,从而建立了合作关系;


对于乙丙两方来说;


则是单纯的利益考量,从而形成了短期的合作;


然而对于那些身处甲乙丙三方合作的网友们,只能在内心轻轻的嘀咕一句:人在社会,身不由己


作者:知了一笑
来源:juejin.cn/post/7203377276557852730
收起阅读 »

大龄,掘金,疫情,酒店,转型,前端满两年,搞公司后端两个月,年后离职还是继续等待?

大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但现在幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想...
继续阅读 »


大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但现在幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗并付诸于行动。



喜欢的可以到创作者榜单点点我,估计也没几个人点我哈哈,自己点自己嘞


1、前言


就跟随着标题一个一个的来总结一下自己的2022吧,绝望中透露着一丝的希望,让我不得不在逆境中重生,寻找新的出路。


2、欠薪6个月


今年上了12个月的班,但是呢不算12月的工资,竟然还有6个月的工资没发,公司确实欠薪了,而且也非常的难受。怎么办呢?我自己也不清楚,过完年再说吧,希望年前最后一个月还能发点工资吧。


3、大龄


88年大龄前端:转行前端不到两年|2022年年中总结


这是我在2022年年中的时候总结的文章,那个时候计划2022年下半年输出大概16篇文章,而我下半年真正输出了46篇文章,当然其中有一部分是在我脚骨折只能在家卧床的时候写的,所以从时间上来看有一些水分,但是从完成任务的角度我还是超额完成的,我对自己的表现非常满意,哈哈哈。


大龄也许就是一个分水岭,有的人踏过去了,也有的人就此放弃了,还有的人根本不当回事,那么你又是哪一种呢?


大龄,没学历,没背景,没资源就只能躺平吗?反正我觉得如果真躺平了,那就是平了,而我选择了继续努力,每天保持不断的学习努力有所成长,就会得到满足,,哪怕一点点,也经得起长时间的积累。


4、掘金



  • 收获最多的地方
    1bed61531924d964bbf75dd5d12911f.jpg


这里应该是收获最多的地方,55篇这放在任何时候想都不敢想,万万没想到竟然能输出这么多,而且还收获了掘金非常多的礼物,在此感谢掘金,感谢川哥https://juejin.cn/user/1415826704971918, 不用想肯定是你认识的那个若川视野。


61da0551e864447baa877f208eb0f43.jpg


这里的礼物只是一部分,还有另外一部分,什么背包帽子,等等的每次收到都非常的开心。


324f7d177af92efe44023043cd25583.jpg


这个创作先锋将我个人还是非常的意外,也是不经意间老婆收到的快递,简直开心到起飞。



  • 去年在掘金的阅读


image.png


2021年一年可以说是入门前端,和众多刚毕业以及毕业一两年的前端的道友们一起在这里不断的收获,这里我个人点赞(共683篇)的文章大多都是研读的文章。



  • 今年在掘金的阅读


9e851faeebda2eed0f7e074f72d93d3.jpg


同时依靠掘金我的github也竟然有了200多的小星星,实属难得


image.png


这里顺便提一下极客时间的学习


0e79faf2e59a08ba062182d24596aed.jpg


212ec2c1481895c931dd57c9f9cbee8.jpg


只能说尽力学对自己有用的,充实自己,其实很多篇我都是反复看,看的自己明明白白的。不过确实也收获到了知识。


2022年一年可以说是入门后的腾飞,不断在掘金的引领下,让我在自我思考的摸索中寻找到坚定的方向。同时在川哥的带领下我也能看懂一点牛逼开源项目的源码了,这真的可以说是比较大的突破了。同时可以发现2022年的阅读量会更大一些,由于自己也会进行输出,在输出的过程中其实更需要对知识进行再三确认。


5、疫情,酒店,转型




  • 万万没想到就在现在此时此刻,全国所有人正在经历着,或者自己的至亲正在经历着,又或者自己身边的人正在经历着“鼻子封水泥、喉咙吞刀片、内脏咳出胸、”等症状,本来这篇文章准备在12月23日发出来的,但早上一醒来就进入炼狱般的状态了,昨天一天在头痛和发烧中度过的。




  • 由于公司主营业务便是服务于酒店业务,公司在2020年和2021年的收入有所影响,但总体可控影响不大。但是时间节点来到2021年年底以及2022年的全年,各种突发情况,慢慢的让公司的收入锐减。




  • 同时公司在2020年也有了初步的判断,需要拓展业务,才有了新的业务赛道,可能是由于决策和对新赛道的陌生,也使得前期大幅投入迟迟达不到预期,迟迟也没有收入,公司也由360多人,一度减员到8月份低谷时期,总人数不到80吧。




6、前端满两年




  • 从2020年9月25日入职公司,开始接触vue2,然后着手公司pc端:vue2+elementui,微信端h5:vue2+vant, 然后android app webview嵌套 vue2+vant,期间也接触了一个react项目




  • 2021年年初开始走上,vite+vue3+echarts大屏项目,相对于熟悉了解了vue2后,直接用vue2的语法来写是没问题的,然后慢慢的也在学习vue3+setup的语法,也将某些组件进行了转换




  • 2021年4月开始一个新的pc项目,采用了qiankun微前端,主应用使用vite+vue3,其他子应用采用vuecli+vue3 + element-plus,刚使用qiankun时,还是遇到了一些问题




  • pc端项目经过几个月的时间,陆续稳定上线,然后期间封装了pc端的json form表单生成器和json table列表生成器,这两个组件节省了很多PC端重复的工作,以及bug修改,感觉封装出来还是有点成就感的,我的前端兄弟都觉得非常的nice。




  • 搞pc期间还接触了leaflet、leaflet-geoman来给地图打点或者画区域,上手略有难度,但经过几天的摸索熟悉后,能够磕磕绊绊的将需要的功能实现出来了,使用过后感觉这个类库的功能还是非常强大的。




  • 2021年年底开始在原有android app webview的基础上增加新的功能,考虑到对vue3以及qiankun的熟悉,准备添加一个子应用,使用vue3+vant的模式来处理新增的业务功能




  • 此时可着手两个组件的封装,一个当然还是json form表单生成器的,逻辑上跟pc组件是类似的,只是换了一套vant的组件。另外一个相当于pc端的table列表,但是在移动端的h5当中每个列表的样式可能不同,就单独提取了一个模板,加速充血了一波,待组件稳定后,其实大致到了2022年的3月份了。




  • 2022年4月份的时候公司有一个专门数据采集的项目,最终要的功能便是用到了根据json生成form表单的并且对接通用接口,json的生成也是通过页面进行配置。其中难度比较大的便是数据的联动控制显示隐藏,以及数据校验、正则匹配、以及将部分js代码通过界面去编写,前端解析json后再动态执行js代码也是一个不小的难点。




  • 另外一个突破便是将vant 列表数据模板,做了两个通用的,根据SQL配置 接口返回通用的数据结构列表,去匹配模板列表。其实这里也有思考通过后台配置,拖拽元素实现列表的一行数据样式展示,但是在渲染的时候我是根据屏幕宽高比去进行等比的展示,但是发现样式会有所变形,主要是通过transform: scale(0.9) 计算出比例,然后填充数值,我猜测可能是我实现的方式还存在问题,等有时间再来看看,主要是我觉得这个思路好像是没问题的。




  • 期间5、6月份开始解决vue3 移动端中 列表到详情再返回列表,并且要记录当时的位置的问题,其实解决起来还是蛮麻烦的,当时查阅资料或者水平还不够,没能实现,但是线上的问题又必须要解决,于是硬着头皮看了一下vue3 keppalive组件的源码,其实还是看了蛮久的,看完解决完问题后,我还专门写了一篇小文,一不小心算是上了掘金的头条,真的非常开心。




  • 同时解决微信小程序中嵌套webview场景中的一些小问题,最主要的一个问题其实微信中打开h5页面,如果有使用到localstorage或者cookie,再在微信小程序中嵌套h5页面,那么会存在脏读的问题。我是通过根据window.navigator.userAgent.toLowerCase() 先判断其中是否包含 'miniprogram',有则代表是在微信小程序中,再判断是否包含'micromessenger',有则代表是在微信环境中,这样针对每个环境去设置不同的key,然后在当前环境中使用当前的key就不会产生冲突了。




  • 2022年7月份意外脚骨折在家里呆了三个周吧,然后上下班打车两个月终于摆脱拐杖,不得不说真的是伤筋动骨100天呢。




  • 2022年8月和9月正常开始迭代新的需求和项目的bug修复,期间有指出有新的项目要开始了。由于自己自身的尴尬(原先前端由我来管理的,但是骨折期间和之后发生了一些令人不悦的事情,没办法我直接提出交出去吧),自己也不能闲下来,于是开始新项目的准备,前端我可以干,有时间了也开始参与后端的代码。




7、后端两个多月的时间了(从2022年10月至今)


之前使用过.net framework,而公司有个项目正好使用的是.net core,所以上手难度相对较小但由于很久没用,区别还是有的,,最大的区别当然就是跨平台了。于是在今年10月份开始接触.net core,这两个多月的时间下来对公司后端代码也算是有了更加深入的了解。之前的两年时间算是全部都花在了前端代码里。从我现在的角度来看后端,其实思路相对来说也非常的明确。




  • 熟悉操作linux常用的各种命令,因为要发布测试上线,服务器都是linux




  • 熟悉基础的后端代码,然后能够独立的实现CRUD增删改查




  • 熟悉mysql的基本操作,由于数据量比较大,所以对索引的使用也上了一个台阶,要不然严重影响接口的响应时间




  • 当然还有其他的但是目前来看还只算是皮毛,有待进一步的加强学习




8、年后离职还是继续等待?


关于这个问题其实自己思考过了,看年后一两个月的情况就可以快速决定了。没办法,从现在开始只能说我要时刻准备着,时刻准备让自己拥有更多的技能,能够让自己变得更加强大。


9、2023年计划


没有目标一切都将是空谈,给自己制定一个切实有效的目标,那么到了来年,可以跟随时间和需求的变化,再随时调整目标。


关于前端计划




  • 继续攻坚前端工程化




  • 继续攻坚前端组件的封装




  • 继续攻坚react的使用和深入,公司项目主要是vue3,自己玩无用武之地




关于后端计划




  • 微服务架构模式学习深入




  • 消息队列在项目各场景中灵活运用,比如先攻克一个rabbitmq




  • redis在项目中发挥桥梁的作用




  • mysql数据库如何在项目中发挥护城墙的作用,把好最后一道关卡




  • 项目整个架构相关的学习实战




所以最后争取吧,一年36篇小作文,也就是每个月三篇,目标不算远大,但好好的去完成也需要一些精力,关键是要对当前的自己要有用处。


10、总结




  • 35岁真的会被毕业吗?而且是会被永久毕业吗?如果身边的朋友、同学、又或者是同学的朋友、同事的朋友等等真的是大批量的都被毕业了,那么我才会觉得风险是真的来了。




  • 现在就是时刻准备着可能要发生的事情,企业如果真不行了,或者自己真的想换工作了,就提前准备不就完事了。




  • 说真的每天时间就那么有限,自从你有了家,有了娃,时间就如白驹过隙




  • 没什么负面情绪,如果有的话就转化为正面动力吧




  • 浅层的学习靠输入,深层的学习靠输出:通过几期的学习源码,能深刻感受到自己看一遍和写一遍真的是非常不一样




  • 兄弟们加油吧,也许在疫情的催化下底层人民过的将会更加艰苦,多关照一下家里的老年人




  • 在疫情的催化下我们也要重新考虑一下我们的工作和生活方式了




  • 喜欢的可以到创作者榜单点点我,估计也没几个人点我哈哈,自己点自己嘞




作者:那个曾经的少年回来了
来源:juejin.cn/post/7181095134758387773
收起阅读 »

工作 7 年的老程序员,现在怎么样了

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。 我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个...
继续阅读 »

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。


我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个人组队玩,那会觉得很嗨,上头。


后来看室友在玩魔兽世界,那会不知道是什么游戏,就感觉很好玩,再后来就入坑了。还记得刚开始玩,完全不会,玩个防骑,但是打副本排DPS,结果还被人教育,教育之后还不听(因为别的职业不会玩),就经常被 T 出组。之后,上课天天看游戏攻略和玩法,或者干脆看小说。前两年就这么过去了


1 跟风考研


大三开始,觉得这么混下去不行了。在豆瓣上找了一些书,平时不上课的时候找个自习室学习。那会家里打电话说有哪个亲戚家的孩子考研了,那是我第一次知道“考研”这个词。那会在上宏微观经济学的课,刚好在豆瓣上看到一本手《牛奶面包经济学》,就在自习室里看。刚好有个同院系的同学在里面准备考研,在找小伙伴一起战斗(毕竟,考研是一场长跑,没有同行者,会很艰难)。我一合计,就加入了他们的小团队。从此成为“中国合伙人”(刚好四个人)中的一员。


我那会也不知道毕业了之后能去哪些公司,能找哪些岗位,对于社会完全不了解,对于考研也是完全不了解。小团队中的三个人都是考金融学,我在网上查,知道了学硕和专硕的区别,也知道专硕学费贵。我家里没钱,大学时期的生活费都是自己去沃尔玛、麦当劳、发传单挣得,大学四年,我在沃尔玛工作超过了 2 年、麦当劳半年,食堂倒盘子半年,中途还去发过传单,暑假还去实习。没钱,他们考金融学专硕,那我就靠经济学学硕吧,学硕学费便宜。


从此开始了考研之路。


2 三次考研


大三的时候,报名不是那么严格,混进去报了名,那会还没开始看书,算是体验了一把考研流程;


还记得那次政治考了 48 分,基本都过了很多学校的单科线,那会就感觉政治最好考(最后发现,还是太年轻)。


大四毕业那年,把所有考研科目的参数书都过了 2 遍,最后上考场,最后成绩也就刚过国家线。


毕业了,也不知道干啥,就听小伙伴的准备再考一次,之前和小伙伴一起来了北京,租了个阳台,又开始准备考研。结果依然是刚过国家线。这一年也多亏了一起来北京的几个同学资助我,否则可能都抗不过考试就饿死街头了。


总结这几次考研经历,失败的最大原因是,我根本不知道考研是为了什么。只是不知道如果工作的话,找什么工作。刚好别人提供了这样一个逃避工作的路,我麻木的跟着走而已。这也是为什么后面两次准备的过程中,一有空就看小说的原因。


但是,现在来看,我会感谢那会没有考上,不然就错过了现在喜欢的技术工作。因为如果真的考上了经济学研究生,我毕业之后也不知道能干啥,而且金融行业的工作我也不喜欢,性格上也不合适,几个小伙伴都是考的金融,去的券商,还是比较了解的。


3 入坑 JAVA 培训


考完之后,大概估了分,知道自己大概率上不了就开始找工作了。那会在前程无忧上各种投简历。开始看到一个做外汇的小公司,因为我在本科在一个工作室做过外汇交易相关的工作,还用程序写了一段量化交易的小程序。


所以去培训了几天,跟我哥借了几千块钱,注册了一个账号,开始买卖外汇。同时在网上找其他工作。


后面看介绍去西二旗的一家公司面试,说我的技术不行,他们提供 Java 培训(以前的套路),没钱可以贷款。


我自己也清楚本科一行 Java 代码没写过,直接工作也找不到工作。就贷款培训了,那会还提供住宿,跟学校宿舍似的,上下铺。


4 三年新手&非全研究生


培训四个月之后,开始找工作。那会 Java 还没这么卷,而且自己还有个 211 学历,一般公司的面试还是不少的。但是因为培训的时候学习不够刻苦(也是没有基础)。最后进了一个小公司,面试要 8000,最后给了 7000。这也是我给自己的最底线工资,少于这个工资就离开北京了,这一年是 2015 年。


这家公司是给政府单位做内部系统的,包括中石油、气象局等。我被分配其中一个组做气象相关系统。第二年末的时候,组内的活对我来说已经没什么难度了,就偷偷在外面找工作,H3C 面试前 3 面都通过了,结果最后大领导面气场不符,没通过。最后被另外一家公司的面试官劝退了。然后公司团建的时候,大领导也极力挽留我,最后没走成。


这次经历的经验教训有 2 个,第 1 个是没有拿到 offer 之前,尽量不要被领导知道。第 2 个是,只要领导知道你要离职,就一定要离职。这次就是年终团建的时候,被领导留下来了。但是第二年以各种理由不给工资。


之前自己就一直在想出路,但是小公司,技术成长有限,看书也对工作没有太大作用,没有太大成长。之后了解到研究生改革,有高中同学考了人大非全。自己也就开始准备非全的考试。最后拿到录取通知书,就开始准备离职了。PS:考研准备


在这家公司马上满 3 年重新签合同的时候,偷偷面试了几家,拿到了 2 个还不错的 offer。第二天就跟直属领导提离职了。这次不管直属领导以及大领导如何劝说,还是果断离职了。


这个公司有两个收获。一个是,了解了一般 Java Web 项目的全流程,掌握了基本开发技能,了解了一些大数据开发技术,如Hadoop套件。另外一个是,通过准备考研的过程,也整理出了一套开发过程中应该先思而后行。只有先整理出


5 五年开发经历


第二家公司是一家央企控股上市公司,市场规模中等。主要给政府提供集成项目。到这家公司第二年就开始带小团队做项目,但是工资很低,可能跟公司性质有关。还好公司有宿舍,有食堂。能省下一些钱。


到这家公司的时候,非全刚好开始上课,还好我们 5 点半就下班,所以我天天卡点下班,大领导天天给开发经理说让我加班。但是第一学期要上课,领导对我不爽,也只能这样了。


后来公司来了一个奇葩的产品经理,但是大领导很挺他,大领导下面有 60 号人,研发、产品、测试都有。需求天天改,还不写在文档上。研发都开发完了,后面发现有问题,要改回去,产品还问,谁让这么改的。


是否按照文档开发,也是大领导说的算,最后你按照文档开发也不对,因为他们更新不及时;不按照文档开发也不对,写了你不用。


最后,研发和产品出差,只能同时去一波人,要是同时去用户现场,会打架。最后没干出成绩,产品和大领导一起被干走了。


后面我们整体调整了部门,部门领导是研发出身。干了几个月之后,领导也比较认可我的能力,让我带团队做一个中型项目,下面大概有 10 号人,包括前后端和算法。也被提升为开发经理。


最后因为工资、工作距离(老婆怀孕,离家太远)以及工作内容等原因,跳槽到了下一家互联网公司。


6 入行互联网


凭借着 5 年的工作经历,还算可以的技术广度(毕竟之前啥都干),985 学校的非全研究生学历,以及还过得去的技术能力。找到了一家知名度还可以的互联网公司做商城开发。


这个部门是公司新成立的部门,领导是有好几家一线互联网经验的老程序员,技术过硬,管理能力强,会做人。组内成员都年轻有干劲。本打算在公司大干一场,涨涨技术深度(之前都是传统企业,技术深度不够,但是广度可以)。


结果因为政策调整,整个部门被裁,只剩下直属领导以及领导的领导。这一年是 2020 年。这个时候,我在这个公司还不到 1 年。


7 再前行


拿着上家公司的大礼包,马上开始改简历,投简历,面试,毕竟还有房贷要还(找了个好老婆,她们家出了大头,付了首付),马上还有娃要养,一天也不敢歇息。


经过一个半月的面试,虽然挂的多,通过的少。最终还是拿了 3 个不错的offer,一个滴滴(滴滴面经)、一个XXX网络(最终入职,薪资跟滴滴基本差不多,技术在市场上认可度也还不错。)以及一个建信金科的offer。


因为大厂部门也和上家公司一样,是新组建的部门,心有余悸。然后也还年轻,不想去银行躺平,也怕银行也不靠谱,毕竟现在都是银行科技公司,干几年被裁,更没有出路。最终入职XXX网络。


8 寒冬


入职XXX网络之后,开始接入公司的各种技术组件,以及看到比较成熟的需求提出、评估、开发、测试、发布规范。也看到公司各个业务中心、支撑中心的访问量,感叹还是大公司好,流程规范,流量大且有挑战性。


正要开心的进入节奏,还没转正呢(3 个月转正),组内一个刚转正的同事被裁,瞬间慌得一批。


刚半年呢,听说组内又有 4 个裁员指标,已经开始准备简历了。幸运的是,这次逃过一劫。


现在已经 1 年多了,在这样一个裁员消息满天飞的年代,还有一份不错的工作,很幸运、也很忐忑,也在慢慢寻找自己未来的路,共勉~


9 总结


整体来看,我对自己的现状还算满意,从一个高中每个月 300 块钱生活费家里都拿不出来;高考志愿填报,填学校看心情想去哪,填专业看专业名字的的村里娃,走到现在在北京有个不错的工作,组建了幸福的家庭,买了个不大不小的房子的城里娃。不管怎么样,也算给自己立足打下了基础,留在了这个有更多机会的城市;也给后代一个更高的起点。


但是,我也知道,现在的状态并不稳固,互联网工作随时可能会丢,家庭成员的一场大病可能就会导致整个家庭回到解放前。


所以,主业上,我的规划就是,尽力提升自己的技术能力和管理能力,争取能在中型公司当上管理层,延迟自己的下岗年龄;副业上,提升自己的写作能力,尝试各种不同的主题,尝试给各个自媒体投稿,增加副业收入。


希望自己永远少年,不要下岗~



作者:六七十三
来源:juejin.cn/post/7173506418506072101
收起阅读 »

一个97年的前端卷不动了,跑去学瑜伽?

大家好, 我是刘子弃。现在是23年5月, 裸辞已经快两个月了。 前两天看到行业前辈左耳朵耗子的不幸消息。 突然有一个想把自己这两个月来的心路历程记录一下的想法。 23年3月, 一个倒霉的周五 那是3月的一天周五,周五本来是打工人比较快乐的一天,但是还没上班就...
继续阅读 »

大家好, 我是刘子弃。现在是23年5月, 裸辞已经快两个月了。 前两天看到行业前辈左耳朵耗子的不幸消息。 突然有一个想把自己这两个月来的心路历程记录一下的想法。



23年3月, 一个倒霉的周五


那是3月的一天周五,周五本来是打工人比较快乐的一天,但是还没上班就预示那天的不平凡。 刚起床准备上班。匆忙的洗漱吃片面包就冲向地铁。刚到地铁口发现手机坏了, 读不出SIM卡。重启也无济于事。


先回家连上wifi到公司群说明情况。 就奔向家附近唯一一家手机维修点。 好不容易到了之后发现今天居然不营业。 只好去旁边花几百块买了一个红米备用机(感谢红米)。


一路坎坷到了公司,通知今天周会要宣布一个大事情。 我们项目组做的是web3相关的业务(我非常热爱这个项目组和每天做的工作内容!)。 之前就有风传出来要去香港落地。 当时还激动了一下, 结果周会宣布:“我们解散啦!”


开始毕业


由于项目突然黄了。 就要考虑转岗或者拿大礼包的事情。 显而易见我选择了拿了大礼包。 为什么不去转岗到其他组呢?其中有对前端这个方向未来发展的考虑, 最重要的考虑还是健康吧。 因为生活不规律, 陆陆续续出现过如下几种身体情况:



  1. 胸口痛

  2. 神经衰弱

  3. 颈椎痛

  4. 失眠

  5. 突然来一下全身无力

  6. 注意力难集中

  7. 脑鸣

  8. 鬼压床看到各种幻觉,甚至有几次都感觉魂都快飘出来了。


就这样, 毫无规划的我就毕业了。


计划恢复健康


本以为不上班了会好一点, 结果每天的状态还是比较差。 去体检万幸没有什么大问题。 不幸的是症状还是一如既往的存在。 最后实在不不行就去了精神科果然有了一点轻度的抑郁症。 所以在决定之后做什么之前, 我决定先养好身体, 恢复健康。 毕竟身体是最重要的。


开始卷起了瑜伽教培


由于之前接触过一些身心灵行业的人。 也有过一些冥想的经验和经历。 在健身和身心灵两个方向中我选择了一个最兼顾和均衡的方向就是练习瑜伽。 索性直接报了一个教培班。 一是恢复身体。二是系统的学习一下防止受伤、更快的练习、避免不正当的危险操作。 并且最后可以拿到认证证书打算之后作为一个长久的职业方向发展一下。 就这样报名了瑜伽教培。 现在已经完成了200小时认证, 6月完成500小时认证。目前身体经过训练确实基本健康了, 症状都没有了。身体舒适了不少。 (或许是不上班都会健康哈哈哈)。 精神也放松了许多, 每天早上起来舒爽+ 没有压力的感觉终于又回来了。


11ca0851fc75aad27b66c4510731059.jpg


之后做什么


当然还是需要考虑收入的问题。 裸辞之后, 如果不干程序员了去干什么。我想不止我一个人想过这个问题。 跑滴滴? 外卖员? 对我来说有点不现实。 但是可能也由不得我选择。现在的就业情况能有一个offer就乐上天了。 所以我想做一个实验。 借各位大佬的光。如果您也想过类似的问题。可以把建议告诉我。 我去实际操作一下。 然后再反馈给大家。


作者:刘子弃
来源:juejin.cn/post/7233589699215147069
收起阅读 »

优 雅 被 裁

后疫情时代的影响,互联网行业每况愈下,而重庆这个地方更是互联网荒漠一般的存在。 上一份工作换的时间是 2022 年的 6 月,仅仅过了一年 3 个月,我又要换工作了,不过这次是被动的。 💡 希望我的经历能给那些正在经历同样遭遇的打工人提供一些参考和启示。 第一...
继续阅读 »

后疫情时代的影响,互联网行业每况愈下,而重庆这个地方更是互联网荒漠一般的存在。


上一份工作换的时间是 2022 年的 6 月,仅仅过了一年 3 个月,我又要换工作了,不过这次是被动的。


💡 希望我的经历能给那些正在经历同样遭遇的打工人提供一些参考和启示。


第一章 - 裁员来袭,初尝挫败



“最初,没有人在意这场灾难,这不过是一场山火,一次旱灾,一个物种的灭绝,一座城市的消失。直到这场灾难和每个人息息相关。” ——《流浪地球》



这份工作开始于 2022 年 6 月 7 日,当时面了挺多公司,那时的市场还算可以,还没有彻底到寒冬,所以手上的 offer 还能让我选选。


这个公司是当时来说给的最高的,理所当然的选了。(岗位高级前端


回看,公司很早就有裁员的预兆 🔍



  1. 22 年 年终奖金没有发全(只发了一半

  2. 业务收缩,大幅缩减新业务拓展

  3. 实习生转正率只有一半

  4. 第一波裁员,开始裁实习生

  5. 开始将开发人员的工作转交给实习生

  6. 第二波裁员,开发、产品 7. 第三波裁员,测试 8. 第四波,到我咯


⛔ 我之前就有了快要到斩杀线的感觉,


因为当你要被裁的时候,你头上就会出现 “危” 字。很难不察觉


第二章 - 未雨绸缪,寻找下家



“我希望你没有把全部鸡蛋放在一个篮子里。” ——《华尔街》



感觉到危险的气息是因为手上本就不多的工作突然加入了同事来接手。


随更新简历 📄,打开求职状态。


😢 不得不说,现在的市场真的偏向用人单位。


在去年的 6 月,每天基本能有 3-5 个 HR 主动来找我询问,当时的公司我还能选择性去面。


而今年的 9 月,基本两周才能有 3-5 个 Hr 来找我,加之现在市面上公司少之又少,刷来刷去就是那么几家。


期间一共面了三家,两家 offer。


一家上升期的公司,注重业务扩展,规模 500 左右。但是之前有朋友在里面说管理混乱加班严重,💩 屎山代码成堆,于是放弃。


另一家人员较少。技术部门 20 人不到,业务跨境金融。无需加班,管理扁平。感觉还不错。


与此同时,由于没有明确的说要裁我。每天都过的很焦虑,一想到 💰 房贷、车贷、房租水电、还有臭宝一堆花销 就开始掉头发。(本来头发就不多


也问过直系领导,但是问了个寂寞,就说人员一直都有在调整。。。


只能两手准备。


拿到 offer 的后心里踏实了不少,但是一直不说什么时候裁,搞得我没法回复那边。


最后找到 “线人” 去问了大领导,确定了月底上完(还好兄弟我平时人缘还不错


第三章 - 运气不错,无缝衔接



“看,前面漆黑一片,什么也看不到。”


“也不是,天亮后会很美的。”


——《喜剧之王》



听到了可靠消息,确定了新公司的入职时间,心里踏实了很多。


过了几天后,领导找了我私聊确定了国庆最后一天走人。


替代文本

赔偿 n+1,自己提离职,钱跟着工资一起发。


替代文本

不知道大家之前在网上看到过一个说法没:不要自己提离职,不要签字,不然赔偿拿不到。


其实心里也挺慌的,问了之前的同事:也是自己提的离职,赔偿给够。


所以就按照流程走了。希望公司还是能遵守约定吧


新公司 10 月 9 号入职。十一期间我可以多休息两天,准备把老头环好好玩玩。


对了,新公司还涨了一些,已经很知足啦。


期待能在新公司能干出一些值得骄傲的项目。


终章 - 自我反思,相信希望



“人生总是这么苦么,还是只有小时候?”


“总是如此。”


——《这个杀手不太冷》



浅浅总结一下,当然只针对我个人体质,不适合所有人哦



  • 现在的环境很复杂,最好每年都去市场上检验一下自己的价值。

  • 如果突然手上的活被人接手了,要警惕

  • 关注一下公司的财务状况,不要被打个措手不及

  • 尽量放平心态,不要再给自己增加压力啦,难度已经很高了

  • 不要放弃学习,舒适圈里待太久会丧失动力

  • 好好活着,这条适合所有人🦾


作者:前端小蜗
来源:juejin.cn/post/7283151314024497209
收起阅读 »

最近的生活

上一篇文章是8.4号写的,一个多月没有写东西了,按照现在的情况估计,年底要写到40篇有点悬,虽然有很多要写的,但现在事情太多了也太累了。今天写点随笔,写到哪算哪吧。 工作 这次参与的新项目,前景还是不错的,不过活也是真多。这段日子,很多时候十一二点才下班,有的...
继续阅读 »

上一篇文章是8.4号写的,一个多月没有写东西了,按照现在的情况估计,年底要写到40篇有点悬,虽然有很多要写的,但现在事情太多了也太累了。今天写点随笔,写到哪算哪吧。


工作


这次参与的新项目,前景还是不错的,不过活也是真多。这段日子,很多时候十一二点才下班,有的时候搞到两点,周末再加一天班。


业务快速发展,不断地有新同学加入,架构也在不断迭代,其实感觉还是蛮不错的,有点像当初刚工作负责电商的时候了,始终创业啊。


雷总曾经说过,要顺势而为,其实是对的,选对方向往往事半功倍,但选对方向需要极强的能力。新的变革已经到来了。


文化


这里的文化是指公司文化,扩展一下也指家庭文化。为什么突然聊文化?


最近感觉无论是家庭还是公司,让大家聚集在一起努力的,相同的文化或者三观是重要的一环。文化认同不一致,很难长久的在一起,这个没有对错,每个人都有选择的权利,没必要强求,很多时候祝福就好。


还是想夸一下字节的文化,虽然看过很多公司的文化宣言,感觉字节的带着哲思在里面,这种文化不是只对公司有利,而是说在自己的生活中,用这种文化来要求自己也是好的。认同这种文化的人在一起,办事效率、质量要高很多,很多时候,损失来自于内耗。


1.1追求极致


不断提高要求,延迟满足感


在更大范围里找最优解


不放过问题,思考本质


持续学习和成长


1.2务实敢为


直接体验,深入事实


不自嗨,注重效果


能突破有担当,打破定式


尝试多种可能,快速迭代


1.3开放谦逊


内心阳光,信任伙伴


乐于助人和求助,合作成大事


格局大,上个台阶想问题


对外敏锐谦虚,ego(自我) 小,听得进意见


1.4坦诚清晰


敢当面表达真实想法


能承认错误,不装不爱面子


实事求是,暴露问题,反对“向上管理”


准确、简洁、直接,有条理有重点


1.5始终创业


自驱,不设边界,不怕麻烦


有韧性,直面现实并改变它


拥抱变化,对不确定性保持乐观


始终像公司创业第一天那样思考


1.6多元兼容


理解并重视差异和多元,建立火星视角


打造多元化的团队,欢迎不同背景的人才,激发潜力


鼓励人人参与,集思广益,主动用不同的想法来挑战自己


创造海纳百川,兼容友好的工作环境


教育


最近在想,怎么教育好下一代?或者话题小一点,如何在知道A选项不好的情况下,让子女听自己的?


以前看过一篇文章,说是孩子们总归不会听你的,但他们也终会在跌跌撞撞中长大,然后他们的子女再来一次循环。


但我觉得,还是有可能教育好的,不过要付出很多,这是一个细雨润无声,充斥在点点滴滴生活中的事情,它永远不是一个一次性任务,或者说几次道理就能达成的。


拿选择来说,需要做到

  1. 父母本身就对每种选择的结果比较知晓

  2. 父母很了解子女的性格和能力

  3. 子女相对相信父母

  4. 或者 子女已经培养的很好了,有了自己的主见和三观,知道自己的性格和能力


如果能培养到第四点,那真是轻松很多。不过呀,最重要的还是得立志,论语里说:“不愤不启,不悱不发,举一隅不以三隅反,则不复也。”也是这个道理。立志能给人以动力,自己主观上想干了,才能干好。


家庭


最近媳妇工作上的事情也比较多,我感觉很神奇,好像每次事情都会像商量好似得一起来,这时候考验的就是毅力和耐力,不松气,努力干,总能顶过这一波。


或许真像媳妇说的,人生就像一场游戏,努力就完事了,别想太多。


前些日子和媳妇都阳了,好在不太严重,也不知道什么时候是个头。看到满满的小药箱,比起去年12月的时候,还是感觉安全一些的。


哦,对了,前些日子公司冷藏柜漏水,导致我摔了一跤,电脑都飞出去了。怎么说呢,幸亏电脑没事,就人伤着一点,哈哈哈。本来想投诉一下,但负责人一直在会议室门口等我们会议结束,又道歉又拿药,加了联系方式方便后面有问题及时联系;同时讲了原因和后续的改进措施。做的挺不错的。


以前对摔倒的影响概念不深,现在倒蛮有体会的了,有时候在想,如果六七十岁的人,以这个力道被摔,真的很危险。大家还是要多多注意。


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

职场坐冷板凳的那些日子

曾经有一段职场生涯,坐了很长时间的冷板凳,也正是那段经历,彻底改变了整个职场生涯。今天这篇文章聊聊自己曾经的经历,也聊聊如果在职场中被坐了冷板凳该咋办。 关于冷板凳 有人的地方就有江湖。而这个江湖中是否性情相同,是否因某些事(或利益)产生矛盾,都可能造成职场坐...
继续阅读 »

曾经有一段职场生涯,坐了很长时间的冷板凳,也正是那段经历,彻底改变了整个职场生涯。今天这篇文章聊聊自己曾经的经历,也聊聊如果在职场中被坐了冷板凳该咋办。


关于冷板凳


有人的地方就有江湖。而这个江湖中是否性情相同,是否因某些事(或利益)产生矛盾,都可能造成职场坐冷板凳的情况。


冷板凳常见于上级对下级的打压。一般手段就是让你无所事事或安排一些边缘性的事务,不怎么搭理你,从团队层面排挤你,甚至否定你或PUA你,别人也不敢跟你沟通,以至于让你在团队中形成孤立的的状态。


根据矛盾或冲突的不同,冷板凳的程度也不同。常见的有:浅层次的冲突,可进行修复;不可调和,无法修复;中间的灰度状态。


通常根据具体情况,判断程度,有没有可能或必要修复,再决定下一步的行动。


第一,可修复的冷板凳


有很多同学,特别是技术人,在职场上有时候特别的“刚”,为了某个技术点跟领导争的面红耳赤的,导致被坐冷板凳。


比如有同学曾有这样的经历:领导已经拍板的决定,他很刚的去跟领导据理力争,导致起了冲突,大吵一架,领导也下不来台。随后领导好几天没搭理他。


针对这种情况,一般也就是一顿火锅的事,找领导主动沟通,重拾信任。甚至可能会出现不打不相识的情况。当然,一顿火锅不够还可以两顿。


第二,清场性质的冷板凳


这种情况常见于业绩或能力不达标,已经是深层次的矛盾,一般会空降过来一个领导,故意将其边缘化。属于清场接替工作性质的,基本上无法修复。


针对这种情况,看清局势,准备好找下家就是了。如果做得好,准备好交接工作,给彼此一个体面。毕竟,很多事情我们是无法改变的。


第三,灰度状态的冷板凳


以上两个常见都比较极端,而大多数情况下都是灰度状态的,大的可能性就是一直僵持着。这时作为下属的人,一般建议主动去沟通、修复。


如果阅历比较浅,看不出中间的微妙关系以及深层次的冲突点,就请人帮你看看,听听别人的建议和决策。再决定值不值得修复,要不要修复。


我的冷板凳


曾经我在一家公司坐的冷板凳属于第三种,但却把这个冷板凳坐到了极致。下面就讲讲我曾经的故事。


跟着一个领导到一家新公司,本来领导带领技术部门的,但由于内部斗争的失利,去带产品团队了,而我也归属到他对手的手下了。这种情况下,冷板凳是坐定了,但也不至于走人。


被新领导安排了一个很边缘的业务:对接和维护一套三方的系统。基本上处于不管不问,开会不带,接触不到核心,也与其他人无交流的状态。起初这种状态非常难受,人毕竟是社群动物,需要一个归属感和存在感的。


但慢慢的,自己找到了一些属于自己的乐趣。


首先,没人管没人问,那就可以自己掌控节奏和状态了。看他们天天加班到凌晨一两点,而自己没人管,六七点就下班了。最起码在那段持续疯狂加班的岁月里,自己保住了头发。那位大领导后来加班太多,得了重病,最终位置也没保住。


其次,有了大把的时间。上班几乎没人安排工作,于是上班的时间完全自己安排。三方服务商安排了对接人,好歹自己作为甲方,于是天天就跟服务商的技术沟通,询问他们系统的设计实现,技术栈什么的。


在那段岁月里,完成了几个改变后续职场生涯的事项。


事项一:那时Spring Boot 1.5刚刚发布,公司的技术栈还没用上,但服务商的这套系统已经用上了。感觉这玩意太好用了,于是疯狂的学学习。因为当初的学习,后来出版了书籍《Spring Boot技术内幕》那本书。


事项二:写技术博客,翻译技术文档,录技术视频。服务商的系统中还用到了规则引擎,当时市面上没有相关的中文资料。于是边跟对方技术沟通,边翻译英文文档,写博客。后来,还把整理的文档录制成视频,视频收入有几万块吧。


这算是自己第一次尝试翻译文档、录制教学视频,而且这个领域网络上后续的很多技术文章都是基于我当初写文章衍生出来的。最近,写的第二本书便是关于规则引擎的,坐等出版了。


事项三:学习新技术,博客输出。当时区块链正火爆时。由于有大量的时间,于是就研究起来了,边研究边写技术博客。也是在这个阶段,养成了写技术博客的习惯。


因为区块链的博客,也找到了下家工作。同时写了CSDN当时类似极客时间的“Chat”专栏,而且是首批作者。也尝试搞了区块链的知识星球。后来,因为区块链的工作,做了第一次公开课的分享。还是因为区块链相关,与别人合著了一本书,解释了出版社的老师,这也是走上出书之路的开始。


因为这次冷板凳,让职场生涯变得极其丰富,也扭转了大的方向,发展了副业,接触了不同行业领域的人。


最后的小结


在职场混,遇到坐冷板凳的情况不可避免,但如何化解,如何抉择却是一个大学问。尽量主动沟通,毕竟找工作并不容易,也不能保证下家会更好。同时,解决问题,也是人生成长的一部分,所以,尽量尝试化解。


但如果矛盾真的不可调和或持续僵持,那么就更好做好决策,选择对自己最有利的一面。


曾在朋友圈发过这样一段话,拿来与大家分享:


“始终难守樊登讲过的一句话:人生成长最有效的方法,就是无论命运把你抛在任何一个点上,你就地展开做力所能及的事情。


如果还要加上一句,那就是:还要占领制高点。与君共勉~”


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

UIButton 扩大点击区域

iOS
在开发过程中经常会遇到设计给出的button尺寸偏小的情况.这种UIButton在使用中会非常难点击,极大降低了用户体验 解决方案一:重写UIButton的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEven...
继续阅读 »

在开发过程中经常会遇到设计给出的button尺寸偏小的情况.这种UIButton在使用中会非常难点击,极大降低了用户体验


解决方案一:重写UIButton的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event

{

//获取当前button的实际大小
CGRect bounds = self.bounds;

//若原热区小于44x44,则放大热区,否则保持原大小不变

CGFloat widthDelta = MAX(44.0 - bounds.size.width, 0);

CGFloat heightDelta = MAX(44.0 - bounds.size.height, 0);
//扩大bounds

bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);

//如果点击的点 在 新的bounds里,就返回YES

return CGRectContainsPoint(bounds, point);

}

系统默认写法是:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return CGRectContainsPoint(self.bounds, point);
}

其实是在判断的时候对响应区域的bounds进行了修改.CGRectInset(view, 10, 20)方法表示对rect大小进行修改


解决方案二 runtime关联对象来改变范围,- (UIView) hitTest:(CGPoint) point withEvent:(UIEvent) event里用新设定的 Rect 来当着点击范围。

#import "UIButton+EnlargeTouchArea.h"
#import <objc/runtime.h>

@implementation UIButton (EnlargeTouchArea)

static char topNameKey;
static char rightNameKey;
static char bottomNameKey;
static char leftNameKey;

- (void)setEnlargeEdgeWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left
{
objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void)setTouchAreaToSize:(CGSize)size
{
CGFloat top = 0, right = 0, bottom = 0, left = 0;

if (size.width > self.frame.size.width) {
left = right = (size.width - self.frame.size.width) / 2;
}

if (size.height > self.frame.size.height) {
top = bottom = (size.height - self.frame.size.height) / 2;
}

[self setEnlargeEdgeWithTop:top right:right bottom:bottom left:left];
}

- (CGRect)enlargedRect
{
NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);
NSNumber *rightEdge = objc_getAssociatedObject(self, &rightNameKey);
NSNumber *bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
NSNumber *leftEdge = objc_getAssociatedObject(self, &leftNameKey);
if (topEdge && rightEdge && bottomEdge && leftEdge)
{
return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
self.bounds.origin.y - topEdge.floatValue,
self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
}
else
{
return self.bounds;
}
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect rect = [self enlargedRect];
if (CGRectEqualToRect(rect, self.bounds) || self.hidden)
{
return [super hitTest:point withEvent:event];
}
return CGRectContainsPoint(rect, point) ? self : nil;
}

@end


解决方案三:使用runtime swizzle交换IMP

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error = nil;
[self hg_swizzleMethod:@selector(pointInside:withEvent:) withMethod:@selector(hitTest_pointInside:withEvent:) error:&error];
NSAssert(!error, @"UIView+HitTest.h swizzling failed: error = %@", error);
});
}

- (BOOL)hitTest_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
return [self hitTest_pointInside:point withEvent:event];
}
CGRect relativeFrame = self.bounds;
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}



category的诞生只是为了让开发者更加方便的去拓展一个类,它的初衷并不是让你去改变一个类。



技术点总结


关联对象,也就是绑定对象,可以绑定任何东西

//关联对象
objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
// self 关联的类,
//key:要保证全局唯一,key与关联的对象是一一对应关系。必须全局唯一
//value:要关联类的对象。
//policy:关联策略。有五种关联策略。
//OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
//OBJC_ASSOCIATION_RETAIN_NONATOMIC等价于 @property(strong, //nonatomic)。
//OBJC_ASSOCIATION_COPY_NONATOMIC等价于@property(copy, nonatomic)。
//OBJC_ASSOCIATION_RETAIN等价于@property(strong,atomic)。
//OBJC_ASSOCIATION_COPY等价于@property(copy, atomic)。

NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);

// 方法说明
objc_setAssociatedObject 相当于 setValue:forKey 进行关联value对象

objc_getAssociatedObject 用来读取对象

objc_AssociationPolicy 属性 是设定该value在object内的属性,即 assgin, (retain,nonatomic)...等

objc_removeAssociatedObjects 函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。

方法交换 Method Swizzling 注意点


对于已经存在的类,我们通常会在+load方法,或者无法获取到类文件,我们创建一个分类,也通过其+load方法进行加载swizzling


  • Swizzling应该总在+load中执行
  • Swizzling应该总是在dispatch_once中执行
  • Swizzling在+load中执行时,不要调用[super load]。如果多次调用了[super load],可能会出现“Swizzle无效”的假象。

交换实例方法


以class为类

void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
{
//class_getInstanceMethod(),如果子类没有实现相应的方法,则会返回父类的方法。
Method originMethod = class_getInstanceMethod(class, originalSEL);
Method replaceMethod = class_getInstanceMethod(class, replacementSEL);

//class_addMethod() 判断originalSEL是否在子类中实现,如果只是继承了父类的方法,没有重写,那么直接调用method_exchangeImplementations,则会交换父类中的方法和当前的实现方法。此时如果用父类调用originalSEL,因为方法已经与子类中调换,所以父类中找不到相应的实现,会抛出异常unrecognized selector.
//当class_addMethod() 返回YES时,说明子类未实现此方法(根据SEL判断),此时class_addMethod会添加(名字为originalSEL,实现为replaceMethod)的方法。此时在将replacementSEL的实现替换为originMethod的实现即可。
//当class_addMethod() 返回NO时,说明子类中有该实现方法,此时直接调用method_exchangeImplementations交换两个方法的实现即可。
//注:如果在子类中实现此方法了,即使只是单纯的调用super,一样算重写了父类的方法,所以class_addMethod() 会返回NO。

//可用BaseClass实验
if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod)))
{
class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else {
method_exchangeImplementations(originMethod, replaceMethod);
}
}


这里存在的问题是继承时子类没有实现父类方法的问题:
基类A类 有方法 -(void)test
子类B类继承自基类A,但没有重写test方法,即其类[B class]中没有test这个实例方法
当我们交换子类B中的方法test,交换为testRelease方法(这必然会在子类B中写testRelease的实现),子类B中有没有test方法的实现时,就会将基类A的test方法与testRelease替换,当仅仅使用子类B时,不会有问题。
但当我们使用基类A的test方法时,由于test指向的IMP是原testRelease的IMP,而基类A中没有这个实现,因为我们是写在子类B中的。所以就出现了unrecognized selector



交换类方法


由于类方法存储在元类中,以实例方法存在,所以实质就是交换元类的实例方法
上面交换实例方法基础上,传入cls为元类即可。
获取的元类可以这样objc_getMetaClass("ClassName")或者object_getclass([NSObject class])


事件响应者链


如图所示,不再赘述



 两个重要的方法

- (nullable UIView*)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;称为方法A

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;称为方法B

对view进行重写这两个方法后,点击屏幕后,首先响应的是方法A;

  • 如果方法A中,我们没有调用父类([super hitTest:point withEvent:event];)的这个方法,那就根据这个方法A的返回view,作为响应事件的view。(当然返回nil,就是这个view不响应)

  • 如果方法A中,我们调用了父类的方法([super hitTest:point withEvent:event];)那这个时候系统就要调用方法B;通过这个方法的返回值,来判断当前这个view能不能响应消息

  • 如果方法B返回的是no,那就不用再去遍历它的子视图。方法A返回的view就是可以响应事件的view。

  • 如果方法B返回的是YES,那就去遍历它的子视图。(就是上图我们描述的那样,找到合适的view返回,如果找不到,那就由方法A返回的view去响应这个事件。)


总结


返回一个view来响应事件 (如果不想影响系统的事件传递链,在这个方法内,最好调用父类的这个方法)

- (nullable UIView*)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event{
    return [super hitTest:point withEvent:event];
}

返回的值可以用来判断是否继续遍历子视图(返回的根据是触摸的point是否在view的frame范围内)

- (BOOL)pointInside:(CGPoint)point withEvent:(nullableUIEvent *)event;      

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

手把手教你集成环信ReactNative离线推送(下)

点此链接查看:手把手教你集成环信ReactNative离线推送(上)三、从原生将device_token 传到RN 并且绑定1、原生调用方法 reactContext.getJSModule(DeviceEventManagerModule.RCTDevice...
继续阅读 »

点此链接查看:手把手教你集成环信ReactNative离线推送(上)

三、从原生将device_token 传到RN 并且绑定

1、原生调用方法


reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("deviceToken",jsonObject.toString());

通过PushModule 类进行传递,PushModule 代码如下:


package com.awesomeproject;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.xiaomi.mipush.sdk.MiPushClient;
import org.json.JSONException;
import org.json.JSONObject;

public class PushModule extends ReactContextBaseJavaModule {
private ReactApplicationContext reactContext;
public PushModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@Override
public String getName() {
return "PushModule";
}
/**
从RN界面里面调用该方法
**/

@ReactMethod
public void getDeviceToken(){
MainApplication.getReactPackage().mModule.sendDataToJS( MiPushClient.getRegId(MainApplication.getContext()));


}

public void sendDataToJS(String deviceToken){
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("deviceToken",deviceToken);
jsonObject.put("deviceName","2882303761517520571");

} catch (JSONException e) {
throw new RuntimeException(e);
}

this.reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("deviceToken",jsonObject.toString());
}



}

2、RN 层进行获取数据


NativeModules.PushModule.getDeviceToken();
DeviceEventEmitter.addListener('deviceToken',(res)=>{
const goosid = JSON.parse(res);
deviceToken = goosid.deviceToken;
manufacturer = goosid.deviceName;
console.log('React Native界面,收到数据:',goosid);

3、获取到数据后调用环信RN sdk 方法进行绑定

ChatClient.getInstance().updatePushConfig(push);

js 代码如下

// 导入依赖库
import React, { useEffect } from 'react';
import {
DeviceEventEmitter,
NativeModules,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import {
ChatClient,
ChatMessage,
ChatMessageChatType,
ChatOptions,
ChatPushConfig,
} from 'react-native-chat-sdk';
// 创建 app
const App = () => {
// 进行 app 设置
const title = 'ChatQuickstart';
var deviceToken='';
var manufacturer='';
NativeModules.PushModule.getDeviceToken();
DeviceEventEmitter.addListener('deviceToken',(res)=>{
const goosid = JSON.parse(res);
deviceToken = goosid.deviceToken;
manufacturer = goosid.deviceName;
console.log('React Native界面,收到数据:',goosid);

})
const [appKey, setAppKey] = React.useState('1137220225110285#demo');
const [username, setUsername] = React.useState('p9');
const [password, setPassword] = React.useState('1');
const [userId, setUserId] = React.useState('');
const [content, setContent] = React.useState('');
const [logText, setWarnText] = React.useState('Show log area');

// 输出 console log 文件
useEffect(() => {
logText.split('\n').forEach((value, index, array) => {
if (index === 0) {
console.log(value);
}
});
}, [logText]);

// 输出 UI log 文件
const rollLog = text => {
setWarnText(preLogText => {
let newLogText = text;
preLogText
.split('\n')
.filter((value, index, array) => {
if (index > 8) {
return false;
}
return true;
})
.forEach((value, index, array) => {
newLogText += '\n' + value;
});
return newLogText;
});
};

// 设置消息监听器。
const setMessageListener = () => {
let msgListener = {
onMessagesReceived(messages) {
for (let index = 0; index < messages.length; index++) {
rollLog('received msgId: ' + messages[index].msgId);
}
},
onCmdMessagesReceived: messages => {},
onMessagesRead: messages => {},
onGroupMessageRead: groupMessageAcks => {},
onMessagesDelivered: messages => {},
onMessagesRecalled: messages => {},
onConversationsUpdate: () => {},
onConversationRead: (from, to) => {},
};

ChatClient.getInstance().chatManager.removeAllMessageListener();
ChatClient.getInstance().chatManager.addMessageListener(msgListener);
};

// SDK 初始化。
// 调用任何接口之前,请先进行初始化。
const init = () => {

let option = new ChatOptions({
autoLogin: false,
appKey: appKey
});
ChatClient.getInstance().removeAllConnectionListener();
ChatClient.getInstance()
.init(option)
.then(() => {
rollLog('init success');
this.isInitialized = true;
let listener = {
onTokenWillExpire() {
rollLog('token expire.');
},
onTokenDidExpire() {
rollLog('token did expire');
},
onConnected() {
rollLog('login success.');
setMessageListener();
},
onDisconnected(errorCode) {
rollLog('login fail: ' + errorCode);
},
};
ChatClient.getInstance().addConnectionListener(listener);
})
.catch(error => {
rollLog(
'init fail: ' +
(error instanceof Object ? JSON.stringify(error) : error),
);
});
};

// 注册账号。
const registerAccount = () => {
if (this.isInitialized === false || this.isInitialized === undefined) {
rollLog('Perform initialization first.');
return;
}
rollLog('start register account ...');
ChatClient.getInstance()
.createAccount(username, password)
.then(response => {
rollLog(`register success: userName = ${username}, password = ******`);
})
.catch(error => {
rollLog('register fail: ' + JSON.stringify(error));
});
};

// 用环信即时通讯 IM 账号和密码登录。
const loginWithPassword = () => {
if (this.isInitialized === false || this.isInitialized === undefined) {
rollLog('Perform initialization first.');
return;
}
rollLog('start login ...');
ChatClient.getInstance()
.login(username, password)
.then(() => {
rollLog('login operation success.');
let push = new ChatPushConfig({
deviceId:manufacturer,
deviceToken:deviceToken,

});
console.log("--------------------------------------------");
console.log(manufacturer);
console.log(deviceToken);
console.log("--------------------------------------------");
ChatClient.getInstance().updatePushConfig(push);
})
.catch(reason => {
rollLog('login fail: ' + JSON.stringify(reason));
});
};

// 登出。
const logout = () => {
if (this.isInitialized === false || this.isInitialized === undefined) {
rollLog('Perform initialization first.');
return;
}
rollLog('start logout ...');
ChatClient.getInstance()
.logout()
.then(() => {
rollLog('logout success.');
})
.catch(reason => {
rollLog('logout fail:' + JSON.stringify(reason));
});
};

// 发送一条文本消息。
const sendmsg = () => {
if (this.isInitialized === false || this.isInitialized === undefined) {
rollLog('Perform initialization first.');
return;
}
let msg = ChatMessage.createTextMessage(
userId,
content,
ChatMessageChatType.PeerChat,
);
const callback = new (class {
onProgress(locaMsgId, progress) {
rollLog(`send message process: ${locaMsgId}, ${progress}`);
}
onError(locaMsgId, error) {
rollLog(`send message fail: ${locaMsgId}, ${JSON.stringify(error)}`);
}
onSuccess(message) {
rollLog('send message success: ' + message.localMsgId);
}
})();
rollLog('start send message ...');
ChatClient.getInstance()
.chatManager.sendMessage(msg, callback)
.then(() => {
rollLog('send message: ' + msg.localMsgId);
})
.catch(reason => {
rollLog('send fail: ' + JSON.stringify(reason));
});
};

// UI 组件渲染。
return (
<SafeAreaView>
<View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text>
</View>
<ScrollView>
<View style={styles.inputCon}>
<TextInput
multiline
style={styles.inputBox}
placeholder="Enter appkey"
onChangeText={text => setAppKey(text)}
value={appKey}
/>
</View>
<View style={styles.buttonCon}>
<Text style={styles.btn2} onPress={init}>
INIT SDK
</Text>
</View>
<View style={styles.inputCon}>
<TextInput
multiline
style={styles.inputBox}
placeholder="Enter username"
onChangeText={text => setUsername(text)}
value={username}
/>
</View>
<View style={styles.inputCon}>
<TextInput
multiline
style={styles.inputBox}
placeholder="Enter password"
onChangeText={text => setPassword(text)}
value={password}
/>
</View>
<View style={styles.buttonCon}>
<Text style={styles.eachBtn} onPress={registerAccount}>
SIGN UP
</Text>
<Text style={styles.eachBtn} onPress={loginWithPassword}>
SIGN IN
</Text>
<Text style={styles.eachBtn} onPress={logout}>
SIGN OUT
</Text>
</View>
<View style={styles.inputCon}>
<TextInput
multiline
style={styles.inputBox}
placeholder="Enter the username you want to send"
onChangeText={text => setUserId(text)}
value={userId}
/>
</View>
<View style={styles.inputCon}>
<TextInput
multiline
style={styles.inputBox}
placeholder="Enter content"
onChangeText={text => setContent(text)}
value={content}
/>
</View>
<View style={styles.buttonCon}>
<Text style={styles.btn2} onPress={sendmsg}>
SEND TEXT
</Text>
</View>
<View>
<Text style={styles.logText} multiline={true}>
{logText}
</Text>
</View>
<View>
<Text style={styles.logText}>{}</Text>
</View>
<View>
<Text style={styles.logText}>{}</Text>
</View>
</ScrollView>
</SafeAreaView>
);
};

// 设置 UI。
const styles = StyleSheet.create({
titleContainer: {
height: 60,
backgroundColor: '#6200ED',
},
title: {
lineHeight: 60,
paddingLeft: 15,
color: '#fff',
fontSize: 20,
fontWeight: '700',
},
inputCon: {
marginLeft: '5%',
width: '90%',
height: 60,
paddingBottom: 6,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
inputBox: {
marginTop: 15,
width: '100%',
fontSize: 14,
fontWeight: 'bold',
},
buttonCon: {
marginLeft: '2%',
width: '96%',
flexDirection: 'row',
marginTop: 20,
height: 26,
justifyContent: 'space-around',
alignItems: 'center',
},
eachBtn: {
height: 40,
width: '28%',
lineHeight: 40,
textAlign: 'center',
color: '#fff',
fontSize: 16,
backgroundColor: '#6200ED',
borderRadius: 5,
},
btn2: {
height: 40,
width: '45%',
lineHeight: 40,
textAlign: 'center',
color: '#fff',
fontSize: 16,
backgroundColor: '#6200ED',
borderRadius: 5,
},
logText: {
padding: 10,
marginTop: 10,
color: '#ccc',
fontSize: 14,
lineHeight: 20,
},
});

export default App;

注:需要再登录成功以后进行绑定

四、推送测试

1、push测试
如何查看绑定的证书信息:
登录环信console—>即时推送—>找到对应的用户id—>点击查看用户绑定推送证书(如下图)

如何测试推送
登录环信console—> 即时推送—>填写相关的内容—>发送预览—>确认推送

收到推送

2、离线消息测试
登录环信console—> 即时通讯—>用户管理—>找到对应的用户id—>发送rest 消息



至此,ReactNative 推送集成完成。

收起阅读 »

手把手教你集成环信ReactNative离线推送(上)

前言:在集成ReactNative推送之前,需要了解ReactNative与Android原生交互一、RN与Android原生交互RN给原生传递参数步骤:1.用Android Studio打开一个已经存在的RN项目,即用AS打开 项目文件夹/android,如...
继续阅读 »

前言:在集成ReactNative推送之前,需要了解ReactNative与Android原生交互

一、RN与Android原生交互

RN给原生传递参数

步骤:

1.用Android Studio打开一个已经存在的RN项目,即用AS打开 项目文件夹/android,如下图所示


2.在Android原生这边创建一个类继承ReactContextBaseJavaModule,这个类里边放我们需要被RN调用的方法,将其封装成一个原生模块。


MyNativeModule.java代码如下:

package com.awesomeproject;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.xiaomi.mipush.sdk.MiPushClient;
import org.json.JSONException;
import org.json.JSONObject;

public class PushModule extends ReactContextBaseJavaModule {
private ReactApplicationContext reactContext;
public PushModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@Override
public String getName() {
return "PushModule";
}
/**
从RN界面里面调用该方法
**/

@ReactMethod
public void getDeviceToken(){
MainApplication.getReactPackage().mModule.sendDataToJS( MiPushClient.getRegId(MainApplication.getContext()));


}

public void sendDataToJS(String deviceToken){
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("deviceToken",deviceToken);
jsonObject.put("deviceName","");

} catch (JSONException e) {
throw new RuntimeException(e);
}

this.reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("deviceToken",jsonObject.toString());
}



}

本类中存放我们要复用的原生方法,继承了ReactContextBaseJavaModule类,并且实现了其getName()方法,构造方法也是必须的。按着Alt+Enter程序会自动提示。接着定义了一个方法,该方法必须使用注解@ReactMethod标明,说明是RN要调用的方法。

3.在Android原生这边创建一个类实现接口ReactPackage包管理器,并把第二步创建的类加到原生模块(NativeModule)列表里。


PushPackage.java代码如下:


package com.awesomeproject;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class PushPackage implements ReactPackage {
public PushModule mModule;
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> list = new ArrayList<>();
mModule = new PushModule(reactContext);
list.add(mModule);
return list;
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

4.将第三步创建的包管理器添加到ReactPackage列表里(getPackage方法里)

MainApplication.java代码如下:


package com.awesomeproject;

import android.app.Application;
import android.content.Context;
import android.util.Log;

import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.soloader.SoLoader;
import com.awesomeproject.newarchitecture.MainApplicationReactNativeHost;
import com.vivo.push.IPushActionListener;
import com.vivo.push.PushClient;
import com.vivo.push.PushConfig;
import com.vivo.push.util.VivoPushException;
import com.xiaomi.channel.commonutils.logger.LoggerInterface;
import com.xiaomi.mipush.sdk.Logger;
import com.xiaomi.mipush.sdk.MiPushClient;

import java.lang.reflect.InvocationTargetException;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {

private final ReactNativeHost mReactNativeHost =
new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}

@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(mCommPackage);
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return packages;
}

@Override
protected String getJSMainModuleName() {
return "index";
}
};

private final ReactNativeHost mNewArchitectureNativeHost =
new MainApplicationReactNativeHost(this);

@Override
public ReactNativeHost getReactNativeHost() {
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
return mNewArchitectureNativeHost;
} else {
return mReactNativeHost;
}
}


static Context context;

public static Context getContext() {
return context;
}
private static final PushPackage mCommPackage = new PushPackage();
public static PushPackage getReactPackage() {
return mCommPackage;
}



@Override
public void onCreate() {
super.onCreate();
context = this;
// If you opted-in for the New Architecture, we enable the TurboModule system
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());



//初始化push

try {
//PushConfig.agreePrivacyStatement属性及含义说明请参考接口文档
//使用方法
PushConfig config = new PushConfig.Builder()
.agreePrivacyStatement(true)
.build();
PushClient.getInstance(MainApplication.this).initialize(config);
} catch (VivoPushException e) {
Log.d("VivoPushException","-------------"+e.toString());
//此处异常说明是有必须的vpush配置未配置所致,需要仔细检查集成指南的各项配置。
e.printStackTrace();
}



// 打开push开关, 关闭为turnOffPush,详见api接入文档
PushClient.getInstance(this).turnOnPush(new IPushActionListener() {
@Override
public void onStateChanged(int state) {
// TODO: 开关状态处理, 0代表成功,获取regid建议在state=0后获取;
Log.d("vivo初始化------","开关状态处理, 0代表成功,获取regid建议在state=0后获取----"+state);
}
});


//小米初始化push推送服务

MiPushClient.registerPush(this, "2882303761517520571", "5841752092571");

//打开Log
LoggerInterface newLogger = new LoggerInterface() {

@Override
public void setTag(String tag) {
Log.d("MainApplication-------",tag);
// ignore
}

@Override
public void log(String content, Throwable t) {
Log.d("MainApplication-------",content+"-----"+t.toString());

}

@Override
public void log(String content) {
Log.d("MainApplication-------",content);
}
};
Logger.setLogger(this, newLogger);
}

/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*
*
@param context
*
@param reactInstanceManager
*/

private static void initializeFlipper(
Context context, ReactInstanceManager reactInstanceManager) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/

Class<?> aClass = Class.forName("com.awesomeproject.ReactNativeFlipper");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}

}

5.在RN中去调用原生模块,必须import NativeModule模块。
修改App.js文件,需要从‘react-native’中引用‘NativeModules’,
App.js代码如下:

NativeModules.PushModule.getDeviceToken();

来分析一下程序运行流程:
(1)在配置文件AndroidManifest.xml中,android:name=“.MainApplication”,则MainApplication.java会执行。
(2)在MainApplication.java中,有我们创建的包管理器对象。程序加入PushPackage.java中。
(3)在PushPackage.java中,将我们自己创建的模块加入了原生模块列表中,程序进入PushModule.java中。
(4)在PushModule.java中,提供RN 调用的方法getDeviceToken

实现数据从Android原生回调到RN前端界面

我们都知道,要被RN调用的方法必须是void 类型,即没有返回值,但是项目中很多地方都需要返回数据。那怎么实现呢?

步骤:
1.在Android原生这边创建一个类继承ReactContextBaseJavaModule,这个类里边放我们需要被RN调用的方法,将其封装成一个原生模块。
在上面的PushModule中已经继承了ReactContextBaseJavaModule
我们需要调用sendDataToJS将数据传到RN 层。
PushModule.java 代码如下


package com.awesomeproject;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.xiaomi.mipush.sdk.MiPushClient;
import org.json.JSONException;
import org.json.JSONObject;

public class PushModule extends ReactContextBaseJavaModule {
private ReactApplicationContext reactContext;
public PushModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@Override
public String getName() {
return "PushModule";
}
/**
从RN界面里面调用该方法
**/

@ReactMethod
public void getDeviceToken(){
MainApplication.getReactPackage().mModule.sendDataToJS( MiPushClient.getRegId(MainApplication.getContext()));


}

public void sendDataToJS(String deviceToken){
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("deviceToken",deviceToken);
jsonObject.put("deviceName","2882303761517520571");

} catch (JSONException e) {
throw new RuntimeException(e);
}

this.reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("deviceToken",jsonObject.toString());
}



}

步骤
1、在RN 中调用原生的方法


NativeModules.PushModule.getDeviceToken();

2、原生提供对应的方法,将数据传递

3、RN 接收原生传递的数据

至此,我们实现了RN复用原生代码,即将原生模块封装成一个接口,在RN中调用。并且可以封装更加复杂的方法,同时实现了数据回调,即将数据从原生模块中传递到RN前端。

二、原生获取设备信息和ReactNative进行绑定信息

本文介绍如何如何从原生获取推送所需要的设备信息以及ReactNative 绑定信息

前提条件
集成环信即时通讯 React-Native,并且可以正常运行,初始化以及登录
集成文档见环信官网:https://docs-im-beta.easemob.com/document/react-native/quickstart.html

原生获取设备信息:

华为:
在获取华为推送token 之前,我们需要先集成华为sdk,可以参考华为官网官网的集成,也可以参考环信官网进行集成;
获取推送token 参考华为官网文档
获取代码如下:

private void getToken() {
// 创建一个新线程
new Thread() {
@Override
public void run() {
try {
// 从agconnect-services.json文件中读取APP_ID
String appId = "your APP_ID";

// 输入token标识"HCM"
String tokenScope = "HCM";
String token = HmsInstanceId.getInstance(MainActivity.this).getToken(appId, tokenScope);
Log.i(TAG, "get token: " + token);

// 判断token是否为空
if(!TextUtils.isEmpty(token)) {
sendRegTokenToServer(token);
}
} catch (ApiException e) {
Log.e(TAG, "get token failed, " + e);
}
}
}.start();
}
private void sendRegTokenToServer(String token) {
Log.i(TAG, "sending token to server. token:" + token);
}

华为官网有详细的集成介绍,可以仔细阅读, getToken() 方法获取到的就是推送所需要的token。

小米:

1. 前提条件
您已启用推送服务,并获得应用的AppId、AppKey和AppSecret。
2. 接入准备

  1. 下载MiPush Android客户端SDK软件包
    MiPush Android客户端SDK从5.0.1版本开始,提供AAR包接入方式,其支持的最低Android SDK版本为19。
    下载地址:https://admin.xmpush.xiaomi.com/zh_CN/mipush/downpage
    建议您下载最新版本。

  2. 如您之前通过JAR包方式接入过MiPush客户端SDK,需将原JAR包接入配置完全删除,具体配置请参见《Android客户端SDK集成指南(JAR版)》。

  3. 接入指导
    添加依赖
    首先将MiPush SDK的AAR包如MiPush_SDK_Client_xxx.aar 复制到项目/libs/目录,然后在项目APP module的build.gradle中依赖:

android{
repositories {
flatDir {
dirs 'libs'
}
}
}
dependencies {
implementation (name: 'MiPush_SDK_Client_xxx', ext: 'aar')
}

然后需要把该自定义BroadcastReceiver注册到AndroidManifest.xml文件中,注册内容如下:

<receiver
android:exported="true"
android:name="com.xiaomi.mipushdemo.DemoMessageReceiver">


<intent-filter>
<action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />
intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" />
intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.ERROR" />
intent-filter>
receiver>

注意:请务必确保该自定义BroadcastReceiver所在进程与调用注册推送接口(MiPushClient.registerPush())的进程为同一进程(强烈建议都在主进程中)。

注册推送服务
通过调用MiPushClient.registerPush来初始化小米推送服务。注册成功后,您可以在自定义的onCommandResult和onReceiveRegisterResult中收到注册结果,其中的regId即是当前设备上当前app的唯一标示。您可以将regId上传到自己的服务器,方便向其发消息。
为了提高push的注册率,您可以在Application的onCreate中初始化push。您也可以根据需要,在其他地方初始化push。 代码如下:


public class DemoApplication extends Application {

public static final String APP_ID = "your appid";
public static final String APP_KEY = "your appkey";
public static final String TAG = "your packagename";

@Override
public void onCreate() {
super.onCreate();
//初始化push推送服务
if(shouldInit()) {
MiPushClient.registerPush(this, APP_ID, APP_KEY);
}
//打开Log
LoggerInterface newLogger = new LoggerInterface() {

@Override
public void setTag(String tag) {
// ignore
}

@Override
public void log(String content, Throwable t) {
Log.d(TAG, content, t);
}

@Override
public void log(String content) {
Log.d(TAG, content);
}
};
Logger.setLogger(this, newLogger);
}

private boolean shouldInit() {
ActivityManager am = ((ActivityManager) getSystemService(Context.ACTIVITY_SERVICE));
List<RunningAppProcessInfo> processInfos = am.getRunningAppProcesses();
String mainProcessName = getApplicationInfo().processName;
int myPid = Process.myPid();
for (RunningAppProcessInfo info : processInfos) {
if (info.pid == myPid && mainProcessName.equals(info.processName)) {
return true;
}
}
return false;
}
}

最后获取推送token,代码如下


MiPushClient.getRegId(MainApplication.getContext())

一、集成sdk
1. 导入aar 包
将解压后的libs文件夹中vivopushsdk-VERSION.aar(vivopushsdk-VERSION.aar为集成的jar包名字,VERSION为版本名称)拷贝到您的工程的libs文件夹中。
在android项目app目录下的build.gradle中添加aar依赖。


dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')

implementation files("libs/vivo_pushSDK_v3.0.0.7_488.aar")
}

2. 添加权限
vivo Push集成只需要配置网络权限,请在当前工程AndroidManifest.xml中的manifest节点下添加以下代码:

<!Vivo Push需要的权限--> 

<uses-permission android:name="android.permission.INTERNET"/>

3. 配置appid 、api key等信息
vivo Push集成需要配置对应的appid 、app key信息,其中appid 和app key是在开发者平台中申请的,详见 vivo push 操作手册。
请在当前工程AndroidManifest.xml中的Application节点下添加以下代码(建议复制粘贴防止出错):


<!--Vivo Push开放平台中应用的appid 和api key-->
<meta-data
android
:name="api_key"
android
:value="xxxxxxxx"/>

<meta-data
android
:name="app_id"
android
:value="xxxx"/>

4. 自定义通知回调类
在当前工程中新建一个类 PushMessageReceiverImpl(自定义类名)继承OpenClientPushMessageReceiver 并重载实现相关方法。并在当前工程的AndroidManifest.xml文件中,添加自定义Receiver信息,代码如下:

<!--push应用定义消息receiver声明--> 
<receiver android:name="xxx.xxx.xxx.PushMessageReceiverImpl(自定义类名)"
android
:exported="false">
<intent-filter>
<!--接收push消息-->
<action android:name="com.vivo.pushclient.action.RECEIVE"/>
</intent-filter>
</receiver>

5. 注册service
接入SDK,需注册相关服务以确保正常。
请在当前工程AndroidManifest.xml中的Application节点下添加以下代码(建议复制粘贴防止出错):


<!--Vivo Push需要配置的service、activity-->
<service
android
:name="com.vivo.push.sdk.service.CommandClientService"
android
:permission="com.push.permission.UPSTAGESERVICE"
android
:exported="true"/>

6. 配置sdk版本信息(仅通过jar包集成方式需要配置,通过aar包集成无需配置)
通过jar包方式接入SDK,需配置SDK版本信息确保正常。
请在当前工程AndroidManifest.xml中的Application节点下添加以下代码(建议复制粘贴防止出错):


<!--Vivo Push SDK的版本信息-->
<meta-data
android
:name="sdk_version_vivo"
android
:value="488"/>

二、启动推送

在工程的Application中,添加以下代码,用来启动打开push开关,成功后即可在通知消息到达时收到通知。
//在当前工程入口函数,建议在Application的onCreate函数中,在获取用户的同意后,添加以下代码:

//初始化push
try {
//PushConfig.agreePrivacyStatement属性及含义说明请参考接口文档
//使用方法
PushConfig config = new PushConfig.Builder()
.agreePrivacyStatement(true/false)
.build();
PushClient.getInstance(this).initialize(config);
} catch (VivoPushException e) {
//此处异常说明是有必须的vpush配置未配置所致,需要仔细检查集成指南的各项配置。
e.printStackTrace();
}

// 打开push开关, 关闭为turnOffPush,详见api接入文档
PushClient.getInstance(getApplicationContext()).turnOnPush(new IPushActionListener() {
@Override
public void onStateChanged(int state) {
// TODO: 开关状态处理, 0代表成功,获取regid建议在state=0后获取;
}
});

三、获取token


即获取regId,使用getRegId() 函数获取参考如下:
PushClient.getInstance(context).getRegId(new IPushQueryActionListener() {
@Override
public void onSuccess(String regid) {
//获取成功,回调参数即是当前应用的regid;
}

@Override
public void onFail(Integer errerCode) {
//获取失败,可以结合错误码参考查询失败原因;
}});
Api 接口 turnOnPush回调成功之后,即可获取到注册id

注:详情及别的功能见vivo 官网文档:https://dev.vivo.com.cn/documentCenter/doc/365

oppo:

SDK集成步骤
注册并下载SDK
Android的SDK以aar形式提供,第三方APP只需要添加少量代码即可接入OPPO推送服务。
代码参考demo下载:heytapPushDemo
下载aar文件,即3.1.0版本sdk:com.heytap.msp_3.1.0.aar

aar依赖
第一步:添加maven仓库


repositories {
google()
mavenCentral()
}

第二步:添加maven依赖


implementation(name: 'com.heytap.msp_3.1.0', ext: 'aar')
//以下依赖都需要添加
implementation 'com.google.code.gson:gson:2.6.2'
implementation 'commons-codec:commons-codec:1.6'
implementation 'com.android.support:support-annotations:28.0.0'(SDK中的接入最小依赖项,也可以参考demo中的依赖)

第三步:添加aar配置
在build文件中添加以下代码


Android{
....

repositories {
flatDir {
dirs 'libs'
}
}

....
}

配置AndroidManifest.xml


1)OPPO推送服务SDK支持的最低安卓版本为Android 4.4系统。
<uses-sdk android:minSdkVersion="19"/>

2)推送服务组件注册
//必须配置
<service
android:name="com.heytap.msp.push.service.XXXService"
android:permission="com.heytap.mcs.permission.SEND_PUSH_MESSAGE"
android:exported="true">

<intent-filter>
<action android:name="com.heytap.mcs.action.RECEIVE_MCS_MESSAGE"/>
<action android:name="com.heytap.msp.push.RECEIVE_MCS_MESSAGE"/>
intent-filter>
service>(兼容Q版本,继承DataMessageCallbackService)

<service
android:name="com.heytap.msp.push.service.XXXService"
android:permission="com.coloros.mcs.permission.SEND_MCS_MESSAGE"
android:exported="true">

<intent-filter>
<action android:name="com.coloros.mcs.action.RECEIVE_MCS_MESSAGE"/>
intent-filter>
service>(兼容Q以下版本,继承CompatibleDataMessageCallbackService)

注册推送服务
1)应用推荐在Application类主进程中调用HeytapPushManager.init(…)接口,这个方法不是耗时操作,执行之后才能使用推送服务
2)业务需要调用api接口,例如应用内开关开启/关闭,需要调用注册接口之后,才会生效
3)由于不是所有平台都支持MSP PUSH,提供接口HeytapPushManager.isSupportPush()方便应用判断是否支持,支持才能执行后续操作
4)通过调用HeytapPushManager.register(…)进行应用注册,注册成功后,您可以在ICallBackResultService的onRegister回调方法中得到regId,您可以将regId上传到自己的服务器,方便向其发消息。初始化相关参数具体要求参考详细API说明中的初始化部分。
5)为了提高push的注册率,你可以在Application的onCreate中初始化push。你也可以根据需要,在其他地方初始化push。如果第一次注册失败,第二次可以直接调用PushManager.getInstance().getRegister()进行重试,此方法默认会使用第一次传入的参数掉调用注册。

至此,我们获取到了不同设备的device_token


点此链接查看:手把手教你集成环信ReactNative离线推送(下)


收起阅读 »

如何制作 GitHub 个人主页

iOS
原文链接:http://www.bengreenberg.dev/posts/2023-… 人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源...
继续阅读 »

原文链接:http://www.bengreenberg.dev/posts/2023-…


人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主页可能是人们为了了解你而去的第一个地方。


你希望你的GitHub个人主页说些什么?你希望如何以简明易读的方式向访客表达对你的重要性以及你是谁?无论他们是未来的雇主还是开源项目的潜在合作伙伴,你都必须拥有一个引人注目的个人主页。


使用GitHub Actions,你可以把一个静态的markdown文档变成一个动态的、保持对你最新信息更新的良好体验。那么如何做到这一点呢?


我将向你展示一个例子,告诉你如何在不费吹灰之力的情况下迅速做到这一点。在这个例子中,你将学习如何抓取一个网站并使用这些数据来动态更新你的GitHub个人主页。我们将在Ruby中展示这个例子,但你也可以用JavaScript、TypeScript、Python或其他语言来做。


GitHub个人主页如何运作


你的GitHub个人主页可以通过在网页浏览器中访问github.com/[你的用户名]找到。那么该页面的内容来自哪里?


它存在于你账户中一个特殊的仓库中,名称为你的账户用户名。如果你还没有这个仓库,当你访问github.com/[你的用户名]时,你不会看到任何特殊的内容,所以第一步是确保你已经创建了这个仓库,如果你还没有,就去创建它。


探索仓库中的文件


仓库中唯一需要的文件是README.md文件,它是你的个人主页页面的来源。

./
├── README.md

继续在这个文件中添加一些内容并保存,刷新你的用户名主页,你会看到这些内容反映在那里。


为动态内容添加正确的文件夹


在我们创建代码以使我们的个人主页动态化之前,让我们先添加文件夹结构。


在顶层添加一个名为.github的新文件夹,在.github内部添加两个新的子文件夹:scripts/workflows/


你的文件结构现在应该是这样的:

./
├── .github/
│ ├── scripts/
│ └── workflows/
└── README.md

制作一个动态个人主页


对于这个例子,我们需要做三件事:


  • README中定义一个放置动态内容的地方
  • scripts/中添加一个脚本,用来完成爬取工作
  • workflows/中为GitHub Actions添加一个工作流,按计划运行该脚本

现在让我们逐步实现。


更新README


我们需要在README中增加一个部分,可以用正则来抓取脚本进行修改。它可以是你的具体使用情况所需要的任何内容。在这个例子中,我们将在README中添加一个最近博客文章的部分。


在代码编辑器中打开README.md文件,添加以下内容:

### Recent blog posts

现在我们有了一个供脚本查找的区域。


创建脚本


我们正在构建的示例脚本是用Ruby编写的,使用GitHub gem octokit与你的仓库进行交互,使用nokogiri gem爬取网站,并使用httparty gem进行HTTP请求。


在下面这个例子中,要爬取的元素已经被确定了。在你自己的用例中,你需要明确你想爬取的网站上的元素的路径,毫无疑问它将不同于下面显示的在 posts 变量中定义的,以及每个post的每个titlelink


下面是示例代码,将其放在scripts/文件夹中:

require 'httparty'
require 'nokogiri'
require 'octokit'

# Scrape blog posts from the website
url = "<https://www.bengreenberg.dev/blog/>"
response = HTTParty.get(url)
parsed_page = Nokogiri::HTML(response.body)
posts = parsed_page.css('.flex.flex-col.rounded-lg.shadow-lg.overflow-hidden')

# Generate the updated blog posts list (top 5)
posts_list = ["\n### Recent Blog Posts\n\n"]
posts.first(5).each do |post|
title = post.css('p.text-xl.font-semibold.text-gray-900').text.strip
link = "<https://www.bengreenberg.dev#{post.at_css('a')[:href]}>"
posts_list << "* [#{title}](#{link})"
end

# Update the README.md file
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
repo = ENV['GITHUB_REPOSITORY']
readme = client.readme(repo)
readme_content = Base64.decode64(readme[:content]).force_encoding('UTF-8')

# Replace the existing blog posts section
posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m
updated_content = readme_content.sub(posts_regex, "#{posts_list.join("\n")}\n")

client.update_contents(repo, 'README.md', 'Update recent blog posts', readme[:sha], updated_content)

正如你所看到的,首先向网站发出一个HTTP请求,然后收集有博客文章的部分,并将数据分配给一个posts变量。然后,脚本在posts变量中遍历博客文章,并收集其中的前5个。你可能想根据自己的需要改变这个数字。每循环一次博文,就有一篇博文被添加到post_list的数组中,其中有该博文的标题和URL。


最后,README文件被更新,首先使用octokit gem找到它,然后在README中找到要更新的地方,并使用一些正则: posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m


这个脚本将完成工作,但实际上没有任何东西在调用这个脚本。它是如何被运行的呢?这就轮到GitHub Actions出场了!


创建Action工作流


现在我们已经有了脚本,我们需要一种方法来按计划自动运行它。GitHub Actions 提供了一种强大的方式来自动化各种任务,包括运行脚本。在这种情况下,我们将创建一个GitHub Actions工作流,每周在周日午夜运行一次该脚本。


工作流文件应该放在.github/workflows/目录下,可以命名为update_blog_posts.yml之类的。以下是工作流文件的内容:

name: Update Recent Blog Posts

on:
schedule:
- cron: '0 0 * * 0' # Run once a week at 00:00 (midnight) on Sunday
workflow_dispatch:

jobs:
update_posts:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Install dependencies
run: gem install httparty nokogiri octokit

- name: Scrape posts and update README
run: ruby ./.github/scripts/update_posts.rb
env:
GITHUB_TOKEN: $
GITHUB_REPOSITORY: $

这个工作流是根据cron语法定义的时间表触发的,该时间表指定它应该在每个星期天的00:00(午夜)运行。此外,还可以使用workflow_dispatch事件来手动触发该工作流。


update_posts工作由几个步骤组成:


  • 使用 actions/checkout@v2操作来签出仓库。
  • 使用 ruby/setup-ruby@v1 操作来设置 Ruby,指定的 Ruby 版本为 3.1。
  • 使用 gem install 命令安装所需的 Ruby 依赖(httpartynokogiri 和 octokit)。
  • 运行位于.github/scripts/目录下的脚本 update_posts.rbGITHUB_TOKENGITHUB_REPOSITORY环境变量被提供给脚本,使其能够与仓库进行交互。

有了这个工作流程,你的脚本就会每周自动运行,抓取博客文章并更新README文件。GitHub Actions负责所有的调度和执行工作,使整个过程无缝且高效。


将所有的东西放在一起


如今,你的网络形象往往是人们与你联系的第一个接触点--无论他们是潜在的雇主、合作者,还是开源项目的贡献者。尤其是你的GitHub个人主页,是一个展示你的技能、项目和兴趣的宝贵平台。那么,如何确保你的GitHub个人主页是最新的、相关的,并能真正反映出你是谁?


通过利用 GitHub Actions 的力量,我们展示了如何将你的 GitHub 配置文件从一个静态的 Markdown 文档转变为一个动态的、不断变化关于你是谁的例子。通过本指南提供的例子,你已经学会了如何从网站上抓取数据,并利用它来动态更新你的 GitHub个人主页。虽然我们的例子是用Ruby实现的,但同样的原则也可以用JavaScript、TypeScript、Python或你选择的任何其他语言来应用。


回顾一下,我们完成了创建一个Ruby脚本的过程,该脚本可以从网站上抓取博客文章,提取相关信息,并更新你的README.md文件中的"最近博客文章"部分。然后,我们使用GitHub Actions设置了一个工作流,定期运行该脚本,确保你的个人主页中保持最新的内容。


但我们的旅程并没有就此结束。本指南中分享的技术和方法可以作为进一步探索和创造的基础。无论是从其他来源拉取数据,与API集成,还是尝试不同的内容格式,都有无限的可能性。


因此,行动起来让你的 GitHub 个人主页成为你自己的一个充满活力的扩展。让它讲述你的故事,突出你的成就,并邀请你与他人合作。


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

谈谈饭碗的边界问题

主题 不知觉间,写东西也坚持一年多了,这一年间歇性的思考、充斥着空杯吸收的忙碌和工作之外的尝试,最近一段和领导之间的思考有所共鸣,记录下来,希望能引起边界问题的思考吧。 负重与前行 去年的严冬渲染,在今年3~4月份达到了顶峰,去年的焦虑最重,我所得出来的结论是...
继续阅读 »

主题


不知觉间,写东西也坚持一年多了,这一年间歇性的思考、充斥着空杯吸收的忙碌和工作之外的尝试,最近一段和领导之间的思考有所共鸣,记录下来,希望能引起边界问题的思考吧。


负重与前行


去年的严冬渲染,在今年3~4月份达到了顶峰,去年的焦虑最重,我所得出来的结论是:“即便是有天大的本事,也失去了意义”,得出这个结论的前提是,我已经尽我所能的驱动自己全盘吸收,认真做事,在此之外不停的摸索第二种小范围的业务试水,成功了一部分,但远远达不到预期的效果。


“日中则昃,月盈则食”,也许是预期见底,觉得即便是见底了也没多大了不起的事情,心态上好像好听点叫背水一战、悲观一些叫预期见底,已然死猪不怕开水烫。


搁置争议


因为一些非我这个层级的事情,但莫名其妙的旁观参与,和领导有一番恳谈,最终的思路基本上也就归结为 “还年轻,最终能依靠的也就是自身的能力,这个能力既包含执行、学习速度、业务、当然也包含为人处事的灵活性,但最重要的是选择大于努力”,事实上,从程序员的角度来讲,我已不再年轻,但相较于领导还算年轻,可能是角度不同,认知稍微有些差别,但大的方向没有任何问题,偏重点有所不同,领导对 “业务和学习速度” 很推崇,“工作处事的机变” 差不多是基本要求了😵,至于选择,其实也已经没多大选择了。


于我而言,我的定位其实一直立足于 “执行” 这个层级,并觉得以此为根基,空杯心理去掌握业务、学习速度、文档、软件全周期等内容,这基本也是我一直以来的理念,近一年多,基本接触的形形色色厉害的人有许多,事务杂,内容多,各种杂七杂八的东西,但不妨碍近一年多逐渐的总结和认识不足,可见性的提升是巨大的。 边界的问题,基本上属于 “能力边界是公司给个体划定的边界,你必须符合这个水平线之上,但是个人应该是对自己不设边界,但可以划定阶段方向”,我就以实际的接触来谈。


之后,和相对能够听得进去的同事也有讨论,毕竟绝大多数都是 “鸵鸟心态,今日不忧明日事,大事临身心态蹦”,对互联网从业者,没有人会相信一个人可以在一个公司待一辈子,但即便是有规划者,也很难在局限中做出合适的选择,但总有一条,心中愈惧怕愈是自身欠缺的,也许是个排错的选项。


拉回正题,集中讨论的话题也在于语言发展和执行力的问题上,就软件执行力而言,以单端来说,执行力和责任心,均算不错,但基本有个问题就是人为设定自身边界和定位,导致的结果就是一直在舒适区画圈,也仅此而已,我技术上学习模型基本上属于 结果->解决->问题->资料->细节 ,但接触许多人,往往纯纯的就是依靠,总觉得有人能解决,以蒙混过关的心态解决问题,从来不会涉及一个问题以月为单位摸索,即便这个事情已经过去了。


认知上,年龄到了这个阶段,单纯的开发执行能力在一般的事务上没啥本质的竞争力,因为复杂度的上限就在哪里,同事们在去年的环境思想鞭挞中,已经充分的有了认知,最后的结论都落脚在到那一天再说,陡然之间,可能发生的事情已然有了时间线的征兆,似乎一下子有些不知所措。


所以?


愕然也好,有准备也罢,但于我而言,能力的认可和肯定以及自我的肯定,让我的内心,在逐步见好的招聘中,找到了意义,也期待第二个年头,更强大有底气的自我,后面的着力点也会往行业的宽泛性、汇报交流的表达能力、架构设计的层次化展现力上去争取提升,当然,软件的开发能力是绝对不能松懈的,至于时间和精力,谁说这段没有悄悄提升自己的生产力工具呢, 重复性开发工作和一些杂项,已然没啥提升诉求的工作,必然是要借助工具释放自身的生产力了,而我又该继续往感兴趣的方向去学习了...


容我吐槽


Github语言趋势分析系列貌似发布总是说有啥不符合规范,唉,总是在发布的时候遭退~~~。


另外最近感觉好多外头和工作的事情累加了好几项,心态越发的好了,事情推进的有条不紊的,相比于之前,万事靠自身,其实合作也不错。


PS


发现身边事儿、聊点周奇遇,我是沈二,期待奇遇的互联网灵魂~、一起聊天吹水,探索新的可能~wx:breathingss,入圈吧!


附录



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

SF Symbols 4 使用指南

iOS
本文基于 WWDC 2022 Session 10157 和 Session 10158 梳理,为了更方便没有 SF Symbols 经验的读者理解,也将往年的 SF Symbols 相关内容一并整理。本文从 SF Symbols 4 的新特性切入,讨论 SF...
继续阅读 »

本文基于 WWDC 2022 Session 10157Session 10158 梳理,为了更方便没有 SF Symbols 经验的读者理解,也将往年的 SF Symbols 相关内容一并整理。本文从 SF Symbols 4 的新特性切入,讨论 SF Symbols 这款由系统字体支持的符号库有哪些优点以及该如何使用。在这次 WWDC 2022 中,除了符号的数量的增加到了 4000+ 之外,还有自动渲染模式、可变符号等新特性推出,让 SF Symbols 这把利器变得又更加趁手和锋利了。




本文是 WWDC22 内参 的供稿。



什么是 SF Symbols


符号在界面中起着非常重要的作用,它们能有效地传达意义,它们可以表明你选择了哪些项目,它们可以用来从视觉上区分不同类型的内容,他们还可以节约空间、整洁界面,而且符号出现在整个视觉系统的各处,这使整个用户界面营造了一种熟悉的感觉。


符号的实现和使用方式多种多样,但设计和使用符号时有一个亘古不变的问题,那就是将符号与用户界面的另一个基本元素——「文本」很好地配合。符号和文字在用户界面中以各种不同的大小被使用,他们之间的排列形式、对齐方式、符号颜色、文本字重与符号粗细的协调、本地化配置以及无障碍设计都需要开发者和设计师来细心配置和协调。




为了方便开发者更便捷、轻松地使用符号,Apple 在 iOS 13 中开始引入他们自己设计的海量高质量符号,称之为 SF Symbols。SF Symbols 拥有超过 4000 个符号,是一个图标库,旨在与 Apple 平台的系统字体 San Francisco 无缝集成。每个符号有 9 种字重和 3 种比例,以及四种渲染模式,它们的默认设计都与文本标签对齐,同时这些符号是矢量的,这意味着它们是可以被拉伸的,使得他们在无论用什么大小时都会呈现出很好的效果。如果你想去创造具有相似设计特征或无障碍功能的自定义符号,它们也可以被导出并在矢量图形编辑工具中进行编辑以创建新的符号。


对于开发者来说,这套 SF Symbols 无论是在 UIKit,AppKit 还是 SwiftUI 中都能运作良好,且使用方式也很简单方便,寥寥数行代码就可以实现。对于设计师来说,你只需要为符号只做三个字重的版本,SF Symbols 会自动地帮你生成其余 9 种字重和 3 种比例的符号,然后在 SF Symbols 4 App 中调整四种渲染模式的表现,就制作好了一份可以高度定制化的 symbol。




如何使用 SF Symbols


SF Symbols 4 App


在开始介绍如何使用 SF Symbols 之前,我们可以先下载来自 Apple 官方的 SF Symbols 4 App,这款 App 中收录了所有的 SF Symbols,并且记录了每个符号的名称,支持的渲染模式,可变符号的分层预览,不同语言下的变体,不同版本下可能出现的不同的名称,并且可以实时预览不同渲染模式下不同强调色的不同效果。你可以在这里下载 SF Symbols 4 App。




符号的渲染模式


通过之前的图片你可能已经注意到了,SF Symbols 可以拥有多种颜色,有一些 symbol 还有预设的配色,例如代表天气、肺部、电池的符号等等。如果要使用这些带有自定义颜色的符号,你需要知道,SF Symbols 在逻辑上是预先分层的(如下图的温度计符号就分为三层),根据每一层的路径,我们可以根据渲染模式来调整颜色,而每个 SF Symbols 有四种渲染模式。




单色模式 Monochrome


在 iOS 15 / macOS 11 之前,单色模式是唯一的渲染模式,顾名思义,单色模式会让符号有一个单一的颜色。要设置单色模式的符号,我们只需要设置视图的 tint color 等属性就可以完成。

let image = UIImage(systemName: "thermometer.sun.fill")
imageView.image = image
imageView.tintColor = .systemBlue

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.blue)

分层模式 Hierarchical


每个符号都是预先分层的,如下图所示,符号按顺序最多分成三个层级:Primary,Secondary,Tertiary。SF Symbols 的分层设定不仅在分层模式下有效,在后文别的渲染模式下也是有作用的




分层模式和单色模式一样,可以设置一个颜色。但是分层模式会以该颜色为基础,生成降低主颜色的不透明度而衍生出来的其他颜色(如上上图中的温度计符号看起来是由三种灰色组合而成)。在这个模式中,层级结构很重要,如果缺少一个层级,相关的派生颜色将不会被使用。

let image = UIImage(systemName: "thermometer.sun.fill")
let config = UIImage.SymbolConfiguration(hierarchicalColor: .lightGray)
imageView.image = image
imageView.preferredSymbolConfiguration = config

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.gray)
.symbolRenderingMode(.hierarchical)

调色盘模式 Palette


调色盘模式和分层模式很像,但也有些许不同。和分层模式一样是,调色盘模式也会对符号的各个层级进行上色,而不同的是,调色盘模式允许你自由的分别设置各个层级的颜色。

let image = UIImage(systemName: "thermometer.sun.fill")
let config = UIImage.SymbolConfiguration(paletteColors: [.lightGray, .cyan, .systemTeal])
imageView.image = image
imageView.preferredSymbolConfiguration = config

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.lightGray, .cyan, .teal)

多色模式 Muticolor


在 SF Symbols 中,有许多符号的意象在现实生活中已经深入人心,比如:太阳应该是橙色的,警告应该是黄色的,叶子应该是绿色的的等等。所以 SF Symbols 也提供了与现实世界色彩相契合的颜色模式:多色渲染模式。当你使用多色模式的时候,就能看到预设的橙色太阳符号,红色的闹铃符号,而你不需要指定任何颜色。

let image = UIImage(systemName: "thermometer.sun.fill")
imageView.image = image
imageView.preferredSymbolConfiguration = .preferringMulticolor()

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.symbolRenderingMode(.multicolor)

自动渲染模式 Automatic


谈论完了四种渲染模式,可以发现每次设置 symbol 的渲染模式其实也是一件费心的事情。为了解决这个问题,在最新的 SF Symbols 中,每个 symbol 都有了一个自动渲染模式。例如下图的 shareplay 符号,你可以看到在右侧面板中,shareplay 符号的第二个模式(分层模式)的下方有一个空心小圆点,这意味着该符号在代码中使用时,假如你不去特意配置他的渲染模式,那么他将使用分层模式作为他的默认渲染模式。



你可以在 SF Symbols 4 App 中查询到所有符号的自动渲染模式。





可变颜色


在有的时候,符号并不单单代表一个单独的概念或者意象,他也可以代表一些数值、比例或者程度,例如 Wi-Fi 强度或者铃声音量,为了解决这个问题,SF Symbols 引入了可变颜色这个概念。


你可以在 SF Symbol 4 App 中的 Variable 目录中找到所有有可变颜色的符号,平且可以通过右侧面板的滑块来查看不同百分比程度下可变颜色的形态。另外你也可以注意到,可变颜色的可变部分实际上也是一种分层的表现,但这里的分层和上文提到的渲染模式使用的分层是不同的。一个符号可以在渲染模式中只分两层,在可变颜色的分层中分为三层,下图中第二个符号喇叭 speaker.wave.3.fill 就是如此。关于这里的分层我们会在后文如何制作可变颜色中详细讨论。




在代码中,我们只需要在初始化 symbol 时增加一个 Double 类型的 variableValue 参数,就可以实现可变颜色在不同程度下的不同形态。值得注意的是,假如你的可变颜色(例如上图 Wi-Fi 符号)可变部分有三层,那么这个 variableValue 的判定将会三等分:在 0% 时将不高亮信号,在 0%~33% 时,将高亮一格信号,在 34%~67 % 时,将高亮 2 格信号,在 68% 以上时,将会显示满格信号。

let img = NSImage(symbolName: "wifi", variableValue: 0.2)

可变颜色的可变部分是利用不透明度来实现的,当可变颜色和不同的渲染模式结合后,也会有很好的效果。




如何制作和调整可变颜色


在 SF Symbols 4 App 中,我们可以自定义或者调整可变颜色的表现,接下来我将带着大家以 party.popper 这个符号为基础制作一个带可变颜色的符号。

  1. 首先我们打开 SF Symbols 4 App,在右上角搜索 party.popper,找到该符号后右键选择 复制为1个自定符号。推荐你在上方将符号的排列方式修改为画廊模式,如下图所示。


  2. 可以注意到右下角的  这个板块,这个符号默认是由两个层级组成的,分别是礼花和礼花筒,同时我们也可以看到,礼花和礼花筒又分别是由更零碎的路径组成的,通过勾选子路径我们可以给每个层新增或者减少路径。那我现在想要给这个符号新增一层,我只需要在画廊模式下,将符号的某一部分拖拽到层里就可以。


  3. 通过这样的操作,我们可以将这个符号整理为四层:礼花筒、线条礼花、小球礼花和大球礼花。为了可变颜色的效果,我们需要按照从下到上:礼花筒、线条礼花、大球礼花和小球礼花的顺序去放置层级,另外,我们可以切换到分层模式、调色板模式和多色模式里面去调整成自己喜欢的颜色来预览效果,我这里调整了多色模式中的配色,具体效果如下。


  4. 接下来,我们将前三层,也就是除了礼花筒外的三层,最右侧的可变符号按钮选中,来表示这三层将可以在可变符号的变化范围内活动。接下来,只要点击颜色区域内的可变符号按钮,我们就可以拖动滑块来查看可变颜色的形态。


  5. 至此,我们就完成了一个带可变颜色的自定义符号,我们可以在合适的地方使用这个符号。例如我的 App 有一个 4 个步骤的新手引导,这时候就可以给每一个步骤配备一个符号来让界面变得更加的活泼。


统一注释 Unified annotations


其实我们已经接触到了 Unified annotations 这个过程,它就是将符号的层级,路径以及子路径整理成在四个渲染模式下都能良好工作的过程,就如同上文彩色礼花筒的例子,我们通过统一注释,让彩色礼花筒符号在不同渲染模式、不同环境色、不同主题色下,都能良好的运作。


那一般来说,对于单色模式,不需要过多的调整,它就能保持良好的形态;对于分层模式和调色盘模式,我们需要在给每个层设定好哪个是 Primary 层、哪个是 Secondanry 层以及哪个是 Tertiary 层,这样系统就会按优先级给符号上合适的颜色;对于多色模式,我们可以根据喜好以及符号的意义,给它预设一个合理的颜色,另外还要注意的是,如果设计了可变颜色在符号中,那么要注意保持可变符号的效果在四个渲染模式上都表现正常。


除了这些之外,还有一些特别的地方需要注意,我们以 custom.heart.circle.fill 为例子。你可以注意到,这个垃爱心符号是有一个圆形的背景的,在这种情况下,假如我们按照原来的规则去绘制单色模式,会发现:背景的圆形和爱心的图案将会是同一个颜色,那我们就将看不见圆形背景下的图案了。




这时我们可以使用 Unified annotations 给我们提供的新功能,我们将上图在 板块的爱心,将它从 Draw 改成 Erase,这样,我们就相当于以爱心的形状镂空了这个白色的背景,从而使该图形展现了出来并且在单色模式下能够一直表现正常。同理,在分层模式和调色盘模式中,也有这个 Erase 的功能共大家调整使用。


字重和比例


SF Symbols 和 Apple 平台的系统字体 San Francisco 一样,拥有九种字重和三种比例可以选择,这意味着每个 SF Symbol 都有 27 种样式以供使用。

let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold, scale: .large)
imageView.preferredSymbolConfiguration = config

// SwiftUI
Label("Heart", systemImage: "heart")
.imageScale(.large)
.font(.system(size: 20, weight: .semibold))

符号的字重和文本的字重原理相同,都是通过加粗线条来增加字重。但 SF Symbols 的三种比例尺寸并不是单纯的对符号进行缩放。如果你仔细观察,会发现对于同一个字重,但是不同比例的符号来说,他们线条的粗细是一样的,但是对符号的整体进行了扩充和延展,以应对不一样的使用环境。


要实现这样的效果,意味着每个 symbol 的底层逻辑并不是一张张图片,而是由一组组的路径构成,这也是为什么在当你想要自定义一个属于自己的 symbol 的时候,官方要求你用封闭路径 + 填充效果去完成一个符号,而不是使用一条简单路径 + 路径描边(stroke)来完成一个符号。



更多关于如何制作一个 symbol 的内容,请移步 WWDC 21 内参:定制属于你的 Symbols





除了字重和比例之外,SF Symbols 还在很多方面进行了努力来方便开发者的工作,例如:符号的变体、不同语言下符号的本地化、符号的无障碍化等,关于这些内容,以及其它由于篇幅原因未在本文讨论的细节问题,请移步 WWDC 21 内参:SF Symbols 使用指南


总结


从上文介绍 SF Symbols 的特性和优点我们可以看到,它的出现是为了解决符号与文本之间的协调性问题,在保证了本地化、无障碍化的基础上,Apple 一直在实用性、易用度以及多样性上面给 SF Symbols 加码,目前已经有了 4000+ 的符号可以使用,相信在未来还会有更多。这些符号的样式和图案目前看来并不是那么的广泛,这些有限的符号样式并不能让设计师安心代替所有界面上的符号,但是有失必有得,在这样一个高度统一的平台上,SF Symbols 在规范化、统一化、表现能力、代码与设计上的简易程度,在今年都又进一步的提升了,达到了让人惊艳的程度,随着 SF Symbols 的继续发展,我相信对于部分开发者来说,即将成为一个最优的符号工具🥳。


更多资料


以下是这几年关于 SF Symbols 的资料:



以下是更早的 SF Symbols 资料:



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

用 Metal 画一个三角形(Swift 函数式风格)

iOS
由于今年工作中用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。 顺便试试 Swift 在函数式方面能达到啥好玩的程度。主要是我不会 Swift,仅仅为了好玩。 创建工程 随便创建个工程,小玩具就不打算跑...
继续阅读 »

由于今年工作中用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。

顺便试试 Swift 在函数式方面能达到啥好玩的程度。主要是我不会 Swift,仅仅为了好玩。


创建工程


随便创建个工程,小玩具就不打算跑在手机上了,因为我的设备是 ARM 芯片的,所以直接创建个 Mac 项目,记得勾上包含测试。


构建 MTKView 子类


现在来创建个 MTKView 的子类,其实我现在已经不接受这种所谓的面向对象,开发者用这种方式,就要写太多篇幅来描述一个上下文结构跟函数就能实现的动作。

import MetalKit

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
render()
}
}

extension MetalView {
func render() {
// TODO: 具体实现
}
}

我们这里给 MetalView extension 了一个 render 函数,里面是后续要写得具体实现。


普通的方式画一个三角形


先用常见的方式来画一个三角形

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
render()
}
}

extension MetalView {
func render() {
guard let device = device else { fatalError("Failed to find default device.") }
let vertexData: [Float] = [
-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0
]

let dataSize = vertexData.count * MemoryLayout<Float>.size
let vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
let library = device.makeDefaultLibrary()
let renderPassDesc = MTLRenderPassDescriptor()
let renderPipelineDesc = MTLRenderPipelineDescriptor()
if let currentDrawable = currentDrawable, let library = library {
renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
renderPipelineDesc.vertexFunction = library.makeFunction(name: "vertexFn")
renderPipelineDesc.fragmentFunction = library.makeFunction(name: "fragmentFn")
renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
let commandQueue = device.makeCommandQueue()
guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
let commandBuffer = commandQueue.makeCommandBuffer()
guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
if let renderPipelineState = try? device.makeRenderPipelineState(descriptor: renderPipelineDesc) {
encoder.setRenderPipelineState(renderPipelineState)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}
}
}

然后是我们需要注册的 Shader 两个函数

#include <metal_stdlib>

using namespace metal;

struct Vertex {
float4 position [[position]];
};

vertex Vertex vertexFn(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
return vertices[vid];
}

fragment float4 fragmentFn(Vertex vert [[stage_in]]) {
return float4(0.7, 1, 1, 1);
}

在运行之前需要把 StoryBoard 控制器上的 View 改成我们写得这个 MTKView 的子类。




自定义操作符


函数式当然不是指可以定义操作符,但是没有这些操作符,感觉没有魂灵,所以先定义个管道符


代码实现

precedencegroup SingleForwardPipe {
associativity: left
higherThan: BitwiseShiftPrecedence
}

infix operator |> : SingleForwardPipe

func |> <T, U>(_ value: T, _ fn: ((T) -> U)) -> U {
fn(value)
}

测试管道符


因为创建项目的时候,勾上了 include Tests,直接写点测试代码,执行测试。

final class using_metalTests: XCTestCase {
// ...

func testPipeOperator() throws {
let add = { (a: Int) in
return { (b: Int) in
return a + b
}
}
assert(10 |> add(11) == 21)
let doSth = { 10 }
assert(() |> doSth == 10)
}
}

目前随便写个测试通过嘞。


Functional Programming


现在需要把上面的逻辑分割成小函数,事实上,因为 Cocoa 的基础是建立在面向对象上的,我们还是没法完全摆脱面向对象,目前先小范围应用它。


生成 MTLBuffer


先理一下逻辑,代码开始是创建顶点数据,生成 buffer

fileprivate let makeBuffer = { (device: MTLDevice) in
let vertexData: [Float] = [
-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0
]

let dataSize = vertexData.count * MemoryLayout<Float>.size
return device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
}

创建 MTLLibrary


接着是创建 MTLLibrary 来注册两个 shader 方法,还创建了一个 MTLRenderPipelineDescriptor 对象用于创建 MTLRenderPipelineState,但是创建的 MTLLibrary 对象是一个 Optional 的,所以其实得有两步,总之先提取它再说吧

fileprivate let makeLib = { (device: MTLDevice) in device.makeDefaultLibrary() }

抽象 map 函数


根据我们有限的函数式编程经验,像 Optional 这种对象大概率有一个 map 函数,所以我们自家实现一个,同时还要写成柯里化的(建议自动柯里化语法糖入常),因为这里有逃逸闭包,所以要加上 @escaping

func map<T, U>(_ transform: @escaping (T) throws -> U) rethrows -> (T?) -> U? {
return { (o: T?) in
return try? o.map(transform)
}
}

处理 MTLRenderPipelineState


这里最终目的就是 new 了一个 MTLRenderPipelineState,顺带处理把程序的一些上下文给渲染管线描述器(MTLRenderPipelineDescriptor),譬如我们用到的着色器(Shader)函数,像素格式。
最后一行直接 try! 不处理错误啦,反正出问题直接会抛出来的

fileprivate let makeState = { (device: MTLDevice) in
return { (lib: MTLLibrary) in
let renderPipelineDesc = MTLRenderPipelineDescriptor()
renderPipelineDesc.vertexFunction = lib.makeFunction(name: "vertexFn")
renderPipelineDesc.fragmentFunction = lib.makeFunction(name: "fragmentFn")
renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
return (try! device.makeRenderPipelineState(descriptor: renderPipelineDesc))
}
}

暂时收尾


已经不想再抽取函数啦,其实还能更细粒度地处理,因为函数式有个纯函数跟副作用的概念,像 Haskell 里是可以用 Monad 来处理副作用的情况,这个主题留给后续吧。先把 render 改造一下

fileprivate let render = { (device: MTLDevice, currentDrawable: CAMetalDrawable?) in
return { state in
let renderPassDesc = MTLRenderPassDescriptor()
if let currentDrawable = currentDrawable {
renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
let commandQueue = device.makeCommandQueue()
guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
let commandBuffer = commandQueue.makeCommandBuffer()
guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
encoder.setRenderPipelineState(state)
encoder.setVertexBuffer(device |> makeBuffer, offset: 0, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}
}

然后再调用,于是就变成下面这副鸟样子

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
device |> map {
makeLib($0)
|> map(makeState($0))
|> map(render($0, self.currentDrawable))
}
}
}

最后执行出这种效果




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

展开&收起,使用SwiftUI搭建一个侧滑展开页面交互

iOS
项目背景 闲来无事,在使用某云音乐听歌的时候发现一个侧滑展开的内页,交互效果还不错。 那么这一章节中,我们将使用SwiftUI搭建一个侧边展开页面交互。 项目搭建 首先,创建一个新的SwiftUI项目,命名为SlideOutMenu。 逻辑分析 首先我们来分...
继续阅读 »

项目背景


闲来无事,在使用某云音乐听歌的时候发现一个侧滑展开的内页,交互效果还不错。


那么这一章节中,我们将使用SwiftUI搭建一个侧边展开页面交互。


项目搭建


首先,创建一个新的SwiftUI项目,命名为SlideOutMenu




逻辑分析


首先我们来分析下基本的逻辑,一般的侧滑展开方式的交互是,在首页右上角有一个“更多”的按钮,点击按钮时,内页菜单从左往右划出,滑出至离右边20~30的位置停止。


然后首页背景将蒙上一个蒙层,点击蒙层时,侧滑展开的页面从右往左收起


简单分析完逻辑后,我们来实现这个交互。


首页入口


首先,我们需要在首页搭建一个入口,示例:

// 顶部导航入口
private var moreBtnView: some View {
    Button(action: {
    }) {
        Image(systemName: "list.bullet")
            .foregroundColor(.black)
    }
}

然后,我们可以使用NavigationViewnavigationBarItems创建顶部导航按钮样式,示例:

var body: some View {
    NavigationView {
        Text("点击左上角侧滑展开")
            .padding()
            .navigationBarTitle("首页", displayMode: .inline)
            .navigationBarItems(leading: moreBtnView)
    }
}



如此,首页入口部分我们就完成了。


左边菜单


接下来,我们来构建左侧菜单的内容。我们可以沿用之前设计过的“设置”页面的结构,我们先来构建栏目结构。示例:

// MARK: 栏目结构
struct listItemView: View {
    var itemImage: String
    var itemName: String
    var body: some View {
        Button(action: {
        }) {
            HStack {
                Image(systemName: itemImage)
                    .font(.system(size: 17))
                    .foregroundColor(.black)
                Text(itemName)
                    .foregroundColor(.black)
                    .font(.system(size: 17))
                Spacer()
                Image(systemName: "chevron.forward")
                    .font(.system(size: 14))
                    .foregroundColor(.gray)
            }.padding(.vertical, 10)
        }
    }
}

在我们构建侧滑展开的页面前,我们需要声明两个变量,一个是侧滑展开的页面的宽度,一个是当前这个页面的位置。示例:

@State var menuWidth = UIScreen.main.bounds.width - 60
@State var offsetX = -UIScreen.main.bounds.width + 60

我们设置的侧滑展开页面的宽度是屏幕宽度-60,而当前侧滑展开页面的位置是负位置,这样就可以在展示的时候先把页面隐藏起来


而当我们点击顶部导航中的“更多”按钮时,将offsetX偏移量X轴坐标设置为0。示例:

// 顶部导航入口
private var moreBtnView: some View {
    Button(action: {
        withAnimation {
            offsetX = 0
        }
    }) {
        Image(systemName: "list.bullet")
            .foregroundColor(.black)
    }
}

然后,我们创建一个新视图来构建侧滑展开的页面内容,示例:

// MARK: 左侧菜单
struct SlideOutMenu: View {
    @Binding var menuWidth: CGFloat
    @Binding var offsetX: CGFloat

    var body: some View {
        Form {
            Section {
            }
            Section {
                listItemView(itemImage: "lock", itemName: "账号绑定")
                listItemView(itemImage: "gear.circle", itemName: "通用设置")
                listItemView(itemImage: "briefcase", itemName: "简历管理")
            }
            Section {
                listItemView(itemImage: "icloud.and.arrow.down", itemName: "版本更新")
                listItemView(itemImage: "leaf", itemName: "清理缓存")
                listItemView(itemImage: "person", itemName: "关于掘金")
            }
        }
        .padding(.trailing, UIScreen.main.bounds.width - menuWidth)
        .edgesIgnoringSafeArea(.all)
        .shadow(color: Color.black.opacity(offsetX != 0 ? 0.1 : 0), radius: 5, x: 5, y: 0)
        .offset(x: offsetX)
        .background(
            Color.black.opacity(offsetX == 0 ? 0.5 : 0)
                .ignoresSafeArea(.all, edges: .vertical)
                .onTapGesture {
                    withAnimation {
                        offsetX = -menuWidth
                    }
                })
    }
}

上述代码中,我们也对页面宽度menuWidth、偏移位置offsetX进行了声明,方便之后我们在ContentView视图中进行双向绑定


我么使用Form表单和Section段落构建样式,这点就不说了。


值得说的一点是,我们设置了在页面展开的时候,也就是offsetX页面偏移量X轴坐标不为0,我们加了一个阴影,完善了侧滑展开页面的悬浮效果


然后使用offset调整页面初始位置。背景部分,除了根据offsetX页面偏移量X轴坐标加了一个蒙层,而且当我们点击的背景的时候,我们将偏移位置offsetX重新赋值,这样就能实现收起的交互效果。


我们在ContentView视图中展示侧滑展开视图,示例:

var body: some View {
    ZStack {
        NavigationView {
            Text("点击左上角侧滑展开")
                .padding()
                .navigationBarTitle("首页", displayMode: .inline)
                .navigationBarItems(leading: moreBtnView)
        }
        SlideOutMenu(menuWidth: $menuWidth, offsetX: $offsetX)
    }
}

项目展示




恭喜你,完成了本章的全部内容!


快来动手试试吧。


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

程序员要学会“投资知识”

iOS
啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢? 然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。 不幸的是,它们是有限的资产。随着新技术的...
继续阅读 »

啊,富兰克林,那家伙总是说些深刻的道理。嗯,我们真的可以通过早睡早起变成优秀的程序员吗?早起的鸟儿可能抓住虫子,但早起的虫子会怎么样呢?


然而,富兰克林的开场白确实击中了要害 - 知识和经验确实是你最有价值的职业资产。


不幸的是,它们是有限的资产。随着新技术的出现和语言环境的发展,你的知识可能会过时。不断变化的市场力量可能会使你的经验变得陈旧和无关紧要。考虑到技术和社会变革的加速步伐,这可能会发生得特别迅速。


随着你的知识价值的下降,你在公司或客户那里的价值也会降低。我们希望阻止所有这些情况的发生。


学习新知识的能力是你最关键的战略资产。但如何获取学习的方法,知道要学什么呢?


知识投资组合。


我们可以将程序员对计算过程、其工作应用领域的了解以及所有经验视为他们的知识投资组合。管理知识投资组合与管理金融投资组合非常相似:


1、定期的投资者有定期投资的习惯。


2、多样化是长期成功的关键。


3、聪明的投资者在投资组合中平衡保守和高风险高回报的投资。


4、投资者在低点买入,在高点卖出以获取最大回报。


5、需要定期审查和重新平衡投资组合。


为了在职业生涯中取得成功,你必须遵循相同的指导原则管理你的知识投资组合。


好消息是,管理这种类型的投资就像任何其他技能一样 - 它可以被学会。诀窍是从一开始就开始做,并养成习惯。制定一个你可以遵循并坚持的例行程序,直到它变成第二天性。一旦达到这一点,你会发现自己自动地吸收新的知识。


建立知识投资组合。


· 定期投资。 就像金融投资一样,你需要定期地投资你的知识投资组合,即使数量有限。习惯本身和总数量一样重要,所以设定一个固定的时间和地点 - 这有助于你克服常见的干扰。下一部分将列出一些示例目标。


· 多样化。 你知道的越多,你变得越有价值。至少,你应该了解你目前工作中特定技术的细节,但不要止步于此。计算机技术变化迅速 - 今天的热门话题可能在明天(或至少不那么受欢迎)变得几乎无用。你掌握的技能越多,你的适应能力就越强。


· 风险管理。 不同的技术均匀地分布在从高风险高回报到低风险低回报的范围内。把所有的钱都投资在高风险的股票上是不明智的,因为它们可能会突然崩盘。同样,你不应该把所有的钱都投资在保守的领域 - 你可能会错过机会。不要把你的技术鸡蛋都放在一个篮子里。


· 低买高卖。 在新兴技术变得流行之前开始学习可能就像寻找被低估的股票一样困难,但回报可能同样好。在Java刚刚发明出来后学习可能是有风险的,但那些早期用户在Java变得流行时获得了可观的回报。


· 重新评估和调整。 这是一个动态的行业。你上个月开始研究的时髦技术可能现在已经降温了。也许你需要刷新一下你很久没有使用过的数据库技术的知识。或者,你可能想尝试一种不同的语言,这可能使你在新的角色中处于更好的位置......


在所有这些指导原则中,下面这个是最简单实施的。


(程序员的软技能:ke.qq.com/course/6034346)


定期在你的知识投资组合中进行投资。


目标。


既然你有了一些指导原则,并知道何时添加什么到你的知识投资组合中,那么获取构成它的智力资产的最佳方法是什么呢?以下是一些建议:


· 每年学习一门新语言。


不同的语言以不同的方式解决相同的问题。学习多种不同的解决方案有助于拓宽你的思维,避免陷入常规模式。此外,由于充足的免费资源,学习多门语言变得更加容易。


· 每月阅读一本技术书籍。


尽管互联网上有大量的短文和偶尔可靠的答案,但要深入理解通常需要阅读更长的书籍。浏览书店页面,选择与你当前项目主题相关的技术书籍。一旦养成这个习惯,每月读一本书。当你掌握了所有当前使用的技术后,扩大你的视野,学习与你的项目无关的东西。


· 也阅读非技术书籍。


请记住,计算机是被人类使用的,而你所做的最终是为了满足人们的需求 - 这是至关重要的。你与人合作,被人雇佣,甚至可能会面临来自人们的批评。不要忘记这个方程式的人类一面,这需要完全不同的技能(通常被称为软技能,听起来可能很容易,但实际上非常具有挑战性)。


· 参加课程。


在当地大学或在线寻找有趣的课程,或者你可能会在下一个商业博览会或技术会议上找到一些课程。


· 加入当地的用户组和论坛。


不要只是作为观众成员;要积极参与。孤立自己对你的职业生涯是有害的;了解你公司之外的人在做什么。


· 尝试不同的环境。


如果你只在Windows上工作,花点时间在Linux上。如果你对简单的编辑器和Makefile感到舒适,尝试使用最新的复杂IDE,反之亦然。


· 保持更新。


关注不同于你当前工作的技术。阅读相关的新闻和技术文章。这是了解使用不同技术的人的经验以及他们使用的特定术语的极好方式,等等。


持续的投资是至关重要的。一旦你熟悉了一门新的语言或技术,继续前进并学习另一门。


无论你是否在项目中使用过这些技术,或者是否应该将它们放在你的简历上,都不重要。学习过程将拓展你的思维,开启新的可能性,并赋予你在处理任务时的新视角。思想的跨领域交流是至关重要的;尝试将你所学应用到你当前的项目中。即使项目不使用特定的技术,你仍然可以借鉴其中的思想。例如,理解面向对象编程可能会导致你编写更具结构的C代码,或者理解函数式编程范 paradigms 可能会影响你如何处理Java等等。


学习机会。


你正在狼吞虎咽地阅读,始终站在你领域的突破前沿(这并不是一项容易的任务)。然而,当有人问你一个问题,你真的不知道的时候,不要停在那里 - 把找到答案当做一个个人挑战。问问你周围的人或在网上搜索 - 不仅在主流圈子中,还要在学术领域中搜索。


如果你自己找不到答案,寻找能够找到答案的人,不要让问题无解地悬而未决。与他人互动有助于你建立你的人际网络,你可能会在这个过程中惊喜地找到解决其他无关问题的方法 - 你现有的知识投资组合将不断扩展。


所有的阅读和研究需要时间,而时间总是不够的。因此,提前准备,确保你在无聊的时候有东西可以阅读。在医院排队等候时,通常会有很好的机会来完成一本书 - 只需记得带上你的电子阅读器。否则,你可能会在医院翻阅旧年鉴,而里面的折叠页来自1973年的巴布亚新几内亚。


批判性思维。


最后一个要点是对你阅读和听到的内容进行批判性思考。你需要确保你投资组合中的知识是准确的,没有受到供应商或媒体炒作的影响。小心狂热的狂热分子,他们认为他们的观点是唯一正确的 - 他们的教条可能不适合你或你的项目。


不要低估商业主义的力量。搜索引擎有时只是优先考虑流行的内容,这并不一定意味着这是你最好的选择;内容提供者也可以支付费用来使他们的材料排名更高。书店有时会将一本书突出地摆放,但这并不意味着它是一本好书,甚至可能不受欢迎 - 这可能只是有人支付了那个位置。


(程序员的软技能:ke.qq.com/course/6034346)


批判性分析你所阅读和听到的内容。


批判性思维本身就是一个完整的学科,我们鼓励你深入研究和学习这门学科。让我们从这里开始,提出一些发人深省的问题。


· 五问“为什么”。


我最喜欢的咨询技术之一是至少连续问五次“为什么”。这意味着在得到一个答案后,你再次问“为什么”。像一个坚持不懈的四岁孩子提问一样重复这个过程,但请记住要比孩子更有礼貌。这样做可以让你更接近根本原因。


· 谁从中受益?


尽管听起来可能有点功利主义,但追踪金钱的流动往往可以帮助你理解潜在的联系。其他人或其他组织的利益可能与你的利益保持一致,也可能不一致。


· 背景是什么?


一切都发生在自己的背景下。这就是为什么声称“解决所有问题”的解决方案通常站不住脚,宣扬“最佳实践”的书籍或文章经不起审查的原因。 “对谁最好?” 是一个需要考虑的问题,以及关于前提条件、后果以及情况是短期还是长期的问题。


· 在何种情况下和何地可以起作用?


在什么情况下?是否已经太晚了?是否还太早了?不要只停留在一阶思维(接下来会发生什么);参与到二阶思维中:接下来会发生什么?


· 为什么这是一个问题?


是否有一个基础模型?这个基础模型是如何工作的?


不幸的是,如今找到简单的答案是具有挑战性的。然而,通过广泛的知识投资组合,并对你遇到的广泛技术出版物进行一些批判性分析,你可以理解那些复杂的答案。


(程序员的软技能:ke.qq.com/course/6034346)


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

也谈“前端已死”

一、一些迹象 逛社区,偶然看到了这张图片: 嗯……我眉头一皱,久久不语,心想,有这么夸张吗,假的吧? 突然想到,最近我在社区发了个前端招聘的信息,结果简历漫天纷飞,塞爆邮箱。 莫非,前端这个岗位真的不再是供不应求了?🤔 二、原因分析 我细想下,也差不多到时候...
继续阅读 »

一、一些迹象


逛社区,偶然看到了这张图片:



嗯……我眉头一皱,久久不语,心想,有这么夸张吗,假的吧?


突然想到,最近我在社区发了个前端招聘的信息,结果简历漫天纷飞,塞爆邮箱。


莫非,前端这个岗位真的不再是供不应求了?🤔


二、原因分析


我细想下,也差不多到时候了。


从16年到现在,算算,7年的时间了。


前端大火就是从16年开始的,多种原因,包括:


移动互联网的兴起,传统行业的数字化转型,大前端技术的普及等。


紧接着是Vue为代表的前端框架和工具的兴起,使得前端开发的门槛进一步降低,前端也成为进入互联网圈子的最快最容易的跳板,促使前端圈进一步繁荣。


然而,连王菲都知道,没有什么是长盛不衰的。



发展,稳定,衰落是亘古不变的事物发展规律。


各种迹象表明,无论是有意还是无意,目前互联网的发展似乎进入了平稳期,这也意味着岗位的需求也开始变得平稳,而涌入这个行业的新人却没有停止,这就必然导致到了某个时间点,前端从业人员会达到饱和,于是那些没有竞争力的人就会遇到求职困境。


遇困的人多了,在社区的声音多了,自然也就会出现“前端已死”这样的言论。


三、破局之道


想要改变这种现状,只能是下面两种方法。


一是烧香拜佛,祈祷互联网大环境好转,最好再来一波生产力或生产环境的变革,让前端行业再赶上一波发展的春风,催生更大的岗位需求,何愁就业?


但显然,寄希望于大环境是不靠谱的,生产力虽然一定是往上走的,但说不定不是助力行业的发展,而是革了行业的命。


比方说现在很火的chatGPT,你说是会增加前端岗位呢,还是空窗加倍绝绝子?


所以,要想前端碗端得稳,前端饭吃得香,还是得靠下面这个方法,也就是想办法提高个人的核心竞争力。


提高核心竞争力


所谓核心竞争力,说白了,就是你能干别人干不了的活,能做别人做不了的事情。


更直白一点,就是你能给团队创造比别人更多的价值。


很普通的一句话,对不对?但是意识到和意识不到,那可是天差地别。


最近虽然收到了很多简历,但是看完之后都只能无奈摇头,不能说一模一样嘛,可以说极其雷同,缺少区分度。


专业技能均是全覆盖,工作描述均是自己用了什么前端框架,做了什么什么工作。


没有任何吸引人的信息,给人感觉,就是个普通的前端从业人员,领导安排个需求,然后接受,排期,完成开发,上线,这种。



这就……对吧,不是不给机会,实在是给不了。


一百份简历竞争一个招聘HC,肯定是把面试机会留给那些有突出亮点的人的。


拿工作描述举例,你一个一个罗列你做的项目,用了哪些技术有什么用?所有投简历的人都有做项目,都有使用前端技术,你的这些描述完全就是废话,简历扔垃圾箱的那种。


不需要扯那么多,你就说你比别人牛在什么地方!


注意,这个牛,不一定就是技术水平或者业务成果,任何亮点都可以,只要是能够做到别人做不到的事情,同时是对团队有帮助的,都可以。


举几个例子:


– 我参与了团队所有项目的开发,“所有”就是亮点,隐约让人觉得你是可信任的。


– 我是团队下班最晚的,工作最积极的。也是亮点,可以提,工时越长,通常产出越多,性价比就越高。


– 我在团队里做了很多看不见的工作。亮点,主动承担边缘工作不是所有人都可以做到的。


– 我是团队内分享(面授或文章都可以)次数第一。亮点,加分,帮助团队成长也是一种价值产出。


– 我连续获得四星五星荣誉,或者优秀员工称号,加分,公司的认可比自己在简历上吹上天都有用。


甚至是工作以外的特长都可以,我是钓鱼大佬,我是跑步达人,我是综艺专家,我是健身狂人,都可以,因为一个人能坚持自己的爱好并做到出众,也是不简单的。



可偏偏问题就在于,能够获得面试机会的亮点如此简单,很多人却没有,一个也没有。


因为在日常工作中就没有这种意识,就是我要做得比别人更好、我要强化我的优势、我要想办法让团队变得更好的意识。


平时工作就是浑浑噩噩的状态,等需求,写代码,上线,拿钱,一切都是在被动进行,仅把前端当作职业而非事业,总是希望干活少,拿钱多。


所以做事难以精益求精,也不会为了更好的未来努力让当下的自己变得更好,也不会主动做那些工作以外的对团队有帮助的事情,典型的被网上的躺平言论给忽悠瘸了。


弄错了因果,即,我给老板加班,又不会给我涨薪,我为什么要加班?我学习更底层的技术,平时又用不到,我为什么要学?我平时工作那么忙,还要我去写文档做分享,我为什么要做?


所以,找不到工作就不要怨天尤人了,也别说什么“前端已死”,前端行业好着呢,优秀的前端不知道多缺,年薪不知道有多高!


框架的能力


很多人做开发非常熟练,各种得心应手,于是就会觉得自己是个挺有竞争力的前端开发人员。



高启强没有说话,只是呵呵一笑。


这是不小心把框架的能力当作自己的能力了。


大家不妨冷静想一想,借助一个成熟的框架,开发出一个合格的Web应用,他的难度有多高?


更具体点,我们经常使用的各种小程序和快应用,让一个培训班里培训了3个月的新人,以及充足的时间,他能不能捣鼓出来?


答案显而易见,肯定可以,至少绝大多数人都可以。


因为使用一个东西的难度要比创造一个东西的难度低多了。


也就是,基于Vue等前端框架的开发,它是需要技术的,但是,它并不需要的很高的技术。


这种状态最容易迷惑人,所谓满瓶不动半瓶摇。


如果不能跳出自己所处的环境,正在更高的视角看待自己,非常容易对自己在行业所处的层次造成误判,譬如,我明明干活很利索,怎么没有面试机会,一定是我们这个行业出问题了。


这就是误判,有问题的不是行业,而是自己的竞争力不足。


我再说一遍,希望大家不要嫌啰嗦,使用工具的能力,并不能作为核心竞争力,因为现在学习资料很丰富,社区很活跃,什么问题都可以找到解决方案,你能做到的别人也能做到,没有任何优势,不属于竞争力。


反而是下面这些能力有足够的区分度。


  • 比他人涉猎更广,例如音视频处理、图形表现实现或者Node开发有较多经验;
  • JS、CSS等前端基本功扎实,积累深厚,各种API特性了然于心,最佳实践信手捏来;
  • 具有设计审美或者产品嗅觉灵敏,开发的产品体验非常好,干活很细。

拥有这些能力或特质,并在简历上表现出来,最好有材料佐证,那找到一份满意的工作是非常轻松的事情。


就怕一年经验十年用,从此外卖天天送。



当然,不可否认,虽说框架与工具让很多人陷入了温床,但对于国家整个数字化转型和互联网的发展是做出了重大贡献的。


在巨大需求出现的时候,有足够多的人力迅速投身这个行业,带动整个行业的发展。


只是,潮水终会退去,只有那些真正会游泳的才能继续在大海中徜徉。


四、未来如何


常常有人问我,旭哥,我应该学什么才有前途?


每当看到这样的问题,我都会眉头紧锁,过于功利的心态,在技术这条路上注定难有大成。


这就有点类似于养殖业,比如说前两年养鲈鱼很赚钱,结果很多养殖户改养鲈鱼,造成今年鲈鱼泛滥,市场存量是过去数倍,根本卖不出价格,最后赔得裤衩都不剩了。


技术其实也是类似,有人一看前端就业形势大好,都去搞前端,结果“前端已死”。


技术栈也是一样,妄图学完之后自己就成了香饽饽,可能吗?人是趋利性的动物,就算你眼光独到,命运垂怜,抢得先机,但数年之后呢?


所以,其实重要的不是学了什么,而是学得怎么样。


心无旁骛,专注自身,无论学什么,从事哪个职业,只要自己足够有竞争力,都有前途。


无论是历史悠久的后端开发,还是巅峰期早已过去的客户端开发,亦或者是开始进入稳定期的前端开发,均是如此。


前端的未来


随着消费和广告行业的慢慢复苏,前端的就业情况会有所好转。但是……


首先,这个好转不会很快,而是很缓慢那种,因为当一个事物陷入低谷再要起来,前期都是缓慢的,需要升到某一个临界点之后,才会明显加速。


其次,就算前端的就业情况有所恢复,也不可能恢复到疫情之前的那种火热,那个时候遍地都是前端培训班,非常夸张。


至于前端是否会死,这个完全不要担心。


只要互联网还在,前端这个职业就不会消失,因为无论设备介质如何变化,用户的交互行为都不会消失,而前端就是一个处理人机交互的职业。


而人工智能的兴起,确实会对前端这个职业产生影响,是危机但也是机遇,如果你安于现状,则是危机,如果你勤于学习,则人工智能是机遇,会让你的产出更加高效。


这么看来,最核心的竞争力应该是学习的能力!


(完)


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

大专生自学前端求职历险记

关于我 由于高中的游手好闲、不学无术,没有考上大学。去了一所专科学校,本以为自己能够浪子回头,在学校好好学习。可惜的是,来到一个陌生又充满诱惑的城市后,迅速的迷失了自己,天天埋头打游戏,学习的事情早已抛之脑后。 一晃眼,到了2020年,疫情的接踵而至,让我这个...
继续阅读 »

关于我


由于高中的游手好闲、不学无术,没有考上大学。去了一所专科学校,本以为自己能够浪子回头,在学校好好学习。可惜的是,来到一个陌生又充满诱惑的城市后,迅速的迷失了自己,天天埋头打游戏,学习的事情早已抛之脑后。


一晃眼,到了2020年,疫情的接踵而至,让我这个本来没有任何技术、学历的“闲散人士”更加雪上加霜。豪不夸张的说,当时去实习,就差跪着求人家要我,说自己不要薪资。经历过一个月后,也就是2020年5月底,我找到了一份前端开发工作,从此开启了我的前端开发工作之旅。


在专科学校里的时间,我并没有意识到社会市场的残酷,甚至天真的认为自己还是能够辛苦点的找到一份工作。可是,现实给了我当头一棒,没有技术、没有学历、疫情打击。那一段时间应该是真的认知自己的时间,家里也没什么闲钱供我去培训班,我也不知道我出去能干嘛。去看了一圈市场,与跟同学的了解,了解到了前端开发工作,所以就一股脑扎进这个行业当中。


求职之旅


跟大多数人一样,并不知道应该从何处下手,当时在我的认知当中就知道一个 JQuery,所谓的 MVVM 框架简直是一无所知。点开小破站,找到点击率最高的视频,开始自学起来。


了解到一点框架的皮毛、然后死记硬背一点基础,统统写进简历当中。


所以我的学习曲线是如图下所示



跟大多数人一样,我是直接通过框架起手学习的前端。导致了我对于问题的处理能力几乎为零,遇到问题直接就双手离开键盘。看不懂,是真的看不懂(如果有相同感受的可以在评论抠一个 1)。


对着视频学了十天左右,写了一个 demo,屁颠颠的去求职。结果也是可想而知,人家也不是傻子一眼识破。四处碰壁,简历丢出去,根本没人看。兜兜转转持续了一个月左右,终于有一家小公司愿意给一个面试机会,马不停蹄的出发去面试,坐了一个小时左右的地铁抵达一个破旧不堪的写字楼,当时要不是看到周围还有一个高校,我还以为我去了一个搞传销的地方。。。推开一个破旧的们,一个很小的房间,两个人坐在里面给我面试。我也很直白的说自己只会一点点皮毛,他们也很直白的告诉我:我们条件有限,相当于是各取所需。其实老实说,我挺感动的,没有给我画大饼,也很直白的说我图他们要我,他们图我不要啥钱。


最终,我也算是如愿找到了这份实习工作,一个月 2000。也算是不错的结果了。


实习项目开发


去到公司以后,也马不停蹄的开始了开发工作。首先就是让我从一个简单的后台管理系统开始入手。但是问题也来了,我根本不知道什么叫管理系统,连项目搭建我都不会,然后就是两眼一抹黑。不停的去百度,查看如何搭建一个后台管理系统。


老实说,我当时连路由是什么我都不清楚,更别说加一堆乱七八糟的功能在里面了。哪个过程可想而知,多么的折磨人。经历了半个月,模板被我折腾起来了一个简单的样子,对着人家的管理系统样子进行拙劣的模仿。但是 bug 满天飞也是避免不了的问题。并且没有丝毫的设计可言,纯纯的依托答辩。


最后的最后,实在是看不下去了(包括我自己),去网上扒了一个模板开始自己去折腾。为什么一开始不考虑使用模板呢?因为我看不懂代码,下不去手。


虽然最后跌跌撞撞的项目启动起来了,但是也算是我第一次项目开发的经历吧。后续持续的添加一些功能,改动一些简单的样式,还好老板也很佛系,没有为难我,基本上没有魔改模板。所以也算是顺利的完成了后台管理系统的开发任务。


小插曲


在实习工作的期间,在技术群中认识了一个很牛的大佬。经常我在群里问一些傻逼问题(因为自己基础太差了),但是他都会很耐心的给我讲解,甚至是下班后抽出时间给我远程讲课。也算是我的半个引路人吧,让我知道了如何去玩儿前端。在这里手动抠一个感谢🙏🙏🙏。


步入正轨


在经历过第一个项目开发后,也算是知道了框架应该如何去玩儿(也就是知道了框架的 api 如何去调用)。也知道了如何去学好前端,所以慢慢的回头去了解基前端的三大基础知识 js css html


其实我相信很多人跟我一样,开始都是赶鸭子上架的形式去开发项目,遇到问题束手无策;遇到 bug 不知道如何去排查;遇到不知道如何去实现。。。最后我也总结出了问题所在,那就是基础的不扎实,学习顺序的问题,导致了这些问题。


啰嗦一句


哪怕是现在,我有时候跟网友聊天的时候也能听到一些让人不能理解的观点:前端那么简单有什么难度?前端不就是写写页面?前端。。。。


从我的观点出发而言,前端这个岗位确实是属于,宽进严出。想入行确实很容易,毕竟像我这样啥也不懂的,通过十来天的学习都能去做前端开发的事情。


但是,但是,但是,重要的话说三遍,前端的简单是因为它的入行门槛低。但是入门和会还是有本质的区别,绝大多数前端开发工作都是写 后台管理系统,这种开发,都是直接套用现成模板与组件就能够写。如果是定制化开发,脱离了后台管理系统的开发,那还是有手就行吗?


继续步入正轨


在工作的时间中,也认识了很多互联网大厂的大牛:滴滴、网易、腾讯等,经常厚着脸皮去请教他们。但是他们回应最多的是:多看基础,看书!


大佬们都这么说,那还等什么!直接开始行动。


  • 绿宝书:犀牛书
  • 红宝书:javascript高级程序设计
  • 黄宝书:你不知道的js

直接搞起来!虽然我很讨厌看书,但是看到自己实习的 2k 工资,我还不动起来,那可能真就废了。


所以每天下班后,回家翻开书籍,开始看。果不其然,一看就打瞌睡,生涩、枯燥的知识内容。没办法,继续去请教如何看书学习,得到的答案就是:好记性,不如烂笔头。


然后读书的时候,边看边写,跟做笔记一样。效果果然好多了,没那么容易打瞌睡。而且我也买了一些零食(口香糖、耐嚼的肉干之类的)边看边吃,让自己集中注意力。总之是为了能够学到真知识,想尽了各种办法。


半个月后,看了几章节基础,感觉确实潜移默化的改变了一些。写代码的时候不会那么的茫然;反复调试的次数少了一些;知道了更多好用的 api ,代码质量有一定的提高。


读书笔记分享


读书笔记


在这里分享一篇,自己从零开始写的一些笔记。不过自己已经停更很久了。


实习总结


经过两个月的实习后,时间也来到了 2020年7月,我毕业了。我也学到了很多东西,但是我觉得,这样子的工作状态并不是我喜欢的。


回学校简单收拾了一下,也决定了辞职。去找一份更加有前途的工作,当然这里肯定有很多人疑惑:你凭什么啊?确实是如此,包括我的父母,也是很疑惑并且还质疑的问道:你上几个月班,忘了自己的实际情况了?


我也开始反思,自己真的就那么的蠢、那么的不堪吗?


果断辞职


经过我的深思熟虑后,还是在毕业后辞职了。在出租屋沉淀了一个月,这一个月基本上每天只睡了五六个小时,其余时间都花在了基础的夯实上面,狠狠的补充前端基础知识。每天醒来就是:看书、写 demo、请教大佬,每天如此,孜孜不倦。


一个月后,整理自己的简历,然后又开始了自己的求职之旅。


二次求职


求职之路,也并没有自己想的那么顺利。别人也没有因为我简历写的东西多了那么一点可怜的东西而青睐你。


我也在开始反思,自己的辞职是否正确。因为我的本质问题并没有解决:没有学历、没有经验。期间也在自我怀疑、自我安慰,也在凌晨的时候,抓耳挠腮,头发也在开始一大把一大把的掉。


就这样持续了一个月左右,我终于又收到了一份面试邀请。马不停蹄的前去面试,结果却出乎我的意料,他们并没有问我八股文,反而是对我所说的经历感兴趣。我也是添油加醋的说了一顿我的实习经历、辞职后的这一个月的学习经历。


最后的最后,他们通过了我的初试。给我说需要老大亲自面试,我开始很忐忑。但是见到老大后,他是一个很和蔼的老师,并没有刁难我,也没有问我刁钻问题,只是跟我谈了一下基本情况、了解了我的基本情况,就通过了我的二次面试。


二次求职之旅结果


我很幸运,因为,让我去打工的地方是一个资源丰富的高校。我的老大也是院长,初次面试的两位也是两位老师。我也如愿以偿的又有了一份新的工作,接触到了极其丰富的资源。


老师们也很愿意教授知识,让我的技术再次的突飞猛进。


开发项目:


  • 北京冬奥会水立方保电系统
  • 基于负荷聚合的园区能量态势感知与交易系统
  • 电压暂降仿真模拟系统

薪资变化


毕业后,我的薪资也算是以每年翻倍的涨幅进步。也算是我的学习换来的回报吧。还是挺不错的~


现在


截至目前,经过三年零两个月的工作时间,也算是勉强迈入了初级前端开发的门槛吧。不断的学习中,也在积极的参与开源的贡献。



这些都是本人参与开发、贡献的项目,有兴趣可以点开看看。如果觉得有用也可以点一个小星星🌟~~~


最后


学习确实是一个枯燥的过程,也是一个很痛苦的过程。包括自己,如果不是那些大佬对我的帮助,我也不会那么快的进步。最后还是很衷心的感谢他们对我的帮助~


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

利用 UICollectionView 实现图片浏览效果

iOS
废话开篇:利用 UICollectionView 简单实现一个图片浏览效果。 一、效果展示 二、实现思路 1、封装 UICollectionViewLayout ,实现内部 UICollectionViewCell 的布局。 UICollectionView...
继续阅读 »

废话开篇:利用 UICollectionView 简单实现一个图片浏览效果。


一、效果展示




二、实现思路


1、封装 UICollectionViewLayout ,实现内部 UICollectionViewCell 的布局。

UICollectionViewLayout 在封装瀑布流的时候会用到,而且担负着核心功能的实现。其实从另一个角度也可以把 UICollectionViewLayout 理解成“数据源”,这个数据不是 UI 的展示项,而是 UI 的尺寸项。在内部进行预计算 UICollectionViewCellframe


UICollectionViewUIScrollView的子类,只不过,它里面子控件通过“重用”机制实现了优化,一些复用的复杂逻辑还是扔给了系统处理。开发过程中只负责对 UICollectionViewLayout 什么时候需要干什么进行自定义即可。


2、获取 UICollectionView 目前可见的 cells,通过进行缩放、旋转变换实现一些简单的效果。

3、自定义 cell ,修改锚点属性。

三、代码整理


1、PhotoBrowseViewLayout

这里有一点需要注意的,在 UICollectionViewLayout 内部会进行计算每一个 cellframe,在计算过程中,为了更好的展示旋转变换,cell 的锚点会修改到 (0.5,1),那么,为了保证 UI 展示不变,那么,就需要将 y 增加 cell 高度的一半

#import "PhotoBrowseViewLayout.h"

@interface PhotoBrowseViewLayout()

@property(nonatomic,strong) NSMutableArray * attributeArray;

@property(nonatomic,assign) CGFloat cellWidth;

@property(nonatomic,assign) CGFloat cellHeight;

@property(nonatomic,assign) CGFloat sep;

@property(nonatomic,assign) int showCellNum;


@end

@implementation PhotoBrowseViewLayout

- (instancetype)init
{
    if (self = [super init]) {
        self.sep = 20;
        self.showCellNum = 2;
    }
    return self;
}

//计算cell的frame
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.cellWidth == 0) {
        self.cellWidth = **self**.collectionView.frame.size.width * 2 / 3.0;
    }
    if (self.cellHeight == 0) {
        self.cellHeight = self.collectionView.frame.size.height;
    }
    CGFloat x = (self.cellWidth + self.sep) * indexPath.item;
//这里y值需要进行如此设置,以抵抗cell修改锚点导致的UI错乱
    CGFloat y = self.collectionView.frame.size.height / 2.0;
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    attrs.frame = CGRectMake(x, y, self.cellWidth, self.cellHeight);
    return attrs;
}

//准备布局
- (void)prepareLayout
{
    [super prepareLayout];
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (int i = 0; i <count; i++) {
        UICollectionViewLayoutAttributes *attris = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
        [self.attributeArray addObject:attris];
    }
}

//返回全部cell的布局集合
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attributeArray;
}

//一次性提供UICollectionView 的 contentSize
- (CGSize)collectionViewContentSize
{
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    CGFloat maxWidth = count * self.cellWidth + (count - 1) * self.sep;
    return CGSizeMake(maxWidth, 0);
}

- (NSMutableArray *)attributeArray
{

    if (!_attributeArray) {
        _attributeArray = [[NSMutableArray alloc] init];
    }
    return _attributeArray;
}

@end

2、PhotoBrowseCollectionViewCell

这里主要是进行了锚点修改(0.5,1),代码很简单。

#import "PhotoBrowseCollectionViewCell.h"

@interface PhotoBrowseCollectionViewCell()

@property(nonatomic,strong) UIImageView * imageView;

@end

@implementation PhotoBrowseCollectionViewCell


- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
//设置(0.5,1)锚点,以底部中点为轴旋转
        self.layer.anchorPoint = CGPointMake(0.5, 1);
        self.layer.masksToBounds = YES;
        self.layer.cornerRadius = 8;
    }
    return self;
}

- (void)setImage:(UIImage *)image
{
    self.imageView.image = image;
}


- (UIImageView *)imageView
{

    if (!_imageView) {
        _imageView = [[UIImageView alloc] init];
        _imageView.contentMode = UIViewContentModeScaleAspectFill;
        _imageView.backgroundColor = [UIColor groupTableViewBackgroundColor];
        [self.contentView addSubview:_imageView];
    }
    return _imageView;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.imageView.frame = **self**.contentView.bounds;
}

@end

3、CollectPhotoBrowseView

CollectPhotoBrowseView 负责进行一些 cell 的图形变换。

#import "CollectPhotoBrowseView.h"
#import "PhotoBrowseCollectionViewCell.h"
#import "PhotoBrowseViewLayout.h"

@interface CollectPhotoBrowseView()<UICollectionViewDelegate,UICollectionViewDataSource>

@property(nonatomic,strong) UICollectionView * photoCollectView;

@end

@implementation CollectPhotoBrowseView

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self makeUI];
    }
    return self;
}

- (void)makeUI{
//设置自定义 UICollectionViewLayout
    PhotoBrowseViewLayout * photoBrowseViewLayout = [[PhotoBrowseViewLayout alloc] init];
    self.photoCollectView = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:photoBrowseViewLayout];
    self.photoCollectView.delegate = self;
    self.photoCollectView.dataSource = self;
    [self.photoCollectView registerClass:[PhotoBrowseCollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
    self.photoCollectView.showsHorizontalScrollIndicator = NO;
    [self addSubview:self.photoCollectView];
//执行一次可见cell的图形变换
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self visibleCellTransform];
    });
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoBrowseCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
    [cell setImage: [UIImage imageNamed:[NSString stringWithFormat:@"fd%ld",indexPath.item % 3 + 1]]];
    return cell;
}

#pragma mark - 滚动进行图形变换
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//滑动的时候,动态进行cell图形变换
    [self visibleCellTransform];
}

#pragma mark - 图形变化
- (void)visibleCellTransform
{
//获取当前可见cell的indexPath集合
    NSArray * visibleItems =  [self.photoCollectView indexPathsForVisibleItems];
//遍历动态进行图形变换
    for (NSIndexPath * visibleIndexPath in visibleItems) {
        UICollectionViewCell * visibleCell = [self.photoCollectView cellForItemAtIndexPath:visibleIndexPath];
        [self transformRotateWithView:visibleCell];
    }
}

//进行图形转换
- (void)transformRotateWithView:(UICollectionViewCell *)cell
{
//获取cell在当前视图的位置
    CGRect rect = [cell convertRect:cell.bounds toView:self];
//计算当前cell中轴线与中轴线的距离的比值
    float present = ((CGRectGetMidX(rect) - self.center.x) / (self.frame.size.width / 2.0));
//根据位置设置选择角度
    CGFloat radian = (M_PI_2 / 15) * present;
//图形角度变换
    CGAffineTransform transformRotate = CGAffineTransformIdentity;
    transformRotate = CGAffineTransformRotate(transformRotate, radian);
//图形缩放变换
    CGAffineTransform transformScale = CGAffineTransformIdentity
    transformScale = CGAffineTransformScale(transformScale,1 -  0.2 *  fabs(present),1 - 0.2 * fabsf(present));
//合并变换
    cell.transform = CGAffineTransformConcat(transformRotate,transformScale);
}

@end

四、总结与思考


UICollectionView 也是 View,只不过系统为了更好的服务于开发者,快速高效的实现某些开发场景,进行了封装与优化,将复杂的逻辑单独的封装成一个管理类,这里就是 UICollectionViewLayout,交给它去做一些固定且复杂的逻辑。所以,自定义复杂UI的时候,就需要将功能模块足够细化,以实现更好的代码衔接。代码拙劣,大神勿笑[抱拳][抱拳][抱拳]


作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7119028552263008293
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从互联网到国企、从一线城市到三线省会

6月的北京格外的闷热,比起内蒙真的热了不少,整整四个月没来北京了,晚上出高铁来到清河站时还是那么的熟悉,挤上13号线路过五道口、知春路,去西直门换乘2号线,再换上5号线到了宋家庄,最后换上回村的亦庄线,从北京的最西北边走到最东南角。看着地铁上的疲惫的人们,这次...
继续阅读 »

6月的北京格外的闷热,比起内蒙真的热了不少,整整四个月没来北京了,晚上出高铁来到清河站时还是那么的熟悉,挤上13号线路过五道口、知春路,去西直门换乘2号线,再换上5号线到了宋家庄,最后换上回村的亦庄线,从北京的最西北边走到最东南角。看着地铁上的疲惫的人们,这次回来自己更像是一个游客的视角,观察着以前的“自己”。从3月初离职一直没有记录过这段经历,但这次去北京让我觉得有必要写一些自己的感受和体会。


离职前的纠结


意外通过的面试


毕业四年一直从事Java开发,在京东两年左右,2月份很偶然的看到内蒙的一则国企招聘,本着今年大概率要回去工作的想法,顺便就报名了,又很顺利的通过了笔试和面试,面试时特意请假一天从北京跑回内蒙,下午等待面试的时候手机被收走四个小时,四个小时没处理工作消息差点爆炸,各种报警和需求沟通群里被@,面试完急匆匆的坐高铁回北京继续上班。本来只是想试下机会,莫名就通过了,这下轮到自己开始纠结了。


无时无刻的报警&下不了的班


在京东工作应该是我做开发这些年达到的事业最高峰,从之前写简单逻辑的小菜鸟一下子开阔了视野,见到从未了解的新领域,对流量并发有了新的认识。但这份工作确实很辛苦,我们几乎是7*24小时待命,每天都要保持手机开机,随时都会有接口报警,一定要第一时间响应处理,核心接口还要配置语音报警,即使晚上也会直接打电话进行通知,如果不接电话就会被系统记录,还有一些产品运营的问题也会随时发生,某些定位的门店或者商品不展示了,都要及时给人家反馈处理。这两年我们真的不管去哪里都要带着电脑,出去旅游或者逛街都是如此,脑子里的那根弦一直紧紧绷着,就像是悬在头上的达摩克利斯之剑。


还有每天忙碌的工作,写不完的需求,开不完的会,解决不完的问题,下不了的班,从早上九点去了就开始忙碌,经常晚上十点多才可以下班,很多人可能会待到12点甚至更久,但我确实是卷不动了,身上压着的三座大山,需求排期、日常报警、绩效目标,每个月的发版上线是不可能变得,再多事情也得把需求开发完和前后端联调完,再让测试验证通过,而这期间有报警问题也要第一时间处理,不然会记录个人的问题处理能力,如果报警拖久了变成事故,那就是全部人背锅了。每个季度的绩效目标也要完成,否则到了季度末绩效考核验证时,即使需求都写完,报警都处理了,绩效目标没完成也是不合格。时间就是那么多,任何事情的优先级都很高,只能自己不断加班去做。京东工作这两年都没有写过自己的博客,因为确实是没有时间,这些以后想写一个京东工作系列再详细记录下来。


坚持还是放弃


即使吐槽了很多,但压力确实让人成长,这些年是对我职业生涯的重新铸造,就像炼铁般一锤一锤反复敲打,从思维逻辑到开发能力、沟通交流等方面都有了很大的改变,自己逐渐成长为了部门最能背锅的,顶住了网关这个问题爆炸源。此时走难免不甘心,上个季度末刚拿到了A+的绩效,国企面试通过的同时也通过了京东内部晋升评审,正如自己一直喜欢开发这行,眼前也是事业逐渐越来越好,朝着预期的目标不断靠近,此时真的要激流勇退吗。


这个问题真的思考了很久很久,在北京很快乐也很痛苦,做着最喜欢的事情,但这么多年也只有自己,我慢慢认为生活不应该是这样的,生活不应该只有工作,工作是为了生活,但生活不是为了工作。回去之后的问题:


一是:工资大幅度缩水,降到生活快要不能自理,有种刚毕业的感觉。


二是:技术这方面基本就不会再有大的进步。第二点真的是让我最难以接受的,看着京东的神灯社区里面各种技术文章,职业生涯的巅峰就此打住真是非常不甘心。回去之后就有了更多的时间,不再全部投入到技术上,去找朋友,同学,家人,放下背负了太久太久的压力。


在北京感觉自己就是一节电池,现在的我有90%的电量,但如此大的压力不可能一直保持冲劲,等到了互联网人退休年龄,我还能有机会再体面的回去吗,对于北京,年轻人都是一茬一茬的韭菜,


最终还是选择了离开,带着遗憾和不舍,带着对新生活的期待。


国企的压力


与之前每次在北京换工作的压力不同,国企的压力是找不到目标,每天两点一线的生活,早上喝茶下午喝咖啡,看看文档看看资料,写一些工作总结,催一催开发进度,这些就是一天的工作内容,开始的一两个月真的有些迷失,这些就是我想要的吗,安逸圈也未免太安逸了,当你突然从强压状态下换到清闲的环境里,一时之间非常不适应,总感觉人生就要如此荒废过去,前一两个月我总想看别的工作机会,想让自己重新忙起来,以前在井底只能看到那片蓝天,现在好不容易出来拥有了大片蓝天,却又想赶紧再找到自己的井。


学习不止工作


四个月过去了,逐渐开始看清楚自己的目标,也有了一些简单的规划,现在的时间越来越多,其实完全可以做更多自己想做的事情,互联网的知识不再像以前那么集中,需要你有更大的耐心和毅力去坚持学习,可以学到的理论更多,但实践的机会比较少,以前站在巨人的肩膀上用海量的数据和流量来验证,现在还在吃过去的老本,扩充了知识的广度,而深度还停滞在那里。


目前只是摆平了自己的心态,逐渐认清形势再改变还需要时间,时间会让一切都变得更好,只要你愿意的话。以前所有的生活都给了工作,现在工作只是生活的一部分,用生活之余学到的东西继续反哺工作,提高本就不多工作的效率,也顺便去学习业务,技术在三线城市不再是唯一,而究竟如何平衡二者的关系,让自己还能继续拥有竞争力,这是我目前还看不清的。


人际关系


没有绝对的公平,但在北京是有相对的公平,而回到三线城市的国企,公平变得妙不可言,人际关系成为了重中之重,小小的部门内部已然是派系林立,十几个人的关系层级更是深不可测,想起在jd,工位后面坐着小领导,对面最在平台部负责人,管理几百人的领导也是和我们一样坐在一起,同事们经常说:在互联网公司比你大好几级的领导,和你就是平级。而在国企所有的工作,事情都有条条框框去限制,你永远看不清里面的水有多深,同时生活也被同事关系所入侵,大家经常吃饭喝酒聊工作,即使在开怀畅饮的时候也要时刻谨慎提防,说错话和做错事要比想的更加严重。在互联网公司争吵是必不可少的,不吵就说不清楚需求,甩不了锅,而回到这里,所有人都客客气气的,所有人都慈眉善目和你微笑,只是面具背后的脸很难看到。


做技术的本身比较呆板,不会八面玲珑也不会左右逢源,我只想做好自己的事情,做一个不出声的小透明,不争不抢,做自己喜欢的事情。


所见所想


记忆拉回现实,看着北京地铁上的众生相,感觉大家都很疲惫但眼里还有希望,曾经北漂的我现在只想逃离,虽然只回去四个月但依然接受不了快节奏的北京,紧绷了四年的弦已经彻底放松,去总部和过往的同事吃了个饭,大家坐着聊聊天,为他们还能坚持在北京奋斗而加油,每个人都有自己的选择,我的退缩也需要勇气,时至今日也乐得接受自己的选择的路。


如今互联网的大潮正在褪去,可能越来越多的人面临这样的选择,假如我们还有的选的话,其实生活中大部分事情我们是没得选的,生活一步一步推着你往前走。


如果问我,刚毕业选择来大城市后悔吗,我坚信自己不后悔,这里让我看到学到也付出了太多。


如果问我,现如今离开大城市后悔吗,我也坚信自己不后悔,这里没有家没有归宿,我终究要回去,只怕走的越远越迷茫。


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

你网站的网速是很快,但是在没有网络的情况下你怎么办?🐒🐒🐒

web
在现代的网络世界里,5G 网络的普及,我们可以访问一个网站或者使用一个 App 的速度极其快,但是在没有网络的情况下你啥都看不了,只能大眼瞪小眼了。 离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络...
继续阅读 »

在现代的网络世界里,5G 网络的普及,我们可以访问一个网站或者使用一个 App 的速度极其快,但是在没有网络的情况下你啥都看不了,只能大眼瞪小眼了。


离线应用是指通过离线缓存技术,让资源在第一次被加载后缓存在本地,下次访问它时就直接返回本地的文件,就算没有网络连接。


通过离线应用,主要有以下几个优点:



  1. 在没有网络的情况下也能打开网页。

  2. 由于部分被缓存的资源直接从本地加载,对用户来说可以加速网页加载速度,对网站运营者来说可以减少服务器压力以及传输流量费用。


离线应用的核心是离线缓存技术,要实现这种方式,我们可以使用 Service Worker 来实现这种缓存技术。


什么是 Service Worker


Service Worker 服务器和浏览器之间的之间的桥梁或者中间人。


Service Worker 运行在一个与页面 JavaScript 主线程独立的线程上,并且无权访问 DOM 结构。但是它能拦截当前网站所有的请求,对请求使用相应的逻辑进行判断,如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器。从而大大提高浏览体验。


注册 Service Worker


要使用 Service Worker,首先我们要判断浏览器是否支持 Service Worker,具体代码逻辑如下:


if (navigator.serviceWorker) {
window.addEventListener("DOMContentLoaded", function () {
navigator.serviceWorker.register("/worker.js");
});
}

这段代码的主要目的是在支持 Service Worker 的浏览器中,当页面加载完成后注册一个指定的 Service Worker 脚本。这个传入的 worker.js 就是 Service Worker 的运行环境。


这个脚本被安装到浏览器中后,就算用户关闭了当前网页,它仍会存在。 也就是说第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。


Service Worker 安装和激活


注册完成后,worker.js 文件会自动下载、安装,然后激活。它提供了一些 API 给我们做一些监听事件:


self.addEventListener("install", function (e) {
console.log("Service Worker 安装成功");
});

self.addEventListener("fetch", function (event) {
console.log("service worker is fetch");
});

当 install 完成并且成功激活之后,就能够监听 fetch 操作了,如上代码所示,输出结构如下图所示:


20230918074308


使用 Service Workers 实现离线缓存


在上面的内容我们已经知道了 Service Workers 在注册成功后会在其生命周期中派发出一些事件,通过监听对应的事件在特点的时间节点上做一些事情。


在 Service Workers 安装成功后会派发出 install 事件,需要在这个事件中执行缓存资源的逻辑,实现代码如下:


// 当前缓存版本的唯一标识符,用当前时间代替
const cacheKey = new Date().toISOString();

// 需要被缓存的文件的 URL 列表
const cacheFileList = ["/index.html", "/index.js", "/index.css"];

// 监听 install 事件
self.addEventListener("install", function (event) {
// 等待所有资源缓存完成时,才可以进行下一步
event.waitUntil(
caches.open(cacheKey).then(function (cache) {
// 要缓存的文件 URL 列表
return cache.addAll(cacheFileList);
})
);
});

在 install 阶段我们就已经指定了要被缓存的内容了,那么就可以在 fetch 阶段中听网络请求事件去拦截请求,复用缓存,代码如下:


self.addEventListener("fetch", function (event) {
event.respondWith(
// 去缓存中查询对应的请求
caches.match(event.request).then(function (response) {
// 如果命中本地缓存,就直接返回本地的资源
if (response) {
return response;
}
// 否则就去用 fetch 下载资源
return fetch(event.request);
})
);
});

通过上面的操作,创建和添加了一个缓存的库,如下图所示:


20230918080142


缓存更新


线上的代码有时需要更新和重新发布,如果这个文件被离线缓存了,那就需要 Service Workers 脚本中有对应的逻辑去更新缓存。


这可以通过更新 Service Workers 脚本文件做到,浏览器针对 Service Worker 有如下机制:



  1. 每次打开接入了 Service Workers 的网页时,浏览器都会去重新下载 Service Workers 脚本文件,如果发现和当前已经注册过的文件存在字节差异,就将其视为新服务工作线程。

  2. 新 Service Workers 线程将会启动,且将会触发其 install 事件。

  3. 当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。

  4. 新 Service Workers 线程取得控制权后,将会触发其 activate 事件。


新 Service Workers 线程中的 activate 事件就是最佳的清理旧缓存的时间点,代码如下:


var cacheWhitelist = [cacheKey];

self.addEventListener("activate", function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
// 不在白名单的缓存全部清理掉
if (cacheWhitelist.indexOf(cacheName) === -1) {
// 删除缓存
return caches.delete(cacheName);
}
})
);
})
);
});

这样能确保只有那些我们需要的文件会保留在缓存中,我们不需要留下任何的垃圾,毕竟浏览器的缓存空间是有限的,手动清理掉这些不需要的缓存是不错的主意。


参考资料



总结


Service Worker 作为服务器和浏览器两者之间的桥梁,它并且可以缓存技术,通过这种方式,在断网的时候,去获取缓存中相应的数据以展示给客户显示。


当断网之后,直接给他页面返回一个俄罗斯方块让他玩足一整天。


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

我入职了

web
前言 从5月底离职到现在,一个半月的时间,通过内推+BOSS直聘,前前后后约到了10家面试,终于拿到了一个满意的offer,一家做saas系统的上市公司。 本文就跟大家分享下我这段时间找工作的心路历程,欢迎各位感兴趣的开发者阅读本文。 无所畏惧 6月1号,裸辞...
继续阅读 »

前言


从5月底离职到现在,一个半月的时间,通过内推+BOSS直聘,前前后后约到了10家面试,终于拿到了一个满意的offer,一家做saas系统的上市公司。


本文就跟大家分享下我这段时间找工作的心路历程,欢迎各位感兴趣的开发者阅读本文。


无所畏惧


6月1号,裸辞的第一天,制定了接下来的每日计划,终于可以全身心投入做自己喜欢的事情啦。



  • 06:30,起床、洗漱、蒸包子

  • 07:00,日常学英语

  • 08:00,吃早餐,顺便刷一下BOSS直聘

  • 08:30,日常学算法、看面试题

  • 11:40,出门吃饭,午休

  • 14:00,维护开源项目

  • 18:00,出门吃饭,去附近的湖边逛一圈,放松下心情

  • 20:30,将当天所学做一个总结,归纳成文章

  • 23:00,洗澡睡觉,充实的一天结束


image-20230717212836044


be9877e8144d437c9a2f9ea9b188c7fe


内推情况


通过在掘金、V站和技术群发的文章,为我带来了20多个内推,从大厂到中厂到小厂,约到面试的只有4个。其他的技术部认可我,但是HR卡学历(统招本科)。


image-20230717214830630


image-20230717215823196


image-20230717215836796


image-20230718195936071


无响应式网站开发经验被拒


这是一家杭州的公司,可以远程办公,跟我约了线上面试。做完自我介绍后,他对我的开源项目比较感兴趣,问了我:



  • 你为什么会选择写一个聊天工具来作为开源项目?

  • 你的截图功能是怎么实现的?


行,那我们来聊几个技术问题吧。



  • 讲一下webpack的打包流程

  • webpack的热更新原理是怎样的?

  • 讲一下你对webpack5模块联邦的理解


这些问题回答完后,他问我你有做过响应式网站开发吗?


我:我知道怎么写一个响应式网站,在工作中我没接触过这方面的业务。


面试官:行,那你讲一下要怎么实现一个响应式网站?


我:用css3的媒体查询来实现,如果移动端跟PC端布局差异很大的话,就写两套页面,对应两个域名,服务端根据http请求头判断设备类型来决定是否要重定向到移动端。


面试官:还有其他方案吗?


我:嗯...,应该没有了吧,我只了解过这两种方式。


面试官:好吧,在seo优化方面,前端要从哪些点去考虑?


我:标签语义化、ssr服务端渲染、img标签添加alt属性来、在head中添加meta标签、优化网站的加载速度,提高搜索引擎的排名。


面试官:我的问题问完了,你有什么想了解的?


我:团队人员配比是怎么样的?


面试官:我们这个团队,前端的话有4个人,有2个后端。然后,前端有时候需要用node写一些接口。


我:如果我进去的话,主要负责哪块业务的开发?


面试官:负责一些响应式网站业务的开发,再就是负责我们内部系统的一个开发。


我:行,我的问题就这些。


面试官:OK,那今天的面试就先到这。



大概过了3天时间,也没有给我答复。因为这个是他们老板在v站看到了我的文章,觉得我还不错,加了微信,让他们技术面的我,我也不好意思问结果。


很大可能是因为我没有响应式网站的实际开发经验,所以拒了我吧。😔



期望太高被拒


这是一家上海的公司,他们的主要业务是做产品包装。有自己品牌的网站、小程序、app。他们公司一个负责公司内部事务的人加了我微信,跟我简单聊了下,让我体验下他们的产品,看看有没有什么我能帮到他们的地方。


image-20230718161603524


image-20230718161614565


image-20230718161740495


image-20230718161655844


聊完后,他一直没有主动联系我,我也没有约到其他面试,我就主动出击了,看能不能确定下来,约个面试。


image-20230718162410852


image-20230718162435259


image-20230718162606997


我整理了一套方案,发到了他的邮箱,期望薪资我写了20k,过了两天,他给了我答复,告诉我期望太高。我说薪资可以商量的,但无济于事。


image-20230718164059392


白嫖劳动力


这家公司是做物流的,是一个群友曾经面过的公司,但是最后没去。看到hr在朋友圈发了招聘信息,在招高级前端,就推给我了,约了线下面试。


到公司后,按照惯例填了一张表,写了基本信息。过了一会,一个男的来面我,让我做了自我介绍,顺着我的回答提问了公司的规模以及业务。


提问完成后,他说我看你期望薪资写了15k,你上家才12k,为什么涨幅会这么高?


我:因为我经过两年的努力以及公司业务的积累,自己的技术水平有显著提升。我对这一行很喜欢,平常80%的业余时间都用来学习了。


面试官:好,我让技术来面下你,看看你实力如何。


等了5分钟左右,他来了告诉我说:技术在开会,我先带你做一下机试吧。你把这两个页面(后台管理系统登陆页与后台首页)画出来就行。


我把页面画出来后,又过来一个人看我做的,他说 你就把页面画出来了?我说:对啊,刚才带我过来那个人说让我画页面出来的。


他说,那可能是他没说清楚,那这样肯定是不行的,你要自己重新建项目,把页面画出来后,要调接口的,把整个流程走通才行的。现在已经11点40多了,你下午再过来继续弄吧。


我直接满脸问号,把整个流程走通只是时间问题,你们这个机试到底想考察啥呢?


他说,页面在我们这里不重要,调接口,走通整个流程才重要。


我直接无语了,就说 抱歉,我下午有其他安排了,我就先走了。


image-20230718172136268


焦虑不安


时间来到6月20日,已经好多天没有约到面试了,逐渐焦虑起来了,虽然兜里余粮还有很多,但始终无法静下心来做事情,充满了对未知的恐惧。


就在这时,我还迎来了别人的嘲讽。他成功让我生气了,我努力的平复心情,告诉自己不要把这件事放在心上,通过让自己忙起来转移注意力,通过学习来克制焦虑。


image-20230718191713122


image-20230718191749187



白天我可以通过学习来缓解焦虑,但是一到晚上躺在床上,我就会开始胡思乱想。想着自己一直找不到工作怎么办,难道我真的不适合吃这碗饭吗,我怎么这么差劲,连个面试都约不到...唉,怎么会这样,我明明已经很努力了,为什么结果会是这样...



完善打招呼语


内推无望,BOSS直聘发消息也是送达、已读未回。这个时候,有个网友建议我把招呼语改改,hr不懂什么开源不开源的,他们只会关键词匹配,只要包含了,就会收你简历,于是我就把打招呼语改成了:


image-20230718195551550



招呼语改完后,效果好了一些,终于有HR愿意收我简历了🥳



学历歧视、贬低、pua、拒了offer


改完打招呼语后,我在BOSS直聘上约到了第一家面试,这家公司是做可视化VR编辑器的,团队有30来个人,BOSS直聘的薪资范围是20K~25K。


我经历了五轮面试,拿到了offer,给了18K,但是最终还是拒绝了,本章节就跟大家分享下这段故事。


技术面


技术面是去线下的,按照惯例做完自我介绍,面试官提问了我:



  • 你刚才说你写了个web端的截图插件,你能讲一下你是怎么实现的吗?

  • 我看你上家公司是做动画编辑器的,你在做这个项目的时候有遇到过哪些难点吗?

  • 你刚才提到了你为编辑器做了一些性能优化,你都做了哪些优化?

  • 你刚才说你还实现了svg类型的文本组件搜索功能,你能讲讲你是如何实现的吗?


问完这些后,他说我的问题问完了,你有什么想要了解的吗?


我:团队人员配比是怎么样的?


面试官:我们这边是重前端的,因为是做编辑器嘛,难点在前端这块,目前有4个前端,计划再招3个,再就是有几个做算法的、做c++的,1个产品经理,2个后端,2个UI,3个测试。


我:如果我进去的话,是做哪方面的项目?


面试官:你进来的话,主要是负责VR编辑器项目的,这个项目刚开始做。目前的话,比较累,会加班,基本上是早9晚8,有时候可能要10点才能走。再就是,我们这边是大小周,你能接受的吧?
我:哦哦 明白了,我可以接受


面试官:那行,你稍等下,我让我们的产品经理面下你。


产品经理面


过了一会儿,产品经理过来了。他说:我们的技术对你的评价很高,我再来面面你,你先做个自我介绍吧。做完自我介绍后,产品经理顺着我的介绍进行了提问:



  • 你刚才说你这个截图插件Gitee的产品经理在网上看到了,是码云官方的吗?

  • 我看你上家公司也是做编辑器的,你们这个产品主要面向的用户群体是哪些?

  • 你们这个产品啥时候上线的,你主要负责的是什么?

  • 你们的团队配比是怎么样的?

  • 你们在开发项目时,是如何管理git分支的?


问完这些后,他让我稍等下,让HR来面下我。


过了3分钟左右,他过来说:我们HR这会儿太忙了,抽不开身,这样,你今晚有空吧,我让她跟你电话聊聊。我回答说,7点后我都有空。


HR电话面


因为约了晚上7点的电话面试,所以我就随便吃了点,就匆匆忙忙回家等电话了。我等到了晚上9点,也没电话打过来,我就在boss直聘问了下,对方说:可能是HR忙忘了,我让她明天给你打。


晚上躺床上睡觉的时候,不出意外,我又开始胡思乱想了,心想:我这煮熟的鸭子该不会飞了吧,会不会是面试表现的不好人家婉拒我了呢,会不会是...,又焦虑了。


到了第二天下午2点多的时候,HR终于给我打了电话,问我期望薪资多少。我说22k,她问我上家薪资多少,我说12k。不出意外,她很震惊:你这涨幅也太大了吧,能说说原因吗?我说:你们这里是大小周,工作强度比较大,而且做的项目也是较为复杂的,我看BOSS直聘标的价格也是20k~25k。


她说:我们这个岗位是中、高级前端都招聘的,你这边最低能接受的薪资是多少呢?
我说:20k


她说:行,了解了,我再跟面试官对接下,晚些时候我加你微信聊。


又过了一天,她加了我微信,跟我说:我只匹配他们的中级开发岗位,让架构师再跟我聊聊。


image-20230718210811195


前端架构师面


跟架构师约的是电话面试,做完自我介绍后,他提问了我:



  • 讲一下webpack的打包原理

  • 讲一下webpack的loader和plugin

  • 讲一下webpack5的模块联邦

  • 讲一下Babel的原理,讲一下AST抽象语法树

  • 讲一下你所知道的设计模式

  • 讲一下浏览器的垃圾回收机制

  • 讲一下浏览器的渲染流程

  • 讲一下浏览器多进程的渲染优势

  • 谈谈你对浏览器架构的理解


我回答完之后,他说:我大概知道你的技术水平了。你现在的水平还不到P6,也就P5多一点,远远不及P7。


我刚才问你的问题,你每回答完一个我都问你有没有要补充的,你都说没有,我从你嘴里没听到任何性能优化相关的东西,这些知识现在还都不是你的,你只知道这么个东西,缺乏实践。就好比,我刚问了你垃圾回收机制,你回答的是chrome的,那火狐呢?edge呢?


你对你未来的规划是怎么样的?


我说:我还是以技术为主,我会继续学习来充实自己,未来如果有机会的话,希望能做到技术管理的位置。


面试官冷笑了下说:你一个大专怎么做管理?


我沉默了一会儿说:未来我会把自己的学历提升下的


面试官:你要认清自己的地位,你要想一下你的价值是什么?你能给我们公司带来什么?我们要用到three.js,你只是学过它,没有落地项目做支撑,你进来后我们还是要给你时间来熟悉项目的,跟没学过的人没啥两样。就好比,我问你three.js的坐标系用的是啥,你都不知道。
我:这个我知道,它用的是右手坐标系


面试官楞了一下说:你知道这个也没啥的,这很简单的,我们这边随便拉一个人都会这些,而且比你厉害。


我继续保持沉默。


面试官:我对你的评价就这么多,你在我们这边是能学到很多东西的,你多想想我今天跟你说的,我不知道你的业务能力怎么样,回头我再跟其他面试官聊聊,今天的面试就先到这。


第二天,HR联系我了,跟我说薪资在16k~18k左右,跟我约了下午1点30的面试。


image-20230718215534222


image-20230718215606136


老板面


到公司后,HR直接带我进了老板办公室,跟我说这个是X总,你们聊吧。 跟老板聊了一个多小时,聊的内容大概是谈人生、理想,大概能记得起的一些问题有:



  • 你觉得你是一个什么样的人?

  • 你有哪些优点?

  • 你想成为一个什么样的人?

  • 你觉得你的技术水平怎么样?

  • 如果让你给自己打标签,你会打什么标签?

  • 回看你的过往人生,你后悔吗?


考虑再三 终拒offer


从公司回来后的第二天,HR告诉我面试结束了,最终给我定的薪资是17k,发了offer。


image-20230718222724254


发了offer后,我本该高兴的,但是我却高兴不起来,那一晚我想了很多,觉得早9晚8,大小周。这个钱还是太少了,而且那个前端架构师说的话让我很不舒服,pua的气息太重了。入职后,跟这种人一起工作,我也不会开心。思考再三后,我最终还是拒掉了这个offer。


image-20230718222252549


image-20230718222337117


比较钟意的小外企


这是我在BOSS直聘约到的第二家面试(15k~20k),面试体验很好。到公司后,接待我的人很有礼貌,告诉我前端是技术总监来面的,他还没来,你先坐着等他一会儿。


等了一会儿后,看到了技术总监,主动跟我握了个手。然后说:他临时有个会开,让我稍等下他,然后安排我在会议室坐了会儿,倒了一杯水给我。


我在会议室坐了40多分钟,他会开完了,喊我去办公室聊,按照惯例做完自我介绍后。他问我:



  • 你刚才提到了你做了编辑器的性能优化,你具体是怎么做的?

  • 你们这个编辑器前端编辑的应该是dom吧,最后生成的视频是怎么生成的?

  • 我看你的项目经验都是vue,你应该对vue全家桶都很熟了吧?


问完这些问题后,他用笔记本打开了我简历上的项目,边看边问我这块你是怎么实现的,有没有遇到过啥问题,你是怎么解决的。项目看完后,他说你技术没问题,我了解完了。我跟你介绍下我们这边的项目,我们在做...。介绍完了后,他问了我离职原因,以及我的期望薪资。


我说了20k,他说,站在客观角度来说,你的学历是大专,在我们这里拿到这个数很难,我们也不是什么特别有钱的公司。但是,我们的产品是很有发展前景的,已经拿了一轮800w美金的融资了,这个岗位我在boss直聘挂了1个月了,收到了300多份简历,有很多大厂出来的,但是我都不太满意,偶然间看到你的简历,觉得你是一个爱学习、肯钻研的人,就约你来面试了。你是我面的第一个前端。


我听他这么说后,我就说:那薪资17、18也可以。


他说:行,明白了,我回头跟老板说说,尽量帮你争取。我们这边工作氛围很棒,团队是一支很精湛的团队组成的,我们这边做算法的是麻省理工毕业的,这边的一个后端是之前抖音短视频架构组出来的。你在这里也能学到很多前端之外的东西,我们是早上10点上班,晚上6点30下班,不打卡,双休。


我听他这么说后,觉得很不错,就说:那15k也行。


他说:你也不用太勉强,不然你进来了也不开心,我们这里发展空间很大的,未来拿到更多的融资,你在这里是可以涨薪的。那今天我们就先到这里,后天就是端午节了,这样,我端午节后的那周给你具体的答复。


就这样,我又进入了焦灼的等待期。


端午节后的第2天,那边还没答复,我就主动问了下,他给我的答复是:


image-20230721214840273


又过了3天,一直没约到面试,焦虑的很。我就又厚着脸皮问了下情况,得来的答复是他们还没找到合适的产品经理。(这个时候,心里很难受到极点了,泪水在眼珠里打转,我焦虑到哭了😔)


image-20230721215034742



晚上躺在床上又开始胡思乱想了,觉得老天很不公平,为什么好运总是不能降临到我头上。唉...就这样想着想着,不知想了多久,也不知道自己睡着了没,只记得手机的闹钟响了,关了闹钟继续睡去了...



随遇而安


又浑浑噩噩的过了几天,时间来到7月3日,BOSS直聘有人跟我约面试了,一天下来约了3个面试,都是很多天之前联系的,今天才收了我简历,我的心情终于好了一些。


做物联网的公司


这家公司距离我住的地方很近,步行1.1公里就能到。BOSS直聘标的价格是(15k~18k),到了公司后,前台让我扫二维码关注他们的公众号,填写面试登记表(基本信息、期望薪资、上家公司薪资)。


填写完后,前台带我进了公司,等了5分钟左右,面试官来了,按照惯例做完自我介绍后,他问了我:



  • 你讲一下vue双向绑定的原理

  • 讲一下vue3相比vue2,它在diff算法上做了哪些优化?

  • Vue2为什么要对数组的常用方法进行重写?

  • Vue的nextTick是怎么实现的?

  • 讲一下你对EventLoop的理解吧

  • 讲一下webpack5的模块联邦


这里我讲一下EventLoop这个问题吧,我回答完之后,他反问我:你确定宏任务先执行的吗?我很确信的说,是的,宏任务先执行的。(之所以这么自信是因为我之前特意研究了这方面的知识,写了大量的用例做验证,写了文章做总结,绝对错不了)


那你意思是,setTimeoutPromise().then()先执行,


我回答:是的。


面试官:你回去再查查资料吧,看一看到底是哪个先执行吧。我的问题问完了,你有什么想问我的吗?


我问了他部门做的产品是什么、团队情况、如果我进来的话负责的是哪块的东西。了解完之后,他让我稍等下。


过了3分钟左右,HR过来了,她问我觉得这场面试咋样,刚才面你的人职级在我们这里算是比较高的了,然后她就跟我介绍了她们公司的情况以及福利制度。介绍完之后,她问我说:我对你写的这个期望薪资比较好奇,我看你上家薪资是12k,怎么期望薪资写了18k呢?涨幅这么高。


我说了理由后,她说:今年市场很差,求职者很多,很多公司都在降低成本,你要是放在互联网红利的时候,你这个涨幅没问题,但2023年这个大环境,你这个涨幅是不可能的。你这边最低期望薪资是多少?


我说:16k,她在求职表上用笔写了下。随后她说,那行,今天的面试就先到这,后面我们电话联系。


回到家后,我立马查了我写的那篇事件循环的文章,验证下我有没有记错。看完之后我发现我并没有记错,于是我又问了下AI,他给我的答案是:


image-20230722182035941


我就纳闷儿了,于是我说宏任务先执行的吧,它的回答是:


image-20230722182223460


它还在嘴硬,我就反问了句,你确定?它终于改变口风了。


image-20230722182301304



这家公司是7月5号面的,等了3天都没联系我,看来是有人要价比我低🌚



做交易所的公司


这家公司是在一个技术交流群看到的招聘信息,公司在海外,远程办公的方式,给的薪资是20k~25k。按照惯例做完自我介绍后他问我:



  • 讲一下vue的生命周期

  • 讲一下computed与watch的区别

  • 讲一下vue的双向绑定和原理

  • 讲一下vue3相比vue2有哪些提升

  • 你有开发过不用脚手架的项目吗?

  • seo优化有了解过吗?讲一下你的见解

  • 响应式网站开发你知道哪些方案?


回答完这些问题后,按照惯例我问了他团队的人员情况以及项目情况,就结束了这场面试。他问的问题也很简单,我回答的也不错。但是,过了3天,最终还是没下文。


做工具软件的公司


这家公司是朋友内推的,经历了三轮面试,我看了下BOSS直聘标价是15k~25k。先是用腾讯会议,让打开屏幕共享和摄像头,做一份笔试题。内容是填空题、判断题、代码题。填空跟判断就是一些简单的问题,代码题是:



  • 观察一组数列,写一个方法求出第31个数字是什么?(通过观察后,发现那是一组斐波那契数列

  • 实现一个深拷贝函数

  • 写一个通用的方法来获取地址栏的某个参数对应的值,不能使用正则表达式。


线上技术面


笔试题做完发给HR后,等待了半个小时,面试官进入了腾讯会议,按照惯例做完自我介绍后他问我:



  • vue3的diff算法做了哪些改进

  • vue双向绑定的原理是什么

  • 假设要设计一个全局的弹窗组件你会怎么设计?

  • 如果这个弹窗组件可以弹出多个,消息会垂直排列,新消息会把旧消息顶起来,每个消息都可以设置一个停留时间,到了时间后就会消失,这一块你会怎么设计?

  • 你了解堆这种数据结构吗?讲一讲你对它的理解


回答完这些问题后,我按照惯例问了他项目情况以及我进去后所负责的模块,就结束了这场线上面试,第二天收到了一面通过的答复。


image-20230722234026788


线下总监面


时间来到7月6日,本来是7月5日面试的,但是面试官临时有事改了时间。


image-20230722234450217


这家公司在林和西地铁站这边,地处CBD,公司应该是很有钱的。到了公司后,HR接待了我,带我进了会议室,等了3分钟左右,技术总监过来了,做完自我介绍后,他问我:



  • 挑一个你最拿手的项目讲一下吧

  • 看你写了很多开源项目,是个爱捣鼓的人,讲一下你的开源项目吧

  • 你会Java,是用的SpringBoot吗?你讲一下你这个开源项目的后端服务是怎么设计的吧

  • 你都知道哪些数据库?进行SQL查询时,你有哪些优化手段来优化查询效率

  • 你讲下vue3和vue2的一个区别吧

  • 你觉得你跟别人相比,你的优势是什么?


回答完这些问题后,我问了他团队的规模以及公司的人员情况,他跟我说:我们公司总共有52个人,很大一部分都是程序员,他们都是全能的,任何一个人拉出来,前端、后端、运维都能做,就好比你让运维来写前端的业务代码他也能写,你也看到了,我们目前不缺人,是想招一个优秀的人做候补。我们这边的技术栈是vue和Electron,你进来的话,负责前端页面以及一些node后端服务的编写。你稍等下,我让我们的HR来面下你。


线下HR面


等了4分钟左右,HR来了,她带我去到了另一个会议室聊,她问了我:



  • 你的离职原因是什么?

  • 你对新工作的期望是怎么样的?

  • 如果公司让你休年假,你必须要做一件事情,你会做什么事情?


问完这些问题后,她问了我期望薪资,我说了20k,她说了一些其他的东西,大概意思就是给不到的话你最低期望是多少,我说18k。


她说:行,了解了,我们这边要做一下横向对比,尽快给你答复,你放心无论结果如何,我们都会给你一个答复的。


面试完的第二天,那个hr跟我发消息说结果还没定。


image-20230723002131979


进入新的一周后,她给我发来了感谢信。


image-20230723002232232



只能感叹卷王太多了,全干工程师的价格已经被你们打到18k以下了👍



做旅游的公司


这是一家在BOSS直聘上约到的面试(11k~17k),到了公司后,HR先让我做了一份笔试题,这份笔试题全是八股文,我把答案短的都写了,比较长的就写了面试时候讲。


做完笔试题后,她带我进了会议室,是两个人面我,一个是前端负责人,另一个是他的领导,做完自我介绍后,那个前端负责人说:我之前在网上看到过你的截图插件,写的很不错。我相信你的技术肯定没问题的,他和他的领导交叉问了我问题:



  • vue3相比vue2做了哪些提升?

  • 讲一下vue的diff算法吧

  • 讲一下V8的垃圾回收机制

  • 讲一下chrome是如何渲染一个网页的

  • 大文件分块上传以及断点续传,你会怎么实现


回答问这些问题后,他们让我稍等下,找来了HR跟我聊,HR问了我期望薪资,我说17K,她也惊讶的说,你上家才给你12k,你怎么一下子要求涨幅这么多,是出于什么考虑呢?我说了理由后,她说:结合我们公司的情况和制度,我们这边给不到你这么多。


我:那大概能给到多少呢?


HR:15k,有些事情我要提前跟你说清楚,我们这边试用期是一个月,现在项目组比较忙,是需要加班的,基本上是996,大概要忙到9月份,项目第一期做好后,就可以按照正常时间上下班了。忙的这段时间是可以累积调休的。试用期不缴纳社保,我们只有五险,没有公积金。


我听了这些后,头皮发麻,一时不知道说啥,我就说了:哦哦 好


HR:如果你能接受的话,我这边是没问题的。


我:我要考虑考虑,晚些时候给你答复。


到了第二天,HR在boss直聘上给我发了消息,问我考虑的如何了,我拒绝了她。


image-20230723004628907


做saas系统的上市公司


这家公司是我6月13号在BOSS直聘上沟通的,6月27号收了我简历,7月3号跟我约了面试,一直持续到7月14号,经历了三轮面试,最终拿到了offer。


HR面(线上)


按照惯例做完自我介绍后,HR让我介绍下公司的产品,以及我在公司的一个职位,技术水平在公司排第几,为什么离职,职业规划和一些其他问题:


HR:你能接受出差吗?


我:这个看情况,如果距离不是很远,出差时间不超过1周,交通、住宿这些都能报销的话,我是接受的。


HR:交通、住宿这些肯定都报销,不然谁愿意出差,我们除了这个外,每天还有一个xxx块的补贴。你在广州这边,出差的话就是去深圳,一般也就去个3、4天,你是前端,几乎不怎么出差。


我:哦哦 那可以的


HR:你对加班是怎么看的?


我:加班的话,如果是项目比较急,我是没问题的,但是如果是其他原因的一些强迫加班,我就不太能接受了


HR:我们这边加班的话,是项目比较急的时候才会,加班不会太频繁。如果加班的话,是可以1:1兑换成调休的,法定节假日加班的话,我们会按照法律规定发放3倍工资


我:哦哦 行


HR:你这边是在广州,如果面试通过的话,是广州的编制。我们广州分部在xx,距离这块的话,你能接受吧?


我:我有查过公司的位置,从我住的这边过去也挺近的,40分钟左右就到了,我可以接受


HR:那行,今天的面试就先到这,后面会安排我们的技术面下你。


技术面(线上)


HR面完后,过了一天,跟我约了技术面。


image-20230723083059122


时间来到7月5号,一男一女,两个人一起面的我。按照惯例做完自我介绍后,他们问了我:



  • 我看你写了很多开源项目和技术文章,这是一个很好的习惯,能很多年坚持做一件事,并且能把这件事情做好,你很厉害。

  • 刚才听你自我介绍说你会Java,你Java目前是一个什么水平?

  • 我看你们公司项目是做web动画编辑器的,你在这个项目中担任的角色是什么?有没有什么印象比较深刻的难题,你是如何解决的?

  • 我看你简历上还写了一个海外项目的重构经验,你能介绍下这个项目吗?以及你在这里面担任的角色是什么?

  • 我看你简历上的项目都是以Vue为主的,那你应该对Vue很熟悉,你讲一下watch与computed的区别

  • vue中组件通信都有哪些方式?

  • vuex刷新后数据会丢失,除了把数据放本地存储外,你还知道其他什么方法吗?

  • 我看你写的那个截图的开源项目用到了canvas,你应该对canvas很熟悉了吧,有这样一个场景:超市中的货架,上面有很多商品。现在要把这个货架用canvas画出来,商品需要支持一些交互,调整大小,移动位置,你会怎么实现?


问完这些问题后,按照惯例,我问了下他们的团队情况以及所做的业务,我进去后所负责的模块,就结束了这场面试。


事业部总经理面(线上)


过了一天,告知我技术面通过了,跟我约了第二天的面试,我看到她说:总经理同时面我跟其他两位候选人。我就压力有点大,从业4年了,第一次遇到这种大场面😂


image-20230723084854849


image-20230723085150444


到了约定好的面试时间,我跟其他两位候选人都进入了会议,过了10分钟,总经理还是没有进来,我就私聊问了下HR。过了一会儿,HR进入了会议。她说:总经理临时有点事情,要换个时间约面试了,真不好意思。


image-20230723085623543


时间来到7月10号,总经理进入腾讯会议后,他先让我们轮流做自我介绍,然后抛出问题,让我们挨个回答,最后他做了总结,给我们三个人做了评价:



  • A(1号面试者):你的组织协调能力应该不错

  • B(我):我看了你在掘金上发的文章以及个人网站,能看出来你的技术实力是最强的。

  • C(3号面试者):你的业务能力应该不错


说完这些后,总经理说晚上会抽时间再单独打电话给我们再聊聊,到了第二天早上我一直没等到电话,我就问了下HR。


image-20230723090532956


过了半个小时左右,电话打来了,他问了我离职原因和两个场景题:



  • 前端的框架有很多,当有新项目的时候,你会通过哪些方面来考虑应该使用哪个框架?

  • 有一个上线的项目它是vue2写的,如果想升级到vue3,但是没有太多的专用时间来做这件事,此时你会怎么做?


回答完这些问题后,挂断了电话,下午1点40多的时候,HR联系我说面试通过了,开始走发offer流程了,到时候会有她的另一个同事联系我。


时间来到7月14号,第一面面我的那个人打电话给我了,跟我聊了薪资、福利制度和五险一金,她说我们公司的五险一金是按照实际工资进行缴纳的,没有绩效,有季度奖和年终奖,会按照公司的盈利情况以及你的工作表现进行发放,后面还有其他问题的话,你随时联系加你微信的那个HR,她是华南区域的负责人。


电话挂断后,过了2小时左右吧,HR联系我说发offer了,我突然想到忘记问上下班时间了,我就确认了下(BOSS直聘标记了时间)。


image-20230723093034336


image-20230723092444819



截止发文时间,我已经入职这家公司很多天了,团队氛围很棒。入职的第一天下午,我接到了我们主管的电话,他让我第二天去一趟武汉,事业部的总经理是在武汉分部的,他要见一下你,那边也有前端在,跟你讲解下业务,熟悉熟悉团队的人。


广州这边的后端架构师同事告诉我出差是不需要自己花钱的,公司内部有一个平台可以直接在上面定高铁票和酒店,我的内部OA和钉钉账号后,他教了我怎么操作。


来武汉后,跟这边的团队成员熟悉了下,聊了下业务,主管告诉我说大概7月26号左右就可以回广州了。我们是双休,我入职后的第一个周六、日是在武汉过的,在这边跟群友面了基,逛了下附近的粮道街,去了玫瑰街、黄鹤楼等地方🥳



作者:神奇的程序员
来源:juejin.cn/post/7258952063219384376
收起阅读 »

一个古诗文起名工具

web
大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。 这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词...
继续阅读 »

大家好,我是 Java陈序员,我们常常会为了给孩子取名而烦恼,取名不仅要好听而且要规避大众化。其实,我们中华文化博大精深,可以借鉴先辈文人们留下的经典诗词中的文字来起名。今天,给大家介绍一个古诗文起名的工具。


这个工具支持从《诗经》、《楚辞》、《唐诗》、《宋词》、《乐府诗集》、《古诗三百首》、《著名辞赋》等经典中来生成不同的名字。


Img


我们可以根据自己的姓氏来生成名字,例如《陈》姓:
Img


一次性可以生成六个姓名,并有对应的诗句来源说明,是不是很nice呢!


再比如,《李》姓:
Img


当然了,这个项目没有任何人工智能, 没有判断名字价值的目标函数,所以都是随机生成的。因此可以孕育出一些惊艳、惊鸿一瞥的名字,反之也会生成智障、搞笑的名字,大家可自行甄别。


大家如果对于这个项目感兴趣的话,也可自行下载代码到本地运行:


# 克隆代码
git clone https://github.com/holynova/gushi_namer.git

# 安装依赖
npm install

# 本地调试
npm start

# 编译
npm run build

或者直接使用线上地址:


http://xiaosang.net/gushi_namer/

线上地址也是完美支持移动端的。


Img


大家快把这个地址收藏到收藏夹吃灰吧,以免需要的时候找不到!


最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7282692430100201535
收起阅读 »

限流:别说算法了,就问你“阈值”怎么算?

基础 限流是通过限制住流量大小来保护系统,它尤其能够解决异常突发流量打崩系统的问题。 算法 限流算法也可以像负载均衡算法那样,划分成静态算法和动态算法两类。 静态算法包含令牌桶、漏桶、固定窗口和滑动窗口。这些算法就是要求研发人员提前设置好阈值。在算法运行期间...
继续阅读 »

基础


限流是通过限制住流量大小来保护系统,它尤其能够解决异常突发流量打崩系统的问题。


算法


限流算法也可以像负载均衡算法那样,划分成静态算法和动态算法两类。



  • 静态算法包含令牌桶、漏桶、固定窗口和滑动窗口。这些算法就是要求研发人员提前设置好阈值。在算法运行期间它是不会管服务器的真实负载的。

  • 动态算法也叫做自适应限流算法,典型的是 BBR 算法。这一类算法利用一系列指标来判定是否应该减少流量或者放大流量。动态算法和 TCP 的拥塞控制是非常接近的,只不过 TCP 控制的是报文流量,而微服务控制的是请求流量。


令牌桶


系统会以一个恒定的速率产生令牌,这些令牌会放到一个桶里面,每个请求只有拿到了令牌才会被执行。每当一个请求过来的时候,就需要尝试从桶里面拿一个令牌。如果拿到了令牌,那么请求就会被处理;如果没有拿到,那么这个请求就被限流了。


漏桶


漏桶是指当请求以不均匀的速度到达服务器之后,限流器会以固定的速率转交给业务逻辑。


漏桶是绝对均匀的,而令牌桶不是绝对均匀的。


固定窗口与滑动窗口


固定窗口是指在一个固定时间段,只允许执行固定数量的请求。比如说在一秒钟之内只能执行 100 个请求。


滑动窗口类似于固定窗口,也是指在一个固定时间段内,只允许执行固定数量的请求。区别就在于,滑动窗口是平滑地挪动窗口,而不像固定窗口那样突然地挪动窗口。


限流对象


可以是集群限流或者单机限流,也可以是针对具体业务来做限流。


针对业务对象限流,这一类限流对象就非常多样。



  • VIP 用户不限流而普通用户限流。

  • 针对 IP 限流。用户登录或者参与秒杀都可以使用这种限流,比方说设置一秒钟最多只能有 50 个请求,即便考虑到公共 IP 的问题,正常的用户手速也是没那么快的。

  • 针对业务 ID 限流,例如针对用户 ID 进行限流。


限流后的做法



  • 同步阻塞等待一段时间。如果是偶发性地触发了限流,那么稍微阻塞等待一会儿,后面就有极大的概率能得到处理。比如说限流设置为一秒钟 100 个请求,恰好来了 101 个请求。多出来的一个请求只需要等一秒钟,下一秒钟就会被处理。但是要注意控制住超时,也就是说你不能让人无限期地等待下去。

  • 同步转异步。它是指如果一个请求没被限流,那就直接同步处理;而如果被限流了,那么这个请求就会被存储起来,等到业务低峰期的时候再处理。这个其实跟降级差不多。

  • 调整负载均衡算法。如果某个请求被限流了,那么就相当于告诉负载均衡器,应该尽可能少给这个节点发送请求。


亮点


突发流量



漏桶算法非常均匀,但是令牌桶相比之下就没那么均匀。令牌桶本身允许积攒一部分令牌,所以如果有偶发的突发流量,那么这一部分请求也能得到正常处理。但是要小心令牌桶的容量,不能设置太大。不然积攒的令牌太多的话就起不到限流效果了。例如容量设置为 1000,那么要是积攒了 1000 个令牌之后真的突然来了 1000 个请求,它们都能拿到令牌,那么系统可能撑不住这突如其来的 1000 个请求。



请求大小


如果面试官问到为什么使用了限流,系统还是有可能崩溃,或者你在负载均衡里面聊到了请求大小的问题,都可以这样来回答,关键词是请求大小。



限流和负载均衡有点儿像,基本没有考虑请求的资源消耗问题。所以负载均衡不管怎么样,都会有偶发性负载不均衡的问题,限流也是如此。例如即便我将一个实例限制在每秒 100 个请求,但是万一这个 100 个请求都是消耗资源很多的请求,那么最终这个实例也可能会承受不住负载而崩溃。动态限流算法一定程度上能够缓解这个问题,但是也无法根治,因为一个请求只有到它被执行的时候,我们才知道它是不是大请求。



计算阈值


总体上思路有四个:看服务的观测数据、压测、借鉴、手动计算。


看服务的性能数据属于常规解法,基本上就是看业务高峰期的 QPS 来确定整个集群的阈值。如果要确定单机的阈值,那就再除以实例个数。所以你可以这样来回答,关键词是业务性能数据。



我们公司有完善的监控,所以我可以通过观测到的性能数据来确定阈值。比如说观察线上的数据,如果在业务高峰期整个集群的 QPS 都没超过 1000,那么就可以考虑将阈值设定在 1200,多出来的 200 就是余量。 不过这种方式有一个要求,就是服务必须先上线,有了线上的观测数据才能确定阈值。并且,整个阈值很有可能是偏低的。因为业务巅峰并不意味着是集群性能的瓶颈。如果集群本身可以承受每秒 3000 个请求,但是因为业务量不够,每秒只有 1000 个请求,那么我这里预估出来的阈值是显著低于集群真实瓶颈 QPS 的。



压测



不过我个人觉得,最好的方式应该是在线上执行全链路压测,测试出瓶颈。即便不能做全链路压测,也可以考虑模拟线上环境进行压测,再差也应该在测试环境做一个压力测试。



从理论上来说,你可以选择 A、B、C 当中的任何一个点作为你的限流的阈值。


A 是性能最好的点。A 之前 QPS 虽然在上升,但是响应时间稳定不变。在这个时候资源利用率也在提升,所以选择 A 你可以得到最好的性能和较高的资源利用率。


B 是系统快要崩溃的临界点。很多人会选择这个点作为限流的阈值。这个点响应时间已经比较长了,但是系统还能撑住。选择这个点意味着能撑住更高的并发,但是性能不是最好的,吞吐量也不是最高的。


C 是吞吐量最高的点。实际上,有些时候你压测出来的 B 和 C 可能对应到同一个 QPS 的值。选择这个点作为限流阈值,你可以得到最好的吞吐量。


性能 A、并发 B、吞吐量 C。


无法压测:



不过如果真的做不了,或者来不及,或者没资源,那么还可以考虑参考类似服务的阈值。比如说如果 A、B 服务是紧密相关的,也就是通常调用了 A 服务就会调用 B 服务,那么可以用 A 已经确定的阈值作为 B 的阈值。又或者 A 服务到 B 服务之间有一个转化关系。比如说创建订单到支付,会有一个转化率,假如说是 90%,如果创建订单的接口阈值是 100,那么支付的接口就可以设置为 90。



如果我这是一个全新的业务呢?也就是说,你都没得借鉴。这个时候就只剩下最后一招了——手动计算。



实在没办法了,就只能手动计算了。也就是沿着整条调用链路统计出现了多少次数据库查询、多少次微服务调用、多少次第三方中间件访问,如 Redis,Kafka 等。举一个最简单的例子,假如说一个非常简单的服务,整个链路只有一次数据库查询,这是一个会回表的数据库查询,根据公司的平均数据这一次查询会耗时 10ms,那么再增加 10 ms 作为 CPU 计算耗时。也就是说这一个接口预期的响应时间是 20ms。如果一个实例是 4 核,那么就可以简单用 1000ms÷10ms×4=400 得到阈值。




手动计算准确度是很差的。比如说垃圾回收类型语言,还要刨除垃圾回收的开销,相当于 400 打个折扣。折扣多大又取决于你的垃圾回收频率和消耗。



升华:



最好还是把阈值做成可以动态调整的。那么在最开始上线的时候就可以把阈值设置得比较小。后面通过观测发现系统还很健康,就可以继续上调阈值。





此文章为9月Day25学习笔记,内容来源于极客时间《后端工程师的高阶面经》


作者:09cakg86qfjwymvm8cd3h1dew
来源:juejin.cn/post/7282245376425459768
收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里就可以精准扔出去了。


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

降本增效后胡诌一下

上周我ld下午突然找我喝咖啡,暗示的事情不言而喻,果然下一波降本增效不期而遇了,当然这次我是主动要的桶,说句实话此时此刻我不太看好阿逼,几次降本之后明显能感觉到人心早就散了,即使留着我估摸着也找不到我喜欢的工作状态了。 另外啊,人到中年的我心态也还是不太稳定啊...
继续阅读 »

上周我ld下午突然找我喝咖啡,暗示的事情不言而喻,果然下一波降本增效不期而遇了,当然这次我是主动要的桶,说句实话此时此刻我不太看好阿逼,几次降本之后明显能感觉到人心早就散了,即使留着我估摸着也找不到我喜欢的工作状态了。


另外啊,人到中年的我心态也还是不太稳定啊,现在整个市场行情挺差的,基本上来说这周也就几家公司约了我面试,我这个时候才感受到之前别人说的手机没响是多么恐怖,相对来说竞争力确实是完全比不上年轻人了。


好在过了几天压力期之后这几天仿佛也想开了,想了想错的也不是我们这些浮萍,而是这个世界。不求一生安好,但求问心无愧吧。


另外其实还有好多想做的事情并没有做完,也还是挺遗憾的。比如最新的kotlin和compose,还有我最近刚打算推进的资源文件治理等等。也算是抱憾而去了啊。


另外前几天那个5000星github大佬也让我有点大大的破防,被人称呼为七年大龄我还是不李姐啊,成年人的世界还真的是很残忍啊


愿后续找工作顺利,对我自己来说吧,我觉得我还是处于技术人当打之年的,我也还是想做些有意思得事,在此与诸君共勉。


年纪越大越喜欢老歌,这几天只能靠沉默是金来安慰自己。冥冥中都注定你我苦与贫,是错永不对真还是真。


作者:究极逮虾户
来源:juejin.cn/post/7281162206947622949
收起阅读 »