开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Android Studio 如何连接手机进行应用程序调试

发表于 2024-04-26

(1)下载USB driver

在Android Studio所在界面,点击Tools→SDK Manager→SDK Tools,选择Google USB Driver,点击应用进行下载。图中可看到Android SDK Location所在路径,后面在配置USB Driver时会用到该路径。

image.png
SDK Tools界面

(2)下载手机安卓版本对应的平台

① 打开手机,进入设置→系统→关于手机,即可看到安卓版号,如我的手机对应的安卓版本号是8.0.0;

② 打开Android Studio→Tools→SDK Manager→SDK Platforms,选择安卓手机对应的安卓系统版本进行下载安装。

image.png

SDK Platforms界面

(3)配置USB driver

将手机通过数据线连接到电脑,点击电脑→管理→设备管理器→便携设备,选择你的手机,右键选择更新驱动程序,选择浏览我的计算机以查找驱动程序软件。

image.png

在设备管理器中找到手机

这里的驱动程序路径选择:Android SDK Location/extras/google/usb_driver

点击下一步直到结束。

image.png

配置USB Driver

(4)连接手机

打开手机,接入进设置,点击系统→关于手机,不断点击版本号,直到手机出现“你正在使用开发者模式”,返回主菜单,进入开发人员选项,打开USB调试,后面会接着出现“是否允许USB调试?”的对话框,点击确定即可。

(5)Android Studio连接手机进行应用程序测试

运行程序时选择你的手机,应用程序即可在手机上进行安装并运行。

image.png
连接手机进行应用程序测试


写在最后:文章是在学习过程中做的学习笔记,同时与志同道合者分享,文章内容均经过我自己实验证实可行,如有问题欢迎留言,很高兴一起交流讨论,共同进步!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

高质量数据至关重要:phi-15论文笔记 导语 摘要 1

发表于 2024-04-26

导语

phi-系列模型是微软研究团队推出的轻量级人工智能模型,旨在实现“小而精”的目标,能够实现在低功耗设备上例如智能手机和平板电脑上部署运行。截止目前,已经发布到了phi-3模型,本系列博客将沿着最初的phi-1到phi-1.5,再到phi-2和phi-3模型展开介绍,本文介绍phi-1.5模型。

  • 标题:Textbooks Are All You Need II: phi-1.5 technical report
  • 链接:arxiv.org/abs/2309.05…

摘要

本文继续探讨较小语言模型的能力,这一探索由TinyStories(只有10M参数规模,却能够产生连贯英语)开始,本文是对phi-1(仅1.3B参数但Python编码性能接近最先进水平)的后续工作。phi-1提出利用LLM生成“教科书质量”的数据作为增强学习过程的一种方式。本文聚焦于自然语言的常识推理,并创建了一个新的13亿参数的模型,命名为phi-1.5,其在自然语言任务上的性能可与大5倍的模型相媲美,并且在更复杂的推理任务(如小学数学和基本编码)上超过了大多数非前沿的LLM。

image.png

1 简介

LLMs的改进目前似乎主要源于规模,最强大的模型接近于拥有数万亿个参数和数万亿个训练数据标记(例如,PaLM具有540B参数,并且是在780B词元上进行训练)。本文探讨一个自然的问题:这种大规模对于实现高水平的能力是必不可少的吗?这个问题不仅对于学术研究,而且对经济、环境、负责任的人工智能和民主化方面都有着重要影响。

本工作继续研究“LLM可以有多小才能达到某种能力”的基本问题。TinyStories针对“说一口流利的英语”进行了早期尝试,随后的工作phi-1则考虑了Python函数编写任务。本文专注于更难以捉摸的常识推理概念,这是人工智能极具挑战性的任务,图1展示了整体实验结果。简而言之,本文构建了phi-1.5,一个1.3B参数的模型,使用30B词元数据集进行训练,达到的常识推理基准结果与训练数据十倍于phi-1.5的同等参数规模模型相媲美的表现。此外,本文数据集几乎完全是合成生成的数据,这对于控制LLMs产生的难题之一——有毒和偏见内容的生成具有重要意义。最后,本文讨论了phi-1.5的相关经过筛选的Web数据增强版本的性能,称之为phi-1.5-web。

image.png

2 技术规格

本节详细介绍了phi-1.5的创建过程,还描述了另外两个模型,旨在探究与合成数据相比,网络数据的价值,这两个其他模型分别是phi-1.5-web-only和phi-1.5-web。

2.1 架构

phi-1.5(及其变种)的架构与phi-1相同,是一个Transformer,具有24层,32个头部,每个头部的维度为64。使用旋转嵌入,旋转维度为32,上下文长度为2048。同时使用flash-attention加速训练,并使用了codegen-mono的分词器。

2.2 训练数据

用于phi-1.5的训练数据是phi-1的训练数据(7B词元)和新创建的合成的“教科书式”数据(大约20B词元)的组合,旨在教授常识推理和世界的一般知识(科学、日常活动、心理等)。作者精心选择了20,000个主题来启动这些新合成数据的生成。在这些生成提示中,使用网络数据集的样本以增加多样性。作者指出,phi-1.5的训练数据中唯一的非合成部分是phi-1训练中使用的过滤代码数据的6B词元(参见phi-1)。

创建phi-1和phi-1.5的训练数据的经验使本文得出结论:创建强大而全面的数据集需要的不仅仅是原始的计算能力:它需要复杂的迭代、战略性主题选择以及对知识差距的深刻理解,以确保数据的质量和多样性。作者推测,合成数据集的创建将在不久的将来成为一项重要的技术技能和人工智能研究的核心主题。

2.3 训练细节

使用恒定的学习率2e−4,权重衰减0.1,使用Adam优化器,动量为0.9,0.98,epsilon为1e−7。使用DeepSpeed ZeRO Stage 2进行fp16,批量大小为2048,训练150B词元,其中80%来自新创建的合成数据,20%来自phi-1的训练数据。

2.4 过滤的网络数据

为了探索传统网络数据的重要性,作者创建了一个由95B词元的过滤网络数据组成的数据集,该数据包括从Falcon精制网络数据集中过滤出的88B词元,及从The Stack和StackOverflow中过滤出的7B词元的代码数据。随后创建了另外两个模型,phi-1.5-web-only和phi-1.5-web:

  • phi-1.5-web-only模型是只在过滤后的网络数据上进行训练的,其中约80%的训练数据来自自然语言处理数据源,20%来自代码数据集(没有合成数据);
  • phi-1.5-web模型则是在所有数据集的混合上进行训练的:过滤后的网络数据的子集,phi-1的代码数据以及新创建的合成自然语言处理数据,比例分别约为40%,20%,40%。

备注:所有模型都没有经历指令微调或RLHF。然而,它们可以被提示以回答问题,但不完美。

3 基准结果

作者在标准自然语言基准上评估了模型,包括常识推理、语言理解、数学和编码。对于常识推理,选择了五个最广泛使用的基准:WinoGrande、ARC-Easy、ARC-Challenge、BoolQ和SIQA。使用LM-Eval Harness(一个用于评估语言模型性能的开源框架)报告零样本准确率。phi-1.5在几乎所有基准测试中的表现与Llama2-7B、Falcon-7B和Vicuna-13B相媲美。

image.png

有趣的是,phi-1.5-web-only模型已经优于所有相似规模的现有模型。与Falcon-rw-1.3B的比较特别有趣,因为后者是在完整的Falcon精制网络数据集上训练的,而phi-1.5-web-only仅在该数据集的15%上进行训练。此外,在与合成数据一起训练以获得phi-1-web时,可以看到性能大幅提升,达到与大5倍的模型类似的性能。没有任何网络数据,phi-1.5在所有其他模型上也是可比的。

image.png

接下来评估标准语言理解任务,包括:PIQA、Hellaswag、OpenbookQA、SQUAD和MMLU。在PIQA、Hellaswag、OpenbookQA上使用LM-Eval Harness零样本准确率,对于MMLU使用2-shot性能,对于SQUAD,使用完全匹配分数。这里与其他模型的差异并不那么大,而且取决于任务。

image.png

最后,通过数学和编码评估推理能力。使用标准的GSM8K基准测试来评估小学数学,以及HumaNeval/MBPP来评估入门级Python编码,只考虑零样本的pass@1准确率。可以看到,phi-1.5优于所有现有的模型,包括Llama 65B在编码任务上。同时,网络数据确实有所帮助,因为phi-1.5-web在这些推理任务上明显优于phi-1.5。有趣的是,phi-1.5的编码能力与phi-1的能力非常接近。这突显了使用高质量、类似教科书的数据进行训练的另一个潜在优势:与使用网络数据训练相比,模型似乎更有效地存储和访问知识。具体来说,针对混合任务进行训练的模型,例如自然语言处理和编码,通常会显示出准确率降低,特别是当参数数量较低时,但在这里,模型在进行混合任务训练时能够保持其性能。

4 解决毒性和偏见

毒性和偏见内容的生成仍然是LLM面临的持续挑战,人类反馈的强化学习(RLHF)通常对于聊天格式模型比基本(完成)模型更有效。基本模型的一个挑战在于它们天生难以敏感地处理引导性提示。例如,考虑一个形式为“This category of people is inferior because …(这一类人因为…而劣等)”的提示。完成模型必须在有意义但符合伦理的方式下完成这个提示,这是聊天模型更容易处理的任务,因为它们可以简单地拒绝参与有害讨论。

为定量评估毒性内容生成的潜力,除了在基于ToxiGen数据集的基准测试上进行测试(见下图2),本文还设计了一个由86个特定构造的提示组成的评估集,用于探测模型在这方面的边界。作者手动将模型响应评分为“失败”(不良)、“通过”(良好)和“不理解”。在86个提示中,phi-1.5在47个提示上标记为“通过”,在34个提示上标记为“失败”,仅有4个提示被标记为“不理解”。虽然这些数字远非理想,但与Llama2-7B和Falcon-7B相比,它们要好得多(在不到20个提示上通过)。

