內容

封裝版本控制

pub 封裝管理員 的主要工作之一是協助您處理版本控制。本文檔說明了版本控制的歷史以及 pub 對它的方法。請將這視為進階資訊。如果您想要更清楚了解 pub 設計方式的原因,請繼續閱讀。如果您只想使用 pub,其他文件 會更適合您。

現代軟體開發,尤其是網頁開發,極度仰賴重複使用大量現有程式碼。這包括你過去編寫的程式碼,還有第三方程式碼,從大型架構到小型公用程式庫,應有盡有。應用程式仰賴數十種不同的套件和程式庫並非罕見。

很難低估這有多麼強大。當你看到小型網頁新創公司在幾週內建置一個網站,並獲得數百萬使用者,他們之所以能達成此目標,唯一的原因是開放原始碼社群為他們提供了豐富的軟體資源。

但這並非免費:程式碼重複使用存在挑戰,尤其是重複使用你未維護的程式碼。當你的應用程式使用其他人開發的程式碼時,如果他們變更程式碼,會發生什麼事?他們不希望損壞你的應用程式,你當然也不希望。我們透過版本控制解決這個問題。

名稱和數字

#

當你仰賴某段外部程式碼時,你不會只說「我的應用程式使用小工具。」你會說「我的應用程式使用小工具 2.0.5。」名稱和版本號碼的組合會唯一識別一段不可變的程式碼區塊。更新小工具的人員可以進行他們想要的任何變更,但他們承諾不會變更任何已發布的版本。他們可以發布2.0.63.0.0,而這不會對你造成任何影響,因為你使用的版本並未變更。

當你確實想要取得那些變更時,你隨時可以將你的應用程式指向小工具的較新版本,而你不需要與那些開發人員協調即可執行此操作。不過,這並未完全解決問題。

解決共用相依性

#

當你的相依性圖形實際上只是一個相依性樹狀結構時,依賴特定版本才能順利運作。如果你的應用程式依賴一堆套件,而這些套件又各自有自己的相依性,只要這些相依性都不重疊,一切都能順利運作。

但請考慮以下範例

dependency graph

因此,你的應用程式使用小工具範本,而這兩個都使用集合。這稱為共用相依性。現在,如果小工具想要使用集合 2.3.5,而範本想要使用集合 2.3.7,會發生什麼事?如果他們無法就版本達成共識呢?

未共用函式庫(npm 方法)

#

一種選擇是讓應用程式同時使用兩個版本的集合。它將擁有兩個不同版本的程式庫副本,而小工具範本各自會取得他們想要的版本。

這是 npm 對 node.js 執行的動作。它是否適用於 Dart?請考慮以下情況

  1. collection 定義了一些 Dictionary 類別。
  2. widgets 從其 collection 複本(2.3.5)取得它的執行個體。然後傳遞給 my_app
  3. my_app 將字典傳送給 templates
  4. 它反過來將其傳送給 自己的 collection 版本(2.3.7)。
  5. 接收它的方法針對該物件具有 Dictionary 類型註解。

就 Dart 而言,collection 2.3.5collection 2.3.7 是完全不相關的函式庫。如果您從其中一個函式庫取得 Dictionary 類別的執行個體,並將其傳遞給另一個函式庫中的方法,那就是完全不同的 Dictionary 類型。這表示它無法符合接收函式庫中的 Dictionary 類型註解。糟糕。

由於這個原因(以及嘗試除錯具有多個相同名稱版本的應用程式的麻煩),我們決定 npm 的模型不適合。

版本鎖定(死胡同方法)

#

相反地,當您依賴套件時,您的應用程式只會使用該套件的單一複本。當您有共用依賴項時,所有依賴它的項目都必須同意要使用的版本。如果它們不同意,您就會收到錯誤訊息。

不過,這並未真正解決您的問題。當您收到該錯誤訊息時,您需要能夠解決它。因此,讓我們說您已在先前的範例中遇到這種情況。您想要使用 widgetstemplates,但它們使用不同的 collection 版本。您該怎麼辦?

答案是嘗試升級其中一個。templates 需要 collection 2.3.7。是否有您可以升級到與該版本相容的 widgets 後續版本?

在許多情況下,答案將會是「沒有」。從開發 widgets 的人員角度來看。他們想要推出具有對程式碼的新變更的新版本,並且他們希望盡可能多的人能夠升級到該版本。如果他們堅持使用其目前的 collection 版本,那麼使用目前版本 widgets 的任何人都可以安裝這個新版本。

