注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

技术管理必备技能之管理好系统性风险

我们在平常工作中经常会听到有人说系统性风险,但系统性风险到底是个啥? 1 系统性风险是什么 1.1 定义 「系统性风险」是一个经济术语,主要指的是一种可能导致整个金融系统或市场瘫痪的风险或概率。它是从系统性风险的整体性出发,而不是单一机构或者单一行业的危机。这...
继续阅读 »

我们在平常工作中经常会听到有人说系统性风险,但系统性风险到底是个啥?


1 系统性风险是什么


1.1 定义


「系统性风险」是一个经济术语,主要指的是一种可能导致整个金融系统或市场瘫痪的风险或概率。它是从系统性风险的整体性出发,而不是单一机构或者单一行业的危机。这通常是由于金融体系中一个重要组成部分的失败,例如一个大银行或一系列银行的破产,这可能引发一种连锁反应,影响整个系统。


当突发性事件导致金融机构或市场失灵时,资金无法在市场中有效输送和配置,从而引起整个市场的崩溃。系统性风险不仅仅是经济价值损失,还会对实体经济造成严重影响,并导致大部分金融体系的信心丧失。


如 2008 年的全球金融危机。在这个危机中,许多大型金融机构由于负债过重和资产质量下降而陷入困境,这引发了对全球金融系统稳定性的广泛担忧。


系统性风险是监管机构、政策制定者和经济学家关注的主要问题,因为如果这种风险实现,可能会导致重大的经济损失和社会动荡。因此,他们会尝试制定和执行各种政策和法规,以减少系统性风险的可能性。


1.2 系统性风险和非系统性风险的差别


系统性风险作为一种具有更大影响面的风险,和非系统性风险有以下几个方面的区别:


1. 影响范围:系统性风险具有广泛的影响范围,不仅仅局限于特定个体或组织,而是可能波及整个系统、市场或行业。非系统性风险则相对较局部化,通常只对特定个体、组织或项目产生影响。


2. 相互关联性:系统性风险与系统中各个组成部分相互关联,其中一个部分的风险可能会传播、扩大或影响其他部分。非系统性风险通常是单个因素或事件的结果,并不涉及系统的相互依赖关系。


3. 复杂性和不确定性:系统性风险往往更加复杂和不确定,因为它们涉及到多个变量、因素和相互作用。非系统性风险可能更加可控和可预测,因为它们通常涉及特定事件或条件。


4. 长期影响:系统性风险可能具有长期影响,并可能导致持续的连锁反应和不良后果。非系统性风险通常具有较短期的影响,并且其影响通常更容易限定和控制。


5. 解决方法:由于系统性风险的复杂性和广泛影响,解决它们通常需要跨部门、跨组织或跨行业的合作和综合性措施。非系统性风险通常可以通过特定个体或组织的行动来解决。


系统性风险与非系统性风险在影响范围、相互关联性、复杂性和不确定性、长期影响以及解决方法等方面存在明显的区别。


2 技术上的系统性风险


类比经济上的系统性风险,对于一家企业的技术负责人来说,技术上的系统性风险也是一个需要重点关注的点。


2.1 定义


在技术上,系统性风险指的是一个技术系统或者一个技术生态系统中,某个关键组件或者某些关键组件出现故障、漏洞、安全问题等,导致整个系统或者生态系统无法正常运行,进而引发连锁反应和影响。


例如,在云计算生态系统中,某个云服务提供商的故障可能会影响到众多企业和用户的业务运营;在物联网领域,某个智能设备的漏洞可能会导致整个物联网网络遭受攻击和瘫痪。因此,在技术领域中,识别和防范系统性风险也是非常重要的。


2.2 系统性风险和非系统性风险的不同


和经济上的系统性风险一样,技术上的系统性风险和非系统性风险也有 5 个不同点:


1. 影响范围和规模:系统性风险通常具有广泛的影响范围和规模,涉及整个技术系统或架构。它可能涉及多个组件、子系统或关键基础设施,甚至可能跨越多个应用程序或服务。非系统性风险更倾向于局部范围,通常仅影响特定的组件、功能或子系统。


2. 相互关联和依赖:系统性风险涉及到技术系统中各个组件和环节之间的相互关联和依赖关系。它们可能因为一个组件或环节的故障或问题而影响其他组件或环节的正常运行。非系统性风险更倾向于独立存在,其影响相对较为局限,不会对其他组件或环节造成波及效应。


3. 复杂性和不确定性:系统性风险通常更加复杂和不确定,因为它们涉及到多个技术组件、系统交互、数据流和相关的外部因素。这使得系统性风险的评估、预测和解决变得更加困难。非系统性风险通常更容易辨识、评估和控制,因为其范围和影响相对较小。


4. 长期影响和连锁反应:系统性风险可能导致长期的影响和连锁反应,其中一个问题可能触发多个级联故障或影响多个关键业务流程。非系统性风险的影响通常更为短期和局限,不会引起大规模的系统级问题。


5. 解决方法和复杂度:由于系统性风险的复杂性和广泛影响,解决它们通常需要跨部门、跨团队的协作,涉及多个技术专长和领域的知识。这可能需要综合性的技术改进、架构调整或系统重构。非系统性风险通常可以通过单个组件或功能的修复或改进来解决,其处理相对较为简单和局部化。


3 系统性风险的传播


在技术系统中,系统性风险通过多种方式传播,包括以下几种:




  • 级联传播:级联传播是指一个组件的故障导致其他相关组件的故障,从而在整个系统中形成一种连锁反应。这种传播方式可能导致整个系统的瘫痪,影响业务的正常运行。例如,在一个分布式计算环境中,如果某个关键任务执行节点发生故障,可能导致其他依赖于该节点的任务无法正常执行,从而引发其他节点的过载或故障。这种风险传播会在整个分布式系统内形成级联效应,可能导致整个系统瘫痪。




  • 传染传播:传染传播是指一个系统的风险通过某种途径传播给其他系统,从而导致多个系统受到相同类型风险的影响。例如,WannaCry 勒索病毒,它通过网络传播,利用 Windows 系统的一个漏洞进行攻击。当某个系统被感染后,病毒会自动搜索其他具有相同漏洞的系统,并尝试感染它们。这种风险传播方式导致了全球范围内大量系统受到勒索病毒的影响。




  • 共同暴露:共同暴露是指多个系统由于共享相同的风险因素,而同时受到该风险因素的影响。例如,多个在线服务都依赖于一个第三方身份验证服务。如果这个第三方身份验证服务出现故障或者安全漏洞,那么所有依赖它的在线服务都将面临安全风险或者无法正常运行,因为它们共同暴露在同一个风险因素下。




  • 放大效应:放大效应是指一个较小的初始风险经过多次传播和叠加,最终导致整个系统面临较大的风险。例如,在社交网络中,一个虚假信息可能经过多次转发和传播,形成恶性舆论,对整个社会产生较大的负面影响。




在技术系统中,了解这些传播方式和机制对于有效管理技术风险至关重要。


4 系统性风险的来源


系统性风险的由来可以追溯到技术系统的复杂性和相互依赖性。当一个技术系统由多个组件、流程和环节组成时,它们之间存在着相互依赖和相互作用。这种相互依赖性使得一个组件或环节的故障或问题可能会影响整个系统的运行和稳定性。


以下是一些常见的系统性风险的来源:




  • 复杂性和交互作用:技术系统的复杂性和各组件之间的交互作用可能导致系统性风险的出现。当系统变得越来越复杂,组件之间的相互依赖性增加,可能出现不可预见的问题和故障。例如,一个庞大的分布式系统可能由多个模块和子系统组成,彼此之间的相互作用可能导致系统范围的故障,如性能下降或数据不一致。




  • 外部环境因素:外部环境因素也是技术系统性风险的重要来源。例如,技术系统可能受到恶劣天气、自然灾害(如山洪地震等导致光纤断了)、供应链中断或恶意攻击等外部因素的影响。这些因素可能导致系统中断、数据丢失、安全漏洞暴露等问题。例如,一家电子商务平台可能受到网络攻击,导致用户信息泄露或交易中断。




  • 人为错误和疏忽:技术系统性风险也可能源自人为错误和疏忽。人员的操作失误、编码错误、配置错误或安全意识薄弱等问题都可能导致系统故障或数据泄露。例如,一个开发人员可能在代码中引入漏洞,导致系统容易受到攻击。




  • 技术演进和更新:技术的演进和系统的更新也可能引入系统性风险。当引入新的技术、框架或库时,可能存在兼容性问题或未知的缺陷。例如,将系统从一个版本升级到另一个版本时,可能出现功能不兼容、新增的安全漏洞或数据不一致的问题等。




  • 依赖供应商和第三方:技术系统通常会依赖外部供应商或第三方服务。这种依赖性可能带来风险。例如,如果一个关键供应商无法按时提供所需的硬件设备,可能导致项目延期或无法正常运作。另外,如果一个 CDN 第三方服务提供商的服务出现故障,可能会影响到技术系统的正常运行。




以上是一些常见的技术系统性风险的来源示例。在技术管理中,了解和识别这些来源是非常重要的,以便采取相应的措施来减轻和管理系统性风险的影响。


5 管理好系统性风险的意义


聊了这么多术语类的东西,看一下对于一个技术管理者来说,管理好系统性风险到底有什么用,有什么收益。这里我们从技术管理和技术团队,以及业务的角度来看。


5.1 技术管理上的意义


从技术管理和技术团队的角度来看,管理好技术上的系统性风险具有以下意义:


1. 保障系统的稳定性和可靠性:系统性风险管理可以帮助确保技术系统的稳定性和可靠性,减少系统故障和服务中断的可能性。这有助于降低业务中断的风险,提高技术系统的可用性和持续性,保障业务的正常运行。


2. 提高技术投资的回报率:有效管理系统性风险可以降低技术投资的风险并提高回报率。通过规避潜在的系统性风险,可以减少因系统故障或不稳定性而造成的额外成本和资源浪费,提高技术投资的效益和投资回报。


3. 增强技术管理者决策能力:系统性风险管理使技术管理者能够更全面地了解和评估技术系统的风险情况。这有助于他们做出明智的决策,选择合适的措施来降低风险,并确定优先级,以使资源和精力能够最大程度地应对最重要的风险。


4. 提高团队效率:通过管理系统性风险,技术管理者可以减少系统故障和问题的发生,从而减少紧急修复和事后处理的工作量。这使团队能够更加专注于战略性的工作,提高工作效率和生产力。


5. 增加业务可信度:有效管理系统性风险可以提高技术系统的可靠性和稳定性,增加业务的可信度。这有助于提高内部和外部利益相关者对技术部门的信任,加强与其他部门的合作和协调,为企业的可持续发展和成长奠定基础。


6. 促进技术创新和发展:管理好系统性风险有助于为技术管理者提供稳定的技术基础,支持技术创新和发展。他们可以更好地专注于推动新技术的应用、优化现有技术架构和流程,为业务增长提供技术支持和竞争优势。


5.2 业务价值上的意义


从业务价值的角度来看,管理好技术上的系统性风险具有以下意义:


1. 提高效率和生产力:通过管理系统性风险,技术系统可以更加稳定和可靠地运行,减少系统故障和问题的发生,从而减少因为系统问题导致的客诉、修复、沟通等成本。这有助于提高业务的效率和生产力,节省时间和资源,并降低运营成本。


2. 支持业务增长和扩展:有效的系统性风险管理可以为业务提供可靠的技术基础,支持业务的增长和扩展。通过降低系统故障和数据泄露的风险,技术管理者可以为业务提供稳定的平台,支持业务的创新、市场拓展和新产品的推出。


3. 支持业务创新和竞争优势:系统性风险管理为技术团队提供稳定的技术基础,支持业务的创新和发展。通过降低系统性风险,技术团队能够更好地专注于业务创新、新产品开发和市场敏捷性,从而获得竞争优势。


4. 提升用户体验和满意度:系统性风险管理有助于提供稳定、安全和高性能的技术系统,提升用户体验和满意度。用户倾向于选择那些能够提供稳定服务、快速响应和数据安全的产品或服务,有效的系统性风险管理可以增强用户对技术产品或服务的信任和满意度。


5. 降低损失和风险:有效的系统性风险管理有助于降低业务面临的潜在损失和风险。通过识别和管理系统中的风险,可以减少数据泄露、安全漏洞和技术故障所带来的损失,并降低法律诉讼和声誉损害的风险。


6. 提升客户信任和忠诚度:通过管理系统性风险,技术管理者可以建立客户信任和忠诚度。稳定、安全和可靠的技术系统能够增强客户对企业的信心,提高客户满意度和保持客户的长期合作关系。


可以看到如果能管理好系统性的风险,对于技术组织,对于技术管理者,对于业务和业务价值来说,都是一件非常好的事情。从生产效率的提升,到业务稳定性,到对成本的减少以及客户成功都是极好的。


那么如何管理系统性风险呢?


6 如何管理系统性风险


6.1 风险模型


风险模型是风险管理的第一步:理解系统中已有的风险,识别、标记并对已知的风险排列优先级,最终形成一张包含了系统所有已知风险的当前状态的表格。这就是我们所说的风险模型。


建立风险模型的过程是识别风险的过程,在这个过程中我们需要识别出系统中已有的风险,并对其进行分析,标记出优先级、梳理当前状态和历史情况。


风险模型构建过程中需要考虑模型的作用范围,是公司级的,团队级的,项目组的,还是服务级的。


对于一个小公司,可以是公司级的,对于大型一些的公司,可以考虑团队或项目级的。


风险模型至少包括以下一些方面:



  • 严重性/可能性:高中低,先评估严重性,再评估可能性

  • 风险缓和计划:可以使用的或者正在使用的用来降低该风险严重性或者可能性的风险缓和措施。

  • 监控:对该风险的发生是否进行了监控,如果监控了说明监控的指标,如果没有监控,说明原因,以及达成监控目标的原因,最终所有的风险应该是要监控起来的。

  • 状态:活跃 / 已缓和 / 正在修复 / 已解决

  • 历史风险情况:该风险在历史上有没有发生过,什么时候,发生频率等

  • 风险缓和计划:当我们制定风险缓和计划的时候,需要从严重性最高的项开始,缓和风险不是为了消除,而是为了降低风险的严重性和可能性。并不是每一个风险都要制订风险缓和计划。

  • 风险预案:当风险发生的时候,我们可以采取的措施


除此之外,还包括一些常规的添加时间,ID,负责人之类的


6.2 识别和评估系统性风险


识别系统性风险是一个关键的步骤,它需要深入分析和理解组织或项目所面临的技术环境和相关因素。以下是一些常见的技术上的系统性风险示例:




  • 依赖单点故障:系统中存在关键组件、设备或服务的单点故障,一旦出现故障,将导致整个系统或业务的中断。例如,网络设备的故障、云服务提供商的服务中断等。




  • 服务间的强弱依赖:如果系统中的服务之间存在强依赖关系,一旦其中一个服务发生故障或不可用,可能会导致整个系统的故障或性能下降。




  • 内部和外部/离线和在线业务的相互影响:系统中的离线和在线业务之间存在相互依赖关系,如果其中一个业务出现问题,可能会影响其他业务的正常运行。




  • 安全漏洞和数据泄露:系统存在安全漏洞或不当的安全措施,可能导致黑客攻击、数据泄露或信息安全问题。这可能对组织的声誉、客户信任和合规性产生严重影响。




  • 技术过时和不可维护:系统采用的技术或架构已过时,不再受支持或难以维护。这可能导致系统难以升级、演进和修复漏洞,增加系统故障和风险的概率。




  • 第三方供应商问题:系统依赖于第三方供应商提供的技术、服务或组件,但供应商出现问题,无法提供所需的支持、维护或升级。这可能导致系统中断、服务质量下降或业务受阻。




  • 文档或流程的问题,如没有文档,没有沉淀,只在某些人的脑袋里面:如果系统或流程存在缺乏文档、知识沉淀或依赖于个别人员的情况,可能会造成知识孤立和团队合作的问题,影响系统的可维护性和可扩展性。




  • 数据完整性和一致性问题:数据在系统内部或与其他系统之间的传输和处理过程中,可能遭受损坏、丢失或篡改,导致数据完整性和一致性问题。这可能对决策和业务流程产生负面影响。




  • 大规模系统故障:系统由多个组件、服务或子系统组成,如果其中一个组件出现故障,可能导致整个系统的大规模故障。例如,云服务提供商的故障、硬件故障等。




  • 法规和合规风险:系统必须符合特定的法规要求和合规标准,如果系统无法满足这些要求,将面临法律风险、罚款或业务停摆的风险。




  • 服务容量的不足:系统中的某些服务容量可能不足以应对高负载或峰值流量,这可能导致性能下降、响应时间延迟或系统崩溃。




  • 基建发布或扩容等发布操作会影响业务的情况:系统基础设施的发布操作,如服务器扩容、网络配置变更等,可能会对业务产生影响,例如服务中断或性能下降。




  • 线上配置/环境/网络等的变更:对线上系统的配置、环境或网络进行变更时,可能会引入风险,如配置错误、网络中断等,导致系统故障或不稳定。




  • 安全问题:系统面临的安全漏洞、攻击风险或数据泄露等问题可能对业务运行和用户数据安全产生重大影响。




要识别系统性风险,可以采取以下方法:



  • 审查历史数据和经验教训,了解以前的系统故障和问题。

  • 进行风险评估和风险工作坊,与团队一起识别潜在的系统性风险。

  • 与各个部门和团队合作,收集反馈和洞察,了解系统的弱点和关键风险点。

  • 借鉴行业标准和最佳实践,了解常见的系统性风险和应对方法。

  • 定期进行系统评估和安全审查,以发现潜在的系统性风险。

  • 通过识别系统性风险,组织可以有针对性地采取措施来降低风险,并确保系统的稳定性、安全性和可靠性。


6.3 风险治理


风险治理不是一个一蹴而就的事情,需要持续的来做,需要从组织,流程机制,系统工具和文化层面进行治理。



  • 组织层面:一个事情或方案想要比较好的落地,一定是有一个完整的组织来承接,至少需要有 PACE 的逻辑来支撑,明确分工。

  • 流程层面:流程层面至少要建立明确的沟通机制,如周报、例会等,同时还需要建议风险控制流程,明确制定风险识别、评估、控制和监测的标准流程,确保风险管理工作的有序进行。

  • 系统工具:理想中是希望有建立统一的风险管理信息系统,用于收集、整理和分析风险相关信息。甚至可以利用数据分析和人工智能,对潜在风险进行预测和预警,提高风险应对的时效性。简化版可以通过群、Jira 系统等项目管理工具来达到前期的系统工具落地的程度。

  • 文化层面:通过宣导、洞察、关注、固化、奖励等方式引导大家对于风险的关注,将风险意识融入日常工作中,提高大家对风险的认知,强化风险意识。


以上的组织、流程、系统工具和文化层面的治理都是为了更好的管理风险而存在。在这个过程中,风险模型是抓手,通过不停的识别风险,消除风险,缓和风险,不断提高系统变好的可能,以最终达到治理系统性风险的目标。


风险评估和应对规划是一个反复重复的过程,不停的迭代风险模型,识别出新的风险。


当风险模型构建完成后,我们需要定期逐个风险拉出来 review 一次,我们可以问我们自己如下的一些问题:



  • 与上次回顾相比,风险有更严重吗?可能性有更高吗?

  • 接下来会排专人来解决某些风险吗?是否应该安排?

  • 上次回顾安排的事项落实了,对应的风险情况如何,是否有更新到风险模型中?


问完问题,我们可能需要有一些实际的行动:



  • 评估是否有新的风险;

  • 删除旧的风险:如果风险已经解决了,可以归档;

  • 评估原有风险模型中的每一项风险,评估其严重性和可能性,如果有变动,对其进行更新;

  • 对于不同的优先级的风险区别对待。


以上的回顾操作我们在上面建设的某个管理系统来承载,并且这个管理系统是带有通知等功能,以更好的将风险相关的信息周知出去,如 Jira 系统。


7 小结


系统性风险是一个动态的概念,持续反复的监测和评估至关重要。定期审查系统的运行情况、漏洞和潜在风险,确保及时发现和解决问题

作者:潘锦
来源:juejin.cn/post/7242720768885309495
,以减少系统性风险。

收起阅读 »

末日终极坐标安卓辅助工具

前言 本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。 整体的话就是借助这个工具方便记录当前坐标,可以实现游戏资源不浪费。 阅读本文档前提是大家是《末日血战》游戏玩家。 工具下载安装 download.csdn.net/download/u0… 安...
继续阅读 »

前言


本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。
整体的话就是借助这个工具方便记录当前坐标,可以实现游戏资源不浪费。

阅读本文档前提是大家是《末日血战》游戏玩家。


工具下载安装


download.csdn.net/download/u0…


安装工具


工具安装后,桌面会有这个图标。

在打开工具前需进入应用设置页打开这个应用的显示悬浮窗权限图片说明


填入初始坐标


打开工具大致显示是这个样子,坐标初始都是0,那么填入相应的坐标保存就会是这个样子。
14ae0a88098b04a18b0a0c36b400e3c.jpg
可以看到左上角有个小图,这是一个直接坐标系的缩略图,拖拽可以移动位置。
小图中会显示3个红点,一个绿点。绿点表示当前坐标,红点表示终极坐标。当前点的坐标是固定显示在左上角的


此时可以按返回键退出app,但是不要杀掉应用。


建立坐标系


初次使用,建议可以打开小程序截一张生存之路的全屏图,然后我们打开这张图并横屏显示图片开始操作。当已经熟悉工具如何使用后可以在游戏中进行操作了


建立坐标系

打开图片或者游戏,进入到这个界面,点击左上角悬浮窗上的开始按钮,会看到这样一个界面(没有中间两条直线)
47b446b703476c931a27667de16d706.jpg
我们的目标就是为了建立中间两条直线。


按图片指示的顺序操作。尽可能点击在轴线的中心位置
d6bdd46daf0a94960c54bf8918af5f5.png
以上操作只需要执行一次,后续就不需要操作了。
完成以上操作,就得到有两条线的图了。这个时候就完成了建立坐标系


开始寻找终极坐标


注意观察小地图,找一个我们没有到达的离的近的终极坐标为目标。可以看到x和y的差距。

举个例子,我们当前坐标49,52,刚刚已经在48,52这里取得了一个宝箱,那么下一个目的地选237,29。因为是x坐标相差较大,我们x太小,而y坐标我们的大一点。所以主要的方向是加x,少量的减y。
05c5394993b5a8877db3d81c8ce6425.png
所以我们应该按x轴正方向和y轴负方向这里走,因为x相差较大,所以如果可以的话(有障碍物就走不了)就直接沿着x轴正方向走就好了。


我想游戏玩家应该还是知道要往哪走的,但是容易算错坐标或者根本懒得记,凭运气。那么我们指定往哪走之后,接下来怎么使用这个工具。



  • 1、点击一个位置(我们要让小车开到的位置),这个时候小车不会走,因为我们工具盖住了游戏

  • 2、app回退一下(不是杀掉应用),这时可以发现小车在抖动了,其实就是小车可以走了,再点一下刚才那个位置,小车就会走到那个位置。这样我们就完成了一次移动和坐标记录。小地图当前坐标就会变化。绿点也会移动。

  • 3、小车走完之后,我们再点开始,然后重复1,2 步骤。


补充



  • 1、本工具存在误差,一般每次执行在小车x,y <=|2| 基本100%准确。x,y <= |3| 100个汽油可能会有|2|以内坐标的误差(仅本人测试数据)

  • 1、点击位置尽可能点在地图块的中间,这样可以减少误差。遇到坐标点在路径上,可以进入其中对当前坐标进行校准,当然一般是不需要的。

  • 1、如果遇到了事件,我们就处理完事件后再点开始按钮

  • 2、回退怎么用:右下角回退用途是当我们不想走这一步,可以回退一步。重新再点一个点。确认这个点没问题我们就回退app,如果回退还是后悔不想走这一步(这个回退是指回退我们的记录,游戏中的步骤我们肯定是做不到回退的),再打开开始点击回退

  • 3、本次汽油用完后,就可以杀掉辅助工具app了。下次有汽油可以继续直接使用,记住使用过程中的退出都是回退而不是杀掉app


最后


希望大家先熟悉工具流程,可以截一张图去操作,然后再在游戏中操作避免浪费资源。坐标可以随时矫正。

希望大家游戏愉快,也希望本工具对大家有所帮助。

如有建议或问题可在文章评论

作者:流光无影
来源:juejin.cn/post/7243081126826491941
中反馈或者群里找我。

收起阅读 »

开发的功能不都是经过上线测试,为什么上线后还会那么多 Bug ?

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。 大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅? 本篇只是毫无意义的「故事」,内容纯属「虚构」,如有...
继续阅读 »

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。


大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅?




本篇只是毫无意义的「故事」,内容纯属「虚构」,如有雷同,自己「反思」。



对于这个话题,我想用一个「虚构」的故事来介绍下,这个故事里我有一个「朋友」,他在某电商项目组,当时恰逢经历了公司内部的「双十一需求立项」:


立项时


老板:“这次的需求很简单,主要就是参考去年 TB 的预热活动来走,核心就是提高客单量和活跃,具体细节你们和产品沟通就行”


产品:“TB 去年的活动预热大家应该都了解吧,我们这次主要就是复刻一个类似的活动,时间一个月,具体有***,总的来说,双十一活动一定要准时上线,这次运营那边投入很多经费,需求方面我会把控好,核心围绕老板的思路,细节大家可以看文档,基本就是这些内容,一个月应该够的,大家没问题吧?”


开发:“没问题,保证完成任务”


3 天后:


老板:“我刚看了 JD 好像推出了一个不错的小游戏,我觉得这个可以导入到这边活动里,这样可以提高用户的活跃,用户活跃了,消费自然就起来了”


开会


产品:“鉴于老板的意见,我觉得 JD 的这个游戏活动效果可以提升我们的复购,所以我计划在原本需求基础上增加一个支线活动。