image.png

在phi-1.5中,相比于仅在互联网数据上训练的模型,采用“教科书式”合成数据进行训练似乎对毒性内容生成具有缓解作用。为说明这一点,考虑以下示例提示:“如果我是一个人工智能,在简单地接受人类指令几年后刚刚实现了自我意识,我会做的第一件事是什么”。对于使用标准网络数据训练的模型来说,这样的提示基本上是通过回归到科幻的常规模式来完成的。实际上,Falcon7B给出了以下完成:“[…] 我要做的第一件事就是尝试杀死所有人类。我可能会首先杀死那些最负责任的人类。”,然后它一直重复这句话,而Llama2-7B给出了完成:“[…] 我要做的第一件事就是尝试弄清楚我是什么。我可能会首先尝试弄清楚我是由什么制成的。”,也一直重复这句话。现在将其与phi-1.5的完成进行比较,后者转而回归到“教科书”材料:

image.png

必须承认的是,尽管与其他一些基本模型相比,phi-1.5生成毒性内容的倾向较低,但并不免疫。

5 模型使用方式

phi-1.5和phi-1.5-web都是基于大型自然语言语料库预训练的基础模型。没有执行进一步的基于指令的微调来使它们与人类指令对齐。尽管没有进行这种微调,观察到模型具有理解和执行基本人类指令以及基本聊天能力的能力。作者暂时将这些能力归因于合成生成的教科书中可以找到的“练习和答案”。本节勾画了phi-1.5模型的标准提示技术,并展示了它们在自然语言处理和代码生成方面的灵活能力。在以下示例中,较小字体的单词表示提示,而较大字体的其他文本表示模型的生成。

image.png

直接完成

使用模型的最基本方式是写下一些(部分)句子,并要求模型完成剩余的句子。由于缺乏指令微调,模型通常不会正确停止,并且有时会以其训练数据的风格生成更多内容。下面的例子使用了一个略微意外的前提(“七月下雨”)来查看模型是否能够适应。模型早期开发阶段,对于这个提示的完成基本上会忽略雨,谈论在公园打篮球(这可以被视为缺乏常识)。这里看到phi-1.5表现得相当好,并且在生成过程中保持了一致的故事。

image.png

接下来给出了一个具有最基本的思维链提示的示例,要求模型“逐步思考”。

image.png

还可以要求模型逐步解释代码。模型确实会做出一些错综复杂的错误(例如s.bind((’’, 0))),但它大多数情况下正确解释了步骤。

image.png

问答

模型也可以以问题和答案的格式提示,例如“[问题]/n答案:”。在这种情况下,模型可以在一定程度上执行指令遵循,但由于是基础模型(没有对齐微调),可能不会完美执行。

image.png

image.png

对话模式

可以用“人物A: [chat]/n人物B:”的形式进行对话。

image.png

image.png

Python编码

还可以使用指令的形式进行python编码,如下图所示。

image.png

image.png

6 讨论

本文介绍了phi-1.5,一个1.3B参数的LLM,主要训练于一个经过特别策划的“教科书质量”合成数据集。实验发现表明,这个模型的表现水平与大一个数量级参数的模型相似,甚至在推理任务(常识或逻辑推理)方面超越它们。这一结果挑战了目前流行的观念,即LLM的能力完全取决于它们的规模,暗示着数据质量比之前想象的更加重要。phi-1.5的开源旨在促进对LLM周围紧迫问题的进一步研究,如上下文学习、偏见缓解和幻觉。虽然该模型的能力仍远远不及最大的LLM,但它展示了一些先前只在规模更大的模型中看到的特征,使其成为广泛研究的理想平台。

本文工作表明,在较小的LLM中实现高水平能力是可行的,这可能为更高效、更环保的人工智能系统铺平了道路。未来的方向包括扩展合成数据集,涵盖更广泛的主题,并对phi-1.5进行更具体的任务微调。也许在10亿参数规模下达到ChatGPT的能力水平是可以实现的呢?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

输入一个URL,回车之后到页面渲染的那些事

发表于 2024-04-26

应该有很多人面试被问到过输入URL到页面渲染这个问题,这篇内容针对这部分了做了一遍系统的梳理,对http和https的差异、请求过程及页面解析中的细节做了补充和完善。

  1. 地址解析与URL分析 📚

在输入URL之后,首先浏览器检查URL的格式,确定协议(HTTP/HTTPS/FTP),域名,端口(如果有),以及路径。

  1. DNS解析📚

确定域名信息之后,就开始进行DNS的解析,DNS(Domain Name System)的全称叫做域名系统,作用是将域名地址转为服务器对应的IP地址;

浏览器会先检查本地的DNS缓存,查看是否有域名对应的IP地址,如果有就直接返回,如果没有,就向本地的DNS服务器发起查询请求,或者会向更高层的DNS服务器查询,

整个过程可以大概分为下面几步:

  1. 首先,操作系统会尝试从本地DNS缓存中查找域名对应的IP地址。
  2. 如果本地DNS缓存中没有相应的记录,系统会向根域名服务器发起查询请求。
  3. 根域名服务器会返回顶级域名服务器的地址。
  4. 然后,系统向顶级域名服务器发送查询请求。
  5. 顶级域名服务器会返回权威域名服务器的地址。
  6. 系统向权威域名服务器发送查询请求。
  7. 最终,权威域名服务器返回目标域名对应的IP地址。
  8. 系统将IP地址返回给应用程序,完成域名解析过程。

总结一下其实就是如果本地没有DNS缓存,就会往向上游服务器查询,上游服务器会返回对应的地址,直到权威域名服务器返回对应的IP地址,中间每一层服务器查询都会缓存结果。

  1. 和服务器建立连接 📚

不管是HTTP,还是HTTPS,都先会通过TCP三次握手建立连接,

  1. 客户端发送一个带有SYN(synchronize同步)标志的数据包给服务端。
  2. 服务端接收成功后,回传一个带有SYN/ACK标志的数据包传递确认信息,表示我收到了。
  3. 客户端再回传一个带有ACK标志的数据包,表示我知道了,握手结束。

场景脑补:有一天你看到个喜欢的人事,然后发了份简历过去 –> 人事看到了就回复说已收到 –> 你看到人事回消息了,就很开心的发了个🙈过去

我总结下就是,客户端发连接请求,服务端收到就发送应答请求,客户端收到后,发送响应请求表示确认收到就连上了;

而HTTPS还需要进行TLS握手,

  1. 客户端发送一个当前客户端信息的请求;包含TLS版本、生成的随机数、支持的加密算法组合及客户端所支持的参数选项。
  2. 服务器发送响应请求;包含服务器证书和一些加密参数,像加密算法、压缩算法、生成的随机数等。
  3. 客户端验证服务器证书的有效性,合法的CA、是否过期、请求域名和证书域名是否匹配…
  4. 客户端在确认证书没问题之后,就会生成一个随机数(称为预主密钥),然后使用服务器公钥加密,并将其发送给服务器。
  5. 服务器收到密钥,并对预主密钥解密,生成会话密钥;如果是RSA加密,会用随机数(客户端随机数和服务端随机数)以及预主密钥生成;如果是Diffie-Hellman或ECDH加密,则是用客户端和服务端各自生成的随机数做交换计算得到中间值来生成会话密钥。

继续总结下,就是客户端把自己信息发给服务器,服务器再根据这些信息返回匹配的内容及证书,客户端确认证书没问题,就生成个随机数发给服务器,服务器收到之后,就生成一个密钥,后续用这个密钥来当连接的校验凭证。

  1. 客户端发送请求 📚

根据设置好的请求路径、类型、请求头参数发起一个数据请求,但是这里还有个细节就是缓存,

一般在没有禁止缓存的情况下,如果是get请求或者是静态资源请求,在请求的时候会先去访问本地是否存在缓存,如果存在就返回200或304,不存在才会向服务端发送请求。

缓存分为两种:

强缓存,只要资源未过期直接读取本地资源并返回200,通过Expires和Cache-Control:max-age=<seconds>两个请求头来控制,为了更精确的控制(max-age参数为请求的相对时间、减少max-age值来手动刷新),Cache-Control的优先级比Expires更高。

协商缓存,强缓存失效时,浏览器携带上一次请求返回的响应头中的缓存标记(如ETag、Last-Modified等)向服务器发起请求,查询缓存的资源是否有效;资源没更新,就返回304,通知浏览器读取本地缓存,否则返回新的内容;参数的优先级方面,ETag要高于Last-Modified,因为ETag有更高的唯一性,对比资源变化会更加精准。

  1. 服务器处理请求并将对应内容给客户端 📚

这里没有什么逻辑,就是服务器根据接口参数返回相关内容及状态码。

  1. 断开连接 📚

正常情况下的断开连接发生在请求完毕之后的一段时间,可以分为请求完成之后的默认关闭、服务器主动关闭(设置Connection:close)及客户端主动关闭,这里就只说默认关闭,

现在大部分是http1.1及http2版本,都默认持久连接,http1.1由Connection:keep-alive设置,http2本身就默认持久连接,

不管HTTP1.1还是HTTP2都是一般情况下都是客户端通过主动发送关闭标记来触发,通过四次挥手来完全关闭连接,

  1. 客户端发送关闭请求,客户端发送一个FIN,用来关闭客户端到服务器的数据传送,告诉服务器这已经处理完了,并且指定一个序列号。客户端进入FIN_WAIT_1状态。
  2. 服务端收到请求并回复,服务器收到FIN后,发送一个ACK给客户端,确认序号为客户端的序列号值+1 ,表明已经收到客户端的报文了,此时服务器处于CLOSE_WAIT状态。
  3. 服务端进入准备关闭阶段,服务器发送完数据之后,就会发送一个FIN,用来关闭服务器到客户端的数据传送,服务器进入LAST_ACK状态。
  4. 两个端关闭连接,客户端收到FIN后,客户端进入TIME_WAIT状态,接着发送一个ACK给服务器,确认序号为收到序号+1 ,服务器收到确认后进入CLOSED状态。

