注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

首届OpenHarmony竞赛训练营结营颁奖,75所高校学子助力建设开源生态

由OpenAtomOpenHarmony(以下简称“OpenHarmony”)项目群工作委员会和OpenHarmony项目群技术指导委员会主办的首届OpenHarmony竞赛训练营,历时2个月,吸引了来自上海交通大学、复旦大学、电子科技大学、湖南大学、北京理工...
继续阅读 »

由OpenAtomOpenHarmony(以下简称“OpenHarmony”)项目群工作委员会和OpenHarmony项目群技术指导委员会主办的首届OpenHarmony竞赛训练营,历时2个月,吸引了来自上海交通大学、复旦大学、电子科技大学、湖南大学、北京理工大学、四川大学、华中科技大学、中北大学等75个赛队共200+学生报名参与,其中重点本科学院覆盖85%,11月3日经过一天激烈的决赛角逐共有7个赛队脱颖而出。

在11月4日“技术筑生态,智联赢未来”第二届开放原子开源基金会OpenHarmony技术大会上,OpenHarmony项目群工作委员会和OpenHarmony项目群技术指导委员会的专家出席仪式并为12个获奖团队颁奖。

OpenHarmony竞赛训练营合影

构筑开源未来,培育学生英才

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。行业创新,人才先行。为培养更多应用型人才和产业需求有效链接,吸引更多的高校师生参与到OpenHarmony的开发和应用中,今年OpenHarmony项目群工作委员会和OpenHarmony项目群技术指导委员会联合创新推出首届OpenHarmony竞赛训练营。训练营以实战竞赛+赋能培训的模式,帮助高校师生充分掌握并利用OpenHarmony进而实现行业需求和三方库补齐,推动OpenHarmony生态系统的建设和发展,促进技术创新和知识共享,为行业发展注入新的活力和动力。

本届OpenHarmony竞赛训练营分为报名、作品开发、赋能培训、提交作品、作品公示、决赛等环节。其中,赋能培训环节包含技术赋能、线上直播培训、线下核心城市路演培训。主办方邀请了OpenHarmony行业使能专家、三方库领域专家、TSC专家和高校老师作为技术指导和评委,为参赛者提供专业的技术指导、培训、项目评审,以提高其作品的质量和竞争力。

此外,OpenHarmony竞赛训练营鼓励参赛者在作品开发过程中积极参与OpenHarmony开源社区和拉瓦尔社区,利用社区丰富的文档、代码、开发工具等资源优化作品。同时训练营配备了专门的社区小助手为参赛选手解决问题,以积极互动问答的方式和开源合作的精神促进OpenHarmony生态系统的健康发展。

动手实践成果初现,开源英才未来可期

以终为始,着眼实际。训练营旨在鼓励高校人才以OpenHarmony为技术底座去解决更加具体的问题,此次获奖的团队作品涌现出很多创新亮点,充分体现了开源生态蓬勃共建的良好势头和巨大潜力。

一等奖花落选择行业使能赛队的华中科技大学“名称暂定队”赛队,他们提交的病床巡检终端作品,能够更快速、方便地获取病床上病人的关键数据,从而提升巡检效率,帮助实现医疗行业需求,为OpenHarmony行业生态系统的建设和发展作出了突出贡献。

金陵科技学院“我是个rapper”赛队、上海交通大学“Almony”赛队荣获二等奖。作为本次竞赛的黑马,“我是个rapper”赛队提交的参赛作品是一款轻量的图表绘制组件库,提供了丰富的图表类型和灵活的定制选项,使用户能够轻松地创建出精美、直观的图表,可以增强OpenHarmony在数据可视化方面的能力。

在本次比赛中,荣获三等奖的队伍分别是:华中科技大学的“宵宫世界第一”赛队、“阁下又将如何应队”赛队以及“1024队”。而上海交通大学的“Almony”赛队则获得了创新奖。获得优秀奖的队伍分别是:西安交通大学的“西安交通大学2队”、武汉大学的“小乖乖”、大连理工大学的“6舍622队”以及“男2舍708”、兰州大学的“咕咕队伍”。

值得一提的是,本次OpenHarmony竞赛训练营三方库赛道的多个作品达到了合入OpenHarmony社区主干的要求,完成门禁审核后,将提供给所有开发者访问和使用。

OpenHarmony项目群技术指导委员会主席陈海波为获奖赛队颁发一等奖

OpenHarmony项目群技术指导委员会委员武延军

OpenHarmony项目管理委员会主席任革林颁发二等奖

OpenHarmony项目群技术指导委员会委员贾宁

OpenHarmony项目管理委员会委员李锋颁发三等奖

OpenHarmony项目群技术指导委员会委员臧斌宇为获奖赛队颁发创新奖

OpenHarmony项目群技术指导委员会委员张兆生

OpenHarmony知识体系组组长王治文颁发优秀奖

我们正迎来最好的万物智联时代,同时也伴随着诸多挑战。躬身入局、合力共建将是智赢未来的关键所在。未来OpenHarmony竞赛训练营将带来更多领域的命题和更强大多元的技术指导和支持,多方位为高校师生赋能,吸引更多OpenHarmony参与者、贡献者、共创者,构筑开源生态美好未来。

收起阅读 »

构建系统安全“堡垒” OpenHarmony技术大会OS安全分论坛意义深远

随时工业4.0时代的到来,数据已成为企业数字化、智能化发展的基石。然而,面对呈爆发式增长的数据量,如何确保系统安全、防止数据被窃取,已成为全球用户关注的焦点。在这样的背景下,2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会OS安...
继续阅读 »

随时工业4.0时代的到来,数据已成为企业数字化、智能化发展的基石。然而,面对呈爆发式增长的数据量,如何确保系统安全、防止数据被窃取,已成为全球用户关注的焦点。在这样的背景下,2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会OS安全分论坛上,众多专家学者就系统安全、数据安全、大模型安全以及软件安全分析等方向展开了热烈探讨,并且针对OS安全所关注的前沿技术和实践应用提出了建设性意见,一同为OpenHarmony提倡的万物智联筑起了坚实的安全保障。华为副首席科学家、终端BG/车BU首席安全架构师付天福担任论坛出品人。

论坛汇聚了中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华,上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌,华为系统安全实验室主任、系统安全首席专家谭寅,清华大学网络研究院副院长、副教授、博士生导师张超(临时时间冲突委托朱文宇博士),OpenHarmony安委会产业安全使能与标准化工作组副组长翟世俊,华为OS内核安全专家、形式化验证助理科学家李屹,北京中科微澜科技有限公司CEO杨牧天,北京邮电大学副教授、博士生导师梁洪亮,中国科学院软件研究所特别研究助理凌祥等十位技术学者,共同探讨了OpenHarmony安全的前沿技术和未来发展方向。

OS安全分论坛现场

首先,中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华在《数据要素流通的安全挑战与对策》分享中介绍了万物智慧互联是推动数据广泛传播的关键驱动力,促进了数据泛在共享,使得数字经济高度依赖于数据要素的流通。然而,随着数据从传统的共享模式演变为要素流通,现有的安全技术无法满足数据多轮交易的安全需求,因此急需构建一套全面的解决方案,以确保数据要素流通的安全性。李老师提出隐私计算整体的一套理论体系和技术方案,正确利用不同技术解决泛在共享环境中不同环节的数据安全与隐私保护问题。

付天福补充说明,OpenHarmony原生提供了全套数据生命周期的创建、存储、流通、使用和销毁的安全机制,为数据成为数字经济最重要的生产要素,提供了基础保障能力。

中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华主题分享

随后,华为副首席科学家、终端BG/车BU首席安全架构师付天福谈到,安全是OpenHarmony内嵌在基因里的核心架构,为了应对大数据时代的安全挑战, OpenHarmony 提供了对数据整个生命周期的标记、加密、隔离、访问控制等全套机制,为数据在虚拟世界建立了一套完备的保障措施。他表示,在智能汽车时代,安全是最为核心的竞争力,安全是最大的豪华。智能辅助驾驶安全已经成为OpenHarmony赋能汽车行业的独特优势,而网络安全对功能安全的影响也日益增大,OpenHarmony基于分级安全架构,天生就适合智能汽车这样的分布式计算机体系,充分发挥了强安全部件的能力为整车提供保护,为智能汽车提供了网络安全上的领先解决方案。

华为副首席科学家、终端BG/车BU首席安全架构师付天福主题分享

上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌在报告中提出了“智能终端操作系统的个人数据处理与保护”的新思路。该方法利用人工智能和机密计算等新技术,实现对个人数据的存储、使用和保护。他指出,智能终端作为个人的数字世界主要入口,记录了大量的个人数据,合理利用这些数据将带来效率的极大提升。通过提供一个端云一体化的机密计算能力,既满足了AI时代对数据驱动模型下的智能化广泛应用,又保障了个人隐私不泄露企业数据不泄密,OpenHarmony有望成为大模型的入口,为个人数据的安全和隐私提供更强大的保障。

上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌主题分享

华为系统安全实验室主任、系统安全首席专家谭寅在报告中提出了面向OpenHarmony的软硬协同系统安全底座的构筑方法。当前操作系统安全技术已经取得了长足的进展,但随着软件功能复杂性和体量急剧提升,攻击面也持续扩大。即使采用了大量的系统安全防护方法,还是避免不了高危漏洞被利用并最终导致系统被攻破的情况。在此背景下,面向OpenHarmony体系化构筑安全底座,突破关键安全技术,从机制上打造OpenHarmony的安全架构就尤为重要。本报告抛砖引玉介绍当前在OpenHarmony安全防护的工作,以期促进系统安全领域学者与研究人员共同构筑OpenHarmony安全。

华为系统安全实验室主任、系统安全首席专家谭寅主题分享

清华大学网络研究院副院长、副教授、博士生导师张超老由于临时时间冲突,委托博士生朱文宇博士代为分享了题为《闭源软件安全智能化分析》的主题报告,深入探讨了在安全对抗场景下闭源软件分析的挑战,并指出了传统分析技术的局限性。他强调,提取和恢复闭源程序的结构和语义信息对分析其安全性具有重要意义,但由于编译过程中的信息损失,二进制程序分析相较于源码分析更具挑战性。令人欣喜的是,张超分享了团队近期的研究成果,展示了流行的大语言模型在二进制程序分析中的显著效果。这一创新方法为闭源软件安全智能化分析提供了新的思路和解决方案,为未来的软件安全研究开辟了广阔的前景。

清华大学网络研究院副院长、副教授、博士生导师张超委托朱文宇博士主题分享

OpenHarmony安委会产业安全使能与标准化工作组副组长翟世俊对OpenHarmony发行版证通电子的LightBeeOS获得CCRCEAL4+认证做了介绍,介绍了中国网络安全审查技术与认证中心(CCRC)评估保障级(EAL)认证情况,CCRCEAL4+认证对操作系统的安全提出了非常高的要求,需要对系统的安全架构进行全面严格的审核。首先对LightBeeOS的安全功能进行全面测评,并结合详细设计、源码审计、渗透测试、现场审核等对LightBeeOS安全进行全面评估,证明LightBeeOS以及OpenHarmony具有较高的安全水平,为万物互联期待提供了一套坚实的底座,生态厂商可基于OpenHarmony开发更多高安应用场景的发行版OS,聚焦业务场景,减少对安全的巨大投入。

OpenHarmony安委会产业安全使能与标准化工作组副组长 翟世俊 主题分享

本次论坛同时也邀请到了证通电子公司董事、安全支付事业部总经理、OpenHarmony研究院院长程胜春先生,程总表示:拿通用的整套操作系统,而且是国产自主的基于OpenHarmony的操作系统进行这么高级别的安全认证我们是第一次,这次LightBeeOS通过EAL4+认证,我们感到非常开心也深受鼓舞,非常感谢在整个认证过程当中,各位专家老师们给予证通电子的宝贵意见与帮助。证通电子LightBeeOS通过Eal4+认证,首先证明我们OpenHarmony数字底座本身的具有基础的安全能力;其次,融合证通在金融安全上的安全组件等安全技术优势,整体上让这样的一个发行版操作系统信息安全保障能力达到了较高水平。

证通电子董事、安全支付事业部总经理程胜春先生和付天福、翟世俊博士合影

在《操作系统视角看大模型安全挑战》的主题演讲中,华为OS内核安全专家、形式化验证助理科学家李屹指出,操作系统是软件栈的底座,操作系统的安全性在各种场景都具有举足轻重的意义。随着大模型等AI应用的广泛兴起,数据安全正在面临前所未有的挑战。所以,他认为行业有必要站在操作系统的视角,来观察我们将如何应对这些挑战。李博提出了以隔离、跟踪、协同一体化的思路,希望以操作系统为底座,以数据安全为第一原则,逐步构建可信的原生智能,为大模型时代构建坚实的数据和系统安全底座。

华为OS内核安全专家、形式化验证助理科学家李屹主题分享

北京中科微澜科技有限公司CEO杨牧天谈到了建设安全可靠的OpenHarmony生态软件仓库的重要性。他认为,尽管OpenHarmony生态的软件数量众多,但如何确保它们的安全性仍然是一个关键问题。因此,他介绍了OHPM在OpenHarmony生态中三方软件管理方面的完善安全运营机制,旨在预防和处理开源三方库常见的风险,保障OpenHarmony生态的安全可靠发展。

北京中科微澜科技有限公司CEO杨牧天主题分享

北京邮电大学副教授、博士生导师梁洪亮在《软件缺陷漏洞分析》的主题演讲中分享了他的深刻见解。他认为,软件缺陷或漏洞广泛存在,攻击者可以利用它们窃取信息或控制系统。因此,在部署之前,检测和分析软件中的缺陷或漏洞至关重要。鉴于传统测试方法效率的局限,梁老师提出了一种序列引导的多目标混合模糊测试的方案LeoFuzz,通过创新的技术手段,大大提升了软件系统的缺陷的发现效率和准确率。为开源社区软件安全分析提供了一种高效的解决思路,为安全的OpenHarmony生态安全助力。

北京邮电大学副教授、博士生导师梁洪亮主题分享

中国科学院软件研究所特别研究助理凌祥在主题演讲中强调了正确使用OpenHarmony等开源操作系统的API对系统稳定性和安全性的重要性。由于API更新频繁且使用复杂,开发者容易发生误用,导致系统安全漏洞。因此,他介绍了一种基于自动挖掘API路径模式的API误用缺陷检测方法APP-Miner,此方法在针对开源操作系统和大型软件系统的缺陷检测中发挥了显著效果,发现了隐藏很深的多个Linux内核API误用缺陷。希望类似的创新方案能为社区安全治理助力,共同推动开源软件社区安全发展。

中国科学院软件研究所特别研究助理凌祥主题分享

随着万物智联时代的到来,设备联网数量不断增加,技术发展带来的大模型的机遇,随之而来的安全和隐私问题也日益突出。大量的数据将被传输和存储,包括敏感数据如医疗记录、银行信息和公司机密。这些数据的泄露可能会导致巨大的经济损失和信任危机。因此,OpenHarmony将充分发挥其操作系统的安全优势,从数据安全、系统安全、AI安全、软件安全分析等各个维度,打造坚固的“堡垒”,构建分布式全场景协同的开源操作系统底座与生态系统,以保障万物智联产业的繁荣发展。

收起阅读 »

Coremail重磅发布2023年Q3企业邮箱安全性报告

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布《2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?一、国内垃圾邮件激增,环比增长31.63%根据Corem...
继续阅读 »

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?

一、国内垃圾邮件激增,环比增长31.63%

根据Coremail邮件安全人工智能实验室(以下简称“AI实验室”)数据,2023 年Q3国内企业邮箱用户共收到近 亿封的垃圾邮件,环比增长 7.89%,同比去年同期增长 0.91%尤其是国内垃圾邮件激增,环比增长31.63%

经 AI 实验室分析,在 TOP100 接收列表中,教育领域收到的垃圾邮件高达 2.41 亿封,环比上涨13.8%,持续处于前列。

二、境内钓鱼邮件数量激增,首次超过境外

2023 年Q3,全国的企业邮箱用户共收到钓鱼邮件高达 8606.4 万封,同比激增 47.14%,环比也有 23.67%的上升。从总的钓鱼邮件数量来看,境内和境外的钓鱼邮件都呈现增长趋势。但在 2023 年第三季度,境内钓鱼邮件的数量显著增长,超过了境外钓鱼邮件的数量。

Coremail 邮件安全人工智能实验室发现黑产越来越多利用国内的云平台的监管漏洞发送钓鱼邮件,这对国内云服务提供商而言是巨大的挑战

三、Q3垃圾邮件呈现多元化趋势

2023 年 Q3 的垃圾邮件呈现出多元化的趋势,利用各种语言、主题和策略来达成发送垃圾邮件的目的,包括测试邮件、多语言内容、退税和通知等,数量巨大,层出不穷。

而钓鱼邮件常伪装为系统通知或补贴诈骗,这增加了账户被劫和数据泄露的风险。钓鱼邮件主题常利用紧迫性日常相关性模糊性专业性来吸引受害者,建议用户对此类钓鱼邮件保持高度警惕。

四、关键发现:基于邮件的高级威胁

1横向钓鱼攻击

横向钓鱼攻击直接利用了人们的信任关系,已经成为组织面临的重大威胁,而生成型 AI 为攻击者提供了更加强大的工具,使得这些攻击更加难以防范。

以下为 Coremail 在第三季度的横向钓鱼 (也称为内域钓鱼邮件)的检测和拦截数据分析解读:

① 嵌入式钓鱼 URL 的利用:高达 95%的横向钓鱼攻击使用嵌入钓鱼 URL 的邮件。

② 攻击频率:平均每月,约 25%的组织或企业会遭受一次横向钓鱼攻击。

③ 检测挑战:79%的横向钓鱼邮件需要动态分析嵌入的钓鱼URL,这增加了检测的复杂性和时间成本。

④ 更高的威胁等级:接收横向钓鱼邮件的人员的中招率上升了 200%。

2、商业电子邮件欺诈

商业电子邮件欺诈(BEC)涉及网络罪犯伪装成高管或受信任的供应商,以操纵员工转移资金或敏感信息。

针对商业电子邮件欺诈,以下为 Coremail 在第三季度的数据分析解读:

① 账号失陷与社交工程:高达 90%的 BEC 攻击与账户失陷同时发生,而 9%采用社交工程方法。

② 攻击方法:BEC 攻击主要侧重于直接诈骗钱财或信息。

③ 仿冒策略:85%的 BEC 攻击使用以下仿冒策略。

④ 邮件内容分析:70%的邮件为“银行信息变更请求”,15%为催促付款,12%为银行信息变更。

基于 AI 的新威胁

当然,BEC 攻击不仅仅是技术挑战,它更多的是一个人为问题。这类攻击强调了员工培训和安全意识的重要性,因为员工是这类攻击的第一道防线。同时,技术如双因素身份验证、邮件过滤防护和 AI 驱动的安全工具可以提供额外的防护。

五、新措施:监控,响应与安全意识

邮件作为企业沟通的主要方式,不幸地成为了许多网络威胁的首要入口。鉴于此,维护邮件安全不仅是技术问题,还涉及到组织的多个层面。以下分析了邮件安全厂商、邮箱管理员和用户在邮件安全中的作用以及他们分别在监控、响应和安全意识三个方面的关键角色。

1、组织安全的关键挑战

① 员工的安全意识

员工经常成为安全的最弱环节。安全意识方面的缺乏、不够严格的密码策略、轻率地点击可疑链接或不当地处理敏感信息,都可能导致严重的安全事件。

② 威胁响应流程

一个好的安全响应不仅要能有效地解决问题,还要迅速执行。然而,许多组织的反馈机制和响应矩阵的复杂性导致了繁琐的流程,最终导致效率低下和暴露更多风险。

2Coremail 针对性解决方案

① 利用 LLM 进行用户报告的预分类

为了应对迫在眉睫的网络威胁,Coremail 策略性地利用了大语言模型(LLM)即时预分类用户报告的电子邮件。通过 LLM 系统进行即时评估,安全团队可以迅速优先处理威胁,确保高风险邮件得到及时处理。这不仅极大地提高了威胁管理的效率,而且显著降低了由于延迟响应而可能出现的损害风险。

② 让用户成为安全架构的一部分

对于 Coremail 来说,用户不仅仅是被动的实体,而是安全生态系统中的主动参与者。用户是企业安全中的重要角色。通过培养用户主动报告潜在威胁的文化,不仅强化了安全防御,而且增强了用户的安全意识,从而减轻了管理负担。

如上图是“仿冒发信人,仿冒系统通知”的钓鱼漏判响应处理案例的流程。这个流程中,积极的用户参与、即时的邮件威胁响应以及管理员和邮件厂商的紧密合作,得以确保邮件系统的安全性和邮件威胁管理效率。

收起阅读 »

SQL中的DDL(数据定义)语言:掌握数据定义语言的关键技巧!

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。一、DDL介绍...
继续阅读 »

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。

前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。

一、DDL介绍

这里我们先回顾一下前面讲过的SQL语言的概念:SQL(Structured Query Language),即结构化查询语言,是在关系型数据库(诸如Mysql、SQL Server、Oracle等)里进行相关操作的标准化语言,可以根据sql的作用分为以下几种类型:

下面再来看DDL语言是什么:

DDL,全称为Data Definition Language,即数据定义语言。它是SQL语言的重要组成部分,主要用于定义和管理数据库的结构。

二、DDL语言能做什么?

通过DDL,我们可以创建、修改和删除数据库、表、视图等对象。

创建数据库: 使用CREATE DATABASE语句,我们可以创建一个新的数据库。

删除数据库: 使用DROP DATABASE语句,我们可以删除一个已经存在的数据库。

创建表: 使用CREATE TABLE语句,我们可以在数据库中创建新的表。

** 删除表:**使用DROP TABLE语句,我们可以删除一个已经存在的表。

修改表结构: 使用ALTER TABLE语句,我们可以修改已经存在的表的结构,如添加、删除或修改字段等。

三、什么是数据库对象

数据库对象是数据库的组成部分,常见的有以下几种:

1、表(Table )

数据库中的表与我们日常生活中使用的表格类似,它也是由行(Row) 和列(Column)组成的。

Description

列由同类的信息组成,每列又称为一个字段,每列的标题称为字段名。行包括了若干列信息项。一行数据称为一个或一条记录,它表达有一定意义的信息组合。一个数据库表由一条或多条记录组成,没有记录的表称为空表。每个表中通常都有一个主关键字,用于唯一确定一条记录。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

2、索引(Index)

索引是根据指定的数据库表列建立起来的顺序。它提供了快速访问数据的途径,并且可监督表的数据,使其索引所指向的列中的数据不重复。

Description

3、视图(View)

视图看上去同表似乎一模一样,具有一组命名的字段和数据项,但它其实是一个虚拟的表,在数据库中并不实际存。视图是由查询数据库表产生的,它限制了用户能看到和修改的数据。

Description

4、图表(Diagram)

图表其实就是数据库表之间的关系示意图。利用它可以编辑表与表之间的关系。

Description

5、缺省值(Default)

缺省值是当在表中创建列或插入数据时,对没有指定其具体值的列或列数据项赋予事先设定好的值。

Description

6、规则(Rule)

规则是对数据库表中数据信息的限制,它限定的是表的列。

7、触发器(Trigger)

触发器是一个用户定义的SQL事务命令的集合。当对一个表进行插入、更改、删除时,这组命令就会自动执行。

Description

8、存储过程(Stored Procedure)

存储过程是为完成特定的功能而汇集在一起的一组SQL 程序语句,经编译后存储在数据库中的SQL程序。

Description

9、用户(User)

所谓用户就是有权限访问数据库的人。

四、DDL常用语句

4.1 数据库相关

1)查看所有数据库

格式:show databases;

2)创建数据库

格式:create database 数据库名 charset=utf8;

举例:

#创建一个名为test的数据库
#create database 库名;
create database test;
#创建一个名为test的数据库并指定字符集和编码格式
create database test default charset utf8 collate utf8_general_ci;

3)查看数据库信息

格式:show create database 库名;

**4)删除数据库 **

格式:drop database 数据库名;

举例:

#删除test数据库
drop database test;

5)使用数据库

执行表相关和数据库相关的SQL语句之前必须先使用了某个数据库

格式:use 数据库名;

举例:

use test;

4.2 表相关

1)创建表

格式:create table 表名(字段1名 类型,字段2名 类型,…)

举例:

create table person(name varchar(50),age int);
create table person(name varchar(50),age int);
create table stydent(name varchar(50),chinese int ,math int, english int)charset=utf8;
创建一个员工表emp 保存名字,工资和工作
create table emp(name varchar(50),salary int,job varchar(20));

2)查询所有表

格式:show tables;

3)查询表信息

格式:show create table 表名;

举例:

show create table emp;

4)查询表字段

格式:desc 表名; (description)

5)修改表名

格式:rename table 原名 to 新名;

举例:

rename table stydent to stu;

6)删除表

格式:drop table 表名;

4.3 alter表操作相关

1)添加表字段

格式(最后面添加):alter table 表名 add 字段名 类型;

格式(最前面添加):alter table 表名 add 字段名 类型 first;

在xxx字段后面添加:alter table 表名 add 字段名 类型 after 字段名;

举例:

alter table emp add gender gender varchar(5);
alter table emp add id int first;
alter table emp add dept varchar(20) after name;

2)删除表字段

格式:alter table 表名 drop 字段名;

举例:

alter table emp drop dept;

3)修改表字段

格式:alter table 表名 change 原名 新名 新类型;

举例:

alter table emp change job dept varchar(10);

4)修改列属性

格式:alter table 表名 modify 列名 新列属性

举例(只有MySQL是这样写的):

alter table student modify age int;

关于DDL常用语句就讲这么多了,尽管现在有许多图形化工具可以替代传统的SQL语句进行操作,同时在Java等语言中也可以使用数据库,但对于SQL各类语句的了解仍然非常重要。

收起阅读 »

人民网《外企谈信心》| Denodo:加强数据管理技术合作 护航数字经济高质量发展

本文摘自:人民网责编:王震、吕骞近日,Denodo技术公司首席执行官兼创始人AngelViña做客人民网时表示,中国拥有巨大的经济体量,吸引了众多大型全球公司来华设置分支机构,且中国企业怀有“从中国走向全球”的愿望,种种因素使得开辟中国市场成为以全球化为使命的...
继续阅读 »

本文摘自:人民网

责编:王震、吕骞

近日,Denodo技术公司首席执行官兼创始人AngelViña做客人民网时表示,中国拥有巨大的经济体量,吸引了众多大型全球公司来华设置分支机构,且中国企业怀有“从中国走向全球”的愿望,种种因素使得开辟中国市场成为以全球化为使命的Denodo公司的“必然的决策”。

微信图片_20231103092343.jpg

本期嘉宾:Denodo 全球 CEO 兼创始人Angel Viña


【采访摘要】

主持人:作为Denodo公司创始人您如何看待中国市场?Denodo选择来中国发展的缘由又有哪些?

Angel Viña:进入中国是一个必然的决策:

1. 因为我们现在有超过150家全球客户在中国有分支机构,Denodo管理数据的方式可以确保他们更好地运营全球业务。

2. 中国是全球第二大经济体,有很多公司在“从中国走向全球”,这是我们重点合作发展的一类客户。

3. Denodo是一个国际化的组织,中国是我们全球化布局的重要区域,是公司发展战略的重要一步。


主持人:近年中国出台了一系列政策,保障外资企业在华发展。您认为这一系列政策的出台将对外资企业在华发展起到什么作用?

Angel Viña:中国市场有很多积极的变化,包括新的外资政策以及创造稳定、互信的营商环境等。政府牵头进行咨询调研,公开征询外资企业的反馈,我们愿积极参与。

在一系列新法规中,跨境数据管理占到了相当的篇幅,数据交换、数据跨境、数据安全等。全球公司,无论是在华运营的跨国公司,还是出海拓展全球市场的中国公司,都有信息整合的需求。需要完整的数据来支持更明智的决策并实现公司更高效的运营,从而提供更好的客户体验。这些法规规定了数据跨境传输的方式,而Denodo的逻辑数据管理技术在这一场景中可以起到重要作用,确保数据使用的安全合规。

此外,我们会与中国本土丰富的人才网络对接,尤其是与中国的顶级研究中心合作。利用本地优势资源来支持我们的全球拓展,才能更好地服务国际公司和中国出海公司。

中国在不断加强知识产权保护,为像Denodo这样的知识密集型外资公司带来保障。同时我们希望与政府加强信任与合作,因为政府是Denodo在全球各地区的重要使用者,而信任需要依托于规范和监管,我们会积极拥护当地政策。


主持人:如今很多技术,如大数据、云计算、人工智能、区块链等正在加速创新,日益融入经济社会发展全过程。在您看来,数据管理在数字经济发展中发挥着怎样的作用?外国企业参与中国数字经济发展可以享受哪些红利?

Angel Viña:业务高层如今十分注重数据的价值,数据资产化的重要性已得到普遍的认知。用好数据才能真正地做好业务运营,政府部门才能更好地提供社会服务。任何交易、决策、工业系统或制造系统的自动化流程都离不开数据。数据技术是当今首席执行官最注重的技术,做好数据管理是第一要务。

数字经济需要构建在一个强大而统一的数据基础之上。Denodo就是用来帮助组织将多个数据资产统一为一个业务目标,创建统一的数据视图,以便业务用户能够使用数据。

各种新兴技术也都基于数据,如区块链主要用于交易的可追溯性,而不同的数据层都存储在不同的节点中,基于数据才能真正构建个性化的业务应用来支持不同行业和领域的应用场景。对于AI,数据是机器学习流程的核心,生成式AI也是如此,用AI算法构建的任何一个应用都需要一个连接数据资产的桥梁。对于制造业自动化,从机器人到各种先进技术,具备数据管理能力才能创建一个敏捷灵活的数据基础,才能实现所有现代化技术。


主持人:您认为中国企业在数字化转型中面临哪些困难?Denodo如何充分发挥自身优势助力中国企业数字化转型发展?

Angel Viña:第一点,数据的使用率。尽管数据成熟度会有或多或少的差异,但是全球的数据挑战都大同小异。拥有数据资产不是最重要的,重要的是理解数据可以如何使用。因为数据激增的现象非常普遍,数据量在未来两到三年内会以两位数的增速继续增长,甚至达到十几倍以上的增长。其次是数据碎片化的问题,数据普遍分散在许多不同的数据存储库或数据孤岛中。最后就是数据的使用,据统计,全球采集到的数据只有不到4%被使用。挖掘更多数据集的价值,才能获得更多更好的信息来支撑决策。我们也可称之为“数据民主化”,但最终目的是为了让数据更容易被用户访问和使用。

业务部门能够更容易地获得所需的数据,这就是Denodo的独特技术可以帮助中国公司的事情——显著提升数据的使用率。但在Denodo的功能之上,我认为需要来自政府或公司高层管理者对数据文化的贯彻才能真正提升数据的使用,更快地获取客户,更好地改善公民生活,改善公司的客户体验,提高运营效率和员工生产力。这些都需要对数据交易的可见性,在数据生态系统中实现数据使用。

第二点,安全和合规。在增加数据用量的同时,还需要对使用过程加以控制。我们用逻辑或虚拟化的方式来管理数据,就是为了在底层的物理数据存储和运算系统之上创建一个支持层,用来执行管控,确保数据安全,执行数据治理策略来真正地帮助企业把数据交付给正确的用户,或授权正确的用户来使用数据。控制数据使用的过程非常重要,可以确保数据交易的公开可见。一个组织每年可能有上百万或者上亿的数据交易,很多数据查询都可以实现秒级的执行,因此确保数据以安全、合法的方式被正确地使用至关重要。

第三点,对于在华运营的外国公司以及出海的中国企业如何才能解决跨境数据管理和数据分享的问题。各个国家对于数据隐私、数据主权、数据资产管控等都有强有力的立法约束,数据安全与国家安全密不可分。而我们提供的是一种真正能实现数据可操作性,同时又确保其符合跨境、跨国数据法规的方式,对全球公司来说意义重大。他们需要整合数据,做好跨全球数据的安全使用,才能更好地管理如全球人力资源部等关键职能部门的信息交换,或者更好地服务全球客户,Denodo在这方面有丰富的经验。


主持人:在您看来,数字化如何赋能中国企业更好地走出去,与世界共享合作与发展机遇?

Angel Viña:数字化是必经之路,没有数字化转型就没有未来。领导者与落后者的区别就在于数字化交付水平,也可称之为数据成熟度、数字化或者自动化。中国企业对数字化高度重视,非常清楚要想“走出去”并在全球获得竞争力,数据成熟度是一个关键指标。在这一过程中Denodo可以贡献我们的专长,这是我们多年来为世界最顶级最复杂的公司服务而积累的实践经验。20多年来始终帮助顶尖的国际公司建立数据成熟度和数据基础,让他们以更有竞争力的方式来运营和拓展现代业务。

图片5.png

嘉宾简介

Angel Viña是 Denodo Technologies 的创始人兼首席执行官,同时也是公司董事会主席。他负责监督 Denodo 整体企业战略、业务运营和全球扩张。在他的领导下,Denodo 已发展为全球数据管理的领导者,有 800 名员工和 1000 位客户,在 30 个国家/地区设有 25 个办事处。Angel 在开始 IT 职业生涯前已积淀深厚的学术基础,他于 1987 年获得马德里技术大学电信工程博士学位,随后在美国加利福尼亚大学洛杉矶分校和斯坦福大学担任博士后研究访问学者,并在多所大学担任教授。


关于Denodo

Denodo是数据管理领域的领导者。屡获殊荣的Denodo平台是领先的数据集成、管理和交付平台,使用逻辑方法实现自助式商业智能、数据科学、混合/多云数据集成和企业数据服务。Denodo的客户覆盖30多个行业的大型企业和中型市场公司。这些客户实现了超过400%的投资回报率和数百万美元的收益,在不到半年的时间内就获得回报。更多信息,请访问 denodo.com.cn。

收起阅读 »

Denodo全球CEO兼创始人Angel Viña访华 共襄中国经济数字化转型新机遇

近日,全球领先的企业数据管理服务公司Denodo全球CEO兼创始人Angel Viña叶苏斯先生携总部及亚太区高管团队访华。在为期一周的访问中,Angel Viña叶苏斯先生一行与政府机构及战略合作伙伴共商未来数据管理的新高度和新起点,共同探索Denodo助力...
继续阅读 »

近日,全球领先的企业数据管理服务公司Denodo全球CEO兼创始人Angel Viña叶苏斯先生携总部及亚太区高管团队访华。在为期一周的访问中,Angel Viña叶苏斯先生一行与政府机构及战略合作伙伴共商未来数据管理的新高度和新起点,共同探索Denodo助力中国经济数字化转型的新机遇。

图片1.png


据悉,来自西班牙的Denodo平台是领先的数据集成、管理和交付平台,使用逻辑方法实现自助式商业智能、数据科学、混合/多云数据集成和企业数据服务。目前,Denodo的客户覆盖30多个行业的大型企业和中型市场公司,拥有800多名员工和1000多家客户,在30个国家/地区设有25个办事处。


密集访问,共话在华发展机遇

北京市副市长靳伟先生、商务部投资促进局局长刘殿勋先生、中国国际商会秘书长孙晓先生、中国发展研究基金会副秘书长俞建拖先生等政府机构代表会见了Angel Viña团队,就数字科技外资企业在华发展及促进产业地方合作等议题进行了深入交流。商务部投资促进局刘殿勋局长表示,投资促进局将发挥专业机构的作用更大力度吸引和利用外资。中国数字科技领域市场发展前景广阔,欢迎Denodo把握中国市场机遇,深化在华投资合作,加强创新和绿色发展,为推动中外企业合作共赢做出贡献。

图片2.png


对此,Angel Viña先生表达了Denodo在华投资与发展的决心。他表示,尽管经过不平凡的三年,中国经济依旧展现出强大的韧性。政府在出台吸引外商投资相关政策和切实优化外商在华运营环境方面不懈努力,蓬勃发展的数字经济和各组织对数据的重视程度,让他对中国巨大的市场潜力充满信心。Denodo会持续加码在中国的投资发展和技术能力储备,为在华运营的跨国公司和出海拓展的中国企业提供安全、合规、高效的跨境数据管理平台。

Angel Viña先生一行还拜访了西班牙驻华大使馆,与大使拉斐尔·德兹卡拉尔·德马扎雷多(Rafael Dezcallar de Mazarredo)先生和商务参赞阿方索·诺列加·戈麦斯(Alfonso Noriega Gomez)先生进行会谈。


合作研讨,共探数据管理未来