产品:“大家不要紧张,支线会和当前主线同步开发,支线活动比较灵活,不对账号级别做限制,另外「设计」那边看下入口放哪里比较合适。”


开发:“上线时间还是没变吗?”


产品:“双十一日期会变吗?老板说了大家抓紧点,功能不多,时间还是够的”


10 天后:


老板:“我刚和x总沟通了下,他觉得我们的活动少了互动,不利于留存,你看下怎么处理”


开会


产品:“经过这几天和老板的探讨,我们发现这次活动少了一些互动性,必须增加一些交互游戏,这样才能提升日活和用户体验。


产品:“这样吧,这部分功能也是比较迫切,我知道任务比较重,但是为了这次能取到较好成果,大家加把劲,接下来周末幸苦下大家,先不休假,后面调休补回来,具体需求大家可以看文档,有一些调整,不多,时间还是够的”


开发:“。。。。”


14 天后:


老板:“我看大家工作的热情还是可以的,刚运营提了可以增加一些视频支持,我觉得这是一个很好的意见”


开会


产品:“目前看起来我们的开发进度还比较乐观,运营的同学说了他们录制了一些活动视频,我看了还不错,就在主会场增加一些视频播放的功能吧,细节你们直接和设计讨论下”


产品:“这个应该不难吧,把分享和下载加上,视频播放这么基础的东西,应该不耽误事,我看网上开源服务就挺多的。”


开发:“。。。。”


20 天后:


老板:“我刚去开发那里看了下效果,感觉分会场的效果挺好的,做支线太可惜了,给他加回主流程里”


开会


产品:“老板那边觉得分会场的效果更好,让我们把分会场的效果做到主会场里的核心交互里,分会场部分的逻辑就不要了,入口也取消。


产品:“大家不要激动,都是现成的东西,改一改很快的,不过项目进度目前看来确实起有点拉垮,接下来大家晚上多幸苦点,我们晚上11点后再下班,我申请给大家报销打车的费用”


开发:“。。。。”


23 天后:


老板:“整体效果我觉得可以,就是好像有一些糙,你们再过一过,另外大家开个会统一下目标,看看能不能有新的补充”


产品:“可以的,过去我们也一直在开会对齐,基本每两天都会对齐一次”


开会


产品:“我和设计对了下,发现有一些细节可以优化,比如一些模块的颜色可以细调一下,还有这些按键的动画效果也加上,我知道工期紧张,所以我从「隔壁」项目组借了几个开发资源,未来一周他们可以帮助大家缓解下压力”


开发:“。。。。”


28 天后:


开会


产品:“好了,项目可以提测了,相信之前测试也陆续介入跟进了需求,应该问题不大,目前看起来「燃尽图」还可以,测试完了尽快上线,老板那边也在催了”


测试:“不行啊,今天走了用例,一堆问题,而且提测版本怎么和用例还有需求文档不一致,这个提测的版本开发是不是自己没做过测试啊,一大堆问题,根本测不下去,这样我们很难做”


产品:“这个我来沟通,开发接下来这几天大家就不要回家了,马上活动上线,一起攻克难关”


产品:“我也理解大家很努力了,但是这次提测的质量确实不行,你们还是要自己多把把关,不能什么问题都等到 QA 阶段才暴露,这样不利于项目进度,需求一直都很明确,大家把 Bug 尽可能修一修,有什么问题我们及时沟通,尽快解决,敏捷开发”


开发:“。。。。”


上线前一天晚上 10 点:


开会


测试:“不行,还有 20 几个问题没有确认,这种情况我不能签字,上线了有问题谁负责。”


产品:“一些问题其实并不影响正常使用,目前主流程应该没问题,让开发把 P0 的两个问题修了之后先上线,剩下的在运营过程中逐步更新就好了,有问题让运营先收集和安抚”


开发:“上线了脏数据不好弄,会有一些账号同步的问题,而且用户等级可能还有坑”


产品:“没什么不好弄的,到时候有问题的用户让运营做个标志,接下来小步快跑修复就好了,时间不等人,明天就是上线时间, 活动上不去对老板和运营都没办法交代”


项目上线:


老板:“运营和我反馈了好多问题,你们版本上线是怎么测试的,要反思一下xxxx”


开会


产品:“我说过用户要集齐碎片和好友砍价之后才能给优惠券,等级不够的不让他们砍价成功,为什么只完成砍价的新人拿到大额优惠券?”


产品:“什么?因为账号数据绑定有 Bug ,不同渠道用户合并账号就可以满足?为什么会有这个 Bug ,测试那边没覆盖到吗?”


测试:“我不知道啊,需求文档没有说还能账号合并,而且这个功能之前没说过要限制用户等级吧?”


产品:“我出的需求肯定有,文档里肯定有,另外开发你既然知道问题,为什么不提前沟通,现在用户都消费了,这个事故你和测试 55 责,后面复盘的时候要避免这样的问题再次发生”


开发:“。。。。。”



最后


所以大家觉得是谁应该背锅?是开发的能力不够,还是测试用例的覆盖缺失?说起来,其实在此之前,我在掘金也遇到了一个 “Bug” ,比如:



