【NJ02】NodeJS基础篇—02 Npm&Yarn

184 阅读12分钟

前言

npm的全称是啥?哈哈哈,不会有同学不清楚吧,来来来,赶紧记住哈,node.js package management,中文翻译成nodejs包管理工具。 这篇文章才刚开始,是不是已经涨知识了~

全球最大的模块生态系统,里面所有的模块都是开源免费的;也是Node.js的包管理工具。

1. npm

1.1 npm源

  • -registry
npm config set registry=https//registry.npm.taobao.org
  • cnpm

淘宝NPM镜像,与官方NPM的同步频率目前为10分钟一次 官网: npm.taobao.org/
npm install -g cnpm –registry=https//registry.npm.taobao.org

使用cnpm安装包: cnpm install 包名

  • nrm

作用:修改镜像源 项目地址:www.npmjs.com/package/nrm
安装:npm install -g nrm

1.2 npm 常用指令

1.2.1 全局安装 ‐g

全局安装的包位于Node.js环境(全局)的node_modules目录下,全局安装的包一般用于命令行工具。

npm install ‐g 包名称 (全局安装)

1.2.2 本地安装

本地安装的包在当前目录下的node_modules里面,本地安装的包一般用于实际的开发工作

npm install 包名称 (本地安装)

1.2.3 生产环境‐‐save‐dev(项目部署上线之后的服务器环境)

‐‐save‐dev 向开发环境添加依赖 加载到了DevDependencies

1.2.4 开发环境‐‐save(平时开发使用的环境)

‐‐save 向生产环境添加依赖 加载到了dependencies的依赖

1.2.5 npm常用的命令:

  1. 安装包的时候可以指定版本
    npm install 包名称@版本号

  2. 卸载包
    npm uninstall 包名

  3. 更新包(更新到最新版本)
    npm update 包名

  4. 初始化包 npm init

  5. 安装所有依赖

npm install
  1. 执行包
npm run
  1. 搜索(也可以直接去npmjs官网找)
npm search <packageName>
  1. 查看已经安装的依赖包
npm ls
  1. npm查看本地缓存
npm config get cache

1.3 npm的安装机制

下面我们会通过一个流程图来具体学习npm install的安装机制

image.png

npm install执行之后, 首先会检查和获取 npm的配置,这里的优先级为:

项目级的.npmrc文件 > 用户级的 .npmrc文件 > 全局级的 .npmrc > npm内置的 .npmrc 文件

然后检查项目中是否有 package-lock.json文件

  • 如果有, 检查 package-lock.jsonpackage.json声明的依赖是否一致:

    • 一致, 直接使用package-lock.json中的信息,从网络或者缓存中加载依赖
    • 不一致, 根据上述流程中的不同版本进行处理
  • 如果没有, 那么会根据package.json 递归构建依赖树,然后就会根据构建好的依赖去下载完整的依赖资源,在下载的时候,会检查有没有相关的资源缓存:

    • 存在, 直接解压到node_modules文件中
    • 不存在, 从npm远端仓库下载包,校验包的完整性,同时添加到缓存中,解压到 node_modules中

最后, 生成 package-lock.json 文件

其实, 在我们实际的项目开发中,使用npm作为团队的最佳实践: 同一个项目团队,应该保持npm 版本的一致性

从上面的安装流程,不知道大家注意到了一点没有,在实际的项目开发中,如果每次都去安装对应依赖时,如果相关的依赖包体积过大或者是依赖于网络,无疑会增加安装的时间成本;那么,缓存在这里的就是一个解决问题的好办法。

2. Yarn

类比npm基本使用,安装yarn工具

npm install ‐g yarn

2.1 yarn 源

  • -registry
yarn config set registry=https//registry.npm.taobao.org

2.2 yarn 常用指令

2.2.1 全局安装 global

全局安装的包位于Node.js环境(全局)的node_modules目录下,全局安装的包一般用于命令行工具。

yarn global add 包名称 (全局安装)

2.2.2 本地安装

本地安装的包在当前目录下的node_modules里面,本地安装的包一般用于实际的开发工作

yarn add 包名称 (本地安装)

2.2.3 生产环境‐‐dev(项目部署上线之后的服务器环境)

--dev 向开发环境添加依赖 加载到了DevDependencies

yarn add xxx ‐‐dev

2.2.4 开发环境‐‐save(平时开发使用的环境)

‐‐save 向生产环境添加依赖 加载到了dependencies的依赖

yarn add xxx  //自动加了‐‐save

2.2.5 yarn常用的命令:

  1. 安装包的时候可以指定版本
    yarn add 包名称@版本号

  2. 卸载包
    yarn remove 包名

  3. 更新包(更新到最新版本)
    yarn upgrade 包名

  4. 初始化包 yarn init

  5. 安装所有依赖

