package.json

335 阅读10分钟

scripts

npm scripts 使用指南

NPM脚本

npm 允许在package.json文件里面,使用scripts字段定义脚本命令。

{
  // ...
  "scripts": {
    "build": "node build.js"
  }
}

上面代码是package.json文件的一个片段,里面的scripts字段是一个对象。它的每一个属性,对应一段脚本。比如,build命令对应的脚本是node build.js。

命令行下使用npm run命令,就可以执行这段脚本。

$ npm run build

等同于执行

$ node build.js

NPM脚本原理

npm 脚本的原理非常简单。每当执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。

比较特别的是,npm run新建的这个 Shell,会将当前目录的node_modules/.bin子目录加入PATH变量,执行结束后,再将PATH变量恢复原样。

这意味着,当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。比如,当前项目的依赖里面有 Mocha,只要直接写mocha test就可以了。

向NPM脚本传参

向 npm 脚本传入参数,要使用--标明。

限制多个脚本的执行顺序

如果是并行执行(即同时的平行执行),可以使用&符号。

$ npm run script1.js & npm run script2.js

如果是继发执行(即只有前一个任务成功,才执行下一个任务),可以使用&&符号。

$ npm run script1.js && npm run script2.js

钩子

npm 脚本有pre和post两个钩子。举例来说,build脚本命令的钩子就是prebuild和postbuild。

"prebuild": "echo I run before the build script",
"build": "cross-env NODE_ENV=production webpack",
"postbuild": "echo I run after the build script"

用户执行npm run build的时候,会自动按照下面的顺序执行。

npm run prebuild && npm run build && npm run postbuild

因此,可以在这两个钩子里面,完成一些准备工作和清理工作。下面是一个例子。

"clean": "rimraf ./dist && mkdir dist",
"prebuild": "npm run clean",
"build": "cross-env NODE_ENV=production webpack"

自定义的脚本命令也可以加上pre和post钩子。比如,myscript这个脚本命令,也有premyscript和postmyscript钩子。

简写形式

四个常用的 npm 脚本有简写形式。

npm start是npm run start
npm stop是npm run stop的简写
npm test是npm run test的简写
npm restart是npm run stop && npm run restart && npm run start的简写

变量

npm 脚本有一个非常强大的功能,就是可以使用 npm 的内部变量。

首先,通过npm_package_前缀,npm 脚本可以拿到package.json里面的字段。比如,下面是一个package.json。

{
  "name": "foo", 
  "version": "1.2.5",
  "scripts": {
    "view": "node view.js"
  }
}

那么,变量npm_package_name返回foo,变量npm_package_version返回1.2.5。

// view.js
console.log(process.env.npm_package_name); // foo
console.log(process.env.npm_package_version); // 1.2.5

bin

NPM Docs:bin

package.json 中的 bin 字段作用

A lot of packages have one or more executable files that they'd like to install into the PATH. npm makes this pretty easy (in fact, it uses this feature to install the "npm" executable.)

通过bin字段用来指定各个内部命令对应的可执行文件的位置。

全局安装该依赖包时,bin字段注册的命令会链接到全局的bin目录,可以在终端直接通过命令名运行对应的可执行文件。(如果提示找不到该命令的话,确认下全局bin目录是否被添加到了系统变量中)

When this package is installed as a dependency in another package, the file will be linked where it will be available to that package either directly by npm exec or by name in other scripts when invoking them via npm run-script. 当该包当做依赖安装后,bin字段注册的命令可以通过两种方式执行,一种方式是通过npm-exec执行,一种方式在scripts脚本中直接写命令的名字,然后通过npm run 脚本名的方式来执行。

file

The optional files field is an array of file patterns that describes the entries to be included when your package is installed as a dependency.