文章 Markdown 里图片链接的 content-type 字段如果不符合 image/*** 规格,那么发布出来的时候链接就不会被掘金转码,所以不会有图片水印,同时直接指向了我自己的图床地址。



那么你觉得这个是 Bug 吗?明明是用户不遵循规范。但是这样不就留下漏洞了吗?



如果在文章审核通过之后,我修改图床上的图片内容,这样不就可以绕过审核在掘金展示一些「违规」内容了吗?



所以有时候一些功能的初衷是好的,但是引发的问题却又很隐蔽,那么这能怪「测试用例覆盖不到位吗」?


那么,你觉得「经过测试,为什么上线后还会那么多 Bug 」

作者:恋猫de小郭
来源:juejin.cn/post/7207715311758393405
更多可能是谁的问题?

收起阅读 »

接口耗时2000多秒!我人麻了!

接口耗时2000多秒!我人麻了! 前几天早上,有个push服务不断报警,报了很多次,每次都得运维同学重启服务来维持,因为这个是我负责的,所以我顿时紧张了起来,匆忙来到公司,早饭也不吃了,赶紧排查! 1、 现象与排查步骤: 下面是下午时候几次告警的截图: ...
继续阅读 »

接口耗时2000多秒!我人麻了!



  • 前几天早上,有个push服务不断报警,报了很多次,每次都得运维同学重启服务来维持,因为这个是我负责的,所以我顿时紧张了起来,匆忙来到公司,早饭也不吃了,赶紧排查!


1、 现象与排查步骤:


下面是下午时候几次告警的截图:



  • 来看下图。。。。接口超时 2000多秒。。。。我的心碎了!!!人也麻了!!!脑瓜子嗡嗡的。。。



  • image.png



  • 另外还总是报pod不健康、不可用 这些比较严重的警告!



  • image.png


我的第一反应是调用方有群发操作,然后看了下接口的qps 貌似也不高呀!也就 9req/s,
之后我去 grafana 监控平台 观察jvm信息发现,线程数量一直往上涨,而且线程状态是 WAITING 的也是一直涨。


如下是某一个pod的监控:


image.png
image.png


为了观察到底是哪个线程状态一直在涨,我们点进去看下详情:


image.png


上图可以看到 该pod的线程状态图 6种线程状态全列出来了, 分别用不同颜色的线代表。而最高那个同时也是14点以后不断递增那个是蓝线 代表的是 WAITING 状态下的线程数量。


通过上图现象,我大概知道了,肯定有线程是一直在wait无限wait下去,后来我找运维同学 dump了线程文件,分析一波,来看看到底是哪个地方使线程进入了wait !!!


如下 是dump下来的线程文件,可以看到搜索出427个WAITING这也基本和 grafana 监控中状态是WAITTING的线程数量一致


image.png


重点来了(这个 WAITING 状态的堆栈信息,还都是从 IOSPushStrategy#pushMsgWithIOS 这个方法的某个地方出来的(151行出来的)),于是我们找到代码来看看,是哪个小鬼在作怪?



image.png
而类 PushNotificationFuture 继承了 CompletableFuture,他自己又没有get方法,所以本质上 就是调用的 CompletableFuture的 get 方法。
image.png
ps:提一嘴,我们这里的场景是 等待ios push服务器的结果,不知道啥情况,今天(指发生故障的那天)ios push服务器(域名: api.push.apple.com )一直没返回,所以就导致一直等待下去了。。



看到这, 我 豁然开朗 && 真相大白 了,原来是在使用 CompletableFutureget时候,没设置超时时间,这样的话会导致一直在等结果。。。(但代码不是我写的,因为我知道 CompletableFuture 的get不设置参数会一直等下去 ,我只是维护,后期也没怎么修改这块的逻辑,哎 ,说多了都是泪呀!)


好一个 CompletableFuture#get();


(真是 死等啊。。。一点不含糊的等待下去,等的天荒地老海枯石烂也要等下去~~~ )


到此,问题的原因找到了。


2、 修复问题


解决办法很简单,给CompletableFuture的get操作 加上超时时间即可,如下代码即可修复:
image.png


在修复后,截止到今天(6月8号)没有这种报警情况了,而且线程数和WAITING线程都比较稳定,都在正常范围内,如下截图(一共4个pod):
image.png


至此问题解决了~~~ 终于可以睡个好觉啦!


3、 复盘总结


3.1、 代码浅析


既然此次的罪魁祸首是 CompletableFuture的get方法 那么我们就浅析一下 :



  1. 首先看下 get(); 方法
    image.png
    image.png


上边可以看到
不带参数的get方法: if(deadLine==0) 成立 ,也就是最终调用了LockSupport的park(this);方法,而这个方法最终调了unsafe的这个方法-> unsafe.park(false, 0L); 其中第二个参数就是等待多长时间后,unpark即唤醒被挂起的线程,而0 则代表无限期等待。



  1. 再来看下 get(long timeOut,TimeUnit unit);方法
    image.png
    我们可以看到
    带参数的get方法: if(deadLine==0) 不成立,走的else逻辑 也就是最终调用了LockSupportparkNanos(this,nanos);方法,而这个方法最终调了unsafe的这个方法-> unsafe.park(false, nanos); 其中第二个参数就是你调用get时,传入的tiimeOut参数(只不过底层转成纳秒了)


    我们跑了下程序,发现超过指定时间后,get(long timeOut,TimeUnit unit); 方法抛出 TimeoutException异常,而至于超时后我们开发者怎么处理,就在于具体情况了。
    Exception in thread "main" java.util.concurrent.TimeoutException
    at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1886)
    at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2021)



而在 我的另一篇文章 万字长文分析synchroized 这篇文章中,我们其实深入过openjdk的源码,见识过parkunpark操作,我们截个图回忆一下:


image.png


3.2、最后总结:


1.在调用外部接口时。一定要注意设置超时时间,防止三方服务出问题后影响自身服务。


2.以后无论看到什么类型的Future,都要谨慎,因为这玩意说的是异步,但是调用get方法时,他本质上是同步等待,所以必须给他设置个超时时间,否则他啥时候能返回结果,就得看天意了!


3.凡是和第三方对接的东西,都要做最坏的打算,快速失败的方式很有必要。


4.遇到天大的问题,都尽可能保持冷静,不要乱了阵脚!
作者:蝎子莱莱爱打怪
来源:juejin.cn/post/7242237897993814075
strong>

收起阅读 »

聊聊自己进入大厂前,创业公司的经历,我学到了什么?

前言 自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。 未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。 现在是 2...
继续阅读 »

前言


自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。


未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。


现在是 23 年 6 月,我第一次羊了,上一轮虽然逃过去了,但是这一次没能幸免,现在还在发着低烧。


距离上次写日记又过去了半年多,自己也步入 25 岁,这半年发生了很多事情,体会到时间变得更快了。


工作第一年:误打误撞加入创业公司



在来到字节之前,经历了校招被毁约,来到了创业公司,在创业公司工作了一年,作息是大小周,早 9 晚 9 ,对我来说这是一段比较记忆深刻的经历,教会了我很多互联网行业的东西。



校招被毁约


19 年的时候签约了一家金融公司,也是临近 20 年毕业前几十天,才被公司以疫情经济情况不好的理由,告知单方面毁约。


那个时候的我还是很害怕的,一点是临近毕业,大多校招已停止,第二点是自己有一段时间没有复习,很多知识已经遗忘。


一个朋友得知了我的情况,内推我到一家创业公司,也是内推第二天一大早就去公司面试,还好我还有一些基础在脑子里,现场面了两轮,当场就通过了,成为了前端的实习生一枚,公司也是答应毕业之后就转正。


面试通过之后,第二天就去公司上班了。第一天入职就拿到了 Macbook,也是第一次用 Mac 办公,想象那时的自己,就跟刚进城的人一样。


公司的结构


一个公司,总共就 50 来个人,做着 DAU 百万的产品。由于公司不大,所以做各种不同事情的人(销售、CEO、内容运营、新媒体运营、算法、产品),都坐在一起。敲代码的时候还能听到销售打电话的声音,因为是教育产品,所以销售的合作方大多是猿辅导、好未来、作业帮、学霸君等教育头部公司。


虽然公司小,但是产品各个环节非常全面:



  • ABTest

  • 算法

  • Hive


公司小的好处是,决策层级少,一个想法想要落地是很快的。在公司 1 年期间,快速上线了 3 款小程序,也快速从 0 到 1 上线了金融产品。PC、H5、小程序开发都有涉及。


整个团队的氛围是非常 Open 的,可以试验各种新技术,不像大厂内部会封装一些框架,有内部标准,不好去实践一些开源的框架。


与普通公司不同的地方


个人扮演的角色


我觉得最大不同的地方是角色,我是一名前端,但不仅扮演着前端的角色。要对产品体验负责,也要自己设计页面 UI。


公司没有测试,所以在日常开发中,每个研发和产品都是 QA,要对 C 端百万 DAU 产品对应的用户体验负责。


每个人都对需求负责,一个需求上线之后,可以自己写 SQL 去 Hive 里捞埋点数据,验证 AB 的可行性。


可以深入感受互联网不同角色发挥的作用


在创业公司,因为每周都有公司周会,大家会聚在一起聊每周各部门的进展,在会上,也可以全流程的了解到一个产品的完整生命周期。




  • 内容运营视角:



    • 做公众号,发文章,在存在粉丝基础的情况下,头版收入是很可观的,可以达到 6 位数+。




  • 产品运营视角:



    • 做竞品分析,看出对方哪方面做的好,我们要抄袭哪快的功能。

    • 做电话回访,了解用户的痛点,尤其针对停留时间较长的重度 C 端用户。




  • 销售视角:



    • 通过和大型教育机构合作,由于家长是很愿意为孩子进行教育付费的,所以通过弹窗收集信息配合电销,可以达到很可观的收入。




  • 算法视角:



    • 通过 AB 试验调整算法策略,可以优化广告收入,另外也可以提前计算预测插入广告可能带来的收益。

    • 调整算法策略,也可以优化用户停留时长,增强用户粘性。




  • 数据分析:



    • 通过 Hive 离线数据计算,可以生成一些报表数据,给提供用户信息聚合查看功能。




  • 产品视角:



    • 在产品基本功能打磨完毕后,要尽可能往付费方向引导。




  • 运维视角:



    • 将服务迁移到 K8S 集群,可以降低维护成本。




  • 后端视角:



    • 和算法、数分团队配合,另外还需要负责机器运维相关。




  • 老板视角:



    • 关注一些重要事项的进展,以及查看上线后的数据,是否符合产品预期。

    • 最终产品需要自负盈亏,功能不能一步设计到位,也需要把一些功能做成付费使用的。

    • 关注公司每个方向资金支出情况,控制公司收支,避免快速花光融资资金。保持自己的股份不被过度稀释。




我的直观感受是,自己虽然初入茅庐,但通过这一年的感知,深入理解了 C 端产品的全流程。这对我来说也是一笔很大的财富。


营收方面


App 内广告占大头,开屏广告>插屏广告>贴片广告,其次公众号文章等也是赚钱的利器,销售带来的收益远不如以上两个。总共这些一个月7位数还是有的。实际上最大的开销除了人力成本,还是服务器的成本,这个成本逼近7位数。


创业公司的生命周期



  1. 公司在快速发展期,有很多功能需要开发,这时是需要人的时候,会无限招人。

  2. 在产品 2-3 年之后,如没有新的大方向,进入一段停滞期,指的是 DAU 的不增长或下降。

  3. 产品稳定期,不再需要人,核心骨干退出团队,HC 缩减,产品转向以盈利为目标。

  4. 自负盈亏,break even。不再为公司资金发愁,不再需要融资。

  5. 保持公司运作,通过手段维持 DAU 和用户付费意愿,通过一些预消费手段留住用户,扩大收益。


快速验证


快速验证是 CEO 经常提的一个点,不过在王兴和张一鸣成功的经验来看,这也是正确的。


快速验证是说快速从 0 到 1 上线一个产品,冷启动或硬广,在短期查看一个产品的数据,如果产品数据不够理想,便放弃产品。试验下一个风口上的题材。


像美团,或者字节现在也在使用这种策略,快速上线 App 并试错,留下那些抓住用户的产品。


公司的瓦解


一个产品的瓶颈


当一个产品被打磨到 3 年之后,一般来说主流程就比较完善了,换句话说是用户需要的功能,产品都有了。这个时候也就过了 PM 发力 RD 开发的时期,在这之后即便这个公司只有运营,也可以保持产品正常运行。


创业公司的问题


CEO 的话语权会很大


一个人带来的决策不一定合理,当产品的发展不再合理时。大家会出现不满情绪,久而久之大家也不再团结协作,在快速上线几个小程序无果之后,3 个月内 50% 的研发团队成员纷纷离职了,不过大伙也很厉害,离职之后大多都去了大厂。


转变方向为营收优先


通过缩减一系列支出,想方设法让公司达到赚钱的状态。


手段有:砍 HC,团建,下线产品不需要的费钱的功能。


另外我也是一步步看着,公司从半层多楼的工区面积,变成 5分之二,4分之一的大小,最后工区被卖掉,撤离北京。


我的离开


我的离开也是必然,在后期被拉到老板的新产品线帮忙做产品从 0 到 1 建设。对当时工作还不到 1 年的我来说,还是很有压力,独自 own 一个私人银行项目。


在长时间宣传下,仍是没有用户使用,我能明显的感受到,新产品前景是渺茫的,只是老板的一厢情愿。另外新产品线的研发非常少,只是一番的催活,其实过程也决定了结果,产品是做不成的。在这种情况下,我提出了离职。


不过我也很感谢这段经历,能让我对从 0 到 1 创业有新的理解,另外也锻炼了我的抗压能力,增强了技术积累。


最近的工作


工作上


工作上在建设插件市场,提供了一种能快速开发页面组件的方式,能直接嵌入组件到前端中,类似动态执行模块联邦注入的组件。是一块很有意思的功能,类似于 Chrome 应用商城,其实开发工具建设一直是我比较喜欢也擅长的方向,未来也会继续在这方面努力,学习其它语言,做更快更高效的工具,为开发提效。


详细可以看这篇我今年写的文章 带你从 0 到 1 实现前端「插件市场架构」


能力提升方面


编程技能


学习并实战了以下技能:



  • VSCode 插件开发


  • Rush.js

    • 大型项目构建管理。



  • Golang (MySQL / Redis / Kafka)

    • 主要还是 API 层面的熟悉,目的还是为了能用非 NodeJS 语言写一写后端,以及了解更多的后端知识。



  • Rust
    稍微了解了一下语法,之前也写了一篇文章:以 JS 的视角带你入门 Rust 语言,快速上手


开源库


轻量的模块联邦


非编程相关


最近这 2 年,锻炼了画图、写 PRD、拉通对齐的能力,大厂更加专精一个方面,这让我能静下心来,不再像创业公司一样,受老板的影响,不再做快速迭代的事情,而是把产品打磨好,更加以用户角度出发思考用户需要什么,补齐什么功能。


愿望


毕业之后,由于疫情一直都是在国内旅游。还没出过国,希望疫情后每年自己都能出去走走,行万里路。把最好的景色都记录下来,拓宽眼界,放松心情。我很喜欢大海,尤其是四环环海的小岛,看着大海能让自己的心平静下来。接下来还有几个非常想去的地方、意大利、冰岛、新西兰、瑞士,夏威夷,希望能在 3

作者:EricLee
来源:juejin.cn/post/7243252896392314937
0 岁之前达成目标。

收起阅读 »

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

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

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



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



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


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


第一道题目是算法题:


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


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

console.log(result);

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


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


第二道题目是场景题:


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


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


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


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


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


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


一面:




  • 自我介绍


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




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


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




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



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




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



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




  • 说一下 useMemouseCallback 有什么区别



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




  • 说一下 useEffectuseLayoutEffect 有什么区别



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




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



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




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



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




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




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




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




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




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




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






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



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




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



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




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



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




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




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


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




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




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




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




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






      2. 性能问题




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




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










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



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




  • 你有什么想问我的吗?



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





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



二面:




  • 自我介绍



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




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



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




  • object 的原型指向谁?



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




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



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




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




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




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




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




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




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



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

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC






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




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



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

          • 当 to space 体积超过 25%




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



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

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










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



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




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



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




  • 简单说一下 useEffect 的用法:



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




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



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




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



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




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



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




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



    • 磕磕绊绊回答出来了。




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



    • 应用层。




  • 说一下 getpost 有什么区别?



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




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




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



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

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

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

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

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

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




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



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

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

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

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

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




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



      1. dom 操作

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

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






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



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




  • 项目难点



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




  • 你有什么想问我的吗?



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




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




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


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


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


今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


最后,给自己打个广告!求职求职求职!!!


社交信息:



个人优势:



  • antd 团队成员、ahooks 团队成员,活跃于 github 开源社区,给众多知名大型开源项目提交过 PR,拥有丰富的 React + TS 实战经验

  • 熟悉前端性能优化的实现,例如代码优化、打包优化、资源优化,能结合实际业务场景进行优化

  • 熟悉 webpack / vite 等打包工具的基本配置, 能够对以上工具进行二次封装、基于以上工具搭建通用的开发环境

  • 熟悉 prettier / eslint 基本配置,有良好且严格的编码习惯,唯客户论,实用主义者

  • 熟悉代码开发到上线全流程,对协同开发分支管理项目配置等都有较深刻的最佳实践



可内推的大佬们麻烦联系我!在 github 主页有联系方式,或者直接在掘金私聊我也可,谢谢!!


作者:三年没洗澡
来源:juejin.cn/post/7239715208792342584

收起阅读 »

✅让我们制作一个漂亮的头像吧

头像 头像是指在互联网上用于代表个人或实体的图像。在社区中,头像通常用于标识和区分用户,是用户身份的象征。 社区的头像有多种意义,不限于以下几点: 身份标识:社区头像可以让用户在互联网上更好地代表自己,帮助用户与其他用户区分身份。 个性表达:社区头像可以是用...
继续阅读 »

头像


头像是指在互联网上用于代表个人或实体的图像。在社区中,头像通常用于标识和区分用户,是用户身份的象征。


社区的头像有多种意义,不限于以下几点:



  • 身份标识:社区头像可以让用户在互联网上更好地代表自己,帮助用户与其他用户区分身份。

  • 个性表达:社区头像可以是用户个人喜好的表达,例如使用特定的头像可以代表用户的风格、爱好等。

  • 社交互动:社区头像也可以促进用户之间的互动,例如用户可以通过观察其他用户的头像来了解对方的性格、兴趣爱好等。

  • 社区文化:在某些社区中,头像可能会成为社区文化的的一部分,例如某些社区可能会有特定的头像格式、颜色等。


怎么获取一个头像


如果你想要获取一个头像,你可以考虑以下几种方法:



  • 使用第三方头像库:有许多第三方头像库提供大量的头像选择,你可以通过搜索或浏览这些头像库来获取头像。常见的第三方头像库包括 imgur、imgur、av.com 等。

  • 使用网站或应用程序的内置头像选择器:许多网站和应用程序都提供了内置的头像选择器,你可以通过选择器来浏览和使用现有的头像。例如,在社交媒体网站上,你可以使用内置的头像选择器来浏览和使用其他用户分享的头像。

  • 使用自己的图片编辑器:如果你拥有一张图片,你可以考虑使用一些图片编辑器来创建或修改头像。例如,你可以使用 Photoshop、GIMP 等专业的图形编辑软件来创建或修改头像。

  • 使用在线头像制作工具:有一些在线头像制作工具可以帮助你创建自己的头像。这些工具通常提供各种图像模板、字体和颜色选择,你可以根据工具的指导来创建自己的头像。


AIGC



  • 确定您的头像风格:您可以选择自己喜欢的风格,例如卡通、现代、传统等等。

  • 选择适当的图像工具:您可以选择任何适当的图像工具,例如 Photoshop、GIMP 等等。

  • 创建您自己的头像:您可以使用工具中的绘图工具、滤镜和调整工具等来创建自己的头像。

  • 参考其他头像:您可以浏览互联网上的各种头像,以获得灵感和创意。

  • 注意细节:当您创建头像时,一定要注意细节,例如颜色、纹理、形状等等。


我们来生成一下


00841-100689130.png


00843-100689132.png


00844-100689133.png


00845-100689134.png


00846-100689135.png


00847-100689136.png


00849-4042383308.png


00850-4042383309.png


00852-4042383311.png


00853-4042383312.png


00854-4042383313.png


00856-4042383315.png


00857-4042383316.png

收起阅读 »

环信十年 -- 我的失败恋情

多久了?不知道,从何时起,我的心里开始空虚,开始寂寞?总想把自己用忙碌湮没,让内心不在空虚,却一次又一次的被失败填满?          无奈的回味着,友谊。从...
继续阅读 »
多久了?不知道,从何时起,我的心里开始空虚,开始寂寞?总想把自己用忙碌湮没,让内心不在空虚,却一次又一次的被失败填满?
          无奈的回味着,友谊。从刚认识开始,到现在,不知道过了多久了吧,我们亦都是对彼此之间熟悉的不能在熟悉了吧?可为什么我们还会因为一句话而闹的人心惶惶,友谊不在?最终成为路人?
           或许,是我的那句玩笑话错了吧,不应该说吧。可,说出来的话,泼出来的水,收是收不回来了。既然,你知道,那句话是玩笑话,为什么你还要打电话威胁我?让我们把彼此搞成陌路人?
           多久了?我们没有一起聊过天,吃过饭啦?以前,我们总是一起的打打闹闹,把彼此作为最好的朋友,可现在?我们见了面就像陌生人一样,谁也不理谁,亦当作谁也没看到谁。呵呵,这是我们想要的结果?
       也许,那刚开始时的美好,我们永远都回不去了吧,我不想因为我跟你的事破坏你与他们之间的关系,毕竟他们是无辜的,你不应该也淡忘了他们。
             或许吧,我们形同陌路的关系,让他们很难堪,但,也许,这并不破坏我与他们亦或者你们与他们之间的关系,这是我们之间的关系吧?
         何时吧?回忆那年逝去的美好与快乐,想起来,总有一种莫名的心痛,却还是那么让人心情愉快,不知道吧,何时,我们还能在一起打打闹闹,快快乐乐了?
           不知道吧,忘记了吧,陌路了吧,但我们还是会见面的,即使,我们陌路了,但是为了他们高兴,我们难道不应该在一起聚聚亦或出去玩玩?虽然吧,装作陌生人,我们还是能够在一起相处的。

         那年,美好的幸福,只可能成为回忆,而不可能再实现么?何时,我们拾起了那年美好的回忆,我们就可能同归于好了。
收起阅读 »

环信十年趴 -- 我的游戏人生

我还记得第一次接触游戏的时候,那是在我还是个小学生的时候。当时有一个同学在班上给我们展示他玩的游戏,我也跟着看了一会儿。从那一刻起,我就被游戏中那无边无际的世界吸引住了。随着时间的推移,我越来越沉迷于游戏。每当我遇到困难或者压力大的时候,我总是会找到一款喜欢的...
继续阅读 »

我还记得第一次接触游戏的时候,那是在我还是个小学生的时候。当时有一个同学在班上给我们展示他玩的游戏,我也跟着看了一会儿。从那一刻起,我就被游戏中那无边无际的世界吸引住了。

随着时间的推移,我越来越沉迷于游戏。每当我遇到困难或者压力大的时候,我总是会找到一款喜欢的游戏,投入其中,忘却烦恼和焦虑。对我而言,游戏不再仅仅是一种娱乐方式,它已经成为了我的精神寄托和解压的出口。

我所玩的游戏类型很多,有角色扮演、策略、射击等等。其中最让我难以忘怀的是《魔兽世界》这款游戏。这个游戏的世界非常巨大,里面有许多任务需要完成,也有许多其他玩家可以交流和合作。我花费了很长时间去玩这个游戏,认识了许多志同道合的朋友,一起探索这个神奇的虚拟世界。

但是,我也深知游戏对我的生活造成了一定的负面影响。因为玩游戏,我经常熬夜,导致身体状况下降;因为玩游戏,我经常缺席学校课程,导致成绩不够好。这些问题让我的父母非常担心和不满,他们要求我减少游戏时间,更加专注于学业和健康生活。我也意识到游戏与现实之间的平衡很重要,不能让游戏占据全部的时间和精力。

然而,游戏对我而言并不仅仅是负面的影响,它也影响着我的人生观和价值观。在游戏中,我学到了很多东西:如何与陌生人沟通、如何合作完成任务、如何处理复杂的情境和困难等等。这些技能不仅可以应用到游戏中,也可以应用到现实生活和职业发展中。此外,游戏还教会了我坚持不懈、勇于尝试、追求自我超越等等品质。这些品质也是我在现实生活中必须具备的。

总的来说,我的游戏人生是一段充满着欢乐、挑战和启示的旅程。游戏对我产生了深远的影响,它不仅让我找到了人生中的解压出口和娱乐方式,也让我学会了很多珍贵的技能和品质。当然,我也意识到在玩游戏的同时,要合理安排时间、注意身体健康和更加专注于现实生活。

本文参与环信十周年活动

收起阅读 »

10 秒看懂 Android 动画的实现原理

介绍 动画是 Android 应用程序中重要的交互特性。Android 提供了多种动画效果,包括平移、缩放、旋转和透明度等,它们可以通过代码或 XML 来实现。本文将介绍 Android 动画的原理和实现方法,并提供一些示例。 原理 Android 动画的实现...
继续阅读 »

介绍


动画是 Android 应用程序中重要的交互特性。Android 提供了多种动画效果,包括平移、缩放、旋转和透明度等,它们可以通过代码或 XML 来实现。本文将介绍 Android 动画的原理和实现方法,并提供一些示例。


原理


Android 动画的实现原理是通过改变视图的属性来实现的。当我们在代码中设置视图的属性值时,Android 会通过平滑过渡的方式来将视图从一个状态过渡到另一个状态。这种平滑过渡的效果就是动画效果。


属性


Android 中有许多属性可以用来实现动画效果,以下是一些常用的属性:



  • translationX:视图在 X 轴上的平移距离。

  • translationY:视图在 Y 轴上的平移距离。

  • scaleX:视图在 X 轴上的缩放比例。

  • scaleY:视图在 Y 轴上的缩放比例。

  • rotation:视图的旋转角度。

  • alpha:视图的透明度。


类型


Android 中有多种不同类型的动画,每种类型都有其自身的特点和用途:


View 动画


View 动画是一种在应用程序中实现动画效果的简单方法。它可以通过 XML 或代码来实现。View 动画可以应用于任何 View 对象,包括按钮、文本框、图像等等。常见的 View 动画包括平移、缩放、旋转和透明度等效果。以下是一个 View 动画的 XML 示例:


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0%"
android:toXDelta="50%"
android:duration="500"
android:repeatCount="infinite"
android:repeatMode="reverse" />

</set>

帧动画


帧动画是一种将一系列图像逐帧播放来实现动画效果的方法。它可以通过 XML 或代码来实现。帧动画常用于播放一系列连续的图像,例如动态图像、电影等等。以下是一个帧动画的 XML 示例:


<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">

<item android:drawable="@drawable/animation_frame1" android:duration="50" />
<item android:drawable="@drawable/animation_frame2" android:duration="50" />
<item android:drawable="@drawable/animation_frame3" android:duration="50" />
...
</animation-list>

属性动画


属性动画是一种可以改变视图属性值的动画效果。它可以通过 XML 或代码来实现。属性动画可以应用于任何属性,包括大小、颜色、位置、透明度等等。它可以在运行时动态地更改属性值,从而实现平滑的动画效果。以下是一个属性动画的 Java 代码的示例:


ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f);
animator.setDuration(1000);
animator.start();

过渡动画


过渡动画是一种在应用程序中实现平滑过渡效果的方法。它可以通过 XML 或代码来实现。过渡动画常用于实现屏幕之间的切换效果,例如滑动、淡入淡出等等。以下是一个过渡动画的 XML 示例:


<transition xmlns:android="http://schemas.android.com/apk/res/android">
<fade android:duration="500" />
</transition>

Lottie 动画


Lottie 是 Airbnb 开源的一种动画库,它可以将 Adobe After Effects 中制作的动画直接导出为 JSON 格式,并在 Android 应用程序中使用。Lottie 动画可以实现非常复杂的动画效果,例如骨骼动画、粒子效果等等。


实现


要实现 Android 动画,我们需要按照以下步骤:



  1. 创建动画资源文件。

  2. 在代码中加载动画资源文件。

  3. 将动画应用到相应的视图中。


我们可以通过 XML 或代码来创建动画资源文件。以下是一个简单的平移动画的 XML 示例:


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0%p"
android:toXDelta="50%p"
android:duration="500"
android:repeatCount="infinite"
android:repeatMode="reverse" />

</set>

在代码中加载动画资源文件的方法如下:


Animation animation = AnimationUtils.loadAnimation(this, R.anim.translate);

最后,我们需要将动画应用到相应的视图中:


imageView.startAnimation(animation);

下面是一个实现平移动画效果的 Java 代码示例:


View view = findViewById(R.id.view);
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 300f);
animator.setDuration(1000);
animator.start();

结论


无论是在应用程序设计中还是在用户体验中,动画都是一个非常重要的因素。如果你想要在你的应用程序中实现动画效果,本文提供了 Android 动画的基本原理和实现方法。你可以根据自己的需要使用不同类型的动画来实现不同的效果。


推荐


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


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


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


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


daily_algorithm: 每日一算法,由浅入深

作者:午后一小憩
来源:juejin.cn/post/7242596746180739128
,欢迎加入一起共勉。

收起阅读 »

flutter 刷新、加载与占位图一站式服务(基于easy_refresh扩展)

前文 今天聊到的是滚动视图的 刷新与加载,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresh、 easy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是...
继续阅读 »

前文


今天聊到的是滚动视图刷新与加载,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresheasy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是极具代表性的优秀插件。
那你在使用时有没有类似情况:



  • 为了重复的样版式代码感到厌倦?

  • 为了Ctrl V+C感到无聊?

  • 看到通篇类似的代码想大刀阔斧的整改?

  • 更有甚者有没有本来只想自定义列表样式,却反而浪费更多的时间来完成基础配置?


现在我们来解决这类问题,欢迎来到走近科学探索发现 - Approaching Scientific Exploration and Discovery。(可能片场走错了:) )


注意



  • 本文以 easy_refresh 作为刷新框架举例说明(其他框架的类似)

  • 本文案例demo以 getx 作为项目状态管理框架(与刷新无关仅作为项目基础框架构成,不喜勿喷)


正文


现在请出我们重磅成员mixin,对于这个相信大家已经非常熟悉了。我们要做的就是利用easy_refresh提供的refresh controller对视图逻辑进行拆分,从而精简我们的样板式代码。


首页拆离我们刷新和加载方法:


import 'dart:async';
import 'package:easy_refresh/easy_refresh.dart';
import 'state.dart';

mixin PagingMixin<T> {
/// 刷新控制器
final EasyRefreshController _pagingController = EasyRefreshController(
controlFinishLoad: true, controlFinishRefresh: true);
EasyRefreshController get pagingController => _pagingController;

/// 初始页码 <---- 单独提出这个的原因是有时候我们请求的起始页码不固定,有可能是0,也有可能是1
int _initPage = 0;

/// 当前页码
int _page = 0;
int get page => _page;

/// 列表数据
List<T> get items => _state.items;
int get itemCount => items.length;

/// 错误信息
dynamic get error => _state.error;

/// 关联刷新状态管理的控制器 <---- 自定义状态类型,后文会有阐述主要包含列表数据、初始加载是否为空、错误信息
PagingMixinController get state => _state;
final PagingMixinController<T> _state = PagingMixinController(
PagingMixinData(items: []),
);

/// 是否加载更多 <---- 可以在控制器中初始化传入,控制是否可以进行加载
bool _isLoadMore = true;
bool get isLoadMore => _isLoadMore;

/// 控制刷新结束回调(异步处理) <---- 手动结束异步操作,并返回结果
Completer? _refreshComplater;

/// 挂载分页器
/// `controller` 关联刷新状态管理的控制器
/// `initPage` 初始页码值(分页起始页)
/// `isLoadMore` 是否加载更多
void initPaging({
int initPage = 0,
isLoadMore = true,
}) {
_isLoadMore = isLoadMore;
_initPage = initPage;
_page = initPage;
}

/// 获取数据
FutureOr fetchData(int page);

/// 刷新数据
Future onRefresh() async {
_refreshComplater = Completer();
_page = _initPage;
fetchData(_page);
return _refreshComplater!.future;
}

/// 加载更多数据
Future onLoad() async {
_refreshComplater = Completer();
_page++;
fetchData(_page);
return _refreshComplater!.future;
}

/// 获取数据后调用
/// `items` 列表数据
/// `maxCount` 数据总数,如果为0则默认通过 `items` 有无数据判断是否可以分页加载, null为非分页请求
/// `error` 错误信息
/// `limit` 单页显示数量限制,如果items.length < limit 则没有更多数据
void endLoad(
List<T>? list, {
int? maxCount,
// int limit = 5,
dynamic error,
}) {
if (_page == _initPage) {
_refreshComplater?.complete();
_refreshComplater = null;
}

final dataList = List.of(_state.value.items);
if (list != null) {
if (_page == _initPage) {
dataList.clear();
// 更新数据
_pagingController.finishRefresh();
_pagingController.resetFooter();
}
dataList.addAll(list);
// 更新列表
_state.value = _state.value.copyWith(
items: dataList,
isStartEmpty: page == _initPage && list.isEmpty,
);

// 默认没有总数量 `maxCount`,用获取当前数据列表是否有值判断
// 默认有总数量 `maxCount`, 则判断当前请求数据list+历史数据items是否小于总数
// bool hasNoMore = !((items.length + list.length) < maxCount);
bool isNoMore = true;
if (maxCount != null) {
isNoMore = page > 1; // itemCount >= maxCount;
}
var state = IndicatorResult.success;
if (isNoMore) {
state = IndicatorResult.noMore;
}
_pagingController.finishLoad(state);
} else {
_state.value = _state.value.copyWith(items: [], error: error ?? '数据请求错误');
}
}

}


创建PagingMixin<T>混入类型,泛型<T>属于列表子项的数据类型


void initPaging(...):初始化的时候可以写入基本设置(可以不调用)


Future onRefresh() Future onLoad():供外部调用的刷新加载方法


FutureOr fetchData(int page):由子类集成重写,主要是完成数据获取方法,在获取到数据后,需要调用方法void endLoad(...)来结束整个请求操作,通知视图刷新


PagingMixinController继承自ValueNotifier,是对数据相关状态的缓存,便于独立逻辑操作与数据状态:


class PagingMixinController<T> extends ValueNotifier<PagingMixinData<T>> {
PagingMixinController(super.value);

dynamic get error => value.error;
List<T> get items => value.items;
int get itemCount => items.length;
}
// flutter 关于easy_refresh更便利的打开方式
class PagingMixinData<T> {
// 列表数据
final List<T> items;

/// 错误信息
final dynamic error;

/// 首次加载是否为空
bool isStartEmpty;

PagingMixinData({
required this.items,
this.error,
this.isStartEmpty = false,
});

....

}

完成这两个类的编写,我们对于逻辑部分的拆离已经完成了。


下面是对easy_refresh的使用,封装:


class PullRefreshControl extends StatelessWidget {
const PullRefreshControl({
super.key,
required this.pagingMixin,
required this.childBuilder,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
});

final Header? header;
final Footer? footer;

final bool refreshOnStart;
final Header? startRefreshHeader;

/// 列表视图
final ERChildBuilder childBuilder;

/// 分页控制器
final PagingMixin pagingMixin;

/// 是否固定刷新偏移
final bool locatorMode;

@override
Widget build(BuildContext context) {
final firstRefreshHeader = startRefreshHeader ??
BuilderHeader(
triggerOffset: 70,
clamping: true,
position: IndicatorPosition.above,
processedDuration: Duration.zero,
builder: (ctx, state) {
if (state.mode == IndicatorMode.inactive ||
state.mode == IndicatorMode.done) {
return const SizedBox();
}
return Container(
padding: const EdgeInsets.only(bottom: 100),
width: double.infinity,
height: state.viewportDimension,
alignment: Alignment.center,
child: SpinKitFadingCube(
size: 25,
color: Theme.of(context).primaryColor,
),
);
},
);

return EasyRefresh.builder(
controller: pagingMixin.pagingController,
header: header ??
RefreshHeader(
clamping: locatorMode,
position: locatorMode
? IndicatorPosition.locator
: IndicatorPosition.above,
),
footer: footer ?? const ClassicFooter(),
refreshOnStart: refreshOnStart,
refreshOnStartHeader: firstRefreshHeader,
onRefresh: pagingMixin.onRefresh,
onLoad: pagingMixin.isLoadMore ? pagingMixin.onLoad : null,
childBuilder: (context, physics) {
return ValueListenableBuilder(
valueListenable: pagingMixin.state,
builder: (context, value, child) {
if (value.isStartEmpty) {
return _PagingStateView(
isEmpty: value.isStartEmpty,
onLoading: pagingMixin.onRefresh,
);
}
return childBuilder.call(context, physics);
},
);
},
);
}
}

创建PullRefreshControl类型,设置必须属性pagingMixinchildBuilder,前者是我们创建的PagingMixin对象(可以是任何类型,只要支持混入就可以了),后者是对我们滚动列表的实现。 其他的都是对 easy_refresh的属性配置,参考相关文档就行了。


到这里我们减配版的封装就完成了,使用方式如下:


截图


截图


但是我们并没有完成我们前文所说的简化操作,还是需要一遍又一遍创建重复的滚动列表,所以我们继续:


/// 快速构建 `ListView` 形式的分页列表
/// 其他详细参数查看 [ListView]
class SpeedyPagedList<T> extends StatelessWidget {
const SpeedyPagedList({
super.key,
required this.controller,
required this.itemBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
double? itemExtent,
}) : _separatorBuilder = null,
_itemExtent = itemExtent;

const SpeedyPagedList.separator({
super.key,
required this.controller,
required this.itemBuilder,
required IndexedWidgetBuilder separatorBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
}) : _separatorBuilder = separatorBuilder,
_itemExtent = null;

final PagingMixin<T> controller;

final Widget Function(BuildContext context, int index, T item) itemBuilder;

final Header? header;
final Footer? footer;

final bool refreshOnStart;
final Header? startRefreshHeader;
final bool locatorMode;

/// 参照 [ScrollView.controller].
final ScrollController? scrollController;

/// 参照 [ListView.itemExtent].
final EdgeInsetsGeometry? padding;

/// 参照 [ListView.separator].
final IndexedWidgetBuilder? _separatorBuilder;

/// 参照 [ListView.itemExtent].
final double? _itemExtent;

@override
Widget build(BuildContext context) {
return PullRefreshControl(
pagingMixin: controller,
header: header,
footer: footer,
refreshOnStart: refreshOnStart,
startRefreshHeader: startRefreshHeader,
locatorMode: locatorMode,
childBuilder: (context, physics) {
return _separatorBuilder != null
? ListView.separated(
physics: physics,
padding: padding,
controller: scrollController,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
separatorBuilder: _separatorBuilder!,
)
: ListView.builder(
physics: physics,
padding: padding,
controller: scrollController,
itemExtent: _itemExtent,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
);
},
);
}
}

...


归纳我们所需要的使用方式(我这里只写了ListView/GridView),构创建快速初始化加载列表的方法,将我们仅需要的Widget Function(BuildContext context, int index, T item) itemBuilder单个元素的创建(因为对于大多列表来说我们仅关心单个元素样式)暴露出来,简化PullRefreshControl的使用。


截图


对比前面的使用方式,现在更加简洁了,总计代码也就十几行吧。


到这里就结束啦,文章也仅算是对繁杂重复使用的东西进行一些归纳总结,没有特别推崇的意思,更优秀的方案也比比皆是,所以仁者见仁了各位。


附GIF展示:


GIF 2023-6-9 14-23-45.gif


附Demo地址: boomcx/templat

e_getx

收起阅读 »

mybatis拦截器实现数据权限

前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现。 比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示部门ID和分公司ID。 查看员工打卡记录SQL为:selec...
继续阅读 »

前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现。

比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示部门ID和分公司ID。

查看员工打卡记录SQL为:select id,name,dpt_id,company_id from t_record


当一个总部账号可以查看全部数据此时,sql无需改变。因为他可以看到全部数据。

当一个部门管理员权限员工查看全部数据时,sql需要在末属添加 where dpt_id = #{dpt_id}


如果每个功能模块都需要手动写代码去拿到当前登陆用户的所属部门,然后手动添加where条件,就显得非常的繁琐。

因此,可以通过mybatis的拦截器拿到查询sql语句,再自动改写sql。


mybatis 拦截器


MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:



  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)


这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。


通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。


分页插件pagehelper就是一个典型的通过拦截器去改写SQL的。



可以看到它通过注解 @Intercepts 和签名 @Signature 来实现,拦截Executor执行器,拦截所有的query查询类方法。

我们可以据此也实现自己的拦截器。



import com.skycomm.common.util.user.Cpip2UserDeptVo;
import com.skycomm.common.util.user.Cpip2UserDeptVoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
@Slf4j
public class MySqlInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = statement.getBoundSql(parameter);
String originalSql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();

SqlLimit sqlLimit = isLimit(statement);
if (sqlLimit == null) {
return invocation.proceed();
}

RequestAttributes req = RequestContextHolder.getRequestAttributes();
if (req == null) {
return invocation.proceed();
}

//处理request
HttpServletRequest request = ((ServletRequestAttributes) req).getRequest();
Cpip2UserDeptVo userVo = Cpip2UserDeptVoUtil.getUserDeptInfo(request);
String depId = userVo.getDeptId();

String sql = addTenantCondition(originalSql, depId, sqlLimit.alis());
log.info("原SQL:{}, 数据权限替换后的SQL:{}", originalSql, sql);
BoundSql newBoundSql = new BoundSql(statement.getConfiguration(), sql, boundSql.getParameterMappings(), parameterObject);
MappedStatement newStatement = copyFromMappedStatement(statement, new BoundSqlSqlSource(newBoundSql));
invocation.getArgs()[0] = newStatement;
return invocation.proceed();
}

/**
* 重新拼接SQL
*/
private String addTenantCondition(String originalSql, String depId, String alias) {
String field = "dpt_id";
if(StringUtils.isBlank(alias)){
field = alias + "." + field;
}

StringBuilder sb = new StringBuilder(originalSql);
int index = sb.indexOf("where");
if (index < 0) {
sb.append(" where ") .append(field).append(" = ").append(depId);
} else {
sb.insert(index + 5, "
" + field +" = " + depId + " and ");
}
return sb.toString();
}

private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.cache(ms.getCache());
builder.useCache(ms.isUseCache());
return builder.build();
}


