初识npm
npm全称为Node Package Manager
,是一个基于Node.js
的包管理器,用于查找、安装、管理和发布 Node.js
应用程序所需的各种包和模块,避免 JS 开发者重复造轮子,让大家的劳动成果可以共享。
npm
是前端工程化的必须要掌握的基础知识,本文将整理通过npm安装依赖的一些细节。
参考
- 2018 年了,你还是只会 npm install 吗,这篇文章讲的很全,不妨移步阅读
相关概念
模块与包
参考:
模块Module
是任何的能被Nodejs
程序使用require加载的模块。满足以下条件均可以称为Module:
- 一个文件夹包含package.json文件并指定了main字段
- 一个文件夹包含index.js文件
- 一个javascript文件
JavaScript模块规范有CommonJS
、ES6 module
、AMD
等多种形式,已整理至博客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
pacakge.json中还有一个scripts
配置项,用来声明当前工程常用脚本。
常见的开发环境,都有类似的一个或多个npm scripts
命令,如npm run build
、npm run dev
等
每当执行npm run
,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。
由于开发环境依赖的一些包可能自己也提供了可执行命令,比如hexo
就提供了hexo draft
、hexo publish
等命令,除了全局安装之后,也可以通过npx
直接运行node_modules
中某个包提供的命令。
局部安装的包,如果提供了二进制命令,将会放在node_modules/.bin
目录下, npx 会帮你执行依赖包里的二进制文件。
//非全局安装
npm i webpack -D
// 无npx 执行 webpack 的命令
./node_modules/.bin/webpack -v
// 使用npx
npx webpack -v
安装依赖
开发依赖与运行依赖
关于包依赖有devDependencies
和dependencies
两个字段,他们的区别在于:
- 前者是开发的时候需要的依赖项,比如
webpack
等,使用npm i xxx -D
安装,对应的依赖包会放在devDependencies
中 - 后者是程序正常运行需要的包,比如
vue
,jQuery
等,使用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的问题,社区还出现了诸如yarn
、pnpm
等包管理工具,文章最后再简单介绍一下。
安装流程
参考:
有了npm之后,我们只需要通过npm install xxx
就可以从npm仓库里面下载别人写好的包,然后在代码里面引入相关的模块进行开发。
安装流程如下
- npm 模块安装机制:
- 发出npm install命令
- 查询node_modules目录之中是否已经存在指定模块
- 若存在,不再重新安装
- 若不存在
- npm 向 registry 查询模块压缩包的网址
- 下载压缩包,存放在根目录下的.npm目录里
- 解压压缩包到当前项目的node_modules目录
具体阶段可分为下面几个步骤
- 如果工程定义了
preinstall
钩子,则先执行 - 根据
package.json
中的dependencies
和devDependencies
属性中直接指定的模块确定首层依赖,构建一颗以工程本身为根节点的依赖树 - 分析完模块依赖,就开始获取模块,获取模块是一个递归的过程
- 获取模块信息,在下载一个模块之前,首先要确定需要下载的包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
参数
npm --registry https://registry.npm.taobao.org install
可以通过命令npm config
永久修改设置安装源
npm config set registry https://registry.npmmirror.com
或者通过在命令行内搭梯子之后再安装,需要一点科学上网的知识,这里不再展开
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源
# 列举目前配置的源列表
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_modules
或package.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.x
到2.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 adduser
、npm login
登录需要发布模块的账号- 注意包名和版本号,是否已经存在了
目前npm的包名为了防止“误植”攻击,会自动检测相近的包名,参考https://www.w3cvip.org/topics/393。
解决办法是加命名空间,然后修改发布权限
"name": "@shymean/koa-mock",
npm publish --access=public
具体实践可以参考之前写的一篇文章:
搭建本地npm服务器
有些包可能不方便发布到公共的npm仓库,因此就有了搭建私有npm服务器的需求
在之前可以使用sinopia来搭建npm私有仓库,但sinopia已经年久失修了,目前一般使用verdaccio
# 全局安装
npm i verdaccio -g
# 启动服务
verdaccio
# 如果希望开启守护经常,可以使用pm2
pm2 start verdaccio
http://localhost:4873
可以修改vs ~/.config/verdaccio/config.yaml
的相关配置,比如当找不到包时如果希望去其他镜像查找,则修改uplinks
参数
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等
后面有时间单独在梳理。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。