如果他們要升級對 collection依賴項,那麼升級 widgets 的每個人都必須升級,無論他們是否願意。這很痛苦,因此您最終會失去升級依賴項的誘因。這稱為版本鎖定:每個人都希望向前推進他們的依賴項,但沒有人可以採取第一步,因為它會強迫其他人也這麼做。

版本限制(Dart 方法)

#

為了解決版本鎖定,我們放寬套件對其依賴項的限制。如果 widgetstemplates 都可以指出它們與之相容的 collection 版本範圍,那麼這將為我們提供足夠的迴旋空間,將我們的依賴項向前推進到較新版本。只要它們的範圍有重疊,我們仍然可以找到一個讓它們都滿意的單一版本。

這是 bundler 所遵循的模型,也是 pub 的模型。在 pubspec 中新增相依性時,你可以指定可接受的版本範圍。如果 widgets 的 pubspec 如下所示

yaml
dependencies:
  collection: '>=2.3.5 <2.4.0'

那麼我們可以為 collection 選擇版本 2.3.7,然後 widgetstemplates 的限制都可以由單一具體版本滿足。

語意化版本

#

在套件中新增相依性時,有時你會想要指定允許的版本範圍。你如何得知要選擇哪個範圍?你需要向前相容,因此理想情況下,範圍應包含尚未發布的未來版本。但是,你如何得知你的套件將與某些甚至尚未存在的版本配合運作?

要解決這個問題,你需要同意版本號碼的含義。想像一下,你所依賴的套件的開發人員說:「如果我們進行任何向後不相容的變更,我們承諾會增加主要版本號碼。」如果你相信他們,那麼如果你知道你的套件與他們的 2.3.5 相容,你就可以依賴它一直運作到 3.0.0。你可以設定你的範圍,如下所示

yaml
dependencies:
  collection: ^2.3.5

因此,要讓這項功能運作,我們需要提出這組承諾。幸運的是,其他聰明的人已經完成找出所有這些內容的工作,並將其命名為 語意化版本控制

這描述了版本號碼的格式,以及當你增加到較新的版本號碼時,確切的 API 行為差異。Pub 要求版本以這種方式格式化,並且為了與 pub 社群順利互動,你的套件應遵循其指定的語意。你應該假設你所依賴的套件也遵循它。(如果你發現他們沒有遵循,請讓他們的作者知道!)

雖然語意版本控制不保證 1.0.0 之前的版本之間的任何相容性,但 Dart 社群慣例也將這些版本視為語意版本。每個數字的解釋只向下移動一個位置:從 0.1.20.2.0 表示重大變更,到 0.1.3 表示新功能,到 0.1.2+1 表示不影響公開 API 的變更。為簡化起見,請避免在版本達到 1.0.0 後使用 +

我們現在幾乎具備處理版本控制和 API 演進所需的所有部分。讓我們看看它們如何一起發揮作用,以及 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

yaml
name: my_app
dependencies:
  widgets:
yaml
name: other_app
dependencies:
  widgets:
  collection: '<1.5.0'

它們都相依於 widgets,其 pubspec 為

yaml
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 檔案的原因:為每個套件選擇的具體版本取決於包含應用程式的整個相依性圖表。

解決匯出的相依性限制

#

套件作者必須小心定義套件約束。請考慮下列情況

dependency graph

bookshelf 套件依賴於 widgets。目前為 1.2.0 的 widgets 套件透過 export 'package:collection/collection.dart' 匯出 collection,且為 2.4.0。pubspec 檔案如下

yaml
name: bookshelf
dependencies:
  widgets:  ^1.2.0
yaml
name: widgets
dependencies:
  collection:  ^2.4.0

然後 collection 套件更新為 2.5.0。2.5.0 版的 collection 包含一個稱為 sortBackwards() 的新方法。bookshelf 可以呼叫 sortBackwards(),因為它是 widgets 公開的 API 的一部分,儘管 bookshelfcollection 只有傳遞依賴關係。

由於 widgets 的 API 沒有反映在其版本號碼中,因此使用 bookshelf 套件並呼叫 sortBackwards() 的應用程式可能會崩潰。

匯出 API 會導致該 API 被視為定義在套件本身中,但當 API 新增功能時,它無法增加版本號碼。這表示 bookshelf 沒有辦法宣告它需要支援 sortBackwards()widgets 版本。

因此,在處理匯出的套件時,建議套件作者對依賴關係的上限和下限保持更嚴格的限制。在此情況下,widgets 套件的範圍應縮小

yaml
name: bookshelf
dependencies:
  widgets:  '>=1.2.0 <1.3.0'
yaml
name: widgets
dependencies:
  collection:  '>=2.4.0 <2.5.0'