yarn add
  1. 执行包
yarn run

2.3 yarn的安装机制

让我们先来简单的看一下Yarn的安装理念。

简单来说, Yarn的安装大致分为5个步骤:

image.png

检测(checking) ---> 解析包(Resolving Packages) ---> 获取包(Fetching) ---> 链接包(Linking Packages) ---> 构建包(Building Packages)

那么接下来我们要开始具体分析这些过程中都做了哪些事情:

检测包

这一步,最主要的目的就是检测我们的项目中是否存在npm相关的文件,比如package-lock.json等; 如果有,就会有相关的提示用户注意:这些文件可能会存在冲突。在这一步骤中 也会检测系统OS, CPU等信息。

解析包

这一步会解析依赖树中的每一个包的信息:

首先呢,获取到首层依赖: 也就是我们当前所处的项目中的package.json定义的dependenciesdevDependenciesoptionalDependencies的内容。

紧接着会采用遍历首层依赖的方式来获取包的依赖信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过的包和正在进行解析包用Set数据结构进行存储,这样就可以保证同一版本范围内的包不会进行重复的解析:

举个例子
  • 对于没有解析过的包A, 首次尝试从 yarn.lock中获取版本信息,并且标记为已解析
  • 如果在yarn.lock中没有找到包A, 则向Registry发起请求获取满足版本范围内的已知的最高版本的包信息,获取之后将该包标记为已解析。

总之,经过解析包这一步之后呢,我们就已经确定了解析包的具体版本信息和包的下载地址。

image.png

获取包

这一步首先我们会检查缓存中是否有当前依赖的包,同时呢将缓存中不存在的包下载到缓存的目录中。但是这里有一个小问题需要大家思考一下:

比如: 如何去判断缓存中有当前的依赖包呢?

其实呢,在Yarn中会根据 cacheFolder+slug+node_modules+pkg.name 生成一个路径;判断系统中是否存在该path,如果存在证明已经有缓存,不用重新下载。这个path也就是依赖包缓存的具体路径。

那么对于没有命中的缓存包呢?在 Yarn 中存在一个Fetch队列,按照具体的规则进行网络请求。如果下载的包是一个file协议,或者是相对路径,就说明指向一个本地目录,此时会调用Fetch From Local从离线缓存中获取包;否则调用 Fetch From External 获取包,最终获取的结果使用 fs.createWriteStream 写入到缓存目录。

image.png

链接包

我们上一步已经把依赖放到了缓存目录,那么下一步,我们应该要做什么事情呢?是不是应该把项目中的依赖复制到node_modules目录下呢,没错;只不过此时需要遵循一个扁平化的原则。复制依赖之前, Yarn会先解析 peerDepdencies,如果找不到符合要求的peerDepdencies的包,会有 warning提示,并最终拷贝依赖到项目中。

image.png 构建包

如果依赖包中存在二进制包需要进行编译,那么会在这一步进行。

3. npm & yarn 区别

locklife文件不同

我们可以来看一下 yarn.lock的结构:

"@babel/cli@^7.1.6", "@babel/cli@^7.5.5":
  version "7.8.4"
  resolved "http://npm.in.zhihu.com/@babel%2fcli/-/cli-7.8.4.tgz#505fb053721a98777b2b175323ea4f090b7d3c1c"
  integrity sha1-UF+wU3IamHd7KxdTI+pPCQt9PBw=
  dependencies:
    commander "^4.0.1"
    convert-source-map "^1.1.0"
    fs-readdir-recursive "^1.1.0"
    glob "^7.0.0"
    lodash "^4.17.13"
    make-dir "^2.1.0"
    slash "^2.0.0"
    source-map "^0.5.0"
  optionalDependencies:
    chokidar "^2.1.8"

熟悉npm的package-lock.json文件的朋友,可能一眼就看到了一些不同; package-lock.json采用的是JSON的结构,而yarn并没有采用这种结构,而是一种自定义的标记方式;我们可以看出新的自定义的方式,也同样保持了高度的可读性。

相比于npm,Yarn另一个显著的区别就是yarn.lock的子依赖的版本不是固定的版本。这其实就说明了一个问题: 一个单独的yarn.lock的问题并不能确定✅node-modules的文件结构,还需要package.json的配合。

资源来源优先级

yarn默认采用的是perfer-online模式,即优先使用网络资源。如果网络资源请求失败,再去请求缓存数据。

4. 自定义包

包的规范

  • package.json必须在包的顶层目录下
  • 二进制文件应该在bin目录下
  • JavaScript代码应该在lib目录下
  • 文档应该在doc目录下
  • 单元测试应该在test目录下

