JS 套件管理大師
Caution
🚨 CAUTION |
---|
Windows 使用 NTFS 檔案系統, npm / pnpm 的軟連結在 Windows 平台是採用 JUNCTION 實現的, 也是一個類似軟連結的機制, VScode 會判定這東西是軟連結, 但使用 Python 的 os.path.islink 是測不出來的。 |
Prerequisites
module resolution algorithm (node)
假設 node 要找一個第三方模組 art-template
1. 內建模組:若是內建模組名稱,加載模組
2. 本地模組:若是相對路徑或絕對路徑,加載模組
3. 第三方模組:若上面兩個都不是,那就是第三方了
a. 尋找當前目錄中,有無 node_modules 目錄
i. 沒有就往上一層找,退回步驟 3.a
b. 尋找 node_modules 目錄中,有無 art-template 目錄
i. 沒有就往上一層找,退回步驟 3.a
c. 尋找 art-template 目錄中,有無 package.json
i. 若無則預設 import art-template/index.js
ii. 若連 index.js 也沒有,就往上一層找,退回步驟 3.a
d. 尋找 package.json 內的 "main" key,其 value 將會是最終被 import 的檔案
i. 若無則預設 import art-template/index.js
ii. 若連 index.js 也沒有,就往上一層找,退回步驟 3.a
node_modules
before npm v3
node_modules
├── A@1.0.0
│ └── node_modules
│ └── C@1.0.0
├── B@1.0.0
│ └── node_modules
│ └── C@1.0.0
├── C@1.0.1
└── D@1.0.0
嵌套式的依賴結構。 每個套件都直接包含了各自使用的套件, 這會導致:
- 路徑長到靠北:node_modules/A/node_modules/C/node_modules...
- 一大堆重複安裝的套件
npm v3
node_modules
├── A@1.0.0
│ └── node_modules
│ └── C@1.0.0
├── B@1.0.0
│ └── node_modules
│ └── C@1.0.0
├── C@1.0.1
└── D@1.0.0
扁平化的依賴結構。 依賴關係通通都被抬升 (hoisting) 到最頂層, 這樣就不用重複安裝這麼多套件了,只保留一份即可。 但有個要注意的點:只有最新版套件會被 hoisting, 若有舊版套件被依賴,仍會保留在它們各自的 node_modules。 如上,假設 D@1.0.0 依賴最新版套件 C@1.0.1,這樣 C@1.0.1 放在頂層沒問題。 而 A@1.0.0 和 B@1.0.0 依賴舊版套件 C@1.0.0, 它們就只能將 C@1.0.0 放在各自的 node_modules。
這與 Python pip 的套件管理不同,雖然它也是扁平式的沒錯, 但 pip 不會出現一個套件有兩個版本的情況。 這也是因為 Node.js Module Resolution Algorithm 的特殊性, 所以 Node.js 的 npm 可以這樣玩,但 Python 的 pip 不能。 |
npm v5
package-lock.json (lockfile) 的出現
npm 的 package.json (或者是 pip 的 requirements.txt) 指定的範圍版本號雖然對相容性友好, 但在多人開發時,由於團隊成員 install 套件的時間差, 可能在過程中,依賴套件有新版本釋出,這就導致可能下載到不同版本。 這表示大家一同開發的專案,開發時運行的依賴套件的版本卻可能不盡相同。 這在多人開發中是一大隱憂。 也因此 lockfile 應運而生,例如:
- npm 的 packge-lock.json
- yarn 的 yarn.lock
- pnpm 的 pnpm-lock.yaml
pip(抱歉了沒有你)- poetry 的 poetry.lock
- ...
其作用就是將下載的依賴套件的精確版本號記錄下來, 這樣子的話,只要團隊中所有成員拿到的是同一個 package-lock.json, 它們 install 的每個依賴套件的版本也將會是相同的。
package.json
只記錄一些特別的
bin
: key 表示 CLI 名稱,value 表示 JS 腳本- 全域安裝時,會在管理目錄頂層放 shell 腳本,它會執行你 value 指定的腳本 (管理目錄有註冊在 PATH 環境變數,也就是說 CLI 名稱是全域可見的)
- 區域安裝時,會在
node_modules/bin
目錄放 shell 腳本,它會執行你 value 指定的腳本 (詳見npm run
)
{ "bin": { "q-cli": "./bin/q-cli.js" } }
Nouns
lockfile
即「紀錄所有套件的精確版本號的檔案」。
scoped package
開頭有 @
的套件,比如 @babel/eslint-parser
、@vue/cli-service
,
有點類似命名空間,可以防止自己寫的套件名稱和別人衝突。
flat dependency directory
phantom dependency
即「幽靈依賴套件」。詳見此文。 假設專案使用到 axios 套件,package.json 應長這樣
{
"dependencies": {
"axios": "^1.7.2"
}
}
然而 axios 套件內部依賴 follow-redirects,所以你很合理的可以這樣寫
const followRedirects = require('follow-redirects')
console.log(followRedirects)
follow-redirects 就是所謂的 phantom dependency, 雖然看起來好像沒甚麼,但這通常被視為一種 anti-pattern, 假設某一天 axios 套件出新版本了, 在新版本中,它可能不依賴 follow-redirects 套件了。 然後你的 npm 根據專案 packge.json 重新下載 axios 套件最新版, 你的舊代碼就炸裂了。
peer dependency
簡單來說就是插件 (plugins) 和宿主 (host) 之間的依賴關係。詳見 npm doc。
而 peer dependency 指定的版本,就是插件所依賴的宿主版本。
有玩過魔獸世界嗎?印象中最多人用的就是彩虹 UI 插件包嘛。
首先:
- plugins (插件) 不能獨立運行,它必須倚靠 host (魔獸) 才能運行。
- plugins (插件) 是會過期的,通常僅限於 host (魔獸) 某幾個版本能用。 比如要塞系統插件,只有德拉諾之霸 (也就是 6 版) 能用 (我真它〇老人), 而這就與 host 版本更新 (魔獸本身的版本更新) 有關。
拿魔獸當例子的話,比如:
{
"name": "要塞小助手",
"version": "1.0.0",
"peerDependencies": {
"魔獸世界": "6.x"
}
}
舉個實際點的例子,Ant Design 和 React:
{
"name": "antd",
"version": "5.21.6",
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
}
monorepo
待續...
npm
套件管理工具
references
- 🔗 package.json
- 🔗 npm SemVer Calculator
- 🔗 PJCHENder - 發布 npm 套件
- 🎬 PJCHENder - npm create 是什麼?
- 🎬 How To Create And Publish Your First NPM Package
note
📘 NOTE |
---|
Windows 管理目錄:~\AppData\Roaming\npm |
🔮 IMPORTANT |
---|
使用 command line 執行 CLI 工具:能執行 global 安裝的 CLI 工具 |
使用 npm scripts 執行 CLI 工具:能執行 local 安裝的 CLI 工具 |
npm init
| npm create
雖然它倆是 alias,但後兩個 case 比較常用 create
npm init
:初始化專案,會創建 package.jsonnpm create <pkg>
: 同npm exec create-<pkg>
(前綴 create)npm init foo
->npm exec create-foo
npm create <@scoped>
: 同npm exec <@scoped>/create
(前綴 create)npm init @usr/foo
->npm exec @usr/create-foo
npm install
| npm i
npm install <pkg>
:local 安裝、生產依賴套件npm install -g <pkg>
:global 安裝、生產依賴套件npm install --save-dev <pkg>
:local 安裝、開發依賴套件npm install --package-lock-only
:只產生 package-lock.json- 🔮 IMPORTANT:在僅使用 pnpm 的專案中,若想支援 npm,這個指令十分好用
npm install
:根據 package.json 的紀錄下載套件- 若存在 package-lock.json,將按照此檔案指定的精確版本號下載套件
npm update
npm update
:更新所有已安裝的依賴套件
npm uninstall
npm uninstall <pkg>
移除指定依賴套件
npm list
npm list
:已安裝的套件npm list -g
:顯示全域安裝的依賴套件
npm outdated
npm outdated
:顯示可更新的套件
npm run
執行在 package.json 中指定的 scripts (npm scripts)。
只能執行 local 套件的 scripts。
在 local 專案的 node_modules/.bin
目錄中,尋找 shell scripts 執行。
假設是 sass 的,那麼 shell scripts 的內容基本上就是執行 node node_modules/sass/sass.js
。
npm run <script-name>
{ "scripts": { "start": "node app.js", "test": "jest" } }
npm exec
| npm x
執行不在 package.json 中指定的 scripts。
若 local 沒有安裝套件,會去 global 找,
若 global 也沒有安裝套件,會暫時安裝,執行完畢後再解除安裝。
npm exec -- <command> [arg]...
npm exec --package <pkg> -- <command> [arg]...
--package
:當套件名稱與它在.bin
的 shell scripts 名稱不同時,指定套件名稱。- 會指定這個選項,表示你應該是想要暫時安裝。
npm cache
npm cache clean --force
:清除 npm 快取 (功能同 pip 快取)
npm link
在測試發布套件時很好用
☢️ WARNING |
---|
不會處理依賴套件版本衝突問題 |
npm link
: 在 global 套件中,放置一個當前目錄的軟連結 (假安裝)- 取消假安裝也很簡單,就是
uninstall
- 取消假安裝也很簡單,就是
npm link <pkg>
: global 安裝它 (若在 global 套件中找不到它),再以軟連結形式 local 安裝它
npm login
登入 npm
npm publish
發布必須登入 npm
npm publish
: 發布npm publish --access=public
: public 發布
🚨 CAUTION |
---|
npm 的 private 發布是付費功能, 注意 scoped package 預設是 private 發布, 如果免費帳號使用預設是會發布失敗的,要手動指定成 public 發布。 |
☢️ WARNING |
---|
在 npm 發布套件後,無法再撤下套件的條件:npm unpublish policy |
npx
功能與 npm exec
完全相同,指令也完全相同
note
📘 NOTE |
---|
Linux 暫存目錄:~/.npm/_npx |
Windows 暫存目錄:~\AppData\Local\npm-cache\_npx |
npm-run-all
一個開源小專案,使用 npm scripts 執行多個腳本時更方便。
- Simplify
- Before:
npm run clean && npm run build:css && npm run build:js && npm run build:html
- After:
npm-run-all clean build:*
- Cross platform
- 在 Linux 平台中,
&
是非阻塞執行的,但在 Windows 平台是阻塞執行的。 run-p
能確保腳本在 Windows 是非阻塞執行的。
references
run-p
| npm-run-all --parallel
- parallel 運行多個腳本
run-s
| npm-run-all --sequential
- sequential 運行多個腳本
nvm
多版本 Node.js 環境
references
note
🚨 CAUTION |
---|
Windows 版和 Linux 版的指令似乎不大一樣 |
Windows 版不能用 ls-remote 和 alias QQ |
🔮 IMPORTANT |
---|
每次開啟 Shell 時, nvm 會抓取 alias 為 default 的版本 (Linux) |
nvm ls-remote
(Linux)
列出遠端可用的 Node.js 版本
nvm list
列出本地可用的 Node.js 版本
nvm install
安裝指定 Node.js 版本
nvm alias
(Linux)
nvm alias <alias> <version>
:設定版本別名
nvm use
使用指定 Node.js 版本
nvm current
當前使用 Node.js 版本
pnpm
套件管理工具 (🔥大推) 有可能是當今地表上,綜合指標最頂尖的 package manager
references
note
🚨 CAUTION |
---|
初次使用 pnpm 時,要先用 pnpm setup 設定,它會加一個 PNPM_HOME 的環境變數 (管理目錄路徑),接著重開 terminal 就可以用了 |
📘 NOTE |
---|
Linux 管理目錄:~/.local/share/pnpm |
Windows 管理目錄:~\AppData\Local\pnpm |
📘 NOTE |
---|
管理目錄裡的 global 目錄,放的是全域套件,基本上同 npm 的全域安裝 |
管理目錄裡的 store 目錄,放的是區域套件,pnpm 會統一管理所有專案用到的套件 |
Why pnpm?
1. 省空間
🔮 IMPORTANT |
---|
每個專案都有各自的依賴套件,即便是依賴同種套件也可能是不同的版本, 這每種套件的每種版本,就會在 store 目錄被統一管理, 然後在專案內區域安裝時, 再藉由 hardlink 連結到 store 目錄中的套件用到的那些檔案即可。 |
稍微偷看一下裡面的目錄結構,大膽猜測原理應該類似 git object model 的概念, 以檔案做為基本單位 (object) 劃分,不同版本之間,只儲存有差異的檔案, 以此實現不重複儲存相同檔案的技術。 |
🔮 IMPORTANT |
---|
騙人!你說使用 hardlink 會省空間,可是我打開目錄詳細資訊的所占空間還是很胖耶! |
沒有騙人。舉個例子,圖書館有 5 本電子書 (檔案), 現在有 4 個人 (專案) 各自借了裡面的任意 3 本電子書, 每個人都會自稱它擁有 3 本電子書 (你在目錄詳細資訊看到的所占空間), 但實際上,圖書館並沒有 12 本電子書,而只有 5 本電子書 (真正所占空間)。 |
2. 省時間
這裡有 benchmark,讓你比一比有多快
- npm
- pnpm
3. 非扁平化
專案內 (local) 的 node_modules 目錄,採用非扁平化的方式管理
🔮 IMPORTANT |
---|
用 bottom-up 的脈絡化思考,更好理解為什麼 pnpm 要採用這種方式。 |
- bar 和 foo 是兩個套件。
bar (v1.0.0) 是明確使用套件:也就是在 package.json 指定的套件。
foo (v1.0.0) 是依賴套件:也就是明確使用套件 (bar) 依賴的那些套件。├── foo └── bar
- 為了讓 bar 看得到 foo (以及其他它需要的依賴套件),
我們要在最外層加一個 node_modules。
(複習:Node.js Module Resolution Algorithm)└── node_modules ├── foo └── bar
- 這是針對 bar (v1.0.0) 的依賴情況,所以我們把它裝成一個目錄,
而每個套件都有它們的依賴情況,它們都需要一個目錄。└── bar@1.0.0 └── node_modules ├── foo └── bar (主角)
- 這每個目錄就會被統一放在 .pnpm 目錄下,
所以說,專案所有套件都會放在 .pnpm 目錄底下。
(所有套件 = 明確使用套件 + 依賴套件).pnpm ├── foo@1.0.0 └── bar@1.0.0 └── node_modules ├── foo └── bar
- 開始接硬連結 (foo 沒有依賴套件,可直接硬連結)
.pnpm ├── foo@1.0.0 -> <store>/foo@1.0.0 (hard link) └── bar@1.0.0 └── node_modules ├── foo └── bar -> <store>/bar@1.0.0 (hard link)
- 然後接軟連結 (軟連結導向到硬連結,硬連結再導向真正的資源位置)
.pnpm ├── foo@1.0.0 -> <store>/foo@1.0.0 └── bar@1.0.0 └── node_modules ├── foo -> ../../foo@1.0.0 (symbolic link) └── bar -> <store>/bar@1.0.0
- 由於我們的明確使用套件只有 bar,
pnpm 為了避免 phantom dependency,
再外面又多包一層 node_modules (頂層目錄),
並只把明確使用套件放在頂層目錄供 import。node_modules ├── bar └── .pnpm ├── foo@1.0.0 -> <store>/foo@1.0.0 └── bar@1.0.0 └── node_modules ├── foo -> ../../foo@1.0.0 └── bar -> <store>/bar@1.0.0
- 最後一步,把明確使用套件的軟連結接上 (完工了!)
node_modules ├── bar -> ./.pnpm/bar@1.0.0/node_modules/bar (symbolic link) └── .pnpm ├── foo@1.0.0 -> <store>/foo@1.0.0 └── bar@1.0.0 └── node_modules ├── foo -> ../../foo@1.0.0 └── bar -> <store>/bar@1.0.0
pnpm add
安裝依賴套件。
同 npm install <package>
。
🚨 CAUTION |
---|
再次提醒,local 專案下載的套件會影響到 store 目錄 |
📗 TIP |
---|
若 dev 誤裝成 prod,指定選項可自動移除並重裝一次 |
--save-prod
(-P
)--save-dev
(-D
)--save-optional
(-O
)--save-peer
pnpm install
根據 package.json 安裝所有依賴套件。
--shamefully-hoist
: 將所有依賴套件提升到頂層 (shamefully,呵呵,還不忘羞辱你這違反他們的宗旨)
pnpm create
效果同 npm create
pnpm list
pnpm list --depth 1
: 列出依賴樹 (深度可加)
pnpm run
執行在 package.json 中指定的 scripts (npm scripts)。
同 npm run <script-name>
和 pnpm <script-name>
。
📗 TIP |
---|
run 可以不用寫 |
📗 TIP |
---|
<script-name> 可用 regex,可執行多個 |
pnpm exec
執行不在 package.json 中指定的 scripts。
類似 npm exec <script-name>
,但無法暫時下載 (所以不能用 --package
選項)。
pnpm dlx
執行不在 package.json 中指定的 scripts。
類似 npm exec <script-name>
,但專門用來暫時下載。
pnpm dlx <command> [arg]...
pnpm --package=<pkg> dlx <command> [arg]...
pnpm store
pnpm store add
: 向 store 目錄新增套件 (套件不會安裝到 local 專案)pnpm store path
:store 目錄位置pnpm store prune
:清理 store 目錄中沒被任何專案引用到的套件 (orphan)📗 TIP 建議定期清理
pnpm import
pnpm import
:支援將以下幾種 lockfile 轉成 pnpm 可以辨識的pnpm-lock.yaml
package-lock.json
npm-shrinkwrap.json
yarn.lock
pnpm licenses
pnpm licenses list
:列出所有用到的套件的 licenses
Publishing Package
pnpm link
pnpm link --global
: 類似npm link
pnpm link --global <pkg>
: 類似npm link <pkg>
pnpm unlink
pnpm unlink <pkg>
: 移除 local 假安裝軟連結套件
pnpm remove
pnpm remove --global <pkg>
: 移除 global 假安裝軟連結套件