套件版本控制
Pub 套件管理器可協助您處理版本控制。本指南簡要說明版本控制的歷史以及 pub 的方法。
請將此視為進階資訊。若要瞭解 pub 的設計原因,請繼續閱讀。如果您想使用 pub,請參閱其他文件。
現代軟體開發,尤其是 Web 開發,非常仰賴重複使用大量現有程式碼。這包括您過去編寫的程式碼,以及來自第三方的程式碼,從大型框架到小型工具函式庫。應用程式依賴數十個不同的套件和函式庫並不罕見。
這有多強大,實在難以言喻。當您看到小型網路新創公司在幾週內建立一個擁有數百萬使用者的網站的故事時,他們能夠實現這一目標的唯一原因是因為開源社群為他們準備了豐盛的軟體饗宴。
但這並非沒有代價:程式碼重複使用存在挑戰,尤其是重複使用您不維護的程式碼。當您的應用程式使用其他人開發的程式碼時,如果他們變更程式碼會發生什麼事?他們不想破壞您的應用程式,您當然也不想。我們透過版本控制來解決這個問題。
名稱和數字
#當您依賴某些外部程式碼時,您不會只說「我的應用程式使用 widgets」。您會說「我的應用程式使用 widgets 2.0.5」。名稱和版本號碼的組合唯一地識別了不可變的程式碼區塊。更新 widgets 的人員可以進行他們想要的所有變更,但他們承諾不會觸及任何已發佈的版本。他們可以發佈 2.0.6 或 3.0.0,這不會對您產生任何影響,因為您使用的版本沒有變更。
當您確實想要取得這些變更時,您可以隨時將您的應用程式指向較新版本的 widgets,而且您無需與這些開發人員協調即可完成。但是,這並不能完全解決問題。
本指南中討論的版本號碼可能與套件檔案名稱中設定的版本號碼不同。它們可能包含 -0 或 -beta。這些標記不會影響相依性解析。
解析共用相依性
#當您的相依性圖表實際上只是一個相依性樹狀結構時,依賴特定版本可以正常運作。如果您的應用程式依賴許多套件,而這些套件又具有自己的相依性等等,那麼只要這些相依性都不重疊,一切都運作良好。
考慮以下範例