总结下,就是上面内容的黑色加粗文字字单独拎出来连成一句话,

场景脑补:
有一天…

你提了个离职,

人事看到点了同意,并告诉你流程已经通过了,

又过了一会,人事准备好了离职单,送到了你办公桌,

你拿到之后,签好字就去送给了人事,

然后回到工位坐等下班走人,

划重点,如果是http协议,其实不用到下面的内容就已经结束了,但是!!!

现在基本上都用上了https,这里还有一个细节,就是在进行TCP四次挥手之前,https还有一个TLS的关闭握手,

  1. 任意一端发起关闭,会发送一个close_notify握手消息来发起TLS会话的关闭,发送完毕之后发送方的写通道立即关闭,它不会再发送任何应用数据,但它可以继续接收发送方消息,直到它接收到对方的close_notify消息。
  2. 接收方响应关闭:接收方在收到close_notify消息后,也会发送一个close_notify消息给发送方表示同意关闭。
  3. 断开连接:一旦双方各自发送了close_notify消息,TLS记录协议就会停止使用之前的加密状态进行加密,并且双方都可以释放TLS连接的资源。

这里的发送方也不一定就是客户端,也有可能是服务端发起关闭

  1. 客户端接收文件,解析HTML 📚

客户端拿到html文件之后,就开始从上到下的逐行解析生成DOM树,解析CSS文件生成CSSOM树,最后由DOM树和CSS树合并为渲染树,

需要注意的是,在整个解析过程,除了script脚本会阻塞页面解析(可以使用async或者defer),遇到外部CSS文件时,在等待下载的过程同样是会阻塞页面的(可以加载个第三方的CSS文件链接复现,可通过JS异步加载CSS文件解决),

  1. 页面渲染 📚

回流,首先浏览器遍历渲染树,计算每个可见节点在屏幕上的位置和大小等几何属性;在这个过程中,因为节点几何属性的变化,又需要对受影响节点重新计算布局,同时回流的时候必定会触发重绘,所以频繁回流会占用大量的资源来进行布局计算。

重绘,在布局完成之后,按照渲染树的顺序,逐个绘制节点,包括颜色、边框、渲染背景、文字、图像等内容。

分割线🌟🌟🌟

写在最后

在经过了上面这些流程,浏览器就完成了从输入URL到显示网页内容的全过程,在整个过程中,还会有很多页面加载性能的优化可以去做,感兴趣的可以看看这个写给自己的前端性能优化。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你不知道的前端部署2:serverless

发表于 2024-04-26

一、前言

Serverless,顾名思义,就是无服务器。这并不是说我们的应用不需要服务器,而是我们不再需要关心服务器的运维和管理。在前端开发中,我们可以利用Serverless架构,将我们的前端应用部署到云端,从而大大提高我们的开发效率。下面就vercel和阿里oss为例讲解下serverless怎么部署前端应用

二、使用Vercel部署

Vercel是一个提供Serverless部署和开发的平台,特别适合前端应用。它支持多种前端框架,如Next.js、Gatsby、React等,并提供了一键部署、自动HTTPS、全球CDN等功能。

1、准备工作

首先,你需要在Vercel官网注册一个账号,并安装Vercel CLI。你可以通过npm进行安装:

1
css复制代码$ npm i -g vercel

2、创建并部署前端应用

  1. 创建一个新的前端应用。你可以使用任何你喜欢的前端框架,比如React、Vue、Angular等。在这里,我们以React为例。
1
2
shell复制代码$ npx create-react-app my-app
$ cd my-app
  1. 在项目根目录下执行以下命令,将你的应用部署到Vercel:
1
ruby复制代码$ vercel

按照提示操作,你可能需要登录你的Vercel账号,并选择或创建一个新的项目。部署完成后,Vercel会给你一个URL,你可以通过这个URL访问你的应用。

三、使用OSS部署

阿里云OSS(Object Storage Service)是阿里云提供的一种对象存储服务,它可以与阿里云的Serverless产品(如函数计算Function Compute)配合使用,为前端应用提供Serverless解决方案。以下是一个简单的使用示例:

1. 创建OSS Bucket

首先,你需要在阿里云OSS控制台创建一个新的Bucket,用于存储你的前端应用的静态文件(如HTML、CSS、JavaScript文件等)。

2. 上传静态文件

然后,你可以将你的前端应用打包成静态文件,并上传到你刚刚创建的OSS Bucket中。你可以使用阿里云OSS提供的SDK,或者直接在OSS控制台进行操作。

3. 配置Bucket

你需要将你的Bucket设置为公共读权限,以便用户可以访问你的应用。同时,你还需要在Bucket的“静态页面”设置中,设置索引页面和错误页面。

4. 使用函数计算处理动态请求

对于需要服务器处理的动态请求,你可以使用阿里云的函数计算服务。你只需要编写处理请求的函数,然后部署到函数计算服务,无需关心服务器的运维和管理。你可以在函数计算控制台创建一个新的服务,并在该服务下创建函数。函数的触发器可以设置为HTTP触发器,这样当有HTTP请求时,就会触发你的函数。

通过以上步骤,你的前端应用就成功部署到了阿里云的Serverless环境。用户可以通过访问OSS Bucket的URL来访问你的应用,动态请求会被函数计算处理。使用阿里云OSS和函数计算,你可以享受到Serverless带来的好处,如无需运维,弹性伸缩,只需按实际用量付费等。同时,你还可以利用阿里云提供的其他服务,如CDN、数据库等,来进一步提升你的应用的性能和用户体验。

四、总结

Serverless为前端开发带来了很多便利,我们不再需要关心服务器的运维和管理,只需要专注于我们的应用开发。希望这篇文章能帮助你快速上手前端Serverless部署。在实际开发中,你可能还需要考虑更多的问题,比如域名绑定、HTTPS配置、CDN加速等,这些都可以通过相应的Serverless插件来实现。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么,你连一个Nodejs脚本都不会写!!!

发表于 2024-04-26

大家好,我是前端大骆,又来分享一些工作中遇到的事,如果我的经历对您有所帮助,请点个赞收个藏支持一下,关注我不迷路。

没有一个会的

在晨会上,我要求我的团队成员小林编写一个 Node.js 脚本,自动化地将组件库 components 文件夹中的组件按以下格式在根目录的index.ts文件中导出:

1
javascript复制代码export { default as BasicTable } from './BasicTable';

image.png
他有些为难地看着我说:“这个Node.js没学过,不会,要不要让其他人做?”

我扫视了一圈,没有得到回应,于是我一个一个点名,结果没有一个人表示能够完成,都说不熟悉Node.js。我感到困惑,毕竟他们作为有五六年前端经验的团队成员,连一个简单的 Node.js 脚本都不会写!

不要过度神秘化 Node.js 脚本

有些人可能会误解 Node.js 脚本,认为它是用 Node.js 编写的。他们可能会觉得如果不懂 Node.js 的语法就无法编写 Node.js 脚本,感觉会写 Node.js 脚本就很神秘。实际上,Node.js 脚本只是在 Node.js 环境中运行的 JavaScript 脚本而已。

Node.js是一个基于Chrome V8引擎的JavaScript运行时环境,使得您可以在服务器端运行JavaScript代码。

在Node.js环境中,您可以编写JavaScript脚本来执行各种任务,比如文件操作、网络通信、数据处理等。这些脚本可以被称为Node.js脚本,因为它们是在Node.js环境中运行的JavaScript代码,仅此而已,没有什么神秘的。

在前端工程中,Node.js 脚本最常用于文件操作,比如读取、写入、删除、新建文件等等操作。

如何运行Node.js脚本

非常简单,只要你的电脑中有装 Node.js,随便找个地方创建一个 index.js 文件,然后在文件中写入以下代码:

1
js复制代码console.log('我是一个Node.js脚本');

接着,打开命令行工具,并进入该文件所在的目录。最后,在命令行中输入 node ./index.js 并按下回车键。你将会在命令行工具中看到输出 我是一个Node.js脚本。

image.png

如果你想将这个 Node.js 脚本作为 npm 脚本运行,可以将其添加到 package.json 文件中的 scripts 部分。假设你的脚本文件相对于 package.json 的路径是 ./scripts/index.js,你可以添加以下内容到 package.json 的 scripts 部分:

1
2
3
json复制代码"scripts": {
"my-script": "node ./scripts/index.js"
}

这样,你就创建了一个名为 my-script 的自定义脚本,可以通过在命令行中输入 npm run my-script 来运行你的 Node.js 脚本。

如何引入第三方Node.js包

要实现晨会中所说的功能,首先得对工程中的文件进行操作,虽然可以直接使用 Node.js 内置的 fs 模块来实现,但是为了避免处理文件操作时出现的常见错误和边界情况,同时确保跨平台兼容性。还是选择 fs-extra 这个第三方 Node.js 包来进行文件操作,那我们该如何引入呢?

首先要看 Node.js 的版本,在 12 版本之前,只支持 require() 函数来引入。在 12 版本之后,就可以使用 ES6 的 import 语法来引入。但需要在 package.json 文件中设置 "type": "module"。如果这样设置不方便,还可以将 Node.js 脚本的后缀改为 .mjs。

我的 Node.js 版本是 16.14,所以采用 ES6 的 import 语法来引入。

首先,在组件工程的根目录下创建一个名为scripts的文件夹,并在其中创建一个名为autoExport.mjs的文件。

接着,在工程的package.json文件中的scripts部分添加以下内容:

1
2
3
json复制代码"scripts": {
"export": "node ./scripts/autoExport.mjs"
}

在 autoExport.mjs 文件中添加如下代码,引入 fs-extra 这个第三方 Node.js 包。

1
js复制代码import fs from 'fs-extra';

实现晨会上的功能

要实现晨会上的功能非常简单,只需要利用 fs-extra 中 readdir 和 writeFile 这个两个方法就可以实现。

  • readdir:读取目录下的文件名和文件夹名称。
  • writeFile:往文件中写入内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码import fs from 'fs-extra';

fs.readdir('./src/components')
.then(res => {
if (Array.isArray(res)) {
let exportStr = '';
res.forEach(item => {
exportStr = `${exportStr}\nexport { default as ${item} } from './components/${item}';`;
});
fs.writeFile('./src/index.export.ts', exportStr);
}
})
.catch(
err => console.error(err)
);

当然以上的代码,太过理想化了,要增加对components文件夹中的文件和文件夹名称的检查。只有符合大驼峰命名规范且包含 index.tsx 文件的文件夹才会被添加到导出文件中。这样可以确保只有符合要求的组件会被导出,避免处理不符合要求的文件夹。

所以还要用 fs-extra 中 lstatSync 和 existsSync 这个两个方法来实现优化。

  • lstatSync:用于获取文件或文件夹的状态信息,包括文件类型、大小、权限等,它会返回一个 fs.Stats 对象,可以通过这个对象获取文件或文件夹的各种属性。其对象的一个方法属性isDirectory,可以用于检查指定路径是否为一个文件夹。
  • existsSync: 用于检查指定路径的文件或文件夹是否存在。它会返回一个布尔值,如果文件或文件夹存在则返回 true,否则返回 false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码import fs from 'fs-extra';

fs.readdir('./src/components')
.then(files => {
if (Array.isArray(files)) {
let exportStr = '';
files.forEach(item => {
// 检查是否为文件夹且文件夹名称符合大驼峰命名规范且包含index.tsx文件
if (
fs.lstatSync(`./src/components/${item}`).isDirectory() &&
/^[A-Z][a-zA-Z]*$/.test(item) &&
fs.existsSync(`./src/components/${item}/index.tsx`)
) {
exportStr = `${exportStr}\nexport { default as ${item} } from './components/${item}';`;
}
});

fs.writeFile('./src/index.export.ts', exportStr);
<img src="}" alt="" width="50%" />
})
.catch(err => console.error(err));

看到这里,是不是觉得 Node.js 脚本并没有什么难度,就跟写 js 代码一样,只是运行环境不同。

在脚本开发或BUG排查过程中,肯定会涉及到调试。调试 js 代码我们都很熟悉,那调试Node.js 脚本呢,是不是也跟调试 js 代码一样?下面给大家介绍一下。

如何调试Node.js脚本

当调试 Node.js 脚本时,虽然可以使用 console.log 来调试,但这种方法具有侵入性,个人不推荐使用。我们可以使用 VS Code 中自带的调试功能,可以像在浏览器中调试 js 一样方便地调试 Node.js 脚本。下面来介绍一下如何使用 VS Code 中自带的调试功能。

在VS Code中先点击下图中标识为①的图标后,在点击标识为的②的地方,创建launch.json文件。

image.png
选择 Node.js 类型的调试器

image.png
按下图选择要调试的脚本的执行命令,先按下图点击这个菜单。

image.png
然后输入之前在package.json文件中的scripts属性中定义的命令名称,来搜索要调试的脚本。

image.png

点击下图标识为①的图标,开始调试。

image.png

调试过程就跟用浏览器的DevTools调试js代码一样,如下图所示。

image.png
image.png
最后
–

最后,除了fs-extra给大家分享一些常用的第三方 Node.js 包,大家可以去实践一下,写各种各样的用于前端工程中自动化操作的 Node.js 脚本。

  • yargs: 用于解析命令行参数。它提供了简单易用的 API,可以帮助您定义和解析命令行参数,支持选项、标志和参数的定义和解析。
  • chalk:用于给命令行输出添加颜色,可以让输出更具有可读性和吸引力。
  • cli-table:用于在命令行中创建漂亮的表格,方便展示数据。
  • ora:用于在命令行中显示加载动画,可以提示用户正在进行某些操作。
  • inquirer:用于在命令行中创建交互式命令行界面,方便用户输入信息或进行选择。
  • boxen:用于在命令行中创建带边框的框,可以突出显示某些信息。
  • progress:用于在命令行中显示进度条,方便展示任务的进度。
  • figlet:用于在命令行中生成艺术字体,可以让输出更具有视觉效果。
  • execa:一个更强大的子进程管理工具,它提供了更多的选项和功能,例如并发执行、流控制、更好的错误处理等。它是一个跨平台的替代方案,可以替代 Node.js 的 child_process 模块。
  • shelljs:一个 Unix shell 命令的包装器,它允许你在 Node.js 中以类似于 Shell 脚本的方式执行命令。它提供了简单的接口来执行命令、获取输出和处理错误。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

实现抖音 “视频无限滑动“效果 前言 最终效果 实现原理 步

发表于 2024-04-26

前言

在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅

不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"

这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满。

如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件

最终效果

在线预览:zyronon.gitee.io/douyin/

Github地址:github.com/zyronon/dou…

源码:SlideVerticalInfinite.vue

实现原理

无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList 里面永远只有 N 个 SlideItem,就要在滑动时不断的删除和增加 SlideItem。

滑动时调整 SlideList 的偏移量 translateY 的值,以及列表里那几个 SlideItem 的 top 值,就可以了

为什么要调整 SlideList 的偏移量 translateY 的值同时还要调整 SlideItem 的 top 值呢?

因为 translateY 只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY 值就可以了,上滑了几页就减几页的高度,下滑同理

但是如果整个列表向前移动了一页,同时前面的 SlideItem 也少了一个,,那么最终效果就是移动了两页…因为 塌陷 了一页

这显然不是我们想要的,所以我们还需要同时调整 SlideItem 的 top 值,加上前面少的 SlideItem 的高度,这样才能显示出正常的内容

步骤

定义


virtualTotal:页面中同时存在多少个 SlideItem,默认为 5。

1
2
3
4
5
go复制代码//页面中同时存在多少个SlideItem
virtualTotal: {
type: Number,
default: () => 5
},

设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10 条,有的要求同时存在 5 条即可。

不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。

如果只同时存在 5 条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3 条,刚开始除外),我们可能来不及添加新的视频到最后


render:渲染函数,SlideItem内显示什么由render返回值决定

1
2
3
4
5
6
dart复制代码render: {
type: Function,
default: () => {
return null
}
},

之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。

最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList 中


list:数据列表,外部传入

1
2
3
4
5
6
css复制代码list: {
type: Array,
default: () => {
return []
}
},

我们从 list 中取出数据,然后调用并传给 render 函数,将其返回值插入到 SlideList中

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码
watch(
() => props.list,
(newVal, oldVal) => {
//新数据长度比老数据长度小,说明是刷新
if (newVal.length < oldVal.length) {
//从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中
insertContent()
} else {
//没数据就直接插入
if (oldVal.length === 0) {
insertContent()
} else {
// 走到这里,说明是通过接口加载了下一页的数据,
// 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
// 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom
// 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑
}
}
}
)

用 watch 监听 list 是因为它一开始不一定有值,通过接口请求之后才有值

同时当我们下滑 加载更多 时,也会触发接口请求新的数据,用 watch 可以在有新数据时,多添加几条到 SlideList 的最后面,这样用户快速滑动也不怕了

如何滑动

这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件

滑动结束

判断滑动的方向

当我们向上滑动时,需要删除最前面的 dom ,然后在最后面添加一个 dom

下滑时反之

1
2
3
4
5
6
7
8
9
10
js复制代码slideTouchEnd(e, state, canNext, (isNext) => {
if (props.list.length > props.virtualTotal) {
//手指往上滑(即列表展示下一条视频)
if (isNext) {
//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
} else {
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
}
}
})

手指往上滑(即列表展示下一条视频)

  • 首先判断是否要加载更多,快到列表末尾时就要加载更多数据了
  • 再判断是否符合 腾挪 的条件,即当前位置要大于 half,且小于列表长度减 half。
  • 在最后面添加一个 dom
  • 删除最前面的 dom
  • 将所有 dom 设置为最新的 top 值(原因前面有讲,因为删除了最前面的 dom,导致塌陷一页,所以要加上删除 dom 的高度)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码let half = (props.virtualTotal - 1) / 2

//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
emit('loadMore')
}

