前端包管理器

106 阅读17分钟

概念

module、library、package

模块(modulue):通常以单个文件形式存在的功能片段,入口文件通常称之为入口模块或者主模块

库(library/lib):以一个或多个模块组成的完整功能块,为开发中某一方面的问题提供完整的解决方案

包(package):包含元数据的库,这些元数据包括:名称、描述、git主页、许可证协议、作者、依赖等

image.png

背景

commonJS的出现,使node环境下的js代码可以用模块更加颗粒度的划分,一个类、一个函数、一个对象、一个配置等等都可以作为一个模块,这种细粒度的划分,是开发大型应用的基石

为了解决在开发过程中遇到的常见问题,比如加密、提供常见的工具方法、模拟数据等,一时间,在前端社区出现了大量的第三方库,这些库使用commonJs标准书写而成,非常容易使用。

然而,在下载使用这些第三方库的时候,遇到了难以处理的问题:

  • 下载过程繁琐
    • 进入官网或github主页
    • 找到并下载对应版本
    • 拷贝到工程的目录中
    • 遇到同名的库,修改名称
  • 如果该库依赖其他的库,还需要按照要求先下载其他的库
  • 开发环境中安装的大量的库如何在生产环境中还原,又如何区分
  • 更新一个库极度麻烦
  • 自己开发的库,如何在下一次开发使用

以上这些问题,就是包管理器工具要解决的问题

前端包管理器

npm

几乎可以这样认为,前端所有的包管理器都是基于npm的,目前,npm既是一个包管理器,也是其他包管理器的基石

npm:node package manager,即node包管理器,它运行在node环境中,让开发者用简单的方式完成包的查找、更新、卸载、上传等操作

npm之所以要运行在node环境,而不是浏览器环境,根本原因是因为浏览器环境无法提供下载、删除、读取本地文件的功能(为了保护用户隐私,js不允许操作本地文件),而node属于服务器环境,没有浏览器的种种限制,理论上可以完全掌控运行node的计算机

npm的出现,弥补了node没有包管理器的缺陷,于是很快,node在安装文件中就内置了npm,安装好node就自动安装了npm,node环境还为npm提供了良好的支持,使得使用npm下载的包变得更加方便

npm的组成

  1. registry:入口
    • 可以想象成一个巨大的数据库
    • 第三方库的开发者,将自己的库按照npm的规范,打包上传到数据库中
    • 使用者通过统一的地址下载第三方库
  2. 官网
    • 查询包
    • 注册、登录、管理个人信息
  3. CLI(Command Line Interface):命令行接口
    • 安装好npm后,使用CLI来使用npm的各种功能

npm CLI

1、仓库设置

npm的官方registry服务器位于国外,导致下载时速度缓慢或者下载失败;淘宝提供了一个国内的registry地址,可以使用设置淘宝的镜像源地址为registry地址进行依赖的下载

设置淘宝镜像源

npm config set registry https://registry.npmmirror.com/

备注:淘宝镜像源的域名变更

image.png

查看registry

npm config get registry
2、安装方式
  • 本地安装
npm install XXX
npm i XXX
- 本地安装的包出现在当前目录的`node_modules`目录中
- `node_modules`目录可能会异常庞大,不适合传输到生产环境,因此使用`.gitignore`文件来忽略该目录的内容
- 本地安装适用于大多数的包,他会在当前目录及其子目录中发挥作用,通常在项目的根目录中使用本地安装
- 安装一个包时,npm会自动管理依赖,将该包得依赖包下载到`node_modules`- 如果本地安装的包带有CLI,npm会将他的CLI脚本文件放置在`node_modules/.bin`下,使用 `npx 命令名` 即可调用
  • 全局安装
npm install --global xxx
npm i -g xxx

全局安装的包放置在一个特殊的全局目录下,可以通过命令npm config get prefix查看该目录

全局安装的包并非所有工程可用,他仅提供全局的CLI工具

大部分情况下,都不需要全局安装,除非:

  1. 包的版本非常稳定,很少有大的更新
  2. 提供的CLI工具在各个工程中使用得非常频繁
  3. CLI工具仅为开发环境提供支持,而非部署环境
3、包配置

目前遇到的问题:

1、拷贝后的工程如何还原?

2、如何区分开发依赖和生产依赖?

3、如果自身的项目也是一个包,如何描述包的信息

以上这些问题都需要通过包的配置文件解决

配置文件

npm将每个使用npm的工程本身都看做一个包,包的信息需要通过一个名称固定的配置文件描述,配置文件的名称固定为package.json

