注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

事件传递(android)常见面试题

一.事件分发传递流程 Android 事件分发传递流程主要分为三个阶段:事件分发阶段、事件捕获阶段和事件处理阶段。下面我将进一步解释这三个阶段的具体内容: 事件分发阶段 事件分发阶段是从 Activity 的 dispatchTouchEvent() 方法...
继续阅读 »

一.事件分发传递流程


Android 事件分发传递流程主要分为三个阶段:事件分发阶段、事件捕获阶段和事件处理阶段。下面我将进一步解释这三个阶段的具体内容:



  1. 事件分发阶段


事件分发阶段是从 Activity 的 dispatchTouchEvent() 方法开始,在这一阶段中,系统会先将事件传递给 ViewRootImpl,再由 ViewRootImpl 分发到 Activity 的 decorView,最后通过 View 的 onTouchEvent() 方法交给子 view。


具体流程如下:




  1. Activity 从 ViewRootImpl 中获取 MotionEvent 事件。




  2. ViewRootImpl 调用 ViewGroup 中的 dispatchTouchEvent() 方法,将事件分发给对应的子 View 进行处理。




  3. 子 View 依次递归处理事件,如果有子 View 拦截了事件,那么事件就不会再继续传递下去。




  4. 如果根据 onTouchEvent() 返回结果判断当前 View 处理了事件,那么事件就不会向子 View 再次传递。




  5. 如果事件都没有被拦截,那么这个事件将传递到 Activity 中的 onTouchEvent() 方法中。




  6. 事件捕获阶段




事件捕获阶段是指事件从根 View 开始,依次由父 View 捕获,直到当前 View,同时将事件自下向上传递给其祖先 View 的一种机制。主要是为了方便更改事件的属性,例如事件的坐标等。


具体流程如下:




  1. 系统会先将事件传递给该事件的目标 View,并将事件从根节点向下传递。




  2. 在向下子 View 传递事件时,父级 View 可以拦截事件并记录事件的坐标。




  3. 当事件传递到目标 View 的时候,事件的坐标信息会被用来进行对 View 执行操作的计算和判定。




  4. 如果当前 View 拦截了事件,那么此后的再次触摸事件都会被该 View 标记为 “拦截状态”,直到事件处理完毕,被放手传递给上级 View。




  5. 事件处理阶段




事件处理阶段是指最终处理 MotionEvent 的对象(除系统级别的)或者被设置成 Action.Cancel 的最后一个 View。在处理阶段中,会根据 View 的事件类型判断该事件是否可以被处理,如果可以则会继续进行处理,否则会将事件传递给父 View 或者其他 View 进行处理,直至事件被处理或者被放弃。


具体流程如下:




  1. 当一个 View 接收到一个 MotionEvent 事件时,它会首先检查自己的 onTouchEvent() 方法是否可以处理该事件。




  2. 如果当前 View 无法处理该事件,那么事件会被传递给上一级父 View 进行处理。




  3. 如果事件一直没有被处理,那么最终会将事件交给系统完成处理。




总的来说,Android 事件分发传递流程的机制是基于 View 树的节点结构,并且支持通过拦截事件、处理事件和返回事件等手段实现对事件的监测、干涉和响应。对于 Android 开发人员而言,深刻理解这个事件流程,有助于更完善地开发自己的 Android 应用程序。


二. Android常见的事件类型以及监听方式


Android 事件驱动机制是指通过一系列的事件监听器和回调函数来实现对用户交互操作的监听和相应的机制。Android 系统支持多种类型的事件监听器,包括触摸事件监听器、键盘事件监听器、手势事件监听器等,并且通过回调函数来实时监听用户的操作,从而实现对应用程序的控制和交互。下面我将分别介绍一下 Android 事件驱动机制中的各种监听器和回调函数:



  1. 触摸事件监听器


触摸事件监听器是 Android 事件驱动机制中的核心部分,主要用于对触摸事件的监听和处理。在 Android 中,触摸事件可以通过 MotionEvent 类来描述,主要包括三种基本的事件类型:ACTION_DOWN、ACTION_MOVE 和 ACTION_UP。可以通过实现 OnTouchListener 接口来监听触摸事件,并重写 onTouch() 方法来处理事件。例如:

view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 处理按下事件
break;
case MotionEvent.ACTION_MOVE:
// 处理移动事件
break;
case MotionEvent.ACTION_UP:
// 处理抬起事件
break;
}
return false;
}
});


  1. 键盘事件监听器


键盘事件监听器与触摸事件监听器类似,主要用于对键盘事件的监听和处理。在 Android 中,键盘事件可以通过 KeyEvent 类来描述,主要包括 ACTION_DOWN 和 ACTION_UP 两种基本的事件类型。可以通过实现 OnKeyListener 接口来监听键盘事件,并重写 onKey() 方法来处理事件。例如:

view.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
// 处理音量键加事件
break;
case KeyEvent.KEYCODE_VOLUME_DOWN:
// 处理音量键减事件
break;
}
}
return false;
}
});


  1. 手势事件监听器


手势事件监听器主要用于对手势事件的监听和处理。在 Android 中,手势事件需要使用 GestureDetector 类来进行监听,并实现 OnGestureListener 和 OnDoubleTapListener 接口来处理手势事件。例如:

// 在 Activity 中实例化 GestureDetector,并将它绑定到 View 上
GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
// 处理单击事件
return super.onSingleTapConfirmed(e);
}

@Override
public boolean onDoubleTap(MotionEvent e) {
// 处理双击事件
return super.onDoubleTap(e);
}
});

// 绑定 OnTouchListener 和 OnGestureListener
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
});

除了上述的三种事件监听器,Android 还提供了多种其他类型的事件监听器,例如文本变化监听器、列表滚动监听器、视图绘制监听器等,这些监听器通过实现相应的接口和重写对应的回调函数来实现对应用程序的控制和响应。


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

Android 中将多个子模块合并成一个 aar

1. 项目的结构 目标:将模块A打包成 aar,提供给其他工程师使用。 模块之间的关系:模块A引用模块B,模块B引用模块C。 2. 使用 fat-aar-android 三方库进行实现 fat-aar-android 中文使用文档 添加以下代码到工程...
继续阅读 »

1. 项目的结构




image.png


目标:将模块A打包成 aar,提供给其他工程师使用。


模块之间的关系:模块A引用模块B,模块B引用模块C。


2. 使用 fat-aar-android 三方库进行实现




  1. 添加以下代码到工程根目录的build.gradle文件中:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.github.kezong:fat-aar:1.3.8'
}
}


  1. 添加以下代码到主模块的build.gradle中(Module A):
apply plugin: 'com.kezong.fat-aar'


  1. 添加以下代码到主模块的build.gradle中(Module A):
embed project(path: ':ModuleB', configuration: 'default')
embed project(path: ':ModuleC', configuration: 'default')


Module B 引用 Module C,则需要在 Module B 的 build.gradle中进行引用


eg: implementation project(path: ':Module B')




  1. 执行 assemble 命令
# assemble all 
./gradlew :ModuleA:assemble

到这里的话就顺利完成了,遇到其它问题的话,我暂时不会。


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

为什么需要拥有「不靠工作赚钱的技能」

为什么要不靠工作赚钱 一个关于时间的故事 前几天看到一个故事,有一个探险家路过一个村庄,发现村庄外竖立了很多墓碑,这些墓碑上记录了这些过世的人的生命时间。 探险家感到很好奇,因为这些人的生命时间都非常短,短的只有一两年,长的也只有不到十年。 探险家感到很悲伤,...
继续阅读 »

为什么要不靠工作赚钱


一个关于时间的故事


前几天看到一个故事,有一个探险家路过一个村庄,发现村庄外竖立了很多墓碑,这些墓碑上记录了这些过世的人的生命时间。


探险家感到很好奇,因为这些人的生命时间都非常短,短的只有一两年,长的也只有不到十年。


探险家感到很悲伤,以为这个村子里发生了什么意外,导致这么多小孩去世,于是他进入这个村庄,想要一探究竟。


他问村子里的人,这里曾经是否发生过灾难,村子里的人表示并没有啊,这里从来都是一个非常安静、祥和的村庄。


于是探险家把村口墓碑的事情告诉了村里人,问为什么死者都是可怜的孩子呢,村里人一听哈哈大笑起来。


原来,这个村子有一个习俗,每个人都会有一个记录时间的册子。当感到快乐的时候,就把这些时间记录在册子上。


等到这个人离世的时候,人们会打开他的本子,把这个人所有快乐的时间加在一起,刻在墓碑上,所以墓碑上的时间都特别短。


其实人生真正快乐的时间竟然如此短暂。


除去睡觉的时间、焦虑的时间、难过的时间、忙碌奔波的时间,真正留给自己享受生活的时间其实非常少。


工作大概率不能带来快乐


工作通常占了我们人生很大一部分的时间,而工作又大概率不能带来快乐。


如果能找到一份自己很喜欢热爱,并且能从中收获非常多成就感和快乐的工作,当然很好。但大部分人没有这样的幸运。


而且有的时候,明明很喜欢一件事情,但是把这件事情变成工作后,就会丧失之前的热情。


在成为程序员之前,我是非常喜欢写代码的,我觉得这件事情能带给我很大的成就感。其实就在工作的前几年,我也很享受写代码的感觉。


但是最近,却感觉越来越疲惫,工作中单纯的写代码的时间越来越少,更多的是与人沟通扯皮的过程。这样的工作内容,已经渐渐不再是我喜欢中的样子。


如果能不靠工作赚钱,会多出很多时间,去做自己喜欢的快乐的事情。


工作无法带来安全感


公司的存在,就是为了让各个岗位标准化,大家都做很小的一部分螺丝钉的工作,这样公司离开了谁都能马上找到一个类似的螺丝钉替换上。


但对于个人来说,如果你只拥有螺丝钉的技能,离开了公司这个平台,可能会发现,你的技能一文不值。


35岁危机是很多程序员都在关注和担忧的问题。在互联网行业,永远都有比你年轻、你比便宜、比你能加班的人出现,那么你的优势在哪里。


国内的互联网其实不需要太多高深的技术,更多的是需要快速迭代、堆人力堆时间。


如果没有不可替代性(其实这在公司的发展中就是伪命题),等到中年家庭压力变大、工作时间变少、薪资还高,自然会被更便宜的人力所取代。


在现在行业整体下行的情况下,各个公司都在降本增效,更会追求人力的性价比。


不靠工作赚钱


靠工作赚钱,必须付出大量的时间成本,而人真正的价值,不在于拥有多少金钱,而在于拥有多少有效时间。


金钱只是身外之物,时间才是一个人最宝贵的财富。用最宝贵的财富,去换取身外之物,是很不划算的事情。


生命只有一次,去追求自己喜欢的事情,真正把时间好好利用起来,去体验人生,才能不虚此行。


怎样才能不靠工作赚钱


凯文凯利曾经说过一个1000个铁杆粉丝的理论,意思是,如果你有1000个铁杆粉丝,就可以靠创作生存。


粉丝数量不需要太多,但是一定要对足够支持,愿意主动为你的创作付费。


发展一个真正属于自己的事业,在一个细分领域上,做到领先水平,通过满足这个领域内的需求获取收入。


如果你能在某个垂直领域里,成为专家,有自己独一无二的价值,有固定的粉丝,那这些价值是无法被他人剥夺的。


在这个领域上的积累,也不是一两年就能完成的,可能需要5-10年的长期积累。


在积累的前期,可以先通过副业的形式,利用业余时间在职做。


等到副业的收入可以几乎与主业匹配时,就可以考虑放弃主业全职做副业了。


寻找好的副业方向


好的副业一定要能撬动更大的杠杆。


因为主业通常是没有什么杠杆的,很简单的用时间和劳动来换金钱的模式。


如果副业,还是选择同样的,用时间换金钱的套路,那收入也是固定的,没有太大的想象空间。


所以,我觉得好的副业,一定是能撬动更大收益的方式,一旦成功能带来大量的超额收益。


比如,接私活就不是一个好的副业,因为接私活的收益完全取决于你干了多少活,手停口停,无法带来被动收益。


程序员可以考虑的方向


1. 投资


身边炒股的程序员非常多,赚钱的也很多,程序员与一般的股市投资者相比,通常还是有一定的优势。


首先是基础知识,在炒股之前,掌握多一些经济金融知识,学习基本面分析,还是非常有帮助的。


其次是数据分析能力,程序员可以借助自己在代码方面的优势,做一些数据分析的工作,帮助进行买卖决策。


2. 做在线课程


做在线课程,本质也是在做内容,必须要先有优质的内容,后面的运营和推广才有意义。


这个副业的好处是,只需要制作一次课程,就可以一直售卖,成本有限,而收益无限。


而且做在线课程,对本职工作通常也有帮助。


因为要做出某个方向上的好课程,需要对这个领域有深入的了解,如果和主业是相同的方向,对主业也会有积极的帮助。


3. 做自媒体


自媒体也是打造个人IP,个人影响力的重要渠道。需要花费不少的精力,持续输出优质内容。


和做在线课程一样,内容才是王道。很多人其实把自媒体和在线课程一起在做。


不需要等到比99%的人都厉害了才开始输出,如果目前的水平只要比70%的人强,那你输出的东西也可以帮助到70%的人。


所以不用等你完全准备好了才开始,先做起来,在做的过程中慢慢锻炼能力,也是可以的。


4. 运营自己的产品


身边有不少程序员同事,全职或兼职做自己的产品,前几年我也和同学合作开发过一些App产品。


我个人其实很喜欢开发一款产品的过程,那段时间我每天下班回到家后,就坐到书桌前敲代码,一直写到1点也不觉得累。看到每天都有人购买我们App的会员,就觉得非常开心。


对于喜欢做产品的同学,开发和运营自己的产品,也是一个不错的选择,而且一旦项目稳定之后,收入也是很不错的。


更多副业方向


我上面介绍的几个副业方向仅仅只是冰山一角,而且这些也是被大家尝试最多的方向,可能也是最卷的方向。


我个人目前准备尝试3自媒体和4运营自己的产品。


做自媒体的原因,我前面的文章也有写到,更多的是想分享一些东西,以输出push输入,帮助自己整理出知识体系。如果在这个过程中能帮助到有需要的小伙伴,我也会很开心。但这个过程中,我不强求一定要涨粉一定要赚钱。


尝试运营自己产品的原因,是因为我个人很喜欢做一款产品的感觉,这个会给我带来很大的成就感。


大家可以根据自己的观察,去发现一些小众的未被满足的需求,然后想办法去满足这些需求。可能这个才是不卷,又能获得不错收益的方式。


每个人的特长和喜欢的事情都不一样,想做的擅长的事情也都不一样,如果能找到你有而别人没有的技能,那就能走上不卷的道路。


读这篇文章的你,有没有什么你有别人没有的特长呢?如果实现了财务自由,最想去做什么呢?


欢迎大家评论区交流呀~


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

228欢乐马事件,希望大家都能平安健

我这个人体质很奇怪,总能遇到一些奇怪的事。 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。 比如两年前去福州出差,差点永远回不了家。 比如十点从实验室下班,被人半路拦住。 比如这次,被人冒充 (在我心里这事和前几件同样恶劣) 不过幸好每次都是化险为...
继续阅读 »

我这个人体质很奇怪,总能遇到一些奇怪的事。



  • 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。

  • 比如两年前去福州出差,差点永远回不了家。

  • 比如十点从实验室下班,被人半路拦住。

  • 比如这次,被人冒充 (在我心里这事和前几件同样恶劣)


不过幸好每次都是化险为夷,所以我顺顺利利活到现在。




事情起因是这样的:


去年朋友B突然告诉我:发现了你的闲鱼账号。


:我没有闲鱼啊?


他给我截图,那个人卖掘金的周边,名字叫LolitaAnn


image.png


因为我遇到太多离谱的事,再加上看的一堆被冒充的新闻,所以我第一反应是:这人也在冒充我


当时朋友F 说我太敏感了,他觉得只是巧合。


但我觉得不是巧合,因为LolitaAnn是我自己造的词。




把我的沸点搬到小红书


又是某一天, 朋友H告诉我:你的小红书上热门了。


:?我没有小红书啊?


然后他们给我看,有个人的小红书完全照搬我的沸点。


为此我又下载小红书和他对线。起初正常交涉,但是让他删掉,他直接不回复我了,最后还是投诉他,被小红书官方删掉的。


image.png


现在想了想,ip一样,极有可能是一个人干的。




闲鱼再次被挖出来


今年,有人在掘金群里说我卖周边。


我跑到那个群解释,说我被人冒充了。


群友很热心,都去举报那个人的昵称。昵称被举报下去了。


但是几天之后:


image.png


看到有人提醒我,它名字又改回来了。


当时以为是热心群友,后来知道就是它冒充我,现在回想起来一阵恶寒。


把名字改回去之后还在群里跟炫耀一样,心里想着:我改回来了,你们不知道是我吧。




冒充我的人被揪出来了


2.28的时候, 朋友C突然给我发了一段聊天记录。


是它在群里说 它的咸鱼什么掘金周边都有。结果打开一看,闲鱼是我的名字和头像。


image.png


事情到今天终于知道是谁冒充我了


虽然Smile只是它的微信小号之一,都没实名认证。但是我还是知道了一些强哥的信息。


发现是谁冒充我,我第一反应必然是喷一顿。


刚在群里被我骂完,它脑子也不太好使,马上跑去改了自己掘金和闲鱼的名字。这不就是自爆了? 证明咸鱼和掘金都是他的号。


奔跑姐妹兄弟(原名一只小小黄鸭) ←点击链接即可鞭尸。


image.png




牵扯出一堆小号


本来我以为事情已经结束了,我就去群里吐槽他。结果一堆认识他的掘友们给我说它还有别的掘金号。因为它和不同掘友用不同掘金号认识的。所以掘友们给我提供了一堆。我就挨个搜。


直到我看到了这两条:


image.png


因为我搜欢乐马出来上万个同名账号, 再加上他说自己有脚本,当时我以为都是他的小号。


后来掘友们提醒我,欢乐马是微信默认生成的,所以有一堆欢乐马,不一定是他的小号。


但是我确信他有很多小号,比如:


image.png


比如咸鱼卖了六七个掘金鼠标垫,卖了掘金的国行switch……




  • 你们有没有想过为什么fixbug不许助攻了




  • 你们有没有想过为什么矿石贬值了,兑换商店越来越贵了?




  • 你们有没有想过为什么掘金活动必得奖励越来越少了?




有这种操作在,普通用户根本没办法玩吧。


所以最后,我就把这个事交给官方了。




处理结果


所幸官方很给力,都处理了,感谢各位官方大大。



本次事件,共涉及主要近期活跃UserID 4个,相关小号570个。 具体可以看
2023 年 2 月社区治理数据公布





我再叨叨几句




  • 卖周边可以, 你别冒充别人卖吧,又不是见不得光,做你自己不丢人。




  • 开小号可以,你别一开开一堆,逼得普通玩家拿不到福利吧。




  • 先成人后成才,不指望能为国家做多大贡献,起码别做蛀虫吧。




  • 又不是没生活,专注点自己的东西,别老偷别人沸点。




  • 我以后改名了嗷。叫Ann的八百万个,别碰瓷你爹了。






最后祝大家生活愉快,正常的掘友们身体健康,工作顺利。


不正常的也收敛点,别把自己搞进去。


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

特斯拉靠谱不靠谱 ?

去年9月底提了特斯拉modelY丐版,大半年时间开了大概8000多公里,简单总结分享一下,以答一些朋友的疑问。 首先聊聊为什么选择modelY,主要还是 个人喜好 。 具体来说分价格、用车体验两个方面。订购之前大概了解沃尔沃v90cc,奥迪a6旅行版,理想,蔚...
继续阅读 »

去年9月底提了特斯拉modelY丐版,大半年时间开了大概8000多公里,简单总结分享一下,以答一些朋友的疑问。


首先聊聊为什么选择modelY,主要还是 个人喜好


具体来说分价格、用车体验两个方面。订购之前大概了解沃尔沃v90cc,奥迪a6旅行版,理想,蔚来,比亚迪,大众等,试驾过奔驰glc,沃尔沃xc60,极氪001。现在看起来看的功课好像做的很多,但是很多品牌都是走马观花;即使是试驾,里程较短,实际上了解比较有限。这导致了买完就降价的大亏损,被市场割韭菜了。


价格当初心理价位是30~35w左右,恰逢油价高峰,所以更倾向购买电动车,降低一下日常使用成本。其实更偏爱v90cc这种旅行版小众车,但是小众车保有量比较低,二手交易折价会较高,钱包有限,只好放弃。再加上新能源的税费优惠,北京市转换燃油牌照到新能源牌照还有额外补贴。还有比较看重的一点是,国内国外车价格的差异,相对来说特斯拉在国内比国外便宜,让人感觉比较赚。


用车体验主要是平常爱好出去露营,modelY的收纳空间不错,可以带较多的装备;自己又比较懒,特斯拉的极简(廉价)风,比较容易打理,吸尘器加抹布就搞定。智能化比较适中,传统车的车机感觉较浓的安卓风,不是特别流畅,大屏也有点强加块屏硬凑的感觉;新势力呢,则是有点太浮夸了,娱乐性太多,觉得钱都花到娱乐设施上了,车主要还是开的。品牌这块呢,特斯拉争议很大,不吹不黑,作为码农还是对科技 先行者 有一些好感。铁锂比三元锂更稳定,又没有飙车的需求,所以标准版就够用了。


这半年使用起来,整体感觉还是比较满意。


先谈谈几个缺点吧。 第一是特斯拉基本没有客户服务。买完后第一周就降价了,从销售到支持专员,没有任何解释和安慰,官方也没有任何说法,感受还是很糟糕的。买车不是买股票,提车也有专员催促的,多多少少造成提前提车的预期。只能够阿Q自己,自己是原装正版,别人是打折促销货了:) 。


第二是有一些功能很鸡肋,自动辅助FSD会免费试用三个月,但是刚提车完全不熟悉,不敢使用;自动泊车,好像也不行,尝试了一次,感觉都要撞柱子,放弃后再也没有使用;语音识别准确率较低,地图不怎么好用。


第三是OTC升级略频繁,让人有不稳定的感觉。


最后是冬天里程衰减较多,大概打了 六折 吧,这应该是所有纯电的通病。


满意的地方也有一些。第一是里程,买之前对满电可以跑多远没有太了解。官网都标的CLTC工况545公里,实际拿到车才知道可以跑435公里,少了100公里。这样上班每天单程20公里,一周充一次电没有什么压力。


第二是充电时间,同样开始也了解有限。现在国网的超充很方便,速度和特斯拉的超充差不多,相反特斯拉超充价格较贵,还收超时费,一般用的不多。大概情况是100A左右直流桩,可以 50分钟从30%充到90% 。充电时间是不等比的,需要特别介绍一下,初始的10%要预热电池,最后的10%可能要防止过热,都充的较慢。交流桩则充电很慢,5个小时多,一般是夜间休息使用。之前会每周白天跑步时候使用快充(直流)一次电,现在则是晚上使用慢充(交流)一次电,据说慢充对电池较好。


第三是使用体验很很傻瓜式,拿手机上车,给电就走,中途不用换挡,自动Hold;关门就走,不用拉手刹,熄火和锁车;远光近光自动切换,雨刷也可以自动,转向灯可以自动回收。


第四是辅助驾驶在高速上帮助比较大,春节期间驾车去湖北中部,全程来回3000公里,没有轮换驾驶员,中途使用车道保持放松,并没有特别累人的感觉。


以上都是个人感受向的内容,下面简单介绍几个重点问题。


刹车问题


首先来说,特斯拉是有刹车的。普通人会看到特斯拉单踏板模式的说法,进而产生特斯拉没有刹车的印象。这不是愚人节笑话,我有次洗车的时候,旁边大姐就是这样问我的:) 我打开车窗让她看了一下。


所谓单踏板模式,是指可以使用电车油门(其实应该叫电门吧,油门叫习惯了)进行加速和减速控制,和油车的油门加速和刹车减速的逻辑有较大差别。




  • 可以看到驾驶位右侧一样有大个的刹车踏板和小个的油门踏板

  • 特斯拉很简陋,所以需要自己假装一些配件,比如手机导航支架,很怪异


单踏板模式标准说法大概叫 保持 , 可以在刹车的时候利用惯性反转电机给电池充电,从而提高里程。



这样日常使用会有两个需要适应的地方。第一个是如果有 备刹 的习惯,则需要调整。备刹是指在过路口,需要将右脚放刹车上,防止突然来的行人电动车,可以快速刹车。但是单踏板模式下,不踩油门车不走,就没法备刹车。我的应对方式是,路口减速,多观察,同时养成车辆报警就移动脚踩刹车的习惯。


第二个是停车的时候,不给油不走,而且给油走的还挺快,需要注意。


总体来说,我个人觉得,单踏板是可以适应的,适应后用起来还是不错,但是尽量别油车和电车混着开。就像安卓和苹果混着用,有时候会迷茫一下下。


至于刹不住车的问题,不在讨论范围。开车都要谨慎驾驶,车撞了还好,人受伤可受不了,生命权至上。


长途问题


电动车能够不能够跑长途?我的答案是能够,但是不多。春节期间,单程1200公里的长途,出于安全,中途都住了一晚,如果是油车按说是不需要住宿的。



高德导航可以使用新能源模式,提醒那些服务区可以进行充电补能。上图显示需要进行三次充能,每次充能按一个小时算,看起来只会比油车慢3个小时。但是这些是理论值,实际上我是充了4次,因为叠合了冬天气温低。


除了电池电量对长途有影响,还有一个重要的点是电车的工作模式。电车是高速状况下能耗偏高,低速状况下能够较低,这和油车是相反的。一般推荐是时速110比较高效,所以高速上实际跑的比油车慢。


另外就是服务区的充电状况了,目前是服务区的桩少,车也不多,大多数情况下不用排队。实际上油车有时候加油也是要排长队的。需要提前做好规划,最好副驾协助进行充电规划。


如果再让我选择跑超过1000公里的长途,我大概不会选电车了:)


能耗问题


modelY标配是435公里,这是新车满电理论状况。实际上半年后,我这里显示是433,衰减了2公里。我猜测原因除了电池衰减,应该还有胎压的关系。刚提车胎压是3.1,现在都是2.8,摩擦力增大,所以持续里程也有减少。




  • 315+118=433


从上图可以看到最近的平均能耗要比总体能耗低不少,应该是天气和日常市区行车的原因。里程的计算也是根据驾驶习惯,实时动态计算的,比如下面的能耗曲线,预测里程是368,比表现高了53公里。




  • 368-315=53


成本问题


电车基本不用怎么维护,比如不用油车常见的半年/1万公里的保养。我的近期维护清单如下, 近期看就是1年换一下滤网:



上图可以看到制动液检测的进度条有bug,实际上车机是会有bug。有发生过一次高速上车机黑屏的问题,第一次碰到吓的够呛,所以一定要多了解这个大玩具。还有一次,从延庆回城,里程能耗特别低感觉计算也不太对。


最省钱的地方就是电费。我贴了一个直流和一个交流的订单如下:



简单的说,使用公用电桩一次充电大概60,一周一次,一个月大概260块钱,平均 = 260 / ( 40*22 )三毛钱一公里。 使用公用桩都会收取服务费,如果是私桩,这一部分可以免去,还可以大大降低,大概可以达到 一毛钱一公里 , 对比油车大概是 一块钱一公里,是真省。


好了,以上就是modelY的开箱报告了,希望对选车的朋友有所帮助。 当然,电车就是未来!


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

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,

Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

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


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


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

程序员增强自控力的方法

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法: 1. 设定明确的目标和计划 制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任...
继续阅读 »

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法:


1. 设定明确的目标和计划


制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任务清单、时间追踪工具等,来帮助我们控制时间并更有效地完成任务。


2. 掌控情绪


作为程序员,我们需要面对很多挑战和压力,容易受到情绪的影响。因此,掌握情绪是一个非常重要的技能。可以通过冥想、呼吸练习、运动等方法,来帮助我们保持冷静、积极和乐观的心态。


3. 管理焦虑和压力


焦虑和压力是我们常常遇到的问题之一,所以我们需要学会如何管理它们。我们可以使用放松技巧、适度锻炼、交流沟通等方法,来减轻我们的焦虑和压力。


4. 培养自律习惯


自律是一个非常重要的品质。我们可以通过设定目标、建立规律和强化自我控制等方式,来培养自律习惯。


5. 自我反思和反馈


经常进行自我反思和反馈可以帮助我们更好地了解自己的优缺点和行为模式。我们可以使用反馈工具或与他人交流,来帮助我们成长和改进。


6. 持续学习和自我发展


程序员需要不断学习和自我发展,以保持竞争力和提升自己的技能。通过阅读书籍、参加培训、探究新技术等方式,可以帮助我们持续成长,增强自我控制力。


结论


自控力是我们工作和生活中重要的的品质之一,可以帮助我们更好地应对各种挑战和压力。通过设定目标、掌控情绪、管理焦虑和压力、培养自律习惯、自我反思和反馈、持续学习和自我发展等方法,我们可以帮助自己增强自我控制

作者:郝学胜
来源:juejin.cn/post/7241015051661312061
能力并提高工作效率。

收起阅读 »

来自智能垃圾桶的诱惑,嵌入式开发初探

背景原因 最近裸辞离职在老家,正在收拾老家的房子准备住进去,但迫于经济压力,只能先装非常有必要的东西,例如床、马桶、浴室柜等。 但是垃圾桶是生活必备品,我发现大家现在都在用智能垃圾桶,那种可以感应开盖的,还有那种自动封袋的。我极其的羡慕。但是迫于经济压力,...
继续阅读 »

背景原因



最近裸辞离职在老家,正在收拾老家的房子准备住进去,但迫于经济压力,只能先装非常有必要的东西,例如床、马桶、浴室柜等。



但是垃圾桶是生活必备品,我发现大家现在都在用智能垃圾桶,那种可以感应开盖的,还有那种自动封袋的。我极其的羡慕。但是迫于经济压力,我没有足够的资金购买智能垃圾桶!



作为一个学美术的程序员,我在想我要不要去少儿美术培训教小孩,或者去街头卖艺,甚至去给人画遗照赚点钱呢?我想了想算了,给别人留点活路吧,我这么专业的美术人才去了,那些老师、街头艺人、画师不得喝西北风去。我想了想,我除了是学美术的,我还是个程序员啊!


虽然我没有学过嵌入式、硬件、IOT等等技术,但是入门应该不太困难吧!已经好多年没有从0开始学习一门技术了,非常怀念当时学习Web技术的那种感觉,可以没日没夜的去探索新知识,学习东西。之前上班的时候根本没有精力去学习或者搞一些有趣的东西,下班只想躺着看动漫,听爽文。既然现在赋闲在家,那就搞一搞吧!


项目启动


俗话说得好,找到一个好师傅就是成功的一半!在这方面我有着得天独厚的优势,我的前公司,北京犬安科技就是一个做车联网安全的公司,这家公司的Slogan是:从硅到云,守护网联汽车安全。可以看出,犬安的硬件软件安全能力都非常的强。像我,天天和测试组的同事去山西面馆吃鱼香肉丝盖饭,喝青岛雪花,他们组里面都是硬件大佬,甚至有人造过火箭。此时不向他们学习,更待何时?


他们告诉我,我需要一个开发板、一个超声波传感器、一个舵机。


我还在研究什么是stm32/esp32的时候,他们告诉我用Arduino,特别简单。我就去研究Arduino,在B站上搜了 Arduino,有一个播放量比较高的系列教程看了一下。我顿时就悟了!


我本来以为我需要买烙铁,焊电路板的,后来我发现,只需要买以上提到的三个东西,外加一个面包版,一些杜邦线就可以,甚至可以不需要面包版。



当然为了学习,我在淘宝上买了一些乱七八糟的东西,比如按钮、各种电阻、各种颜色led灯、面包版。买各种颜色的LED灯的时候,购买欲泛滥了,就想每个颜色都买一些,什么白发红,白发黄,白发翠绿,后来我买回来发现,不亮的情况下都是白色的,我根本分不清颜色。原来那个白发黄的意思就是不亮的时候是白色,亮了以后发黄色的光~我还买了接受RGB三色的LED灯,不知道为啥不是RGBA,难道不能让灯透明吗?



我还发现不同欧姆的电阻他上面的“杠杠”是不一样的。



主要是这玩意太便宜了,两块钱就买好多,但是作为新手玩家来说,根本用不了。我只买了公对公的杜邦线,然后我又单独下单了其他杜邦线,果然两块多就能包邮。



我在不同的店铺里面选了很多配件,包括我使用的主要配件:Arduino uno 开发板、SG90舵机、HC-SR04 超声波传感器。


开搞


设备买回来以后,废了9牛2虎之力才成功的能把我写的程序上传到开发板里。


一开始一直报下面这个错误,网上众说纷纭,始终没有找到解决方案。


avrdude: stk500_getsync() attempt 1 of 10: not in sync: resp=0x30

后来我去淘宝详情页仔细研究了一番才搞明白,我本以为我买的是Arduino的开发板,其实是esp8266,只是兼容Arduino。我从淘宝详情页找到了正确姿势,成功的刷入了程序。


看了 Arduino 的视频,我主要悟出来了什么?


开发板、舵机、传感器上有很多小尖尖或者小洞洞,他叫引脚,我们需要把舵机、传感器的引脚,通过一种名为杜邦线的线(通常有公对公/母对母/公对母,代表线两头是小尖尖还是小洞洞),连接到开发板的引脚上。然后使用程序控制某个引脚的电平向舵机发送信号,或者接受传感器的信号。


第一个项目


我举一个最简单的LED灯闪烁的例子: 线是这么接的:



面包版上面,竖着的五个小洞洞里面其实是连在一起的。我的电路是从一个引脚出来,到了一个电阻上,然后连接一个LED灯,然后接地。 接地的概念应该和负极的概念很像,但是不是,不过我暂且不关心它。 为什么不直接从引脚出来接LED,再接地呢,因为视频上说,LED几乎是没有电阻的,如果直接从串口出来接到LED,直接接地的话,开发板就废了,所以加了个电阻。


接下来我让ChatGPT帮我写一个小LED灯闪烁的代码:



我们可以看到在loop里面,调用了digitalWrite,把那个引脚进行了高低电平的切换,这样就实现了小LED灯闪烁的效果,高电平就相当于给小LED灯供电了。


我们只需要把他写的代码中的引脚的编号改成我们的编号就好了。


每种开发板的引脚编号都不一样!我在淘宝上买的这个板,他没有给我资料,我干了一件特别愚蠢的事情,分别给不同的数字编号供电,插线看哪个亮,就记录对应开发板上的引脚号和数字。


第二天我在网上稍微搜了一下资料就找到了。后来我发现,竟然还有一个编号对应两个引脚的,具体为什么我也不知道。


不过具体怎么给舵机传信号,或者接受传感器的信号呢?如果在 Arduino IDE 里面的话就是简单的调用API就好了。这一块我没写代码,都是ChatGPT帮我写的。



实现目标的主要部分


我使用同样的方法,制作了垃圾桶的功能的电路和代码。 使用超声波传感器的时候,我发现这玩意都好神奇啊,仿佛是魔法一样。 比如超声波传感器,要先发送一个超声波信号出去,然后等待回波信息,拿到发送和接收的时间差,通过计算得到距离。



我在调试超声波传感器的时候,我发现我能听到超声波传感器发出的声音,只要耳朵对着它不用靠太近就可以听得到。我朋友说我是超级耳。


