前端进阶 - exec/spawn/fork

3,300 阅读3分钟

借助execspawn,我们可以在node中运行执行控制台命令。

异步和同步执行

exec之于execSyncspawn之于spawnSync,是各自的异步与同步命令。

用多了就会觉得,异步执行的灵活性和可操作性,比同步执行要高很多。

同步执行

// execSync.js
const { execSync } = require('child_process')
const data = execSync('echo 123')
console.log('terminal:', data.toString('utf-8'))

执行

% node execSync.js
terminal: 123

execSync会把控制台执行输出一次性吐成Buffer返回,这样就意味着,同步执行不能收集过多的信息,否则有可能内存溢出。

spawnSysc的主要区别,在于其执行的每个指令,都必须用数组单独存储:

const { spawnSync } = require('child_process')
const data = spawnSync('ls', ['-l', '/tmp'])
console.log(data.stdout.toString('utf-8'))

注意这里返回的结构有所不同,相比execSync而言,如果要执行较大输出量的命令,spawnSync更为合适。 执行:

% node spawnSync.js
lrwxr-xr-x@ 1 root  wheel  11  1  1  2020 /tmp -> private/tmp

异步执行

execspawn异步执行会返回一个任务流。针对该流进行操作,可以接驳、串联各种操作。

// exec.js
const { exec, spawn } = require('child_process')
const task = exec(`curl -h`, { encoding: 'utf-8' })
task.stdout.on('data', chunk => {
    console.log(chunk)
})

上面的代码,通过data事件抓取任务流的输出,而后用主进程的console.log输出到屏幕,实际上就是将子任务与主进程的一种接驳方式。

更直接的,也可以直接接驳到process.stdout

spawn('ls', ['-l'], { stdio: [process.stdin, process.stdout, process.stderr, 'ipc'] })

这里的输入输出,直接桥接到主进程的io中,不必再使用data + cosnole.log监听。

fork

如果不需要带参数运行,可直接使用fork来运行文件。

// pulse.js
setInterval(() => {
    const now = Date.now()
    process.send ? process.send(now + ':send') : console.log(now + ':log')
}, 1000)

直接运行该文件,会每隔1秒打印一个时间戳。下面是fork调用代码:

// fork.js
const { fork } = require('child_process')
const task = fork('./pulse.js')
task.on('message', msg => {
    console.log('msg', msg)
})

注意:当fork一个文件时,子进程会与主进程建立ipc连接,子进程的process则会具备process.send方法。这个以后会用到。

fork对问答模式也可无缝对接。

// qa.js
const readline = require('readline')
const q = readline.createInterface(process.stdin, process.stdout)
q.question('Your name?\n', a => {
    console.log('Hello ' + a)
    q.close()
})
// fork.js
const { fork } = require('child_process')
fork('./qa.js')

使用IPC实现自动交互

刚才的qa.js文件,交互过程是:先提示输入用户名,等用户输入后,再打印Hello xxx

% node qa  
Your name?
dog
Hello dog

这种交互需要用户输入,有时比较繁琐,尤其是需要CI的时候,如果工具没有-y参数,集成不容易实现。

这时候我们可以通过创建一个子进程,在子进程与主进程之间建立ipc通道;主进程根据子进程的提示,自动写入相应的内容。

// auto.js
const { exec } = require('child_process')

const child = exec('node ./qa.js', {
    stdio: [null, null, null, 'ipc'], // 这里开启ipc
})
child.stdout.on('data', chunk => {

    const msg = chunk.toString('utf-8')
    console.log(msg.trim())
    
    if(msg.startsWith('Your')) {
        setTimeout(() => {
            child.stdin.write('dog2\n')   // 这里写入子进程交互内容,注意结尾要带个换行符
        }, 200)
    } else if(msg.startsWith('Hello')) {
        process.exit(0)
    }
})

看下运行效果

% node auto
Your name?
Hello dog2

实战自动初始化npm定制项目

// auto-init.js
const { exec } = require('child_process')

const child = exec('npm init', { stdio: [null, null, null, 'ipc'] })
child.stdout.on('data', chunk => {

    const msg = chunk.toString('utf-8')
    console.log(msg.trim())

    if(msg.startsWith('package name')) {
        const val = 'auto-init'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('version')) {
        const val = '1.2.3'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('description')) {
        const val = 'auto-init o~ init'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('entry point')) {
        const val = './dist/index.js'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('test command')) {
        const val = 'exit(1)'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('git repository')) {
        const val = 'git:xxx'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('keywords')) {
        const val = 'exec spawn fork node npm'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('author')) {
        const val = 'dog@dog.com'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('license')) {
        const val = 'MIT'
        console.log('>', val)
        child.stdin.write(val + '\n')
    }

    if(msg.startsWith('Is this OK')) {
        const val = ''
        console.log('>', val)
        child.stdin.write(val + '\n')
    }
})

控制台输出

% node auto-init.js
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (git)
> auto-init
version: (1.0.0)
> 1.2.3
description:
> auto-init o~ init
entry point: (1.js)
> ./dist/index.js
test command:
> exit(1)
git repository:
> git:xxx
keywords:
> exec spawn fork node npm
author:
> dog@dog.com
license: (ISC)
> MIT
About to write to /Users/dog/package.json:

{
  "name": "auto-init",
  "version": "1.2.3",
  "description": "auto-init o~ init",
  "main": "./dist/index.js",
  "scripts": {
    "test": "exit(1)"
  },
  "repository": {
    "type": "git",
    "url": "git:xxx"
  },
  "keywords": [
    "exec",
    "spawn",
    "fork",
    "node",
    "npm"
  ],
  "author": "dog@dog.com",
  "license": "MIT"
}
Is this OK? (yes)
> yes

生成的package.json

{
  "name": "auto-init",
  "version": "1.2.3",
  "description": "auto-init o~ init",
  "main": "./dist/index.js",
  "scripts": {
    "test": "exit(1)"
  },
  "repository": {
    "type": "git",
    "url": "git:xxx"
  },
  "keywords": [
    "exec",
    "spawn",
    "fork",
    "node",
    "npm"
  ],
  "author": "dog@dog.com",
  "license": "MIT"
}

以上。