你可能不知道的Node.js和NPM

前言的前言

这篇文章来自内部分享,目的是让大家会用并且用好Node.js和NPM,毕竟会用和用好还有很大的距离。

另外就是让大家从每天都打交道的工具中听出来新东西。

前言

今天跟大家分享一些实用但很少有资料一次性说清楚的知识,不限于Node.js和NPM,包含一些常识性的知识,会对大家工作学习有比较大的帮助。

希望达到的目标是大家能把Node.js和NPM用好。最不济的情况是遇到问题的时候能知道往哪个方向查。

先做一个小调查。

大家在用哪个版本的Node.js?

有没有4.x的?

有没有5.x的?

有没有7.x的?

有没有9.x的?

有没有11.x的?

有没有12.x的?

我刚来那会儿(2018年5月)看大家有用Node.js6的,有用Node.js7的,有用Node.js8的,有用Node.js9的,还有用Node.js10的,再后来也看见了有用Node.js11的。

现在咱们在okt里做了Node.js版本的限制,环境统一也能帮助大家提升效率,尤其是多个人合作的时候。

基础常识

  1. 高版本一定比低版本好吗?

答:显然不是,为什么?稳定的才是最好的,三天两头环境出问题谁受得了?没法开开信心的干活了。

  1. 什么是LTS版本?

答:英文全称Long Term Support,即长期支持版,从名字就可以看出支持的时间会更长一些,通常几年。而非长期支持版维护时间可能几天到几个月,甚至不维护。不会修bug,没有安全支持。所以生产环境一定要选择长期支持板。当然不是所有软件都有LTS的概念。

  1. LTS与Alpha、Beta、RC什么关系?

答:没有直接关系,他们两个不是一个维度的概念。

Node.js 与 NPM 什么关系?

NPM是Node.js的包管理器,类似于Java中的Maven,PHP中的Composer。一般会跟随Node.js一同安装。

NPM是否是唯一的包管理器呢?答案是否定的,起码还有yarn吧。

Node.js

下面开始Node.js部分。

如何选择版本

下面的几张图是从 Node.js Releases List 截取的,咱们应该选择哪个版本?



当然是选择最新的LTS版,也就是10.14.1。

有没有发现Node.js9没有发布LTS版本?是不是他忘记了?我们发个邮件提醒一下好不好?

其实这就是Node.js的发版规则,只有偶数版本才会发布LTS版,而且他还有个名字。

LTS的名字是怎么来的?

这就得介绍一位大牛,对,门捷列夫,名字是门捷列夫取的。其实是用了化学元素来命名,门捷列夫那个年代哪里有Node.js。没有命名的版本不是LTS版。

io.js是怎么回事儿?

刚才的那个io.js是个什么东西?为啥会在Node.js的列表中?

历史上Node.js闹过分家,后来在大家的妥协下又合并到了一起。

4.0.0版本的Node.js已经不是原来的Node.js了,而是io.js改名的产物。

LTS的一生

首先想一个问题,长期到底是多长?

是不是我选择了长期支持版就意味着我可以一直使用下去了?当然不是,长期不等于无限期。

我们来看Node.js的发版计划,Release Schedule。第一张精度高,第二张图形象,大家喜欢看哪张?

其实每年四月都会有一个偶数的版本被发布,六个月后,也就是当年的十月发布LTS,这时我们就可以使用了。

LTS的前18个月是积极维护期,所有bug都会修复,所有的安全问题都会修复,而且还可能会有一些性能改进加进来。当然这些都是向后兼容的,大家可以放心大胆的升级。

18个月以后进入维护期,或者叫维持期,时长12个月,这个阶段只修复关键bug和关键安全问题,也就是说小问题就不修复了,这时候就得开始考虑升级了。

这样算下来一个偶数版本历时三年,其中两年半是LTS。

每年都会发布一个LTS版本,那也就是说同一时刻会有两到三个LTS版本,这是为了给大家留出了升级的时间。如果时间没有重叠大家是不是就没有时间升级了。

另外,大家有没有发现Node.js8比其他版本维护的时间短?这个是有实际困难在的,Node.js8所使用的OpenSSL到明年十二月就不维护了,Node.js8也就不维护了,比正常少了四个月。所以现在最建议大家使用的是Node.js10的长期支持版,刚两个月,还可以用接近两年半。其次是Node.js8,还有四个多月的积极维护期,Node.js6已经处在维持期,新安装刘不要使用了,如果想体验新功能用Node.js11,实际工作记得切换回去。

如下是寿终正寝的版本,大家就不要在把他们挖出来了。

如何快速切换Node.js版本?

如果想灵活的切换Node.js版本我们有nvm和n可供选择。nvm是用shell编写的,不依赖Node.js环境,n本身是一个Node.js模块,用n得先有Node.js,大家自己灵活选择。

NPM

下面正式开始NPM部分。

npm init

npm init用于初始化一个package,默认以交互式运行,生成package.json和一些简单的字段。

这个很简单,多说一句,npm init是可以自定义初始化模板的,咱们用不到,因为咱们有okt。