File patterns follow a similar syntax to .gitignore, but reversed: including a file, directory, or glob pattern. (*, **/* and such) will make it so that file is included in the tarball when it's packed.

未配置files字段相当于配置的 ["*"], 表示所有文件都被包括。

一些文件或目录将无视files字段的配置,始终被排除或包括。

无论files字段如何配置,下面这些文件将始终被包括。

package.json README LICENSE / LICENCE The file in the "main" field

type

Node.js 如何处理 ES6 模块

Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"。

如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。

type: "module" # 默认值commonjs。一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。

如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。

总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。

注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。

CommonJS模块加载ES6模块

CommonJS 的require()命令不能加载 ES6 模块,会报错,只能使用import()这个方法加载。

(async () => {
  await import('./my-app.mjs');
})();

上面代码可以在 CommonJS 模块中运行。

ES6模块加载CommonJS模块

ES6 模块的import命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。

// 正确
import packageMain from 'commonjs-package';

// 报错
import { method } from 'commonjs-package';

这是因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是module.exports,是一个对象,无法被静态分析,所以只能整体加载。

加载单一的输出项,可以写成下面这样。

import packageMain from 'commonjs-package';
const { method } = packageMain;

一个模块同时支持两种格式的模块

一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易。

如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如export default obj,使得 CommonJS 可以用import()进行加载。

如果原始模块是 CommonJS 格式,那么可以加一个包装层。

# ES6模块写的包装层
import cjsModule from '../index.js'; # 引入commonJS模块
export const foo = cjsModule.foo; # 导出

上面代码先整体输入 CommonJS 模块,然后再根据需要输出具名接口。

你可以把这个文件的后缀名改为.mjs,或者将它放在一个子目录,再在这个子目录里面放一个单独的package.json文件,指明{ type: "module" }。

另一种做法是在package.json文件的exports字段,指明两种格式模块各自的加载入口。

"exports"{ 
    "require": "./index.js""import": "./esm/wrapper.js" 
}

上面代码指定require()和import,加载该模块会自动切换到不一样的入口文件。

main

ECMAScript 6 入门-package.json 的 main 字段

官方介绍

main字段指定了程序的入口。比如,一个名为foo的包被安装后,通过require('foo')安装时,main字段配置的导出对象会被返回。

未设置main字段时,默认为包根目录的index.js文件

如果type被执行为module

package.json文件有两个字段可以指定模块的入口文件:main和exports。比较简单的模块,可以只使用main字段,指定模块加载的入口文件。

// ./node_modules/es-module-package/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

上面代码指定项目的入口脚本为./src/index.js,它的格式为 ES6 模块。如果没有type字段,index.js就会被解释为 CommonJS 模块。

然后,import命令就可以加载这个模块。

// ./my-app.mjs

import { something } from 'es-module-package';
// 实际加载的是 ./node_modules/es-module-package/src/index.js

上面代码中,运行该脚本以后,Node.js 就会到./node_modules目录下面,寻找es-module-package模块,然后根据该模块package.json的main字段去执行入口文件。

这时,如果用 CommonJS 模块的require()命令去加载es-module-package模块会报错,因为 CommonJS 模块不能处理export命令。

总结:main字段执行的文件是入口文件,该入口应该通过何种方式导入,取决于type字段是如何定义。

module

package.json 中的 exports、main、module 字段

可能很多人对于这个字段的含义都有些误解,会认为它就是 ESM 模块系统的入口位置。

import xxx from 'xxx'

通过以上的导入方式就能命中 module 字段所指向的文件位置,进行模块的导入。这在使用了构建工具的项目中是没有问题的,可以正常执行,因为常见的构建工具都能识别它。 但如果在一个 package 中,如果 main 字段指向一个 CJS 文件,而 module 字段指向一个 ESM 文件,直接在开启了 ESModule 的 Nodejs 中通过按需加载的 import 方式导入,那么将会报错

SyntaxError: Named export 'xx' not found. The requested module 'xx' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'xx';
const { b, a } = pkg;

上述报错信息可以说明,我们引入的文件是一个 Commonjs module 文件,而 module 指向的是 ESModule 文件。说明 Nodejs 目前(v18.7.0)还不能识别 module 字段

来源:What is the "module" package.json field for?

The module field is not officially defined by Node.js and support is not planned. Instead, the Node.js community settled on package exports which they believe is more versatile.

This is used by bundler tools for ESM (ECMAScript Module) detection. The Rollup documentation says it pretty well:

If your package.json file also has a module field, ES6-aware tools like Rollup and webpack 2 will import the ES6 module version directly.

private

如果设置为 true,则可以防止应用程序/软件包被意外地发布到 npm, 如果要发布到npm, 则需要设置为false

engines

设置了此软件包/应用程序在哪个版本的 Node.js 上运行

"engines": {
  "node": ">= 6.0.0",
  "npm": ">= 3.0.0",
  "yarn": "^0.13.0"
}

peerDependencies

You might have more than one copy of React in the same app

指定npm包与主npm包的兼容性,当开发插件时是需要的,例如开发React组件时,其组件是依赖于react、react-domnpm包的,可以在peerDependencies指定需要的版本。

"peerDependencies": {
  "react": ">=16.8.0",
  "react-dom": ">=16.8.0"
}

注:如果peerDependencies指定的npm包没有下载,npm版本1和2会直接下载。 npm3不会下载,会给出警告。

resolutions 选择性依赖解决

什么时候使用resolutions

  1. 有些时候,项目会依赖一个不常更新的包,但这个包又依赖另一个需要立即升级的包。 这时候,如果这个(不常更新的)包的依赖列表里不包含需要升级的包的新版本,那就只能等待作者升级,没别的办法。

  2. 项目的子依赖(依赖的依赖)需要紧急安全更新,来不及等待直接依赖更新。

  3. 项目的直接依赖还可以正常工作但已经停止维护,这时子依赖需要更新。 同时,你清楚子依赖的更新不会影响现有系统,但是又不想通过 fork 的方式来升级直接依赖。

  4. 项目的直接依赖定义了过于宽泛的子依赖版本范围,恰巧这其中的某个版本有问题,这时你想要把子依赖限制在某些正常工作的版本范围里。

这样就可以解决的编译时的依赖报错问题了。