访华期间,Angel Viña先生一行还与包括国家区块链技术创新中心、中国信息通信研究院、清华大学等专业学术和科研机构等在内的科研院校、机构积极接洽,探索数据管理技术发展,以及讨论该领域人才引入对接的可能性。

在清华大学和中国信息通信研究院等举办的一系列“数据编织(Data Fabric)”专家研讨会上Angel Viña先生作为特别嘉宾发表精彩演讲,并吸引众人的关注。他感叹于中国庞大的优秀人才储备,希望与各学术科研机构加深合作,为数据管理领域的技术标准制定和行业化数据解决方案贡献Denodo的专业知识和洞察。

图片3.png


在访问中国最大的IT服务提供商神州数码集团时,Angel Viña先生与神州数码集团总裁王冰峰先生及其核心管理团队进行了深入交流。对神州数码集团 “数云融合”的最新战略深表赞赏,希望通过双方密切的本地协作,为各行业大型企业和政府机构提供数字化转型的最佳产品和服务。

图片4.png


“此次访华所见证的技术创新水平给我留下了深刻的印象。中国是一个经济大国,也是数据大国、创新大国。”Angel Viña先生总结此次访华收获时如是说。在接受人民网专访时他还提出,Denodo进入中国是一个必选项,而不是可选项。Denodo是一家全球性公司,但在各个区域也要成为一家本地化公司,才能真正满足不同市场独特的需求,期待Denodo独特的逻辑数据管理能力能够为中国的数字化经济发展增添新色!

图片5.png

(图:来源人民网专访)

收起阅读 »

符号绑定的另一种打开方式

懒加载和非懒加载iOS对于引用的外部符号,分为Lazy Symbol和Non-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的...
继续阅读 »

懒加载和非懒加载

iOS对于引用的外部符号,分为Lazy SymbolNon-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。

Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的函数地址写入到对应的地址中,实现绑定。而Non-Lazy Symbol则会在第一次调用该函数的时候,为其动态寻找真实函数地址并进行绑定。

facebook基于符号绑定机制,写出了hook神器fishhook,通过查找符号指针并替换,从而达到hook效果!!!

然而,基于模块检测反hook,却甚是烦人。你可能会有反反hook来应付,但是它也有可能会有反反反hook来对付你~~~

那么,怎么才能终结这场hook与反hook的心理战呢?

Mach-O View 分析动态符号绑定过程

简单分析一下Lazy Symbol的绑定过程:



这里以NSLog为例:

可以看出,符号NSLog所指向的地址为:0x0000000100006474。

转化为文件偏移为:0x0000000100006474 - 0x100008078 + 32888 = 0x6474;

到文件偏移为0x6474的位置查看:

这是一段可执行代码,地址0x6474处的意思是:读取0x647c位置处的四个字节的数据(0x1d),保存到w16寄存器。然后无条件跳转到0x645c(这里的地址,全部都是指文件偏移)。


这段代码,光这么看其实看不出什么,但是如果去调试的话,就会发现,这段代码实际上是在调用dyld_stub_binder为懒加载符号绑定真实地址。而刚刚在0x6474处的代码获取到的四字节的数据,实际上是符号绑定信息的偏移:


0xc428+0x1d = 0xc445

也就是说,动态绑定NSLog所需要的数据,就存储在0xc445处。

那么,理论上来说,如果我们尝试着修改这里的数据,是不是就会改变符号的查找的过程呢?

实践

想的再多,都不如动手操作!!!

新建一个工程,书写如下代码(main.m):

__attribute__((constructor)) static void entry(int argc,char *argv[],char **apple,char **executablepath,struct mach_header_64 **mh_ptr){

if (!strncmp(argv[0], "aaa", 3)) {
printf("the same!!");
}
}

并按照如上方式,查找到函数strncmp的Lazy Binding Info,做如下修改:


修改后:


编写动态库并注入到可执行文件:


__attribute__((visibility("default"))) int strncmq(const char *__s1, const char *__s2, size_t __n);

int strncmq(const char *__s1, const char *__s2, size_t __n){
      printf("hook:%s\nhook:%s",__s1,__s2);
       return strncmp(__s1, __s2, __n);
}

重签名运行!!


发现已经替换成功了!!!

但是,用ida或者hopper分析一下二进制文件,会发现调用的还是原来的strncmp符号:


说明如果进行模块检测的话,还是可以检测出来的~因为虽然符号查找替换了,但是实际上"外套"还是strncmp。所以,继续把外套也修改了!!!

修改这两个处:


修改后:


总结

对于动态绑定的外部引用符号,能动手脚的地方确实很多!!!

收起阅读 »

iOS应用砸壳

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢? 其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!! 首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件...
继续阅读 »

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢?
其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!!
首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件,所以当设备运行应用时,加载进内存中的数据,肯定是经过解密后的数据,因此咱们只需把内存里的对应的解密部分dump下来即可。
这里介绍一个砸壳工具:dumpdecrypted
该工具的原理就是在程序运行之后注入一个动态库,然在内存中dump下解密之后的部分。
原理和实现不赘述,想进一步了解,可以查看源码。
这里简单讲一下如何用该工具进行解密app:
1、运行源码,生成动态链接库。
解压下载下来的dumpdecrypted-master.zip到当前目录;打开终端,cd到该目录。


在该目录下直接运行make:


如果运行不成功,报错找不到文件。运行xcode-select --print-path ,查看目录是否指向/Applications/Xcode.app/Contents/Developer。如果不是,则用xcode-select -s /Applications/Xcode.app/Contents/Developer修改一下。再执行make。
运行成功,会在当前目录生成dumpdecrypted.dylib动态库。
2、注入动态库
ssh登录进手机,用ps命令查看目标app所在的目录。


记住这个目录。待会儿有用。
再用cycript获取目标app的沙盒目录。


获取到目标app的沙盒目录之后,退出cycriptcontrol+D),将第一步得到的dumpdecrypted.dylib,导入到沙盒目录(导入方法有很多,scp、PP助手、ifiles等)。
cd到沙盒目录,运行DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Containers/Bundle/Application/DDFA8DC8-F19A-4A6F-932B-22E8170BB22D/HDXinHuaDict.app/HDXinHuaDict:


看到一系列的+就说明运行成功,在沙盒目录下就会有被解密的二进制文件。



收起阅读 »

🔥🔥🔥15万奖池,环信 IM+AI编程挑战赛开启报名!

大赛背景即时通讯已成为现代生活中不可或缺的一部分,近年来人工智能的迅猛发展也为即时通讯带来了前所未有的智能化体验。高度拟人的陪伴型机器人,全能实用的服务型机器人,高效智能的对话机器人,AI 与即时通讯的结合创新,为用户提供了众多智慧化的沟通方式。借助 AI 模...
继续阅读 »


大赛背景

即时通讯已成为现代生活中不可或缺的一部分,近年来人工智能的迅猛发展也为即时通讯带来了前所未有的智能化体验。高度拟人的陪伴型机器人,全能实用的服务型机器人,高效智能的对话机器人,AI 与即时通讯的结合创新,为用户提供了众多智慧化的沟通方式。借助 AI 模型的强大能力,即时通讯正迎来全新的场景和体验。

本次「未来已来·创新无限 - 环信 IM+AI编程挑战赛」由环信举办,旨在鼓励开发者、编程爱好者和创新者们发挥创意,融合即时通讯与人工智能技术,重新定义人们的沟通方式,创造出更智能、更便捷、更有趣的沟通体验。无论您是热衷于编程技术,还是对即时通讯与人工智能融合充满好奇,本次编程挑战赛都将是您展示才华和创新的绝佳机会。

本次大赛已正式开启报名!让我们一起开创未来,用编程点亮「 IM+AI 」的交融之美!

赛程安排


赛题介绍

参赛作品须基于环信即时通讯IM(SDK或Demo),自由调用业内领先的AI大模型能力,实现具有应用场景和创新功能的AI+IM的应用。鼓励大家在本次挑战赛中发挥奇思妙想的创意, 展现花样丰富、智能化的即时通讯体验及应用场景。

以下场景及功能描述供参考:

社交场景 | AI聊天机器人、AI游戏陪玩、虚拟人物、

  • AI聊天机器人:闲聊、恋爱、角色扮演、心理咨询、情感陪伴等。
  • AI游戏陪玩: 根据玩家游戏习惯、兴趣爱好等信息,自动生成陪玩建议和游戏策略,使玩家在游戏中获得更加个性化的体验。

电商场景| 智能客服、精准营销、AI模特换装、虚拟主播

  • 智能客服:有效引导用户购买、使用产品、智能回复、处理订单和退货需求等。
  • 精准推荐:根据用户喜好,提供个性化商品描述、推荐语。
  • AI模特换装、虚拟主播

医疗场景| 智慧导诊、智能在线问诊、诊室听译机器人

  • 智慧导诊:安排预约、患者回访、发通知、收集信息,帮助患者找最近的就诊医院等。
  • 智能在线问诊:AI医生线上辅助接诊,辅助医师诊断和开方。
  • 诊室听译机器人:将医患沟通实时录音,结构化处理病情数据后自动生成病历。

教育场景| AI助教、智能教学、个性化教育

  • AI助教:智能化的处理学生反馈、教师评估、行政协助等基础功能。
  • 智能教学:智能解答和辅导学生问题,帮助学生更好理解和掌握知识。
  • 个性化教育:根据学生学习情况和反馈,自动分析和评估学生的学习成绩,能力,兴趣,提供针对性的学习计划,教学建议和改进措施。

金融保险| AI虚拟代理或顾问

  • AI虚拟代理或顾问:自动处理提交索赔,提供状态更新,执行其他基本任务等。

软件/工具 |会议助手AI洞察、群未读信息摘要、群分享链接摘要

  • 会议助手AI洞察:实时分析在线会议内容,生成会议关键点及摘要,会议结束后智能生成会议结论及待办。
  • 未读信息摘要: 群内大量未读信息,智能生成摘要,方便快速获取信息。
  • 分享链接摘要: 群内分享的海量信息智能生成摘要。

奖项设置


参赛流程

1 、点击 报名链接 完成报名
2 、扫码添加“环信冬冬”为好友,回复【队伍名+报名时填写的姓名】进群

3 、作品开发 开发应用,编写规范文档、帮助文档(包含应用介绍、应用截图、代码模块说明等);

4、 于12月10日前,根据作品提交流程,上传代码及相关文档,完成作品提交。

5、 等待评审结果

作品提交

  1. Fork 本次创新赛官方作品提交仓库至你的个人GitHub 仓库

  2. Clone 你的 GitHub 仓库代码「https://github.com/你的GitHub名/Easemob2023-Innovation-Challenge」

  3. 在本地的「Innovation- Challenge」文件夹下新创建个人项目文件夹,命名格式为“队伍名-作品名“,将参赛作品的相关文件与代码放置在该文件夹内,勿要“感染”其他文件或者文件夹

  4. 最后通过 Pull Request 将作品内容推送至官方仓库

  5. Review 通过,合入本次比赛的主分支。

参赛规则

  • 参赛者可以以个人或团队形式参赛,团队参赛请指定一名联系人

  • 竞赛所使用的编程语言和开发平台不限,参赛者可根据自身喜好和技术背景选择适合的开发平台。

  • 参赛作品不限平台,不限领域。移动端、Web端、PC端、小程序等均可。

  • 比赛期间可随时报名和提交作品,所有参赛作品必须为原创,不得抄袭他人的代码或设计。

  • 请注意作品截止提交时间,将作品内容在规定时间内推送至官方仓库,否则将不参与作品评选。

  • 参赛作品须保证原创性,不违反相关法律法规,不存在任何法律或合规风险,作品中使用的素材(包括但不限于开源代码、图片、视频等)不存在版权问题。

评选规则


活动支持

收起阅读 »

【Java集合】单列集合Collection常用方法详解,不容错过!

嗨~ 今天的你过得还好吗?路途漫漫终有一归幸与不幸都有尽头🌞在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学...
继续阅读 »


嗨~ 今天的你过得还好吗?

路途漫漫终有一归

幸与不幸都有尽头

🌞

在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学习吧。

在Java基础中我们也学过,在类实现接口后,该类就会将接口中的抽象方法继承过来,此时该类需要重写该抽象方法,完成具体的逻辑。


Collection 常用功能

Collection 是所有单列集合的父接口,因此在Collection 中定义了单列集合(List 和 Set)通用的一些方法,这些方法可用于操作所有的单列集合。

20190428183815681.png

1.1 方法如下:

image.png

打开api文档,我们可以看到Collection 在 java.util 下,我们通过练习来演示下这些方法的使用:


640 (28).gif


1.2方法演示

public class Demo1Collection {    

       public static void main(String[] args) {

           //创建集合对象

           //使用多态的形式 定义
           Collection<String> person = new ArrayList<>();
           //输出不是一个对象地址,所以说重写了toString 方法

           System.out.println(person);

   

   //        boolean add(Object o) 向集合中添加一个元素
   //        返回值是一个boolean值,一般可以不用接收
           person.add("科比");
           person.add("姚明");
           person.add("库里");

           person.add("麦迪");

           //添加完我们在输出一下这个集合

           System.out.println(person);


   //        boolean remove(Object o) 删除该集合中指定的元素

   //        返回 集合中存在元素,删除元素,返回true;集合中不存在,删除返回false

           boolean res1 = person.remove("科比");

           boolean res2 = person.remove("奥尼尔");

           System.out.println("res1=" +res1);

           System.out.println("res2=" +res2);

   //        boolean isEmpty() 判断该集合是否为空

           boolean empty = person.isEmpty();
           System.out.println("empty=" + empty);    
   //        boolean contains(Object o) 判断该集合中是否包含某个元素
           boolean contains = person.contains("麦迪");

           System.out.println("contains=" + contains);

   //        int size() 获取该集合元素个数

           int size = person.size();

           System.out.println("size = " + size);

   

   //        public Object[] toArray() 把集合总的元素,存储到数组中

           Object[] personArray = person.toArray();

           for (int i = 0; i < personArray.length; i++) {

               System.out.println("数组--" + personArray[i]);
           }    
   //        void clear() 删除该集合中的所有元素,但是集合还存在
           person.clear();
           System.out.println(person);    
   
           //通过多态的方式,如果我们把arrayList 换成HashSet,发现也能使用,这就是我们实现接口的好处
       }
   }

注意:有关Collection中的方法不止上面这些,其他方法可以自行查看API学习。


查询集合中的元素-Iterator 迭代器

2.1 Iterator 接口

在程序开发中,经常需要遍历集合中的所有元素,就是要看看里面所有的元素,那我们怎么办呢? 

20201216223250385.png

针对这种需求,JDK 专门提供了一个接口: java.util.Iterator。该接口也是 Java集合中的一员,但它与 Collection、Map 接口有所不同,Collection 接口 与 Map 接口 主要用于存储元素,而 Iterator 主要用于迭代访问(即 遍历) Collection中的元素,因为Iterator 对象也被称为迭代器。 


下面介绍一下迭代器的概念:

迭代即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种方式专业术语称为迭代。


通过文档,我们可以看到 Iterator 是一个接口,我们无法直接使用,而需要使用Iterator接口的实现类对象,通过Collection接口中的 Iterator()方法,就可以返回迭代器的实现类对象:

public Iterator iterator():  获取结合对应的迭代器,用来遍历集合中的元素。

通过API文档,我们可以看到 Collection 中 Itrerator 接口的常用方法如下:

  • public E next()  返回迭代的下一个元素

  • public boolean hasNext() 如果仍有元素可以迭代,则返回true

image.png

接下来我们通过案例学习,如何使用Iterator 迭代集合中的元素:

 /**
    *  Iterator 迭代器使用
    */

   public class Demo1Iterator {    
       public static void main(String[] args) {            /**
            * 使用步骤:
            * 1. 使用集合中的方法 iterator() 获取迭代器的实现类对象,使用Iterator接口接收 (使用接口接收返回值,这就是我们说的多态)
            * 2. 使用Iterator接口中的方法 hashNext() 判断有没有下一个元素
            * 3. 使用Iterator接口中的方法 next() 取出集合中的下一个元素
            *
            */

           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");
           ball.add("乒乓球");    
           //我们来获取一个迭代器,多态
           Iterator<String> iterator = ball.iterator();    
           //判断
           boolean b = iterator.hasNext();
           System.out.println("是否有元素--" + b);            //取出
           String next = iterator.next();
           System.out.println("元素--" + next);    
           //判断
           boolean b1 = iterator.hasNext();
           System.out.println("是否有元素--" + b1);            //取出
           String next1 = iterator.next();
           System.out.println("元素--" + next1);    
           //判断
           boolean b2 = iterator.hasNext();
           System.out.println("是否有元素--" + b2);            //取出
           String next2 = iterator.next();
           System.out.println("元素--" + next2);    
           //判断
           boolean b3 = iterator.hasNext();
           System.out.println("是否有元素--" + b3);            //取出
           String next3 = iterator.next();
           System.out.println("元素--" + next3);    
           //判断
           boolean b4 = iterator.hasNext();
           System.out.println("是否有元素--" + b4);            //取出
   //        String next4 = iterator.next();
   //        System.out.println("元素--" + next4);
           //如果没有元素,在取的话,会报一个NoSuchElementException 的错误
   
   
           /**
            *
            * 代码优化 上面这些步骤是一个重复的过程,我们可以使用循环来优化,那我们选择哪种来呢
            * 我们说 知道元素个数,使用for
            * 不知道元素个数,使用while
            *
            * 那当前我们迭代器的个数,我们不知道,所以使用while循环,而我们的hasNext 就可以作为
            * while的条件来判断
            *
            */

           while (iterator.hasNext()) {
               String ballResult = iterator.next();
               System.out.println("--优化--" + ballResult);
           }
   
       }
   }

分析: 

在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException 没有集合元素的错误。


640 (28).gif


2.2 迭代器的实现原理

我们在之前的案例中已经完成了Iterator 遍历集合的整个过程。当遍历集合时,首先通过调用集合的iterator() 方法获得迭代器对象,然后使用hasNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已经到达集合末尾,停止遍历元素。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Itearator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了让初学者能更好的理解迭代器的工作原理,接下来通过一个图例来演示 Iterator 对象迭代元素的过程:

Description

在获取迭代器的实现类对象是,会把索引指向集合的-1位置,也就是在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素。

  • 当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,

  • 当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,

  • 以此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。


查询集合中的元素-增强for

3.1  概念

增强for循环(也称为 for each 循环)是JDK5以后出来的一个高级for循环,专门用来遍历数组和集合的。 通过api文档,Collection 继承了一个Iterable 接口 ,而实现这个接口允许对象成为 “foreach” 语句目标,也就是所有的单列集合都可以使用增强for。 

它的内部原理其实是个Iterator 迭代器,只是用for循环的方式来简化迭代器的写法,所以在遍历的过程中,不能对集合中的元素进行增删改查操作。

格式:

for (元素类型 元素名 : 集合名或数组) {
       访问元素
   }

它用于遍历 Collection 和数组。通常只进行遍历元素,不在遍历的过程中对集合元素进行增删操作。


640 (28).gif


3.2 练习1:遍历数组

public class Demo2Foreach {    
       public static void main(String[] args) {    
           int[] array = {1,2,3,4,5};    
           for (int i : array) {
               System.out.println("--数组元素--" + i);    
               if (i == 2) {
                   i = 19;
               }
           }    
           //在增强for中修改 元素,并不能赋值
           System.out.println(Arrays.toString(array));
       }
   }

640 (28).gif


3.3 练习2:遍历集合

public class Demo3Foreach {    
       public static void main(String[] args) {
           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");    
           for (String s : ball) {
               System.out.println("---" + s);
           }    
           //相对于Iterator遍历方式,增强for 简化了很多,所以优先使用该方式。
       }
   }

新for 循环必须有被遍历的目标。目标只能是Collection 或者是数组。仅仅作为遍历操作出现。


总结

本篇中主要介绍了单列集合接口Collection为我们提供的常用接口,也通过代码的方式带大家体会了一下。在后面的内容中为大家介绍了如何把单列集合中的内容查看出来(遍历),通过讲解一些底层的原理,让大家感受了一下迭代器的使用。


当然集合的遍历不仅仅限于这两种方式,例如java8为我们提供的流式遍历集合,希望大家下去后自己也能搜搜相关的遍历方式,尝试使用一下,ok,本文就到这里了。



收起阅读 »

作为前端,这几个关于console的小知识点,你知道吗

