侧边栏

初识npm

发布于 | 分类于 前端/模块化

npm全称为Node Package Manager,是一个基于Node.js的包管理器,用于查找、安装、管理和发布 Node.js 应用程序所需的各种包和模块,避免 JS 开发者重复造轮子,让大家的劳动成果可以共享。

npm是前端工程化的必须要掌握的基础知识,本文将整理通过npm安装依赖的一些细节。

参考

相关概念

模块与包

参考:

模块Module是任何的能被Nodejs程序使用require加载的模块。满足以下条件均可以称为Module:

  • 一个文件夹包含package.json文件并指定了main字段
  • 一个文件夹包含index.js文件
  • 一个javascript文件

JavaScript模块规范有CommonJSES6 moduleAMD等多种形式,已整理至博客JavaScript模块管理机制

大多数Package都是一个Module,下面是官网上关于pacekage的定义,可以大致了解一下,不必拘泥于相关概念

  • (a)一个包含了程序和描述该程序的 package.json 文件 的 文件夹
  • (b)一个包含了 (a) 的 gzip 压缩文件
  • (c)一个可以下载得到 (b) 资源的 url (通常是 http(s) url)
  • (d)一个格式为 <name>@<version> 的字符串,可指向 npm 源(通常是官方源 npmjs.org)上已发布的可访问 url,且该 url 满足条件 (c)
  • (e)一个格式为 <name>@<tag> 的字符串,在 npm 源上该<tag>指向某 <version> 得到 <name>@<version>,后者满足条件 (d)
  • (f)一个格式为 <name> 的字符串,默认添加 latest 标签所得到的 <name>@latest 满足条件 (e)
  • (g)一个 git url, 该 url 所指向的代码库满足条件 (a)

package.json

package.json 文件就是定义了项目的各种元信息,包括项目的名称、git仓库地址、作者等等,最重要的是其中定义了我们项目的依赖插件和环境。

可以通过npm init初始化一个Node项目,根据命令提示输入关键信息,最后会自动创建package.json

也可以通过npm init -y快速创建package.json文件,相关字段都是默认的占位数据,可以自己手动编辑。

npm script

参考:npm scripts 使用指南

pacakge.json中还有一个scripts配置项,用来声明当前工程常用脚本。

常见的开发环境,都有类似的一个或多个npm scripts命令,如npm run buildnpm run dev

每当执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。

由于开发环境依赖的一些包可能自己也提供了可执行命令,比如hexo就提供了hexo drafthexo publish等命令,除了全局安装之后,也可以通过npx直接运行node_modules中某个包提供的命令。

局部安装的包,如果提供了二进制命令,将会放在node_modules/.bin目录下, npx 会帮你执行依赖包里的二进制文件。

bash
 //非全局安装
npm i webpack -D

// 无npx 执行 webpack 的命令
./node_modules/.bin/webpack -v

// 使用npx
npx webpack -v

安装依赖

开发依赖与运行依赖

关于包依赖有devDependenciesdependencies两个字段,他们的区别在于:

  • 前者是开发的时候需要的依赖项,比如webpack等,使用npm i xxx -D安装,对应的依赖包会放在devDependencies
  • 后者是程序正常运行需要的包,比如vuejQuery等,使用npm i xxx -S安装,对应的依赖包会放在dependencies

此外某些框架或工具还会额外扩展package.json的配置,如babel等可以也可以直接在package.json中配置

语义化版本号

npm包的版本号使用semver 定义,可以通过npm i [email protected]的形式安装指定版本的包。

版本号实际上是一个特定含义的字符串,其格式为主版本号.次版本号.修订号,版本号递增规则如下

  • 主版本号:当你做了不兼容的 API 修改,
  • 次版本号:当你做了向下兼容的功能性新增,
  • 修订号:当你做了向下兼容的问题修正。

比较大的开源包,一般都会遵循严格的版号,避免用户在升级过程中出现兼容问题。