有了LED灯的经验以后,超声波识别距离,调用舵机旋转很快就实现了。



最开始我在考虑这个舵机的力道到底能不能支撑起来垃圾盖子,不过现在感觉力道还是挺大的。不过现在没有办法试,因为我还没有做出来合适的垃圾桶。


接下来


接下来我要去找到一个合适的垃圾桶,我也不确定需要用什么方式去打开垃圾桶的盖子,是不是需要其他的比如初中学过的滑轮、齿轮、杠杆原理什么的?这块还没有开始涉猎,等我接下来研究研究来写第二部分。



作者:明非
来源:juejin.cn/post/7215217068803145785

感谢大家阅读~

收起阅读 »

一个大龄小前端的年终悔恨

web
今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧 仔细想了想今年也没有做什么呀! 真是年纪越大时间越快 为什么有大有小啊? 95的够大了吧 步入前端也才不到3年 So一个大龄的小前端 技术有长进么? 一个PC端项目 用了 react a...
继续阅读 »

image.png




今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧



仔细想了想今年也没有做什么呀! 真是年纪越大时间越快




为什么有大有小啊?


95的够大了吧


步入前端也才不到3年


So一个大龄的小前端


技术有长进么?


一个PC端项目 用了 react antd redux-toolkit react-router ahooks axios 也就这样吧,就一点简单的项目,react熟练了么?有点会用了,可是我工作快3年了,写项目还是要来回查文档,antd用的熟练的时候倒是可以不用去查文档,可是过了就忘了,今天写项目就有点想不起来怎么用了,查了文档才可以继续写下去


有长进么?




  1. react熟练了一些,可以自己看源码了




  2. 自己解决问题的能力有了一点提升




  3. 技术的广度认识有了(23年目标是深度)




  4. 数据结构了解一点了 二叉树 队列 链表 队列 (还学了一点算法,不过忘了🤣)




  5. 写代码喜欢封装组件了




  6. node学了一点又忘了




  7. ts会的多了一点




  8. antd也好一点了,以前在群里问一些小白问题,还好有个大哥经常帮我




  9. css 还是不咋地 不过我刚买了一个掘金小册 s.juejin.cn/ds/hjUap4V[…




生活上有什么说的呢?


生活很好 吃喝不愁


就是太久没有回家了 老家黑龙江 爷爷奶奶年纪大了 有时候想不在杭州了 回哈尔滨吧 这样可以多陪陪他们 可是回哈尔滨基本就是躺平了 回去我能做什么? 继续做前端? 好好补补基础去做一个培训讲师?


回去的好处是房子压力小 可以买一个车 每天正常上班 下班陪家人 到家有饭吃 想想也挺好


不过女朋友想在杭州,所以我还会在杭州闯一下的,毕竟我们在杭州买房子也是可以努力一下的


女朋友对我很好 我们在一起也快3年了 我刚步入前端的时候我们刚在一起 2020-05-20 她把我照顾的很好 她很喜欢我我感觉的到 我平时不太会表达 其实我是想跟她结婚的我也喜欢她 我对她耐心少了一点 这一点我会改的 以后我想多跟她分享我每天发生的事 我想这样她会更开心一点吧


今年她给我做了好多的饭,有段时间上班都是她晚上下班回来做的(她下班的早 离家近) 第二天我们好带去(偶尔我们吃一段时间的轻食) 可是我还是胖了




image.png


2023要怎么做?


我想成为大佬 我想自律一些 还有工资也要多一点吧



  • 开年主要大任务 两个字 搞钱 咱们不多来 15万可以吧 嗯 目标攒15W

  • 紧接上条 要是买 20W-30W的车 那你可以少攒点 8万到10万 (买车尽量贷款10W)

  • MD 减肥可以吧 你不看看你多胖了呀 175的身高 快170斤了减到140斤 (总觉得不胖,壮)

  • 技术一定要提升 你不能再这样下去了 要被清除地~





技术我们来好好的捋一下,该怎么提升




  1. 现有项目自己codeReview(改改你的垃圾代码吧)

  2. css多学点

    1. css in js

    2. Tailwindcss

    3. css Module less 写法好好研究一下

    4. css 相关配置要会



  3. react源码要搞一下

    1. fiber

    2. hooks

    3. diff

    4. 一些相关的库的源码 (router,redux等)



  4. webpack vite (要能写出来插件)

  5. node 这个一定要学会 (最起码能自己写接口和工具)

  6. 文章要搞起来 (最起码要写20篇,前5篇要一周一篇文章)


2023 搞一个 pc端 H5 小程序 后台接口 要齐全 必须搞出来一个 加

作者:奈斯啊小刘超奈斯_
来源:juejin.cn/post/7174789490580389925
油💪🏻

收起阅读 »

转转商品到手价

1 背景介绍 1.1 问题 搜索结果落地页,按照价格筛选及排序,结果不太准确; 用户按照价格筛选后的商品与实际存在的商品不符,可能会缺失部分商品,影响到用户购物体验。 1.2 到手价模块在促销架构中所处的位置 在整体架构中,商品的到手价涉及红包,...
继续阅读 »

1 背景介绍


1.1 问题




  • 搜索结果落地页,按照价格筛选及排序,结果不太准确;




  • 用户按照价格筛选后的商品与实际存在的商品不符,可能会缺失部分商品,影响到用户购物体验。




image


1.2 到手价模块在促销架构中所处的位置


在整体架构中,商品的到手价涉及红包,活动等模块的协同工作。通过将商品售价、红包、活动等因素纳入综合考虑,计算出最终的到手价,为顾客提供良好的购物体验。


image


2 设计目标



  • 体验:用户及时看到商品的最新到手价,提升用户购物体验;

  • 实时性:由原来的半小时看到商品的最新到手价,提升到3分钟内。


3 技术方案核心点


3.1 影响因素


image


影响商品到手价的主要因素:




  1. 商品,发布或改价;




  2. 红包,新增/删除或过期;




  3. 活动/会馆,加入或踢出。




3.2 计算公式


image


如图所示,商品详情页到手价的优惠项明细可用公式总结如下:


商品的到手价 = 商品原价 - 活动促销金额 - 红包最大优惠金额


4 落地过程及效果


image


随着业务需求的变化,系统也需要不断地增加新功能或者对现有功能进行改进,通过版本演进,可以逐步引入新的功能模块或优化现有模块,以满足业务的需求。


商品的到手价设计也是遵循这样规则,从开始的v1.0快速开发上线,响应业务; 到v2.0,v3.0进行性能优化,升级改造,使用户体验更佳,更及时。


4.1 v1.0流程


image


v1.0流程一共分为两步:




  1. 定时任务拉取拉取特定业务线的全量商品,将这批商品全量推送给各个接入方;




  2. 促销系统提供回查接口,根据商品id等参数,查询商品的到手价;




4.2 v1.0任务及接口


image




  1. v1.0任务执行时间长,偶尔还会出现执行失败的情况;而在正常情况下用户大概需要半小时左右才能感知到最新的商品到手价;




  2. 需要提供额外的单商品及批量商品接口查询到手价,无疑会对系统产生额外的查询压力,随着接入方的增加,接口qps会成比例增加;




4.3 v2.0设计


针对v1.0版本长达半个小时更新一次到手价问题,v2.0解决方案如下:



  • 实时处理部分


商品上架/商品改价;


商品加入/踢出活动;


商品加入/踢出会馆;


这部分数据的特点是,上述这些操作是能实时拿到商品的infoId,基于这些商品的infoId,可以立即计算这些商品的到手价并更新出去。


image


商品发布/改价,加入活动/会馆,踢出活动/会馆;接收这些情况下触发的mq消息,携带商品infoId,直接计算到手价。



  • 3min任务,计算特定业务线的全量商品到手价


红包: 新增/更新/删除/过期;


这部分数据的特点是,一是不能很容易能圈出受到影响的这批商品infoIds,二是有些红包的领取和使用范围可能会涉及绝大部分的商品,甚至有些时候大型促销会配置一些全平台红包,影响范围就是全量商品。


综上,结合这两种情况,以及现有的接口及能力实现v2.0;


image


推送商品的到手价,消息体格式如下,包括商品id,平台类型,到手价:


[
{"infoId":"16606xxxxxxx174465"
"ptType":"10""
realPrice"
:"638000"}
]

image


首先在Redis维护全量商品,根据商品上架/下架消息,新增或删除队列中的商品;其次将全量商品保存在10000队列中,每个队列只存一部分商品:


queue1=[infoId...]

queue2=[infoId...]

...

queue9999=[infoId...]

右图显示的是每个队列存储的商品数,队列商品数使其能保持在同一个量级。


image


多线程并发计算,每个线程只计算自己队列的商品到手价即可;同时增加监控及告警,查看每个线程的计算耗时,右图可以看到大概在120s内各线程计算完成。


注意事项:




  1. 避免无意义的计算: 将每次变化的红包维护在一个队列中,任务执行时判断是否有红包更新;




  2. 并发问题: 当任务正在执行中时,此时恰巧商品有变化(改价,加入活动等),将此次商品放入补偿队列中,延迟执行;




综上,结合这两种场景可以做到:




  1. 某些场景影响商品的到手价(如改价等),携带了商品infoId,能做到实时计算并立即推送最新的到手价;




  2. 拆分多个商品队列,并发计算; 各司其职,每个线程只计算自己队列商品的到手价;




  3. 降低促销系统压力,接入方只需要监听到手价消息即可。




4.4 v3.0设计


image


可以看到随着商品数的增加,计算耗时也成比例增加。


image


解决办法:




  1. 使用分片,v2.0是将一个大任务,由jvm多线程并发执行各自队列的商品;
    v3.0则是将这个大任务,由多个分片去执行各自队列中的商品,使其分布式执行来提高任务的执行效率和可靠性;




  2. 扩展性及稳定性强,随着商品的增多,可以适当增加分片数,降低计算耗时。




5 总结




  • 系统扩展性 数据量日渐增大,系统要能做升级扩展;




  • 系统稳定性 业务迭代,架构升级,保持系统稳定;




  • 完备的监控告警 及时的监控告警,快速发现问题,解决问题;




  • 演进原则 早期不过度设计,不同时期采用不同架构,持续迭代。







关于作者



熊先泽,转转交易营销技术部研发工程师。代码创造未来,勇于挑战,不断学习,不断成长。



转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~


作者:转转技术团队
来源:juejin.cn/post/7240006787947135034

收起阅读 »

分享近期研究的 6 款开源API网关

随着API越来越广泛和规范化,对标准化、安全协议和可扩展性的需求呈指数级增长。随着对微服务的兴趣激增,这一点尤其如此,微服务依赖于API进行通信。API网关通过一个相对容易实现的解决方案来满足这些需求。 也许最重要的是,API网关充当用户和数据之间的中介。AP...
继续阅读 »

随着API越来越广泛和规范化,对标准化、安全协议和可扩展性的需求呈指数级增长。随着对微服务的兴趣激增,这一点尤其如此,微服务依赖于API进行通信。API网关通过一个相对容易实现的解决方案来满足这些需求。


也许最重要的是,API网关充当用户和数据之间的中介。API网关是针对不正确暴露的端点的基本故障保护,而这些端点是黑客最喜欢的目标。考虑到一个被破坏的API在某些情况下可能会产生惊人的灾难性后果,仅此一点就使得API网关值得探索。网关还添加了一个有用的抽象层,这有助于将来验证您的API,防止由于版本控制或后端更改而导致的中断和服务中断。


不幸的是,许多API网关都是专有的,而且价格不便宜!值得庆幸的是,已经有几个开源API网关来满足这一需求。我们已经回顾了六个著名的开源API网关,您可以自行测试,而无需向供应商作出大量承诺。


Kong Gateway (Open Source)


Kong Gateway(OSS)是一个受欢迎的开源API网关,因为它界面流畅、社区活跃、云原生架构和广泛的功能。它速度极快,重量轻。Kong还为许多流行的基于容器和云的环境提供了现成的部署,从Docker到Kubernetes再到AWS。这使您可以轻松地将Kong集成到现有的工作流程中,从而使学习曲线不那么陡峭。


Kong支持日志记录、身份验证、速率限制、故障检测等。更好的是,它有自己的CLI,因此您可以直接从命令行管理Kong并与之交互。您可以在各种发行版上安装开源社区Kong Gateway。基本上,Kong拥有API网关所需的一切。


Tyk Open-Source API Gateway


Tyk被称为“行业最佳API网关”。与我们列表中的其他API网关不同,Tyk确实是开源的,而不仅仅是开放核心或免费增值。它为开源解决方案提供了一系列令人印象深刻的特性和功能。和Kong一样,Tyk也是云原生的,有很多插件可用。Tyk甚至可以用于以REST和GraphQL格式发布自己的API。


Tyk对许多功能都有本机支持,包括各种形式的身份验证、配额、速率限制和版本控制。它甚至可以生成API文档。最令人印象深刻的是,Tyk提供了一个API开发者门户,允许您发布托管API,因此第三方可以注册您的API,甚至管理他们的API密钥。Tyk通过其开源API网关提供了如此多的功能,实在令人难以置信。


KrakenD Open-Source API Gateway


KrakenD的开源API网关是在Go中编写的,它有几个显著的特点,尤其是对微服务的优化。它的可移植性和无状态性是其他强大的卖点,因为它可以在任何地方运行,不需要数据库。由于KrakenDesigner,它比我们列表中的其他一些API网关更灵活、更易于接近,这是一个GUI,它可以让您直观地设计或管理API。您还可以通过简单地编辑JSON文件轻松地编辑API。


KrakenD包括速率限制、过滤、缓存和授权等基本功能,并且提供了比我们提到的其他API网关更多的功能。在不修改源代码的情况下,可以使用许多插件和中间件。它的效率也很高-据维护人员称,KrakenD的吞吐量优于Tyk和Kong的其他API网关。它甚至具有本地GraphQL支持。所有这些,KrakenD的网关非常值得一看。


Gravitee OpenSource API Management


Gravite.io是另一个API网关,它具有一系列令人印象深刻的功能,这次是用Java编写的。Gravitee有三个模块用于发布、监控和记录API:




  • API管理(APIM):APIM是一个开源模块,可以让您完全控制谁访问您的API以及何时何地。




  • 访问管理(AM):Gravite为身份和访问管理提供了一个本地开源授权解决方案。它基于OAuth 2.0/OpenID协议,具有集中的身份验证和授权服务。




  • 警报引擎(AE):警报引擎是一个用于监视API的模块,允许您自定义多渠道通知,以提醒您可疑活动。




Gravitee还具有API设计器Cockpit和命令行界面graviteio-cli。所有这些都使Gravitee成为最广泛的开源API网关之一。您可以在GitHub上查看Gravite.io OpenSource API管理,或直接下载AWS、Docker、Kubernetes、Red Hat,或在此处作为Zip文件。


Apinto Microservice Gateway


显然,Go是编写API网关的流行语言。Apinto API网关用Go编写,旨在管理微服务,并提供API管理所需的所有工具。它支持身份验证、API安全以及流控制。


Apinto支持HTTP转发、多租户管理、访问控制和API访问管理,非常适合微服务或具有多种类型用户的任何开发项目。Apinto还可以通过多功能的用户定义插件系统轻松地为特定用户定制。它还具有API健康检查和仪表板等独特功能。


Apinto Microservice针对性能进行了优化,具有动态路由和负载平衡功能。根据维护人员的说法,Apinto比Nginx或Kong快50%。


Apache APISIX API Gateway


我们将使用世界上最大的开源组织之一Apache软件基金会的一个开源API网关来完善我们的开源API网关列表。Apache APISIX API网关是另一个云原生API网关,具有您目前已经认识到的所有功能——负载平衡、身份验证、速率限制和API安全。然而,有一些特殊的功能,包括多协议支持和Kubernetes入口控制。


关于开源API网关的最后思考


不受限制、不受限制的API访问时代已经结束。随着API使用的广泛普及,有无数理由实现API网关以实现安全性,因为不正确暴露的API端点可能会造成严重损害。API网关可以帮助围绕API设置速率限制,以确保安全使用。而且,如果您向第三方供应商支付高昂的价格,开源选项可能会减少您的每月IT预算。


总之,API网关为您的API环境添加了一个重要的抽象层,这可能是它们最有用的功能。这样的抽象层是防止API端点和用户数据不当暴露的一些最佳方法。然而,几乎同样重要的是它为您的API增加了灵活性。


如果没有抽象层,即使对后端的微小更改也可能导致下游的严重破坏。添加API网关可以使敏捷框架受益,并有助于

作者:CV_coder
来源:juejin.cn/post/7241778027876401213
简化CI/CD管道。

收起阅读 »

一位25岁普通女程序员的年中总结

前言 距离上一次的年中总结已经过去了一年,来深圳的日子也即将一年过半,看了看去年的年终总结,基本上已经全部完成,除了依然没吃胖点,反而有了两三次低血糖之外(T_T)。 工作 去年2月26上完最后一天班晚上到的深圳,3月8号来现在的公司入职(印象比较深是因为入...
继续阅读 »

前言


距离上一次的年中总结已经过去了一年,来深圳的日子也即将一年过半,看了看去年的年终总结,基本上已经全部完成,除了依然没吃胖点,反而有了两三次低血糖之外(T_T)。


image.png


工作


去年2月26上完最后一天班晚上到的深圳,3月8号来现在的公司入职(印象比较深是因为入职当天有500的女神节红包,哈哈哈)。


今年年初的时候,公司换了办公地方,再也不用过地铁转公交的日子了(^▽^),现在可以从坪洲坐8站直接到深大,通勤时间少了一点;


年后陆陆续续做了三四个公司内部用的系统,用到了一个腾讯的UI框架,使用感还不错,顺便学习了TDesign 提供了一个脚手架 tdesign-starter-cli,通过它来初始化项目,有兴趣的可以看看(新手不会搭建脚手架的可以做个参考)。


image.png


image.png


上个月调薪(10%),虽然没有预期的涨薪幅度高,不过涨了总比不涨好(O(∩_∩)O哈哈哈~),经过一次涨薪,也明白了这个涨薪幅度跟什么有直接的关系,如果明年年初不跳槽的话,到年中的时候能涨个15%吧,虽然不多,但是福利好,工作不多,不加班,如果明年离职,掘友们有兴趣的话,可以来找我内推(^▽^)。


学习


今年在一边工作之余,也一直有在坚持学习,当然有时候还是会摸鱼偷懒(捂嘴笑(^▽^))。




  1. 收货



    • 输出30余篇文章(不过写的水平很一般,基本上都是记录自己的学习,没有给广大掘友带来什么收货)

    • 学习了vue3

    • 学习了react(不精通)

    • 学习了react native(为了接项目挣钱)

    • 接了个小程序的项目,顺道回顾了小程序的写法(长时间不写,真的会忘记)




  2. 不足



    • 学习的vue3一直拖延重构,没有实际用到项目中去

    • react前面学着,后面忘着(记性真的差)




生活


早上一口气写完了这半年的工作和学习,到了生活这里,好像卡壳了——




  1. 吐槽


    好像没什么说的,又好像想说的很多,今年这半年,是对这慢无休止的疫情最烦闷最讨厌的半年,马上快八月了,核酸做的已经变得麻木,想必辛苦做核酸的护士们也麻木了吧,这么热的天气做的一次比一次敏捷(敷衍),从年后回到深圳,基本上一天一做,最少的时候三天要做一次。


    后来上海疫情爆发,封城,看着曾经的朋友们封城后的生活,心情五味杂陈,难以言说(此处省略500字)。


    吐槽结束,把刚刚的收起来。




  2. 平淡


    其实除了疫情,无休止的核酸,就是工作,出租屋两点一线的平淡了;趁着清明,和朋友们一起自驾游了一次,我是纯游,因为有俩司机开车,哈哈哈哈。在五一也去了一直没去的珠海玩了一趟,虽然上了外伶仃岛,被困在岛上了,但在海边,心情真的有被治愈。


    图片放到文章最后啦!




总结


曾经看到的一句话:



生活可以忙忙碌碌随大流,思想必须偷偷摸摸求上进



其实以前挺不喜欢这种生活方式的,后来发现自己也是这样的活着,还是和去年文章中一句话说的那样:



行到水穷处,坐看云起时是选择,卧薪尝胆,三千越甲可吞吴也是选择,怎么选,都有理,怎么选,都对



因为我们都在为自己想要的生活,或努力或躺平着。


接下来,还是该工作工作,该学习学习,该玩玩,该吃吃,该喝喝。


晒图



  1. 清明——潮州-南澳岛
    dcee0296084f2dd6ad8489bd2ca416f.jpg


fa2b81d23593eb83b48f18246442024.jpg


0e22613cabf1391c8ee71653afb9c0b.jpg


7413ffa236e32ab90cffd36ed6d9fed.jpg


e77b1233aa72d335cfd5e236b03a5e5.jpg
304e9e51a55173f4ff234a8e27ae0eb.jpg



  1. 五一——珠海-外伶仃岛


b705e7f1638136458d54638e7891c06.jpg


54c1fb0813c4df0e117ab323769b663.jpg


e564f7e708f55b817e0e87fb7f44f17.jpg


0f3e70a4f93e0230def4773ac5489c3.jpg


a16ec85ebee0af6bba8460459863fcd.jpg


a088d73521a1bec7e750a786305ffb1.jpg
3. 吃吃喝喝


f9f79ad3138bf3d0054f2b9bbe4f933.jpg


bdf75dd09aea6c79bce24789a157727.jpg


8feeb742aac2475e6f55e05bcce63b0.jpg

收起阅读 »

变“鼠”为“鸭”——为SVG Path制作FIFO路径变换动画,效果丝滑

web
一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下: 我随便找了一个字体Aa剑豪...
继续阅读 »

一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下:


我随便找了一个字体Aa剑豪体,然后随机选取了两个汉字:,再用上文提到的文章介绍的提取整体字形区块方法取出了SVG:


image.png


image.png


可以看到很简单就提取出了两个字整体的字形,下面用D3做一个简单的变换动画展示:


初始变换


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>鼠鼠我鸭</title>
</head>
<body style="text-align: center"></body>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script type="module">
const _svg = d3
.select("body")
.append("svg")
.attr("width", "1000")
.attr("height", "1000")
.style("zoom", "0.3")
.style("transform", "rotateX(180deg)");
_svg.append("g")
.attr("transform", "translate(0, 160)")
.append("path")
.attr("fill", "#3fd")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", 上面提到的鼠的SVG_Path...)
.transition().delay(1000).duration(1200)
.attr("fill", "#ef2")
.attr("d", 上面提到的鸭的SVG_Path...);
</script>
</html>

这里调整了svg尺寸以及zoomtransform等属性更好的适应画面,还做了个g标签套住并将定位交给它,动画效果如下图所示:


Animation.gif


很明显的看到,效果非常奇怪,怎么一开始就突然变得很乱?一开始乱这一下显得很突兀,这是因为两段图像的path长度就相差很多,绘进方式也完全不一样,很难真正的渐变过去,我试了一个有优化此过程的库d3-interpolate-path,用上去效果也没有什么差别,而且它用的还是d3@v5版本的,不知道怎么path中还会穿插一些NaN的值,很怪异,看来只能自己做了。


想真正的点对点的渐移过去,可能还是有些难的,所以我想出了一个较为简单的方案,实现一种队列式的效果,的笔画慢慢消失,而则跟随在后面逐步画出,实现一种像队列中常说的FIFO(先进先出)的效果


首先就是拆解,做一个while拆分两个字所有的节点,然后再一步步绘上拆出来的节点以验证拆的是否完整,再才能进行后面的处理。


事先要将“鸭鼠”各自的path定义为常量sourceresult,将二者开头的M与结尾的Z都去掉(中间的M不要去掉),因为动画中字形是流动的,起止点不应提前定义。


拆分路径点


const source = 鼠的SVG_Path(没有MZ)...
const result = 鸭的SVG_Path(没有MZ)...
const actionReg = new RegExp(/[a-z]/, "gi");
const data = new Array();
let match;
let lastIndex;
while ((match = actionReg.exec(result))) {
data.push(result.substring(lastIndex, match.index));
lastIndex = match.index;
}
data.push(result.substring(lastIndex));

就这样就能把的部分拆开了,先直接累加到试验一下是否成功:


叠加试验


let tran = g
.append("path")
.attr("fill", "red")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", "M" + source + "Z")
.transition()
.delay(800);

let step = "L";
data.map(item => {
step += item + " ";
tran = tran.transition().attr("d", "M" + source + step.trimEnd() + "Z").duration(20);
});

首先是把上面path独立出来改一改,变成红色的利于观看,然后下面慢慢的拼合上每个节点,效果如下:


Animation.gif


是理想中的效果,那么下一步就是加速FIFO先进先出的变换了:


FIFO先进先出


这一步是不能用SVG动画的,要用setInterval定时器进行动画调节,SVG始终还是只能处理很简单的path变化,效果不如直接变来的好,这里设计成把每一帧的动画存进一个方法数组然后交给setInterval计时器循环执行(写起来比较方便),先是改一下tran的定义,因为不是动画了,所以现在改叫path就好了,border也不需要了:


let path = g
.append("path")
.attr("fill", "red")
.attr("d", "M" + source + "Z");

就这样简单的初始化一下就好了,然后就是最核心的一个过程,path的绘制循序就像一个FIFO队列:


let step = "";
let pre = source;
const funs = new Array();
data.map(async function (item, i) {
step += item + " ";
match = pre && actionReg.exec(source);
if (!match) {
pre = "";
} else if (~["M", "L", "T"].indexOf(match[0])) {
pre = source.substring(match.index + 1);
}
const d = "M" + pre + (pre ? "L" : "") + step.trimEnd() + "Z";
funs.push(() => path.attr("d", d));
});

首先是pre负责的字形,这个字形是要慢慢消失的前部,这个前部不是所有的节点都能用的,而是"M", "L", "T"这种明确有点位的动作才行,毕竟这是动画的起始点。然后step就是代表,要一步一步累加。循环结束funs数组也就累计好了所有的帧(方法),然后用定时器执行这些带参方法即可:


const animation = setInterval(() => {
if (!funs.length) {
clearInterval(animation);
return;
}
funs.shift()();
}, 20);

这种方式虽然非常少见,不过这个定时器流程还是很好理解的过程,效果如下:


Animation.gif


是想象中的效果,但稍微有些单调,可以加上一段摇摆的动画配合变换:


摇摆动画


let pathTran = path;
Array(8)
.fill(0)
.map(function () {
pathTran = pathTran
.transition()
.attr("transform", "skewX(10)")
.duration(300)
.transition()
.attr("transform", "skewX(-10)")
.duration(300);
});
pathTran.transition().attr("transform", "").duration(600);

这段动画要不断赋值才能形成连贯动画,所以直接用path处理动画是不行的,因为上面计时器也是用到这个path对象,所以要额外定义一个pathTran专门用于动画,这段摇摆动画效果如下:


Animation.gif


时间掐的刚刚好,那边计时器停掉,这边摇摆动画也缓停了。


写的十分简便,一点小创

作者:lyrieek
来源:juejin.cn/post/7241826575951200293
意,供大家参考观赏。

收起阅读 »

图像识别,不必造轮子

闲来无事研究了百度图像识别 API,发现该功能还算强大,在此将其使用方法总结成教程,提供大家学习参考 首先预览下效果 从以上预览图中可看出,每张图片识别出5条数据,每条数据根据识别度从高往下排,每条数据包含物品名称、识别度、所属类目 准备工作 1、注册百度账...
继续阅读 »

闲来无事研究了百度图像识别 API,发现该功能还算强大,在此将其使用方法总结成教程,提供大家学习参考


首先预览下效果


图片


从以上预览图中可看出,每张图片识别出5条数据,每条数据根据识别度从高往下排,每条数据包含物品名称识别度所属类目


准备工作


1、注册百度账号


2、登录百度智能云控制台


3、在产品列表中找到 人工智能->图像识别


4、点击创建应用,如下图:


图片


图片


图片


已创建好的应用列表


代码部分


1、获取access_token值


注意:使用图像识别需用到access_token值,因此需先获取到,以便下面代码的使用


access_token获取的方法有多种,这里使用PHP获取,更多有关access_token获取的方法以及说明可查看官方文档:


ai.baidu.com/docs#/Auth/…


创建一个get_token.php文件,用来获取access_token值


PHP获取access_token代码示例:


<?php

//请求获取access_token值函数
function request_post($url = '', $param = '') {

if (empty($url) || empty($param)) {
return false;
}

$postUrl = $url;
$curlPost = $param;
$curl = curl_init();//初始化curl
curl_setopt($curl, CURLOPT_URL,$postUrl);//抓取指定网页
curl_setopt($curl, CURLOPT_HEADER, 0);//设置header
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);//要求结果为字符串且输出到屏幕上
curl_setopt($curl, CURLOPT_POST, 1);//post提交方式
curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
$data = curl_exec($curl);//运行curl
curl_close($curl);

return $data;
}

$url = 'https://aip.baidubce.com/oauth/2.0/token'; //固定地址
$post_data['grant_type'] = 'client_credentials'; //固定参数
$post_data['client_id'] = '你的 Api Key'; //创建应用的API Key;
$post_data['client_secret'] = '你的 Secret Key'; //创建应用的Secret Key;
$o = "";
foreach ( $post_data as $k => $v )
{
$o.= "$k=" . urlencode( $v ). "&" ;
}
$post_data = substr($o,0,-1);

$res = request_post($url, $post_data);//调用获取access_token值函数

var_dump($res);

?>

返回的数据如下,红框内的就是我们所要的access_token值


图片


2、图片上传及识别


2.1、在项目的根目录下创建一个upload文件夹,用于存放上传的图片


2.2、创建一个index.html文件,用于上传图片及数据渲染


代码如下:


<!DOCTYPE html>  
<html>
<head>
<meta charset="utf-8"> 
<title>使用百度 API 实现图像识别</title> 
<style type="text/css">
  .spanstyle{
    display:inline-block;
    width:500px;
    height:500px;
    position: relative;
  }
</style>
<script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script>

<script>

  function imageUpload(imgFile) {

    var uploadfile= imgFile.files[0]  //获取图片文件流

    var formData = new FormData();    //创建一个FormData对象

    formData.append('file',uploadfile);
    //将图片放入FormData对象对象中(由于图片属于文件格式,不能直接将文件流直接通过ajax传递到后台,需要放入FormData对象中。在传递)

    $("#loading").css("opacity",1);


     $.ajax({
          type: "POST",       //POST请求
          url: "upload.php",  //接收图片的地址(同目录下的php文件)
          data:formData,      //传递的数据
          dataType:"json",    //声明成功使用json数据类型回调

          //如果传递的是FormData数据类型,那么下来的三个参数是必须的,否则会报错
          cache:false,  //默认是true,但是一般不做缓存
          processData:false, //用于对data参数进行序列化处理,这里必须false;如果是true,就会将FormData转换为String类型
          contentType:false,  //一些文件上传http协议的关系,自行百度,如果上传的有文件,那么只能设置为false

         success: function(msg){  //请求成功后的回调函数


              console.log(msg.result)

              //预览上传的图片
              var filereader = new FileReader();
              filereader.onload = function (event) {
                  var srcpath = event.target.result;
                  $("#loading").css("opacity",0);
                  $("#PreviewImg").attr("src",srcpath);
                };
              filereader.readAsDataURL(uploadfile);


                //将后台返回的数据进行进一步处理
                var data=  '<li style="margin:2% 0"><span>物品名称:'+msg.result[0].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[0].score*100+'%'+';</span><span>所属类目:'+msg.result[0].root+';</span></li>'

                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[1].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[1].score*100+'%'+';</span><span>所属类目:'+msg.result[1].root+';</span></li>'

                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[2].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[2].score*100+'%'+';</span><span>所属类目:'+msg.result[2].root+';</span></li>'

                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[3].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[3].score*100+'%'+';</span><span>所属类目:'+msg.result[3].root+';</span></li>'


                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[4].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[4].score*100+'%'+';</span><span>所属类目:'+msg.result[4].root+';</span></li>'



                //将识别的数据在页面渲染出来
               $("#content").html(data);


        }
  });


   }



</script>
</head>
<body>

  <fieldset>
     <input type="file"  onchange="imageUpload(this)" >
     <legend>图片上传</legend>
  </fieldset>



<div style="margin-top:2%">
    <span class="spanstyle">
      <img id="PreviewImg" src="default.jpg" style="width:100%;max-height:100%"  >
      <img id="loading" style="width:100px;height:100px;top: 36%;left: 39%;position: absolute;opacity: 0;" src="loading.gif" >
    </span>


    <span class="spanstyle" style="vertical-align: top;border: 1px dashed #ccc;background-color: #4ea8ef;color: white;">
        <h4 style="padding-left:2%">识别结果:</h4>
        <ol style="padding-right: 20px;" id="content">

        </ol>
    </span>

</div>



</body>
</html>

2.3、创建一个upload.php文件,用于接收图片及调用图像识别API


备注:百度图像识别API接口有多种,这里使用的是【通用物体和场景识别高级版】 ;该接口支持识别10万个常见物体及场景,接口返回大类及细分类的名称结果,且支持获取图片识别结果对应的百科信息


该接口调用的方法也有多种,这里使用PHP来调用接口,更多有关通用物体和场景识别高级版调用的方法以及说明可查看官方文档:


ai.baidu.com/docs#/Image…


PHP请求代码示例:


<?php  

        //图像识别请求函数    
        function request_post($url ''$param ''){

            if (empty($url) || empty($param)) {
                return false;
            }

            $postUrl $url;
            $curlPost $param;
            // 初始化curl
            $curl curl_init();
            curl_setopt($curl, CURLOPT_URL, $postUrl);
            curl_setopt($curl, CURLOPT_HEADER, 0);
            // 要求结果为字符串且输出到屏幕上
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
            // post提交方式
            curl_setopt($curl, CURLOPT_POST, 1);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
            // 运行curl
            $data curl_exec($curl);
            curl_close($curl);

            return $data;
        }

        $temp explode("."$_FILES["file"]["name"]);
        $extension end($temp);     // 获取图片文件后缀名


        $_FILES["file"]["name"]=time().'.'.$extension;//图片重命名(以时间戳来命名)

        //将图片文件存在项目根目录下的upload文件夹下
        move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);


        $token '调用鉴权接口获取的token';//将获取的access_token值放进去
        $url 'https://aip.baidubce.com/rest/2.0/image-classify/v2/advanced_general?access_token=' . $token;
        $img file_get_contents("upload/" . $_FILES["file"]["name"]);//本地文件路径(存入后的图片文件路径)
        $img base64_encode($img);//文件进行base64编码加密

        //请求所需要的参数
        $bodys array(
            'image' => $img,//Base64编码字符串
            'baike_num'=>5  //返回百科信息的结果数 5条
        );
        $res request_post($url$bodys);//调用请求函数

        echo $res;  //将识别的数据输出到前端


?>

结语补充


在实际开发过程中,获取access_token值并不是单独写成一个页面文件,而是写在项目系统的配置中;由于access_token值有效期为30天,可通过判断是否失效,来重新请求acc

作者:程序员Winn
来源:juejin.cn/post/7241874482574770235
ess_token值

收起阅读 »

《环信十周年趴——我的程序人生之一路向西》

六年前,我毕业于一个著名的计算机学院。在校期间,我就非常热爱计算机专业,对编程有着浓厚的兴趣。就像很多人一样,我梦想能写出自己的程序,让它变得更好。 于是我开始了另一段工作旅程。我加入了一家小程序公司,开始专注于小程序的开发,这是新的开始,也是全新的挑战。在这...
继续阅读 »

