注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

抓包技术的应用示例:薅瑞幸羊毛 🦙

前言 最近瑞幸在搞活动,每天免费送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;

参考资料

特别鸣谢两位道友文章非常有用,可以作为参考:

收起阅读 »

vue3项目打包时We're sorry but XXX doesn't work properly without JavaScript

vue
题引: 这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。 正文: 当界面做完之后且打包完成,就打开了 dist 文件夹里的 inde...
继续阅读 »

题引:


这周末公司突然分配了一个任务,让我搞一个混合代码的平板项目:vue3+安卓原生 来配合实现。看了一眼,问题不大,那边只要求把做好的页面打包成 dist 文件发给组长即可。开干。


正文:


当界面做完之后且打包完成,就打开了 dist 文件夹里的 index.html 。突然发现页面是空白的,打开调试器之后突然发现了一个报错:

<strong>We’re sorry but XXX doesn’t work properly without JavaScript enabled</strong>



看了一下vue-router、pinia没有什么问题,调用顺序也没错。于是就往打包的文件夹查看,才发现了引用的路径是以 / 绝对路径开头的,以至于资源无法加载而导致页面空白。


发现了这个问题,直接定位到 vue.config.js 文件,如果是vite创建的话应该是 vite.config.js

//vue.config.js
export default = {
publicPath: './', //打包文件的路径
... // 其他配置
}

//vite.config.js
import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
publicPath: './', //打包文件的路径
...
})

当然,上网查了一下前端的路由也会导致这个问题的出现。可以把mode值从 history 改成 hash

import {createRouter,createWebHashHistory} from 'vue-router';

const routes = [];
const router = createRouter({
router,
history:createWebHashHistory()
})

结尾:


以上就是处理打包上线时遇到 项目在没有启用JavaScript的情况下无法正常工作 的情况。


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

安卓与串口通信-数据分包的处理

前言 本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。 其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使...
继续阅读 »

前言


本文是安卓串口通信的第 5 篇文章。本来这篇文章不在计划内,但是最近在项目中遇到了这个问题,正好借此机会写一篇文章,在加深自己理解的同时也让大伙对串口通信时接收数据可能会出现分包的情况有所了解。


其实关于串口通信会可能会出现分包早有耳闻,但是我自己实际使用时一直没有遇到过,或者准确的说,虽然遇到过,但是并没有特意的去处理:


分包?不就是传过来的数据不完整嘛,那我把这个数据丢了,等一个完整的数据不就得了。


亦或者,之前使用的都是极少量的数据,一次读取的数据只有 1 byte ,所以很少出现数据包不完整的情况。


何为分包?


严格意义上来说,其实并不存在分包的概念。


因为由于串口通信的特性,它并不知道不知道也无法知道所谓的 “包” 是什么,它只知道你给了数据给它,他就尽可能的把数据发出去。


因为串口通信时使用的是流式传输,也就是说,所有数据都是以流的形式进行发送、读取,也不存在所谓的“包”的概念。


所谓的“包”只是我们在应用层人为的规定了多少长度的数据或者满足什么样格式的数据为一个“包”。


而为了最大程度的减少通信时的请求次数,在处理数据流时,通常会尽可能多的读取数据,然后缓存起来(即所谓的缓冲数据),直至达到设置的某个大小或超过某个时间没有读取到新的数据。


例如,我们人为的规定了一个数据包为 10 字节,PLC 或 其他串口设备发送时将这 10 个字节的数据连续的发送出来。但是安卓设备或其他主机在接收时,由于上面所说的原因,可能会先读到 4 字节的数据,再读到 6 字节的数据。也就是说,我们需要的完整数据不会在一次读取中读到,而是被拆分成了不同的“数据包”,此即所谓的 “分包”:


1.gif


怎么处理分包?


其实谜底就在谜面上,通过上面对分包出现的原因进行简单的解释之后,相信大伙对于怎么解决分包问题已经有了自己的答案。


解决分包的核心原理说起来非常简单,无非就是把我们需要的完整的数据包从多次读取到的数据中取出来,再拼成我们需要的完整数据包即可。


问题在于,我们应该怎么才能知晓读取到数据属于哪个数据包呢?我们又该怎么知道数据包是否已经完整了呢?


这就取决于我们在使用串口通信时定义的协议了。


一般来说,为了解决分包问题,我们常用的定义协议的方法有以下几种:



  1. 规定所有数据为固定长度。

  2. 为一个完整的数据规定一个终止字符,读到这个字符表示本次数据包已完整。

  3. 在每个数据包之前增加一个字符,用于表示后续发送的数据包长度。


固定数据包长度


固定数据长度指我们规定每次通信时发送的数据包长度都是固定的长度,如果实际长度不足规定的长度则使用某些特殊字符如 \0 填充剩余的长度。


对于这种情况,非常好处理,只要我们每次读取数据时都判断读取到的数据长度,如果数据长度没有达到符合的固定长度,则认为读取数据不完整,就接着读取,直至数据长度符合:

val resultByte = mutableListOf<Byte>()
private fun getFullData(count: Int = 0, dataSize: Int = 20): ByteArray {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}

// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
// 超时
return ByteArray(0)
}
}
}

但是这种方式也有一个明显的缺点,那就是使用场景局限性特别强,只适合于主机发送请求,从机器回应的这种场景,因为如果是在从机不停的发送数据,而主机可能在某个时间段读取,也可能一直轮询读取的情况下,光靠数据长度判断是不可靠的,因为我们无法确保我们读到的指定长度的数据一定就是同一个完整数据,有可能参杂了上一次的数据或者下一次的数据,而一旦读取错一次,就意味着以后每次读取的数据都是错的。


增加结束符


为了解决上述方式导致的局限性,我们可以给每一帧数据增加一个结束符号,通常来说我们会规定 \r\n 即 CRLF (0x0D 0x0A)为结束符号。


所以,我们在读取数据时会循环读取,直至读取到结束符号,则我们认为本次读取结束,已经获得了一个完整的数据包:

val resultByte = mutableListOf<Byte>()
private fun getFullData(): ByteArray {
var isFindEnd = false

while (!isFindEnd) {
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, 2000)
if (readLen != 0) {
for (i in 0 until readLen) {
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) {
isFindEnd = true
}
}
}

return resultByte.toByteArray()
}

但是这个方法显然也有一个缺陷,那就是如果是单次间隔读取或者轮询时第一次读取数据有可能也是不完整的数据。


因为我们虽然读取到了结束符号,但是并不意味着这次读取的就是完整的数据,或许前面还有数据我们并没有读到。


不过这种方式可以确保轮询时只有第一次读取数据有可能不完整,但是后续的数据都是完整的。


只是单次间隔读取的话就无法保证读取到的是完整数据了。


在开头增加数据包长度


和增加结束符类似,我们也可以在数据包开头增加一个特殊字符,然后在后面紧跟着一个指定长度(1byte)字符指定接下来的数据包长度有多长。


这样,我们就可以在解析时首先查找这个开始符号,查找到之后则认为一个新的数据包开始了,然后读取之后 1byte 的字符,获取到这个数据包的长度,接下里按照这个这个指定长度,循环读取直到长度符合即可。


具体读取方式其实就是上面两种方式的结合,所以这里我就不贴代码了。


最好的情况


最方便的解决数据分包的方法当然是在数据中既包括固定数据头、固定数据尾、甚至连数据长度都是固定的。


例如某款温度传感器,发送的是数据格包为固定 10 位长度,且有结束符 CRLF,并且数据包开头有且只有 -+ (0x2B 0x2D 0x20)三种情况,那么我们在接收数据时就可以这么写:

val resultByte = mutableListOf<Byte>()
val READ_WAIT_MILLIS = 2000
private fun getFullData(count: Int = 0, dataSize: Int = 14): ByteArray {
var isFindStar = false
var isFindEnd = false
while (!isFindStar) { // 查找帧头
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
if (buffer.first() == 0x2B.toByte() || buffer.first() == 0x2D.toByte() || buffer.first() == 0x20.toByte()) {
isFindStar = true
for (i in 0 until readLen) { // 有帧头,把这次结果存入
resultByte.add(buffer[i])
}
}
}
}

while (!isFindEnd) { // 查找帧尾
val buffer = ByteArray(1024)
val readLen = usbSerialPort.read(buffer, READ_WAIT_MILLIS)
if (readLen != 0) {
for (i in 0 until readLen) { // 先把结果存入
resultByte.add(buffer[i])
}
if (buffer[readLen - 1] == 0x0A.toByte() && buffer.getOrNull(readLen - 2) == 0x0D.toByte()) { // 是帧尾, 结束查找
isFindEnd = true
}
}
}


// 判断数据长度是否符合
return if (resultByte.size == dataSize) {
resultByte.toByteArray()
} else {
if (count < 10) {
getFullData(count + 1, dataSize)
}
else {
return ByteArray(0)
}
}

粘包呢?


上面我们只说了分包情况,但是在实际使用过程中,还有可能会出现粘包的现象。


粘包,顾名思义就是不同的数据包在一次读取中混合到了一块。


如果想要解决粘包的问题也很简单,类似于解决分包,也是需要我们在定义协议时给出能够区分不同数据包的方式,这样我们按照协议解析即可。


总结


其实串口通信中的分包或者粘包解决起来并不难,问题主要在于串口通信一般都是每个硬件设备厂商或者传感器厂商自己定义一套通信协议,而有的厂商定义的协议比较“不考虑”实际,没有给出任何能够区分不同数据包的标志,这就会导致我们在接入这些设备时无法正常的解析出数据包。


但是也并不是说就没有办法去解析,而是需要我们具体情况具体分析,比如温度传感器,虽然通信协议中没有给出数据头、数据尾、数据长度等信息,但是其实它返回的数据格式几乎都是固定的,我们只要按照这个固定格式去解析即可。


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

Android 如何统一处理登录后携带数据跳转到目标页面

需求场景 我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法: 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验...
继续阅读 »

需求场景


我们在开发应用的时候经常会遇到先登录,登录成功后再跳转到目标页面。比如商品详情页面我们点击购买必须要先登录,登录完成才能去下单支付。针对这种场景,我们一般有两种做法:



  1. 点击购买跳转到登录,登录完成需要用户再次点击购买才能去下单支付页面,这种用户体验不是很好。

  2. 点击购买跳转到登录,登录完成直接跳转到下单支付页面。


第一种我们就不谈了产品经理不同意🐶。第二种我们一般是在 onActivityResult 里面获取到登录成功,然后根据 code 跳转到目标页面。这种方式缺点就是我们要在每个页面都处理相同的逻辑还有定义各种 code,如果应用里面很多这种场景也太繁琐了。那有没有统一的方式去处理这种场景就是我们今天的主题了。


封装方式


我们的应用是组件化的,APP 的页面跳转使用了 Arouter。所以我们统一处理使用 Arouter 封装。直接上代码

fun checkLoginToTarget(postcard: Postcard) {//Postcard 是 Arouter 的类
if (User.isLogin()) {
postcard.navigation()
} else {
//不能使用 postcard 切换 path 直接跳转,因为 group 可能不同,所以重新 build
ARouter.getInstance().build(Constant.LOGIN)
.with(postcard.extras)//获取携带的参数重新转入
.withString(Constant.TAGACTIVIFY, postcard.path)//添加目标路由
.navigation()
}
}
//登录成功后在登录页面执行这个方法
fun loginSuccess() {
val intent= intent
val target = intent.getStringExtra(Constant.TAGACTIVIFY)//获取目标路由
target?.apply {
if (isNotEmpty()){
val build = ARouter.getInstance().build(this)
val extras = intent.extras//获取携带的参数
if (extras != null) {
build.with(extras)
}
build.navigation()
}
}
finish()
}

代码加了注释,使用 Kotlin 封装了顶层函数,登录页面在登录成功后跳转到目标页面,针对上面的场景直接调用 checkLoginToTarget 方法。

checkLoginToTarget(ARouter.getInstance().build(Constant.PAY_PAGE).withInt(Constant.GOOD_ID,id))

通过 Arouter 传入下单支付的路由地址,并且携带了商品的 ID,生成了 Postcard 参数。登录成功后能带着商品 ID
直接下单支付了。


最后


如果项目里没有使用路由库可以使用 Intent 封装实现,或者别的路由库也可以用上面的方式去做统一处理。


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

揭开Android视图绘制的神秘面纱

揭开Android视图绘制的神秘面纱 在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android...
继续阅读 »

揭开Android视图绘制的神秘面纱


在Android的UI中,View是至关重要的一个组件,它是用户界面的基本构建块。在View的绘制过程中,涉及到很多重要的概念和技术。本文将详细介绍Android View的绘制过程,让你能够更好地理解和掌握Android的UI开发。


什么是View?


View是Android系统中的一个基本组件,它是用户界面上的一个矩形区域,可以用来展示文本、图片、按钮等等。View可以响应用户的交互事件,比如点击、滑动等等。在Android中,所有的UI组件都是继承自View类。


View的绘制过程


View的绘制过程可以分为三个阶段:测量、布局和绘制。下面我们将逐一介绍这三个阶段。


测量阶段(Measure)


测量阶段是View绘制过程的第一个重要阶段。在测量阶段,系统会调用View的onMeasure方法,测量View的宽度和高度。在这个过程中,系统会根据View的LayoutParams和父容器的大小来计算出View的大小。


例:下面代码是一个自定义View的onMeasure方法例程。在测量过程中,我们设定了View的大小。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取宽度的Size和Mode
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 如果Mode是精确的,直接返回
if (widthMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, heightMeasureSpec);
return;
}

// 计算View的宽度
int desiredWidth = getPaddingLeft() + getPaddingRight() + defaultWidth;
int measuredWidth;
if (desiredWidth < widthSize) {
measuredWidth = desiredWidth;
} else {
measuredWidth = widthSize;
}

// 设置宽度和高度的Size和Mode
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredHeight = defaultHeight;
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = Math.min(defaultHeight, heightSize);
}
setMeasuredDimension(measuredWidth, measuredHeight);
}

在测量阶段结束后,系统会将计算好的宽度和高度传递给布局阶段。


布局阶段(Layout)


布局阶段是View绘制过程的第二个重要阶段。在布局阶段,系统会调用View的onLayout方法,将View放置在父容器中的正确位置。在这个过程中,系统会根据View的LayoutParams和父容器的位置来确定View的位置。


例:下面代码是一个自定义ViewGroup的onLayout方法例程。在布局过程中,我们遍历子View,并根据LayoutParams确定子View的位置和大小。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getMeasuredWidth() - getPaddingRight();
int bottom = getMeasuredHeight() - getPaddingBottom();

for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}

LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = left + lp.leftMargin;
int childTop = top + lp.topMargin;
int childRight = right - lp.rightMargin;
int childBottom = bottom - lp.bottomMargin;
child.layout(childLeft, childTop, childRight, childBottom);
}
}

绘制阶段(Draw)


绘制阶段是View绘制过程的最后一个重要阶段。在绘制阶段,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


例:下面代码是一个自定义View的onDraw方法例程。在绘制过程中,我们使用Paint对象绘制了一段文本。

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//绘制文本
String text = "Hello World";
Paint paint = new Paint();
paint.setTextSize(50);
paint.setColor(Color.RED);
paint.setAntiAlias(true);
canvas.drawText(text, 0, getHeight() / 2, paint);
}

除了绘制内容,我们还可以在绘制阶段绘制View的背景和前景。系统会调用drawBackgrounddrawForeground方法来绘制背景和前景。值得注意的是,View的绘制顺序是:先绘制背景,再绘制内容,最后绘制前景。


View的绘制流程


View的绘制流程可以看作是一个递归调用的过程,下面我们将具体介绍这个过程。


Step 1:创建View


在View绘制过程的开始阶段,我们需要创建一个View对象,并将它添加到父容器中。在这个过程中,系统会调用View的构造函数,并将View的LayoutParams传递给它。


Step 2:测量View


接下来,系统会调用View的measure方法,测量View的宽度和高度。在这个过程中,View会根据自身的LayoutParams和父容器的大小来计算出自己的宽度和高度。


Step 3:布局View


在测量完成后,系统会调用View的layout方法,将View放置在父容器中的正确位置。在这个过程中,View会根据自身的LayoutParams和父容器的位置来确定自己的位置。


Step 4:绘制背景


在布局完成后,系统会调用View的drawBackground方法,绘制View的背景。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 5:绘制内容


接下来,系统会调用View的onDraw方法,绘制View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 6:绘制前景


在绘制内容完成后,系统会调用View的drawForeground方法,绘制View的前景。在这个过程中,我们同样可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 7:绘制子View


接着,系统会递归调用ViewGroup的dispatchDraw方法,绘制所有子View的内容。在这个过程中,我们可以使用Canvas对象来绘制各种形状、文本和图片等等。


