zoukankan      html  css  js  c++  java
  • 了不起的Nodejs学习笔记(前五章)

    了不起的Nodejs学习笔记(前五章)

    五大部分组成

    • Node核心设计理念
    • Node核心模块API
    • Web开发
    • 数据库
    • 测试

    一、安装与概念

    1、执行文件

    • Node.js通过node命令来执行Node脚本

    • 创建server.js

      var http = require('http');
      var server = http.createServer(function (req,res){
          res.writeHead(200);
          res.end('Hello World');
      });
      server.listen(3000);
      
    • 执行命令:node server.js

    • 浏览器输入:https://localhost:3000

    2、NPM

    2.1、安装模块

    • 执行以下命令

      mkdir my-project/
      cd my-project/
      npm install colors
      
    • 验证安装成功可以查看 my-project/node_modules/colors目录

    • 创建index.js

      require('colors');
      console.log('smashing node'.rainbow)
      
    • 执行命令

      node index.js
      # 输出:smashing node  (彩色字体)
      

    2.2、自定义模块

    • 创建package.json文件

      • 好处
        • 不需要将整个node_modules目录发给别人
        • 方便记录所依赖模块的版本号
        • 分享更简单(npm publish发布到NPM库)
    • 创建项目

      mkdir my-project/
      cd my-project/
      touch package.json
      
    • 编辑package.json

      {
          "name":"my-colors-project",
          "version":"0.0.1",
          "dependencies":{
              "colors":"1.4.0"
          }
      }
      

      文件为json文件,遵循JSON格式。

    • 创建index.js

      require('colors');
      console.log('smashing node'.rainbow)
      
    • 执行命令

      npm install
      node index # 注意:这里文件名不需要加'.js'后缀
      
    • 发布

      • 编辑package.json

        {
            "name":"my-colors-project",
            "version":"0.0.1",
            "main":"./index",
            "dependencies":{
                "colors":"1.4.0"
            }
        }
        
        • 当别人使用require('my-project')时,为了能够让Node知道该载入哪个文件,我们可以使用main属性来指定
        • 查看package.json文件所有的属性文档,可以使用命令:npm help json
        • 如果不想发布模块,在package.json文件加入"private":"true"。可以避免误发布
      • 发布模块

        npm publish
        

    2.3、安装二进制工具包

    有的项目分发的Node编写的命令行工具。安装时要增加-g标志

    举例来说,Web框架express就包含一个用于创建项目的可执行工具

    npm install -g express
    

    安装后,执行以下命令

    # 创建目录
    mkdir my-site
    # 进入目录
    cd my-site
    express
    

    如果想要发布此类脚本。发布时,在package.json文件添加"bin":"./path/to/script"项,并将其值指向可执行的脚本或者二进制文件。

    2.4、浏览NPM仓库

    • npm search 模块名

      • 该命令会在已发布模块的name、tags以及description字段中搜索此关键字,并返回匹配的模块。
    • npm view 模块名

      • search命令找到感兴趣模块后,通过该命令查看package.json文件及与NPM相关的属性
    • npm help

      • 可以查看某个NPM命令的帮助文档
      • npm help publish就会叫你如何发布模块

    二、JavaScript概览

    1、介绍

    • 基于原型、面向对象、弱类型的动态脚本语言
    • 根据ECMAScript语言标准来实现。

    2、基础

    2.1、类型

    • 基本类型:访问基本类型,访问的是值
      • number
      • boolean
      • string
      • null
      • undefined
    • 复杂类型:访问复杂类型,访问的是对值的引用
      • array
      • function
      • object

    2.2、类型的困惑

    要在JavaScript中准确无误的判断变量值的类型并非易事。

    • 字符串

      // 创建字符串的两种形式
      var a = 'woot';
      var b = new String('woot');
      a + b; // 'woot woot'
      
      // 用 typeof 和 instanceof 操作符判断
      typeof a; // 'string'
      typeof b; // 'object'
      a instanceof String; // false
      b instanceof String; // true
      
      // == 与 ===
      a == b; // true
      a === b; // false
      
      // 事实上,两个变量都是字符串
      a.substr == b.substr; // true
      

      考虑到以上差异,建议始终通过直观方式进行定义,避免使用new。

    • 条件表达式中的一些特定值会被判定为false

      // null、undefined、' '、0
      var a = 0;
      if(a){
          // 这里始终不会被执行
      }
      a == false; // true
      a === false; // false
      
      // typeof不会把null识别为类型为null
      typeof null = 'object'; // true
      // 数组也不例外,就算是通过[]定义数组也是如此
      typeof [] = 'object'; // true
      
      // 判断数组
      Object.prototype.toString.call([]) == '[object Array]';
      
      // instanceof Array这种方式只适用于与数组初始化在相同上下文中才有效
      

    2.3、函数

    在JavaScript中,函数最为重要。

    /** 以下属于一级函数:可以作为引用存储在变量中,随后可以像其他对象一样,进行传递 **/
    var a = function(){}
    console.log(a); // 将函数作为参数传递
    
    /** JavaScript中所有函数都可以进行命名。有一点很重要,就是要能区分出函数名和变量名 **/
    var a = function a (){
        console.log('function' == typeof a); // true
    }
    a();
    
    /** THIS、FUNCTION#CALL、FUNCTION#APPLY **/
    // 下述代码中函数被调用时,this的值是全局对象。在浏览器中,就是windows对象
    function a (){
        console.log(window == this); // true
    }
    a();
    
    // 调用以下函数,使用.call和.apply方法可以改变this的值
    function a () {
        console.log(this.a == 'b'); // true
    }
    a.call({a:'b'});
    
    // call和apply的区别在于,call接受参数列表,而apply接受一个参数数组
    function a (b,c){
        console.log(b == 'first'); // true
        console.log(c == 'second'); // true
    }
    a.call({a:'b'},'first','second');
    a.apply({a:'b'},['first','second']);
    

    2.4、函数的参数数量

    该属性指明函数声明时可接受的参数数量。在JavaScript中,该属性名为length

    var a = function (a,b,c);
    a.length == 3; // true
    
    // 尽管这在浏览器端很少用,但,在流行的Node.js框架就是通过此属性来根据不同参数个数提供不同的功能。
    

    2.5、闭包

    在JavaScript中,每次函数调用时,新的作用域就会产生。

    2.5.1、作用域
    // 在某个作用域中定义变量只能在该作用域或其内部作用域(该作用域中定义的作用域)中才能访问到
    var a = 5;
    function woot () {
        console.log(a == 5); // false
        var a = 6;
        function test(){
            console.log(a == 6); // true
        }
        test();
    };
    woot();
    
    2.5.2、自执行函数
    // 自执行函数是一种机制,通过这种机制声明和调用一个匿名函数,能够达到仅定义一个新作用域的作用
    var a = 3;
    (function () {
    	var a = 5;    
    })();
    console.log(a == 3); // true
    
    // 自执行函数对声明私有变量是有用的,这样可以让私有变量不被其他代码访问。
    

    2.6、类

    // JavaScript中没有`class`关键字。类只能通过函数定义
    function Animal(){}
    // 要给所有Animal的实例定义函数,可以通过prototype属性来完成
    Animal.prototype.eat = function (food) {
        // eat method
    }
    // 值得一提的是,在prototype的函数内部,this并非像普通函数那样指向global对象
    // 而是指向通过该类创建的实例对象
    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.getName = function(){
        return this.name;
    }
    var animal = new Animal('tobi');
    console.log(animal.getName() == 'tobi'); //true
    

    2.7、继承

    JavaScript有基于原型的继承的特点。通常,你可以通过以下方式来模拟类继承。

    // 定义Animal
    function Animal(name) {
        this.name = name;
    }
    // Animal类方法eat
    Animal.prototype.eat = function (food) {
        console.log("吃:"+ food)
    }
    // 定义Ferret类:一个要继承自Animal的构造器
    function Ferret () {};
    // 要定义继承链,首先创建一个Animal对象,然后将其赋值给Ferret.prototype
    Ferret.prototype = new Animal();
    // 随后,可以为子类定义属性和方法。
    Ferret.prototype.type = 'domestic';
    // 通过prototype重写和调用父类函数
    Ferret.prototype.eat = function (food) {
        // 第一种调用
        Animal.prototype.eat(food);
        // 第二种调用
        Animal.prototype.eat.call(this,food)
        // ferret特有的逻辑写在这里
    }
    // 构建ferret
    var ferret = new Ferret();
    // 打印type属性值
    console.log(ferret.type);
    // 调用父类方法
    ferret.eat('冰淇凌');
    
    // 这项技术很赞,它是同类方案中最好的(相比其他函数式技巧),而且它不会破坏instanceof操作符的结果
    var animal = new Animal();
    console.log(animal instanceof Animal); // true
    console.log(animal instanceof Ferret); // false
    
    var ferret1 = new Ferret();
    console.log(ferret1 instanceof Animal); // true
    console.log(ferret1 instanceof Ferret); // true
    
    // 它最大的不足就是声明继承的时候创建的对象总要进行初始化`Ferret.prototype = new Animal()`
    // 一种解决该问题的方法就是在构造器中添加判断条件
    function Animal (a) {
        if (false !== a) return;
        // 初始化
    }
    Ferret.prototype = new Animal(false);
    // 另外一种方法就是再定义一个新的空构造器,并重写它的原型
    function Animal () {
        // constructor struff
    }
    function f () {};
    f.prototype = Animal.prototype;
    Ferret.prototype = new f;
    // V8提供了更简洁的解决方案,见后文
    

    2.8、异常捕获

    /** try/catch允许进行异常捕获。**/
    // 以下代码会抛出异常
    var a = 5;
    a() // TypeError: a is not a function
    
    // 当函数抛出错误时,代码就停止执行了
    function () {
        throw new Error('hi');
        console.log('hi') // 这里永远不会被执行到
    }
    
    // 若使用try/catch则可以进行错误处理,并让代码继续执行
    function () {
        var a = 5;
        try{
            a();
        }catch (e) {
            e instanceof Error; // true
        }
        console.log('you get here')
    }
    

    3、V8中的JavaScript

    3.1、OBJECT#KEYS

    // 想要获取下述对象的键(a和c)
    var a = {a:'b',c:'d'}
    // 通常会使用如下迭代的方式
    for(var i in a){
        console.log(i); // a // c
    }
    // 通过对键进行迭代,可以将它们收集到一个数组中。
    // 不过如果采用如下方式对Object.prototype进行过扩展
    Object.prototype.c = 'd'
    // 为了避免再迭代过程中把c也获取到,就需要使用hasOwnProperty来进行检查
    // hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)
    for (var i in a){
        if (a.hasOwnProperty(i)){}
    }
    // 在V8中,要获取对象上的所有的自有键,还有更简单的方法
    var a = {a:'b',c:'d'}
    Object.keys(a); // ['a','c']
    

    3.2、ARRAY#ISARRAY

    // 对数组使用typeof操作符会返回object
    // 下面介绍如何判断数组
    console.log(Array.isArray(new Array())); // ture
    console.log(Array.isArray([])); // ture
    console.log(Array.isArray(null)); // false
    console.log(Array.isArray(arguments)); // false
    

    3.3、数组方法

    // 1、遍历数组:forEach
    [1,2,3].forEach(function (v) {
        console.log(v);
    })
    // 打印:
    // 1
    // 2
    // 3
    
    // 2、过滤数组元素:filter
    let filterRes = [1,2,3].filter(function (v) {
        return v < 3;
    });
    console.log(filterRes) // [ 1, 2 ]
    
    // 3、改变数组中每个元素的值:map
    let mapRes = [5,10,15].map(function (v) {
        return v * 2;
    })
    console.log(mapRes) // [ 10, 20, 30 ]
    
    // 4、V8还提供了不常用的方法,如reduce、reduceRight以及lastIndexOf
    

    3.4、字符串方法

    // 移除字符首末的空格
    console.log(' hello '.trim()); // 'hello'
    

    3.5、JSON

    // V8提供了JSON.stringify和JSON.parse方法来对JSON数据进行解码和编码
    // JSON是一种编码标准和JavaScript对象字面量很相近,它用于大部分的Web服务和API服务:
    var obj = JSON.parse('{"a":"b"}')
    console.log(obj.a == 'b');  // true
    

    3.6、FUNCTION#BIND

    // .bind 允许改变对this的引用
    function a () {
        console.log(this.hello == 'world'); // true
    }
    
    var b = a.bind({hello:'world'})
    b()
    

    3.7、FUNCTION#NAME

    // V8还支持非标准的函数属性名
    var a = function woot () {};
    console.log(a.name == 'woot'); // true
    
    // 该属性用于V8内部的堆栈追踪。
    // 当有错误抛出时,V8会显示一个堆栈追踪的信息,会告诉你是哪个函数调用导致了错误的发生
    var woot = function () {throw new Error();};
    woot()	// 此时错误信息没有 函数名
    
    var woot = function buggy () {throw new Error();};
    woot()	// 此时错误信息含有 函数名
    
    // 为函数命名有助于调试,因此,推荐始终对函数进行命名
    

    3.8、继承

    // _proto_ 使得定义继承链变得更加容易
    function Animal(){}
    function Ferret(){}
    Ferret.prototype._proto_ = Animal.prototype
    
    • 借助中间构造器
    • 借助OOP的工具类库。无须再引入第三方模块来进行基于原型继承的声明

    3.9、存取器

    你可以通过调用方法来定义属性,访问属性就使用__defineGetter__、设置属性就使用__defineSetter__

    // 以下例子,为所有的Date实例都添加了ago获取器,它会返回以自然语言描述的日期距离现在的时间间隔。
    // 简单地访问该属性就会调用事先定义好的函数,无须显式调用
    // 双下划线
    Date.prototype.__defineGetter__('ago',function(){
        var diff = (
            ( (new Date()).getTime() - this.getTime() ) / 1000
        ), 
            day_diff = Math.floor(diff / 86400);
        return (
            diff < 60 && "just now" ||
            diff < 120 && "1 minute ago" ||
            diff < 3600 && Math.floor(diff / 60 ) + " minute ago" ||
            diff < 7200 && "1 hour ago" ||
            diff < 86400 && Math.floor(diff / 3600 ) + " hour ago" ||
            day_diff == 1 && "Yesterday" ||
            day_diff < 7 && day_diff + " days ago" ||
            Math.ceil( day_diff / 7 ) + " weeks ago"
        )
    });
    
    var a = new Date('12/12/1990')
    console.log(a.ago);
    

    三、阻塞与非阻塞IO

    1、共享状态并发

    • 在Node中,需要对回调函数如何修改当前内存中的变量(状态)特别小心。

    • 除此之外,你要需要特别注意对错误的处理是否会潜在地修改这些状态,从而导致整个进程不可用。

    nodejs代码

    var books = ['笑傲江湖','天龙八部']
    function serveBooks () {
        // 给客户端的html
        var html = '<b>' + books.join('</b><br/><b>') + '</b>';
        // 修改状态
        books = [];
        return html;
    }
    

    PHP代码

    $books = array('笑傲江湖','天龙八部')
    function serveBooks () {
        $html = '<b>'.join($book,'</b><br/><b>').</b>;
        $books = array();
        return html;
    }
    
    • 以上两段代码,books是存放图书的数组,假设books就是状态,该数组用来将图书列表以HTML的形式返回给客户端
    • 以上两端代码,都在serveBooks函数中将books数组重置
    • 分别对两个服务发起各两次请求
    • Node会将完整的图书列表返回给第一个请求,而第二个请求则返回一个空的图书列表
    • PHP都能将完整的图书列表返回给两个请求
    • 两者区别在于架构
      • Node采用一个长期运行的进程。serveBooks再次被调用,此时books数组为空
      • Apache会产出多个线程(每个请求一个线程),每次都会刷新状态。在PHP中当解释器再次执行时,变量$books会被重新赋值。

    2、阻塞

    尝试区分下面PHP代码和Node代码有什么不同?

    PHP

    print('Hello');
    sleep(5);
    print('World')
    

    Node

    console.log('Hello')
    setTimeout(function () {
        console.log('World')
    },5000)
    
    • 区别

      • 语义区别(Node.js使用回调函数)

      • 阻塞和非阻塞

        • PHP:sleep()阻塞了线程的执行。当程序进入休眠,就什么事情也不做

        • Node.js使用了事件轮询,因此setTimeout时非阻塞的。参看以下代码:

          console.log('Hello')
          setTimeout(function () {
              console.log('World')
          },5000)
          console.log('Bye')
          // 输出:
          // Hello
          // Bye
          // World
          
    • 事件轮询意味着什么?

      本质上讲,Node会先注册事件,随后不停地询问内核这些事件是否已经分发。

      当事件分发时,对应的回调函数就会被触发。然后继续执行下去。

      如果没有事件触发,则继续执行其他代码,直到有新事件时,再去执行对应的回调函数。

    • Node并发实现也采用了事件轮询

      所有像http、net这样的原生模块中的IO部分也都采用了事件轮询技术

      tomeout机制中Node内部会不停地等待,并当超时完成时,触发一个和文件描述符相关的通知

    • 文件描述符

      文件描述符是抽象的句柄,存有对打开的文件、socket、管道等的引用

      本质上,Node接受到从浏览器发来的HTTP请求时,底层的TCP连接会分配一个文件描述符

      随后,如果客户端向服务器发送数据,Node就会收到该文件描述符,然后触发JavaScript回调函数

    3、单线程的世界

    有一点很重要,Node是单线程的。在没有第三方模块的帮助下是无法改变这一事实的。

    var start = Date.now();
    setTimeout(function () {
        console.log(Date.now() - start);
        for (var i=0;i<1000000000;i++){}
    },1000)
    setTimeout(function () {
        console.log(Date.now() - start);
    },2000)
    // 打印 
    // 1000
    // 3738
    
    • 以上程序显示了每个setTimeout执行的时间间隔,其结果和代码中设定的值并不相同
    • 原因:
      • 事件轮询被JavaScript代码阻塞。
      • 当第一个事件分发时,会执行JavaScript回调函数。
      • 由于回调函数需要执行很长时间(循环次数很多),所以下一个事件轮询执行时间远超2秒
      • 因此,JavaScript的timeout并不能严格遵守时钟设置

    Node如何做到高并发?

    • 所有同步任务都在主线程上执行,形成一个执行栈
    • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
    • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
    • 主线程不断重复上面的第三步。

    4、错误处理

    首先,很重要的一点。Node应用依托在一个拥有大量共享状态的大进程中。

    举例来说,如果某个回调函数发生错误,整个进程都会遭殃。

    var http = require('http')
    http.createServer(function () {
        throw new Error('错误不会被捕捉')
    }).listen(3000)
    

    因为错误未被捕获,若访问Web服务器,进程就会崩溃。

    • uncatchException处理器

      // 添加了uncaughtException处理器,进程不会退出,并且之后的事情你都能掌控
      process.on('uncaughtException',function (err) {
          console.error(err);
          process.exit(1); // 手动退出
      })
      // 以上例子中,行为方式和分发error事件的API行为方式一致
      
    • error事件

      var net = require('net')
      net.createServer(function (connection) {
          connection.on('error',function (err){
              // err是一个错误对象
          })
      }).listen(400);
      // Node中许多像http、net这样的原生模块都会分发error事件。如果该事件未处理,就会抛出未捕获的异常
      

    除了uncaughtException和error事件外,绝大部分Node异步API接受的回调函数,第一个参数都是错误对象和NULL。

    var fs = require('fs')
    fs.readFile('/etc/passwd',function (err,data){
        if (err) return console.error(err);
        console.log(data);
    })
    

    错误处理中,每一步都很重要,因此它能让你书写更安全的程序,并且不丢失出发错误的上下文信息。

    5、堆栈追踪

    在JavaScript中,当错误发生时,在错误信息中,可以看到一系列的函数调用,这称为堆栈追踪。

    function c () {
        b();
    }
    function b () {
        a();
    }
    function a () {
        throw new Error('here');
    }
    c();
    
    // 运行以上代码,可以看到堆栈追踪信息。
    // 可以清晰看见导致错误发生的函数调用路径
    
    Error: here
        at a (e:demo	est	est.js:8:11)
        at b (e:demo	est	est.js:5:5)
        at c (e:demo	est	est.js:2:5)
        at Object.<anonymous> (e:demo	est	est.js:10:1)
        at Module._compile (internal/modules/cjs/loader.js:959:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
        at Module.load (internal/modules/cjs/loader.js:815:32)
        at Function.Module._load (internal/modules/cjs/loader.js:727:14)
        at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
        at internal/main/run_main_module.js:17:11
    
    // 引入事件轮询
    function c () {
        b();
    }
    function b () {
        a();
    }
    function a () {
        setTimeout(function () {
            throw new Error('here');
        },10)
    }
    c();
    
    // 可以发现,有价值的堆栈信息丢失了
    // 堆栈信息显示的是从事件轮询开始的
    
    Error: here
        at Timeout._onTimeout (e:demo	est	est.js:10:15)
        at listOnTimeout (internal/timers.js:531:17)
        at processTimers (internal/timers.js:475:7)
    

    同理,要捕获一个未来才会执行到的函数所抛出的错误是不可能的。

    这会直接抛出未捕获的异常,并且catch代码块永远都不会被执行。

    try{
        setTimeout(function () {
            throw new Error('here');
        },10)
    } catch(e) {}
    

    这就是为什么Nodejs中,每步都要正确进行错误处理的原因。

    一旦遗漏,就会发现错误后很难追踪,因为上下文信息都丢失了。

    四、Node中的JavaScript

    1、global对象

    • 在浏览器中,全局对象指window对象。在window对象上定义的任何内容都可以被全局访问到。

      • setTimeout:其实就是window.setTimeout
      • document:其实就是window.document
    • Node中有两个类似但却各自代表不同含义的对象

      • global:和window一样,任何global对象上的属性都可以被全局访问到。
      • process:所有全局执行上下文中的内容都在process对象中
        • 在浏览器中,只有一个window对象。
        • 在Node中,也只有一个process对象。

    2、实用的全局对象

    • process.nextTick:将一个函数的执行时间规划到下一个事件循环中
    • console:最早由Firefox中辅助开发的插件-Firebug实现。
      • console.log
      • console.error

    3、模块系统

    • JavaScript语言标准中并未为模块依赖以及模块独立定义专门的API
    • Node摒弃了采用定义一堆全局变量的方式,转而引入模块系统.
      • 模块系统三个核心的全局对象
        • require
        • module
        • exports

    4、绝对和相对模块

    • 绝对模块:指Node通过在其内部node_modules查找到的模块,或者Node内置的如fs这样的模块。
      • colors:修改了String.prorotype,因此无须暴露API。require('colors')
      • fs:暴露了一系列函数。var fs = require('fs');fs.readFile(...)
    • 相对模块:将require指向一个相对工作目录中的JavaScript文件。require(./module)

    5、暴露API

    要让模块暴露一个API称为require调用的返回值,就要依靠module和exports这两个全局变量

    • 默认情况下,每个模块都会暴露出一个空对象

      // module_a.js
      exports.name = 'zhangsan'
      exports.data = 'data'
      
      var pv = 5;
      exports.getPv = function () {
          return pv;
      }
      
      // index.js
      var a = require('./module_a')
      console.log(a.name)
      console.log(a.data)
      console.log(a.getPv())
      
      • 以上例子,exports其实就是对module.exports的引用,其在默认情况下是一个对象。

    要是在该对象上逐个添加属性无法满足你都需求,你还可以彻底重写module.exports。

    • 常见的将模块中构造器暴露出来的例子

      // person.js
      module.exports = Person;
      
      function person (name) {
          this.name = name;
      }
      
      Person.prototype.talk = function () {
          console.log('我的名字:',this.name)
      }
      
      // index.js
      var Person = require('./person')
      var john = new Person('john')
      john.talk();
      
      • 以上是一个具备JavaScript OOP风格的Node.js模块的例子
      • 在index.js文件中,不再接受一个对象作为返回值,而是函数,归功于module.exports的重写

    6、事件

    Node.js中的基础API之一就是EventEmitter。

    无论是在Node中还是浏览器中,大量代码都依赖于所监听或者分发的事件。

    • 浏览器中负责处理事件相关的DOM API包括

      • addEventListener
      • removeEventListener
      • dispatchEvent
      • 它们还用在一系列从window到XMLHTTPRequest等的其他对象上
    • Node暴露了Event EmitterAPI

      • on

      • emit

      • removeListener

      • 以process.EventEmitter形式暴露

        var EventEmitter = require('events').EventEmitter,a = new EventEmitter;
        a.on('event',function () {
            console.log('event called')
        })
        a.emit('event')
        
      • 添加到自己的类

        var EventEmitter = process.EventEmitter,MyClass = function (){}
        MyClass.prototype.__proto__ = EventEmitter.prototype;
        // 这样,所有MyClass的实例都具备了事件功能
        var a = new MyClass();
        a.on('某一事件',function () {
            // 做些什么
        })
        

    ​ 事件是Node非阻塞设计的重要体现。Node通常不会直接返回数据(因为这样可能会在等待某个资源的时候发生线程阻塞),而是采用分发事件来传递数据的方式。

    ​ 以HTTP服务为例。请求到达时,Node会调用一个回调函数,这个时候数据可能不会一下子都到达。

    // 当用户提交表单时,通常会监听请求的data和end事件
    http.Server(function (req,res) {
        var buf = '';
        req.on('data',function (data) {
            buf += 'data';
        });
        req.on('end',function () {
            console.log('数据接收完毕!')
        })
    })
    // 将请求数据内容进行缓冲(data)事件
    // 等到所有数据都接收完毕(end事件)再对数据进行处理
    

    7、buffer

    除了模块之外,Node还弥补了语言另外一个不足之处,对二进制数据的处理。

    • buffer

      • 是一个表示固定内存分配的全局对象(也就是说,要放到缓冲区中的字节数需要提前定下)

      • 它好比一个由八位字节元素组成的数组,可以有效地在JavaScript中表示二进制数据

      • 该功能的一部分作用就是可以对数据进行编码转换

        // 创建一副用base64表示的图片,将其作为二进制PNG图片的形式写入到文件中
        // buffers/index.js
        var mybuffer = new Buffer('.....','base64')
        console.log(mybuffer)
        require('fs').writeFile('logo.png',mybuffer)
        
        // 运行
        node index
        open logo.png
        

    五、命令行工具(CLI)以及FS API

    1、需求

    • 需求
      • 程序需要在命令行运行。
        • 要么通过node命令来执行
        • 要么直接执行,然后通过终端提供交互给用户进行输入、输出。
      • 程序启动后,需要显示当前目录列表
      • 选择某个文件时,程序需要显示该文件内容
      • 选择一个目录时,程序需要显示改目录下的信息
      • 运行结束后程序退出
    • 项目步骤
      • 创建模块
      • 决定采用同步fs还是异步fs
      • 理解什么是流(Stream)
      • 实现输入输出
      • 重构
      • 使用fs进行文件交互
      • 完成

    2、首个Node项目

    2.1、创建模块

    package.json

    {
        "name":"file-explorer",
        "version":"0.0.1",
        "description":"A command-file file explorer!"
    }
    
    # 验证package.json有无问题
    npm insall
    

    2.2、同步还是异步

    • fs模块是唯一一个同时提供同步和异步API的模块
    • 为了学习单线程中创建能够处理高并发的高效程序,就得采用异步、事件驱动的程序。
    // index.js
    var fs = require('fs')
    fs.readdir(__dirname,function (err,files){
        console.log(files);
    })
    // 输出:[ 'index.js', 'package-lock.json', 'package.json' ]
    

    2.3、流(stream)

    process全局对象包含了三个流对象,分别对应三个UNIX标准流

    • stdin:标准输入。可读流。
    • stdout:标准输出。可写流。
    • stderr:标准错误。可写流。

    stream对象和EventEmitter很像,事实上,前者继承自后者。

    2.4、输入和输出

    var fs = require('fs')
    fs.readdir(process.cwd(), function (err, files) {
        // 为了输出友好,首先输出一个空行
        console.log('');
        if (!files.length) {
            // 如果files数组为空,告知用户当前目录没有文件
            // 33[31m 与 33[39m 为了让文本呈现红色
            // 
     换行
            return console.log('    33[31m No files to show!33[39m
    ');
        }
        console.log('    Select which file or directory you want to see
    ');
    
        function file(i) {
            // 获取文件名
            var filename = files[i];
            // fs.stat获取文件或目录的元数据
            fs.stat(__dirname + '/' + filename, function (err,stat){
                // 判断是不是目录,用颜色区分目录和文件
                if(stat.isDirectory()){
                    console.log('    ' +i+ '    33[36m' + filename + '/33[39m');
                }else{
                    console.log('    ' +i+ '    33[90m' + filename + '33[39m');
                }
                // 计数器不断递增,与此同时,检查是否有未处理的文件
                i++;
                if(i == files.length){
                    console.log('');
                    // 无需换行,让用户可以直接在提示语后进行输入
                    process.stdout.write('    33[33mEnter your choice: 33[39m');
                    // 等待用户输入
                    process.stdin.resume();
                    // 设置流编码为utf8,这样就能支持特殊字符了
                    process.stdin.setEncoding('utf8')
                }else{
                    // 还有未处理的文件,则递归递归调用该函数来进行处理
                    file(i);
                }
            });
        }
        file(0)
    })
    // 直到列出所有文件,用户输入完毕后,下一步串行处理
    

    2.5、重构

    // 要做重构,我们认为从几个常量(如:stdin和stdout)创建快捷变量开始
    var fs = require('fs'),stdin = process.stdin,stdout = process.stdout;
    fs.readdir(process.cwd(), function (err, files) {
        function file(i) {
            var filename = files[i];
            fs.stat(__dirname + '/' + filename, function (err,stat){
                if(stat.isDirectory()){
                    console.log('    ' +i+ '    33[36m' + filename + '/33[39m');
                }else{
                    console.log('    ' +i+ '    33[90m' + filename + '33[39m');
                }
                if(++i == files.length){
                    read()
                }else{
                    file(i);
                }
            });
        }
        // 由于代码是异步的,因此,随着含数量的增长(特别是流控制层的增加),过多的函数嵌套会让程序的可读性变差
        // 为避免此类问题,应为每一个异步操作预先定义一个函数
        // 首先,抽离一个读取stdin函数
        function read() {
            console.log('');
            stdout.write('    33[33mEnter your choice: 33[39m');
            stdin.resume();
            stdin.setEncoding('utf8');
            // 用户选择需要读取的文件,监听其data事件
            stdin.on('data',option);
        }
        // 检查用户的输入是否匹配files数组中的下标
        // files数组是fs.readdir回调函数的一部分
        // 将utf-8编码的字符串类型data转化为Number类型,方便做检查
        function option (data) {
            if(!files[Number(data)]){
               stdout.write('    33[33mEnter your choice: 33[39m')
            }else{
                // 如果检查通过,确保再次将流暂停(回到默认状态),以便fs操作后,程序能顺利退出
               stdin.pause(); 
            }
        }
        file(0)
    })
    

    2.6、fs操作文件

    var fs = require('fs'),stdin = process.stdin,stdout = process.stdout;
    var stats = [];
    fs.readdir(process.cwd(), function (err, files) {
        function file(i) {
            var filename = files[i];
            fs.stat(__dirname + '/' + filename, function (err,stat){
                // 保存Stat对象
                stats[i] = stat;
                if(stat.isDirectory()){
                    console.log('    ' +i+ '    33[36m' + filename + '/33[39m');
                }else{
                    console.log('    ' +i+ '    33[90m' + filename + '33[39m');
                }
                if(++i == files.length){
                    read()
                }else{
                    file(i);
                }
            });
        }
        function read() {
            console.log('');
            stdout.write('    33[33mEnter your choice: 33[39m');
            stdin.resume();
            stdin.setEncoding('utf8');
            stdin.on('data',option);
        }
        function option (data) {
            var filename = files[Number(data)];
            if(!files[Number(data)]){
               stdout.write('    33[33mEnter your choice: 33[39m')
            }else{
               stdin.pause(); 
               // 如果选择的是目录
               if(stats[Number(data)].isDirectory()){
                   fs.readdir(__dirname + '/' + filename,function(err,files){
                        console.log('');
                        // 包含几个文件
                        console.log('    (' + files.length + ' files)');
                        // 文件列表
                        files.forEach(function (file){
                        console.log('    -  '+file);
                        })
                        console.log('');
                   })
               }else{ // 如果选择的是文件
                   // 指定编码,得到的数据就是相应的字符串
                   fs.readFile(__dirname + '/' + filename,'utf8',function (err,data){
                       console.log('')
                       // 正则表达式添加一些辅助缩进后将文件内容进行输出
                       console.log('33[90m' + data.replace(/(.*)/g,'    $1') + '33[39m');
                   });
               }
            }
        }
        file(0)
    })
    

    3、对CLI一探究竟

    3.1、argv

    process.argv包含了所有Node程序运行时的参数值

    console.log(process.argv)

    • 第一个元素始终是 node,执行文件的目录
    • 第二个元素始终是执行文件的文件路径,紧接着是命令行后紧跟的参数

    console.log(process.argv.slice(2)):获取真正的元素,首先要将数组的前两个元素去掉

    3.2、工作目录

    • 获取当前工作目录process.cwd
    • 灵活的更改工作目录process.chdir('/')

    3.3、环境变量

    process.env

    通过process.env.SHELL变量轻松访问shell环境下的变量

    最常见的是process.env.NODE_ENV,控制是生产环境还是开发环境。

    3.4、退出

    要让一个应用退出,可以调用process.exit并提供一个退出代码。

    当错误发生时,要退出程序,这个时候最好是使用退出代码1

    process.exit(1)

    3.5、信号

    进程和操作系统进行通信的其中一种方式就是通过信号。

    比如,要让进程终止,可以发送SIGKILL信号。

    Node程序是通过在process对象上以事件分发的形式来发送信号的

    process.on('SIGKILL',funciton(){
        // 信号已收到  
    })
    

    3.6、ANSI转义码

    要在文本终端下控制格式、颜色以及其他输出选项,可以使用ANSI转义码

    // 以下代码文本周围添加的明显不用于输出的字符,称为非打印字符
    console.log('33[90m' + data.replace(/(.*)/g,'    $1') + '33[39m');
    // 33表示转义序列的开始
    // [ 表示开始颜色设置
    // 90表示前景色为亮灰色
    // m表示颜色设置结束
    

    ANSI转义码表(页面左侧可转中文)

    4、对fs一探究竟

    fs模块允许通过Stream API对数据进行读写操作。

    readFilewriteFile方法不同,它对内存的分配不是一次完成的。

    • 考虑一个例子:

    ​ 解析一个大文件,文件内容由上百万行逗号分隔文本组成。

    • 解决:
      • 方式一:
        • 要完整的读取该文件来进行解析,意味着要一次性分配很大的内存。
      • 方式二:
        • 更好的方式应该是一次只读取一块内容,以行尾结束符(" ")来切分,然后再逐块进行解析。
        • Node Stream就是对上述解决方案完美的实现,

    5 、Stream

    // 以下例子中
    // 回调函数必须等到整个文件读取完毕、载入到RAM、可用的情况下才会触发
    
    fs.readFile('test.txt',function (err,contents){
        // 对文件进行处理
    })
    
    // 以下例子
    // 每次会读取可变大小的内容块,并且每次读取后会触发回调函数
    var stream = fs.createReadStream('test.txt')
    stream.on('data',function (chunk){
        // 处理文件部分内容
    })
    stream.on('end',function (chunk){
        // 文件读取完毕
    })
    
    // 视频上传到Web服务
    // 网站访问日志
    

    6、监视

    Node允许监视文件或目录是否发生变化。

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

    例如用一种可以编译为CSS的语言书写CSS样式。监视源文件发生改变,则编译为CSS文件

    var stream = fs.createReadStream('test.txt')
    var fs = require('fs')
    // 获取工作目录下所有文件
    var files = fs.readdirSync(process.cwd());
    files.forEach(function (file){
        // 监听".css"后缀的文件
        if(/.css/.test(file)){
            fs.watchFile(process.cwd() + '/' + file, function(){
                console.log(' - ' + file + ' changed!')
            })
        }
    })
    // 除了fs.watchFile外,还可以使用fs.watch来监视整个目录
    
  • 相关阅读:
    C# 编码解码
    asp.net跨域问题
    C# crc16modbus
    c# 日志生成
    C# 对newtonsoft.json对象进行ascii排序
    C# 字节转结构体、结构体转字节
    按ascill排序参数
    C# Rsa加密(私钥加密、公钥解密、密钥格式转换、支持超大长度分段加密)
    Interview
    Leetcode
  • 原文地址:https://www.cnblogs.com/luckyzs/p/13496440.html
Copyright © 2011-2022 走看看