注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Minary_Acdream

http://f10.moe/

 
 
 

日志

 
 

Node.js学习-第四章-命令行工具(CLI)以及FS API:首个Node应用  

2014-06-08 20:56:10|  分类: node.js |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
前言
    这章介绍使用Node.js中一些重要级API:处理进程(studio)的 stdin 以及 stdout 相关的 API ,还有那些与文件系统(fs)相关的API。
    上章讲到了Node通过使用回调和事件机制来实现并发。这些API会接触到基于非阻塞事件的I/O编程中的流控制。

需求
        我们先做个简单的应用,需求如下:
  • 程序需要在命令行运行。这意味着程序要么通过node命令执行,要么直接执行,然后通过终端提供交互给用户输入、输出。
  • 程序启动后,需要显示当前目录下列表
  • 选择某个文件时,程序需要显示该文件的内容。
  • 选择一个目录时,程序需要显示该目录下的信息。
  • 运行结束后程序退出。
       根据上述需求,可以将此项目细分到下面几个步骤:
  1. 创建模块。
  2. 决定采用同步的fs还是异步的fs。
  3. 理解什么是流(stream)。
  4. 实现输入输出。
  5. 重构。
  6. 使用fs进行文件交互。
  7. 完成。            
创建模块  
    我们首先要创建一个项目目录开始。按照此项目的需求,将该目录命名为file-explorer

    package.json文件能方便的对NPM中注册的模块进行管理,将来也能对模块进行发布。
    我们首先创建一个简单的package.json文件
de>
#package.json
{
"name" : "file-explorer",
"version" : "0.0.1",
"description" : "A command-file file explorer"
}
de>
注意:NPM遵循一个名为semver的版本控制标准。因此使用0.0.1而不是0.1或1

要验证你的package.json是否正确,可以运行$ npm install 来验证。


决定采用同步的fs还是异步的fs

接着我们创建一个简单的包含程序完整功能的JavaScript文件: index.js

由于stdio API是全局process对象的一部分,所以这里只需要依赖fs模块。

fs模块是唯一一个既提供同步和异步的API的模块。

获取向前目录的文件列表:
同步表示:
de>
console.log( require ('fs').readdirSync(__dirname));
de>

异步表示:
de>
function async (err, files) {console.log (files); };
require( ' fs' ).readdir( '.' , async );
de>

要在单线程中创建能够处理高并发的高效程序, 就得采用异步、事件驱动的程序。

为了获取文件列表,我们需要使用fs.readdir。我们提供的回调函数首个参数是一个错误对象(如果没有错误发生,该对象是null),另外一个参数是一个files数组。
de>
fs.readdir( __dirname, function(err, files) {
    console.log(files);
});
de>


理解什么是流(stream)
我们已经 知道,console.log会输出到控制台,事实上,console.log内部做了这样的事情:它在指定的字符串后面加了\n字符,并将其写到stdout流中。
如果用process.stdout.write('hello world');输出的时候就没有换行。

process全局对象中包括了三个流对象,分别对应三个UNIX标准流:
- **stdin**:标准输入    ---可读流    (默认状态是暂停的(paused),除非有IO等待,否则node总是会自动退出)
- **stdout** : 标准输出  ---可写流
- **stderr** : 标准错误   ---可写流

流的另一个属性:它的默认编码。如果在流上设置了编码,那么就会得到编码后的字符串(utf-8,ASCII等),而不是原始的Buffer作为事件参数。

简而言之,当涉及持续不断地对数据进行读写时,流就出现了。

实现输入输出
    先尝试实现列出当前目录下的文件,等待用户输入:
de>

fs.readdir(process.cwd(), function (err, files) {
    console.log('');                        //表示友好,先输出一空行

    if(!file.length){
        return console.log('    \033[31m No files to show!\033[039m\n');   // 文本周围的\033[31m 和\033[39m是为了是文本呈现红色
    }
    console.log('   Select which file or directory you want to see\n');

    function file(i){                            //数组中每个元素都会执行该函数。这里也出现了贯穿本书始终的第一种异步流控制模式:串行执行
        var filename = file[i];
        //fs.stat会给出文件或者目录的元数据
        fs.stat(__dirname + '/' + filename, function (err, stat) {            //回调函数中还给出了错误对象(如果有的话)和stat对象
            if(stat.isDirectory()) {                                        //用到的是stat对象上的isDirectory方法
                console.log('    ' + i + '      \033[36m' + filename + '/\033[39m');
            }else{
                console.log('    ' + i + '      \033[90m' + filename + '/\033[39m');
            }

            //检查是否还有未处理的文件
            i ++;
            if(i == files.length) {
                console.log('');
                process.stdout.write('    \033[33mEnter you choice:  \033[39m');
                process.stdin.resume();                //等待用户输入
                process.stdin.setEncoding('utf8');    
            }else{
                file(i);
            }
        });
    }

    file(0);
});

