package.json
package.json
的屬性 "dependencies"
package.json
的屬性 "bin"
package.json
的屬性 "license"
node:
協定匯入本章說明 npm 套件是什麼,以及它們如何與 ESM 模組互動。
必要的知識:我假設你對 ECMAScript 模組的語法略有了解。如果你不了解,你可以閱讀「JavaScript for impatient programmers」中的〈模組〉一章。
在 JavaScript 生態系統中,套件 是一種組織軟體專案的方式:它是一個具有標準化配置的目錄。套件可以包含各種檔案,例如
套件可以依賴其他套件(稱為其相依性),其中包含
套件的相依性安裝在該套件內部(我們很快就會看到如何執行)。
套件之間的一個常見區別是
下一個小節說明如何發布套件。
發布套件的主要方式是將其上傳到套件註冊表 – 一個線上軟體儲存庫。事實上的標準是npm 註冊表,但它並非唯一的選項。例如,公司可以主機它們自己的內部註冊表。
套件管理員是一個命令列工具,用於從註冊表(或其他來源)下載套件並在本地或全域安裝它們。如果套件包含 bin 腳本,它也會在本地或全域提供這些腳本。
最受歡迎的套件管理員稱為npm,並與 Node.js 捆綁在一起。它的名稱最初代表「Node 套件管理員」。後來,當 npm 和 npm 註冊表不僅用於 Node.js 套件時,定義被更改為「npm 不是套件管理員」(來源)。
還有其他流行的套件管理員,例如 yarn 和 pnpm。所有這些套件管理員預設使用 npm 註冊表。
npm 註冊表中的每個套件都有名稱。有兩種名稱
全域名稱在整個註冊表中是唯一的。以下兩個是範例
minimatch mocha
範圍名稱由兩部分組成:範圍和名稱。範圍是全球唯一的,名稱在每個範圍內都是唯一的。以下是兩個範例
@babel/core
@rauschma/iterable
範圍以 @
符號開頭,並以斜線與名稱分隔。
一旦套件 my-package
完全安裝,它幾乎總是看起來像這樣
my-package/
package.json
node_modules/
[More files]
這些檔案系統條目的用途是什麼?
package.json
是每個套件都必須有的檔案
node_modules/
是安裝套件相依性的目錄。每個相依性還有一個包含其相依性的 node_modules
資料夾,依此類推。結果是一個相依性樹。有些套件也有檔案 package-lock.json
,它位於 package.json
旁邊:它記錄已安裝相依性的確切版本,如果我們透過 npm 新增更多相依性,它會保持最新狀態。
package.json
這是透過 npm 建立的入門 package.json
{
"name": "my-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
這些屬性的用途是什麼?
某些屬性是公開套件(在 npm 註冊表上發布)所必需的
name
指定此套件的名稱。version
用於版本管理,並遵循 語意化版本控制,使用三個以點分隔的數字
公開套件的其他屬性是選用的
description
、keywords
、author
為選填,有助於尋找套件。license
說明此套件的使用方式。如果套件以任何方式公開,建議提供此值。“選擇開源授權” 可協助做出選擇。main
是具有函式庫程式碼的套件的屬性。它指定「就是」套件的模組(本章稍後說明)。
scripts
是用於設定套件指令碼的屬性,也就是開發時間殼層指令的縮寫。這些指令碼可透過 npm run
執行。例如,指令碼 test
可透過 npm run test
執行。有關此主題的更多資訊,請參閱 §15「透過 npm 套件指令碼執行跨平台任務」。
其他有用的屬性
dependencies
列出套件的相依性。其格式將在稍後說明。
devDependencies
是僅在開發期間需要的相依性。
下列設定表示所有副檔名為 .js
的檔案都將詮釋為 ECMAScript 模組。除非我們處理的是舊有程式碼,否則建議加入此設定
"type": "module"
bin
列出bin 指令碼,也就是 npm 安裝為殼層指令碼的套件內的 Node.js 模組。其格式將在稍後說明。
license
指定套件的授權。其格式將在稍後說明。
一般來說,name
和 version
屬性為必填,如果遺漏,npm 會發出警告。不過,我們可以透過下列設定變更此設定
"private": true
這可防止套件意外發布,並允許我們省略名稱和版本。
有關 package.json
的更多資訊,請參閱 npm 文件。
package.json
的 "dependencies"
屬性以下是 package.json
檔案中相依性的外觀
"dependencies": {
"minimatch": "^5.1.0",
"mocha": "^10.0.0"
}
這些屬性會記錄套件名稱和其版本的限制條件。
版本本身遵循 語意化版本 標準。它們最多有三個數字(第二和第三個數字為選填,預設為零),並以點號分隔
Node 的版本範圍說明在 semver
儲存庫 中。範例包括
沒有任何額外字元的特定版本表示已安裝的版本必須與版本完全相符
"pkg1": "2.0.1",
major.minor.x
或 major.x
表示數字組成部分必須相符,x
或省略的組成部分可以有任何值
"pkg2": "2.x",
"pkg3": "3.3.x",
*
相符任何版本
"pkg4": "*",
>=version
表示已安裝的版本必須為 version
或更高
"pkg5": ">=1.0.2",
<=version
表示已安裝的版本必須為 version
或更低
"pkg6": "<=2.3.4",
version1-version2
與 >=version1 <=version2
相同
"pkg7": "1.0.0 - 2.9999.9999",
^version
(如前一個範例中所用)是範圍符號,表示已安裝的版本可以為 version
或更高,但不得引入重大變更。也就是說,主要版本必須相同
"pkg8": "^4.17.21",
package.json
的屬性 "bin"
這是我們可以告訴 npm 將模組安裝為 shell 腳本的方式
"bin": {
"my-shell-script": "./src/shell/my-shell-script.mjs",
"another-script": "./src/shell/another-script.mjs"
}
如果我們使用此 "bin"
值在全球安裝套件,Node.js 會確保命令 my-shell-script
和 another-script
可在命令列中使用。
如果我們在本地安裝套件,我們可以在套件腳本或透過 npx
命令 使用這兩個命令。
字串也可以作為 "bin"
的值
{
"name": "my-package",
"bin": "./src/main.mjs"
}
這是縮寫
{
"name": "my-package",
"bin": {
"my-package": "./src/main.mjs"
}
}
package.json
的屬性 "license"
屬性 "license"
的值永遠是包含 SPDX 授權識別碼的字串。例如,下列值會拒絕其他人使用套件的任何條款(如果套件未發布,這很有用)
"license": "UNLICENSED"
SPDX 網站列出所有可用的授權識別碼。如果您發現很難選擇一個,網站「選擇開源授權」 可以提供協助,例如,如果您「想要簡單且寬鬆」時,這是一個建議
MIT 授權簡短且切中要點。它允許人們對您的專案做幾乎任何他們想做的事,例如製作和發行封閉原始碼版本。
Babel、.NET 和 Rails 使用 MIT 授權。
您可以這樣使用該授權
"license": "MIT"
npm 儲存庫中的套件通常以兩種不同的方式封存
無論如何,套件都會在沒有其相依項目的情況下封存,我們在使用它之前必須安裝這些相依項目。
如果套件儲存在 git 儲存庫中
package-lock.json
的原因。如果套件發布到 npm 註冊表
package-lock.json
永遠不會上傳到 npm 註冊表的原因。開發相依項目(package.json
中的屬性 devDependencies
)只會在開發期間安裝,但在我們從 npm 註冊表安裝套件時不會安裝。
請注意,在開發期間,git 儲存庫中未發布的套件處理方式與已發布的套件類似。
要從 git 安裝套件 pkg
,我們複製其儲存庫,然後
cd pkg/
npm install
然後執行下列步驟
node_modules
並安裝相依項目。安裝相依項目也表示下載該相依項目並安裝其相依項目(等等)。package.json
設定。如果根套件沒有 package-lock.json
檔案,它會在安裝期間建立(如前所述,相依項目沒有這個檔案)。
在相依項目樹中,同一個相依項目可能會存在多次,可能使用不同的版本。有方法可以將重複降到最低,但這超出了本章的範圍。
這是修正相依項目樹中問題的一個(有點粗糙的)方法
cd pkg/
rm -rf node_modules/
rm package-lock.json
npm install
請注意,這可能會導致安裝不同、較新的套件。我們可以透過不刪除 package-lock.json
來避免此情況。
有許多工具和技術可供設定新套件。這是一個簡單的方法
mkdir my-package
cd my-package/
npm init --yes
之後,目錄會如下所示
my-package/
package.json
此 package.json
具有我們已經看過的入門內容。
現在,my-package
沒有任何相依項。假設我們想要使用函式庫 lodash-es
。以下是我們將其安裝到套件中的方式
npm install lodash-es
此命令執行下列步驟
套件會下載到 my-package/node_modules/lodash-es
。
其相依項也會安裝。然後是其相依項的相依項。等等。
新的屬性會新增到 package.json
"dependencies": {
"lodash-es": "^4.17.21"
}
package-lock.json
會更新為已安裝的確切版本。
其他 ECMAScript 模組中的程式碼可透過 import
陳述式存取(A 行和 B 行)
// Static import
import {namedExport} from 'https://example.com/some-module.js'; // (A)
console.log(namedExport);
// Dynamic import
import('https://example.com/some-module.js') // (B)
.then((moduleNamespace) => {
console.log(moduleNamespace.namedExport);
; })
靜態匯入和動態匯入都使用 模組指定符 來參照模組
from
之後的字串。有三種模組指定符
絕對指定符 是完整 URL – 例如
'https://www.unpkg.com/browse/yargs@17.3.1/browser.mjs'
'file:///opt/nodejs/config.mjs'
絕對指定符主要用於存取直接在網路上主機的函式庫。
相對指定符 是相對 URL(以 '/'
、'./'
或 '../'
開頭)– 例如
'./sibling-module.js'
'../module-in-parent-dir.mjs'
'../../dir/other-module.js'
每個模組都有 URL,其協定取決於其位置(file:
、https:
等)。如果它使用相對指定符,JavaScript 會根據模組的 URL 解析該指定符,將其轉換為完整 URL。
相對指定符主要用於存取相同程式碼庫中的其他模組。
裸指定符 是路徑(沒有協定和網域),既不以斜線開頭,也不以點開頭。它們以套件名稱開頭。這些名稱可以選擇後接 子路徑
'some-package'
'some-package/sync'
'some-package/util/files/path-tools.js'
裸指定符也可以參照具有範圍名稱的套件
'@some-scope/scoped-name'
'@some-scope/scoped-name/async'
'@some-scope/scoped-name/dir/some-module.mjs'
每個裸指定符都明確地指涉套件內的一個模組;如果它沒有子路徑,則指涉套件的指定「主要」模組。裸指定符從不直接使用,但總是解析為絕對指定符。解析方式取決於平台。我們很快就會了解更多。
.js
或 .mjs
。樣式 1:沒有子路徑
樣式 2:沒有檔案名稱副檔名的子路徑。在這種情況下,子路徑就像套件名稱的修改器
'my-parser/sync'
'my-parser/async'
'assertions'
'assertions/strict'
樣式 3:有檔案名稱副檔名的子路徑。在這種情況下,套件被視為模組的集合,而子路徑指向其中一個
'large-package/misc/util.js'
'large-package/main/parsing.js'
'large-package/main/printing.js'
樣式 3 裸指定符的注意事項:檔案名稱副檔名的解釋方式取決於依賴項,可能與匯入套件不同。例如,匯入套件可能會對 ESM 模組使用 .mjs
,對 CommonJS 模組使用 .js
,而依賴項匯出的 ESM 模組可能具有檔案名稱副檔名 .js
的裸路徑。
讓我們看看模組指定符如何在 Node.js 中運作。
Node.js 解析演算法 的運作方式如下
以下是演算法
如果指定符是絕對的,則解析已完成。最常見的三種協定
file:
用於本機檔案https:
用於遠端檔案node:
用於內建模組(稍後討論)如果指定符是相對的,則會根據匯入模組的 URL 解析。
如果指定符是裸的
如果以 '#'
開頭,則透過在套件匯入(稍後說明)中查詢並解析結果來解析。
否則,它是一個裸指定符,具有下列其中一種格式(子路徑是選用的)
«package»/sub/path
@«scope»/«scoped-package»/sub/path
解析演算法會遍歷目前目錄及其上層目錄,直到找到具有與裸指定符開頭相符的子目錄的 node_modules
目錄,亦即
node_modules/«套件»/
node_modules/@«範圍»/«範圍套件»/
該目錄是套件的目錄。預設情況下,套件 ID 之後的(可能為空的)子路徑會被解釋為相對於套件目錄。預設值可透過套件匯出覆寫,後續會說明。
解析演算法的結果必須指向一個檔案。這說明了為什麼絕對規格符和相對規格符總是具有檔案名稱副檔名。裸規格符大多數沒有,因為它們是會在套件匯出中查詢的縮寫。
模組檔案通常具有這些檔案名稱副檔名
.mjs
,則它始終是 ES 模組。.js
,則在最近的 package.json
中有此項目時,它是一個 ES 模組
"type": "module"
如果 Node.js 執行透過 stdin、--eval
或 --print
提供的程式碼,我們會使用 下列命令列選項,以便將其解釋為 ES 模組
--input-type=module
在此小節中,我們使用具有下列檔案配置的套件
my-lib/
dist/
src/
main.js
util/
errors.js
internal/
internal-module.js
test/
套件匯出 是透過 package.json
中的屬性 "exports"
指定,並支援兩個重要功能
沒有屬性 "exports"
時,可以透過套件名稱後的相對路徑存取套件 my-lib
中的每個模組,例如
'my-lib/dist/src/internal/internal-module.js'
屬性存在後,只能使用其中列出的規格符。其他所有內容都對外部隱藏。
回想裸規格符的三種樣式
套件匯出有助於我們處理所有三種樣式
package.json
:
{
"main": "./dist/src/main.js",
"exports": {
".": "./dist/src/main.js"
}
}
我們僅提供 "main"
以保持向後相容性(與較舊的打包器和 Node.js 12 及更早版本相容)。否則,"."
的輸入就足夠了。
有了這些套件匯出,我們現在可以從 my-lib
匯入,如下所示。
import {someFunction} from 'my-lib';
這會從這個檔案匯入 someFunction()
my-lib/dist/src/main.js
package.json
:
{
"exports": {
"./util/errors": "./dist/src/util/errors.js"
}
}
我們將規格說明子路徑 'util/errors'
對應到一個模組檔案。這會啟用以下匯入
import {UserError} from 'my-lib/util/errors';
前一個小節說明如何為沒有副檔名的子路徑建立單一對應。還有一種方法可以透過單一輸入建立多個此類對應
package.json
:
{
"exports": {
"./lib/*": "./dist/src/*.js"
}
}
現在可以匯入任何 ./dist/src/
的後代檔案,而不需要檔案副檔名
import {someFunction} from 'my-lib/lib/main';
import {UserError} from 'my-lib/lib/util/errors';
請注意此 "exports"
輸入中的星號
"./lib/*": "./dist/src/*.js"
這些是關於如何將子路徑對應到實際路徑的更多說明,而不是與檔案路徑片段相符的萬用字元。
package.json
:
{
"exports": {
"./util/errors.js": "./dist/src/util/errors.js"
}
}
我們將規格說明子路徑 'util/errors.js'
對應到一個模組檔案。這會啟用以下匯入
import {UserError} from 'my-lib/util/errors.js';
package.json
:
{
"exports": {
"./*": "./dist/src/*"
}
}
在這裡,我們縮短 my-package/dist/src
下整個子樹的模組規格說明
import {InternalError} from 'my-package/util/errors.js';
沒有匯出時,匯入陳述會是
import {InternalError} from 'my-package/dist/src/util/errors.js';
請注意此 "exports"
輸入中的星號
"./*": "./dist/src/*"
這些不是檔案系統 glob,而是關於如何將外部模組規格說明對應到內部規格說明的說明。
透過以下技巧,我們公開目錄 my-package/dist/src/
中的所有內容,但 my-package/dist/src/internal/
除外
"exports": {
"./*": "./dist/src/*",
"./internal/*": null
}
請注意,當匯出沒有檔案副檔名的子樹時,此技巧也適用。
我們也可以讓匯出 有條件:然後,給定的路徑會根據套件使用的環境對應到不同的值。
Node.js 與瀏覽器。例如,我們可以為 Node.js 和瀏覽器提供不同的實作
"exports": {
".": {
"node": "./main-node.js",
"browser": "./main-browser.js",
"default": "./main-browser.js"
}
}
"default"
條件在沒有其他金鑰相符時相符,並且必須放在最後。當我們區分平台時,建議有一個,因為它會處理新的和/或未知的平台。
開發與製作。條件套件匯出的另一個使用案例是在「開發」和「製作」環境之間切換
"exports": {
".": {
"development": "./main-development.js",
"production": "./main-production.js",
}
}
在 Node.js 中,我們可以這樣指定環境
node --conditions development app.mjs
套件匯入讓套件定義其本身可內部使用的模組規格縮寫(套件匯出定義其他套件的縮寫)。以下是一個範例
package.json
:
{
"imports": {
"#some-pkg": {
"node": "some-pkg-node-native",
"default": "./polyfills/some-pkg-polyfill.js"
}
},
"dependencies": {
"some-pkg-node-native": "^1.2.3"
}
}
套件匯入#
是有條件的(具有與有條件套件匯出相同的功能)
如果目前套件是在 Node.js 上使用,模組規格'#some-pkg'
會參考套件some-pkg-node-native
。
在其他地方,'#some-pkg'
會參考目前套件內的檔案./polyfills/some-pkg-polyfill.js
。
(只有套件匯入可以參考外部套件,套件匯出無法這麼做。)
套件匯入有哪些使用案例?
使用套件匯入搭配打包器時要小心:這項功能相對較新,你的打包器可能不支援。
node:
協定匯入Node.js 有許多內建模組,例如'path'
和'fs'
。這些模組都同時提供 ES 模組和 CommonJS 模組。這些模組的一個問題是,它們可能會被安裝在node_modules
中的模組覆寫,這既是一個安全風險(如果意外發生),也是一個問題,如果 Node.js 想在未來推出新的內建模組,而其名稱已被 npm 套件使用。
我們可以使用node:
協定來清楚表示我們要匯入內建模組。例如,以下兩個匯入陳述式幾乎是等效的(如果沒有安裝名稱為'fs'
的 npm 模組)
import * as fs from 'node:fs/promises';
import * as fs from 'fs/promises';
使用node:
協定的另一個好處是,我們可以立即看出匯入的模組是內建的。由於內建模組數量眾多,這在閱讀程式碼時很有幫助。
由於node:
規格具有協定,因此它們被視為絕對的。這就是為什麼它們不會在node_modules
中被查詢。