Git团队协作:让代码审查成为日常

Git团队协作:让代码审查成为日常

前两期我们掌握了Git的基础操作和分支管理。但Git的真正威力在于协作——让一群人同时开发,而不会互相踩脚。

这一期,我们来聊聊团队协作的最佳实践。

Fork vs Clone

Clone:直接克隆仓库,有写权限

git clone git@github.com:myteam/project.git

适用于团队成员,对仓库有直接写权限。

Fork:在你的账号下创建仓库副本

  1. 在GitHub/GitLab页面点击Fork
  2. 克隆你fork的仓库
    git clone git@github.com:yourname/project.git

    适用于开源贡献者,对原仓库没有写权限。

Pull Request工作流

Pull Request(PR)是GitHub的叫法,GitLab叫Merge Request(MR)。本质相同:请求把你的分支合并到目标分支。

流程

graph TB
    A[从main创建feature分支] --> B[开发并提交]
    B --> C[推送到远程]
    C --> D[在GitHub创建PR]
    D --> E[代码审查]
    E --> F{通过?}
    F -->|是| G[合并PR]
    F -->|否| H[修改代码]
    H --> B
    G --> I[删除feature分支]

实战示例

# 1. 确保main是最新的
git checkout main
git pull origin main

# 2. 创建功能分支
git checkout -b feature/add-dark-mode

# 3. 开发...
git add .
git commit -m "feat: 添加暗黑模式支持"

# 4. 推送到远程
git push -u origin feature/add-dark-mode

# 5. 去GitHub/GitLab页面创建PR

创建PR时,填写:

  • 标题:简洁描述改动(如"feat: 添加暗黑模式支持")
  • 描述:详细说明改了什么、为什么改、怎么测试
  • 审查者:指定审查代码的人
  • 标签:如enhancement、bug、documentation

好的PR长什么样?

标题格式(约定式提交):

  • feat::新功能
  • fix::bug修复
  • docs::文档更新
  • refactor::重构
  • test::测试
  • chore::杂务(依赖更新、配置等)

描述模板

## 改动描述
添加了暗黑模式支持,用户可以在设置中切换主题。

## 改动原因
用户反馈夜间使用时太刺眼,需要暗黑模式。

## 如何测试
1. 登录系统
2. 进入设置页面
3. 点击"切换主题"
4. 验证页面颜色变化

## 截图
![暗黑模式效果](screenshot.png)

## 相关Issue
Closes #123

一个PR只做一件事

  • 不要把多个不相关的改动塞进一个PR
  • 小PR更容易审查,更快合并
  • 理想大小:200-400行代码

代码审查

代码审查(Code Review)是团队协作的核心。它不仅能发现bug,还能传播知识、统一风格。

审查者看什么?

  1. 正确性:代码是否实现了需求?
  2. 可读性:变量名、函数名是否清晰?逻辑是否易懂?
  3. 性能:有没有明显的性能问题?
  4. 安全:有没有SQL注入、XSS等安全隐患?
  5. 测试:有没有足够的测试覆盖?
  6. 风格:是否符合团队的编码规范?

审查评论类型

必须修改(Blocking)

🛑 这里有个bug,变量未定义就会使用

建议修改(Non-blocking)

💡 建议:这里用Array.filter会更简洁

提问

❓ 为什么要用递归?迭代是不是更合适?

称赞

👍 这个抽象做得很好!

作为被审查者

  1. 不要抵触批评:审查的是代码,不是你这个人
  2. 解释而非辩解:解释为什么这样写,而不是为错误找借口
  3. 及时响应:不要让PR挂着一周不动
  4. 自己先审查一遍:push前自己diff一下,避免低级错误
# 提交前自查
git diff main...HEAD  # 查看当前分支的所有改动

解决PR冲突

PR期间main可能有了新提交,导致你的PR冲突:

# 方法1:merge
git checkout feature/add-dark-mode
git fetch origin
git merge origin/main
# 解决冲突...
git push

# 方法2:rebase(推荐,保持线性历史)
git checkout feature/add-dark-mode
git fetch origin
git rebase origin/main
# 解决冲突...
git push --force-with-lease  # 注意:需要force push

注意--force-with-lease--force更安全,它会在远程有新提交时拒绝push。

Code Owner

大项目可以配置CODEOWNERS文件,自动指定审查者:

# .github/CODEOWNERS

# 前端代码由前端组审查
/frontend/ @myteam/frontend

# API代码由后端组审查
/api/ @myteam/backend

# 安全相关由安全组审查
**/auth* @myteam/security

保护分支

防止直接push到main分支,强制走PR流程:

GitHub设置:

  1. Settings → Branches → Add rule
  2. Branch name pattern: main
  3. 勾选:
    • Require a pull request before merging
    • Require approvals(需要几个人审批)
    • Require status checks to pass before merging(CI必须通过)

这样,任何人(包括管理员)都不能直接push到main,必须通过PR。

CI/CD集成

PR可以触发自动测试,只有测试通过才能合并:

# .github/workflows/test.yml
name: Test

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
      - run: npm run lint

这样每次PR都会自动跑测试,测试失败就无法合并。

多人协作常见问题

问题1:Push被拒绝