可以手动创建一个package.json文件,也可以使用npm init可以生成一个package.json文件,该命令会引导填写包的相关信息,也可以使用npm init --yes 或者npm init -y生成一个默认配置的package.json文件

配置文件中可以描述大量的信息,包括:

  1. name
  2. version:版本
  3. description:包的描述
  4. homepage:官网地址
  5. author:包的作者,必须是有效的npm账户名,书写规范account<email>,不正确的账号和邮箱就可能会导致发布包时失败
  6. mian:包的入口文件

package.json最重要的作用,是记录当前工程的依赖

  • dependencies:生产环境的依赖包
  • devDependencies:仅开发环境的依赖包

配置好依赖后,使用下面的命令就可以安装依赖

// 本地安装所有依赖 dependencies+devDependencies
npm install
npm i

//仅安装生产环境的依赖 dependencies
npm install --production

这样一来,代码移植就不是问题了,只需要移植源代码和package.json文件,不用移植node_modules目录,然后在移植之后通过命令即可恢复安装

//安装生产环境依赖
npm i --save xxx
npm i -S xxx
npm i XXX
//安装开发环境依赖
npm i --save-dev xxxx
npm i -D xxx

现在安装依赖默认保存到package.json,可以省略--save

4、包使用(查找规则)

使用nodejs导入模块时, eg:var = require('xxx')

if(如果模块路径是./或者../){
    根据当前目录去查找你的本地代码文件
}else{
    if(导入的模块是node的内置模块){
        直接导入node的内置模块(fs、path等)     
    }else{
        //首先在当前目录的以下位置寻找文件
        if(node_modules/xxx.js存在){
            找到该模块
        }else if(node_modules/xxx/入口文件 存在){
            找到该模块    
        }else{
            返回上级目录按照同样方式查找
            先找node_modules/XXX.js,再找node_modules/xxx/入口文件
            如果到了顶级目录都无法找到文件,则抛出错误     
        }
    }
}

入口文件按照以下规则确定:

1、查看导入包的package.json文件,读取main字段作为入口文件

2、若不包含main字段,则使用index.js作为入口文件

5、语义版本

版本号规则:

版本规范:主版本号.次版本号.补丁版本号

主版本号:仅当程序发生了重大变化时才增加,如新增了重要的功能、新增了大量的API,技术架构发生了重大变化

次版本号:仅当程序发生了一些小变化时才会增加,如新增了一些小功能,新增了一些辅助型的API

补丁版本号:仅当解决了一些bug或进行了一些局部优化时更新,如修复了某个函数的bug,提升了某个函数的运行效率

符号含义示例示例描述
'>'大于该版本>1.0.0大于1.0.0版本
>=大于等于该版本>=1.0.0大于等于1.0.0版本
<小于该版本<1.0.0小于1.0.0版本
<=小于等于该版本<=1.0.0小于等于1.0.0版本
=等于该版本=1.0.0等于1.0.0版本
-介于两版本之间1.0.0-1.2.0介于1.0.0和1.2.0之间
x不固定的版本号1.3.X只要保证主版本号是1,次版本号是3即可
~主版本和次版本锁定,补丁版本可增~1.3.6保证主版本号是1,次版本号是3,补丁版本号大于等于6
'^'主版本锁定,次版本和补丁版本可增^1.4.7报好着呢个主版本号是1,次版本号可以大于等于4,补丁版本号可以大于等于7
*最新版本*始终安装最新版本

避免还原的差异

npm在安装包的时候,会自动生成一个package-lock.json文件,该文件记录了安装包时的确切依赖关系,移植工程时,如果移植了package-lock.json,恢复安装时,会按照package-lock.json文件中的确切依赖进行安装,最大限度的避免了差异

6、 npm脚本(npm scripts)

在开发过程中,我们可能会反复的使用很多第三方的CLI命令,这些命令繁杂难于记忆,所以npm支持了脚本,只需要在package.json中配置scripts字段,即可配置各种脚本名称,配置后,就可以通过npm run 脚本名称 来完成各种操作了,npm对常用的脚本名称做了简化,start、stop、test脚本名称是可以省略run,start脚本的默认值:node server.js

本地安装的依赖含有CLI,在node_modules/.bin目录下会出现该依赖的CLI命令,直接在终端中运行会报错,可以使用npx 命令来运行,eg:如果安装了nodemon,则可以使用npx nodemon index.js来运行,将CLI配置在脚本中,配置时可以省略npx,eg:"start:nodemon index.js

脚本中可以配置任何命令

    "script":{
        "mkdir": "mkdir src",
        "open":"chrome https//www.xxx.com"
     }
