npm
在持续迭代,本文也会持续更新,大家可以点赞👍收藏💖关注哦~
npm
在前端开发流程中提供了非常完善的自动化工具链,已成为每个前端开发者必备的工具,但是同样由于其强大性导致很多前端开发者只会简单的使用它。本文将总结在日常开发中所需要的npm
知识点,以便开发者们更好的将npm
运用在实际开发中。
1. npm install 机制
我们都知道,npm install
用来给项目中安装依赖包,对于其是如何安装的却往往被人忽略。因为很多同学在实际开发中只关注依赖包提供的功能,而不关注依赖包所处的层级关系。依赖包安装错综复杂,这也导致很多同学在安装包出现问题的时候不知所措。所以,我们还是有必要去了解npm install
的机制,以致于我们能够更从容的面对安装问题。
假设项目App
中有如下三个依赖:
"dependencies": {
A: "1.0.0",
B: "1.0.0",
C: "1.0.0"
}
A
、B
、C
三个模块又有如下依赖:
A@1.0.0 -> D@1.0.0
B@1.0.0 -> D@2.0.0
C@1.0.0 -> D@2.0.0
嵌套安装
在npm 2.x
时代,安装依赖方式比较简单直接,以递归的方式,按照包依赖的树形结构下载填充本地目录结构,也就是说每个包都会将该包的依赖安装到当前包所在的node_modules
目录中。
执行npm install
后,项目App
的node_modules
会变成如下目录结构:
├── node_modules
│ ├── A@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
│ ├── B@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
│ └── C@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
很显然这样的依赖组织结构,有如下优点:
- 层级结构明显
- 简单的实现了多版本兼容
- 保证了对依赖包无论是安装还是删除都会有统一的行为和结构
但是缺点也一样很明显:
- 可能造成相同模块大量冗余问题
- 可能造成目录结构嵌套比较深的问题
扁平安装
从npm 3.x
开始就采用了扁平化的方式来安装模块(由于笔者都是在npm 6.14.5
的版本下进行验证,所以这一小结包括后面的论述都是基于该版本进行阐述),优先将模块安装在一级node_modules
中。当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的node_modules
下安装该模块。
还以项目App
为例,执行npm install
后,node_modules
会变成如下目录结构:
├── node_modules
│ ├── A@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
│ ├── B@1.0.0
│ ├── C@1.0.0
│ └── D@2.0.0
也可能变成如下目录结构:
├── node_modules
│ ├── A@1.0.0
│ ├── B@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
│ ├── C@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
│ └── D@1.0.0
D@2.0.0
模块和D@1.0.0
模块都有可能优先被安装在一级node_modules
目录下。那多个相同但版本不同的模块,哪个会被优先安装在一级node_modules
目录下呢?最常见的说法有两种:
根据package.json
里依赖的顺序依次安装,也就是说哪个模块优先排在package.json
的前面,将会优先被安装在一级node_modules
目录下;优先将高版本(大版本)的模块放在一级node_modules
中,低版本的包则会安装在当前模块的node_modules
中。
很遗憾,经过笔者反复的试验,以上两种说法都不成立。笔者得出的结论是:
npm install
时,首先将package.json
里的依赖按照首字母(@排最前)进行排序,然后将排序后的依赖包按照广度优先遍历的算法进行安装,最先被安装到的模块将会被优先安装在一级node_modules
目录下。
所谓广度优先遍历的安装方式,就是优先将同一层级的模块包及其依赖安装好,而不是优先将一个模块及其所有的子模块安装好。
这里提供笔者测试的一些真实模块,大家有兴趣的话可以去验证下:
大家在测试时,删除
node_modules
的同时,别忘了要把package-lock.json
文件也一并删除!
假设D@2.0.0
被优先安装在一级node_modules
目录下,再在项目中安装模块E@1.0.0
(依赖于模块D@1.0.0
),目录结构变为:
├── node_modules
│ ├── A@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
│ ├── B@1.0.0
│ ├── C@1.0.0
│ ├── D@2.0.0
│ ├── E@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
可以看到,E
模块下仍会安装模块D@1.0.0
。所以可以得出结论,在一级node_moudles
中已经存在依赖包的情况下,新安装的依赖包如果存在版本冲突,则会安装到新依赖包的node_modules
中。
再在项目中安装模块F@1.0.0
(依赖于模块D@2.0.0
),目录结构变为:
├── node_modules
│ ├── A@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
│ ├── B@1.0.0
│ ├── C@1.0.0
│ ├── D@2.0.0
│ ├── E@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
│ └── F@1.0.0
可以看到,只会安装F
模块。所以可以得出结论,在一级node_moudles
中已经存在依赖包的情况下,新安装的依赖包如果不存在版本冲突,则会忽略安装。
从以上示例可以看出,npm 6.x
并没有完美的解决npm 2.x
中的问题,甚至还会退化到npm 2.x
的行为。
为了解决目录中存在很多副本的情况,可以通过npm dedupe
指令把所有二级的依赖模块D@1.0.0
重定向到一级目录下(前提是所依赖的D@1.0.0
模块升级到了D@2.0.0
):
├── node_modules
│ ├── A@1.0.0
│ ├── D@2.0.0
│ ├── B@1.0.0
│ ├── C@1.0.0
│ ├── E@1.0.0
│ └── F@1.0.0
node_modules
路径查找机制:模块在找对应的依赖包时,nodejs
会尝试从当前模块所在目录开始,尝试在它的node_modules
文件夹里加载相应模块,如果没有找到,那么就再向上一级目录移动,直到全局安装路径中的node_modules
为止。
package-lock.json
从npm 5.x
开始,执行npm install
时会自动生成一个package-lock.json 文件。
npm
为了让开发者在安全的前提下使用最新的依赖包,在package.json
中通常做了锁定大版本的操作,这样在每次npm install
的时候都会拉取依赖包大版本下的最新的版本。这种机制最大的一个缺点就是当有依赖包有小版本更新时,可能会出现协同开发者的依赖包不一致的问题。
package-lock.json
文件精确描述了node_modules
目录下所有的包的树状依赖结构,每个包的版本号都是完全精确的。以sass-loader
在package-lock.json
中为例:
"dependencies": {
"sass-loader": {
"version": "7.1.0",
"resolved": "http://registry.npm.taobao.org/sass-loader/download/sass-loader-7.1.0.tgz",
"integrity": "sha1-Fv1ROMuLQkv4p1lSihly1yqtBp0=",
"dev": true,
"requires": {
"clone-deep": "^2.0.1",
"loader-utils": "^1.0.1",
"lodash.tail": "^4.1.1",
"neo-async": "^2.5.0",
"pify": "^3.0.0",
"semver": "^5.5.0"
},
"dependencies": {
"pify": {
"version": "3.0.0",
"resolved": "http://registry.npm.taobao.org/pify/download/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
}
}
}
}
package-lock.json
的详细描述主要由version
、resolved
、integrity
、dev
、requires
、dependencies
这几个字段构成:
version
:包唯一的版本号resolved
:安装源integrity
:表明包完整性的hash值(验证包是否已失效)dev
:如果为true,则此依赖关系仅是顶级模块的开发依赖关系或者是一个的传递依赖关系requires
:依赖包所需要的所有依赖项,对应依赖包package.json
里dependencies
中的依赖项dependencies
:依赖包node_modules
中依赖的包,与顶层的dependencies
一样的结构
在上面的package-lock.json
文件中可以发现,在requires
和dependencies
中都存在pify
依赖项。那我们顺便去node_modules
里面探下究竟:
- 打开根目录的
node_modules
会发现安装了sass-loader
所需要的所有依赖包,这些依赖包中除了pify
以外,所有依赖包的大版本号都与sass-loader
所需要的一致。 - 到根目录的
node_modules
找到pify
依赖包,发现版本为4.0.1
。 - 找到
sass-loader
项目依赖包,打开其node_modules
发现其中也存在个pify
依赖包,但版本为3.0.0
。这个版本的sass-loader
真正依赖的是这个版本的pify
。
通过以上几个步骤,说明package-lock.json
文件和node_modules
目录结构是一一对应的,即项目目录下存在package-lock.json
可以让每次安装生成的依赖目录结构保持相同。
在开发一个应用时,建议把package-lock.json
文件提交到代码版本仓库,从而让你的团队成员、运维部署人员或CI
系统可以在执行npm install
时安装的依赖版本都是一致的。
但是在开发一个库时,则不应把package-lock.json
文件发布到仓库中。实际上,npm
也默认不会把package-lock.json
文件发布出去。之所以这么做,是因为库项目一般是被其他项目依赖的,在不写死的情况下,就可以复用主项目已经加载过的包,而一旦库依赖的是精确的版本号那么可能会造成包的冗余。
2. npm 中的依赖包
依赖包分类
在 node
中其实总共有5种依赖:
-
dependencies - 业务依赖
-
devDependencies - 开发依赖
-
peerDependencies - 同伴依赖
-
bundledDependencies / bundleDependencies - 打包依赖
-
optionalDependencies - 可选依赖
作为npm
的使用者,我们常用的依赖是dependencies
和devDependencies
,剩下三种依赖则是作为包的发布者才会使用到的字段。
dependencies
这种依赖在项目最终上线或者发布npm
包时所需要,即其中的依赖项应该属于线上代码的一部分。比如框架vue
,第三方的组件库element-ui
等,这些依赖包都是必须装在这个选项里供生产环境使用。
通过命令npm install/i packageName -S/--save
把包装在此依赖项里。如果没有指定版本,直接写一个包的名字,则安装当前npm仓库中这个包的最新版本。如果要指定版本的,可以把版本号写在包名后面,比如npm i vue@3.0.1 -S
。
从
npm 5.x
开始,可以不用手动添加-S/--save
指令,直接执行npm i packageName
把依赖包添加到dependencies
中去。
devDependencies
这种依赖只在项目开发时所需要,即其中的依赖项不应该属于线上代码的一部分。比如构建工具webpack
、gulp
,预处理器babel-loader
、scss-loader
,测试工具e2e
、chai
等,这些都是辅助开发的工具包,无须在生产环境使用。
通过命令npm install/i -D/--save-dev
把包安装成开发依赖。如果想缩减安装包,可以使用命令npm i --production
忽略开发依赖,只安装基本依赖,这通常在线上机器(或者QA
环境)上使用。
千万别以为只有在
dependencies
中的模块才会被一起打包,而在devDependencies
中的不会!模块能否被打包,取决于项目里是否被引入了该模块! 在业务项目中dependencies
和devDependencies
没有什么本质区别,只是单纯的一个规范作用,在执行npm i
时两个依赖下的模块都会被下载;而在发布npm
包的时候,包中的dependencies
依赖项在安装该包的时候会被一起下载,devDependencies
依赖项则不会。
peerDependencies
这种依赖的作用是提示宿主环境去安装插件在peerDependencies
中所指定依赖的包,然后插件所依赖的包永远都是宿主环境统一安装的npm
包,最终解决插件与所依赖包不一致的问题。
这句话听起来可能有点拗口,举个例子来给大家说明下。element-ui@2.6.3
只是提供一套基于vue
的ui
组件库,但它要求宿主环境需要安装指定的vue
版本,所以你可以看到element
项目中的package.json
中具有一项配置:
"peerDependencies": {
"vue": "^2.5.16"
}
它要求宿主环境安装3.0.0 > vue@ >= 2.5.16
的版本,也就是element-ui
的运行依赖宿主环境提供的该版本范围的vue
依赖包。
在安装插件的时候,peerDependencies
在npm 2.x
和npm 3.x
中表现不一样:
在npm 2.x
中,安装包中peerDependencies
所指定的依赖会随着npm install packageName
一起被强制安装,并且peerDependencies
中指定的依赖会安装在宿主环境中,所以不需要在宿主环境的package.json
文件中指定对所安装包中peerDependencies
内容的依赖。
在npm 3.x
中,不会再要求peerDependencies
所指定的依赖包被强制安装,npm 3.x
只会在安装结束后检查本次安装是否正确,如果不正确会给用户打印警告提示,比如提示用户有的包必须安装或者有的包版本不对等。
大白话:如果你安装我,那么你最好也要按照我的要求安装A、B和C。
bundledDependencies / bundleDependencies
这种依赖跟npm pack
打包命令有关。假设package.json
中有如下配置:
{
"name": "font-end",
"version": "1.0.0",
"dependencies": {
"fe1": "^0.3.2",
...
},
"devDependencies": {
...
"fe2": "^1.0.0"
},
"bundledDependencies": [
"fe1",
"fe2"
]
}
执行打包命令npm pack
,会生成front-end-1.0.0.tgz
压缩包,并且该压缩包中包含fe1
和fe2
两个安装包,这样使用者执行npm install front-end-1.0.0.tgz
也会安装这两个依赖。
在
bundledDependencies
中指定的依赖包,必须先在dependencies
和devDependencies
声明过,否则打包会报错。
optionalDependencies
这种依赖中的依赖项即使安装失败了,也不影响整个安装的过程。需要注意的是,如果一个依赖同时出现在dependencies
和optionalDependencies
中,那么optionalDependencies
会获得更高的优先级,可能造成一些预期之外的效果,所以尽量要避免这种情况发生。
在实际项目中,如果某个包已经失效,我们通常会寻找它的替代者,或者换一个实现方案。不确定的依赖会增加代码判断和测试难度,所以这个依赖项还是尽量不要使用。
依赖包版本号
npm
采用了semver
规范作为依赖版本管理方案。
按照semver
的约定,一个npm
依赖包的版本格式一般为:主版本号.次版本号.修订号(x.y.z
),每个号的含义是:
-
主版本号(也叫大版本,
major version
)大版本的改动很可能是一次颠覆性的改动,也就意味着可能存在与低版本不兼容的
API
或者用法,(比如vue 2 -> 3
)。 -
次版本号(也叫小版本,
minor version
)小版本的改动应当兼容同一个大版本内的
API
和用法,因此应该让开发者无感。所以我们通常只说大版本号,很少会精确到小版本号。如果大版本号是 0 的话,表示软件处于开发初始阶段,一切都可能随时被改变,可能每个小版本之间也会存在不兼容性。所以在选择依赖时,尽量避开大版本号是 0 的包。
-
修订号(也叫补丁,
patch
)一般用于修复
bug
或者很细微的变更,也需要保持向前兼容。
常见的几个版本格式如下:
-
"1.2.3"
表示精确版本号。任何其他版本号都不匹配。在一些比较重要的线上项目中,建议使用这种方式锁定版本。
-
"^1.2.3"
表示兼容补丁和小版本更新的版本号。官方的定义是“能够兼容除了最左侧的非 0 版本号之外的其他变化”(Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple)。这句话很拗口,举几个例子大家就明白了:
"^1.2.3" 等价于 ">= 1.2.3 < 2.0.0"。即只要最左侧的 "1" 不变,其他都可以改变。所以 "1.2.4", "1.3.0" 都可以兼容。 "^0.2.3" 等价于 ">= 0.2.3 < 0.3.0"。因为最左侧的是 "0",那么只要第二位 "2" 不变,其他的都兼容,比如 "0.2.4" 和 "0.2.99"。 "^0.0.3" 等价于 ">= 0.0.3 < 0.0.4"。大版本号和小版本号都为 "0" ,所以也就等价于精确的 "0.0.3"。
从这几个例子可以看出,
^
是一个兼具更新和安全的写法,但是对于大版本号为 1 和 0 的版本还是会有不同的处理机制的。 -
"~1.2.3"
表示只兼容补丁更新的版本号。关于
~
的定义分为两部分:如果列出了小版本号(第二位),则只兼容补丁(第三位)的修改;如果没有列出小版本号,则兼容第二和第三位的修改。我们分两种情况理解一下这个定义:"~1.2.3" 列出了小版本号 "2",因此只兼容第三位的修改,等价于 ">= 1.2.3 < 1.3.0"。 "~1.2" 也列出了小版本号 "2",因此和上面一样兼容第三位的修改,等价于 ">= 1.2.0 < 1.3.0"。 "~1" 没有列出小版本号,可以兼容第二第三位的修改,因此等价于 ">= 1.0.0 < 2.0.0"
从这几个例子可以看出,
~
是一个比^
更加谨慎安全的写法,而且~
并不对大版本号 0 或者 1 区别对待,所以 "~0.2.3" 与 "~1.2.3" 是相同的算法。当首位是 0 并且列出了第二位的时候,两者是等价的,例如 "~0.2.3" 和 "^0.2.3"。 -
"1.x" 、"1.X"、1.*"、"1"、"*"
表示使用通配符的版本号。x、X、* 和 (空) 的含义相同,都表示可以匹配任何内容。具体来说:
"*" 、"x" 或者 (空) 表示可以匹配任何版本。 "1.x", "1.*" 和 "1" 表示匹配主版本号为 "1" 的所有版本,因此等价于 ">= 1.0.0 < 2.0.0"。 "1.2.x", "1.2.*" 和 "1.2" 表示匹配版本号以 "1.2" 开头的所有版本,因此等价于 ">= 1.2.0 < 1.3.0"。
-
"1.2.3-alpha.1"、"1.2.3-beta.1"、"1.2.3-rc.1"
带预发布关键词的版本号。先说说几个预发布关键词的定义:
alpha(α):预览版,或者叫内部测试版;一般不向外部发布,会有很多bug;一般只有测试人员使用。 beta(β):测试版,或者叫公开测试版;这个阶段的版本会一直加入新的功能;在alpha版之后推出。 rc(release candidate):最终测试版本;可能成为最终产品的候选版本,如果未出现问题则可发布成为正式版本。
以包开发者的角度来考虑这个问题:假设当前线上版本是 "1.2.3",如果我作了一些改动需要发布版本 "1.2.4",但我不想直接上线(因为使用 "~1.2.3" 或者 "^1.2.3" 的用户都会直接静默更新),这就需要使用预发布功能。因此我可能会发布 "1.2.4-alpha.1" 或者 "1.2.4-beta.1" 等等。
">1.2.4-alpha.1"表示接受 "1.2.4-alpha" 版本下所有大于 1 的预发布版本。因此 "1.2.4-alpha.7" 是符合要求的,但 "1.2.4-beta.1" 和 "1.2.5-alpha.2" 都不符合。此外如果是正式版本(不带预发布关键词),只要版本号符合要求即可,不检查预发布版本号,例如 "1.2.5", "1.3.0" 都是认可的。 "~1.2.4-alpha.1" 表示 ">=1.2.4-alpha.1 < 1.3.0"。这样 "1.2.5", "1.2.4-alpha.2" 都符合条件,而 "1.2.5-alpha.1", "1.3.0" 不符合。 "^1.2.4-alpha.1" 表示 ">=1.2.4-alpha.1 < 2.0.0"。这样 "1.2.5", "1.2.4-alpha.2", "1.3.0" 都符合条件,而 "1.2.5-alpha.1", "2.0.0" 不符合。
版本号还有更多的写法,例如范围(a - b
),大于等于号(>=
),小于等于号(<=
),或(||
)等等,因为用的不多,这里不再展开。详细的文档可以参见语义化版本(semver)。它同时也是一个 npm
包,可以用来比较两个版本号的大小,以及是否符合要求等。
依赖包版本管理
npm 2.x/3.x
已成为过去式,在npm 5.x
以上环境下(版本最好在5.6
以上,因为在5.0 ~ 5.6
中间对package-lock.json
的处理逻辑更新过几个版本,5.6
以上才开始稳定),管理项目中的依赖包版本你应该知道(以^
版本为例,其他类型版本参照即可):
- 在大版本相同的前提下,如果一个模块在
package.json
中的小版本要大于package-lock.json
中的小版本,则在执行npm install
时,会将该模块更新到大版本下的最新的版本,并将版本号更新至package-lock.json
。如果小于,则被package-lock.json
中的版本锁定。
// package-lock.json 中原版本
"clipboard": {
"version": "1.5.10",
},
"vue": {
"version": "2.6.10",
}
// package.json 中修改版本
"dependencies": {
"clipboard": "^1.5.12",
"vue": "^2.5.6"
...
}
// 执行完 npm install 后,package-lock.json 中
"clipboard": {
"version": "1.7.1", // 更新到大版本下的最新版本
},
"vue": {
"version": "2.6.10", // 版本没发生改变
}
- 如果一个模块在
package.json
和package-lock.json
中的大版本不相同,则在执行npm install
时,都将根据package.json
中大版本下的最新版本进行更新,并将版本号更新至package-lock.json
。
// package-lock.json 中原版本
"clipboard": {
"version": "2.0.4",
}
// package.json 中修改版本
"dependencies": {
"clipboard": "^1.6.1",
}
// 执行完npm install后,package-lock.json 中
//
"clipboard": {
"version": "1.7.1", // 更新到大版本下的最新版本
}
-
如果一个模块在
package.json
中有记录,而在package-lock.json
中无记录,执行npm install
后,则会在package-lock.json
生成该模块的详细记录。同理,一个模块在package.json
中无记录,而在package-lock.json
中有记录,执行npm install
后,则会在package-lock.json
删除该模块的详细记录。 -
如果要更新某个模块大版本下的最新版本(升级小版本号),请执行如下命令:
npm update packageName
- 如果要更新到指定版本号(升级大版本号),请执行如下命令:
npm install packageName@x.x.x
- 卸载某个模块,请执行如下命令:
npm uninstall packageName
- 安装模块的确切版本:
npm install packageName -D/S --save-exact # 安装的版本号将会是精准的,版本号前面不会出现^~字符
通过上述的命令来管理依赖包,package.json
和package-lock.json
中的版本号都将会随之更新。
我们在升级/卸载依赖包的时候,尽量通过命令来实现,避免手动修改
package.json
中的版本号,尤其不要手动修改package-lock.json
。
3. npm scripts 脚本
package.json
中的 scripts 字段可以用来自定义脚本命令,它的每一个属性,对应一段脚本。以vue-cli3
为例:
"scripts": {
"serve": "vue-cli-service serve",
...
}
这样就可以通过npm run serve
脚本代替vue-cli-service serve
脚本来启动项目,而无需每次敲一遍这么冗长的脚本。
npm run
是npm run-script
的缩写,一般都使用前者,但是后者可以更好地反应这个命令的本质。
工作原理
package.json 中的 bin 字段
package.json
中的字段 bin 表示的是一个可执行文件到指定文件源的映射。通过npm bin
指令显示当前项目的bin
目录的路径。例如在@vue/cli
的package.json
中:
"bin": {
"vue": "bin/vue.js"
}
如果全局安装@vue/cli
的话,@vue/cli
源文件会被安装在全局源文件安装目录(/user/local/lib/node_modules
)下,而npm
会在全局可执行bin
文件安装目录(/usr/local/bin
)下创建一个指向/usr/local/lib/node_modules/@vue/cli/bin/vue.js
文件的名为vue
的软链接,这样就可以直接在终端输入vue
来执行相关命令。如下图所示:
如果局部安装@vue/cli
的话,npm
则会在本地项目./node_modules/.bin
目录下创建一个指向./node_moudles/@vue/cli/bin/vue.js
名为vue
的软链接,这个时候需要在终端中输入./node_modules/.bin/vue
来执行。
软链接(符号链接)是一类特殊的可执行文件, 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用。在
bin
目录下执行ll
指令可以查看具体的软链接指向。在对链接文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。
PATH 环境变量
在terminal
中执行命令时,命令会在PATH
环境变量里包含的路径中去寻找相同名字的可执行文件。局部安装的包只在./node_modules/.bin
中注册了它们的可执行文件,不会被包含在PATH
环境变量中,这个时候在terminal
中输入命令将会报无法找到的错误。
那为什么通过npm run
可以执行局部安装的命令行包呢?
是因为每当执行npm run
时,会自动新建一个Shell
,这个 Shell
会将当前项目的node_modules/.bin
的绝对路径加入到环境变量PATH
中,执行结束后,再将环境变量PATH
恢复原样。
我们来验证下这个说法。首先执行 env 查看当前所有的环境变量,可以看到PATH
环境变量为:
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
再在当前项目下执行npm run env
查看脚本运行时的环境变量,可以看到PATH
环境变量为:
PATH=/usr/local/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/mac/Vue-projects/hao-cli/node_modules/.bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
可以看到运行时的PATH
环境变量多了两个路径:npm
指令路径和项目中node_modules/.bin
的绝对路径。
所以,通过npm run
可以在不添加路径前缀的情况下直接访问当前项目node_modules/.bin
目录里面的可执行文件。
PATH
环境变量,是告诉系统,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到哪些目录下去寻找。
用法指南
传入参数
关于scripts
中的参数,这里要多说几句。网上有很多不是很准确的说法,经过本人的反复试验,node
处理scripts
参数其实很简单,比如:
"scripts": {
"serve": "vue-cli-service serve",
"serve1": "vue-cli-service --serve1",
"serve2": "vue-cli-service -serve2",
"serve3": "vue-cli-service serve --mode=dev --mobile -config build/example.js"
}
除了第一个可执行的命令,以空格分割的任何字符串(除了一些shell的语法)都是参数,并且都能通过process.argv
属性访问。
process.argv
属性返回一个数组,这个数组包含了启动node
进程时的命令行参数。第一个元素为启动node
进程的可执行文件的绝对路径名process.execPath,第二个元素为当前执行的JavaScript文件路径。剩余的元素为其他命令行参数。
比如执行npm run serve3
命令,process.argv
的具体内容为:
[ '/usr/local/Cellar/node/7.7.1_1/bin/node',
'/Users/mac/Vue-projects/hao-cli/node_modules/.bin/vue-cli-service',
'serve',
'--mode=dev',
'--mobile',
'-config',
'build/example.js']
很多命令行包之所以这么写,都是依赖了 minimist 或者 yargs 等参数解析工具来对命令行参数进行解析。
以minimist
对vue-cli-service serve --mode=dev --mobile -config build/example.js
解析为例,解析后的结果为:
{ _: [ 'serve' ],
mode: 'dev',
mobile: true,
config: 'build/example.js',
'$0': '/Users/mac/Vue-projects/hao-cli/node_modules/.bin/vue-cli-service'}
在./node_modules/.bin/vue-cli-service
文件中可以看到minimist
对命令行参数的处理:
const rawArgv = process.argv.slice(2)
const args = require('minimist')(rawArgv, {
boolean: [
// build
'modern',
'report',
'report-json',
'watch',
// serve
'open',
'copy',
'https',
// inspect
'verbose'
]
})
const command = args._[0]
service.run(command, args, rawArgv).catch(err => {
error(err)
process.exit(1)
})
我们还可以通过命令行传参的形式来进行参数传递:
npm run serve --params // 参数params将转化成process.env.npm_config_params = true
npm run serve --params=123 // 参数params将转化成process.env.npm_config_params = 123
npm run serve -params // 等同于--params参数
npm run serve -- --params // 将--params参数添加到process.argv数组中
npm run serve params // 将params参数添加到process.argv数组中
npm run serve -- params // 将params参数添加到process.argv数组中
多命令运行
有的项目在启动时可能需要同时执行多个任务,多个任务的执行顺序决定了项目的表现。
串行执行
串行执行,要求前一个任务执行成功以后才能执行下一个任务,使用&&
符号来连接。
npm run script1 && npm run script2
串行命令执行过程中,只要一个命令执行失败,则整个脚本终止。
并行执行
并行执行,就是多个命令可以同时的平行执行,使用&
符号来连接。
npm run script1 & npm run script2
这两个符号是Bash
的内置功能。此外,还可以使用第三方的任务管理器模块:script-runner、npm-run-all、redrun。
env 环境变量
在执行npm run
脚本时,npm
会设置一些特殊的env
环境变量。其中package.json
中的所有字段,都会被设置为以npm_package_
开头的环境变量。比如package.json
中有如下字段内容:
{
"name": "sh",
"version": "1.1.1",
"description": "shenhao",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/xxxx/sh.git"
}
}
可以通过process.env.npm_package_name
可以获取到package.json
中name
字段的值sh
,也可以通过process.env.npm_package_repository_type
获取到嵌套属性type
的值git
。
同时,npm
相关的所有配置也会被设置为以npm_config_
开头的环境变量。
此外,还会设置一个比较特殊的环境变量npm_lifecycle_event
,表示正在运行的脚本名称。比如执行npm run serve
的时候,process.env.npm_lifecycle_event
值为serve
,通过判断这个变量,可以将一个脚本使用在不同的npm scripts
中。
这些环境变量只能在
npm run
的脚本执行环境内拿到,正常执行的node
脚本是获取不到的。所以,不能直接通过env $npm_package_name
的形式访问,但可以在scripts
中定义脚本"scripts": {"bundle": "echo $npm_package_name"}
来访问。
指令钩子
在执行npm scripts
命令(无论是自定义还是内置)时,都经历了pre
和post
两个钩子,在这两个钩子中可以定义某个命令执行前后的命令。
比如在执行npm run serve
命令时,会依次执行npm run preserve
、npm run serve
、npm run postserve
,所以可以在这两个钩子中自定义一些动作:
"scripts": {
"preserve": "xxxxx",
"serve": "vue-cli-service serve",
"postserve": "xxxxxx"
}
当然,如果没有指定preserve
、postserve
,会默默的跳过。如果想要指定钩子,必须严格按照pre
和post
前缀来添加。
上面提到过一个环境变量process.env.npm_lifecycle_event
可以配合钩子来一起使用:
const event = process.env.npm_lifecycle_event
if (event === 'preserve') {
console.log('Running the preserve task!')
} else if (_event === 'serve') {
console.log('Running the serve task!')
}
4. npm 配置
npm
的配置操作可以帮助我们预先设定好npm
对项目的行为动作,也可以让我们预先定义好一些配置项以供项目中使用。所以了解npm
的配置机制也是很有必要。
优先级
npm
可以从不同的来源获取其配置值,按优先级从高到低的顺序排序:
命令行
npm run serve --params=123
执行上述命令时,会将配置项params
的值设为123
,通过process.env.npm_config_params
可以访问其配置值。这个时候的params
配置值将覆盖所有其他来源存在的params
配置值。
env 环境变量
如果env
环境变量中存在以npm_config_
为前缀的环境变量,则会被识别为npm
的配置属性。比如在env
环境变量中设置npm_config_package_lock
变量:
export npm_config_package_lock=false // 修改的是内存中的变量,只对当前终端有效
这时候执行npm install
,npm
会从环境变量中读取到这个配置项,从而不会生成package-lock.json
文件。
查看某个环境变量:
echo $NODE_ENV
删除某个环境变量:unset NODE_ENV
npmrc 文件
通过修改 npmrc 文件可以直接修改配置。系统中存在多个npmrc
文件,这些npmrc
文件被访问的优先级从高到低的顺序为:
-
项目级的
.npmrc
文件只作用在本项目下。在其他项目中,这些配置不生效。通过创建这个
.npmrc
文件可以统一团队的npm
配置规范。 -
用户级的
.npmrc
文件mac
下的地址为~/.npmrc
。(npm config get userconfig
可以看到存放的路径) -
全局级的
npmrc
文件mac
下的地址为$PREFIX/etc/npmrc
。(npm config get globalconfig
可以看到存放的路径) -
npm
内置的npmrc
文件这是一个不可更改的内置配置文件,为了维护者以标准和一致的方式覆盖默认配置。
mac
下的地址为/path/to/npm/npmrc
。
.npmrc参照 npm/ini 格式编写。
默认配置
通过npm config ls -l
查看npm
内部的默认配置参数。如果命令行、环境变量、所有配置文件都没有配置参数,则使用默认参数值。
npm config 指令
npm
提供了几个 npm config 指令来进行用户级和全局级配置:
set
npm config set <key> <value> [-g|--global]
npm config set registry <url> # 指定下载 npm 包的来源,默认为 https://registry.npmjs.org/ ,可以指定私有源
npm config set prefix <path> # prefix 参数指定全局安装的根目录
# 配置 prefix 参数后,当再对包进行全局安装时,包会被安装到如下位置:
# Mac 系统:{prefix}/lib/node_modules
# Windows 系统:{prefix}/node_modules
# 把可执行文件链接到如下位置:
# Mac 系统:{prefix}/bin
# Windows 系统:{prefix}
使用-g|--global
标志修改或新增全局级配置,不使用的话修改或者新增用户级配置(相应级别的.npmrc
文件会更新)。
如果key
不存在,则会新增到配置中。如果省略value
,则key
会被设置成true
。
还可以覆盖package.json
中config
字段的值:
// package.json
{
"name" : "foo",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}
// server.js
console.log(process.env.npm_package_config_port) // 打印8080
执行指令:
npm config set foo:port 8000
// server.js
console.log(process.env.npm_package_config_port) // 打印8000
get
npm config get <key>
npm config get prefix # 获取 npm 的全局安装路径
按照配置优先级,获取指定配置项的值。
delete
npm config delete <key>
npm
官网上说可以删除所有配置文件中指定的配置项,但经实验无法删除项目级的.npmrc
文件里指定的配置项。
list
npm config list [-l] [--json]
加上-l
或者--json
查看所有的配置项,包括默认的配置项。不加的话,不能查看默认的配置项。
edit
npm config edit [-g|--global]
在编辑器中打开配置文件。使用-g|--global
标志编辑全局级配置和默认配置,不使用的话编辑用户级配置和默认配置。
参考 npm config 来获取更多的默认配置。
5. npm 工程管理
项目版本号管理
package.json
中的version
字段代表的是该项目的版本号。每当项目发布新版本时,需要将version
字段进行相应的更新以便后期维护。虽然可以手动的修改vsersion
字段,但是为了整个发布过程的自动化,尽量使用 npm version 指令来自动更新version
:
npm version (v)1.2.3 # 显示设置版本号为 1.2.3
npm version major # 大版本号加 1,其余版本号归 0
npm version minor # 小版本号加 1,修订号归 0
npm version patch # 修订号加 1
显示的设置版本号时,版本号必须符合
semver
规范,允许在版本号前加上个v
标识。
如果不想让此次更新正式发布,还可以创建预发布版本:
# 当前版本号为 1.2.3
npm version prepatch # 版本号变为 1.2.4-0,也就是 1.2.4 版本的第一个预发布版本
npm version preminor # 版本号变为 1.3.0-0,也就是 1.3.0 版本的第一个预发布版本
npm version premajor # 版本号变为 2.0.0-0,也就是 2.0.0 版本的第一个预发布版本
npm version prerelease # 版本号变为 2.0.0-1,也就是使预发布版本号加一
在git
环境中,执行npm version
修改完版本号以后,还会默认执行git add
->git commit
->git tag
操作:
其中commit message
默认是自动修改完的版本号,可以通过添加-m/--message
选项来自定义commit message
:
npm version xxx -m "upgrade to %s for reasons" # %s 会自动替换为新版本号
比如执行npm version minor -m "feat(version): upgrade to %s for reasons"
后:
如果git
工作区还有未提交的修改,npm version
将会执行失败,可以加上-f/--force
后缀来强制执行。
如果不想让npm version
指令影响你的git
仓库,可以在指令中使用--no-git-tag-version
参数:
npm --no-git-tag-version version xxx
如果想默认不影响你的git
仓库,可以在npm
设置中禁止:
npm config set git-tag-version false # 不自动打 tag
npm config set commit-hooks false # 不自动 commit
模块 tag 管理
不经常发布包的同学可能对模块 tag 概念不是很清楚。以vue
为例,首先执行npm dist-tag ls vue
查看vue
包的tag
:
beta: 2.6.0-beta.3
csp: 1.0.28-csp
latest: 2.6.10
上面列出的beta
、csp
、latest
就是tag
。每个tag
对应了一个版本。
那tag
到底有什么用呢?tag
类似于git
里面分支的概念,发布者可以在指定的tag
上发布版本,而使用者可以选择指定的tag
来安装包。不同的标签下的版本之间互不影响,这在发布者发布预发布版本包和使用者尝鲜预发布版本包的同时,不影响到正式版本。
在发布包的时候执行npm publish
默认会打上latest
这个tag
,实际上是执行了npm publish --tag latest
。而在安装包的时候执行npm install xxx
则会默认下载latest
这个tag
下面的最新版本,实际上是执行了npm install xxx@latest
。当然,我们也可以自定义tag
:
# 当前版本为1.0.1
npm version prerelease # 1.0.2-0
npm publish --tag beta
npm dist-tag ls xxx # # beta: 1.0.2-0
npm install xxx@beta # 下载beta版本 1.0.2-0
当prerelease
版本已经稳定了,可以将prerelease
版本设置为稳定版本:
npm dist-tag add xxx@1.0.2-0 latest
npm dist-tag ls xxx # latest: 1.0.2-0
域级包管理
细心的同学会发现,在package.json
中的依赖有两种形式:
"devDependencies": {
"@commitlint/cli": "^7.2.1",
"commitizen": "^3.0.4"
}
其中以@
开头的包名,是一个域级包(scoped package),这种域级包的作用是将一些packages
集中在一个命名空间下,一方面可以集中管理,一方面可以防止与别的包产生命名冲突。
要发布域级包,首先要在项目的package.json
的name
属性中添加scope
相关的声明,可以通过指令添加:
npm init --scope=scopeName -y
package.json
变为:
{
"name": "@scopeName/package"
}
可以将用户名作为域名,也可以将组织名作为域名。
由于用@
声明了该包,npm
会默认将此包认定为私有包,而在npm
上托管私有包是需要收费的,所以为了避免发布私有包,可以在发布时添加--accss=public
参数告知npm
这不是一个私有包:
npm publish --access=public
域级包不一定就是私有包,但是私有包一定是一个域级包。
同时,在安装域级包时需要按照域级包全名来安装:
npm install @scopeName/package
6. npm 的几个实用技巧
npx
在介绍bin
字段的时候有提到过,如果局部安装@vue/cli
的话,调用vue
指令只能在项目脚本和package.json
的scripts
字段里面,如果想在命令行下调用,需要输入:
## 项目根目录
`./node_modules/.bin/vue`
为了方便调用项目内部安装的包,我们可以使用npx
命令代替执行:
npx vue
npx
的原理很简单,就是运行的时候,会到./node_modules/.bin
路径和环境变量$PATH
里面,检查命令是否存在。
由于npx
会检查环境变量$PATH
,所以系统命令也可以调用。
# 等同于ls
npx ls
注意,Bash
内置的命令不在$PATH
里面,所以不能用。比如,cd
是Bash
命令,因此就不能用npx cd
。
除了调用项目内部包,npx
还可以从npm
仓库下载包到本地全局,执行完命令以后再删除。也就是说,可以使用npx
来使用你本地没有安装过但是存在npm
仓库上的包。
npx将按照以下规则执行下载包的命令:
- 如果
package.json
中的bin
字段中有一个条目,或者如果所有条目都是同一命令的别名,则将使用该命令。 - 如果
package.json
中的bin
字段有多个bin
条目,并且其中一个与name
字段的无作用域部分匹配,则将使用该命令。 - 如果
package.json
中没有bin
条目,或者bin
中的条目都与包的名称不匹配,则npx
执行将会退出并显示错误。
所以在本地没有安装@vue/cli
脚手架的情况下,我们还可以使用如下命令来创建你的项目:
## 自动安装,使用完后删除,再次执行则会再次安装
npx @vue/cli create vue-project
## 等同于
npm i @vue/cli -g
vue create vue-project
npm init
使用npm init
初始化一个新的项目时会提示你去填写一些项目描述信息。如果觉得填写这些信息比较麻烦的话,可以使用-y
标记表示接受package.json
中的一些默认值:
npm init -y
也可以设置初始化的默认值:
npm config set init-author-name <name> -g
npm config set init-author-email <email> -g
上面两条指令为你的npm
设置了默认的作者名和邮箱,当执行npm init -y
的时候,package.json
中的作者姓名和邮箱字段就会自动写入预设的值。
上面是我们对npm init
最熟悉的认知,其实npm init
还隐藏了一个不为人知却非常实用的功能:
npm init <initializer>
通常被用于创建一个新的或者已经存在的npm
包。
initializer
在这里是一个名为create-<initializer>
的npm
包,该包将由npx
来安装,然后执行其package.json
中bin
属性对应的脚本,会创建或更新package.json
并运行一些与初始化相关的操作。
npm init <initializer>
时转换成npx
命令的规则为:
npm init foo
->npx create-foo
npm init @usr/foo
->npx @usr/create-foo
npm init @usr
->npx @usr/create
Vite
脚手架推荐我们使用npm init
来初始化:
npm init vite@latest -> npx create-vite@latest
实际上也就是通过npx
去下载create-vite
最新的包。
查看脚本命令
查看当前项目的所有npm
脚本命令最直接的办法就是打开项目中的package.json
文件并检查scripts
字段。我们还可以使用不带任何参数的npm run
命令查看:
npm run
查看环境变量
通过env
查看当前的所有环境变量,而查看运行时的所有环境变量可以执行:
npm run env
检查环境
可以通过npm doctor
命令在我们的环境中运行多个检查。比如,检查我们当前的环境是否能够连接到npm
服务、检查node
和npm
版本、检查npm
源、检查缓存文件的权限等:
npm doctor
模块管理
检查当前项目依赖的所有模块,包括子模块以及子模块的子模块:
npm list/ls
如果还想查看模块的一些描述信息(package.json
中的description
中的内容):
npm la/ll // 相当于npm ls --long
一个项目依赖的模块往往很多,可以限制输出模块的层级来查看:
npm list/ls --depth=0 // 只列出父包依赖的模块
检查项目中依赖的某个模块的当前版本信息:
npm list/ls <packageName>
查看某个模块包的版本信息:
npm view/info <packageName> version // 模块已经发布的最新的版本信息(不包括预发布版本)
npm view/info <packageName> versions // 模块所有的历史版本信息(包括预发布版本)
npm view/info <packageName> <package.json中的key值> // 还能查看package.json中字段对应的值
查看一个模块到底是因为谁被安装进来的,如果显示为空则表明该模块为内置模块或者不存在:
npm ll <packageName>
查看某个模块的所有信息,包括它的依赖、关键字、更新日期、贡献者、仓库地址和许可证等:
npm view/info <packageName>
查看当前项目中可升级的模块:
npm outdated
整理项目中无关的模块:
npm prune
查看模块文档
打开模块的主页:
npm home <packageName>
打开模块的代码仓库:
npm repo <packageName>
打开模块的文档地址:
npm docs <packageName>
打开模块的 issues 地址:
npm bugs <packageName>
在不同的目录下运行脚本
你的文件夹中肯定存在很多应用程序,而当你想要启动某个应用程序时,肯定是通过cd
指令一步步进入到你所想要启动的应用程序目录下,然后再执行启动命令。npm
提供了--prefix
可以指定启动目录:
npm run dev --prefix /path/to/your/folder
本地包调试
- 假设你开发的是一个全局命令行包
A
,这时候你需要本地调试它,而不希望每次都npm publish
后安装调试。这个时候你可以在模块的目录下执行:
npm link
上述命令通过链接目录和可执行文件,实现任意位置的npm
模块命令的全局可执行。执行完上述命令后,就可以全局调用A
命令了。
npm link
主要做了两件事:
- 为目标
npm
模块创建软链接,将其链接到全局node
模块安装路径/usr/local/lib/node_modules/
。- 为目标
npm
模块的可执行bin
文件创建软链接,将其链接到全局node
命令安装路径/usr/local/bin/
。
- 假设你开发的是一个功能包
B
,然后你需要在项目C
中调试它,你可以直接暴力的将功能包B
的代码拷贝到需要项目C
中调试,但这不是很好的办法。我们可以执行如下操作:
# 功能包 B 中,把 B link 到全局
npm link
# 项目 C 中,link 功能包 B
npm link B
上述命令为功能包B
在全局创建一个软链接,然后将其链接到项目C
模块安装路径./node_modules/
,等同于生成了本地模块的符号链接。
我们还可以直接将功能包B
的路径作为普通的npm
包link
到项目C
中:
cd /path/C
npm link /path/B
使用这种方式,npm
也会为功能包B
在全局创建软链接。
然后,我们就可以在项目C
中加载该模块:
// 项目 C 中
const B = require('B')
所有对功能包B
的修改,都会直接反映在项目C
中。当你的项目不再需要该模块的时候,需要解除软连接,否则当你在项目中安装npm
上的包时将会出错:
# 项目 C 中
npm unlink B
# 功能包 B 中
npm unlink
安全漏洞检查
检查项目中是否存在具有安全漏洞的依赖包,如果存在,则将生成其漏洞报告显示在控制台中:
npm audit [--json] # 加上--json,以 JSON 格式生成漏洞报告
npm
升级到6.x
版本以后,在项目中更新或者下载新的依赖包以后会自动执行 npm audit 命令,对项目依赖包进行安全检查,如果存在安全漏洞,将生成漏洞报告并在控制台中显示。
修复存在安全漏洞的依赖包(自动更新到兼容的安全版本):
npm audit fix
执行npm audit fix
能修复大部分存在安全漏洞的依赖包,对于一些没能自动修复漏洞的依赖包,说明出现了SERVER WARNING
之类的警告(主要发生在依赖包更改了不兼容的api
或者大版本做了升级的情况下),这意味着推荐的修复版本还可能出现问题,这时可以执行如下命令来修复这些依赖包:
npm audit fix --force
--force
会将依赖包版本号升级到最新的大版本,而不是兼容的安全版本。大版本的升级可能会出现一些不兼容的用法,所以尽量避免使用--force
。
如果执行npm audit fix --force
后还是存在有安全漏洞的依赖包,手动执行npm audit
打印出还存在安全漏洞的依赖包的具体信息,其中More info
对应的链接中可能给出了解决方案。
如果想知道audit fix
会怎么处理项目中的依赖包,可以预先查看:
npm audit fix --dry-run --json
如果只想修复生产环境的依赖包(只更新dependencies
中的依赖包,不更新devDependencies
中的依赖包):
npm audit --only=prod
如果不想修复依赖包,只修改package-lock.json
文件:
npm audit fix --package-lock-only
如果想安装某个包时不进行安全漏洞检查:
npm install packageName --no-audit
要想安装所有包时都不进行安全漏洞检查,则可以修改npm
配置:
npm config set audit false
依赖锁定
npm
默认安装模块时,会通过脱字符^
来限定所安装模块的主版本号。可以配置npm
通过波浪符~
来限定安装模块版本号:
npm config set save-prefix="~"
当然还可以配置npm
仅安装精确版本号的模块:
npm config set save-exact true
在持续集成环境中安装包
在持续集成环境中建议使用npm ci来代替npm install
:
npm ci
它与npm install
之间的主要区别是:
- 要求项目中必须具有
package-lock.json
或npm-shrinkwrap.json
,否则执行npm ci
将会报错。 - 如果检测到
package.json
和package-lock.json
中的依赖项不匹配的话,npm ci
将退出并报错,而不是更新两个文件中的版本号。 npm ci
只根据package-lock.json
来安装包,而npm install
在安装的过程中会结合package.json
和package-lock.json
来计算依赖包版本的差异性问题。所以相比较npm install
,npm ci
既能提升包的安装速度,又能避免在生产化境中出现包版本不一致的问题。- 如果项目中已经存在
node_modules
,npm ci
将会先删除它,然后再安装。 npm ci
只能一次安装整个项目的依赖包,而不能为项目安装单个依赖包。
7. npm 包发布
准备工作
在你为你的包搭建了一个开发构建环境(这里不做详细说明)后,有两点需要注意:
- 在
package.json
文件的main
字段中配置引入包的入口文件,一般配置打包后的文件路径。
{
"main": "./lib/index.js",
}
- 在包的根目录新建
.npmignore
文件,来指定包的哪几个文件夹不需要被发布。
.idea
node_modules
src
test
.eslintignore
.eslintrc
发布流程
- 如果没有
npm
账号,先注册个账号; - 进入到项目的根目录下,执行
npm login
,然后根据提示输入用户名、密码; - 执行
npm publish
发布,发布成功后,你的npm
包就可以在npm
官网上检索到了; - 撤回发布的版本,可以执行
npm unpublish packageName@x.x.x
。
发布常见的报错
400
:版本问题,修改package.json
的version
即可;401
:npm
源设置成第三方源的时候发生的,比如我们经常会将镜像源设置成淘宝源。只要把镜像源改回默认的即可;403
:包名重复,修改包名重新发布即可。
发布一个支持 tree shaking 机制的包
tree shaking
是依赖ES Module
的模块特性来工作的,那是因为ES Module
模块的依赖关系是在编译时确定的(和运行时无关),并且之后不能再改变,所以基于此特性可以进行可靠的静态分析。
而通常,我们开发的npm
包为了更好的兼容浏览器,会借助babel
将代码进行兼容性转译,而其中用的最频繁的@babel/preset-env
预设中包含了ES2015 modules to CommonJS transform
的插件,这个插件的作用会将ES Module
语法转换成CommonJS
语法,这样就失去了ES Module
的特性,也就导致无法tree shaking
。
那我们直接把package.json
中的main
字段指向ES6
语法的文件不就好了吗?这样会带来两个问题:
babel
默认会忽略node_modules
中的文件来提高编译速度,如果想要把你的ES6
语法的包向后兼容,则需要给项目中的babel
配置复杂的屏蔽规则,从而将你的包加入到白名单中。- 如果在
nodejs
环境中使用你的包,并且恰巧nodejs
环境中不支持ES Module
规范,那么就会导致代码报错。
为了解决以上问题,我们需要在package.json
中增加两个配置项。
module
该字段指向一个既符合ES Module
模块规范但是又使用ES5
语法的源文件。这么做的目的是为了启动tree shaking
的同时,又避免代码兼容性的问题。
{
"main": "./lib/index.js", // 指向 CommonJS 模块规范的代码入口文件
"module": "./lib/index.es.js" // 指向 ES Module 模块规范的代码入口文件
}
如上配置要求你的包中要发布两种模块规范的版本。如果你的npm
环境支持module
字段,则会优先使用ES Module
模块规范的入口文件,如果不支持则会使用CommonJS
模块规范的入口文件。
sideEffects
该字段表示你的npm
包是否有副作用。具体点就是,当该字段设为false
时,表明这个包时没有副作用的,可以应用tree shaking
;如果为数组时,数组的每一项表示的是有副作用的文件,这些文件将不会应用tree shaking
。
{
"sideEffects": [
"dist/*",
"es/components/**/style/*",
"lib/components/**/style/*",
"*.less"
]
}
其实要想发布一个支持tree shaking
机制的包,最主要是要构建出一个符合module
字段要求的源文件,也就是一个既符合ES Module
模块规范但是又采用ES5
语法的源文件。
rollup
可以直接构建出符合ES Module
模块规范的文件,但是webpack
不能。所以我们只需要使用rollup
提供的构建能力,在配置文件中把output
的格式设置为es
即可:
// rollup.config.js
export default {
...,
output: {
file: 'bundle.es.js',
format: 'es'
}
}
为了更好地使用
ES Module
模块规范来开启tree shaking
功能,优先选用rollup
来开发npm
包。
8. 最后
关于npm
的知识点暂时总结到这里,可能还有很多方面没有总结到位,后续如果还有新的知识点会随时更新,也欢迎各位大佬们随时来补充,共同进步!