介绍
前端工程化离不开npm
或者Yarn
这些管理工具。npm或Yarn 在工程项目中,除了负责依赖的安装和维护以外,还能通过 npm scripts
串联起各个职能部分,让独立的环节自动运转起来。另外在使用管理工具的时候会遇到一些问题如下:
项目依赖出现问题时,删除大法好,即删除 node_modules 和 lockfiles,再重新 install,这样操作是否存在风险
重新安装可能会改变依赖版本, 是有风险的
因为删除lock文件以后, 模块的安装顺序可能影响 node_modules 内的文件结构
insatll命令执行的时候,获取依赖包的顺序不一样,可能导致扁平化机制生成的依赖树不一样我们的应用依赖了公共库 A 和公共库 B,同时公共库 A 也依赖了公共库 B,那么公共库 B 会被多次安装或重复打包吗
看版本范围是否在一个范围内,同一个版本范围,不会重复。
一个项目中,既有人用 npm,也有人用 Yarn,这会引发什么问题
lock文件不同,可能会存在冲突,导致最终安装版本不一致。
我们是否应该提交 lockfiles 文件到项目仓库呢
如果开发一个应用,建议把 package-lock.json 文件提交到代码版本仓库。这样可以保证项目组成员、运维部署成员或者 CI 系统,在执行 npm install 后,能得到完全一致的依赖安装内容。
如果你的目标是开发一个给外部使用的库,那就要谨慎考虑了,因为库项目一般是被其他项目依赖的,在
不使用 package-lock.json 的情况下,就可以复用主项目已经加载过的包,减少依赖重复和体积因此,一个推荐的做法是:把 package-lock.json 一起提交到代码库中,不需要 ignore。但是执行 npm publish 命令,发布一个库的时候,它应该被忽略而不是直接发布出去
为什么单一的 package.json 不能确定唯一的依赖树
不同版本的 npm 的安装依赖策略和算法不同;
npm install 将根据 package.json 中的 semver-range version 更新依赖,某些依赖项自上次安装以来,可能已发布了新版本。
为什么有时候可以离线安装一些包
因为包管理工具的缓存机制
另外很多时候我们在配置package.json中除了scripts
属性的配置,其他的配置也会有些陌生,npm和yarn的一些操作也不太懂.
关于package.json
1 | { |
npm
npm的安装机制
注意:
1 | 1. 这里的config配置(npm配置)是指`.npmrc文件` |
npm缓存机制
对于一个依赖包的同一版本进行本地化缓存,是当代依赖包管理工具的一个常见设计。
通过npm config get cache
可以获得npm缓存配置的路径, 比如:/Users/chen/.npm
切换到缓存配置的路径以后,可以看到一个_cacache文件夹
。该文件夹存放依赖包的缓存。
当然你可以通过 npm cache clean --force
清空缓存。打开_cacache文件夹
1 | # cache文件夹下的目录文件: |
Npm v5+ 缓存策略如下:
当 npm install 执行时,通过pacote把相应的包解压在对应的 node_modules 下面。npm 在下载依赖时,先下载到缓存当中,再解压到项目 node_modules 下。pacote 依赖npm-registry-fetch来下载包,npm-registry-fetch 可以通过设置 cache 属性,在给定的路径下根据IETF RFC 7234生成缓存数据。
接着,在每次安装资源时,根据 package-lock.json 中存储的 integrity、version、name 信息生成一个唯一的 key,这个 key 能够对应到 index-v5 目录下的缓存记录。如果发现有缓存资源,就会找到 tar 包的 hash,根据 hash 再去找缓存的 tar 包,并再次通过pacote把对应的二进制文件解压到相应的项目 node_modules 下面,省去了网络下载资源的开销。
npm v5 版本之前:
每个缓存的模块在 ~/.npm 文件夹中以模块名的形式直接存储,储存结构是:{cache}/{name}/{version}。
npm常用命令
1 | // 调用 shell 脚本输出一个初始化的 package.json 文件 |
关于
npm link
这个命令:在开发公共包的时候,比如我开发了一个组件库, 现在组件库内新增了一个组件
,但我不能发布新版本,因为不确定这个组件的正确性。那我如何确定这个新增的组件能够在我的业务项目中使用呢?使用教程的代码参考
- 比较笨的办法就是打包现在的组件库,然后把打包的组件库放入node_modules里面,然后在验证
- 正确的做法是通过npm link. (假设我们之前的组件库打包后叫做
npm-package-ui
)
- 首先我们进入我们的组件库,执行npm link(这样 npm link 通过链接目录和可执行文件,实现 npm 包命令的全局可执行)。
- 然后进入本地的业务项目,执行
npm link npm-package-ui
. 它就会去 /usr/local/lib/node_modules/ 这个路径下寻找是否有这个包,如果有就建立软链接.- 这样我本地的业务项目就可以和本地最新的组件库建立软链接了,然后启动程序,我就可以用新组件
了。不过每次npm-package-ui的更新,都需要本地业务项目重新
启动,才可以得到新的变化。- 最后测试结束以后,
npm unlink
取消关联,然后测试没问题就可以发布新版本的组件库包了。
npx的用法
- npx 可以自动去 node_modules/.bin 路径和环境变量 $PATH 里面检查命令是否存在,而不需要再在 package.json 中定义相关的 script。通过
npm install -g npx
命令来安装npx (需要npm V5.2+)
比如之前需要这样才能执行eslint
1 | "scripts": { |
使用npx以后:
1 | npx eslint --init |
- 另外除了调用项目内部模块,npx 还能避免全局安装的模块。比如,
create-react-app
这个模块是全局安装,npx 可以运行它,而且不进行全局安装。
1 | // npx 将create-react-app下载到一个临时目录,使用以后再删除。 |
- 切换node版本
1 | // nvm 的命令如下 |
使用nvm 管理 node版本
1 | // 全局安装nvm |
使用nrm切换npm 源
有时候我们可能觉得切换比较麻烦,老是要npm config set registry 某npm镜像url
. 这时候可以使用nrm
1 | // 全局安装nrm |
yarn
Yarn 是一个由 Facebook、Google、Exponent 和 Tilde 构建的新的 JavaScript 包管理器
下面是yarn包管理器的优点(个人倾向于使用npm):
确定性:通过 yarn.lock 等机制,保证了确定性。即不管安装顺序如何,相同的依赖关系在任何机器和环境下,都可以以相同的方式被安装。(在 npm v5 之前,没有 package-lock.json 机制,只有默认并不会使用的npm-shrinkwrap.json。)
__采用模块扁平安装模式__:将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余(npm 目前也有相同的优化)。Yarn 在安装依赖时会自动执行 dedupe 命令
__网络性能更好__:Yarn 采用了请求排队的理念,类似并发连接池,能够更好地利用网络资源;同时引入了更好的安装失败时的重试机制。
采用缓存机制: 实现了离线模式(npm 目前也有类似实现)。
关于yarn的锁文件yarn.lock
区别于package-lock.json, yarn.lock 并没有使用 JSON 格式,而是采用了一种自定义的标记格式
另外和npm lock文件相比, yarn.lock 中子依赖的版本号不是固定版本。这就说明单独一个 yarn.lock
确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。
不管是 npm 还是 Yarn,说到底它们都是一个包管理工具,在项目中如果想进行 npm/Yarn 切换,并不是一件麻烦的事情。甚至还有一个专门的 synp 工具,它可以将 yarn.lock 转换为 package-lock.json
yarn的安装机制
安装过程: 检测(checking)→ 解析包(Resolving Packages) → 获取包(Fetching Packages)→ 链接包(Linking Packages)→ 构建包(Building Packages)
检测包
这一步主要是检测项目中是否存在一些 npm 相关文件,比如 package-lock.json 等。如果有,会提示用户注意:这些文件的存在可能会导致冲突。在这一步骤中,也会检查系统 OS、CPU 等信息
解析包
这一步会解析依赖树中每一个包的版本信息:
首先获取当前项目中 package.json 定义的 dependencies、devDependencies、optionalDependencies 的内容,这属于首层依赖
接着采用遍历首层依赖的方式获取依赖包的版本信息,以及递归查找每个依赖下嵌套依赖的版本信息,并将解析过和正在解析的包用一个 Set 数据结构来存储,这样就能保证同一个版本范围内的包不会被重复解析
对于没有解析过的包 A,首次尝试从 yarn.lock 中获取到版本信息,并标记为已解析
如果在 yarn.lock 中没有找到包 A,则向 Registry 发起请求获取满足版本范围的已知最高版本的包信息,获取后将当前包标记为已解析
获取包
这一步我们首先需要检查缓存中是否存在当前的依赖包,同时将缓存中不存在的依赖包下载到缓存目录
如何判断缓存中是否存在当前的依赖包?Yarn 会根据 cacheFolder+slug+node_modules+pkg.name 生成一个 path,判断系统中是否存在该 path,如果存在证明已经有缓存,不用重新下载。这个 path 也就是依赖包缓存的具体路径
对于没有命中缓存的包,Yarn 会维护一个 fetch 队列,按照规则进行网络请求。如果下载包地址是一个 file 协议,或者是相对路径,就说明其指向一个本地目录,此时调用 Fetch From Local 从离线缓存中获取包;否则调用 Fetch From External 获取包。最终获取结果使用 fs.createWriteStream 写入到缓存目录下
链接包
将项目中的依赖复制到项目 node_modules 下,同时遵循扁平化原则。在复制依赖前,Yarn 会先解析 peerDependencies (同伴依赖,它用来告知宿主环境需要什么依赖以及依赖的版本范围 ),如果找不到符合 peerDependencies 的包,则进行 warning 提示,并最终拷贝依赖到项目中
构建包
如果依赖包中存在二进制包需要进行编译,会在这一步进行
yarn的常用命令
1 | // 安装所有依赖包 |