npm包管理工具

2018-05-27 fishedee 前端

1 概述

npm包管理工具,npm作为包依赖的管理工具,可以说是相当成熟了,而且它需要解决的问题比我原来想象的要复杂很多。

2 语义化版本号

npm允许使用特殊符号,指定所要使用的版本范围,假定当前版本是1.0.4

  • 只接受补丁包:1.0 或者 1.0.x 或者 ~1.0.4
  • 只接受小版本和补丁包:1 或者 1.x 或者 ^1.0.4
  • 接受所有更新:* or x

也就是说,版本号中的第一位是允许breaking change的大版本,第二位是包含功能变动的中版本,第三位是只包含补丁,没有功能变动的小版本。

3 脚本

Screen Shot 2018-05-29 at 8.18.08 A
npm run build

package.json中可以加入scripts,这样可以用npm run 来执行对应的脚本代码,它是用来代替C++中的Makefile,linux中的bash的工具,专门用于nodejs环境下的脚本工具。

为什么要这样做,因为像Makefile和bash的脚本中的命令都是来源于系统的PATH变量,这意味着构建前我们需要预先安装好各种构建工具才可以。但是在nodejs的哲学里面,它认为库依赖和构建工具依赖是一样的,应该由package.json指定这些构建工具的依赖,然后npm run scrips时,将当前目录node_modules/.bin也加入到PATH环境变量中,这样npm run scripts中的命令不仅包含全局的命令,还包含node_modules/.bin下的命令了。这样做,就能实现一个项目的构建相当简单,首先用npm install安装构建工具依赖,然后用npm run scripts来实现调用构建工具来构建,而且不同的项目的构建工具可以是完全不同的。避免了像java,python中的需要多个构建工具版本如何在全局共存的问题。

npx scripts 

除了用npm run scripts来执行node_modules/.bin下的命令,你可以用npx命令来执行本地命令,它会首先查找node_modules/.bin是否含有该命令,没有的话再往全局的node_modules上查找。

4 依赖安装

npm的主要工作就是根据package.json的内容,来获取当前包需要的依赖,并将这些依赖都安装到本地的node_modules位置。

4.1 dependence

这个package.json的dependence,是运行该包时所必要的依赖,当这个包被其他项目依赖时,dependence会被自动安装。

4.2 devDependence

这个package.json的devDependence,是构建开发该包时必要的依赖,注意,与dependence不同的是,devDependence被其他项目依赖时,devDependence默认不会被自动安装。

4.3 peerDependence

peerDependence可谓是npm中的亮点,详情可以看这里,它主要解决了插件包依赖宿主包时,这种依赖情况是如何表达的问题。

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

为什么插件包依赖宿主包这个问题这么棘手?因为对于一般的非插件包而言,它依赖的第三方包写在dependence中,npm是允许多个不同版本的第三方包同时存在在同一个项目中的,在上面的例子中,当前项目依赖的是request@2.12.0,而当前项目直接依赖的some-other-library依赖的是request@1.9.9,两个request是不同版本的,而且它们含有breaking change的不同版本,不能二选一。npm的解决办法是,将request@1.9.9放在some-other-library的private node_modules目录上,这样,some-other-library查找request时,优先查找到的是request@1.9.9,而当前项目查找request时,优先查找到的是request@2.12.0。

简而言之,npm允许不同版本的第三方包共存在同一个项目,解决方法就是这些第三方包在不同的node_modules目录上。但是,对于babel项目而言,它有不同的插件,例如是babel-transform-async-to-generator,bael-transform-decorators,不同的插件由于开发时间的不同,往往有不同的babel依赖项。

└─┬ babel-transform-async-to-generator@1.0.0
  └── babel@5.29.0
└─┬ bael-transform-decorators@1.0.0
  └── babel@6.24.0

按照原来的办法,不同的babel依赖放在插件包自己私有的node_moules目录上。可是这样就会出问题了,当当前目录调用babel时,它是该调用babel@5.29.0,还是调用babel@6.24.0呢?而且,无论调用谁都会有问题,因为两个插件与不同的babel依赖通信,这意味着当babel含有副作用操作时,插件只会写入到属于私有的babel上,另外一个插件对这个私有的babel的副作用变化是毫不知情的,这会造成更深层的隐藏bug。

npm ERR! peerinvalid The package react@16.0.0-alpha.6 does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer react-native@0.44.0 wants react@16.0.0-alpha.6
npm ERR! peerinvalid Peer react-redux@5.0.5 wants react@^0.14.0 || ^15.0.0-0 || ^16.0.0-0
npm ERR! peerinvalid Peer react-native-image-zoom-viewer@1.2.47 wants react@^0.14.0 || ^15.0.0
npm ERR! peerinvalid Peer react-native-camera@0.7.0 wants react@>=15.4.0
npm ERR! peerinvalid Peer redux-devtools@3.4.0 wants react@^0.14.9 || ^15.3.0
npm ERR! peerinvalid Peer react-dom@15.4.2 wants react@^15.4.2
npm ERR! peerinvalid Peer react-test-renderer@15.4.2 wants react@^15.4.2

这个根本问题是,多个插件包依赖的宿主包时,宿主包是不能像放在自己私有的node_modules上,多个插件包必须要共享同一个宿主包,这样才会避免这样的bug。当多个插件包依赖的宿主包,如果依赖的版本有冲突,就必须要报错!在npm上,插件包依赖宿主包时,它既不是写在dependence,也不是写在devDependence,而是写在peerDependence。

并且,和devDependence一样,peerDependence在被依赖时,默认不会安装的。

4.4 算法

npm install第三方包时的算法

  • 按照深度优先算法遍历每个依赖包
  • 依赖包的依赖文件优先放在顶层的node_modules,实在有冲突才放到自己私有的node_modules上。
  • 处理好后生成clone_tree。
  • 比较original_tree和clone_tree生成install的install,update,remove和move子命令
  • 根据子命令逐个执行依赖包下载安装

4.5 最佳实践

对于开发project而言,npm的package.json就十分宽松,毕竟没有人会包含你这个project。但对于开发library而言,npm的package.json是十分讲究,写不正确会有奇怪的bug。

  • babel,webpack,jest这些构建工具放在devDependence。
  • 插件方式的library包,依赖的宿主放在devDepedence和peerDepedence,依赖的library放在dependence。
  • 非插件方式的library包,依赖的library放在dependence。

安装

  • 先安装宿主,再安装插件,避免反过来。

对于顶部project,需要在npm install后将package-lock.json一起提交到版本管理系统,以避免部署环境时造成不必要的bug。

4 入口

  • bin,可执行库的路径
  • main,library的es5入口
  • module,library的es6入口,es6入口的好处是,可以统一做babel转换,并且在webpack和rollup打包工具时做tree shaking

5 镜像

npm config set registry https://registry.npm.taobao.org

设置淘宝镜像,速度更快,但是你要发布npm包时,需要更改为官方的npm地址。

6 代理

npm config set proxy=http://127.0.0.1:8087

设置代理的方法

7 总结

find . -maxdepth 3 -name "node_modules" | xargs rimraf

批量删除node_modules的方法

包依赖管理是一个复杂的问题,我想很难有比npm做得更加成熟的了。

相关文章