web
在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况 就比如下面这个形式的对象: const obj = { "err_no": 0, "err_msg": "success", "data": { "user_ba...
继续阅读 »

在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况



就比如下面这个形式的对象:


const obj = {
"err_no": 0,
"err_msg": "success",
"data": {
"user_basic": {
"university": {},
"major": {}
},
"user_counter": {},
"user_growth_info": {}
}
}

我们一般会使用 console.log() 看一下它的值: console.log(obj)


image.png


我们点击这个按钮可以一层层的展开这个对象:


image.png


除了 console.log() 外,根据实际情况我们还可以使用下面几种。


console.dir


我们还可以使用 console.dir()。在使用它输出 JS 数据类型数据的时候它和使用 console.log() 的效果差不多:


image.png


我们展开这个对象,可以查看我们想看的数据:


image.png


当我们想打印出个某个 DOM 对象时就不一样了,使用 console.log() 输出的是这个 DOM 元素:


image.png


使用 console.dir() 输出的是这个 DOM 对象:


image.png


JSON.stringify()


我们还可以使用 console.log() 配合 JSON.stringify()


console.log(JSON.stringify(obj, null, 4))

运行效果如下:


image.png


可以看到,这里以字符串的形式将这个对象输出在了控制台。


console.table


我们还可以使用 console.table(),它会以一种表格的形式来输出结果:


image.png


可以看到,这样看着还是很整齐的。


如果我们要打印的是一个数组的话,使用 console.table() 输出数据,看起来会更方便一些:


const arr = ['a', 'b', 'c']
console.table(arr)

image.png


还有,输出多个数据的使用使用 console.table() 也有利于查看数据,如:


const a = 'a', b = 'b', c = 'c'
console.table({a, b, c})

效果如下:


image.png


consle.time 和 console.timeEnd


还有,在我们开发的过程中,有时候需要去看一段代码执行到底消耗了多少时间,我们可以使用 console.time()consle.timeEnd() 包裹想要测试运行时间的代码,比如下面这段代码:


function test() {
for (let i = 0; i < 10000; i++) { }
}

console.time()
test()
console.timeEnd()

运行代码,可以看到控制台输出了这段代码在本机大概的一个运行时间:


image.png



作者:程序员黑豆
来源:juejin.cn/post/7292969465298567187
收起阅读 »

只有一年前端经验,简历到底该怎么写?

Hello,大家好,我是雷布斯。 但不少同学还是会问“我是工作了 x 年的前端,该怎么改呢?”,“我觉得我的项目就是没有亮点,该怎么挖掘呢?”之类的问题。 那我们今天继续用一个真实的例子,给大家分享该用什么样的思路去优化自己的简历。 提前说明,参与简历修改分享...
继续阅读 »

Hello,大家好,我是雷布斯。


但不少同学还是会问“我是工作了 x 年的前端,该怎么改呢?”,“我觉得我的项目就是没有亮点,该怎么挖掘呢?”之类的问题。


那我们今天继续用一个真实的例子,给大家分享该用什么样的思路去优化自己的简历。


提前说明,参与简历修改分享的张同学,于今年 4 月参加了我们的面试全流程辅导,修改意见由我们团队的 uncle13 老师提供,关键信息已打码,并且已经获得该同学的授权来进行分享。


下面将按照简历中的模块顺序逐一进行分析。


个人简介 & 教育经历



这位同学可能是使用的简历模板,这两部分的内容都写的很简洁。


那么问题来了:



  • 简历右上角的二维码,扫出来是某简历网站,而且要求登录后才能查看电子简历,操作这么繁琐,放到纸质版的简历中是有什么目的呢?

  • 部分内容不标注清楚,面试官无法快速了解你,比如南京是现居地还是籍贯?zxxxxx.top是个人博客还是项目网站?


所以这部分可以优化下:



  • 去掉二维码

  • 把关键信息标注清楚。比如个人博客:https://xxxx.top

  • 教育经历如果没有太大的竞争力,可以考虑一块放到个人信息中,样式上会好看,改成:陕西理工大学 | 本科 | 网络工程


专业技能



简历中的专业技能是展示自己适合特定职位的能力和经验的重要方式。


但这位同学写的中规中矩,没有什么特色。


一般来说,面试官会从这个模块,了解到面试者的擅长的技术栈,熟练使用的工具等等。


编写清晰、明确的专业技能部分,有助于大家更好地展示自己的专业知识和经验。
但大家还是需要主要,对于自己写的所有关键字,都需要好好准备,面试官可以从任何一个感兴趣的方向进行提问。


针对这位同学的呢绒,可以这样进行优化:



  1. 熟练掌握HTML+CSS。熟练掌握弹性布局、网格布局以及响应式布局,能够快速实现Web端和移动端页面搭建。

  2. 熟练掌握 JavaScript,能脱离框架进行原生开发,熟悉 TypeScript

  3. 熟练使用 React 框架以及相关生态技术,能够独立完成项目搭建和项目部署,熟悉相关框架原理。

  4. 了解 vue 框架技术,有相关实践经验。

  5. 熟悉浏览器原理以及计算机网络相关技术,在性能优化方面有实践经验。

  6. 了解 Node.js 和常用模块,在此基础上能够使用 ExpressWeb 框架进行简单的服务器应用程序开发。

  7. 了解微信小程序原生开发,能够使用小程序原生开发进行开发。

  8. 对前端工程模块化有一定理解,熟悉 webpackvite 等打包工具及其日常开发配置,可以从 0 到 1 独立搭建项目,并优化构建流程。


大家发现没有,同样是描写熟悉的技术,但经过优化后,立马就变得高大上了。


工作经历



接着来看“工作经历”的模块。


其实比较推荐的介绍方式是,写清楚自己在公司的岗位、工作时间、个人的职责,同时也推荐加上公司的业务简介,不用太详细,1 - 2 句话简单介绍下就行。


在公司的个人职责就不要写的太简单了,你完全可以在这个模块,向面试官展示你的职业发展路径,工作经验和成果等等信息。


还有就是工作经历尽量用倒序的方式来组织,把最近的一份工作写在最上面。


还是以张同学的工作经历为例,我们可以这样修改:



  • 南京xx有限公司(前端开发工程师)20xx年xx月 - 至今


工作内容:



  1. 主要负责xx系统、xx系统等项目的业务开发工作,使用Vue+ElementUI技术栈。

  2. 通过使用虚拟列表等技术优化大列表性能,加快页面加载速度,提高用户体验。

  3. 参与各类项目,包括SPA应用、PC端和移动端的开发。

  4. 深入了解并熟练应用Webpack、ES6、Vue生命周期和组件通信等前端技术,持续提升自身技能水平。

  5. 在团队中积极沟通和合作,负责项目的需求分析、设计和开发,并对项目进展情况进行有效的跟踪和反馈。



  • 西安xxxx有限公司(实习前端开发)20xx年xx月 - 20xx年xx月


工作内容:



  1. 负责xx平台部分功能模块的前端开发工作。

  2. 和后端工程师配合完成接口联调,确保数据交互正常。

  3. 在实习期间积累了从需求分析到代码实现的全流程开发经验。


项目经历


“项目经历”和“工作经历”都是简历的重点。


之前分享的文章也介绍了很多挖掘项目亮点的方法,我们今天也不再过多介绍了。


现在直接来看张同学的例子。


项目一:生鲜到家



首先还是项目顺序的问题,最近的项目写在前面。


写的很简洁,一看就像是个人练手项目。但商城项目真的烂大街了,而且个人职责也完全体现不出这个项目的难度和亮点。


这样的项目,面试官真的是一点问的兴趣都没有。


不过这位同学附上了项目的访问地址,做了私有化的部署,能挽回一些印象分。


我们可以这样调整:



  • 项目名称:生鲜到家


项目简介:生鲜到家是一个在线购物商城,实现了商铺选品、购物计费、购物车、购物下单等功能。


项目地址xxxx.top/


技术栈:Vue3,Vuex,Vue-router,Axios,Mock.js


主要职责



  1. 负责整个项目前端生产环境的整体搭建(仓库创建、项目搭建、工程发布)以及项目管理。

  2. 采用 vue 技术生态开发了核心业务模块(购物车、商品选择等),并针对业务做了一些公共组件的抽离:通信、错误处理、图片优化以及其他业务组件的二次封装。

  3. 使用 Mock.js 进行数据模拟,并封装 Axios 发送请求,减少对后端接口的依赖。成功实现了前后端分离开发并独立部署上线。

  4. 使用 Vue3 组合式 API 进行项目逻辑的编写,提高了代码复用性和可维护性。同时,在模块化开发过程中,成功将代码行数降低了 20% 以上。

  5. 利用 Vue-router 路由钩子函数实现系统角色权限控制,并且统一了前端路由的交互模式。

  6. 结合业务以及 UI 规范封装了 css 原子化组件,组件开发效率提升了 30%

  7. 开发过程学了XX、XX、XX、并沉淀了相关文档 ,在社区与大家分享。


大家要注意,职责中的量化数据,可以让自己的工作更具体、更形象,也更有说服力。


项目二:xx旅行



其实看这个项目的介绍,也像是个人做的 demo。我们还是基于这个项目,再来介绍一种项目介绍方式。



  • xx旅行


项目简介:xx旅行是一款基于微信云开发模式的城市旅游小程序,包括景点浏览、预定、支付、评价等功能,并且配有一个后台管理系统。


主要职责



  1. 使用微信小程序云开发工具,从0到1完成了小程序的开发,实现了用户注册、登录、浏览、预定、支付、评价等核心功能。

  2. 利用Vue框架搭建前端页面,并使用 Koa2 搭建后台管理系统,实现了对景点信息、订单信息、评价信息等的管理和统计。

  3. 对小程序的性能进行了优化,提高了小程序界面响应速度和加载速度,缩短了用户等待时间。

  4. 借助微信云函数,实现了客户端和服务端之间的交互,提高了小程序的稳定性和可靠性。

  5. 配置小程序的各种环境变量,例如AppID、密钥等,成功将小程序发布上线。


项目成果



  1. 完成的小程序共计有 15个页面,为 xx 位用户提供了景点介绍服务。

  2. 对小程序进行的性能优化,平均响应时间由原来的 2s 降低至 500ms,提高了小程序流畅度和稳定性。

  3. 在后台的开发中,广泛应用 Vue 框架和 ElementUI 组件库,提高了代码的可读性和可维护性。


可以看到从项目简介、主要职责和项目成果三个方面对项目进行介绍,结构清晰,内容也很丰富。


张同学还有几个项目,就不再这儿一一列举了。


最后


相信看到了这儿的同学,都有不少收获,修改简历的技能又有所增长,仿佛知道了怎么去做简历的优化。


如果有的同学还要抱怨,“道理我都懂,但我怎么还是改不好自己的简历?”,那么推荐这样的同学报名我们的辅导服务,大厂导师一对一辅导,绝对物超所值。


作者:前端时代周刊
来源:juejin.cn/post/7254081954517073980
收起阅读 »

你知道 XHR 和 Fetch 的区别吗?

web
现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHR 和 Fetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的...
继续阅读 »

现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHRFetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的新兴标准。接下来,我们将一同深入学习它们的使用方法和适用场景。


XMLHttpRequest


XMLHttpRequest,通常简称为 XHR。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。XMLHttpRequest 在 AJAX 编程中(比如 jquery)被大量使用。



AJAX :异步 JavaScript 和 XML。许多人容易把它和 jq 的 ajax 混淆。它是一个技术统称,本身不是一种技术。



特点



  1. 异步请求:XHR 允许进行异步请求,它可以在后台执行,而不会阻止页面的其他操作。

  2. 支持跨域请求:通过服务器端设置允许跨域请求,从不同域的服务器获取数据。

  3. 事件驱动:提供了 onloadonerroronprogress 等一系列事件来监听请求的状态变化。

  4. 灵活性:提供了对请求头、响应头以及请求方法的完全控制,使其非常灵活。


工作原理


XHR 的工作原理主要为:



  1. 创建 XHR 对象实例:通过new XMLHttpRequest()创建一个 XHR 对象。

  2. 配置请求:使用open()方法设置请求方法(GET、POST 等)、URL,以及是否要异步执行请求。

  3. 设置回调函数:设置事件处理程序来处理请求完成、成功、失败等不同的状态。

  4. 发起请求:使用send()方法发送请求。

  5. 处理响应:在事件处理程序中处理响应数据,通常使用responseTextresponseXML来访问响应内容。


// 创建一个新的XHR对象
const xhr = new XMLHttpRequest();

// 配置请求
xhr.open("GET", "https://api.baidu.com/test", true);

// 设置响应处理函数
xhr.onload = function() {
if (xhr.status === 200) {
// 请求成功
const responseData = xhr.responseText;
console.log("成功获取数据:", responseData);
} else {
// 请求失败
console.error("请求失败,状态码:" + xhr.status);
}
};

// 发起请求
xhr.send();

XHR 的响应处理通常在onreadystatechange事件处理程序中完成。在上面的例子中,我们等待 XHR 对象的状态变为 4(表示请求完成)并且 HTTP 状态码为 200(表示成功响应)时,解析响应数据。


Fetch API


Fetch 是一种现代的数据网络请求 API,它旨在解决 XHR 的一些问题,提供了更强大、更灵活的方式来处理 HTTP 请求。可以理解为 XMLHttpRequest 的升级版。


特点



  1. Promise 风格:Fetch API 使用 Promise 对象来处理异步请求,使代码更具可读性和可维护性。

  2. 更简单的语法:相较于 XHR,Fetch API 的语法更加简单明了,通常只需要几行代码来完成请求。

  3. 默认不接受跨域请求:为了安全性,Fetch API 默认不接受跨域请求,但可以通过 CORS(跨域资源共享)来进行配置。

  4. 更现代的架构:Fetch API 是建立在 PromiseStream 之上的,支持更灵活的数据处理和流式传输。


工作原理


Fetch 的工作原理主要为:



  1. 使用fetch()函数创建请求:传入要请求的 URL,以及可选的配置参数,例如请求方法、请求头等。

  2. 处理响应:fetch()返回一个 Promise,您可以使用.then()链式调用来处理响应数据,例如使用.json()方法解析 JSON 数据或.text()方法获取文本数据。

  3. 错误处理:您可以使用.catch()方法来捕获任何请求或响应的错误。

  4. 使用async/await:如果需要,您还可以使用async/await来更清晰地处理异步操作。


Fetch API 的特性和简单的语法使它在许多前端项目中成为首选工具。然而,它也有一些限制,例如不支持同步请求,因此需要谨慎使用。


fetch("https://api.baidu.com/test")
.then(response => {
if (!response.ok) {
throw new Error("请求失败,状态码:" + response.status);
}
return response.json();
})
.then(data => {
// 请求成功,处理响应数据
console.log("成功获取数据:", data);
})
.catch(error => {
// 请求失败,处理错误
console.error(error);
});

XHR 和 Fetch 的对比


XHR 和 Fetch 都用于进行 HTTP 请求,但它们之间存在一些关键区别:



  • 语法: Fetch 使用 Promise,更直观和易于理解。

  • 跨域请求: Fetch 在跨域请求方面更灵活,支持 CORS。

  • 流式传输: Fetch 支持可读流,适用于大文件下载。

  • 维护性: Fetch 更容易维护和扩展。


常用库和插件


基于 XHR 封装的库



  • jquery:一个 JavaScript 库,提供了用于处理 DOM 操作、事件处理和 XHR 请求的便捷方法。

  • axios:一个流行的 HTTP 请求库,基于 XHR 开发,支持浏览器和 Node.js。


基于 fetch 封装的库



  • redaxios:它具有与 axios 类似的 API,但更轻量级且适用于现代 Web 开发。

  • umi-request:由 Umi 框架维护的网络请求库,提供了强大的拦截器、中间件和数据转换功能。


总结


XMLHttpRequest (XHR) 和 Fetch API 都是前端开发中用于进行数据请求的有力工具。XHR 在传统项目中仍然有用,而 Fetch API 则在现代 Web 开发中越来越流行。具体选择哪个工具取决于项目的需求和开发团队的偏好,希望本文对你有帮助!


作者:王绝境
来源:juejin.cn/post/7295551704816189467
收起阅读 »

以订单退款流程为例,聊聊如何优化策略模式

如果有人问你什么是策略模式?你可以尝试这样回答 策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。 在策略模式中,通常有一个抽象的策略接口,定义了一系列可以...
继续阅读 »

如果有人问你什么是策略模式?你可以尝试这样回答



策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。


在策略模式中,通常有一个抽象的策略接口,定义了一系列可以被不同策略实现类所实现的方法。在使用时,可以根据需要选择合适的具体策略类,并通过接口进行调用。



虽然解释了策略模式,但是面试官可能会认为你在吊书袋,完全没有自己的理解。我因为被领导卷到了,所以对策略模式有一些其他的理解,接下来我从业务中台实际遇到的问题出发,谈谈怎么被领导卷到,谈谈我对策略模式的优化和理解。


在业务中台,可以根据具体情况选择合适的策略类来执行相应的业务逻辑,从而满足不同业务方的要求。


由于各个业务方的业务逻辑存在差异性和相似性,编写中台代码时需要考虑这些差异性,并预留扩展点以供不同业务方根据自身需求提供策略类。在这种情况下,可以应用策略模式。


image.png


当业务方有特殊的业务逻辑时,只需要添加新的策略实现类即可,不需要修改现有的代码,方便了后续的维护和扩展。


然而在我们在应用策略模式时遇到了几个问题


遇到的实际问题


由于业务中台逻辑非常复杂,每个业务线的业务场景都很难保证完全一样,在代码实践中我们的系统出现了非常庞大复杂的策略类,将大量业务逻辑整合到同一个策略中,这导致系统能力复用非常困难。


举个例子,在退款校验逻辑中 业务场景 A,(售卖红包,类似于美团饿了么会员)。 需要判断如下校验逻辑,如果命中,则不允许退款!




  1. 订单是否在生效期,过期不允许退




  2. 订单是否在已使用,已使用不可退




  3. 订单如果是某类特殊商品订单,则不可退




  4. 如果超过当天最大退款次数,则不可退。




于是我们在编写代码时,就新定义 ConcreteStrategyA ,将以上 4 个业务逻辑放到策略类中。


过一段时间,又出现了业务场景 B(售卖红包,类似于美团饿了么会员)。它的校验逻辑和A 大同小异




  1. 订单是否在生效期,过期不允许退




  2. 订单售卖的红包完全使用不可退,部分使用可以退。




  3. 如果超过当天最大退款次数,则不可退。




业务场景B 相比A 而言,少校验了 “特殊商品订单不可退”的逻辑,同时增加了部分使用可以退的逻辑。


如何设计代码实现两种业务场景的校验逻辑呢? 这是非常具体的问题,如果是你如何实现呢?


完全独立的策略类


我们在最开始写代码时,分别独立实现了 ConcreteStrategyA、ConcreteStrategyB。两者的校验逻辑各自独立,没有代码复用。


此时的系统类图如下


image.png


这种实现方式是大量的代码拷贝,例如退款次数限制、生效期校验等代码都大量重复拷贝了。


后来我们发现了这个问题,将相关校验方法抽到父类中。


继承共同的父类


为了更好的复用校验策略,我们将校验生效期的方法、校验退款次数的方法抽取到共同的父级策略类。由具体的子策略类继承BaseStrategy 父策略类,在校验逻辑中,使用父级的校验方法。


如下图的类图所示
image.png


在相当长的一段时间里,我们认为这已经是最优的实现方式了。


但是被领导卷到了!


image.png


被卷到了


“新增业务场景时,为什么校验逻辑都是复用的原有能力,还需要新增扩展类,还需要开发代码呢?” 领导这样问道。


我尝试回答领导的问题:“开发这段代码,并不算太难,只需要增加一个扩展类就可以了!”


“你数数现在有多少个扩展类了?” 领导似乎有些生气,


我一看扩展类的数量,被吓到了,已经有了15个扩展类。这一刻真的非常尴尬,平时领导从不亲自看代码。估计是突然心血来潮。从表情来看,他好像很生气,估计是代码没看懂!


“我看到这部分代码时,想要查看具体的策略类,但Idea直接刷出了15个策略类……为什么会有这么多的扩展类呢?” 领导进一步补充道。


场面僵住了,我也没什么好办法……。当然领导向来的传统是:只提问题,不给解决办法! 我只能自己想解决办法!如何解决策略类过多、扩展类膨胀的问题呢?


image.png


策略类过多怎么办!


当业务场景非常多,业务逻辑非常复杂时,确实会出现非常多的策略类。但领导的问题是,现有业务场景有很多相似性,某些新增业务场景和原有业务场景类似,校验逻辑也是类似的,为什么还需要新增扩展类呢?


经过深思熟虑,让我发现问题所在!


策略类粒度太粗,导致系统复用难


目前我们的系统设计是每个业务场景都有单独的策略,这是大多数人认可的做法。不同的业务场景需要不同的策略。


然而,仔细思考一下我们会发现,不同业务场景的退款校验逻辑实际上是由一系列细分校验逻辑组合而成的,退款校验逻辑在不同的业务场景中是可以被重复使用的。


为什么不能将这些细分退款校验逻辑抽象为策略呢?例如,将过期不允许退款、已生效不可退款、超过最大退款次数不允许退款等校验逻辑抽象成为校验策略类。


各个业务场景组合多个校验策略,这样新增业务场景时,只需要组合多个校验策略即可。


如何组合校验策略


首先需要抽象校验策略接口: VerifyStrategy


classDiagram
class VerifyStrategy{
+void verify(VerifyContext context);
}

然后定义 VerifyScene 类


classDiagram
class VerifyScene{
+ Biz biz;
+ List<VerifyStrategy> strategies;
}

如何把对应具体策略类配置到VerifyScene中呢?本着 ”能不开发代码,就不要开发代码,优先使用配置!“的原则,我们选择使用Spring XML 文件配置。(在业务中台优先使用配置而非硬编码,否则这个问题不好回答。“业务方和中台都需要开发,为啥走你中台?”)


使用Spring XML 组合校验策略


在Spring XML文件中,可以声明VerifyScene类的Bean,初始化属性时,可以引用相关的校验策略。


 <bean name="Biz_A_Strategy" p:biz="A" class="com.XX.VerifyScene">
<property name="strategies">
<list>
<ref bean="checkPeriodVerifyStrategy"/> <!--校验是否未过期-->
<ref bean="checkUsageInfoVerifyStrategy"/> <!--校验使用情况-->
<ref bean="checkRefundTimeVerifyStrategy"/><!--校验退款次数-->
</list>
</property>
</bean>

当需要新增业务场景时,首先需要评估现有的校验策略是否满足需求,不满足则新增策略。最终在XML文档中增加 VerifyScene 校验场景,引用相关的策略类。


这样新增业务场景时,只要校验逻辑是复用的,就无需新增扩展类,也无需开发代码,只需要在XML中配置策略组合即可。


在XML文档中可以添加注释,说明当前业务场景每一个校验单元的业务逻辑。在某种程度上,这个XML文档就是所有业务的退款校验的业务文档。甚至无需再写文档说明每个业务场景的退款策略如何如何~


和领导汇报以后,领导很是满意。对业务方开始宣称,我们的中台系统支持零开发,配置化接入退款能力。


结束了吗?没有 ,我们后来想到更加优雅的方式。


使用Spring Configuration 和 Lamada


Spring 提供了@Bean注解注入Bean,我们可以使用Spring @Bean方式声明校验策略类


@Bean
public VerifyStrategy checkPeriodVerifyStrategy(){
return (context)->{
//校验生效期
};
}

通过以上方式,可以把checkPeriodVerifyStrategy 校验策略注入到Spring中,spring beanName就是方法名checkPeriodVerifyStrategy。


在Spring XML中可以使用 <ref bean="checkPeriodVerifyStrategy"/> 引用这个bean。


并且当点击XML中beanName时,可以直接跳转到 被@Bean修饰的checkPeriodVerifyStrategy方法。这样在梳理校验流程时,可以很方便地查看代码


点击这个BeanName,会跳转到对应的方法。(付费版Idea支持,社区版 Idea 不支持这个特性)
image.png


总结


总结几个问题




  1. 策略模式目的是:根据不同的业务场景选择不同的策略来执行相应的逻辑




  2. 策略模式一定要进行细化,通过组合多个细分策略模式为一个更大的策略,避免使用继承方案。




  3. 使用Spring XML 组合多个策略模式,可以避免开发。减少新增策略类




  4. 使用Spring Configuration @Bean 将策略类注入Spring 更加优雅




作者:他是程序员
来源:juejin.cn/post/7295010992122101801
收起阅读 »

从码农到工匠-怎么写好一个方法

感谢你阅读本文 今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。 后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架...
继续阅读 »

感谢你阅读本文


今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。


后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架的实现,逐渐对于开始理解书中的思想。


下文只列举了书中的的“方法封装”,“参数”和“方法短小”这三个,其他的没有列举,因为我觉得其实只要这三点我们执行到位,写出来的代码虽然谈不上优秀,但是维护性和可读性一定会大大提高。


封装的艺术


我们在学习面向对象编程时就知道面向对象的几大特征是抽象,继承,封装,多态,所以学会合理的封装是一件很重要的事情,这也是我们每个程序员应该有的意识。


判断封装


在方法中,总是难免会有很多判断,判断越多,程序就会变得越复杂,但是又不能不判断,我们可以使用设计模式来改善判断很多的方法,使用设计模式来优化判断,比如策略模式,但是其实它并没有消除判断,换句话说,判断是消除不了,我们能做的只能是让判断的代码可读,下面我们来看一段代码。


如下是一个判断,对文件小于1G,类型为txt,文件名中包含user_click_data或者super_user_click_data的文件可以进行处理


对于这个判断,虽然条件并不复杂,但是在代码中就显得“不清洁”,特别是随着系统的不断迭代,加入的人员不断增多,那么判断将会越来越多,如果开发人员加上清晰的注释,那么就比较容易读懂,但是并不是谁都有这样的习惯。


之前我遇到的代码中,有些一个方法中四五十个判断,而且判断还特别复杂,各种&&,||等一大堆,读起来十分费劲,对于这样的代码,遇到真的会让人抓狂。


如何解决呢?


封装判断就是一个好的方案,上面的判断代码我们就可以进行封装,如下。



上面我判断我们将其抽出来作为一个单独的方法,在加上可理解的方法名,那么别人在读你代码的时候就很容易读懂,也保证了代码的清洁,当然,方法命名是一件很难的事情,不过如果命名得不够准确,我们可以加上清晰的注释来解决。


虽然上面这段代码看似没啥改变,但是随着系统的不断复杂,就能体现出它的作用了,我经常看到别人在主流程的代码中加入很多复杂的判断,在我刚开始写代码的时候,也做过这种事,很多时候如果注释不清,命名不规范,上下文不清晰,那么读起来就很痛苦,而且整个方法看起来让人特别难受。


所以,一个看似很小的改动,却能在时间的作用下显得如此重要,保持这样一个习惯,不仅是对自己负责,也是对他人负责。


参数问题


参数问题也是我们应该去考虑的问题,当在一个方法中,如果参数特别多是很不友好的,我之前碰到过一个方法有十五六个参数,首先这么多个参数,在传递的时候,如果使用对IDEA使用比较熟悉,那么可以使用快捷键查看下一个参数是什么类型,如果使用不熟悉,那么就需要点进方法中去看,这是一个体力活。


加上参数越多,维护成本就越高,比如我需要加一个参数,但是这个参数并不是必须的,有些方法用到,有些不用,但是由于使用的是同一个方法,所以调用这个方法的地方都需要加上这个参数,调用的地方越多,成本就越大,如果是一个RPC接口,修改的成本更大。


复杂的参数封装成类。


对于复杂的参数,推荐封装成类然后进行传递,在添加或者减少字段的时候是在类中进行,没有对形参进行增加或者减少,对调用此方法的地方基本没任何影响,我们只需要在需要添加或者减少这个参数的地方进行操作,不需要的地方就保持不动。


方法尽量保持短小


方法短小更容易让人阅读和维护,当我去阅读别人的代码的时候,有些类有七八千行代码,一个方法有五六百行代码的时候,其实内心是奔溃的!


当然,在我刚开始写代码的时候,我也不懂,所以一个方法从来不去优化,也是一个方法一口气干到底,我记得当时给老师做一个项目,一个类我在Controller层直接写了几百行,Service层都不用,还有写Vue,也不会去使用组件和封装函数,所以一个Vue文件几千行,当需要修改的时候,十分麻烦,找一个方法和变量也特别费力。


记得上一个星期,我和前端同事联调,我看他去找一个方法找了足足三四分钟,因为项目十分庞大,加上设计有很大的缺陷,我看他们一个VUE文件可达到八九千行,所以才出现这个问题。


其实无论对于前端还是后端,Java还是golang等等,代码的短小都比较容易去阅读和维护,如果发现方法越写越大,那我们就要考虑去提取,去拆分了,《代码精进之路》作者推荐Java一个方法不要超过20行,不过也不是绝对的。


对于像Spring这种特别复杂的框架,因为不断升级和迭代,所以有些方法也变得特别长,代码也变得可读性不那个高,但是也不影响它是Java生态中使用最广泛,最强大的框架。


只不过对于我们普通人来说,因为水平肯定没有Spring团队的强,所以保证方法的可读性是十分重要的。


今天的分享就到这里,感谢你的观看,我们下期见。


参考:《代码精进之路,从码农到工匠》weread.qq.com/web/reader/…?


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

开发一个简单的管理系统,前端选择 Vue 还是 React?

web
在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。 我们在低代码开发领域探索了多年...
继续阅读 »

在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。



我们在低代码开发领域探索了多年,从2014 开始研发低代码前端渲染,到 2018 年开始研发后端低代码数据模型,发布了JNPF快速开发平台。


JNPF是一个Vue2/Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可完成业务需求。


前端采用的是Vue、Element-UI…;后端采用Java(.net)、Springboot…;使用门槛低,支持分布式、k8s集群部署,适用于开发复杂的业务管理系统(ERP、MES等);采用可视化组件模式可以有效地扩展不同的业务功能,并方便实现各种业务需求,且不会导致系统臃肿,若想使用某个组件,按需引入即可,反之亦然。



低代码平台的前端框架采用Vue的优势有哪些?




  •  Vue是组件化开发,减少代码的书写,使代码易于理解。




  •  最突出的优势在于可以对数据进行双向绑定。




  •  相比较传统的用超链接进行页面的切换与跳转,Vue使用的是路由,不用刷新页面。




  •  Vue是单页应用,加载时不用获取所有的数据和dom,提高加载速度,优化了用户体验。




  •  Vue的第三方组件库丰富,低代码平台能够获得更多的支持和资源。




JNPF-Web-Vue3 的技术栈介绍


JNPF 快速开发平台的 Vue3.0 版本是基于 Vue3.x、Vue-router4.x、Vite4.x、Ant-Design-Vue3.x、TypeScript、Pinia、Less 的后台解决方案,采用 Pnpm 包管理工具,旨在为中大型项目做开发,提供开箱即用的解决方案。前端同时适配Vue2/Vue3技术栈。


以下对各项技术做简单的拓展介绍:


(1)Vue3.x

Vue3.x 作为一款领先的 JavaScript 框架,通过响应式数据绑定和组件化架构实现高效的应用开发。相较于 Vue2.x,在大规模应用场景下,Vue3.x 的渲染速度提升了近 3 倍,初始化速度提升了 10 倍以上,这不仅为我们提供了更出色的用户体验,也为企业应用的开发和维护提供了极大的便利。


此外,它所支持Composition API 可以更加灵活地实现代码复用和组件化,让我们的代码更加可读、可维护。总而言之,Vue3 在许多方面都进行了改进,包括更好的性能、更少的代码大小和更好的开发体验。


(2)Vue-router4.x

Vue-router4.x 作为 Vue.js 框架中的路由管理器,具备出色的性能和扩展性,为开发者提供了一种高效而灵活的前端路由解决方案。Vue Router 主要用于构建单页应用程序,允许创建可导航的Web 应用,使您可以轻松地构建复杂的前端应用。


(3)Vite4.x

一个基于 ES Module 的 Web 应用构建工具。作为一种全新的开发模式,Vite 相对于Webpack 更加出色,内置了许多优化手段,包括 HMR、代码分割、CSS 提取、缓存策略等,从而在保证开发速度的前提下,为应用程序的加载速度和性能提供了极致的保障。此外,它还支持快速的冷启动、模块化的打包方式以及自动化的多页面构建等特性,极大的提升了前端开发效率。


(4)Ant-Design-Vue3.x

一款基于 Vue3.x 的企业级 UI 组件库,旨在帮助开发者快速搭建出高质量、美观且易用的界面。不同于其他类似的组件库,Ant-Design-Vue3.x 更注重用户体验和可定制性,提供了一整套视觉、交互和动画设计解决方案,结合灵活的样式配置,可以满足大部分项目的UI 需求,帮助开发者事半功倍。


(5)TypeScript

TypeScript 作为一种静态类型的 JavaScript 超集,不仅完美兼容 JavaScript,还提供了强大的静态类型约束和面向对象编程特性,极大地提升了代码的可读性和重用性。TypeScript拥有强大的类型系统,可以帮助开发者在代码编写阶段发现潜在的错误,减少未知错误发生概率,并提供更好的代码补全和类型检查。这一特性让团队协作更加高效,同时也降低了维护代码的成本。


(6)Pinia

Pinia 是 Vue3.x 的状态管理库,基于 Vue3.x 的 Composition API 特性,为开发者提供了清晰、直观、可扩展和强类型化的状态管理方案,可以更好地管理应用数据和状态。无论是在小型项目还是庞大的企业级应用中,我们都可以依靠这个强大的状态管理库来迅速构建出高质量的应用。


(7)Less

一种 CSS 预处理器,能够以更便捷、灵活的方式书写和管理样式表。通过 Less,开发者可以使用变量、嵌套规则、混合、运算、函数等高级功能,使得样式表的编写更加简单、易于维护。使用 Less 不仅可以提高 CSS 开发效率,还可以生成更快、更小的 CSS 文件,从而减少网站加载时间,提升网站性能。


(8)Pnpm

Pnpm 作为一种快速、稳定、安全的包管理工具,它能够帮助我们管理 JavaScript 包的依赖关系,通过采用更为精简的数据存储结构,极大地减少冗余数据的存储,从而有效地节省磁盘空间。


其他亮点


作为一款基于SpringBoot+Vue3的全栈开发平台,满足微服务、前后端分离架构,基于可视化流程建模、表单建模、报表建模工具,快速构建业务应用,平台即可本地化部署,也支持K8S部署。


引擎式软件快速开发模式,除了上述功能,还配置了图表引擎、接口引擎、门户引擎、组织用户引擎等可视化功能引擎,基本实现页面UI的可视化搭建。内置有百种功能控件及使用模板,使得在拖拉拽的简单操作下,也能大限度满足用户个性化需求。


如果你是一名开发者,可以试试我们研发的JNPF开发平台。基于低代码充分利用传统开发模式下积累的经验,高效开发。


最后,给予一点建议


关于Vue,简单易上手,官方的文档很清晰,易于使用,同时它拥有更好的新能且占据的空间相比其他框架更少,同时vue的学习曲线是很平滑的,所以这是我为什么推荐优先学习vue的原因,对于新手来说易上手,快速帮助新手熟悉一些中小型的项目,但是对于大型的项目,这就要说到Vue响应机制上的问题了,大型项目的state(状态)是特别多的,这时watcher也会很多,进而导致卡顿。


对于React,主要是适应大型项目,由于React灵活的结构和可扩展性,相比Vue对于大型项目的适配性更高,此外其跨浏览器兼容、模块化、单项数据流等都是其优点,但是与Vue相反的就是它的学习曲线是陡峭的,由于复杂的设置过程,属性,功能和结构,它需要深入的知识来构建应用程序,这对于新手来说是不太适合作为一个入门级别的框架。


作者:冲浪中台
来源:juejin.cn/post/7295565904405790761
收起阅读 »

用1100天做一款通用的管理后台框架

web
前言 去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。 很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大...
继续阅读 »

前言


去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。


很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大家对这种非干货的文章也挺感兴趣。于是在这个三年的时间点上(没错,也就是1100天),我打算继续出来和大家唠唠,这一年我又做了些什么事,或者说,如何把一款好的后台框架变得通用?


题外话:如果你对我以前的文章感兴趣,可以点我头像进入主页查看;如果你期待我以后的文章,也可以点个关注。


痛点


因为 Fantastic-admin 是基于 Element Plus 这款 UI 组件库进行开发的,于是今年我陆陆续续被问到一些问题:



  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?

  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?

  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



类似的问题一多,我也在思考一个问题:我的这款框架是不是被 Element Plus 绑架了?如果开发者在做技术选型的时候,因为 UI 组件库不符合预期,而将我的框架筛掉,这是我不希望看到的结果。


基于这个潜在隐患,我开始计划对框架进行转型。


方案


方案一


既然开发者对 UI 组件库有各自的偏好,我又想拉拢这部分开发者,那是不是多出几套不同 UI 组件库版本的就可以了呢?没错,这是我最开始冒出来的念头。


我参考了一些同类产品的做法,尽管它们把不同 UI 组件库版本做得很像,但在使用体验过程中,还是会带来操作上的割裂感。并且因为无法抹平不同 UI 组件库在 API 上的差异,导致在框架功能上,不同版本之间也会有一些差异。



你可以分别对比左右或者上下两张图,包括左侧导航栏的样式、导航收起/展开按钮的位置、右侧项目配置中提供的功能等,都能明显发现它们的差异。


虽然这可能不是什么大问题,但我认为视觉风格上的统一是能帮助产品提高识别度的。就比如上面 4 款基于不同 UI 组件库开发的后台框架,虽然它们属于同一个产品,但如果我不告诉你,你未必能通过图片确定它们师出同门。


其次就是后台框架提供的功能不统一,这里面有一定的原因是因为 UI 组件库导致的。试想一个场景,如果你要从 Element Plus 版本的后台,迁移到 Ant Design Vue 版本的后台,框架的配置文件是否能原封不动的复制过去?如果导航(路由)数据是后端返回的,数据结构能否保持完全一致,后端无需做任何修改?因为不同 UI 组件库对菜单组件的使用方式是完全不同的,比如 Element Plus 是需要手动拼装的,而 Naive UI 则是数据驱动的,只需要传入一个树形结构的数据给组件即可。如果数据结构无法保证一致,就会增加迁移和学习的成本。


最后就是我的一点私心,因为多一个 UI 组件库的版本,势必会占据我更多的业余时间,如果同时维护 4、5 个版本,那我大概下班后的所有时间都要投入到其中,并且如果未来又有新的 UI 组件库成为流行,那就又多一个版本的维护,这并不是一个可持续发展的方案。


方案二


既然上一个方案不符合我的期望,于是我开始思考,框架本身能不能不依赖这些 UI 组件库?如果框架本身不依赖于三方的 UI 组件库,那开发者不就可以根据需要自行引入想要的组件库了么。



就如上图,主/次导航和顶栏是属于框架的部分,而这部分其实并没有用到太多 UI 组件库提供的组件,以 Element Plus 举例,我统计了一下目前 Fantastic-admin 用到的组件:



  • Menu 菜单(主/次导航)

  • Breadcrumb 面包屑(顶栏)

  • Popover 气泡卡片(顶栏右侧的工具栏)

  • Dropdown 下拉菜单(顶栏右侧的工具栏)

  • Drawer 抽屉(应用配置)

    • Message 消息提示

    • Button 按钮

    • Input 输入框

    • Radio 单选框

    • Select 选择器

    • Switch 开关

    • …(等等表单类组件)




可以看到,虽然抽屉组件里用了很多表单类的组件,但这部分组件都是在应用配置里使用的,而应用配置这个模块,主要是方便在线测试框架提供的各种功能,在实际业务开发中,是完全不需要这个模块的。



所以初步算下来,后台框架真正依赖于 Element Plus 实现的组件就只有 4 个:



  • Menu 菜单

  • Breadcrumb 面包屑

  • Popover 气泡卡片

  • Dropdown 下拉菜单


那我为什么不找一些独立的第三方插件替代呢?是的,这是我第二个方案,就是找一些独立的插件替换 UI 组件库中的组件。但问题也立马迎面而来,就是偌大一个 Github ,居然找不到符合我需求和审美的插件。


比如菜单插件,我希望它和 Element Plus 里的菜单组件在功能上没有太大差异,支持水平/垂直模式、支持折叠收起、支持设置默认激活菜单、支持默认展开等。


比如面包屑插件,或许是因为这个插件功能太简单,并且大部分 UI 组件库都有提供,在 Github 能搜到独立的面包屑插件很少,搜到的也基本上是 N 年前的上传的,既没有人维护,风格样式也很丑。


这个方案似乎也行不通……吗?


方案三


虽然方案二在实施的第一步就扑街了,但有一点思路还是正确的,就是让框架本身不依赖于三方 UI 组件库。既然网上搜不到合适的插件,那我为什么不自己写一个呢。


比如面包屑,这是一个很简单的功能,任何前端初学者应该都可以写一个面包屑组件。


而气泡卡片和下拉菜单我没有计划自己写,因为找到了一个还不错的插件 Floating Vue,它由 Vue 团队核心人员开发并维护,并且最重要的是它支持自定义样式,意味着我可以将它魔改成想要的样子,尽可能和我的框架在视觉风格上保持统一。


最后一个比较难啃的骨头就是菜单,因为找不到合适的替代品,自己写的话又比较有挑战,虽然我有一点实现思路,但不多。当然最终还是决定自己写一个,因为觉得三方 UI 组件库这么多,实在写不出来我就去读他们源码,总不能每一个源码我都读不懂吧。


这 4 个组件的替换方案确定后,剩下就是抽屉组件和它里面的一些表单组件了,这些要怎么解决呢?这会我想到了 Headless UI ,它是完全无样式的 UI 组件库,通过与 Tailwind CSS / UnoCSS 集成使用,可以快速构建出属于自己风格的组件。


但是 Headless UI 提供的组件非常有限,并不能覆盖我需要的表单组件。不过它的设计给了我启发。表单组件我并不需要非常复杂的功能,原生的表单控件其实就能满足我的使用需求,只是原生的样式比较丑,和我想要的风格不统一,那我只需要给他们定制一套统一的风格就可以了,也就写一套原子化的 CSS 样式。


于是,方案敲定,开始实操。


实操


我决定从易到难开始处理,因为这样在初期能快速看到进度推进,也避免一上来就被一个菜单功能卡住好几天,甚至十几天都没有进展,打击到自己的信心。


1. 面包屑


和预期一样,并没有什么难度,很轻松就实现了。只不过目前还是保持和 Element Plus 一样的使用方式,就是需要手动拼装,后期计划改成数据驱动的使用方式。



2. 气泡卡片 & 下拉菜单


这部分参考了 nuxt/devtoolsFloating Vue 的自定义样式,以及 nuxt/ui 中下拉菜单的样式风格,最终形成了我自己满意的风格



3. 抽屉


使用了 Headless UI 中的 Dialog 组件,因为它和抽屉组件有相同的交互方式,它们都是在遮罩层上展示内容,只不过 Dialog 更多时候是居中展示,而抽屉则是在左右两侧展示。


其次在使用过程中,发现 Headless UI 中的 Transition 组件是一个惊喜。虽然 Vue 本身就有提供 <transition> 组件用于处理过渡动画,但有一个场景会比较难处理,官方的描述是:



This technique is needed when you want to coordinate different animations for different child elements – for example, fading in a Dialog's backdrop, while at the same time sliding in the contents of the Dialog from one side of the screen.
当您要为不同的子元素协调不同的动画时,就需要使用这种技术,例如,在淡入对话框背景的同时,从屏幕的一侧滑入对话框的内容。



这说的不就是抽屉组件么?于是按照官方的示例,修改了整体风格,最终效果也就出来了。



4. 表单组件


之前的计划是修改原生表单控件的样式,但在开发过程中发现会有一定的局限性。比如 <select> 无法控制弹出选项框的样式,我的解决办法就是用 Floating Vue 封装模拟一个 select 组件。


同时也在开发过程中发现了一些被遗漏组件,于是边做边补,最终大概做了 10 多个组件。虽然看着不少,它们都秉持着最小可用的状态。什么意思呢?就是我不会给它们设计太多的 API ,因为它们的定位和三方 UI 组件库不同,它们只要满足框架本身使用即可,用不到的 API 不会进行开发。并且使用上也不会有太大负担,如果不是对框架进行二次开发,开发者是可以完全不用关注这部分组件。



5. 菜单


菜单组件确实是个难啃的骨头,我差不多用了 3 周的晚上时间去开发。


第一周,按照自己的思路徒手撸,做到一半卡壳,做不下去了;


第二周,开始看 Element Plus 、Naive UI 、Ant Design Vue 里菜单的源码;



Ant Design Vue 的没看懂,放弃;


Naive UI 的看到一半发现核心实现被作者封装到 treemate 这个独立包中了,虽然这个包是开源的,目的也是针对树形结构的一体化解决方案。但我粗略看了一遍文档,感觉有点大材小用,因为它有很多 API 我是用不到的,而我对菜单组件又有一些自己的想法,不确定是否它这个包能否满足我的需求,放弃;


最后选择看 Element Plus 的,通过在本地一点点打印数据,大概理解了实现思路,但组件递归调用,父子组件通过 provide / inject 传递数据和函数的方式,数据状态的变动也是一层层向上级组件通知,直到通知到顶层组件,在我看来有点不太优雅,如果数据能统一在顶层组件里操作就好了。其次我的计划是写一个数据驱动的菜单组件,而不是像 Element Plus 需要手动拼装的,所以虽然我大致看懂了 Element Plus 菜单组件是怎么实现的,但在我自己实现的时候,还是有很大的不同,能参考的代码并不多。


这部分的开发总结,我可能会在以后单独写一篇文章详细说说,因为这部分也是整个方案中唯一的难点。



第三周,因为实现思路大致有了,所以开发上就没有太多的卡壳,最终结果也还不错,基本达到了我的需求。


同时因为组件完全可控,顺带解决了之前使用 Element Plus 菜单组件上无法解决的 bug ,比如当菜单收起时,弹出的悬浮菜单如果数量过多,超出屏幕高度,超出的部分就无法查看了,就像这样:



但是现在则会有滚动条,使用体验上更舒服。



验证


至此,我的后台框架已经摆脱对 Element Plus 的依赖,接下来就需要验证一下是否可以方便的替换成其他 UI 组件库。


我分别用 Ant Design Vue 、Arco Design Vue 、Naive UI 、TDesign 这四款热度比较高的组件库进行了验证:















Ant Design Vue Arco Design Vue Naive UI TDesign

结果还是很满意的,都能够顺利替换,并且替换过程并没有花费很多时间,一个小时内就可以替换成功。



由于登录页这个特殊的存在,替换组件库后是需要对其用到的 Element Plus 组件进行手动修改的,这部分会比较花时间,因为会涉及到表单验证之类的东西,不同组件库的写法差异还是比较大的。



详细的替换步骤可以在 Fantastic-admin 官方文档里找到。


回顾


让我们重新看下一开始的痛点是否都解决了么:




  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?



    虽然不会有,但可以自己动手,根据教程将默认的 Element Plus 替换成你想要的 UI 组件库就可以了





  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?



    不会有冲突,现在可以彻底移除 Element Plus ,安装并使用自己的 UI 组件库





  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



    可以用 Fantastic-admin 源码先进行 UI 组件库的替换,之后再将老项目的业务代码逐部迁移





除了解决这些痛点,甚至还有新收获:




  • 帮助公司/企业打造视觉风格统一的产品,提高产品辨识度



    大公司可能有不止一个项目团队,不同项目团队的技术偏好可能无法完全统一,导致开发的后台长得也千变万化。但即使在这种情况下,使用 Fantastic-admin 依旧可以保持整体视觉风格上的统一。





  • 近乎于 0 的上手成本



    因为后台框架始终都只有一套,开发者不会因为切换 UI 组件库后,要重新了解后台框架的使用





  • 维护成本更低,产品生命周期更长



    这一点是对我自己说的,不管未来会出现多少个新的 UI 组件库,我都不需要去新增一个版本进行单独维护;或者 Element Plus 如果有一天停止维护了,我的产品也不会因此进入了死亡倒计时





总结


文章写到这里,差不多就结束了,虽然阅读一遍可能只花了不到10分钟,但为了做成这件事,我大概从今年 6 月份就开始构思了,也是花了蛮多的精力,所以很感谢你的耐心。


当一款产品做到第 4 个年头,周围大部分同类产品都进入到半停更的状态,这一年里我经常思考如何延长产品的生命周期,如何让更多人来使用,而这篇文章就是对我自己今年的一个总结,也是一份答卷,希望大家能喜欢。


另外,Fantastic-admin V4.0 已经正式发布,感兴趣的朋友可以来看看,或许你的下一个项目,就可以用上了。


作者:Hooray
来源:juejin.cn/post/7295624857432850468
收起阅读 »

终结屏幕适配这个话题

物理像素、逻辑像素、百分比适配 日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px),逻辑像素(dp, point)。 那物理像素和逻辑像素的区别是? 这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。 此时...
继续阅读 »

物理像素、逻辑像素、百分比适配


日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px)逻辑像素(dp, point)


那物理像素和逻辑像素的区别是?


这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。



此时想象这个图片放置在 `10*12px` 的屏幕中会是怎样呢。对比如下,会发现该图片放置在分辨率更高的屏幕中会变得非常狭小。

image.png
继续我们的例子,如果该屏幕想要保证图片能跟前面的低分辨率的设备显示效果一致的话,则图片的宽高应增加1倍的大小。即设备需要2倍的像素比例dpr(device percent ratio)。这样图片3*4逻辑像素的尺寸的图片在高分辨率设备中可以映射成6*8物理像素,而在低分辨率的设备(像素比例时1的设备),则3*4逻辑像素的图片映射为3*4物理像素的图片。


这是逻辑像素的大致机制。逻辑像素会根据目标设备的分辨率和尺寸计算出设备的缩放比例。逻辑像素出现是为了让不同分辨率的设备中显示相同的内容能取得大致相同的效果,当然逻辑像素并不是这样简单的百分比换算。


在Android中这个逻辑像素是dp,而ios中则是pt。在android中dp的换算公式中具体换算公式想了解的可以点击下面链接了解。
betterprogramming.pub/cracking-an…


在Android开发中将不同分辨率设备的中的物理像素比率进行如下分类。所以假设设备是230dpi的话也以hdpi1.5倍进行换算。所以这跟百分比的换算是不太一样的。以“微信”应用举例。


底部的Tab(微信、通讯录、发现,我),假设设计图中屏幕的宽度是375dp,根据tab均分,单个tab为93.75。你如果通过水平布局指定宽度为93.75逻辑像素的话则会发现出来的效果在某些手机上并不是均分的。


如下图类似微信界面运行在Iphone 14 pro。此时应该用百分比进行适配,即在不同的分辨率中基于设计图的尺寸进行等比例换算。如:设计图的分辨率为375*812,而显示设备的分辨率为1080*1920,则设计图上1像素相当于目标设备1 ✖️ “显示设备基于设计图的比例(1080/375=2.88)”像素,即 1✖️2.88=2.88像素。这就是百分比适配。对比下图可以发现逻辑像素适配的“我”是偏左的。


image.png


image.png


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


但是百分比并不是万能的。如下图逻辑像素适配和百分比像素适配的对比。在列表中,百分比布局则会出现一个问题。你会发现在大尺寸高分辨率的设备中,列表中的每一项都特别大。则如果用逻辑像素(dp、pt)则是这样。使用逻辑像素能充分发挥大屏的优势,屏幕越大显示的内容更多。


image.png


什么时候应该用逻辑像素,百分比像素。


具体什么时候应该用逻辑像素和百分比像素适配,取决于设计图UI。根据不同设计意图决定何种方案。大部分情况下使用逻辑像素不会出现什么问题,列表item必定使用逻辑像素。但是什么时候应该用百分比像素呢?


举个例子:



ps: 例子中我会以百分比像素表示将设计图像素根据不同分辨率设备等比例换算的像素。即1百分比像素= 1✖️ [(设计图分辨率)/ (目标设备分辨率)]。



下面是一个“购买成功”的UI图。中间有个票根信息。票根信息有个票根背景图片。


标注图中的屏幕分辨率为 393*852


image.png


这里票根信息UI应该用逻辑像素还是百分比像素适配呢?


通过标注图能明显看出票根信息在宽度上固定需要占用一定比例。所以这里宽度应该为 353百分比像素 。为了宽高比例正确,故高度也应为 346百分比像素 。注意这里高度的 346百分比像素 也应该是基于屏幕宽度 393 的百分比像素。即 目标设备屏幕宽度 * 346 / 393


因为整个票根的宽高都为百分比适配,则里面子部件的摆放、间距都应按照百分比的方式进行适配。不然则会出现子部件没法像标注图那样正确对齐的情况。


总结


物理像素(px)是屏幕上的实际物理点,表示屏幕上显示内容的最小单位。逻辑像素(dp、pt)是开发中使用的抽象单位,与物理像素的关系由设备的像素密度决定。