六年前,我毕业于一个著名的计算机学院。在校期间,我就非常热爱计算机专业,对编程有着浓厚的兴趣。就像很多人一样,我梦想能写出自己的程序,让它变得更好。


当我进入我的第一家公司时,我的兴奋和期待之情无以言表。这家公司以开发iOS应用为主,我也开始从事iOS开发。在这个公司里,我经历了很多挑战和机遇。在这里我学到了很多关于软件开发的知识,也养成了很好的开发习惯和团队协作能力。我从一名初学者变成了一个熟练的iOS开发工程师。但是,在某个阶段,我突然发现自己好像停滞不前,感到很无聊,也觉得缺乏动力。


于是我开始了另一段工作旅程。我加入了一家小程序公司,开始专注于小程序的开发,这是新的开始,也是全新的挑战。在这家公司里,我需要从头开始学习新的开发技能,适应小程序的开发环境和工作方式。在这个过程中,我也发现了小程序和iOS尽管有着共同的底层技术,但是却有着截然不同的开发方式,和值得深入研究的地方。在这家公司里,我经历了团队的合作,让我感受到了小程序技术能够如何影响一个团队的凝聚力和升华。


作为一个开发者,我非常喜欢关注新技术,不断地尝试新东西。这让我尝试学习 Flutter,并在一家制造业公司担任 Flutte 工程师,继续我的职业生涯。Flutter 能够提供极高的开发效率和跨平台兼容性,这让我非常留下深刻的印象。同时在这家公司里,我应对更为复杂和有挑战性的技术难题,这让我不断成长和进步。


除了不断学习新技能,我的程序人生也因为自己的勇气而得以改变,我曾在几年间换过不同的公司和城市。我从一个陌生的城市一步一步地适应过来,也从完全新的团队和开发环境中学会自我调节和协作。换工作或换城市,可能会让你失去一些舒适和熟悉的东西,但是也会给你带来新的成长和机会。


这六年的程序人生,让我成长为一个更加成熟和自信的人。我已经拥有了丰富的代码编写经验和技术能力,同时也学会了如何处理工作上的各种挑战,看各种複雜问题,并持续保持了学习的动力和热情。虽然这些年我经历了很多疲惫和挑战,但我也再一次发现自己的阻力和激情,让我不断前进并充满信心地继续我的程序人生。

收起阅读 »

高级程序员和新手小白程序员区别你是那个等级看解决bug速度

IT入门深似海 ,程序员行业,我觉得是最难做的。加不完的班,熬不完的夜。 和产品经理,扯不清,理还乱的宿命关系 一直都在 新需求-做项目-解决问题-解决bug-新需求 好像一直都是这么一个循环。(哈哈哈)我觉得一个好的程序员,判断根本取决于,遇到生产问题和...
继续阅读 »

IT入门深似海 ,程序员行业,我觉得是最难做的。加不完的班,熬不完的夜。



和产品经理,扯不清,理还乱的宿命关系



一直都在 新需求-做项目-解决问题-解决bug-新需求

好像一直都是这么一个循环。(哈哈哈)我觉得一个好的程序员,判断根本取决于,遇到生产问题和bug,解决的问题的思路,和解决问题时间效率


大家平时都是怎么解决bug和问题的。


入门程序员


遇到了问题如。服务器启动不了端口8080已经被占用。会第一时间去查找百度。
然后按照百度给的各种解决方案去实操。最终在一定时间内完美解决bug。


哈哈不过我不建议使用百度搜索了。广告太多,搜索出来内容质量太差了。有时候我想去搜索一下官网。搜索了结果筛选了几页,才筛选到官网。



懂得都懂不过多,解释


初级程序员


开始会间接使用 谷歌搜索必应搜索。 我觉得谷歌在搜索内容和质量,确实是吊打某度了。你给他垂直的内容。搜索出来的内容第一页首页首条,可能就是你要的官网。


或者说是你要的答案,而且广告内垃圾内容几乎很少看到。 搜索出来内容质量也挺高不一样。


这里访问谷歌需要一些技巧, 大家可以通过这个去访问。


点击进入


当然必应搜索。也可以用至少比某度很多。


中级程序员


使用更垂直IT社区内容,进行问题站内搜索。
比如 博客园,CSDN
掘金 等IT博客内容社区网站。


相信大家,在这个时候,自己也会写技术博客,或者记录文章吧,这些IT社区,是不错选择,可以看到很多大牛,或者好的技术文章。


我觉得写博客挺重要的,不管是自己想写,还是处于记录。养成写作是一个好习惯。



  1. 是写文章时候可以提升自己学习能力和写作能力

  2. 更是巩固自己所学习的知识内容。

  3. 也是对自己学习的一个记录,后面遇到忘记了或者同样问题可以查看

  4. 也是对自己业余时间养成一个爱好。


高级程序员


开始接触开源社区,技术论坛等,通过GitHub
isssues 或者 stack overflow
进行问题解决,和提问。


这类效率往往是最快的,直达的,


软件开发工程师


间接开始阅读源码 遇到问题第一时间,去看程序报错我信息


通过断点和本地调试自己先尝试解决。可以通过直接阅读官方文档来解决问题。


当然上面所有解决问题的手段,只是你个人能力循序渐进过程。随着你入行年限,和工作年限,你会接触越多,遇到问题,也不会和开始一样慌张,毫无头绪。


解决问题时间效率,也越来越高,会开始注重代码质量,刻意与避免一些低级bug产生


对自己会有更高的要求。


我来讲讲我目前遇到问题的解决思路大概流程。




  1. 自行本地断点调试。查看具体错误信息代码分析具体业务逻辑问题场景。一般能解决70%问题




  2. 问AI智能ChatGPT ,然后通过谷歌搜索引擎,IT技术论坛 去查询类似问题。




  3. 通过官方文档,或者github等去解决,或者直接提isssues




这里我提到了ChatGPT 我觉得ChatGPT 至少目前能基本取代我用搜索引擎时间。 效率比搜索引擎要高很多。


如果不知道如何使用的,这里我提供了免费的在线使用


点击进入


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

2023年35大龄程序员最后的挣扎

一、自身情况 我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。 其实30岁的时候已经开始焦虑了,并且努力想找出路。 提升技术,努力争增加自己的能力。 努力争取进入管理层,可是卷无处不在,没有人离开这...
继续阅读 »

一、自身情况


我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。



  1. 其实30岁的时候已经开始焦虑了,并且努力想找出路。

  2. 提升技术,努力争增加自己的能力。

  3. 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。

  4. 大环境我们普通人根本改变不了。

  5. 自己大龄性价比不高,中年危机就是客观情况。

  6. 无非就是在本赛道继续卷,还是换赛道卷的选择了。


啊Q精神:我还不是最惨的那一批,最惨的是19年借钱买了恒大的烂尾楼,并且在2021年就失业的那拨人。简直不敢想象,那真是绝望啊。心里不够坚强的,想不开轻生的念头都会有。我至少拿了点赔偿,手里还有些余粮,暂时饿不死。


image.png


二、大环境情况




  1. 大环境不好已经不是秘密了,整个经济走弱。大家不敢消费,对未来信心不足已经是板上钉钉的事了。




  2. 这剧本就是30年前日本的剧本,不敢说一摸一样。可以说大差不差了,互联网行业的薪资会慢慢的回归平均水平,或者技术要求在提升一个等级。




  3. 大部分普通人,还是做应用层拧螺丝,少部分框架师能造轮子也就是2:8理论。




  4. 能卷进这20%里,就能在上一层楼。也不是说这行就不行了,只不过变成了存量市场,而且坑位变少,人并没有变少还增加了。




  5. 不要怀疑自己的能力,这也不是你的问题了,是外部环境导致的市场萎缩。我们能做的就是,脱下孔乙己的长衫,先保证生活。努力干活,不违法乱纪做什么都是光荣了,不要带有色眼镜看待任何人。




三、未来出路


未来的出路在哪里?


这个我也很迷惑,因为大佬走的路,并不是我们这些普通的不能在普通的人能够走的通的。当然也有例外的情况,这些就是幸存者偏差了。


我先把chartGPT给的答应贴出来:


image.png


可以看到chartGPT还是给出,相对可行有效的方案。当然这些并不是每个人都适用。


我提几个普通人能做的建议(普通人还是围绕生存在做决策):



  1. 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。

  2. 摆摊,国家也都改变政策了。

  3. 超市,配送员,外卖员。

  4. 开滴滴网约车。

  5. 有能力的,可以润出G。可以吸一吸GW“free的air”,反正都是要被ZBJ榨取的。


以上都是个人不成熟的观点,jym多多包涵。


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

flutter有哪些架构的框架?该怎么选择

flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架: BLoC (Business Logic Co...
继续阅读 »

flutter有哪些架构的框架?


Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:



  1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。
    Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。

  2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。

  3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。
    以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


除了上面提到的框架之外,还有以下几个Flutter架构框架:



  1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。

  2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。

  3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。

  4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。


总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


Flutter BLoC


Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


如何选择使用


选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


GetX和BLoC的优缺点


GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


GetX


优点:


简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


缺点:


社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


BLoC


优点:


灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


缺点:


学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


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

现代化 Android 开发:基础架构 古哥E下

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。 目前...
继续阅读 »

Android 开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。


目前的整体感觉就是,移动开发基本是奄奄一息了。不过也不用过于悲观:一是依旧有很多存量的 App 堪称屎山,是需要有维护人员的,就跟现在很多人去卷 framework 层一样,千万行代码中找 bug。 二是 AI 日益成熟,那么应用层的创新也会出现,在没有更简洁的设备出现前,手机还是主要载体,总归是需要移动开发去接入的,如果硬件层越来越好,模型直接跑在手机上也不是不可能,所以对跨平台技术也会是新一层的考验,有可能直接去跨平台化了。毕竟去中台化也成了历史的选择。


因而,在这个存量市场,虽然竞争压力很大,但是如果技术过硬,还是能寻求一席之地的。因而我决定用几篇文章来介绍下,当前我认为的现代化 Android 开发是怎样的。其目录为:



  • 现代化 Android 开发:基础架构(本文)

  • 现代化 Android 开发:数据类

  • 现代化 Android 开发:逻辑层

  • 现代化 Android 开发:组件化与模块化的抉择

  • 现代化 Android 开发:多 Activity 多 Page 的 UI 架构

  • 现代化 Android 开发:Jetpack Compose 最佳实践

  • 现代化 Android 开发:性能监控


Scope


提到 Android 基础架构,大家可能首先想到的是 MVCMVPMVVMMVI 等分层架构。但针对现代化的 Android 开发,我们首要有的是 scope 的概念。其可以分两个方面:



  • 结构化并发之 CoroutineScope:目前协程基本已经是最推荐的并发工具了,CoroutineScope 的就是对并发任务的管理,例如 viewModelScope 启动的任务的生命周期就小于 viewModel 的存活周期。

  • 依赖注入之 KoinScope:虽然官方推荐的是 hilt,但其实它并没有 koin 好用与简洁,所以我还是推荐 koinKoinScope 是对实例对象的管理,如果 scope 结束, 那么 scope 管理的所有实例都被销毁。


一般应用总会有登录,所以大体的 scope 管理流程图是这样的:


scope



  • 我们启动 app, 创建 AppScope,对于 koin 而言就是用于存放单例,对于协程来说就是全局任务

  • 当我们登录后,创建 AuthSessionScope, 对于 koin 而言,就是存放用户相关的单例,对于协程而言就是用户执行相关的任务。当退出登录时,销毁当前的 AuthSessionScope,那么其对应的对象实例、任务全部都会被销毁。用户再次登录,就再次重新创建 AuthSessionScope。目前很多 App 对于用户域内的实例,基本上还是用单例来实现,退出登录时,没得办法,就只能杀死整个进程再重启, 所以会有黑屏现象,实现不算优雅。而用 scope 管理后,就是一件很自然而实现的事情了。所以尽量用依赖注入,而不要用单例模式

  • 当我们进入界面后,一般都是从逻辑层获取数据进行渲染,所以依赖注入没多大用了。而协程的 lifecycleScopeviewModelScope 就比较有用,管理界面相关的异步任务。


所以我们在做架构、做某些业务时,首要考虑 scope 的问题。我们可以把 CoroutineScope 也作为实例存放到 KoinScope 里,也可以把 KoinScope 作为 Context 存放到 CorutineScope 里。


岐黄小筑是将 CoroutineScope 放到 koin 里去以便依赖查找

val sessionCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + coroutineLogExceptionHandler(TAG))
val sessionKoinScope = GlobalContext.get().createScope(...)
sessionKoinScope.declare(sessionCoroutineScope)


其实我们也完全可以用 CoroutineScopeContext 来做实例管理,而移除 koin 的使用。但是 Context 的使用并没有那么便捷,或许以后它可以进化为完全取代 koin



架构分层


随着软件复杂度的提升,MVCMVPMVVMMVI 等先后被提出,但我觉得目前所有的开发,都大体遵循某一模式而又不完全遵循,很容易因为业务的节奏,很容易打破,变成怎么方便怎么来。所以使用简单的分层 + 足够优秀的组件化,才是保证开发模式不被打破的最佳实践。下图是岐黄小筑的整体架构图:



整体架构不算复杂,其实重点是在于组件库,emo 已经有 20 个子库了,然后岐黄小筑有一些对于通用逻辑的抽象与封装,使得逻辑层虽然都集中在 logic 层,但整体都是写模板式的代码,可以面向 copy-paste 编程。


BookLogic 为例:


// 通过依赖注入传参, 拿到 db 层、网络层、以及用户态信息的应用
class BookLogic(
val authSession: AuthSession,
val kv: EmoKV,
val db: AccountDataBase,
private val bookApi: BookApi
) {
// 并发请求复用管理
private val concurrencyShare = ConcurrencyShare(successResultKeepTime = 10 * 1000L)

// 加载书籍信息,使用封装好的通用请求组件
fun logicBookInfo(bookId: Int, mode: Int = 0) = logic(
scope = authSession.coroutineScope, // 使用用户 session 协程 scope,因为有请求复用,所以退出界面,再进入,会复用之前的网络请求
mode = mode,
dbAction = { // 从 db 读取本地数据
db.bookDao().bookInfo(bookId)
},
syncAction = { // 从网络同步数据
concurrencyShare.joinPreviousOrRun("syncBookInfo-$bookId") {
bookApi.bookInfo(bookId).syncThen { _, data ->
db.runInTransaction {
db.userDao().insert(data.author)
db.bookDao().insert(data.info)
}
SyncRet.Full
}
}
}
)
// 类似的模板代码
suspend fun logicBookClassicContent(bookId: Int, mode: Int = 0) = logic(...)
suspend fun logicBookExpoundContent(bookId: Int, mode: Int = 0) = logic(...)
...
}

//将其注册到 `module` 中去,目前好像也可以通过注解的方式来做,不过我还没采用那种方式:
scopedOf(::BookLogic)

ViewModel 层浮层从 Logic 层读取数据,并可以进行特殊化处理:

class BookInfoViewModel(navBackStackEntry: NavBackStackEntry) : ViewModel() {
val bookId = navBackStackEntry.arguments?.getInt(SchemeConst.ARG_BOOK_ID) ?: throw RuntimeException("book_id is required!.")

val bookInfoFlow = MutableStateFlow(logicResultLoading<BookInfoPojo>())

init {
viewModelScope.launch {
runInBookLogic {
logicBookInfo(bookId, mode).collectLatest {
bookInfoFlow.emit(it)
}
}
}
}
}

Compose 界面再使用 ViewModel

@ComposeScheme(
action = SchemeConst.ACTION_BOOK_INFO,
alternativeHosts = [BookActivity::class]
)
@SchemeIntArg(name = SchemeConst.ARG_BOOK_ID)
@Composable
fun BookInfoPage(navBackStackEntry: NavBackStackEntry) {
LogicPage(navBackStackEntry = navBackStackEntry) {
val infoVm = schemeActivityViewModel<BookInfoViewModel>(navBackStackEntry)
val detailVm = schemeViewModel<BookDetailViewModel>(navBackStackEntry)
val bookInfo by infoVm.bookInfoFlow.collectAsStateWithLifecycle()
//...
}
}

这样整个数据流从网络加载、到存储到数据库、到传递给 UI 进行渲染的整个流程就结束了。


对于其中更多的细节,例如逻辑层具体是怎么封装的?UI 层具体是怎么使用多 ActivityPage?可以期待下之后的文章。


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

大专前端,三轮面试,终与阿里无缘

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:

const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:




  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。




  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。




  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。




  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。




  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。




  • 说一下 React 的 Fiber 架构是什么




    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:




      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。




      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。




      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。




      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)






  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。




  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。




  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。




  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?




    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:




      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:




        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。




        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。




        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。






      2. 性能问题




        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。




        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。










  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……




  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:




  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。




  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)




  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。




  • node 的内存管理跟垃圾回收机制有了解过吗?




    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:




    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间




      • 新生代空间: 用于存活较短的对象




        • 又分成两个空间: from 空间 与 to 空间




        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC






      • 老生代空间: 用于存活时间较长的对象




        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%




        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行










  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。




  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。




  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。




  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。




  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)




  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。




  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。




  • 追问:http 是在哪一层实现的?



    • 应用层。




  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……




  • 说一下浏览器输入 url 到页面加载的过程:




    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面




    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。




    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……






  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。




  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。




  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。




  • 最后问了期望薪资什么的,然后就结束了。




二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ

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

TypeScript中的枚举,点燃你的代码创意!

前言 枚举是 TypeScript 中一个非常有趣且实用的特性,它可以让我们更好地组织和管理代码。 什么是枚举? 在 TypeScript 中,枚举(Enum)是一种用于定义命名常量集合的数据类型。它允许我们为一组相关的值赋予一个友好的名字,从而使代码更加可读...
继续阅读 »

前言


枚举是 TypeScript 中一个非常有趣且实用的特性,它可以让我们更好地组织和管理代码。


什么是枚举?


在 TypeScript 中,枚举(Enum)是一种用于定义命名常量集合的数据类型。它允许我们为一组相关的值赋予一个友好的名字,从而使代码更加可读和易于理解。枚举可以帮助我们避免使用魔法数值,提高代码的可维护性和可读性。


枚举的基本用法


让我们从枚举的基本用法开始,以一个简单的例子来说明。

enum Direction {
Up,
Down,
Left,
Right,
}

在这个例子中,我们定义了一个名为 Direction 的枚举,它包含了四个值:Up、Down、Left 和 Right。这些值默认情况下是从0开始自增的索引值。


我们可以使用枚举中的值来进行变量的赋值和比较。

let myDirection: Direction = Direction.Up;

if (myDirection === Direction.Up) {
console.log("向上");
} else if (myDirection === Direction.Down) {
console.log("向下");
}

在这个例子中,我们声明了一个名为 myDirection 的变量,并将其赋值为 Direction.Up。然后,我们使用 if 语句对 myDirection 进行比较,并输出相应的信息。


枚举的进阶用法


除了基本的用法外,枚举还有一些进阶的用法,让我们一起来看看。


1. 指定枚举成员的值


我们可以手动为枚举成员指定具体的值,而不是默认的自增索引值。

enum Direction {
Up = 1,
Down = 2,
Left = 3,
Right = 4,
}

在这个例子中,我们手动指定了每个枚举成员的值。这样,Up 的值为1,Down 的值为2,依此类推。


2. 使用枚举成员的名称


我们可以使用枚举成员的名称来访问其值。

enum Direction {
Up,
Down,
Left,
Right,
}

console.log(Direction.Up); // 输出

: 0
console.log(Direction[0]); // 输出: "Up"

在这个例子中,我们分别通过成员的名称和索引值来访问枚举成员的值。


3. 枚举的反向映射


枚举还具有反向映射的特性,可以通过值找到对应的名称。

enum Direction {
Up,
Down,
Left,
Right,
}

console.log(Direction.Up); // 输出: 0
console.log(Direction[0]); // 输出: "Up"

在这个例子中,我们通过 Direction.Up 输出了 0,通过 Direction[0] 输出了 "Up"。这种反向映射可以在某些场景下非常有用。


总结


枚举是一种用于定义命名常量集合的数据类型,可以帮助我们更好地组织和管理代码。我们了解了枚举的基本用法,以及一些进阶的技巧,如指定枚举成员的值、使用枚举成员的名称和枚举的反向映射。


希望能够帮助到大家更好地掌握 TypeScript 中的枚举,并在实际开发中灵活运用。


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

年后被吊打的第一面

背景 base重庆,面试中高级,目标先检验一下自己的水平和能力顺便看看薪资,好直接开始把。 自我介绍 讲了一下自己的技术栈:掌握vue全家桶,底层及上层框架、掌握react底层原理、熟悉js、熟悉工程化、熟悉微信小程序、使用过node、关注前端趋势有开源经历、...
继续阅读 »

背景


base重庆,面试中高级,目标先检验一下自己的水平和能力顺便看看薪资,好直接开始把。


自我介绍


讲了一下自己的技术栈:掌握vue全家桶,底层及上层框架、掌握react底层原理、熟悉js、熟悉工程化、熟悉微信小程序、使用过node、关注前端趋势有开源经历、主方向工程化等。


大概说了1分钟把,可能是我一边自我介绍一边在笑,面试官就问了一下:“你看起来心态很好啊!你是不是要写面经啊?”


我:“我是紧张才笑,应该是可以吗?要不多问点把~”


面试官:“可以,行”


记住这句话,我现在很后悔


步入正题


面试官:"那我们先来点基础把",下面都是我原话,小伙伴们可以纠正一下。


JS浏览器基础篇


1、dom树是怎么生成的


"浏览器是多进程架构,而其中有一个渲染进程,负责页面的渲染和js脚本的执行,而在渲染进程中有一个HTML解析器,oh对还有一个网络进程,网络进程负责根据content-type创建渲染进程,然后渲染进程用类似stream流管道那种接字节流将它解析为dom"


“而解析时,我觉得可以对标现在的各种转移编译工具,都有一个词法分析、语法分析、transfrom、genoretor的流程”


“你能具体说说这个过程吗?”


“(心理活动:当时脑子就蒙了、有点超纲啊、我要不猜一下)稍等我想一下、跟babel应该很像吧、会对一些声明命令赋值注释分词,这块我不是很了解,但应该对于html分词就是分的标签和文本内容,然后再通过算法去转换成dom”


“行,你上面提到了分析器,那如果当分析器遇到了script标签那”


“(心理活动:......这八股文味不对劲啊),我不知道对不对,但表现的是当遇到了scrpit会暂停html转换dom,去解析jascript,而async和defer会异步加载不会阻塞html转换”。


2、渲染进程


还可以你可以自信点虽然有些地方不是很对,但已经够用了。刚听你说了渲染进程,你说说它下面的几个线程把。


“emmm,下面的主线程吗,有主线程、GUI渲染线程、事件触发线程、定时器触发线程(后面发现漏了一个http线程),em,主线程和GUI是互斥的、js执行太长会造成页面渲染卡顿,但现在有很多解决方案,比如:在react中的调度器预留的5ms空闲时间、web worker之类的。然后是事件触发线程和定时器线程都是在任务队列去做Loop”


“行,那事件循坏我就不问你了,问问你V8的垃圾回收把”


“(你问啊!你问啊!)”


3、v8垃圾回收


“首先js因为是单线程,垃圾回收会占用主线程,导致页面卡顿,所以需要一个算法或者说策略,而v8采用的是分代式回收,而垃圾回收在堆中分成了很多部分用作不同的作用(我在说什么啊!当时),回收主要表现在新老生代上,新生代就活得短一点的对象,老生代就活得长一点的对象。


“在新生代里有一个算法,将新生代分成了两个区,一个FORM,一个TO,每次经过Scavenge会将FORM区中的没引用的销毁,然后活着的TO区调换位置,反复如此,当经过一次acavange后就会晋升的老生代还有个条件就是TO区的内存超过多少了也会晋升。”


“而老生代,采用标记清除和标记整理,但标记清除会造成内存不连续,所以会有标记整理取解决掉内存碎片,就是清理掉边界碎片”


“为什么TO超过25%要晋升老生代?标记清除是怎么清除的?”


“不知道~”


“第一个问题是为了不影响后续FORM空间的分配,第二个问题你应该看过有关这方面的文章把,垃圾回收会构建一个根列表,从根节点去访问那些变量,可访问到位活动,不可就是垃圾”


4、浏览器缓存


就强制缓存,协商缓存,浏览器内存那些,有兴趣看看文章,讲细点就行。写着太累了,当时讲了一大滩,直接说面试官问题把。


“因为提到了这些缓存,你觉得他们对于我们实际的业务场景下怎么运用”


“(蒙蔽),很大一部分是浏览器优化,一些http缓存我们可以做一些控制,本质上我感觉这些都属于性能优化的部分。”


“行”


5、JS上下文执行栈和闭包


“几个概念把,esc、上下文:作用域链,AO/VO,this。esc存储执行的上下文”


“算了我以一个函数来说把,主要是创建和执行。假设有一个A函数,过程是这样的创建全局执行上下文、压入esc、全局上下文初始化、执行A函数、创建A函数执行上下文,压入esc,A函数上下文初始化,这个初始化过程是这样的:创建作用域链、emm我上面提漏了一个A函数被创建全局上下文被保存到scope中的过程,是复制scpoe创建作用域链,用arguments创建活动对象,初始化活动对于,将活动对象压入链顶,执行完毕,上下文弹出。”


“但是全局上下文一直在栈底,而VO和AO的确认,我感觉是取决是是否可访问的。”


“而闭包就是上下文链中上下文scope被销毁了,但因为保持了对scope中某个变量的引用,这应该就是你上面说的回收原理的根节点实现的这个东西把,导致没销毁干净,留存在了内存中,完成了闭包”


“你怎么看待闭包的副作用”


“emmm,其实我觉得闭包是语言特性,虽然有副作用但我觉得其实挺好的,但只要管理好它就好了。况且又不是只有闭包会造成这些问题,就比如:react里面还有专门去清理一些链表和难回收的东西,去帮助v8回收。我觉得这得取决于写代码的人。”


“可以的,我感觉你的基础还是挺好的,你说下es6的东西把,控制下时间”


“你想听哪方面的那?因为东西太多了”


“工程化把,因为我前面听你介绍主方向是工程化”


“(...我怎么感觉工程化相关的只有一个esm模块化啊,这个怎么分类啊)esm:异步加载、引入抛出,编译时,值的引用。大概就这些东西把,其他的不知道了”


“行”


“那您觉得还有哪些那”


“就比如:Promise和async之类的啊”


“(..................)”


“来手写几道题把”


6、bind pipe compose 深拷贝


这个网上太多了,大伙自己去看。


CSS基础篇


“我可能CSS有点烂,但基础的应该都知道”


“先说说BFC把”


1、BFC


“BFC给我的感觉就像是个封闭盒子,不会在布局上影响到外面的元素。平常会触发BFC比较多的就是body,浮动元素、绝对定位、flex、overflow之类的。在BFC可以清除浮动、阻止元素被浮动环绕之类的。(然后我一边说一边尴尬的笑)”


“大概知道你CSS是个啥水平了,简单问点把,你说BFC可以清除浮动吗?为什么?”


“不知道”


“其实准确的说不是清除,是因为浮动元素也是BFC,两个BFC互不影响。你提到了BODY触发BFC?”


“emmm,可能是也许不是BODY,是HTML根元素”


“是的,不是BODY”


2、居中


"flex布局,positon,flex margin:auto,position transform,table-cell"


"行了,层叠上下文和层叠顺序"


3、层叠上下文


“(.......这时候感觉每多问我一个CSS都是煎熬啊),em我其实一般对于会遇到有层叠上下文不清晰的情况都是指定z-index.”


“行”


4、flex布局


“这样吧,你平时用得比较多的是什么布局?”


“flex布局把”


“那我们来聊一下flex布局把”


“(拜托我真的会哭,我感觉面试官他好兴奋),emmmm,我觉得布局无非是控制内部元素和控制容器,而flex布局对于容器的控制是基于轴这个概念的,而flex中的轴分为:主轴、垂直轴、换行轴。”


“主轴指的就是元素排列的方向轴,而flex-direction是最重要的属性在主轴中,row和col控制角度,reverse控制方向,但我们其实平时用得比较多的就默认的row和column,column会把元素垂直去排列。而主轴的另一个属性justify-content是控制元素在轴上的排列,然后我说了一下常用的几种就start end center between around”


“垂直轴就是垂直于主轴的方向轴~然后我停了大概有20秒(又开始笑了)”


“没了?”


“(啊啊啊啊啊!)可能就我只知道align-items控制垂直轴上的位置,然后说了下,start end center。”


“还有换行轴那”


“嗷对,就是刚才提漏了,垂直轴是针对于当前行,但换行轴是针对于整个容器。”


“这个针对怎么说?你继续说换行轴属性。”


就是高度嘛,布局换行后,垂直轴的高度只会是当前行高度。flex-wrap,但我只用过wrap,emm对于控制内部容器我了解得很粗浅。


"你可以了解一下wrap-reverse,下来可以去看一下正负剩余空间和flex-grow flex-shrink这些"


“抢答!就是flex:1这种写法的哈”


“对的,你知道吗”


“不知道”


我们两个同时沉默了(啊!!!!!!!!)。


"没事其实比我想象得稍微好一点,至少你在你不擅长的东西上也是花了时间去学的,css就不问了,下面问点框架把"


“谢谢!谢谢!谢谢!”


框架基础篇


“你简历里是React和Vue都会,那先说说你是怎么看这两个框架的把”


1、对React和Vue的看法


“在开发模式上,React发明了JSX这种开发模式,并且用了很多年时间让社区去接受,而Vue则是采用现成的模版语法的开发模式。但感觉就这两年这两个框架都在往一个函数式组件的方向靠,不应该说靠是已经实现了,Vue3中的函数式组件甚至在某种层面上说比react更自由。当然洛现在声明式的编程是主流嘛”


“在实现层面上说的话,就那一套,单向数据流,双向绑定,数据不可变性,更智能的依赖收集,优化的方式不同等”


“听到你说了更自由和智能的依赖收集,具体指的是?”


"比如react useEffect要手动加依赖,但vue3的wachEffect和computed就不用"。


“自由,em就比如,hook得顶层,不要条件循坏用,而且react重心智负担:就闭包陷阱那一套过时变量嘛,依赖的选择,还有重复渲染这些问题,我一直不理解为什么不把这些心智负担收到框架里,我觉得react是有这个能力的。vue3的话,你想咋用咋样api咋样,setup也会只用一次,也不会像新旧树对比hook多次调用”


“哈哈感觉你对react怨气好大,因为我看你文章写了很多react源码的嘛,如果是你你会怎么去收敛这个心智负担到框架内部?”


(.......绷不住了啊,吹过头了),稍等想1分钟,就是hook顶层那个和条件循坏应该不好动,因为react不能去破坏hooks链表的结构,对于过时变量react18已经将usestate全改为异步了,依赖的选择的心智问题我觉得是否说可以更明确一点再未来加入配置项的话,将依赖的收集在updateEffect和mountedEffect前去提前收集,就做一个依赖系统专门去处理这个事情,感觉可以从编译器自动生成依赖数组,现在react只是一层浅比较。但其实这么想,大部分问题的根源,是React函数组件机制所限:每次组件渲染,组件里的所有代码都会被重新调用一次,在这上面其实也可以动下手(自己说了感觉当没说,感觉好尴尬啊硬吹)。就长话短说就是,react的心智模型让我要花很多精力去处理边界情况。


“其实你最后句话说得挺好的,因为react要求你用声明式的方式去写组件,你就不该去纠结引用会不会变,多添加个依赖很不舒服,重新渲染这种事情,你需要保证的是无论重新渲染多少次这个组件都不会变。假设你useEffect依赖于AB,但你的B就可能只在首次创建后永远不变,它确实显得很“多余”但你不用纠结这个。真正的问题可能就在于你觉得的永远不会变只是你觉得,我们平时出现问题很多都是这种以为的边界问题导致B变造成的”


2、为什么react需要合成事件


“兼容性的考虑把,可以抹平不同浏览器事件对象差异,还有个就是避免了垃圾回收。”


“我们公司主要是Vue,你的简历里Vue也更擅长一些,我们谈一下Vue把”


3、生命周期


随便了一下,每个生命周期,父子生命周期,每个生命周期的定义和写法。


4、路由


5、指令


6、响应式原理


7、数组处理


8、key,diff算法


9、V3组合式API


10、一些TS系统


11、V3编译原理


“上面也问了挺多的了,你讲讲Vue3里面的模版编译和函数式组件编译把。”


“(我崩不住了,妈妈我想回家)先巴拉巴拉扯了一下pnpm和menorepo和整体v3源码,然后讲到complier,同样vue模版的编译也是通俗流程就是parse transform gen,我没具体研究过,但parse应该也是对标签属性文本指令等一系列做处理去干成AST,然后transform做转换最后生成。”


“行上午先面到这,二面通知你,有什么要问的吗?”


“em,你觉得我咋样(我好直白~)”


“挺不错的,2年经验,感觉你知识的掌握程度大于你的年限,有点好奇平时你怎么安排学习时间的”


“就可能目前这家公司比较清闲把,再加上学习写代码对我比较快乐,就能投入很多时间和精力,哎,就二面能不能快点!可以加个微信吗~”


“行”


总结


然后二面就是项目面了,啊好累啊,现在前端真卷啊,其实感觉可以多问点工程化,虽然准备了很多但是没被问上。


emmm,因为看评论区嘛,就想说,乐观点,其实也没这么卷,只要自己多写写代码和看看八股文都可以的,当然最重要的还是思考。emm想加群一起卷的可以看沸点


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

抓包技术的应用示例:薅瑞幸羊毛 🦙

前言 最近瑞幸在搞活动,每天免费送10000份咖啡,我是个不喝咖啡的人儿,所以没咋关注,今早我们的团宠小妹,拉着我 10点整拼手速,想着帮她抢一杯,于是点开瑞幸咖啡小程序主页,banner 栏轮播图中有一张海报入口,操作一通下来,果然,没抢到。 手速不够快不是...
继续阅读 »

前言


最近瑞幸在搞活动,每天免费送10000份咖啡,我是个不喝咖啡的人儿,所以没咋关注,今早我们的团宠小妹,拉着我 10点整拼手速,想着帮她抢一杯,于是点开瑞幸咖啡小程序主页,banner 栏轮播图中有一张海报入口,操作一通下来,果然,没抢到。


手速不够快不是主要原因,手指操作延迟 + 系统页面跳转耗时加起来到 http 发出就已经耽误了1 -2 秒钟了,这个时间才是关键,本文从技术角度探讨下怎么在最小成本比如几分钟内,实现一个小工具,来解决这个问题。


抓包工具


首先需要一个抓包工具,iphone 手机可以用 stream, 有几个注意点:


1、默认安装后是无法抓取 https 类型的,需要在设置里进行相关配置:



如果您要抓取 HTTPS 的请求,需要先启动抓包,然后安装 CA 证书后,去设置-通用-关于-证书信任设置 里信任 CA,才可以查看请求的内容。



Pasted image 20230601122258.png


2、注意小程序里面哦(原生的可能抓不到),拿到的接口名如下:


https://mkt.lkcoffee.com/ladder/capi/resource/m/promo/activity/send


stream 提供了 curl 的拷贝,将其复制并导入到 postman 中。


WechatIMG247.png


postman 导入&复现


点击 import 按钮,在弹窗中选择 raw text 将复制的 curl 字符串粘贴进去,点击确认,就成功的将 这个 http 接口导入到了 postman 中,尝试点击 send 按钮,发现拿到了正确的响应,验证了该接口已经可以正常使用。