Step 8:完成绘制


最后,所有的View绘制完成,整个View树也就绘制完成。


例:下面代码是一个自定义ViewGroup的绘制流程例程。在绘制过程中,我们先画背景,再绘制每个子View的内容。

public class MyViewGroup extends ViewGroup {

public MyViewGroup(Context context) {
super(context);
}

public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量子View的宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);

// 获取ViewGroup的宽高大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

// 设置ViewGroup的宽高
setMeasuredDimension(widthSize, heightSize);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 遍历所有子View,设置它们的位置和大小
int childCount = getChildCount();
int left, top, right, bottom;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
left = childView.getLeft();
top = childView.getTop();
right = childView.getRight();
bottom = childView.getBottom();
childView.layout(left, top, right, bottom);
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画背景
canvas.drawColor(Color.WHITE);
}

@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// 绘制每个子View的内容
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.draw(canvas);
}
}
}

在ViewGroup的绘制流程中,系统会先调用ViewGroup的draw方法,然后依次调用dispatchDraw方法和绘制每个子View的draw方法。ViewGroup的绘制顺序是先绘制自己的背景,再绘制每个子View的内容和背景,最后绘制自己的前景。


总结


本文详细介绍了Android View的绘制过程,包括测量阶段、布局阶段和绘制阶段。同时,我们还在代码实现的角度,详细说明了Android ViewGroup的绘制流程,帮助你更好地理解和掌握Android的UI开发。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


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

那些拿20k💰的大佬在职场都是怎么沟通的❓

☀️ 前言 大家好我是小卢,职场沟通是每个职场人必备的技能,但是如何提高职场沟通能力却是需要不断学习和实践。 下面就给大家带来四点方法能很大程度提升你在职场的沟通能力。 ⌨️ 了解每个人的沟通方式 每个人的个性、经验和教育背景都不同,这些因素都会影响到个...
继续阅读 »

☀️ 前言



  • 大家好我是小卢,职场沟通是每个职场人必备的技能,但是如何提高职场沟通能力却是需要不断学习和实践。

  • 下面就给大家带来四点方法能很大程度提升你在职场的沟通能力。


⌨️ 了解每个人的沟通方式



  • 每个人的个性、经验和教育背景都不同,这些因素都会影响到个人的沟通方式。有些人可能会喜欢直接表达自己的想法和意见,而有些人可能更倾向于暗示询问

  • 在实际职场生活中,我们不仅需要了解自己的沟通方式,还需要了解对方的沟通方式。简单举个例子吧:

  • 假设你是一个刚入公司不久的产品经理,你每周或者每月都需要给上司做一个工作汇报。

    • 你知道你的上司更喜欢使用图表和数据进行沟通,你可以准备好相关的数据和图表,并在开会过程中使用它们。这可以帮助你的上司更好地理解你的意思,并更快地进入到讨论的核心问题。

    • 你知道你的上司比较喜欢使用直接表达的方式来沟通,在这种情况下,直接表达你的想法和意见可能更为有效。你可以以明确的方式表达你对项目的看法,并解释你的看法背后的原因。



  • 在与他人交流时,我们需要时刻注意自己的语言、态度和非语言信号,并根据对方的反应进行调整。这需要一定的敏感度和经验,但是通过不断地练习和反思,我们可以逐渐提高自己的职场沟通能力,并取得更好的效果。


🤗 注意语速和语调



  • 职场沟通中,语速和语调是非常重要的因素,它们往往可以决定对方对你的印象和理解,如果你的语速太快或者语调不合适,很容易让对方感到困惑或者不舒服。

  • 除了注意自己的语速和语调,我们还需要注意对方的语速和语调。如果在某次交谈中你发现对方特别激动,说话特别快导致你不能全部理解,你可以说:“你说的内容非常重要,我来总结一下刚刚你分享的信息,看看是否符合预期,以便我能更好地理解你的意思?”

  • 这样的话语不仅能够有效地表达自己的需求,也能够尊重对方的沟通方式,让双方都能够更好地理解彼此。


👂 学会倾听



  • 职场沟通不仅仅是说话,更重要的是倾听。倾听意味着不仅是听别人说话,还包括尊重对方的意见和观点,关注对方的情感和态度,以及在适当的时候提出问题和反馈,以达到更好的沟通效果。

  • 要成为一个好的倾听者,我们需要全神贯注地聆听对方说话。这意味着不要分心,不要中途打断对方,而是要给对方充足的时间和空间来表达自己的想法和意见。如果你有不同的看法或者意见,可以先把它们记在脑海里,等对方表达完后再进行回应。

  • 在倾听的过程中,我们还需要注意对方的情绪和表情。通过观察对方的肢体语言和面部表情,我们可以更好地了解对方的真实意图和情感状态,从而更好地回应和理解对方的想法和需求,建立更好的信任和合作关系。

  • 举个例子吧:假设你是一个团队的领导,正在讨论下一步的项目计划。你发现其中一个成员很少发表意见,似乎对讨论不太感兴趣。你可以采取主动倾听的方式,问他对当前的计划有何看法,或者给他更具体的问题,以激发他的参与度。这样可以让他感受到自己的意见被认真听取,也有助于整个团队更好地理解和解决问题


👺 简明扼要



  • 简明扼要是职场中非常重要的一个点。当你需要向同事或客户提出需求时,最好提前思考好问题的前提条件、现状和问题的分支情况,一次性把问题说明白,尽量减少来回问答的次数,这样可以更有效地利用大家的时间和精力。

  • 为了让自己的观点更清晰地传达给别人,你可以先说出结论和重点,然后再说明为什么这么认为,并提供相关的事实依据。在接受问题或错误的指责时,也应该直接说明问题并找到解决办法,而不是遮掩或解释,以保证工作的顺利完成。

  • 我有一个同事在公司寻求大佬帮助的时候把前置说了很久,导致一直进入不到重点,别人根本不知道你想表达什么,这不仅浪费了别人的时间,还会让人对你产生厌恶。

  • 你可以简单干脆一点:这个问题导致了 xxx,影响了 xxx 的用户,他的原因是 xxx,我的想法是 xxx ,所以想问一下有没有更好的方案?


wallhaven-8o6rmo.jpeg


👋 写在最后



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

  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~

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

怎么算是在工作中负责?

作为打工人,受人之禄,忠人之事。但就像呼兰说的,躺有躺的价格,卷有卷的价格。身为程序员,我们在平时工作中要做到怎样才能算是“负责”了呢? 我们可以把工作边界和范围分为三部分: 个人基本能力 工作内容 工作时间 对自己的基本能力负责 基本能力包括两部分:1)...
继续阅读 »

作为打工人,受人之禄,忠人之事。但就像呼兰说的,躺有躺的价格,卷有卷的价格。身为程序员,我们在平时工作中要做到怎样才能算是“负责”了呢?


我们可以把工作边界和范围分为三部分:



  • 个人基本能力

  • 工作内容

  • 工作时间


对自己的基本能力负责


基本能力包括两部分:1)技术能力,2)熟悉公司系统的能力。


程序员是一个非常需要持续学习的职业,我们在实际工作中,遇到自己不会的问题在所难免,这时可以向别人请教,但是千万不要觉得请教完就没事儿了,我们需要思考复盘自己为什么不会,要想办法补齐自己的知识和技能短板。


我们学的东西一定要在实际工作中使用,这样才能够激发学习的积极性,同时验证自己的学习成果。当公司准备技术升级或者技术转型时,这也是我们为自己的技能升级的好机会。


很多公司都会有自己的内部系统,熟练掌握和使用这些系统,也是我们需要做到的,它的价值在于,内部系统一般都是和公司的整个监控系统集成好的,例如公司内部的SOA框架或者微服务框架,都是和公司内部的监控系统有集成的,即使这个框架再“不好”,公司内部的项目还是会使用,我们需要让自己站得高一些,去看待内部系统在整个公司级别的作用和地位,这样才能更好地发挥自己的技术能力。


对安排的工作负责


程序员职业的特殊性在于,工作本身的具体内容和难度,会随着被安排的工作内容的改变而改变。从对工作负责的角度来说,我们大部分人会付出比当初预想的更多的时间,才能让自己按时完成工作。


如果一件事情的复杂度远远超过之前的预估,在规定的时间内,自己确实无法完成,这时正确的态度不是硬着头皮上,而是将情况理清楚,早点找经理或者负责人,让他们知道事情的进度和之前没有预想到的难度,把事情重新安排一下。


从管理者的角度来看,一件事情安排的不合理,就应该早发现,早计划,重新安排资源。


对工作时间负责


对工作时间负责,是说最好在“实际上班”时间之前到,避免有人找你却找不到的情况。


这不只是为了保证工作时间,而是想强调程序员的工作不止是写代码,还有很多沟通交流的事情,要保证基本的工作时间,才能更有效的和团队交流,确保我们的工作的价值。


对于项目和团队安排的各种会议,要准时参加,如果不能参加,需要提前告知经理或者会议组织者,避免浪费大家的事情。


总之,我们工作中的责任是一点点增加的,负责任的态度和习惯,也是从平时工作中一件件事情中养成的。形成这样的习惯,成为一个受人信任的人,是我们在职场中要培养的重要品质。



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

何不食肉糜?

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。 视频见这里:http://www.bilibili.com/video/BV1Bb… 当时我印象非常...
继续阅读 »

21年的时候,微博上有过一番口诛笔伐,就是就是管清友建议刚开始工作的年轻人就近租房不要把时间浪费在上班的路上,要把时间利用起来投资自己,远比省下的房租划算。


视频见这里:http://www.bilibili.com/video/BV1Bb…



当时我印象非常深刻,微博评论是清一色的 “何不食肉糜”,或者说“房租你付?”


可能是因为这件事情的刺激,管清友后来才就有了“我特别知道年轻人建议专家不要建议”的言论。


对还是错?


在我看来,管清友的这个建议可以说是掏心掏肺,非常真诚,他在视频里也说了,他是基于很多实际案例才说的这些话,不是说教。


为什么我这么肯定呢?


很简单,我就是代表案例。


我第一家公司在浦东陆家嘴,四号线浦东大道地铁站旁边,我当时来上海的时候身无分文,借的家里的钱过来的,我是贫困家庭。


但,为了节约时间,我就在公司附近居住,步行五分钟,洗头洗澡都是中午回住的地方完成,晚上几乎都是11:00之后回去,倒头就睡,因为时间可以充分利用。


节约的时间,我就来研究前端技术,写代码,写文章,做项目,做业务,之前的小册(免费章节,可直接访问)我也提过,有兴趣的可以去看看。


现在回过头来看那段岁月,那是充满了感激和庆幸,自己绝对做了一个非常正确的决定,让自己的职业发展后劲十足。


所以,当看到管清友建议就近租房的建议,我是非常有共鸣的,可惜世界是参差的,管清友忽略了一个事实,那就是优秀的人毕竟是少数,知道如何主动投资自己的人也是凤毛麟角,他们根本就无法理解。


又或者,有些人知道应该要投资自己,但是就是做不到,毕竟辛苦劳累,何苦呢,做人,不就是应该开心快乐吗?


说句不好听的,有些人的时间注定就是不值钱的。


工作积极,时间长是种优势?


一周前,我写了篇文章,谈对“前端已死”的看法,其中提到了“团队下班最晚,工作最积极”可以作为亮点写在简历里。


结果有人笑出了声。



好巧的是,管清友的租房建议也有人笑了,出没出声并不知道。



也有人回复“何不食肉糜”。


这有些出乎我的意料,我只是陈述一个简单的事实,却触动了很多人的敏感神经。


我突然意识到,有些人可能有一个巨大的认知误区,就是认为工作时长和工作效率是负相关的,也就是那些按时下班的是效率高,下班晚的反而是能力不足,因为代码不熟,bug太多。



雷军说你说的很有道理,我称为“劳模”是因为我工作能力不行。


你的leader也认为你说的对,之前就是因为我每天准时下班,证明了自己的能力,所以自己才晋升的。


另外一个认知误区在于,把事实陈述当作目标指引。


如果你工作积极,是那种为自己而工作的人,你就在简历中体现,多么正常的建议,就好比,如果你是北大毕业的,那你就在简历中体现,没任何问题吧。


我可没有说让你去拼工作时长,装作工作积极,就好比我没有让你考北大一样。


你就不是这种类型的人,对吧,你连感同身受都做不到,激动个什么呢,还一大波人跟着喊666。


当然,我也理解大家的情绪,我还没毕业的时候,也在黑心企业待过,钱少事多尽煞笔,区别在于,我相对自驱力和自学能力强一些,通过自己的努力跳出了这个循环。


但大多数人还是被工作和生活推着走,所以对加班和内卷深恶痛绝,让本就辛苦的人生愈发艰难,而这种加班和内卷并没有带来收入的提升。


那问题来了,有人通过努力奋斗蒸蒸日上,有人的辛苦努力原地踏步,同样的,有的人看到建议觉得非常有用,有的人看到建议觉得何不食肉糜,区别在哪里呢?


究竟是资本作恶呢?还是自己能力不足呢?


那还要建议吗?


管清友不再打算给年轻人建议了,我觉得没必要。


虽然,大多数时候,那些听得进去建议的人大多不需要建议,而真正需要建议的又听不进,但是,那么多年轻人,总有一部分潜力股,有一些真正需要帮助的人。


他们可能因为环境等原因,有短暂的迷茫与不安,但是,来自前人发自真心的建议或许可以让他们坚定自己前进方向,从而走出不一样的人生。


就像当年我被乔布斯的那些话语激励过那般。


所以,嘲笑之人任其笑之,只要能帮助到部分人,那就有了价值。


因此,我不会停止给出那些我认为对于成长非常有帮助的建议。


(完)


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

聊聊我在阿里第一年375&晋升的心得

前言 思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。 绩效 首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。 很多同学都知道阿里的绩效是361制度,也就是30%...
继续阅读 »

前言


思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。


绩效


首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。


很多同学都知道阿里的绩效是361制度,也就是30%的人拿A,60%的人拿B,10%的人拿C,不过在阿里,我们一般不用ABC来表示,除去价值观考核,我们常用的表达是3.75、3.5、3.25,初略的对应关系如下:

361通用阿里
30%A3.75
60%B3.5 ±
10%C3.25

那么,了解了阿里的绩效制度,再来看看绩效里面的门道。


首先,讲一个职场潜规则,「团队的新人是用来背绩效的」,也就是会把差绩效(325)指标分配给新人,因为如果把325给老人,容易产生团队不稳定的因素,而且不得不承认的是,少一个老人比少一个新人的成本更大,另一方面,你作为新人,业务不熟,根基不稳,也不见得能产出多大的价值,所以对于多数新人来说,也只能接受。据我所知,只有极少的公司里面会有「绩效保护」这种政策,而且一般还是针对的应届生,社招来说,还是要看能力的。


其次,基于潜规则,大部分新人都在为保住3.5做努力,只有少数优秀的人可以拿到更好的绩效。而新人拿375在阿里是很少的存在,即使是老人,拿375都是非常不容易的,何况是一个新人。


最后,就是晋升,晋升的前提条件就是满一年且是好绩效,加上现在降本增效的大环境,有的要求连续两年375才行,甚至都不一定有名额,当然,晋升也和团队绩效和级别有关系,但总之,男上加男,凤毛麟角。


个人背景


我是21年8月份入职阿里的,2022年是在阿里的第一个整财年。


之前一直是在小厂摸爬滚打,没有大厂经历,这对于我来说,是劣势,写代码不注重安全性防护,没有code review,没有ab test,没有自动化测试,没有文档沉淀,没有技术分享,纯纯小作坊。更重要的是,小厂和大厂的做事风格流程什么的,真的是千差万别,所以当时的landing对我来说,还是很难受的。但是,我有没有自带的优势呢,也有,写作能力,但是光有写作能力还是不够的,你没东西可写也不行啊。


其实试用期结束之后就差不多进入新的财年了,对于刚刚进入状态的我,也迎来了更大的挑战。过去的一整年有较多的精力都投入在研发效能和安全生产方面,这对于以前纯业务开发的我来说,完全是一个新的领域,好在不负重托,也略有成果。


其实回想过去一年的经历来看,今天的成绩是多维度的结合,比如硬实力和软实力、个人和团队、内部和外部等多个维度,下面将分别介绍一些我个人的心得经验,仅供参考。


沟通能力


这也用说?不就是说话吗,谁不会说?


我看过很多的简历,如果有「自我评价」,几乎都会提到「具备良好的沟通能力」。
可是沟通能力看起来真的有那么简单吗?我认为不是的,实际上我甚至认为它有点难。