逻辑像素是开发中使用的抽象单位,它们与物理像素之间有一个映射关系。在不同的设备上,逻辑像素的布局和大小是相对统一的。使用逻辑像素可以让开发者在不同分辨率的设备上保持一致的布局和显示效果。


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


一般情况下,使用逻辑像素可以保持在不同设备上显示内容的一致性和最佳效果,特别是在涉及列表和大屏幕显示的情况下,需要根据设计图,决定使用何种方案。可以通过先分析使用逻辑像素思考是否合理,再考虑百分比适配的情况。在一些特定的设计需求下,如背景图片的铺满屏幕、比例布局等,可以考虑使用百分比适配来实现更精确的布局和显示效果。


作者:淹没
来源:juejin.cn/post/7294853623849812002
收起阅读 »

如何用Compose TV写电视桌面

写在前面 Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等. 下图为最终效果成品。 Demo源码地址 整体UI框架搭建 标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColu...
继续阅读 »

写在前面


Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等.


下图为最终效果成品。



Demo源码地址


整体UI框架搭建


标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColumn)



标题-TabRow



val tabs = listof("我的", "影视", "应用")

TabRow(
selectedTabIndex = selectedTabIndex,
indicator = { tabPositions, isActivated ->
// 移动的白色色块
TopBarMoveIndicator(...
}
) {
tabs.forEachIndexed { index, title ->
Tab(
// colors设置了 默认,上焦,选中的颜色
colors = TabDefaults.pillIndicatorTabColors(
contentColor = Color.White,
focusedContentColor = Color.Black,
selectedContentColor = Color.White,
)
...
) {
Text(...)
}
}
}

移动的白色色块,这里只是我写的Demo,都是可以自定义的.


fun TopBarMoveIndicator(
currentTabPosition: DpRect,
isFocused: Boolean
)
{
val width by animateDpAsState(targetValue = currentTabPosition.width, label = "width")
val height = if (isFocused) currentTabPosition.height else 2.dp
val leftOffset by animateDpAsState(targetValue = currentTabPosition.left, label = "leftOffset")
// 有焦点的时候,是矩形,无焦点的时候,是下划线.
val moveShape = if (isFocused) ShapeDefaults.ExtraLarge else ShapeDefaults.ExtraSmall

Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(leftOffset, currentTabPosition.top)
.width(width)
.height(height)
.background(color = Color.White, shape = moveShape)
.zIndex(-1f)
)
}

NatHost(内容切换) + 内容(TvLazyColumn)


内容切换


NatHost 功能类似 ViewPager,对 "我的","影视","应用" 几个 页面内容进行切换.


NavHost(
...
builder = {
composable(...) { // 我的
// 我的野蛮
}
composable(...) {// 影视
// 影视页面
}
composable(...) { // 应用
// 应用页面
}
}
)

内容布局


TvLazyColumn 与 LazyColumn 功能是差不多的,纵向布局,就不过多赘述,具体看谷歌的开发文档,网上相关视频教程 或 看DEMO源码.


TvLazyColumn(
...
) {
item {
ImmersiveList(...) // 沉浸式列表
}
item {
TvLazyRow(...) // 热门推荐
}
item {
TvLazyRow(...)
}
item {
TvLazyRow(...) // 豆瓣高分
TvLazyRow(...)
}
item {
TvLazyRow(...) // 预热抢先看
}
... ...
}

TvLazyColumn的相关参数,记住这个参数 pivotOffsets,它是设置滚动的位置的,比如设置滚动一直在中间位置.


fun TvLazyColumn(
modifier: Modifier = Modifier,
state: TvLazyListState = rememberTvLazyListState()
,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
userScrollEnabled: Boolean = true,
pivotOffsets: PivotOffsets = PivotOffsets(),
content: TvLazyListScope.() -> Unit
)

TvLazyRow + Item


TvLazyColumn 每行又包含了 TvLazyRow 横向布局 (如果是固定的几个,可以用 Row。


自定义的布局可以用 Surface 包含的,几个关键属性, Scale(放大),Border(边框),Glow(阴影)。



TvLazyRow(...) {
items(...) { ...
Surface(
onClick = {//点击事件}
scale = ClickableSurfaceDefaults.scale(...),
border = ClickableSurfaceDefaults.border(...),
glow = ClickableSurfaceDefaults.glow(...)
) {
// 你自定义的卡片内容,比如 图片(AsyncImage) + 文本(Text)
}
}
}

我Demo里面用的是 谷歌提供的一个包含 图片+文本的控件 StandardCardLayout


ImmersiveList 沉浸式列表

有点类似 爱奇艺,腾讯,哔哩哔哩等电视应用这种列表.


ImmersiveList(
modifier = Modifier.onGloballyPositioned { currentYCoord = it.positionInWindow().y },
background = {
// 背景图片内容
}
) {
// 布局内容
// 大标题 + 详情
// TvLazyRow
}

TV其它控件推荐


Carousel 轮播界面



TvLazyVerticalGrid/TvLazyHorizontalGrid


ModalNavigationDrawer抽屉式导航栏


ListItem


分辨率适配


TV开发涉及分辨率适配问题,Compose 也能很简单的处理此问题,无论你在1920x1080,还是1280x720等分辨率下,无缝切换,毫无压力.


val displayMetrics = LocalContext.current.resources.displayMetrics
val fontScale = LocalDensity.current.fontScale
val density = displayMetrics.density
val widthPixels = displayMetrics.widthPixels
val widthDp = widthPixels / density
val display = "density: $density\nwidthPixels: $widthPixels\nwidthDp: $widthDp"
KLog.d("display:$display")
CompositionLocalProvider(
LocalDensity provides Density(
density = widthPixels / 1920f,
fontScale = fontScale
)
) {
// 我们写的Compose主界面布局
}

参考资料


What's new with TV and intro to Compose


Android TV 上使用 Jetpack Compose


Compose TV官方设计文档


JetStreaamCompose TV demo


Compose TV demo


写在后面


近几年Android推出了很多东西,我的心尖尖是 MVI,flow(完爆Rxjava),Compose>>>


TV开发的发展,一开始是 RecycleView,要去解决焦点,优化等问题,后来是Leanback,到现在的Compose TV(开发速度提升了很多很多).


我也真的很喜欢Compose的写法,简单明了,强烈推荐Compose TV开发电视,我相信谷歌,能将Compose性能优化的越来越好.


最后一篇TV开发的文章了,以后搞车载相关去了.


作者:冰雪情缘long
来源:juejin.cn/post/7294907512444010559
收起阅读 »

Android使用Hilt依赖注入,让人看不懂你代码

前言 之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt...
继续阅读 »

前言


之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt不了解或是想了解得更多,那么接下来的内容将助力你玩转Hilt。


通过本篇文章,你将了解到:




  1. 什么是依赖注入?

  2. Hilt 的引入与基本使用

  3. Hilt 的进阶使用

  4. Hilt 原理简单分析

  5. Android到底该不该使用DI框架?



1. 什么是依赖注入?


什么是依赖?


以手机为例,要组装一台手机,我们需要哪些部件呢?

从宏观上分类:软件+硬件。

由此我们可以说:手机依赖了软件和硬件。

而反映到代码的世界:


class FishPhone(){
val software = Software()
val hardware = Hardware()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}
//软件
class Software() {
fun handle(){}
}
//硬件
class Hardware() {
fun handle(){}
}

FishPhone 依赖了两个对象:分别是Software和Hardware。

Software和Hardware是FishPhone的依赖(项)。


什么是注入?


上面的Demo,FishPhone内部自主构造了依赖项的实例,考虑到依赖的变化挺大的,每次依赖项的改变都要改动到FishPhone,容易出错,也不是那么灵活,因此考虑从外部将依赖传进来,这种方式称之为:依赖注入(Dependency Injection 简称DI)

有几种方式:




  1. 构造函数传入

  2. SetXX函数传入

  3. 从其它对象间接获取



构造函数依赖注入:


class FishPhone(val software: Software, val hardware: Hardware){
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone的功能比较纯粹就是打电话功能,而依赖项都是外部传入提升了灵活性。


为什么需要依赖注入框架?


手机制造出来后交给客户使用。


class Customer() {
fun usePhone() {
val software = Software()
val hardware = Hardware()
FishPhone(software, hardware).call()
}
}

用户想使用手机打电话,还得自己创建软件和硬件,这个手机还能卖出去吗?

而不想创建软件和硬件那得让FishPhone自己负责去创建,那不是又回到上面的场景了吗?


你可能会说:FishPhone内部就依赖了两个对象而已,自己负责创建又怎么了?


解耦


再看看如下Demo:


interface ISoftware {
fun handle()
}

//硬件
interface IHardware {
fun handle()
}

//软件
class SoftwareImpl() : ISoftware {
override fun handle() {}
}

//硬件
class HardwareImpl : IHardware {
override fun handle() {}
}

class FishPhone() {
val software: ISoftware = SoftwareImpl()
val hardware: IHardware = HardwareImpl()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone 只关注软件和硬件的接口,至于具体怎么实现它不关心,这就达到了解耦的目的。
既然要解耦,那么SoftwareImpl()、HardwareImpl()就不能出现在FishPhone里。

应该改为如下形式:


class FishPhone(val software: ISoftware, val hardware: IHardware) {
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

消除模板代码


即使我们不考虑解耦,假若HardwareImpl里又依赖了cpu、gpu、disk等模块:


//硬件
class HardwareImpl : IHardware {
val cpu = CPU(Regisgter(), Cal(), Bus())
val gpu = GPU(Image(), Video())
val disk = Disk(Block(), Flash())
//...其它模块
override fun handle() {}
}

现在仅仅只是三个模块,若是依赖更多的模块或者模块的本身也需要依赖其它子模块,比如CPU需要依赖寄存器、运算单元等等,那么我们就需要写更多的模板代码,要是我们只需要声明一下想要使用的对象而不用管它的创建就好了。


class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {
override fun handle() {}
}

可以看出,下面的代码比上面的简洁多了。




  1. 从解耦和消除模板代码的角度看,我们迫切需要一个能够自动创建依赖对象并且将依赖注入到目标代码的框架,这就是依赖注入框架

  2. 依赖注入框架能够管理依赖对象的创建,依赖对象的注入,依赖对象的生命周期

  3. 使用者仅仅只需要表明自己需要什么类型的对象,剩下的无需关心,都由框架自动完成



先想想若是我们想要实现这样的框架需要怎么做呢?

相信很多小伙伴最朴素的想法就是:使用工厂模式,你传参告诉我想要什么对象我给你构造出来。

这个想法是半自动注入,因为我们还要调用工厂方法去获取,而全自动的注入通常来说是使用注解标注实现的。


2. Hilt 的引入与基本使用


Hilt的引入


从Dagger到Dagger2再到Hilt(Android专用),配置越来越简单也比较容易上手。

前面说了依赖注入框架的必要性,我们就想迫不及待的上手,但难度可想而知,还好大神们早就造好了轮子。

以AGP 7.0 以上为例,来看看Hilt框架是如何引入的。


一:project级别的build.gradle 引入如下代码:


plugins {
//指定插件地址和版本
id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}

二:module级别的build.gradle引入如下代码:


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//使用插件
id 'com.google.dagger.hilt.android'
//kapt生成代码
id 'kotlin-kapt'
}
//引入库
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'

实时更新最新版本以及AGP7.0以下的引用请参考:Hilt最新版本配置


Hilt的简单使用


前置步骤整好了接下来看看如何使用。


一:表明该App可以使用Hilt来进行依赖注入,添加如下代码:


@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}

@HiltAndroidApp 添加到App的入口,即表示依赖注入的环境已经搭建好。


二:注入一个对象到MyApp里:

有个类定义如下:


class Software {
val name = "fish"
}

我们不想显示的构造它,想借助Hilt注入它,那得先告诉Hilt这个类你帮我注入一下,改为如下代码:


class Software @Inject constructor() {
val name = "fish"
}

在构造函数前添加了@Inject注解,表示该类可以被注入。

而在MyApp里使用Software对象:


@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: Software

override fun onCreate() {
super.onCreate()
println("inject result:${software.name}")
}
}

对引用的对象使用@Inject注解,表示期望Hilt帮我将这个对象new出来。

最后查看打印输出正确,说明Software对象被创建了。


这是最简单的Hilt应用,可以看出:




  1. 我们并没有显式地创建Software对象,而Hilt在适当的时候就帮我们创建好了

  2. @HiltAndroidApp 只用于修饰Application



如何注入接口?


一:错误示范
上面提到过,使用DI的好处之一就是解耦,而我们上面注入的是类,现在我们将Software抽象为接口,很容易就会想到如下写法:


interface ISoftware {
fun printName()
}

class SoftwareImpl @Inject constructor(): ISoftware{
override fun printName() {
println("name is fish")
}
}

@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: ISoftware

override fun onCreate() {
super.onCreate()
println("inject result:${software.printName()}")
}
}

不幸的是上述代码编译失败,Hilt提示说不能对接口使用注解,因为我们并没有告诉Hilt是谁实现了ISoftware,而接口本身不能直接实例化,因此我们需要为它指定具体的实现类。


二:正确示范

再定义一个类如下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}



  1. @Module 表示该类是一个Hilt的Module,固定写法

  2. @InstallIn 表示模块在哪个组件生命周期内生效,SingletonComponent::class指的是全局

  3. 一个抽象类,类名随意

  4. 抽象方法,方法名随意,返回值是需要被注入的对象类型(接口),而参数是该接口的实现类,使用@Binds注解标记,



如此一来我们就告诉了Hilt,SoftwareImpl是ISoftware的实现类,于是Hilt注入ISoftware对象的时候就知道使用SoftwareImpl进行实例化。
其它不变运行一下:
image.png


可以看出,实际注入的是SoftwareImpl。



@Binds 适用在我们能够修改类的构造函数的场景



如何注入第三方类


上面的SoftwareImpl是我们可以修改的,因为使用了@Inject修饰其构造函数,所以可以在其它地方注入它。

在一些时候我们不想使用@Inject修饰或者说这个类我们不能修改,那该如何注入它们呢?


一:定义Provides模块


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():Hardware {
return Hardware()
}
}



  1. @Module和@InstallIn 注解是必须的

  2. 定义object类

  3. 定义函数,方法名随意,返回类型为我们需要注入的类型

  4. 函数体里通过构造或是其它方式创建具体实例

  5. 使用@Provides注解函数



二:依赖使用

而Hardware定义如下:


class Hardware {
fun printName() {
println("I'm fish")
}
}

在MyApp里引用Hardware:

在这里插入图片描述


虽然Hardware构造函数没有使用@Inject注解,但是我们依然能够使用依赖注入。


当然我们也可以注入接口:


interface IHardware {
fun printName()
}

class HardwareImpl : IHardware {
override fun printName() {
println("name is fish")
}
}

想要注入IHardware接口,需要定义provides模块:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}


@Provides适用于无法修改类的构造函数的场景,多用于注入第三方的对象



3. Hilt 的进阶使用


限定符


上述 ISoftware的实现类只有一个,假设现在有两个实现类呢?

比如说这些软件可以是美国提供,也可以是中国提供的,依据上面的经验我们很容易写出如下代码:


class SoftwareChina @Inject constructor() : ISoftware {
override fun printName() {
println("from china")
}
}

class SoftwareUS @Inject constructor() : ISoftware {
override fun printName() {
println("from US")
}
}

@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

//依赖注入:
@Inject
lateinit var software: ISoftware

兴高采烈的进行编译,然而却报错:
image.png


也就是说Hilt想要注入ISoftware,但不知道选择哪个实现类,SoftwareChina还是SoftwareUS?没人告诉它,所以它迷茫了,索性都绑定了。


这个时候我们需要借助注解:@Qualifier 限定符注解来对实现类进行限制。

改造一下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
@China
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
@US
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China

定义新的注解类,使用@Qualifier修饰。

而后在Module里,分别使用注解类修饰返回的函数,如bindSoftwareCh函数指定返回SoftwareChina来实现ISoftware接口。


最后在引用依赖注入的地方分别使用@China @US修饰。


    @Inject
@US
lateinit var software1: ISoftware

@Inject
@China
lateinit var software2: ISoftware

此时,虽然software1、software2都是ISoftware类型,但是由于我们指定了限定符@US、@China,因此最后真正的实现类分别是SoftwareChina、SoftwareUS。



@Qualifier 主要用在接口有多个实现类(抽象类有多个子类)的注入场景



预定义限定符


上面提及的限定符我们还可以扩展其使用方式。

你可能发现了,上述提及的可注入的类构造函数都是无参的,很多时候我们的构造函数是需要有参数的,比如:


class Software @Inject constructor(val context: Context) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
//注入
@Inject
lateinit var software: Software

这个时候编译会报错:

image.png
意思是Software依赖的Context没有进行注入,因此我们需要给它注入一个Context。


由上面的分析可知,Context类不是我们可以修改的,只能通过@Provides方式提供其注入实例,并且Context有很多子类,我们需要使用@Qualifier指定具体实现类,因此很容易我们就想到如下对策。

先定义Module:


@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {
@Provides
@GlobalContext
fun provideContext(): Context? {
return MyApp.myapp
}
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext

再注入Context:


class Software @Inject constructor(@GlobalContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

可以看出,借助@Provides和@Qualifier,可以实现全局的Context。

当然了,实际上我们无需如此麻烦,因为这部分工作Hilt已经预先帮我们弄了。

与我们提供的限定符注解GlobalContext类似,Hilt预先提供了:


@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}

因此我们只需要在需要的地方引用它即可:


class Software @Inject constructor(@ApplicationContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

如此一来我们无需重新定义Module。




  1. 除了提供Application级别的上下文:@ApplicationContext,Hilt还提供了Activity级别的上下文:@ActivityContext,因为是Hilt内置的限定符,因此称为预定义限定符。

  2. 如果想自己提供限定符,可以参照GlobalContext的做法。



组件作用域和生命周期


Hilt支持的注入点(类)


以上的demo都是在MyApp里进行依赖,MyApp里使用了注解:@HiltAndroidApp 修饰,表示当前App支持Hilt依赖,Application就是它支持的一个注入点,现在想要在Activity里使用Hilt呢?


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {

除了Application和Activity,Hilt内置支持的注入点如下:
image.png


除了Application和ViewModel,其它注入点都是通过使用@AndroidEntryPoint修饰。



注入点其实就是依赖注入开始的点,比如Activity里需要注入A依赖,A里又需要注入B依赖,B里又需要注入C依赖,从Activity开始我们就能构建所有的依赖



Hilt组件的生命周期


什么是组件?在Dagger时代我们需要自己写组件,而在Hilt里组件都是自动生成的,无需我们干预。
依赖注入的本质实际上就是在某个地方悄咪咪地创建对象,这个地方的就是组件,Hilt专为Android打造,因此势必适配了Android的特性,比如生命周期这个Android里的重中之重。

因此Hilt的组件有两个主要功能:




  1. 创建、注入依赖的对象

  2. 管理对象的生命周期



Hilt组件如下:
image.png


可以看出,这些组件的创建和销毁深度绑定了Android常见的生命周期。

你可能会说:上面貌似没用到组件相关的东西,看了这么久也没看懂啊。

继续看个例子:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@InstallIn(SingletonComponent::class) 表示把模块安装到SingletonComponent组件里, SingletonComponent组件顾名思义是全局的,对应的是Application级别。因此安装的这个模块可在整个App里使用。


问题来了:SingletonComponent是不是表示@Provides修饰的函数返回的实例是同一个?

答案是否定的。


这就涉及到组件的作用域。


组件的作用域


想要上一小结的代码提供全局唯一实例,则可用组件作用域注解修饰函数:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@Singleton
fun provideHardware():IHardware {
return HardwareImpl()
}
}

当我们在任何地方注入IHardware时,获取到的都是同一个实例。

除了@Singleton表示组件的作用域,还有其它对应组件的作用域:

image.png


简单解释作用域:

@Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例

@ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例

@ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例

@ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致




  1. Hilt默认不绑定任何作用域,由此带来的结果是每一次注入都是全新的对象

  2. 组件的作用域要么不指定,要指定那必须和组件的生命周期一致



以下几种写法都不符合第二种限制:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {
@Provides
@Singleton//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

除了修饰Module,作用域还可以用于修饰构造函数:


@ActivityScoped
class Hardware @Inject constructor(){
fun printName() {
println("I'm fish")
}
}

@ActivityScoped表示不管注入几个Hardware,在同一个Activity里注入的实例都是一致的。


构造函数里无法注入的字段


一个类的构造函数如果被@Inject注入,那么构造函数的其它参数都需要支持注入。


class Hardware @Inject constructor(val context: Context) {
fun printName() {
println("I'm fish")
}
}

以上代码是无法编译通过的,因为Context不支持注入,而通过上面的分析可知,我们可以使用限定符:


class Hardware @Inject constructor(@ApplicationContext val context: Context) {
fun printName() {
println("I'm fish")
}
}

这就可以成功注入了。


再看看此种场景:


class Hardware @Inject constructor(
@ApplicationContext val context: Context,
val version: String,
) {
fun printName() {
println("I'm fish")
}
}

很显然String不支持注入,当然我们可以向@ApplicationContext 一样也给String提供一个@Provides和@Qualifier注解,但可想而知很麻烦,关键是String是动态变化的,我们确实需要Hardware构造的时候传入合适的String。


由此引入新的写法:辅助注入


class Hardware @AssistedInject constructor(
@ApplicationContext val context: Context,
@Assisted
val version: String,
) {

//辅助工厂类
@AssistedFactory
interface Factory{
//不支持注入的参数都可以放这,返回值为待注入的类型
fun create(version: String):Hardware
}

fun printName() {
println("I'm fish")
}
}

在引用注入的地方不能直接使用Hardware,而是需要通过辅助工厂进行创建:


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
@Inject
lateinit var hardwareFactory : Hardware.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)

val hardware = hardwareFactory.create("3.3.2")
println("${hardware.printName()}")
}
}

如此一来,通过辅助注入,我们还是可以使用Hilt,值得一提的是辅助注入不是Hilt独有,而是从Dagger继承来的功能。


自定义注入点


Hilt仅仅内置了常用的注入点:Application、Activity、Fragment、ViewModel等。

思考一种场景:小明同学写的模块都是需要注入:


class Hardware @Inject constructor(
val gpu: GPU,
val cpu: CPU,
) {
fun printName() {
println("I'm fish")
}
}

class GPU @Inject constructor(val videoStorage: VideoStorage){}

//显存
class VideoStorage @Inject constructor() {}

class CPU @Inject constructor(val register: Register) {}

//寄存器
class Register @Inject() constructor() {}

此时小刚需要引用Hardware,他有两种选择:




  1. 使用注入方式很容易就引用了Hardware,可惜的是他没有注入点,仅仅只是工具类。

  2. 不选注入方式,则需要构造Hardware实例,而Hardware依赖GPU和CPU,它们又分别依赖VideoStorage和Register,想要成功构造Hardware实例需要将其它的依赖实例都手动构造出来,可想而知很麻烦。



这个时候适合小刚的方案是:



自定义注入点



方案实施步骤:

一:定义入口点


@InstallIn(SingletonComponent::class)
interface HardwarePoint {
//该注入点负责返回Hardware实例
fun getHardware(): Hardware
}

二:通过入口点获取实例


class XiaoGangPhone {
fun getHardware(context: Context):Hardware {
val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)
return entryPoint.getHardware()
}
}

三:使用Hardware


        val hardware = XiaoGangPhone().getHardware(this)
println("${hardware.printName()}")

注入object类


定义了object类,但在注入的时候也需要,可以做如下处理:


object MySystem {
fun getSelf():MySystem {
return this
}
fun printName() {
println("I'm fish")
}
}

@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {
@Provides
@Singleton
fun provideSystem():MySystem {
return MySystem.getSelf()
}
}
//使用注入
class Middleware @Inject constructor(
val mySystem:MySystem
) {
}

4. Hilt 原理简单分析


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}

Hilt通过apt在编译时期生成代码:


public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {

private boolean injected = false;

Hilt_SecondActivity() {
super();
//初始化注入监听
_initHiltInternal();
}

Hilt_SecondActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}

private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
//真正注入
inject();
}
});
}

protected void inject() {
if (!injected) {
injected = true;
//通过manager获取组件,再通过组件注入
((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));
}
}
}

在编译期,SecondActivity的父类由AppCompatActivity变为Hilt_SecondActivity,因此当SecondActivity构造时就会调用父类的构造器监听create()的回调,回调调用时进行注入。



由此可见,Activity.onCreate()执行后,Hilt依赖注入的字段才会有值



真正注入的过程涉及到不少的类,都是自动生成的类,有兴趣可以对着源码查找流程,此处就不展开说了。


5. Android到底该不该使用DI框架?


有人说DI比较复杂,还不如我直接构造呢?

又有人说那是你项目不复杂,用不到,在后端流行的Spring全家桶,依赖注入大行其道,Android复杂的项目也需要DI来解耦。


从个人的实践经验看,Android MVVM/MVI 模式还是比较适合引入Hilt的。
image.png


摘抄官网的:现代Android 应用架构

通常来说我们这么设计UI层到数据层的架构:


class MyViewModel @Inject constructor(
val repository: LoginRepository
) :ViewModel() {}

class LoginRepository @Inject constructor(
val rds : RemoteDataSource,
val lds : LocalDataSource
) {}

//远程来源
class RemoteDataSource @Inject constructor(
val myRetrofit: MyRetrofit
) {}

class MyRetrofit @Inject constructor(
) {}

//本地来源
class LocalDataSource @Inject constructor(
val myDataStore: MyDataStore
) {}

class MyDataStore @Inject constructor() {}

可以看出,层次比较深,使用了Hilt简洁了许多。


本文基于 Hilt 2.48.1

参考文档:

dagger.dev/hilt/gradle…

developer.android.com/topic/archi…

repo.maven.apache.org/maven2/com/…


作者:小鱼人爱编程
来源:juejin.cn/post/7294965012749320218
收起阅读 »

记一次使用babel做代码转换的经历

web
前言 前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。 这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访...
继续阅读 »

前言


前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。


这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访问不通,还得拿到本地来。


得,谁让咱们天生就是找事的好手呢,那整吧。


方案对比


既然来活了,那咱们首先得先确定下这个事怎么做?有以下几个方案:


方案一: 发挥中华民族的优良传统,勤劳,即手动将全部的静态资源引用处替换为本地引用,想想手就疼


方案二: 将偷懒运用到极致,将静态资源全部放到public/assets目录下(Vite项目中public目录下的文件会被直接复制到打包目录下),同时修改资源引用的统一前缀为 /assets,即可引用到该静态资源。目测几分钟就能完成


方案三: 写个脚本,自动完成 1 操作,瞬间手就不疼了,但是脑壳开始疼了


对比下这三个方案的优缺点,选出最优解



方案一


优点:简单


缺点:手疼且低效


方案二


优点:省时、省力


缺点:需要考虑打包后的引用路径,同时因为文件都是直接复制到包中的,并没有经过hash处理,浏览器会缓存该文件,后续如果文件修改,不能第一时间反应再客户端。


方案三


优点:高效、一劳永逸、文件会经过Vite处理,生成带有hash值的新文件,没有缓存问题


缺点:这个脚本有点难写,涉及代码转换和项目文件扫描等知识,脑壳疼



最终,本着一劳永逸的想法,我选择了方案三。


过程


整体思路:



  1. 将全部静态资源引用汇总到统一文件中,方便管理及代码分析

  2. 使用代码转换工具将上面的文件内容转换为使用 import 导入的方式


静态资源汇总


所有的静态资源引用散布在项目的各个文件中,这不利于代码分析,也不利于代码转化,所以,第一步就是将散布在项目各个文件中的静态资源引用汇总到一个文件中,方便管理、代码分析、代码转化。


这一步是纯体力活,一次劳动,收益无穷。


最终静态资源汇总文件应该是这样的:


import { ASSETS_PREFIX } from './constants';

const contactUs = `${ASSETS_PREFIX}/login/contact_us.png`;
const userAvatar = `${ASSETS_PREFIX}/login/default_avatar.png`;
const loginBg = `${ASSETS_PREFIX}/login/login_bg.jpg`;

export {
contactUs,
userAvatar,
loginBg,
}


  1. 一个静态资源对应一个变量,一个变量对应一个静态资源路径

  2. 静态资源路径必须使用模版字符串统一前缀,便于后续做替换

  3. 统一导出


代码转换


静态资源全部会送完毕后,接下来就是做代码分析及转换。


我们的目标其实就是将上面的代码转换到下面这种:


import contactUs from '@/assets/login/contact_us.png';
import userAvatar from '@/assets/login/default_avatar.png';
import loginBg from '@/assets/login/login_bg.jpg'

export {
contactUs,
userAvatar,
loginBg,
}

既然涉及代码转换,很自然的就能想到使用babel做转换。


先来简单说下babel做代码转换的过程:



  1. 使用 @babel/parser 将代码解析为抽象语法树(AST: 表示当前代码结构的js对象)

  2. 找到标识为 const 的变量,拿出该变量,并将其后对应的变量内容拿出来,将模版字符串中的变量替换为@/assets,得到新静态资源本地路径(@/assets/login/contact_us.png)

  3. 组合 import 的 AST 对象,并使用该对象替换原来的 const 相关的AST

  4. 使用 @babel/generator 将新的AST转换为代码输出到对应文件中


代码如下:


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import fs from 'fs';

// 静态资源汇总文件
let imgInfoFilePath = 'src/scripts/assets.ts';
// 要替换为的静态资源路径前缀
let replaceToCode = '@/assets';

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历const声明节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

node.declarations.forEach(decl => {
// 存储变量名
const localName = decl.id.name;
// 组装import路径
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;
// 组装import结构
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
const result = generate.default(ast, {}, code);
// 备份原文件
fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);
// 代码输出
fs.writeFileSync(imgInfoFilePath, result.code);
} catch (error: any) {
logError(error);
}
}

这样,代码就转换完成了。


这样转换完后,ts文件中相关的静态资源引用就替换完成了,但是css文件中的静态资源引用还没有被转换。


因为css文件中的静态资源路径都是完整路径,不存在其中掺杂变量的情况,所以我们只需要找到所有的css文件,并将其中的路径前缀统一替换为@/assets 即可。


import { globSync } from 'glob';
import fs from 'fs';

let replaceStr = 'https://xxxxx.xxxx.xxxxx';
let replaceToCode = '@/assets';

function replaceHttpsCode() {
try {
// 扫描文件
const files = globSync('./src/**/*.{scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 写入文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}


  1. 使用 glob 扫描当前目录下的scss、css文件。

  2. 读取文件内容,并使用replace方法替换掉静态资源路径

  3. 写入文件,完成转换


至此,代码全部转换完成。


封装成工具包


因为多个项目都会涉及静态资源转换的问题,所以我将此脚本封装为npm包,并提炼了 transform build 命令,只需执行该命令,即可完成资源转换,以下是源码分享:


cli.ts


import { Command } from 'commander';
import { version } from '../package.json';
import buildAction from './transform';
const program = new Command();

program
.command('build')
.description('transform assets and code')
.option(
'--replaceStr <path>',
'[string] 需要全局替换的字符串,默认值: https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage',
)
.option('--imgInfoFilePath <path>', '[string] 统一的静态资源文件路径 默认值: src/scripts/assets.ts')
.option('--replaceToCode <path>', '[string] 替换为的代码 默认值: @/assets')
.option('--assetsDir <path>', '[string] 静态资源文件目录 默认值: src/assets')
.action(options => {
buildAction(options);
});

program.version(version);

program.parse();

transfrom.ts


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import chalk from 'chalk';
import { globSync } from 'glob';
import fs from 'fs';

interface Options {
replaceStr?: string;
imgInfoFilePath?: string;
replaceToCode?: string;
assetsDir?: string;
}

let replaceStr = 'https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage';
let imgInfoFilePath = 'src/scripts/assets.ts';
let replaceToCode = '@/assets';
let assetsDir = './src/assets';

function checkAssetsDir() {
logInfo('检查 src/assets 目录是否存在');

if (!fs.existsSync(assetsDir)) {
logError('assets 目录不存在,请先联系相关人员下载对应项目的静态资源文件,并放置在 src/assets 目录下');
} else {
logSuccess('assets 目录存在');
}
}

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历VariableDeclarator节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

// @ts-ignore
node.declarations.forEach(decl => {
// @ts-ignore
const localName = decl.id.name;

// @ts-ignore
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;

// @ts-ignore
logInfo(`替换 ${replaceStr}${decl?.init?.quasis?.[1]?.value?.raw}${filePath}`);

// 构建导入规范
// @ts-ignore
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
// @ts-ignore
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
// @ts-ignore
const result = generate.default(ast, {}, code);

logInfo(`备份 ${imgInfoFilePath} 文件为 ${imgInfoFilePath}.bak`);

fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);

fs.writeFileSync(imgInfoFilePath, result.code);

logSuccess(`转换 ${imgInfoFilePath} 成功`);
} catch (error: any) {
logError(error);
}
}

function replaceHttpsCode() {
logInfo('开始转换 其余文件中引用https导入的静态资源');

try {
// 扫描文件
const files = globSync('./src/**/*.{vue,js,ts,scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

if (content.includes(replaceStr)) {
logInfo(`替换 ${file} 中的 ${replaceStr}${replaceToCode}`);
}

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 保存文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}

function logInfo(info: string) {
console.log(chalk.gray(`[INFO] - 🆕 ${info}`));
}

function logSuccess(info: string) {
console.log(chalk.green(`[SUCCESS] - ✅ ${info}`));
}

function logError(info: string) {
console.log(chalk.red(`[ERROR] - ❌ ${info}`));
}

export default function main(options: Options) {
replaceStr = options.replaceStr || replaceStr;
imgInfoFilePath = options.imgInfoFilePath || imgInfoFilePath;
replaceToCode = options.replaceToCode || replaceToCode;
assetsDir = options.assetsDir || assetsDir;

checkAssetsDir();
babelTransformCode();
replaceHttpsCode();
}

作者:程序员小杨v1
来源:juejin.cn/post/7295276751595798580
收起阅读 »

Flutter开发者,需要会原生吗?-- Android 篇

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。 由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flut...
继续阅读 »

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。

由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flutter开发者到底需不需要懂原生平台?



缘起


《Flutter开发者需要掌握原生Android吗?》

这个话题跟Flutter与RN对比Flutter会不会凉同属一类,都是前两年社群最喜欢争论的话题。激烈的讨论无非是观望者太多,加之Flutter不成熟,在使用过程中会遇到不少坑。


直到今年3.7.0、3.10.0相继发布,框架改进和社区的丰富,让更多人选择拥抱Flutter,关于此类型的话题才开始沉寂下来。很多招聘网站也直接出现了Flutter开发这个岗位,而且技能也不要求原生,甚至加分项前端的技能。似乎Flutter开发者在开发过程中很少用到原生的技能,然而事实绝非如此。


我专攻Flutter有3年了,期间Android、iOS、Windows应用做过不少,Web、Linux也都略有研究;这次我将直接从Android平台出发,用切身经历来论述下:Flutter开发者,真的需要懂Android。


Flutter只是个UI框架


打开一个Flutter的项目,我们可以看到整个应用其实是基于一个Activity运行的,属于单页应用。


package com.wxq.test

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}

Activity继承自FlutterActivity,FlutterActivityonCreate内会创建FlutterActivityAndFragmentDelegate


// io/flutter/embedding/android/FlutterActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
switchLaunchThemeForNormalTheme();

super.onCreate(savedInstanceState);
// 创建代理,ActivityAndFragment都支持哦
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this); // 这个方法创建引擎,并且将context吸附上去
delegate.onRestoreInstanceState(savedInstanceState);

lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);

configureWindowForTransparency();

// 设置Activity的View,createFlutterView内部也是调用代理的方法
setContentView(createFlutterView());
configureStatusBarForFullscreenFlutterExperience();
}

这个代理将会通过engineGr0up管理FlutterEngine,通过onAttach创建FlutterEngine,并且运行createAndRunEngine方法


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
void onAttach(@NonNull Context context) {
ensureAlive();

if (flutterEngine == null) {
setupFlutterEngine();
}

if (host.shouldAttachEngineToActivity()) {

Log.v(TAG, "Attaching FlutterEngine to the Activity that owns this delegate.");
flutterEngine.getActivityControlSurface().attachToActivity(this, host.getLifecycle());
}
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);

host.configureFlutterEngine(flutterEngine);
isAttached = true;
}