截屏2023-06-01 12.43.31.png


Pasted image 20230601122933.png


自动化脚本?


其实到这一步,已经实现了目标,点击 send 直接发送请求,大大提升了抢到的概率,如果你还想更进一步,那么可以尝试将其封装成 自动化脚本,来实现定时、自动、重复发送;


点开右侧代码块,选择语言,假设选择 python(也可以任意选择你擅长的语言),然后就自动生成 python 版本的可执行代码片段,我们就在这个基础上拓展功能;


截屏2023-06-01 12.48.19.png


示例代码如下:

import requests
import time

url = "http://example.com" # 将此处的 URL 替换为你要请求的地址
payload = {}
headers = {
#将 postman 中的headers 复制过来
}

start_time = "09:59:55" # 设置开始请求的时间
end_time = "10:00:30" # 设置结束请求的时间

def make_request():
response = requests.get(url, headers=headers, data=payload)
if "成功" in response.text:
print("响应内容:", response.text)
raise SystemExit # 中断程序

while True:
current_time = time.strftime("%H:%M:%S", time.localtime())
if current_time >= start_time and current_time <= end_time:
make_request()
time.sleep(1) # 每秒检查一次当前时间


将其保存到本地并通过 python 指令来执行,就可以运行了。


总结


用今天的午睡时间,写了这篇文,以瑞幸的营销活动为例子,带你感受了下技术的魅力,其中涉及到了抓包、自动化脚本、定时任务、请求策略、stream 和 postman 等知识;


然后我想问下大家,对于其带来的潜在公平问题,你们怎么看呢?欢迎讨论。


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

什么是优雅的代码设计

今天我来解释一下什么样的代码才是优雅的代码设计。当然我们的代码根据实际的应用场景也分了很多维度,有偏向于底层系统的,有偏向于中间件的,也有偏向上层业务的,还有偏向于前端展示的。今天我主要来跟大家分析一下我对于业务代码的理解以及什么样才是我认为的优雅的业务代码设...
继续阅读 »

今天我来解释一下什么样的代码才是优雅的代码设计。当然我们的代码根据实际的应用场景也分了很多维度,有偏向于底层系统的,有偏向于中间件的,也有偏向上层业务的,还有偏向于前端展示的。今天我主要来跟大家分析一下我对于业务代码的理解以及什么样才是我认为的优雅的业务代码设计。


大家吐槽非常多的是,我们这边的业务代码会存在着大量的不断地持续的变化,导致我们的程序员对于业务代码设计得就比较随意。往往为了快速上线随意堆叠,不加深入思考,或者是怕影响到原来的流程,而不断在原来的代码上增加分支流程。


这种思想进一步使得代码腐化,使得大量的程序员更失去了“好代码”的标准。


那么如果代码优雅,那么要有哪些特征呢?或者说我们做哪些事情才会使得代码变得更加优雅呢?


结构化


结构化定义是指对某一概念或事物进行系统化、规范化的分析和定义,包括定义的范围、对象的属性、关系等方面,旨在准确地描述和定义所要表达的概念或事物。



我觉得首要的是代码,要一个骨架。就跟我们所说的思维结构是一样,我们对一个事物的判断,一般都是综合、立体和全面的,否则就会成为了盲人摸象,只见一斑。因此对于一个事物的判断,要综合、结构和全面。对于一段代码来说也是一样的标准,首先就是结构化。结构化是对一段代码最基本的要求,一个有良好结构的代码才可能称得上是好代码,如果只是想到哪里就写到哪里,一定成不了最优质的代码。


代码的结构化,能够让维护的人一眼就能看出主次结构、看出分层结构,能够快速掌握一段代码或者一段模块要完成的核心事情。


精简



代码跟我们抽象现实的物体一样,也要非常地精简。其实精简我觉得不仅在代码,在所有艺术品里面都是一样的,包括电影。电影虽然可能长达一个小时,两个小时,但你会发现优雅的电影它没有一帧是多余的,每出现的一个画面、一个细节,都是电影里要表达的某个情绪有关联。我们所说的文章也是一样,没有任何一个伏笔是多余的。代码也是一样,严格来说代码没有一个字符、函数、变量是多余的,每个代码都有它应该有的用处。就跟“奥卡姆剃刀”原理一样,每块代码都有它存在的价值包括注释。


但正如我们的创作一样,要完成一个功能,我们把代码写得复杂是简单的,但我们把它写得简单是非常难的。代码是思维结构的一种体现,而往往抽象能力是最为关键的,也是最难的。合适的抽象以及合理的抽象才能够让代码浓缩到最少的代码函数。


大部分情况来说,代码行数越少,则运行效率会越高。当然也不要成为极端的反面例子,不要一味追求极度少量的代码。代码的优雅一定是精要的,该有的有,不该有的一定是没有的。所以在完成一个业务逻辑的时候,一定要多问自己这个代码是不是必须有的,能不能以一种简要的方式来表达。


善用最佳实践


俗话说太阳底下没有新鲜事儿,一般来说,没有一个业务场景所需要用到的编码方式是需要你独创发明的。你所写的代码功能大概率都有人遇到过,因此对于大部分常用的编码模式,也都大家被抽象出来了一些最佳实践。那么最经典的就是23种设计模式,基本上可以涵盖90%以上的业务场景了。


以下是23种设计模式的部分简单介绍:

  1. 单例模式(Singleton Pattern):确保类只有一个实例,并提供全局访问点。
  2. 工厂模式(Factory Pattern):定义一个用于创建对象的接口,并让子类决定实例化哪个对象。
  3. 模板方法模式(Template Method Pattern):提供一种动态的创建对象的方法,通过使用不同的模板来创建对象。
  4. 装饰器模式(Decorator Pattern):将对象包装成另一个对象,从而改变原有对象的行为。
  5. 适配器模式(Adapter Pattern):将一个类的接口转换成客户希望的另一个接口,以使其能够与不同的对象交互。
  6. 外观模式(Facade Pattern):将对象的不同方面组合成一个单一的接口,从而使客户端只需访问该接口即可使用整个对象。

我们所说的设计模式就是一种对常用代码结构的一种抽象或者说套路。并不是说我们一定要用设计模式来实现功能,而是说我们要有一种最高效,最通常的方式去实现。这种方式带来了好处就是高效,而且别人理解起来也相对来说比较容易。


我们也不大推荐对于一些常见功能用一些花里胡哨的方式来实现,这样往往可能导致过度设计,但实际用处可能反而会带来其他问题。我觉得要用一些新型的代码,新型的思维方式应该是在一些比较新的场景里面去使用,去验证,而不应该在我们已有最佳实践的方式上去造额外的轮子。


这个就比如我们如果要设计一辆汽车,我们应该采用当前最新最成熟的发动机方案,而不应该从零开始自己再造一套新的发动机。但是如果这个发动机是在土星使用,要面对极端的环境,可能就需要基于当前的方案研制一套全新的发动机系统,但是大部分人是没有机会碰到土星这种业务环境的。所以通常情况下,还是不要在不需要创新的地方去创新。


除了善用最佳实践模式之快,我们还应该采用更高层的一些最佳实践框架的解决方案。比如我们在面对非常抽象,非常灵活变动的一些规则的管理上,我们可以使用大量的规则引擎工具。比如针对于流程式的业务模型上面,我们可以引入一些工作流的引擎。在需要RPC框架的时候,我们可以根据业务情况去调研使用HTTP还是DUBBO,可以集百家之所长。


持续重构



好代码往往不是一蹴而就的,而是需要我们持续打磨。有很多时候由于业务的变化以及我们思维的局限性,我们没有办法一次性就能够设计出最优的代码质量,往往需要我们后续持续的优化。所以除了初始化的设计以外,我们还应该在业务持续的发展过程中动态地去对代码进行重构。


但是往往程序员由于业务繁忙或者自身的懒惰,在业务代码上线正常运行后,就打死不愿意再动原来的代码。第一个是觉得跑得没有问题了何必去改,第二个就是改动了反而可能引起故障。这就是一种完全错误的思维,一来是给自己写不好的线上代码的一个借口,二来是没有让自己持续进步的机会。


代码重构的原则有很多,这里我就不再细讲。但是始终我觉得对线上第一个要敬畏,第二个也要花时间持续续治理。往往我们在很多时候初始化的架构是比较优雅的,是经过充分设计的,但是也是由于业务发展的迭代的原因,我们持续在存量代码上添加新功能。


有时候有一些不同的同学水平不一样,能力也不一样,所以导致后面写上的代码会非常地随意,导致整个系统就会变得越来越累赘,到了最后就不敢有新同学上去改,或者是稍微一改可能就引起未知的故障。


所以在这种情况下,如果还在追求优质的代码,就需要持续不断地重构。重构需要持续改善,并且最好每次借业务变更时,做小幅度的修改以降低风险。长此以往,整体的代码结构就得以大幅度的修改,真正达到集腋成裘的目的。下面是一些常见的重构原则:

  1. 单一职责原则:每个类或模块应该只负责一个单一的任务。这有助于降低代码的复杂度和维护成本。
  2. 开闭原则:软件实体(类、模块等)应该对扩展开放,对修改关闭。这样可以保证代码的灵活性和可维护性。
  3. 里氏替换原则:任何基类都可以被其子类替换。这可以减少代码的耦合度,提高代码的可扩展性。
  4. 接口隔离原则:不同的接口应该是相互独立的,它们只依赖于自己需要的实现,而不是其他接口。
  5. 依赖倒置原则:高层模块不应该依赖低层模块,而是依赖应用程序的功能。这可以降低代码的复杂度和耦合度。
  6. 高内聚低耦合原则:尽可能使模块内部的耦合度低,而模块之间的耦合度高。这可以提高代码的可维护性和可扩展性。
  7. 抽象工厂原则:使用抽象工厂来创建对象,这样可以减少代码的复杂度和耦合度。
  8. 单一视图原则:每个页面只应该有一个视图,这可以提高代码的可读性和可维护性。
  9. 依赖追踪原则:对代码中的所有依赖关系进行跟踪,并在必要时进行修复或重构。
  10. 测试驱动开发原则:在编写代码之前编写测试用例,并在开发过程中持续编写和运行测试用例,以确保代码的质量和稳定性。

综合


综上所述,代码要有结构化、可扩展、用最佳实践和持续重构。追求卓越的优质代码应该是每一位工程师的基本追求和基本要求,只有这样,才能不断地使得自己成为一名卓越的工程师。



作者:ali老蒋
来源:juejin.cn/post/7241115614102863928

收起阅读 »

何谓实事求是地工作?

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。 随着我这两年的学习和总结,我越...
继续阅读 »

提到实事求是,大家第一时间会想到什么?我想大部分是客观,事实,脚踏实地?这么一想,大家都会觉得,自己挺实事求是的呀,没毛病。但是,我会经常在工作中感受到不是那么实事求是的行为,比如张嘴就来,不带思考,做事全靠猜的行为,真太多了。


随着我这两年的学习和总结,我越发觉得实事求是非常重要,并把它视为我做事情和成长的基石。对于实事求是,我主要有以下 3 层理解。


首先,尊重客观事实,探寻真理。我们要承认事实,即使这个事实有多么的难以置信,但存在即是合理,我们首先要尊重它,承认它。然后我们还要积极主动地面对它,探寻事实背后的真理,获得真知,这样才能真正的成长,并有可能寻得机会。当某个事情的进展超出自己预期的时候,我们正确的态度应该是思考为什么会这样,而不是去想对错得失。


其次,数据说话,数据驱动。事实如何去量化?答案是数据。使用数据去表达事实,是我们程序员应该有的技能。工作的本质就是解决问题,之前的文章有讲解,问题就是理想状态和现实状态之间的差别,因此,我们在工作当中做的每一项决策的依据、制定的每一个目标,都应该用数据说话。我们应该使用数据表达现状,并使用数据衡量目标,驱动自己去工作。一些沟通的细节就能够体现出他是不是在实事求是地工作,比如“这个页面加载太慢了,需要优化”。那这个页面加载到底有多慢?业界标准或者竞品的加载耗时是多少?优化的目标值是多少?


最后,从客观事实中获取反馈,不断迭代。工作中想要获得成功和成长,最核心的一个环节是反馈。很多人没有意识到这点。更多的人没有意识到的是,获取反馈其实很简单,随处都是。敏捷开发、精益创业、增长黑客,这些理论的底层核心都是基于事实和数据的反馈,不断迭代改进自己的产品,从而获得成功。对于个人成长来说也是一样的,我们要从客观事实中获取反馈,思考总结,不断迭代自己的能力。


总结一下,实事求是地工作有 3 个层次,首先,要正视事实,并主动探究真理;然后我们慢慢地开始用数据驱动自己的工作;最后让数据驱动变成循环,不断迭代,并把这种循环融入到各个方面,包括工作和个人成长,让它成为自己下意识的动作。


我在努力学习和践行实事求是地工作,我也希望我的团队可以用实事求是的态度来工作,以此文共勉!



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

收起阅读 »

什么是 HTTP 长轮询?

web
什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。


HTTP 轮询


上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:



  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。

  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。


HTTP 长轮询解决了使用 HTTP 进行轮询的缺点



  1. 请求从浏览器发送到服务器,就像以前一样

  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送

  3. 客户端等待服务器的响应。

  4. 当数据可用时,服务器将其发送给客户端

  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


HTTP 长轮询


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。



  • 浏览器将始终在可用时接收最新更新

  • 服务器不会被永远无法满足的请求所搞垮。


长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。



  • 随着使用量的增长,您将如何编排实时后端?

  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?

  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?

  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?


在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。


HTTP 长轮询 MQ


然后出现几个明显的问题:



  • 服务器应该将数据缓存或排队多长时间?

  • 应该如何处理失败的客户端连接?

  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?

  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?


所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良

作者:demo007x
来源:juejin.cn/post/7240111396869161020
好的代理环境中挣扎。

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;



作者:shichuan

来源:juejin.cn/post/7230810119122190397

收起阅读 »

初学后端,如何做好表结构设计?

前言 最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计? 大家关心的问题阳哥必须整理出来,希望对大家有帮助。 先说结论 这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从...
继续阅读 »

前言


最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计?


大家关心的问题阳哥必须整理出来,希望对大家有帮助。


先说结论


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:

  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)
  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度。

4个方面


设计数据库表结构需要考虑到以下4个方面:

  1. 数据库范式:通常情况下,我们希望表的数据符合某种范式,这可以保证数据的完整性和一致性。例如,第一范式要求表的每个属性都是原子性的,第二范式要求每个非主键属性完全依赖于主键,第三范式要求每个非主键属性不依赖于其他非主键属性。
  2. 实体关系模型(ER模型):我们需要先根据实际情况画出实体关系模型,然后再将其转化为数据库表结构。实体关系模型通常包括实体、属性、关系等要素,我们需要将它们转化为表的形式。
  3. 数据库性能:我们需要考虑到数据库的性能问题,包括表的大小、索引的使用、查询语句的优化等。
  4. 数据库安全:我们需要考虑到数据库的安全问题,包括表的权限、用户角色的设置等。

设计原则


在设计数据库表结构时,可以参考以下几个优雅的设计原则:

  1. 简单明了:表结构应该简单明了,避免过度复杂化。
  2. 一致性:表结构应该保持一致性,例如命名规范、数据类型等。
  3. 规范化:尽可能将表规范化,避免数据冗余和不一致性。
  4. 性能:表结构应该考虑到性能问题,例如使用适当的索引、避免全表扫描等。
  5. 安全:表结构应该考虑到安全问题,例如合理设置权限、避免SQL注入等。
  6. 扩展性:表结构应该具有一定的扩展性,例如预留字段、可扩展的关系等。

最后,需要提醒的是,优雅的数据库表结构需要在实践中不断迭代和优化,不断满足实际需求和新的挑战。



下面举个示例让大家更好的理解如何设计表结构,如何引入内存,有哪些优化思路:



问题描述



如上图所示,红框中的视频筛选标签,应该怎么设计数据库表结构?除了前台筛选,还想支持在管理后台灵活配置这些筛选标签。


这是一个很好的应用场景,大家可以先自己想一下。不要着急看我的方案。


需求分析

  1. 可以根据红框的标签筛选视频
  2. 其中综合标签比较特殊,和类型、地区、年份、演员等不一样
  • 综合是根据业务逻辑取值,并不需要入库
  • 类型、地区、年份、演员等需要入库

3.设计表结构时要考虑到:

  • 方便获取标签信息,方便把标签信息缓存处理
  • 方便根据标签筛选视频,方便我们写后续的业务逻辑

设计思路

  1. 综合标签可以写到配置文件中(或者写在前端),这些信息不需要灵活配置,所以不需要保存到数据库中
  2. 类型、地区、年份、演员都设计单独的表
  3. 视频表中设计标签表的外键,方便视频列表筛选取值
  4. 标签信息写入缓存,提高接口响应速度
  5. 类型、地区、年份、演员表也要支持对数据排序,方便后期管理维护

表结构设计


视频表


字段注释
id视频主键id
type_id类型表外键id
area_id地区表外键id
year_id年份外键id
actor_id演员外键id

其他和视频直接相关的字段(比如名称)我就省略不写了


类型表


字段注释
id类型主键id
name类型名称
sort排序字段

地区表


字段注释
id类型主键id
name类型名称
sort排序字段

年份表


字段注释
id类型主键id
name类型名称
sort排序字段

原以为年份字段不需要排序,要么是年份正序排列,要么是年份倒序排列,所以不需要sort字段。


仔细看了看需求,还有“10年代”还是需要灵活配置的呀~


演员表


字段注释
id类型主键id
name类型名称
sort排序字段

表结构设计完了,别忘了缓存


缓存策略


首先这些不会频繁更新的筛选条件建议使用缓存:


  1. 比较常用的就是redis缓存
  2. 再进阶一点,如果你使用docker,可以把这些配置信息写入docker容器所在物理机的内存中,而不用请求其他节点的redis,进一步降低网络传输带来的耗时损耗
  3. 筛选条件这类配置信息,客户端和服务端可以约定一个更新缓存的机制,客户端直接缓存配置信息,进一步提高性能

列表数据自动缓存


目前很多框架都是支持自动缓存处理的,比如goframe和go-zero


goframe


可以使用ORM链式操作-查询缓存


示例代码:


package main

import (
"time"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var (
db = g.DB()
ctx = gctx.New()
)

// 开启调试模式,以便于记录所有执行的SQL
db.SetDebug(true)

// 写入测试数据
_, err := g.Model("user").Ctx(ctx).Data(g.Map{
"name": "xxx",
"site": "https://xxx.org",
}).Insert()

// 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

// 执行更新操作,并清理指定名称的查询缓存
_, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: -1,
Name: "vip-user",
Force: false,
}).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update()
if err != nil {
g.Log().Fatal(ctx, err)
}

// 再次执行查询,启用查询缓存特性
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

go-zero


官方都做了详细的介绍,不作为本文的重点。


讨论


我的方案也在我的技术交流群里引起了大家的讨论,也和大家分享一下:


Q1 冗余设计和一致性问题



提问: 一个表里做了这么多外键,如果我要查各自的名称,势必要关联4张表,对于这种存在多外键关联的这种表,要不要做冗余呢(直接在主表里冗余各自的名称字段)?要是保证一致性的话,就势必会影响性能,如果做冗余的话,又无法保证一致性



回答:


你看文章的上下文应该知道,文章想解决的是视频列表筛选问题。


你提到的这个场景是在视频详情信息中,如果要展示这些外键的名称怎么设计更好。


我的建议是这样的:

  1. 根据需求可以做适当冗余,比如你的主表信息量不大,配置信息修改后同步修改冗余字段的成本并不高。
  2. 或者像我文章中写的不做冗余设计,但是会把外键信息缓存,业务查询从缓存中取值。
  3. 或者将视频详情的查询结果整体进行缓存

还是看具体需求,如果这些筛选信息不变化或者不需要手工管理,甚至不需要设计表,直接写死在代码的配置文件中也可以。进一步降低DB压力,提高性能。


Q2 why设计外键?



提问:为什么要设计外键关联?直接写到视频表中不就行了?这么设计的意义在哪里?



回答:

  1. 关键问题是想解决管理后台灵活配置
  2. 如果没有这个需求,我们可以直接把筛选条件以配置文件的方式写死在程序中,降低复杂度。
  3. 站在我的角度:这个功能的筛选条件变化并不会很大,所以很懂你的意思。也建议像我2.中的方案去做,去和产品经理拉扯喽~

总结


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:

  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)
  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度

作者:王中阳Go
来源:juejin.cn/post/7212828749128876092


收起阅读 »

写给焦虑,迷茫的前端人的思考

前言 现在好多人说程序员的红利时代已经过去 裁员的企业比比皆是 简历投出去基本没人理,2,3个月能找到工作就不错了,而且还是降薪 有重点学校毕业的也只能找到外包的工作 千万别学计算机相关专业,无异于50年加入国军 车贷,房贷都要还,降薪我也接受 等等上面现...
继续阅读 »

前言


现在好多人说程序员的红利时代已经过去



  • 裁员的企业比比皆是

  • 简历投出去基本没人理,2,3个月能找到工作就不错了,而且还是降薪

  • 有重点学校毕业的也只能找到外包的工作

  • 千万别学计算机相关专业,无异于50年加入国军

  • 车贷,房贷都要还,降薪我也接受


等等上面现象都是我或听说或看到的,或亲身经历的。


现在行业不景气,也不知道是否有恢复生机的一天,或者也许一直会走下坡路,也触动着你我的神经。


真实经历


去年年底的时候我为我们公司面试员工,通过率变得很低,所以面试难度要相对加大,有一次面试一个女生,她技术我觉得一般,感觉没怎么准备,八股文也没有背熟,所以我给了她一些建议,然后我问了一下她从上一个公司的离职原因,她说是被裁掉了,然后说现在找工作很困难,已经找了很长时间了,上家公司工资16,现在要12,13好多公司都还在压价,她说主要现在有车贷,房贷,还有小孩,所以迫不得已得赶紧找到下一家,所以薪资她一降再降,只希望能快点找到工作,等等,说了很多。


非常同情她的遭遇,但是遇到现在行业不景气,而且技术也一般,所以这就很难受,感觉她的焦虑要比平常人大很多,因为还有车贷,房贷,小孩的原因在里面。说到这里屏幕前的你是否有些焦虑呢?


焦虑由来


如果我们踏入程序员这一行业,那么就意味着终生学习。如果平时喜欢躺平,喜欢摸鱼,那么随着年龄的增大,或者行业不景气,焦虑就会不断找上你。用大多数的人来说,'这个行业就是吃青春饭的',可能在行情好的时候,有些人感觉不出来,觉得自己没有问题的,能干到40岁,但是行情下滑,失业率加大的时候,可能这个问题又会被放大。但是焦虑一定就是坏事么?


什么是焦虑?


引用百度百科里的解释:



焦虑是人对现实或未来事物的价值特性出现严重恶化趋势所产生的情感反映 。与之相反的情感形式是企盼,即企盼是人对现实或未来事物的价值特性出现明显利好趋势所产生的情感反映。


焦虑是指个人对即将来临的、可能会造成的危险或威胁所产生的紧张、不安、忧虑、烦恼等不愉快的复杂情绪状态。



所以我的意思还是希望大家能够主动拥抱变化,主动改变自己,行动起来,那么问题又来了,我们努力的方向在哪里?努力是有方法的,努力要选择好努力的方向,否则辛苦付出了,收获并不会很大。所以说到这里你是否又开始迷茫了?


你是否有这种迷茫?


在技术的学习中大家应该有过下面这种现象,有的同学感觉付出了很多努力,但是收获很少。尤其进阶阶段,研究源码阶段,精通阶段等等,付出了大量时间,但是收效甚微,有时候我们就会产生这样的想法,我选择的这条道路对么?为什么感觉自己还是很菜?我花这么多时间研究源码干什么?


于是这个时候你开始变得焦虑,迷茫?想想工作1,2年的程序员2周就能学会vue并上手了,而你花了相同的时间看了《javascript高级程序设计》这本书还不到20页, oh my god, 这样下去会不会下一个被裁的就是我


这个时候在我脑子里浮现了一个问题'你说工作7,8年的程序员相对于工作两三年的程序员优势在哪里呢?'


如何选择好以后的道路


针对程序员该如何选择自己的道路,我采访过一些工作比较久的程序员。


我的问题是这样的


'你说工作7,8年的程序员相对于工作2,3年的程序员优势在哪里呢?'


下面是一些回答



有的人觉得优势是经验,包括成本预估、架构策略、性能评估、风险评估等, 但是除了这些也不得不承认其他基本都是劣势




有的人觉得工作5年的程序员应该达到技术的至高点,根据统计学规律,程序员以后主要往两条路发展,一种是系统架构师,一种是业务负责人,要早做规划




还有的人说找准目标,缺啥补啥,当然目前是根据环境动态调整的,先把自己能把控好的事做好,大环境不好就降低目标,也能把更多精力放到其他上面




还有的人给出了更加详细的解答



    1、更多关注全局架构的精力超过专注细节的精力
工作早期,接到的任务大多是单点的功能模块和系统,更多关注编码、技术实现问题。
工作7、8年会,会逐渐承担整块的业务,进而会从业务的整体角度来思考系统的架构,目标从“实现某个功能”转变到“支撑某个业务”,把业务当作整体来看,视角逐渐从“语言、框架、数据库”转换到“通信、架构、安全合规、网络、数据”。

2、意识到对技术的表达能力很重要
这不是指为人处事的沟通表达能力,而是对技术的表达能力;
例如更加关注流程图、架构图,不再排斥ppt、文档,会更多思考怎么更加准确、简洁的描述出你的系统架构、业务流程,能够让合作方、上下游更加准确的理解你的思路和设计,更加高效的合作,避免出现理解偏差。
例如编码,早起很喜欢炫技,喜欢各种高大上的编码风格,例如各种函数式语法,工作久了就不再喜欢追求这些,更加看重你的代码是否能够很容易理解,是否足够健壮,能够被测试到,特殊逻辑能否充分注释;

3、不再纠结于单点的性能提升
新人很容易陷入性能崇拜,过分关注有限的性能优化,为了有限的性能提升,直接在db层面写复杂的sql直接处理,导致代码可读性变差。
后来会更加关注有限在符合基础性能的前提下,优先满足业务的诉求,再根据业务的特性、预判一段时间内的发展,适当的在性能和可读性、可复用性上做一定的取舍。

4、更加追求简单的架构、代码
好的代码、架构一定是简洁的,如果一段代码、设计很多人不容易理解,那么这样的设计迟早会出问题。
给你的上下游以简洁、清晰的接口、文档和功能,避免给到用户各种条件选择,避免过度原子化拆分,在可拓展性、易用性方面需要做取舍,

在这里非常感谢以上各位的回答,相信上面的回答能帮助大家或多或少的解决努力方向的问题


just do it


首先说明一点,没有人不迷茫的,大家都第一次做人,怎么会知道接下来每一步该怎么走呢?


有一个人十分崇拜杨绛。高中快毕业的时候,他给杨绛写了一封长信,表达了自己对他的仰慕之情以及自己的一些人生困惑。


杨绛回信了,淡黄色的竖排红格信纸,毛笔字。除了寒暄和一些鼓励晚辈的句子外,杨绛的信里其实只写了一句话,诚恳而不客气:


“你的问题主要在于读书不多而想得太多”。


参考


收起阅读 »

低代码的那些事

web
在当今数字化的时代,前端开发已成为构建出色用户体验的重要领域。然而,传统的前端开发过程往往需要耗费大量时间和精力,对于那些没有技术背景或时间有限的人来说,这无疑是一个巨大的挑战。然而,随着技术的不断进步,低代码开发正迅速崛起,为我们提供了一种简化开发流程的全新...
继续阅读 »

在当今数字化的时代,前端开发已成为构建出色用户体验的重要领域。然而,传统的前端开发过程往往需要耗费大量时间和精力,对于那些没有技术背景或时间有限的人来说,这无疑是一个巨大的挑战。然而,随着技术的不断进步,低代码开发正迅速崛起,为我们提供了一种简化开发流程的全新方法。


终端概念


终端 = 前端 + 客户端


在讲低代码之前,我们先来聊一聊终端概念,这个终端不是指敲命令行的小窗口,而是 终端 = 前端 + 客户端。乍一听,这个不是和大前端的概念类似吗?为什么又提出一个重复的名词,实际上它俩还是有很大区别的,在大前端里面,岗位是有不同区分的,比如前端开发工程师、客户端开发工程师,每个岗位的分工是不一样的,但是你可以把终端看成一个岗位。


image.png


下面是阿里巴巴终端开发工程师招聘的 JD,因为内容较长,我将他分成了三张图片,我们从上到下依次看。


第一张图片:
2024届实习生的招聘,招聘岗位为终端开发工程师





第二张图片:
这是他们对终端开发工程师的描述,大家主要看标了特殊颜色的字体就行



它包括原有的“前端工程师”和“移动端工程师” 相较过去,我们强调面向最终的用户进行交付,不局限于“前端〞、“移动端〞,这将显著拓宽工程师的职责、 能力边界。






第三张图片:
这是他们对终端开发工程师的岗位要求,可以从要求的第1、2、3项看到,这个岗位更侧重于基础技术、终端界面,而不是在于要求你会使用某某框架。





大家对终端概念有了一定了解之后,那么这个终端概念是谁提出的呢?没错,就是阿里巴巴。

阿里巴巴公众号改名史


这个公众号可能会有一些朋友之前关注过,它会发布前端和客户端相关的文章,但是之前的名字并不叫阿里巴巴终端技术。


image.png


我们来看看他的改名史:



  • 2019年05月10日注册 "Alibaba FED"(FED:Front-end Developer 前端开发者)

  • 2019年06月12日 "Alibaba FED" 认证 Alibaba F2E"(F2E:Front-end Engineer 前端工程师)

  • 2022年07月08日 "Alibaba F2E" 帐号迁移改名"阿里巴巴终端技术"


所以是从此又多了一个终端开发工程师的岗位吗,显然不是的,终端开发工程师最终是要取代前端开发工程师和客户端开发工程师的,最终的目的是达到降本增效。


那如何让前端开发工程师和客户端开发工程师过渡成为终端开发工程师。


终端走向荧幕


在阿里 2022 年举办的第 17 届 D2 终端技术大会上,当然他们是这一届将大会名字改成了终端,其中有一篇卓越工程的主题简介如下:


image.png



在过去十年,不管是前端的工具链还是客户端的版本交付效能等都在快速演进,面向未来,我们升级工程体系走向终端工程一体化,覆盖前端及客户端工程师从研发交付到运维的全生命周期,利用低代码、极速构建、全链路运维、Serverless 等新型的工程技术,在卓越工程标准推动下引领终端工程师走向卓越。



可以看到,低代码是可以作为实践终端的一种技术方案,并且将其放在了第一位,那么什么是低代码,低代码能做什么事情,为什么使用低代码可以让终端开发工程师变的更加卓越?低代码对我们普通的一线开发又能带来什么改变或者赋能?
好,接下来,我们就来聊一聊低代码。


什么是低代码


Low-Code


低代码来源于英语翻译——Low-Code,当然,此“Low”非彼“Low”,它意指一种快速开发的方式,使用最少的代码、以最快的速度来交付应用程序。


低代码的定义是什么


虽然低代码已经是个老生常谈的话题了,但关于它的定义我觉得还是有必要描述一遍(来自ChatGPT):


低代码是一种软件开发方法,旨在通过最小化手动编码的工作量,利用可视化工具和组件来快速构建应用程序。它提供了一个图形化的界面,使开发者能够以图形化方式设计和创建应用程序的用户界面、业务逻辑和数据模型,而无需编写大量的传统代码。


低代码它作为一种软件的开发方法,他不仅仅可以作为终端的解决方案,也可以在后端、IOT、大数据等领域上进行使用,并且这些领域也都有现成的低代码开源工具或者平台。


传统前端开发 VS 低代码开发


传统的前端开发方式,我们需要使用 HTML + CSS 描绘出页面的骨架,再使用 JAVASCRIPT 完成页面的功能逻辑。


image.png


可以看到图片上,我们定义了三个 div 元素,并且给每个 div 元素加上了背景颜色,并且加上了点击事件。每一块的元素,都需要我们使用相应的代码去描述。页面越复杂,描述的代码量就会越多。页面的代码量越多,相应的维护的成本就会越高。


我们在来看下如何使用低代码进行开发:


Untitled2.png


左侧物料区,中间画布区,右侧物料配置区,这种区域划分也是比较常见的低代码平台布局。选择物料以后,将物料拖进画布区,接下来我们就可以对物料进行属性配置。


相较于故枯燥难懂的代码,直观的拖拉拽显得更加简单,也更加容易理解。


背后的原理是什么


通过简单的托拉拽后我们可以看到一份表格页面,那么页面实际上是如何生成的呢?


背后实际对应的一份 Schema 描述,主要由物料描述和布局描述组成。


10921685966317_.pic.jpg


我们从左到右依次看,componentsMap 记录了我们当前页面使用的组件,可以看到我们使用了Form.Item、Input、Table,以及这些组件来自的 package 包信息。


componentsTree 里面记录了整个页面布局的信息,最外层由Page组件包裹,然后依次是 Form.Item组件,label 标签为姓名,里面还有一个 input 作为子元素,后面还有两个 Form.Item,为年龄和地址,最后的元素是 Table 组件,通过这些信息,我们就可以布局出一份简单的表格页面。


componentsMap 描述了我们当前页面所需的物料,componentsTree 描述了我们当前页面的布局顺序,将这两份数据组合,通过特定的解析器,就可以得到一份页面。低代码的页面渲染是通过我们事先约定好的数据结构进行生成的。


Schema ⇒ 页面,会不会使我的页面加载变慢


可能会有一些同学心中会有疑问,通过 Schema 生成页面,应该是需要一层 runtime 层吧,通过一段运行时的代码,将 Schema 转换为页面。


那在将 Schema 的内容转换为页面的时候,难免会产生性能上的损耗吧?


性能 VS 可维护性


这里就涉及到了性能 和 可维护性的取舍了,平台的意义在于为你掩盖底层代码的操作。让你用更直观的方式来描述你的目的,其实这里可以牵扯出另外一个相似的话题。


真实DOM VS 虚拟DOM


10931685966489_.pic.jpg


现代化的前端框架都会使用虚拟 DOM,那大家觉得真实DOM更快还是虚拟DOM更快?


框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。


没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。


针对任何一处基准,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。


你会发现低代码的 Schema 和 虚拟 DOM 是比较相似的,都是通过对象的形式去描述 DOM 节点,
虚拟 DOM 的另外一个优势,是在于实现跨端,将底层对 DOM 操作的 API 更换为对 Android 或者 IOS 的 UI 操作。同理低代码的 Schema 也可以比较好的实现跨端,思路基本是一致的,Schema 只是一份组件 + 页面的描述,你可以根据不同的场景,进行不同平台的组件的渲染。


有没有办法可以优化页面渲染的性能吗?


10941685966667_.pic.jpg


那么有没有解决方案呢?


是有的,我们可以通过 Schema 的方式对页面进行出码,出码后是一套完整的应用工程,基于应用工程在去对应用进行一次构建,将这部分负担转移到编译时去完成。


生成的应用工程和我们平常开发的应用工程基本相似:


10951685966768_.pic.jpg


什么是低代码?


