child_precess exec 坏了 ?
事故如下:
先陈述一下背景: 在开发(特别提醒后面有呼应)
egg.js 项目的时候在 service 层,需要开子进程执行脚本操作, 用于触发 vuepress 的文档处理和构建。
项目大概目录如下:
简略代码如下:
// app/service/doc.js
const Service = require('egg').Service
const buildDir = p => path.join(__dirname, '../../vuepress', p)
const buildFilePath = buildDir('/build.js')
const path = require('path')
const { promisify } = require('util')
const exec = promisify(require('child_process').exec)
class DocService extends Service {
async get() {
// 省略若干代码 ...
const {
stdout,
stderr
} = await triggerBuild()
// 省略若干代码 ...
}
}
function triggerBuild() {
const shell = `node ${buildFilePath}`
return exec(shell, {
// 子进程的工作目录
cwd: buildDir('')
})
}
module.exports = DocService
// vuepress/build.js
const { promisify } = require('util')
const exec = promisify(require('child_process').exec)
async start() {
// 省略若干代码...
const {
stdout,
stderr
} = awiat handleBuildDoc()
// 省略若干代码...
}
function handleBuildDoc() {
// 省略若干代码...
const shell = 'npm run build'
return exec(shell, {
cwd: __dirname,
})
}
「当然了可能会有其他疑问,为什么分两个进程执行这一流程,一个完全够用了,因为需要分别做不同的事情,为保证开闭原则就对其进行了分离,中间产物.temp为桥梁。」
回到正题, 上面的代码第一眼看写的没啥毛病吧?
但是,运行起来就抛出了一个 Warn
❝
npm WARN lifecycle The node binary used for scripts is /var/folders/j5/lzphf_yn4154ms5pn9c_9qh40000gn/T/yarn--1635578680391-0.7100593669876971/node but npm is using /usr/local/bin/node itself. Use the
--scripts-prepend-node-path
option to include the path for the node binary npm was executed with.❞
这个警告信息 提示的已经很明了了, npm run-script 时候路径出现了异常,需要加一个 --scripts-prepend-node-path
的option,这是 npm 运行的路径异常导致的警告,并不会影响执行结果。
「具体细节可以查看 npm run-script[1]」
把上面 shell 改成如下, warn 就不会出现了。
const shell = 'npm run build --scripts-prepend-node-path=auto'
其实如果只是 warn 也就算了,可能就直接忽视了,毕竟不影响正常流程。 「但是呢,后面等着的一个大大的 Error !!」
❝
Cannot find module '/Users/zhi/Desktop/zhuanzhuan/node_webview_backend/vuepress/docs/.vuepress/dist/manifest/client.json
❞
说实话这个玩意刚看到时候,立马看了一下 vuepress/docs/.vuepress/dist/manifest
目录, 懵逼了,怎么只有 vuepress/docs/.vuepress/dist/manifest/server.json
。 「我明明执行的就是 npm run build 可是怎么没有生产出 build 的中间产物呢???」
空气突然就安静了。我也陷入了沉思之中。
然后 看了一下错误栈信息,
'- /Users/zhi/Desktop/zhuanzhuan/node_webview_backend/vuepress/node_modules/@vuepress/core/lib/node/build/index.js\n' +\n" +
" '- /Users/zhi/Desktop/zhuanzhuan/node_webview_backend/vuepress/node_modules/@vuepress/core/lib/node/App.js\n' +\n" +
" '- /Users/zhi/Desktop/zhuanzhuan/node_webview_backend/vuepress/node_modules/@vuepress/core/lib/index.js\n' +\n" +
" '- /Users/zhi/Desktop/zhuanzhuan/node_webview_backend/vuepress/node_modules/vuepress/lib/registerCoreCommands.js\n' +\n" +
" '- /Users/zhi/Desktop/zhuanzhuan/node_webview_backend/vuepress/node_modules/vuepress/cli.js\n' +\n" +
在 node_modules/@vuepress/core/lib/node/build/index.js
中看到了 render 函数会用到 manifest/client.json ,
// node_modules/@vuepress/core/lib/node/build/index.js
/**
* Compile and render pages.
*
* @returns {Promise<void>}
* @api public
*/
async render () {
// compile!
const stats = await compile([this.clientConfig, this.serverConfig])
const serverBundle = require(path.resolve(this.outDir, 'manifest/server.json'))
const clientManifest = require(path.resolve(this.outDir, 'manifest/client.json'))
// remove manifests after loading them.
await fs.remove(path.resolve(this.outDir, 'manifest'))
// ...
}
// ...
/**
* Compile a webpack application and return stats json.
*
* @param {Object} config
* @returns {Promise<Object>}
*/
function compile (config) {
return new Promise((resolve, reject) => {
webpack(config, (err, stats) => {
if (err) {
return reject(err)
}
if (stats.hasErrors()) {
stats.toJson().errors.forEach(err => {
console.error(err)
})
reject(new Error(`Failed to compile with errors.`))
return
}
if (env.isDebug && stats.hasWarnings()) {
stats.toJson().warnings.forEach(warning => {
console.warn(warning)
})
}
resolve(stats.toJson({ modules: false }))
})
})
}
看了vuepress 的源码之后, 初步判断应该是 compile 的时候出现了异常,没有生产出来目标文件产物。 好像有点思路了, 然后就在 node_modules/@vuepress 目录下全局搜索了 client.json ,
可以看到, node_modules/@vuepress/core/lib/node/webpack/createClientConfig.js
文件里也用到了此文件,这应该就是该文件的产出出口的地方了,具体内容如下:
module.exports = function createClientConfig (ctx) {
const { env } = require('@vuepress/shared-utils')
const createBaseConfig = require('./createBaseConfig')
const safeParser = require('postcss-safe-parser')
const config = createBaseConfig(ctx)
//...
// generate client manifest only during build
if (process.env.NODE_ENV === 'production') {
// This is a temp build of vue-server-renderer/client-plugin.
console.log('调试信息:', '---- output manifest/client.json ----', config)
config
.plugin('ssr-client')
.use(require('./ClientPlugin'), [{
filename: 'manifest/client.json'
}])
}
//...
}
因此,在函数内部加了 log 信息,重新执行了一下, 居然发现,压根就没执行, 然后 打印了一下 process.env.NODE_ENV
,结果是 'development'。 我明明运行的是 npm run build , 为什么 NODE_ENV 是 development ??
。
那么问题来了,难道我的 child_precess 的 exec 出问题了?
显然不是的,因为 egg.js 运行在 dev 开发模式
下,eggjs 主进程环境变量 env.NODE_ENV 是开发模式是正常的。
然后想了想,在运行 exec('npm run build') 之前把 process.env.NODE_ENV = “development”
就完事了,然后回调结束后再 process.env.NODE_ENV = old_NODE_ENV
。 就完美解决了。 实践证明,确实如此,终于可以正常运行了。
其实上面这种 强制修改主进程的全局变量 显然是会有副作用的 很可能某一天会产生其他不为人知的问题
,会给他人和自己挖坑。 看了一下官方文档 nodejs.cn/api-v14/chi…] exce 的 options.env <Object> 环境变量键值对。 默认值: process.env。
原来 node 在启动进程时候是可以设置 改进程的 环境变量配置的, 因此 下面写法才是正道的光 :
exec(shell, {
cwd: __dirname,
env: {
...process.env,
NODE_ENV: 'production',
},
})
总结
以上就是这次分享的踩坑经历。
下面是我的一些解决问题的经验和思路,当遇到问题的时候:
- 不要慌, 静下心思考 🤔
- google 可以帮你解决80%+的问题
- 还没有思路或者答案,就根据错误栈信息,去源码里追踪(不要对源码有心里压力和恐惧感)
- 习惯性的 debug 去排查问题
- 找到答案后不要以为解决了就完事了,要思考是不是最优解,会不会给他人埋坑
- 复盘总结,下次解决问题如何能更快更好 🎉🎉🎉
Reference
[1] npm run-script: www.axihe.com/api/npm/cli…
[2] child_process:exec: nodejs.cn/api-v14/chi…