7、运行环境配置

代码运行的环境一般有三种

1、开发环境

2、测试环境

3、生产环境

有的时候,我们需要在代码中根据环境做出不同的处理,如何让node知道处于什么环境中,是极其重要的

通常我们使用如下的处理方式:

node有一个全局变量global,global有一个属性process,该属性是一个包含了运行node程序的计算机的很多信息的对象,其中一个信息是env,是一个对象,包含了计算机中所有的系统变量

通常,我们通过系统变量NODE_ENV的值来判断node程序处于何种环境,设置NODE_ENV有如下两种方式:

  • 永久设置:在系统的环境变量中,添加NODE_ENV,使用时通过proces.env.NODE_ENV读取值
  • 临时设置:在配置脚本时,使用set NODE_ENV=xxx(windows) 来设置环境变量后启动程序,eg:"start":"set NODE_ENV=development && node index.js";不同操作系统设置环境变量的方式不同,mac和服务器:"start":"export NODE_ENV=development && node index.js";为了避免不同系统的设置方式差异,可以使用第三方库cross-env对环境变量进行设置,"start":"cross-env NODE_ENV=development node index.js"

在node中读取package.json文件中的配置,如果你导入的内容是一个json数据,导入时node会将该json数据转成一个对象

8、 npm命令
//安装
//安装精确版本
npm install --save-exact xxx
npm i -E xxx
//安装指定版本
npm i xxx@版本号

//查询
//查看包安装路径
npm root [-g]
//查看包信息,view别名:v、info、show
npm view 包名 [子信息]
//查询安装包
npm list [-g] [--depth=依赖深度]

//更新
//检查哪些包需要更新
npm outdated
//更新包,update别名:up、upgrade
npm update [-g] [包名]

//卸载
//unstall别名:remove、rm、r、un、unlink
npm uninstall [-g] 包名

//npm配置
//查询目前生效的各种配置
npm config ls [-l] [--json]
//获取某个配置项
npm config get 配置项
//设置某个配置项
npm config set 配置项 值
//移除某个配置
npm config delete 配置项
9、发布包

准备工作:

1、移除淘宝镜像源

2、npm官网注册账号并完成邮箱认证

3、使用npm login登录(可以使用npm whoami 查看当前登录的账号,使用npm logout退出登录)

4、创建工程根目录

5、npm init进行初始化

发布:

1、开发

2、确定版本

3、npm publish 完成发布

yarn

yarn使用的是npm的registry

过去npm存在的问题

  1. 依赖包嵌套层级深(过去,npm的依赖是嵌套的,在windows中这是一个极大的问题,windows的目录层级不能过深,默认情况下,目录路径最多支持256个字符)
  2. 下载速度慢
    • 依赖嵌套,所以下载是串行的,导致宽带资源未被完全利用
    • 多个相同版本包被重复下载
  3. 控制台输出繁杂,错误信息定位难
  4. 工程移植问题(当时的npm无lock文件,版本依赖模糊,导致工程移植后,依赖的版本不一致)

yarn如何解决过去npm存在的问题

  1. 使用扁平目录结构
  2. 并行下载,不重复下载,使用本地缓存
  3. 控制台输出关键信息
  4. yarn-lock记录确切的版本

yarn的优化内容:

  1. 增加了某些功能强大的命令
  2. 让既有的命令更加语义化
  3. 本地安装的CLI工具可以直接使用yarn直接启动,eg:yarn nodemon index.js
  4. 把全局安装的目录作为一个普通工程,生成package.json,便于全局安装移植