de>

重构
    要做重构,就先从几个常用的变量(如stdin和stdout)创建快捷变量开始:
de style="font-size: 14px; line-height: 21px;" >
    var fs = require('fs');
    var stdin = process.stdin;
    var stdout = process.stdout;
de>
   
    由于我们书写的代码都是异步的,因此,随着函数量的增长(特别是流控制层的增加),多过的函数嵌套会让程序可读性变差。
    为了避免此类问题,我们可以为每个异步操作预先定义一个函数。
    修改如下:
de>
    function file(i){                        
        var filename = file[i];
    
        fs.stat(__dirname + '/' + filename, function (err, stat) {            
            if(stat.isDirectory()) {                                        
                console.log('    ' + i + '      \033[36m' + filename + '/\033[39m');
            }else{
                console.log('    ' + i + '      \033[90m' + filename + '/\033[39m');
            }

            if(++== files.length){
                read();
            }else{
                file(i);
            }
        });
    }
    function read() {
        console.log('');
        stdout.write('    \033[33mEnter you choice:  \033[39m');
        stdin.resume();                //等待用户输入
        stdin.setEncoding('utf8');

        stdin.on('data', option);    //设置data事件来监听,根据用户输入的内容做出相应处理。    
    }                                //上述代码是新的stdin和stdout的引用。

    function option(data) {
        if(!files[Number[data]]) {        //检查用户的输入是否匹配files数组中的下标。
            //需记得files数组是fs.readdir回调函数中的一部分
            //上述代码中,将utf-8编码的字符串类型转换成Number类型方便检查
            stdout.write('    \033[31mEnter your choice:  \033[39m');
        }else{
            stdin.parse();
        }
    }
de>


使用fs进行文件交互
既然已经能够定位到文件了,是时候去读取它了!
de>
function option(data){
        var filename = files[Number(data)];
        var number = stats[Number(data)];
        if(number){
            if(stats[Number(data)].isDirectory()){  //判断输入项是否正确
                console.log('   ');
                console.log('  (  ' + files.length + '   files)');
                files.forEach(function(file){ //遍历文件夹
                    console.log('   -   '+file);
                });
                console.log('');
                stdout.write('  \033[31mEnter you choice:\033[39m');
            }else{
                stdin.pause();
                fs.readFile(__dirname + '/' + filename,'utf8',function(err,data){
                    //文件名含文件的路径,设置编码,callback
                    console.log('');
                    console.log('\033[91m' + data.replace(/(.*)/g,' $1')+'\033[39m');
                });
            }
        }else{
            stdin.pause();
        }
    }
de>
但是,要是选取的是目录呢?这种情况下就应该将其文件列表显示出来。
为了避免再次执行 fs.stat ,我们在 file 函数中,将 stat 对象保存下来。
de>
    var status = [];
    function file(i) {
        var filename = files[i];

        fs.stat(__dirname + '/' + filename, function(err, stat) {
            status[i] = stat;
            //...
        });
    }
de>


完成

下述代码为完整代码:
de>
var fs = require('fs');
var stdin = process.stdin;
var stdout = process.stdout;


fs.readdir(process.cwd(), function (err, files) {
    console.log('');

    if(!files.length){
        return console.log('    \033[31m No files to show!\033[039m\n');
    }
    console.log('   Select which file or directory you want to see\n');
    var stats = [];
    if(!files.length){  //判断是否有文件
        return console.log('\033[31m No files to show! \033[39m\n');
    }
    console.log('Select which file or directory you want to see\n');
    var stats=[];
    function file(i){
        var filename = files[i];
        fs.stat(__dirname + '/' + filename , function(err , stat){  //stat返回文件信息,argument为路径
            stats[i] = stat;
            if(stat.isDirectory()){ //判断是否为目录
                console.log('   '+i+'   \033[36m'+filename+'\033[39m');
            }else{
                console.log('   '+i+'   \033[90m'+filename+'\033[39m');
            }
            if(++== files.length){
                read();
            }else{
                file(i)
            }
        });
    }
    function read(){
        console.log('');
        stdout.write('  \033[33mEnter your choice:\033[39m')
        stdin.resume();  // 等待用户输入
        stdin.setEncoding('utf8');//设置输入流的编码
        stdin.on('data',option); //
    }

    function option(data){
        var filename = files[Number(data)];
        var number = stats[Number(data)];
        if(number){
            if(stats[Number(data)].isDirectory()){  //判断输入项是否正确
                console.log('   ');
                console.log('  (  ' + files.length + '   files)');
                files.forEach(function(file){ //遍历文件夹
                    console.log('   -   '+file);
                });
                console.log('');
                stdout.write('  \033[31mEnter you choice:\033[39m');
            }else{
                stdin.pause();
                fs.readFile(__dirname + '/' + filename,'utf8',function(err,data){
                    //文件名含文件的路径,设置编码,callback
                    console.log('');
                    console.log('\033[91m' + data.replace(/(.*)/g,' $1')+'\033[39m');
                });
            }
        }else{
            stdin.pause();
        }
    }
    file(0);
});
de>


