前端架構師的 git 功力,你有幾成火候?

語言: CN / TW / HK

本文從前端工程,團隊協作,生產部署的角度,介紹架構人員需要掌握的 git 實踐能力。

大綱預覽

本文介紹的內容包括以下方面:

  • 分支管理策略
  • commit 規範與提交驗證
  • 誤操作的撤回方案
  • Tag 與生產環境
  • 永久杜絕 443 Timeout
  • hook 實現部署?
  • 終極應用: CI/CD

分支管理策略

git 分支強大的同時也非常靈活,如果沒有一個好的分支管理策略,團隊人員隨意合併推送,就會造成分支混亂,各種覆蓋,衝突,丟失等問題。

目前最流行的分支管理策略,也稱工作流(Workflow),主要包含三種:

  • Git Flow
  • GitHub Flow
  • GitLab Flow

我司前端團隊結合實際情況,制定出自己的一套分支管理策略。

我們將分支分為 4 個大類:

  • dev-*
  • develop
  • staging
  • release

dev-* 是一組開發分支的統稱,包括個人分支,模組分支,修復分支等,團隊開發人員在這組分支上進行開發。

開發前,先通過 merge 合併 develop 分支的最新程式碼;開發完成後,必須通過 cherry-pick 合併回 develop 分支。

develop 是一個單獨分支,對應開發環境,保留最新的完整的開發程式碼。它只接受 cherry-pick 的合併,不允許使用 merge。

staging 分支對應測試環境。當 develop 分支有更新並且準備釋出測試時,staging 要通過 rebase 合併 develop 分支,然後將最新程式碼釋出到測試伺服器,供測試人員測試。

測試發現問題後,再走 dev-* -> develop -> staging 的流程,直到測試通過。

release 則表示生產環境。release 分支的最新提交永遠與線上生產環境程式碼保持同步,也就是說,release 分支是隨時可釋出的。

當 staging 測試通過後,release 分支通過 rebase 合併 staging 分支,然後將最新程式碼釋出到生產伺服器。

總結下合併規則:

  • develop -> (merge) -> dev-*
  • dev-* -> (cherry-pick) -> develop
  • develop -> (rebase) -> staging
  • staging -> (rebase) -> release

為什麼合併到 develop 必須用 cherry-pick?

使用 merge 合併,如果有衝突,會產生分叉;dev-* 分支多而雜,直接 merge 到 develop 會產生錯綜複雜的分叉,難以理清提交進度。

而 cherry-pick 只將需要的 commit 合併到 develop 分支上,且不會產生分叉,使 git 提交圖譜(git graph)永遠保持一條直線。

再有,模組開發分支完成後,需要將多個 commit 合為一個 commit,再合併到 develop 分支,避免了多餘的 commit,這也是不用 merge 的原因之一。

為什麼合併到 staging/release 必須用 rebase?

rebase 譯為變基,合併同樣不會產生分叉。當 develop 更新了許多功能,要合併到 staging 測試,不可能用 cherry-pick 一個一個把 commit 合併過去。因此要通過 rebase 一次性合併過去,並且保證了 staging 與 develop 完全同步。

release 也一樣,測試通過後,用 rebase 一次性將 staging 合併過去,同樣保證了 staging 與 release 完全同步。

commit 規範與提交驗證

commit 規範是指 git commit 時填寫的描述資訊,要符合統一規範。

試想,如果團隊成員的 commit 是隨意填寫的,在協作開發和 review 程式碼時,其他人根本不知道這個 commit 是完成了什麼功能,或是修復了什麼 Bug,很難把控進度。

為了直觀的看出 commit 的更新內容,開發者社群誕生了一種規範,將 commit 按照功能劃分,加一些固定字首,比如 fix:feat:,用來標記這個 commit 主要做了什麼事情。

目前主流的字首包括以下部分:

  • build:表示構建,釋出版本可用這個
  • ci:更新 CI/CD 等自動化配置
  • chore:雜項,其他更改
  • docs:更新文件
  • feat:常用,表示新增功能
  • fix:常用:表示修復 bug
  • perf:效能優化
  • refactor:重構
  • revert:程式碼回滾
  • style:樣式更改
  • test:單元測試更改

