主要内容
- 📝 什么是依赖
- 📤 依赖从哪来
- 🤹 安装到哪儿
- 🎥 版本控制
- 🧑💻 哪些需要装,哪些不需要
- 🎨 package-lock.json
- 🛠 npm install 过程回顾
什么是依赖
有时候,依赖是一堆 可执行的代码 ;
有时候,依赖只是 一句声明。
1.当我们的业务逻辑中使用到 Vue
时,我们只需要依赖(引入)它,我们就可以使用它的能力
import vue from 'vue'
or
此时,依赖就是获取一堆可用的代码
2.当我们在开发组件库时,我们需要使用到 Vue
中的一些方法
import { ref } from 'vue'
在这种场景下,组件所使用的 vue.js
,实际上是宿主环境所依赖的 Vue
。
此时,依赖仅仅是一个声明
依赖从哪来
获取依赖的来源有哪些
- 静态文件打包
- CDN 引入
- 仓库级引用
- 从 npm 源安装
静态文件打包
前端资源通过相对路径进行引入,整体打包到项目中。
CDN 引入
优点:
- 多域复用
- 就近传输
- 通过跨域达到 突破浏览器并发限制 的效果
- ……
缺点:
引入公共 CDN
资源,需要评估风险,避免 CDN
域名污染后,资源异常
仓库级引用
git submodule
文章参考:《Git 中 submodule 的使用》
从 npm 源安装
1.公网 npm registry
npm install vue
2.私有 npm registry
npm install @xxfe/babel-plugin-icover --registry=http://ires.xxcorp.com/repository/xxnpm/
.npmrc
指定仓库源地址
registry=http://ires.xxcorp.com/repository/xxnpm/
or
// 特定命名空间下
@xxfe:registry=http://ires.xxcorp.com/repository/xxnpm/
3.指定 git
仓库
{
"name": "foo",
"version": "0.0.0",
"dependencies": {
"express": "git+ssh://git@github.com:npm/cli.git#v1.0.27"
}
}
通过指定 协议
、仓库地址
以及 tag/commit-hash
等信息,可以精准指定到某个版本的代码
文档参考:《npm docs》
4.post-install 玩法
从命名上能够看出,post-install
的意思是指 install
之后之后会主动触发的一个钩子。
通过在这个钩子内执行脚本,你可以去下载任何你想要的内容,包括但不限于:.exe
、.avi
、.pdf
等等…
npm install
执行的过程会经历三个勾子方法:
- preinstall
- install
- postinstall
npm install
命令发起后,根据工程定义决定是否执行 preinstall
,install
、postinstall
是 npm install
命令必然会执行的阶段
文档参考:
- 《npm docs》
- 《滥用 npm 库导致数据暗渡》
安装到哪儿
node_modules ? 当然是对的!但是并不准确。
依赖地狱
说到 node_modules,总是离不开要看看它的依赖地狱图
我们分别以 React
和 Vue
为例,单独安装和通过 cli
工具进行安装,node_modules
的安装结果:
- 单独安装
React
和ReactDOM
,占用5.2M
空间; - 单独安装
Vue(3.x)
,占用16.3M
空间; - 使用
create-react-app
创建一个空白React
项目,占用344.8M
空间; - 使用
vue-cli
创建一个空白Vue
项目,占用163.9M
空间。
node_modules 层级
npm2.x 版本 node_modules 层级 – 递归式
先定义一种语法
A{B,C}
代表A
包依赖了B
包和C
包
A{D@1.0.0}, B{D@2.0.0}, C{D@1.0.0}
├── 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@1.0.0
可以想象,这样做的确能尽量保证每个模块自身的可用性。但是,当项目规模达到一定程度时,也会造成许多问题:
- 依赖树的层级非常深。如果需要定位某依赖的依赖,很难找到该依赖的文件所在(例如,如果想定位模块
D
,就不得不先知道他在依赖树中的位置); - 不同的依赖树分支里,可能有大量实际上是同样版本的依赖(例如,
D@1.0.0
在A
和C
下版本是一致的); - 安装时额外下载或拷贝了大量重复的资源,并且实际上也占用了大量的硬盘空间资源等(例如,
D
模块在依赖目录中出现了三次); - 安装速度慢,甚至因为目录层级太深导致文件路径太长的缘故,在
windows
系统下删除node_modules
文件夹也可能失败!
这可谓:子又生孙,孙又生子,子子孙孙无穷尽也……
npm3.x 版本 node_modules 层级 – 扁平式
在 npm3.x
版本后,npm
采用了更合理的方式去解决依赖地狱问题。npm3.x
尝试把依赖以及依赖的依赖都尽量的平铺在项目根目录下的 node_modules
文件夹下以共享使用;如果遇到因为需要的版本要求不一致导致冲突,没办法放在平铺目录下的,回退到 npm2.x
的处理方式,在该模块下的 node_modules
里存放冲突的模块。
A{D@1.0.0}, B{D@1.0.0}, C{D@2.0.0}
├── node_modules
│ ├── A@1.0.0
│ ├── B@1.0.0
│ │── C@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
│ ├── D@1.0.0
幽灵依赖问题
A{D@2.0.0},B
├── node_modules
│ ├── A@1.0.0
│ ├── B@1.0.0
│ ├── D@2.0.0
const A = require('A');
const D = require('D'); ???
不确定性问题
A@1.0.0{C@1.0.0},B@1.0.0{C@2.0.0}
node_modules
├── A@1.0.0
├── B@1.0.0
│ └── node_modules
│ └── C@2.0.0
├── C@1.0.0
node_modules
├── A@1.0.0
│ └── node_modules
│ └── C@1.0.0
├── B@1.0.0
├── C@2.0.0
依赖分身/多重依赖问题
A{B@1.0.0}, C{B@2.0.0}, D{B@1.0.0}, E{B@2.0.0}
node_modules
├── A@1.0.0
├── B@1.0.0
├── D@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── E@1.0.0
└── node_modules
└── B@2.0.0
版本控制
- ^1.1.0 和 ~1.1.0 的区别是什么?
- 1.01.02 是否合法?
- 1.0.1、1.0.1-alpha.2 、1.0.1-rc.2 这三个版本号由大到小的顺序是什么?
- vue@latest 应该命中哪个版本?由谁决定?那么 vue@v2-beta 呢?
在软件管理领域里存在着被称作“依赖地狱”的死亡之谷。
系统规模越大,加入的包越多,你就越有可能在未来的某一天发现自己已经深陷绝望之中。
在依赖高的系统中发布新版本包可能很快会成为噩梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)。如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理的数量)。
当你项目的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。
这就是为什么需要使用语义化版本的原因
SemVer
语义化的版本控制(Semantic Versioning),简称语义化版本,英文缩写为 SemVer。
语义化版本通过一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开发源码软件所广泛使用的惯例所设计。
格式
语义化版本格式:主版本号.次版本号.修订号(MAJOR.MINOR.PATCH)。
版本号递增规则如下:
- 主版本号(MAJOR):通常只有在重构、API 不向下兼容时才会进行升级;
- 次版本号(MINOR):通常在增加向下兼容新特性时升级此版本号;
- 修订号(PATCH):通常在发布向下兼容的问题修复时更新
先行版本号及版本编译信息可以加到 “主版本号.次版本号.修订号” 的后面,作为延伸。
先行版本号
Snapshot:快照,也被称为开发版,处于开发阶段。这个版本的代码禁止用于生产环境。
Alpha (α):内测版,内部交流或专业测试人员测试使用。
Preview:预览版,与 Alpha 类似,有时还会细分 M1,M2 版本。
Beta (β):公测版,专业爱好者大规模测试使用,存在一些 Bug,不适合一般用户使用。
Gamma (λ):比较成熟的测试版。
RC (Release Candidate):候选版本,处于 Gamma 阶段,该版本已经完成了全部功能并清除了大量的 Bug。
到了这个阶段,只会修复 Bug,不会对软件做任何大的更改。
一般来说,Alpha -> Beta -> Gamma 是迭代的关系,RC1 -> RC2 是取舍的关系。
Release:发行版本,正式发行的版本,已经经过测试,一般不会出现严重的 Bug,适合一般用户使用。
对于不开源的软件, Release 可能是带有免费使用时间限制的版本。
Stable:稳定版,同 Release 版本。
版本匹配策略
我们会发现安装的依赖版本会出现: ^1.1.0 或 ~1.1.0,这是什么意思呢?
模糊匹配策略
- ^1.0.1、1、1.x 代表了可以命中主版本一致、但更新的版本号。
- ~1.0.1、1.1、1.1.x 代表了可以命中主版本、次版本一致、但更新的版本号。
- * 和 x 可以命中一切新发布的版本号。
dist-tag 和版本号
npm install vue@latest
# 或者换一句
npm install vue@next
npm 指令:dist-tag
npm dist-tag add @ []
//发布
npm publish --tag beta
beta、latest、next、preview、legacy、v2-beta
……
哪些需要装,哪些不需要
Q:你能一口气说清楚项目里 node_modules 里的那些依赖都是怎么来的吗?为什么下载了它们,以及为什么只下载了它们?
package.json 中的各种 dependencies
- dependencies(应用依赖,或业务依赖)
- devDependencies(开发环境依赖)
- peerDependencies(同等依赖,或同伴依赖)- 指定当前包兼容的宿主版本,如 gulp 插件
- optionalDependencies(可选依赖)- 不阻断整体流程
- bundledDependencies(打包依赖)- 包含依赖包名的数组对象,发布包时会打到最终的发布包里面
dependencies VS devDependencies
看一下上面的这个依赖关系图,你能说出哪些会被安装到 node_modules 么?
答案是:B、C、D、F
dependencies 和 devDependencies 的影响不是直接的,而是跨代的! 参考文章
package-lock.json
已经有了 package.json, 为什么会有 package-lock.json 文件呢?
项目依赖A@1.0.0,A 依赖了B@1.3.2和C@2.0.3
{
"dependencies": {
"A": "^1.0.0"
}
}
- 依赖
A
安装时下载了最新版,如1.2.3
,出现了兼容问题,项目出现 bug; - 依赖
A
所依赖的B
和C
下载了别的版本,导致A
出现问题,从而项目出现问题
作用:锁定安装时的包的版本号及包的依赖的版本号, 以保证其他所有人人在使用 npm install 时下载的依赖包都是一致的
关于 package.json 和 package-lock.json 的几个小结论:
- package.json 用于告诉 npm 项目运行需要哪些包, 但包的最终安装的版本不能够只依靠这个文件进行识别, 还需以 package-lock.json 为准
- package.json 中修改版本号会影响 package-lock.json, 并且 package.json 比 package.lock.json 的优先级高
- 为了保证该项目的环境依赖一致, 在项目移动时需要同时复制 package.json 和 package.lock.json 两个文件
- 不要轻易动 package.json 与 package-lock.json
删除重装一时爽,版本不对火葬场!!!
npm install 过程回顾
最后,咱们来看一下 npm install
的执行过程,来加深一下依赖管理的具体使用场景。
部分参考资料
- 前端工程化 – 剖析 npm 的包管理机制
- 拿来吧您!把“前端依赖”纳入知识体系
- npm — install 安装流程
- 详解 package-lock.json 的作用
- 版本校验工具
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net