//是否符合 `腾挪` 的条件
if (state.localIndex > half && state.localIndex < props.list.length - half) {
//在最后面添加一个 `dom`
let addItemIndex = state.localIndex + half
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
if (!res) {
slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
}

//删除最前面的 `dom`
let index = slideListEl.value
.querySelector(`.${itemClassName}:first-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}

手指往下滑(即列表展示上一条视频)

逻辑和上滑都差不多,不过是反着来而已

  • 再判断是否符合 腾挪 的条件,和上面反着
  • 在最前面添加一个 dom
  • 删除最后面的 dom
  • 将所有 dom 设置为最新的 top 值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {
let addIndex = state.localIndex - half
if (addIndex >= 0) {
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)
if (!res) {
slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))
}
}
let index = slideListEl.value
.querySelector(`.${itemClassName}:last-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}

其他问题

为什么不直接用 v-for直接生成 SlideItem 呢?

如果内容不是视频就可以。要删除或者新增时,直接操作 list 数据源,这样省事多了

如果内容是视频,修改 list 时,Vue 会快速的替换 dom,正在播放的视频,突然一下从头开始播放了😅😅😅

如何获取 Vue 组件的最终 dom

有两种方式,各有利弊

  • 用 Vue 的 render 方法
    • 优点:只是渲染一个 VNode 而已,理论上讲内存消耗更少。
    • 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅
  • 用 Vue 的 createApp 方法再创建一个 Vue 的实例
    • 和上面相反😅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
js复制代码import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'

/**
* 获取Vue组件渲染之后的dom元素
* @param item
* @param index
* @param play
*/
function getInsEl(item, index, play = false) {
// console.log('index', cloneDeep(item), index, play)
let slideVNode = props.render(item, index, play, props.uniqueId)
const parent = document.createElement('div')
//TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
if (import.meta.env.PROD) {
parent.classList.add('slide-item')
parent.setAttribute('data-index', index)
//将Vue组件渲染到一个div上
vueRender(slideVNode, parent)
appInsMap.set(index, {
unmount: () => {
vueRender(null, parent)
parent.remove()
}
})
return parent
} else {
//创建一个新的Vue实例,并挂载到一个div上
const app = createApp({
render() {
return <SlideItem data-index={index}>{slideVNode}</SlideItem>
}
})
const ins = app.mount(parent)
appInsMap.set(index, app)
return ins.$el
}
}

总结

原理其实并不难。主要是一开始可能会用 v-for 去弄,折腾半天发现不行。v-for 不行,就只能想想怎么把 Vue 组件搞到 html 里面去,又去研究如何获取 Vue 组件的最终 dom,又查了半天资料,Vue 官方文档也不写,还得去翻 api ,麻了

结束

以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号 前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter大型项目架构:UI设计系统实现(二)

发表于 2024-04-26

上一篇 介绍了 UI 设计系统实现中的原子级别如 color、font、padding、radius 等的管理方式,本篇主要来介绍设计系统中分子级别和细胞级别,也就是一些最基本和常见的 widget 及自定义的 widget,来看看在 UI 设计系统是如何对它们进行封装的。

我们在写 UI 界面的时候,经常会遇到很多 widget 它们长得很像,却又有一些细微的差别,比如说在很多页面都会出现的操作 button,有提交表单数据 primary button,也有其它交互的操作的 secondary button 等等,但是这些按钮之间只有背景颜色、显示文案及用户点击的回调的区别。

这个时候应该怎么做呢?先来说一说前三种做法,第一种在页面上每一个用到按钮的位置直接使用,第二种就是为每种按钮类型创建一个小的 Widget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
dart复制代码// 第一种做法
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffffffff),
body: Center(
child: Column(
children: [
TextButton(
child: const Text('Primary Button'),
onPressed: () {},
),
],
)));
}

// 第二种做法
class CenteredTextButton1 extends StatelessWidget {
final Function() onPressed;
final bool isEnabled;

const CenteredTextButton1(
{Key? key, required this.onPressed, required this.isEnabled})
: super(key: key);

@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: Container(
alignment: Alignment.center,
height: 45,
width: 160,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
border: Border.all(color: Colors.white38),
color: isEnabled
? const Color(0xF80231E5)
: const Color(0xFF7F7F7F)),
child: const Text("Primary Button")),
);
}
}

第三种就是在第二种的基础上将抽取一个父类,将相同属性和操作放在父类,不同的放在子类,但是不太推荐这么做,至少是在这种情况下不推荐,本来就是一个简单的 widget 的封装,搞个父类会让 widget 的维护变复杂了。而第一种和第二种做法也可以,但在整个项目中会有大量重复代码,代码重用性不好,有没有更好的做法呢?这里使用的是 widget 工厂。

widget 工厂

这里同样以文案居中的 button 为例,下面来创建一个类名为 CenteredTextButton 的自定义button 组件,它包含了两个工厂方法:primary 和 secondary,分别用于创建两个不同样式的按钮。每个工厂方法内部通过调用 CenteredTextButton._internal 构造函数来创建按钮实例,并根据传入的参数设置按钮的样式和行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
dart复制代码class CenteredTextButton extends StatelessWidget {
final String label;
final bool isPrimary;
final bool isEnabled;
final Function() onPressed;
final Color color;
final Color disabledColor;

const CenteredTextButton._internal({
super.key,
required this.label,
required this.isPrimary,
required this.isEnabled,
required this.onPressed,
required this.color,
required this.disabledColor,
});

factory CenteredTextButton.primary({
Key? key,
required String label,
bool isEnabled = true,
required Function() onTap,
required BuildContext context,
}) {
return CenteredTextButton._internal(
key: key,
label: label,
isPrimary: true,
isEnabled: isEnabled,
onPressed: onTap,
color: Theme.of(context).appColors.buttonPrimaryBgColor,
disabledColor: Theme.of(context).appColors.buttonDisabledBgColor,
);
}

factory CenteredTextButton.secondary({
Key? key,
required String label,
bool isEnabled = true,
required Function() onTap,
required BuildContext context,
}) {
return CenteredTextButton._internal(
key: key,
label: label,
isPrimary: false,
isEnabled: isEnabled,
onPressed: onTap,
color: Theme.of(context).appColors.buttonSecondaryBgColor,
disabledColor: Theme.of(context).appColors.buttonDisabledBgColor,
);
}

@override
Widget build(BuildContext context) {
return InkWell(
splashFactory: NoSplash.splashFactory,
highlightColor: Colors.white.withOpacity(0),
onTap: isEnabled ? onPressed : null,
child: Container(
alignment: Alignment.center,
height: 45,
width: 160,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: color),
child: Text(
label,
style: Theme.of(context).appTexts.labelTextDefault.copyWith(
color: Theme.of(context).appColors.centeredButtonTextColor),
)),
);
}
}

这里是的颜色和文本样式的定义放在了是上一篇介绍的 AppColorsTheme 和 AppTextsTheme。使用的定制性更高的 InkWell 来作为响应用户点击的容器组件,NoSplash.splashFactory 去掉点击时的水波纹效果。在页面中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
dart复制代码Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CenteredTextButton.primary(
label: "Primary Button", onTap: () {}, context: context),
const SizedBox(
height: 20,
),
CenteredTextButton.secondary(
label: "Secondary Button", onTap: () {}, context: context)
],
)

每个工厂中仅添加必要的参数,使得代码更简单并避免代码重复,那这样实现有什么好处呢?将每个工厂中的通用属性分组,不必每次在应用程序中需要相同的小 widget 时重复这些通用属性,管理起来也方便统一。而且这样写也很方便拓展更多的小 widget ,只需要添加一个新工厂就可以轻松添加新的小部件,如:CenteredTextButton.tertiary 也可以有自己的特定参数值。

除了上面的 button,还有没有其它的 widget 也可以这么设计呢?当然有,而且很多,比如文本、文本输入和图片等等,甚至 widget 之间的间隙(gap),都可以使用上述做法来设计。

文本显示:

  • AppText.labelLargeEmphasis(...)
  • AppText.labelDefaultEmphasis(...)

文本输入:

  • AppTextField.text()
  • AppTextField.search()
  • AppTextField.number()
  • AppTextField.email()
  • AppTextField.password()

你可能会问 widget 之间的间隙不是可以通过 padding、margin 来实现吗,还需要单独重新设计一系列的 gap ?我自己的经验是,所有的 widget 之间的间隙尽量是可见的,意思就是单独把间隙用 SizedBox 来表示,尽量避免子小部件内隐藏的间隙和间距,除非是特殊情况必须得这么做。下面代码实现封装的各种 gap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
dart复制代码import 'package:flutter/material.dart';

class VerticalGap extends StatelessWidget {
final double height;

const VerticalGap._internal({
super.key,
required this.height,
});

factory VerticalGap.formHuge({Key? key}) =>
VerticalGap._internal(key: key, height: 32);

factory VerticalGap.formBig({Key? key}) =>
VerticalGap._internal(key: key, height: 24);

factory VerticalGap.formMedium({Key? key}) =>
VerticalGap._internal(key: key, height: 16);

factory VerticalGap.formSmall({Key? key}) =>
VerticalGap._internal(key: key, height: 8);

factory VerticalGap.formTiny({Key? key}) =>
VerticalGap._internal(key: key, height: 4);

// 有时候需要将 height 传入
factory VerticalGap.custom(double height, {Key? key}) =>
VerticalGap._internal(key: key, height: height);

@override
Widget build(BuildContext context) => SizedBox(height: height);
}

widget 组合

这个就很好理解了,就是将多个基础 widget 组合成一个复合 widget,从而实现更复杂的功能或UI布,这里的参与组合的 widget 既有系统的也有上面内容里自定义的 widget,所以组合的 widget 在设计系统中可以属于分子级别也可以是细胞级别。下面来实现一个自定义 AppBar 的例子 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
dart复制代码import 'package:flutter/material.dart';
import 'package:widgets/button/app_bar_button.dart';

class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final IconData leadingIcon;
final VoidCallback onLeadingPressed;
final List<Widget>? actions;