package.json中的依赖版本号可以包含^~>=等前缀,详情可见文档

  • caret(箭头)表示: ^2.0.2能帮你下载最新的2.x.x的包,不能下载1.x.x的包。比如最新的是2.1.0, 就是直接下载2.1.0。
  • tilde(波浪线)表示: ~2.0.2能帮你下载2.0.x的最新包,不能下载2.1.x的包,比 ^ 要更加谨慎一些。比如最新的包如果是2.0.3, 就会下载,而如果是2.1.3就不会下载。
  • >=表示需要版本号大于或等于指定版本
  • <=表示需要版本号小于或等于指定版本
  • 没有任何符号就表示严格匹配,必须下载该版本号的依赖包
  • 任意两条规则,通过 || 连接起来,表示“或”逻辑,即两条规则的并集。

node_modules

node_modules 是 Node.js 项目的一个关键目录,用于存储项目中的所有依赖项和子依赖项。

在 Node.js 项目中,node_modules 是一个标准目录,Node.js 在运行时会自动查找和加载这个目录中的模块

一般情况下,不建议将nodu_modules放入版本控制(在.gitignore中声明),只需要管理这个package.json文件,确保版本控制系统只存储代码和依赖关系定义,然后通过npm install,就会自动下载相关依赖。

因为node_modules这个目录可能非常大,而且可以根据环境和安装时的依赖关系不同而有所变化。

对于每个项目而言,执行npm install 都会为该项目下载全部依赖,并放在node_modules中,这导致初始化项目工程的时候可能会等待挺长一段时间。

虽然NPM有一个全局缓存机制,可以减少重新安装时的下载量,但在某些情况下,缓存可能会导致问题。

为了解决npm的问题,社区还出现了诸如yarnpnpm等包管理工具,文章最后再简单介绍一下。

安装流程

参考:

有了npm之后,我们只需要通过npm install xxx就可以从npm仓库里面下载别人写好的包,然后在代码里面引入相关的模块进行开发。

安装流程如下

  • npm 模块安装机制:
  • 发出npm install命令
  • 查询node_modules目录之中是否已经存在指定模块
    • 若存在,不再重新安装
    • 若不存在
      • npm 向 registry 查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

具体阶段可分为下面几个步骤

  • 如果工程定义了preinstall钩子,则先执行
  • 根据package.json中的dependenciesdevDependencies 属性中直接指定的模块确定首层依赖,构建一颗以工程本身为根节点的依赖树
  • 分析完模块依赖,就开始获取模块,获取模块是一个递归的过程
    • 获取模块信息,在下载一个模块之前,首先要确定需要下载的包semver版本,然后返回对应的压缩包地址
    • 获取模块实体,上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地node_modules是否已经安装该包,如果没有则从仓库下载,下载的文件会放在node_modules
    • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。

这里需要理解模块扁平化(dedupe)的概念。一棵完整的依赖树,其中可能包含大量重复模块,重复模块指的是模块名相同且semver兼容。已重复的模块不需要重新安装,这可以使更多冗余模块在 dedupe 过程中被去掉。

  • npm2中,node_modules采用简单的递归安装方法,不同的依赖包里面可能包含重复的底层包依赖
  • npm3中,node_modules 目录改成了更加扁平状的层级结构,得益于 node 的模块加载机制,模块可以在上层的node_modules目录中成功加载依赖,从而实现模块扁平化
  • npm5中,新增了package-lock.json文件,其作用是锁定依赖安装结构,内部与node_modules 目录的文件层级结构是一一对应的

优化install速度

选择安装源

由于npm服务器在国外,国内使用npm安装比较慢,一般会选择从大厂的镜像仓库比如淘宝镜像安装

之前的淘宝镜像是http://npm.taobao.org/ ,该域名已经停止服务了,现在最新的是https://registry.npmmirror.com 还有些教程推荐使用cnpm替代npm进行安装,这可能会导致出现一些奇奇怪怪的依赖问题,我跟人不建议使用

如果是临时修改镜像源,可以通过--registry参数

bash
npm --registry https://registry.npm.taobao.org install

可以通过命令npm config永久修改设置安装源

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

或者通过在命令行内搭梯子之后再安装,需要一点科学上网的知识,这里不再展开

bash
export https_proxy=http://127.0.0.1:7897 http_proxy=http://127.0.0.1:7897 all_proxy=socks5://127.0.0.1:7897

# 然后执行
npm i

切换镜像

日常工作可能需要从npm官方源、淘宝镜像、公司私有源等仓库来回切换,因此推荐nrm工具管理npm源

bash
# 列举目前配置的源列表
nrm ls 
# 使用淘宝源
nrm use taobao
# 增加一种源,然后就可以在ls 和 use中使用了
nrm add xxxxx

