深入淺出 package-json,目測大多數人不瞭解它
npm 是前端開發人員廣泛使用的包管理工具,項目中通過 package.json 來管理項目中所依賴的 npm 包的配置。package.json 就是一個 json 文件,除了能夠描述項目的包依賴外,允許我們使用 “語義化版本規則” 指明你項目依賴包的版本,讓你的構建更好地與其他開發者分享,便於重複使用。
本文主要從最近的實踐出發,結合最新的 npm 和 node 的版本,介紹一下 package.json 中一些常見的配置以及如何寫一個規範的 package.json
package.json
package.json 常用屬性
package.json 環境相關屬性
package.json 依賴相關屬性
package.json 三方屬性
一、package.json
1. package.json 簡介
在 nodejs 項目中,package.json 是管理其依賴的配置文件,通常我們在初始化一個 nodejs 項目的時候會通過:
npm init
然後在你的目錄下會生成 3 個目錄 / 文件, node_modules, package.json 和 package.lock.json。其中 package.json 的內容爲:
{
"name": "Your project name",
"version": "1.0.0",
"description": "Your project description",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
},
"author": "Author name",
"license": "ISC",
"dependencies": {
"dependency1": "^1.4.0",
"dependency2": "^1.5.2"
}
}
上述可以看出,package.json 中包含了項目本身的元數據, 以及項目的子依賴信息 (比如 dependicies 等)。
2. package-lock.json
我們發現在 npm init 的時候,不僅生成了 package.json 文件,還生成了 package-lock.json 文件。那麼爲什麼存在 package.json 的清空下,還需要生成 package-lock.json 文件呢。
本質上 package-lock.json 文件是爲了鎖版本,在 package.json 中指定的子 npm 包比如:react: "^16.0.0",在實際安裝中,只要高於 react 的版本都滿足 package.json 的要求。這樣就使得根據同一個 package.json 文件,兩次安裝的子依賴版本不能保證一致。
而 package-lock 文件如下所示,子依賴 dependency1 就詳細的指定了其版本。起到 lock 版本的作用。
{
"name": "Your project name",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"dependency1": {
"version": "1.4.0",
"resolved":
"https://registry.npmjs.org/dependency1/-/dependency1-1.4.0.tgz",
"integrity":
"sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
"dependency2": {
"version": "1.5.2",
"resolved":
"https://registry.npmjs.org/dependency2/-/dependency2-1.5.2.tgz",
"integrity":
"sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ=="
}
}
}
二、package.json 常用屬性
本章來聊聊 package.json 中常用的配置屬性,形如 name,version 等屬性太過簡單,不一一介紹。本章主要介紹一下 script、bin 和 workspaces 屬性。
2.1 script
在 npm 中使用 script 標籤來定義腳本,每當制定 npm run 的時候,就會自動創建一個 shell 腳本,這裏需要注意的是,npm run 新建的這個 Shell,會將本地目錄的 node_modules/.bin 子目錄加入 PATH 變量。
這意味着,當前目錄的 node_modules/.bin 子目錄裏面的所有腳本,都可以直接用腳本名調用,而不必加上路徑。比如,當前項目的依賴裏面有 esbuild,只要直接寫 esbuild xxx 就可以了。
{
// ...
"scripts": {
"build": "esbuild index.js",
}
}
{
// ...
"scripts": {
"build": "./node_modules/.bin/esbuild index.js"
}
}
上面兩種寫法是等價的。
2.2 bin
bin 屬性用來將可執行文件加載到全局環境中,指定了 bin 字段的 npm 包,一旦在全局安裝,就會被加載到全局環境中,可以通過別名來執行該文件。
比如 @bytepack/cli 的 npm 包:
"bin": {
"bytepack": "./bin/index.js"
},
一旦在全局安裝了 @bytepack/cli,就可以直接通過 bytepack 來執行相應的命令,比如
bytepack -v
//顯示1.11.0
如果非全局安裝,那麼會自動連接到項目的 node_module/.bin 目錄中。與前面介紹的 script 標籤中所說的一致,可以直接用別名來使用。
2.3 workspaces
在項目過大的時候,最近越來越流行 monorepo。提到 monorepo 就繞不看 workspaces,早期我們會用 yarn workspaces,現在 npm 官方也支持了 workspaces.
workspaces 解決了本地文件系統中如何在一個頂層 root package 下管理多個子 packages 的問題,在 workspaces 聲明目錄下的 package 會軟鏈到最上層 root package 的 node_modules 中。
直接以官網的例子來說明:
{
"name": "my-project",
"workspaces": [
"packages/a"
]
}
在一個 npm 包名爲 my-project 的 npm 包中,存在 workspaces 配置的目錄。
.
+-- package.json
+-- index.js
`-- packages
+-- a
| `-- package.json
並且該最上層的名爲 my-project 的 root 包,有 packages/a 子包。此時,我們如果 npm install, 那麼在 root package 中 node_modules 中安裝的 npm 包 a,指向的是本地的 package/a.
.
+-- node_modules
| `-- packages/a -> ../packages/a
+-- package-lock.json
+-- package.json
`-- packages
+-- a
| `-- package.json
上述的
-- packages/a -> ../packages/a
指的就是從 node_modules 中 a 鏈接到本地 npm 包的軟鏈
三、package.json 環境相關屬性
常見的環境,基本上分爲瀏覽器 browser 和 node 環境兩大類,接下來我們來看看 package.json 中,跟環境相關的配置屬性。環境的定義可以簡單理解如下:
-
browser 環境:比如存在一些只有在瀏覽器中才會存在的全局變量等,比如 window,Document 等
-
node 環境: npm 包的源文件中存在只有在 node 環境中才會有的一些變量和內置包,內置函數等。
3.1 type
js 的模塊化規範包含了 commonjs、CMD、UMD、AMD 和 ES module 等,最早先在 node 中支持的僅僅是 commonjs 字段,但是從 node13.2.0 開始後,node 正式支持了 ES module 規範,在 package.json 中可以通過 type 字段來聲明 npm 包遵循的模塊化規範。
//package.json
{
name: "some package",
type: "module"||"commonjs"
}
需要注意的是:
-
不指定 type 的時候,type 的默認值是 commonjs,不過建議 npm 包都指定一下 type
-
當 type 字段指定值爲 module 則採用 ESModule 規範
-
當 type 字段指定時,目錄下的所有. js 後綴結尾的文件,都遵循 type 所指定的模塊化規範
-
除了 type 可以指定模塊化規範外,通過文件的後綴來指定文件所遵循的模塊化規範,以. mjs 結尾的文件就是使用的 ESModule 規範,以. cjs 結尾的遵循的是 commonjs 規範
3.2 main & module & browser
除了 type 外,package.json 中還有 main,module 和 browser 3 個字段來定義 npm 包的入口文件。
-
main : 定義了 npm 包的入口文件,browser 環境和 node 環境均可使用
-
module : 定義 npm 包的 ESM 規範的入口文件,browser 環境和 node - 環境均可使用
-
browser : 定義 npm 包在 browser 環境下的入口文件
我們來看一下這 3 個字段的使用場景,以及同時存在這 3 個字段時的優先級。我們假設有一個 npm 包爲 demo1,
----- dist
|-- index.browser.js
|-- index.browser.mjs
|-- index.js
|-- index.mjs
其 package.json 中同時指定了 main,module 和 browser 這 3 個字段,
"main": "dist/index.js", // main
"module": "dist/index.mjs", // module
// browser 可定義成和 main/module 字段一一對應的映射對象,也可以直接定義爲字符串
"browser": {
"./dist/index.js": "./dist/index.browser.js", // browser+cjs
"./dist/index.mjs": "./dist/index.browser.mjs" // browser+mjs
},
// "browser": "./dist/index.browser.js" // browser
默認構建和使用,比如我們在項目中引用這個 npm 包:
import demo from 'demo'
通過構建工具構建上述代碼後,模塊的加載循序爲:
browser+mjs > module > browser+cjs > main
這個加載順序是大部分構建工具默認的加載順序,比如 webapck、esbuild 等等。可以通過相應的配置修改這個加載順序,不過大部分場景,我們還是會遵循默認的加載順序。
3.3 exports
如果在 package.json 中定義了 exports 字段,那麼這個字段所定義的內容就是該 npm 包的真實和全部的導出,優先級會高於 main 和 file 等字段。
舉例來說:
{
"name": "pkg",
"exports": {
".": "./main.mjs",
"./foo": "./foo.js"
}
}
import { something } from "pkg"; // from "pkg/main.mjs"
const { something } = require("pkg/foo"); // require("pkg/foo.js")
從上述的例子來看,exports 可以定義不同 path 的導出。如果存在 exports 後,以前正常生效的 file 目錄到處會失效,比如 require('pkg/package.json'),因爲在 exports 中沒有指定,就會報錯。
exports 還有一個最大的特點,就是條件引用,比如我們可以根據不同的引用方式或者模塊化類型,來指定 npm 包引用不同的入口文件。
// package.json
{
"name":"pkg",
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
},
"type": "module"
}
上述的例子中,如果我們通過
const p = require('pkg')
引用的就是 "./main-require.cjs"。
如果通過:
import p from 'pkg'
引用的就是 "./main-module.js"
最後需要注意的是 :如果存在 exports 屬性,exports 屬性不僅優先級高於 main,同時也高於 module 和 browser 字段。
三、package.json 依賴相關屬性
package.json 中跟依賴相關的配置屬性包含了 dependencies、devDependencies、peerDependencies 和 peerDependenciesMeta 等。
dependencies 是項目的依賴,而 devDependencies 是開發所需要的模塊,所以我們可以在開發過程中需要的安裝上去,來提高我們的開發效率。這裏需要注意的時,在自己的項目中儘量的規範使用,形如 webpack、babel 等是開發依賴,而不是項目本身的依賴,不要放在 dependencies 中。
dependencies 除了 dependencies 和 devDependencies,本文重點介紹的是 peerDependencies 和 peerDependenciesMeta。
3.1 peerDependencies
peerDependencies 是 package.json 中的依賴項, 可以解決核心庫被下載多次,以及統一核心庫版本的問題。
//package/pkg
----- node_modules
|-- npm-a -> 依賴了react,react-dom
|-- npm-b -> 依賴了react,react-dom
|-- index.js
比如上述的例子中如果子 npm 包 a,b 都以來了 react 和 react-dom, 此時如果我們在子 npm 包 a,b 的 package.json 中聲明瞭 PeerDependicies 後,相應的依賴就不會重新安裝。
需要注意的有兩點:
-
對於子 npm 包 a, 在 npm7 中,如果單獨安裝子 npm a, 其 peerDependicies 中的包,會被安裝下來。但是 npm7 之前是不會的。
-
請規範和詳細的指定 PeerDependicies 的配置,筆者在看到有些 react 組件庫,不在 PeerDependicies 中指定 react 和 react-dom,或者將 react 和 react-dom 放到了 dependicies 中,這兩種不規範的指定都會存在一些問題。
-
其二,正確的指定 PeerDependicies 中 npm 包的版本,react-focus-lock@2.8.1,peerDependicies 指定的是:"react": "^16.8.0 || ^17.0.0 || ^18.0.0",但實際上,這個 react-focus-lock 並不支持 18.x 的 react
3.2 peerDependenciesMeta
看到 “Meta” 就有元數據的意思,這裏的 peerDependenciesMeta 就是詳細修飾了 peerDependicies,比如在 react-redux 這個 npm 包中的 package.json 中有這麼一段:
"peerDependencies": {
"react": "^16.8.3 || ^17 || ^18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
這裏指定了 "react-dom","react-native" 在 peerDependenciesMeta 中,且爲可選項,因此如果項目中檢測沒有安裝 "react-dom" 和 "react-native" 都不會報錯。
值得注意的是,通過 peerDependenciesMeta 我們確實是取消了限制,但是這裏經常存在非 A 即 B 的場景,比如上述例子中,我們需要的是 “react-dom” 和 "react-native" 需要安裝一個,但是實際上通過上述的聲明,我們實現不了這種提示。
四、package.json 三方屬性
package.json 中也存在很多三方屬性,比如 tsc 中使用的 types、構建工具中使用的 sideEffects,git 中使用的 husky,eslint 使用的 eslintIgnore,這些擴展的配置,針對特定的開發工具是有意義的這裏不一一舉例。
作者:yuxiaoliang
https://juejin.cn/post/7099041402771734559
關注「大前端技術之路」加星標,提升前端技能
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/cB7LzsIyaDi_FM7xXFEv3w