更新说明:对文章目录排版做了调整。
更新时间:2022-05-04
本Week代码提交支:lesson04
第一章:本周导学
1-1 本周整体内容介绍和学习方法
标题
基于Commander完成脚手架命令注册和命令执行过程开发
收获
如何设计高性能脚手架
Node多线程开发
javascript面向对象编程的实战技巧
内容
图解高性能脚手架架构设计方法
封装通用的Package和Command类
基于缓存 + Node 多进程 实现动态命令加载和执行
将业务逻辑和脚手架逻辑彻底解耦
加餐
Node多进程开发进阶–child_process源码解析
深入Node源码看清spawn/exec/execFile/fork的本质区别,彻底搞懂Node多进程原理。
第二章:imooc-cli脚手架命令注册
2-1 imooc-cli脚手架初始化+全局参数注册
(本节有代码编写)
本节的主要内容为使用commander这个库在全局添加注册命令
cd core/cli
npm i -S commander
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function registerCommand ( ){ program .name (Object .keys (pkg.bin )[0 ]) .usage ('<command> [options]' ) .version (pkg.version ) .option ('-d, --debug' , '是否开启调试模式' , false ); program.on ('option:debug' ,function ( ){ if (program.opts ().debug ){ process.env .LOG_LEVEL ='verbose' }else { process.env .LOG_LEVEL ='info' } log.level = process.env .LOG_LEVEL }) program.on ('command:*' ,function (obj ){ const availableCommands = program.commands .map (cmd => cmd.name ()) console .log (colors.red ('未知的命令:' +obj[0 ])) if (availableCommands.length > 0 ){ console .log (colors.red ('可用命令为:' +availableCommands.join (',' ))) } }) program.parse (program.argv ) if (program.args && program.args .length < 1 ) { program.outputHelp (); console .log () } }
2-2 imooc-cli脚手架命令注册
(本节有代码编写)
本节的主要内容为添加第一个comman操作:‘init ’,并在commands文件夹下创建新的init包
1 2 3 4 5 6 7 8 9 10 11 12 13 ...... const init = require ('@cloudscope-cli/init' )...... program .command ('init [projectName]' ) .option ('-f,--force' ,'是否强制更新项目' ) .action (init) ......
第三章:高性能脚手架架构设计和缓存结构设计
3-1 当前imooc-cli脚手架架构痛点分析
(本节无代码编写)
当前的代码架构如图:
3-2 高性能脚手架架构设计
(本节无代码编写)
对以上架构(之前代码编写)的主要优化点有以下三个方面
将init命令做成了一个动态加载的形式
动态加载的脚手架通过缓存形式进行存储:执行哪个命令下载哪个命令
动态加载的时候,通过node多进程进行执行:深挖cpu性能
3-3 脚手架命令动态加载功能架构设计
(本节无代码编写)
上图架构初看有些难度,在代码编写之后再去回顾,会有更深理解。
本节简单讲述了两点:
require加载文件的用法:
require(‘/xxx/yyy/index.js’) ---- 加载绝对路径
require(‘./index.js’)----加载相对路径
require(‘fs’) ---- 加载内置模块
require(‘npmlog’) ---- 加载第三方包
node执行模块两种方式
node 执行文件: ** node core/cli/bin/index.js**
node -e ‘字符串’:node -e “require(./core/cli/bin/index.js)”
第四章 通用 npm 模块类 Package 封装
4-1 脚手架命令本地调试功能支持
(本节有代码编写)
通过前面画图了解,我们要实现的第一步是initCommand的动态命令加载,即3-3 章节所示图。
是否执行本地代码,我们通过一个属性来进行标识:targetPath
1 2 3 4 5 6 7 8 9 program. .option ('-tp, --targetPath <targetPath>' ,'是否指定本地调试文件路径' ,'' ) program.on ('option:targetPath' ,function ( ){ process.env .CLI_TARGET_PATH = program.opts ().targetPath })
1 2 3 4 5 6 7 8 9 'use strict' ;function init (projectName,options,command ) { console .log ('init' ,projectName,command.opts ().force ,process.env .CLI_TARGET_PATH ) } module .exports = init;
本节需要注意的一点是如果commander版本低于7.0.0,那么 program.action()中传入的参数为两个。
7.0.0版本以上的传入的参数为三个(name.options,cmd)
另外,访问targetPath这个参数的时候,需要program.opts().targetPath访问。
4-2 动态执行库exec模块创建
(本节有代码编写)
core下新建包文件: lerna create @cloudscope-cli/exec core/
然后在core/cli/lib/index.js文件中将exec包引入,将action(init)此处改为action(exec)
4-3 创建npm模块通用类Package
(本节有代码编写)
首先讲解了exec模块逻辑
targetPath -> modulePath
modulePath -> Package(npm模块)
Package.getRootFile(获取入口文件)
Package.update / Package.install
代码实现:
在model文件下创建新的模块Package:lerna create @cloudscope-cli/package
在core/exec/lib/index.js文件中引入:const Package = require(‘@cloudscope-cli/package’)
4-4 Package类的属性、方法定义及构造函数逻辑开发
(本节有代码编写)
本节主要有三处代码讲解
core/exec中创建一个Package对象
model/package中Package类的构造方法
utils/utils中添加isObject方法:判断一个属性是否为对象
代码分别如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 'use strict' ;const Package = require ('@cloudscope-cli/package' )const log = require ('@cloudscope-cli/log' )const SETTINGS = { init : '@cloudscope-cli/init' } function exec ( ) { let targetPath = process.env .CLI_TARGET_PATH const homePath = process.env .CLI_HOME_PATH let storeDir ='' ; let pkg; log.verbose ('targetPath' , targetPath); log.verbose ('homePath' , homePath); const cmdObj = arguments [arguments .length - 1 ]; const cmdName = cmdObj.name (); const packageName = SETTINGS [cmdName]; const packageVersion = 'latest' ; pkg = new Package ({ targetPath, storeDir, packageName, packageVersion }) console .log (pkg) } module .exports = exec;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 'use strict' ;const { isObject } = require ('@liugezhou-cli-dev/utils' );class Package { constructor (options ){ if ( !options){ throw new Error ('Package类的options参数不能为空!' ) } if ( !isObject (options) ){ throw new Error ('Package类的options参数必须为对象!' ) } this .targetPath = options.targetPath this .storeDir = options.storeDir this .packageName = options.packageName this .packageVersion = options.packageVersion ; } exists ( ){} install ( ){} update ( ){} getRootFilePath ( ){} } module .exports = Package ;
1 2 3 4 5 6 7 8 9 'use strict' function isObject (obj ){ return Object .prototype .toString .call (obj).slice (8 ,-1 ) === 'Object' } module .exports = { isObject }
4-5 Package类获取入口文件路径功能开发(pkg-dir应用+解决不同操作系统路径兼容问题)
(本节有代码编写)
本节主要实现models/package/lib/index.js中获取入口文件路径的方法实现getRootfile()
思路:
获取package.json的所在目录–通过安装pkg-dir库
读取package.json
寻找main/lib
路径的兼容macOS/windows --新建包:utils/format-path,且新建路径兼容方法
核心代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ………… const dir = pkgDir (targetPath);if (dir) { const pkgFile = require (path.resolve (dir, 'package.json' )); if (pkgFile && pkgFile.main ) { return formatPath (path.resolve (dir, pkgFile.main )); } } return null ;…………
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 'use strict' ;const path = require ('path' )function formatPath (p ) { const sep = path.sep ; if (p && typeof p === 'string' ){ if (sep !=='/' ){ return p.replace (/\\/g ,'/' ) } } return p } module .exports = formatPath;
4-6 利用npminstall库安装npm模块
(本节有代码编写)
本节实现的内容为exec中的install方法,通过npminstall这个库。
使用之前现在测试项目下使用之:测试代码 。
1 2 3 4 5 6 7 8 9 10 11 const npminstall = require ('npminstall' )const path = require ('path' )const userHome = require ('user-home' )npminstall ({ root : path.resolve (userHome,'.cli-test' ), storeDir : path.resolve (userHome,'.cli-test' ,'node_modules' ) , registry :'https://registry.npmjs.org' , pkgs :[ {name :'foo' ,version :'~1.0.0' } ] })
首先,我们的项目在开发过程中可能会有错误,有的需要去看执行栈,有的不需要,因此我们在core/cli/lib/index中的core方法中,catch语句中加入如下代码(debug模式下显示执行栈错误)
1 2 3 if (program.opts ().debug ){ console .log (e) }
2.在core/exec/lib/index.js文件中,我们修改代码如下(主要加入了如果不存在targetPath的逻辑梳理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 'use strict' ;const path = require ('path' ) const Package = require ('@cloudscope-cli/package' )const log = require ('@cloudscope-cli/log' )const SETTINGS = { init : '@imooc-cli/init' } const CATCH_DIR = 'dependencies' async function exec ( ) { let targetPath = process.env .CLI_TARGET_PATH const homePath = process.env .CLI_HOME_PATH let storeDir ='' ; let pkg; log.verbose ('targetPath' , targetPath); log.verbose ('homePath' , homePath); const cmdObj = arguments [arguments .length - 1 ]; const cmdName = cmdObj.name (); const packageName = SETTINGS [cmdName]; const packageVersion = 'latest' ; if (!targetPath){ targetPath = path.resolve (homePath,CATCH_DIR ); storeDir = path.resolve (targetPath,'node_modules' ) log.verbose ('targetPath:' ,targetPath) log.verbose ('storeDir:' ,storeDir) pkg = new Package ({ targetPath, storeDir, packageName, packageVersion }); if (await pkg.exists ()){ log.verbose ('更新package' ) await pkg.update (); }else { await pkg.install (); } }else { pkg = new Package ({ targetPath, packageName, packageVersion }) const rootFile = pkg.getRootFilePath (); if (rootFile){ require (rootFile).apply (null ,arguments ); } } } module .exports = exec;
model/package包中文件主要加入了安装package这个方法,使用了npminstall这个库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 async install ( ){ await this .prepare () return npminstall ({ root : this .targetPath , storeDir : this .storeDir , registry :getDefaultRegistry (), pkg :{ name :this .packageName , version :this .packageVersion } }) }
4-7 Package类判断模块是否存在方法开发
(本节有代码编写)
本节的主要内容是实现package/lib/index.js中的exists方法,代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ………… this .cacheFilePathPrefix = this .packageName .replace ('/' , '_' ) ………… get cacheFilePath () { return path.resolve (this .storeDir ,`_${this .cacheFilePathPrefix} @${this .packageVersion} @${this .packageName} ` ) } async prepare ( ){ if (this .storeDir && !pathExists (this .storeDir )){ fse.mkdirpSync (this .storeDir ) } if (this .packageVersion === 'latest' ){ this .packageVersion = await getNpmLatestVersion (this .packageName ); } } async exists ( ){ if (this .storeDir ){ await this .prepare () return pathExists (this .cacheFilePath ); }else { return pathExists (this .targetPath ); } }
4-8 Package类更新模块逻辑开发
(本节有代码编写)
本节内容主要为如果Package包有升级,那么需要去更新,主要实现代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ………… getSpecificCacheFilePath (packageVersion ){ return path.resolve (this .storeDir ,`_${this .cacheFilePathPrefix} @${packageVersion} @${this .packageName} ` ) } async update ( ){ const latestPackageVersion = await getNpmLatestVersion (this .packageName ); const latestFilePath = this .getSpecificCacheFilePath (latestPackageVersion) if (!pathExists (latestFilePath)){ await npminstall ({ root :this .targetPath , storeDir :this .storeDir , registry :getDefaultRegistry (), pkgs :[{ name :this .packageName , version :latestPackageVersion } ] }) this .packageVersion = latestPackageVersion }else { this .packageVersion = latestPackageVersion } return latestFilePath; }
4-9 Package类获取缓存模块入口文件功能改造
(本节有代码编写)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 getRootFilePath ( ){ function _getRootFile (targetPath ) { const dir = pkgDir (targetPath); if (dir) { const pkgFile = require (path.resolve (dir, 'package.json' )); if (pkgFile && pkgFile.main ) { return formatPath (path.resolve (dir, pkgFile.main )); } } return null ; } if (this .storeDir ) { return _getRootFile (this .cacheFilePath ); } else { return _getRootFile (this .targetPath ); } }
ps:关于项目的代码以上就结束了,代码提交至:lesson04
第五章:预备知识:Node 多进程开发入门
5-1 进程的基本概念(讲解在操作系统中如何查看进程的嵌套关系)
官方文档中文版: http://nodejs.cn/api/child_process.html
进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单元,是操作系统结构的基础。
概念主要两点:
第一,进程是一个实体。每一个进程都有它自己的地址空间。
第二,进程是一个“执行中的程序”,存在嵌套关系
Node进程存在的感知:
终端中输入:ps -ef | grep node 命令。
UID是当前用户获取权限的ID
PID是当前进程ID
PPID是当前进程ID的父ID
使用webstorm调试一个node程序的图示如下:
5-2 child_process异步方法使用教程(exec&execFile)
child_process用法:
异步用法
同步用法
execSync
execFileSync
spawnSync
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const cp = require ('child_process' )cp.exec ('ls -al' ,function (err,stdout,stderr ){ console .log (err) console .log (stdout) console .log (stderr) }) cp.execFile ('ls' ,['-al' ],function (err,stdout,stderr ){ console .log (err) console .log (stdout) console .log (stderr) })
小结:exec和execFile在输出上看不出来区别。
exec主要用来执行一个shell命令,本质是execFile,只是参数不同,不支持传入arguments参数。
execFile只能执行一个文件,且加入一些命令,不能使用管道符。
5-3 child_process spawn用法以及与exec&execFile的区别
exec、execFile、fork底层都是使用的spawn。
spawn使用的时候,没有回调,需要监听获取结果。
新建一个test.shell文件的时候,如果要读取这个文件,那么需要添加权限:chmod +x test.shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const cp = require ('child_process' )const path = require ('path' )const child = cp.spawn (path.resolve (__dirname,'test.shell' ),['-al' ,'-bl' ],{ cwd :path.resolve ('..' ) }); child.stdout .on ('data' ,function (chunk ){ console .log ('stdout' ,chunk.toString ()) }) child.stderr .on ('data' ,function (chunk ){ console .log ('stderr' ,chunk.toString ()) })
spawn:耗时任务(比如 npm install): 不断打印日志
exec/execFile:耗时、开销比较小的任务: 一次性返回结果。
fork:多进程、多线程的下载。
5-4 child_process fork用法及父子进程通信机制讲解
fork主要是使用node来执行我们的命令。
fork会执行两个进程 主进程与子进程。
fork的本质也是调用spawn。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const cp = require ('child_process' )const path = require ('path' )const child = cp.fork (path.resolve (__dirname,'child.js' ))child.send ('hello child process!' ,()=> { }) console .log ('main pid:' ,process.pid )child.on ('message' ,msg => { console .log (msg) child.disconnect () }) console .log ('child pid:' ,process.pid )process.on ('message' , (msg )=> { console .log (msg) }) process.send ('hello main process!' )
5-5 child_process同步方法使用教程
execSync
execFileSync
spawnSync
1 2 3 4 5 6 7 8 9 10 11 12 13 const cp = require ('child_process' )const ret = cp.execSync ('ls -al | grep index.js' )console .log (ret.toString ())const ret2 = cp.execFileSync ('ls' , ['-al' ])console .log (ret2.toString ())const ret3 = cp.spawnSync ('ls' ,['-al' ])console .log (ret3.stdout .toString ())
第六章 基于 Node 多进程构建高性能脚手架
6-1 通过脚手架命令Command类封装
(本节有代码编写)
在 model文件夹下安装Command包:lerna create @cloudscope-cli/command
在commands/init 的package.json中引入上面新创建的包command,继承它作为它的子类。
然后我们来到新建的command包的lib/index文件中,添加如下代码(同时删除core/cli/lib/index.js中关于checNodeVersion方法的代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 'use strict' ;const semver = require ('semver' )const colors = require ('colors/safe' )const LOWEST_NODE_VERSION = '12.0.0' class Command { constructor (argv ){ this ._argv = argv let runner = new Promise ((resolve,reject )=> { let chain = Promise .resolve () chain = chain.then (()=> this .checkNodeVersion ()) chain.catch (e => { console .log (e.message ) }) }) } checkNodeVersion ( ){ const currentNodeVersion = process.version const lowestNodeVersion = LOWEST_NODE_VERSION if (semver.ltr (currentNodeVersion, lowestNodeVersion)) { throw new Error (colors.red (`cloudscope-cli 需要安装 v${lowestNodeVersion} 以上版本的node.js` )) } } init ( ){ throw Error ('init必须实现' ) } exec ( ){ throw Error ('exec必须实现' ) } } module .exports = Command ;
接着,我们来到commands/init/lib/index.js文件中,进行代码的改写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 'use strict' ;const Command = require ('@cloudscope-cli/command' )class InitCommand extends Command {} function init (argv ) { return new InitCommand (argv) } module .exports = initmodule .exports .InitCommand = InitCommand ;
这里的argv参数传递是从 exec/lib/index.js中的require传递过来的,因此参数传递修改
1 2 3 require (rootFile).apply (null ,arguments );require (rootFile).call (null ,Array .from (arguments ));
6-2 脚手架参数初始化方法开发
(本节有代码编写)
首先在command包中引入log包,用于chain的catch错误打印(上节代码已更新)
其次,对于class Comman需要对传入的参数argv进行一个判断,
如果为空需要抛出异常,这里需要注意抛出的异常在core/cli/lib/index.js中并没有捕获,需要在core/exec/lib/index.js中require的这个targetPath文件进行try catch捕获。
对argv这个参数进行是否对数组的判断。
对argv是否为空数组进行判断
1 2 3 4 5 6 7 8 9 if (!argv){ throw new Error ('argv参数不能为空' ) } if (!Array .isArray (argv)){ throw new Error ('argv参数必须为数组' ) } if (argv.length < 1 ){ throw new Error ('参数列表为空' ) }
在chain里面实现initArgs方法:主要用来进行参数的分解,将argv最后一个参数与之前的参数装到两个参数中去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let chain = Promise .resolve () chain = chain.then (()=> this .checkNodeVersion ()) chain = chain.then (()=> this .initArgs ()) chain = chain.then (()=> this .init ()) chain = chain.then (()=> this .exec ()) chain.catch (e => { log.error (e.message ) }) initArgs ( ){ const len = this ._argv .length - 1 this ._cmd = this ._argv [len].opts () this ._argv = this ._argv .slice (0 ,len) }
chain中的init方法与exec均抛出异常,需要由子类去实现:即commands/init/lib/index.js.
6-3 利用Node多进程动态执行命令(stdio的inherit属性讲解)
(本节有代码编写)
我们回到exec/lib/index.js中,其中之前的那行代码
require(rootFile).call(null,Array.from(arguments ));
是在当前进程中调用的,我们需要修改为在node进程中进行调用,这便是本节的重点。
我们通过本周第五章的内容,已经知道了如何使用child_process下的同步或者异步方法进行子进程的执行,这里我有两种方法可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const cp = require ('child_process' ) const child = cp.spawn ('node' ,['-e' ,code],{ cwd :process.cwd (), stdio :'inherit' }) child.on ('error' , e => { log.error (e.message ); process.exit (1 ); }) child.on ('exit' , e => { log.verbose ('命令执行成功' + e); process.exit (e); })
spawn方法中的参数stdio默认值为’pipe’管道,pipe使得主进程与子进程会产生通信通道,因此需要通过on这种方式去进行监听。
stdio还有一个值为’inherit’,它将相应的stdio传给父进程或者从父进程传入。也就是说:直接将process.stdin、process.stdout、process.stderr直接和父进程进行绑定,这样就无须去监听结果,可以直接将结果打印出来。
6-4 生成Node多进程动态执行代码
(本节有代码编写)
通过上一节的学习,我们通过代码const args = Array.from(arguments )
const cmd = args[args.length - 1]可以知道,现在需要做的就是拼成上面spawn在执行时所需的code
我们之前的代码为 require(rootFile).call(null,Array.from(arguments));
_
也就是兼容 conse code = require(rootFile).call(null,Array.from(arguments));
rootfile -> ${rootfile},难点是 Array.from(arguments)的传入。
1 2 3 4 5 6 7 8 9 10 const args = Array .from (arguments )const cmd = args[args.length - 1 ] const o = Object .create (null )Object .keys (cmd).forEach (key => { if (cmd.hasOwnProperty (key) && !key.startsWith ('_' )&& key!=='parent' ){ o[key]=cmd[key] } }) args[args.length -1 ] = o const code = `require('${rootFile} ').call(null,${JSON .stringify(args)} )`
注:由于我使用的commander是7.0.0的,低于此版本传入的参数为两个,但7.0.0版本传入参数为3个,因此上面的代码,我这里直接写成(不知道后续是否还会有错误):
1 2 let args = Array .from (arguments ).splice (0 ,2 )const code = `require('${rootFile} ').call(null,${JSON .stringify(args)} )`
到这里,使用node多进程执行代码的功能就完成了。
6-5 windows操作系统spawn执行命令兼容
(本节有代码编写)
windows操作系统与macOS关于spawn的执行是不一样的,本节解决的就是windows操作系统下的兼容
将本方法封装在utils/utils包中:通过process.platform来判断操作系统
1 2 3 4 5 6 function exec (command,args,options ){ const win32 = process.platform === 'win32' ; const cmd = win32 ? 'cmd' : command const cmdArgs = win32 ? ['/c' ].concat (command,args) : args; return require ('child_process' ).spawn (cmd, cmdArgs,options || {}) }
到以上为止,我们完成了动态命令的加载和执行。
第七章 加餐:Node 进阶: child_process 源码分析
7-1 Node多进程child_process库exec方法源码执行流程分析
疑问和收获:
exec和execFile到底有什么区别?
为什么exec/execFile/fork都是通过spawn实现的,spawn的作用到底是什么?
为什么spawn调用后没有回调,而exec和execFile能够回调?
为什么spawn调用后需要手动调用child.stdout.on(‘data’,callback),这里的child.stdout/child.stderr到底是什么?
为什么有data/error/exit/close这么多种回调,他们的执行顺序到底是怎样的?
exec源码粗略分析
点击查看【processon】
在未学习exec源码之前,我们先对上面的拓扑图进行一个简单的学习,看到exec内部的执行流程
不难看到exec执行的是execlFile这个方法,且不同的地方就是传入的参数不同,而execFile执行的是spawn这个方法,且spawn这个方法调用的是node内部库的一个child_process方法。
我们在webstorm中打开一个项目
1 2 3 4 5 6 7 8 9 10 const cp = require ('child_process' )const path = require ('path' )cp.exec ('ls -al|grep node_modules ' ,function (err,stdout,stderr ){ console .log (err) console .log (stdout) console .log (stderr) })
第五行代码打断点,配置webstorm调试设置后,执行命令(我这里是liugezhou-test),进入到exec源码
1 2 3 4 5 6 function exec (command, options, callback ) { const opts = normalizeExecArgs (command, options, callback); return module .exports .execFile (opts.file , opts.options , opts.callback ); }
正如上面拓扑图画的那样,首先执行一个normalLizeExecArgs方法,然后调用execFile这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function execFile (file ) { ……………… const child = spawn (file, args, { cwd : options.cwd , env : options.env , gid : options.gid , uid : options.uid , shell : options.shell , windowsHide : !!options.windowsHide , windowsVerbatimArguments : !!options.windowsVerbatimArguments }); ……………… function exithandler (code, signal ) { ……………… callback (ex, stdout, stderr); } function errorhandler (e ) { ……………… exithandler (); } if (child.stdout ) { ……………… } if (child.stderr ) { ……………… } child.addListener ('close' , exithandler); child.addListener ('error' , errorhandler); return child; }
上面代码的4行,我们看到调用了spawn方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function spawn (file, args, options ) { const opts = normalizeSpawnArguments (file, args, options); const child = new ChildProcess (); …………………… child.spawn ({ file : opts.file , args : opts.args , cwd : options.cwd , windowsHide : !!options.windowsHide , windowsVerbatimArguments : !!options.windowsVerbatimArguments , detached : !!options.detached , envPairs : opts.envPairs , stdio : options.stdio , uid : options.uid , gid : options.gid , serialization : options.serialization , }); return child; }
spawn方法的第2行如拓扑图所示,对参数执行了normalizeSpawnArguments方法,这里通过调试查看参数,发现,opts这个对象的file为 ‘bin/sh’,这里涉及到一个重要的知识点:
shell的使用
直接执行shell文件: /bin/sh test.shell
直接执行shell语句: /bin/sh -c “ls -al|grep node_modules”
spawn方法的第3行 const child = new ChildProcess
通过分析,我们知道这个ChildProcess调用的是内部库 internal/child_process的this._handler,再进一步如拓扑图所示,调用的是c++文件,不做继续跟踪。
继续往后该方法第6行,spwan方法调用的child.spwan如拓扑图所示,真正调用的是internal/child_process中的spawn–>this._hanlde.spawn方法,该方法执行完毕后,子进程便开启了.
在spwan最后返回child后,我们再返回到execFile中,发现child.stdout与child.stderr方法的输出,以及回调f
unction exithandler和errorhandler
上面就是对exec源码的略读过程。
7-2 高能:child_process库exec源码精度
上一节我们阅读了exec源码的第一遍,对答题流程有了认识,这节开始阅读第二遍,进行细节的解读。
首先进入到exec的normalizeExecArgs方法,逻辑简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function normalizeExecArgs (command, options, callback ) { if (typeof options === 'function' ) { callback = options; options = undefined ; } options = { ...options }; options.shell = typeof options.shell === 'string' ? options.shell : true ; return { file : command, options : options, callback : callback }; }
然后我们进入到execFile中,分析流程写入到下面代码之中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 function execFile (file ) { let args = []; let callback; let options; let pos = 1 ; if (pos < arguments .length && ArrayIsArray (arguments [pos])) { args = arguments [pos++]; } else if (pos < arguments .length && arguments [pos] == null ) { pos++; } if (pos < arguments .length && typeof arguments [pos] === 'object' ) { options = arguments [pos++]; } else if (pos < arguments .length && arguments [pos] == null ) { pos++; } if (pos < arguments .length && typeof arguments [pos] === 'function' ) { callback = arguments [pos++]; } if (!callback && pos < arguments .length && arguments [pos] != null ) { throw new ERR_INVALID_ARG_VALUE ('args' , arguments [pos]); } options = { encoding : 'utf8' , timeout : 0 , maxBuffer : MAX_BUFFER , killSignal : 'SIGTERM' , cwd : null , env : null , shell : false , ...options }; validateTimeout (options.timeout ); validateMaxBuffer (options.maxBuffer ); options.killSignal = sanitizeKillSignal (options.killSignal ); const child = spawn (file, args, { cwd : options.cwd , env : options.env , gid : options.gid , uid : options.uid , shell : options.shell , windowsHide : !!options.windowsHide , windowsVerbatimArguments : !!options.windowsVerbatimArguments }); ………………
上面代码走到51行后,进入spawn源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function spawn (file, args, options ) { const opts = normalizeSpawnArguments (file, args, options); const child = new ChildProcess (); options = opts.options ; debug ('spawn' , opts.args , options); child.spawn ({ file : opts.file , args : opts.args , cwd : options.cwd , windowsHide : !!options.windowsHide , windowsVerbatimArguments : !!options.windowsVerbatimArguments , detached : !!options.detached , envPairs : opts.envPairs , stdio : options.stdio , uid : options.uid , gid : options.gid , serialization : options.serialization , }); return child; }
7-3 深度分析child_process库spawn底层实现
接着上一节代码块中走到了child.spawn:
第一步是通过getValidStdio去生成pipe,创建一个管道实例:第一个是输入,第二个是输出,第三个是error(只是生成了管道,但是还没创建socket的通信)
第二步对spawn的一些参数进行处理:下面代码未贴
第三步通过this._handle.spawn 子进程被创建出来
第四步通过createSocket方法,将之前的pipe和子进程与socket绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ChildProcess .prototype .spawn = function (options ) {……………… stdio = getValidStdio (stdio, false ); ……………… const err = this ._handle .spawn (options); …………… for (i = 0 ; i < stdio.length ; i++) { ……………… if (stream.handle ) { stream.socket = createSocket (this .pid !== 0 ? stream.handle : null , i > 0 ); if (i > 0 && this .pid !== 0 ) { this ._closesNeeded ++; stream.socket .on ('close' , () => { maybeClose (this ); }); } } } ……………… return err; };
然后我们再返回到execFile中,接着往下走:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 function execFile (file ) {……………… let encoding; const _stdout = []; const _stderr = []; if (options.encoding !== 'buffer' && Buffer .isEncoding (options.encoding )) { encoding = options.encoding ; } else { encoding = null ; } ……………… function exithandler (code, signal ) { if (exited) return ; exited = true ; if (timeoutId) { clearTimeout (timeoutId); timeoutId = null ; } if (!callback) return ; let stdout; let stderr; if (encoding || ( child.stdout && child.stdout .readableEncoding )) { stdout = _stdout.join ('' ); } else { stdout = Buffer .concat (_stdout); } if (encoding || ( child.stderr && child.stderr .readableEncoding )) { stderr = _stderr.join ('' ); } else { stderr = Buffer .concat (_stderr); } if (!ex && code === 0 && signal === null ) { callback (null , stdout, stderr); return ; } if (args.length !== 0 ) cmd += ` ${args.join(' ' )} ` ; if (!ex) { ex = new Error ('Command failed: ' + cmd + '\n' + stderr); ex.killed = child.killed || killed; ex.code = code < 0 ? getSystemErrorName (code) : code; ex.signal = signal; } ex.cmd = cmd; callback (ex, stdout, stderr); } function errorhandler (e ) { ex = e; if (child.stdout ) child.stdout .destroy (); if (child.stderr ) child.stderr .destroy (); exithandler (); } ……………… if (options.timeout > 0 ) { ……………… } if (child.stdout ) { if (encoding) child.stdout .setEncoding (encoding); child.stdout .on ('data' , function onChildStdout (chunk ) { const encoding = child.stdout .readableEncoding ; const length = encoding ? Buffer .byteLength (chunk, encoding) : chunk.length ; stdoutLen += length; if (stdoutLen > options.maxBuffer ) { const truncatedLen = options.maxBuffer - (stdoutLen - length); _stdout.push (chunk.slice (0 , truncatedLen)); ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER ('stdout' ); kill (); } else { _stdout.push (chunk); } }); } if (child.stderr ) { if (encoding) child.stderr .setEncoding (encoding); child.stderr .on ('data' , function onChildStderr (chunk ) { const encoding = child.stderr .readableEncoding ; const length = encoding ? Buffer .byteLength (chunk, encoding) : chunk.length ; stderrLen += length; if (stderrLen > options.maxBuffer ) { const truncatedLen = options.maxBuffer - (stderrLen - length); _stderr.push (chunk.slice (0 , truncatedLen)); ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER ('stderr' ); kill (); } else { _stderr.push (chunk); } }); } child.addListener ('close' , exithandler); child.addListener ('error' , errorhandler); return child; }
7-4 child_process事件应用方法详解
本节我们进入到child_process源码的第三轮,彻底搞懂process的回调流程,也是child_process中最复杂的部分。同样,我们通过processOn图对流程进行梳理一遍:
点击查看【processon】
在分析了上面流程后,我们先写一些测试代码以理解上面的流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const cp = require ('child_process' )const path = require ('path' )const child = cp.exec ('ls -al|grep node_modules' ,function (err,stdout,stderr ){ console .log ('callback--------start' ) console .log (stdout) console .log ('callback--------end' ) }) child.stdout .on ('data' ,(chunk )=> { console .log ('stdout data:' ,chunk) }) child.stderr .on ('data' ,(chunk )=> { console .log ('stderr data:' ,chunk) }) child.stderr .on ('close' ,()=> { console .log ('stderr close' ) }) child.on ('exit' ,(exitCode )=> { console .log ('exit:' ,exitCode) }) child.on ('close' ,()=> { console .log ('close!' ) })
7-5 高难度:深度解析child_process库spawn方法回调原理
这一章完全听的懵逼了。略过。。。。。。。
7-6 child_process库fork执行流程分析
略。。。。。。。
7-7 精化:Node多进程源码总结
exec/execFile/spawn/fork的区别
exec: 原理是调用/bin/sh -c 执行我们传入的shell脚本,底层调用略execFile
execFile:原理是直接执行我们传入的file和args,底层调用spawn创建和执行子进程,并建立略回调,一次性将所有的stdout和stderr结果返回
spawn:原理是调用略internal/child_process,实例化略ChildProcess子进程对象,再调用child.spawn创建子进程并执行命令,底层是调用了child.)handle.spawn执行process_wrap中的spwan方法,执行过程是异步的,执行完毕后再通过PIPE进行单向数据通信,通信结束后子进程发起onexit回调,同时Socket会执行close回调。
fork:原理是通过spawn创建子进程和执行命令,采用node执行命令,通过setupchannel创建IPC用于子进程和父进程之间的双向通信。
data/error/exit/close回调的区别
data:用于主进程读取数据过程中通过onStreamRead发起的回调
error: 命令执行失败后发起的回调
exit: 子进程关闭完成后发起的回调
close:子进程所有Socket通信端口全部关闭后发起的回调
stdout close/stderr close:特定的PIPE读取完成后调用onReadableStreamEnd关闭Socket时发起的回调。