我的 child_precess exec 怎么坏了 ?

669 阅读4分钟

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, {
    // 子进程的工作目录
    cwdbuildDir('')
  })
}

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.clientConfigthis.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 {Objectconfig
 * @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({ modulesfalse }))
    })
  })
}

看了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',
    },
  })

总结

以上就是这次分享的踩坑经历。

下面是我的一些解决问题的经验和思路,当遇到问题的时候:

  1. 不要慌, 静下心思考 🤔
  2. google 可以帮你解决80%+的问题
  3. 还没有思路或者答案,就根据错误栈信息,去源码里追踪(不要对源码有心里压力和恐惧感)
  4. 习惯性的 debug 去排查问题
  5. 找到答案后不要以为解决了就完事了,要思考是不是最优解,会不会给他人埋坑
  6. 复盘总结,下次解决问题如何能更快更好 🎉🎉🎉

Reference

[1] npm run-script: www.axihe.com/api/npm/cli…

[2] child_process:exec: nodejs.cn/api-v14/chi…