• 15812字教你从零入门node.js(基础篇)


    写在前面

    node是每个js开发者都会使用,也是必会的工具,可能很多人都和我一样,平时都会用,但是没有系统去学习过他。这段时间,抽空从头开始学了学node基础,并把这段时间的学习作为node第一阶段,同时把第一阶段,node基础分为七个部分,共八篇,各位看官想分条细看,可以去我的主页找这八篇笔记。现在,我把这八篇笔记合八为一,综合成《15812字教你从零入门node.js(基础篇)》。

    模块化

    加载模块

    ​ JS 并没有“脚本间贡献全局命名空间”这一特性。一旦JS代码被载入网页,就会添加进入全局命名空间。全局命名空间是被所有已载入的脚本所贡献的通用地址空间,这样不免会有一些安全问题、冲突以及一些难以跟踪和解决的一般性错误。

    ​ 不过,Node为服务器端的JS制定了一些规范,并且实现了CommonJS模块标准。这样,每个模块都拥有一个上下文来将其他模块隔离开,这意味着没有全局污染(根本就没有全局作用域)。

    如何加载模块

    ​ Node中可以用文件路径也可以用名称来引用模块。如果不是核心模块,使用名称引用的模块都会被映射成文件路径(即等价)。

    ​ 不管什么模块,都可以使用require函数 引入:

    var module = require('module_name')
    

    ​ 导入一个模块将返回一个对象。该对象表示模块对外暴露的API,根据模块不同,该对象可能是任意的JS值:函数、对象…

    导出模块

    ​ 在Node中,CommonJS模块系统是文件之间共享对象、数据、函数等等之类的唯一方式。

    ​ 在Node中,文件和模块是一一对应的。

    ​ Feudalman.js文件:

    function feudalman(x,y) {
        function add (x,y) {
            return x + y;
        }
        function sub (x,y) {
            return x - y;
        }
    }
    
    module.exports = feudalman;
    

    ​ 重点在于最后一行:module是一个变量,表示模块自身。而module.exports表示导出的对象(可以是任意对象)。module初始被定义为一个空对象。

    ​ 还可以导出多个对象:

    module.exports.add = feudalman.add;
    module.exports.sub = feudalman.sub;
    

    ​ 此时如果需要使用,则这么写(同目录下文件):

    const mod = require('./Feudalman');
    const res1 = mod.add(1,1);
    const res2 = mod.sub(1,2);
    

    加载模块

    ​ 引用模块的方式取决于模块类型——核心模块、第三方模块、本地模块。

    核心模块

    ​ 核心模块只能通过模块名引用。如果有同名模块,核心模块加载优先级更高。

    ​ 例如,如果想加载http模块:

    const http = require("http");
    

    文件模块

    ​ 可以提供绝对路径从系统文件中加载非核心模块,如:

    const x = require('/home/node/my_module/my_module');
    

    ​ 或者也可以使用相对路径定位项目中的模块,如上面引入Feudalman所写。

    ​ 注意,可以省略模块文件的拓展名.js,如果没有找到该文件,Node会在文件名后自己加上拓展名查找。

    加载文件夹模块

    ​ 还可以使用文件夹路径加载模块:

    const x = require('./myModule');
    

    ​ Node会假定该文件夹是一个包,并试图查找配置文件package.json

    ​ 如果没有配置文件,会默认入口为index.js

    ​ 相反,如果存在配置,就会解析配置文件找到入口。这个后面会讲到。

    node_modules问价夹加载

    ​ 如果require传参不是上面的任何一种,那么Node就会尝试在当前目录下的node_modules下查找模块。如果没有找到,会自动去父级文件夹的node_modules中查找,直到根目录。

    ​ npm会自动帮你管理,所以不用操心包到底怎么放。

    缓存模块

    ​ 模块在首次加载时会被缓存起来,这意味着如果模块名能被解析为相同的文件名,那么每次调用require('myModule')都会返回同一模块。

    ​ 举个例子就能理解了:

    ​ 创建一个模块,内容为:

    console.log("init");
    module.exports = function () {
        console.log("export!");
    }
    console.log("exported!");
    

    ​ 然后加载一次模块:

    const myModule = require('./myModule');
    

    ​ 会输出initexported

    ​ 而加载两次:

    const myModule = require('./myModule');
    const myModule = require('./myModule');
    
    最终发现打印结果和上面一模一样,也就是说,不会重复多次初始化同一个模块。
    

    小结:

    1. Node取消了JS默认的全局命名空间,而用CommonJS系统代替。
    1. 不同的模块require的不同传参
    1. 如何引用并使用模块
    

    缓冲区

    利用缓冲区处理、编码、解码二进制数据

    创建缓冲区

    使用字符串创建:

    var buf = new Buffer('Hello World');
    

    如果需要指定编码格式,则添加第二个参数如base64表明即可。

    可以被接受的编码格式(注意大小写下不同的写法):

    • ascii——ASCII:该编码格式仅适用于ASCI字符集。
    • utf8——UTF-8:这是一种变宽度的编码格式,可以表示Unicode字符集中的任意字符,它是网络上的首选编码格式。在你没有指定编码格式参数的情况下,8是默认的编码格式。
    • base64——Base64:这种编码格式基于64个可打印的ASCI字符来表示二进制数据, Bse64通常用于在字符文档内嵌入可以被转换成字符串的二进制数据,在需要时又可以完整无损地转换回原来的二进制格式。

    如果没有初始化内容,可以指定大小:

    var buf = new Buffer(1024); // 创建长度为1024个字节的缓冲区
    

    在缓冲区中获取和设置数据

    创建之后,可以查看和修改:

    var buf = new Buffer("my buffer content");
    // 像数组一样读取
    console.log(buf[10]);
    // 同样也可以修改
    buf[5] = 123;
    

    注意,当创建一个用长度初始化的缓冲区时,里面默认的数据并不是0,而是一些随机值

    注意:在某些情况下不会出错。如:

    1. 如果将缓冲区中的某个位置设置为一个大于255的数,那么将会用256对该数取模,最终将取模结果赋给该位置。
    2. 如果将缓冲区中某个位置设置为256,那么该位置实际上是被赋值为0。
    3. 如果将缓冲区中某个位置设置成100.7这样的小数,那么该位置仅会存储整数部分,本例中存储的是100
    4. 如果尝试给超出缓冲区边界的位置赋值,那么赋值操作将以失败告终,缓冲区也不会发生变化

    除了修改和查看,还可以获取缓冲区长度:

    const length = buf.length;
    

    切分缓冲区

    可以通过指定开始和结束位置来从一个长缓冲区中划分区域,从而创建一个小的缓冲区。

    // 使用缓冲对象上的slice方法,第一个参数为开始索引,第二个参数为结束索引
    var smallerBuffer = buf.slice(8,15);
    console.log(smallerBuffer.toString()); // 可以用toString查看
    

    但是!slice创建小的缓冲区,并没有进行复制、分配新的内存,而是引用了父缓冲区中的内存,也就是只是保存了原来的地址,只是初始化了小缓冲区的头尾指针。所以有下面一些问题:

    1. 父缓冲区被修改,如果涉及子缓冲区的数据,同样也会被修改。
    2. 父缓冲区创建子缓冲区,之后如果成为内存垃圾,也不会被垃圾回收机制回收,有内存泄露。
    3. 那怎么办呢?看下一点。

    复制缓冲区

    可以使用copy方法,将一个缓冲区的一部分复制到另一个缓冲区中:

    const buf1 = new Buffer("this is the content of first buffer");
    let buf2 = new Buffer(11);
    
    /**
     * 参数分别为:
     * 1. 目标缓冲区
     * 2. 复制后,从目标中的哪个位置开始放置
     * 3. 被复制缓冲区中,复制开始的索引
     * 4. 被复制缓冲区中,复制结束的索引
     */
    buf1.copy(buf2,0,8,19); // buf2的内容 => the content
    

    缓冲区解码

    缓冲区可以转换成UTF-8格式的字符串。

    var str = buf.toString();
    

    如果给toString()传参,可以指定解码的编码格式。

    const str = buf.toString('base64');
    

    小结

    1. 有时候不得不对二进制数据进行处理,但是原生JS并没有明确提供处理方法。
    2. Node的Buffer类对内存操作进行了封装,可以处理二进制。
    3. 二进制和字符串可以互相转换。

    事件发生器

    在Node中很多对象都能发射事件,比如客户端连接的时候,Tcp服务器会发射connect事件等等。这些对象被称为事件发射器,而事件发生时就会调用对应的回调函数。这是典型的发布订阅模式。

    还可以创建自定义事件发生器,因为Node提供了一个伪类EventEmitter

    标准回调模式

    在回调模式下,每个函数在执行完毕后可以调用另一个函数以使得程序能够继续运行下去。

    举个例子:

    var fs = require("fs");
    fs.readFile('./test',function(err,fileContent){
    	if(err) {
    		throw err;
    	}
    	console.log(fileContent.toString());
    })
    

    可以看到,readFile函数的第二参数是一个函数,而这个函数接受到的第一个参数是上一级函数发生错误时的错误信息,第二个参数是上一级函数的返回值。

    理解事件发生器模式

    在回调模式中,如果在函数执行时,发生了多个事件或者事件反复发生了多次,这时候回调模式工作的就不是很好。而这时事件发生器模式将会派上用场。

    在使用事件发生器模式时,通常会设计多个对象,包括事件发生器和多个监听器

    事件发生器是可以发射事件的对象,而监听器是绑定到事件发射器上的代码,负责监听对应事件类型,来看个例子:

    const req =  http.request(options,function(response){
    	response.on("data",function(data){
    		console.log("data from response:" + data);
    	});
    	response.on("on",function(){
    		console.log("response ended");
    	})
    });
    req.end();
    

    在上面的代码中,我利用Node的http库创建了一个请求,当HTTP服务器响应的时候会调用一个回调函数,request执行完毕后,就会将响应对象传递给回调函数,而这个响应对象,即上述代码中的response就是一个事件发生器,他能够发出很多个事件,如上面代码里面有的data、end事件。每当这些事件发生的时候,后面的监听函数就会执行。

    大家可以结合代码具体理解一下两种模式的不同。一般而言,需要在操作完成后重新获取控制权的时候就用回调模式,当事件可能发生多次的时候就用事件发射器模式。

    事件类型

    事件是具有类型的,如上面的“data”、“end”,使用字符串标明的。你无法通过编程判断事件内部会出发什么事件,所以一般这些API都是有文档的,看文档的能力也很重要。

    一旦有相关事件发生,事件发射器就会调用相对的事件监听器,并将相关数据作为参数传递。如上面的data事件,会接受一个data参数,而end事件没有参数,不用接收。

    虽然事件发射器是服务任意类型事件的通用接口,但是有一个特例即error事件。大多数程序在发生错误的时候都会发射error事件,如果没有对应的监听,那么事件发射器会向上跑出一个未捕获的异常。

    事件发生器API

    任何一个实现了事件发生器模式的对象(如上面用过的http)都实现了如下方法:

    • addLinsteneron:添加事件监听器。
    • once:给指定事件绑定一个只会调用一次的监听器。
    • removeEventListener:删除某个监听器。
    • removeAllEnventListener:删除所有监听器。

    几个小点:

    • on只是addLinstener的简写,都一样。这样用:

      obj.on("事件名",传递的对应处理函数);
      
    • 可以给同一个发射器发射的同类型事件绑定多个监听器,会从上到下执行每个监听器函数。这意味着,有可能事件发生之后,在调用他的监听器之前会有别的事件的监听器被执行。

    • removeListener是这么用的:

      obj.removeListener("事件类型",监听函数名); 
      //函数必须有名字才能删除,匿名的怎么删对不对。
      
    • 删除所有,这个用的少且,不推荐使用,如果需要删除自己创建对象的所有监听器,也可以。传入事件名可删除对应事件上所有监听器,传入SIGTERM字符串可删除进程中所有监听器。

    创建事件监听器

    从node事件发射器继承

    可以创建一个继承自EventEmitter的伪类。

    const util = require('util');
    const ee = require('events').EventEmitter;
    // 创建一个类构造器
    const MyClass = function(){
    }
    // 继承,inherits的翻译就是继承
    // 这个方法创建了一条原型链,使得MyClass能够使用原型上的方法。
    util.inherits(MyClass,ee);
    

    发射事件

    通过继承的类,就可以发射事件了:

    MyClass.prototype.someMethod = function(){
    	this.emit("MyEvent","argument1","argument2");
    }
    

    someMethod方法被调用的时候,会发射一个MyEvent事件,后面的参数可以传递给监听器。
    监听:

    const instance = new MyClass();
    instance.on("MyEvent",e1,e2) {
    	...
    }
    

    小结

    • 发射器模式实现了特定事件与触发后执行逻辑的解耦。
    • 使用on来注册监听器等等。
    • 可以通过继承EventEmitter类以及使用emit()函数来创建自定义事件发射器。

    函数执行

    本来首先应该介绍setTimeoutsetInterval这两个函数的,但是网上的详细教学已经很多了,所以就不再赘述,简单提一下需要掌握的几个点:

    • 在延迟一定时间之后调用函数
    • 取消定时器

    使用process.nextTick将函数推迟到下一轮事件循环

    上面两个延迟函数是在时间纬度上,将代码推迟一段时间然后再执行。而process.nextTick是在内存纬度,等到别的事件都执行完了,再执行需要延迟的代码。

    Node的事件循环在一个处理事件的队列里面循环运行,事件循环每被执行一次就被称为一个Tick。

    通过nextTick来取代setTimeout,回调函数会在事件队列内所有事件处理完毕之后立刻执行,这样效率更高,不用浪费额外时间等待:

    // process是Node中的全局对象!
    process.nextTick(function(){
    	// 自己的代码
    	myFunction();
    })
    

    阻塞事件循环

    JS都运行时采用的是单线程事件循环(不懂单线程多线程的可以简单查查概念),当有程序占用了很长的运行时间,那么程序就阻塞在这儿,后面的程序无法执行,会使得服务变得很卡。

    退出事件循环

    通过使用process.nextTick将一个关键性的任务推迟到下一个时间循环再执行,这样可以疏通循环。

    如,要删除之前创建的一个文件,但是又不想在对客户端做出响应之前删除,就可以这样:

    stream.on("data",function(data){
    	stream.end(res);
    	process.nextTick(function(){
    		fs.unlink("./myFile");
    	})
    })
    

    使用setTimeout代替setInterval强制代码串行执行

    假如你希望你的函数重复执行一些I/O操作(比如解析日志文件),可以使用setInterval函数,:

    const interval = 1000;
    setInterval(function(){
    	myF();
    })
    

    但是,考虑这样一种情况:你的函数运行时间大于一秒,即大于interval大小,这个时候,你希望串行执行的函数可能不再串行,而是并行,如果你在你的函数中进行发送请求等等操作,时间不确定性还很大,你无法确定怎么设定时间。

    因此,需要强制指定myF执行结束与下一个myF开始之间的时间间隔,可以这么做:

    const interval = 1000;
    (function x(){
    	setTimeout(function (){
    		myF(function(){
    			x();
    		});
    	},interval)
    }());
    

    上面声明了一个名为x的立即执行函数,里面有个定时器,一秒之后将调用自己的函数,而自己的函数执行完毕后,又会调用作为回调函数的匿名函数,继续执行x,重复上面的过程。

    查询和读写文件之路径处理

    Node中有一组流API,可以像处理网络流一样处理文件。流API很方便,但要求只能以连续的方式处理文件。如果需要在指定位置进行读写,流API就行不通了,所以需要用到一些更底层都操作。

    本文主要内容:

    • 如何打开文件
    • 如何读取文件的指定部分
    • 如何写入文件
    • 如何关闭文件

    Node与文件有关的API很多与UNIX很像,使用文件描述符的方法就和UNIX中的一样。文件描述符句柄在Node中也是一个整数,代表进程文件描述符表的某个入口的索引。

    有三个特殊的文件描述符:

    • 1:标准输入文件(只读流,可以读取数据)
    • 2:标准输出文件(对外输出流,输出数据)
    • 3:标准错误文件(对外输出流,输出错误信息)

    一旦进程启动完毕,就能使用上述几个文件描述符,他们并不对应实际的文件。与操作网络流相似,不能在文件内指定位置读写,使用文件描述符只能连续的读写,就是说,举个例,在写的时候就不能去更改已经写入的东西了。

    文件则不受这样的限制。

    处理路径

    几乎所有对文件的操作都需要对文件路径进行处理,所以需要掌握。

    路径分为绝对路径和相对路径,这个就不再赘述。

    在Node中可以使用字符串处理路径,但是,例如当你想连接路径的不同部分时,有些是以“/”结尾而有些不是,且不同操作系统路径分隔符也不同。。。

    但是Node中有一个path模块,可以帮助你规范化、连接和解析路径,还有其他好用的一些功能。该模块只是对字符串进行处理,二不需要通过和文件系统交互来进行一些验证或功能。

    规范化路径

    由用户输入或者由配置文件获取的路径,还有多个路径连接得到的路径通常都需要规范化。

    可以使用path模块中的normalize()函数来处理,他能处理.、//等等字符。

    例如:

    const path = require("path");
    path.normalize("/foo/bar//baz/x/..");
    // 处理返回'/foo/bar/baz/x'
    

    连接路径

    使用path.join()可以连接任意个路径字符串:

    const path = require(’path’);
    path.join(/foo’,’bar’,‘x/y’);
    // 处理返回:'/foo/bar/x/y'
    // 会自动规范化
    

    解析路径

    使用path.resolve()将多个路径解析为规范化的绝对路径。该函数的作用是,就像对路径挨个cd一样,不过,这里的路径可以为文件,并且可以不存在,只是对路径字符串的处理。

    const path = require('path');
    path.resolve('/foo/bar','./baz'); // /foo/bar/baz
    path.resolve('/foo/bar','/tmp/file/'); // /tmp/file
    

    如果解析结果不是绝对路径,那么会将当前工作目录的路径加到前面。

    查找两个绝对路径之间的相对路径

    path.relative(/data/x/y/z‘,’/data/x/a/b’);
    // => ../../a/b
    

    提取路径的组成部分

    如果你在’/foo/my.md’,你想获取’/foo’里面的内容,或者读取相同目录下的其他文件,可以使用dirname获取文件路径的目录部分。

    path.dirname("/foo/bar/my.md"); // /foo/bar
    

    而提取文件名,可以用basename。这个函数第一个接受一个路径,返回最后一个/后面的所有内容。

    他还有第二个可选参数,如果你传入文件的扩展名,那么就会返回扩展名而没有文件名。但是这样很鸡肋,你必须事先知道扩展名才能获取到。

    所以还可以用extname(),这样可以直接拿到扩展名。但是注意:

    /foo/x => ""
    /foo/x.html => ".html"
    /foo/. => ""
    /foo/x. => "."
    

    确定路径是否存在

    使用exist(),返回的是布尔值。

    注意,Node 0.8之后fs.exists代替了path.exist,二者在语义上没有区别

    const fs = require('fs');
    fs.exists("路径",function(e){
    	// 回调,e是exists的返回值,布尔值
    })
    

    因为path.exists()进行了IO操作,所以是异步的,需要回调函数。而path.existsSync(),是同步的,功能一样,但不需要回调,而是直接返回返回值。

    文件操作-fs模块

    const fs = require('fs');
    

    引入fs模块之后,可以利用其中的接口进行文件操作。

    查询文件的统计信息

    可以使用 fs.stat函数查询,第一个参数为文件路径,第二个参数为查询完成之后的回调函数,回调函数接受一个参数,为查询的返回值。

    返回值有文件的基本信息,具体就不列了,看文档即可。

    该函数会将stats类的一个实例作为返回值传给回调,通过这个实例,我们可以调用以下函数:

    • isFile() :如果是标准文件而不是目录、套接字、符号链接或者设备的话,返回true。
    • isDirectory() :如果是目录返回true
    • isBlockDevice :是块设备返回true
    • isCharacterDevice() :如果是字符设备返回true
    • …多得就不举例了,各种格式都能查,看文档即可。

    打开文件

    在进行文件操作前,需使用 fs.open函数打开文件,然后使用文件描述符调用回调。然后可以在回调函数中读写。

    文件描述符按照我的理解,就是相当于把某一路径下的文件打开后,让一个变量指向他,这个变量是一个特殊对象,可以使用很多文件接口。

    第一个参数为文件路径;第二个是一个标志位,表明文件以何种模式打开,例如有r、r+、w、w+等等,表示读取、读写等以及数据流的位置在何处;第三个参数就是回调函数了,回调第一个参数为错误捕获,第二个参数为文件描述符。

    读取文件

    读取之前,请保证文件被打开,并且有一个缓冲区存放数据。(不知道缓冲区的可以参考此系列的第二篇博客)

    fs.open('./x.text','r',function y(err,fd){
        if(err) {throw err} // throw会自动return
        let bf = new Buffer(1024);
        let bfOffset = 0;
        let bfLength = bf.length;
        let filePosition = 100;
        fs.read(fd,bfOffset,bfLength,filePosition,function read(err,readBytes){
            if(err) {throw err};
            // readBytes为读入缓存区的字节数,如果为0代表到达文件末尾
            if(readBytes > 0) {
                console.log(bf.slice(0,readBytes));
            }
        })
    })
    

    上述代码在打开文件之后,从缓冲区的第100个字节(filePosition)开始,读取随后的1024个字节的数据。仔细读一下代码,在对应看变量,大家应该很好理解每个参数是什么作用了。

    注意:一旦将缓冲区传递给fs.open,缓冲区的控制权就移交给read命令,在没有返还控制权到你手上的时候,也就是回调函数没有被调用的时候,不应该对该缓冲区进行操作,不然会乱套,你在读或者写我也在读或者写,这就会造成想象得到和想象不到的很多问题。

    写入文件

    使用 fs.write,通过向其传递一个包含数据的缓冲区,可以实现写入。

    fs.open('./x.text',"a",function w(err,fd){
        let bf = new Buffer('writing this string'),
            bfPosition = 0,
            bfLength = bf.length,
            filePosition = null;
        fs.write(fd,bf,bfPosition,bfLength,filePosition,function(err,writen){
            if(err) throw err;
            console.log(writen);
        })
    })
    

    上面的代码中,第一行用追加模式打开一个文件,然后在第六行向文件写入,传递的缓冲区包含以下信息:

    • 准备写入缓冲区的数据
    • 待写入数据在缓冲区中的起始位
    • 写入数据的长度
    • 从文件中的什么位置开始写入数据
    • 写入完成后的回调

    filePosition的初值为null,表示从当前文件的游标处开始。

    注意:和读取操作一样,不要随便操作缓冲区

    关闭文件

    上述所有例子中,都没有关闭文件,因为上面的代码只需要运行一次,运行完毕之后,程序退出,文件自动被关闭。但是实际代码中,一个文件被打开,那么就一定要被关闭。

    如果想要关闭,那么就需要用到文件描述符(一个指向文件的对象)。

    简单使用:

    fs.close(fd,function(){
    	...
    })
    

    后续

    到目前为止,我们学习了用底层的基础语句来操作文件,而Node还有一种更高级的方式,就是使用 Stream。这个会在后面讲到,很快就要讲到了。

    创建和控制外部进程

    Node是被设计用来高效处理/O操作的,但正如你所见,某些类型的程序并不适用于这种模式。比如当用Node处理一个CPU密集型任务时可能会阻塞事件循环,所以应该将CPU密集任务分配给另一个进程去处理,释放事件循环,分担主进程的压力。

    Node中可以创建进程,并把这些进程当成当前启动他的进程的子进程。

    也许你不是很了解进程和线程,但这不是本文的重点,所以如果想要了解的更清楚,请查一查,简单了解一下。

    本文的内容在于:

    • 如果生成外部命令
    • 创建子进程
    • 通信
    • 终止
    • 在Node进程之外实现多任务操作

    提一句,后面用到的process对象是Node中的一个全局对象。

    执行外部命令

    使用child_process模块来执行外部的shell命令或者可执行文件。

    使用exec函数:

    const child_process = require('child_process');
    child_process.exec("命令","可选参数","回调");
    

    第一个参数为字符串类型的待执行的shell命令,第二参数是回调。回调应该有三个参数:error、stdout、stderr。如果出现错误,第一个参数接收,如果第一个参数不包含错误,第二个参数接收命令的的输出信息,最后一个包含命令的错误输出信息。

    三个参数为啥叫这个名字: std => standard ; out => output ; err => error

    可选参数是一个可传可不传的对象,他可以包含很多配置属性,比如超时时间、当前工作的目录(如果想命令到指定地方工作)、编码格式、环境变量等等等等,很多,可以自己查阅文档。例如,也许你想给子进程提供一组环境变量,作为父进程环境变量的扩展,如果直接修改process.env,就会导致每个模块都被修改,所以你可以复制process.env到一个本地变量,然后进行修改,最后把这个本地变量传入你的进程。

    可以在同一个目录创建两个文件,par.jschild.js,通过前者启动后者。

    // 这是par.js里面的代码
    const exec = require('child_process').exec;
    exec('node child.js' ,{env: { number: 123 }},function(err ,stdout ,stderr) {
        if(err) return;
        console.log(stdout);
        console.log(stderr);
    })
    
    // 这是child.js中的代码
    const number = process.env.number;
    console.log(number); // 输出一个字符串
    

    结合上面的介绍体会体会。

    生成子进程

    使用exec函数启动外部进程,并且在结束时回调一个函数,不过存在缺点:

    • 除了命令行参数和环境变量,没有通信手段。
    • 子进程的输出被缓存,导致无法对其进行流操作,可能导致内存耗尽。

    ** 不过child_process可以实现更精细的工作。上面的例子是执行了一个外部进程,这个进程是外部的,不是父进程创建的,而是启动的。所以通过创建一个子进程,我们可以获取更多的操作权限。

    创建子进程

    const spawn = require("child_process").spawn;
    const child = spawn("tail",['-f', '/var/log/system.log']);
    

    我们创建了一个子进程,依据传递中括号中的两个参数执行tail命令,tail会监视路径标识的那个文件,将输出的数据加载到stdout流。spawn会返回一个ChildProcess对象,他是一个句柄,封装了对实际进程的访问。

    句柄:一种特殊的智能指针,当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。句柄与普通指针的区别在于,指针包含的是引用对象的内存地址,而句柄则是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。这种间接访问对象的模式增强了系统对引用对象的控制。

    监听子进程的输出数据

    spawn返回的对象都具有一个 stdout属性,它以流的形式表示标准输出信息,可以在其上绑定事件监听:

    child.stdout.on('data', function(data){
    	console.log(data);
    })
    

    每当子进程将数据标准输出的时候,父进程就会触发事件,调用回调函数打印数据。

    除了标准输出之外,还有一个默认输出流(standard error),通常用于输出错误消息。

    例如,文件不存在,那么tail进程就会报错没有该文件。父进程通过监听stderr流获取通知:

    chlid.stderr.on('data', function(data){
        console.log("error: " + data);
    })
    

    向子进程发送信息

    标准输入流使用childProcess.stdin。子进程也可以使用process.stdin监听数据。

    默认process.stdin是暂停状态,所以先要恢复他

    stdin => standard input

    子进程程序(C.js):

    // 恢复标准输入流
    process.stdin.resume();
    process.stdin.on('data', function(data) {
    	console.log(data);
    	// 输出
    	process.stdout.write("childe data : " + data);
    })
    

    如果你执行运行这个程序,那么在窗口中会等待你输入一个值,按下回车之后会在屏幕上看到输出值。

    父进程程序(P.js):

    // 使用node进程创建一个子进程执行子进程程序
    const child = spawn('node', ['C.js']); // 中括号中可只填写一个路径,此时两个js文件在同一目录下,加上node拼接成一句完整命令
    // 随便发送一个数据到子进程
    child.stdin.write("childe");
    child.stdout.once('data', function(data){
        console.log("子进程传递过来的数据/n");
        console.log(data);
    })
    

    为什么要是用noce?如果子进程在不同时间多次发送数据,我们想获取所有数据,可以设置一个定时器,如果在定时器中使用on绑定事件,那么随着时间推移将要注册多个回调函数(回到函数不是覆盖而是追加!因为并没有清楚之前的回调函数!),这样每次触发事件,所有回调都会被调用。所以使用once是个良好的习惯。

    当子进程退出时获得通知

    child.on("exit", function(code) {
        // code: 子进程终止的退出码,非0则是不正常退出
        console.log(code);
    })
    

    退出之后,会自动打印终止的有关信息。

    如果子进程是被一个信号终止的(kill函数,看下面),那么相应的信号会作为第二个参数传递给回调。

    终止子进程

    信号是父子通信的简单方式,还可以用来终止进程。

    一般可以使用child.kill方法向子进程发送一个信号,默认发送的是’SIGTERM’,还可以手动传递一个信号给kill函数。

    尽管方法叫kill,但是发送的信息却不一定会终止进程!

    子进程可以定义程序重写信号默认行为:

    process.on('SIGUSR2', function() {
    	...
    })
    

    SIGKILL、SIGSTOP比较特殊,无法重写默认行为。

    读写数据流

    流,是Node中的一个抽象概念。

    细分可以分为两个:可读流和可写流。他们可以由一些Node对象实现(牢记流只是一个概念)。举几个流的例子:TCP套接字、文件读写。。。

    虽然之前也接触过,但本文以更正式的方式介绍。

    注意:流是由几个Node对象实现的抽象概念,创建或者获取流的方式取决于使用流的类型。例如,可以基于文件显式地创建一个可读流或者可写流。
    但是,服务器端的TCP套接字流只有在客户端连接时才能为你所用。
    除了可读流或可写流的特性外,一个对象还可以具有其他一些特殊的属性或行为。例如,文件可读流还包含“路径”属性,而这一属性在其他类型的流中也许并不存在。

    使用可读流

    可读流就像一个阀门,你可以对其进行控制,比如有暂停和恢复,这个后面会用到。而创建或获取可读流的方法取决于流的类型。

    等待数据

    通过监听data事件,每次读取到数据的时候都会触发回调,在回调中可以拿到数据(缓冲区或字符串,取决于编码)。

    可以使用stream.setEncoding()来指定编码格式。

    let rs = ...;
    rs.on('data', function(data){
        // 没有指定编码,默认作为缓冲区传送数据
        console.log(data);
    })
    
    rs.setEncoding('utf8');
    ...// 再监听data,就会作为字符串传送数据
    

    注意:因为UT℉-8字符可能是多字节的,所以当你希望获取UT℉-8字符时,也许需要在两个独立的“data”事件发生之后才能获得一个字符,当你将编码格式设置为utf8时,流仅会在一个字符完整的情况下传送它

    暂停与恢复流

    可以通过暂停,来停止接受数据:

    strem.pause
    

    之后,你边不会再接收到‘data’事件,这一特性可以帮你规避缓冲区问题。

    注意:暂停流在不同的情况下会被解释成不同的行为。例如,如果是一个文件流,Node就会停止从该文件中读取数据。如果流是一个TCP套接字,Node就不再会读取新的数据包,这会终止从其他终端来的数据包流。其他对象对pause的实现各不相同。

    想要恢复,那么使用stream.resume方法即可。

    何时终止流

    比如,当一个文件流到达文件末尾的时候,流就会发射end事件,代表终止。其余的类型,如果流工作结束,也会发射end事件。

    可读流终止之后,你就不会再接收到data事件了。

    使用可写流

    可写流是一个抽象概念,可以向其发送数据。

    将数据写入流

    可以通过传递缓冲区或字符串来写入数据。

    let ws = ...;
    ws.write("This is the data");
    

    还可以在write的第二个参数传入编码格式,不然默认utf-8.

    也可以传入缓冲区:

    const bf = new Buffer('this is the buffer');
    ws.write(bf);
    

    发送过程

    一旦写入数据,那么数据就会被送到核缓冲区(数据需要排队等待写入,这里就相当于等候室),如果此时没有位置了,就会把多出来的数据传入到一个队列,这个队列在内存中。通过write函数的返回值可以知道数据正在送往那儿(如果送往核缓冲区,返回true,如果是送往内存,返回false,后面会知道有什么用)。

    等待流被清空

    Node不会在I/O操作上产生注射,所以它也不会在读写上产生阻塞。比如,在调用写入函数的时候,你可以知道数据正是保存在哪儿。

    当流的核缓冲区终于不再满员,成功被刷新,就会发射drain事件。后面你会看到怎么通过这些操作限制内存增长,提高性能。

    下一篇将举例,您会有更深刻的理解。

  • 相关阅读:
    课后题:冒泡法排序
    LSTM -长短期记忆网络(RNN循环神经网络)
    数组扁平化的方法
    【2023最新B站评论爬虫】用python爬取上千条哔哩哔哩评论
    如何取消Blazor Server烦人的重新连接?
    Altium Designer20.2.3安装详解
    亲测可用fiddler手机抓包配置代理后没有网络
    RabbitMQ 入门系列:7、保障消息不重复消费:产生消息的唯一ID。
    vuex 中使用了modules,如何在页面中调用actions里面的方法
    LLMs之RAG:利用langchain实现RAG应用五大思路步骤—基于langchain使用LLMs(ChatGPT)构建一个问题回答文档的应用程序实战代码
  • 原文地址:https://blog.csdn.net/qq_51574759/article/details/126963870