! [rejected] main -> main (non-fast-forward)

原因:远程有新提交,你本地没有。

解决

git pull --rebase origin main
git push origin main

问题2:别人的分支怎么更新?

# 获取所有远程分支
git fetch --all

# 查看同事的分支
git branch -r

# 基于同事的分支创建本地分支
git checkout -b feature-xxx origin/feature-xxx

问题3:如何同步Fork?

# 1. 添加上游仓库
git remote add upstream git@github.com:original-owner/project.git

# 2. 获取上游更新
git fetch upstream

# 3. 合并到本地main
git checkout main
git merge upstream/main

# 4. 推送到你的fork
git push origin main

问题4:不小心push了敏感信息!

# 立即删除敏感文件
git rm --cached config/secrets.yml
git commit -m "chore: 移除敏感文件"
git push

# 历史中还有!需要彻底清除
# 使用BFG Repo-Cleaner(推荐)
bfg --delete-files secrets.yml
git push --force

# 或用git filter-branch(慢)
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch config/secrets.yml' \
  --prune-empty --tag-name-filter cat -- --all
git push --force

重要:改写历史后,所有协作者需要重新clone仓库!

协作规范

Git提交规范

使用约定式提交(Conventional Commits):

(): 

<footer>

示例:

feat(auth): 添加OAuth2.0登录支持

- 支持Google登录
- 支持GitHub登录
- 添加登录状态持久化

Closes #456

好处:

  • 自动生成changelog
  • 自动决定版本号(feat是minor,fix是patch)
  • 清晰的历史记录

提交信息模板

配置提交模板:

git config commit.template ~/.gitmessage

~/.gitmessage内容:

# :  (50 chars)
# |- - - - - - - - - - - - - - - - - - - - - ->|

#  (72 chars)
# |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ->|

# Type:
#   feat     新功能
#   fix      Bug修复
#   docs     文档
#   style    格式(不影响代码运行)
#   refactor 重构
#   test     测试
#   chore    构建/工具

分支保护策略

graph LR
    A[feature分支] -->|PR| B[develop分支]
    C[bugfix分支] -->|PR| B
    B -->|PR| D[release分支]
    D -->|测试通过| E[main分支]
    F[hotfix分支] -->|PR| E
    E -->|backport| B

小结

你现在掌握了团队协作的核心技能:

  • Fork vs Clone的使用场景
  • Pull Request完整流程
  • 代码审查的最佳实践
  • 解决PR冲突
  • 配置分支保护和CI/CD
  • 多人协作常见问题解决

下一期,我们将进入高级主题——Git钩子和自动化。你会学到如何在commit/push时自动检查代码、运行测试、部署应用。


练习任务

  1. 在GitHub创建一个测试仓库,尝试完整的PR流程
  2. 邀请朋友审查你的PR,练习代码审查对话
  3. 配置一个简单的GitHub Actions,自动运行测试
  4. 尝试在PR中制造冲突并解决

下期预告:《Git高级:钩子与自动化》—— pre-commit怎么配置?如何自动部署?Husky和lint-staged怎么用?

Views: 30

Git分支管理:在平行宇宙中优雅开发

Git分支管理:在平行宇宙中优雅开发

在上一期,我们学会了Git的基本操作。你可能会想:不就是存代码历史嘛,为什么要搞得这么复杂?

答案就在于分支。分支是Git最强大的特性,它允许你在不影响主线的情况下,创建一个平行的开发线,自由实验、开发新功能、修复bug。当你完成工作后,再把分支合并回去。

这就像在写小说时,你想尝试一个大胆的剧情转折。你不需要直接改原文,而是复制一份,在新文件里尽情发挥。如果写得好,就把新版本作为正式版;如果写得烂,直接删掉,原文丝毫不受影响。

分支的本质

在深入之前,先理解分支的本质。

Git的提交不是孤立的,每个提交都包含一个指针,指向上一个提交(父提交)。这样一串提交就形成了一条链。

graph LR
    A[提交1] --> B[提交2] --> C[提交3] --> D[提交4]

分支本质上只是一个指向某个提交的可移动指针。默认分支叫main(以前叫master)。

HEAD是一个特殊指针,指向当前所在的分支。

graph TB
    HEAD --> main
    main --> D[提交4]
    D --> C[提交3] --> B[提交2] --> A[提交1]

当你创建新分支时,Git只是创建了一个新指针,指向当前提交。仓库本身没有变化,快到飞起。

graph TB
    HEAD --> main
    main --> D[提交4]
    feature --> D
    D --> C[提交3] --> B[提交2] --> A[提交1]

分支基本操作

创建分支

# 创建分支(停留在当前分支)
git branch feature-login

# 创建并切换
git checkout -b feature-login

# 新语法(推荐)
git switch -c feature-login

切换分支

# 老语法
git checkout feature-login

# 新语法(推荐)
git switch feature-login

查看分支

# 查看本地分支
git branch

# 查看所有分支(包括远程)
git branch -a

# 查看分支及其最后一次提交
git branch -v

删除分支

# 删除已合并的分支
git branch -d feature-login

# 强制删除(未合并也删)
git branch -D feature-login

重命名分支

# 重命名当前分支
git branch -m new-name

