包版本控制
Pub 包管理器幫助你處理版本控制。本指南解釋了版本控制的歷史以及 Pub 的版本控制方法。
這被認為是進階資訊。要了解 Pub 為何被設計成現在這樣,請繼續閱讀。如果你想使用 Pub,請參考其他文件。
現代軟體開發,特別是 Web 開發,在很大程度上依賴於大量現有程式碼的重用。這包括你過去編寫的程式碼,但也包括第三方程式碼,從大型框架到小型實用庫。一個應用程式依賴於幾十個不同的包和庫是很常見的。
這種力量的強大之處怎麼強調都不為過。當你看到一些小型 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` 將該 dictionary 傳送給 `templates`。
- `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`。
約束上下文
#選擇包版本會考慮*所有*依賴於它的包這一事實具有重要意義:*為包選擇的具體版本是使用該包的應用的全域性屬性。*
下面的示例展示了這意味著什麼。假設我們有兩個應用。這是它們的 pubspecs:
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` 本身。有趣的是,它對 `collection` 的版本約束與 `widgets` 不同。
這意味著你不能僅僅孤立地檢視 `widgets` 包來確定它將使用哪個版本的 `collection`。這取決於上下文。在 `my_app` 中,`widgets` 將使用 `collection 1.9.9`。但在 `other_app` 中,由於 `other_app` 對它施加的*另一個*約束,`widgets` 將不得不使用 `collection 1.4.9`。
這就是為什麼每個應用都有自己的 `package_config.json` 檔案:為每個包選擇的具體版本取決於包含該應用的整個依賴圖。
匯出依賴的約束求解
#包作者必須謹慎地定義包約束。考慮以下場景:

`bookshelf` 包依賴於 `widgets`。`widgets` 包,當前版本為 1.2.0,透過 `export 'package:collection/collection.dart'` 匯出了 `collection`,並且 `collection` 的版本為 2.4.0。pubspec 檔案如下:
name: bookshelf
dependencies:
widgets: ^1.2.0name: 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。當有人釋出 `collection` 的 2.5.0 版本時,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 文章。