0.npm 前置知识
npm是node.js的包管理工具,实际开发中我们通常通过以下命令来安装/卸载npm 包:
npm install/uninstall <package_name>
0.1 dependencies依赖分类
1)dependencies 生产环境 && dev Dependencies 开发环境
dependencies中的依赖最后都会成为线上的依赖 dev Dependencies不会被生产环境自动下载
2)peer Dependencies 前置依赖
3) optioinal Dependencies 可选依赖
就是这个包即使下载失败了,也不会阻塞项目的运行,一般这种情况会增加不确定性和复杂性,项目中不建议使用
4) bundle Dependencies 待打包依赖
0.2 package.lock.json文件的作用
在我们npm install的时候,会根据package.json文件的版本范围下载最新的npm包,但是当不同的开发人员下载的时候,会导致同一个项目,不同人员之间的npm包版本不同,为了保证项目包版本的绝对相同,所以出现了lock文件。
因为package.json文件中的npm包版本都是存在一定范围的,npm包的更新是按照版本规则去注明的。按照npm包更新内容的多少可以分为:patch-修订版本、minor-小版本、major-大版本,其中小版本和大版本的主要区别在于:修改内容的多少,小版本一般内容比较少,并且可以向之前的历史版本兼容,用户更新后无需修改之前代码。但是大版本一般不考虑兼容了,更新之后需要考虑历史代码兼容问题,改动较大。
版本规范
在package.json中我们经常看见在dependencies字段中看见:"dayjs": "^1.10.6",dayjs包的版本范围在
主要的版本运算符为:~、^、-、<、<=、>、>=、=。
| 版本运算符 | 版本范围 |
|---|---|
| ~大概匹配版本 | ~3 / ~3.1 / ~3.1.2 分别表示版本范围是:3.0.0 <= v < 4.0.0 / 3.1.0 <= v < 3.2.0 / 3.1.2 < v < 3.2.0, 即~ 表示大概匹配某个版本,一般来说如果minor指定了,那么major.minor指定,patch任意(0,n); 如果只有major指定,那么minor和patch版本号任意(major.0.0, major+1.0.0)。此外如果~3.2.1,major.minor.patch都指定,那么起始值就是从path开始到minor+1。例如:~3.1 就是大版本位.小版本位.修订位中修订位没有出现,那就是3.1.x,且初始值位3.1.0,所以范围是【3.1.0, 3.1.9】 |
| ^ 兼容某个版本 | 版本号中最左边的非0数字的右侧可以任意,例如:^1.1.2,则版本范围是【1.1.2, 2.0.0) 包括1.2.11,1.2.12,..., 2.0.0 |
| x-range | 1.2.x,表示可以1.2.0,1.2.1,.....,1.2.n,其中x可以是任意值,起始点始终为0 |
注意点1:^和~的区别
1.范围区别:
~指明的范围 :他会更新到当前minor version(也就是中间的那位数字)中最新的版本,也就是只变动patch到最新版本,它不会自动更新minor版本, 波浪符号是曾经npm安装时候的默认符号,现在已经变为了插入符号。
^指明的范围,^就比较灵活了,即最左侧非0的那一位不变就可以,也就是说^1.2.0,最左侧非0的是1(major),那么只要保证major不变,版本范围是【1.2.0, 2.0.0) ,可以看出匹配的版本范围还是比较大的,^是优先匹配最新的大版本号
关键:~比较固定,就是更新patch版本,而^则比较灵活,但是基于的原则是:最左侧的第一个非0位 ,只有在这一位右侧的变动,才被包含在这个 ^ 指定的范围内
2.匹配优先级:(都是匹配最新)
^ 是优先匹配最新的大版本号,例如^1.2.7如果最新的是1.3.0(不包括2.0.0),那么会下载最新的minor版本(记住:箭头是向上,只保major版本)
~ 匹配最新的小版本号,例如~1.2.3会匹配所有1.2.x版本,但不包括1.3.0
注意点2: 如何使用~和^
一般项目使用时候,箭头^保major,保证major大版本不变,允许minor修订版本和patch改良版本的变化,~保的是major和minor,只允许修订版本的变化
tips: 有人问,我直接锁定版本不好吗,为什么要用这个范围。主要考虑两种情况:
1.你使用的npm包,作者修复补丁发布,你不知道怎么办?
2.作者将她的npm包发布新功能,更好用,你怎么半?
1.npm 安装机制
问题:当我们拉取新项目的时候,执行npm install 为什么项目的package.lock.json文件也会有更新?package-lock.json就是用来固定依赖版本的,按理说依赖的版本都固定了,又没有安装新的依赖就不应该改变的,这是为什么? 什么情况下会出现lock文件更新的情况呢?
参考文献:juejin.cn/post/699982…
1.1 npm install安装机制
图1.npm安装机制详细图
juejin.cn/post/699982…
图2.npm安装机制详细图
综合下来的npm安装机制是:- step1: 先检查npm对应的config配置
- step2: 确定版本机制判断是否存在lock文件,影响到是否需要重新构建依赖树 && 确定版本号:
判断项目中是否package.lock文件,确定npm版本号。这一步的关键在于,下载之前是否已经知道了npm包的版本号。
a.存在:那么就需要比较一下lock中的版本是否满足package.json的版本:
1)存在lock且 满足package.json版本范围 : 那么我们就知道了我们需要下载npm包的具体版本号(lock文件中的版本号),接下来就准备去下载这个指定的版本号了;
2) 存在lock 但 不满足package.json版本范围:此时以package.json文件作为最高解释问及爱你,即认为lock中的版本号过期了,需要更新最新版本号,这个时候就会重新下载最新的版本号,然后更新lock文件。
b.不存在:package.lock.json文件,那么就直接下载对应范围下最新的版本号npm,下载好之后去生成lock文件
总结:确定包版本这块涉及到:package.json文件和lock文件以及两者优先级谁更高问题。 - step3: 下载机制:确定好版本后,是去缓存还是远端获取对应版本号的包 - 准备下载npm包
(1)去缓存中获取:因为项目中下载的依赖包除了会存在项目文件下之外,本地也会在对应的npm缓存目录下做一份备份。(具体的缓存目录可以通过npm config get cache的到npm ./cache缓存目录),下次其他项目如果也需要install这个包的时候,就根据lock文件中的version,name,integrity构建唯一的key直接从缓存中解压该包,然后放入到项目中的node-module中。这样就节省了本地install时候网络的开销。
(2)去远端下载:下载规则是递归下载,先下载最外层的依赖,然后看这个依赖是否存在其他依赖,如果存在其他依赖继续往下下载。相当于从树的根节点一直下载到叶子结点依赖。
先去npm 缓存中去查看是否对应版本的包,如果不存在就去npm远端仓库中下载依赖。下载完成后,还是会将包先缓存到npm缓存中,然后再解压到项目中的node_modules(伺机更新lock.json文件) - step4: 安装机制:如何构建依赖树
为什么需要“扁平化原则”:减少重复依赖的重复下载 && 嵌套层级过深(文件系统最深不能超过255层) 扁平化带来的问题:因为扁平化+查找机制,导致幽灵依赖引用; - npm5之后的扁平化原则
注意:任何团队成员package/lock.json文件之后,其他成员应该拉取代码重新npm install更新依赖
1.2 npm缓存 (5.4以上版本)
背景:从上述的npm安装机制可以看出,可以总结为4个步骤:检查配置、确定npm包版本号、下载对应版本包、将所有版本包管理到依赖树。
其中,在第三步下载中,我们需要知道npm会先去缓存中去获取对应的版本号,如果没有才会去npm远端获取包。因此这里我们重点讲一下,第三步中的npm缓存。
我们要知道,npm并不是每次都是从远端下载的,不然会很浪费时间,因此解决的办法就是缓存,缓存的地址是在本地,具体地址可以通过: npm config get cache查看
命令:npm config get cache
获取npm缓存地址:/Users/roy/.npm 所以npm下载后包都放在这个目录下
如果要清楚缓存:npm cache clean --force
/Users/roy/.npm 目录下存在一个_cacache文件夹,里面的content-v2文件中就是npm包内容(二进制存储)
因此,在第三步骤中,npm 确定好版本号准备下载npm包的时候,会先去缓存中判断是否存在,如果存在会通过[pacote]将对应的二进制包解压到项目中的node_moduels中(如果不存在则先下载,缓存到cache中,在解压到node_moduels)
总结:
关键1:记住npm缓存的体现作用两点:是在npm下载指定包的时候体现(从缓存中拿)/ 远端下载先到缓存,由缓存统一解压
关键2: npm缓存的地址查看命令,清除缓存命令 npm cache clean --force
(link.juejin.cn/?target=htt… www.npmjs.com/package/pac…")
1.3 node_module 构建依赖树规则 - 扁平化:
背景:在npm install下载机制中,最后一步是所有的npm在下载过程中,需要怎么去存在在项目中的node_modules中,这就是本小节需要探讨的问题。
(1) 历史版本npm
历史npm中依赖树的构建规则:简单粗暴,以递归的形式嵌套树
存在问题:
- 在不同层级的依赖中,可能引用了同一个模块,导致大量冗余。
- 在 Windows 系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题。
(2) 5.4更新版本
为了解决嵌套问题,npm3之后采用扁平化原则:安装模块时,不管其是直接依赖还是子依赖的依赖,优先将其安装在 node_modules 根目录。npm5.0之后采用package.lock.json文件
npm5.4版本之后,依赖树的构建规则采用package.lock.json锁版本和扁平化原则,具体情况可以分为两种:
1.情况: 下载node_modules中不存在的新依赖包
这个时候不管这个依赖包是在第几层依赖中的所依赖的,按照扁平化原则都统一下载到node_modules中第一层中。
2.情况: 下载node_modules中第一层已经存在的包(这里指名称相同的包)
比较版本,先看下node_moduels中第一层的包能不能用,也就是说第一层包的版本号是不是符合依赖包的下载范围。如果如何就拿着用,不用再存放到node_moduels中。如果不满足,那么就把下载的目标npm包,放到下面对应的层级树中(不是放在第一层,第一层已经有了)。由此可见,第二种不满足的情况就是node_modules依赖树层级变深的主要原因。 www.cnblogs.com/yalong/p/15…
3.问题:扁平化安装是否完全解决了依赖冗余的问题: 没有,当情况2出现的时候(同一个报名,不同版本的情况下,依然存在冗余依赖包的情况:即相同名称不同版本的包中,层级在下面的包被多个依赖引用,那么多个依赖的node-module中都会重复下载安装)
如图所示:即使扁平化解决了部门冗余问题,但是依然存在特殊情况存在依赖包重复冗余
这种冗余现象称为“二级冗余”,对于这种情况,npm也做了相应的补丁操作来解决二级冗余问题,即通过npm dedupe,将在二级层级下的同名不同版本的包提到一级中,如下所示:
1.4 npm总结
npm的知识点主要分为两块:1.npm install之后的执行流程;2.这种流程存在的缺点
(1) npm install之后的执行流程:首先对于安装流程主要分为3个部分:确定版本范围、缓存or下载依赖包、生成安装依赖树(确定node-module,这期间被扁平化放在第一层的包重复的会被删除)。
(2) 这种流程的三个部门存在的缺点:目前存在的缺点主要集中在后面两部分:
一个是下载部分是按照package.json文件中的顺序串行下载,npm 安装慢:npm安装时按照package.json中的顺序串行下载安装;
另一个就是安装依赖树的扁平化构建(扁平化的目的是为了解决重复下载的问题,那么我们来看两点:
1.是否彻底解决?(部分解决,对于不在第一层的同名不同版本的包,依然重复下载,且嵌套很深)
2.是否引起了新的问题?有,幽灵依赖问题)
2.npm script 脚本 & 原理
2.1 npm run xxx发生了什么
package.json中的bin字段
我们都知道我们在终端输入npm run start是用来启动项目的,也知道实际上是运行package.json中的script脚本文件的。
例如,运行npm run dev等价于运行vue-cli-service serve,但是当我们直接运行vue-cli-service serve命令的时候为什么会识别指令错误的报错呢?那是因为我们在全局中并没有安装这个指令。例如,我们全局安装了node,因此终端可以识别node -v命令,但是如果我们没有全局安装vue-cli-service,就会报错。
总结:npm run xxx发生了什么?
step1: 通过npm运行命令的时候会在当前的 node_module/.bin中查看有没有对应的可执行文件名(vue-cli-service)
step2: 没有找到则从全局的 node_modules/.bin 中查找,
npm i -g xxx就是安装到到全局目录。step3: 如果全局目录还是没找到,那么就从 path 环境变量中查找有没有其他同名的可执行程序
2.2 原理:package.json中的bin字段
package.json中的
1.bin字段就是命令名到本地名的映射表集合。
// package.json
// bin 字段也支持对象模式配置
"bin": { "lee-cli": "./bin/www.js" }, // 这个时候这个 lee-cli 就是这个脚手架的命令
// 类似vue-cli中的vue create
2.使用yarn link链接全局
这样就可以在终端直接执行lee-cli了,然后就会去执行脚手架文件中的/bin/www .js 文件
3.配置好与bin字段地址对应的可执行文件
而对应的bin目录下的www.js文件内容为:
#! /usr/bin/env node
// 为什么要在文件头部添加这段代码,#!符号Shebang,用于指定脚本的解释程序,如果开发npm包的时候
// 需要注意的是,这个文件中的#!/user/bin/env node 是指:我要用系统中的这个目录/usr/bin/env的node环境来执行此文件,且需要注意必须放在文件开头,不能有空格,否则会语法报错。
const commander = require('commander')
const packageInfo = require('../package.json')
commander
.version(packageInfo.version)
.usage('<command> [options]')
.command('init <projectName> <templateName>', 'init an app with a template')
.command('dev', 'run live-reloaded dev server')
.command('test', 'test your code with mocha and chai')
.command('build', 'build app with webpack')
.command('lint', 'lint your code with specified rules')
.command('config', 'generate formula\'s config files')
.command('deploy', 'deploy your project to remote server')
.command('publish', 'tag your code and trigger \'release\' procedure')
.command('release', '[Deprecated]')
.command('check', 'version verify')
.command('githooks', 'link all hooks for git')
.command('auto-changelog', 'make changelog update automatically')
.command('changelog', 'update changelog manually')
.command('slim', 'list redundant xhs packages')
.parse(process.argv)
3.npm 小技巧
3.1 npm init
3.2 npm link
3.3 npx作用
4.npm 多源镜像和私服部署原理
查漏补缺文献:juejin.cn/post/684490…