在职场经常会有这些问题:



  1. 这个点我没太理解,你能在解释一下吗?

  2. 为什么要这么做?为什么不这么做?

  3. 现在有什么问题?

  4. 你的诉求是什么?

  5. 讲话的时候经常被打断等等...


这些问题你是不是被问到过,或者这么问别人呢。


而这些问题的背后,则反映了沟通的不完整和不够清晰。面对他人的挑战,或向跨部门的同学讲述目标价值时,会沟通的同学会显的游刃有余,而不会沟通的同学则显得捉襟见肘。


沟通方面,其实也包含很多场景。


首先是逻辑要清晰。


面对用户的一线同事,比如销售和客服,他们都是有话术的,话术就是沟通的技巧。


为什么脸皮薄/不懂拒绝的人容易被销售忽悠?


因为销售在跟客户沟通的时候,就是有一套完整的话术,他先讲一,看你反应再决定讲二还是三;当你拒绝了A方案,他还有B方案C方案。一套完整的话术逻辑把你都囊括在里面,最后只能跟着他的思维走,这就是话术的意义。


同样的,在职场,你跟人沟通的时候,不能直说怎么做,解决方案是什么,而背景和问题同样重要,同时讲述问题的时候要尽可能的拆解清楚,避免遗漏,这样不只是让别人更理解你的意思,其实有时候换个视角,解决方案可能有更优的。


逻辑清晰,方案完善,对方就会处于一个比较舒服的状态,有时候能起到事半功倍的效果。你可能会觉得有些麻烦,但如果因为没有表达清楚而导致最后的结果不符合预期,孰轻孰重,应该拎得清的吧?


其次是要分人。


我在之前的面经中提到,自我介绍要分人,技术面试官和HR的关注点一定是不一样的,面对这种不同的出发点,你讲的东西肯定不能是同一套,要有侧重点,你跟HR讲你用了什么牛逼的技术、原理是什么,那不是瞎扯嘛。


这个逻辑在职场也是一样的,你和同事讨论技术、向领导汇报、回答领导的领导问题、跟产品、运营、跨部门沟通,甚至出现故障的时候给客满提供的话术,面对不同的角色、不同的场合,表达出来的意思一定是要经过「翻译」的,多换位思考。


即要把自己的意思传达到,也要让对方get到,这才是有效沟通。


所谓沟通能力,不只是有表达,还要有倾听。


倾听方面,比如当别人给你讲一个事情的时候,你能不能快速理解,能不能get到对方的点,这也很重要。
快速且高效,这是一个双向的过程。这里面会涉及到理解能力,而能理解的基础是基于现有的认知,也就是过往的工作经验、项目经历和知识面,这是一个积累的过程。


当然,也有可能是对方表达的不够清楚,这时候就要不耻下问,把事情搞清楚,搞不清楚就会有不确定性,就是有风险,如果最终的结果不符合预期,那么复盘的时候,委屈你一下,不过分吧😅。


最后是沟通媒介。


我们工作沟通的场景一般都是基于钉钉、微信、飞书之类的即时通讯办公平台,文字表达的好处是,它可以留档记录,方便后期翻阅查看,但是也一定程度在沟通表达的传递上,存在不高效的情况。


那这时候,语音、电话就上场了,如果你发现文字沟通比较费劲的时候,那一定不如直接讲来的更高效。


但是语音、电话就一定能聊的清楚吗,也不见得。


“聊不清楚就当面聊”,为什么当面聊就能比语音、电话聊的清楚?因为当面聊,不只是可以听到语气看到表情肢体动作,更重要的是当面聊的时候,双方一定是专注在这个事情上的,不像语音的时候还可以干其他的事,文字甚至会长时间已读不回,所以讲不清楚的时候,当面聊的效果是最好的。为了弥补留档的缺陷,完事再来个文字版的纪要同步出来,就ok了。


其他。


上面提到逻辑要清晰,要分人,还有倾听能力,和善用沟通媒介。


其实沟通里还包括反应能力,当你被突然问到一个问题的时候,能不能快速流畅的表达清楚,这个也很关键,如果你支支吾吾,反反复复的都没有说清楚,设想一下,其他人会怎么看你,“这人是不是不行啊?”,长此以往,这个信任度就会降低,而一旦打上标签,你就需要很多努力才能证明回来。


还包括争辩能力,比如在故障复盘的时候,能不能有效脱身不被拉下水,能不能大事化小小事化了,也都是沟通的技巧,限于篇幅,不展开了。


学会复盘


复盘是什么?


复盘是棋类术语,指对局完毕后,复演该盘棋的记录,以检查对局中招法的优劣与得失关键。在工作中,复盘是通过外在的表象、客观的结果找出本质,形成成功经验或失败教训,并应用到其他类似事件中,提升面向未来的能力。


所以,复盘不是流水账的记录经过和简单总结,也不是为了表彰罗列成绩,更不是为了甩锅而相互扯皮。找出本质的同时一定要有思考,这个思考可以体现在后续的一些执行事项和未来规划上,总之,就是要让「复盘」变的有意义。


什么是有意义的复盘?


就是你通过这次复盘,能知道哪些错误是不能再犯的,哪些正确的事是可以坚持去做的。会有一种「再来一次结果肯定不一样」的想法,通过有意义的复盘让「不完美」趋向「完美」。


我个人复盘的三个阶段:



  • 回顾:回顾目标、经过、结果的整个流程;

  • 分析:通过主观、客观的视角分析,失败的原因是什么,成功的原因是什么;

  • 转化:把成功经验和失败教训形成方法论,应用到类似事件中;


Owner意识


什么是owner意识?


简单讲就是主人翁精神。认真负责是工作的底线,积极主动是「owner意识」更高一级的要求。


如果你就是怀着只干活的心态,或者躺平的心态,换我是领导,也不认为你能把活做好。因为这种心态就是「做了就行,能用就行」,但有owner意识不一样,这种人做事的时候就会多思考一点、多做一点,这里面最主要的就是主动性,举个栗子,好比写代码,当你想让程序运行的更好的时候,你就会多关注代码编写的鲁棒性,而不是随便写写完成功能就行。


但人性自私,责任感也不是人人都有,更别提主动性了,所以这两种心态的人其实差别很大,有owner意识的人除了本职工作能做好之外,在涉及到跨团队的情况,也能主动打破边界去push,有责任心有担当,往往能受到团队内外的一致好评。


在其位谋其职,我其实并没有特意去固化自己的owner意识,就是怀着一个要把事情做好的心态,跟我个人性格也有关系,现在回想起来,不过是水到渠成而已。



卷不一定就有owner意识,不卷也不代表没有。



向上管理


这个其实我一开始做的并不好,甚至可以说是很差,小公司出身哪需要什么向上管理,活干好就行。


但是现在不一样了,刚入职比较大的一个感受就是,我老板(领导)其实并不太过问我的工作内容,只是偶尔问一下进度。


然而这个「问」,其实也能反应出一些过往不太在意的问题:



  1. 没有及时汇报,等到老板来问的时候其实处于一个被动的局面了,虽然也不会有什么太大的影响,可能多数人也都是这样,但是这不正说明我不够出众吗?

  2. 不确定性,什么进度?有没有遇到问题?这些都是不确定性,老板不喜欢“惊喜”,有困难要说,有风险要提前暴,该有结果的时候没有,老板也很被动,你会留下一个什么印象?


当然,向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道。不要只提问题找老板要解决方案,我会简述问题,评估影响面,还会给出解决方案的Plan A和Plan B,这样老板就是做选择题了,即使方案不够完美,老板指点一下不正是学习的好机会吗。


学会写TODO


为什么写todo?


写todo的习惯其实是在上家公司养成的,因为要写周报,如果不记录每天做了什么,写周报的时候就会时不时的出现遗漏的情况。除了记录当天所做的事情之外,我还列了第二天要做的事情。虽然一直有给自己定规划的习惯,但是颗粒度都没有这么细过。彼时的todo习惯,不仅知道当天做了什么,还规划了第二天做什么,时刻有目标,也不觉得迷茫。


进入阿里之后,虽然没有周报月报这种东西,但是这个习惯也一直保持着,在此之上,我还做了一些改良。



  1. 优先级,公司体量一旦大起来之后,业务线就很多,随之而来的事情就很多,我个人对处理工作是非常反感多线程并发的,特别是需要思考的工作,虽然能并行完成,但完成度不如专注在一件事情上的好,但是有些事情又确实得优先处理,所以就只能根据事情的紧急程度排一下优先级,基本很少有一件事需要从早到晚都投入在里面的,所以抽出身来就可以处理下一件事,所以也不会出现耽误其他事情的情况,当然线上故障除外。

  2. 消失的时间,因为真的是事情又多又杂,时常在下班的时候会有一种瞎忙的感觉,就是「忙了一天,好像什么都没干」,但又确实很忙很累,仿佛陷入一个怪圈。所以后来我就把颗粒度拆的更细,精确到小时,也不是几点钟该做什么,就是把做的事情记录下来,并备注一下投入的时间,比如上午排障答疑投入了两小时,这样到中午吃饭的时候也不至于上午就这样虚度了。让“消失的时间”有迹可循,治愈精神内耗。

  3. 归纳总结,我现在是在语雀以月度为单位记录每天的todo,这样就可以进行月度的总结和复盘,在半年度的OKR总结的时候,还有了归纳总结的来源,有经过、有结果、还有时间线,一目了然,再也不用为写总结发愁了。


总之,写todo的好处除了记录做了什么、要做什么,它还能辅助你把工作安排的更好。


有规划有目标,也不会陷入一个迷茫虚度的状态,属于一个成本非常低而收益却很高的好习惯,不止工作,学习也是如此,生活亦然。


其他方面


适应能力


于我个人来说,工作强度比以前要大很多,慢慢习惯了就行,在大厂里面阿里还不是最卷的,但钱也不是最多的;工作流程方面只是有些不清楚而已,并没有什么门槛,熟悉了就行;还有阿里味儿,确实要学很多新词儿、缩写、甚至潜台词,这没啥说的,还没见到有能独善其身的😅。


适应能力也不是随便说说,有太多的人入职新公司干的怀疑人生、浑身难受而跑路的,抛开公司的问题不说,难道就没有自己的问题吗?🐶


我把适应分为两个阶段,第一个阶段就是适应工作环境,熟悉公司、同事、产品、项目;第二个阶段就是适应之后,要想想如何迎接没有新手光环的挑战,如何去展示自己、提升自己等。


技术能力


夯实自己的技术能力尤为重要,毕竟是吃饭的家伙,是做事拿结果的重要工具和手段。


在大家技术能力的基本面都不会太差的情况下,如何在技术上建立团队影响力,就是需要思考的问题。


要找准自己在团队中的定位,并在这一领域深耕,做到一提这个技术就能想到你的效果,建立技术壁垒。


其实也不只是技术,要学会举一反三,找到自己在团队的差异性,虽然不可替代性在公司离了谁都可以转的说法下有些牵强,但是可以提高自己在团队的重要性和信任度。


信息渠道


要学会拓宽自己的信息渠道,有句话叫,掌握的信息越多,决策就越正确



  • 多看,看别人在做什么,输出什么,规划什么;

  • 多聊,找合作方,相同目标的同事,甚至其他公司的朋友,互通有无;


看完聊完要把对自己有用的信息提炼出来哦。


影响力


内部的话,主要是建立同事间的信任,技术的占比相对要多一些;


外部的话,主要是在合作方那里建立一个靠谱的口碑,如果配合超预期那就更好了,我就是被几个大佬抬了一手,虽然不起决定性作用,但是也很重要。


摆脱固化


跳脱出程序员的固化思维


程序的世界非0即1,程序员的思维都是非常严谨的,这个严谨有时候可能会显得有些“死板”,在商业化的公司里面,很多事情不是能不能的问题,而是要不要的问题。而在这里面,技术大多数都不是第一要素,出发点得是业务视角、用户视角,很多技术难点、卡点,有时候甚至不用技术也能解决。


小结



  • 沟通能力:逻辑要清晰,对象要分人,还有倾听能力,和善用沟通媒介;

  • owner意识:认真负责是工作的底线,积极主动是「owner意识」更高一级的要求;

  • 向上管理:向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道;

  • 写TODO:辅助工作,治愈内耗,一个成本低而收益高的好习惯;

  • 其他方面:拓宽信息渠道,找到技术方向,简历内部外部的影响力等;



实际上不止这些,今天就到这吧。



最后


当下的市场环境无论是求职还是晋升,都挺难的,都在降本增效,寒气逼人,我能拿到晋升的门票,诚然是实力的体现,但也有运气的成分。没晋升也不一定是你的问题,放平心态,当下保住工作才是最重要的。


哔哔了这么多,可能很多同学道理也都懂,估计就难在知行合一吧...


最后送给大家一句罗翔老师的经典名言:



人生最大的痛苦,就是你知道什么是对的,但却永远做出错误的选择,知道和做到这个巨大的鸿沟,你永远无法跨越。


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

协程和协程作用域

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。 1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。 2、父协程通过调用协程构建器(如 launch、async)来启动子...
继续阅读 »

理清子协程,父协程,协程作用域,协程生命周期,协程作用域的生命周期等的关系。


1、协程是在协程作用域内执行的轻量级并发单元。当协程的代码块执行完成时,它会挂起并返回到其父协程或顶层协程中。




2、父协程通过调用协程构建器(如 launchasync)来启动子协程。在启动子协程时,子协程会继承父协程的上下文(包括调度器、异常处理器等)。这意味着子协程会以与父协程相同的上下文执行。父协程可以通过 join() 方法等待子协程执行完成,以确保子协程的结果可用。


父协程可以通过取消操作来取消子协程。当父协程被取消时,它会递归地取消所有的子协程。子协程会接收到取消事件,并根据取消策略来决定如何处理取消。


父协程和子协程之间的关系可以帮助管理协程的层次结构和生命周期。通过父协程启动和取消子协程,可以有效地组织和控制协程的执行流程,实现更灵活和可靠的协程编程。

fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

parentJob.join()
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#3 childJob start
main @coroutine#3 childJob complete
main @coroutine#2 parentJob complete
main @coroutine#1 parentJob cancel
Process finished with exit code 0
fun main() {
runBlocking {
val parentJob = launch {

val childJob = launch {
printMsg("childJob start")
delay(500)
printMsg("childJob complete")
}

childJob.join()
printMsg("parentJob complete")

}

//parentJob.join() <----------变化在这里
parentJob.cancel()
printMsg("parentJob cancel")
}
}

//日志
main @coroutine#1 parentJob cancel
Process finished with exit code 0



3、协程作用域(CoroutineScope)是用于协程的上下文环境,它提供了协程的启动和取消操作的上下文。协程作用域定义了协程的生命周期,并决定了协程在何时启动、在何时取消。


协程作用域是一个接口,定义了两个主要方法:




  • launch:用于启动一个新的协程。launch 方法会创建一个新的协程,并将其添加到当前协程作用域中。启动的协程将继承父协程的上下文,并在协程作用域内执行。




  • cancel:用于取消协程作用域中的所有协程。cancel 方法会发送一个取消事件到协程作用域中的所有协程,使它们退出执行。