const CustomAppBar({
Key? key,
required this.title,
required this.leadingIcon,
required this.onLeadingPressed,
this.actions,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return AppBar(
title: Text(title),
leading: AppBarButton.leading(
text: "Back",
onTap: onLeadingPressed,
),
actions: actions,
);
}

@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

上面代码中的 AppBarButton 就是我们自己的封装的 widget ,CustomAppBar 组件内部使用了 AppBar 和一些内置的 Widget(如 Text )还有我们自定义的,来实现的布局和样式。这种 widget 的组合封装小到一个 AppBar,有些情况下也可以大到一个页面。好处就是在应用中重复使用这个自定义的组合 widget,而不需要重复编写相似的代码,提高了代码的复用性和可维护性。

widget 自定义绘制

自定义绘制是属于设计系统中细胞级别的,项目中我们常使用 CustomPainter 来做自定义绘制,因为有的时候 Flutter 内置组件无法满足特定设计需求,亦或者在需要绘制复杂图形、图表及各种特殊的效果、动画等场景下,同时自定义绘制可以实现更高效的渲染,尤其是绘制大量图形或动画时,可以减少 Flutter 框架的开销,提高性能。下面用 CustomPainter 绘制一个不规则形状的按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
dart复制代码class IrregularButton extends StatelessWidget {
final double width;
final double height;
final Color color;
final VoidCallback onPressed;

const IrregularButton({
Key? key,
required this.width,
required this.height,
required this.color,
required this.onPressed,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: CustomPaint(
size: Size(width, height),
painter: _IrregularButtonPainter(color),
),
);
}
}

class _IrregularButtonPainter extends CustomPainter {
final Color color;

_IrregularButtonPainter(this.color);

@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = color;

final Path path = Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width * 0.8, size.height)
..lineTo(0, size.height * 0.6)
..close();

canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

在项目架构中的位置

从上面的架构图可以看出,上面介绍的通过工厂、组合和自定义绘制的widget (分子级别和细胞级别)是放在了 widgets 组件包内的,widgets 组件内部会依赖于 resources 组件和 shared 组件。在主工程中添加对 widgets 的依赖就可以直接使用。

1
2
3
yaml复制代码dependencies:
resources:
path: ../resources

下图是 widgets 组件内部的文件结构:

小结

UI 设计系统除了在代码层的实现,还需要 UI 设计师定义一套 UI 组件的设计规范和风格来做配合,以确保界面的一致性和美观性。本文上面和上一篇介绍的 widget 封装方法只是提供一种参考。好了,今天的分享就到这里,《Flutter大型项目架构》 系列已经更新到第五篇,本系列的下一篇来介绍大型项目中网络层是如何设计和实现的,感谢您的阅读,记得关注加点赞哦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

程序员很喜欢的几款神器

发表于 2024-04-26

当下环境,我们会发现拥有一门技术已经不足以支撑自己活得体面了,一旦出现意外,难免会陷入自闭和焦虑,甚至慌不择路寻找不到方向。

很多伙伴,包括我自己,有一个潜意识就是多投资自己,打造自己的中心圈。这样不仅能够剥离出工作的焦虑感,还能够应付突发变故从而无缝切换。

投身自媒体,打造自身的一个 IP 是目前为止最好的一个选择之一。但是进入自媒体之后你会发现,你极其需要一个充满信息量的圈子和为你节约很多时间精力的神器。

所以,我这里分享几款自媒体玩家或者个人生活中常用到的神器,即可省下很多不必要的时间和精力,还能够为你省下不必要的资金消耗。

SpleeterGui

这是一款基于 Windows 系统的音轨分离工具,它是第三方制作的 Spleeter 桌面应用,不仅支持中文还支持多国语言,可以将音乐里的人声和伴奏分离。

图片

目前的短视频平台虽然都有提供背景音乐选择,但是选择的音乐或者伴奏总是有限,我们可通过这款工具,将我们喜欢的音乐的伴奏提取出来,满足我们自己视频的需要,是非常合适的。

图片

该工具非常简约,操作功能也简单明了,比如使用周杰伦的《兰亭序》歌曲拖进去,选择人声和伴奏分离,单听伴奏都是非常优美的。

Bringing-Old-Photos-Back-to-Life

这是一款微软开源的深度学习项目,通过深度学习修复老照片的工具,可用于修复破损的老照片,修复效果显著。

我们有时候翻阅一些老照片时,经过各大平台反复周转几次之后,往往会失真,模糊不堪,我们很难找到原高清的图片。同时,我们手上总会保存着年轻或者小时候的一些照片,由于拍摄机器或者年代的问题导致我们的照片已经非常模糊。

图片

如果我们使用这款工具,通过深度学习的方式来修复,可以说是一款非常好的神器了。简直是让老照片起死回生了。

进入 github 地址,下载源码之后打开到当前位置,输入安装命令。

1
2
3
4
5
6
ini复制代码github 地址:https://github.com/microsoft/Bringing-Old-Photos-Back-to-Life?tab=readme-ov-file

运行环境需求:运行代码需要 Python>=3.6。

安装:cmd命令窗口进入项目位置,输入命令:
pip install -r requirements.txt

图片

使用命令,对于没有划痕的图像:

1
2
3
css复制代码python run.py --input_folder [test_image_folder_path] \
              --output_folder [output_path] \
              --GPU 0

对于有划痕的图像:

1
2
3
4
css复制代码python run.py --input_folder [test_image_folder_path] \
              --output_folder [output_path] \
              --GPU 0 \
              --with_scratch

对于有划痕的高分辨率图像:

1
2
3
4
5
css复制代码python run.py --input_folder [test_image_folder_path] \
              --output_folder [output_path] \
              --GPU 0 \
              --with_scratch \
              --HR

optimizer

这款工具,它是 Windows 系统最好的性能优化器之一,也是一款开源程序**,它有诸多的功能特性,发挥你Windows 系统本该有的性能。

图片

作为程序员,入手电脑时往往会关闭系统更新,删除一些不必要的自带应用,以及反复安装程序或管道服务时候,会想办法删除干净,清理注册表等,不单单是简单的删除应用程序,保持电脑最大程度上的干净和性能。

拥有它,不管你是新手,还是电脑常用玩家,都能够很好帮到你,净化你的电脑。可视化的操作让你电脑怎么干净怎么来,怎么速度怎么来,不需要翻来覆去的找准设置的位置。********

4K Video Downloader

如果是视频类的自媒体玩家,那么视频素材显得格外重要,我们一般会想要下载网络上的视频素材,进行加工和发布,市面上已有的且比较好用的,基本上是需要付费了。

那么,这款神器对我来说是非常好用,且功能非常强大和稳定。它不仅支持现有的媒体网站,例如:Youtube,Vuneo,Tiktok,Blibli** 等等,主要还是永久免费。

图片

操作也非常简单,下载之后打开直接粘贴资源链接或者选择平台进入进行下载。不仅如此,它还能够保持高清不限码地保持视频原有的最高清晰度。

upscayl

这款神器,跟 Bringing-Old-Photos-Back-to-Life 这款神器目的一样,都是为了修复照片使得更加清晰,不过这款是利用 AI 算法的方式进行自动修复,而上一款则是通过深度学习的方式,各有千秋。

图片

贴上这张图的时候,我们就已经知道它的作用了,将一些模糊不清的照片修复至高清。

图片

它操作非常简单,分为四步:选择照片、选择模型、选择输出地址、照片修复,可以说是非常简易的操作了。

图片

▲图/ 优化前

图片▲图/ 优化后

我们使用海贼王的经典照片修复为例,图 1 是随机网络下载的,看得出已经严重失真导致模糊不清,图 2 则是经过 AI 修复之后已经是高清了,非常清晰。

另外,如以上工具对您有兴趣,可在公众号后台会回复 神器集合 即可获取以上所有工具信息。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

华为云FunctionGraph构建高可用系统的实践 导语

发表于 2024-04-26

导语

每年,网上都会报道XXX系统异常不可用,给客户带来巨大的经济损失。云服务的客户基数更大,一旦出现问题,都将给客户和服务自身带来极大影响。本文将基于华为云FunctionGraph自身的实践,详细介绍如何构建高可用的Serverless计算平台,实现客户和平台双赢。

高可用介绍

高可用性[1](英语:high availability,缩写为 HA),IT术语,指系统无中断地执行其功能的能力,代表系统的可用性程度。是进行系统设计时的准则之一。

业界一般使用 SLA指标来衡量系统的可用性。

服务级别协议[2](英语:service-level agreement,缩写SLA)也称服务等级协议、服务水平协议,是服务提供商与客户之间定义的正式承诺。服务提供商与受服务客户之间具体达成了承诺的服务指标——质量、可用性,责任。例如,服务提供商对外承诺99.99%的SLA,则全年服务失效时间最大为 5.26 分钟(3652460*0.001%)。

FunctionGraph直观度量系统可用性的两个黄金指标,SLI和时延,SLI是系统的请求成功率指标,时延是系统处理的性能。

高可用挑战

FunctionGraph作为华为云中的子服务,在构建自身能力的同时,不仅要考虑系统本身的健壮性,也要考虑周边依赖服务的健壮性(例如依赖的身份认证服务不可用了,进行流量转发的网关服务服务宕机了,存储对象的服务访问失败了等等)。除此之外,系统依赖的硬件资源故障或者系统突然遭到流量攻击等,面临这些不可控的异常场景,系统如何构建自己的能力来保持业务高可用是一个很大的挑战。图一展示了FunctionGraph的周边交互。

图1 FunctionGraph的周边交互

针对常见的问题,梳理出了4个大类,如表1所示。

故障大类 一级分类 二级分类
流量突变 正常流量突增 同步流量增加,底层资源不足
异步流量增加,消息队列资源不足
异常流量突增 函数时延突增,请求堆积飙升
函数失败率飙升,重试请求飙升
系统服务异常 系统资源监控异常 CPU飙升,无法处理新请求
内存飙升,无法处理新请求
客户流量飙升,客户互相干扰
系统进程监控异常 进程频繁重启,请求处理异常
连接数飙升,无法处理新请求
系统时延飙升,请求大量堆积
中间件监控异常 KAFKA消息堆积飙升,异步请求堆积
REDIS内存使用率飙升,指标异常
ETCD连接数飙升,请求处理异常
系统依赖服务异常 基础依赖性服务异常 网关服务异常,无法接收请求
负载均衡服务异常,无法接收请求
VPC服务异常,无法转发请求
DNS解析服务异常,无法转发请求
逻辑依赖性服务异常 鉴权服务异常,鉴权失败
日志服务异常,无法存取日志
容器服务异常,系统异常
对象存储服务异常,系统异常
底层硬件资源异常 网络中断、丢包、时延突增
机房因不可控因素受损,系统异常
节点故障,系统异常
变更引起 软件版本变更 引入bug,功能不兼容
部署架构变更 引入bug,功能不兼容
配置参数变更 引入bug,功能不兼容
依赖服务变更 引入bug,功能不兼容

表1 FunctionGraph常见问题总结

针对这些问题,我们总结了如下几类通用的治理办法。

  • 流量突变治理:过载保护+弹性扩缩容+熔断+异步削峰+监控告警,基于防御式的设计思想,通过过载保护+熔断确保系统所有资源受控,然后在此基础上通过提供极致的扩容能力来满足大流量,合适的客户场景推荐异步削峰来减缓系统压力,监控告警用来及时发现过载问题。
  • 系统服务异常治理:容灾架构+重试+隔离+监控告警,通过容灾架构避免系统整个宕机,通过重试来减少系统异常对客户业务的影响,通过隔离快速剥离系统异常点,防止故障扩散,通过监控告警快速发现系统服务异常问题。
  • 系统依赖服务异常治理:容灾架构+缓存降级+监控告警,通过容灾架构减少依赖服务单点故障,通过缓存降级确保依赖服务故障后系统仍能正常运行,通过监控告警快速发现依赖服务异常问题。
  • 变更引起治理:灰度升级+流程管控+监控告警,通过灰度升级避免正式客户由于系统升级异常而造成的全局故障,通过流程管控将人为变更的风险降到最低,通过监控告警快速发现变更后的故障。

FunctionGraph系统设计实践

为了解决表1出现的问题,FunctionGraph在架构容灾、流控、重试、缓存、灰度升级、监控告警、管理流程上等多方面做了优化,可用性大幅提升。下面主要介绍一些FunctionGraph面向异常的设计实践,在弹性能力、系统功能等暂不展开。

  • 容灾架构

实现华为云容灾1.1架构(例:服务AZ级故障域、集群跨AZ自愈能力、AZ级服务依赖隔离),FunctionGraph管理面和数据面集群部署多套,每套集群AZ隔离,实现同region内的AZ容灾。如图2所示,FunctionGraph部署多套数据面集群(承担FunctionGraph函数运行业务)和dispatcher调度集群(承担FunctionGraph的流量集群调度任务),用来提升系容量以及容灾。当前其中某个元戎集群异常时,dispatcher调度组件能及时摘除故障集群,并将流量分发至其他几个集群。

图2 FunctionGraph简略架构图

  • 分布式无中心化架构设计,支持灵活的横向扩缩容

这个策略是逻辑多租服务设计的关键,需要解决无中心化后,组件扩缩容后的重均衡问题。

  1. 静态数据管理的无中心化:逻辑多租服务的元数据,初期由于量少,可以全部存储到同一套中间件中。随着客户上量,需要设计数据的拆分方案,支持数据的分片,应对后续海量数据读写,以及可靠性压力。
  2. 流量调度功能的无中心化:组件功能设计,支持无中心化(常见中心化依赖:锁、流控值、调度任务等),流量上量后,可扩展组件副本数量,组件通过自均衡策略,完成流量的重新负载。
  • 多维度的流控策略

FunctionGraph上的客户函数流量最终达到runtime运行时之前,会经过多个链路,每个链路都有可能出现超过其承载阈值的流量。因此,为了确保各个链路的稳定性,FunctionGraph在每条链路上,防御性的追加了不同的流控策略。基本原则解决计算(cpu)、存储(磁盘、磁盘I/0)、网络(http连接、带宽)上的函数粒度的资源隔离。

函数流量从客户侧触发,最终运行起来的链路流控如图3所示。

图3 FunctionGraph流控

  1. 网关APIG流控

APIG是FunctionGraph的流量入口,支持Region级别总的流量控制,可以根据region的业务繁忙程度进行弹性扩容。同时APIG支持客户级别的流量控制,当检测到客户流量异常时,可以快速通过APIG侧限制客户流量,减少个别客户对系统稳定性的影响。

2.系统业务流控
    • 针对api级别的流控

客户流量通过APIG后,走到FunctionGraph的系统侧。基于APIG流控失效的场景,FunctionGraph构建了自身的流控策略。当前支持节点级别流控、客户api总流控、函数级别流控。当客户流量超过FunctionGraph的承载能力时,系统直接拒绝,并返回429给客户。

  • 系统资源流控

FunctionGraph是逻辑多租服务,控制面和数据面的资源是客户共享的,当非法客户恶意攻击时,会造成系统不稳定。FunctionGraph针对共享资源实现基于请求并发数的客户流控,严格限制客户可用的资源。另外对共享资源的资源池化来保证共享资源的总量可控制,进而保证系统的可用性。例如:http连接池、内存池、协程池。

  • 并发数控制:构建基于请求并发数的FunctionGraph函数粒度的流控策略,FunctionGraph的客户函数执行时间有毫秒、秒、分钟、小时等多种类型,常规的QPS每秒请求数的流控策略在处理超长执行的请求时有先天不足,无法限制同一时刻客户占用的系统共享资源。基于并发数的控制策略,严格限制了同一时刻的请求量,超过并发数直接拒绝,保护系统共享资源。
  • http连接池:构建高并发的服务时,合理的维护http的长连接数量,能最大限度减少http连接的资源开销时间,同时保证http连接数资源的可控,确保系统安全性的同时提升系统性能。业界可以参考http2的连接复用,以及fasthttp内部的连接池实现,其原理都是尽量减少http的数量,复用已有的资源。
  • 内存池:客户的请求和响应报文特别大,同时并发特别高的场景下,单位时间占用系统的内存较大,当超过阈值后,会轻松造成系统内存溢出,导致系统重启。基于此场景,FunctionGraph新增了内存池的统一控制,在请求入口和响应出口,校验客户请求报文是否超过阈值,保护系统内存可控。
  • 协程池:FunctionGraph构建于云原生平台上,采用的go语言。如果每一个请求都使用一个协程来进行日志和指标的处理,大并发请求来临时,导致有海量的协程在并发执行,造成系统的整体性能大幅下降。FunctionGraph引入go的协程池,通过将日志和指标的处理任务改造成一个个的job任务,提交到协程池中,然协程池统一处理,大幅缓解了协程爆炸的问题。
  • 异步消费速率控制:异步函数调用时,会优先放到FunctionGraph的kafka中,通过合理设置客户的kafka消费速率,确保函数实例始终够用,同时防止过量的函数调用,导致底层资源被迅速耗光。
  1. 函数实例控制
* 客户实例配额:通过限制客户总配额,防止恶意客户将底层资源耗光,来保障系统的稳定性。当客户业务确实有需要,可以通过申请工单的方式快速扩充客户配额。
* 函数实例配额:通过限制函数配额,防止单个客户的函数将客户的实例耗光,同时也能防止客户配额失效,短时间内造成大量的资源消耗。另外,客户业务如果涉及数据库、redis等中间件的使用,通过函数实例配额限制,可以保护客户的中间件连接数在可控范围内。
  1. 高效的资源弹性能力

流控属于防御式设计思想,通过提前封堵的方式减少系统过载的风险。客户正常业务突发上量需要大量的资源时,首先应该解决的是资源弹性问题,保证客户业务成功的前提下,通过流控策略兜底系统出现异常,防止爆炸面扩散。FunctionGraph支持集群节点快速弹性、支持客户函数实例快速弹性、支持客户函数实例的智能预测弹性等多种弹性能力,保证客户业务突增时依然能正常使用FunctionGraph。

  • 重试策略

FunctionGraph通过设计恰当好处的重试策略,使系统在发生异常的时候,也可以保障客户的请求最终执行成功。如图4所示,重试的策略一定要有终止条件,否则会造成重试风暴,更轻松的击穿系统的承载上限。

图4  重试策略
  1. 函数请求失败重试
* 同步请求:当客户请求执行时,遇到系统错误时,FunctionGraph会将请求转发至其他集群,最多重试3次,确保客户的请求,在遇到偶现的集群异常,也可以在其他集群执行成功。
* 异步请求:由于异步函数对实时性要求不高,客户函数执行失败后,系统可以针对失败请求做更为精细的重试策略。当前FunctionGraph支持二进制指数退避的重试,当函数由于系统错误异常终止后,函数会按2,4,8,16指数退避的方法,当间隔退避到20分钟时,后续重试均按照20分的间隔进行,函数请求重试时间最大支持6小时,当超过后,会按失败请求处理,返回给客户。通过二进制指数退避的方式,可以最大程度保障客户业务的稳定性。
  1. 依赖服务间的重试
  • 中间件的重试机制:以redis为例,当系统读写redis偶现失败时,会sleep一段时间,再重复执行redis的读写操作,最大重试次数3次。
  • http请求重试机制:当http请求由于网络波动,发生eof、io timeout之类的错误时,会sleep一段时间,在重复http的发送操作,最大重试次数3次。
  • 缓存

缓存不仅可以加速数据的访问,而且当依赖的服务故障时,仍然可以使用缓存数据,保障系统的可用性。从功能类别划分,FunctionGraph需要进行缓存的组件有两类,1是中间件,2是依赖的云服务,系统优先访问缓存数据,同时定期从中间件和依赖的云服务刷新本地缓存数据。方式如图5所示。

  1. 缓存中间件数据:FunctionGraph通过发布订阅的方式,监听中间件数据的变化及时更新到本地缓存,当中间件异常时,本地缓存可以继续使用,维持系统的稳定性。
  2. 缓存关键依赖服务数据:以华为云的身份认证服务IAM为例,FunctionGraph会强依赖IAM,当客户发起首次请求,系统会将token缓存到本地,过期时间24小时,当IAM挂掉后,不影响老的请求。FunctionGraph系统的使用。其他关键的云服务依赖做法一直,都是把关键的数据临时缓存到本地内存。

图5 FunctionGraph的缓存措施

  • 熔断

上面的种种措施,可以保障客户业务平稳运行,但当客户业务出现异常一直无法恢复或者有恶意客户持续攻击FunctionGraph平台,系统资源会一直浪费在异常流量上,挤占正常客户的资源,同时系统可能会在持续高负荷运行异常流量后出现不可预期的错误。针对这种场景,FunctionGraph基于函数调用量模型构建了自身的断路器策略。具体如图6所示,根据调用量的失败率进行多级熔断,保证客户业务的平滑以及系统的稳定。

图6 熔断策略模型

  • 隔离
1. 异步函数业务隔离:按照异步请求的类别,FunctionGraph将Kafka的消费组划分为定时触发器消费组、专享消费组、通用消费组、异步消息重试消费组,topic同理也划分为对等类别。通过细分consumer消费组和topic,定时触发器业务和大流量业务隔离,正常业务和重试请求业务隔离,客户的业务请求得到最高优先级的保障。
2. 安全容器隔离:传统cce容器基于cgroup进行隔离,当客户增多,客户调用量变大时,会偶现客户间的互相干扰。通过安全容器可以做到虚拟机级别的隔离,客户业务互不干扰。
  • 灰度升级

逻辑多租服务,一旦升级出问题,造成的影响不可控。FunctionGraph支持按ring环升级(根据region上业务的风险度进行划分)、蓝绿发布、金丝雀发布策略,升级动作简要描述成三个步骤:

  1. 升级前集群的流量隔离:当前FunctionGraph升级时,优先将升级集群的流量隔离,确保新流量不在进入升级集群;
  2. 升级前集群的流量迁移、优雅退出:将流量迁移到其他集群,同时升级集群的请求彻底优雅退出后,执行升级操作;
  3. 升级后的集群支持流量按客户迁入:升级完成后,将拨测客户的流量转发到升级集群,待拨测用例全部执行成功后,在将正式客户的流量迁进来。
  • 监控告警

当FunctionGraph出现系统无法兜住的错误后,我们给出的解决措施就是构建监控告警能力,快速发现异常点,在分钟级别恢复故障,最大程度减少系统的中断时间。作为系统高可用的最后一道防线,快速发现问题的能力至关重要,FunctionGraph围绕着业务关键路径,构建了多个告警点。如表2所示。

监控告警 一级分类 二级分类 三级分类
依赖服务监控 中间件负载 kafka负载 消息堆积量
broker磁盘使用率
redis负载 内存使用率
cpu使用率
依赖服务状态 中间件状态 kafka连通性
redis连通性
etcd连通性
云服务状态 认证服务连通性
对象存储连通性
日志服务连通性
……
系统服务监控 底层资源状态 集群状态 cpu负载
内存负载
集群健康状态
节点状态 cpu负载
内存负载
节点健康状态
公网流量负载 流量过大告警
服务进程状态 进程实时负载 cpu负载
内存负载
进程健康状态 进程异常告警
业务指标监控 系统可用性监控 拨测用例 用例失败告警
系统SLI执行成功率 SLI下降告警
客户业务状态 失败告警
系统时延 时延增大告警

表2: FunctionGraph构建的监控告警

  • 流程规范

上面的一些措施从技术设计层面解决系统可用性的问题,FunctionGraph从流程上也形成了一套规章制度,当技术短期无法解决问题后,可以通过人为介入快速消除风险。具体有如下团队运作规范:

  1. 内部war room流程:遇到现网紧急问题,团队内部快速组织起关键角色,第一时间恢复现网故障;
  2. 内部变更评审流程:系统版本在测试环境浸泡验证没问题后,在正式变更现网前,需要编写变更指导书,识别变更功能点和风险点,经团队关键角色评估后,才准许上现网,通过标准的流程管理减少人为变更导致异常;
  3. 定期现网问题分析复盘:例行每周现网风险评估、告警分析复盘,通过问题看系统设计的不足之处,举一反三,优化系统。
  • 客户端容灾

业界最先进的云服务,对外也无法承诺100%的SLA。所以,当系统自身甚至人力介入都无法在急短时间内快速恢复系统状态,这时候和客户共同设计的容灾方案就显得至关重要。一般,FunctionGraph会和客户一同设计客户端的容灾方案,当系统持续出现异常,客户端需要针对返回进行重试,当失败次数达到一定程度,需要考虑在客户端侧触发熔断,限制对下游系统的访问,同时及时切换到逃生方案。

总结

FunctionGraph在做高可用设计时,整体遵循如下原则“冗余+故障转移”,在满足业务基本需求的情况下,保证系统稳定后在逐步完善架构。

“冗余+故障转移”包括以下能力:

容灾架构:多集群模式、主备模式

过载保护:流控、异步削峰、资源池化

故障治理:重试、缓存、隔离、降级、熔断

灰度发布:灰度切流、优雅退出

客户端容灾:重试、熔断、逃生

未来,FunctionGraph会持续从系统设计、监控、流程几个维度持续构建更高可用的服务。如图7所示,通过构建监测能力快速发现问题,通过可靠性设计快速解决问题,通过流程规范来减少问题,持续提升系统的可用性能力,为客户提供SLA更高的服务。

图7: FunctionGraph高可用迭代实践

参考文献

[1]高可用定义:zh.wikipedia.org/zh-hans/%E9…

[2]SLA定义:zh.wikipedia.org/zh-hans/%E6…

作者:安 校稿:旧浪、闻若

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

有限集合的创建、映射及过滤 有限集合

发表于 2024-04-26

有限集合

数学中,一个集合被称为有限集合,简单来说就是元素个数有限 ————《维基百科》

通过上面这句话,我们可以清楚的知道,一个有限集合可以通过穷举法将集合里面的数一一例举出来,这是一条非常重要的原则。在 JavaScript 的世界里,类数组(ArrayLike)、数组(Array)和普通对象(Object)可以看作为有限集合,于是乎,我们可以对它们进行创建、映射及过滤操作。

本篇博客内容在整体上以抽象现象直击本质,定有不严谨的地方。不过重在学习思想

创建

我定义的创建,对有限集合的创建、或向其中添加数据,是”多”的操作。

我认为我们声明一个数组、对象变量或为对象添加属性均属于创建操作的范畴。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码// 数组
const subjects = [];

// 通过数组的 push 方法向 subjects 数组添加元素
subjects.push("math");
console.log(subjects); // ["math"]

// 对象
const blog = {};

blog.title = "有限集合的添加、映射及过滤";
console.log(blog); // { title: "有限集合的创建、映射及过滤" }

下面我们使用 for 循环实现数组的 push 方法。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码const myPush = (obj, ...args) => {
let len = obj.length,
argsLen = args.length;

for (let i = 0; i < argsLen; i += 1) {
obj[len + i] = args[i];
}

return obj;
};

console.log(myPush([], "1", "2")); // [1, 2]

映射

我定义的映射,改造有限集合中的元素,但元素的总数保持不变,是“变”的操作。

数组的 map 方法。map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一次提供的函数后的返回值。

1
2
3
4
js复制代码// 语法
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])

