執行某些命令的時候,你是否遇到過提醒版本過低,需要升級版本的提示,那麼對於版本號,是以一個怎樣的規則來進行的限制和匹配的呢?
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.x
和1.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;
}
...
複製程式碼
clean
和valid
都用到了一個方法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;
};
複製程式碼