語義化版本控制模組-Semver

我很可愛你信不信發表於2019-03-04

執行某些命令的時候,你是否遇到過提醒版本過低,需要升級版本的提示,那麼對於版本號,是以一個怎樣的規則來進行的限制和匹配的呢?
semver, 是一個語義化版本號管理的模組,可以實現版本號的解析和比較,規範版本號的格式。

版本號的基本規則

結構

版本號一般有三個部分,以.隔開,就像X.Y.Z,其中

  • X:主版本號,不相容的大改動
  • Y:次版本號,功能性的改動
  • Z:修訂版本號,問題修復

每個部分為整數(>=0),按照遞增的規則改變。

在修訂版本號的後面可以加上其他資訊,用-連線,比如:

  • X.Y.Z-Alpha: 內測版
  • X.Y.Z-Beta: 公測版
  • X.Y.Z-Stable: 穩定版

範圍規則

package.json檔案中,我們所安裝的依賴,都會有版本號的描述,比如使用初始化的一個react工程,在它的package.json裡自動安裝的依賴

"devDependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
複製程式碼

其實我們平時看到的版本號,不止有^字首的,還有~,那麼他們代表的含義是什麼呢?

^: 允許在不修改[major, minor, patch]中最左非零數字的更改(匹配大於X、Y、Z的更新Y、Z的版本號)

X.Y.Z結構的版本號中,X、Y、Z都是非負的整數,上面定義的意思就是說從左向右,遇到第一個非零數字是不可修改的,下一個數字可以更改,比如:

  • X、Y、Z都不為0,^15.6.1",最左的非零數字是15,所以X是不允許更新的,也就是說主版本號不會超過15,表示的就是版本號>=15.6.1 && <16.0.0
  • 如果X為0,那麼第一個非零數字就是Y,就只能對z做出修改,^0.1.2表示版本號>=0.1.2 && < 0.2.0
  • 如果X、Y的數字都是0的話,第一個非零數字就是Z,表示的就是版本號不允許更新;^0.0.2,主版本號和次版本號都是0,修訂號為非零,表示的就是版本號>=0.0.2 && < 0.0.3

~: 匹配大於X.Y.Z的更新Z的版本號

  • X、Y、Z都不為0,~1.2.3表示版本號>=1.2.3 && < 1.3.0
  • X為0,~0.2.3表示版本號>=0.2.3 && < 0.3.0,這種情況下,~等價於^
  • X、Y為0,0.0.3表示版本號>=0.0.3 && < 0.1.0

x: 可以替代X、Y、Z中任意一個,表示該位置可更新

  • 1.2.x: >=1.2.0 && < 1.3.0
  • 1.x: >=1.0.0 && < 2.0.0
  • *: 任意版本都可以

上面的x可以用*代替,其實,用x*的地方可以省略不寫,比如1.2.x1.2表示的意思是一樣的

-:包含第一個版本號和第二個版本號的範圍
表示的是一個閉區間,-連線的兩個版本號範圍都包括

  • 0.1.02: >=0.1.0 && < 3.0.0
  • 0.1.02.1.1: >=0.1.0 && <= 2.1.1

安裝

npm install semver
複製程式碼

用法

// 引入模組
const semver = require(`semver`)
 
semver.clean(` =v1.1.1 `);// 1.1.1,解析版本號,忽略版本號前面的符號
 
semver.valid(`1.1.1`); // true,版本號是否合法
semver.valid(`a.b.c`); // false
 
semver.satisfies(`1.2.4`, `1.2.3 - 1.2.5`); // true, 判斷版本是否在某個範圍
複製程式碼

這裡只列舉了部分用法,具體的可以在文件中檢視。

實現原理

看了semver的原始碼,整理了部分方法的實現原理

clean

...
exports.clean = clean;
function clean(version, loose) {
  // 替換引數中的空格和符號
  var s = parse(version.trim().replace(/^[=v]+/, ``), loose);
  return s ? s.version : null;
}
...
複製程式碼

valid

...
exports.valid = valid;
function valid(version, loose) {
  var v = parse(version, loose);
  return v ? v.version : null;
}
...
複製程式碼

cleanvalid都用到了一個方法parse,這個方法是用來對版本號進行解析檢查是否規範,最後返回一個規範的格式

parse

對版本號的格式進行解析,判斷是否合法,這個方法在很多方法的實現裡面都用到了

exports.parse = parse;
 
