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

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


  • 首页

  • 归档

  • 搜索

LC串联谐振拓扑仿真建模

发表于 2024-04-25

直流高压电源主要应用于高端精密分析仪器、高端医疗分析仪器、静电应用、激光雷达、核探测、惯性导航、雷达通信、电子对抗、高功率脉冲、等离子体推进等行业领域。

LC串联谐振拓扑是直流高压电源中最为常用的拓扑结构。上一期内容中我们对 LC 串联谐振变换器的工作原理进行了分析,今天继续为大家分享 LC 串联谐振变换器的仿真建模及控制策略分析。

根据开关频率 f~ s ~ 与谐振频率 f ~ r ~ 的关系,变换器有三种工作模式,而实际应用时一般工作在 DCM 模式(0< f ~ s ~ < 0.5f ~ r~)。这里我们将对电路参数进行设计,并使用 Simulink 软件搭建LC串联谐振变换器模型,对电路 DCM 模式进行仿真。

一、电路设计

01、电路拓扑设计

LC 串联谐振拓扑包括: 原边 LC 全桥串联谐振电路、变压器和副边整流电路。

副边电路常用的有全桥整流电路以及倍压整流电路,这里以副边整流采用全桥整流电路为例,电路拓扑结构如图所示:

02、电源技术指标设计

❏**输入电压 ** v~ in~ : 100V(95~105)

❏**充电电压 ** v~ o~ **:**1000V

❏**充电时间 ** t : 1s

❏**负载电容 ** c~ d~ : 500μF

❏**最大工作频率 ** f~ smax~ **:**10kHz

03、器件参数设计

▍变压器变比N设计

V~ omax~

N ~max ~ = ——————

V~ inmin~

V~ omin~

N ~min ~ = ——————

V~ inmax~

这里变压器变比选取 N=10

▍谐振频率设计

电路工作在 DCM 模式下 0<f~ s<0.5f r,f ~ r ~ = 2fsmax ~= 20kHz

▍谐振电感与谐振电容设计

根据上式可以解得 L ~ r~ =1.1mH,C ~ r~ =6.9μF。

二、电路仿真

01、电路模型搭建

目前,电路仿真软件很多,本次我们采用Matlab中的可视化电路仿真软件包 Simulink 进行电路模型搭建。

Simulink 被广泛应用于线性系统、非线性系统、数字控制及数字信号处理的建模和仿真中。

接下来就让我们一起进行 LC 串联谐振变换器电路模型搭建。

▍启动 Simulink

打开 Matlab 软件,启动 Simulink;

▍模块****器件选择

点击“ 模块库浏览器 ”图标进行器件选择。

以直流电压源为例,搜索“Elec trical Sources”,选择“DC Voltagte Source”,拖拽至模型搭建界面;

▍参数设置

双击器件进行参数设置。

以直流电压源为例,双击电压源图标会弹出参数设置界面,填入输入额定电压值“100”V即可

▍电路模型

重复上述步骤进行器件选择与参数设置后,按照电路拓扑结构对器件进行连接,得到的LC串联谐振变换器模型如图:

02、开环调试

电路模型搭建完成后,在输入与输出端添加传感器模块,并接入示波器模块中进行波形观察;然后搭建 PWM 波形产生电路并输入至开关器件端。

开环调试电路如图所示:

此处 PWM 控制方式为调频控制,通过改变开关频率达到调节输出电压的目的。

首先设置 PWM 开关频率为 1kHz,占空比为40%,可以看到输出电压幅值在1200V左右;然后设置开关频率为 5kHz,可以观察到输出电压为350V左右。

如此,电路输出电压波形符合预期,且可通过改变开关频率实现输出电压调节,符合电路控制规律。

03、闭环调试

这里闭环采用 PI 控制方式,电路设计如图:

点击“运行”按钮进行拓扑电路的闭环调试,点击波形采集窗口可以观察到输出电压波形如图。

这里设置的闭环输出电压为1000V,可以看到输出电压最终稳定在1000V,符合变换器设计要求。

到这里,LC 串联谐振变换器的电路设计与仿真已经完成了,电源的输出基本符合预期。

本文转载自: 掘金

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

css奇技淫巧——优雅的消除height:100%继承地狱

发表于 2024-04-25

前言

在开发过程中,我们常常会遇到需要设置一个元素的高度为其祖父祖父元素高度的100%的需求。

如果按常规方法:

image.png
这代码,优雅与否咱先不说,但凡有点代码洁癖,一定看着很难受吧

这篇博客将演示如何优雅的解决height: 100%继承地狱

正文

解决这个问题的关键在于:

绝对定位元素的高度是相对于其最近的定位祖先元素,而不是其直接的父元素。`

首先我们将有原始高度的.app元素设置为定位元素

1
2
3
4
5
6
7
css复制代码.app {
height: 100px;
width: 100px;
border: 1px solid;
/* 关键代码 */
position: relative;
}

然后将目标元素设置为绝对定位元素

1
2
3
4
5
6
7
css复制代码.div4{
height: 100%;
border: 1px solid red;
/* 关键代码 */
position: absolute;
width: 100%
}

最终效果

image.png

这个技巧允许我们优雅地解决height: 100%的继承问题,避免了不必要的复杂性和代码混乱。虽然这个技巧并不能适用于所有的场景,但在大多数情况下,它能够提供一个快捷和有效的解决方案。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script>
</head>
<body>
<div class="app">
<div class="div1">
<div class="div2">
<div class="div3">
<div class="div4"></div>
</div>
</div>
</div>
</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码.app {
height: 100px;
width: 100px;
border: 1px solid;
/* 关键代码 */
position: relative;
}
.div4{
height: 100%;
border: 1px solid red;
/* 关键代码 */
position: absolute;
width:100%;
}

知识扩展

首先我们了解一下隐式高度和显式高度

显式高度:当你通过 CSS 直接为一个元素设置高度时,这个高度就是显式高度。显式高度可以通过像 “height” 或 “max-height” 这样的 CSS 属性来设置。

例如:

css

1
2
3
css复制代码.box {
height: 150px;
}

在这个例子中,在类名为 “box” 的元素中显式地设置了高度为150px。

隐式高度:如果没有设置元素的显式高度,或者如果元素的高度设置为 “auto”,那么元素的高度将依赖于元素中的内容来决定。这就是所谓的隐式高度。

例如:

html

1
2
3
xml复制代码<div class="container">
<p>这是一段文字。</p>
</div>

在这个例子中, “.container” 的高度将取决于其中的 <p> 标签内容的高度,因此它具有隐式的高度。

最简单,高效判断显隐高度的方法

打开控制台,样式查看切换至Computed

image.png

只要height为灰色,则为隐式高度。

我们解释这么久显隐高度,为了什么?因为隐式高度,是无法被继承的。

这时候我们也可以用上诉方法来解决这个问题

image.png

感谢您的阅读,有不足之处请为我指出!

本文转载自: 掘金

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

AI代码建议与编辑器代码建议我全都要 前言 只有空格或者回车

发表于 2024-04-25

前言

我的上一篇文章《在大公司工作之后才真正领悟到它真的是宇宙级编辑器》在组内受到广泛好评。小伙伴们也和我交流了一些vscode的使用问题,其中一个问题令我印象深刻。

他在使用我司自研ai编码助手(vscode插件)的时候无法快速选取自己想要的建议,因为我自己有一段时间不在一线编码了,所以对目前火热的提高编码效率的ai插件没什么过多了解

为了具有普遍性,我选择了vscode插件市场的某款ai插件,发现也有上述小伙伴说到的问题,下面记录了具体细节和解决方案

只有空格或者回车等少数按键才可触发ai建议

我的目的是用document获取id为focus-area的元素,按下字母d,结果发现没有任何的ai建议,如下图

b1.gif

通过按下esc键取消vscode提供的代码建议小组件我发现其实是有ai代码建议的

而且确实是我想要的,比vscode内置的代码建议小组准确多了!如下图

b2.gif

问题定位与解决

我尝试继续输入,发现ai建议又没了。

基本可以确定是vscode的内置代码建议小组件优先级较高,覆盖了ai插件的行为。

为什么会这样呢?

按理说代码建议小组件展示它自己的,ai插件展示它自己的,井水不犯河水。

仔细看代码提示小组件会发现它默认选择了第一个代码建议,应该是这个原因。

那么只要让它默认什么都不选中,应该就可以解决。

巧了,我正好知道vscode有这么个配置

1
2
3
4
5
6
7
8
9
js复制代码/*
控制在显示小组件时是否选择建议。
请注意,这仅适用于
(“#editor.quickSuggestions#”和“#editor.suggestOnTriggerCharacters#”)自动触发的建议
并且始终在显式调用时选择建议,例如通过“Ctrl+Space”。

never: 自动触发 IntelliSense 时,切勿选择建议。
*/
"editor.suggest.selectionMode": "never",

如下图,会同时出现代码建议小组件和ai代码建议,按下tab成功输出了ai建议的代码

b3.gif

“完美”!!!

天真的我在多输入一些代码体验的时候就被啪啪打脸,如下图。

我在输入字母d的时候我希望的是调用deep函数,此次是内置建议小组件更加准确。

b4.gif

有人会想这不已经解决了吗,你按下箭头直接选择内置小组件的建议有什么问题?

你真的连一下手指头都不想动吗???那你干脆开发个意念编程,赛博飞升算了!!!

不不不,该动还是要动,但是应该一步到位才对,上图只补全为deep,应该是ai建议又被覆盖了,直接deep(obj)才完美。

这种场景应该是内置代码建议与ai建议结合起来才更好。

其实仔细观察会发现,每当焦点移到了内置建议小组件选项的时候,编辑器上那些灰色的建议代码都没有了,如果取消了这个行为应该就能解决这个问题。

巧了,我正好知道vscode有这么个配置

1
2
js复制代码// 控制是否在编辑器中预览建议结果
"editor.suggest.preview": true

哈哈,”完美解决”,如下图

b5.gif

然后当我按下tab的时候傻眼了,如下图。

b6.gif

清清楚楚的写着使用tab接受建议啊,实际效果好像是接受了内置小组件的建议

image.png

于是我直接找到vscode的快捷键配置界面,没有问题啊

image.png

难道是因为快捷键冲突?我尝试改为了其他快捷键,均没有作用。后来又折腾半天都没有解决。

去卫生间时,看到一个人满身疲惫,眼里没有光,丝毫没有xx范!噢,原来那是我啊o( ̄︶ ̄)o

晚上回家躺在床上突然想起了这个问题还没解决,虽然理想很美好,但是vscode及ai建议插件都不是我开发的,我又能如何呢?

偏头看向窗外,我不过就像点点繁星中的某颗毫不起眼的一员罢了,哪能散发极致的光与热,有什么用?

……对啊,有什么用?我改为了其他快捷键有什么用?

没用!即有用!说明不是快捷键被占用,而是快捷键根本没生效。

我记得之前好像看到这个快捷键有很长的when表达式,是不是这些限制条件导致不生效?

我直接跳下床,去查看这个快捷键的具体信息,果然发现了根本原因,如下图。

tab生效的其中一条限制就是!suggestWidgetVisible,翻译过来就是不能有建议小组件

image.png

所以解决方案就是去除这条限制,在键盘配置文件里添加如下配置

image.png

1
2
3
4
5
6
7
8
9
10
js复制代码{
"key": "tab", // 可以根据需要改为自己想用的键,个人觉得tab最好,不用动
"command": "editor.action.inlineSuggest.commit",
"when": "inlineSuggestionHasIndentationLessThanTabSize && inlineSuggestionVisible && !editorHoverFocused && !editorTabMovesFocus"
},
{
"key": "tab",
"command": "-editor.action.inlineSuggest.commit",
"when": "inlineSuggestionHasIndentationLessThanTabSize && inlineSuggestionVisible && !editorHoverFocused && !editorTabMovesFocus && !suggestWidgetVisible"
}

效果如下,tab即可接受ai建议,如果就想用建议小组件的代码,按下enter就行

b7.gif

更多示例,个人觉得ai建议比内置建议小组件更加准确,内容更多

b8.gif

b9.gif

总结

简单体验了下,发现这款插件挺好用的,也无需什么配置。所有人都可以下载使用,免费,不会出现与付费用户的区别对待。不限于vscode及webstorm的多平台支持等

为了避免打广告的嫌疑,就不具体透露了。

如果读者朋友对ai编码插件感兴趣的话可以在评论区里交流学习。合适的话我会把有用的信息补充到这篇文章里

可能有人会觉得,就一编辑器,至于吗?

当我做完这一切。走到阳台,看着那片璀璨的星空,我终于明白了,那理由便是所有生命与生俱来被赋予的能力——为了所爱之物竭尽所能!

请陪我一同仰望这片星空吧。

本文转载自: 掘金

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

使用 Python接入 OpenAI API,实现简单的对话

发表于 2024-04-25

利用OpenAI 的API 可以做好多事情,今天就用简单的代码来解密一下,如何使用Python 来对接OpenAI 的API,实现一些简单的文本对话生成的功能,学习完OpenAI的API对接方法,以后遇到其他对话大模型的时候就可以触类旁通了。

前言

参考官方文档:

platform.openai.com/docs/guides…

今天对接文档中的OpenAI的文本生成模型(Text generation models),就是著名的ChatGPT系列。

需要提前安装两个Python第三方包

1
复制代码pip install openai python-dotenv

再新建一个.env文件填写自己的API Key

1
markdown复制代码OPENAI_API_KEY=sk-******************IQV

同目录下新建一个Python文件,最简单的实现方式,核心代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码from openai import OpenAI
from dotenv import load_dotenv, find_dotenv

_ = load_dotenv(find_dotenv())
client = OpenAI()

response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]
)
print(response) # 打印的是一个对象
print(response.choices[0].message.content) # 打印的是一个具体的回复的内容
1
2
python复制代码ChatCompletion(id='chatcmpl-9HtS3CKCgSyOjyxhTfuY8TEjEOUAG', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='The World Series in 2020 was played at Globe Life Field in Arlington, Texas.', role='assistant', function_call=None, tool_calls=None))], created=1714051759, model='gpt-3.5-turbo-0125', object='chat.completion', system_fingerprint='fp_c2295e73ad', usage=CompletionUsage(completion_tokens=18, prompt_tokens=53, total_tokens=71))
The World Series in 2020 was played at Globe Life Field in Arlington, Texas.

在可以增加model="gpt-3.5-turbo",后增加一行response_format={ "type": "json_object" }, 可以返回json格式响应内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"role": "assistant",
"content": "The World Series in 2020 was played at Globe Life Field in Arlington, Texas."
}
}
],
"created": 1714051759,
"id": "chatcmpl-6p9XYPYSTTRi0xEviKjjilqrWU2Ve",
"model": "gpt-3.5-turbo-0125",
"object": "chat.completion",
"usage": {
"prompt_tokens": 18,
"completion_tokens": 54,
"total_tokens": 72
}
}

OpenAI API 响应可以看作是一个 JSON 对象,包含以下主要字段:

  • choices: 模型生成的回复列表,通常只有一个元素,但可以通过参数 n 进行控制。每个元素包含以下信息:
+ **finish\_reason:** 回复结束的原因,例如 `stop` 表示模型已经生成完整回复。
+ **index:** 当前回复在 choices 数组中的索引。
+ **message:** 回复内容,包含以下两种类型:


    - **role = "user":** 用户输入的内容。
    - **role = "assistant":** 模型生成的回复。


        * **content:** 文本内容。
        * **function\_call:** 未来可能支持的函数调用,目前保留。
  • created: 回复生成的时间戳。
  • id: OpenAI 内部使用的标识符。
  • model: 使用的模型名称。
  • object: 固定值为 “chat.completion”。
  • usage: 本次请求消耗的 token 数,用于计费。
+ **prompt\_tokens:** 输入 prompt 中的 token 数。
+ **completion\_tokens:** 模型生成回复中的 token 数。
+ **total\_tokens:** 总 token 数,等于 `prompt_tokens` + `completion_tokens`。

也可以模仿OpenAI官网的传输

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码from openai import OpenAI
from dotenv import load_dotenv, find_dotenv

_ = load_dotenv(find_dotenv())
client = OpenAI()

stream = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Say this is a test"}],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
print(chunk.choices[0].delta.content, end="")

最后

注意的一点的是对接OpenAI的API需要相应的网络环境,下一期会继续研究OpenAI API的其他功能,讲一下API中的角色和函数调用,敬请期待~

本文转载自: 掘金

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

为什么需要架构师

发表于 2024-04-25

在聊架构师这个角色之前,我们得先搞清楚一件事:行业里对这个职位的看法其实挺模糊的。回顾一下,过去在一些大公司,有那么一段时间,架构师被视作一个专职的角色。但现在,情况有所变化,这个称呼渐渐退回到了“工程师”、“专家”或“研究员”这类更加技术性的职位名称里。换句话说,那些曾经被冠以“架构师”头衔的人,现在可能更多的是以工程师或研究的身份出现。

但这并不意味着架构师这个角色就消失了。事实上,在我的个人工作经验中,遇到的所谓“架构师”五花八门。特别是在一些小团队中,项目经理可能也会自封为架构师。这里的“架构师”,更多的时候不是一个官方职位,而是根据项目需要,某人暂时扮演的一个角色。

如果你想了解架构师到底是什么,先得接受一个事实:在当前的技术领域,架构师这个角色还没有一个清晰且统一的定义。它更像是一个根据项目情况变化的角色,而不是一个固定的职业路径。这也就意味着,成为一个架构师,与其说是达到某个职位的高度,不如说是在特定情境下,扮演的一个必要角色。

1、架构师的定义

架构师:任何复杂结构的设计人员。

架构师这个概念是从建筑业借鉴过来的。实际上,如果我们将“Software Architect”直译成中文,它意味着“软件建筑师”。这不仅仅是一个简单的名字借用;在很多方面,软件架构师的角色确实与建筑师有着相似之处。为了深入理解这种联系,我曾经翻阅了不少关于建筑设计的书籍(比如,《建筑的永恒之道》是一本极好的参考资料),通过这些学习,我发现软件架构与建筑设计之间不仅有着历史上的联系,它们的发展轨迹在某些方面也可能朝着相同的方向前进。

  • 一脉相承:无论是传统的建筑师还是现代的软件架构师,他们的核心职责都是为了构建一个宏大的设计蓝图,确保在需求方和实施团队之间架起一座沟通的桥梁。
  • **分道扬镳:**这种分歧主要是因为两个领域发展阶段的不同。建筑行业有数千年的实践历史和几百年的理论基础,已经发展成为一个高度模式化的领域。相比之下,软件架构作为一个领域的历史还不足二十年,仍然处于快速发展和变化之中。在这个阶段,软件架构师更多的是关注于技术的选择和实现方式,而不是设计的美感,这也是为什么软件架构师通常被看作是高级工程师,而不是设计师。
  • 殊途同归:尽管如此,计算机科学的发展历程也证明了技术的持续抽象和模式化。从面向服务的架构(SOA)到物联网(IoT),再到“如果这个,那么那个”(IFTTT)的编程理念,我们已经开始看到软件领域向着建筑业已经达到的模块化水平迈进。随着技术的发展,软件架构师的工作越来越多地涉及到决定“要做什么”,而不仅仅是“怎么做”。这种变化预示着,未来软件架构师可能真正成为一个关注设计本身的职业,大学中甚至可能开设专门的“软件架构”专业。

当然,要实现这样的转变,我们这一代技术人员面临着巨大的挑战。我们需要像建筑行业的先驱那样,不断地规范化技术实践,形成设计模式,同时还需要建立一套既考虑架构美学又不忽视功能设计的统一标准。这是一条漫长而艰难的道路,但正如建筑领域所展现的那样,通过不懈努力,最终能够达到的成就是无限的。

2、架构师的职责

在软件行业的早期,”架构师”这个职位并不存在。那时候,大家都是程序员,也许会有一个领头的,称之为”主程序员”。但随着时间的推移,计算机技术飞速发展,软件开始渗透到生活的方方面面,不仅覆盖面广,而且复杂度大增。现在,拥有数百万甚至数千万行代码的软件系统已经变得司空见惯。随着软件日益复杂,开发者面临的挑战也与日俱增,因为人脑处理信息的能力终究是有限的。为了应对这些挑战,软件开发工具和方法也在不断进化,从汇编语言到高级编程语言,从基本的函数编程到复杂的框架,从面向过程到面向对象,从设计模式到架构模式,这一切都在展示着人类在软件工具开发上不断追求”封装”和”抽象”。

在这个抽象和封装的进程中,架构设计可谓达到了顶峰。作为架构师,不再需要过分纠结于编程语言、函数或设计模式等具体细节,而是要从一个更高的视角,全面考虑整个软件系统的设计,确保技术方案的合理性、需求的完整实现,以及与商业目标的契合度——这些构成了架构师的技术职责。

随着行业的不断发展,软件项目参与的角色和人员也变得越来越多样化,不仅仅局限于程序员和需求方,还扩展到了技术、产品、设计、商务、项目管理等多个团队。同时,技术团队内部的分工也越发细化,形成了前端、后端、测试、运维、技术支持等多个专业领域。在这种背景下,架构师成为了技术团队与产品、设计等非技术团队之间的桥梁,负责协调不同团队间的沟通,确保技术与业务的有效结合。作为技术团队的领导者,架构师需要勾画出整个项目的蓝图,明确各个环节的边界,引导各个专业领域的团队成员协同工作,共同完成软件系统的构建和发布——这就是架构师的组织职责。

2.1、架构师的技术职责

讨论软件架构师和建筑师的角色时,我们常常会发现两者之间存在着引人入胜的相似性和关键性的差异。这种比较不仅帮助我们理解软件架构师的角色,还揭示了软件开发过程中的独特挑战和机遇。

让我们来看看那两个在建筑领域根深蒂固,但在软件架构界至少目前不完全适用的基本理念:

  • 职业路径的差异:在建筑领域,成为一名建筑设计师通常不需要经历建筑工人或工程师的角色。相反,软件架构师的成长路径几乎总是从软件工程师开始的,通过深入实践中积累经验和技术深度,逐渐演化成为能够担当架构设计重任的专家。这种差异反映了软件行业对于实际编码和项目经验的高度重视。
  • 职责范围的差异:建筑学与工程学之间存在明确的分工——建筑师负责概念化设计,即决定要建造什么,而工程师解决实现问题,即如何建造。软件架构师则通常需要兼顾这两方面,他们不仅定义软件的功能和外观,还必须深入到技术实现的关键部分,确保设计的可行性和实用性。

这两个差异引出了软件架构师的三大技术职责,主要分为三大块:抽象设计、非功能设计以及关键技术设计。每一项都对成功的软件开发至关重要。

抽象设计的艺术:架构师的任务是在不同的抽象层次上自由地分析需求,每个层次或视角都为我们提供了一个独特的视图。这些视图不仅相互验证,而且共同组成了一个完整的设计蓝图。抽象设计可以从两个维度来看:

  • 垂直维度:这里我们从顶层的企业架构到底层的系统架构,分别关注不同层面的需求和决策。比如,CTO更关心企业架构,因为它关系到公司整体的IT战略方向;产品经理和运维团队则更关注应用架构,涉及产品的业务流程和部署问题;而研发团队则深入到系统架构,专注于具体系统的设计和框架。
  • 水平维度:针对特定业务,架构设计可以进一步细化为业务架构、数据架构、技术架构和应用架构。这些视角涵盖了从业务流程分析到技术选型的全方位设计。架构师和产品经理合作确定业务的核心领域模型;数据架构师设计数据模型;技术架构师选定技术栈;应用架构师规划应用的架构布局。

这样的划分使得每个角色都能在其专业领域内发挥最大的作用,同时确保整体设计的协调一致。架构设计的目的是为了确保技术解决方案能够精准地匹配业务需求,正如不同类型的桥梁设计师面对的挑战各不相同,软件架构的设计也需要根据业务领域的特性来定制。每个业务领域的独特性要求架构设计必须具有灵活性和创新性,以实现最佳的业务支持。

非功能需求的分析:架构的真正价值体现在对非功能性需求的满足上。这不仅仅是关于软件能做什么,更重要的是它如何做得好。我们谈到的非功能性需求包括软件系统的可靠性、扩展性、可测性、数据一致性、安全性和性能等方面。在真实世界的约束条件下,如成本、运行环境的限制,往往难以同时满足所有这些需求。

这就要求架构师进行精细的权衡。例如,在算法设计中可能需要在时间和空间之间做出选择,或者在系统性能和可靠性之间找到平衡点。有时,这种权衡甚至触及到学术领域,例如CAP理论就是关于在一致性、可用性和分区容错性之间做权衡的经典案例。架构师的工作就是在这些多维度的需求中找到最优解,确保系统在满足核心需求的同时,保持良好的性能和可用性。

关键技术设计:架构师的角色并不仅限于宏观设计。正如建筑师不仅关心建筑的整体外观,还会深入到细节设计一样,软件架构师也需要关注那些对系统整体质量有重大影响的关键技术细节。拿高迪的巴塞罗那圣家堂为例,连一把椅子的设计都不放过,每个细节都被赋予了深思熟虑的考虑。

在软件架构中,这意味着对系统中的关键组件进行详尽的设计,不仅是功能实现,更包括如何实现这些功能的具体技术选型、性能优化、安全策略等。架构师需要深入到系统的内部,确保每一个关键点都经得起考验,无论是在系统扩展、数据处理还是安全性方面。通过这样的细节关注,架构师确保软件不仅在今天有效,也能面对未来的挑战。

2.2、架构师的组织职责

架构师,作为企业中的一个核心角色,担当着“边界人”的重要职责。他们不仅是技术决策的制定者,也是不同角色和团队之间沟通协调的桥梁。

架构师与业务、产品团队的合作

在现实世界里,每个软件系统背后都有一个问题需要解决。简单地说,这就是软件存在的理由。但问题的解决并不只是随便写写代码就行,而是需要深入理解业务本身。这就是为什么,当一个软件的商业模式明确后,架构师要和业务、产品团队紧密地工作在一起。他们的目标是什么呢?是确定软件系统应该如何支撑业务,也就是说,他们需要设计出一个既能解决当前问题,又能支持未来业务发展的架构和领域模型。

这里的“架构”和“领域模型”其实就是把复杂的业务逻辑分解成一个个更容易理解和实施的部分。这种分解的好坏,直接影响到软件是否只能解决眼前的问题,还是能成为一个真正能随着业务成长的产品。

但要注意,业务和产品团队与架构师之间的关系并不总是那么简单。他们既是合作伙伴,又可能是谈判桌上的对手,尤其是在外包项目中。这时,架构师的角色不仅仅是技术决策者,更是需要在业务需求和技术实现之间找到平衡点的关键人物。简而言之,架构师的任务是确保软件既能满足当前的业务需求,又能灵活适应未来的发展。

架构师与技术团队的合作

在与技术团队的合作中,架构师的角色不仅仅是技术的引领者,更是团队合作的枢纽和策略制定者。直接切入重点,我们看到架构师在研发阶段的作用不仅限于构建技术框架和确定开发边界,还包括对项目中关键的非功能性需求——比如系统的性能、可靠性和安全性——进行精准的设计和实现。这意味着架构师不仅需要具备宏观的视野,将不同的研发团队和业务领域有序地编织在一起,还需要深入到技术细节中,亲自确保这些非功能需求能够得到满足。

在部署阶段,架构师与运维团队的合作变得尤为关键。他们需要共同评估如何在确保系统满足所有预定非功能需求的同时,实现成本和性能的最优平衡。这涉及到复杂的决策过程,如选择合适的硬件资源、决定是否采用CDN以提高性能、如何确保系统的高可靠性以及部署安全策略等。架构师在这一过程中扮演的是策略家和协调者的角色,旨在设计出一个既经济又高效的部署方案。

站在技术团队的角度,架构师的定位呈现出一种动态平衡。一方面,深耕于技术团队让架构师能够更深入地理解产品和业务需求,从而做出更加精准的技术设计和决策。另一方面,保持适当的独立和客观视角使得架构师能够从更宏观的层面审视和规划软件架构,避免过分陷入具体技术细节而失去整体的协调和控制。架构师需要在深入与独立之间找到合适的定位,确保既不脱离技术团队的实际,又能保持必要的全局视角。

除了技术设计和决策,架构师还承担着重要的组织职能——团队培养。架构师通过制定关键技术方案,不仅展示了技术领导力,还为团队成员提供了学习和成长的机会。这要求架构师既要有足够的技术洞察力亲自解决核心问题,又要给予团队足够的空间和信任,让他们在实践中学习和成长,即使这意味着需要承担一定的风险和责任。架构师的这一角色不仅是技术领导者,更是教练和导师,引导团队不断前进,提升技术实力。

综上所述,架构师与技术团队的协作是一场精心设计的平衡游戏,需要架构师在保证技术先进性和系统稳定性的同时,促进团队的协作与成长。架构师必须在技术的深度与广度、团队内部与外部的定位、以及领导与培养之间精准把握,以确保既能实现高效的技术创新,又能维护和促进团队的整体协作和发展。

和其他角色的协作

想象一下,一个架构师不仅仅是坐在电脑前写代码的技术人员,他其实更像是一个大指挥官。他的任务是什么呢?是确保软件项目从开始到结束都能顺利进行。这听起来简单,实际上却涉及到很多方面。

架构师需要和谁合作?首先是产品和技术团队,这个不用说,毕竟软件是由他们一起打造的。但这还不够,架构师还要和项目经理合作,确保项目按时按质完成。还有外部客户,他们是软件的最终用户,架构师需要理解他们的需求。甚至连公司财务部门也逃不过架构师的合作名单,毕竟软件项目的预算和成本也是非常关键的部分。

架构师的角色远不止是技术实施那么简单,他必须与所有相关方保持沟通和协调,从技术方案的角度出发,确保每个人的需求都得到满足。这就是架构师作为技术方案总负责人的真正含义:他是连接所有点的线,确保这些点能够形成一个完整的、成功的项目。

如何沟通

沟通是团队合作的基石,而对于架构师来说,沟通的艺术不仅仅是说话和写字那么简单。他们需要的是一种更高效、更直观的沟通方式——图表。为什么呢?因为图表能够跨越语言和专业的界限,让复杂的概念变得易于理解。

对不同的团队,架构师使用不同的图表作为沟通工具。比如,和产品团队沟通时,架构师会用业务架构图、用例图和领域模型图来说明软件要解决的业务问题和如何解决。这些图表帮助产品团队理解软件的业务价值和功能范围。

当转向研发团队,架构师则切换到应用架构图、组件图和时序图。这些工具帮助研发人员把握软件的内部结构和各部分如何协同工作。

对于运维团队,架构师又会用部署架构图来说明软件如何在实际环境中部署和运行。这样运维团队就能更好地理解和准备所需的资源和配置。

图表的力量在于它们提供了一个共同的语言,让所有人都能理解软件的设计和运作原理,无论他们的专业背景如何。同时,图表还能将设计文档化,便于传承和未来参考,确保软件的长期成功。简而言之,架构师通过使用图表作为沟通的桥梁,不仅促进了团队之间的理解和合作,也为软件的成功奠定了基础。

3、架构师的成长

在探讨架构师的角色时,我们首先要明确一点:架构师的职责直接定义了他们必须具备的能力。这意味着,作为架构师,不仅需要掌握广泛的技术知识,成为一个全面的技术专家,同时还要精通沟通与协作技巧。这样的定位要求架构师在技术领域有深入的理解和广泛的视野,能够看到技术如何服务于业务目标;另一方面,他们还需要具备出色的人际交往能力,能够有效地与团队成员、利益相关者进行沟通和协作,确保技术解决方案的顺利实施。简而言之,架构师的角色是技术与沟通能力的完美结合体,他们在将复杂概念分解成易于理解的部分方面发挥着关键作用,确保所有人都能跟上项目的进展。

所以,如果我们要总结架构师成长的路径,其实可以看作是两个主要方向:

3.1、技术层面

作为架构师,你的主战场是抽象建模,但战斗前的准备不能少,那就是深入了解你的业务领域。只有当你对业务有深刻的理解时,你才能高效地进行抽象和建模,并能够提炼出通用的设计方法。回想起几年前,我看到我们公司首席架构师的书单时,明白了这一点。尽管我们那时仅是金融领域边缘的一家支付公司,他的书单上却涵盖了银行卡组织介绍、零售银行业务分析等领域。

另外,架构师不仅需要理解业务,还得对涉及的技术领域有广泛甚至深入的知识。对于互联网行业的架构师而言,这包括从编程语言、算法、数据库,到网络协议、分布式系统、服务器、中间件、IDC等各个层面。简而言之,架构师既是技术团队的门面,也是解决外部技术问题的关键人。除了技术的广度,深度同样重要,架构师对关键技术模块的设计应具备权威性见解。这样的角色定位,要求架构师既是全面的技术探索者,也是业务领域的深度分析师。

3.2、组织和个人成长层面

架构师站在技术与业务的十字路口,不仅需要精通各自的语言,更要在沟通中架起桥梁。这意味着,架构师的能力远不止于技术深度,还包括能够以口头和书面(特别是通过标准化图表)的形式,清晰、准确地传达设计思路和决策逻辑。这样的沟通技巧对于确保团队成员、利益相关者和客户之间的顺畅交流至关重要。

架构师的工作本质上是一场不断的权衡和平衡艺术,涉及技术选型、团队合作方式、人才培养、任务分配,以及如何在商业需求与成本控制、产品需求与技术能力之间找到最佳匹配点。这种持续的权衡过程不仅展现了架构师的策略思维,也是他们价值的体现。与工程师的角色相比,架构师更需要适应并接受不完美的解决方案和在给定条件下的近似精确,这往往是因为现实世界的复杂性和资源的限制。

从工程师到架构师的转变,意味着从追求代码的完美到追求系统设计和决策的优化平衡。这个过程中,架构师需要发展出对业务敏感性,深入理解业务背后的逻辑和需求,并以此为基础设计出既符合技术发展又服务于业务目标的架构方案。同时,架构师还要在技术前沿不断学习和探索,确保所采用的技术方案既前瞻性又实用,能够支撑业务的长期发展。

本文转载自: 掘金

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

Android Perfetto 监控应用启动耗时

发表于 2024-04-25

Perfetto 是一个 Google 开发的用于安卓系统性能监控和调试的工具,它旨在提供实时数据收集和可视化功能,帮助我们分析和优化应用程序的性能表现。Perfetto 可以捕获系统事件、CPU、内存、网络、GPU 等性能指标数据,并将其记录为轻量级的 Trace 文件,我们可以通过 Perfetto 的可视化界面(ui.perfetto.dev/)或者命令行工具进行查…

此前更多使用的是 Systrace,而 Perfetto 相比 Systrace 有更多的优势。

Perfetto 支持更多的性能指标数据的采集和记录,包括系统事件、内核跟踪、堆栈跟踪等,提供了更全面的性能分析功能。其次, Perfetto 的可视化界面也更加友好和直观,方便我们快速理解和分析数据。

在实际的开发过程中,做启动优化和监控单纯靠 Traceview 是不太准确的,因为 Traceview 获取的信息比较局限,而 Perfetto 能站在上帝视角来查看应用的启动过程。

接下来我举一个使用 Perfetto 的 ADB 命令来监控应用启动耗时的例子

在应用启动的时候故意写一个耗时(渲染一个布局800次):

1
2
3
4
5
6
7
8
9
js复制代码class MyApp : Application() {

override fun onCreate() {
super.onCreate()
for (i in 0 until 800) {
LayoutInflater.from(this).inflate(R.layout.activity_main, null)
}
}
}

把应用安装到手机上,杀掉要监控的应用的进程,回到桌面,准备工作就算完成了

1f6e85091e14452d84e4cfbac9b2ff2a.png
使用 adb 命令开启监控:

adb shell perfetto -o /data/misc/perfetto-traces/trace_file.perfetto-trace -t 5s sched freq idle am wm gfx view binder_driver hal dalvik res memory

一按回车会进入录制状态:

20e9d14aa23c4235b9611e9e15cbac93.png
在录制状态下打开应用,等待录制结束

34385c44084143eaa72cddd07f98576f.png

录制完成后再使用 adb 命令将 Trace 文件导到本地电脑上:

adb pull /data/misc/perfetto-traces/trace_file.perfetto-trace

将 Trace 文件拖入可视化界面(ui.perfetto.dev/ )它会自动打开

a0ce455349f849dc8e8341f2ca2d26b8.png

确认有 Android App Startups 标识(没有的话重新录制一遍)

展开要监控的包名就能看到启动信息了:

6f8199cd63094e82b98d25e224d05662.png

这个 bindApplication 就是 App 启动初始化的过程了,可见是非常长的,也就是比较耗时。我们将其放大即可看到具体的耗时操作(使用键盘上的 ws 可以缩放,ad 可以左右移动)

45332adfe1274cc3ba9f5f09b5078e84.png

可见上面密密麻麻的 inflate,因为我们在 Application 的 onCreate 中渲染了 800 次布局

我们知道了做了哪些耗时操作后,再根据业务实际情况进行异步等优化处理,这样子我们启动优化的目的就达到了

本文转载自: 掘金

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

一文搞懂 Webpack 和 Vite

发表于 2024-04-25

image.png

前言

Webpack 和 Vite 都是前端工程化工具。Webpack 作为老大哥,在前端社区也算站稳了脚跟,它提供了许多配置和插件,让开发者定制化构建项目。随着Vite 的出现,Webpack 在启动时间和热更新方面的缺陷就慢慢暴露,Vite 逐渐开始替代 Webpack。

Webpack

使用步骤

1. 初始化项目

1
bash复制代码yarn init -y

2. 安装依赖

1
bash复制代码yarn add webpack webpack-cli -D

3. 在项目中创建 src目录,编写代码

4. 打包

1
bash复制代码yarn webpack

配置文件

Webpack提供配置项和插件系统,允许我们在webpack.config.js文件中进行各种配置,从而帮助我们定制化构建项目。

mode

设置打包的模式,production为生产模式,development为开发模式

1
2
3
js复制代码module.exports = {
mode:'production',
}

entry

指定打包入口文件,默认为./src/index.js

1
2
3
js复制代码module.exports = {
entry:'./src/a.js',
}

指定打包入口文件为./src/a.js

1
js复制代码entry:['./src/a.js','./src/b.js']

使用数组可以设置多个入口文件,但是最后始终打包成一个文件

1
2
3
4
js复制代码entry:{
aa:'./src/a.js',
bb:'./src/b.js'
},

对象形式也可设置多个入口文件,但是会打包成多个文件。Webpack 会将value文件打包成key.js文件,比如上述例子会将'./src/a.js'文件打包成aa.js文件、'./src/b.js'件打包成bb.js文件。

output

对打包后的文件进行配置,默认打包文件地址为./dist/main.js

1
2
3
4
5
6
7
8
9
js复制代码const path = require('path')

module.exports = {
output:{
filename: "bundle.js", //打包后的文件名
clean: true, //每次打包前清除上次打包的文件
path: path.resolve(__dirname, 'hello'), //打包后的文件存放路径,必须要绝对路径
}
}

在filename中Webpack提供模版字符串的形式,可以自动生成唯一文件名:

  • filename: "[name].js" 打包多个文件时匹配对应的文件名,[name]与 entry 中的 key 对应。
  • filename: "[hash].js" 会随机生成哈希值,一般用于区分版本。

等等,详情参考官方文档 Output | template-strings

loader

Webpack默认情况下,只会处理 js 文件,如果我们希望它可以处理其他类型的文件,则要为其引入loader

以css为例,如果我们不进行任何操作

1
2
3
4
js复制代码//直接将css引入到js中
import './style/index.css';

document.body.insertAdjacentHTML('beforeend', '<h1>hello webpack</h1>');

直接打包

image.png

报错了,Webpack说读不懂。

我们需要安装解析css文件的loader:

1
bash复制代码yarn add css-loader

配置loader,rules是一个数组,允许我们配置多个loader,test属性是一个正则表达式,用于匹配对应loader的文件,use属性则是匹配对应loader。

1
2
3
4
5
6
7
8
js复制代码module:{
rules:[
{
test:/\.css$/, //匹配文件
use:'css-loader'
},
]
}

配置完毕,成功打包,但是页面上并没有样式,但是打包文件中是有样式的啊,为什么就没有显示到页面上呢?

image.png

因为loader遵循职责单一原则,一个loader只做一件事情,如果你想在打包的文件中使用上css,光有解析loader还不够,还需要安装另一个loader:

1
bash复制代码yarn add style-loader

然后配置style-loader:

1
2
3
4
5
6
7
8
9
10
js复制代码module.exports = {
module:{
rules:[
{
test:/\.css$/, //匹配文件
use:['style-loader','css-loader'] //从后往前执行,有顺序要求
},
]
}
}

这里需要注意⚠️,如果我们配置多个loader,则用数组存放,存放的顺序遵循从右往左,先执行的放右边,否则会报错。这里先编译css再使用,所以顺序为'style-loader','css-loader'。

babel-loader

在编写js代码时,经常需要使用一些js中的新特性,而新特性在旧的浏览器中兼容性并不好。但是我们现在希望能够使用新的特性,我们可以采用折中的方案。依然使用新特性编写代码,但是代码编写完成时我们通过一些工具将新代码转换为旧代码。

Babel 就是这样一个工具,可以将新的js语法转换为旧的js,以提高代码的兼容性。我们如果希望在Webpack支持babel,则需要向Webpack中引入babel-loader。

1
2
3
js复制代码document.body.onclick(()=>{
alert('hello webpack')
})

这里我们使用一个箭头函数,在不做任何处理的情况下进行打包

image.png

打包后发现还是箭头函数。

于是我们安装babel-loader以及一些配置:

1
bash复制代码yarn add -D babel-loader @babel/core @babel/preset-env

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码module:{
rules:[
{
test: /\.m?js$/, //以.mjs或js后缀的文件
exclude: /(node_modules|bower_components)/, //排除node_modules中的文件
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'] //根据目标浏览器或运行时环境自动选择合适的转换规则
}
}
}
]
}

再试试呢:

image.png

成功将箭头函数打包成普通函数。

配置兼容性

我们可以在package.json文件中配置需要兼容的浏览器,详情见 github

1
2
3
4
5
6
7
json复制代码"browserslist": [
"defaults", //默认
"ie <= 11", //ie版本低于11
"last 2 versions", //至少存在两个版本
"> 1%", //市场占有率大于1%
"iOS 7",
]

plugin

plugin 的作用是 Webpack 扩展功能。loader 可以理解为转换器,用于处理模块之间的转换,plugin 则用于执行更广泛的任务,它可以访问 Webpack 的生命周期,在合适的时机执行插件的功能。

举个例子🌰:

我想在打包目录生成html文件,用于访问打包的js文件,我们可以手动创建,但是我们不建议直接操作dist打包目录,我们可以通过plugin自动生成该文件。

安装自动生成html文件的插件

1
bash复制代码yarn add -D html-webpack-plugin

配置plugin

1
2
3
4
5
6
7
js复制代码plugins:[
//自动创建html文件
new HTMLPlugin({
title:'Hello Webpack', //配置html文件的标题
template:'./src/index.html' //以哪个文件为模版创建
})
]

当我们再次打包,就会自动生成包含对应配置的html文件。

image.png

服务器环境

webpack-cli提供了许多命令帮助我们打包运行项目

1
bash复制代码yarn webpack

每次修改源码都需要重新打包,太麻烦了

1
bash复制代码yarn webpack --watch

加上--watch后每次修改源码都会被监听到,并且重新打包。

image.png

但是这个命令不能完美还原项目上线的场景,因为它访问的是文件目录,而不是服务器。

image.png

为此,我们可以安装一个Webpack服务:

1
bash复制代码yarn add -D webpack-dev-server

当我们运行以下命令,它能够将项目部署到一个开发服务器上:

1
bash复制代码yarn webpack serve --open

当我们访问本地8080端口,就能够看到我们的项目。

image.png

⚠️需要注意的是:这个服务它只会将我们的项目打包并运行在这个服务器,但是本地看不到这个dist的,所以当我们项目调试完毕,还需要自己手动打包一下。

快捷命令

我们一般会将常用的命令配置一下,使用起来更方便:

1
2
3
4
5
json复制代码"scripts": {
"build": "webpack",
"watch": "webpack --watch",
"dev": "webpack serve --open"
},

Vite

Webpack 是先打包再运行,而 Vite 开发时并不打包,而是直接采用 ES Module 运行项目,部署的时候再打包,开箱即用。

使用步骤

1. 初始化项目

1
bash复制代码yarn init -y

2. 安装 Vite

1
bash复制代码yarn add -D vite

3. Vite的源码目录就是项目根目录,创建index.html文件,以ES Module的方式引入js文件

1
html复制代码<script type="module" src="./index.js"></script>

4. 在本地5173端口启动一个开发服务器,进行项目调试

1
bash复制代码yarn vite

5. 打包代码

1
bash复制代码yarn vite build

快速创建项目

参考 开始 | Vite 官方中文文档 (vitejs.cn)

1
bash复制代码yarn create vite

然后按照提示操作一步步配置即可!

如果你选的是原生JS,你就会得到一个这样的完整项目路径:

image.png

配置文件

首先在根目录下创建vite.config.js文件,⚠️注意这里的抛出语法与 Webpack 不同,Webpack使用 CommonJS 语法,而 Vite 使用的是 ES Module 语法。

1
2
3
4
js复制代码//webpack
module.exports = {
...
}
1
2
3
4
js复制代码//vite
export default = {
...
}

这里Vite还提供了一个可选配置项defineConfig

1
2
3
4
5
js复制代码import { defineConfig } from "vite";//需不需要提示

export default defineConfig({
...
})

它的作用是在我们写配置的时候会不会有提示。

加了:

image.png

没加:

image.png

对比不难发现区别。

Vite需要配置loader吗?

试验一下:

1
2
3
css复制代码h2{
background-color: pink;
}
1
2
js复制代码import './index.css'
document.body.insertAdjacentHTML('beforeend','<h2>hello vite</h2>');

这里我们在js中引入css样式,然后yarn vite 运行一下

image.png

结果显而易见,vite不用配置loader就能编译和使用css。

插件

Vite 可以使用插件进行扩展,这得益于 Rollup 优秀的插件接口设计和一部分 Vite 独有的额外选项。详情参见 使用插件 | Vite 官方中文文档 (vitejs.cn)

举个例子🌰:

要想为传统浏览器提供支持,类似于Babel,需要引入官方插件@vitejs/plugin-legacy和压缩工具terser。

1
bash复制代码yarn add -D @vitejs/plugin-legacy terser

然后配置插件:

1
2
3
4
5
6
7
8
9
10
js复制代码import { defineConfig } from "vite";//需不需要提示
import legacy from "@vitejs/plugin-legacy";

export default defineConfig({
plugins:[
legacy({
targets:['ie 11'], //兼容IE 11
})
]
})

我们来个箭头函数检验一下

1
2
3
js复制代码document.body.onclick=()=>{
alert('hello vite');
}

yarn vite build一下

image.png

我们可以看到Vite给我们生产了三个js文件和一个html文件,来分析一下:

index.js:

image.png

这个文件中的箭头函数并没有转为普通函数;

index-legacy.js:

image.png

这个文件中的箭头函数成功转成了普通函数;

polyfills-legacy.js:

image.png

这个文件的作用是用于向下兼容旧版浏览器,确保旧版浏览器能够读懂新特性。

index.html:

image.png

这个文件的意图就很明显了,如果浏览器支持ESModule(即现代浏览器),则一般可以支持新语法,因此就可以直接引入新语法文件;但是如果浏览器不支持ESModule,则可能是旧版本的浏览器,它们可能不支持新特性,这时候就需要引入兼容性代码来填补这些功能的缺失。在这种情况下,一般会引入 polyfills,以及旧版文件,来确保应用在旧版浏览器中的正常运行。

vite不像webpack只生成一个降级之后的文件,而是两个都生成,再根据浏览器的兼容性,动态地确定加载哪个版本的代码。对于现代浏览器来说,直接加载现代版本的代码可以获得更快的加载速度和更好的性能。而对于不支持 ES Module 的旧版浏览器,则加载降级版本的代码以确保兼容性。

以上是对兼容性插件legacy的分析,更多插件可参见 插件 | Vite 官方中文文档 (vitejs.cn)

快捷命令

1
2
3
4
5
json复制代码"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},

对比总结

  1. 构建方式: Webpack 通过构建整个项目的依赖图,将所有资源打包成一个或多个 bundle 文件,每次重启都需要打包。Vite 采用了即时编译的方式,在开发模式下通过浏览器原生支持的 ES Module 特性进行加载,不需要打包。
  2. 开发体验: Webpack 需要较多的配置,对复杂的项目来说,需要花费时间和精力来配置各种 loader 和 plugin。Vite 开箱即用,不需要复杂的配置即可快速启动项目,支持各种插件以满足特定需求。
  3. 热更新: Webpack 的热更新通常需要借助 webpack-dev-server 等插件,在一些情况下配置起来比较复杂。Vite 内置了基于浏览器原生模块热更新的开发服务器,无需额外配置即可实现快速的热更新。

最后

码字不易,感谢三连!

已将学习代码上传至 github,欢迎大家学习指正!

技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 “点赞 收藏+关注” ,感谢支持!!

本文转载自: 掘金

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

OKHttp源码解读 HTTP/11

发表于 2024-04-25

HTTP版本发展

HTTP/0.9——单行协议

请求由单行指令构成,只支持GET方法,服务器只能回应HTML格式的字符串,不能回应别的格式,当服务器发送完毕,就关闭TCP连接。

HTTP/1.0——构建可扩展性

  • 请求方式新增了POST,DELETE,PUT,HEADER等方式
  • 增添了请求头和响应头的概念,在通信中指定了 HTTP 协议版本号,以及其他的一些元信息 (比如: 状态码、权限、缓存、内容编码)
  • 扩充了传输内容格式,图片、音视频资源、二进制等都可以进行传输
不足:
  • 无法复用连接
    每次发送请求,都需要进行一次tcp连接(即3次握手4次挥手),使得网络的利用率非常低
  • 队头阻塞
    HTTP 1.0 规定在前一个请求响应到达之后下一个请求才能发送,如果前一个阻塞,后面的请求也给阻塞的

HTTP/1.1

  • 引入了持久连接(persistent connection),连接可以复用,节省了多次打开 TCP 连接加载网页文档资源的时间。
  • 引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。新增了请求方式 PUT、PATCH、OPTIONS、DELETE 等。
  • 支持响应分块。
  • 引入额外的缓存控制机制。
  • 新增了请求方式 PUT、PATCH、OPTIONS、DELETE 等。
  • 支持断点传输,在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率
  • 凭借 Host 标头,能够使不同域名配置在同一个 IP 地址的服务器上。
不足:

虽然1.1版允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为”队头堵塞”(Head-of-line blocking)。

HTTP/2——为了更优异的表现

  • 二进制分帧
  • 多路复用: 在共享TCP链接的基础上同时发送请求和响应
  • 头部压缩
  • 服务器推送:服务器可以额外的向客户端推送资源,而无需客户端明确的请求

HTTP/3——基于 QUIC 的 HTTP

  • 基于google的QUIC协议,而quic协议是使用udp实现的,QUIC 旨在为 HTTP 连接设计更低的延迟。
  • 减少了tcp三次握手时间,以及tls握手时间;
  • 解决了http 2.0中前一个stream丢包导致后一个stream被阻塞的问题;
  • 优化了重传策略,重传包和原包的编号不同,降低后续重传计算的消耗;
  • 连接迁移,不再用tcp四元组确定一个连接,而是用一个64位随机数来确定这个连接;
  • 更合适的流量控制。

OkHttp

OkHttp 是一个默认高效的 HTTP 客户端:

  • HTTP/2 支持允许对同一主机的所有请求共享套接字。
  • 连接池可减少请求延迟(如果 HTTP/2 不可用)。
  • 透明 GZIP 缩小了下载大小。
  • 响应缓存完全避免了网络重复请求。

一次请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
.url(url)
.build();

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {

}

@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {

}
});

请求整体流程

client.newCall(request).execute()开始执行请求,这里的execute()点进去是抽象方法:

1
2
3
kotlin复制代码actual interface Call : Cloneable {
actual fun enqueue(responseCallback: Callback)
}

向前查看client.newCall(request)返回的Call的实现:

1
2
3
4
5
6
kotlin复制代码open class OkHttpClient internal constructor(
builder: Builder
) : Call.Factory, WebSocket.Factory {

override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
}

返回的是RealCall,查看enqueue()的实现:

1
2
3
4
5
6
kotlin复制代码override fun enqueue(responseCallback: Callback) {
check(executed.compareAndSet(false, true)) { "Already Executed" }

callStart()
client.dispatcher.enqueue(AsyncCall(responseCallback))
}

这里涉及两个类:Dispatcher、AsyncCall。Dispatcher是一个调度器,里面有一个线程池ExecutorService实现多线程的调度,maxRequests是并发执行的最大请求数,maxRequestsPerHost是每个主机并发执行的最大请求数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码class Dispatcher() {
var maxRequests = 64

var maxRequestsPerHost = 5

private var executorServiceOrNull: ExecutorService? = null

internal fun enqueue(call: AsyncCall) {
synchronized(this) {
readyAsyncCalls.add(call)

// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
// the same host.
if (!call.call.forWebSocket) {
val existingCall = findExistingCallWithHost(call.host)
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
}
}
promoteAndExecute()
}
}

enqueue里调用promoteAndExecute(),这个方法首先是选取符合条件的请求,未超负载且未执行的请求,然后执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码synchronized(this) {
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val asyncCall = i.next()

if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.
// maxRequests,maxRequestsPerHost未超负载
i.remove()
asyncCall.callsPerHost.incrementAndGet()
executableCalls.add(asyncCall)
// 加入执行队列
runningAsyncCalls.add(asyncCall)
// 加入正在执行的队列
}
isRunning = runningCallsCount() > 0
}

asyncCall.executeOn(executorService)
//最后执行

最后进入asyncCall.executeOn(executorService),调用 executorService.execute(this),execute()的参数是Runable执行的是Runable的run方法,继续查看当前类的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码fun executeOn(executorService: ExecutorService) {
client.dispatcher.assertThreadDoesntHoldLock()

var success = false
try {
executorService.execute(this)
success = true
} catch (e: RejectedExecutionException) {
failRejected(e)
} finally {
if (!success) {
client.dispatcher.finished(this) // This call is no longer running!
}
}
}
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
kotlin复制代码override fun run() {
threadName("OkHttp ${redactedUrl()}") {
var signalledCallback = false
timeout.enter()
try {
val response = getResponseWithInterceptorChain()
signalledCallback = true
responseCallback.onResponse(this@RealCall, response)
} catch (e: IOException) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)
} else {
responseCallback.onFailure(this@RealCall, e)
}
} catch (t: Throwable) {
cancel()
if (!signalledCallback) {
val canceledException = IOException("canceled due to $t")
canceledException.addSuppressed(t)
responseCallback.onFailure(this@RealCall, canceledException)
}
throw t
} finally {
client.dispatcher.finished(this)
}
}
}

这里调用getResponseWithInterceptorChain,然后拿到结果回调,流程结束。

总结:

RealCall的enqueue->Dispatcher的enqueue->promoteAndExecute->AsyncCall的executeOn->run->RealCall的getResponseWithInterceptorChain。

getResponseWithInterceptorChain 解析

上面了解到,最后的流程getResponseWithInterceptorChain进入到这个函数,这个函数也是最核心的部分。

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
kotlin复制代码internal fun getResponseWithInterceptorChain(): Response {
// 一个拦截器列表
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
// 创建一个RealInterceptorChain
val chain = RealInterceptorChain(
call = this,
interceptors = interceptors,
index = 0,
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)

var calledNoMoreExchanges = false
try {
// proceed
val response = chain.proceed(originalRequest)
if (isCanceled()) {
response.closeQuietly()
throw IOException("Canceled")
}
return response
} catch (e: IOException) {
calledNoMoreExchanges = true
throw noMoreExchanges(e) as Throwable
} finally {
if (!calledNoMoreExchanges) {
noMoreExchanges(null)
}
}
}

这里有三个关键部分,第一部分是把拦截器放到列表里,然后生成一个拦截器的责任链,然后就开始链式执行。

这里就不得不说下责任链模式,程序员老王需要度蜜月请假,写假条给组长,组长把他的工作安排给别人,然后批假后给主管审批,主管根据项目进度决定批假后给老板审批,老板审批后还给主管,主管再还给组长,组长再交给组长:

未命名绘图.drawio.png

每个人都有三部分工作:前置,中置和后置。组长的前置是去了解工作安排,中置是自己审批然后交给主管,等审批一圈主管传递回来后,后置是组长根据上级审批的结果在安排叮嘱老王安心渡假并注意门户。

接下来回到代码中,首先从第一个内置的拦截器开始,也就是索引为 0 那个,每次调用proceed,索引加一,也就是执行下一个拦截器:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码val chain = RealInterceptorChain(
call = this,
interceptors = interceptors,
index = 0,
// 下标为0
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)
1
2
3
4
5
6
kotlin复制代码override fun proceed(request: Request): Response {
// Call the next interceptor in the chain.
val next = copy(index = index + 1, request = request)
val interceptor = interceptors[index]

}

RetryAndFollowUpInterceptor 重试和重定向拦截器

索引为零的第一个拦截器是RetryAndFollowUpInterceptor,这里面的前置是为接下来的请求做准备,找到对应请求地址等,找到一个可以承载请求的连接,中置就是交给下一个拦截器,然后等返回结果,失败则选择重试或者重定向,成功返回结果。

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
kotlin复制代码while (true) {
// 前置准备工作
call.enterNetworkInterceptorExchange(request, newRoutePlanner, chain)

// 交给下一个拦截器,如果失败重试
try {
response = realChain.proceed(request)
newRoutePlanner = true
} catch (e: IOException) {
// An attempt to communicate with a server failed. The request may have been sent.
if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
throw e.withSuppressed(recoveredFailures)
} else {
recoveredFailures += e
}
newRoutePlanner = false
continue
}

val exchange = call.interceptorScopedExchange
// 获取请求结果,根据不同结果做出不同响应
val followUp = followUpRequest(response, exchange)

if (followUp == null) {
if (exchange != null && exchange.isDuplex) {
call.timeoutEarlyExit()
}
closeActiveExchange = false
return response
}

val followUpBody = followUp.body
if (followUpBody != null && followUpBody.isOneShot()) {
closeActiveExchange = false
return response
}

response.body.closeQuietly()

if (++followUpCount > MAX_FOLLOW_UPS) {
throw ProtocolException("Too many follow-up requests: $followUpCount")
}
}

BridgeInterceptor 桥接拦截器

BridgeInterceptor主要是补全请求的请求头和元数据,并添加了gzip压缩。

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
java复制代码override fun intercept(chain: Interceptor.Chain): Response {
val userRequest = chain.request()
val requestBuilder = userRequest.newBuilder()

val body = userRequest.body
if (body != null) {
val contentType = body.contentType()
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString())
}

val contentLength = body.contentLength()
if (contentLength != -1L) {
requestBuilder.header("Content-Length", contentLength.toString())
requestBuilder.removeHeader("Transfer-Encoding")
} else {
requestBuilder.header("Transfer-Encoding", "chunked")
requestBuilder.removeHeader("Content-Length")
}
}

if (userRequest.header("Host") == null) {
requestBuilder.header("Host", userRequest.url.toHostHeader())
}

if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive")
}

// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
var transparentGzip = false
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true
requestBuilder.header("Accept-Encoding", "gzip")
}

val cookies = cookieJar.loadForRequest(userRequest.url)
if (cookies.isNotEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies))
}

if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", userAgent)
}

val networkRequest = requestBuilder.build()
val networkResponse = chain.proceed(networkRequest)

cookieJar.receiveHeaders(networkRequest.url, networkResponse.headers)

val responseBuilder = networkResponse.newBuilder()
.request(networkRequest)

if (transparentGzip &&
"gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
networkResponse.promisesBody()) {
val responseBody = networkResponse.body
if (responseBody != null) {
//解压
val gzipSource = GzipSource(responseBody.source())
val strippedHeaders = networkResponse.headers.newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build()
responseBuilder.headers(strippedHeaders)
val contentType = networkResponse.header("Content-Type")
responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
}
}

return responseBuilder.build()
}

CacheInterceptor 缓存拦截器

根据缓存策略,如果命中缓存,直接返回,否则交给下一个拦截器执行,拿到结果后,如果需要缓存就缓存数据。

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
scss复制代码override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
val cacheCandidate = cache?.get(chain.request())

val now = System.currentTimeMillis()

// 缓存策略
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
val networkRequest = strategy.networkRequest
val cacheResponse = strategy.cacheResponse

cache?.trackResponse(strategy)
val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE

if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it.
cacheCandidate.body.closeQuietly()
}

// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(HTTP_GATEWAY_TIMEOUT)
.message("Unsatisfiable Request (only-if-cached)")
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build().also {
listener.satisfactionFailure(call, it)
}
}

// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(cacheResponse.stripBody())
.build().also {
listener.cacheHit(call, it)
}
}

if (cacheResponse != null) {
listener.cacheConditionalHit(call, cacheResponse)
} else if (cache != null) {
listener.cacheMiss(call)
}

var networkResponse: Response? = null
try {
networkResponse = chain.proceed(networkRequest)
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
cacheCandidate.body.closeQuietly()
}
}

// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse?.code == HTTP_NOT_MODIFIED) {
val response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers, networkResponse.headers))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis)
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis)
.cacheResponse(cacheResponse.stripBody())
.networkResponse(networkResponse.stripBody())
.build()

networkResponse.body.close()

// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache!!.trackConditionalCacheHit()
cache.update(cacheResponse, response)
return response.also {
listener.cacheHit(call, it)
}
} else {
cacheResponse.body.closeQuietly()
}
}

val response = networkResponse!!.newBuilder()
.cacheResponse(cacheResponse?.stripBody())
.networkResponse(networkResponse.stripBody())
.build()

if (cache != null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
val cacheRequest = cache.put(response)
return cacheWritingResponse(cacheRequest, response).also {
if (cacheResponse != null) {
// This will log a conditional cache miss only.
listener.cacheMiss(call)
}
}
}

if (HttpMethod.invalidatesCache(networkRequest.method)) {
try {
cache.remove(networkRequest)
} catch (_: IOException) {
// The cache cannot be written.
}
}
}

return response
}

ConnectInterceptor

这个拦截器没有后置,前置完成后直接交给前面的拦截器。

这里关键在于realChain.call.initExchange(realChain),生成一个Exchange:编码解码,是否需要加密,写入数据流,找到可用连接,建立连接。

1
2
3
4
5
6
7
8
9
kotlin复制代码object ConnectInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
val exchange = realChain.call.initExchange(realChain)
val connectedChain = realChain.copy(exchange = exchange)
return connectedChain.proceed(realChain.request)
}
}

CallServerInterceptor

这个拦截器是和服务器交互。主要是 IO 操作。

本文转载自: 掘金

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

git rebase入坑指南(流程图+实验全解)

发表于 2024-04-25

在协同开发时,我们不可避免的需要代码整合,一般来说,merge是我们最先接触的代码整合方法。

但是我最近发现为什么有些人不喜欢用 merge而提倡用rebase了,因为在 merge 之后,commit 历史就会出现分叉,显得非常混乱。如下图:

这种分叉再汇合的结构总会让强迫症的我觉得很不舒服,不知道哪次改动在哪个commit记录中。

听说备受推崇的rebase能够解决这个问题,让commit历史成一条直线更加清晰。

但是,问题来了,我想解决的这个分叉虽然是我造成,但是他为什么会分叉呢?

一、merge为什么会产生分叉?

首先小明创建分支test,在master的基础上进行了一点小改动


与此同时,在小明想要将代码合并到master前,小刚将他的代码合并到了master。

image.png
此时,小明想要将代码合并到master中,发现merge request出现了冲突,于是就回到自己的分支通过merge master into xiaoming 在自己的分支上解决冲突。

image.png
image.png
于是,分叉就出现了。此时紫色的分叉代表着xiaoming分支的改动,灰色的分支代表着master分支的改动。最后通过一个merger的commit进行合并。

无论有没有冲突,commit记录都会出现分叉。如果有冲突的话,最后的merge commit会显示冲突解决的过程;如果没有冲突的话,这里也会有一个无意义的merge commit。

xiaoming merge master后的分支如下:

root同步小明的提交合并小刚的提交merge-commit
也就是说,两个人同时对功能进行开发,如果进行代码整合,就不可避免地会造成commit 历史分叉。 两个人可能看起来还比较清晰,如果是一个大团队十几个人进行开发,那这个混乱程度想想就很绝望。

所以,我想让自己分支的commit历史变成成一条直线,是不是会更加清晰,更方便管理?

于是,我们就需要用到rebase了。

二、对rebase的理解

2.1 rebase的含义

相比较于merge(合并) 这种望文生义,通俗易懂的词语,rebase(变基) 貌似有点的过于生僻而难以理解了。

其实这个翻译还是比较准确的。rebase 的意思是,把你现在所在的 commit 以及它所在的 commit 串,以指定的⽬标分支最新提交为基础,依次重新提交⼀次。

官方的rebase解释如下:
当执行rebase操作时,git会从两个分支的共同祖先开始提取待变基分支上的修改,然后将待变基分支指向基分支的最新提交,最后将刚才提取的修改追加到基分支的最新提交的后面。

2.2 rebase操作流程

在rebase xiaoming onto master中,待变基的分支是xiaoming,基分支是master。两个分支的共同祖先为 同步这个commit。执行这个命令的流程如下:

2.2.0. 原有xiaoming分支上的情况

root同步小明的提交

2.2.1. 提取xiaoming分支上的修改小明的提交

root同步小明的提交

2.2.2. xiaoming分支指向master分支上最新的提交合并小刚的提交

root同步合并小刚的提交小明的提交

2.2.3. 将提取xiaoming分支出来的修改小明的提交,追加到master分支上最新的提交合并小刚的提交后面。

root同步合并小刚的提交小明的提交
从上述rebase的流程图来看,就很容易就能发现为什么rebase不会产生分叉了。

2.3 rebase的缺陷

rebase后远程仓库和本地仓库会出现这样的情况。

  • git远程仓库中分支情况如下:

root同步小明的提交

  • 本地仓库分支情况如下:

root同步合并小刚的提交小明的提交
这个时候你按照原有的习惯直接push是一定会报错的。因为git的push操作默认是假设远端的分支和你本地的分支可以进行fast-forward操作,换句话说就是这个push命令假设你的本地分支和远端分支的唯一区别是你本地有几个新的commit,而远端没有。

所以上面这种情况下是不能进行fast-forwad模式的合并操作的,所以当执行 git push origin xiaoming 命令时会报错。

此时没有其他人在这个分支上进行提交操作,那么可以直接进行强制推送 git push –force origin xiaoming ,–force可以直接理解为用你本地分支的状态去覆盖掉远端origin分支的状态,也就是执行过后,本地的分支什么样,远端分支就什么样

这里切记,一定要确保是在自己的分支上只有自己在开发,不能在主干分支上进行force操作,否则会直接覆盖掉别人已经提交的代码,会被打的。

如果这个分支有多人协同开发,最好不要用rebase避免 push --force, 如果一定要用rebase,那我更推荐另外一种更安全的命令 git push –force-with-lease origin xiaoming 使用该命令在强制覆盖前会进行一次检查如果其他人在该分支上有提交会有一个警告,此时可以避免覆盖代码的风险。

三、rebase实践

现在让我们回到最初的起点,reset xiaoming分支到未merge之前。同样的小明和小刚,同样的xiaoming和master,此时master上已经合并了小刚的提交。

xiaoming对master进行rebase,需要checkout xiaoming 然后 rebase xiaoming onto master

image.png
image.png
需要注意的是rebase仍然需要进行冲突解决(选择merge):

image.png
rebase生成了新的commit

image.png
此时需要注意的点在于本地已经将远程的记录给更改了,一定要要push –force才能推送上去

image.png
push -force

关于git rebase的操作解决 push时被拒绝,整体流程_git push rebase-CSDN博客

git rebase后无法push远程分支的问题解决_git rebase master后git push-CSDN博客

image.png
image.png
四、rebase与merge的争论


git pull –rebase的作用是什么,它与git pull有什么区别?-CSDN博客

Git 两个常用命令:Git Pull和Git Rebase|极客教程 (geek-docs.com)

总结:使用 rebase 来将远程的变更整合到本地仓库是一种更好的选择。(仅限自己的分支)

  • 用 merge 拉取远程变更的结果是,每次你想获取项目的最新进展时,都会有一个多余的 merge 提交。
  • 而使用 rebase 的结果更符合我们的本意:我想在其他人的已完成工作的基础上进行我的更改。

参考文章

深入理解git rebase - 知乎 (zhihu.com)

git rebase详解(图解+最简单示例,一次就懂)-CSDN博客

详解git rebase,让你走上git大神之路 - 知乎 (zhihu.com)

Git 分支整合之道:Merge 与 Rebase 的理念碰撞与实践指南 - 掘金 (juejin.cn)

本文转载自: 掘金

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

Android开发——1套灵活收发广播组合拳,打遍应用层所有

发表于 2024-04-25

为何要收藏这篇干货?——番茄憨憨的自我独白

我是番茄憨憨,我要坦白。

伴随着Android版本升级,某些静态广播过滤已受到限制,而动态广播又完全依赖于应用的启动,这就导致在实现一些需求时显得比较呆板,如:
全局监听无探网的WiFi时,将给到用户一个友好的Dialog提示。

针对于这个需求,如果只是在应用层添加广播,如果此时应用不启动,又检测到了此时连接的wifi为无探网,显然友好的dialog并不会弹出,这很不友好.

063993e59dfddbe27b2228ee9f6175f7.jpg

那么到底该怎么做呢?一套灵活收发广播组合拳带给大家,打遍应用层所有dialog开发需求.

先上结论:

    1. framework层系统服务中添加广播
    1. 应用AndroidManifest.xml注册广播接收器
    1. 实现透明主题Activity及dialog

注:关于正文的分析我会围绕为什么、怎么做去进行,而大家的思考可以反其道而行之,从他是怎么做的、为什么这样做、做的正确吗?有更好的方案欢迎大家在下方评论点评,让我们一起进步!

1、framework层系统服务中添加广播

1.1. 为什么要在fraemwork层系统服务中添加广播?

  如上面独白中所说,只是在应用层添加广播,静态广播存在局限性,有些IntentFilter存在限制,而动态广播应用没起来的情况下,是不生效的,无法实现一个全局监听数据变化的效果,而在框架中的系统服务中添加广播,因系统服务依赖于系统,即可做到一个实时监听的效果.

1.2. 相关代码添加

a.在服务中注册广播(某些服务中已经注册了广播,我们可以直接添加过滤器即可,以AudioService为例,这里将演示完整的注册代码)

1
2
3
4
java复制代码/*
* 1、定义AudioService的广播
*/
private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
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
scss复制代码/*
* 2、IntentFilter的添加和广播注册封装成一个方法
*/
private void initExternalEventReceivers() {
mSettingsObserver = new SettingsObserver();

// 可以看到下面添加了一堆过滤条件
IntentFilter intentFilter =
new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
intentFilter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
getAudioServiceExtInstance().getBleIntentFilters(intentFilter);
intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
//由于是在系统服务里面,所以调用的广播注册接口传是需要传更多参数的.
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null,
Context.RECEIVER_EXPORTED);
SubscriptionManager subscriptionManager = mContext.getSystemService(
SubscriptionManager.class);
if (subscriptionManager == null) {
Log.e(TAG, "initExternalEventReceivers cannot create SubscriptionManager!");
} else {
subscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionChangedListener);
}
}
1
2
3
4
arduino复制代码/*
* 3、对initExternalEventReceivers方法进行调用
*/
未贴代码,随便找个初始化的地方调用即可.
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
scala复制代码 /*
* 4、对AudioServiceBroadcastReceiver增加逻辑处理,因为前面已经添加了过滤器,所以可以在自己的过滤器中做自己想做的事情,我们在这里要做的,就是发送一个自定义的广播出去,绕开某些静态广播存在限制导致接收的问题.
*/
private class AudioServiceBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)){
sendWifiNoInternetBroadcasts(nai);
}
}

//自定义发送的广播
private void sendWifiNoInternetBroadcasts(NetworkAgentInfo nai){
Slog.d(TAG, "sendWifiNoInternetBroadcasts");
final Intent wifiNoIntent = new Intent("android.tinno.action.WIFI_NO_INTERNET_ACTION")
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
if (nai != null){
String ssid = WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid());
Slog.d(TAG, "sendWifiNoInternetBroadcasts :: ssid == " + ssid);
if (ssid != null){
wifiNoIntent.putExtra("ssid",ssid);
}
}
mContext.sendBroadcast(wifiNoIntent);
}

2、应用AndroidManifest.xml注册广播接收器

2.1 广播为什么还注册在应用的AndroidManifest里面?

  可以注意到,我们上面发送的广播的IntentFilter是android.tinno.action.WIFI_NO_INTERNET_ACTION,过滤器是自定义的,所以这是我们再去在应用中注册静态广播,只要自定义的广播发出来,是一定能够接收到的,说到底还是为了饶开某些Android原生过滤条件无法在静态广播中使用的情况,相关代码如下:

1
2
3
4
5
6
7
8
9
xml复制代码<!--在AndroidManifest.xml注册静态广播-->
<receiver
android:name="com.qualcomm.qti.settings.watchwifi.WifiNoInterNetReceiver"
android:enabled="true"
android:exported="true" >
<intent-filter>
<action android:name="android.tinno.action.WIFI_NO_INTERNET_ACTION" />
</intent-filter>
</receiver>

2.2 如此大费周章的接收到广播,此广播究竟干什么?

  回到我们的需求,我们是要实现一个Dialog,而Dialog是依附于Activity的,那依附于哪个Activity呢?没错,我们此广播做的唯一一件事,就是去指定并启动这个Activity,相关代码如下:

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
scala复制代码package com.qualcomm.qti.settings.watchwifi;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.provider.Settings;

import com.qualcomm.qti.settings.watchwifi.WifiNoInterNetActivity;

import android.net.wifi.WifiManager;

/**
* @Descpition: 只负责接受框架层发送的wifi无网广播
**/
public class WifiNoInterNetReceiver extends BroadcastReceiver {
private static final String TAG = "WifiNoInterNet";

@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "WifiNoInterNetReceiver :: onReceive");
intent.setClass(context, WifiNoInterNetActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
//启动Activity!
context.startActivity(intent);
}
}

其实到这里,这套灵活的广播组合拳已经打完,其目的就是确保我们在应用中注册的静态广播是一定能够收到相关消息,现在我们来看最后一个小知识点——透明主题.

3、实现透明主题Activity及dialog

3.1 为什么需要透明主题的Activity?

  我们的目的只是为了显示一个Dialog,而不是为了显示一个Activity,所以为了视觉上的呈现效果(给用户营造dialog就在当前的页面上显示的),我们就需要透明主题的实现,关于透明主题没啥好说的,咱直接上代码.

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
typescript复制代码package com.qualcomm.qti.settings.watchwifi;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import android.view.Gravity;
import android.os.Handler;
import android.os.Looper;
import android.widget.TextView;

/**
* @Descpition: 透明主题,只显示一个view,1s后自动销毁.
* @Author wpc
* @Date 2024/4/23 13:52
**/
public class WifiNoInterNetActivity extends Activity{
public static final String TAG = "WifiNoInterNet";
private Handler mHandler;
private TextView textView;

private String ssid;
private Intent intent;
//可以看到,onCreate里面设置了1.5s后此页面会被销毁.
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG,"WifiNoInterNetActivity :: onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.no_internet_dialog);
if (intent == null) {
intent = getIntent();
}
if (intent.hasExtra("ssid")){
ssid = (String)intent.getExtra("ssid");
Log.d(TAG, "WifiNoInterNetActivity :: intent.hasExtra(\"ssid\") && ssid == " + ssid);
}
String targetSsid = ssid + " " +"has no internet access.";
textView = findViewById(R.id.wifi_no_internet_tv);
textView.setText(targetSsid);
mHandler = new Handler(Looper.getMainLooper());
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
textView = null;
finish();
}
}, 1500);

}
}
1
2
3
4
5
6
7
xml复制代码    <!--透明主题的Style,直接在style文件中添加这一段,然后直接在Activity注册的地方引用此主题样式即可-->
<style name="TransparentActivityTheme">
<item name="android:windowBackground">@color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
</style>
1
2
3
4
5
6
xml复制代码    <!--activity的注册,在这里引用其透明主题,不要在activity的具体布局中去引用-->
<activity
android:name="com.qualcomm.qti.settings.watchwifi.WifiNoInterNetActivity"
android:theme="@style/TransparentActivityTheme"
android:excludeFromRecents="true"
android:launchMode="singleInstance" />

至此,一套灵活收发广播组合拳,打遍应用层所有Dialog开发需求核心内容就已经搞定,此方案具备很大的灵活性,针对于应用层开发来说,能够接收到自己在框架层定义发送的广播,其实已经完成了一个收发闭环。

当然,解决问题的方式有很多种,这不会是唯一一种,也绝不是最好的一种,欢迎大家关注、收藏,转发,最后再评论,我是番茄憨憨,哈哈哈哈哈!!!

u=4130461739,665980987&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto.webp

本文转载自: 掘金

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

1…171819…956

开发者博客

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