协程作用域与协程之间的关系是协程在协程作用域内执行的。协程作用域为协程提供了上下文环境,使得协程可以访问到必要的上下文信息,例如调度器(Dispatcher)和异常处理器(ExceptionHandler)。通过在协程作用域中启动协程,可以确保协程的生命周期受到协程作用域的管理,并且在协程作用域取消时,所有协程都会被取消。

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch(Job()) { <---------协程2不使用协程作用域的上下文,会脱离协程作用域的控制
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed 1685615335423
main @coroutine#1 Coroutine scope completed 1685615335424 <-------协程1执行完,协程作用域就执行完
Process finished with exit code 0 <---------程序退出



4、如果使用 GlobalScope.launch 创建协程,则协程会成为全局协程,它的生命周期独立于程序的其他部分。当协程的代码执行完毕后,全局协程不会自动退出,除非应用程序本身退出。因此,全局协程可以在整个应用程序的生命周期内持续执行,直到应用程序终止。


如果使用协程作用域(例如 runBlockingcoroutineScope 等)创建协程,则协程的生命周期受协程作用域的限制。当协程的代码执行完毕后,它会返回到作用域的父协程或顶级协程中,而不会自动退出。




5、当协程作用域内的所有协程执行完成后,协程作用域仍然存在,但其中的协程会被标记为完成状态。这意味着协程作用域仍然可以用于启动新的协程,但之前的协程不会再执行。


协程作用域的生命周期不仅仅依赖于其中的协程执行状态,还取决于其父协程或顶级协程的生命周期。如果协程作用域的父协程或顶级协程被取消或完成,那么协程作用域也将被取消

fun main() = runBlocking {
coroutineScope {
launch {
delay(1000)
printMsg("Coroutine 1 completed")
}

launch {
delay(2000)
printMsg("Coroutine 2 completed")
}
}
printMsg("Coroutine scope completed")
}

//日志
main @coroutine#2 Coroutine 1 completed
main @coroutine#3 Coroutine 2 completed
main @coroutine#1 Coroutine scope completed
Process finished with exit code 0 <---------程序退出

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

一个96年前端的2022年中总结 (落户,看房,还贷,被裁)

人到中年中年总是觉得很累,每天忙不完的事,操不完的心,曾今那些无忧无虑的日子似乎消失的无影无踪, 工作三年一点存款没有 落户 关于落户这个事, 就是一个很突然的想法,很突然, 得知天津有个"海河英才计划",只要是本科生, 就比较容易落户,所以就想着试一试, ...
继续阅读 »


人到中年中年总是觉得很累,每天忙不完的事,操不完的心,曾今那些无忧无虑的日子似乎消失的无影无踪, 工作三年一点存款没有



落户


关于落户这个事, 就是一个很突然的想法,很突然, 得知天津有个"海河英才计划",只要是本科生, 就比较容易落户,所以就想着试一试, 心想起码这个是一个阶级的跨越(农村-->城市), 然后就在网上各种搜索,问一些了解的朋友,看看怎么操作, 最后发现好多人说有北京社保,在天津落户会被查出来, 然后就被拉入黑名单了. 最后为了稳妥还是找了一个中介帮忙操作了一下,花了1w, 等我办完,不久就有我的朋友自己办的三无人员落户, 一分钱没花. 唉😌!!!!!


看房


一开始在安居客上看, 因为穷, 基本也没啥可选择性, 一筛选也就那么几个, 然后就联系了一个销售去看房, 不得不说这个销售真的可以, 不知道他们能赚多少钱哈, 但是服务是真的不错, 一到了高铁站他们就去接你,还请你吃饭, 带着你看,看完之后还把你送到高铁站. 想着一开始就看西青和北辰的,但是西青的都好贵, 北辰好像会好点, 然后就去北辰看, 销售说他觉得武清也不错, 就带着去武清也看了, 看完之后, 心里比较了一下, 感觉被武清的哪个样板间和户型深深吸引了,并且价格也比较合适些, 然后我回到北京一周左右吧, 就跟那个销售说,打算买武清哪个, 然后然后我父母给我拿了20w, 剩下的就是我自己的,还借了朋友一些, 首付了40多, 然后就打印征信, 一顿签字就买了. 感觉很随意, 感觉买房就跟买菜似的, 不过还是有一点区别的, 买菜之后不会让我身无分文, 买房会😭. 现在就是每个月5000多贷款"真爽".


image.png


学习



  1. 缺失了刚毕业那会的激情,刚毕业那会,每天下班还会去学习, 刷视频, 看文章, 现在下班回家已经不想再打开电脑了. 刷视频(此视频非彼视频)

  2. 今年也学习了一些新的东西摸鱼之间,刷了一些课程,<破解JavaScript高级玩法,成为精通JS的原生专家> <Vue3全家桶>

  3. React技术栈是我工作一直使用的,也会持续性的学习一些,每天刷刷Medium和掘金

  4. 深度学习Nginx,进行了一半了,以前对nginx只停留在使用的层面

  5. 上半年在公司分享了一下架手架的原理以及实践

  6. 英语的学习说实话真的有点三天打鱼两天晒网了, 好在现在有一半了


img_1.png


工作


21年年底, 老东家北京这边合作的项目,终止了,然后面临了裁员, 不过当初也确实有了想跳槽的想法了,本来想,等到年终奖发了,就提离职. 没想到提前到来了, 给了正常的赔偿(n+1) 拿了三个月的赔偿, 正好月底, 算上本月的薪资,还有一些调休啥的 加起来一共是4个月, 感觉还挺爽的, 因为大概还有个20天左右吧, 就快过年了, 然后在回家和找工作抉择了一下, 决定先找找工作,然后就开始学习在掘金上查看面经, 感觉都是各种源码, 给我搞的有点懵, 毕竟缺钱嘛. 先后面试了一些公司: 金山, 58, 携程, 欢聚, 等等; 说实话,这段时间招人的还挺多的. 所以我很快就入职了, hr问我年前能入职吗? 其实那段时间疫情严重了, 老家那边也不让北京的回去, 所以在过年的前一周我入职了, (没钱的人不配拥有假期😭) . 唉, 第一次自己在外地过年.


关于兼职:



  • 今年和朋友一块干了一个公司的官网,本来也没打算要钱的,最后老板一人给了一张京东e卡

  • 还干了一个审核ppt的工作,一个ppt给150,不用改, 就说哪里写的不合适, 不过这个活有点恶心, 每个人理解不一样, 每次我这边审核后, 拿去交付,还是很多问题


生活



  1. 上半年感觉一直有疫情断断续续,大家都比较封闭,也没出去玩过, 偶尔和朋友去爬个山. 5, 6月还居家办公了好久, 记不太清了,应该得有一个月

  2. 从去年十月一到现在一直没有回过家, 有些想回去看看, 但是最近每个周末都有事😞

  3. 和女朋友去看过两次脱口秀, 感觉现场的感觉还是很棒的, 比电视看好太多

  4. 因为对象住他们单位的宿舍, 所以我自己平常下班也懒得收拾屋子, 只有礼拜天,才会大扫除一下, 或者对象来的时候😁

  5. 前段时间迷上了王者,以前从来不怎么玩游戏的, 熬夜打游戏, 导致生活节奏有点乱, 每天的精神状态也不如以前,正在积极调整. 但是吧,我告诉你们我的云中君玩的贼6的 不服来战哦

  6. 养了一只鹦鹉, 刚来还不是很好看, 现在尾巴长长的了


image.png


下半年flag



  • 完成Nginx的深度学习

  • 希望能出去转一圈, 看看外面的世界

  • 继续背单词学习

  • 看看车车, 目前感觉Crv和宋大妈还不错

  • 能再进行一次有价值的分享


作者:nanfeiyan
来源:juejin.cn/post/7124511406099005471
收起阅读 »

箭头函数函数是否有原型

web
问题:箭头函数是否有原型 今天在博客重构之余,看到某个前端群有群友这样问: 面试被问到了一个题,箭头函数是否有原型 大家觉得有吗? 首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。 箭头函数是什么 箭头函数表达式的语法比函数表达式更简...
继续阅读 »

问题:箭头函数是否有原型


今天在博客重构之余,看到某个前端群有群友这样问:



面试被问到了一个题,箭头函数是否有原型
大家觉得有吗?



首先不说它是不是,我们来回顾一下 箭头函数 是什么,原型 又是什么。


箭头函数是什么



箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。



developer.mozilla.org/zh-CN/docs/…



引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。



一般来说如果问题是函数是否有原型时,那么可以好不犹豫的回答说是,但因为箭头函数的特殊性导致了答案的不确定性。


原型是什么



当谈到继承时,JavaScript 只有一种结构:对象。
每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。
原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null。
根据定义,null 没有原型,并作为这个原型链(prototype chain)中的最后一个环节。



developer.mozilla.org/zh-CN/docs/…



一个 Function 对象在使用 new 运算符来作为构造函数时,会用到它的 prototype 属性。它将成为新对象的原型。



developer.mozilla.org/zh-CN/docs/…


什么是原型在 mdn 中已经讲得很清楚了,也就是对象的prototype


试验


所以按理来说对象都有原型,那么试试就知道了。


首先看看箭头函数的原型:


const a = () => {};

console.log(a.prototype); // undefined

在浏览器控制台可以把上述代码执行一遍,可以发现结果是 undefined。


那么是否就说明箭头函数没有原型了呢?别急继续往下看。


const a = () => {};

console.log(a.prototype); // undefined

console.log(a.__proto__); // ƒ () { [native code] }

我们可以看到 a.__proto__ 是一个 native function


那么 __proto__ 又是什么呢?


__proto__是什么



Object.prototype (en-US) 的 __proto__ 属性是一个访问器属性(一个 getter 函数和一个 setter 函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。




__proto__ 的读取器 (getter) 暴露了一个对象的内部 [[Prototype]] 。对于使用对象字面量创建的对象,这个值是 Object.prototype (en-US)。对于使用数组字面量创建的对象,这个值是 Array.prototype。对于 functions,这个值是 Function.prototype。对于使用 new fun 创建的对象,其中 fun 是由 js 提供的内建构造器函数之一 (Array, Boolean, Date, Number, Object, String 等等),这个值总是 fun.prototype。对于用 JS 定义的其他 JS 构造器函数创建的对象,这个值就是该构造器函数的 prototype 属性。




__proto__ 属性是 Object.prototype (en-US) 一个简单的访问器属性,其中包含了 get(获取)和 set(设置)的方法,任何一个 __proto__ 的存取属性都继承于 Object.prototype (en-US),但一个访问属性如果不是来源于 Object.prototype (en-US) 就不拥有 __proto__ 属性,譬如一个元素设置了其他的 __proto__ 属性在 Object.prototype (en-US) 之前,将会覆盖原有的 Object.prototype (en-US)。



developer.mozilla.org/zh-CN/docs/…


看解释可能有人不大理解,举个例子 🌰:


function F() {}
const f = new F();

console.log(f.prototype); // undefined
console.log(f.__proto__ === F.prototype); // true
console.log(f.__proto__.constructor); // F(){}

new F 也即是 F 的实例 ff__proto__ 属性指向 Fprototype


由此可以得出实例与原型的关系:new function得到实例,实例的 __proto__ 又指向function的原型,原型的构造器指向原函数。


结论


好了,理解了什么是 __proto__ 后,我们回到原来的问题上:箭头函数函数是否有原型?


通过上述的代码我们可以知道箭头函数就是Function的实例,如果你觉得不是,那么请看下面的例子:


const a = () => {};

console.log(a instanceof Function); // true
console.log(a.__proto__ === Function.prototype); // true
console.log(a.prototype); // undefined

所以最终得出两种结论:如果按中文语意那么箭头函数是Function的实例,而依据实例与原型的关系,它是有原型的;如果原型仅仅只说的是prototype,那么结论就是没有



注:以上代码结果都是在chrome113下运行得出



如有错误,欢迎指正~


参考


箭头函数
developer.mozilla.org/zh-CN/docs/…


构造器
developer.mozilla.org/zh-CN/docs/…


原型链
developer.mozilla.org/zh-CN/docs/…


Function.prototype
developer.mozilla.org/zh-CN/docs/…


__proto__
developer.mozilla.org/zh-CN/

docs/…

收起阅读 »

关于晋升的一点思考

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。 晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。 总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,...
继续阅读 »

晋升是一个极好的自我review的机会,不,应该是最好,而且没有之一。

晋升是最好的自我review的一次机会,不管有没有晋升成功,只要参加了晋升,认真准备过PPT,就已经包赚不赔了。


总的来说,晋升的准备工作充分体现出了——功夫在平时。平时要是没有两把刷子,光靠答辩准备的一两个月,是绝无可能把自己“包装”成一个合格的候选人的。

下面整体剖析一下自己在整个准备过程中的观察、思考、判断、以及做的事情和拿到的结果。


准备工作


我做的第一件事情并不是动手写PPT,而是搜集信息,花了几天在家把网上能找到的所有关于晋升答辩的文章和资料全撸了一遍,做了梳理和总结。


明确了以下几点:



  • 晋升是在做什么

  • 评委在看什么

  • 候选人要准备什么

  • 评判的标准是什么

  • 常见的坑有哪些


首先要建立起来的,是自己对整个晋升的理解,形成自己的判断。后面才好开展正式的工作。



写PPT


然后开始进入漫长而又煎熬的PPT准备期,PPT的准备又分为四个子过程,并且会不断迭代进行。写成伪代码就是下面这个样子。


do {
确认思路框架;
填充内容细节;
模拟答辩;
获取意见并判断是否还需要修改;
} while(你觉得还没定稿);

我的PPT迭代了n版,来来回回折腾了很多次,思路骨架改了4次,其中后面三次均是在准备的后半段完成的,而最后一次结构大改是在最后一周完成的。这让我深深的觉得前面准备的1个月很多都是无用功。


迭代,迭代,还是迭代


在筹备的过程中,有一个理念是我坚持和期望达到的,这个原则就是OODA loop ( Boyd cycle)


OODA循环是美军在空战中发展出来的对敌理论,以美军空军上校 John Boyd 为首的飞行员在空战中驾驶速度慢火力差的F-86军刀,以1:10的击落比完胜性能火力俱佳的苏联米格-15。而Boyd上校总结的结论就是,不是要绝对速度快,而是要比对手更快的完成OODA循环


而所谓的OODA循环,就是指 observe(观察)–orient(定位)–decide(决策)–act(执行) 的循环,是不是很熟悉,这不就是互联网的快速迭代的思想雏形嘛。


相关阅读 what is OODA loop

wiki.mbalib.com/wiki/包以德循环 (from 智库百科)

en.wikipedia.org/wiki/OODA_l… (from Wikipedia)


看看下图,PPT应该像第二排那样迭代,先把框架确定下来,然后找老板或其他有经验的人对焦,框架确定了以后再填充细节。如果一开始填充细节(像第一排那样),那么很有可能越改越乱,最后一刻还在改PPT。


btw,这套理论对日常工作生活中的大部分事情都适用。


一个信息论的最新研究成果


我发现,程序员(也有可能是大部分人)有一个倾向,就是show肌肉。明明很简单明了的事情,非要搞得搞深莫测,明明清晰简洁的架构,非要画成“豆腐宴”。


晋升述职核心就在做一件事,把我牛逼的经历告诉评委,并让他们相信我牛逼。


所以,我应该把各种牛逼的东西都堆到PPT里,甚至把那些其实一般的东西包装的很牛逼,没错吧?


错。


这里面起到关键作用的是 “让他们相信我牛逼” ,而不是“把我牛逼的故事告诉评委”。简单的增大的输出功率是不够的,我要确保评委能听进去并且听懂我说的东西,先保证听众能有效接收,再在此基础上,让听众听的爽。


How?


公式:喜欢 = 熟悉 + 意外


从信息论的角度来看,上面的公式说的就是旧信息和新信息之间要搭配起来。那么这个搭配的配比应该是多少呢?


这个配比是15.87% ——《科学美国人》


也就是说,你的内容要有85%是别人熟悉的,另外15%是能让别人意外的,这样就能达到最佳的学习/理解效果。这同样适用于心流、游戏设计、神经网络训练。所以,拿捏好这个度,别把你的PPT弄的太高端(不知所云),也别搞的太土味(不过尔尔)。


能够否定自己,是一种能力


我审视自己的时候发现,很多时候,我还保留一张PPT或是还持续的花心思做一件事情,仅仅是因为——舍不得。你有没有发现,我们的大脑很容易陷入“逻辑自洽”中,然后越想越对,越想越兴奋。


千万记得,沉没成本不是成本,经济学里成本的定义是放弃了的最大价值,它是一个面向未来的概念,不面向过去。


能够否定和推翻自己,不留恋于过去的“成就” ,可以帮助你做出更明智的决策。


我一开始对好几页PPT依依不舍,觉得自己做的特牛逼。但是后来,这些PPT全被我删了,因为它们只顾着自己牛逼,但是对整体的价值却不大,甚至拖沓。


Punchline


Punchline很重要,这点我觉得做的好的人都自觉或不自觉的做到了。想想,当你吧啦吧啦讲的时候,评委很容易掉线的,如果你没有一些点睛之笔来高亮你的成果和亮点的话,别人可能就糊里糊涂的听完了。然后呢,他只能通过不断的问问题来挖掘你的亮点了。


练习演讲


经过几番迭代以后,PPT可以基本定稿,这个时候就进入下一个步骤,试讲。


可以说,演讲几乎是所有一线程序员的短板,很多码农兄弟们陪电脑睡的多了,连“人话”有时候都讲不利索了。我想这都要怪Linus Torvalds的那句


Talk is cheap. Show me the code.


我个人的经验看来,虽然成为演讲大师长路漫漫不可及,但初级的演讲技巧其实是一个可以快速习得的技能,找到几个关键点,花几天时间好好练几遍就可以了,演讲要注意的点主要就是三方面:



  • 形象(肢体语言、着装等)

  • 声音(语速、语调、音量等)

  • 文字(逻辑、关键点等)



演讲这块,我其实也不算擅长,我把仅有的库存拿出来分享。


牢记表达的初衷


我们演讲表达,本质上是一个一对多的通信过程,核心的目标是让评委或听众能尽可能多的接受到我们传达的信息


很多程序员同学不善于表达,最明显的表现就是,我们只管吧啦吧啦的把自己想说的话说完,而完全不关心听众是否听进去了。


讲内容太多


述职汇报是一个提炼的过程,你可能做了很多事情,但是最终只会挑选一两件最有代表性的事情来展现你的能力。有些同学,生怕不能体现自己的又快又猛又持久,在PPT里塞了太多东西,然后又讲不完,所以只能提高语速,或者囫囵吞枣、草草了事。


如果能牢记表达的初衷,就不应该讲太多东西,因为听众接收信息的带宽是有限的,超出接收能力的部分,只会转化成噪声,反而为你的表达减分。


过度粉饰或浮夸


为了彰显自己的过人之处,有时候会自觉或不自觉的把不是你的工作也表达出来,并没有表明哪些是自己做的,哪些是别人做的。一旦被评委识破(他本身了解,或问问题给问出来了),那将会让你陈述的可信度大打折扣。


此外,也表达的时候也不要过分的浮夸或张扬,一定的抑扬顿挫是加分的,但过度浮夸会让人反感。


注意衔接


作为一个演讲者,演讲的逻辑一定要非常非常清晰,让别人能很清晰明了的get到你的核心思路。所以在演讲的时候要注意上下文之间的衔接,给听众建设心理预期:我大概会讲什么,围绕着什么展开,分为几个部分等等。为什么我要强调这个点呢,因为我们在演讲的时候,很容易忽略听众的感受,我自己心里有清楚的逻辑,但是表达的时候却很混乱,让人一脸懵逼。


热情


在讲述功能或亮点的时候,需要拿出自己的热情和兴奋,只有激动人心的演讲,才能抓住听众。还记得上面那个分布图吗?形象和声音的占比达到93%,也就是说,你自信满满、热情洋溢的说“吃葡萄不吐葡萄皮”,也能打动听众。


第一印象


这个大家都知道,就是人在最初形成的印象会对以后的评价产生影响 。

这是人脑在百万年进化后的机制,可以帮助大脑快速判断风险和节省能耗——《思考,快与慢》

评委会刻意避免,但是人是拗不过基因的,前五分钟至关重要,有经验的评委听5分钟就能判断候选人的水平,一定要想办法show出你的与众不同。可以靠你精心排版的PPT,也可以靠你清晰的演讲,甚至可以靠一些小 trick(切勿生搬硬套)。


准备问题


当PPT准备完,演讲也练好了以后,不出意外的话,应该没几天了。这个时候要进入最核心关键的环节,准备问题。


关于Q&A环节,我的判断是,PPT和演讲大家都会精心准备,发挥正常的话都不会太差。这就好像高考里的语文,拉不开差距,顶多也就十几分吧。而Q&A环节,则是理综,优秀的和糟糕的能拉开50分的差距,直接决定总分。千万千万不可掉以轻心。


问题准备我包含了这几个模块:



  • 业务:业务方向,业务规划,核心业务的理解,你做的事情和业务的关系,B类C类的差异等

  • 技术:技术难点,技术亮点,技术选型,技术方案的细节,技术规划,代码等

  • 数据:核心的业务数据,核心的技术指标,数据反映了什么等等

  • 团队:项目管理经验,团队管理经验

  • 个人:个人特色,个人规划,自己的反思等等


其中业务、技术和数据这三块是最重要的,需要花80%的精力去准备。我问题准备大概花了3天时间,整体还是比较紧张的。准备问题的时候,明显的感觉到自己平时的知识储备还不太够,对大业务方向的思考还不透彻,对某些技术细节的把控也还不够到位。老话怎么说的来着,书到用时方恨少,事非经过不知难。


准备问题需要全面,不能有系统性的遗漏。比如缺少了业务理解或竞品分析等。


在回答问题上,也有一些要点需要注意:


听清楚再回答


问题回答的环节,很多人会紧张,特别是一两道问题回答的不够好,或气氛比较尴尬的时候,容易大脑短路。这个时候,评委反复问你一个问题或不断追问,而自己却觉得“我说的很清楚了呀,他还没明白吗”。我见过或听说过很多这样的案例,所以这应该是时有发生的。


为了避免自己也踩坑,我给自己定下了要求,一定要听清楚问题,特别是问题背后的问题。如果觉得不清楚,就反问评委进行doubel check。并且在回答的过程中,要关注评委的反映,去确认自己是否答到点子上了。


问题背后的问题


评委的问题不是天马行空瞎问的,问题的背后是在考察候选人的某项素质,通过问题来验证或挖掘候选人的亮点。这些考察的点都是公开的,在Job Model上都有。


我认为一个优秀的候选人,应当能识别出评委想考察你的点。找到问题背后的问题,再展开回答,效果会比单纯的挤牙膏来的好。


逻辑自洽、简洁明了


一个好的回答应该是逻辑自洽的。这里我用逻辑自洽,其实想说的是你的答案不一定要完全“正确”(其实往往也没有标准答案),但是一定不能自相矛盾,不能有明显的逻辑漏洞。大部分时候,评委不是在追求正确答案,而是在考察你有没有自己的思考和见解。当然,这种思考和见解几乎都是靠平时积累出来的,很难临时抱佛脚。


此外,当你把逻辑捋顺了以后,简洁明了的讲出来就好了,我个人是非常喜欢能把复杂问题变简单的人的。一个问题的本质是什么,核心在那里,关键的几点是什么,前置条件和依赖是什么,需要用什么手段和资源去解决。当你把这些东西条分缕析的讲明白以后,不用再多啰嗦一句,任何人都能看出你的牛逼了。


其他


心态调整


我的心态经历过过山车般的起伏,可以看到



在最痛苦最难受的时候,如果身边有个人能理解你陪伴你,即使他们帮不上什么忙,也是莫大的宽慰。如果没有这样的人,那只能学会自己拥抱自己,自己激励自己了。


所以,平时对自己的亲人好一点,对朋友们好一点,他们绝对是你人生里最大的财富。


关于评委


我从一开始就一直觉得评委是对手,是来挑战你的,对你的汇报进行证伪。我一直把晋升答辩当作一场battle来看待,直到进入考场的那一刻,我还在心理暗示,go and fight with ths giants。


但真实的经历以后,感觉评委更多的时候并不是站在你的对立面。评委试图通过面试找到你的一些闪光点,从而论证你有能力晋升到下一个level。从这个角度来讲,评委不但不是“敌人”,更像是友军一般,给你输送弹药(话题)。


一些教训




  • 一定要给自己设置deadline,并严格执行它。如果自我push的能力不强,就把你的deadline公开出来,让老板帮你监督。




  • 自己先有思考和判断,再广开言路,不要让自己的头脑成为别人思想的跑马场。




  • 坚持OODA,前期千万不要扣细节。这个时候老板和同事是你的资源,尽管去打扰他们吧,后面也就是一两顿饭的事情。




附件


前期调研



参考文章


知乎


作者:酒红
来源:juejin.cn/post/7240805459288162360
收起阅读 »

程序员创业:从技术到商业的转变

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。 为什么程...
继续阅读 »

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。


为什么程序员要创业?


创业其实并非只适用于商学院的毕业生或者有创新理念的企业家。程序员在业内有着相当高的技术储备和市场先知,因此更容易从技术角度前瞻和切入新兴市场,更好地利用技术储备来实现创业梦想。


此外,创业可以释放我们的潜力,同时也可以让我们找到自己的定位和方向。在创业的过程中,我们可能会遇到各种挑战和困难,但这些挑战也将锻炼我们的意志力和决策能力,让我们更好地发挥自己的潜力。


创业需要具备的技能


作为一名技术人员,创业需要具备更多的技能。首先是商业和运营的技能:包括市场分析、用户研究、产品策划、项目管理等。其次是团队管理和沟通能力,在创业的过程中,人才的招聘和管理是核心问题。


另外,还需要具备跨界合作的能力,通过开放性的合作与交流,借助不同团队的技术和资源,完成创业项目。所以我们应该将跨界合作看作是创业过程中的重要选择,选择和加强自己的跨界交流和合作能力,也能为我们的企业注入活力和创新精神。


如何创业?


从技术到商业的转变,从最初想法的诞生到成熟的企业的创立,都需要一个创业的路线图。以下是一些需要注意的事项:




  1. 研究市场:了解市场趋势,分析需求,制定产品策略。可以去参加行业论坛,争取到专业意见和帮助。




  2. 制定商业计划:包括产品方案、市场营销、项目管理、团队建设等。制定一个系统的商业计划是投资者和团队成员对创业企业的认可。




  3. 招募团队:由于我们一般不是经验丰富的企业家,团队的选择尤为重要。要找的不仅要是技能和经验匹配的团队,更要找能一起携手完成创业项目的合作者。




  4. 行动计划:从实现规划步入到实战行动是创业项目的关键。按部就班地完成阶段性任务,控制实施进度和途中变化,在完成一个阶段后可以重新评估计划。




  5. 完成任务并分析:最后,团队成员需要根据企业进展,完整阶段性的目标,做自己的工作。及时完成考核任务并一起分享数据分析、事件解决和项目总结等信息,为项目下一阶段做出准确预测。




结语


创业是一条充满挑战性和机遇的路线,也是在我们的技术和业务的进一步升级中一条非常良好的通道。越来越多的技术人员意识到了自己的潜力,开始考虑自己创业的可能性。只要学会逐步掌握创业所需的技能和知识,并制订出详细的创业路线图,大可放手去尝试,才能最终实现

作者:郝学胜
来源:juejin.cn/post/7240465997002047547
自己心中的创业梦想。

收起阅读 »

App高级感营造之 高斯模糊

效果 类似毛玻璃,或者马赛克的效果。我们可以用它来提升app背景的整体质感,或者给关键信息打码。 源代码 import 'dart:ui'; import 'package:flutter/material.dart'; void main() { ...
继续阅读 »

效果


类似毛玻璃,或者马赛克的效果。我们可以用它来提升app背景的整体质感,或者给关键信息打码。


高斯模糊1.gif


高斯模糊2.gif


源代码


import 'dart:ui';

import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
// 高斯模糊的第一种写法 ImageFiltered 包裹要模糊的组件

/// 将子组件进行高斯模糊
/// [child] 要模糊的子组件
Widget _imageFilteredWidget1({required Widget child, double sigmaValue = 1}) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: sigmaValue, sigmaY: sigmaValue),
child: child,
);
}

