語義化版本控制模塊-Semver

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

版本號的基本規則

結構

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

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

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

在修訂版本號的後面能夠加上其餘信息,用-鏈接,好比:github

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

範圍規則

package.json文件中,咱們所安裝的依賴,都會有版本號的描述,好比使用初始化的一個react工程,在它的package.json裏自動安裝的依賴npm

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

其實咱們平時看到的版本號,不止有^前綴的,還有~,那麼他們表明的含義是什麼呢?json

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

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

  • 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的版本號dom

  • 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中任意一個,表示該位置可更新ui

  • 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.0-2: >=0.1.0 && < 3.0.0
  • 0.1.0- 2.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源碼地址

相關文章
相關標籤/搜索