crossenv 介绍
💡 跨平台运行脚本,支持 设置 和 使用 环境变量
解决什么问题
大多数情况下,当我们在 package.json
的 scripts
中使用类似于 NODE_ENV=production
的命令设置环境变量的时候,往往会提示报错。因为 Windows 和其他 Unix 系统 bash 的命令是不一样的。其设置环境变量的命令也不同:
- Windows:
"SET NODE_ENV=production && webpack"
- Unix:
"EXPORT NODE_ENV=production && webpack"
同样,在 Windows 和 Unix 命令行中使用环境变量的方式也有所不同:
- Windows:
%ENV_VAR%
- Unix 上:
$ENV_VAR
因此,我们可以使用 cross-env
将命令兼容于 Windows 和 Unix 。这样就可以用 Unix 方式设置环境变量,同时兼容 Windows 系统。即用一行命令,再在不同端执行:"cross-env NODE_ENV=production && webpack $NODE_ENV"
crossenv 源码分析
在深入源码前让我们先了解一下 npm scripts 原理
npm scripts
我们通常会在 package.json
的 scripts
中写下类似的命令:
{
"scripts": {
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
}
}
我们通过 cross-env
设置环境变量 NODE_ENV=production
,同时调用 cross-spawn
执行 webpack --config build/webpack.config.js
。那当我们执行 npm run build
时,背后究竟发生了什么呢?
npm 脚本的原理非常简单。每当执行 npm run
,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。
比较特别的是,npm run
新建的这个 Shell,会将当前目录的 node_modules/.bin
子目录加入 PATH
变量,执行结束后,再将 PATH
变量恢复原样。
这意味着,当前目录的 node_modules/.bin
子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。比如,当前项目的依赖里面有 cross-env
,只要直接写 cross-env ***
就可以了。
我们可以在依赖 cross-env
的项目中找到 ./node_modules/.bin/cross-env
文件,其内容如下:
#!/usr/bin/env node
const crossEnv = require('..')
crossEnv(process.argv.slice(2))
由于 npm 脚本的唯一要求就是可以在 Shell 执行,因此它不一定是 Node 脚本,任何可执行文件都可以写在里面。所以第一行 #!/usr/bin/env node
的作用是指定这个脚本的解释程序是 node。
紧接着我们看到 crossEnv
函数的引入,并且把 process.argv
传入 crossEnv
进行调用,在上面的 build
示例中,crossEnv
的入参为 ['NODE_ENV=production', 'webpack', '--config', 'build/webpack.config.js']
。
crossEnv
逻辑
function crossEnv(args, options = {}) {
// 1
const [envSetters, command, commandArgs] = parseCommand(args)
// 2
const env = getEnvVars(envSetters)
if (command) {
// 3
const proc = spawn(
commandConvert(command, env, true),
commandArgs.map(arg => commandConvert(arg, env)),
{
stdio: 'inherit',
shell: options.shell,
env,
},
)
// 4
process.on('SIGTERM', () => proc.kill('SIGTERM'))
process.on('SIGINT', () => proc.kill('SIGINT'))
process.on('SIGBREAK', () => proc.kill('SIGBREAK'))
process.on('SIGHUP', () => proc.kill('SIGHUP'))
proc.on('exit', (code, signal) => {
let crossEnvExitCode = code
if (crossEnvExitCode === null) {
crossEnvExitCode = signal === 'SIGINT' ? 0 : 1
}
process.exit(crossEnvExitCode)
})
return proc
}
}
crossEnv
函数分四步:
-
将入参
args
分为[envSetters, command, commandArgs]
三部分。接着上述例子:args = ['NODE_ENV=production', 'webpack', '--config', 'build/webpack.config.js'] envSetters = { NODE_ENV: 'production' } command = 'webpack' commandArgs = ['--config', 'build/webpack.config.js']
-
处理
envSetters
并将其与process.env
融合生成env
-
如果
command
存在,则利用spawn
开启子进程,跑command
命令,并附带上commandArgs
,同时将子线程的process.env
设置成env
。 -
监听进程的信号事件,并将其传递给子/父进程。
接下来让我们来仔细看每一步是如何实现的。
parseCommand
逻辑
const envSetterRegex = /(\w+)=('(.*)'|"(.*)"|(.*))/
function parseCommand(args) {
const envSetters = {}
let command = null
let commandArgs = []
for (let i = 0; i < args.length; i++) {
const match = envSetterRegex.exec(args[i])
if (match) {
let value
if (typeof match[3] !== 'undefined') {
value = match[3]
} else if (typeof match[4] === 'undefined') {
value = match[5]
} else {
value = match[4]
}
envSetters[match[1]] = value
} else {
let cStart = []
cStart = args
.slice(i)
.map(a => {
const re = /\\\\|(\\)?'|([\\])(?=[$"\\])/g
return a.replace(re, m => {
if (m === '\\\\') return '\\'
if (m === "\\'") return "'"
return ''
})
})
command = cStart[0]
commandArgs = cStart.slice(1)
break
}
}
return [envSetters, command, commandArgs]
}
可以看到 parseCommand
逻辑为遍历 args
数组,用正则表达式 /(\w+)=('(.*)'|"(.*)"|(.*))/
来提取环境变量,如(NODE_ENV=production
),其返回数组 match
,表达式第 i
个括号包围表达式所匹配的字符串储存在 match[i]
中,match[0]
为整个表达式所匹配的字符串。所以 match[3]
的作用是删掉变量两边的 ''
,match[4
] 同理。match[2]
未被使用,笔者觉得这应该是个冗余。
遍历 args
直到表达式未匹配成功,这时默认接下来第一个参数是 command
,往后的参数都是 commandArgs
,并且用正则表达式 /\\\\|(\\)?'|([\\])(?=[$"\\])/g
对它们进行处理。?=
为向前断言,感兴趣的读者可以看其具体介绍。
getEnvVars
函数
getEnvVars
也主要是利用正则表达式来处理输入,具体细节读者可以自行看源码,这里不再赘述
spawn
函数
spawn
开启一个子进程,并在子进程里执行我们所期望的操作。
commandConvert
函数
const isWindows = () =>
process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
function commandConvert(command, env, normalize = false) {
if (!isWindows()) {
return command
}
const envUnixRegex = /\$(\w+)|\${(\w+)}/g // $my_var or ${my_var}
const convertedCmd = command.replace(envUnixRegex, (match, $1, $2) => {
const varName = $1 || $2
return env[varName] ? `%${varName}%` : ''
})
return normalize === true ? path.normalize(convertedCmd) : convertedCmd
}
commandCovert
将环境变量的使用转换成兼容当前操作系统的形式。判断是否为 Windows 系统主要依据 process.platform
和 process.env.OSTYPE
的值。如果当前操作系统不是 Windows 则直接返回 command
,否则进行处理。主要是将 $my_var
或 ${my_var}
变成 %my_var%
。
总结
cross-env
之所以能跨平台运行脚本同时支持设置和使用环境变量,是因为在跑实际脚本前加了一层,在这层内处理环境变量的兼容性。建议读者自己动手翻翻源码。可以对 regexp
的实际使用有更深入的了解,同时也能收获 npm scripts 和 node 子进程的知识。