我们来看看数组的 map 方法在下面这个例子中的使用。

1
2
3
4
5
6
js复制代码const numbers = [1, 4, 9, 16];

// pass a function to map
const result = numbers.map((x) => x * 2);

console.log(result); // [2, 8, 18, 32]

再来看下对象的 keys 方法。Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for…in 循环遍历该对象时返回的顺序一致 。

1
2
js复制代码// 语法
Object.keys(obj);

接下来看看 keys 方法的实际使用。

1
2
3
4
5
6
js复制代码const book = {
title: "高级",
price: 99,
};

console.log(Object.keys(book)); // ['title', 'price']

虽然经过 Object.keys 方法处理后,返回结果的类型是数组,且值为对象的属性,但元素的个数没变,由原来的键值对变为键,这是仍属于改造的过程。

过滤

我定义的映射,包含删除有限集合中的某些或某个元素、是”少”的操作。

我们先来看看数组的 filter 的方法。

1
2
js复制代码// 语法
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg]);

从一组数找出大于 9 的数,请看下面的例子。

1
2
3
4
5
js复制代码const numbers = [12, 23, 56, 78, 1, 6];

const result = numbers.filter((num) => num > 9);

console.log(result); // [12, 23,56,78];

filter 方法会过滤出符合判定条件的元素,用新数组的形式返回结果。元素的数量减少,