如果之前你没有听过低代码,到这里你还是没有明白低代码是什么。没关系,你可以暂时的把他理解一个可视化编辑器,通过拖拽元素就能生成页面,页面背后实际对应的是一串 Schema JSON 数据。到最后,我们会重新对低代码进行一个定义。


低代码发展趋势


低代码发展时间线


image.png


我们来看下低代码发展的时间线:



  • 1980年代:出现了第四代编程语言(ABAP, Unix Shell, SQL, PL/SQL, Oracle Reports, R)第四代编程语言指的是非过程的高级规范语言,包括支持数据库管理、报告生成、数学优化、图形用户界面(GUI)开发和 web 开发。

  • 2000年:出现了 VPL 语言(即 visual programming language 可视化变成语言)

  • 2014年:提出了低代码 / 零代码概念

  • 2016年:国内独立的低代码开发平台开始相继发布

  • 2021年:中国市场逐渐形成完整的低代码、无代码生态体系


发展趋势


image.png


这是 OSS Insight 上关于低代码的一组统计数据,他们分析了50亿的 Github event数据,得出了这些报告,我从里面摘选了和低代码相关的部分。


首先,在2022年热门话题的开源存储库活跃度,LowCode 以76.3%活跃度位居第一,其次是Web3、Github Actions、Database、AI,可见大部分低代码开源仓库都处于一个开发或者维护的状态。


image.png


我们在来看下,低代码发展的趋势图,从2016年到2020年,低代码整体处于上升趋势,并且新增仓库在2020年达到最高点,新增低代码相关的仓库高达了300%以上。


在2020年野蛮生长后,2021年的新增仓库趋近于0,但是在2021低代码相关仓库的start数量将近增长了200%,2022年的数据开始趋于平缓,并且新增仓库数量减少,标志着低代码技术沉淀趋于平稳,百花齐放的时代已经过去。


没有规矩,不成方圆


百花齐放下其实是关于低代码标准的缺失,每套低代码平台都有自己的行为标准,各个平台之间的物料是有隔阂的,无法通用的。就有点像现在百花齐放的小程序,比如微信小程序、支付宝小程序、百度小程序等等,从一定程度上来讲,标准的缺失,会给用户和开发者带来了一定的困扰。


如果有行业组织或者技术社区可以积极推动低代码标准化的倡议,制定统一的行为标准和规范,标准物料的
定义,那么对于低代码的未来发展是一件非常有利的事情。


低代码产品矩阵


我们看来下国外的低代码产品矩阵,种类和平台还是非常多的。
10981685967331_.pic.jpg


可以看到关于低代码的落地场景其实有非常多,并且已经有了大量成熟应用流入市场,所以低代码作为一种开发软件的方法,可以将其灵活运用在各个场景。


而且每个场景的低代码,在其下还可以细分领域,比如 Web 应用程序开发,可以细分为中后台管理搭建、活动推广页面、图表大盘页面等。


一款低代码平台,通常都会有它自己的定位,是为了解决某种特定领域下的特定业务而生。所以一个公司内部有十几个低代码平台是很正常的,他们在细分下的业务场景有不同的分工。


我们正在做什么


这一章节分为三个小块来讲,为什么要做低代码、怎么去做低代码、现在做的怎么样了


为什么要做低代码


我们为什么要做低代码,低代码其实能解决的问题和场景有非常多,那么低代码能帮我们研发团队解决哪些问题?


1.由繁去简


image.png


通常一个需求下来,



  1. 产品会先对需求进行规划,产出一份原型图交付给 UI 设计

  2. UI 通过产品提供的原型图,对原型进行美化,产出一份设计稿

  3. 前端对照设计稿进行页面开发,交付高保真的页面

  4. 最后进行接口联调,将静态的数据更改为接口获取的数据


做程序本质上也是在做交流,假设我们现在是一位前端或者客户端的开发工程师,



  1. 我们需要先和产品 battle 原型图

  2. 和 UI 讨论设计稿

  3. 交付高保真的页面

  4. 和后端进行接口联调


可以看到绝大部分的时间都花在了如何去做页面上,在加上关于各个环节对页面的讨论和修改,这中间会产生大量的浸没成本。


如果说,现在有一个工具,可以做到产品交付的就是高保真页面,你会选择用还是不用?


image.png


这个是使用低代码后的开发流程,由产品直接生成高保真页面交付给前端,极大提高了开发生产力。那么,这个时候前端可以更加聚焦于业务逻辑,聚焦于工程体系,而不是页面本身。


2. 我不想在去改样式了


好像所有的产品经理都喜欢在项目即将上线前,对页面的样式进行调整,没错,既不是测试阶段,也不是预发阶段,而是即将发布前,改完一个,在改一个,好像总是也改不完。


而使用低代码平台后,将页面生成的权利递到产品经理手中,他们可以随心所欲的修改,尽情的展示自己的创造力,我们也不在需要反复的修改样式,反复的重新构建应用发布,你可以专心的去做其它事情。


3. 真正的所见即所得


真正的所见即所得,相比于黑盒子的代码,低代码平台显得更加直观,操作的是页面,而不是代码,你可以在平台上尽情的组装,就像是搭积木一样。


怎么去做低代码


image.png


在能够协调足够多的资源的情况下,选择自研是比较好的一条路,因为一切都是可控的。


但是在资源有限的情况下,选择开源或许是一种最快最便捷的方法了。我们在低代码发展趋势中,可以发现低代码平台和开源技术已经趋于稳定。


使用稳定的开源框架可以更快的帮助我们创建低代码平台,并且有足够多懂低代码底层的人,去帮助我们维护基础设施,站在巨人的肩膀上出发,往往会事半功倍。


我们选择的是阿里开源的 lowcode-engine,在其基础上进行二次开发,选择它的理由有很多:


10991685967710_.pic.jpg


现在做的怎么样了


下面是平台的真实演示,目前已经支持开发以及发布预览了。
_d.gif


低代码架构图:
image.png


平台使用流程的步骤图:


image.png



  • 第一步是创建应用

  • 第二步是创建页面,当然一个应用下面可能会有多个页面,每个页面都会是相互独立的,

  • 第三步是布局调整,可以在选中的页面进行拖拽物料进行布局

  • 第四步是属性配置,配置物料的属性,配置物料的事件或者样式,提供自定义样式和逻辑的功能支持

  • 第五步是保存发布,将当前各个页面的schema进行保存上传,存储到数据库

  • 第六步是页面渲染,可以直接通过平台生成的页面地址,打开页面


被误解的低代码


我相信是会有一大部分的程序员从内心抵制低代码的,一方面,作为一个技术工种,对自己的技术是有底气的,有傲骨的,人为写的代码都不怎么样,还指望低代码平台上的代码吗,另一方面,在低代码平台的代码上维护,那简直就是在屎山上维护,维护的成本会更大吧


出码 VS 不出码


这里的痛点是在于需不需要维护低代码产出的代码,前面我们讲到过出码,出码可以用于产物构建。但构建这一块,是平台去做的,用户并不会感知到背后实际的应用工程。


出码同时也可以用于用户的自定义需求,如果平台的物料完全覆盖了你的业务场景,你是不需要去出码的。但是,如果平台的物料无法满足你的业务场景,你需要的组件又具备足够的特殊性,这个时候你可能需要使用出码功能,在出码的应用工程基础下,添加自己的业务代码,那么这个时候,你是需要维护整个应用工程的。


对低代码的分歧往往是这个时候产生的,每个人心中都有自己的标准代码,都会本能的去抵触和自己标准不同的代码。


不出码如何自定义开发需求?


那有没有既不出码,又可以满足自定义开发的需求呢?


因为我们更多的是希望平台去维护工程,而不是通过人为方式去维护。像我们平时开发页面,本质上就是在写组件,通过拼装组件,形成页面。


我们的思想上可以做个转变,既然 80%~90% 的工作平台可以承接,剩余的平台无法实现,可以自己实现自定义组件进行发布,发布的组件可以沉淀到市场,你的其它项目可以使用自己的业务组件,其他同事也可以使用你的组件。


低代码会不会导致前端岗位变少?


其实完全可以将低代码看成提升工作效率的一个工具,低代码解决页面视图,页面逻辑和数据联调需要开发者在平台上进行完成,开发者的关注点更多的聚焦于业务逻辑,聚焦于如何去做好工程化体系。


AI Code 不是更好吗?


那好像 AI Code 也可以做到低代码能做的地步?


在今年3月份 GPT-4 的发布会上,只需要在草稿本上用纸笔画出一个非常粗糙的草图,再拍照告诉GPT-4需要一个这样的网站,AI 就可以在10秒钟左右根据草图,生成一个网站完整的前端 HTML 代码。


GPT-4发布:一张草图,一秒生成网站


image.png


这简直就是低代码 plus,回归我第一次使用 GPT 的时候,我确实是被惊讶到,特别是他能衔接上下文关系,给出你想要的答案。


我们开发应用,其实本身就是一个庞大的上下文,版本一直迭代,需求一直新增,通过人本身去记住业务上下文,在一个足够复杂的应用下,他的上下文会足够大,足够的冗余,我们去抽离组件,抽离函数,使用数据结构优化代码,实际上就是在优化上下文,写代码并不难,难的是如何梳理页面的组件和那些难以理解的业务以及那些人与人的沟通。


至少现在看来,GPT 无法做到承接复杂应用的上下文,现在的他只能帮助你快速产出一个 demo 应用,前提你需要做到甄别代码的能力,以及还需要面临后续版本迭代更新的窘境问题。


或者说,如果 AI 真的能做到独立开发复杂应用,程序员就真的会被取代吗,做程序本身就是一个相对复杂的活,需要持续学习,持续精进。如果AI真的能做到独立开发这一步,那我觉得离真正的无人驾驶也不远了,出租车司机全部都得失业,因为做一个程序相比于驾驶车辆,会难上不少,当然还包括其它行业,80% 以上的职业都极有可能面临下岗危机。


这个是政客、政府不允许的,虽然科技进步是好事,但是 AI 并没有带来实际的增量工作岗位,反而导致失业率变高,失业率若变高,整体社会的稳定性结构就会变差。
所以,我们更多的将 AI 看成工具,关注点在于,如何用 AI 去做成更多的事情。


什么是低代码?


讲到这里,基本上今天的分享就已经进入尾声了,最后我们在来确定下什么是低代码?



低代码是一种软件开发方法,旨在通过最小化手动编码的工作量,利用可视化工具和组件来快速构建应用程序。它提供了一个图形化的界面,使开发者能够以图形化方式设计和创建应用程序的用户界面、业务逻辑和数据模型,而无需编写大量的传统代码。



一千个人眼中,有一千个哈姆雷特,每个人对低代码的理解都会有些许不同,首先低代码是一种软件开发的方法,这套方法可以用在很多场景。如果一个平台提供了可视化的工具和组件并且又提供部分手动编码的能力,它就可以是一个低代码平台。


在前端低代码的方案中,并不是不再使用 HTML、CSS、JAVASCRIPT 进行开发,而是大大减少他们的使用频率,通过少量的代码,就可以完成一个页面的开发。


参考


收起阅读 »

新时代,你需要了解一下苹果的 VisionOS 系统

这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。 沉浸的光谱。 Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在...
继续阅读 »

这是一个全新的平台。熟悉的框架和工具。请准备好为 Apple vision Pro 设计和构建全新的应用程序和游戏世界。


沉浸的光谱。


Apple vision Pro 提供无限的空间画布供您探索、试验和玩耍,让您自由地完全重新思考您的 3D 体验。人们可以在与周围环境保持联系的同时与您的应用互动,或者完全沉浸在您创造的世界中。您的体验可以是流畅的:从一个窗口开始,引入 3D 内容,过渡到完全身临其境的场景,然后马上回来。


选择权在您手中,这一切都始于 visionOS 上的空间计算构建块。


figure_2x.webp


窗口(Windows)


您可以在 visionOS 应用程序中创建一个或多个窗口。它们使用 SwiftUI 构建,包含传统视图和控件,您可以通过添加 3D 内容来增加体验的深度。


体积(Volumes)


使用 3D 体积为您的应用添加深度。 Volumes 是一种 SwiftUI 场景,可以使用 RealityKit 或 Unity 展示 3D 内容,从而创建可从共享空间或应用程序的完整空间中的任何角度观看的体验。


空间(Space)


默认情况下,应用程序启动到共享空间,在那里它们并排存在——很像 Mac 桌面上的多个应用程序。应用程序可以使用窗口和音量来显示内容,用户可以将这些元素重新放置在他们喜欢的任何位置。为了获得更身临其境的体验,应用程序可以打开一个专用的完整空间,其中只会显示该应用程序的内容。在完整空间内,应用程序可以使用窗口和体积、创建无限的 3D 内容、​​打开通往不同世界的门户,甚至可以让某人完全沉浸在某个环境中。




Apple 框架 - 扩展空间计算


SwiftUI


无论您是要创建窗口、体积还是空间体验,SwiftUI 都是构建新的 visionOS 应用程序或将现有 iPadOS 或 iOS 应用程序引入该平台的最佳方式。凭借全新的 3D 功能以及对深度、手势、效果和沉浸式场景类型的支持,SwiftUI 可以帮助您为 Vision Pro 构建精美且引人入胜的应用程序。 RealityKit 还与 SwiftUI 深度集成,以帮助您构建清晰、响应迅速且立体的界面。 SwiftUI 还可以与 UIKit 无缝协作,帮助您构建适用于 visionOS 的应用程序。


RealityKit


使用 Apple 的 3D 渲染引擎 RealityKit 在您的应用程序中呈现 3D 内容、​​动画和视觉效果。 RealityKit 可以自动调整物理光照条件并投射阴影、打开通往不同世界的门户、构建令人惊叹的视觉效果等等。为了创作您的材料,RealityKit 采用了 MaterialX,这是一种用于指定表面和几何着色器的开放标准,由领先的电影、视觉效果、娱乐和游戏公司使用。


ARKit


在 vision Pro 上,ARKit 可以完全了解一个人的周围环境,为您的应用提供与周围空间交互的新方式。默认情况下,ARKit 支持内核系统功能,您的应用程序在共享空间中时会自动受益于这些功能——但是当您的应用程序移动到完整空间并请求许可时,您可以利用强大的 ARKit API,例如平面估计、场景重建、图像锚点、世界轨道和骨骼手部轨道。所以在墙上泼水。从地板上弹起一个球。通过将现实世界与您的内容融合在一起,打造令人惊叹的体验。


Accessibility


visionOS 的设计考虑了可访问性,适用于希望完全通过眼睛、声音或两者的组合与设备交互的人。对于喜欢以不同方式导航内容的人,Pointer Control 允许他们选择食指、手腕或头部作为替代指针。您可以使用已在其他 Apple 平台上使用的相同技术和工具为 visionOS 创建易于访问的应用程序,并帮助使 vision Pro 成为每个人的绝佳体验。




您需要的所有工具。


Xcode


visionOS 的开发从 Xcode 开始,其中包括 visionOS SDK。将 visionOS 目标添加到您现有的项目或构建一个全新的应用程序。在 Xcode 预览中迭代您的应用程序。在全新的 visionOS Simulator 中与您的应用程序交互,探索各种房间布局和照明条件。创建测试和可视化以探索空间内容的碰撞、遮挡和场景理解。


reality composer Pro


探索全新的 reality composer Pro,旨在让您轻松预览和准备 visionOS 应用程序的 3D 内容。随 Xcode 一起提供的 reality composer Pro 可以帮助您导入和组织资产,例如 3D 模型、材料和声音。最重要的是,它与 Xcode 构建过程紧密集成以预览和优化您的 visionOS 资产。


Unity


现在,您可以使用 Unity 强大、熟悉的创作工具来创建新的应用程序和游戏,或者为 visionOS 重新构想现有的 Unity 创建的项目。除了熟悉的 Unity 功能(如 AR foundation)之外,您的应用程序还可以获得 visionOS 的所有优势,例如直通和动态注视点渲染。通过将 Unity 的创作和模拟功能与 RealityKit 管理的应用程序渲染相结合,使用 Unity 创建的内容在 visionOS 上看起来和感觉起来就像在家里一样。




您的 visionOS 之旅从这里开始。


visionOS SDK 本月晚些时候与 Xcode、visionOS 模拟器、reality composer Pro、文档、示例代码、设计指南等一起发布。


为 visionOS 做准备


无论您已经在 App Store 上拥有应用程序,还是这是您第一次为 Apple 平台开发应用程序,您现在都可以做很多事情来为 visionOS SDK 的到来做好准备。了解如何更新您的应用程序并探索现有框架,让您更轻松地开始使用 visionOS。


Prepare for visionOS


了解 visionOS


visionOS 拥有一流的框架和工具,是帮助您创造令人难以置信的空间体验的完美平台。无论您是在构想游戏、构建媒体体验、设计与 SharePlay 的连接和协作时刻、创建业务应用程序,还是更新您的网站以支持 visionOS,我们都有会议和信息来帮助您制定计划。为第 46 场 WWDC23 会议准备好 visionOS SDK,以帮助您了解平台开发、空间体验设计以及测试和工具。


Learn about visionOS


与苹果合作


在为 visionOS 开发应用程序和游戏时,获得 Apple 的直接支持。了解即将举行的活动、测试机会和其他计划,以支持您为此平台创造令人难以置信的体验。


Learn about working with Apple


#visionOS #苹果MR #苹果VR #苹果AR



翻译原文地址

收起阅读 »

环信十周年趴——程序如人生,常历常新

    “人之生也,与忧患俱来,知其无可奈何,而安之若命”。    二零一五年,正是移动互联网产业井喷爆发的中期。在那个毕业季的夏天,无数走出校园的学子摩拳擦掌,准备在当时遍地黄金的互联网世界中大展宏...
继续阅读 »

    “人之生也,与忧患俱来,知其无可奈何,而安之若命”。


    二零一五年,正是移动互联网产业井喷爆发的中期。在那个毕业季的夏天,无数走出校园的学子摩拳擦掌,准备在当时遍地黄金的互联网世界中大展宏图,发挥自己的价值。作为百万毕业大军中的一员,我参加了培训班的培训,加上之前在学校时移动互联网课程的专业培训的积累,幸运的找到了一家P2P金融公司,担任iOS开发工程师岗位。

    这是一家初创公司,老板非常年轻,是北京某财经大学毕业的高材生,毕业后和几个同学合伙,找到投资人,创办了这家公司,员工都是和我一样同龄的九零后,八零后都极少,刚入职得我没有隐瞒培训经验,顺利通过了面试,老板和技术团队的领导也接纳了我成为他们的一员。从此我便跟着同组的同事,一边工作完成安排的开发任务,一边继续学习积累经验。团队成员相处之间都很融洽,在那一个全民创业的年代中,做这样一份行业并非是件很困难的事情,我们的发展也算顺风顺水。在这期间,我通过学习iOS的知识,在同事和领导的帮助下,我逐渐成长为可以独立开发独当一面的项目组成员。

    时间过得很快,转眼到了二零一六年,公司内初创团队的人员流动了不少,因为在当时满地的机会,大家随便跳槽薪资就可以轻松上涨数千元,甚至翻倍,尽管跳槽带来的薪资十分诱人,但是我仍然稳住了心态,因为当时的经验还不足,需要足够的时间积累,而没有积累足够的开发经验和开发的眼界,盲目的跳槽也不会长久。况且我不是一个喜欢跳槽和追求不稳定的人。这样的心态下,我一直没有动作,一心想着安心做好我负责的事情,并且牢记人而不学,其犹正墙面而立的道理,继续拜读技术文章和书籍,提升自己的开发技能水平。


    就这样,我在这家公司一直做了三年,时间来到了二零一八年,互联网的那股热潮开始褪去。P2P行业也出现一些变故,似乎预示着暴风雨前来临的宁静。我们就如同大海中稳定的小舟,虽小,但是也能挡住惊涛骇浪。我们当时的App经过多次迭代已经稳定,只有小改小动的小需求,团队也没有加班任务,大家正常上下班,一切似乎朝着安稳的方向发展了。我也放松了警惕,但是正如点题中所说,人一生下来,忧患就是随之而来的,若放弃了这一点,则遇到真正的问题时会措手不及。不懂这些道理的我,就遇到了我的程序人生中的第一个转折点。同年八月的一天凌晨,我们公司老板在没有任何预警的情况下,在App内发布了一个清盘公告,告知投资人,将逐步清盘,有序退出投资人的资金,没多久这个公告就撤下了。但是仍然已经被用户截图发到了各个投资群中。第二天一早我们去上班的时候,已经有投资人坐在公司门口等着我们了。那是漫长的一天,在那一天里,投资人、民警、公司领导、看热闹的其他公司员工,凑在公司门口,上演了一出闹剧。我们甚至中间出去躲避了几个钟头,下午才回来办公。到了晚上,事情又发生了变化,出乎所有人意料。经济侦查的执法同志来了,并告知我们,老板已经被控制,目前公司涉嫌自融犯罪以及虚假标的,让我们每个人记下个人身份信息和手机号,拿走自己的个人物品,离开公司。

    大家都懵了,执法同志也没有为难我们,放我们走了以后,大家都在公司楼下不肯离去,最后有人提议吃一顿饭去。餐后,大家商量了一下对策,决定先回家等待公司消息。当晚,人事在公司群里说,已经帮大家社保减员,各自可以去找新工作了,我们才知道,公司黄了,相当于遣散了各位。我意识了点题的后半句,既然知道了这是无可奈何的事情,就接受命运的安排。


    八月底,秋似乎来得过于早了些,虽然正午的阳光仍然毒辣,但是在北京这片钢铁丛林的阴影下的风,已经不再燥热了。我奔波在一栋一栋的大楼间,进出一个又一个公司,去参加对我来说已经十分陌生的面试。由于对未来的不确定,以及自己给自己制造的焦虑,我匆匆忙忙入职一家做企业资源管理软件的公司。然而,忧患和安逸,如同鸟之两翼,车之双轮,互为条件,彼此支撑。不可不思进取,但也不能因盲目担忧从而给自己做出了错误的选择。我入职的这家公司,也因为整体经济形势的冲击,导致遇到了裁员,在十二月份我即将通过试用期时,和一旁的安卓同事,以及两位后端一起遭到了公司的裁员。

    没有任何辩解和理由,我也没有去争取赔偿,但是我没有意识到盲目的焦虑是不可取的,仍然如同热过上蚂蚁一般继续面试,对待专业技能的相关文章也是泛泛阅览,缺乏思考,正所谓眼中了了,心下匆匆,方寸无多,不仅影响自己,而且对职业生涯的提升也无益处。在骑驴找马的心态下,我匆匆入职一家小公司,不仅单休而且工作很累,经常加班,我工作了十天,在这十天里,我对自己的选择进行了反省,对自己的心态进行了反思,也对大环境现状理解了一些,正所谓天下将兴,其积必有源;天下将亡,其发必有门。移动互联网的发展早有征兆,我在要拼搏的时候选择了安逸,却要在要安逸的时候选择拼搏,因此才会在这半年过得如此艰难。


    正巧,之前投的一份简历约到了面试,是一家央企旗下的子公司,开出的待遇低于我的心理预期,但是我仍然下定决心入职,于是迅速地和目前的公司办理离职,拿到离职证明后,终于入职了这家我心仪的公司。

    二零一八年底,我来到了这家公司,我在这里工作了已经四年半了,也渡过了整个疫情期间席卷移动互联网的经济风暴,但是我仍然牢记着之前留下的教训。我没有落下继续进步学习的步伐,尽管在这家公司入职的头两年依然加班很累,工作压力也很大,也遭受了一些领导的排挤。但是我也积累了一些自己的经验教训,也逐渐改变了领导的看法,我学到了心有所畏,方能言有所戒,行有所止。我得到的不仅仅是技术上的提升,更是做人做事的道理,在未来,也许不知道哪一天,我的程序人生可能会换到全新的道路上去,但是不管做任何事,要和做人一样,要坚持三省吾身,谓之思危思退思变,牢记生于忧患死于安乐的道理,那么在未来,也一定能继续实现自己的人生价值,为社会做出自己的一份贡献。


    最后,感谢环信可以提供这样一个平台,仅以此文祝贺环信十周年生日快乐,祝愿环信在新的十年大展宏图,乘风破浪,奋勇向前,继续开创新的辉煌。



本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026

收起阅读 »

环信十周年趴——我的从业之路

        我的从业经历,对大家来说就是一个避坑史,感觉自己啥坑都遇到过。       2015年,我大学毕业了。毕业即失业,校招压根没公司来看一眼我们。你肯定会说,这怪谁,谁...
继续阅读 »

        我的从业经历,对大家来说就是一个避坑史,感觉自己啥坑都遇到过。

       2015年,我大学毕业了。毕业即失业,校招压根没公司来看一眼我们。你肯定会说,这怪谁,谁让你学校垃圾。是的,不可否认,学校确实不行,这也导致我们无人问津。毕业之后回到了所在的城市,整天往人才市场跑,奈何这个城市没啥网络企业以及科技公司,全是招聘销售人员。药品销售,保险销售,地产销售,我看着人来人往的人才市场,偌大的城市好像容不下我一人。迎面走来一个招聘的小姐姐,婀娜多姿,我的目光在她身上移不开。在当时我感觉她的声音就像乡间的轻铃,清脆悦耳,字字敲击耳膜。她看向我,目光流转,翘眉生盼,然后诚恳的让我加入她们,一起为保险事业做贡献。我欣喜若狂,使劲的点头,但是我又还想找互联网的公司,我不甘的又摇头。她说你不想跟着我一起为众人的健康事业而努力奋斗?我点头又摇头,她拽着我就要去办理手续,最终我还是坚守住了本心,我知道我们俩的相遇只是命运的一个玩笑,因为她结婚了。

       后来,我在这个城市找了份网管的工作,日常任务就是跟着一个老师傅去这个城市的各个地方机房里去维护。都是些没人想干的脏活累活扔给外包公司,然后我们去干,去加油站里面给机房走线,整理机房,布置服务器机架,没有丝毫的技术含量。自己也在慢慢沉沦,可能也就要这样,匆匆忙忙,无所事事的度过往后,但是我又好不甘心...

       就这样在一次次的纠结,煎熬中,来到了2016年5月份。我厌倦了这样的工作内容,转身投奔了在北京的朋友。来到北京一切都是那么的新奇,心情是那么的兴奋,感觉就连空气都是甜的,四周都是自由的气息。朋友把我带到了他在城中村租的一个屋子,虽简陋但却很整洁,他说:有wifi有空调就够了。是啊,还奢求什么呢,北京本来就令人向往。我们坐在一起吃着肉,喝着啤酒,高谈阔论。唯一一点让我感觉不爽的是空调,因为它不仅制冷,还喷水,向内喷,喷的身上到处都是,也不知道房东通过什么神通手段安装了一个这么个奇葩空调。后面我在朋友这,边住边找工作,最终找到了一个愿意收留我的公司,虽然我会的不多,但是公司看我还算本分,一问三不知,那是确实一点都不知道。我在公司跟了一个老员工开启了Android之路,他会分我点特别简单的工作,然后把自己珍藏多年的种子,哦,不是,是搜集的Android项目,让我学习,尽快能承担更多的工作。在这个公司我一直在成长,学的也很快,公司看我本分,所以工资也很本分,后面也有调整,但是还是调整的很本分。我感觉我是有野心的,所以在两年之后,我选择了离开,之后便开启了找工作之旅。

       没多久我就进入了一家做线上游戏陪玩的公司,因为工资足够的低。我进去之后才知道公司之前融资过2000w,但不肯在我身上多花一分钱。钱呢大部分被老板挥霍了,挥霍到哪了呢,我猜大部分是挥霍到自己兜里了,因为他又给自己买了辆50来万的车。随着资金越来越少,我们的办公场地,办公环境以肉眼可见的速度在迅速变差,不仅越来越小,最后只能跟其他公司在一个屋里面拼凑,要不是同事早都认识了,我估计大家都会认为,这个屋里的都是自己同事。2019年开始,公司颓势越发严重,工资也开始断断续续,但是好在老板还有点良心,每个月给发,只是日期不固定了。在4月的时候,终于还是元气耗尽,
公司不行了。我再一次的开始了找工作,没几天就去了一家搞平行进口车的公司,这家公司有3个前端维护网站,然后新招一个Android跟一个ios。移动端项目接口用网页端的,进来按着原型图搞就行了。上班第一天就开始匆匆忙忙开始写项目。晚上加班更是家常便饭,时不时还有人一直催。功夫不负有心人,在2个多月的时候,项目搞完了。这个公司的转正需要提前半个月申请,我们申请之后发现,批准就没动过,也找人事主管咨询过,她说不清楚是怎么回事,然后在某一天,人事找到我跟ios,告诉我们转正不批了,让我们走人吧。费半天劲,项目搞完,卸磨杀驴,遇到这样的也是恶心至极。


       干了三个月,又要匆匆忙忙的找工作,随后来到了一家搞广告传媒的公司。本以为可以安安稳稳的待一段时间,但是谁料屋漏偏逢连夜雨。在经过四个月的试用期80%工资之后,疫情突起,铺天盖地而至,开启了在家办公、值班的方式,公司业务也大受打击,随即开始降薪,只发一半工资。又在当前公司挣扎了5个月之后,看着日渐空扁的钱包,实在是无以为继,只能重新开始找工作。后来呢入职了一家稍正常的公司,正常呢,也只是说可以正常发工资,日期也是充满随机。随着年龄的增长,钱包却还是依然空扁如故,看到别人都是锦衣玉食,衣冠华丽,而我却还在原地兜兜转转。最终在2022年,离开了北京,一个待了6年的地方,一个充满回忆的地方,去的时候充满了向往,回来的时候带走了满眼的沧桑...


本文参与环信十周年活动,活动链接:https://www.imgeek.net/question/474026

收起阅读 »

2023和自己聊聊

自我质疑,他人质疑 前几天约了面试,被面试官问了一个问题,你做这么久的开发,有哪些技术沉淀呢,或者自己擅长哪些呢,我突然楞了一下,其实自己也想过这个问题,结论是啥也不是,很一般。给面试官说目前 vue 用的比较久,做过不少项目,对这个比较擅长一点吧,从开始做项...
继续阅读 »

自我质疑,他人质疑


前几天约了面试,被面试官问了一个问题,你做这么久的开发,有哪些技术沉淀呢,或者自己擅长哪些呢,我突然楞了一下,其实自己也想过这个问题,结论是啥也不是,很一般。给面试官说目前 vue 用的比较久,做过不少项目,对这个比较擅长一点吧,从开始做项目都是自己摸索,从最开始的 vue2 到现在的 vue3 和 react 项目技术框架是我负责主导的,然后也会帮组员处理一些问题之类的。但是我从你的面试结果来看,多数情况下是了解或者知道某个知识点的简单使用,但细节的问题就看你支支吾吾的,应该是理解不到位吧。有什么比较好的项目,或者攻克了哪些技术难点可以做下分享吗。我沉思了一下,好像觉得没什么值得去展示的,总觉得自己的项目很平常,就算是平常遇到一些问题,很快就能解决,不是自己厉害,而是实在是项目简单而已。那你为啥觉得你能帮助别人解决问题,帮助其他组员成长呢,我陷入了无限的沉思...。


工作经历




  1. 第一家是一个外包公司,算是用了包装的简历蒙混进去的,结果是差一点给自己干离职,压力太大了,真的是s什么都不会,感觉实在是扛不下来了,于是在项目交付的前三天说自己家里有事,提了辞职。结果没辞成,老板说你忙完了再回来就行,你的工作我先让其他同事接替你。(当时也去了新的面试,但是结果可想而知)于是在请假这两周中迅速恶补,像是要奔赴战场,硬着头皮回去了,在那个接替我的同事的帮助下终于开心的(提心吊胆,每天想着二次辞职,又碍于没有脸面再提,咬咬牙终于坚持了下来,整理了八百字的小作文描述当时的过程,后来想想还是不写出来了吧)完成了第一个jsp版的项目。




  2. 后来公司接了一个新的项目,做一个后台管理系统,让我来做前端,说写好页面给到java那边,让他们来开发,还是用jsp那套。当时心想着是用 vue 脚手架搭建,来做前后端分离模式,但是我一点经验也没有,问了我那个同事,她也没这做过这种模式的,她坚持自己的意见是说用老一套,类似 jsp 那样。毕竟她比我有经验一些,那就听她的先做下试试,但心里还是想着用前后端分离来做,没人指导,只能自己去摸索,最后还是找我领导商量前后端分离模式开发。他之前做 java 的,对前端也不懂,问了我前后端分离的东西,我也是现学现卖,告诉他怎么好怎么好,但是我之前没用过,是有试错成本的,他问了我这些技术目前成熟吗,我说好多公司都开始用了,以后这个是主流。在我的忽悠下同意了这个方案。当然一切都没那么顺利,也是一步一个坑,一步步趟了过来。也感谢我这个领导,在五月份我准备辞职回去毕业答辩时帮我申请了两周的假,顺利毕业。在这个后台管理项目如期上线以后,我也终于松了一口气,没有辜负领导的信任。也感谢当时的自己坚持了自己的想法,虽然过程很难,但是也扛了下来。




  3. 慢慢的发现遇到了技术瓶颈,最开始的自己像一个海绵,进入公司后一直在吸水给自己充电,后来充电越来越慢,甚至出现了漏电的情况。于是准备跳槽,在这个外包公司离职后进入了外派的这家公司,等于从乙方进了甲方,等于好像并没有跳。日复一日的上班,加班,下班好像做了很多,但是又好像什么都没做,整天做一些表单,表格的增删改查,没什么长进,差不多一年。于是准备第二次跳槽。然后准备过完年开始第二次跳槽。就遇上了疫情,然后又呆了一段时间,准备再过了年跳槽,然后在已经开始谈 offer ,准备再多面几家时,上海又开始了疫情,直接封了三个月,那个 offer 也就不了了之了。去年年底约了些面试,都不太理想,多数都是外包,然后就到了现在。想想还是因为自己不够坚决吧。




精神内耗


一方面觉得自己不够优秀,想要去努力,另一个方面在学习时发现很多东西太难了,然后就放弃了。于是在一边想要躺平,一边想要好好学习的的状态下无限循环。然后开始了自我怀疑,自己适合做这方面的工作吗,自己做这方面有优势吗,自己有什么技术上的优点值得拿出来说说吗,好像都没有。一次次的面试,一次次的没了下文,然后都把原因归结于自己不够优秀。于是又进入了,那为啥不好好学,我试着去学了,但是学不进去,学不会的轮循怪圈。


反思与醒悟


2023年了,想着自己要去改变些什么,但是又不知如何去做,之前买了不少的书,但看的也就几本其他都在吃灰。看朋友圈有人在微信读书,于是也试着看一些书看解决一下心理浮躁的问题,不能这么浑浑噩噩下去,不然真就废了。工作,生活,情感压力感觉都快抑郁了。直到最近看了大佬分享的书,才开始有所醒悟,是自己太急于求成了。太想在刚投入一点精力就要看到成果了,平常是看了不少学习的资料,但也都是在自己舒适区内,一旦遇到难的就告诉自己肯定学不会,所以就放弃了,不会将难题碎片化,一次解决一个小问题,爬山也不都是一步一步走上去的嘛。学会去接受自己的平凡,但是不能以自己是个普通人为理由而不去努力。实践是验证真理的唯一标准,所以我们在学习时也更要去思考,去试着用自己的话看能不能书写出来,讲给别人听,看对方能听明白不。如果只是以为自己去学习了,就万事大吉了,但过段时间可能就会忘记了,这一点我最近特别有体会。就拿写的两篇 vue 的基础知识点来说,以为自己很容易就能写出来,但写的时候发现没那么容易的。有的地方可能还需要再查下资料才能搞明白,不过也加深了对这些东西的理解,如果在帮助自己的同时能帮助别人就更好了。