所以您的應用程式使用 widgets 和 templates,而它們都使用 collection。這稱為共用相依性。現在,當 widgets 想要使用 collection 2.3.5 而 templates 想要 collection 2.3.7 時會發生什麼事?如果它們對版本沒有共識呢?
非共用函式庫 (npm 方法)
#一種選擇是讓應用程式使用兩個版本的 collection。它將擁有兩個不同版本的函式庫副本,而 widgets 和 templates 將各自取得它們想要的版本。
這是 npm 為 node.js 所做的事情。這適用於 Dart 嗎?考慮以下情境
- collection 定義了一些 Dictionary 類別。
- widgets 從其 collection 副本 (2.3.5) 取得它的實例。然後,它將其傳遞給 my_app。
- my_app 將字典傳送給 templates。
- 然後,它將其傳送給其版本的 collection (2.3.7)。
- 接受它的方法具有該物件的 Dictionary 類型註解。
就 Dart 而言,collection 2.3.5 和 collection 2.3.7 是完全不相關的函式庫。如果您從一個函式庫中取得 Dictionary 類別的實例,並將其傳遞給另一個函式庫中的方法,那麼這是一個完全不同的 Dictionary 類型。這表示它將無法與接收函式庫中的 Dictionary 類型註解匹配。糟糕。
由於這個原因(以及嘗試除錯具有相同名稱的多個版本應用程式的麻煩),我們認為 npm 的模型不太適合。
版本鎖定 (死路方法)
#相反地,當您依賴一個套件時,您的應用程式只會使用該套件的單一副本。當您有共用相依性時,所有依賴它的項目都必須同意使用哪個版本。如果它們不同意,您就會收到錯誤。
但這實際上並沒有解決您的問題。當您確實收到該錯誤時,您需要能夠解決它。假設您已讓自己陷入先前範例中的情況。您想要使用 widgets 和 templates,但它們正在使用不同版本的 collection。您該怎麼辦?
答案是嘗試升級其中一個。templates 想要 collection 2.3.7。是否有您可以升級到的 widgets 後續版本,以便與該版本搭配使用?
在許多情況下,答案將是「否」。從開發 widgets 的人員的角度來看。他們想要發佈一個新版本,其中包含對其程式碼的新變更,並且他們希望盡可能多的人能夠升級到該版本。如果他們堅持使用當前版本的 collection,那麼任何使用當前版本 widgets 的人都將能夠加入這個新版本。
如果他們要升級他們對 collection 的相依性,那麼每個升級 widgets 的人都必須升級,無論他們是否願意。這很痛苦,因此最終會導致升級相依性的意願降低。這稱為版本鎖定:每個人都想向前推進他們的相依性,但沒有人可以邁出第一步,因為這也會迫使其他人也這樣做。
版本約束 (Dart 方法)
#為了解決版本鎖定問題,我們放寬了套件對其相依性的約束。如果 widgets 和 templates 都可以指出它們可以使用的 collection 版本範圍,那麼這就為我們提供了足夠的迴旋空間,將我們的相依性向前推進到較新版本。只要它們的範圍有重疊,我們仍然可以找到一個讓它們都滿意的單一版本。
這是 bundler 遵循的模型,也是 pub 的模型。當您在 pubspec 中新增相依性時,您可以指定您可以接受的版本範圍。如果 widgets 的 pubspec 如下所示
dependencies:
collection: '>=2.3.5 <2.4.0'
您可以為 collection 選擇 2.3.7 版本。單一具體版本將滿足 widgets 和 templates 套件的約束。
語意化版本
#當您將相依性新增至您的套件時,有時您會想要指定允許的版本範圍。您如何知道要選擇哪個範圍?您需要向前相容,因此理想情況下,範圍應包含尚未發佈的未來版本。但是您如何知道您的套件將與某些甚至還不存在的新版本搭配使用?
為了解決這個問題,您需要就版本號碼的意義達成共識。想像一下,您依賴的套件的開發人員說:「如果我們進行任何向後不相容的變更,那麼我們承諾會增加主要版本號碼。」如果您信任他們,那麼如果您知道您的套件與他們的 2.3.5 版本搭配使用,您可以依靠它一直運作到 3.0.0。您可以將您的範圍設定為
dependencies:
collection: ^2.3.5
為了讓這項機制運作,我們需要提出這組承諾。幸運的是,其他聰明人已經完成了弄清楚這一切的工作,並將其命名為語意化版本控制。
它描述了版本號碼的格式,以及當您遞增到較新的版本號碼時的確切 API 行為差異。Pub 要求版本以這種方式格式化,並且為了與 pub 社群良好互動,您的套件應遵循其指定的語意。您應該假設您依賴的套件也遵循它。(如果您發現它們沒有遵循,請告知其作者!)
雖然語意化版本控制不保證 1.0.0 之前的版本之間有任何相容性,但 Dart 社群慣例是也以語意方式處理這些版本。每個數字的解釋只是向下移動一個位置:從 0.1.2 變更為 0.2.0 表示重大變更,變更為 0.1.3 表示新功能,變更為 0.1.2+1 表示不影響公開 API 的變更。為了簡單起見,請避免在版本達到 1.0.0 後使用 +
。
現在我們幾乎擁有了處理版本控制和 API 演進所需的所有部分。讓我們看看它們如何協同運作以及 pub 的作用。
約束求解
#當您定義套件時,您會列出其直接相依性。這些是您的套件使用的套件。對於每個套件,您指定您的套件允許的版本範圍。然後,每個相依套件都可能具有自己的相依性。這些稱為遞移相依性。Pub 會遍歷這些相依性,並為您的應用程式建立完整的相依性圖表。
對於圖表中的每個套件,pub 都會查看所有依賴它的項目。它會收集所有這些項目的版本約束,並嘗試同時解決它們。基本上,它會交叉它們的範圍。然後,pub 會查看已發佈的該套件的實際版本,並選擇符合所有這些約束的最新版本。
例如,假設我們的相依性圖表包含 collection,並且有三個套件依賴它。它們的版本約束是
>=1.7.0
^1.4.0
<1.9.0
collection 的開發人員已發佈這些版本
1.7.0
1.7.1
1.8.0
1.8.1
1.8.2
1.9.0
符合所有這些範圍的最高版本號碼是 1.8.2,因此 pub 會選擇它。這表示您的應用程式以及您的應用程式使用的每個套件都將使用 collection 1.8.2。
約束上下文
#選擇套件版本時會考慮到每個依賴它的套件,這個事實有一個重要的後果:將為套件選擇的特定版本是使用該套件的應用程式的整體屬性。
以下範例說明了這意味著什麼。假設我們有兩個應用程式。以下是它們的 pubspec
name: my_app
dependencies:
widgets:
name: other_app
dependencies:
widgets:
collection: '<1.5.0'
它們都依賴 widgets,其 pubspec 為
name: widgets
dependencies:
collection: '>=1.0.0 <2.0.0'
other_app 套件直接依賴 collection 本身。有趣的部分是,它碰巧對它的版本約束與 widgets 不同。
這表示您不能僅僅孤立地查看 widgets 套件來判斷它將使用哪個版本的 collection。這取決於上下文。在 my_app 中,widgets 將使用 collection 1.9.9。但在 other_app 中,widgets 將因為 otherapp 對其施加的其他約束而背負 collection 1.4.9。
這就是為什麼每個應用程式都有自己的 package_config.json
檔案的原因:為每個套件選擇的具體版本取決於包含應用程式的整個相依性圖表。
匯出相依性的約束求解
#套件作者必須謹慎定義套件約束。考慮以下情境

