注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android单元测试

本文主要内容 1、单元测试介绍 2、java单元测试 3、android单元测试 4、常用方法介绍 1、单元测试介绍 单元测试,是指对软件中的最小可测试单元进行检查和验证。 在Java中,最小单元可以是类也可以是方法,比如刚刚开发完成一个下载的方法,此时可...
继续阅读 »

本文主要内容



  • 1、单元测试介绍

  • 2、java单元测试

  • 3、android单元测试

  • 4、常用方法介绍


1、单元测试介绍


单元测试,是指对软件中的最小可测试单元进行检查和验证。


在Java中,最小单元可以是类也可以是方法,比如刚刚开发完成一个下载的方法,此时可以用单元测试其是否ok。如果不用单元测试,用手写代码调用的方式,则工作量会较大。


使用Android studio进行单元测试,一共有两种类型,一种就是普通的java单元测试,另一种就是android单元测试,android单元测试包括对ui测试,activity的相关方法进行测试等等,需要context参数


image.png


进行单元测试需要引入对应的依赖。

testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'

前面3个依赖包,在创建工程的时候会默认加进来,最后一个貌似不会默认添加,需要手动添加。最后一个依赖包与activity相关的单元测试有关。


2、java单元测试


以一个最简单的例子,计算器为例:

public class Util {

public static int add(int a, int b){
return a + b;
}

public int addInt(int a, int b){
return a + b;
}
}

Util类中有一个静态方法,一个非静态方法,都是简单的相加逻辑。接下来,可以右键选中方法,然后点击goto选项,生成对应的单元测试文件。





最后一步中可以选择为当前类中的哪些方法添加单元测试,也可以勾选before和after两个选项,顾名思义,before和after方法分别在单元测试前后调用,我们可以在这两个方法中做一些事情,例如初始化、回收等等。

public class UtilTest {

Util util;

@Before
public void setUp() throws Exception {
util = new Util();
System.out.println("sutup");
}

@After
public void tearDown() throws Exception {
System.out.println("tearDown");
}

@Test
public void add() {
assertEquals(2,Util.add(1, 1));
}

@Test
public void addInt() {
assertEquals(2, util.addInt(1,1));
}
}

Util类中,写了一个静态方法和非静态方法,其实就是为了演示 setUp 方法的作用,如果在单元测试中需要初始化一些类,则可以在 setUp 中初始化,在测试方法中使用已经初始化过的实例即可。


Java单元测试运行依赖于 JVM,执行单元测试方法非常简单,右键单元测试文件执行即可,也可以选择某个方法,只执行这一个方法。


3、android单元测试


Android单元测试,它依赖于Android的执行环境,也就是需要在android机器上运行。与java单元测试相比,它有一点点的不同。


前一章中讲过java单元测试,提到了 before 和 after 这两个选项,有点类似于切面编程,可以在其中做一些初始化的动作。但android中最常用的是activity,如何在activity中也添加一些周期回调函数呢?

@Rule
public ActivityTestRule rule = new ActivityTestRule(MainActivity.class){
@Override
protected Intent getActivityIntent() {
Intent intent = new Intent();
intent.putExtra("data","world");
return intent;
}

@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Log.i("okunu","before");
}
};

通过如上方式添加activity相关的单元测试周期回调函数。


getActivityIntent ,顾名思义,对启动activity的intent进行测试封装,上例中就添加了相关的参数。值得注意的是,为何 intent 中没有添加 action 呢?我猜想就是 ActivityTestRule 对象已经与MainActivity相关联了,它就是要去启动MainActivity的,加不加action都无所谓了。这里也隐含了另一层意思,要对某个activity相关的任何方法进行单元测试,都要添加与之相关联的ActivityTestRule 对象。


beforeActivityLaunched ,就是在activity启动之前执行的函数


本例中,有一个EditText,TextView和一个Button,点击Button,将EditText中的文字显示到TextView,同时也会接收Intent中的相关参数,显示在TextView中

public class MainActivity extends AppCompatActivity {

String mData;
TextView text;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mData = getIntent().getStringExtra("data");
text = (TextView)findViewById(R.id.text);
text.setText(mData != null ? mData : "");
}

public void sayHello(View view){
EditText edit = (EditText)findViewById(R.id.edit);
String str = "hello " + mData + " " + edit.getText().toString() + " !";
text.setText(str);
}
}

它的单元测试类依然可以和第2节一样生成,我们看看详细代码:

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

@Rule
public ActivityTestRule rule = new ActivityTestRule(MainActivity.class){
@Override
protected Intent getActivityIntent() {
Intent intent = new Intent();
intent.putExtra("data","world");
return intent;
}

@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Log.i("okunu","before");
}
};

Context appContext;
@Before
public void setUp() throws Exception {
Log.i("okunu","setUp");
appContext = InstrumentationRegistry.getTargetContext();
}

@After
public void tearDown() throws Exception {
Log.i("okunu","tearDown");
}

@Test
public void sayHello() {
onView(withId(R.id.edit)).perform(typeText("jim"), closeSoftKeyboard()); //line 1
onView(withText("hello")).perform(click()); //line 2
String expectedText = "hello " + "world " + "jim" + " !";
onView(withId(R.id.text)).check(matches(withText(expectedText))); //line 3
}
}

注意,context是可以获取的。另外最重要的就是理解这几个生命周期回调函数的作用。可以在setUp函数中获取context,如果与activity启动相关的要改动,则在ActivityTestRule类中修改即可。


4、常用方法介绍


在android单元测试中需要获取到某个view,如何获取呢?



  • withText:通过文本来获取对象,如:ViewInteraction save = onView(withText(“保存”)) ;

  • withId:通过id来获取对象,如:ViewInteraction save = onView(withId(R.id.save)) ;


通过文本获取,如上例,如果某个view上的文本是“保存”,则返回此view。通过id获取就比较容易理解了,建议使用id方式。


那么对view操作的接口又有哪些呢?


使用方式是onView(…).perform() 。也可以执行多个操作在一个perform中如:perform(click(),clearText()) 。所有的操作都有一个前提 ———— 就是要执行的view必须在当前界面上显示出来(有且可见)。

方法名含义
click()点击view
clearText()清除文本内容
swipeLeft()从右往左滑
swipeRight()从左往右滑
swipeDown()从上往下滑
swipeUp()从下往上滑
click()点击view
closeSoftKeyboard()关闭软键盘
pressBack()按下物理返回键
doubleClick()双击
longClick()长按
scrollTo()滚动
replaceText()替换文本
openLinkWithText()打开指定超链

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

写了个程序员真香的简历制作工具

看效果 不废话直入主题,这里是编辑页面,你可以来尝试一下 用法 左边写Markdown内容,右边显示渲染效果,纯前台数据,不涉及数据库 扩展语法 Markdown本身并没有实现多列布局,以及针对简历布局的需要,所以我自己写了个插件来支持这些语法并实现Mark...
继续阅读 »

看效果


不废话直入主题,这里是编辑页面,你可以来尝试一下


image.png


用法


左边写Markdown内容,右边显示渲染效果,纯前台数据,不涉及数据库


扩展语法


Markdown本身并没有实现多列布局,以及针对简历布局的需要,所以我自己写了个插件来支持这些语法并实现Markdown的一些常用语法(只实现了常用的),比如多列布局、图标、个人信息、主体内容布局语法


多列布局


实现一个三列布局,原理就是flex

::: start
第1列
:::
第2列
:::
第3列
::: end

多列布局显示效果


image.png


图标语法


下面的语法将被解析为<i></i>,可以通过下面即将介绍的自定义CSS来对其进行样式设置,感兴趣可以自行尝试一下.

icon:github

图标显示效果


image.png


其他语法


更多可以查看这里的语法助手


工具栏


为了更自由的定制化需求,暴露了一些工具提供使用,这里以编写CSS为例,写的CSS可以直接作用在简历模板上


image.png


编写CSS这里所有的样式都需要写在.jufe容器下面,以防影响到其他节点的样式


image.png


设置后的效果


image.png



其他小功能比如自定义主题配色、自定义字体颜色、调节模板边距等可以自行尝试一下,就不过多赘述了,都是比较实用的功能



两种编辑模式


提供了两种编辑模式,markdown模式内容模式,提供内容模式主要就是给一些不熟悉 Markdown的同学使用,那这里就介绍一下内容模式吧


点击切换编辑模式(左侧右侧工具栏都有提供切换按钮,这里使用左侧的)


image.png


这样就切换到了内容模式,这两种模式之间的数据是同步的,可以自由切换


image.png



内容模式本质就是一个富文本编辑器模式,就类似写word一样就可以了,比如想修改图标,直接点击想修改的图标就会弹出图标选择框,点击想替换的图标后直接就完成了替换,这个感兴趣的自行尝试吧



模板


模板目前有十几个,都随便用,没事的时候就会更新一下


image.png


其他


其实这个工具还有很多功能都没介绍,因为全部写下来的话篇幅会太大,感兴趣可以自己去尝试,都是些比较实用的功能


仓库


GitHub Repo
    Gitee Repo


感兴趣可以点个Star,感谢支持,如果你有不错的Idea,欢迎给仓库贡献代码.


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

我本可以忍受黑暗,如果我未曾见过光明

老文章? 这篇文章大体结构早已在我语雀里写完了很久很久~~~ 因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学...
继续阅读 »

老文章?


这篇文章大体结构早已在我语雀里写完了很久很久~~~


假期就有构思了,现在埋坑


因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学一起上课,而且下一届还是我带的班级,想想那种感觉“咦,武哥你怎么在这上课”而我,内心qs:杀了我把,太羞辱了,脚指头已经扣除一套四合院了)


朋友问我成绩,当时孩子都傻了


所以这段时间我正在经历自我内耗,就向是欠了谁东西,到了deadline,到了审判的日子才能释怀!也至于最近心理一直在想着这个事情,导致最近焦虑的一批,最近几天自己都不正常了,但是终于结束了~~~(非常感谢老师)



言归正传


好了好了,又跑题了,书归正题,你可能会疑惑我为什么用这个标题,难道我经历了什么涩会黑暗,被潜规则,被PUA......(给你个大逼斗子,停止瞎想,继续向下看)



这篇文章灵感来源于我很喜欢的B站一位高中语文老师讲解《琵琶行》,突然我被这个短短 3分51秒的视频搞得愣住了,直接神游五行外,大脑开始快速的回顾自己最近的生活~~~(再次表白真的很爱这摸温柔的语文老师,他的课真的让我感觉到什么叫“腹有诗书气自华”)

视频链接:https://www.bilibili.com/video/BV1bW4y1j7Un/

最爱的语文老师


其实人生当中很残忍的一个事儿是什么呢?就是你一直以为未来有无限可能的时候,就像琵琶女觉得她能够过上那样的生活一直下去。一直被“五陵年少争缠头”,一直被簇拥着的时候,突然有一天你意识到好像这辈子就只能这样,就只能去来江头守空船,守着这一这艘空船,默默的度过慢慢的长夜。
就是如果如果你不曾体验过那样的生活,你会觉得好像“我”最终嫁给了一个商人,然后至少衣食不愁,至少也能活得下去,好像也还算幸福。但是如果我曾经经历过那样的生活,我此刻内心多多少少是有些不甘的。


很喜欢的一幅油画


亦或者是像白居易,如果他是从平民起身,然后一直一步一步做到了江州司马可能觉得也还是不错,但是你要知道他在起点就是在京城为官,所以这里其实是有很明显的,一种落差。那也同样,如果此刻你回到我们说所有的文学都是在读自己,你想想看你自己,此刻你可能没有这种感觉。


30公里鲜啤



哈哈哈,兄弟们不要emo啊,让我们珍惜当下,还是那句话,我们还年轻,谁都不怕。(但是遇到刀枪棍棒还是躲一躲呀,毕竟还是血肉之躯)



其实反思反思人生中最大的挑战,就是接受自己生来平凡。自己没有出色的外表,我也没有过人的才华,我可能也少了些许少年时的锐意。但是这个emo点我并不care,因为我还在拥有选择的阶段,我也在尝试探索不一样的人生,这也许就是喜欢记录生活和写下灵机一动时候想法的意义。但是也就向UP主@peach味的桃子记录自己第44次开学,也是最后一次开学表达自己点点滴滴,也同样是不同的感受;我们同样有应届生的迷茫,但是想想也没什么可怕,还在学习,还在向目标奔跑,也还在享受校园生活~~~


打卡老馆子-群乐饭店


啊呀,好像又唠跑偏了,就是说我对这个视频那么的不一样,尤其是这个主题,因为自己的寒假的实习给我带来了新的视野,哦不,应该是旷野,很有幸能去华为在我们省份的办事处,又被出差派往华为在一个某市分部工作了半个月。这短短的实习经历,让我在大三这个迷茫的时期多了份坚定,在这个期间和大佬们一起工作,真的看到了人家的企业文化和那种行动力,最主要被军团的大佬们很牛掰技术折服,在相处这段时间真的知道了什么是向往的生活,这个学历门槛迈过去,你将会迎来什么样的明天~~~


(谁说我去卖手机去了,我揍他啊[凶狠])


游客打卡照


所以我可能对之前年终总结看法有了些改变,我之前年终总结写到,薪资又不会增加多少,浪费三年那不纯属XX嘛,没错,今天我被打脸了,为我之前的幼稚想法感到可笑;写到这里脑子已经开始疼了,最近甲流,朋友圈注意身体,这个东西真的会影响我们的战斗力,好吧,这也只是一个随想录,留点内容给年中总结,要不到时候就词穷了,哈哈~~


很nice的江景房


近期反思


其实每个人的出发点不一样不能一概而论,就向我自己出发,一个来自十八线农村的孩子,父母通过自己一代人的努力从农村到乡镇,而我就通过自己的求学之路一直到,貌似能够在这个省份的省会立足,这也就是我能做的进步,不管怎么说,我们都是从自身出发,其实谈到这个问题,我自身也很矛盾,小城市就真的不好吗,人的一生除了衣食无忧,在向下追求的不就是快乐,如果真的能和一个爱的人,在做一些自己喜欢做的事情,难道不就是“人生赢家”,城市在这种维度下考虑貌似也不重要~~(如果你想diss这种想法,没有考虑子女的教育问题,其实我想到了,但是我目前的年龄和所处的位置吧,感觉很片面,所以就不对这个点展开讨论了)


过度劳累,小酌一杯


回复问题


有人怕别人看到自己以往的文章写的很幼稚,就不想写了,我有不同的看法,只有看到曾经的对事情的看法和处理方式幼稚了,才能证明自己的成长呀,谁能一下子从孩子成为一个大人!(但是某些时候谁还是不是一个孩子[挑眉])



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

职业发展应该思考什么

没有一种适合所有人的职业规划。因为每个人对职业的诉求不一样。 这里谈谈我思考的维度。 一、 生活。 工作服务于生活,你想过什么样的生活呢?短期你会因为挣钱不挑活儿,但长期你一定要从生活出发考量职业,不要成为工作的奴隶,本末倒置。 提前想明白你对生活的诉求是什...
继续阅读 »

没有一种适合所有人的职业规划。因为每个人对职业的诉求不一样。


这里谈谈我思考的维度。


一、 生活。


工作服务于生活,你想过什么样的生活呢?短期你会因为挣钱不挑活儿,但长期你一定要从生活出发考量职业,不要成为工作的奴隶,本末倒置。



  1. 提前想明白你对生活的诉求是什么,提早布局。

  2. 尽早确定你要在哪个城市定居,你的孩子要在哪个城市上学


二、 你有认真想过自己适合干什么吗?


职业可以划分为三类:



  1. 生产商品:设计师、产品、程序员、QA、按摩师、理发(服务本身也是商品)

  2. 链接关系:销售、中介

  3. 资源分配:管理


大多数人的职业属于前两类,要么直接创造价值,要么通过链接商品、间接创造价值。


很多朋友的职业发展是,找到什么工作就干什么,公司安排干什么就干什么,随波逐流。运气好跟着业务一起晋升,运气不好到了中年举步维艰。


先发散,假设你什么都不会,你想去做什么。然后收敛,选出可行的方向,你不一定非要在程序员这个行业死磕的。


往小了说,技术类有很多细分类别,有没有更适合你的领域。长期从事你并不喜欢的工作,会被职业磨得毫无生气,苟延残喘。


很多人的顾虑是,我不会这个,别人要我吗?这些对年轻人来说,都不是问题。


我见过最多情况是,等着工作的安排,把成长完全寄托于工作。能做到需要什么学什么就很不错了。


少有人能把成长和工作划分开,独立规划自己的职业,设置学习计划并严格执行。


3. 人脉。


职业的每个阶段,使命不一样,实现的路径也不同。


30多岁跳槽,基本靠朋友互推,这个阶段的人脉靠你20多岁来积累,有些人从来不出去交流,不向别人展示你擅长什么,到了三十岁时籍籍无名。


一次分享,一次沙龙,一次愉快的合作,长期维护的博客都是向外展示自己,积累人脉。


重要的岗位上,老板是否用你,排在第一看的是"信任关系",你是老板你难道不优先用嫡系么?卧榻之侧岂容他人鼾睡。


为什么狗是人类最喜欢的宠物--忠诚。


很多人三十多岁进退维谷,都是年轻时候不作为埋下的雷,没有办法,人生就是这样,我们终究要为自己的选择负责。


4. 聚焦。


职场和高考最大的区别在于:


基础教育强调通识性,学校要求你全面发展,偏科的劣势非常明显。


职场是流水线,用的是专才,专业的优势非常明显。


职场人的成长路线是,先集中力量聚焦一点,打穿打透,以此立足,之后再迁移拓展。越早在一个点上打穿,就为你后面的发展争取了更多的时间。


非常忌讳什么都干,什么都不精通,这意味着你所有的工作都可以轻松交接给别人。


建议:不要什么活儿都接,尽量让工作收敛到一个可持续发展的领域。 你不能赌哪个人能混好提前去抱大腿,对自己下注最保险。


不要仅因为晋升或取悦老板,啥事都冲在前线,尤其是做大量沟通协调、低价值的工作。


沟通能力对程序员来说是锦上添花,是突破瓶颈需要的能力,但大部分人还远没到这个层次,混的不好是基础的专业能力不过关,


你敢自信的说,自己精通哪一门技术吗?


输入 & 阅读


《纳瓦尔宝典》一书中,把人分成两类,一类是读书的人,一类是不读书的人。


有些朋友知道要多思考,但是"脑子很空",思维打不开。


原因是输入太少,小学只能解决最初级的加减乘除计算,复杂的空间几何要到中学阶段,更复杂的近现代数学(微积分、线性代数、概率论)要到大学阶段。


知识储备决定思维、格局的边界。


有个原始部落里民风很淳朴没有偷盗事件,原因是他们的语言没有"盗窃"这个词,因此他们就没有进化出这种意识行为。


你的知识储备决定你思维进化的程度。


要在年轻的时候系统的阅读,构建自己各个领域的通识基础,逐步融会贯通。每一门学科都是一门逻辑自洽的完整体系,都能为你打开一扇看世界窗户。横看成岭侧成峰,远近高低各不同。


趁年轻,多读一点有难度的,信息密度高的书,越往后阅读越轻松。


建议,每个月至少读一本书,一年12本。达不到这个量,很难有质的跃迁。


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

一个前端实习生在蔚来的成长小结

一、聊聊工作氛围 & 个人成长 1. 这是我的期许 “所谓前途,不过是以后生活的归途。” 这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行做过金融实习(其实就是纯打杂)。 我很喜欢这样...
继续阅读 »

一、聊聊工作氛围 & 个人成长


1. 这是我的期许


“所谓前途,不过是以后生活的归途。”


这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行做过金融实习(其实就是纯打杂)。


image.png


我很喜欢这样一段话: “我曾以为我的23岁会手提皮包西装革履,但我还是穿着休闲裤,带着十几岁的意气行事,幼稚又成熟;我曾以为我的23岁会性格外向,做事圆滑,但我连最简单的亲情都处理不好;我曾以为我的23岁会和喜欢的人看山河大海落日余晖,但没想道周围的人谈婚论嫁都近在眼前,我还在路上找自己。”


我一直在探索着自己的边界,在能闯能疯的年纪反复横跳,寻找着自己的热爱与期许。在真正从事这个行业之后,我发现了我对于这个岗位的喜爱,当你看着一个个实际的视图出现于自己的手中,你会有一种莫名其妙的成就感,这种感觉难以描述的程度就好像你要向一个完全不看vtuber的人描述你对嘉然的喜爱。


2. 工作氛围:这里是一个乌托邦(适合摸鱼学习的好地方!)


说实话,我最开始预期是每天九点来上班,九点下班的(因为看学长们实习都好辛苦的样子)。


来了之后发现完全不是,每天十点上班,六点下班(我当然是准点跑路)



实习两个月左右的时候接的一个需求,第一天是另一个前端实习生来搞,后来他要跑路,leader就把活给我了。


周四,后端六点把接口给另一个前端实习生。


另一个前端实习生:“明天再说”


周五我来接这个活,我边画页面边让他加字段。


然后提完了,六点他给我改好的接口,让我看看有没问题


我:“下周再说”。


后端:“前端是不是,都很快乐啊[流泪]”



image.png


最开始因为我对 react 不是特别熟悉,leader 让我看着组内文档学了半个月,才开始了第一个需求。


leader 没有给我指定 mentor,所以当我有问题的时候,我看组内谁没开会(或者有时间)就会去问,都能得到很耐心的解答,这点来说还是很有安全感的。


然后每天都会跟着老板和大老板一起去吃饭,有时听他们说说自己的事情,有时听听他们对某个语言的看法,也算有不少收获。


值得一提的是刚入职三天部门就开始团建了,从周五下午五点玩到了第二天凌晨两点,炫了一只烤全羊,然后就开始电玩篮球各种 happy,后面玩狼人杀我次次狼人,大老板也总觉得我是狼人,我次次和他对着刚(乐)



马上就要第二次团建了,可惜参加不了呜呜呜



在团建上 leader 说我是从五个面试感觉都 ok 的人里面选出来的(当时我超惊喜的)


还有几件有趣的事情值得一提



第一件事情是中午和 leader 散步,他说:“你干了两个月这里的情况也看到,很难接触到同龄的小姐姐的,找对象的优先级应该要提高了。”


我:“说的对说的对。”


当时我心里就暗暗想着,这是我不想找吗?这tm是我找不到啊(悲)


第二件事情是我有事开了自己的热点,热点的名字叫:“要失业了咋办呐。


被同事发到了前端大群里。


同事:“这是谁的啊?”


我:“是实习生的(悲)”



3. 个人成长:“不卑不亢,低调务实”


最开始入职当然会担心一些七的八的,诸如这样说会不会不太客气,这样搞会不会让老板不爽,后来和老板还有大老板一起吃饭之后发现他们人都挺随和的,没什么架子,他们更多的关心的是这件事情做的怎么样。


大老板曾经在周会上说:“这个事情可以做的慢一些,这是能力上的问题,这个可以商量,但是如果到了约定的日期没有交付,这就有问题了。 ”这个是说的务实。


然后就是为人处事方面了,自己有时候挺跳脱的,没有什么边界感,在实习和他们一起吃饭的时候我就回默默的听着,有些问题大家都不会问,算是看着看着就成长了。


回校远程的时候我写了这样一段话:



去打工吧,去打上海冬夜准时下班,踩雪走回家的工。


去打工吧,去打一边聊天一边发现,这个产品也是清华✌️的工。


去打工吧,去打测试前一天,人都走光了,mentor陪我赶工到半夜的工。


去打工吧,去打部门团建,大leader带我们玩狼人杀到凌晨两点,超级尽兴的工。


冴羽曾在一次读书会上分享:“开眼界就像开荤一样,只有见过了才会产生饥饿感。”


打工虽然让我变成了稍不注意就会摆烂的成年人,但大平台汇聚了很多丰富有趣的同事,让我看到了截然不同的经历与一波三折的人生。


不知道是不是部门的原因,我这边总是十六五准点上下班。


我现在依然处于打工真香的阶段,不用早起,不用日复一日的和同龄人卷同一件事,身边的人年岁不同,人生阶段也不相同,卷不到一起去。


我还在路上~



image.png


4. 代码方面 learning


说实话看到组内项目的时候体会到了不少的震撼,看着组内的项目之后真的就感觉自己写的东西和玩具一样,每次写完项目,都会兴冲冲的找组内的哥哥姐姐帮忙 CR,然后 CR 出一堆问题,自己在一个一个的修改,把这些规范点记周报上,总之就是学到了很多很多。


timeLine 大概是这样的



  • 前两周熟悉 react 写小 demo

  • 然后以两周一个需求的速度给咱活干~


记得第二次写完一个稍微有点复杂的需求,带着我做这个需求的 mentor 还夸了我一波(骄傲)


5. 对于技术和业务的想法


大leader组织组内 vau 对齐的时候我仔细的听了听,我们的很多东西都需要落地,相比来说技术只是一个实现的手段,并不是做这个的目的。


但怎么说呢,我个人还是对技术本身抱有很大的期许的,希望自己能够变得很厉害,参与到很多的开源项目中,我坚信代码可以改变世界。


二、展望未来



实习不去字节,就像读四大名著不看红楼梦,基督徒不看圣经,学相对论不知道爱因斯坦,看vtuber不看嘉然今天吃什么,这个人的素养与精神追求不足,成了无源之水,无本之木。他的格局就卡在这里了,只能度过一个相对失败的人生!




  • 话是这么说啦,但最后还是没有成功去到字节,但是我是字节不折不扣的舔狗,后面再看吧。

  • 字节给我发面试一定是喜欢我(普信)


下面这段是之前写的



离开的契机也很简单,我在小红书实习的同学跑路了,然后要找继任,顺手把我的简历投过去了,然后我顺手面了一下小红书,小红书顺手给我发了个Offer(bushi,然后就去小红书了。



image.png


小红书确实Offer了,但是老板和我约谈了很久,我决定继续远程实习,在这篇文章发布的当天,我已经实习了 一百四十天,我相信,我的旅途还在继续。


image.png


三、写在最后


不知不觉就实习快半年了啊


我真的非常感谢遇到的leader和同事,感恩遇到的每一位愿意拉我一把的人。


在这段时间里学到了好多一个人学习学不到的东西啊。


那么这就是我在蔚来的实习小结啦!


感谢阅读~


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

日本,我劝你别来

大家好,我小j,很久不见,这几个月摸鱼摸得过分了,一直没找到新想法来写文章,正巧最近有很多群友交流疑问求职问题,这边我就写一篇干货,以我自身经历以及和来日本很久的朋友交流中总结下日本的程序员工作相关的一些话题 在讲这个话题之前,首先要引入很多概念,当然不愿意看...
继续阅读 »

大家好,我小j,很久不见,这几个月摸鱼摸得过分了,一直没找到新想法来写文章,正巧最近有很多群友交流疑问求职问题,这边我就写一篇干货,以我自身经历以及和来日本很久的朋友交流中总结下日本的程序员工作相关的一些话题


在讲这个话题之前,首先要引入很多概念,当然不愿意看也跟我没关系,可以自己选择折叠和跳段


日本的雇佣情况




  • 正社员


    正社员是最常见的日本公司雇佣情况,具体表现是入职合同没有工作期限或者期限在退休年龄之后,一般来说正社员是大部分日本公司的正式雇员,公司需要帮助其按时缴纳厚生年金和医疗保险的费用,同时也会享受公司的全部福利。


    根据日本法律的情况,除非自己离职,或者沟通选择离职,一般公司无法以一般理由开除正社员。同时推特、谷歌、indeed等外企在裁员时日本分部也是裁员最少,赔偿金月数最高的地方。


    极端来说,大部分日本公司或者外企都要脸,在日本法律的要求下,不会像国内一样大量的裁员关闭部门,基本上都是软磨硬泡砸钱沟通让你走,如果你求助工会赖着不走,公司也没有什么很好的处理方法,身边有例子沟通裁员后赖到现在还没走。


    当然,公司宣布倒闭的话,也没有法律和脸面的说法了。


    简单来说,可以类比国内带编制的国企职工




  • 劳务派遣


    劳务派遣是日本第二常见的雇佣情况,具体可类比国内的外包开发,甲方有开发需求时,向派遣公司提出要人要求,派遣公司的雇员通过甲方面试后进入甲方的公司进行开发。


    综合来说派遣是水最深的地方,你的求职体验跟要你的甲方,雇佣你的派遣公司乃至跟你同期进去的同事息息相关,任何一方恶心到你都会极大的影响你的工作体验。能否remote,是否需要穿正装,都根据不同甲方的要求各异。


    但是相对而言,派遣也是自由性很大的职业,一旦自己觉得不爽了就可以跟派遣公司申请换现场,只要能通过面试就可以进入新的现场工作。同时因为自己不是正式员工,也不需要关心公司的制度要求,到点下班,根本不care其他人的情况。由于日本派遣制度的成熟,很多传统大手有大量任务需要交给派遣完成,所以只要日语ok,技术尚可,找到新现场也不是难事,这也是很多人喜欢一直在派遣的原因。




  • 契约雇佣


    这种雇佣形式和国内类似,由于某些原因,公司没有按正社员的形式雇佣你,而是签一年、三年等时间的定期合同,到期需要公司续签。




  • 项目委托


    这种形式常见于freelancer和部分派遣公司,雇佣的目的是为了完成某个项目,而不是缺人辅助开发各种系统,俗称一锤子买卖,交付完成之后就相当于完成合同,之后可能会继续交付项目亦或是直接拜拜




日本的公司类型




  • 日资公司


    日资公司就是常见的真日本人担任法人的公司,俗称日企,其中日企也可以分为两类




    • 传统日企


      传统日企整体公司以日本人为主,主要沟通语言为日语,或多或少会带有一些日企味道,根据公司要求,可能会要求穿正装,年功序列,熬年限等等情况。


      例子有:大部分日本本土公司,如银行,制造业




    • 现代日企


      有许多日本本土互联网公司吸收了大量外国雇员,如乐天、line、Woven Planet、paypay等公司,这些公司大部分沟通语言为英语,整体比较年轻化,管理比较自由,整体和国内公司的情况类似。






  • 外资公司


    外资企业就是我们常说的外企,比如amazon、google、apple、微软等公司,整体风格就跟其他国家的外企类似,主要语言为英语,面试要求较高,相对应工资也会比较高。




  • 华人公司


    华人公司就是中国人在日本开办的互联网业务公司,整体风格千差万别,无法具体定论




  • 派遣公司


    正如上面提到的劳务派遣情况,当甲方缺干活的人,又不想招聘自己的员工时(招聘是一个非常费钱的过程),就向派遣公司要人,派遣公司提供自己的员工去面试,面试通过则进入甲方工作。


    派遣公司有几个比较大的问题:



    1. 跟华人公司一样,派遣公司好不好,良不良心完全看老板的风格,除了base这种大型上市派遣公司以外,大部分派遣公司都比较夫妻店一言堂,所以完全看老板是不是一个有良心的人。

    2. 派遣并不包找到现场,所以一旦你因为语言或者技术的问题而久久没有公司接收你的时候,你只能根据合同可能能收到一些待机费,待机太久可能会有辞退的风险。

    3. 派遣也会按正社员、非正社员招人,一般是正社员的工资会比非正社员低一些,但是公司会帮正社员提交社保,非正社员需要自己提交社保,年底统计会更麻烦一些。

    4. 由于2022年小红书、知乎、v2ex中介的发力,引来了大批量无日语,有(无)技术的国人来到日本,导致目前派遣现场非常卷,基本都要求有一些日语能力,能看懂式样书,稍微做一些技术。请不要轻信中介的说法,无技术无语言,在日本是很难找到工作的。小红书上有大量派遣员工找不到现场待业的文章。

    5. 请拒绝一切派遣公司要求原件的要求,办理在留签证不需要任何原件,打印件即可,一旦交付原件以后可能会被提不合理要求




个人建议


总的来说,要先找交厚生年金的正社员工作,然后是每个月都有全额工资拿的契约社员,最后才考虑派遣公司。


华人公司因为聊得太少,暂时没有添加进排序里


高薪外企/大手日企 > 低薪外企/日企小公司 > 派遣正社员 > 派遣契约社员


个人是非常不建议入职派遣的,因为鱼龙混杂,如果你日语不好初来乍到很容易被骗被坑,但是你非说我就为了留在日本拿签证或者我能力暂时不足以进其他情况的公司那我也没法拦你


记住,一旦遇到工作上的违法事情或者パワハラ要记得找hellowork来谋求自己的合法权益


面试要求


一定要掌握一门外语


一般来说日本传统日企要求不高,日本公司不像国内公司一样八股乱飞,妖魔横行,更多是要求日语好,通过交流来确认你确实有解决问题的能力,之后就是看重behavior question,考察你的合作能力,


外企和国际化的日企会接轨国外的考察方式,一般一面是hr交流看你的意愿,之后进行一轮oa,之后技术面试会考察算法、系统设计、bq和交流能力


华人和派遣无法一概而论


如何准备面试


面试之前请确定你的目标公司,因为传统日企和其他公司有着决定性的差别。


每个人的情况不同,以下只给出一些对应的网站


语言:日语/英语做到交流无碍,可以没证书,但是起码交流大家都听的懂(乐天要求托业800分以上证书)


算法:leetcode


bq:彻底击碎行为问题


system design: 系统设计面试题精选


etc.


怎么找到公司


除了google、indeed、amazon、apple、微软等大外企直接在官网找招聘入口之外,大部分公司都会在以下几种途径进行招聘



  1. 内推

  2. linkedln

  3. tokyo-dev、japan-devgreen-japanindeeddodaopenworkforkwellfindylapras等求职网站

  4. 猎头


当准备好一份英文简历后,挂在linkedln上,关注一些目标公司,经常会有猎头或hr邀请。


怎么区分派遣公司


说句实话,目前小红书、知乎、v2ex上招人的大多都是派遣公司,大部分自社业务的公司较少会直接在这些平台发帖招人,如果你拿不准这个公司是否是派遣公司,可以依照以下几个方法区分:



  1. 查询该公司的linkedln首页,一般linkdln有信息,有员工的的较少为派遣公司

  2. 查询该公司主页,如果没有明确说出自己产品或者包含提供技术人才、提供解决方案等关键词,大概率为派遣公司

  3. 如果该公司没有母公司,且在谷歌上信息很少,大概率为派遣公司


选择公司需慎重


Q&A


日本卷不卷


事实上,日本之前确实很卷,毕竟是社畜这个词的产生地,但是安倍上台后2015年发生了电通事件后立法限制了加班时间,我正规公司的正社员程序员极少存在加班现象,周末不oncall,假期不上班是常态。


日本工资低不低


说实话,低,大部分日企开发天花板远低于中国,胜在一个稳定。日本的程序员工作属于一个普通工种,很多本国人不愿意开发,所以提供了大量岗位吸收国外的开发。


薪资参考:


Untitled (5).png
具体查询薪资网站:opensalary.jp/en/roles/so…


不会日语能否在日本生活


首先给出结论:可以。


要相信人的理解能力,除了银行卡需要日语(入职后可以找同事陪同)之外,其他的不是有中文服务就是可以网上申请。


在日本生活会不会比国内好


不一定,因为工资低,物价不便宜,所以在日本能否享受你在中国的生活水平因人而异。


日本买房多少钱


suumo


有身份可以做到8-10倍年收,0首付,0.5-0.3%利率,有房产税,一年大概1.2%,之后折旧会降低


日本是否值得来


长远角度,日本经济处于要完未完的状态,长期停滞,物价上涨,工资比不上物价的涨幅,而且岛国环境,依附于美国势力,长期发展肯定是远不如国内的,但是生活不止宏大叙事,过好自己的生活,享受安逸才最关键,所以值不值得来,就看你看中什么了


日本生活相关


跟本篇内容无关,之后再说(咕了)


是否适合出国


请你先想好出国的风险,以及作通家里的思想工作之后,再考虑出国的决定,希望你想出国只是想看看异国的文化和工作体验,而不是当个键政小鬼只会润润润。


Untitled (1).jpeg


以上


对年轻人应届生的小建议


目前国内确实经济处于不良好的阶段,大厂裁员事件频发,hc逐年降低,应届生比往年更难以拥有满意体面的offer,以我的经验对应届生可以写几条建议



  1. 除了希望大学期间打好基本功,多参与项目之外,也建议可以读研等方法跳过经济周期,等待几年后的复苏。

  2. 在求职道路中,除了私企国企公务员等项目,确实也可以多考虑外企乃至出国工作的机会,不要停止英语学习,这样路子会更多更广。

  3. 减少键政,少看知乎等键政乐子人聚居地,天天看帖子只会形成信息茧房,充满你满意的观点的帖子,但是在你阅读帖子的时候,只有发泄情绪浪费时间,年轻人压力大找地方发泄无可厚非,但是只会谩骂指责并不能对你有任何帮助,除非你渴望机械降神,否则还是希望多用时间去充实自己,提高自己。少键政,少参与骂战,少乐少甜,都是浪费时间。

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

《我的程序人生》

《我的程序人生》 在这个充满挑战和机遇的数字时代,我回首过去,分享我的程序生涯。这是一个展示我职业计划、职场成长、掌握的新技能、薪资增幅、阅读的书籍、硬件装备、半路出道、良师益友以及职场经历的机会。 职业计划是我程序人生的指南针。早年,我对计算机科学的热情引领...
继续阅读 »

《我的程序人生》


在这个充满挑战和机遇的数字时代,我回首过去,分享我的程序生涯。这是一个展示我职业计划、职场成长、掌握的新技能、薪资增幅、阅读的书籍、硬件装备、半路出道、良师益友以及职场经历的机会。


职业计划是我程序人生的指南针。早年,我对计算机科学的热情引领我进入这个领域。我设定了远大的目标,希望成为一位卓越的软件工程师。通过不断学习和实践,我逐渐掌握了多种编程语言和技术,包括Python、Java、JavaScript等。这些技能为我在职场上的发展奠定了坚实的基础。


职场成长是我程序人生中最宝贵的财富之一。我从最初的实习生开始,逐步晋升为项目经理。在每个岗位上,我都努力追求卓越,并学会了如何与团队成员合作,如何管理时间和资源。这些经验让我不断成长,成为一个全面发展的专业人士。


掌握新技能是我程序人生中不可或缺的一部分。我不断追求学习和进步,通过参加培训课程、在线教育平台和技术社区来拓宽自己的知识面。我学习了人工智能、数据科学、云计算等前沿技术,并将其应用于实际项目中。这些新技能不仅提高了我的竞争力,还让我能够为公司带来更多的价值。


薪资增幅是我程序人生中的一项重要衡量指标。随着经验的积累和技能的提升,我的薪资水平也得到了显著提高。然而,对我而言,薪资并不仅仅是一个数字,更是对我努力工作和不断进步的认可。它激励着我继续追求卓越,并为自己设定更高的目标。


阅读的书籍是我程序人生中的灵感之源。我相信知识改变命运,因此我不断阅读各种技术书籍和领导力书籍。这些书籍开阔了我的视野,激发了我的创造力,并帮助我更好地理解技术和人际关系的复杂性。每一本书都为我带来了新的启示,让我在程序人生的道路上不断前行。


硬件装备是我程序人生中的得力助手。一台高性能的电脑、一款舒适的键盘和一台高清显示器都让我的工作更加高效和愉悦。这些工具不仅是我工作的必备品,也是我对技术的热爱和追求的象征。


半路出道是我程序人生中的一段特殊经历。曾经,我在一个完全不同的行业工作,但内心深处的激情让我决定转行进入程序领域。这段经历让我更加珍惜我现在的职业,并以一种全新的视角看待问题和挑战。


在程序人生的道路上,我结识了许多良师益友。他们是那些与我分享知识、经验和智慧的人。通过与他们的交流和合作,我不仅学到了更多的技术和职场技巧,也收获了珍贵的友谊和支持。他们的存在让我感到坚定和勇敢,让我相信自己可以克服任何困难。


职场经历中也有奇葩不愉快的时刻。这些经历教会了我如何处理冲突、管理压力和保持专业。它们让我更加坚韧,更加懂得珍惜每一个机会。


回顾我的程序人生,我感到由衷的自豪和满足。我从一个对计算机科学充满热情的年轻人成长为一名有实力和经验的程序员。我相信,未来的道路上还有更多的挑战和机遇等待着我。我会继续努力学习,不断进步,为自己的程序人生书写更加辉煌的篇章!

收起阅读 »

为什么强烈不建议使用继承

这两天有空的时候看了下 继承和复合如何选择这个知识点,其实之前开发的时候遇到类似的问题我是无脑继承的,也没有考虑这么多,因为这些新增的父子类,都是在包内使用,而且父子类基本都是我们同一个开发人员,所以一般不会有什么意外情况。 但是如果我们要开发新的类,这个类需...
继续阅读 »

这两天有空的时候看了下 继承和复合如何选择这个知识点,其实之前开发的时候遇到类似的问题我是无脑继承的,也没有考虑这么多,因为这些新增的父子类,都是在包内使用,而且父子类基本都是我们同一个开发人员,所以一般不会有什么意外情况。


但是如果我们要开发新的类,这个类需要对外开放,有很多模块会来继承我们的类(或者我们会继承第三方提供的公共类),这个时候就需要很小心的设计了,如果小伙伴们觉得以后不会接到这样的需求,其实就不用继续看的,下面内容还是有点枯燥无聊的🤣。


学习的内容




1. 继承(Inheritance)是什么


继承其实不用过多的去解释,因为大家都是非常熟悉的,它和封装(encapsulation)抽象(abstraction)多态(polymorphism) 组成面向对象编程的(Object-Oriented Programming)主要特征。


image.png



  • 代码示例
//父类
public class Animal {
   //名称
   protected String name;
   //种类
   String species;

   protected String getName() {
       return name;
  }
   protected void setName(String name) {
       this.name = name;
  }
   protected String getSpecies() {
       return species;
  }
   protected void setSpecies(String species) {
       this.species = species;
  }
}


//子类
public class Birds extends Animal {
   //翅膀长度
   protected String wingSize;

   protected String getWingSize() {
       return wingSize;
  }
   protected void setWingSize(String wingSize) {
       this.wingSize = wingSize;
  }
}

总结:


继承的优点:



  1. 子类可以复用父类的代码,继承父类的特性,可以减少重复的代码量

  2. 父子类之前结构层次更加清晰


继承的缺点:



  1. 父子类之间属于强耦合性,一旦父类改动(比如增加参数),很可能会影响到子类,这就导致代码变得脆弱

  2. 如果子类新增一个方法,但是后续父类升级之后,和子类的方法签名相同返回类型不同,这会导致子类编译失败

  3. 会破坏封装性

  4. 不能进行访问控制


缺点第三条的解释:


下面是新建了一个集成HashSet的类,主要目的是想统计这个实例一共添加过多少次元素



  • addAll:批量增加数据

  • add:单个数据增加


最终统计出来的结果是 4 ,只是因为 super.addAll(c) 最终会调用add方法,也就导致重复计数了。


出现这种情况是因为我们在编写子类逻辑时不清楚父类方法的实现细节,从而造成了错误,即使我们把add中的addCount++ 删除,也同样不能保证父类的逻辑会不会变动,这样就会导致子类非常脆弱且不可控,简单总结就是子类依赖了父类的实现细节,所以这就是为什么会说破坏了封装性。



封装性:将数据和行为结合,形成一个整体,使用者不用了解内部细节,只能通过对外提供的接口进行访问

@Slf4j
public class DestroyInheritance<E> extends HashSet<E> {
   private int addCount = 0;


   @Override
   public boolean add(E o) {
       addCount++;
       return super.add(o);
  }

   @Override
   public boolean addAll(Collection<? extends E> c) {
       addCount += c.size();
       return super.addAll(c);
  }

   public int getAddCount() {
       return addCount;
  }

   public static void main(String[] args) {
      //测试类,使用addAll批量增加
       DestroyInheritance<String> item = new DestroyInheritance<>();

       String[] arr = new String[]{"s","a"};
       item.addAll(Arrays.asList(arr));
       log.info("count:{}",item.getAddCount());
  }
}

2.复合是什么


复合从字面意思上也是可以理解的,就是将多个实例的行为和特征组合成一个新的类,简单理解就是新增的这个类,它拥有其他一个或者多个类的特征,比如家用汽车有轮子、底盘、发动机等等组件组成,那车子这个类就包含了轮子类和底盘类这些属性。


image.png


看一下下面这段代码,Car是一个使用了复合的类,他包含了引擎类Engine和轮胎类Tyre,那为什么要这样写呢,我的想法是有下面几点:



  1. 在doSomething方法中,我无需全部继承引擎类或者轮胎类,只需要根据实际情况调用某些方法即可,减少了之前对父类的严重依赖,造成的耦合性影响

  2. 引擎类只需要提供个别公共的方法给Car类使用,不需要完全暴露其内部细节,也不用担心会出现类似addAll最终调用add的问题



  • 代码示例
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
* 汽车类
**/
@Slf4j
public class Car {
   //引擎类实例
   private final Engine engine;
//轮胎类实例
   private final Tyre tyre;

   public Car(Engine engine,Tyre tyre) {
       this.engine = engine;
       this.tyre = tyre;
  }

   public void doSomething(){
       //自定义逻辑
       engine.setBrand("坏牌子轮胎");
  }

   public String getEngineBrand(){
       //返回轮胎名称
      return engine.getBrand();
  }

   public static void main(String[] args) {
       Engine engine = new Engine();
       engine.setBrand("好牌子引擎");
       engine.setPower("250");

       Tyre tyre = new Tyre();
       tyre.setBrand("好牌子轮胎");
       tyre.setSize("50cm");

       Car car = new Car(engine, tyre);
       car.doSomething();
       log.info("轮胎名称:{}",car.getEngineBrand());
  }
}

/**
* 引擎类
*/
@Data
class Engine{
   private String brand;

   private String power;
}
/**
* 轮胎类
*/
@Data
class Tyre{
   private String brand;

   private String size;
}


3.继承和复合如何选择


image.png


说了半天,那究竟是用复合还是用继承呢,我觉得最重要的一点我觉得是要搞清楚类之间的关系,对于继承而言,它是 "is-a" 的关系,是对事情的一种比如:人是动物、华为mate60是手机,只是对于动物、手机这种是更为抽象的事物,人和华为是对其类的衍生。


复合则是 "has-a" 的关系,比如:健康的人有两只眼睛、家用汽车有四个轮子,对于这种情况而言,我们就需要用到复合。


继承和复合并非是绝对的好与坏,而是我们要结合实际情况,如果是is-a关系,只有当子类和父类非常明确存在这种关系时,我们可以使用继承,并且在代码设计时,一定要考虑日后可能出现的继承问题及后续代码的升级迭代,不然很可能出现令人崩溃的后续问题;而如果某一个对象只是新增类中的一个属性时,我们就要使用复合来解决问题。




总结


写这篇文章也搜索了一些其他博主的文章,总体的感受就是国内主流博客上相关的文章并不多,大家好像并不关心这个点😑,搜到有几篇还都是直接抄书放上去(而且长得都一模一样),很是郁闷,最后还是去跳出去看了几篇别人的文章,感觉还是有很多点还是得仔细琢磨琢磨,后面再继续学习学习。


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

挂起函数的返回值

返回值类型 协程中挂起函数的返回值类型是 Object,无论代码中写的是什么。 我们写的协程代码编译成 Java 代码后,挂起函数的返回值类型就会被修改成 Object,如下:// 定义一个挂起函数,其返回值类型是 Int private suspend ...
继续阅读 »

返回值类型



协程中挂起函数的返回值类型是 Object,无论代码中写的是什么。



我们写的协程代码编译成 Java 代码后,挂起函数的返回值类型就会被修改成 Object,如下:

// 定义一个挂起函数,其返回值类型是 Int
private suspend fun test2(): Int {...}

// javap -v 反编译对应的 class 文件
.method private final test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

通过反编译可以看到,挂起函数额外多了个 Continuation 类型的参数,返回值类型也变成了 Object。关于前者它是协程能恢复的关键,是协程底层原理的基础知识,此处忽略。对于后者是本文重点。


返回值类型被修改的原因



调用到挂起函数时会返回特殊对象 COROUTINE_SUSPENDED,最终也会返回自己定义的返回值。



一个挂起函数会被调用多次,当它执行到另一个挂起函数时会返回 COROUTINE_SUSPENDED 给调用者。执行到函数最后时,它会返回该返回的值给调用者。因此,挂起函数会返回两种类型的数据,所以返回结果型只能是 Object 类型。


验证


为验证上面结论,以下面代码为例说明

private suspend fun test2(): Int {
// withContext 是挂起函数
val a = withContext(Dispatchers.IO) {
delay(100)
1
}
return 1 + a
}

首先通过 as 自带的 show kotlin bytecode 查看上述代码对应的 java 代码,如下


Xnip2023-06-28_15-44-46.png


关于 if 判断是否成立,可以直接反编译生成的 apk,向 apk 中插入代码,可以发现它和 var5 是同一个对象,所以 if 判断成立,因此此时 test() 返回的是 COROUTINE_SUSPENDED。


现在确定下上图中的 $continuation 到底是什么类型,反编译 apk 查看 smali 代码,可以看到 $continuation 其实是 MainActivity$test2$1 类型。

// test2 定义在 MainActivity 类中,所以生成的内部类都是 MainActivity$ 开头

new-instance v0, Lcom/example/demo/MainActivity$test2$1;
invoke-direct {v0, p0, p1}, Lcom/example/demo/MainActivity$test2$1;-><init>(Lcom/example/demo/
MainActivity;Lkotlin/coroutines/Continuation;)V
:goto_0
move-object p1, v0
.local p1, "$continuation":Lkotlin/coroutines/Continuation;

MainActivity$test2$1 继承 ContinuationImpl,最核心代码是它的 invokeSuspend(),对应的 smali 代码如下,看懂它的代码有助于我们理解 test2() 第二次执行逻辑:

.method public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
.locals 2
// 将 p1 赋值给 p0 的 result 中
// p0 是当前对象。invokeSuspend() 非 static 函数,默认有一个参数 this,即 p0
// 这句代码就是:将参数赋值给当前对象的 result 字段
iput-object p1, p0, Lcom/example/demo/MainActivity$test2$1;->result:Ljava/lang/Object;

// v0 = p0.label。即将当前对象的 label 赋值给 v0
iget v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I

// v1 = Int.MIN_VALUE
const/high16 v1, -0x80000000

// v0 与 v1 或运算,并将结果存储至 v0
or-int/2addr v0, v1
// 将 v0 赋值给 this.label
iput v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I

// this$0 是 jvm 中内部类添加的一个字段,用于表示外问类的引用,此处即 MainActivity 对象
// 这句话就是将 MainActivity 赋值给 v0
iget-object v0, p0, Lcom/example/demo/MainActivity$test2$1;->this$0:Lcom/example/demo/MainActivity;

// 用 v1 指向当前对象,即 v1 = this
move-object v1, p0
// 判断 v1 是不是 instanceof Continuation,肯定成立
check-cast v1, Lkotlin/coroutines/Continuation;

// 调用 MainActivity 的静态方法 access$test2,同时传入参数 MainActivity 实例
// 以及当前类对象
invoke-static {v0, v1}, Lcom/example/demo/MainActivity;->access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

// 将上面 access$test2() 执行结果赋值给 v0
move-result-object v0

// 返回 v0,也就是返回 access$test2() 的执行结果
return-object v0
.end method

在这段代码的最开始会将参数赋值给对象的 result 属性,结合验证一节中的截图 $result 字段,看一下它的赋值,就可以明白为啥 $result 取到的是挂起函数的返回值了。


上面代码提到了 MainActivity 的静态方法 access$test2 方法,看一眼,代码更简单:

.method public static final synthetic access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
.locals 1
.param p0, "$this" # Lcom/example/demo/MainActivity;
.param p1, "$completion" # Lkotlin/coroutines/Continuation;

.line 16
// 直接执行 MainActivity 的 test2() 方法
invoke-direct {p0, p1}, Lcom/example/demo/MainActivity;->test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
// 同时将 test2() 的返回值直接返回
move-result-object v0

return-object v0
.end method

目前可知 test2() 由 invokeSuspend() 调用的,那该方法是由谁调用的呢?根据协程的基础知识可知,协程的恢复都是由它的 resumeWith() 开始的,该方法定义在 BaseContinuationImpl 中,如下:


Xnip2023-06-28_18-52-11.png


上图中会调用 invokeSuspend(),也就是调用本节分析的 invokeSuspend() 方法,最终会执行到 test2() 方法,拿到 test2() 的最终返回值。结合 while 死循环,最终会执行到 test3() 后面的步骤。


以上就是协程的挂起恢复流程,也说明了挂起函数的返回值为啥是 Object。


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

Android面试经:Broadcast需要注意哪些细节

前言 在android中,广播也是我们经常使用的组件,但是因为大部分使用场景简单,所以关注不多,今天就让我们来探索一下Broadcast。 注册 这个是常识了,两种注册方式:静态注册(menifast)和动态注册,不展开说了。 这里注意动态注册后,我们一般会手...
继续阅读 »

前言


在android中,广播也是我们经常使用的组件,但是因为大部分使用场景简单,所以关注不多,今天就让我们来探索一下Broadcast。


注册


这个是常识了,两种注册方式:静态注册(menifast)和动态注册,不展开说了。


这里注意动态注册后,我们一般会手动进行注销,不过如果没有手动注销,当context对象被销毁时,Broadcast会自动注销,但是我们还是及时注销释放资源。


线程及ANR


默认Broadcast都是运行在主线程,而且android对它的运行(onReceive)有一个时间限制——10秒,即ANR时间,所以不要在onReceive执行耗时操作。


但是Broadcast其实可以运行在其他线程(这时候就没有时间限制了),但是必须是动态注册才可以,Context的registerReceiver其实是一系列函数,其中就有

public abstract Intent registerReceiver(BroadcastReceiver receiver,
IntentFilter filter, @Nullable String broadcastPermission,
@Nullable Handler scheduler)

这里可以传入一个Handler,这个Handler就可以是其他线程创建的,这样就可以在其他线程运行Broadcast。


官方说明如下:



This method is always called within the main thread of its process, unless you


explicitly asked for it to be scheduled on a different thread using
{@link android.content.Context#registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)}. When it runs on the main
thread you should
never perform long-running operations in it (there is a timeout of
10 seconds that the system allows before considering the receiver to
be blocked and a candidate to be killed). You cannot launch a popup dialog
in your implementation of onReceive().



那么既然onReceive中不能执行耗时操作,我们是否可以在onReceive中开启一个新的线程来处理?


在onReceive中开启新的线程,因为与其生命周期有关,所以下面与生命周期一起来说。


生命周期


Broadcast生命周期很简单,只有onReceive,当它在执行onReceive时是活跃状态,当执行完成则处于失活状态。根据网上资料:


拥有一个活跃状态的广播接收器的进程被保护起来而不会被杀死,但仅拥有失活状态组件的进程则会在其它进程需要它所占有的内存的时候随时被杀掉。 


而根据Broadcast的官方文档,当onReceive执行完这个Broadcast对象不再是alive状态,所以可以随时被回收销毁。所以不能在onReceive中进行异步操作,即开启新的线程,因为当onReceive执行完处于失活状态,它和这个新的线程可能随时被销毁,导致不可预计的程序问题。如果想在onReceive中执行一些异步操作,那么可以使用JobService,或者service。官方文档如下:



If this BroadcastReceiver was launched through a <receiver> tag,
then the object is no longer alive after returning from this
function.
This means you should not perform any operations that
return a result to you asynchronously. If you need to perform any follow up
background work, schedule a {@link android.app.job.JobService} with
{@link android.app.job.JobScheduler}.


If you wish to interact with a service that is already running and previously
bound using {@link android.content.Context#bindService(Intent, ServiceConnection, int) bindService()},
you can use {@link #peekService}.



所以说当Broadcast执行完onReceive后就可以随时被销毁了,当然动态注册不一样,因为它是手动创建的,所以还需要关心它的引用可达性。


同时,Broadcast的创建也一样,动态注册的时候我们手动创建,所以是一个对象。


而静态注册的时候,应该与activity等组件类似,(binder机制中)先通过intent条件查找创建Broadcast对象,经过测试每次都是重新创建。比如我们在menifast中静态注册一个Broadcast,然后通过一个按钮发送这个广播,在Broadcast的onReceive中打印自己的对象的toString,发现每次点击都是一个新的对象来执行。所以给Broadcast设置类变量,防止重复接收不会起作用,因为每次都是一个新的对象。


如果在onReceive中执行耗时操作,如下:

public class MyBroadcast extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.e("ssss", this.toString() + ":start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("ssss", this.toString() + ":end");
}
}

再反复点击按钮发送广播,就会发现这些广播会按顺序执行,当上一个执行完才开始执行下一个(因为是在一个线程中)。


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

四个有用的Android开发技巧,又来了

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。 一. 通过堆栈快速定位系统版本 这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本: 1. 快速区分当前系统版本是Android10以下,还是Androi...
继续阅读 »

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。


一. 通过堆栈快速定位系统版本


这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本:


1. 快速区分当前系统版本是Android10以下,还是Android10及以上;


首先Android10及以上引入了一个新的服务Service:ActivityTaskManagerService,将原本ActivityMangerService原本负责的一些职能拆分给了前者,所以当你的问题堆栈中出现了ActivityTaskManagerService相关的字眼,那肯定是Android10及以上了



大家在Android9及以下的源码中是找不到这个类的。


2. 快速区分当前系统版本是Android12以下,还是Android12及以上;


这个就得借助Looper了,给大家看下Android12上Looper的源码:



Looper分发消息的核心方法loop(),现在会转发给loopOnce()进行处理,这个可是Android12及以上特有的,而Looper又是Android处理消息必要的一环,是咱们问题堆栈的源头祖宗,类似于下面的:



所以这个技巧相信还是非常有必要的:当你从问题堆栈中一看有loopOnce() 这个方法,那必定是Android12无疑了。


二. 实现按钮间距的一种奇特方式


最近看了一个新的项目代码,发现该项目实现按钮之间、按钮与顶部底部之间间距实现了,用了一种我之前没了解过的方式,于是这里分享给大家瞧瞧。


这里就以TextView和屏幕顶部间设置间距为例,初始的效果如下:



接下来我们来进行一步步改造:


1. 首先TextView是有一个自定义的xml背景:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:height="70dp"
android:gravity="center_vertical">
<shape>
<solid android:color="#ff0000" />
</shape>
</item>
</layer-list>

核心就是定义了android:heightandroid:gravity这两个属性,来确保我们自定义背景在组件中的高度及居中位置。


2. 其次将布局中TextView的属性调整下:




  1. 首先height属性一定要调整为wrap_content保证最后TextView按钮的高度的测量最终取minHeight设置的属性值和背景设置的高度这两者的最大值




  1. 其次还要设置minHeight最小高度属性,注意一定要比背景设置的高度值大,保证能和屏幕顶部产生边距效果;




  1. 最后要设置字体的位置为垂直居中,保证字体位置和背景不发生错位


经过上面处理,效果就出来了:



其实上下空白的部分都是属于TextView,设置点击事件也会被响应,这算是其中的缺点之一,当前也可能在业务场景中认为这是一种合理表现。


上面实现的逻辑和TextView的测量逻辑密不可分,感兴趣的同学可以看下这块代码,这里就不带大家进行一一分析了:




三. logcat快速查看当前跳转的Activity类信息


忘了是在哪里看到的了,只要日志过滤start u0,就可以看到每次跳转的Activity信息,非常的有帮助,既不需要改动业务层,也不需要麻烦的安装一些插件啥的。


使用时记得将logcat右边的过滤条件置为,否则你就只能在左边切换到系统进程去看了:


这里我们演示下效果:


1. 跳转到Google浏览器



logcat界面会输出:



会打印一些跳转到包名类名等相关信息。


2. 跳转到系统设置界面



logcat输出:



可以说start u0还是相当好用的。


四. 项目gradle配置最好指向同一本地路径


最近开发中经常存在需要一次性检索多个项目的场景,而这样项目的gradle版本都是相同的,没啥区别。但每打开一个项目就得重新走一遍gradle下载流程,下载速度又是蜗牛一样的慢。


所以强烈建议大家,本地提前准备好几个gradle版本,然后通过设置将项目的gradle指向本地已存在好的gradle:



这样项目第一次打开的速度将是非常快的,而且按道理来说相同gradle版本的项目指向同一本地路径,也可以实现缓存共享。猜的


如果项目好好的编译运行着,突然没网了,可能会提示一些找不到依赖库资源啥的,其实你本地都已经缓存好依赖库资源了,只需要设置下off-mode,不走网络直接通过本地资源编译运行即可



总结


本篇文章主要是介绍了Android开发一些技巧,感觉都是项目中挺常用到的,算是我最近一个月收获的吧,后续准备研究研究compose了,毕竟看到大家们都在搞这个,羡慕的口水都流了一地了哈哈。


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

个人职业复盘-我的路

概述 从2009年毕业开始工作以来,至今已是第14个年头了,在这段漫长的职业生涯里,我干过好多件蠢事,也做对了一些事。 目前在一家公司,担任后端开发负责人,管着一个15人的技术团队。对我而言,这个是我当前的职业生涯里最亮眼的职位了。虽然团队不大,但是要对整个...
继续阅读 »

概述


从2009年毕业开始工作以来,至今已是第14个年头了,在这段漫长的职业生涯里,我干过好多件蠢事,也做对了一些事。


目前在一家公司,担任后端开发负责人,管着一个15人的技术团队。对我而言,这个是我当前的职业生涯里最亮眼的职位了。虽然团队不大,但是要对整个IT部门的系统稳定性负责,要承担的责任还是非常大的。


只要在IT公司做过几年程序员的都应该知道,要想走技术管理的路,并不是那么容易的,这里有运气的成分,也需要自己实力的加持。这篇文章就是想把自己如何走上技术管理之路的经历分享出来,供大家参考,为广大程序员尽一点绵薄之力。


文章的大纲分为如下几个部分:



  • 毫无成长的前五年;

  • 职业生涯转折点:进入了唯品会;

  • 第一次当技术组长;

  • 第一次当技术经理;

  • 第一次当后端技术负责人。


期间到底如何打怪升级的,请看下面的章节的详细内容。


毫无成长的前五年


前五年可以总结成一句话:



像一只无头苍蝇一样,到处乱撞,但一事无成。



这五年,待过了4家【传统行业的公司】,基本上一年一跳,其中两家还是外包公司。当时跳槽的原因只有一个,哪里钱多就往哪里去,就算只是涨幅2、3千块钱,我也去。完全没有任何的职业计划,现在回想起来了,已能知道为啥当时自己会那么做,原因就是:



没有良师益友的指导。



就是说没人帮你,没人带你,告知你应该如何珍惜前五年,如何做职业规划。人很容易困在自己的圈子里,认知很难突破的,如果没有高手帮忙拉你一下的话,你可能10年都是那个样子,一直碌碌无为,一直没有方向。


但毕竟工作五年了,为啥说毫无成长呢? 这里有几个原因。



  • 在外包公司(去过两家外包公司,待了两年多一些)的经历,基本可以忽略。因为做的东西都是企业级管理的后端系统,且也很难接触到真正的核心系统,也没有针对C端用户的大流量系统(就算有,也轮不到你来做),真的就是按照产品的需求做出来就行。团队不会严格要求你必须得有技术方案、技术设计,因为这种项目对技术的要求本身就很低。因此需求一来,火急火燎的,马上开工就行;

  • 虽然也待过两家非外包的公司,但是公司没什么名气,对技术人的要求也不高,还是需求一来,马上开工的那种状态。如何做设计,如何提升代码质量,代码如何写才是好代码,没人告诉你。


没做过好项目,周围也没遇到过高手,加上自己也没啥天赋,比较笨,就一直浑浑噩噩度过了五年。我是属于需要有人指导一下,才能开窍的那种。


经验教训:



在传统公司里做程序员,不太容易遇到好公司好项目好团队和好的人,你可能还自我感觉良好,觉得自己做的不错,但是在互联网的HR和面试官看来,你可能一文不值。这个时候的你,应该赶紧去找一下好公司,突破上去,上到一个新台阶。



职业生涯转折点:进入了唯品会


14年结婚后不久,老婆语重深长的说:



准备屡战屡败,屡败屡战,去互联网公司,把履历洗白一下,不然你以后的路会越来越难走的。



我当时是挺不屑的,她懂啥呀。但是听了她的分析后,我被说服了。



  • 工作五年了,履历毫无亮点。没在有名气的公司待过,也没做过有大流量的项目,还是一个普普通通的程序员;

  • 再过几年,就七八年工作经验了,但是你的技术跟年龄,完全不匹配呀。


可以说是当头一棒了,确实不能在这样下去。于是赶紧更新了简历,开始投递广州的一些互联网公司,开始走上了互联网公司求职之路。如上文的描述,我一直以为我的技术还是相当不错的,像Spring、Hibernate、Struts用的很熟练,但是去面了几次后,我才知道跟别人差距有多大。面试官一上来都是问JVM、高并发、高可用等一些技术点,我全都不会。


当时我是屡战屡败,屡败屡战。记得还连续去了广州一家知名互联网公司面试过4次,都没通过,每次都被打击的死死的。虽说每次都失败,但是也注意到了互联网公司到底都用一些什么技术,后面认真的准备了半年,啃了N本书,看了N个技术博客,写了N个技术Demo。终于在2015年成功进入了广州唯品会。虽然是降工资,降了两个等级过去的(我当时资深开发,但是去了唯品会,就变成普通开发,连高级开发都没有),但是我仍然愿意去。因为进入一家比较有名的互联网公司,当时对我来说实在太重要了。


2015年,唯品会刚好开始进行核心系统大重构,我刚好在公司的重构部完整的经历了整个过程,参与了几个核心系统的重构工作(其中一个商品项目,在唯品会2016年的大促里,流量是最高的,真的是百万级别的瞬时流量),都是从零开始弄的项目。整个过程下来,自己的JAVA技术、系统设计和架构、以及技术认知,都提高了好几个档次。也终于见识了,一个高并发、高可用的系统是怎么弄出来了,要经历过一些什么,要注意一些什么。记得在由于体会越来越多,也陆续写了很多博客,后来还成了CSDN技术博客专家。


期间由于表现还不错,晋升到高级开发(P3),刚晋升后不久,我就跟原来的leader提出说,能不能让自己带一个小团队,锻炼一下带队的能力。得到的回复是:



团队暂时没有这样的坑位。



但是带团队的经历对我来说是非常重要的,因为我当时的年龄已经不小了,技术上也没有什么特别的优势,但是项目管理的潜力还不错。最后没有办法,我只能提离职申请,离开待了快3年的唯品会。当时的离职不是一时冲动的,而是职业发展所需。



  • 年纪也30好几了,技术天赋也一般,走技术专家不合适我;

  • 对项目管理非常有兴趣也有一定的潜力;

  • 当时的团队暂时没有坑位;


基于上面几点,我又开始走上了寻求技术管理岗的求职之路。


经验教训:




  • 早期不要毫无目的的频繁换工作,很多中大型公司很在意这个的,会因为你频繁换工作而把你刷掉。因为职业发展所需才去跳槽才是对的;




  • 如果能进入到一家好的互联网公司,最好是去核心部门,学到的东西才多,成长才足够快;




  • 只要去过一家比较有名气的互联网公司,是能将你不好履历洗白的。这个对后面找工作非常有帮助;




  • 找一个合适的靠谱的女人结婚,对你的影响是一生的。




第一次当技术组长


以我当时的实力和背景,去大厂带团队是不可能的,只能去一些中小公司,但是这个没关系,只要能让我带团队就行,先入门先。


刚好当时唯品会有几个认识的同事离职了,去了深圳的一些创业公司做技术负责人或者架构师。当时有3家公司可以选择,也都面试通过了(有熟人推荐和介绍,确实成功率高很多)。但是只有其中一家有机会带一个7人小团队,加上当时的推荐人在那边是技术负责人,对技术这块有完全的话语权,我就果断了选择了这家。


这家创业公司是做小程序电商的,是做这块业务的头部公司,业务发展非常快。基本上所有的业务项目都是倒排期的,技术团队是没有任何的反驳的余地的。



经常出现大老板对外说,我们什么时候推出什么样的产品,而我和我带的小团队,一行代码都还没写呢?



因此如何按时交付倒排期大项目,成为当时我面临的最大挑战。当时是小团队,我也是参与写大量代码的(必须一直在一线),但同时得兼顾如下几个角色:



  • 架构师,主要是关键系统的设计和核心模块的开发;

  • 项目管理者,负责与产品和业务方沟通,并制定详细的项目计划,让大家能依计划行事;

  • 管理者,主要是团队的成长、绩效这块。


那刚开始顶得住吗? 肯定是顶不住呀,我经常因为赶工,睡公司沙发。能力还没修炼到家,那就只能拼时间了。


这个节奏一直持续接近两年,非常的辛苦,但是成长也是有的。具体如下:



  • 正儿八经的带过团队了,团队虽小,但也算五脏俱全了。团队管理遇到的一些问题也基本遇到了,这个就是我当时想要的,因为后续再去找工作的时候,我就是实打实的有带团队的经验了;

  • 项目管理能力得到飞速的提升,毕竟要经常应付紧急大项目。项目管理能力是技术管理者的一项重要的横向能力,是一定要懂的;

  • 在创业公司,业务交付是最重要的,技术的长期规划是不重要的。老板都不知道某块业务上线后又没有用呢,你IT团队操心个什么技术的长期价值呢?

  • 在创业公司带团队,自己的硬实力一定要有,随时能在一线大量的写代码;

  • 人脉对找工作极其关键,我能快速的入职这家公司并带一个小团队,也是因为在唯品会累积了一些人脉。他们有好的去向,你是可以跟着去的。


第一次当技术经理


后来这家创业公司业务发展不行了,开始裁员了,我也不得不考虑一下自己的将来,于是又借助人脉,去了一家餐饮公司,当时这家公司的CTO以前也是认识的,简单唠叨几句后,很快就接到了该公司的面试通知,由于自己有唯品会的重构经历也有创业公司的带队经历,整个面试过程还是很顺利的,拿到了一个技术经理的职位,负责中后台业务,团队是13人。为了能快速的融入这家公司的IT部门,我当时做了几件事情。



  • 通过一对一沟通,了解团队的瓶颈;

  • 积极的去一线处理线上问题,以便快速熟悉业务和了解应用系统的情况,同时也能让更多的人认识到我;

  • 大重构;


记得刚进入这家公司没几天,上级就提到,我负责的团队的情况不是很乐观,项目经常延期且跟其他职能团队配合的也不太好,让我想办法解决一下。言外之意就是说,这个团队不好管,让我多花点心思。由于之前已经有两年的带团队经历,深知跟团队成员一对一沟通的重要性,尤其是跟核心员工。


于是便找了团队的两个老员工,请他们吃饭,顺便了解团队的情况。当时其中一个员工情绪稍微有点激动:



团队负责的核心应用,大部分还是PHP写的,但是PHP开发只有3个,业务需求又多,根本忙不过来。而JAVA开发基本又没事干。



基本是一针见血了,这个是团队的真正问题所在。忙的人忙的要死,不忙的又闲的无聊。事后我跟上级反馈了这个问题并提出了解决思路:



必须进行核心系统的重构,除了提高稳定性外,也能极大的提高业务需求交付速度;



在得到上级的批准后,便紧锣密鼓的操办起重构来,那会的阻力很大,因为产品负责人觉得这样会阻碍业务需求的交付,而测试负责人则反馈,重构期间,测试人员得两边测试,极大的加大了测试工作量。没有办法,我只能把问题上升上去,直到CTO那边。最终CTO拍板,必须重构,并让我列出详细的重构计划。


于是乎中后台技术团队足足花了10多个月,把核心的应用全部从PHP转成JAVA。期间也出现了很多线上问题,但由于有完善的灰度和回滚方案,都在5分钟内恢复正常。我因为这个重构项目,在次年晋升为高级技术经理,而我的上司则晋升为技术总监,记得当时他去晋升的时候,CEO还特意说了句:系统最近稳定了很多。


期间为了能快速熟悉业务,我做了一件很疯狂的事:



我个人在线值班3个月,并作为线上问题的对接人。



无论是线下门店的问题还是线上的app和小程序问题,都可以直接找我。这个当然会占用我很大的一部分时间,但我认为值得,理由如下:



带着问题去询问或者查看代码,效率是非常高的,因为问题非常明确,就摆在那,你压根就不用去想,从哪个地方切入去熟悉业务,直接把当前的问题解决掉,就能了解到一点业务,通过长期的坚持的处理一个一个的问题,慢慢的从点到线到面的了解整体的业务。随着解决的问题越来越多,你会越来越了解当前的业务应用,有哪些模块,都是负责什么的,当前是怎么玩的,哪些是关键的地方绝对不能出问题的,哪些又是强依赖第三方的,渐渐的会对应用有个整体的认识。当然也会知道系统哪些地方需求去打补丁,哪些地方需要去做监控。业务熟悉了除了有助于自己快速的融入到团队里去,还可以为自己后续的任务合理分配和系统有效规划打好基础,当然最重要的还是有助于与团队内部、外部、上司进行良好的沟通,如果业务都不熟悉的话,很多事情都无法直接跟你谈。



没有这段处理线上问题的经历,我是无法很好的推动重构的。因为业务和周围的人你都不熟悉,别人也不了解你。


在这家公司除了做重构之外,也交付了很多能给公司带来GMV的业务需求,上司和CTO也都比较满意,基本上每年都给我涨了不少工资。在CTO眼中,能帮大忙的,钱就尽量给够。那几年公司发展的不错,中后台技术团队的业绩也很不错,每年的蛋糕分的都比较多,团队的人都很开心。


经验分享:



  • 要去做有挑战有难度的事情,只要做成了,团队就容易拿到好的成绩,对团队的稳定性是极其有用的。一定要从团队的角度出发来考虑问题,像当时如果没有做好重构项目,我估计团队就会散掉了;

  • 跟着你一个看好你支持你的上级以及CTO,超级重要。他会信任你保护你并授权给你,让你做重要的项目,让你出成绩;

  • 稳定性是技术团队保命用的,如果系统不稳定,随时会被干掉的;

  • 空降兵,一定不要急着去改变团队现状,一定要先融入,了解团队的人和事,找到那几个关键的问题,然后全力推动去解决;

  • 项目管理能力很重要,因为大老板很看重这个能力。能按期按节奏交付系统,是很重要的。作为技术管理者,必须掌握这个技能;


我在这家公司待了也2年半。


第一次当后端技术负责人


在2022年2月,我又跳槽去了一家公司,当时离开上家公司是没有办法的事情,因为原CTO离职了,公司新招了一个技术总裁,哇去,带来了N多老部下。几乎所有的核心老员工都需要走,包括我。


当时我心里就想着一个事,CTO是否稳定太重要了。


因此从那家餐饮公司离职后,陆续去面试了几家,每每到最后,我都会问面试官,CTO的背景是什么,是否稳定,因为我吃过亏了。


当时选择这家公司有如下几个原因:



  • 这个行业的资本很活跃,一直在投资公司。也即是资本看好这个行业,赛道足够大;

  • 公司是这个行业的头部公司;

  • 我应聘的职位是后端技术负责人,责任变得更加大了,同时title也升级了;

  • CTO背景非常好;


这次我没有借助人脉,完全靠自己的判断和职业诉求去找工作。对团队内部的情况并不了解,里面一个人都不认识。相当于是进入了一个完全陌生的环境。因此也踩了不少坑,导致在早期,我在这家公司,过的极其的辛苦。


首先是leader层以及技术总监的技术专业水平,强到完全超乎我的想象,他们在技术架构、技术规划、技术管理、项目管理,有一套极其严格的规范,且是他们在上家公司严格经过考验的。这个导致我很不习惯,因为有一堆的方法论在卡着你,我在前两家创业公司里,做事的时候,有时候是有点野路子的,当然这个也是被逼的,因为当时业务方压的太紧了。


但在这家公司里,业务需求是能停一停的。可以专注的去做技术项目,提高系统的稳定性。大家应该知道,如果纯做技术项目,对你的技术能力要求是非常高的,但我已经有两年多没写代码了,刚开始完全适应不了。


另外呢,他们的要求又极其的高,像技术方案、代码质量,code reivew,压测,技术架构合理性,没按照要求来做的,通通打回重搞。再加上我的上级是细节控,一路盯着你,哎,搞得我压力山大。我曾经一度怀疑自己,我还合适待在这家公司吗? 怎么感觉之前累积的东西,在这里完全没用了。


后面压力实在太大了,只能多次的敞开的跟上级和CTO聊,慢慢的,自己的心态才缓过来。那会我开始明白了一些事情。



  • 勇敢的承认自己的不足,不是什么丢人的事情。一定要敞开的跟老板聊,老板虽然严格,但是他会帮你的;

  • 心态要好,有些人的实力就是比你强大很多倍,在他眼中很容易做成的事情,你可能一直都无法做好或者说需要花费很大的精力才能完成。要能接受这种差距,并努力提高自己,慢慢减少差距;

  • 有些事情真的急不来,解决它就是需要花时间的。


在这家公司经历了12个月的磨练后,哇去,我自己成长真的是飞快,而且成长质量非常高,比以往任何一家公司都高。毕竟跟着一波高人做事情,成长是最好最快的。


当然期间你得能挨过去,中间我其实有离开的想法,但是后面还是坚持了下去,到目前为止,基本上迈过了这个坎了。


那么在这家公司我学习到哪些技能?



  • 对团队要高要求,团队成长才足够快。中间如果有人适应不了的,那就请他走;

  • 如何用最标准的方式,准备一场大促;

  • 如何管理自己的时间。这个是我在这家公司才真正掌握到的,做管理的,其实有一条暗线,那就是管理自己的时间。如果做的不好,你可能一路都被别人牵着走;

  • 如何高质量的从0到1,搭建应用系统。以前我都是短平快的把事情做成,在这边,学习到如何系统性、体系化、长期化的去思考和解决问题;

  • 做管理的,如果自己都没重点,那团队也就没有重点,这个是很危险的事情;

  • 如何从0到1的搭建研发流程体系。


小结一下的话,就是到目前为止,我学习到了一些技术总监才懂的技能。有一种从八路军到正规军的转变的感觉。


总结


提几个重点吧。



  • 跳槽要根据自己的职业规划来,每次跳槽都是往上跳。呈上升的趋势,不要搞降级跳,平级跳。我这边在后面几年就是按照这个思路来的。传统公司-->唯品会高级开发-->技术组长-->技术经理-->后端开发负责人;

  • 每次跳到一个新的台阶,你都会累到半死,只要你能挨过去,就能得到飞速的成长;

  • 跟着高手才能学习到好东西,要注意成长的质量;

  • 勇敢的承认自己的不足,不是什么丢人的事情;

  • 心态要好,要能接受差距,并努力提高自己,慢慢减少差距;

  • 团队管理,必须在实战中体会和掌握。

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

面试必问的三个问题,你怎么回答

现在已是四月中旬,企业招聘也快到了落幕时分。 相信参加校招的同学们都或多或少参加过面试。 今天学长就和大家盘点一下咱们在面试过程中一定会被问的问题。 01 请介绍一下自己 自我介绍是面试中非常关键的一步。很多面试官第一个问题都会问:能否请您做一下自我介绍? 因...
继续阅读 »

现在已是四月中旬,企业招聘也快到了落幕时分。


相信参加校招的同学们都或多或少参加过面试。


今天学长就和大家盘点一下咱们在面试过程中一定会被问的问题。


01 请介绍一下自己


自我介绍是面试中非常关键的一步。很多面试官第一个问题都会问:能否请您做一下自我介绍?


因为在自我介绍的时候,面试官通过自我介绍,可以考察到他们的语言表达能力、应变能力和岗位的胜任能力。


一般来讲,自我介绍的时间一般控制在 2-3 分钟,如果面试官有严格的时间要求注意不要超时。


那么,在如此短的时间内,求职者该如何彰显出自己呢?应该说些什么?应该怎么去说?


准备好一段简洁而生动的自我介绍,突出自己的优势和特点,并结合应聘的工作职责进行陈述。


自我介绍没有技巧,最重要的就是真诚二字。


可以美化自己的经历但一定要基于事实,保证后续如果问到相关的问题能对答如流,而不是自己给自己挖坑。


最好是提前练习自我介绍,不要在面试的第一关就出现紧张忘词的情况,也不要过于死板。


学长为大家准备了一个自我介绍的模板,仅供参考!


首先,我有丰富的项目经历。比如我曾经在某公司实习,参与多个项目。


其次,我的协同合作能力也不错。比如我在校期间参加某某活动,对接多个部门。


最后,我有很强的专业技能。在实习和项目中有很强的动手能力,学习能力较强,能很快上手。


02 你对我们公司有什么了解?


在面试前,通过查看公司官方网站、新闻文章等信息,掌握公司的背景信息,对公司有基本的了解。


从你在调查收集的信息中,你可以选择在回答这个问题时应该提到的关键细节。


你需要特别注意那些单位重要的成就或他们即将发布的产品。


你可以重点谈谈公司的成就、公司历史上的重要里程碑。


以及与你的职业目标或个人目标相一致的品牌使命和价值观。


此外,你必须用一种真诚的方式表达你对加入公司的渴望。


03 说说你的缺点


有的同学可能想皮一下:我的缺点就是没有缺点。


面试官 Belike:


image.png


人无完人,每个人都会有大大小小的缺点。说点真实的小缺点可以适当加分,说明你能认识到自己的缺点。


HR 想通过这个问题看到你解决问题的能力,以及你个人为缺点的改正做了哪些努力。


说自己的缺点,要学会避重就轻。


假如你面试开发岗位,你可以说自己的领导力没有那么强。


要注意的是,这些问题都没有标准答案。


总之,在回答问题时,需要结合自己的经验和公司的实际情况作出恰当的回答。


同时要注意自己的表达方法和语言表达,保持积极、开放和真实的态度。


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

“丑小鸭”真的能变成“白天鹅”吗?

周末到了,忙碌了5天的小伙伴们,估计也没啥心思再看技术文章了吧。其实总写算法和源码解析的文章,我也有些腻了,今天的文章随便聊聊吧。 关于今天文章的主题,其实很多人都迷茫过,我也曾迷茫彷徨,尤其是看过《最强大脑》之后,更觉得自己平庸得简直无药可救了,怎么聪明人那...
继续阅读 »

周末到了,忙碌了5天的小伙伴们,估计也没啥心思再看技术文章了吧。其实总写算法和源码解析的文章,我也有些腻了,今天的文章随便聊聊吧。


关于今天文章的主题,其实很多人都迷茫过,我也曾迷茫彷徨,尤其是看过《最强大脑》之后,更觉得自己平庸得简直无药可救了,怎么聪明人那么厉害?我这完全没有跟这些人拼下去的资本和机会啊!


同样在学的时候,很奇怪的是,班级里总是会有胖胖憨憨的同学,总是会有打架很厉害的同学,也同时大概率都会有那种一上课就趴桌子睡觉,然后一考试就名列前茅的“奇才”。我高中就有这样的同学,我们戏称他为“觉主”(睡觉的“觉”,谐音教主)。


那么,面对着这些天才和大牛,我们怎么办呢?**总不能束手就擒,任其宰割吧!**当然不能这样了,一句话“凭什么!”。


为了能找到解决的办法,我们可以从问题的产生原因出发。以我们软件研发人员为例:技术团队里的牛人太多了,有的人架构思想深厚,有的人编码速度飞快,有的算法思维缜密,而反观自己,貌似都很平平,然后越发自卑,没有破解之道。那么,其实我们进入了“小巷思维”,即:我们用一维视角去看待问题了


一维是什么?简单说,就是一条线!比如:技术水平的高低,就是一条线。我们发现自己拼劲了全力,依然“只能用自己的极限去挑战对方的怠速”,拼不赢,怎么办?那么我们就增加自己的二维能力,而此时我们的能力就是一个面。


比如,我默默提升自己的英语能力。好!你就拥有了英语这个二维能力了。加上这个能力后,自己还是不能在人群中脱颖而出,怎么办?我们增加自己的三维能力。比如,学习演讲技巧,可以在公司内外进行技术宣讲。或者提升管理能力,让自己成为技术+管理的复合型人才……那好,我们现在的能力就从一个面,提升为了一个三维体了
file
还是没有感触?没关系。我们来看一下周围的世界,有太多太多采用“多维竞争”的例子了。以这几年我国新兴的国产品牌新能源车为例,除了汽车的基本功能属性外,我们跟你拼大屏幕、拼屏幕多、拼座椅舒适、拼车载KTV、拼车载影院、拼全方位的客户服务、拼车子长、拼车子宽……


在以上这些多维度的加持下,不得不说,当电车突破了发动机和变速箱的技术壁垒之后,我们的国产新能源车竞争力越来越高了。甚至已经让BBA等国际车企们措手不及了(看看宝马i3、奥迪e-tron、奔驰EQC等等合资或外企的新能源车销售量有多低、优惠折扣有多大就知道了)。
file
所以,归根结底,我们不要只盯着某个一维空间上自己又多“弱小”,而应该发掘自己在多维上面的优势。要谨记这句话——“天生我材必有用”,不要放弃自己,不要让自己躺平,要“支棱”起来。我们不是鄙视平凡——我们努力了,但是没有获得好的结果,迫使自己终归平凡,这种平凡是那种“凡事全力争取,结果顺其自然”的平凡,是自己释怀自己的平凡;而不去奋斗,不去争取,好吃懒做而使得自己平凡,这种才是真的会让周围人鄙视的平凡了。


多维度提升自己的能力,不要轻易放弃自己,牢记能力提升复利曲线,我们需要做的只是持续成长,当积累终归由量变转化为质变的时候,终会感叹——“回首向来萧瑟处,也无风雨也无晴”。


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

如何避免简历石沉大海!

背景 疫情当下,金三银四已经失去了原本的辉煌。在投了一段时间简历,仅收获寥寥几份回复,甚至自己看好的岗位始终没得到沟通消息后陷入迷茫。 这岗位是假的吧...... 唉~环境真的太差了...... 其实也不全归咎于环境,企业也还在招人,岗位还是有不少的。说直白一...
继续阅读 »

背景


疫情当下,金三银四已经失去了原本的辉煌。在投了一段时间简历,仅收获寥寥几份回复,甚至自己看好的岗位始终没得到沟通消息后陷入迷茫。


这岗位是假的吧......


唉~环境真的太差了......


其实也不全归咎于环境,企业也还在招人,岗位还是有不少的。说直白一些,只不过是你不合适,或者说你的简历不合适而已。


之前总是听人在强调简历的重要性,但是没有特别深刻的认识,最近因为人力不足只能自己去发布岗位,筛简历,参与了简历筛选工作后才发现:哦,简历原来这么重要!!!(痛只有在自己身上,才知道有多痛!别人说的很难让自己重视!)


关于在线简历


在线简历一定要认真写!在线简历一定要认真写!在线简历一定要认真写!


大家都知道,注册了招聘网站后就让你填写在线简历


很多人可能都没太关注,随便填一下就去看职位了。找到自己感兴趣(我觉得我可以)的岗位后,就发起了沟通。


企业微信截图_16806604679636.png


或者,过了一段时间准备找工作了,只认真更新了自己的附件简历,忽略了在线简历。


如果你有这种情况,那么你就得抓紧更新一下自己的在线简历吧!!




原因: 在招聘平台,以BOSS ZP为例,求职者向职位发布者发起沟通后,职位发布者仅能查看在线简历!!! 而且在线简历在一开始真的巨多, 也就是说,你在线简历随便写写,没有抓住发布者眼球,那就只能先拜拜了。


当然,有些硬性要求也会导致筛选直接被否掉,比如学历,稳定性......


关于沟通


前边我们有提到,找到自己感兴趣(我觉得我可以)的岗位后,就发起了沟通。这个沟通,真的就只是一句话的沟通,在简历太多的情况下,很容易被忽略,要吸引发布者查看自己简历其实很简单:多发几条,让他知道你真的有关注,比如:


image.png


如果能够介绍一下自己与岗位的匹配点那就更好了:


image.png


关于简历内容


此时不出意外,岗位发布者就要点开你的在线简历查看了。这个时候内容为王,一定要在短时间内,让他觉得OK,可以聊一聊。


关于简历怎么写,已经有很多的文章说明了,这里就不多说。简单提两点:


工作经历部分,一定要简洁,还要能体现价值
项目经验部分,还是要简洁,挑重点,体现价值和亮点,当然相对工作经历来说要详细。 写的太多真的很难抓住重点。所以这里也可以根据情况,针对岗位单独做一份简历,重点突破。




做到前面三个步骤,基本上自己的简历不会被简历海淹没掉,有合适的岗位肯定是能够收到面试邀请的。至于面试的事情,那就后续再聊。


最后,祝愿找工作的人都能拿到满意的offer。


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

大龄,掘金,疫情,酒店,转型,前端满两年,搞公司后端两个月,年后离职还是继续等待?

大家好,我是 那个曾经的少年回来了。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岁真的会被毕业吗?而且是会被永久毕业吗?如果身边的朋友、同学、又或者是同学的朋友、同事的朋友等等真的是大批量的都被毕业了,那么我才会觉得风险是真的来了。




  • 现在就是时刻准备着可能要发生的事情,企业如果真不行了,或者自己真的想换工作了,就提前准备不就完事了。




  • 说真的每天时间就那么有限,自从你有了家,有了娃,时间就如白驹过隙




  • 没什么负面情绪,如果有的话就转化为正面动力吧




  • 浅层的学习靠输入,深层的学习靠输出:通过几期的学习源码,能深刻感受到自己看一遍和写一遍真的是非常不一样




  • 兄弟们加油吧,也许在疫情的催化下底层人民过的将会更加艰苦,多关照一下家里的老年人




  • 在疫情的催化下我们也要重新考虑一下我们的工作和生活方式了




  • 喜欢的可以到创作者榜单点点我,估计也没几个人点我哈哈,自己点自己嘞


作者:那个曾经的少年回来了
链接:https://juejin.cn/post/7181095134758387773
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

既当产品又当研发,字节大哥手把手带我追求极致

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做...
继续阅读 »

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做的东西,上面说的特点至少有一个不具备,甚至通通不具备。


而我在字节实习的过程中,所经手的恰恰就是这么一个需求不明确、解决方案不明确、最终产品效果不明确的项目。整个过程中有过焦头烂额毫无进展的时刻也有过欲哭无泪的时刻,还好有我的mentor带着我一路披荆斩棘、过关斩将。


首先和大家讲一下项目背景,当时我在的组是视频会议移动端,经历了近三年大流感的洗礼,相信大家对于视频会议中可能遇到的各种问题如数家珍,包括但不限于没声了、没音了、没画面了、画面卡顿、画面不清晰、画面和语音不同步、同步屏幕时闪退等等等等。作为一个服务企业级的B端产品,出现以上问题时就可能会投诉,然后经过客户成功部门转手到运营再转手到研发这里,研发就需要判断分析一下究竟是我们产品的原因、还是客户本身设备的问题、或者是第三方环境的因素,当用户的量级上来后,这样的客诉就会很多,会严重占用oncall的研发人员的时间以及精力。


我的mentor,一个专注于解决问题、避免重复劳动的人,一个字节范我觉得有E+的人,一个虽然身处移动端但是前后端甚至网络也都会的人,觉得这样很不OK,应该有个工具,能够自动的分析出来客户究竟遇到了什么问题,分析不出来的再找研发进行排查。没有这个工具也不影响业务开发的进展,所以整个项目并不存在时间上的紧迫性,但是呢,有这个工具做出来后肯定会大大降低研发的开发时间,所以项目的必要性还是有的。于是,我作为刚入职的实习新人,这个项目就交给我来做了。


而我,一个还没有从校园中完全出来的新兵蛋子,说实话面对这样的场面是一脸懵逼的,对于要做啥、要怎么做可以说是一无所知,我的mentor在我入职后,让我先了解了解背景,第一周就带着我oncall了,让我知道都可能有样的客诉,手把手给我演示他们正常的排查问题的方式。先了解客户反馈的情况,然后捞出来客户对应时间的设备信息以及设备日志。


说实话,作为一个新人,或者说我本身对于项目有一种畏难心理,碰到一点难题就总是想着往后拖,或者摆烂先不管然后就搁置在一边不想再问津了,但是我的mentor是一个有着坚定信念的人,差不多就是见山开山,见水架桥这种,遇到问题会主动找到相关人员一起解决,可以说就是有那种主人翁,项目owner的意识。于是,我就跟在他的后面,和整个团队的不同角色沟通他们遇到问题时排查的思路,试图总结出来一种通用的流程。在过程中,难免有许多困难,我的第一反应是退缩,但是导师的第一反应是拉会拉上相关人员一起讨论,看看用什么方式可以解决。比如在如何确定设备日志和故障表现的映射关系时,先后调研了多种方式看看相关团队有没有类似的做法以及他们最后实现的效果,包括大数据机器学习、代码状态流转图、自定义规则引擎等多种方式,最后调研后决定采用自定义规则引擎的方式。在实现需求的时候,需要其他团队协作时,他总是直接向前提出自己的需求,而我向一个陌生人发消息之前总要做一些心理建设,总是在担心些什么、害怕些什么,而事实上大家都是打工人,谁也不比谁厉害多少,对方不配合那就拉+1进群一起看看,解决不了就向上暴露问题。


于是,导师披荆斩棘在前,我在后面跟着实现他的设想。我们很快就做出来了第一个版本。通过Python自动化下载设备日志,然后正则匹配筛选出含有特定标记的日志,并对他们的出现频率次数做出判断。因为Python是解释型的语言,所以可以把规则直接写成python语言,用eval或者exec函数进行执行。第一个版本做出来后,导师又积极的带着我去给其他人宣传我们的这个工具。然后根据他们的反馈继续进行相关改进,最后我离职前实现的效果就是@ 一个群里的机器人,告诉他出现问题的ID,他就能自动化的拉下来日志进行排查,然后告诉你他分析的结果,整个交互非常的方便。


一个成功的项目必须要有一个负责的owner,我的导师就向我展示了一个优秀的owner是如何一步步解决问题、排除项目中的难关,如今我也正式成为一名打工人,希望我也能早日如他一般自如的面对工作。


我是日暮与星辰之间,出道两年半的Java选手,相信时间的力量,一起成为更好的自己!


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

思考 | 公司活力

经常看到一些互联网公司,尤其是创业公司,宣传自己的年轻员工占比高,公司充满活力。久而久之,大家都开始习惯这套说辞,认为一家公司有没有活力的标志就是年轻人多不多。可是当我们说一家公司充满活力时,到底在说什么? 我们是在说这家公司员工的体力充沛么?肯定不是,毕竟我...
继续阅读 »

经常看到一些互联网公司,尤其是创业公司,宣传自己的年轻员工占比高,公司充满活力。久而久之,大家都开始习惯这套说辞,认为一家公司有没有活力的标志就是年轻人多不多。可是当我们说一家公司充满活力时,到底在说什么?


我们是在说这家公司员工的体力充沛么?肯定不是,毕竟我们不用比拼打螺丝的速度,也不用比拼抡大锤的力度。再说,抡大锤,小年轻们不一定比得过中年大叔呢。


我们是在说这家公司员工满面红光、笑容可掬么?那也不是,毕竟要论这两项,高速收费口的工作人员早把各位杀的片甲不留。


我们是在说这家公司盈利颇丰么?那更不是,毕竟谁也不会把中烟草和活力二字扯上关系。可是若论赚钱能力,互联网的各位在中烟草面前都是弟弟。


那我们到底在说什么?


想必各位都看到今年上半年AI圈的迅猛增势,这些公司中有创业公司,譬如OpenAI和Midjourney,也有巨头公司,譬如Nvidia和Google。它们都可以称得上充满活力,原因并非它们拥有年轻的员工,而是它们一直做着创新突破的工作,是真正把大家带入更好未来的公司。


所以当我们说一家公司充满活力时,并不是想说它的员工充满活力,而是它的产品、它的创新工作充满活力。


事实上,Nvidia和Google这类公司中,大龄工程师并不少,而且多数都是研发的主力。举个例子,在我接触过的Google工程师中(Runtime和ART团队居多),多数都拥有10年以上的工作经历。那这是否说明大龄工程师更具优势?难道国内的35岁理论要失效了?


问题的关键其实不在于工程师的年纪,而是工程师是否优秀。一个优秀的工程师25岁时表现优秀,等到50岁时就会变得卓越。而一个平庸的工程师,不论25岁还是50岁,都不会有多少突破性的工作。这就好比,不是老人坏了,而是坏人老了。你非要拿着年龄去衡量工程师的水平,多半会徒劳无功。


那这么说,年龄不是标准?其实年龄也是标准。因为优秀的工程师终归是少数,刚工作的时候泥沙俱下,分不清好坏。工作愈久,大浪淘沙的效应就愈明显。最终,那些少数优秀的大龄工程师都身居了要位,而剩下的那些,则频繁地活跃在求职市场。


国内职场令人吊诡的一点是,优秀的人才最终都走向了管理的位置,或者说,如果你走不到管理的位置,就不会被认定为优秀。这在技术领域其实是有严重问题的。专业人才得不到足够的重视和认可,站在背后指手画脚的人却被捧上了天,搞得大家都去研究PPT,学习向上管理,却忽略了创新不同于打仗,它主要来自于直接的实践,而非稳坐后方的指挥。


不过现实的残酷在于,创新只属于一小撮人。大多数的工作谈不上创新,只是时间和精力的耗散。不然为什么会有“人力资源”这个词?当把创新或者人的主观能动性从工作中剥离后,人便是资源,和矿产无异,而管理者的高贵就体现在如何支配这些资源。


我希望有一天,50岁的人也可以写代码,写出很漂亮的代码。毕竟,我们都有50岁的一天。


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

被一个问题卡了近两天,下班后我哭了。。。

写在前面 好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。 近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。 好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。 整个过程 没经过...
继续阅读 »

写在前面


好像很久没有更文了,感觉有很多想写的,但却又不知道该写些什么了。。。


近阶段,整个人的状态都好,本计划这月想给自己充电,做一些自己想做的事,结果真的就是事与愿违吧。


好像每个人都一样,都是为了生活而疲于奔命,依然忙碌于各种事情之间。


整个过程


没经过深思熟虑的计划制定


两周前,组内同事想让我帮忙做冒烟测试脚本,原因是因为每次发版测试的时间耗时特别长,所以在结束批量测试工具的开发工作后,我便主动和领导请缨做冒烟测试脚本的开发工作。



和领导说,脚本开发需要5天,整个冒烟测试每次需要大约5分钟!



领导听完很吃惊,我自己说应该差不多吧。


迷之自信?


可能很多同学也会和我的领导一样吃惊,为什么?


系统发版后的回归测试,就测试场景和流程来看,工作量肯定不小,姑且不说技术问题,就业务流程的梳理就很费时间了。


而我却说整个过程只需要五天,可见我是多想证明自己了


其实不然,我自己还是有一些考量的,才说出五天,原因有两个:



  • 因为信任,所以备受期待,同事信任我,真的感觉自己被需要,并且想为团队贡献出一份自己的力量;

  • 因为之前做过测试环境的性能测试脚本,以为很多接口可以直接拿来就用(我天真了,因为改了不少,需要重做)。


理性永远在给感性收拾烂摊子


整个系统总共6个测试流程,也就是说我每天要完成1.2个流程的脚本开发。


我特别喜欢现在团队的氛围,第一天到下班点时,差一个模块就完成了一个流程。


所以在责任心的驱使下,心想加个班吧,今天能赶出来这个模块,明天其他的流程就能复用了。


一切看似很好,也正是这个模块把我彻底卡住了,我遇到了一个让我很抓狂的问题:



打个类比,比如发起申请接口,申请成功了,到领导审批,点击同意的时候报错,而发起申请这个接口却不报错,你在页面同样的操作,领导同意却是正常好用的。



被问题卡住,心态开始崩盘


这个问题,我反复查了近两天......


这期间我积极的找开发同事帮忙排查问题,并确认是否是我的入参不对导致节点数据不正确。


由于开发同事比较忙,能帮我排查问题的时间有限,所以只有在开发稍微有点时间,才能帮忙排查联调。


也正因为开发同事的尽心尽力帮忙,几次下来,让我感觉离问题根源好像又进了一步。


也知道为什么不能审批了,因为虽然请求成功了,但是没走业务逻辑,导致部分数据还是默认值,所以审批报错。


关于入参的排查,暂时告一段落了,因为数据状态不对,无法进行审批,意味着还是没有解决问题。


到这已经是第三天了,一个流程都没整完,感觉整个人都不好了,心态有点崩了......


于是向领导说明原因,领导了解后,并说先把耗时最长的做完,虽然没那么大压力,但是心里还是有些深深地自责。


我还是没忍住,终于哭了出来......


距离周五晚上发版测试还有两天,这个问题不解决,怎么也说不过去,心里一直憋着这个劲特别难受。


当时的想法,真的是谁能帮帮我,帮帮我行么?


但是我也不知道该找谁帮忙,谁又能帮助我?


为什么?说是业务问题吧?还不算?技术问题吧,入参还查不出来啥问题?真的就是进退两难!


因为开发太忙,实在没时间,暂时也没想到什么好的解决办法,我就先下班回了家。


把车停好后,习惯性地给女友打了电话,那天还是我的生日,再加上那阶段烦心事特别多,说着说着我哭了出来,突然感觉好无助而且很没用,最后彻底哭了出来,为什么就那么难?


我以为我很颓废,今天我才知道,原来我早废了。


因为烦心事特别多,导致整个人都不好了,哭出来后,感觉真的很舒服,而且整个人平和了许多。


没人能教你,只有自己能拯救自己


回到家后,搭建好环境,改用工具进行测试,使用jmeter+fiddler抓包开始,重新调接口来模拟测试,结果居然成功了,真的很意外,难道是我代码写的有问题?


第二天上班,我把自己代码接口调用及入参与昨天做好的jmeter脚本一一对照,发现入参一模一样,这让我产生了怀疑,是我封装的工具类有问题?


我代码走的HTTP协议,而jmeter脚本是HTTPS协议才成功的。


这让我想到,可能我的httpclient需要走HTTPS协议请求会让接口调用后,数据应该会正常显示吧。


有了思路,就开始找httpclient如何进行HTTPS请求的相关文章。


经过一番搜索,找到的重点都是围绕使用ssl和根证书的使用的代码片段,我又对httpclient底层封装进行改造,改造完再次使用封装工具类调用接口,结果还是数据状态不对,我真的彻底绝望了。


于是,我又去找到了强哥(我北京的同事),强哥说你干嘛自己封装,用hutool呀。


我照着强哥的思路,又去照着hutool中的工具类,开始写demo,逐一调用接口,结果竟然成功了,这让我欣喜若狂,真的好用。


于是,我对写好的demo,再次进行封装,也就是hutool中的工具类封装,封装好后,再次使用封装好的工具类调用,结果数据状态又不对了。


我真的服了,这是玩我吗?分开就好使,封装就不行。


有的同学说了,应该是你封装的有问题,那为什么其他模块都好用,就这个模块不行?


后来,我灵机一动,那就都对分开可用这部分代码进行简单封装,保证流程跑通就行,算是退而求其次的解决方法,虽然,它很low,但是能用。


也正因为这个临时解决方案,助力我在周五发版前成功的让同事用上了,一个流程的冒烟测试,跑完这一个流程仅需113秒,比手动回归快了近10倍的时间。


写在最后


整个过程让我记忆深刻,在此特别记录一下,真的是头一次被问题卡的这么难受,那种既生气,又干不掉难题的感觉,太难受了!


你有被难题阻塞,一直无法继续下去的情况吗?欢迎文末给我留言哦!


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

PC网站如何实现微信扫码登录

不管你运营什么类型的网站,用户注册都是很重要的一个环节,用户注册的方式也是很多的,比如邮箱注册、手机号注册、第三方授权登录等。其中,第三方授权登录是最常用的一种方式,微信扫码登录是其中的一种,但是微信扫码登录的实现方式有很多种,比如公众号扫码,小程序扫码,网页...
继续阅读 »

不管你运营什么类型的网站,用户注册都是很重要的一个环节,用户注册的方式也是很多的,比如邮箱注册、手机号注册、第三方授权登录等。其中,第三方授权登录是最常用的一种方式,微信扫码登录是其中的一种,但是微信扫码登录的实现方式有很多种,比如公众号扫码,小程序扫码,网页扫码等。


本文将介绍一种简单的实现方式。


技术栈



  • 后端:NodeJs / 企业级框架 Egg.js

  • 前端:Vue

  • 微信小程序:uni-app

  • 数据库:MySQL


实现思路



  1. PC 端网站生成一个二维码,定时 3s 轮询请求接口,判断用户是否扫码,如果扫码,则返回用户的微信信息。

  2. 用户微信扫码后,会跳转到微信小程序,小程序打开点击注册按钮,会获取到用户的微信信息,然后将用户信息发送到后端。

  3. 后端接收到用户信息后,判断用户是否已经注册,如果已经注册,则直接登录,如果没有注册,则将用户信息 openid 和 mobile 保存到数据库中,新建用户,生成一个 token,返回给 PC 端,展示用户登录成功。

  4. 微信小程序展示用户扫码成功。


实现步骤




  • 需要申请一个微信小程序,用于扫码登录,申请地址:mp.weixin.qq.com/




  • 建表






  • PC 端网站生成二维码



实现效果如下:





  • 微信小程序扫码登录








  • 后端接口实现


路由:app/router.js




  • 生成带唯一 scene 参数的小程序码


app/controller/login.js




收起阅读 »

gradle 实用技巧

前言 总结一些日常开发中非常有用的 gradle 脚本、自定义功能实现。 实现 以下实现基于 AGP 8.0.2 版本 ,AGP 的 API 隔三岔五就会迎来一波破坏性的变更,导致脚本和插件无法使用,因此这里需要关注一下版本。 输出打包后 apk 文件路径及 ...
继续阅读 »

前言


总结一些日常开发中非常有用的 gradle 脚本、自定义功能实现。


实现


以下实现基于 AGP 8.0.2 版本 ,AGP 的 API 隔三岔五就会迎来一波破坏性的变更,导致脚本和插件无法使用,因此这里需要关注一下版本。


输出打包后 apk 文件路径及 apk 大小。


Android Studio 最新版本 Run 之后,每次输出的 apk 并没有在这 app/build/outputs 文件夹下(不知道 Android 官方是出于什么考虑要更改这个路径),而是移动到了 build\intermediates\apk\{flavor}\debug\ 目录下。为了方便后续快速找到每次运行完成的 apk ,可以在每次打包后输出 apk 文件路径及大小,从而可以关注一下日常开发过程中自己
的 apk 体积大概是一个什么样的范围。


static def getFileHumanSize(length) {
def oneMB = 1024f * 1024f
def size = String.valueOf((length / oneMB))
def value = new BigDecimal(size)
return value.setScale(2, BigDecimal.ROUND_HALF_UP)
}
/**
* 打包完成后输出 apk 大小*/
android {
applicationVariants.all { variant ->
variant.assembleProvider.configure() {
it.doLast {
variant.outputs.forEach {
logger.error("apk fileName ==> ${it.outputFile.name}")
logger.error("apk filePath ==> ${it.outputFile}")
logger.error("apk fileSize ==> ${it.outputFile.length()} , ${getFileHumanSize(it.outputFile.length())} MB")
}
}
}
}
}

apk fileName ==> app-huawei-global-debug.apk
apk filePath ==> D:\workspace\MinApp\app\build\intermediates\apk\huaweiGlobal\debug\app-huawei-global-debug.apk
apk fileSize ==> 11987818 , 11.43 MB

可以看到 apk 的路径在 build/intermediates 目录下。当然,我们可以通过下面的方法修改这个路径,定义成我们习惯的路径。


gradle 自定义功能的模块化


日常开发中,会有很多关于 build.gradle 的修改和更新。日积月累,build.gradle 的内容越来越多,代码几乎要爆炸了。其实,可以用模块化的思路将每一个小功能单独抽取出来,这样不仅可以减少 build.gradle 的规模,同时小功能可以更加容易的复用。


比如上面定义的输出打包后 apk 文件路径及 apk 大小的功能,我们就可以把他定义在 report_apk_size_after_package.gradle 这样一个文件中,然后在要使用的 build.gradle 中导入即可。


比如我们要在 app module 中使用这个功能,那么就可以直接在其 build.gradle 文件中按照相对路径引入即可。


gradle_dep.png


apply from: file("../custom-gradle/report_apk_size_after_package.gradle") // 打包完成后输出 apk 大小


修改 release 包的输出路径及文件名


输出 apk 后改名的需求,应该已经很普遍了。在最终输出的 apk 文件中,我们可以追加一些和代码相关的信息,方便通过 apk 文件名迅速确定一些内容。


def getCommit() {
def stdout = new ByteArrayOutputStream()
exec {
commandLine "git"
args "rev-parse", "--short", "HEAD"
standardOutput = stdout
}
return stdout.toString().trim()
}

def getBranch() {
def stdout = new ByteArrayOutputStream()
exec {
commandLine "git"
args "rev-parse", "--abbrev-ref", "HEAD"
standardOutput = stdout
}
return stdout.toString().trim()
}

def gitLastCommitAuthorName() {
return "git log -1 --pretty=format:'%an'".execute(null, rootDir).text.trim().replaceAll("\'", "")
}

def gitLastCommitAuthorEmail() {
return "git log -1 --pretty=format:'%ae'".execute(null, rootDir).text.trim().replaceAll("\'", "")
}


android {
def i = 0
applicationVariants.all { variant ->
if (variant.assembleProvider.name.contains("Debug")) {
// 只对 release 包生效
return
}

// 打包完成后复制到的目录
def outputFileDir = "${rootDir.absolutePath}/build/${variant.buildType.name}/${variant.versionName}"
//确定输出文件名
def today = new Date()
def path = ((project.name != "app") ? project.name : rootProject.name.replace(" ", "")) + "_" + variant.flavorName + "_" + variant.buildType.name + "_" + variant.versionName + "_" + today.format('yyyy_MM_dd_HH_mm') + "_" + getBranch() + "_" + getCommit() + "_" + gitLastCommitAuthorName() + ".apk"
println("path is $path")
variant.outputs.forEach {
it.outputFileName = path
}
// 打包完成后做的一些事,复制apk到指定文件夹
variant.assembleProvider.configure() {
it.doLast {
File out = new File(outputFileDir)
copy {
variant.outputs.forEach { file ->
copy {
from file.outputFile
into out
}
}
}
}
}
}
}

打 release 包后的日志


let me do something after assembleHuaweiGlobalRelease
apk fileName ==> MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk
apk filePath ==> D:\workspace\MinApp\app\build\outputs\apk\huaweiGlobal\release\MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk
apk fileSize ==> 4959230 , 4.73 MB

通过上面的日志,可以看到 MiniApp_huaweiGlobal_release_1.0.0_2306292226_2023_06_29_22_26_master_b0c6937_rookie.apk 包含了 ProjectName、flavor、debug/release、打包时间、分支、commitId 即最后一个 commitor 邮箱这些信息。通过这样的信息,可以更加方便快速的定位问题和解决问题。


妙用 flavor 实现不同的功能


使用 flavor 可以定制代码的不同功能及组合。不用把所有内容一锅乱炖似的放在一起搞。比如 MiniApp 随着演示代码的增多,已经逐渐丧失了 Mini 的定位,Apk 大小已经来到了 22 MB之多。究其原因,就是把所有代码验证和功能都放在一起导致的,音视频、compose、C++ 代码全都混在一起。部分功能不常用,但是每次为了验证一部分小功能,却要连带编译这些所有功能,同时打出的 apk 包体积也变大了,从编译到安装,无形中浪费了很多时间。


因此,可以通过 flavor 将一些不常用的功能,定义到不同的 flavor 中,真正需要的时候,编译相应 flavor 的包即可。


首先我们可以从 type 维度定义两个 flavor


    flavorDimensions "channel", "type"
productFlavors {
xiaomi {
dimension "channel"
}
oppo {
dimension "channel"
}
huawei {
dimension "channel"
}

global {
dimension "type"
}
local {
dimension "type"
}
}

在 type 维度,我们可以认为 global 是功能完整的 flavor,而 local 是部分功能缺失的 flavor 。那么具体缺失哪些功能呢?这就要从实际情况出发了,比如产品定义,代码架构及模块组合之类的。回到 Mini App 中,我们使用不同 flavor 的目标就是通过减少非常用功能模块,获得一个体积相对较小的 apk. 因此,可以做如下配置。


    if (source_code.toBoolean()) {
globalImplementation project(path: ':thirdlib')
} else {
globalImplementation 'com.engineer.third:thirdlib:1.0.0'
}
globalImplementation project(path: ':compose')
globalImplementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer:v8.1.8-release-jitpack'

如上我们只在 global 这个 flavor 依赖 thirdlib, compose, GSYVideoPlayer 这些组件。这样 local flavor 就不会引入这些组件,那么就会带来一个问题,local flavor 编译的时候没有这些组件的类,会出现找不到类的情况。


class_missing.png


对于这种情况,我们可以在项目 src 和 main 同级的目录下,创建 local 文件夹,然后在其内部按照具体 Class 文件的路径创建相应的类即可。


package com.engineer.compose.ui

import com.engineer.BasePlaceHolderActivity

/**
* Created on 2022/7/31.
* @author rookie
*/

class MainComposeActivity : BasePlaceHolderActivity()



package com.engineer.third

import com.engineer.BasePlaceHolderActivity

/**
* Created on 2022/7/31.
* @author rookie
*/

class CppActivity : BasePlaceHolderActivity()

package com.engineer

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.engineer.android.mini.ext.toast

/**
* Created on 2022/8/1.
* @author rookie
*/

open class BasePlaceHolderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
"please use global flavor ".toast()
finish()
}
}

local_flavor.png


这里的思路其实很简单,类似于 leanCanary ,就是在不需要这个功能的 flavor 提供空实现,保证编译可以正常通过即可。缺什么类,建按照类的完整路径创建相应的 Class 文件,这样既保证了编译可以通过,同时在不需要次功能的 flavor 又减少无冗余的代码。


flavor 扩展


其实顺着上面的思路,基于不同 flavor 我们可以做更多的事情。基于 Java 虚拟机的类加载机制限制,相同的类只能有一个,因此我们无法做的事情是,通过 flavor 创建同名的类,去覆盖或重写其他 flavor 的逻辑,这种在编译阶段(其实是在创建同名类的阶段)就会报错。


但是,有些功能是可以被覆盖和定制的。比如包名、App 的名称、icon 之类。这些配置可以通过 AndroidManifest.xml/gradle 进行配置。可以在 local 目录下创建这个 flavor 特有的一些资源文件,这样就可以实现基于 flavor 的产品功能定制了。


比如最简单的修改 applicationId


        global {
dimension "type"
}
local {
dimension "type"
applicationId "com.engineer.android.mini.x"
}

这样,local 和 global 就有了各自不同的 applicationId, 这两种不同 flavor 的包就可以安装在同一台设备了。当然,现在这两个包的 label 和 icon 都是一样的,完全看不出区别。这里就可以利用 flavor 各自的文件夹,来定制各类资源和命名了。


flavor 过滤


不同维度的 flavor 会导致最终的 variant 变多。比如定义 product、channel、type 这些几个 dimension 的之后,后续新增的 flavor 会以乘积的形式增长,但是有些 flavor 又是我们不需要的,这个时候我们就可以过滤掉某些不需要的 flavor 。


比如以上面定义的 channel,type 这两个维度为例,在这两个维度下分别又扩展了 xiaomi/opop/huawei,global/local 这些 flavor 。按照规则会有 2x3x2=12 种 flavor,但实际情况可能不需要这么多,为了减少编译的压力,提升代码的可维护性,我们可以对 flavor 进行过滤。


    variantFilter { variant ->
println "variant is ${variant.flavors*.name}"
def dimens = variant.flavors*.name
def type = dimens[1]
def channel = dimens[0]
switch (type) {
case "global":
if (channel == "xiaomi") {
setIgnore(true)
}
break
case "local":
if (channel == "oppo") {
setIgnore(true)
}
break
}
}

这样我们就成功的过滤掉了 xiaomiGlobal 和 oppoLocal 的 flavor ,一下子就去掉了 4 个 flavor 。


基于现有 task 定制任务


再回顾一下上面的 修改 release 包的输出路径及文件名 的代码实现,我们是在打包完成之后进行了 apk 文件的重命名。


        // 打包完成后做的一些事,复制apk到指定文件夹
variant.assembleProvider.configure() {
it.doLast {
File out = new File(outputFileDir)
copy {
variant.outputs.forEach { file ->
copy {
from file.outputFile
into out
}
}
}
}
}

这里的 doLast 就是说,无论是否需要,每次都会在 assemble 这个 task 完成之后做一件事。这样在某些情况下显得非常的不灵活,尤其是当 doLast 闭包中要做的事情非常繁重的时候。这里的 copy 操作显然是比较轻量的,但是换做是其他操作,比如 apk 安全加固等操作,并不是每次必然需要的操作。这种情况下,就需要我们换一种方式去实现相应的逻辑了。


我们就以加固为例,一般情况下,我们需要对各个版本的 release 包进行加固。因此,我们可以基于现有的 assembleXXXRelease 这个 task 展开。


android {
applicationVariants.all { variant ->
if (variant.assemble.name.contains("Debug")) {
// 只对 release 包生效
return
}

def taskPrefix = "jiagu"
def groupName = "jiagu"
def assembleTask = variant.assembleProvider.name
def taskName = assembleTask.replace("assemble", taskPrefix)
tasks.create(taskName) {
it.group groupName
it.dependsOn assembleTask
variant.assembleProvider.configure() {
it.doLast {
logger.error("let me do something after $assembleTask")
}
}
}
}
}

添加上面的代码后,再执行一下 gradle sync ,我们就可以看到新添加的 jiagu 这个 group 和其中的 task 了。


jiagu.png


这里使用创建 task 的一种方式,使用 createdependsOn ,动态创建 task,并指定其依赖的 task 。


这样当我们执行 ./gradlew jiaguHuaweiLocalRelease 时就可以看到结果了。


> Task :app:assembleHuaweiLocalRelease
let me do something after assembleHuaweiLocalRelease

>
Task :app:jiaguHuaweiLocalRelease

BUILD SUCCESSFUL in 56s
82 actionable tasks: 12 executed, 70 up-to-date

可以看到我们自定义的 task 已经生效了,会在 assembleXXXRelease 这个 task 完成之后执行。


关于 gradle 的使用,可以说是孰能生巧,只要逐渐熟悉了 groovy 的语法和 Java 语法之间的差异,那么就可以逐渐摸索出更多有意思的用法了。


本文源码可以参考 Github MiniApp


小结


可以看到基于 gradle 构建流程,我们仅仅通过编写一些脚本,可以做的事情还是很多的。但是由于 groovy 语法过于灵活,不像 Java 那样有语法提示,因此尝试一些新的语法时难免不知所措。面对这种情况,去看他的源码就好。通过源码,我们就可以知道某个类有哪

作者:IAM四十二
来源:juejin.cn/post/7250071693543145529
些属性,有哪些方法。

收起阅读 »

手撸一个私信功能

web
前言 几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了, 这篇文章就直接粘了之前的代码简单的改了改,说明一下问题; 主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等; 也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题...
继续阅读 »

前言


几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了,

这篇文章就直接粘了之前的代码简单的改了改,说明一下问题;

主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等;

也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题了;


效果


界面大概是这样的
image.png


整体动态效果是这样的


test 00_00_00-00_00_30~1.gif


test1 00_00_00-00_00_30.gif


说下大致思路吧


首先是把界面分成左边和右边,左边占少一部分,是朋友目录界面;

右边占多一点,右边是聊天的详情界面;

点击左边对应的那个人,右边就会出现本人跟点击的那个人的聊天详情;


左边人员目录的思路


左边的人员目录和显示的头像,最新的一条消息还有时间,这些都是后端返给前端的;

前端把数据展现出来就行,

时间那里可以根据公司需求以及后端返回的格式转成前天,刚刚等根据需求而定;

我这块时间项目中是有分开前天,昨天,刚刚的,

只不过这里就自己造的数据时间随便写的;

当然这里数据多的时候,可做成虚拟滚动效果;
每个人头像那个红色是消息数量,当读完消息时,就恢复成剩下的消息数量;


右边聊天详情的思路


右边是左边点击对应的聊天人员时,

拿这个人的id之类的数据去请求后端,拿对应的聊天详情数据;

最下面的显示的是最新的聊天信息,后端给的排序不对,可自己反转去排序;

这里也做成虚拟滚动;

最上面显示的那个名称是当前和谁聊天的那个人的昵称;


image.png


聊天界面里也显示的是时间,昵称,头像,聊天信息内容,

时间也需要分昨天,前天,刚刚等。。。


发送消息的思路


我这里也做了按键和点击按钮两种方式;

按键就是在代码里添加一个键盘的监听事件就可;


    var footerTarget = document.getElementById('footer');
footerTarget.addEventListener('keydown', this.footerKeydown);

Enter按键是13;



//底部keydown监听事件
footerKeydown = (e) => {
if (e?.keyCode === 13) {
this.handleSubmit();
}
};

发送消息界面其实就是个表单,做成那个样子就可以啦;

发送消息时,调用后端接口,把这条消息添加在消息数据后面就可;


结尾


只是简单写下思路就已经写这么多了;

代码后面有空给粘上;

由于我是临时把几年前的代码拿出来粘的,

为了显示效果,数据也是自己造的,

一些时间呀以及显示,已读信息的数量呀以及其他一些细节都没有管,

实际项目中直接对应接口嘛,

所以这里就只是随便

作者:浅唱_那一缕阳光
来源:juejin.cn/post/7250029035744149541
改改说明一下问题哈;

收起阅读 »

什么是布隆过滤器?在php里你怎么用?

布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否属于某个集合的概率型数据结构。它基于哈希函数和位数组实现,可以高效地检索一个元素是否存在,但不提供元素具体的存储和获取功能。 布隆过滤器原理 上面的思路其实就是布隆过滤器的思想,只不过因为 ...
继续阅读 »

布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否属于某个集合的概率型数据结构。它基于哈希函数和位数组实现,可以高效地检索一个元素是否存在,但不提供元素具体的存储和获取功能。


image.png


布隆过滤器原理


上面的思路其实就是布隆过滤器的思想,只不过因为 hash 函数的限制,多个字符串很可能会 hash 成一个值。为了解决这个问题,布隆过滤器引入多个 hash 函数来降低误判率。


下图表示有三个 hash 函数,比如一个集合中有 x,y,z 三个元素,分别用三个 hash 函数映射到二进制序列的某些位上,假设我们判断 w 是否在集合中,同样用三个 hash 函数来映射,结果发现取得的结果不全为 1,则表示 w 不在集合里面。


image.png


布隆过滤器处理流程


布隆过滤器应用很广泛,比如垃圾邮件过滤,爬虫的 url 过滤,防止缓存击穿等等。下面就来说说布隆过滤器的一个完整流程,相信读者看到这里应该能明白布隆过滤器是怎样工作的。


第一步:开辟空间


开辟一个长度为 m 的位数组(或者称二进制向量),这个不同的语言有不同的实现方式,甚至你可以用文件来实现。


第二步:寻找 hash 函数


获取几个 hash 函数,前辈们已经发明了很多运行良好的 hash 函数,比如 BKDRHash,JSHash,RSHash 等等。这些 hash 函数我们直接获取就可以了。


第三步:写入数据


将所需要判断的内容经过这些 hash 函数计算,得到几个值,比如用 3 个 hash 函数,得到值分别是 1000,2000,3000。之后设置 m 位数组的第 1000,2000,3000 位的值位二进制 1。


第四步:判断


接下来就可以判断一个新的内容是不是在我们的集合中。判断的流程和写入的流程是一致的。


在PHP中如何使用?


在PHP中,可以使用BloomFilter扩展库或自行实现布隆过滤器。下面我将介绍两种方法。


1. 使用BloomFilter扩展库:


PHP中有一些第三方扩展库提供了布隆过滤器的功能。其中比较常用的是phpbloomd扩展,它提供了对布隆过滤器的支持。你可以按照该扩展库的文档进行安装和使用。


示例代码如下:


// 创建一个布隆过滤器
$filter = new BloomFilter();

// 向过滤器添加元素
$filter->add("element1");
$filter->add("element2");
$filter->add("element3");

// 检查元素是否存在于过滤器中
if ($filter->has("element1")) {
echo "Element 1 may exist.";
} else {
echo "Element 1 does not exist.";
}


2. 自行实现布隆过滤器:


如果你不想使用第三方扩展库,也可以自行实现布隆过滤器。下面是一个简单的自实现布隆过滤器的示例代码:


class BloomFilter {
private $bitArray;
private $hashFunctions;

public function __construct($size, $numHashFunctions) {
$this->bitArray = array_fill(0, $size, false);
$this->hashFunctions = $numHashFunctions;
}

private function hash($value) {
$hashes = [];
$hash1 = crc32($value);
$hash2 = fnv1a32($value);

for ($i = 0; $i < $this->hashFunctions; $i++) {
$hashes[] = ($hash1 + $i * $hash2) % count($this->bitArray);
}

return $hashes;
}

public function add($value) {
$hashes = $this->hash($value);

foreach ($hashes as $hash) {
$this->bitArray[$hash] = true;
}
}

public function has($value) {
$hashes = $this->hash($value);

foreach ($hashes as $hash) {
if (!$this->bitArray[$hash]) {
return false;
}
}

return true;
}
}

// 创建一个布隆过滤器
$filter = new BloomFilter(100, 3);

// 向过滤器添加元素
$filter->add("element1");
$filter->add("element2");
$filter->add("element3");

// 检查元素是否存在于过滤器中
if ($filter->has("element1")) {
echo "Element 1 may exist.";
} else {
echo "Element 1 does not exist.";
}


无论是使用扩展库还是自行实现,布隆过滤器在处理大规模数据集合时可以提供高效的元素存在性检查功能,适用于需要快速判断元素是否属于某个集合的场景。


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

我工作中用到的性能优化全面指南

web
在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。 最小化和压缩代码 在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除...
继续阅读 »

在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。



最小化和压缩代码


在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除不必要的空格、换行、注释,以及缩短变量和函数名。工具如UglifyJS和Terser等可以帮助我们完成这个任务。


// 原始代码
function hello(name) {
let message = 'Hello, ' + name;
console.log(message);
}

// 压缩后的代码
function hello(n){var e='Hello, '+n;console.log(e)}

利用浏览器缓存


浏览器缓存是提升Web应用性能的一个重要手段。我们可以将一些经常用到的、变化不大的数据存储在本地,以减少对服务器的请求。例如,可以使用localStorage或sessionStorage来存储这些数据。


// 存储数据
localStorage.setItem('name', 'John');

// 获取数据
var name = localStorage.getItem('name');

// 移除数据
localStorage.removeItem('name');

// 清空所有数据
localStorage.clear();

避免过度使用全局变量


全局变量会占用更多的内存,并且容易导致命名冲突,从而降低程序的运行效率。我们应尽量减少全局变量的使用。


// 不好的写法
var name = 'John';

function greet() {
console.log('Hello, ' + name);
}

// 好的写法
function greet(name) {
console.log('Hello, ' + name);
}

greet('John');

使用事件委托减少事件处理器的数量


事件委托是将事件监听器添加到父元素,而不是每个子元素,以此来减少事件处理器的数量,并且提升性能。


document.getElementById('parent').addEventListener('click', function (event) {
if (event.target.classList.contains('child')) {
// 处理点击事件...
}
});

好的,下面我会详细解释一下这些概念以及相关的示例:


async 和 defer


asyncdefer 是用于控制 JavaScript 脚本加载和执行的 HTML 属性。



  • async 使浏览器在下载脚本的同时,继续解析 HTML。一旦脚本下载完毕,浏览器将中断 HTML 解析,执行脚本,然后继续解析 HTML。


<script async src="script.js"></script>


  • defer 也使浏览器在下载脚本的同时,继续解析 HTML。但是,脚本的执行会等到 HTML 解析完毕后再进行。


<script defer src="script.js"></script>

在需要控制脚本加载和执行的时机以优化性能的场景中,这两个属性是非常有用的。


防抖和节流


throttle(节流)和 debounce(防抖)。



  • throttle 保证函数在一定时间内只被执行一次。例如,一个常见的使用场景是滚动事件的监听函数:


function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
return func(...args);
};
}

window.addEventListener('scroll', throttle(() => console.log('Scrolling'), 100));


  • debounce 保证在一定时间内无新的触发后再执行函数。例如,实时搜索输入的监听函数:


function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}

searchInput.addEventListener('input', debounce(() => console.log('Input'), 300));

利用虚拟DOM和Diff算法进行高效的DOM更新


当我们频繁地更新DOM时,可能会导致浏览器不断地进行重绘和回流,从而降低程序的性能。因此,我们可以使用虚拟DOM和Diff算法来进行高效的DOM更新。例如,React和Vue等框架就使用了这种技术。


// React示例
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

ReactDOM.render(<Hello name="John" />, document.getElementById('root'));

避免长时间运行的任务


浏览器单线程的运行方式决定了JavaScript长时间运行的任务可能会阻塞UI渲染和用户交互,从而影响性能。对于这类任务,可以考虑将其分解为一系列较小的任务,并在空闲时执行,这就是“分片”或者“时间切片”的策略。


function chunk(taskList, iteration, context) {
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && taskList.length > 0) {
iteration.call(context, taskList.shift());
}

if (taskList.length > 0) {
chunk(taskList, iteration, context);
}
});
}

chunk(longTasks, (task) => {
task.execute();
}, this);

虚拟列表(Virtual List)


当我们在页面上渲染大量的元素时,这可能会导致明显的性能问题。虚拟列表是一种技术,可以通过只渲染当前可见的元素,来优化这种情况。



虚拟列表的等高方式实现:



// 列表项高度
const ITEM_HEIGHT = 20;

class VirtualList {
constructor(container, items, renderItem) {
this.container = container;
this.items = items;
this.renderItem = renderItem;

this.startIndex = 0;
this.endIndex = 0;
this.visibleItems = [];

this.update();

this.container.addEventListener('scroll', () => this.update());
}

update() {
const viewportHeight = this.container.clientHeight;
const scrollY = this.container.scrollTop;

this.startIndex = Math.floor(scrollY / ITEM_HEIGHT);
this.endIndex = Math.min(
this.startIndex + Math.ceil(viewportHeight / ITEM_HEIGHT),
this.items.length
);

this.render();
}

render() {
// 移除所有的可见元素
this.visibleItems.forEach((item) => this.container.removeChild(item));
this.visibleItems = [];

// 渲染新的可见元素
for (let i = this.startIndex; i < this.endIndex; i++) {
const item = this.renderItem(this.items[i]);
item.style.position = 'absolute';
item.style.top = `${i * ITEM_HEIGHT}px`;
this.visibleItems.push(item);
this.container.appendChild(item);
}
}
}

// 使用虚拟列表
new VirtualList(
document.getElementById('list'),
Array.from({ length: 10000 }, (_, i) => `Item ${i}`),
(item) => {
const div = document.createElement('div');
div.textContent = item;
return div;
}
);


优化循环


在处理大量数据时,循环的效率是非常重要的。我们可以通过一些方法来优化循环,例如:避免在循环中进行不必要的计算,使用倒序循环,使用forEach或map等函数。


// 不好的写法
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

// 好的写法
let length = arr.length;
for (let i = 0; i < length; i++) {
console.log(arr[i]);
}

// 更好的写法
arr.forEach(function (item) {
console.log(item);
});

避免阻塞UI


JavaScript的运行是阻塞UI的,当我们在进行一些耗时的操作时,应尽量使用setTimeout或Promise等异步方法,以避免阻塞UI。


setTimeout(function () {
// 执行耗时的操作...
}, 0);

使用合适的数据结构和算法


使用合适的数据结构和算法是优化程序性能的基础。例如,当我们需要查找数据时,可以使用对象或Map,而不是数组;当我们需要频繁地添加或移除数据时,可以使用链表,而不是数组。


// 使用对象进行查找
var obj = { 'John': 1, 'Emma': 2, 'Tom': 3 };
console.log(obj['John']);

// 使用Map进行查找
var map = new Map();
map.set('John', 1);
map.set('Emma', 2);
map.set('Tom', 3);
console.log(map.get('John'));

避免不必要的闭包


虽然闭包在某些情况下很有用,但是它们也会增加额外的内存消耗,因此我们应该避免不必要的闭包。


// 不必要的闭包
function createFunction() {
var name = 'John';
return function () {
return name;
}
}

// 更好的方式
function createFunction() {
var name = 'John';
return name;
}

避免使用with语句


with语句会改变代码的作用域,这可能会导致性能问题,因此我们应该避免使用它。


// 不好的写法
with (document.getElementById('myDiv').style) {
color = 'red';
backgroundColor = 'black';
}

// 好的写法
var style = document.getElementById('myDiv').style;
style.color = 'red';
style.backgroundColor = 'black';

避免在for-in循环中使用hasOwnProperty


hasOwnProperty方法会查询对象的整个原型链,这可能会影响性能。在for-in循环中,我们应该直接访问对象的属性。


// 不好的写法
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key + ': ' + obj[key]);
}
}

// 好的写法
for (var key in obj) {
console.log(key + ': ' + obj[key]);
}

使用位操作进行整数运算


在进行整数运算时,我们可以使用位操作符,它比传统的算术运算符更快。


// 不好的写法
var half = n / 2;

// 好的写法
var half = n >> 1;

避免在循环中创建函数


在循环中创建函数会导致性能问题,因为每次迭代都会创建一个新的函数实例。我们应该在循环外部创建函数。


// 不好的写法
for (var i = 0; i < 10; i++) {
arr[i] = function () {
return i;
}
}

// 好的写法
function createFunction(i) {
return function () {
return i;
}
}

for (var i = 0; i < 10; i++) {
arr[i] = createFunction(i);
}

使用Web Worker进行多线程处理


JavaScript默认是单线程运行的,但我们可以使用Web Worker来进行多线程处理,以提升程序的运行效率。


// 主线程
var worker = new Worker('worker.js');

worker.onmessage = function (event) {
console.log('Received message ' + event.data);
}

worker.postMessage('Hello Worker');

// worker.js
self.onmessage = function(event) {
console.log('Received message ' + event.data);
self.postMessage('You said: ' + event.data);
};

使用WebAssembly进行性能关键部分的开发


WebAssembly是一种新的编程语言,它的代码运行速度接近原生代码,非常适合于进行性能关键部分的开发。例如,我们可以用WebAssembly来开发图形渲染、物理模拟等复杂任务。


// 加载WebAssembly模块
WebAssembly.instantiateStreaming(fetch('module.wasm'))
.then(result => {
// 调用WebAssembly函数
result.instance.exports.myFunction();
});

使用内存池来管理对象


当我们频繁地创建和销毁对象时,可以使用内存池来管理这些对象,以避免频繁地进行内存分配和垃圾回收,从而提升性能。


class MemoryPool {
constructor(createObject, resetObject) {
this.createObject = createObject;
this.resetObject = resetObject;
this.pool = [];
}

acquire() {
return this.pool.length > 0 ? this.resetObject(this.pool.pop()) : this.createObject();
}

release(obj) {
this.pool.push(obj);
}
}

var pool = new MemoryPool(
() => { return {}; },
obj => { for (var key in obj) { delete obj[key]; } return obj; }
);

使用双缓冲技术进行绘图


当我们需要进行频繁的绘图操作时,可以使用双缓冲技术,即先在离屏画布上进行绘图,然后一次性将离屏画布的内容复制到屏幕上,这样可以避免屏幕闪烁,并且提升绘图性能。


var offscreenCanvas = document.createElement('canvas');
var offscreenContext = offscreenCanvas.getContext('2d');

// 在离屏画布上进行绘图...
offscreenContext.fillRect(0, 0, 100, 100);

// 将离屏画布的内容复制到屏幕上
context.drawImage(offscreenCanvas, 0, 0);

使用WebGL进行3D渲染


WebGL是一种用于进行3D渲染的Web标准,它提供了底层的图形API,并且能够利用GPU进行加速,非常适合于进行复杂的3D渲染。


var canvas = document.getElementById('myCanvas');
var gl = canvas.getContext('webgl');

// 设置清空颜色缓冲区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

使用Service Workers进行资源缓存


Service Workers可以让你控制网页的缓存策略,进一步减少HTTP请求,提升网页的加载速度。例如,你可以将一些不常变化的资源文件预先缓存起来。


// 注册一个service worker
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(error) {
console.log('ServiceWorker registration failed: ', error);
});

// service-worker.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll([
'/style.css',
'/script.js',
// 更多资源...
]);
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

使用内容分发网络(CDN)


你可以将静态资源(如JavaScript、CSS、图片等)上传到CDN,这样用户可以从离他们最近的服务器下载资源,从而提高下载速度。


<!-- 从CDN加载jQuery库 -->
<script src="https://cdn.example.com/jquery.min.js"></script>

使用HTTP/2进行资源加载


HTTP/2支持头部压缩和多路复用,可以更高效地加载资源。如果你的服务器和用户的浏览器都支持HTTP/2,那么你可以使用它来提高性能。


// 假设我们有一个HTTP/2库
var client = new Http2Client('https://example.com');

client.get('/resource1');
client.get('/resource2');

使用Web Socket进行数据通信


如果你需要频繁地与服务器进行数据交换,可以使用Web Socket,它比HTTP有更低的开销。


var socket = new WebSocket('ws://example.com/socket');

socket.addEventListener('open', function() {
socket.send('Hello, server');
});

socket.addEventListener('message', function(event) {
console.log('Received message from server: ' + event.data);
});

使用Progressive Web Apps(PWA)技术


PWA可以让你的网站在离线时仍然可用,并且可以被添加到用户的主屏幕,提供类似于原生应用的体验。PWA需要使用Service Workers和Manifest等技术。


// 注册Service Worker
navigator.serviceWorker.register('/service-worker.js');

// 检测是否支持Manifest
if ('manifest' in document.createElement('link')) {
var link = document.createElement('link');
link.rel = 'manifest';
link.href = '/manifest.json';
document.head.appendChild(link);
}

使用WebRTC进行实时通信


WebRTC是一种提供实时通信(RTC)能力的技术,允许数据直接在浏览器之间传输,对于需要实时交互的应用,如视频聊天、实时游戏等,可以使用WebRTC来提高性能。


var pc = new RTCPeerConnection();

// 发送offer
pc.createOffer().then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
// 发送offer给其他浏览器...
});

// 收到answer
pc.setRemoteDescription(answer);

使用IndexedDB存储大量数据


如果你需要在客户端存储大量数据,可以使用IndexedDB。与localStorage相比,IndexedDB可以存储更大量的数据,并且支持事务和索引。


var db;
var request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = function(event) {
db = event.target.result;
var store = db.createObjectStore('myStore', { keyPath: 'id' });
store.createIndex('nameIndex', 'name');
};
request.onsuccess = function(event) {
db = event.target.result;
};
request.onerror = function(event) {
// 错误处理...
};

使用Web Push进行后台消息推送


Web Push允许服务器在后台向浏览器推送消息,即使网页已经关闭。这需要在Service Worker中使用Push API和Notification API。


// 请求推送通知的权限
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('Push notification permission granted');
}
});

// 订阅推送服务
navigator.serviceWorker.ready.then(function(registration) {
registration.pushManager.subscribe({ userVisibleOnly: true }).then(function(subscription) {
console.log('Push subscription: ', subscription);
});
});

// 在Service Worker中接收和显示推送通知
self.addEventListener('push', function(event) {
var data = event.data.json();
self.registration.showNotification(data.title, data);
});

通过服务器端渲染(SSR)改善首次页面加载性能


服务器端渲染意味着在服务器上生成HTML,然后将其发送到客户端。这可以加快首次页面加载速度,因为用户可以直接看到渲染好的页面,而不必等待JavaScript下载并执行。这对于性能要求很高的应用来说,是一种有效的优化手段。


// 服务器端
app.get('/', function(req, res) {
const html = ReactDOMServer.renderToString(<MyApp />);
res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});

利用HTTP3/QUIC协议进行资源传输


HTTP3/QUIC协议是HTTP/2的后续版本,采用了全新的底层传输协议(即QUIC),以解决HTTP/2中存在的队头阻塞(Head-of-line Blocking)问题,从而进一步提高传输性能。如果你的服务器和用户的浏览器都支持HTTP3/QUIC,那么可以考虑使用它进行资源传输。


使用Service Worker与Background Sync实现离线体验


通过Service Worker,我们可以将网络请求与页面渲染解耦,从而实现离线体验。并且,结合Background Sync,我们可以在用户离线时提交表单或同步数据,并在用户重新联网时自动重试。


// 注册Service Worker
navigator.serviceWorker.register('/sw.js');

// 提交表单
fetch('/api/submit', {
method: 'POST',
body: new FormData(form)
}).catch(() => {
// 如果请求失败,使用Background Sync重试
navigator.serviceWorker.ready.then(reg => {
return reg.sync.register('sync-submit');
});
});

// 在Service Worker中监听sync事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-submit') {
event.waitUntil(submitForm());
}
});

使用PostMessage进行跨文档通信


如果你的应用涉及到多个窗口或者iframe,你可能需要在他们之间进行通信。使用postMessage方法可以进行跨文档通信,而不用担心同源策略的问题。


// 父窗口向子iframe发送消息
iframeElement.contentWindow.postMessage('Hello, child', 'https://child.example.com');

// 子iframe接收消息
window.addEventListener('message', function(event) {
if (event.origin !== 'https://parent.example.com') return;
console.log('Received message: ' + event.data);
});

使用Intersection Observer进行懒加载


Intersection Observer API可以让你知道一个元素何时进入或离开视口,这对于实现图片或者其他资源的懒加载来说非常有用。


var images = document.querySelectorAll('img.lazy');

var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
var img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});

images.forEach(img => {
observer.observe(img);
});

利用OffscreenCanvas进行后台渲染


OffscreenCanvas API使得开发者可以在Web Worker线程中进行Canvas渲染,这可以提高渲染性能,尤其是在进行大量或者复杂的Canvas操作时。


var offscreen = new OffscreenCanvas(256, 256);
var ctx = offscreen.getContext('2d');

// 在后台线程中进行渲染...

利用Broadcast Channel进行跨标签页通信


Broadcast Channel API提供了一种在同源的不同浏览器上下文之间进行通信的方法,这对于需要在多个标签页之间同步数据的应用来说非常有用。


var channel = new BroadcastChannel('my_channel');

// 发送消息
channel.postMessage('Hello, other tabs');

// 接收消息
channel.onmessage = function(event) {
console.log('Received message: ' + event.data);
};

使用Web Cryptography API进行安全操作


Web Cryptography API 提供了一组底层的加密API,使得开发者可以在Web环境中进行安全的密码学操作,例如哈希、签名、加密、解密等。


window.crypto.subtle.digest('SHA-256', new TextEncoder().encode('Hello, world')).then(function(hash) {
console.log(new Uint8Array(hash));
});

使用Blob对象进行大型数据操作


Blob对象代表了一段二进制数据,可以用来处理大量的数据,比如文件。它们可以直接从服务端获取,或者由客户端生成,这对于处理大型数据或者二进制数据很有用。


var fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function(event) {
var contents = event.target.result;
processContents(contents);
};
reader.readAsArrayBuffer(file);
});

使用Page Visibility API进行页面可见性调整


Page Visibility API提供了一种方式来判断页面是否对用户可见。利用这个API,你可以在页面不可见时停止或减慢某些操作,例如动画或视频,从而节省CPU和电池使用。


document.addEventListener('visibilitychange', function() {
if (document.hidden) {
pauseAnimation();
} else {
resumeAnimation();
}
});

使用WeakMap和WeakSet进行高效的内存管理


在处理大量数据时,如果不小心可能会产生内存泄漏。WeakMap和WeakSet可以用来保存对对象的引用,而不会阻止这些对象被垃圾回收。这在一些特定的应用场景中,例如缓存、记录对象状态等,可能非常有用。


let cache = new WeakMap();

function process(obj) {
if (!cache.has(obj)) {
let result = /* 对obj进行一些复杂的处理... */
cache.set(obj, result);
}

return cache.get(obj);
}

使用requestAnimationFrame进行动画处理


requestAnimationFrame能够让浏览器在下一次重绘之前调用指定的函数进行更新动画,这样可以保证动画的流畅性,并且减少CPU的使用。


function animate() {


// 更新动画...
requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

使用CSS3动画替代JavaScript动画


CSS3动画不仅可以提供更好的性能,还可以在主线程之外运行,从而避免阻塞UI。因此,我们应该尽可能地使用CSS3动画替代JavaScript动画。


@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.myDiv {
animation: fadeIn 2s ease

-in-out;
}

避免回流和重绘


回流和重绘是浏览器渲染过程中的两个步骤,它们对性能影响很大。优化的关键在于尽可能减少触发回流和重绘的操作,例如一次性修改样式,避免布局抖动等。


var el = document.getElementById('my-el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 尽量避免上面的写法,以下为优化后的写法
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

使用CSS3硬件加速提高渲染性能


使用 CSS3 的 transform 属性做动画效果,可以触发硬件加速,从而提高渲染性能。


element.style.transform = 'translate3d(0, 0, 0)';

避免使用同步布局


同步布局(或强制布局)是指浏览器强制在 DOM 修改和计算样式之后,立即进行布局。这会中断浏览器的优化过程,导致性能下降。一般出现在连续的样式修改和读取操作之间。


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 读样式,导致同步布局
let width = div.offsetWidth;
// 再写样式
div.style.height = width + 'px'; // 强制布局

为避免这个问题,可以将读操作移到所有写操作之后:


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 写样式
div.style.height = '100px';

// 读样式
let width = div.offsetWidth;

使用ArrayBuffer处理二进制数据


ArrayBuffer 提供了一种处理二进制数据的高效方式,例如图像,声音等。


var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}

利用ImageBitmap提高图像处理性能


ImageBitmap对象提供了一种在图像处理中避免内存拷贝的方法,可以提高图像处理的性能。


var img = new Image();
img.src = 'image.jpg';
img.onload = function() {
createImageBitmap(img).then(function(imageBitmap) {
// 在这里使用 imageBitmap
});
};
作者:linwu
来源:juejin.cn/post/7249991926307864613

收起阅读 »

我看UI小姐姐就是在为难我这个切图仔

web
前言 改成这个样子 咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢. 实现过程 刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的bord...
继续阅读 »

前言



image.png


改成这个样子


image.png


咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢.


image.png


实现过程


刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的border干掉,给外面的组件加上border不就搞定了,看起来也不是很复杂的样子.第一版长这样


image.png


发现问题了嘛,select下拉选项的宽度和表单元素不一样长,当然我觉得问题不大能用就行,但是在ui眼里那可不行,必须要一样长,不然不好看.
好吧,在我的据理力争下,我妥协啦,开始研究下一版.


image.png


在第一版的基础上我发现只有Select有这个问题,那就好办了,针对它单独处理就行了,解决方法思考了3种:



  • 第一种就是antd的Select可以设置dropdownStyle,通过获取父元素的宽度来设置下拉菜单的宽度,以此达到等长的目的

  • 第二种就是通过设置label元素为绝对定位,同时设置Select的paddingLeft

  • 还有一种就是通过在Select里添加css伪元素(注意这种方法需要把content里的中文转成unicode编码,不然可能会乱码)


最终我采用的是第二种方法,具体代码如下


import React, { CSSProperties, PropsWithChildren, useMemo } from 'react';
import { Form, FormItemProps, Col } from 'antd';
import styles from './index.module.less';

interface IProps extends FormItemProps {
label?: string;
style?: CSSProperties;
className?: string;
isSelect?: boolean;
noMargin?: boolean;
col?: number;
}
export const WrapFormComponent = ({ children, className, isSelect, style, col, noMargin = true, ...props }: PropsWithChildren<IProps>) => {
const labelWidth = useMemo(() => {
if (!isSelect || !props.label) return 11;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context!.font = '12px PingFang SC';
const metrics = context!.measureText(props.label);
return metrics.width + (props.colon === undefined || props.colon ? 10 : 0) + 11;
}, [isSelect, props.label, props.colon]);
return (
<Col span={col}>
<Form.Item
style={{ '--label-length': labelWidth + 'px', marginBottom: noMargin ? 0 : '16px', ...style } as CSSProperties}
className={`${styles['wrap-form']} ${isSelect ? styles['wrap-form-select'] : ''} ${className || ''}`}
{...props}
>

{children}
</Form.Item>
</Col>

);
};


less代码


.wrap-form {
padding: 0 !important;
padding-left: 11px !important;
border: 1px solid #c1c7cd;
border-radius: 4px;

:global {
.ant-form-item-label {
display: inline-flex !important;
align-items: center !important;
}

.ant-form-item-label > label {
height: auto;
color: #919399;
font-weight: 400;
}

.ant-picker {
width: 100%;
}

.ant-input,
.ant-select-selector,
.ant-picker,
.ant-input-number {
border: none;
border-color: transparent !important;
}

.ant-input-affix-wrapper {
background: none;
border: none;
}
}
}

.wrap-form-select {
position: relative;
padding: 0 !important;

:global {
.ant-form-item-label {
position: absolute;
top: 50%;
left: 11px;
z-index: 1;
text-align: left;
transform: translateY(-50%);
}

.ant-select-selector {
padding-left: var(--label-length) !important;
}

.ant-select-selection-search {
left: var(--label-length) !important;
}

.ant-select-multiple .ant-select-selection-search {
top: -2px;
left: 0 !important;
margin-left: 0 !important;
}

.ant-select-multiple .ant-select-selection-placeholder {
left: var(--label-length) !important;
height: 28px;
line-height: 28px;
}
}
}

最后就变成这样了,完美解决,这下ui总不能挑刺儿了吧.


image.png

收起阅读 »

这几个自学前端/编程的苦,你千万不要吃

今天我给你们分享一些前端自学路上,你会吃的苦,以及如何避免和解决。 这些让你吃苦的地方,是造成你前端学不好、并且学不下去的根本原因。 首先是, 一、学习的苦 一般建议你自学的人,会告诉你,自己到B站找个系统课,然后跟着学就行了。 这样跟你说的人,不是蠢就是坏。...
继续阅读 »

今天我给你们分享一些前端自学路上,你会吃的苦,以及如何避免和解决。


这些让你吃苦的地方,是造成你前端学不好、并且学不下去的根本原因。


首先是,


一、学习的苦


一般建议你自学的人,会告诉你,自己到B站找个系统课,然后跟着学就行了。


这样跟你说的人,不是蠢就是坏。


你这样做的结果,只会导致,你要学的内容非常多,并且又臭又长。


有很多人看到这么多要学的,第一想法就是放弃。


而一些有想法的呢,愿意逼着自己啃下去,大部分的结果是,看了好几十集就看不下去了。


我有学生就是这样,整个大一下学期,一直在B站看某个机构的前端课,看了八十多集就看不下去了,也不知道学到哪里才有成效。


我过去也是这样学的,因为没办法。


那时候的我,不知道怎么样的方式才是更好、更有效的,我只能逼自己把所有的先看一遍。


很多自学上岸的人,也都是这样做的,所以他们就算给你建议,也只会给你这一个。


我不清楚你们的学习效果如何啊,就我这么多年看到的情况来说,我见过非常多的人,


这里面有自学的,有培训出来的,但是哪怕他们工作了四五年,甚至有些七八年,依旧没有学明白。


什么叫没学明白?我给你举个例子。


HTML、CSS这两个东西,你去网上找,很多人告诉你要学一个月,实际上,这部分我总共就花了 10 分钟。


并且你学了这么久,让你自己去写东西,很多人依旧没思路,但是我这十分钟掌握的内容,我至今用了快十年,就没有搞不定的页面。


你学得时候理解困难,学完了又做不出东西,自然你就觉得学前端是一件很难的事情,你在这上面吃的苦越多,你越抗拒做这件事情。你越抗拒,自然就越做不好。


这部分如果继续展开讲,又有许多内容,我们留到下次再说。


接着我们来看你会吃的第二个苦,


二、拿不到结果的苦


学编程已经是最容易拿到结果的一个了,前端更是如此。


在计算机的世界里,你永远可以相信,只要你做对了,结果就是对的,如果结果不对,那一定是有某个地方你没做对。


并且计算机不需要什么细节操作,不会因为你打字打得快,键盘敲得响,你就能做对了。


真正有用的是,你能写出代码,完成你要的效果。你打字打得慢,只会影响你代码敲完的速度,但不影响你代码能够完成的效果,能明白我的意思吗?


但是对于刚开始学习前端的你来说,想要把代码写对,是一件非常不容易的事情。我没说它难啊,我说的是非常不容易。


这里的把代码写对,还不涉及到代码的逻辑,哪怕是你单纯的跟着视频里的代码去敲,也是一件不容易做对的事情。


我之前帮学员排查问题,就是单纯把该写“;”的地方,写成了“,”,“methods”写成了“method”。她自己看了一个小时多没整出来,我看一眼就解决了。


不要觉得这个问题有多简单啊,对于有经验的人,和正在听我讲的你来说,这是很容易发现的问题,但是对于正在学习的小白来说,是很难发现这些情况的。


我上大学的时候,有一次课程设计就是这样,我不小心把字母n,打成了字母b,坐在教室里,我找了将近一个上午没有找出原因。


这些让你得不到正确结果的情况,会打击你的学习兴致,还是那句话,越吃苦,越抗拒,越抗拒,越得不到结果。


这里呢,我给你们一个解决办法,那就是,


学会“一比一模仿”。能够完完整整的把你看的内容,给复刻出来。


这个方法,对于正在学习的你来说,就已经足够了。


那些你觉得厉害的人,也都是这么过来的。不要怀疑,现实的真相就是如此。


三、认知缺乏的苦


许多自学出来的人都觉得,别人都能像他一样,能解决自己遇到的任何问题。


我的学习能力,相比于大多数人来说,算是足够优秀的了,如果你关注我的时候够长,你就知道我没在说胡话。


但即使是学习能力优秀如我,依旧有不知道怎么解决的问题。


我曾经在大二的寒假,认为我只要努力学好 Java,就能跟着别人做项目。


于是我花了 3 天时间,每天坐在电脑前看视频 16~18 个小时,出了上厕所,就没离开过,包括吃饭。


你知道后面怎么了吗?


我迷茫了,下一步该做什么,我完全没思路。然后我就“摆烂”了一个星期,看了一周的火影忍者。


作为已经经历过这些阶段的人来说,事情已经变得很简单了。


就好比现在的我,如果回到那时候,我会主动去联系之前找过的老师,问他能不能跟着做项目了,或者下一步我该做什么。


但是身处那个阶段的我,是想不到可以这么做的。


这样的苦,是属于认知层面的苦。而认知这个东西,当你没跳出自己的认知范围时,你是不知道外面的世界是怎样的。


正在看这篇内容的你,已经比当初的我要优秀了,至少能想到去找解决方案,至少能刷到我这篇内容。


解决办法我也说了,


去网上、去相关的群里问;去搜相关的问题;等系统给你推荐优质的内容,都是一种方法。


四、结语


现在回过头来看,这些苦都不是什么大事情,熬一熬也能过去。所以这么多的人,才会推荐你说,自学前端就够了。


但是现在的我看法不一样了。


哪怕是回到过去,我也依旧会努力认识一个或多个优秀的前辈,向他们汲取经验。


这些苦我都吃过了,我知道能熬过去,但是这个熬过去的这部分时间,我本可以用来做更多有意义的事情。


如果你正在学习前端开发,我建议你一定要找一个,你信得过的前辈。


不要去那种“新人交流群”,那没意义,只不过是一群人在抱团取暖罢了。


要找就找一个高手,那种你的问题能够一点就透的,并且还能言简意赅的给你讲明白的。


我见过太多在群里交流了半天,得不到一个确定的解的。讨论了半天,远不如一位高手,

作者:Wetoria
来源:juejin.cn/post/7249943229289267258
一两句话就能讲明白。

收起阅读 »

档案真的很重要,它关乎你的未来,请一定保管好

看到那个妈妈拆那个学生的档案,只能一声叹息。 档案的用途 档案对你升学、考公、考编、办理退休等都非常重要,没有档案,你基本办不了。打个不恰当的比喻,你的档案就相当于是你的蘑菇云,可以不用,但必须保证它完好无损,放在它该放的地方。等用的时候你再折腾,晚了。 大学...
继续阅读 »

看到那个妈妈拆那个学生的档案,只能一声叹息。


档案的用途


档案对你升学、考公、考编、办理退休等都非常重要,没有档案,你基本办不了。打个不恰当的比喻,你的档案就相当于是你的蘑菇云,可以不用,但必须保证它完好无损,放在它该放的地方。等用的时候你再折腾,晚了。


大学毕业档案处置


你毕业的时候,有的学校会把你的档案直接发到你的生源地,有的学校会把档案交到你手里。当你手里拿到你档案的那一刻起,你的档案就是你现在最最最重要的事情,没有什么事情比他更重要,你必须确保它的完好无损。


如果你是大学生,你们老师让把档案存哪你就抓紧存哪去,比如你们当地人才市场、人社局,别磨蹭。档案在你手里放的时间太长会变成死档,很多地方是不接收死档的。如果以后你有需要,到时候你再着急,没用了。


高中毕业档案处置


如果你是高中生,拿到档案后不要随便一扔,一定要找个合适的地方保管好了。跟家里人说清楚这个东西谁都别碰,这个东西一旦损坏你就毁了,最好能找个地方把它锁好,等你上大学的时候,把它交给学校。


损坏后能修补吗


档案万一损坏了,你可以试着联系下发给你档案的地方,看看能不能重新密封。说实话,难!


当时给我们发档案的时候,我们老师跟我们说,如果你的档案万一被拆了,理论上你可以拿着你的档案找学校给你重新密封什么的,但这只是理论上。实际非常难,几乎就是不可能,跟你档案相关的所有单位都要核对一遍、盖章,他给你核对,他是要担责的,你的档案万一有问题,他怎么办?所以你千万不要想着我拆了咋了,再密封下不就行了

作者:程序员黑黑
来源:juejin.cn/post/7249930189181993020
,明确的告诉你不行。

收起阅读 »

附件类文件存储在环信和不存储在环信时的两种实现方式

场景一: 附件类文件存储在环信服务器使用环信EMFileMessageBody#getLocalUrl判断有没有本地文件; EMImageMessageBody imgBody = (EMImageMessageBody) message.getBody();...
继续阅读 »

场景一: 附件类文件存储在环信服务器

使用环信EMChatManager#downloadAttachment下载附件方案
(本篇文章以图片消息为例,其他附件类消息类似):

一、 通过EMFileMessageBody#getLocalUrl判断有没有本地文件;


EMImageMessageBody imgBody = (EMImageMessageBody) message.getBody();
//本地文件的资源路径
String imageLocalUrl = imgBody.getLocalUrl();



1 、 如果存在本地路径,直接使用本地文件;(本案例使用的Glide)

      Glide.with(this).load(imageLocalUrl).into(image);


2. 如果不存在本地文件,调用EMChatManager#downloadAttachment下载,下载成功后展示图片;(本案例使用的Glide)

EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgId);
EMCallBack callback = new EMCallBack() {
public void onSuccess() {
EMLog.e(TAG, "onSuccess" );
runOnUiThread(new Runnable() {
@Override
public void run() {
Uri localUrlUri = ((EMImageMessageBody) msg.getBody()).getLocalUri();
Glide.with(mContext)
.load(localUrlUri)
.apply(new RequestOptions().error(default_res))
.into(image);
}
});
}

public void onError(final int error, String message) {
EMLog.e(TAG, "offline file transfer error:" + message);
}

public void onProgress(final int progress, String status) {
EMLog.d(TAG, "Progress: " + progress);
}
};
msg.setMessageStatusCallback(callback);
EMClient.getInstance().chatManager().downloadAttachment(msg);


二、 如果对本地存储的路径有特殊要求:

1 可以先通过EMFileMessageBody#setlocalUrl去修改路径;
2 然后再调用EMChatManager#downloadAttachment下载(下载操作可以参考上边);

EMImageMessageBody imgBody = (EMImageMessageBody) message.getBody();
//本地文件的资源路径
imgBody.setLocalUrl(localUrl);


场景二: 附件类文件存储在自己服务器

一、发送自定义消息时,携带文件存储的url;

EMMessage customMessage =EMMessage.createSendMessage(EMMessage.Type.CUSTOM);
// `event` 为需要传递的自定义消息事件,比如礼物消息,可以设置:
String event = "gift";
EMCustomMessageBody customBody = new EMCustomMessageBody(event);
// `params` 类型为 `Map`
Map params = new HashMap<>();
params.put("imageUrl","服务器的图片url");
customBody.setParams(params);
customMessage.addBody(customBody);
// `to` 指另一方环信用户 ID(或者群组 ID,聊天室 ID)
customMessage.setTo(to);
// 如果是群聊,设置 `ChatType``GroupChat`,该参数默认是单聊(`Chat`)。
customMessage.setChatType(chatType);
EMClient.getInstance().chatManager().sendMessage(customMessage);


二 、接收消息时,解析字段获取到url,进行下载;


@Override
public void onMessageReceived(List messages) {
super.onMessageReceived(messages);
for (EMMessage message : messages) {
EMCustomMessageBody emCustomMessageBody = (EMCustomMessageBody) message.getBody();
Map params = emCustomMessageBody.getParams();
String imageUrl = params.get("imageUrl");
}
}

收起阅读 »

如何实现比 setTimeout 快 80 倍的定时器?

起因 很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4m...
继续阅读 »

起因


很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard 规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:

let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。



更详细的原因,可以参考 为什么 setTimeout 有最小时延 4ms ?


探索


假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,上面那篇 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,你可以参考这里所说的 window.postMessage()



这篇文章里的作者给出了这样一段代码,用 postMessage 来实现真正 0 延迟的定时器:

(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回调函数的执行时机和 setTimeout 类似,都属于宏任务,所以可以简单利用 postMessageaddEventListener('message') 的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:



全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于 postMessage 的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。但空口无凭,咱们用数据说话。


作者设计了一个实验方法,就是分别用 postMessage 版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。读者也可以在这里自己跑一下测试


实验代码:

function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过 setZeroTimeout 也就是 postMessage 版本来递归计数到 100,然后切换成 setTimeout 计数到 100。


直接放结论,这个差距不固定,在我的 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在我硬件更好的台式机上,甚至能到 200 倍以上。



Performance 面板


只是看冷冰冰的数字还不够过瘾,我们打开 Performance 面板,看看更直观的可视化界面中,postMessage 版的定时器和 setTimeout 版的定时器是如何分布的。



这张分布图非常直观的体现出了我们上面所说的所有现象,左边的 postMessage 版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的 setTimeout 版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


借用 React Scheduler 为什么使用 MessageChannel 实现 这篇文章中的一段伪代码:

const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage 的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?参考我的这篇对 EventLoop 规范的解读 深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


通过本文,你大概可以了解如下几个知识点:



  1. setTimeout 的 4ms 延迟历史原因,具体表现。

  2. 如何通过 postMessage 实现一个真正 0 延迟的定时器。

  3. postMessage 定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。

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

搞懂Kotlin委托

1、委托是什么 委托是一种设计模式,它的基本理念是操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。也就是说在委托模式中,会有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。 委托模式中,有三个角色,约束、委托...
继续阅读 »

1、委托是什么


委托是一种设计模式,它的基本理念是操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。也就是说在委托模式中,会有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。


委托模式中,有三个角色,约束委托对象被委托对象


委托模式其实不难理解,生活中有很多类似的地方。假如你手里有一套房子想要租出去,想要把房子租出去,联系房客、带人看房是必不可少的,如果让你自己来进行前面所说的工作,可能会占用你自己的业余生活时间,所以这种时候就可以把这些事委托给中介处理,接下来你不需要自己联系房客,也不需要亲自带人看房子,这些工作都由中介完成了,这其实就是一种委托模式。在这里,约束就是联系房客、看房子等一系列操作逻辑,委托对象是你,也就是房主,被委托对象是中介。


在Kotlin中将委托功能分为两种:类委托属性委托


1.1、类委托


类委托的核心思想是将一个类的具体实现委托给另一个类去完成


以上面租房子的例子,我们使用Kotlin的by关键字亲自实现一下委托模式。


首先,我们来定义一下约束类,定义租房子需要的业务:联系房客、看房子。

// 约束类
interface IRentHouse {
// 联系房客
fun contact()
// 带人看房
fun showHouse()
}

接着,我们来定义被委托类,也就是中介。

// 被委托类,中介
class HouseAgent(private val name: String): IRentHouse {
override fun contact() {
println("$name中介 联系房客")
}
override fun showHouse() {
println("$name中介 带人看房")
}
}

这里我们定义了一个被委托对象,它实现了约束类的接口。


最后,定义委托类,也就是房主。

// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse by agent {
// 签合同
fun sign() {
println("房主签合同")
}
// 带人看房
override fun showHouse() {
println("房主带人看房")
}
}

这里定义了一个委托类HouseOwner,同时把被委托对象作为委托对象的属性,通过构造方法传入。


在Kotlin中,委托用关键字by,by后面就是委托的对象,可以是一个表达式。


测试:

fun main() {
val agent = HouseAgent("张三") // 初始化一个名叫张三的中介
val owner = HouseOwner(agent) // 初始化房主,并把“中介”介绍给他
owner.contact()
owner.showHouse()
owner.sign()
}

运行结果如下

张三中介 联系房客
房东带人看房
房主签合同

可以看到,在整个租房过程中,房主的一些工作由中介帮着完成,例如联系房客。如果房东心血来潮,觉得自己更理解自己的房子,想把原来委托给中介的工作自己处理,也可以自己来进行,例如带人看房。最后签合同只有房主能处理,所以这是房主独有的操作。以上就是一个委托的简单应用。


而这也是委托模式的意义所在,就是让大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法,那么房东租房的整个逻辑就能顺利进行了。


有的人可能会说,这样的话不用by关键字我也可以实现委托。如下:

// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse {
// 签合同
fun sign() {
println("房主签合同")
}
// 联系房客
override fun contact() {
agent.contact()
}
// 带人看房
override fun showHouse() {
println("房主带人看房")
}
}

运行结果如

张三中介 联系房客
房东带人看房
房主签合同

可以看到,与前面的输出结果一样。


但是这种写法是有一定弊端的,如果约束接口中的待实现方法比较少还好,如果有几十甚至上百个方法的话就会出现问题。


前面也说过委托模式的最大意义在于,大部分的委托类方法实现可以调用被委托对象中的方法。而既然使用委托模式,就说明委托类中的大部分的方法实现是可以通过调用被委托对象中的方法实现的,这样的话每个都像“联系房客”那样去调用被委托对象中的相应方法实现,还不知道要写到猴年马月,而且会产生大量样板代码,很不优雅。


所以Kotlin提供了关键字by,在接口声明的后面使用by关键字,再接上被委托的辅助对象,这样可以免去仅调用被委托对象方法的模版代码。


而如果要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利。


如果想加入独有的方法逻辑,直接写一个方法即可。


这几种情况在前面的“租房”场景中都有体现。


1.2、属性委托


属性委托的核心思想是将一个属性的具体实现委托给另一个类去完成


我们来看一下委托属性的语法结构

class Test {
// 属性委托
var prop: Any by Delegate()
}

可以看到,这里使用by关键字连接了左边的prop属性和右边的Delegate实例,这种写法就代表着将prop属性的具体实现委托给了Delegate类去完成


1.2.1、什么是属性委托

前面也说了属性委托是将一个属性的具体实现委托给另一个类去完成。那么属性把什么委托了出去,被委托类又有哪些实现呢?


其实,属性委托出去的是其set/get方法,委托给了被委托类的setValue/getValue方法。

// 属性的get/set方法
var prop: Any
get() {}
set(value) {}

// 委托后
var prop: Any by Delegate()

注意这里prop声明的是var,即可变变量,如果委托给Delegate类的话,则必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。

class Delegate {
private var propValue: String? = null

operator fun getValue(thisRef: Any, property: KProperty<*>): String? {
return propValue
}

operator fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
propValue = value
}
}

到这里,属性委托已经完成了,这时候,当你点开by关键字的时候会出现如下提示。



这就表明prop已经把具体实现委托给Delegate类完成。


当调用prop属性的时候会自动调用Delegate类的getValue()方法,当给prop属性赋值的时候会自动调用Delegate类的setValue()方法。


如果prop声明的是val,即不可变变量,则Delegate类只需要实现getValue()方法即可。


有些人第一次看到方法中的参数可能有点懵,但其实这是一种标准的代码实现样板,并且官方也提供了接口类帮助我们实现,具体的接口类下面会说到。


虽然是一套固定的样板,但我们也要理解其中参数的含义。



  • thisRef:用于声明该Delegate类的委托功能可以在什么类中使用。必须与 属性所在类 类型相同或者是它的父类,如果是扩展函数,则指的是扩展的类型。

  • property:KProperty是Kotlin中的一个属性操作类,可用于获取各种属性相关的值。多数情况下都不需要修改。

  • value:具体要赋值给委托属性的值,必须与getValue的返回值相同。


那么为什么要使用属性委托呢?


假如想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段field中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作委托给一个辅助对象(类),这个辅助对象就是被委托类。说白了,就是避免样板代码,防止出现大量重复逻辑。


1.2.2、ReadOnlyProperty/ReadWriteProperty接口

前面说到,如果要实现属性委托,就必须要实现getValue/setValue方法,可以看到getValue/setValue方法结构比较复杂,很容易遗忘,为了解决这个问题,Kotlin 标准库中声明了2个含所需operator方法的 ReadOnlyProperty/ReadWriteProperty 接口。

interface ReadOnlyProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

val属性实现ReadOnlyProperty接口,var属性实现ReadWriteProperty接口。这样就可以避免自己写复杂的实现方法了。

// val 属性委托实现
class Delegate1: ReadOnlyProperty{
private var propValue: String = "zkl"

override fun getValue(thisRef: Any, property: KProperty<*>): String {
return propValue
}
}
// var 属性委托实现
class Delegate2: ReadWriteProperty{
private var propValue: String? = null

override fun getValue(thisRef: Any, property: KProperty<*>): String? {
return propValue
}

override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
propValue = value
}
}

2、Kotlin标准库的几种委托


2.1、延迟属性 lazy


2.1.1、使用by lazy进行延迟初始化

使用by lazy()进行延迟初始化相信大家都不陌生,在日常使用中也能信手拈来。如下,是DataStoreManager对象延迟初始化的例子。

//这里使用by lazy惰性初始化一个实例
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DataStoreManager(store) }

by lazy()代码块是Kotlin提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当变量(instance)首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋值给变量。 调用如下:

fun main() {
println(instance::class.java.simpleName)
println(instance::class.java.simpleName)
println(instance::class.java.simpleName)
}

打印结果如下:

第一次调用时执行
DataStoreManager
DataStoreManager
DataStoreManager

可以看到,只有第一次调用才会执行代码块中的逻辑,后续调用只会返回代码块的最终值


那么什么时候适合使用by lazy进行延迟初始化呢?当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时,就非常适合了。


当然,如果变量第一次初始化时抛出异常,那么lazy将尝试在下次访问时重新初始化该变量。


2.1.2、拆解by lazy

可能大家刚接触的时候会觉得by lazy本是一体使用的,其实不是,实际上,by lazy并不是连在一起的关键词,只有by才是Kotlin中的关键字,lazy只是一个标准库函数而已


那么就把二者拆开看,先点开by关键字

@kotlin.internal.InlineOnly
public inline operator fun Lazy.getValue(thisRef: Any?, property: KProperty<*>): T = value

会发现它是Lazy 类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy类中,这个value便是返回的值。

//惰性初始化类
public interface Lazy {

//懒加载的值,一旦被赋值,将不会被改变
public val value: T

//表示是否已经初始化
public fun isInitialized(): Boolean
}

接下来看一下lazy,这个就是一个高阶函数,用来创建lazy实例的。

public actual fun  lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer)

可以看到,该方法中会把initializer,也就是代码块中的内容,传递给SynchronizedLazyImpl类进行初始化并返回。大部分情况我们使用的都是这个方法。


当然我们也可以设置mode,这样会调用下面的lazy方法,该方法中会根据mode类型来判断初始化那个类。如下

public actual fun  lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}

// 使用如下
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
println("第一次调用时执行")
DataStoreManager(store)
}

三个mode解释如下:



  • LazyThreadSafetyMode.SYNCHRONIZED:添加同步锁,使lazy延迟初始化线程安全

  • LazyThreadSafetyMode. PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。

  • LazyThreadSafetyMode. NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程


而第一个lazy不设置mode时默认的就是SYNCHRONIZED,也是最常用的mode,这里我们直接看一下对应类的代码:

//线程安全模式下的单例
private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable {
private var initializer: (() -> T)? = initializer
//用来保存值,当已经被初始化时则不是默认值
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
//锁
private val lock = lock ?: this

override val value: T
//见分析1
get() {
//第一次判空,当实例存在则直接返回
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
//使用锁进行同步
return synchronized(lock) {
//第二次判空
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
//真正初始化
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

//是否已经完成
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)
}

这个单例就是双重校验锁实现的。


2.2、可观察属性


Kotlin除了提供了lazy函数实现属性延迟加载外,还提供了Delegates.observableDelegates.vetoable标准库函数来观察属性变化。先来看observable


2.2.1、observable函数
    public inline fun  observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty =
object : ObservableProperty(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}

可以看到,该标准库函数接收了两个参数initialValueonChange



  • initialValue:初始值

  • onChange:属性值变化时的回调逻辑。回调有三个参数:propertyoldValuenewValue,分别表示:属性、旧值、新值。


使用如下:

var observableProp: String by Delegates.observable("初始值") { property, oldValue, newValue ->
println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
}
// 测试
fun main() {
observableProp = "第一次修改值"
observableProp = "第二次修改值"
}

打印如下:

属性:observableProp 旧值:初始值 新值:第一次修改值 
属性:observableProp 旧值:第一次修改值 新值:第二次修改值

可以看到,当把属性委托给Delegates.observable后,每一次赋值,都能观察到属性的变化。


2.2.2、vetoable函数

vetoable函数与observable一样,都可以观察属性值变化,不同的是,vetoable可以通过代码块逻辑决定属性值是否生效。

    public inline fun  vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty =
object : ObservableProperty(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
}

接收的两个参数与observable函数几乎相同,不同的是onChange回调有一个Boolean的返回值。


使用如下:

var vetoableProp: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
newValue > 0
}
// 测试
fun main() {
println("vetoableProp:$vetoableProp")
vetoableProp = 2
println("vetoableProp:$vetoableProp")
vetoableProp = -1
println("vetoableProp:$vetoableProp")
vetoableProp = 3
println("vetoableProp:$vetoableProp")
}

打印如下:

vetoableProp:0
属性:vetoableProp 旧值:0 新值:2
vetoableProp:2
属性:vetoableProp 旧值:2 新值:-1
vetoableProp:2
属性:vetoableProp 旧值:2 新值:3
vetoableProp:3

可以看到-1的赋值并没有生效。


那么具体的逻辑是什么呢?


回看observable和vetoable的源码可以发现,二者继承了ObservableProperty抽象类,不同的是observable重写了该类afterChange方法,vetoable重写了该类beforeChange方法,并且beforeChange会有一个Boolean的返回,返回的是我们自己写的回调逻辑的返回值。


那么接着看setValue逻辑

protected open fun beforeChange(property: KProperty<*>, oldValue: V, newValue: V): Boolean = true

protected open fun afterChange(property: KProperty<*>, oldValue: V, newValue: V): Unit {}

public override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}

可以看到会先执行beforeChange方法,如果beforeChange为false则直接返回,并且不会更新值,为true时才会更新值,接着执行afterChange方法。这里beforeChange方法默认返回true。


其实只要查看这些函数源码可以发现,其内部调用的都是代理类,所以说白了这些都是属性委托。


3、总结


委托在Kotlin中有着至关重要的作用,但是也不能滥用,中间毕竟多了一个中间类,不合理的使用不但不会有帮助,反而会占用内存。前面也说过类委托最大的意义在于,大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法。属性委托的意义在于,对于比较复杂的一些属性,它们处理起来比把值保存在支持字段field中更复杂,且它们的逻辑相同,为了防止出现大量模版代码,可以使用属性委托。所以在使用委托前,我们也可以按照上面的标准考虑一下,合理的使用委托可以减少大量样板代码,提高代码的可扩展性和可读性。


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

Gson与Kotlin"摩擦"的那件小事

大家好,本篇文章分享一下之前使用gson和kotlin碰撞出的一些火花,脑瓜子被整的懵懵的那种。 准备知识 总所周知,当Gson没有无参构造函数时,会使用UnSafe以一种非安全的方式去创建类的对象,这样会产生两个问题: 属性的默认初始值会丢失,比如某个类中...
继续阅读 »

大家好,本篇文章分享一下之前使用gson和kotlin碰撞出的一些火花,脑瓜子被整的懵懵的那种。


准备知识


总所周知,当Gson没有无参构造函数时,会使用UnSafe以一种非安全的方式去创建类的对象,这样会产生两个问题:



  1. 属性的默认初始值会丢失,比如某个类中有这么一个属性public int age = 100,经过unsafe创建该类对象,会导致age的默认值100丢失,变为0;




  1. 会绕过Kotlin的空安全检验,因为经过unsafe创建的对象不会在属性赋值时进行可null校验。


所以一般比较在使用Gson反序列化时,比较推荐的做法就是反序列化的类要有无参构造函数。


PS:其实提供了无参构造函数,还是有可能会绕过Kotlin空安全校验,毕竟在Gson中属性是通过反射赋值的,所以一些人会推荐使用Moshi,这个笔者还没怎么使用过,后续会了解下。


看一个脑瓜子懵的例子


先上代码:

class OutClass {
val age: Int = 555

override fun toString(): String {
return "OutClass[age = $age]"
}

inner class InnerClass {
val age1: Int = 897

override fun toString(): String {
return "InnerClass[age = ${this.age1}]"
}

}
}

以上两个类OutClassInnerClass看起来都有无参构造函数,现在我们来对其进行一一反序列化。


1. 反序列化OutClass

fun main(args: Array<String>) {
val content = "{"content": 10}"
val out = OutClass::class.java
val obj = Gson().fromJson(content, out)
println(obj)
}

反序列化使用的字符串是一个OutClass类不存在的属性content,咱们看下输出结果:



看起来没毛病,由于存在无参构造函数,且反序列化所使用的字符串也不包括age字段,age的默认值555得以保留。


2. 反序列化InnerClass


先上测试代码:

fun main(args: Array<String>) {
val content = "{"content": 10, "location": null}"
val out = OutClass.InnerClass::class.java
val obj = Gson().fromJson(content, out)
println(obj)
}

运行结果如下:



不是InnerClass也是有无参构造函数的吗,为啥age字段的默认值897没有被保留,当时给整蒙了。


于是进行了下debug断点调试,发现最终是通过Unsafe创建了InnerClass



当时是百思不得其解,后续想了想,非静态内部类本身会持有外部类的引用,而这个外部类的引用是通过内部类的构造方法传入进来的,咱们看一眼字节码:



所以非静态内部类根本就没有无参构造方法,所以最终通过Gson反序列化时自然就是通过Unsafe创建InnerClass对象了。


如果想要解决上面这个问题,将非静态内部类改成静态内部类就行了,或者尽量避免使用非静态内部类作为Gson反序列化的类。


另外大家如果感兴趣想要了解下Gson是如何判断的反射无参构造方法还是走Unsafe创建对象的,可以看下源码:


ReflectiveTypeAdapterFactory#create ——>ConstructorConstructor#get


介绍下typeOf()方法


回忆下我们之前是怎么反序列化集合的,看下下面代码:

fun main(args: Array<String>) {
val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
val obj = Gson().fromJson<List<OutClass>>(content, object : TypeToken<List<OutClass>>(){}.type)
println(obj)
}

要创建一个很麻烦的TypeToken对象,获取其type然后再进行反序列化,输出如下正确结果:



为了避免麻烦的创建TypeToken,我之前写了一篇文章来优化这点,大家感兴趣的可以看下这篇文章:Gson序列化的TypeToken写起来太麻烦?优化它


然后之前有个掘友评论了另一个官方提供的解决方法:



于是我赶紧试了下:

@OptIn(ExperimentalStdlibApi::class)
fun main(args: Array<String>) {
val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
val obj = Gson().fromJson<List<OutClass>>(content, typeOf<List<OutClass>>().javaType)
println(obj)
}

运行输出:



没毛病,这个写法要比创建一个TypeToken简单多了,这个api是很早就有了,不过到了kotlin1.6.0插件版本才稳定的,请大家注意下这点:



十分推荐大家使用这种方式,官方支持,就突出一个字:稳。


总结


本篇文章主要是给大家介绍了Gson反序列化非静态内部类时的坑,以及介绍了一个官方支持的api:typeOf(),帮助大家简化反序列化集合的操作,希望本篇文章能对比有所帮助。


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

Android-我对代理模式的理解

以下业务场景不大现实,我这里只是提供一种思路 想象一种场景:有一天,产品经理让你记录某些地方的行为日志并且存储到本地方便查阅,你可能会写下如下代码:interface ILogger { fun logInfo(action: String) ...
继续阅读 »

以下业务场景不大现实,我这里只是提供一种思路

想象一种场景:有一天,产品经理让你记录某些地方的行为日志并且存储到本地方便查阅,你可能会写下如下代码:

interface ILogger {

fun logInfo(action: String)

fun logError(action: String)
}


class Logger : ILogger{

override fun logInfo(action: String) {
//存储到本地
saveToLocalFile(action)
}

override fun logError(action: String) {
//存储到本地
saveToLocalFile(action)
}

private fun saveToLocalFile(action: String) {}
}

当需要调用的时候:

val logger: ILogger = Logger()
logger.logError("出现问题")

当然了,你更大概率是考虑用一个单例类直接调用,而不是每次都这样写。

假如某天换了个产品经理,要求你在这些存储日志之前,先将日志上传到服务器,存储日志后,做一个埋点记录

class Logger : ILogger{

override fun logInfo(action: String) {
//上传到服务器
upLoadToCloud(action)
//存储到本地
saveToLocalFile(action)
//埋点
eventTracking(action)
}

override fun logError(action: String) {
//上传到服务器
upLoadToCloud(action)
//存储到本地
saveToLocalFile(action)
//埋点
eventTracking()
}

private fun saveToLocalFile(action: String) {}

private fun upLoadToCloud(action: String) {}

private fun eventTracking() {}
}

设计模式讲究一个职责单一,那么以上代码最直观的就是不同的功能耦合在一起。


什么是代理模式


一句话解释就是:在不改变原有功能的基础上,通过代理类扩展新的功能,使得功能之间解耦,或者框架和业务之间解耦,有点装饰器模式的味道。


静态代理

interface ILogger {

fun logInfo(action: String)

fun logError(action: String)
}

class Logger : ILogger{

override fun logInfo(action: String) {
//存储到本地
saveToLocalFile(action)
}

override fun logError(action: String) {

//存储到本地
saveToLocalFile(action)
}

private fun saveToLocalFile(action: String) {}

}

class LoggerProxy(val logger: Logger) : ILogger {

override fun logInfo(action: String) {
//上传到服务器
upLoadToCloud(action)
//通过传进来的logger对象来调用原来的实现方法
logger.logInfo(action)
//埋点
eventTracking(action)
}

override fun logError(action: String) {
//上传到服务器
upLoadToCloud(action)
//通过委托logger对象来调用原来的实现方法
logger.logError(action)
//埋点
eventTracking(action)
}

private fun upLoadToCloud(action: String) {}

private fun eventTracking(action: String) {}
}

//使用方式
val logger: ILogger = LoggerProxy(Logger())
logger.logError("出错了")

在第25行,我们新添加了一个新的LoggerProxy代理类同样的实现了ILogger接口,在两个方法中,我们按顺序完成了功能的调用,将上传到服务器和埋点的逻辑和存储到本地的逻辑进行了分离,代理类LoggerProxy在业务的执行前后附加了其他的逻辑。

看到这你可能会觉得,有点脱裤子放屁了。确实,当前代码量特别小,对于当前代码体现的可能不太明显,如果你正在一个设计相对大型的框架,业务和框架代码的分离显得就相对重要了。

作为一种设计思想,他提供的是一种思路,让你写出来的不是面向过程的代码,有好有坏,当然在实际项目中不要为了设计模式而设计模式,不然就适得其反了,写出来的代码可读性差。


动态代理


对于静态代理,上面的代码中我们在代理类中的前后加了两个不同的功能,这两个相对职责不同的功能耦合在了一起,我由于偷懒没将其中的一个功能拆走,正常情况是应该再写一个代理类去做相同的一部分操作,如果功能更多的话就要写更多的代理类,繁琐度可想而知。

再一个,静态代理是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。而动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。

class Logger : ILogger{

override fun logInfo(action: String) {
println("存储到本地: $action")
saveToLocalFile(action)
}

override fun logError(action: String) {
println("存储到本地: $action")
saveToLocalFile(action)
}

private fun saveToLocalFile(action: String) {}

}


class LoggerProxy(private val target: ILogger): InvocationHandler {

fun createProxy() = Proxy.newProxyInstance(
ILogger::class.java.classLoader,
arrayOf<Class<*>>(ILogger::class.java),
LoggerProxy(target)
) as ILogger

override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
val action = args!![0].toString()
if (method?.name == "logInfo") {
uploadToCloud(action)
target.logInfo(action)
eventTracking(action)
} else if (method?.name == "logError") {
uploadToCloud(action)
target.logError(action)
eventTracking(action)
}
return null
}

private fun uploadToCloud(action: String) {
println("上传数据到服务器")
}

private fun eventTracking(action: String) {
println("埋点")
}
}

interface ILogger {

fun logInfo(action: String)

fun logError(action: String)
}

调用方式
val proxy = LoggerProxy(Logger())
proxy.createProxy().logError("出错了")

打印顺序
1. 上传数据到服务器
2. 存储到本地: 出错了
3. 埋点


  1. 在动态代理中,当我们通过createProxy()创建代理对象后,调用logError或logInfo方法的时候

  2. 代理对象的invoke()方法会被调用

  3. 由于我们传入的只有action这个参数,在invoke方法中,可通过args[0]来获取传入的数据;通过method.name获取待执行的方法名,以此来判断逻辑的走向


代理的创建方式createProxy()方法中的代码大部分都是固定的。


总结


静态代理:静态代理在编译时期就已经确定代理类的代码,代理类和被代理类在编译时就已经确定;如果需要扩展的功能越来越多,静态代理的缺点很明显就是要写大量的代理类,管理和维护都不太方便。

动态代理:动态代理在运行时动态生成代理对象,关系灵活,由于是在运行时动态的生成代理类,动态代理解决了静态代理大量代理类的问题,但是有个新的问题就是反射相对耗时一点。


我们常用的Retrofit就有用到动态代理,感兴趣的同学可以去深入了解下,这边就不过多讲解,包括AOP(面向切面编程),动态权限申请等等。


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

来个面试题,看看你对 kotlin coroutine掌握得如何?

给出下面代码:lifecycleScope.launch(Dispatchers.IO) { val task1 = async { throw RuntimeException("task1 failed") } v...
继续阅读 »

给出下面代码:

lifecycleScope.launch(Dispatchers.IO) {
val task1 = async {
throw RuntimeException("task1 failed")
}

val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}

问:app 会发生什么?输出的日志是怎样子的?为什么?


......


......


......


......


......


......


......


......


......


答:app 会 crash,输出日志为


I/test: catch task1: java.lang.RuntimeException: task1 failed
I/test: is coroutine active: false
I/test: catch task2: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=DeferredCoroutine{Cancelling}
I/test: scope end.

魔幻吗?


那我们就来分析下为啥结果是这个样子的。


协程有一个很基础的设定:默认情况下,异常会往外层 scope 抛,用以立刻取消外层 scope 内的其它的子 job。


在上面的例子中,假设:lifecycleScope.launch 创建的子 scope 为 A。task1 用 async 创建 scope A 的子 scope 为 B。task2 用 async 创建 scope A 的子 scope 为 C。


当 scope B 发生异常,scope B 会将异常抛给 scope A,scope A 会 cancel 掉 task2 和自己,再把异常抛给 lifecycleScope,因为 lifecycleScope 没有 CoroutineExceptionHandler 并且 scope A 是通过 launch 启动的,所以 crash 就发生了。


那如何打断异常的这个传播链呢?


答案就是使用 SupervisorJob,或者用基于它的 supervisorScope。它不会把异常往上抛,也不会取消掉其它的子 job。但是,SupervisorJob 对 launch 和 async 启动的协程的态度是不一样的,它的源码注释里写明了的,简单的认为它会吃掉异常是会踩坑的。



翻译出来就是,如果是 launch 启动的子协程,是需要 CoroutineExceptionHandler 配合处理的,如果是 async 启动的协程,就是真的不抛,等到 Deferred.await 时再抛。


所以,在上面的代码中,虽然 lifecycleScope 有用到 SupervisorJob,但异常从 scopeA 往上传时,因为没有 CoroutineExceptionHandler,所以跪了。


那么为什么 async 要往上抛异常,导致 await 的 try catch 还需要 supervisorScope 的配合?感觉有点反人类?


想象一下下面的场合:

lifecycleScope.launch {
val task1 = async { "非常耗时的操作,但没有异常" }
val task2 = async { throw RuntimeException("") }
val result1 = task1.await()
val result2 = task2.await()
}

因为 task2 有异常,所以整个协程必定会失败。如果等 await 时才跑错误, 那么就需要等耗时的 task1 执行完成,轮到 task 的 await 调用时,异常才能跑出来,虽然也没啥问题,就是白白耗费了 task1 的执行。


而依据当前的设计,task2 抛出异常,那么外层 scope 就会把 task1 也给取消了,整个 scope 也就执行结束了。async 源码里提到的原因是为了 structured concurrency,也是期望使用者更多的关注 scope 以及 scope 内各个任务的关联关系吧。不过这坑确实有点让人有时摸不着头脑,可能以后就变了也说不定。


剩下一个问题是,task1 失败后就往上抛吗?为啥 catch task1 后还有日志打印出来?


其实上面已经提到了,异常抛给 scope A 后,它会 cancel 掉自己,再往上抛,而 cancel 掉自己并不是强制终止掉协程的执行,而是先变更状态为 cancelling,所以日志中 isActive 已经变成 false 了,第二个异常也不是 task2 的异常,而是 await 本身抛出的 CancellationException。这里告诉我们要注意两点:



  1. try catch 时如果是 CancellationException,要记得 rethrow。

  2. 一些循环、耗时的点,要记得用 isActive 或者 ensureActive 检查,不要写出不能正常 cancel 的协程。像 delay 等 api,官方已经做好了这方面的检查,极大地方便了开发者。这个线程 interrupted 相关知识点是同一个道理。


了解了各种坑点以及背后的原因,我们就可以把协程用得飞起了。最后,修复文章开头提到的问题,就是简单包个 supervisorScope 就行啦。

lifecycleScope.launch(Dispatchers.IO) {
supervisorScope {
val task1 = async {
throw RuntimeException("task1 failed")
}

val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}
}

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

一点Andorid开发小建议:妥善使用和管理Handler

在Android开发中,我们经常使用Android SDK提供的Handler类来完成线程间通信的任务,但是项目代码中,经常看到Handler过于随意地使用,这些使用可能有一些隐患,本文记录下这些问题,并给出相关建议。 问题一:使用默认无参构造来创建Handl...
继续阅读 »

在Android开发中,我们经常使用Android SDK提供的Handler类来完成线程间通信的任务,但是项目代码中,经常看到Handler过于随意地使用,这些使用可能有一些隐患,本文记录下这些问题,并给出相关建议。


问题一:使用默认无参构造来创建Handler


Handler有个无参构造方法,有的时候偷懒在Activity中直接通过无参构造方法来创建Handler对象,例如:

private final Handler mHandler = new Handler();

那么这个Handler对象会使用当前线程的Looper,这在Activity或自定义View中可能没问题,因为主线程是存在Looper的,但是在子线程中就会出现异常,因为子线程很可能没有进行过Looper.prepare()。另外一个隐患是,new Handler()使用“当前线程”的Looper,可能预期是在子线程,但是一开始的外部调用是在主线程,那么这个使用可能影响主线程的交互体验。



  • 建议:创建Handler对象时必须传递Looper参数来确保Looper的存在并显式控制其线程。


问题二:Looper.getMainLooper()的滥用


在Activity中我们可以使用runOnUiThread方法把一个过程放在主线程执行,但是在其他地方,通过Looper.getMainLooper()也是一个简单的方法,例如:

new Handler(Looper.getMainLooper()).post(() -> {
//do something ...
});

因为太方便了,所以到处都可以用,那么就存在了一个隐患:任何线程都可以通过这种方式来影响主线程。有可能在配置较差的手机出现意料之外的卡顿,而且这种卡顿可能有一定随机性,不容易复现,给排查问题的时候造成一定难度。



  • 建议:避免在线程外部创建该线程的Handler,例如,尽量避免在ActivityFragment和自定义View以外的地方创建主线程的Handler


问题三:在业务功能层面直接创建Hanlder


在开发中,有时候需要利用Handler来切换线程,或者利用Handler的消息功能,然后直接使用Handler,例如下面这段代码:

new Thread(() -> {
presenter.doTaskInBackground();
new Handler(Looper.getMainLooper()).post(() -> {
updateViewInMainThread();
});
}).start()

这个代码槽点太多,这里主要讲下关于Handler的。其实程序的结果并没有什么问题,不足之处在于把Handler这种平台相关的概念混到了业务功能代码里,就好像一个人在读诗朗诵,念完“两个黄鹂鸣翠柳”,然后下面冒出一句“我先喝口水”,喝完水然后继续念。



  • 建议:将Handler的创建和销毁放到框架层面,甚至可以封装一套使用的接口,而不是直接使用postsend等方法。


问题四:没有及时移除Hanlder的消息和回调


HandlerpostDelayedpostAtTime是两个便利的方法,但是这个方法并没有和组件的生命周期绑定,很容易造成Activity或其他大型对象无法及时释放。



  • 建议:不需要的时候,及时调用removeCallbacksAndMessages方法

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

在这个大环境下我是如何找工作的

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。 已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景...
继续阅读 »

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。
已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景下再加上全世界范围内的经济不景气我想每个人都能感受到寒意。


我还记得大约在 20 年的时候看到网上经常说的一句话:今年将是未来十年最好的一年。


由于当时我所在的公司业务发展还比较顺利,丝毫没有危机意识,对这种言论总是嗤之以鼻,直到去年国庆节附近。


虽然我们做的是海外业务,但是当时受到各方面的原因公司的业务也极速收缩(被收购,资本不看好),所以公司不得不进行裁员;
其实到我这里的时候前面已经大概有 2~3 波的优化,我们是最后一波,几乎等于是全军覆没,只留下少数的人维护现有系统。


这家公司也是我工作这么多年来少数能感受到人情味的公司,虽有不舍,但现实的残酷并不是由我们个人所决定的。


之后便开始漫长的找工作之旅,到现在也已经入职半年多了;最近看到身边朋友以及网上的一些信息,往往是坏消息多于好消息。


市场经历半年多的时间,裁员的公司反而增多,岗位也越来越少,所以到现在不管是在职还是离职的朋友或多或少都有所焦虑,我也觉得有必要分享一下我的经历。


我的预期目标


下面重点聊聊找工作的事情;其实刚开始得知要找工作的时候我并不是特别慌,因为当时手上有部分积蓄加上公司有 N+1 的赔偿,同时去年 10 月份的时候岗位相对于现在还是要多一些。


所以我当时的目标是花一个月的时间找一个我觉得靠谱的工作,至少能长期稳定的工作 3 年以上。


工作性质可以是纯研发或者是偏管理岗都可以,结合我个人的兴趣纯研发岗的话我希望是可以做纯技术性质的工作,相信大部分做业务研发的朋友都希望能做一些看似“高大上”的内容。
这一点我也不例外,所以中间件就和云相关的内容就是我的目标。


不过这点在重庆这个大洼地中很难找到对口工作,所以我的第二目标是技术 leader,或者说是核心主程之类的,毕竟考虑到 3 年后我也 30+ 了,如果能再积累几年的管理经验后续的路会更好走一些。


当然还有第三个选项就是远程,不过远程的岗位更少,大部分都是和 web3,区块链相关的工作;我对这块一直比较谨慎所以也没深入了解。


找工作流水账


因为我从入职这家公司到现在其实还没出来面试过,也不太知道市场行情,所以我的想法是先找几家自己不是非去不可的公司练练手。



有一个我个人的偏好忘记讲到,因为最近的一段时间写 Go 会多一些,所以我优先看的是 Go 相关的岗位。



第一家


首先第一家是一个 ToB 教育行业的公司,大概的背景是在重庆新成立的研发中心,技术栈也是 Go;


我现在还记得最后一轮我问研发负责人当初为啥选 Go,他的回答是:



Java 那种臃肿的语言我们首先就不考虑,PHP 也日落西山,未来一定会是 Go 的天下。



由于是新成立的团队,对方发现我之前有管理相关的经验,加上面试印象,所以是期望我过去能做重庆研发 Leader。


为此还特地帮我申请了薪资调整,因为我之前干过 ToB 业务,所以我大概清楚其中的流程,这种确实得领导特批,所以最后虽然没成但依然很感谢当时的 HR 帮我去沟通。


第二家


第二家主要是偏年轻人的 C 端产品,技术栈也是 Go;给我印象比较深的是,去到公司怎么按电梯都不知道🤣



他们办公室在我们这里的 CBD,我长期在政府赞助的产业园里工作确实受到了小小的震撼,办公环境比较好。



当然面试过程给我留下的印象依然非常深刻,我现在依然记得我坐下后面试官也就是 CTO 给我说的第一句话:



我看过你的简历后就决定今天咱们不聊技术话题了,直接聊聊公司层面和业务上是否感兴趣,以及解答我的疑虑,因为我已经看过你写的很多博客和 GitHub,技术能力方面比较放心。



之后就是常规流程,聊聊公司情况个人意愿等。


最后我也问了为什么选 Go,这位 CTO 给我的回答和上一家差不多😂


虽然最终也没能去成,但也非常感谢这位 CTO,他是我碰到为数不多会在面试前认真看你的简历,博客和 GitHub 都会真的点进去仔细阅读👍🏼。



其实这两家我都没怎么讲技术细节,因为确实没怎么聊这部分内容;这时就突出维护自己的技术博客和 GitHub 的优势了,技术博客我从 16 年到现在写了大约 170 篇,GitHub 上开源过一些高 star 项目,也参与过一些开源项目,这些都是没有大厂经历的背书,对招聘者来说也是节约他的时间。





当然有好处自然也有“坏处”,这个后续会讲到。


第三家


第三家是找朋友推荐的,在业界算是知名的云原生服务提供商,主要做 ToB 业务;因为主要是围绕着 k8s 社区生态做研发,所以就是纯技术的工作,面试的时候也会问一些技术细节。



我还记得有一轮 leader 面,他说你入职后工作内容和之前完全不同,甚至数据库都不需要安装了。



整体大概 5、6 轮,后面两轮都是 BOSS 面,几乎没有问技术问题,主要是聊聊我的个人项目。


我大概记得一些技术问题:



  • k8s 相关的一些组件、Operator

  • Go 相关的放射、接口、如何动态修改类实现等等。

  • Java 相关就是一些常规的,主要是一些常用特性和 Go 做比较,看看对这两门语言的理解。


其实这家公司是比较吸引我的,几乎就是围绕着开源社区做研发,工作中大部分时间也是在做开源项目,所以可以说是把我之前的业余爱好和工作结合起来了。


在贡献开源社区的同时还能收到公司的现金奖励,不可谓是双赢。


对我不太友好的是工作地在成都,入职后得成渝两地跑;而且在最终发 offer 的前两小时,公司突然停止 HC 了,这点确实没想到,所以阴差阳错的我也没有去成。


第四家


第四家也就是我现在入职的公司,当时是我在招聘网站上看到的唯一一家做中间件的岗位,抱着试一试的态度我就投了。
面试过程也比较顺利,一轮同事面,一轮 Leader 面。


技术上也没有聊太多,后来我自己猜测大概率也和我的博客和 Github 有关。




当然整个过程也有不太友好的经历,比如有一家成都的“知名”旅游公司;面试的时候那个面试官给我的感觉是压根没有看我的简历,所有的问题都是在读他的稿子,根本没有上下文联系。


还有一家更离谱,直接在招聘软件上发了一个加密相关的算法,让我解释下;因为当时我在外边逛街,所以没有注意到消息;后来加上微信后说我为什么没有回复,然后整个面试就在微信上打字进行。


其中问了一个很具体的问题,我记得好像是 MD5 的具体实现,说实话我不知道,从字里行间我感觉对方的态度并不友好,也就没有必要再聊下去;最后给我说之所以问这些,是因为看了我的博客后觉得我技术实力不错,所以对我期待较高;我只能是地铁老人看手机。


最终看来八股文确实是绕不开的,我也花了几天时间整理了 Java 和 Go 的相关资料;不过我觉得也有应对的方法。


首先得看你面试的岗位,如果是常见的业务研发,从招聘的 JD 描述其实是可以看出来的,比如有提到什么 Java 并发、锁、Spring等等,大概率是要问八股的;这个没办法,别人都在背你不背就落后一截了。


之后我建议自己平时在博客里多记录八股相关的内容,并且在简历上着重标明博客的地址,尽量让面试官先看到;这样先发制人,你想问的我已经总结好了😂。


但这个的前提是要自己长期记录,不能等到面试的时候才想起去更新,长期维护也能加深自己的印象,按照 “艾宾浩斯遗忘曲线” 进行复习。


选择



这是我当时记录的面试情况,最终根据喜好程度选择了现在这家公司。


不过也有一点我现在觉得但是考虑漏了,那就是行业前景。


现在的 C 端业务真的不好做,相对好做的是一些 B 端,回款周期长,同时不太吃现金流;这样的业务相对来说活的会久一些,我现在所在的公司就是纯做 C 端,在我看来也没有形成自己的护城河,只要有人愿意砸钱随时可以把你干下去。


加上现在的资本也不敢随意投钱,公司哪天不挣钱的话首先就是考虑缩减产研的成本,所以裁员指不定就会在哪一天到来。


现在庆幸的是入职现在这家公司也没有选错,至少短期内看来不会再裁员,同时我做的事情也是比较感兴趣的;和第三家有些许类似,只是做得是内部的基础架构,也需要经常和开源社区交流。


面对裁员能做的事情


说到裁员,这也是我第一次碰上,只能分享为数不多的经验。


避免裁员


当然第一条是尽量避免进入裁员名单,这个我最近在播客 作为曾经的老板,我们眼中的裁员和那些建议 讲到在当下的市场情况下哪些人更容易进入裁员名单:



  • 年纪大的,这类收入不低,同时收益也没年轻人高,确实更容易进入名单。

  • 未婚女性,这点确实有点政治不正确,但确实就是现在的事实,这个需要整个社会,政府来一起解决。

  • 做事本本分分,没有贡献也没出啥事故。

  • 边缘业务,也容易被优化缩减成本。


那如何避免裁员呢,当然首先尽量别和以上特征重合,一些客观情况避免不了,但我们可以在第三点上主动“卷”一下,当然这个的前提是你还想在这家公司干。


还有一个方法是提前向公司告知降薪,这点可能很多人不理解,因为我们大部分人的收入都是随着跳槽越来越高的;但这些好处是否是受到前些年互联网过于热门的影响呢?


当然个人待遇是由市场决定的,现在互联网不可否认的降温了,如果你觉得各方面呆在这家公司都比出去再找一个更好,那这也不失为一个方法;除非你有信心能找到一个更好的,那就另说了。


未来计划


我觉得只要一家公司只要有裁员的风声传出来后,即便是没被裁,你也会处于焦虑之中;要想避免这种焦虑确实也很简单,只要有稳定的被动收入那就无所谓了。


这个确实也是说起来轻松做起来难,我最近也一直在思考能不能在工作之余做一些小的 side project,这话题就大了,只是我觉得我们程序员先天就有自己做一个产品的机会和能力,与其把生杀大权给别人,不如握在自己手里。


当然这里得提醒下,在国内的企业,大部分老板都认为签了合同你的 24 小时都是他的,所以这些业务项目最好是保持低调,同时不能影响到本职工作。


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

10年技术进阶路,让我明白了这3件事

这篇也是我分享里为数不多 “进阶” 与 “成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。 十年...
继续阅读 »

这篇也是我分享里为数不多 “进阶”“成长经历” 的文章之一。被别人送到嘴边的食物永远是最香的,但是咱们还是得学会主动去"如何找吃的",授人以鱼不如授人以渔嘛,我希望通过这篇文章能给正在努力的你,迷茫的你,焦虑的你,带来或多或少的参考、建议或者指引。


十年,谁来成就你?


  离开校园,一晃已十年,时日深久,现在我已成为程序员老鸟了,从软件工程师到系统架构师,从被管理者到部门负责人,每一段经历的艰辛,如今回忆仍历历在目。各位同行你们可能正在经历的迷茫,焦虑与取舍,我也都曾经历过。


  今天我打算跟大家分享下我这些年的一个成长经历,以此篇文章为我十年的职业历程画上一个完满的句号。这篇文章虽说不是什么“绝世武功”秘籍,更没法在短时间内把我十年的“功力”全部分享于你。篇幅受限,今天我会结合过往种种挑重点说一说,大家看的过程中,记住抓重点、捋框架思路就行了。希望在茫茫人海之中,能够给到正在努力的你或多或少的帮助,亦或启发与思考。


试问,你的核心竞争力在哪?


  你曾经是否怕被新人卷或者代替?如果怕、担忧、焦虑,我可以很负责任地告诉你,那是因为你的核心竞争力还不够!这话并不好听,但,确是实在话。认清现状,踏实走好当下就行,谁能一开始或者没破茧成蝶时就一下子有所成就。


  实质上,可以这么说,经验才是我们职场老鸟的优势。 但是,经验并不是把同一件事用同一种方式重复做多少年,而是把咱们过往那么多年头的实践经验,还有被验证的理论,梳理成属于自己的知识体系,建立一套自己的思维模式,从而提升咱们的核心竞争力。


    核心竞争力的形成,并非一蹴而就,我们因为积累所以专业,因为专业所以自信,因为自信所以才有底气。积累、专业、自信、底气之间的关系,密不可分。


核心竞争力,祭出三板斧


  道理咱们都懂,能不能来点实在的?行!每当身边朋友或者后辈们,希望我给他们传授一些“功力”时,我都会给出这样的三个建议:



  1. 多面试,验本事。

  2. 写博客,而且要坚持写。

  3. 拥有自己的 Github 项目。 



  其中,博客内容和 Github 项目,将会成为咱们求职道路上的门面,这两者也是实实在在记录你曾经的输出,是非常有力有价值的证明。此外,面试官可以通过咱们的博客和 Github,在短时间内快速地了解你的能力水平等。或许你没有足够吸引、打动人的企业背景,也没有过硬的学历。但!必须有不逊于前两者的作品跟经历。


  再说说面试,我认为,它是我们接受市场与社会检验的一种有效方式。归根结底,咱们所付出的一切,都是为了日后在职业发展上走得越来越好。有朋友会说,面试官看这俩“门面”几率不大,没错,从我多年的求职经历来看,愿意看我作品的面试官也只占了 30%。


  但是,谁又能预判到会不会遇到个好机会呢?有准备,总比啥也没有强,千里马的亮点是留给赏识它的伯乐去发现的


PS:拥有自己 Github 项目与写博,都属于一种输出的方式,本文就以写博作为重点分享。写博与面试会在下文继续展开。


记忆与思考,经验与思维


  武器(三板斧)咱们已经有了,少了“内功心法”也不行。这里分享下我的一些观点,也便于大家后续能够更好地参与到具体的实践中。




  • 记忆——记忆如同对象一样是具有生命周期,久了不用就会被回收(忘记)。




  • 思考——做任何事情就如同咱们写代码Function一样,得有输入同时也得有输出,输入与输出之间还得有执行。






  •  




  日常工作中,就拿架构设计当例子。作为架构师是需要针对现有的问题场景提出解决方案,作为架构师的思考输入是业务场景、团队成员、技术选型等,而它的输出就是基于前面的多种输入参数从而产出的短期或长期的解决方案,而且最终会以文档形式保存下来。


  保存下来的目的,是为方便我们日后检索、回忆、复用。因此,在业余学习中同理,给与我们的输入是书籍、网络的资料或同行的传递等,而作为输出则是咱们记录下来的笔记、博客甚至是 Github 的项目 Demo。



基于上述,我们需要深刻意识到心法三要素:



  1. 带着明确的输出目的,才会真正地促进自己的思考。蜻蜓点水、泛泛而谈,是无法让自己形成对事物的独特见解和具象化输出,长期如此,并无良益。

  2. 只有尽可能通过深度思考过后的产出,才能够形成属于自己真正的经验。

  3. 知识的点与点之间建立联系,构成明晰的知识体系,经验与经验则形成了自己独有的思维模式。


多面试,验本事


  既然“武器”和“内功心法”咱们都有了,那么接下来得开始练“外功”了,而这一招叫"多面试,验本事"。


  我身边的同行与朋友,对我的面试行为感到奇怪:你每隔一段时间就去面试,有时拿到了 offer 还挺不错的,但是又没见想着跳槽,这是为何?


风平浪静,居安思危


  回应这个疑问之前,我想反问大家 4 个问题:



  1. 是否曾遇到过在一家公司呆了太久过于安逸,也阶段性想过离开,发现真要走可却没了跳槽的勇气?

  2. 再想一想,日子一久,你们是不是就不清楚行业与市场上,对人才能力的需求了?

  3. 是否有经历过公司意外裁员,你在找工作的时段里有没有强烈感受到那种焦虑、无助?

  4. 是否对来之不易的 offer,纠结不知道如何抉择,又或者,最终因为迫于各方面压力,勉为其难接受了不太中意的那个?



  刚提到的种种问题,那份焦虑、无助、纠结与妥协,我曾经在职场都经历过。我们想象一下,如果你现在随随便便出去面试五个公司能拿到三四个 offer,你还会有那失业的焦虑么?如果现在拿到的那几个 offer 正好都不喜欢,你全部放弃了,难道你会愁后续没有其他机会了么?显然不会!因为你有了更多底气和信心


  我再三思考,还是觉得有必要给大家分享一个我的真实经历。希望或多或少可以给你一点启发:


  2019 年,因为 A 公司业务原因,我离开了工作 3 年的安逸的环境,市场对人才的需求我已经是模糊的了,当我真正面临时,我焦虑、我无助。幸好曾经跟我合作过的老领导注意到了这我这些年的成长,向我施予援手。入职 B 公司后,我重新审视自己,并给与自己定了个计划——每半年选一批公司面试。


一年以后,因为 B 公司因疫情原因,我再次离职。这次,我没有了焦虑,取而代之的是自信与底气,裸辞在家开始了我的休假计划。在整个休假期,我拒绝了两个满足我的高薪 offer,期间我接了个技术顾问的兼职,剩余时间把以前囤下来的书看了个遍,并实践了平常没触碰到的技术盲区。三个月后,我带着饱满的精神面貌再次"出山",入职了现在这家公司。


  有人会问:你现在还有没有坚持自己的面试计划?毫无避讳回答:有!仍然是半年一次。


乘风破浪,未雨绸缪


  就前面这些问题、情况,这里结合我自己多年来的一些经验,也希望给到大家一点破局建议:保持一定的面试频率,就如上文提到的“三板斧”,面试是接受市场与社会检验,非常直接、快速、有效的一种好方式。 当然,我可不是怂恿你频繁跳槽,没有多少公司能够欣然接受不稳定的员工,特别是岗位越做越高时。


  看到这里,有些伙伴可能会想,我现在稳稳当当的、好端端的,干嘛要去面试,何必折腾自己。假若你在体制内,我这点建议或许参考意义不大。抛开体制内的讨论,大家认为真的有所谓的“稳定”的工作吗?


  我认为所谓的“稳定”,都是只是暂时的,甚至虚幻的,没有任何的人、资本、企业能给你实打实的承诺,唯一能让你“稳定”持续发展下去的,只有你的能力与眼界、格局等。


  疫情也有几年了,相信大家也有了更多思考,工作上,副业上等等各方面吧。人无远虑,必有近忧,未雨绸缪,实属必要!



放平心态,查缺补漏


  面试是相对“主观的”,这是因为“人性”的存在,你可能会听过让人哭笑不得的拒绝你的理由:



  • 连这么基础的知识都回答不上,还想应聘这岗位

  • 你的性格并不适合当管理,过于主动对团队不好


  咱们先抛开这观点的对与错。人无完人,每个人都有自己的优点与缺点,甚至你的优点可能是你的缺点。职场长路漫漫,要是把每一次的面试都当成人生中胜负的较量,那咱们最后可能会输的体无完肤。咱们付出任何的努力,也只是单纯提高“成功率”而已。听我一句劝,放平心态,以沟通交流为主,查漏补缺为辅


  近几年我以面架构师和负责人的岗位为主,面试官大多数喜欢问思想和方法论这类的问题,他们拥有不同的细节的侧重点,因此我们以梳理这些“公共”的点出发,事后复盘自己回答的完整性与逻辑性,对于含糊不清的及时找资料补全清晰,尝试模拟当时回答的场景。每一段面试,如此反复。


  作为技术人我建议,除了会干,还得会说,我们不仅有硬实力,还得有软技能。


PS:篇幅有限,具体面试经历就不展开了,如果大家对具体的面试经历感兴趣,有机会我给大家来一篇多年的"面经"。


持续进步


编程语言本身在不断进步,对于菜鸟开发者来说,需要较高的学习成本。但低代码平台天然就具备全栈开发能力,低代码程序员天然就是全栈程序员。


这里非常推荐大家试试JNPF快速开发平台,依托的是低代码开发技术原理,因此区别于传统开发交付周期长、二次开发难、技术门槛高的痛点,在JNPF后台提供了丰富的解决方案和功能模块,大部分的应用搭建都是通过拖拽控件实现,简单易上手,在JNPF搭建使用OA系统,工作响应速度更快。可一站式搭建生产管理系统、项目管理系统、进销存管理系统、OA办公系统、人事财务等等。


开源链接:http://www.yinmaisoft.com/?from=jueji…


狠下心来,坚持到底


锲而舍之,朽木不折;锲而不舍,金石可镂——荀况


  要是把"多面试"比喻成以"攻"为主的招式,而"写博客"则是以"守"为主的绝招。


  回头看,今年,是我写博客的第八个年头了,虽说写博频率不高,但整体时间跨度还是挺大的。至今我还记得我写博客的初心,用博客记录我的学习笔记,同时抛砖引玉,跟同行来个思维上的碰撞。


  随着工作年限的增长,我写博客的内容慢慢从学习笔记变成了实战记录,也越来越倾向于输出经验总结和实践心得。实质上,都是在传达我的观点与见解。


  而这,至关重要。反过来看,后面机会来了,平台联系人也可以借此快速评估、判断这人会不会讲、能不能讲,讲得怎么样,成的话,人家也就快速联系咱了。进一步讲,每一次,于个人而言,都是好机会。



写博第一步,从记笔记开始


  我相信不少的同行曾经面临这样的境况,都有产生过写博客的念头,有些始终没有迈出第一步,有些中途停了下来,这里可能有不少的原因:要么不知道写什么、要么觉得写了也没人看、还有一种是想写但是比较懒等等。


我觉得,一切的学习,前期都是从模仿开始的 学习笔记,它就是很好的便于着手的一种最佳方式。相信大家在学生年代或多或少都写过日记,就算是以流水账的方式输出,博客也可以作为非常好的开启平台。


  由于在写博客的时候,潜意识里会认为写出来的东西会给更多人看,因此自己写的内容在不明确的地方都会去找资料再三确认,这是很有效的一种督促方法。确认的过程中,也会找到许多相关的知识点,自然而然就会进一步补充、完善、丰富我们自己原有或现在的知识体系


幸运,需要自己争取


  在写博客的这段时间里,除了梳理自己的知识体系之外,还能结交了一些拥有共同目标的同行,我想,这就是真正的志同道合吧。


  甚至在你的博客质量达到了一定程度——有深度与广度,会有一些意象不到的额外小收获。例如有一些兼职找到自己,各大社区平台会邀请自己合作,也会收到成就证明与礼物等等。



意外地成为了讲师


  到目前为止,正式作为讲师或者是技术顾问,以这样不同于往常的既有角色,我真切地经历了几次。虽次数不多,但每一次过后,即便时日深久,可现在回想起来,于我的成长而言,那都是一次又一次新的蜕变,真实而猛烈,且带给我一次次新生力量。


  话说回来,前面提到几次分享,有的伙伴可能会说了,这本来就性格好又爱分享的人,个例罢了,不一定适合大多数啊。说到这儿,我想,我有必要简短地跟你聊一下我自己。


跌跌撞撞,逆水行舟


  对于过往的自己,我的评价是从小就闷骚、内向的那种性格,只要在人多的时候发言就会慌会怂会紧张,自己越慌就越容易表达出错,如此恶性循环。随着我写博的篇幅越多,慢慢地我发现自己讲话时喜欢准备与思考,想好了再去表达,又慢慢地讲话就具有条理性与逻辑性了。


  当代著名哲学家陈嘉映先生,他曾在一本书里说过这样一句话,放到这里再合适不过了—— "成长无时无刻不是在克服某些与生俱来的感觉和欲望"


  回头看,一路走来,我从最初的摸索、探索、琢磨,到看到细微变化,到明显感知到更大层面的进步,再到后来的游刃有余,输出很有见地的思考,分享独到观点。


  我想,这背后,离不开一次次尝试,一次次给自己机会,一次次认真、负责地探索突破自己。其实,大多数人,还真是这么跌跌撞撞挺过来的。


伺机而动,用心准备


  2020 年,我第一次被某企业找到邀请我作为技术顾问是通过我的博客,这一次算是小试牛刀,主要以线上回答问题、交流为主。因为事先收集好了需要讨论的话题与问题,整个沟通持续了两个小时,最终也得到了对方老板的高度认可。


  此事过后,我重新审视了自己,虽然我口才并不突出,但是我基于过往积累的丰富经验与知识融合,并能够正确无误地传达输出给对方,我认为是合格的了。坦率来讲,从那之后我不再怀疑自己的表达能力。同时有另外一件事件更值得重视,基于让自己得到更多更广泛的一个关注,思前想后,概括来讲,我还是觉得落到这句话上更合适,就是:建立个人 IP


建立个人 IP


  那么,我希望打造个人 IP 的原因是什么呢?希望或多或少也可以给你提供一点可供借鉴、探讨的方向。


  我个人而言,侧重这样几个层面吧。



  1. 破局: 一个是我希望打破 35 岁魔咒,这本质上是想平稳快速度过职业发展瓶颈期;

  2. 觅友: 希望结识到拥有同样目标的同行,深度交流,构建技术圈人脉资源网;

  3. 动力 从中获取更多与工作不一样的成就感。有了强驱动力,也会使我在分享这条路上变得更坚定。


链接资源,提影响力


  在《人民的名义》里祁同伟说过一句话,咱们就是人情的社会。增加了人脉,就是增加自己的机会。当然前提是,咱们自己得需要有这个实力。


  建立个人 IP,最要提高知名度,而提知名度的主要方式是两种:写书、做讲师。后面我会展开讲,写书无疑是宣传自己的最好方式之一,但整个过程不容易,周期比较长。作为写书的简化版,我们写博客就是一种捷径了。


主动出击,勿失良机


  而作为讲师,线上线下各类形式参与各种社区峰会露脸,这也是一种方式。不过这种一般会设有门槛。


  这里不得不多提一句,就是建立 IP 它是一个循序渐进的过程,欲速则不达,任何时候咱们都得靠内容作品来说话, 当你输出的质量够了,自然而然社区人员、企业就会找到你,机会顺理成章来了。反过来讲,我们也得常盯着,或者说多留心关注业内各平台的内容风格,利用好业余零碎时间,好好梳理下某个感兴趣的内容平台,看看他们到底都倾向于打造什么样的东西。做到知己知彼,很重要。


  我认识的一个前辈,之前阿里的,他非常乐于在博客上分享自己的经验与见解,随着他分享的干货越多,博客影响力越大,某内容付费平台找到他合作出了个专栏,随着专栏的完结,他基于专栏内容又出了一本书,而现在的他已经离开了阿里,成为了自由职业者。


追求成就感,倒逼突破自我


  每一次写博客、做讲师,都能更大程度上填满我内心深处的空洞,或许是每一个支持我的留言与点赞,或许是每一节分享停顿间的掌声。如果我们抱着非常强的目的去做的时候,可能会事与愿违。就以我做讲师来说,因为我是一个新手,在前期资料准备所花费的精力与时间跟后续的课酬是不成正比的。


  作为动力源,当时我会把侧重点放到结交同行上,同时利用“费曼学习法”重新梳理知识,另外寻找机会突破自己的能力上限。



  大家有没有想过,讲课最终受益者的是谁?有些朋友会回答“双方”。但是我很负责任地告诉你,作者、讲师自己才是最大的知识受益者。


  如前面所讲,写博客为了更好地分享出更具价值性的内容,为保证专业性,咱们得再三确认不明确的点,而讲课基于写博客的基础上,还得以听众的角度,去思考、衡量、迭代,看看怎么让人家更好地理解、吸收、用得上这些知识点,甚至讲师得需要提前模拟、预估可能会在课后被提的问题。


这里总结一下,写博客与讲课的方式完全不同,因为博客是以图、文、表的方式展现,读者看不明白可以回头去看,但是讲课则没有回头路,是一环套一环的,所以梳理知识线的连贯性要求更强


  我个人认为,日常工作大多数是重复的、枯燥的,或者说,任何兴趣成了职业,那将不再是兴趣,或许只有在业余的时候获取那些许的成就感,才会重新燃起自己的那一份初心 ——行之于途而应于心。


源不深而望流之远,根不固而求木之长


  求木之长者,必固其根本;欲流之远者,必浚其源泉——魏徵


  有些同行或许会问:”打铁还需自身硬“这道理咱们都懂,成长进阶都离不开学习,但这要是天天写 BUG 的哪来那么多时间学?究竟学习的方向该怎么走呢?在这里分享下我的实际做法,以及一些切身的个人体会,希望可以提供一点借鉴、参考。


零碎时间,稳中求进


  6 年前,我确定往系统架构师这个目标发展的时候,每天都会做这么两件事:碎片化时间学习,及时产出笔记。



  • 上班通勤与中午休息,我会充分利用这些碎片时间(各 30 分钟)尽可能地学习与吸收知识,每天坚持一小时的积累,积少成多,两年后你会发现,效果非常可观,这就是一个量变到质变的过程


  而且有神经科学相关表明,”间歇式模块化学习的效果最佳,通勤路上就是实践这种模式的理想世界。“大家也可以多试试看。当然,一开始你学习某个领域的知识,可能效率没那么高,我建议你可以反复地把某一节掰开了揉碎了看或者听,直到看明白听懂了为止,接着得怎么做?如我前面说,咱们得要有输出!


  看过这样一段话,”写和想是不同的,书写本身就是逻辑推演和信息梳理的过程。“而且,研究表明,”人的记忆力会在 17-24 岁达到高峰,25 岁之后会下降,理解力的发展曲线会延后 5 年,也就是说在 30 岁之后也会下降。“


  你看,这个也直接或者间接告诉我们,还是趁早多做记录、多学习。文字也好,视频也罢,到底啥形式不重要,适合自己能长久坚持的就行,我相信你一定能从中受益。毕竟,这些累积的,可都是你自己实实在在的经验和思考沉淀!


  话说回来,其实做笔记能花多长时间,就算在工作时间花半小时也有良效,而这时间并不会对自己的工作进度造成多么大的影响,但!一定时日深久,受益良多。


构建知识 体系 丰富 思维 模式


  由于我们日常需要快速解决技术难题,很多时候从外界吸收到的知识点相对来说很零散,而知识体系是由点、线、面、体四个维度构造而成的


  那怎么做能够快速把知识串联起来呢?这里我举个简单的例子,方便大家理解。


  以我们系统性能调优出发,首先我们需要了解系统相关性能瓶颈的业务场景是什么?该功能是 I/O 密集型还是 CPU 密集型?如果是 I/O 密集型多数的性能瓶颈在数据库,这个时候我们就得了解数据库瓶颈的原因,究竟是数据量大还是压力大?如果是数据量大,基于现有的业务场景应该选择数据归档、临时表还是分库分表,这之间的方案优缺点有什么不同?适用场景怎么样?假如是数据压力大了,我们是否能用 Redis 做缓存抗压就行?


  再接着从 Redis 这个点继续思考,假如 Redis 内存满了会怎样?我们又了解到了 Redis 的内存淘汰策略,设置了 volatile-lru 策略,由于我们基本功扎实回忆起 LUR 算法是基于链表的数据结构,虽然链表的写的时间复杂度是 O(1),但是读是 O(n),不过我们得先读后写,所以为了高性能又选择 Hash 这种 O(1)的数据结构辅助读的处理。


  你看,我们是不是从问题出发到架构设计,再从数据库优化方案到 Redis 的使用,最后到数据结构,这一些系统的知识就串联起来了?



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

我是埋点SDK,看我如何让甲方爸爸的页面卡顿10s+

背景音: Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK... 昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就...
继续阅读 »

背景音:



Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK...



昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就开始有用户反馈说网页卡卡地,走得比蜗牛还慢。


六点二十分,第一个用户提交了投诉工单,但这只是个开始。


今天早上九点十分,公司的运维团队已经接到了一大堆反馈工单,许多用户都遭受到了同样的问题。这是一个巨大的问题,一旦得不到解决,可能导致数万的用户受到影响。运维人员立即开始了排查工作,想要找出问题所在。


经过一个小时的紧急排查,他们终于想到了昨日的这名前端程序员,一经沟通发现是SDK版本更新引起的问题。在新的版本中,有一些不稳定的代码导致了性能问题。


然而,这不仅仅是个技术问题,因为接下来,他们要开始着手写事故报告,准备给上层领导交代。


接下来,进入正题:


一、问题排查定位


根据更新的版本体量,可以缩小和快速定位问题源于新引入埋点SDK



  1. 打开 开发者工具-性能分析,开始记录

  2. 刷新页面,重现问题

  3. 停止记录,排查分析性能问题


性能分析


如上图,按照耗时排序,可以快速定位找到对应的代码问题。


首先把编译压缩后的代码整理一下,接下来,深入代码一探究竟。


代码耗时.png


⏸️暂停一下,不妨猜猜看这里是为了干嘛?


🍵喝口茶,让我们沿着事件路径,反向继续摸清它的意图吧。


image.png


这里列举了231个字体名称,调用上文的 detect() 来分析。


⏸️暂停一下,那么这个操作为什么会耗时且阻塞页面渲染呢?


...


休息一下,让我们去看看这段代码的来龙去脉。


上面我们大概猜到代码是用于获取用户浏览器字体,那就简单检索一下 js get browser font


搜索结果.png


代码示例.png


证据确凿,错在对岸。


二、解决问题


相信大家也看出来了,我不是埋点SDK,我也不是甲方爸爸,我只能是一位前端开发。


联系反馈至SDK方,需要走工单,流程,而这一切要多少时间?


我唔知啊!领导也不接受啊!


👐没办法,只能自己缝补缝补了。


那么如何解决呢?



  1. 尝试修复 getFonts detect 字体检测逻辑,避免多次回流导致性能问题




image.png




  1. 缩短待检测字体目录。


人生苦短,我选方案3,直接修改返回值,跳过检测

getFonts () { return 'custom_font' }

那么让我们继续搬砖吧。



  1. 寻根


image.png


首先找到 SDK 加载对应 JS 的加载方式,看看能不能动点手脚。
这里可以看到,是采用很常见的 通过 appendScript loadJs 的方案,那么就可以复写拦截一下 appendChild 函数。



  1. 正源


通过拦截 appendChild,将SDK加载的JS改为加载修复后的JS文件。


核心代码如下:

var tempCAppend = document.head.appendChild
document.head.appendChild = function (t) {
if (t.tagName === 'SCRIPT' && t.src.includes('xxx.js')) {
t.src = 'custom_fix_xxx.js'
}
return tempCAppend.bind(this)(t)
}

三、后续


这件事情发生在21年底,今天为什么拿出来分享一下呢?


近期排查 qiankun 部分子应用路由加载异常的时候,定位到与 document.head.appendChild 被复写有关,于是去看SDK方是否修复,结果纹丝未动....


结合近期境遇,不得不感慨,业务能不能活下去,真的和代码、技术什么的毫无关系。


其他


❄️下雪了,简单看了几眼文心一言的发布会,更凉了。


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

外包仔的自我救赎

本人96年后端Javaer一枚,现在在某知名大厂做外包仔(一入外包深似海,从此自研是路人)。 为什么做外包仔? 开始是没得选 毕业的第三年,通过培训班转行Java,包装了两年经验。非科班出身又是半路出家,当时也不懂外包的概念,于是就糊里糊涂进了外包公司。...
继续阅读 »
本人96年后端Javaer一枚,现在在某知名大厂做外包仔(一入外包深似海,从此自研是路人)。

为什么做外包仔?



开始是没得选



毕业的第三年,通过培训班转行Java,包装了两年经验。非科班出身又是半路出家,当时也不懂外包的概念,于是就糊里糊涂进了外包公司。第一家公司只干了三个多月就跑路了,一方面是工资太低(8K),另一方面是技术比较老旧(SSH)。第二家公司也是外包,但是项目还不错(spring cloud),薪资也可以接受(12K)。



后来是给的多



做开发工作的第二年,跳槽时本来想着找一家自研公司,但是没忍住外包公司开的价格,一时脑热又进了外包,也就是现在这家大厂外包。薪资比较满意(18K),项目也很不错(toC业务,各种技术都有涉及)。


下定决心跳出外包



为什么要离开



干过外包的小伙伴们多多少少会有一些低人一等的感觉,说实话笔者自己也有。就感觉你虽然在大厂,但你是这里身份最低的存在,很多东西是需要权限才能接触到的。再者就是没有归属感,没有年会、没有团建、甚至不知道自己公司领导叫什么(只跟甲方主管和外包公司交付经理有接触)。



潜心修炼技术



在最近这个项目里确实学到了很多生产经验,自己写的接口也确实发生过线上故障,不再是单单的CRUD,也会参与一些接口性能的优化。对业务有了一定的的理解,技术上也有了一定的提升,大厂的开发流程、开发规范确实比较健全。



背诵八股文



三月份开始就在为跳槽做准备,先后学习了并发编程、spring源码、Mysql优化、JVM优化、RocketMQ以及分布式相关的内容(分布式缓存、分布式事务、分布式锁、分布式ID等)。学到后面居然又把前面的忘了


大环境行情和现状



大范围裁员



今年从金三银四开始,各大互联网公司就都在裁员,直到现在还有公司在裁员,说是互联网的寒冬也不为过。笔者所在的厂也是裁员的重灾区,包括笔者自己(做外包都会被优化,说是压缩预算)也遭重了,但是外包公司给换了另外一个项目组(从北京换到了杭州)。



招聘网站行情



笔者八月份先在北京投了一波简历(自研公司,外包不考虑了),三十多家公司只有一家公司给了回应(做了一道算法笔试题,然后说笔者占用内存太多就没有后续了),九月中旬又在杭州投了一波简历(也是只投自研),六十多家公司回复也是寥寥无几,甚至没约到面试(有大把的外包私聊在下,是被打上外包仔的标签了吗)。


如何度过这个寒冬



继续努力



工作之余(摸鱼的时候),笔者仍然坚持学习,今天不学习,明天变垃圾。虽然身在外包,但是笔者仍有一颗向往自研的心,仍然想把自己学到的技术运用到实际生产中(现在项目用到的技术都是甲方说了算,当然我也会思考哪些场景适合哪些技术)。



千万不要辞职



现在的项目组做的是内部业务,并发几乎没有,但是业务相对复杂。笔者只能继续狗着(简历还是接着投,期望降低一些),希望互联网的寒冬早日结束,希望笔者和正在找工作的小伙伴们早日找到心仪的公司(respect)。


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

环信十周年趴——我的游戏人生

   我觉得热爱电子游戏的八零后和九零后,真是太幸福了。     在那个宣传电子游戏是电子迷幻剂的时代,恰巧也是电电子游戏最飞速迅猛发展的时代,日新月异的计算机硬件发展,带来的是游戏画面、游戏玩法以及游戏综合素质的大幅提...
继续阅读 »

   我觉得热爱电子游戏的八零后和九零后,真是太幸福了。


    在那个宣传电子游戏是电子迷幻剂的时代,恰巧也是电电子游戏最飞速迅猛发展的时代,日新月异的计算机硬件发展,带来的是游戏画面、游戏玩法以及游戏综合素质的大幅提升,作为热爱游戏的我,有幸经历过这个时代,并且伴随着那些时间烙印的游戏,连同点点滴滴的回忆,一同化作成为了现在对我来说十分珍贵的青春纪念册。


    我的童年生活在一个部队院校的家属院中,那所院校位于中部省份一个县的边陲地区,我小时候经常会和几个同在家属院中生活的小伙伴,一起去在学院的各个地方玩闹。广阔的部队操场,四百米障碍场,荒芜的大片作训场,一座一座具有苏联特色的小楼都是我们玩耍的“宝地”。有一次我们跟着其中一个小伙伴进到一个学员宿舍楼,看到学员的娱乐室有几台电脑正在开着,上面正在运行着“反恐精英”,以及“红色警戒”等游戏,学员都很热情,也让我们玩了玩,我却连鼠标怎么操作都不知道,只好笑哈哈的站起来将电脑还给他们。在这里,我第一次接触到了电子游戏。后面我们又去过几次,有学员教我们如何玩名为“警察抓小偷”的游戏,很简单的操作,只需要控制方向键以及一个键就可以,这带给了我一种独特的欢乐,也给我留下了深刻的印象。


    再后来,到了上小学的时候,家属院内的房子要分配给新的教工们,我们将部队房子腾退了,随家里大人搬到省会的一个部队院校承建的小区,院校的很多职工干部也都在这里安了家,于是童年的几个发小又聚集在一起了。小孩子的精力总是旺盛的,毕竟那是一个用一根树枝挖土都可以玩一下午的年纪。不满足于在小区里乱窜,我们也会去各个发小的家里互相串门。游戏王的卡牌、数码宝贝的“拍片儿”,还有现在列做违禁品的仿真玩具枪,各式各样的小玩意,每过一段时间大家手里的小玩意都会同一时间更换掉,然后又有新的东西出现。我们一直在探索发现新的东西,直到在一个发小家里看到了电脑游戏。


    我仍然记得第一次真正的接触到游戏的那天,正值盛夏,午后太阳便被乌云层层遮住,下午两三点钟的天气阴沉的如同水墨画一般,随意泼洒在天空,伴随着远处时不时传来的轰隆隆的雷声。我们只好聚在一个发小的家里,一起坐在电脑前,看着他玩起了游戏。游戏的开场是一辆跑车疾驰而过无人的街道,然后是枪声传来,几个游戏角色狂奔出来,接着主角被放倒了,然后游戏转场到另一个雨夜,主角坐在囚车上,被一伙帮派人员顺带着劫出,然后便正式开始了。恰巧,游戏中也是雷雨天,而当时窗外也应景的下起了雨,那副场景深深地烙印在了我的脑海中。


    那款游戏叫《侠盗猎车3》,又名GTA 3。


    我们一起玩的很开心,在拙劣的操控和因画面拖影而造成不流畅的画面下,大家谁也没有通过第一个任务到达终点,我们都嘻嘻哈哈的看着其中一个小伙伴开着汽车翻车,爬出车外,然后又重新回到开场的地点,直到傍晚,雨停下来了,天空中的乌云也消散了,露出蓝灰色的天空,夕阳为剩下的云镶嵌了一圈金边儿。我们也互相道别,各自回去自己的家了。


    这款游戏是我第一次接触的游戏,从此就好像留下了记忆烙印一般。我深深地迷上了这个游戏。又过了一年,家里终于换了电脑,那是一台奔腾处理器,只有几十兆内存和八十千兆硬盘的方正电脑,配上的是传统的17寸大脑袋显示器,这台电脑伴随我度过了小学时期,我从外面买来了当时几元一张的盗版游戏光盘,安装上了侠盗猎车3,靠着从各处搜集来的资料和盗版盘上附带的游戏攻略,我也慢慢的接触到了剧情,完成一个又一个游戏中的任务。到后面又接触了罪恶都市、过山车大亨2、包青天七侠五义、合金弹头之类的游戏,由于当年能够接触的游戏少,每一款我都玩得很细致,这些游戏到现在我还会偶尔打开,游玩一会。


    快上初中时,老电脑也来到要更换的时候了,我已经忘记了哪个部件出现了损坏,长辈便又去买了一台电脑主机回来,第二款主机的配置增强了一些,显卡是GeForce 7300LE,靠着显卡和处理器的升级,我玩上了诸如圣安地列斯、马克思佩恩、荣誉勋章血战太平洋、极品飞车、战地等游戏,这些游戏在我看来画面更为精致,场景更加宏大,内容也更加丰富,它们又陪伴我度过了一段美好的时光。


    到了上初中以后,那台17寸显示器也不堪重负坏掉了,在我向家长的恳求下,家里的电脑便被更换为19寸液晶显示器和一套全新的主机,显卡更换为了Nvidia 9600GT,处理器也更换为了AMD的羿龙x2系列处理器。我便彻底接触到了第七世代(即微软Xbox 360和PS3游戏机热销的时候)的那些经典大作。诸如侠盗猎车4、使命召唤4、5、6、无主之地、刺客信条、孤岛危机、战地叛逆连队和战地3等游戏,每个游戏中的场景都宏大真实,各有特色,它们带我开启了“高成本、高质量、高体量游戏,即3A大作”的大门。同期还有些腾讯的网游,诸如穿越火线、QQ飞车、QQ三国,我也和同学一起玩过,但是我对那些游戏感到兴趣缺缺,也对网络游戏中需要签到、完成日常任务的模式感到疲劳,因此也只是基本有过接触。


    高中毕业的那个暑假,家长又一次同意了我更换配置的请求,因此电脑升级成AMD的FX4100处理器、8G内存、HD6770显卡,以前的中低特效运行的游戏变为了高特效运行,我因此玩到了更多同期的新游戏、诸如质量效应3、虐杀原形2、使命召唤8,上古卷轴5等作品。


    大学时期,我用的是一台戴尔的笔记本,当时的配置是i7 3632QM处理器、 GT 640M显卡、1TB的机械硬盘以及8G的内存,但是彼时也正是游戏机平台从PS3向PS4更新换代的时期,一些新出的游戏作品为了适配最新的PS4游戏机,对配置要求都大幅增加。这台电脑的显卡和家里的台式机电脑显卡性能类似,因此我没有再接触那些新出的游戏作品,主要玩的游戏也转变为当时大热的英雄联盟、反恐精英Online,还有之前接触的一些网络游戏之类。


    毕业以后,我从事iOS开发的工作,在新的城市中,奔波于工作、租房以及各式各样其他的事情里,也没空接触游戏了,直到2017年,工作几年的我,因收到绝地求生的大火影响,也想跃跃欲试体验一把吃鸡的快感,便在那年的618下单了一台神舟的笔记本,这次完全是我自己挑选的型号以及配置。处理器是Intel的i5 6300HQ,显卡是GTX 1060,再加上我自己更换的屏幕,增加的128gb固态硬盘,以及16gb的内存。绝地求生、使命召唤、生化危机、巫师3,侠盗猎车5,这些游戏又带我走进了一个个更加宏大、真实的游戏世界中。购买了这台笔记本后,我如同打开了一个全新游戏世界的盒子,再过了半年,我又买到了PS4,在此之前我接触的游戏机只有一台老旧的索尼PS1,还有一台亲戚送的Wii体感游戏机,尽管冒着极低的分辨率,我仍然通关了天诛、最终幻想、恐龙危机,还有Wii平台的生化危机0,生化危机4这些游戏。买到PS4以后,神秘海域,最后生还者,战神4,血源诅咒这些游戏让我领略到了动作游戏的独特魅力。便尝试购买各类游戏主机,再之后两年多的时间里,我购买过Xbox one和Xbox one S,任天堂Switch,PS3,Xbox 360,还有Playstation vita以及好几台3ds、我购买这些游戏机后,会先安装那些在我青少年时期玩不到,或一直期待的游戏,然后再把它们卖出。到疫情前,我的手里还有一台PS4的升级版PS4 Pro,一台Xbox one S以及一台任天堂switch。


   疫情来势汹汹,我的收入也因经济原因下跌,我不得不卖掉这些游戏机,好能够度过那些困难的日子。就这样,时间来到2022年的夏天。这个时候的我,仅仅有一台微软最新的Xbox Series X,在前几年购买的笔记本也扔到了老家里。就在那年夏天的某一天,我坐在家里,正听着窗外的蝉鸣,感受空调吹来的风时,突然决定我想要买一台主机来玩。说干就干,没多久我便从二手购物平台买到一台在当时已经过时的电脑主机,买家标价只有千余元,配置是一台搭载AMD 锐龙六核处理器的2600,华硕的A320M主板,不带显卡,内存16G,加上120g的固态硬盘,400瓦的电源,我又额外购买了一片蓝宝石RX 580显卡。这台电脑也算是组成了,在拼凑完成并打开它的一瞬间,我心中的喜悦之情简直无以言表,我意识到组装电脑带来的快乐和童年时期接触到游戏的那些惊喜一样。尽管过去的那些年我没有过多的研究电脑硬件,但是我仍然知道我购买的是一台在疫情前就十分热销的主流游戏配置。有了这次令我满足的经历没多久,我又开始觉得显卡游戏性能似乎有些不够,便又开始在二手平台搜罗,没多久买到了一块GTX 1070显卡,这是六年前的高端型号,我很满足这片显卡的性能。不出意外的,没几天回去,我又开始的置换的道路,我不断的买入二手显卡,并卖出之前收购到的旧显卡,在踩了两次坑,交了两次学费后,我终于收到一片品相非常好的七彩虹RTX 2070 Super显卡。同时对于处理器,我也没有停下更新的脚步,从R5 2600,再到R5 3600X,再到R5 3800X,电源也更换成了600瓦的新电源。更换完这些配件后,我看着眼前的电脑,突然觉得索然无味,没多久,我便将整机一同挂出,卖给了一位同城的用户。


    在这次经历后,我发现我对电脑硬件的折腾几乎可以比肩队游戏的热情,卖出电脑后没两个月,我又开始撺掇着自己再配一台电脑了,不出两周,一台搭载着AMD R5 5500处理器,AMD RX 5700的显卡,16g内存和1tb固态硬盘的侧透玻璃机箱便组成了,我安装好了系统,玩的却是和之前一样的游戏,令我感觉意兴阑珊,年后,这台主机也一并卖出给了一位同城的买家。


    2023年的春天,万物都显示出欣欣向荣的样子,我心中那股对电脑硬件的热情也一并复苏了。我开始挑选硬件,思考要一台什么样的配置。起初,我的预想是用几千元预算,配置一台低配置处理器和入门级显卡的电脑,然而看到各类购物平台在淡季时低价销售各类配件后,我逐渐将配置一层一层加高,经过不断的购买和置换,从AMD的R5,到Intel的i5,再到i7,主板也从适配AMD处理器的B450主板,到Intel处理器的B660主板,再到B760主板。显卡更是层层加码,从RTX 3060Ti,到RTX 3070Ti、RTX 3080………,散热器也从风冷更换为了水冷,还加了灯效风扇。在这篇文章写成时,这台电脑已经变成了一台Intel十二代i7处理器、RTX 3080Ti的前代旗舰配置了,我问过自己,折腾的这一切是为了什么?然后我给出了自己的回答,大概是源自热爱吧。


    敬游戏玩家。



本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026


收起阅读 »

老菜鸟为什么喜欢代码重构

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗? “今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼? “君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另...
继续阅读 »

“花有重开日,人无再少年”,诸位,如果再给你们一次机会还会选择做个码农吗?


“今年花胜去年红,可惜明年花更好”,诸位,对码农生涯还有多少期盼?


“君看今日树头花,不是去年枝上朵”,诸位,可曾想过:如果在几年前便拥有现在的技术、薪资,面对生活、谈女朋友会不会是另外一番滋味呢...


好了!扯多了,还是谈一谈正题:老菜鸟为什么喜欢代码重构!


屎山上挖呀挖


最近几个月流行的"花园种花"跟大家分享一下:


222213.jpg


小朋友们准备开始
在小小的花园里面 ~ 挖呀挖呀挖 ~ 种小小的种子 ~ 开小小的花
在大大的花园里面 ~ 挖呀挖呀挖 ~ 种大大的种子 ~ 开大大的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花
在什么样的花园里面 ~ 挖呀挖呀挖 ~ 种什么样的种子 ~ 开什么样的花
在特别大的花园里面 ~ 挖呀挖呀挖 ~ 种特别大的种子 ~ 开特别大的花


每天对着自己项目中的代码,真的想改编一下:


在小小的屎山里面   ~ 挖呀挖呀挖 ~ 
在大大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~
在特别大的屎山里面 ~ 挖呀挖呀挖 ~

没被铲走的屎山就是好屎山



诸位,想不想一睹屎山真容?----去你自己项目上找吧,哈哈; 重构并不一定是因为屎山,屎山可能不一定需要重构





  1. 于公司、项目而言,能正常运行、满足客户需要的代码就是OK的,甭管它屎山还是花园,能帮公司赚到钱的就是王道也!




  2. 为什么会有屎山的存在,源于项目早期的设计、架构不合理、中后期编码不规范、或者说压根就没有架构、没有规范;




  3. 很多大厂或一些比较讲究的公司,有自己的架构师、质量团队,那他们产出的项目质量就会非常高,就算是重构也可能是跨代、技术层面的升级,绝非屎山引起的重构!




  4. 爷爷都是从孙子过来的,谁还没个小菜成长记呢,谁小时候(初级阶段)还没(写过一些垃圾代码)在裤子上拉过屎呢;能接受别人的喷说明你心智成熟了,能发现项目中的糟糕代码说明你技术成长了;




  5. 最重要的一点,用发展的眼光看问题,以当下的技术、潮流去审视多年前的项目,还是要充满敬意,而不只是吐槽!




优化


你需要可能是优化


重构并不一定是因为屎山,可能是技术自身的转换升级; 屎山可能不一定要重构,或许只需要做代码的优化; 倘若项目的技术栈还比较年轻,那我们面对的可能是优化,而不是重构;



那糟糕的代码源自何处呢?


  • 曾经的你? 曾经梦想仗剑走天涯,如今大肚秃顶撸代码;

  • 已经离职的同事?人走茶凉,雁过拔毛;


话说谁还不是从小菜成长起来的呢,当你发现项目上诸多不合理时,说明你的技术已经成长、提高了不少;



  • 可能你接手了一个旧的项目,看到的代码是公司几年前的产品设计

  • 可能你自己今年前写的代码,因为项目赚钱了,又要升级

  • 可能你接手了(不太讲究的)同事的代码

  • 可能就是因为赶进度,只要功能实现


如果仅仅是代码不够友好,我们需要的或许只是长期优化了...


网友谈重构



当你看到眼前的屎山会作何感想? TMD,怎么会会会有如此的代码呢,某某某真**,ε=(´ο`*)))唉? 正常,如果你没有这样的感慨,下文就不用看了,直接吐槽就行了...



看看网友回复



  • 看心情




  • 别自己找trouble




  • 又不是不能用,你领导怕成本太高吧,新项目可以用新架构




  • 除非你自己愿意花时间去重构,不然哪个老板舍得花这个钱和时间




  • 应该选择成为领导,让底下人996用新技术重构
    .... 下面的更绝




  • 小伙子还年轻吧,动不动就重构




  • 代码和你 有一个能跑就行
    哈哈,太多了,诸位,你会怎么想,面对糟糕的代码你会重构吗?




cg3.jpg


问题



当你发现问题时说明你用心了;当你吐槽槽糕代码时,说明你技术提升了;当你想爆粗口时说明你对美好生活是充满向往的;



那么,你的项目上可能有哪些问题呢(以前端代码为例)?



  • 技术栈过于古老

  • 架构设计不合理

  • 技术选型、规范不合理

  • 不合理的三目运算符

  • 过多的if嵌套

  • 回调地狱

  • 冗余的方法、函数

  • 全局变量混乱

  • 项目结构混乱

  • 路由管理糟糕

  • 状态数据管理混乱

  • CSS样式样式混乱

  • .....
    .... 哪些该重构,哪些该优化?


sikao6.jpg


机会



“祸兮福之所倚,福兮祸之所伏”, 问题的背后往往就是机会;



路人甲: 明明就是一座屎山,又何来机会一说?有扒拉屎山的功夫,搞点新技能,搞点原创不开心吗?答案是肯定的


路人乙: 解决屎 OR 新项目搭建,会选择哪个呢? 我想脑子正常点的人应该会选择重新搭建吧!


个人觉得对于经验比较丰富的开发当然选容易的,对于经验不丰富的开发而言当然也选容易的!对于有一定基础 && 想要快速提升综合能力者,解决屎山或许别有一番滋味,未尝不是一件闻起来臭吃起来香的(臭豆腐)幸事;



  • 理解老旧项目的初始架构、设计有助于了解、理解技术发展的脉络

  • 有很多老旧项目的设计、架构是非常优秀的,值得去深入学习背后的思想

  • 重构的整个过程也是不断审视自己不足的过程,查漏补缺,提升最短的那块板

  • 一切技术服务于业务,重构的过程也是深入理解业务的过程

  • 有机会重构是一种幸福,面对诸多压力本身就是是一种心智的磨练,不经历风云怎能见彩虹; 大成功者必是大磨难者!


挑战



问题的背后是机会, 机会的身边往往伴随着诸多挑战;面对重构不去争取,那老油条和小菜鸟有有什么区别(什么价值),不要等到退出行业了,再说: “曾经有一次重构的机会摆在我面前,我没有珍惜,等到了失去的时候才后悔莫及,尘世间最痛苦的事莫过于此。如果老板可以再给我一个再来一次的机会的话,我会说:重构!重构! 重构!。如果一定要加一个期限的话,我希望是公司设定的时间之前!”



QQ截图20230628143530.jpg



  • 时间:领导、公司会不会给予足够的时间深入重构,万一没有如期完成呢?

  • 回报:重构是否能达到预期,是否能带来质变级的体验,万一重构后没成功呢?

  • 能力:自身能力是否能承担得起重构的重任,是否具备一定的抗压能力

  • 博弈:重构也是一种资源的博弈,考虑如何让自己的利益最大化的


重构




  • 重构的机会应该是去争取的,而不是被赋予的




  • 生活中处处有压力,又总是压得人喘不过气,如果连个代码重构都不敢想、不敢搞,那生活中的种种不如意又当如何?正如那句话:“做人如果没有梦想,和咸鱼有什么区别?”




QQ截图20230628143731.jpg


收获


其实,个人感觉收获最大的还是心智的磨练; 其次是技术的提升;


结语


生活已经够累了,跟大家闲扯一下,放松!放松!放松! 最重要的天热了要多喝水,多吃蔬菜和水果,适度的体育活动!欢迎大家说说自己的看法


heshui15.jpg

收起阅读 »

记两次优化导致的Bug

web
人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。 废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到...
继续阅读 »

人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。


废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到的问题。Bug本天成,菜鸡偶遇之。


和requestFrameAnimation有关。


和requestFrameAnimation就是在下一帧的时候调用, 调用这个方法后会返回一个id,凭借此id可以取消下一帧的调用cancelAnimationFrame(id)


在图形渲染中,经常会使用连续渲染的方式来不断更新视图,使用此api可以保证每一帧只绘制一次,一帧绘制一次就够了,两次没有意义。 当然,这里说的一次,是指一整个绘制,包含了后处理(比如three的postprocess)的一系列操作。


大致情况是这样的, 现在已经封装好了一个3d渲染器,然后在更新场景数据的,会有比较多的循环,这个时候,3d渲染就可以停掉了,这里就是做了这个优化。


来看主要代码,渲染器部分。只要调用animate方法就会循环渲染, pause停止,resume恢复渲染。 看上去好像没啥问题。


// 渲染 
animate = () => {
if (this.paused ) return ;
this.animateId = requestAnimationFrame(this.animate);
};
pause() {
this.paused = true;
}
resume() {
if (this.paused) {
this.paused = false;
this.animate()
}
}

再看,更新数据的部分。 问题就出在这里,更新数据这个操作,是没有任何异步的,也就是说在当前帧的宏任务里,先暂停后恢复, 结果就是,下一帧执行animate的时候, paused仍为true, 这个优化毫无意义。


view.current.pause() ;

//更新数据完成
view.current.resume()


无意义倒也没啥,但是 resume方法执行了之后,会新开一个requestAnimationFrame, 上一个requestAnimationFrame的循环又没有取消,


所以现在, 一帧里会执行两次render方法。 这个卡顿啊,就十分明显了,也就是当时的项目模型还不是特别大,只有在某些场景模型较大的时候才会卡,所以没急着处理。


过了几个月,临近更新了,不得不解决。排查的时候,是靠git记录二分才找出来的。


其次,要说明的是,使用了requestAnimationFrame连续渲染,这种在一帧里先暂停再继续的操作肯定是无意义的,因为下一帧才执行,只看执行前的结果。


当然,前面的暂停和继续的逻辑,也是一个隐患。 于是,就改成了我惯用的那种方式。 那就是暂停的时候,只是不渲染,循环继续空跑,如此而已。


  // 渲染
animate = () => {
!this.paused && this.renderer.render(this.scene, this.camera);
this.animateId = requestAnimationFrame(this.animate);
};

pause() {
this.paused = true;
}

resume() {
this.paused = false;
}

和 URL.create 有关


上面的那个代码,还可以说是写的人不熟悉requestAnimationFrame的特性造成的。 这一次的这个,真的是因为,引用关系比较多。


这次是一个纯2d项目,这里有一个将(后端)处理后的图片在canvas2d编辑器上显示出来的操作。这个图,叫产品图, 产品图是用户上传的, 也可以不经过后端处理,就直接进入到2d编辑器里。 后端处理之后,返回的是一个url。 所以有了下面的函数。


  function afterImgMatter(img:File|string) {
setShowMatter(false);
if (img instanceof File) {
tempImg.src = URL.createObjectURL(img);
} else {
tempImg.src = img
}

console.log(tempImg.src);
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
if (refCanvas.current) {
tempImg.onload = () => {

// 产品图层不可手动删除
productImg = refCanvas.current!.addImg(tempImg, false, null, 'product');
if (img instanceof File) {
productImg.id = globalData.productId;
} else {
productImg.id = globalData.matteredId;
}
setCanvasEditState(true);
!curScene && changeSene(scenes[0]);

}
}
}


如果是没经过后端处理的,那个图片就是文件转URL,就用到了这个方法URL.createObjectURL, 这个方法为二进制文件生成一个临时路径,mdn强调了一定要手动释放它。 浏览器在 document 卸载的时候,会自动释放它们,也就是说在这之前GC不会自动释放他们,即便找不到引用了。


那这个优化,我必然不能不做。 所以,我就判断了一下,如果之前的src是这个方法生成的,我就去释放它。 于是,在重新上传图片,也就是二次触发这个方法的时候出问题了, 画布直接白屏了,原来是报错了。


Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.


说我提供的图像源是破碎的,所以这个drawImage方法不能执行,其实上面还有一个报错,我之前一直没在意, 说得是一个url路径失效了,图片加载失败了,因为我释放了路径,所以我觉得出现这个,应该是正常的。


但是,现在结合drawImage的执行失败,这里还是有问题的。 我发现,确实就是因为我释放了要用做图像源的那个路径。 因为这里的productImg和 tempImg其实是通一个引用,只不过语义不同。


解决办法也很简单,那就是把释放的这一段代码,放到onload的回调里执行即可,图片加载完成之后,释放这个url也能正常工作


    tempImg.onload = () => {
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
}

这里之所以会有 productImg和 tempImg通一个引用,语义不同, 也是因为我想优化一下。之前是每次加载图片的时候,都new Image,实际上这个可以用同一个对象,所以就有了tempImg


结束


本文记录了两个bug,顺带说了一下requestAnimationFrame URL.createObjectURL的部分用法。


没有直接阐述他们的用法,有兴趣了解的可以直接看文档。


requestAnimationFrame


URL.createObj

ectURL

收起阅读 »

一个专科程序员的2023年中总结

前言 本篇是笔者对自己的这一年来的年中总结,之前也是陆陆续续看过一些总结,在6月17日的时候,爬了一下青城山,突然思绪从脑子当中炸出,写下了这篇文章 说在前面的话 在今年的二月份,我入职了我现在的这家公司,相比于去年的情况整体来说是好了一些的。去年笔者的情...
继续阅读 »

前言



本篇是笔者对自己的这一年来的年中总结,之前也是陆陆续续看过一些总结,在6月17日的时候,爬了一下青城山,突然思绪从脑子当中炸出,写下了这篇文章



说在前面的话


在今年的二月份,我入职了我现在的这家公司,相比于去年的情况整体来说是好了一些的。去年笔者的情况是蛮糟糕的,大概是半年是没有收入的,不是说我没有在工作辣,而是你们知道。


不过,这段经历让我心态有了一定的提升,在联盟中,有一句台词是这样的,天下万般兵刃,唯有过往最伤人。当人处于这种情况下,真的很难熬住。回到当时的爬山场景,我也在想,就是我爬的很累很累了,但是我还是要迈出下一步,究竟是为了什么才一路向上的。


我在爬山的时候,累了,我就休息一下,然后又继续前进,路虽不是那么平坦,心却平坦了一些,就像之前我也是这么过来的,让自己休息好了,再继续前进。


所以,我发现我后来对这些事情无关紧要了,就如莎士比亚说过的,凡是过往,皆为序章。一旦发生了的事情就已经成为了过去。不要沉溺于过去,把握当下,面对未来。


路途中,我看见很多人步伐各不相同,那是因为他们有着不一样的心之所向。


回顾


先说一下,这半年中,我自己做了什么




  • 面试,找工作,不过这一部分,我没有背八股文,几乎是知道的能说就说




  • 参与了华为 DevUI 组件库开源 顺利提了一个 PR ,后续也会持续关注继续做




  • 工作中,实现了一些个人觉得比较好的组件,签到日历,级联选择器,数字滚动,上下漂浮动画组件等等




  • 对外的话,做了我自己的一个小工具,JsonToTypescript ,可以把 JSON 格式转为对应的 Typescript 接口声明




  • 搭建了自己的 个人博客 使用了 hexo 框架




  • 准备了我自己的长期专栏 手摸手带你学习Javascript 、后期会写一些面试题相关




  • 更了几篇文章,后续也会一直更新




  • 持续学习自己定的方向,前端工程化




这些内容的话,我觉得在做组件那一块我的进步相对于之前来说是有提升的,写作的话,看了诗词相关的视频,也会用到我的内容中,整体还不错。


这里我们谈一谈组件吧,简单谈一谈我的一些浅薄理解。


Vue 组件设计


1. 如何设计一个组件?



  • 定义 Props (核心)

  • 确定组件的职责是做什么?展示、数据录入、布局等等

  • 根据主题设计组件样式


假如,我们现在设计一个抽奖组件


如下图所示:


抽奖转存失败,建议直接上传图片文件

有一个这样的抽奖实现,当我点击下这个开始按钮,那么需要,沿着按钮顺时针跑。对咯,我们更多的时候,其实会看见一个 UI 设计图之类的,你需要转变为这种更直观一些的


通常我第一步简单分析以后,我不会考虑很多其他场景,大部分还是业务中,够用就行


第二步,我们开始设计当前组件的 Props ,命名的话大家自己语义化一些,需要注意下


我们简单定义四个



  1. source 数据来源,为了让他有更好的复用性,数据是传递来的,在其他开发者使用的时候,需要遵循你的组件规则,这要从使用者的角度转变。你就想,怎么让别人好用

  2. speed 速率,用于控制顺时针跑的快慢,也就是抽奖速度

  3. change emit 事件,当点击的时候考虑是否对外发送什么通知之类的,非必要

  4. finish emit 事件,当组件完成之后触发的事件


第三步分析实现原理


如上图,我给大家标记出来了数据占位是 [0,1,2,3,4,5,6,7,8] 除了4以后,其他也就是奖品所在位置了


实现动画效果,顺时针抽奖,我们可以直观看见运动轨迹是 0-1-2-5-8-7-6-3 ,此为一个周期,要配合 speed 控制速率实现


展示高亮效果,就是拟定一个 active 变量,然后依次设置不同的值,也就是 排他思想


最后就是完成我们的代码逻辑


React 业务组件相关设计


1. 如何设计一个 React 组件?


我平时开发主要还是以 Vue 为主,所以,有些理念是学习来的,大家随意抽出对自己有用的为自己使用就好辣



  1. 单一职责:每个业务组件应该专注于解决特定的功能或问题,这会使得我们组件简洁,并且好维护

  2. 组件拆分:将复杂的业务需求拆分为小组件,每个组件负责特定的页面或者是功能

  3. PropsState 的设计:React 是单向数据流,合理的定义 PropsState ,确保值能够传递和保存到对应的组件。Props 的用于接受父组件传递的数据,State 用于管理数据状态


假如我需要设计点击一个新增\修改按钮设计出 Drawer 既可以新增又可以修改


我这里列出一个图,大家具体看这个图就好


image-20230625192757624.png
以上是对于组件的一些个人理解,后续学到实用的东西,也会进行相关内容的输出。


说一些面试相关的吧


我觉得在面试这件事,虽然现在投简历很多不会回复或者多看一眼。在这样做,我很担心,就是很多朋友,有些空余时间,没有去思考过,自己要走的一条路是什么。


面试的时候,你可能需要准备的知识,是要超过你目前的期望薪资的知识储备量的,这本身就是自身要具备的硬技能。


我个人在今年面试的时候,没准备,会有答不上,答不好的情况,但是基础的还行,这一块不需要背,自己知道的,没什么深度。不过写专栏就是在审视自己这个问题。我不建议大家学习我不去准备,我只是当时纯纯偷懒罢了...


笔者,是一个普通平凡的人,没有什么高质量的输出,还在不断学习中,后续会继续更新我的创作。


什么是平凡


说实话,写这个命题的时候,我是有点纠结的。曾看过这样一句话,一个人怎样看待自己,决定了此人的命运,指向了他的归宿。


我觉得自己会有一些不同的思想和见解,每个人亦是如此。也立过鸿鹄之志,我思考,我是想去拥有过一个不错的生活,为之而奋斗。不知道什么时候开始,我觉得这已然成为我的一种束缚,为什么呢?因为不快乐。


所以,我,是一个普通平凡的人,我自人间浪漫,平生事、南北西东。(我在人间放纵而活,不受世俗约束,平生事,无所谓南北西东)


虽然,并不会太洒脱,人生,总有十有八九不如人意。顺其自然就好啦,做自己喜欢的事,坚持下去就好了。总有一天可以在一寸冰封的土地,绽放出绚丽的春华,那是属于你个人。


这车水马龙的人间,读者们可以试着慢慢来,不问结果反而一身轻松,路的尽头是什么也许从来不重要。生活,有所为,有所爱,清醒、自律、知进退。


我认清自我,找到自我,从今以后,不在过我应该过的生活,而是去过我想过的人生,平凡挺好的,放下了一些,能变得更富有。


自己的方向


今年后半年大致是以下几点



  1. 更新进阶的 JavaScript 知识,这一部分大纲已经整理好了,在空余的时间,我会慢慢更新

  2. 学习 Nest ,做一下个人小应用

  3. 如果可以有时间更新自己的面试题专栏

  4. 持续更文


都是很简单的,没有太难的任务了,目前个人产出还是自我满足就够了。


结语


多花时间研究自己

作者:sakana
来源:juejin.cn/post/7248606482027675706

收起阅读 »

跨端技术总结

通过淘宝weex,微信,美团kmm,天猫Waft 等不同项目来了解目前各家公司在跨平台方向上有哪些不同的项目,用了什么不同技术实现方式,然后在对比常用的react native ,flutter 和WebAssembly具体在技术上的区别在哪里。 客户端渲染...
继续阅读 »

通过淘宝weex,微信,美团kmm,天猫Waft 等不同项目来了解目前各家公司在跨平台方向上有哪些不同的项目,用了什么不同技术实现方式,然后在对比常用的react native ,flutter 和WebAssembly具体在技术上的区别在哪里。


image.png


客户端渲染执行逻辑


android


层次的底部是 Linux,它提供基本的系统功能,如进程管理,内存管理,设备管理,如:相机,键盘,显示器等内核处理的事情。体系结构第三个部分叫做Java虚拟机,是一种专门设计和优化的 Android Dalvik 虚拟机。应用程序框架层使用Java类形式的应用程序提供了许多的更高级别的服务。允许应用程序开发人员在其应用程序中使用这些服务。应用在最上层,即所有的 Android 应用程序。一般我们编写的应用程序只被安装在这层。应用的例子如:浏览器,游戏等。


image.png


绘制流程




  1. 创建视图




ui生成就是把代码中产生的view和在xml文件配置的view,经过measure,layout,dewa 形成一个完整的view树,并调用系统方法进行绘制。Measure 用深度优先原则递归得到所有视图(View)的宽、高;Layout 用深度优先原则递归得到所有视图(View)的位置;到这里就得到view的在窗口中的布局。Draw 目前 Android 支持了两种绘制方式:软件绘制(CPU)和硬件加速(GPU),会通过系统方法把要绘制的view 合成到不同的缓冲区上


最初的ui配置


image.png


构建成内存中的view tree


image.png


2.视图布局


image.png


3.图层合成


SurfaceFlinger 把缓存 区数据渲染到屏幕,由于是两个不同的进程,所以使用 Android 的匿名共享内存 SharedClient 缓存需要显示的数据来达到目的。 SurfaceFlinger 把缓存区数据渲染到屏幕(流程如下图所示),主要是驱动层的事情,这 里不做太多解释。


image.png


4. 系统绘制


image.png


绘制过程首先是 CPU 准备数据,通过 Driver 层把数据交给 CPU 渲 染,其中 CPU 主要负责 Measure、Layout、Record、Execute 的数据计算工作,GPU 负责 Rasterization(栅格化)、渲染。由于图形 API 不允许 CPU 直接与 GPU 通信,而是通过中间 的一个图形驱动层(Graphics Driver)来连接这两部分。图形驱动维护了一个队列,CPU 把 display list 添加到队列中,GPU 从这个队列取出数据进行绘制,最终才在显示屏上显示出来。


ios:


架构


image.png



  1. Core OS layer



  • 核心操作系统层包括内存管理、文件系统、电源管理以及一些其他的操作系统任务,直接和硬件设备进行交互



  1. Core Services layer



  • 核心服务层,我们可以通过它来访问iOS的一些服务。包含: 定位,网络,数据 sql



  1. Media layer



  • 顾名思义,媒体层可以在应用程序中使用各种媒体文件,进行音频与视频的录制,图形的绘制,以及制作基础的动画效果。



  1. Cocoa Touch layer



  • 本质上来说它负责用户在iOS设备上的触摸交互操作

  • 包括以下这些组件: Multi-Touch Events Core Motion Camera View Hierarchy Localization Alerts Web Views Image Picker Multi-Touch Controls.


ios 的视图树


image.png


ios的 绘制流程:


image.png


image.png


image.png


显示逻辑



  • CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;

  • RenderServer解析提交的子树状态,生成绘制指令;

  • GPU执行绘制指令;

  • 显示渲染后的数据;


提交流程


image.png


1、布局(Layout)


调用layoutSubviews方法; 调用addSubview:方法;


2、显示(Display)


通过drawRect绘制视图; 绘制string(字符串);



每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时候,仅会设置图层为dirty。 当一个视图第一次或者某部分需要更新的时候iOS系统总是会去请求drawRect:方法。


以下是触发视图更新的一些操作:



  • 移动或删除视图

  • 通过将视图的hidden属性设置为NO

  • 滚动消失的视图再次需要出现在屏幕上

  • 视图显式调用setNeedsDisplay或setNeedsDisplayInRect:方法


视图系统都会自动触发重新绘制。对于自定义视图,就必须重写drawRect:方法去执行所有绘制。视图第一次展示的时候,iOS系统会传递正方形区域来表示这个视图绘制的区域。


在调用drawRect:方法之后,视图就会把自己标记为已更新,然后等待下一次视图更新被触发。



3、准备提交(Prepare)


解码图片; 图片格式转换;


4、提交(Commit)


打包layers并发送到渲染server;


递归提交子树的layers;


web :


web内容准备阶段


web 通常需要将所需要的html,css,js都下载下来,并进行解析执行后才进行渲染,然后是绘制过程,先来看下前期工作


image.png


一个渲染流程会划分很多子阶段,整个处理过程叫渲染流水线,流水线可分为如下几个子阶段:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。每个阶段都经过输入内容 -->处理过程-->输出内容三个部分。



  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构


image.png



  1. 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets(生存CSSDOM树),计算出DOM节点的样式


image.png


styleSheets格式
image.png



  1. 创建布局树(LayoutTree),并计算元素的布局信息。


我们有DOM树和DOM树中元素的样式,那么接下来就需要计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局。根据元素的可见信息构建出布局树。


image.png
4. 对布局树进行分层,并生成分层树(LayerTree)。


image.png



  1. 为每个图层生成绘制列表,并将其提交到合成线程。


image.png



  1. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图


image.png


image.png



  1. 合成线程发送绘制图块命令DrawQuad给浏览器进程。浏览器进程根据DrawQuad消息生成页面,并显示到显示器上


image.png


web 在android 中的绘制


WebView实际上是一个ViewGroup,并将后端的具体实现抽象为WebViewProvider,而WebViewChromium正是一个提供基于Chromium的具体实现类。


再回到WebView的情况。当WebView部件发生内容更新时,例如页面加载完毕,CSS动画,或者是滚动、缩放操作导致页面内容更新,同样会在WebView触发invalidate方法,随后在视图系统的统筹安排下,WebView.onDraw方法会被调用,最后实际上调用了AwContents.onDraw方法,它会请求对应的native端对象执行OnDraw方法,将页面的内容更新绘制到WebView对应的Canvas上去。


draw()先得到一块Buffer,这块Buffer是由SurfaceFlinger负责管理的。


然后调用view的draw(canvas)当view draw完后,调用surface.java的unlockAndPostCanvas().


将包含有当前view内容的Buffer传给SurfaceFlinger,SurfaceFlinger将所有的Buffer混合后传给FrameBuffer.至此和native原有的view 渲染就是一样的了。


image.png


成熟的框架的底层原理:


react :


RN 的 Android Bridge 和 IOS Bridge 是两端通信的桥梁, 是由一个转译的桥梁实现的不同语言的通信, 得以实现单靠 JS 就调用移动端原生 APi


架构


image.png



  • RN 的核心驱动力就来自 JS Engine, 我们所有的 JS 代码都会通过 JS Engine 来编译执行, 包括 React 的 JSX 也离不开 JS Engine, JavaScript Core 是其中一种 JS 引擎, 还有 Google 的 V8 引擎, Mozilla 的 SpiderMonkey 引擎。

  • RN 是用类 XML 语言来表示结构, 用 StyleSheet 来规划样式, 但是 UI 控件调用的是 RN 里自己的两端实现控件(android 和 IOS)



  • JavaScript 在 RN 的作用就是给原生组件发送指令来完成 UI 渲染, 所以 JavaScript Core 是 RN 中的核心部分



  • RN 是不用 JS 引擎的 UI 渲染控件的, 但是会用到 JS 引擎的 DOM 操作管理能力来管理所有 UI 节点, 每次在写完 UI 组件代码后会交给 yoga 去做布局排版, 然后调用原生组件绘制

  • bridge 负责js 和native的通讯,以android为例:Java层与Js层的bridge分别存有相同一份模块配置表,Java与Js互相通信时,通过bridge里的配置表将所调用模块方法转为{moduleID,methodID,args}的形式传递给处理层,处理层通过bridge的模块配置表找到对应的方法执行,如果有callback,则回传给调用层


image.png


通讯机制


Java -> Js: Java通过注册表调用到CatalystInstance实例,通过jni,调用到 javascriptCore,传递给调用BatchedBridge.js,根据参数{moduleID,methodID}require相应Js模块执行。


Js -> Java: JS不主动传递数据调用Java。在需要调用调Java模块方法时,会把参数{moduleID,methodID}等数据存在MessageQueue中,等待Java的事件触发,再把MessageQueue中的{moduleID,methodID}返回给Java,再根据模块注册表找到相应模块处理。


事件循环


JS 开发者只需要开发各个组件对象,监听组件事件, 然后利用framework接口调用render方法渲染组件。


而实际上,JS 也是单线程事件循环,不管是 API调用, virtural DOM同步, 还是系统事件监听, 都是异步事件,采用Observer(观察者)模式监听JAVA层事件, JAVA层会把JS 关心的事件通过bridge直接使用javascriptCore的接口执行固定的脚本, 比如"requrire (test_module).test_methode(test_args)"。此时,UI main thread相当于work thread, 把系统事件或者用户事件往JS层抛,同时,JS 层也不断调用模块API或者UI组件 , 驱动JAVA层完成实际的View渲染。JS开发者只需要监听JS层framework定义的事件即可


react 的渲染流程


image.png


首先回顾一下当前Bridge的运行过程。当我们写了类似下面的React源码。


<View style={{ backgroundColor: 'pink', width: 200, height: 200}}/>

JS thread会先对其序列化,形成下面一条消息


UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])

通过Bridge发到ShadowThread。Shadow Tread接收到这条信息后,先反序列化,形成Shadow tree,然后传给Yoga,形成原生布局信息。接着又通过Bridge传给UI thread。UI thread 拿到消息后,同样先反序列化,然后根据所给布局信息,进行绘制。


从上面过程可以看到三个线程的交互都是要通过Bridge,因此瓶颈也就在此。


image.png


首次渲染流程



  1. Native 打开 RN 页面

  2. JS 线程运行,Virtual DOM Tree 被创建

  3. JS 线程异步通知 Shadow Thread 有节点变更

  4. Shadow Thread 创建 Shadow Tree

  5. Shadow Thread 计算布局,异步通知 Main Thread 创建 Views

  6. Main Thread 处理 View 的创建,展示给用户


image.png


react native 新架构


image.png



  • JSI:JSI是Javascript Interface的缩写,一个用C++写成的轻量级框架,它作用就是通过JSI,JS对象可以直接获得C++对象(Host Objects)引用,并调用对应方法


另外JSI与React无关,可以用在任何JS 引擎(V8,Hermes)。有了JSI,JS和Native就可以直接通信了,调用过程如下:JS->JSI->C++->ObjectC/Java



  • Fabric 是 UI Manager 的新名称, 将负责 Native UI 渲染, 和当前 Bridge 不同的是, 可以通过 JSI 导出自己的 Native 函数, 在 JS 层可以直接使用这些函数引用, 反过来 Native 可以直接调用 JS 层, 从而实现同步调用, 这带来更好的数据传输和性能提升


image.png


对比


image.png


flutter:


生产环境中 Dart 通过 AOT 编译成对应平台的指令,同时 Flutter 基于跨平台的 Skia 图形库自建了渲染引擎,最大程度地保证了跨平台渲染的一致性


image.png



  • embedder: 可以称为嵌入器,这是和底层的操作系统进行交互的部分。因为flutter最终要将程序打包到对应的平台中,对于Android平台使用的是Java和C++,对于iOS和macOS平台,使用的是Objective-C/Objective-C++。



  • engine:Flutter engine基本上使用C++写的。engine的存在是为了支持Dart Framework的运行。它提供了Flutter的核心API,包括作图、文件操作、网络IO、dar运行时环境等核心功能。Flutter Engine线程的创建和管理是由embedder负责的。



  • Flutter framework: 这一层是用户编程的接口,我们的应用程序需要和Flutter framework进行交互,最终构建出一个应用程序。


Flutter framework主要是使用dart语言来编写的。framework从下到上,我们有最基础的foundational包,和构建在其上的 animation, painting和 gestures 。


再上面就是rendering层,rendering为我们提供了动态构建可渲染对象树的方法,通过这些方法,我们可以对布局进行处理。接着是widgets layer,它是rendering层中对象的组合,表示一个小挂件。


Widgets 理解


Widgets是Flutter中用户界面的基础。你在flutter界面中能够观察到的用户界面,都是Widgets。大的Widgets又是由一个个的小的Widgets组成,这样就构成了Widgets的层次依赖结构,在这种层次结构中,子Widgets可以共享父Widgets的上下文环境。在Flutter中一切皆可为Widget。


举例,这个Containerks 控件里的child,color,Text 都是Widget。


  color: Colors.blue,
child: Row(
children: [
Image.network('http://www.flydean.com/1.png'),
const Text('A'),
],
),
);

Widgets表示的是不可变的用户UI界面结构。虽然结构是不能够变化的,但是Widgets里面的状态是可以动态变化的。根据Widgets中是否包含状态,Widgets可以分为stateful和stateless widget,对应的类是StatefulWidget和StatelessWidget。


渲染和绘制


渲染就是将上面我们提到的widgets转换成用户肉眼可以感知的像素的过程。Flutter代码会直接被编译成使用 Skia 进行渲染的原生代码,从而提升渲染效率。


代码首先会形成widgets树如下,这些widget在build的过程中,会被转换为 element tree,其中ComponentElement是其他Element的容器,而RenderObjectElement是真正参与layout和渲染的element。。一个element和一个widget对应。然后根据elementrtree 中需要渲染的元素形成RenderTree ,flutter仅会重新渲染需要被重新绘制的element,每次widget变化时element会比较前后两个widget,只有当某一个位置的Widget和新Widget不一致,才会重新创建Element和widget。 最后还会一个layer tree,表示绘制的图层。



四棵树有各自的功能



image.png


Flutter绘制流程


image.png



  • Animate,触发动画更新下一帧的值

  • Build,触发构建或刷新 Widget Tree、Element Tree、RenderObject Tree

  • Layout,触发布局操作,确定布局大小和位置信息

  • CompositeBits,更新需要合成的 Layer 层标记

  • Paint,触发 RenderObject Tree 的绘制操作,构建 Layer Tree

  • Composite,触发 Layer Tree 发送到 Engine,生成 Engine LayerTree


在 UIThread 构建出四棵树,并在 Engine 生成 Scene,最后提交给 RasterThread,对 LayerTree 做光栅化合成上屏。


Flutter 渲染流程


image.png




  • UIThread


    UIThread 是 Platform 创建的子线程,DartVM Root Isolate 所有的 dart 代码都运行在该线程。阻塞 UIThread 会直接导致 Flutter 应用卡顿掉帧。




  • RasterThread


    RasterThread 原本叫做 GPUThread,也是 Platform 创建的子线程,但其实它是运行在 CPU 用于处理数据提交给 GPU,所以 Flutter 团队将其名字改为 Raster,表明它的作用是光栅化。


    C++ Engine 中的光栅化和合成过程运行在该线程。




  • C++ Engine 触发 Platform 注册 VSync 垂直信号回调,通过 Platform -> C++ Engine -> Dart Framework 触发整个绘制流程




  • Dart Framework 构建出四棵树,Widget Tree、Element Tree、RenderObject Tree、Layer Tree,布局、记录绘制区域及绘制指令信息生成 flutter::LayerTree,并保存在 Scene 对象用以光栅化,这个过程运行在 UIThread




  • 通过 Flutter 自建引擎 Skia 进行光栅化和合成操作, 将 flutter::LayerTree 转换为 GPU 指令,并发送给 GPU 完成光栅化合成上屏显示操作,这个过程执行在 RasterThread




整个调度过程是生产者消费者模型,UIThread 负责生产 flutter::Layer Tree,RasterThread 负责消费 flutter::Layer Tree。


flutter 线程模型


image.png


Mobile平台上面每一个Engine实例启动的时候会为UI,GPU,IO Runner各自创建一个新的线程。所有Engine实例共享同一个Platform Runner和线程。




  • Platform Task Runner


    Flutter Engine的主Task Runner,可以理解为是主线程,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用。改线程不仅仅处理与Engine交互,它还处理来自平台的消息。




  • UI Task Runner Thread(Dart Runner)


    UI Task Runner被Flutter Engine用于执行Dart root isolate代码,Root isolate运行应用的main code。负责触发构建或刷新 Widget Tree、Element Tree、RenderObject Tree,生成最终的Layer Tree。


    Root Isolate还是处理来自Native Plugins的消息响应,Timers,Microtasks和异步IO(isolate是有自己的内存和单线程控制的运行实体,isolate之间的内存在逻辑上是隔离的)。




  • GPU Task Runner


    GPU Task Runner中的模块负责将Layer Tree提供的信息转化为实际的GPU指令,执行设备GPU相关的skia调用,转换相应平台的绘制方式,比如OpenGL, vulkan, metal等。GPU Task Runner同时也负责配置管理每一帧绘制所需要的GPU资源




  • IO Task Runne




IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner的渲染做好准备


Dart 是单线程的,但是采用了Event Loop 机制,也就是不断循环等待消息到来并处理。在 Dart 中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart 总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。


image.png


isolate机制尽管 Dart 是基于单线程模型的,但为了进一步利用多核 CPU,将 CPU 密集型运算进行隔离,Dart 也提供了多线程机制,即 Isolate。每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。Isolate 通过发送管道(SendPort)实现消息通信机制。我们可以在启动并发 Isolate 时将主 Isolate 的发送管道作为参数传给它,这样并发 Isolate 就可以在任务执行完毕后利用这个发送管道给我们发消息。


如果需要在启动一个 Isolate 执行某项任务,Isolate 执行完毕后,发送消息告知我们。如果 Isolate 执行任务时,同时需要依赖主 Isolate 给它发送参数,执行完毕后再发送执行结果给主 Isolate这样的双向通信,让并发 Isolate 也回传一个发送管道即可。


weex:


架构:


image.png



  1. 将weex源码生成JS Bundle,由template、style 和 script等标签组织好的内容,通过转换器转换成JS Bundle

  2. 服务端部署JS Bundle ,将JS Bundle部署在服务器,当接收到终端(Web端、iOS端或Android端)的JS Bundle请求,将JS Bundle下发给终端

  3. WEEX SDK初始化,初始化 JS 引擎,准备好 JS 执行环境

  4. 构建渲染指令树,Weex 里都使用 DOM API 把 Virtual DOM 转换成真实的Element 树,而是使用 JS Framework 里定义的一套 Weex DOM API 将操作转化成渲染指令发给客户端,形成客户端的真实控件

  5. 页面的 js 代码是运行在 js 线程上的,然而原生组件的绘制、事件的捕获都发生在 UI 线程。在这两个线程之间的通信用的是 callNative 和 callJS 这两个底层接口。callNative 是由客户端向 JS 执行环境中注入的接口,提供给 JS Framework 调用。callJS 是由 JS Framework 实现的,并且也注入到了执行环境中,提供给客户端调用。


渲染过程


Weex 里页面的渲染过程和浏览器的渲染过程类似,整体可以分为【创建前端组件】-> 【构建 Virtual DOM】->【生成“真实” DOM】->【发送渲染指令】->【绘制原生 UI】这五个步骤。前两个步骤发生在前端框架中,第三和第四个步骤在 JS Framework 中处理,最后一步是由原生渲染引擎实现的。 页面渲染的大致流程如下


image.png


各家项目的实现方式:


淘宝新⼀代⾃绘渲染引擎 的架构与实践


Weex 技术发展历程


image.png


Weex 2.0 简版架构


最上层的前端生态还是没变的,应该还是以vue的响应式编程为主。


image.png


2.0多了js和c++的直接调用,减少js引擎和布局引擎的通讯开销。


image.png


image.png


Weex 2.0 重写了渲染层的实现,不再依赖系统 UI,改成依赖统一的图形库 Skia 自绘渲染,和 Flutter 原理很像,我们也直接复用了 Flutter Engine 的部分代码。底层把 Weex 对接过的各种原生能力、三方扩展模块都原样接入。对于上层链路,理论上讲业务 JS 代码、Vue/Rax、JS Framework 都是不需要修改的。在 JS 引擎层面也做了一些优化,安卓上把 JavaScriptCore 换成了 QuickJS,用 bytecode 加速二次打开性能,并且结合 Weex js bundle 的特点做针对性的优化。


字节码编译原理


image.png


渲染原理


渲染引擎通用的渲染管线可以简化为【执行脚本】-->【构建节点】-->【布局/绘制】--> 【合成/光栅化】--【上屏】这几个步骤。Weex 里的节点构建逻辑主要在 JS 线程里执行,提交一颗静态节点树到 UI 线程,UI 线程计算布局和绘制属性,生成 Layer Stack 提交到 GPU 线程。


image.png


天猫:WAFT:基于WebAssembly和Skia 的AIoT应用开发框架


整体方案


image.png


为什么选择WebAssemy?


支持 AOT 模式,拔高了性能上限;活跃的开源社区,降低项目推进的风险;支持多语言,拓宽开发者群体。


WebAssembly(又名wasm)是一种高效的,低级别的编程语言。 它让我们能够使用JavaScript以外的语言(例如C,C ++,Rust或其他)编写程序,然后将其编译成WebAssembly,进而生成一个加载和执行速度非常快的Web应用程序


WebAssembly是基于堆栈的虚拟机的二进制指令格式,它被设计为编程语言的可移植编译目标。目前很多语言都已经将 WebAssembly 作为它的编译目标了。


image.png


waft 开发方式


可以看到是采用类前端的开发方式,限定一部分css能力。最后编译为WebAssembly,打包成wasm bundle。 在进行aot 编译城不同架构下的机器码。


image.png


运行流程


可以看到bundle 加载过程中,会执行UI区域的不同的生命周期函数。然后在渲染过程则是从virtual dom tree 转化到widget tree,然后直接通过skia 渲染库直接进行渲染。


image.png


Waft 第二阶段成果-跨平台


image.png


美团KMM在餐饮SaaS中的探索与实践


KMP:Kotlin Multiplatform projects 使用一份kotlin 代码在不同平台上运行


KMM:Kotlin MultiplatformMobile 一个用于跨平台移动应用程序的 SDK。使用 KMM,可以构建多平台移动应用程序并在 Android 和 iOS 应用程序之间共享核心层和业务逻辑等方面。开发人员可以使用单一代码库并通过共享数据层和业务逻辑来实现功能。其实就是把一份逻辑代码编译为多个平台的产物编译中间产物,在不同平台的边缘后端下转为不同的变异后端产物,在不同平台下运行。


image.png


IR 全称是 intermediate representation,表示编译过程中的中间信息,由编译器前端对源码分析后得到,随后会输入到后端进一步编译为机器码


IR 可以有一系列的表现方式,由高层表示逐渐下降(lowering)到低层


我们所讨论的 Kotlin IR 是抽象语法树结构(AST),是比较高层的 IR 表示类型。


有了完备的 IR,就可以利用不同的 后端,编出不同的目标代码,比如 JVM 的字节码,或者运行在 iOS 的机器码,这样就达到了跨端的目的


image.png


对比


image.png


总结


当前存在4种多端方案:



  1. Web 容器方案

  2. 泛 Web 容器方案

  3. 自绘引擎方案

  4. 开放式跨端框架


image.png


引用文章:


zhuanlan.zhihu.com/p/20259704​​


zhuanlan.zhihu.com/p/281238593​​


zhuanlan.zhihu.com/p/388681402​​


juejin.cn/post/708412…​​


guoshuyu.cn/home/wx/Flu…​​


oldbird.run/flutter/u11…​​


w4lle.com/2020/11/09/…​​


blog.51cto.com/jdsjlzx/568…​​


http://www.devio.org/2021/01/10/…​​


gityuan.com/flutter/​​


gityuan.com/2019/06/15/…​​


zhuanlan.zhihu.com/p/78758247​​


http://www.finclip.com/news/f/5

作者:美好世界
来源:juejin.cn/post/7249624871721041975
035…​​

收起阅读 »

年终奖也许会迟到,年中(终)绩效可不会

它来了,它来了,它抱着恐怖的绩效跑来了!!! 公司惯例,年中绩效考核,年终奖也许会迟到,年中(终)绩效可不会!!! 公司考核标准基于·smart·原则: 按照公司的标准,作为领导者写下了通用类年中评价的套话, 千变万变套路不变。 年中评价 项目组: **...
继续阅读 »

它来了,它来了,它抱着恐怖的绩效跑来了!!!

公司惯例,年中绩效考核,年终奖也许会迟到,年中(终)绩效可不会!!!


公司考核标准基于·smart·原则:
51578860_36515712.719082.jpg


按照公司的标准,作为领导者写下了通用类年中评价的套话, 千变万变套路不变。


年中评价


项目组: ***平台--***监管项目组


产品列表



  • ****平台·PC端

  • ****平台·移动端

  • ****服务平台

  • **平台·医院端

  • **平台·患者端

  • **平台·管理端

  • **平台·药房端

  • ***数据资产·PC端

  • ***数据资产·移动端


测试组(1-6 月)


测试人员:测试A同学测试B同学测试C同学测试D同学


测试-推荐模板




  • 引言部分:开始评语时,可以表达对员工的认可和欣赏,例如强调其专业能力、工作态度和对团队的贡献(对测试人员半年内的工作认可)。




  • 工作表现:评价员工在测试工作方面的表现。可以包括以下内容:



    • 质量控制:评估员工在确保产品质量方面的能力,包括发现和报告问题、提供准确的测试结果等。

    • 测试方法和策略:评价员工在测试方法和策略上的创新能力和应用情况,以提高测试效率和覆盖范围。

    • 缺陷管理:考察员工在缺陷管理方面的能力,包括及时跟踪和解决缺陷、与开发团队合作等。





  • 团队合作:强调员工在团队合作中的表现。包括:



    • 与开发人员的合作:评价员工与开发人员的有效沟通和协作,以促进问题解决和改进产品质量(与开发对接,与产品对接)。

    • 跨部门合作:考察员工与其他团队成员、产品经理等部门的合作,以确保测试工作与整个开发流程的顺畅衔接(与运营沟通,与合作团队沟通)。





  • 发展潜力:讨论员工的发展潜力和个人成长方面的表现。包括:



    • 学习能力:评价员工学习新技能和掌握新技术的能力,以提高测试工作的效率和质量。

    • 自我提升:强调员工在自我提升方面的积极性,例如主动参加培训、学习新的测试工具和技术等(跨领域 多涉猎 了解前后端相关基础知识)。





  • 目标设定:与员工一起讨论并设定下一阶段的发展目标和改进计划。确保目标具体、可衡量和可达成,并提供必要的支持和资产(定目标,完善现有流程的缺陷)。




  • 总结和鼓励:总结评语时,强调员工在测试工作中的优点和成就,并鼓励其继续努力和发展。




测试-参考评语



  • 引言部分:我非常欣赏你在过去半年中在测试团队中的出色表现。

  • 工作表现:你的精确测试和准确的缺陷报告帮助我们提高了产品质量,使得我们的监管平台系统能够更高效地发现和解决问题;在最近的**流转平台·患者端中,你的测试工作帮助我们发现并解决了重要的问题,确保了产品的稳定性和可靠性;积极与开发人员合作,通过有效的沟通和合作解决了许多测试和开发之间的问题;

  • 发展潜力:希望你进一步拓展自己的技能,参与更多复杂项目的测试,并考虑接触新的测试方法和领域,以更好地发展自己的测试职业生涯;

  • 目标设定: 期待你在下半年中继续提高测试覆盖率和质量,尤其是在自动化测试方面的能力;

  • 总结和鼓励:你在过去的半年里展现出了坚韧的工作态度、出色的技术能力和团队精神;对你的未来发展充满信心,相信你将继续在测试领域中取得更大的成就。


测试-完整评语


  我非常欣赏你在过去半年中在测试团队中的出色表现;你的精确测试和准确的缺陷报告帮助我们提高了产品质量,使得我们的监管平台系统能够更高效地发现和解决问题;在最近的**流转平台·患者端中,你的测试工作帮助我们发现并解决了重要的问题,确保了产品的稳定性和可靠性;积极与开发人员合作,通过有效的沟通和合作解决了许多测试和开发之间的问题;

  希望你进一步拓展自己的技能,参与更多复杂项目的测试,并考虑接触新的测试方法和领域,以更好地发展自己的测试职业生涯;期待你在下半年中继续提高测试覆盖率和质量,尤其是在自动化测试方面的能力;

  你在过去的半年里展现出了坚韧的工作态度、出色的技术能力和团队精神;对你的未来发展充满信心,相信你将继续在测试领域中取得更大的成就。


测试-有待改进


无自动化测试


无接口测试


无性能测试


无测试用例


. . . 待补充


开发组(1-6 月)


后端人员: 后端A同学后端B同学后端C同学后端D同学后端E同学后端F同学

前端人员: 前端A同学前端B同学前端C同学前端D同学前端E同学, 前端F同学前端G同学前端H同学


开发-推荐模板




  • 引言部分:开门见山地表达对开发人员的肯定和欣赏




  • 技术能力和贡献



    • 强调开发人员在技术方面的能力和专业知识

    • 举例说明开发人员在某个具体项目或任务中的成就和贡献(具体工作模块 以及相关业务梳理

    • 强调开发人员在团队合作中的积极性和贡献(团队文档 主动分担任务





  • 创新和解决问题能力



    • 强调开发人员的创新意识和解决问题的能力(bug 跟进 解决效率

    • 举例说明开发人员在解决技术难题或改进现有系统方面的成果(实际项目****平台,****管理端,****患者端,****药房端,***数据资产)





  • 质量和效率



    • 强调开发人员在代码质量和开发效率方面的表现(暂时无bug统计,不好评估)。

    • 提及开发人员采用的工具和技术,以提高开发效率和质量(CI/CD Jenkins 脚手架)。





  • 发展潜力和建议



    • 强调开发人员的学习能力和自我提升意愿

    • 提供具体的建议和指导,帮助开发人员进一步发展和提升





  • 总结和展望



    • 总结开发人员在过去半年的工作表现,并再次表达对其的肯定和感谢(跨部门项目技术支持)

    • 展望未来,激励开发人员继续努力和成长(跨领域 多涉猎)




开发-参考评语



  • 引言部分:我非常欣赏你在过去半年中在开发团队中的出色表现。

  • 技术能力和贡献:你的编码技巧和架构设计使得我们能够按时交付了一个功能强大且稳定的产品;积极与测试人员和产品经理合作,确保需求理解准确,并及时响应和解决问题。

  • 创新和解决问题能力:你提出了一些创新的想法和解决方案,帮助我们改进了产品的用户体验和性能;

  • 质量和效率: 你的代码规范和代码审查质量有助于减少缺陷率,并提高了整体代码质量;你的使用和推广新的开发工具(CI/CD Jenkins)自动化测试框架(暂无)为团队节省了大量时间和精力。

  • 发展潜力和建议: 建议你继续扩展你的技术广度,并积极参与跨部门项目,以进一步发展你的领导能力。

  • 总结和展望:总体而言,你在过去半年的表现令人印象深刻,感谢你为团队做出的出色贡献;相信在你的持续努力下,你将继续在技术领域中取得更大的突破和成功。


开发-个人-完整评语


  你展现出了卓越的技术能力和深厚的专业知识。在应对各种技术挑战和难题时,展现了出色的解决问题的能力。你熟练掌握各种编程语言和技术工具,并能够灵活应用它们来实现高质量的代码和解决方案。

  你在开发过程中展现出了创新和创造力。你不断寻找新的解决方案和优化方法,通过引入新技术和工具,推动产品的创新和发展。你的创造力为我们带来了许多惊喜和突破。

  你的编码技巧和架构设计使得我们能够按时交付了一个功能强大且稳定的产品;积极与测试人员和产品经理合作,确保需求理解准确,并及时响应和解决问题;提出了一些创新的想法和解决方案,帮助我们的产品改进了产品的用户体验和性能;代码规范和代码审查质量有助于减少缺陷率,并提高了整体代码质量;

  你持续学习和追求个人成长。参加培训课程、研讨会和行业活动,不断更新自己的技术知识和技能。你的学习精神和求知欲将帮助我们保持在行业的前沿。

  建议你继续扩展你的技术广度,并积极参与跨部门项目,以进一步发展你的领导能力。
  感谢你们在过去的半年里付出的努力和奉献。你们的工作和成就为整个团队带来了无限的骄傲和信心。我期待着在未来的工作中继续与你们携手合作,共同创造更加辉煌的业绩。


开发-有待改进


无前端异常监控


无接口自动化


无Bug统计


. . . 待补充


产品组(1-6 月)


产品人员: 产品A同学产品B同学产品C同学产品D同学


产品-推荐模板




  • 引言部分:开门见山地表达对产品经理的赞赏和认可(后续可提出不满意地方)




  • 产品规划和战略



    • 强调产品经理在产品规划和战略方面的能力.

    • 举例说明产品经理在某个具体项目或产品中的规划和执行能力





  • 需求管理和沟通



    • 强调产品经理在需求管理和沟通方面的表现(产品文档 产品说明 需求管理

    • 提及产品经理与客户或用户之间的良好关系和沟通能力(与开发 与测试 与运营





  • 项目管理和协调能力



    • 强调产品经理在项目管理和协调方面的能力(需求池 量把控 人员协调 任务分配

    • 提及产品经理在团队合作中的积极性和协作精神





  • 用户体验和市场反馈(运营/医院/领导)



    • 强调产品经理对用户体验和市场反馈的重视和应对能力

    • 举例说明产品经理在改进产品体验方面的成果(产品反馈 响应速度 优化





  • 发展潜力和建议



    • 强调产品经理的发展潜力和提升空间(医疗业务能力 产品转换能力 业务能力)。

    • 提供具体的建议和指导,帮助产品经理进一步发展和提升。





产品-参考评语



  • 引言部分:回顾过去的半年,我想表达对你在项目中的杰出表现和卓越工作的赞赏。你展现出的才华、责任心和敬业精神让整个团队都感到鼓舞和激励。(套话

  • 产品规划和战略:你对市场趋势的敏锐洞察力和战略眼光使得我们能够制定出有前瞻性和竞争力的产品路线图

  • 需求管理和沟通:你的处方流转产品规划和推进使得我们成功地推出了一款满足客户需求并取得市场成功的产品

  • 项目管理和协调能力:作为产品经理,你成功地协调了跨部门的合作,与开发团队、设计团队和测试团队紧密合作,确保项目的顺利推进。你的沟通和协调能力为团队提供了强大的支持,并帮助我们克服了许多挑战.

  • 用户体验和市场反馈(运营/医院/领导):你不断关注用户体验和市场反馈,并能够及时调整产品策略和功能,以满足运营、医院的需求;你的用户调研和用户测试帮助我们发现并解决了产品中的痛点,提升了用户满意度.

  • 发展潜力和建议: 作为产品经理,技术理解能力对于与开发团队的有效沟通和协作至关重要。继续加强对技术的学习和理解,与开发团队密切合作,深入了解产品的技术实施细节,以便更好地与团队协作并推动产品的技术实现


产品-个人-完整评语


  回顾过去的半年,我想表达对你在项目中的杰出表现和卓越工作的赞赏。你展现出的才华、责任心和敬业精神让整个团队都感到鼓舞和激励。

  你展现了出色的洞察力和理解能力,能够深入了解用户的需求和期望。你与用户进行积极的沟通和交流,从而确保我们的产品能够准确地满足他们的需求;在产品规划和策划方面展现出卓越的能力。你能够将复杂的业务需求转化为清晰、可执行的产品规划,并制定了一系列的策略和目标,使整个团队能够朝着共同的方向努力。

  作为产品经理,你成功地协调了跨部门的合作,与开发团队、设计团队和测试团队紧密合作,确保项目的顺利推进。你的沟通和协调能力为团队提供了强大的支持,并帮助我们克服了许多挑战;你以身作则,展现出卓越的项目管理技巧和能力。你能够有效地制定项目计划、管理进度和资源,确保项目按时交付,并保持高质量的工作成果。你的组织能力和决策能力使整个团队能够高效地工作

  行业和技术的发展变化很快,作为产品经理,需要不断学习和保持敏锐的洞察力。参加行业会议、研讨会和培训课程,与同行交流经验,持续学习新的工具和技能,保持对行业趋势的关注

  我相信你拥有无限的潜力,只要持续努力和学习,你将能够在产品经理的职业道路上取得更大的成就。请继续保持对工作的热情和责任心,发挥你的创造力和领导能力,我期待在未来的工作中见证你的成长和成功。


产品-有待改进


需求调研能力不足


需求内审力度不足


医疗业务能力不足


产品整合能力不足


. . . 待补充






愿你我远离套路,投进,,的怀抱!!!


以上内容仅代表个人观点!!!


下班~~~


7-21062Q025150-L.gif


下图提供下载参考:
年中网络版.png

收起阅读 »

如何实现比 setTimeout 快 80 倍的定时器?

web
很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这...
继续阅读 »

很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard 规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:


let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。



更详细的原因,可以参考 为什么 setTimeout 有最小时延 4ms ?


探索


假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,上面那篇 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,你可以参考这里所说的 window.postMessage()



这篇文章里的作者给出了这样一段代码,用 postMessage 来实现真正 0 延迟的定时器:


(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回调函数的执行时机和 setTimeout 类似,都属于宏任务,所以可以简单利用 postMessageaddEventListener('message') 的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:



全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于 postMessage 的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。但空口无凭,咱们用数据说话。


作者设计了一个实验方法,就是分别用 postMessage 版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。读者也可以在这里自己跑一下测试


实验代码:


function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过 setZeroTimeout 也就是 postMessage 版本来递归计数到 100,然后切换成 setTimeout 计数到 100。


直接放结论,这个差距不固定,在我的 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在我硬件更好的台式机上,甚至能到 200 倍以上。



Performance 面板


只是看冷冰冰的数字还不够过瘾,我们打开 Performance 面板,看看更直观的可视化界面中,postMessage 版的定时器和 setTimeout 版的定时器是如何分布的。



这张分布图非常直观的体现出了我们上面所说的所有现象,左边的 postMessage 版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的 setTimeout 版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


借用 React Scheduler 为什么使用 MessageChannel 实现 这篇文章中的一段伪代码:


const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage 的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?参考我的这篇对 EventLoop 规范的解读 深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


通过本文,你大概可以了解如下几个知识点:



  1. setTimeout 的 4ms 延迟历史原因,具体表现。

  2. 如何通过 postMessage 实现一个真正 0 延迟的定时器。

  3. postMessage 定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7249633061440749628

收起阅读 »

项目提交按钮没防抖,差点影响了验收

web
前言 一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误...
继续阅读 »

前言


一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误个别单据流程给弄不正常了,一些报表的数据统计也不对了,客户相关人员很不满意,马上该交付了,出这问题可还了得,项目款不按时给了,这责任谁都担不起🤣


QQ图片20230627163527.jpg


领导紧急组织相关技术人员开会分析原因


初步分析原因


发生这个情况前端选手应该会很清楚这是怎么回事,明显是项目里的按钮没加防抖导致的,按钮点击触发接口,接口响应慢,用户多点了几次,可能查询接口还没什么问题,如果业务复杂的地方,部分按钮的操作涉及到一些数据计算和后端多次交互更新数据的情况,就会出现错误。


看下项目情况


用到的框架和技术


项目使用 angular8 ts devextreme 组合。对!这就是之前文章提到的那个屎山项目(试用期改祖传屎山是一种怎么样的体验


项目规模


业务单据页面大约几百个,项目里面的按钮几千个,项目里面的按钮由于场景复杂,分别用了如下几种写法:



  • dx-button

  • div

  • dx-icon

  • input type=button

  • svg


由于面临交付,领导希望越快越好,最好一两天之内解决问题


还好我们领导没有说这问题当天就要解决 😁


解决方案


1. 添加防抖函数


按钮点击添加防抖函数,设置合理的时间


function debounce(func, wait) {
let timeout;
return function () {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(func, wait)
}
}

优点


封装一个公共函数,往每个按钮的点击事件里加就行了


缺点


这种情况有个问题就是在业务复杂的场景下,时间设置会比较棘手,如果时间设置短了,接口请求慢,用户多次点击还会出现问题,如果时间设置长了,体验变差了


2. 设置按钮禁用


设置按钮的 disabled 相关属性,按钮点击后设置禁用效果,业务代码执行结束后取消禁用


this.disabled = true
this.disabled = false

优点


原生按钮和使用的UI库的按钮设置简单


缺点


div, icon, svg 这种自定义的按钮的需要单独处理效果,比较麻烦


3. 请求拦截器中添加loading


在请求拦截器中根据请求类型显示 loading,请求结束后隐藏


优点


直接在一个地方设置就行了,不用去业务代码里一个个加


缺点


由于我们的技术栈使用的 angular8 内置的请求,无法实现类似 axios 拦截器那种效果,还有就是项目中的接口涉及多个部门的接口,不同部门的规范命名不一样,没有统一的标准,在实际的业务场景中,一个按钮的行为可能触发了多个请求,因此这个方案不适合当前的项目


4. 添加 loading 组件(项目中使用此方案)


新增一个 loading 组件,绑定到全局变量中,按钮点击触发显示 loading,业务执行结束后隐藏。


loading 组件核心代码


import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private isLoading$ = new BehaviorSubject<boolean>(false);
  private message$ = new BehaviorSubject<string>('正在加载中...');
  constructor() {}
  show(): void {
    this.isLoading$.next(true);
  }
  hide(): void {
    this.isLoading$.next(false);
  }
}

主要是 show()hide() 函数,将 loading 组件绑定到 app.components.ts 中,绑定组件到window 对象上,


window['loading'] = this.loadingService

在按钮点击时触发 show() 函数,业务代码执行结束后触发 hide() 函数


window['loading'].show();
window['loading'].hide();

优点


这种方式很好的解决了问题,由于 loading 有遮罩层还避免了用户点击某提交按钮后,接口响应慢,这时候去点击了别的操作按钮的情况。


缺点


需要在业务单据的按钮提交的地方一个个加


问题来了,一两天解决所有问题了吗?


QQ图片20230627165837.png


这么大的项目一两天不管哪种方案,把所有按钮都处理好是不现实的,经过分析讨论,最终选择了折中处理,先把客户提出来的几个业务单据页面,以及相关的业务单据页面添加上提交 loading 处理,然后再一边改 bug 一边完善剩余的地方,优先保证客户正常使用



还有更好的解决思路吗?欢迎JYM讨论交流


作者:草帽lufei
来源:juejin.cn/post/7249288087820861499

收起阅读 »

面试官问:如何实现 H5 秒开?

web
我在简历上写了精通 H5,结果面试官上来就问: 同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开? 由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点: 网络优化:http2、dns 预解析、使用 CDN 图片优化:压缩、懒加...
继续阅读 »

我在简历上写了精通 H5,结果面试官上来就问:



同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开?



image.png


由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点:




  • 网络优化:http2、dns 预解析、使用 CDN

  • 图片优化:压缩、懒加载、雪碧图

  • 体积优化:分包、tree shaking、压缩、模块外置

  • 加载优化:延迟加载、骨架屏

  • ...



看得出来面试官不太满意,最后面试也挂了。于是我请教了我的好友 Gahing ,问问他的观点。



Gahing:


你列的这些优化手段本身没啥问题,如果是一个工作一两年的我会觉得还可以。但你已经五年以上工作经验了,需要有一些系统性思考了。



好像有点 PUA 的味道,于是我追问道:什么是系统性的思考?



Gahing:


我们先说回答方式,你有没有发现,你回答时容易遗漏和重复。


比如说「图片懒加载」,你归到了「图片优化」,但其实也可以归到「加载优化」。同时你还漏了很多重要的优化手段,比如资源缓存、服务端渲染等等。


究其原因应该是缺少抽象分类方法。



那针对这个问题,应该如何分类回答?



Gahing:


分类并非唯一,可以有不同角度,但都需遵从 MECE 原则(相互独立、完全穷尽) ,即做到不重不漏




  • 按页面加载链路分类:容器启动、资源加载、代码执行、数据获取、绘制渲染。




  • 按资源性能分类:CPU、内存、本地 I/O、网络。该分类方法又被叫做 USE 方法(Utilization Saturation and Errors Method)




  • 按协作方分类:前端、客户端、数据后台、图片服务、浏览器引擎等。




  • 按流程优化分类前置、简化、拆分



    • 前置即调整流程,效果上可能是高优模块前置或并行,低优模块后置;

    • 简化即缩减或取消流程,体积优化是简化,执行加速也是简化;

    • 拆分即细粒度拆解流程,本身没有优化效果,是为了更好的进行前置和简化。

    • 这个角度抽象层次较高,通常能回答出来的都是高手。




  • 多级分类:使用多个层级的分类方法。比如先按页面加载链路分类,再将链路中的每一项用协作方或者流程优化等角度再次分类。突出的是一个系统性思维。




选择好分类角度,也便于梳理优化方案的目标。



现在,尝试使用「页面加载链路+流程优化+协作方」的多级分类思维,对常见的首屏性能优化手段进行分类。


image.png


PS: 可以打开飞书文档原文查看思维导图


好像有点东西,但是我并没有做过性能优化,面试官会觉得我在背八股么?



Gahing:


可以没有实操经验,但是得深入理解。随便追问一下,比如「页面预渲染效果如何?有什么弊端?什么情况下适用?」,如果纯背不加理解的话很容易露馅。


另外,就我个人认为,候选人拥有抽象思维比实操经验更重要,更何况有些人的实操仅仅是知道怎么做,而不知道为什么做。



那我按上面的方式回答了,能顺利通过面试么 🌝 ?



Gahing:


如果能按上面的抽象思维回答,并顶住追问,在以前应该是能顺利通过面试的(就这个问题)。


但如今行业寒冬,大厂降本增效,对候选人提出了更高的要求,即系统性思考业务理解能力


从这个问题出发,如果想高分通过,不仅需要了解优化方案,还要关注研发流程、数据指标、项目协作等等,有沉淀自己的方法论和指导性原则,能实施可执行的 SOP。。




最后,我还是忍不住问了 Gahing :如果是你来回答这个问题,你会怎么回答?



Gahing:


H5 秒开是一个系统性问题,可以从深度和广度两个方向来回答。


深度关注的是技术解决方案,可以从页面加载链路进行方案拆解,得到容器启动、资源加载、代码执行、数据获取、绘制渲染各个环节。其中每个环节还可以从协作方和流程优化的角度进一步拆解。


广度关注的是整个需求流程,可以用 5W2H 进行拆解,包括:



  • 优化目标(What):了解优化目标,即前端首屏加载速度

  • 需求价值(Why):关注需求收益,从技术指标(FMP、TTI)和业务指标(跳失率、DAU、LT)进行分析

  • 研发周期(When):从开发前到上线后,各个环节都需要介入

  • 项目协作(Who):确定优化专项的主导方和协作方

  • 优化范围(Where):关注核心业务链路,确定性能卡点

  • 技术方案(How):制定具体的优化策略和行动计划

  • 成本评估(How much):评估优化方案的成本和效益。考虑时间、资源和预期收益,确保优化方案的可行性和可持续性。


通过 5W2H 分析法,可以建立系统性思维,全面了解如何实现 H5 秒开,并制定相应的行动计划来改进用户体验和页面性能。





限于篇幅,后面会单独整理两篇文章来聊聊关于前端首屏优化的系统性思考以及可实施的解决方案。


👋🏻 Respect!欢迎一键三连 ~


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

日常宕机?聊聊内存存储的Redis如何持久化

Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。 Redis 中的两种持久化方式:...
继续阅读 »

Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。


Redis 中的两种持久化方式: RDB(Redis DataBase)和 AOF(Append Of File)


1. RDB(Redis DataBase)


在指定的 时间间隔内 将内存中的数据集 快照 写入磁盘,也就是行话讲的快照(Snapshot),它恢复时是将快照文件直接读到内存里


1.1 原理


不使用Fork存在的问题





  • Redis 是一个 单线程 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。




  • 在 持久化的同时内存数据结构 还可能在 变化,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束。





Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是 最后一次持久化后的数据可能丢失


1.2 fork 函数


根据操作系统多进程 COW(Copy On Write) 机制Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,简单理解也就是基于当前进程 复制 了一个进程,主进程和子进程会共享内存里面的代码块和数据段。




  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术

  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。



1.3 RDB流程图


image.png


1.4 RDB相关配置


1.4.1 配置文件



  • RDB文件默认在redis主目录下的 dump.rdb


image.png



  • 快照默认的保持策略




  1. 先前的快照是在3600秒(1小时)前创建的,并且现在已经至少有 1 次新写入,则将创建一个新的快照;

  2. 先前的快照是在300秒(5分钟)前创建的,并且现在已经至少有 100 次新写入,则将创建一个新的快照;

  3. 先前的快照是在60秒(1分钟)前创建的,并且现在已经至少有 10000 次新写入,则将创建一个新的快照;



image.png


1.4.2 相关指令


save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。


bgsave: Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。


lastsave:获取最后一次成功执行快照的时间


image.png


1.5 RDB如何备份



  1. config get dir  查询rdb文件的目录

  2. 关闭Redis

  3. 将备份文件 dump.rdb 移动到 redis 安装目录并启动 redis 服务即可,备份数据会直接加载。


image.png


2. AOF(Append Of File)


RDB快照不是很持久。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 kill -9 的实例意外发生,则写入 Redis 的最新数据将丢失。


2.1 原理


AOF(Append Of File) 是以 日志 的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来( 读操作不记录 ) , 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作


2.2 AOF流程图



  1. 客户端的请求写命令会被append追加到AOF缓冲区内;

  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;


image.png


2.3 AOF相关配置


2.3.1 AOF启动配置


image.png


配置文件默认关闭AOF,将配置文件设置为 appendonly yes 启动AOF。


AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)


2.3.2 AOF 同步fsync频率设置


image.png



  • appendfsync always始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

  • appendfsync everysec每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

  • appendfsync noredis不主动进行同步,把同步时机交给操作系统。


借助 glibc 提供的 fsync(int fd) 函数来讲指定的文件内容 强制从内核缓存刷到磁盘。但  "强制开车"  仍然是一个很消耗资源的一个过程,需要  "节制" !通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 fsync 操作就可以了。


Redis 同样也提供了另外两种策略,一个是 永不 fsync,来让操作系统来决定合适同步磁盘,很不安全,另一个是 来一个指令就 fsync 一次,非常慢。但是在生产环境基本不会使用,了解一下即可。


2.3.3 查看appendonly.aof文件


image.png
image.png
最后一条del non_existing_key没有追加到appendonly.aof文件中,因为它没有对数据实际造成修改


2.4 AOF如何备份


修改默认的appendonly no,改为yes


2.4.1 正常恢复



  1. config get dir  查询rdb文件的目录

  2. 关闭Redis

  3. 将备份文件 appendonly.aof 移动到 redis 安装目录并启动 redis 服务即可,备份数据会直接加载。


2.4.2 异常恢复



  1. 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复备份被写坏的AOF文件。

  2. 恢复:重启redis,备份数据会直接加载。


2.5 Rewrite重写


Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志 "瘦身"
Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其 原理 就是 开辟 (fork) 一个子进程 对内存进行 遍历 转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件 中。序列化完毕后再将操作期间发生的 增量 AOF 日志 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。


2.5.1 配置文件


image.png



  • no-appendfsync-on-rewrite=yes:不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

  • no-appendfsync-on-rewrite=no:还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)


image.png



  • auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

  • auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。



例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB


系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,


如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。



2.5.2 Rewrit流程图




  1. bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

  2. 主进程fork出子进程执行重写操作,保证主进程不会阻塞。

  3. 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

  4. 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

  5. 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。



image.png


3. 总结


3.1 Redis 4.0 混合持久化


重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。


Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小,于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。


3.2 官方建议



  • 官方推荐两个都启用。

  • 如果对数据不敏感,可以选单独用RDB。

  • 不建议单独用 AOF,因为可能会出现Bug。

  • 如果只是做纯内存缓存,可以都
    作者:芒猿君
    来源:juejin.cn/post/7249382407245037623
    不用。

收起阅读 »

Spring Cloud 框架优雅关机和重启

背景 我们编写的Web项目部署之后,经常会因为需要进行配置变更或功能迭代而重启服务,单纯的kill -9 pid的方式会强制关闭进程,这样就会导致服务端当前正在处理的请求失败,那有没有更优雅的方式来实现关机或重启呢? 优雅停机 在项目正常运行的过程中,如果直接...
继续阅读 »

背景


我们编写的Web项目部署之后,经常会因为需要进行配置变更或功能迭代而重启服务,单纯的kill -9 pid的方式会强制关闭进程,这样就会导致服务端当前正在处理的请求失败,那有没有更优雅的方式来实现关机或重启呢?


优雅停机


在项目正常运行的过程中,如果直接不加限制的重启可能会发生一下问题



  1. 项目重启(关闭)时,调用方可能会请求到已经停掉的项目,导致拒绝连接错误(503),调用方服务会缓存一些服务列表导致,服务列表依然还有已经关闭的项目实例信息

  2. 项目本身存在一部分任务需要处理,强行关闭导致这部分数据丢失,比如内存队列、线程队列、MQ 关闭导致重复消费


为了解决上面出现的问题,提供以下解决方案:



  1. 关于问题 1 采用将需要重启的项目实例,提前 40s 从 nacos 上剔除,然后再重启对应的项目,保证有 40s 的时间可以用来服务发现刷新实例信息,防止调用方将请求发送到该项目

  2. 使用 Spring Boot 提供的优雅停机选项,再次预留一部分时间

  3. 使用 shutdonwhook 完成自定的关闭操作


一、主动将服务剔除


该方案主要考虑因为服务下线的瞬间,如果 Nacos 服务剔除不及时,导致仍有部分请求转发到该服务的情况


在项目增加一个接口,同时在准备关停项目前执行 stop 方法,先主动剔除这个服务,shell 改动如下:


run.sh


function stop()  
{
echo "Stop service please waiting...."
echo "deregister."
curl -X POST "127.0.0.1:${SERVER_PORT}/discovery/deregister"
echo ""
echo "deregister [${PROJECT}] then sleep 40 seconds."
# 这里 sleep 40 秒,因为 Nacos 默认的拉取新实例的时间为 30s, 如果调用方不修改的化,这里应该最短为 30s
# 考虑到已经接收的请求还需要一定的时间进行处理,这里预留 10s, 如果 10s 还没处理完预留的请求,调用方肯定也超时了
# 所以这里是 30 + 10 = 40sleep 40
kill -s SIGTERM ${PID}
if [ $? -eq 0 ];then
echo "Stop service done."
else
echo "Stop service failed!"
fi
}

在项目中增加 /discovery/deregister 接口


Spring Boot MVC 版本


import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@RestController
@RequestMapping("discovery")
@Slf4j
public class DeregisterInstanceController {

@Autowired
@Lazy
private ServiceRegistry serviceRegistry;

@Autowired
@Lazy
private Registration registration;


@PostMapping("deregister")
public ResultVO<String> deregister() {
log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());
try {
serviceRegistry.deregister(registration);
} catch (Exception e) {
log.error("deregister from nacos error", e);
return ResultVO.failure(e.getMessage());
}
return ResultVO.success();
}
}

Spring Cloud Gateway


通过使用 GatewayFilter 方式来处理


package com.br.zeus.gateway.filter;  

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted;

import com.alibaba.fastjson.JSON;
import com.br.zeus.gateway.entity.RulesResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Component
@Slf4j
public class DeregisterInstanceGatewayFilter implements GatewayFilter, Ordered {

@Autowired
@Lazy
private ServiceRegistry serviceRegistry;

@Autowired
@Lazy
private Registration registration;

public DeregisterInstanceGatewayFilter() {
log.info("DeregisterInstanceGatewayFilter 启用");
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isAlreadyRouted(exchange)) {
return chain.filter(exchange);
}

log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());

RulesResult result = new RulesResult();
try {
serviceRegistry.deregister(registration);
result.setSuccess(true);
} catch (Exception e) {
log.error("deregister from nacos error", e);
result.setSuccess(false);
result.setMessage(e.getMessage());
}

ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(result));
response.setStatusCode(HttpStatus.OK);
return response.writeWith(Mono.just(bodyDataBuffer));
}

@Override
public int getOrder() {
return 0;
}
}


在路由配置时,增加接口和过滤器的关系


.route("DeregisterInstance", r -> r.path("/discovery/deregister")  
.filters(f -> f.filter(deregisterInstanceGatewayFilter))
.uri("https://example.com"))

二、Spring Boot 自带的优雅停机方案


要求 Spring Boot 的版本大于等于 2.3


在配置文件中增加如下配置:


application.yaml


server:  
shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 10s

当使用 server.shutdown=graceful 启用时,在 web 容器关闭时,web 服务器将不再接收新请求,并将等待活动请求完成的缓冲期。使用 timeout-per-shutdown-phase 配置最长等待时间,超过该时间后关闭


三、使用 ShutdownHook


public class MyShutdownHook {
public static void main(String[] args) {
// 创建一个新线程作为ShutdownHook
Thread shutdownHook = new Thread(() -> {
System.out.println("ShutdownHook is running...");
// 执行清理操作或其他必要的任务
// 1. 关闭 MQ
// 2. 关闭线程池
// 3. 保存一些数据
});

// 注册ShutdownHook
Runtime.getRuntime().addShutdownHook(shutdownHook);

// 其他程序逻辑
System.out.println("Main program is running...");

// 模拟程序执行
try {
Thread.sleep(5000); // 假设程序运行5秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}

// 当程序退出时,ShutdownHook将被触发执行
}
}
作者:双鬼带单
来源:juejin.cn/post/7249286832168566840

收起阅读 »

websocket 实时通信实现

web
轮询和websocket对比 开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式: 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果 这种方式比较古老,但是兼容性...
继续阅读 »

轮询和websocket对比


开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式:




  1. 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果


    这种方式比较古老,但是兼容性强。


    缺点就是不断请求,耗费了大量的带宽和 CPU 资源,而且存在一定的延迟性




  2. websocket 长连接:全双工通信,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,更加方便




websocket 实现


创建 websocket 连接


建立ws连接,有如下两种形式:


ws 代表明文,默认端口号为 80,例如ws://http://www.example.com:80, 类似http


wss 代表密文,默认端口号为 443,例如wss://http://www.example.com:443, 使用SSL/TLS加密,类似https


const useWebSocket = (params: wsItem) => {
// 定义传参 url地址 phone手机号
let { url = "", phone = "" } = params;
const ws = (useRef < WebSocket) | (null > null);
// ws数据
const [wsData, setMessage] = (useState < wsDataItem) | (null > null);
// ws状态
const [readyState, setReadyState] =
useState < any > { key: 0, value: "正在连接中" };
// 是否在当前页
const [isLocalPage, setIsLocalPage] = useState(true);

// 创建Websocket
const createWebSocket = () => {
try {
window.slWs = ws.current = new WebSocket(
`wss://${url}/ws/message/${phone}`
);
// todo 全局定义发送函数
window.slWs.sendMessage = sendMessage;
// todo 准备初始化
initWebSocket();
} catch (error) {
// 创建失败需要进行异常捕获
slLog.error("ws创建失败", error);
// todo 准备重连
reconnect();
}
};

return { isLocalPage, wsData, closeWebSocket, sendMessage };
};

初始化 websocket


当前的连接状态定义如下,使用常量数组控制:


const stateArr = [
{ key: 0, value: "正在连接中" },
{ key: 1, value: "已经连接并且可以通讯" },
{ key: 2, value: "连接正在关闭" },
{ key: 3, value: "连接已关闭或者没有连接成功" },
];

主要有四个事件,连接成功的回调函数(onopen)、连接关闭的回调函数(onclose)、连接失败的回调函数(onerror)、收到消息的回调函数(onmessage)


const initWebSocket = () => {
ws.current.onopen = (evt) => {
slLog.log("ws建立链接", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 心跳检查重置
keepHeartbeat();
};
ws.current.onclose = (evt) => {
slLog.log("ws链接已关闭", evt);
};
ws.current.onerror = (evt) => {
slLog.log("ws链接错误", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 重连
reconnect();
};
ws.current.onmessage = (evt) => {
slLog.log("ws接受消息", evt.data);
if (evt && evt.data) {
setMessage({ ...JSON.parse(evt.data) });
}
};
};

ws_1.png


websocket 心跳机制


在使用 ws 过程中,可能因为网络异常或者网络比较差,导致 ws 断开链接了,此时 onclose 事件未执行,无法知道 ws 连接情况。就需要有一个心跳机制,监控 ws 连接情况,断开后,可以进行重连操作。


目前的实现方案就是:前端每隔 5s 发送一次心跳消息,服务端连续 1 分钟没收到心跳消息,就可以进行后续异常处理了


const timeout = 5000; // 心跳时间间隔
let timer = null; // 心跳定时器

// 保持心跳
const keepHeartbeat = () => {
timer && clearInterval(timer);
timer = setInterval(() => {
if (ws.current?.readyState == 1) {
// 发送心跳 消息接口可以自己定义
sendMessage({
cmd: "SL602",
content: { type: "heartbeat", desc: "发送心跳维持" },
});
}
}, timeout);
};

如下图所示,为浏览器控制台中的截图,可以查看ws连接请求及消息详情。


注意:正常情况下,是需要对消息进行加密的,最好不要明文传输。


ws_2.png


websocket 重连处理


let lockFlag = false; // 避免重复连接
// 重连
const reconnect = () => {
try {
if (lockFlag) {
// 是否已经执行重连
return;
}
lockFlag = true;
// 没连接上会一直重连
// 设置延迟避免请求过多
lockTimer && clearTimeout(lockTimer);
var lockTimer = setTimeout(() => {
closeWebSocket();
ws.current = null;
createWebSocket();
lockFlag = false;
}, timer);
} catch (err) {
slLog.error("ws重连失败", err);
}
};

websocket 关闭事件


关闭事件需要暴露出去,给外界控制


// 关闭 WebSocket
const closeWebSocket = () => {
ws.current?.close();
};

websocket 发送数据


发送数据时,数据格式定义为对象形式,如{ cmd: '', content: '' }


// 发送数据
const sendMessage = (message) => {
if (ws.current?.readyState === 1) {
// 需要转一下处理
ws.current?.send(JSON.stringify(message));
}
};

页面可见性


监听页面切换到前台,还是后台,可以通过visibilitychange事件处理。


当页面长时间处于后台时,可以进行关闭或者异常的逻辑处理。


// 判断用户是否切换到后台
function visibleChange() {
// 页面变为不可见时触发
if (document.visibilityState === "hidden") {
setIsLocalPage(false);
}
// 页面变为可见时触发
if (document.visibilityState === "visible") {
setIsLocalPage(true);
}
}

useEffect(() => {
// 监听事件
document.addEventListener("visibilitychange", visibleChange);
return () => {
// 监听销毁事件
document.removeEventListener("visibilitychange", visibleChange);
};
}, []);

页面关闭


页面刷新或者是页面窗口关闭时,需要做一些销毁、清除的操作,可以通过如下事件执行:


beforeunload:当浏览器窗口关闭或者刷新时会触发该事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。


onunload:当文档或一个子资源正在被卸载时,触发该事件。beforeunload在其前面执行,如果点的浏览器取消按钮,不会执行到该处。


function beforeunload(ev) {
const e = ev || window.event;
// 阻止默认事件
e.preventDefault();
if (e) {
e.returnValue = "关闭提示";
}
return "关闭提示";
}
function onunload() {
// 执行关闭事件
ws.current?.close();
}

useEffect(() => {
// 初始化
window.addEventListener("beforeunload", beforeunload);
window.addEventListener("unload", onunload);
return () => {
// 销毁
window.removeEventListener("beforeunload", beforeunload);
window.removeEventListener("unload", onunload);
};
}, []);

执行 beforeunload 事件时,会有如下取消、确认弹框


ws_3.png


参考文档:



作者:时光足迹
来源:juejin.cn/post/7249204284180086842
收起阅读 »