咱們一般會把項目中使用的第三方依賴寫在 package.json 文件裏,而後使用 npm 、cnpm 或者 yarn 這些流行的依賴管理工具來幫咱們管理這些依賴。可是它們是如何管理這些依賴的、它們之間有什麼區別,若是出現了循環依賴應該怎麼解決。node
在回答上面幾個問題以前,先讓咱們瞭解下語義化版本規則。react
使用第三方依賴時,一般須要指定依賴的版本範圍,好比webpack
"dependencies": {
"antd": "3.1.2",
"react": "~16.0.1",
"redux": "^3.7.2",
"lodash": "*"
}
複製代碼
上面的 package.json 文件代表,項目中使用的 antd 的版本號是 3.1.2,可是 3.1.1 和 3.1.二、3.0.一、2.1.1 之間有什麼不一樣呢。語義化版本規則規定,版本格式爲:主版本號.次版本號.修訂號,而且版本號的遞增規則以下:git
主版本號的更新一般意味着大的修改和更新,升級主版本後可能會使你的程序報錯,所以升級主版本號需謹慎,可是這每每也會帶來更好的性能和體驗。次版本號的更新則一般意味着新增了某些特性,好比 antd 的版本從 3.1.1 升級到 3.1.2,以前的 Select 組件不支持搜索功能,升級以後支持了搜索。修訂號的更新則每每意味着進行了一些 bug 修復。所以次版本號和修訂號應該保持更新,這樣能讓你以前的代碼不會報錯還能獲取到最新的功能特性。github
可是,每每咱們不會指定依賴的具體版本,而是指定版本範圍,好比上面的 package.json 文件裏的 react、redux 以及 lodash,這三個依賴分別使用了三個符號來代表依賴的版本範圍。語義化版本範圍規定:web
所以,上面的 package.json 文件安裝的依賴版本範圍以下:算法
語義化版本規則定義了一種理想的版本號更新規則,但願全部的依賴更新都能遵循這個規則,可是每每會有許多依賴不是嚴格遵循這些規定的。所以,如何管理好這些依賴,尤爲是這些依賴的版本就顯得尤其重要,不然一不當心就會陷入因依賴版本不一致致使的各類問題中。npm
在項目開發中,一般會使用 npm、yarn 或者 cnpm 來管理項目中的依賴,下面咱們就來看看它們是如何幫助咱們管理這些依賴的。json
npm 發展到今天,能夠說經歷過三個重大的版本變化。redux
最先的 npm 版本在管理依賴時使用了一種很簡單的方式。咱們稱之爲嵌套模式。好比,在你的項目中有以下的依賴。
"dependencies": {
A: "1.0.0",
C: "1.0.0",
D: "1.0.0"
}
複製代碼
這些模塊都依賴 B 模塊,並且依賴的 B模塊的版本還不一樣。
A@1.0.0 -> B@1.0.0
C@1.0.1 -> B@2.0.0
D@1.0.0 -> B@1.0.0
複製代碼
經過執行 npm install
命令,npm v1 生成的 node_modules目錄以下:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
複製代碼
很明顯,每一個模塊下面都會有一個 node_modules 目錄存放該模塊的直接依賴。模塊的依賴下面還會存在一個 node_modules 目錄來存放模塊的依賴的依賴。很明顯這種依賴管理簡單明瞭,但存在很大的問題,除了 node_modules 目錄長度的嵌套過深以外,還會形成相同的依賴存儲多份的問題,好比上面的 B@1.0.0 就存放了兩份,這明顯也是一種浪費。因而在 npm v3 發佈後,npm 的依賴管理作出了重大的改變。
對於一樣的上述依賴,使用 npm v3 執行 npm install
命令後生成的 node_modules 目錄以下:
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
├── D@1.0.0
複製代碼
顯而易見,npm v3 使用了一種扁平的模式,把項目中使用的全部的模塊和模塊的依賴都放在了 node_modules 目錄下的頂層,遇到版本衝突的時候纔會在模塊下的 node_modules 目錄下存放該模塊須要用到的依賴。之因此能這麼實現是基於包搜索機制的。包搜索機制是指當你在項目中直接 require('A')
時,首先會在當前路徑下搜索 node_modules 目錄中是否存在該依賴,若是不存在則往上查找也就是繼續查找該路徑的上一層目錄下的 node_modules。正由於此,npm v3 才能把以前的嵌套結構拍平,把全部的依賴都放在項目根目錄的 node_modules,這樣就避免了 node_modules 目錄嵌套過深的問題。此外,npm v3 還會解析模塊的依賴的多個版本爲一個版本,好比 A依賴 B@^1.0.1,D 依賴 B@^1.0.2,則只會有一個 B@1.0.2 的版本存在。雖然 npm v3 解決了這兩個問題,可是此時的 npm 仍然存在諸多問題,被人詬病最多的應該就是它的不肯定性了。
什麼是肯定性。在 JavaScript 包管理的背景下,肯定性是指在給定的 package.json 和 lock 文件下始終能獲得一致的 node_modules 目錄結構。簡單點說就是不管在何種環境下執行 npm install
都能獲得相同的 node_modules 目錄結構。npm v5 正是爲解決這個問題而產生的,npm v5 生成的 node_modules 目錄和 v3 是一致的,區別是 v5 會默認生成一個 package-lock.json 文件,來保證安裝的依賴的肯定性。好比,對於以下的一個 package.json 文件
"dependencies": {
"redux": "^3.7.2"
}
複製代碼
對應的 package-lock.json 文件內容以下:
{
"name": "test",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"lodash": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
},
"lodash-es": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
"integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
},
"loose-envify": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
"requires": {
"js-tokens": "3.0.2"
}
},
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"requires": {
"lodash": "4.17.4",
"lodash-es": "4.17.4",
"loose-envify": "1.3.1",
"symbol-observable": "1.1.0"
}
},
"symbol-observable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.1.0.tgz",
"integrity": "sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw=="
}
}
}
複製代碼
不難看出,package-lock.json 文件裏記錄了安裝的每個依賴的肯定版本,這樣在下次安裝時就能經過這個文件來安裝同樣的依賴了。
yarn 是在 2016.10.11 開源的,yarn 的出現是爲了解決 npm v3 中的存在的一些問題,那時 npm v5 還沒發佈。yarn 被定義爲快速、安全、可靠的依賴管理。
yarn 生成的 node_modules 目錄結構和 npm v5 是相同的,同時默認生成一個 yarn.lock 文件。對於上面的例子,只安裝 redux 的依賴生成的 yarn.lock 文件內容以下:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
js-tokens@^3.0.0:
version "3.0.2"
resolved "http://registry.npm.alibaba-inc.com/js-tokens/download/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
lodash-es@^4.2.1:
version "4.17.4"
resolved "http://registry.npm.alibaba-inc.com/lodash-es/download/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
lodash@^4.2.1:
version "4.17.4"
resolved "http://registry.npm.alibaba-inc.com/lodash/download/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
loose-envify@^1.1.0:
version "1.3.1"
resolved "http://registry.npm.alibaba-inc.com/loose-envify/download/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
js-tokens "^3.0.0"
redux@^3.7.2:
version "3.7.2"
resolved "http://registry.npm.alibaba-inc.com/redux/download/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
dependencies:
lodash "^4.2.1"
lodash-es "^4.2.1"
loose-envify "^1.1.0"
symbol-observable "^1.0.3"
symbol-observable@^1.0.3:
version "1.1.0"
resolved "http://registry.npm.alibaba-inc.com/symbol-observable/download/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32"
複製代碼
不難看出,yarn.lock 文件和 npm v5 生成的 package-lock.json 文件有以下幾點不一樣:
關於爲何會有這些不一樣、yarn 的肯定性算法以及和 npm v5 的區別,yarn 官方的一篇文章詳細介紹了這幾點。因爲篇幅有限,這裏就再也不贅述,感興趣的能夠移步到個人翻譯文章 Yarn 肯定性去看。
yarn 的出現除了帶來安裝速度的提高之外,最大的貢獻是經過 lock 文件來保證安裝依賴的肯定性,保證相同的 package.json 文件,在何種環境何種機器上安裝依賴都會獲得相同的結果也就是相同的 node_modules 目錄結構。這在很大程度上避免了一些「在我電腦上是正常的,在其餘機器上失敗」的 bug。可是在使用 yarn 作依賴管理時,仍然須要注意如下3點。
yarn upgrade
命令,避免手動修改 package.json 和 yarn.lock 文件。cnpm 在國內的用戶應該仍是蠻多的,尤爲是對於有搭建私有倉庫需求的人來講。cnpm 在安裝依賴時使用的是 npminstall,簡單來講, cnpm 使用連接 link 的安裝方式,最大限度地提升了安裝速度,生成的 node_modules 目錄採用的是和 npm 不同的佈局。 用 cnpm 裝的包都是在 node_modules 文件夾下以 版本號 @包名 命名,而後再作軟連接到只以包名命名的文件夾上。一樣的例子,使用 cnpm 只安裝 redux 依賴時生成的 node_modules 目錄結構以下:
cnpm 和 npm 以及 yarn 之間最大的區別就在於生成的 node_modules 目錄結構不一樣,這在某些場景下可能會引起一些問題。此外也不會生成 lock 文件,這就致使在安裝肯定性方面會比 npm 和 yarn 稍遜一籌。可是 cnpm 使用的 link 安裝方式仍是很好的,既節省了磁盤空間,也保持了 node_modules 的目錄結構清晰,能夠說是在嵌套模式和扁平模式之間找到了一個平衡。
npm、yarn 和 cnpm 均提供了很好的依賴管理來幫助咱們管理項目中使用到的各類依賴以及版本,可是若是依賴出現了循環調用也就是循環依賴應該怎麼解決呢?
循環依賴指的是,a 模塊的執行依賴 b 模塊,而 b 模塊的執行又依賴 a 模塊。循環依賴可能致使遞歸加載,處理很差的話可能使得程序沒法執行。探討循環依賴以前,先讓咱們瞭解一下 JavaScript 中的模塊規範。由於,不一樣的規範在處理循環依賴時的作法是不一樣的。
目前,通行的 JavaScript 規範能夠分爲三種,CommonJS 、 AMD 和 ES6。
從2009年 node.js 出現以來,CommonJS 模塊系統逐漸深刻人心。CommonJS 的一個模塊就是一個腳本文件,經過 require
命令來加載這個模塊,並使用模塊暴漏出的接口。加載時執行是 CommonJS 模塊的重要特性,即腳本代碼在 require
的時候就會執行模塊中的代碼。這個特性在服務端是沒問題的,但若是引入一個模塊就要等待它執行完才能執行後面的代碼,這在瀏覽器端就會有很大的問題了。所以出現了 AMD 規範,以支持瀏覽器環境。
AMD 是 「Asynchronous Module Definition」 的縮寫,意思就是「異步模塊定義」。它採用異步加載方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行。最有表明性的實現則是 requirejs。
不一樣於 CommonJS 和 AMD 的模塊加載方案,ES6 在 JavaScript 語言層面上實現了模塊功能。它的設計思想是,儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係。在遇到模塊加載命令 import
時,不會去執行模塊,而是隻生成一個引用。等到真的須要用到時,再到模塊裏面去取值。這是和 CommonJS 模塊規範的最大不一樣。
請看下面的例子:
a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
複製代碼
b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
複製代碼
main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
複製代碼
在這個例子中,a 模塊調用 b 模塊,b 模塊又須要調用 a 模塊,這就使得 a 和 b 之間造成了循環依賴,可是當咱們執行 node main.js
時代碼卻沒有陷入無限循環調用當中,而是輸出了以下內容:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true
複製代碼
爲何程序沒有報錯,而是輸出如上的內容呢?這是由於 CommonJs 模塊的兩個特性。第一,加載時執行;第二,已加載的模塊會進行緩存,不會重複加載。下面讓咱們分析下程序的執行過程:
從上面的執行過程當中,咱們能夠看到,在 CommonJS 規範中,當遇到 require()
語句時,會執行 require 模塊中的代碼,並緩存執行的結果,當下次再次加載時不會重複執行,而是直接取緩存的結果。正由於此,出現循環依賴時纔不會出現無限循環調用的狀況。雖然這種模塊加載機制能夠避免出現循環依賴時報錯的狀況,但稍不注意就極可能使得代碼並非像咱們想象的那樣去執行。所以在寫代碼時仍是須要仔細的規劃,以保證循環模塊的依賴能正確工做(官方原文:Careful planning is required to allow cyclic module dependencies to work correctly within an application)。
除了仔細的規劃還有什麼辦法能夠避免出現循環依賴嗎?一個不太優雅的方法是在循環依賴的每一個模塊中先寫 exports 語句,再寫 require 語句,利用 CommonJS 的緩存機制,在 require()
其餘模塊以前先把自身要導出的內容導出,這樣就能保證其餘模塊在使用時能夠取到正確的值。好比:
A.js
exports.done = true;
let B = require('./B');
console.log(B.done)
複製代碼
B.js
exports.done = true;
let A = require('./A');
console.log(A.done)
複製代碼
這種寫法簡單明瞭,缺點是要改變每一個模塊的寫法,並且大部分同窗都習慣了在文件開頭先寫 require 語句。
我的經驗來看,在寫代碼中只要咱們注意一下循環依賴的問題就能夠了,大部分同窗在寫 node.js 中應該不多碰到須要手動去處理循環依賴的問題,更甚的是極可能大部分同窗都沒想過這個問題。
要想知道 ES6 中循環依賴的解法就必須先了解 ES6 的模塊加載機制。咱們都知道 ES6 使用 export
命令來規定模塊的對外接口,使用 import
命令來加載模塊。那麼在遇到 import 和 export 時發生了什麼呢?ES6 的模塊加載機制能夠歸納爲四個字一靜一動。
import 靜態執行是指,import 命令會被 JavaScript 引擎靜態分析,優先於模塊內的其餘內容執行。
export 動態綁定是指,export 命令輸出的接口,與其對應的值是動態綁定關係,經過該接口能夠實時取到模塊內部的值。
讓咱們看下面一個例子:
foo.js
console.log('foo is running');
import {bar} from './bar'
console.log('bar = %j', bar);
setTimeout(() => console.log('bar = %j after 500 ms', bar), 500);
console.log('foo is finished');
複製代碼
bar.js
console.log('bar is running');
export let bar = false;
setTimeout(() => bar = true, 500);
console.log('bar is finished');
複製代碼
執行 node foo.js
時會輸出以下內容:
bar is running
bar is finished
foo is running
bar = false
foo is finished
bar = true after 500 ms
複製代碼
是否是和你想的不同呢?當咱們執行 node foo.js
時第一行輸出的不是 foo.js 的第一個 console 語句,而是先輸出了 bar.js 裏的 console 語句。這就是由於 import 命令是在編譯階段執行,在代碼運行以前先被 JavaScript 引擎靜態分析,因此優先於 foo.js 自身內容執行。同時咱們也看到 500 毫秒以後也能夠取到 bar 更新後的值也說明了 export 命令輸出的接口與其對應的值是動態綁定關係。這樣的設計使得程序在編譯時就能肯定模塊的依賴關係,這是和 CommonJS 模塊規範的最大不一樣。還有一點須要注意的是,因爲 import 是靜態執行,因此 import 具備提高效果即 import 命令的位置並不影響程序的輸出。
在咱們瞭解了 ES6 的模塊加載機制以後來讓咱們來看一下 ES6 是怎麼處理循環依賴的。修改一下上面的例子:
foo.js
console.log('foo is running');
import {bar} from './bar'
console.log('bar = %j', bar);
setTimeout(() => console.log('bar = %j after 500 ms', bar), 500);
export let foo = false;
console.log('foo is finished');
複製代碼
bar.js
console.log('bar is running');
import {foo} from './foo';
console.log('foo = %j', foo)
export let bar = false;
setTimeout(() => bar = true, 500);
console.log('bar is finished');
複製代碼
執行 node foo.js
時會輸出以下內容:
bar is running
foo = undefined
bar is finished
foo is running
bar = false
foo is finished
bar = true after 500 ms
複製代碼
foo.js 和 bar.js 造成了循環依賴,可是程序卻沒有因陷入循環調用報錯而是執行正常,這是爲何呢?仍是由於 import 是在編譯階段執行的,這樣就使得程序在編譯時就能肯定模塊的依賴關係,一旦發現循環依賴,ES6 自己就不會再去執行依賴的那個模塊了,因此程序能夠正常結束。這也說明了 ES6 自己就支持循環依賴,保證程序不會由於循環依賴陷入無限調用。雖然如此,可是咱們仍然要儘可能避免程序中出現循環依賴,由於可能會發生一些讓你迷惑的狀況。注意到上面的輸出,在 bar.js 中輸出的 foo = undefined
,若是沒注意到循環依賴會讓你以爲明明在 foo.js 中 export foo = false
,爲何在 bar.js 中倒是 undefined
呢,這就是循環依賴帶來的困惑。在一些複雜大型項目中,你是很難用肉眼發現循環依賴的,而這會給排查異常帶來極大的困難。對於使用 webpack 進行項目構建的項目,推薦使用 webpack 插件 circular-dependency-plugin 來幫助你檢測項目中存在的全部循環依賴,儘早發現潛在的循環依賴可能會免去將來很大的麻煩。
講了那麼多,但願此文能幫助你更好的瞭解 JavaScript 中的依賴管理,而且處理好項目中的循環依賴問題。