/// 使用第一种模糊方式的案例
Widget _demo1() {
return Container(
padding: const EdgeInsets.all(50),
color: Colors.blue.shade100,
width: double.infinity,
child: Column(
children: [
_imageFilteredWidget1(
child: SizedBox(
width: 150,
child: Image.asset(
"assets/images/bz1.jpg",
fit: BoxFit.fitHeight,
),
),
),
const SizedBox(height: 100),
_imageFilteredWidget1(
child: const Text(
"测试高斯模糊",
style: TextStyle(fontSize: 30, color: Colors.blueAccent),
),
sigmaValue: 2)
],
),
);
}

/// 利用 BackdropFilter 做高斯模糊
_backdropFilterWidget2({
required Widget child,
double sigmaValueX = 1,
double sigmaValueY = 1,
}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigmaValueX, sigmaY: sigmaValueY),
child: child,
),
);
}

///
Widget _demo2() {
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Image.asset(
"assets/images/bz1.jpg",
fit: BoxFit.fill,
),
),
Positioned(
child: _backdropFilterWidget2(
sigmaValueX: _sigmaValueX,
sigmaValueY: _sigmaValueY,
child: Container(
width: MediaQuery.of(context).size.width - 100,
height: MediaQuery.of(context).size.height / 2,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x90ffffff),
),
child: const Text(
"高斯模糊",
style: TextStyle(fontSize: 30, color: Colors.white),
),
)),
top: 20,
),
_slider(
bottomMargin: 200,
themeColors: Colors.yellow,
title: '横向模糊度',
valueAttr: _sigmaValueX,
onChange: (double value) {
setState(() {
_sigmaValueX = value;
});
},
),
_slider(
bottomMargin: 160,
themeColors: Colors.blue,
title: '纵向模糊度',
valueAttr: _sigmaValueY,
onChange: (double value) {
setState(() {
_sigmaValueY = value;
});
},
),
_slider(
bottomMargin: 120,
themeColors: Colors.green,
title: '同时调整:',
valueAttr: _sigmaValue,
onChange: (double value) {
setState(() {
_sigmaValue = value;
_sigmaValueX = value;
_sigmaValueY = value;
});
},
),
],
),
);
}

Widget _slider({
required String title,
required double bottomMargin,
required Color themeColors,
required double valueAttr,
required ValueChanged<double>? onChange,
}) {
return Positioned(
bottom: bottomMargin,
child: Row(
children: [
Text(title, style: TextStyle(color: themeColors, fontSize: 18)),
SliderTheme(
data: SliderThemeData(
trackHeight: 20,
activeTrackColor: themeColors.withOpacity(.7),
thumbColor: themeColors,
inactiveTrackColor: themeColors.withOpacity(.4)
),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: Slider(
value: valueAttr,
min: 0,
max: 10,
onChanged: onChange,
),
),
),
SizedBox(
width: 50,
child: Text('${valueAttr.round()}',
style: TextStyle(color: themeColors, fontSize: 18)),
),
],
),
);
}

double _sigmaValueX = 10;
double _sigmaValueY = 10;

double _sigmaValue = 10;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _demo2(),
);
}
}

实现原理


实现高斯模糊,在flutter中有两种方式:


ImageFiltered


它可以对其包裹的子组件施加高斯模糊,需要传入 ImageFilter 控制模糊程度,分为X Y两个方向的模糊,实际上就是对图片进行拉伸,数字越大,模糊效果越大。


/// 将子组件进行高斯模糊
/// [child] 要模糊的子组件
Widget _imageFilteredWidget1({required Widget child, double sigmaValue = 1}) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: sigmaValue, sigmaY: sigmaValue),
child: child,
);
}

BackDropFilter


同样需要一个 ImageFilter参数控制模糊度,与 ImageFilter的区别是,它会对它覆盖的组件整体模糊。
所以如果我们需要对指定的子组件进行模糊的话,需要再包裹一个ClipRect裁切。


/// 利用  BackdropFilter 做高斯模糊
_backdropFilterWidget2({required Widget child, double sigmaValue = 1}) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaY: sigmaValue, sigmaX: sigmaValue),
child: child,
),
);
}

由于 BackdropFilter 会对其子组件进行图形处理,所以其子组件可能会变得更加消耗性能。因此,需要谨慎使用 BackdropFilter 组件。


作者:拳布离手
来源:juejin.cn/post/7239631010429108280
收起阅读 »

不用递归也能实现深拷贝

web
前言 在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。 深...
继续阅读 »

前言


在现代化的 Web 开发中,深拷贝是一个常见的数据处理需求,它允许我们复制并操作数据,而不影响原始数据。然而,使用递归实现深拷贝的方法可能对性能产生负面影响,特别是在处理大规模数据时。因此,越来越多的前端开发者开始关注另一种不用递归的方式实现深拷贝。


深拷贝的实现方式


我们先来看看常用的深拷贝的实现方式


JSON.parse(JSON.stringify())


利用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 把字符串解析成新的对象实现深拷贝。


这种方式代码简单,常用于深拷贝简单类型的对象。


在复杂类型的对象上会有问题:



  1. undefined、function、symbol 会被忽略或者转为 null(数组中)

  2. 时间对象变成了字符串

  3. RegExp、Error 对象序列化的结果将只得到空对象

  4. NaN、Infinity 和-Infinity,则序列化的结果会变成 null

  5. 对象中存在循环引用的情况也无法正确实现深拷贝


函数库 lodash 的 cloneDeep 方法


这种方式使用简单,而且 cloneDeep 内部是使用递归方式实现深拷贝,因此不会有 JSON 转换方式的问题;但是需要引入函数库 js,为了一个函数而引入一个库总感觉不划算。


递归方法


声明一个函数,函数中变量对象或数组,值为基本数据类型赋值到新对象中,值为对象或数组就调用自身函数。


// 手写深拷贝
function deepCopy(data) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp",
};
var copyData;
var type = map[Object.prototype.toString.call(data)];
if (type === "array") {
copyData = [];
data.forEach((item) => copyData.push(deepCopy(item)));
} else if (type === "object") {
copyData = {};
for (var key in data) {
copyData[key] = deepCopy(data[key]);
}
} else {
copyData = data;
}
return copyData;
}

递归方式结构清晰将任务拆分成多个简单的小任务执行,可读性强,但是效率低,调用栈可能会溢出,函数每次调用都会在内存栈中分配空间,而每个进程的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致溢出。


深拷贝其实是对树的遍历过程


嵌套对象很像下面图中的树。


Untitled.png


递归的思路是遍历 1 对象的属性判断是否是对象,发现属性 2 是一个对象在调用函数本身来遍历 2 对象的属性是否是对象如此反复知道变量 9 对象。