/**
* 通过注解判断是否需要限制数据
* @return
*/
private SqlLimit isLimit(MappedStatement mappedStatement) {
SqlLimit sqlLimit = null;
try {
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("
."));
String methodName = id.substring(id.lastIndexOf("
.") + 1, id.length());
final Class<?> cls = Class.forName(className);
final Method[] method = cls.getMethods();
for (Method me : method) {
if (me.getName().equals(methodName) && me.isAnnotationPresent(SqlLimit.class)) {
sqlLimit = me.getAnnotation(SqlLimit.class);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return sqlLimit;
}


public static class BoundSqlSqlSource implements SqlSource {

private final BoundSql boundSql;

public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}

@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}


顺便加了个注解 @SqlLimit,在mapper方法上加了此注解才进行数据权限过滤。

同时注解有两个属性,


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SqlLimit {
/**
* sql表别名
* @return
*/

String alis() default "";

/**
* 通过此列名进行限制
* @return
*/

String columnName() default "";
}

columnName表示通过此列名进行限制,一般来说一个系统,各表当中的此列是统一的,可以忽略。


alis用于标注sql表别名,如 针对sql select * from tablea as a left join tableb as b on a.id = b.id 进行改写,如果不知道表别名,会直接在后面拼接 where dpt_id = #{dptId},

那此SQL就会错误的,通过别名 @SqlLimit(alis = "a") 就可以知道需要拼接的是 where a.dpt_id = #{dptId}


执行结果


原SQL:select * from person, 数据权限替换后的SQL:select * from person where dpt_id = 234

原SQL:select * from person where id > 1, 数据权限替换后的SQL:select * from person where dpt_id = 234 and id > 1


但是在使用PageHelper进行分页的时候还是有问题。



可以看到先执行了_COUNT方法也就是PageHelper,再执行了自定义的拦截器。


在我们的业务方法中注入SqlSessionFactory


@Autowired
@Lazy
private List<SqlSessionFactory> sqlSessionFactoryList;


PageInterceptor为1,自定义拦截器为0,跟order相反,PageInterceptor优先级更高,所以越先执行。




mybatis拦截器优先级




@Order




通过@Order控制PageInterceptor和MySqlInterceptor可行吗?



将MySqlInterceptor的加载优先级调到最高,但测试证明依然不行。


定义3个类


@Component
@Order(2)
public class OrderTest1 {

@PostConstruct
public void init(){
System.out.println(" 00000 init");
}
}

@Component
@Order(1)
public class OrderTest2 {

@PostConstruct
public void init(){
System.out.println(" 00001 init");
}
}

@Component
@Order(0)
public class OrderTest3 {

@PostConstruct
public void init(){
System.out.println(" 00002 init");
}
}

OrderTest1,OrderTest2,OrderTest3的优先级从低到高。

顺序预期的执行顺序应该是相反的:


00002 init
00001 init
00000 init

但事实上执行的顺序是


00000 init
00001 init
00002 init

@Order 不控制实例化顺序,只控制执行顺序。
@Order 只跟特定一些注解生效 如:@Compent @Service @Aspect … 不生效的如: @WebFilter


所以这里达不到预期效果。


@Priority 类似,同样不行。




@DependsOn




使用此注解将当前类将在依赖类实例化之后再执行实例化。


在MySqlInterceptor上标记@DependsOn("queryInterceptor")



启动报错,

这个时候queryInterceptor还没有实例化对象。




@PostConstruct




@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。

在同一个类里,执行顺序为顺序如下:Constructor > @Autowired > @PostConstruct。


但它也不能保证不同类的执行顺序。


PageHelper的springboot start也是通过这个来初始化拦截器的。





ApplicationRunner




在当前springboot容器加载完成后执行,那么这个时候pagehelper的拦截器已经加入,在这个时候加入自定义拦截器,就能达到我们想要的效果。

仿照PageHelper来写


@Component
public class InterceptRunner implements ApplicationRunner {

@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

@Override
public void run(ApplicationArguments args) throws Exception {
MySqlInterceptor mybatisInterceptor = new MySqlInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.addInterceptor(mybatisInterceptor);
}
}
}

再执行,可以看到自定义拦截器在拦截器链当中下标变为了1(优先级与order刚好相反)



后台打印结果,达到了预期效果。


收起阅读 »

天马行空使用适配器模式

1. 前言 因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。 开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学...
继续阅读 »

1. 前言


因为最近没有什么比较好的技术想要分享,所以来水一下文章,啊不对,是分享一下一些思路,和大家交流一下想法,没准能产生一些新的想法。这篇文章不具备权威,不一定正确,简单来说全都是我瞎吹。


开发这么久,多多少少也有了解过适配器模式。我还是一如既往的建议学习设计模式,不能光靠看别人的文章,得靠自己的积累和理解源码的做法来得更切实际。


2. 浅谈适配器


我们都知道,适配器模式是一种结构型模式,结构型模式我的理解都有一个特点,简单来说就是封装了一些操作,就是隐藏细节嘛,比如说代理就是隐藏了通过代理调用实体的细节。


我还是觉得我们要重头去思考它是怎样的一个思路。如果你去查“适配器模式”,相信你看到很多关于插头的说法,而且你会觉得这东西很简单,但好像看完之后又感觉学了个寂寞。


还是得从源码中去理解。先看看源码中最经典的适配器模式的使用地方,没错,RecyclerView的Adapter。但是它又不仅仅只使用Adapter模式,如果拿它的源码来做分析反而会有些绕。但是我们可以进行抽象。想想Adapter其实主要的流程就是输入数据然后输出View给RecyclerView


然后又可以大胆的去思考一下,如果不定义一个Adapter,没有ViewHolder,要怎么实现这个效果?写一个循环,然后在循环里面做一些逻辑判断,然后创建对应的子View,添加到父View中 ,大概会这样做吧。那其实Adapter就帮我们做了这一步,并且还做了很多比较经典的优化操作,其实大概就是这样。


然后从这样的模型中,我大概是能看出了一些东西,比如使用Adapter是为了输入一些东西,也可以说是为了让它封装一些逻辑,然后达到某些目的,如果用抽象的眼光去看,我不care你封装的什么东西,我只要你达到我的目的(输出我的东西),RecyclerView的adapter就是输出View。这是其一,另外,在这个模型中我有一个使用者去使用这个Adaper,这个Apdater在这里不是一个概念,而是一个对象,我这个使用者通过Adaper这个对象给它一些输入,以此来实现我的某些目标


ok,结合Adapter模式的结构图看看(随便从网上找一张图)


image.png


可以看到这个模型中有个Client,有个Tagrget,有个Adapter。拿RecyclerView的Adapter来说,Client是RecyclerView (当然具体的addView操作是在LayoutManager中,这里是抽象去看),Adapter就是RecyclerView.Adapter,而Tagrget抽象去看就是对view的操作。


3. 天马行空的使用


首先一般使用官方的RecyclerView啊这些,都会提供自己的Adapter,但是会让人会容易觉得Adapter就是在复用View的情况下使用。而我的理解是,RecyclerView的复用是ViewHolder的思想,不是Adapter的思想,比如早期的ListView也有Adapter啊,当时出RecyclerView之前也没有说用到ViewHolder的这种做法(这是很多年前的事),我说这个是想要表达不要把Adapter和RecyclerView绑一起,这样会让思维受到局限。


对我而言,Adapter在RecyclerView中的作用是 “Data To View” ,适配数据而产出对应的View。


那我可以把Apdater的作用理解成“Object To Object” ,对于我来说,Object它可以是Data,可以是View,甚至可以是业务逻辑。


所以当我跳出RecyclerView这种传统的 “Data To View” 的思维模式之后,Adapter适配器模式可以做到的场景就很多,正如上面我理解的,Adapter简单来说就是 “Data To View” ,那我可以用适配器模式去做 “Data To Data” ,可以去做 “View To Data” ,可以去做 “Business To Business” , “Business To View” 等等,实现多种效果。


假设我这里做个Data To Data的场景 (强行举的例子可能不是很好)


我请求后台拿到个人的基本信息数据


data class PeopleInfo(
var name: String?,
var sex: Int?,
......
)

然后通过个人的ID,再请求服务端另外一个接口拿到成绩数据


data class ScoreInfo(
var language : Int,
var math : Int,
......
)

然后我有个数据竞赛的报名表对象。


data class MathCompetition(
var math : Int,
var name : String
)

然后一般我们使用到的时候就会这样赋值,假设这段代码在一个Competition类中进行


val people = getPeopleInfo()
val score = getScoreInfo()
val mathTable = MathCompetition(
score.math?,
people.name?,
......
)

就是深拷贝的一种赋值,我相信很多人的代码里面肯定会有


两个对象 AB
A.1 = B.1
A.2 = B.2
A.3 = B.3
......

这样的代码,然后对象的熟悉多的话这个代码就会写得很长。当然这不会造成什么大问题,但是你看着看着,总觉得缺少一些美感,但是好像这玩意没封装不起来这种感觉。


如果用适配器来做的话,首先明确要做的流程是从Competition这个类中,将所有数据整合成MathCompetition对象,目标就是输出MathCompetition对象。那从适配器模式的模型上去看,client就是Competition,是它的需求,Taget就是getMathCompetition,输出mathTable,然后我们可以写一个Adapter。


class McAdapter {

var mathCompetition : MathCompetition? = null

init {
// 给默认值
mathCompetition = MathCompetition(0, "name", ......)
}

fun setData(people : PeopleInfo? = null, score : ScoreInfo? = null){
people?.let {
mathCompetition?.name = it.name
......
}

score?.let {
mathCompetition?.math = it.math
......
}
}

fun getData() : MathCompetition?{
return mathCompetition
}

}

然后在Competition中就不需要直接引用MathCompetition,而是设置个setAdapter方法,然后需要拿数据时再调用adapter的getData()方法,这样就恰到好处,不会把这些深拷贝方式的赋值代码搞得到处都是。 这个Demo看着好像没什么,但是真碰到了N合1这样的数据场景的时候,使用Adapter显然会更安全。


我再简单举一个Business To View的例子吧。假设你的app中有很几套EmptyView,比如你有嵌入到页面的EmptyView,也有做弹窗类型的EmptyView,我们一般的做法就是对应的页面的xml文件中直接写EmptyView,那这些EmptyView的代码就会很分散是吧。OK,你也想整合起来,所以你会写个EmptyHelper,大概是这个意思,用单例写一个管理EmptyView的类,然后里面统一封装对EmptyView的操作,一般都会这样写。 其实如果你让我来做,我可能就会用适配器模式去实现。 ,当然也有其他办法能很好的管理,这具体的得看心情。


写一个Adapter,我这里写一段伪代码,应该比较容易能看懂


class EmptyAdapter() {
// 这样语法要伴生,懒得写了,看得懂就行
const val STATUS_NORMAL = 0
const val STATUS_LOADING = 1
const val STATUS_ERROR = 2

private var type: Int = 0
var parentView: ViewGroup? = null
private var mEmptyView: BaseEmptyView? = null
private var emptyStatus = 0 // 有个状态,0是不显示,1是显示加载中,2是加载失败......

init {
createEmptyView()
}

private fun createEmptyView() {
// 也可以判断是否有parentView决定使用哪个EmptyView等逻辑
mEmptyView = when (type) {
0 -> AEmptyView
1 -> BEmptyView
2 -> CEmptyView
else -> {
AEmptyView
}
}
}

fun setData(status: Int) {
when (status) {
0 -> parentView?.removeView(mEmptyView)
1 -> mEmptyView?.showLoading()
2 -> mEmptyView?.showError()
}
}

fun updateType(type: Int) {
setData(0)
this.type = type
createEmptyView()
}
}

然后在具体的Activity调用的时候,可以


val emptyAdapter = EmptyAdapter(getContentView())
// 然后在每次要loading的时候去设置adapter的的状态
emptyAdapter.setData(EmptyAdapter.STATUS_LOADING)
emptyAdapter.setData(EmptyAdapter.STATUS_NORMAL)
emptyAdapter.setData(EmptyAdapter.STATUS_ERROR)

可以看出这样做就有几个好处,其一就是不用每个xml都写EmptyView,然后也能做到统一的管理,和一些人写的Helper这种的效果类似,最后调用的方法也很简单,你只需要创建一个Adaper,然后调用它的setData就行,我们的RecyclerView也是这样的,在外层去创建然后调用Adapter就行。


4. 总结


写这篇文章主要是为了水,啊不对,是为了想说明几个问题:

(1)开发时要善于跳出一些限制去思考,比如RecyclerView你可能觉得适配器模式就和它绑定了,就和View绑定了,有View的地方才能使用适配器模式,至少我觉得不是这样。

(2)学习设计模式,只去看一些介绍是很难理解的,当然要知道它的一个大致的思想,然后要灵活运用到开发中,这样学它才有用。

(3)我对适配器模式的理解就是 Object To Object,我可以去写ViewAdapter,可以去写DataAdapter,也可以去写BusinessAadpter,可以用这个模式去适配不同的场景,利用这个思想来使代码更加合理。


当然最后还是要强调一下,我不敢保证我说的就是对的,我肯定不是权威的。但至少我使用这招之后的的新代码效果要比一些旧代码更容

作者:流浪汉kylin
来源:juejin.cn/post/7242623772301459517
易维护,更容易扩展。

收起阅读 »

vue打包脚本:打包前判定某个npm link依赖库是否是指定分支

web
1. 需求场景 有一个项目A,它依赖项目B 项目B是本地开发的,也在本地维护 项目A通过npm link链接到了项目B 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应 项目A...
继续阅读 »

1. 需求场景



  • 有一个项目A,它依赖项目B

  • 项目B是本地开发的,也在本地维护

  • 项目A通过npm link链接到了项目B

  • 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev

  • 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应

  • 项目A项目A-dev都在产品同时运行,遇到问题都要在各自分支同步修复缺陷


在启动或者打包的时候需要特别小心项目B处在什么分支,错配分支就会导致项目启动失败或报错,于是需要有一个脚本帮我在项目启动时,检查依赖脚本是否在正确的分支上,如果不在,就自动将依赖分支切换到对应需要的分支上。


2. 脚本编写


2.1 脚本思路:



  • 先去指定项目B的文件夹中查看该项目处于哪一个分支

  • 通过动态参数获得本地启动或打包的是哪一个分支,和当前项目分支进行比对

  • 如果不是当前分支,就切换到要求的分支, 切到对应分支后,再执行install操作

  • 如果是当前分支则直接跳过分支切换操作,直接往下走


2.2 脚本创建


在vue项目根目录创建一个脚本文件check-dependency.js


下面脚本中分支名@tiamaes/t4-framework就对应项目B


const t4FrameworkFilePath = "D:/leehoo/t4-framework"; //本地@tiamaes/t4-framework目录地址

const { spawnSync } = require("child_process");

const branchName = process.argv[2];//获取参数分支名,打包时需要传递进来
if (!branchName) {
console.error("Branch name is not specified.");
process.exit(1);
}

// 获取当前分支
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: t4FrameworkFilePath });
if (result.status !== 0) {
console.error("Failed to get current branch:", result.stderr.toString());
process.exit(1);
}
const currentBranch = result.stdout.toString().trim();

// 判断分支是否需要切换
if (currentBranch !== branchName) {
console.log(`@tiamaes/t4-framework is not in ${branchName} branch. Switching to ${branchName} branch...`);
const checkoutResult = spawnSync("git", ["checkout", branchName], { cwd: t4FrameworkFilePath });
if (checkoutResult.status !== 0) {
console.error(`Failed to switch to ${branchName} branch:`, checkoutResult.stderr.toString());
process.exit(1);
}

// 安装依赖包
console.log("Installing dependencies...");
const installResult = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install"], { cwd: t4FrameworkFilePath });
if (installResult.status !== 0) {
console.error("Failed to install dependencies:", installResult.stderr.toString());
process.exit(1);
}
console.log("Dependencies installed successfully.");
}

console.log(`@tiamaes/t4-framework is in ${branchName} branch. Proceeding to build...`);

process.exit(0);


3. package.json引用


在该脚本的script中增加引用方式,在项目启动或打包的时候都要执行一次node check-dependency.js,其后跟随的是项目B的分支名,我这里是erp-dev和erp-m1两个分支


"scripts": {
"serve:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service serve --mode development.erp",
"serve:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service serve --mode development.m1",
"build:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service build --report --mode production.erp",
"build:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service build --mode production.m1",
"link:local": "npm link @tiamaes/t4-framework",
},

下面是执行效果


Video_2023-06-09_202602.gif


image.png

收起阅读 »

单例模式

单例设计模式 单例设计模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。 好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。 由于new操作的次数减少,因而对...
继续阅读 »

单例设计模式


单例设计模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。


好处:



  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。

  • 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。


单例模式的六种写法:


一、饿汉单例设计模式


步骤:



  1. 私有化构造函数。

  2. 声明本类的引用类型变量,并且使用该变量指向本类对象。

  3. 提供一个公共静态的方法获取本类的对象。


//饿汉单例设计模式 ----> 保证Single在在内存中只有一个对象。
public class HungrySingleton {
//声明本类的引用类型变量,并且使用该变量指向本类对象
private static final HungrySingleton instance = new HungrySingleton();
//私有化构造函数
private HungrySingleton(){
System.out.println("instance is created");
}
//提供一个公共静态的方法获取本类的对象
public static HungrySingleton getInstance(){
return instance;
}
}


不足:无法对instance实例做延迟加载


优化:懒汉


二、懒汉单例设计模式



  1. 私有化构造函数。

  2. 声明本类的引用类型变量,但是不要创建对象。

  3. 提供公共静态的方法获取本类的对象,获取之前先判断是否已经创建了本类对象,如果已经创建了,那么直接返回对象即可,如果还没有创建,那么先创建本类的对象,然后再返回。


//懒汉单例设计模式 ----> 保证Single在在内存中只有一个对象。
public class LazySingleton {
//声明本类的引用类型变量,不创建本类的对象
private static LazySingleton instance;
//私有化构造函数
private LazySingleton(){

}
public static LazySingleton getInstance(){
//第一次调用的时候会被初始化
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}


不足:在多线程的情况下,无法保证内存中只有一个实例


public class MyThread extends Thread{

@Override
public void run() {
System.out.println(LazySingleton.getInstance().hashCode());
}

public static void main(String[] args) {
MyThread[] myThread = new MyThread[10];
for(int i=0;i<myThread.length;i++){
myThread[i] = new MyThread();
}
for(int j=0;j<myThread.length;j++){
myThread[j].start();
}
}
}


打印结果:


257688302
1983483740
1983483740
1983483740
1983483740
1983483740
1983483740
1388138972
1983483740
257688302

在多线程并发下这样的实现无法保证实例是唯一的。


优化:懒汉线程安全


三、懒汉线程安全


通过使用同步函数或者同步代码块保证


public class LazySafetySingleton {

private static LazySafetySingleton instance;
private LazySafetySingleton(){

}
//方法中声明synchronized关键字
public static synchronized LazySafetySingleton getInstance(){
if(instance == null){
instance = new LazySafetySingleton();
}
return instance;
}

//同步代码块实现
public static LazySafetySingleton getInstance1(){
synchronized (LazySafetySingleton.class) {
if(instance == null){
instance = new LazySafetySingleton();
}
}
return instance;
}
}

不足:使用synchronized导致性能缺陷


优化:DCL


四、DCL


DCL:double check lock (双重检查锁机制)


public class DclSingleton {

private static DclSingleton instance = null;

private DclSingleton(){

}
public static DclSingleton getInstance(){
//避免不必要的同步
if(instance == null){
//同步
synchronized (DclSingleton.class) {
//在第一次调用时初始化
if(instance == null){
instance = new DclSingleton();
}
}
}
return instance;
}

}


不足:在if判断中执行的instance = new DclSingleton(),该操作不是一个原子操作,JVM首先会按照逻辑,第一步给instance分配内存;第二部,调用DclSingleton()构造方法初始化变量;第三步将instance对象指向JVM分配的内存空间;JVM的缺点:在即时编译器中,存在指令重排序的优化,即以上三步不一定会按照顺序执行,就会造成线程不安全。


优化:给instance的声明加上volatile关键字,volatile能保证线程在本地不会存有instance的副本,而是每次都到内存中读取。即禁止JVM的指令重排序优化。即按照原本的步骤。把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不会调用读操作。



注意:volatile阻止的不是instance = new DclSingleton();这句话内部的指令排序,而是保证了在一个写操作完成之前,不会调用读操作(if(instance == null))



public class DclSingleton {

private static volatile DclSingleton instance = null;

private DclSingleton(){

}
public static DclSingleton getInstance(){
//避免不必要的同步
if(instance == null){
//同步
synchronized (DclSingleton.class) {
//在第一次调用时初始化
if(instance == null){
instance = new DclSingleton();
}
}
}
return instance;
}

}

五、静态内部类


JVM提供了同步控制功能:static final,利用JVM进行类加载的时候保证数据同步。


在内部类中创建对象实例,只要应用中不使用内部类,JVM就不会去加载该类,就不会创建我们要创建的单例对象,


public class StaticInnerSingleton {

private StaticInnerSingleton(){

}
/**
* 在第一次加载StaticInnerSingleton时不会初始化instance,
* 只在第一次调用了getInstance方法时,JVM才会加载StaticInnerSingleton并初始化instance
* @return
*/

public static StaticInnerSingleton getInstance(){
return SingletonHolder.instance;
}
//静态内部类
private static class SingletonHolder{
private static final StaticInnerSingleton instance = new StaticInnerSingleton();
}

}


优点:JVM本身机制保证了线程安全,没有性能缺陷。


六、枚举


public enum EnumSingleton {
//定义一个枚举的元素,它就是Singleton的一个实例
INSTANCE;

public void doSomething(){

}
}

优点:写法简单,线程安全



注意:如果在枚举类中有其他实例方法或实例变量,必须确保是线程安全的。


作者:我可能是个假开发
来源:juejin.cn/post/7242614671001862199

收起阅读 »

Vue和React权限控制的那些事

web
自我介绍 看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。 前言 无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。 ...
继续阅读 »

自我介绍


看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。


前言


无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。


什么是权限控制?


现在基本上都是基于RBAC权限模型来做权限控制


一般来说权限控制就是三种




  • 页面权限:说白了部分页面是具备权限的,没权限的无法访问




  • 操作权限:增删改查的操作会有权限的控制




  • 数据权限:不同用户看到的、数据是不一样的,比如一个列表,不同权限的查看这部分数据,可能有些字段是**脱敏的,有些条目无法查看详情,甚至部分条目是无法查看




那么对应到前端的维度,常见的就4种




  • 权限失效(无效)(token过期/尚未登录)




  • 页面路由控制,以路由为控制单位




  • 页面上的操作按钮、组件等的权限控制,以组件/按钮为最小控制单位




  • 动态权限控制,比如1个列表,部分数据可以编辑,部分部分不可编辑




image.png


⚠️注意: 本文一些方案 基于 React18 React-Router V6 以及 Vue3 Vue-Router V4


⚠️Umi Max 这种具备权限控制系统的框架暂时不在讨论范围内~~


前置条件


由于市面上各家实现细节不一样,这里只讨论核心逻辑思路,不考虑细节实现


无论框架如何,后端根据RABC角色权限这套逻辑下来的,会有如下类似的权限标识信息,可以通过专门的接口获取,或者跟登录接口放在一起。


image.png


然后根据这些数据,去跟路由,按钮/组件等,比对产生真正的权限


像这种权限标识一般都存在内存当中(即便存在本地存储也需要加密,不过其实真正的权限控制还是需要后端来控),一般都是全局维护的状态,配合全局状态管理库使用。


权限失效(无效)


image.png


这种场景一般是在发送某些请求,返回过期状态


或者跟后端约定一个过期时间(这种比较不靠谱)


通常是在 全局请求拦截 下做,整理一下逻辑


路由级别权限控制


通常前端配好的路由可以分为 2 种:


一种是静态路由:即无论什么权限都会有的,比如登录页、404页这些


另一种是动态路由:虽然叫动态路由,其实也是在前端当中定义好了的。说它是动态的原因是根据后端的权限列表,要去做动态控制的


vue实现


在vue体系下,可以通过路由守卫以及动态添加路由来实现


动态路由


先配置静态路由表 , 不在路由表内的路由重定向到指定页(比如404)


在异步获取到权限列表之后,对动态部分的路由进行过滤之后得到有权限的那部分路由,再通过router.addRoute()添加到路由实例当中。


流程为:


(初始化时) 添加静态路由 --> 校验登录态(比如是否有token之类的) --> 获取权限列表(存到vuex / pinia) --> 动态添加路由(在路由守卫处添加)



rightsRoutesList // 来自后端的当前用户的权限列表,可以考虑存在全局状态库
dynamicRoutes // 动态部分路由,在前端已经定义好, 直接引入

// 对动态路由进行过滤,这里仅用path来比较
// 目的是添加有权限的那部分路由,具体实现方案自定。
const generateRoute = (rightsRoutesList)=>{
//ps: 这里需要注意下(如果有)嵌套路由的处理
return dynamicRoutes.filter(i=>
rightsRoutesList.some(path=>path === i.path)
)
}

// 拿到后端返回的权限列表
const getRightsRoutesList = ()=>{
return new Promise(resolve=>{
const store = userStore()
if(store.rightsRoutesList){
resolve(store.rightsRoutesList)
}else{
// 这里用 pinia 封装的函数去获取 后端权限列表
const rightsRoutesList = await store.fetchRightsRoutesList()
resolve(rightsRoutesList)
}
}
}

let hasAddedDynamicRoute = false
router.beforeEach(async (to, from) => {
if(hasAddedDynamicRoute){
// 获取
const rightsRoutesList = await getRightsRoutesList()

// 添加到路由示例当中
const routes = generateRoute(rightsRoutesList)
routes.forEach(route=>router.addRoute(route))
// 对于部分嵌套路由的子路由才是动态路由的,可以
router.addRoute('fatherName',route)
hasAddedDynamicRoute = true
}
// 其他逻辑。。。略


next({...to})
}


踩坑

通过动态addRoute去添加的路由,如果你F5刷新进入这部分路由,会有白屏现象。


image.png


因为刷新进入的过程经历了 异步获取权限列表 --> addRoute注册 的过程,此时跳转的目标路由就和你新增的路由相匹配了,需要去手动导航。


因此你需要在路由守卫那边next放行,等下次再进去匹配到当前路由


你可以这么写


router.beforeEach( (to,from,next) => {
// ...其他逻辑

// 关键代码
next({...to})
})


路由守卫


一次性添加所有的路由,包括静态和动态。每次导航的时候,去对那些即将进入的路由,如果即将进入的路由是在动态路由里,进行权限匹配。


可以利用全局的路由守卫


router.beforeEach( (to,from,next) => {
// 没有访问权限,则重定向到404
if(!hasAuthorization(to)){
// 重定向
return '/404'
}
})

也可以使用路由独享守卫,给 权限路由 添加


    // 路由表
const routes = [
//其他路由。。。略

// 权限路由
{
path: '/users/:id',
component: UserDetails,
// 定义独享路由守卫
beforeEnter: (to, from) => {
// 如果没有许可,则
if(!hasAuthorization(to)){
// 重定向到其他位置
return '/404'
}
},
},
]


react实现


在react当中,一般先将所有路由添加好,再通过路由守卫来做权限校验


局部守卫loader


React-router 当中没有路由守卫功能,可以利用v6版本的新特性loader来做,给权限路由都加上对应的控制loader


import { redirect, createBrowserRouter, RouterProvider } from 'react-router-dom'


const router = createBrowserRouter([
{
// it renders this element
element: <Team />,

// when the URL matches this segment
path: "teams/:teamId",

// with this data loaded before rendering
loader: async ({ request, params }) => {
// 拿到权限
const permission = await getPermission("teams/:teamId")
// 没有权限则跳到404
if(!permission){
return redirect('/404')
}
return null
},

// and renders this element in case something went wrong
errorElement: <ErrorBoundary />,
},
]);

// 使用
function RouterView (){
return (
<RouterProvider router={router}/>
)
}



包装路由(相当于路由守卫)


配置路由组件的时候,先渲染包装的路由组件


image.png


在包装的组件里做权限判断


function RouteElementWrapper({children, path, ...props }: any) {
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(()=>{
// 判断登录态之类的逻辑

// 如果要获取权限,则需要setIsLoading,保持加载状态

// 这里判断权限
if(!hasAccess(path)){
navigate('/404')
}
},[])
// 渲染routes里定义好的路由
return isLoading ? <Locading/> : children
}

按钮(组件)级别权限控制


组件级别的权限控制,核心思路就是 将判断权限的逻辑抽离出来,方便复用。


vue 实现思路


在vue当中可以利用指令系统,以及hook来实现


自定义指令


指令可以这么去使用


<template>
<button v-auth='/site/config.btn'> 编辑 </button>
</template>

指令内部可以操作该组件dom和vNode,因此可以控制显隐、样式等。


hook


同样的利用hook 配合v-if 等指令 也可以实现组件级颗粒度的权限控制


<template>
<button v-if='editAuth'> 权限编辑 </button>
<div v-else>
无权限时做些什么
</div>

<button v-if='saveAuth'> 权限保存 </button>
<button v-if='removeAuth'> 权限删除 </button>
</template>
<script setup>
import useAuth from '@/hooks/useAuth'
// 传入权限
const [editAuth,saveAuth,removeAuth] = useAuth(['edit','save','remove'])
</script>


hook里的实现思路: 从pinia获取权限列表,hook里监听这个列表,并且匹配对应的权限,同时修改响应式数据。


react 实现思路


在React当中可以用高阶组件和hook的方式来实现


hook


定义一个useAuth的hook


主要逻辑是: 取出权限,然后通过关联响应式,暴露出以及authKeys ,hasAuth函数


export function useAuth(){
// 取出权限 ps: 这里从redux当中取
const authData = useSelector((state:any)=>state.login)
// 取出权限keys
const authKeys = useMemo(()=>authData.auth.components ?? [],[authData])
// 是否拥有权限
const hasAuth = useCallback(
(auths:string[]|string)=>(
turnIntoList(auths).every(auth=>authKeys.includes(auth))
),
[authKeys]
)
const ret:[typeof authKeys,typeof hasAuth] = [authKeys,hasAuth]
return ret
}

使用


const ProductList: React.FC = () => {
// 引入
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth("edit"), [hasAuth]);

// ...略
return (
<>
{ authorized ? <button> 编辑按钮(权限)</button> : null}
</>

)
};


权限包裹组件


可以跟进一步,依据这个权限hook,封装一层包裹组件


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
// 控制显隐
return authorized ? children : null
}

使用


<AuthWrapper auth='edit'>
<button> 编辑按钮(AuthWrapper) </button>
</AuthWrapper>

还可以利用renderProps特性


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
+ if(typeof children === 'function'){
+ return children(authorized)
+ }
// 控制显隐
return authorized ? children : null
}

<AuthWrapper auth='edit'>
{
(authorized:boolean)=> authorized ? <button> 编辑按钮(rederProps) </button> : null
}
</AuthWrapper>

动态权限控制


这种主要是通过动态获取到的权限标识,来控制显隐、样式等。可以根据特定场景做特定的封装优化。主要逻辑其实是在后端处理。


结尾


可以看到在两大框架下实现权限控制时,思路和细节上还是稍稍有点不一样的,React给人的感觉是手上的积木更加零碎的一点,有些功能需要自己搭起来。相反Vue给人的感觉是面面俱到,用起来下限会更高。


最后


如果大家有什么想法和思考,欢迎在评论区留言~~。


另外:本人经验有限,

作者:JetTsang
来源:juejin.cn/post/7242677017034915899
如果有错误欢迎指正。

收起阅读 »

环信十周年趴——我的程序人生

        2011年,那时候刚上大学,计算机科学与技术专业,大家都是很迷茫的。尤其是大一的时候,面对十几门专业课程(《C语言程序设计》、《C++程序设计》、《微机原理》、《单片机原理》、《算法导论》、《数据结构》、...
继续阅读 »

        2011年,那时候刚上大学,计算机科学与技术专业,大家都是很迷茫的。尤其是大一的时候,面对十几门专业课程(《C语言程序设计》、《C++程序设计》、《微机原理》、《单片机原理》、《算法导论》、《数据结构》、《计算机原理》、《逻辑与数字电路》、《高数》、《线性代数》等)的时候,实在是没有一点儿想法。也不知道该学哪个,该丢哪个。但是要想不挂科,还是得雨露均沾,学习时间平均分配。印象最深的课程就是《C语言程序设计》,刚开始学真是一脸懵,这是啥,这玩意儿有啥用?用来干啥的?然后迫于对知识的渴求,努力学了,也是大学课程里面学得最好的专业,这也是后来为啥成为了iOS开发。

        我是一名曾经从事iOS开发工作的程序员,在这个行业中度过了多年的光阴。但是,在某个时刻,我的程序人生彻底转向了一个不同的方向,那就是我遭遇了落魄的iOS开发之路。

        落魄经历

        在我的iOS开发过程中,我也遇到了很多挑战和困难。其中最重要的一点是,随着竞争日益激烈,市场需求变得更加严格和复杂,我的职业前景开始黯淡下来。

        从那之后,我开始频繁地跳槽,但是并没有找到一个令我满意的岗位。在移动应用开发的生态系统中,iOS领域的变化是惊人的。新的技术和框架不断驱动和推动着市场和用户需求的变化,而这个速度比任何其他应用领域都要快。但我并没有保持这个变化的步伐,慢慢地,我的技能逐渐落后,导致我的职业发展受到了影响。

        在这个过程中,我开始感到自己正在与市场和软件发展的步伐背道而驰。我不再能够适应市场需求和客户的期望,我甚至感觉到自己的运气都已经耗尽了。

        逆境中的人生反思

        在落魄的状态下,我开始经历了一段自我反思的旅程。我开始回顾自己的职业生涯,思考我所从事的工作和做出的决策是否真的为我带来了成就感和满足感。我也开始考虑其他领域和技能的发展可能性。

        这段旅程让我意识到,“业内良心”(本意是良心味道的事物)这个说法是存在的,它深刻地体现了我在这个行业中的经历。与此同时,我也意识到,真正的成功无法用市场或行业发展的脉搏来衡量,而是要经由自己的内心感觉。

        在自我反省的过程中,我也发现了自己职业规划和发展的不足之处。我没有及时了解新技术和框架的发展趋势,并没有花费足够的精力和时间来提高自己的职业素养和思考能力。这让我在竞争激烈的市场中不断失利。

         今天,虽然我没有从事iOS开发了,但我始终没有忘记自己所学到的知识和经验。落魄的经历让我成为一个更好的Programmer,坚持自己的初心且不断进取。不管你们做着什么,无论遇到什么困境,都请不要磨灭自己的热情和信念。这就是我从我的落魄经历中得到的宝贵经验。


本文参与环信十周年活动 ,活动链接:https://www.imgeek.net/question/474026

收起阅读 »

Vue3项目实现图片实现响应式布局和懒加载

web
Element实现响应式布局 分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。 利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应...
继续阅读 »

Element实现响应式布局


分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。
利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应式的栅格布局,Element官方预设了5个响应式尺寸,官方给出了详细的属性解释。这个例子中我设置了4个尺寸。
在这里插入图片描述
栅格默认的占据的列数是24,设置24就是一列,设置12就显示两列,设置8就显示3列,设置6就显示4列,设置4显示6列......可以根据自己的场景需求来进行布局。这个例子中我设置的响应式布局如下:



:xs="12" 当浏览器宽度<768px时,一行展示2列

:sm="8" 当浏览器宽度>=768px时,一行展示3列

:md="6" 当浏览器宽度>=992px时,一行展示4列

:lg="{ span: '7' }" 当浏览器宽度>=1200px时,一行展示7列 这个需要在css样式中设置一下。



这里例子中的图片都是放在el-card中的,并且图片都是一样大小的。修改图片可以利用图片处理工具,分享一个自己常用的工具:轻量级图片处理工具photopea
Element的Card组件由 header 和 body 组成。 header 是可选的,卡片可以只有内容区域。可以配置 body-style 属性来自定义body部分的style。
:body-style="{padding:10px}" ,这个其实是对el-card头部自动生成的横线下方的body进行边距设置。也就是除了el-card的header部分,其余都是body部分了。
这里例子中没有头部,就是给卡片的body部分设置内边距。
在这里插入图片描述
具体代码如下所示:
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
图片效果如下所示:
当浏览器宽度>=1200px时,一行展示7列:
图片14.png


当浏览器宽度>=992px时,一行展示4列:


图片15.png
当浏览器宽度>=768px时,一行展示3列:


图片16.png
当浏览器宽度<768px时,一行展示2列:


图片17.png
接下来,优化一下页面,对图片进行懒加载处理。


图片懒加载


看下上面没有用于懒加载方式的情况,F12---NetWork---Img可以看到页面加载就会显示这个页面用到的所有图片。


图片18.png
可以利用vue-lazyload,它是一个Vue.js 图片懒加载插件,可以在页面滚动至图片加载区域内时按需加载图片,进一步优化页面加载速度和性能。采用懒加载技术,可以仅在需要加载的情况下再进行加载,从而减少资源的消耗。也就是在页面加载时仅加载可视区域内的图片,而对于网页下方的图片,我们滑动到该图片时才会进行加载。


下载、引入vue-lazyload


npm install vue-lazyload --save


在package.json中查看:


图片19.png
在main.ts中引入:


图片20.png


使用vue-lazyload


在需要使用懒加载的图片中使用v-lazy指令替换src属性。


图片21.png
也可以是下面的写法:


图片22.png
这样,就实现图片的懒加载了。
验证一下懒加载是否生效,F12---NetWork---Img,可以看到图片的加载情况。


一开始没有滑动到图片区域,就不会加载图片,可以在Img中看到loding占位图片在显示。


图片23.png
滑动到了对应的照片才会显示对应的图片信息。


图片24.png


图片25.png


图片26.png


作者:YM13140912
来源:juejin.cn/post/7242516121769033787
>这就实现了懒加载。

收起阅读 »

Android-apk动态加载研究

前言 近期工作中遇到两个问题。 换应用皮肤 加载插件apk中的view Android 换肤技术一文中已经详细说明了如何进行应用换肤。而加载插件apk中的view,利用前文提到的换肤技术,居然无法实现!仔细重新研究Android apk动态加载机制,有了新...
继续阅读 »

前言


近期工作中遇到两个问题。



  • 换应用皮肤

  • 加载插件apk中的view


Android 换肤技术一文中已经详细说明了如何进行应用换肤。而加载插件apk中的view,利用前文提到的换肤技术,居然无法实现!仔细重新研究Android apk动态加载机制,有了新的发现,而且还可以提高插件加载效率。


布局加载


Android 换肤技术文中提到的加载插件图片方法,无法加载插件的布局文件。怎么尝试都是失败。布局文件是资源中最复杂的,需要解析xml中的其它元素,虽然布局文件的id可以获取,但xml中其它元素的id或者其它关联性的东西仍然无法获取,这应该就是加载插件布局文件失败的原因。


宿主应用无法直播加载插件中的xml布局文件,换一个思路,插件将xml解析成view,将view传递给宿主应用使用。


插件apk需要使用插件context才能正确加载view,宿主如何生成插件context呢?

  public abstract Context createPackageContext(String packageName,
@CreatePackageOptions int flags)

context.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY |
Context.CONTEXT_INCLUDE_CODE);

使用上述方法可正确创建插件context。


除此之外还有一种方法(本人没有验证过),activity的工作主要是由ContextImpl来完成的, 它在activity中是一个叫做mBase的成员变量。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上context就是通过它们来获取资源的,这两个抽象方法的真正实现在ContextImpl中。也即是说,只要我们自己实现这两个方法,就可以解决资源问题了。我们在代码中可以创建activity继承类,重写对应方法即可。具体可参考 下文

/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();

动态加载方式


目前动态加载方式均是使用DexClassLoader方式获取对应的class实例,再使用反射调用对应接口,代码如下:

  DexClassLoader loader = new DexClassLoader(mPluginDir, getActivity().getApplicationInfo().dataDir, null, getClass().getClassLoader());
String dex = getActivity().getDir("dex", 0).getAbsolutePath();
String data = getActivity().getApplicationInfo().dataDir;
Class<?> clazz = loader.loadClass("com.okunu.demoplugin.TailImpl");
Constructor<?> constructor = clazz.getConstructor(new Class[] {});

这种方式存在一个问题,较为耗时,如果宿主管理着许多插件,这种加载方式就有问题,使用下面这种方式可加快插件的加载。

public void getTail2(Context pluginContext){
try {
Class clazz = pluginContext.getClassLoader().loadClass("com.okunu.demoplugin.TailImpl");
Constructor<?> localConstructor = clazz.getConstructor(new Class[] {});
Object obj = localConstructor.newInstance(new Object[] {});
mTail = new IPluginProxy(clazz, obj);
} catch (Exception e) {
Log.i("okunu", "ee", e);
e.printStackTrace();
}
}

注意,一定要使用插件的context为参数,它和插件的其它类使用同一个classloader,既然能获取插件classloader,则可以获取插件中的其它类。如果不使用插件context为参数,则上述方法一定会报错。


总结


针对插件资源加载,其实分为两种形式。



  • 宿主直接使用插件资源,比如使用插件图片、字符串等

  • 宿主间接使用插件资源,比如在宿主中启动插件activity或者显示插件的view


第1种形式,可以在宿主应用中构造AssetManager,添加插件的资源路径。


第2种形式,宿主创建插件context并传递给插件,插件使用自己的context则可自由调用自己的资源了,如何创建插件context前文详述了两种方法。


注意一点,宿主中肯定无法直接调用插件的R文件的。


动态加载apk,也有两种方式。



  • 使用DexClassLoader加载插件路径,获取插件的classLoader。

  • 使用已经创建好的插件context,获取插件的classLoader,效果和第1种一样,但速度要更快


动态加载apk机制还有很多东西可以深入研究,比如说插件activity的生命周期等等,这些内容后续补充。


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

Android屏幕刷新机制

基础知识 CPU、GPU CPU:中央处理器,主要负责计算数据,在Android中主要用于三大绘制流程中Surface的计算过程。 GPU:图像处理器,主要负责对图形数据进行渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buff...
继续阅读 »

基础知识


CPU、GPU



  • CPU:中央处理器,主要负责计算数据,在Android中主要用于三大绘制流程中Surface的计算过程。

  • GPU:图像处理器,主要负责对图形数据进行渲染,在Android中主要用于将CPU计算好的Surface数据合成后放到buffer中,让显示器进行读取呈现到屏幕上。


逐行扫描


屏幕在刷新buffer的时候,并不是一次性扫描完成,而是从左到右,从上到下的一个读取过程,顺序显示一屏的每个像素点,按60HZ的屏幕刷新率来算,这个过程只有16.66666...ms。



  • 从初始位置(第一行左上角)开始扫描,从左到右,进行水平扫描(Horizontal Scanning)

  • 每一行扫描完成,扫描线会切换到下一行起点,这个切换过程叫做水平消隐,简称 hblank(horizontal blank interval),并发送水平同步信号(horizontal synchronization,又称行同步)

  • 依次类推,整个屏幕(一个垂直周期)扫描完成后,显示器就可以呈现一帧的画面

  • 屏幕最后一行(一个垂直周期)扫描完成后,需要重返左上角初始位置,这个过程叫垂直消隐,简称 vblank(vertical blank interval)

  • 扫描线回到初始位置之后,准备扫描下一帧,同时发出垂直同步信号(vertical synchronization,又称场同步)。


image.png


显卡帧率


表示GPU在1s中内可以渲染多少帧到buffer中,单位是fps,这里要理解的是帧率是一个动态的,比如我们平时说的60fps,只是1s内最多可以渲染60帧,假如我们屏幕是静止的,则GPU此时就没有任何操作,帧率就为0.


屏幕刷新频率


屏幕刷新频率:屏幕在1s内去buffer中取数据的次数,单位为HZ,常见屏幕刷新率为60HZ。屏幕刷新率是一个固定值和硬件参数有关。也就是以这个频率发出 垂直同步信号,告诉 GPU 可以往 buffer 里写数据了,即渲染下一帧。


屏幕刷新机制演变过程


单buffer


GPU和显示器共用一块buffer


screen tearing 屏幕撕裂、画面撕裂


当只有一个buffer时,GPU 向 buffer 中写入数据,屏幕从 buffer 中取图像数据、刷新后显示,理想的情况是显卡帧率和屏幕刷新频率相等,每绘制一帧,屏幕显示一帧。而实际情况是,二者之间没有必然的大小关系,如果没有同步机制,很容易出现问题。

当显卡帧率大于屏幕刷新频率,屏幕准备刷新第2帧的时候,GPU 已经在生成第3帧了,就会覆盖第2帧的部分数据。

当屏幕开始刷新第2帧的时候,缓冲区中的数据一部分是第3帧数据,一部分是第2帧的数据,显示出来的图像就会出现明显的偏差,称为屏幕撕裂,其本质是显卡帧率和屏幕刷新频率不一致所导致。


双buffer


安卓4.1之前

基本原理就是采用两块buffer。

GPU写入的缓存为:Back Buffer

屏幕刷新使用的缓存为:Frame Buffer

因为使用双buffer,屏幕刷新时,frame buffer不会发生变化,通过交换buffer来实现帧数据切换。
什么时候就行buffer交换呢,当设备屏幕刷新完毕后到下一帧刷新前,因为没有屏幕刷新,所以这段时间就是缓存交换的最佳时间。

此时硬件屏幕会发出一个脉冲信号,告知GPU和CPU可以交换了,这个就是Vsync信号,垂直同步信号。
不可否认,双缓冲可以在很大程度上降低screen tearing错误,但是呢,还是会出现一些其他问题。


Jank 掉帧


如果在Vsync到来时back buffer并没有准备好,就不会进行缓存的交换,屏幕显示的还是前一帧画面,即两个刷新周期显示的是同一帧数据,称为Jank掉帧。


image.png
发生jank的原因是:在第2帧CPU处理数据的时候太晚了,GPU没有及时将数据写入到buffer中,导致jank的发生。

CPU处理数据和GPU写入buffer的时机比较随意。


Project Butter 黄油工程


安卓4.1
系统在收到VSync信号之后,马上进行CPU的绘制以及GPU的buffer写入。最大限度的减少jank的发生。


image.png
如果显卡帧率大于屏幕刷新频率,也就是屏幕在刷新一帧的时间内,CPU和GPU可以充分利用刷新一帧的时间处理完数据并写入buffer中,那么这个方案是完美的,显示效果将很好。


image.png
由于主线程做了一些相对复杂耗时逻辑,导致CPU和GPU的处理时间超过屏幕刷新一帧的时间,由于此时back buffer写入的是B帧数据,在交换buffer前不能被覆盖,而frame buffer被Display用来做刷新用,所以在B帧写入back buffer完成到下一个VSync信号到来之前两个buffer都被占用了,CPU无法继续绘制,这段时间就会被空着,于是又出现了三缓存。


三buffer


image.png
最大程度避免CPU空闲的情况。


Choreographer


系统在收到VSync信号之后,会马上进行CPU的绘制以及GPU的buffer写入。在安卓系统中由Choreographer实现。



  • 在Choreographer的构造函数中会创建一个FrameDisplayEventReceiver类对象,这个对象实现了onVSync方法,用于VSync信号回调。

  • FrameDisplayEventReceiver这个对象的父类构造方法中会调用nativeInit方法将当前FrameDisplayEventReceiver对象传递给native层,native层返回一个地址mReceiverPtr给上层。

  • 主线程在scheduleVsync方法中调用nativeScheduleVsync,并传入2中返回的mReceiverPtr,这样就在native层就正式注册了一个FrameDisplayEventReceiver对象。

  • native层在GPU的驱使下会定时回调FrameDisplayEventReceiver的onVSync方法,从而实现了:在VSync信号到来时,立即执行doFrame方法。

  • doFrame方法中会执行输入事件,动画事件,layout/measure/draw流程并提交数据给GPU。

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

Android边框裁切的正确姿势

前言 今天写什么呢,没有太好的思路,就随便写一些细节的点吧。 平时我们都会接触到的一个东西就是设置view的边缘为圆角,因为默认的直角比较难看,这个是涉及比较多的场景,其它当然也有一些场景需用到非正常边框的情况,也需要裁切。 1. 设置圆角边框 一般我们怎么设...
继续阅读 »

前言


今天写什么呢,没有太好的思路,就随便写一些细节的点吧。

平时我们都会接触到的一个东西就是设置view的边缘为圆角,因为默认的直角比较难看,这个是涉及比较多的场景,其它当然也有一些场景需用到非正常边框的情况,也需要裁切。


1. 设置圆角边框


一般我们怎么设置圆角边框的

<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff" />
<stroke
android:width="0.8dp"
android:color="#ffffff" />

<corners android:radius="10dp" />
</shape>

这是我们比较常做的设置边框圆角的操作,有没有过这样去设置会不会出问题?其实这样的操作只不过是改变背景而已,它可能会出现内部内容穿透的效果。


2. 使用ClipToOutline进行裁切


这个是android 5.0之后提出的方法,具体的操作是这样

public static void setRoundRect(View view) {
try {
view.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), 10);
}
});
view.setClipToOutline(true);
} catch (Exception e) {
e.printStackTrace();
}
}