依赖包安装

依赖管理是npm的核心功能,执行npm install时会将dependencies 和 devDependencies中列出的模块安装到./node_modules中。

执行npm install <package name>会将指定依赖包安装到./node_modules

执行npm install -g <package name会将依赖包安装到全局。

一、 dependencies 与 devDependencies 什么区别?

答:如果你开发了一个package,别人通过npm install <your package name>安装,那么npm会自动帮你安装dependencies中列出的依赖,而不会帮助你安装devDependencies中列举的模块。

二、那么问题来了,你写了一个React组件,你应该把React写到dependencies还是 devDependencies

答:都不是

三、什么是peerDependencies

答:一般在插件或者指定框架的组件开发时,不应该直接依赖插件的宿主,而是应该声明自己所依赖的环境。peerDependencies就是干这件事儿的,所以上一个问题有答案了。

四、 什么是module?

答:在node_modules中可以被require的目录或者文件。

这样说是不是有点抽象,我们分开来说,满足下面条件之一的就是一个合法的module

  • 包含package.json的目录,并且package.json中包含main字段。
  • 包含index.js的目录。
  • JavaScript文件。

五、 什么是package?

答:包含package.json的目录(文件夹)或者文件。

是不是还是有点抽象,我们也分开说,满足下面条件之一的就是一个合法的pacakge。

  • a) 包含package.json文件作为程序描述的目录。
  • b) 把(a)gzip后得到的压缩文件。
  • c) 能访问到(b)的URL。
  • d) 发布到registry上的<name>@<version>格式。
  • e) 指向(d)的<name>@<tag>格式,tag对应一个版本号。
  • f) 格式,是(e)的<name>@latest简写。
  • g) 一个clone后满足(a)的git url。

六、 可以如何共享package?

知道什么是package了我们就来研究下我们可以如何共享package。

  1. 通过本地目录共享

我们来看实的例子。

~/tmp/test_module目录下创建package.json文件,内容如下

{
"name": "test_module",
"version": "1.0.0",
"description": "",
"main": "index.js"
}

在该目录下创建index.js,在文件内编写相应代码。

在测试项目~/workspace/test下执行

$ npm i --save file:~/tmp/test_module

查看~/workspace/test/package.json,如下

{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"test_module": "file:../../tmp/test_module"
}
}

可以发现新增的dependencies,指向的起本地的一个目录。

查看node_modules目录可以发现新增了一个test_module的module,仔细看你会发现这个一个连接。

  1. 通过本地文件共享

~/tmp/test_module打包成gzip包。

$ tar -zcvf test_module.tar.gz test_module

在测试项目~/workspace/test1下执行

$ npm i --save file:~/tmp/test_module.tar.gz

查看package.json,如下

{
"name": "test1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"test_module": "file:../../tmp/test_module.tar.gz"
}
}

查看node_modules会发现test_module的存在,但是这种情况test_module不再是连接,也就是说npm做了解压,使用的是解压后的结果。

  1. 通过http server分享

这种方式就是上一种方式的另一种形式,只不过由file协议编程了http协议,没什么可特殊说明的。

  1. 通过公共npm源分享

这个是大家最常见的方式,执行npm i <package name>时默认就是从公共npm源获取的,也就是npmjs.com。

  1. 通过私有npm源分享

很多公司为了方便分享私有npm package或者提高安装速度会搭建私有npm源。

  1. 通过git仓库分享
<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]

<protocol>git、git+ssh、git+http、git+https或者git+file之一。

如果提供#<commit-ish>npm将clone指定的提交。如果 commit-ish 是#semver:<semver>格式(<semver>可以是任何合法semver版本号),npm将会去匹配版本号对应的tags或者refs。如果没有提供#<commit-ish>或者 #semver:<semver>npm将会使用master分支。

举例

git+ssh://git@github.com:npm/cli.git#v1.0.27
git+ssh://git@github.com:npm/cli#semver:^5.0
git+https://isaacs@github.com/npm/cli.git
git://github.com/npm/cli.git#v1.0.27

npm i -g 全局安装做了一件什么事儿?

npm i将package安装到当前目录下的node_modules中,而npm i -g 则将包安装到 /usr/local/lib/node_modules下(mac),全局安装的package一般都会提供一个或者几个命令,比如我比较喜欢的serve包。原理是什么在下面讲。

package.json中的bin

如果你的package想提供可执行的命令那么你需要在package.json中指定一个bin字段,类似如下

{
"bin": {
"mycli": "./cli.js"
}
}

其中mycli就是你要提供的命令,而当前路径下的cli.js是真正执行的文件。

对于全局安装npm会在安装时在/usr/local/bin下创建一个名字为mycli的连接链接到/usr/local/lib/node_modules/<your package>/cli.js,这样你在全局安装后命令直接就是可用的。

对于非全局安装,npm不会把bin链接到/usr/local/bin,而是会将命令放到node_modules/.bin下并且连接到./node_modules/<your package>/cli.js。此时我们不能直接使用命令,但是我们可以通过./node_modules/.bin/mycli的方式使用,这样很麻烦。后来从npm5.2开始npm加入了npx,直接npx mycli完成命令执行。