Untitled 1.png


9 对象的属性中没有对象然后返回 5 对象去遍历其他属性是否是对象,没有再返回 2 对象,最后返回到 1 对象发现其 3 属性是一个对象。


Untitled 2.png


Untitled 3.png


最后在找 4 对象。


Untitled 4.png


可以看到递归其实是对树的深度优先遍历。


那么不用递归可以实现树的深度优先遍历么?


答案是肯定的。


不用递归实现深度优先遍历深拷贝


观察递归算法可以发现实现深度优先遍历主要是两个点



  1. 利用栈来实现深度优先遍历的节点顺序

  2. 记录哪些节点已经走过了


第一点可以用数组来实现栈


const stack = [source]
while (stack.length) {
const data = stack.pop()
for (let key in data) {
if (typeof source[key] === "object") {
stack.push(data[key])
}
}
}

这样就能把所有的嵌套对象都放入栈中,就可以遍历所有的嵌套子对象。


第二点因为发现对象属性值是对象时会中断当前对象的属性遍历改去遍历子对象,因此要记录对象的遍历的状态。由于 for in 的遍历是无序的即使用一个变量存 key 也没办法知道哪些 key 已经遍历过了,需要一个数组记录所有遍历过的属性。


这里还有另一种简单的方法就是用 Object.keys 来获取对象的 key 数组放到 stack 栈中。


const stack = [...Object.keys(source).map(key => ({ key, source: source }))]
while (stack.length) {
const { key, data } = stack.pop()
if (typeof data[key] === "object") {
stack.push(...Object.keys(data[key]).map(k => ({ key: k, data: data[key] })))
}
}

这样 stack 中深度优先遍历的遍历的对象顺序也记录其中。


这里将代码优化下, 把 Object.keys 换成 Object.entries 更为精简


const stack = [...Object.entries(source)]
while (stack.length) {
const [ key, value ] = stack.pop()
if (typeof value === "object") {
stack.push(...Object.entries(value))
}
}

遍历完成下一步就是创建一个新的对象进行赋值。


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = {}
let id = 0
let cache
while (stack.length) {
const [key, value, id] = stack.pop()
if (id != undefined && cacheMap[id]) {
cache = cacheMap[id]
} else {
cache = result
}
if (typeof value === "object") {
cacheMap[id] = cache[key] = {}
stack.push(...Object.entries(value).map(item => [...item, id++]))
} else {
cache[key] = value
}
}
return result

因为对象时引用类型,因此可以通过 cacheMap[id] 来快速访问 result 的嵌套对象。


代码还可以优化:


cacheMap 可以用 WeakMap 来声明减少 id 的声明:


const stack = [...Object.entries(source)]
const result = {}
const cacheMap = new WeakMap()
let cache
while (stack.length) {
const [key, value, parent] = stack.pop()
if (cacheMap.has(parent)) {
cache = cacheMap.get(parent)
} else {
cache = result
}
if (typeof value === "object") {
cache[key] = {}
cacheMap.set(value, cache[key])
stack.push(...Object.entries(value).map(item => [...item, value]))
} else {
cache[key] = value
}
}
return result

stack 中的数组项中的 parent 可以换成目标对象:


const result = {}
const stack = [...Object.entries(source).map(item => [...item, result])]
while (stack.length) {
const [key, value, target] = stack.pop()
if (typeof value === "object") {
target[key] = {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result

加上数组的判断最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = Array.isArray(source) ? [] : {}
const stack = [...Object.entries(source).map(item => [...item, result])]
const toString = Object.prototype.toString
while (stack.length) {
const [key, value, target] = stack.pop()
if (map[toString.call(value)] === 'object' || map[toString.call(value)] === 'array') {
target[key] = Array.isArray(value) ? [] : {}
stack.push(...Object.entries(value).map(item => [...item, target[key]]))
} else {
target[key] = value
}
}
return result
}

console.log(cloneDeep({ a: 1, b: '12' }))
//{ a: 1, b: '12' }
console.log(cloneDeep([{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]))
//[{ a: 1, b: '12' }, { a: 2, b: '12' }, { a: 3, b: '12' }]

广度优先遍历实现深拷贝


同样的思路,实现深拷贝的最终代码为:


function cloneDeep(source) {
const map = {
"[object Number]": "number",
"[object Boolean]": "boolean",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Object]": "object",
"[object Null]": "null",
"[object Undefined]": "undefined",
"[object Date]": "date",
"[object RegExp]": "regexp"
}
const result = {}
const stack = [{ data: source, target: result }]
const toString = Object.prototype.toString
while (stack.length) {
let { target, data } = stack.unshift()
for (let key in data) {
if (map[toString.call(data[key])] === 'object' || map[toString.call(data[key])] === 'array') {
target[key] = Array.isArray(data[key]) ? [] : {}
stack.push({ data: data[key], target: target[key] })
} else {
target[key] = data[key]
}
}
}
return result
}

作者:千空
来源:juejin.cn/post/7238978371689136185
收起阅读 »

慢慢的喜欢上泛型 之前确实冷落了

前言 下图 CSDN 水印 为自身博客 什么泛型 通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可...
继续阅读 »

前言


下图 CSDN 水印 为自身博客


什么泛型



通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,同时,还可以提高代码的可读性和安全性。



泛型带来的好处



在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的



public class GlmapperGeneric<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }

public static void main(String[] args) {
// do nothing
}

/**
* 不指定类型
*/

public void noSpecifyType(){
GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 需要强制类型转换
String test = (String) glmapperGeneric.get();
System.out.println(test);
}

/**
* 指定类型
*/

public void specifyType(){
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 不需要强制类型转换
String test = glmapperGeneric.get();
System.out.println(test);
}
}



上面这段代码中的 specifyType 方法中 省去了强制转换,可以在编译时候检查类型安全,可以用在类,方法,接口上。



泛型中通配符



我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢?



常用的 T,E,K,V,?



本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:




  • ?表示不确定的 java 类型

  • T (type) 表示具体的一个java类型

  • K V (key value) 分别代表java键值中的Key Value

  • E (element) 代表Element

  • < T > 等同于 < T extends Object>

  • < ? > 等同于 < ? extends Object>


?无界通配符



先从一个小例子看起:



// 范围较广
static int countLegs (List<? extends Animal > animals ) {
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}
// 范围定死
static int countLegs1 (List< Animal > animals ){
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}

public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
// 不会报错
countLegs( dogs );
// 报错
countLegs1(dogs);
}



对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。



上界通配符 < ? extends E>



上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:



1.如果传入的类型不是 E 或者 E 的子类,编译不成功
2. 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用


List<? extends Number> eList = null;
eList = new ArrayList<Integer>();
//语句1取出Number(或者Number子类)对象直接赋值给Number类型的变量是符合java规范的。
Number numObject = eList.get(0); //语句1,正确

//语句2取出Number(或者Number子类)对象直接赋值给Integer类型(Number子类)的变量是不符合java规范的。
Integer intObject = eList.get(0); //语句2,错误

//List<? extends Number>eList不能够确定实例化对象的具体类型,因此无法add具体对象至列表中,可能的实例化对象如下。
eList.add(new Integer(1)); //语句3,错误

下界通配符 < ? super E>



下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object



在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。


List<? super Integer> sList = null;
sList = new ArrayList<Number>();

//List<? super Integer> 无法确定sList中存放的对象的具体类型,因此sList.get获取的值存在不确定性
//,子类对象的引用无法赋值给兄弟类的引用,父类对象的引用无法赋值给子类的引用,因此语句错误
Number numObj = sList.get(0); //语句1,错误

//Type mismatch: cannot convert from capture#6-of ? super Integer to Integer
Integer intObj = sList.get(0); //语句2,错误
//子类对象的引用可以赋值给父类对象的引用,因此语句正确。
sList.add(new Integer(1)); //语句3,正确

1. 限定通配符总是包括自己
2. 上界类型通配符:add方法受限
3. 下界类型通配符:get方法受限
4. 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
5. 如果你想把对象写入一个数据结构里,
6. 使用 ? super 通配符 如果你既想存,又想取,那就别用通配符
7. 不能同时声明泛型通配符上界和下界


?和 T 的区别


// 指定集合元素只能是T类型
List<T> list = new ArrayList<T>();
// 集合元素可以是任意类型的,这种是 没有意义的 一般是方法中只是为了说明用法
List<?> list = new Arraylist<?>();


?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :



// 可以
T t = operate();

// 不可以
?car = operate();


T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。



区别1 通过 T 来 确保 泛型参数的一致性


   public <T extends Number> void test1(List<T> dest, List<T> src) {
System.out.println();
}

public static void main(String[] args) {
test test = new test();
// integer 是number 的子类 所以是正确的
List<Integer> list = new ArrayList<Integer>();
List<Integer> list1 = new ArrayList<Integer>();
test.test1(list,list1);
}

在这里插入图片描述



通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型



public void
test(List<? extends Number> dest, List<? extends Number> src)

GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();
List<String> dest = new ArrayList<>();
List<Number> src = new ArrayList<>();
glmapperGeneric.testNon(dest,src);
//上面的代码在编译器并不会报错,但是当进入到 testNon 方法内部操作时(比如赋值),对于 dest 和 src 而言,就还是需要进行类型转换

区别2:类型参数可以多重限定而通配符不行



使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定



区别3:通配符可以使用超类限定而类型参数不行



类型参数 T 只具有 一种 类型限定方式



T extends A



但是通配符 ? 可以进行 两种限定



? extends A
? super A

Class和 Class<?>区别


Class<"T"> (默认没有双引号 系统会自动把T给我换成特殊字符才加的引号) 在实例化的时候,T 要替换成具体类。Class<?>它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况


作者:进阶的派大星
来源:juejin.cn/post/7140472064577634341
收起阅读 »

Vue 为什么要禁用 undefined?

web
Halo Word!大家好,我是大家的林语冰(挨踢版)~ 今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined? 敏感话题 我们会讨论几个敏感话题,包括但不限于—— 测不准的 undefined 如何引发复合 BUG? 薛定谔的...
继续阅读 »

Halo Word!大家好,我是大家的林语冰(挨踢版)~


今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined




敏感话题


我们会讨论几个敏感话题,包括但不限于——



  1. 测不准的 undefined 如何引发复合 BUG?

  2. 薛定谔的 undefined 如何造成二义性?

  3. 未定义的 undefined 为何语义不明?


懂得都懂,不懂关注,日后再说~




1. 测不准的 undefined 如何引发复合 BUG?


一般而言,开源项目对 undefined 的使用有两种保守方案:



  • 禁欲系——能且仅能节制地使用 undefined

  • 绝育系——禁用 undefined


举个粒子,Vue 源码就选择用魔法打败魔法——安排黑科技 void 0 重构 undefined


vue-void.png


事实上,直接使用 undefined 也问题不大,毕竟 undefined 表面上还是比较有安全感的。


readonly-desc.gif


猫眼可见,undefined 是一个鲁棒只读的属性,表面上相当靠谱。


虽然 undefined 自己问题不大,但最大的问题在于使用不慎可能会出 BUG。undefined 到底可能整出什么幺蛾子呢?


你知道的,不同于 null 字面量,undefined 并不恒等于 undefined 原始值,比如说祂可以被“作用域链截胡”。


举个粒子,当 undefined 变身成为 bilibili,同事的内心是崩溃的。


bilibili.png


猫眼可见,写做 undefined 变量,读做 'bilbili' 字符串,这样的代码十分反人类。


这里稍微有点违和感。机智如你可能会灵魂拷问,我们前面不是已经证明了 undefined 是不可赋值的只读属性吗?怎么祂喵地一言不合说变就变,又可以赋值了呢?来骗,来偷袭,不讲码德!


这种灵异现象主要跟变量查找的作用域链机制有关。读写变量会遵循“就近原则”优先匹配,先找到谁就匹配谁,就跟同城约会一样,和樱花妹异地恋的优先级肯定不会太高,所以当前局部作用域的优先级高于全局作用域,于是乎 JS 会优先使用当前非全局同名变量 undefined


换而言之,局部的同名变量 undefined 屏蔽(shadow,AKA“遮蔽”)了全局变量 globalThis.undefined


关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。倘若你不会搓麻将,你也可以命名为“作用域链抢断”。倘若你不会打篮球,那就叫“作用域链拦截”吧。


globalThis.undefined 确实是只读属性。虽然但是,你们重写非全局的 undefined,跟我 globalThis.undefined 有什么关系?


周树人.gif


我们总以为 undefined 短小精悍,但其实 globalThis.undefined 才能扬长避短。


当我们重新定义了 undefinedundefined 就名不副实——名为 undefined,值为任意值。这可能会在团队协作中引发复合 BUG。


所谓“复合 BUG”指的是,单独的代码可以正常工作,但是多人代码集成就出现问题。


举个粒子,常见的复合 BUG 包括但不限于:



  • 命名冲突,比如说 Vue2 的 Mixin 就有这个瑕疵,所以 Vue3 就引入更加灵活的组合式 API

  • 作用域污染,ESM 模块之前也有全局作用域污染的老毛病,所以社区有 CJS 等模块化的轮子,也有 IIFE 等最佳实践

  • 团队协作,Git 等代码版本管理工具的开发冲突


举个粒子,undefined 也可能造成类似的问题。


complex-bug.png


猫眼可见,双方的代码都问题不大,但放在一起就像水遇见钠一般干柴烈火瞬间爆炸。


这里分享一个小众的冷知识,这样的代码被称为“Jenga Code”(积木代码)。


Jenga 是一种派对益智积木玩具,它的规则是,先把那些小木条堆成一个规则的塔,玩家轮流从下面抽出一块来放在最上面,谁放上之后木塔垮掉了,谁就 GG 了。


jenga.gif


积木代码指的是一点点的代码带来了亿点点的 BUG,一行代码搞崩整个项目,码农一句,可怜焦土。


换而言之,这样的代码对于 JS 运行时是“程序正义”的,对于开发者却并非“结果正义”,违和感拉满,可读性和可为维护性十分“赶人”,同事读完欲哭无泪。


所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。


祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。


举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——



  • 有人鞠躬尽瘁粮食安全

  • 有人精神饥荒疯狂倒奶


这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。


“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。所谓“前猫拉屎,后人铲屎”大抵也是如此。


综上所述,要警惕测不准的 undefined 在团队开发中造成复合 BUG。




2. 薛定谔的 undefined 如何造成二义性?


除了复合 BUG,undefined 还可能让代码产生二义性。


代码二义性指的是,同一行代码,可能有不同的语义。


举个粒子,JS 的一些代码解读就可能有歧义。


mistake.png


undefined 也可能造成代码二义性,除了上文的变量名不副实之外,还很可能产生精神分裂的割裂感。


举个粒子,代码中存在两个一龙一猪的 undefined


default.png


猫眼可见,undefined 的值并不相同,我只觉得祂们双标。


undefined 变量之所以是 'bilibili' 字符串,是因为作用域链就近屏蔽,cat 变量之所以是 undefined 原始值,是因为已声明未赋值的变量默认使用 undefined 原始值作为缺省值,所以没有使用局部的 undefined 变量。


倘若上述二义性强度还不够,那我们还可以写出可读性更加逆天的代码。


destruct.png


猫眼可见,undefined 有没有精神分裂我不知道,但我快精神分裂了。


代码二义性还可能与代码的执行环境有关,譬如说一猫一样的代码,在不同的运行时,可能有一龙一猪的结果。


strict-mode.png


猫眼可见,我写你猜,谁都不爱。


大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var 是不可能 var 的,这辈子都不可能 var


问题在于,墨菲定律告诉我们,只要可能有 BUG,就有可能有 BUG。说不定你的猪队友下一秒就给你来个神助攻,毕竟不是每个人都像你如此好学,既关注了我,还给我打 call。


语冰以前也不相信倒牛奶这么“离离原上谱”的事件,但是写做“impossible”,读做“I M possible”。


事实上,大多数教程一般不会刻意教你去写错误的代码,这其实恰恰剥夺了我们犯错的权利。不犯错我们就不会去探究为什么,而对知识点的掌握只停留在表面是什么,很多人知错就改,下次还敢就是因为缺少了试错的成就感和多巴胺,不知道 BUG 的 G 点在哪里,没有形成稳固的情绪记忆。


请相信我,永远写正确的代码本身就是一件不正确的事情,你会看到这期内容就是因为语冰被坑了气不过,才给祂载入日记。


语冰很喜欢的一部神作《七龙珠》里的赛亚人,每次从濒死体验中绝处逢生战斗力就会增量更新,这个设定其实蛮科学的,譬如说我们身边一些“量变到质变”的粒子,包括但不限于:



  • 骨折之后骨头更加坚硬了

  • 健身也是肌肉轻度撕裂后增生

  • 记忆也是不断复习巩固