這些字首每次提交都要寫,剛開始很多人還是記不住的。這裡推薦一個非常好用的工具,可以自動生成字首。地址在這裡

首先全域性安裝:

sh npm install -g commitizen cz-conventional-changelog

建立 ~/.czrc 檔案,寫入如下內容:

js { "path": "cz-conventional-changelog" }

現在可以用 git cz 命令來代替 git commit 命令,效果如下:

WX20210922.png

然後上下箭選擇字首,根據提示即可方便的建立符合規範的提交。

有了規範之後,光靠人的自覺遵守是不行的,還要在流程上對提交資訊進行校驗。

這個時候,我們要用到一個新東西 —— git hook,也就是 git 鉤子。

git hook 的作用是在 git 動作發生前後觸發自定義指令碼。這些動作包括提交,合併,推送等,我們可以利用這些鉤子在 git 流程的各個環節實現自己的業務邏輯。

git hook 分為客戶端 hook 和服務端 hook。

客戶端 hook 主要有四個:

  • pre-commit:提交資訊前執行,可檢查暫存區的程式碼
  • prepare-commit-msg:不常用
  • commit-msg:非常重要,檢查提交資訊就用這個鉤子
  • post-commit:提交完成後執行

服務端 hook 包括:

  • pre-receive:非常重要,推送前的各種檢查都在這
  • post-receive:不常用
  • update:不常用

大多數團隊是在客戶端做校驗,所以我們用 commit-msg 鉤子在客戶端對 commit 資訊做校驗。

幸運的是,不需要我們手動去寫校驗邏輯,社群有成熟的方案:husky + commitlint

husky 是建立 git 客戶端鉤子的神器,commitlint 是校驗 commit 資訊是否符合上述規範。兩者配合,可以阻止建立不符合 commit 規範的提交,從源頭保證提交的規範。

husky + commitlint 的具體使用方法請看這裡

誤操作的撤回方案

開發中頻繁使用 git 拉取推送程式碼,難免會有誤操作。這個時候不要慌,git 支援絕大多數場景的撤回方案,我們來總結一下。

撤回主要是兩個命令:resetrevert

git reset

reset 命令的原理是根據 commitId 來恢復版本。因為每次提交都會生成一個 commitId,所以說 reset 可以幫你恢復到歷史的任何一個版本。

這裡的版本和提交是一個意思,一個 commitId 就是一個版本

reset 命令格式如下:

sh $ git reset [option] [commitId]

比如,要撤回到某一次提交,命令是這樣:

sh $ git reset --hard cc7b5be

上面的命令,commitId 是如何獲取的?很簡單,用 git log 命令檢視提交記錄,可以看到 commitId 值,這個值很長,我們取前 7 位即可。

這裡的 option 用的是 --hard,其實共有 3 個值,具體含義如下:

  • --hard:撤銷 commit,撤銷 add,刪除工作區改動程式碼
  • --mixed:預設引數。撤銷 commit,撤銷 add,還原工作區改動程式碼
  • --soft:撤銷 commit,不撤銷 add,還原工作區改動程式碼

這裡要格外注意 --hard,使用這個引數恢復會刪除工作區程式碼。也就是說,如果你的專案中有未提交的程式碼,使用該引數會直接刪除掉,不可恢復,慎重啊!

除了使用 commitId 恢復,git reset 還提供了恢復到上一次提交的快捷方式:

sh $ git reset --soft HEAD^

HEAD^ 表示上一個提交,可多次使用。

其實平日開發中最多的誤操作是這樣:剛剛提交完,突然發現了問題,比如提交資訊沒寫好,或者程式碼更改有遺漏,這時需要撤回到上次提交,修改程式碼,然後重新提交。

這個流程大致是這樣的:

```sh

1. 回退到上次提交

$ git reset HEAD^

2. 修改程式碼...

...

3. 加入暫存

$ git add .

4. 重新提交

$ git commit -m 'fix: ***' ```

針對這個流程,git 還提供了一個更便捷的方法:

sh $ git commit --amend