对CLI一探究竟(对上述程序的细节分析)
        
        详细见官方文档http://nodejs.org/api/process.html
        
        argv
  process.argv包含了所有node.程序运行时的参数值
  第一个元素始终是node,第二个元素始终是执行的文件路径。紧接着是命令行后紧跟的参数。
  要获取这些真正的元素,需要首先将数组的前两个元素去掉。   
 console.log(process.argv.slice(2));

        工作目录
            在前面的例子里,我们用__dirname来获取执行文件时该文件系统所在的目录。
            不过,如果想获得程序运行时的当前工作目录,用__dirname是不能满足这个要求的。
            因为index.js文件的路径依旧没有改变,所以无论在那个目录下运行,获取的仍是和现在一样的目录。

            所以,获取当前工作目录,可以调用process.cwd方法。
            Node还提供了process.chdir方法,允许灵活的更改工作目录。
    
       环境变量
            Node允许通过process.env变量来轻松访问shell环境下的变量。
            一个最常见的环境变量就是NODE_ENV, 该变量用来控制程序是运行在开发模式下还是产品模式下。
            
    
        退出
            要让一个应用退出,可以调用process.exit并提供一个退出代码。
            例如,当发生错误时,要退出程序,这个时候最好是使用退出代码1。
de>
 console.log('ERROR');           
 process.exit(1);
de>
            这样可以让Node命令行程序和操作系统中其它工具进行更好的偕同。

       信号
            进程和操作系统进行通信的其中一种方式就是通过信号,比如要让进程终止,就可以发送SIGKILL信号。
de>
process.on('SIGKILL'function() {
    //信号已收到
});
de>

       ANSI转义码
            要在文本终端下控制格式,颜色以及其它输出选项,可以使用ANSI转义码。
            在文本周围添加的明显不用于输出的字符,称为非打印字符
        
            比如,看下面例子:
                console.log('\033[90m' + data.replace(/(.*)/g,' $1')+'\033[39m');
  •  \033表示转义序列的开始
  • 表示开始颜色设置
  • 90表示前景色为亮灰色
  • m表示颜色设置结束  
  • 最后的39 是将颜色再设置回去  
            具体的可以去查ANSI转义码表。
        
        对fs一探究竟    (官方文档:http://nodejs.org/api/fs.html)
            fs模块允许通过Stream API来对数据进行读写操作。与readFile及writeFile方法不同,它对内存的分配不是一次完成的。
            比如一个大文件,文件内容由上百万行逗号分割文本组成。
            要完整地读取该文件来进行解析,意味着要一次性分配很大的内存。更好的方式应当是一次只读取一块内容,以(\n)来切分,然后再逐块进行解析。
        
      Stream
             fs.createReadStream(path, [options])方法允许为一个文件创造一个可读的Stream对象。
            为了更好的理解stream的威力,可以看下下面两个例子:     
de>
fs.readFile('MyFiles.txt'function (err, contents) {
    //对文件的处理
});
de>
            上述例子,回调函数必须等到整个文件读取完毕、载入到RAM、可用的情况下才会触发。

            下面的例子中,每次都会读取可变大小的内容块,并且每次读取后会触发回调函数:
de>
var stream = fs.createReadStream('MyFiles.txt');
stream.on('data'function(chunk) {
    //对文件的处理
});
stream.on('end'function(chunk) {
    //文件读取完毕
});
de>

      监视
          Node允许监视文件或者目录是否发生变化。监视意味着当文件系统中的文件(或者目录)发生变化时,会分发一个事件,然后触发指定的回调函数。







  评论这张
 
阅读(304)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018