yarn的出现促进了npm的发展,npm借鉴yarn对自身进行了优化,几乎解决了上面的问题:

  1. 目录扁平化
  2. 并行下载,本地缓存(npm-cache,清空缓存:npm cache clean
  3. package-lock记录确切依赖
  4. 增加了大量的命令别名
  5. 内置了npx,可启动本地的CLI
  6. 简化了控制台的输出

yarn的核心命令

//初始化
yarn init [--yes/-y] 

//添加指定包
yarn [global] add xxx [--dev/-D] [--exact/-E]

//安装package.json中所有的依赖
yarn install [--production/-prod]

//脚本和本地CLI(start、stop、test可以省略run)
yarn run 脚本名/CLI//查询
//查看bin目录
yarn [global] bin
//查询包信息
yarn info 包名 [子字段]
//列举已经安装的依赖
yarn [global] list [--depth=依赖深度]


//更新
//查询需要更新的包
yarn outdated
//更新包
yarn [global] upgrade [包名]

//卸载
yarn remove 包名

yarn 增加的功能强大的命令

//验证package.json文件的依赖记录和lock文件是否一致,防止篡改
yarn check

// 检查本地安装的包有哪些漏洞
yarn audit

//为什么安装了这个包,哪些包用到了它
yarn why

//一步完成安装脚手架和搭建工程
yarn create
//示例
yarn create react-app my-app 
//等于以下两个命令
yarn global add create-react-app
create-react-app my-app

cnpm

因为以前的npm未提供修改registry的功能,所以淘宝提供了CLI工具cnpm,支持除了npm publish以外的其他所有命令,只不过连接的是淘宝镜像源

pnpm

pnpm的优势

  1. 安装效率高于npm和yarn
  2. 极其简洁的node_modules目录
  3. 避免了开发时使用间接依赖的问题
  4. 能极大的降低磁盘空间占用

安装和使用

npm i -g pnpm

使用时,只需要把npm替换成pnpm即可

如果要执行本地的CLI,可以使用pnpx,和npx功能完全一样,唯一不同的区别是,在使用pnpx执行一个需要安装的命令时,会使用pnpm进行安装

补充:npx或pnpx在执行本地CLI时,没有该命令,npx/pnpx会自动的、临时的安装该依赖包,再运行命令;比如:npx mocha执行本地mocha命令时,如果没有mocha,则npx会自动的、临时的安装mocha,安装好后,自动运行mocha命令

pnpm的原理

1、同yarn和npm一样,pnpm依然使用缓存来保存已经安装过的包,以及使用pnpm-lock.yaml来记录详细的依赖版本

2、不同于yarn和npm,pnpm使用符号链接和硬链接的做法来放置依赖,从而规避了从缓存中拷贝文件的时间,使得安装和卸载的速度更快

3、由于使用了符号链接和硬链接,pnpm可以规避windows操作系统路径过长的问题,因为,它选择使用树形的依赖结果,有着几乎完美的依赖管理。也因为如此,项目只能使用直接依赖,不能使用间接依赖

补充知识点

HCI:Human-Computer Interaction,人机交互,研究和设计人类与计算机之间交互的学科

HCI的分类:

  1. CLI:Command Line Interfalce
  2. GUI:Graphical user Interface,图形用户界面
  3. 触摸屏
  4. 语音交互
  5. 虚拟现实VR以及增强现实AR
  6. 脑机接口
  7. 可穿戴设备

文件的本质

在操作系统中,文件的本质是一个指针,只不过指向的不是内存地址,而是一个外部存储地址(硬盘、u盘、网络),当我们删除文件时,删除的实际上是指针,无论删除多大的文件,速度都非常快

image.png

文件的拷贝

复制一个文件时,是将该文件的指针指向的内容进行复制,然后产生一个新文件指向新的内容

image.png

硬链接 hard link

将一个文件A指针复制到另一个文件B指针中(类似于对象的赋值),文件B就是文件A的硬链接

image.png 通过硬链接,不会产生额外的磁盘占用,并且,两个文件都能找到相同的磁盘内容,硬链接没有数量限制,可以为同一个文件产生多个硬链接

cmd中使用命令mklink /h 链接名称 目标文件创建硬链接,由于文件目录不存在文件内容,所以文件夹(目录)不可以创建硬链接,在windows中,通常不要跨越盘符创建硬链接

符号链接(软链接) symbol link

如果为某个文件或者文件夹A创建符号链接B,则B指向A

image.png

创建符号链接:mklink /d 链接名称 目标文件,/d:创建目录的符号链接

符号链接和硬链接的区别

1、硬链接仅能链接文件,符号链接可以链接目录和文件 2、硬链接在链接完成后仅和文件内容相关联,和之前链接的文件没有任何关系;符号链接始终和之前链接的文件关联,和文件内容不直接相关

快捷方式

快捷方式类似于符号链接,是windows系统早期就支持的链接方式

它不仅仅是一个指向其他文件或目录的指针,其中还包括了各种信息,如权限、兼容性启动方式等其他各种属性

由于快捷方式是windows系统独有的,在跨平台的应用中一般不会使用

node环境对硬链接和符号链接的处理

  • 硬链接:硬链接是一个实实在在的文件,node不对其做任何特殊处理,因为无从知晓该文件是不是一个硬链接
  • 符号链接:由于符号链接指向的是另一个文件或目录,当node执行符号链接下的js文件时,会使用原始路径(可自行借助__dirname验证是否是原始路径)

pnpm中如何使用硬链接和符号链接来构建node_modules目录

屏幕截图 2024-09-14 171136.png