這個命令會直接修改當前的提交資訊。如果程式碼有更改,先執行 git add,然後再執行這個命令,比上述的流程更快捷更方便。

reset 還有一個非常重要的特性,就是真正的後退一個版本

什麼意思呢?比如說當前提交,你已經推送到了遠端倉庫;現在你用 reset 撤回了一次提交,此時本地 git 倉庫要落後於遠端倉庫一個版本。此時你再 push,遠端倉庫會拒絕,要求你先 pull。

如果你需要遠端倉庫也後退版本,就需要 -f 引數,強制推送,這時原生代碼會覆蓋遠端程式碼。

注意,-f 引數非常危險!如果你對 git 原理和命令列不是非常熟悉,切記不要用這個引數。

那撤回上一個版本的程式碼,怎麼同步到遠端更安全呢?

方案就是下面要說的第二個命令:git revert

git revert

revert 與 reset 的作用一樣,都是恢復版本,但是它們兩的實現方式不同。

簡單來說,reset 直接恢復到上一個提交,工作區程式碼自然也是上一個提交的程式碼;而 revert 是新增一個提交,但是這個提交是使用上一個提交的程式碼。

因此,它們兩恢復後的程式碼是一致的,區別是一個新增提交(revert),一個回退提交(reset)。

正因為 revert 永遠是在新增提交,因此本地倉庫版本永遠不可能落後於遠端倉庫,可以直接推送到遠端倉庫,故而解決了 reset 後推送需要加 -f 引數的問題,提高了安全性。

說完了原理,我們再看一下使用方法:

sh $ git revert -n [commitId]

掌握了原理使用就很簡單,只要一個 commitId 就可以了。

Tag 與生產環境

git 支援對於歷史的某個提交,打一個 tag 標籤,常用於標識重要的版本更新。

目前普遍的做法是,用 tag 來表示生產環境的版本。當最新的提交通過測試,準備釋出之時,我們就可以建立一個 tag,表示要釋出的生產環境版本。

比如我要發一個 v1.2.4 的版本:

sh $ git tag -a v1.2.4 -m "my version 1.2.4"

然後可以檢視:

```sh $ git show v1.2.4

tag v1.2.4 Tagger: ruims [email protected] Date: Sun Sep 26 10:24:30 2021 +0800

my version 1.2.4 ```

最後用 git push 將 tag 推到遠端:

sh $ git push origin v1.2.4

這裡注意:tag 和在哪個分支建立是沒有關係的,tag 只是提交的別名。因此 commit 的能力 tag 均可使用,比如上面說的 git resetgit revert 命令。

當生產環境出問題,需要版本回退時,可以這樣:

```sh $ git revert [pre-tag]

若上一個版本是 v1.2.3,則:

$ git revert v1.2.3 ```

在頻繁更新,commit 數量龐大的倉庫裡,用 tag 標識版本顯然更清爽,可讀性更佳。

再換一個角度思考 tag 的用處。

上面分支管理策略的部分說過,release 分支與生產環境程式碼同步。在 CI/CD(下面會講到)持續部署的流程中,我們是監聽 release 分支的推送然後觸發自動構建。

那是不是也可以監聽 tag 推送再觸發自動構建,這樣版本更新的直觀性是不是更好?

諸多用處,還待大家思考。

永久杜絕 443 Timeout

我們團隊內部的程式碼倉庫是 GitHub,眾所周知的原因,GitHub 拉取和推送的速度非常慢,甚至直接報錯:443 Timeout。

我們開始的方案是,全員開啟 VPN。雖然大多時候速度不錯,但是確實有偶爾的一個小時,甚至一天,程式碼死活推不上去,嚴重影響開發進度。

後來突然想到,速度慢超時是因為被牆,比如 GitHub 首頁打不開。再究其根源,被牆的是訪問網站時的 http 或 https 協議,那麼其他協議是不是就不會有牆的情況?

想到就做。我們發現 GitHub 除了預設的 https 協議,還支援 ssh 協議。於是準備嘗試一下使用 ssh 協議克隆程式碼。

用 ssh 協議比較麻煩的一點,是要配置免密登入,否則每次 pull/push 時都要輸入賬號密碼。