5. package.json字段分析

  • name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格【必填】
  • description:包的简要说明
  • version:符合语义化版本识别规范的版本字符串,格式 x.x.x【必填】
  • keywords:关键字数组,通常用于搜索
  • maintainers:维护者数组,每个元素要包含name、email(可选)、web(可选)字段
  • contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者数组的第一个元素
  • bugs:提交bug的地址,可以是网站或者电子邮件地址
  • licenses:许可证数组,每个元素要包含type(许可证名称)和url(链接到许可证文本的地址)字段
  • repositories:仓库托管地址数组,每个元素要包含type(仓库类型,如git)、url(仓库的地址)和path(相 对于仓库的路径,可选)字段
  • dependencies:生产环境包的依赖,一个关联数组,由包的名称和版本号组成
  • devDependencies:开发环境包的依赖,一个关联数组,由包的名称和版本号组成
  • main: 包的入口主文件
  • scripts: 自定义脚本,通过 npm run 脚本名称即可执行脚本定义的命令
"scripts": {"test": "echo \"Error: no test specified\" && exit 1" },

6. 其他问题

  • 当你项目依赖出现问题的时候, 我们会不会是直接删除 node_modules 和 lockfiles依赖, 再重新 npm install,删除大法是否真的好用?这样的使用方案会不会带来什么问题?

  • 一个项目中, 你使用 yarn, 我使用npm,会不会有问题呢?

  • 还有一个问题, lockfiles 文件 我们提交代码的时候需不需要提交到仓库中呢?

那么package-lock.json文件的作用是什么呢? 
「锁定依赖的安装结构」, 
 这么做的目的是为了保证在任意的机器上我们去执行 npm install,
 都会得到完全相同的 node_modules安装结果。
 
 - 如果是开发一个应用, 我的理解是 `package-lock.json`文件提交到代码版本仓库.
 这样可以保证项目中成员、运维部署成员或者是 CI 系统, 在执行 `npm install`后,
 保证在不同的节点能得到完全一致的依赖安装的内容。
 
 - 如果说我们开发的库依赖了一个精确版本号的模块, 那么在我们去提交 lockfiles 到仓库中可能就会出现, 
 同一个依赖被不同版本都被下载的情况。
 如果我们作为一个库的开发者, 其实如果真的使用到某个特定的版本依赖的需求, 
 那么定义**peerDependencies** 是一个更好的选择。
  • 为什么会有 xxxDependencies?

其实, npm 设计了以下的几种依赖类型声明:

-   dependencies 项目依赖
-   devDependencies 开发依赖
-   peerDependencies 同版本的依赖
-   bundledDependencies 捆绑依赖
-   optionalDependencies 可选依赖

其实, npm 设计了以下的几种依赖类型声明:

  • dependencies 项目依赖
  • devDependencies 开发依赖
  • peerDependencies 同版本的依赖
  • bundledDependencies 捆绑依赖
  • optionalDependencies 可选依赖

它们起到的作用和声明意义是各不相同的。下面我们来具体介绍一下:

dependencies 表示项目依赖,这些依赖都会成为你的线上生产环境中的代码组成的部分。当它关联到 npm 包被下载的时候, dependencies下的模块也会作为依赖, 一起被下载。

devDependencies表示开发依赖, 不会被自动下载的。因为 devDependencies 一般是用于开发阶段起作用或是只能用于开发环境中被用到的。 比如说我们用到的 Webpack,预处理器 babel-loaderscss-loader,测试工具E2E等, 这些都相当于是辅助的工具包, 无需在生产环境被使用到的。

这里有一点还是需要我去啰嗦一下的,并不是只有在dependencies中的模块才会被一起打包, 而是在 devDependencies 中的依赖一定不会被打包的。 实际上, 依赖是否是被打包,完全是取决你的项目里的是否是被引入了该模块

peerDependencies 表示同版本的依赖, 简单一点说就是: 如果你已经安装我了(PackageA), 那么你最好也安装我对应的依赖(A\B\C\D)。 这里举个小例子: 加入我们需要开发一个react-ui 就是一个基于react 开发的UI组件库, 它本身是会需要一个宿主环境去运行的, 这个宿主环境还需要指定的 react版本来搭配使用的, 所以需要我们去 package.json中去配置:

"peerDependencies": { "React": "^17.0.0" }

optionalDependencies表示可选依赖,就是说当你安装对应的依赖项安装失败了, 也不会对整个安装过程有影响的。一般我们很少会用到它, 这里我是 不建议大家去使用, 可能会增加项目的不确定性和复杂性。