一起共勉


书上的几个观点觉得很有用,分享给大家,如果目前有小伙伴也有我上面的焦虑

1. 试着跟自己和解,停止精神内耗,接受自己的普通,但不能因此而止步不前,摆烂

2. 在自己跳一跳就能够得着的地方做拉伸,在舒适区和困难区要么无所事事,要么备受打击

3. 不要急于求成,罗马不是一天建成了,只管按照自己的节奏去努力,事实会告诉你答案

4. 输入的同时也要去输出,形成闭环,实践是验证真理的唯一标准,试着去做到知行合一


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

程序员增强自控力的方法

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法: 1. 设定明确的目标和计划 制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任...
继续阅读 »

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法:


1. 设定明确的目标和计划


制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任务清单、时间追踪工具等,来帮助我们控制时间并更有效地完成任务。


2. 掌控情绪


作为程序员,我们需要面对很多挑战和压力,容易受到情绪的影响。因此,掌握情绪是一个非常重要的技能。可以通过冥想、呼吸练习、运动等方法,来帮助我们保持冷静、积极和乐观的心态。


3. 管理焦虑和压力


焦虑和压力是我们常常遇到的问题之一,所以我们需要学会如何管理它们。我们可以使用放松技巧、适度锻炼、交流沟通等方法,来减轻我们的焦虑和压力。


4. 培养自律习惯


自律是一个非常重要的品质。我们可以通过设定目标、建立规律和强化自我控制等方式,来培养自律习惯。


5. 自我反思和反馈


经常进行自我反思和反馈可以帮助我们更好地了解自己的优缺点和行为模式。我们可以使用反馈工具或与他人交流,来帮助我们成长和改进。


6. 持续学习和自我发展


程序员需要不断学习和自我发展,以保持竞争力和提升自己的技能。通过阅读书籍、参加培训、探究新技术等方式,可以帮助我们持续成长,增强自我控制力。


结论


自控力是我们工作和生活中重要的的品质之一,可以帮助我们更好地应对各种挑战和压力。通过设定目标、掌控情绪、管理焦虑和压力、培养自律习惯、自我反思和反馈、持续学习和自我发展等方法,我们可以帮助自己增强自我控制能力并提高工作效率。


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

🎖️怎么知道我的能力处于什么水平?我该往哪里努力?

🎖️职业水平怎么样才算达到平均标准?我来告诉你 嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 毕业后进入社会,我像大家一样感到恐惧和不安。在这个新的阶段,我们都投入了大量时间和精力来从事各种社会劳动,同时也努力满足自己的经济需求。我们每个人在这个过程中都会去思考...
继续阅读 »

🎖️职业水平怎么样才算达到平均标准?我来告诉你


嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


毕业后进入社会,我像大家一样感到恐惧和不安。在这个新的阶段,我们都投入了大量时间和精力来从事各种社会劳动,同时也努力满足自己的经济需求。我们每个人在这个过程中都会去思考如何实现自己的人生价值,追求小时候美好的憧憬和期盼。💼


然而,在这个思考的过程中,没有人能为我提供确切答案。离开了学校的庇护和老师的指导,我感到比学校学习时更加困惑。未来的方向不太清晰,这使我在面对职业选择、个人发展和人生道路时遇到了许多挑战和困惑。🤔


有没有想过你职业生涯的下一步应该是什么呢?🤔


你怎么知道接下来要学习什么工具、原则或编程语言呢?📚


我想和大家分享一个超级简单的程序员分级定义思路,也许它可以帮助你这个处于职业生涯各个阶段的开发人员找到下一个目标并迈向更高的境界!✨


🚩声明:不一定正确,只是一组思路


以下的内容可能不一定正确,因为不同企业对员工能力的定义可能会有所不同。甚至每个人对这些级别的定义也会有很大的差异。🚫


然而,排除了内卷化的分级标准后,我接下来要介绍的每个阶段都代表了职业生涯中大多数人可能达到的“位置”。🎯


在每个等级之间,都存在一些过渡,可能需要在特定领域获得更多的知识和经验,也可能需要提升社交方面的知识和经验。🔀


每个等级都是在上一个等级的基础上进一步发展而设立的,我对此有着自己的职场经验启发。💡


然而,请注意:我所说的这些并不一定与你目前所处的职位相对应。 🚫


在某些公司,拥有“高级开发工程师”职称的人,实际上在技能和专业知识能力方面可能只是初级开发工程师!👨‍💻🏢


在职场中,许多人之所以被晋升,仅仅是因为他们在该领域(无论是前端、后端还是运维)有几年的经验,并非因为他们具备胜任所需的技能和知识。📚


同时,很多情况下,他们之所以成为公司中业务经验最丰富的“高级开发工程师”,仅仅是因为他们在同一家公司工作了很长时间,从而“熬掉”了许多老员工。⏳


这个世界并不公平,我相信大多数人都已经看到并经历了这种情况。🌍


当然,我还想补充一点,我所描述的这些等级并不是一成不变的标准。在你所在的领域中,有些地方对这些要求可能并不那么严格,所以你不需要过于关注我所提到的要求。🤔


以下内容仅供参考,希望能够帮助你更好地管理和掌握你未来的职业规划。说到底这仅仅是一种思路,我不是行业领袖,它仅仅是一组思路。🔍


1️⃣编程爱好者



“我有点不知道该怎么给这个阶段的 coder 定个级,算了,咱们姑且称他们为"编程爱好者"吧,但其实我还是觉得这个说法不太准确。😕”



我这里所指的“编程爱好者”是指广义上的 coder ,也就是那些会写代码或者热衷于写代码的人。💻


这些人可能有以下特征:



  1. 他们并非以“编程”为主业,而只是因为兴趣或者作为该专业的学生而加入到我们这个圈子中。对于那些以编程为职业的开发人员来说,他们算是“业余”的。🔍

  2. 这些开发爱好者了解编程语言的语法,并且能够熟练运用他们擅长的编程语言,甚至有时候比一些专业开发人员表现得更出色!📚

  3. 他们有能力独立开发一些小型项目,例如脚本、网页、游戏或应用程序。🚀

  4. 他们擅长使用搜索引擎自发解决问题。🔎

  5. 然而,在这个阶段,他们的编程能力并不能直接转化为经济利益,也就是说他们并不能通过技能获得收入。🚫


2️⃣初级开发工程师


"初级开发工程师"代表着那些已经以专业人士的身份进入IT领域的人,他们需要与其他专业人士合作,一起完成工作任务。👩‍💻


他们可能有以下特征:



  1. 他们是以编程为主要职业的专业人士,企业需要支付报酬雇佣他们加入生产。💼

  2. "初级开发工程师"会被分配到一个或多个项目中工作,但他们可能无法完全理解整个项目的结构,因为对于他们来说,项目可能还是“太大”了。🔨 在这个阶段,他们更多地承担一些被拆分成小模块的任务,对于项目的整体认识,他们并不清晰。🔎

  3. 他们可能只对自己专业领域有了解,在工作中需要继续学习前后端通信和数据库连接等跨系统的知识。📚

  4. 他们需要在中级开发工程师或高级开发工程师的指导下完成工作。🤝



“这些特征是一般情况下的描述,具体的职位要求和工作内容可能因公司和行业而异。📋💼”



3️⃣中级开发工程师


到了"中级开发工程师"阶段,他们已经适应了业内的整体开发节奏,成为了一名合格的开发团队成员和代码贡献者。🚀


在这个阶段,他们具备以下特征:



  1. 能够独立构建业务模块,并熟悉最佳实践。例如,在Web应用中开发单点登录模块。🏗️

  2. 开始了解项目的基本系统架构,对领域内的架构、性能和安全性有一定的了解。🏢

  3. 能够熟练使用专业工具来提高工作效率。🛠️

  4. 对设计模式和良好的编码习惯有基本的了解。🎨

  5. 能够在常规工作中独立操作,无需过多监督。💪

  6. 对于高级开发工程师来说,他们可能缺乏经验,需要经历几次完整的开发周期和遇到很多“坑”之后,才能学会如何在下次避免它们。🔍



“这个阶段的开发工程师最缺乏的就是项目实践经验。只要有不断地项目经历,通过实践和经验积累,他们就会不断成长。🌱”



4️⃣高级开发工程师


遗憾的是我们中大多数人在职业生涯中大部分时间都在面临从“中级开发工程师”到“高级开发工程师”的门槛。


有些“开发工程师”可能在整个职业生涯中一直停留在中级水平。


“高级开发工程师”之所以与众不同,是因为他们知道什么可以做,什么不可以做。这种洞察力是通过过去犯过的错误和经验教训获得的。


开发经验对于成为“高级开发工程师”至关重要。


根据我的理解,“高级开发工程师”应该具备以下特征:



  1. 精通团队所使用的核心技术,对其应用得非常熟练。💪

  2. 熟悉系统架构设计和设计模式,并能够在团队项目中应用这些概念,构建更复杂的系统。🏢

  3. 拥有构建“完整”解决方案的经验,能够考虑到项目的各个方面并提供全面的解决方案。🔍

  4. 在服务器部署和维护方面有一定的经验,了解负载平衡、连接池等跨领域知识。🖥️

  5. 作为团队的核心成员,能够担任导师的角色,积极指导中级和初级开发工程师。👥


其中最后一条是最最重要的。如果不能把你的经验、专业知识和知识传授给你的团队成员,我认为这就不是一个合格的“高级开发工程师”。


成为“高级开发工程师”的一个重要指标:一定是团队的其他成员经常向你寻求建议和帮助



“如果你还在沮丧为什么同事老是问我问题,也许现在可以改变一下想法了。💼


因为你是你们团队最重要的百科全书呢!也许现在是时候考虑向老板提出加薪的要求了呢?💰”



5️⃣开发领袖



这个阶段我也有点困惑,不知道要给他们这个等级取一个准确的称号。我想了两个名字:“高级架构师”和“团队领导者”,但是我又想,其实高级工程师也可以领导团队,也有架构能力啊。那就还是加“领袖”两个字,突出在技术领域的高级能力、团队领导能力和架构能力。这样看起来就更厉害了!👨‍💼



在这个阶段,程序员们已经不再仅仅为一个团队服务。他们可能同时为多个团队提供支持,并向下属团队提供更底层的指导,特别是在设计和早期产品开发阶段。💪


在国内,由于很难找到同时在业务领域和专业领域都深耕的人才,这类职位可能被企业分拆为不同的职能,更加注重管理能力而非专业能力。 🤔最终可能招聘了一个“高级监工”(毕竟,同时在业务领域和专业领域同时深耕的人真的少之又少,而且一般企业也不愿意花费与之对等的报酬)。


因此,大部分人可能会不同意我这个阶段的观点。 😕开发领袖的职能范围可能涵盖“敏捷教练(scrum master)”、“DevOps”、“项目经理(PM)”、“CTO”等管理职务。


因此,开发领袖最重要的特征是:



  1. 对业务领域有深刻的理解,能够消除开发团队与企业其他业务部门之间的沟通障碍。🌐

  2. 发挥"PM"职能: 协助规划产品开发和时间表,向营销或销售团队提供反馈。📈

  3. 发挥"CTO"职能: 协助高层管理,实现企业愿景,领导开发团队实现企业的业务目标。📊


因此,开发领袖必须对所处的业务领域(如医疗、金融、人力资源等)的产品有深入的了解。🏥 基于这些了解,他们能够理解软件所解决的业务问题,并且必须了解其他学科,如管理、产品开发、营销等,以消除各部门合作之间的沟通障碍。


简而言之,高级开发工程师和开发领袖的区别在于:



  1. 高级开发工程师也担任团队领导的角色,但主要面向开发团队的“内部”。👥

  2. 开发领袖则超越团队内部管理,他们的管理职能是面向“外部”的,致力于消除开发团队与公司其他部门之间的沟通障碍。🌍


因此,成为开发领袖需要具备高层领导的全局视野,并能够将业务术语和技术术语相互转化。🔑


如果你能够在公司内很好地与业务同事交流技术解决方案,并让其理解,那么你已经拥有了“开发领袖”其一的核心能力。💡


6️⃣领域专家


这个阶段的他们已经跳出了企业的限制,在一些特定领域也颇负盛名。他们的解决方案不再是只为一家企业服务,他们擅长的领域也不是一般的学科分类,而是一个非常有针对性地细分领域。🚀


可惜的是,一般的开发者们很难接触到这些领域,你想要了解他们的知识都不知道从哪儿下手,因为他们的知识分享大多是封闭的,只在内部共享,不对外传播。🔒



“可能你会觉得这与你对开源软件行业的理解不太一样,开源难道不是互联网发展的第一推动力吗?是啊,我同意你的观点,但你不了解不代表它不存在。其实大部分的技术分享都是在内部进行的,许多讲座和峰会也只限邀请制🔐。”



他们可能是某种编程语言的奠基人,可能是Web安全领域的重要任务驱动者,也可能是教导其他前端开发者如何使用React的大师,甚至还有那些在特定行业中扮演技术导师角色的人!👨‍💻


他们还可能是某个社区的建设者,在互联网和社会上有一群人将他们视为直接或间接的导师。🏢


他们也可能是支持特定事业或理念,并为之做出显著贡献的思想领袖。💡


他们会公开地讨论自己的专业领域和他们所推崇的理念。🗣️



“如果你也有自己的小圈子。比如在掘金社区;比如在GITHUB,拥有自己的互联网开源项目,并且有一大群粉丝用户支持和拥护你的产品和理念。那你也可以算是某一细分领域的专家了。👥”



总而言之,他们的一举一动都可能对互联网技术的发展产生重大影响。😄




🎉 你觉得怎么样?你认为自己处于哪个阶段?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


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

Android抓包环境搭建

文章不会从零开始教你搭建抓包环境,需要你具备一定的环境基础,满足以下条件才能继续看文章: 电脑已经安装好Fiddler或则Charles; 手机已经root且具备Xposed环境; 如果已经具备上诉环境,那么可以开始继续搭建环境了。 搭建入门抓包环境 An...
继续阅读 »

文章不会从零开始教你搭建抓包环境,需要你具备一定的环境基础,满足以下条件才能继续看文章:



  • 电脑已经安装好Fiddler或则Charles;

  • 手机已经root且具备Xposed环境;


如果已经具备上诉环境,那么可以开始继续搭建环境了。


搭建入门抓包环境


Android手机抓包


手机有Xposed环境,抓包就很简单了,按照下面的步骤操作即可:



  1. 安装Xposed模块—TrustMeAlready_1.11.apk,软件可强制跳过证书验证。

  2. TrustMeAlready.apk内勾选需要抓包的app。

  3. 安装抓包工具-小黄鸟(HttpCanary.apk)

  4. 开始抓包


这种抓包方式有个缺点:有的请求会抓不到。


电脑抓包


电脑抓包的话需要安装根证书,可以参考这篇文章


抓包环境进阶


上面入门环境有时候会出现抓不到某些接口的情况,这时需要将手机端所有的请求进行拦截转发,然后用电脑端抓包工具查看。具体的步骤如下



  1. 安装拦截转发工具(VProxid_1.2.0.apk),可将流量劫持到PC端。

  2. 配置VProxid


这里详细的说下如何配置VProxid,它的界面如下


image20230103172419726.png




  1. 点击右下角的➕,界面如下


    image20230103204810203.png




  2. 设置完成后,回到主界面,点击绿色三角就可以劫持应用的网络通讯




  3. 电脑端抓包工具就可以看到应用的请求记录了。




这种抓包方式,可以抓到应用的所有网络请求。


总结


抓包可以说是逆向的第一步,本篇文章介绍了手机抓包和电脑抓包的方式,进一步的介绍了如何劫持应用的流量到PC端,通过劫持应用的流量到PC端,可以抓到app的所有网络请求,建议使用这种方式,不然可能找不到想要的内容。


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

为什么要使用Kotlin 对比 Java,Kotlin简介

什么是Kotlin 打开Kotlin编程语言的官网,里面大大的写着, A modern programming languagethat makes developers happier. 是一门让程序员写代码时更有幸福感的现代语言 Kotlin语法...
继续阅读 »

什么是Kotlin


打开Kotlin编程语言的官网,里面大大的写着,



A modern programming languagethat makes developers happier.




是一门让程序员写代码时更有幸福感的现代语言




  • Kotlin语法糖非常多,可以写出更为简洁的代码,便于阅读。

  • Kotlin提供了空安全的支持,可以的让程序更为稳定。

  • Kotlin提供了协程支持,让异步任务处理起来更为方便。

  • Google:Kotlin-first,优先支持kotlin,使用kotlin可以使用更多轮子


接下来对比Java举一些例子。


简洁


当定义一个网络请求的数据类时


Java

public class JPerson {
private String name;
private int age;
//getter
//setter
//hashcode
//copy
//equals
//toString
}

Kotlin

data class KPerson(val name: String,val age: Int)

这里用的是Kotlin 的data class 在class 前面加上data 修饰后,kotlin会自动为我们生成上述Java类注释掉的部分


当我们想从List中筛掉某些我们不想要的元素时


Java

List<Integer> list = new ArrayList<>();  

List<Integer> result = new ArrayList<>();
for (Integer integer : list) {
if (integer > 0) { //只要值>0的
result.add(integer);
}
}

System.out.println(result);

Kotlin

val list: List<Int> = ArrayList()

println(list.filter { it > 0 })

如上代码,都能达到筛选List中 值>0 的元素的效果。


这里的filter是Kotlin提供的一个拓展函数,拓展函数顾名思义就是拓展原来类中没有的函数,当然我们也可以自定义自己的拓展函数。


当我们想写一个单例类时


Java

public class PersonInJava {
public static String name = "Jayce";
public static int age = 10;

private PersonInJava() {
}
private static PersonInJava instance;
static {
instance = new PersonInJava();
}
public static PersonInJava getInstance() {
return instance;
}
}

Kotlin

object PersonInKotlin {
val name: String = "Jayce"
val age: Int = 10
}

是的,只需要把class换成object就可以了,两者的效果一样。


还有很多很多,就不一一举例了,接下来看看安全。


安全


空安全

var name: String = "Jayce" //name的定义是一个非空的String
name = null //将name赋值为null,IDE会报错,编译不能通过,因为name是非空的String

var name: String? = "Jayce" //String后面接"?"说明是一个可空的String
name.length //直接使用会报错,需要提前判空
//(当然,Kotlin为我们提供了很多语法糖,我们可以很方便的进行判空)

类型转换安全

fun gotoSleep(obj: Any) {
if (obj is PersonInKotlin) {//判断obj是不是PersonInKotlin
obj.sleep() // 在if的obj已经被认为是PersonInKotlin类型,所以可以直接调用他的函数,调用前不需要类型转换
}
}

协程


这里只是简单的举个例子


Kotlin的协程不是传统意义上那个可以提高并发性能的协程序


官方的对其定义是这样的



  • 协程是一种并发设计模式,您可以在Android平台上使用它来简化异步执行的代码

  • 程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。


当我们用Java请求网络数据时,一般是这么写的。

 getPerson(new Callback<Person>() {//这里有一个回调            
@Override
public void success(Person person) {
runOnUiThread(new Runnable() { //切换线程
@Override
public void run() {
updateUi(person)
}
})
}

@Override
public void failure(Exception e) {
...
}
});

Kotlin协程后我们只需要这么写

 CoroutineScope(Dispatchers.Main).launch { //启动一个协程
val person = withContext(Dispatchers.IO) {//切换IO线程
getPerson() //请求网络
}
updateUi(person)//主线程更新UI
}

他们两个都干的同一件事,最明显的区别就是,代码更为简洁了,如果在回调里面套回调的话回更加明显,用Java的传统写法就会造成人们所说的CallBack Hell。


除此之外协程还有如下优点



  • 轻量

  • 更少的内存泄漏

  • 内置取消操作

  • 集成了Jatpack


这里就不继续深入了,有兴趣的同学可以参考其他文章。


Kotlin-first


在Google I/O 2019的时候,谷歌已经宣布Kotlin-first,建议Android开发将Kotlin作为第一开发语言。


为什么呢,总结就是因为Kotlin简洁、安全、兼容Java、还有协程。


至于有没有其他原因,我也不知道。(手动狗头)


Google将为更多的投入到Kotlin中来,比如




  • 为Kotlin提供特定的APIs (KTX, 携程, 等)




  • 提供Kotlin的线上练习




  • 示例代码优先支持Kotlin




  • Jetpack Compose,这个是用Kotlin开发的,没得选。。。。。




  • 跨平台开发,用Kotlin实现跨平台开发。






好的Kotlin就先介绍到这里,感兴趣的同学就快学起来吧~
接下来在其他文章会对Kotlin和携程进行详细的介绍。


df7da83ea7648fdeb9963e9552ba2425.gif


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

Android 文件上传(包括大文件上传)

1.简介: android 文件上传可以分为两类:一个是小文件,直接上传文件;一个是大文件,这个需要分块上传。Okhttp+Retrofit实现文件上传。 2. 需要的依赖和权限: implementation 'com.squareup.retrofi...
继续阅读 »

1.简介:


android 文件上传可以分为两类:一个是小文件,直接上传文件;一个是大文件,这个需要分块上传。Okhttp+Retrofit实现文件上传。


2. 需要的依赖和权限:

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
    <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

3.示例:


3.1.小文件上传:直接上传文件(图片上传为例)

public class UpLoadImageUtils {
private static final String TAG = "UpLoadImageUtils";
//需要上传的图片数量
private static int imgSum;
//上传成功的图片数量
private static int uploadSuccessNum;
private static String enRttId;
//失败数量
private static int errorNum;

private static TestService apiService;


public static void getService(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
apiService = retrofit.create(TestService.class);
}

@SuppressLint("CheckResult")
public static void uploadImage(String url, File file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
Map<String, RequestBody> map = new HashMap<>();

//"image/png" 是内容类型,后台设置的类型

RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);

builder.addFormDataPart("name", file.getName());
builder.addFormDataPart("size", "" + file.length());
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = apiService.imgUpload(url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
Log.i(TAG, JsonUtil.jsonToString(baseResponse));
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
Log.i(TAG, "onComplete: ---" + uploadSuccessNum);
errorNum = 0;
uploadSuccessNum++;
if (imgSum == uploadSuccessNum) {
finishUpload(true);
uploadSuccessNum = 0;
}
} else {
if (errorNum < 4) {
uploadImage(url, file);
errorNum++;
}else {
finishUpload(false);
}
}
}, throwable -> {
Log.i(TAG, "onComplete: ---" + throwable.getMessage());
});
}


/**
* 上传
*
* @param compressFile 需要上传的文件
* @param urls 需要上传的文件地址
*/
public static void uploadList(List<String> urls, List<File> compressFile) {
getService();
//多张图片
imgSum = urls.size();
for (int i = 0; i < compressFile.size(); i++) {
uploadImage(urls.get(i), compressFile.get(i));
}
}


public interface TestService {
@POST()
Observable<BaseResponse> imgUpload(@Url String url, @Body MultipartBody multipartBody);
}
}

public class UpLoadImageUtils {
private static final String TAG = "UpLoadImageUtils";
//需要上传的图片数量
private static int imgSum;
//上传成功的图片数量
private static int uploadSuccessNum;
private static String enRttId;
//失败数量
private static int errorNum;

private static TestService apiService;


public static void getService(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
apiService = retrofit.create(TestService.class);
}

@SuppressLint("CheckResult")
public static void uploadImage(String url, File file) {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
Map<String, RequestBody> map = new HashMap<>();

//"image/png" 是内容类型,后台设置的类型

RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);

builder.addFormDataPart("name", file.getName());
builder.addFormDataPart("size", "" + file.length());
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = apiService.imgUpload(url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
Log.i(TAG, JsonUtil.jsonToString(baseResponse));
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
Log.i(TAG, "onComplete: ---" + uploadSuccessNum);
errorNum = 0;
uploadSuccessNum++;
if (imgSum == uploadSuccessNum) {
finishUpload(true);
uploadSuccessNum = 0;
}
} else {
if (errorNum < 4) {
uploadImage(url, file);
errorNum++;
}else {
finishUpload(false);
}
}
}, throwable -> {
Log.i(TAG, "onComplete: ---" + throwable.getMessage());
});
}


/**
* 上传
*
* @param compressFile 需要上传的文件
* @param urls 需要上传的文件地址
*/
public static void uploadList(List<String> urls, List<File> compressFile) {
getService();
//多张图片
imgSum = urls.size();
for (int i = 0; i < compressFile.size(); i++) {
uploadImage(urls.get(i), compressFile.get(i));
}
}


public interface TestService {
@POST()
Observable<BaseResponse> imgUpload(@Url String url, @Body MultipartBody multipartBody);
}
}


3.2.大文件分块上传(视频上传为例)同步

public class UploadMediaFileUtils {

private static final String TAG = "UploadMediaFileUtils";
private static UploadService uploadService;
//基础的裁剪大小20m
private static final long baseCuttingSize = 20 * 1024 * 1024;
//总的块数
private static int sumBlock;
//取消上传
private static boolean isCancel = false;
//是否在上传中
private static boolean isUploadCenter=false;

public static void uploadMediaFile(String url, String uploadName, File file, String appInfo, IOUploadAudioListener ioResultListener) {
if (file.exists()) {
getService();
//总的分块数
sumBlock = (int) (file.length() / baseCuttingSize);
if (file.length() % baseCuttingSize != 0) {
sumBlock = sumBlock + 1;
}
isCancel = false;
isUploadCenter = true;
uploadMedia(url, uploadName, file, appInfo, 1, ioResultListener);
} else {
Log.i(TAG, "文件不存在");
ioResultListener.errorResult("-1", "文件不存在");
}
}

@SuppressLint("CheckResult")
public static void uploadMedia(String url, String uploadName, File file, String appInfo, int currentBlock, IOUploadAudioListener ioResultListener) {
if (isCancel){
Log.i(TAG, "取消上传");
return;
}

byte[] fileStream = cutFile(file, currentBlock - 1, ioResultListener);
if (fileStream == null) {
Log.i(TAG, "uploadMedia: getBlock error");
ioResultListener.errorResult("-1", "fileStream为空");
return;
}

MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);//表单类型
RequestBody requestBody = RequestBody.create(MultipartBody.FORM, fileStream);
builder.addFormDataPart("name", uploadName);
builder.addFormDataPart("size", "" + fileStream.length);
Log.i(TAG, "size" + fileStream.length);
builder.addFormDataPart("num", "" + currentBlock);
/*
* 这里重点注意:
* com_img[]里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/

builder.addFormDataPart("file", file.getName(), requestBody);
MultipartBody body = builder.build();
Observable<BaseResponse> meSetIconObservable = uploadService.mediaUpload("huizhan", appInfo, url, body);

meSetIconObservable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseResponse -> {
if ("000000".equalsIgnoreCase(baseResponse.getCode())) {
double progress = 100 * div(currentBlock, sumBlock, 2);
ioResultListener.progress("" + progress);
if (currentBlock < sumBlock) {
uploadMedia(url, uploadName, file, appInfo, currentBlock + 1, ioResultListener);
return;
}
ioResultListener.successResult(baseResponse);
} else {
ioResultListener.errorResult(baseResponse.getCode(), baseResponse.getDesc());
}
}, throwable -> {
ioResultListener.errorResult("-1", "上传失败");
});
}

public static void getService() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
uploadService = retrofit.create(UploadService.class);
}


public interface UploadService {
@POST()
Observable<BaseResponse> mediaUpload(@Header("X-Biz-Id") String bizId,
@Header("X-App-Info") String AppInfo,
@Url String url,
@Body MultipartBody multipartBody);
}



/**
* 写入本地(测试用)
*
* @param list
*/
public static void writeFile(List<byte[]> list) {
FileWriter file1 = new FileWriter();
String path = Environment.getExternalStorageDirectory() + File.separator + "12345.wav";
try {
file1.open(path);
for (int i = 0; i < list.size(); i++) {
Log.i(TAG, "writeFile: " + list.get(i).length);
file1.writeBytes(list.get(i), 0, list.get(i).length);
}
file1.close();
LogUtils.i(TAG, "writeFile: ");
} catch (Exception e) {
e.printStackTrace();
}
}


public static byte[] cutFile(File file, int currentBlock, IOUploadAudioListener ioResultListener) {
Log.i(TAG, "getBlockThree:000000---" + currentBlock);
int size = 20 * 1024 * 1024;
byte[] endResult = null;
byte[] result = new byte[size];
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
accessFile.seek(currentBlock * size);
//判断是否整除
if (file.length() % baseCuttingSize != 0) {
//当前的位数和总数是否相等(是不是最后一段)
if ((currentBlock + 1) != sumBlock) {
int len = accessFile.read(result);
out.write(result, 0, len);
endResult = out.toByteArray();
} else {
//当有余数时
//当前位置2147483647-20971520
byte[] bytes = new byte[(int) (file.length() % baseCuttingSize)];
int len = accessFile.read(bytes);
out.write(bytes, 0, len);
endResult = out.toByteArray();
}
} else {
int len = accessFile.read(result);
out.write(result, 0, len);
endResult = out.toByteArray();
}
accessFile.close();
out.close();
} catch (IOException e) {
// e.printStackTrace();
ioResultListener.errorResult("-1", "cutFile失败");
}
return endResult;
}

/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入。
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 取消上传
*/
public static void cancelUpload() {
if (isUploadCenter) {
isCancel = true;
}
}

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

关于 Android 稳定性优化你应该了解的知识点

前言 Android 稳定性优化是一个需要长期投入,持续运营和维护的一个过程,不仅深入探讨了 Java Crash、Native Crash 和 ANR 的解决流程及方案,还分析了其内部实现原理和监控流程。本文对稳定性优化方面的知识做了一个全面总结,主要内容如...
继续阅读 »

前言


Android 稳定性优化是一个需要长期投入,持续运营和维护的一个过程,不仅深入探讨了 Java Crash、Native Crash 和 ANR 的解决流程及方案,还分析了其内部实现原理和监控流程。本文对稳定性优化方面的知识做了一个全面总结,主要内容如下:


image.png


如何提升App的稳定性


一般性的App能接触到稳定性的需求其实并不多,只有大型的处于稳定运营期的App才会重视App的稳定性,稳定性实际上是一个大问题,一个稳定的产品才能够保证用户的留存率,所以稳定性是质量体系中最基本也是最关键的一环:



  • 稳定性是大问题,Crash是P0优先级:对于用户来说很难容忍你的应用发生崩溃

  • 稳定性可优化的面很广:不仅仅是指崩溃,像卡顿、耗电等也属于稳定性优化的范畴,对于移动端高可用这个标准来说,性能优化只是高可用的一部分,还有一部分就是应用业务流程功能上的可用


稳定性维度



  • Crash维度:一般会将Crash单独作为一项重要指标进行突破,最常见的统计指标就是Crash率,后面会说到

  • 性能维度:启动速度、内存、卡顿、流量、电量等等,在解决应用的Crash之后,就应该着手保障性能体系的稳定

  • 业务高可用维度:业务层面的高可用是相当关键的一步,需要使用多种手段去保障App业务的主流程及核心路径的可用性


稳定性优化概述


如果App到了线上才发现异常,其实已经造成了损失,所以稳定性优化重点在于预防



  • 重在预防、监控必不可少:从开发到测试到发布上线运维这些各个阶段都需要预防异常的发生,或者说要将发生异常造成的损失降到最低,用最小的代价暴露最多的问题,同时监控也是必不可少的一步,需要拥有一定的监控手段来更加灵敏的发现问题

  • 思考更深一层、重视隐含信息:比如你发现了一个崩溃,但是你不能简单的只看这一个崩溃,要考虑这个崩溃是不是在其他地方也有同样或者类似的,如果有就考虑是否统一处理,今后该如何预防,总结经验

  • 长效保持需要科学流程:在项目的每一个阶段建立完善的相关规范,保证长效的优化效果


如何有效降低应用崩溃率


Crash相关指标


1.UV、PV Crash率



  • UV Crash率:等于Crash UV/DAU:主要针对于用户使用量的统计,它统计一段时间内所有用户中发生过崩溃的用户占比,和UV强相关,UV是指Unique Visitor一天内访问网站的人数(是以cookie为依据),一天内同一访客的多次访问只计算为1,一台电脑不同的浏览器的cookie值不同。

  • PV Crash率:针对用户使用频率的统计,统计一段时间内所有用户启动次数中发生崩溃的占比,和PV强相关,PV是指PageView也就是页面点击量,每次刷新就算一次浏览,多次打开同一页面会累加。

  • UV Crash方便评估用户影响范围,PV Crash方便评估相关Crash的影响严重程度

  • 注意:沿用同一种衡量方式:不管你是使用UVCrash还是PVCrash作为主要指标,你应该一直使用它,因为和Crash率相关的会有一些经验值,这些经验值需要对应一个衡量指标


2.Java、Native Crash率



  • Java Crash:在Java代码中,出现了未捕获的异常,导致程序异常退出

  • Native Crash:一般都是因为在Native代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动abort,这些都会产生相应的signal信号,导致程序异常退出。目前Native崩溃中最成熟的方案是BreakPad


3.启动、重点流程Crash率



  • 启动Crash率:在启动阶段用户还没有完全打开就发生了崩溃的占比,只要是用户打开App10s之内发生的崩溃都被视为启动Crash,它是Crash分类中最严重的一类,我们需要重点关注这个指标,而且降低的越低越好,并且我们应该结合客户端的容灾策略进行自主修复


4.增量、存量Crash率



  • 增量Crash:指新增Crash,它是新版本Crash率变动的原因,如果没有新增的Crash,那么新版本的Crash率应该是和老版本Crash率保持一致,所以增量Crash是新版本中需要重点解决的问题

  • 存量Crash:指老版本中已经存在的Crash,这些Crash一般都是难以解决或者是需要在特定场景下才会出现的难以复现的问题,这类问题需要长期投入精力持续解决

  • 优先解决增量、持续跟进存量


5.Crash率评价指标



  • 务必在千分之二以下:Java和Native的崩溃率加起来需要在千分之二以下才能算是合格的

  • Crash率处于万分位视为优秀的标准


Crash关键问题


1.尽可能还原Crash现场


一旦发生崩溃,我们需要尽可能保留崩溃现场信息,这样有利于还原崩溃发生时的各种场景信息,从而推断出可能导致崩溃的原因,对于采集环节可以参考以下采集点:



  • 采集堆栈、用户设备、OS版本、发生崩溃的进程、线程名、崩溃前后的Logcat

  • 前后台、使用时长、App版本、小版本、渠道

  • CPU架构、内存信息、线程数、资源包信息、行为日志


image.png


上面是一张Bugly后台的截图,对于成熟的性能监控平台不仅有Crash的单独信息,同时会对各种Crash进行聚合以及报警。


2.APM后台聚合展示



  • Crash现场信息:包括Crash具体堆栈信息及其它额外信息

  • Crash Top机型、OS版本、分布版本、发生地域:有了这些Top Crash信息之后就能够知道哪些Crash的影响范围比较大需要重点关注

  • Crash起始版本、上报趋势、是否新增、持续版本、发生量级等等:可以从多个视角判断Crash发生的可能原因以及决定是否需要修复,在哪些版本上进行修复


3.Crash相关的整体架构


image.png


4.非技术相关的关键问题