直接从缓存安装

参考:

一个模块安装以后,本地其实保存了两份。一份是~/.npm目录下的压缩包,另一份是node_modules目录下解压后的代码。

运行npm install的时候,只会检查node_modules目录,而不会检查~/.npm目录。

也就是说,如果一个模块在~/.npm下有压缩包,但是没有安装在node_modules目录中,npm 依然会从远程仓库下载一次新的压缩包

这种行为固然可以保证总是取得最新的代码,但有时并不是我们想要的。

最大的问题是,它会极大地影响安装速度。因此可以通过--cache-min从本地的缓存目录中直接解压包文件。

安全问题

这里整理了npm安装依赖时可能存在的一些安全问题,这里指的安全问题是依赖缺失或者依赖版本不兼容导致的代码错误。

package-lock

默认使用-S-D安装时会自动添加^修饰符,在不同的时候,执行安装命令就可能会安装不同的版本。

package-lock.json是当 node_modulespackage.json发生变化时自动生成的文件。这个文件主要功能是确定当前安装的包的依赖,以便后续重新安装的时候生成相同的依赖,而忽略项目开发过程中有些依赖已经发生的更新。

Lock机制是为了保证多人开发的统一性。什么是统一性?就是无论何时来了一个新人、换了个新电脑,我们npm i的包都是一致的,不管在那一台机器上执行 npm install 都会得到完全相同的 node_modules 结果。

随着项目越来越大,依赖越来越多,很难保证每一个npm包的最新版本都是适合的、有用的。Lock机制可以最大化解决此类冲突。在多人协作时同步开发环境。至于什么时候用新的包,到时候再同步lock文件就是。

基于以上原因,建议将package-lock文件锁定安装时的包的版本号,并且上传到git,以保证其他人在npm install时大家的依赖能保证一致。

影子依赖

由于NodeJS会在运行时会自动查找和加载node_modules中的模块,而node_modules中又保存了整个项目的依赖,就可能存在下面这种情况。

  • package.json 中声明了包 A,而包 A 依赖于包 B
  • 执行 npm install 时,Node.js 的包管理器会自动下载并安装 A 以及 A 的所有子依赖项(包括 B)。
  • 在这种情况下,虽然 B 没有直接在 package.json 中声明,但由于 A 的依赖,B 也会被安装到 node_modules 中,因此在代码中使用 require('B') 也会正常工作。

在这种情况下,B就被称作影子依赖(Shadow Dependencies)。

由于B没有在package中声明,如果A的依赖发生了变化(升级或者移除了B),导致require(B)这里就会报错,因为这个时候执行npm i 不会再安装B了。

另外一种可能导致影子依赖的情况是引入了未在package.json中声明的全局模块,在更换了环境之后由于新环境没有这个全局模块,代码也可能会报错。

因此所有代码中使用的直接依赖,都需要在package.json中进行显式声明,尤其是运行时的依赖。

如何升级依赖

现在的项目基本上都不会从0开始搭建,一个正常的前端项目中,或多或少都有一些外部依赖,有时候需要升级这些依赖。下面是升级仓库的注意事项。

根据前面提到的语义化版本,选择合适的目标版本是很重要的,这决定了升级的是一个bugfix、一个feature还是一个break change,因为外部包的代码都是不可控的,升级之后项目中可能会导致构建失败、甚至是运行时线上错误。

以升级element-ui为例,类似于1.x2.x之类的大版本更新,相关的接口改动可能会比较多,因此动手之前记得先查看官方的更新日志,比如releases,这样对于可能产生冲突的地方会有大致的了解。

由于node_modules是不会进行git版本管理的,所有分支用的都是同一个node_modules文件夹,在不同的分支频繁安装或某个依赖,都涉及到还原的问题。

因此,针对项目依赖库的升级,更明智的选择应当是:克隆一个仓库然后独立进行升级,当升级完成之后再merge代码,在原始工作目录执行命令更新依赖。

如何发布自己的npm包

编写本地模块

参考

首先,新建一个txm文件夹,管理我们的整个模块文件,

  • index.js,用来存放模块的主要逻辑,注意按照CommonJS规范来书写模块,即每个模块使用exports暴露接口
  • package.json,模块的配置,比如名称,版本和相关依赖等等
  • README.md,模块的介绍和使用等