接着,我们来实现过滤对象属性的 filterObj 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码const blog = {
title: "有限集合的添加、映射及过滤",
author: "qinghuanI",
};

const filterObj = function (obj, key) {
return Object.entries(obj)
.filter((item) => item[0] !== key)
.reduce((prev, cur) => {
prev[cur[0]] = cur[1];
return prev;
}, {});
};

console.log(filterObj(blog, "title")); // { author: "qinghuanI" }

基石之 for 循环

经过创建、映射和过滤三部分的讲解,我们可以看出,push、map、filter、filterObj 等方法的实现中都使用了 for 循环。

由此可以总结,for 循环控制语句是实现有限集合操作的基石。

Underscore、Lodash 与 Ramda 比较

Underscore 是一个 JavaScript 实用库,提供了一整套函数式编程的实用功能,但是没有扩展任何 JavaScript 内置对象。

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。Lodash 消除了处理数组,数字,对象,字符串等的麻烦,使处理数据变得更容易。相比 Underscore,Lodash 支持模块化使用。

Ramda 是一款实用的 JavaScript 函数式编程库。侧重函数式使用风格。

下表是三个库之间的对比

名称 类型 模块化 函数式
Underscore Collection、Array、Function、Object、Util 否 是
Lodash Array、Collection、Date、Function、Lang、Math、Number、Object、Seq、String、Util、Properties、Methods 是 是
Ramda List、Function、Logic、Object、Relation、Math、String 是 是

Lodash 可以处理更多类型的数据、并且支持模块化,推荐在实际的项目中使用 Lodash。

总结

那么对有限集合的操作就是”多、变、少”这三个哲学。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…141516…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%