建立规范的流程保证开发人员能够及时处理线上发生的问题:



  • 专项小组轮值:成立专门小组来跟踪每个版本周期之内线上产生的Crash,保证一定有人跟进处理

  • 自动分配匹配:可以自定义某些业务模块名称,自动分配给相应人员处理

  • 处理流程全纪录:记录相应人员处理Crash的每一步,后期出现问题追究相关责任人也是有据可查的


单个Crash处理方案



  • 根据堆栈及现场信息找答案:一般来说堆栈信息可以帮助我们解决90%的问题

  • 找共性比如机型、OS、实验开关、资源包等:有些Crash信息通过堆栈找不到有用的帮助,不能直接解决,这种情况下可以通过Crash发生时的各种现场信息作辅助判断,分析这些Crash用户拥有哪些共性

  • 线下复现、远程调试:有了共性之后尝试在线下进行复现,或者尝试能否进行远程调试


Crash率治理方案



  • 解决线上常规Crash:抽出一定时间来专门解决所有常规的Crash,这些Crash一般相对来说比较容易解决

  • 系统级Crash尝试Hook绕过:当然Android系统版本一直在不断的升级,随着新系统的覆盖率越来越高,老版本的系统Bug可能会逐渐减少

  • 疑难Crash重点突破、更换方案:做到长期跟踪,团队合作重点突破,实在不行可以考虑更换实现方案


通过以上几点应该可以解决大部分的存量Crash,同时再控制好新增Crash,这样一来整体的Crash率一般都能够得到有效降低。


这一部分的内容有点杂而多,一般也是需要多端配合,所以不太好做具体演示,大家可以在网上多查找相关资料进行巩固学习。


如何选择合适的崩溃服务



  1. 腾讯Bugly: 除了有crash数据还有运营数据

  2. UC 啄木鸟:可以捕获Java、Native异常,被系统强杀的异常,ANR,Low Memory Killer、killProcess。技术深度以及捕获能力强

  3. 网易云捕:继承便捷,访问快,捕获以及上报速度及时,支持实时报警,提供多种报警选项,可以自定义参数。

  4. Google的Firebase

  5. crashlytics:服务器在国外,访问速度慢,会丢掉数据

  6. 友盟:crash之后会在再次启动的时候上报数据,所以不能立即获得这部分信息


移动端业务高可用方案


移动端高可用方案不仅仅是指性能方面的高可用,业务方面的高可用也是尤为重要的,如果业务都走不通,试问你性能做的再好又有何用呢?



  • 高可用:性能+业务

  • 业务高可用侧重于用户功能完整可用

  • 业务高可用影响公司实际收入:比如支付流程不通


对于业务是否可用不像Crash一样,如果发生Crash我们可以收到系统的回调,业务不可用实际上我们是无从知道的,所以针对建设移动端业务高可用的方案总结以下几点:


1.数据采集



  • 梳理项目主流程、核心路径、关键节点:一般需要对项目主流程和核心路径做埋点监控,比如用户下单需要从列表页到详情页再到下单页,这就是一个核心路径,我们可以监控具体每个页面的到达率和下单成功率

  • AOP自动采集、统一上报:数据采集的时候可以采用AOP的方式,减少接入成本,上报的时候可以采取统一的上报减少流量和电量消耗,上传到后台之后再做详细的分析,得出所有业务流程的转化率,以及相应界面的异常率


2.报警策略



  • 阈值报警:比如某个业务失败的次数超过了阈值就报警通知

  • 趋势报警:对比前一天的异常情况,如果增加的趋势超过了一定的比例即便是未达阈值也要触发报警

  • 特定指标报警、直接上报:比如支付SDK调用失败,这种错误无需跟着统一的数据上报,出现立即上报


3.异常监控



  • Catch代码块:实际开发中我们为了避免程序崩溃,经常会写一些try{}catch(){}来捕获相关异常,但是这样操作完成之后,程序确实不崩溃了,相应的功能也是无法使用的,所以这些被Catch住的异常也要上报,有利于分析功能不可用的原因

  • 异常逻辑:比如我们需要对结果为true的调用方法进行处理,结果为false时不执行任务,但是我们也需要上报异常,统计一下出现这种情况的用户的占比情况,以便针对性的优化


这里简单的举个栗子,表明意思:

        try {
//业务处理
LogUtils.i("...");
}catch (Exception e){
//如果未加上统计,就无法知道具体是什么原因导致的功能不可用
ExceptionMonitor.monitor(Log.getStackTraceString(e));
}

boolean flag = true;
if (flag){
//正常,继续执行相关任务
}else {
//异常,不执行任务,这种情况产生的异常也应该进行上报
ExceptionMonitor.monitor("自定义业务失败标识");
}

4.单点追查



  • 需要针对性分析的特定问题:这些问题相对小众,很可能是和特定用户的操作习惯、账户体系相关,所以要尽可能获取多的数据重点分析

  • 全量日志回捞,专项分析:很多日志信息默认都是只记录不上传,比如用户全部的行为日志,这些日志只有在需要的时候才有用,平时没必要上传,没啥用还浪费流量,如果需要排查特定用户的详细信息,也可以通过服务端下发指令客户端接收指令后上传


5.兜底策略


当你通过监控了解到业务不正常之后,请问该如何修复?这里就要用到兜底策略了,就是到了最后一步各种措施都做了,用户还是出现了异常,这种情况仍然还是要有相关联的配置手段来达到高可用。对于业务上的异常除了热修复的手段之外,还可以通过建立配置中心,将功能开关关闭。



  • 配置中心,功能开关:实际项目中很多数据都是通过服务端动态下发配置的,将这些功能集合起来的处理平台就是配置中心。举个栗子:比如新版本上线了一个新功能,加了一个入口,上线之后发现功能不稳定,此时就可以通过服务端配置的方式将此功能开关关闭,这样即使用户无法使用新功能,但是至少不会发现业务的异常

  • 跳转分发中心:熟悉组件化开发的朋友都知道做组件化module的拆分必不可少的就是要有一个路由,它的作用就是跳转分发中心,所有的跳转都是通过路由来做,如果匹配到需要跳转到有Bug的功能界面时可以统一跳转到一个异常处理的页面


移动端容灾方案


移动端容灾必要性


说到容灾,首先来看一下需要防范的灾是什么?主要分为两部分:性能异常和业务异常,只要是对用户的实际体验产生了很大的影响,都是需要防范的App线上灾害。



  • 灾:性能、业务异常


传统的流程是如何处理线上出现的紧急问题的呢?传统的处理流程首先需要用户反馈出现的不正常情况,接着开发人员进行紧急的BUG修复,然后重新打包上传渠道进行更新,可见传统的流程比较繁琐,灵敏度较低,如果日活量较高,随着Bug在线上存活的时间延长对用户基数的影响是巨大的,势必是无法接受的



  • 传统流程:用户反馈、重新打包、渠道更新,不可接受


移动端容灾最佳实践


1.功能开关



  • 配置中心,服务端下发配置控制:首先对任何新上线的功能加上功能开关,可以通过配置中心的方式下发开关决定是否显示新功能的入口,出现异常情况可以随时关闭入口,这样可以保证上线的新功能处于可控状态

  • 针对场景,功能新加或代码改动:一是新增了功能,二是出现了代码改动,比如重构代码,最好保留之前的老方案,在新方案上线之后如果有问题,可以切回之前的老方案


这里简单的做个演示:

public class ConfigManager {

public static boolean mOpenClick = true; //默认值为true

}

mAdapter.setOnItemClickListener((view, position) -> {
//控制点击事件是否开启
if (ConfigManager.mOpenClick){ //mOpenClick的值从接口获取,由服务端控制
//处理具体业务
}
});

2.统跳中心


组件化之后的项目的页面跳转都是通过路由来做的,如果发现线上产生了异常,可以在路由跳转这一步拦截有Bug的跳转,重定向到备用方案,或者统一的错误处理中界面,更多的情况是为了对线上用户产生的影响降到最低,如果有Bug不能进行热修复,也没有合适的开关可用,会做一个临时的H5页面,让用户点击之后跳转到临时的H5页面,这样用户还是可以操作,只是在体验上稍微差一点,总归来说比不能用强的多



  • 界面切换通过路由,路由决定是否重定向

  • Native Bug不能热修则跳转到临时H5


3.动态化修复


目前为止,国内市场安卓的热修复方案已经比较成熟了,对于大型项目来说,一般都会支持热修复的能力,热修复技术就是用户不需要重新安装一个Apk,就可以实现比原有Apk有较大更新的能力,比如微信的Tinker和美团的Robust都是非常好的热修复实现方案。需要注意的是,热修复也只是一个功能,对于热修复也需要加上各种完善的统计,需要知道热修方案是否真正有效果,没有用造成更大的损失



  • 热修复能力,可监控、灰度、回滚、清除

  • 推拉结合、多场景调用保证到达率

  • Weex、RN增量更新


4.安全模式


安全模式侧重于移动端发生严重Crash时的自动恢复,做的好的安全模式往往会有几级不同的策略,比如App多次启动失败,那就重置整个App到安装的状态,避免因为一些脏数据导致的App持续闪退,同时如果有Bug并且非常严重到了最严重的等级,可以采用阻塞性热修来解决,即:必须等热修成功之后才可进入主页面。需要注意的是,安全模式不仅仅可以针对App Crash,也可以针对一些组件,比如网络请求多次失败后也可以进入安全模式,暂时拒绝用户的网络请求,避免给服务端造成的额外压力



  • 根据Crash信息自动恢复,多次启动失败重置App

  • 严重Bug可阻塞性热修复

  • 异常熔断:多次请求失败则主动拒绝


容灾方案总结:


image.png


这几种方式是由简单到复杂的递进,为了保障线上的稳定性,最好在应用中多加入几个稳定性保障方案。


稳定性长效治理


对于稳定性优化来说是一个细活,需要打持久战,不能一个版本优化了,后面又恶化了,因此需要在项目开发的整个周期内的不同阶段都加上相应的方案。


1.开发阶段


在开发阶段组内每个开发人员的编码实力都是不一样的,因此需要统一编码规范,然后结合一些手段增强人员的编码功底,尽可能的将问题消灭在编码阶段,尽可能的写出高质量的代码,同时要结合开发阶段的技术评审,以及每天的互相CodeReview机制,坚持几个月编码水平肯定会有明显的提升,开发阶段明显的问题应该就不会再有了,而且代码风格结构也会大体一致。同时开发阶段还需要做的事情就是架构优化,项目的架构应该根据项目的不同发展阶段来不断优化,这里说两点,第一能力收敛比如界面切换的能力用路由来实现,对网络请求要统一网络库统一使用方式,这样可以避免不正当的使用带来的Bug,第二统一容错,比如对于网络请求来说可以在网络请求回来的时候加上预先校验,判断回来的数据是否合法,如果不合法就不需要再把数据转给上层业务了



  • 统一编码规范、增强编码功底、技术评审、CodeReview机制

  • 架构优化:能力收敛、统一容错


2.测试阶段



  • 功能测试、自动化测试、回归测试、覆盖安装

  • 特殊场景、机型等边界测试

  • 云测平台:辅助测试,满足对特殊机型的测试需求


3.合码阶段


开发时肯定是在自己的分支进行开发,测试通过之后才会往主干分支合入,合入之前首先需要进行代码的编译检查和静态扫描发现可能存在的问题,经过校验之后也不能直接合入,应该将自己的分支首先合入到一个和主干分支一样的分支中进行预编译,编译通过之后最好加上主流程的回归测试



  • 编译检测,静态扫描

  • 预编译流程、主流程自动回归


4.发布阶段


到了发布阶段一般来说App都是经过了开发自测、QA测试、内部测试等测试环节,相对来说比较稳定了,但是需要注意的是,很多问题你不可能全部测出来,所以必须谨慎对待



  • 多轮灰度:灰度的量级要由小变多,争取以最小的代价暴露最多的问题

  • 分场景、维度全面覆盖


5.运维阶段


任何一个小问题在海量用户面前都会影响巨大,因此这个阶段必须要依靠APM的灵敏监控



  • APM灵敏监控

  • 回滚、降级策略

  • 热修复、容灾方案

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

SharedPreferences的一种极简优雅且安全的用法

针对Android平台键值对的持久化存储,虽然Jetpack出了新的DataStore,但实际项目中SharedPreferences还是有大量使用,本文结合以前的使用经验给出一种极简且优雅且安全的实践。(示例项目见 gitee.com/spectre1225...
继续阅读 »

针对Android平台键值对的持久化存储,虽然Jetpack出了新的DataStore,但实际项目中SharedPreferences还是有大量使用,本文结合以前的使用经验给出一种极简且优雅且安全的实践。(示例项目见 gitee.com/spectre1225…


1. SharedPreferences的使用与改进


SharedPreferences的基本读写代码如下:

preferences.edit().putInt("intKey", 1).apply();//写
preferences.getInt("intKey", 0);//读,0为默认值

代码中直接这么用的话,键会很不好管理,不清楚一个键值对到底有多少地方使用,当键发生改变需要修改的时候,也容易遗漏。于是就有了以下改进:

public interface XXXConfig{
String KEY_PROPERTY_AA = "key_aa";
String KEY_PROPERTY_BB = "key_bb";
String KEY_PROPERTY_CC = "key_cc";
//more key.......
}

//使用的地方
preferences.edit().putInt(XXXConfig.KEY_PROPERTY_AA, 1).apply();//写
preferences.getInt(XXXConfig.KEY_PROPERTY_AA, 0);//读,0为默认值

但这样写仍然有问题,就是缺少值类型的约束:一个key对应的value,可能有很多种类型。这种情况下,需要额外的注释或文档来记录每一个key对应的value的类型信息。于是,有人想到可以像JavaBean一样,采用getter和setter方法的形式:

public class XXXConfig {
private SharedPreferences preferences;
private String KEY_PROPERTY_AA = "key_aa";
private String KEY_PROPERTY_BB = "key_bb";

//中间省略初始化......

public int getPropertyAA() {
return preferences.getInt(KEY_PROPERTY_AA, 0);
}

public void setPropertyAA(int value) {
preferences.edit().putInt(KEY_PROPERTY_AA, value);
}

public int getPropertyBB() {
return preferences.getInt(KEY_PROPERTY_BB, 0);
}

public void setPropertyBB(int value) {
preferences.edit().putInt(KEY_PROPERTY_BB, value);
}
}

这种写法改进了类型安全,但每次新增就需要写一个属性和两个方法,过程比较繁琐。理想情况,我还是希望像写文档一样只需要写下面这样的信息:

属性1:类型 int
属性2:类型 String

然后使用的地方可以直接取值。因此,就有了下面介绍的新的封装方法:暂且称为NeoPreference。


2. NeoPreference简单使用


首先,我们需要需要创建一个inferface来继承Config接口,这个新的接口对应一个SharedPreferences,默认接口名即为SharedPreferences的名称,例如:

public interface DemoConfig extends Config {

}

这里的DemoConfig即为SharedPreferences的名称。有时候我们想要自己另外指定名称,则可以使用Config.Name注解:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {

}

这个时候SharedPreferences名称就是my_demo_config


然后我们就可以通过ConfigManager来获取DemoConfig的实例:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);

到目前为止还没有什么新鲜的,接下来我们往里面添加新的配置项/属性:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
Property<Integer> versionCode();
}

在上述基础上,只需要添加一行代码,就添加了新的键值对:key的值为versionCode,value的类型为Integer。然后我们的读写代码可以这么写:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
Integer versionCode = config.versionCode().get();//读
config.versionCode().set(versionCode + 1);//写

如果我们想要单独定key的名字,我们可以使用对应属性的注解:

@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code")
Property<Integer> versionCode();
}

我们还可以指定值的范围和默认值:


@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code", start = 1, to = 10000, defaultValue = 1)
Property<Integer> versionCode();
}

这样,在值不符合规范的时候会抛出异常:

DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
config.versionCode().set(-1);//throw exeception

3. NeoPreference API说明


这个工具的API除了ConfigManager类以外主要分两部分:Property类以及类型对应的注解。


3.1 ConfigManager接口说明


ConfigManager是单例实现,维护一个SharedPreferencesConfig的注册表,提供getConfigaddListener两个方法。


以下是getConfig方法签名:

public <P extends Config> P getConfig(Class<P> pClass);
public <P extends Config> P getConfig(Class<P> pClass, int mode);

参数pClass是继承Config类的接口class,可选参数mode对应SharedPreferences的mode。


addListener的方法监听指定preferenceName中内容的变化,签名如下:

public void addListener(String preferenceName, WeakReference<Listener> listenerRef);
public void addListener(LifecycleOwner lifecycleOwner, String preferenceName, Listener listener);

第一个方法接受一个Listener的弱引用,需要调用者自己持有监听器的引用,自己管理生命周期,否则可能被回收。第二个方法不采用弱引用参数,而是额外添加LifecycleOwner,这个监听器的声明周期采用LifecycleOwner对应的生命周期。


3.2 Property类接口说明


Property接口包括:

public final String getKey();//获取属性对应的key
public T get(T defValue); //获取属性值,defValue为默认值
public T get(); //获取属性值,采用缺省默认值
public void set(T value); //设置属性值
public Optional<T> opt(); //以Optional的形式返回属性值
public final void addListener(WeakReference<Listener<T>> listenerRef) //类似ConfigManager,不过只监听该属性的值变化
public final void addListener(LifecycleOwner owner, Listener<T> listener)//类似ConfigManager,不过只监听该属性的值变化

泛型参数支持LongIntegerFloatBooleanStringSet<String>SharedPreferences支持的几种类型,以及额外的Serializable


3.3 类型相关注解介绍


这些注解对应SharedPreferences支持的几种类型(其中description字段暂时不用)。

@interface StringItem {
String key() default "";
boolean supportEmpty() default true;
String[] valueOf() default {};
String defaultValue() default "";
String description() default "";
}

@interface BooleanItem {
String key() default "";
boolean defaultValue() default false;
String description() default "";
}

@interface IntItem {
String key() default "";
int defaultValue() default 0;
int start() default Integer.MIN_VALUE;
int to() default Integer.MAX_VALUE;
int[] valueOf() default {};
String description() default "";
}

@interface LongItem {
String key() default "";
long defaultValue() default 0;
long start() default Long.MIN_VALUE;
long to() default Long.MAX_VALUE;
long[] valueOf() default {};
String description() default "";
}

@interface FloatItem {
String key() default "";
float defaultValue() default 0;
float start() default -Float.MIN_VALUE;
float to() default Float.MAX_VALUE;
float[] valueOf() default {};
String description() default "";
}

@interface StringSetItem {
String key() default "";
String[] valueOf() default {};
String description() default "";
}

@interface SerializableItem {
String key() default "";
Class<?> type() default Object.class;
String description() default "";
}

4. 完整实现


见:gitee.com/spectre1225…


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

年轻人疯狂逃离的东方小巴黎|出路在哪里

不知不觉回到哈尔滨工作三年了,经历了两家公司,最近又燃起了换工作的心思。都说人挪活、树挪死,所以,我还想要活着,甚至获得更好一点,只能选择挪一挪,说句不好听的,这就有种矮子里拔大个的感觉。 概述 稍微熟悉哈尔滨的人都应该知道,哈尔滨是座没有互联网的城市。回...
继续阅读 »

不知不觉回到哈尔滨工作三年了,经历了两家公司,最近又燃起了换工作的心思。都说人挪活、树挪死,所以,我还想要活着,甚至获得更好一点,只能选择挪一挪,说句不好听的,这就有种矮子里拔大个的感觉。



概述


微信图片_20220607092204.jpg
稍微熟悉哈尔滨的人都应该知道,哈尔滨是座没有互联网的城市。回溯过去的十几甚至二十年,哈尔滨整体的软件行业基本处于停滞的状态。每年的各种互联网公司排行,独角兽公司排行甚至从没有出现过哈尔滨这三个字眼。被人们所熟知的BJTA等互联网巨头,他们甚至没有在哈尔滨这座城市扎根的丝毫想法。当然,我指的是研发团队,毕竟研发才是整个互联网公司的核心,他们支撑着公司各条业务线的运转。


你可能会说,没有大厂的城市多了去了。确实如此,但是哈尔滨作为最北方的省会城市,有着美丽的称号东方小巴黎,作为二线城市中排名靠前的城市,软件行业的发展确实是没有进步。最近几年,同为二线城市的成都武汉西安等城市的发展可谓是突飞猛进,一跃成为新一线城市,工作机会与工资水平大大提升。然而作为共和国长子中的省会城市,却在原地踏步,夸张点说,甚至是在退步!


城市表现


微信图片_20220607093006.jpg


随着2022年全国城市商业魅力排名的发布,我们清晰的看到,全国共有4个一线城市,15个新一线城市,而哈尔滨不出意外的出现在二线城市当中,且排名二线城市的第9位。更夸张的是,东北竟然无一城市入选新一线,去年杀入榜单的沈阳今年也铩羽而归了。


2020年时,全国千万级人口城市有18个,其中就有哈尔滨,到了2021年,哈尔滨居然跌出了这份榜单,全是常驻人口为988.5万,且全是人口增长率为-2%。那么你不禁要问,人口去哪了?为何会越来越少?


我愿意用恶劣这两个字来形容如今哈尔滨年轻人的工作环境。2022年哈尔滨工资全国排名第38位7160元,乍一看,这个水平不低了,确实,但是你真正了解就会发现,工资区间在3k~5k的人大有人在,他们同样是大学毕业,活力满满的年轻人,但是却没有机会获得一份高薪水的工作,甚至五险一金都是奢望。高收入人群虽然是少数,但是他们的工资却达到普通人的几倍,甚至十几倍。你会说,低收入的人还是没有能力,然而真的如此吗?


对比其他新一线城市,哈尔滨还是差了太多,我指的工作的机会。人确实有优秀的人,也有相对差一些的人,但是他们的层次也不至于相差十几倍这么夸张。但是在哈尔滨恰恰就是这样一种体现,年轻人甚至没有机会去展示自己的能力。


绝大多数的企业,高层都是在公司深耕多年的老人,他们的存在注定年轻人没有取代他们的机会,这并不是一个完全靠能力就能上位的城市。公司内部没有明确的晋升机制,没有规范的涨薪条例,更没有给你预留以后的位置。


软件行业的工资水平,远远低于行业的平均值。3~5年的程序员工资水平大约在7k-9k,5-10年的大约水平在11-15k左右。这种水平在一线甚至其他新一线城市已经是低薪了。毫不夸张的说,这还是不错的那些程序员能够拿到的薪水。


技术水平


微信图片_20220607092151.jpg
既然咱们是程序员,那就来聊聊哈尔滨的程序员水平如何。


2019年我刚回到哈尔滨,面试过几家公司,对于技术层面的面试基本都是皮毛,仅限于是否使用过哪些工具?用过哪些方法?如何写一个sql?


是否觉得现在面试还有如此不卷的公司?有,而且在哈尔滨90%都是这样的。虽说整体面试难度看起来不大,也很好通过,但是你觉得仅凭如此,能获得不菲的薪水?当然,基于这种环境,你就不可能获得高薪水。因为你的面试官根本也不懂更深层面的内容。如果一个饱受面试摧残的程序员来到哈尔滨面试,我相信你会发现一个新大陆。当你面试入职后,你甚至会不屑于与他们讨论技术原理性问题,因为他们根本不懂。


我作为一个后端开发,竟然遇到这样一个面试题:会用div画三角形吗?你能相信这是一个哈工大博士生,且工作10余年的java程序员能问出来的问题?


在目前的微服务发展形势下,当然也有不少公司开始使用这些技术,然而他们不是基于业务去拆分,而是基于功能,你能想象到的是,原本没有多少功能的系统,生生拆出十几甚至二十个服务,上线后每天的用户量,用一个手指就可以数的过来。当然,有部分程序员还是有理智的,不会认同你们leader的这种恶劣行为,但是你的反对不会有效果。


有一句话,我听得时候很不屑,现在想想,这就是现实!你学这些干啥?学了你也用不上。确实,很多东西学了你也用不上。说个简单的方面,哈尔滨企业的容器化,或者云化的程度微乎其微。且大部分功能在领导看来能用就可以。


长期在哈尔滨的程序员,经过这种环境熏陶,相当于浪费生命,真的好比坐井观天,他们如果出去其他新一线城市,相信必然会感叹程序员居然是这样的


新的机遇


微信图片_20220607092208.jpg
那么,哈尔滨软件行业到底有没有新的机遇?前一段时间相信哈尔滨人都知道的一个新闻,六大巨头宣布与哈尔滨达成深度合作,其中包含华为,百度,京东,腾旭,中兴,中科。初一听,这好像确实是对哈尔滨的好消息,这几个公司基本都是行业的领头羊,如果他们到来必定能够带来不少的就业岗位,提升一定的经济增长。


但是细细观察你会发现,最高兴的人无异于房地产行业,似乎踩到他们的尾巴一般,各种营销号,短视频不断的四处宣扬。但也不得不说,疫情导致哈尔滨的商业难以开展,所有的行业都停滞了,他们也像找到了救命稻草。


但是我却并不很看好这次机会,从本质来说,这六个企业能来,还是黑龙江政府给到了足够的吸引力,提供了强大的政策支持,不然我不认为会有企业发善心一样的去带动你你们的经济发展。


另外,本次六个企业的到来,至少我不认为是软件从业人员的机遇,研发团队无论从质量,还是数量,相比于其他城市,哈尔滨还是有很大的不足的。即使有,我觉得也只是外包团队,而不会是核心研发团队。


在从目前公布的几个公司的业务方先来看:华为在算力中心、人工智能、人才培养,百度在“一基地、三中心、七平台”,京东在“一店、一基地、一底座、一战略、三平台”,腾讯在数字转型、生物医药、创意设计,中兴在5G产业链,中科在咨询智库、数智制造业等方面提出落地诉求。在我看来特别像是老板在给我们画大饼。


如何找工作


微信图片_20220607093003.jpg
不管以后如何发展,现在还是要找一分稳定的工作。作为程序员,我还是总结一下几个重要的方面:


1、技术方面,即使处于哈尔滨这种躺平的环境,还是要不断的学习的。深度与广度同样重要,可以让你在工作当中得心应手,有更多的解决方案,也就意味着有更多被领导关注的机会。平时的沟通中,能更多的体现你的技术能力,将会使你在领导的印象里增加重量。同时在面试时,你技术能力的体现,还是有助于获得相对高一些的薪水。


2、工作环境。目前很多企业克扣员工较为严重,名义上双休,实际单休;加班、出差严重,且没有加班费,调休等机制;五险一金按照最低工资标准缴纳,设置有些工资没有一金;领导PUA严重,公司氛围沉闷,逢场作戏,拍领导马屁层出不穷。选择的时候慎重考虑。


3、工作距离。目前哈尔滨的地铁建设、覆盖程度不高,虽然公交线路较多,但是堵车较严重。大部分公司聚集在香坊开发区,松北等较远地带,没有地铁,但是部分公司带有通勤车,考虑目前的油价和你的薪水,建议慎重考虑出行方式。


4、薪水提升。在哈尔滨,想要薪水double,基本没有可能,涨个三五千就算不错的,如果有这种机会,且考虑我前面分析的三个方向,基本符合你的设想,别犹豫。虽然有可能是坑,但也是能拿到更多钱的坑。


总结


工作之余,闲聊而已。希望给在哈尔滨的同仁们,和即将回来的同仁们一点参考。虽然咱们哈尔滨环境不好,但是在吃这方面还是顶呱呱的。如果您有幸进入国企,或者拿到公务员的铁饭碗,作为养老城市还是不错的选择。也希望咱们哈尔滨能够越来越好吧,不要让年轻人无奈的离开。


微信图片_20220607092142.jpg

收起阅读 »

Springboot如何优雅的进行数据校验

基于 Spring Boot ,如何“优雅”的进行数据校验呢? 引入依赖 首先只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。 注意: Spring Boot 2.3 1 之后,spring-...
继续阅读 »

基于 Spring Boot ,如何“优雅”的进行数据校验呢?


引入依赖


首先只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。


image.png


注意:
Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上!


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证 Controller 的输入


一定一定不要忘记在类上加上 @ Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。


验证请求体


验证请求体即使验证被 @RequestBody 注解标记的方法参数。


PersonController


我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。


