注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

你的flask服务开启https了吗?

一、你的flask服务开启https了吗? 1.事件起因 计划做文心一言插件,我购买了服务器,开始按照我的想象部署插件,结果工作不错,但是最后一步图片显示不正常。哭晕了,如下图所示。 仔细阅读,发现返回图片地址什么的都很正常啊,也可以访问得到,但是为什么就不...
继续阅读 »

一、你的flask服务开启https了吗?


1.事件起因


计划做文心一言插件,我购买了服务器,开始按照我的想象部署插件,结果工作不错,但是最后一步图片显示不正常。哭晕了,如下图所示。


e3dde086617a68f9a585f78f8bfdc20.png
仔细阅读,发现返回图片地址什么的都很正常啊,也可以访问得到,但是为什么就不能看到呢,很奇怪。


经多方排查,最终确定是图片跨域导致无法显示。


2.解决思路


知道是跨域问题就好了,因为文心一言是https访问,所以提供服务的也需要https,那么就开始作了(解决)。


二、解决办法


1.无效1.0解决办法


知道要https那我就加https了,直接百度解决办法。



  • Flask(更具体地说其实是Werkzeug),支持使用即时证书,这对于通过HTTPS快速提供应用程序非常有用,而且不会搞乱系统的证书。你只有需要做的就是将 ssl_context ='adhoc' 添加到程序的 app.run() 调用中。遗憾的是,Flask CLI无法使用此选项。举个例子,下面是官方文档中的“Hello,World” Flask应用程序,并添加了TLS加密:

  • 安装库 pip install pyopenssl


from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run(ssl_context='adhoc')


这样启动起来就是https了,但是访问问题依旧,图片仍然没有显示。。。。。。仔细看来浏览器说是假的ssl,那就继续解决。


2.无效2.0自签名证书解决办法


所谓的自签名证书是使用与同一证书关联的私钥生成签名的证书,就是自己动手丰衣足食。


微信截图_20231126163515.png



  • 生成证书

  • flask加载证书


openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run(ssl_context=('cert.pem', 'key.pem'))
复制代码

然后看结果,依然无效,因为是自签名。。。。。。


3.0终极解决办法


后来发现要真正的证书,那么你必须要有域名,才会发给你,就是说证书和域名是绑定的,就跟户籍一样,户籍都没有说什么学区房,没人理你。



  • 为此我花了8块买了一个cn一年的域名,并且进行了实名。

  • 在腾讯云申请ssl证书,参考地址 cloud.tencent.com/document/pr…

  • 申请地址 console.cloud.tencent.com/ssl
    申请时必须先证明该证书属于你,需要按提示加入cname,进行验证。申请完毕略等一会就会通过,下载证书即可,具体包含以下几个问题件:


微信截图_20231126164111.png



  • 加载证书
    因为有4个文件,没有详细写,因此我测试了几次,最终成功。


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', ssl_context=( 'erniebotplugins.cn_bundle.crt','erniebotplugins.cn.key'), port=8081)

三、最终效果


用上最终大招后,最终成功,具体效果如下。


微信截图_20231126164250.png


可见现在ssl非常普遍,不安全别的网站都懒得搭理你。


作者:Livingbody
来源:juejin.cn/post/7305235970285568011
收起阅读 »

《十分钟冥想》和《注意力:专注的科学与训练》

最近看了两本关于注意力的书籍《十分钟冥想》和《注意力:专注的科学与训练》,前者是关于冥想训练很不错的一本书,后者则是对注意原理的剖析和训练实践,虽然仅仅只有后三章是关于实践的,但是我觉得也不错。 读完之后,我想把我觉得最打动我的地方分享出来,应该对大家也有启发...
继续阅读 »

最近看了两本关于注意力的书籍《十分钟冥想》和《注意力:专注的科学与训练》,前者是关于冥想训练很不错的一本书,后者则是对注意原理的剖析和训练实践,虽然仅仅只有后三章是关于实践的,但是我觉得也不错。


读完之后,我想把我觉得最打动我的地方分享出来,应该对大家也有启发。先前朋友问过我一个问题,他说我每天工作的时候能集中注意的时间大概是多久,我说大概只有三个小时,当我细细的考虑这个问题时,真的觉得是有些恐怖的,你想我每天可是工作超过 12 小时,但是不得不承认的是,专注的时间确实很少,会经常性地被打断,所以我也在思考,这对我来说,或者我的注意力来说,是不是有点儿问题呢?而且我走神确实挺严重的,我会经常陷入思考,虽然它没有到影响生活的程度,但是我觉得需要调节下注意力,所以我一直在为注意力主题相关的阅读划时间。说多了,进入正题吧:


大家有没有过这样的想法:觉得自己的大脑不应该胡思乱想什么东西,或者它始终都应该是专注的,但是实际上,大脑就是思绪纷飞的,每个人都是。书中讲到了一个观点,我觉得超级贴切,思绪就像是马路上行驶的汽车,而你坐在路边,车子有不同的颜色和不同的尺寸,有时你会被汽车的声音吸引,而有时又会被它们的外饰吸引,你可能会随着它们跑起来,当你跑起来的时候,这就是分神的过程,甚至有时候你会跑到马路中间去指挥交通,但是实际上你并不能阻止想法的出现,它们的出现都是自发的,有时候你在随车跑动时,会意识到自己在做什么,就在此时,你又重新回到路边坐下来,也就是所谓的回过神来了,当你明白了这个,慢慢地不再频繁地跑到路上,而是越来越安心的坐在路边,观察想法的来去时,你的专注程度就变得更好了,所以当发现自己分神时,耐心地,轻轻地提醒下自己,把注意力再拉回来就好,不必有其他的负面想法,这很正常。我们的心其实就像是一片澄澈湛蓝的天空,有时候会被阴云笼罩,我们会想将它们赶走,但是这可能会带来更多纷扰,所谓被压制的,必将再浮上来,其实我们可以搬把椅子,坐看云卷云舒,蓝色的天空会穿过阴云展露出来,当我们不过分的执着于那些阴云时,它会显露的更快,重要的是:无论生活中发生什么事,要相信心中始终都有一份安全和安定。


如果大家想有意识的训练下注意力的话,可以专注在生活中一些事情的细节上,保持足够的好奇心和训练注意力的目标,它们可以是刷牙,刷碗或者做饭等等,观察牙膏的颜色,体会牙膏的味道或者刷碗的泡沫,什么都可以,只要它有你能够专注的点,也可以说成是认真的有意识的去做生活中的每一件小事。书中解释了茶道为什么能够修身养性,在泡茶的各个阶段,投入注意,不管是多么简单的步骤,都耐心地去做,我也确实认可,因为泡茶很厉害的人摆弄那一套茶具就足够“麻烦”了,一般摆弄这些的人确实挺大师的...


最后就是关于日常事务的处理,要善于对它们进行拆分,细化,分成多个小任务,不断地在小的时间范围内保持专注,这让我想到了番茄钟工作法,拆分任务由大化小确实是很不错的一个方法,建议大家在生活和工作中实践。


注意力就像是一头野兽,我们不能强制它,而是要学会驯服它,它也有和我们本身互相牵扯或者说互相理解的一个过程,不要强迫自己,对自己保有耐心。更加的专注我觉得意味着活在当下,享受此刻,它带来的是一份心神的安定,让我们能够回到他人身边,更好的感受生活,最后,就用其中我喜欢的一句话来结尾吧,“在欲念和动荡的世界中冥思得到的精神力量就像火中盛开的莲花,不可摧毁”。


大家周末快乐,早些休息。


作者:方圆想当图灵
来源:juejin.cn/post/7304997932444942345
收起阅读 »

新项目,不妨采用这种架构分层,很优雅!

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。 在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将...
继续阅读 »

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。


在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将专注于DDD的分层架构和实体模型,期望为大家落地DDD提供一些有益的参考。首先,让我们回顾一下熟悉的MVC三层架构。


1. MVC 架构


在传统应用程序中,我们通常采用经典的MVC(Model-View-Controller)架构进行开发,它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。


在遵循此分层架构的开发过程中,我们通常会建立三个Maven Module:Controller、Service 和 Dao,它们分别对应表现层、逻辑层和数据访问层,如下图所示:


image-20230602123152660


(图中多画了一个Model层是因为 Model 通常只是简单的 Java Bean,只包含数据库表对应的属性。有的应用会将其单独抽取出来作为一个Maven Module,但实际上它可以合并到 DAO 层。)


1.1 MVC架构模型的不足


在业务逻辑较为简单的应用中,MVC三层架构是一种简洁高效的开发模式。然而,随着业务逻辑的复杂性增加和代码量的增加,MVC架构可能会显得捉襟见肘。其主要的不足可以总结如下:



  • Service层职责过重:在MVC架构中,Service层常常被赋予处理复杂业务逻辑的任务。随着业务逻辑的增长,Service层可能变得臃肿和复杂。业务逻辑有可能分散在各个Service类中,使得业务逻辑的组织和维护成为一项挑战。

  • 过于关注数据库而忽视领域建模:虽然MVC的设计初衷是对数据、用户界面和控制逻辑进行分离,但它在面对复杂业务场景时并未给予领域建模足够的重视。这可能导致代码难以理解和扩展,因为代码更像是围绕数据库而不是业务需求进行设计。

  • 边界划分不明确:在MVC架构中,顶层设计上的边界划分并没有明确的规则,往往依赖于技术负责人的经验。在大规模的团队协作中,这可能导致职责不清晰、分工不明确等问题。

  • 单元测试困难:在MVC架构中,Service层通常以事务脚本的方式进行开发,并且往往耦合了各种中间件操作,如数据库、缓存、消息队列等。这种耦合使得单元测试变得困难,因为要在没有这些中间件的情况下运行测试可能需要大量的模拟或存根代码。


在深入探讨MVC架构之后,我们将进入今天的主题:DDD的分层架构模型。


2. DDD的架构模型


在DDD中,通常将应用程序分为四个层次,分别为用户接口层(Interface Layer)应用层(Application Layer)领域层(Domain Layer)基础设施层(Infrastructure Layer),每个层次承担着各自的职责和作用。分层模型如下图所示:


image.png



  1. 接口层(Interface Layer):负责处理与外部系统的交互,包括UI、Web API、RPC接口等。它会接收用户或外部系统的请求,然后调用应用层的服务来处理这些请求,最后将处理结果返回给用户或外部系统。

  2. 应用层(Application Layer):承担协调领域层和基础设施层的职责,实现具体的业务逻辑。它调用领域层的领域服务和基础设施层的基础服务,完成业务逻辑的实现。

  3. 领域层(Domain Layer):该层包含了业务领域的所有元素,如实体、值对象、领域服务、聚合、工厂和领域事件等。这一层的主要职责是实现业务领域的核心逻辑。

  4. 基础设施层(Infrastructure Layer):主要提供通用的技术能力,如数据持久化、缓存、消息传输等基础设施服务。它可被其他三层调用,提供各种必要的技术服务。


在这四层中,调用关系通常是单向依赖的,即上层依赖下层,下层并不依赖上层。例如,接口层依赖应用层,应用层依赖领域层,领域层依赖基础设施层。但值得注意的是,尽管基础设施层在物理结构上可能位于最底层,但在DDD的分层模型中,它位于最外层,为内部各层提供技术服务。


image-20230604220949124


2.1 依赖反转原则


依赖反转原则(Dependency Inversion Principle, DIP)是一种有效的设计原则,有助于减小模块间的耦合度,提高系统的扩展性和可维护性。依赖反转原则的核心思想是:高层模块不应直接依赖低层模块,它们都应该依赖抽象。抽象不应该依赖具体的实现,而具体的实现应当依赖于抽象。


在 DDD 的四层架构中,领域层是核心,是业务的抽象化,不应直接依赖其他任何层。这意味着领域层的业务对象应该与其他层(如基础设施层)解耦,而不是直接依赖于具体的数据库访问技术、消息队列技术等。但在实际运行时,领域层的对象需要通过基础设施层来实现数据的持久化、消息的发送等。


为了解决这个问题,我们可以使用依赖翻转原则。在领域层,我们定义一些接口(如仓储接口),用于声明领域对象需要的服务,具体的实现则由基础设施层完成。在基础设施层,我们实现这些接口,并将实现类注入到领域层的对象中。这样,领域层的对象就可以通过这些接口与基础设施层进行交互,而不需要直接依赖于基础设施层。


2.2 DDD四层架构的优势


在复杂的业务场景下,采用DDD的四层架构模型可以有效地解决使用MVC架构可能出现的问题:



  1. 职责分离:在DDD的设计中,我们尝试将业务逻辑封装到领域对象(如实体、值对象和领域服务)中。这样可以降低应用层(原MVC中的Service层)的复杂性,同时使得业务逻辑更加集中和清晰,易于维护和扩展。

  2. 领域建模:DDD的核心理念在于通过建立富有内涵的领域模型来更真实地反映业务需求和业务规则,从而提高代码的灵活性,使其更容易适应业务的变化。

  3. 明确的边界划分:DDD通过边界上下文(Bounded Context)的概念,对系统进行明确的边界划分。每个边界上下文都有自己的领域模型和业务逻辑,使得大规模团队协作更加清晰、高效。

  4. 易于测试:由于业务逻辑封装在领域对象中,我们可以直接对这些领域对象进行单元测试。同时,基础设施层(如数据库、缓存和消息队列)被抽象为接口,我们可以使用模拟对象(Mock Object)进行测试,避免了直接与真实中间件的交互,大大提升了测试的灵活性和便利性。


接下来看看如何在代码中遵循DDD的分层架构。


3. 如何实现DDD分层架构


为了遵循DDD的分层架构,在代码实现时有两种实现方法。


第一种是在模块中通过包进行隔离,即在模块中建立4个不同的代码包,分别对应领域层(Domain Layer)、应用层(Application Layer)、基础设施层(Infrastructure Layer)和用户接口层(User Interface Layer)。这种方法的优点是结构简单,易于理解和维护。但缺点是各层之间的依赖关系可能不够明确,容易导致代码耦合。


image.png


第二种实现方法是建立4个不同的Maven Module层,每个Module分别对应领域层、应用层、基础设施层和用户接口层。这种方法的优点是各层之间的依赖关系更加明确,有利于降低耦合度和提高代码的可重用性。同时,这种方法也有助于团队成员更好地理解和遵循DDD的分层架构。然而,这种方法可能会导致项目结构变得复杂,增加了项目的维护成本。


image.png


在实际项目中,可以根据项目规模、团队成员的熟悉程度以及项目需求来选择合适的实现方法。对于较小规模的项目,可以采用第一种方法,通过包进行隔离。而对于较大规模的项目,建议采用第二种方法,使用Maven Module层进行隔离,以便更好地管理和维护代码。无论采用哪种方法,关键在于确保各层之间的职责分明,遵循DDD的原则和最佳实践。


在DailyMart项目中,我最初打算采用第一种方法,通过包进行隔离。然而,在微信群中进行投票后,发现近90%的人选择了第二种方法。作为一个倾听粉丝意见的博主,我决定采纳大家的建议。因此,DailyMart将采用Maven Module层隔离的方式进行编码实践。
image.png


4. DDD中的数据模型


在DDD中,我们采用特定的模型来映射和处理不同的领域概念和责任,常见的有三种数据模型:实体对象(Entity)、数据对象(Data Object,DO)和数据传输对象(Data Transfer Object,DTO)。这些模型在DDD中有着明确的角色和使用场景:



  • Entity(实体对象): 实体对象代表业务领域中的核心概念,其字段和方法应与业务语言保持一致,与持久化方式无关。这意味着实体和数据对象可能具有完全不同的字段命名、字段类型,甚至嵌套关系。实体的生命周期应仅存在于内存中,无需可序列化和可持久化。

  • Data Object (DO、数据对象): DO可能是我们在日常工作中最常见的数据模型。在DDD规范中,数据对象不能包含业务逻辑,并且位于基础设施层,仅负责与数据库进行交互,通常与数据库的物理表一一对应。

  • DTO(数据传输对象): 数据传输对象主要用作接口层和应用层之间传递数据,例如CQRS模式中的命令(Command)、查询(Query)、事件(Event)以及请求(Request)和响应(Response)。DTO的重要性在于它能够适配不同的业务场景需要的参数,从而避免业务对象变成庞大而复杂的"万能"对象。


在DDD中,这三种数据对象在很多场景下需要相互转换,例如:




  1. Entity <-> DTO:在应用层返回数据时,需要将实体对象转换成DTO,这一般通过一个名为DTO Assembler的转换器来完成。




  2. Entity <-> DO:在基础设施层的Repository实现时,我们需要将实体转换为DO以存储到数据库。同样地,查询数据时需要将DO转换回实体。这通常通过一个名为Data Converter的转换器来完成。




当然,不管是Entity转DTO,还是Entity转DO,都会有一定的开销,无论是代码量还是运行时的操作来看。手写转换代码容易出错,而使用反射技术虽然可以减少代码量,但可能会导致显著的性能损耗。这里给用Java的同学推荐MapStruct这个库,MapStruct在编译时生成代码,只需通过接口定义和注解配置就能生成相应的代码。由于生成的代码是直接赋值,所以性能损耗可以忽略不计。


image.png



在SpringBoot老鸟系列中我推荐大家使用 Orika 进行对象转换,理由是只需要编写少量代码。但是在DDD中不同对象都有严格的代码层级,并且一般会引入专门的Assembler和Converter转换器,既然代码量省不了,必然要选择性能最高的组件。


各种转换器的性能对比:Performance of Java Mapping Frameworks | Baeldung



5. 小结


本篇文章详细介绍了DDD的分层架构,并详细解释了如何在项目代码中实现这种分层架构。同时,还详细DDD中三种常用的数据对象:数据对象(DO)、实体(Entity)和数据传输对象(DTO)。这三种数据对象的区别可以通过下图进行精炼总结:


image-20230523220725247


至此,我们已经深入解析了DDD中的核心概念。同时,我们的DailyMart商城系统已完成所有的前期准备,现在已经准备好进入实际的编码阶段。在接下来的章节中,我们将从实现注册流程开始,逐步探索如何在实际项目中应用DDD。


作者:飘渺Jam
来源:juejin.cn/post/7242129428511113272
收起阅读 »

全方位了解 JavaScript 类型判断

web
JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeof、instanceof 和 Object.prototype.toString()。这些方法各有...
继续阅读 »

JavaScript 是一种弱类型语言,因此了解如何进行类型检测变得尤为重要。在本文中,我们将深入探讨 JavaScript 中的三种常见类型检测方法:typeofinstanceofObject.prototype.toString()。这些方法各有特点,通过详细的解释,让我们更好地理解它们的用法和限制。


JS 类型判断详解:typeof、instanceof 和 Object.prototype.toString()


1. typeof


1.1 准确判断原始类型


typeof 是一种用于检测变量类型的操作符。它可以准确地判断除 null 之外的所有原始类型,包括 undefinedbooleannumberstringsymbol。(js中还有一种类型叫“大整型”)


console.log(typeof undefined); // 输出: "undefined"
console.log(typeof true); // 输出: "boolean"
console.log(typeof 42); // 输出: "number"
console.log(typeof "hello"); // 输出: "string"
console.log(typeof Symbol()); // 输出: "symbol"

1.2 判断函数


typeof 还可以用于判断函数类型。


function exampleFunction() {}
console.log(typeof exampleFunction); // 输出: "function"

解释说明: 注意,typeof 能够区分函数和其他对象类型,这在某些场景下是非常有用的。


2. instanceof


2.1 只能判断引用类型


instanceof 运算符用于判断一个对象是否是某个构造函数的实例。它只能判断引用类型。


const arr = [1, 2, 3];
console.log(arr instanceof Array); // 输出: true

2.2 通过原型链查找


instanceof 的判断是通过原型链的查找实现的。(原型链详解移步 => juejin.cn/post/730493… )如果对象的原型链中包含指定构造函数的原型,那么就返回 true


function Animal() {}
function Dog() {}

Dog.prototype = new Animal();

const myDog = new Dog();
console.log(myDog instanceof Dog); // 输出: true
console.log(myDog instanceof Animal); // 输出: true

解释说明: instanceof 通过检查对象的原型链是否包含指定构造函数的原型来判断实例关系。


3. Object.prototype.toString()


3.1 调用步骤


Object.prototype.toString() 方法用于返回对象的字符串表示。当调用该方法时,将执行以下步骤:



  1. 如果 this 值为 undefined,则返回字符串 "[object Undefined]"。

  2. 如果 this 值为 null,则返回字符串 "[object Null]"。

  3. this 转换成对象(如果是原始类型,会调用 ToObject 将其转换成对象)。

  4. 获取对象的 [[Class]] 内部属性的值。

  5. 返回连接的字符串 "[Object"、[[Class]]、"]"。


console.log(Object.prototype.toString.call(undefined)); // 输出: "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // 输出: "[object Null]"

console.log(Object.prototype.toString.call(42)); // 输出: "[object Number]"
console.log(Object.prototype.toString.call("hello")); // 输出: "[object String]"

console.log(Object.prototype.toString.call([])); // 输出: "[object Array]"
console.log(Object.prototype.toString.call({})); // 输出: "[object Object]"

function CustomType() {}
console.log(Object.prototype.toString.call(new CustomType())); // 输出: "[object Object]"

解释说明: Object.prototype.toString() 是一种通用且强大的类型检测方法,可以适用于所有值,包括原始类型和引用类型。


结语


了解 typeofinstanceofObject.prototype.toString() 的使用场景和限制有助于我们更加灵活地进行类型检测,提高代码的可读性和健壮性。选择合适的方法取决于具体的情境和需求,合理使用这些方法将使你的 JavaScript 代码更加优雅和可维护。


作者:skyfker
来源:juejin.cn/post/7305348040209629220
收起阅读 »

实现一个自己的vscode插件到发布

web
前言 本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学 文章最后又github地址 说在前面的话: 在看内容之前,确保你想了解如何开发一款 vscode 插件 内容以大白文教学形式输出,如果写的不清...
继续阅读 »

前言



本篇文章讲述了一个 vscode 插件开发的过程,希望能帮助到想了解 vscode 插件是如何开发的同学


文章最后又github地址



说在前面的话:



  1. 在看内容之前,确保你想了解如何开发一款 vscode 插件

  2. 内容以大白文教学形式输出,如果写的不清晰的地方,欢迎留言告诉我,这会帮助我理解到各位的痛点

  3. 看一万遍不如自己写一遍

  4. 学会这个思路,可以尝试去给开源的 UI 组件写提示插件,做出一些开源贡献

  5. 以上看完之后,请带着思考去看下面内容


一、为什么要做这个 vscode 插件🤔


为我们公司自己而用


在之前,我问到我们 UI设计师 老师,


我: 能给我一些我们的颜色的设计资源吗?


UI: 可以呀


然后就给了我一些主题色,辅色,然后线条色等等。


OK,当我拿到之后,对于颜色我们前端创建了一个 vars.scss 的文件夹,用于定义一些变量,大致是这样:


:root {
--tsl-doc-white: #fff;
// 文字色
--tsl-doc-gray-1: #e2e5e8;
--tsl-doc-gray-2: #d2d5d8;
--tsl-doc-gray-3: #b6babf;
--tsl-doc-gray-4: #afb2b7;
--tsl-doc-gray-5: #999b9f;
--tsl-doc-gray-6: #66686c;
--tsl-doc-gray-7: #3c3d3f;
}


使用 color: var(--tsl-doc-white) ,就达到目的,其实就是 css 变量,没什么的,当我们做完一系列之后,发现有个痛点~~


妈的(骂骂咧咧),这个颜色我起的名字是什么,笑死🤣,根本记不住,然后就导致了开发人员是一种什么情况,一边看变量文件一边写,我寻思,还不如直接写颜色来的快这样。


所以啊,所以,我在思考之后,我就想起,我一直在下一些提示插件,那么别人是如何实现的?


突然,我是不是也可以做一个,这样我们就可以避免这种问题了。于是就开始了我的插件开发之路。


二、如何实现一个 vscode 插件🖥️


2.1 一些或许有点用的文档资源


【vscode 官方文档】:Your First Extension | Visual Studio Code Extension API


【VS Code插件创作中文开发文档】: 你的第一个插件 - VS Code插件创作中文开发文档


2.2 需要提前准备的环境


Node环境: 大于16,主要使用 npm


安装一些脚手架(给我装就完了):


npm install -g yo generator-code

执行命令 yo code ,过一会儿就会看到下面这段话


# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press to choose default for all options below ###

# ? What'
s the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Enable stricter TypeScript checking in '
tsconfig.json'? Yes
# ? Setup linting using '
tslint'? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm

code ./helloworld

细节的一些可以看官方,有视频,😏,当你已经能成功输出 Hello World 然后,在回来看我这里


2.3 分析需求


明确知道自己要做什么:



  1. 输入我们指定的 tsl、--tsl 这些是不是要出现提示呀,告知我们可以选择哪些

  2. 鼠标放到 --tsl-doc-white 显式出对应的变量,不要觉得自己能记住了


就两个效果,明白之后我们就开始进行配置和 Coding


2.4 实现 variable-prompt


配置


主要还是 package.json 进行配置,先看我的这份:


{
"name": "variable-prompt",
"displayName": "variable-prompt",
"icon": "src/assets/tsl-logo.png", # 插件的图标就是这里来的
"description": "css variable prompt",# 描述插件的用途
"version": "1.0.0",
"publisher": "sakanaovo",
"engines": {
"vscode": "^1.56.0" # 这里要和 types/vscode 同步一下
},
"categories": [
"Other"
],
"main": "./extension.js",
"contributes": {},
# activationEvents 激活事件,这里配置了以下这些文件激活
"activationEvents": [
"onLanguage:vue",
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:javascriptreact",
"onLanguage:typescriptreact",
"onLanguage:scss",
"onLanguage:css",
"onLanguage:less"
],
"scripts": {
"lint": "eslint .",
"pretest": "npm run lint",
"build": "vsce package", # 打包命令
"test": "node ./test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.56.0",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.1",
"@types/node": "20.2.5",
"eslint": "^8.41.0",
"vsce": "^2.13.0", # 打包 后面会介绍
"glob": "^8.1.0",
"mocha": "^10.2.0",
"typescript": "^5.1.3",
"@vscode/test-electron": "^2.3.2"
}
}

这里看完了教帮助大家记忆训练


window高玩 :Ctrl + CCtrl + V


mac高玩:Cmd + CCmd + V


编码




  1. 创建 src/helper.jssrc/variableMap.js


    image-20230804133618857.png




  2. 清空根目录 extension.js 代码


    function activate(context) {
    console.log("启动成功");

    }

    // This method is called when your extension is deactivated
    function deactivate() {}

    module.exports = {
    activate,
    deactivate,
    };


    按下 F5 ,就可以启动容器,好的,那我们是不是想看这个 console 日志在哪儿,有两种



    • 第一种,在你开发插件vscode中查看调试控制台,一般在vscode左侧,找不到或者就 Ctrl+Shift+Y 就可以看是否打印

    • 第二种,在你启动的容器中,按 Ctrl+Shift+I ,也可以打开一个控制台,并查看你的日志信息,这是因为 vscode 是用 Electron 开发的,Electron 也是这样查看调试




  3. 实现 Hover 效果


    src/helper.js 中我们简单实现鼠标放上去就显式悬停效果


    const vscode = require("vscode");

    function provideHover(document, position, token) {
    // 获取鼠标位置的单词
    const word = document.getText(document.getWordRangeAtPosition(position));

    // 创建悬停内容
    const hoverText = `这是一个悬停示例,你鼠标放上去的单词是:${word}`;
    const hover = new vscode.Hover(hoverText);

    return hover;
    }

    module.exports = {
    provideHover
    };

    src/extension.js 中我们注入一下


    const vscode = require("vscode");
    const { provideHover } = require("./src/helper.js");
    // 添加一些文件类型
    const files = [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact",
    "vue",
    "scss",
    "less",
    "css",
    "sass",
    ];

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    }

    然后 F5 ,如果你已经启动过会有这个小标记,如下图:


    image-20230804135238225.png


    那我们点击一下框住的这个刷新按钮,然后在容器中调试一下,随便写下一段代码,下图是一个展示效果:


    image-20230804135427582.png


    OK,到这里,我们就实现了悬停了效果




  4. variableMap.js 完善一下映射规则


    大致如下:


    // 这个文件是 变量映射表 --tsl-color:#fa8c16
    const variableMap = {
    // 用于存放变量的映射关系
    "--tsl-primary-color": "#33c88e",
    "--tsl-doc-white": "#ffffff",
    "--tsl-doc-gray-1": "#e2e5e8",
    "--tsl-doc-gray-2": "#d2d5d8",
    "--tsl-doc-gray-3": "#b6babf",
    "--tsl-doc-gray-4": "#afb2b7",
    "--tsl-doc-gray-5": "#999b9f",
    "--tsl-doc-gray-6": "#66686c",
    "--tsl-doc-gray-7": "#3c3d3f",
    "--tsl-bg-gray-1": "#f2f4f4",
    "--tsl-warn-color": "#ff6813",
    "--tsl-accent-color": "#f9ba41",
    "--tsl-disabled-color-1": "#edfff8",
    "--tsl-disabled-color-2": "#b4e7d2",
    "--tsl-disabled-color-3": "#9eedcc",
    };

    module.exports = variableMap;


    非常简单,就是把我们的定义的一些,在这里写好就行




  5. 根据 variableMap.js 实现触发提示


    src/helper.js 中我们实现 provideCompletionItems


    const VARIABLE_RE = /--tsl(?:[\w-]+)?/;

    function provideCompletionItems(document, position) {
    const lineText = document.lineAt(position.line).text;

    const match = lineText.match(VARIABLE_RE);
    if (
    lineText.includes("tsl") ||
    match ||
    lineText.includes("--tsl") ||
    lineText.includes("t")
    ) {
    // 拿到 variableMap 中的所有变量
    const variables = Object.keys(variableMap);
    const completionItems = variables.map((variable) => {
    const item = new vscode.CompletionItem(variable);
    const color = variableMap[variable];
    item.detail = color;
    // 给detail 添加注释
    const formattedDetail = `这是一个颜色变量,值为 ${color}`;
    // 创建一个 MarkdownString
    const markdownString = new vscode.MarkdownString();
    // 添加普通文本和代码块
    markdownString.appendText(formattedDetail);
    // 将注释转换为 markdown 格式
    item.documentation = markdownString;
    item.kind = vscode.CompletionItemKind.Variable;
    return item;
    });
    return completionItems;
    }
    return [];
    }

    module.exports = {
    provideHover,
    provideCompletionItems,
    };


    src/extension.js 中我们注入一下


    const { provideHover, provideCompletionItems } = require("./src/helper.js");

    function activate(context) {
    console.log("启动成功");

    context.subscriptions.push(
    vscode.languages.registerHoverProvider(files, {
    provideHover,
    })
    );
    // 注入的提示
    context.subscriptions.push(
    vscode.languages.registerCompletionItemProvider(files, {
    provideCompletionItems,
    })
    );
    }

    刷新,和上面操作一样,然后我们输入 tsl 就会出现这样的一个效果,如下图:


    image-20230804140502999.png


    为了让能有点颜色看看我们需要小小的改造一下下,在 provideCompletionItems 中,把 kind 设置为 Color ,修改成这样:


    item.kind = vscode.CompletionItemKind.Color ,然后我们刷新启动看看效果:


    image-20230804141025071.png


    这样我们就实现了带颜色提示




  6. 改造我们的 Hover 效果


    src/helper.js 中我们把 provideHover 改成这样:


    function provideHover(document, position) {
    const lineText = document.lineAt(position.line).text;
    const regex = /--[\w-]+/g;
    const match = lineText.match(regex);
    const word = match[0];
    if (match.length > 0 && word.includes("--tsl")) {
    const completeVariable = match.find((variable) => variable.includes(word));
    const hoverText = variableMap[completeVariable];
    if (hoverText) {
    return new vscode.Hover(hoverText);
    }
    }
    }

    最终效果就是我们鼠标放在对应的变量上会告诉我们对应的16进制值是什么,效果如下:


    image-20230804143637675.png




好了,到这里,我们就已经完全实现了,我们可以运行 npm run build 然后选择 y 就可以生成一个 variable-prompt-1.0.0.vsix 文件


三、如何发布🎉


我只教你手动上传,因为我也是手动上传,自动挡还没学会。


访问这个: Manage Extensions | Visual Studio Marketplace 去掉地址最后的 sakanaovo 然后输入你自己的 publisher


选择这个 vscode 插件


image-20230804150207072.png


然后 variable-prompt-1.0.0.vsix 文件拖进去完毕


当然,如果你不想发布你可以选择在拓展中通过下图这种方式安装:


image-20230804151354242.png


四、结语💯


好久没有写文章了,上次写文章还是在上次。本章,我们通过简短的代码,实现了css变量提示vscode插件,希望能帮助到各位。


看完打开电脑,打开vscode,点开笔者文章链接,写下你的第一个Hello World 插件吧!先写5分钟


作者:sakana
来源:juejin.cn/post/7263305276397355063
收起阅读 »

实现仅从登录页进入首页才展示弹窗

web
需求:仅存在以下两种情况展示弹窗 登录页进入首页 用户保存了登录状态后通过地址栏或书签直接进入首页 本文用两种方案实现: 使用Document.referrer获取上个页面的 URI 使用sessionStorage存储弹窗展示数据 每个方案我都会讲...
继续阅读 »

需求:仅存在以下两种情况展示弹窗



  • 登录页进入首页

  • 用户保存了登录状态后通过地址栏或书签直接进入首页


本文用两种方案实现:



  • 使用Document.referrer获取上个页面的 URI

  • 使用sessionStorage存储弹窗展示数据



每个方案我都会讲讲解决思路和存在问题,记录一下自己的idea。


方案一:使用Document.referrer获取上个页面的 URI


解决思路


这是我想到的第一个解决方案。



  1. 在进入首页界面时,调用Document.referrer获取跳转到首页的起点页面 URI

  2. 将获取的 URI 与登录页的 URL 作比较

  3. 如一致,则展示弹窗;反之则不展示


实现伪代码如下:


const previousUrl = document.referrer;  // 获取上个页面的 URI
const loginUrl = '登录页 URL';
// 比较登录页 URL 与 previousUrl 是否相等 或 获得的 URI 是否为空,不相等则不展示。
const showDialog = loginUrl === previousUrl || previousUrl === '';

为什么还有一个previousUrl === ''判断呢?它判断的其实是第二种情况(直接进入首页),如果用户是通过地址栏或书签直接进入首页的话,Document.referrer返回的是空字符串


1699583988078.png


存在问题


讲到这,这个方案是不是已经解决我们在文章开头提出的需求了呢?从代码、逻辑以及实践是可以的,但是,我提出以下几个场景,大家判断一下弹窗是否会出现。


场景1 用户从登录页进入首页后(此时弹窗已成功展示并关闭),刷新首页,此时弹窗会再次出现吗?


场景2 登录页和首页的域名不一样,用户从登录页进入首页后会出现弹窗吗?


答案揭晓,前者会出现弹窗,后者则不会出现弹窗。


场景1解析


用户从登录页进入首页,在此前提下我们在首页调用Document.referrer得到登录页的 URI ;随后用户做刷新操作,再次在调用Document.referrer,获得新的 URI 和之前登录页 URI 是一致的,所以弹窗还会再次出现。


为了大家方便理解,我以GitHub为例:


我从 GitHub 登录页进入其主页,然后在控制台获取上个页面的 URI 。此时,我在主页点击刷新,再次在控制台调用Document.referrer,获得的 URI 与第一次获取的相同。


b669e-86sdi.gif


场景2解析


场景2是Document.referrer返回的 URI 与登录页 URL 不同导致的。其实不仅仅是域名不同会导致这个问题,文件路径或者文件名不同都有可能导致返回的 URI 与登录页 URL 不同。


小伙伴们有没有发现,我多次提及Document.referrer返回的字符串是 URI 。URI(统一资源标识符)与 URL(统一资源定位符)是有区别的,尤其,URI 并不是固定的,是相对的。(想了解更多“关于 URI 与 URL 区别”的小伙伴点击这里


先解释为什么登录页域名和首页域名不同,获得的 URI 就会和登录页不一样呢?举个例子,


这是我登录页的 URL:


1699595868152.png


我登录进入首页后,在控制台输出Document.referrer


1699596493250.png


发现没有,朋友们,获得的 URI 与登录页本身的 URL 不同,所以弹窗不展现。为什么会不同呢?再次贴出我另外一篇文章,点击了解更多哦




方案二:使用sessionStorage存储弹窗展示数据


众所周知,当用户打开一个窗口,会有一个sessionStorage对象;当窗口关闭时,会清除对应的sessionStorage。这一特性刚好符合我们的需求。


解决思路



  • 用户每次进入首页都会从sessionStorage获取 key 为弹窗ID的值

  • 判断值是否存在:

    • 如果值存在的话说明该弹窗已经展现过,不必再展示,直接跳出

    • 如果值为undefined则说明该弹窗在此窗口中没有展现过,则把 key 为弹窗ID的数据保存到sessionStorage,然后展示弹窗




伪代码如下:


const sessionItemKey = '弹窗ID';
if (sessionStorage.getItem(sessionItemKey)) return;
sessionStorage.setItem(sessionItemKey, 'Y');
this.dialogVisible = true;



存在问题


方案二似乎解决了方案一存在的刷新问题,也不会有获取 URI 与登录页 URL 不同的潜在问题,是个完美的解决方案!


不过,小伙伴们要注意一个场景:用户在一个窗口内多次登入和登出首页,弹窗会不会展示呢? 答案是不会展示。因为登入和登出操作都是在同一个会话当中发生的,多次登录进入首页,sessionStorage的数据都不会清除。


我们理一遍逻辑:



  • 用户打开新的登录页面窗口,登录成功进入首页

  • 首页跑了一次以上伪代码中值不存在的情况,在sessionStorage中保存了数据

  • 用户退出登录,再次进入登录页面(在同个会话中)

  • 用户登录成功后进入首页,首页跑了一次以上伪代码中值存在的情况


所以!sessionStorage的特性也会导致问题。不同的方案适用于不同的场景,就看大家怎么选择啦!


结束语


本次分享又到尾声啦!欢迎有疑惑或不同见解的小伙伴们在评论区留言哦~


作者:Swance
来源:juejin.cn/post/7299598252629901350
收起阅读 »

一位未曾涉足算法的初学者收获

正如标题所言,在我四年的编程经历中就没刷过一道算法题,这可能与我所编写的应用有关,算法对我而言提升不是特别大。加上我几乎都是在需求中学习,而非系统性的学习。所以像算法这种基础知识我自然就不是很熟悉。 那我为何会接触算法呢? 我在今年暑假期间有一个面试,当时面试...
继续阅读 »

正如标题所言,在我四年的编程经历中就没刷过一道算法题,这可能与我所编写的应用有关,算法对我而言提升不是特别大。加上我几乎都是在需求中学习,而非系统性的学习。所以像算法这种基础知识我自然就不是很熟悉。


那我为何会接触算法呢?


我在今年暑假期间有一个面试,当时面试官想考察一下我的算法能力,而我直接明摆了和说我不行(指算法上的不行),但面试官还是想考察一下,于是就出了道斐波那契数列作为考题。


但我毕竟也接触了 4 年的代码,虽然不刷算法,但好歹也看过许多文章和代码,斐波那契数列使用递归实现的代码也有些印象,于是很快我就写出了下面的代码作为我的答案。


function fib(n) {
if (n <= 1) return n

return fib(n - 1) + fib(n - 2)
}

面试官问我还有没有更好的答案,我便摇了摇头表示这 5 行不到的代码难道不是最优解?



事实上这份代码看起来很简洁,实际却是耗时最慢的解法



毫无疑问,在算法这关我肯定是挂了的,不过好在项目经验及后续的项目实践考核较为顺利,不然结局就是回去等通知了。最后面试接近尾声时,面试官友情提醒我加强基础知识(算法),强调各种应用框架不断更新迭代,但计算机的底层基础知识是不变的。于是在面试官的建议下,便有了本文。


好吧,我承认我是为了面试才去学算法的。


对上述代码进行优化


在介绍我是从何处学习算法以及从中学到了什么,不妨先来看看上题的最优答案是什么。


对于有接触过算法的同学而言,不难看出时间复杂度为 O(n²),而指数阶属于爆炸式增长,当 n 非常大时执行效果缓慢,且可能会出现函数调用堆栈溢出。


如果仔细观察一下,会发现这其中进行了非常多的重复计算,我们不妨将设置一个 res 变量来输出一下结果


function fib(n) {
if (n <= 1) {
return n
}

const res = fib(n - 1) + fib(n - 2)
console.log(res)
return res
}

当 n=7 时,所输出的结果如下


Untitled


这还只是在 n=7 的情况下,便有这么多输出结果。而在算法中要避免的就是重复计算,这能够高效的节省执行时间,因此不妨定义一个缓存变量,在递归时将缓存变量也传递进去,如果缓存变量中存在则说明已计算过,直接返回结果即可。


function fib(n, mem = []) {
if (n <= 1) {
return n
}

if (mem[n]) {
return mem[n]
}

const res = fib(n - 1, mem) + fib(n - 2, mem)
console.log(res)
mem[n] = res
return res
}

此时所输出的结果可以很明显的发现没有过多的重复计算,执行时间也有显著降低。


Untitled


这便是记忆化搜索,时间复杂度被优化至 O(n)。


可这还是免不了递归调用出现堆栈溢出的情况(如 n=10000 时)。


Untitled


从上面的解法来看,我们都是从”从顶至底”,比方说 n=7,会先求得 n=6,n=5 的结果,然后依次类推直至得到底层 n=1 的结果。


事实上我们可以换一种思路,先求得 n=1,n=2 的结果,然后依次类推上去,最终得到 n=6,n=7 的结果,也就是“从底至顶”,而这就是动态规划的方法。


从代码上来分析,因此我们可以初始化一个 dp 数组,用于存放数据状态。


function fib(n) {
const dp = [0, 1]

for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}

return dp[n]
}

最终 dp 数组的最后一个成员便是原问题的解。此时输出 dp 数组结果。


Untitled


且由于不存在递归调用,因此你当 n=10000 时也不在会出现堆栈溢出的情况(只不过最终的结果必定超出了 JS 数值可表示范围,所以只会输出 Infinity)


对于上述代码而言,在空间复杂度上能够从 O(n) 优化到 O(1),至于实现可以参考 空间优化,这里便不再赘述。


我想至少从这里你就能看出算法的魅力所在,这里我强烈推荐 hello-algo 这本数据结构与算法入门书,我的算法之旅的起点便是从这本书开始,同时激发起我对算法的兴趣。


两数之和


于是在看完了这本算法书后,我便打开了大名鼎鼎的刷题网站 LeetCode,同时打开了究极经典题目的两数之和



有人相爱,有人夜里开车看海,有人 leetcode 第一题都做不出来。



题干:



给定一个整数数组 nums  和一个整数目标值 target,请你在该数组中找出和为目标值target的那 两个 整数,并返回它们的数组下标。


你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。


你可以按任意顺序返回答案。



以下代码将会采用 JavaScript 代码作为演示。


暴力枚举


我初次接触该题也只会暴力解法,遇事不决,暴力解决。也很验证了那句话:不论多久过去,我首先还是想到两个 for。


var twoSum = function (nums, target) {
const n = nums.length

for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (nums[i] + nums[j] === target && i !== j) {
return [i, j]
}
}
}
}

当然针对上述 for 循环优化部分,比如说让 j = i + 1 ,这样就可以有效避免重复数字的循环以及 i ≠ j 的判断。由于用到了两次循环,很显然时间复杂度为 O(n²),并不高效。


哈希表


我们不妨将每个数字通过 hash 表缓存起来,将值 nums[i] 作为 key,将 i 作为 value。由于题目的条件则是 x + y = target,也就是 target - x = y,这样判断的条件就可以由 nums[i]+ nums[j] === target 变为 map.has(target - nums[i]) 。如果 map 表中有 y 索引,那么显然 target - nums[i] = y,取出 y 的索引以及当前 i 索引就能够得到答案。代码如下


var twoSum = function (nums, target) {
const map = new Map()

for (let i = 0; i < nums.length; i++) {
if (map.has(target - nums[i])) {
return [map.get(target - nums[i]), i]
}
map.set(nums[i], i)
}
}

而这样由于只有一次循环,时间复杂度为 O(N)。


双指针算法(特殊情况)


假如理想情况下,题目所给定的 nums 是有序的情况,那么就可以考虑使用双指针解法。先说原理,假设给定的 nums 为 [2,3,5,6,8],而目标的解为 9。在上面的做法中都是从索引 0 开始枚举,也就是 2,3,5…依次类推,如果没找到与 2 相加的元素则从 3 开始 3,5,6…依次类推。


此时我们不妨从最小的数最大的数开始,在这个例子中也就是 2 和 8,很显然 2 + 8 > 9,说明什么?说明 8 和中间所有数都大于 9 即 3+8 ,5+8 肯定都大于 9,所以 8 的下标必然不是最终结果,那么我们就可以把 8 排除,从 [2,3,5,6] 中找出结果,同样的从最小和最大的数开始,2 + 6 < 9 ,这又说明什么?说明 2 和中间这些数相加肯定都下雨 9 即 2+3,2+5 肯定都小于 9,因此 2 也应该排除,然后从 [3,5,6] 中找出结果。就这样依次类推,直到找到最终两个数 3 + 6 = 9,返回 3 与 6 的下标即可。


由于此解法相当于有两个坐标(指针)不断地向中间移动,因此这种解法也叫双指针算法。当然,要使用该方式的前提是输入的数组有序,否则无法使用。


用代码的方式来实现:



  1. 定义两个坐标(指针)分别指向数组成员最左边与最右边,命名为 left 与 right。

  2. 使用 while 循环,循环条件为 left < right。

  3. 判断 nums[left] + nums[right]target 的大小关系,如果相等则说明找到目标(答案),如果大于则 右指针减 1 right—-,小于则左指针加 1 left++


function twoSum(nums, target) {
let left = 0
let right = nums.length - 1

while (left < right) {
const sum = nums[left] + nums[right]
if (sum === target) {
return [left, right]
}

if (sum > target) {
right--
} else if (sum < target) {
left++
}
}
}



针对上述两道算法题浅浅的做个分享,毕竟我还只是一名初入算法的小白。对我而言,我的算法刷题之旅还有很长的一段时间。且看样子这条路可能不会太平坦。


算法对我有用吗?


在我刷算法之前,我在网上看到鼓吹算法无用论的人,也能看到学算法却不知如何应用的人。


这也不禁让我思考 🤔,算法对我所开发的应用是否真的有用呢?


在我的开发过程中,往往面临着各种功能需求,而通常情况下我会以尽可能快的速度去实现该功能,至于说这个功能耗时 1ms,还是 100 ms,并不在乎。因为对我来说,这种微小的速度变化并不会被感知到,或者说绝大多数情况下,处理的数据规模都处在 n = 1 的情况下,此时我们还会在意 n² 大还是 2ⁿ 大吗?


但如果说到了用户感知到卡顿的情况下,那么此时才会关注性能优化,否则,过度的优化可能会成为一种徒劳的努力。


或许正是因为我都没有用到算法解决实际问题的经历,所以很难说服自己算法对我的工作有多大帮助。但不可否认的是,算法对我当前而言是一种思维上的拓宽。让我意识到一道(实际)问题的解法通常不只有一种,如何规划设计出一个高效的解决方案才是值得我们思考的地方。


结语


借 MIT 教授 Erik Demaine 的一句话



If you want to become a good programmer, you can spend 10 years programming, or spend 2 years programming and learning algorithms.



如果你想成为一名优秀的程序员,你可以花 10 年时间编程,或者花 2 年时间编程和学习算法。


这或许就是学习算法的真正意义。


参考文章


初探动态规划


学习算法重要吗?


作者:愧怍
来源:juejin.cn/post/7278952595423133730
收起阅读 »

展望GPU“一卡难求”现状下AI初创企业的出路

Strategies for the GPU-Poor 原文链接:matt-rickard.com/strategies-… 原文作者:Matt Rickard 译者:Regan Yue P.S. 原文作者并没有审校过本译文,且译者在翻译本内容时夹带有个人对原...
继续阅读 »

Strategies for the GPU-Poor


原文链接:matt-rickard.com/strategies-…


原文作者:Matt Rickard


译者:Regan Yue


P.S. 原文作者并没有审校过本译文,且译者在翻译本内容时夹带有个人对原文的理解,并尝试对其进行解读。可能理解或解读有误,麻烦请在评论区指出!



编者按:GPU已然成为当下的硬通货,尤其是在人工智能领域。然而,初创企业并不一定需要大量GPU才能在这个领域取得成功。


本文提供了一些有效的策略,可以帮助GPU资源有限的初创企业取得竞争优势。这些策略包括:在用户端进行模型推理来避免网络延迟,将产品服务商品化以获得更多流量,专注于某个垂直领域快速响应市场需求,以及利用最新技术提高模型推理效率等。


期望读者通过遵循这些策略,在GPU资源有限的情况下,也能在人工智能领域开拓出一片天地。



如今GPU已经成为了一种硬通货,这种用于处理图形和并行计算的硬件在人工智能等计算密集型任务中广泛应用,已经到了供不应求的局面。然而,由于供应链问题、全球芯片短缺等各种原因,GPU如今“一卡难求”。


由于供应满足不了需求,导致现在二手市场的GPU价格飙升,即便愿意出高价,还往往需要到处寻找卖家。云计算提供商的GPU资源也面临供应短缺的问题,导致用户无法获得足够的GPU实例,即使在云计算的按需定价中,GPU的价格也没有显著降低,因为供需不平衡导致价格仍然较高。


但是,对于缺少 GPU 的初创企业来说,在人工智能领域可以有其他不同的策略。初创公司并不一定需要大量的GPU资源才能取得竞争优势,可以通过其他方式获得竞争优势,可以利用硬件和软件的发展周期,选择具有较低成本和高性能的替代硬件,或者是凭借其独特的分销策略。因此,在未来几个季度内,GPU资源匮乏的初创公司甚至可能会在市场中占据较好的位置。


那么作为一家缺少 GPU 的初创企业,该如何运作呢?


我有几个想法:



  • 在用户端进行推理。将小型模型部署在终端用户的设备上进行推理。目前理论上可以在浏览器或手机端上实现。这样做可以消除网络延迟带来的负面影响,并能更好的保护用户隐私,但受限于用户设备的计算能力,所以只能使用较小的模型。

  • 将产品\服务商品化。HuggingFace是一个集上传、下载和展示模型于一体的平台。虽然这个平台不是运行模型的最佳选择,但该平台拥有大量源源不断的优秀机器学习研究人员和黑客的流量。换句话说,通过在HuggingFace平台上展示我们的模型,可以从该平台获取更多的新用户和流量。

  • 不引入太多额外的复杂功能,而是专注于提供基本的封装和抽象。利用模型推理层(inference layer)不断增长的竞争优势,选择成本最低的提供商,而无需在特定模型的优化上浪费时间。大语言模型在理论上是可以互换的,即可以在不同的提供商之间进行切换,而不会对产品效果产生太大影响。

  • 专注于某一特定的垂直市场。与其他公司花费数月时间进行大模型训练相比,GPU资源有限的初创公司可以专注于解决实际的客户问题。这意味着初创公司可以更快地响应市场需求并提供解决方案,而不需要依赖GPU进行大规模的模型训练。在产品与市场需求相适应之前,初创公司可以通过解决实际问题来建立自己的竞争优势,而不必过早地投入大量的计算资源

  • 想办法提高推理效率。尽管初创公司可能没有使用大型GPU训练集群的能力,但可以利用最新的开源大模型推理优化技术。这些技术可以通过优化大模型推理过程来提高效率,从而在不需要大量计算资源的情况下获得更优秀的性能和更好的运行效果。


作者:菜鸟魔王
来源:juejin.cn/post/7305308668232056841
收起阅读 »

别以为逃离大城市你就能舒适了,小城市可比你想象的内卷!

大家早上好,今天聊一下最近的一些经历和感悟还有回到三线城市的感悟,希望对大家有一定的帮助! 一、我不适合躺,也躺不了 我毕业之后就到了成都,去了一家做基础软件的上市公司,不过我们部门还是属于业务部门,差不多干了两年,因为公司属于比较传统的企业,自然没有互联网的...
继续阅读 »

大家早上好,今天聊一下最近的一些经历和感悟还有回到三线城市的感悟,希望对大家有一定的帮助!


一、我不适合躺,也躺不了


我毕业之后就到了成都,去了一家做基础软件的上市公司,不过我们部门还是属于业务部门,差不多干了两年,因为公司属于比较传统的企业,自然没有互联网的内卷,基本上没什么加班,特别是第二年,基本上没啥事做!


这种情况下我开始意识到了问题,如果再这样呆下去,对自己的发展会很不利,如果部门的业务再推动不了,那么到时候也得面临调整,总之,留与不留对自己大多都是不好的,所以我就准备离开了!


成都在别人的映像中是一个休闲城市,吃喝玩乐,但是那是属于有钱人的,打工人只有辛苦和内卷,我就住在天府软件园对面,每晚软件园里面都是灯火通明的!


二、机缘巧合


不过对于我来说,我已经没有想法继续留在成都了,当时是想去杭州或者深圳,不过在离职完的第二天,贵阳这边的公司就叫我面试,我都不记得是啥时候投的简历!


因为两年来都没有面过试了,所以练练嘴皮子,经过几面后,给了offer,从开始面试到给offer差不多半个月,给了offer后我十来天就去入职了!


从离职后到进去新公司这段时间差不多一个月,我就在成都耍了20来天,一直在做思想斗争,说实话,去一线城市和回故乡这两个抉择是很难做选择的,为什么呢?我表达一下我的观点。


三、我认为的大城市小城市


首先,一线的机会肯定会比小城市的机会多,接触到的人也相对来说比较厉害一点,这样自己的视野也会开阔一点,不过这也要根据自己的能力来看,如果自己本身就啥也不是,那基本上也无缘接触到厉害的人!


其次,一线的人情世故不像小城市那么复杂,特别像深圳这样的城市,大家都是从外面来的,所以来了就是深圳人,包容性比较高,这样的话能够减少一些心理压力,而小城市则不然,因为好一点的单位,保安都会和你吹他家那个亲戚是省里的,不然他也谋不了这个职位,往上就更不用说,哈哈!


所以小城市的人因为地缘原因,就会产生一定的优越感,所以整体下来说,其实是不那么包容的,不那么开放的,在这样的环境下对自己或多或少有一定的影响,当然,大城市也会有,只是相对于小城市来说会轻很多!


以上只是很小的一部分,还有很多就没必要说了!


四、为什么我还要回到小城市


上面也说了因为很快拿到offer,还是在自己的故乡,而且这个企业在贵州省内来说也算比较好,属于本土企业,所以这算是一个因素,还有就是心中有一点想法想回到故土,因为很多时候确实会思念家乡的,所以二者一碰撞,自然给自己顺理成章找了一个理由回来,当然,也可以说是自己无能,这也是没错的!


五、我以为小城市相对来说比较轻松


我并不想把自己的时间都花费在工作上,因为我是一个把生活和工作分得比较清楚的人,现在是这样,以后也是这样!


这边公司是早上9:00晚上5:30,中午休息两小时,所以口头上听着倒是挺舒服,但是当任务压下来,一切美梦都是泡沫。


我来了两三个月了,除了第一个月没加班,后面基本上都加班,周末有时候也来加班,前天晚上还通宵上线了,昨天下午四点过睡醒来,就觉得应该写点什么!


不光是我们公司,我听在这边工作了很久的同事说,大家都差不多这样,所以卷不卷就不用说了。


六、不光是互联网卷,其他的更卷


前天晚上通宵上线的时候,我和几个同事聊天,我说实在干不动了,我准备考公了,他说:“你别想了,我考了那么多次都没上,更别说你连书都没去看过,你拿什么和人家卷!”。


我省内的很多朋友和同学现在都在考公,不过据我所知,基本没几个真的考上了公务员,有些已经毕业很多年,一边上班一边考,有些一毕业就全职考!


不光是考公务员,在贵阳这个地方,做啥工作都卷,用他们的话说,你去当销售,不打满五百个电话你出不来!


没错,小城市的卷是你想不到的,我们常说大城市太卷了,大城市虽然卷,但是机会多,薪酬高一点,而小城市不但机会少,而且薪酬也少,但是人多,特别对于贵阳这种城市,经济比较落后,做生做死三千几真的不是开玩笑的!


七、后悔了吗


我想说,一点也不后悔,虽然之前在成都很轻松,但是那不是我想要的,现在很忙,也累,不过也不是我想要的,那么这不是自相矛盾了吗?其实一点都不矛盾!


我始终觉得,如果你脑子里觉得你这辈子只有靠打工才能赚到钱,那么你将劳累一辈子!


现在打工对于我来说更多是积淀经验,养活自己,我不会迷失自己的,我清楚自己想要过什么生活,做什么样的人,加之我这个人的物质欲望比较低,所以我不会把自己活得很累,而是把钱和时间更多的用在提升自己和丰富自己上面!


八、建议


我想说人各有志,每个人所追求的人生不一样,每个人的欲望不一样,所以无论去一线还是小城市都没有真正的对与错!


首先对于我来说,我上面已经表达过,我对物质的欲望比较低,我是没多大的欲望,但是我很爱钱,我想做其他的事,发展其他的,所以对我来说,去任何一个地方的区别都不是很大,无非钱多点少点!


不过对于刚离开学校的朋友,如果想在职业生涯有所建树,并且家里条件不怎么好的,没啥背景资源的,尽量去一线城市,即使赚不了钱也长长见识,如果可以的话,尽量去互联网企业,别去传统行业,因为对自己的发展会比较不利!


如果一毕业就回到自己的那个小地方,那么就可能一辈子都出不来了,当然,如果有能力在自己的家乡混得风生水起,那么是真的牛逼,不过对于没啥资源背景的,基本上可能性不大!


九、最后


现在这个环境真的很严峻,各个行业都很难,真的很卷,所以无论一线还是十八线都一样,普通人想躺平基本上不可能!


对于我们来说,无论处于什么样的环境,都不要过于依赖它,不要故步自封,一定要保持思想活跃,有居安思危的意识,做好准备,这样才能保证在时代的浪潮中不被拍打得遍体鳞伤!


最后借用一句话:


人生成长最有效的方法,就是无论命运把你抛在任何一个点上,你就地展开做力所能及的事情


作者:刘牌
来源:juejin.cn/post/7288603764264075316
收起阅读 »

别再抱怨后端一次性传给你 1w 条数据了,几行代码教会你虚拟滚动!

web
如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。 我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚...
继续阅读 »

如果后端一次性传给你 1 万条数据,该怎么办,当然是让他圆润的走开,哈哈,开个玩笑。虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将基于 vue3 实现一个简单的虚拟滚动。



我们都知道,如果一次性展示所有的数据,那么会造成页面卡顿,虚拟滚动的原理就是将数据根据滚动条的位置进行动态截取,只渲染可视区域的数据,这样浏览器的性能就会大大提升,废话不多说,我们开始。


具体实现


首先,我们先模拟 500 条数据


const data = new Array(500).fill(0).map((_, i) => i); // 模拟真实数据

然后准备以下几个容器:


<template>
<div class="view-container">
<div class="content-container"></div>
<div class="item-container">
<div class="item"></div>
</div>
</div>
</template>


  • view-container是展示数据的可视区域,即可滚动的区域

  • content-container是用来撑起滚动条的区域,它的高度是实际的数据长度乘以每条数据的高度,它的作用只是用来撑起滚动条

  • item-container是实际渲染数据的区域

  • item则是具体渲染的数据


我们给这几个容器一点样式:


.view-container {
height: 400px;
width: 200px;
border: 1px solid red;
overflow-y: scroll;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.content-container {
height: 1000px;
}

.item-container {
position: absolute;
top: 0;
left: 0;
}

.item {
height: 20px;
}

view-container固定定位并居中,overflow-y设置为scroll


content-container先给它一个1000px的高度;


item-container绝对定位,topleft都设为 0;


每条数据item给他一个20px的高度;


先把 500 条数据都渲染上去看看效果:


初始渲染


这里我们把高度都写死了,元素的高度是实现虚拟滚动需要用到的变量,因此肯定不能写死,我们可以用动态绑定style来给元素加上高度:


首先定义可视高度和每一条数据的高度:


const viewHeight = ref(400); // 可视容器高度
const itemHeight = ref(20); // 每一项的高度

用动态绑定样式的方式给元素加上高度:


<div class="view-container" :style="{ height: viewHeight + 'px' }">
<div
class="content-container"
:style="{
height: itemHeight * data.length + 'px',
}"

>
</div>
<div class="item-container">
<div
class="item"
:style="{
height: itemHeight + 'px',
}"

>
</div>
</div>
</div>

content-container 使用每条数据的高度乘以数据总长度来得到实际高度。


然后我们定义一个数组来动态存放需要展示的数据,初始展示前 20 条:


const showData = ref<number[]>([]); // 显示的数据
showData.value = data.slice(0, 20); // 初始展示的数据 (前20个)

showData里的数据才是我们要在item遍历渲染的数据:


<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>

接下来我们就可以给view-container添加滚动事件来动态改变要展示的数据,具体思路就是:



  1. 根据滚动的高度除以每一条数据的高度得到起始索引

  2. 起始索引加上容器可以展示的条数得到结束索引

  3. 根据起始结束索引截取数据


具体代码如下:


const scrollTop = ref(0); // 初始滚动距离
// 滚动事件
const handleScroll = (e: Event) => {
// 获取滚动距离
scrollTop.value = (e.target as HTMLElement).scrollTop;
// 初始索引 = 滚动距离 / 每一项的高度
const startIndex = Math.round(scrollTop.value / itemHeight.value);
// 结束索引 = 初始索引 + 容器高度 / 每一项的高度
const endIndex = startIndex + viewHeight.value / itemHeight.value;
// 根据初始索引和结束索引,截取数据
showData.value = data.slice(startIndex, endIndex);

console.log(showData.value);
};

打印一下数据看看数据有没有改变:


滚动数据改变


可以看到数据是动态改变了,但是页面上却没有按照截取的数据来展示,这是因为什么呢? 查看一下元素:


问题


可以看到存放数据的元素 也就是 item-container 也跟着向上滚动了,所以我们不要让它滚动,可以通过调整它的 translateY 的值来实现,使其永远向下偏移滚动条的高度


<div
class="item-container"
:style="{
transform: 'translateY(' + scrollTop + 'px)',
}"

>

<div
class="item"
:style="{
height: itemHeight + 'px',
}"

v-for="(item, index) in showData"
:key="index"
>

{{ item }}
</div>
</div>

看效果:


效果


文章到此就结束了。这只是一个简单的实现,还有很多可以优化的地方,例如滚动太快出现白屏的现象等,大家可以尝试一下,并试着优化一下。


希望本文能够对你有帮助。


作者:路遥知码li
来源:juejin.cn/post/7301911743487590452
收起阅读 »

🔥🔥通过浏览器URL地址,5分钟内渗透你的网站!很刑很可拷!

今天我来带大家简单渗透一个小破站,通过这个案例,让你深入了解为什么很多公司都需要紧急修复各个中间件的漏洞以及进行URL解析拦截等重要操作。这些措施的目的是为了保护网站和系统的安全性。如果不及时升级和修复漏洞,你就等着被黑客攻击吧! 基础科普 首先,我想说明一下...
继续阅读 »

今天我来带大家简单渗透一个小破站,通过这个案例,让你深入了解为什么很多公司都需要紧急修复各个中间件的漏洞以及进行URL解析拦截等重要操作。这些措施的目的是为了保护网站和系统的安全性。如果不及时升级和修复漏洞,你就等着被黑客攻击吧!


基础科普


首先,我想说明一下,我提供的信息仅供参考,我不会透露任何关键数据。请不要拽着我进去喝茶啊~


关于EXP攻击脚本,它是基于某种漏洞编写的,用于获取系统权限的攻击脚本。这些脚本通常由安全研究人员或黑客编写,用于测试和演示系统漏洞的存在以及可能的攻击方式。


而POC(Proof of Concept)概念验证,则是基于获取到的权限执行某个查询的命令。通过POC,我们可以验证系统的漏洞是否真实存在,并且可以测试漏洞的影响范围和危害程度。


如果你对EXP攻击脚本和POC感兴趣,你可以访问EXP攻击武器库网站:http://www.exploit-db.com/。 这个网站提供了各种各样的攻击脚本,你可以在这里了解和学习不同类型的漏洞攻击技术。


另外,如果你想了解更多关于漏洞的信息,你可以访问漏洞数据库网站:http://www.cvedetails.com/。 这个网站提供了大量的漏洞信息和漏洞报告,你可以查找和了解各种不同的漏洞,以及相关的修复措施和建议。


但是,请记住,学习和了解这些信息应该用于合法和道德的目的,切勿用于非法活动。网络安全是一个重要的问题,我们应该共同努力保护网络安全和个人隐私。


利用0day or nday 打穿一个网站(漏洞利用)



  • 0day(未公开)和nday(已公开)是关于漏洞的分类,其中0day漏洞指的是尚未被公开或厂商未修复的漏洞,而nday漏洞指的是已经公开并且有相应的补丁或修复措施的漏洞。

  • 在Web安全领域,常见的漏洞类型包括跨站脚本攻击(XSS)、XML外部实体注入(XXE)、SQL注入、文件上传漏洞、跨站请求伪造(CSRF)、服务器端请求伪造(SSRF)等。这些漏洞都是通过利用Web应用程序的弱点来实施攻击,攻击者可以获取用户敏感信息或者对系统进行非法操作。

  • 系统漏洞是指操作系统(如Windows、Linux等)本身存在的漏洞,攻击者可以通过利用这些漏洞来获取系统权限或者执行恶意代码。

  • 中间件漏洞是指在服务器中常用的中间件软件(如Apache、Nginx、Tomcat等)存在的漏洞。攻击者可以通过利用这些漏洞来获取服务器权限或者执行恶意操作。

  • 框架漏洞是指在各种网站或应用程序开发框架中存在的漏洞,其中包括一些常见的CMS系统。攻击者可以通过利用这些漏洞来获取网站或应用程序的权限,甚至控制整个系统。


此外,还有一些公司会组建专门的团队,利用手机中其他软件的0day漏洞来获取用户的信息。


我今天的主角是metinfo攻击脚本: admin/column/save.php+【秘密命令】(我就不打印了)


蚁剑远控工具


中国蚁剑是一款开源的跨平台网站管理工具,它主要面向合法授权的渗透测试安全人员和常规操作的网站管理员。蚁剑提供了丰富的功能和工具,帮助用户评估和加强网站的安全性。


你可以在以下地址找到蚁剑的使用文档和下载链接:http://www.yuque.com/antswordpro…


然后今天我来破解一下我自己的网站,该网站是由MetInfo搭建的,版本是Powered by MetInfo 5.3.19


image


开始通过url渗透植入


现在我已经成功搭建好了一个网站,并且准备开始破解。在浏览器中,我直接输入了一条秘密命令,并成功地执行了它。下面是执行成功后的截图示例:


image


好的,现在我们准备启用我们的秘密武器——蚁剑。只需要输入我攻击脚本中独有的连接密码和脚本文件的URL地址,我就能成功建立连接。连接成功后,你可以将其视为你的远程Xshell,可以随意进行各种操作。


image


我们已经定位到了我们网站的首页文件,现在我们可以开始编写一些内容,比如在线发牌~或者添加一些图案的元素等等,任何合适的内容都可以加入进来。


image


不过好像报错了,报错的情况下,可能是由于权限不足或文件被锁导致的。


image


我们可以通过查看控制台来确定导致问题的原因。


image


我仔细查看了一下,果然发现这个文件只有root用户才有操作权限。


image


find提权


好的,让我们来探讨一下用户权限的问题。目前我的用户权限是www,但是我想要获得root权限。这时候我们可以考虑一下suid提权的相关内容。SUID(Set User ID)是一种Linux/Unix权限设置,允许用户在执行特定程序时以该程序所有者的权限来运行。然而,SUID提权也是一种安全漏洞,黑客可能会利用它来获取未授权的权限。为了给大家演示一下,我特意将我的服务器上的find命令设置了suid提权。我们执行一下find index.php -exec whoami \;命令,如果find没有设置suid提权的话,它仍然会以www用户身份输出结果。所以,通过-exec ***这个参数,我省略了需要执行的命令,我们可以来查看一下index.php的权限所有者信息。


image


我来执行一下 find index.php -exec chown www:index.php \; 试一试看看是否可以成功,哎呦,大功告成。我再次去保存一下文件内容看看是否可以保存成功。


image


果不其然,我们的推测是正确的。保存文件失败的问题确实是由于权限问题引起的。只有当我将文件的所有者更改为当前用户时,才能顺利保存成功。


image


让我们现在来看一下进行这些保存后的效果如何。


image


总结


当然了,黑客的攻击手段有很多。除了自己做一些简单的防护措施外,如果有经济条件,建议购买正规厂商的服务器,并使用其安全版本。例如,我在使用腾讯云的服务器进行攻击时,会立即触发告警并隔离病毒文件。在最次的情况下,也要记得拔掉你的网线,以防攻击波及到其他设备。


在这篇文章中,我仅仅演示了使用浏览器URL地址参数和find提权进行安全漏洞渗透的一些示例。实际上,针对URL地址渗透问题,现在已经有很多免费的防火墙可以用来阻止此类攻击。我甚至不得不关闭我的宝塔面板的免费防火墙才能成功进入系统,否则URL渗透根本无法进行。


至于find提权,你应该在Linux服务器上移除具有提权功能的命令。这是一种非常重要的安全措施,以避免未经授权的访问。通过限制用户权限和删除一些危险命令,可以有效防止潜在的攻击。


总而言之,我们应该时刻关注系统的安全性,并采取必要的措施来保护我们的服务器免受潜在的攻击。


作者:努力的小雨
来源:juejin.cn/post/7304263961238143011
收起阅读 »

浏览器跨标签星球火了,简单探究一下实现原理

web
一、前言 最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!! 准备自己也搞搞玩一下 二、实现 原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的...
继续阅读 »

output3.gif


一、前言


最近 推特上 一位懂设计和写代码的大神一个两个浏览器之间 星球粒子交互的动画火了, 让人看了大呼脑洞大开, 浏览器竟然还能这么玩!!!


准备自己也搞搞玩一下


output3.gif


二、实现


原作者的粒子动画非常炫酷, 但是不是我们本文重点, 我们通过一个元素在不同窗口的拖拽实现一个可以变幻的例子来学习一下原理, 后续在实现一个稍微复杂的多窗口的小游戏。关于粒子动画的内容,有兴趣的小伙伴可以自己实现


其实实现类似的功能需要的难点并不多,不在乎以下几个步骤



  • 1、 屏幕坐标和窗口坐标转换

  • 2、 跨标签通讯


1、 先来看第一个点, 获取屏幕坐标与窗口坐标


// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

我们先简单实现一个卡片, 通过url上面传递颜色值, 设置定位


在卡片本上设置上点击拖动等事件


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>


2、 跨标签传输


单个元素的拖动就实现了, 很简单, 如何让其他标签的元素也能同步进行, 需要实现跨标签方案了, 可以参考该文章- 跨标签页通信的8种方式


我们就选择第一种,使用 BroadCast Channel, 使用也很简单


// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
console.log('接收',event)
};
// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

只需要在移动时发送消息, 再其他标签页就可以接收到值了, 现在关键的就是收到发送的坐标点后, 如何处理, 其实关键就是要让几个窗口的卡片位置转化到同一个纬度, 让其再超出浏览器的时候,再另一个窗口的同一个位置出现, 所以就需要将窗口坐标转化成屏幕坐标,发送给其他窗口后, 再转化成窗口坐标进行渲染即可


// 鼠标移动发送消息的时候,窗口坐标转化成屏幕坐标
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);

// 接收消息的时候,屏幕坐标转化成窗口坐标
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

完整代码,在最下面


三、总结


本文通过移动一个简单的图形, 在不同浏览器之间穿梭变换, 初步体验了多个浏览器之间如何进行交互, 通过拖拽元素,通过跨标签的通讯, 将当前窗口元素的位置进行发送, 另一个窗口进行实时接收, 然后通过屏幕坐标和窗口坐标进行转换, 就能实现,从一个浏览器拖动到另一个浏览器时, 变化元素颜色的功能了, 当然变化背景色只是举例子, 你也可以变化扑克牌, 变化照片, 这样看起来像变魔术一样,非常神奇,看似浏览器不同标签之间没有联系,当以这种方式产生联系后, 就会产生很多不可思议的神奇事情。 就像国外大神的多标签页的两个星球粒子, 产生吸引 融合的效果。原理其实是一样的。


后续前瞻


在通过小demo的学习,知道多浏览器的玩法后, 接下来的我们会实现一个更有意思的小游戏,通过浏览器化身一个小木棒, 接小球游戏, 先看一下 gif, 接下来的文章会写具体实现


output3.gif


完整代码实现如下


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>跨标签通讯</title>
</head>
<style>
.card {
width: 300px;
height: 300px;
background-color: #f00;
position: fixed;
top: 100px;
left: 100px;
}
</style>
<body>
跨标签通讯
<div class="card">card</div>
</body>
<script>
const barHeight = () => window.outerHeight - window.innerHeight;
const cardDom = document.querySelector(".card");
cardDom.style.top = 100 + "px";
cardDom.style.left = 100 + "px";
cardDom.style.background =
new URLSearchParams(window.location.search).get("color") || "red";

// 屏幕坐标转换为窗口坐标
const screenToClient = (screenX, screenY) => {
const clienX = screenX - window.screenX;
const clienY = screenY - window.screenY - barHeight();
return [clienX, clienY];
};

// 窗口坐标转换为屏幕坐标
const clientToScreen = (clienX, clienY) => {
const screenX = clienX + window.screenX;
const screenY = clienY + window.screenY + barHeight();
return [screenX, screenY];
};

// 创建 Broadcast Channel
const channel = new BroadcastChannel("myChannel");
// 监听消息
channel.onmessage = (event) => {
// 处理接收到的消息
const [clienX, clienY] = screenToClient(...event.data);
// 不同窗口的卡片要在同一个位置, 要放到同一个坐标系下面,保持屏幕坐标一致
cardDom.style.left = clienX + "px";
cardDom.style.top = clienY + "px";
};

// 发送消息
const sendMessage = (message) => {
channel.postMessage(message);
};

window.onload = function () {
cardDom.onmousedown = function (e) {
cardDom.style.cursor = "pointer";
let x = e.pageX - cardDom.offsetLeft;
let y = e.pageY - cardDom.offsetTop;
window.onmousemove = function (e) {
cardDom.style.left = e.clientX - x + "px";
cardDom.style.top = e.clientY - y + "px";
// 发送消息
const clientCoordinateX = e.clientX - x;
const clientCoordinateY = e.clientY - y;
const ScreenCoordinate = clientToScreen(
clientCoordinateX,
clientCoordinateY
);
sendMessage(ScreenCoordinate);
};
window.onmouseup = function () {
window.onmousemove = null;
window.onmouseup = null;
cardDom.style.cursor = "unset";
};
};
};
</script>
</html>

作者:重阳微噪
来源:juejin.cn/post/7304598711992598566
收起阅读 »

杭州程序员打工的一天

关于早晨 8:40 闹钟搅乱我一池美梦,手伸出去探手机,窝在被子里睁开惺忪的双眼扫了一眼手机,略刷个十分钟强制让自己开机,家人们这种痛苦谁能懂? 8:50 起床洗漱换衣服 9:10 戴上耳机就出门,骑着我的小毛驴一边听音乐一边拼命赶路。P.S. ...
继续阅读 »

关于早晨



  • 8:40 闹钟搅乱我一池美梦,手伸出去探手机,窝在被子里睁开惺忪的双眼扫了一眼手机,略刷个十分钟强制让自己开机,家人们这种痛苦谁能懂?


1101700819100_.pic.jpg




  • 8:50 起床洗漱换衣服




  • 9:10 戴上耳机就出门,骑着我的小毛驴一边听音乐一边拼命赶路。P.S. 今天循环了一整天阿梨粤的《晚风心里吹》




  • 9:30 每天都庆幸踩点到,然后刷脸打卡




  • 9:35 开始洗杯子接水坐在工位前开启今天一天的工作




1121700819101_.pic.jpg



  • 11:40 结束上午工作,骑上我的小毛驴准备回家


关于中午



  • 11:55 到家,不太想吃外卖,所以到家开始简单弄点吃的


1091700819099_.pic.jpg




  • 12:05 全身心放松下来,一边干饭一边刷剧(最近迷恋韩剧,顺便学点韩语)




  • 12:25 准备午睡,抱着🐱睡大约1h




  • 13:20 带上耳机骑上我心爱的小毛驴穿梭蒋墩路




  • 13:40 准点到公司,开启下午的工作(偶尔,不小心睡过头就是两点到)




1071700819098_.pic.jpg



  • 15:00 下午茶,顺便瞄一眼基金(惨不忍睹,我真想哭😭)


1.jpg



  • 18:40 结束一天工作,over~


关于晚上



  • 19:00 到家,瘫着,然后跟🐱互动,最后顺便铲下💩,🤮


WechatIMG126.jpg




  • 19:40 开启扫地机开始打扫,同时开始洗香香🧼




  • 20:20 晚上一般不吃饭,一边看韩剧一边干点零食




  • 22:20 学韩语大约半小时




1051700819097_.pic.jpg




  • 22:50 刷牙洗脸,躺床上思考人生,顺便YY自己暴富的场景




  • 23:00 大约这时就进入梦乡了,开始做暴富的梦~




结尾


日子就在这种平凡且无趣的时光里一天天缓缓度过。相对来说,我的生活比较轻松,压力也比较小,但是我也有在试图学些什么来打破这“温水煮青蛙”的生活,比如:学习别国语言,尝试去写作来记录我平凡乏味的生活。怎么说呢?不管怎样,也许从我踏出第一步的时候就已经算是小小的成功了吧!共勉~


1111700819100_.pic.jpg


作者:落完这场雨
来源:juejin.cn/post/7304888432735617064
收起阅读 »

一周的努力化为泡影,前端找工作是很难

web
这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget...
继续阅读 »

这周又是面了一周,今天是周五了,目前还没有一个offer。好几家面试都是聊的很好,问题回答的自我感觉挺好(可能面试官没觉得好),然后就没有了后续。这周一共面试了6家公司,目前有2家过了2面。下周约了3面线下,可能工资不会给的太多。其中最遗憾的一家是bitget,二面面试官迟到了10分钟,然后面了半个小时不到,面试官匆匆结束面试,整个过程我也没觉得讲的多差,反正草草收场让我有点懵逼,我后来问HR说没有后续了,很可能我是被当成KPI了。下周继续努力吧!!!


以下是我这周的面试题。


1. 天学网


面试时间


一面:2023/11/06 10:00 腾讯会议


二面:2023/11/07 19:00 腾讯会议


一面问题




  1. 自我介绍

  2. 介绍一下你在上家公司的主要工作

  3. 介绍一个你之前过往工作中最满意的一个项目

  4. 你在这个项目中做的性能优化的事情有哪些?

  5. webworker中为什么能提升js执行的性能?

  6. 你是怎么使用webworker的?

  7. 浏览器内存你在实战中处理过吗?

  8. 浏览器的垃圾回收机制是什么样的?

  9. 你在做微前端的时候,为什么选择qiankun

  10. qiankun的原理了解哪些

  11. 你在使用qiankun的时候,有没有发现这个框架的不足之处

  12. 使用ts的时候,有没有什么心得

  13. ts注解用过没有?是什么?

  14. webpack熟悉吗?webpack打包流程是什么?

  15. 你在公司制定前端规范的时候,都有哪些内容

  16. 场景题:答案评分,根据给定的答案和作答打分,如何设计?



二面问题




  1. 问了一下工作经历

  2. 说一个自己的满意的项目

  3. 业务场景:负责的项目,用户反馈体验不友好,该如何优化



做教学工具的,也算是教育行业,下周二面。


2. 小黑盒


面试时间


一面:2023/11/06 15:00 牛客网面试


面试问题




  1. coding

    1. 中位数

    2. 孩子发糖果

    3. 无重叠区间





错一个直接挂。。。无情哈拉少。


3. bitget


面试时间


一面:2023/11/07 16:00 腾讯会议面试


一面问题




  1. 自我介绍

  2. 小程序跟H5的区别是什么?

  3. react和vue的语法是是如何在小程序中运行的?

  4. uni-app是如何打包成各个平台能运行的代码的?

  5. vue3中做了哪些优化?

  6. vue2和vue3的响应式有什么区别?

  7. vue中的watchEffect是什么?

  8. nextjs中运行时机制什么样?你们自己封装的还是?

  9. interface和type的区别是什么?

  10. vite、webpack、roolup的区别是什么?你怎么选择

  11. promise有哪些方法?

  12. coding题

  13. 手写Promise.all



二面问题




  1. 自我介绍

  2. 工作经历

  3. 为什么一直在教育行业

  4. 前端监控如何设计

  5. 讲一个你过往项目中遇到的问题,如何解决的



感觉更像是在搞KPI,最后二面草草结束,也没给我机会提问题。


4. 冲云破雾科技


面试时间


2023-11-08 16:00


薪资范围


30-50K 16薪


面试问题




  1. 自我介绍

  2. 数组乱序

  3. 一个数组,里面是[{name: 'xxx', 'age': 12, ....}],请根据name或者age进行排序,如果name中有中文是如何排序的

  4. 在vue中,v-modal是如何传递给子组件的

  5. 密码校验,要求包含大小写字母,数字,长度为6,至少满足三个条件

  6. 布局适配问题,响应式,rem,em,flex等



这是一家专门搞小程序算是,公司没有前端,跟第三方合作,面试我的也是第三方的前端,问的问题也比较偏业务场景。最后没啥结果了。


5. 燃数科技


薪资范围


25-40K*14薪


面试时间


2023/11/09 11:00-11:30


面试问题




  1. 自我介绍

  2. 低代码如何设计的

  3. react路由原理

  4. react生命周期

  5. 什么是回调地狱,如何解决

  6. jwt和session有什么区别

  7. js文件相互引用有什么问题?如何解决

  8. 一个很大的json文件,前端读取如何优化



面试我的不像是前端,更像是个后端,公司目前有两个前端,之前离职一个,现在想找一个填补空缺。做低代码可视化平台的。下周线下二面。


6. 58同城


面试时间


2023/11/10 10:30-11:30


面试题




  1. 自我介绍

  2. coding

    1. 三数之和

    2. 连续正整数之和



  3. 最新了解的一些前端新技术

    1. vite为什么比webpack快

    2. vite的预构建是如何做的

    3. tree-shaking是如何做的,commonjs能用吗



  4. 微前端了解过哪些框架,如何选型

    1. qiankun的js沙箱和css沙箱原理是啥



  5. 讲讲你做的低代码平台

    1. 你觉得这个低代码平台跟别的比有什么优势或者有什么亮点吗?

    2. 实时预览功能是如何做的?

    3. 有没有版本回退功能?



  6. 讲一个你做的比较拿手的项目

    1. SDK

    2. 脚手架

    3. 难点是什么?

    4. 技术亮点是什么?





总结面试不足:coding能力有待提高,项目对于大厂面试来说不够有亮点,难度不够,对于技术细节不够深入。下周继续加油,噢力给给!!!😭😭😭


如果你现在正在找工作,可以关注一下我的公众号「白哥学前端」,进群领取前端面试小册,和群友一起交流。本群承诺没有任何交易,没有买卖,权当为了督促我自己,也为了找到志同道合的道友一起渡劫。


作者:白哥学前端
来源:juejin.cn/post/7299392213481439243
收起阅读 »

大部分公司都是草台班子,甚至更水

我第一份实习是在一家咨询公司,我以为我们能够给我们的客户提供极具商业价值的战略指导,但其实开始干活了之后,发现我们就是PPT和调研报告的搬运工。后来我去了一家互联网大厂,我以为我的身边全都是逻辑超强的技术和产品大佬,直到我们的产品带着一堆的bug上线了.......
继续阅读 »

我第一份实习是在一家咨询公司,我以为我们能够给我们的客户提供极具商业价值的战略指导,但其实开始干活了之后,发现我们就是PPT和调研报告的搬运工。后来我去了一家互联网大厂,我以为我的身边全都是逻辑超强的技术和产品大佬,直到我们的产品带着一堆的bug上线了......


大四秋招的时候,我跟一个应届生一起面试,他简历上写了精通数据分析,还有很多获奖。我当时很羡慕他的能力,直到一起入职之后发现他只会用Excel......


刚从学生变成打工人的时候,我觉得每一家公司都是一个严丝合缝,非常精密的巨大的仪器,我要达到某一个水平或者有某种资质,我才能够去做一些工作,或者达到一些成就。但后来随着工作久了,我就想明白了一件事情,让我觉得之前的班真是白上了。


其实一个公司它的运营机制,并不是有很多个有远见的领导把规划都想明白,然后再有很多个能力强的下属把这些规划全部落地,这种太理想化了。电影里都没有这么演的。公司的运营机制就是面多了就加水,水多就加面,所有的公司都是大的草台班子。这里边绝大多数的工作,它的粗糙程度都远超过我们的想象。我们根本不用陷入所谓的入场券陷阱,觉得别人都很厉害,别人都是科班出身的,我得像别人一样厉害,一样有资质了,我才能够去做,这只不过是我给我自己设的一个假想敌。


想明白这一点之后,我的焦虑和内耗就好多了。既然大家都很水,那在职场这个大的草台班子上,我如果不去争取机会,那就被还不如我的人抢走了。勇敢的人先享受生活,同样勇敢的打工人也会先当上生活中的主演。


在争取机会的过程中,难免你就会用到一些职场作弊小技巧,就是自我包装。身边就有几个这样的人,敢于勇敢地表现自己,让别人觉得他能够创造很多价值。包装造势在掠夺职场资源的竞争力是非常有效的。


包装的方式分为职业和爱好。在职场上一定不要沉迷那些琐碎的工作中无法自拔,不要显得自己每天都很忙,加班都很晚,效率低下偷懒的人才要加班,不要不满现状,导致不想思考,不要未经选择直接就开始低效率的行动,所以要适当的停下来,寻找自我包装的发力点。


就像我们公司今年越来越重视数据分析,所以我就利用下班的时间多学习了数据分析。包装它肯定不只是一句空话,不然用不了多久就露馅了,所以要找到快速高效的学习方法。


如果你的职业发展方向也是产品运营,市场数据分析类似的岗位,那就要尽早的培养起你的数据分析能力,用好SQL,Python,统计学还有Excel,这些都会帮助你去提取处理和分析数据,再结合上你所在行业的专业知识,技能buff叠加在职场中会非常的加分。


职场里其实并没有那么多很厉害的人,大家都是在现学现卖,反正都是在草台班上演戏,不妨大胆一点去探索新的东西,去尝试你想尝试的,去找到能够把自己包装好的那个点,然后去大大方方的展示和表现自己。


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

Taro | 高性能小程序的最佳实践

web
前言 作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。 熟悉 Taro...
继续阅读 »

前言


作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。


熟悉 Taro 的开发者应该知道,相比于 Taro 1/2,Taro 3 是一个更加注重运行时而轻量化编译时的框架。它的优势在于提供了更高效的代码编写方式,并拥有更丰富的生态系统。然而,这也意味着在性能方面可能会有一些损耗。


但是,使用 Taro 3 并不意味着我们必须牺牲应用的性能。事实上,Taro 已经提供了一系列的性能优化方法,并且不断探索更加极致的优化方案。


本文将为大家提供一些小程序开发的最佳实践,帮助大家最大程度地提升小程序应用的性能表现。


一、如何提升初次渲染性能


如果初次渲染的数据量非常大,可能会导致页面在加载过程中出现一段时间的白屏。为了解决这个问题,Taro 提供了预渲染功能(Prerender)。


使用 Prerender 非常简单,只需在项目根目录下的 config 文件夹中找到 index.js/dev.js/prod.js 三者中的任意一个项目配置文件,并根据项目情况进行修改。在编译时,Taro CLI 会根据你的配置自动启动预渲染功能。


const config = {
...
mini: {
prerender: {
match: 'pages/shop/**', // 所有以 `pages/shop/` 开头的页面都参与 prerender
include: ['pages/any/way/index'], // `pages/any/way/index` 也会参与 prerender
exclude: ['pages/shop/index/index'] // `pages/shop/index/index` 不用参与 prerender
}
}
};

module.exports = config


更详细说明请参考官方文档:taro-docs.jd.com/docs/preren…



二、如何提升更新性能


由于 Taro 使用小程序的 template 进行渲染,这会引发一个问题:所有的 setData 更新都需要由页面对象调用。当页面结构较为复杂时,更新的性能可能会下降。


当层级过深时,setData 的数据结构如下:


page.setData({
'root.cn.[0].cn.[0].cn.[0].cn.[0].markers': [],
})

期望的 setData 数据结构:


component.setData({
'cn.[0].cn.[0].markers': [],
})

目前有两种方法可以实现上述结构,以实现局部更新的效果,从而提升更新性能:


1. 全局配置项 baseLevel


对于不支持模板递归的小程序(例如微信、QQ、京东小程序等),当 DOM 层级达到一定数量后,Taro 会利用原生自定义组件来辅助递归渲染。简单来说,当 DOM 结构超过 N 层时,Taro 将使用原生自定义组件进行渲染(可以通过修改配置项 baseLevel 来调整 N 的值,建议设置为 8 或 4)。


需要注意的是,由于这是全局设置,可能会带来一些问题,例如:



  • 在跨原生自定义组件时,flex 布局会失效(这是影响最大的问题);

  • SelectorQuery.select 方法中,跨自定义组件的后代选择器写法需要增加 >>>:.the-ancestor >>> .the-descendant


2. 使用 CustomWrapper 组件


CustomWrapper 组件的作用是创建一个原生自定义组件,用于调用后代节点的 setData 方法,以实现局部更新的效果。


我们可以使用它来包裹那些遇到更新性能问题的模块,例如:


import { View, Text } from '@tarojs/components'

export default function () {
return (
<View className="index">
<Text>Demo</Text>
<CustomWrapper>
<GoodsList />
</CustomWrapper>
</View>

)
}

三、如何提升长列表性能


长列表是常见的组件,当生成或加载的数据量非常大时,可能会导致严重的性能问题,尤其在低端机上可能会出现明显的卡顿现象。


为了解决长列表的问题,Taro 提供了 VirtualList 组件和 VirtualWaterfall 组件。它们的原理是只渲染当前可见区域(Visible Viewport)的视图,非可见区域的视图在用户滚动到可见区域时再进行渲染,以提高长列表滚动的流畅性。


image


1. VirtualList 组件(虚拟列表)


以 React Like 框架使用为例,可以直接引入组件:


import VirtualList from '@tarojs/components/virtual-list'

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualList
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



2. VirtualWaterfall 组件(虚拟瀑布流)


以 React Like 框架使用为例,可以直接引入组件:


import { VirtualWaterfall } from `@tarojs/components-advanced`

一个最简单的长列表组件如下所示:


function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}

const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>

)
})

export default class Index extends Component {
state = {
data: buildData(0),
}

render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualWaterfall
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>

)
}
}


更多详情可以参考官方文档:taro-docs.jd.com/docs/virtua…



四、如何避免 setData 数据量较大


众所周知,对小程序性能的影响较大的主要有两个因素,即 setData 的数据量和单位时间内调用 setData 函数的次数。在 Taro 中,会对 setData 进行批量更新操作,因此通常只需要关注 setData 的数据量大小。下面通过几个例子来说明如何避免数据量过大的问题:


例子 1:删除楼层节点要谨慎处理


目前 Taro 在处理节点删除方面存在一些缺陷。假设存在以下代码写法:


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
{isShowModal && <Modal />}
</View>

isShowModaltrue 变为 false 时,模态弹窗会消失。此时,Modal 组件的兄弟节点都会被更新,setData 的数据是 Slider + Goods 组件的 DOM 节点信息。


一般情况下,这不会对性能产生太大影响。然而,如果待删除节点的兄弟节点的 DOM 结构非常复杂,比如一个个楼层组件,删除操作的副作用会导致 setData 的数据量变大,从而影响性能。


为了解决这个问题,可以通过隔离删除操作来进行优化。


<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
<View>
{isShowModal && <Modal />}
</View>

</View>

例子 2:基础组件的属性要保持引用


当基础组件(例如 ViewInput 等)的属性值为非基本类型时,假设存在以下代码写法:


<Map
latitude={22.53332}
longitude={113.93041}
markers={[
{
latitude: 22.53332,
longitude: 113.93041,
},
]}
/>

每次渲染时,React 会对基础组件的属性进行浅比较。如果发现 markers 的引用不同,就会触发组件属性的更新。这最终导致了 setData 操作的频繁执行和数据量的增加。 为了解决这个问题,可以使用状态(state)或闭包等方法来保持对象的引用,从而避免不必要的更新。


<Map
latitude={22.53332}
longitude={113.93041}
markers={this.state.markers}
/>

五、更多最佳实践


1. 阻止滚动穿透


在小程序开发中,当存在滑动蒙层、弹窗等覆盖式元素时,滑动事件会冒泡到页面上,导致页面元素也会跟着滑动。通常我们会通过设置 catchTouchMove 来阻止事件冒泡。


然而,由于 Taro3 事件机制的限制,小程序事件都是以 bind 的形式进行绑定。因此,与 Taro1/2 不同,调用 e.stopPropagation() 并不能阻止滚动事件的穿透。


解决办法 1:使用样式(推荐)


可以为需要禁用滚动的组件编写以下样式:


{
overflow:hidden;
height: 100vh;
}

解决办法 2:使用 catchMove


对于极个别的组件,比如 Map 组件,即使使用样式固定宽高也无法阻止滚动,因为这些组件本身具有滚动的功能。因此,第一种方法无法处理冒泡到 Map 组件上的滚动事件。 在这种情况下,可以为 View 组件添加 catchMove 属性:


// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />

2. 跳转预加载


在小程序中,当调用 Taro.navigateTo 等跳转类 API 后,新页面的 onLoad 事件会有一定的延时。因此,为了提高用户体验,可以将一些操作(如网络请求)提前到调用跳转 API 之前执行。


对于熟悉 Taro 的开发者来说,可能会记得在 Taro 1/2 中有一个名为 componentWillPreload 的钩子函数。然而,在 Taro 3 中,这个钩子函数已经被移除了。不过,开发者可以使用 Taro.preload() 方法来实现跳转预加载的效果:


// pages/index.js
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/detail' })

// pages/detail.js
console.log(getCurrentInstance().preloadData)

3. 建议把 Taro.getCurrentInstance() 的结果保存下来


在开发过程中,我们经常会使用 Taro.getCurrentInstance() 方法来获取小程序的 apppage 对象以及路由参数等数据。然而,频繁地调用该方法可能会导致一些问题。


因此,建议将 Taro.getCurrentInstance() 的结果保存在组件中,并在需要时直接使用,以避免频繁调用该方法。这样可以提高代码的执行效率和性能。


class Index extends React.Component {
inst = Taro.getCurrentInstance()

componentDidMount() {
console.log(this.inst)
}
}

六、预告:小程序编译模式(CompileMode)


Taro 一直追求并不断突破性能的极限,除了以上提供的最佳实践,我们即将推出小程序编译模式(CompileMode)。


什么是 CompileMode?


前面已经说过,Taro3 是一种重运行时的框架,当节点数量增加到一定程度时,渲染性能会显著下降。 因此,为了解决这个问题,Taro 引入了 CompileMode 编译模式。


CompileMode 在编译阶段对开发者的代码进行扫描,将 JSXVue template 代码提前编译为相应的小程序模板代码。这样可以减少小程序渲染层虚拟 DOM 树节点的数量,从而提高渲染性能。 通过使用 CompileMode,可以有效减少小程序的渲染负担,提升应用的性能表现。


如何使用?


开发者只需为小程序的基础组件添加 compileMode 属性,该组件及其子组件将会被编译为独立的小程序模板。


function GoodsItem () {
return (
<View compileMode>
...
</View>

)
}


目前第一阶段的开发工作已经完成,我们即将发布 Beta 版本,欢迎大家关注!
想提前了解的可以查看 RFC 文档: github.com/NervJS/taro…



结尾


通过采用 Taro 的最佳实践,我们相信您的小程序应用性能一定会有显著的提升。未来,我们将持续探索更多优化方案,覆盖更广泛的应用场景,为开发者提供更高效、更优秀的开发体验。


作者:凹凸实验室
来源:juejin.cn/post/7304584222963613715
收起阅读 »

Android自定义控件:一款多特效的智能loadingView

先上效果图(如果感兴趣请看后面讲解): 1、登录效果展示 2、关注效果展示 1、【画圆角矩形】 画图首先是onDraw方法(我会把圆代码写上,一步一步剖析): 首先在view中定义个属性:private RectF rectf = new RectF();...
继续阅读 »

先上效果图(如果感兴趣请看后面讲解):


1、登录效果展示


img


2、关注效果展示


img


1、【画圆角矩形】


画图首先是onDraw方法(我会把圆代码写上,一步一步剖析): 首先在view中定义个属性:private RectF rectf = new RectF();//可以理解为,装载控件按钮的区域


rectf.left = current_left;
rectf.top = 0; //(这2点确定空间区域左上角,current_left,是为了后面动画矩形变成等边矩形准备的,这里你可以看成0)
rectf.right = width - current_left;
rectf.bottom = height; //(通过改变current_left大小,更新绘制,就会实现了动画效果)
//画圆角矩形
//参数1:区域
//参数2,3:圆角矩形的圆角,其实就是矩形圆角的半径
//参数4:画笔
canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);

2、【确定控件的大小】


上面是画圆角,那width和height怎么来呢当然是通过onMeasure;


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
height = measuredHeight(heightMeasureSpec); //这里是测量控件大小
width = measureWidth(widthMeasureSpec); //我们经常可以看到我们设置控件wrap_content,match_content或者固定值
setMeasuredDimension(width, height);
}

下面以measureWidth为例:


private int measureWidth(int widthMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
//这里是精准模式,比如match_content,或者是你控件里写明了控件大小
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
//这里是wrap_content模式,其实这里就是给一个默认值
//下面这段注销代码是最开始如果用户不设置大小,给他一个默认固定值。这里以字体长度来决定更合理
//result = (int) getContext().getResources().getDimension(R.dimen.dp_150);
//这里是我设置的长度,当然你写自定义控件可以设置你想要的逻辑,根据你的实际情况
result = buttonString.length() * textSize + height * 5 / 3;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}

3、【绘制文字text】


这里我是用自己的方式实现:当文字长度超过控件长度时,文字需要来回滚动。所以自定义控件因为你需要什么样的功能可以自己去实现(当然这个方法也是在onDraw里,为什么这么个顺序讲,目的希望我希望你能循序渐进的理解,如果你觉得onDraw方代码太杂,你可以用个方法独立出去,你可以跟作者一样用private void drawText(Canvas canvas) {}), //绘制文字的路径(文字过长时,文字来回滚动需要用到)


private Path textPath = new Path():


textRect.left = 0;
textRect.top = 0;
textRect.right = width;
textRect.bottom = height; //这里确定文字绘制区域,其实就是控件区域
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
//这里是获取文字绘制的y轴位置,可以理解上下居中
int baseline = (textRect.bottom + textRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
//这里判断文字长度是否大于控件长度,当然我控件2边需要留文字的间距,所以不是大于width,这么说只是更好的理解
//这里是当文字内容大于控件长度,启动回滚效果。建议先看下面else里的正常情况
if ((buttonString.length() * textSize) > (width - height * 5 / 3)) {
textPath.reset();
//因为要留2遍间距,以heigh/3为间距
textPath.moveTo(height / 3, baseline);
textPath.lineTo(width - height / 3, baseline);
//这里的意思是文字从哪里开始写,可以是居中,这里是右边
textPaint.setTextAlign(Paint.Align.RIGHT);
//这里是以路径绘制文字,scrollSize可以理解为文字在x轴上的便宜量,同时,我的混动效果就是通过改变scrollSize
//刷新绘制来实现
canvas.drawTextOnPath(buttonString, textPath, scrollSize, 0, textPaint);
if (isShowLongText) {
//这里是绘制遮挡物,因为绘制路径没有间距这方法,所以绘制遮挡物类似于间距方式
canvas.drawRect(new Rect(width - height / 2 - textSize / 3, 0, width - height / 2, height),paintOval);
canvas.drawRect(new Rect(height / 2, 0, height / 2 + textSize / 3, height), paintOval);
//这里有个bug 有个小点-5 因画笔粗细产生
canvas.drawArc(new RectF(width - height, 0, width - 5, height), -90, 180, true, paintOval);
canvas.drawArc(new RectF(0, 0, height, height), 90, 180, true, paintOval);
}

if (animator_text_scroll == null) {
//这里是计算混到最右边和最左边的距离范围
animator_text_scroll = ValueAnimator.ofInt(buttonString.length() * textSize - width + height * 2 / 3,-textSize);
//这里是动画的时间,scrollSpeed可以理解为每个文字滚动控件外所需的时间,可以做成控件属性提供出去
animator_text_scroll.setDuration(buttonString.length() * scrollSpeed);
//设置动画的模式,这里是来回滚动
animator_text_scroll.setRepeatMode(ValueAnimator.REVERSE);
//设置插值器,让整个动画流畅
animator_text_scroll.setInterpolator(new LinearInterpolator());
//这里是滚动次数,-1无限滚动
animator_text_scroll.setRepeatCount(-1);
animator_text_scroll.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//改变文字路径x轴的偏移量
scrollSize = (int) animation.getAnimatedValue();
postInvalidate();
}
});
animator_text_scroll.start();
}
} else {
//这里是正常情况,isShowLongText,是我在启动控件动画的时候,是否启动 文字有渐变效果的标识,
//如果是长文字,启动渐变效果的话,如果控件变小,文字内容在当前控件外,会显得很难看,所以根据这个标识,关闭,这里你可以先忽略(同时因为根据路径绘制text不能有间距效果,这个标识还是判断是否在控件2遍绘制遮挡物,这是作者的解决方式,如果你有更好的方式可以在下方留言)
isShowLongText = false;
/**
* 简单的绘制文字,没有考虑文字长度超过控件长度
* */

//这里是居中显示
textPaint.setTextAlign(Paint.Align.CENTER);
//参数1:文字
//参数2,3:绘制文字的中心点
//参数4:画笔
canvas.drawText(buttonString, textRect.centerX(), baseline, textPaint);
}

4、【自定义控件属性】


"1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SmartLoadingView">
<attr name="textStr" format="string" />
<attr name="errorStr" format="string" />
<attr name="cannotclickBg" format="color" />
<attr name="errorBg" format="color" />
<attr name="normalBg" format="color" />
<attr name="cornerRaius" format="dimension" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<attr name="scrollSpeed" format="integer" />
declare-styleable>

resources>

这里以,文案为例, textStr。比如你再布局种用到app:txtStr="文案内容"。在自定义控件里获取如下:


public SmartLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//自定义控件的3参方法的attrs就是我们设置自定义属性的关键
//比如我们再attrs.xml里自定义了我们的属性,
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SmartLoadingView);
//这里是获取用户有没有设置整个属性
//这里是从用户那里获取有没有设置文案
String title = typedArray.getString(R.styleable.SmartLoadingView_textStr);
if (TextUtils.isEmpty(title)){
//如果获取来的属性是空,那么可以默认一个属性
//(作者忘记设置了!因为已经发布后期优化,老尴尬了)
buttonString ="默认文案";
}else{
//如果有设置文案
buttonString = title;
}

}

5、【设置点击事件,启动动画】


为了点击事件的直观,也可以把处理防止重复点击事件封装在里面


//这是我自定义登录点击的接口
public interface LoginClickListener {
void click();
}

public void setLoginClickListener(final LoginClickListener loginClickListener) {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (loginClickListener != null) {
//防止重复点击
if (!isAnimRuning) {
start();
loginClickListener.click();
}

}
}
});
}

6、【动画讲解】


6.1、第一个动画,矩形到正方形,以及矩形到圆角矩形(这里是2个动画,只是同时进行)


矩形到正方形(为了简化,我把源码一些其他属性去掉了,这样方便理解)


//其中  default_all_distance = (w - h) / 2;除以2是因为2遍都往中间缩短
private void set_rect_to_circle_animation() {
//这是一个属性动画,current_left 会在duration时间内,从0到default_all_distance匀速变化
//想添加多样化的话 还可以加入插值器。
animator_rect_to_square = ValueAnimator.ofInt(0, default_all_distance);
animator_rect_to_square.setDuration(duration);
animator_rect_to_square.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//这里的current_left跟onDraw相关,还记得吗
//onDraw里的控件区域
//控件左边区域 rectf.left = current_left;
//控件右边区域 rectf.right = width - current_left;
current_left = (int) animation.getAnimatedValue();
//刷新绘制
invalidate();
}
});

矩形到圆角矩形。就是从一个没有圆角的变成完全圆角的矩形,当然我展示的时候只有第三个图,最后一个按钮才明显了。


其他的我直接设置成了圆角按钮,因为我把圆角做成了一个属性。


还记得onDraw里的canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);circleAngle就是圆角的半径


可以想象一下如果全是圆角,那么circleAngle会是多少,当然是height/2;没错吧,所以


因为我把圆角做成了属性obtainCircleAngle是从xml文件获取的属性,如果不设置,则为0,就没有任何圆角效果


animator_rect_to_angle = ValueAnimator.ofInt(obtainCircleAngle, height / 2);
animator_rect_to_angle.setDuration(duration);
animator_rect_to_angle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//这里试想下如果是一个正方形,刚好是圆形的圆角,那就是一个圆
circleAngle = (int) animation.getAnimatedValue();
//刷新绘画
invalidate();
}
});

2个属性动画做好后,用 private AnimatorSet animatorSet = new AnimatorSet();把属性动画加进去,可以设置2个动画同时进行,还是先后顺序 这里是同时进行所用用with


animatorSet
.play(animator_rect_to_square).with(animator_rect_to_angle);

6.2、变成圆形后,有一个loading加载动画


这里就是画圆弧,只是不断改变,圆弧的起始点和终点,最终呈现loading状态,也是在onDraw里


//绘制加载进度
if (isLoading) {
//参数1:绘制圆弧区域
//参数2,3:绘制圆弧起始点和终点
canvas.drawArc(new RectF(width / 2 - height / 2 + height / 4, height / 4, width / 2 + height / 2 - height / 4, height / 2 + height / 2 - height / 4), startAngle, progAngle, false, okPaint);

//这里是我通过实践,实现最佳loading动画
//当然这里有很多方式,因为我自定义这个view想把所有东西都放在这个类里面,你也可以有你的方式
//如果有更好的方式,欢迎留言,告知我一下
startAngle += 6;
if (progAngle >= 270) {
progAngle -= 2;
isAdd = false;
} else if (progAngle <= 45) {
progAngle += 6;
isAdd = true;
} else {
if (isAdd) {
progAngle += 6;
} else {
progAngle -= 2;
}
}
//刷新绘制,这里不用担心有那么多刷新绘制,会不会影响性能
//
postInvalidate();
}

6.3、loading状态,到打勾动画


那么这里首先要把loading动画取消,那么直接改变isLoading=false;不会只它同时启动打勾动画;打勾动画的动画,这里比较麻烦,也是我在别人自定义动画里学习的,通过PathMeasure,实现路径动画


/**
* 路径--用来获取对勾的路径
*/

private Path path = new Path();
/**
* 取路径的长度
*/

private PathMeasure pathMeasure;

//初始化打勾动画路径;
private void initOk() {
//对勾的路径
path.moveTo(default_all_distance + height / 8 * 3, height / 2);
path.lineTo(default_all_distance + height / 2, height / 5 * 3);
path.lineTo(default_all_distance + height / 3 * 2, height / 5 * 2);
pathMeasure = new PathMeasure(path, true);
}

//初始化打勾动画
private void set_draw_ok_animation() {
animator_draw_ok = ValueAnimator.ofFloat(1, 0);
animator_draw_ok.setDuration(duration);
animator_draw_ok.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public
void onAnimationUpdate(ValueAnimator animation) {
startDrawOk = true;
isLoading = false;
float value = (Float) animation.getAnimatedValue();
effect = new DashPathEffect(new float[]{pathMeasure.getLength(), pathMeasure.getLength()}, value * pathMeasure.getLength());
okPaint.setPathEffect(effect);
invalidate();

}
});
}

//启动打勾动画只需要调用
animator_draw_ok.start();

onDraw里绘制打勾动画


//绘制打勾,这是onDraw的,startDrawOk是判断是否开启打勾动画的标识
if (startDrawOk) {
canvas.drawPath(path, okPaint);
}

6.4、loading状态下回到失败样子(有点类似联网失败了)


之前6.1提到了矩形到圆角矩形和矩形到正方形的动画,


那么这里只是前面2个动画反过来,再加上联网失败的文案,和联网失败的背景图即刻


6.5、loading状态下启动扩散全屏动画(重点)


这里我通过loginSuccess里参数的类型启动不同效果:


1、启动扩散全屏动画
public void loginSuccess(Animator.AnimatorListener endListener) {}

2、启动打勾动画
public void loginSuccess(AnimationOKListener animationOKListener) {}

启动扩散全屏是本文的重点,里面还涉及到了一个自定义view


CirclBigView,这个控件是全屏的,而且是从一个小圆不断改变半径变成大圆的动画,那么有人会问,全屏肯定不好啊,会影响布局,
但是这里,我把它放在了activity的视图层:
ViewGr0up activityDecorView = (ViewGr0up) ((Activity) getContext()).getWindow().getDecorView();
ViewGr0up.LayoutParams layoutParams = new ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT);
activityDecorView.addView(circlBigView, layoutParams);

这个灵感也是前不久在学习微信,拖拽退出的思路里发现的。全部代码如下:


public void toBigCircle(Animator.AnimatorListener endListener) {
//把缩小到圆的半径,告诉circlBigView
circlBigView.setRadius(this.getMeasuredHeight() / 2);
//把当前背景颜色告诉circlBigView
circlBigView.setColorBg(normal_color);
int[] location = new int[2];
//测量当前控件所在的屏幕坐标x,y
this.getLocationOnScreen(location);
//把当前坐标告诉circlBigView,同时circlBigView会计算当前点,到屏幕4个点的最大距离,即是当前控件要扩散到的半径
//具体建议读者看完本博客后,去下载玩耍下。
circlBigView.setXY(location[0] + this.getMeasuredWidth() / 2, location[1]);
if (circlBigView.getParent() == null) {
ViewGr0up activityDecorView = (ViewGr0up) ((Activity) getContext()).getWindow().getDecorView();
ViewGr0up.LayoutParams layoutParams = new ViewGr0up.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT);
activityDecorView.addView(circlBigView, layoutParams);
}
circlBigView.startShowAni(endListener);
isAnimRuning = false;
}

结束语


因为项目是把之前的功能写成了控件,所以有很多地方不完善。希望有建议的大牛和小伙伴,提示提示我,让我完善的更好。谢谢


作者:花海blog
来源:juejin.cn/post/7300845863462436873
收起阅读 »

大白话DDD(DDD黑话终结者)

一、吐槽的话 相信听过DDD的人有很大一部分都不知道这玩意具体是干嘛的,甚至觉得它有那么一些虚无缥缈。原因之一是但凡讲DDD的,都是一堆特别高大上的概念,然后冠之以一堆让人看不懂的解释,。作者曾经在极客时间上买了本DDD实战的电子书,被那些概念一路从头灌到尾,...
继续阅读 »

一、吐槽的话


相信听过DDD的人有很大一部分都不知道这玩意具体是干嘛的,甚至觉得它有那么一些虚无缥缈。原因之一是但凡讲DDD的,都是一堆特别高大上的概念,然后冠之以一堆让人看不懂的解释,。作者曾经在极客时间上买了本DDD实战的电子书,被那些概念一路从头灌到尾,灌得作者头昏脑涨,一本电子书那么多文章愣是没有一点点像样的案例,看到最后也 没明白那本电子书的作者究竟想写啥。原因之二是DDD经常出现在互联网黑话中,如果不能稍微了解一下DDD中的名词,我们一般的程序员甚至都不配和那些说这些黑话的人一起共事。


为了帮助大家更好的理解这种虚无缥缈的概念,也为了更好的减少大家在新词频出的IT行业工作的痛苦,作者尝试用人话来解释下DDD,并且最后会举DDD在不同层面上使用的例子,来帮助大家彻底理解这个所谓的“高大上”的概念。


二、核心概念


核心的概念还是必须列的,否则你都不知道DDD的名词有多么恶心,但我会用让你能听懂的话来解释。


1、领域/子域/核心域/支撑域/通用域


领域

DDD中最重要的一个概念,也是黑话中说的最多的,领域指的是特定的业务问题领域,是专门用来确定业务的边界。


子域

有时候一个业务领域可能比较复杂,因此会被分为多个子域,子域分为了如下几种:



  • 核心子域:业务成功的核心竞争力。用人话来说,就是领域中最重要的子域,如果没有它其他的都不成立,比如用户服务这个领域中的用户子域

  • 通用子域:不是核心,但被整个业务系统所使用。在领域这个层面中,这里指的是通用能力,比如通用工具,通用的数据字典、枚举这类(感叹DDD简直恨不得无孔不入)。在整个业务系统这个更高层面上,也会有通用域的存在,指的通用的服务(用户服务、权限服务这类公共服务可以作为通用域)。

  • 支撑子域:不是核心,不被整个系统使用,完成业务的必要能力。


2、通用语言/限界上下文


通用语言

指的是一个领域内,同一个名词必须是同一个意思,即统一交流的术语。比如我们在搞用户中心的时候,用户统一指的就是系统用户,而不能用其他名词来表达,目的是提高沟通的效率以及增加设计的可读性


限界上下文

限界上下文指的是领域的边界,通常来说,在比较高的业务层面上,一个限界上下文之内即一个领域。这里用一张不太好看的图来解释:


image.png


3、事件风暴/头脑风暴/领域事件


事件风暴

指的是领域内的业务事件,比如用户中心中,新增用户,授权,用户修改密码等业务事件。


头脑风暴

用最俗的人话解释,就是一堆人坐在一个小会议室中开会,去梳理业务系统都有哪些业务事件。


领域事件

领域内,子域和子域之间交互的事件,如用户服务中用户和角色交互是为用户分配角色,或者是为角色批量绑定用户,这里的领域事件有两个,一个是“为用户分配角色”,另一个是“为角色批量绑定用户”。


4、实体/值对象


实体

这里可以理解为有着唯一标识符的东西,比如用户实体。


值对象

实体的具体化,比如用户实体中的张三和李四。


实体和值对象可以简单的理解成java中类和对象,只不过这里通常需要对应数据实体。


5、聚合/聚合根


聚合

实体和实体之间需要共同协作来让业务运转,比如我们的授权就是给用户分配一个角色,这里涉及到了用户和角色两个实体,这个聚合即是用户和角色的关系。


聚合根

聚合根是聚合的管理者,即一个聚合中必定是有个聚合根的,通常它也是对外的接口。比如说,在给用户分配角色这个事件中涉及两个实体分别是用户和角色,这时候用户就是聚合根。而当这个业务变成给角色批量绑定用户的时候,聚合根就变成了角色。即使没有这样一个名词,我们也会有这样一个标准,让业务按照既定规则来运行,举个上文中的例子,给用户A绑定角色1,用户为聚合根,这样往后去查看用户拥有的角色,也是以用户的唯一标识来查,即访问聚合必须通过聚合根来访问,这个也就是聚合根的作用。


三、用途及案例


目前DDD的应用主要是在战略阶段和战术阶段,这两个名词也是非常的不讲人话,所谓的战略阶段,其实就是前期去规划业务如何拆分服务,服务之间如何交互。战术阶段,就是工程上的应用,用工程化做的比较好的java语言举例子,就是把传统的三层架构变成了四层架构甚至是N层架构而已。


1、微服务的服务领域划分

这是对于DDD在战略阶段做的事情:假如目前我司有个客服系统,内部的客服人员使用这个系统对外上亿的用户提供了形形色色的服务,同时内部人员觉得我们的客服系统也非常好用,老板觉得我们的系统做的非常好,可以拿出去对外售卖以提高公司的利润,那么这时候问题就来了,客服系统需要怎样去改造,才能够支持对外售卖呢?经过激烈的讨论,大致需求如下:



  • 对外售卖的形式有两种,分别是SaaS模式和私有化部署的模式。

  • SaaS模式需要新开发较为复杂的基础设施来支持,比如租户管理,用户管理,基于用户购买的权限系统,能够根据购买情况来给予不同租户不同的权限。而私有化的时候,由于客户是打包购买,这时候权限系统就不需要再根据用户购买来判断。

  • 数据同步能力,很多公司原本已经有一套员工管理系统,通常是HR系统或者是ERP,这时候客服系统也有一套员工管理,需要把公司人员一个一个录入进去,非常麻烦,因此需要和公司原有的数据来进行同步。

  • 老板的野心还比较大,希望造出来的这套基础设施可以为公司其他业务系统赋能,能支持其他业务系统对外售卖


在经过比较细致的梳理(DDD管这个叫事件风暴/头脑风暴)之后,我们整理出了主要的业务事件,大致如下:


1、用户可以自行注册租户,也可以由运营在后台为用户开通租户,每个租户内默认有一个超级管理员,租户开通之后默认有系统一个月的试用期,试用期超级管理员即可在管理端进行用户管理,添加子用户,分配一些基本权限,同时子用户可以使用系统的一些基本功能。


2、高级的功能,比如客服中的机器人功能是属于要花钱买的,试用期不具备此权限,用户必须出钱购买。每次购买之后会生成购买订单,订单对应的商品即为高级功能包。


3、权限系统需要能够根据租户购买的功能以及用户拥有的角色来鉴权,如果是私有化,由于客户此时购买的是完整系统,所以此时权限系统仅仅根据用户角色来鉴权即可。


4、基础设施还需要对其他业务系统赋能。


根据上面的业务流程,我们梳理出了下图中的实体


image.png


最后再根据实体和实体之间的交互,划分出了用户中心服务以及计费服务,这两个服务是两个通用能力服务,然后又划分出了基于通用服务的业务层,分别是租户管理端和运营后台以及提供给业务接入的应用中心,架构图如下:


image.png


基础设施层即为我们要做的东西,为业务应用层提供通用的用户权限能力、以及售卖的能力,同时构建开发者中心、租户控制台以及运营后台三个基础设施应用。


2、工程层面

这个是对于DDD在战术设计阶段的运用,以java项目来举例子,现在的搞微服务的,都是把工程分为了主要的三层,即控制层->逻辑层->数据层,但是到了DDD这里,则是多了一层,变成了控制层->逻辑层->领域能力层->数据层。这里一层一层来解释下:


分层描述
控制层对外暴漏的接口层,举个例子,java工程的controller
逻辑层主要的业务逻辑层
领域能力层模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。
数据层操作数据,java中主要是dao层

四、总结


在解释完了各种概念以及举例子之后,我们对DDD是什么有了个大概的认知,相信也是有非常多的争议。作者搞微服务已经搞了多年,也曾经在梳理业务的时候被DDD的各种黑话毒打过,也使用过DDD搞过工程。经历了这么多这方面的实践之后觉得DDD最大的价值其实还是在梳理业务的时候划分清楚业务领域的边界,其核心思想其实还是高内聚低耦合而已。至于工程方面,现在微服务的粒度已经足够细,完全没必要再多这么一层。这多出来的这一层,多少有种没事找事的感觉。更可笑的是,这个概念本身在对外普及自己的东西的时候,玩足了文字游戏,让大家学的一头雾水。真正好的东西,是能够解决问题,并且能够很容易的让人学明白,而不是一昧的造新词去迷惑人,也希望以后互联网行业多一些实干,少说一些黑话。


作者:李少博
来源:juejin.cn/post/7184800180984610873
收起阅读 »

Console党福音!封装 console.log 再也不用打注释了!

背景 大家好~, 我是刚入职场不久的前端小帅,每周分享我的一丢丢想法,希望能够帮助到大家~ 众所周知,console在项目开发调试中是不可或缺的,在日常工作中经常会使用到它,但使用多了之后会发现,虽然它足够短小精炼,但耐不住用的多,再加上注释指示,工作量隐约还...
继续阅读 »

背景


大家好~, 我是刚入职场不久的前端小帅,每周分享我的一丢丢想法,希望能够帮助到大家~


众所周知,console在项目开发调试中是不可或缺的,在日常工作中经常会使用到它,但使用多了之后会发现,虽然它足够短小精炼,但耐不住用的多,再加上注释指示,工作量隐约还是不小的(懒惰,因此我对它进行小小的封装,让它用起来更短更方便,分享给大家,希望能够对大家开发有所帮助


优势



  • 面向 Vue2

  • 拒绝通用的注释打印

  • 针对对象值自动序列化

  • 支持一行输出 N个变量打印

  • 支持输出data变量和局部变量

  • 支持限制特定页面输出变量

  • 更短的函数名


示例:


data() {
return {
joker: 'jym',
handsomeMan: 'me',
info: {
job: {
name: '专业炒粉'
}
}
}
},
methods: {
getJoker() {
const demo = 'test'
this.$c('joker', 'handsomeMan', 'info', 'info.job.name', { demo }) //输出语句
}
}

输出结果:

image.png


**是的,只需要一行代码!!! **


是的,只需要一行代码!!!


(如果是原来的console.log打印 光想就已经累了......)


封装


接下来看看是如何封装的吧~


方法名再短点!


// main.js
Vue.prototype.$c = function() {...}

因为要全局使用,因此直接挂载到 Vue 的原型对象上,这样各个组件里都能通过this.$c()进行调用


传参与打印



如何访问对应Vue实例的数据



因为我们需要访问this.xxx, 获取到this指向必不可缺,因此结合this的小知识,封装的函数必须是普通函数而不是箭头函数,这样在vue实例中调用时this.$cthis会指向Vue实例自身,我们就可以访问this下的数据啦~



传参



参数肯定需要支持多个,数组冗余了 ,直接扁平化输入

再结合Vue里是通过this.xxx获取数据,可知我们传入的需要是字符串

可得步骤



  • 传入多个待打印的变量名

  • 遍历每个变量名

  • 通过 this变量名 获取到data里的值

  • 判断是否为对象,分别处理

  • 打印

  • over


// main.js
Vue.prototype.$c = function(...words) {
words.forEach(word => {
const val = this[word]
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
})
}

对于对象类型的变量,通常我们需要获取实时的对象,直接console.log(obj), 但这样获取的obj通常不是准确的值,想要获取实时的值则需要调用console.log(JSON.stringify(obj, null, 2))通过json字符串化打印实时的值



什么?JSON.stringify后面两个参数是干嘛的?



我TM直接就是一个


参数一 接收 函数或数组,用来转换过滤对象

参数二 接收 数字或字符串,用于指定缩进的空格数或缩进字符串


示例:


const obj = {
name: 'mach',
age: 24,
sex: 1
}
console.log(JSON.stringify(obj, ['name', 'age']))
// {"name":"mach","age":24}

console.log(JSON.stringify(obj, ['name', 'age'], 2))
// {
// "name": "mach",
// "age": 24
// }

OK, 这个时候我们进行打印测试一下


//xxx.vue
data() {
return {
name: 1,
age: 2,
obj: {
name: 'mach',
age: 24,
sex: 1
}
}
},
created() {
this.$c('name', 'age', 'obj')
}

image.png


这时候看看效果,注释有了!变量的值也有了!


局部变量


现在能够访问Vue实例的值了,那少不了局部变量。

问题来了,访问Vue实例是应用了this指向,那局部变量该怎么获取到呢?


最核心的肯定获取是局部变量的执行上下文


呃呃呃呃呃 闭包? eval 试了一下貌似都不行


呃呃呃 , 有了,就是你了!


// xxx.vue
methods:{
getList() {
const joker = 'you'
}
}
this.$c({ 'joker': joker })

高端的食材,往往只需要最朴素的烹饪方式~

忙碌了两小时之后,小师傅开始传入对象


直接用对象传变量名和变量值!


// 简写
this.$c({ joker })

瞧瞧 { joker } 同样很短小,非常适合开发,美吱吱~~


于是函数长这样,对局部变量和Vue data里的变量做区分:



  • 判断 对象 则是局部变量

  • 判断 字符串 则是vue实例数据


Vue.prototype.$c = function(...words) {
words.forEach(word => {
if (typeof word === 'string') {
const val = this[word]
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
}
const [name, value] = Object.entries(word)[0]
if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') {
console.log(`${name} ======> `)
console.log(JSON.stringify(value, null, 2))
return
}
console.log(`${name} ======> `, value)
})
}

使用


// xxx.vue
methods:{
getList() {
const joker = 'you'
this.$c('name', 'age', 'obj', { joker })
}
}

image.png


Perfect ~


指明位置及套个漂亮显眼的框


对了,因为$c的函数是定义在main.js里的,因此控制台打印会显示是出现在main.js


image.png


解决办法就是在壳里加个 this.$options.name,指明调试语句来自哪个组件

再给打印区套个漂亮的壳,便于做区分


Vue.prototype.$c = function(...words) {
console.log(`来自${this.$options.name}-🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉`) // 新增
words.forEach(word => {
if (typeof word === 'string') {
const val = this[word]
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
}
const [name, value] = Object.entries(word)[0]
if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') {
console.log(`${name} ======> `)
console.log(JSON.stringify(value, null, 2))
return
}
console.log(`${name} ======> `, value)
})
console.log('🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉')
}

测试打印结果:


image.png


圆满结束啦


访问嵌套属性的值


还有嘞 ~ 有人问,刚刚只是访问变量第一层,如果只想访问变量的第n层怎么办?


很简单,核心就是嵌套调用~


const val = this[word]

改成


const val = word.split('.').reduce((pre, cur) => {
return pre[cur]
}, this)

这个时候通过 xxx.xxxx.xx 就可以访问Vue实例下数据的值啦~


data(){
return {
obj: {
name: 'joker'
age: 24
}
}
}
this.$c('obj.age', 'obj.name')

image.png

这里xxx.xxxx.xxxxx|xxxx|xxxxx-xxxx-xx没啥区别,就是做分割罢了,但用.好分辨点贴近开发


解决完Vue实例下数据的嵌套访问,这时候有人会问,访问局部变量的嵌套属性咋办


Emmm 好问题 , 转动我的机灵小脑袋~~~~~ 有了!!!


const obj = {
accont: {
xx: {
id: 11,
name: 'rich'
},
money: 10086
}
}
const xx = obj.accont.xx // 新增

// 调用
this.$c({xx})

高端的食材,往往只需要最朴素的烹饪方式 ~


const joker = {
info: {
name: 'jym',
age: 18,
height: 185
}
}
const name = joker.info.name
this.$c({ joker }, { name })

image.png


这样纸代码看起来也清晰嘛~ 嗯,针不戳


限制特定页面输出变量


假设我现在只需要path1path2的页面输出变量,关闭其他页面的输出,咋做嘞~


简简单单,咱已经拿到指向Vue实例的this,可以直接访问当前路由地址和名做一下限制即可


在函数开头加上


Vue.prototype.$c = function(...words) {
const whitelist = ['path1', 'path2']
const currentPath = this.$route.path

if (!whitelist.includes(currentPath)) return
......
}

现在就只有path1, path2的页面下的组件可以输出啦,是不是很方便呢?


只有 Vue2 可以这么封装吗?


虽然这个封装的代码是面向 Vue2, 但很显然封装的核心对于Vue是通用的,把this换成组件实例即可,如Vue3里是this(选项式) 或者 getCurrentInstance()(组合式),至于React,就只能朴素一丢丢喏(你懂得)


代码 没写 我就不贴了


集帅们 也可以把代码打在评论区,供参考借鉴~~


总结


全部代码:


Vue.prototype.$c = function(...words) {
const whitelist = ['path1', 'path2']
const currentPath = this.$route.path
if (!whitelist.includes(currentPath)) return

console.log(`🎉🎉🎉🎉🎉🎉🎉🎉来自-${this.$options.name}-🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉`)
words.forEach(word => {
if (typeof word === 'string') {
const val = word.split('.').reduce((pre, cur) => {
return pre[cur]
}, this)
if (Object.prototype.toString.call(val).slice(8, -1) === 'Object') {
console.log(`${word} ======> `)
console.log(JSON.stringify(val, null, 2))
return
}
console.log(`${word} ======> `, val)
return
}
const [name, value] = Object.entries(word)[0]
if (Object.prototype.toString.call(value).slice(8, -1) === 'Object') {
console.log(`${name} ======> `)
console.log(JSON.stringify(value, null, 2))
return
}
console.log(`${name} ======> `, value)
})
console.log('🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉')
console.log('')
}

优势



  • 无需通用的注释打印

  • 支持限制特定页面输出变量

  • 针对对象值自动序列化

  • 支持一行输出 N个变量打印

  • 支持打印data变量和局部变量

  • 更短的函数名


总的来说,这个封装主要是我在项目console烦了,特别是加上注释和对象json序列化,于是这几天蚌埠住了,整了这个封装,总的来说我觉得效果是不错滴,一行顶之前 N 行, 不用加注释,也不需要该死的 JSON.stringify(xx, null, 2)


但是特殊打印就回归正常 console.log 哦, 没必要封装太深入


希望能够帮助到那些和我一样的console党~ (wink), 觉得有帮助可以点赞关注一下我喔~~~


往期奇思妙想:


《这个前端Api管理方案会更好?》

《这个前端Api管理方案会更好?(二)》


作者:爱吃好果汁丶
来源:juejin.cn/post/7300122001768071168
收起阅读 »

工作一年多,聊聊校招程序员晋升与涨薪的秘密

成年人的世界里,要尽量改掉只要如何就能如何这种思维,因为万事没有绝对。但我认为,有些事情,依旧可以摸索出一些规律,或者说给我们一些启发。 早在入职公司之前,我就在思考,要怎样才能实现快速晋升呢?为此我也做了很多努力,比如工作上非常认真,技术成长上非常积极,但这...
继续阅读 »

成年人的世界里,要尽量改掉只要如何就能如何这种思维,因为万事没有绝对。但我认为,有些事情,依旧可以摸索出一些规律,或者说给我们一些启发。


早在入职公司之前,我就在思考,要怎样才能实现快速晋升呢?为此我也做了很多努力,比如工作上非常认真,技术成长上非常积极,但这并不代表我能以超常的速度进行晋升。当我工作一年多被现实教育之后,停下来,回头看,会发现一些观念并不正确。所以我想聊聊作为校招程序员(工作经验0-2年的程序员),这期间我观察到的关于晋升与涨薪的秘密。


绩效在分活的时候已经决定


脉脉上经常有人提到这句“你的绩效不是360之后定下来的,而是分活的时候就已经决定了”,看似暗黑,实则真实。


第一种情况是按劳分配。 对于一个开发团队来说,假设有一批校招生入职,你作为领导会怎样分活呢?以我之前的经验举例,我当时在一个大前端部门,支持近千人的大部门的前端开发需求。这种情况下,很难说谁支持的业务更好。因为大家都是随时可以被分配出去的资源,哪个业务发展的好,哪个业务发展的差,其实与你无关。


然而,虽然业务实际的发展与我们无关,但不同业务的工作量是不同的,这一点在分活的时候其实很难评估。就导致部门内可能忙的忙死,闲的闲死。尽管作为大前端部门,可以相互支援,但执行起来并没有那么简单。A业务整体的运转由小a同学负责,那小b同学去支援,就需要重新熟悉上下文,再去参与开发。如果这个需求不大不小,那叫一个新的人进来支援可能反而拖累效率。


所以在每周进度同步的时候,就有人需要加班,但有人不需要。这一点领导心中有数,在评定绩效,评估调薪的时候也会给予倾斜。


在这种情况下,只要你分到了一个忙碌的活,就能享受加班费,和不错的绩效和调薪。所以在企业里也不要羡慕事情少的同学,钱也会少。


还有一种情况就是工作量已知,但是含金量不同。 这种往往让人心里不服气,凭什么好的活领导就是不分给自己?这种我很难评价,但需要思考一下:



  1. 我的能力是否已经让团队领导放心,是否建立了充足的信任? 如果领导不信任我,自然不可能分配高价值的活过来。建立信任也是一个漫长的过程,日久见人心,需要长期的保持高质量产出,并且要让领导了解自己的辛苦付出。

  2. 我是否有和领导表达过我想承接更多需求的意愿? 如果没有,日理万机的领导也很难照顾到每一位同学,因此要主动多沟通。机会是自己争取来的,不要抹不开面子。

  3. 团队内的资源是否充足,是否已经到了需要争抢的阶段? 不可否认,不是每个部门都处于上升期,有些部门发展空间大,需要的人多,有些部门反之。但需要先梳理清楚是不是自己真的就没有问题了。


投机不如专心做事


这一点我只能谈我在字节的感受,因为我知道在腾讯不同部门的年终奖差N倍。但是在字节,给我的个人感受是更平衡。不会因为你在最赚钱的部门,就能拿到相比新业务的超额回报。


这种机制,有效的保护了公司的人才分布,不然最优秀的人才一定会被最赚钱的部门吸干。


所以对于校招程序员来说,与其思考要不要跳去一个当红炸子鸡部门,不如在当前部门好好打磨,好好思考业务价值,做到更好。


当然,也要看业务机会。你的业务是成长期还是瓶颈期,卷不卷?在成长期的业务,你会感受到朝气蓬勃,有非常多事情可以做,涨薪以及前景都很让人满意,也能遇到更多有挑战的机会。但具体处在哪个业务,作为校招生,其实没有这个眼光,完全看运气,投机成功的概率并不大。


接受排队晋升的规则


在目前的环境下,DDDD。不能用以前的眼光来衡量今天的晋升,还想着那么容易,那么迅速。


我知道有很多培训班都在卖类似《大厂P6直通车》之类的课程,给你一个学完立马能晋升大厂P6的幻想。然而,还是擦干口水,醒醒……


按照现在明面上的晋升要求,很多同学早就能达到,但未必给升。晋升名额非常有限,需要一个一个来。


按理说,职级代表的是武功,体现的是你的能力水平。绩效代表的是苦功,是你的实际产出水平。但现在职级越来越两者都要,不仅仅你要能力强,还得产出多。光有技术远远不够,要有足够的业务价值。


抛弃快速晋升的幻想,主动和领导聊,表达意愿,的同时,做好眼下的事情。相信晋升终将会在意料之外的一天发生。


高绩效的秘密


绩效最终是领导评定的,所以想要高绩效,一定要搞清楚领导最在意的点是什么?直接去和领导谈即可,毕竟自己想也未必想得正确,揣摩圣意也很累。


沟通好目标,确定好方向之后,认真去做。及时暴露风险,遇到很难解决的问题带着自己的思考及时申请援助,不要拖到暴雷了才沟通。


当然完成这些,可能依旧是中等绩效。所以在这过程中,需要挖掘出流程上,业务上可以优化的地方,适时抛出新的见解与思考。并且得到的好评等等,都可以冲击更好的绩效。


薪酬与公平


校招程序员在工作头2-3年的工资差异往往是因为进了不同的公司导致的。比如进了大公司,整体起薪就会相对高。但如果想在同一个团队内部,获得超额回报很难。在同一个团队内部的校招程序员,往往是按照同一尺度招聘进来的,人才评估标准也相同。因此就算开出了不同级别的offer差别也不会太大,这和团队预算也有关。


同部门的,工龄3年内都不会拉开过大的差距。所以在部门内如果你获得涨薪,如果职位没有提升,大概率团队内的其他人也会同步或异步一起涨,最终差距依旧不大。


所以如果爱这行,就长期奋斗,让K值更大一点点,着眼于长期获得更大的回报。


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

将文字复制到剪切板

web
笔者在开发过程中遇到点击按钮之后将文字复制到剪切板的需求,先将按钮的回调函数封装起来,便于以后使用,需要的朋友可以自取~ const _copyToClipboard = staticPart => dynamicPart => { i...
继续阅读 »

笔者在开发过程中遇到点击按钮之后将文字复制到剪切板的需求,先将按钮的回调函数封装起来,便于以后使用,需要的朋友可以自取~


  const _copyToClipboard = staticPart => dynamicPart => {
if (!dynamicPart) return;
const textToCopy = `${staticPart}${dynamicPart}`;
const tempInput = document.createElement('input');
tempInput.value = textToCopy;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
};

这个函数将复制到剪切板中的内容分成两个部分:静态的和动态的,因此在使用的时候可以这样做:


const copyFunc = _copyToClipboard('http://localhost:3000/api?id=');
copyFunc('678');
copyFunc('123');

作者:慕仲卿
来源:juejin.cn/post/7304538094783184937
收起阅读 »

Android 九宫格视频展示

一、前言 一个有趣的现象,抖音上一度热传九宫格视频,其本质都是利用视频合成算法将视频原有视频编辑裁剪,最终展示处理。但实际还有更简单的方法,无需编辑视频的情况下,同样也可以实现九宫格展示。 二、实现原理 2.1 原理 做音视频开发也有一段时间了,在这个领域很...
继续阅读 »

一、前言


一个有趣的现象,抖音上一度热传九宫格视频,其本质都是利用视频合成算法将视频原有视频编辑裁剪,最终展示处理。但实际还有更简单的方法,无需编辑视频的情况下,同样也可以实现九宫格展示。



二、实现原理


2.1 原理


做音视频开发也有一段时间了,在这个领域很多看似高大上的东西,实际上往往都有很多简便的方法去代替,从视频编辑到多屏投影,某些情况下并非一定要学习open gl才可以做到。


Android中提供了Path工具,其功能非常强大,很多不规则形状往往都需要Path实现,同样,本篇会利用Path进行镂空视频画布。


Path.Op 作为多个Path合成的重要操作符,其功能同样可以实现将Path闭合空间进行挖空的操作,目前主要有以下操作符。


Path.Op.DIFFERENCE          Path1调用合并函数:减去Path2后Path1区域剩下的部分
Path.Op.INTERSECT 保留Path2 和 Path1 共同的部分
Path.Op.UNION 保留Path1 和 Path 2
Path.Op.XOR 保留Path1 和 Path2 + 共同的部分
Path.Op.REVERSE_DIFFERENCE 与 Path.Op.DIFFERENCE相反,减去Path1后Path2区域剩下的部分


今天我们主要用到Path.Op.DIFFERENCE ,原因是XOR 多次存在叠加问题,下图Path节点的地方,实际上正如XOR所述进行了叠加,因此这里使用XOR效果不符合预期。



2.2 核心代码


        float columWidth = clipRect.width() / col;  //每列的宽度
float rowHeight = clipRect.height() / row; //每行的高度


for (int i = 1; i < col; i++) {
tmpPath.reset();
float position = i * columWidth - lineWidth/2;
tmpPath.addRect(offsetLeft + position, offsetTop, offsetLeft + position + lineWidth / 2, height - offsetBottom, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.XOR);
}
for (int i = 1; i < row; i++) {
tmpPath.reset();
float position = i * rowHeight - lineWidth/2;
tmpPath.addRect(offsetLeft , offsetTop + position, width - offsetRight, offsetTop + position + lineWidth / 2, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.XOR);
}

尝试修改行列的效果



三、完整代码


public class GridFrameLayout extends FrameLayout {
private Path clipPath;
private Path tmpPath = new Path();
private RectF clipRect;
private Paint paint;
//由于有的视频存在黑边,添加如下offset便于剔除黑边,保留纯画面区域
private int offsetTop = 0;
private int offsetBottom = 0;
private int offsetRight = 0;
private int offsetLeft = 0;
private PaintFlagsDrawFilter mPaintFlagsDrawFilter;

private int row = 3; //行数
private int col = 4; //列数

private int lineWidth = 0;


public GridFrameLayout(Context context) {
super(context);
init();
}

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

public GridFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.STROKE);
lineWidth = dpToPx(5);
}

public void setRow(int row) {
this.row = row;
}
public void setColum(int col) {
this.col = col;
}
public void setOffsetTop(int offsetTop) {
this.offsetTop = offsetTop;
}
public void setOffsetBottom(int offsetBottom) {
this.offsetBottom = offsetBottom;
}

public void setOffsetRight(int offsetRight) {
this.offsetRight = offsetRight;
}

public void setOffsetLeft(int offsetLeft) {
this.offsetLeft = offsetLeft;
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
clipRect = null;
}

@Override
protected void dispatchDraw(Canvas canvas) {
int height = getHeight();
int width = getWidth();
DrawFilter drawFilter = canvas.getDrawFilter();

int saveCount = canvas.save();
//保存当前状态

canvas.setDrawFilter(mPaintFlagsDrawFilter);
if (clipPath == null) {
clipPath = new Path();
}
if (clipRect == null) {
clipRect = new RectF(offsetLeft, offsetTop, width - offsetRight, height - offsetBottom);
} else {
clipRect.set(offsetLeft, offsetTop, width - offsetRight, height - offsetBottom);
}

clipPath.reset();
float radius = dpToPx(10);
float[] radii = new float[]{
radius, radius,
radius, radius,
radius, radius,
radius, radius
};

clipPath.addRoundRect(clipRect, radii, Path.Direction.CCW);

float columWidth = clipRect.width() / col;
float rowHeight = clipRect.height() / row;

for (int i = 1; i < col; i++) {
tmpPath.reset();
float position = i * columWidth - lineWidth / 2;
tmpPath.addRect(offsetLeft + position, offsetTop, offsetLeft + position + lineWidth / 2, height - offsetBottom, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.DIFFERENCE);
}
for (int i = 1; i < row; i++) {
tmpPath.reset();
float position = i * rowHeight - lineWidth / 2;
tmpPath.addRect(offsetLeft, offsetTop + position, width - offsetRight, offsetTop + position + lineWidth / 2, Path.Direction.CCW);
clipPath.op(tmpPath, Path.Op.DIFFERENCE);
}

canvas.clipPath(clipPath);
//裁剪画布,注意,这里不仅裁剪外围,内部挖空区域也会被裁剪
//为什么在dispatchDraw中使用,因为dispatchDraw方便控制子View的绘制
super.dispatchDraw(canvas);

canvas.restoreToCount(saveCount);
//恢复到之前的区域

canvas.setDrawFilter(drawFilter);
if (hasFocus()) {
canvas.drawPath(clipPath, paint); //有焦点时画一个边框
}
}
private int dpToPx(int dps) {
return Math.round(getResources().getDisplayMetrics().density * dps);
}
public void setLineWidth(int lineWidth) {
this.lineWidth = lineWidth;
}
}

四、总结


Canvas 作为2D绘制常用的组件,其实有很高级功能,如Matrix、Camera、Shader、drawBitmapMesh等,正确的使用往往能带来事半功倍的效果,因此有必要通过不断的摸索才能发挥极致。


作者:时光少年
来源:juejin.cn/post/7304272076641484834
收起阅读 »

前端黑科技篇章之scp2,让你一键打包部署服务器

web
scp2是一个使用nodejs对于SSH2的模拟实现,它可以让我们编译之后将项目推送至测试环境,以方便测试。 项目安装scp2 npm i scp2 -D 编写配置文件 创建scp2的配置文件 upload.server.js const serInfo =...
继续阅读 »

scp2是一个使用nodejs对于SSH2的模拟实现,它可以让我们编译之后将项目推送至测试环境,以方便测试。


项目安装scp2


npm i scp2 -D

编写配置文件


创建scp2的配置文件 upload.server.js


const serInfo = JSON.parse(process.env.npm_config_argv).cooked // 获取终端命令
const server = {
host: serInfo[2], // 服务器ip
port: '22', // 端口一般默认22
username: serInfo[3] || 'root', // 用户名
password: serInfo[4] || 'root', // 密码
pathNmae: '', // 上传到服务器的位置
locaPath: './dist/' // 本地打包文件的位置
}

const argv1 = process.argv
console.log(argv1)
console.log(serInfo)
// 引入scp2
const client = require('scp2')
const ora = require('ora')
const spinner = ora('正在发布到服务器...')

const Client = require('ssh2').Client
const conn = new Client()

console.log('正在建立连接')
conn.on('ready', () => {
console.log('已连接')
if (!server.pathNmae) {
console.log('连接已关闭')
conn.end()
return false
}

conn.exec('rm -rf' + server.pathNmae + '/*', (err, stream) => {
console.log(err + '删除文件')
stream.on('close', (code, signal) => {
console.log('开始上传')
spinner.start()
client.scp(server.locaPath, {
'host': server.host,
'port': server.port,
'username': server.username,
'password': server.password,
'path': server.pathNmae
}, err => {
spinner.stop()
if (!err) {
console.log('项目发布完毕')
} else {
console.log('err', err)
}
conn.end()
})
})
})
}).connect({
host: server.host,
port: server.port,
username: server.username,
password: server.password
// privateKey: '' // 私秘钥
})



配置package.json


image.png


两种上传方式,个人比较喜欢第二种哟



  1. build:pub:一键打包并上传服务器

  2. build:打包上传然后执行npm run publish 上传服务器


实战例子


弄好以上配置,在终端上敲 npm run build 然后再敲 npm run publish 得到以下结果。


image.png


已经发布到指定IP知道目录的服务器啦,这样我们就不用通过第三方ftp工具去上传了哦,是不是方便了很多,如果觉得该文章对你有帮助,请点个小赞赞吧。


作者:大码猴
来源:juejin.cn/post/6955070802035228685
收起阅读 »

是时候让自己掌握一款自动化构建工具了

后端:“麻烦给我一份XXXX版本的包”; 前端:”***,XXX版本有别的版本没有的依赖包,又得切分支还得卸载无用的包,还好我搭了Jenkins“ 前端: "好了,你去XXX环境上自己拿吧!" 我们身为前端有时候也需要对项目的不同版本进行控制,这时候自动化构建...
继续阅读 »

后端:“麻烦给我一份XXXX版本的包”;

前端:”***,XXX版本有别的版本没有的依赖包,又得切分支还得卸载无用的包,还好我搭了Jenkins“

前端: "好了,你去XXX环境上自己拿吧!"


我们身为前端有时候也需要对项目的不同版本进行控制,这时候自动化构建就能解决我们工作区上对应不同版本有着不同依赖的需求,以下我们来看下怎么去搭建属于自己的自动化构建吧(jenkins)。


1、搭建前的环境准备



  1. 这边需要Linux的支持,我这边是叫运维帮我新起一个1段(带外网,方便下载运行环境)的服务器。

  2. JDK11以上的环境(注意:当前jenkins支持的Java版本最低为Java11)。

  3. 安装Maven。

  4. Git环境。




我这开始一步步带着安装,老手可以直接跳到搭建配置。


2、安装JDK11


// 注意:没有yum可以利用apt-get install yum 来安装yum

yum list java* // 查看所有的JDK版本,找到java-11-openjdk.x86_64

yum install java-11-openjdk.x86_64 // 安装JDK11

java -version // 如果安装成功,就可以查看当前版本


image.png


3、安装Maven


安装:


cd /usr/loca  // 安装目录

wget https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz // 根据需要下载对应版本

tar -vxf apache-maven-3.6.3-bin.tar.gz // 解压

mv apache-maven-3.6.3 maven // 修改文件名

修改环境变量:


vim /etc/profile  // 进到配置文件

// 按 ins键进入编辑状态,写入以下配置,按esc 输入wq保存
export MAVEN_HOME=/usr/local/maven
export PATH=${PATH}:${MAVEN_HOME}/bin

source /etc/profile // 需要重新加载/etc/profile文件以使更改生效

mvn -v // 查看Maven版本

image.png


4、安装git


yum install git // 直接装

git --version // 查看当前git版本

image.png


5、安装Jenkins


安装Jenkins镜像源


mkdir jenkins && cd jenkins // 创建Jenkins文件夹,并进入Jenkins文件夹

wget https://updates.jenkins-ci.org/latest/jenkins.war // 远程下载Jenkins的war包

nohup java -jar jenkins.war --httpPort=8088 // 执行启动命令


image.png


这时终端可能存在无法输入的情况,我们另起终端,输入下面命令查看服务是否在运行


netstat -tlnp // 查看TCP协议进程端口

这时我们发现8088端口被运行了


image.png
接着,我们去浏览器输入IP+端口。


image.png
哟,这不就成功了?我们紧接着配置。


6、配置Jenkins


我们部署Jenkins的时候,会生成一个密码文件-initialAdminPassword,不知道路径的我们一步步找


cd / && find -name 'initialAdminPassword' // 进入/ 全举查找文件名为initialAdminPassword的文件

image.png


查到之后我们查看当前文件内容


cat ~/.jenkins/secrets/initialAdminPassword

image.png
这就是默认密码啦,我们复制粘贴到刚刚打开的Jenkins界面,回车,登录成功之后会出现以下界面


image.png


之后我们跳过自定义Jenkins,点击开始使用Jenkins,进入如下界面


image.png


紧接着,我们汉化下Jenkins操作界面,不想汉化的可以跳过此配置


点击界面的Manage Jenkins 》 Plugins 》 Available plugins 搜索chinese,之后我们按install就好了


image.png
记得在下载页面勾选重启Jenkins配置,重启完之后就汉化成功啦


image.png


接下来我们安装GitHub插件,流程跟安装汉化插件一致,我就直接输出结果了


image.png
记得勾选,不然得手动重启


image.png


趁下载的功夫,我们打开GitHub官网
settings 》 Developer settings 选择Personal Access Token --> Generate new token, 新建一个有读写权限的用户。


image.png
创建好之后复制下面密钥


image.png
接下来我们回到Jenkins配置页面配置GitHub
系统管理 => 系统设置 => Github Server 添加信息


image.png
之后添加Jenkins凭证
select选项为刚刚得到的GitHub 密钥


image.png


选择凭证,测试链接,得到以下信息


image.png
点击保存,接下来配置java环境,首先回到我们终端


echo $JAVA_HOME // 查看下我们JAVA的环境变量

如果没有不要着急,我们先进入系统环境配置文件,这里跟配置MAVEN环境变量操作一致,解释下上文为什么没配置Java环境变量却能打印。
因为我们是直接通过运行java命令,系统将使用默认的Java安装来执行该命令,并打印版本信息的。


which java 先查看java安装在哪

vi /etc/profile // 编辑环境变量文件,写入下面两行,并wq保存

export JAVA_HOME=/usr/bin/java
export PATH=$JAVA_HOME/bin:$PATH

source /etc/profile // 需要重新加载/etc/profile文件以使更改生效

image.png


这时我们再echo输出Java环境变量


image.png
然后我们拿到Jenkins上配置,点保存


image.png
之后回到首页,点新建任务,选择自由风格,点确定


image.png
之后弹出构建配置,我们往下拉,找到Build Steps,如果没弹出可以根据标签页找到对应配置


cd /test // 事先创建好文件
git clone https://github.com/LIAOJIANS/sa-ui.git // 可为你GitHub上的私人仓库,或者开放性仓库
cd sa-ui
npm install
npm run build

image.png
回到我们项目首页,然后点击立即构建


image.png
呀,好家伙你会发现红XX,这代表我们构建失败了


image.png
点击构建项目日志,查看控制台输出,好家伙原来没有node环境


image.png
老规矩,安装node环境,并添加软连接


wget https://nodejs.org/dist/v14.5.0/node-v14.5.0-linux-x64.tar.gz // 去官网找到指定版本的node

tar -zxvf node-v14.5.0-linux-x64.tar.gz -C /usr
/local/ // 解压到指定目录(/usr/local

mv node-v14.5.0-linux-x64/
nodejs // 重命名为nodejs

/
/ 把node和npm创建软链接到/usr/local/bin/目录下,系统在使用命令时,默认会到/usr/local/bin/读取命令。
ln -s /usr
/local/nodejs/bin/node /usr/local/bin/node
ln -s /usr/local/nodejs/bin/npm /usr/local/bin/npm

image.png
然后我们再换一下NPM源镜像


    npm config set registry https://registry.npmmirror.com/  // 新淘宝源地址
npm config get registry

image.png
然后我们再回到Jenkins进行构建


image.png
看到success就证明构建完成啦,现在我们就可以跟后端说,你自己去XXX服务器,XXX路径拿,如果想一键推送到后端服务器请参考, 前端黑科技篇章之scp2,让你一键打包部署服务器这篇文章,可以在Jenkins配置上传路径和命令等等。


完结撒花,感谢耐心观看的你们。


作者:大码猴
来源:juejin.cn/post/7304538199144955940
收起阅读 »

技术大佬 问我 订单消息乱序了怎么办?

技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了? 佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都揉捏的像一个麻花了嘛 技术大佬 :哦,这次又是遇到什么难题了? 佩琪: 由于和大佬讨论过消息不丢,消息防重等技能(见  kafka 消息...
继续阅读 »

技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了?


佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都揉捏的像一个麻花了嘛


技术大佬 :哦,这次又是遇到什么难题了?


佩琪: 由于和大佬讨论过消息不丢,消息防重等技能(见  kafka 消息“零丢失”的配方 和技术大佬问我 订单消息重复消费了 怎么办? ),所以在简历的技术栈里就夸大似的写了精通kafka消息中间件,然后就被面试官炮轰了里面的细节


佩琪: 其中面试官给我印象深刻的一个问题是:你们的kafka消息里会有乱序消费的情况吗?如果有,是怎么解决的了?


技术大佬 :哦,那你是怎么回答的了?


佩琪:我就是个crud boy,根本不知道啥是顺序消费啥是乱序消费,所以就回答说,没有


技术大佬 :哦,真是个诚实的孩子;然后呢?


佩琪:然后面试官就让我回家等通知了,然后就没有然后了。。。。


佩琪对了大佬,什么是消息乱序消费了?


技术大佬 :消息乱序消费,一般指我们消费者应用程序不按照,上游系统 业务发生的顺序,进行了业务消息的颠倒处理,最终导致消费业务出错。


佩琪低声咕噜了下你这说的是人话吗?大声问答:这对我的小脑袋有点抽象了,大佬能举个实际的栗子吗?


技术大佬 :举个上次我们做的促销数据同步的栗子吧,大概流程如下:


1700632936991.png


技术大佬 :上次我们做的促销业务,需要在我们的运营端后台,录入促销消息;然后利用kafka同步给三方业务。在业务流程上,是先新增促销信息,然后可能删除促销信息;但是三方消费端业务接受到的kafka消息,可能是先接受到删除促销消息;随后接受到新增促销消息;这样不就导致了消费端系统和我们系统的促销数据不一致了嘛。所以你是消费方,你就准备接锅吧,你不背锅,谁背锅了?


佩琪 :-_-||,此时佩琪心想,锅只能背一次,坑只能掉一次。赶紧问到:请问大佬,消息乱序了后,有什么解决方法吗?


技术大佬 : 此时抬了抬眼睛,清了清嗓子,面露自信的微笑回答道。一般都是使用顺序生产,顺序存储,顺序消费的思想来解决。


佩琪摸了摸头,能具体说说,顺序生产,顺序存储,顺序消费吗?


技术大佬 : 比如kafka,一般建议同一个业务属性数据,都往一个分区上发送;而kafka的一个分区只能被一个消费者实例消费,不能被多个消费者实例消费。


技术大佬 : 也就是说在生产端如果能保证 把一个业务属性的消息按顺序放入同一个分区;那么kakfa中间件的broker也是顺序存储,顺序给到消费者的。而kafka的一个分区只能被一个消费者消费;也就不存在多线程并发消费导致的顺序问题了。


技术大佬 :比如上面的同步促销消息;不就是两个消费者,拉取了不同分区上的数据,导致消息乱序处理,最终数据不一致。同一个促销数据,都往一个分区上发送,就不会存在这样的乱序问题了。


佩琪哦哦,原来是这样,我感觉这方案心理没底了,大佬能具体说说这种方案有什么优缺点吗?


技术大佬 :给你一张图,你学习下?


优点缺点
生产端实现简单:比如kafka 生产端,提供了按指定key,发送到固定分区的策略上游难保证严格顺序生产:生产端对同一类业务数据需要按照顺序放入同一个分区;这个在应用层还是比较的难保证,毕竟上游应用都是无状态多实例,多机器部署,存在并发情况下执行的先后顺序不可控
消费端实现也简单 :kafka消费者 默认就是单线程执行;不需要为了顺序消费而进行代码改造消费者处理性能会有潜在的瓶颈:消费者端单线程消费,只能扩展消费者应用实例来进行消费者处理能力的提升;在消息较多的时候,会是个处理瓶颈,毕竟干活的进程上限是topic的分区数。
无其它中间件依赖使用场景有取限制:业务数据只能指定到同一个topic,针对某些业务属性是一类数据,但发送到不同topic场景下,则不适用了。比如订单支付消息,和订单退款消息是两个topic,但是对于下游算佣业务来说都是同一个订单业务数据

佩琪大佬想偷懒了,能给一个 kafka 指定 发送到固定分区的代码吗?


技术大佬 :有的,只需要一行代码,你要不自己动手尝试下?


KafkaProducer.send(new ProducerRecord[String,String](topic,key,msg),new Callback(){} )

topic:主题,这个玩消息的都知道,不解释了

key: 这个是指定发送到固定分区的关键。一般填写订单号,或者促销ID。kafka在计算消息该发往那个分区时,会默认使用hash算法,把相同的key,发送到固定的分区上

msg: 具体消息内容


佩琪大佬,我突然记起,上次我们做的 订单算佣业务了,也是利用kafka监听订单数据变化,但是为什么没有使用固定分区方案了?


技术大佬 : 主要是我们上游业务方:把订单支付消息,和订单退款消息拆分为了两个topic,这个从使用固定分区方案的前提里就否定了,我们不能使用此方案。


佩琪哦哦,那我们是怎么去解决这个乱序的问题的了?


技术大佬 :主要是根据自身业务实际特性;使用了数据库乐观锁的思想,解决先发后至,后发先至这种数据乱序问题。


大概的流程如下图:


1700632983267.png


佩琪摸了摸头,大佬这个自身业务的特性是啥了?


技术大佬 :我们算佣业务,主要关注订单的两个状态,一个是订单支付状态,一个是订单退款状态
订单退款发生时间肯定是在订单支付后;而上游订单业务是能保证这两个业务在时间发生上的前后顺序的,即订单的支付时间,肯定是早于订单退款时间。所以主要是利用订单ID+订单更新时间戳,做为数据库佣金表的更新条件,进行数据的乱序处理。


佩琪哦哦,能详细说说 这个数据库乐观锁是怎么解决这个乱序问题吗?


技术大佬 : 比如:当佣金表里订单数据更新时间大于更新条件时间 就放弃本次更新,表明消息数据是个老数据;即查询时不加锁


技术大佬 :而小于更新条件时间的,表明是个订单新数据,进行数据更新。即在更新时 利用数据库的行锁,来保证并发更新时的情况。即真实发生修改时加锁


佩琪哦哦,明白了。原来一条带条件更新的sql,就具备了乐观锁思想


技术大佬 :我们算佣业务其实是只关注佣金的最终状态,不关注中间状态;所以能用这种方式,保证算佣数据的最终一致性,而不用太关注订单的中间状态变化,导致佣金的中间变化。


总结


要想保证消息顺序消费大概有两种方案


1700633024660.png


固定分区方案


1、生产端指定同一类业务消息,往同一个分区发送。比如指定发送key为订单号,这样同一个订单号的消息,都会发送同一个分区

2、消费端单线程进行消费


乐观锁实现方案


如果上游不能保证生产的顺序;可让上游加上数据更新时间;利用唯一ID+数据更新时间,+乐观锁思想,保证业务数据处理的最终一致性。


作者:程序员猪佩琪
来源:juejin.cn/post/7303833186068086819
收起阅读 »

kafka 消息“零丢失”的配方

如果在简历上写了使用过kafka消息中间件,面试官大概80%的概率会问你:"如何保证kafka消息不丢失?"反正我是屡试不爽。 如果你的核心业务数据,比如订单数据,或者其它核心交易业务数据,在使用kafka时,要保证消息不丢失,并让下游消费系统一定能获得订单数...
继续阅读 »

如果在简历上写了使用过kafka消息中间件,面试官大概80%的概率会问你:"如何保证kafka消息不丢失?"反正我是屡试不爽。

如果你的核心业务数据,比如订单数据,或者其它核心交易业务数据,在使用kafka时,要保证消息不丢失,并让下游消费系统一定能获得订单数据,只靠kafka中间件来保证,是并不可靠的。


kafka已经这么的优秀 了,为什么还会丢消息了?这一定是初学者或者初级使用者心中的疑惑


kafka 已经这么的优秀了,为啥还会丢消息了?----太不省心了


1698128144031.png


图一 生产者,broker,消费者


要解决kafka丢失消息的情况,需要从使用kafka涉及的主流程和主要组件进行分析。kafka的核心业务流程很简单:发送消息,暂存消息,消费消息。而这中间涉及到的主要组件,分别是生产端,broker端,消费端。


生产端丢失消息的情况和解决方法


生产端丢失消息的第一个原因主要来源于kafka的特性:批量发送异步提交。我们知道,kafka在发送消息时,是由底层的IO SEND线程进行消息的批量发送,不是由业务代码线程执行发送的。即业务代码线程执行完send方法后,就返回了。消息到底发送给broker侧没有了?通过send方法其实是无法知道的。
1698128080140.png


那么如何解决了?
kafka提供了一个带有callback回调函数的方法,如果消息成功/(失败的)发送给broker端了,底层的IO线程是可以知道的,所以此时IO线程可以回调callback函数,通知上层业务应用。我们也一般在callback函数里,根据回调函数的参数,就能知道消息是否发送成功了,如果发送失败了,那么我们还可以在callback函数里重试。一般业务场景下 通过重试的方法保证消息再次发送出去。


90%的面试者都能给出上面的标准回答。


但在一些严格的交易场景:仅仅依靠回调函数的通知和重试,是不能保证消息一定能发送到broker端的


理由如下:

1、callback函数是在jvm层面由IO SEND线程执行的,如果刚好遇到在执行回调函数时,jvm宕机了,或者恰好长时间的GC,最终导致OOM,或者jvm假死的情况;那么回调函数是不能被执行的。恰好你的消息数据,是一个带有交易属性核心业务数据,必须要通知给下游。比如下单或者支付后,需要通知佣金系统,或者积分系统,去计算订单佣金。此时一个JVM宕机或者OOM,给下游的数据就丢了,那么计算联盟客的订单佣金数据也就丢了,造成联盟客资损了。


2、IO SEND线程和broker之间是通过网络进行通信的,而网络通信并不一定都能保证一直都是顺畅的,比如网络丢包,网络中的交换机坏了,由底层网络硬件的故障,导致上层IO线程发送消息失败;此时发送端配置的重试参数 retries 也不好使了。


如何解决生产端在极端严格的交易场景下,消息丢失了?

如果要解决jvm宕机,或者JVM假死;又或者底层网络问题,带来的消息丢失;是需要上层应用额外的机制来保证消息数据发送的完整性。大概流程如下图


1698128183781.png


1、在发送消息之前,加一个发送记录,并且初始化为待发送;并且把发送记录进行存储(可以存储在DB里,或者其它存储引擎里);
2、利用带有回调函数的callback通知,在业务代码里感知到消息是否发送成功;如果消息发送成功,则把存储引擎里对应的消息标记为已发送
3、利用延迟的定时任务,每隔5分钟(可根据实际情况调整扫描频率)定时扫描5分钟前未发送或者发送失败的消息,再次进行发送。


这样即使应用的jvm宕机,或者底层网络出现故障,消息是否发送的记录,都进行了保存。通过持续的定时任务扫描和重试,能最终保证消息一定能发送出去。


broker端丢失消息的情况和解决方法


broker端接收到生产端的消息后,并成功应答生产端后,消息会丢吗? 如果broker能像mysql服务器一样,在成功应答给客户端前,能把消息写入到了磁盘进行持久化,并且在宕机断电后,有恢复机制,那么我们能说broker端不会丢消息。


1698128217696.png


但broker端提供数据不丢的保障和mysql是不一样的。broker端在接受了一批消息数据后,是不会马上写入磁盘的,而是先写入到page cache里,这个page cache是操作系统的页缓存(也就是另外一个内存,只是由操作系统管理,不属于JVM管理的内存),通过定时或者定量的的方式(
log.flush.interval.messages和log.flush.interval.ms)会把page cache里的数据写入到磁盘里。


如果page cache在持久化到磁盘前,broker进程宕机了,这个时候不会丢失消息,重启broker即可;如果此时操作系统宕机或者物理机宕机了,page cache里的数据还没有持久化到磁盘里,此种情况数据就丢了。


kafka应对此种情况,建议是通过多副本机制来解决的,核心思想也挺简单的:如果数据保存在一台机器上你觉得可靠性不够,那么我就把相同的数据保存到多台机器上,某台机器宕机了可以由其它机器提供相同的服务和数据。


要想达到上面效果,有三个关键参数需要配置

第一:生产端参数 ack 设置为all

代表消息需要写入到“大多数”的副本分区后,leader broker才给生产端应答消息写入成功。(即写入了“大多数”机器的page cache里)


第二:在broker端 配置 min.insync.replicas参数设置至少为2

此参数代表了 上面的“大多数”副本。为2表示除了写入leader分区外,还需要写入到一个follower 分区副本里,broker端才会应答给生产端消息写入成功。此参数设置需要搭配第一个参数使用。


第三:在broker端配置 replicator.factor参数至少3

此参数表示:topic每个分区的副本数。如果配置为2,表示每个分区只有2个副本,在加上第二个参数消息写入时至少写入2个分区副本,则整个写入逻辑就表示集群中topic的分区副本不能有一个宕机。如果配置为3,则topic的每个分区副本数为3,再加上第二个参数min.insync.replicas为2,即每次,只需要写入2个分区副本即可,另外一个宕机也不影响,在保证了消息不丢的情况下,也能提高分区的可用性;只是有点费空间,毕竟多保存了一份相同的数据到另外一台机器上。


另外在broker端,还有个参数unclean.leader.election.enable

此参数表示:没有和leader分区保持数据同步的副本分区是否也能参与leader分区的选举,建议设置为false,不允许。如果允许,这这些落后的副本分区竞选为leader分区后,则之前leader分区已保存的最新数据就有丢失的风险。注意在0.11版本之前默认为TRUE。


消费端侧丢失消息的情况和解决方法


消费端丢失消息的情况:消费端丢失消息的情况,主要是设置了 autoCommit为true,即消费者消费消息的位移,由消费者自动提交。

自动提交,表面上看起来挺高大上的,但这是消费端丢失消息的主要原因。
实例代码如下


while(true){
consumer.poll(); #①拉取消息
XXX #②进行业务处理;
}

如果在第一步拉取消息后,即提交了消息位移;而在第二步处理消息的时候发生了业务异常,或者jvm宕机了。则第二次在从消费端poll消息时,会从最新的位移拉取后面的消息,这样就造成了消息的丢失。


消费端解决消息丢失也不复杂,设置autoCommit为false;然后在消费完消息后手工提交位移即可
实例代码如下:


while(true){
consumer.poll(); #①拉取消息
XXX #②处理消息;
consumer.commit();
}

在第二步进行了业务处理后,在提交消费的消息位移;这样即使第二步或者第三步提交位移失败了又或者宕机了,第二次再从poll拉取消息时,则会以第一次拉取消息的位移处获取后面的消息,以此保证了消息的不丢失。


总结


在生产端所在的jvm运行正常,底层网络通顺的情况下,通过kafka 生产端自身的retries机制和call back回调能减少一部分消息丢失情况;但并不能保证在应用层,网络层有问题时,也能100%确保消息不丢失;如果要解决此问题,可以试试 记录消息发送状态+定时任务扫描+重试的机制。


在broker端,要保证消息数据不丢失;kafka提供了多副本机制来进行保证。关键核心参数三个,一个生产端ack=all,两个broker端参数min.insync.replicas 写入数据到分区最小副本数为2,并且每个分区的副本集最小为3


在消费端,要保证消息不丢失,需要设置消费端参数 autoCommit为false,并且在消息消费完后,再手工提交消息位置


无论是生产端重复发送消息,还是消费端手工提交消费位移,都会可能会遇到消息重复消费的问题,但这是另外一个消息防重复消费的话题,咋们下期在聊。


原创不易,请 点赞,留言,关注,转载 4暴击^^


参考资料:


kafka.apache.org/20/document… kafka2.0 官方文档


kafka.apache.org/documentati… kafka 0.10.2官方文档


kafka.apache.org/documentati… kafka 3.4.x官方文档


作者:程序员猪佩琪
来源:juejin.cn/post/7293289855076565032
收起阅读 »

谈谈外网刷屏的量子纠缠效果

web
大家好,我卡颂。 最近被一段酷炫的量子纠缠效果刷屏了: 原作者是@_nonfigurativ_,一位艺术家、程序员。 今天简单讲讲他的核心原理。 基础概念 首先我们需要知道两个概念: 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点 窗口坐标系,页面窗口...
继续阅读 »

大家好,我卡颂。


最近被一段酷炫的量子纠缠效果刷屏了:


acda85f4-d21d-407e-b433-b88a4a65468b.gif


原作者是@_nonfigurativ_,一位艺术家、程序员。



今天简单讲讲他的核心原理。


基础概念


首先我们需要知道两个概念:




  • 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点




  • 窗口坐标系,页面窗口左上角就是窗口坐标系的圆点





如果只用一台电脑,不外接屏幕的话,我们会有:




  • 一个屏幕坐标系




  • 打开几个页面,每个页面有各自的窗口坐标系




如果外接了屏幕(或外接pad),那么就存在多个屏幕坐标系,这种情况的计算需要用到管理屏幕设备的API —— window.getScreenDetails,在本文的讨论中不涉及这种情况。


当我们打开一个新页面窗口,窗口的左上角就是窗口坐标系的圆点,如果要在页面正中间画个圆,那圆心的窗口坐标系坐标应该是(window.innerWidth / 2, window.innerHeight / 2)



对于一个打开的窗口:




  • 他的左上角相对于屏幕顶部的距离为window.screenTop




  • 他的左上角相对于屏幕左边的距离为window.screenLeft





所以,我们可以轻松得出圆的圆心在屏幕坐标系中的坐标:



位置检测


在效果中,当打开两个页面,他们能感知到对方的位置并作出反应,这是如何实现的呢?



当前,我们已经知道圆心在屏幕坐标系中的坐标。如果打开多个页面,就会获得多个圆心的屏幕坐标系坐标


现在需要做的,就是让这些页面互相知道对方的坐标,这样就能向对应的方向做出连接的特效。


同源网站跨页面通信的方式有很多,比如:




  • Window.postMessage




  • LocalStorageSessionStorage




  • SharedWorker




  • BroadcastChannel




甚至Cookie也能用于跨页面通信(可以在同源的所有页面之间共享)。


在这里作者使用的是LocalStorage



只需要为每个页面生成一个唯一ID


const pageId = Math.random().toString(36).substring(2); // 生成一个随机的页面ID

每当将圆心最新坐标存储进LocalStorage时:


localStorage.setItem(
pageId,
JSON.stringify({
x: window.screenX,
y: window.screenY,
width: window.innerWidth,
height: window.innerHeight,
})
);

在另一个页面通过监听storage事件就能获取对方圆心的屏幕坐标系坐标


window.addEventListener("storage", (event) => {
if (event.key !== pageId) {
// 来自另一个页面
const { x, y } = JSON.parse(event.newValue);
// ...
}
});

再将对方圆心的屏幕坐标系坐标转换为自身的窗口坐标系坐标,并在该坐标绘制一个圆,就能达到类似窗口叠加后,下面窗口的画面出现在上面窗口内的效果。


通俗的讲,所有页面都会绘制其他页面的圆,只是有些圆在页面窗口外,看不见罢了。



考虑到页面性能,检测圆心的屏幕坐标系坐标渲染圆相关操作可以放到requestAnimationFrame回调中执行。


后记


上述只是该效果的核心原理。要完全复刻效果,还得考虑:




  • 渲染大量粒子(我们示例中用代替),且多窗口通信时的性能问题




  • 窗口移动时的阻尼效果




  • 当前的实现是在同一个屏幕坐标系中,如果要跨屏幕实现,需要使用window.getScreenDetails




不得不感叹跨界(作者是艺术家 + 程序员)迸发的想象力真的不一般。



作者:魔术师卡颂
来源:juejin.cn/post/7304531203771301923
收起阅读 »

[自定义View]一个简单的渐变色ProgressBar

web
Android原生ProgressBar 原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。 style: style效果@android:style/Widget.ProgressBar.Horizontal水平进...
继续阅读 »

Android原生ProgressBar



原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。



style:


style效果
@android:style/Widget.ProgressBar.Horizontal水平进度条
@android:style/Widget.ProgressBar.Small小型圆形进度条
@android:style/Widget.ProgressBar.Large大型圆形进度条
@android:style/Widget.ProgressBar.Inverse反色进度条
@android:style/Widget.ProgressBar.Small.Inverse反色小型圆形进度条
@android:style/Widget.ProgressBar.Large.Inverse反色大型圆形进度条
@android:style/Widget.Material**MD风格

原生的特点就是单调,实现基本的功能,使用简单样式不复杂;要满足我们期望的效果就只能自定义View了。


自定义ProgressBar



自定义View的实现方式有很多种,继承已有的View,如ImageView,ProgressBar等等;也可以直接继承自View,在onDraw中绘制需要的效果。
要实现的效果是一个横向圆角矩形进度条,内容为渐变色。
所以在设计时要考虑到可以定义的属性:渐变色、进度等。



<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="progress">
<attr name="progress" format="float" />
<attr name="startColor" format="color" />
<attr name="endColor" format="color" />
</declare-styleable>
</resources>

View实现



这里直接继承子View,读取属性,在onDraw中绘制进度条。实现思路是通过定义Path来绘制裁切范围,确定绘制内容;再实现线性渐变LinearGradient来填充进度条。然后监听手势动作onTouchEvent,动态绘制长度。


同时开放公共方法,可以动态设置进度颜色,监听进度回调,根据需求实现即可。



package com.cs.app.view

/**
*
*/

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.cs.app.R

class CustomProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val progressPaint = Paint()
private val backgroundPaint = Paint()
private var progress = 50f
private var startColor = Color.parseColor("#4C87B7")
private var endColor = Color.parseColor("#A3D5FE")
private var x = 0f
private var progressCallback: ProgressChange? = null

init {
// 初始化进度条画笔
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.FILL

// 初始化背景画笔
backgroundPaint.isAntiAlias = true
backgroundPaint.style = Paint.Style.FILL
backgroundPaint.color = Color.GRAY

if (attrs != null) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.progress)
startColor = typedArray.getColor(R.styleable.progress_startColor, startColor)
endColor = typedArray.getColor(R.styleable.progress_endColor, endColor)
progress = typedArray.getFloat(R.styleable.progress_progress, progress)
typedArray.recycle()
}
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val width = width.toFloat()
val height = height.toFloat()

//绘制Path,限定Canvas边框
val path = Path()
path.addRoundRect(0f, 0f, width, height, height / 2, height / 2, Path.Direction.CW)
canvas.clipPath(path)

//绘制进度条
val progressRect = RectF(0f, 0f, width * progress / 100f, height)
val colors = intArrayOf(startColor, endColor)
val shader = LinearGradient(0f, 0f, width * progress / 100f, height, colors, null, Shader.TileMode.CLAMP)
progressPaint.shader = shader
canvas.drawRect(progressRect, progressPaint)
}

override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
x = event.rawX

//实现点击调整进度
progress = (event.rawX - left) / width * 100
progressCallback?.onProgressChange(progress)
invalidate()
}

android.view.MotionEvent.ACTION_MOVE -> {
//实现滑动调整进度
progress = (event.rawX - left) / width * 100
progress = if (progress < 0) 0f else if (progress > 100) 100f else progress
progressCallback?.onProgressChange(progress)
invalidate()
}

else -> {}
}
return true
}

fun setProgress(progress: Float) {
this.progress = progress
invalidate()
}

fun setOnProgressChangeListener(callback: ProgressChange) {
progressCallback = callback
}

interface ProgressChange {
fun onProgressChange(progress: Float)
}
}

示例


class CustomViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_view)
val progressTv: TextView = findViewById(R.id.progress_textview)
val view: CustomProgressView = findViewById(R.id.progress)
view.setProgress(50f)

view.setOnProgressChangeListener(object : CustomProgressView.ProgressChange {
override fun onProgressChange(progress: Float) {
progressTv.text = "${progress.toInt()}%"
}
})
}
}

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#0E1D3C"
android:layout_height="match_parent">


<com.cs.app.view.CustomProgressView
android:id="@+id/progress"
android:layout_width="200dp"
android:layout_height="45dp"
app:endColor="#A3D5FE"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.274"
app:progress="60"
app:startColor="#4C87B7" />


<TextView
android:id="@+id/progress_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:textColor="#ffffff"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果如下:


HnVideoEditor_2023_11_23_144034156.gif
作者:LANFLADIMIR
来源:juejin.cn/post/7304531564342837287
收起阅读 »

2023小红书Android面试之旅

一面 自我介绍 看你写了很多文章,拿你理解最深刻的一篇出来讲一讲 讲了Binder相关内容 Binder大概分了几层 哪些方法调用会涉及到Binder通信 大概讲一下startActivity的流程,包括与AMS的交互 全页面停留时长埋...
继续阅读 »

一面




  • 自我介绍




  • 看你写了很多文章,拿你理解最深刻的一篇出来讲一讲


    讲了Binder相关内容




  • Binder大概分了几层




  • 哪些方法调用会涉及到Binder通信




  • 大概讲一下startActivity的流程,包括与AMS的交互




  • 全页面停留时长埋点是怎么做的


    我在项目中做过的内容,主要功能是计算用户在每个Activity的停留时长,并且支持多进程。这里的多进程支持主要是通过以ContentProvider作为中介,然后通过ContentResolver.call方法去调用它的各种方法以实现跨进程




  • 动态权限申请是什么


    详见 Android动态权限申请从未如此简单 这篇文章




  • 你做的性能监测工具,FPS是怎么采集的




  • 性能监测工具用在了什么场景




  • 有没有通过这个性能监测工具去做一些优化




  • 图片库,例如Glide,一般对Bitmap有哪些优化点




  • 过期的Bitmap可以复用吗




  • 有没有基于ASM插桩做过一些插件




  • 讲了一下当时做过的一个个人项目 FastInflate


    这个项目没能达到最终的目标,但通过做这个项目学习了很多新知识,比如APT代码生成、阅读了LayoutInflater源码、AppCompatDelegateImpl实现的LayoutInflater.Factory2会极大的拖慢布局创建的速度等




  • 怎么优化布局创建速度


    提示了预加载,但我当时脑抽在纠结xml的缓存,没想到可以提前把视图先创建好




  • 说一下你觉得你最擅长或者了解最透的点


    我回答的自定义View




  • 解决过View的滑动冲突吗




  • 讲解了一个之前写过的开源控件 SwipeLoadingLayout




  • 一般遇到困难的解决方案是什么




  • 算法题:反转链表




  • 反问阶段




    • 咱们组主要负责哪些内容




    • 主要使用Java还是Kotlin


      Kotlin




    • 小红书的面试一般是怎么个流程?多少轮?


      一般三轮技术面,一轮HR面




    • 面试完一般多久会给到结果


      比较快,一两天的样子






二面




  • 自我介绍




  • 为什么这个时间节点想要出来换工作呢




  • 在B站这些年做了什么




  • 做了哪些基础组件


    讲解了一下之前写的 SwipeLoadingLayout




  • 介绍一下Android的事件传递机制




  • 你写的这个分享模块是如何设计的


    对外采用流式调用的形式,内部通过策略模式区分不同的平台以及分享类型,给每个平台创建了一个中间Activity作为分享SDK请求的发起方(SDK.getApi().share())以及分享结果的接收方(onActivityResult),然后通过广播将分享的结果送入到分享模块内进行处理,最终调用用户设置的分享回调告知结果




  • 看你之前在扇贝的时候有开发过一些性能监测工具,那有做过性能优化吗




  • 你是如何收集这些性能数据的




  • 有没有对哪方面做过一些针对性的优化




  • Android系统为什么会触发ANR,它的机制是什么




  • 有解过ANR相关的问题吗?有哪几种类型?




  • 算法题:二叉树的层序遍历




  • Queue除了LinkedList还有哪些实现类




  • 现在还在面其他公司吗?你自己后面职业生涯的选择是怎么样的?




  • 给我介绍了一下团队,说我面试的这个部门应该说是小红书最核心的团队,包括主页、搜索、图文、视频等等都在部门业务范畴内,部门主要分三层,除了业务层之外还有基础架构层以及性能优化层




  • 反问阶段




    • 部门分三层的话,那新人进来的话是需要从业务层做起吗?


      不是这样的,我们首先会考虑这个同学能干什么,然后会考虑这个同学愿意去做什么,进来后,有经验的同学也会来带你的,不会一上来就让你抗输出,总之会把人放到适合他的团队里




    • 小红书会使用到一些跨端技术吗?


      会,之前在一些新的App上使用的Flutter,现在主要用的是RN,还会使用到一些DSL,这个不能算跨段。为什么在小红书社区App中跨端技术提及的比较少,是因为小红书App非常重视用户体验,对性能的要求比较高






三面




  • 自我介绍




  • 介绍一下目前负责的业务




  • 工作过程中有碰到过什么难题,最后是怎么解决的


    一开始脑抽了没想到该说什么,随便扯了一个没啥技术含量的东西,又扯了一个之前做的信号捕获的工具,后来回忆起来了,重新说了一个关于DEX编排的东西(主DEX中方法数超过65535导致打包失败,写了个脚本将一部分Class从主DEX中移除到其他DEX中)




  • 如何设计一个头像的自定义View,要求使头像展示出来是一个圆形




  • 介绍一下Android事件的分发流程




  • 如何处理View的防误触




  • 怎么处理滑动冲突




  • ActivityonCreate方法中调用了finish方法,那它的生命周期会是怎样的




  • 如果我想判断一个Activity中的一个View的尺寸,那我什么时候能够拿到




  • RecyclerView如何实现一个吸顶效果




  • JavaKoltin你哪个用的比较多




  • 有用过Kotlin的协程吗




  • Kotlin中的哪些Feature你用的多,觉得写的好呢




  • 你是怎么理解MVVM




  • 你有用过Jetpack Compose




  • 有用过kotlin中的by lazylateinit




  • kotlin中怎么实现单例,怎么定义一个类的静态变量




  • 算法题:增量元素之间的最大差值




  • 你这次看机会的原因是什么




  • 反问阶段我感觉之前问的差不多了,这次就没再问什么问题了




HR面




  • 现在是离职还是在职状态




  • 介绍一下之前负责的工作




  • 用户量怎么样




  • 这个项目是从0到1开发的吗




  • 这个业务有什么特点,对于客户端开发有什么挑战与困难




  • 团队分工是怎样的




  • 这个项目能做成现在这个样子,你自己的核心贡献有哪些




  • 这个事情对你来说有什么收获吗




  • 在B站的工作节奏是怎么样的




  • 离职的原因是什么呢




  • 你自己希望找一个什么样的环境或者什么阶段的业务




  • 你对小红书有什么了解吗




  • 未来两三年对于职业发展的想法




  • 你觉得现在有什么限制了你或者你觉得你需要提升哪些部分




  • 反问阶段



    • 问了一些作息、福利待遇之类的问题




总结


小红书面试总体而言给我的体验是很好的,每轮面试后基本上都是当天就能出结果,然后约下一轮的面试。最终从一面到HR面结束出结果,一共花了9天时间,还是挺快的。二面结束后,一面的面试官加我微信说小红书目前很缺人,感兴趣的同学也可以来试试。


作者:dreamgyf
来源:juejin.cn/post/7304267413637333029
收起阅读 »

关于鸿蒙开发,我暂时放弃了

起因 在最近鸿蒙各种新闻资讯说要鸿蒙不再兼容android之后,我看完了鸿蒙视频,并简单的撸了一个demo。 # 鸿蒙HarmonyOS从零实现类微信app效果第一篇,基础界面搭建 # 鸿蒙HarmonyOS从零实现类微信app效果第二篇,我的+发现...
继续阅读 »

image.png


image.png


起因


在最近鸿蒙各种新闻资讯说要鸿蒙不再兼容android之后,我看完了鸿蒙视频,并简单的撸了一个demo。


企业微信截图_6f8acb94-bd68-4f56-9460-4a59d2370a4a.png



鸿蒙的arkui,使用typescript作为基调,然后响应式开发,对于我这个old android来说,确实挺惊艳的。而且在模拟器中运行起来也很快,写demo的过程鸡血满满,着实很愉快。


后面自己写的文章,也在掘金站点上获得了不错的评价。


企业微信截图_fa34f233-af43-4567-8dac-57ef5666f1bd.png


image.png


打击


今天下午,刚好同事有一个遥遥领先(meta 40 pro),鸿蒙4.0版本


怀着秀操作的想法,在同事手机上运行了起来。very nice。 一切出奇的顺利。


but ...


尼玛,点击的时候,直接卡住不对,黑屏。让人瞬间崩溃。


本着优先怀疑自己的原则,我找了一个官方的demo。 运行起来。


额...


尼玛。还是点击之后卡住了,大概30s之后,才跳转到新的页面。


image.png


这一切,让我熬夜掉的头发瞬间崩溃。


放弃了...


放弃了...


后续


和其他学习鸿蒙的伙伴沟通,也遇到了同样的问题,真机不能运行,会卡线程。但是按下home键,再次回到界面,页面会刷新过来


我个人暂时决定搁置对于鸿蒙开发的学习了,后续如果慢慢变得比较成熟之后,再次接触学习吧。


作者:王先生技术栈
来源:juejin.cn/post/7304538094736343052
收起阅读 »

自由职业的好与坏:谈谈我的真实感受

回家自由职业八个多月了,有很多很多和上班不一样的感受。 最近心态算是平稳了,打算把这些写下来。 当然,感受这个东西很主观,每个人都不一样,所以没普适性,看看就好。 整体来说,自由职业有好有坏。 陪家人时间变多了 回家自由职业第一个好处就是陪家人的时间多了很多很...
继续阅读 »

回家自由职业八个多月了,有很多很多和上班不一样的感受。


最近心态算是平稳了,打算把这些写下来。


当然,感受这个东西很主观,每个人都不一样,所以没普适性,看看就好。


整体来说,自由职业有好有坏。


陪家人时间变多了


回家自由职业第一个好处就是陪家人的时间多了很多很多,之前在一线城市打工,我都是过年的时候回家待几天,甚至国庆我都不愿意回家。


那时候接收到家人的消息就是通过电话聊下最近遇到的事情,帮忙出出主意之类的,就像是以一个旁观者的身份在听故事。


而现在我们是在一起经历着这些事情,比如今天上午我妈骑车带我去银行办了一些业务,比如我们会讨论快餐店的哪个咸汤好喝。


这些鸡毛蒜皮的小事,就是生活本身。


在老家陪着家人一起生活,会让我有种踏实感,因为我的根就在这里。


收入焦虑带来的效率不增反降


说实话,自由职业注定会面临一些焦虑,因为之前打工的时候,只要每天按时上下班,每月就能收到工资。


比如之前 30 号中午会收到银行卡工资到账的消息。


而现在这种稳定的收入没有了。


虽然我写东西是能赚到钱的,但现在写东西和之前写东西心态就不一样了。


现在会感觉玩游戏、看小说、刷短视频,都会有负罪感。


一天过去如果啥也没写,就会焦虑。


而这种焦虑反而会让我产生逃避心理,会更多的去玩游戏、看小说、刷短视频。


所以我自由职业后的产出是不增反降了不少。


之前平均每周 3 篇左右的技术文章,现在可能就一篇,而且小册更的也不快。


我最近在有意识的戒掉这些,所以好多了。


有人说当你存款到一定的程度就没这种焦虑了,可能吧,但我还没解锁这个前提。


脱离工作场景后技术成长变慢


工作的时候,做一个个需求,你会遇到各种问题,而解决这些问题的过程中,你会发现一些新的知识,或者对一些技术会有一些新的研究。


而这些其实就是技术成长,也是我之前写文章的重要来源。


但自由职业后就不会自然的收获这些技术成长,需要你刻意的去研究一些东西,而且可能不会有太深入细节的场景应用。


这也导致了我经常不知道写什么。


这也是我文章更的少了的一个原因。


可以自由利用时间做你想做的事情


说了好几个自由职业的缺点,再来讲几个优点。


自由职业有大把时间去研究你感兴趣的一些东西。


比如 Nest 和后端技术栈,我在上次换工作后就很少有机会涉及了,也没时间去做各种实践。


而现在可以花大量时间在这些东西上,短时间内就可以把它研究到一定的程度。


可以自由利用自己的时间,这个还是挺过瘾的。


复利会让你后期起飞


说实话,其实我的收入在自由职业之后是下跌了不少的。


单看收入的话,为什么还要继续呢?


其实写文章、写小册这种事情是有积累效应的,会一直产生复利。


比如我很久之前写过的文章,每天还会有人会阅读点赞


比如我去年写的调试小册,前几天双十一还卖了好几百份


这些会一直源源不断增加我的影响力。


而且会慢慢就会有更多人期待我的下一篇文章、下一本小册。


我现在写了 300 篇文章,还算是有小小的影响力,那如果我再写 500 篇、1000 篇呢?


现在我每本小册平均销量在 5500,那如果我再写 5 本、再写 10 本呢?


只有对这个方向有热情,就是可以长期做下去的,这就是很多人说的相信长期价值。


而且国内程序员还有 35 岁问题,但当你能不靠公司独立获取一定程度的收入,这种问题就会消失。


所以我根本不迷茫 35 岁以后能干啥。


我之前说的想写一辈子技术文章也不是说说而已。


与收入有关的一些问题


很多人关心我收入的问题,那我就来细说下这方面。


我获取收入的方式主要是公众号广告和小册。


公众号广告一个 1000,我大概一个月接 3、4 个,这些够我生活费用了。


感谢印客学院的负责推广的小姐姐,人很 nice。


很多人会反感广告,但你让一个人靠爱发电去写 300 篇技术文章也不现实。


所以,这个就当作很自然的事情就好了。


小册这个,确实有点难受。


比如我的 nest 小册,单看毛收入,是 50 万。


但是扣税 20%、平台分成 30%,加上去掉各种 7 折、 5 折活动、再去掉分销的返现。


能到手一半就不错了。


我最近听朋友说小鹅通这种就没有分成,而且当天打钱,差不多你卖了 50 万能到手 49 万。


类似的,知识星球、小报童等等这些平台,都差不多收入多少到手多少。


所以说,在掘金或者其他平台上出课,有好有坏吧。


要不要交社保


打工的时候公司给交社保,自由职业后就没公司给交了,要不要自己交也是个问题。


这个问题也是自由职业肯定会面临的。


据说社保里有用的就是医保,可以报销医疗费,但我感觉用不大到。


再就是养老保险要交满 15 年社保,我也就交了 5 年多,要不要自己再交 10 年我还没想好。


目前我还没自己交过。


脱离社会的游离感


自由职业之后,不再有固定的上下班时间,不用定闹钟,感受不到周一的痛苦、周五和周末的快乐。


没有这种周的概念,会感觉每天都差不多,时间过的特别快。


而且自己一个人在家里写东西,没有同事、领导等交流,没有团建。


你会有种脱离社会的游离感。


可能喜欢交际的人受不了这种,但我还好,我还是挺喜欢一个人的状态的。


不过感觉不到周和周的界限这个,确实会让我感觉生活少了一些东西,过于平淡了。


小县城没有夜生活和小众圈子


小县城是没有夜生活的,我们这最大的超市 9 点就关门了。


10 点的时候,整个城市差不多就都睡了。


而在北京,12 点睡觉都感觉很早了。


而且,之前在北京上海的时候,不管你有啥特殊爱好,都能找到不少志同道合的人,可以和你交流这些小众的话题。


但回到小县城之后,就不要想了,找不到的。


总结


回到老家小县城很久了,有很多的感受,但直到今天才梳理了一下。


自由职业有好有坏,会有收入焦虑、时间多了但产出不一定会高很多、脱离工作场景遇到的可研究的技术问题会变少,会有脱离社会的游离感,但是陪家人的时间变多了,而且可以把大把时间花在想做的事情上,可以做一些有积累的事情。


当然,感受这个比较主观,只是我个人的一些总结。


当你感觉状态不对的时候,可以像我一样剖析下自己,看看自己最近遇到的问题、发生的变化,和状态好的时候的一些区别。


把这些问题摆出来,并且找到问题的原因,也就能更好的去调整自己。


我的自由职业之路大概率会一直持续下去,希望过段时间我能调节好自己,有超过打工时的状态和输出效率。未来可期,加油吧。


作者:zxg_神说要有光
来源:juejin.cn/post/7304561386888511514
收起阅读 »

糟糕!试用期被裁了

如果你觉得找工作已经够难了?那么抱歉,接下来我说的这件事情可能更令你更糟心。 什么事情呢? 如题所见,就是“试用期被裁这件事”。 每年都会有同学找到我,说自己被裁了,比如下面这些。 同程旅行被裁: 一家知名外企被裁: 其他类似的同学还有很多,比如 B 站试...
继续阅读 »

如果你觉得找工作已经够难了?那么抱歉,接下来我说的这件事情可能更令你更糟心。


什么事情呢?


如题所见,就是“试用期被裁这件事”。


每年都会有同学找到我,说自己被裁了,比如下面这些。


同程旅行被裁:
211efa6455114289e6f46309f7f72bf.jpg
一家知名外企被裁:
c99be7e145f29436f1fc5fd9ca1bb7a.jpg
其他类似的同学还有很多,比如 B 站试用期被裁、小红书被裁、得物被裁等等,因为换了手机,之前的聊天没有了,所以这里只能给大家看最近的截图了。


为什么被裁?


每个人被裁的原因可能都不一样,但大概可能被分为两类:



  1. 主观原因

    1. 技术能力不够

    2. 理解能力不够

    3. 表达、沟通能力不够

    4. 上下级关系没有处理好



  2. 客观原因

    1. 公司财务收紧

    2. 公司业务线调整

    3. 公司转型




其中,客观原因已经超出了我们的掌控范围,所以不是本文要讨论的重点。我们本文主要讨论的是第一类问题,以及如何规避这些问题。


如何避免被裁?


其实知道了被裁的原因,反向提升自己的能力,就可以避免被裁的厄运了(当然非主观原因除外)。


1.多提升技术能力


时刻提醒自己,技术岗的核心竞争力是“技术”,所以既然选择了“技术”这条路,那么只有不断的学习,才是提升自己竞争力的关键。逆水行舟,有时候不是自己退步了,而是身边的人都进步了,那你自然而然就成了垫底的人了,这个时候,离末位淘汰和被裁就不远了。所以,一定要注意。


2.多做有效沟通


沟通分为有效沟通和无效沟通两类,有效沟通是指信息能够准确、清晰地传达、理解和接收的过程。它是在交流中实现真正的相互理解和达成共识的能力。


有效沟通包括以下几个方面:



  1. 表述清晰明确:有效沟通需要明确表达自己的意思,使用简洁、清晰的语言,避免模糊或含糊不清的词语或说法。理解者也要确保准确理解对方的意思,必要时可以进行追问或澄清。

  2. 倾听和理解:有效沟通不仅需要表达自己的观点,也需要认真倾听对方的观点和意见。倾听包括积极关注对方所说的话,并努力理解对方的观点和感受。

  3. 适应对方:有效沟通需要考虑对方的背景、情况和特点。要注意使用对方能够理解和接受的语言和方式进行沟通,避免使用无意义的行话或专业术语。

  4. 反馈和确认:为了确保信息的准确传达和理解,有效沟通需要进行反馈和确认。在沟通过程中,可以提问、请求对方重述或总结所表达的内容,以确保信息传达的一致性和准确性。


3.带上解决方案和自己的思考


上面教你要“多沟通”,但不代表,遇到任何事情都要给领导汇报和沟通,这样只会适得其反。我之前就有一个同事,工作 8~9 年了,我当时是部门负责人,他在工作中的任何问题,无论大小,都要和我反复沟通和确认。刚开始我还能好脾气的和他聊几句,后面就发现,如果要一直这样,那么我什么事也做不了,什么任务也完成不了,自己就像他的私人顾问一样。


这让我很苦恼,后来不得不开除他,因为如果留他在公司,那么我的所有任务都会延期和完成不了。


所以,领导一定是比你忙的,他的事情是最多的。所以遇到事情之后,一定要三思,自己先琢磨,如果自己深入思考之后,还是解决不了,那么这个时候就需要及时和领导沟通汇报了,千万不要怕丢脸。


还有就是沟通之前,最好自己先有几种方案,然后再和领导沟通汇报,让他参与到这件事并做最终的决策。


这里要遵循的原则是:小事、自己能搞明白的事,认真思考之后,能不打扰领导就尽量不打扰领导,但如果真的遇到问题,千万不能藏着掖着,要及时上报,获得领导的支持和协助。


4.笨鸟先飞 + 谦虚谨慎


刚去公司的时候,无论你的技术再好,一定要谦虚谨慎,起码你的业务能力相比于老同事差的还很远,所以这个时候一定要下“奔功夫”。也就是早上早早去公司,提前熟悉公司的业务和开发流程,下班之后,如果同事都在加班,那么自己就多看会公司的文档,刚去切记不要给领导留下偷奸耍滑的印象,一定要能下得了“笨功夫”。


5.多听 + 充分准备


尤其在开需求会的时候,因为刚去公司可能对业务不熟悉,这个时候一定要多听,少发表意见。不要刚到一家公司就指手画脚的,先了解了实际情况之后再发表意见,不要给同事和领导留下“轻浮”的印象。


并且,最好在开需求大会之前,先深入了解业务和需求,做好充分的准备,以便自己能跟得上开会的节奏,也能发表真正有建设性的意见。


6.做事积极 + 可靠


公司给你分配的任务,尤其是试用期,不要表现悲观、不满等情绪,刚去公司试用期还是以低调、谦虚为主。领导给你分下来的活,无论大小,积极主动去做,因为事情你总是要做的,积极也是做,消极也是做,还不如积极一点,给领导留下一个好印象。


其次,做事一定要有结果和反馈,比如领导给你安排一件事要 3 天做完,那么 3 天之后,一定要把当前的任务的具体情况主动主动汇报给领导,这样领导才会觉得你办事靠谱,起码把他说的事认真落实了。


能不能完成任务是能力问题,有没有把领导安排的事积极落实是态度问题,所有的领导都喜欢听话的、办事靠谱的员工,这一点至关重要。


小结


找到工作后,能否平安度过试用期至关重要,所以在试用期期间,一定要下足“笨功夫”,用心做事、谨慎谦虚、积极主动、认真负责,多提升自己的技术能力、多和领导进行有效的沟通,以上这些都是平安度过试用期的关键。加油,少年~


作者:Java中文社群
来源:juejin.cn/post/7304538151455309875
收起阅读 »

努力学习和工作就等于成长吗?

努力学习和工作与成长的关系是一个值得去深思的问题。 有趣的是,参加实习的时候,我将手上的工作做完之后去学其他的技术了,因为那时候刚好比较忙,所以领导就直接提了一个箱子过来,然我去研究一下那个硬件怎么对接。 我看了下文档,只提供了两种语言,C++和JavaScr...
继续阅读 »

努力学习和工作与成长的关系是一个值得去深思的问题。


有趣的是,参加实习的时候,我将手上的工作做完之后去学其他的技术了,因为那时候刚好比较忙,所以领导就直接提了一个箱子过来,然我去研究一下那个硬件怎么对接。


我看了下文档,只提供了两种语言,C++和JavaScript,显然排除了C++,而是使用JavaScript,不过对于写Java的我来说,虽然也玩过两年的JS,但是明显还是不专业。


我将其快速对接完成后,过了几天,又搞了几台硬件过来叫我对接。


显然这次我不想去写好代码再发给前端了,于是直接拉前段代码来和他们一起开发了。


一个后端程序员硬生生去写了前端。


那么这时候,有些人就会说,“哎呀,能者多劳嘛,你看你多么nb,啥都能干,领导就喜欢这种人了”


屁话,这不是能力,这是陷阱。



之前看到一个大佬在他的文章中写道,“如果前端和后端都能干的人,那么大概率是前端能力不怎么滴,后端能力也不怎么滴”。


我们排除那种天生就学习能力特别强的人,这种人天生脑子就是好,学啥都很快,而且学得特别好,但是这样的人是很少数的,和我们大多数人是没关的。


就像有一个大佬,后端特别厉害,手写各种中间件都不在话下,起初我以为他是个全才。


知道有一天,他要出一门教程,然后自己连最基本的CSS和HTML都不会写,然后叫别人给他写。


那么,这能说明他不厉害吗?


各行各业,精英大多都是在自己的领域深耕的。


这个世界最不缺的就是各领域的高手。


在职场中,也并不是什么都会就代表领导赏识你,只能证明你这颗螺丝比较灵活,可以往左边扭,也可以往右边扭。



在自己擅长的领域去做,把一件事尽可能垂直。


之前和一朋友聊天,他说他干过python,干过java,干过测试,干过开发,干过实施......


反正差不多什么都干过了,但是为什么后面还是啥也没干成?


我们顶多能说他职业经历丰富,但是不能说他职业经验丰富,经历是故事,而经验才是成长。


可见垂直是很重要的,不过执着追求垂直也未必是一件好事,还要看风往那边吹,不然在时代发展的潮流中也会显得无力。


就像前10年左右,PHP可谓是一领Web开发的龙头!


那句“PHP是世界上最好的语言”可谓是一针强心剂。


可是现在看来,PHP已经谈出Web领域了,很多PHP框架早已转型,比如swoole,swoft等,只留下那句“PHP是世界上最好的语言”摇摇欲坠。


可笑的是,之前看到一个群友说,领导叫他去维护一套老系统,而老系统就是PHP写的,于是他去学了好久ThinkPHP框架,但是过了半年,这个项目直接被Java重构了。


真是造化弄人啊!



深度学习和浅尝辄止


在我们还没有工作的时候,在学校看着满入眼帘的技术,心中不免有一种冲动,“老子一定要把它全部学完”


于是从表面去看一遍,会一点了,然后马上在自己学习计划上打一个勾。


但是当遇到另外一个新技术的时候,完全又懵了,于是又重复之前的动作。


这看似学了很多,但是实际上啥也没学会。


个人的精力完全是跟不上时代的发展的,十年前左右,随便会一点编程知识,那找工作简直是别人来请的,但是现在不一样了,即使源码看透了,机会也不多。


而如果掌握了核心,那么无论技术再怎么变革,只需要短暂学习就能熟练了。


就像TCP/IP这么多年了,上层建筑依然是靠它。



看似努力,实则自我感动!


在我们读书的时候,总有个别同学看似很努力,但是考试就是考不好。


究其本质,他的努力只是一种伪装,可能去图书馆5个小时,刷抖音就用了四个小时,然后发个朋友圈,“又是对自己负责的一天”。


也有不少人天天加班,然后也会发个朋友圈,“今天的努力只是为了迎接明天更好的自己”。


事实如此吗?


看到希望,有目的性的努力才是人间清醒。


如果觉得自己学得很累,工作得很累,但是实际上啥也没学到,啥也没收获,那么这样得努力是毫无意义的。


这个世界欺骗别人很容易,但是欺骗自己很难!


作者:追梦人刘牌
来源:juejin.cn/post/7303804693192081448
收起阅读 »

日本排放的核污水就像软件项目迭代中的技术债

日本最终还是排放了核污水 2023年8月24日,日本最后还是排放了核污水。网上的讨论很多,有说不应该排放的,也有说污染其实不严重,不需要担心的。首先说明,我是反对排放的。不是因为核污水超标,即使没有超标,我也不认为应该排放到海里。 为什么呢,因为这些辐射元素排...
继续阅读 »

日本最终还是排放了核污水


2023年8月24日,日本最后还是排放了核污水。网上的讨论很多,有说不应该排放的,也有说污染其实不严重,不需要担心的。首先说明,我是反对排放的。不是因为核污水超标,即使没有超标,我也不认为应该排放到海里。


为什么呢,因为这些辐射元素排放到海水中后,会通过食物链富集效应,最终大量的集中到人类身上。即使排放时的指标是安全的,最终,辐射污染富集到人体身上后,迟早要超标的。


关于日本排放核污水的事,我觉得和软件开发过程中的技术债很像。所以今天就蹭个热点,聊一聊项目中的技术债。


软件项目的迭代过程


我记得大学的时候,课本上学的软件迭代还是瀑布流的迭代方式。这个就比较简单,提出一个大项目,然后细化各个功能。架构师拿到十分完善的需求文档后,开始架构设计。然后开发,测试,上线,验收,完成整个项目。在这个过程中,需求是明确的,按照需求去实现就行。


可是后来出现了敏捷开发。老板们一看,这个好呀,敏捷开发,不就是快速开发吗,大大提高开发速度。尤其是互联网公司,讲究的就是一个唯快不破。然后,,,很明显就感觉到,项目中的技术债越来越多了。


原先项目需求明确后,即使开发过程中有需求变化,也都是在代码上线前修改,可以把看到的不合理代码设计给改掉,影响可控。敏捷后,项目需求是逐步迭代上线的,有时候需求间是相互影响的,再加上互联网的人员换的也勤快,需求时间也短,这前后的代码越来越不融洽。


技术债的积累


技术债是怎么积累的呢,其实是项目过程中,为了短期的收益(上线时间)而做出的技术上的妥协(怎么快怎么来),这些妥协可能会在未来导致更多的工作,或者可能的线上问题。可以看出,技术债务并不总是坏事。有时,为了满足紧迫的市场需求或截止日期,团队可能需要做出一些妥协。关键是要意识到这些妥协,并在适当的时候偿还这些债务。如果需求上线了,就不管对应的技术债了,这个技术债可不就是越来越多吗。


就像文章最开始的日本排放核污水。从当年日本核电站地震出事,日本采用了注水冷却并储存核污水的方案,我们就能意识到,这个核污水一定要处理掉的,不然越堆越多,最后就只能排放出来的。10几年了,日本就没想过要解决核污水,现在说要排放,我觉得吧,这个估计是10几年前就确定的方案了,只是没有往外说,最后就看什么时间排放。最后,我们就这样又一次见证了历史。


重构


技术债不断积累后,会导致新需求越做越慢,bug还多。这个时候,老板就会觉得做这个需求的人不行,做的又慢,bug又多。但是只有做需求的人才知道,真的改不动呀。


所以,当你做一个需求,感觉改不动,或者明明没有改多少代码,但是bug特别多。 那么一定要想一想这个项目是不是存在了很长时间了。如果存在了很长时间了,那么大概率需要解决技术债的时候到了。怎么解决,重构!!


怎么重构,这个话题很大,一两句说不清楚。不过,一旦你成功重构了一个老项目,那么大概率你的技术水平能有一大步的提升。


就像有人说的,一个文明的衰退等于一个新的文明的诞生。






作者:写代码的浩
来源:juejin.cn/post/7271140848850272267
收起阅读 »

Java代码是如何被CPU狂飙起来的?

无论是刚刚入门Java的新手还是已经工作了的老司机,恐怕都不容易把Java代码如何一步步被CPU执行起来这个问题完全讲清楚。但是对于一个Java程序员来说写了那么久的代码,我们总要搞清楚自己写的Java代码到底是怎么运行起来的。另外在求职面试的时候这个问题也常...
继续阅读 »

无论是刚刚入门Java的新手还是已经工作了的老司机,恐怕都不容易把Java代码如何一步步被CPU执行起来这个问题完全讲清楚。但是对于一个Java程序员来说写了那么久的代码,我们总要搞清楚自己写的Java代码到底是怎么运行起来的。另外在求职面试的时候这个问题也常常会聊到,面试官主要想通过它考察求职同学对于Java以及计算机基础技术体系的理解程度,看似简单的问题实际上囊括了JVM运行原理、操作系统以及CPU运行原理等多方面的技术知识点。我们一起来看看Java代码到底是怎么被运行起来的。


Java如何实现跨平台


在介绍Java如何一步步被执行起来之前,我们需要先弄明白为什么Java可以实现跨平台运行,因为搞清楚了这个问题之后,对于我们理解Java程序如何被CPU执行起来非常有帮助。


为什么需要JVM


write once run anywhere曾经是Java响彻编程语言圈的slogan,也就是所谓的程序员开发完java应用程序后,可以在不需要做任何调整的情况下,无差别的在任何支持Java的平台上运行,并获得相同的运行结果从而实现跨平台运行,那么Java到底是如何做到这一点的呢?


其实对于大多数的编程语言来说,都需要将程序转换为机器语言才能最终被CPU执行起来。因为无论是如Java这种高级语言还是像汇编这种低级语言实际上都是给人看的,但是计算机无法直接进行识别运行。因此想要CPU执行程序就必须要进行语言转换,将程序语言转化为CPU可以识别的机器语言。


image.png


学过计算机组成原理的同学肯定都知道,CPU内部都是用大规模晶体管组合而成的,而晶体管只有高电位以及低电位两种状态,正好对应二进制的0和1,因此机器码实际就是由0和1组成的二进制编码集合,它可以被CPU直接识别和执行。


image.png


但是像X86架构或者ARM架构,不同类型的平台对应的机器语言是不一样的,这里的机器语言指的是用二进制表示的计算机可以直接识别和执行的指令集集合。不同平台使用的CPU不同,那么对应的指令集也就有所差异,比如说X86使用的是CISC复杂指令集而ARM使用的是RISC精简指令集。所以Java要想实现跨平台运行就必须要屏蔽不同架构下的计算机底层细节差异。因此,如何解决不同平台下机器语言的适配问题是Java实现一次编写,到处运行的关键所在。


那么Java到底是如何解决这个问题的呢?怎么才能让CPU可以看懂程序员写的Java代码呢?其实这就像在我们的日常生活中,如果双方语言不通,要想进行交流的话就必须中间得有一个翻译,这样通过翻译的语言转换就可以实现双方畅通无阻的交流了。打个比方,一个中国厨师要教法国厨师和阿拉伯厨师做菜,中国厨师不懂法语和阿拉伯语,法国厨师和阿拉伯厨师不懂中文,要想顺利把菜做好就需要有翻译来帮忙。中国厨师把做菜的菜谱告诉翻译者,翻译者将中文菜谱转换为法文菜谱以及阿拉伯语菜谱,这样法国厨师和阿拉伯厨师就知道怎么做菜了。


image.png


因此Java的设计者借助了这样的思想,通过JVM(Java Virtual Machine,Java虚拟机)这个中间翻译来实现语言转换。程序员编写以.java为结尾的程序之后通过javac编译器把.java为结尾的程序文件编译成.class结尾的字节码文件,这个字节码文件需要JVM这个中间翻译进行识别解析,它由一组如下图这样的16进制数组成。JVM将字节码文件转化为汇编语言后再由硬件解析为机器语言最终最终交给CPU执行。


640.png


所以说通过JVM实现了计算机底层细节的屏蔽,因此windows平台有windows平台的JVM,Linux平台有Linux平台的JVM,这样在不同平台上存在对应的JVM充当中间翻译的作用。因此只要编译一次,不同平台的JVM都可以将对应的字节码文件进行解析后运行,从而实现在不同平台下运行的效果。


image.png


那么问题又来了,JVM是怎么解析运行.class文件的呢?要想搞清楚这个问题,我们得先看看JVM的内存结构到底是怎样的,了解JVM结构之后这个问题就迎刃而解了。


JVM结构


JVM(Java Virtual Machine)即Java虚拟机,它的核心作用主要有两个,一个是运行Java应用程序,另一个是管理Java应用程序的内存。它主要由三部分组成,类加载器、运行时数据区以及字节码执行引擎。


image.png


类加载器


类加载器负责将字节码文件加载到内存中,主要经历加载-》连接-》实例化三个阶段完成类加载操作。


image.png


另外需要注意的是.class并不是一次性全部加载到内存中,而是在Java应用程序需要的时候才会加载。也就是说当JVM请求一个类进行加载的时候,类加载器就会尝试查找定位这个类,当查找对应的类之后将他的完全限定类定义加载到运行时数据区中。


运行时数据区


JVM定义了在Java程序运行期间需要使用到的内存区域,简单来说这块内存区域存放了字节码信息以及程序执行过程数据。运行时数据区主要划分了堆、程序计数器虚拟机栈、本地方法栈以及元空间数据区。其中堆数据区域在JVM启动后便会进行分配,而虚拟机栈、程序计数器本地方法栈都是在常见线程后进行分配。


image.png


不过需要说明的是在JDK 1.8及以后的版本中,方法区被移除了,取而代之的是元空间(Metaspace)。元空间与方法区的作用相似,都是存储类的结构信息,包括类的定义、方法的定义、字段的定义以及字节码指令。不同的是,元空间不再是JVM内存的一部分,而是通过本地内存(Native Memory)来实现的。在JVM启动时,元空间的大小由MaxMetaspaceSize参数指定,JVM在运行时会自动调整元空间的大小,以适应不同的程序需求。


字节码执行引擎


字节码执行引擎最核心的作用就是将字节码文件解释为可执行程序,主要包含了解释器、即使编译以及垃圾回收器。字节码执行引擎从元空间获取字节码指令进行执行。当Java程序调用一个方法时,JVM会根据方法的描述符和方法所在的类在元空间中查找对应的字节码指令。字节码执行引擎从元空间获取字节码指令,然后执行这些指令。


JVM如何运行Java程序


在搞清楚了JVM的结构之后,接下来我们一起来看看天天写的Java代码是如何被CPU飙起来的。一般公司的研发流程都是产品经理提需求然后程序员来实现。所以当产品经理把需求提过来之后,程序员就需要分析需求进行设计然后编码实现,比如我们通过Idea来完成编码工作,这个时候工程中就会有一堆的以.java结尾的Java代码文件,实际上就是程序员将产品需求转化为对应的Java程序。但是这个.java结尾的Java代码文件是给程序员看的,计算机无法识别,所以需要进行转换,转换为计算机可以识别的机器语言。


image.png


通过上文我们知道,Java为了实现write once,run anywhere的宏伟目标设计了JVM来充当转换翻译的工作。因此我们编写好的.java文件需要通过javac编译成.class文件,这个class文件就是传说中的字节码文件,而字节码文件就是JVM的输入。


image.png


当我们有了.class文件也就是字节码文件之后,就需要启动一个JVM实例来进一步加载解析.class字节码。实际上JVM本质其实就是操作系统中的一个进程,因此要想通过JVM加载解析.class文件,必须先启动一个JVM进程。JVM进程启动之后通过类加载器加载.class文件,将字节码加载到JVM对应的内存空间。


image.png


当.class文件对应的字节码信息被加载到中之后,操作系统会调度CPU资源来按照对应的指令执行java程序。


image.png


以上是CPU执行Java代码的大致步骤,看到这里我相信很多同学都有疑问这个执行步骤也太大致了吧。哈哈,别着急,有了基本的解析流程之后我们再对其中的细节进行分析,首先我们就需要弄清楚JVM是如何加载编译后的.class文件的。


字节码文件结构


要想搞清楚JVM如何加载解析字节码文件,我们就先得弄明白字节码文件的格式,因为任何文件的解析都是根据该文件的格式来进行。就像CPU有自己的指令集一样,JVM也有自己一套指令集也就是Java字节码,从根上来说Java字节码是机器语言的.class文件表现形式。字节码文件结构是一组以 8 位为最小单元的十六进制数据流,具体的结构如下图所示,主要包含了魔数、class文件版本、常量池、访问标志、索引、字段表集合、方法表集合以及属性表集合描述数据信息。


image.png


这里简单说明下各个部分的作用,后面会有专门的文章再详细进行阐述。


魔数与文件版本


魔数的作用就是告诉JVM自己是一个字节码文件,你JVM快来加载我吧,对于Java字节码文件来说,其魔数为0xCAFEBABE,现在知道为什么Java的标志是咖啡了吧。而紧随魔数之后的两个字节是文件版本号,Java的版本号通常是以52.0的形式表示,其中高16位表示主版本号,低16位表示次版本号。。


常量池


在常量池中说明常量个数以及具体的常量信息,常量池中主要存放了字面量以及符号引用这两类常量数据,所谓字面量就是代码中声明为final的常量值,而符号引用主要为类和接口的完全限定名、字段的名称和描述符以及方法的名称以及描述符。这些信息在加载到JVM之后在运行期间将符号引用转化为直接引用才能被真正使用。常量池的第一个元素是常量池大小,占据两个字节。常量池表的索引从1开始,而不是从0开始,这是因为常量池的第0个位置是用于特殊用途的。


访问标志


类或者接口的访问标记,说明类是public还是abstract,用于描述该类的访问级别和属性。访问标志的取值范围是一个16位的二进制数。


索引


包含了类索引、父类索引、接口索引数据,主要说明类的继承关系。


字段表集合


主要是类级变量而不是方法内部的局部变量。


方法表集合


主要用来描述类中有几个方法,每个方法的具体信息,包含了方法访问标识、方法名称索引、方法描述符索引、属性计数器、属性表等信息,总之就是描述方法的基础信息。


属性表集合


方法表集合之后是属性表集合,用于描述该类的所有属性。属性表集合包含了所有该类的属性的描述信息,包括属性名称、属性类型、属性值等等。


解析字节码文件


知道了字节码文件的结构之后,JVM就需要对字节码文件进行解析,将字节码结构解析为JVM内部流转的数据结构。大致的过程如下:


1、读取字节码文件


JVM首先需要读取字节码文件的二进制数据,这通常是通过文件输入流来完成的。


2、解析字节码


JVM解析字节码的过程是将字节码文件中的二进制数据解析为Java虚拟机中的数据结构。首先JVM首先会读取字节码文件的前四个字节,判断魔数是否为0xCAFEBABE,以此来确认该文件是否是一个有效的Java字节码文件。JVM接着会解析常量池表,将其中的常量转换为Java虚拟机中的数据结构,例如将字符串常量转换为Java字符串对象。解析类、接口、字段、方法等信息:JVM会依次解析类索引、父类索引、接口索引集合、字段表集合、方法表集合等信息,将这些信息转换为Java虚拟机中的数据结构。最后,JVM将解析得到的数据结构组装成一个Java类的结构,并将其放入元空间中。


在完成字节码文件解析之后,接下来就需要类加载器闪亮登场了,类加载器会将类文件加载到JVM内存中,并为该类生成一个Class对象。


类加载


加载器启动


我们都知道,Java应用的类都是通过类加载器加载到运行时数据区的,这里很多同学可能会有疑问,那么类加载器本身又是被谁加载的呢?这有点像先有鸡还是先有蛋的灵魂拷问。实际上类加载器启动大致会经历如下几个阶段:


image.png


1、以linux系统为例,当我们通过"java"启动一个Java应用的时候,其实就是启动了一个JVM进程实例,此时操作系统会为这个JVM进程实例分配CPU、内存等系统资源;


2、"java"可执行文件此时就会解析相关的启动参数,主要包括了查找jre路径、各种包的路径以及虚拟机参数等,进而获取定位libjvm.so位置,通过libjvm.so来启动JVM进程实例;


3、当JVM启动后会创建引导类加载器Bootsrap ClassLoader,这个ClassLoader是C++语言实现的,它是最基础的类加载器,没有父类加载器。通过它加载Java应用运行时所需要的基础类,主要包括JAVA_HOME/jre/lib下的rt.jar等基础jar包;


4、而在rt.jar中包含了Launcher类,当Launcher类被加载之后,就会触发创建Launcher静态实例对象,而Launcher类的构造函数中,完成了对于ExtClassLoader及AppClassLoader的创建。Launcher类的部分代码如下所示:


public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
//类静态实例
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;

public static Launcher getLauncher() {
return launcher;
}
//Launcher构造器
public Launcher() {
ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}

try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}

Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}

if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}

System.setSecurityManager(var3);
}

}
...
}

双亲委派模型


为了保证Java程序的安全性和稳定性,JVM设计了双亲委派模型类加载机制。在双亲委派模型中,启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)以及应用程序类加载器(Application ClassLoader)按照一个父子关系形成了一个层次结构,其中启动类加载器位于最顶层,应用程序类加载器位于最底层。当一个类加载器需要加载一个类时,它首先会委派给它的父类加载器去尝试加载这个类。如果父类加载器能够成功加载这个类,那么就直接返回这个类的Class对象,如果父类加载器无法加载这个类,那么就会交给子类加载器去尝试加载这个类。这个过程会一直持续到顶层的启动类加载器。


image.png


通过这种双亲委派模型,可以保证同一个类在不同的类加载器中只会被加载一次,从而避免了类的重复加载,也保证了类的唯一性。同时,由于每个类加载器只会加载自己所负责的类,因此可以防止恶意代码的注入和类的篡改,提高了Java程序的安全性。


数据流转过程


当类加载器完成字节码数据加载任务之后,JVM划分了专门的内存区域内承载这些字节码数据以及运行时中间数据。其中程序计数器、虚拟机栈以及本地方法栈属于线程私有的,堆以及元数据区属于共享数据区,不同的线程共享这两部分内存数据。我们还是以下面这段代码来说明程序运行的时候,各部分数据在Runtime data area中是如何流转的。


public class Test {
public static void main(String[] args) {
User user = new User();
Integer result = calculate(user.getAge());
System.out.println(result);
}

private static Integer calculate(Integer age) {
Integer data = age + 3;
return data;
}

}

以上代码对应的字节码指令如下所示:


image.png


如上代码所示,JVM创建线程来承载代码的执行过程,我们可以将线程理解为一个按照一定顺序执行的控制流。当线程创建之后,同时创建该线程独享的程序计数器(Program Counter Register)以及Java虚拟机栈(Java Virtual Machine Stack)。如果当前虚拟机中的线程执行的是Java方法,那么此时程序计数器中起初存储的是方法的第一条指令,当方法开始执行之后,PC寄存器存储的是下一个字节码指令的地址。但是如果当前虚拟机中的线程执行的是naive方法,那么程序计数器中的值为undefined。


那么程序计数器中的值又是怎么被改变的呢?如果是正常进行代码执行,那么当线程执行字节码指令时,程序计数器会进行自动加1指向下一条字节码指令地址。但是如果遇到判断分支、循环以及异常等不同的控制转移语句,程序计数器会被置为目标字节码指令的地址。另外在多线程切换的时候,虚拟机会记录当前线程的程序计数器,当线程切换回来的时候会根据此前记录的值恢复到程序计数器中,来继续执行线程的后续的字节码指令。


除了程序计数器之外,字节码指令的执行流转还需要虚拟机栈的参与。我们先来看下虚拟机栈的大致结构,如下图所示,栈大家肯定都知道,它是一个先入后出的数据结构,非常适合配合方法的执行过程。虚拟机栈操作的基本元素就是栈帧,栈帧的结构主要包含了局部变量、操作数栈、动态连接以及方法返回地址这几个部分。


image.png


局部变量


主要存放了栈帧对应方法的参数以及方法中定义的局部变量,实际上它是一个以0为起始索引的数组结构,可以通过索引来访问局部变量表中的元素,还包括了基本类型以及对象引用等。非静态方法中,第0个槽位默认是用于存储this指针,而其他参数和变量则会从第1个槽位开始存储。在静态方法中,第0个槽位可以用来存放方法的参数或者其他的数据。


操作数栈


和虚拟机栈一样操作数栈也是一个栈数据结构,只不过两者存储的对象不一样。操作数栈主要存储了方法内部操作数的值以及计算结果,操作数栈会将运算的参与方以及计算结果都压入操作数栈中,后续的指令操作就可以从操作数栈中使用这些值来进行计算。当方法有返回值的时候,返回值也会被压入操作数栈中,这样方法调用者可以获取到返回值。


动态链接


一个类中的方法可能会被程序中的其他多个类所共享使用,因此在编译期间实际无法确定方法的实际位置到底在哪里,因此需要在运行时动态链接来确定方法对应的地址。动态链接是通过在栈帧中维护一张方法调用的符号表来实现的。这张符号表中保存了当前方法中所有调用的方法的符号引用,包括方法名、参数类型和返回值类型等信息。当方法需要调用另一个方法时,它会在符号表中查找所需方法的符号引用,然后进行动态链接,确定方法的具体内存地址。这样,就能够正确地调用所需的方法。


方法返回地址:


当一个方法执行完毕后,JVM会将记录的方法返回地址数据置入程序计数器中,这样字节码执行引擎可以根据程序计数器中的地址继续向后执行字节码指令。同时JVM会将方法返回值压入调用方的操作栈中以便于后续的指令计算,操作完成之后从虚拟机栈中奖栈帧进行弹出。


知道了虚拟机栈的结构之后,我们来看下方法执行的流转过程是怎样的。


1、JVM启动完成.class文件加载之后,它会创建一个名为"main"的线程,并且该线程会自动调用定义在该类中的名为"main"的静态方法,这也是Java程序的入口点;


2、当JVM在主线程中调用当方法的时候就会创建当前线程独享的程序计数器以及虚拟机栈,在Test.class类中,开始执行mian方法 ,因此JVM会虚拟机栈中压入main方法对应的栈帧;


image.png


3、在栈帧的操作数栈中存储了操作的数据,JVM执行字节码指令的时候从操作数栈中获取数据,执行计算操作之后再将结果压入操作数栈;


4、当进行calculate方法调用的时候,虚拟机栈继续压入calculate方法对应的栈帧,被调用方法的参数、局部变量和操作数栈等信息会存储在新创建的栈帧中。其中该栈帧中的方法返回地址中存放了main方法执行的地址信息,方便在调用方法执行完成后继续恢复调用前的代码执行;


image.png


5、对于age + 3一条加法指令,在执行该指令之前,JVM会将操作数栈顶部的两个元素弹出,并将它们相加,然后将结果推入操作数栈中。在这个例子中,指令的操作码是“add”,它表示执行加法操作;操作数是0,它表示从操作数栈的顶部获取第一个操作数;操作数是1,它表示从操作数栈的次顶部获取第二个操作数;


6、程序计数器中存储了下一条需要执行操作的字节码指令的地址,因此Java线程执行业务逻辑的时候必须借助于程序计数器才能获得下一步命令的地址;


7、当calculate方法执行完成之后,对应的栈帧将从虚拟机栈中弹出,其中方法执行的结果会被压入main方法对应的栈帧中的操作数栈中,而方法返回地址被重置到main现场对应的程序计数器中,以便于后续字节码执行引擎从程序计数器中获取下一条命令的地址。如果方法没有返回值,JVM仍然会将一个null值推送到调用该方法的栈帧的操作数栈中,作为占位符,以便恢复调用方的操作数栈状态。


8、字节码执行引擎中的解释器会从程序计数器中获取下一个字节码指令的地址,也就是从元空间中获取对应的字节码指令,在获取到指令之后,通过翻译器翻译为对应的汇编语言而再交给硬件解析为机器指令,最终由CPU进行执行,而后再将执行结果进行写回。


CPU执行程序
通过上文我们知道无论什么编程语言最终都需要转化为机器语言才能被CPU执行,但是CPU、内存这些硬件资源并不是直接可以和应用程序打交道,而是通过操作系统来进行统一管理的。对于CPU来说,操作系统通过调度器(Scheduler)来决定哪些进程可以被CPU执行,并为它们分配时间片。它会从就绪队列中选择一个进程并将其分配给CPU执行。当一个进程的时间片用完或者发生了I/O等事件时,CPU会被释放,操作系统的调度器会重新选择一个进程并将其分配给CPU执行。也就是说操作系统通过进程调度算法来管理CPU的分配以及调度,进程调度算法的目的就是为了最大化CPU使用率,避免出现任务分配不均空闲等待的情况。主要的进程调度算法包括了FCFS、SJF、RR、MLFQ等。


CPU如何执行指令?
前文中我们大致搞清楚了类是如何被加载的,各部分类字节码数据在运行时数据区怎么流转以及字节码执行引擎翻译字节码。实际上在运行时数据区数据流转的过程中,CPU已经参与其中了。程序的本质是为了根据输入获得相应的输出,而CPU本质就是根据程序的指令一步步执行获得结果的工具。对于CPU来说,它核心工作主要分为如下三个步骤;


1、获取指令


CPU从PC寄存器中获取对应的指令地址,此处的指令地址是将要执行指令的地址,根据指令地址获取对应的操作指令到指令寄存中,此时如果是顺存执行则PC寄存器地址会自动加1,但是如果程序涉及到条件、循环等分支执行逻辑,那么PC寄存器的地址就会被修改为下一条指令执行的地址。


2、指令译码


将获取到的指令进行翻译,搞清楚哪些是操作码哪些是操作数。CPU首先读取指令中的操作码然后根据操作码来确定该指令的类型以及需要进行的操作,CPU接着根据操作码来确定指令所需的寄存器和内存地址,并将它们提取出来。


3、执行指令


经过指令译码之后,CPU根据获取到的指令进行具体的执行操作,并将指令运算的结果存储回内存或者寄存器中。


image.png


因此一旦CPU上电之后,它就像一个勤劳的小蜜蜂一样,一直不断重复着获取指令-》指令译码-》执行指令的循环操作。


CPU如何响应中断?


当操作系统需要执行某些操作时,它会发送一个中断请求给CPU。CPU在接收到中断请求后,会停止当前的任务,并转而执行中断处理程序,这个处理程序是由操作系统提供的。中断处理程序会根据中断类型,执行相应的操作,并返回到原来的任务继续执行。


在执行完中断处理程序后,CPU会将之前保存的程序现场信息恢复,然后继续执行被中断的程序。这个过程叫做中断返回(Interrupt Return,IRET)。在中断返回过程中,CPU会将处理完的结果保存在寄存器中,然后从栈中弹出被中断的程序的现场信息,恢复之前的现场状态,最后再次执行被中断的程序,继续执行之前被中断的指令。
那么CPU又是如何响应中断的呢?主要经历了以下几个步骤:


image.png


1、保存当前程序状态


CPU会将当前程序的状态(如程序计数器、寄存器、标志位等)保存到内存或栈中,以便在中断处理程序执行完毕后恢复现场。


2、确定中断类型


CPU会检查中断信号的类型,以确定需要执行哪个中断处理程序。


3、转移控制权


CPU会将程序的控制权转移到中断处理程序的入口地址,开始执行中断处理程序。


4、执行中断处理程序


中断处理程序会根据中断类型执行相应的操作,这些操作可能包括保存现场信息、读取中断事件的相关数据、执行特定的操作,以及返回到原来的程序继续执行等。


5、恢复现场


中断处理程序执行完毕后,CPU会从保存的现场信息中恢复原来程序的状态,然后将控制权返回到原来的程序中,继续执行被中断的指令。


后记


很多时候看似理所当然的问题,当我们深究下去就会发现原来别有一番天地。正如阿里王坚博士说的那样,要想看一个人对某个领域的知识掌握的情况,那就看他能就这个领域的知识能讲多长时间。想想的确如此,如果我们能够对某个知识点高度提炼同时又可以细节满满的进行展开阐述,那我们对于这个领域的理解程度就会鞭辟入里。这种检验自己知识学习深度的方式也推荐给大家。


作者:慕枫技术笔记
来源:juejin.cn/post/7207769757570482234
收起阅读 »

前端半自动化部署

web
在前端项目部署时,通常会经历以下步骤: 构建项目:在部署之前,需要使用相应的构建工具(如Webpack、Vite等)对项目进行构建,生成生产环境所需的静态文件(如HTML、CSS、JavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化...
继续阅读 »

在前端项目部署时,通常会经历以下步骤:


image.png



  1. 构建项目:在部署之前,需要使用相应的构建工具(如WebpackVite等)对项目进行构建,生成生产环境所需的静态文件(如HTMLCSSJavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化等操作。

  2. 选择部署方式:根据项目的实际需求,选择适合的部署方式。常见的部署方式包括将静态文件部署到静态文件托管服务(如NetlifyVercelGitHub Pages等)、与后端API服务一起部署到云服务器(如AWS、阿里云、腾讯云等)等。

  3. 配置域名和SSL证书:如果你有自定义域名,需要在域名服务商处将域名解析到部署好的静态文件托管服务或云服务器上。同时,为了保障网站的安全性,建议配置SSL证书,使网站能够通过HTTPS协议进行访问。

  4. 持续集成/持续部署(CI/CD :可以考虑使用CI/CD工具(如GitHub ActionsGitLab CITravis CI等)来实现自动化的构建和部署流程,以提高开发效率并确保部署过程的稳定性。

  5. 性能优化:在部署完成后,可以对网站进行性能优化,包括使用CDN加速、资源压缩、缓存配置等,以提高网站的加载速度和用户体验。

  6. 监控和日志:部署完成后,建议设置监控系统以及日志记录系统,及时发现和解决线上问题。


具体的部署流程会因项目和需求的不同而有所差异。


本文从部署方案一步步做实践,最终实现半自动化部署,当然也可以直接使用Docker或其他方案实现自动化部署。


手动化的部署流程是利用xshell连接服务器,利用xftp进行文件传输。操作流程相对比较原始化。


image.png


假如要实现协同开发人员可以实现共同部署,并且可以减去每次部署都要打开xshell。可以写一段脚本实现连接服务器进行文件传输过程,保证打包后能够运行脚本自动化上传文件。


首先要解决连接服务器问题,可以通过ssh实现。(ftpssh是两种常用的远程文件传输协议,可以高效地将代码上传到服务器。)


await ssh.connect({
host: '主机名',
username: '用户名',
password: '密码'
})

服务器连接成功后,可以进行文件传输


await ssh.putDirectory('本地目录路径', '远程目录路径', {
recursive: true, // 上传整个目录
concurrency: 10, // 同时上传的文件数量
tick(localPath, remotePath, error) { // 通过tick回调函数来监听上传过程中的状态
if(error) {
console.log(`无法上传${localPath}${remotePath}${error}`)
} else {
console.log(`${localPath}上传至${remotePath}`)
}
}
})

此时即可实现脚本的基本功能,只需要在每次npm run build结束后,自动执行这段脚本即可。


因此,只需要在package.json文件中scripts命令下添加一行代码即可。


"build:deploy": "vue-cli-service build && node deploy.js"

这段代码因不同的框架版本可能有所不同,只需要在普通npm run build执行内容后面拼接node deploy.jsdeploy.js就是我们所写的脚本文件。


打包完之后,上传文件过程如图所示,非常丝滑:


企业微信截图_17006194685935.png


此时会有一个问题,打包的dist文件每次上传至服务器时,dist文件一直被覆盖,无法实现按版本回滚。


只需要在上传之前,修改服务器旧的dist文件名,这样旧版本就得以保存。


// 判断服务器dist文件是否存在
let newDistExist = await ssh.execCommand(`ls 旧版本`)
while(newDistExist.code === 0) {
i ++
newDistExits = await ssh.execCommand(`ls 旧版本i`)
}
// 重命名旧版本dist文件
await ssh.execCommand(`mv 旧版本 新版本`)

此时即可实现旧版本保存,以便可以按版本实现回滚操作。


上述过程仅仅是针对个人打包上传服务器,如果想要实现协同开发,则要实现代码共享(例如上传git仓库),为了安全性,账号密码不能以明文暴露。


可以采取以下三种方案,当然没有绝对意义上的安全。



  1. terminal实现账号密码输入


可以利用password-prompt插件,它可以帮助你在命令行中以安全的方式提示用户输入密码。


const getUserInfo = async() => {
const username = await passwordPrompt('输入用户名:')
const password = await passwordPrompt('输入密码:', { method: 'hide' })
return { username, password }
}

将输入的usernamepassword传到远程服务器进行校验即可。



  1. 账号密码加密


由于代码要上传git,所以可以在git上传前进行加密处理,调用gitpre-commit钩子,执行加密,每次pull时候进行解密处理,调用post-merge钩子,调用post-merge钩子时候仅仅需要输入解密口令即可。解密口令只需要做到组员共享即可,此时的解密口令同样可以借助password-prompt插件进行输入。相对第一种方案,输入的内容更少了😂。


const crypto = require('crypto') // 密钥和加密算法 
const secretKey = 'your-secret-key' // 这里替换为你自己的密钥
const algorithm = 'aes-256-cbc' // 使用的加密算法
function decryptData(encryptedData) {
const decipher = crypto.createDecipher(algorithm, secretKey)
let decryptedData = decipher.update(encryptedData, 'hex', 'utf8')
decryptedData += decipher.final('utf8')
return decryptedData
}
// 从环境变量或其他安全方式获取加密的敏感信息
const encryptedInfo = process.env.ENCRYPTED_INFO // 这里假设加密的信息存储在环境变量中
// 解密敏感信息
const decryptedInfo = decryptData(encryptedInfo)
// 在这里使用解密后的敏感信息进行后续操作
console.log('Decrypted info:', decryptedInfo)

git的钩子函数可以采用git hooks工具哈士奇(husky)进行配置,具体配置方式不再赘述。


以上就是本文在前端半自动化部署方面的探索,大家可以贡献自己的想法/做法呀!


作者:一颗多愁善感的派大星j
来源:juejin.cn/post/7303862023618805795
收起阅读 »

Android 自定义理化表达式View

一、前言 在 Android 中实现上下标我们一般使用 SpannableString 去完成,需要计算开始位置和结束位置,也要设置各种 Span,而且动态性不是很好,因为无法做到规则统一约束,因此有必要进行专有规则设定,提高代码使用的灵活程度。 当然,也有很...
继续阅读 »

一、前言


在 Android 中实现上下标我们一般使用 SpannableString 去完成,需要计算开始位置和结束位置,也要设置各种 Span,而且动态性不是很好,因为无法做到规则统一约束,因此有必要进行专有规则设定,提高代码使用的灵活程度。


当然,也有很多开源的项目,但是对于简单的数学和化学表达式,大多都缺少通用性,仅限于项目本身使用,这也是本篇实现的主要目的之一。对于其他类型如求和公式、平方根公式、分子分母其实也可以通过本篇的思想,进行一些列改造即可,当然也可以借助语法树,实现自己的公式编辑器。



二、效果预览



三、实现


实现其实很简单,本身就是借助Canvas#drawTextXXX实现,但是我们这里仍然需要回顾的问题是字体测量和基线计算问题。


3.1 字体测量


常用的宽高测量如下


        //获取文本最小宽度(真实宽度)
private static int getTextRealWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.width();
}

//获取文本最小高度(真实高度)
private static int getTextRealHeight(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.height();
}

//真实宽度 + 笔画左右两侧间隙(一般绘制的的时候建议使用这种,左右两侧的间隙和字形有关)
private static int getTextWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
return (int) paint.measureText(text);
}

//真实宽度 + 笔画上下两侧间隙(符合文本绘制基线)
private static int getTextHeight(Paint paint) {
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int textHeight = ~fm.top - (~fm.top - ~fm.ascent) - (fm.bottom - fm.descent);
return textHeight;
}

3.2基线计算


在Canvas 绘制,实际上Html中的Canvas一样都需要计算意义,因为文字的受到不同文化的影响,表现形式不同,另外音标等问题存在,所以使用基线来绘制更合理。



推导算法如下


       /**
* 基线到中线的距离=(Descent+Ascent)/2-Descent
* 注意,实际获取到的Ascent是负数。公式推导过程如下:
* 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。
*/

public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}

3.3 全部代码


public class MathExpressTextView extends View {

private final List<TextInfo> TEXT_INFOS = new ArrayList<>();
private int textSpace = 15;
private String TAG = "MathExpressTextView";
protected Paint mTextPaint;
protected Paint mSubTextPaint;
protected Paint mMarkTextPaint;
protected float mContentWidth = 0f;
protected float mContentHeight = 0f;
protected float mMaxSize = 0;


public void setMaxTextSize(float sizePx) {
mMaxSize = sizePx;

mTextPaint.setTextSize(mMaxSize);
mSubTextPaint.setTextSize(mMaxSize / 3f);
mMarkTextPaint.setTextSize(mMaxSize / 2f);

invalidate();
}

public void setTextSpace(int textSpace) {
this.textSpace = textSpace;
}

public void setContentHeight(float height) {
mContentHeight = height;
invalidate();
}

public MathExpressTextView(Context context){
this(context,null);
}
public MathExpressTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
setEditDesignTextInfos();
}

public MathExpressTextView setText(String text, String subText, String supText, float space) {
this.TEXT_INFOS.clear();
TextInfo.Builder tb = new TextInfo.Builder(text, mTextPaint, mSubTextPaint)
.subText(subText)
.supText(supText)
.textSpace(space);

this.TEXT_INFOS.add(tb.build());
return this;
}

public MathExpressTextView appendMarkText(String text) {
TextInfo.Builder tb = new TextInfo.Builder(text, mMarkTextPaint, mMarkTextPaint);
this.TEXT_INFOS.add(tb.build());
return this;
}

public MathExpressTextView appendText(String text, String subText, String supText, float space) {
TextInfo.Builder tb = new TextInfo.Builder(text, mTextPaint, mSubTextPaint)
.subText(subText)
.supText(supText)
.textSpace(space);

this.TEXT_INFOS.add(tb.build());
return this;
}

private void setEditDesignTextInfos() {

if (!isInEditMode()) return;
// setText("2H", "2", "", 10)
// .appendMarkText("+");
// appendText("O", "2", "", 10);
// appendMarkText("=");
// appendText("2H", "2", "", 10);
// appendText("O", "", "", 10);

// setText("sin(Θ+α)", "", "", 10)
// .appendMarkText("=");
// appendText("sinΘcosα", "", "", 10);
// appendMarkText("+");
// appendText("cosΘsinα", "", "", 10);

setText("cos2Θ", "1", "", 10)
.appendMarkText("=");
appendText("cos", "", "2", 10);
appendText("Θ", "1", "", 10);
appendMarkText("-");
appendText("sin", "", "2", 10);
appendText("Θ", "1", "", 10);

}
public Paint getTextPaint() {
return mTextPaint;
}

public Paint getSubTextPaint() {
return mSubTextPaint;
}

public Paint getMarkTextPaint() {
return mMarkTextPaint;
}

private float dpTopx(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}


private void init() {

mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setAntiAlias(true);
mTextPaint.setStyle(Paint.Style.STROKE);

mMarkTextPaint = new Paint();
mMarkTextPaint.setColor(Color.WHITE);
mMarkTextPaint.setAntiAlias(true);
mMarkTextPaint.setStyle(Paint.Style.STROKE);

mSubTextPaint = new Paint();
mSubTextPaint.setColor(Color.WHITE);
mSubTextPaint.setAntiAlias(true);
mSubTextPaint.setStyle(Paint.Style.STROKE);

setMaxTextSize(dpTopx(30));

}


private void setSubTextShader() {
if (this.colors != null) {
float textHeight = mSubTextPaint.descent() - mSubTextPaint.ascent();
float textOffset = (textHeight / 2) - mSubTextPaint.descent();
Rect bounds = new Rect();
mSubTextPaint.getTextBounds("%", 0, 1, bounds);
mSubTextPaint.setShader(new LinearGradient(0, mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f, 0,
mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f - bounds.height(), colors, positions, Shader.TileMode.CLAMP));
} else {
mSubTextPaint.setShader(null);
}

}

private void setMarTextShader() {
if (this.colors != null) {
float textHeight = mMarkTextPaint.descent() - mMarkTextPaint.ascent();
float textOffset = (textHeight / 2) - mMarkTextPaint.descent();
Rect bounds = new Rect();
mMarkTextPaint.getTextBounds("%", 0, 1, bounds);
mMarkTextPaint.setShader(new LinearGradient(0, mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f, 0,
mContentHeight / 2 + textOffset - mMaxSize / 100 * 22f - bounds.height(), colors, positions, Shader.TileMode.CLAMP));
} else {
mMarkTextPaint.setShader(null);
}

}

private void setTextShader() {
if (this.colors != null) {
float textHeight = mTextPaint.descent() - mTextPaint.ascent();
float textOffset = (textHeight / 2) - mTextPaint.descent();
Rect bounds = new Rect();
mTextPaint.getTextBounds("A", 0, 1, bounds);
mTextPaint.setShader(new LinearGradient(0, mContentHeight / 2 + textOffset, 0, mContentHeight / 2 + textOffset - bounds.height(), colors, positions, Shader.TileMode.CLAMP));
} else {
mTextPaint.setShader(null);
}
}


public void setColor(int unitColor, int numColor) {
mSubTextPaint.setColor(unitColor);
mTextPaint.setColor(numColor);
}

RectF contentRect = new RectF();

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

if (mContentWidth <= 0) {
mContentWidth = getWidth();
}
if (mContentHeight <= 0) {
mContentHeight = getHeight();
}

if (mContentWidth == 0 || mContentHeight == 0) return;

setTextShader();
setSubTextShader();
setMarTextShader();

if (TEXT_INFOS.size() == 0) return;

int width = getWidth();
int height = getHeight();


contentRect.left = (width - mContentWidth) / 2f;
contentRect.top = (height - mContentHeight) / 2f;
contentRect.right = contentRect.left + mContentWidth;
contentRect.bottom = contentRect.top + mContentHeight;

int id = canvas.save();
float centerX = contentRect.centerX();
float centerY = contentRect.centerY();
canvas.translate(centerX, centerY);

contentRect.left = -centerX;
contentRect.right = centerX;
contentRect.top = -centerY;
contentRect.bottom = centerY;


float totalTextWidth = 0l;
int textCount = TEXT_INFOS.size();

for (int i = 0; i < textCount; i++) {
totalTextWidth += TEXT_INFOS.get(i).getTextWidth();
if (i < textCount - 1) {
totalTextWidth += textSpace;
}
}

drawGuideBaseline(canvas, contentRect, totalTextWidth);

float startOffsetX = -(totalTextWidth) / 2f;
for (int i = 0; i < textCount; i++) {
TEXT_INFOS.get(i).draw(canvas, startOffsetX, contentRect.centerY());
startOffsetX += TEXT_INFOS.get(i).getTextWidth() + textSpace;
}

canvas.restoreToCount(id);

}

private void drawGuideBaseline(Canvas canvas, RectF contentRect, float totalTextWidth) {

if (!isInEditMode()) return;

Paint guidelinePaint = new Paint();
guidelinePaint.setAntiAlias(true);
guidelinePaint.setStrokeWidth(0);
guidelinePaint.setStyle(Paint.Style.FILL);

RectF hline = new RectF();
hline.top = -1;
hline.bottom = 1;
hline.left = -totalTextWidth / 2;
hline.right = totalTextWidth / 2;
canvas.drawRect(hline, guidelinePaint);

RectF vline = new RectF();
hline.left = -1;
vline.top = contentRect.top;
vline.bottom = contentRect.bottom;
vline.right = 1;

canvas.drawRect(vline, guidelinePaint);
}


private static class TextInfo {
Paint subOrSupTextPaint = null;
String subText = null;
String supText = null;
Paint textPaint = null;
String text;
float space;

private TextInfo(String text, String subText, String supText, Paint textPaint, Paint subOrSupTextPaint, float space) {
this.text = text;
if (this.text == null) {
this.text = "";
}
this.subText = subText;
this.supText = supText;
this.space = space;
this.textPaint = textPaint;
this.subOrSupTextPaint = subOrSupTextPaint;
}

public void draw(Canvas canvas, float startX, float startY) {

if (this.textPaint == null) {
return;
}

canvas.drawText(this.text, startX, startY + getTextPaintBaseline(this.textPaint), this.textPaint);

if (this.subOrSupTextPaint == null) {
return;
}
if (this.supText != null) {
RectF rect = new RectF();
rect.left = startX + space + getTextWidth(this.text, this.textPaint);
rect.top = -getTextHeight(this.textPaint) / 2;
rect.bottom = 0;
rect.right = rect.left + getTextWidth(supText, this.subOrSupTextPaint);
canvas.drawText(supText, rect.left, rect.centerY() + getTextPaintBaseline(this.subOrSupTextPaint), this.subOrSupTextPaint);
}


if (this.subText != null) {
RectF rect = new RectF();
rect.left = startX + space + getTextWidth(this.text, this.textPaint);
rect.top = 0;
rect.bottom = getTextHeight(this.textPaint) / 2;
rect.right = rect.left + getTextWidth(subText, this.subOrSupTextPaint);
canvas.drawText(subText, rect.left, rect.centerY() + getTextPaintBaseline(this.subOrSupTextPaint), this.subOrSupTextPaint);
}

}

/**
* 基线到中线的距离=(Descent+Ascent)/2-Descent
* 注意,实际获取到的Ascent是负数。公式推导过程如下:
* 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。
*/

public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
}


public float getTextWidth() {

if (textPaint == null) {
return 0;
}

float width = 0;

width = getTextWidth(this.text, textPaint);

float subTextWidth = 0;
if (this.subText != null && subOrSupTextPaint != null) {
subTextWidth = getTextWidth(this.subText, subOrSupTextPaint) + space;
}

float supTextWidth = 0;
if (this.supText != null && subOrSupTextPaint != null) {
supTextWidth = getTextWidth(this.supText, subOrSupTextPaint) + space;
}
return width + Math.max(subTextWidth, supTextWidth);
}


//获取文本最小宽度(真实宽度)
private static int getTextRealWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.width();
}

//获取文本最小高度(真实高度)
private static int getTextRealHeight(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
Rect rect = new Rect(); // 文字所在区域的矩形
paint.getTextBounds(text, 0, text.length(), rect);
//获取最小矩形,该矩形紧贴文字笔画开始的位置
return rect.height();
}

//真实宽度 + 笔画左右两侧间隙(一般绘制的的时候建议使用这种,左右两侧的间隙和字形有关)
private static int getTextWidth(String text, Paint paint) {
if (TextUtils.isEmpty(text)) return 0;
return (int) paint.measureText(text);
}

//真实宽度 + 笔画上下两侧间隙(符合文本绘制基线)
private static int getTextHeight(Paint paint) {
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int textHeight = ~fm.top - (~fm.top - ~fm.ascent) - (fm.bottom - fm.descent);
return textHeight;
}


private static class Builder {

Paint subOrSupTextPaint = null;
Paint textPaint = null;
String subText = null;
String supText = null;
String text;
float space;

public Builder(String text, Paint textPaint, Paint subOrSupTextPaint) {
this.text = text;
this.textPaint = textPaint;
this.subOrSupTextPaint = subOrSupTextPaint;
}

public Builder subText(String subText) {
this.subText = subText;
return this;
}

public Builder supText(String supText) {
this.supText = supText;
return this;
}

public Builder textSpace(float space) {
this.space = space;
return this;
}

public TextInfo build() {
return new TextInfo(text, this.subText, this.supText, this.textPaint, this.subOrSupTextPaint, this.space);
}
}

}
private int[] colors = new int[]{
0xC0FFFFFF, 0x9fFFFFFF,
0x98FFFFFF, 0xA5FFFFFF,
0xB3FFFFFF, 0xBEFFFFFF,
0xCCFFFFFF, 0xD8FFFFFF,
0xE5FFFFFF, 0xFFFFFFFF};
private float[] positions = new float[]{
0f, 0.05f,
0.3f, 0.4f,
0.5f, 0.6f,
0.7f, 0.8f,
0.9f, 1f};

public void setShaderColors(int[] colors) {
this.colors = colors;
}

public void setShaderColors(int c) {
this.colors = new int[]{c, c, c, c,
c, c, c, c, c, c};
}

}

3.4 使用


        MathExpressTextView m1 = findViewById(R.id.math_exp_1);
MathExpressTextView m2 = findViewById(R.id.math_exp_2);
MathExpressTextView m3 = findViewById(R.id.math_exp_3);
MathExpressTextView m4 = findViewById(R.id.math_exp_4);


m1.setShaderColors(0xffFF4081);
m1.setText("2H","2","",10)
.appendMarkText("+")
.appendText("O","2","",10)
.appendMarkText("
=")
.appendText("
2H","2","",10)
.appendText("
O","","",10);

m2.setShaderColors(0xffff9922);
m2.setText("
2","","2",10)
.appendMarkText("
+")
.appendText("
5","","-1",10)
.appendMarkText("
=")
.appendText("
4.2","","",10);

m3.setShaderColors(0xffFFEAC4);
m3.setText("
H","2","0",10)
.appendMarkText("
+")
.appendText("
Cu","","+2",10)
.appendText("
O","","-2",10)
.appendMarkText("
==")
.appendText("
Cu","","0",10)
.appendText("
H","2","+1",10)
.appendText("
O","","-2",10);


m4.setText("
985","","GB",10)
.appendMarkText("
+")
.appendText("
211","","MB",10);

四、总结


相对来说本篇相对简单,没有过多复杂的计算。但是对于打算实现公式编辑器的项目,可参考本方案的设计思想:



  • 组合化:通过大公式,组合小公式,这样也方便使用语法树,提高通用性。

  • 对象化:单独描述单独片段

  • 规则化:对不同的片段进行规则化绘制,如appendMarkText方法


作者:时光少年
来源:juejin.cn/post/7303792111719792666
收起阅读 »

极简原生js图形验证码

web
       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。 示例: 思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑...
继续阅读 »

       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。


示例:


1700643433840.png



思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑登录出错刷新,此处只做样式不写进去)
实现过程为1.先写一个canvas标签做绘图容器。    →     2.将拿到的值绘制到容器中并写好样式。    →     3.点击刷新重新绘制。



写一个canvas标签当容器


<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>

并设置容器宽高背景颜色或图片等样式


写一个数值绘制到canvas的方法


//text为传递的数值
function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

拿到数值调用绘制方法



此处为样式示例,因此数值我用4位随机数表示,实际情况为你从后端取得的值,并依靠这个值在后端判断验证码是否一致。



// 调用函数生成验证码并显示在页面上  
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });

监听标签点击实现点击刷新



此处要注意一定要先清空canvas中已绘制图像再渲染新数值,因此直接将清除范围设置较大。



 // 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

最后实现效果:


1700645148990.png


完整代码演示


<!DOCTYPE html>
<html>

<head>
<title>String to Captcha</title>
</head>

<body>
<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>


<script>
// 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
</script>
</body>

</html>

作者:方苕爱吃瓜
来源:juejin.cn/post/7304182005285830693
收起阅读 »

数据库优化之:like %xxx%该如何优化?

今天给大家分享一个小知识,实际项目中,like %xxx%的情况其实挺多的,比如某个表单如果支持根据公司名进行搜索,用户一般都是输入湖南xxx有限公司中的xxx进行搜索,所以对于接口而言,就必须使用like %xxx%来支持,从而不符合最左前缀原则导致索引失效...
继续阅读 »

今天给大家分享一个小知识,实际项目中,like %xxx%的情况其实挺多的,比如某个表单如果支持根据公司名进行搜索,用户一般都是输入湖南xxx有限公司中的xxx进行搜索,所以对于接口而言,就必须使用like %xxx%来支持,从而不符合最左前缀原则导致索引失效,那么该如何优化这种情况呢?


第一种可以尝试的方案就是利用索引条件下推,我先演示再讲原理,比如我有下面一张订单表:


就算给company_name创建一个索引,执行where company_name like '%腾讯%'也不会走索引。


但是如果给created_at, company_name创建一个联合索引,那么执行where created_at=CURDATE() and company_name like '%腾讯%'就会走联合索引,并且company_name like '%腾讯%'就会利用到索引条件下推机制,比如下图中Extra里的Using index condition就表示利用了索引条件下推。


所以,并不是like %xxx%就一定会导致索引失效,原理也可以配合其他字段一起来建联合索引,从而使用到索引条件下推机制。


再来简单分析一下索引条件下推的原理,在执行查询时先利用SQL中所提供的created_at条件在联合索引B+树中进行快速查找,匹配到所有符合created_at条件的B+树叶子节点后,再根据company_name条件进行过滤,然后再根据过滤之后的结果中的主键ID进行回表找到其他字段(回表),最终才返回结果,这样处理的好处是能够减少回表的次数,从而提高查询效率。


当然,如果实在不能建立或不方便建立联合索引,导致不能利用索引条件下推机制,那么其实可以先试试Mysql中的全文索引,最后才考虑引入ES等中间件,当然Mysql其他一些常规优化机制也是可以先考虑的,比如分页、索引覆盖(不select *)等。


作者:爱读源码的大都督
来源:juejin.cn/post/7301955975337738279
收起阅读 »

让Android开发Demo页面变得简单起来

Github: github.com/eekidu/devl… DevLayout DevLayout支持使用代码的方式,快速添加常用调试控件,无需XML,简化调试页面开发过程 背景 我们在开发组件库的时候,通常会开发一个Demo页面,用于展示或者调试该组件库...
继续阅读 »

Screenshot_20231122_163444.png


Github: github.com/eekidu/devl…


DevLayout


DevLayout支持使用代码的方式,快速添加常用调试控件,无需XML,简化调试页面开发过程


背景


我们在开发组件库的时候,通常会开发一个Demo页面,用于展示或者调试该组件库。
这种页面对UI的美观度要求很低,注重的是快速实现

使用XML布局方式开发会比较繁琐,该库会简化这一页面UI的开发流程:



  • 对常用的控件进行了封装,可以通过调用DevLayout的方法进行创建;

  • 并按流式布局或者线性布局的方式摆放到DevLayout中。


image.png

引入依赖


在Project的build.gradle在添加以下代码


allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

在Module的build.gradle在添加以下代码


最新版本:


implementation 'com.github.eekidu:devlayout:Tag'

使用


DevLayout是一个ViewGr0up,你可以把它摆放到页面上合适的位置,然后通过调用它的方法来添加需要子控件。


//1、创建或者获取一个DevLaout实例
var mDevLayout = findViewById<DevLayout>(R.id.devLayout)


//2、调用方法添加调试控件

/**
* 添加功能按钮
*/

mDevLayout.addButton("功能1") {
//点击回调
}

/**
* 添加开关
*/

mDevLayout.addSwitch("开关1") { buttonView, isChecked ->
//状态切换回调
}

/**
* 添加SeekBar
*/

mDevLayout.addSeekBar("参数设置1") { progress ->
//进度回调
}.setMax(1000).setProgress(50).setEnableStep(true)//启用步进


/**
* 添加输入框
*/

mDevLayout.addEditor("参数设置") { inputText ->
textView.text = inputText
}

/**
* 单选,切换布局样式
*/

mDevLayout.addRadioGr0up("布局方式")
.addItem("流式布局") {
mDevLayout.setIsLineStyle(false)
}.addItem("线性布局") {
mDevLayout.setIsLineStyle(true)
}.setChecked(0)

/**
* 添加日志框
*/

mDevLayout.addLogMonitor()

/**
* 输出日志
*/

mDevLayout.log(msg)
mDevLayout.logI(msg)
mDevLayout.logD(msg)
mDevLayout.logW(msg)
mDevLayout.logE(msg)


/**
* 添加换行
*/

mDevLayout.br()
/**
* 添加分割线
*/

mDevLayout.hr()

//其他类型控件见Demo MainActivity.kt


耗时监控


我们调试代码一个重要的目的就是:发现耗时方法从而进行优化,DevLayout提供一个简易的耗时打印功能,实现如下:
大部分需要调试的代码,会在控件的回调中触发,那么对回调进行代理,在代理中监控原始回调的执行情况,就可以得到调试代码的执行耗时。


伪代码如下:


class ClickProxyListener(val realListener: OnClickListener) : OnClickListener {

override fun onClick(v: View) {
val startTime = Now()// 1、记录起始时间

realListener.onClick(v)//原始回调执行

val eTime = Now() - startTime//2、计算执行耗时
log("执行耗时:${eTime}")
}
}

//创建代理对象
val listenerProxy = ClickProxyListener(realListener)

由于控件种类很多,回调类的类型也都不一样,如何对形形色色的回调统一进行监控?


动态代理:封装了ProxyListener代理类,对原始回调进行代理


open class ProxyListener<T>(val realListener: T) : InvocationHandler {

override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any {
val startTime = Now()// 1、记录起始时间

val result = method.invoke(realListener, *(args ?: emptyArray()))//原始回调执行

val eTime = Now() - startTime//2、计算执行耗时
log("执行耗时:${eTime}")
return result
}
}

//动态创建代理对象
val listener = Proxy.newProxyInstance(_, listenerInterface , ProxyListener(realListener))

结合该例子感受动态代理的优点:




  • 灵活性:动态代理允许在运行时创建代理对象,而不需要在编译时指定具体的代理类。这使得代理对象可以根据需要动态地适应不同的接口和实现类。




  • 可扩展性:动态代理可以用于实现各种不同的功能,例如日志记录、性能监控、事务管理等。通过在代理对象的方法调用前后插入额外的逻辑,可以轻松地扩展现有的代码功能。




  • 解耦合:动态代理可以将代理逻辑与真实对象的实现逻辑分离。这样,代理对象可以独立于真实对象进行修改和维护,而不会影响到真实对象的代码。




  • 减少重复代码:通过使用动态代理,可以将一些通用的代码逻辑抽取到代理类中,从而减少代码的重复性。这样可以提高代码的可维护性和可读性。




  • 动态性:动态代理可以在运行时动态地创建代理对象,这意味着可以根据需要动态地修改代理对象的行为。这种灵活性使得动态代理在一些特定的场景下非常有用,例如AOP(面向切面编程)。




日志


日志是调试代码的重要方式,在某些场景下需要将日志输出到UI上,方便在设备没有连接Logcat,无法通过控制台监测日志时,也能对程序执行的中间过程或执行结果有一定的展示。


我们可以添加一个日志框到UI界面上,以此来展示Log信息,方式如下:


//添加日志框,默认尺寸,添加后也可以通过UI调整
mDevLayout.addLogMonitor()
mDevLayout.addLogMonitorSmall()
mDevLayout.addLogMonitorLarge()

//输出日志
mDevLayout.log(msg)
mDevLayout.logI(msg)
mDevLayout.logD(msg)
mDevLayout.logW(msg)
mDevLayout.logE(msg)

支持过滤:



  • 按等级过滤

  • 按关键词过滤,多关键字格式:key1,key2


同时,日志信息会在Logcat控制台输出,通过 tag:DevLayout 进行过滤查看。


image.png


最后


Github: github.com/eekidu/devl…


欢迎Star,如果有更好的优化方案,欢迎在github上提出,我们一起互相学习!


作者:Caohaikuan
来源:juejin.cn/post/7304182005285584933
收起阅读 »

软件著作权证书申请

大家好,我是小悟 对我们行业来说,软著有什么作用不言而喻。对于在读学生来说,可能对加学分、评奖学金、保研、简历装饰有帮助。对于企业来说,可能对高企申请、应用市场上架有帮助。对于职场人士来说,可能对职称评定、升职加薪有帮助。但最重要的一点是,能保护你的软件成果...
继续阅读 »

大家好,我是小悟


image.png


对我们行业来说,软著有什么作用不言而喻。对于在读学生来说,可能对加学分、评奖学金、保研、简历装饰有帮助。对于企业来说,可能对高企申请、应用市场上架有帮助。对于职场人士来说,可能对职称评定、升职加薪有帮助。但最重要的一点是,能保护你的软件成果。


关于这个软著模板,已经有很多小伙伴领取到了,能不能用,全在于小伙伴们自己,只要用心去做,我觉得总会成功。

有按照模板申请成功的


图片


也有觉得模板没用的


图片


甚至还有觉得没用爆粗口的,难听,这就不放图了,这种人就是想着不要发挥自己的一点点脑筋,就是想找个不用自己修改一丁点的,最好是直接就能用的。很无奈啊。


但大部分小伙伴是有礼貌的


图片


经过实践,自己已经申请过成功很多张软著证书了,积累了一定经验。


image.png


image.png


image.png


image.png


image.png


image.png


您的一键三连,是我更新的最大动力,谢谢


山水有相逢,来日皆可期,谢谢阅读,我们再会


我手中的金箍棒,上能通天,下能探海


作者:悟空码字
来源:juejin.cn/post/7303827467037442048
收起阅读 »

你的团队是“活”的吗?

最近有同学离职,让我突然思考一个话题。 之前在腾讯,内部转岗叫做活水,是希望通过内部转岗,盘活团队。让团队保持一定的人员流动性,让个人与团队双向奔赴,满足各自的需要。因此,我们都希望,团队是活水,而不是一潭死水。 为什么团队要保持一定的人员流动性呢? “优”...
继续阅读 »

最近有同学离职,让我突然思考一个话题。


之前在腾讯,内部转岗叫做活水,是希望通过内部转岗,盘活团队。让团队保持一定的人员流动性,让个人与团队双向奔赴,满足各自的需要。因此,我们都希望,团队是活水,而不是一潭死水


为什么团队要保持一定的人员流动性呢?



  • “优”胜“劣”汰。这里不是指恶意竞争和卷。而是通过一定的人员流动性,有进有出,从而找到更加适合团队的人。找到跟团队价值观一致的,志同道合的成员。而跟团队匹配度不是很高的人,可以去寻找更加适合自己的团队和岗位,这对于双方都是有好处的。

  • 激活团队。当一个团队保持稳定太久,就会有点思想固化,甚至落后了。这时候,需要通过一些新鲜血液,带来不同的思想和经验,来激活团队,这就像鲶鱼一样。


那想要形成一个“活”的团队,需要什么条件呢?



  • 薪资待遇要好。首先是基本福利待遇要高于业界平均水平。其次,绩效激励是有想象空间的。如果没有这个条件,那人员流动肯定是入不敷出的,优秀的人都被挖跑了。

  • 团队专业。团队在业界有一定的影响力,在某一方面的专业技术和产出保持业界领先。这个条件隐含了一个信息,就是团队所在业务是有挑战的,因为技术产出一般都是依赖于业务的,没有业务实践和验证,是做不出优秀的技术产出的。因此,待遇好、有技术成长、有职业发展空间,这三者是留住人才的主要手段。

  • 梯队完整。在有了前面 2 个条件之后,就有了吸引人才的核心资源了。那接下来就需要有一个完整的梯队。因为资源是有限的,团队资源只能分配到有限人手里,根据最经典的 361,待遇和职业发展空间最多只能覆盖 3 成,技术成长再多覆盖 3 成人已经不错了。那剩下的 4 成人怎么办?所以,团队需要有一些相对稳定的人,他们能完成安排的事情,不出错,也不需要他们卷起来。


这是我当前的想法,我想我还需要更多的经验和讨论的。


那我目前的团队是“活”的吗?答案是否定的。


首先,过去一年,公司的招聘被锁了,内部转岗也基本转不动。薪资待遇就更不用说了。整个环境到处都充斥着“躺”的氛围。


其次,团队专业度一般,在金融业务,前端的发挥空间极其有限。我也只能尽自己所能,帮大家寻求一些技术成长的空间,但还是很有限。


最后,梯队还没有完整,还在建设中,不过也是步履维艰。因为前两个条件限制,别说吸引优秀人才了,能不能保住都是个问题。


最近公司开始放开招聘了,但还不是大面积的,不过还是有希望可以给有人员流失的团队补充 hc 的。但比较难受的是,这个 hc 不是过我的手的,哈哈,又有种听天由命的感觉。


这就是我最近的一个随想,那么,你的团队是“活”的吗?


作者:潜龙在渊灬
来源:juejin.cn/post/7298347391164383259
收起阅读 »

像mysql一样查询ES,一看就会,爽歪歪

ElasticSearch是现在最流行的搜索引擎了,查询快,性能好。可能唯一的缺点就是查询的语法Query DSL(Domain Specific Language)比较难记,今天分享一个直接用sql查询ES的方法。 ::: 1.简介 先简单介绍一下这个s...
继续阅读 »

ElasticSearch是现在最流行的搜索引擎了,查询快,性能好。可能唯一的缺点就是查询的语法Query DSL(Domain Specific Language)比较难记,今天分享一个直接用sql查询ES的方法。 :::




1.简介


先简单介绍一下这个sql查询,因为社区一直反馈这个Query DSL 实在是太难用了。大家可以感受一下下面这个es的查询。


GET /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title""search" } },
        {
          "bool": {
            "should": [
              { "term": { "category""books" } },
              { "term": { "category""music" } }
            ]
          }
        }
      ],
      "filter": {
        "range": {
          "price": { "gte": 20, "lte": 100 }
        }
      }
    }
  },
  "aggs": {
    "avg_price_per_category": {
      "terms": {
        "field""category",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field""price"
          }
        }
      }
    }
  }
}

这个查询使用了bool查询来组合多个条件,包括must、should和filter。同时也包含了聚合(aggs)来计算不同类别的平均价格。对于业务查询来讲,这个查询很普通。但是还是很难理解,特别是对于新手来讲,更难记了,很容易出错。


如果是mysql的查询,就是这么写


SELECT title, category, price 
FROM my_index 
WHERE (title = 'search' AND (category = 'books' OR category = 'music')) 
AND price >= 20 AND price <= 100 
GR0UP BY category 
ORDER BY AVG(price) DESC 
LIMIT 10

mysql 的查询就很简洁明了,看起来更舒服,后续维护也更方便。


既然都是查询,为啥不兼容一下mysql的语法呢,像很多工具现在都是兼容mysql的语法,比如说hive,starrocks,flink等等,原因就是因为mysql的用户多,社区活跃。还有一个原因就是因为mysql的语法比较简单,容易理解。所以ElasticSearch 官方ElasticSearch 从 6.3.0 版本也开始支持 SQL 查询了,这就是一个喜大奔普的事情了,哈哈。



下面是官方的文档和介绍,大家可以看看 http://www.elastic.co/guide/en/el…


2.准备环境


大家在ES官网下载一下ES 启动就可以了,注意的是ES 需要JDK环境,然后就是需要在6.3.0以上的版本。 http://www.elastic.co/cn/download…



建议也下载一下kibana



我这边下载的是7.15.2版本


3.搞起


创建一个索引 my_index


PUT /my_index
{
  "mappings": {
    "properties": {
      "title": { "type""text" },
      "category": { "type""keyword" },
      "price": { "type""float" }
    }
  }
}

插入一些数据


POST /my_index/_doc/1
{
  "title""ES学习手册",
  "category""books",
  "price": 29.99
}

POST /my_index/_doc/2
{
  "title""on my way",
  "category""music",
  "price": 13.57
}

POST /my_index/_doc/3
{
  "title""Kibana中文笔记",
  "category""books",
  "price": 21.54
}

传统的查询所有


GET /my_index/_search
{
  
}

返回的是文档的格式


如果用sql 查询


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index"
}

返回的是类似数据库的表格形式,是不是写起来更舒服呢。



  1. 分页limit


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index limit 1"
}


和mysql 一样没啥,很简单。



  1. order by 排序


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index order by price desc"
}



  1. gr0up by 分组


POST /_sql?format=txt
{
  "query""SELECT category,count(1) FROM my_index group by category"
}



  1. SUM 求和


POST /_sql?format=txt
{
  "query""SELECT sum(price) FROM my_index"
}



  1. where


POST /_sql?format=txt
{
  "query": "SELECT * FROM my_index where price = '13.57'"
}


看看是不是支持时间的转换的处理,插入一些数据


POST /my_index/_doc/4
{
  "title""JAVA编程思想",
  "category""books",
  "price": 21.54,
  "create_date":"2023-11-18T12:00:00.123"
}

POST /my_index/_doc/5
{
  "title""Mysql操作手册",
  "category""books",
  "price": 21.54,
  "create_date":"2023-11-17T07:00:00.123"
}

时间转换为 yyyy-mm-dd 格式


POST /_sql?format=txt
{"query": "SELECT title, DATETIME_FORMAT(create_date, 'YYYY-MM-dd') date from my_index where category'books'" }


时间加减


POST /_sql?format=txt
{"query": "SELECT date_add('hour', 8,create_date) date from my_index where category'books'" }


字符串拆分


POST /_sql?format=txt
{
  "query""SELECT SUBSTRING(category, 1, 3) AS SubstringValue FROM my_index"
}


基本上mysql 能查的 es sql 也能查,以后查询ES 数据就很方便的,特别是对于做各种报表的查询。像这样。



一般对于这种报表,返回的数据都是差不多json数组的格式。而对于es sql,查询起来很方便


[
        {
            "data": "5",
            "axis": "总数"
        },
        {
            "data": "0",
            "axis": "待出库"
        },
        {
            "data": "0",
            "axis": "配送中"
        },
        {
            "data": "5",
            "axis": "已签收"
        },
        {
            "data": "0",
            "axis": "交易完成"
        },
        {
            "data": "0",
            "axis": "已取消"
        },
        {
            "data": "5",
            "axis": "销售"
        }

4.总结


ES SQL查询的优点还是很多的,值得学习。使用场景也很多



  1. 简单易学:ES SQL查询使用SQL语法,对于那些熟悉SQL语法的开发人员来说,学习ES SQL查询非常容易。

  2. 易于使用:ES SQL查询的语法简单,易于使用,尤其是对于那些不熟悉Query DSL语法的开发人员来说。

  3. 可读性强:ES SQL查询的语法结构清晰,易于阅读和理解。


5.最后附上相关链接


ES 官方下载

http://www.elastic.co/cn/download…


ES sql文档 http://www.elastic.co/guide/en/el…


作者:Yanyf765
来源:juejin.cn/post/7302308448581812258
收起阅读 »

独立开发月入1K的实践之路与可持续思考

梦想不止,CV不息。 在从事后端开发的这几年里,尝试过很多所谓的副业,企图在某一时刻实现财富自由,我相信这也是很多程序员的梦想。但程序员做出一款产品很容易,如果想要很多人用起来就可以说是痴人说梦了。第一就是不知道怎么推广,第二是抓不住客户的痛点,以程序员思维做...
继续阅读 »

梦想不止,CV不息。


在从事后端开发的这几年里,尝试过很多所谓的副业,企图在某一时刻实现财富自由,我相信这也是很多程序员的梦想。但程序员做出一款产品很容易,如果想要很多人用起来就可以说是痴人说梦了。第一就是不知道怎么推广,第二是抓不住客户的痛点,以程序员思维做出的产品,与市场格格不入。 中间试过公众号运营,做了个粉丝2W的号,但变现成了问题,就放弃了。也试过一些google chrome插件,抖音去水印小程序、小区信息发布,这些开发周期都在3天到1周,但苦于开发好之后没有人用,都一 一的放弃了。


很多次的试水,虽然做的东西不尽人意,但从另一方面来说,的确丰富了自己的技术栈。于是我开始思考,我可不可以用心的做一款产品,然后让人用起来,即使不盈利,如果自己的产品有人用,也算是很有成就感了。 


做什么? 在开发之前,我翻遍了论坛和博客,想要从中找到普通人的需求。中间发现很多帖子:独立开发者的三件套:todo、记账、笔记。很多人对此嗤之以鼻,但我想试一试todo。


先别喷,看看我的想法。


当你不知道做什么的时候,一定不要让自己闲着。todo的需求简单,开发周期极短,1周内可以完成上线,甚至还能更新几个迭代。 就这样,我翻了市面上的所有待办清单,对已有产品的待办逻辑进行了梳理,分析了差异点。结合我自己的办公需求,我决定做一个极简的todo,毕竟审美有限,功能多了肯定不好看。 独立开发不只要考虑开发,还要考虑开发后的运营。我开发出来之后,怎么能够快速投入到市场,让人用起来。


这时我发现了uTools,分析了一下uTools的用户量,用户特征,新插件下载使用率,以及审核发布机制,这些能够满足自己的期望值,就决定以uTools为依托,开发一款以element ui+springboot的待办应用,起名大气一点:超级待办!不管产品超不超级,先吸引人下载就好了。 


开发周期零零散散大概一周,就上线到uTools了。 



主要功能有任务的增删改查,可以快速拖拽,快速编辑、关联笔记、分类、待办的微信推送提醒。还注册了一个微信服务号,实现uTools超级待办与微信互通,随时查看自己的待办,目前通过该应用 公众号粉丝到400左右。




第一天下载量达到了50多,没有任何用户反馈。刚巧第二天,uTools发了新应用的推文,我的应用可能因为提交审核时间,排在推文的第一,下载量达到了300多,这时,各种用户反馈开始来了。用户量超出了自己的预期,于是我开始在反馈中做出功能的取舍,然后进行一版一版的迭代上线。 


从6.25上线到现在,一共迭代了55个版本,每个迭代用时大概30分钟-1个小时。 用户量满1000后,开始进行差异化付费模式,主打基础功能免费+个性化功能付费。从9月开始付费到现在,每月平均付费1K左右,并且在11月,我只迭代了3次,总用时20分钟。目前总下载量10596个,实际注册用户数4099个,日活114左右。 



后续规划: 


1、多思考,多动手,多充实技术栈。


2、尝试围绕几百条反馈,总结需求点,继续完善应用,开发收费点。 


3、尝试多做些其他类型的应用。 


一些思考: 


1、不要鄙视独立开发三件套,他是你入门独立开发的第一步,能够在一定程度上锻炼你的产品思维能力。 


2、开发之前需要考虑产品的全生命周期,包含时间成本、需求取舍、盈利预期,推广模式。


3、重视客户需求反馈,做出自己的判断,把反馈完美的融入到现有的产品中去。 


4、不要低估产品的可用性,即使你做个记事本,也是有人用的。 


5、苍蝇再小也是肉,苍蝇多了,肉也就多了。 


最后希望各位独立开发者盆满钵满,早日财富自由,躺赚斗金。 


有同学可能会关心这么小的服务,提供在线服务会不会连服务器成本都收不回来。其实有另一款盈利产品在支撑,目前每月4K左右,能够兼顾成本,该项目后续我再进行分享。


作者:探路
来源:juejin.cn/post/7303847314896175116
收起阅读 »

如何判断一个对象是否可以被回收

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。 首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以...
继续阅读 »

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。


首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以被回收呢?一般都是通过引用计数法,可达性算法。


引用计数法


对每个对象的引用进行计数,每当有一个地方引用它时计数器+1、引用失效(改为引用其他对象,赋值为null,或者生命周期结束)则-1,引用的计数放到对象头中,大于0的对象被认为是存活对象,一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。


public void f(){
Object a = new Object(); // 对象a引用计数为1
g(a);
// 退出g(a),对象b的生命周期结束,对象a引用计数为1
}// 退出f(), 对象a的生命周期结束,引用计数为0

public void g(Object a){
Object b = a; // 对象a引用计数为2
Object c = a; // 对象a引用计数为3
Object d = a; // 对象a引用计数为4
d = new Object(); // 对象a引用计数为3
c = null; // 对象a引用计数为2
}

引用计数法实现起来比较容易,但是存在一个严重的问题,那就是无法检测循环依赖。如下所示:


public class A{
public B b;
public A(){

}
}

public class A{
public A a;
public B(){

}
}

A a = new A(); // a的计数为1
B b = new B(); // b的计数为1
a.b = b; // b的计数为2
b.a = a; // a的计数为2
a = null; // a的计数为1
b = null; // b的计数为1

最终a,b的计数都为1,无法被识别为垃圾,所以无法被回收。


Python使用的就是引用计数算法,Python的垃圾回收机制,很大一部分是为了处理可能产生的循环引用,是对引用计数的补充。


虽然循环引用的问题可通过Recycler算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。


可达性算法


介绍


Java最终并没有采用引用计数算法,JVM的主流垃圾回收器采取的是可达性分析算法。


我们把对象之间的引用关系用数据结构中的有向图来表示。图中的顶点表示对象。如果对象A中的变量引用了对象B,那么,我们便在对象A对应的顶点和对象B对应的顶点之间画一条有向边。


在有向图中,有一组特殊的顶点,叫做GC Roots。哪些对象可以作为GC Roots呢?



  1. 系统加载的类:rt.jar。

  2. JNI handles。

  3. 线程运行栈上所有引用,包括方法参数,创建的局部变量等。

  4. 已启动未停止的java线程。

  5. 已加载类的静态变量。

  6. 用于同步的监控,调用了对象的wait()/notify()/notifyAll()。


JVM以GC Roots为起点,遍历(深度优先遍历或广度优先遍历)整个图,可以遍历到的对象为可达对象,也叫做存活对象,遍历不到的对象为不可达对象,也叫做死亡对象。死亡对象会被虚拟机当做垃圾回收。


JVM实际上采用的是三色算法来遍历整个图的,遍历走过的路径被称为reference chain。



  • Black: 对象可达,且对象的所有引用都已经扫描了(“扫描”在可以理解成遍历过了或加入了待遍历的队列)

  • Gray: 对象可达,但对象的引用还没有扫描过(因此 Gray 对象可理解成在搜索队列里的元素)

  • White: 不可达对象或还没有扫描过的对象



引用级别


遍历到的对象一定会存活吗?事实上,JVM会根据对象A对对象B的引用强不强烈作出相应的回收措施。


基于此JVM根据引用关系的强烈,将引用关系分为四个等级:强引用,软引用,弱引用,虚幻引用。


强引用


类似Object obj = new Object() 这类的引用都属于强引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象,只有在和GC Roots断绝关系时,才会被回收。


如果要对强引用进行垃圾回收,需要设置强引用对象为 null,或者让其超出对象的生命周期范围,则认为改对象不存在引用。类似obj = null;


参考代码:


public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

软引用


用于描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。可以使用SoftReference 类来实现软引用。


Object obj = new Object();
SoftReference<Object> softRef = new SoftReference(obj);

弱引用


也是用于描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。可以使用WeakReference 类来实现弱引用。


Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
obj = null;
System.gc();
TimeUnit.SECONDS.sleep(200);
System.out.println(weakReference.get());
System.out.println(weakReference.isEnqueued());

虚引用


它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置一个虚引用关联的唯一目的是能在这个对象被垃圾回收时收到一个系统通知。可以通过PhantomReference 来实现虚引用。


Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, refQueue);
System.out.println(phantomReference.get());
System.out.println(phantomReference.isEnqueued());

基于虚引用,有一个更加优雅的实现方式,那就是Java 9以后新加入的Cleaner,用来替代Object类的finalizer方法。


STW


虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。我们把运行应用程序的线程叫做用户线程,把执行垃圾回收的线程叫做垃圾回收线程,如果在执行垃圾回收线程的同时还在执行用户线程,那么对象的引用关系可能会在垃圾回收途中被用户线程修改,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)


误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存,导致程序出错。


为了解决漏报的问题,保证垃圾回收线程不会被用户线程打扰,最简单粗暴的方式就是在垃圾回收的过程中,暂停用户线程,直到垃圾回收结束,再恢复用户线程,这就是STW(STOP THE WORLD)。


但是如果STW的时间过程,就会严重影响程序的性能,因此优化垃圾回收过程,尽量减少STW的时间,是垃圾回收器努力优化的方向,


安全点


上述除了STW的响应时间的问题,还有另外一个问题,就是如何从一个正确的状态停止,再从这个状态正确恢复。Java虚拟机中的STW是通过安全点(safepoint)机制来实现的。当Java虚拟机收到STW请求,它便会等待所有的线程都到达安全点,才允许请求Stop-the-world的线程进行独占的工作。


当然,安全点的初始目的并不是让用户线程立刻停下,而是找到一个稳定的执行状态。在这个执行状态下,JVM的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析,才能找到完整GC Roots。


是不是所有的用户线程在垃圾回收的时候都要停止呢?实际上,JVM也做了优化,如果某个线程处于安全区(不会改变对象引用关系的一段连续的代码区间),那么这个线程不需要停止,可以和垃圾回收线程并行执行。一旦离开安全区,JVM会检查是否处于STW阶段,如果是,则需要阻塞该线程,等垃圾回收完再恢复。


作者:Shawn_Shawn
来源:juejin.cn/post/7304181581303447589
收起阅读 »

被约谈,两天走人,一些思考

五档尼卡引爆全场 前言 个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流 来龙去脉 上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎...
继续阅读 »

五档尼卡引爆全场



前言


个人身边发生的事,分享自己的一些思考,有不同意见是正常的,欢迎探讨交流


来龙去脉


上周坐我前面的前端开发工程师突然拿了张纸去找业务线领导签字了,领导坐我旁边,我看两人表情都认真严肃,一句话没说,那个前端同事签完字就坐自己工位上了,似乎有什么事发生


微信上问了一句:什么情况?


前端同事:裁员,做好准备



公司现状


从我去年入职公司后,就在喊降本增效了,周一晨会时不时也会提一下降本增效,毕竟大环境不好,公司的业务这两年也受到不小的影响


今年好几个项目组人手不够,两三月前还在疯狂面试前后端测试产品,我们这边的业务线前端都面试都超过五十个人了,最后招了一个前端一个后端一个测试


想着这种情况,公司薪资给的也不高,新员工不大量招就算降本了吧,再优化优化各方面流程等提提效率,没想到降本的大刀直接落下来首先砍掉的是技术开发人员


裁员情况


公司北京总部这边,目前我们部门是裁了两个前端一个后端,其他部门也有有裁员,人数岗位就不清楚了


从被裁掉的同事那边了解到的消息,上周三下班后下午找他们谈的,周四交接,周五下班后就走了,按照法律规定赔偿


上周只是一个开始,应该是边裁边看,什么时候结束最终裁员比例目前还不清楚,由其他来源得到的消息来源说是这次裁员力度很大


现在如果不是核心项目员工,如果不是和领导关系比较好的员工,每个人头上都悬着一把达摩克利斯之剑


个人思考


看待裁员


我认为首先是放平心态吧


国际经济形去全球化,贸易战,疫情,到现在的各种制裁,俄乌战争等,极端气候频发,真是多灾多难的年代


裁员这几年大家也见多了,该来的总会来


我认为裁员好说,正常赔偿就行,好聚好散,江湖再见


企业层面


裁员也是企业激发组织活力的一种方式,正常看待就行,关于企业组织活力介绍的,这里推荐一本前段时间刚读完的一本书 《熵减:华为活力之源》



熵是来源于物理科学热力学第二定律的概念,热力学第二定律又称熵增定律。熵增表现为功能减弱直到逐渐丧失,而熵减表现为功能增强...



个人层面


1.如果公司正常走法律流程,拿赔偿走人,继续找工作,找工作的过程也能发现自己的不错,更加了解市场,甚至倒逼自己成长


2.如果公司只想着降低成本,不做人事,有那种玩下三滥手段的公司,一定要留好证据,拍照,录音,截图,保存到自己的手机或者云盘里,不想给赔偿或恶意玩弄手段的,果断仲裁,我们员工相对企业来讲是弱势群体,这时候要学会用法律武器保护自己(可能也是唯一的武器)



这年头行情不好,老板损失的可能只是近期收益,有的员工失去的可能是全家活下去的希望



日常准备


做好记录


日常自己工作上的重大成果,最好定期梳理一下,或者定期更新简历,也可以不更新简历,找地方记录下来,例如项目上的某个重大模块的开发升级,或者做的技术上的性能优化等,我是有写笔记博客的习惯,技术相关的有时间一般会写成文章发到社区里


保持学习


日常保持学习的基本状态,这个可能我们每个人都会有这个想法,但是能定期沉下心来去学习提升,系统地去提升自己的时候,很少能坚持下来,万事开头难,开头了以后剩下的是坚持,我自己也是,有些事情经常三天打鱼,两天晒网,一起加油


关注公司


如果公司有查考勤,或者重点强调考勤了,一般都是有动作了,我们公司这次就是,年中会后的第二周吧,大部门通报考勤情况,里面迟到的还有排名,没多久就裁员了


保护自己


有的公司可能流程操作不规范,也有的可能不想赔偿或者少赔偿,可能会在考勤上做文章,例如迟到啥的,如果公司有效益不好的苗头,一定要注意自己这方面的考勤,以及自己的绩效等,做好加班考勤截图,领导HR与自己的谈话做好录音,录屏等,后面可能用的上,也可能会让自己多一点点谈判筹码


经营关系


虽然裁员明面上都是根据工作表现来的,好多时候大家表现都差不多,这个时候就看人缘了,和领导关系好的,一般都不是优先裁员对象,和领导团队成员打成一片真的很重要



以前我还有过那种想法:


我一个做技术的,我认真做好我自己的工作不就行了?专心研究技术,经过多年的工作发现,很多时候真的不行,我们不是做的那种科研类的,只有自己能搞,国内的大部分软件开发岗可能都是用的开源的技术做业务相关的,这种没什么技术难度,技术上来看基本没有什么替代性的难度


可能可替代性比较难的就是某个技术人长期负责的某个大模块,然后写了一堆屎山吧,毕竟拉屎容易,吃屎难


越是优秀的代码,可读性越强,简洁优雅,像诗一样



关于简历


如果是刚毕业的,可能简历上还好,大部分都优化都是已经是有一定的工作经验了,简历的更新就比较重要了,尤其工作了两三年了,如果简历看起来内容很少,不是那么丰富或者看起来很简陋,在简历筛选这一关会降低自己的面试几率,这时候一定要丰富一下,也有一些可能不知道自己简历是否丰富的,网上有那种简历模板可以搜搜看看,也可以找大佬帮忙看看,也有技术圈提供简历优化的有偿服务


再找工作


我个人的感觉是如果还是继续老本行继续打工,这年头行情不好,最好第一时间找工作,不能因为拿了赔偿就想着休一个月再说之类的,我周围有那种本来准备休半个月或一个月的,结果一下子休了一年以上的,我面试的时候如果遇到那种空窗期很长的,如果第一轮技术面能力都差不多的情况,到第二轮的领导面或者HR面,他们有优先考虑让空窗期短的人加入


关于空窗期


基本所有的公司都会关注离职空窗期,如果这个空窗期时间长了,那么求职的竞争力会越来越小,我在面试的时候我也会比较关注空窗期,因为我会有如下思考(举个例子,纯属乐子哈)


1.为什么这个人求职者三个月多了不找工作,家里有矿?家里有矿还上班,工作不会是找个地方打发时间的吧



我朋友的朋友就是这样,北京土著,家中独子,前几年拆迁了,家里好几套房,自己开俩车,人家上班就是找地方交个社保,顺便打发一下时间




2.能力不行吗?找工作这么久都没找到,是太菜了吗?还是太挑剔了?长时间不敲代码,手也生疏了,来我们团队行不行呀,我们这里赶项目压力这么大,招进来万一上手一段时间干不了怎么办,自己还被牵连了



几年前在某家公司做团队leader的时候,我们做的又是AI类项目,用的技术也比较前沿,当时AI的生态还不完善,国内做AI的大部分还处于摸索阶段,项目中用的相关技术栈也没个中文文档,由于公司创业公司,价格给的很低,高手招不进来,没办法只能画饼招综合感觉不错的那种,结果好几个人来了以后又是培训,又是有把手把手教的,结果干了没多久干不动走了,留下的烂摊子还得自己处理



关于社保


如果自己家里没矿,最好还是别让社保断了,拿北京举例,社保断了影响医疗报销,影响买车摇号等等


如果实在没找到工作,又马上要断缴社保了,可以找个第三方机构帮忙代缴,几千块钱,这时候的社保补缴相对来讲代价就比较高了



我遇到的情况是,社保断了一个月,后来找到工作了,第三方机构补缴都补不了,后来一通折腾总算弄补缴上了



关于入职


先拿offer,每一家公司的面试都认真对待,抱着一颗交流开放互相尊重的心


如果自己跳槽频繁,再找公司,可能需要考虑一下自己是否能够长待了,跳槽越频繁,后面找工作越困难,没有哪个公司希望招的人干一年就走了


所以面试结束后,最好根据需要面试情况,以及网上找到的资料,分析一下公司的业务模式了,分析这家公司的行业地位,加入的业务线或者部门是否赚钱,所在的团队在公司属于什么情况,分析团队是否是边缘部门,招聘的业务线是否核心业务线,如果不是核心业务线,可能过段时间效益不好还会被砍掉,有时候虽然看拿了对应的赔偿,但是再找工作,与其他同级选手对比的话,竞争力会越来越低


不论是技术面试官,还是负责面试的HR,大部分也都是公司的普通员工,他们可能不会为公司考虑,基本都会为自己考虑的,万一招了个瘟神到公司或者团队里,没多久自己团队也解散了怎么整



这里也许迷信了,基于我的一些经历来看有些人确实会有一些人是看风水,看人分析运势的


之前在创业公司的时候,有幸和一些投资人,上市公司的总裁,央企董事长等所谓的社会高层接触过,越是那些顶级圈里的人,有些人似乎很看中这个,他们有人研究周易,有人信仰佛教,有人招聘必须看人面相,有人师从南怀瑾等等



再次强调


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步


作者:草帽lufei
来源:juejin.cn/post/7264236820725366840
收起阅读 »

气死了😤,过五关,斩六将,结果被 HR 捅了一刀!!

大家有没有遇到过这样的事情:“过五关,斩六将。通过了两轮、甚至是三轮的技术面,最后 HR 面被“捅死”了” 这样的事情,最近在一位同学身上连续出现了两次,弄得人都麻了。 所以,我们昨天直接腾讯会议沟通了一个小时,分析这到底是因为什么问题导致的。 经过沟通之...
继续阅读 »

大家有没有遇到过这样的事情:“过五关,斩六将。通过了两轮、甚至是三轮的技术面,最后 HR 面被“捅死”了”


这样的事情,最近在一位同学身上连续出现了两次,弄得人都麻了。



所以,我们昨天直接腾讯会议沟通了一个小时,分析这到底是因为什么问题导致的。



经过沟通之后发现,这位同学在 HR 面中所存在的问题,是很多其他的同学也都存在的。所以这一篇文章,咱们就主要来说一说【HR 面的常见问题,以及应对方式】


01、HR 面出现的时机


HR 面根据公司不同,在整个面试流程中出现的位置也不相同。一般情况下会有两个不同的时机:



  1. 技术面试开始之前

  2. 技术面试结束之后


通常,一个完整的面试流程中,HR 只会出现一次。所以,以上两个时机是【二选一】的。


但是,在不同的时机之下,HR 面所代表的意义也会完全不同。


1:技术面试前进行 HR 面


把 HR 面试放到整个技术面试开始之前,通常的作用是:判断你的技术是否可以匹配团队业务、询问你的基本个人信息是否符合公司“价值观”。


这样的情况下,HR 通常会拿着技术部门预先给的八股文让你进行回答,只要你的回答不会偏离八股文太多,通常都会得到面试的机会。(同理:内容不要也不要偏离八股文太多,秀技术不要在这里秀,因为TA听不懂😂)


好处是,这样的公司 HR 权限一般有限,技术团队用人权限会更大。只要技术面试可以顺利通过,那么入职就问题不大。



如果你遇到的面试 HR 面是在初面,那么恭喜你!你会少遇到很多幺蛾子事。



2:技术面试后进行 HR 面


这样的 HR 面一般是作为终面进行的。这也就意味着:HR 有可能拥有一票否决权。


此时你就要小心了,一个不慎,可能就会前功尽弃。


所以,接下来咱们就来看看 HR 面的应对方式。


02、HR 面基础注意事项


无论你有多么迫切的期望得到这份工作,你都没有必要把自己的位置放的过低(过于高傲也是不行的)。


记住:面试本质上是一个双向的选择。


在 保持礼貌 的同时,以 平常心 看待当前的面试。反而会比过于 “谄媚” 或过于 “高傲” 要有效的多。


03、自我介绍


自我介绍是一个必答题。


那么我们在进行自我介绍时,应该 重点描述,匹配当前岗位的业务能力和技术能力。如果有 数据可以支撑的,尽量多说 数据不要 有太多的 主观 词汇。


同时,自我介绍的时长,尽量控制在 40 秒 - 60 秒 之内。


能力强的同学,可以根据面试公司的业务不同,把自己的履历在自我介绍中尽量的往面试公司的业务上去靠。


需要注意的是:HR 面和技术面的自我介绍是不一样的。因为 HR 听不懂太多的技术词汇,他们关心的点都是在“非技术”的问题上



面试时容易紧张的同学,可以事先把自我介绍写在纸上(200-270字即可),背下来去说,总比临阵说不出来要好。



04、你为什么从上家公司离职


一般 HR 都会问这样的问题,同时这个问题也并不好回答(因为真实的离职原因一般很难说)。



内心的真实离职原因:还能因为啥?老板太SB、给的钱太少、加班太多、饼太硬......



心里可以这么想,但是你嘴上不能这么说吧。


所以嘴上说的时候就要注意了,不要说在很多公司都会出现的问题。


可以从以下几点进行思考:



  1. 首先:千万不要暴露自己的问题。(记住:没有公司想要一个问题员工)

  2. 其次:不要贬低上家公司,即时它确实很差劲

  3. 最后:不要贬低同事或领导,因为这在别人听来会非常主观


可以从 公司搬家、自己搬家、公司裁员(现在裁员的多)、更高挑战 等, 现公司不太可能会出现的问题 上回答。


05、你对加班怎么看


我相信很多同学都是 不喜欢加班的,包括我在内。


但是,你 绝对不能 直接说 我不加班。除非你不想要这个 offer


网上有很多比较 “含蓄” 的说法,如:



任何公司,加班都是不可避免的。


如果工作实在有需要,我会愿意加班。


同时,我也会尽量提升工作效率,努力保证在规定的时间内,完成工作。



但是这种说法在实际面试中,可能并没有那么可靠。毕竟大家都不是傻子,话里话外什么意思,TA还能听不懂吗?


所以尽量不要耍这种小聪明(特别是急需这份工作的同学),可以直接说:“加班这种事情不可避免,之前的公司加班也挺多的,所以正常的加班还是没有问题的” 。


以这样的说辞,先拿到 offer 再说。


06、你凭什么认为你值这个钱?


这个话虽然不中听,但是在很多垃圾 HR 嘴里确实蹦出来过。


你也没必要生气,怼他一顿走了虽然很爽,但是这个 offer 同样也会飞掉了。先咽下这口气,等以后入职了再慢慢拾到TA。


这样的问题可以直接从 前面的技术面试 以及 过往履历的匹配度 上面去说。


例如:



之前和技术面试官的沟通整体还是非常愉快地。面试官那边对我的技术也非常认可,同时贵公司目前在做的业务和我过去的工作经验匹配度也比较高,所以说我和该岗位的整体匹配度还是很高的。


同时,我要的薪资也是符合目前市场行情价的,我之前的薪资是xxx,所以我并没有漫天要价



07、你的学校也太差了,还不是本专业。比你履历好的多的是,我们为什么要选择你?


和上一个问题类似,这种问题我是真的在 HR 面试中遇到过的。


这样的问题会很让人生气,同时也很难进行回答。


所以,当你遇到类似这种让人生气的问题时,先想清楚 你是想要一时爽还是想要这个 offer 。


如果想要这个 offer 的话,那么回答可以参考 【06、你凭什么认为你值这个钱?】回答的前半部分。


08、你认为你的缺点都有什么


一般 HR 问这个问题,都是从一些《HR的自我修养》这类书中看到的。但是不得不说,这样的问题并不好回答。


我要是直说我这人:好吃懒做,喜欢插科打屁 好像也不太合适。


所以说最好的方式是:列举出一些不太严重的问题。


这里列举出来几点,为大家提供参考:



  1. 不要说自己的大缺点:比如,“我很懒、我喜欢摸鱼打滑”。这种千万不要说。

  2. 不要把优点当成缺点说:比如,“我总是追求工作的完美,所以在一项工作上会花费更多事件”。毕竟大家都不是傻子。

  3. 不要说影响应聘当前岗位的缺点:比如,“应聘开发,说我总是很粗心,项目上线总是丢三落四”。


大家可以从一些,不影响当前应聘岗位的缺点说起,同时 强调自己已经认识到了这个问题,并且在改正


比如:



我之前特别喜欢熬夜,特别是休息日的时候。


这样并不好,所以我现在在有意识的,强迫自己早早休息。



09、还有拿到其他的 offer 吗?


HR 这么问其实是想要判断 给你发了 offer 之后,你能入职的概率有多少。 因为 HR 自己很清楚自己公司的成色。


但是,这样的问题如果直接说【没有其他 offer】也不好,因为这也有可能会被认为 “你没人要” 。


所以,最好的一个回答方式是:表现出来有 offer,但是对当前贵公司非常认可。


例如:



我当前有几个面试已经再走 offer 的流程了。但是在贵公司的整个面试过程,是我感觉比较专业的,也是体验比较好的。同时贵公司的业务也是我比较喜欢的,如果贵公司给我发 offer 的话,那么我会优先选择贵公司入职。



10、 你还有什么想要问我的吗?


这个问题一般是面试的结尾才会出现的。


这里建议大家,尽量抓住这个机会,多了解了解总是没错的。


但是要注意,尽量不要 问一些 “敏感” 的问题。


比如:



  • 公司忙吗?加班多不多?

  • 公司福利咋样,有下午茶吗?

  • 上班打卡吗?迟到扣钱吗?

  • 这只会 充分暴露,我们想养老的本心 😄 ,所以不要这么说。


如果你特别想要聊这些事情,那么可以在 收到 offer 之后,再去聊。


在收到 offer 之前,尽量聊一些工作之内的事情。


比如:



  • 当前岗位的明确工作内容

  • 公司对该岗位的要求

  • 是否会有新人培训

  • 团队人数与在公司的工作年限(如果团队有很多人在公司的工作年限比较长,那么可以侧面证明公司还不赖。)


总结


目前国内的很多 HR 面试,确实让整个面试的过程变得更加的复杂。


同时不得不说,很多中小企业的 HR 并不专业,很问出很多非常业余并且容易激发矛盾的问题(比如:第六题和第七题)。但是同时部分公司 HR 的权限又非常高,这就导致很多同学不得不陪TA们去玩这个愚蠢的游戏。


最后,老规矩:祝大家都可以拿到满意的 offer,高薪入职心仪的公司~~


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

工信部又出新规!爬坑指南

一、背景 工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。 二、整改 2.1 个...
继续阅读 »

一、背景


工信部最近发布了新的入网要求,明确了app进网检测要求的具体变化,主要涉及到一些app权限调用,个人信息保护,软件升级以及敏感行为。为了不影响app的正常运行,依据工信部的文件进行相关整改,下文将从5个方向来阐述具体的解决思路。


二、整改


2.1 个人信息保护


2.1.1 基本模式(无权限、无个人信息获取模式)


这次整改涉及到最大的一个点就是基本模式,基本模式指的是在用户选择隐私协议弹窗时,不能点击“不同意”即退出应用,而是需要给用户提供一个除联网功能外,无任何权限,无任何个人信息获取的模式且用户能正常使用。


这个说法有点抽象,我们来看下友商已经做好的案例。


腾讯视频



从腾讯视频的策略来看,用户第一次使用app,依旧会弹出一个“用户隐私协议”弹窗供用户选择,但是和以往不同的是,“不同意”按钮替换为了“不同意并进入基本功能模式”,用户点击“不同意并进入基本功能模式”则进入到一个简洁版的页面,只提供一些基本功能,当用户点击“进入全功能模式”,则再次弹出隐私协议弹窗。当杀死进程后,再次进入则直接进入基本模式。


网易云音乐



网易云音乐和腾讯视频的产品策略略有不同,在用户在一级授权弹窗点击“不同意”,会跳转至二级授权弹窗,当用户在二级弹窗上点击“不同意,进入基本功能模式”,才会进入基本功能页面,在此页面上点击“进入完整功能模式”后就又回到了二级授权页。当用户杀死进程,重新进入app时,还是会回到一级授权页。


网易云音乐比腾讯视频多了一个弹窗,也只是为了提升用户进入完整模式的概率,并不涉及到新规。


另外,B站、酷狗音乐等都已经接入了基本模式,有兴趣的伙伴可以自行下载体验。


2.1.2 隐私政策内容


如果app存在读取并传送用户个人信息的行为,需要检查其是否具备用户个人信息收集、使用规则,并明确告知读取和传送个人信息的目的、方式和范围。


判断权限是否有读取、修改、传送行为,如果有,需要在隐私协议中明文告知。


举个例子,app有获取手机号码并且保存在服务器,需要在协议中明确声明:读取并传送用户手机号码。


2.2 app权限调用


2.2.1 应用内权限调用



  1. 获取定位信息和生物特征识别信息


在获取定位信息以及生物特征识别信息时需要在调用权限前,单独向用户明示调用权限的目的,不能用系统权限弹窗替代。



如上图,申请位置权限,需要在申请之前,弹出弹窗供用户选择,用户同意调用后才可以申请位置权限。



  1. 其他权限


其他权限如上面隐私政策一样,需要在调用时,声明是读取、修改、还是传送行为,如下图



2.3 应用软件升级


2.3.1 更新


应用软件或插件更新应在用户授权的情况下进行,不能直接更新,另外要明确告知用户此行为包含下载和安装。


简单来说,就是在app进行更新操作时,需要用弹窗告知用户,是否更新应用,更新的确认权交给用户,并且弹窗上需要声明此次更新有下载和安装两个操作。如下图



2.4 应用签名


需要保证签名的真实有效性。


作者:付十一
来源:juejin.cn/post/7253610755126476857
收起阅读 »

运气大于努力???

标题前言   我想看见这个标题,可能要是近两年,换工作或者刚毕业的小伙伴们最身有体感吧。我先从我自身出发,来讲一下这个我理解,也可能是大家遇见的最多的。 须知:   我是7月25号晚上被辞退,什么原因我上篇文文章已经写过了,就不细细描述了。本人男,25岁,专科...
继续阅读 »

标题前言


  我想看见这个标题,可能要是近两年,换工作或者刚毕业的小伙伴们最身有体感吧。我先从我自身出发,来讲一下这个我理解,也可能是大家遇见的最多的。


须知:


  我是7月25号晚上被辞退,什么原因我上篇文文章已经写过了,就不细细描述了。本人男,25岁,专科,工作经验三年。个人能力不出众,没做过什么可以拿出来吹嘘的东西。就这样平平无奇的开始了,在找工作的路上征战,征战地(上海~杭州),目前是已经入职2个多月了,今天也是有时间上来谈下感受,当然也谢谢上篇文章下各位小伙伴以及各位前辈的鼓励以及帮助


进入正题


  刚刚被辞的时候,我是调整了半个月,准备了一个星期*(一定要自己准备好,别浪费机会,现在的机会很可贵,花点时间没关系的),当时我想的是要走两条路,一个是产品助理,一个就是接着前端了。我其实已经在思考变局这件事。所以也算是一次尝试。虽然尝试的结果并不好,没有什么机会




  回归正题,我当时还是首选的上海,在上海找工作,也面试了几家。大概我总结一下啊,8月份的行情可以说是,一塌糊涂。首先我个人比较跨,特别大的公司,因为我学历的问题,基本是约不到面试的。我就讲我面的几家公司,总体是体量100多人的公司双休很少,大部分是大小周,100以下可能就是双休,但是你基本上要独立负责了。(当然我是没面过外包的,外包我个人觉得是可以的,但是我个人不怎么喜欢外包的氛围。如果给的多的话,无脑冲,哈哈)。后来因为个人感情问题也是决定去杭州,当时也是网上也是各种传递说,专科真别去杭州,一点机会没有,去了也是白去。但是我也是对感情一腔热血啊,直接就冲了。发现没有想象的那么差,机会也是有的。也大大小小的面了不少的公司。就突然觉得运气大于努力了!为什么这么说,因为无论是我在上海或者是杭州,面试的时候都会有觉得不错,而且回答的很好的时候。但是往往这些就是说,回去等通知,因为我这边还有约的。到时候通知你,当然也可能遇见过。这时候你自己回家的时候就会有幻想,觉得嗯,可以,应该没啥问题。可是这些往往没了后续。也遇见过说,技术面过以后,说你要的薪资我们肯定给不到的,你能接受多少多少吗?这时候你看下招聘信息,你就挠头,心里想着:“啊,不是,招聘信息上不是有吗”。为啥又给不到,那你就要说的低一点,试试能不能行。然后照常等通知。再遇见,去面试,花了很久过去,然后人事说 技术开会,人事说,技术出差,人事说,技术这会在忙。然后让你等一会,等一会人事就会告诉你,今天不行了,等晚上技术给你电话面试。然后你就点头说,好好好,那晚上几点呢?8点钟吧。ok这时候你已经得到了有用的信息,准备回家了。开始回家准备,7点半就已经严阵以待,8点钟没有电话,8点半没有,9点了你开始想用招聘信息联系一下,然后得到的信息是,技术忙忘了,明天吧,,我只想说别等了,等不到的。再有的,在面试过程中,面试官上来用他自己很明白很清楚的话术问你问题的,你知道,但是你听不懂他先问啥的。这时候你就怀疑自己,我是不是不会,没遇见过。不妨你再问一边,你说,你能把这个问题举个例子吗?可能这时候你就会豁然开朗,原来他问的是这个。我个人遇见get 跟git 一个发音的,导致他觉得我没用过。我为什么会知道,因为他问的问题我基本没答上来。最后他让我问他有什么问题的时候,我才给他说,我说你想问什么呢?你问的我咋都没听过,然后他就开始给我解答,他问的问题是什么,我知道以后我都裂开了。你这问的跟你刚刚问的有关系吗?害,当时觉得自己小丑。当然结果肯定不过。那时候就陷入了一个很奇怪的误区,我每天那么努力,去背,去准备。结果完全用不上。




  看了看那些找工作的群,很多人不如你,却都拿着比你高的工资,嘴里说着不知道为啥就过了,也没问啥问题,就过了,听见后。心理一阵恍惚。觉得自己运气真差啊。真不争气啊。不知所措,迷茫冲上心头,就是不知道差哪了,虽然不入那些真正的大佬牛皮,可是也比刚出来的培训生好吧(这里解释一下,没有其他意思,个人帮很多培训的看简历,以及回答问题,如有冒犯,本人现在就很对不起,可私聊喷我,当场对不起,评论区还是正向一点),为啥事事不如意。然后就那种失望的情绪会笼罩你,会让你觉得,努力不过如此。




  努力,百度百科里是这么解释的:努力指用尽力气去做事情,后来指一种做事情的积极态度,当我们失去积极的态度的时候我们其实压根就没努力。那运气,百度百科里是怎么写的呢,某种事件发生的概率微小、随机性强、无法计算且不可控制的情况下,事件结果产生后恰好与某人的猜想或个人情况决定一致,并且在现实中发生一般为不可思议或完全不可能存在的背景下发生的事件。这句话不做任何解答,自己多读两遍,然后再去认知你的努力。


结束啦


  其实我写的东西很简单,又很生活,讲不出来大道理,聊不出人生观。只有生活,也只是我自己的生活。你们的生活嘛,当然是要 努力 .那运气呢?你们肯定都是有的,如果自己最近运气不好也没有关系。去走一走,转一转,可能你的运气,就会来了。各位也可以评论一下,各位对运气还有努力的看法,评论我会认真回。这篇文章如果有冒犯到的地方,也请见谅。


作者:想努力的菜菜
来源:juejin.cn/post/7295328074601218060
收起阅读 »

聊聊Android中的DRM工具-Widevine

曾几何时,我一直好奇,像爱奇艺、腾讯视频、优酷这些视频平台是如何控制版权的,就比如,如何防止用户下载后发布到其他渠道,最近接触了DRM技术,瞬间就懂了。 DRM介绍 DRM(Digital Rights Management),即数字版权管理,是在数字内容交易...
继续阅读 »

曾几何时,我一直好奇,像爱奇艺、腾讯视频、优酷这些视频平台是如何控制版权的,就比如,如何防止用户下载后发布到其他渠道,最近接触了DRM技术,瞬间就懂了。


DRM介绍


DRM(Digital Rights Management),即数字版权管理,是在数字内容交易过程中,对知识产权进行保护的技术、工具和处理过程。它的目的是防止数字内容被未经授权的用户复制、修改和分发,以保护知识产权所有者的权益。在日常生活中,我们经常与 DRM 技术打交道。比如,电影上映前,我们不能在视频网站上观看电影,只能去电影院。这是内容提供(发行)商对自己的数字内容进行管理的一种结果。


DRM工作原理


先贴一张图,然后我们再做简单的说明drm工作原理


上图中,RAM想要给SHYAM传递小纸条,但因为距离较远,中间需要三个人进行传达,为了防止这三个人偷看小纸条内容,他们找来了HARI,HARI手上有一本密码本,每次RAM传递小纸条之前先找HARI拿到密码本,然后根据密码本的规则对小纸条内容进行加密,然后再将加密后的小纸条传递给SHYAM,这样,即使中间三个人偷看了小纸条,因为没有密码本,所以也看不懂纸条的内容。SHYAM收到小纸条后,再向HARI获取密码本,然后对小纸条内容进行解密,这样SHYAM就能看到原始内容了。


现在,我们把RAM看成是视频发行商,SHYAM看成是观众,HARI看成是版权管理商,就有了以下这种关系图


drm工作原理2


从上图中可以看出,我们想要向认证用户安全地发送一部电影。需要:



  • 向DRM厂商的服务器请求密码本

  • 然后使用密码本加密视频

  • 将电影视频发送给用户

  • 用户向DRM厂商的服务器请求密码本解密视频

  • 现在用户就可以观看电影了


这下视频版权管理是不是就一目了然了。但以上只是最初DRM的设计思想,现实中却无法正常运行,因为还没有解决多种分辨率的问题,这就需要对视频进行切片(ABR)和打包。


视频切片和打包


ABR: 通过使用ABR技术,电影可以被编码成不同的码率-分辨率组合(也称为码率阶梯)并被分割成小的视频块或者切片。每个视频切片包含几秒钟视频,可以被单独解码。


打包是指将电影分割成小的视频切片,并使用清单(manifest)或者播放列表对其进行描述。当用户想要播放电影的时候,他需要按照播放列表的信息播放。


根据可用带宽,播放器请求特定码率版本的视频切片,CDN响应后返回被请求切片。


drm工作原理3


这就结束了吗?不,这里面还存在很大的一个问题需要解决,视频的加密问题。


视频加密


前面说,视频发行商在发布视频时,需要向DRM服务商获取密码本,这里的密码本实际上是一种授权,就是说经过DRM服务商的授权,他才会对你的视频进行版权保护,并不是对视频内容进行加密,真正的视频加密还得涉及到密码学相关的技术,最常用的加密方式是AES,AES属于对称加密,这就涉及到密钥的保存。在DRM中,密钥也保存在DRM服务商手上,随着视频清单一起发送给视频播放器


drm工作原理4


好了,DRM的核心原理大概就是这些,如果想了解更详细的内容,可阅读下面的参考文献。


DRM厂商


上述DRM工作原理图中,有一个很重要的角色就是DRM服务商,目前主要有三大服务商,分别对应自己的DRM技术方案,分别是:




  • Apple FairPlay




  • Google Widevine




  • Microsoft PlayReady




国内爱奇艺最近也自主研发了自己的DRM解决方案:iQIYI DRM-S。而国内的视频平台几乎都是打包了所有的的DRM方案,以针对不同的平台和系统。以下是爱奇艺的整体DRM解决方案


爱奇艺drm方案


Widevine介绍


Widevine仅适用于基于Chromium的操作系统、Android设备以及其他Google相关设备和浏览器。


Widevine的安全级别



  • L1


在L1级别,提供了最高的安全性。内容在设备内进行解密,并使用硬件保护,以防止原始数据泄露。通常用于高质量视频和高分辨率的流媒体。获得L1认证的设备可以播放高质量的内容。像Amazon Prime Video和Netflix等流媒体服务需要L1安全性。如果在未获得认证的设备上观看,无法播放高清或超高清的高质量内容。



  • L2


L2具有较高的安全性,但不像L1那么严格。即使设备未获得L1认证,仍然可以播放内容。一些设备使用软件来保护数据。对于较低分辨率的视频和音乐内容,可能会使用L2。如果想要享受更高质量的内容,建议使用获得L1认证的设备,而不是L2。虽然L2可能不够满足要求,但某些内容仍然可能提供高质量的视频。因此,不能一概而论地认为必须使用L1。



  • L3


L3的安全级别最低。主要用于模拟器和一些旧设备等情况,内容保护相对较弱,分析和复制相对容易。此外,一些服务如Amazon Prime Video和Netflix也可能使用L3。虽然可以使用L3,但风险较高,不应期望高质量的内容。使用L3时需要谨慎考虑这些因素。


查看Widevine级别


可以使用DRM Info App查看设备的widevine安全级别,该App可以在Google Play上找到,文末贴了App的下载链接。大多数主流制造商的智能手机通常都支持L1至L3的某一个级别。如果发现您的设备不支持Widevine,那可能是制造商为了简化流程或者您的智能手机不符合标准。


image-20231121164459796


如果app打开闪退,说明设备并不支持Widevine。


测试Widevine功能


许多流媒体app都使用了Widevine,比如Youku、腾讯视频、IQIYI、YouTube、Netflix等,这里推荐使用Google的官方播放器ExoPlayer进行测试,文末提供下载链接


image-20231121165120972


(重点)在Android中集成Widevine


step1:获取Widevine源码


官网下载Widevine源码,注意,AOSP默认是没有Widevine源码的,需要手动集成,因为需要跟Google签订法律协议,然后由Google授权访问Widevine代码库,具体见Google官网流程。


step2:将源码放置到vendor目录下vendor/widevine/


image-20231121170544865


step3:添加编译配置


device/qcom/{product combo name}/BoardConfig.mk中添加


#这里设置的L3级别,L1级别需要跟Google签订协议,获取Keybox
BOARD_WIDEVINE_OEMCRYPTO_LEVEL := 3

device/qcom/{product combo name}/{product combo name}.mk中添加


PRODUCT_PROPERTY_OVERRIDES += drm.service.enabled=true
PRODUCT_PACKAGES += com.google.widevine.software.drm.xml \
com.google.widevine.software.drm
PRODUCT_PACKAGES += libwvdrmengine

vendor/qcom/proprietary/common/config/device-vendor.mk中修改


SECUREMSM += InstallKeybox
#L3级别需要删除oemcrypto库
#SECUREMSM += liboemcrypto
#SECUREMSM += liboemcrypto.a
SECUREMSM += libhdcpsrm

最后编译刷机,使用app工具验证即可,如果能显示Widevine级别,说明集成成功。


总结


好了,现在你应该彻底知道Widevine是怎么回事了


参考链接


中学生也能看懂的DRM


构建DRM系统的重要基石——EME、CDM、AES、CENC和密钥


爱奇艺DRM修炼之路


什么是Widevine?Widevine DRM详解


Google Widevine


Widevine安全级别查看app:


链接:pan.baidu.com/s/1lIJq-_eg…
提取码:fnk6


ExoPlayer:


链接:pan.baidu.com/s/1dUseWHIi…
提取码:nszh


作者:小迪vs同学
来源:juejin.cn/post/7303723984180101139
收起阅读 »