前端依赖管理那点事儿

主要内容

  • 📝 什么是依赖
  • 📤 依赖从哪来
  • 🤹 安装到哪儿
  • 🎥 版本控制
  • 🧑‍💻 哪些需要装,哪些不需要
  • 🎨 package-lock.json
  • 🛠 npm install 过程回顾

什么是依赖

有时候,依赖是一堆 可执行的代码
有时候,依赖只是 一句声明

1.当我们的业务逻辑中使用到 Vue 时,我们只需要依赖(引入)它,我们就可以使用它的能力

import vue from 'vue'

or

<script src="https://unpkg.com/vue@next"></script>

此时,依赖就是获取一堆可用的代码

2.当我们在开发组件库时,我们需要使用到 Vue 中的一些方法

import { ref } from 'vue'

在这种场景下,组件所使用的 vue.js,实际上是宿主环境所依赖的 Vue

此时,依赖仅仅是一个声明

依赖从哪来

获取依赖的来源有哪些

  • 静态文件打包
  • CDN 引入
  • 仓库级引用
  • 从 npm 源安装

静态文件打包

前端资源通过相对路径进行引入,整体打包到项目中。

CDN 引入

优点:

  • 多域复用
  • 就近传输
  • 通过跨域达到 突破浏览器并发限制 的效果
  • ……

缺点:

引入公共 CDN 资源,需要评估风险,避免 CDN 域名污染后,资源异常

仓库级引用

git submodule

文章参考:《Git 中 submodule 的使用》

从 npm 源安装

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
installpostinstallnpm install 命令必然会执行的阶段

文档参考:

安装到哪儿

node_modules ? 当然是对的!但是并不准确。

依赖地狱

说到 node_modules,总是离不开要看看它的依赖地狱图

我们分别以 ReactVue 为例,单独安装和通过 cli 工具进行安装,node_modules的安装结果:

  • 单独安装 ReactReactDOM,占用 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.0AC 下版本是一致的);
  • 安装时额外下载或拷贝了大量重复的资源,并且实际上也占用了大量的硬盘空间资源等(例如,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 <pkg>@<version> [<tag>]
    
    //发布
    
    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 所依赖的 BC 下载了别的版本,导致 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 的执行过程,来加深一下依赖管理的具体使用场景。


    内容收录于github 仓库

    参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端荣耀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值