bookshelf 套件依賴 widgets。widgets 套件目前為 1.2.0,透過 export 'package:collection/collection.dart'
匯出 collection,版本為 2.4.0。pubspec 檔案如下所示
name: bookshelf
dependencies:
widgets: ^1.2.0
name: widgets
dependencies:
collection: ^2.4.0
然後,collection 套件更新為 2.5.0。2.5.0 版本的 collection 包含一個名為 sortBackwards()
的新方法。bookshelf 可以呼叫 sortBackwards()
,因為它是 widgets 公開的 API 的一部分,儘管 bookshelf 僅對 collection 具有遞移相依性。
由於 widgets 具有未在其版本號碼中反映的 API,因此使用 bookshelf 套件並呼叫 sortBackwards()
的應用程式可能會崩潰。
匯出 API 會導致該 API 被視為在套件本身中定義,但當 API 新增功能時,它無法增加版本號碼。這表示 bookshelf 無法宣告它需要支援 sortBackwards()
的 widgets 版本。
因此,在處理匯出套件時,建議套件作者對相依性的上限和下限保持更嚴格的限制。在這種情況下,widgets 套件的範圍應縮小
name: bookshelf
dependencies:
widgets: '>=1.2.0 <1.3.0'
name: widgets
dependencies:
collection: '>=2.4.0 <2.5.0'
這表示 widgets 的下限為 1.2.0,collection 的下限為 2.4.0。當有人發佈 2.5.0 版本的 collection 時,pub 會將 widgets 更新為 1.3.0,並更新相應的約束。
即使其中一個不是直接相依性,使用此慣例也能確保使用者擁有兩個套件的正確版本。
鎖定檔
#那麼,一旦 pub 解決了您的應用程式的版本約束,接下來會發生什麼?最終結果是您的應用程式直接或間接依賴的每個套件的完整清單,以及適用於您的應用程式約束的該套件的最佳版本。
對於每個套件,pub 都會取得該資訊,從中計算內容雜湊,並將兩者寫入您應用程式目錄中名為 pubspec.lock
的鎖定檔。當 pub 為您的應用程式建立 .dart_tool/package_config.json
檔案時,它會使用鎖定檔來知道要參照每個套件的哪個版本。(如果您想知道它選擇了哪些版本,您可以閱讀鎖定檔來找出答案。)
pub 接下來要做的重要事情是停止觸碰鎖定檔。一旦您為您的應用程式取得鎖定檔,pub 就不會觸碰它,直到您告訴它為止。這很重要。這表示您不會在沒有意圖的情況下自發地開始在您的應用程式中使用隨機套件的新版本。一旦您的應用程式被鎖定,它就會保持鎖定狀態,直到您手動告訴它更新鎖定檔。
如果您的套件適用於應用程式,請將您的鎖定檔簽入您的原始碼控制系統!這樣,您團隊中的每個人在建置您的應用程式時都將使用完全相同的每個相依性版本。您也可以在部署您的應用程式時使用它,以確保您的生產伺服器使用的套件與您開發時使用的套件完全相同。
出錯時
#當然,所有這些都假設您的相依性圖表是完美無瑕的。即使使用版本範圍以及 pub 的約束求解和語意化版本控制,您也永遠無法完全免除版本災難的危險。
您可能會遇到以下問題之一
您可能會遇到不相交的約束
#假設您的應用程式使用 widgets 和 templates,並且兩者都使用 collection。但是 widgets 要求它的版本介於 1.0.0 和 2.0.0 之間,而 templates 想要介於 3.0.0 和 4.0.0 之間。這些範圍甚至沒有重疊。沒有任何可能的版本可以運作。
您可能會遇到範圍內沒有已發佈版本的情況
#假設在將所有約束放在共用相依性上之後,您有一個狹窄的範圍 >=1.2.4 <1.2.6
。這不是一個空範圍。如果相依性有 1.2.4 版本,您就會很順利。但也許他們從未發佈該版本。相反,他們直接從 1.2.3 跳到 1.3.0。您有一個範圍,但裡面什麼都沒有。
您可能會遇到不穩定的圖表
#到目前為止,這是 pub 版本求解過程中最具挑戰性的部分。該過程被描述為建立相依性圖表,然後解決所有約束並選擇版本。但它實際上並非如此運作。在您選擇任何版本之前,您如何建立整個相依性圖表?pubspec 本身是版本特定的。同一套件的不同版本可能具有不同的相依性集。
當您選擇套件版本時,它們正在改變相依性圖表本身的形狀。隨著圖表的變更,這可能會改變約束,這可能會導致您選擇不同的版本,然後您又會繞回原點。
有時這個過程永遠不會穩定下來並形成穩定的解決方案。凝視深淵
name: my_app
version: 0.0.0
dependencies:
yin: '>=1.0.0'
name: yin
version: 1.0.0
dependencies:
name: yin
version: 2.0.0
dependencies:
yang: '1.0.0'
name: yang
version: 1.0.0
dependencies:
yin: '1.0.0'
在所有這些情況下,都沒有一組具體的版本適用於您的應用程式,當這種情況發生時,pub 會報告錯誤並告訴您發生了什麼事。它絕對不會讓您處於某種奇怪的狀態,讓您認為事情可以運作但實際上不會。
摘要
#總而言之
- 雖然程式碼重複使用有優點,但套件需要獨立發展的能力。
- 版本控制使這種獨立性成為可能。依賴單一具體版本缺乏彈性。再加上共用相依性,就會導致版本鎖定。
- 為了應對版本鎖定,您的套件應依賴版本範圍。然後,Pub 會遍歷您的相依性圖表,並為您選擇最佳版本。如果 Pub 無法選擇合適的版本,它會提醒您。
- 一旦您的應用程式為其相依性設定了一組穩定的版本,該組版本就會固定在鎖定檔中。這可確保執行您應用程式的每部機器都使用所有相依性的相同版本。
若要瞭解更多關於 pub 的版本求解演算法,請參閱 Medium 上的 PubGrub 文章。
除非另有說明,否則本網站上的文件反映的是 Dart 3.7.1 版本。頁面最後更新於 2024-11-18。 檢視原始碼 或 回報問題。