@VisibleForTesting
/* package */ void setupFlutterEngine() {
Log.v(TAG, "Setting up FlutterEngine.");

// 省略处理引擎缓存的代码
String cachedEngineGr0upId = host.getCachedEngineGr0upId();
if (cachedEngineGr0upId != null) {
FlutterEngineGr0up flutterEngineGr0up =
FlutterEngineGr0upCache.getInstance().get(cachedEngineGr0upId);
if (flutterEngineGr0up == null) {
throw new IllegalStateException(
"The requested cached FlutterEngineGr0up did not exist in the FlutterEngineGr0upCache: '"
+ cachedEngineGr0upId
+ "'");
}

// *** 重点 ***
flutterEngine =
flutterEngineGr0up.createAndRunEngine(
addEntrypointOptions(new FlutterEngineGr0up.Options(host.getContext())));
isFlutterEngineFromHost = false;
return;
}

// Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
// FlutterView.
Log.v(
TAG,
"No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
+ " this FlutterFragment.");

FlutterEngineGr0up group =
engineGr0up == null
? new FlutterEngineGr0up(host.getContext(), host.getFlutterShellArgs().toArray())
: engineGr0up;
flutterEngine =
group.createAndRunEngine(
addEntrypointOptions(
new FlutterEngineGr0up.Options(host.getContext())
.setAutomaticallyRegisterPlugins(false)
.setWaitForRestorationData(host.shouldRestoreAndSaveState())));
isFlutterEngineFromHost = false;
}

再调用onCreateView创建SurfaceView或者外接纹理TextureView,这个View就是Flutter的赖以绘制的画布。


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
@NonNull
View onCreateView(
LayoutInflater inflater,
@Nullable ViewGr0up container,
@Nullable Bundle savedInstanceState,
int flutterViewId,
boolean shouldDelayFirstAndroidViewDraw)
{
Log.v(TAG, "Creating FlutterView.");
ensureAlive();

if (host.getRenderMode() == RenderMode.surface) {
FlutterSurfaceView flutterSurfaceView =
new FlutterSurfaceView(
host.getContext(), host.getTransparencyMode() == TransparencyMode.transparent);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterSurfaceViewCreated(flutterSurfaceView);

// Create the FlutterView that owns the FlutterSurfaceView.
flutterView = new FlutterView(host.getContext(), flutterSurfaceView);
} else {
FlutterTextureView flutterTextureView = new FlutterTextureView(host.getContext());

flutterTextureView.setOpaque(host.getTransparencyMode() == TransparencyMode.opaque);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterTextureViewCreated(flutterTextureView);

// Create the FlutterView that owns the FlutterTextureView.
flutterView = new FlutterView(host.getContext(), flutterTextureView);
}

flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
// 忽略一些代码...
return flutterView;
}

由此可见,Flutter的引擎实际上是运行在Android提供的View上,这个View必然是设置在Android的组件上,可以是Activity、Framgent,也可以是WindowManager。

这就给我们带来了很大的可塑性,只要你能掌握这套原理,混合开发就随便玩了。


Android,是必须的能力


通过对Flutter运行机制的剖析,我们很明确它就是个单纯的UI框架,惊艳的跨端UI都离不开Android的能力,这也说明Flutter开发者不需要会原生注定走不远

下面几个例子,也可以充分论证这个观点。


一、Flutter插件从哪里来


上面讲述到的原理,Flutter项目脚手架已经帮我们做好,但这只是UI绘制层面的;实际上很多Flutter应用,业务能力都是由Pub.dev提供的,随着社区框架的增多,开发者大多时候是感知不到需要Android能力的。

然而业务的发展是迅速的,我们开始需要很多pub社区并不支持的能力,比如:getMetaDatagetMacAddressreboot/shutdownsendBroadcast等,这些能力都需要我们使用Android知识,以编写插件的形式,提供给Flutter调用。

Flutter Plugin在Dart层和Android层都实现了MethodChannel对象,同一个Engine下,只要传入一致的channelId字符串,就能建立双向的通道互相传输基本类型数据。


class FlutterNativeAbilityPlugin : FlutterPlugin, MethodCallHandler {
private var applicationContext: Context? = null

private lateinit var channel: MethodChannel

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
applicationContext = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_native_ability")
channel.setMethodCallHandler(this)
}

class MethodChannelFlutterNativeAbility extends FlutterNativeAbilityPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('flutter_native_ability');
}

发送端通过invokeMethod调用对应的methodName,传入arguments;接收端通过实现onMethodCall方法,接收发送端的invokeMethod操作,执行需要的操作后,通过Result对象返回结果。


@override
Future<String> getMacAddress() async {
final res = await methodChannel.invokeMethod<String>('getMacAddress');
return res ?? '';
}

@override
Future<void> reboot() async {
await methodChannel.invokeMethod<String>('reboot');
}

"getMacAddress" -> {
Log.i(TAG, "onMethodCall: getMacAddress")
val macAddress = CommonUtils().getDeviceMac(applicationContext)
result.success(macAddress)
}
"reboot" -> {
Log.i(TAG, "onMethodCall: reboot")
beginToReboot(applicationContext)
result.success(null)
}

ps:invokeMethod和onMethodCall双端都能实现,都能作为发送端和接收端。


二、Flutter依赖于Android机制,得以“横行霸道”


目前我们将Flutter应用于OS的开发,这需要我们不单是从某个独立应用去思考。很多应用、服务都需要从整个系统业务去设计,在以下这些需求中,我们深切感受到:Flutter跟Android配合后,能发挥更大的业务价值。



  • Android服务运行dart代码,广播接收器与Flutter通信


我们很多服务需要开机自启,这必须遵循Android的机制。通常做法是:接收开机广播,在广播接收器中启动Service,然后再去运行DartEngie,执行跨平台的代码;


class MyTestService : Service() {

private lateinit var engineGr0up: FlutterEngineGr0up

override fun onCreate() {
super.onCreate()
startForeground()

engineGr0up = FlutterEngineGr0up(this)
// initService是Flutter层的方法入口点
val dartEntrypoint = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"initService"
)
val flutterEngine = engineGr0up.createAndRunEngine(this, dartEntrypoint)
// Flutter调用Native方法的 MethodChannel 也初始化一下,调用安装接口需要
FlutterToNativeChannel(flutterEngine, this)
}
}

同时各应用之间需要通信,这时我们也会通过Broadcat广播机制,在Android的广播接收器中,通过MechodChannel发送给Flutter端。


总而言之,我们必须 遵循系统的组件规则,基于Flutter提供的通信方式,将Android的消息、事件等发回给Flutter, 带来的跨端效益是实实在在的!



  • 悬浮窗需求


悬浮窗口在视频/直播场景下用的最多,当你的应用需要开启悬浮窗的时候,Flutter将完全无法支持这个需求。

实际上我们只需要在Android中创建一个WindowManager,基于EngineGround创建一个DartEngine;然后创建flutterView,把DartEngine吸附到flutterView上,最后把flutterView Add to WindowManager即可。


private lateinit var flutterView: FlutterView
private var windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
private val inflater =
context.getSystemService(Service.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private val metrics = DisplayMetrics()

@SuppressLint("InflateParams")
private var rootView = inflater.inflate(R.layout.floating, null, false) as ViewGr0up

windowManager.defaultDisplay.getMetrics(metrics)
layoutParams.gravity = Gravity.START or Gravity.TOP

windowManager.addView(rootView, layoutParams)

flutterView = FlutterView(inflater.context, FlutterSurfaceView(inflater.context, true))
flutterView.attachToFlutterEngine(engine)

engine.lifecycleChannel.appIsResumed()

rootView.findViewById<FrameLayout>(R.id.floating_window)
.addView(
flutterView,
ViewGr0up.LayoutParams(
ViewGr0up.LayoutParams.MATCH_PARENT,
ViewGr0up.LayoutParams.MATCH_PARENT
)
)
windowManager.updateViewLayout(rootView, layoutParams)


  • 不再局限单页应用


最近我们在升级应用中,遇到一个比较尴尬的需求:在原有OTA功能下,新增一个U盘插入本地升级的功能,希望升级能力和UI都能复用,且互不影响各自流程。


如果是Android项目很简单,把升级的能力抽象,通过多个Activity管理自己的业务流程,互不干扰。但是Flutter项目属于单页应用,不可能同时展示两个路由页面各自处理,所以也必须 走Android的机制,让Flutter应用同时运行多个Activity。


我们在Android端监听了U盘的插入事件,在需要本地升级的时候直接弹出Activity。Activity是继承FlutterActivity的,通过<metadata>标签指定方法入口点。与MainActivity运行main区分开,然后通过重写getDartEntrypointArgs方法,把必要的参数传给Flutter入口函数,从而独立运行本地升级的业务,而且UI和能力都能复用。


class LocalUpgradeActivity : FlutterActivity() {
}

<activity
android:name=".LocalUpgradeActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustResize">

<meta-data
android:name="io.flutter.Entrypoint"
android:value="runLocalUpgradeApp" />
<!-- 这里指定Dart层的入口点-->
</activity>

override fun getDartEntrypointArgs(): MutableList<String?> {
val filePath: String? = intent?.getStringExtra("filePath")
val tag: String? = intent?.getStringExtra("tag")
return mutableListOf(filePath, tag)
}

至此,我们的Flutter应用不再是单页应用,而且所有逻辑和UI都将在Flutter层实现!


总结


我们遵循Android平台的机制,把逻辑和UI都尽可能的交给Flutter层,让其在跨平台上发挥更大的可能性,在落地过程确实切身体会到Android的知识是何等的重要!

当然我们的应用场景可能相对复杂,一般应用也许不会有这么多的应用组合;但无论Flutter如何完善,社区更加壮大,它都离不开底层平台的支持。

作为Flutter开发者,有精力的情况下,一定要多学各个平台的框架和能力,让Flutter、更让自己走的更远!


作者:Karl_wei
来源:juejin.cn/post/7295571705689423907
收起阅读 »

正式变实习?掌趣科技这波操作太恶心

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历? 1.Offer 展示 2.面试题真题 问项目: 讲讲你最好的一个项目怎么实现的 ? 问八股: JVM 的执行...
继续阅读 »

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历?


1.Offer 展示



2.面试题真题


问项目:


讲讲你最好的一个项目怎么实现的 ?


问八股:



  1. JVM 的执行流程?

  2. JVM 是怎么找到一个类的?

  3. JVM 的内存布局?

  4. Java 虚拟机是怎么进行垃圾回收的?

  5. Java 有可能产生内存泄漏吗,举例? 内存泄露会导致什么问题?

  6. GC 的过程, 内存是怎么分配的, 是一片一片的使用呢, 还是一大块的使用? 还是想 C/C++ 那样零散的内存呢?怎么解决内存碎片问题?

  7. Java 中你都用过什么容器?HashMap 和 HashTable 的区别?ConcurrentHashMap 怎么保证线程安全问题的?

  8. 你用过 Java 的多线程锁吗?ReentrantLock 和 Synchronized 的区别?你了解什么是悲观锁?什么是乐观锁吗?

  9. Java 是怎么实现多态的? Java 中能多继承吗? Java 中的注解怎么实现的?

  10. Java 虚拟机是怎么找到一个方法,一个类,怎么根据注解找到对应的方法

  11. Java 中反射的作用,反射怎么实现的?

  12. 反射可以实现注解吗?

  13. 你了解自动拆箱装箱吗? 拆箱装箱怎么实现的?

  14. 项目中有碰见过死锁的问题吗? 什么是死锁? 只锁一个对象会产生死锁吗?

  15. Netty 用过吗?了解过吗?

  16. MySQL 有几种索引? 按字段特性分,按物理存储分呢? 索引底层有几种实现呢? MySQL 的有哪几种存储引擎? InnoDB 和 MyISAM 有什么区别?有什么优缺点?

  17. 做一个查询操作的时候,使用哪种索引?

  18. 什么叫回表查询? 如果没有创建主键,怎么进行回表查询?

  19. 什么叫索引覆盖?

  20. 一个表是索引越多越好,还是越少越好?

  21. 你了解什么叫表锁?什么叫行锁吗?什么情况下使用行锁?什么情况下使用表锁?

  22. 如果一个表的写操作比较多,是加行锁比较好,还是加表锁比较好?

  23. 你能说一下事务的隔离级别吗?

  24. Redis 数据库怎么是实现持久化的 ?Redis 里边都有哪些数据类型? ZSet 怎么用?

  25. 什么是缓存雪崩、缓存击穿、缓存穿透,分别怎么解决?

  26. 做过 Java 的网络编程吗?给我说一下 Socket 套接字的创建流程?

  27. 讲一讲三次握手,四次挥手的流程?TCP 和 UDP 的区别?

  28. 使用 UDP 来传输数据,怎么来保证他的可靠性,就像你刚刚说的后发先至问题?



PS:面试问的还挺细的,但因为做足了准备,所以这些面试题基本都拿下了。


以上问题来自学员的整理,在此感谢。



3.套路来了


拿到 Offer 本来是件开心的事,然而当聊完入职事项之后,整个人就不好了,来看看具体的经过吧:


问题1:正式工作变实习?


秋招明明投递的是正式工作,然而拿到 Offer 之后,HR 却必须先让去实习 5 个月。


投递详情如下:



明确是秋招正式岗位,而非实习。


正常的逻辑应该是拿到 Offer 之后,等明年毕业之后先去公司报道,只有 3 个月试用期,试用期没问题就转为正式员工了。



问题2:实习薪资


实习也是按照实习的工资,而非正式工资的 80% 发放的。


问题3:可能不通过&不谈正式薪资


HR 说明年四月实习期过了之后,再谈正式薪资,言外之意,如果实习期没过,那就不要你了,也就不用谈薪资了。那么请问,投递正式岗位又有什么意义呢?


问题4:实习时间超长


通常实习时间也就是 2-3 个月,而掌趣要求 11 月中旬去实习,至少实习到明年 4 月份,这个实习的时间未免要太长了。


小结


作为国内老牌上市公司,竟然以找正式工作的幌子把入选人悄悄转为实习生,这件事对有经验的人来说,一眼就知道怎么回事,然而对于涉世未深,刚步入社会的年轻人来说,却是满满的套路,还未感受生活的美好,就经历了人心的险恶。


人在做天在看,希望某些公司不要有这样的骚操作,招实习就是招实习,招正式员工就是招正式员工,不要混为一谈,更不要暗箱操作,更不要欺负那些涉世未深的年轻人。


作者:Java中文社群
来源:juejin.cn/post/7292946512585162806
收起阅读 »

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:




  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }



  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }



  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }



  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }



  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }



  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }



  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:




  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。




  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »

HarmonyOS开发:基于http开源一个网络请求库

前言 网络封装的目的,在于简洁,使用起来更加的方便,也易于我们进行相关动作的设置,如果,我们不封装,那么每次请求,就会重复大量的代码逻辑,如下代码,是官方给出的案例: // 引入包名 import http from '@ohos.net.http'; //...
继续阅读 »

前言


网络封装的目的,在于简洁,使用起来更加的方便,也易于我们进行相关动作的设置,如果,我们不封装,那么每次请求,就会重复大量的代码逻辑,如下代码,是官方给出的案例:


// 引入包名
import http from '@ohos.net.http';

// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {
console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(
// 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
"EXAMPLE_URL",
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递内容
extraData: {
"data": "data to send",
},
expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
usingCache: true, // 可选,默认为true
priority: 1, // 可选,默认为1
connectTimeout: 60000, // 可选,默认为60000ms
readTimeout: 60000, // 可选,默认为60000ms
usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
}, (err, data) => {
if (!err) {
// data.result为HTTP响应内容,可根据业务需要进行解析
console.info('Result:' + JSON.stringify(data.result));
console.info('code:' + JSON.stringify(data.responseCode));
// data.header为HTTP响应头,可根据业务需要进行解析
console.info('header:' + JSON.stringify(data.header));
console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 当该请求使用完毕时,调用destroy方法主动销毁
httpRequest.destroy();
} else {
console.info('error:' + JSON.stringify(err));
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 当该请求使用完毕时,调用destroy方法主动销毁。
httpRequest.destroy();
}
}
);

以上的案例,每次请求书写这么多代码,在实际的开发中,是无法承受的,所以基于此,封装是很有必要的,把公共的部分进行抽取包装,固定不变的参数进行初始化设置,重写基本的请求方式,这是我们封装的基本宗旨。


我们先看一下封装之后的调用方式:


异步请求


Net.get("url").requestString((data) => {
//data 为 返回的json字符串
})

同步请求


const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
//data 为 返回的json字符串

装饰器请求


@GET("url")
private getData():Promise<string> {
return null
}

封装之后,不仅使用起来更加的便捷,而且还拓展了请求类型,满足不同需求的场景。


本篇的文章内容大致如下:


1、net库主要功能点介绍


2、net库快速依赖使用


3、net库全局初始化


4、异步请求介绍


5、同步请求介绍


6、装饰器请求介绍


7、上传下载介绍


8、Dialog加载使用


9、相关总结


一、net库主要功能点介绍


目前net库一期已经开发完毕,har包使用,大家可以看第二项,截止到发文前,所支持的功能如下:


■ 支持全局初始化


■ 支持统一的BaseUrl


■ 支持全局错误拦截


■ 支持全局头参拦截


■ 支持同步方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持异步方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持装饰器方式请求(get/post/delete/put/options/head/trace/connect)


■ 支持dialog加载


■ 支持返回Json字符串


■ 支持返回对象


■ 支持返回数组


■ 支持返回data一层数据


■ 支持上传文件


■ 支持下载文件


□ 数据缓存开发中……


二、net库快速依赖使用


私服和远程依赖,由于权限和审核问题,预计需要等到2024年第一季度面向所有开发者,所以,只能使用本地静态共享包和源码 两种使用方式,本地静态共享包类似Android中的aar依赖,直接复制到项目中即可,目前源码还在优化中,先暴露静态共享包这一使用方式。


本地静态共享包har包使用


首先,下载har包,点击下载


下载之后,把har包复制项目中,目录自己创建,如下,我创建了一个libs目录,复制进去



引入之后,进行同步项目,点击Sync Now即可,当然了你也可以,将鼠标放置在报错处会出现提示,在提示框中点击Run 'ohpm install'。


需要注意,@app/net,是用来区分目录的,可以自己定义,比如@aa/bb等,关于静态共享包的创建和使用,请查看如下我的介绍,这里就不过多介绍。


HarmonyOS开发:走进静态共享包的依赖与使用


查看是否引用成功


无论使用哪种方式进行依赖,最终都会在使用的模块中,生成一个oh_modules文件,并创建源代码文件,有则成功,无则失败,如下:



三、net库全局初始化


推荐在AbilityStage进行初始化,初始化一次即可,初始化参数可根据项目需要进行选择性使用。


Net.getInstance().init({
baseUrl: "https://www.vipandroid.cn", //设置全局baseurl
connectTimeout: 10000, //设置连接超时
readTimeout: 10000, //设置读取超时
netErrorInterceptor: new MyNetErrorInterceptor(), //设置全局错误拦截,需要自行创建,可在这里进行错误处理
netHeaderInterceptor: new MyNetHeaderInterceptor(), //设置全局头拦截器,需要自行创建
header: {}, //头参数
resultTag: []//接口返回数据参数,比如data,items等等
})

1、初始化属性介绍


初始化属性,根据自己需要选择性使用。


属性类型概述
baseUrlstring一般标记为统一的请求前缀,也就是域名
connectTimeoutnumber连接超时,默认10秒
readTimeoutnumber读取超时,默认10秒
netErrorInterceptorINetErrorInterceptor全局错误拦截器,需继承INetErrorInterceptor
netHeaderInterceptorINetHeaderInterceptor全局请求头拦截器,需继承INetHeaderInterceptor
headerObject全局统一的公共头参数
resultTagArray接口返回数据参数,比如data,items等等

2、设置请求头拦截


关于全局头参数传递,可以通过以上的header参数或者在请求头拦截里均可,如果没有同步等逻辑操作,只是固定的头参数,建议直接使用header参数。


名字自定义,实现INetHeaderInterceptor接口,可在netHeader方法里打印请求头或者追加请求头。


import { HttpHeaderOptions, NetHeaderInterceptor } from '@app/net'

class MyNetHeaderInterceptor implements NetHeaderInterceptor {
getHeader(options: HttpHeaderOptions): Promise<Object> {
//可以进行接口签名,传入头参数
return null
}
}

HttpHeaderOptions对象


返回了一些常用参数,可以用于接口签名等使用。


export class HttpHeaderOptions {
url?: string //请求地址
method?: http.RequestMethod //请求方式
header?: Object //头参数
params?: Object //请求参数
}

3、设置全局错误拦截器


名字自定义,实现INetErrorInterceptor接口,可在httpError方法里进行全局的错误处理,比如统一跳转,统一提示等。


import { NetError } from '@app/net/src/main/ets/error/NetError';
import { INetErrorInterceptor } from '@app/net/src/main/ets/interceptor/INetErrorInterceptor';

export class MyNetErrorInterceptor implements INetErrorInterceptor {
httpError(error: NetError) {
//这里进行拦截错误信息

}
}

NetError对象


可通过如下方法获取错误code和错误描述信息。


/*
* 返回code
* */

getCode():number{
return this.code
}

/*
* 返回message
* */

getMessage():string{
return this.message
}

四、异步请求介绍


1、请求说明


为了方便数据的针对性返回,目前异步请求提供了三种请求方法,在实际的 开发中,大家可以针对需要,选择性使用。


request方法


Net.get("url").request<TestModel>((data) => {
//data 就是返回的TestModel对象
})

此方法,针对性返回对应的data数据对象,如下json,则会直接返回需要的data对象,不会携带外层的code等其他参数,方便大家直接的拿到数据。


{
"code": 0,
"message": "数据返回成功",
"data": {}
}

如果你的data是一个数组,如下json:


{
"code": 0,
"message": "数据返回成功",
"data": []
}

数组获取


Net.get("url").request<TestModel[]>((data) => {
//data 就是返回的TestModel[]数组
})

//或者如下

Net.get("url").request<Array<TestModel>>((data) => {
//data 就是返回的TestModel数组
})

可能大家有疑问,如果接口返回的json字段不是data怎么办?如下:


举例一


{
"code": 0,
"message": "数据返回成功",
"items": {}
}

举例二


{
"code": 0,
"message": "数据返回成功",
"models": {}
}

虽然网络库中默认取的是json中的data字段,如果您的数据返回类型字段有多种,如上json,可以通过全局初始化resultTag进行传递或者局部setResultTag传递即可。


全局设置接口返回数据参数【推荐】


全局设置,具体设置请查看上边的全局初始化一项,只设置一次即可,不管你有多少种返回参数,都可以统一设置。


 Net.getInstance().init({
resultTag: ["data", "items", "models"]//接口返回数据参数,比如data,items等等
})

局部设置接口返回数据参数


通过setResultTag方法设置即可。


Net.get("")
.setResultTag(["items"])
.request<TestModel>((data) => {

})

requestString方法


requestString就比较简单,就是普通的返回请求回来的json字符串。


Net.get("url").requestString((data) => {
//data 为 返回的json字符串
})

requestObject方法


requestObject方法也是获取对象,和request不同的是,它不用设置返回参数,因为它是返回的整个json对应的对象, 也就是包含了code,message等字段。


Net.get("url").requestObject<TestModel>((data) => {
//data 为 返回的TestModel对象
})

为了更好的复用共有字段,你可以抽取一个基类,如下:


export class ApiResult<T> {
code: number
message: string
data: T
}

以后就可以如下请求:


Net.get("url").requestObject<ApiResult<TestModel>>((data) => {
//data 为 返回的ApiResult对象
})

回调函数

回调函数有两个,一个成功一个失败,成功回调必调用,失败可选择性调用。


只带成功


Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

成功失败都带


Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
}, (error) => {
//失败
})