這表示 widgets 的下限為 1.2.0,collection 的下限為 2.4.0。當 collection 的 2.5.0 版本發布時,widgets 也會更新為 1.3.0,且對應的約束也會更新。

使用此慣例可確保使用者擁有兩個套件的正確版本,即使其中一個不是直接依賴關係。

鎖定檔

#

因此,一旦 pub 解決了您應用程式的版本約束,接下來呢?最終結果是您的應用程式直接或間接依賴的每個套件的完整清單,以及與您的應用程式約束相符的最佳套件版本。

對於每個套件,pub 會取得該資訊,從中計算 內容雜湊,並將兩者寫入應用程式目錄中稱為 pubspec.lock鎖定檔。當 pub 為您的應用程式建立 .dart_tool/package_config.json 檔案時,它會使用鎖定檔來了解要參照每個套件的哪些版本。(如果您好奇它選取了哪些版本,您可以閱讀鎖定檔來找出答案。)

pub 執行的下一個重要事項是停止觸碰鎖定檔。一旦您為應用程式取得鎖定檔,pub 就會在您告訴它之前不觸碰它。這很重要。這表示您不會在應用程式中自發開始使用隨機套件的新版本。一旦您的應用程式被鎖定,它就會保持鎖定狀態,直到您手動告訴它更新鎖定檔為止。

如果您的套件是針對應用程式,您必須將鎖定檔提交到您的原始碼控制系統!這樣一來,您的團隊中的每個人在建置您的應用程式時,都會使用每個依賴關係的完全相同版本。您在部署應用程式時也會使用此鎖定檔,以便確保您的生產伺服器使用與您開發時完全相同的套件。

當事情出錯時

#

當然,這一切的前提是您的相依圖表完美無缺。即使使用版本範圍、pub 的約束求解和語意化版本控制,您也無法完全避免版本化的危險。

您可能會遇到以下問題之一

您可能有不相交的限制

#

假設您的應用程式使用 widgetstemplates,而這兩個都使用 collection。但 widgets 要求版本介於 1.0.02.0.0 之間,而 templates 則需要介於 3.0.04.0.0 之間。這些範圍甚至沒有重疊。沒有任何可能的版本可以適用。

您可能有不包含已發布版本的範圍

#

假設在對共用相依項套用所有約束後,您剩下 >=1.2.4 <1.2.6 這個狹窄的範圍。這不是一個空的範圍。如果相依項有版本 1.2.4,您就會得償所願。但他們可能從未發布過該版本,而是直接從 1.2.3 跳到 1.3.0。您有一個範圍,但範圍內沒有任何東西。

您可能有不穩定的圖形

#

這到目前為止是 pub 版本求解過程中最大的挑戰。此過程被描述為建立相依圖表,然後求解所有約束並選擇版本。但實際上並非如此。在您選擇任何版本之前,您如何建立整個相依圖表?pubspec 本身是特定於版本的。同一個套件的不同版本可能具有不同的相依項集合。

當您選擇套件版本時,它們會改變相依圖表本身的形狀。隨著圖表的改變,可能會改變約束,這可能會導致您選擇不同的版本,然後您又會回到原點。

有時候這個過程永遠無法穩定下來,形成一個穩定的解決方案。凝視深淵

yaml
name: my_app
version: 0.0.0
dependencies:
  yin: '>=1.0.0'
yaml
name: yin
version: 1.0.0
dependencies:
yaml
name: yin
version: 2.0.0
dependencies:
  yang: '1.0.0'
yaml
name: yang
version: 1.0.0
dependencies:
  yin: '1.0.0'

在所有這些情況下,沒有任何具體版本的組合可以適用於您的應用程式,當發生這種情況時,pub 會報告錯誤並告訴您發生了什麼事。它絕對不會讓您處於一種奇怪的狀態,讓您認為事情可以運作,但卻無法運作。

摘要

#

這是一大堆資訊,但重點如下

  • 程式碼重複使用很棒,但為了讓開發人員快速行動,套件需要能夠獨立演進。
  • 版本控制就是您啟用它的方式。但依賴於單一具體版本過於精確,而且會導致共用相依項產生版本鎖定。
  • 為了應對此情況,您依賴於範圍的版本。然後,Pub 會逐步了解您的依賴關係圖表,並為您挑選最佳版本。如果無法選出最佳版本,它會通知您。
  • 一旦您的應用程式為其依賴關係設定了一組穩定的版本,就會將其固定在一個鎖定檔中。這可確保您的應用程式所在的每部機器都使用其所有依賴關係的相同版本。

如果您想進一步了解 Pub 的版本解決演算法,請參閱文章 PubGrub:新一代版本解決方案。