语冰并不是让大家在物理层面去骨折,而是鼓励大家从 BUG 中学习。私以为大神从来不是没有 BUG,而是 fix 了足够多的 BUG。正如爱迪生所说,我没有失败 999 次,而是成功了 999 次,我成功证明了那些方法完全达咩。


综上所述,undefined 的二义性在于可能产生局部的副作用,一猫一样的代码在不同运行时也可以有一龙一猪的结果,最终导致一千个麻瓜眼中有一千个哈利波特,读码人集体精神分裂。




3. 未定义的 undefined 为何语义不明?


除了可维护性感人的复合 BUG 和可读性感人的代码二义性,undefined 自身的语义也很难把握。


举个粒子,因为太麻烦就全写 undefined 了。


init.png


猫眼可见,原则上允许我们可以无脑地使用 undefined 初始化任何变量,万物皆可 undefined


虽然但是,绝对的光明等于绝对的黑暗,绝对的权力导致绝对的腐败。undefined 的无能恰恰在于祂无所不能,语冰有幸百度了一本书叫《选择的悖论》,这大约也是 undefined 的悖论。


代码是写给人看的,代码的信息越具体明确越好,偏偏 undefined 既模糊又抽象。你知道的,我们接触的大多数资料会告诉我们 undefined 的意义是“未定义/无值”。


虽然但是,准确而无用的观念,终究还是无用的。undefined 的正确打开方式就是无为,使用 undefined 的最佳方式是不使用祂。




免责声明



本文示例代码默认均为 ESM(ECMAScript Module)筑基测评,因为现代化前端开发相对推荐集成 ESM,其他开发环境下的示例会额外注释说明,edge cases 的解释权归大家所有。



今天的《ES6 混合理论》就讲到这里啦,我们将在本合集中深度学习若干奇奇怪怪的前端面试题/冷知识,感兴趣的前端爱好者可以关注订阅,也欢迎大家自由言论和留言许愿,共享 BUG,共同内卷。


吾乃前端的虔信徒,传播 BUG 的福音。


我是大家的林语冰,我们一期一会,不散不见,掰掰~


作者:大家的林语冰
来源:juejin.cn/post/7240483867123220540
收起阅读 »

由阿里裁员引发的一些思考

前言 从阿里淘系离开差不多2年多了,最近阿里又来到风口浪尖上,也是打出一套眼花缭乱的组合拳, 先是马老板回国; 3.28日 阿里开启成立24年来最大组织架构变革,逍遥子张勇,宣布启动“1+6+N”组织变革,各个大业务线实行自负盈亏,有独立融资和上市的可能性;...
继续阅读 »


前言


从阿里淘系离开差不多2年多了,最近阿里又来到风口浪尖上,也是打出一套眼花缭乱的组合拳



  1. 先是马老板回国;

  2. 3.28日 阿里开启成立24年来最大组织架构变革,逍遥子张勇,宣布启动“1+6+N”组织变革,各个大业务线实行自负盈亏,有独立融资和上市的可能性;

  3. 最近又开始爆出阿里大裁员,各种小道消息什么 阿里云7%,天猫淘宝25%,然后阿里开始辟谣,且不论真假,一时间,给整个互联网圈传递一股寒气,今天就在茶话会上聊聊这个事情。


理性的看待阿里裁员


阿里裁员其实是有个心理预期的,个人觉得主要有以下原因吧



  1. 阿里 361制度末位淘汰10%,连续2年3.25就会被淘汰,每年本身就有一批人要淘汰

  2. 组织架构拆解需要自负盈亏,一些子业务之前可以吃大锅饭,现在分田到户了,就需要人员进行优化提高组织效率,达到降本增效

  3. 核心业务一直被蚕食,人才盘点降本增效,淘汰贵的产出一般的 换一拨 校招生既能补充新鲜血液又可以降低成本


目前在互联网下行这个大环境下,加之之前一直宣传的35岁危机更加放大了裁员带来的恐慌。互联网正在慢慢回归理性这是个不争的事实,甚至连老美的硅谷互联网大厂都裁了一波,看看马斯克接手推特后"大杀四方"的狠劲。


互联网的退潮期


大潮正在退去


行业也是要顺势而为,风口来了猪都上天,不过目前国内互联网已经过了之前的高速增长期,监管也在收紧,大家都在拼存量市场,都卷到到菜市场了(各种买菜 多多买菜、橙心优选等等)从蓝海杀到了红海,增长上不去了,要么开源,要么节流。开源的话寻找新赛道何其难,元宇宙的尸体还热乎着,前几年大家都选择出海,但是除了字节还算可以(也难),小米在印度被阿三罚了好几十亿,整体看来开源难度过大。 image.png 大厂纷纷启用了节流大招,字节去年就在喊去肥增瘦各个项目开始review roi要求打正,肯定打不正的有的就地正法了,教育部门、游戏部门都是重灾区; 腾讯去年也是整合内部资源,PCG(破产G)去年基本上干掉一半,其实阿里这一波跟去年腾讯一样半斤八两。 而且这几年互联网大厂之间好像有点默契,年终之后都在裁员,降低员工流动性,不仅对业务稳定带来好处也能减少薪资开支。不过跳槽涨薪确实香,之前基本2年一跳,早些时候行情好能double,最不济也有40%的涨幅,不过现在在字节已经2年多了,确实没有任何跳槽打算了


非理性招聘慢慢在回归理性


大厂员工的招聘本身就是非理性的,还记得前年微信出了一个爆炸的表情,当时看到一篇文章作者自嘲自己是清华毕业在微信研究"炸屎"表情。。。非理性主要有以下几个方面造成的:



  1. 赛马机制导致团队重复


早期在业务遇到增长瓶颈和重大的课题时候,往往采用加人的方法,《人月神话》早就证伪了技术在这方面的不可靠,可能反而会让协作效率降低。这也间接导致了大厂的山头主义,由于领导需要使用团队规模来确立地位,毕竟更多的HC,就意味着揽到更多的事情,获得更高的地位。这在早期也是被更高层的领导所默许的,腾讯大名鼎鼎的赛马机制,就是使用多支团队来做同样的事情,微信当年就是这么诞生的,观察大部门的大厂对团队的分工有时候是可以模糊的,而且资源都还不错,如果一个团队不行,就让另一个上,这也不可避免的引起了内耗,这种机制效率上有有提升,但是代价是巨大资金开销,内部组织臃肿。



  1. 人才储备过盛


前些年,大厂业务增长太强劲了,各个赛道都要投入人力,人员分工更加细化,大厂的app可能一个按钮就是一个业务线,这也经常自嘲为拧螺丝的,因此招聘规模也是空前的,先招进来再内部淘汰,挑选了最优秀的,而且也是变相的打击竞争对手,不由得想起来了华为,华为之前财大气粗,连续狂招几年,直接把中兴干的人才断档。 在当前这个战略收缩的过冬阶段,这样的裁员可能还是结构性的、长期的,只要业务不行公司可能就及时止损了,带来的就是裁员,得有个清醒的认识。



  1. 大厂员工真的不便宜


互联网作为行业天花板,经历了资本的无序扩张,资金充备,花的都是投资人的钱,互相竞争着加价招人,大厂校招生的白菜价也是其他行业所无法企及的,几年下来每年的普调、跳槽的几轮加价,大家都来到了一个薪资高位。这些都是建立在你做的业务能给公司转来更多的钱,当增长停止时候,你还能给公司赚这么多吗?从经济学的供需关系来看很简单:公司年薪100w招你来,你真的能持续给公司多赚150w吗?业务增长时候,公司开掉你,人力成本节省了100w,公司的业务会降低100w吗?如果没有那裁掉的你是个理性的选择,因为你实在是太贵了。这些年大厂不光是干掉大头兵,甚至连一些高P开始受到波及了,因为他们更贵。所以薪资来到高位的找个好业务能苟着尽量苟着,风口没了,猪是飞不上天的。加上最近几年大学生找工作难,裁员换血一波,对公司也是极好的。 再看看老美那边,大厂基本也都是正式员工(贵)+外包(便宜),国内感觉也会慢慢往这个方向发展,看看国内华为这几年,外包招的飞起,之前在阿里 QA、前端、UI都是正式工+外包搭配。(除了移动端+服务端好像没见过)


大趋势下的互联网人该如何做


黄金时代一起不复返了,看清行业的大趋势,做好心理预期:



  1. 降低自己的经济杠杆,适当降低消费欲望,毕竟手里有粮心不慌。我就把房贷提前还了大头,留点尾巴抵抵税

  2. 尽量找个稳一点的业务线,延长职业生涯,认真做事,苟住就好,边缘业务可能隔三差五就要一波拥抱变化,我是活水到了字节一个比较赚钱的业务

  3. 有余力的可以探索一些副业,可以是老本行,或者家里人脉广的也可以涉足其他行业,比如水果店啥的,看自己人脉关系了,试试看副业能不能养起来。可以是一种商业模式,我看就有很多网红收割校招生搞星球,搞培训啥的忙的不亦乐乎;也可以是发现一个痛点,上次就听说 一个还没交付的楼盘一个业主在业主群搞了个公众号,直播楼盘进度,小的私域流量也是能赚点小钱的。培养一些产品思维,注意观察生活吧,不过副业确实比较难要有耐心,最近我就在googleplay上架一个游戏app,不过自然流量太低了,国内的话现在对个人开发者太不又好了,不仅应用商店很多都需要企业资质,穿山甲、优量汇这些广告平台也是需要企业资质,基本把个人开发者路封死了,注册个企业比较麻烦成本也高,这块有兴趣后面可以展开说说,还有灰黑产的话还是要慎重,来钱快可能进去也快

  4. 要是还想在业内混的话,专业能力还是不能丢,提高个人竞争力,保持技术关注度,加强 技术的深度以及广度,做一个T型人才,尽量成为一个全栈吧。其实现在服务端go的gin框架,java的spring学起来也很快,前端搞个小程序基本上就齐活了,不仅是个人竞争力,也是副业的基础;工作中也要注意提高自己的软实力,比如 稳定的情绪、有效的沟通、适当的向上管理,做一个大家都认可的靠谱的合作伙伴


最后尽人事 听天命吧,心态还是要稳住,积极乐观的工作和生活。好了,茶也喝完了,本期的茶话会就到这里吧,祝大家工作顺利,欢迎留言讨论


作者:Android茶话会
来源:juejin.cn/post/7237489935901032504
收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。


我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。


和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”


这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。


现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。


再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?


知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)


第一,认死理。


和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)


常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。


例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。


比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。


如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。


第二,喜欢拿技术套市场。


​这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。


举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。


可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。


第三,不擅长合作。


为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。


他们会搞钱。


他们会搞钱,是​因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。


大部分人,在创业路上直接卡死在这条路线上了。


投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。


那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。



--- 


我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。


只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。


最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身

作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
体,活下来才有输出。

收起阅读 »

python计算质数的几种方法