@RestController
@RequestMapping("/api/person")
@Validated
public class PersonController {

@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {
return ResponseEntity.ok().body(personRequest);
}
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {

@NotNull(message = "classId 不能为空")
private String classId;

@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;

@Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;

}

使用 Postman 验证


image.png


验证请求参数


验证请求参数(Path Variables 和 Request Parameters)即是验证被 @PathVariable 以及 @RequestParam 标记的方法参数。


PersonController


@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {

@GetMapping("/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}

@PutMapping
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}

使用 Postman 验证


image.png


image.png


嵌套校验


在一个校验A对象里另一个B对象里的参数


需要在B对象上加上@Valid注解


image.png


image.png


常用校验注解总结


JSR303 定义了 Bean Validation(校验)的标准 validation-api,并没有提供实现。Hibernate Validation是对这个规范/规范的实现 hibernate-validator,并且增加了 @Email、@Length、@Range 等注解。Spring Validation 底层依赖的就是Hibernate Validation。


JSR 提供的校验注解:



  • @Null 被注释的元素必须为 null

  • @NotNull 被注释的元素必须不为 null

  • @AssertTrue 被注释的元素必须为 true

  • @AssertFalse 被注释的元素必须为 false

  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内

  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

  • @Past 被注释的元素必须是一个过去的日期

  • @Future 被注释的元素必须是一个将来的日期

  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式


Hibernate Validator 提供的校验注解



  • @NotBlank(message =) 验证字符串非 null,且长度必须大于 0

  • @Email 被注释的元素必须是电子邮箱地址

  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内

  • @NotEmpty 被注释的字符串的必须非空

  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内


image.png


@JsonFormat与@DateTimeFormat注解的使用


@JsonFormat用于后端传给前端的时间格式转换,@DateTimeFormat用于前端传给后端的时间格式转换


JsonFormat


1、使用maven引入@JsonFormat所需要的jar包


        <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.8</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>

2、在需要查询时间的数据库字段对应的实体类的属性上添加@JsonFormat


   @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateDate;

注: timezone:是时间设置为东八区,避免时间在转换中有误差,pattern:是时间转换格式


DataTimeFormat


1、添加依赖


       <dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.3</version>
</dependency>

2、我们在对应的接收前台数据的对象的属性上加@DateTimeFormat


@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime acquireDate;

3.这样我们就可以将前端获取的时间转换为一个符合自定义格式的时间格式存储到数据库了
全局异常统一处理:拦截并处理校验出错的返回数据
写一个全局异常处理类


@ControllerAdvice

public class GlobalExceptionHandler{
/**
* 处理参数校验异常
*/

@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public ErrorResponseData validateException(MethodArgumentNotValidException e) {
log.error("参数异常"+e.getBindingResult().getFieldError().getDefaultMessage(),e);
return new ErrorResponseData(10001,e.getBindingResult().getFieldError().getDefaultMessage());
}

/**
* 处理json转换异常(比如 @DateTimeFormat注解转换日期格式时)
*/

@ExceptionHandler({HttpMessageNotReadableException.class})
@ResponseBody
public ErrorResponseData jsonParseException(HttpMessageNotReadableException e) {
log.error("参数异常"+e.getLocalizedMessage(),e);
return new ErrorResponseData(10001,e.getCause().getMessage());
}

}

作者:Hypnosis
来源:juejin.cn/post/7241114001324228663
收起阅读 »

让弹窗更易于使用~

web
标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。 关键字:react / modal 问题 实际业务中,不乏弹窗组件中包含大量复杂的业务逻辑。如: function Order() { // 省略上百行方法状态    const [vis...
继续阅读 »

标题又名:简单弹窗、多弹窗、复杂弹窗怎么做代码和状态的解耦。


关键字:react / modal


问题


实际业务中,不乏弹窗组件中包含大量复杂的业务逻辑。如:


function Order() {
// 省略上百行方法状态
   const [visible,setVisible] = useState(false)
   const withModalState = useState<any>()
   
   return (
  <Modal>
      <Input/>
           <Input/>
<Select/>
<Checkbox/>
       </Modal>

  )
}

甚至还有多弹窗的情形。如:


function Order() {
// 省略上百行方法状态
   const [visible1,setVisible1] = useState(false)
   const [visible2,setVisible2] = useState(false)
   const [visible3,setVisible3] = useState(false)
   
   const withModalState1 = useState<any>()
   const withModalState2 = useState<any>()
   const withModalState3 = useState<any>()
   
   // 省略 不懂多少 handlexx
   return (
       <main>
        ...
           <Modal1></* 省略很多代码 */></Modal1>

           <Modal2></* 省略很多代码 */></Modal3>
           <Modal3></* 省略很多代码 */></Modal3>
       </main>
  )
}


非常的痛:



  1. 如果弹窗在处理完内部流程后,又还有返回值,这有又会有一大通的处理函数。

  2. 如果这些代码都写在一个文件中还是不太容易维护的。


而且随着业务的断增长,每次都这么写还是很烦的。


期望的使用方式


因此有没有一种更贴近业务实际的方案。


让弹窗只专注于自己的内部事务,同时又能将控制权交给调用方呢。


或者说我就是不喜欢 Modal 代码堆在一起……


如下:


// Modal1.tsx
export function Modal1(props) return <Modal></* ... */></Modal>

// 甚至可以将MOdal中的逻辑做的更聚合。通过2次封再导出给各方使用
export const function Check_XXX_Window() {/* */ return open(Modal1)}
export const function Check_XXX_By_Id_Window() { return open(Modal1)}
export const function Check_XXX_Of_Ohter_Window() { return open(Modal1)}

// Order.tsx
function Order() {

function xxHndanle {
const expect = Check_XXX_Window(Modal1,args) // 调用方法,传入参数,获得预期值 ,完成1个流程
}
return (
<main>
...
// 不实际挂载 Modal 组件
</main>

)
}

像这样分离两者的代码,这样处理起来肯定是清爽很多的。


实现思路


实现的思路都是一致的。



  1. 创建占位符组件,放到应用的某处。

  2. 将相关状态集管理,并与Modal做好关联。

  3. 暴露控制权。


因此基于不同的状态管理方案,实现是多种多样的。相信很多大佬都自己内部封装过不少了。


但是在此基础上,我还想要3点。



  1. API使用简单,但也允许一定配置。

  2. 返回值类型推导,除了帮我管理 close 和 open 还要让我用起来带提示的。

  3. 无入侵性,不依赖框架以外的东西。


ez-modal-react


emm......苦于市面上找不到这类产品(感觉同类并不多……讨论的也不多?)


于是我自己开源了一个。回应上文实现思路,可以点击看代码,几百行而已。



并不是突发奇想而来,其实相关特性早就在企业内部是使用多年了。我也是受前辈和社区启发。



基本特性



  1. 基于Promise封装

  2. 返回值类型推导

  3. 没有入侵性,体积小。


使用画面


import EasyModal, { InnerModalProps } from 'ez-modal-react';

+ interface IProps extends InnerModalProps<'fybe?'> /*传入返回值类型*/ {
+ age: number;
+ name: string;
+ }

export const InfoModal = EasyModal.create(
+ (props: Props) => {
return (
<Modal
title="Hello"
open={props.visible}
onOk={() => {
+ props.hide(); // warn 应有 1 个参数,但获得 0 个。 (property) hide: (result: "fybe?") => void ts(2554)
}}
onCancel={() => {
props.hide(null); //safe hide 接受 null 作为参数。它兼具 hide resolve 两种功能。
}}
>
<h1>{props.age}</h1>
</Modal>
);
});

+ // warn 类型 "{ name: string; }" 中缺少属性 "age",但类型 "ModalProps<Props, "fybe?">" 中需要该属性。
EasyModal.show(InfoModal, { name: 'foo' }).then((resolve) => {
console.log(resolve);
+ //输出 "fybe?"
});

也支持用 hook


import EasyModal, { useModal, InnerModalProps } from 'ez-modal-react';

interface IProps extends InnerModalProps<'苏振华'>/* 指定返回值类型 */ {
age: number;
name: string;
}

export const Info = EasyModal.create((props: Props) => {
const modal = useModal<Props>();

function handleOk(){
modal.hide(); // ts(2554) (property) hide: (result: "苏振华") => void ts(2554)
}

return <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>
});


EasyModal.show(Info,{age:18,}) // 缺少属性 "age"

还有一些特性如支持配置 hide 弹窗时的默认行为。(我认为大多数情况下可能用不上)



export const Info = EasyModal.create((props: Props) => {
const modal = useModal<Props>();

function handleOk(){
modal.hide();
+ modal.resolve('苏振华') // 需要手动抛出成功
+ modal.remove() // 需要手动注销组件。可用于反复打开弹窗,但是不希望状态被清除的场景。
}

return <Modal open={modal.visible} onOk={handleOk} onCancel={modal.hide}></Modal>

// Ohter.tsx
EasyModal.open(Info, {},
+ config:{
+ resolveOnHide:false, // 默认为 true
+ removeOnHide:false, // 默认为 true
+ }
);


当然以上是针对弹窗内有复杂业务场景的状况。


大部分场景都是调用方只在乎 open或close ,仅仅解耦代码这一项好处,就可以让代码变得清爽。


如常用的有展示类,设置类的弹窗,TS类型都用不上。


仓库


其他就不一一介绍了,主要的已经说完了,想了解更多,可以看看仓库。github


🎮 Codesandbox Demo


Codesandbox是一个线上集成环境,可以直接打开玩玩。点击 Demo Link 前往


初衷


让弹窗使用更轻松。



包名 ez 开头,是因为我DOTA在东南亚打多了,觉得该词特别贴切。



授之于鱼叉


GitHub仓库地址
ez-modal-react


觉得有用帮我点个星~~~ 感恩,有啥问题可以提出,看到会回复。如果有需要会持续维护该项目。


下列诸神,望您不吝赐教

作者:胖东蓝
来源:juejin.cn/post/7238917620849246263

收起阅读 »

为什么需要PNPM ?

web
PNPM是什么? 在日常工作里面,总是有同事让我将npm替换成pnpm,理由是这玩意速度更快,效率更高。那到底pnpm是什么呢?他为什么比npm/yarn有着更大的优势?首先,毫不疑问,pnpm是作为一个前端包管理器工具被提出的,作者的初衷是为了能够开发出一款...
继续阅读 »


PNPM是什么?


在日常工作里面,总是有同事让我将npm替换成pnpm,理由是这玩意速度更快,效率更高。那到底pnpm是什么呢?他为什么比npm/yarn有着更大的优势?
首先,毫不疑问,pnpm是作为一个前端包管理器工具被提出的,作者的初衷是为了能够开发出一款能够有效节省磁盘空间,提高安装速度的包管理工具。
其次,npm和yarn存在的诸多问题,也让开发者诟病已久,pnpm也是在这样的背景下被开发出来并广受欢迎的。
image.png
image.png
image.png


PNPM解决了什么样的问题?


在讨论pnpm解决了什么样的问题之前,我们可以先看看npm和yarn这些传统包管理工具到底存在什么样的问题。


NPM 2


在Npm2.x里面,当你观察node_modules时,你会发现对于不同包之间的依赖关系,npm2.x采用的是层层嵌套的方式去管理这些依赖包。
image.png
对于依赖关系来说,嵌套的管理方式虽然让不同包之间的依赖关系一目了然,但是却存在着诸多的问题。



  • 公共的依赖无法重复利用。不同的包里难免会存在相同的依赖,但是在npm2.x里面,这些公共依赖会被复制很多次,存在于不同的包的嵌套node_modules里,这样会导致下载速度的下降以及浪费了许多磁盘空间。

  • **嵌套关系过深时,路径名过长。**在window下,有很多程序无法处理超过260个字符的文件路径名,如果嵌套关系过深时,就有可能会超出这个限制,导致windows下存在无法解析的现象。



NPM 3 & YARN


针对上述Npm 2存在的两个典型的问题,Yarn和Npm3采用了扁平化的依赖管理方式,所有的依赖不再层层嵌套,而是全部都在同一层,这样就同时解决了重复依赖多次复制和路径名过长的问题了。
image.png
可以看到使用npm3安装的express使,大部分的包都不会有二层的node_modules的,当然,如果同时存在多个版本的包时,则还是会出现部分嵌套node_modules的情况。
另外,Yarn和Npm 3都采用了lock文件,借此来保证每次拉取同一个项目的依赖时,使用的是同一个版本,避免不同版本之间的差异性导致的项目Bug。
但是,问题又来了,难道使用了扁平化的依赖管理方式就是完美的吗,这种管理方式会不会产生新的问题?答案否定的,扁平化的管理方式会导致两个最主要的问题



  • 幽灵依赖

  • 磁盘空间问题没有完全解决


幽灵依赖


幽灵依赖是指你的项目明明没有在package.json文件里声明的依赖,但是在代码里面却可以使用到。导致这个问题的最主要的原因就是,依赖扁平化了,当你的项目寻找依赖的时候,可以找到所有node_modules里的最外层依赖。
显然,幽灵依赖是会带来隐患的,当你依赖的包A有一天不再需要包B了, 你对B的幽灵依赖就会导致错误,从而发生问题。


磁盘问题


当同一个项目依赖了某个包的多个版本时,Npm3和Yarn只会提升其中的一个,而其余版本的包还是不能避免复制多次的问题 。


PNPM是如何解决这些问题的?


在讨论Pnpm的机制之前,我们需要先学习下一些操作系统相关的知识。


链接


链接实际上是一种文件共享的方式。在Linux的文件系统中,除了文件名和文件内容,还有一个很重要的概念,就是inode,inode类似于C语言的指针,它指向了物理硬盘的一个区块,只有有文件指向这个区块,他就不会从硬盘中消失。而硬链接和软连接最大的区别,则是inode的与原文件的关系。


硬链接


一般来说,inode与文件名、文件数据是一对一的关系,但我们可以通过shell命令让多个文件名指向同一个inode,这种就是硬链接(hard link)。由于硬链接文件和源文件使用同一个inode,并指向同一块文件数据,除文件名之外的所有信息都是一样的。所以这两个文件是等价的,可以说是互为硬链接文件。修改任意一个文件,可以看到另外一个文件的内容也会同步变化。
也正是因为有多个相同的inode指向同一个区块,所以硬链接的源文件即使被删除,也不会对链接文件有任何影响。


软链接


软连接又称符号链接,与硬链接共用一个inode不同的是,软链接会创建一个新的inode,存放着源文件的绝对路径信息,并指向源文件。当用户访问软链接时,系统会自动将其替换为该软链接所指向的源文件的文件路径,然后访问源文件。
而PNPM则是利用了以上链接的机制,来解决Npm和Yarn存在的问题。
当你使用Pnpm安装依赖时,Pnpm会在全局的仓库里保存一份npm包的内容,然后再利用链接的方式,从全局仓库里链接到你项目里的虚拟仓库,也就是node_modules里的是.pnpm。


image.png


你所安装的依赖,都会单独存放在node_modules下,而依赖所依赖的npm包都会通过软链接的方式,链接.pnpm里的虚拟仓库里的包,而.pnpm里的包,则是通过硬链接从全局Store里链接过来的。
image.pngimage.png
通过软硬链接结合的方式,Pnpm可以很有效的解决了Npm和yarn遗留的问题:
1、抛弃了扁平化的管理方式,避免了幽灵依赖。
2、Npm包的存储方式都是放在全局,使用到的时候只会建立一个硬链接,硬链接和源文件共享同一份内存空间,不会造成重复依赖的空间浪费。另外,当依赖了同一个包的不同版本时,只对变更的文件进行更新,不需要重复下载没有变更的部分。
3、下载速度,当存在已经使用过的npm包时,只会建立链接,而不会重新下载,大大提升包安装的速度。


PNPM天生支持Monorepo?


monorepo 是在一个项目中管理多个包的项目组织形式。
它能解决很多问题:工程化配置重复、link 麻烦、执行命令麻烦、版本更新麻烦等。
而利用Pnpm的workSpace配合changesets,就可以很简单的完成一个Monorepo项目的搭建,所以说Pnpm天生就是Mo

作者:Gamble_
来源:juejin.cn/post/7240662396020916282
norepo的利器。

收起阅读 »

前端自动部署:从手动到自动的进化

web
在现代 Web 开发中,前端自动化已经成为了必不可少的一部分。随着项目规模的增加和开发人员的增多,手动部署已经无法满足需求,因为手动部署容易出错,而且需要大量的时间和精力。因此,自动化部署已经成为了前端开发的趋势。在本文中,我们将介绍前端自动化部署的基本原理和...
继续阅读 »

在现代 Web 开发中,前端自动化已经成为了必不可少的一部分。随着项目规模的增加和开发人员的增多,手动部署已经无法满足需求,因为手动部署容易出错,而且需要大量的时间和精力。因此,自动化部署已经成为了前端开发的趋势。在本文中,我们将介绍前端自动化部署的基本原理和实现方式,并提供一些示例代码来说明。


前端自动化部署的基本原理


前端自动化部署的基本原理是将人工操作转换为自动化脚本。这些脚本可以执行一系列操作,例如构建、测试和部署应用程序。自动化部署可以帮助开发人员节省时间和精力,并提高应用程序的质量和可靠性。


自动化部署通常包括以下步骤:



  1. 构建应用程序:使用构建工具(例如 webpack、gulp 或 grunt)构建应用程序的代码和资源文件。

  2. 运行测试:使用测试工具(例如 Jest、Mocha 或 Karma)运行应用程序的单元测试、集成测试和端到端测试。

  3. 部署应用程序:使用部署工具(例如 Jenkins、Travis CI 或 CircleCI)将应用程序部署到生产服务器或云平台上。


实现前端自动化部署的方式


实现前端自动化部署的方式有很多种,以下是其中的一些:


1. 使用自动化部署工具


自动化部署工具可以帮助我们自动化构建、测试和部署应用程序。这些工具通常具有以下功能:



  • 与版本控制系统集成,例如 Git 或 SVN。

  • 与构建工具集成,例如 webpack、gulp 或 grunt。

  • 与测试工具集成,例如 Jest、Mocha 或 Karma。

  • 与部署平台集成,例如 AWS、Azure 或 Heroku。


自动化部署工具可以帮助我们节省时间和精力,并提高应用程序的质量和可靠性。


以下是一个使用 Jenkins 自动化部署前端应用程序的示例:


pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'npm install'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm run test'
}
}
stage('Deploy') {
steps {
sh 'npm run deploy'
}
}
}
}

在这个示例中,我们使用 Jenkins 构建、测试和部署前端应用程序。我们将应用程序的代码和资源文件打包到一个 Docker 容器中,并将容器部署到生产服务器上。


2. 使用自动化构建工具


自动化构建工具可以帮助我们自动化构建应用程序的代码和资源文件。这些工具通常具有以下功能:



  • 支持多种语言和框架,例如 JavaScript、React 和 Vue。

  • 支持多种模块化方案,例如 CommonJS 和 ES6 模块。

  • 支持多种打包方式,例如单文件和多文件打包。

  • 支持多种优化方式,例如代码压缩和文件合并。


以下是一个使用 webpack 自动化构建前端应用程序的示例:


const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};

在这个示例中,我们使用 webpack 构建前端应用程序的代码和资源文件。我们将应用程序的入口文件设置为 src/index.js,将输出文件设置为 dist/bundle.js,并使用 Babel 转换 JavaScript 代码和使用 CSS Loader 加载 CSS 文件。


3. 使用自动化测试工具


自动化测试工具可以帮助我们自动化运行应用程序的单元测试、集成测试和端到端测试。这些工具通常具有以下功能:



  • 支持多种测试框架,例如 Jest、Mocha 和 Jasmine。

  • 支持多种测试类型,例如单元测试、集成测试和端到端测试。

  • 支持多种测试覆盖率工具,例如 Istanbul 和 nyc。

  • 支持多种测试报告工具,例如 JUnit 和 HTML。


以下是一个使用 Jest 自动化测试前端应用程序的示例:


test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

在这个示例中,我们使用 Jest 运行一个简单的单元测试。我们将 sum 函数的输入设置为 1 和 2,并将期望输出设置为 3。如果测试通过,Jest 将输出 PASS,否则将输出 FAIL


结论


前端自动化部署已经成为了现代 Web 开发的趋势。通过使用自动化部署工具、自动化构建工具和自动化测试工具,我们可以节省时间和精力,并提高应用程序的质量和可靠性。在未来,前端自动化部署将会变得更加普遍和重要,因此我们需要不断学习和掌

作者:_大脑斧
来源:juejin.cn/post/7240636320761921594
握相关的技术和工具。

收起阅读 »

程序员增强自控力的方法

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法: 1. 设定明确的目标和计划 制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任...
继续阅读 »

作为一名程序员,我们经常会面临工作压力和时间紧迫的情况,因此有一个好的自控力对于我们的工作和生活都是至关重要的。以下是一些可以帮助程序员增强自控力的方法:


1. 设定明确的目标和计划


制定明确的目标和计划可以帮助我们更好地管理时间和精力。我们可以使用日程表、任务清单、时间追踪工具等,来帮助我们控制时间并更有效地完成任务。


2. 掌控情绪


作为程序员,我们需要面对很多挑战和压力,容易受到情绪的影响。因此,掌握情绪是一个非常重要的技能。可以通过冥想、呼吸练习、运动等方法,来帮助我们保持冷静、积极和乐观的心态。


3. 管理焦虑和压力


焦虑和压力是我们常常遇到的问题之一,所以我们需要学会如何管理它们。我们可以使用放松技巧、适度锻炼、交流沟通等方法,来减轻我们的焦虑和压力。


4. 培养自律习惯


自律是一个非常重要的品质。我们可以通过设定目标、建立规律和强化自我控制等方式,来培养自律习惯。


5. 自我反思和反馈


经常进行自我反思和反馈可以帮助我们更好地了解自己的优缺点和行为模式。我们可以使用反馈工具或与他人交流,来帮助我们成长和改进。


6. 持续学习和自我发展


程序员需要不断学习和自我发展,以保持竞争力和提升自己的技能。通过阅读书籍、参加培训、探究新技术等方式,可以帮助我们持续成长,增强自我控制力。


结论


自控力是我们工作和生活中重要的的品质之一,可以帮助我们更好地应对各种挑战和压力。通过设定目标、掌控情绪、管理焦虑和压力、培养自律习惯、自我反思和反馈、持续学习和自我发展等方法,我们可以帮助自己增强自我控制

作者:郝学胜
来源:juejin.cn/post/7241015051661312061
能力并提高工作效率。

收起阅读 »

哎,今天在公司的最后一天了

“啊!” 我今天居然被通知裁员了!!! 虽然之前一直盛传我们这边要裁员,但是我想着,应该一时半会轮不到我这边,我感觉我们项目还是相对挣点钱的。但是呢,心里也是挺忐忑的。 今天,我正在那全心全意的敲代码,敲的正起劲呢。我突然看到我们项目经理被叫走了。啊?我一想啥...
继续阅读 »

“啊!” 我今天居然被通知裁员了!!!


虽然之前一直盛传我们这边要裁员,但是我想着,应该一时半会轮不到我这边,我感觉我们项目还是相对挣点钱的。但是呢,心里也是挺忐忑的。


今天,我正在那全心全意的敲代码,敲的正起劲呢。我突然看到我们项目经理被叫走了。啊?我一想啥情况?要开会的话怎么只叫我们项目经理,怎么不叫我呀。


难道是要裁员?!难道真的是要裁员?!然后我就看着我们项目经理和我们的上级领导他们一起坐在小屋里聊了半天,啊,我的小心脏呀,我心里就祈祷呀:“千万不要裁员啊!千万不要裁员呀!千万不要裁员呀!!!”


等我们项目经理出来之后,他走到了我这边,然后 “啪” 拍了一下我的肩膀,然后 “哎” 叹了口气。他说:“我们这个项目要被裁掉了。”


我说心里特别失落,但还故作镇定的说:“为啥?我们的项目不是还挣钱呢吗?”


项目经理说:“哎,挣钱也不行。我们现在不需要这么多人了。我们现在的项目,没有一个大的发展了啊!你先等一会吧,等一会他们还得找你谈。”。他走的时候,又拍了拍我肩膀。


哎,当时我就感觉我心里呀那种失落感呀,没法说的那种感觉。果然,没一会,我们经理就来了。他过来之后跟我说:“走,请你到小屋里喝点水。”


我苦笑着跟他说:“经理,我能不去吗?我现在不渴。”


然后我们经理说:“哎,不行呀,我都已经给你倒好了,走吧走吧,歇会去。”


然后我就默默的跟他去了。进去之后呢,我们俩都坐下了。经理跟我笑着说:“恭喜你呀,脱离苦海了。”


哎,我当时心情比较低落,我说:“是呀,脱离苦海了,但又上了刀山了呀。哈哈哈。。。”


然后他说:“哎,确实是,没办法,现在,哎,公司也不容易。现在有一些项目确实得收缩。”


我说:“哎,这也没啥,这都很正常。咱公司还算不错的,最起码还让过了个节。很多公司什么都不管,就这样让走了呀。哎!”


后面我们就谈了一些所谓的那种离职补偿啊,等等一些列的东西**。**


反正当时感觉着吧,就是,嗯,聊完之后呢就准备出去嘛。然后走路的时候呀,就感觉这个腿上啊就跟绑了铅块一样。


当时我感觉,哎,裁员这玩意怎么说呢,都没法回去和亲人说呀,弄的一下午这个心里慌慌的。怎么跟家人交代呢?人至中年居然混成这样,哎!!!


郑重声明,本文不是为了制造焦虑,发文的原因有两个:



  1. 我今年 33 了,一方面给大家展现下一个普通程序员 35 岁后能咋样?是送外卖还是跑滴滴?难道真的就找不到工作了吗?

  2. 感觉我并没有走好自己的人生路,把自己的经历写出来发到网上,让年轻人以我为鉴,能更好的走好自己的人
    作者:程序员黑黑
    来源:juejin.cn/post/7110887208953282590
    生路。

收起阅读 »

我的前端开发学习之路,从懵懂到自信

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。 刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接...
继续阅读 »

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。


刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接触一些流行的框架和库时,我才发现自己真正的水平有多菜。当时,我就像一只踩在滑板上的小猪,不断地摔倒,但是我并没有放弃,我一直在努力地学习和实践。


在学习的过程中,我遇到了许多困难和挑战,但是也有很多有趣的体验和经历。有一次,我在编写一个简单的网页时,花了一整天的时间,结果发现自己的代码有一个很小的错误,导致整个网页无法正常显示。当时我感觉自己就像一个猴子在敲打键盘,非常无助和懊恼。但是,通过不断地调试和修改,我最终找到了错误,并且成功地将网页显示出来。当时,我感觉自己就像一只成功攀爬上树的小猴子,非常自豪和兴奋。


除了遇到困难和挑战,我在学习前端开发过程中也经历了许多有趣的体验。有一次,我在编写一个小型的应用程序时,发现我的代码出现了一个非常有趣的小 bug。当用户在页面上进行操作时,页面上的一些元素会突然出现在屏幕的右侧,然后又突然消失不见。当时我还担心这个 bug 会影响用户的正常使用,但是后来发现这个 bug 其实很有趣,而且还能给用户带来一些意外的乐趣。于是我就把这个 bug 留了下来,并且在用户操作时添加了一些特效,让这个小 bug 变成了一个有趣的亮点。


12.jpg
总结一波:
第一点,学习前端开发需要有耐心。前端开发不是一个短时间内可以学会的技能,它需要大量的时间和精力。尤其是在学习的早期,你可能会觉得有些技术和概念非常难以理解。但是,只要你有耐心,坚持不懈地学习,最终你一定会掌握这些技能。


第二点,建立一个良好的学习计划非常重要。前端开发有很多不同的技术和概念,你需要有一个清晰的学习计划来帮助你系统地学习和掌握这些知识。首先,你需要了解 HTML、CSS 和 JavaScript 这三大基本技术。其次,你可以学习一些流行的框架和库,如 React、Vue、jQuery 等,这些技术可以帮助你更快捷地构建网站和应用程序。


第三点,实践是学习前端开发的关键。你可以通过练习编写代码来更好地理解前端开发的技术和概念。在学习的过程中,你可以尝试编写一些小项目,比如一个简单的网页或者一个小型的应用程序。通过实践,你可以更深入地了解前端开发的各个方面,并且提高你的编程技能。


第四点,不要害怕向他人寻求帮助。前端开发是一个非常开放和社交的领域,你可以通过参加社区活动、参与在线讨论、向他人寻求帮助等方式来更好地学习和成长。有时候,你可能会遇到一些困难,或者对某些概念不是很理解,这时候向他人寻求帮助是非常重要的。你可以参加一些线上或线下的前端开发社区,与其他开发者交流经验和技巧,也可以在 GitHub 等平台上查看其他人的代码,从中学习和借鉴。


第五点,不断更新自己的知识和技能。前端开发是一个不断发展和变化的领域,新技术和新概念层出不穷。因此,你需要不断地更新自己的知识和技能,跟上前端开发的最新动态。你可以通过阅读博客、参加培训课程、观看技术视频等方式来学习新的技术和概念。


总之,学习前端开发需要有耐心、建立一个良好的学习计划、实践、寻求帮助和不断更新自己的知识和技能。这些都是非常重要的,也是我在学习前端开发过程中得到的宝贵经验。通过不断地学习和实践,相信你我可以成为一名优

作者:梦想橡皮擦丶
来源:juejin.cn/post/7239363820875513916
秀的前端开发工程师。

收起阅读 »

基于环信Web Vue3 Demo使用electron快速打包生成桌面端应用

前言一直以来都有听说利用yarn install安装项目相关 npm 依赖。在此项目目录下打开终端请敲下wait-on以及wait-on 是一个 Node.js 包,它可以用于等待多个指定的资源(如 HTTP 资源、TCP 端口或文件)变得可用。它通...
继续阅读 »

前言

一直以来都有听说利用electron可以非常便捷的将网页应用快速打包生成为桌面级应用,并且可以利用 electron 提供的 API 调用原生桌面 API 一些高级功能,于是这次借着论证环信 Web 端 SDK 是否可以在 electron 生成的桌面端正常稳定使用,我决定把官方新推出的 webim-vue3-demo,打包到桌面端,并记录一下这次验证的过程以及所遇到的问题以及解决方式。

前置技能

  • 拥有良好的情绪自我管理,能够在遇到棘手问题时不一拳给到键盘。
  • 拥有较为熟练的水群能力,能够在遇到问题时,主动向技术群内参差不齐的群友们抛出自己的问题。
  • 【重要】拥有较为熟练的搜索引擎使用能力。
  • 能够看到这篇文章,那说明以上能力你已完全具备。

测试流程记录

第一步、准备工作

  • 克隆 vue3 Demo 项目到本地 环信 vue3-demo 源码地址
  • 在编辑器内打开此项目并执行yarn install安装项目相关 npm 依赖。
  • 在此项目目录下打开终端请敲下yarn add electron,从而在该项目中安装 electron。
  • 安装一些依赖工具wait-on以及cross-env

wait-on 是一个 Node.js 包,它可以用于等待多个指定的资源(如 HTTP 资源、TCP 端口或文件)变得可用。它通常用于等待应用程序的依赖项准备好后再启动应用程序。例如,您可以使用 wait-on 等待数据库连接、消息队列和其他服务就绪后再启动您的应用程序。这样可以确保您的应用程序在尝试使用这些资源之前不会崩溃。

cross-env 是一个 npm 包,它的作用是在不同平台上设置环境变量。在不同操作系统中,设置环境变量的方式是不同的。例如,在 Windows 中使用命令 set NODE_ENV=production 设置环境变量,而在 Unix/Linux/Mac 上则需要使用 export NODE_ENV=production 命令。

此时可能会进入到漫长的等待阶段,第一、这个包本身就比较大,第二、相信大家都懂由于网络原因导致,并且有可能进行会经历几次TIMOUT安装失败。此时就需要心平气和,且有耐心的进行改变镜像地址科学进行上网WIFI切换为移动流量多去重试几次,相信道友你总会成功过的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2xyqXdwF-1685947556715)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/69f46811946344499047553af9abccd9~tplv-k3u1fbpfcp-watermark.image?)]
有如下输出则应该为安装成功。

第二步、项目目录增加 electron 文件

在项目增加 electron 文件时我们需要扩展一部分知识从而了解为什么创建创建这个目录,并在该目录下增加main.js文件的作用。当然如果觉得不需要可以直接略过。

主进程与渲染进程的概念

在 Electron 中,主进程和渲染进程是两个不同的概念。主进程是 Electron 应用程序的核心,它运行在一个 Node.js 实例中,并管理应用程序的生命周期、窗口创建和销毁、与底层操作系统进行交互等。主进程还可以通过 IPC(进程间通信)机制与渲染进程进行通信。
渲染进程则是应用程序的 UI 界面所在的进程。每个 Electron 窗口都有其自己的渲染进程。渲染进程是一个 Chromium 渲染引擎实例,它运行在一个仅包含 Web API 的环境中。渲染进程负责渲染 HTML、CSS 和 JavaScript,并处理来自用户的输入事件,同时通过 IPC 机制与主进程进行通信。
由于渲染进程只能访问 Web API 而不能直接访问 Node.js API,因此如果需要在渲染进程中使用 Node.js API,就需要通过 IPC 机制向主进程发出请求,由主进程代为执行并将结果返回给渲染进程。

主进程与渲染进程分别应该写在哪?

在 Electron 应用程序中,主进程通常写在名为 main.js 或者 index.js 的 JavaScript 文件中,这个文件是应用程序的入口点。而渲染进程则通常写在 HTML 文件和其引入的 JavaScript 文件中。在一个 Electron 窗口中,可以通过调用 webContents 对象的 loadURL 方法来加载一个 HTML 文件,其中包含了渲染进程所需的代码和资源。该 HTML 文件中的 JavaScript 代码将运行在对应的渲染进程中,可以通过 Electron 提供的一些 API 和 Web API 来进行与用户界面相关的操作
需要注意的是,在 Electron 中,由于主进程和渲染进程是不同的 Node.js 实例,因此它们之间并不能直接共享变量或者调用函数。如果想要实现主进程和渲染进程之间的通信,必须使用 Electron 提供的 IPC 机制,通过发送消息的方式来进行进程间通信。

有些 electron 文件目录下 preload.js 的作用

在 Electron 中,preload.js 文件是一个可选的 JavaScript 文件,用于在渲染进程创建之前加载一些额外的脚本或者模块,从而扩展渲染进程的能力。preload.js 文件通常存放在与主进程代码相同的目录下。

preload.js 的实际运用主要有以下几个方面:

  1. 托管 Node.js API:preload.js 中可以引入 Node.js 模块,并将其暴露到 window 对象中,从而使得在渲染进程中也能够使用 Node.js API,避免了直接在渲染进程中调用 Node.js API 带来的安全风险。
  2. 扩展 Web API:preload.js 中还可以定义一些自定义的函数或者对象,然后将它们注入到 window 对象中,这样在渲染进程中就可以直接使用它们了,而无需再进行额外的导入操作。
  3. 进行一些初始化操作:preload.js 文件中的代码会在每个渲染进程的上下文中都运行一遍,在这里可以进行一些初始化操作,比如为页面添加一些必要的 DOM 元素、为页面注册事件处理程序等。

需要注意的是,preload.js 文件中的代码运行在渲染进程的上下文中,因此如果 preload.js 中包含一些恶意代码,那么它很可能会危及整个渲染进程的安全性。因此,在编写 preload.js 文件时,一定要格外小心,并且仅引入那些你信任的模块和对象。

1、 添加 electron 文件

  • 此时项目目录

2、 electron 下新建main.js示例代码如下:

const { app, BrowserWindow } = require('electron');
const path = require('path');
const NODE_ENV = process.env.NODE_ENV;
app.commandLine.appendSwitch('allow-file-access-from-files');
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 980,
height: 680,
fullscreen: true,
skipTaskbar: true,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
},
});

if (NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:9001/');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`);
}
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.

app.whenReady().then(() => {
createWindow();
});

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
});

3、 electron 下新建preload.js,示例代码如下:

此文件为可选文件

//允许vue项目使用 ipcRenderer 接口, 演示项目中没有使用此功能
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('ipcRender', ipcRenderer);

4、修改package.json,当前示例代码如下:

  • 修改"main"配置,将其指向为"main": "electron/main.js"
  • 增加一个针对 electron 启动的"scripts""electron:dev": "wait-on tcp:3000 && cross-env NODE_ENV=development electron ./"

当前项目配置如下所示

{
"name": "webim-vue3-demo",
"version": "0.1.0",
"private": true,
"main": "electron/main.js",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:dev": "wait-on tcp:9001 && cross-env NODE_ENV=development electron ./"
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"agora-rtc-sdk-ng": "^4.14.0",
"axios": "^0.27.2",
"benz-amr-recorder": "^1.1.3",
"core-js": "^3.8.3",
"easemob-websdk": "^4.1.6",
"element-plus": "^2.2.5",
"nprogress": "^0.2.0",
"pinyin-pro": "^3.10.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"cross-env": "^7.0.3",
"electron": "^24.3.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"wait-on": "^7.0.1"
}
}

第三步、本地启动起来验证一下

  1. 启动运行原 vue 项目

这里启动项目至端口号 9001,跟上面 electron/main.jsmainWindow.loadURL(' http://localhost:9001/')是可以对应上的,也就是 electron 运行起来将会加载此服务地址。

yarn run dev
  1. 新开一个终端执行,输入下方命令启动 electron

执行下面命令

yarn run electron:dev

可以看到自动开启了一个 electron 页面



并且经过测试验证登录没有什么问题。

第四步、尝试打包并验证打包出来的安装包是否可用。

1、安装electron-builder

该工具为 electron 打包工具库

终端执行下面命令安装 electron-builder

yarn add electron-builder --dev

2、package.json 配置打包脚本命令以及设置打包个性化配置项

参考配置如下

具体配置项作用请参考官网文档,下面有些配置也是 CV 大发过来的,没有具体深入研究。

{
"name": "webim-vue3-demo",
"version": "0.1.0",
"private": true,
"main": "electron/main.js",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:dev": "wait-on tcp:9001 && cross-env NODE_ENV=development electron ./",
"electron:build": "rimraf dist && vue-cli-service build && electron-builder",
"electron:build2": "electron-builder"
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"agora-rtc-sdk-ng": "^4.14.0",
"axios": "^0.27.2",
"benz-amr-recorder": "^1.1.3",
"core-js": "^3.8.3",
"easemob-websdk": "^4.1.6",
"element-plus": "^2.2.5",
"nprogress": "^0.2.0",
"pinyin-pro": "^3.10.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"cross-env": "^7.0.3",
"electron": "^24.3.1",
"electron-builder": "^23.6.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"wait-on": "^7.0.1"
},
"build": {
"productName": "webim-electron",
"appId": "com.lvais",
"copyright": "2023@easemob",
"directories": {
"output": "output"
},
"extraResources": [
{
"from": "./src/assets",
"to": "./assets"
}
],
"files": ["dist/**/*", "electron/**/*"],
"mac": {
"artifactName": "${productName}_${version}.${ext}",
"target": ["dmg"]
},
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
],
"artifactName": "${productName}_${version}.${ext}"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true
},
"linux": {}
}
}

3、开始 build

  • 先这样

build 原始 vue 项目

yarn run build
  • 再那样

build electron 项目

yarn run electron:build

可能会进入漫长的等待,但是不要慌,可能与网络关系比较大,需要耐心等待。


打包成功之后可以看到有一个 output 文件夹的生成,打开之后可以选择双击打开软件验证看下是否可以正常开启应用

正常开启页面的话,证明没有问题,如果遇到了问题,下方会有一些我遇到的问题,可以作为参考。

令人痛苦的问题汇总

问题一、打包后页面空白,并且出现类似(Failed to load resource: net::ERR_FILE_NOT_FOUND)报错

问题简述:发现只有在打包之后的 electron 应用,启动后存在页面空白,dev 情况下正常。

解决手段之一:

经排查,更改vue.config.jspublicPath的配置为’./’

const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
devServer: {
host: 'localhost',
port: 9001,
// https:true
},
publicPath: './',
chainWebpack: (config) => {
//最小化代码
config.optimization.minimize(true);
//分割代码
config.optimization.splitChunks({
chunks: 'all',
});
},
});

原因打包后的应用 electron 会从相对路径开始找资源,所以经过此配置可以所有资源则开始从相对路径寻找。

    默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如 `https://www.my-app.com/`。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 `https://www.my-app.com/my-app/`,则设置 `publicPath``/my-app/`

这个值也可以被设置为空字符串 (`''`) 或是相对路径 (`'./'`),这样所有的资源都会被链接为相对路径,这样打出来的包可以被部署在任意路径,也可以用在类似 Cordova hybrid 应用的文件系统中。

解决手段之二:

经过一顿操作之后发现仍然还是空白,并且打开控制台看到页面可以正常加载资源文件,但是 index.html 返回此类错误:We're sorry but XXX doesn't work properly without JavaScript,经过查找发现可以通过修改路由模式来解决,经过测试确实有效。

修改后的代码示例:

const router = createRouter({
//改为#则可以直接变更路由模式
history: createWebHistory('#'),
routes,
});

问题二、

问题简述:页面展示正常后,调用登录发现出现下图报错

解决方式:经发现原来是发起 axios 请求环信置换连接 token 接口的时候,协议的获取是通过window.location.protocol来获取的,那么打包之后的协议为file:那么这时发起的请求就会变更为以 file 协议发起的请求,那么修改这里的逻辑,判断如果为 file 协议则默认走 http 协议发起请求,示例代码如下:

import axios from 'axios';
const defaultBaseUrl = '//a1.easemob.com';
console.log('window.location.protocol', window.location.protocol);
// create an axios instance
const service = axios.create({
withCredentials: false,
// baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
baseURL: `${
window
.location.protocol === 'file:' ? 'https:' : window.location.protocol
}
${defaultBaseUrl}`
,
// withCredentials: true, // send cookies when cross-domain requests
timeout: 30000, // request timeout
headers: { 'Content-Type': 'application/json' },
});
// request interceptor
service.interceptors.request.use(
(config) => {
// do something before request is sent
return config;
},
(error) => {
// do something with request error
console.log('request error', error); // for debug
return Promise.reject(error);
}
);

// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/


/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/

(response) => {
const res = response.data;
const code = response.status;
// if the custom code is not 20000, it is judged as an error.
if (code >= 400) {
return Promise.reject(new Error(res.desc || 'Error'));
} else {
return res;
}
},
(error) => {
if (error.response) {
const res = error.response.data; // for debug
if (error.response.status === 401 && res.code !== '001') {
console.log('>>>>>无权限');
}
if (error.response.status === 403) {
res.desc = '您没有权限进行查询和操作!';
}
return Promise.reject(res.desc || error);
}
return Promise.reject(error);
}
);

export default service;

参考资料

特别鸣谢两位道友文章非常有用,可以作为参考:

收起阅读 »