function parse(version, loose) {
  if (version instanceof SemVer)
    return version;
 
  if (typeof version !== `string`)
    return null;
 
  if (version.length > MAX_LENGTH)
    return null;
    
  // 是否應用寬鬆模式
  var r = loose ? re[LOOSE] : re[FULL];
  if (!r.test(version))
    return null;
 
  try {
    return new SemVer(version, loose);
  } catch (er) {
    return null;
  }
}
 
/* 
* 引數中的loose表示是否寬鬆檢查版本號
* loose為true的時候,檢查版本號的格式不會那麼嚴格
* 比如定義數字識別符號,就定義了一種寬鬆的匹配模式
* /
 
// ## Numeric Identifier
// A single `0`, or a non-zero digit followed by zero or more digits.
 
var NUMERICIDENTIFIER = R++;
src[NUMERICIDENTIFIER] = `0|[1-9]\d*`;  // 單個0或者0後面跟著0個或多個不為0的數字
var NUMERICIDENTIFIERLOOSE = R++;
src[NUMERICIDENTIFIERLOOSE] = `[0-9]+`; // 0-9的1位或多位數字
 
複製程式碼

satisfies

exports.satisfies = satisfies;
function satisfies(version, range, loose) {
  try {
    // Range會判斷輸入的範圍是否合法,並返回一個格式化之後的range
    range = new Range(range, loose);
  } catch (er) {
    return false;
  }
  return range.test(version);
}

複製程式碼

satisfies呼叫了Range,用於對使用者輸入的範圍進行規範化

exports.Range = Range;
function Range(range, loose) {
  if (range instanceof Range) {
    if (range.loose === loose) {
      return range;
    } else {
      return new Range(range.raw, loose);
    }
  }
  if (range instanceof Comparator) {
    return new Range(range.value, loose);
  }

  if (!(this instanceof Range))
    return new Range(range, loose);

  this.loose = loose;
  
  /*
  * 將範圍按照‘||’分開
  * 對每個範圍進行解析,並且過濾出沒有意義的範圍
  */
  // First, split based on boolean or ||
  this.raw = range;
  // 用split將輸入的範圍劃分成陣列
  this.set = range.split(/s*||s*/).map(function(range) {
    // 對陣列的每一項進行解析
    return this.parseRange(range.trim());
  }, this).filter(function(c) {
    // throw out any that are not relevant for whatever reason
    return c.length;
  });
 
  if (!this.set.length) {
    throw new TypeError(`Invalid SemVer Range: ` + range);
  }
 
  this.format();
}
複製程式碼
/*
* 對使用者輸入的範圍進行解析檢驗,返回規範的格式
*/
Range.prototype.parseRange = function(range) {
  var loose = this.loose;
  range = range.trim();  // 去掉前後的空格
  debug(`range`, range, loose);
  
  // 判斷是否是寬鬆模式,並應用‘連字元’的正則去匹配替換
  // 將連字元的形式替換成比較符號的形式,`1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4
  var hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE];
  range = range.replace(hr, hyphenReplace);
  debug(`hyphen replace`, range);
 
  // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
  range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace);
  debug(`comparator trim`, range, re[COMPARATORTRIM]);
 
  // `~ 1.2.3` => `~1.2.3`
  range = range.replace(re[TILDETRIM], tildeTrimReplace);
 
  // `^ 1.2.3` => `^1.2.3`
  range = range.replace(re[CARETTRIM], caretTrimReplace);
 
  // 將表示範圍的字串的多個空格替換成一個空格
  range = range.split(/s+/).join(` `);
 
  // At this point, the range is completely trimmed and
  // ready to be split into comparators.
 
  var compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR];
  // 將表示範圍的字串按照空格劃分為陣列,對每一個陣列向進行解析檢驗,返回規範的表示並重新連線成字串
  var set = range.split(` `).map(function(comp) {
    return parseComparator(comp, loose);
  }).join(` `).split(/s+/);
  if (this.loose) {
    // 在寬鬆模式下,過濾掉所有不合法的比較器
    set = set.filter(function(comp) 
      return !!comp.match(compRe);
    });
  }
  set = set.map(function(comp) {
    return new Comparator(comp, loose);
  });
 
  return set;
};
 
/**
* 將規範後的範圍字串重新連線起來並返回
*/
Range.prototype.format = function() {
  this.range = this.set.map(function(comps) {
    return comps.join(` `).trim();
  }).join(`||`).trim();
  return this.range;
};
複製程式碼

參考:semver文件
semver原始碼地址

相關文章