摘要:無需手動拷貝文件或者創建軟鏈接到目錄,有更優雅的解決方案。這是因為識別協議的,得知這個包需要直接從文件系統中獲取,會自動創建軟鏈接到中,完成安裝過程。
nodejs 社區乃至 Web 前端工程化領域發展到今天,作為 node 自帶的包管理工具的 npm 已經成為每個前端開發者必備的工具。但是現實狀況是,我們很多人對這個nodejs基礎設施的使用和了解還停留在: 會用 npm install 這里(一言不合就刪除整個 node_modules 目錄然后重新 install 這種事你沒做過嗎?)
當然 npm 能成為現在世界上最大規模的包管理系統,很大程度上確實歸功于它足夠用戶友好,你看即使我只會執行 install 也不必太擔心出什么大岔子. 但是 npm 的功能遠不止于 install 一下那么簡單,這篇文章幫你扒一扒那些你可能不知道的 npm 原理、特性、技巧,以及(我認為的)最佳實踐。
你懶得讀的 npm 文檔,我幫你翻譯然后試驗整理過來了 ???
我們都知道 package.json 文件是用來定義一個 package 的描述文件, 也知道npm init 命令用來初始化一個簡單的 package.json 文件,執行該命令后終端會依次詢問 name, version, description 等字段。
1.1 npm init 執行默認行為
而如果想要偷懶步免去一直按 enter,在命令后追加 --yes 參數即可,其作用與一路下一步相同。
npm init --yes
1.2 自定義 npm init 行為npm init 命令的原理并不復雜,調用腳本,輸出一個初始化的 package.json 文件就是了。所以相應地,定制 npm init 命令的實現方式也很簡單,在 Home 目錄創建一個 .npm-init.js 即可,該文件的 module.exports 即為 package.json 配置內容,需要獲取用戶輸入時候,使用 prompt() 方法即可。
例如編寫這樣的 ~/.npm-init.js
const desc = prompt("description?", "A new package...") const bar = prompt("bar?", "") const count = prompt("count?", "42") module.exports = { key: "value", foo: { bar: bar, count: count }, name: prompt("name?", process.cwd().split("/").pop()), version: prompt("version?", "0.1.0"), description: desc, main: "index.js", }
此時在 ~/hello 目錄下執行 npm init 將會得到這樣的 package.json:
{ "key": "value", "foo": { "bar": "", "count": "42" }, "name": "hello", "version": "0.1.0", "description": "A new package...", "main": "index.js" }
除了生成 package.json, 因為 .npm-init.js 是一個常規的模塊,意味著我們可以執行隨便什么 node 腳本可以執行的任務。例如通過 fs 創建 README, .eslintrc 等項目必需文件,實現項目腳手架的作用。
2. 依賴包安裝依賴管理是 npm 的核心功能,原理就是執行 npm install 從 package.json 中的 dependencies, devDependencies 將依賴包安裝到當前目錄的 ./node_modules 文件夾中。
2.1 package定義我們都知道要手動安裝一個包時,執行 npm install
閱讀 npm的文檔, 我們會發現package 準確的定義,只要符合以下 a) 到 g) 其中之一條件,就是一個 package:
# | 說明 | 例子 |
---|---|---|
a) | 一個包含了程序和描述該程序的 package.json 文件 的 文件夾 | ./local-module/ |
b) | 一個包含了 (a) 的 gzip 壓縮文件 | ./module.tar.gz |
c) | 一個可以下載得到 (b) 資源的 url (通常是 http(s) url) | https://registry.npmjs.org/we... |
d) | 一個格式為 |
webpack@4.1.0 |
e) | 一個格式為 |
webpack@latest |
f) | 一個格式為 |
webpack |
g) | 一個 git url, 該 url 所指向的代碼庫滿足條件 (a) | git@github.com:webpack/webpack.git |
上面表格的定義意味著,我們在共享依賴包時,并不是非要將包發表到 npm 源上才可以提供給使用者來安裝。這對于私有的不方便 publish 到遠程源(即使是私有源),或者需要對某官方源進行改造,但依然需要把包共享出去的場景來說非常實用。
場景1: 本地模塊引用
nodejs 應用開發中不可避免有模塊間調用,例如在實踐中經常會把需要被頻繁引用的配置模塊放到應用根目錄;于是在創建了很多層級的目錄、文件后,很可能會遇到這樣的代碼:
const config = require("../../../../config.js");
除了看上去很丑以外,這樣的路徑引用也不利于代碼的重構。并且身為程序員的自我修養告訴我們,這樣重復的代碼多了也就意味著是時候把這個模塊分離出來供應用內其他模塊共享了。例如這個例子里的 config.js 非常適合封裝為 package 放到 node_modules 目錄下,共享給同應用內其他模塊。
無需手動拷貝文件或者創建軟鏈接到 node_modules 目錄,npm 有更優雅的解決方案。
方案:
創建 config 包:
新增 config 文件夾; 重命名 config.js 為 config/index.js 文件; 創建 package.json 定義 config 包
{ "name": "config", "main": "index.js", "version": "0.1.0" }
在應用層 package.json 文件中新增依賴項,然后執行 npm install; 或直接執行第 3 步
{ "dependencies": { "config": "file:./config" } }
(等價于第 2 步)直接在應用目錄執行 npm install file:./config
此時,查看 node_modules 目錄我們會發現多出來一個名為 config,指向上層 config/ 文件夾的軟鏈接。這是因為 npm 識別 file: 協議的url,得知這個包需要直接從文件系統中獲取,會自動創建軟鏈接到 node_modules 中,完成“安裝”過程。
相比手動軟鏈,我們既不需要關心 windows 和 linux 命令差異,又可以顯式地將依賴信息固化到 dependencies 字段中,開發團隊其他成員可以執行 npm install 后直接使用。
場景2: 私有 git 共享 package
有些時候,我們一個團隊內會有一些代碼/公用庫需要在團隊內不同項目間共享,但可能由于包含了敏感內容,或者代碼太爛拿不出手等原因,不方便發布到源。
這種情況下,我們可以簡單地將被依賴的包托管在私有的 git 倉庫中,然后將該 git url 保存到 dependencies 中. npm 會直接調用系統的 git 命令從 git 倉庫拉取包的內容到 node_modules 中。
npm 支持的 git url 格式:
://[ [: ]@] [: ][:][/] [# | #semver: ]
git 路徑后可以使用 # 指定特定的 git branch/commit/tag, 也可以 #semver: 指定特定的 semver range.
例如:
git+ssh://git@github.com:npm/npm.git#v1.0.27 git+ssh://git@github.com:npm/npm#semver:^5.0 git+https://isaacs@github.com/npm/npm.git git://github.com/npm/npm.git#v1.0.27
場景3: 開源 package 問題修復
使用某個 npm 包時發現它有某個嚴重bug,但也許最初作者已不再維護代碼了,也許我們工作緊急,沒有足夠的時間提 issue 給作者再慢慢等作者發布新的修復版本到 npm 源。
此時我們可以手動進入 node_modules 目錄下修改相應的包內容,也許修改了一行代碼就修復了問題。但是這種做法非常不明智!
首先 node_modules 本身不應該放進版本控制系統,對 node_modules 文件夾中內容的修改不會被記錄進 git 提交記錄;其次,就算我們非要反模式,把 node_modules 放進版本控制中,你的修改內容也很容易在下次 team 中某位成員執行 npm install 或 npm update 時被覆蓋,而這樣的一次提交很可能包含了幾十幾百個包的更新,你自己所做的修改很容易就被淹沒在龐大的 diff 文件列表中了。
方案:
最好的辦法應當是 fork 原作者的 git 庫,在自己所屬的 repo 下修復問題后,將 dependencies 中相應的依賴項更改為自己修復后版本的 git url 即可解決問題。(Fork 代碼庫后,也便于向原作者提交 PR 修復問題。上游代碼庫修復問題后,再次更新我們的依賴配置也不遲。)
3. npm install 如何工作 —— node_modules 目錄結構npm install 執行完畢后,我們可以在 node_modules 中看到所有依賴的包。雖然使用者無需關注這個目錄里的文件夾結構細節,只管在業務代碼中引用依賴包即可,但了解 node_modules 的內容可以幫我們更好理解 npm 如何工作,了解從 npm 2 到 npm 5 有哪些變化和改進。
為簡單起見,我們假設應用目錄為 app, 用兩個流行的包 webpack, nconf 作為依賴包做示例說明。并且為了正常安裝,使用了“上古” npm 2 時期的版本 webpack@1.15.0, nconf@0.8.5.
3.1 npm 2npm 2 在安裝依賴包時,采用簡單的遞歸安裝方法。執行 npm install 后,npm 2 依次遞歸安裝 webpack 和 nconf 兩個包到 node_modules 中。執行完畢后,我們會看到 ./node_modules 這層目錄只含有這兩個子目錄。
node_modules/ ├── nconf/ └── webpack/
進入更深一層 nconf 或 webpack 目錄,將看到這兩個包各自的 node_modules 中,已經由 npm 遞歸地安裝好自身的依賴包。包括 ./node_modules/webpack/node_modules/webpack-core , ./node_modules/conf/node_modules/async 等等。而每一個包都有自己的依賴包,每個包自己的依賴都安裝在了自己的 node_modules 中。依賴關系層層遞進,構成了一整個依賴樹,這個依賴樹與文件系統中的文件結構樹剛好層層對應。
最方便的查看依賴樹的方式是直接在 app 目錄下執行 npm ls 命令。
app@0.1.0 ├─┬ nconf@0.8.5 │ ├── async@1.5.2 │ ├── ini@1.3.5 │ ├── secure-keys@1.0.0 │ └── yargs@3.32.0 └─┬ webpack@1.15.0 ├── acorn@3.3.0 ├── async@1.5.2 ├── clone@1.0.3 ├── ... ├── optimist@0.6.1 ├── supports-color@3.2.3 ├── tapable@0.1.10 ├── uglify-js@2.7.5 ├── watchpack@0.2.9 └─┬ webpack-core@0.6.9 ├── source-list-map@0.1.8 └── source-map@0.4.4
這樣的目錄結構優點在于層級結構明顯,便于進行傻瓜式的管理:
例如新裝一個依賴包,可以立即在第一層 node_modules 中看到子目錄
在已知所需包名和版本號時,甚至可以從別的文件夾手動拷貝需要的包到 node_modules 文件夾中,再手動修改 package.json 中的依賴配置
要刪除這個包,也可以簡單地手動刪除這個包的子目錄,并刪除 package.json 文件中相應的一行即可
實際上,很多人在 npm 2 時代也的確都這么實踐過,的確也都可以安裝和刪除成功,并不會導致什么差錯。
但這樣的文件結構也有很明顯的問題:
對復雜的工程, node_modules 內目錄結構可能會太深,導致深層的文件路徑過長而觸發 windows 文件系統中,文件路徑不能超過 260 個字符長的錯誤
部分被多個包所依賴的包,很可能在應用 node_modules 目錄中的很多地方被重復安裝。隨著工程規模越來越大,依賴樹越來越復雜,這樣的包情況會越來越多,造成大量的冗余。
——在我們的示例中就有這個問題,webpack 和 nconf 都依賴 async 這個包,所以在文件系統中,webpack 和 nconf 的 node_modules 子目錄中都安裝了相同的 async 包,并且是相同的版本。
+-------------------------------------------+ | app/ | +----------+------------------------+-------+ | | | | +----------v------+ +---------v-------+ | | | | | webpack@1.15.0 | | nconf@0.8.5 | | | | | +--------+--------+ +--------+--------+ | | +-----v-----+ +-----v-----+ |async@1.5.2| |async@1.5.2| +-----------+ +-----------+3.2 npm 3 - 扁平結構
主要為了解決以上問題,npm 3 的 node_modules 目錄改成了更加扁平狀的層級結構。文件系統中 webpack, nconf, async 的層級關系變成了平級關系,處于同一級目錄中。
+-------------------------------------------+ | app/ | +-+---------------------------------------+-+ | | | | +----------v------+ +-------------+ +---------v-------+ | | | | | | | webpack@1.15.0 | | async@1.5.2 | | nconf@0.8.5 | | | | | | | +-----------------+ +-------------+ +-----------------+
雖然這樣一來 webpack/node_modules 和 nconf/node_modules 中都不再有 async 文件夾,但得益于 node 的模塊加載機制,他們都可以在上一級 node_modules 目錄中找到 async 庫。所以 webpack 和 nconf 的庫代碼中 require("async") 語句的執行都不會有任何問題。
這只是最簡單的例子,實際的工程項目中,依賴樹不可避免地會有很多層級,很多依賴包,其中會有很多同名但版本不同的包存在于不同的依賴層級,對這些復雜的情況, npm 3 都會在安裝時遍歷整個依賴樹,計算出最合理的文件夾安裝方式,使得所有被重復依賴的包都可以去重安裝。
npm 文檔提供了更直觀的例子解釋這種情況:
假如 package{dep} 寫法代表包和包的依賴,那么 A{B,C}, B{C}, C{D} 的依賴結構在安裝之后的 node_modules 是這樣的結構:
A +-- B +-- C +-- D
這里之所以 D 也安裝到了與 B C 同一級目錄,是因為 npm 會默認會在無沖突的前提下,盡可能將包安裝到較高的層級。
如果是 A{B,C}, B{C,D@1}, C{D@2} 的依賴關系,得到的安裝后結構是:
A +-- B +-- C `-- D@2 +-- D@1
這里是因為,對于 npm 來說同名但不同版本的包是兩個獨立的包,而同層不能有兩個同名子目錄,所以其中的 D@2 放到了 C 的子目錄而另一個 D@1 被放到了再上一層目錄。
很明顯在 npm 3 之后 npm 的依賴樹結構不再與文件夾層級一一對應了。想要查看 app 的直接依賴項,要通過 npm ls 命令指定 --depth 參數來查看:
npm ls --depth 1
PS: 與本地依賴包不同,如果我們通過 npm install --global 全局安裝包到全局目錄時,得到的目錄依然是“傳統的”目錄結構。而如果使用 npm 3 想要得到“傳統”形式的本地 node_modules 目錄,使用 npm install --global-style 命令即可。3.3 npm 5 - package-lock 文件
npm 5 發布于 2017 年也是目前最新的 npm 版本,這一版本依然沿用 npm 3 之后扁平化的依賴包安裝方式,此外最大的變化是增加了 package-lock.json 文件。
package-lock.json 的作用是鎖定依賴安裝結構,如果查看這個 json 的結構,會發現與 node_modules 目錄的文件層級結構是一一對應的。
以依賴關系為: app{webpack} 的 "app" 項目為例, 其 package-lock 文件包含了這樣的片段。
{ "name": "app", "version": "0.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { // ... 其他依賴包 "webpack": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.8.11.tgz", "integrity": "sha1-Yu0hnstBy/qcKuanu6laSYtgkcI=", "requires": { "async": "0.9.2", "clone": "0.1.19", "enhanced-resolve": "0.8.6", "esprima": "1.2.5", "interpret": "0.5.2", "memory-fs": "0.2.0", "mkdirp": "0.5.1", "node-libs-browser": "0.4.3", "optimist": "0.6.1", "supports-color": "1.3.1", "tapable": "0.1.10", "uglify-js": "2.4.24", "watchpack": "0.2.9", "webpack-core": "0.6.9" } }, "webpack-core": { "version": "0.6.9", "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz", "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=", "requires": { "source-list-map": "0.1.8", "source-map": "0.4.4" }, "dependencies": { "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "requires": { "amdefine": "1.0.1" } } } }, //... 其他依賴包 } }
看懂 package-lock 文件并不難,其結構是同樣類型的幾個字段嵌套起來的,主要是 version, resolved, integrity, requires, dependencies 這幾個字段而已。
version, resolved, integrity 用來記錄包的準確版本號、內容hash、安裝源的,決定了要安裝的包的準確“身份”信息
假設蓋住其他字段,只關注文件中的 dependencies: {} 我們會發現,整個文件的 JSON 配置里的 dependencies 層次結構與文件系統中 node_modules 的文件夾層次結構是完全對照的
只關注 requires: {} 字段又會發現,除最外層的 requires 屬性為 true 以外, 其他層的 requires 屬性都對應著這個包的 package.json 里記錄的自己的依賴項
因為這個文件記錄了 node_modules 里所有包的結構、層級和版本號甚至安裝源,它也就事實上提供了 “保存” node_modules 狀態的能力。只要有這樣一個 lock 文件,不管在那一臺機器上執行 npm install 都會得到完全相同的 node_modules 結果。
這就是 package-lock 文件致力于優化的場景:在從前僅僅用 package.json 記錄依賴,由于 semver range 的機制;一個月前由 A 生成的 package.json 文件,B 在一個月后根據它執行 npm install 所得到的 node_modules 結果很可能許多包都存在不同的差異,雖然 semver 機制的限制使得同一份 package.json 不會得到大版本不同的依賴包,但同一份代碼在不同環境安裝出不同的依賴包,依然是可能導致意外的潛在因素。
相同作用的文件在 npm 5 之前就有,稱為 npm shrinkwrap 文件,二者作用完全相同,不同的是后者需要手動生成,而 npm 5 默認會在執行 npm install 后就生成 package-lock 文件,并且建議你提交到 git/svn 代碼庫中。
package-lock.json 文件在最初 npm 5.0 默認引入時也引起了相當大的爭議。在 npm 5.0 中,如果已有 package-lock 文件存在,若手動在 package.json 文件新增一條依賴,再執行 npm install, 新增的依賴并不會被安裝到 node_modules 中, package-lock.json 也不會做相應的更新。這樣的表現與使用者的自然期望表現不符。在 npm 5.1 的首個 Release 版本中這個問題得以修復。這個事情告訴我們,要升級,不要使用 5.0。
——但依然有反對的聲音認為 package-lock 太復雜,對此 npm 也提供了禁用配置:
npm config set package-lock false4. 依賴包版本管理
依賴包安裝完并不意味著就萬事大吉了,版本的維護和更新也很重要。這一章介紹依賴包升級管理相關知識,太長不看版本請直接跳到 4.3 最佳實踐
4.1 semvernpm 依賴管理的一個重要特性是采用了語義化版本 (semver) 規范,作為依賴版本管理方案。
semver 約定一個包的版本號必須包含3個數字,格式必須為 MAJOR.MINOR.PATCH, 意為 主版本號.小版本號.修訂版本號.
MAJOR 對應大的版本號迭代,做了不兼容舊版的修改時要更新 MAJOR 版本號
MINOR 對應小版本迭代,發生兼容舊版API的修改或功能更新時,更新MINOR版本號
PATCH 對應修訂版本號,一般針對修復 BUG 的版本號
對于包作者(發布者),npm 要求在 publish 之前,必須更新版本號。npm 提供了 npm version 工具,執行 npm version major|minor|patch 可以簡單地將版本號中相應的數字加1.
如果包是一個 git 倉庫,npm version 還會自動創建一條注釋為更新后版本號的 git commit 和名為該版本號的 tag
對于包的引用者來說,我們需要在 dependencies 中使用 semver 約定的 semver range 指定所需依賴包的版本號或版本范圍。npm 提供了網站 https://semver.npmjs.com 可方便地計算所輸入的表達式的匹配范圍。常用的規則示例如下表:
range | 含義 | 例 |
---|---|---|
^2.2.1 | 指定的 MAJOR 版本號下, 所有更新的版本 | 匹配 2.2.3, 2.3.0; 不匹配 1.0.3, 3.0.1 |
~2.2.1 | 指定 MAJOR.MINOR 版本號下,所有更新的版本 | 匹配 2.2.3, 2.2.9 ; 不匹配 2.3.0, 2.4.5 |
>=2.1 | 版本號大于或等于 2.1.0 | 匹配 2.1.2, 3.1 |
<=2.2 | 版本號小于或等于 2.2 | 匹配 1.0.0, 2.2.1, 2.2.11 |
1.0.0 - 2.0.0 | 版本號從 1.0.0 (含) 到 2.0.0 (含) | 匹配 1.0.0, 1.3.4, 2.0.0 |
任意兩條規則,通過 || 連接起來,則表示兩條規則的并集:
如 ^2 >=2.3.1 || ^3 >3.2 可以匹配:
* `2.3.1`, `2,8.1`, `3.3.1` * 但不匹配 `1.0.0`, `2.2.0`, `3.1.0`, `4.0.0`
PS: 除了這幾種,還有如下更直觀的表示版本號范圍的寫法:
* 或 x 匹配所有主版本
1 或 1.x 匹配 主版本號為 1 的所有版本
1.2 或 1.2.x 匹配 版本號為 1.2 開頭的所有版本
PPS: 在常規僅包含數字的版本號之外,semver 還允許在 MAJOR.MINOR.PATCH 后追加 - 后跟點號分隔的標簽,作為預發布版本標簽 - Prerelese Tags,通常被視為不穩定、不建議生產使用的版本。例如:
1.0.0-alpha
1.0.0-beta.1
1.0.0-rc.3
上表中我們最常見的是 ^1.8.11 這種格式的 range, 因為我們在使用 npm install
問題來了,在安裝完一個依賴包之后有新版本發布了,如何使用 npm 進行版本升級呢?——答案是簡單的 npm install 或 npm update,但在不同的 npm 版本,不同的 package.json, package-lock.json 文件,安裝/升級的表現也不同。
我們不妨還以 webpack 舉例,做如下的前提假設:
我們的工程項目 app 依賴 webpack
項目最初初始化時,安裝了當時最新的包 webpack@1.8.0,并且 package.json 中的依賴配置為: "webpack": "^1.8.0"
當前(2018年3月) webpack 最新版本為 4.2.0, webpack 1.x 最新子版本為 1.15.0
如果我們使用的是 npm 3, 并且項目不含 package-lock.json, 那么根據 node_modules 是否為空,執行 install/update 的結果如下 (node 6.13.1, npm 3.10.10 環境下試驗):
# | package.json (BEFORE) | node_modules (BEFORE) | command (npm 3) | package.json (AFTER) | node_modules (AFTER) |
---|---|---|---|---|---|
a) | webpack: ^1.8.0 | webpack@1.8.0 | install | webpack: ^1.8.0 | webpack@1.8.0 |
b) | webpack: ^1.8.0 | 空 | install | webpack: ^1.8.0 | webpack@1.15.0 |
c) | webpack: ^1.8.0 | webpack@1.8.0 | update | webpack: ^1.8.0 | webpack@1.15.0 |
d) | webpack: ^1.8.0 | 空 | update | webpack: ^1.8.0 | webpack@1.15.0 |
根據這個表我們可以對 npm 3 得出以下結論:
如果本地 node_modules 已安裝,再次執行 install 不會更新包版本, 執行 update 才會更新; 而如果本地 node_modules 為空時,執行 install/update 都會直接安裝更新包;
npm update 總是會把包更新到符合 package.json 中指定的 semver 的最新版本號——本例中符合 ^1.8.0 的最新版本為 1.15.0
一旦給定 package.json, 無論后面執行 npm install 還是 update, package.json 中的 webpack 版本一直頑固地保持 一開始的 ^1.8.0 巋然不動
這里不合理的地方在于,如果最開始團隊中第一個人安裝了 webpack@1.8.0, 而新加入項目的成員, checkout 工程代碼后執行 npm install 會安裝得到不太一樣的 1.15.0 版本。雖然 semver 約定了小版本號應當保持向下兼容(相同大版本號下的小版本號)兼容,但萬一有不熟悉不遵循此約定的包發布者,發布了不兼容的包,此時就可能出現因依賴環境不同導致的 bug。
下面由 npm 5 帶著 package-lock.json 閃亮登場,執行 install/update 的效果是這樣的 (node 9.8.0, npm 5.7.1 環境下試驗):
下表為表述簡單,省略了包名 webpack, install 簡寫 i, update 簡寫為 up
# | package.json (BEFORE) | node_modules (BEFORE) | package-lock (BEFORE) | command | package.json (AFTER) | node_modules (AFTER) |
---|---|---|---|---|---|---|
a) | ^1.8.0 | @1.8.0 | @1.8.0 | i | ^1.8.0 | @1.8.0 |
b) | ^1.8.0 | 空 | @1.8.0 | i | ^1.8.0 | @1.8.0 |
c) | ^1.8.0 | @1.8.0 | @1.8.0 | up | ^1.15.0 | @1.15.0 |
d) | ^1.8.0 | 空 | @1.8.0 | up | ^1.8.0 | @1.15.0 |
e) | ^1.15.0 | @1.8.0 (舊) | @1.15.0 | i | ^1.15.0 | @1.15.0 |
f) | ^1.15.0 | @1.8.0 (舊) | @1.15.0 | up | ^1.15.0 | @1.15.0 |
與 npm 3 相比,在安裝和更新依賴版本上主要的區別為:
無論何時執行 install, npm 都會優先按照 package-lock 中指定的版本來安裝 webpack; 避免了 npm 3 表中情形 b) 的狀況;
無論何時完成安裝/更新, package-lock 文件總會跟著 node_modules 更新 —— (因此可以視 package-lock 文件為 node_modules 的 JSON 表述)
已安裝 node_modules 后若執行 npm update,package.json 中的版本號也會隨之更改為 ^1.15.0
由此可見 npm 5.1 使得 package.json 和 package-lock.json 中所保存的版本號更加統一,解決了 npm 之前的各種問題。只要遵循好的實踐習慣,團隊成員可以很方便地維護一套應用代碼和 node_modules 依賴都一致的環境。
皆大歡喜。
4.3 最佳實踐總結起來,在 2018 年 (node 9.8.0, npm 5.7.1) 時代,我認為的依賴版本管理應當是:
使用 npm: >=5.1 版本, 保持 package-lock.json 文件默認開啟配置
初始化:第一作者初始化項目時使用 npm install
初始化:項目成員首次 checkout/clone 項目代碼后,執行一次 npm install 安裝依賴包
不要手動修改 package-lock.json
升級依賴包:
升級小版本: 本地執行 npm update 升級到新的小版本
升級大版本: 本地執行 npm install
也可手動修改 package.json 中版本號為要升級的版本(大于現有版本號)并指定所需的 semver, 然后執行 npm install
本地驗證升級后新版本無問題后,提交新的 package.json, package-lock.json 文件
降級依賴包:
正確: npm install
錯誤: 手動修改 package.json 中的版本號為更低版本的 semver, 這樣修改并不會生效,因為再次執行 npm install 依然會安裝 package-lock.json 中的鎖定版本
刪除依賴包:
Plan A: npm uninstall
Plan B: 把要卸載的包從 package.json 中 dependencies 字段刪除, 然后執行 npm install 并提交 package.json 和 package-lock.json
任何時候有人提交了 package.json, package-lock.json 更新后,團隊其他成員應在 svn update/git pull 拉取更新后執行 npm install 腳本安裝更新后的依賴包
恭喜你終于可以跟 rm -rf node_modules && npm install 這波操作說拜拜了(其實并不會)
npm scripts 是 npm 另一個很重要的特性。通過在 package.json 中 scripts 字段定義一個腳本,例如:
{ "scripts": { "echo": "echo HELLO WORLD" } }
我們就可以通過 npm run echo 命令來執行這段腳本,像在 shell 中執行該命令 echo HELLO WORLD 一樣,看到終端輸出 HELLO WORLD.
—— npm scripts 的基本使用就是這么簡單,它提供了一個簡單的接口用來調用工程相關的腳本。關于更詳細的相關信息,可以參考阮一峰老師的文章 npm script 使用指南 (2016年10月).
簡要總結阮老師文章內容:
npm run 命令執行時,會把 ./node_modules/.bin/ 目錄添加到執行環境的 PATH 變量中,因此如果某個命令行包未全局安裝,而只安裝在了當前項目的 node_modules 中,通過 npm run 一樣可以調用該命令。
執行 npm 腳本時要傳入參數,需要在命令后加 -- 標明, 如 npm run test -- --grep="pattern" 可以將 --grep="pattern" 參數傳給 test 命令
npm 提供了 pre 和 post 兩種鉤子機制,可以定義某個腳本前后的執行腳本
運行時變量:在 npm run 的腳本執行環境內,可以通過環境變量的方式獲取許多運行時相關信息,以下都可以通過 process.env 對象訪問獲得:
npm_lifecycle_event - 正在運行的腳本名稱
npm_package_
npm_package_
上面所說的 node_modules/.bin 目錄,保存了依賴目錄中所安裝的可供調用的命令行包。
何謂命令行包?例如 webpack 就屬于一個命令行包。如果我們在安裝 webpack 時添加 --global 參數,就可以在終端直接輸入 webpack 進行調用。但如果不加 --global 參數,我們會在 node_modules/.bin 目錄里看到名為 webpack 的文件,如果在終端直接輸入 ./node_modules/.bin/webpack 命令,一樣可以執行。
這是因為 webpack 在 package.json 文件中定義了 bin 字段為:
{ "bin": { "webpack": "./bin/webpack.js" } }
bin 字段的配置格式為:
正如上一節所說,npm run 命令在執行時會把 ./node_modules/.bin 加入到 PATH 中,使我們可直接調用所有提供了命令行調用接口的依賴包。所以這里就引出了一個最佳實踐:
將項目依賴的命令行工具安裝到項目依賴文件夾中,然后通過 npm scripts 調用;而非全局安裝
舉例而言 webpack 作為前端工程標配的構建工具,雖然我們都習慣了全局安裝并直接使用命令行調用,但不同的項目依賴的 webpack 版本可能不同,相應的 webpack.config.js 配置文件也可能只兼容了特定版本的 webpack. 如果我們僅全局安裝了最新的 webpack 4.x 并使用 webpack 命令調用,在一個依賴 webpack 3.x 的工程中就會無法成功執行構建。
但如果這類工具總是本地安裝,我們要調用一個命令,要手動添加 ./node_modules/.bin 這個長長的前綴,未免也太麻煩了,我們 nodejs 開發者都很懶的。于是 npm 從5.2 開始自帶了一個新的工具 npx.
5.3 npxnpx 的使用很簡單,就是執行 npx
除了這種最簡單的場景, npm cli 團隊開發者 Kat Marchán 還在這篇文章中介紹了其他幾種 npx 的神奇用法: Introducing npx: an npm package runner, 國內有位開發者 robin.law 將原文翻譯為中文 npx是什么,為什么需要npx?.
有興趣的可以戳鏈接了解,懶得點鏈接的,看總結:
場景a) 一鍵執行遠程 npm 源的二進制包除了在 package 中執行 ./node_modules/.bin 中已安裝的命令, 還可以直接指定未安裝的二進制包名執行。例如我們在一個沒有 package.json 也沒有 node_modules 的目錄下,執行:
npx cowsay hello
npx 將會從 npm 源下載 cowsay 這個包(但并不安裝)并執行:
_______ < hello > ------- ^__^ (oo)\_______ (__) )/ ||----w | || ||
這種用途非常適合 1. 在本地簡單測試或調試 npm 源上這些二進制包的功能;2. 調用 create-react-app 或 yeoman 這類往往每個項目只需要使用一次的腳手架工具
PS: 此處有彩蛋,執行這條命令試試:
npx workin-hard場景b) 一鍵執行 GitHub Gist
還記得前面提到的 2.1 package定義 么,npm install
剛好 GitHub Gist 也是 git 倉庫 的一種,集合 npx 就可以方便地將簡單的腳本共享給其他人,擁有該鏈接的人無需將腳本安裝到本地工作目錄即可執行。將 package.json 和 需執行的二進制腳本上傳至 gist, 在運行 npx
原文作者 Kat Marchán 提供了這個示例 gist, 執行:
npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
可得到一個來自 GitHubGist 的 hello world 問候。
場景c) 使用不同版本 node 執行命令將 npx 與 Aria Stewart 創建的 node 包 (https://www.npmjs.com/package... 結合,可以實現在一行命令中使用指定版本的 node 執行命令。
例如先后執行:
npx node@4 -e "console.log(process.version)" npx node@6 -e "console.log(process.version)"
將分別輸出 v4.8.7 和 v6.13.0.
往常這種工作是由 nvm 這類 node 版本管理工具來做的,但 npx node@4 這種方式免去 nvm 手動切換配置的步驟,更加簡潔簡單。
6. npm 配置 6.1 npm confignpm cli 提供了 npm config 命令進行 npm 相關配置,通過 npm config ls -l 可查看 npm 的所有配置,包括默認配置。npm 文檔頁為每個配置項提供了詳細的說明 https://docs.npmjs.com/misc/c... .
修改配置的命令為 npm config set
proxy, https-proxy: 指定 npm 使用的代理
registry 指定 npm 下載安裝包時的源,默認為 https://registry.npmjs.org/ 可以指定為私有 Registry 源
package-lock 指定是否默認生成 package-lock 文件,建議保持默認 true
save true/false 指定是否在 npm install 后保存包為 dependencies, npm 5 起默認為 true
刪除指定的配置項命令為 npm config delete
除了使用 CLI 的 npm config 命令顯示更改 npm 配置,還可以通過 npmrc 文件直接修改配置。
這樣的 npmrc 文件優先級由高到低包括:
工程內配置文件: /path/to/my/project/.npmrc
用戶級配置文件: ~/.npmrc
全局配置文件: $PREFIX/etc/npmrc (即npm config get globalconfig 輸出的路徑)
npm內置配置文件: /path/to/npm/npmrc
通過這個機制,我們可以方便地在工程跟目錄創建一個 .npmrc 文件來共享需要在團隊間共享的 npm 運行相關配置。比如如果我們在公司內網環境下需通過代理才可訪問 registry.npmjs.org 源,或需訪問內網的 registry, 就可以在工作項目下新增 .npmrc 文件并提交代碼庫。
proxy = http://proxy.example.com/ https-proxy = http://proxy.example.com/ registry = http://registry.example.com/
因為項目級 .npmrc 文件的作用域只在本項目下,所以在非本目錄下,這些配置并不生效。對于使用筆記本工作的開發者,可以很好地隔離公司的工作項目、在家學習研究項目兩種不同的環境。
將這個功能與 ~/.npm-init.js 配置相結合,可以將特定配置的 .npmrc 跟 .gitignore, README 之類文件一起做到 npm init 腳手架中,進一步減少手動配置。
6.3 node 版本約束雖然一個項目的團隊都共享了相同的代碼,但每個人的開發機器可能安裝了不同的 node 版本,此外服務器端的也可能與本地開發機不一致。
這又是一個可能帶來不一致性的因素 —— 但也不是很難解決,聲明式約束+腳本限制即可。
聲明:通過 package.json 的 engines 屬性聲明應用運行所需的版本運行時要求。例如我們的項目中使用了 async, await 特性,查閱兼容性表格得知最低支持版本為 7.6.0,因此指定 engines 配置為:
{ "engines": { "node": ">=7.6.0"} }
強約束(可選):在 npm 中以上字段內容僅作為建議字段使用,若要在私有項目中添加強約束,需要自己寫腳本鉤子,讀取并解析 engines 字段的 semver range 并與運行時環境做對比校驗并適當提醒。
7. 小結 npm 最佳實踐使用 npm-init 初始化新項目
統一項目配置: 需團隊共享的 npm config 配置項,固化到 .npmrc 文件中
統一運行環境,統一 package.json,統一 package-lock 文件
合理使用多樣化的源安裝依賴包: npm install
使用 npm: >=5.2 版本
使用 npm scripts 與 npx (npm: >=5.2) 腳本管理應用相關腳本
8. 更多資料參考
npm team 成員 Ashley Williams 在 2016 年 Node.js Live 上的 talk: You Don"t Know npm, 當時還沒有 npm 5
YouTube 視頻鏈接: Node.js Live (Paris) - Ashley Williams, You Don"t Know npm
演講用的 slides: the ag_deck
這篇 2015 年的文章介紹了如何使用把本地模塊打包到 node_modules 依賴中: Build modular application with npm local modules
一篇很好的介紹 package-lock.json 的文章: Everything you wanted to know about package-lock.json
阮一峰 npm scripts 使用指南
Kat Marchán 介紹npx:
原文 Introducing npx: an npm package runner
中文 npx是什么,為什么需要npx?
文檔
npm 官方文檔, 無中文翻譯
package.json 文件
npm config 配置
npm semver 計算器
node_modules 目錄扁平化
yarn 中文文檔,雖然是 npm 競爭者但兼容 package.json 和 node_modules 目錄,因此這兩部分一樣可參考:
package.json - 中文
依賴與版本 - 中文
延伸閱讀
sam boyer 《所以你想開發一個包管理系統》,從無關特定語言的角度,介紹一個包管理系統的方面: So you want to write a package manager
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93656.html
摘要:幸運的是,供應似乎與需求相匹配,并且有多種選擇。讓我們來看看年值得關注的十大動畫庫。八年了,仍然是一個強大的動畫工具。接下來在這個令人驚嘆的動畫庫列表上的就是了。,通常被稱為動畫平臺,我們忽略它在列表中的排名,它是列表中最受歡迎的庫之一。 原文鏈接原譯文鏈接 現代網站的客戶端依靠高質量的動畫,這就使得JavaScript動畫庫的需求不斷增加。幸運的是,供應似乎與需求相匹配,并且有多種選...
摘要:因為涉及業務敏感話題,本文只記錄我們學習的歷程。我由衷的感謝團隊的小伙伴們,感謝你們的堅韌不拔,感謝你們的持續成長。這個變化只是在每天的堅持和刻意練習中發生的,是那么的神奇。 因為涉及業務敏感話題,本文只記錄我們學習的歷程。 關于堅持 ??從2016年起,我們團隊堅持每天早晨8:50-10:30的100分鐘早晨討論,到現在已經兩年了,期間沒有中斷過。我由衷的感謝團隊的小伙伴們,感謝你們...
摘要:因為涉及業務敏感話題,本文只記錄我們學習的歷程。我由衷的感謝團隊的小伙伴們,感謝你們的堅韌不拔,感謝你們的持續成長。這個變化只是在每天的堅持和刻意練習中發生的,是那么的神奇。 因為涉及業務敏感話題,本文只記錄我們學習的歷程。 關于堅持 ??從2016年起,我們團隊堅持每天早晨8:50-10:30的100分鐘早晨討論,到現在已經兩年了,期間沒有中斷過。我由衷的感謝團隊的小伙伴們,感謝你們...
摘要:第二十二期掘金團隊請來了進階解密作者劉望舒做了為期三天的活動活動已結束。我們在此精選了一些來自用戶的提問及劉望舒的回答。提醒本期分布式微服務主題的正在進行,歡迎前去提問,傳送門關于劉望舒進階之光進階解密的作者,安卓巴士等技術大會特邀講師。第二十二期 AMA 掘金團隊請來了《Android進階解密》作者-- 劉望舒做了為期三天的 Ask Me Anything (AMA) 活動(活動已結束)。...
閱讀 2807·2021-11-22 14:44
閱讀 548·2021-11-22 12:00
閱讀 3689·2019-08-30 15:54
閱讀 1580·2019-08-29 17:15
閱讀 1906·2019-08-29 13:50
閱讀 1116·2019-08-29 13:17
閱讀 3522·2019-08-29 13:05
閱讀 1186·2019-08-29 11:31