2、get请求


 Net.get("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

3、post请求


Net.post("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

4、delete请求


 Net.delete("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

5、put请求


Net.put("url").request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options。


OPTIONS
HEAD
TRACE
CONNECT

7、各个方法调用


除了正常的请求方式之外,你也可以调用如下的参数:


方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:


Net.get("url")
.setHeaders({})//单独添加请求头参数
.setBaseUrl("")//单独替换BaseUrl
.setParams({})//单独添加参数
.setConnectTimeout(10000)//单独设置连接超时
.setReadTimeout(10000)//单独设置读取超时
.setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
.setUsingCache(true)//使用缓存,默认为true
.setPriority(1)//设置优先级 默认为1
.setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
.setResultTag([""])//接口返回数据参数,比如data,items等等
.setContext(this.context)//设置上下文,用于上传文件和下载文件
.setCustomDialogController()//传递的dialog控制器,用于展示dialog
.request<TestModel>((data) => {
//data 为 返回的TestModel对象
})

五、同步请求介绍


同步请求需要注意,需要await关键字和async关键字结合使用。


 private async getTestModel(){
const testModel = await Net.get("url").returnData<TestModel>()
}

1、请求说明


同步请求和异步请求一样,也是有三种方式,是通过参数的形式,默认直接返回data层数据。


返回data层数据


和异步种的request方法类似,只返回json种的data层对象数据,不会返回code等字段。


 private async getData(){
const data = await Net.get("url").returnData<TestModel>()
//data为 返回的 TestModel对象
}

返回Json对象


和异步种的requestObject方法类似,会返回整个json对象,包含code等字段。


 private async getData(){
const data = await Net.get("url").returnData<TestModel>(ReturnDataType.OBJECT)
//data为 返回的 TestModel对象
}

返回Json字符串


和异步种的requestString方法类似。


private async getData(){
const data = await Net.get("url").returnData<string>(ReturnDataType.STRING)
//data为 返回的 json字符串
}

返回错误


异步方式有回调错误,同步方式如果发生错误,也会直接返回错误,结构如下:


{
"code": 0,
"message": "错误信息"
}

除了以上的错误捕获之外,你也可以全局异常捕获,


2、get请求



const data = await Net.get("url").returnData<TestModel>()

3、post请求



const data = await Net.post("url").returnData<TestModel>()

4、delete请求



const data = await Net.delete("url").returnData<TestModel>()

5、put请求



const data = await Net.put("url").returnData<TestModel>()

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如Net.options


OPTIONS
HEAD
TRACE
CONNECT

7、各个方法调用


除了正常的请求方式之外,你也可以调用如下的参数:


方法类型概述
setHeadersObject单独添加请求头参数
setBaseUrlstring单独替换BaseUrl
setParamsstring / Object / ArrayBuffer单独添加参数,用于post
setConnectTimeoutnumber单独设置连接超时
setReadTimeoutnumber单独设置读取超时
setExpectDataTypehttp.HttpDataType设置指定返回数据的类型
setUsingCacheboolean使用缓存,默认为true
setPrioritynumber设置优先级 默认为1
setUsingProtocolhttp.HttpProtocol协议类型默认值由系统自动指定
setResultTagArray接口返回数据参数,比如data,items等等
setContextContext设置上下文,用于下载文件
setCustomDialogControllerCustomDialogController传递的dialog控制器,用于展示dialog

代码调用如下:


const data = await Net.get("url")
.setHeaders({})//单独添加请求头参数
.setBaseUrl("")//单独替换BaseUrl
.setParams({})//单独添加参数
.setConnectTimeout(10000)//单独设置连接超时
.setReadTimeout(10000)//单独设置读取超时
.setExpectDataType(http.HttpDataType.OBJECT)//设置指定返回数据的类型
.setUsingCache(true)//使用缓存,默认为true
.setPriority(1)//设置优先级 默认为1
.setUsingProtocol(http.HttpProtocol.HTTP1_1)//协议类型默认值由系统自动指定
.setResultTag([""])//接口返回数据参数,比如data,items等等
.setContext(this.context)//设置上下文,用于上传文件和下载文件
.setCustomDialogController()//传递的dialog控制器,用于展示dialog
.returnData<TestModel>()
//data为 返回的 TestModel对象

六、装饰器请求介绍


网络库允许使用装饰器的方式发起请求,也就是通过注解的方式,目前采取的是装饰器方法的形式。


1、请求说明


装饰器和同步异步有所区别,只返回两种数据类型,一种是json字符串,一种是json对象,暂时不提供返回data层数据。 在使用的时候,您可以单独创建工具类或者ViewModel或者直接使用,都可以。


返回json字符串


@GET("url")
private getData():Promise<string> {
return null
}

返回json对象


@GET("url")
private getData():Promise<TestModel> {
return null
}

2、get请求


@GET("url")
private getData():Promise<TestModel> {
return null
}

3、post请求


@POST("url")
private getData():Promise<TestModel> {
return null
}

4、delete请求


@DELETE("url")
private getData():Promise<TestModel> {
return null
}

5、put请求


@PUT("url")
private getData():Promise<TestModel> {
return null
}

6、其他请求方式


除了常见的请求之外,根据系统api所提供的,也封装了如下的请求方式,只需要更改请求方式即可,比如@OPTIONS。


OPTIONS
HEAD
TRACE
CONNECT

当然,大家也可以使用统一的NET装饰器,只不过需要自己设置请求方法,代码如下:


@NET("url", { method: http.RequestMethod.POST })
private getData():Promise<string> {
return null
}

7、装饰器参数传递


直接参数传递


直接参数,在调用装饰器请求时,后面添加即可,一般针对固定参数。


@GET("url", {
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
private getData():Promise<string> {
return null
}

动态参数传递


动态参数适合参数可变的情况下传递,比如分页等情况。


@GET("url")
private getData(data? : HttpOptions):Promise<string> {
return null
}

调用时传递


private async doHttp(){
const data = await this.getData({
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
}

装饰器参数传递


使用DATA装饰器,DATA必须在上!


@DATA({
baseUrl: "", //baseUrl
header: {}, //头参数
params: {}, //入参
connectTimeout: 1000, //连接超时
readTimeout: 1000, //读取超时
isReturnJson: true//默认false 返回Json字符串,默认返回json对象
})
@GET("url")
private getData():Promise<string> {
return null
}

七、上传下载介绍


1、上传文件


Net.uploadFile("")//上传的地址
.setUploadFiles([])//上传的文件 [{ filename: "test", name: "test", uri: "internal://cache/test.jpg", type: "jpg" }]
.setUploadData([])//上传的参数 [{ name: "name123", value: "123" }]
.setProgress((receivedSize, totalSize) => {
//监听上传进度
})
.request((data) => {
if (data == UploadTaskState.COMPLETE) {
//上传完成
}
})

方法介绍


方法类型概述
uploadFilestring上传的地址
setUploadFilesArray上传的文件数组
setUploadDataArray上传的参数数组
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求上传,data类型为UploadTaskState,有三种状态:START(开始),COMPLETE(完成),ERROR(错误)

其他方法


删除上传进度监听

uploadRequest.removeProgressCallback()

删除上传任务

uploadRequest.deleteUploadTask((result) => {
if (result) {
//成功
} else {
//失败
}
})

2、下载文件


Net.downLoadFile("http://10.47.24.237:8888/harmony/log.har")
.setContext(EntryAbility.context)
.setFilePath(EntryAbility.filePath)
.setProgress((receivedSize, totalSize) => {
//监听下载进度
})
.request((data) => {
if (data == DownloadTaskState.COMPLETE) {
//下载完成
}
})

方法介绍


方法类型概述
downLoadFilestring下载的地址
setContextContext上下文
setFilePathstring下载后保存的路径
setProgress回调函数监听进度,receivedSize下载大小, totalSize总大小
request请求下载,data类型为DownloadTaskState,有四种状态:START(开始),COMPLETE(完成),PAUSE(暂停),REMOVE(结束)

其他方法


移除下载的任务

    downLoadRequest.deleteDownloadTask((result) => {
if (result) {
//移除成功
} else {
//移除失败
}
})

暂停下载任务

downLoadRequest.suspendDownloadTask((result) => {
if (result) {
//暂停成功
} else {
//暂停失败
}
})

重新启动下载任务

downLoadRequest.restoreDownloadTask((result) => {
if (result) {
//成功
} else {
//失败
}
})

删除监听下载进度

downLoadRequest.removeProgressCallback()

八、Dialog加载使用



1、定义dialog控制器


NetLoadingDialog是net包中自带的,菊花状弹窗,如果和实际业务不一致,可以更换。


private mCustomDialogController = new CustomDialogController({
builder: NetLoadingDialog({
loadingText: '请等待...'
}),
autoCancel: false,
customStyle: true
})

2、调用传递控制器方法


此方法会自动显示和隐藏dialog,如果觉得不合适,大家可以自己定义即可。


setCustomDialogController(this.mCustomDialogController)

九、相关总结


开发环境如下:


DevEco Studio 4.0 Beta2,Build Version: 4.0.0.400

Api版本:9

hvigorVersion:3.0.2

目前呢,暂时不支持缓存,后续会逐渐加上,大家在使用的过程中,需要任何的问题,都可以进行反馈,都会第一时间进行解决。


作者:程序员一鸣
来源:juejin.cn/post/7295397683397181450
收起阅读 »

工作六年,看到这样的代码,内心五味杂陈......

工作六年,看到这样的代码,内心五味杂陈...... 那天下午,看到了令我终生难忘的代码,那一刻破防了...... 🔊 本文记录那些年的 Java 代码轶事 ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起...... 📖一、历史背景 1.1 数据隔...
继续阅读 »

工作六年,看到这样的代码,内心五味杂陈......


那天下午,看到了令我终生难忘的代码,那一刻破防了......



🔊 本文记录那些年的 Java 代码轶事



ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起......


📖一、历史背景


1.1 数据隔离


预发,灰度,线上环境共用一个数据库。每一张表有一个 env 字段,环境不同值不同。特别说明: env 字段即环境字段。如下图所示:
image.png


1.2 隔离之前


🖌️插曲:一开始只有 1 个核心表有 env 字段,其他表均无该字段;
有一天预发环境的操作影响到客户线上的数据。 为了彻底隔离,剩余的二十几个表均要添加上环境隔离字段。


当时二十几张表已经大量生产数据,隔离需要做好兼容过渡,保障数据安全。


1.3 隔离改造


其他表历史数据很难做区分,于是新增加的字段 env 初始化 all ,表示预发线上都能访问。以此达到历史数据的兼容。


每一个环境都有一个自己独立标志;从 application.properties 中读该字段;最终到数据库执行的语句如下:


SELECT XXX FROM tableName WHERE env = ${环境字段值} and ${condition}

1.4 隔离方案


最拉胯的做法:每一张表涉及到的 DO、Mapper、XML等挨个添加 env 字段。但我指定不能这么干!!!


image.png


具体方案:自定义 mybatis 拦截器进行统一处理。 通过这个方案可以解决以下几个问题:



  • 业务代码不用修改,包括 DO、Mapper、XML等。只修改 mybatis 拦截的逻辑。

  • 挨个添加补充字段,工程量很多,出错概率极高

  • 后续扩展容易


1.5 最终落地


在 mybatis 拦截器中, 通过改写 SQL。新增时填充环境字段值,查询时添加环境字段条件。真正实现改一处即可。 考虑历史数据过渡,将 env = ${当前环境}
修改成 en 'all')


SELECT xxx FROM ${tableName} WHERE env in (${当前环境},'all') AND ${其他条件}

具体实现逻辑如下图所示:


image.png



  1. 其中 env 字段是从 application.properties 配置获取,全局唯一,只要环境不同,env 值不同

  2. 借助 JSqlParser 开源工具,改写 sql 语句,修改重新填充、查询拼接条件即可。链接JSQLParser


思路:自定义拦截器,填充环境参数,修改 sql 语句,下面是部分代码示例:


@Intercepts(
{@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)

@Component
public class EnvIsolationInterceptor implements Interceptor {
......
@Override
public Object intercept(Invocation invocation) throws Throwable {
......
if (SqlCommandType.INSERT == sqlCommandType) {
try {
// 重新 sql 执行语句,填充环境参数等
insertMethodProcess(invocation, boundSql);
} catch (Exception exception) {
log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), exception);
throw exception;
}
}

return invocation.proceed();
}
}

一气呵成,完美上线。


image.png


📚二、发展演变


2.1 业务需求


随着业务发展,出现了以下需求:



  • 上下游合作,我们的 PRC 接口在匹配环境上与他们有差异,需要改造


SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?,'all')


  • 有一些环境的数据相互相共享,比如预发和灰度等

  • 开发人员的部分后面,希望在预发能纠正线上数据等


2.2 初步沟通


这个需求的落地交给了来了快两年的小鲜肉。 在开始做之前,他也问我该怎么做;我简单说了一些想法,比如可以跳过环境字段检查,不拼接条件;或者拼接所有条件,这样都能查询;亦或者看一下能不能注解来标志特定方法,你想一想如何实现......


image.png


(●ˇ∀ˇ●)年纪大了需要给年轻人机会。


2.3 勤劳能干


小鲜肉,没多久就实现了。不过有一天下午他遇到了麻烦。他填充的环境字段取出来为 null,看来很久没找到原因,让我帮他看看。(不久前也还教过他 Arthas 如何使用呢,这种问题应该不在话下吧🤔)


2.4 具体实现


大致逻辑:在需要跳过环境条件判断的方法前后做硬编码处理,同环切面逻辑, 一加一删。填充颜色部分为小鲜肉的改造逻辑。


image.png


大概逻辑就是:将 env 字段填充所有环境。条件过滤的忽略的目的。


SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${其他条件}

2.5 错误原因


经过排查是因为 API 里面有多处对 threadLoal 进行处理的逻辑,方法之间存在调用。 简化举例: A 和 B 方法都是独立的方法, A 在调用 B 的过程,B 结束时把上下文环境字段删除, A 在获取时得到 null。具体如下:


image.png


2.6 五味杂陈


当我看到代码的一瞬间,彻底破防了......


image.png


queryProject 方法里面调用 findProjectWithOutEnv,
在两个方法中,都有填充处理 env 的代码。


2.7 遍地开花


然而,这三行代码,随处可见,在业务代码中遍地开花.......


image.png


// 1. 变量保存 oriFilterEnv
String oriFilterEnv = UserHolder.getUser().getFilterEnv();

// 2. 设置值到应用上下文
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());

//....... 业务代码 ....

// 3. 结束复原
UserHolder.getUser().setFilterEnv(oriFilterEnv);

image.png


改了个遍,很勤劳👍......


2.8 灵魂开问


image.png


难道真的就只能这么做吗,当然还有......



  • 开闭原则符合了吗

  • 改漏了应该办呢

  • 其他人遇到跳过的检查的场景也加这样的代码吗

  • 业务代码和功能代码分离了吗

  • 填充到应用上下文对象 user 合适吗

  • .......


大量魔法值,单行字符超500,方法长度拖几个屏幕也都睁一眼闭一只眼了,但整这一出,还是破防......


内心涌动😥,我觉得要重构一下。


📒三、重构一下


3.1 困难之处


在 mybatis intercept 中不能直接精准地获取到 service 层的接口调用。 只能通过栈帧查询到调用链。


3.2 问题列表



  • 尽量不要修改已有方法,保证不影响原有逻辑;

  • 尽量不要在业务方法中修改功能代码;关注点分离;

  • 尽量最小改动,修改一处即可实现逻辑;

  • 改造后复用能力,而不是依葫芦画瓢地添加这种代码


3.3 实现分析



  1. 用独立的 ThreadLocal,不与当前用户信息上下文混合使用

  2. 注解+APO,通过注解参数解析,达到目标功能

  3. 对于方法之间的调用或者循环调用,要考虑优化


同一份代码,在多个环境运行,不管如何,一定要考虑线上数据安全性。


3.4 使用案例


改造后的使用案例如下,案例说明:project 表在预发环境校验跳过


@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})


@SneakyThrows
@GetMapping("/importSignedUserData")
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void importSignedUserData(
......
HttpServletRequest request,
HttpServletResponse response)
{
......
}

在使用的调用入口处添加注解。


3.5 具体实现



  1. 方法上标记注解, 注解参数定义规则

  2. 切面读取方法上面的注解规则,并传递到应用上下文

  3. 拦截器从应用上下文读取规则进行规则判断


image.png


注解代码


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {

/**
* 是否跳过环境。 默认 true,不推荐设置 false
*
* @return
*/

boolean isKip() default true;

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipEnvList() default {};

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipTableList() default {};
}


3.6 不足之处



  1. 整个链路上的这个表操作都会跳过,颗粒度还是比较粗

  2. 注解只能在入口处使用,公共方法调用尽量避免


🤔那还要不要完善一下,还有什么没有考虑到的点呢? 拿起手机看到快12点的那一刻,我还是选择先回家了......


📝 四、总结思考


4.1 隔离总结


这是一个很好参考案例:在应用中既做了数据隔离,也做了数据共享。通过自定义拦截器做数据隔离,通过自定注解切面实现数据共享。


4.2 编码总结


同样的代码写两次就应该考虑重构了



  • 尽量修改一个地方,不要写这种边边角角的代码

  • 善用自定义注解,解决这种通用逻辑

  • 可以妥协,但是要有底线

  • ......


4.3 场景总结


简单梳理,自定义注解 + AOP 的场景


场景详细描述
分布式锁通过添加自定义注解,让调用方法实现分布式锁
合规参数校验结合 ognl 表达式,对特定的合规性入参校验校验
接口数据权限对不同的接口,做不一样的权限校验,以及不同的人员身份有不同的校验逻辑
路由策略通过不同的注解,转发到不同的 handler
......

自定义注解很灵活,应用场景广泛,可以多多挖掘。


4.4 反思总结



  • 如果一开始就做好技术方案或者直接使用不同的数据库是否可以解决这个问题

  • 是否可以拒绝那个所谓的需求

  • 先有设计再有编码


4.5 最后感想


image.png



在这个只讲业务结果,不讲技术氛围的环境里,突然有一些伤感;身体已经开始吃不消了,好像也过了那个对技术较真死抠的年纪; 突然一想,这么做的意义又有多大呢?



作者:uzong
来源:juejin.cn/post/7294844864020430902
收起阅读 »

背调,程序员入职的紧箍咒

首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。 今天跟大家分享出来,给大家在求职路上避避坑。 上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴...
继续阅读 »

首先说下,目前,我的表哥自己开一家小的背调公司,所以我在跟他的平时交流中,了解到了背调这个行业的一些信息。


今天跟大家分享出来,给大家在求职路上避避坑。


上周的某天,以前的阿里同事小李跟我说,历经两个月的面试,终于拿到了开水团的offer。我心里由衷地替他高兴,赶紧恭喜了他,现在这年头,大厂的offer没这么好拿的。


又过了两周,小张沮丧地跟我说,这家公司是先发offer后背调,结果背调之后,offer GG了,公司HR没有告知他具体原因,只是委婉地说有缘自会再相见。(手动狗头)


我听了,惋惜之余有些惊讶,问了他具体情况。


原来,小李并没有在学历上作假,也没有做合并或隐藏工作经历的事。


他犯错的点是,由于在上家公司,他跟他老板的关系不好,所以他在背调让填写上级领导信息的时候,只写了上级领导的名字,电话留的是他一个同事的。


我听后惋惜地一拍脑门儿,说:“你这么做之前,怎么也不问我一下啊?第三方背调公司进行手机号和姓名核实,都是走系统的,秒出结果。而且,这种手机号的机主姓名造假,背调结果是亮红灯的,必挂。”


小李听后,也是悔得肠子都青了,没办法,只能重新来过了。


我以前招人的时候,遇到过一次这样的情况,当时有个候选人面试通过,发起背调流程。一周后,公司HR给了我一份该候选人背调结果的pdf,上面写着:


“候选人背调信息上提供,原公司上级为郭xx,但经查手机号主为王xx,且候选人原公司并无此人。”


背调结果,红灯,不通过。


基本面


学历信息肯定不能造假,这个大家应该都清楚,学信网不是吃素的,秒出结果。


最近两份工作的入离职时间不要出问题,这个但凡是第三方背调,近两份工作是必查项,而且无论是明察还是暗访,都会查得非常仔细,很难钻空子的。


再有就是刚才说的,手机号和人名要对上,而且这个人确实是在这家公司任职的。


大家耳熟能详的大厂最好查,背调公司都有人才数据库的,而且圈子里的人也好找。再有就是,随便找个内部员工,大厂的组织结构在内部通讯软件里都能看到的。


小厂难度大一些,如果人才数据库没有的话,背调员会从网上找公司电话,然后打给前台,让前台帮忙找人。但有的前台听了会直接挂断电话。


薪资方面,不要瞒报,一般背调公司会让你打印最近半年或一年的流水,以及纳税信息。


直接上级


这应该也是大家最关心的问题之一。


马云曾经说过:离职无非两种原因,钱没给够,心委屈了。而心委屈了,绝大多数都跟自己的直接上级有关。


如果在背调的时候,担心由于自己跟直接上级关系不好,从而导致背调结果不利的话,可以尝试以下三种方式。


第一,如果你在公司里历经了好几任领导的话,可以留关系最好的那任领导的联系方式,这个是在规则允许范围内的。


第二,如果你的直接上级只是一个小组长,而你跟大领导(类似于部门负责人)关系还可以的话,可以跟大领导沟通一下,然后背调留他的信息。像这个,一般背调公司不会深究的。


就像我的那个表哥,背调公司的老板所说的:“如果一个腾讯员工,马化腾都出来给他做背调了,那我们还能说什么呢?”


第三,如果前两点走不通的话,还可以坦诚地跟HR沟通一次,说明跟上级之间确实存在一些问题,原因是什么什么。


比如:我朋友遇到了这种情况,公司由于经营不善而裁员,老板竟然无耻地威胁我朋友,如果要N+1赔偿的话,背调就不会配合。


如果你确实不是责任方的话,一般HR也能理解。毕竟都是打工人,何苦相互为难呢。


你还可以这么加上一句:“我之前工作过的公司,您背调哪家都可以,我的口碑都很好的,唯独这家有些问题。”


btw:还有一些朋友,背调的时候留平级同事的真实电话和姓名,用来冒充领导,这个是有风险的。但是遇到背调不仔细的公司,也能通过。通过概率的话,一半一半吧。


就像我那个朋友所说:“现在人力成本不便宜,如果公司想盈利的话,我的背调员一天得完成5个背调,平均不到两个小时一个。你总不能希望他们个个都是名侦探柯南吧。”


信用与诉讼


一般来讲,背调的标准套餐还包括如下内容:金融违规、商业利益冲突、个人信用风险和有限民事诉讼。其中后两个大家尽量规避。


个人信用风险包括:网贷/逾期风险、反欺诈名单和欠税报告。


网贷这块,当时我有一个同事,2021年的时候,拿了4个offer,结果不明不白地都挂在了背调上,弄得他很懵逼。


当他问这三家公司HR原因的时候,HR都告诉他不便透露。


最后,他动用身边人脉,才联系上一家公司的HR出来吃饭,HR跟他说:“以后网贷不要逾期,尤其是不同来源的网贷多次逾期。”


同事听了,这才恍然大悟。


欠税这个,就更别说了,呵呵,大家都懂,千万别心存侥幸。


再说说劳动仲裁和民事诉讼。


现在有些朋友确实法律意识比较强,受到不公正待遇了,第一想法就是“我要去仲裁”,仲裁不满意了,就去打官司。


首先我要说的是,劳动仲裁是查不到的,所以尽量在这一步谈拢解决。


但民事诉讼在网上都是公开的,而且第三方背调公司也是走系统的,一查一个准儿。如果非必要的话,尽量不要跟公司闹到这一步。


如果真遇到垃圾公司或公司里的垃圾人,第一个想法应该是远离,不要让他们往你身上倒垃圾。


尤其是你主动跟公司打官司这种,索要个加班费、年终奖什么的,难免会让新公司产生顾虑,会不会我offer的这名候选人,以后也会有对簿公堂的一天。


结语


现在这大市场行情,求职不易,遇到入职前背调更是如履薄冰,希望大家都能妥善处理好,一定要避免节外生枝的情况发生,不要在距离成功一米的距离倒下。


最后,祝大家工作顺利,纵情向前,人人都能拿到自己满意的offer,开开心心地入职。


作者:库森学长
来源:juejin.cn/post/7295160228879204378
收起阅读 »

迷茫的前端们

web
前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。 最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。 什么情况 首先...
继续阅读 »

前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。


最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。


什么情况


首先,要看看为什么会有这个观点。原因也很简单,自从去年开始,各大公司都开始裁员,然后HC也开始减少。但是因为前几年互联网工资收入高,每年毕业的应届生那是一年比一年多。前端培训班也火热的不行,每年都向社会输送大面积的前端开发。


所以,从供需角度看,前端开发找工作,尤其是刚毕业和刚培训毕业的前端开发,今年找工作就是地狱模式。所以有人说“前端已死”,自然很快就能获得共鸣


另外的声音


然后,有意思的来了。“前端已死”这个话题火了后,各大前端自媒体,前端前辈都出来发声。观点出奇的一致:前端不会死。一下子就把前端已死的观点给压下去了。这种社区声音180度的大转弯也是少见。


到底死没死


说完现象,我说下我的观点。



  1. “前端已死”的声音能获得共鸣,说明前端已经发展到了一个关键的时间点。

  2. 只要还有页面,还有小程序,前端就不会死。

  3. 不会死,并不是说能活的很好。


行业周期


互联网在行业分类中属于第三产业,也就是服务业。服务业的特点就是周期性。如果所有服务业的周期是往下走的,互联网也好不了。三年疫情,大家都知道,服务业已经被折腾的不行了,即使今年放开了,大家还敢开线下店吗?被互联网服务的行业大部分都过的不好,互联网能好吗。


当然,后面随着经济复苏,互联网也会慢慢恢复景气。但是这里还有另外一个点,由于互联网前几年工资都太高了,年年倒挂,导致选计算机专业的人越来越多。以大学四年计算,后面3年每年毕业的计算机专业的学生,还是会持续增加的。


所以呢,从供需角度,技术互联网恢复景气了,互联网招聘未必会恢复景气。

当然了,我觉得,后面几年,不会像今年这么卷,投10份简历,连个面试机会都没有。(预测这种东西,很容易打脸,大家看看就好)


另外,就前端开发而言,我感觉好多东西都很成熟了。最近看技术社区的文章,感觉很久没有看到很新的内容了(也许是推荐系统搞得信息茧房)。大家都在几个领域里面深挖。如果没有新的方向出来,真的很难容纳这么多的求职者。后面看看AI能不能创新出一波职位吧。


所以呢,从互联网行业和前端行业来看,前端已经发展到了一个关键点,后面如果没有新的方向出来,即使互联网恢复景气,求职也不会像以前那么轻松了。

但是,毕竟还有那么多业务需要页面,需要小程序,这块的需求还是在的,所以还是会有前端开发的。


技术周期


从我毕业,基本上都在做互联网,期间其实经历过好几轮技术迭代。比如最早的Flash, 到PHP, PC端的JQuery。你很难在招聘网站上找到要求这些技术的岗位了。

随着前端的发展,技术栈也一直在变化。但是,大家想想,现在流行的React 和 Vue,已经用了多久了。如果还是只会React,Vue,是不是说明已经很久没有成长了。后面会不会有新的技术栈出来,比如VR出来后,是不是还用React开发?

如果还是继续用React,Vue,那么和行业周期里面说的一样,前端不会死,但是不会像以前那么活的好。


新方向在哪里


其实聊了这么多,本质就是一个供需。供需这个角度真的太强大了,能解释很多问题。如果未来没有一个新的大方向出现,那么在岗位需求变化不大的情况下,每年大量学生毕业,也就是供应增多,求职困难是必然的。

那么会不会有新方向出现呢,目前看,有几个可能的方向:



  1. AI。从去年底开始,AI持续火热,目前虽然有退烧,但是AI的趋势已经明确。后面要看的是,AI是不是需要大量的前端工作。这块大家可以说说自己的判断。

  2. VR元宇宙。Meta搞元宇宙,差点把自己搞死,现在苹果也出VR设备,观察下能不能把这个行业带起来。大家说有没有可能。

  3. Web3。去年年初,Web3还很火了的,现在好像,嗯,一般般。后面能不能再次爆发,也要继续观察。大家也可以说说自己的开发。


扯一句


最后扯一句。我看了好多前端前辈对“前端已死”的看法,都说前端不会死,会死的都是初级前端,前端要持续学习,要让自己不可替代。说实话,我觉得这都是屁话。写代码的谁不是从初级开始的呢,现在是完全不给初级的机会,断档了!写代码的人,已经是最爱学习的那一批人了吧,永远在学习新东西。还有让自己不可替代,公司会想办法让你可替代,后面聊一聊不可替代这个话题,我也有很多想说的。


结束


后面会逐步把掌握的前端知识以及职场知识沉淀下来。 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:写代码的浩
来源:juejin.cn/post/7253437782333669434
收起阅读 »

协程-来龙去脉

首先必须声明,此文章是观看油管《KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov》的感想,如何可以,更建议您去观看这个视频而不是阅读本篇文章 代码的异步控制 举个例子,假设你要去论坛...
继续阅读 »

首先必须声明,此文章是观看油管《KotlinConf 2017 - Introduction to Coroutines by Roman Elizarov》的感想,如何可以,更建议您去观看这个视频而不是阅读本篇文章


代码的异步控制


举个例子,假设你要去论坛上发布消息,你必须先获取token以表明你的身份,然后创建一个消息,最后去发送它


//这是一个耗时操作
fun requestToken():Token = {
... //block to wait to receive token
returen token
}

//这也是一个耗时操作
fun creatMessage() : Message ={
... //block to wait to creat Message
returen token
}

fun sendMessage(token :Token,message:Message)

fun main() {
val token = requestToken()
val meassage = creatMessage()
sendMessage(token,message)
}


这种情况显然不符合我们的现实情况,我们不可能在没有拿到token就等待在那里,我们可以开启一个线程去异步执行



fun requestToken():Token = {
... //new thread to request token
returen token
}

像这样一个任务我们就需要创建一个线程,creatMessage与requestToken可以并行进行,因此我们需要再次创建一个线程去执行creatMessage,从而创建两个线程。现在我们的手机性能很很高,我们可以创建一个,两个,甚至是一百个个线程去执行任务,但是到达一千个,一万个呢,恐怕手机的不足以支撑。
怎么解决这样的问题呢。我们只需要建立一种通知机制,在token返回后告诉我们,我们再继续完成creatMessage,进而sendMessage。这也就是callback方式



fun requestTokenCallback(callback : (Token) -> Unit) {
... //block to wait to receive token
callback.invoke(token)
}

fun creatMessageCallback : (Message) -> Unit){
... //nblock to wait to creat Message
callback.invoke(message)
}

fun sendMessage(token :Token,message:Message)

fun main() {
//创建一个线程
Thead {
requestTokenCallback { token ->
creatMessageCallback { message ->
{
sendMessage(token, message)
}
}
}

}

}


这仅仅是一个简单的案例,就产生了如此多的嵌套和连续的右括号,在实际业务中往往更为复杂,比如请求失败或者一些异常情况,甚至是一些特定的业务操作,想想这样叠加下去,简直是灾难。
如何解决这种问题呢,java中有一个CompleteFuture,正如其名,它能够异步处理任务,以期获取未来的结果去处理,我们只需要允诺我们将来在某个时间点一定会返回某种类型的数据,我们就可以以预知未来的方式使用他,


//创建一个线程去异步执行
fun requestToken() :CompletableFuture<Token> = ...

//创建一个线程去异步执行
fun creatMessage(token) : CompletableFuture<Send> = ...

fun sendMessage(send :Send)

fun main() {
requestToken()
.thenCompose{token -> creatMessage(token)}
.thenAccept{send -> sendMessage(send)}
}


令人头疼的代码已不复存在,我们可以自由组合我们任务
creatMessage的调用方式和sendMessage并不相同,kotlin为了统一这两种调用,产生了一个suspend 关键字,它能够像拥有魔法一样,让世界的时间暂停,自己又不会暂停,然后去执行自己的任务,执行完成之后,时间恢复,任务继续执行



suspend fun requestToken() :Token = ...

suspend fun creatMessage() : message = ...


fun sendMessage(token :Token,message:Message)

fun main() {
val token = requestToken()
val meassage = creatMessage()
sendMessage(token,message)
}


执行这段代码的时候,会发生编译错误,因为main函数只是一个普通的函数,并没有被suspend标记,当requestToken使时间暂停的同时,主程序也时间暂停了,那这与最开始的阻塞方法有什么不一样的呢


协程Coroutine


本文要介绍的是协程,协程是什么呢,在我看来,协程就是一个容器,让suspend标记的函数可以运行,可以开启魔法 ,让它在时间暂停的同时,并不影响主线程


public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,//创建容器的上下文
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T//拥有魔法的函数
)
: Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy) //创建协程
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block) //启动魔法开关
return coroutine
}

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext, //创建容器的上下文
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit //拥有魔法的函数
)
: Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy) //创建协程
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block) //启动魔法开关,启动协程
return coroutine
}

我们可以通过Deferred.await() 获取将来的值T。自此,我们有了新的代码



suspend fun requestToken() :Token = ...

suspend fun creatMessage() : message = ...


fun sendMessage(token :Token,message:Message)

fun main() {
val token = async {requestToken()}
val meassage = async{creatMessage()}
sendMessage(token.await() ,message.await() )
}


实际业务中,我们的任务并不是一直都是需要返回结果的,所以还有另一种容器,只需要去执行


public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
)
: Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}


轻量级线程


协程被视为轻量级线程,轻量在哪呢?


实际上,async与launch 并没有什么魔法,只是将封装好的任务交由线程池去执行,所以suspend标记的函数可以任意暂停


协程只是代码层级的概念,操作系统对于此是无感知的,但是线程作为cpu调度的基本单位,创建和调用是很重的,需要中断机制去进行调度,消耗很多额外的资源,所以协程被视为轻量级线程。


上面的代码中launch 与async 都是CoroutineScope 的函数,那么CoroutineScope 是什么呢
这个就是协程运行的温床,也可说是运行的基础,也就是作用域。创建处一个协程,必须有一个管理容器去管理协程的创建,分发,调度,这就是CoroutineScope。


有一种常见的需求是网络执行完成切换UI线程去执行,因此,我们需要在创建容器的时候需要一个参数,去声明协程到底在线程池中执行,UI线程池还是普通线程池,


val scope = CoroutineScope(Dispatchers.IO) //IO线程池去处理
val scope = CoroutineScope(Dispatchers.Main) // UI线程去处理

题外


阅读到这里可以知道suspend实际上就是一个callback封装, 对于其如何将标记函数转化为可挂起恢复的,可浏览# Kotlin Vocabulary | 揭秘协程中的 suspend 修饰符,个人觉得讲的不错


在推荐的这篇文章中,可以看到使用了状态机,其目的是为了节省Continuation对象的创建,可以借鉴学习


关于我


一个希望友友们能提出建议的代码下毒糕手


作者:小黑不黑
来源:juejin.cn/post/7294852698460373004
收起阅读 »

一亿数据大表,我们是如何做分页的

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。 最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功...
继续阅读 »

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。


最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功能进行优化。


对于深分页问题,一般都是采用传递主键ID的做法来解决,也就是每次查询下一页数据时,把上一页数据中最大的ID传递给下一页,这样在查询某一页时就能利用主键索引来快速查询了。


但是由于历史原因,那张一亿数据的大表没有主键ID,-_-,所以这是需要解决的问题之一。


另外,就算能利用主键索引快速查询分页数据,但是毕竟是一亿数据,最终查询比较靠后的页数时,因为数据量大就算走索引最后还是会比较慢,所以就需要分表了。


我们先使用SQL脚本的方式把一亿数据迁移到分表,我们按照时间季度进行分表,比如2022q1、2022q2、2023q1这种方式来进行分表,这样每张分表的数据就不会太多了,大概1000万左右,并且在创建分表时顺便给分表添加主键ID字段,然后在迁移数据时需要按照原始大表的交易时间进行升序排序,保证ID的自增顺序与交易时间顺序是一致的。


不过这种方案存在两个问题,第一个问题就是查询分页时跨分表了怎么办,比如我查第10页数据时,一部分数据在2023q1,一部分数据在2023q2,首先在查询时本身就需要选择年份和季度,所以直接就确定了对应的是哪张分表,查询时根据年份和季度直接定位到分表,然后再进行分页查询,所以不会出现跨表,这个问题就自然而然解决了。


另外,每张表的主键ID要不要连续,比如2022q1的主键是1-1000000,那么2022q1的主键要不要是1000001-2000000,其实我个人觉得需要这么设计,因为两个分表的数据逻辑上其实一张表,所以主键最好不冲突,但是考虑到实现难度,暂时没有采取,而是每张分表的主键ID都是从1开始的,这种方案在目前看来是没有问题的,因为每次都只会从一张分表中查询数据,不会出现跨表的情况。


另外,在设计时也想到过能不能直接用交易时间做为主键来进行分表,但是想到交易时间的精度是秒,所以很有可能出现交易时间相同的记录,这样在做分页时可能会出现下一页数据和上一页数据有重复的,所以还是需要单独设计一个主键ID。


继续思考,假如这张一亿数据的大表要做分页,但是不根据年份和季度做查询来分页,而就是直接分页,那又该如何呢?


首先,肯定还是得利用分表和主键索引,我的思路是,先给原始表添加主键ID并生成自增ID,然后再按主键ID进行分表,分表记录数也可以控制在1000万左右,前端查询时仍然按ID来查分页就可以了,但是此时就存在跨表的问题,比如每页如果是20条,查第10页数据时,如果从分表一只查出了5条,那么后端接口判断5小于20,就尝试从分表二中继续查。


当然了,如果既有复杂的查询条件,又需要进行分页,那用关系型数据库就不太好做了,就可以考虑ES之类的了。


大家有什么更好的经验或思路,欢迎不吝赐教。


我是爱分享技术的大都督周瑜,欢迎关注我的公众号:Hoeller。公众号里有更多高质量干货系列文章和精品面试题。


记得点赞、分享哦!!!


作者:爱读源码的大都督
来源:juejin.cn/post/7294823722807017499
收起阅读 »

分享下最近找工作的行情

国庆前开始陆续随便投了一点公司试了下行情,确实不很乐观,但是当时还没有特别强烈的感觉。 目前这国庆之后再投了几天的感受特别强烈,就是基本凉凉的节奏。 目前感受下来,招人的公司有限,上海这边就字节、PDD、美团、小红书、得物这些在大量招人,在招聘平台脉脉、拉勾、...
继续阅读 »

国庆前开始陆续随便投了一点公司试了下行情,确实不很乐观,但是当时还没有特别强烈的感觉。


目前这国庆之后再投了几天的感受特别强烈,就是基本凉凉的节奏。


目前感受下来,招人的公司有限,上海这边就字节、PDD、美团、小红书、得物这些在大量招人,在招聘平台脉脉、拉勾、BOSS上基本上也是这些有反馈,另外猎头基本上推的也就是得物、字节这些公司,大部分都是得物。


总结下来的情况就是,要么就是一线大厂在招人,要么就是在这个经济不好的情况下业务做得好的、没什么影响的在招人,比如得物了。


朋友问了一圈,一些公司有HC,但是在招聘平台上都是没有声音的。


华为朋友告诉我这个学历只能去OD,985都在干OD。


字节告诉我卷死来,心脏和字节只能有一个跳动。


腾讯有少量HC,但是也是一直在裁员,很不稳定,米哈游也一样。


饿了么还有一些HC,但是也是在一边裁员,一边有些部门有少量HC。


SAP目前没HC,也是裁员。


联想也没有HC,早就没了。


携程和一些其他的小一点的公司,还有一些HC,一线开发,但是好像薪资不是很高的样子,没有细问了。


诸如此类。。。


还有一些小点的公司,去年还在招人,问我要不要过去,今年普遍都在裁员,没有HC了。


还有一些在家办公的在招人,这个你懂的,但是也非常的卷。


大致情况市场就是这样,要么去卷,要么失业的节奏。


想找钱多活少的,目前看起来是非常不现实了,甚至于想找个工作都非常困难。


听说大厂现在都要卡学历、业务、大厂经历,三高匹配至少两项。


总体来看,暂时没有什么好的出路,还在继续看。。。


有好公司欢迎拉我入坑。。。


就这样。


作者:艾小仙
来源:juejin.cn/post/7288540474058457125
收起阅读 »

如何在网页中展示源代码

web
如何在网页中展示你的源代码 如下图所示: 在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码 第一步: 自定义 vite插件 首先需要一个自定义插件用来转换 vue 的自定义块 ...
继续阅读 »

如何在网页中展示你的源代码


如下图所示:


1698137911654.png


在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码


第一步: 自定义 vite插件


首先需要一个自定义插件用来转换 vue 的自定义块


在vite.config.ts文件里


import fs from 'fs'
import {baseParse} from '@vue/compiler-core'
const vueDemoPlugin = {
name: "vue-block-demo",
transform(code, path) {
if (!/vue&type=demo/.test(path)) {
return;
}
const filePath = path.split("?")[0];
//异步读取文件内容,并转为string类型
const file = fs.readFileSync(filePath).toString();
//将读取到的文件中的自定义快渲染为AST
const parsed = baseParse(file).children.find((n) => n.tag === "demo");
//读取自定义模块中的文本内容
const title = parsed.children[0].content;
//将读取文件中的自定义块切分,并转为字符串类型
const main = file.split(parsed.loc.source).join("").trim();
//以JSON数据类型返回
return `export default Comp => {
Comp.__sourceCode = ${JSON.stringify(main)}
Comp.__sourceCodeTitle = ${JSON.stringify(title)}
}`
;
},
};
export default defineConfig({
plugins: [vue(), vueDemoPlugin],
})

第二步:在要展示的源代码文件里面加上一个自定义块


比如我要展示 SwitchDemo01.vue 文件


<template> 加上 <demo> 自定义块,里面内容写上 title


<demo>常规用法</demo>
<template>
<hx-switch v-model="value1" />
<hx-switch
v-model="value2"
class="ml-2"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
/>
</template>

<script lang="ts" setup>
import { HxSwitch } from 'hx-gulu-ui';
import { ref } from 'vue'

const value1 = ref(true)
const value2 = ref(true)
</script>

第三步: 在页面展示文件里面引用


如: showSwitch.vue 文件:


<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre>{{ SwitchDemo01.__sourceCode }}</pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

// __sourceCode 这里面是源文件去除了 <demo> 外的所有代码
console.log('SwitchDemo01', SwitchDemo01.__sourceCode)

// __sourceCodeTitle 这里面是 <demo>常规用法</demo> 里面的文字
console.log('SwitchDemo01', SwitchDemo01.__sourceCodeTitle)

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

此时,已经可以在代码上显示源文件代码了,但是代码没有任何样式,很丑怎么办呢?


第四步: 引入 prismjs




  • prismjs 是代码主题的插件



    • 官网

    • 安装: npm i prismjs




  • 调用


    import 引入 好像有问题,只支持 require('prismjs'),同时在window属性下 添加了 Prismjs属性,大家可以自己试一下


    <script setup lang='ts'>
    import 'prismjs'
    import 'prismjs/themes/prism-okaidia.min.css'
    const Prism = (window as any).Prism

    const code = `var data = 1;`;
    const html = Prism.highlight(code, Prism.languages.javascript, 'javascript');
    </script>

    示例:




<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre class="language-html" v-html="html"></pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

import 'prismjs'
import 'prismjs/themes/prism-okaidia.min.css'
const Prism = (window as any).Prism

const html = computed(() => {
return Prism.highlight(SwitchDemo01.__sourceCode, Prism.languages.html, 'html')
})

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

最终效果如下图所示:


1698140953360.png


作者:Blink46
来源:juejin.cn/post/7293348981664399397
收起阅读 »

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

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

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


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


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


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


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


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


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


第一,认死理。


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


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


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


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


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


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


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


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


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


第三,不擅长合作。


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


他们会搞钱。


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


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


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


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



--- 


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


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


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


最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身体,活下来才有输出。


作者:风海铜锣
来源:juejin.cn/post/7238443713873199159
收起阅读 »

使用小程序中的 observe 实现数据监听

web
小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。 什么是 observe? observe 是小程序中的一个方法...
继续阅读 »

小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。


什么是 observe


observe 是小程序中的一个方法,用于监听数据的变化并触发相应的回调函数。它可以用于监听页面数据、组件数据以及其他数据对象。


如何使用 observe


监听页面数据


在页面的 .js 文件中,可以使用 Page 函数中的 data 中的 observe 字段来监听数据的变化。以下是一个示例:


// pages/index/index.js
Page({
data: {
count: 0,
},

// 监听 count 数据的变化
observe: {
'count': function (newVal, oldVal) {
console.log('count 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},

// 增加 count 值的函数
increaseCount() {
this.setData({
count: this.data.count + 1,
});
},
});

在上述示例中,我们在 observe 字段中定义了一个监听器,当 count 数据发生变化时,会触发相应的回调函数。


监听组件数据


在小程序的组件中,也可以使用 observe 来监听组件数据的变化。以下是一个示例:


// components/my-component/my-component.js
Component({
data: {
message: 'Hello, World!',
},

methods: {
changeMessage() {
this.setData({
message: 'New Message!',
});
},
},

// 监听 message 数据的变化
observe: {
'message': function (newVal, oldVal) {
console.log('message 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},
});

在组件的 observe 字段中同样定义了一个监听器,用于监听 message 数据的变化。


常见应用场景


1. 数据绑定


observe 可以用于在数据变化时自动更新视图,实现数据绑定。这对于构建响应式的页面和组件非常有用。


2. 数据校验


通过监听数据的变化,可以在数据变化时进行校验,确保数据的合法性。例如,监听输入框中的文本变化并验证其格式。


3. 事件通知


当某个数据变化时,可以通过 observe 触发相关的事件,通知其他部分的代码执行相应的操作。


4. 数据持久化


在某些情况下,需要将数据持久化到本地存储或服务器,可以在数据变化时触发数据保存操作。


注意事项和最佳实践


在使用 observe 进行数据监听时,有一些注意事项和最佳实践需要考虑:




  1. 数据引用类型的监听:当监听对象是引用类型(例如对象或数组)时,需要注意对象的引用是否发生变化。observe 监听的是对象的引用,而不是对象内部属性的变化。如果需要监听对象内部属性的变化,可以使用深度监听或手动触发。




  2. 避免过多监听器:不要过度使用 observe,因为过多的监听器可能会导致性能问题。只监听那些真正需要监控的数据。




  3. 监听器的性能开销:监听器的回调函数在数据变化时会被频繁调用,因此要确保回调函数的执行效率较高,以避免影响应用的性能。




  4. 避免循环引用:在监听器回调函数中不要再次修改被监听的数据,以防止循环引用和无限循环触发监听器。




  5. 生命周期管理:在页面或组件销毁时,要记得取消监听以防止内存泄漏。可以在 onUnload 生命周期中取消监听。




onUnload() {
this.setData({
observe: null, // 取消监听
});
}

通过谨慎使用 observe,你可以实现有效的数据监听和响应,提高小程序应用的可维护性和性能。


结语


observe 是小程序中非常有用的功能,它允许你监听数据的变化并执行相应的操作。无论是在页面中还是在组件中,都可以使用 observe 来实现数据的监听和响应。通过合理利用 observe,你可以构建更加动态和交互性的小程序应用。


作者:依旧_99
来源:juejin.cn/post/7295237661618438196
收起阅读 »

这个故事有点长 - 苏州

这个故事有点长 - 苏州 👉故事的开始 之前已经去过苏州了,只是当时觉得园林都一样,就只去了拙耕园、留园、山塘街、观前街、平江路、平门古城墙、金鸡湖,印象最深刻的就是,当初围绕着金鸡湖走了一半,累了打车回去了。 最近心情有点郁闷😠,决定去走走,思来想去,就发...
继续阅读 »

这个故事有点长 - 苏州



👉故事的开始


之前已经去过苏州了,只是当时觉得园林都一样,就只去了拙耕园留园山塘街观前街平江路平门古城墙金鸡湖,印象最深刻的就是,当初围绕着金鸡湖走了一半,累了打车回去了。


最近心情有点郁闷😠,决定去走走,思来想去,就发现苏州还有地方没去,于是乎就去了,提前一天定了酒店,住的花居酒店,挺不错的,停车位很多,免费停车,其他酒店都是40一天,由于姑苏区车辆限行,就在姑苏通上申请了,结果都回来了,申请才通过。


🚗第一天


由于没有那么远的距离,九点才从上海出发,大概十一点左右到的酒店,然后再楼下简单吃了顿饭,就驱车去了虎丘,在停车场还有人指导我倒车入库,然后就一直推荐旅游套餐,说虎丘+XXX好几个地方只需100元,车接车送,很划算,不能单买虎丘的,于是乎果断在美团上买了70的门票。







大概逛了三四个小时,期间卖了一块网红雪糕,宝塔形状的。五点左右就回去了,休息了一个小时,然后就打车去金鸡湖音乐喷泉,是个妹子开的车,然后在车上聊了起来,期间还为我表演了一次丝滑插车,下车时还让我给她了一个好评。七点半开始,我六点半左右就到了,结果还是没有抢到最佳观看位置,都太卷了,人很多,与外滩有一拼。




在等待期间,认真观察了苏州的“大裤衩”,期间总共变了三种颜色,期初是文字,后来变成了图案。





从金鸡湖回来, 坐地铁一号线去了观前街,第二次来了,故地重游,感觉就是不一样,又拍了两张照片😄,还是那么的热闹 。




🌅第二天


一觉睡到了十点,吃了早饭去狮子林,搜了一下,距离只有一公里多一些,果断又打车了,门票40元,中间蹭了导游的一些讲解,又看到了九狮台,能看的出来是九头狮子吗?



接下来就是假山群,里面有很多山洞,据说女儿国就在这里拍的,当初乾隆皇帝就在这里完了很久,直呼真有趣,于是就有了真趣亭




之后在附近吃了松鼠桂鱼,然后就回去了,一路都在与天气赛跑,一段下雨一段晴。




🚩起点既是终点


时间过得好快,兜兜转转了一圈,从哪来又回了哪去,晚安🌜🌛。



这个故事有点长 - 故事还在继续🏃🏃🏃.......



作者:小小愿望
来源:juejin.cn/post/7294130755840540735
收起阅读 »

用canvas画出一片星空

web
前言 由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理Canvas。Canvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。 Canvas 介绍 首先我们来介绍介...
继续阅读 »

前言


由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理CanvasCanvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。


Canvas 介绍


首先我们来介绍介绍CanvansCanvasHTML5提供的一个绘图API,它允许通过JavaScriptHTML元素创建和操作图形。Canvas提供的是一个矩形画布,我们可以在上面绘制各种图形、动画与交互效果。


功能


上面了解了Canvas是什么之后, 我们再来和大家展开说说Canvas 一些主要功能:



  1. 画布:Canvas提供了一个矩形的画布区域, 我们可以通过HTML中的<canvas>标签来创建并通过设置画布的宽高来确定绘图区域的大小。

  2. 绘画API:Canvas提供了丰富的绘图API,包括绘制路径、直线、曲线、矩形、圆形、文本等。 我们使用这些API来创建各种图形,并自定义样式、颜色、透明度等属性。

  3. 动画:Canvas可以与JavaScript的动画函数结合使用,实现动态的图形效果。通过在每一帧中更新画布上的内容,能创建平滑的动画效果。

  4. 图像处理:Canvas可以加载和绘制图像。我们可以使用Canvas的API对图像进行裁剪、缩放、旋转等操作,从而实现图像处理的功能。

  5. 事件处理:Canvas支持鼠标和触摸事件的处理,我们可以通过监听这些事件来实现交互效果,例如点击、拖拽、缩放等等。


注意:Canvas 绘制的内容是即时生成的,它不会被浏览器缓存,所以每次页面加载Canvas都需要重新绘制,所以我们在使用时需要考虑性能问题。


星空


在介绍完Canvas之后,我们再来用CanvasJS结合,实现一片星空的效果。


第一步,我们先创建好html 结构,代码如下:


  <div class="landscape">
</div>
<canvas id="canvas"></canvas>
<div class="filter"></div>
<script src="./round_item.js"></script>

现在是没有效果的,我们在给它加上css代码,将其美化,同时我们再在css上加一些动画效果,完整代码如下:


* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}

body {
background: linear-gradient(to bottom, #000 0%, #5788fe 100%);

}

.landscape {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(./star/xkbg.png);
background-repeat: repeat-x;
background-size: 1000px 250px;
background-position: center bottom;
}
.filter{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background: #fa7575;
animation: colorChange 30s ease-in infinite;
}
/* 渐变动画*/
@keyframes colorChange {
0%,100%{
opacity: 0;
}
50%{
opacity: 0.7;
}

}

这时候,我们看到的效果是这样的:


1698327282484.gif


这效果好像只有黄昏的颜色变化,少了星空。那这最后一步,就该我们Canvas上场了,我们要用Canvas画出来一片星空,并配合css的颜色变化,实现一个夜晚到清晨的感觉。


我们 js 代码这样写:


//创建星星的函数
function RoundItem(index, x, y, ctx) {
this.index = index
this.x = x
this.y = y
this.ctx = ctx
this.r = Math.random() * 2 + 1
this.color = 'rgba(255,255,255,1)'
}
// 绘制
RoundItem.prototype.draw = function () {
this.ctx.fillStyle = this.color //指定颜色
this.ctx.beginPath() // 开始绘制
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false) // 绘制圆形
this.ctx.closePath() // 结束绘制
this.ctx.fill() //填充形状
}
//移动
RoundItem.prototype.move = function () {
this.y -= 0.5
this.draw()
}


let canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
round = [],
initRoundPopulation = 200; //星星的个数

const WIDTH = document.documentElement.clientWidth,
HEIGHT = document.documentElement.clientHeight;

canvas.width = WIDTH
canvas.height = HEIGHT

init()
// setInterval(animate, 1700)
animate()
function init() {
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new RoundItem(i, Math.random() * WIDTH, Math.random() * HEIGHT, ctx)
round[i].draw()
}
}
function animate() {
ctx.clearRect(0, 0, WIDTH, HEIGHT) //清除画布
for (let star of round) {
star.move()
}
requestAnimationFrame(animate) //通过刷新帧数来调用函数
}


将上述代码添加上之后,我们也就完成星星的绘制并添加到了页面上,最终的效果如下:
1698327645625.gif


到这里,我们就用canvas 画出了一片美丽的星空。同时我们也看到了canvas的强大功能,感兴趣的小伙伴可以深入的了解它更多的用法哦。


作者:潘小七
来源:juejin.cn/post/7294103091019612212
收起阅读 »

怎么办,代码发布完出问题了

作者:蒋静 前言 回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先...
继续阅读 »

作者:蒋静



前言


回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先解决线上问题。


构建部署之“痛”


我的几段公司的工作经历:



  1. 第一段经历,是在一个传统的公司,没有运维,要我们自己登录一个跳板机,把文件部署到服务器,非常麻烦。

  2. 第二段经历,是在一个初创公司,基建几乎没有,前端的规模也很小,发布就是打个 zip 包发给运维,运维去上线。但是久而久之,运维也就不耐烦了。

  3. 后来去了稍微大些的公司,构建、部署有一套比较完善的体系,在网页上点击按钮就可以了。


那么构建部署是如何实现的呢?下面我要来介绍古茗的部署和回滚代码机制。


发布分析


我们的最终目的是发布上线,我们发布的是什么呢?是一条分支,所以我们需要先创建一条分支(更加规范的步骤应该是:基于某个需求和某个应用去拉一条分支)。在分支上开发完我们就可以进行发布的操作啦!


这个时候我们就可以操作发布,我们填写需要的配置项后就可以点击发布按钮了。但是肯定不能让所有人随随便便就发布成功,所以我们要进行一些前置校验。比如说你有没有发布的权限、代码有没有冲突、是不是节假日或非发布窗口期、这个应用有没有正在被发布。。。等等的校验,总之就是确保代码是可以被你发布的。


然后我们的发布平台就会叫 Jenkins 拿着仓库信息、分支信息,以及其他等等的配置信息去仓库拉取代码了,拉到代码之后根据不同类型的应用进行区分,进行编译打包(这个过程不同应用之间是不同的),生成对应的产物。


1. 容器化发布


image.png



注:图中Wukong是我们自研DevOps平台



容器化发布发布的是镜像,镜像 id 代表了这次发布和这个镜像的关联关系。回滚的时候只需要找到这次发布对应的 id ,运维脚本根据这个 docker 的 id 找到 docker 镜像,直接部署这次 docker 镜像,做到回滚。由于发布的是 docker 的镜像,不仅可以保证产物是相同的,发布还很快。


容器化之前的发布:先找到对应的发布,根据这次发布找到对应的 tag,然后打包发布,但是这样只能保证业务代码是相同的,不能保证机器环境、打包机的环境、依赖的版本、打包的产物等等是一样的,并且需要的时间比容器化的方式慢得多。


2. oss发布


image.png


oss 发布和容器化发布流程的区别在于不用打包镜像而是将js、css等资源传到了 oss。通过 oss 发布的应用,只需要记住版本和 oss 上面资源路径的对应关系就可以了。


例如在我们这里的实现是:每次发布完成之后会记下有 hash 的 manifest 的地址,点击回滚后会根据发布 id 找到当次的产物,通过 oss 将 manifest 内容替换为有hash 的,从而就切换了访问的资源(html 的 manifest 地址不变,改变的是 manifest 文件的内容)。


3. 小程序


image.png
钉钉小程序的回滚就比较简单了,一般在我们点击回滚之后,内部会通过 http 接口调用小程序的 api 传递需要回滚的版本好后即回滚完成。或者你也可以选择手动到开发者后台的历史版本点回滚。
例如: open.dingtalk.com/document/or…


未来展望


有了完善的部署回滚机制,我们的产研团队才能有更好的交付体验。工作中的业务价值在我们整个交付内容占比应当是比较高的,而不应当把大量的时间花费在处理部署等流程上,让我们能够更快的去完成业务交付。


更好更稳定的回滚方式,能够让我们做到出现问题时快速恢复。这样才能保证一个较低的试错成本。


对于古茗来说,我认为一个很大的优势是,我们的规模不算很大,可以更好地做好研发流程对应的工具服务的统一,打通研发流程的各个流程,每个环节之间更好地进行串联,更好的助力业务发展。


作者:古茗前端团队
来源:juejin.cn/post/7295160228878106650
收起阅读 »

2D的雪碧图已经够炫了,那么3D的呢?

web
前言 前2篇文章,分别介绍了dat.gui和纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷! 这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/...
继续阅读 »

output-16_6_11.gif


前言


前2篇文章,分别介绍了dat.gui纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷!


这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/jsm/libs/tween.module.js,使用起来也是比较简单。


初始化


老样子,场景、相机、渲染器三要素初始化,并导入需要的插件库,插件库都已在three中内置:


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;

// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);

// 场景
scene = new THREE.Scene();

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
}

// 渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera)
}

CSS3DSprite创建521个水球


// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
})
image.src = './static/img/sprite.png';


上面的代码中,我们创建了img标签,并使用CSS3DSpriteHTML元素转化为threejs的CSS3精灵模型,类似与转换成了three中的网格,并随机分布在-2000,2000的立方体中。


看下效果:


three04-1.jpg


添加控制器


关于控制器,前面也已经介绍过啦,通过控制器,我们就可以改变相机的位置,观察不同角度的物体。


// 定义控制器
let controls;
controls = new TrackballControls( camera, renderer.domElement );

// 渲染
function animate() {
...
controls.update();
...
}

注意哦,controls.update需要防止在animate中,每帧执行。


有了控制器。我们就可以实现交互啦:


output-15_37_12.gif


让小球按规律放大缩小


让小球按照正弦时间,放大缩小,即有一种闪烁的效果:


const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {
const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);
}

output-15_37_59.gif


让小球生成特定图形


生成矩形,对应的每个小球坐标:


const amount = 8;
const separationCube = 150;
const offset = ( ( amount - 1 ) * separationCube ) / 2;

for ( let i = 0; i < particlesTotal; i ++ ) {

const x = ( i % amount ) * separationCube;
const y = Math.floor( ( i / amount ) % amount ) * separationCube;
const z = Math.floor( i / ( amount * amount ) ) * separationCube;

positions.push( x - offset, y - offset, z - offset );

}

tween.js使用


const position = {x: 0,y: 0};
;//创建一段tween动画
const tween = new TWEEN.Tween(position)
//经过2秒,position对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start();
// 动画效果 类似annimation
tween.easing()
// 完成时执行的钩子,里面可以继续执行下一个操作
tween.onComplete()

使杂乱的小球变成矩形


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;

for (let i = 0; i < particlesTotal; i++) {

const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;

positions.push(x - offset, y - offset, z - offset);

}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

controls = new TrackballControls(camera, renderer.domElement);

}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

杂乱的小球变成多种形态完整代码


限制文件大小啦,没法完全展示,大家自行运行看看吧!


output-15_49_5.gif


<div id="container"></div>
<script type="module">
import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// Plane
const amountX = 16;
const amountZ = 32;
const separationPlane = 150;
const offsetX = ((amountX - 1) * separationPlane) / 2;
const offsetZ = ((amountZ - 1) * separationPlane) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amountX) * separationPlane;
const z = Math.floor(i / amountX) * separationPlane;
const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200;
positions.push(x - offsetX, y, z - offsetZ);
}

// Cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;
positions.push(x - offset, y - offset, z - offset);
}

// Random
for (let i = 0; i < particlesTotal; i++) {
positions.push(
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000
);
}

// Sphere
const radius = 750;
for (let i = 0; i < particlesTotal; i++) {
const phi = Math.acos(- 1 + (2 * i) / particlesTotal);
const theta = Math.sqrt(particlesTotal * Math.PI) * phi;
positions.push(
radius * Math.cos(theta) * Math.sin(phi),
radius * Math.sin(theta) * Math.sin(phi),
radius * Math.cos(phi)
);
}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
controls = new TrackballControls(camera, renderer.domElement);
}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

</script>

作者:八月十八
来源:juejin.cn/post/7294301361835147290
收起阅读 »

你眼中的失业潮是怎样的?

逛 V 站刷到一个北漂的码农,他的生存指南直接让我破防,但也不得不佩服他的生存能力和心态。 先介绍下这位老哥的情况,普通本科非科班,9 年 IOS 开发经验,其中 1 年 RN,一年 flutter。北漂,名下无车,老婆和孩子都在北京,目前孩子在读幼儿园。 上...
继续阅读 »

图片


逛 V 站刷到一个北漂的码农,他的生存指南直接让我破防,但也不得不佩服他的生存能力和心态。


先介绍下这位老哥的情况,普通本科非科班,9 年 IOS 开发经验,其中 1 年 RN,一年 flutter。北漂,名下无车,老婆和孩子都在北京,目前孩子在读幼儿园。


上半年已经被优化裁员了,更新简历重新找工作,整体情况就是一周打招呼几十次,只成功投了 3 份简历,0 次面试。


实在不敢想象几个月没工作心态会崩成啥样。虽然没有做好心理准备,但也没有别的办法了。


社保代缴违法了也断缴了,每个月剩下 3k 左右,够孩子上两个月的幼儿园。


失业一段时间之后,注册了滴滴代驾,等待审核通过之后打算买一个折叠自行车。


发现北京的滴滴代驾已经没有名额了,只能等缺人的时候才能继续走下一步操作。


图片


然后注册了美团众包,新手第一天还得培训,培训完才能接单。


之后便开始做起了外卖员,第一次跑从早上 5 点到 7 点 50 分,跑了 6 单 ,15km 的路程,一共赚了 44 元,如果扣除保险的就是 41 元,时薪则是 16.4 元。


由于刚开始,并不累,只是觉得有点意思,目前心态还算好。有一个好处就是,不会觉得挣钱很容易了,坐办公室一天就是几百上千。


之所以会优先选择代驾和外卖的原因,是因为今年的行情的确很糟糕,找了几个月的工作还没找到,感觉这次的找工作周期并不短。


另一个原因是时间比较自由,不会影响我学习和面试。开发这行我也比较热爱,不愿意丢弃这份工作。


图片


并不是说找工作期间完全找不到工作。而是找的工作降薪太多,从企业的角度来说不稳定的因素也有。自己也不甘心,一旦降下去很难说下次按上次的高薪去涨。


由于北京代驾排队人数较多,暂时没有开放名额,所以入职了美团和小鸟。


从 5 月份 12 号开始跑,一开始基本上早上 5 点到 7 点 20 这段时间,然后回家接孩子上幼儿园。


只跑早上,是因为白天还得学习,后来发现没一个面试,索性白天时间段也开始跑了。


5 月份跑单统计下来,美团 275 单,一共 1900 元,扣了 180 元,有一次上午的单子已经餐损了,结果下午又流转到我这新手的头上了,由于没有经验不会操作导致餐损和违规算到我头上了,反馈给客服也不顶用。


蜂鸟 44 单,一共 280 元,扣除 20 元的保险和超时扣费。


每小时基本 20 元左右,叠加活动奖励差不多能到 26.7 元一小时,基本上 1元/km。这个 km 不止是单纯商家到顾客的距离,还要加上骑手接单时的位置到商家的距离。


所用的工具就是一辆最高 25km/h 的国标电动车。所以同时最多挂3单,那些车速快的我看到能接 9 单。没解速是因为觉得外卖只是暂时的,解速以后带孩子骑行就不安全了。


跑外卖整体的收获就是,觉得挣钱不容易,远不如之前坐办公室一天就好几百。甚至觉得跑外卖的时候还期盼下雨,因为会有天气补贴,1 单有 0.5-1 元的补贴,自己想想都觉得心酸。


但也没有那么焦虑了,虽然不够房租和日常开销,但好歹能够也算有收入。其实内心也是有点焦虑,不过在家里就是不能表现得太明显。


我比较喜欢开车,所以还是会去跑代驾。由于滴滴排队太慢了,到现在还在排队中,就先加入了别的代驾。


做代驾前,我做了一些功课。在抖音上搜各种档式,包括鸡腿挡、怀挡、旋钮挡等。还有各种车的启动方式,比如宝马、奔驰、路虎、特斯拉等。


心理建设还是有的,毕竟是开别人的车,怕刮蹭出事故。自己第一次上线接单前足足鼓励了半小时。


我没有买电动车和自行车,靠地铁和共享单车。因此接单我都跑市里,从 2 环到 4 环,跑偏僻地方的概率很小,不至于给我干到廊坊回不来。


刚开始的时候  14 点就出门,但是基本上 19 点开始才可能有单,再加上后来回家晚,第二天可能要补觉,所以基本上就是晚上 20 点到 凌晨。


图片


且家里住的比较远,基本上到家都在凌晨1点半以上,最晚曾到凌晨4点。


整体收入,没接过 200 元以上得大单,所以时薪在 0-120 元左右。由于单量不稳定,有时候要等个把小时才来单,现在每天差不多几十到 100元 不等。


有一次接了一个 300+ 的大单,预计开车 1 小时多点,但是被取消了,有点心痛!


与外卖相比大概率好点,除非代驾没单或者外卖有活动。


近两个月找工作情况,一共3个面试,其中2 个内推,1 个网投。一个非大厂,终面挂,2 个大厂,分别一面挂和二面挂。


目前已经降薪 30% 在找了,但是还没有找到,笑哭。


同时也在继续开发自己的 APP,由于苹果账号到期了所以现在还没搜到。目前正在做得内容有 商品码识别、小组件和内购,希望掌握技能的同时增加收入。


看到这里,我默默的问一句,大家眼中的失业潮是怎样的?


看了不少的消息,基本上有一个共同的意识就是,环境不太好,都在挣扎做一些副业之类挣额外的收入来维持。


另外代驾和外卖数量逐渐开始到达红线的消息传来,也不是空穴来潮。


我所在的开发团队,也都有一个共同意识,就是做好留一手的准备,该考驾-照的考驾-照,该练体力的练体力,该做其他准备的做其他准备。


公司个月都在汇报信息化建设程度,我们疯狂给他们开发系统,开发工具,省人省力省成本。


做到了一定程度竟然听到在清算人力,听到之后不禁打了个寒颤,至少我也是帮凶了吧。


文中主人公 V 站地址👉:http://www.v2ex.com/t/956789


作者:桑小榆呀
来源:juejin.cn/post/7293804880707059750
收起阅读 »

重复请求优化

web
设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:...
继续阅读 »

设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:两个组件必须有一个共同的祖先组件,如果这两个组件是同级的兄弟组件倒也还好,如果非同级,那么数据的传参就会有些麻烦了。那么还有其他办法吗?当然是有的。


我们可以换个思路,每个组件还是保持原有的业务逻辑不变,从请求接口处做文章。既然是同一个接口调用了两次,而且还是返回了相同的请求结果,那么不妨在第一次时调用成功时,就把请求结果缓存起来,等到第二次再调用时,直接返回缓存的数据。按照这个思路可以写出第一版的代码(这里用了 TS 方便查看参数的类型):


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* @param request 请求函数
*/

function cacheRequest<T>(request: (...args: any[]) => Promise<T>) {
const cache = Symbol("cache")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}


  • 首先 cacheRequest 函数,需要接收一个参数 requestrequest 是一个返回结果为 Promise 对象的函数。cacheRequest 执行完后返回一个新的匿名函数。

  • 然后,在匿名函数的内部,先判断 request 的原型对象上是否有 cache(这里的 cache 使用了 Symbol 类型,确保键名唯一)。也即,是否有缓存过的请求结果,如果没有,说明是第一次调用,则将 request 的执行结果存到缓存里。如果有缓存,则直接返回缓存。

  • 可以看到,缓存也是一个 Promise 类型。在同时调用多次请求时,只要在第一次调用执行后,已经把 Promise 存到缓存里了,后续的请求返回的也是缓存里的 Promise,从而保证多个请求都指向同一个 Promise ,也即只会调用一次 API 接口。

  • 这里需要注意一点,由于需要往 request 的原型对象上挂载缓存,所以 request 不能是箭头函数。因为箭头函数没有 this,也就意味着没有原型对象。


小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
return function (...args) {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
newRequest()

version1.png


可以看到虽然 newRequest 调用了三次,但是 fetch request 只打印了一次,也就是说 request 只调用了一次,符合预期!但是,最后一次 newRequest 的调用,是在 3 秒后调用的,也是走的缓存,没有重新执行。仔细思考一下,后续无论什么时候调用 newRequest 都会使用缓存里的数据,不会重新调用请求了,这显然是不合理的。我们还需要加个缓存的过期时间,超过这个时间,就重新发起新的请求。第二版如下:


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* 保证在 cacheTime 时间间隔内的多次请求,只会调用一次
* @param request 请求函数
* @param cacheTime 最大缓存时间(单位毫秒)
*/

export function cacheRequest<T>(request: (...args: any[]) => Promise<T>, cacheTime = 1000) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}


  • 首先,cacheRequest 新增一个入参 cacheTime,用于设置过期时间,默认为 1 秒。

  • 其次,在原型对象上新增了一个 lastTime 属性,用来记录最后一次调用的时间。

  • 当缓存为空,或者当前时间距离上一次调用时间超过缓存过期时间时,更新 cachelastTime


再来小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args) {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
setTimeout(newRequest, 3000)

version2.png


这一次,fetch request 打印了两次,符合预期,完美!


作者:showlotus
来源:juejin.cn/post/7294597695478333476
收起阅读 »

接手了一个外包开发的项目,我感觉我的头快要裂开了~

嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,...
继续阅读 »

嗨,大家好,我是飘渺。


最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,在这段时间里,我受到了来自"外包"和"微服务"这双重debuff的折磨。


image-20231016162237399


今天,我想和大家分享一下我在这几天中遇到的问题。希望这几个问题能引起大家的共鸣,以便在未来的微服务开发中避免再次陷入相似的困境。


1、服务模块拆分不合理


绝大部分网上的微服务开源框架都是基于后台管理进行模块拆分的。然而在实际业务开发中,应该以领域建模为基础来划分子服务。


目前的服务拆分方式往往是按照团队或功能来拆分,这种不合理的拆分方式导致了服务调用的混乱,同时增加了分布式事务的风险。


2、微服务拆分后数据库并没拆分


所有服务都共用同一个数据库,这在物理层面无法对数据进行隔离,也导致一些团队为了赶进度,直接读取其他服务的数据表。


这里不禁要问:如果不拆分数据库,那拆分微服务还有何意义?


3、功能复制,不是双倍快乐


在项目中存在一个基础设施模块,其中包括文件上传、数据字典、日志等基础功能。然而,文件上传功能居然在其他模块中重复实现了一遍。就像这样:


image-20231017185809403


4、到处都是无用组件堆彻


在项目的基础模块中,自定义了许多公共的Starter,并且这些组件在各个微服务中被全都引入。比如第三方登录组件、微信支付组件、不明所以的流程引擎组件、验证码组件等等……


image.png


拜托,我们已经有自己的SSO登录,不需要微信支付,还有自己的流程引擎。那些根本用不到的东西,干嘛要引入呢?


5、明显的错误没人解决


这个问题是由上面的问题所导致的,由于引入了一个根本不需要的消息中间件,项目运行时不断出现如下所示的连接异常。


image-20231013223714103


项目开发了这么久,出错了这么久,居然没有一个人去解决,真的让人不得不佩服他们的忍受力。


6、配置文件一团乱麻


你看到服务中这一堆配置文件,是不是心里咯噔了一下?


image-20231017190214587


或许有人会说:"没什么问题呀,按照不同环境划分不同的配置文件”。可是在微服务架构下,已经有了配置中心,为什么还要这么做呢?这不是画蛇添足吗?


7、乱用配置中心


项目一开始就明确要使用Apollo配置中心,一个微服务对应一个appid,appid一般与application.name一致。


但实际上,多个服务却使用了相同的appid,多个服务的配置文件还塞在了同一个appid下。


更让人费解的是,有些微服务又不使用配置中心。


8、Nacos注册中心混乱


由于项目有众多参与的团队,为了联调代码,开发人员在启动服务时不得不修改配置文件中Nacos的spring.cloud.nacos.discovery.group属性,同时需要启动所有相关服务。


这导致了两个问题:一是某个用户提交了自己的配置文件,导致其他人的服务注册到了别的group,影响他人的联调;二是Nacos注册中心会存在一大堆不同的Gr0up,查找服务变得相当麻烦。


其实要解决这个问题只需要重写一下网关的负载均衡策略,让流量调度到指定的服务即可。据我所知,他们使用的开源框架应该支持这个功能,只是他们不知道怎么使用。


9、接口协议混乱


使用的开源脚手架支持Dubbo协议和OpenFeign调用,然而在我们的项目中并不会使用Dubbo协议,微服务之间只使用OpenFeign进行调用。然而,在对外提供接口时,却暴露了一堆支持Dubbo协议的接口。


10、部署方式混乱


项目部署到Kubernetes云环境,一般来说,服务部署到云上的内部服务应该使用ClusterIP的方式进行部署,只有网关服务需要对外访问,网关可以通过NodePort或Ingress进行访问。


这样做可以避免其他人或服务绕过网关直接访问后端微服务。


然而,他们的部署方式是所有服务都开启了NodePort访问,然后在云主机上还要部署一套Nginx来反向代理网关服务的NodePort端口。


image-20231016162150035


结语


网络上涌现着众多微服务开源脚手架,它们吸引用户的方式是将各种功能一股脑地集成进去。然而,它们往往只是告诉你“如何集成”却忽略了“为什么要集成”。


尽管这些开源项目能够在学习微服务方面事半功倍,但在实际微服务项目中,我们不能盲目照搬,而应该根据项目的实际情况来有选择地裁剪或扩展功能。这样,我们才能更好地应对项目的需求,避免陷入不必要的复杂性,从而更加成功地实施微服务架构。


最后,这个开源项目你们认识吗?


image-20231017190633190



关注公众号 Java日知录 获取更多精彩文章



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

某月薪5万的朋友关于处理BUG的心得

引言 大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。 今天我想和大家分享一个有趣的故事。这个故事的主角是我的一个朋友,他是一位资深的软件工程师,月薪高达5万。你可能会觉得这个收入水平的人应该过着无忧无虑的生活,但是他告诉我,他的工作压力非常大,尤其是...
继续阅读 »

引言


大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。


今天我想和大家分享一个有趣的故事。这个故事的主角是我的一个朋友,他是一位资深的软件工程师,月薪高达5万。你可能会觉得这个收入水平的人应该过着无忧无虑的生活,但是他告诉我,他的工作压力非常大,尤其是在处理BUG的时候。


BUG是什么


手下留情,我不是你们要找的BUG


BUG,这个词在软件开发中是非常常见的。它是计算机程序中的一个错误或问题,可能会导致程序无法正常运行。对于软件工程师来说,找到并修复这些BUG是一项非常重要的任务


心得


高手支招


这位朋友告诉我,他在处理BUG的过程中,有以下几点心得





  1. 耐心:找到并修复BUG需要非常强的耐心。有时候,你可能需要花费几个小时,甚至几天的时间来追踪一个BUG的来源。这需要你有足够的耐心去面对这个问题。




  2. 细心:在处理BUG的过程中,你需要非常细心。因为BUG可能隐藏在程序的任何一个地方,只有通过仔细的检查,才能找到它。




  3. 创新:有时候,传统的解决方法可能无法解决某个BUG。这时候,你需要有创新的思维,去寻找新的解决方案。




  4. 团队协作:处理BUG并不是一个人的事情,而是需要整个团队的协作。每个人都有自己的专长,只有通过团队的协作,才能更有效地解决问题。




  5. 不断学习:技术是不断发展的,新的编程语言、新的开发工具、新的开发方法会不断出现。作为软件工程师,你需要不断学习,才能跟上技术的发展。





我这位朋友的心得让我深受启发。他告诉我,虽然处理BUG的过程可能会很艰难,但是当你成功解决一个问题的时候,那种成就感是无法用言语表达的


我的看法


patience is key in life


首先,让我们来谈谈耐心。在游戏开发中,找出BUG的来源是一项非常复杂的任务。你可能需要查看大量的代码,甚至需要反复测试多次,才能找到问题的根源。这个过程可能会非常耗时,但是没有耐心的话,你可能会错过一些重要的线索。因此,耐心是处理BUG的第一步


细心


其次细心也是非常重要的。在编写代码的过程中,程序员可能会犯一些错误,这些错误可能会导致程序无法正常运行。这些错误可能是语法错误,也可能是逻辑错误。只有通过仔细的检查,才能找到这些错误。因此,细心是处理BUG的第二步


创新


接下来,我们来谈谈创新。在游戏开发中,传统的解决方法可能无法解决某些复杂的问题。这时候,你需要有创新的思维,去寻找新的解决方案。例如,你可以尝试使用新的编程语言或者新的开发工具来解决问题。因此,创新是处理BUG的第三步


团队协作


然后,我们来谈谈团队协作。在游戏开发中,处理BUG并不是一个人的事情,而是需要整个团队的协作。每个人都有自己的专长,只有通过团队的协作,才能更有效地解决问题。例如,设计师可以帮助程序员理解用户的需求,测试人员可以帮助程序员发现程序的问题。因此,团队协作是处理BUG的第四步


不断学习


最后,我们来谈谈不断学习。技术是不断发展的,新的编程语言、新的开发工具、新的开发方法会不断出现。作为游戏开发者,你需要不断学习,才能跟上技术的发展。只有这样,你才能更好地处理BUG。因此,不断学习是处理BUG的最后一步


结语


总的来说,处理BUG是一项需要耐心、细心、创新、团队协作和不断学习的任务。虽然这个过程可能会很艰难,但是当你成功解决一个问题的时候,那种成就感是无法用言语表达的。


希望这些心得能对你们有所帮助


在哪里可以看到如此清晰的思路,快跟上我的节奏!关注我,和我一起了解游戏行业最新动态,学习游戏开发技巧。


我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。


AD:笔者线上的小游戏《重力迷宫球》《贪吃蛇掌机经典》《填色之旅》大家可以自行点击搜索体验。


作者:亿元程序员
来源:juejin.cn/post/7294563074936356875
收起阅读 »

废掉一个程序员的十大铁律,你中招了吗?

写在前面 废掉一个人,其实还是挺简单的。最简单的两种方式就是:让他忙,一直忙;让他闲,一直闲。 一个人开始废掉的标志 终日懒散,无所事事;没有目标,没有规划;不想上班,只想打游戏;除了这些,可能下一个将要废掉的人,就是看似每天都很忙碌的你! 冰河在多年的从业...
继续阅读 »

写在前面


废掉一个人,其实还是挺简单的。最简单的两种方式就是:让他忙,一直忙;让他闲,一直闲。


一个人开始废掉的标志


终日懒散,无所事事;没有目标,没有规划;不想上班,只想打游戏;除了这些,可能下一个将要废掉的人,就是看似每天都很忙碌的你!


图片


冰河在多年的从业生涯中,总结出10条序员如何让自己废掉的铁律。大家对号入座。


如何让自己更快的废掉?


图片


忙得要死


有人说,废掉一个程序员最隐蔽的方式,就是让他忙到没有时间成长。每天定的计划特别满,正常工作时间完不成的,必须留下来加班。程序员每天都要很努力的工作,不停的coding,写业务代码。每天都非常忙碌,甚至周末都不能休息。有些公司“996”还不行,还要弄个“007”出来,程序员完全无法留出时间自我总结和成长。


冰河见过太多这样的人了,工作几年了,只会写CRUD。为哈?因为他太忙了,忙到没有时间提升自己。很多工作三年的程序员就只会CRUD。跳槽面试时,一旦被问到性能调优、高并发、Dubbo、SpringCloud等底层的原理和技术时,基本就歇菜了。


闲得要命


有人说,废掉一个程序员最快的方式,是让他闲着,让他没有方向感,不知道干啥。这类程序员也不会规划自己的时间,只是被动的接受别人的安排。时间久了,忘记了自己是干啥的,成功的废掉了自己。


拖延症


这类程序员就是自己把自己拖死的。工作任务不重,但是每天会加班到很晚,开始觉得很勤奋,但是细想,就这点工作不至于加班这么久吧?一问才知道,这是拖延症的表现。有些程序员做事情喜欢拖延,工作前要先聊会QQ,聊会微信,看看微博,刷刷朋友圈。总觉得时间还早,晚点再干吧。久而久之,养成了难以戒掉的拖延症。


吃老本


不及时更新自己的技术栈,拒绝接受新技术。总觉得自己的技术很牛X,殊不知,你的技术已经赶不上时代的潮流了,刚毕业的小学生掌握的技术都比你先进。


不会规划


对自己的职业生涯没有规划,未来朝着哪些方向努力,完全不知道。当一天和尚,撞一天钟。一旦公司或者企业出现变故,这类程序员基本没戏。


没有目标


这点与不会规划有些雷同,但是更加强调的是目标感。没有目标感,无法更好的驱动自己去完成相应的事项,无法让自己更聚焦到优先级高的事项中,即使再忙,也是眉毛胡子一把抓,最后累苦了自己,还拖累了别人。


自我感觉良好


这类程序员就是自我感觉啥都会,将自己封闭在自身的世界中,不愿意与他人沟通和交流,觉得自己都是对的,就这么干!美其名曰:自我良好!实际上,啥也不是。久而久之,不废才怪!


专注度不够


做事情注意力不集中,写两行代码,看看手机、刷刷微博和朋友圈,无法让自己专注的完成手上的任务。时间久了,养成了三心二意的习惯。


知识面欠缺


不愿拓宽自己的技术栈,发自内心愿意当一颗小小的螺丝钉,不愿意尝试新技术和新业务,守着自己的一亩三分地,反反复复就那些CRUD操作。


啥都想学


这类程序员本质上就是没有一个方向,对自己的职业生涯没有规划,啥都想学,一会想学Java,一会想学Python,一会又想学Go,最后又想学大数据。到头来,却是啥也没学会。


以上这些,希望小伙伴们有则改之,无则加勉~~


最后,除上面10条铁律外,还有一点就是 改需求 也能让程序员死得快些!


好了,今天就到这儿吧,小伙伴们,你们知道哪些能够让程序员更快废掉的方式呢?请在文末留言,说出你的观点。我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7294853623849435170
收起阅读 »

【Java集合】了解集合的框架体系结构及常用实现类,从入门到精通!

嗨~ 今天的你过得还好吗?以不同的方式长大谁都没有轻轻松松🌞- 2023.10.27 -通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构...
继续阅读 »

640 (13).jpg

嗨~ 今天的你过得还好吗?

以不同的方式长大

谁都没有轻轻松松

🌞

- 2023.10.27 -

通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构

从集合框架开始,也就是进入了java这些基础知识及面向对象思想进入实际应用编码的过程,通过jdk中集合这部分代码的阅读学习,就能发现这一点。


5bbd4f4683e25cfda6c3946e9925c48f.gif


本计划在这篇中把框架体系和一些集合的常用方法一起编写。仔细考虑之后,本着突出重点,结构清晰的思路,所以把框架体系单独拉出来,为让各位看官对java的集合框架有个清晰的认识,最起码记住常用的几种常用实现类!


好的,下面我们进入正题。


集合的框架体系结构

可以在很多JAVAEE进阶知识的学习书籍或者教程中看到,JDK中提供了满足各种需求的API,主要是让我们去学习和了解它提供的各种API。

54f18513580ae2f9b13d1b648e6c2380.jpeg

在使用这些API之前,我们往往需要先了解其继承与接口架构,才能了解何时采用哪个实现类,以及类之间如何彼此合作,从而达到灵活应用。


查看api文档,集合按照存储结构可以分为两大类,

  • 单列集合 java.util.Collection

  • 双列集合 java.util.Map


通过jdk api 来看在 JDK中 提供了丰富的集合类库,为了便于初学者进行系统地学习,我们通过结构图来分别描述集合类的继承体系。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


640 (27).gif


Collection

Description

Collection: 单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口:

  • java.util.List List的特点是 元素有序,元素可重复。

  • java.util.Set Set的特点是 元素无序(不全是),而且不可重复


List 接口主要的实现类有 java.util.ArrayList 和 java.util.LinkedList,Set 接口的主要实现类有 java.util.HashSet 和 java.util.TreeSet。



640 (27).gif

Map

Description

Map: 双列集合,用于存储具有映射关系的对象。常用的实现类有

  • java.util.HashMap

  • java.util.LinkedHashMap

注:图片中 小标中有 I的都是接口类型,而 C 的都是具体的实现类。


好的,框架的介绍就到这里了。本文中主要介绍了框架的两大类,以及我们在开发工作中使用的几种常见的接口和实现类,在后面的文章中,一一介绍吧。



收起阅读 »

宽度 or 深度?

😰服了,随便起的标题也太擦边了🙈(肯定是我的工作伙伴带坏了我~) 在阿里和京东,会把人才分为“L 型人才”和“T型人才”,其实这个很容易理解,L型 —— 先保证技术深度再保证技术宽度;T型 —— 先保证技术宽度再保证技术深度 。恰好本人对于这个问题也没有明确...
继续阅读 »

😰服了,随便起的标题也太擦边了🙈(肯定是我的工作伙伴带坏了我~)



在阿里和京东,会把人才分为“L 型人才”和“T型人才”,其实这个很容易理解,L型 —— 先保证技术深度再保证技术宽度;T型 —— 先保证技术宽度再保证技术深度 。恰好本人对于这个问题也没有明确的答案,所以在这里想跟大家讨论一下:对于一个职业生涯初期的程序猿来说,在职业生涯初期应该以技术宽度为重还是以技术深度为重?


前辈的看法


先po一下几位前辈的观点。


一位掘金朋友的意见



单向发展局限性比较大,不管是做技术管理还是技术专家,都需要一定的广度,但是这个广度我觉得是建立在已经有深度的基础上,单纯为了扩展广度而去学很多基础的东西,意义不大,毕竟不是为了做外包,所以还是看你处于哪个阶段的瓶颈期吧,如果是1-3年,可能花时间搞清楚原理性的东西带来价值更大,而如果是3-5年,应该是要培养考虑问题的思维能力上吧,这是我的个人看法~



一位字节大佬的意见:



突然发现,前端好像没几个做到 CTO 的……仔细想想,国内的前端界比较出名的前端出身做到很高职位的,玉伯算是一个代表,后期他基本上已经成为一个产品设计方面的负责人了,脱离了单纯前端的范畴。而普遍对于前端天花板的看法都差不多,确实是认为有后端工程背景的人升为 VP/CTO 级别的概率比较高,而前端更倾向于在框架中日复一日的迷失。而整体来说,后端天花板的高度普遍远远高于前端天花板的高度。



职业背景


本人是处在一个制造业公司的前端开发(也是唯一一个前端),公司主要技术栈为sql,公司主要通过sql解决公司业务问题,公司有雇佣外包公司(可以理解为公司大部分业务通过外包实现,公司没有固定的IT产品,也理所当然的没有技术支持,每个人能实现日常业务即可,因此可能对于个人的技术深度发展帮助有限?),其实公司对于职业定位真的区分不大,推崇的理念是你能干活即可,因此对于业务没有特定的划分,因此虽然我是前端,但是我也能碰后端(java)、sql、运维(部署)的活。


我的看法


刚想搜一下资讯,结果就看到了这个沸点。



😅在一个内卷如此严重、形势如此悲惨的状况下,还真的有必要去思考选择走哪条路吗?


我的看法是,其实无论是选择技术宽度还是选择技术深度,如果真的不知道选择什么,与其迷茫停止前进不如跟着环境走?我相信只要持续学习,学到的终归是自己的,到最后肯定是会有所收获的💪。至于怎么选择,好像得以后考虑了...


👀最后


其实这个问题我真的没有答案,如果有大佬有其他看法,希望能为我答疑解惑吧😭


作者:码外生活
来源:juejin.cn/post/7287020579831988258
收起阅读 »

俞敏洪:我曾走在崩溃的边缘

web
大家在人生的经历中遇到过很崩溃的事情吗? 我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。 长时间下来,这个习惯让我对名人的苦难经历...
继续阅读 »

大家在人生的经历中遇到过很崩溃的事情吗?


我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。


长时间下来,这个习惯让我对名人的苦难经历或者处理棘手问题的经历有强烈的好奇心。最近,读了俞敏洪的自传《我曾走在崩溃的边缘》,感觉挺有意思。


俞敏洪是新东方的老板,在“双减”政策之后,新东方转型做了直播,也就是大家熟知的东方甄选,可能很多人还买过他们直播间的大米。当然,我没有买过,因为理智促使我很少为情怀买单。


离开北大


俞敏洪曾经是北大的老师,他的梦想是出国留学。但老师的工资低,很难赚够出国的学费。作为南方人的他,天生的商人基因让他找到了赚钱的路子——开英语培训班。这条路子获得的收入比工资高十几倍,利润十分丰厚。


于是,他打着北大的招牌私下招生,这意味着和北大“官方”的托福培训班形成了竞争关系。学校当然不会允许北大老师和北大抢生意,况且学校禁止老师私下办培训班。俞老师无法避免地和校领导发生了冲突,并因此被处分。


图片


处分的通告在学校的高音喇叭上足足播了一个礼拜,这件事情闹得人尽皆知,对俞敏洪名声的伤害极大。后来,学校分房自然没有俞老师的份了。在中国的社会体系下,名声对一个人来说极其重要。这种“德治”社会虽然在人口大国里对秩序起着巨大的作用,但也给一些人带来了巨大伤害。一遭名声败坏,要背一辈子,这对当事人是多大的打击。


那时俞敏洪已经结婚,本可以在大学教书过安稳的生活,但这一纸处分,让他决定从北大离职。最后,他骑着三轮车拉着家当离开了北大,开启了新东方的事业。


图片


死磕办学证


办培训班需要办学证,类似于现在的牌照。如果没有就无法公开招生,这意味着无法扩大规模。俞敏洪没办法,找了当时一个叫东方大学的机构联合办培训班,条件是支付总收入的25%给东方大学。


东方大学不参与招生、培训等所有事情,却要分掉一大笔钱。随着培训班的规模越来越大,俞敏洪意识到这不是长久之计,他决定就算再难,死磕也要把办学证拿到手。


要拿到办学证要符合两个条件:一是必须有大学副教授以上职称,二是要经原单位同意。


俞敏洪在北大只是讲师,没有副教授职称,而且北京大学处分了他,不可能同意他办学。两个条件都不符合,教育局直接拒绝,并叫他不要来了。


不得不说,俞老师的脸皮是够厚的,每隔一两星期就去教育局和办事的人聊天,久了大家就混熟了。


大概耗了半年,教育局放低了办学的要求,只要他能够在人才交流中心拿到允许办学证明就放行。可是人才交流中心的工作人员根本不给他开证明。直到遇见他一个在这里工作的学生,在她的帮助下才拿到证明。


办学证到手后,俞敏洪离开东方大学,开始独立办培训班。原来的“东方大学外语培训部”这块招牌积累了相当的名气,新东方成立后,大量学生还去那边报名。为了顺利切换品牌,新的培训机构起名叫新东方,而且从东方大学买断了“东方大学外语培训部”三年的使用权,每年支付20万。


这一系列的操作,可见俞敏洪有相当不错的商业头脑。


被赶下董事长的位置


中国是一个人情社会,比如亲情、友情、同学情。在这种社会成长起来的人,自然会想到找自己熟悉的人一起做事业。俞敏洪也不例外。新东方的培训班办得风生水起,俞敏洪开始寻找人才。


除了拉亲人朋友入伙,他还出国把大学同学王强、徐小平拉回来一起跟他干事业。这三人被称为“东方三驾马车”,也就是电影《中国合伙人》的原型。


image.png


亲人、同学、朋友之间,天然有信任感,在事业的初创阶段一起工作沟通效率非常高,而且为了共同的目标,凝聚力非常强。


当公司到了一定的规模,这种人情关系构建起来的团队,会使公司的人事关系变得非常复杂。


一是,团队没有组织架构,决策效率低下;二是,老板没有话语权,下面的人不知道该听谁的,却谁都不敢得罪。


后来,在新东方改革期间,创始团队出现各种矛盾,俞敏洪无法短期内处理好这些矛盾,被管理层认为是不合格的董事长。于是,俞敏洪从位置上退了下来。


退位期间,其他几个领导轮流做主,也无法处理好团队的矛盾。俞敏洪开始大量阅读公司管理、股权管理的书籍,积累比其他领导更丰富的管理知识。两三年后,他重新回到董事长的位置上。


他能回到位置上,管理知识是一方面,我斗胆猜测,运气的成分占比很大。毕竟被自己的公司赶走的大有人在。


结尾


除了上面3个故事,俞敏洪还有很多非常精彩的故事,比如“被抢劫险些丧命”、“知识产权侵权风波”、“新东方上市”、“遭遇浑水公司做空”等等。


语言是思想的外衣。他来自农村,《我曾走在崩溃的边缘》这本书语言坦诚,像他本人一样。他的人生非常精彩,展现了他强大的韧性。


他的成功,有时代的机遇,也有个人的努力。我们可能无法准确把握时代的机遇,但可以学习他的努力和韧性,在崩溃之时屹立不倒。


作者:华仔很忙
来源:juejin.cn/post/7218487123212091450
收起阅读 »

JS小白请看!一招让你的面试成功率大大提高——规范代码

web
前言 规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如...
继续阅读 »

前言


规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如何使我们的代码可读性提高。


正文



  • 这里我们先放出一道面试题


输入一个数组,例如:array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
返回一个固定格式的电话号码 例如:(123456-789

function phoneNumber(numbers){

}


  • 注释


当我们拿到这道题时,我们需要先做什么呢?是一上来直接实现这个函数的功能吗?一般人可能就这样上了,但是,如果面试官看到你这样去写代码,这样会显现你的编程素养特别差,面试官对你的好感度肯定也会随之下降。那我们应该怎么做呢?我们需要先写一个良好的注释


/**
* @func 返回固定格式的电话号码 函数功能
* @params array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
* @return (123) 456-7890
* @author jser
*/

// 函数定义
function phoneNumber(numbers) {

}

良好的注释可以提高代码的可读性。小伙伴们要记住,代码的可读性高于一切!


在大公司中,一份代码可能会经过许多的程序员阅读或修改。如果你写了良好的注释,当你的代码被他人阅读时,其他程序员可以快速读懂这份代码,或者根据自己的需要去修改这份代码,这样大大节省了时间,也提高了团队的效率。



  • 换行


当我们把良好的注释写好之后,就可以写代码去实现功能了。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]+ ")" + " " + numbers[3]+numbers[4]+numbers[5] + "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9]+""
}