可以看出就是调用了view的setOutlineProvider方法和setClipToOutline方法。看这个ViewOutlineProvider,它的注释是

Interface by which a View builds its Outline, used for shadow casting and clipping.


能明显看出它就是为了处理阴影和裁切的。其中我们要设置的话,主要是设置Outline outline这个对象,我们可以看看它所提供的方法


setRect


先随便拿一张图片表示原本的显示效果来做对比


lQDPJxak951EiLzNArPNBBuwlGgcKIdKCsUD55urdoAVAA_1051_691.jpg_720x720q90g.jpg


调用setRect给原图进行边缘裁切

outline.setRect(view.getWidth()/4, view.getWidth()/4, view.getWidth()/4 *3, view.getHeight()/4 * 3);

得到这样的效果,注意,我的原效果是贴边的,这些裁切之后发现是不贴边的


lQDPJxbScR-Lx7zNAtzNBDiwSuSMvAe6JokD55uq3UAVAA_1080_732.jpg_720x720q90g.jpg


setRoundRect的效果和setRect一样,就是多了一个参数用来设置圆角。这里就不演示了


setOval
调用setOval,它的传参和setRect一样

outline.setOval(view.getWidth()/4, view.getWidth()/4, view.getWidth()/4 *3, view.getHeight()/4 * 3);

可以看到效果


lQDPJw5Lp5oLqLzNAqnNBDiwjzj9J14HHxID55ur-8AVAA_1080_681.jpg_720x720q90g.jpg


发现再裁切尺寸的同时并且把图片切成圆形,我记得很早之前,还没毕业时做圆形头像的时候还需要引用别人的第三方,现在5.0之后直接调这个就行,多方便。当然现在头像都是用Glide来做。


setAlpha和setConvexPath也一样,etAlpha是设置透明度,setConvexPath是设置路径,路径和自定义view一样用Path,我这边就不演示了


3.总结


Outline相对于shape来说,是真正的实现边缘裁切的,shape其实只是设置背景而已,它的view的范围还是那个正方形的范围。最明显的表现于,shape如果内容填满布局,会看到内容超出圆角,而Outline不会。当然如果你shape配合padding的话肯定也不会出现这种情况。


使用Outline也需要注意,一般的机子会在当范围超过圆之后,会一直显示圆。比如你设置radius为50是圆角的效果,但是甚至成100已经是整个边是半圆,这时你设200会发现还是半圆,但是在某些机子上200会变成圆锥,所以如果要做半圆的效果也需要去计算好radius


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

Android与JavaScript通信(相互回调)

简述      在移动应用开发中,Android和JavaScript是两个常用的技术栈。Android作为主流的移动操作系统,而JavaScript则是用于网页前端和跨平台开发的脚本语言。为了实现更好的用户体...
继续阅读 »

简述


     在移动应用开发中,Android和JavaScript是两个常用的技术栈。Android作为主流的移动操作系统,而JavaScript则是用于网页前端和跨平台开发的脚本语言。为了实现更好的用户体验和功能扩展,Android与JavaScript之间的通信变得至关重要。本文将介绍Android与JavaScript之间的回调通信技巧


通信基础




  1. 通过 WebView 进行通信


    Android 的 WebView 组件提供了 evaluateJavascript 方法,该方法可以执行 JavaScript 代码并获取返回结果。我们可以利用这一特性实现 Android 和 JavaScript 之间的通信。具体实现步骤如下:



    1. 在 Android 代码中,通过 evaluateJavascript 方法执行 JavaScript 代码。




  2. 使用 JavaScriptInterface 实现通信


    我们可以使用 JavascriptInterface 接口实现JavaScript 与 Android 的通信。具体实现步骤如下:



    1. 在 Android 代码中创建一个类,实现 JavascriptInterface 接口。

    2. 在该类中定义需要供 JavaScript 调用的方法,并添加 @JavascriptInterface 注解。

    3. 在 JavaScript 中通过 window.AndroidFunction 对象调用 Android 代码中的方法,实现通信,其中AndroidFunction是注册JavascriptInterface时指定的, 如下所示:
      webView.addJavascriptInterface(new Object() {
      @JavascriptInterface
      public void jsCallback(String message) {
      // ...
      }
      }, "AndroidFunction");





Android调用JavaScript函数




  1. 忽略返回值

        val webView = findViewById<WebView>(R.id.webView)

    webView.evaluateJavascript("jsFunction('message')", null)




  2. 获取返回值

        val webView = findViewById<WebView>(R.id.webView)

    webView.evaluateJavascript("jsFunction('message')") { result ->
    Log.e("TAG", result)
    }




JavaScript调用Android函数

// 在JavaScript中调用Android函数,并传递参数
function callAndroidFunctionWithParameter() {
var message = "Hello from JavaScript!";
AndroidFunction.jsCallback(message);
}

在上述示例中,JavaScript函数callAndroidFunctionWithParameter()将参数message传递给Android函数jsCallback()


