深入理解npm scripts

故事要从改造公司项目脚手架说起,去年(2022年)我在部门做了vite技术分享,会后与前端基建同学聊了一下,打算将公司脚手架的构建工具由webpack升级成vite,提升开发体验,生产环境依然采用webpack。迎来的第一个问题就是如何解决node版本隔离问题?公司目前大多数项目node版本都是8.9.4,而vite要求node版本最小是12.0.0,当然开发同学可以本地自行安装高版本,但是存在以下弊端:

  • 操作繁琐,开发需要手动安装
  • 污染全局,可能导致其他项目无法运行
  • 不够优雅,不能无缝切换
  • 心智负担,频繁切换node版本

其实这个问题已经有解决方案了,并且公司一直在使用这个方案,就是使用volta(点我了解一下)来做到项目级别的node版本控制。但是目前的问题是:volta已经用于生产环境打包,没法在一个项目里边设置多个node版本(当然手动切换版本也可以,但比较繁琐)。如果我们直接修改volta的配置,虽然可以满足运行vite版本要求 但也导致生产环境node版本被改了,结果具有不可预测性,显然不是一个好的做法。

 

那我们能否从volta、nvm、n等node版本管理解决方案上得到一些启发呢?比如在运行npm scripts时候指定node的版本,如果不存在就从网络下载zip包,解压得到可执行文件。运行指定路径的node可执行文件。实际以上这些工具就是这么干的,实现思路大同小异。

 

在经过一番调研后,发现其实社区已经有解决方案了,这里列举2个npm包, 这两个包都可以做到项目级别的node版本控制,使用方式和工作原理也很类似,但是他们有一个很大的区别:

nodeinstall   同时安装node与npm,可指定node下载镜像与执行路径

node 只安装node,不提供参数配置

 

在经过一番demo测试后,发现确实可以做到项目隔离,不会影响全局node。 看文档得知以下2点重要结论:

  • 下载npm包(包含了node二进制可执行文件)到本地
  • npm scripts会优先去本地node_modules中的.bin目录寻找环境变量

 

 首先验证一下结论1,查看一下node_moduls/.bin文件夹里都有啥,发现眼熟的node其实是一个软连接!其真实路径为node_modules/node/bin/node

 

细心的同学可能发现了,.bin目录下的文件全是软连接

 

 

接着验证结论2,分别在全局执行node -v和使用npm scripts执行node -v,结果如下:

 

可以看到,两者的node版本不一样!

 

经过一番验证,确实如文档描述一样。那么有意思的事情就来了,这里要提出几个问题:

  • 为什么终端可以运行js脚本
  • node_modules下的.bin文件夹是干什么用的
  • 为什么运行npm scripts会优先使用本地的node

带着疑问,我们一步步揭开npm的神秘面纱。

当我们输入npm run xxx,敲下回车的时候,都发生了什么?

首先我们知道一个命令想要执行,至少先要找到命令的位置,其次还得是可执行文件/脚本,那我们就顺藤摸瓜,看看npm命令的执行流程就能找到最终答案。 

 

 

 

找到npm本尊以后,这里我通过vscode断点调试,整理其核心实现如下:

将项目node_modules/.bin 与npm全局安装位置下的node-gyp-bin添加到PATH环境变量最前端(注意追加顺序,node-gyp-bin在node_modules/.bin之前),为什么要追加到最前面?这里就要涉及到一点操作系统的知识了。

简单来说,PATH最重要的功能就是为系统查找命令/变量提供一个规则。当我们在终端输入一个指令的时候,系统就会去PATH中去查找,而查找的优先级就是从左到右。

 

 

 

 

 

设置好环境变量以后,就开始运行npm script了,核心实现可以看到是利用node子进程去执行命令 

 

 

 

通过源码分析得出结论:

  • npm会在运行之前为追加PATH路径
  • 然后使用node子进程执行命令

再来验证一下,是否符合预期,查看系统PATH环境变量

 

运行npm scripts时候,输出系统PATH环境变量

 

可以看到多出来两个环境变量,就是前边提到的最前端追加变量。到这一步已经可以解释为什么我们运行node命令的时候会优先从项目node_modules/.bin目录中去查找,以及.bin目录是干什么用的

 

那再来说说,为什么终端能运行js代码?这里以vite为例,可以看到vite的真实路径是 node_modules/vite/bin/vite.js 

 

打开这个文件,可以看到引入眼帘的第一句就是一行类似‘注释’的代码,不要小看这一句’注释‘,他包含了两个部分:

  • #! 它叫shebang,起到标识的作用,说明这个文件可以当做脚本来运行
  • /usr/bin/env就是告诉系统可以在PATH目录中查找

所以配置#!/usr/bin/env node, 就是告诉系统去PATH环境中查找命令,使用node解释执行。同时也解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件,这也解释了为什么我们在写cli的时候需要在第一行加入这段`注释`

  

 

最后顺便谈谈 npm hooks,写了如下demo

 

 可以看到不仅是内置install、publish有钩子,其实可以自定义npm hook,只要满足命名规则就可以。

 npm hooks处理核心代码,最关键的一句。如果当前scripts不是以pre或者post开头的,则在当前命令前后追加pre和post命令,然后顺序执行

 

以上疑问都已解答,看到这里相信你对npm的执行流程应该有一个清晰的认识了。

那最终我是如何实现node版本隔离的呢?由于`node`这个npm包无法指定node安装路径,这会导致运行所有npm scripts都会默认去.bin目录寻找node,在部署生产环境的时候不希望使用本地node。

前面说过nodeinstall可以指定execPath参数,利用这个就可以指定一个node安装路径,这样就不会影响到项目其他npm脚本执行所用的node版本。有了node可执行文件,接下来就是解决在运行npm scripts的时候如何指定我们安装的本地node,这里使用node子进程的fork方法就可以实现,其可以指定execPath(既node可执行文件位置),这样就达到了最终效果。

 2023第一篇文章,加油!

posted @ 2023-02-13 16:48  zt123123  阅读(207)  评论(0编辑  收藏  举报