GitHub 配置 SSH 的官方文件在這裡

英文吃力的同學,可以看這裡

總之,生成公鑰後,開啟 GitHub 首頁,點 Account -> Settings -> SSH and GPG keys -> Add SSH key,然後將公鑰貼上進去即可。

現在,我們用 ssh 協議克隆程式碼,例子如下:

sh $ git clone [email protected]:[organi-name]/[project-name]

發現瞬間克隆下來了!再測幾次 pull/push,速度飛起!

不管你用哪個程式碼管理平臺,如果遇到 443 Timeout 問題,請試試 ssh 協議!

hook 實現部署?

利用 git hook 實現部署,應該是 hook 的高階應用了。

現在有很多工具,比如 GitHub,GitLab,都提供了持續整合功能,也就是監聽某一分支推送,然後觸發自動構建,並自動部署。

其實,不管這些工具有多少花樣,核心的功能(監聽和構建)還是由 git 提供。只不過在核心功能上做了與自家平臺更好的融合。

我們今天就拋開這些工具,追本溯源,使用純 git 實現一個 react 專案的自動部署。掌握了這套核心邏輯,其他任何平臺的持續部署也就沒那麼神祕了。

由於這一部分內容較多,所以單獨拆出去一篇文章,地址如下:

純 Git 實現前端 CI/CD

終極應用: CI/CD

上面的一些地方也提到了持續整合,持續部署這些字眼,現在,千呼萬喚始出來,主角正式登場了!

可以這麼說,上面寫到的所有規範規則,都是為了更好的設計和實現這個主角 ——— CI/CD。

首先了解一下,什麼是 CI/CD ?

核心概念,CI(Continuous Integration)譯為持續整合,CD 包括兩部分,持續交付(Continuous Delivery)和持續部署(Continuous Deployment)

從全域性看,CI/CD 是一種通過自動化流程來頻繁向客戶交付應用的方法。這個流程貫穿了應用的整合,測試,交付和部署的整個生命週期,統稱為 “CI/CD 管道”。

雖然都是像流水線一樣自動化的管道,但是 CI 和 CD 各有分工。

持續整合是頻繁地將程式碼整合到主幹分支。當新程式碼提交,會自動執行構建、測試,測試通過則自動合併到主幹分支,實現了產品快速迭代的同時保持高質量。

持續交付是頻繁地將軟體的新版本,交付給質量團隊或者使用者,以供評審。評審通過則可以釋出生產環境。持續交付要求程式碼(某個分支的最新提交)是隨時可釋出的狀態。

持續部署是程式碼通過評審後,自動部署到生產環境。持續部署要求程式碼(某個分支的最新提交)是隨時可部署的。

持續部署與持續交付的唯一區別,就是部署到生產環境這一步,是否是自動化

部署自動化,看似是小小的一步,但是在實踐過程中你會發現,這反而是 CI/CD 流水線中最難落實的一環。

為什麼?首先,從持續整合到持續交付,這些個環節都是由開發團隊實施的。我們通過團隊內部協作,產出了新版本的待發布的應用。

然而將應用部署到伺服器,這是運維團隊的工作。我們要實現部署,就要與運維團隊溝通,然而開發同學不瞭解伺服器,運維同學不瞭解程式碼,溝通起來困難重重。

再有,運維是手動部署,我們要實現自動部署,就要有伺服器許可權,與伺服器互動。這也是個大問題,因為運維團隊一定會顧慮安全問題,因而推動起來節節受阻。

目前社群成熟的 CI/CD 方案有很多,比如老牌的 jenkins,react 使用的 circleci,還有我認為最好用的GitHub Action等,我們可以將這些方案接入到自己的系統當中。

這篇文章篇幅已經很長了,就到這裡結束吧。接下來我會基於 GitHub Action 單獨出一篇詳細的 react 前端專案 CI/CD 實踐,記得關注我的專欄哦。

更多精彩

這個專欄會長期輸出前端工程與架構方向的文章,已釋出如下:

如果喜歡我的文章,請點贊支援我吧!也歡迎關注我的專欄,感謝🙏🙏