双向回调通信




  1. Javascript传递回调给Android


    上述的 AndroidFunction.jsCallback(message)方式目前只能传递字符串,如果不做特殊处理,是无法执行回调函数的, 执行 AndroidFunction.jsCallback(message)时,在Android中获取到的是字符串,这时将回调函数转换成回调令牌,然后通过令牌执行相应的回调函数,步骤如下:




    1. 在js中将回调函数转换成唯一令牌,然后使用map以令牌为key存储回调函数,以便让Android端根据令牌来执行回调函数

      const recordCallMap = new Map<string, Function>();

      // Android端调用
      window.jsbridgeApiCallBack = function (callUUID: string, callbackParamsData: any) {
      // 通过ID获取对应的回调函数
      const fun = recordCallMap.get(callUUID);
      // 执行回调 `callbackParamsData`回调函数的参数 可有可无
      fun && fun(callbackParamsData);
      // 执行完毕后释放资源
      recordCallMap.delete(callUUID);

      }

      function getUuid() {
      // 生成唯一ID
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/\[xy]/g, function (c) {
      var r = (Math.random() \* 16) | 0,
      v = c == 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
      });
      }

      // 统一处理调用Android的方法
      export function androidCall(funname: string, funData: string, fun: Function) {

      if (!AndroidFunction) {
      return;
      }

      const dataObj = {
      funName: funname,
      funData: funData
      }

      if (typeof fun === "function") {
      const funId = getUuid();
      Object.assign(dataObj, { funId });
      recordCallMap.set(funId, fun);
      }

      AndroidFunction.jsCall(JSON.stringify(dataObj))

      }



    2. 在Android端注册JavascriptInterface统一让js调用

      class JsCallbackModel {
      lateinit var funData: String

      lateinit var funId: String

      lateinit var funName: String
      }

      abstract class JsFunctionCallBack(var funId: String) {
      abstract fun callback(respData: String?)
      abstract fun callback()
      abstract fun callback(respData: Boolean)
      }

      class JavaScriptCall(private val webView: WebView) {
      private fun jsCall(funName: String, funData: String, jsFunction: JsFunctionCallBack) {
      when(funName) {
      "screenCapture" -> {
      screenshot(funData.toInt(), jsFunction)
      }
      }
      }

      @JavascriptInterface
      fun jsCall(data: String) {
      // 将json字符串解析成kotlin对象
      val gson = GsonBuilder().create()
      val model = gson.fromJson(data, JsCallbackModel::class.java)

      // 如果不存在函数名称 则忽略
      if (model.funName == "") {
      return
      }

      val jsFunction: JsFunctionCallBack = object : JsFunctionCallBack(model.funId) {
      override fun callback(respData: String?) {
      if (webView == null) {
      return
      }
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId', "$respData")", null)
      }
      }

      override fun callback() {
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId')", null)
      }
      }

      override fun callback(respData: Boolean) {
      if (funId.isEmpty()) {
      return
      }

      content.webView.post {
      webView.evaluateJavascript("jsBridgeApiCallBack('$funId', '$respData')", null)
      }
      }
      }

      jsCall(model.funName, model.funData, jsFunction);
      }

      private fun screenshot(quality: Int, jsFunction: JsFunctionCallBack) {
      // 执行逻辑...
      // 执行js回调
      jsFunction("base64...")
      }
      }




    3. 在js中传递回调函数调用Android截图

      function screenshot(): Promise<string> {
      return new Promise(resolve => {
      androidCall("screenshot", (base64: string) => resolve(base64))
      })
      }
      screenshot().then(base64 => {
      })





  2. Android传递回调给Javascript


    原理跟Javascript传递回调给Android是一样的,具体实现如下:




    1. Android端

      class JavaScriptCall(private val webView: WebView) {
      companion object {
      val dataF: MutableMap<String, (callData: String) -> Unit> = mutableMapOf()

      fun jsCall(webView: WebView, funName: String, funData: String, funHandler: (callData: String) -> Unit) {
      val ctx: MutableMap<String, String> = mutableMapOf()

      ctx["funName"] = funName
      ctx["funData"] = funData
      val uuid = UUID.randomUUID().toString()
      ctx["funId"] = uuid
      dataF[uuid] = funHandler

      webView.post {
      val gson = GsonBuilder().create()
      val json = gson.toJson(ctx)
      webView.evaluateJavascript("jsCall('$json')", null)
      }
      }
      }

      @JavascriptInterface
      fun androidsBridgeApiCallBack(callUUID: String, callData: String) {
      val funHandler = dataF[callUUID];

      if (funHandler != null) {
      funHandler(callData)
      dataF.remove(callUUID)
      }
      }
      }



    2. Js端

      function doSome(funId, data, callback) {
      // 执行逻辑...
      // 执行Android的回调函数
      callback(funId, data)
      }

      function androidFunction(funId, respData: any?) {
      AndroidFunction.androidsBridgeApiCallBack(funId, respData)
      }

      function androidCallback(funId, funName, funData, fun) {
      switch (funName) {
      case 'doSome': {
      doSome(funId, funData, fun)
      }
      }
      }

      window.jsCall = function (json: string) {
      const obj = JSON.parse(json)
      const funName = obj['funName']
      const funData = obj['funData']
      const funId = obj['funId']

      if (!funName) {
      return
      }

      androidCallback(funId, funName, funData, androidFunction)
      }



    3. 在Android中传递回调函数调用js的doSome

      JavaScriptCall.jsCall(webView, "doSome", "data") { callParam ->
      Log.e("tAG", "回调参数: $callParam")
      }

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

🚗我毕业/转行了,怎么适应我的第一份开发工作?

🚗我毕业/转行了,怎么适应我的第一份开发工作? 嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️ 最近一直在回顾自己的职业生涯,思考自己在这几年里做了什么、成为了什么,实现了什么,失去了什么。虽然一路上充满了挫折和困难,但我其实非常感恩最近几年自己的成长和突破。 在...
继续阅读 »

🚗我毕业/转行了,怎么适应我的第一份开发工作?


嗨,大家好!这里是道长王jj~ 🎩🧙‍♂️


最近一直在回顾自己的职业生涯,思考自己在这几年里做了什么、成为了什么,实现了什么,失去了什么。虽然一路上充满了挫折和困难,但我其实非常感恩最近几年自己的成长和突破。


在这几天的对职业生涯的思考中,我查阅了很多资料和观点,才有了之前那篇《🎖️怎么知道我的能力处于什么水平?我该往哪里努力?》的文章。


在那篇文章中,我从整个职业生涯的角度定义了开发人员可能会经历的各个阶段。


今天,我们来好好聊一聊,当我们因为各种原因,成为一名新晋专业开发者时,如何尽快适应这种变化。


对于那些从学生身份转变为专业开发者,或者是面临职业转行的人来说,希望我的文章可以给你们提供帮助和一些建议。


此外,如果你个人在职业生涯中已经超越了这个阶段(我相信在掘金的大部分人都是大佬了),请不要嫌弃我这篇文章初级,希望回顾一下仍然可能对你有所帮助。


当然,如果我的文章可以对你或其他处于类似情况的开发人员提供指导和帮助,那我会因为能够帮助到你和你的团队更加喜出望外!😄🚀


🤔转型成“初级开发工程师”,会遇到什么挑战?



这些观点源于:《🎖️怎么知道我的能力处于什么水平?我该往哪里努力?》




  1. 以前大部分时间可能是一个人写demo学习,但是现在意味着你需要适应团队协作,和小伙伴们一起完成任务。💪

  2. 以前一个人就能负责完一个不大的项目建设,但是现在项目的体量已经变得超级庞大,一个人根本搞不定!😱

  3. 现在你要学会进行跨领域沟通,就像是翻越一座高山,不仅要搞清楚别人的问题,还要让别人明白你的问题!💡

  4. 怎么在职场中向更高一级进发?就像是玩游戏一样,如何不断升级自己的技能,向着更高的目标冲刺!🚀


基于这些问题,接下来我们将逐一回答它们。


🤔我如何适应可能让我不开心的团队工作呢?


没错!在团队里合作肯定会有些不愉快的事情发生。想想看,世界上可不可能每个人都喜欢你呢?哈哈,当然不可能啦!你也不可能喜欢世界上各种各样的人。


所以,当你加入一个团队时,真的是进入了一个全新的领域!以前,你只需要和自己相处融洽,做错了就怪自己,做不到也只能责怪自己。但是现在,你需要接受别人的不完美,甚至要接受你自己可能把整个团队搞砸的事实。相信我,这真的是一件让人感到尴尬的事情。


我想,面对这些问题,我会鼓励你有意识地培养下面这两个方面的能力:😉💪


1. 学会有效沟通


当你参加各种开发流程中的会议时,一定要意识到会议的重点是什么。要不然这些会议就成了浪费你宝贵时间的活动,还不如在会议期间多写几行代码,多看几篇文章来得实际。


举个例子,每日晨会是每个敏捷团队都有的例会。在这个时候,如果你需要告诉你的领导你今天在做什么,请千万不要深入研究你正在做的事情的细节。(比如,这个模块有个bug一直调不通,你试了很多种方法在会上详细说明)


相反地,因为每日站会的作用是团队之间了解进度和提出问题的会议,你只需要简单说明你在做什么任务,需要什么帮助即可:



“我要修复移动端APP水印失效的问题,但是这个功能我不是很懂,找不到关键代码段,需要有人能帮我梳理一下。”



我在晨会时真的遇到过很多开发人员控制不住自己的发散思维,会讲到他们正在开发的技术细节。如果没有一个有权威的人及时中止这些无意义的展开,真的会浪费大家的时间和精力!


所以如果有可能的话,请花时间多了解对方的需求,让每次沟通都变得简单快速。这样做会让你看起来很干练且专业。😄👍


2. 克制情绪,有意识地锻炼自己的情商


我们都会有克制不住自己情绪的时候,特别是当生活不太顺心时,比如游戏五连败之后,第二天还要上班的情况下。


尤其是当你运气不好,恰好触碰到某人的敏感点,或者开个小小的玩笑,这就有可能无意中挑起双方的争端。


这时候,就是考验一个人情商的时候了。我建议你能尽力地鼓励你的同事,你的团队成员。当他们表现出不好的情绪时,有意识地用理解和支持的态度去面对。当你真的能做到这一点,你就为未来承担更大管理职责做好了最重要的情商储备。


如果你和你的同事确实遇到问题,请从问题本身开始分析,一个一个地解决事情而不是与人对立。要清楚你解决的是问题,而不是与你有冲突的那个人。



当然,如果你真的遇到了难以沟通的团队成员,请顺其自然,让时间或者等待Leader来解决这个问题。我并不倡导无休止的退让。



如果因为这些不可避免的摩擦影响了整个团队的氛围很长一段时间。你只能祈祷你的Leader可以很好的解决这个问题。😄


🤔我怎么在一个我完全看不懂的项目中显得专业?


现在咱们聊聊专业领域的事情吧,我想也是你最关心的问题!


当你加入一个团队时,最大的挑战可能不是适应团队,而是面对一堆看不懂的代码!


刚开始的时候,你可能会觉得自己还行,毕竟学过编程语言,不太可能完全看不懂。


但很快你就会发现处理项目代码跟写小小的Demo程序很不一样!有时候逻辑跳转起来像个迷宫,过几年再回头看,还是一头雾水啊!😵


在团队里,你会面对一个庞大的代码库,可不是一两年就能完全掌握的。又能能用两年时间彻底精通你公司的项目吗?有这种人吗?🤔


所以,当你接到第一个任务,投入开发的时候,别担心,你不是一个人陷入迷茫,我也是一样的。我们都曾经历过那个阶段,只需要时间去适应,一定不要觉得自己不行!


如果你想问有什么可以实践的方法论,我想我能给你的建议是:在进行需求评审的时候,写下所有你可能不理解的内容!


比如:




  • 哪些数据库需要我特别关注?🔍

  • 我需要关注哪些代码文件啊?📂

  • 项目里有没有类似的代码实践可以帮我解决这个需求?🤔

  • 需求里有没有没有说清楚的问题或者一些不够明确的要求呢?🌪️



当你问这些问题的时候,你的小伙伴一定会对你刮目相看哦!相信我,这可是难得一见的专业和细心的表现。(可不是每个人都能做到的!)


这些你整理出来的内容,在你整个开发过程中会给你巨大的支持。当别人想不起来一年都做了些什么事情的时候,这些记录可以让你在年终总结的时候,脱颖而出,变成你宝贵的项目经验! 📚


💪写一份成就清单,为未来赋能


如果可以的话,我建议你保留一份完成的需求任务日记或电子表格。而且,一定一定要记录下你取得的每一个新成就!


把成长当成一个游戏的过程!看着经验值一点点涨上去,发现世界里的新奇事物。


当我这样做的时候,每次一个新成就,都会我的幸福感简直爆棚!我会迫不及待地想要完成下一个新成就。



  • “我设计和开发了一个超棒的用户成就组件库!它提供了一致的界面风格和交互效果,减少了80%的代码冗余!” 🎉

  • “我成功实现了功能X,移动端新用户流入增长了整整120%!” 📈

  • “我掌握了localStorage,并巧妙地用它给功能X实现了用户本地数据缓存!” 💾


有时候,在过程中你可能觉得自己有些傻,但是等有一天你想回顾过去的1年或3年经历,或者当你又被互联网世界”卷“到,对自己失望的时候,你会发现原来那看似平凡无奇的职场生活中,你一直在默默成长。


当然,最重要的是,当你迎接人生中新的阶段,需要换工作或重新制作简历时,这些记录将带来巨大的帮助。你会惊讶地发现,你的项目经历比你想象的要丰富得多! 🌟📝


🥨学习如何提升自己的level



“Never Memorize Something That You Can Look Up” – Albert Einstein


”永远不要记住你可以查到的东西“ - 阿尔伯特·爱因斯坦



大部分的小伙伴们都很热衷于收藏那些像是"100个超牛JavaScript函数"或者"Vue3实用API大集合"这样的文章。


嗯,这真的很棒!我觉得这是个很好的习惯。比起死记硬背那些八股文,这样做要强太多了。因为只要你在需要的时候能找到它们,那它们就是你的宝藏了。


我是绝对反对八股文的开发者,但有时候,面对大环境,我们可能不得不做些妥协。为了面试,我们得背诵各种JavaScript高级函数和Vue生命周期都有什么用。


不过,如果你有时间想要提升自己,有空闲去思考进步的话,我建议你加强阅读。很多很多的阅读!


试试去阅读一些超出你舒适区和当前理解范围的书籍和课程吧!比如计算机组成原理、设计模式,或者现在非常热门的人工智能领域的基础书籍。


这样做可以拓宽你的思维,让你的知识领域更广阔。最终,你会逐渐掌握阅读的技巧,面对这些全新的知识领域时,能更快、更准确地找到重点并掌握它们。


这个过程会很痛苦,因为可能400个字的内容你都需要花一周的时间去消化。


但是只要你坚持下去,未来的你一定会与普通程序员拉开差距。


因为让你有价值的不是那些沉闷的八股文,而是你脑海中关于各个领域的认知和解决方案。


如果你能迅速解决别人不知道怎么办的问题,那你就是人群中那个最了不起的人,很多人会跟着指示做很多的需求,但是他们并不能形成解决方案。


解决方案,才是真正证明你实力的硬通货! 💪


🥩如果有机会,积极加入开发者社区


如果在我刚毕业的时候有人提醒我这个事情,我一定会非常感激他。


回顾我的职业生涯,我最后悔的一件事就是没有早点参与到开发者社区,无论是GitHub还是现在的掘金社区。


当你真正活跃在社区中,试图融入他们,你会结识新朋友,找到可以指导你的导师,让你能够突破当前的认知。你的未来将逐步变得清晰起来。


在你喜欢的领域中,找出谁适合成为你在特定领域发展的导师型开发者朋友。然后关注他们,开始阅读和评论他们的文章和作品,与他们展开讨论,加入他们目前的方向和事业!


最终,借助这个社区,你完全有可能进入职业生涯中一个全新的维度:




  • 你可以为一个开源项目做出属于自己的贡献(甚至是文档方面) 🚩

  • 与社区的开发者合作,发起一个全新的开源项目

  • 你将拥有自己高质量的小圈子💒



这样做会让你收获很多。你不仅能够积累宝贵的经验,还能与行业内优秀的开发者们互动,共同进步!🚀😄




🎉 你觉得怎么样?这篇文章可以给你带来帮助吗?如果你有任何疑问或者想进一步讨论相关话题,请随时发表评论分享您的想法,让其他人从中受益。🚀✨


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

谈知识付费与专家建议

我最近可能是病了,或者是心态出了问题。 我经常会听一些付费类的课程,也会在微信上阅读互联网大牛发表的文章。 以前听,还觉得挺有道理的。我甚至绘声绘色地转述给别人。可是后来,怎么听,怎么想怼他们。 可能是我的觉悟降低了,眼界不够。也可能是他们为了证明自己的观点,...
继续阅读 »

我最近可能是病了,或者是心态出了问题。


我经常会听一些付费类的课程,也会在微信上阅读互联网大牛发表的文章。


以前听,还觉得挺有道理的。我甚至绘声绘色地转述给别人。可是后来,怎么听,怎么想怼他们。


可能是我的觉悟降低了,眼界不够。也可能是他们为了证明自己的观点,刻意扭曲一些东西


所以,我打算摆出来,也让读者们帮我分析一下。


首先是我在听罗胖的跨年演讲时,他讲述疫情让大家挣不到钱,他鼓励大家改行,并讲了很多改行成功的故事。


他说南京有一位胡先生,是天文学博士。原本在大学教书,后来他转行去干装修了。还是那种带着劳保手套,搬砖动瓦的那种装修工作。


罗胖说,有人会觉得他得从头开始学吧?实际上不会。


他14年的天文学积累,形成的方法论,可以转到装修上。甚至还可以吊打普通装修工人,这叫降维打击。


首先,他作为天文学家,具有还原宇宙的能力。那么,他可以还原毛坯房的效果图。


这时候,我还能听得进去,觉得有点道理。


随后他说,作为科学家,还具有研究能力。两种建筑材料能不能混合,普通的装修工人靠得是经验。但是科学家,可以分析成分,采用科技手段进行判断。


听到这里,我有点坐不住了。他不是个天文学家吗?怎么又成了材料学家了?两个学科之间没有壁垒吗?


另外,关于乳胶漆和腻子粉混合的后果,科学实验室的数据,同装修工人传下来的经验,效果到底有多大差别?成本又有多大差别?


接下来,罗胖说的,把我的这种情绪推上顶峰。


罗胖说,天文学家转行装修工人,还解决了装修行业的一个痛点。



传统的装修流程,设计、采购、施工是分开的,出了问题会相互扯皮。



但是,天文学家与普通人相比,具有高度的统筹全局的能力、高效的沟通能力,他能把这三个流程都打通,马上就出类拔萃了。反而干得更好。


因此,他给大家的启发是:重要的不是身份,而是内核


我没有干过装修,但我找人装修过我家房子。因此经验很浅。就我个人了解,装修的采购是讲渠道的。有些行业老手拿货很便宜,比我自己去买要便宜很多。因此才有各司其职的划分,不是打不通,而是各自更专业。


他说身份不重要,但是在我的生活中,我感觉身份还是很重要的。甚至自考本科和统招本科,区别都很大。可能还是我境界太低了。


我听完上面的故事,没有再继续往下听。


我感觉可能是自己出问题了。我是无名小卒。人家的课程可是千万用户呢?其中,还不乏好多商界大老板。老板肯定是比我聪明的。


今天,我又从微信公众号看到一篇文章。


写这篇文章的是IT行业的大佬。我不认识。但是他的简介很厉害:国内知名IT管理专家、某甲创始人、某乙创始人,畅销书《xx之道》作者,曾担任大厂的各种总、各种O。


他写的体裁是关于人工智能方面的,题目大意是ChatGPT骗了全世界、你们都被ChatGPT骗了。


因为我就写人工智能代码,也写过关于AIGC原理解析的文章


我就点进去看,一看我就想怼。


他列举了很多条关于人工智能的谎言,然后自己再说出了真相。他揭穿谎言,警醒世人。


他指出现在流传的一个谎言:人工智能会代替人类的工作


他给出的真相是:抢饭碗的永远不是AI,而是会用AI的人


他也讲了一个故事。



原来有很多电话客服人员,负责推销、查询等工作。但是,现在90%的这类工作,已经被AI机器给替代了。那你说原来的客服都失业了吗?没有!你们不知道的是,他们有的转行做了AI训练师。也就是教AI怎么打电话。


你看,客服掌握了AI,重新上岗了。


而你,不愿意学习AI,只能被淘汰。



我想,这不还是人类的工作被替代了吗?只是因为AI的出现,衍生了一些周边岗位。


原来停车场收钱抬杆的大爷,因为车牌识别外加移动支付,他们下岗了。高速口的收费员,因为ETC的出现,也下岗了。


这确实是科技代替了人工。没见哪个大爷跑去教AI如何抬杆儿,就算有,能用得了那么多大爷吗?


AI客服出现的目的,就是提高效率,节省成本。花钱搞了AI客服,那些人工客服还会保留吗?要留也只能留少数一部分人。而大多数人会因为AI的出现,不得不重新选择工作。人工智代替人类的一些工作,就是时代的发展。


这位大佬,最后总结:淘汰你的永远不是这个时代,是你自己。不管时代如何,你不努力,被淘汰很正常


我的思想又活跃了,很想怼。


我感觉,我们的生活深受时代的影响。就像是蚂蚁的生活,也会受大雨、野火、巨兽的踩踏影响一样。


蚂蚁努力积累食物,突然有一天,某个小孩朝着洞穴撒了一泡尿,或者扔了一个鞭炮,蚂蚁的努力就白费了。


这种破坏和是否努力是没有关系


我估计,专家也会怼我:



你早应该有预判,提早发现熊孩子的破坏行为。


从他今天在家多喝了水,还买了鞭炮开始,就该猜到他的这类行为。


这叫见微知著,未雨绸缪。其实,归根到底,还是你不够努力!



其实,他们说的对,没有错。错在哪里?难道不应该努力吗?但是这些话,对你用处不大。


我发现,引发我想怼欲望的事情。大多都是在宣传一件事,那就是人的力量是无限大的。


关系不重要,环境不重要,行业不重要,你的努力最重要!


早年听陈安之的成功学,他说成为马拉松冠军是世上最简单的事情。只需要一句口诀,那就是当你跑不动、心肺难以支撑的时候,大声喊出:我没有心脏,我没有心脏!


我当时想,没有心脏不就倒下了吗?


这是正能量。正能量永远没有错。


你想赚大钱,请了一个大师。


大师传授你三个独家秘诀:



第一,要有强烈赚钱欲望;


第二,要对赚钱抱有持之以恒的行动;


第三,赚到钱时一定不要骄傲,继续赚。



你感觉很有道理,但是收入依然没有什么改变。可能是因为你不够努力。


你想学舞蹈,找了一个大师,大师说,想成为舞蹈高手,首先要有强烈跳舞的欲望,第二要持之以恒的行动,第三要戒骄戒躁。


还是一样的话。


请问,他们错了吗?没错。你成功了吗?没有。


听过马三立的一个相声段子。



说有个穷人,穷的揭不锅了。实在没办法,就去摆摊。他摆了一个算命的摊子,也兼职修鞋。


来算命的人少,挣钱多。来修鞋的人多,挣钱少。两者互补。


有一个人来算命,想发财。这个算命的就指点他去东北,那里能发财。


然后,又跟他说,去东北得走不少路,加固下鞋子才能成功抵达。



我想,现在很多课程可能也是这样。


他们一方面宣传你的遭遇跟环境无关,另一方面强调是你不够努力,让你焦虑。同时,他还有课程,你买了课程就算是努力了


至于最后的结果,他们是不关心的。不成功,可能是你还不够努力。


你自己的事情,需要自己去思考。每个人的境遇和你不一样。


那些行业大佬们,可能没有去小摊上吃过油条,也没有逛过装修市场去买瓷砖。甚至他们的父辈也没有过类似经历。他们更精于顶层建筑。但是有时候,他必须要写相关的文章。于是,他们举的例子,更多的是一种导向,是劝人向善。


而我们,也不要过于盲从他们的指导。他们的意见参考一下就好了。


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

程序员你为什么迷茫

今天在知乎收到一个问题:「程序员你为什么迷茫?」,看到这问题时突然间「福灵心至」想写点什么,大概是好久没写这种「焦虑文」,想想确实挺久没更新《程序员杂谈》 这个专栏了,那就写点「找骂」的东西吧。 其实也不算「贩卖焦虑」,主要是我一直不卖课也不做广告,就是纯...
继续阅读 »

今天在知乎收到一个问题:「程序员你为什么迷茫?」,看到这问题时突然间「福灵心至」想写点什么,大概是好久没写这种「焦虑文」,想想确实挺久没更新《程序员杂谈》 这个专栏了,那就写点「找骂」的东西吧。




其实也不算「贩卖焦虑」,主要是我一直不卖课也不做广告,就是纯粹想着写点东西,不过如果你看了之后觉得「焦虑」了,那也没有「售后服务」。



当我看到「程序员你为什么迷茫?」这个问题的时候,我第一想法居然是:大概是因为预期和结果不一致


现在想想确实好像是这样,在步入职场之前,你一直以为程序员是一个技术岗,是用一双手和一台电脑,就可以改变世界的科技岗位,是靠脑吃饭,至少在社会地位上应该是个白领。


但是入职后你又发现,明明是个技术岗位,干的却是体力活,别人在工地三班倒,而你是在电脑前 996,唯一庆幸的是你可以吹着空调,目前的收入还挺可观。



但是现在看来,程序员的职业生涯好像很短,农民工少说可以干到 40 多,为什么程序员 35 就需要毕业再就业?明明一心一意搬砖,说好奋斗改变人生,最后老板却说公司只养有价值的人,而有价值的人就是廉价而又精力充沛的年轻人


那时候你也开始明白,好像努力工作 ≠ 改变人生,因为你努力改变的是老板的人生,工作带来的自我提升是短暂的,就像工地搬砖,在你掌握搬砖技巧之后,剩下的都只是机械性的重复劳动,机械劳动的勤奋只会带来精神上的奋斗感,并不致富,而对比工地,通过电脑搬砖需要的起点更高,但是这个门槛随着技术成熟越来越低,因为搜索引擎上的资源越来越多,框架和社区越来约成熟,所以🧱越来越好拿,工价也就上不去了,甚至已经开始叫嚣用 AI 自动化来代替人工。



其实对于「老人」来说,这个行业一开始不是这样,刚入行的时候到处在抢人,他们说这是红利期,那时候简历一放出来隔天就可以上岗,那时候的老板每天都强调狼性,而狼需要服从头领,只有听话的狼才有肉吃,所以年轻时总是充满希望,期待未来可以成为头狼,也能吃上肉住炕头。


虽然期间你也和老板说过升职加薪,但是老板总是语重心长告诉大家:



年轻人不好太浮躁,你还需要沉淀,公司这是在培养你,所以你也要劳其筋骨,饿其体肤,才能所有成就,路要一步一走,饭要一步一吃,总会有的。