然后再txm文件夹中使用npm pack将整个文件夹打包,会显示生成txm-0.0.1.tgz(这个版本号是在package.json中定义)。

然后返回上一层,使用npm install txm/txm-0.0.1.tgz将txm模块包进行安装。
这里需要注意不能再txm文件夹中直接使用npm install txm-0.0.1.tgz,会出现Refusing to install xm as a dependency of itself的错误信息

可以在整个项目文件夹的node_modules文件夹中发现我们的模块包了。 最后在项目的文件比如main.js中就可以直接使用let txm = require('txm')来加载我们的模块了。

发布及注意事项

编写好本地包之后,如果需要发布到npm仓库上供其他用户使用,按照以下步骤进行

  • 将镜像切换回https://registry.npmjs.org/,其他源如淘宝npm会定期从npm官网上同步包,因此只需要发布在npm上即可
  • npm addusernpm login登录需要发布模块的账号
  • 注意包名和版本号,是否已经存在了

目前npm的包名为了防止“误植”攻击,会自动检测相近的包名,参考https://www.w3cvip.org/topics/393

解决办法是加命名空间,然后修改发布权限

"name": "@shymean/koa-mock",
  
npm publish --access=public

具体实践可以参考之前写的一篇文章:

搭建本地npm服务器

有些包可能不方便发布到公共的npm仓库,因此就有了搭建私有npm服务器的需求

在之前可以使用sinopia来搭建npm私有仓库,但sinopia已经年久失修了,目前一般使用verdaccio

bash
# 全局安装
npm i verdaccio -g 

# 启动服务
verdaccio

# 如果希望开启守护经常,可以使用pm2 
pm2 start verdaccio

http://localhost:4873

可以修改vs ~/.config/verdaccio/config.yaml的相关配置,比如当找不到包时如果希望去其他镜像查找,则修改uplinks参数

yml
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    # 可以修改为淘宝镜像 url: https://registry.npm.taobao.org/

如果是公司级别的npm私库,可以考虑使用Docker安装verdaccio镜像,此外各大云服务提供商业提供了企业级别的私有包托管仓库,直接购买购买云服务应该也可以

其他Node包管理工具

yarn

Yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具,主要目的是弥补npm的一些设计缺陷。

在当时还是npm4.x的时代背景下,npm存在的一些缺陷

  • npm install的时候非常慢,要安装的依赖太多了,并且是按照队列顺序安装每个依赖
  • 同一个项目,安装的时候无法保持一致性,没错,当时还没有package-lock.json

yarn具备的优点

  • 安装速度快,主要靠:并行下载、缓存离线安装
  • yarn.lock记录每个被安装的依赖的版本

npm5.x之后做出了一些改动

  • 增加了package-lock.json
  • 文件依赖优化,通过symlinks依赖本地模块(之前是拷贝文件到node_modules)

yarn切换镜像的话,可以使用yrm,跟上面的nrm基本一致。

cnpm

不要用,不如使用npm然后改个taobao镜像。

pnpm

NPM与Yarn都是将项目依赖的包安装到项目目录node_modules下,PNPM采用了一种不同的方法,它在您的计算机上创建一个单一的虚拟存储库,并使用硬链接将其链接到各个项目目录,而不是将文件复制到每个项目目录中。

这样可以实现多个项目共享相同的包版本而无需重复复制。PNPM还支持多个注册表、私有包和基于锁定文件的确定性构建,通过在项目之间使用共享存储来减少磁盘空间占用和加快安装速度

目前使用PNPM可以在Monorepo中更高效地管理依赖关系。

PNPM的特点之一是使用虚拟存储库和硬链接来共享依赖项,从而减少磁盘空间占用和安装时间。

这对于Monorepo来说非常有用,因为不同项目可以共享相同的依赖项,而不必在每个项目中都复制一份,这样可以减少冗余,并提供更快的安装和构建速度。

小结

本文介绍了关于npm的一些基础概念,以及使用npm安装和升级依赖的流程,最后还介绍了如何发布自己的npm包。

其中还有很多细节没有涉及,比如

  • package.json中的其他字段
  • npm run build工程构建问题,是在本地还是CI上面构建等
  • 开发自己的npm包时的常见问题如本地调试、monorepo等

后面有时间单独在梳理。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。