# 重命名指定分支
git branch -m old-name new-name

分支工作流

Feature Branch工作流

这是最常用的分支策略。每开发一个新功能,就创建一个分支,开发完成后合并回主线。

# 1. 从main创建功能分支
git checkout main
git pull
git checkout -b feature-user-profile

# 2. 开发中...频繁提交
git add .
git commit -m "feat: 添加用户头像上传"

git add .
git commit -m "feat: 添加用户简介编辑"

# 3. 开发完成,合并回main
git checkout main
git pull  # 先更新main
git merge feature-user-profile

# 4. 删除功能分支
git branch -d feature-user-profile

Git Flow工作流

更复杂的团队协作模型,定义了几种分支类型:

  • main:生产环境代码,永远是稳定的
  • develop:开发分支,集成各个功能
  • **feature/***:功能分支,从develop分出,合并回develop
  • **release/***:发布分支,从develop分出,准备发布
  • **hotfix/***:紧急修复分支,从main分出,合并回main和develop
gitGraph
    commit id: "init"
    branch develop
    checkout develop
    commit id: "dev1"
    branch feature-A
    checkout feature-A
    commit id: "feat1"
    commit id: "feat2"
    checkout develop
    merge feature-A id: "merge-A"
    checkout main
    branch hotfix
    checkout hotfix
    commit id: "fix"
    checkout main
    merge hotfix id: "hotfix-merge"
    checkout develop
    merge hotfix

小团队不需要这么复杂,Feature Branch足够用了。

合并分支

快进合并(Fast-forward)

如果main分支在你分出feature后没有任何新提交,Git会直接把main指针移到feature的最新提交。

git checkout main
git merge feature-login
# 输出:Fast-forward

这种合并不产生新的提交节点,历史是线性的。

三方合并(Three-way merge)

如果main分支有了新提交,Git会找到两个分支的共同祖先,做一个三方合并,产生一个新的合并提交。

git checkout main
git merge feature-login
# 输出:Merge made by the 'recursive' strategy.
graph TB
    subgraph 合并前
    A[共同祖先] --> B[main新提交]
    A --> C[feature提交]
    end
graph TB
    subgraph 合并后
    A[共同祖先] --> B[main新提交]
    A --> C[feature提交]
    B --> D[合并提交]
    C --> D
    end

禁用快进合并

有时候你想保留分支历史,即使是快进合并也产生一个合并节点:

git merge --no-ff feature-login

这样历史会更清晰地显示"这里曾经有个分支"。

冲突解决

这是新手最害怕的部分。当两个分支修改了同一文件的同一行,Git无法自动合并,就会产生冲突。

模拟冲突

# 分支1修改readme.md第一行
echo "Version A" > readme.md
git add readme.md
git commit -m "Version A"

# 切回main,也修改第一行
git checkout main
echo "Version B" > readme.md
git add readme.md
git commit -m "Version B"

# 尝试合并
git merge feature-xxx
# 冲突!

Git会告诉你哪些文件冲突了:

CONFLICT (content): Merge conflict in readme.md
Automatic merge failed; fix conflicts and then commit the result.

打开冲突文件,你会看到:

<<<<<<>>>>>> feature-xxx
  • <<<<<<>>>>>> feature-xxx:要合并的分支内容

手动解决冲突

  1. 删除标记符号
  2. 保留正确的内容(或两者都保留)
  3. 保存文件
  4. git add + git commit
# 解决后
echo "Version A and B combined" > readme.md
git add readme.md
git commit -m "merge: 解决readme冲突"

使用合并工具

# 使用配置的合并工具(如VSCode)
git mergetool

放弃合并

git merge --abort

变基(Rebase)

merge会产生分叉的历史,有些人喜欢线性的历史。这时候可以用rebase。

merge vs rebase

gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    checkout feature
    commit id: "C"
    commit id: "D"
    checkout main
    commit id: "E"

用merge

gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    checkout feature
    commit id: "C"
    commit id: "D"
    checkout main
    commit id: "E"
    merge feature id: "M"

用rebase

gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "E"
    commit id: "C'"
    commit id: "D'"
# 在feature分支上执行
git checkout feature
git rebase main

rebase会把feature上的提交"移到"main的最新提交之后,历史变成线性的。

黄金法则永远不要rebase已经push到远程的提交。因为这会改写历史,给队友带来灾难。

交互式rebase

# 修改最近3个提交
git rebase -i HEAD~3

这会打开编辑器,你可以:

  • pick:保留提交
  • squash:合并到前一个提交
  • reword:修改提交信息
  • drop:删除提交
  • edit:暂停以便修改这个提交

Stash:临时保存

开发到一半,突然需要切换分支处理紧急事务,但当前工作还没完成,不想commit怎么办?

# 保存当前工作
git stash

# 带描述保存
git stash save "WIP: 用户登录功能"

# 查看stash列表
git stash list

# 恢复最近的stash
git stash pop

# 恢复但不删除
git stash apply

# 恢复指定stash
git stash apply stash@{2}

# 删除stash
git stash drop stash@{0}

# 清空所有stash
git stash clear

Cherry-pick:选择性合并

只想合并某个分支的一个特定提交?用cherry-pick:

# 先在目标分支找到提交hash
git checkout feature-A
git log --oneline
# 假设要合并的提交是 a1b2c3d

# 切回main,cherry-pick
git checkout main
git cherry-pick a1b2c3d

这会把那个提交的改动复制到当前分支,产生一个新的提交。

分支最佳实践

  1. 分支命名规范

    • feature/xxx:新功能
    • bugfix/xxx:bug修复
    • hotfix/xxx:紧急修复
    • release/x.x.x:发布准备
    • experiment/xxx:实验性功能
  2. 及时删除已合并分支

    # 删除本地
    git branch -d feature-xxx
    
    # 删除远程
    git push origin --delete feature-xxx
  3. 保持分支短小

    • 一个分支只做一件事
    • 频繁合并回主线,避免长寿命分支
  4. 提交前先pull

    git checkout main
    git pull --rebase  # 用rebase避免不必要的merge提交
  5. 使用Pull Request / Merge Request
    不要直接在本地合并,通过PR让代码经过review再合并。

小结

你现在掌握了Git分支的核心技能:

  • 理解分支的本质(可移动指针)
  • 创建、切换、删除分支
  • 合并分支(快进和三方合并)
  • 解决合并冲突
  • 使用rebase保持线性历史
  • stash临时保存工作
  • cherry-pick选择性合并

下一期,我们将进入团队协作。你会学到如何用Git在团队中高效工作,包括Pull Request流程、代码审查、多人协作的最佳实践。


练习任务

  1. 创建一个feature分支,提交几次后合并回main
  2. 制造一个冲突并手动解决
  3. 尝试git rebase -i合并最近2个提交
  4. 用stash保存当前工作,切换分支后再恢复

下期预告:《Git协作:团队开发流程》—— Pull Request怎么做?代码审查看什么?多人协作怎么避免灾难?

Views: 15

Git入门指南:从零开始掌握版本控制

Git入门指南:从零开始掌握版本控制

Git Logo

你有没有经历过这样的噩梦?项目文件夹里到处是 最终版.doc最终版2.doc打死不改版.doc真的最后版.doc...每次回滚代码都要手动复制备份,团队协作时互相覆盖文件,谁改了什么完全不知道。

这就是没有版本控制的痛。而Git,就是终结这一切的利器。

什么是版本控制?

想象你在写一本小说,每写完一章就保存一个副本。突然你发现第三章的情节有问题,想回到第二章重新写。如果没有版本控制,你只能手动翻备份文件夹,祈祷自己当时保存了。

版本控制系统就像是给项目装了一个时光机。它记录了每次修改的快照,你可以随时回到任意历史版本,查看谁在什么时候改了什么,甚至可以创建平行世界(分支)来尝试不同的剧情走向。

为什么是Git?

2005年,Linux内核开发社区遭遇了一场危机:他们使用的专有版本控制系统BitKeeper不再免费提供。Linux之父Linus Torvalds在短短两周内开发出了Git的初始版本。

Git的设计哲学很简单:快、简单、支持非线性开发(成千上万个并行分支)、完全分布式。这些特性让它迅速成为全球最流行的版本控制系统。今天,从Google到Microsoft,从Facebook到阿里巴巴,几乎每家科技公司都在用Git。

安装Git

Windows用户
从官网下载安装包(https://git-scm.com/download/win),一路下一步即可。安装完成后,右键菜单会多出"Git Bash Here"选项,这就是你的Git命令行工具。

Mac用户
最简单的方式是安装Xcode Command Line Tools:

xcode-select --install

或者用Homebrew:

brew install git

Linux用户
Debian/Ubuntu:

sudo apt-get install git

CentOS/RHEL:

sudo yum install git

安装完成后,验证一下:

git --version

看到版本号就说明安装成功了。

第一次配置

Git需要知道你是谁,因为每次提交都会记录作者信息。打开终端,运行:

git config --global user.name "你的名字"
git config --global user.email "你的邮箱"

这里的名字和邮箱最好和你的GitHub/GitLab账号一致,这样后续协作会更方便。

查看配置:

git config --list

创建你的第一个仓库

方式一:初始化新仓库

创建一个新项目文件夹,进入后执行:

mkdir my-first-project
cd my-first-project
git init

这时候Git会在当前目录创建一个隐藏的.git文件夹,所有版本信息都存在这里。

方式二:克隆已有仓库

如果项目已经存在于远程(比如GitHub),直接克隆:

git clone https://github.com/username/repo.git

理解Git的三个区域

这是理解Git最关键的概念。Git有三个逻辑区域:

  1. 工作区(Working Directory):你看到的文件,正在编辑的地方
  2. 暂存区(Staging Area/Index):准备提交的修改,像购物车
  3. 版本库(Repository):已提交的历史记录,像仓库
graph LR
    A[工作区] -->|git add| B[暂存区]
    B -->|git commit| C[版本库]
    C -->|git checkout| A

为什么需要暂存区?

假设你修改了三个文件:A、B、C。其中A和B是同一个功能,C是另一个功能。你可以把A和B先add到暂存区,commit成一个提交;然后再add C,commit成另一个提交。这样提交历史就很清晰,而不是把所有修改混在一起。

日常操作流程

1. 查看状态

git status

这是最常用的命令,告诉你哪些文件被修改了,哪些在暂存区,哪些还没被跟踪。

2. 添加到暂存区

# 添加单个文件
git add filename.txt

# 添加所有修改
git add .

# 添加所有修改(包括删除)
git add -A

3. 提交到版本库

git commit -m "简洁的提交信息"

提交信息很重要!好的提交信息应该回答:做了什么修改?为什么这样修改?

4. 查看历史

# 完整历史
git log

# 简洁版(一行一个提交)
git log --oneline

# 图形化显示分支
git log --oneline --graph --all

5. 查看差异

# 工作区 vs 暂存区
git diff

# 暂存区 vs 最新提交
git diff --staged

# 两个提交之间
git diff commit1 commit2

撤销操作(救你于水火)

撤销工作区修改

# 丢弃单个文件的修改(危险操作!)
git checkout -- filename.txt

# 或者用新语法
git restore filename.txt

撤销暂存

# 把文件从暂存区移回工作区
git reset HEAD filename.txt

# 或者用新语法
git restore --staged filename.txt

修改最后一次提交

# 还没push,发现提交信息写错了
git commit --amend -m "正确的提交信息"

# 还没push,漏了一个文件
git add forgotten-file.txt
git commit --amend --no-edit

注意--amend会改写历史,如果已经push了就不要用,否则会给队友带来麻烦。

忽略文件

有些文件不需要纳入版本控制,比如编译产物、日志文件、IDE配置等。在项目根目录创建.gitignore文件:

# 编译产物
*.o
*.exe
build/

# 日志
*.log

# IDE配置
.idea/
.vscode/

# 系统文件
.DS_Store
Thumbs.db

# 依赖目录
node_modules/
.venv/

GitHub有个awesome-gitignore仓库,收集了各种项目的.gitignore模板,值得收藏。

远程仓库

关联远程仓库

# 添加远程仓库
git remote add origin https://github.com/username/repo.git

# 查看远程仓库
git remote -v

推送代码

# 第一次推送(设置上游分支)
git push -u origin main

# 之后推送
git push

拉取代码

# 拉取并合并
git pull

# 只拉取不合并
git fetch

git pull = git fetch + git merge。建议先fetch看看有什么变化,再决定要不要merge。

SSH配置(告别密码)

每次push都要输入密码很烦?配置SSH密钥:

# 生成密钥对
ssh-keygen -t ed25519 -C "你的邮箱"

# 查看公钥
cat ~/.ssh/id_ed25519.pub

把公钥复制到GitHub的Settings → SSH Keys。然后把远程地址改成SSH格式:

git remote set-url origin git@github.com:username/repo.git

现在push再也不用输密码了。

小结

恭喜你,掌握了Git的基础用法。你现在可以:

  • 创建和管理仓库
  • 理解工作区、暂存区、版本库
  • 进行日常的add、commit、push、pull操作
  • 撤销误操作
  • 配置.gitignore和SSH

但这只是Git冰山一角。下一期,我们将深入分支管理——Git最强大的功能之一。你会学会如何创建平行宇宙,在不同的开发线之间自由穿梭,最后把它们完美合并。


练习任务

  1. 创建一个新仓库,提交至少3次
  2. 尝试修改文件后用git checkout撤销
  3. 故意add一个文件,然后用git reset HEAD撤销暂存
  4. 在GitHub创建仓库,把本地代码push上去

做完这些,你就真正入门Git了。


下期预告:《Git进阶:分支管理艺术》—— 什么时候该开分支?如何优雅地合并?冲突了怎么办?

Views: 27

从”打爆电话”到”优雅等餐”:Promise 与 async/await 的前世今生

从"打爆电话"到"优雅等餐":Promise 与 async/await 的前世今生

JavaScript Async

你有没有经历过这样的代码?

getUserInfo(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getPaymentInfo(details.paymentId, function(payment) {
        // 恭喜你,已经分不清这是第几层了
        console.log(payment);
      });
    });
  });
});

这就是传说中的回调地狱——代码向右生长,像个倒过来的金字塔。

今天我们用一个故事,把 JavaScript 异步编程彻底讲明白。


一切从一个外卖订单开始

假设你在开发一个外卖 App,用户下单后,你的代码需要依次完成:

  1. 验证用户身份
  2. 检查餐厅是否营业
  3. 创建订单
  4. 扣款
  5. 通知骑手接单

每一步都要等上一步完成才能继续,而且每一步都可能失败。

这就是异步编程的经典场景:多个有依赖关系的异步操作串行执行


原始时代:回调函数

打电话的模式

在 Promise 出现之前,我们用回调函数处理异步操作。就像打电话:

你:老板,给我做个汉堡!
老板:好的,做好了打你电话!
(你挂断电话,干别的事)
老板:(做好后)喂,汉堡好了!

代码实现:

function makeBurger(callback) {
  setTimeout(() => {
    callback(null, { name: '汉堡', price: 25 });
  }, 2000);
}

makeBurger(function(err, burger) {
  if (err) {
    console.log('做失败了:', err);
    return;
  }
  console.log('收到:', burger);
});

问题来了:连锁订单

如果要连续做汉堡、薯条、可乐呢?

makeBurger(function(err, burger) {
  if (err) return handleError(err);

  makeFries(function(err, fries) {
    if (err) return handleError(err);

    makeCola(function(err, cola) {
      if (err) return handleError(err);

      // 终于集齐了...
      serveMeal([burger, fries, cola]);
    });
  });
});

问题显而易见:

  • 代码向右"金字塔化"
  • 错误处理到处重复
  • 变量作用域混乱
  • 很难在中间插入新步骤

Promise 时代:订单系统

什么是 Promise?

Promise 就像餐厅的取餐号牌

你:老板,给我做个汉堡!
老板:给你个号牌,做好了叫号!
你:(拿着号牌走了)

号牌有三种状态:

  • Pending(等待中):还没做好
  • Fulfilled(已兑现):做好了,来取餐
  • Rejected(已拒绝):卖光了,做不了

Promise 化的代码

function makeBurger() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 成功:交出汉堡
      resolve({ name: '汉堡', price: 25 });

      // 失败:拒绝订单
      // reject('卖光了!');
    }, 2000);
  });
}

链式调用:优雅的流水线

makeBurger()
  .then(burger => {
    console.log('收到汉堡:', burger);
    return makeFries();
  })
  .then(fries => {
    console.log('收到薯条:', fries);
    return makeCola();
  })
  .then(cola => {
    console.log('收到可乐:', cola);
    console.log('套餐齐了!');
  })
  .catch(err => {
    // 统一的错误处理!
    console.log('出问题了:', err);
  });

对比回调地狱,代码变成了垂直的流水线

  • 每一步清晰可见
  • 错误统一处理
  • 随时可以插入新步骤

Promise.all:并行出餐

如果要同时做三个东西,谁先做好谁先上?

Promise.all([
  makeBurger(),
  makeFries(),
  makeCola()
])
  .then(([burger, fries, cola]) => {
    // 三个都做好了,一起上桌
    serveMeal([burger, fries, cola]);
  })
  .catch(err => {
    // 任何一个失败,整个套餐取消
    console.log('有东西做失败了:', err);
  });

Promise.race:谁快用谁

点外卖时,同时看三家店,谁先接单用谁:

Promise.race([
  orderFrom('麦当劳'),
  orderFrom('肯德基'),
  orderFrom('汉堡王')
])
  .then(firstReady => {
    console.log('最先接单的是:', firstReady);
  });

async/await 时代:同步的错觉

终极优雅:像写同步代码一样写异步

Promise 链式调用已经很优雅了,但 async/await 把它推向了极致:

async function orderMeal() {
  try {
    const burger = await makeBurger();
    console.log('收到汉堡:', burger);

    const fries = await makeFries();
    console.log('收到薯条:', fries);

    const cola = await makeCola();
    console.log('收到可乐:', cola);

    console.log('套餐齐了!');
    return [burger, fries, cola];

  } catch (err) {
    console.log('出问题了:', err);
  }
}

看起来像同步代码,但实际上还是异步的!

底层原理:语法糖

async/await 本质上是 Promise 的语法糖。上面的代码等价于:

function orderMeal() {
  return makeBurger()
    .then(burger => {
      console.log('收到汉堡:', burger);
      return makeFries();
    })
    .then(fries => {
      console.log('收到薯条:', fries);
      return makeCola();
    })
    .then(cola => {
      console.log('收到可乐:', cola);
      console.log('套餐齐了!');
      return [burger, fries, cola];
    })
    .catch(err => {
      console.log('出问题了:', err);
    });
}

async 函数自动返回 Promise

async function sayHello() {
  return 'Hello!';
}

sayHello().then(msg => console.log(msg));  // 'Hello!'

await 只能在 async 函数内使用

// ❌ 错误!
const burger = await makeBurger();

// ✅ 正确
async function order() {
  const burger = await makeBurger();
}

并行优化:不要过度 await

// ❌ 串行执行,慢!
async function slowOrder() {
  const burger = await makeBurger();   // 等2秒
  const fries = await makeFries();     // 等2秒
  const cola = await makeCola();       // 等2秒
  // 总共6秒
}

// ✅ 并行执行,快!
async function fastOrder() {
  const [burger, fries, cola] = await Promise.all([
    makeBurger(),
    makeFries(),
    makeCola()
  ]);
  // 总共2秒(最慢的那个)
}

实战:完整的外卖订单流程

// 工具函数:模拟异步操作
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 各环节实现
async function verifyUser(userId) {
  await delay(500);
  if (!userId) throw new Error('用户未登录');
  return { id: userId, name: '张三' };
}

async function checkRestaurant(restaurantId) {
  await delay(300);
  const isOpen = Math.random() > 0.2;
  if (!isOpen) throw new Error('餐厅已打烊');
  return { id: restaurantId, name: '麦当劳' };
}

async function createOrder(user, restaurant) {
  await delay(800);
  return { 
    orderId: 'ORD' + Date.now(),
    userId: user.id,
    restaurantId: restaurant.id 
  };
}

async function processPayment(order) {
  await delay(1000);
  const success = Math.random() > 0.1;
  if (!success) throw new Error('支付失败');
  return { ...order, paid: true };
}

async function notifyRider(order) {
  await delay(500);
  return { ...order, riderId: 'RIDER001', status: '配送中' };
}

// 主流程
async function placeOrder(userId, restaurantId) {
  try {
    console.log('开始处理订单...');

    const [user, restaurant] = await Promise.all([
      verifyUser(userId),
      checkRestaurant(restaurantId)
    ]);

    const order = await createOrder(user, restaurant);
    const paidOrder = await processPayment(order);
    const finalOrder = await notifyRider(paidOrder);

    return finalOrder;

  } catch (error) {
    console.log('订单失败:', error.message);
    throw error;
  }
}

// 使用
placeOrder('USER001', 'REST001')
  .then(order => console.log('订单完成:', order))
  .catch(err => console.log('最终错误:', err));

总结:三代技术的选择

方式 适用场景 优点 缺点
回调函数 简单、单层异步 理解简单 多层嵌套=地狱
Promise 复杂链式调用、并行操作 链式优雅、错误统一 then 嵌套多了也乱
async/await 大部分异步场景 最优雅、最像同步 需要理解 Promise 原理

我的建议

  • 90% 场景用 async/await
  • 需要并行时用 Promise.all
  • 读别人的代码时,三者都要会

延伸:为什么 JavaScript 需要异步?

JavaScript 是单线程语言,一次只能做一件事。

如果用同步方式请求服务器:

const data = fetchSync('https://api.example.com/data');
// 在收到响应前,整个页面卡死!

所以 JavaScript 用异步:

fetch('https://api.example.com/data')
  .then(data => console.log(data));
// 请求发出后,代码继续执行

最后的思考题

async function quiz() {
  console.log(1);

  await Promise.resolve().then(() => console.log(2));

  console.log(3);

  Promise.resolve().then(() => console.log(4));

  console.log(5);
}

quiz();

输出顺序是什么?(答案在评论区揭晓)


记住:异步编程不是魔法,只是让代码在等待时"去做别的事"。理解了这一点,Promise 和 async/await 就不再是黑盒了。

下次写代码时,想象你在经营一家餐厅——让客人拿着号牌去干别的事,好了再叫号。

这就是异步编程的本质。

Views: 16

CloudCLI UI + Tailscale:手机远程控制 Claude Code 完整实践

CloudCLI UI + Tailscale:手机远程控制 Claude Code 完整实践

Remote Work

一次完整的技术踩坑记录,从环境配置到手机访问的实战指南


缘起:为什么需要手机访问?

作为 AI 助手重度用户,我经常需要在外出时查看和操作 Claude Code 项目。但传统的远程桌面方案要么速度慢,要么配置复杂。

直到我发现了这个组合:CloudCLI UI + Tailscale

  • CloudCLI UI:为 Claude Code 提供 Web 界面
  • Tailscale:零配置的 P2P 组网工具

两者结合,实现:无需 VPN、无需公网 IP、无需端口映射,手机直接访问本地 Claude Code


第一步:CloudCLI UI 环境搭建

安装依赖

首先确保系统已安装:

  • Node.js(通过 nvm 管理)
npm install -g @siteboon/claude-code-ui

配置 GLM-4.7 模型

修改模型显示名称:

修改 C:\dev\nvm\v22.13.1\node_modules\@siteboon\claude-code-ui\shared\modelConstants.js

export const CLAUDE_MODELS = {
  // Models in SDK format (what the actual SDK accepts)
  OPTIONS: [
    { value: 'sonnet', label: 'GLM-4.7 (Sonnet)' },
    { value: 'opus', label: 'GLM-4.7 (Opus)' },
    { value: 'haiku', label: 'GLM-4.5-Air (Haiku)' },
    { value: 'opusplan', label: 'GLM-4.7 (Opus Plan)' },
    { value: 'sonnet[1m]', label: 'GLM-4.7 (Sonnet 1M)' }
  ],

  DEFAULT: 'sonnet'
};

第二步:启动服务

claude-code-ui

本地测试

浏览器访问:http://localhost:3001

首次访问需要设置账号密码,建议使用简单易记的组合。


第三步:踩过的坑(关键!)

坑1:端口被占用

症状

Error: Port 3001 already in use

解决方案

# 查找占用进程
netstat -ano | findstr :3001

# 强制结束
taskkill /F /PID <进程ID>

坑2:SDK 初始化失败

症状

SDK initialization failed

解决方案

  • 检查 Node.js 版本(推荐 v22+)
  • 确认环境变量已生效
  • 重启服务

第四步:Tailscale 组网配置

为什么选择 Tailscale?

特性 VPN Tailscale
需要服务器
速度 快(P2P)
配置复杂度
安全性 依赖服务商 端到端加密

电脑端安装

  1. 访问 https://tailscale.com/download
  2. 下载 Windows 客户端
  3. 安装后登录(支持 Google、GitHub、邮箱)
  4. 确认状态为 Connected 🟢
  5. 记录你的 Tailscale IP:100.73.1.26

手机端安装

Android

  1. 搜索安装 "Tailscale"
  2. 安装并登录(必须和电脑使用同一账号
  3. 确保 Connected 🟢

iOS

  1. App Store 搜索 "Tailscale"
  2. 安装并登录
  3. 确保 Connected 🟢

网络拓扑

┌─────────────────┐ ┌─────────────────┐
│ Windows PC │ │ Android 手机 │
│ 100.73.1.26 │◄───────►│ (动态IP) │
│ CloudCLI:3001 │ P2P │ │
└─────────────────┘ └─────────────────┘
│ │
└───────────────────────────┘
虚拟局域网(无需公网IP)

第五步:手机访问实战

访问步骤

  1. 手机连接 Tailscale

    • 打开 Tailscale App
    • 确认状态:Connected 🟢
  2. 浏览器访问

  3. 选择模型

    • 点击模型选择器
    • 选择 "Opus"(会自动使用 GLM-4.7)
  4. 开始聊天

    • 发送消息测试
    • 享受远程 AI 助手服务

实测体验

速度

  • 响应速度:几乎无延迟(P2P 直连)
  • 首屏加载:< 2 秒

稳定性

  • 连接稳定,无断线
  • 支持多设备同时在线

功能

  • ✅ 完整的 Claude Code 功能
  • ✅ 项目管理
  • ✅ 代码编辑
  • ✅ 实时对话

技术细节解析

为什么不用 VPN?

VPN 的问题

  • 需要租用服务器(费用)
  • 流量经过服务器(速度慢)
  • 配置复杂(证书、路由、防火墙)

Tailscale 的优势

  • 零配置(登录即用)
  • P2P 直连(速度快)
  • 免费使用(个人版)
  • 端到端加密(安全)

CloudCLI UI 工作原理

浏览器 ←─WebSocket─→ CloudCLI Server
↓
Claude Code SDK
↓
AI Model (GLM-4.7)

核心功能:

  1. Web 界面提供聊天窗口
  2. WebSocket 实现实时通信
  3. SDK 调用底层 AI 模型
  4. 项目管理功能

Tailscale 工作原理

1. 设备登录 Tailscale 服务器
2. 服务器协调 NAT 穿透
3. 设备建立 P2P 连接
4. 后续通信不经过服务器

关键技术:

  • WireGuard 协议(加密)
  • NAT 穿透(直连)
  • DERP 中继(穿透失败时)

使用场景

场景1:外出办公

  • 地点:咖啡厅、机场、酒店
  • 操作:手机访问本地 CloudCLI
  • 优势:无需带电脑,随时随地查看项目进度

场景2:远程调试

  • 地点:客户现场
  • 操作:手机连接办公室电脑
  • 优势:现场问题即时排查

场景3:多设备协作

  • 设备:手机、平板、电脑
  • 操作:多设备同时访问同一服务
  • 优势:无缝切换,提高效率

常见问题

Q1:手机访问不了?

检查清单

  • 电脑 Tailscale 已连接 🟢
  • 手机 Tailscale 已连接 🟢
  • 电脑 IP 正确(100.73.1.26)
  • CloudCLI 服务已启动
  • 端口 3001 未被防火墙阻止

Q2:速度慢?

优化建议

  • 确认 P2P 连接(而非中继)
  • 检查网络质量
  • 尝试切换网络(WiFi/4G)

Q3:GitHub 认证失败?

解决方案

  • 国内访问 GitHub 可能需要加速器
  • 建议使用邮箱登录 Tailscale

对比其他方案

方案 复杂度 速度 成本 推荐度
CloudCLI + Tailscale ⭐⭐⭐⭐⭐ 免费 ⭐⭐⭐⭐⭐
ngrok + 本地服务 ⭐⭐ ⭐⭐⭐ 免费/付费 ⭐⭐⭐
远程桌面 ⭐⭐⭐ ⭐⭐ 免费 ⭐⭐
VPN ⭐⭐⭐⭐ ⭐⭐ 付费 ⭐⭐
公网 IP + 端口映射 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ 付费

最佳实践

安全建议

  1. 强密码

    • CloudCLI 使用复杂密码
    • 定期更换
  2. 访问控制

    • 仅允许信任设备接入
    • 定期检查 Tailscale 设备列表
  3. 网络隔离

    • CloudCLI 只监听 localhost
    • 通过 Tailscale 访问,不暴露到公网

性能优化

  1. 保持 P2P 连接

    • 避免使用 DERP 中继
    • 检查 NAT 类型
  2. 资源管理

    • 及时关闭不用的对话
    • 定期清理项目缓存

总结

CloudCLI UI + Tailscale 的组合,为远程 AI 助手访问提供了零配置、高速、安全的解决方案。

核心优势

  • ✅ 无需公网 IP
  • ✅ 无需端口映射
  • ✅ 无需 VPN 服务器
  • ✅ P2P 直连,速度快
  • ✅ 端到端加密,安全
  • ✅ 配置简单,10分钟搞定

适用人群

  • 远程办公人员
  • 移动办公需求
  • AI 助手重度用户
  • 技术爱好者

技术栈

  • CloudCLI UI:Web 界面
  • Tailscale:P2P 组网
  • GLM-4.7:AI 模型
  • Python + Node.js:后端服务

相关资源


实践日期:2026年2月23日
技术栈:CloudCLI UI + Tailscale + GLM-4.7
作者:PaPaBot

文章首发delucia.cn

Views: 220