基于node实现一个简单的脚手架工具(node控制台交互项目)
- 实现控制台输入输出
- 实现文件读写操作
- 全原生实现一个简单的脚手架工具
- 实现vue-cli2源码
一、实现控制台输入输出
关于控制台的输入输出依然是基于node进程管理对象process,在process上有三个基于流的对象分别是:标准输入流(stdin)、标准输出流(stdout)、标准错误(stderr)。
关于流这里不做太多解析,后面会专门针对node核心模块做详细的分析,这里简单的说说它们的应用。输入流可以理解为将控制台的输入内容读到一个内存中;输出流就是可以理解为将内存中的内容输出;错误流可以理解为特殊的输出流,负责将内存中描述的错误信息按照特殊标准的输出流输出。
比如拿输出流来说他就是将内存中的数据输出,我们经常使用的console.log就是基于这个输出流,它实现将要输出的数据缓存到一个内存中,然后再按照输出设置的字符编码方式将内存中的数据输出,如果不是设置编码方式就会按照内存中的<Buffer>原始结构输出数据。
同样输入流就是输出流的一个反向操作,他只负责将数据按照指定的进制<Buffer>输入到一个内存中。
上面大概的介绍了以下流的一些概念,接下来看看一个最简单的控制台输入输出示例然后对API做一些解析:
1 #!/usr/bin/env node 2 3 process.stdout.write('\033[33m输入:\033[39'); 4 process.stdin.resume(); //等待输入 5 process.stdin.setEncoding('utf-8'); 6 process.stdin.on('data',function(data){ 7 process.stdin.pause(); 8 console.log('\033[90m输入的内容是:' + data + '\033[39m'); 9 })
(如果不清楚如何在控制台启动node.exe执行这段代码可以参考这篇博客: 手动封装一个node命令集工具 )
接着在控制台测试这段代码的效果是这样:
首先stdout.write你可以认为他就是一个console.log,虽然这并不准确,但在这篇博客的内容中可以这么理解。可能你会关注到'\033[33m **** \033[39',它是用来设置输出的颜色,这不需要过多关注,如果有兴趣可以自行查找资料了解。
然后就是输入流stdin,这里使用了四个API,其作用分别是:resume负责等待输入;setEncoding负责设置输入数据的编码;on负责注册一个‘data’事件,回调函数传入内存中缓存的数据;pause负责停止等待输入。
这部分内存看示应用上好像非常简单,但实际上并非代码所表现出来的这样简单,关于流是node非常重要的IO内容,也是比较复杂的一部分内容,如果想彻底弄明白流可以多差一些资料和教程,我后期也会围绕这个重要的内容单独写一篇博客。
二、实现文件的读写操作
关于文件读写操作也同样是一个复杂的内容,它的底层同样是基于流,而且还是node中唯一的一个同时提供同步和异步的模块,这篇博客也只是围绕实现一个简单的手脚架简单的介绍一些应用。
2.1.基于控制台输入信息指定打印文件内容,这里先将上一节内容与文件读取操作结合起来,了解文件操作的基本逻辑:
1 #!/usr/bin/env node 2 3 const fs = require('fs'); //导入fs模块 4 const argv = process.argv; //获取命令行参数 5 const cwd = process.cwd(); //获取当前执行路径 6 const dirname = __dirname; //获取当前程序的全局绝对路径 7 const templetPath = dirname + '\\templet'; //这里需要注意linux与windows的路径差异 8 9 const stdin = process.stdin; //获取当前进程上的输入流 10 const stdout = process.stdout; //获取当前进程上的输出流 11 12 //读取文件 13 fs.readdir(templetPath, function(err, files){ 14 //读取模板文件目录 15 function file(i){ 16 let filename = files[i]; 17 fs.stat(templetPath + '\\' + filename, function(err, stat){ 18 if(stat.isDirectory()){ //如果stat是目录 19 console.log(' ' + i + ' \033[36m' + filename + '\\\033[39m'); 20 }else{ 21 //这里将读取到文件标记上编号打印到控制台界面,为选择文件提供参考信息 22 console.log(' ' + i + ' \033[90m' + filename + '\033[39m'); 23 } 24 i++; 25 if(i === files.length){ 26 read();//读取文件 27 }else{ 28 file(i); 29 } 30 }); 31 } 32 //读取文件(写入编号读取文件) 33 function read(){ 34 console.log(' '); 35 stdout.write(' \033[33m输入文件编号:\033[39m'); 36 stdin.resume(); //等待输入 37 stdin.setEncoding('utf8'); 38 stdin.on('data', write); //基于输入流上的data事件调用文件读取方法 39 } 40 //基于控制台写入的文件编号,找到要操作的文件(这里先使用fs.readFile方法读一下文件) 41 function write(data){ 42 //data是控制台输入的内容,这里即文件索引 43 let filename = files[Number(data)]; 44 let filepath = templetPath + '\\' + filename; 45 if(!filename){ 46 stdout.write(' \033[31m输入的编号'+ data.trim() +'不存在,请重新输入:\033[39m'); 47 }else{ 48 stdin.pause(); 49 //打印拷贝的文件内容 50 fs.readFile(filepath, 'utf8', function(err, data){ 51 console.log('\033[90m' + data.replace(/(.*)/g, ' $1') + '\033[39m');//给文件添加缩进并打印 52 }); 53 } 54 } 55 file(0);//读取文件目录————根据编号选择操作的文件————将选择的文件写入当前执行路径的page文件夹中 56 });
如果你需要测试这部分代码,别忘记在项目的根目录下添加一个templet文件,并在里面放几个有内容的文件。
解析一下在示例代码中实现文件读取相关的一些属性、方法、模块:
fs模块:nodejs内部提供的操作文件的基础模块;
process.argv:启动执行项目的命令参数,测试代码中的功能实现暂时用不到,可以尝试打印一下看看,如果你需要实现一个完整的CLI工具肯定有用;
process.cwd:当前执行的路径(工作目录),测试代码中暂时用不到,但后面的示例代码用得到;
__dirname:当前程序的全局绝对路径,这是nodejs入门第一节课必将的内容,也非常简单,如果有疑问可以查看官方文档了解;
fs.readdir:读取文件目录;
fs.stat:基于路径读取文件描述属性,这里用来判断读取的文件路径是文件夹还是文件;
fs.readFile:基于文件路径读取文件内容。
上面的代码展示了文件操作的基本过程和逻辑,但还有一些需要注意的是文件操作内部是非常复杂的,涉及的内容包含流、buffer、异步同步等问题,比如上面的示例代码就是基于异步实现的,这是因为nodejs的单线程异步特性才能发挥它的程序性能,所以不建议在非必要的情况下使用同步的方式操作文件。
2.2实现文件拷贝复制(write改一些代码,并增加在一个writeFile方法):
1 //写入文件的方法 2 function writeFile(path,data,fun){ 3 fs.writeFile(path, data, function(err){ 4 if(err){ 5 console.log(' /033[90m' + err.toString() + '\033[39m'); 6 } 7 fun(); 8 }); 9 } 10 //基于控制台写入的文件编号,找到要操作的文件(这里先使用fs.readFile方法读一下文件) 11 function write(data){ 12 //data是控制台输入的内容,这里即文件索引 13 let filename = files[Number(data)]; 14 let filepath = templetPath + '\\' + filename; 15 let filePagePath = pagePath + "\\" + filename; //最终将文件内容写入到的目标路径 16 17 if(!filename){ 18 stdout.write(' \033[31m输入的编号'+ data.trim() +'不存在,请重新输入:\033[39m'); 19 }else{ 20 stdin.pause(); 21 //打印拷贝的文件内容 22 fs.readFile(filepath, 'utf8', function(err, data){ 23 console.log('\033[90m' + data.replace(/(.*)/g, ' $1') + '\033[39m');//给文件添加缩进并打印 24 fs.readdir(cwd, function(err, files){ 25 //判断工作区间是否存在安装文件的page文件夹 26 if(!files.length || files.indexOf('page') === -1){ 27 //如果没有page文件夹,在工作区间创建一个page文件夹 28 fs.mkdir(pagePath,(err)=>{ 29 if(err){ 30 console.log(' /033[90m' + err.toString() + '\033[39m'); 31 process.exit(1); //退出当前进程 32 } 33 writeFile(filePagePath,data,writeFileFun); //向工作区间安装文件 34 }); 35 }else{ 36 writeFile(filePagePath,data,writeFileFun); //向工作区间安装文件 37 } 38 }); 39 }); 40 } 41 //写入操作的回调函数 42 function writeFileFun(){ 43 console.log(' \033[33m' + filename + '写入成功。\033[39m'); 44 } 45 } 46 file(0);//读取文件目录————根据编号选择操作的文件————将选择的文件写入当前执行路径的page文件夹中 47 });
添加上面修改过的新代码到前面的代码中就可以实现一个简单的手脚架工具了,完整的代码在第三节。
关于添加的这部分代码做一些简单的分析:
这里重点在于需要判断工作区间是否由安装文件的文件夹“page”,如果没有需要在工作区间添加这个文件夹,实现这个功能的是fs模块上的mkdir方法,需要注意这已然是一个异步方法。然后就是使用了fs模块上的writeFile实现了文件的写入,即安装。还有一个细节就是在添加文件夹的时候如果报错我是用了process.exit(1)退出当前进程,这个逻辑就不需要过的说明了,仅仅说一下个代码的作用,如果还有不理解的话就查一下node官方文档吧。
还有就是关于templetPath以及templet文件夹这其实是一个非常简单的东西,就是在CLI全局下有一个templet用来存放模板文件,在这个文件里你可以存放一些简单的模板代码文件用于测试。
然后介绍了一些可能你会需要的内容:
进程和操作系统是基于信号的方式,如果需要让进程终止可以发送SIGKILL信号;
process.on('SIGKILL',function(){ //信号以收到 })
ANSI转义:这个内容在前面有提到过,就是用来修改控制台打印内容的颜色的操作,这里拿“\033[90m xxxx \033[39m”来简单的说一下:‘\033’标识转义的开始;‘[’表示开始设置颜色;‘90’表示一种颜色(亮灰色);‘m’表示颜色设置结束;‘39’用来将颜色设置回去。
最后重复提示一下关于文件操作流很重要,在node中有一个Stream模块,虽然我的代码中没有使用fs.createReadStream这样基于流的API,其实fs.readFile底层依然是基于流,只是fs.readFile是将所有内容读出来,而fs.createReadStream可以分段的读取文件,这对于文件操作非常重要,你可以想一下如果操作的是一个非常大的文件会如何。同样写入文件也一样,而且我的示例代码中的fs.writeFile将整个文件写入,如果文件中已经存在同名的文件会直接覆盖,这当然不能覆盖有问题,但对于大文件分段写入显然是不合适的。
然后,谈到了分段写入肯定就要考虑在写入的过程中文件发生了改变了怎么办,这时候就需要对文件进行监视,fs.watchFile就可以实现监视整个目录。
肯定还有很多问题我没有讲到,也不是在一篇博客中能讲完,特别是在FS模块和Stream模块后期肯定会有解析的博客,如果又不理解的地方或问题可以在评论区留言。
三、全原生实现一个简单的脚手架工具
1 #!/usr/bin/env node 2 3 const fs = require('fs'); //导入fs模块 4 const argv = process.argv; //获取命令行参数 5 const cwd = process.cwd(); //获取当前执行路径 6 const dirname = __dirname; //获取当前程序的全局绝对路径 7 const templetPath = dirname + '\\templet'; //这里需要注意linux与windows的路径差异 8 9 const stdin = process.stdin; //获取当前进程上的输入流 10 const stdout = process.stdout; //获取当前进程上的输出流 11 const pagePath = cwd + "\\page"; //设置文件写入的路径(当前工作区间的静态文件目录下) 12 13 //读取文件 14 fs.readdir(templetPath, function(err, files){ 15 //读取模板文件目录 16 function file(i){ 17 let filename = files[i]; 18 fs.stat(templetPath + '\\' + filename, function(err, stat){ 19 if(stat.isDirectory()){ //如果stat是目录 20 console.log(' ' + i + ' \033[36m' + filename + '\\\033[39m'); 21 }else{ 22 //这里将读取到文件标记上编号打印到控制台界面,为选择文件提供参考信息 23 console.log(' ' + i + ' \033[90m' + filename + '\033[39m'); 24 } 25 i++; 26 if(i === files.length){ 27 read();//读取文件 28 }else{ 29 file(i); 30 } 31 }); 32 } 33 //读取文件(写入编号读取文件) 34 function read(){ 35 console.log(' '); 36 stdout.write(' \033[33m输入文件编号:\033[39m'); 37 stdin.resume(); //等待输入 38 stdin.setEncoding('utf8'); 39 stdin.on('data', write); //基于输入流上的data事件调用文件读取方法 40 } 41 //写入文件的方法 42 function writeFile(path,data,fun){ 43 fs.writeFile(path, data, function(err){ 44 if(err){ 45 console.log(' /033[90m' + err.toString() + '\033[39m'); 46 } 47 fun(); 48 }); 49 } 50 //基于控制台写入的文件编号,找到要操作的文件(这里先使用fs.readFile方法读一下文件) 51 function write(data){ 52 //data是控制台输入的内容,这里即文件索引 53 let filename = files[Number(data)]; 54 let filepath = templetPath + '\\' + filename; 55 let filePagePath = pagePath + "\\" + filename; //最终将文件内容写入到的目标路径 56 57 if(!filename){ 58 stdout.write(' \033[31m输入的编号'+ data.trim() +'不存在,请重新输入:\033[39m'); 59 }else{ 60 stdin.pause(); 61 //打印拷贝的文件内容 62 fs.readFile(filepath, 'utf8', function(err, data){ 63 console.log('\033[90m' + data.replace(/(.*)/g, ' $1') + '\033[39m');//给文件添加缩进并打印 64 fs.readdir(cwd, function(err, files){ 65 //判断工作区间是否存在安装文件的page文件夹 66 if(!files.length || files.indexOf('page') === -1){ 67 //如果没有page文件夹,在工作区间创建一个page文件夹 68 fs.mkdir(pagePath,(err)=>{ 69 if(err){ 70 console.log(' /033[90m' + err.toString() + '\033[39m'); 71 process.exit(1); //结束当前进程 72 } 73 writeFile(filePagePath,data,writeFileFun); //向工作区间安装文件 74 }); 75 }else{ 76 writeFile(filePagePath,data,writeFileFun); //向工作区间安装文件 77 } 78 }); 79 }); 80 } 81 //写入操作的回调函数 82 function writeFileFun(){ 83 console.log(' \033[33m' + filename + '写入成功。\033[39m'); 84 } 85 } 86 file(0);//读取文件目录————根据编号选择操作的文件————将选择的文件写入当前执行路径的page文件夹中 87 });
四、实现vue-cli2源码
这个源码我正在写,过几天写完上传到github上,到时候再来添加项目地址。