feat(docs): 完善开发工具文档并新增交互式演示组件
主要更新内容: ## 新增交互式演示组件 - SSH 认证流程可视化演示组件 - 正则表达式交互式测试工具 - 环境变量配置可视化演示 - 端口与 localhost 概念可视化解释 - 从晶体管到 CPU 的演进过程动画演示 - 数据编码(二进制/十六进制/base64)可视化演示 ## 文档内容完善 - 补全 SSH 认证与密钥管理完整指南,包含最佳实践 - 完善正则表达式教程,增加常用模式示例 - 完成环境变量与 PATH 配置详解 - 详细解释端口、localhost 和网络基础知识 - 新增 Netlify 部署指南,包含表单处理和函数示例 ## 样式优化 - 移除演示组件中的装饰性图标元素 - 清理未使用的 CSS 样式规则 - 统一组件视觉风格 ## 资源清理 - 删除 chapter4 中未使用的冗余图片文件(57张) ## 其他改进 - 移除文档中的 placeholder 待实现标记 - 增加实用示例和故障排查提示 - 添加 API 密钥和环境变量安全最佳实践 - 优化文档结构和导航体验 涉及文件:189 个文件变更,新增 21989 行,删除 7784 行
This commit is contained in:
@@ -1,244 +1,237 @@
|
||||
# 数据的编码、存储与传输
|
||||
# 什么是数据的编码与传输?
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**计算机如何表示和存储各种数据?** 文字、图片、视频、声音...这些在现实世界中形态各异的信息,是如何变成 0 和 1 的?又是如何存储和传输的?本章带你理解数据的编码、存储和传输原理。
|
||||
:::
|
||||
> 💡 **学习指南**:当你给朋友发一张照片、发一条微信,或者下载一个几 GB 的游戏时,这些信息是怎么穿过大半个地球、完好无损地出现在你的屏幕上的?
|
||||
>
|
||||
> 本章节会围绕一个经常困扰新手的问题展开:**为什么我收到的文件变成了乱码?**
|
||||
>
|
||||
> 顺着这个问题,我们将彻底揭开计算机底层最核心的三大基石:**编码、存储与传输**。
|
||||
|
||||
在开始之前,我们需要先明确一个经常被新手忽略的物理事实:
|
||||
|
||||
计算机其实极其“死板”。它不认识汉字,不认得色彩,也听不懂周杰伦的歌。
|
||||
|
||||
它的底层全是由无数个微小的半导体开关组成的,**它只能一次又一次地判断“通电(1)”或“断电(0)”**。
|
||||
|
||||
既然计算机只认识 0 和 1,那我们怎么让它显示五颜六色的图片和复杂的文字呢?
|
||||
|
||||
答案就是:**规定一本“密码本”**。
|
||||
|
||||
我们和计算机约定好:如果底层发来 `01000001` 这串微小的电信号,它在屏幕上就专门画出英文字母 `A`;如果发来另外一串信号,就专门显示红色。
|
||||
|
||||
这个**制定并使用密码本进行来回翻译的过程,就叫做“编码(Encoding)”**。
|
||||
|
||||
明白了“计算机里的一切本质上都是密码”这个逻辑起点,你就能瞬间明白日常最容易碰到的一个见鬼现象——乱码,到底是怎么产生的了。
|
||||
|
||||
---
|
||||
|
||||
## 0. 全景图:数据的生命周期
|
||||
## 0. 引言:为什么文件会变成“天书”?
|
||||
|
||||
想象你要寄一封信给朋友:
|
||||
想象一下,你收到一份重要的同事发来的文件,双击打开一看,里面全是类似“浣犲ソ”或“ä½ å¥½”这种奇怪的文字。
|
||||
|
||||
1. **编码**:把想法变成文字(信息编码)
|
||||
2. **存储**:写在纸上(数据存储)
|
||||
3. **传输**:通过邮局寄出(数据传输)
|
||||
直觉上,你肯定觉得:是不是文件在发送的过程中损坏了?是不是丢包了?
|
||||
|
||||
计算机处理数据也是类似的过程:
|
||||
但实际上,绝大多数所谓的“文件损坏”,真相只有一个——**你的电脑“没找对阅读规则”**。
|
||||
|
||||
| 阶段 | 做什么 | 核心问题 | 类比 |
|
||||
|------|--------|---------|------|
|
||||
| **编码** | 把信息变成 0 和 1 | 如何用二进制表示各种数据? | 把想法变成文字 |
|
||||
| **存储** | 把数据保存起来 | 数据存在哪里?怎么组织? | 写在纸上 |
|
||||
| **传输** | 把数据送到别处 | 如何可靠、高效地传输? | 邮局寄信 |
|
||||
👇 **动手点点看**:
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
**编码**:计算机只认识 0 和 1,所以所有数据都要"翻译"成二进制。文字用 ASCII 或 Unicode 编码,数字用二进制表示,图片用像素值,声音用采样值。
|
||||
试着在下方的模拟器里,切换不同的“解码密码本”,来读取同一串底层的电信号字节。
|
||||
|
||||
**存储**:编码后的数据需要保存起来。存储介质从快到慢有:寄存器 → 缓存 → 内存 → SSD → 硬盘 → 云存储。越快的存储越贵、容量越小。
|
||||
<GarbledTextDemo />
|
||||
|
||||
**传输**:数据需要在不同设备间流动。传输方式有串行(一位一位传)和并行(多位同时传)。现代高速接口(USB、PCIe)多采用串行方式。
|
||||
:::
|
||||
**🎯 核心领悟:没对齐的密码本**
|
||||
|
||||
字节(0和1序列)本身是没有绝对意义的,是人类制定的**「编码规则」**赋予了它们意义。
|
||||
|
||||
这就像是一串摩斯密码“滴滴答”,如果你用中文电报密码本去查,它是一个字;如果用美军密码本去查,它是另一个字。
|
||||
|
||||
**发件人用 UTF-8 密码本把汉字翻译成了数字发给你,你如果硬要用 GBK 密码本去解读这些数字,拼出来的当然全是乱码。**
|
||||
|
||||
要彻底搞懂为什么没损坏的数据会变乱码,我们需要了解数据处理的完整链条。即数据的“一生”:**编码**、**存储**、**传输**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据编码:用 0 和 1 表示一切
|
||||
## 1. 什么是数据编码?(把万物变成数字)
|
||||
|
||||
### 1.1 文本编码
|
||||
简单来说:
|
||||
|
||||
<EncodingDemo />
|
||||
> **数据编码(Encoding)**,就是建立一本“双向翻译词典”,把现实世界中复杂多样的信息(文字、色彩、声音),强制映射成计算机能理解的 0 和 1 的规则。
|
||||
|
||||
::: tip 💡 字符编码的演变
|
||||
**ASCII(1963年)**:
|
||||
- 用 7 位二进制表示 128 个字符
|
||||
- 包括英文字母、数字、常用符号
|
||||
- 问题:只能表示英语,无法表示中文等
|
||||
### 1.1 把文字变成数字:从 ASCII 到万国码
|
||||
|
||||
**Unicode(1991年)**:
|
||||
- 统一编码标准,覆盖世界上所有文字
|
||||
- 目前已收录超过 14 万个字符
|
||||
- 常用编码方式:UTF-8(变长编码,1-4 字节)
|
||||
我们每天在微信里打字,每按下一个键,计算机其实暗中都在做一件事:**查表替换**。
|
||||
|
||||
**UTF-8 的巧妙设计**:
|
||||
- ASCII 字符(0-127)只用 1 字节,完全兼容
|
||||
- 常用汉字用 3 字节
|
||||
- 根据"前导位"判断一个字符占几个字节
|
||||
:::
|
||||
**第一阶段:ASCII 的小天地**
|
||||
|
||||
**常见字符编码对比:**
|
||||
发明电脑初期,美国人觉得世界上只有 26 个英文字母、数字和一些标点符号,于是制定了一本很薄的密码本叫做 **ASCII 码**。
|
||||
|
||||
| 编码 | 字节数 | 支持字符 | 特点 |
|
||||
|------|--------|---------|------|
|
||||
| **ASCII** | 1 字节 | 128 个 | 仅英语,兼容性好 |
|
||||
| **UTF-8** | 1-4 字节 | 所有文字 | 变长编码,主流标准 |
|
||||
| **UTF-16** | 2-4 字节 | 所有文字 | 定长为主,Windows 常用 |
|
||||
| **GBK** | 1-2 字节 | 中英文 | 中文专用,不推荐新项目使用 |
|
||||
它只规定了 128 个符号,比如规定数字 `65` 代表大写字母 `A`。由于字符很少,**1 个字节(Byte,等于 8 个比特位 Bit)** 的空间能容纳 256 种变化,绰绰有余。
|
||||
|
||||
### 1.2 数字编码
|
||||
**第二阶段:群雄割据的战国时代**
|
||||
|
||||
::: tip 💡 整数如何用二进制表示?
|
||||
**无符号整数**:直接用二进制表示
|
||||
- 8 位可以表示 0-255
|
||||
- 32 位可以表示 0 到约 42 亿
|
||||
但后来,电脑走向了世界。大家发现:**汉字有几万个,日本还有假名,光靠 1 个字节根本装不下!**
|
||||
|
||||
**有符号整数**:用补码表示
|
||||
- 最高位是符号位(0 正 1 负)
|
||||
- 正数:直接用二进制
|
||||
- 负数:正数的二进制取反加 1
|
||||
于是,中国搞了 GBK 密码本(用 2 个字节存一个汉字),日本搞了 Shift_JIS……世界陷入了混乱。你在中国做好的网页,发给美国客户,他们电脑里没有 GBK 词典,打开全是一堆乱码。
|
||||
|
||||
**为什么用补码?**
|
||||
- 加法减法统一处理
|
||||
- 0 的表示唯一
|
||||
- 硬件实现简单
|
||||
:::
|
||||
**第三阶段:天下一统的 Unicode(万国码)**
|
||||
|
||||
**浮点数表示(IEEE 754 标准):**
|
||||
最后,计算机界的大神们坐在一起商量:“大家别各玩各的了,我们做一本收录地球上所有符号的超级大字典吧!”这就是大名鼎鼎的 **Unicode(万国码)**。它给世界上每一个文字、甚至你常用的每个 Emoji 表情都分配了一个独一无二的编号。
|
||||
|
||||
| 部分 | 作用 | 位数(32位浮点) |
|
||||
|------|------|-----------------|
|
||||
| **符号位** | 正负 | 1 位 |
|
||||
| **指数位** | 决定大小范围 | 8 位 |
|
||||
| **尾数位** | 决定精度 | 23 位 |
|
||||
而你经常听到的 **UTF-8**,就是 Unicode 字典目前最流行的一套“存储规则”。它最聪明的点在于它是**变长**的:遇到英文只用 1 个字节,遇到中文用 3 个字节,非常节省空间。
|
||||
|
||||
### 1.3 多媒体编码
|
||||
👇 **动手点点看**:
|
||||
|
||||
**图像编码**:
|
||||
- **位图**:每个像素用 RGB 值表示(红绿蓝各 8 位)
|
||||
- **压缩**:JPEG(有损)、PNG(无损)
|
||||
- **矢量图**:用数学公式描述形状(SVG)
|
||||
在下面的输入框里随便打几个中英文或 Emoji(比如:`你好 Hello 🎉`),看看计算机底层是怎么“查表”占用空间的。
|
||||
|
||||
**音频编码**:
|
||||
- **采样**:把连续声波变成离散点
|
||||
- **量化**:把采样值变成数字
|
||||
- **压缩**:MP3(有损)、FLAC(无损)
|
||||
<CharacterEncodingExplorer />
|
||||
|
||||
**视频编码**:
|
||||
- 视频是一帧帧图像
|
||||
- 关键技术:帧间压缩(只记录变化部分)
|
||||
- 常见格式:H.264、H.265、VP9
|
||||
**💡 惊奇发现**:
|
||||
|
||||
- 一个英文字母在 UTF-8 里只占 **1 个字节**。
|
||||
- 一个普通汉字通常占 **3 个字节**。
|
||||
- 一个 Emoji 表情(🎉),竟然需要 **4 个字节**!
|
||||
|
||||
> **冷知识**:为什么很多人觉得发同样长度的短信,纯英文能发好长一段,纯中文只能发几句?因为在底层的电信号序列里,中文的物理尺寸足足是英文的 3 倍大!
|
||||
|
||||
### 1.2 颜色和声音怎么变数字?
|
||||
|
||||
文字可以查表,那蒙娜丽莎的微笑、周杰伦的歌声怎么变成 0 和 1 呢?
|
||||
|
||||
方法同样是:**切割与映射**。
|
||||
|
||||
* **图片的编码**:
|
||||
把一张照片无限放大,它其实是由几百万个发光的小方块(像素)组成的。我们只要规定每个颜色的编号(比如 `#FF0000` 代表红色),然后把几百万个方块的编号存下来,照片就变成了数字。
|
||||
|
||||
👇 **动手点点看**:悬停在左侧画布的小格子上,看看图像颜色是怎么映射成十六进制代码的。
|
||||
<ImageEncodingDemo />
|
||||
|
||||
* **声音的编码**:
|
||||
声音本质是空气的震荡波。如果我们每秒去测量这个波浪的高度 44100 次(采样),记录下代表高度的数值。连续存下来,连通的声波就变成了离散的数字数组。
|
||||
|
||||
👇 **动手点点看**:拖动滑块,看看连续的模拟声波是怎么被“切片”成数字音频的。
|
||||
<AudioEncodingDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据存储:速度与容量的权衡
|
||||
## 2. 存储桥梁:发出去之前,总得先放个地方
|
||||
|
||||
### 2.1 存储层次结构
|
||||
数据编完码之后,准备发给别人。但在这之前,必须要先把它放在电脑的物理介质里。这就涉及到一个不可避免的硬件铁律。
|
||||
|
||||
<StorageDemo />
|
||||
你可能会想:**“既然都要存,全存在读写最快的地方不就好了吗?”**
|
||||
|
||||
### 2.2 存储器类型
|
||||
然而在硬件世界里,永远有个鱼和熊掌不可兼得的魔咒:**速度越快的存储介质,通常造价越贵,能做出的容量也越小。**
|
||||
|
||||
| 类型 | 原理 | 特点 | 应用 |
|
||||
|------|------|------|------|
|
||||
| **SRAM** | 触发器 | 极快,但昂贵 | CPU 缓存 |
|
||||
| **DRAM** | 电容充放电 | 较快,需刷新 | 内存 |
|
||||
| **Flash** | 浮栅晶体管 | 断电不丢失,有写入寿命 | SSD、U 盘 |
|
||||
| **HDD** | 磁盘磁性记录 | 容量大,有机械延迟 | 机械硬盘 |
|
||||
为了用尽可能少的钱换取尽可能快的电脑运行速度,计算机科学家不得已设计了**「存储层次结构」**(也就是存储金字塔)。
|
||||
|
||||
### 2.3 存储的关键指标
|
||||
👇 **动手点点看**:
|
||||
|
||||
::: tip 💡 如何评估存储性能?
|
||||
**访问时间**:从发出请求到获得数据的时间
|
||||
- 内存:约 100 纳秒
|
||||
- SSD:约 100 微秒
|
||||
- HDD:约 10 毫秒
|
||||
点击金字塔的不同层级,看看现代计算机是怎么精打细算的。
|
||||
|
||||
**吞吐量**:单位时间能传输的数据量
|
||||
- 内存:几十 GB/s
|
||||
- SSD:几 GB/s
|
||||
- HDD:100-200 MB/s
|
||||
<StoragePyramidDemo />
|
||||
|
||||
**IOPS**:每秒能进行的读写操作次数
|
||||
- SSD:几万到几十万
|
||||
- HDD:几百
|
||||
:::
|
||||
**🎯 核心领悟:操作系统的搬运工哲学**
|
||||
|
||||
世界上没有完美的存储器。因此,操作系统(如 Windows, macOS)就像一个极度聪明、一刻不停的仓库管理员:
|
||||
|
||||
1. 它把海量的电影、游戏塞在速度慢、容量大(便宜)的仓库——**SSD 或机械硬盘**里。
|
||||
2. 当你要玩游戏时,它赶紧把相关的高清贴图文件,从硬盘搬运到速度极快但容量有限的操作台——**内存(RAM)**上。
|
||||
3. 当你关闭游戏时,它再把内存清空,腾出操作台给别的文件用。
|
||||
|
||||
> **解惑**:当你玩大型开放世界游戏时,遇到场景切换要黑屏很久(读条),本质上就是因为硬盘仓库太慢,搬运工(系统)正在玩命地把下一张地图的数据搬到内存操作台上呢。
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据传输:从串行到并行
|
||||
## 3. 什么是数据传输?(让 0 和 1 出发旅行)
|
||||
|
||||
### 3.1 传输方式
|
||||
数据编完码、存在了内存里,接下来就是发给朋友了。
|
||||
|
||||
<TransmissionDemo />
|
||||
> **数据传输**,就是把代表 0 和 1 的电信号(或光信号),顺着网线、电缆或无线电波,准确无误地从一台机器送到另一台机器的过程。
|
||||
|
||||
### 3.2 常见接口标准
|
||||
### 3.1 硬件与局域网传输:一条导线的物理极限
|
||||
|
||||
| 接口 | 类型 | 速度 | 应用 |
|
||||
|------|------|------|------|
|
||||
| **USB 3.0** | 串行 | 5 Gbps | 外设连接 |
|
||||
| **USB 4** | 串行 | 40 Gbps | 高速外设 |
|
||||
| **SATA III** | 串行 | 6 Gbps | 硬盘接口 |
|
||||
| **PCIe 4.0 x16** | 串行(多通道) | 32 GB/s | 显卡、SSD |
|
||||
| **以太网** | 串行 | 1-100 Gbps | 网络传输 |
|
||||
在机箱内部,或者两台靠得很近的电脑之间发数据,我们面临的是**纯粹的物理挑战**。
|
||||
|
||||
### 3.3 传输的可靠性
|
||||
很多人第一个想到的点子是:“一根电线一次发 1 个信号,那我并排接 8 根线,速度不就是 8 倍吗?”
|
||||
这就是早期用来插硬盘的**并行传输(Parallel)**思路。
|
||||
|
||||
::: tip 💡 如何保证传输不出错?
|
||||
**校验机制**:
|
||||
- **奇偶校验**:简单的错误检测
|
||||
- **CRC 校验**:更强的错误检测能力
|
||||
- **校验和**:快速检测数据完整性
|
||||
然而,今天手机的 Type-C、外部的 USB 和主板内部的 PCIe 接口,用的全都是**串行传输(Serial,只有一根主通道发数据)**。
|
||||
|
||||
**纠错机制**:
|
||||
- **重传**:发现错误就重新发送
|
||||
- **前向纠错**:发送冗余信息,接收方能自动纠正
|
||||
👇 **动手点点看**:
|
||||
比较一下串行和并行传输的动画。
|
||||
|
||||
**流量控制**:
|
||||
- 防止发送方发太快,接收方来不及处理
|
||||
- 类似"确认收到再发下一个"
|
||||
:::
|
||||
<DataTransmissionDemo />
|
||||
|
||||
**💡 为什么“一条小路”击败了“八车道”?**
|
||||
|
||||
在速度不快时,8 根线确实强。但当我们需要每秒发几十亿次信号时,问题出现了:
|
||||
并排的几根线上的微弱电流会产生极强的电磁波互相干扰(串扰 Crosstalk);而且你根本无法保证发送端同时发出的 8 个信号,能完美**同时**到达终点线。只要有一根线因为杂质阻抗慢了一丝拉,8 个拼在一起的字就彻底乱了。
|
||||
|
||||
所以,与其花天价去调平 8 条赛道,不如把所有技术资源砸在 1 辆跑车上,把它拉到光速。这就是串行接口一统天下的物理真相。
|
||||
|
||||
### 3.2 广域网与互联网传输:漂洋过海的防丢艺术
|
||||
|
||||
如果你的数据不是发给机箱里一寸外的显卡,而是要发给大洋彼岸美国服务器呢?
|
||||
|
||||
一根连续的导线是不可能的。数据要穿过光缆、海底基站、无数个破旧的路由器。这时候,面临的不再是物理极限,而是**容错保全挑战**。
|
||||
|
||||
当你用微信发送 1GB 的超大视频时,底层的逻辑像极了国际搬家——你不可能整个集装箱直接扔给邮政。
|
||||
|
||||
1. **分包(Packetization)**:网络会把视频切成几万个信封大小的“数据包”(通常是 1500 字节)。
|
||||
2. **校验(Checksum)**:为防止途中海底光缆被鲨鱼咬断一根线,导致某个包里的 `0` 翻转成了 `1`,系统会在发件前,用复杂的数学公式对信封里的信件算出一个“特征码”贴在上面。
|
||||
3. **TCP重发与确认**:接收方拿到信封,先自己在纸上验算一遍特征码。如果不对(沿途受损),或者发现序号从 31 直接跳到了 33(丢包),就会通过网络大喊一声:**“我没收到 32 号,请你再重发一遍 32 号!”**
|
||||
|
||||
正因为有了这种底层叫做 **TCP(传输控制协议)** 的极其严密的切包对账机制,你在地下室或者极不稳定的 WiFi 下下载微信文件,就算下了半小时,下载完的那一瞬间,文件也必定是 100% 完整、0 损坏的。
|
||||
|
||||
---
|
||||
|
||||
## 4. 编码、存储、传输的协作
|
||||
## 4. 终局实战:从拍下快门到发朋友圈的全流程
|
||||
|
||||
让我们看一个完整的例子:**保存一张照片到云端**
|
||||
前面我们将“如何翻译成数字(编码)”、“放在哪里保管(存储)”、“如何完好地走完旅途(传输)”都分块讲了一遍。
|
||||
|
||||
```
|
||||
1. 编码阶段
|
||||
- 相机传感器捕捉光线 → 模拟信号
|
||||
- ADC 转换 → 数字信号(RAW 格式)
|
||||
- JPEG 编码 → 压缩后的二进制数据
|
||||
现在,让我们把这些积木搭起来,沉浸式观看一个日常中再普通不过的操作:**拍一张照片自动备份到云端。**
|
||||
|
||||
2. 存储阶段
|
||||
- 写入手机内存(RAM)→ 临时存储
|
||||
- 写入手机闪存(Flash)→ 持久存储
|
||||
当你按下快门的那一秒钟,手机内部其实已经打响了一场极其恢弘的数字战争。
|
||||
|
||||
3. 传输阶段
|
||||
- 读取闪存数据 → 内存
|
||||
- 通过 Wi-Fi/4G 发送 → 网络传输
|
||||
- 云端接收 → 写入云端存储
|
||||
```
|
||||
👇 **动手点点看**:
|
||||
|
||||
::: tip 💡 理解这个流程
|
||||
每一步都涉及编码、存储、传输:
|
||||
点击“执行这一步”,追踪这笔数据惊险的完整生命旅程。
|
||||
|
||||
1. **编码**:把图像变成二进制数据
|
||||
2. **存储**:在本地保存
|
||||
3. **传输**:通过网络发送到云端
|
||||
|
||||
这三个环节紧密配合,才能完成"保存照片到云端"这个看似简单的操作。
|
||||
:::
|
||||
<PhotoUploadJourneyDemo />
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结:数据的三重奏
|
||||
## 5. 名词对照表
|
||||
|
||||
让我们用一个比喻总结编码、存储、传输:
|
||||
当你阅读其他文档时,可能会遇到下面这些行话,这里为你准备了一张速查表:
|
||||
|
||||
| 概念 | 比喻 | 核心任务 |
|
||||
|------|------|---------|
|
||||
| **编码** | 翻译 | 把信息变成 0 和 1 |
|
||||
| **存储** | 仓库 | 把数据保存起来 |
|
||||
| **传输** | 快递 | 把数据送到目的地 |
|
||||
|
||||
::: tip 💡 核心启示
|
||||
**数据处理的本质是"转换、保存、移动"**。
|
||||
|
||||
- 编码解决"如何表示"的问题
|
||||
- 存储解决"如何保存"的问题
|
||||
- 传输解决"如何传递"的问题
|
||||
|
||||
理解了这三点,你就会明白:
|
||||
- 为什么不同文件格式要选择不同的编码方式
|
||||
- 为什么需要不同层次的存储介质
|
||||
- 为什么传输速度和可靠性需要平衡
|
||||
:::
|
||||
| 术语 / 缩写 | 中文对照 | 简单解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Bit (b)** | 比特 / 位 | 计算机世界最小的单位,只能是 0 或者 1。 |
|
||||
| **Byte (B)** | 字节 | 8 个 Bit 捆在一起就是一个 Byte。它是文件大小最基础的衡量单位。 |
|
||||
| **Character Set** | 字符集 | 就像是“字典的目录”,规定了某个文字存在,并没有规定在硬盘里具体怎么写。 |
|
||||
| **Encoding** | 编码 | 具体的“存储规则”,决定了字典里的那个字,对应底层到底是哪几个字节(如 UTF-8)。 |
|
||||
| **RAM** | 内存 / 运行内存 | 极其快速但断电就清空的工作台。你手机的 8G/16G 运存指的就是这个。 |
|
||||
| **SSD** | 固态硬盘 | 现代电脑负责永久保存数据的仓库,基于闪存芯片,比老式机械硬盘快几十倍。 |
|
||||
| **Serial / Parallel** | 串行 / 并行 | 串行是一条通道挨个排队飞奔;并行是多条通道齐头并进(但不适合极高频率)。 |
|
||||
| **Checksum** | 校验和 | 传输数据时附带的验证码。收件人算一遍,如果和包裹上写的一致,说明没坏。 |
|
||||
| **TCP** | 传输控制协议 | 互联网的基石协议。负责把大文件切片、贴序号、丢包重发,保证数据 100% 完整送达。 |
|
||||
|
||||
---
|
||||
|
||||
## 延伸阅读
|
||||
## 总结
|
||||
|
||||
- **字符编码详解**:深入学习 ASCII、Unicode、UTF-8 的设计原理
|
||||
- **存储技术发展**:了解从磁带到 SSD 的技术演进
|
||||
- **网络传输协议**:学习 TCP/IP 如何保证可靠传输
|
||||
- **数据压缩算法**:了解 ZIP、JPEG、MP3 等压缩原理
|
||||
文章一开始提出的诸多疑惑,现在你已经站在了系统底层的视角有了答案:
|
||||
|
||||
- **为什么同样的文件你收到后变乱码了?**
|
||||
数据没坏,只是你的阅读软件没选对密码本(编码问题)。
|
||||
|
||||
- **为什么现在电脑背后的线大多是一根小小的 Type-C,却比以前很宽的线传输还要快?**
|
||||
因为以前是几辆马车并排慢跑容易撞车(并行),现在是一列高铁在专线上极速狂飙(串行)。
|
||||
|
||||
- **为什么大型游戏在读取场景时要黑屏很久?**
|
||||
因为它需要把动辄几十 GB 的大文件,从速度慢的硬盘(仓储区),拼命搬运拼接到速度快但昂贵的内存(核心工作台)里。
|
||||
|
||||
计算机的本质其实非常朴素:
|
||||
|
||||
**它不过是一个擅长把所有的光影文字“转换(编码)”、放在某个硅片里“保管(存储)”、然后再把它切碎成电平脉冲“邮寄出去(传输)”的机器**。
|
||||
|
||||
读懂了这个循环往复的过程,你就真正握住了推开计算机底层原理大门的那把钥匙。
|
||||
|
||||
@@ -1,395 +1,132 @@
|
||||
# 编程语言图谱
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**为什么有这么多编程语言?它们之间有什么关系?** 从机器语言到现代高级语言,每种语言都有其设计哲学和适用场景。本章带你理解编程语言的演化历程和核心概念。
|
||||
:::
|
||||
> 💡 **学习指南**:为什么有这么多编程语言?该学哪个?本章带你从"语言演化"到"编程范式"到"如何选择",建立对编程语言全景的理解。**结论先行:没有最好的语言,只有最适合场景的语言。**
|
||||
|
||||
---
|
||||
|
||||
## 0. 想象你要和外国人交流:
|
||||
## 0. 人类如何和计算机"说话"?
|
||||
|
||||
- **直接用肢体语言**:最原始,但效率极低(机器语言)
|
||||
- **学习对方的语言**:需要翻译,但表达丰富(高级语言)
|
||||
- **使用世界语**:设计完美,但没人用(某些学术语言)
|
||||
- **使用翻译软件**:自动转换,但可能不准确(编译器/解释器)
|
||||
想象你要和一个只懂二进制的机器人沟通:
|
||||
|
||||
**编程语言就是人类与计算机沟通的桥梁**,不同的语言有不同的设计哲学。
|
||||
- **直接打 0 和 1** — 最原始,效率极低,一个 0 写成 1 就全错了(机器语言)
|
||||
- **用助记符代替** — `MOV AX, 1` 比 `10110000 00000001` 好认多了(汇编语言)
|
||||
- **用接近自然语言** — `int sum = 1 + 2;` 人类可以直接读懂(高级语言)
|
||||
|
||||
<LanguageMapDemo />
|
||||
**编程语言就是人类与计算机沟通的桥梁**,70 多年来一直在朝着"更接近人类思维"的方向进化。
|
||||
|
||||
---
|
||||
|
||||
## 1. 编程语言的演化
|
||||
|
||||
### 1.1 第一代:机器语言(1940s)
|
||||
👇 动手点点看:探索编程语言从 1940 年代到今天的演化历程
|
||||
|
||||
::: tip 💡 机器语言是什么?
|
||||
直接用 0 和 1 编写程序,计算机可以直接执行。
|
||||
<LanguageMapDemo />
|
||||
|
||||
**示例**:让计算机计算 1 + 2
|
||||
```
|
||||
10110000 00000001 ; 将 1 放入寄存器
|
||||
10110001 00000010 ; 将 2 放入另一个寄存器
|
||||
10100010 ; 执行加法
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 人类难以理解和记忆
|
||||
- 容易出错,一个 0 写成 1 就全错了
|
||||
- 不同 CPU 有不同的机器语言
|
||||
::: tip 💡 一句话总结
|
||||
编程语言的演化趋势:**越来越接近人类思维,越来越安全,越来越高效**。从手写 0/1,到汇编助记符,到 C 的结构化编程,到 Java 的面向对象,再到 Rust 的内存安全——每一代语言都在解决上一代的痛点。
|
||||
:::
|
||||
|
||||
### 1.2 第二代:汇编语言(1950s)
|
||||
|
||||
用**助记符**代替 0 和 1:
|
||||
|
||||
```asm
|
||||
MOV AX, 1 ; 将 1 放入 AX 寄存器
|
||||
MOV BX, 2 ; 将 2 放入 BX 寄存器
|
||||
ADD AX, BX ; 将 BX 加到 AX
|
||||
```
|
||||
|
||||
::: tip 💡 汇编语言 vs 机器语言
|
||||
| 特性 | 机器语言 | 汇编语言 |
|
||||
|------|---------|---------|
|
||||
| **可读性** | 极差 | 较好 |
|
||||
| **执行效率** | 最高 | 最高(汇编器直接转换) |
|
||||
| **移植性** | 无 | 无(依赖 CPU 架构) |
|
||||
| **使用场景** | 几乎不用 | 嵌入式、操作系统内核 |
|
||||
:::
|
||||
|
||||
### 1.3 第三代:高级语言(1950s - 至今)
|
||||
|
||||
**用接近自然语言的方式编程**:
|
||||
|
||||
```c
|
||||
int sum = 1 + 2; // C 语言
|
||||
```
|
||||
|
||||
**里程碑语言**:
|
||||
|
||||
| 年代 | 语言 | 意义 |
|
||||
|------|------|------|
|
||||
| **1957** | Fortran | 第一个高级语言,科学计算 |
|
||||
| **1958** | Lisp | 函数式编程鼻祖 |
|
||||
| **1959** | COBOL | 商业数据处理 |
|
||||
| **1972** | C | 系统编程,影响深远 |
|
||||
| **1983** | C++ | 面向对象 + C 的效率 |
|
||||
| **1991** | Python | 简洁优雅,AI 时代主角 |
|
||||
| **1995** | Java | 跨平台,企业应用 |
|
||||
| **1995** | JavaScript | Web 开发,无处不在 |
|
||||
| **2009** | Go | 并发友好,云原生 |
|
||||
| **2010** | Rust | 内存安全,系统编程新选择 |
|
||||
|
||||
### 1.4 第四代:领域特定语言(DSL)
|
||||
|
||||
为特定领域设计的语言:
|
||||
|
||||
| 语言 | 领域 | 示例 |
|
||||
|------|------|------|
|
||||
| **SQL** | 数据库查询 | `SELECT * FROM users` |
|
||||
| **HTML** | 网页结构 | `<div>Hello</div>` |
|
||||
| **CSS** | 样式定义 | `color: red;` |
|
||||
| **Regex** | 文本匹配 | `\d{3}-\d{4}` |
|
||||
| **MATLAB** | 数学计算 | `A = [1 2; 3 4]` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 编程范式:思考问题的方式
|
||||
|
||||
::: tip 💡 什么是编程范式?
|
||||
编程范式是**编程的思维方式**,决定了你如何组织代码和解决问题。
|
||||
编程范式不是语言特性,而是**思维方式**——就像写作有诗歌、小说、论文不同的文体。
|
||||
|
||||
就像写作有不同的文体(诗歌、小说、论文),编程也有不同的"文体"。
|
||||
:::
|
||||
|
||||
### 2.1 命令式编程(Imperative)
|
||||
|
||||
**核心思想**:告诉计算机"怎么做"
|
||||
### 2.1 命令式 — "一步步告诉计算机怎么做"
|
||||
|
||||
```c
|
||||
// 计算数组总和
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
sum += arr[i];
|
||||
}
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 关注**过程**和**步骤**
|
||||
- 通过**语句**改变程序状态
|
||||
- 最接近计算机实际执行方式
|
||||
|
||||
**代表语言**:C, Fortran, BASIC
|
||||
|
||||
### 2.2 面向对象编程(OOP)
|
||||
|
||||
**核心思想**:把数据和操作封装在"对象"中
|
||||
### 2.2 面向对象 — "把数据和行为封装成对象"
|
||||
|
||||
```python
|
||||
class Dog:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def bark(self):
|
||||
print(f"{self.name} says woof!")
|
||||
|
||||
dog = Dog("Buddy")
|
||||
dog.bark() # Buddy says woof!
|
||||
```
|
||||
|
||||
**四大特性**:
|
||||
|
||||
| 特性 | 含义 | 生活类比 |
|
||||
|------|------|---------|
|
||||
| **封装** | 隐藏内部细节 | 汽车方向盘,不需要知道引擎原理 |
|
||||
| **继承** | 子类继承父类 | 儿子继承父亲的基因 |
|
||||
| **多态** | 同一接口不同实现 | 不同动物发出不同叫声 |
|
||||
| **抽象** | 提取共同特征 | "动物"是对猫、狗的抽象 |
|
||||
|
||||
**代表语言**:Java, C++, Python, Ruby
|
||||
|
||||
### 2.3 函数式编程(Functional)
|
||||
|
||||
**核心思想**:把计算视为函数求值,避免状态变化
|
||||
### 2.3 函数式 — "用纯函数组合,不修改状态"
|
||||
|
||||
```haskell
|
||||
-- 计算数组总和
|
||||
sum arr = foldl (+) 0 arr
|
||||
|
||||
-- 或者更简洁
|
||||
sum = foldl (+) 0
|
||||
-- 相同输入永远产生相同输出
|
||||
```
|
||||
|
||||
**核心原则**:
|
||||
|
||||
| 原则 | 含义 | 好处 |
|
||||
|------|------|------|
|
||||
| **纯函数** | 相同输入永远产生相同输出 | 易测试、易推理 |
|
||||
| **不可变数据** | 数据一旦创建就不变 | 无副作用、线程安全 |
|
||||
| **高阶函数** | 函数可以作为参数和返回值 | 代码复用、灵活组合 |
|
||||
| **无副作用** | 函数不修改外部状态 | 可预测、易调试 |
|
||||
|
||||
**代表语言**:Haskell, Lisp, Erlang, F#
|
||||
|
||||
### 2.4 声明式编程(Declarative)
|
||||
|
||||
**核心思想**:告诉计算机"做什么",而不是"怎么做"
|
||||
### 2.4 声明式 — "只说做什么,不管怎么做"
|
||||
|
||||
```sql
|
||||
-- 查询所有活跃用户
|
||||
SELECT name, email
|
||||
FROM users
|
||||
WHERE active = true
|
||||
ORDER BY created_at DESC
|
||||
SELECT name FROM users WHERE active = true
|
||||
-- 数据库自己决定怎么查最快
|
||||
```
|
||||
|
||||
**对比命令式**:
|
||||
|
||||
| 命令式 | 声明式 |
|
||||
|--------|--------|
|
||||
| "从第一行开始遍历..." | "给我所有活跃用户" |
|
||||
| "检查每个用户是否活跃..." | "按创建时间排序" |
|
||||
| "如果活跃就加入结果..." | 数据库自己决定怎么执行 |
|
||||
| "最后排序返回..." | |
|
||||
|
||||
**代表语言**:SQL, Prolog, HTML
|
||||
::: tip 💡 实际开发中
|
||||
现代语言大多是**多范式**的。Python 既支持面向对象,也支持函数式;JavaScript 也一样。不用纠结"哪个范式最好",而是根据问题选择最合适的方式。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 3. 类型系统:数据的分类规则
|
||||
## 3. 类型系统:数据的交通规则
|
||||
|
||||
::: tip 💡 什么是类型系统?
|
||||
类型系统是编程语言的**交通规则**,规定数据如何分类和操作。
|
||||
| | 强类型 | 弱类型 |
|
||||
|---|---|---|
|
||||
| **静态** | Java, Rust, TypeScript — 最安全 | C, C++ — 高效但要小心 |
|
||||
| **动态** | Python, Ruby — 灵活且安全 | JavaScript, PHP — 灵活但易出错 |
|
||||
|
||||
就像现实世界:
|
||||
- **整数** = 整数类型(1, 2, 3...)
|
||||
- **文字** = 字符串类型("hello")
|
||||
- **是/否** = 布尔类型(true/false)
|
||||
:::
|
||||
**关键问题**:`"1" + 1` 等于什么?
|
||||
- **JavaScript(弱类型)**:`"11"` — 悄悄帮你转了
|
||||
- **Python(强类型)**:`TypeError` — 让你自己想清楚
|
||||
|
||||
### 3.1 静态类型 vs 动态类型
|
||||
|
||||
| 特性 | 静态类型 | 动态类型 |
|
||||
|------|---------|---------|
|
||||
| **类型检查时机** | 编译时 | 运行时 |
|
||||
| **代码示例** | `int x = 1;` | `x = 1` |
|
||||
| **错误发现** | 编译期就发现 | 运行时才发现 |
|
||||
| **灵活性** | 较低 | 较高 |
|
||||
| **性能** | 较高(编译优化) | 较低(运行时检查) |
|
||||
| **代表语言** | Java, C++, Rust, TypeScript | Python, JavaScript, Ruby |
|
||||
|
||||
**静态类型示例(Java)**:
|
||||
|
||||
```java
|
||||
String name = "Alice";
|
||||
name = 123; // 编译错误!类型不匹配
|
||||
```
|
||||
|
||||
**动态类型示例(Python)**:
|
||||
|
||||
```python
|
||||
name = "Alice"
|
||||
name = 123 # 没问题,运行时类型改变
|
||||
```
|
||||
|
||||
### 3.2 强类型 vs 弱类型
|
||||
|
||||
| 特性 | 强类型 | 弱类型 |
|
||||
|------|--------|--------|
|
||||
| **类型转换** | 不允许隐式转换 | 允许隐式转换 |
|
||||
| **类型安全** | 高 | 低 |
|
||||
| **代码示例** | `"1" + 1` 报错 | `"1" + 1 = "11"` |
|
||||
| **代表语言** | Python, Java, Rust | JavaScript, PHP, C |
|
||||
|
||||
**弱类型示例(JavaScript)**:
|
||||
|
||||
```javascript
|
||||
console.log("1" + 1) // "11" (字符串拼接)
|
||||
console.log("1" - 1) // 0 (自动转数字)
|
||||
console.log([] + []) // "" (空字符串)
|
||||
console.log([] + {}) // "[object Object]"
|
||||
```
|
||||
|
||||
**强类型示例(Python)**:
|
||||
|
||||
```python
|
||||
"1" + 1 # TypeError: can only concatenate str to str
|
||||
```
|
||||
|
||||
### 3.3 类型推断
|
||||
|
||||
现代语言可以**自动推断**变量类型:
|
||||
|
||||
```typescript
|
||||
// TypeScript
|
||||
let x = 1; // 推断为 number
|
||||
let y = "hello"; // 推断为 string
|
||||
|
||||
// Rust
|
||||
let x = 1; // 推断为 i32
|
||||
let y = "hello"; // 推断为 &str
|
||||
```
|
||||
想深入了解类型系统?→ [类型系统与编译原理入门](./type-systems-compilers)
|
||||
|
||||
---
|
||||
|
||||
## 4. 编译型 vs 解释型
|
||||
|
||||
::: tip 💡 程序如何运行?
|
||||
编程语言写的代码需要转换成机器能理解的指令,有两种主要方式:
|
||||
:::
|
||||
|
||||
### 4.1 编译型语言
|
||||
|
||||
**流程**:源代码 → 编译器 → 机器码 → 执行
|
||||
|
||||
```
|
||||
源代码 (main.c)
|
||||
↓
|
||||
编译器 (gcc)
|
||||
↓
|
||||
可执行文件 (main.exe)
|
||||
↓
|
||||
CPU 直接执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 执行速度快 | 编译时间长 |
|
||||
| 编译时发现错误 | 跨平台需要重新编译 |
|
||||
| 不需要运行时环境 | 调试较困难 |
|
||||
|
||||
**代表语言**:C, C++, Rust, Go
|
||||
|
||||
### 4.2 解释型语言
|
||||
|
||||
**流程**:源代码 → 解释器 → 逐行执行
|
||||
|
||||
```
|
||||
源代码 (main.py)
|
||||
↓
|
||||
解释器 (python)
|
||||
↓
|
||||
逐行解释执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
| 优点 | 缺点 |
|
||||
|------|------|
|
||||
| 跨平台 | 执行速度慢 |
|
||||
| 开发调试快 | 运行时才能发现错误 |
|
||||
| 代码即运行 | 需要解释器环境 |
|
||||
|
||||
**代表语言**:Python, JavaScript, Ruby, PHP
|
||||
|
||||
### 4.3 混合型语言(JIT)
|
||||
|
||||
**即时编译(Just-In-Time)**:先解释执行,热点代码编译成机器码
|
||||
|
||||
```
|
||||
源代码
|
||||
↓
|
||||
字节码(中间代码)
|
||||
↓
|
||||
解释执行 + JIT 编译热点代码
|
||||
↓
|
||||
执行
|
||||
```
|
||||
|
||||
**代表语言**:Java, JavaScript (V8), Python (PyPy)
|
||||
| | 编译型 | 解释型 | JIT |
|
||||
|---|---|---|---|
|
||||
| **过程** | 先全部翻译,再执行 | 边读边执行 | 先解释,热点再编译 |
|
||||
| **速度** | 最快 | 较慢 | 中等 |
|
||||
| **调试** | 需编译等待 | 即时反馈 | 即时 + 优化 |
|
||||
| **代表** | C, Rust, Go | Python, Ruby | Java, JavaScript |
|
||||
|
||||
---
|
||||
|
||||
## 5. 如何选择编程语言?
|
||||
|
||||
::: tip 💡 没有最好的语言,只有最适合的语言
|
||||
选择语言要考虑:
|
||||
1. **问题领域**:Web 开发?系统编程?数据分析?
|
||||
2. **团队熟悉度**:团队擅长什么?
|
||||
3. **生态系统**:有没有现成的库?
|
||||
4. **性能需求**:需要多高的性能?
|
||||
5. **开发效率**:需要多快开发完成?
|
||||
:::
|
||||
### 按场景选择
|
||||
|
||||
### 5.1 按应用场景选择
|
||||
|
||||
| 场景 | 推荐语言 | 原因 |
|
||||
|------|---------|------|
|
||||
| **Web 前端** | JavaScript, TypeScript | 浏览器原生支持 |
|
||||
| **Web 后端** | Java, Go, Python, Node.js | 生态成熟,框架丰富 |
|
||||
| 场景 | 推荐语言 | 理由 |
|
||||
|---|---|---|
|
||||
| **Web 前端** | JavaScript, TypeScript | 浏览器只认 JS |
|
||||
| **Web 后端** | Go, Java, Python, Node.js | 生态成熟 |
|
||||
| **移动开发** | Swift (iOS), Kotlin (Android) | 官方推荐 |
|
||||
| **数据分析** | Python, R | 库丰富,社区活跃 |
|
||||
| **人工智能** | Python | TensorFlow, PyTorch |
|
||||
| **系统编程** | C, C++, Rust | 性能高,控制精细 |
|
||||
| **游戏开发** | C++, C#, Lua | 引擎支持 |
|
||||
| **嵌入式** | C, Rust | 资源受限环境 |
|
||||
| **云原生** | Go, Rust | 并发友好,部署简单 |
|
||||
| **AI / 数据** | Python | PyTorch、Pandas 全在 Python |
|
||||
| **系统编程** | C, Rust | 直接操控硬件 |
|
||||
| **云原生** | Go, Rust | Docker/K8s 都是 Go 写的 |
|
||||
|
||||
### 5.2 学习路线建议
|
||||
### 学习路线建议
|
||||
|
||||
**初学者**:
|
||||
1. Python(语法简单,应用广泛)
|
||||
2. JavaScript(Web 开发必备)
|
||||
3. 选择一门静态类型语言(Java 或 TypeScript)
|
||||
|
||||
**进阶**:
|
||||
1. 学习 C 理解底层
|
||||
2. 学习函数式编程思想(Haskell 或 F#)
|
||||
3. 学习 Rust 理解内存安全
|
||||
1. **Python** — 语法最简单,AI 时代入口
|
||||
2. **JavaScript** — Web 开发必备,前后端通吃
|
||||
3. **TypeScript** — 给 JS 加上类型系统,体验静态类型
|
||||
4. **Go 或 Rust** — 理解编译型语言和底层概念
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
::: tip 📚 核心要点
|
||||
1. **编程语言演化**:从机器语言到高级语言,越来越接近人类思维
|
||||
2. **编程范式**:命令式、面向对象、函数式、声明式,各有优劣
|
||||
3. **类型系统**:静态/动态、强/弱类型,影响代码安全和灵活性
|
||||
4. **运行方式**:编译型快但需编译,解释型慢但灵活
|
||||
5. **选择语言**:没有银弹,根据场景选择合适的工具
|
||||
1. **语言演化**:从机器语言到高级语言,越来越接近人类思维
|
||||
2. **编程范式**:命令式、面向对象、函数式、声明式,各有适用场景
|
||||
3. **类型系统**:静态/动态、强/弱,影响安全性和灵活性
|
||||
4. **运行方式**:编译型快,解释型灵活,JIT 兼顾
|
||||
5. **没有银弹**:根据场景选语言,而不是追求"最好的语言"
|
||||
:::
|
||||
|
||||
**下一步学习**:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 从晶体管到 CPU
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
::: tip 核心问题
|
||||
**计算机是怎么"思考"的?** 你可能知道 CPU 是电脑的"大脑",但这个大脑到底是怎么工作的?它怎么从一堆金属和塑料变成能执行程序、处理数据的智能设备?本章带你从最底层的晶体管开始,一步步理解 CPU 的构造原理。
|
||||
:::
|
||||
|
||||
@@ -8,22 +8,20 @@
|
||||
|
||||
## 0. 全景图:从沙子到智能
|
||||
|
||||
<TransistorDemo />
|
||||
|
||||
现代计算机的"思考"能力,归根结底来自于一个简单的东西:**开关**。
|
||||
|
||||
想象你有一个开关,可以控制灯的亮灭。现在,如果你有几十亿个这样的开关,并且能用它们组合出各种复杂的逻辑,会发生什么?这就是计算机的奥秘。
|
||||
|
||||
**从沙子到智能的层次结构:**
|
||||
|
||||
| 层级 | 名称 | 数量级 | 作用 | 类比 |
|
||||
|------|------|--------|------|------|
|
||||
| **1** | 晶体管 | 数十亿 | 最基本的开关单元 | 一个开关 |
|
||||
| **2** | 逻辑门 | 数亿 | 实现基本逻辑运算 | 开关组合 |
|
||||
| **3** | 功能单元 | 数百 | 实现特定功能(加法、存储等) | 功能模块 |
|
||||
| **4** | CPU 核心 | 1-128 | 完整的处理器 | 大脑 |
|
||||
| 层级 | 名称 | 数量级 | 作用 | 类比 |
|
||||
| ----- | -------- | ------ | ---------------------------- | -------- |
|
||||
| **1** | 晶体管 | 数十亿 | 最基本的开关单元 | 一个开关 |
|
||||
| **2** | 逻辑门 | 数亿 | 实现基本逻辑运算 | 开关组合 |
|
||||
| **3** | 功能单元 | 数百 | 实现特定功能(加法、存储等) | 功能模块 |
|
||||
| **4** | CPU 核心 | 1-128 | 完整的处理器 | 大脑 |
|
||||
|
||||
::: tip 📊 逐行解读这张表
|
||||
::: tip 逐行解读这张表
|
||||
**第1层(晶体管)**:这是最底层的"开关"。现代 CPU 使用的是 MOSFET(金属氧化物半导体场效应晶体管),它的特点是:给栅极加电压,源极和漏极之间就导通;不加电压,就断开。这就是"用电控制电"的开关。
|
||||
|
||||
**第2层(逻辑门)**:把晶体管组合起来,就能实现"与"、"或"、"非"等逻辑运算。比如 AND 门:两个输入都为 1 时输出才为 1。这就像两个串联的开关,必须都按下灯才会亮。
|
||||
@@ -37,12 +35,15 @@
|
||||
|
||||
## 1. 晶体管:数字世界的开关
|
||||
|
||||
<TransistorDemo />
|
||||
|
||||
### 1.1 什么是晶体管?
|
||||
|
||||
::: tip 💡 晶体管是什么?
|
||||
::: tip 晶体管是什么?
|
||||
**晶体管(Transistor)** 是一种半导体器件,它可以像开关一样控制电流的通断。
|
||||
|
||||
**生活类比**:想象一个水龙头:
|
||||
|
||||
- **水龙头**:你用手拧开关,控制水流
|
||||
- **晶体管**:用电压控制开关,控制电流
|
||||
|
||||
@@ -51,23 +52,24 @@
|
||||
|
||||
**晶体管的三个极:**
|
||||
|
||||
| 极 | 名称 | 作用 | 类比 |
|
||||
|---|------|------|------|
|
||||
| **源极 (Source)** | 电流入口 | 电流从这里进入 | 水管入口 |
|
||||
| **漏极 (Drain)** | 电流出口 | 电流从这里流出 | 水管出口 |
|
||||
| **栅极 (Gate)** | 控制端 | 控制是否导通 | 水龙头开关 |
|
||||
| 极 | 名称 | 作用 | 类比 |
|
||||
| ----------------- | -------- | -------------- | ---------- |
|
||||
| **源极 (Source)** | 电流入口 | 电流从这里进入 | 水管入口 |
|
||||
| **漏极 (Drain)** | 电流出口 | 电流从这里流出 | 水管出口 |
|
||||
| **栅极 (Gate)** | 控制端 | 控制是否导通 | 水龙头开关 |
|
||||
|
||||
### 1.2 晶体管如何表示 0 和 1?
|
||||
|
||||
计算机只认识 0 和 1,这和晶体管有什么关系?
|
||||
|
||||
::: tip 💡 用电压表示 0 和 1
|
||||
::: tip 用电压表示 0 和 1
|
||||
**核心思想**:用电压的高低来表示 0 和 1。
|
||||
|
||||
- **高电压(如 3.3V)**:表示 1
|
||||
- **低电压(如 0V)**:表示 0
|
||||
|
||||
这就像灯泡的亮和灭:
|
||||
|
||||
- 灯亮 = 1
|
||||
- 灯灭 = 0
|
||||
|
||||
@@ -80,15 +82,15 @@
|
||||
|
||||
**现代 CPU 的晶体管数量:**
|
||||
|
||||
| 年份 | CPU | 晶体管数量 | 制程工艺 |
|
||||
|------|-----|-----------|---------|
|
||||
| 1971 | Intel 4004 | 2,300 | 10μm |
|
||||
| 1993 | Intel Pentium | 310万 | 0.8μm |
|
||||
| 2006 | Intel Core 2 | 2.91亿 | 65nm |
|
||||
| 2020 | Apple M1 | 160亿 | 5nm |
|
||||
| 2023 | Apple M3 Max | 920亿 | 3nm |
|
||||
| 年份 | CPU | 晶体管数量 | 制程工艺 |
|
||||
| ---- | ------------- | ---------- | -------- |
|
||||
| 1971 | Intel 4004 | 2,300 | 10μm |
|
||||
| 1993 | Intel Pentium | 310万 | 0.8μm |
|
||||
| 2006 | Intel Core 2 | 2.91亿 | 65nm |
|
||||
| 2020 | Apple M1 | 160亿 | 5nm |
|
||||
| 2023 | Apple M3 Max | 920亿 | 3nm |
|
||||
|
||||
::: tip 💡 什么是制程工艺?
|
||||
::: tip 什么是制程工艺?
|
||||
**制程工艺**(如 5nm、3nm)指的是晶体管的尺寸。数字越小,晶体管越小,同样面积能容纳的晶体管越多。
|
||||
|
||||
- **5nm**:大约是 50 个原子的宽度
|
||||
@@ -110,21 +112,25 @@
|
||||
### 2.2 基本逻辑门详解
|
||||
|
||||
**AND 门(与门)**:
|
||||
|
||||
- **规则**:两个输入都为 1,输出才为 1
|
||||
- **生活类比**:串联的两个开关,必须都按下灯才亮
|
||||
- **应用**:判断"多个条件是否同时满足"
|
||||
|
||||
**OR 门(或门)**:
|
||||
|
||||
- **规则**:任一个输入为 1,输出就为 1
|
||||
- **生活类比**:并联的两个开关,按任意一个灯就亮
|
||||
- **应用**:判断"是否满足任一条件"
|
||||
|
||||
**NOT 门(非门)**:
|
||||
|
||||
- **规则**:输入和输出相反
|
||||
- **生活类比**:反相器,开变关、关变开
|
||||
- **应用**:取反操作
|
||||
|
||||
**XOR 门(异或门)**:
|
||||
|
||||
- **规则**:两个输入不同时输出 1
|
||||
- **生活类比**:判断"两个值是否不同"
|
||||
- **应用**:比较、加法运算
|
||||
@@ -135,15 +141,18 @@
|
||||
|
||||
::: tip 💡 加法器是怎么工作的?
|
||||
**半加器**:处理两个 1 位二进制数相加
|
||||
|
||||
- 输入:A、B(各 1 位)
|
||||
- 输出:和(S)、进位(C)
|
||||
- 公式:S = A XOR B,C = A AND B
|
||||
|
||||
**全加器**:处理两个 1 位二进制数相加,加上上一位的进位
|
||||
|
||||
- 输入:A、B、Cin(进位输入)
|
||||
- 输出:和(S)、Cout(进位输出)
|
||||
|
||||
**多位加法器**:把多个全加器级联起来
|
||||
|
||||
- 第 1 位加法器的进位输出,连接到第 2 位加法器的进位输入
|
||||
- 就像我们手算加法时"逢二进一"
|
||||
:::
|
||||
@@ -154,13 +163,15 @@
|
||||
|
||||
### 3.1 常见功能单元
|
||||
|
||||
| 单元 | 功能 | 组成 | 类比 |
|
||||
|------|------|------|------|
|
||||
| **加法器** | 做加法 | 多个全加器级联 | 计算器的加法功能 |
|
||||
| **多路选择器** | 选择数据 | AND 门 + OR 门 | 多选一开关 |
|
||||
| **译码器** | 解码指令 | 多个 AND 门 | 翻译器 |
|
||||
| **寄存器** | 存储数据 | 触发器(锁存器) | 临时笔记本 |
|
||||
| **计数器** | 计数 | 触发器级联 | 计分牌 |
|
||||
| 单元 | 功能 | 组成 | 类比 |
|
||||
| -------------- | -------- | ---------------- | ---------------- |
|
||||
| **加法器** | 做加法 | 多个全加器级联 | 计算器的加法功能 |
|
||||
| **多路选择器** | 选择数据 | AND 门 + OR 门 | 多选一开关 |
|
||||
| **译码器** | 解码指令 | 多个 AND 门 | 翻译器 |
|
||||
| **寄存器** | 存储数据 | 触发器(锁存器) | 临时笔记本 |
|
||||
| **计数器** | 计数 | 触发器级联 | 计分牌 |
|
||||
|
||||
<RegisterDemo />
|
||||
|
||||
### 3.2 寄存器:存储 1 位数据
|
||||
|
||||
@@ -168,6 +179,7 @@
|
||||
寄存器使用**触发器**电路来存储数据。触发器的特点是:一旦设置了状态,就能保持住,直到下一次改变。
|
||||
|
||||
**生活类比**:想象一个跷跷板:
|
||||
|
||||
- 推一下左边,左边就沉下去,右边翘起来
|
||||
- 即使你松手,跷跷板也会保持这个状态
|
||||
- 只有再推一下,才会改变状态
|
||||
@@ -187,17 +199,18 @@
|
||||
|
||||
CPU 执行一条指令,需要经过四个阶段:
|
||||
|
||||
| 阶段 | 名称 | 做什么 | 类比 |
|
||||
|------|------|--------|------|
|
||||
| **1** | 取指 (Fetch) | 从内存读取指令 | 从书架上取书 |
|
||||
| **2** | 解码 (Decode) | 分析指令要做什么 | 阅读书的内容 |
|
||||
| **3** | 执行 (Execute) | 执行运算 | 按书中的指示行动 |
|
||||
| 阶段 | 名称 | 做什么 | 类比 |
|
||||
| ----- | ----------------- | ---------------- | ------------------ |
|
||||
| **1** | 取指 (Fetch) | 从内存读取指令 | 从书架上取书 |
|
||||
| **2** | 解码 (Decode) | 分析指令要做什么 | 阅读书的内容 |
|
||||
| **3** | 执行 (Execute) | 执行运算 | 按书中的指示行动 |
|
||||
| **4** | 写回 (Write Back) | 把结果存回寄存器 | 把结果记在笔记本上 |
|
||||
|
||||
::: tip 💡 指令周期
|
||||
这四个阶段组成一个**指令周期**。CPU 不断重复这个周期,一条一条执行指令,就实现了"计算"。
|
||||
|
||||
现代 CPU 使用**流水线技术**,让多个指令的不同阶段并行执行:
|
||||
|
||||
- 第 1 条指令在执行时
|
||||
- 第 2 条指令在解码
|
||||
- 第 3 条指令在取指
|
||||
@@ -207,12 +220,12 @@ CPU 执行一条指令,需要经过四个阶段:
|
||||
|
||||
### 4.3 CPU 性能的关键指标
|
||||
|
||||
| 指标 | 含义 | 影响 | 典型值 |
|
||||
|------|------|------|--------|
|
||||
| **主频** | 每秒执行多少个时钟周期 | 主频越高,执行越快 | 3-5 GHz |
|
||||
| **核心数** | 独立的处理器数量 | 核心越多,并行能力越强 | 4-64 核 |
|
||||
| **缓存** | CPU 内部的高速存储 | 缓存越大,访问内存越少 | 8-64 MB |
|
||||
| **指令集** | CPU 能理解的指令集合 | 决定兼容性和功能 | x86、ARM |
|
||||
| 指标 | 含义 | 影响 | 典型值 |
|
||||
| ---------- | ---------------------- | ---------------------- | -------- |
|
||||
| **主频** | 每秒执行多少个时钟周期 | 主频越高,执行越快 | 3-5 GHz |
|
||||
| **核心数** | 独立的处理器数量 | 核心越多,并行能力越强 | 4-64 核 |
|
||||
| **缓存** | CPU 内部的高速存储 | 缓存越大,访问内存越少 | 8-64 MB |
|
||||
| **指令集** | CPU 能理解的指令集合 | 决定兼容性和功能 | x86、ARM |
|
||||
|
||||
---
|
||||
|
||||
@@ -220,23 +233,9 @@ CPU 执行一条指令,需要经过四个阶段:
|
||||
|
||||
让我们回顾一下从晶体管到 CPU 的完整路径:
|
||||
|
||||
```
|
||||
沙子(硅)
|
||||
↓ 提纯、切割
|
||||
硅晶圆
|
||||
↓ 光刻、蚀刻、掺杂
|
||||
晶体管(开关)
|
||||
↓ 组合
|
||||
逻辑门(AND、OR、NOT...)
|
||||
↓ 组合
|
||||
功能单元(加法器、寄存器...)
|
||||
↓ 组合
|
||||
CPU 核心(ALU、控制器、寄存器组...)
|
||||
↓ 编程
|
||||
软件应用
|
||||
```
|
||||
<EvolutionFlowDemo />
|
||||
|
||||
::: tip 💡 核心启示
|
||||
::: tip 核心启示
|
||||
**计算机的本质是"开关的组合"**。
|
||||
|
||||
- 一个开关做不了什么
|
||||
@@ -244,6 +243,7 @@ CPU 核心(ALU、控制器、寄存器组...)
|
||||
- 这就是"量变引起质变"的最好例证
|
||||
|
||||
理解这一点,你就会明白:
|
||||
|
||||
- 为什么计算机只认识 0 和 1
|
||||
- 为什么编程语言最终都要翻译成机器码
|
||||
- 为什么算法效率如此重要(因为每一步操作都需要大量晶体管参与)
|
||||
|
||||
@@ -1,475 +1,151 @@
|
||||
# 类型系统与编译原理入门
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**编程语言如何理解你的代码?** 当你写下 `int x = 10 + 5;` 时,编译器需要理解每个字符的含义、检查类型是否正确、优化代码、最终生成机器能执行的指令。本章带你理解这个神奇的过程。
|
||||
:::
|
||||
> 💡 **学习指南**:当你写下 `int x = 10 + 5;` 时,编译器是如何理解每个字符、检查类型是否正确、最终生成机器指令的?本章用两个核心概念——**类型系统**和**编译流程**——帮你理解编程语言背后的"翻译机制"。
|
||||
|
||||
---
|
||||
|
||||
## 0. 想象你在翻译一本书:
|
||||
## 0. 想象你是翻译官
|
||||
|
||||
- **识别单词**:把句子拆成一个个单词(词法分析)
|
||||
- **理解语法**:判断句子是否符合语法规则(语法分析)
|
||||
- **理解含义**:确保句子意思正确(语义分析)
|
||||
- **优化表达**:让句子更简洁(代码优化)
|
||||
- **翻译输出**:翻译成目标语言(代码生成)
|
||||
翻译一本书,你需要:
|
||||
|
||||
**编译器就是编程语言的"翻译官"**,将人类可读的代码转换为机器可执行的指令。
|
||||
1. **识别单词** — 把句子拆成一个个单词(词法分析)
|
||||
2. **理解语法** — 判断句子是否符合语法规则(语法分析)
|
||||
3. **理解含义** — 确保句子意思正确,类型不冲突(语义分析)
|
||||
4. **优化表达** — 让句子更简洁流畅(代码优化)
|
||||
5. **翻译输出** — 翻译成目标语言(代码生成)
|
||||
|
||||
**编译器就是编程语言的"翻译官"**,将你写的代码转换为机器能执行的指令。而**类型系统**就是翻译过程中的"语法检查器"——确保你不会把数字当文字用。
|
||||
|
||||
---
|
||||
|
||||
## 1. 类型系统:数据的交通规则
|
||||
|
||||
👇 动手点点看:探索四种类型系统的区别
|
||||
|
||||
<TypeSystemDemo />
|
||||
|
||||
::: tip 💡 一句话总结
|
||||
类型系统在两个维度上做选择:**何时检查**(编译时 vs 运行时)和**是否允许隐式转换**(强类型 vs 弱类型)。没有最好的组合,只有最适合的场景。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 类型系统基础
|
||||
### 1.1 静态类型 vs 动态类型
|
||||
|
||||
### 1.1 什么是类型?
|
||||
| | 静态类型 | 动态类型 |
|
||||
|---|---|---|
|
||||
| **检查时机** | 编译时(还没运行就检查) | 运行时(跑到那行才检查) |
|
||||
| **发现 bug** | 早(写完就知道) | 晚(用户操作时才暴露) |
|
||||
| **灵活性** | 较低(类型固定) | 较高(类型可变) |
|
||||
| **IDE 支持** | 好(自动补全、重构) | 差(运行时才知道类型) |
|
||||
| **代表** | Java, TypeScript, Rust | Python, JavaScript, Ruby |
|
||||
|
||||
::: tip 💡 类型的本质
|
||||
类型是对数据的**分类**,规定了数据可以进行的操作。
|
||||
### 1.2 强类型 vs 弱类型
|
||||
|
||||
就像现实世界:
|
||||
- **整数**:可以加减乘除,但不能分割
|
||||
- **字符串**:可以拼接、截取,但不能直接运算
|
||||
- **布尔**:只有 true/false,用于逻辑判断
|
||||
:::
|
||||
**核心区别**:`"1" + 1` 会发生什么?
|
||||
|
||||
**基本数据类型**:
|
||||
- **强类型(Python)**:直接报错 `TypeError` — "你得明确告诉我怎么转"
|
||||
- **弱类型(JavaScript)**:悄悄转成 `"11"` — "我猜你想拼字符串"
|
||||
|
||||
| 类型 | 表示 | 占用空间 | 取值范围 |
|
||||
|------|------|---------|---------|
|
||||
| **整数** | int | 4 字节 | -2^31 到 2^31-1 |
|
||||
| **浮点数** | float | 4 字节 | 约 ±3.4 × 10^38 |
|
||||
| **双精度** | double | 8 字节 | 约 ±1.8 × 10^308 |
|
||||
| **字符** | char | 1 字节 | 0 到 255 |
|
||||
| **布尔** | bool | 1 字节 | true/false |
|
||||
弱类型的"好意"常常带来意想不到的 bug。
|
||||
|
||||
### 1.2 静态类型 vs 动态类型
|
||||
### 1.3 类型推断:两全其美
|
||||
|
||||
::: tip 💡 核心区别
|
||||
**静态类型**:变量类型在**编译时**确定
|
||||
**动态类型**:变量类型在**运行时**确定
|
||||
:::
|
||||
|
||||
**静态类型示例(Java)**:
|
||||
|
||||
```java
|
||||
String name = "Alice"; // 编译时确定 name 是 String 类型
|
||||
name = 123; // 编译错误!类型不匹配
|
||||
```
|
||||
|
||||
**动态类型示例(Python)**:
|
||||
|
||||
```python
|
||||
name = "Alice" # 运行时 name 是 str 类型
|
||||
name = 123 # 运行时 name 变成 int 类型
|
||||
print(type(name)) # <class 'int'>
|
||||
```
|
||||
|
||||
**对比分析**:
|
||||
|
||||
| 特性 | 静态类型 | 动态类型 |
|
||||
|------|---------|---------|
|
||||
| **类型检查时机** | 编译时 | 运行时 |
|
||||
| **错误发现** | 早(编译期) | 晚(运行时) |
|
||||
| **代码灵活性** | 低 | 高 |
|
||||
| **执行性能** | 高(编译优化) | 低(运行时检查) |
|
||||
| **IDE 支持** | 好(自动补全) | 差(运行时才知道类型) |
|
||||
| **代表语言** | Java, C++, Rust, TypeScript | Python, JavaScript, Ruby |
|
||||
|
||||
### 1.3 强类型 vs 弱类型
|
||||
|
||||
::: tip 💡 核心区别
|
||||
**强类型**:不允许隐式类型转换
|
||||
**弱类型**:允许隐式类型转换
|
||||
:::
|
||||
|
||||
**弱类型示例(JavaScript)**:
|
||||
|
||||
```javascript
|
||||
console.log("1" + 1) // "11" - 字符串拼接
|
||||
console.log("1" - 1) // 0 - 自动转数字
|
||||
console.log([] + []) // "" - 空数组转空字符串
|
||||
console.log(true + 1) // 2 - 布尔转数字
|
||||
```
|
||||
|
||||
**强类型示例(Python)**:
|
||||
|
||||
```python
|
||||
"1" + 1 # TypeError: can only concatenate str to str
|
||||
"1" - 1 # TypeError: unsupported operand type(s)
|
||||
```
|
||||
|
||||
**类型系统四象限**:
|
||||
|
||||
| | 强类型 | 弱类型 |
|
||||
|---|--------|--------|
|
||||
| **静态** | Java, Rust, Haskell | C, C++ |
|
||||
| **动态** | Python, Ruby | JavaScript, PHP |
|
||||
|
||||
### 1.4 类型推断
|
||||
|
||||
现代语言可以**自动推断**变量类型,结合静态类型的安全性和动态类型的简洁性:
|
||||
现代语言的类型推断让你**写着像动态语言,编译器检查像静态语言**:
|
||||
|
||||
```typescript
|
||||
// TypeScript
|
||||
let x = 1; // 推断为 number
|
||||
let arr = [1, 2, 3]; // 推断为 number[]
|
||||
let fn = (x) => x; // 推断为 (x: any) => any
|
||||
|
||||
// Rust
|
||||
let x = 1; // 推断为 i32
|
||||
let s = "hello"; // 推断为 &str
|
||||
let v = vec![1, 2]; // 推断为 Vec<i32>
|
||||
let x = 1 // 编译器自动推断为 number
|
||||
let arr = [1, 2, 3] // 推断为 number[]
|
||||
x = "hello" // ❌ 编译错误!类型不匹配
|
||||
```
|
||||
|
||||
你不用显式写类型声明,编译器也能帮你严格检查。
|
||||
|
||||
---
|
||||
|
||||
## 2. 编译原理基础
|
||||
## 2. 编译流程:从代码到机器码
|
||||
|
||||
### 2.1 编译器的任务
|
||||
|
||||
::: tip 💡 编译器做什么?
|
||||
编译器将**源代码**转换为**目标代码**,主要完成:
|
||||
|
||||
1. **理解代码**:分析源代码的结构和含义
|
||||
2. **检查正确性**:发现语法和语义错误
|
||||
3. **优化代码**:提高执行效率
|
||||
4. **生成代码**:输出目标机器的指令
|
||||
:::
|
||||
👇 动手点点看:输入代码,观察编译器的六步翻译过程
|
||||
|
||||
<CompilerDemo />
|
||||
|
||||
### 2.2 词法分析(Lexical Analysis)
|
||||
::: tip 💡 一句话总结
|
||||
编译器的六步流水线:源代码 → Token(词法分析)→ AST(语法分析)→ 带类型的 AST(语义分析)→ IR(中间代码)→ 优化后的 IR → 机器码。
|
||||
:::
|
||||
|
||||
**任务**:将源代码分解为**词法单元(Token)**
|
||||
---
|
||||
|
||||
**示例**:
|
||||
### 2.1 词法分析:拆出每个"单词"
|
||||
|
||||
```
|
||||
源代码: int x = 10 + 5;
|
||||
|
||||
词法单元:
|
||||
Token 流:
|
||||
[int] → 关键字
|
||||
[x] → 标识符
|
||||
[=] → 运算符
|
||||
[10] → 整数字面量
|
||||
[10] → 数字
|
||||
[+] → 运算符
|
||||
[5] → 整数字面量
|
||||
[5] → 数字
|
||||
[;] → 分隔符
|
||||
```
|
||||
|
||||
**词法分析器的工作**:
|
||||
|
||||
| 输入 | 处理 | 输出 |
|
||||
|------|------|------|
|
||||
| `int` | 匹配关键字表 | `KEYWORD(int)` |
|
||||
| `x` | 匹配标识符规则 | `IDENTIFIER(x)` |
|
||||
| `10` | 匹配数字规则 | `NUMBER(10)` |
|
||||
|
||||
### 2.3 语法分析(Syntax Analysis)
|
||||
|
||||
**任务**:根据语法规则,将 Token 流组织成**语法树(AST)**
|
||||
|
||||
**示例**:
|
||||
### 2.2 语法分析:构建语法树(AST)
|
||||
|
||||
```
|
||||
表达式: 1 + 2 * 3
|
||||
|
||||
语法树:
|
||||
+
|
||||
/ \
|
||||
1 *
|
||||
语法树: 为什么?
|
||||
+ 因为 * 的优先级
|
||||
/ \ 高于 +,所以
|
||||
1 * 2 * 3 先结合
|
||||
/ \
|
||||
2 3
|
||||
```
|
||||
|
||||
::: tip 💡 为什么是这棵树?
|
||||
根据运算优先级,`*` 优先级高于 `+`,所以 `2 * 3` 先结合。
|
||||
### 2.3 语义分析:检查"意思"是否正确
|
||||
|
||||
如果表达式是 `(1 + 2) * 3`,语法树会变成:
|
||||
| 检查内容 | 示例 | 结果 |
|
||||
|---|---|---|
|
||||
| 类型检查 | `int x = "hello"` | ❌ 类型不匹配 |
|
||||
| 作用域分析 | 使用未声明的变量 | ❌ 变量不存在 |
|
||||
| 类型推断 | `1 + 2.0` | ✅ 推断为 float |
|
||||
|
||||
```
|
||||
*
|
||||
/ \
|
||||
+ 3
|
||||
/ \
|
||||
1 2
|
||||
```
|
||||
:::
|
||||
### 2.4 代码优化:让程序跑得更快
|
||||
|
||||
**语法规则(文法)**:
|
||||
|
||||
```
|
||||
表达式 → 表达式 + 项 | 表达式 - 项 | 项
|
||||
项 → 项 * 因子 | 项 / 因子 | 因子
|
||||
因子 → 数字 | (表达式)
|
||||
```
|
||||
|
||||
### 2.4 语义分析(Semantic Analysis)
|
||||
|
||||
**任务**:检查语义正确性,进行类型检查
|
||||
|
||||
**主要工作**:
|
||||
|
||||
| 工作 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **类型检查** | 检查类型是否匹配 | `int x = "hello";` → 错误 |
|
||||
| **作用域分析** | 检查变量是否声明 | 使用未声明变量 → 错误 |
|
||||
| **符号表构建** | 记录所有标识符信息 | 变量名、类型、作用域 |
|
||||
| **类型推断** | 推断表达式类型 | `1 + 2.0` → float |
|
||||
|
||||
**符号表示例**:
|
||||
|
||||
```
|
||||
int x = 10;
|
||||
float y = 3.14;
|
||||
string name = "Alice";
|
||||
|
||||
符号表:
|
||||
┌──────────┬────────┬─────────┐
|
||||
│ 名称 │ 类型 │ 作用域 │
|
||||
├──────────┼────────┼─────────┤
|
||||
│ x │ int │ global │
|
||||
│ y │ float │ global │
|
||||
│ name │ string │ global │
|
||||
└──────────┴────────┴─────────┘
|
||||
```
|
||||
|
||||
### 2.5 中间代码生成
|
||||
|
||||
**任务**:生成平台无关的中间表示(IR)
|
||||
|
||||
**三地址码示例**:
|
||||
|
||||
```
|
||||
源代码: int x = (a + b) * c;
|
||||
|
||||
三地址码:
|
||||
t1 = a + b
|
||||
t2 = t1 * c
|
||||
x = t2
|
||||
```
|
||||
|
||||
::: tip 💡 为什么需要中间代码?
|
||||
1. **平台无关**:一次编写,多平台编译
|
||||
2. **便于优化**:在 IR 层面进行优化
|
||||
3. **支持多语言**:不同语言可以编译到同一 IR
|
||||
|
||||
例如 LLVM IR 支持 C、C++、Rust、Swift 等多种语言。
|
||||
:::
|
||||
|
||||
### 2.6 代码优化
|
||||
|
||||
**任务**:提高代码执行效率
|
||||
|
||||
**常见优化技术**:
|
||||
|
||||
| 优化技术 | 说明 | 示例 |
|
||||
|---------|------|------|
|
||||
| **常量折叠** | 编译时计算常量表达式 | `10 + 5` → `15` |
|
||||
| **死代码消除** | 删除不会执行的代码 | `if (false) { ... }` → 删除 |
|
||||
| **内联展开** | 函数调用替换为函数体 | `add(1, 2)` → `1 + 2` |
|
||||
| **循环优化** | 减少循环开销 | 循环展开、循环不变量外提 |
|
||||
| **公共子表达式消除** | 避免重复计算 | `a+b` 计算一次,多次使用 |
|
||||
|
||||
**优化示例**:
|
||||
|
||||
```c
|
||||
// 优化前
|
||||
int x = 10 + 5; // 常量折叠
|
||||
int y = x * 2; // x 已知为 15
|
||||
if (false) { // 死代码
|
||||
printf("never");
|
||||
}
|
||||
|
||||
// 优化后
|
||||
int x = 15;
|
||||
int y = 30;
|
||||
// if 语句被删除
|
||||
```
|
||||
|
||||
### 2.7 目标代码生成
|
||||
|
||||
**任务**:生成目标机器的机器码
|
||||
|
||||
**汇编代码示例**:
|
||||
|
||||
```asm
|
||||
; int x = 15;
|
||||
mov eax, 15
|
||||
mov dword ptr [x], eax
|
||||
|
||||
; int y = 30;
|
||||
mov eax, 30
|
||||
mov dword ptr [y], eax
|
||||
```
|
||||
|
||||
**代码生成的主要任务**:
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| **指令选择** | 选择合适的机器指令 |
|
||||
| **寄存器分配** | 决定哪些变量放在寄存器 |
|
||||
| **指令调度** | 安排指令顺序,提高流水线效率 |
|
||||
| 优化技术 | 优化前 | 优化后 |
|
||||
|---|---|---|
|
||||
| 常量折叠 | `x = 10 + 5` | `x = 15` |
|
||||
| 死代码消除 | `if (false) { ... }` | 直接删除 |
|
||||
| 常量传播 | `y = x * 2`(x=15) | `y = 30` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 编译型 vs 解释型 vs JIT
|
||||
|
||||
### 3.1 编译型语言
|
||||
程序写完后,有三种"翻译方式"让它运行:
|
||||
|
||||
**流程**:源代码 → 编译器 → 机器码 → 执行
|
||||
| | 编译型 | 解释型 | JIT 即时编译 |
|
||||
|---|---|---|---|
|
||||
| **过程** | 先编译成机器码,再执行 | 边读边执行 | 先解释,热点代码再编译 |
|
||||
| **速度** | 最快 | 最慢 | 中等(热点代码接近编译型) |
|
||||
| **启动** | 慢(需编译) | 快(直接运行) | 中等(需预热) |
|
||||
| **跨平台** | 需要重新编译 | 天然跨平台 | 跨平台 |
|
||||
| **代表** | C, Rust, Go | Python, Ruby | Java, JavaScript (V8) |
|
||||
|
||||
```
|
||||
main.c → [编译器] → main.exe → [CPU] → 执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- ✅ 执行速度快
|
||||
- ✅ 编译期发现错误
|
||||
- ❌ 编译时间长
|
||||
- ❌ 跨平台需要重新编译
|
||||
|
||||
**代表语言**:C, C++, Rust, Go
|
||||
|
||||
### 3.2 解释型语言
|
||||
|
||||
**流程**:源代码 → 解释器 → 逐行执行
|
||||
|
||||
```
|
||||
main.py → [解释器] → 逐行解释执行
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- ✅ 跨平台
|
||||
- ✅ 开发调试快
|
||||
- ❌ 执行速度慢
|
||||
- ❌ 运行时才能发现错误
|
||||
|
||||
**代表语言**:Python, Ruby, PHP
|
||||
|
||||
### 3.3 JIT(即时编译)
|
||||
|
||||
**流程**:源代码 → 字节码 → JIT 编译 → 执行
|
||||
|
||||
```
|
||||
源代码 → [编译器] → 字节码 → [JIT] → 机器码 → 执行
|
||||
```
|
||||
|
||||
**工作原理**:
|
||||
1. 先将源代码编译成字节码(中间代码)
|
||||
2. 解释器逐行执行字节码
|
||||
3. 发现热点代码(频繁执行),JIT 编译成机器码
|
||||
4. 后续直接执行机器码
|
||||
|
||||
**特点**:
|
||||
- ✅ 兼顾性能和跨平台
|
||||
- ✅ 热点代码执行快
|
||||
- ❌ 启动慢(需要预热)
|
||||
- ❌ 内存占用大
|
||||
|
||||
**代表语言**:Java (JVM), JavaScript (V8), Python (PyPy)
|
||||
::: tip 💡 为什么 JavaScript 这么快?
|
||||
V8 引擎的 JIT 编译器会监测哪些代码被频繁执行(热点代码),然后把它们编译成高度优化的机器码。所以虽然 JavaScript 是"解释型语言",但在 V8 中它的性能可以接近编译型语言。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 4. 实践:手写简单解释器
|
||||
|
||||
### 4.1 目标
|
||||
|
||||
实现一个简单的计算器,支持加减乘除:
|
||||
|
||||
```
|
||||
输入: 1 + 2 * 3
|
||||
输出: 7
|
||||
```
|
||||
|
||||
### 4.2 词法分析器
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
Token = namedtuple('Token', ['type', 'value'])
|
||||
|
||||
def tokenize(code):
|
||||
tokens = []
|
||||
for match in re.finditer(r'\d+|[+\-*/()]', code):
|
||||
value = match.group()
|
||||
if value.isdigit():
|
||||
tokens.append(Token('NUMBER', int(value)))
|
||||
else:
|
||||
tokens.append(Token(value, value))
|
||||
return tokens
|
||||
|
||||
# 测试
|
||||
print(tokenize('1 + 2 * 3'))
|
||||
# [Token(type='NUMBER', value=1), Token(type='+', value='+'), ...]
|
||||
```
|
||||
|
||||
### 4.3 语法分析器
|
||||
|
||||
```python
|
||||
class Parser:
|
||||
def __init__(self, tokens):
|
||||
self.tokens = tokens
|
||||
self.pos = 0
|
||||
|
||||
def parse(self):
|
||||
return self.expr()
|
||||
|
||||
def expr(self):
|
||||
result = self.term()
|
||||
while self.current() in ('+', '-'):
|
||||
op = self.consume()
|
||||
right = self.term()
|
||||
if op == '+':
|
||||
result += right
|
||||
else:
|
||||
result -= right
|
||||
return result
|
||||
|
||||
def term(self):
|
||||
result = self.factor()
|
||||
while self.current() in ('*', '/'):
|
||||
op = self.consume()
|
||||
right = self.factor()
|
||||
if op == '*':
|
||||
result *= right
|
||||
else:
|
||||
result //= right
|
||||
return result
|
||||
|
||||
def factor(self):
|
||||
token = self.consume()
|
||||
if token.type == 'NUMBER':
|
||||
return token.value
|
||||
elif token.value == '(':
|
||||
result = self.expr()
|
||||
self.consume() # )
|
||||
return result
|
||||
```
|
||||
|
||||
### 4.4 完整解释器
|
||||
|
||||
```python
|
||||
def evaluate(code):
|
||||
tokens = tokenize(code)
|
||||
parser = Parser(tokens)
|
||||
return parser.parse()
|
||||
|
||||
print(evaluate('1 + 2 * 3')) # 7
|
||||
print(evaluate('(1 + 2) * 3')) # 9
|
||||
print(evaluate('10 - 2 * 3')) # 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
## 4. 总结
|
||||
|
||||
::: tip 📚 核心要点
|
||||
1. **类型系统**:静态/动态、强/弱类型,影响代码安全和灵活性
|
||||
2. **编译流程**:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成
|
||||
3. **执行方式**:编译型快但需编译,解释型慢但灵活,JIT 兼顾两者
|
||||
4. **实践价值**:理解编译原理有助于写出更好的代码
|
||||
1. **类型系统**:静态/动态决定检查时机,强/弱决定是否允许隐式转换
|
||||
2. **编译六步**:词法分析 → 语法分析 → 语义分析 → 中间代码 → 优化 → 代码生成
|
||||
3. **三种执行**:编译型快但需编译,解释型灵活但慢,JIT 兼顾两者
|
||||
4. **类型推断**:现代语言让你享受动态语言的简洁和静态语言的安全
|
||||
:::
|
||||
|
||||
**下一步学习**:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 编辑器与 AI 编程助手
|
||||
|
||||
> 待实现
|
||||
@@ -1,3 +1,179 @@
|
||||
# 环境变量与 PATH
|
||||
|
||||
> 待实现
|
||||
> 💡 **学习指南**:每次你在终端输入 `git` 或 `python`,系统都要去找这个程序在哪里。每次你的代码调用大模型 API,程序要知道用哪个密钥。这两件事背后都是同一套机制——**环境变量**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 每个程序身边都带着一组配置
|
||||
|
||||
运行中的每个程序,都持有一组「键=值」配置,叫做**环境变量**。程序可以随时读取这些配置,用来了解当前的运行环境。
|
||||
|
||||
点击下方列表里的任意变量,在终端里"查看"它的值:
|
||||
|
||||
<EnvVarOverviewDemo />
|
||||
|
||||
---
|
||||
|
||||
## 1. PATH:Shell 怎么找到你输入的命令
|
||||
|
||||
`PATH` 是一个特殊的环境变量,存着一串目录路径(用冒号分隔)。你输入 `git` 时,Shell 就按这串目录的顺序,一个一个地进去找名叫 `git` 的可执行文件——找到第一个就立刻停止。
|
||||
|
||||
```bash
|
||||
$ echo $PATH
|
||||
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
```
|
||||
|
||||
选择一个命令,观察 Shell 逐目录搜索的过程:
|
||||
|
||||
<PathSearchDemo />
|
||||
|
||||
**三个关键规律**:
|
||||
- 目录在 PATH 里越靠前,优先级越高
|
||||
- 找到第一个就停止,不会继续搜索
|
||||
- 所有目录都没有 → `command not found`
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么安装工具后要重启终端?
|
||||
|
||||
安装 nvm、Homebrew、conda 这类工具时,安装脚本会自动在 `~/.zshrc` 里追加一行,把自己的目录加入 PATH:
|
||||
|
||||
```bash
|
||||
# 安装脚本自动写入的内容(示例)
|
||||
export PATH="/usr/local/opt/python@3.12/bin:$PATH"
|
||||
```
|
||||
|
||||
这行代码只在**新 Shell 启动时**才执行。已经打开的终端窗口不受影响,所以:
|
||||
|
||||
```bash
|
||||
# 不重启也能立刻生效
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
**AI 开发工具常见情况**:
|
||||
|
||||
```bash
|
||||
# Ollama / pipx 装完报 command not found
|
||||
which ollama # 查实际安装位置
|
||||
|
||||
# pip 安装的 CLI 工具路径(加入 PATH)
|
||||
# macOS:~/Library/Python/3.x/bin
|
||||
# Linux:~/.local/bin
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
|
||||
# 推荐用 pipx 安装命令行工具,自动管理 PATH
|
||||
pipx install aider-chat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 变量的作用域:谁能看见这个变量?
|
||||
|
||||
环境变量不是广播给所有程序的——每个进程持有**自己的一份副本**,从父进程继承而来,修改自己的副本不会影响父进程。
|
||||
|
||||
下图展示三个层级。在「用户级」里 export 一个新变量,看它是否出现在「进程级」:
|
||||
|
||||
<EnvScopeDemo />
|
||||
|
||||
---
|
||||
|
||||
## 4. export:决定子进程能不能读到这个变量
|
||||
|
||||
设置变量时,加不加 `export` 是完全不同的两件事:
|
||||
|
||||
<EnvExportDemo />
|
||||
|
||||
要让变量跨会话永久存在,把 `export` 写入配置文件:
|
||||
|
||||
```bash
|
||||
# macOS (zsh)
|
||||
echo 'export MY_VAR="value"' >> ~/.zshrc
|
||||
source ~/.zshrc # 立刻生效,不用重开终端
|
||||
|
||||
# Linux (bash)
|
||||
echo 'export MY_VAR="value"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 密钥:绝对不能写进代码
|
||||
|
||||
调用 OpenAI、Anthropic、DeepSeek 等 API 时,密钥就是你的「身份证 + 信用卡」。泄露了,别人可以用你的额度消费,费用由你承担。
|
||||
|
||||
最常见的错误是把密钥直接写在代码里:
|
||||
|
||||
<ApiKeyDangerDemo />
|
||||
|
||||
---
|
||||
|
||||
## 6. 本地开发:用 .env 文件管密钥
|
||||
|
||||
本地开发时,把密钥放在项目根目录的 `.env` 文件里,代码通过 dotenv 库读取。`.env` 必须加入 `.gitignore`,不能提交到 Git。
|
||||
|
||||
左边写配置,右边读取——切换语言看两种写法:
|
||||
|
||||
<DotEnvDemo />
|
||||
|
||||
---
|
||||
|
||||
## 7. 生产环境:让运行平台注入密钥
|
||||
|
||||
`.env` 是开发阶段的便利工具。服务器和云平台上,应该由**运行环境**负责注入密钥,代码本身完全不感知密钥放在哪里:
|
||||
|
||||
<ServerSecretDemo />
|
||||
|
||||
---
|
||||
|
||||
## 8. 实战排错
|
||||
|
||||
### `command not found`
|
||||
|
||||
```bash
|
||||
# 第一步:确认是否在 PATH 里
|
||||
which python3 # 有输出说明找到了
|
||||
|
||||
# 第二步:找到程序实际位置(macOS)
|
||||
brew list python | grep bin
|
||||
|
||||
# 第三步:把目录加入 PATH
|
||||
export PATH="/找到的路径:$PATH"
|
||||
source ~/.zshrc # 写入配置文件后记得 source
|
||||
```
|
||||
|
||||
### 装了两个版本,用的不是我想要的
|
||||
|
||||
```bash
|
||||
which python
|
||||
# /usr/bin/python ← 系统旧版,在 PATH 靠前
|
||||
|
||||
# 把新版目录放到 PATH 最前面
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
|
||||
which python
|
||||
# /usr/local/bin/python ← 新版,现在优先了
|
||||
```
|
||||
|
||||
### 变量明明设置了,程序却读不到
|
||||
|
||||
| 原因 | 解决 |
|
||||
|:---|:---|
|
||||
| 忘了 `export` | 加上 `export` 再试 |
|
||||
| 改了 `~/.zshrc` 没生效 | `source ~/.zshrc` |
|
||||
| 用了 `.env` 但没装 dotenv | `pip install python-dotenv` / `npm install dotenv` |
|
||||
| 服务器上只在 SSH 会话有效 | 改用 systemd `EnvironmentFile` |
|
||||
|
||||
---
|
||||
|
||||
## 名词速查
|
||||
|
||||
| 术语 | 含义 |
|
||||
|:---|:---|
|
||||
| **PATH** | 存储 Shell 搜索可执行文件的目录列表,冒号分隔,顺序决定优先级 |
|
||||
| **export** | 将变量标记为可继承,子进程启动时自动获得副本 |
|
||||
| **source** | 在当前 Shell 重新执行配置文件,使修改立即生效 |
|
||||
| **which** | 显示某命令对应的可执行文件路径(PATH 搜索的结果) |
|
||||
| **.env** | 项目本地配置文件,存开发用密钥,必须加入 `.gitignore` |
|
||||
| **.env.example** | 变量名完整、值留空的模板,可以安全提交到 Git |
|
||||
| **chmod 600** | 文件权限:只有所有者可读写,适合保护密钥文件 |
|
||||
| **Secret Scanner** | GitHub 等平台自动扫描密钥泄露,发现后通知厂商吊销 |
|
||||
|
||||
@@ -1,348 +1,558 @@
|
||||
# Git:代码的时光机
|
||||
::: tip 🎯 核心问题
|
||||
**写代码时最怕什么?** 写错了想回退、改崩了想重来、多人同时改同一个文件...这些头疼的事,Git 都能帮你搞定!它就像是代码世界的"时光机",让你随时回到过去,又能和队友在各自的"平行宇宙"里安全开发。
|
||||
:::
|
||||
|
||||
> 💡 **学习指南**:这一章专门写给完全没用过 Git 的人。我们不会上来就让你背命令,而是先搞清楚"Git 到底在帮你解决什么问题",再一步步把命令和概念串起来。读完后,你应该能独立完成:本地提交、创建分支、推送到 GitHub。
|
||||
|
||||
---
|
||||
|
||||
## 0. 最常用的 5 个场景(直接照抄)
|
||||
## 0. 先问一个问题:你有没有经历过这些噩梦?
|
||||
|
||||
如果你只想"立刻能用",先把这块过一遍:每个场景都是现实工作中最常见的 Git 流程。
|
||||
**场景一:版本地狱**
|
||||
|
||||
<GitScenariosDemo />
|
||||
你写论文或者写代码,改到一半发现改错了,想回到三天前的版本——但你找不到了。
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要学 Git?三大痛点
|
||||
|
||||
### 1.1 痛点一:版本混乱
|
||||
|
||||
你是否经历过这种绝望?
|
||||
|
||||
```text
|
||||
论文_初稿.doc
|
||||
论文_修改版.doc
|
||||
论文_最终版.doc
|
||||
论文_最终版_打死不改版.doc
|
||||
论文_绝对是最后一次修改版.doc
|
||||
```
|
||||
项目_v1.zip
|
||||
项目_v2_修改版.zip
|
||||
项目_v3_最终版.zip
|
||||
项目_v3_最终版_真的最终版.zip
|
||||
项目_v3_最终版_打死不改了.zip
|
||||
```
|
||||
|
||||
**Git 的解决方案**:不需要复制副本,一个文件夹搞定所有历史版本。想回到哪次修改,一键恢复。
|
||||
每次存一个新副本,硬盘越来越乱,而且你根本记不住哪个版本改了什么。
|
||||
|
||||
### 1.2 痛点二:无法后悔
|
||||
**场景二:协作噩梦**
|
||||
|
||||
::: tip 💡 这个场景你一定遇到过
|
||||
写代码写了 3 小时,突然发现之前的思路更好,但已经改不回去了...或者删错了一段代码,想找回原来的版本。
|
||||
你和队友同时改同一个文件:
|
||||
- 你改了第 10 行,添加了登录功能
|
||||
- 队友改了第 10 行,修复了一个 Bug
|
||||
- 你们用邮件互发代码,结果合并时一个人的改动被另一个人覆盖了
|
||||
- 没人知道最后哪段代码是对的
|
||||
|
||||
有了 Git,这种情况永远不会发生。每次重要节点都能"存档",出问题随时"读档"重来。
|
||||
:::
|
||||
**场景三:没有"后悔药"**
|
||||
|
||||
### 1.3 痛点三:协作冲突
|
||||
|
||||
你和队友同时改同一个文件:
|
||||
|
||||
- 你改了 A 文件的第 10 行
|
||||
- 队友改了 A 文件的第 15 行
|
||||
- 怎么合并?谁覆盖谁?
|
||||
|
||||
**Git 的解决方案**:智能合并,自动处理大部分冲突。只有当你们真的改了同一行代码时,才需要手动决定用谁的。
|
||||
你在生产环境部署了新代码,结果出 Bug 了,想紧急回退到上一个稳定版本——但你不知道怎么回退,只能手忙脚乱地找备份。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:三区模型
|
||||
**Git 就是为了解决这三个问题而生的。**
|
||||
|
||||
Git 的设计哲学其实很像**寄快递**。
|
||||
Git 是一个**版本控制系统**(Version Control System)。它的本质是:**把你每一次"存档"操作都记录下来,形成一条完整的历史时间线,让你可以随时回到任意一个历史节点。**
|
||||
|
||||
<GitThreeAreasDemo />
|
||||
|
||||
### 2.1 三个区域是什么?
|
||||
|
||||
::: tip 📦 用快递理解 Git
|
||||
想象你在寄快递:
|
||||
|
||||
- **工作区(Working Dir)** = 你的**书桌**。你在这里整理要寄的东西,想怎么乱改都行。
|
||||
- **暂存区(Staging Area)** = **快递盒**。你把要寄的文件放进去(`git add`),准备打包。
|
||||
- **仓库(Repository)** = **快递柜**。一旦你封箱寄出(`git commit`),这个版本就被永久记录下来了。
|
||||
:::
|
||||
|
||||
| 区域 | 作用 | 对应命令 | 状态 |
|
||||
| ---------- | ------------------ | --------------------- | ------------- |
|
||||
| **工作区** | 你当前正在改的代码 | `git status` 查看修改 | 红色 = 未暂存 |
|
||||
| **暂存区** | 准备提交的文件 | `git add` 添加 | 绿色 = 已暂存 |
|
||||
| **仓库** | 永久保存的历史版本 | `git commit` 提交 | 只读,不能改 |
|
||||
|
||||
::: tip 💡 关键理解
|
||||
只有提交到**仓库**的内容才是安全的。工作区里没提交的内容,丢了就真丢了。所以经常`git commit`是好习惯!
|
||||
:::
|
||||
不夸张地说,Git 是现代软件开发最重要的工具之一。几乎所有的公司、所有的开源项目都在用它。
|
||||
|
||||
---
|
||||
|
||||
## 3. 基础工作流:存档三步走
|
||||
## 1. Git 和 GitHub 是一回事吗?
|
||||
|
||||
日常开发中,你 90% 的时间都在重复这三个动作。
|
||||
很多初学者会混淆这两个概念,先澄清一下:
|
||||
|
||||
<GitWorkflowDemo />
|
||||
| | Git | GitHub |
|
||||
| :--- | :--- | :--- |
|
||||
| **是什么** | 一个运行在你电脑上的版本控制工具 | 一个存放 Git 仓库的网站(云端) |
|
||||
| **在哪里** | 你的本地电脑 | 互联网上 |
|
||||
| **能独立使用吗** | ✅ 可以,只管理本地历史 | ❌ 需要配合 Git 使用 |
|
||||
| **类比** | 你本地的日记本 | 存日记的云盘 |
|
||||
|
||||
### 3.1 第一步:修改代码(工作区)
|
||||
简单说:**Git 是工具,GitHub 是托管服务。** 就像 Word 是工具,OneDrive 是云盘一样,两者配合使用,但并不是同一个东西。
|
||||
|
||||
在工作区写写画画,想怎么改就怎么改。这时候修改只在你本地,还没记录。
|
||||
除了 GitHub,类似的服务还有 GitLab、Gitee(国内)等。
|
||||
|
||||
### 3.2 第二步:挑选文件(git add → 暂存区)
|
||||
---
|
||||
|
||||
::: tip 🤔 为什么要先 add 再 commit?
|
||||
你可能问:为什么不能直接 commit 所有修改?
|
||||
## 2. 核心概念:三个区域
|
||||
|
||||
**答案**:因为有时候你不想一次性提交所有改动。
|
||||
这是整个 Git 最重要的设计,理解了这三个区域,你就理解了 Git 的灵魂。
|
||||
|
||||
- 今天改了 5 个文件,但只想提交其中 3 个(完成了一个功能)
|
||||
- 另外 2 个文件还在调试中,不想现在提交
|
||||
Git 把你的文件状态分成三层:
|
||||
|
||||
`git add` 让你有选择权:决定这次提交包含哪些文件。
|
||||
:::
|
||||
**工作区(Working Directory)**
|
||||
就是你的**普通文件夹**,你现在看到的、正在编辑的所有文件都在这里。你随便改,Git 会感知到你改了什么,但不会做任何记录。
|
||||
|
||||
**常用命令**:
|
||||
**暂存区(Staging Area / Index)**
|
||||
这是一个**"预备提交"的中转站**。你可以把工作区里想要保存的文件"放进"暂存区,就像把快递放进快递盒——还没寄出去,但已经选好了要寄什么。
|
||||
|
||||
**仓库(Repository)**
|
||||
这是**永久存档的历史记录库**,藏在 `.git` 文件夹里。每次你执行 `git commit`,暂存区里的内容就会被封存进仓库,形成一条不可篡改的历史记录。
|
||||
|
||||
👇 **动手点点看**:依次点击命令按钮,观察文件在三个区域之间的流转。
|
||||
|
||||
<GitCommitFlow />
|
||||
|
||||
### 为什么要"两步走"(add + commit)?
|
||||
|
||||
很多初学者会问:为什么不能直接一键保存,非要先 `add` 再 `commit`?
|
||||
|
||||
**因为现实开发中,你经常不想把所有改动都一起提交。**
|
||||
|
||||
举个例子:你今天改了 5 个文件:
|
||||
- `login.js`:完成了登录功能(想提交)
|
||||
- `style.css`:调整了登录页样式(想提交)
|
||||
- `debug.log`:临时调试输出(**不想**提交)
|
||||
- `experiment.js`:正在测试的新功能,还没完成(**不想**提交)
|
||||
- `todo.txt`:你的个人备忘(**不想**提交)
|
||||
|
||||
如果没有暂存区,你要么把这 5 个文件全部提交(提交记录很混乱),要么一个都不提交。
|
||||
|
||||
有了暂存区,你可以精确控制:`git add login.js style.css`,只把这两个文件放进快递盒,然后 `commit`,这次提交就清清楚楚地记录"登录功能完成"。
|
||||
|
||||
---
|
||||
|
||||
## 3. 第一次使用 Git:初始化和基础工作流
|
||||
|
||||
### 3.1 安装和初始化
|
||||
|
||||
安装好 Git 后(macOS 自带,Windows 去 git-scm.com 下载),打开终端,进入你的项目文件夹:
|
||||
|
||||
```bash
|
||||
# 添加单个文件
|
||||
git add index.html
|
||||
# 在当前文件夹初始化一个 Git 仓库
|
||||
git init
|
||||
|
||||
# 添加所有修改
|
||||
git add .
|
||||
# Git 会创建一个隐藏的 .git 文件夹,所有历史记录存在里面
|
||||
# 输出:Initialized empty Git repository in .../your-project/.git/
|
||||
```
|
||||
|
||||
# 查看哪些文件被暂存了
|
||||
第一次使用还需要告诉 Git 你是谁(这个信息会附在每次提交记录上):
|
||||
|
||||
```bash
|
||||
git config --global user.name "你的名字"
|
||||
git config --global user.email "你的邮箱"
|
||||
```
|
||||
|
||||
### 3.2 日常工作流:三步存档
|
||||
|
||||
初始化之后,日常开发 90% 的操作就是反复执行这三步:
|
||||
|
||||
**第一步:查看状态**
|
||||
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
### 3.3 第三步:封箱提交(git commit → 仓库)
|
||||
这是你用得最多的命令,没有之一。它告诉你:
|
||||
- 你在哪个分支上
|
||||
- 哪些文件被修改了(红色 = 未暂存)
|
||||
- 哪些文件在暂存区里(绿色 = 已暂存,等待提交)
|
||||
|
||||
给这次修改起个名字(比如"修复了登录 Bug"),永久存档。
|
||||
|
||||
**重要:commit message 要写清楚!**
|
||||
**第二步:把文件放进暂存区**
|
||||
|
||||
```bash
|
||||
# ❌ 不好的写法
|
||||
git commit -m "update"
|
||||
# 添加单个文件
|
||||
git add login.js
|
||||
|
||||
# ✅ 好的写法
|
||||
git commit -m "feat: 添加用户登录功能"
|
||||
git commit -m "fix: 修复首页在 iOS 的显示问题"
|
||||
git commit -m "docs: 更新 README 的部署说明"
|
||||
# 添加多个文件
|
||||
git add login.js style.css
|
||||
|
||||
# 添加当前文件夹里所有修改过的文件(用 . 表示"全部")
|
||||
git add .
|
||||
```
|
||||
|
||||
::: tip 💡 commit message 规范
|
||||
推荐用**类型+描述**的格式:
|
||||
> ⚠️ 初学者常见误区:`git add .` 非常方便,但会把所有修改都加进去,包括你不想提交的临时文件。养成精确 add 的习惯,或者用 `.gitignore` 排除不想追踪的文件(后面会讲)。
|
||||
|
||||
- `feat:` 新功能
|
||||
- `fix:` 修复 bug
|
||||
- `docs:` 文档更新
|
||||
- `style:` 代码格式(不影响功能)
|
||||
- `refactor:` 重构(不改变功能)
|
||||
- `test:` 测试相关
|
||||
- `chore:` 构建/工具相关
|
||||
**第三步:提交,写上说明**
|
||||
|
||||
这样以后翻历史记录,一眼就知道每次提交做了什么。
|
||||
:::
|
||||
```bash
|
||||
git commit -m "feat: 添加用户登录功能"
|
||||
```
|
||||
|
||||
`-m` 后面引号里的内容叫做 **commit message**(提交说明)。这是写给未来的自己和队友看的,要写得有意义。
|
||||
|
||||
### 3.3 Commit Message 怎么写才专业?
|
||||
|
||||
```bash
|
||||
# ❌ 没用的写法——看了不知道做了什么
|
||||
git commit -m "update"
|
||||
git commit -m "fix"
|
||||
git commit -m "改了一些东西"
|
||||
|
||||
# ✅ 好的写法:类型 + 冒号 + 一句话描述
|
||||
git commit -m "feat: 添加用户登录功能"
|
||||
git commit -m "fix: 修复首页在 iOS Safari 上的白屏问题"
|
||||
git commit -m "docs: 更新 README 中的部署说明"
|
||||
git commit -m "refactor: 将 UserService 拆分为独立模块"
|
||||
git commit -m "style: 统一代码缩进为 2 空格"
|
||||
```
|
||||
|
||||
**常用前缀含义:**
|
||||
|
||||
| 前缀 | 含义 |
|
||||
| :--- | :--- |
|
||||
| `feat:` | 新功能(feature) |
|
||||
| `fix:` | 修复 Bug |
|
||||
| `docs:` | 文档改动 |
|
||||
| `style:` | 代码格式调整(不影响功能) |
|
||||
| `refactor:` | 代码重构(功能不变,结构优化) |
|
||||
| `chore:` | 构建、工具、依赖相关 |
|
||||
| `test:` | 测试相关 |
|
||||
|
||||
养成这个习惯,几个月后翻历史记录,一眼就知道每次提交做了什么。这在团队协作中尤其重要。
|
||||
|
||||
### 3.4 查看历史记录
|
||||
|
||||
```bash
|
||||
# 详细格式(每次提交的完整信息)
|
||||
git log
|
||||
|
||||
# 简洁格式(每行一条,推荐日常使用)
|
||||
git log --oneline
|
||||
|
||||
# 示例输出:
|
||||
# a1b2c3d (HEAD -> main) feat: 添加用户登录功能
|
||||
# 9f3e1b2 init: 项目初始化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 平行宇宙:分支(Branch)的魔法
|
||||
## 4. 平行宇宙:分支(Branch)
|
||||
|
||||
这是 Git 最强大的功能!
|
||||
**分支**是 Git 最强大、也是最让初学者困惑的功能。但理解了它之后,你会发现这个设计非常优雅。
|
||||
|
||||
::: tip 🌌 用游戏理解分支
|
||||
想象你在玩游戏,前面有个大 Boss(上线新功能),你怕打不过导致游戏结束(系统崩溃)。
|
||||
### 4.1 分支是什么?用"平行宇宙"来理解
|
||||
|
||||
这时候,你可以开一个**分支(Branch)**,相当于**复制了一个平行世界**:
|
||||
想象你在玩一个角色扮演游戏,游戏里有一个关键选择:
|
||||
- 选择 A:去挑战大 Boss(开发新功能)
|
||||
- 选择 B:继续稳定当前局面(主线不动)
|
||||
|
||||
- 在**平行世界**(新分支)里打 Boss,输了也不怕,因为主世界(主分支)没影响
|
||||
- 打赢了就把成果"合并"回主世界
|
||||
- 多个队友可以在各自的平行世界开发,互不干扰
|
||||
:::
|
||||
如果你直接在主存档上做选择 A,万一失败了,整个游戏进度就毁了。
|
||||
|
||||
<GitBranchMergeDemo />
|
||||
但如果你**复制一个存档**,在副本里去挑战 Boss:
|
||||
- 打赢了?把副本的成果合并回主存档
|
||||
- 打输了?主存档完全没有影响,删掉副本重来
|
||||
|
||||
### 4.1 主分支 vs 开发分支
|
||||
**Git 分支就是这个"副本存档"机制。**
|
||||
|
||||
| 分支类型 | 作用 | 特点 |
|
||||
| ------------------- | -------------- | ------------------------------------ |
|
||||
| **main/master** | 稳定的线上版本 | 只有测试通过的代码才能进来 |
|
||||
| **dev/feature-xxx** | 你的试验田 | 这里炸了地球也没关系,不影响主分支 |
|
||||
| **hotfix-xxx** | 紧急修复 | 生产出 bug 时,从 main 开分支快速修复 |
|
||||
在 Git 里,`main`(或 `master`)分支是你的"主存档",永远保持稳定可用。当你要开发新功能时,你从 main 创建一个新分支,在那里开发、测试,完成后再合并回 main。
|
||||
|
||||
### 4.2 分支操作流程
|
||||
### 4.2 分支的可视化演示
|
||||
|
||||
**创建分支并切换**:
|
||||
👇 **动手点点看**:依次点击命令按钮,观察下方分支图如何分叉、延伸、最终合并。重点关注 HEAD 标签的位置变化——它始终指向"你当前在哪里"。
|
||||
|
||||
<GitBranchVisual />
|
||||
|
||||
### 4.3 分支操作详解
|
||||
|
||||
**创建并切换到新分支:**
|
||||
|
||||
```bash
|
||||
# 创建新分支
|
||||
git branch feature-login
|
||||
# 方式一:先创建,再切换(两步)
|
||||
git branch feature-login # 创建分支
|
||||
git checkout feature-login # 切换过去
|
||||
|
||||
# 切换到新分支
|
||||
git checkout feature-login
|
||||
|
||||
# 或者一步到位:创建并切换
|
||||
# 方式二:一步到位(推荐)
|
||||
git checkout -b feature-login
|
||||
|
||||
# 输出:Switched to a new branch 'feature-login'
|
||||
```
|
||||
|
||||
**在分支上开发**:
|
||||
|
||||
```bash
|
||||
# 在 feature-login 分支上改代码...
|
||||
git add .
|
||||
git commit -m "feat: 添加登录表单"
|
||||
创建分支后,你的命令行提示符会显示当前分支名,比如:
|
||||
```
|
||||
user@mac ~/project (feature-login) $
|
||||
```
|
||||
|
||||
**合并回主分支**:
|
||||
**查看所有分支:**
|
||||
|
||||
```bash
|
||||
# 切回主分支
|
||||
git branch
|
||||
|
||||
# 输出(* 表示当前所在分支):
|
||||
# * feature-login
|
||||
# main
|
||||
```
|
||||
|
||||
**在分支上正常开发:**
|
||||
|
||||
```bash
|
||||
# 在 feature-login 分支上,改代码、add、commit,和平时完全一样
|
||||
git add login.js
|
||||
git commit -m "feat: 添加登录表单 HTML 结构"
|
||||
|
||||
git add login.js api.js
|
||||
git commit -m "feat: 完成登录接口对接"
|
||||
```
|
||||
|
||||
这些提交只在 `feature-login` 分支上,`main` 分支完全不知道你做了什么。
|
||||
|
||||
**切回主分支,合并:**
|
||||
|
||||
```bash
|
||||
# 切回 main
|
||||
git checkout main
|
||||
|
||||
# 合并 feature-login
|
||||
# 把 feature-login 的所有改动合并进来
|
||||
git merge feature-login
|
||||
|
||||
# 删除已合并的分支(可选)
|
||||
# 合并完成后,可以删掉这个分支(可选)
|
||||
git branch -d feature-login
|
||||
```
|
||||
|
||||
::: tip 💡 什么时候用分支?
|
||||
**个人开发**:
|
||||
### 4.4 什么时候该开分支?
|
||||
|
||||
- 要尝试新想法,不确定会不会搞崩现有代码 → 开分支
|
||||
- 修一个复杂 bug,需要多次实验 → 开分支
|
||||
| 场景 | 建议 | 理由 |
|
||||
| :--- | :--- | :--- |
|
||||
| 开发一个新功能 | ✅ 开分支 | 功能完成前不影响主线,随时可以放弃 |
|
||||
| 修复线上紧急 Bug | ✅ 从 main 开 `hotfix-xxx` 分支 | 修复完直接合并上线,不带入未完成的功能 |
|
||||
| 和队友并行开发 | ✅ 各自开分支 | 互不干扰,完成后统一通过 Pull Request 合并 |
|
||||
| 只改一个错别字 | ❌ 直接在 main 改 | 风险极低,没必要额外开分支 |
|
||||
|
||||
**团队开发**:
|
||||
### 4.5 团队常用的分支策略
|
||||
|
||||
- 每个功能一个分支,互不干扰
|
||||
- 开发完提 Pull Request,队友 review 后再合并
|
||||
:::
|
||||
在实际项目中,团队通常会约定好分支的命名和用途:
|
||||
|
||||
| 分支名 | 用途 | 特点 |
|
||||
| :--- | :--- | :--- |
|
||||
| `main` / `master` | 生产环境的稳定代码 | 只有测试通过的代码才能进来,不能直接推送 |
|
||||
| `dev` / `develop` | 日常集成分支 | 所有功能分支先合并到这里,测试通过再上 main |
|
||||
| `feature/xxx` | 具体功能开发 | 如 `feature/user-login`,完成后合并到 dev |
|
||||
| `hotfix/xxx` | 紧急修复 | 从 main 创建,修完直接合并回 main 和 dev |
|
||||
|
||||
---
|
||||
|
||||
## 5. 常用命令速查表
|
||||
## 5. 与队友协作:远程仓库
|
||||
|
||||
| 命令 | 作用 | 人话解释 | 使用频率 |
|
||||
| ----------------------- | ---------- | ------------------------------ | --------------------- |
|
||||
| `git init` | 初始化 | "在这里建个新仓库" | 项目开始时用一次 |
|
||||
| `git status` | 查看状态 | "现在乱不乱?有没有东西没提交?" | ⭐⭐⭐⭐⭐ 极高频 |
|
||||
| `git add .` | 添加所有 | "把桌上所有文件都扔进快递盒" | ⭐⭐⭐⭐⭐ 每次提交前 |
|
||||
| `git add file.txt` | 添加单个 | "只要这个文件" | ⭐⭐⭐⭐ 选择性添加 |
|
||||
| `git commit -m "..."` | 提交 | "封箱!贴上标签,写上这次改了啥" | ⭐⭐⭐⭐⭐ 完成功能时 |
|
||||
| `git log` | 查看历史 | "翻翻以前的日记" | ⭐⭐⭐ 回顾历史 |
|
||||
| `git checkout -b dev` | 创建新分支 | "我要去平行宇宙 dev 探险了" | ⭐⭐⭐⭐ 开新功能 |
|
||||
| `git checkout main` | 切换分支 | "回地球(主分支)看看" | ⭐⭐⭐⭐ 切换任务 |
|
||||
| `git merge dev` | 合并分支 | "把平行宇宙的成果带回地球" | ⭐⭐⭐ 完成功能 |
|
||||
| `git branch` | 查看分支 | "现在有哪些平行世界?" | ⭐⭐⭐ 查看状态 |
|
||||
| `git branch -d feature` | 删除分支 | "这个平行世界不需要了,删掉" | ⭐⭐ 合并后清理 |
|
||||
| `git push` | 推送 | "把本地存档上传到云端" | ⭐⭐⭐⭐⭐ 团队协作 |
|
||||
| `git pull` | 拉取 | "把云端最新存档下载到本地" | ⭐⭐⭐⭐⭐ 团队协作 |
|
||||
到目前为止,你学的都是**本地**的 Git 操作——所有历史记录都存在你自己的电脑上。要和队友共享代码,你需要一个**远程仓库**,也就是 GitHub、GitLab 这样的云端存储。
|
||||
|
||||
---
|
||||
### 5.1 远程仓库的工作原理
|
||||
|
||||
## 6. 进阶:解决冲突与远程协作
|
||||
可以把远程仓库理解为**团队共用的"公共存档"**:
|
||||
|
||||
### 6.1 冲突(Conflict)是什么?
|
||||
- 每个人在本地写代码、commit
|
||||
- 写完后 `push`(上传)到远程仓库
|
||||
- 队友 `pull`(下载)远程仓库的最新内容到自己本地
|
||||
- 这样大家的代码就保持同步了
|
||||
|
||||
当你和队友**同时修改了同一个文件的同一行代码**,Git 就会懵:"我该听谁的?"这就是**冲突(Conflict)**。
|
||||
👇 **动手点点看**:依次点击命令,体验从关联远程仓库、推送、到拉取队友更新的完整流程。
|
||||
|
||||
<GitConflictDemo />
|
||||
<GitSyncDemo />
|
||||
|
||||
### 6.2 怎么解决冲突?
|
||||
### 5.2 第一次推送项目到 GitHub
|
||||
|
||||
**Step 1**:打开冲突文件,会看到这样的标记:
|
||||
**第一步**:在 GitHub 上创建一个新仓库(点击右上角 + → New repository),不要勾选初始化选项。
|
||||
|
||||
```text
|
||||
<<<<<<< HEAD
|
||||
你的代码
|
||||
=======
|
||||
队友的代码
|
||||
>>>>>>> feature-branch
|
||||
```
|
||||
|
||||
**Step 2**:手动选择要保留的代码,或合并两者:
|
||||
|
||||
```text
|
||||
# 保留你的代码 → 删除队友的部分和标记
|
||||
# 保留队友的 → 删除你的部分和标记
|
||||
# 合并两者 → 综合两边的代码
|
||||
```
|
||||
|
||||
**Step 3**:删除所有标记,保存文件
|
||||
|
||||
**Step 4**:重新提交
|
||||
**第二步**:回到本地终端,关联远程仓库:
|
||||
|
||||
```bash
|
||||
git add 解决冲突的文件
|
||||
git commit # Git 会自动生成合并提交
|
||||
# 把本地仓库和 GitHub 上的仓库关联起来
|
||||
# "origin" 是远程仓库的别名,是约定俗成的名字(也可以改,但没必要)
|
||||
git remote add origin https://github.com/你的用户名/仓库名.git
|
||||
|
||||
# 确认关联成功
|
||||
git remote -v
|
||||
# 输出:
|
||||
# origin https://github.com/你的用户名/仓库名.git (fetch)
|
||||
# origin https://github.com/你的用户名/仓库名.git (push)
|
||||
```
|
||||
|
||||
::: tip 💡 避免冲突的最佳实践
|
||||
**第三步**:推送本地内容到远程:
|
||||
|
||||
- **频繁沟通**:队友改同一个文件前,先打个招呼
|
||||
- **小步提交**:不要攒着大量代码最后才提交,增加冲突概率
|
||||
- **分支隔离**:不同功能用不同分支,减少直接冲突
|
||||
- **用 Pull Request**:合并前让队友 review,提前发现问题
|
||||
:::
|
||||
```bash
|
||||
# 第一次推送,-u 的意思是"以后 git push 时,默认推到 origin 的 main 分支"
|
||||
git push -u origin main
|
||||
|
||||
### 6.3 远程仓库(Remote)
|
||||
# 之后每次推送只需要:
|
||||
git push
|
||||
```
|
||||
|
||||
**远程仓库**(比如 GitHub/GitLab)就是**云端的备份中心**。
|
||||
### 5.3 日常协作的命令
|
||||
|
||||
<GitRemoteDemo />
|
||||
**推送(你改了东西,要让队友看到):**
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
**核心操作**:
|
||||
**拉取(队友改了东西,你要同步):**
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
|
||||
| 操作 | 命令 | 作用 |
|
||||
| ------------ | ---------------------------------------------- | ------------------------ |
|
||||
| **关联远程** | `git remote add origin https://github.com/...` | 第一次连接云端 |
|
||||
| **推送** | `git push -u origin main` | 把本地存档上传 |
|
||||
| **拉取** | `git pull` | 把云端最新存档下载并合并 |
|
||||
| **克隆** | `git clone https://github.com/...` | 复制整个仓库到本地 |
|
||||
`git pull` 实际上是两个命令的组合:
|
||||
1. `git fetch`:先去远程仓库下载最新的提交记录
|
||||
2. `git merge`:把下载回来的内容合并到你当前的分支
|
||||
|
||||
::: tip 💡 push 和 pull 的区别
|
||||
**第一次从 GitHub 获取别人的项目:**
|
||||
```bash
|
||||
# 把整个远程仓库复制到本地(只需要做一次)
|
||||
git clone https://github.com/某人/某项目.git
|
||||
|
||||
- **push**:你的本地代码 → 云端(你改了东西,要同步给队友)
|
||||
- **pull**:云端代码 → 你的本地(队友改了东西,你要同步下来)
|
||||
# clone 会自动建立与远程的关联,之后直接 push/pull 就行
|
||||
```
|
||||
|
||||
**最佳实践**:每天开始工作前先`git pull`,下班前`git push`,这样减少冲突。
|
||||
:::
|
||||
### 5.4 push 和 pull 的方向
|
||||
|
||||
```
|
||||
你的电脑(本地仓库) ←→ GitHub(远程仓库)
|
||||
|
||||
git push: 本地 → 远程 (你改了东西,上传给队友)
|
||||
git pull: 远程 → 本地 (队友改了东西,下载到你这里)
|
||||
git clone: 远程 → 本地 (第一次完整复制整个仓库)
|
||||
```
|
||||
|
||||
> **最佳实践**:每天开始工作前先 `git pull`,拿到最新代码;下班或完成一个功能后 `git push`,及时备份并让队友看到你的进展。
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结:Git 的核心思想
|
||||
## 6. 进阶:处理冲突
|
||||
|
||||
Git 不是简单的"版本备份",而是一个**完整的代码协作系统**:
|
||||
冲突是协作中不可避免的,但也没那么可怕。
|
||||
|
||||
| 特性 | 解决的问题 | 生活类比 |
|
||||
| ------------ | ------------------------------- | --------------------- |
|
||||
| **版本管理** | 代码改错了怎么办? | 时光机,随时回退 |
|
||||
| **分支** | 多人同时改同一个文件怎么办? | 平行宇宙,互不干扰 |
|
||||
| **暂存区** | 这次提交不想包含所有修改怎么办? | 快递盒,挑拣要寄的东西 |
|
||||
| **远程协作** | 怎么和队友共享代码? | 云备份,随时随地同步 |
|
||||
| **冲突处理** | 真的改到同一行了怎么办? | 智能合并 + 手动协调 |
|
||||
### 6.1 冲突是怎么发生的?
|
||||
|
||||
**学习建议**:
|
||||
当你和队友**同时修改了同一个文件的同一行**,在合并时 Git 不知道该用谁的版本,就会产生冲突。
|
||||
|
||||
1. **先用起来**:不要等"完全理解"再用,一边用一边理解
|
||||
2. **从简单开始**:个人项目先掌握`add/commit/push/pull`,团队项目再学分支
|
||||
3. **看 Git 图形化工具**:SourceTree、GitHub Desktop 等,可视化帮助理解
|
||||
4. **遇到问题不要慌**:Git 的设计就是为了让你能安全地尝试,大不了`git reset`
|
||||
举个例子:
|
||||
- 你在 `login.js` 第 5 行写了:`const timeout = 3000`
|
||||
- 队友同时在同一行写了:`const timeout = 5000`
|
||||
- 当你 `git pull` 或 `git merge` 时,Git 发现了这个矛盾,就会"暂停"并告诉你:我不知道该用哪个,你来决定。
|
||||
|
||||
### 6.2 冲突文件长什么样?
|
||||
|
||||
Git 会在冲突的地方插入特殊标记:
|
||||
|
||||
```javascript
|
||||
function login() {
|
||||
const url = '/api/login'
|
||||
|
||||
<<<<<<< HEAD
|
||||
const timeout = 3000 // 你的版本
|
||||
=======
|
||||
const timeout = 5000 // 队友的版本
|
||||
>>>>>>> feature/update-timeout
|
||||
|
||||
return fetch(url, { timeout })
|
||||
}
|
||||
```
|
||||
|
||||
- `<<<<<<< HEAD` 到 `=======` 之间:是你当前分支的内容
|
||||
- `=======` 到 `>>>>>>> xxx` 之间:是合并过来的内容
|
||||
|
||||
### 6.3 如何解决冲突?
|
||||
|
||||
**第一步**:打开冲突文件,找到所有 `<<<<<<<` 标记(通常 VS Code 等编辑器会自动高亮)
|
||||
|
||||
**第二步**:决定保留哪段代码,然后手动编辑文件,删掉所有标记符号(`<<<<<<<`、`=======`、`>>>>>>>`)。
|
||||
|
||||
比如决定用 5000(队友的版本):
|
||||
```javascript
|
||||
function login() {
|
||||
const url = '/api/login'
|
||||
const timeout = 5000 // 采用队友的修改
|
||||
return fetch(url, { timeout })
|
||||
}
|
||||
```
|
||||
|
||||
**第三步**:重新提交
|
||||
|
||||
```bash
|
||||
# 标记冲突已解决
|
||||
git add login.js
|
||||
|
||||
# 完成合并提交(Git 会自动生成合并提交信息)
|
||||
git commit
|
||||
```
|
||||
|
||||
### 6.4 减少冲突的好习惯
|
||||
|
||||
- **勤 pull**:开始工作前同步最新代码,减少"你落后太多"的情况
|
||||
- **小步提交**:不要写了一周代码才一次性提交,频繁小提交更容易发现和解决冲突
|
||||
- **分支隔离**:不同功能用不同分支,减少对同一行代码的竞争
|
||||
- **沟通**:要改公共文件(比如 `config.js`)前,跟队友打个招呼
|
||||
|
||||
---
|
||||
|
||||
## 附录:名词速查表
|
||||
## 7. 常用命令速查
|
||||
|
||||
| 名词 | 英文 | 用人话解释 |
|
||||
| -------- | ---------- | ------------------------------------- |
|
||||
| **仓库** | Repository | 存放所有版本历史的数据库 |
|
||||
| **提交** | Commit | 一次完整的版本记录,像存档点 |
|
||||
| **分支** | Branch | 独立的开发线,像平行宇宙 |
|
||||
| **合并** | Merge | 把一个分支的改动整合到另一个分支 |
|
||||
| **冲突** | Conflict | 同一行代码被修改多次,Git 不知道选哪个 |
|
||||
| **暂存** | Stage | 把修改加入"准备提交"的列表 |
|
||||
| **远程** | Remote | 云端的仓库副本(GitHub/GitLab) |
|
||||
| **克隆** | Clone | 复制整个远程仓库到本地 |
|
||||
| **推送** | Push | 本地 → 远程,上传代码 |
|
||||
| **拉取** | Pull | 远程 → 本地,下载代码 |
|
||||
| **检出** | Checkout | 切换到某个分支或版本 |
|
||||
| **HEAD** | - | 当前所在的分支/版本的指针 |
|
||||
<GitCommandCheatsheet />
|
||||
|
||||
---
|
||||
|
||||
## 8. 实战:加入一个团队项目的完整流程
|
||||
|
||||
这是你加入新团队或新项目时的标准操作流程,可以直接照抄:
|
||||
|
||||
```bash
|
||||
# ① 第一天:把项目 clone 到本地(只做一次)
|
||||
git clone https://github.com/team/project.git
|
||||
cd project
|
||||
|
||||
# ② 每天开始工作:先拉取最新代码,确保你的代码是最新的
|
||||
git pull origin main
|
||||
|
||||
# ③ 创建自己的功能分支(不要直接在 main 上改)
|
||||
git checkout -b feature/user-profile
|
||||
|
||||
# ④ 正常开发...写代码...
|
||||
|
||||
# ⑤ 完成一个小功能点后,立即提交(不要攒着)
|
||||
git add src/UserProfile.vue
|
||||
git commit -m "feat: 完成用户头像上传功能"
|
||||
|
||||
git add src/UserProfile.vue src/api/user.js
|
||||
git commit -m "feat: 完成用户资料编辑接口"
|
||||
|
||||
# ⑥ 把自己的分支推送到远程,让队友能看到
|
||||
git push origin feature/user-profile
|
||||
|
||||
# ⑦ 在 GitHub 上创建 Pull Request(PR),请求合并到 main
|
||||
# (这步在 GitHub 网页上操作)
|
||||
|
||||
# ⑧ 等队友 Code Review,按反馈修改,继续 commit + push
|
||||
|
||||
# ⑨ PR 合并后,回到 main,更新本地,删掉功能分支
|
||||
git checkout main
|
||||
git pull
|
||||
git branch -d feature/user-profile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. .gitignore:哪些文件不应该被追踪?
|
||||
|
||||
有些文件你**不想**提交到 Git 仓库里,比如:
|
||||
- `node_modules/`:依赖包,体积巨大,可以用 `npm install` 重新生成
|
||||
- `.env`:环境变量文件,里面可能有数据库密码、API Key,**绝对不能上传到公开仓库**
|
||||
- `*.log`:日志文件
|
||||
- `.DS_Store`:macOS 自动生成的隐藏文件
|
||||
- `dist/`、`build/`:编译产物,可以重新构建
|
||||
|
||||
在项目根目录创建一个 `.gitignore` 文件,写上不想追踪的文件规则:
|
||||
|
||||
```gitignore
|
||||
# 依赖包
|
||||
node_modules/
|
||||
|
||||
# 环境变量(重要!密码不能提交)
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# 构建产物
|
||||
dist/
|
||||
build/
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
```
|
||||
|
||||
GitHub 上有各种语言和框架的 .gitignore 模板:[github.com/github/gitignore](https://github.com/github/gitignore)
|
||||
|
||||
---
|
||||
|
||||
## 名词速查表
|
||||
|
||||
| 名词 | 英文 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **仓库** | Repository (Repo) | 存放项目所有版本历史的数据库,在 `.git` 文件夹里 |
|
||||
| **提交** | Commit | 一次完整的版本记录,像游戏存档点,附有说明和时间戳 |
|
||||
| **分支** | Branch | 独立的开发线,像平行时间线,互不影响 |
|
||||
| **合并** | Merge | 把一个分支的改动整合到另一个分支 |
|
||||
| **冲突** | Conflict | 同一行代码被多人修改,Git 不知道该用哪个,需要手动解决 |
|
||||
| **暂存** | Stage / Index | 把修改放入"准备提交"列表的操作 |
|
||||
| **远程** | Remote | 云端的仓库副本(GitHub / GitLab / Gitee) |
|
||||
| **克隆** | Clone | 把整个远程仓库完整复制到本地 |
|
||||
| **推送** | Push | 把本地提交上传到远程仓库 |
|
||||
| **拉取** | Pull | 把远程最新内容下载并合并到本地 |
|
||||
| **HEAD** | HEAD | 当前所在分支/提交的指针,表示"你现在在哪里" |
|
||||
| **origin** | origin | 远程仓库的默认别名(约定俗成的名字) |
|
||||
| **stash** | Stash | 临时保存还没 commit 的改动,切换任务时用 |
|
||||
| **PR / MR** | Pull Request / Merge Request | 请求把你的分支合并进主分支,通常需要队友 review |
|
||||
|
||||
@@ -1,3 +1,392 @@
|
||||
# 包管理器(npm / pip / cargo)
|
||||
# 包管理器
|
||||
|
||||
> 待实现
|
||||
> 💡 **学习指南**:写代码不必从零造轮子——99% 的功能已经有人写好并发布到互联网上了。**包管理器**就是那个帮你找到、下载并管理这些"现成零件"的工具。本章围绕一个核心问题展开:**如何让代码依赖变得可重现、可协作、可维护?**
|
||||
|
||||
---
|
||||
|
||||
## 0. 为什么你一定会用到包管理器?
|
||||
|
||||
想象你要写一个能发 HTTP 请求的 Node.js 程序。有两条路:
|
||||
|
||||
- **方法 A(手动)**:自己实现 TCP 连接、HTTP 协议解析、重定向处理、超时机制……估计要写几千行代码,调试几个月。
|
||||
- **方法 B(包管理器)**:`npm install axios`,十秒钟,一行代码搞定。
|
||||
|
||||
包管理器本质上是**代码的「应用商店」**。它帮你:
|
||||
|
||||
1. 在中央仓库(Registry)里找到别人发布的库
|
||||
2. 自动下载并安装到你的项目里
|
||||
3. 处理这个库自己依赖的其他库(依赖的依赖)
|
||||
4. 记录你用的是哪个精确版本,让团队协作不出问题
|
||||
|
||||
---
|
||||
|
||||
## 1. 各语言 / 系统生态的包管理器一览
|
||||
|
||||
不同编程语言和操作系统有各自的生态工具链,但底层逻辑完全一致。
|
||||
|
||||
👇 **动手点点看**:选择你熟悉的生态,探索它的主流包管理工具。
|
||||
|
||||
<PackageManagerOverviewDemo />
|
||||
|
||||
### 1.1 包去哪里下载?—— Registry(注册表)
|
||||
|
||||
每个生态背后都有一个中央仓库,存放所有可下载的包:
|
||||
|
||||
| 生态 | 注册表 | 包数量 |
|
||||
| :--- | :--- | :--- |
|
||||
| JavaScript | [npmjs.com](https://npmjs.com) | 200 万+ |
|
||||
| Python | [pypi.org](https://pypi.org) | 50 万+ |
|
||||
| Rust | [crates.io](https://crates.io) | 15 万+ |
|
||||
| Go | [pkg.go.dev](https://pkg.go.dev) | 50 万+ |
|
||||
| macOS/Linux 工具 | [formulae.brew.sh](https://formulae.brew.sh) | 7000+ |
|
||||
| Windows 软件 | [winget.run](https://winget.run) / [chocolatey.org](https://chocolatey.org) | 数万款 |
|
||||
|
||||
### 1.2 JavaScript 三强对比:npm vs yarn vs pnpm
|
||||
|
||||
功能相近,区别主要体现在**速度和磁盘占用**:
|
||||
|
||||
```text
|
||||
磁盘占用:pnpm(硬链接共享)< yarn PnP(零 node_modules)< npm(完整复制)
|
||||
安装速度:pnpm ≈ yarn > npm
|
||||
使用习惯:npm(最通用)> pnpm(新项目推荐)> yarn(部分团队)
|
||||
```
|
||||
|
||||
**推荐**:新项目用 `pnpm`,已有项目维持原有工具,不要随意切换。
|
||||
|
||||
### 1.3 Windows 三强对比:winget vs Chocolatey vs Scoop
|
||||
|
||||
| | winget | Chocolatey | Scoop |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **官方背书** | Microsoft 官方 | 第三方 | 第三方 |
|
||||
| **需要管理员** | 部分需要 | 是 | **不需要** |
|
||||
| **适合场景** | 日常软件安装 | 企业批量部署 | 开发工具管理 |
|
||||
| **包数量** | 多且增长快 | 最多(10000+)| 聚焦开发工具 |
|
||||
|
||||
**推荐**:日常用 `winget`,开发工具用 `scoop`,企业自动化用 `Chocolatey`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 安装包 —— 背后发生了什么?
|
||||
|
||||
输入 `npm install axios` 后,命令行安静了几秒,然后就好了。这几秒里到底发生了什么?
|
||||
|
||||
👇 **动手点点看**:选择一个包,点击"运行",观察安装的全过程。
|
||||
|
||||
<PackageInstallDemo />
|
||||
|
||||
### 2.1 四个阶段详解
|
||||
|
||||
**① 依赖解析(Resolve)**
|
||||
|
||||
包管理器先"读懂"你要装什么。以 `axios` 为例,它自己依赖 `follow-redirects`、`form-data` 等包,这些也都要安装。这个过程叫做**构建依赖树**。
|
||||
|
||||
**② 下载(Fetch)**
|
||||
|
||||
从 Registry 下载所有需要的包(`.tgz` 格式的压缩包)。聪明的包管理器会:
|
||||
- 并行下载多个包,而不是一个个等待
|
||||
- 先查本地缓存,命中就不走网络
|
||||
|
||||
**③ 链接(Link)**
|
||||
|
||||
把下载的包解压放到 `node_modules/` 目录,并处理好引用关系。
|
||||
|
||||
**④ 写锁文件(Lockfile)**
|
||||
|
||||
把这次安装的**精确版本号**写入 `package-lock.json`(或 `yarn.lock` / `pnpm-lock.yaml`)。
|
||||
|
||||
### 2.2 最常用命令速查
|
||||
|
||||
```bash
|
||||
# ── JavaScript (npm) ──────────────────────────────────
|
||||
npm install # 按 package.json 安装所有依赖
|
||||
npm install axios # 安装新包(生产依赖)
|
||||
npm install -D jest # 安装开发依赖(只在开发时用)
|
||||
npm install -g tsx # 全局安装(任何目录都能用)
|
||||
npm uninstall axios # 卸载包
|
||||
npm update # 升级所有包到兼容的最新版
|
||||
npm run build # 运行 package.json scripts 里的脚本
|
||||
npx create-react-app . # 临时运行,不安装到项目
|
||||
|
||||
# ── Python (pip) ──────────────────────────────────────
|
||||
pip install requests # 安装包
|
||||
pip install requests==2.28.0 # 安装指定版本
|
||||
pip freeze > requirements.txt # 导出当前依赖列表
|
||||
pip install -r requirements.txt # 按列表安装
|
||||
|
||||
# ── Rust (cargo) ──────────────────────────────────────
|
||||
cargo add serde # 添加依赖(会自动更新 Cargo.toml)
|
||||
cargo build # 构建项目
|
||||
cargo test # 运行测试
|
||||
cargo run # 运行项目
|
||||
|
||||
# ── Go (go mod) ───────────────────────────────────────
|
||||
go get github.com/gin-gonic/gin # 添加依赖
|
||||
go mod tidy # 整理依赖(删多余、补缺失)
|
||||
go build ./... # 构建
|
||||
|
||||
# ── Windows (winget) ──────────────────────────────────
|
||||
winget install Git.Git # 安装软件
|
||||
winget upgrade --all # 更新所有已安装软件
|
||||
```
|
||||
|
||||
### 2.3 npm scripts 是什么?
|
||||
|
||||
`package.json` 里有一个 `scripts` 字段,这是 npm 内置的**任务运行器**:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行方式:`npm run dev`、`npm run build`。这样做的好处是:
|
||||
- **统一入口**:团队成员不需要记住底层工具的具体命令
|
||||
- **环境自动配置**:运行时会自动把 `node_modules/.bin` 加入 PATH,可以直接用本地安装的工具
|
||||
|
||||
---
|
||||
|
||||
## 3. 全局安装 vs 本地安装
|
||||
|
||||
这是新手最容易困惑的概念之一。
|
||||
|
||||
### 3.1 两者的区别
|
||||
|
||||
```bash
|
||||
npm install axios # 本地安装:装到 ./node_modules/,只有当前项目能用
|
||||
npm install -g typescript # 全局安装:装到系统目录,任何项目/目录都能用
|
||||
```
|
||||
|
||||
| | 本地安装 | 全局安装 |
|
||||
| :--- | :--- | :--- |
|
||||
| **存放位置** | `./node_modules/` | 系统级目录(如 `/usr/local/lib/`) |
|
||||
| **适合** | 项目依赖的库(axios、vue、react) | 命令行工具(tsc、eslint、create-react-app) |
|
||||
| **版本隔离** | 每个项目独立版本 ✅ | 全机共用一个版本 ⚠️ |
|
||||
| **团队一致性** | 锁文件保证一致 ✅ | 各人版本可能不同 ⚠️ |
|
||||
|
||||
### 3.2 黄金法则
|
||||
|
||||
> **库类依赖(axios、lodash、vue)永远本地安装;
|
||||
> 命令行工具(tsc、eslint)优先本地安装,用 `npx` 调用。**
|
||||
|
||||
**为什么命令行工具也推荐本地安装?**
|
||||
|
||||
假设你全局安装了 `eslint@8`,但项目 A 需要 `eslint@9` 的新规则,你就要在全局和项目之间反复切换。把 `eslint` 装到本地,用 `npx eslint .` 调用,每个项目都能独立配置自己的版本。
|
||||
|
||||
### 3.3 npx —— 临时运行,不污染环境
|
||||
|
||||
`npx` 是 npm 自带的工具运行器,允许你**不安装直接运行**一个包:
|
||||
|
||||
```bash
|
||||
# 不安装 create-vue,直接运行它来初始化项目
|
||||
npx create-vue my-project
|
||||
|
||||
# 不安装 prettier,直接格式化文件
|
||||
npx prettier --write src/
|
||||
|
||||
# 强制使用指定版本(忽略已安装的)
|
||||
npx typescript@5.4 tsc --version
|
||||
```
|
||||
|
||||
Python 的 `uvx`、Rust 的 `cargo run` 也提供了类似的"临时运行"能力:
|
||||
|
||||
```bash
|
||||
uvx ruff check . # Python:临时运行 ruff 检查器
|
||||
cargo install ripgrep # Rust:安装到全局,变成系统命令 rg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 版本号的秘密 —— 语义化版本
|
||||
|
||||
你在 `package.json` 里会看到这样的内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"typescript": "~5.4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里的 `^` 和 `~` 是什么意思?
|
||||
|
||||
👇 **动手点点看**:鼠标悬停版本号各个部分,理解含义;点击范围符号,看哪些版本会被接受。
|
||||
|
||||
<DependencyTreeDemo />
|
||||
|
||||
### 4.1 为什么不锁死版本?
|
||||
|
||||
| 做法 | 优点 | 缺点 |
|
||||
| :--- | :--- | :--- |
|
||||
| `"axios": "1.6.8"`(精确锁定) | 完全可预测 | 安全补丁无法自动更新 |
|
||||
| `"axios": "^1.6.8"`(兼容范围,推荐) | 自动获取 bug 修复和新功能 | 极少情况下引入小不兼容 |
|
||||
| `"axios": "*"`(任意版本) | 总是最新 | 主版本升级会彻底破坏代码 |
|
||||
|
||||
**最佳实践**:用 `^` 声明范围 + 锁文件固定实际版本,两者配合使用。
|
||||
|
||||
### 4.2 依赖地狱是什么?
|
||||
|
||||
当你依赖 50 个包,每个包又依赖若干包,"依赖树"可能有几百个节点。如果两个你依赖的包需要**同一个库的不兼容版本**,就产生了"依赖冲突"。
|
||||
|
||||
各生态的解法:
|
||||
- **npm v3+**:同主版本提升到顶层共享,不同主版本各自安装一份
|
||||
- **pnpm**:硬链接 + 严格隔离,从根本上防止"幽灵依赖"(没声明却能用的包)
|
||||
- **cargo(Rust)**:语言层面强制每个包只能依赖同一版本,彻底规避冲突
|
||||
- **go mod(Go)**:最小版本选择(MVS)策略,选能满足所有约束的最低版本
|
||||
|
||||
---
|
||||
|
||||
## 5. 锁文件 —— 团队协作的基石
|
||||
|
||||
### 5.1 为什么需要锁文件?
|
||||
|
||||
假设 `package.json` 写的是 `"axios": "^1.6.0"`:
|
||||
|
||||
- 你今天安装 → 装到 `1.6.8`
|
||||
- 队友明天安装 → 可能装到 `1.7.0`(昨晚刚发布)
|
||||
- CI 服务器下周 → 可能装到 `1.7.1`
|
||||
|
||||
同样的代码,三个人跑出不同结果。**锁文件**记录每个包的精确版本,所有人按它安装,结果完全一致。
|
||||
|
||||
| 场景 | 命令 | 行为 |
|
||||
| :--- | :--- | :--- |
|
||||
| 开发环境同步 | `npm install` | 参考锁文件安装,不升级版本 |
|
||||
| CI / 生产部署 | `npm ci` | **严格**按锁文件安装,有差异直接报错 |
|
||||
| 主动升级版本 | `npm update` | 在允许范围内升级,并更新锁文件 |
|
||||
|
||||
### 5.2 锁文件应该提交到 Git 吗?
|
||||
|
||||
**应用程序必须提交,发布到 npm 的库可以不提交。**
|
||||
|
||||
- ✅ **Web 应用、后端服务**:必须提交,确保部署环境和开发环境完全一致
|
||||
- ❌ **npm 发布的库**:通常不提交,库的使用者有自己的锁文件
|
||||
- ✅ **Python 项目**:`requirements.txt` 本身就起锁文件作用,应该提交
|
||||
- ✅ **Go 项目**:`go.sum` 必须提交,用于完整性校验
|
||||
|
||||
---
|
||||
|
||||
## 6. Python 虚拟环境
|
||||
|
||||
Python 有一个特别需要注意的概念:**虚拟环境(venv)**。
|
||||
|
||||
**为什么需要?**
|
||||
|
||||
Python 默认**全局**安装包。你的项目 A 需要 `requests==2.28`,项目 B 需要 `requests==2.31`,两者会互相冲突。
|
||||
|
||||
**解决方案**:为每个项目创建独立的虚拟环境,互不干扰。
|
||||
|
||||
```bash
|
||||
# 1. 创建虚拟环境(在项目根目录运行)
|
||||
python -m venv .venv
|
||||
|
||||
# 2. 激活虚拟环境
|
||||
source .venv/bin/activate # macOS / Linux
|
||||
.venv\Scripts\activate # Windows(命令提示符 CMD)
|
||||
.venv\Scripts\Activate.ps1 # Windows(PowerShell)
|
||||
|
||||
# 3. 激活后,pip install 只影响当前虚拟环境,不污染全局
|
||||
pip install requests
|
||||
|
||||
# 4. 退出虚拟环境
|
||||
deactivate
|
||||
```
|
||||
|
||||
> ⚠️ **Windows 常见问题**:PowerShell 默认禁止运行脚本,需先执行:
|
||||
> ```powershell
|
||||
> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
> ```
|
||||
|
||||
**现代替代方案**:
|
||||
- `conda create -n myproject python=3.11` —— 连 Python 版本都一起管理
|
||||
- `uv venv && source .venv/bin/activate` —— Rust 写的,创建速度飞快
|
||||
|
||||
**`.venv` 要提交到 Git 吗?**
|
||||
|
||||
不要!`.venv` 是本机生成的,应加入 `.gitignore`。用 `requirements.txt` 或 `pyproject.toml` 来描述依赖。
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见问题速查
|
||||
|
||||
**Q: `node_modules` 要提交到 Git 吗?**
|
||||
|
||||
不要!通常有几百 MB,应该加入 `.gitignore`。有了 `package-lock.json`,任何人都能 `npm install` 快速重建。
|
||||
|
||||
**Q: 安装失败 / 出现奇怪报错怎么办?**
|
||||
|
||||
```bash
|
||||
# 清空缓存,删除旧安装,重来
|
||||
npm cache clean --force
|
||||
rm -rf node_modules package-lock.json # macOS/Linux
|
||||
rmdir /s /q node_modules && del package-lock.json # Windows CMD
|
||||
npm install
|
||||
```
|
||||
|
||||
**Q: 安装速度太慢?**
|
||||
|
||||
```bash
|
||||
# 切换到国内镜像(推荐写入 .npmrc 文件,不污染全局)
|
||||
echo "registry=https://registry.npmmirror.com" > .npmrc
|
||||
|
||||
# pip 也可以配置镜像
|
||||
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
**Q: 包有安全漏洞怎么处理?**
|
||||
|
||||
```bash
|
||||
npm audit # 扫描已知漏洞
|
||||
npm audit fix # 自动修复兼容的漏洞
|
||||
npm audit fix --force # 强制升级(可能有破坏性,谨慎用)
|
||||
```
|
||||
|
||||
**Q: 怎么知道某个包是否值得信赖?**
|
||||
|
||||
在 [npmjs.com](https://npmjs.com) 或 [bundlephobia.com](https://bundlephobia.com) 查看:
|
||||
- 周下载量(越高越可信)
|
||||
- 最后更新时间(超过 2 年没更新要谨慎)
|
||||
- 依赖数量(依赖越多,引入问题的可能性越大)
|
||||
- GitHub Stars 和 Issues 活跃度
|
||||
|
||||
**Q: Windows 上 winget 安装的软件在哪?**
|
||||
|
||||
winget 默认安装到系统目录(需要管理员)或 `%LOCALAPPDATA%\Microsoft\WindowsApps`。Scoop 安装的软件统一在 `%USERPROFILE%\scoop\apps\`,方便管理和迁移。
|
||||
|
||||
---
|
||||
|
||||
## 8. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Package** | 包 / 库 | 别人写好并发布的代码模块 |
|
||||
| **Registry** | 注册表 / 仓库 | 所有包的中央存储服务器(如 npmjs.com) |
|
||||
| **Dependency** | 依赖 | 你的项目运行所需要的其他包 |
|
||||
| **devDependency** | 开发依赖 | 只在开发阶段需要的包(测试框架、构建工具等) |
|
||||
| **Lockfile** | 锁文件 | 记录精确版本号,保证环境一致性 |
|
||||
| **SemVer** | 语义化版本 | MAJOR.MINOR.PATCH 版本命名规范 |
|
||||
| **node_modules** | 模块目录 | npm 安装的包实际存放的目录 |
|
||||
| **venv** | 虚拟环境 | Python 项目的独立包隔离沙箱 |
|
||||
| **tarball** | 压缩包 | 包的分发格式,通常为 `.tgz` 文件 |
|
||||
| **Hoisting** | 提升 | npm 将子依赖提升到顶层以避免重复安装 |
|
||||
| **Phantom Dependency** | 幽灵依赖 | 未在配置文件声明却能被使用的包(pnpm 可防止) |
|
||||
| **npx** | — | npm 自带的包运行器,临时运行包而无需安装 |
|
||||
| **go.sum** | — | Go 模块的哈希校验文件,防止依赖被篡改 |
|
||||
| **Crate** | — | Rust 生态中"包"的单位名称 |
|
||||
| **winget** | — | Windows 官方包管理器(Windows 10/11 内置) |
|
||||
|
||||
---
|
||||
|
||||
## 总结:包管理器的本质
|
||||
|
||||
四句话记住核心:
|
||||
|
||||
1. **包管理器 = 应用商店**:帮你找到、安装、管理代码零件,不必重复造轮子。
|
||||
2. **锁文件 = 团队契约**:固定精确版本,让"在我机器上好好的"成为历史。
|
||||
3. **语义化版本 = 沟通语言**:`^` 安全地获取更新,MAJOR 变了就要小心。
|
||||
4. **本地 > 全局**:项目依赖尽量本地安装,`npx` / `uvx` 临时运行工具,保持环境纯净。
|
||||
|
||||
@@ -1,3 +1,248 @@
|
||||
# 端口与 localhost
|
||||
|
||||
> 待实现
|
||||
> 💡 **学习指南**:当你执行 `npm run dev`,终端里出现 `http://localhost:5173` 时,你有没有想过:`localhost` 是什么?`5173` 又代表什么?为什么有时候会报 `EADDRINUSE` 错误?本章就来把这些日常开发中天天见、却很少深究的概念一次讲透。
|
||||
|
||||
在开始之前,建议你先补两块"基础砖":
|
||||
|
||||
- **网络基础**:如果你不太清楚 IP 地址和 HTTP 的概念,可以先看 [计算机基础 - 网络通信](../1-computer-fundamentals/network-fundamentals.md) 部分。
|
||||
- **终端基础**:如果你还不熟悉终端命令行,可以先看 [命令行与 Shell 脚本](./command-line-shell.md)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 引言:那个天天见的 `localhost:5173` 到底是什么?
|
||||
|
||||
<DevServerFlowDemo />
|
||||
|
||||
每个开发者的日常都离不开这一行输出:
|
||||
|
||||
```
|
||||
➜ Local: http://localhost:5173/
|
||||
```
|
||||
|
||||
但你有没有想过,这短短一行字里,藏着好几个关键概念:
|
||||
|
||||
- **http://** → 通信协议(用什么语言对话)
|
||||
- **localhost** → 目标地址(找谁)
|
||||
- **:5173** → 端口号(找到之后,敲哪扇门)
|
||||
|
||||
搞懂这三件事,你就能理解 90% 的开发环境网络问题。接下来我们逐个拆解。
|
||||
|
||||
---
|
||||
|
||||
## 1. 什么是端口?(IP 是大楼,端口是房间号)
|
||||
|
||||
### 1.1 一个直觉比喻
|
||||
|
||||
想象一台服务器是一栋大楼:
|
||||
|
||||
- **IP 地址**(如 `192.168.1.100`)就是大楼的门牌地址——告诉你"去哪栋楼"。
|
||||
- **端口号**(如 `:80`)就是楼里的房间号——告诉你"进哪间房"。
|
||||
|
||||
一栋楼里可以同时有餐厅(80 号房)、咖啡厅(443 号房)、办公室(22 号房)。同理,一台电脑上可以同时运行 Web 服务器、数据库、SSH 服务,各自占用不同的端口。
|
||||
|
||||
👇 **动手点点看**:
|
||||
点击下面的"房间门牌",模拟向不同端口发起连接。注意观察:当端口"开着"(有程序在监听)和"关着"时,分别会发生什么?
|
||||
|
||||
<PortAnalogyDemo />
|
||||
|
||||
### 1.2 端口号的取值范围
|
||||
|
||||
端口号是一个 **0–65535** 之间的整数(共 65536 个)。这么多端口被分为三个区间:
|
||||
|
||||
| 区间 | 范围 | 用途 | 举例 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **系统端口** | 0 – 1023 | 预留给标准协议,普通用户不能随意占用 | 80 (HTTP)、443 (HTTPS)、22 (SSH) |
|
||||
| **注册端口** | 1024 – 49151 | 给常见应用注册使用 | 3306 (MySQL)、5432 (PostgreSQL)、6379 (Redis) |
|
||||
| **动态端口** | 49152 – 65535 | 操作系统临时分配 | 浏览器发请求时,系统随机分配一个源端口 |
|
||||
|
||||
> 为什么你的开发服务器喜欢用 3000、5173、8080?因为这些都在"注册端口"范围内,不需要管理员权限就能监听,又不太容易和系统服务冲突。
|
||||
|
||||
### 1.3 开发中常见的端口号速查
|
||||
|
||||
👇 **动手点点看**:
|
||||
输入端口号或服务名搜索,点击任意一行可以展开查看使用示例。
|
||||
|
||||
<CommonPortsDemo />
|
||||
|
||||
---
|
||||
|
||||
## 2. 什么是 localhost?(自己找自己)
|
||||
|
||||
### 2.1 "环回"的核心概念
|
||||
|
||||
`localhost` 是一个特殊的域名,它永远指向**你自己这台电脑**。
|
||||
|
||||
当你在浏览器输入 `http://localhost:3000` 时,发生了这些事:
|
||||
|
||||
1. 浏览器问操作系统:"`localhost` 的 IP 是多少?"
|
||||
2. 操作系统直接回答:"`127.0.0.1`"(不需要联网查 DNS)
|
||||
3. 数据包发往 `127.0.0.1`,但**不会真的离开本机**
|
||||
4. 操作系统通过"环回接口(loopback interface)"把数据包**折返**回来
|
||||
5. 监听在 3000 端口上的程序收到请求,返回响应
|
||||
|
||||
**整个过程不经过网线、不经过路由器、不需要联网。**
|
||||
|
||||
👇 **动手点点看**:
|
||||
点击"发送请求",观察数据包的完整旅程。然后点击下方的"马甲卡片",了解 localhost 的几种写法和区别。
|
||||
|
||||
<LocalhostLoopbackDemo />
|
||||
|
||||
### 2.2 `localhost` vs `127.0.0.1` vs `0.0.0.0`
|
||||
|
||||
这三个概念经常被混淆,但它们的含义完全不同:
|
||||
|
||||
| 写法 | 含义 | 谁能访问 |
|
||||
| :--- | :--- | :--- |
|
||||
| `localhost` / `127.0.0.1` | 环回地址,仅本机 | 只有你自己的电脑 |
|
||||
| `0.0.0.0` | 监听所有网络接口 | 本机 + 局域网内其他设备 |
|
||||
| `192.168.x.x` | 局域网 IP | 局域网内的设备 |
|
||||
|
||||
**实际场景**:
|
||||
|
||||
```bash
|
||||
# 只有自己能访问(安全,适合开发)
|
||||
npm run dev -- --host localhost
|
||||
|
||||
# 手机也能访问(适合移动端调试)
|
||||
npm run dev -- --host 0.0.0.0
|
||||
```
|
||||
|
||||
> 很多框架(如 Vite、Next.js)默认监听 `localhost`,所以你的手机即使连着同一个 WiFi 也访问不了。想用手机调试?加上 `--host` 参数就行。
|
||||
|
||||
---
|
||||
|
||||
## 3. 端口冲突:最常见的开发环境问题
|
||||
|
||||
### 3.1 为什么会冲突?
|
||||
|
||||
**一个端口同一时刻只能被一个程序监听。** 这就像一个房间只能住一户人家。
|
||||
|
||||
如果你尝试启动第二个服务在同一个端口上,就会看到这个经典错误:
|
||||
|
||||
```
|
||||
Error: listen EADDRINUSE :::3000
|
||||
```
|
||||
|
||||
翻译成人话就是:**"3000 号房已经有人住了,你进不去!"**
|
||||
|
||||
常见的冲突场景:
|
||||
- 上次的开发服务器没关干净,还在后台运行
|
||||
- 两个不同的项目用了相同的默认端口
|
||||
- 某个系统服务已经占用了你想要的端口
|
||||
|
||||
👇 **动手点点看**:
|
||||
试着在下面的模拟器里多次启动服务。当端口冲突时,对比"直接启动"和"智能启动"的不同处理方式。
|
||||
|
||||
<PortConflictDemo />
|
||||
|
||||
### 3.2 排查与解决
|
||||
|
||||
遇到端口冲突时,排查流程非常固定:
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
# 第一步:查看谁在占用 3000 端口
|
||||
lsof -i :3000
|
||||
|
||||
# 第二步:拿到 PID 后,强制终止
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```bash
|
||||
# 第一步:查看谁在占用 3000 端口
|
||||
netstat -ano | findstr :3000
|
||||
|
||||
# 第二步:终止进程
|
||||
taskkill /PID <PID> /F
|
||||
```
|
||||
|
||||
> 很多现代框架(Vite、Create React App 等)遇到端口冲突时会自动询问"是否换一个端口?"。但了解底层原理,能帮你更快地排查那些框架帮不了你的疑难杂症。
|
||||
|
||||
---
|
||||
|
||||
## 4. 开发中的"同源策略"与跨域
|
||||
|
||||
### 4.1 什么是"源"?
|
||||
|
||||
浏览器有一个安全机制叫做**同源策略(Same-Origin Policy)**:只有**协议、域名、端口**三者完全一致,才算"同源"。
|
||||
|
||||
| 地址 A | 地址 B | 是否同源 | 原因 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `http://localhost:5173` | `http://localhost:5173/about` | ✅ 同源 | 协议、域名、端口都一样 |
|
||||
| `http://localhost:5173` | `http://localhost:3000` | ❌ 不同源 | **端口不同**(5173 vs 3000) |
|
||||
| `http://localhost:5173` | `https://localhost:5173` | ❌ 不同源 | **协议不同**(http vs https) |
|
||||
|
||||
### 4.2 为什么前后端分离必然遇到跨域?
|
||||
|
||||
当你的项目架构是:
|
||||
|
||||
```
|
||||
前端 (Vite) → http://localhost:5173
|
||||
后端 (Express) → http://localhost:3000
|
||||
```
|
||||
|
||||
前端页面从 `:5173` 加载,然后用 `fetch('/api/users')` 去请求 `:3000` 的接口——**端口不一样,触发跨域限制!**
|
||||
|
||||
**两种常见解决方案:**
|
||||
|
||||
**方案一:后端配置 CORS**
|
||||
```javascript
|
||||
// Express 后端
|
||||
app.use(cors({ origin: 'http://localhost:5173' }))
|
||||
```
|
||||
|
||||
**方案二:前端配置代理(推荐)**
|
||||
```javascript
|
||||
// vite.config.js
|
||||
export default {
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3000'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
代理的原理:让 Vite 开发服务器帮你"转发"请求。浏览器以为自己在和 `:5173` 通信(同源),实际上 Vite 在背后偷偷帮你把请求转给了 `:3000`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 实战排查:三个最常见的问题
|
||||
|
||||
👇 **动手点点看**:
|
||||
选择一个你遇到过的问题,跟着步骤一起排查。每一步都可以点击"执行"查看输出。
|
||||
|
||||
<PortTroubleshootDemo />
|
||||
|
||||
---
|
||||
|
||||
## 6. 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Port** | 端口 | 一个 0–65535 的数字,用来区分同一台机器上的不同网络服务。每个服务"监听"一个端口,等待客户端连接。 |
|
||||
| **localhost** | 本地主机 | 一个特殊域名,永远指向本机(127.0.0.1)。用于在不联网的情况下访问本机上运行的服务。 |
|
||||
| **Loopback Interface** | 环回接口 | 操作系统的虚拟网络接口。发往 127.0.0.1 的数据包不会离开本机,而是通过该接口"折返"回来。 |
|
||||
| **EADDRINUSE** | 地址已被使用 | Node.js / 操作系统报的错误,表示你要监听的端口已经被另一个程序占用了。 |
|
||||
| **CORS** | 跨域资源共享 | 浏览器安全机制。当前端页面尝试请求不同源(协议/域名/端口不同)的接口时,需要后端明确许可。 |
|
||||
| **Same-Origin Policy** | 同源策略 | 浏览器的安全基石:只允许同协议、同域名、同端口的请求自由通信,阻止跨域的数据读取。 |
|
||||
| **Proxy** | 代理 | 在开发环境中,代理服务器代替浏览器向后端转发请求,绕过浏览器的同源限制。 |
|
||||
| **0.0.0.0** | 所有接口 | 当服务监听 0.0.0.0 时,表示它接受来自任何网络接口(本机、局域网等)的连接。 |
|
||||
| **Well-known Ports** | 知名端口 | 0–1023 端口的统称,预留给 HTTP (80)、HTTPS (443)、SSH (22) 等标准协议。 |
|
||||
| **PID** | 进程 ID | 操作系统为每个运行中的程序分配的唯一编号,用于管理和终止进程。 |
|
||||
| **lsof** | 列出打开的文件 | macOS/Linux 命令,用于查看哪个进程占用了某个端口(`lsof -i :端口号`)。 |
|
||||
| **HMR** | 热模块替换 | 开发服务器的功能:你修改代码后,浏览器自动更新,无需手动刷新页面。底层通过 WebSocket 通知浏览器。 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
端口和 localhost 是开发环境中最基础、最高频的概念:
|
||||
|
||||
- **端口** = 一台机器上区分不同服务的"门牌号"(0–65535)
|
||||
- **localhost** = "自己找自己"的特殊地址(127.0.0.1),数据不出本机
|
||||
- **端口冲突**的本质是"一个门牌只能挂一块牌子"
|
||||
- **跨域**的本质是"端口不同 = 不同源",需要 CORS 或代理来解决
|
||||
|
||||
记住这四句话,你在开发环境里遇到的大多数网络问题,都能快速定位原因。
|
||||
|
||||
@@ -1,3 +1,178 @@
|
||||
# 正则表达式
|
||||
|
||||
> 待实现
|
||||
> 💡 **学习指南**:正则表达式看起来像天书?其实它只是一种"描述文本模式"的迷你语言。本章带你从零开始理解正则的核心思想,学会用几个关键符号解决 80% 的文本搜索和验证问题。
|
||||
|
||||
---
|
||||
|
||||
## 0. 你为什么需要正则表达式?
|
||||
|
||||
想象以下场景:
|
||||
- 从一大段日志里找出所有 IP 地址
|
||||
- 验证用户输入的邮箱格式是否合法
|
||||
- 把文本中所有的日期格式从 `2024/01/15` 替换为 `2024-01-15`
|
||||
- 从网页源码中提取所有链接
|
||||
|
||||
**用普通字符串搜索?** 你需要写一大堆 `if-else` 判断逻辑。
|
||||
**用正则表达式?** 一行模式搞定。
|
||||
|
||||
---
|
||||
|
||||
## 1. 正则入门:三分钟上手
|
||||
|
||||
👇 动手点点看:输入正则表达式,实时查看匹配结果
|
||||
|
||||
<RegexDemo />
|
||||
|
||||
::: tip 💡 一句话理解
|
||||
正则表达式 = **用特殊符号描述"你想找什么样的文本"**。`\d` 代表数字,`+` 代表一个或多个,所以 `\d+` 就是"一个或多个数字"。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心概念:像搭积木一样组合
|
||||
|
||||
正则的本质是用**三类积木**搭出你想要的模式:
|
||||
|
||||
### 2.1 积木一:字符类(匹配什么字符)
|
||||
|
||||
| 语法 | 含义 | 示例 |
|
||||
|---|---|---|
|
||||
| `.` | 任意字符 | `a.c` → abc, a1c, a c |
|
||||
| `\d` | 数字 [0-9] | `\d\d` → 42, 99 |
|
||||
| `\w` | 字母/数字/下划线 | `\w+` → hello, user_1 |
|
||||
| `\s` | 空白字符 | 匹配空格、Tab |
|
||||
| `[abc]` | 集合中的任意一个 | `[aeiou]` → 元音字母 |
|
||||
| `[^abc]` | 不在集合中的 | `[^0-9]` → 非数字字符 |
|
||||
|
||||
### 2.2 积木二:量词(匹配几次)
|
||||
|
||||
| 语法 | 含义 | 示例 |
|
||||
|---|---|---|
|
||||
| `*` | 0 次或多次 | `ab*` → a, ab, abbb |
|
||||
| `+` | 1 次或多次 | `ab+` → ab, abbb(不匹配 a) |
|
||||
| `?` | 0 次或 1 次 | `colou?r` → color, colour |
|
||||
| `{3}` | 恰好 3 次 | `\d{3}` → 123 |
|
||||
| `{2,4}` | 2 到 4 次 | `\d{2,4}` → 12, 1234 |
|
||||
|
||||
### 2.3 积木三:位置和分组
|
||||
|
||||
| 语法 | 含义 | 示例 |
|
||||
|---|---|---|
|
||||
| `^` | 行首 | `^Hello` → 以 Hello 开头的行 |
|
||||
| `$` | 行尾 | `end$` → 以 end 结尾的行 |
|
||||
| `\b` | 单词边界 | `\bcat\b` → cat(不匹配 catch) |
|
||||
| `(...)` | 捕获分组 | `(\d+)-(\d+)` → 分别捕获 |
|
||||
| `a\|b` | 或 | `cat\|dog` → cat 或 dog |
|
||||
|
||||
---
|
||||
|
||||
## 3. 实战:常见验证模式
|
||||
|
||||
### 3.1 邮箱验证
|
||||
|
||||
```
|
||||
[\w.+-]+@[\w-]+\.[\w.]+
|
||||
```
|
||||
|
||||
拆解:
|
||||
- `[\w.+-]+` — 用户名部分(字母数字点加号横杠)
|
||||
- `@` — 字面量 @
|
||||
- `[\w-]+` — 域名部分
|
||||
- `\.` — 转义的点
|
||||
- `[\w.]+` — 顶级域名
|
||||
|
||||
### 3.2 手机号验证(中国)
|
||||
|
||||
```
|
||||
1[3-9]\d{9}
|
||||
```
|
||||
|
||||
拆解:
|
||||
- `1` — 以 1 开头
|
||||
- `[3-9]` — 第二位是 3-9
|
||||
- `\d{9}` — 后面跟 9 位数字
|
||||
|
||||
### 3.3 密码强度检查
|
||||
|
||||
```
|
||||
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
|
||||
```
|
||||
|
||||
拆解:
|
||||
- `(?=.*[a-z])` — 至少一个小写字母(前瞻断言)
|
||||
- `(?=.*[A-Z])` — 至少一个大写字母
|
||||
- `(?=.*\d)` — 至少一个数字
|
||||
- `.{8,}` — 总长度至少 8 位
|
||||
|
||||
---
|
||||
|
||||
## 4. 在代码中使用正则
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
const text = '联系方式:13812345678 或 15099887766'
|
||||
const regex = /1[3-9]\d{9}/g
|
||||
const phones = text.match(regex)
|
||||
// ['13812345678', '15099887766']
|
||||
|
||||
// 替换
|
||||
text.replace(/\d{4}(?=\d{4}$)/, '****')
|
||||
// 隐藏手机号中间四位
|
||||
|
||||
// 验证
|
||||
/^[\w.+-]+@[\w-]+\.[\w.]+$/.test('user@example.com')
|
||||
// true
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
text = '价格是 99 元,优惠 20 元'
|
||||
numbers = re.findall(r'\d+', text)
|
||||
# ['99', '20']
|
||||
|
||||
# 替换
|
||||
re.sub(r'\d+', 'X', text)
|
||||
# '价格是 X 元,优惠 X 元'
|
||||
|
||||
# 分组捕获
|
||||
match = re.search(r'(\d+)-(\d+)', '2024-01-15')
|
||||
match.group(1) # '2024'
|
||||
match.group(2) # '01'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 贪婪 vs 懒惰:一个关键区别
|
||||
|
||||
```
|
||||
文本: <b>hello</b> and <b>world</b>
|
||||
```
|
||||
|
||||
| 模式 | 匹配结果 | 说明 |
|
||||
|---|---|---|
|
||||
| `<b>.*</b>` | `<b>hello</b> and <b>world</b>` | 贪婪:尽量多匹配 |
|
||||
| `<b>.*?</b>` | `<b>hello</b>` | 懒惰:尽量少匹配 |
|
||||
|
||||
::: tip 💡 记住
|
||||
默认是贪婪模式。在量词后面加 `?` 变成懒惰模式。大多数时候,你需要的是懒惰模式。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
::: tip 📚 核心要点
|
||||
1. **正则 = 描述文本模式的迷你语言**,用于搜索、匹配、替换
|
||||
2. **三类积木**:字符类(匹配什么)+ 量词(匹配几次)+ 位置/分组
|
||||
3. **\d \w \s** 是最常用的三个字符类,覆盖数字、单词、空白
|
||||
4. **不需要从零写**:常见场景都有成熟的正则模式可以复用
|
||||
5. **贪婪 vs 懒惰**:默认贪婪(多匹配),加 `?` 变懒惰(少匹配)
|
||||
:::
|
||||
|
||||
**下一步学习**:
|
||||
- [环境变量与 PATH](./environment-path) - 理解系统配置
|
||||
- [SSH 与密钥认证](./ssh-authentication) - 安全连接远程服务器
|
||||
|
||||
@@ -1,3 +1,138 @@
|
||||
# SSH 与密钥认证
|
||||
|
||||
> 待实现
|
||||
> 💡 **学习指南**:每次 `git push` 输密码?连服务器总被提示"Permission denied"?本章用 5 分钟带你搞懂 SSH 密钥认证的原理,以及如何一键免密登录 GitHub 和服务器。
|
||||
|
||||
---
|
||||
|
||||
## 0. 你一定遇到过这些场景
|
||||
|
||||
- `git push` 时反复弹出密码输入框,烦不胜烦
|
||||
- SSH 连接服务器失败,不知道 `id_rsa` 和 `id_ed25519` 是什么
|
||||
- 听说"公钥"和"私钥",但搞不清哪个给别人、哪个自己留
|
||||
|
||||
**核心矛盾**:密码不安全、又麻烦。SSH 密钥就是用来同时解决安全性和便利性的方案。
|
||||
|
||||
---
|
||||
|
||||
## 1. 密码 vs 密钥:为什么密钥更好?
|
||||
|
||||
👇 动手点点看:对比密码登录和密钥登录的区别
|
||||
|
||||
<SSHAuthDemo />
|
||||
|
||||
::: tip 💡 一句话总结
|
||||
密码登录 = 每次把密码发过去让对方核对(密码可能被截获);
|
||||
密钥登录 = 证明"我有钥匙"但不用把钥匙给你看(私钥永不传输)。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 2. 非对称加密:公钥和私钥
|
||||
|
||||
SSH 密钥基于**非对称加密**,一次生成两把钥匙:
|
||||
|
||||
| | 私钥 (Private Key) | 公钥 (Public Key) |
|
||||
|---|---|---|
|
||||
| **保存位置** | 你的电脑 `~/.ssh/id_ed25519` | 服务器/GitHub |
|
||||
| **可以给别人吗** | ❌ 绝不 | ✅ 随便给 |
|
||||
| **功能** | 签名(证明身份) | 验签(验证身份) |
|
||||
| **类比** | 钥匙 | 锁 |
|
||||
|
||||
### 常见密钥类型
|
||||
|
||||
| 类型 | 命令 | 推荐度 | 说明 |
|
||||
|---|---|---|---|
|
||||
| **Ed25519** | `ssh-keygen -t ed25519` | ⭐⭐⭐ | 最新最快最安全 |
|
||||
| **RSA** | `ssh-keygen -t rsa -b 4096` | ⭐⭐ | 兼容性好,但较慢 |
|
||||
| **ECDSA** | `ssh-keygen -t ecdsa` | ⭐ | 一般不推荐 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 实战:生成并配置 SSH 密钥
|
||||
|
||||
### 3.1 生成密钥对
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "your@email.com"
|
||||
```
|
||||
|
||||
执行后会提示:
|
||||
- **文件路径**:直接回车用默认路径 `~/.ssh/id_ed25519`
|
||||
- **密码短语**:可以设置额外保护(也可留空)
|
||||
|
||||
### 3.2 把公钥添加到 GitHub
|
||||
|
||||
```bash
|
||||
# 1. 复制公钥内容
|
||||
cat ~/.ssh/id_ed25519.pub | pbcopy # macOS
|
||||
cat ~/.ssh/id_ed25519.pub | xclip # Linux
|
||||
|
||||
# 2. 打开 GitHub → Settings → SSH and GPG keys → New SSH key
|
||||
# 3. 粘贴公钥,保存
|
||||
|
||||
# 4. 测试连接
|
||||
ssh -T git@github.com
|
||||
# 成功会看到: Hi username! You've been authenticated...
|
||||
```
|
||||
|
||||
### 3.3 把公钥添加到服务器
|
||||
|
||||
```bash
|
||||
# 方式一:ssh-copy-id(推荐)
|
||||
ssh-copy-id user@your-server
|
||||
|
||||
# 方式二:手动复制
|
||||
cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. SSH Config:告别长命令
|
||||
|
||||
在 `~/.ssh/config` 中配置别名,一次配置终身受益:
|
||||
|
||||
```
|
||||
Host dev
|
||||
HostName 192.168.1.100
|
||||
User deploy
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
|
||||
Host github.com
|
||||
HostName github.com
|
||||
User git
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
配置后的效果:
|
||||
|
||||
| 之前 | 之后 |
|
||||
|---|---|
|
||||
| `ssh -i ~/.ssh/id_ed25519 deploy@192.168.1.100` | `ssh dev` |
|
||||
| 每次都要记 IP 和用户名 | 记一个别名就够 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见问题排查
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|---|---|---|
|
||||
| `Permission denied (publickey)` | 公钥没添加到服务器 | `ssh-copy-id user@server` |
|
||||
| `WARNING: UNPROTECTED PRIVATE KEY FILE` | 私钥文件权限太宽 | `chmod 600 ~/.ssh/id_ed25519` |
|
||||
| `Could not resolve hostname` | SSH Config 配置有误 | 检查 `~/.ssh/config` 格式 |
|
||||
| GitHub 还是要密码 | 用的 HTTPS 而非 SSH | 改用 `git@github.com:user/repo.git` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 总结
|
||||
|
||||
::: tip 📚 核心要点
|
||||
1. **密钥 > 密码**:私钥永不传输,比密码安全得多
|
||||
2. **推荐 Ed25519**:最现代的密钥算法,速度快、安全性高
|
||||
3. **公钥随便给,私钥绝不泄露**:记住这条铁律
|
||||
4. **SSH Config**:配一次别名,之后 `ssh 别名` 一键连接
|
||||
5. **GitHub/GitLab**:添加公钥后,`git push/pull` 再也不需要输密码
|
||||
:::
|
||||
|
||||
**下一步学习**:
|
||||
- [端口与 localhost](./ports-localhost) - 理解网络连接的基础
|
||||
- [环境变量与 PATH](./environment-path) - 理解系统配置
|
||||
|
||||
@@ -1,3 +1,473 @@
|
||||
# 前端框架的本质
|
||||
|
||||
> 待实现
|
||||
> 💡 **学习指南**:这篇文章会回答一个根本问题——**前端框架(Vue、React、Svelte 等)到底在做什么?** 如果你只学过 HTML、CSS 和一点 JavaScript,完全没问题,我们从头讲起。
|
||||
|
||||
在开始之前,先确认你知道这两个基础概念。如果不确定,可以先看对应章节:
|
||||
|
||||
- **HTML**:网页的骨架,定义页面上有哪些元素(标题、段落、按钮、图片……)。参见 [HTML 与 CSS 布局](./html-css-layout.md)。
|
||||
- **JavaScript**:让网页"动起来"的编程语言,可以修改页面内容、响应用户操作。参见 [JavaScript 深度指南](./javascript-deep-dive.md)。
|
||||
|
||||
还有一个概念会在后面频繁出现,这里先做一个完整的说明。
|
||||
|
||||
### 什么是 DOM?
|
||||
|
||||
DOM 的全称是 Document Object Model,中文叫"文档对象模型"。
|
||||
|
||||
当你在浏览器中打开一个网页时,浏览器做的第一件事就是读取 HTML 代码。读完之后,浏览器不会直接拿 HTML 文本去显示页面,而是先把 HTML 代码**转换成一棵树形结构**,存放在内存里。这棵树就叫 DOM 树。
|
||||
|
||||
树上的每一个节点(Node)对应 HTML 里的一个标签。标签之间的嵌套关系,在 DOM 树里就变成了父节点和子节点的关系。
|
||||
|
||||
👇 **动手试试看**:
|
||||
把鼠标移到左边的 HTML 代码上,右边 DOM 树中对应的节点会高亮。反过来也一样。每一行 HTML 标签都对应 DOM 树上的一个节点。
|
||||
|
||||
<WhatIsDomDemo />
|
||||
|
||||
**为什么要了解 DOM?** 因为 JavaScript 修改页面的方式,就是操作这棵 DOM 树——增加节点、删除节点、修改节点的内容。而前端框架做的核心工作,就是帮你自动化这些 DOM 操作。后面我们会反复提到 DOM,理解它是理解框架原理的基础。
|
||||
|
||||
---
|
||||
|
||||
## 0. 引言:什么是"前端框架"?
|
||||
|
||||
先解释"框架"这个词。在编程中,**框架(Framework)** 是一套已经写好的代码和规则,它规定了你的代码应该怎么组织、怎么运行。你按照它的方式写代码,它帮你处理大量重复、繁琐的底层工作。
|
||||
|
||||
**前端框架**,就是专门帮你**构建网页界面**的框架。目前最常见的有 Vue、React、Svelte、Angular 这几个。
|
||||
|
||||
那它们到底帮你解决了什么问题?下面这三张卡片概括了核心逻辑:
|
||||
|
||||
<FrameworkMotivationDemo />
|
||||
|
||||
接下来我们一步步展开,从最基础的问题讲起。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心问题:数据变了,界面怎么办?
|
||||
|
||||
### 1.1 先搞清楚"数据"和"界面"是什么
|
||||
|
||||
在任何一个网页应用中,都有两个东西在同时存在:
|
||||
|
||||
- **数据(Data / State)**:程序内部存储的信息。比如"购物车里有 3 件商品"、"用户名是张三"、"当前选中了第 2 个标签页"。这些数据存在 JavaScript 的变量里,用户看不到它们。
|
||||
- **界面(UI)**:用户在屏幕上看到的东西。比如页面上显示"购物车(3)"、显示"欢迎,张三"、第 2 个标签页高亮。这些是 HTML 元素呈现出来的视觉效果。
|
||||
|
||||
**数据和界面之间有对应关系**:数据是"3 件商品",界面上就应该显示"3"。如果数据变成了"4 件商品",界面上也应该跟着变成"4"。
|
||||
|
||||
问题是:**这个"跟着变"的过程,谁来负责?**
|
||||
|
||||
👇 **动手点点看**:
|
||||
点击"添加商品"按钮,注意观察:数据(左边)已经变了,但界面(右边)没有跟着更新——它们之间"断开"了。再点"同步界面"手动修复。
|
||||
|
||||
<DataUIGapDemo />
|
||||
|
||||
### 1.2 为什么 JavaScript 变量变了,界面不会自动变?
|
||||
|
||||
这是零基础最容易困惑的地方,我们把底层原理一步步讲清楚。
|
||||
|
||||
在 JavaScript 中,变量就是一块内存空间,用来存放数据。当你执行 `count = count + 1` 时,JavaScript 引擎做的事情非常简单:把内存中 count 这个位置的值从 3 改成 4。**做完这一步就结束了,不会再发生任何事。**
|
||||
|
||||
而页面上显示的内容(比如 `<span>3</span>` 这个 DOM 节点)存放在另一块完全不同的内存空间里。JavaScript 引擎在修改变量时,根本不知道页面上有一个 DOM 节点正在显示这个变量的值,也没有任何机制让它去检查。
|
||||
|
||||
所以本质原因是:**JavaScript 的变量和 DOM 节点是两块独立的内存,它们之间没有任何自动联动机制。** 修改变量只改变了变量所在的内存,DOM 节点所在的内存不会受到任何影响。
|
||||
|
||||
```javascript
|
||||
let count = 3
|
||||
|
||||
// 页面上有一个 DOM 节点显示着 count 的值:
|
||||
// <span id="counter">3</span>
|
||||
|
||||
count = 4
|
||||
// JavaScript 引擎做了什么?
|
||||
// → 把变量 count 在内存中的值从 3 改成 4
|
||||
// → 结束。没了。
|
||||
// 页面上 <span> 里显示的仍然是 "3"
|
||||
```
|
||||
|
||||
如果你想让页面上的显示也变成"4",你必须**额外写代码**,手动找到那个 DOM 节点,然后修改它的内容:
|
||||
|
||||
```javascript
|
||||
count = 4 // 第 1 步:改变量
|
||||
|
||||
// 第 2 步:你必须自己写——找到 DOM 节点,把它的文字改成新值
|
||||
document.getElementById('counter').textContent = count
|
||||
```
|
||||
|
||||
如果页面上有 5 个地方显示着 count 的值(购物车数量、商品列表、总价、小计、状态提示),你就需要写 5 段这样的代码。**漏掉任何一段,那个位置显示的就还是旧值,用户看到的就是错误信息。**
|
||||
|
||||
### 1.3 框架做了什么?两步建立自动连接
|
||||
|
||||
框架能自动同步,靠的是**两步配合**——缺一不可。
|
||||
|
||||
**第一步:你在模板里"登记"哪些地方要显示这个变量**
|
||||
|
||||
框架的 HTML 模板里,你用 `{{ count }}` 这样的语法来标记"这里要显示 count 的值":
|
||||
|
||||
```html
|
||||
<!-- Vue 模板 -->
|
||||
<span>购物车:{{ count }} 件</span> <!-- 位置 A:我要显示 count -->
|
||||
<span>总价:¥{{ count * 99 }}</span> <!-- 位置 B:我也用了 count -->
|
||||
<span>{{ count > 5 ? '过多' : '正常' }}</span> <!-- 位置 C:我也用了 count -->
|
||||
```
|
||||
|
||||
框架第一次渲染页面时,会把这个"登记关系"记录下来:**位置 A、B、C 都依赖 count**。
|
||||
|
||||
**第二步:框架监视变量,变了就查登记表、自动更新**
|
||||
|
||||
框架用 JavaScript 内置的 `Proxy`(代理)把你的变量"包裹"起来,让它变成一个"被监视的变量"。当你修改这个变量时,Proxy 会在赋值的同时悄悄多做一件事:通知框架"count 变了"。框架收到通知后,去查第一步的登记表,把 A、B、C 三个位置全部更新。
|
||||
|
||||
```
|
||||
原生 JS:
|
||||
你写 HTML → <span id="counter">3</span>(和变量无任何连接)
|
||||
你改变量 → count = 4 → 结束,界面毫无反应
|
||||
你手动补 → document.getElementById('counter').textContent = 4 → 界面才更新
|
||||
|
||||
Vue 框架:
|
||||
你写模板 → <span>{{ count }}</span>(框架记住:这里依赖 count)
|
||||
你改变量 → count = 4 → Proxy 拦截 → 通知框架 → 框架查登记表 → 自动更新 A/B/C
|
||||
```
|
||||
|
||||
这就是为什么"只有框架才能自动同步"——原生 HTML 里的 `<span>` 和 JS 变量之间根本没有任何连接,框架的模板语法(`{{ }}`)才是建立这条连接的关键。你写了 `{{ count }}`,框架才知道这里要显示 count;框架才能在 count 变化时,精准找到这里并更新它。
|
||||
|
||||
👇 **动手点点看**:
|
||||
先选"原生 JavaScript",点"执行"后注意观察——变量改了但界面纹丝不动,你要一步步手动同步每个位置。再切换到"使用框架",同样点"执行"——变量一改,框架自动完成所有步骤,界面立刻跟上。
|
||||
|
||||
<WhyNoAutoSyncDemo />
|
||||
|
||||
### 1.4 对比:手动同步 vs 自动同步的实际效果
|
||||
|
||||
理解了原理之后,我们来看看在一个稍微复杂一点的场景下,手动同步和自动同步的区别有多大。
|
||||
|
||||
👇 **动手点点看**:
|
||||
左边是没有框架时的"手动同步"方式——每个显示区域你都需要单独点"同步"按钮来更新。右边是有框架时的"自动同步"方式——你只管点"添加商品",所有显示区域自动更新。试试在左边故意不同步某个区域,看看会发生什么。
|
||||
|
||||
<ManualVsAutoSyncDemo />
|
||||
|
||||
**这就是前端框架存在的根本原因:给 JavaScript 变量加上"被修改时自动通知界面更新"的能力,消灭手动同步带来的错误。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 框架的核心思想:用数据描述界面
|
||||
|
||||
### 2.1 两种写法的区别
|
||||
|
||||
理解了"自动同步"的价值之后,我们来看框架具体是怎么实现的。
|
||||
|
||||
在没有框架的时代(比如使用 jQuery),代码是这样写的——你一步一步告诉浏览器该做什么:
|
||||
|
||||
```javascript
|
||||
// 第 1 步:找到页面上 id 为 counter 的元素
|
||||
var element = document.getElementById('counter')
|
||||
// 第 2 步:把这个元素的文字内容改成新的值
|
||||
element.textContent = '4'
|
||||
// 第 3 步:找到另一个元素,也改掉
|
||||
document.getElementById('total').textContent = '¥396'
|
||||
// 第 4 步:如果数量大于 5,还要改状态提示……
|
||||
```
|
||||
|
||||
这种写法叫**命令式(Imperative)**——你在"命令"浏览器一步步执行操作。
|
||||
|
||||
有了框架之后,代码变成这样——你只描述"界面应该长什么样":
|
||||
|
||||
```html
|
||||
<!-- 我不管这个值怎么更新到页面上的 -->
|
||||
<!-- 我只说:这里应该显示 count 的值 -->
|
||||
<span>{{ count }}</span>
|
||||
<span>总价:¥{{ count * 99 }}</span>
|
||||
<span v-if="count > 5">商品过多!</span>
|
||||
```
|
||||
|
||||
这种写法叫**声明式(Declarative)**——你在"声明"界面的最终状态,至于怎么达到这个状态,框架自己处理。
|
||||
|
||||
### 2.2 核心公式:UI = f(State)
|
||||
|
||||
所有现代前端框架——不管是 Vue、React 还是 Svelte——都遵循同一个核心思想,可以用一个公式来表达:
|
||||
|
||||
> **UI = f(State)**
|
||||
|
||||
这个公式的意思是:
|
||||
|
||||
- **State(状态)**:你的应用数据。就是 JavaScript 里的那些变量:购物车里有几件商品、用户有没有登录、当前页面是哪个……
|
||||
- **f(函数)**:框架的渲染机制。它知道怎么把数据变成界面。
|
||||
- **UI(界面)**:用户在屏幕上看到的最终结果。
|
||||
|
||||
**含义**:给定一组数据(State),经过框架的处理(f),就能确定性地得到对应的界面(UI)。数据变了,界面就跟着变。开发者只需要关心数据,不需要关心界面怎么更新。
|
||||
|
||||
👇 **动手点点看**:
|
||||
在左边修改数据(State),观察右边的界面(UI)如何自动跟着变化。这就是 `UI = f(State)` 的直观体现。
|
||||
|
||||
<DeclarativeFormulaDemo />
|
||||
|
||||
### 2.3 为什么声明式比命令式好?
|
||||
|
||||
声明式写法的优势在于:
|
||||
|
||||
| 对比维度 | 命令式(没有框架) | 声明式(有框架) |
|
||||
| :--- | :--- | :--- |
|
||||
| **代码量** | 每个更新都要写具体操作代码 | 只写一次模板,框架自动处理 |
|
||||
| **出错概率** | 容易漏更新某个地方 | 框架保证所有地方都更新 |
|
||||
| **可读性** | 代码里混杂着大量 DOM 操作 | 代码清晰地描述界面结构 |
|
||||
| **维护成本** | 修改一个功能要改很多地方 | 修改数据逻辑即可,界面自动跟随 |
|
||||
|
||||
简单说:声明式让你把精力集中在"业务逻辑"(数据怎么变化)上,不用操心"界面怎么更新"这个重复且容易出错的事情。
|
||||
|
||||
---
|
||||
|
||||
## 3. 响应式系统:框架如何知道数据变了?
|
||||
|
||||
### 3.1 什么是"响应式"?
|
||||
|
||||
前面说了"数据变了,界面自动更新"。但这里有一个技术问题:**JavaScript 本身并没有"变量被修改时自动通知别人"的能力**。
|
||||
|
||||
你写 `count = 4`,JavaScript 只是把 `count` 的值从 3 改成 4,不会自动告诉任何人。框架需要一种机制来"发现"你修改了数据。
|
||||
|
||||
**响应式(Reactivity)** 就是这种机制的总称:当数据发生变化时,系统能自动感知到变化,并执行相应的更新操作。
|
||||
|
||||
### 3.2 三种不同的实现方式
|
||||
|
||||
不同的框架采用了不同的技术方案来实现响应式。这也是 Vue、React、Svelte 之间最根本的区别。
|
||||
|
||||
**方式一:代理拦截(Vue 的做法)**
|
||||
|
||||
Vue 使用 JavaScript 内置的 `Proxy`(代理)机制。`Proxy` 可以在你读取或修改一个对象的属性时,自动执行一段你指定的代码。
|
||||
|
||||
Vue 把你的数据对象用 `Proxy` 包裹起来。当你执行 `count = 4` 时,`Proxy` 会拦截这次写入操作,通知 Vue:"count 的值变了",然后 Vue 去更新所有用到 `count` 的界面部分。
|
||||
|
||||
你作为开发者不需要做任何额外的事情——直接赋值就行,Vue 自动感知。
|
||||
|
||||
**方式二:显式调用(React 的做法)**
|
||||
|
||||
React 不使用 `Proxy`。它要求你必须通过一个专门的函数来修改数据:
|
||||
|
||||
```javascript
|
||||
// React 的写法
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
// 不能直接写 count = 4(React 不会感知到)
|
||||
// 必须调用 setCount:
|
||||
setCount(4)
|
||||
```
|
||||
|
||||
只有当你调用 `setCount()` 时,React 才知道数据变了,才会去更新界面。如果你直接写 `count = 4`,React 完全不知道,界面不会更新。
|
||||
|
||||
这种方式更"显式"——每一次数据变化都是你主动告诉框架的,不会有意外的更新。
|
||||
|
||||
**方式三:编译器分析(Svelte 的做法)**
|
||||
|
||||
Svelte 采用了完全不同的路线。它有一个编译器(Compiler),在你的代码运行之前,编译器会先分析你的源代码。
|
||||
|
||||
当编译器看到你写了 `count += 1` 这样的赋值语句时,它会自动在这行代码后面插入一段"通知界面更新"的代码。也就是说,在代码运行的时候,"通知"这个动作已经被编译器提前安排好了。
|
||||
|
||||
你的代码看起来就是普通的 JavaScript 赋值,但编译后的代码里多了更新界面的逻辑。
|
||||
|
||||
👇 **动手点点看**:
|
||||
选择不同的框架标签,点击"修改数据",观察每种框架在"引擎盖下"经历了哪些步骤来完成数据变化的检测和界面更新。
|
||||
|
||||
<ReactivityMechanismDemo />
|
||||
|
||||
### 3.3 三种方式的对比
|
||||
|
||||
| 对比维度 | Vue(Proxy 代理) | React(显式调用) | Svelte(编译器) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **开发者写法** | 直接赋值 `count = 4` | 必须用 `setCount(4)` | 直接赋值 `count = 4` |
|
||||
| **感知变化的时机** | 运行时自动拦截 | 开发者主动通知 | 编译时提前插入通知代码 |
|
||||
| **运行时性能开销** | Proxy 有少量拦截开销 | setState 调度有少量开销 | 几乎没有额外开销 |
|
||||
| **调试难度** | 中等 | 数据流清晰,较容易 | 需要理解编译后的代码 |
|
||||
| **适合场景** | 追求开发效率和自然写法 | 追求可预测的数据流 | 追求极致运行性能 |
|
||||
|
||||
三种方式没有绝对的好坏。Vue 写起来最自然,React 的数据流最可控,Svelte 的运行性能最好。选择哪个取决于项目的具体需求。
|
||||
|
||||
---
|
||||
|
||||
## 4. 组件:把界面拆成可复用的小块
|
||||
|
||||
### 4.1 为什么要拆?
|
||||
|
||||
一个完整的网页可能有导航栏、侧边栏、内容区、搜索框、用户头像、各种按钮……如果所有代码写在一个文件里,这个文件会变得非常长、非常难维护。
|
||||
|
||||
**组件(Component)** 就是把界面拆分成一个个独立的小块,每个小块管自己的数据、自己的界面、自己的逻辑。
|
||||
|
||||
比如一个电商页面可以拆成这些组件:
|
||||
|
||||
- `NavBar` 组件:负责顶部导航栏
|
||||
- `SearchBox` 组件:负责搜索框
|
||||
- `ProductCard` 组件:负责一张商品卡片
|
||||
- `ShoppingCart` 组件:负责购物车
|
||||
|
||||
每个组件都是独立的。`ProductCard` 不需要知道 `NavBar` 里写了什么代码,它只需要管好自己。
|
||||
|
||||
### 4.2 组件的三个好处
|
||||
|
||||
**好处一:复用。** 一个 `ProductCard` 组件写好之后,可以在页面上用 100 次——每次传入不同的商品数据,就会渲染出不同的商品卡片。不需要复制粘贴 100 份 HTML 代码。
|
||||
|
||||
**好处二:封装。** 组件内部的数据和逻辑是独立的。修改 `SearchBox` 组件的代码,不会影响到 `ProductCard` 组件。多人协作时,不同的人可以同时开发不同的组件,互不干扰。
|
||||
|
||||
**好处三:可维护。** 当某个功能出了问题,你可以直接定位到对应的组件去修复,不需要在一个几千行的大文件里翻找。
|
||||
|
||||
👇 **动手点点看**:
|
||||
点击左边的组件名称,查看它在页面上对应的区域。注意观察:同一个 `ProductCard` 组件被复用了多次,每次显示不同的数据。
|
||||
|
||||
<ComponentTreeDemo />
|
||||
|
||||
### 4.3 组件在代码里长什么样?
|
||||
|
||||
以 Vue 为例,一个组件就是一个 `.vue` 文件,里面包含三部分:
|
||||
|
||||
```html
|
||||
<!-- ProductCard.vue -->
|
||||
<template>
|
||||
<!-- 这里写 HTML 结构 —— 组件的"外观" -->
|
||||
<div class="card">
|
||||
<h3>{{ name }}</h3>
|
||||
<p>价格:¥{{ price }}</p>
|
||||
<button @click="addToCart">加入购物车</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 这里写 JavaScript 逻辑 —— 组件的"行为"
|
||||
const props = defineProps(['name', 'price'])
|
||||
|
||||
function addToCart() {
|
||||
// 处理"加入购物车"的逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 这里写 CSS 样式 —— 组件的"样式" */
|
||||
.card {
|
||||
border: 1px solid #ccc;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
使用这个组件时,就像使用一个自定义的 HTML 标签:
|
||||
|
||||
```html
|
||||
<!-- 在其他地方使用 ProductCard 组件 -->
|
||||
<ProductCard name="无线耳机" price="299" />
|
||||
<ProductCard name="机械键盘" price="599" />
|
||||
<ProductCard name="显示器" price="1999" />
|
||||
```
|
||||
|
||||
三行代码就渲染出了三张不同的商品卡片。
|
||||
|
||||
---
|
||||
|
||||
## 5. DOM 操作的代价:为什么框架要费这么大力气?
|
||||
|
||||
### 5.1 什么是 DOM 操作?
|
||||
|
||||
前面提到过 DOM——浏览器把 HTML 解析后生成的树形结构。**DOM 操作**就是用 JavaScript 去修改这棵树上的节点。比如改一段文字、增加一个元素、删除一个元素、修改一个样式。
|
||||
|
||||
这些操作本身不复杂,但是浏览器在执行 DOM 操作之后,需要做很多额外的工作才能让屏幕上的显示更新:
|
||||
|
||||
1. **重新计算样式**:这个节点以及它的子节点的 CSS 样式是否需要变化?
|
||||
2. **重新布局(Layout / Reflow)**:页面上所有元素的位置和大小需要重新计算。因为一个元素的改变可能影响到其他元素的位置。
|
||||
3. **重新绘制(Paint)**:把计算好的内容画到屏幕上。
|
||||
|
||||
这三个步骤每一个都有计算成本。如果你的代码频繁触发 DOM 操作,浏览器就会反复执行这些步骤,页面就会变卡。
|
||||
|
||||
👇 **动手点点看**:
|
||||
观察直接操作 DOM 和批量操作 DOM 的耗时对比。当修改次数增多时,"逐个操作"的耗时会急剧上升。
|
||||
|
||||
<DomOperationCostDemo />
|
||||
|
||||
### 5.2 框架怎么解决这个问题?
|
||||
|
||||
既然直接操作 DOM 很昂贵,框架就想办法**减少 DOM 操作的次数**。具体有两种策略:
|
||||
|
||||
**策略一:虚拟 DOM + 差异比较(Vue、React 的做法)**
|
||||
|
||||
虚拟 DOM(Virtual DOM)是一个 JavaScript 对象,它的结构和真实 DOM 树一一对应,但它只存在于内存中,不会触发浏览器的布局和绘制。
|
||||
|
||||
当数据变化时,框架的处理流程是:
|
||||
|
||||
1. 用 JavaScript 对象创建一棵"新的虚拟 DOM 树",描述数据变化后界面应该长什么样
|
||||
2. 把这棵新树和旧树做对比(这个过程叫 **Diff**,即差异比较),找出哪些节点发生了变化
|
||||
3. 只把真正变化的部分应用到真实 DOM 上(这个过程叫 **Patch**,即打补丁)
|
||||
|
||||
这样一来,不管数据怎么变化,最终对真实 DOM 的操作总是最少的。
|
||||
|
||||
👇 **动手点点看**:
|
||||
点击"修改数据",观察虚拟 DOM 如何对比新旧两棵树,找出变化的节点。注意看最右边的"真实 DOM"——只有真正变化的部分才会闪烁。
|
||||
|
||||
<VirtualDomDiffDemo />
|
||||
|
||||
**策略二:编译时精确定位(Svelte 的做法)**
|
||||
|
||||
Svelte 不使用虚拟 DOM。它的编译器在你写代码时就分析好了:"当 `count` 变化时,需要更新第 3 行的 `<span>` 元素"。运行时直接定位到那个元素去更新,完全不需要对比新旧树。
|
||||
|
||||
这种做法跳过了 Diff 步骤,理论上性能更好。但它依赖编译器的分析能力——编译器需要足够聪明才能正确识别出所有需要更新的地方。
|
||||
|
||||
---
|
||||
|
||||
## 6. 运行时 vs 编译时:框架设计的核心权衡
|
||||
|
||||
### 6.1 两个阶段
|
||||
|
||||
前端代码从你写下到最终在浏览器里运行,会经过两个阶段:
|
||||
|
||||
- **编译时(Compile-time / Build-time)**:你的源代码被构建工具(如 Vite、Webpack)处理,转换成浏览器能直接执行的代码。这个过程发生在你的电脑上,在用户打开网页之前。
|
||||
- **运行时(Runtime)**:转换后的代码在用户的浏览器中执行。框架的核心逻辑(比如虚拟 DOM 的 Diff、响应式的追踪)就在这个阶段工作。
|
||||
|
||||
### 6.2 框架在这两个阶段的工作分配
|
||||
|
||||
不同框架在这两个阶段分配的工作量不同,这决定了它们的性能特征和包体积:
|
||||
|
||||
- **React**:大部分工作在运行时完成。虚拟 DOM 的创建、Diff、Patch 都发生在浏览器中。好处是灵活性高;代价是需要把整个框架的运行时代码(约 40KB)发送给浏览器。
|
||||
- **Vue**:混合方式。模板在编译时被优化(编译器标记出哪些节点是静态的、不会变化的),但最终的界面更新仍然通过运行时的虚拟 DOM 完成。运行时代码约 30KB。
|
||||
- **Svelte**:大部分工作在编译时完成。编译器分析你的代码,直接生成精确的 DOM 更新指令。运行时几乎没有框架代码——最终打包出来只有你自己的业务代码。包体积最小。
|
||||
|
||||
👇 **动手点点看**:
|
||||
点击不同的框架标签,查看它们在"运行时 ↔ 编译时"光谱上的位置,以及各自在打包体积、运行性能、开发体验上的权衡。
|
||||
|
||||
<FrameworkSpectrumDemo />
|
||||
|
||||
### 6.3 行业趋势
|
||||
|
||||
近几年框架的发展方向很明确:**把越来越多的工作从运行时移到编译时**。因为编译时的计算不占用用户的设备资源,不影响页面加载速度。
|
||||
|
||||
- **Vue** 正在开发 Vapor Mode(蒸汽模式),可以跳过虚拟 DOM,在编译时直接生成 DOM 操作代码
|
||||
- **React** 推出了 React Compiler,在编译时自动优化组件的重渲染行为
|
||||
- **Svelte 5** 引入了 Runes 系统,进一步增强编译时的分析能力
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结
|
||||
|
||||
回顾这篇文章的核心要点:
|
||||
|
||||
**前端框架解决的根本问题**:当应用中的数据发生变化时,自动、高效、可靠地更新界面,不需要开发者手动操作 DOM。
|
||||
|
||||
**它们共同遵循的核心思想**:UI = f(State)——界面是数据的函数,开发者只需关注数据的变化,框架负责把数据的变化反映到界面上。
|
||||
|
||||
**它们的关键技术差异**:
|
||||
|
||||
| 技术点 | 含义 |
|
||||
| :--- | :--- |
|
||||
| **响应式系统** | 框架如何检测数据变化。Vue 用 Proxy 拦截、React 用显式 setState、Svelte 用编译器分析。 |
|
||||
| **虚拟 DOM** | Vue 和 React 用一个 JavaScript 对象来模拟 DOM 树,通过对比新旧两棵树(Diff)来找出最小更新量,减少真实 DOM 操作。 |
|
||||
| **组件化** | 把界面拆成独立的、可复用的小块,每个组件管理自己的数据和界面。 |
|
||||
| **编译时优化** | 在代码构建阶段提前做分析和优化,减少运行时的计算量。Svelte 在这方面走得最远。 |
|
||||
|
||||
**一句话**:前端框架的本质工作就是——接管"数据到界面"的同步过程,让开发者只需要思考数据逻辑,不再需要手动操作界面。
|
||||
|
||||
---
|
||||
|
||||
## 名词对照表
|
||||
|
||||
| 英文术语 | 中文对照 | 解释 |
|
||||
| :--- | :--- | :--- |
|
||||
| **Framework** | 框架 | 一套预先编写好的代码和规则,为开发者提供应用的基础结构和常用功能。 |
|
||||
| **DOM** | 文档对象模型 | 浏览器把 HTML 解析后生成的树形数据结构,JavaScript 通过操作它来修改页面。 |
|
||||
| **Virtual DOM** | 虚拟 DOM | 用 JavaScript 对象模拟 DOM 树,通过 Diff 算法找出最小更新路径,减少真实 DOM 操作次数。 |
|
||||
| **State** | 状态 | 应用中的数据,比如用户信息、购物车内容、页面当前状态等。 |
|
||||
| **Reactivity** | 响应式 | 当数据变化时,系统能自动感知并执行对应的界面更新操作。 |
|
||||
| **Proxy** | 代理 | JavaScript 内置机制,可以拦截对一个对象的读取和写入操作。Vue 3 用它来实现响应式。 |
|
||||
| **Component** | 组件 | 一段独立的、可复用的界面代码,包含自己的 HTML 结构、JavaScript 逻辑和 CSS 样式。 |
|
||||
| **Declarative** | 声明式 | 一种编程方式:你描述"最终想要什么结果",由框架来决定怎么实现。 |
|
||||
| **Imperative** | 命令式 | 一种编程方式:你一步一步告诉程序"具体怎么做"。 |
|
||||
| **Diff** | 差异比较 | 对比新旧两棵虚拟 DOM 树,找出哪些节点发生了变化。 |
|
||||
| **Patch** | 打补丁 | 把 Diff 找到的变化部分,应用到真实 DOM 上。 |
|
||||
| **Compile-time** | 编译时 | 代码在构建阶段被处理的时期,发生在用户打开网页之前。 |
|
||||
| **Runtime** | 运行时 | 代码在用户浏览器中执行的时期。 |
|
||||
| **Compiler** | 编译器 | 一个程序,把源代码转换成另一种形式的代码。Svelte 的编译器把 `.svelte` 文件转换成高效的 JavaScript。 |
|
||||
|
||||
@@ -1,551 +1,146 @@
|
||||
# 图形与动画(Canvas / SVG / WebGL)
|
||||
# 图形与动画(Canvas 与他的朋友们)
|
||||
|
||||
::: tip 🎯 核心问题
|
||||
**如何在网页上画图、做动画、甚至开发游戏?** Canvas 提供了一个强大的 2D 绘图能力,让你用代码创造视觉内容。
|
||||
|
||||
以前的网页只能展示干巴巴的文字和图片。但如果你想做打砖块游戏、华丽的动态特效、或是可以自由拖拽的数据报表呢?这就是 **Canvas(画布)** 诞生的原因。
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要学 Canvas?
|
||||
## 1. 什么是 Canvas?
|
||||
|
||||
### 1.1 Canvas 是什么?
|
||||
如果说早期的那些 HTML 标签(如 `<div>`、`<img>`)是用**乐高积木**拼起一个静态的网页,那么 HTML5 的 `<canvas>` 标签就是扔给你一张**巨大的数字白纸**,然后递给你一支靠代码控制的**画笔**,剩下的全交给你自由发挥。
|
||||
|
||||
**Canvas (画布)** 是 HTML5 提供的一个通过 JavaScript 绘制 2D 图形的元素。
|
||||
这里面的画没有任何标签结构,你用画笔涂上去的心血,一旦落笔就变成了最纯粹的**“像素颜料”**。
|
||||
|
||||
你可以把它想象成一张**数字画布**:
|
||||
### 1.1 Canvas vs SVG:两种不同流派的艺术家
|
||||
|
||||
- 🖌️ 你可以用代码"画笔"在上面作画
|
||||
- 🎨 可以画任何东西: 简单的形状、复杂的图表、流畅的动画
|
||||
- 🎮 甚至可以做成完整的游戏
|
||||
在前端画图界,Canvas 有个宿敌叫 **SVG**。它们代表了两种截然不同的绘画观念:
|
||||
|
||||
::: tip 💡 Canvas vs SVG:有什么区别?
|
||||
**Canvas(位图画板):**
|
||||
* **原理**:就像真实在纸上涂色,几笔画上去就变成一团颜料。
|
||||
* **优势**:电脑只管往屏幕上“洒颜料”,性能起飞!能同时画出大几千个活蹦乱跳的闪烁粒子。
|
||||
* **缺点**:画完就没法单独反悔(没法被 DOM 直接选择),而且你用浏览器一旦放大,画面就会马赛克发虚。
|
||||
|
||||
在 Web 开发中,绘制图形主要有两种方式:
|
||||
**SVG(矢量图拼接):**
|
||||
* **原理**:就像在做幻灯片(PPT)。你画一个圆,它就生成一个圆圈的“实体对象”放在画面上。
|
||||
* **优势**:不管被放大成 100 倍还是 10 万倍,永远极其清晰。而且因为每一个形状都是一个独立标签,你可以在任何时候用鼠标点中某个小正方形,命令它换一种颜色。
|
||||
* **缺点**:如果你试图放几万个对象乱飞,繁重的排版引擎会直接把浏览器卡死。
|
||||
|
||||
| 特性 | Canvas | SVG |
|
||||
| -------- | -------------------- | --------------------- |
|
||||
| **类型** | 位图(光栅图形) | 矢量图形 |
|
||||
| **DOM** | 单个 `<canvas>` 元素 | 每个图形都是 DOM 元素 |
|
||||
| **交互** | 需要手动计算碰撞 | 天然支持事件绑定 |
|
||||
| **性能** | 适合大量对象 | 适合少量复杂对象 |
|
||||
| **缩放** | 放大会失真 | 无限缩放不失真 |
|
||||
| **应用** | 游戏、数据可视化 | 图标、插画 |
|
||||
|
||||
**简单总结**:
|
||||
|
||||
- **Canvas** = 像素画,画完就变成像素,性能好但交互麻烦
|
||||
- **SVG** = 矢量图,每个图形都是对象,交互方便但对象多了会慢
|
||||
:::
|
||||
|
||||
### 1.2 Canvas 的应用场景
|
||||
|
||||
Canvas 的用途非常广泛,你可能每天都在用:
|
||||
|
||||
1. **数据可视化**: ECharts、Chart.js 的图表
|
||||
2. **游戏开发**: 网页游戏(如 Phaser.js 引擎)
|
||||
3. **图像处理**: 图片裁剪、滤镜、拼图(如 Fabric.js)
|
||||
4. **创意效果**: 粒子特效、动画背景
|
||||
5. **工程绘图**: CAD、流程图、思维导图
|
||||
**🎮 简单总结:玩动态游戏、做酷炫粒子特效用 Canvas;画精密的 Logo、写交互清晰的小图表用 SVG。**
|
||||
|
||||
---
|
||||
|
||||
## 2. Canvas 基础
|
||||
## 2. 第一笔:用代码找坐标
|
||||
|
||||
### 2.1 Canvas 元素和上下文
|
||||
### 2.1 这张纸的上下怎么颠倒了?
|
||||
|
||||
使用 Canvas 的第一步是在 HTML 中创建一个 `<canvas>` 元素:
|
||||
当你准备下笔时,得先明白 Canvas 里的尺子是反着的。对于传统的数学课坐标系,中心点零点在中间,越往上越大。
|
||||
|
||||
```html
|
||||
<canvas id="myCanvas" width="600" height="400"></canvas>
|
||||
```
|
||||
但在屏幕显示领域,几乎所有设备的“原点(0,0)”都定在**屏幕的最左上角**。向右走 X 轴变大没问题,但是**向下走,Y 轴变大。**
|
||||
|
||||
然后通过 JavaScript 获取**渲染上下文 (Rendering Context)**:
|
||||
👇 **动手点点看**:
|
||||
拖拽下面的这些点,直观地感受一下坐标是如何变化的:
|
||||
|
||||
```javascript
|
||||
const canvas = document.getElementById('myCanvas')
|
||||
const ctx = canvas.getContext('2d') // 获取 2D 上下文
|
||||
```
|
||||
<CoordinateSystemDemo />
|
||||
|
||||
::: tip 💡 关键概念
|
||||
### 2.2 给你的魔法画笔上调料
|
||||
|
||||
- **canvas** 是 DOM 元素,控制画布的大小和位置
|
||||
- **ctx** 是绘图工具,所有的绘制操作都通过它完成
|
||||
- **`"2d"`** 表示使用 2D 渲染上下文(WebGL 使用 `"webgl"`)
|
||||
:::
|
||||
有了坐标,我们就能召唤那支画笔了(在代码里这支笔叫 `Context` 或简称 `ctx`)。
|
||||
|
||||
### 2.2 坐标系统:Canvas 的"地图规则"
|
||||
就像拿着调色盘作画,流程总是固定的三步:
|
||||
1. **调色**:告诉它你需要什么填充色(`fillStyle`)和描边色(`strokeStyle`)
|
||||
2. **构形**:构思你是画一个圈、还是一条直线?
|
||||
3. **下笔**:实打实地去填充(`fill( )`)还是去勾勒边缘(`stroke( )`)
|
||||
|
||||
Canvas 使用的是**屏幕坐标系**,这与传统数学坐标系有所不同:
|
||||
👇 **动手点点看**:
|
||||
试试把下面代码面板里的形状颜色换换:
|
||||
|
||||
- **原点 (0, 0)**: 在**左上角**(不是中心)
|
||||
- **X 轴**: 向右为正方向
|
||||
- **Y 轴**: **向下**为正方向(注意: 数学坐标系中 Y 轴向上)
|
||||
- **单位**: 像素 (px)
|
||||
|
||||
```javascript
|
||||
// 在左上角绘制一个矩形
|
||||
ctx.fillRect(0, 0, 10, 10)
|
||||
|
||||
// 在右下角绘制一个矩形
|
||||
ctx.fillRect(canvas.width - 10, canvas.height - 10, 10, 10)
|
||||
```
|
||||
|
||||
::: tip 💡 记忆技巧
|
||||
|
||||
想象你在看**屏幕**:
|
||||
|
||||
- 向右移 → X 增加 ✅
|
||||
- 向下移(滚动页面) → Y 增加 ✅
|
||||
- 向左移 → X 减少
|
||||
- 向上移(向上滚动) → Y 减少
|
||||
|
||||
这就是 Canvas 的坐标规则。
|
||||
:::
|
||||
|
||||
### 2.3 绘制基本形状
|
||||
|
||||
Canvas 提供了几种绘制基本形状的方法:
|
||||
|
||||
**矩形**:
|
||||
|
||||
```javascript
|
||||
// 填充矩形
|
||||
ctx.fillStyle = '#3498db'
|
||||
ctx.fillRect(x, y, width, height)
|
||||
|
||||
// 描边矩形
|
||||
ctx.strokeStyle = '#2c3e50'
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeRect(x, y, width, height)
|
||||
|
||||
// 清除矩形区域
|
||||
ctx.clearRect(x, y, width, height)
|
||||
```
|
||||
|
||||
**圆形**:
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, radius, startAngle, endAngle)
|
||||
ctx.fill() // 或 ctx.stroke()
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
- **x, y**: 圆心坐标
|
||||
- **radius**: 半径
|
||||
- **startAngle, endAngle**: 起始和结束角度(弧度制)
|
||||
- `0` = 3 点钟方向
|
||||
- `Math.PI / 2` = 6 点钟方向
|
||||
- `Math.PI` = 9 点钟方向
|
||||
|
||||
**线条**:
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x1, y1) // 起点
|
||||
ctx.lineTo(x2, y2) // 终点
|
||||
ctx.stroke()
|
||||
```
|
||||
|
||||
### 2.4 颜色和样式
|
||||
|
||||
Canvas 支持多种颜色设置方式:
|
||||
|
||||
```javascript
|
||||
// 纯色
|
||||
ctx.fillStyle = '#3498db' // 十六进制
|
||||
ctx.fillStyle = 'rgb(52, 152, 219)' // RGB
|
||||
ctx.fillStyle = 'rgba(52, 152, 219, 0.5)' // RGBA(带透明度)
|
||||
|
||||
// 线性渐变
|
||||
const gradient = ctx.createLinearGradient(x1, y1, x2, y2)
|
||||
gradient.addColorStop(0, '#3498db')
|
||||
gradient.addColorStop(1, '#e74c3c')
|
||||
ctx.fillStyle = gradient
|
||||
|
||||
// 径向渐变
|
||||
const radialGradient = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2)
|
||||
radialGradient.addColorStop(0, '#3498db')
|
||||
radialGradient.addColorStop(1, 'transparent')
|
||||
ctx.fillStyle = radialGradient
|
||||
```
|
||||
<CanvasBasicsDemo />
|
||||
|
||||
---
|
||||
|
||||
## 3. 路径:Canvas 的"笔画"
|
||||
## 3. 翻页动画书:如何让画面动起来极度丝滑
|
||||
|
||||
### 3.1 什么是路径?
|
||||
我们刚才说过,Canvas 一旦你填上了颜色,这就变成了永久的马赛克。你怎么可能让马赛克奔跑呢?
|
||||
|
||||
**路径 (Path)** 是 Canvas 中的核心概念。你可以把它想象成用笔画线的过程:
|
||||
**答案是“骗过你的眼睛”。这和翻页手翻书或者电影胶片的原理一模一样。**
|
||||
|
||||
1. **`beginPath()`** - 开始新路径(拿起笔)
|
||||
2. **`moveTo()`** - 移动到起点(不画线)
|
||||
3. **`lineTo()` / `arc()`** - 绘制线条或曲线
|
||||
4. **`closePath()`** - 闭合路径(可选)
|
||||
5. **`fill()` / `stroke()`** - 填充或描边
|
||||
如果你想让一个球飞起来:
|
||||
1. **擦黑板**:用 `clearRect` 把这整块画布上的内容毫不留情地清空!
|
||||
2. **挪位置**:让那个球的 X 坐标往前偷偷加 2 毫米。
|
||||
3. **下笔重画**:把球在新的位置重新画一次。
|
||||
4. **疯狂循环**:浏览器内置了一个极其精准的神仙秒表叫 `requestAnimationFrame`。它会以每秒 60 次(即 60 FPS)的变态速度,重复着【擦除 -> 移动 -> 重绘】。由于人眼自带“视觉残留”,你在屏幕上看到的,不仅不是黑板被擦,反而是如同丝绸般顺滑的动画。
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(100, 100) // 移动到起点
|
||||
ctx.lineTo(200, 100) // 画横线
|
||||
ctx.lineTo(150, 150) // 画斜线
|
||||
ctx.closePath() // 闭合路径(回到起点)
|
||||
ctx.fill() // 填充
|
||||
```
|
||||
👇 **动手点点看**:
|
||||
尝试添加或者减少物体的数量,感受每秒 60 帧带来的无缝快感:
|
||||
|
||||
### 3.2 绘制复杂形状
|
||||
|
||||
通过组合路径,可以绘制任意复杂的形状。
|
||||
|
||||
**三角形**:
|
||||
|
||||
```javascript
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(100, 50)
|
||||
ctx.lineTo(150, 150)
|
||||
ctx.lineTo(50, 150)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = '#e74c3c'
|
||||
ctx.fill()
|
||||
```
|
||||
<AnimationLoopDemo />
|
||||
|
||||
---
|
||||
|
||||
## 4. 动画基础
|
||||
## 4. 瞎子摸象:我在 Canvas 里面怎么点击?
|
||||
|
||||
### 4.1 动画循环
|
||||
因为 Canvas 画布就只是一张没有任何结构的“颜料布”。假设你在这个布上画了一只哥布林:
|
||||
|
||||
在 Canvas 中创建动画,核心是使用 **`requestAnimationFrame`** 方法。
|
||||
如果你想写个代码:“当玩家点中了哥布林,哥布林阵亡”。你根本没法像写普通网页那样通过 `getElementById` 去直接绑定这个外星怪物。因为在浏览器的眼里,**这里永远没有任何怪兽,只有一块宽 600 高 400 的 `<canvas>` 标签死死挡在这里**。
|
||||
|
||||
```javascript
|
||||
function animate() {
|
||||
// 1. 清除画布(或绘制半透明背景产生拖尾效果)
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
那我们要怎么做事件交互呢?
|
||||
1. **监听布面被点**:先获取你目前鼠标点在这个死板的 HTML 大布的哪个具体的 XY 位置。
|
||||
2. **拿账本去对**:然后你必须自己翻你的代码记录,“我记得刚刚我在(100,100)的位置画了一个半径 50 的哥布林”。
|
||||
3. **勾股定理**:我们用初中教的勾股定理公式去疯狂计算——当前鼠标点击的位置,是不是落在了那个(100,100)距离 50 半径的圆内?。
|
||||
|
||||
// 2. 更新状态
|
||||
update()
|
||||
恭喜你!这种疯狂算几何数学距离的方法就是你在各大 3A 游戏里听过的 **“碰撞检测 (Collision Detection)”**
|
||||
|
||||
// 3. 绘制
|
||||
draw()
|
||||
👇 **动手点点看**:
|
||||
打开最下面的“Hover 悬停模式”,你就能看到它内部拼命去算距离有多累了。
|
||||
|
||||
// 4. 请求下一帧
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
// 启动动画
|
||||
animate()
|
||||
```
|
||||
|
||||
::: tip 💡 为什么用 requestAnimationFrame 而不是 setInterval?
|
||||
|
||||
- ✅ 自动优化,通常为 60FPS(每秒 60 帧)
|
||||
- ✅ 页面不可见时自动暂停,节省资源
|
||||
- ✅ 与浏览器刷新周期同步,避免画面撕裂
|
||||
:::
|
||||
|
||||
### 4.2 动画的本质
|
||||
|
||||
动画的本质是**快速连续绘制静态画面**。每帧需要:
|
||||
|
||||
1. **清除旧画面**: `ctx.clearRect()` 或用半透明背景覆盖
|
||||
2. **更新状态**: 计算新位置、新角度等
|
||||
3. **绘制新画面**: 重新绘制所有对象
|
||||
|
||||
```javascript
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 半透明背景(产生拖尾效果)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
```
|
||||
<EventHandlingDemo />
|
||||
|
||||
---
|
||||
|
||||
## 5. 事件处理
|
||||
## 5. 解放算力:粒子系统与视觉魔法
|
||||
|
||||
Canvas 只是一个 DOM 元素,不像 SVG 那样每个图形都是独立的 DOM 元素。因此,我们需要**手动处理交互事件**。
|
||||
到了这一步,当你把【坐标不断重绘的动画】跟【颜色和大小变换】融合,再放进成百上千个小碎片里。这就是引爆视觉的终极杀器:**粒子系统**。
|
||||
|
||||
### 5.1 鼠标事件
|
||||
你只需要建立一个巨大的数组,里面塞满了几百个拥有独立生命值、独立初始随机速度的数字对象。每次“重绘”,让所有的点根据重力或者惯性去减速。你的浏览器里马上就能发生逼真的大爆炸或者漫天飞雪。
|
||||
|
||||
```javascript
|
||||
canvas.addEventListener('click', (e) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
👇 **动手点点看**:
|
||||
试试“烟花”和“鼠标轨迹”!
|
||||
|
||||
console.log(`Clicked at (${x}, ${y})`)
|
||||
})
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
// 检测是否悬停在某个对象上
|
||||
objects.forEach((obj) => {
|
||||
const dist = Math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
|
||||
if (dist < obj.radius) {
|
||||
canvas.style.cursor = 'pointer'
|
||||
obj.hovered = true
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 5.2 拖拽实现
|
||||
|
||||
```javascript
|
||||
let isDragging = false
|
||||
let selectedObject = null
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
const { x, y } = getMousePos(e)
|
||||
|
||||
objects.forEach((obj) => {
|
||||
const dist = Math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
|
||||
if (dist < obj.radius) {
|
||||
isDragging = true
|
||||
selectedObject = obj
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (isDragging && selectedObject) {
|
||||
const { x, y } = getMousePos(e)
|
||||
selectedObject.x = x
|
||||
selectedObject.y = y
|
||||
draw() // 重绘
|
||||
}
|
||||
})
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
isDragging = false
|
||||
selectedObject = null
|
||||
})
|
||||
```
|
||||
<ParticleSystemDemo />
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能优化
|
||||
## 6. 守护 FPS 荣耀:如何应对高烧的 CPU?
|
||||
|
||||
随着绘制的对象增多,Canvas 性能会下降。以下是一些常用的优化技巧:
|
||||
让成千上万个对象在一秒内计算重画 60 遍,这是极其消耗电脑算力(CPU 和内存)的。
|
||||
很多野生小白刚做出来的游戏玩了两分钟可能风扇就起飞了。下面是真正的引擎大佬使用的降温护体绝技:
|
||||
|
||||
### 6.1 离屏 Canvas (Offscreen Canvas)
|
||||
1. **局部擦黑板(脏矩形 Dirty Rect)!** 一个角色在一望无际的草原上奔跑。你千万别每帧把整块大草原都擦了重画!角色经过哪一小块,你就用小板擦把哪里擦掉然后只补哪里的洞,这能省下几千倍的力气。
|
||||
2. **隐藏后台魔法(离屏 Canvas)!** 如果游戏背景是繁星漫天、有各种复杂绚丽的山脉。最好先偷偷在没人的后台建一个内存 Canvas 把它一次性精美地画上去。以后每秒 60 下的刷新,你直接把这幅“定格全图”通过贴图的方式贴到前端(`drawImage`)就行了。
|
||||
3. **批量洗画笔!** 如果画画时你要反复交替使用“红、蓝、红、蓝、红”这几种笔,频繁切换。可以提前把所有红色的兵全归档画完,再清空换蓝颜料画,省去了昂贵的上下文来回切换。
|
||||
|
||||
预渲染静态内容到离屏 Canvas,减少每帧的绘制操作:
|
||||
👇 **动手点点看**:
|
||||
先把对象数量拉满,看着网页快掉进卡顿的深渊,再依次打开右下方的绝技进行抢救。
|
||||
|
||||
```javascript
|
||||
// 创建离屏 Canvas
|
||||
const offscreenCanvas = document.createElement('canvas')
|
||||
const offscreenCtx = offscreenCanvas.getContext('2d')
|
||||
offscreenCanvas.width = 600
|
||||
offscreenCanvas.height = 400
|
||||
|
||||
// 预渲染背景
|
||||
function drawBackground(ctx) {
|
||||
ctx.fillStyle = '#f0f0f0'
|
||||
ctx.fillRect(0, 0, 600, 400)
|
||||
}
|
||||
drawBackground(offscreenCtx)
|
||||
|
||||
// 主渲染循环
|
||||
function draw() {
|
||||
// 直接复制预渲染的背景
|
||||
ctx.drawImage(offscreenCanvas, 0, 0)
|
||||
|
||||
// 只绘制动态对象
|
||||
objects.forEach((obj) => obj.draw(ctx))
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 减少重绘(脏矩形优化)
|
||||
|
||||
只重绘变化的部分:
|
||||
|
||||
```javascript
|
||||
function draw() {
|
||||
objects.forEach((obj) => {
|
||||
if (obj.moved) {
|
||||
// 清除旧位置
|
||||
ctx.clearRect(
|
||||
obj.oldX - obj.size,
|
||||
obj.oldY - obj.size,
|
||||
obj.size * 2,
|
||||
obj.size * 2
|
||||
)
|
||||
|
||||
// 绘制新位置
|
||||
obj.draw(ctx)
|
||||
|
||||
obj.moved = false
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 批量渲染
|
||||
|
||||
减少状态切换(fillStyle、strokeStyle 等):
|
||||
|
||||
```javascript
|
||||
// 按颜色分组
|
||||
const batches = {}
|
||||
objects.forEach((obj) => {
|
||||
if (!batches[obj.color]) {
|
||||
batches[obj.color] = []
|
||||
}
|
||||
batches[obj.color].push(obj)
|
||||
})
|
||||
|
||||
// 批量绘制相同颜色的对象
|
||||
Object.keys(batches).forEach((color) => {
|
||||
ctx.fillStyle = color // 只设置一次颜色
|
||||
batches[color].forEach((obj) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
})
|
||||
})
|
||||
```
|
||||
<PerformanceDemo />
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见库与框架
|
||||
## 7. 名词对照表
|
||||
|
||||
虽然原生 Canvas 已经很强大,但在实际项目中,使用成熟的库可以大大提高开发效率。
|
||||
|
||||
### 7.1 Fabric.js
|
||||
|
||||
**特点**: 对象模型,支持交互
|
||||
|
||||
```javascript
|
||||
const canvas = new fabric.Canvas('c')
|
||||
|
||||
// 创建圆形
|
||||
const circle = new fabric.Circle({
|
||||
radius: 20,
|
||||
fill: '#3498db',
|
||||
left: 100,
|
||||
top: 100
|
||||
})
|
||||
|
||||
canvas.add(circle)
|
||||
|
||||
// 自动处理事件
|
||||
circle.on('click', () => {
|
||||
circle.set('fill', '#e74c3c')
|
||||
canvas.renderAll()
|
||||
})
|
||||
```
|
||||
|
||||
**适用场景**: 图片编辑器、白板工具、图形设计工具
|
||||
|
||||
### 7.2 PixiJS (WebGL)
|
||||
|
||||
**特点**: WebGL 加速,超高性能
|
||||
|
||||
```javascript
|
||||
const app = new PIXI.Application({
|
||||
width: 600,
|
||||
height: 400,
|
||||
backgroundColor: 0x1099bb
|
||||
})
|
||||
document.body.appendChild(app.view)
|
||||
|
||||
const graphics = new PIXI.Graphics()
|
||||
graphics.beginFill(0x3498db)
|
||||
graphics.drawCircle(300, 200, 50)
|
||||
graphics.endFill()
|
||||
app.stage.addChild(graphics)
|
||||
```
|
||||
|
||||
**适用场景**: 大型游戏、粒子系统、大量对象的场景
|
||||
| 术语 | 解释 |
|
||||
| --- | --- |
|
||||
| **Canvas** | Html5 提供的 2D 画布。绘制极快,但画完就变成颜料像素,不支持通过 DOM 操作内容。 |
|
||||
| **SVG** | 矢量图,放大永远不模糊,且每个图形都是独立的标签元素可以单独点击绑定事件。 |
|
||||
| **Context (ctx)** | 获取到的“2D 上下文”,可以理解为用来在这张布上调各种颜色、干各种特殊效果的“画笔”。 |
|
||||
| **requestAnimationFrame** | 浏览器内置的神级节拍器,会以显示器的刷新率(通常 60FPS)不断狂飙执行,专门用来做完美动画。 |
|
||||
| **FPS / Frame Rate** | 帧率。60 FPS 代表一秒钟内浏览器帮我们默默擦除了 60 次黑板并画了 60 副新图,这骗过了视神经,看起来极其丝滑。 |
|
||||
| **Dirty Rect / 脏矩形** | 只在画面中发生变化的微小矩形区域内进行擦除和重绘,强力保留性能。 |
|
||||
| **Offscreen Canvas** | 藏在内存里的“影子画布”,把静态且复杂的树木和山脉先画好,当作死的一张贴图重复利用。 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结与最佳实践
|
||||
|
||||
### 8.1 核心要点回顾
|
||||
|
||||
1. **Canvas 是位图画布**: 绘制后就是像素,无法直接修改已有内容
|
||||
2. **坐标系统**: 原点在左上角,Y 轴向下为正
|
||||
3. **路径系统**: beginPath → moveTo → lineTo → fill/stroke
|
||||
4. **动画原理**: 清除 → 更新 → 绘制 → requestAnimationFrame
|
||||
5. **事件处理**: 需要手动计算碰撞
|
||||
6. **性能优化**: 离屏 Canvas、脏矩形、批量渲染
|
||||
|
||||
### 8.2 最佳实践
|
||||
|
||||
**代码组织**:
|
||||
|
||||
```javascript
|
||||
// 使用类封装对象
|
||||
class GameObject {
|
||||
constructor(x, y) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
}
|
||||
|
||||
update() {
|
||||
// 更新状态
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
// 绘制
|
||||
}
|
||||
|
||||
isHit(x, y) {
|
||||
// 碰撞检测
|
||||
const dist = Math.sqrt((x - this.x) ** 2 + (y - this.y) ** 2)
|
||||
return dist < this.radius
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**性能优化清单**:
|
||||
|
||||
- ✅ 使用 `requestAnimationFrame` 而不是 `setInterval`
|
||||
- ✅ 减少状态切换(按颜色分组绘制)
|
||||
- ✅ 使用离屏 Canvas 预渲染静态内容
|
||||
- ✅ 只重绘变化的部分(脏矩形)
|
||||
- ✅ 限制对象数量,使用对象池
|
||||
- ✅ 避免 `save()` 和 `restore()` 的频繁调用
|
||||
|
||||
---
|
||||
|
||||
## 9. 名词速查表 (Glossary)
|
||||
|
||||
| 名词 | 解释 |
|
||||
| ------------------------- | ----------------------------------------------------------------------- |
|
||||
| **Context / 上下文** | Canvas 的渲染环境,通过 `getContext("2d")` 获取,所有绘制操作都通过它完成 |
|
||||
| **Path / 路径** | 由一系列点连接成的轨迹,是 Canvas 绘图的基础 |
|
||||
| **Stroke / 描边** | 绘制路径的轮廓线 |
|
||||
| **Fill / 填充** | 用颜色填充路径内部 |
|
||||
| **requestAnimationFrame** | 浏览器提供的动画 API,在每次重绘前调用回调函数 |
|
||||
| **Offscreen Canvas** | 离屏 Canvas,用于预渲染静态内容以提高性能 |
|
||||
| **Dirty Rect** | 脏矩形优化,只重绘变化的部分 |
|
||||
| **Collision Detection** | 碰撞检测,判断鼠标或对象是否点击了某个图形 |
|
||||
| **Raster vs Vector** | 位图 vs 矢量图,Canvas 是位图,SVG 是矢量图 |
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
现在你已经掌握了 Canvas 2D 的核心概念:
|
||||
|
||||
- **基本绘图**: 矩形、圆形、线条
|
||||
- **样式控制**: 颜色、渐变、阴影
|
||||
- **动画制作**: requestAnimationFrame + 清除重绘
|
||||
- **交互处理**: 鼠标事件、碰撞检测
|
||||
- **性能优化**: 离屏 Canvas、批量渲染
|
||||
|
||||
**下一步建议**:
|
||||
|
||||
- 如果你想深入学习动画,可以尝试制作一个**贪吃蛇游戏**或**打砖块游戏**
|
||||
- 如果你对数据可视化感兴趣,可以学习 **ECharts** 或 **D3.js**
|
||||
- 如果你想做游戏开发,可以尝试 **Phaser.js** 游戏引擎
|
||||
- 如果你对 WebGL 感兴趣,可以学习 **Three.js** 或 **PixiJS**
|
||||
|
||||
祝你学习愉快! 🎨
|
||||
现在,不管是一把简单的魔法画笔、还是由万千雪花组成的宏大粒子系统,整个能够不断刷新重绘的数字世界引擎,都在你的掌控之中了!
|
||||
|
||||
@@ -64,11 +64,6 @@
|
||||
title="命令行与 Shell 脚本"
|
||||
description="终端操作、Shell 命令、脚本自动化"
|
||||
/>
|
||||
<NavCard
|
||||
href="/zh-cn/appendix/2-development-tools/editors-and-ai"
|
||||
title="编辑器与 AI 编程助手"
|
||||
description="AI 时代的编辑器使用方式与效率提升技巧"
|
||||
/>
|
||||
<NavCard
|
||||
href="/zh-cn/appendix/2-development-tools/git-version-control"
|
||||
title="Git:代码的时光机"
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|------|------|----------|----------|
|
||||
| **腾讯云 CloudBase** | 国内访问速度快,与微信生态深度整合 | 国内用户为主、需要微信小程序支持的项目 | 有免费额度 |
|
||||
| **Vercel** | 前端框架支持好,与 GitHub 集成紧密 | React/Vue/Next.js 等现代前端项目 | 有免费额度 |
|
||||
| **Netlify** | 功能全面,支持表单处理和身份验证,与 Git 集成好 | 需要表单处理、身份验证等高级功能的静态网站 | 有免费额度 |
|
||||
| **Zeabur** | 支持多种语言和服务模板,配置灵活 | 需要部署多种服务(如 Dify、n8n)的复杂项目 | 每月约 5 美元免费额度 |
|
||||
|
||||
---
|
||||
@@ -162,7 +163,126 @@ Vercel 会自动识别项目类型并配置构建命令:
|
||||
|
||||
---
|
||||
|
||||
# 3. Zeabur
|
||||
# 3. Netlify
|
||||
|
||||
Netlify 是另一个非常流行的前端部署平台,与 Vercel 类似,特别适合部署静态网站和单页应用(SPA)。它的特点包括:
|
||||
|
||||
- **功能全面**:除了静态网站托管,还支持表单处理、身份验证、边缘函数等高级功能
|
||||
- **与 Git 深度集成**:支持 GitHub、GitLab、Bitbucket,推送代码自动部署
|
||||
- **分支预览**:每个分支都会自动生成独立的预览链接
|
||||
- **全球 CDN**:网站自动分发到全球节点,访问速度快
|
||||
- **表单处理**:无需后端代码即可处理网站表单提交
|
||||
- **身份验证**:内置用户身份验证功能,可快速实现登录/注册
|
||||
|
||||
> ⚠️ **注意**:Netlify 的国内访问速度可能不如 CloudBase,建议主要面向海外用户的项目使用。
|
||||
|
||||
## 使用 Netlify 部署 Web 应用
|
||||
|
||||
### 步骤 1:注册账号
|
||||
|
||||
访问 [Netlify 官网](https://www.netlify.com),点击 "Sign up" 注册。你可以使用 GitHub、GitLab、Bitbucket 或邮箱注册。
|
||||
|
||||
### 步骤 2:导入项目
|
||||
|
||||
1. 登录后点击 "Add new site" → "Import an existing project"
|
||||
2. 选择你的代码托管平台(如 GitHub)
|
||||
3. 授权 Netlify 访问你的仓库
|
||||
4. 从列表中选择你要部署的仓库
|
||||
|
||||
### 步骤 3:配置构建设置
|
||||
|
||||
Netlify 会自动识别常见的前端框架并配置构建设置:
|
||||
|
||||
| 框架 | 构建命令 | 发布目录 |
|
||||
|------|----------|----------|
|
||||
| React | `npm run build` | `build` |
|
||||
| Vue | `npm run build` | `dist` |
|
||||
| Angular | `ng build` | `dist/<project-name>` |
|
||||
| Next.js | `next build` | `out` |
|
||||
| 纯 HTML | - | `.`(项目根目录) |
|
||||
|
||||
如果自动识别不正确,可以手动配置:
|
||||
- **Build command**: 构建命令,如 `npm run build`
|
||||
- **Publish directory**: 构建输出目录,如 `dist` 或 `build`
|
||||
|
||||
### 步骤 4:部署
|
||||
|
||||
点击 "Deploy site" 按钮,等待构建完成。构建成功后,你会获得一个 `xxx.netlify.app` 的域名,任何人都可以通过这个地址访问你的网站。
|
||||
|
||||
### 步骤 5:配置自定义域名(可选)
|
||||
|
||||
1. 进入站点设置,点击 "Domain management"
|
||||
2. 点击 "Add custom domain"
|
||||
3. 输入你的域名并按照提示配置 DNS 记录
|
||||
4. Netlify 会自动申请并配置 HTTPS 证书
|
||||
|
||||
### 特色功能
|
||||
|
||||
#### 1. 表单处理
|
||||
|
||||
Netlify 提供了一个非常方便的功能:无需后端代码即可处理表单提交。
|
||||
|
||||
只需在 HTML 表单中添加 `netlify` 属性:
|
||||
|
||||
```html
|
||||
<form name="contact" netlify>
|
||||
<p>
|
||||
<label>姓名: <input type="text" name="name" /></label>
|
||||
</p>
|
||||
<p>
|
||||
<label>邮箱: <input type="email" name="email" /></label>
|
||||
</p>
|
||||
<p>
|
||||
<label>留言: <textarea name="message"></textarea></label>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">发送</button>
|
||||
</p>
|
||||
</form>
|
||||
```
|
||||
|
||||
部署后,表单提交的数据会自动发送到 Netlify 后台,你可以在 "Forms" 页面查看所有提交记录,也可以设置邮件通知或将数据转发到其他服务。
|
||||
|
||||
#### 2. Netlify Functions(边缘函数)
|
||||
|
||||
Netlify 支持部署无服务器函数(Serverless Functions),让你可以在不搭建完整后端服务器的情况下,实现简单的 API 接口。你可以使用 JavaScript 或 TypeScript 编写函数,部署后会自动获得一个可访问的 URL。
|
||||
|
||||
例如,创建一个 `hello.js` 文件:
|
||||
|
||||
```javascript
|
||||
exports.handler = async (event, context) => {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({ message: "Hello from Netlify!" })
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
部署后,你可以通过 `https://你的域名/.netlify/functions/hello` 访问这个函数。
|
||||
|
||||
#### 3. 本地开发支持
|
||||
|
||||
Netlify 提供了 CLI 工具,方便你在本地开发和测试:
|
||||
|
||||
```bash
|
||||
# 安装 Netlify CLI
|
||||
npm install -g netlify-cli
|
||||
|
||||
# 登录账号
|
||||
netlify login
|
||||
|
||||
# 本地启动开发服务器
|
||||
netlify dev
|
||||
|
||||
# 本地测试函数
|
||||
netlify functions:serve
|
||||
```
|
||||
|
||||
使用 CLI 工具可以在本地模拟 Netlify 环境,包括表单提交、函数调用等功能,方便在部署前进行测试。
|
||||
|
||||
---
|
||||
|
||||
# 4. Zeabur
|
||||
|
||||
Zeabur 是一个新兴的部署平台,特别适合需要部署多种服务的复杂项目。它的优势在于:
|
||||
|
||||
@@ -354,15 +474,17 @@ Zeabur 是一个新兴的部署平台,特别适合需要部署多种服务的
|
||||
|
||||
# 总结
|
||||
|
||||
在本教程中,我们介绍了三个常用的 Web 应用部署平台:
|
||||
在本教程中,我们介绍了四个常用的 Web 应用部署平台:
|
||||
|
||||
1. **腾讯云 CloudBase**:适合国内用户,访问速度快,与微信生态整合好
|
||||
2. **Vercel**:适合现代前端框架项目,与 GitHub 集成紧密,全球 CDN 加速
|
||||
3. **Zeabur**:适合复杂项目,服务模板丰富,支持多种部署方式
|
||||
3. **Netlify**:功能全面,支持表单处理和身份验证,适合需要高级功能的静态网站
|
||||
4. **Zeabur**:适合复杂项目,服务模板丰富,支持多种部署方式
|
||||
|
||||
选择哪个平台取决于你的具体需求:
|
||||
- 如果主要面向国内用户,推荐 **CloudBase**
|
||||
- 如果使用 React/Next.js 等框架,推荐 **Vercel**
|
||||
- 如果使用 React/Next.js 等框架,推荐 **Vercel** 或 **Netlify**
|
||||
- 如果需要表单处理、身份验证等高级功能,推荐 **Netlify**
|
||||
- 如果需要部署 Dify、n8n 等服务,推荐 **Zeabur**
|
||||
|
||||
无论选择哪个平台,部署的核心流程都是相似的:准备代码 → 选择平台 → 配置构建设置 → 部署上线。掌握这些技能后,你就可以将自己开发的应用分享给全世界了!
|
||||
|
||||
Reference in New Issue
Block a user