本文從前端工程,團隊協作,生產部署的角度,介紹架構人員需要掌握的 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?
release 譯為變基,合併同樣不會產生分叉。當 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
:常用:表示修復 bugperf
:效能優化refactor
:重構revert
:程式碼回滾style
:樣式更改test
:單元測試更改
這些字首每次提交都要寫,剛開始很多人還是記不住的。這裡推薦一個非常好用的工具,可以自動生成字首。地址在這裡
首先全域性安裝:
npm install -g commitizen cz-conventional-changelog
建立 ~/.czrc
檔案,寫入如下內容:
{ "path": "cz-conventional-changelog" }
現在可以用 git cz
命令來代替 git commit
命令,效果如下:
然後上下箭選擇字首,根據提示即可方便的建立符合規範的提交。
有了規範之後,光靠人的自覺遵守是不行的,還要在流程上對提交資訊進行校驗。
這個時候,我們要用到一個新東西 —— 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 支援絕大多數場景的撤回方案,我們來總結一下。
撤回主要是兩個命令:reset
和 revert
git reset
reset 命令的原理是根據 commitId
來恢復版本。因為每次提交都會生成一個 commitId,所以說 reset 可以幫你恢復到歷史的任何一個版本。
這裡的版本和提交是一個意思,一個 commitId 就是一個版本
reset 命令格式如下:
$ git reset [option] [commitId]
比如,要撤回到某一次提交,命令是這樣:
$ 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 還提供了恢復到上一次提交的快捷方式:
$ git reset --soft HEAD^
HEAD^
表示上一個提交,可多次使用。
其實平日開發中最多的誤操作是這樣:剛剛提交完,突然發現了問題,比如提交資訊沒寫好,或者程式碼更改有遺漏,這時需要撤回到上次提交,修改程式碼,然後重新提交。
這個流程大致是這樣的:
# 1. 回退到上次提交
$ git reset HEAD^
# 2. 修改程式碼...
...
# 3. 加入暫存
$ git add .
# 4. 重新提交
$ git commit -m 'fix: ***'
針對這個流程,git 還提供了一個更便捷的方法:
$ 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
引數的問題,提高了安全性。
說完了原理,我們再看一下使用方法:
$ git revert -n [commitId]
掌握了原理使用就很簡單,只要一個 commitId 就可以了。
Tag 與生產環境
git 支援對於歷史的某個提交,打一個 tag 標籤,常用於標識重要的版本更新。
目前普遍的做法是,用 tag 來表示生產環境的版本。當最新的提交通過測試,準備釋出之時,我們就可以建立一個 tag,表示要釋出的生產環境版本。
比如我要發一個 v1.2.4
的版本:
$ git tag -a v1.2.4 -m "my version 1.2.4"
然後可以檢視:
$ git show v1.2.4
> tag v1.2.4
Tagger: ruims <2218466341@qq.com>
Date: Sun Sep 26 10:24:30 2021 +0800
my version 1.2.4
最後用 git push 將 tag 推到遠端:
$ git push origin v1.2.4
這裡注意:tag 和在哪個分支建立是沒有關係的,tag 只是提交的別名。因此 commit 的能力 tag 均可使用,比如上面說的 git reset
,git revert
命令。
當生產環境出問題,需要版本回退時,可以這樣:
$ 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 協議克隆程式碼,例子如下:
$ git clone git@github.com:[organi-name]/[project-name]
發現瞬間克隆下來了!再測幾次 pull/push,速度飛起!
不管你用哪個程式碼管理平臺,如果遇到 443 Timeout 問題,請試試 ssh 協議!
hook 實現部署?
利用 git hook 實現部署,應該是 hook 的高階應用了。
現在有很多工具,比如 GitHub,GitLab,都提供了持續整合功能,也就是監聽某一分支推送,然後觸發自動構建,並自動部署。
其實,不管這些工具有多少花樣,核心的功能(監聽和構建)還是由 git 提供。只不過在核心功能上做了與自家平臺更好的融合。
我們今天就拋開這些工具,追本溯源,使用純 git 實現一個 react 專案的自動部署。掌握了這套核心邏輯,其他任何平臺的持續部署也就沒那麼神祕了。
由於這一部分內容較多,所以單獨拆出去一篇文章,地址如下:
終極應用: 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 實踐,記得關注我的專欄哦。
我的專欄:前端devops