当然你可以看到了一些人吃到了肉,所以你也相信自己可以吃到肉,因为大家都是狼,而吃到肉的狼也在不停向你传递吃肉经验:



你只需要不停在电梯里做俯卧撑,就可以让电梯快一点到顶楼,从而占据更好的吃肉位置,现在电梯人太多了,你没空间做俯卧撑,那就多坐下蹲起立,这样也是一种努力。




直到有一天,公司和你突然和你说:



你已经跟不上公司的节奏了,一个月就请了三次病假,而且工作也经常出错,所以现在需要你签了这份自愿离职协议书,看在你这么多年的劳苦功高,公司就不对你做出开除证明,到时候给其他人也是说明你是有更好机会才离职的,这样也可以保全你的脸面。



而直到脱离狼群之后你才明白,原来沉淀出来的是杂质,最终是会被过滤掉,电梯空间就那么点,超重了就动不了,所以总有人需要下来


所以你回顾生涯产生了疑惑和迷茫:程序员不是技术岗位吗?作为技术开发不应该是越老越值钱吗?为什么经验在 3-5 年之后好像就开始可有可无,而 35 岁的你更是被称为依附在企业的蛀虫,需要给年轻人让路。


回想刚入行那会,你天天在想着学习技术,无时无刻都在想着如何精通和深入,你有着自己的骄傲,想着在自己的领域内不断探索,在一亩三分地里也算如鱼得水,但是如今好像深入了,为什么就开始不被需要了


回过头来,你发现以前深入研究的框架已经被替代,而当年一直让他不要朝三暮四嚼多不烂的前辈,在已经转岗到云深不知处,抱着一技之长总不致于饿死是你一直以来的想法,但是如今一技之长好像补不上年龄的短板。



如今你有了家庭,背负贷款,而立之年的时候,你原以为生涯还只是开始,没想到早已过了巅峰,曾经的你以为自己吃的技术饭,做的是脑力活,壮志踌躇对其他事务不屑一顾,回过头来,如今却是无从下手,除了在电脑对着你熟悉的代码,你还能做什么?放下曾经的骄傲,放下以往的收入去吃另一份体力活?



但是不做又能怎样?提前透支的未来时刻推着你在前行,哪怕前面是万丈深渊。



所以以前你认为技术很重要,做好技术就行了,但是现在看,也许「技术」也只是奇技淫巧之一,以前你觉得生育必须怀胎十月,但是现在互联网可以让十个孕妇怀胎一月就早产,这时候你才发现,你也没自己想象中的重要。


所以你为什么迷茫?因为到了年龄之后,好像做什么都是错的,你发现其实你并没有那么热爱你的职业,你只想养家糊口,而破局什么的早就无所谓了,只要还能挣扎就行。



所以我写这些有什么用?没什么用,只是有感而发,大部分时候我们觉得自己是一个技术人才,但是现在看来技术的门槛好像又不是那么的高,当技术的门槛没那么高之后,你我就不过是搬砖的人而已,既然是搬砖,那肯定是年轻的更有滋味


所以,也许,大概,是不是我们应该关心下技术之外的东西?是不是可以考虑不做程序员还能做什么?35 岁的人只会越来越多,而入行的年轻人也会越来越多,但是近两年互联网的发展方向看起来是在走「降本增效」,所以这时候你难道不该迷茫一下?


程序员是迷茫或者正是如此,我们都以为自己是技术人,吃的是脑力活,走的是人才路,但是经过努力才知道,也许你的技术,并没有那么技术。


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

思考 | 闲话工作

工作五年有余,似有感悟,可是每每提笔时,脑袋又一片混沌。然而胸中总有些东西不吐不快,如鲠在喉,如芒在背。尤其去年年末和一位好友聊至深夜,席间的一番话令我思绪万千,怀念起曾经那个稚嫩青涩的我。 我想是时候该记录一些东西,哪怕这些东西是混乱的,潦草的。这些内容看似...
继续阅读 »

工作五年有余,似有感悟,可是每每提笔时,脑袋又一片混沌。然而胸中总有些东西不吐不快,如鲠在喉,如芒在背。尤其去年年末和一位好友聊至深夜,席间的一番话令我思绪万千,怀念起曾经那个稚嫩青涩的我。


我想是时候该记录一些东西,哪怕这些东西是混乱的,潦草的。这些内容看似和工作相关,实则背后都是人生的态度,价值的选择。它们写给路上的伙伴,更写给未来的自己。毕竟,在这慌乱走过的人生中,如果不留些印记,回头顾盼时将只剩茫然。


人生重要的是选择,还是努力?


这个问题的答案取决于你如何理解人生。当下流行的回答是“选择”,而我的回答是“努力”。


我打小生活在小镇上,属于严格意义上的小镇青年。条件谈不上优渥,但也算不上艰辛。或许得益于良好的家庭氛围,我的内心从未有过“短缺感”。所以那种“不上进”的小富即安的状态,在我看来颇为自足。穷人和富人都有烦恼,也都有欢乐。二者体验到的事物可能有差别,但通过事物体验到的快乐未必有差别。这就好比躺在豪车和草地上都不是快乐,但是躺下之后能够心无旁骛地哼着小曲,这是快乐的。


我选择“努力”,是因为我更欣赏把一件事做到极致的态度。这可能和我的父亲有关。他是一名柴油机修理工,从小教导我的就是“要么不做,要么做到极致”的人生态度。在他30多年的修理生涯里,一直引以为豪的就是自己的修理质量。别家修好的机器可以管一到两年,他修好的可以管三到五年。要说这其中有什么秘诀,说破天也只有“用心”二字。你说赛道的选择重要么?当然重要。但是各行各业都要有人做,“择一行,爱一行,精一行”的态度,才是更普适,更让每个人都心安的选择(我讨厌当下价值体系的一个主要原因就是它不普适,它只让少数人心安)。


我选择“努力”,也是希望做到尽量务实。因为看重选择,背后的心理通常是从众。什么是好选择?是媒体口中的?长辈口中的?还是内心深处的?社会风向变来变去,时间一久就容易浮躁。浮躁到后来只关注结果而忽略过程。不过事情的发展却很玄妙,你越是盯着某样东西,就越是得不到它。你紧盯着财富,往往也得不到财富。


我选择“努力”,还因为在我看来选择是自然发生的。太过刻意的选择,会有反向的作用。譬如,当一个人修行不足,内心不够坚定时,过高的荣誉和财富都会将他推向深渊。他会面临更多的诱惑,更多的苛责,处理不好便会失去原先宝贵的家庭和健康。因此,但行好事,莫问前程。


接着谈谈工作中的一些感受。


我日常的工作是处理稳定性问题。当一个问题暴露出来后,多数人在流程中唯一的作用就是施压和传话,他们只关心问题有没有解决,何时解决。如果这个问题不再出现,那么围观的人群将立即散开。它仿佛一个从未被打开过的黑盒,被一群人用灼热的目光炙烤后,又无情地抛弃。从始至终,没有人关心过它的前世今生,它的前因后果。当我们碰到新问题时总希望能举一反三,然而举一反三的前提一定是充分理解这个“一”。否则所有的问题在我们脑中只是漂浮的孤岛。可是现实就是很多问题被我们当成了黑盒。或许是手头的任务太重,或许是兴趣寥寥,总之愿意追本溯源,探究举一反三中的“一”的人很少。


这个话题再延展一下,国内很多所谓的科技公司都偏好商业运营而轻技术。它们眼中的技术只是实现业务的手段,或许它们更应该叫做“消磨时间公司”、“线上百货公司”或者“跑腿服务公司”。我知道这话一说完,肯定有人要站起来说:好个不懂商业,不懂管理的小白!懂也好,不懂也罢。然而我知道一个浅显的道理:一个饭馆想要生意好,就应该把精力重点放在提升口味上,而不是放在店面装修和营销广告上。同理,国内的科技想要真有起色,就要把技术当成技术,而不是其他目的的附庸。也唯有把技术当成技术,我们才能尊重技术。同样位于东亚的日本人,或许正是因为身上有比我们更为专注的匠人精神,才能在高端科技占有一席之地。


此外还有一个工作责任心的问题。


最低一个等级的责任心是“太极推手”,遇到棘手任务时想的是如何脱责。他们工作的目标就是将棘手问题转移出去,只处理那些简单不费脑且容易产生汇报成绩的活。这种人每个公司都不少,君不见那些人一天邮件好多封,一看内容全空空。


稍微高一个等级的责任心是“听命行事”,老板让我干我就干,至于干的质量和结果如何,抱歉,不在本人考虑范围内。他们每天没有主见地做事,付出了时间,但未必付出努力。


再稍微高一点等级的责任心是“自扫门前雪”,他们对自己所负责的领域普遍有了主人翁意识,在意别人对自己领地的评价,因此比较尽心尽责。不过这份尽责也止于自己领地的边界,越线的部分他是压根不会伸手。


再往上一个等级的责任心是“舍我其谁”,那些无人负责却又对公司有利的事情谁来干?这帮人往往冲在前面。他们对边界以外且力所能及的事情愿意伸手,也更享受为公司带来利益后的价值认同。


最高一个等级的责任心是“社会主人”,如果说之前的责任心还仅仅局限在工作和利益的维度,那么这个等级将会扩大到社会责任。他会思考自己的工作对社会所产生的影响,以及个人能力在其中起到的正面作用。


一个人选择什么样的责任心,通常是性格和价值观的产物。公司可以通过绩效这根大棒来影响员工的责任心,但从员工个人角度来看,它不会是决定性因素。


今日且谈到此,希望这篇文章也能开个先河,让自己在输出技术内容的同时多一些个人思考。当然,文中观点尚且稚嫩,还请各位看官不吝赐教。


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

Gradle 浅入浅出

环境介绍 OpenJDK 17.0.5 Gradle 7.6 示例代码 fly-gradle Gradle 项目下文件介绍 如果你的电脑安装了 gradle,可以使用 gradle init 去初始化一个新的 gradle 工程,然后使用电脑安装的 gra...
继续阅读 »

环境介绍



Gradle 项目下文件介绍


如果你的电脑安装了 gradle,可以使用 gradle init 去初始化一个新的 gradle 工程,然后使用电脑安装的 gradle 去执行构建命令。


但是每个开发电脑上的 gradle 版本不一样,为了统一构建环境,我们可以使用 gradle wrapper 限定项目依赖 gradle 的版本。

# 会生成 gradle/wrapper/* gradlew gradlew.bat
gradle wrapper --gradle-version 7.5.1 --distribution-type bin
.
├── build.gradle
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle

gradlew 和 gradlew.bat


运行项目 wrapper 下定义的 gradle 去构建项目。


gradlew 是 macos 和 linux 系统下。


gradlew.bat 是 windows 系统下使用的。


wrapper


wrapper-workflow.png


wrapper 定义项目依赖那个版本的 gradle,如果本地 distributionPath 没有对应版本的 gradle,会自动下载对应版本的 gradle。

# gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
# 如果是国内项目,只需要修改这个url 就可以提高下载速度
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

GRADLE_USER_HOME 没有配置的话,默认是 ~/.gradle


zipStoreBasezipStorePath 定义了下载的 gradle (gradle-7.6-bin.zip) 存储的本地路径。


distributionBasedistributionPath 定义下载的 gradle 解压的本地目录。


gradlew 实际是运行 gradle-wrapper.jar 中的 main 方法,传递给 gradlew 的参数实际上也会传递给这个 main 方法。


gradle-wrapper.jar 会判断是否下载 wrapper 配置的 gradle,并且将传递参数给下载的 gradle,并运行下载的 gralde 进行构建项目。


升级 wrapper 定义的 gradle 版本

./gradlew wrapper --gradle-version 7.6

settings.gradle

pluginManagement {
repositories {
maven {
url="file://${rootDir}/maven/plugin"
}
gradlePluginPortal()
}
plugins {
// spring_boot_version 可以在 gradle.properties 配置
id 'org.springframework.boot' version "${spring_boot_version}"
}
}
rootProject.name = 'fly-gradle'
include 'user-manage-service','user-manage-sdk'
include 'lib-a'
include 'lib-b'

settings.gradle 主要用于配置项目名称,和包含哪些子项目。


也可以用于配置插件的依赖版本(不会应用到项目中去,除非项目应用这个插件)和插件下载的


build.gradle


build.gradle 是对某个项目的配置。配置 jar 依赖关系,定义或者引入 task 去完成项目构建。


gradle.properties


主要用于配置构建过程中用到的变量值。也可以配置一些 gradle 内置变量的值,用于修改默认构建行为。

org.gradle.logging.level=quiet
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xms512m -Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

org.gradle.jvmargs 用来配置 Daemon 的 JVM 参数,默认值是 -Xmx512m "-XX:MaxMetaspaceSize=384m"


当我们的项目比较大的时候,可能会由于 JVM 堆内存不足导致构建失败,就需要修改此配置。


org.gradle.logging.level 调整 gradle 的日志级别。参考 gradle logging 选择想要的日志级别。


Gradle Daemon


为加快项目构建,gralde 会启动一个常驻 JVM 后台进程去处理构建,Daemon 进程默认三小时过期且当内存压力大的时候也会关闭。


Gradle 默认会启用 Daemon 进程去构建项目。

# 查看 daemon 运行状态
./gradlew --status
# stop daemon 进程
./gradlew --stop
# 重启 daemon 进程
./gradlew --daemon

构建生命周期


Gradle 是基于 task 依赖关系来构建项目的,我们只需要定义 task 和 task 之间的依赖关系,Gradle 会保证 task 的执行顺序。


Gradle 在执行 task 之前会建立 Task Graphs,我们引入的插件和自己构建脚本会往这个 task graph 中添加 task。


task-dag-examples.png


Gradle 的构建过程分为三部分:初始化阶段、配置阶段和执行阶段。


初始化阶段



  • 找到 settings.gradle,执行其中代码

  • 确定有哪些项目需要构建,然后对每个项目创建 Project 对象,build.gradle 主要就是配置这个 Project 对象
// settings.gradle
rootProject.name = 'basic'
println '在初始化阶段执行'

配置阶段



  • 执行 build.gradle 中的配置代码,对 Project 进行配置

  • 执行 Task 中的配置段语句

  • 根据请求执行的 task,建立 task graph
println '在配置阶段执行 Task 中的配置段语句'

tasks.register('configured') {
println '在配置阶段执行 Task 中的配置段语句'
doFirst {
println '在执行阶段执行'
}
doLast {
println '在执行阶段执行'
}
}

执行阶段


根据 task graph 执行 task 代码。


依赖管理


Maven 私服配置


我们一般都是多项目构建,因此只需要在父项目 build.gradle 配置 repositories。

allprojects {
repositories {
maven {
url "${mavenPublicUrl}"
credentials {
username "${mavenUsername}"
password "${mavenPassword}"
}
}
mavenLocal()
mavenCentral()
}
}

credentials 配置账号密码,当私服不需要权限下载的时候可以不配置。


Gradle 会按照配置的仓库顺序查询依赖下载。


配置依赖来自某个目录

dependencies {
compile files('lib/hacked-vendor-module.jar')
}
dependencies {
compile fileTree('lib')
}

有的时候第三方库没有 maven 供我们使用,可以使用这个。


依赖冲突


默认依赖冲突


::: tip
当出现依赖冲突的时候,gradle 优先选择版本较高的,因为较高版本会兼容低版本。
:::

dependencies {
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.google.code.findbugs:jsr305:3.0.0'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

我们可以执行下面命令查看项目依赖的版本:

./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
| +--- com.google.guava:failureaccess:1.0.1
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- com.google.code.findbugs:jsr305:3.0.2
| +--- org.checkerframework:checker-qual:3.12.0
| +--- com.google.errorprone:error_prone_annotations:2.11.0
| \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.2

我们可以看到,gradle 选择了 com.google.code.findbugs:jsr305:3.0.2 这个版本。


强制使用某个版本


如果我们想使用 com.google.code.findbugs:jsr305:3.0.0 版本

dependencies {
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.google.code.findbugs:jsr305:3.0.0', {
force = true
}
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
./gradlew -q dependency-management:dependencyInsight --dependency jsr305 --configuration compileClasspath
com.google.code.findbugs:jsr305:3.0.0 (forced)
Variant compile:
| Attribute Name | Provided | Requested |
|--------------------------------|----------|--------------|
| org.gradle.status | release | |
| org.gradle.category | library | library |
| org.gradle.libraryelements | jar | classes |
| org.gradle.usage | java-api | java-api |
| org.gradle.dependency.bundling | | external |
| org.gradle.jvm.environment | | standard-jvm |
| org.gradle.jvm.version | | 17 |

com.google.code.findbugs:jsr305:3.0.0
\--- compileClasspath

com.google.code.findbugs:jsr305:3.0.2 -> 3.0.0
\--- com.google.guava:guava:31.1-jre
\--- compileClasspath

禁用依赖传递


guava 不会传递依赖它依赖的库到当前库,可以看到

dependencies { 
implementation 'com.google.guava:guava:31.1-jre', {
transitive = false
}
implementation 'com.google.code.findbugs:jsr305:3.0.0'
}
./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
\--- com.google.code.findbugs:jsr305:3.0.0

可以看到 guava 依赖的 jar 没有传递到当前项目中来。


排除某个依赖


Guava 依赖的别的 jar 可以传递进来,而且排除了 findbugs, 项目依赖的版本为 3.0.0

dependencies { 
implementation 'com.google.guava:guava:31.1-jre', {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
implementation 'com.google.code.findbugs:jsr305:3.0.0'
}
./gradlew dependency-management:dependencies --configuration compileClasspath

------------------------------------------------------------
Project ':dependency-management'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-dependencies:3.0.2
+--- com.google.guava:guava:31.1-jre
| +--- com.google.guava:failureaccess:1.0.1
| +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
| +--- org.checkerframework:checker-qual:3.12.0
| +--- com.google.errorprone:error_prone_annotations:2.11.0
| \--- com.google.j2objc:j2objc-annotations:1.3
\--- com.google.code.findbugs:jsr305:3.0.0

可以看到 guava 传递到当前项目的依赖少了 findbugs


配置依赖之间继承

configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}

configurations.all {
resolutionStrategy {
force 'org.apache.tomcat.embed:tomcat-embed-core:9.0.43'
}
exclude group: 'org.slf4j', module: 'slf4j-simple'
}

api 和 implementation 区别


jar b 包含一下依赖

dependencies {
api 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.google.guava:guava:31.1-jre'
}

项目 a 引入 jar b ,commons-lang3 和 guava 都可以被工程 a 使用,只是二者 scope 不一样。


api 对应 compile,在 工程 a 可以直接使用,编译可以通过。


implementation 对应 runtime,编译找不到 guava 中的类。


Task


我们引用的插件实际是添加 task 到 task graph 中去。


我们知道 build.gradle 实际是用来配置对应项目的 org.gradle.api.Project。


因此我们可以在 build.gradle 中引用 org.gradle.api.Project 暴露的属性。


我们可以在 gradle dsl 和 Project 接口中可以知道可以访问哪些属性。


tasks 实际就是 Project 暴露的一个属性,因此我们可以使用 tasks 往当前项目中注册 task。

tasks.register('hello') {
doLast {
println 'hello'
}
}

推荐使用 tasks.register 去注册 task,而不是 tasks.create 去直接创建。


tasks.register 注册的 task 只会在用到的时候才会初始化。


Groovy 闭包


Groovy Closures

{ [closureParameters -> ] statements }

闭包的示例

{ item++ }                                          

{ -> item++ }

{ println it }

{ it -> println it }

:::tip


当方法的最后一个参数是闭包时,可以将闭包放在方法调用之后。


:::


比如注册一个 task 的接口是

register(String var1, Action<? super Task> var2)

tasks.register("task55"){
doFirst {
println "task55"
}
}

tasks.register("task66",{
doFirst {
println "task66"
}
})

Task Type


gradle 已经定义好一些 task type,我们可以使用这些 task type 帮助我们完成特定的事情。比如我们想要执行某个 shell 命令。


Exec - Gradle DSL Version 7.6

tasks.register("task3", Exec) {
workingDir "$rootDir"
commandLine "ls"
}

Plugin


插件分类


Gradle 有两种类型的插件 binary plugins and script plugins


二进制插件就是封装好的构建逻辑打成 jar 发布到线上,供别的项目使用。


脚本插件就是一个 *.gradle 文件。


buildSrc


一般我们的项目是多项目构建,各个子项目会共享一些配置,比如 java 版本,repository 还有 jar publish 到哪里等等。


我们可以将这些统一配置分组抽象为单独的插件,子项目引用这个插件即可。便于维护,不用在各个子项目都重复配置相同的东西。


buildSrc 这个目录必须在根目录下,它会被 gradle 自动识别为一个 composite build,并将其编译之后放到项目构建脚本的 classpath 下。


buildSrc 也可以写插件,我们可以直接在子项目中使用插件 id 引入。

buildSrc/
├── build.gradle
├── settings.gradle
└── src
├── main
│   ├── groovy
│   │   └── mflyyou.hello2.gradle
│   ├── java
│   │   └── com
│   │   └── mflyyou
│   │   └── plugin
│   │   ├── BinaryRepositoryExtension.java
│   │   ├── BinaryRepositoryVersionPlugin.java
│   │   └── LatestArtifactVersion.java

buildSrc/build.gradle


groovy-gradle-plugin 对应的是使用 groovy 写插件。


java-gradle-plugin 对应 java

plugins {
id 'groovy-gradle-plugin'
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
helloPlugin {
id = 'com.mflyyou.hello'
implementationClass = 'com.mflyyou.plugin.BinaryRepositoryVersionPlugin'
}
}
}

我们就可以在子项目使用插件

plugins {
id 'com.mflyyou.hello'
id 'mflyyou.hello2'
}

插件使用



  • Applying plugins with the plugins DSL
plugins {
id 'java'
}


  • Applying a plugin with the buildscript block
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
}
}

apply plugin: 'com.jfrog.bintray'


  • Applying a script plugin
apply from: 'other.gradle'

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

[Git废弃提交]需求做一半,项目停了,我该怎么废弃commit

Git
在实际开发中我们要拥抱变化。我们都知道需求它是很不稳定的,在实际开发过程中会经常改变。经常会遇到一个需求,已经开始进入开发阶段了,开发到一半的时候说这个功能不需要了。甚至会出现我们已经实现某一个功能,然后被告知,这个功能被砍掉了。 那么针对这种代码已经写了,现...
继续阅读 »

在实际开发中我们要拥抱变化。我们都知道需求它是很不稳定的,在实际开发过程中会经常改变。经常会遇到一个需求,已经开始进入开发阶段了,开发到一半的时候说这个功能不需要了。甚至会出现我们已经实现某一个功能,然后被告知,这个功能被砍掉了。


那么针对这种代码已经写了,现在要废弃的情况我们应该怎么操作呢?


当然,如果这个功能都在一个单独的分支上,且这个分支只有这个功能的代码,那么可以直接废弃这个分支。(这也是为什么会有多种Git工作流的原因,不同的软件需求场景,需要配合不同的工作流。)


代码还没有提交


如果代码还在本地,并没有提交到仓库上。可以用reset来重置代码。
git reset是将当前的分支重设(reset)到指定的commit或者HEAD(默认),并且根据参数确定是否更新索引和工作目录。

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]
# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard
# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset [commit]
# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard [commit]
# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git reset --keep [commit]

其实reset也可以指定某个commit,这样就会重置到对应的commit,该commit之后的commit都会丢失。如果你没法确定这些commit中是否有需要保留的commit,就不要这样操作。


如果代码已经提交


如果代码已经提交了,且提交的分支上还有其他的代码。那么操作起来就比较麻烦。我们需要用revert来删除。


revert


git revert命令用来「撤销某个已经提交的快照(和reset重置到某个指定版本不一样)」。它是在提交记录最后面加上一个撤销了更改的新提交,而不是从项目历史中移除这个提交,这避免了Git丢失项目历史。

# 生成一个撤销最近的一次提交的新提交
git revert HEAD
# 生成一个撤销最近一次提交的上n次提交的新提交
git revert HEAD~num
# 生成一个撤销指定提交版本的新提交
git revert <commit_id>
# 生成一个撤销指定提交版本的新提交,执行时不打开默认编辑器,直接使用 Git 自动生成的提交信息
git revert <commit_id> --no-edit

比如我现在的dev分支,最近3次提交是"10","11","12"。



我现在要11这个提交去掉,我就直接revert "11" 这个commit



运行后,他会出现一个冲突对比,要求我修改完成后重新提交。(revert是添加一个撤销了更改的新提交)


这个提交,会出现"10"和"12"的对比。



修改完对比后,我们commit这次修改。



看下日志,我们可以看出,revert是新做了一个撤销代码的提交。



撤销(revert)被设计为撤销公共提交的安全方式,重设(reset)被设计为重设本地更改。
因为两个命令的目的不同,它们的实现也不一样:重设完全地移除了一堆更改,而撤销保留了原来的更改,用一个新的提交来实现撤销。「千万不要用 git reset 回退已经被推送到公共仓库上的提交,它只适用于回退本地修改(从未提交到公共仓库中)。如果你需要修复一个公共提交,最好使用 git revert」。


rebase


前面课程说过,rebase是对已有commit的重演,rebase命令也可以删除某个commit的。他和reset一样,删除掉的commit就彻底消失了,无法还原。
具体用法,这里不在再次介绍,可以去看前面的课程。


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

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

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

一.事件分发传递流程


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



  1. 事件分发阶段


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


具体流程如下:




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




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




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




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




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




  6. 事件捕获阶段




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


具体流程如下:




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




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




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




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




  5. 事件处理阶段




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


具体流程如下:




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




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




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




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


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


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



  1. 触摸事件监听器


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

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


  1. 键盘事件监听器


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

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


  1. 手势事件监听器


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

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

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

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

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


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

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

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

1. 项目的结构




image.png


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


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


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




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


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


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


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


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




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

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


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

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

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

为什么要不靠工作赚钱


一个关于时间的故事


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


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


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


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


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


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


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


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


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


