npm是前端开发者广泛使用的包管理工具。项目使用package.json管理项目依赖的npm包的配置。package.json是一个json文件。除了能够描述项目的包依赖之外,它还允许我们使用“语义版本规则”来指示你的项目依赖包的版本,这样你的构建可以更好地与其他开发者共享和重用。本文主要从近期的实践出发,结合最新的npm和node版本,介绍package.json中的一些常用配置以及如何编写一个标准化的package.jsonpackage.jsonpackage.json公共属性package.json环境相关属性包。json依赖相关属性package.json三方属性1.package.json1.package.json介绍在nodejs项目中,package.json是管理其依赖的配置文件。通常我们在初始化一个nodejs项目的时候,会通过:npminit然后在你的目录下会生成3个目录/文件,node_modules、package.json和package.lock.json。package.json的内容是:{"name":"你的项目名称","version":"1.0.0","description":"你的项目描述","main":"app.js","scripts":{"test":"echo\"Error:notestspecified\"&&exit1",},"author":"作者姓名","license":"ISC","dependencies":{"dependency1":"^1.4.0","dependency2":"^1.5.2"}}从上面可以看出,package.json中包含了项目本身的元数据,以及项目的子依赖信息项目(如依赖项等)。2.package-lock.json我们发现在npminit的时候,不仅生成了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详细指定了它的版本。起到锁版的作用。{"name":"你的项目名称","version":"1.0.0","lockfileVersion":1,"requires":true,"dependencies":{"dependency1":{"version":"1.4.0","已解决":"https://registry.npmjs.org/dependency1/-/dependency1-1.4.0.tgz","完整性":"sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDi},dependency==H“:{“版本”:“1.5.2”,“已解决”:“https://registry.npmjs.org/dependency2/-/dependency2-1.5.2.tgz”,“完整性”:“sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ=="}}}2.package.json的常用属性本章讨论package.json的常用配置属性。名称、版本等属性过于简单,就不一一介绍了。本章主要介绍script、bin和workspaces属性。2.1script使用npm中的script标签定义脚本。每当指定npmrun时,都会自动创建一个shell脚本。这里需要注意的是,npmrun新建的Shell会把本地目录的node_modules/.bin子目录复制到PATH变量中。也就是说,当前目录的node_modules/.bin子目录下的所有脚本都可以直接用脚本名调用,不需要加路径。比如当前项目的dependencies中有esbuild,直接写esbuildxxx即可。{//..."scripts":{"build":"esbuildindex.js",}}{//..."scripts":{"build":"./node_modules/.bin/esbuildindex.js"}}以上两种写法是等价的。2.2binbin属性用于将可执行文件加载到全局环境中。指定bin字段的npm包在全局安装后会加载到全局环境中,通过别名可以执行该文件。比如@bytepack/cli的npm包:"bin":{"bytepack":"./bin/index.js"},全局安装@bytepack/cli后,可以直接通过bytepack执行对应的命令,比如bytepack-v//显示如果没有全局安装1.11.0,会自动连接到项目的node_module/.bin目录下。和前面介绍的script标签里说的一致,直接用别名就可以了。2.3workspaces当项目太大的时候,最近越来越流行monorepo。当谈到monorepo时,我们不看工作空间。在早期,我们使用yarnworkspaces。现在npm正式支持workspaces.workspaces解决了本地文件系统中一个顶级根包下如何管理多个子包的问题。workspaces声明目录下的包会被软链接到顶层根包的node_modules。直接用官网的例子来说明:{"name":"my-project","workspaces":["packages/a"]}在一个名为my-project的npm包中,有一个workspaces配置目录。.+--package.json+--index.js`--包+--a|`--package.json和名为my-project的最顶层根包有packages/a子包。此时如果我们npminstall,那么根包中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到本地npmpackage的a链接3.package.json环境相关属性常见的环境基本分为两大类:浏览器浏览器和节点环境。接下来我们看一下package.json中与环境相关的配置属性。环境的定义可以简单理解为:浏览器环境:比如有一些只存在于浏览器的全局变量,比如window、Document等节点环境:npm包的源文件只存在node环境中的一些变量和内置包、内置函数等。3.1typejs的模块化规范包括commonjs、CMD、UMD、AMD、ES模块等,node中最早支持的只有commonjs字段,但是从node13.2.0开始,node正式支持ES模块规范,在package.json中,可以使用type字段声明npm包遵循的模块规范。//package.json{name:"somepackage",type:"module"||"commonjs"}注:不指定type时,type默认为commonjs,但建议所有npm包都指定type当type字段指定module的值时,采用ESModule规范。当指定类型字段时,目录中所有以.指定文件遵循的模块化规范,.mjs结尾的文件是使用的ESModule规范,.cjs结尾的文件遵循commonjs规范3.2main&module&browser除了type,还有maininpackage.json,module和browser3个字段定义npm包的入口文件。main:定义npm包的入口文件,浏览器环境和node环境均可使用module:定义npm包ESM规范的入口文件,浏览器环境和node环境均可使用node-environmentbrowser:定义npm包在浏览器环境下的入口下面我们来看看这3个字段的使用场景以及这3个字段同时存在时的优先级。我们假设有一个npm包demo1,-----dist|--index.browser.js|--index.browser.mjs|--index.js|--index.mjs在其包中指定。json中同时有main、module和browser三个字段,"main":"dist/index.js",//main"module":"dist/index.mjs",//module//browser可以定义为main/模块字段的一对一映射对象,也可以直接定义为字符串"browser":{"./dist/index.js":"./dist/index.browser.js",//浏览器+cjs"./dist/index.mjs":"./dist/index.browser.mjs"//浏览器+mjs},//"浏览器":"./dist/index.browser.js"//浏览器是默认构建和使用的,比如我们在项目中引用了这个npm包:importdemofrom'demo'通过构建工具构建上面的代码后,模块的加载顺序是:browser+mjs>module>browser+cjs>main这个加载顺序是大部分构建工具默认的加载顺序,比如webapck,esbuild等。这个加载顺序可以是通过相应的配置进行修改,但是在大多数场景下,我们还是会按照默认的加载顺序。3.3exports如果在package.json中定义了exports字段,那么该字段定义的内容就是npm包真正完整的导出,优先级会高于main和file字段。例如:{"name":"pkg","exports":{".":"./main.mjs","./foo":"./foo.js"}}import{something}from"包装";//来自"pkg/main.mjs"const{something}=require("pkg/foo");//require("pkg/foo.js")从上面的例子可以定义exports不同路径的导出。如果有exports,之前有效的文件目录就会到处失效,比如require('pkg/package.json'),因为在exports中没有指定,就会报错。导出的另一个最大特点是条件引用。比如我们可以指定npm包根据不同的引用方式或者模块类型引用不同的入口文件。//package.json{"name":"pkg","main":"./main-require.cjs","exports":{"import":"./main-module.js","require":"./main-require.cjs"},"type":"module"}在上面的例子中,如果我们通过constp=require('pkg')引用“./main-require.cjs”。如果你通过:从'pkg'导入p,“./main-module.js”被引用。最后需要注意的是:如果有exports属性,exports属性不仅优先级高于main,而且高于module和browser字段。3、package.json依赖相关属性package.json中依赖相关的配置属性包括dependencies、devDependencies、peerDependencies、peerDependenciesMeta。Dependencies是项目的依赖,devDependencies是开发需要的模块,所以我们在开发过程中可以安装自己需要的,来提高我们的开发效率。这里需要注意的是尽量在自己的项目中作为标准使用,比如webpack、babel等是开发依赖,不是项目本身的依赖,不应该放在dependencies中。除了dependencies和devDependencies,本文重点介绍peerDependencies和peerDependenciesMeta。3.1peerDependenciespeerDependencies是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中声明PeerDependencies,则不会重新安装对应的依赖。有两点需要注意:对于子npm包a,在npm7中,如果单独安装子npma,会安装其peerDependencies中的包。但是在npm7之前是不行的。请规范详细的说明PeerDependencies的配置。我看到一些react组件库没有在PeerDependencies中指定react和react-dom,或者把react和react-dom放在dependencies中。这两个非标准规格将存在。一些问题。二、在PeerDependencies中正确指定npm包的版本,react-focus-lock@2.8.1,peerDependenciesspecify:"react":"^16.8.0||^17.0.0||^18.0.0",但是事实上,这个react-focus-lock不支持18.x的react3.2peerDependenciesMeta。看到“Meta”就是元数据。这里的peerDependenciesMeta是对peerDependencies进行详细的修改,比如在react-redux的npm包中的package.json中有这么一段话:peerDependenciesMeta这里指定了“react-dom”和“react-native”,并且它们是可选的,所以如果项目检测到“react-dom”和“react-native”没有安装native"将不会报错。"peerDependencies":{"react":"^16.8.3||^17||^18"},"peerDependenciesMeta":{"react-dom":{"optional":true},"react-native":{"optional":true}}值得注意的是,通过peerDependenciesMeta我们确实取消了限制,但是经常有A或者B存在的场景,比如上面的例子,我们需要的是“react-dom”,需要安装“react-native”,但是实际上,通过上面的语句,这个提示我们实现不了四、package.json三方属性package.json中也有很多三方属性,比如tsc中使用的types,buildtools中使用的sideEffects,git中使用的husky,eslint中使用的eslintIgnore,这些扩展配置,对于具体的开发工具来说是有意义的,这里就不举例了。