npm scripts

刚刚介绍了bin,知道本地安装会有./node_modules/.bin目录,而npm scripts在命令查找是会优先查找,./node_modules/.bin目录,这也是为啥我们没有全局安装,我们在命令好直接运行会提示命令找不到,但是在npm scripts中却可以运行的原因。

npm install是如何工作的?

这里主要写的是npm2、npm3、npm5、npm6。

为啥没有npm1?那是一个很古老的故事了,从npm2说起就很能说明问题了。

为啥没有npm4?npm4只在Node.js7中使用过,而通过前面的知识我们已经知道7不会长期支持,没关注,确实了解的也不多。

一、npm2

npm2 会采用简单粗暴的递归方式安装,递归dependenciesdevDependenciespeerDependencies

假设packageA和packageB都依赖了packageC的0.1.0版本,安装的结果是node_modules下包含packageA和packageB,而packageA和packageB的node_modules下都各自有一个packageC,属于同一个版本的两个拷贝。

这样设计缺点很明显,可能有很多包被重复安装了很多次,那个时候node_modules很容易特别大,而且层级特别深的情况下windows会遇到路径超长的问题,windows目录长度大约是256的长度。

二、npm3

为了解决npm2的弊端,结合node的module查找机制,npm3采用了将依赖包打平的做法。还是上面的例子,这时的node_modules下存在是是 packageA、packageB、packageC。

另一个改变是不再安装peerDependencies中的依赖,而是会给一个警告。

打平也带来了一个弊端,就是我们没法直观的在node_modules下看到我们的依赖了,不怕,我们有npm ls

另外一个问题是如果packageA依赖packageC的1.0.0版本,而packageB依赖packageC的2.0.0版本,这两个版本不兼容,这时查看node_modules会发现目录下有packageA、packageB、packageC的1.0.0版本,而packageC的2.0.0版本在packageB的node_modules下。这也可以看出,npm3追求的不是觉得对打平,而是尽可能的打平。

三、npm5

npm5跟随Node.js8发布,引入了package-lock.json,作用是锁定模块的版本,而且是递归锁定的。为什么叫锁定,因为它不是简单的版本一致,还包括你从哪个npm源下载的,这个包对应的hash是多少,再次安装时会做完全匹配。

这样就能保证同一个项目每个人安装的版本是一致的。之前的自动升级机制确实很有一些问题。问题是有一些人不按照semver来升级版本,即便是按照这个规则也会有一些不可避免的失误,这就可能导致生产环境和本地环境有差异,还有就是每个人自己的开发环境有差异。

npm5之前的版本也具备锁定版本的能力,有一个shrinkwrap命令。

既然锁定了版本那么如何升级呢? 推荐的方式是npm i <package name>@<version>,这样package.json和package-lock.json都会升级,把升级之后的文件提交到代码库即可。

npm5还引入了一个功能就是npm i自动将依赖包加入到dependencies中。

四、npm6

npm6没有什么对npm install的改进,主要是在安全方面的改进,这里就不提他了。

npm link是另外一个实用功能,如果你开发一个模块(假设为test_module),还不到发布的时候,需要在项目或者另一个模块中使用怎么办?

可以按照如下步骤操作,

  1. 在test_module下执行 npm link
  2. 在使用的项目目录下执行 npm link test_module

这样test_module就以连接的形式链接到了项目目录下的node_modules中。结合前面package的知识,这个连接是一个合法的module。因为是连接,在package中编辑node_modules实时生效。Web IM就用了这样的方案。如果你的包开发到一半为了调试发布了,别人安装了怎么办?那样会引起很多不可控制的问题。

npm配置

我们可以通过 npm config set 修改npm配置,可以通过npm config delete 删除配置,可以通过npm config get 查看配置。这些都是命令形式的。

其实npm是有配置文件,我们可以在我们自己的项目下创建.npmrc文件来给项目定制配置,比如指向内网的npm源。当前用户的跟目录也会有自己.npmrc文件,项目中的优先级更高。 之前我在群里发了一个配置文件,大家可以把他放到自己的项目里。

为什么你在npm install的时候要加sudo?

原因你是把你的文件系统权限搞坏了,好的习惯是不要乱加sudo。如果你已经搞坏了,可以通过chown来修复这个问题,就是把node_modules转交给你自己。

升级Node.js和npm后会遇到啥问题?

可能会发现包不兼容等奇怪问题

$ npm cache clean --force
$ rm -rf node_modules
$ npm i

总结

时间关系,这里不能一一列举,关于更多npm的具体使用大家可以去翻看npm文档。另外我也希望我能抽时间多帮大家看一下各个项目,发现一些问题并改进,帮助大家不断提高效率。

Q & A

接下来是答疑环节,希望以上的内容大家都能理解并能灵活使用,看看有什么问题。

参考链接

  1. npm-package.json

推荐文章