“什么?这是什么代码,还需要我拖动去查看后面的,你可以走了!”


相信很多小伙伴看到这串代码时,也跟小编一样的头疼,这代码也看的太费劲了吧,怎么全部都挤在一行里去了。运行了一下之后,发现运行结果是对的,但是小编是不建议大家这样写代码的,我们要适当的进行换行操作,这样同样提高了代码的可读性。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""
}

ES6


ES5和ES6都是JavaScript语言的版本。ES5是ECMAScript 5的简称。自ES6(ECMAScript2015)出来后,ES6引入了一些新的语法和关键字,使得代码更加易读、易写,提高了可维护性,例如解构赋值、箭头函数、模板字符串等。



  • 箭头函数


在ES5旧版本中,很多小伙伴会觉得function很繁琐,而且到处都是function。而箭头函数可以算是函数的简版,它的结构变得比函数简单。那我们如何使用箭头函数呢?



  1. 去掉function,在()和{}之间加=>

  2. 如果参数列表只有一个形参,可省略()

  3. 如果函数体只有一句话,可省略{},如果仅有的一句话函数体是return xxx,就必须省略return


在上面那个例子里,我们可以这样写箭头函数


phoneNumber = (numbers) =>"(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""


可以看出,箭头函数的结构比我们使用旧版的函数会简单许多,小伙伴们可以选择使用啊。



  • 模板字符串


模板字符串,也称为模板字面量,是 ECMAScript 6(ES6)引入的一种新的字符串表示法。它允许在字符串中嵌入变量和表达式,使用反引号(``)包围字符串内容。与传统字符串拼接相比,模板字符串具有以下优势:


模板字符串允许在字符串中插入变量值或表达式,使用 ${} 语法。这使得代码更加清晰和可读,不需要繁琐的字符串拼接。


const name = 'junjun'; 
const greeting = `Hello, ${name}!`; // 使用模板字符串插入变量
console.log(greeting);// 输出:Hello, junjun!

那机灵的小伙伴就问了,电话号码那个例子是不是也可以使用模板字符串,我们直接上代码


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

//输出 (123)
// 456
// -7890

根据结果可以看到,使用模板字符串时,当我们进行换行操作时,模板字符串也会换行。模板字符串虽然方便,但小伙伴们要记住,不是在所有情况下,都可以使用模板字符串的。


总结


代码的可读性高于一切。我们作为小白,在慢慢成长的过程中,一定要尽早的规范自己的代码,提高代码的可读性,学习ES6的语法。当我们以后进入企业工作之后,公司会统一我们的代码风格,并且规定使用哪些语句。


作者:来颗奇趣蛋
来源:juejin.cn/post/7294080876827901993
收起阅读 »

Echarts添加水印

web
如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。 Echarts-backgroundColor backgroundColor 支持使用rgb(255,255,255),rgba(255,255,255,...
继续阅读 »

如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。



Echarts-backgroundColor


backgroundColor



支持使用rgb(255,255,255)rgba(255,255,255,1)#fff等方式设置为纯色,也支持设置为渐变色和纹理填充,具体见option.color



color


支持的颜色格式:




  • 使用 RGB 表示颜色,比如 'rgb(128, 128, 128)',如果想要加上 alpha 通道表示不透明度,可以使用 RGBA,比如 'rgba(128, 128, 128, 0.5)',也可以使用十六进制格式,比如 '#ccc'




  • 渐变色或者纹理填充


    // 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比,如果 globalCoord  `true`,则该四个值是绝对的像素位置
    {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 0,
    y2: 1,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 径向渐变,前三个参数分别是圆心 x, y 和半径,取值同线性渐变
    {
    type: 'radial',
    x: 0.5,
    y: 0.5,
    r: 0.5,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 纹理填充
    {
    image: imageDom, // 支持为 HTMLImageElement, HTMLCanvasElement,不支持路径字符串
    repeat: 'repeat' // 是否平铺,可以是 'repeat-x', 'repeat-y', 'no-repeat'
    }



水印


通过一个新的canvas绘制水印,然后在backgroundColor中添加


const waterMarkText = 'YJFicon'; // 水印
const canvas = document.createElement('canvas'); // 绘制水印的canvas
const ctx = canvas.getContext('2d');
canvas.width = canvas.height = 100; // canvas大小 - 控制水印间距
ctx.textAlign = 'center'; // 文字水平对齐
ctx.textBaseline = 'middle'; // 文字对齐方式
ctx.globalAlpha = 0.08; // 透明度
ctx.font = '20px Microsoft Yahei'; // 文字格式 style size family
ctx.translate(50, 50); // 偏移
ctx.rotate(-Math.PI / 4); // 旋转
ctx.fillText(waterMarkText, 0, 0); // 绘制水印

option = {
//...
backgroundColor: {//在背景属性中添加
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
...
}

image.png


如果只想在 toolbox.saveAsImage 下载的图片才展示水印,toolbox.feature.saveAsImage 支持配置backgroundColor,将其设置为水印【纹理】即可


option = {
//...
toolbox: {
show: true,
feature: {
...
saveAsImage: {
type: 'png',
backgroundColor: {
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
}
}
}
//...
}

graphic


除了使用纹理背景,还可以 graphic 添加图形元素,其中包括 text,可用于绘制水印。


option = {
//...
graphic: [
{
type: 'group',
rotation: Math.PI / 4,
bounding: 'raw',
top: 100,
left: 100,
z: 100,
children: [
{
type: 'text',
left: 0,
top: 0,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
},
{
type: 'text',
left: 40,
top: 40,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
}
]
},
],
//...
}

比较繁琐,需要自己设置平铺规律,建议封装一个平铺方法,不能控制区分 saveAsImage。


最初想象


预研


通过源码可以知道 saveAsImage 的实现也是通过内置 API getConnectedDataURL 获取url(base64格式),然后赋值到 a 标签上(带download属性)实现下载。


class SaveAsImage extends ToolboxFeature<ToolboxSaveAsImageFeatureOption> {

onclick(ecModel: GlobalModel, api: ExtensionAPI) {
const model = this.model;
const title = model.get('name') || ecModel.get('title.0.text') || 'echarts';
const isSvg = api.getZr().painter.getType() === 'svg';
const type = isSvg ? 'svg' : model.get('type', true) || 'png';
const url = api.getConnectedDataURL({
type: type,
backgroundColor: model.get('backgroundColor', true)
|| ecModel.get('backgroundColor') || '#fff',
connectedBackgroundColor: model.get('connectedBackgroundColor'),
excludeComponents: model.get('excludeComponents'),
pixelRatio: model.get('pixelRatio')
});
const browser = env.browser;
// Chrome, Firefox, New Edge
if (isFunction(MouseEvent) && (browser.newEdge || (!browser.ie && !browser.edge))) {
const $a = document.createElement('a');
$a.download = title + '.' + type;
$a.target = '_blank';
$a.href = url;
const evt = new MouseEvent('click', {
// some micro front-end framework, window maybe is a Proxy
view: document.defaultView,
bubbles: true,
cancelable: false
});
$a.dispatchEvent(evt);
}
// IE or old Edge
else {
// ...
}
}
// ...
}

思路


可以通过扩展此方法,先获取原始的图片url,基于这个图片重新绘制一个canvas,然后在这个基础上覆盖水印,最后将canvas再次转成url返回。



  • echartsInstance._api.getConnectedDataURL

  • echartsInstance.__proto__.getConnectedDataURL


含有两处实例方法需要处理。


结果



  • ✅可以拦截默认行为获取到添加水印后的图片url

  • ❌通过原api方法获取到url后绘制到新的canvas,涉及到异步处理(img标签需要等待load),由于 saveAsImage 调用 getConnectedDataURL 获取url是同步过程,因此无法正确读取到异步处理完的最终url,导致下载失败;不过,手动调用实例getConnectedDataURL 可以使用,需要配置Promise语法使用。


伪代码:


const originGetConnectedDataURL = echartsInstance._api.getConnectedDataURL
echartsInstance._api.getConnectedDataURL = async function () {
const origin = originGetConnectedDataURL.call(echartsInstance, ...arguments)
const result = await toWaterUrl(origin)
return result
}

function toWaterUrl (url) {
return new Promise(resolve => {
const img = new Image()
img.src = url + '?v=' + Math.random();
img.setAttribute('crossOrigin', 'Anonymous');
img.onload = function() {
// drawCanvas img 转 canvas
// afterWater canvas 绘制水印
resolve(afterWater(drawCanvas(img))
}
})
}

附带drawCanvas/afterWater


function drawCanvas(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
return canvas;
}

function afterWater(canvas, { text = 'WangSu', font = 'italic 10px', fillStyle = 'red', rotate = -30 }, type = 'png') {
return new Promise((resolve, reject) => {
let context = canvas.getContext('2d');
context.font = font;
context.fillStyle = fillStyle;
context.rotate(rotate * Math.PI / 180);
context.textAlign = 'center';
context.textBaseline = 'Middle';
const textWidth = context.measureText(text).width;
for (let i = (canvas.height * 0.5) * -1; i < 800; i += (textWidth + (textWidth / 5))) {
for (let j = 0; j < canvas.height * 1.5; j += 128) {
// 填充文字,i 间距, j 间距
context.fillText(text, i, j);
}
}
resolve(canvas.toDataURL('image/' + type))
})
}

作者:wangsd
来源:juejin.cn/post/7290417906840322106
收起阅读 »

Python枚举类:定义、使用和最佳实践

枚举(Enum)是一种有助于提高代码可读性和可维护性的数据类型,允许我们为一组相关的常量赋予有意义的名字。 在Python中,枚举类(Enum)提供了一种简洁而强大的方式来定义和使用枚举。 一、枚举类 1.1 什么是枚举类? 枚举类是一种特殊的数据类型,用于表...
继续阅读 »


枚举(Enum)是一种有助于提高代码可读性和可维护性的数据类型,允许我们为一组相关的常量赋予有意义的名字。


在Python中,枚举类(Enum)提供了一种简洁而强大的方式来定义和使用枚举。


一、枚举类


1.1 什么是枚举类?


枚举类是一种特殊的数据类型,用于表示一组具有离散取值的常量。它将常量与有意义的名字关联起来,使得代码更易读、更易维护。枚举类的每个成员都有一个唯一的名称和一个关联的值。


枚举类的典型用例包括表示颜色、方向、状态、星期几等常量值。使用枚举可以增强代码的可读性,减少硬编码的风险。


1.2 Python中的枚举类


在Python中,使用内置模块enum来创建和使用枚举类。


enum模块提供了Enum类,允许定义自己的枚举类型。


二、定义和使用枚举类


2.1 定义枚举类


要定义一个枚举类,需要导入Enum类并创建一个继承自它的子类。在子类中,我们定义枚举成员,并为每个成员分配一个名称和一个关联的值。


示例代码:


from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

在这个示例中,定义一个名为Color的枚举类,它有三个成员:REDGREENBLUE,每个成员都有一个整数值与之关联。


2.2 访问枚举成员


定义枚举类,可以通过成员名来访问枚举成员。例如:


print(Color.RED)    # 输出:Color.RED
print(Color.GREEN)  # 输出:Color.GREEN

2.3 获取枚举成员的值


要获取枚举成员的关联值,可以使用成员的value属性。例如:


print(Color.RED.value)    # 输出:1
print(Color.GREEN.value)  # 输出:2

2.4 比较枚举成员


枚举成员可以使用相等运算符进行比较。可以直接比较枚举成员,而不必比较它们的值。例如:


color1 = Color.RED
color2 = Color.GREEN

print(color1 == color2)  # 输出:False

2.5 迭代枚举成员


使用for循环来迭代枚举类的所有成员。例如,要打印所有颜色的名称和值:


for color in Color:
    print(f"{color.name}: {color.value}")

2.6 将值映射到枚举成员


根据枚举成员的值来获取成员本身,可以通过枚举类的__members__属性来实现。


例如,要根据值获取Color枚举成员:


value = 2
color = Color(value)
print(color)  # 输出:Color.GREEN

三、枚举的最佳实践


第三部分:枚举的最佳实践


枚举是一种有用的数据类型,但在使用时需要遵循一些最佳实践,以确保代码的可读性和可维护性。


3.1 使用枚举代替魔术数字


在代码中使用枚举来代替魔术数字(不明确的常量值)可以增加代码的可读性。枚举为常量提供了有意义的名字,使得代码更容易理解。


例如,使用枚举来表示星期几:


from enum import Enum

class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 5
    SATURDAY = 6
    SUNDAY = 7

3.2 避免硬编码


尽量避免在代码中硬编码枚举成员的值。如果需要使用枚举成员的值,最好使用枚举成员本身而不是其值。这可以提高代码的可读性,使得代码更容易维护。


例如,避免这样的写法:


if day == 1:  # 避免硬编码
    print("Today is Monday")

而使用枚举成员:


if day == Weekday.MONDAY:  # 更具表现力
    print("Today is Monday")

3.3 使用枚举成员的名称


枚举成员的名称通常应该使用大写字母,以便与常规变量和函数名称区分开。这是一种约定,有助于提高代码的可读性。例如,使用RED而不是red


3.4 考虑枚举成员的值类型


枚举成员的值通常是整数,但根据上下文和需求,可以选择不同的值类型,如字符串。选择适当的值类型可以使代码更具表现力。


3.5 考虑用法和上下文


在定义枚举时,考虑其用法和上下文。命名枚举成员和选择合适的值应该反映其在应用程序中的含义和用途。这有助于其他开发人员更容易理解和使用枚举。


3.6 枚举的不可变性


枚举成员是不可变的,一旦创建就不能更改其值。这有助于确保枚举成员的稳定性,并防止意外的修改。


遵循这些最佳实践可以帮助你有效地使用枚举,提高代码的可读性和可维护性。枚举是一种强大的工具,可以在代码中代替魔术数字,并提供有意义的常量名称。


总结


Python的枚举类是一种强大的工具,用于表示一组相关的常量,并提高代码的可读性和可维护性。通过枚举,我们可以为常量赋予有意义的名称,避免硬编码的值,以及更容易进行比较和迭代。


在实际编程中,枚举类可以提供一种清晰、可维护且更具表现力的方式来处理常量值。


作者:涛哥聊Python
来源:juejin.cn/post/7294150742112895002
收起阅读 »

手把手教你压测

前言 身为后端程序员怎么也要会一点压力测试相关的技术吧, 不然无脑上线项目万一项目火了进来大量请求时出现程序执行缓慢, 宕机等情况你肯定稳稳背锅, 而且这个时候短时间内还没办法解决, 只能使用物理扩容CPU, 内存, 更换网络等几种方式来解决问题, 妥妥的为公...
继续阅读 »

前言


身为后端程序员怎么也要会一点压力测试相关的技术吧, 不然无脑上线项目万一项目火了进来大量请求时出现程序执行缓慢, 宕机等情况你肯定稳稳背锅, 而且这个时候短时间内还没办法解决, 只能使用物理扩容CPU, 内存, 更换网络等几种方式来解决问题, 妥妥的为公司增加支出好吧, 下一个被开的就是你


都是想跑路拿高薪的打工仔, 身上怎么可以背负污点, 赶紧学一手压力测试进行保命, 我先学为敬


本篇文章主打一个学完就会, 奥利给



文中出现软件的版本



  • JMeter: 5.5

  • ServerAgent: 2.2.3



性能调优对各个开发岗位的区别


各个岗位对性能调优的关键节点



  • 前端工程师:

    • 首屏时间: 初次访问项目等待加载时间

    • 白屏时间: 刷新页面到数据全部展示时间

    • 可交互时间

    • 完全加载时间



  • 后端工程师

    • RT: 响应时间

    • TRS: 每秒事务数

    • 并发数: 这应该不会解释了吧



  • 移动端工程师

    • 端到端相应时间

    • Crash率

    • 内存使用率

    • FPS





主要讲一下后端工程师(Java), 毕竟这是吃饭的家伙

对于后端工程师来说, 影响性能的地方主要有两个



  • 数据库读写, RPC, 网络IO, 代码逻辑复杂度, 缓存

  • JVM(Throughput)
    - JVM(Throughput)



影响性能的关键要素



  • 产品设计

    • 产品逻辑

    • 功能交互

    • 动态效果

    • 页面元素



  • 基础网络

  • 代码质量&架构

    • 架构不合理

    • 研发功底和经验不足

    • 没有性能意识: 只实现功能不注重代码性能, 当业务上量后系统出现连锁反应, 导致性能问题增加

    • 数据库: 慢查询, 过多查询, 索引使用不当, 数据库服务器瓶颈



  • 用户移动端环境

    • 设备类型&性能

    • 系统版本

    • 网络(WiFi, 2G, 3G, 4G, 5G)

    • 硬件及云服务(服务器硬件, CPU, 内存..)




1. 初步了解压力测试


1.1压力测试是什么


压力测试是针对特定系统或组件, 为要确定其稳定性而特意进行的严格测试. 会让系统在超过正常使用条件下运作, 然后再确认其结果
对系统不断施加压力, 来预估系统`负载能力`的一种测试

一般而言, 只有在系统基础功能测试验证完成, 系统趋于稳定的情况下, 才会进行压力测试

1.2压力测试的目的


当负载主键增加时, 观察系统各项性能指标的变化情况是否有异常
发现系统的性能短板, 进行针对性的性能优化
判断系统在**高并发情况下是否会报错**, 进行是否会挂掉
测试在系统某个方面达到瓶颈时, 粗略估计系统性能上限

1.3 压力测试的指标


指标含义
相应时间(RT)是指系统对请求作出响应的平均时间, 对于单用户的系统, 响应时间可以很好地度量系统的性能
吞吐量(Throughput)是指系统在单位时间内处理的数量, 每秒事务数TPS 也算是吞吐量的一种
资源利用率CPU占用率, 内存使用率, 系统负载, 网络IO
并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量, 用户不同的使用模式会导致不同用户在单位时间发出不同数量的请求
错误率失败请求占比, 在测试时添加响应断言, 验证不通过即标记为错误, 若不添加, 响应码非200则为错误


评判系统性能, 主要考虑三个性能指标 RT, TPS, 资源利用率



image.png


上图充分的展示了响应时间, 吞吐量, 利用率和并发用户数之间的关系


随着并发用户的增加经过轻负载区, 达到最优并发数, 此时利用率高,吞吐量高, 响应时间短


但是如果用户数继续增加, 就会到达重负载区, 此时性能最大化, 但是当超过某一临界值(最大并发数)之后, 响应时间会急剧增加, 利用率平缓, 吞吐量急速下降


我们进行压测的目的主要就是测试出这个临界值的大小, 或者说, 我们系统当前能承受住的最大并发数


2. 压力测试工具 JMeter


老规矩, 先来一波软件介绍
JMeter是 Apache组织开发的基于 Java的开源压力测试工具, 具有体积小, 功能全, 使用方便等特点. 最初被设计用于 Web应用测试, 后来被扩展到其他测试领域.


常用压测工具:



  • Apache JMeter可视化的测试工具

  • LoadRunner 预测系统行为和性能的负载测试工具

  • Apache的 ab压力测试

  • nGrinder韩国研发的一款性能测试工具

  • PAS阿里测试工具


压测目标:



  • 负载上升各项指标是否正常

  • 发现性能短板

  • 高并发下系统是否稳定

  • 预估系统最大负载


2.1 安装 JMeter



写在前面, 需要 Java8环境, 没有的话需要去安装, 教程百度上一大堆



官网地址: jmeter.apache.org/


image.png


熟悉的download, 点他


image.png


开始下载(是真的慢)


image.png


解压之后进入 bin目录下, 双击 jmeter.bat, 就可以启动 JMeter了


image.png


上图可以看出, 在我们第一次打开界面时是英文的, 多少有点不友好, 接下来讲解一下怎么将语言更改为中文


2.2 设置 JMeter界面为中文


还是我们的 bin目录下, 有一个 jmeter.properties文件


image.png


双击打开, 搜索 language


image.png


去除 #号, 值更改为 zh_CN, 保存文件然后重启软件(双击jmeter.bat)


image.png


可以看到, 我们的 jmeter成功更改为了中文界面, 这对于我这种英语白痴来说是很舒服的


image.png


2.3 初步使用 JMeter


我们先随便创建一个测试用例, 就是简单测试, 同时讲解一下常用的参数


本次测试采用 20线程, 1秒启动时间, 循环100次, Get请求


2.3.1 创建线程组


image.png


image.png



  • 线程数: 虚拟的用户数, 一个用户占一个线程

  • Ramp-Up: 等待时间, 设置的虚拟用户(线程数)需要多长时间全部启动

  • 循环次数: 单个线程发送请求的次数

  • 调度器:

    • 持续时间: 该任务执行的时间

    • 启动延迟: 等待多少秒开始执行




2.3.2 创建 http请求


右键线程组-添加HTTP请求


image.png


这个中文讲解的很明白, 应该都看得懂的, 有疑问的评论区留言


image.png


2.3.3 结果树



结果树, 聚合报告, 图形结果只有新增, 解释在测试



线程组右键-添加-监听器-查看结果树


image.png


image.png


执行结果分析(启动之后显示界面)


image.png


列表列出了每一次的HTTP请求, 绿色的是成功, 红色的话就是失败



  • 取样器结果参数详解

    • Thread Name:线程组名称

    • Sample Start: 启动开始时间

    • Load time:加载时长

    • Latency:等待时长

    • Size in bytes:发送的数据总大小

    • Headers size in bytes:发送数据的其余部分大小

    • Sample Count:发送统计

    • Error Count:交互错误统计

    • Response code:返回码

    • Response message:返回信息

    • Response headers:返回的头部信息



  • 请求

    • 基本数据

    • 入参

    • 请求头



  • 相应数据

    • 响应码

    • 响应头




2.3.4 聚合报告


线程组右键-添加-监听器-聚合报告


image.png


执行结果分析(启动之后界面)


image.png


参数解释



  • 样本: 并发量

  • 平均值: 接口请求用时(单位毫秒)

  • 中位数: 请求用时中位数(单位毫秒), 例如2000请求以请求时间排序, 排名1000的用时时长

  • 90%百分位, 95%百分位, 99%百分位和中位数同理

  • 最小, 最大值: 请求用时最小和最大

  • 异常% : 请求中异常的百分比

  • 吞吐量: 单位时间内请求次数


2.3.5 图形结果


线程组右键-添加-监听器-图形结果


image.png


执行结果分析(启动之后显示界面)


image.png



  • 样本数目:总共发送到服务器的请求数。

  • 最新样本:代表时间的数字,是服务器响应最后一个请求的时间。

  • 吞吐量:服务器每分钟处理的请求数。

  • 平均值:总运行时间除以发送到服务器的请求数。

  • 中间值:有一半的服务器响应时间低于该值而另一半高于该值。

  • 偏离:表示服务器响应时间变化、离散程度测量值的大小。


2.3.6 断言


断言主要用来判断结果返回是否符合预期


线程组右键-添加-断言-响应断言


image.png


image.png


假设我们接口的返回状态码字段为code, 200为成功, 那么就可以在断言这里进行配置, 来判断请求是否成功


image.png


3. JMeter插件


3.1 插件安装


首先说明 JMeter是不支持插件的, 所以我们先要 JMeter的插件允许插件下载, 这句话多少有点拗口


网址: Install :: JMeter-Plugins.org


image.png


点击上图红框即可下载插件, 前面说过了 JMeter是 Java8开发的, 插件对应的也是一个 jar包


image.png


下好之后就可以放在 JMeter安装目录下的 lib/ext/ 下了, 具体下载页面也有说明


image.png


上述操作结束之后, 在选项里面就可以看到插件中心Plugins Manager


image.png


弹出以下界面, 点击 Available Plugins搜索我们需要的插件Basic GraphsAdditional Graphs, 勾选上, 然后安装


image.png


Basic Graphs主要显示显示平均响应时间,活动线程数,成功/失败交易数等


image.png


Additional Graphs主要显示吞吐量,连接时间,每秒的点击数等


image.png


在安装成功之后, 在监听器会相应的多出很多的 jc开头的, 这就代表安装成功了



我使用的是 5.5版本的, 之前版本安装之后好像要手动重启, 5.5安装完会自动重启



image.png


4. Linux硬件监控


在压测过程中, 我们需要实时了解服务器的CPU, 内存, 网络, 服务器负载等情况的变化, 这个时候我们就需要对我们的 Linux系统进行监控, 通常来讲, 我们查询 Linux系统的资源占用情况可以使用以下几种方法



  • 使用命令: top, iostat, iotop等

  • 使用 Linux远程连接工具 FinalShell等

  • 宝塔

  • JMeter压测工具 PerfMon


在 JMeter中, 如果需要监控服务器硬件, 那么我们还需要安装 PerfMon插件


image.png
PerfMon监控服务器硬件,如CPU,内存,硬盘读写速度等


进入下述地址开始下载监控包: github.com/undera/perf…


image.png


下载好之后我们可以直接解压放到服务器上, 会看到有两个startAgent文件, 分别是Windows系统和Linux系统的启动脚本


image.png


我们直接启动就可以了, 如果脚本启动连接不上的话可以考虑更改脚本内容


例: Linux系统脚本更改为以下内容


## 默认启动运行 startAgent.sh 脚本即可

## 服务启动默认4444端口,根本连接不上,因此自己创建一个部署脚本文件对此进行部署,且把端口修改为7879

nohup java -jar ./CMDRunner.jar --tool PerfMonAgent --udp-port 7879 --tcp-port 7879 > log.log 2>&1 &

## 赋予可执行权限

chmod 755 startAgent.sh

启动成功之后, 脚本同级路径下会多出 log.log的日志文件


image.png


然后我们就可以配置 JMeter了, 线程组-监听器-jp@gc - PerfMon Metrics Collector


image.png


image.png


我是在本地启动了ServerAgent.bat进行测试, 执行结果如下所示:


image.png



注: 文件必须配置, 不然没有图像



具体的配置指标信息建议看官方文档, 太多了....
github.com/undera/perf…


image.png


image.png


ServerAgent闪退问题


Windows系统配置好ServerAgent启动之后窗口闪退可能是 jre版本问题, 可以从下面的链接下载老版的 jre


http://www.aliyundrive.com/s/Yzw3DZ74w…


下载好之后, 建议安装目录设置在ServerAgent/jre


image.png


并更改startAgent.bat脚本, cd 到老版本 jre路径


image.png


作者:宁轩
来源:juejin.cn/post/7248511603883638844
收起阅读 »