学习这个知识,就不不得不完整的学一学前端包管理器的发展史。
概述
包的概念:包含一些元数据:名称,描述,给git主页,许可证协议,作者,依赖等等。
背景
commonjs的出现,node环境有了模块化的概念,可以将代码拆分的特别细。这种很细的划分,是开发大型应用的基石。
开发中一些的常见的问题,就可以使用第三方库封装。 使用CommomJS标准书写,很方便对这些工具进行使用。
但是下载这些第三方库的时候,遇到很多问题:
-
下载过程繁琐:找到地址,下载对应版本,粘贴到工程中,并且遇到同名的库,还得需要该名称
-
如果该库依赖其他库,还要先下载其他的库。也可能下载的不同的库中,有相同的包。下载之后的库还得放到准确的位置。 还可能不同的库中依赖相同的库版本还不同
-
依开发环境中的库如何再生产环境中还原(因为不可能将第三方库的全部上传,文件多,速度慢,还可能丢包)。 还有很多工具只会在开发环境中使用,如何区分这个库是否需要上传服务器。
-
更新库麻烦
-
自己开发的库,如何让别人使用,如何迭代。
前端包管理器
npm:node package manager。 node包管理器。运行在node环境中,解决上述的开发者使用包的各种问题。
前端所有的包管理器几乎都是基于npm。
为什么要在node环境?
浏览器无法进行操作本地文件,无法下载文件功能。 node属于服务器环境,没有浏览器的限制。
npm出现之后,node就内置了npm。
npm可以理解为很庞大的数据库,开发者将自己的库按照npm的规范,打包上传。使用者通过统一的地址下载第三方包。
npm提供了CLI命令。通过这些命令对npm的各种包操作。
包的安装
npm的服务器位于国外,网速会受影响。 所以需要淘宝镜像源。同步了海外的npm服务器。
npm安装一个包,分为两种安装方式: 本地安装,全局安装。
本地安装
在当前目录下,npm i
可以安装多个包 npm i react vue
安装完成之后,会放在node_modules文件夹下
node_modules文件越来越大,是不需要上传到git的, .gitignore中添加文件名后,再上传的时候会忽略其中的文件。
安装一个包,npm会分析依赖树,自动安装该包所依赖的其他包。
如果安装的包,带有自己的CLI命令。npm会将这些命令放在node_modules/.bin文件中。
使用npx可以执行这些命令。
全局安装
查看全局安装的目录:npm cpnfig get prefix
全局安装: npm install --global "包名" 或者 npm i -g '包名'。
全局安装仅 提供全局的CLI工具
什么时候全局安装?
频繁使用的命令工具。 并且这个工具只是在开发环境中支持。
包配置
现在遇到了几个问题:
node_module 是不能上传到服务器的, 那如何还原?
如何区分生产依赖和开发依赖?
如果自己也是一个包,该如何描述?
配置文件
包的描述需要固定名称的package.json文件
可以手动创建,但是一般情况下是使用 npm init 创建。
配置文件中有大量的信息:
- name: 必须使用英文字符
- version: 版本
版本规范: 主版本号.次版本号.补丁版本号
主版本号:当程序发生了巨大变化,新增了重大功能,新增了大量api或者技术架构发生了重大 变 化
次版本号:小范围更改,进行了局部的优化。
补丁版本号:仅解决了一些bug
-
description:描述
-
main:包的入口文件
-
repositort:包的仓库地址
-
keywords:搜索关键字
-
author:作者 必须是有效的npm的账户名 不正确会影响到发包
npm init -y 所有配置都是默认值,快速生成。
保存依赖关系
绝大多数的时候,我们并不是要发包,package.json最重要的作用是记录当前工程的依赖。
- dependencice 生产环境中的依赖
- devdependencice 开发依赖,仅开发环境中使用的依赖
安装依赖
css
// 安装到生产环境的依赖
npm i 包名
npm i --save
npm i -v
// 安装到开发环境的依赖
npm i --save-dev 包名
npm i -D
配置好依赖,使用命令安装
less
//如果后面不加报名,会下载package.json文件中所有依赖
npm install
npm i
// 仅安装生产环境中的依赖
npm i --production
这样就解决了两个问题 1还原node_modules的文件,2 上传代码之后, 服务器如何查找生产环境中所需要的依赖的问题。
npm会将安装的包进行扁平化(并不是一开始就这样,更新迭代出现的), 如果下载A包和B包,他们都依赖了C包。 ABC的目录结构时相同的。 如果C版本不同的话, 会在各自的包里嵌套。
包的使用
当使用nodejs导入模块时,如果模块路径不是以 ./ 或者../开头, node会认为该文件是来自node_modules目录中。
javascript
var _ = require("lodash")
// 这个文件node是如何进行查找的呢?
首先去查找 node_modules/lodash.js
没找到就会找 node_nodules/lodash/入口文件
如果在子目录中导入,首先在当前文件中找。未找到就会向上级查找,直到最顶层(不是当前工程的顶层,而是电脑文件夹的顶层)未找到就会报错
什么是入口文件?
-
node_modules 中 package.json的main字段的值,作为入口文件
-
如果没找到main字段,回会使用index.js
css
var a = requie(./a)
// node如何查找的呢
// 查找a.js文件, 如果没有会把a当作文件夹,并且将它看作是一个包,查看是否有package.json的main路径。 如果没有就继续查找 a.js
语义化版本
版本规范: 主版本号.次版本号.补丁版本号
- 主版本号:当程序发生了巨大变化,新增了重大功能,新增了大量api或者技术架构发生了重大变化
- 次版本号:小范围更改,进行了局部的优化。
- 补丁版本号:仅解决了一些bug
如果我自己编写了一个包, 依赖了其他包, 我们希望依赖的包保持固定版本。 也希望次版本号和补丁版本号可以提升。有时候也希望只有补丁版本号能够提升。
这时候我们就需要配置文件将这些依赖规则描述清楚。 这种规则的描述就是语义化版本。
列几个常见的
-
大于某版本
-
- 介于两个版本之间 1.2.1 - 1.3.4
- ~补丁版本号可以增加 ~1.3.4 保证了主版本号和次版本号, 补丁版本号大于4
- ^主版本不可变。次版本号和补丁版本号可以增加
package-lock.json
如果依赖的某个包(a)使用了某个语义化版本,当再次安装的时候,可能会因为这个包(a)的小版本升级,导致这个包(a)所依赖的包(b)进行了大版本更新。 这就很可能出现问题。
这个文件记录了安装包时的确切依赖关系,最大限度的避免了版本差异。
(最早是yarn解决的,后来npm也解决了)
npm脚本script
在开发的过程中,我们可能会反复使用很多CLI命令。这些命令繁琐复杂,第三方包不同命令也会跟着不同。很难记忆和管理,于是npm通过package.json配置script字段后,就可以很轻松的运行各种指令。
npm run 脚本名称
为了简化, start test stop 这三个时可以省略 run。
脚本命令中可以省略npx。
start脚本如果不配置,有默认配置 node server.js
运行环境配置
我们一般遇到的环境有 生产环境, 开发环境, 测试环境 等等。
根据代码在什么环境中运行, 要进行不同的操作,那该如何使用呢?
node中有一个全局变量global(类似于浏览器的window)
global中有一个属性时process。 这个属性包含了当前运行node程序的计算机的很多信息,有一个属性时env,这个属性包含了所有的环境信息。
我们通常通过系统变量 NODE_ENV(约定成俗)配置具体的值,这样就能结局上述中判断环境的问题。
这个系统变量可以手动添加(永久设置);
也可以使用脚本在项目启动的时候临时添加变量,当项目结束的时候再删除(临时设置);
window临时设置变量 : set NODE_ENV= development
但是不同的环境有不同的命令,为了避免差异有第三方库:cross-env
命令行"cross-env NODE_ENV= development"
yarn
基于npm,仍然使用了npm的数据库,但是提供了全新的CLI对包进行管理。
yarn为什么这么火爆,因为yarn当时解决了npm的很多存在的问题
npm过去的问题是什么?
- 依赖目录嵌套层次很深。 如果a和b同时依赖c包, 则会a包的node_module中安装c。b包的node_module中也安装c。 win系统无法支持很深的目录结构(256字符)
- 下载速度慢。 前一个包下载完才会下载下一个包。并且多个重复的版本重复下载。
- 过去控制台输出繁杂:过去npm安装依赖,输出依赖的详情信息,遇到报错极难排查。
- 工程移植问题:npm版本依赖模糊,确切版本不能一致。 (锁文件)
yarn解决的问题
- 使用了扁平结构,避免层次过深,避免了重复下载。
- 并行下载,多个线程
- 使用本地缓存。下载过的包会直接从本地拷贝
- 控制台仅输出关键信息
- yarn-lock文件记录确切依赖
优化
-
语义化命令
-
本身自带CLI命令的 可以直接使用yarn命令。
很快npm学习了yarn先进的理念,不断对自身进行优化,npm6几乎解决了上面的问题。
-
扁平化
-
并行下载
-
本地缓存
-
package-lock记录依赖
-
增加了命令别名
-
npx命令,启动本地的CLI工具
-
简化控制台输出
npm6之后,和yarn非常接近,甚至没有差距。
nvm
nvm不是包管理器,是用于管理多个node版本的工具
nvm就是用来切换node版本的工具
pnpm
16年出现的包管理器。后起之秀。
npm和yarn缓存的问题
- 是通过复制本地缓存的包到自己的工程。 如果多个工程,把缓存就复制了多个。
- 拷贝本身也需要时间
使用
npm i -g pnpm
命令只需要将之前的npm 换成pnpm就可以。
npx --- pnpx 功能也是相同的,唯一的区别是
npx在执行一个不存在的命令时,会在本地临时安装一个命令,用完之后会删除。
pnpx也是相同,只是安装这个命令的时候会使用pnpm
优势:
-
安装效率高()
-
node_modules目录简洁(只展示了所依赖的)
-
避免开发使用间接依赖的问题
-
极大降低磁盘的占用
原理
- 同样使用了缓存,缓存已经安装过的包,缓存放在当前磁盘的 pnpm-store中
- 和npm和yarn不同的点,pnpm使用软链接和硬链接的做法放置依赖,规避了拷贝的时间。使得安装和卸载的速度都变快。
- 如何做到node_modules只展示当前依赖呢? npm和yarn为了解决重复包和路径过长的问题,做了扁平结构的处理,而pnpm由于使用了符号链接和硬连接,解决了上面两个的问题。树结构本身就可以使依赖更清晰,也就可以恢复使用了。这样也解决了使用间接依赖的问题。
硬链接(指向同一个磁盘中的地址)
文件A -->内存地址
产生一个A的硬链接 B, B的文件也指向了A的内存地址。硬链接没有限制,同个文件可以产生多个硬链接.
如果A文件删除了, B还是可以存在的.
通过硬链接的方式不会产生额外的磁盘占用。
创建一个硬链接(只能给文件创建,不能给文件夹创建)
mklink /h 链接文件名称 目标文件
软连接
A-->内存地址, 创建A的软件链接B 此时B指向的不是A的内存地址,而是指向了A.
如果A文件删除了,B也就无效了.
创建一个软链接
为目录创建软连接 加上/d 如果时为文件创建软链接 不需要加
mklink /d 链接文件名称 目标文件
整理下这两者区别:
- 硬链接只能用于文件不能用于目录,软链接可以.
- 硬链接完成之后只和内容有关系,和链接的文件没有关系. 但是软连接只和连接到文件有关系,和文件内容没有直接关系
- node中运行时,硬链接是无法识别的,. 运行软连接文件,是按照链接文件的路径执行.
pnpm安装依赖的时候分析步骤:
- 要安装依赖A,通过查询依赖关系,.得到要安装的包是A和B
- 查看A和B是否有缓存,如果没有就下载,如果有下一步
- 创建node_nodules文件夹.开始结构化目录
- 创建.pnpm,管理包的依赖关系, node是读不到的(这里指的是,不干扰node寻找文件的规则)
- .pnpm中创建文件夹node_modules文件夹和.refisty.npm.taobao.org (镜像源命名) 真正保存包的文件夹. A包->版本名文件夹->node_modules->A包:创建缓存中A包的硬链接 ; 创建B包的软连接,(只会创建直接依赖)
为什么要创建这个node_modules文件, 因为A中文件,引入了B文件,为了保证A文件能找到B文件,按照node啊查找文件的方式,所以创建了这个文件夹.
- node_modules为什么创建? 简单来说 向下兼容. 总有人创建包的时候 不规范,使用了间接依赖, 使用pnpm之后因为找不到就会报错. 所以间接依赖都存放再这个文件夹下.(使用符号链接)
- .pnpm构建完成之后. 再创建我们项目中的依赖包的软连接.(这样就避免了使用简洁依赖)
pmpm会不会导致目录过长呢?
不会的, pnpm的层级是可以控制的.
因为到版本文件夹下, 就已经结束了..
bower
结论:已经被淘汰
浏览器模块化出现之前, 想解决浏览器端的包管理器
在浏览器端解决了包的问题.
Monorepo
monorepo是一种项目管理的策略模式, "单一代码仓库".
它解决了什么问题呢?
-
你有没有多个项目再不同的仓库, 每个仓库的eslint ts 都是相同的配置,能不能放在一个仓库里一同配置呢?
-
多个项目的存在相同的工具包,如何复用呢? 可能想过发布npm包.开发调试都很麻烦
-
每个项目依赖版本都要各自维护,
解决了这些问题,也就是monorepo的优势了
-
模块独立管理,代码复用方便,耦合度降低
-
统一项目的构建部署流程,统一规范'
缺点:
- 仓库庞大,clone慢
- 权限不能够好的控制
如何搭建
使用pnpm ,为什么是pnpm ,?
-
了解pnpm的都知道, 优点太多了.
-
pnpm提出了workspace, 使得包之间可以互相依赖, 不需要第三方支持, 天然支持monorepo
安装依赖
- 安装全局依赖 pnpm i 包名 -w
- 指定某个包安装依赖 pnpm add axios --filter
- 模块间的依赖 pnpm --filter i -S 给name1的包添加依赖 name2(进入name保重添加) 如果是根目录pnpm install -r --filter
- 给一个模块下添加公共模块: pnpm -F main-project add common