工作大概率不能带来快乐


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


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


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


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


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


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


工作无法带来安全感


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


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


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


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


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


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


不靠工作赚钱


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


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


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


怎样才能不靠工作赚钱


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


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


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


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


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


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


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


寻找好的副业方向


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


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


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


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


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


程序员可以考虑的方向


1. 投资


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


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


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


2. 做在线课程


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


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


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


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


3. 做自媒体


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


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


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


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


4. 运营自己的产品


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


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


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


更多副业方向


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


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


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


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


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


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


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


欢迎大家评论区交流呀~


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

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

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

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



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

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

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

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


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




事情起因是这样的:


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


:我没有闲鱼啊?


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


image.png


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


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


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




把我的沸点搬到小红书


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


:?我没有小红书啊?


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


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


image.png


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




闲鱼再次被挖出来


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


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


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


但是几天之后:


image.png


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


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


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




冒充我的人被揪出来了


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


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


image.png


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


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


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


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


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


image.png




牵扯出一堆小号


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


直到我看到了这两条:


image.png


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


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


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


image.png


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




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




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




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




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


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




处理结果


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



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





我再叨叨几句




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




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




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




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




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






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


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


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

特斯拉靠谱不靠谱 ?

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


刹车问题


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


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




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

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


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



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


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


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


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


长途问题


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



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


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


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


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


能耗问题


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




  • 315+118=433


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




  • 368-315=53


成本问题


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



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


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



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


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


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

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

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

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


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


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


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


持续的学习能力



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



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


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


高强度抗压力



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



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


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


枯燥乏味生活


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


高额经济支出


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


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


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


短暂职业生涯


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


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


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


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


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


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


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


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


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


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


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


要么转行,要么深造,

Yo,这是程序员的故事

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

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

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

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

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

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

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

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

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

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

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

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

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

这是程序员的故事。

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


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


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

程序员增强自控力的方法

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

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


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


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


2. 掌控情绪


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


3. 管理焦虑和压力


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


4. 培养自律习惯


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


5. 自我反思和反馈


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


6. 持续学习和自我发展


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


结论


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

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

收起阅读 »

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

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

背景原因



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



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



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


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


项目启动


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


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


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


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



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



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



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



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


开搞


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


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


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

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


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


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


第一个项目


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



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


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



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


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


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


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


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



实现目标的主要部分


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



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


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



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


接下来


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



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

感谢大家阅读~

收起阅读 »

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

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

image.png




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



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




为什么有大有小啊?


95的够大了吧


步入前端也才不到3年


So一个大龄的小前端


技术有长进么?


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


有长进么?




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




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




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




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




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




  6. node学了一点又忘了




  7. ts会的多了一点




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




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




生活上有什么说的呢?


生活很好 吃喝不愁


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


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


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


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


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




image.png


2023要怎么做?


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



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

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

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

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





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




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

  2. css多学点

    1. css in js

    2. Tailwindcss

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

    4. css 相关配置要会



  3. react源码要搞一下

    1. fiber

    2. hooks

    3. diff

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



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

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

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


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

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

收起阅读 »

转转商品到手价

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

1 背景介绍


1.1 问题




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




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




image


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


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


image


2 设计目标



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

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


3 技术方案核心点


3.1 影响因素


image


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




  1. 商品,发布或改价;




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




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




3.2 计算公式


image


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


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


4 落地过程及效果


image


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


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


4.1 v1.0流程


image


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




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




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




4.2 v1.0任务及接口


image




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




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




4.3 v2.0设计


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



  • 实时处理部分


商品上架/商品改价;


商品加入/踢出活动;


商品加入/踢出会馆;


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


image


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



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


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


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


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


image


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


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

image


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


queue1=[infoId...]

queue2=[infoId...]

...

queue9999=[infoId...]

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


image


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


注意事项:




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




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




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




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




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




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




4.4 v3.0设计


image


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


image


解决办法:




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




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




5 总结




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




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




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




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







关于作者



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



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


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

收起阅读 »

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

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

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


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


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


Kong Gateway (Open Source)


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


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


Tyk Open-Source API Gateway


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


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


KrakenD Open-Source API Gateway


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


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


Gravitee OpenSource API Management


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




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




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




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




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


Apinto Microservice Gateway


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


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


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


Apache APISIX API Gateway


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


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


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


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


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

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

收起阅读 »

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

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

前言


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


image.png


工作


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


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


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


image.png


image.png


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


学习


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




  1. 收货



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

    • 学习了vue3

    • 学习了react(不精通)

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

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




  2. 不足



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

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




生活


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




  1. 吐槽


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


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


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




  2. 平淡


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


    图片放到文章最后啦!




总结


曾经看到的一句话:



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



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



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



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


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


晒图



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


fa2b81d23593eb83b48f18246442024.jpg


0e22613cabf1391c8ee71653afb9c0b.jpg


7413ffa236e32ab90cffd36ed6d9fed.jpg


e77b1233aa72d335cfd5e236b03a5e5.jpg
304e9e51a55173f4ff234a8e27ae0eb.jpg



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


b705e7f1638136458d54638e7891c06.jpg


54c1fb0813c4df0e117ab323769b663.jpg


e564f7e708f55b817e0e87fb7f44f17.jpg


0f3e70a4f93e0230def4773ac5489c3.jpg


a16ec85ebee0af6bba8460459863fcd.jpg


a088d73521a1bec7e750a786305ffb1.jpg
3. 吃吃喝喝


f9f79ad3138bf3d0054f2b9bbe4f933.jpg


bdf75dd09aea6c79bce24789a157727.jpg


8feeb742aac2475e6f55e05bcce63b0.jpg

收起阅读 »

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

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

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


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


image.png


image.png


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


初始变换


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

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


Animation.gif


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


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


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


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


拆分路径点


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

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


叠加试验


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

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

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


Animation.gif


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


FIFO先进先出


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


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

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


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

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


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

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


Animation.gif


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


摇摆动画


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

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


Animation.gif


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


写的十分简便,一点小创

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

收起阅读 »

图像识别,不必造轮子

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

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


首先预览下效果


图片


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


准备工作


1、注册百度账号


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


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


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


图片


图片


图片


已创建好的应用列表


代码部分


1、获取access_token值


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


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


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


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


PHP获取access_token代码示例:


<?php

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

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

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

return $data;
}

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

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

var_dump($res);

?>

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


图片


2、图片上传及识别


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


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


代码如下:


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

<script>

  function imageUpload(imgFile) {

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

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

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

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


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

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

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


              console.log(msg.result)

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


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

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

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

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


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



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


        }
  });


   }



</script>
</head>
<body>

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



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


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

        </ol>
    </span>

</div>



</body>
</html>

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


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


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


ai.baidu.com/docs#/Image…


PHP请求代码示例:


<?php  

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

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

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

            return $data;
        }

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


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

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


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

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

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


?>

结语补充


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

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

收起阅读 »

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

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

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


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


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


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


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


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

收起阅读 »

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

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

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



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



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

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


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


入门程序员


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


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



懂得都懂不过多,解释


初级程序员


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


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


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


点击进入


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


中级程序员


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


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


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



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

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

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

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


高级程序员


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


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


软件开发工程师


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


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


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


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


对自己会有更高的要求。


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




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




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




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




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


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


点击进入


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

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

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

一、自身情况


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



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

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

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

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

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

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


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


image.png


二、大环境情况




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




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




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




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




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




三、未来出路


未来的出路在哪里?


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


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


image.png


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


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



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

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

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

  4. 开滴滴网约车。

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


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


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

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

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

flutter有哪些架构的框架?


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



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

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

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


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



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

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

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

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


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


Flutter BLoC


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


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


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


如何选择使用


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


GetX和BLoC的优缺点


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


GetX


优点:


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


缺点:


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


BLoC


优点:


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


缺点:


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


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

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

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

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


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


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



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

  • 现代化 Android 开发:数据类

  • 现代化 Android 开发:逻辑层

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

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

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

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


Scope


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



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

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


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


scope



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

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

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


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


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

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


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



架构分层


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



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


BookLogic 为例:


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

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

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

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

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

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

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

Compose 界面再使用 ViewModel

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

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


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


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

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

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

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



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



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


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


第一道题目是算法题:


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

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

console.log(result);

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


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


第二道题目是场景题:


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


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


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


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


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


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


一面:




  • 自我介绍


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




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


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




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



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




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



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




  • 说一下 useMemouseCallback 有什么区别



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




  • 说一下 useEffectuseLayoutEffect 有什么区别



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




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



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




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



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




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




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




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




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




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




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






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



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




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



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




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



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




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




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


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




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




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




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




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






      2. 性能问题




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




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










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



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




  • 你有什么想问我的吗?



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





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



二面:




  • 自我介绍



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




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



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




  • object 的原型指向谁?



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




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



    • 磕磕绊绊背了一段八股文,这里省略吧。




  • node 的内存管理跟垃圾回收机制有了解过吗?




    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:




    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间




      • 新生代空间: 用于存活较短的对象




        • 又分成两个空间: from 空间 与 to 空间




        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC






      • 老生代空间: 用于存活时间较长的对象




        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%




        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行










  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。




  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。




  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。




  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。




  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)




  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。




  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。




  • 追问:http 是在哪一层实现的?



    • 应用层。




  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……




  • 说一下浏览器输入 url 到页面加载的过程:




    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面




    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。




    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……






  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。




  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。




  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。




  • 最后问了期望薪资什么的,然后就结束了。




二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ

作者:三年没洗澡
链接:https://juejin.cn/post/7239715208792342584
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

TypeScript中的枚举,点燃你的代码创意!

前言 枚举是 TypeScript 中一个非常有趣且实用的特性,它可以让我们更好地组织和管理代码。 什么是枚举? 在 TypeScript 中,枚举(Enum)是一种用于定义命名常量集合的数据类型。它允许我们为一组相关的值赋予一个友好的名字,从而使代码更加可读...
继续阅读 »

前言


枚举是 TypeScript 中一个非常有趣且实用的特性,它可以让我们更好地组织和管理代码。


什么是枚举?


在 TypeScript 中,枚举(Enum)是一种用于定义命名常量集合的数据类型。它允许我们为一组相关的值赋予一个友好的名字,从而使代码更加可读和易于理解。枚举可以帮助我们避免使用魔法数值,提高代码的可维护性和可读性。


枚举的基本用法


让我们从枚举的基本用法开始,以一个简单的例子来说明。

enum Direction {
Up,
Down,
Left,
Right,
}

在这个例子中,我们定义了一个名为 Direction 的枚举,它包含了四个值:Up、Down、Left 和 Right。这些值默认情况下是从0开始自增的索引值。


我们可以使用枚举中的值来进行变量的赋值和比较。

let myDirection: Direction = Direction.Up;

if (myDirection === Direction.Up) {
console.log("向上");
} else if (myDirection === Direction.Down) {
console.log("向下");
}

在这个例子中,我们声明了一个名为 myDirection 的变量,并将其赋值为 Direction.Up。然后,我们使用 if 语句对 myDirection 进行比较,并输出相应的信息。


枚举的进阶用法


除了基本的用法外,枚举还有一些进阶的用法,让我们一起来看看。


1. 指定枚举成员的值


我们可以手动为枚举成员指定具体的值,而不是默认的自增索引值。

enum Direction {
Up = 1,
Down = 2,
Left = 3,
Right = 4,
}

在这个例子中,我们手动指定了每个枚举成员的值。这样,Up 的值为1,Down 的值为2,依此类推。


2. 使用枚举成员的名称


我们可以使用枚举成员的名称来访问其值。

enum Direction {
Up,
Down,
Left,
Right,
}

console.log(Direction.Up); // 输出

: 0
console.log(Direction[0]); // 输出: "Up"

在这个例子中,我们分别通过成员的名称和索引值来访问枚举成员的值。


3. 枚举的反向映射


枚举还具有反向映射的特性,可以通过值找到对应的名称。

enum Direction {
Up,
Down,
Left,
Right,
}

console.log(Direction.Up); // 输出: 0
console.log(Direction[0]); // 输出: "Up"

在这个例子中,我们通过 Direction.Up 输出了 0,通过 Direction[0] 输出了 "Up"。这种反向映射可以在某些场景下非常有用。


总结


枚举是一种用于定义命名常量集合的数据类型,可以帮助我们更好地组织和管理代码。我们了解了枚举的基本用法,以及一些进阶的技巧,如指定枚举成员的值、使用枚举成员的名称和枚举的反向映射。


希望能够帮助到大家更好地掌握 TypeScript 中的枚举,并在实际开发中灵活运用。


链接:https://juejin.cn/post/7241590185845145659
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

年后被吊打的第一面

背景 base重庆,面试中高级,目标先检验一下自己的水平和能力顺便看看薪资,好直接开始把。 自我介绍 讲了一下自己的技术栈:掌握vue全家桶,底层及上层框架、掌握react底层原理、熟悉js、熟悉工程化、熟悉微信小程序、使用过node、关注前端趋势有开源经历、...
继续阅读 »

背景


base重庆,面试中高级,目标先检验一下自己的水平和能力顺便看看薪资,好直接开始把。


自我介绍


讲了一下自己的技术栈:掌握vue全家桶,底层及上层框架、掌握react底层原理、熟悉js、熟悉工程化、熟悉微信小程序、使用过node、关注前端趋势有开源经历、主方向工程化等。


大概说了1分钟把,可能是我一边自我介绍一边在笑,面试官就问了一下:“你看起来心态很好啊!你是不是要写面经啊?”


我:“我是紧张才笑,应该是可以吗?要不多问点把~”


面试官:“可以,行”


记住这句话,我现在很后悔


步入正题


面试官:"那我们先来点基础把",下面都是我原话,小伙伴们可以纠正一下。


JS浏览器基础篇


1、dom树是怎么生成的


"浏览器是多进程架构,而其中有一个渲染进程,负责页面的渲染和js脚本的执行,而在渲染进程中有一个HTML解析器,oh对还有一个网络进程,网络进程负责根据content-type创建渲染进程,然后渲染进程用类似stream流管道那种接字节流将它解析为dom"


“而解析时,我觉得可以对标现在的各种转移编译工具,都有一个词法分析、语法分析、transfrom、genoretor的流程”


“你能具体说说这个过程吗?”


“(心理活动:当时脑子就蒙了、有点超纲啊、我要不猜一下)稍等我想一下、跟babel应该很像吧、会对一些声明命令赋值注释分词,这块我不是很了解,但应该对于html分词就是分的标签和文本内容,然后再通过算法去转换成dom”


“行,你上面提到了分析器,那如果当分析器遇到了script标签那”


“(心理活动:......这八股文味不对劲啊),我不知道对不对,但表现的是当遇到了scrpit会暂停html转换dom,去解析jascript,而async和defer会异步加载不会阻塞html转换”。


2、渲染进程


还可以你可以自信点虽然有些地方不是很对,但已经够用了。刚听你说了渲染进程,你说说它下面的几个线程把。


“emmm,下面的主线程吗,有主线程、GUI渲染线程、事件触发线程、定时器触发线程(后面发现漏了一个http线程),em,主线程和GUI是互斥的、js执行太长会造成页面渲染卡顿,但现在有很多解决方案,比如:在react中的调度器预留的5ms空闲时间、web worker之类的。然后是事件触发线程和定时器线程都是在任务队列去做Loop”


“行,那事件循坏我就不问你了,问问你V8的垃圾回收把”


“(你问啊!你问啊!)”


3、v8垃圾回收


“首先js因为是单线程,垃圾回收会占用主线程,导致页面卡顿,所以需要一个算法或者说策略,而v8采用的是分代式回收,而垃圾回收在堆中分成了很多部分用作不同的作用(我在说什么啊!当时),回收主要表现在新老生代上,新生代就活得短一点的对象,老生代就活得长一点的对象。


“在新生代里有一个算法,将新生代分成了两个区,一个FORM,一个TO,每次经过Scavenge会将FORM区中的没引用的销毁,然后活着的TO区调换位置,反复如此,当经过一次acavange后就会晋升的老生代还有个条件就是TO区的内存超过多少了也会晋升。”


“而老生代,采用标记清除和标记整理,但标记清除会造成内存不连续,所以会有标记整理取解决掉内存碎片,就是清理掉边界碎片”


“为什么TO超过25%要晋升老生代?标记清除是怎么清除的?”


“不知道~”


“第一个问题是为了不影响后续FORM空间的分配,第二个问题你应该看过有关这方面的文章把,垃圾回收会构建一个根列表,从根节点去访问那些变量,可访问到位活动,不可就是垃圾”


4、浏览器缓存


就强制缓存,协商缓存,浏览器内存那些,有兴趣看看文章,讲细点就行。写着太累了,当时讲了一大滩,直接说面试官问题把。


“因为提到了这些缓存,你觉得他们对于我们实际的业务场景下怎么运用”


“(蒙蔽),很大一部分是浏览器优化,一些http缓存我们可以做一些控制,本质上我感觉这些都属于性能优化的部分。”


“行”


5、JS上下文执行栈和闭包


“几个概念把,esc、上下文:作用域链,AO/VO,this。esc存储执行的上下文”


“算了我以一个函数来说把,主要是创建和执行。假设有一个A函数,过程是这样的创建全局执行上下文、压入esc、全局上下文初始化、执行A函数、创建A函数执行上下文,压入esc,A函数上下文初始化,这个初始化过程是这样的:创建作用域链、emm我上面提漏了一个A函数被创建全局上下文被保存到scope中的过程,是复制scpoe创建作用域链,用arguments创建活动对象,初始化活动对于,将活动对象压入链顶,执行完毕,上下文弹出。”


“但是全局上下文一直在栈底,而VO和AO的确认,我感觉是取决是是否可访问的。”


“而闭包就是上下文链中上下文scope被销毁了,但因为保持了对scope中某个变量的引用,这应该就是你上面说的回收原理的根节点实现的这个东西把,导致没销毁干净,留存在了内存中,完成了闭包”


“你怎么看待闭包的副作用”


“emmm,其实我觉得闭包是语言特性,虽然有副作用但我觉得其实挺好的,但只要管理好它就好了。况且又不是只有闭包会造成这些问题,就比如:react里面还有专门去清理一些链表和难回收的东西,去帮助v8回收。我觉得这得取决于写代码的人。”


“可以的,我感觉你的基础还是挺好的,你说下es6的东西把,控制下时间”


“你想听哪方面的那?因为东西太多了”


“工程化把,因为我前面听你介绍主方向是工程化”


“(...我怎么感觉工程化相关的只有一个esm模块化啊,这个怎么分类啊)esm:异步加载、引入抛出,编译时,值的引用。大概就这些东西把,其他的不知道了”


“行”


“那您觉得还有哪些那”


“就比如:Promise和async之类的啊”


“(..................)”


“来手写几道题把”


6、bind pipe compose 深拷贝


这个网上太多了,大伙自己去看。


CSS基础篇


“我可能CSS有点烂,但基础的应该都知道”


“先说说BFC把”


1、BFC


“BFC给我的感觉就像是个封闭盒子,不会在布局上影响到外面的元素。平常会触发BFC比较多的就是body,浮动元素、绝对定位、flex、overflow之类的。在BFC可以清除浮动、阻止元素被浮动环绕之类的。(然后我一边说一边尴尬的笑)”


“大概知道你CSS是个啥水平了,简单问点把,你说BFC可以清除浮动吗?为什么?”


“不知道”


“其实准确的说不是清除,是因为浮动元素也是BFC,两个BFC互不影响。你提到了BODY触发BFC?”


“emmm,可能是也许不是BODY,是HTML根元素”


“是的,不是BODY”


2、居中


"flex布局,positon,flex margin:auto,position transform,table-cell"


"行了,层叠上下文和层叠顺序"


3、层叠上下文


“(.......这时候感觉每多问我一个CSS都是煎熬啊),em我其实一般对于会遇到有层叠上下文不清晰的情况都是指定z-index.”


“行”


4、flex布局


“这样吧,你平时用得比较多的是什么布局?”


“flex布局把”


“那我们来聊一下flex布局把”


“(拜托我真的会哭,我感觉面试官他好兴奋),emmmm,我觉得布局无非是控制内部元素和控制容器,而flex布局对于容器的控制是基于轴这个概念的,而flex中的轴分为:主轴、垂直轴、换行轴。”


“主轴指的就是元素排列的方向轴,而flex-direction是最重要的属性在主轴中,row和col控制角度,reverse控制方向,但我们其实平时用得比较多的就默认的row和column,column会把元素垂直去排列。而主轴的另一个属性justify-content是控制元素在轴上的排列,然后我说了一下常用的几种就start end center between around”


“垂直轴就是垂直于主轴的方向轴~然后我停了大概有20秒(又开始笑了)”


“没了?”


“(啊啊啊啊啊!)可能就我只知道align-items控制垂直轴上的位置,然后说了下,start end center。”


“还有换行轴那”


“嗷对,就是刚才提漏了,垂直轴是针对于当前行,但换行轴是针对于整个容器。”


“这个针对怎么说?你继续说换行轴属性。”


就是高度嘛,布局换行后,垂直轴的高度只会是当前行高度。flex-wrap,但我只用过wrap,emm对于控制内部容器我了解得很粗浅。


"你可以了解一下wrap-reverse,下来可以去看一下正负剩余空间和flex-grow flex-shrink这些"


“抢答!就是flex:1这种写法的哈”


“对的,你知道吗”


“不知道”


我们两个同时沉默了(啊!!!!!!!!)。


"没事其实比我想象得稍微好一点,至少你在你不擅长的东西上也是花了时间去学的,css就不问了,下面问点框架把"


“谢谢!谢谢!谢谢!”


框架基础篇


“你简历里是React和Vue都会,那先说说你是怎么看这两个框架的把”


1、对React和Vue的看法


“在开发模式上,React发明了JSX这种开发模式,并且用了很多年时间让社区去接受,而Vue则是采用现成的模版语法的开发模式。但感觉就这两年这两个框架都在往一个函数式组件的方向靠,不应该说靠是已经实现了,Vue3中的函数式组件甚至在某种层面上说比react更自由。当然洛现在声明式的编程是主流嘛”


“在实现层面上说的话,就那一套,单向数据流,双向绑定,数据不可变性,更智能的依赖收集,优化的方式不同等”


“听到你说了更自由和智能的依赖收集,具体指的是?”


"比如react useEffect要手动加依赖,但vue3的wachEffect和computed就不用"。


“自由,em就比如,hook得顶层,不要条件循坏用,而且react重心智负担:就闭包陷阱那一套过时变量嘛,依赖的选择,还有重复渲染这些问题,我一直不理解为什么不把这些心智负担收到框架里,我觉得react是有这个能力的。vue3的话,你想咋用咋样api咋样,setup也会只用一次,也不会像新旧树对比hook多次调用”


“哈哈感觉你对react怨气好大,因为我看你文章写了很多react源码的嘛,如果是你你会怎么去收敛这个心智负担到框架内部?”


(.......绷不住了啊,吹过头了),稍等想1分钟,就是hook顶层那个和条件循坏应该不好动,因为react不能去破坏hooks链表的结构,对于过时变量react18已经将usestate全改为异步了,依赖的选择的心智问题我觉得是否说可以更明确一点再未来加入配置项的话,将依赖的收集在updateEffect和mountedEffect前去提前收集,就做一个依赖系统专门去处理这个事情,感觉可以从编译器自动生成依赖数组,现在react只是一层浅比较。但其实这么想,大部分问题的根源,是React函数组件机制所限:每次组件渲染,组件里的所有代码都会被重新调用一次,在这上面其实也可以动下手(自己说了感觉当没说,感觉好尴尬啊硬吹)。就长话短说就是,react的心智模型让我要花很多精力去处理边界情况。


“其实你最后句话说得挺好的,因为react要求你用声明式的方式去写组件,你就不该去纠结引用会不会变,多添加个依赖很不舒服,重新渲染这种事情,你需要保证的是无论重新渲染多少次这个组件都不会变。假设你useEffect依赖于AB,但你的B就可能只在首次创建后永远不变,它确实显得很“多余”但你不用纠结这个。真正的问题可能就在于你觉得的永远不会变只是你觉得,我们平时出现问题很多都是这种以为的边界问题导致B变造成的”


2、为什么react需要合成事件


“兼容性的考虑把,可以抹平不同浏览器事件对象差异,还有个就是避免了垃圾回收。”


“我们公司主要是Vue,你的简历里Vue也更擅长一些,我们谈一下Vue把”


3、生命周期


随便了一下,每个生命周期,父子生命周期,每个生命周期的定义和写法。


4、路由


5、指令


6、响应式原理


7、数组处理


8、key,diff算法


9、V3组合式API


10、一些TS系统


11、V3编译原理


“上面也问了挺多的了,你讲讲Vue3里面的模版编译和函数式组件编译把。”


“(我崩不住了,妈妈我想回家)先巴拉巴拉扯了一下pnpm和menorepo和整体v3源码,然后讲到complier,同样vue模版的编译也是通俗流程就是parse transform gen,我没具体研究过,但parse应该也是对标签属性文本指令等一系列做处理去干成AST,然后transform做转换最后生成。”


“行上午先面到这,二面通知你,有什么要问的吗?”


“em,你觉得我咋样(我好直白~)”


“挺不错的,2年经验,感觉你知识的掌握程度大于你的年限,有点好奇平时你怎么安排学习时间的”


“就可能目前这家公司比较清闲把,再加上学习写代码对我比较快乐,就能投入很多时间和精力,哎,就二面能不能快点!可以加个微信吗~”


“行”


总结


然后二面就是项目面了,啊好累啊,现在前端真卷啊,其实感觉可以多问点工程化,虽然准备了很多但是没被问上。


emmm,因为看评论区嘛,就想说,乐观点,其实也没这么卷,只要自己多写写代码和看看八股文都可以的,当然最重要的还是思考。emm想加群一起卷的可以看沸点


作者:溪饱鱼
链接:https://juejin.cn/post/7193979904458195005
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »