package.json探究

296 阅读9分钟

package.json

为什么需要package.json?npm官网给出的解答为:

  • 列出项目中依赖的包
  • 语义化的方式指明项目中可以使用的包版本
  • 使构建可重复化,便于分享给其他开发者

创建

npm init -y

使用

npm install
yarn install
import name from 'name'

字段含义

官方解释

name

包名称,可以在npm中搜索包名并下载

{
    "name": "my-awesome-package",
    "version": "1.0.0",
    "author": "Your Name <email@example.com>"
}

version

包的当前版本,为x.x.x的semantic version

author

包的作者信息,格式为 Your Name <email@example.com> (http://example.com)

type

js的模块化规范包含了commonjs、CMD、UMD、AMD和ES module等,最早先在node中支持的仅仅是commonjs字段,但是从node13.2.0开始后,node正式支持了ES module规范,在package.json中可以通过type字段来声明npm包遵循的模块化规范

{  
   name"some package",  
   type"module"||"commonjs"   
}

需要注意的是:

  • 不指定type的时候,type的默认值是commonjs,不过建议npm包都指定一下type
  • 当type字段指定值为module则采用ESModule规范
  • 当type字段指定时,目录下的所有.js后缀结尾的文件,都遵循type所指定的模块化规范
  • 除了type可以指定模块化规范外,通过文件的后缀来指定文件所遵循的模块化规范,以.mjs结尾的文件就是使用的ESModule规范,以.cjs结尾的遵循的是commonjs规范

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等等。可以通过相应的配置修改这个加载顺序,不过大部分场景,我们还是会遵循默认的加载顺序。

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包引用不同的入口文件

{   
  "name":"pkg",  
  "main""./main-require.cjs",  
  "exports": {  
    "types": "./types/index.d.ts",
    "import""./main-module.js",  
    "require""./main-require.cjs"  
  },  
  "type""module"  
}

上述的例子中,如果我们通过

const p = require('pkg')

引用的就是"./main-require.cjs"。

如果通过:

import p from 'pkg'

引用的就是"./main-module.js"

包中的类型定义文件引用的是"./types/index.d.ts"

最后需要注意的是 :如果存在exports属性,exports属性不仅优先级高于main,同时也高于module和browser字段。

scripts

运行脚本命令的缩写配置,比如start指定了运行npm start时,所要执行的命令。 需要注意的是在script中的命令不再需要npx(命令行中直接调用项目中的安装包,需要使用npx

  "scripts": {
    "test": "vitest",
    "build": "rollup -c rollup.config.js"
  },

dependencies 和 devDependencies

dependencies字段指定了项目运行所依赖的模块,devDependencies指定项目开发所需要的模块。

它们的值都是一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。

当安装依赖的时候使用--save参数表示将该模块写入dependencies属性,--save-dev表示将该模块写入devDependencies属性

  "devDependencies": {
    "@rollup/plugin-typescript": "^11.0.0",
    "rollup": "^3.17.2",
    "typescript": "^4.9.5"
  },
  "dependencies": {
    "@rollup/pluginutils": "^5.0.2",
    "tslib": "^2.5.0",
    "vitest": "^0.28.5"
  }

依赖与开发依赖的区别

  • 安装一个外部模块时,不会自动安装这个模块下packagejson里边devDependencies的依赖,因为只需要使用模块,而不需要开发模块
  • 运行npm install时,加入--production参数不会下载devDependencies的依赖(前提是保证这里边的依赖不影响项目打包部署,也只有eslint、prettier这种才是完全不影响的了)

版本控制

版本控制遵循 major.minor.patch 语法,按照功能的迭代,按需升级版本。

Code statusStageRuleExample version
First releaseNew productStart with 1.0.01.0.0
Backward compatible bug fixesPatch releaseIncrement the third digit1.0.1
Backward compatible new featuresMinor releaseIncrement the middle digit and reset last digit to zero1.1.0
Changes that break backward compatibilityMajor releaseIncrement the first digit and reset middle and last digits to zero2.0.0

包版本升级策略

  • 指定版本 1.0.1:表示只能安装改版本的包
  • 允许升级patch版本 1.0 1.0.x ~1.0.1:表示只能更新patch,比如1.0.1, 1.0.2, ...
  • 允许升级minor版本 1 1.x ^1.0.1:表示只能更新minor和patch,比如1.0.1, 1.1.0, 1.2.0, ...
  • 安装最新版本:*

可以在npm semver calculator进行在线测试

"lodash": "1.0.1",
"lodash": "~1.0.1",
"lodash": "^1.0.1",
"lodash": "*",

peerDependencies

当我们开发项目的时候,如果项目与所依赖的包同时依赖一个第三方包,并且依赖的是两个不兼容的版本时就会出现问题。比如,你的项目依赖A包和B包的1.0版,而A模块本身又依赖B模块的2.0版。大多数情况下,这不构成问题,B模块的两个版本可以并存,同时运行。

但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。

最典型的场景就是插件,比如A包是B包的插件。用户安装的B模块是1.0版本,但是A插件只能和2.0版本的B包一起使用。这时,用户要是将1.0版本的B的实例传给A,就会出现问题。因此,需要一种机制,在模板安装的时候提醒用户,如果A和B一起安装,那么B必须是2.0包。

peerDependencies字段,就是用来供插件指定其所需要的主工具的版本。可以通过peerDependencies字段来限制,使用chai-as-promised模块必须依赖chai模块的3.9.x版本。

{ 
    "name": "chai-as-promised",
    "peerDependencies": { 
        "chai": "1.x" 
    } 
}

还有一个场景是避免包的重复安装,如下图所示,此时会存在两个版本的request包。可以将request包放在some-other-librarypeerDependencies中,统一使用项目中公共的request包。

├── request@2.12.0 
└─┬ some-other-library@1.2.3 
  └── request@1.9.9

description

一个字符串,用于编写描述信息。有助于人们在npm库中搜索的时候发现你的模块

keywords

一个字符串组成的数组,有助于人们在npm库中搜索的时候发现你的模块

homepage

项目的主页地址

license

是当前项目的协议,让用户知道他们有何权限来使用你的模块,以及使用该模块有哪些限制

engine

engines字段指明了该模块运行的平台,比如Node或者npm的某个版本或者浏览器

"engines" : { "node" : ">=0.10.3 <0.12""npm" : "~1.0.20" } }

private

表示私有模块,如果这个属性被设置为truenpm将拒绝发布它,这是为了防止一个私有模块被无意间发布出去

publishConfig

这个配置是会在模块发布时生效,用于设置发布用到的一些值的集合。如果你不想模块被默认标记为最新的,或者默认发布到公共仓库,可以在这里配置tag或仓库地址。

通常publishConfig会配合private来使用,如果你只想让模块被发布到一个特定的npm仓库,如一个内部的仓库

"private"true,  
"publishConfig": {  
  "tag""1.0.0",  
  "registry""https://registry.npmjs.org/",  
  "access""public"  
}

files

定义了哪些文件应该被包括在 npm install 后的 node_modules中,避免别人在安装包时增加资源下载量。

files属性的值是一个数组,内容是模块下文件名或者文件夹名,如果是文件夹名,则文件夹下所有的文件也会被包含进来(除非文件被另一些配置排除了)。注意,有些文件是自动暴露的,不管是否配置了files,如:package.json, readme等

可以在模块根目录下创建一个.npmignore文件,写在这个文件里边的文件即便被写在files属性里边也会被排除在外,这个文件的写法与.gitignore类似。

bin

bin项用来指定每个内部命令对应的可执行文件的位置。如果你编写的是一个node工具的时候一定会用到bin字段。

当我们编写一个cli工具的时候,需要指定工具的运行命令,比如常用的webpack模块,他的运行命令就是webpack

"bin": {  
  "webpack""bin/index.js",  
}

在模块以非全局依赖的方式被安装,如果存在bin选项。在node_modules/.bin/生成对应的文件,npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本,即scripts中直接调用命令。

所有node_modules/.bin/目录下的命令,都可以用npm run [命令]的格式运行。在命令行下,键入npm run,然后按tab键,就会显示所有可以使用的命令。

模块以全局依赖安装的时候,bin字段会被加载到全局环境中,可以在命令行直接执行相应的命令。 如:nvm ls

man

指定当前模块的man文档的位置

repository

指定一个代码存放地址,对想要为你的项目贡献代码的人有帮助

"repository" : {  
  "type" : "git",   
  "url" : "https://github.com/npm/npm.git"  
}

types

用于指定ts的类型声明定义

"types": "./types/index.d.ts",