因为要学着写渗透工具,这几天都在上python编程基础课,听得我打瞌睡,毕竟以前学过嘛。 最后sherry老师留了作业,其中一道题是这样的: 题目:编写python程序找出10-30之间的质数。 太简单了,我直接给出答案: Prime = [11, 13, 1...
继续阅读 »

因为要学着写渗透工具,这几天都在上python编程基础课,听得我打瞌睡,毕竟以前学过嘛。
最后sherry老师留了作业,其中一道题是这样的:


题目:编写python程序找出10-30之间的质数。


太简单了,我直接给出答案:


Prime = [11, 13, 17, 19, 23, 29]
print(Prime)

输出结果:


[11, 13, 17, 19, 23, 29]

当然,这样做肯定会在下节课被sherry老师公开处刑的,所以说还是要根据上课时学的知识写个算法。


1.穷举法


回想一下上课时学了变量、列表、循环语句之类的东西,sherry老师还亲自演示了多重死循环是怎么搞出来的(老师是手滑了还是业务不熟练啊),所以我们还是要仔细思考一下不要重蹈覆辙。


思路:首先要构造一个循环,遍历所有符合条件的自然数,然后一个一个验证是否为质数,最后把符合条件的质数列出来。


# 最开始编的穷举法,简单粗暴,就是性能拉跨。
# P=因数,N=自然数
import time

t0 = time.time() # 开始时间
Min = 10 # 范围最小值
Max = 30 # 范围最大值
Result = [] # 结果

for N in range(Min, Max): # 给自然数来个遍历
for P in range(2, N):
if (N % P == 0): # 判断是否有因数
break # 有因数那就不是质数,跳出循环
else:
Result.append(N)

print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', Result)
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('计算耗时:', time.time() - t0, '秒')

执行结果(计算耗时是最后加上去的):


2023-05-28-22-35-46.png


到这里作业就搞定了。然后把其他几道题也做完了,发现很无聊,就又切回来想搞点事。这么点计算量,0秒真的有点少,不如趁这个机会烤一烤笔记本的性能,所以直接在Min和Max的值后面加几个0。试试100000-200000。


2023-05-28-23-02-03.png


很尴尬,直接卡住了,这代码有点拉跨啊,完全不符合我的风格。
倒了杯咖啡,终于跑完了。


2023-05-28-23-01-07.png


这个也太夸张,一定是哪里出了问题,很久以前用C写的代码我记得也没那么慢啊。反正周末挺闲的,不如仔细研究一下。


2.函数(CV)大法


为了拓宽一下思路,我决定借鉴一下大佬的代码。听说函数是个好东西,所以就CV了两个函数。


一个函数判断质数,另一个求范围内的所有质数,把它们拼一起,是这个样子:


# 网上学来的,自定义两个函数,但是数值稍微大点就卡死了。
import time

t0 = time.time()
Min = 100000 # 范围最小值
Max = 200000 # 范围最大值


def is_prime(n): return 0 not in [n % i for i in range(2, n//2+1)] # 判断是否为质数


def gen_prime(a, b): return [n for n in range(
a, b+1) if 0 not in [n % i for i in range(2, n//2+1)]] # 输出范围内的质数


print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', gen_prime(Min, Max))
print('计算耗时:', time.time() - t0, '秒')

稍微改动了一下,还是100000-200000,我们试试看。


2023-05-28-23-08-35.png


嗯,一运行风扇就开始啸叫,CPU都快烤炸了。看来CV大法也不行啊。
经过漫长的烤机,这次结果比上次还惨,300多秒,这两个函数本质上还是穷举法,看来这条路也走不通。


3.穷举法改


我们可以分析一下穷举法的代码,看看有没有什么改进的方法。
首先,通过九年义务教育掌握的数学知识,我们知道,质数中只有2是偶数,所以计算中可以把偶数忽略掉,只计算奇数,工作量立马减半!
其次,在用因数P判断N是否为质数时,如果P足够大的话,比如说PxP>=N的时候,那么后面的循环其实是重复无意义的。因为假设PxQ>=N,那么P和Q必然有一个小于sqrt(N),只需要计算P<=sqrt(N)的情况就行了。


因为2作为唯一一个偶数,夹在循环里面处理起来很麻烦,所以放在开头处理掉。最终的代码如下:


# 优化后的代码,减少了一些无意义的循环,比以前快多了。
import time

t0 = time.time()
Min = 100000 # 范围最小值
Max = 200000 # 范围最大值
Prime = [2, 3] # 质数列表
Result = [] # 结果
Loop = 0 # 计算循环次数

if Min <= 2:
Result.append(2)
if Min <= 3:
Result.append(3) # 先把2这个麻烦的偶数处理掉
for N in range(5, Max, 2):
for P in range(3, int(N**0.5)+2, 2): # 只计算到根号N
Loop += 1
if (N % P == 0):
break
else:
Prime.append(N)
if N > Min:
Result.append(N)

print('计算', Min, '到', Max, '之间的质数')
print(Min, '到', Max, '之间的质数序列:', Result)
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

2023-05-28-23-09-54.png


代码量虽然多了,但是效果还是很明显,100000-200000才0.4秒,快了不知道多少,看来我们的思路是对的。
我决定再加到1000000-5000000,看看能不能撑住。因为输出太多了控制台会卡死,所以改一下,只输出最后一个质数。


2023-05-28-23-19-12.png


总共花了64秒,看来还是有点费劲。


4.穷举法魔改


我们再来分析一下,如果我们用于判断的因数,不是用奇数列表,而是用生成的Prime列表里面的质数,因为质数的个数远远少于奇数,所以第二个循环会少一些工作量呢?可以试试看。但是因为这个改动,需要加一些判断语句进去,所以节省的时间比较有限。


# 别看这个代码比较长,但是跑到1000万也不会卡死,而且还很快。
import time

t0 = time.time()
Min = 1000000 # 范围最小值
Max = 5000000 # 范围最大值
Prime = [2, 3] # 质数列表
Result = [] # 结果
Loop = 0 # 计算循环次数

if Min <= 2:
Result.append(2)
if Min <= 3:
Result.append(3)
for N in range(5, Max, 2):
M = int(N**0.5) # 上限为根号N
for P in range(len(Prime)): # 在质数列表Prime中遍历
Loop += 1
L = Prime[P+1]
if (N % L == 0):
break
elif L >= M: # 上限大于根号N,判断为质数并跳出循环
Prime.append(N)
if N > Min:
Result.append(N)
break

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

还是1000000-5000000再试试看


2023-05-28-23-25-29.png


这次耗时22秒,时间又缩短了一大半,但是好像已经没多少改进的空间了,感觉穷举法已经到头了,需要新的思路。


5.埃氏筛法


其实初中数学我们就学过埃氏筛法:
如果P是质数,那么大于P的N的倍数一定不是质数。把所有的合数排除掉,那么剩下的就都是质数了。
我们可以生成一个列表用来储存数字是否是质数,初始阶段都是质数,每次得出一个质数就将它的倍数全部标记为合数。


# 速度已经起飞了。
import time

t0 = time.time()
Min = 1000000 # 范围最小值
Max = 2000000 # 范围最大值
Loop = 0 # 计算循环次数
Result = [] # 结果

Natural = [True for P in range(Max)] # 自然数列表标记为True
for P in range(2, Max):
if Natural[P]: # 标记如果为True,就是质数
if P >= Min:
Result.append(P) # 添加范围之内的质数
for N in range(P*2, Max, P): # 将质数的倍数的标记改为False
Loop += 1
Natural[N] = False

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

2023-05-29-00-11-23.png


1.6秒,比最高级的穷举法还要快上10多倍,这是数学的胜利。
再试试1-50000000。


1.png


很不错,只需要20秒。因为筛法的特性,忽略内存的影响,数值越大,后面的速度反而越快了。


6.欧拉筛法


我们可以仔细分析一下,上面的埃氏筛法在最后标记的时候,还是多算了一些东西,N会重复标记False,比如77,既是7的倍数又是11的倍数,这样会被标记两次,后面的大合数会重复标记多次,浪费了算力,所以标记的时候要排除合数。另外就是P*N大于Max时,后面的计算已经无意义了,也要跳出来。把这些重复的动作排除掉,就是欧拉筛法,也叫线性筛。


# 最终版,优化了很多细节。
import time

t0 = time.time()
Min = 1 # 范围最小值
Max = 50000000 # 范围最大值
Loop = 0 # 计算循环次数
Prime = [2]
Result = [] # 结果

if Min <= 2:
Result.append(2)
Limit = int(Max/3)+1
Natural = [True for P in range(Max+1)] # 自然数列表标记为True
for P in range(3, Max+1, 2):
if Natural[P]: # 标记如果为True,就是质数
Prime.append(P)
if P >= Min:
Result.append(P)
if P > Limit: # 超过Limit不需要再筛了,直接continue
continue
for N in Prime: # 将质数的倍数的标记改为False
Loop += 1
if P*N > Max: # 超过Max就无意义了,直接break
break
Natural[P * N] = False
if P % N == 0: # 判断是否为合数
break

print('计算', Min, '到', Max, '之间的质数')
print('最后一个质数:', Result[-1])
print(Min, '到', Max, '之间的质数个数:', len(Result))
print('循环次数:', Loop)
print('计算耗时:', time.time() - t0, '秒')

(因为之前的版本缩进错了,所以更新了这段代码)


2.png


同样的条件下耗时11.46秒。这是因为多了一个列表和几行判断语句,加上python的解释型特性,所以实际上并不会快好几倍,但是总体效率还是有50%左右的提升。


好了,这次把老师课堂上讲的变量、列表、循环语句什么的都用上了,算是现买现卖、活学活用吧。我觉得这次的作业怎么说也能拿满分吧,sherry老师记得下次上课夸夸我。


作者:ReisenSS
来源:juejin.cn/post/7238199999732695097
收起阅读 »

Vue3 除了keep-alive,还有哪些页面缓存的实现方案

web
引言 有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。 对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页...
继续阅读 »

引言


有这么一个需求:列表页进入详情页后,切换回列表页,需要对列表页进行缓存,如果从首页进入列表页,就要重新加载列表页。


对于这个需求,我的第一个想法就是使用keep-alive来缓存列表页,列表和详情页切换时,列表页会被缓存;从首页进入列表页时,就重置列表页数据并重新获取新数据来达到列表页重新加载的效果。


但是,这个方案有个很不好的地方就是:如果列表页足够复杂,有下拉刷新、下拉加载、有弹窗、有轮播等,在清除缓存时,就需要重置很多数据和状态,而且还可能要手动去销毁和重新加载某些组件,这样做既增加了复杂度,也容易出bug。


接下来说说我的想到的新实现方案(代码基于Vue3)。


省流


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaocheng55…


keep-alive 缓存和清除



keep-alive 缓存原理:进入页面时,页面组件渲染完成,keep-alive 会缓存页面组件的实例;离开页面后,组件实例由于已经缓存就不会进行销毁;当再次进入页面时,就会将缓存的组件实例拿出来渲染,因为组件实例保存着原来页面的数据和Dom的状态,那么直接渲染组件实例就能得到原来的页面。



keep-alive 最大的难题就是缓存的清理,如果能有简单的缓存清理方法,那么keep-alive 组件用起来就很爽。


但是,keep-alive 组件没有提供清除缓存的API,那有没有其他清除缓存的办法呢?答案是有的。我们先看看 keep-alive 组件的props:


include - string | RegExp | Array。只有名称匹配的组件会被缓存。
exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
max - number | string。最多可以缓存多少组件实例。

从include描述来看,我发现include是可以用来清除缓存,做法是:将组件名称添加到include里,组件会被缓存;移除组件名称,组件缓存会被清除。根据这个原理,用hook简单封装一下代码:


import { ref, nextTick } from 'vue'

const caches = ref<string[]>([])

export default function useRouteCache () {
// 添加缓存的路由组件
function addCache (componentName: string | string []) {
if (Array.isArray(componentName)) {
componentName.forEach(addCache)
return
}

if (!componentName || caches.value.includes(componentName)) return

caches.value.push(componentName)
}

// 移除缓存的路由组件
function removeCache (componentName: string) {
const index = caches.value.indexOf(componentName)
if (index > -1) {
return caches.value.splice(index, 1)
}
}

// 移除缓存的路由组件的实例
async function removeCacheEntry (componentName: string) {
if (removeCache(componentName)) {
await nextTick()
addCache(componentName)
}
}

return {
caches,
addCache,
removeCache,
removeCacheEntry
}
}

hook的用法如下:


<router-view v-slot="{ Component }">
<keep-alive :include="caches">
<component :is="Component" />
</keep-alive>
</router-view>

<script setup lang="ts">
import useRouteCache from './hooks/useRouteCache'
const { caches, addCache } = useRouteCache()

<!-- 将列表页组件名称添加到需要缓存名单中 -->
addCache(['List'])
</script>

清除列表页缓存如下:


import useRouteCache from '@/hooks/useRouteCache'

const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List')


此处removeCacheEntry方法清除的是列表组件的实例,'List' 值仍然在 组件的include里,下次重新进入列表页会重新加载列表组件,并且之后会继续列表组件进行缓存。



列表页清除缓存的时机


进入列表页后清除缓存


在列表页路由组件的beforeRouteEnter勾子中判断是否是从其他页面(Home)进入的,是则清除缓存,不是则使用缓存。


defineOptions({
name: 'List1',
beforeRouteEnter (to: RouteRecordNormalized, from: RouteRecordNormalized) {
if (from.name === 'Home') {
const { removeCacheEntry } = useRouteCache()
removeCacheEntry('List1')
}
}
})

这种缓存方式有个不太友好的地方:当从首页进入列表页,列表页和详情页来回切换,列表页是缓存的;但是在首页和列表页间用浏览器的前进后退来切换时,我们更多的是希望列表页能保留缓存,就像在多页面中浏览器前进后退会缓存原页面一样的效果。但实际上,列表页重新刷新了,这就需要使用另一种解决办法,点击链接时清除缓存清除缓存


点击链接跳转前清除缓存


在首页点击跳转列表页前,在点击事件的时候去清除列表页缓存,这样的话在首页和列表页用浏览器的前进后退来回切换,列表页都是缓存状态,只要当重新点击跳转链接的时候,才重新加载列表页,满足预期。


// 首页 Home.vue

<li>
<router-link to="/list" @click="removeCacheBeforeEnter">列表页</router-link>
</li>


<script setup lang="ts">
import useRouteCache from '@/hooks/useRouteCache'

defineOptions({
name: 'Home'
})

const { removeCacheEntry } = useRouteCache()

// 进入页面前,先清除缓存实例
function removeCacheBeforeEnter () {
removeCacheEntry('List')
}
</script>

状态管理实现缓存


通过状态管理库存储页面的状态和数据也能实现页面缓存。此处状态管理使用的是pinia。


首先使用pinia创建列表页store:


import { defineStore } from 'pinia'

interface Item {
id?: number,
content?: string
}

const useListStore = defineStore('list', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
isRefresh: true,
pageSize: 30,
currentPage: 1,
list: [] as Item[],
curRow: null as Item | null
}
},
actions: {
setList (data: Item []) {
this.list = data
},
setCurRow (data: Item) {
this.curRow = data
},
setIsRefresh (data: boolean) {
this.isRefresh = data
}
}
})

export default useListStore

然后在列表页中使用store:


<div>
<el-page-header @back="goBack">
<template #content>状态管理实现列表页缓存</template>
</el-page-header>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === listStore.curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="listStore.currentPage"
:page-size="listStore.pageSize"
layout="total, prev, pager, next"
:total="listStore.list.length"
/>
</div>

<script setup lang="ts">
import useListStore from '@/store/listStore'
const listStore = useListStore()

...
</script>

通过beforeRouteEnter钩子判断是否从首页进来,是则通过 listStore.$reset() 来重置数据,否则使用缓存的数据状态;之后根据 listStore.isRefresh 标示判断是否重新获取列表数据。


defineOptions({
beforeRouteEnter (to: RouteLocationNormalized, from: RouteLocationNormalized) {
if (from.name === 'Home') {
const listStore = useListStore()
listStore.$reset()
}
}
})

onBeforeMount(() => {
if (!listStore.useCache) {
loading.value = true
setTimeout(() => {
listStore.setList(getData())
loading.value = false
}, 1000)
listStore.useCache = true
}
})

缺点


通过状态管理去做缓存的话,需要将状态数据都存在stroe里,状态多起来的话,会有点繁琐,而且状态写在store里肯定没有写在列表组件里来的直观;状态管理由于只做列表页数据的缓存,对于一些非受控组件来说,组件内部状态改变是缓存不了的,这就导致页面渲染后跟原来有差别,需要额外代码操作。


页面弹窗实现缓存


将详情页做成全屏弹窗,那么从列表页进入详情页,就只是简单地打开详情页弹窗,将列表页覆盖,从而达到列表页 “缓存”的效果,而非真正的缓存。


这里还有一个问题,打开详情页之后,如果点后退,会返回到首页,实际上我们希望是返回列表页,这就需要给详情弹窗加个历史记录,如列表页地址为 '/list',打开详情页变为 '/list?id=1'。


弹窗组件实现:


// PopupPage.vue

<template>
<div class="popup-page" :class="[!dialogVisible && 'hidden']">
<slot v-if="dialogVisible"></slot>
</div>
</template>

<script setup lang="ts">
import { useLockscreen } from 'element-plus'
import { computed, defineProps, defineEmits } from 'vue'
import useHistoryPopup from './useHistoryPopup'

const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
// 路由记录
history: {
type: Object
},
// 配置了history后,初次渲染时,如果有url上有history参数,则自动打开弹窗
auto: {
type: Boolean,
default: true
},
size: {
type: String,
default: '50%'
},
full: {
type: Boolean,
default: false
}
})
const emit = defineEmits(
['update:modelValue', 'autoOpen', 'autoClose']
)

const dialogVisible = computed<boolean>({ // 控制弹窗显示
get () {
return props.modelValue
},
set (val) {
emit('update:modelValue', val)
}
})

useLockscreen(dialogVisible)

useHistoryPopup({
history: computed(() => props.history),
auto: props.auto,
dialogVisible: dialogVisible,
onAutoOpen: () => emit('autoOpen'),
onAutoClose: () => emit('autoClose')
})
</script>

<style lang='less'>
.popup-page {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 100;
overflow: auto;
padding: 10px;
background: #fff;

&.hidden {
display: none;
}
}
</style>

弹窗组件调用:


<popup-page 
v-model="visible"
full
:history="{ id: id }">
<Detail></Detail>
</popup-page>


hook:useHistoryPopup 参考文章:juejin.cn/post/713994…



缺点


弹窗实现页面缓存,局限比较大,只能在列表页和详情页中才有效,离开列表页之后,缓存就会失效,比较合适一些简单缓存的场景。


父子路由实现缓存


该方案原理其实就是页面弹窗,列表页为父路由,详情页为子路由,从列表页跳转到详情页时,显示详情页字路由,且详情页全屏显示,覆盖住列表页。


声明父子路由:


{
path: '/list',
name: 'list',
component: () => import('./views/List.vue'),
children: [
{
path: '/detail',
name: 'detail',
component: () => import('./views/Detail.vue'),
}
]
}

列表页代码:


// 列表页
<template>
<el-table v-loading="loading" :data="tableData" border style="width: 100%; margin-top: 30px;">
<el-table-column prop="id" label="id" />
<el-table-column prop="content" label="内容"/>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link type="primary" @click="gotoDetail(row)">进入详情</el-link>
<el-tag type="success" v-if="row.id === curRow?.id">刚点击</el-tag>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:currentPage="currentPage"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="list.length"
/>


<!-- 详情页 -->
<router-view class="popyp-page"></router-view>
</template>

<style lang='less' scoped>
.popyp-page {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: #fff;
overflow: auto;
}
</style>

结尾


地址:


demo: xiaocheng555.github.io/page-cache/…


代码: github.com/xiaoch

eng55…

收起阅读 »

用proxy改造你的console

web
前言 在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。 let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';...
继续阅读 »

前言


在前端平常的开发中,最长使用的调试手段应该就是console大法。console很好用,但有时候打印变量多了,看起来就比较懵。


let name1 = 'kk'; name2 = 'kkk'; name3 = 'kk1k'; name4= 'k1kk';
console.log(name1)
console.log(name2)
console.log(name3)
console.log(name4)

打印如下
image.png


那个变量 对应那个 就比较难分辨。我又不想在写代码来分辨(懒😀),那个打印对应的变量是多少。


解决方案


方案一,通过ast解析console 将变量名放在console后面,奈何esbuild不支持ast操作(不是我不会 哈哈哈哈), 故放弃。


方案二,既然vue能代理对象,那么console是不是也能被代理。


实践


第一步代理console,将原始的console,用全局变量originConsole保存起来,以便后续使用
withLogging 函数拦截console.log 重写log参数


const originConsole = window.console; 
var console = new Proxy(window.console, {
get(target, property) {
if(property === 'log') {
return withLogging(target[property])
}
return target[property] },
})

遇到问题,js中 无法获取获取变量的名称的字符串。就是说无法打印变量名。


解决方案,通过vite中的钩子函数transform,将console.log(name.x) 转化成 console.log(name.x, ['isPlugin', 'name.x'])


      transform(src, id) {
if(id.includes('src')) { // 只解析src 下的console
const matchs = src.matchAll(/console.log\((.*)\);?/g);
[...matchs].forEach((item) => {
const [matchStr, args] = item;
let replaceMatch = ''
const haveSemicolon = matchStr.endsWith(";");
const sliceIndex = haveSemicolon ? -2 : -1;
const temp = matchStr.slice(0,sliceIndex);
const tempArgs = args.split(",").map(item => {
if(item.endsWith('"')) {
return item
}
return `"${item}"`
}).join(",")
replaceMatch = `${temp},['isPlugin',${tempArgs}]);`
src = src.replace(matchStr, replaceMatch)
});
}
return {
code: src,
id,
}
},

这样最终就实现了类型于这样的输出代码


  originConsole.log('name.x=', name.x)

这样也就最终实现了通过变量输出变量名跟变量值的一一对应


最后


我将其写成了一个vite插件,vite-plugin-consoles 感兴趣的可以试试,有bug记得跟我说(●'◡'●)


源码地址:
github.com/ALiangTech/…


作者:平平无奇的阿良
来源:juejin.cn/post/7238508573667344441
收起阅读 »