4 Star 11 Fork 16

Swot/learning

加入 Gitee
与超过 1400万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
NodeJSBascic.adoc 213.20 KB
一键复制 编辑 原始数据 按行查看 历史

NodeJS基础

目录

1. Node.js 入门

Node.js简介

什么是 Node.js

Node.js 简称 Node,是一个可以使 JavaScript 运行在服务器端的开发平台。

JavaScript 本是一种 Web 前端语言,Node.js 让 JavaScript 成为服务器端脚本语言。

Node.js 选择 JavaScript 作为实现语言的原因:

  • JavaScript满足CommonJS标准,符合事件驱动,用户较多且门槛较低;

  • Chrome 的 V8 引擎具有出色的性能。

Node.js 将 V8 引擎封装起来,作为服务器运行平台,以执行 JavasScript 编写的后端脚本程序。


  • Node.js 运行时环境包含执行 JavaScript 程序所需的一切条件。该引擎会将 JavaScript 代码转换为更快的机器码。

    nodejs vs java
    Figure 1. Node.js与Java运行时环境对比
  • Node.js 进一步提升 JavaScript 的能力,使 JavaScript 可以访问文件、读取数据库、访问进程,从而胜任后端任务。

  • Node.js 的最大优点是开发人员可以在客户端和服务器端编写 JavaScript,打通了前后端。

  • Node.js 发展迅速,目前已成为 JavaScript 服务器端运行平台的事实标准。

  • Node.js是跨平台的,能运行在Windows、macOS和Linux平台上。

  • Node.js除了自己的标准类库之外,还可使用大量的第三方模块系统来实现代码的分享和重用。

  • 与其他后端脚本语言不同的是,Node.js内置了处理网络请求和响应的函数库,也就是自备了HTTP服务器,所以不需要额外部署HTTP服务器

    nodejs vs php
    Figure 2. Node.js与PHP对HTTP请求的处理

Node.js 的特点

非阻塞I/O
nodejs io
Figure 3. Node.js 的非阻塞 I/O
  • 非阻塞I/O 又称异步式 I/O,是 Node.js 的重要特点。

  • 阻塞I/O是指线程在执行过程中遇到I/O操作时,操作系统会撤销该线程的CPU控制权,使其暂停执行,处于等待状态,同时将资源转让给其他线程。

  • 非阻塞I/O是指当线程遇到I/O操作时,不会以阻塞方式等待I/O操作完成或数据返回,而只是将I/O请求转发给操作系统,继续执行下一条指令。


事件驱动
event drive
  • 非阻塞I/O是一种异步方式的I/O,与事件驱动密不可分。

  • 事件驱动以事件为中心,Node.js将每一个任务都当成事件来处理。Node.js在执行过程中会维护一个事件队列,需执行的每个任务都会加入事件队列并提供一个包含处理结果的回调函数。

  • 在事件驱动模型中,会生成一个事件循环线程来监听事件,不断地检查是否有未处理的事件。

  • Node.js 的异步机制是基于事件的,所有磁盘I/O、网络通信、数据库查询事件都以非阻塞的方式请求,返回的结果由事件循环线程来处理。


单线程
  • Node.js的应用程序是单进程、单线程的,但是通过事件和回调支持并发,性能变得非常高。

  • 在阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须使用多线程。

  • 在非阻塞模式下,线程不会被I/O操作阻塞,该线程所使用的CPU核心利用率永远是100%,I/O操作以事件的方式通知操作系统。

  • Node.js在主线程中维护一个事件队列,当接收到请求后,就将该请求作为一个事件放入该队列中,然后继续接收其他请求。

  • Node.js内部通过线程池来完成非阻塞I/O操作,Node.js的单线程是指对JavaScript层面的任务处理是单线程的,而Node.js本身是一个多线程平台。

Note
Node.js采用非阻塞I/O与事件驱动相结合的编程模式,与传统同步I/O线性编程思维有很大的不同,Node.js程序的控制很大程度要依靠事件和回调函数,这不符合开发人员的常规线性思路,需要将一个完整的逻辑拆分为若干单元(事件),从而增加了开发和调试的难度。

Node.js 的应用场合

适合用Node.js的场合
  • REST API:REST API是一种前后端分离的应用程序架构。

  • 单页Web应用:加载单个HTML页面,并在用户与应用程序交互时动态更新该页面的Web应用程序。

  • 统一Web应用的UI层:Node.js是面向服务的架构,其能够更好地实现前后端的依赖分离,可以将所有的关键业务逻辑都封装成REST API,UI层只需要考虑如何用这些API构建具体的应用。

  • 准实时系统:如聊天系统、微博系统、博客系统的准实时社交系统,特点是轻量级、高流量,没有复杂的计算逻辑。

  • 游戏服务器:程序员不必使用C语言就能开发游戏的服务器程序。

  • 微服务架构:Node.js也可用于实现基于微服务架构的应用。

不适合用Node.js的场合
  • 数据加密和解密。

  • 数据压缩和解压。

  • 模板渲染。

弥补Node.js不足的解决方案
存在问题 解决方案

CPU密集型任务偏向于CPU计算操作,需要Node.js直接处理,在事件队列中,如果前面的CPU计算任务没有完成,那么后面的任务就会被阻塞,出现响应慢的情况,使得后续I/O操作无法发起

将大型运算任务分解为多个小任务,适时释放CPU计算空间资源,以免阻塞I/O调用的发起

单线程无法利用多核CPU。多CPU或多核CPU的服务器当Node.js被CPU密集型任务占用,导致其他任务被阻塞时,其他CPU核心处于闲置状态,从而造成资源浪费;Node.js程序一旦在某个环节崩溃,整个系统都会崩溃,这会影响其可靠性

  1. 部署Nginx反向代理和负载均衡,开启多个进程,绑定多个端口

  2. 使用cluster模块构建应用集群,启动多个Node.js实例,开启多个进程以监听同一个端口

部署 Node.js 开发环境

在 Windows 系统上安装 Node.js

下载可执行文件 .msi 双击安装: https://nodejs.org/dist/v18.13.0/node-v18.13.0.pkg

在 Linux 系统上安装 Node.js

管理 Node.js 版本

nvm 支持全平台的 Node 版本管理 https://github.com/nvm-sh/nvm

交互式运行环境 — REPL

REPL: Read Eval Print Loop

  • 功能——JavaScript脚本的简单调试

  • 主要执行的操作

    • 读取用户输入,解析输入的JavaScript数据结构并将其存储在内存中。

    • 执行输入的数据结构,打印(输出)结果。

    • 循环操作以上步骤直到用户按下两次<Ctrl>+<C>组合键。

  • 进入命令行界面,执行 node 命令即可启动Node终端,出现“>”提示符表示进入REPL命令行交互界面。

    举例输入 2 * 6

安装开发工具 Visual Studio Code

Download Visual Studio Code: https://code.visualstudio.com/download

开始开发 Node.js 应用

实战演练 — 构建第一个 Node.js 应用程序

#clean src/ch01/helloworld.js
const http = require('http');  // (1)
const httpServer = http.createServer(function (req, res) {  // (2)
  res.writeHead(200, {'Content-Type': 'text/plain'});  // (3)
  res.end("Hello World!\n");  // (4)
});
httpServer.listen(8080, function () {  // (5)
  console.log("服务器正在8080端口上监听!");  // (6)
});
  1. 导入 Node.js 自带的 http 模块,并将实例化的HTTP组件赋值给变量http。模块是Node.js程序组织可重用代码的方式,可使用require()方法来载入模块。

  2. 创建HTTP服务器。调用http模块提供的http.createServer()方法创建服务器,使用一个回调函数作为参数,该回调函数又接受两个参数,分别是代表客户端的请求对象和向客户端发送的响应对象,所有请求和响应都由此回调函数处理。

  3. 设置响应头信息

  4. 发送响应数据

  5. 启动HTTP服务器,并设置监听器的端口号。http.createServer()方法返回一个HTTP服务器对象,它使用listen()方法启动HTTP服务器以监听连接、指定端口号。该方法包含一个回调函数参数,用于设置启动HTTP服务器之后的操作。

  6. 向终端输出信息

测试程序
  1. 在终端窗口中运行程序进行测试

    $ node helloworld.js
    终端输出如下
    服务器正在8080端口上监听!
  2. 通过浏览器访问该 Web 应用程序进行测试

    helloworld

运行 Node.js 程序

  1. 使用 node 命令运行程序

    $ node xxx.js
    • 运行当前目录下的 index.js 脚本文件,可以使用点号代替

      $ node .
    • 选项-e(--eval)表示直接执行某语句:

$ node -e "console.log('Hello World!');"
  1. 使用 npm 命令运行程序

    这需要依赖当前目录下的配置文件 package.json。package.json 是 CommonJS 规定的用于描述包的文件。例如,当前目录下的 package.json 包含如下内容:

    {
      "scripts":{
        "start": "node demo.js",
        "test": "node test.js"
      }
    }

    其中 scripts 属性定义要执行的脚本

    • 在当前目录下执行npm start命令就相当于执行node demo.js 命令;

    • 执行 npm test 命令就相当于执行 node test.js命令。

      这种方式可以为不同的环境(如测试、生产)指定不同的 Node.js 程序。

  2. 使用 nodemon 监视文件改动并自动重启程序

    $ npm i nodemon -g
    $ nodemon xxx.js
  3. 在 Visual Studio Code 中运行程序

    在 vscode 中打开一个终端窗口,运行命令

    $ node xxx.js
#clean src/ch01/package.json

{ "scripts":{ "start": "node demo.js", "test": "node test.js" } }

调试 Node.js 程序

  1. 使用日志工具进行调试

    • 使用console.log()方法检查变量或字符串的值,记录脚本调用的函数,或记录来自第三方服务的响应。

    • 使用console.warn()或console.error()方法记录警告或错误信息。

  2. 使用 Node.js 内置调试器

    Node.js内置一个进程外的调试实用程序,可通过V8检查器和内置调试客户端访问。

    1. 调试之前先将 debudder; 语句插入到脚本的源代码,以在代码中的该位置启用断点。

      global.x = 5;
      setTimeout(() => {   //此处用到的回调函数的形式是箭头函数,() =>相当于function()
        debugger;  // (1)
        console.log('world');
      }, 1000);
      console.log('hello');
      1. 插入 debugger 语句

    2. 执行 node 命令时加上 inspect 参数,指定要调试的脚本的路径。

      $ node inspect debugdemo.js
      调试操作如下:
      < Debugger listening on ws://127.0.0.1:9229/bf92c68e-26b6-44cb-bc75-f5f812fb4c0b
      < For help, see: https://nodejs.org/en/docs/inspector
      <
      < Debugger attached.
      <
       ok
      Break on start in debugdemo.js:1
      > 1 global.x = 5;
        2 setTimeout(() => {   //此处用到的回调函数的形式是箭头函数,() =>相当于function()
        3   debugger;
      debug> cont // (1)
      < hello
      <
      break in debugdemo.js:3
        1 global.x = 5;
        2 setTimeout(() => {   //此处用到的回调函数的形式是箭头函数,() =>相当于function()
      > 3   debugger;
        4   console.log('world');
        5 }, 1000);
      debug> next  // (2)
      break in debugdemo.js:4
        2 setTimeout(() => {   //此处用到的回调函数的形式是箭头函数,() =>相当于function()
        3   debugger;
      > 4   console.log('world');
        5 }, 1000);
        6 console.log('hello');
      debug> repl  // (3)
      Press Ctrl+C to leave debug repl
      > x
      5
      > .exit
      1. cont 继续执行

      2. next 下一步

      3. repl 交互模式

      4. exit 退出调试

      Note
      其中代码行号前面的 > 符号表示程序暂停的位置。
  3. 在 Visual Studio Code 中调试

    vscode debug
    Figure 4. 执行调试
    vscode debug2
    Figure 5. 调试界面
#clean src/ch01/debugdemo.js
global.x = 5;
setTimeout(() => {   //此处用到的回调函数的形式是箭头函数,() =>相当于function()
  debugger;
  console.log('world');
}, 1000);
console.log('hello');

本章小结

习题

2. Node.js 编程基础

JavaScript 基本语法

Node.js 基于 V8 引擎,这意味着其语法与 JavaScript 基本相同,本章先概略地介绍 JavaScript 的语法。JavaScript 语法内容非常多,这里仅介绍与 Node.js 编程有关的 JavaScript 新特性和新要求,建议读者重点关注块级作用域、模板字符串、集合和映射、箭头函数、高阶函数、闭包、严格模式等内容。

版本

ES6 是目前的主流版本, Node.js 自 6.0 版本开始全面支持 ES6。目前的 Node.js 版本对 ES6 标准的支持率达 99%。可以使用 es-checker 包来检测当前 Node.js 版本对 ES6 的支持情况。

Node 的新版本也在逐渐提供对 ES 新标准的支持,尽管有的是部分支持。这里主要以 ES6 版本为例 JavaScript 语法。Node.js 对 ES 标准的支持得益于 V8 引擎,V8 引擎在发展的过程中一直紧追 ECMAScript 发布的脚步,使 Node,js 能及时采用 ECMAScript 的最新语法。

Node.js自7.6版本于开始就默认支持 async/await 异步编程。

运行环境

  • 方式一:Node.js REPL交互式运行环境

    $ node
    Welcome to Node.js v17.9.0.
    Type ".help" for more information.
    > .help
    .break    Sometimes you get stuck, this gets you out
    .clear    Alias for .break
    .editor   Enter editor mode
    .exit     Exit the REPL
    .help     Print this help message
    .load     Load JS from a file into the REPL session
    .save     Save all evaluated commands in this REPL session to a file
    
    Press Ctrl+C to abort current expression, Ctrl+D to exit the REPL
    >
  • 方式二:浏览器控制台

    打开浏览器 Ctrl + Shift + c 可快捷进入开发模式, 再点击 Console 标签

console

语句与注释

JavaScript 语法的基本规则与 Java 语言类似,严格区分大小写。JavaScript 是解释型语言,程序(脚本)由 JavaScript 引擎解释执行。

  1. 语句

    • JavaScript每条语句都以分号“;”结束。(非强制但推荐)

    • 一行代码可包含多条语句。(不推荐)

    • 一行语句太长,则可以使用续行符“\”进行换行。

  2. 语句块

    • 语句块是一组语句的集合,作为一个整体使用大括号“{}”封装。

    • 语句块可以嵌套,形成层级结构。

  3. 注释

    行注释
    // 我是单独一行注释
    console.log("Hello World!"); // 我是行注释
    块注释
    /* 块注释开始
       块注释结束 */

变量

  1. 变量命名

    • JavaScript 的变量可以是任意数据类型。

    • 变量名可以是大小写英文字母、数字、符号“$”或“_”的任意组合,但不能以数字开头。

    • 变量名不能是 javascript 的关键字(如 for、if、while)等。

  2. 变量的声明与赋值

    • 弱类型的编程语言,所有数据类型都可以用 var 关键字声明。在定义变量时无须指定变量类型。

      var hello; // 声明一个名为hello的变量,此时该变量的值为undefined,表示未定义
    • 使用等号对变量进行赋值,可以将任意数据类型赋值给变量。

      hello='我是个字符串'; // 此时变量的值为“我是个字符串”
    • 可以在声明变量的同时对变量进行赋值。

    • 可以反复赋值同一个变量。

  3. 变量提升

    • 变量可以在声明之前使用,值为 undefined

console.log(temp);  // 返回undefined
var temp = '你好';
  • ES6 用 let 关键字改变这种行为,变量一定要在声明之后使用,否则会报错。

console.log(temp);  // Uncaught ReferenceError: can't access lexical declaration 'temp' before initialization
let temp = '你好';
  1. 变量泄露

    • 用来计数的循环变量使用 var 关键字声明后会泄露为全局变量。

var temp = 'Hello!';
for (var i = 0; i < temp.length; i++) {
  console.log(temp[i]);
}
console.log(i); // 返回数字6
  • 改用let关键字来声明循环变量避免变量泄露。

    1. 全局作用域和函数作用域

  • ES5中只有全局作用域(顶层作用域)和函数作用域。

var temp = '你好!';  // 全局作用域
function testScope() {
  var temp = '早上好!';  //函数作用域
  console.log(temp);
}
testScope();        // 返回函数作用域中的“早上好”!
console.log(temp);  // 返回全局变量的“你好”!
  1. 块级作用域与let关键字

    • ES6引入块级作用域,使用let关键字声明的变量只能在当前块级作用域中使用。

function testBlockScope() {
  let name = '小明';
  if (true) {
      let name = '小红';
      console.log(name); // 返回“小红”
  }
  console.log(name);     // 返回“小明”
}
  1. 使用const关键字声明只读常量

    const PI = 3.1415;

数据类型

数值 Number
  • 不区分整数和浮点数,统一用数值表示。

  • 十六进制数使用0x作为前缀。二进制和八进制数值分别使用前缀0b(或0B)和0o(或0O)。

  • 无法计算结果时就可用NaN表示;Infinity表示无限大。

Tip

要注意两个特殊的数值 NaN 和 Infinity

  • 前者表示“Not a Number",当无法计算结果时就可用 NaN 表示;

  • 后者表示无限大,当数值超过JavaScript所能表示的最大值时,就用Infinity表示

  • NaN 与其他任意值均不相等,包括它自己, NaN == NaN 比较的结果为 false。

  • 只有 isNaN() 函数能正确判断 NaN,如 isNaN(NaN) 会返回 true。

字符串 String
  • 字符串是用单引号“'”或双引号“"”括起来的任意文本。

  • ES6 提供模板字符串,可使用反引号包括整个模板字符串,使用${}将变量括起来。

    var msg = `服务器侦听监听地址和端口:${srvip}:${port},请注意!`;
  • 模板字符串中也可以不嵌入任何变量,通常用于按实际格式输出(如换行)。

布尔值 Boolean
  • 布尔值只有true、false两种,经常用于条件判断中。

  • 在比较是否相等时,建议使用===而不要使用==。

null 和 undefined - null表示一个空值,即什么也没有。 - undefined表示“未定义”,仅用于判断函数参数是否正常传递。

数组 Array
  • JavaScript的数组可以包括任意数据类型。

  • 数组用[]表示,元素之间用逗号分隔。

  • 数组的元素可以通过索引来访问,注意索引的起始值为0。

对象 Object
  • 对象是一组由键值对组成的无序集合,用{}表示,键值对之间用逗号分隔。

    var myObj = {
      isobj: true,
      num: [1,2,3],
      desp: '对象好像可以无所不包'
    };
  • 键均为字符串类型,而值可以是任意数据类型。

  • 获取一个对象的属性可用“对象名.属性(键)名”的方式。

  • ES6允许将表达式作为对象的属性名,即把表达式放在方括号内。

let numproperty = 'num';
var myObj = {
  isobj: true,
  [numproperty]: [1,2,3],
  ['des'+'cription']: '我是个对象'
};
myObj[numproperty];    // [1,2,3]
myObj['num'];          // [1,2,3]
myObj['description'];  // "我是个对象"
符号 Symbol
  • ES6 引入数据类型 Symbol,用于表示独一无二的值,其值通过 Symbol() 函数自动生成。

  • Symbol 值用于对象的属性名,可以有 3 种表示方法。

let welcome = Symbol();  // 自动产生一个值
let myObj = {};
myObj[welcome] = '欢迎光临';  // (1)
let myObj = { [welcome]: '欢迎光临' };  // (2)
let myObj = {};
Object.defineProperty(myObj, welcome, { value: '欢迎光临' });  // (3)

+ <1> 第 1 种表示方法 <2> 第 2 种表示方法 <3> 第 3 种表示方法

映射 Map
  • ES6 引入 Map 数据结构,与对象类似,但各种类型的数据(甚至对象)都可以作为键。

  • Map 本身是一个构造函数,用于生成 Map 数据结构。

    const myMap = new Map();
  • 可以使用 Map 结构的 set 方法添加成员

    const myObj = {welcome: '欢迎光临'};
    myMap.set(myObj, '我是一个对象');
  • 使用Map结构的get方法读取键(成员)

    myMap.get(myObj);  // 结果为'我是一个对象
  • Map() 函数也可以将一个数组作为参数, 该数组的成员是表示键值对的数组,例如:

const myMap = new Map([
  ['name',  "王刚"],
  ['title', "博士"]
]);
myMap.get("name");   // 返回“王刚”
myMap.get("title");  // 返回“博士”
  • Map结构的实例支持遍历方法。

    • keys():返回键名的遍历器。

    • values():返回键值的遍历器。

    • entries():返回键值对的遍历器。

    • forEach():使用回调函数遍历每个成员。

集合 Set
  • 集合是无重复的、无序的数据结构,类似于数组,即没有重复的值。

  • 集合是一个构造函数,用于生成 Set 数据结构。

    const mySet = new Set();
  • 可以通过 add() 方法向 Set 结构加入成员。

  • Set() 函数可将 Iterable 类型的数据结构(数组、集合或映射)作为参数,用于初始化集合。

    const mySet = new Set([1, 2, 3, 4, 4]);  //会自动过滤掉其中一个数字 4
  • Set 结构中的元素可以看作是键,与 Map 结构不同的是,它只有键名没有键值。

  • Set 结构使用与 Map 结构相同的 4 种遍历方法来遍历成员。只是 values() 方法返回的也是元素(键名)。

Tip
  • 遍历数组可以采用下标循环,而遍历映射和集合就无法使用下标。

  • 为了统一集合类型,ES6 引入了新的 Iterable 类型,数组、映射和集合都属于 Iterable 类型。

  • 这种类型的集合可以通过新的 for…​of 循环来遍历。

  • 更好的遍历方式是使用 Iterable 类型内置的 forEach 方法。它接收一个函数,每次迭代就目动回调该函数。

    • 以数组 arr 为例,forEach 用法为:

      arr.forEach(function (element, index, array) ...};
    • 以映射map为例,forEach用法为:

map.forEach(function (value, key, map) ...};
  • 以集合set为例,forEach 用法为:

set.forEach(function (element, sameElement, set) ..}

+ 回调函数中第 2 个参数 sameElement 返回的值同第 1 个参数 element 都是元素。

流程控制

JavaScript 默认情况下按顺序执行每一条语句, 直到脚本文件结束,一条路走到底, 也就是线性地执行语句列,这是最基本的顺序结构。

JavaScript 还提供分支结构和循环结构以进行流程控制

  • 分支结构好比有多条岔路可以选择;

  • 循环结构类似于一段路来来回回地反复走。

分支结构
  • if () { …​ } else { …​ }

  • if () { …​ } else if () { …​ } else { …​ }

  • switch …​ case

    switch(变量) {
      case 值1:
        代码1;
        break;
      case 值2:
        代码2;
        break;
      default:
        如果以上条件都不满足则执行该代码;
    }
循环结构

循环结构用于反复执行一段代码, JavaScript 支持以下循环语句。

  • for

    这种循环语句通过初始条件、结束条件和递增条件来循环执行语句块。for 循环的 3 个条件都是可以省略的,如果没有设置退出循环的判断条件,就必须使用 break 关键字退出循环,否则就是死循环。

  • for …​ in

    这是 for 循环的一个变体,可以将一个对象的所有属性依次循环出来。例如:

    for (var key in obj) {
      console.log(key);
    }
  • while

    for 循环更适用于已知循环的初始和结束条件,否则使用 while 循环更好。while 循环只有一个判断条件,只要条件满足,就继续循环,条件不满足时则退出循环。

  • do …​ While

    它与 while 循环的唯一区别在于,不是在每次循环开始的时候判断条件,而是在每次循环完成的时候判断条件。


循环过程中有时需要在未达到循环结束条件时强制跳出循环。

  • break

    break 关键字用于终止一个重复的循环。

  • continue

    coninue 关键字可跳过循环体中位于它后面的语句,回到本层循环的开头,进行下一次循环。

Note
循环结构中的条件需使用括号括起。

函数

函数可以将一个复杂功能划分成若干模块,让程序结构更加清晰,代码重复利用率更高。

函数声明
  • 声明函数需要使用function关键字。例如下面的函数用于求两人年龄的和:

    function sumAge(x,y) {
      return  x + y;
    }
  • ES6 直接支持默认参数

    sumAge(x = 19, y = 20)
  • 在 ES5 中设置默认参数需要采用变通的方法

    function sumAge (x, y) {
      x = arguments[0] === undefined ? 21 : arguments[0]; //设置参数x的默认值为21
      y = arguments[1] === undefined ? 20 : arguments[1]; //设置参数y的默认值为20
      return  x + y;
    }
函数调用及参数
  • 声明函数之后使用“函数名(参数)”的形式,按顺序传入参数进行调用。

    sumAge(22, 20);  // 两人加起来42岁
  • arguments 对象

    JavaScript 的函数默认带有 arguments 对象,利用该对象可以获得调用者传入的所有参数。

    function sumAge (){
      var sum = 0;
      var numcount = arguments.length
      for (var i = 0; i < numcount; i++){
        sum += arguments[i];
      }
      return sum;
    }
  • rest 参数

    ES6 引入 rest 参数(形式为“…​变量名”),用于获取函数的多余参数,以代替 arguments 对象。

    function sumAge(...values){
      var  sum = 0;
      for (var val of values){
        sum += val;
      }
      return sum;
    }
匿名函数

声明函数时省略函数名。

var sumAge = function (x,y) {
  return  x + y;
};

这种没有函数名的函数被称为匿名函数。

例子中这个匿名函数被赋值给了变量 sumAge,在函数末尾需添加一个分号。

通过变量 sumAge 就可以调用该函数。

箭头函数

使用箭头符号 => 定义的函数。

示例 箭头函数 普通函数

一个参数

var f = x => x;
var f = function (x) {
  return x;
};

多个参数

var sumAge = (x, y) => x + y;
var sumAge = function (x,y) {
  return  x + y;
};

无参数

var f = () => 20;
var f = function () { return 20 };

代码块包含多条语句

var diffAge = (x, y) => {
  var diff = x - y;
  return Math.abs(diff);
}
var diffAge = function (x,y) {
    var diff = x - y;
    return Math.abs(diff);
}
高阶函数

以一个或多个函数作为参数的函数。

function diffAge(m, n, abs) {
  return abs(m-n);
}
diffAge(19, 22, Math.abs);  // 相差 3 岁

JavaScript 中内的数组方法 map()、reduce()、filter()、sort() 等都是高阶函数。例如数组的 sort() 方法默认会将所有元素先转换为字符串再排序,如果直接对数字排序,排序结果就会错误。但是sort() 作为一个高阶函数,还可以接收一个比较函数来实现自定义的排序。下面给出一个按数字大小排序的例子:

var arr = [100, 120, -10, 2];
arr.sort(function (m, n) {
  if (m < n) {
    return -1;
  }
  if (m > n) {
    return 1;
  }
  return 0;
});  // [ -10, 2, 100, 120 ]
闭包
  • 当函数作为返回值,或者作为参数传递时,该函数就被称为闭包。

  • 闭包是能够读取其他函数内部变量的函数,可以使用函数之外定义的变量。

var basePrice = 10.00;       // 起步价
var baseMiles = 3.00;        // 起步里程

function taxiPrice(unitPrice, mileAge) {
  function totalPrice() {   //计算总费用 ;这是定义在一个函数内部的函数
    if ( mileAge > baseMiles) {   //超过起步里程
      return  unitPrice * mileAge;  //单价与里程相乘
    }
    else{                    //在起步里程内
      return basePrice;
    }
  }
  return totalPrice();
}
taxiPrice(2.00, 6.00);  // 打车费用12.00
  • 这个示例在一个函数内部定义另一个函数,函数 taxiPrice() 包括函数 totalPrice()。

  • totalPrice()是一个闭包,作为内部函数,它将获取外部函数 taxiPrice() 的两个参数 unitPrice 和 mileAge,以及全局变量 baseMiles 和 basePrice 的值。

  • taxiPrice() 最后调用了 totalPrice() 函数,根据里程是否超过起步里程来计算总费用。

  • 闭包 totalPrice() 函数不接收参数,它使用的值是从执行环境中获取的。

  • 返回的 totalPrice() 函数并没有立刻执行,而是在调用了 taxiPrice() 函数之后才执行。

ES5 通过构造函数生成实例对象 ES6 引入类作为对象的模板,通过 class 关键字定义类
function Visitor(name, sex) {   //来宾信息
    this.name = name;
    this.sex = sex;
}

Visitor.prototype.getInfo = function () {
    return this.name + ', ' + this.sex;
};

var visitor = new Visitor('张勇', '先生');
class Visitor {  // (1)
  constructor(name, sex) {  // (2)
    this.name = name;  // (3)
    this.sex = sex;
  }
  getInfo() {  // (4)
    return this.name + ', ' + this.sex;
  }
}
var visitor = new Visitor('张勇', '先生');  // (5)

ES6 的写法更像面向对象编程的语法。

  1. 定义一个类 Visitor

  2. constructor 是构造方法

  3. this 代表实例对象本身

  4. 定义方法时前面不用加 function 关键字

  5. 实例化对象时直接使用 new,跟构造函数的用法完全一致

  • 在大多数 ES5 实现中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性

  • 而类同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链:子类的 __proto__ 属性表示构造函数的继承,总是指向父类;子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。

ES6 类可以通过 exends 关键字实现继承,这比 ES5 通过修改原型链实现继承更清晰和方便。

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就是静态方法。父类的静态方法,可以被子类继承。

严格模式

早期的 JavaScript 并不强制要求声明变量,如果一个变量没有声明就被使用, 那么该变量就自动被作为全局变量,这会导致在同一网页的不同 JavaScript 文件中如果使用相同的变量名,可能造成变量之间的冲突,产生错误结果。而使用 var 关键字的变量就不是全局变量,其作用域被限制在声明该变量的函数体内。这样不同的函数体内同名变量不会产生冲突。ECMA 从 ES5 版本开始增加严格模式(Strict Mode),要求指定代码在严格条件下执行,强制要求变置在声明之后使用,不能使用未声明的变量。

  1. 使用严格模式

    严格模式通过在脚本或函数的头部添加以下语句来声明。

    'use strict';
    • 在文件第 1 行使用这个声明,表示整个脚本文件都执行严格模式;

    • 在函数第 1 行使用该声明,这个函数内部使用严格模式。

      Note
      • 严格模式声明语句只是一个字符串, 不支持严格模式的 JavaScript 引擎会将它作为一个字符串语句执行,而支持严格模式的 js 引擎将开启严格模式来运行 js 代码。

      • 严格模式消除代码运行的一些不安全之处以保证代码的安全运行,提高编译器效率以提高运行速度。

      • 严格模式禁用了一些可能在未来版本中定义的语法,从而体现了 JavaScript 向更合理、更安全、 更严谨的方向发展。

      • 浏览器 Intenet Explorer 10+、Firefox4+、. Chrome 13+、Safari5.1+ 和 Opera12+ 都支持严格模式。

      • ES6的模块自动采用严格模式,无论是否声明都会使用严格模式。

      • 在Node.js中,建议在所有脚本文件中加入严格模式声明,也可以给 node 命令加上选项 --use_ strict 来默认启用严格模式。

  2. 严格模式的主要限制

    • 变量必须声明后使用。

    • 不能出现两个命名参数同名的情况,否则报错。

    • 不能使用with语句。

    • 不能对只读属性赋值,否则报错。

    • 不能使用前缀0表示八进制数,否则报错。

    • 不能删除不可删除的属性,否则报错。

    • 不能删除变量,只能删除属性。

    • eval关键字不会在它的外层作用域引入变量。

    • eval和arguments不能被重新赋值。

    • 不能使用 arguments.callee 和 arguments.caller 属性。

    • 禁止this关键字指向全局对象。

    • 不能使用 fn.caller 和 fn.arguments 属性获取函数调用的堆栈。

    • 增加了保留字(如 protected、static 和 interface)。

JavaScript 编程规范

  1. 代码格式

    • 每行代码应小于 80个字符。如果代码较长,应尽量换行。

    • 使用UNIX风格的换行,每行以软回车“\n” 结束,不使用Windows的硬回车"nr" 换行符。通过Git工具提交代码时Git会自动将换行符转换为UNIX风格。

    • 行末无空白。

    • 每条JavaScript语句应该以分号结束。

    • 缩进使用2个半角空格或4个半角空格,而不使用Tab键。

    • 每行仅声明一个变量,而不要声明多个变量。

    • 字符串尽量使用单引号。

    • 符号“{”应在行末,表示代码块的开始,符号“}”应在行首,表示代码块的结束。

    • 适当的空白行可以大大提高代码的可阅读性,可以使代码逻辑更清晰易懂。

  2. 命名规范

    • 变量推荐使用小驼峰命名法。

    • 常量名建议全部大写。

    • 函数也采用小驼峰命名法,建议前缀部分为动词。

    • 类和构造函数采用大驼峰命名法,同时建议前缀部分为名词。

    • 文件命名尽量采用下划线分割单词。

    • 类的公共属性和方法与上述变量和函数的命名方法一样。类的私有属性和方法建议前缀为下划线。

    • 文件命名时尽量采用下划线分割单词。

  3. 函数

    • 函数的实现代码尽可能短小精悍,便于阅读。

    • 避免多余的else语句,尽早执行retum语句。

    • 尽可能为闭包命名,便于调试跟踪。

    • 不要嵌套闭包。

    • 使用方法链时,每行仅调用一个方法,并使用缩进表明方法的并列关系。

  4. 注释

    • 注释要尽量简单、清晰明了,同时注重注释的意义。

    • 尽可能从更高层次说明代码的功能。

    • 尽可能使用英文注释。 当然书写中文注释也是无可厚非的。

Node.js 回调函数

Node.js 异步编程的直接体现就是回调( Callback)函数。回调函数在完成任务后就会被调用,Node.js 使用了大量的回调函数,其API支持回调函数。回调函数与异步执行是紧密相关的,Node.js 大部分异步函数的参数列表的最后一项接受一个回调函数,因此这些异步函数实际上就是高阶函数,而回调函数可以看作闭包函数。

学习和使用 Node.js,必须理解回调函数这个概念并能熟练使用。

什么是回调函数

回调函数是作为另外一个函数(主函数)的参数传递给该主函数,然后在该主函数体的某个位置执行的函数,这就涉及在一个函数中调用另外个函数。

function main(info, callback){  // (1)
  console.log('还在回家的路上');
  console.log('到家了,发条信息吧');
  callback (info);  // (2)
}
function sendMsg(msg){  // (3)
  console.log(msg);
}
main('亲爱的,我到家了!', sendMsg);  // (4)
  1. 主函数,参数列表中的 callback 是一个回调函数

  2. 调用回调函数

  3. 回调函数

  4. 执行主函数

回调函数示例

使用 Node.js 程序读取文件有两种方式。

  • 一种是同步操作,只有读取操作结束之后才能进行后面的命令,这种方式称为阻塞。

  • 另一种是异步方式,可以一边读取文件,一边执行其他命令,这种方式又称为非阻塞。

非阻塞方式基于回调函数,允许并行执行操作,操作结果会在事件发生时由回调函数处理,因此程序无须等待某个操作的结果就能继续下一步操作。这就大大提高了 Node.js 的性能,使其可以处理大量的并发请求。

Node.js 异步方式读取文件的方法如下:

fs.readFile(file[, options], callback)
  • 其中参数 file 是文件路径名或文件描述符;

  • 参数 options 可以用来指定字符编码;

  • callback 是个回调函数,它有 err 和 data 两个参数,err 返回错误信息,data 返回读取的文件内容。


下面以一个异步读取文件内容的例子来说明回调函数的使用。

  • 首先在当前目录下准备一个名为 demo.txt 的文本文件(以UTF-8编码格式保存)。

  • 然后新建一个名为 test_callback.js 的文件,并以 UTF-8 编码格式保存。

  • 运行文件 node test_callback.js

    Node程序已经执行结束!
    周董好久没有发新歌了,左盼右盼,终于等来了《说好不哭》
#clean src/ch02/demo.txt

周董好久没有发新歌了,左盼右盼,终于等来了《说好不哭》

#clean src/ch02/test_callback.js
const fs = require("fs");  // (1)

fs.readFile('demo.txt', function (err, data) {  // (2)
    if (err) return console.error(err);  // (3)
    console.log(data.toString());   // (4)
});
console.log("Node程序已经执行结束!");  // (5)
  1. 引入fs(filesystem)模块

  2. 异步读取文件内容

  3. 读取失败则报出错误

  4. 读取成功则输出文件内容

  5. 先于 <4> 执行,因为 <4> 是异步执行的

Node.js 事件机制

目前的应用程序大多是事件驱动的。在可交互的应用程序中,用户会产生一系列事件,如单击按钮、双击按钮、拖动对象等,这些事件会按照顺序加入一个队列。

除了界面交互事件外,还有一些文件读取完毕或其他任务执行产生的事件。Node.js 应用程序是单进程、单线程的,但仍然能够支持高并发,这是通过事件循环(Event Loop)实现的。事件循环就是一个程序启动期间运行的“死”循环。Node.js 的事件循环本身是单线程运行的,但是通过异步执行回调接口和事件驱动,Node.js 就可以处埋大量的并发事件了,而且性能非常高。

事件循环

在事件驱动模型中,会生成一个主循环(事件循环)来监听事件。Node.js 中几乎所有事件机制都是使用观察员模式实现的。Node.js 单线程类似进入一个 while(true) 循环结构的事件循环,注册到每个异步事件上的回调函数相当于个观察员,如果观察员观察到事件发生就调用该回调函数,直到没有事件时观察员退出循环。

以 Web 应用程序为例,当 Web 服务接收到请求时,就将请求交给操作系统内核处理,然后去服务下一个 Web 请求。当一个请求完成时,其结果被放回处理队列,当该结果到达队列开头时,它就被返回给用户。这个模型非常高效,可扩展性强,因为 Web 服务一直接受请求而不用等待任何读写操作。

事件循环尽可能地将相应的操作转给操作系统内核处理,从而让单线程的 JavaScript 程序支持非阻塞 I/O 操作。目前主流的内核都是多线程的,它们可以同时在后台执行多个操作。当其中的某个操作完成时,内核就会通知 Node.js 将与其相关的回调函数添加到轮询队列中并最终得到执行。

Node.js 启动后,它会初始化事件循环,处理提供的输入脚本,该脚本可以进行异步 API 调用、定 时器,或者调用 process.nextTick() 方法,然后开始处理事件循环。下图展示了事件循环的操作顺序,其中每个方框代表事件循环的一个阶段,共有 6 个阶段。

事件循环的每个阶段都会维持一个先进先出的可执行回调函数的队列。这个队列类似于买票排队,排在前面的人买完票就从队列中退出来了。通常情况下当事件循环进入某个阶段时,它将执行这个阶段特有的任何操作,然后执行该阶段的队列中的回调,直到队列结束,或者回调数达到最大限制。当该队列结束或达到回调限制,事件循环将移动到下一阶段继续处理,循环往复。

event loop
Figure 6. 事件循环的操作顺序

由于事件循环中任一操作都可能调度更多的操作,并且在轮询阶段处理的新事件会加入内核的队列,轮询事件被处理时会有新的轮询事件加入。这样,处理时间较长的回调任务会导致轮询阶段的时间超过了定时器的阈值。事件循环的各个阶段说明如下。

  1. 定时器(timers):定时器的用途是让指定的回调函数在某个阈值后会被执行。定时器的回调会在指定的时间后尽快得到执行,然而,操作系统的调度或者其他回调的执行可能会延迟该回调的执行。本阶段执行 setTimeout() 和 setInterval() 方法调度的回调函数。

  2. 待定回调(pending callbacks):执行已被延迟到下一个循环迭代的 I/O 回调函数。

  3. 空闲和准备(idle, prepare):仅在系统内部使用。

  4. 轮询(poll):轮询阶段有两个主要功能,一是执行已到时间的定时器脚本,二是处理轮询队列中的事件。一旦轮询队列空闲,事件循环会查找已到时间的定时器。如果找到,事件循环就回到定时器阶段执行回调。Node.js 将在此阶段根据实际情况进行阻塞操作。

  5. 检查(check):这个阶段允许回调函数在轮询阶段完成后立即被执行。如果轮询阶段空闲,并且存在已经被 setImmediate() 方法加入队列的回调函数,事件循环会进入检测阶段而不是在轮询阶段等待。

  6. 关闭回调(close callbacks):执行一些关闭的回调函数,如 socket.on('close',…​)。例如,一个 Socket (套接字)忽然关闭,close 事件将会在此阶段触发。否则这类事件会通过 process.nextTick() 方法触发。

在事件循环执行过程中,Nodejs 检查循环是否有需要等待的非阻塞 I/O 或定时器,如果没有则彻底结束事件循环。

事件的监听与触发

Node.js 不是在各个线程中为每个请求执行所有的任务,而是把任务添加到事件队列中,然后由一个单独的线程运行一个事件循环,将任务从事件队列中提取出来。事件循环获取事件队列中头部的任务,执行该任务,再找到下一个任务。当执行到长期运行或有阻塞 I/O 的代码时,它不是直接调用函数,而是将函数与一个要在此函数完成后执行的回调函数一起添加到事件队列中。当 Node.js 事件队列中的所有事件都被完成时,Node.js 应用程序终止。阻塞 I/O 停止当前线程的执行并等待响应,直到收到响应线程执行才能继续。Node.js 使用事件回调来避免对阻塞 I/O 的等待。事件回调的关键就是事件轮询。

在 Node.js 中,异步的 I/O 操作在完成时都会发送一个事件到事件从列中。Node.js 中的许多对象也会分发事件,如 net.Server 对象会在毎次有新的连接时分发一个事件,fs.readStream 对象会在文件被打开时分发一个事件。这些产生事件的对象都是 event.EventEmitter 类的实例。

events 模块是 Node.js 的核心,Node.js 中大部分的模块都继承自 events 模块,events 模块只提供了一个EventEmitter 对象。EventEmitter 本质上是观察者模式的实现,这种模式定义了对象间的一种一对多的关系,让多个观察者同时监听某一个对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。Node.js 大多数情况下不会直接使用 EventEmitter 对象,而是在对象中继承它。只要是支持事件响应的 Node.js 核心模块(如fs、net、 http),都是 EventEmitter 类的子类。

事件的监听与触发实例

EventEmitter 类的核心就是事件触发与事件监听器功能的封装。所有能触发事件的对象都是 EventEmitter 类的实例。

EventEmitter 对象的事件均由一个事件名和若干个参数组成,事件名是一个字符串,通常表达一定的语义。对于每个事件,EventEmitter 对象支持若干个事件监听器( Listener )。当事件触发(又称发射)时,注册到这个事件的事件监听器被依次调用,事件参数作为回调函数参数传递。大多数 Node.js 核心 API 基于异步事件驱动架构构建,其中某些类型的对象(又称触发器)会触发命名事件来调用函数(又称监听器)。

当 EventEmitter 对象触发一个事件时,所有绑定在该事件上的函数会被同步地调用。下面以门卫报告有人来了为例简单示范事件的监听与触发,eventEmitter.on() 方法用于注册监听器(有人来就报告),eventEmitter.emit() 方法用于触发事件(发现有人来了)。

#clean src/ch02/test_event1.js
const EventEmitter = require('events');  // (1)
const myEmitter = new EventEmitter.EventEmitter();  // (2)
myEmitter.on('seen', () => {  // (3)(5)
  console.log('报告,有人来了');
});
myEmitter.emit('seen');  // (4)
  1. 引入事件模块

  2. 创建 EventEmitter 对象用于监听

  3. 注册 seen 事件用于监视,有人来了就报告

  4. 触发(发射)seen 事件进行报告

  5. 箭头函数 ()⇒ 相当于匿名函数 function()

运行结果
报告,有人来了
为一个事件注册多个监听器

EventEmitter 类常用 API 列举如下:

  • EventEmiter.on(event,istener)、emitter.addListener(event,istener):为指定事件注册一个监听器,参数 event 和 listener 分别表示事件名称和回调函数。

  • EventEmitter.once(event, listener):为指定事件注册一个单次监听器 ,即监听器最多触发一次,触发后立刻解除该监听器。

  • EvetEmiter.emit(event, [arg1], [arg2], […​]):触发由 event 参数指定的事件,传递若干可选参数到事件监听器的参数表。

  • EveniEniter.removeListener(event, listener):删除指定事件的某个监听器,参数 listener 必须是该事件已经注册过的监听器。

  • emitter.listeners(event):返回由 event 参数指定的事件的监听器的数组。

  • emitter.setMaxListeners(n):设置 emitter 实例的最大事件监听数,默认是10个,设置 0 为不限制。

  • emitter.removeAllListeners(event):删除所有由 event 参数指定的事件的监听器。

下面的例子演示如何为一个事件注册多个监听器。

#clean src/ch02/test_event2.js
const EventEmitter = require('events').EventEmitter;  // (1)
var event = new EventEmitter();   // (2)

event.on('seen', function(who) {  // (3)
    console.log('报告,来人是一位', who);
});
event.on('seen', function() {  // (4)
    console.log('欢迎光临!');
});
event.emit('seen', '女士');  // (5)
  1. 加载事件模块

  2. 实例化事件模块

  3. 注册事件(seen)

  4. 再次注册事件(seen)

  5. 发射(触发)事件(seen)

以上例子为事件 seen 注册了两个事件监听器,然后触发了 seen 事件。从运行结果可以发现两个事 件监听器回调函数被先后调用。

运行结果
报告,来人是一位 女士
欢迎光临!
处理 error 事件

EventEmitter 类定义 error 事件,当遇到异常时通常会触发 error 事件。

EventEmitter 规定,当 error 被触发时,如果没有注册相应的监听器, Node.js 会将它当作异常,退出程序并输出错误信息。

为避免遇到错误后整个程序崩溃,一般要为会触发 error 事件的对象设置监听器,例如:

const events = require("events");
const emitter = new events.EventEmitter();
emitter.emit("error") ;

Node.js 全局对象

对于浏览器引擎来说,JavaScript 脚本中的 window 是全局对象,而 Node.js 程序中的全局对象 是 global, 所有全局变量(除global本身外)都是 global 对象的属性。全局变量和全局对象是所有模块都可以调用的。

  • Node.js 的全局变量包括 filename 和 dirname 等

  • 全局对象包括 console 和 process 等,

  • 全局函数包括定时器函数

  • 还有些对象实际上是模块内部的局部变量,这些局部变量所指向的对象根据模块不同而不同,但是对所有模块都适用,可以看作是伪全局变量,主要有 module、exports、require() 等。可以将这些统称为 Node.js 全局对象。

全局变量 filename 和 dirname

__filename 指向当前正在执行的脚本文件名。

__dirname 指向当前运行的脚本所在的目录。

Note
两个下划线开头

例如,在 /users/zxp 路径下运行 node example.js 命令

  • console.log(__flename) 将输出 /users/zxp/example.js

  • console.log(__dimame) 将输出 /sers/zxp


假定有两个模块 a 和 b, 其中 b 是 a 的依赖文件, a 和 b 的目录结构如下:

/users/zxp/app/a.js
/users/zxp/app/node.modules/b/b.js
  • b.js 中的 __filename 会指向 /users/zxp/app/node_modules/b/b.js

  • a.js 中的 __filename 会指向 /users/zxp/app/a.js

console 模块

console 模块提供了一个简单的调试控制台,类似于 Web 浏览器提供的 JavaScript 控制台。该模块导出两个特定的组件:

  • 全局 console 实例

  • Console 类

全局 console 实例
console.log([data][, …​])

打印到标准输出流 stdout,并以换行符结束。可以传入多个参数,第 1 个参数作为主要信息,其他参数则以类似 C 语言 printf() 命令的格式输出。

console.info([data][, …​])

这是 console.log() 的别名。

console.error([data][, …​])

打印到标准错误流 stderr, 并以换行符结束。可以传入多个参数,第 1 个参数作为主要信息,其他参数则以类似 C 语言 printf() 命令的格式输出。

console.warn([data][, …​])

这是 console.error() 的别名。

console.dir(obj[, options])

使用 util.inspect() 方法转换对象,并将结果字符串以易于阅读的格式打印到标准输出流。选项 showHidden、depth 和 colors 的说明请参见第3章的 util.inspect()。

console.time(label)

表示计时开始,启动一个计时器以计算一个操作的持续时间。计时器由唯一的 label 进行标识。

console.timeEnd(label)

表示计时结束,与 console.time(abel) 配套使用,启动一个计时器后,可以使用相同的 label 来停止计时器,并以毫秒为单位将持续的时间打印到标准输出流。

console.trace(message[, …​])

打印字符串 “Trace:” 到标准错误流,然后将 util.formnat() 格式化的消息和堆栈跟踪打印到代码中的当前位置。

console.assert(value[, message][, …​])

这是一个简单的断言测试,用于验证 value(表达式或变量)是否为真。它接收两个参数,第 1 个参数是表达式,第 2 个参数是字符串。只有当第 1 个参数为 false 时,才会输出第 2 个参数,否则不会有任何结果。

下面给出使用全局 console 实例的示例代码:

#clean src/ch02/console.js
console.log('hello world');          // 打印hello world到标准输出流
console.log('hello %s', 'world');    // 打印hello world到标准输出流

console.error(new Error('错误信息')); // 打印 [Error: 错误信息] 到标准错误流

const name = 'Robert';
console.warn(`Danger ${name}! Danger!`);  // 打印 Danger Robert! Danger!到标准错误流
输出如下:
> node nodejs/src/ch02/console.js
hello world
hello world
Error: 错误信息
    at Object.<anonymous> (/Users/swot/learning/nodejs/src/ch02/console.js:6:15)
    at Module._compile (node:internal/modules/cjs/loader:1099:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47
Danger Robert! Danger!
Console 类

Console 类可用于创建一个具有可配置输出流的简单记录器,可以通过 require('console').Console 或 console.Console 使用:

const { Console } = require('console');
// 或者
const { Console } = console;
Console 语法
new Console(stdout[, stderr])
  • stdout <Writable>

  • stderr <Writable>

通过传入一个或两个可写流实例,创建一个新的 Console 对象。

  • stdout 是一个可写流,用于打印日志或输出信息。

  • stderr 用于输出警告或错误。

  • 如果没有传入 stderr ,则警告或错误输出会被发送到 stdout。

Important

全局的 console 是一个特殊的 Console 实例,它的输出会发送到 process.stdout 和 process.stderr。 相当于调用:

new Console(process.stdout, process.stderr);

Console 类包含 console.log()、console.error()、console.warn() 等方法,可用于写入任何 Node.js 流。

举例如下:

#clean src/ch02/Console_class.js
// 下面 4 行代码还没学,先不用管
var fs = require("fs");
const { Console } = require('console');
const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');

const logger = new Console(output, errorOutput);  // (1)

// 像 console 一样使用 logger
const count = 5;
logger.log('count: %d', count);  // (2)
logger.error(new Error("错误信息"));  // (3)
const name = "Swot";
logger.warn(`Danger ${name}! Danger!`)  // (4)
  1. 自定义的简单记录器

  2. stdout.log 文件中写入 count: 5

  3. stderr.log 文件中写入 Error: 错误信息 …​

  4. stderr.log 文件中写入 Danger Swot! Danger!

stdout.log
count: 5
stderr.log
Error: 错误信息
    at Object.<anonymous> (/Users/swot/learning/nodejs/src/ch02/Console_class.js:13:14)
    at Module._compile (node:internal/modules/cjs/loader:1099:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47
Danger Swot! Danger!

process 对象

process 对象是一个全局变量,其提供有关当前 Node.js 进程的信息井对其进行控制。作为一个全局变量,它始终可供 Node.Js 应用程序使用,无须加载 process 模块。它通常用于编写本地命令行程序。

进程事件

process 对象是 EventEmitter 类的实例,它提供多种事件,这里列出常用的几种事件。

exit 当 Node.js 进程准备退出时触发。此时无法阻止事件循环退出,并且一旦所有 exit 事件的监听器都完成运行,Node.js 进程将终止。

简单的示例代码参下面文件 process_exit.js

beforeExit: 当 Node.js 清空其事件循环并且没有其他工作要调度时,会触发此事件。通常, Node.js 进程将在没有调度工作时退出,但是在 beforeExit 事件上注册的监听器可以进行异步调用,从而导致 Node.js 进程继续。调用监听器回调函数时会将 process.exitCode (退出码) 值作为唯一参数传入。

uncaughtException: 未捕获的 JavaScript 异常一直处于冒泡状态,返回事件循环时,会触发此事件。默认情况下,Node.js 通过将堆栈跟踪打印到标准错误流并返回退出码1来处理此类异常,从而覆盖任何先前设置的 process.exitCode。为 uncaughtException 事件添处理程序会覆盖此默认行为。

信号(Signal)事件:当Node.is进程接收到一个信号时,会触发信号事件。信号名称可对照标准 POSIX 的信号名称列表,例如 SIGINT、SIGHUP。每个事件名称均以信号名称的大写表示。

#clean src/ch02/process_exit.js
console.log("程序开始执行");
process.on('exit', function(code) {
  // 以下代码永远不会执行
  setTimeout(function(){
    console.log("该代码不会执行");
  }, 0);
  console.log('退出码为:',code);
});
console.log("程序执行结束");

运行结果:

程序开始执行
程序执行结束
退出码为: 0
Important
exit 当 Node.js 进程准备退出时触发。所以 “退出码为: 0” 在最后打印。
退出状态码

如果没有异步操作任务正在等待执行,则 Node.js 会以状态码 0 正常退出,其他情形使用的状态码如下表:

状态码 名称&描述

1

Uncaught Fatal Exception 有未捕获异常,并且没有被域或 uncaughtException 处理函数处理。

2

Unused 保留,由 Bash 预留用于内置误用

3

Internal JavaScript Parse Error JavaScript的源码启动 Node 进程时引起解析错误。非常罕见,仅会在开发 Node 时才会有。

4

Internal JavaScript Evaluation Failure JavaScript 的源码启动 Node 进程,评估时返回函数失败。非常罕见,仅会在开发 Node 时才会有。

5

Fatal Error V8 里致命的不可恢复的错误。通常会打印到 stderr ,内容为: FATAL ERROR

6

Non-function Internal Exception Handler 未捕获异常,内部异常处理函数不知为何设置为on-function,并且不能被调用。

7

Internal Exception Handler Run-Time Failure 未捕获的异常, 并且异常处理函数处理时自己抛出了异常。例如,如果 process.on('uncaughtException') 或 domain.on('error') 抛出了异常。

8

Unused 保留,在以前版本的 Node.js 中,退出码 8 有时表示未捕获的异常。

9

Invalid Argument 可能是给了未知的参数,或者给的参数没有值。

10

Internal JavaScript Run-Time Failure JavaScript的源码启动 Node 进程时抛出错误,非常罕见,仅会在开发 Node 时才会有。

12

Invalid Debug Argument 设置了参数—​debug 和/或 --debug-brk,但是选择了错误端口。

128

Signal Exits 如果 Node 接收到致命信号,比如SIGKILL 或 SIGHUP,那么退出代码就是128 加信号代码。这是标准的 Unix 做法,退出信号代码放在高位。

process 对象的属性

Process 提供了很多有用的属性,便于我们更好的控制系统的交互:

序号 属性&描述

1

stdout 标准输出流。

2

stderr 标准错误流。

3

stdin 标准输入流。

4

argv argv 属性返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是node,第二个成员是脚本文件名,其余成员是脚本文件的参数。

5

execPath 返回执行当前脚本的 Node 二进制文件的绝对路径。

6

execArgv 返回一个数组,成员是命令行下执行脚本时,在Node可执行文件与脚本文件之间的命令行参数。

7

env 返回一个对象,成员为当前 shell 的环境变量

8

exitCode 进程退出时的代码,如果进程优通过 process.exit() 退出,不需要指定退出码。

9

version Node 的版本,比如v0.10.18。

10

versions 一个属性,包含了 node 的版本和依赖.

11

config 一个包含用来编译当前 node 执行文件的 javascript 配置选项的对象。它与运行 ./configure 脚本生成的 "config.gypi" 文件相同。

12

pid 当前进程的进程号。

13

title 进程名,默认值为"node",可以自定义该值。

14

arch 当前 CPU 的架构:'arm'、'ia32' 或者 'x64'。

15

platform 运行程序所在的平台系统 'darwin', 'freebsd', 'linux', 'sunos' 或 'win32'

16

mainModule require.main 的备选方法。不同点,如果主模块在运行时改变,require.main可能会继续返回老的模块。可以认为,这两者引用了同一个模块。

#clean src/ch02/process_attr.js
process.stdout.write("Hello World!" + "\n");  // 输出到终端

process.argv.forEach(function(val, index, array) {  // 通过参数读取
   console.log(index + ': ' + val);
});

console.log(process.execPath);  // 获取执行路径
console.log(process.platform);  // 平台信息
执行 node process_attr.js 输出如下:
Hello World!
0: /usr/local/Cellar/nvm/0.39.0/versions/node/v17.9.0/bin/node
1: /Users/swot/learning/nodejs/src/ch02/process_attr.js
/usr/local/Cellar/nvm/0.39.0/versions/node/v17.9.0/bin/node
darwin
执行 node process_attr.js a b c 输出如下:
Hello World!
0: /usr/local/Cellar/nvm/0.39.0/versions/node/v17.9.0/bin/node
1: /Users/swot/learning/nodejs/src/ch02/process_attr.js
2: a
3: b
4: c
/usr/local/Cellar/nvm/0.39.0/versions/node/v17.9.0/bin/node
darwin
process 对象的方法

Process 提供了很多有用的方法,便于我们更好的控制系统的交互:

序号 属性&描述

1

abort() 这将导致 node 触发 abort 事件。会让 node 退出并生成一个核心文件。

2

chdir(directory) 改变当前工作进程的目录,如果操作失败抛出异常。

3

cwd() 返回当前进程的工作目录

4

exit([code]) 使用指定的 code 结束进程。如果忽略,将会使用 code 0。

5

getgid() 获取进程的群组标识(参见 getgid(2))。获取到的是群组的数字 id,而不是名字。 注意:这个函数仅在 POSIX 平台上可用(例如,非Windows 和 Android)。

6

setgid(id) 设置进程的群组标识(参见 setgid(2))。可以接收数字 ID 或者群组名。如果指定了群组名,会阻塞等待解析为数字 ID 。 注意:这个函数仅在 POSIX 平台上可用(例如,非Windows 和 Android)。

7

getuid() 获取进程的用户标识(参见 getuid(2))。这是数字的用户 id,不是用户名。 注意:这个函数仅在 POSIX 平台上可用(例如,非Windows 和 Android)。

8

setuid(id) 设置进程的用户标识(参见setuid(2))。接收数字 ID或字符串名字。如果指定了群组名,会阻塞等待解析为数字 ID 。 注意:这个函数仅在 POSIX 平台上可用(例如,非Windows 和 Android)。

9

getgroups() 返回进程的群组 ID 数组。POSIX 系统没有保证一定有,但是 node.js 保证有。 注意:这个函数仅在 POSIX 平台上可用(例如,非Windows 和 Android)。

10

setgroups(groups) 设置进程的群组 ID。这是授权操作,所以你需要有 root 权限,或者有 CAP_SETGID 能力。 注意:这个函数仅在 POSIX 平台上可用(例如,非Windows 和 Android)。

11

initgroups(user, extra_group) 读取 /etc/group ,并初始化群组访问列表,使用成员所在的所有群组。这是授权操作,所以你需要有 root 权限,或者有 CAP_SETGID 能力。 注意:这个函数仅在 POSIX 平台上可用(例如,非Windows 和 Android)。

12

kill(pid[, signal]) 发送信号给进程. pid 是进程id,并且 signal 是发送的信号的字符串描述。信号名是字符串,比如 'SIGINT' 或 'SIGHUP'。如果忽略,信号会是 'SIGTERM'。

13

memoryUsage() 返回一个对象,描述了 Node 进程所用的内存状况,单位为字节。

14

nextTick(callback) 一旦当前事件循环结束,调用回调函数。

15

umask([mask]) 设置或读取进程文件的掩码。子进程从父进程继承掩码。如果mask 参数有效,返回旧的掩码。否则,返回当前掩码。

16

uptime() 返回 Node 已经运行的秒数。

17

hrtime() 返回当前进程的高分辨时间,形式为 [seconds, nanoseconds]数组。它是相对于过去的任意事件。该值与日期无关,因此不受时钟漂移的影响。主要用途是可以通过精确的时间间隔,来衡量程序的性能。 你可以将之前的结果传递给当前的 process.hrtime() ,会返回两者间的时间差,用来基准和测量时间间隔。

#clean src/ch02/process_method.js
console.log('当前目录: ' + process.cwd());  // 输出当前目录

console.log('当前版本: ' + process.version);  // 输出当前版本

console.log(process.memoryUsage());  // 输出内存使用情况
执行 node process_method.js 输出如下:
当前目录: /Users/swot/learning/nodejs/src/ch02
当前版本: v17.9.0
{
  rss: 25812992,
  heapTotal: 5283840,
  heapUsed: 4542016,
  external: 459236,
  arrayBuffers: 11166
}

Node.js 的定时器

定时器 timers 模块对外暴露一个全局的 API,用于调度在某个时段调用的函数。

因为定时器函数是全局变量,所以不需要加载 timers 模块来使用它。

Node.js 的定时器函数实现了与 Web 浏览器提供的定时器 API 类似的 API,但是它们使用了不同的内部实现机制,Node.js 的定时器函数是基于 Node.js 事件循环构建的。

设置定时器

Node.js 中的定时器在一段时间后会调用给定的函数。何时调用定时器函数取决于用来创建定时器的方法及 Node.js 事件循环正在执行的其他工作。

一次性定时器

基本用法 setTimeout(callback, delay[, …​args])

例如,下面的代码将在 1 秒后输出提示信息,之后定时器就不再起作用。

setTimeout(function () {
  console.log("我是一个一次性定时器");
}, 1000);
Caution
定时器可能不会精确地在指定的时刻调用回调函数。Node.js 不保证回调被触发的确切时间,也不保证它们的顺序。回调函数会尽可能接近指定的时间,并在该时间点被调用。
周期性定时器

基本用法 setInterval(callback, delay[, …​args])

例如,下面的代码将在 1 秒之后输出提示信息,之后定时器每隔 1 秒就重复输出提示信息,除非使用 clearlnterval() 方法取消该定时器或者终止程序。

setInterval(function () {
  console.log("我是一个周期性的定时器");
},1000);
即时定时器

基本用法 setImmediate(callback[, …​args])

function sleep(delay) {
  var start = new Date().getTime()
  while(new Date().getTime() - start < delay) {
    continue
  }
  console.log('ok')
}
console.log('start')
sleep(2000)
console.log('end')

先打印出 start,然后等待 2000ms 后再打印 ok 和 end

function sleep(delay) {
  var start = new Date().getTime()
  while(new Date().getTime() - start < delay) {
    continue
  }
  console.log('ok')
}
console.log('start')
setImmediate(sleep, 2000)  // (1)
console.log('end')

先打印 start,再打印 end, 最后等待 2000ms 后打印 end

  1. 出现这种情况的原因是:setImmediate 将其中的函数作为宏任务,放到 C++ 一个线程中去执行,然后将其放入任务队列中。所以不会影响主线程的执行。

取消定时器

分别用 clearTimeout()、clearInterval() 和 clearImmediate() 方法取消相应定时器,防止该定时器触发。

var testInterval = setInterval(testFunc,2000);
...
clearInterval(testInterval);

Timeout 和 Immediate 类

Node.js 内置两个有关定时器的类 Timeout 和 Immediate,可用于创建相应的对象。

  • Timeout 对象在内部创建,并由 setTimeout() 或 setInterval() 方法返回,可以传递给 clearTimeout() 或 clearInterval() 以取消定时器。

  • Immediate 对象也在内部创建,并由 setImmediate() 方法返回。它可以传递给 clearImmediate() 以取消即时定时器。

Note
需要注意的是,setTimeout() 或 setInterval() 方法中的 this 关键字在 JavaScript 中均指向 Window 对象,而在 Node.js 中, 则都指向 Timeout 对象。

setImmediate() 方法与 setTimeout() 方法

setImmediate() 和 setTimeout() 类似,但由于它们的调用时机不同,所以行为也不相同。

  • setlmmediate() 在当前轮询阶段完成后执行脚本

  • setTimeout() 在指定周期的最小阈值后调度脚本运行

#clean src/ch02/timeout_vs_immediate0.js

上面的这个流程说简单点就是在一个异步流程里,setImmediate 会比定时器先执行,我们写点代码来试试:

console.log('outer');

// EventLoop: timers
setTimeout(() => {  // (1)(7)
  setTimeout(() => {  // (2)
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {  // (3)
    console.log('setImmediate');
  });
}, 0);  // (4)
// EventLoop: pending callbacks
// EventLoop: idle, prepare
// EventLoop: poll
// EventLoop: check   (5)
// EventLoop: close callbacks (6)

运行命令: $ node timeout_vs_immediate0.js

运行结果
outer
setImmediate
setTimeout

代码分析 :

  1. 外层是一个 set Timeout ,所以执行他的回调的时候已经在 timers 阶段了

  2. 处理里面的 setTimeout,因为本次循环的 timers 正在执行,所以他的回调其实加到了下个 timers 阶段

  3. 处理里面的 setImmediate,将它的回调加入 check 阶段的队列

  4. 外层 timers 阶段执行完,进入 pending callbacksidle, preparepoll,这几个队列都是空的,所以继续往下

  5. 到了 check 阶段,发现了 setImmediate 的回调,拿出来执行

  6. 然后是 close callbacks,队列是空的,跳过

  7. 又是 timers 阶段,执行 console.log('setTimeout');

#clean src/ch02/timeout_vs_immediate1.js

定时器执行的顺序根据调用它们的上下文有所不同。如果定时器在主模块内被调用,则计时将受进程性能的约束,比如可能会受到计算机上运行的其他应用程序的影响。

举例如下:

console.log("outer");  // (1)

setTimeout(() => {
  console.log('setTimeout');
}, 0);  // (2)

setImmediate(() => {  // (3)
  console.log('setImmediate');
});
// (4)

运行命令: $ node timeout_vs_immediate1.js

运行结果不确定(可能出现如下两种情况)
outer
setTimeout
setImmediate
outer
setImmediate
setTimeout

分析如下:

  1. 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段

  2. node.js 里面 setTimeout(fn, 0) 会被强制改为 setTimeout(fn, 1), 这在官方文档中有说明

    所以遇到 setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入 times 阶段

  3. 遇到 setImmediate 塞入 check 阶段

  4. 同步代码执行完毕,进入 Event Loop

    • 先进入 times 阶段,检查当前时间过去了 1 毫秒没有,如果过了 1 毫秒,满足 setTimeout 条件,执行回调,如果没过 1 毫秒,跳过。

    • 跳过空的阶段,进入 check 阶段,执行 setImmediate 回调

#clean src/ch02/timeout_vs_immediate2.js
var fs = require('fs')

// EventLoop: timers
// EventLoop: pending callbacks
// EventLoop: idle, prepare
// EventLoop: poll
fs.readFile(__filename, () => {  // (1)
  setTimeout(() => {  // (2)
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {  // (3)
    console.log('setImmediate');
  });
});
// EventLoop: check
// EventLoop: close callbacks

运行命令: $ node timeout_vs_immediate2.js

运行结果
setImmediate
setTimeout

代码分析:

  1. 这里 setTimeout 和 setImmediate 在 readFile 的回调里面,由于 readFile 回调是 I/O 操作,他本身就在 poll 阶段。

  2. 所以他里面的定时器只能进入下个 timers 阶段。

  3. 但是 setImmediate 却可以在接下来的 check 阶段运行,所以 setImmediate 肯定先运行,他运行完后,去检查 timers,才会运行 setTimeout。

process.nextTick() 与 setImmediate() 的对比

process.nextTick() 是一个特殊的异步 API,他不属于任何的 Event Loop 阶段。事实上 Node 在遇到这个 API 时,Event Loop 根本就不会继续进行,会马上停下来执行 process.nextTick(),这个执行完后才会继续 Event Loop。我们写个例子来看下:

#clean src/ch02/nextTick_vs_setImmediate.js
var fs = require('fs')

// EventLoop: timers
// EventLoop: pending callbacks
// EventLoop: idle, prepare
// EventLoop: poll
fs.readFile(__filename, () => {  // (1)
  setTimeout(() => {  // (2)
    console.log('setTimeout');  // (7)
  }, 0);
  setImmediate(() => {  // (3)
    console.log('setImmediate');  // (5)
    process.nextTick(() => {
      console.log('nextTick 2');  // (6)
    });
  });
  process.nextTick(() => {  // (4)
    console.log('nextTick 1');
  });
// EventLoop: check
// EventLoop: close callbacks
});

运行命令: $ node nextTick_vs_setImmediate.js

运行结果
nextTick 1
setImmediate
nextTick 2
setTimeout

代码分析:

  1. 代码都在 readFile 回调里面,他自己执行时,已经在 poll 阶段

  2. 遇到 setTimeout(fn, 0),其实是 setTimeout(fn, 1) 塞入后面的 timers 阶段。

  3. 遇到 setImmediate,塞入后面的 check 阶段

  4. 遇到 nextTick,立马执行,输出 'nextTick 1'

  5. 到了 check 阶段,输出 'setImmediate'

  6. 又遇到个 nextTick,立马输出 'nextTick 2'

  7. 到了下个 timers 阶段,输出 'setTimeout'

Buffer 数据类型

Buffer 实例的创建

  • 可以将Buffer视为一种用来处理二进制数据的数据类型。

  • Buffer类的实例(即对象)类似于整数数组,但实例对应于固定大小的原始内存分配,其大小在创建时被确定且无法更改。

  • Buffer 类位于全局作用域中,因此无须使用 require() 方法加载相应的模块。

Buffer实例创建使用 Buffer.from()、Buffer.alloc() 或 Buffer.allocUnsafe() 方法。


创建一个包含数组 [0x1, 0x2, 0x3] 的 Buffer 实例

const buf1 = Buffer.from([1, 2, 3]);

创建一个包含 UTF-8 字节 [0x74, 0xc3, 0xa9, 0x73, 0x74] 的 Buffer实例

const buf2 = Buffer.from('tést');

创建一个包含 Latin-1 字节 [0x74, 0xe9, 0x73, 0x74] 的 Buffer实例

const buf3 = Buffer.from('tést', 'latin1');

创建一个长度为 10、且用零填充的 Buffer实例

const buf4 = Buffer.alloc(10);

创建一个长度为 10、且用 0x1 填充的 Buffer实例

const buf5 = Buffer.alloc(10, 1);

创建一个长度为 10、且未初始化的 Buffer实例

const buf6 = Buffer.allocUnsafe(10);

Tip
这个方法比调用 Buffer.alloc()更快,但返回的 Buffer 实例可能包含旧数据,因此需要使用 fill() 或 write() 重写。

Buffer 用于编码转换

  • Buffer实例一般用于表示编码字符的序列,如UTF-8、UCS2(UTF-16)、Base64 或十六进制编码的数据。

  • 在文件操作和网络操作中,如果没有显式声明编码格式,返回数据的默认类型为 Buffer。

  • 通过使用显式字符编码将 Buffer 实例与 JavaScript 字符串相互转换。

  • 在创建 Buffer 实例时指定存入字符串的字符编码

    • const buf = Buffer.from('hello world', 'ascii');

  • 将已创建的 Buffer 实例转换成字符串的语法

    • buf.toString([encoding[, start[, end]]])

下面是一个简单的例子:

const buf = Buffer.from('tést');
console.log(buf.toString('hex'));         // 输出结果: 74c3a97374
console.log(buf.toString('utf8', 0, 3));  // 输出结果:té

Node.js 目前支持的字符编码如下:

  • ascii: 仅适用于 7 位 ASCII 数据。此编码速度很快,如果设置这种编码,将从数据中删除高位。

  • utf8: 多字节编码的 Unicode 字符。许多网页和文档都使用这种编码格式。

  • utf16le: 2个或4个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。 ucs2是它的别名。

  • base64: Base6编码。

  • latin1: 一种将 Buffer 编码成单字节编码字符串的方法。binary 是 latin1 的别名。

  • hex: 将每个字节编码成两个十六进制的字符。

Buffer 实例与JSON 对象互相转换

#clean src/ch02/buf_to_json.js

使用 buf.toJSON() 方法将 Buffer 实例转换为 JSON 对象,适用于将二进制数据转换为 JSON 格式。

示例
const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]);
const json = JSON.stringify(buf);
console.log(json);  // (1)

const copy = JSON.parse (json, (key, value) => {
  console.log('key:', key)
  console.log('value:', value)
  console.log('value.type:', value.type)
  console.log('value.data:', value.data)
  return value && value.type === 'Buffer'
      ? Buffer.from(value.data)
      : value;
});
console.log(copy);  // (2)
  1. json 是一个字符串 '{"type":"Buffer","data":[1,2,3,4,5]}'

  2. <Buffer 01 02 03 04 05>

JSON.parse 里的 4 个 console 打印结果
... 前面的输出内容都没用,省略
key:
value: {type: 'Buffer', data: Array(5)}
value.type: Buffer
value.data: (5) [1, 2, 3, 4, 5]

Buffer 实例基本操作

Buffer 写入实例

使用 buf.write() 方法将字符串写入 Buffer 实例

buf.write(string[, offset[, length]][, encoding])
示例
const buf = Buffer.alloc(256);
const len = buf.write('\u00bd + \u00bc = \u00be', 0);
console.log(`${len} 个字节: ${buf.toString('utf8', 0, len)}`);
// 输出: 12 个字节: ½ + ¼ = ¾
Buffer 读取实例
  • 使用 buf.toString() 方法从 Buffer 实例读取字符串

  • 使用其他专用方法从Buffer实例读取其他类型的数据

const buf = Buffer.from([-1, 5]);
console.log(buf.readInt8(0));// 输出结果: -1
console.log(buf.readInt8(1));// 输出结果: 5
console.log(buf.readInt8(2));// 抛出异常 ERR_OUT_OF_RANGE(超出范围)
Buffer 实例合并

可以使用 Buffer.concat()方 法将多个 Bufer 实例拼接合并组成一个新的 Bufer 实例。该方法语法格式如下:

Buffer.concat (list[, totalLength])
  • 参数 list 给出要合并的 Buffer 数组或 Uint8Array 数组

  • totaLength 指定合并后 Buffer 实例的总长度。

  • 如果 list 中没有元素或 totalLength 为 0,则返回一个长度为 0 的 Buffer

  • 如果没有指定 totalLength,则计算 list 中的 Buffer 实例的总长度。

  • 如果 list 中的 Buffer 实例总长度大于 totaLength,则合并后的 Buffer 实例会被截断 totalLength 的长度。

这里给出简单的示例:

const buf1 = Buffer.alloc(5);
const buf2 = Buffer.alloc(7);
const totalLength = buf1.length + buf2.length;
const bufA = Buffer.concat([buf1, buf2], totalLength);
console.log(bufA);  // <Buffer 00 00 00 00 00 00 00 00 00 00 00 00>
Buffer 实例复制

可以使用 buf.copy() 方法将一个 Butter 实例(源对象)某个区域的数据复制到另一个 Buffer 实例(目标对象)的某个区域,目标对象的内存区域可能会与源对象的内存区域重叠。该方法语法格式如下:

buf.copy (target[, targetStart[, sourceStart[, sourceEnd]]])
  • 参数 target 指定目标对象,可以是 Buffer 或 Uint8Array 类型;

  • targetStart 指定开始写入目标对象的偏移量,默认值为 0;

  • sourceStart 指定源对象 buf 开始复制的偏移量,默认值为 0;

  • sourceEnd 指定源对象 buf 结束复制的偏移量,默认值为 buf.lenath

Buffer 实例切片

可以使用 buf.slice() 方法创建一个裁剪过的新 Buffer 实例。该方法语法格式如下:

buf.slice(start[, end]])
  • start 指定开始切片的偏移量,默认值为 0

  • end 指定结束切片的偏移量,默认值为 buf.length

  • 该方法返回一个指向与源 Buffer 实例同一内存区域的新 Buffer 实例,但对新 Buffer 实例进行了裁剪。

  • 修改新建的 Buffer 切片实例,也会同时修改源 Buffer 实例,因为两个对象所分配的内存区域是重叠的。

Node.js 的流操作

概述

流可以看作是某段时间内从一个点移动到另一个点的数据序列,就像水从一根管子的一端流到另-端。 Node.js中的流用于管理和处理数据,可以使用流完成对大量数据的操作以及逐段处理的操作。在 Node.js 异步处理数据的基础上,流将要传输的数据处理成“块”(chunk) 连续传输,这样就可使用较低的内存消耗获得较高的性能提升。

流是 Node.js 中处理流式数据的抽象接口。Node.js 提供了多种流对象,例如 HTTP 服务器的请求和process.stdout 都是流的实例。流可以是可读的、可写的,或者可读可写的。所有流都是 EventEmitter 类的实例。stream 模块用于构建实现了流接口的对象,使用时需要导入,方法如下:

const stream = require('stream');

stream 模块主要用于开发人员创建新类型的流实例。几乎所有的 Node.js 应用程序都在某种程度上使用了流。应用程序只需写入数据到流,或者从流消费数据,并不需要直接实现流的接口,通常也不需要直接加载 stream 模块。本节给出的示例代码没有直接实现流的接口,而是直接利用 fs 模块内置的文件操作流接口。

流的基本类型:

  • 可写流(Writable)

  • 可读流(Readable)

  • 双工流(Duplex)

  • 转换流(Transform)

流的实现类型

  • 普通的流:基于字符串和 Buffer(或 Uint8Array)工作

  • 对象模式的流:使用其他类型的 JavaScript 值工作

可写流和可读流都会在内部的缓冲器中存储数据, 可缓冲的数据大小取决于传入流构造函数的 highWaterMark 选项(相当于水位线)。

双工流和转换流都是可读又可写的,各自维护着两个相互独立的内部缓冲器用于读取和写入。在维护数据流时,读取和写入两端可以各自独立地工作。

可读流

可读流是对提供数据的来源的一种抽象。所有可读流都实现了 stream.Readable 类定义的接口。


可读流有两种模式:流动(Flowing)和暂停(Paused)。

所有可读流一开始都处于暂停模式,之后可以通过以下方式切换到流动模式。

  • 添加 data 事件处理函数。

  • 调用 stream.resume() 方法

  • 调用 stream.pipe() 方法

可读流也可以通过以下方式切换回暂停模式。

  • 如果没有管道目标,则调用 stream.pause() 方法。

  • 如果有管道目标,则移除所有管道目标。调用 stream.unpipe() 方法可以移除多个管道目标。


stream.Readable 类定义的主要事件

  • data:当有数据可读时被触发。

  • end:没有更多的数据可读时被触发。

  • error:当接收过程中发生错误时被触发。

  • readable:当流中有数据可供读取时被触发。当到达流数据的尽头时,readable 事件也会被触发,但是在 end 事件之前被触发。

  • close: 当流或其底层资源被关闭时被触发。这表明不会再触发其他事件,也不会再发生操作。


stream.Readable 类定义的主要方法

  • readable.read([size]):从内部缓冲区拉取并返回数据(默认返回是 Buffer 对象),size 指定要读取的数据的字节数。如果没在可读的数据,则返回 null。它只对处于暂停模式的可读流调用。

  • readable.pause():使流动模式的流停止触发data事件,并切换出流动模式。任何可用的数据都会保留在内部缓存中。

  • readable.resume():将被暂停的可读流恢复触发 data 事件,并将流切换到流动模式。

  • readable.setEncoding(encoding):为从可读流读取的数据设置字符编码。

下面是可读流操作的例子

#clean src/ch02/test_readablestream.js
const fs = require('fs')

var readStream=fs.createReadStream('demo.txt');  // (1)

var str='';
readStream.on('data',function(chunk){  // (2)
    str+=chunk;
})
readStream.on('end',function(chunk){  // (3)
    console.log(str);
})
readStream.on('error',function(err){  // (4)
    console.log(err);
})
  1. 以流的方式读取文件

  2. 保存数据

  3. 读取完成

  4. 读取失败

运行 $ node test_readablestream.js 输出如下:
周董好久没有发新歌了,左盼右盼,终于等来了《说好不哭》

可写流

可写流是对数据被写入的目的地的一种抽象。所有可写流都实现了 stream.Writable 类定义的接口。


stream.Writable 类定义的主要事件如下:

  • close: 当流或其底层资源被关闭时被触发。不是所有可写流都会触发该事件。

  • drain: 如果调用stream.wite(chunk)方法返回false,则当可以继续写入数据到流时会触发该事件。

  • error: 写入数据发生错误时被触发。当触发该事件时,流还未被关闭。

  • finish: 调用 stream.end() 方法且缓冲数据都已传给底层系统之后被触发。

  • pipe: 当在可读流上调用 stream.pipe() 方法时被触发。

  • unpipe: 当在可读流上调用 stream.unpipe() 方法时被触发。


stream.Witable类定义的主要方法是

  • writable.write(chunk[, encoding][, callback])。这个方法写入数据到流,并在数据被完全处理之后调用回调函数。

下面给出一个可写流操作的例子。

#clean src/ch02/test_writablestream.js
const fs = require('fs')

var str = '这是测试可写流的数据,保存起来';
var writerStream = fs.createWriteStream('output.txt');  // (1)

writerStream.write(str,'UTF8');  // (2)
writerStream.end();  // (3)

// 处理流事件
writerStream.on('finish', function() {
    console.log('写入完成!');
});
writerStream.on('error', function(err){
    console.log('写入失败');
});
  1. 创建一个可以写入的流,写入到文件output.txt 中

  2. 使用 utf8 编码写入数据

  3. 标记文件末尾


运行 node test_writablestream.js 终端打印如下
写入完成!

查看 output.txt,内容如下:

这是测试可写流的数据,保存起来

管道读写操作

通常要将从一个流中获取的数据传递到另外一个流中,这可以通过管道操作机制实现。可读流提供的 readable.pipe() 方法在可读流与可写流之间架起桥梁,使数据可以通过管道由可读流进入可写流。该方法的语法如下:

readable.pipe (destination[, options])
  • 参数 destination 指定数据写入的目标

  • options 用于设选项,其中 end 指定当读取结束时是否终止写入,默认为 true。

  • 该方法返回目标可写流,如果该方法返回的目标可写流是双工流或转换流,则可以形成管道链。


下面的代码将可读流的所有数据通过管道推送到file.txt 文件:

const readable = getReadableStreamSomehow();       // 创建一个可读流
const writable = fs.createWriteStream('file.txt'); // 创建一个可写流
readable.pipe(writable); //管道读写操作,将 readable 的所有数据都推送到文件 file.txt

可以在单个可读流上绑定多个可写流。readable.pipe()会返回目标流的引用,这样就可以对流进行链式管道操作。下面给出一段示例代码:

const fs = require('fs');
const r = fs.createReadStream('file.txt');
const z = zlib.createGzip();
const w = fs.createWriteStream('file.txt.gz');
r.pipe(z).pipe(w);// 链式管道操作两个可写流

默认情况下,当来源可读流触发 end 事件时,目标可写流也会调用 stream.end() 结束写入。如果禁用这种默认行为,end 选项应设为 false,这样目标流就会保持打开状态,例如:

reader.pipe(writer, {end: false});
reader.on('end', () => {
  writer.end('结束');
});

如果可读流发生错误,目标可写流不会自动关闭,因此需要手动关闭所有流以避免内存泄漏。

还有一个 readable.unpipe() 方法用于解绑之前使用 stream.pipe() 方法绑定的可写流, 它有一个参数 destination,用于指定要移除管道的可写流。

  • 如果没有指定该参数,则解绑所有管道。

  • 如果指定 destination 参数,但它并没有建立管道,则不起作用。

实战演练 — 提供图片浏览服务

本章小结

习题

3. 模块与包的管理和使用

无论使用什么编程语言,组织代码都是极其重要的,Nodejs的模块系统提供了功能强大的代码组织机制。

Node.js 以模块为单位划分所有功能,便于开发人员将应用程序划分为若干不同的部分,并通过模块的组织和管理实现不同部分有机地整合。

Node,js 的内置模块远远不能满足日常开发需要,因此学会引入第三方模块是一个开发人员必须掌握的技能。

第三方模块以包(Package)的形式提供,开发人员可以通过包管理器 npm 从官方仓库下载各种各样的包,也可以发布自己开发的包,从而实现代码的分享和重用,提高开发效率。

Node.js 目前已成为大规模的开放源代码的生态系统。本章将针对Node.js模块和包的管理及使用进行详细讲解。

3.1 Node.js 的模块系统

模块是 Node.js 使代码易于重用的一种组织和包装方式。本节将对 Node.js 的模块及模块加载机制进行讲解。

Node.js 为什么要使用模块

  • Node.js将可重用代码封装在各种模块中,减少应用程序的代码量,提高开发效率和代码的可读性。

  • 模块打包代码的方式不会改变全局作用域,开发人员可以在被载入的模块文件中选择要对外暴露的方法和变量。

  • 模块可以发布到npm仓库中与他人共享。

  • 开发人员可以使用来自npm仓库的第三方模块,而不必担心某个模块会覆盖其他模块的变量和方法。


exports
Figure 7. 模块工作机制

自定义模块

主流的 JavaScript 模块规范有两种,分别是 AMD 和 CommonJS。

AMD 规范要求异步加载模块,允许指定回调函数,并在回调函数中执行操作。对于浏览器环境,要从服务器端加载模块,必须采用非同步模式,因此浏览器端般采用 AMD规范。

CommonJS 规范要求同步加载模块,也就是说,只有模块加载完成,才能执行后面的操作。CommonJS 是服务器端广泛使用的模块化机制。Node.js 主要用于服务器端编程,模块文件一般保存于本地磁盘,加载速度较快,因而无须考虑非同步加载的方式,所以选择 CommonJS 规范。

按照 CommonJS 规范要求,模块必须通过 module.exports 对象导出对外暴露的变量或接口,通过 require() 方法将其他模块的输出加载到当前模块作用域中。

在 Node.js 模块系统中,每个文件都被视为独立的模块,有目己的作用域,其变量、方法等都对其他文件不可见的。当然,模块也可以是包含一个或多个文件的目录。

模块可以是一个包含 exports 对象属性定义的文件,这些属性可以是任意类型的数据,比如字符串、对象和方法。可将 exports 看作是 module. exports 的简单引用形式。通过在 exports 对象上指定额外的属性,可以将方法和对象等添加到模块的根部。例如,创建一个名为 hello.js 的文件,添加代码来定义模块。

#clean src/ch03/moduledef/hello_exports_method.js
var show_day = new Array('星期一','星期二','星期三','星期四','星期五','星期六','星期日');
var nowTime = new Date();
var day = nowTime.getDay();

exports.sayHello = function(name) {
  console.log('你好!' + name);
  console.log('今天是' + show_day[day-1]);
};

导入模块

使用上述模块要用到 Node.js 的 require() 方法,该方法以要使用的模块的文件路径为参数。Node.js 找到该模块文件并加载其内容。这里创建一个名为 index.js 的文件,通过添加代码来导入模块。

Tip

使用 require() 导入模块是同步 I/O 操作,同步调用会阻塞 Node.js,因此尽量不要在 I/O 操作密集的地方使用 require 方法。

通常只在程序最初加载时使用 require,模块一般在文件顶部导入。

#clean src/ch03/moduledef/index_require_hello_exports_method.js
var hello = require('./hello_exports_method');  // (1)
hello.sayHello('小王');  // (2)
运行 node moduledef/index.js 显示如下
你好!小王
今天是星期三  // (3)
  1. 例中 require() 方法参数中的 ./ 代表当前目录,Node.js 模块文件的默认扩展名为 .js,因此可以省略此扩展名,这样执行该文件时就会在该文件同一目录下查找该模块文件。

  2. Node.js 定位到该模块之后,require() 方法就会返回这个模块定义的 exports 对象中的内容,然后就可以使用模块中 exports 对象的成员方法了。

  3. 运行时的具体日期

使用 module.exports 定义模块

在 Node.js 中,exports 是模块公开的接口,require() 方法用于从外部获取一个模块的接口,即所获取模块的 exports 对象。

如果希望模块成为某个对象的导出,则应将要导出的对象赋值给 module.exports 属性。

如果将要导出的对象赋值给 exports 对象,则会简单地重新绑定本地的 exports 变量。

  • 使用 module.exports 对象可以对外提供 单个交量、方法或者对象

    • 下面 hello_module_exports_method.js 导出了单个方法

  • 使用 module.exports 对象可以对外提供 多个变量和方法

    var baseSalary = 3000.00;  // 基本工资
    var getSalary = function (bonus) {  // 计算工资
      return baseSalary + bonus;  // 基本工资加资金
    };
    module.exports.baseSalary = baseSalary;  // (1)
    module.exports.getSalary = getSalary;  // (2)

    导出了一个变量和一个方法:

    1. 导出 baseSalary 变量

    2. 导出 getSalary 方法

  • 使用 module.exports 导出一个对象

    • 下面 hello_module_exports_object.js 导出单个对象(Hello 大写了,我们称为类)


在每个模块内部,module 对象代表当前模块。它的 exports 属性是对外的接口,将模块的接口对模块外部暴露。

其他文件加载该模块,实际上就是读取 module.exports 属性。

#clean src/ch03/moduledef/hello_module_exports_method.js
var show_day = new Array('星期一','星期二','星期三','星期四','星期五','星期六','星期日');
var nowTime = new Date();
var day = nowTime.getDay();

var sayHello = function(name) {
  console.log('你好!' + name);
  console.log('今天是' + show_day[day-1]);
};
module.exports.sayHello = sayHello;
#clean src/ch03/moduledef/index_require_hello_module_exports_method.js
var hello = require('./hello_module_exports_method');  // (1)
hello.sayHello('小王');  // (2)
运行 node moduledef/index.js 显示如下
你好!小王
今天是星期三  // (3)
  1. 例中 require() 方法参数中的 ./ 代表当前目录,Node.js 模块文件的默认扩展名为 .js,因此可以省略此扩展名,这样执行该文件时就会在该文件同一目录下查找该模块文件。

  2. Node.js 定位到该模块之后,require() 方法就会返回这个模块定义的 module.exports 对象中的内容,然后就可以使用模块中 module.exports 对象的成员方法了。

  3. 运行时的具体日期

#clean src/ch03/moduledef/hello_module_exports_object.js
var show_day = new Array('星期一','星期二','星期三','星期四','星期五','星期六','星期日');
var nowTime = new Date();
var day = nowTime.getDay();

function Hello (name) {  // (1)
  this.name = name;
  this.sayHello = function () {
    console.log('你好!' + this.name);
    console.log('今天是' + show_day[day-1]);
  };
};
module.exports = Hello;  // (2)
  1. Hello 大写,我们认为是定义了一个类(实际用小写也没问题)

  2. 导出单个对象(不再是导出 module.exports 里的属性或者方法)

#clean src/ch03/moduledef/index_require_hello_module_exports_object.js
var Hello = require('./hello_module_exports_object');
var hello = new Hello('小张');   // 此处需要实例化类
hello.sayHello();

exports 与 module.exports 的关系

  • exports 在模块的文件级别作用域内有效,它在模块被执行前被赋予 module.exports 的值。

  • 最终返回给调用程序的是 module.exports 而不是 exports

    如果创建了一个既有 module.exports 又有 exports 的模块,那么该模块只会返回 module.exports,而 exports 会被忽略。例如:

    module.exports.hello = true; // 会被导出
    exports = { hello: false };  // 不会被导出,仅在模块内有效
Tip

具体使用时一定要注意exports与module.exports之间的区别。

使用 exports 导出的方法返回的是模块函数,可以直接调用;

而由 module.exports 导出的方法返回的是一个类,需实例化为对象之后才可以调用。

模块加载顺序

  1. 从缓存中加载

  2. 加载核心模块

  3. 加载文件模块

  4. 文件夹作为模块

    1. 加载 package.json 文件指定的文件

    2. 加载 index.js 文件

    3. 加载 index.node 文件

  5. 从 node_modules 目录加载

  6. 从全局目录加载

  7. 循环加载


模块加载顺序分析

#clean src/ch03/moduledef/main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);

运行 node main.js 输出如下:

b.js  a1      src/ch03/moduledef/b.js:2  // (1)
a.js  b2      src/ch03/moduledef/a.js:2
main.js  a2   src/ch03/moduledef/main.js:1
main.js  b2   src/ch03/moduledef/main.js:2

通过 debug 可以发现:

  1. 运行时,Node 返回 a.js 的不完整版本,后面代码执行,就是取的前面缓存结果了。

#clean src/ch03/moduledef/a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
#clean src/ch03/moduledef/b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';

3.2 使用 Node.js 的核心模块

通常优先使用 Node.js 内置的核心模块,下面介绍部分通用核心模块的使用方法。至于fs (文件系统操作) net (网络通信)和http (Web )等重要的核心模块,会在后面章节中专门讲解。

os 模块—​提供系统操作方法

导入方式: const os = require('os');

  1. 属性:os 模块的 os.EOL 属性用于定义操作系统相关的行末标志

    • \n 用于 POSIX 系统

    • \r\n 用于 Windows 系统

  2. 方法:os模块提供了许多方法,这里列出部分主要的方法。

    方法 描述

    s.type()

    返回操作系统名称。

    s.platom()

    返回Node,js编译时的操作系统平台名称。

    s.hostname()

    返回操作系统的主机名。

    s.arch()

    返回Node.js二进制编译所用的操作系统CPU架构。

    s.cpus()

    返回-个对象数组,其包含每个逻辑CPU内核的信息。,

    s.uptime()

    返回操作系统的上线时间。

    s.networkInterfaces()

    返回包括网络地址的网络接口列表。

    s.totalmem()

    返回系统内存总量(单位为字节)。

    s.freemem()

    返回操作系统空闲内存量(单位为字节)。

  3. 常量:由 os.constants 返回一个包含错误码、处理信号等通用的操作系统特定常量的对象。

    • 错误常量由 os.constants.errno 给出

    • WSAEFAULT 表示无效的指针地址

    • 信号常量由 os.constants.sinnals 给出

    • SIGKILL 表示立即终止进程。

      Note
      不同的操作系统所支持的常量会有差别。
演示 os 模块部分方法的使用
const os = require('os');
console.log('操作系统类型:' + os.type());
console.log('操作系统平台: ' + os.platform());
console.log('系统内存总量:' + os.totalmem() + " 字节");
console.log('空闲内存量:' + os.freemem() + " 字节");
console.log('CPU信息:');
console.log( os.cpus());
运行结果如下
操作系统类型:Darwin
操作系统平台: darwin
系统内存总量:17179869184 字节
空闲内存量:55435264 字节
CPU信息:
[
  {
    model: 'Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz',
    speed: 2200,
    times: { user: 99913180, nice: 0, sys: 60906320, idle: 720479090, irq: 0 }
  },
  ...
]
  • 其中 os.cpus() 返回的是一个对象数组,包含所安装的每个CPU内核的信息:型号(mode)、速度(speed)、时间(times)。

  • 其中速度的单位是 MHz,时间中的 user、nice、sys、idle、irq 分别示 CPU 花费在用户模式、良好模式、系统模式、空闲模式和中断请求模式下的毫秒数。

util 模块—​提供实用工具

导入方式:const util = require('util');

  1. util.inspect(object[, options])

    该方法返回对象的字符串表示,主要用于调试。参数 objet 可以是任何 JavaScript 原始值或对象。 可选的参数 options 用于改变格式化字符串的某些选项,包括以下选项。

    • showHidden: 决定是否在格式化结果中包括不可枚举的符号与属性,默认为false。

    • depth: 指定格式化对象时递归的次数,默认为2,若要无限递归则传入null。

    • colors: 输出样式是否使用ANSI颜色代码,默认为false。

    • customInspect: 是否调用自定义的 inspect(depth, opts) 方法,默认为true。

    • showProxy: Proxy(代理)对象的对象和方法是否展示其 target(目标)和 handler(处理程序)对象,默认为false。

    • maxArrayLength: 定义要显示的数组长度,默认为100, null 表示显示全部数组元素,0或负数则不显示数组元素。

    • breakLength: 设置对象的键被拆分成多行时行的长度,默认为60, Infinity 表示格式化为单行。

      以下代码用于查看 util 对象的所有属性:

      const util = require('util');
      console.log(util.inspect(util, { showHidden: true, depth: null }));
  2. util.format(format[, …​args])

    该方法返回一个格式化后的字符串。第1个参数format是一个类似 printf 的格式字符串,包含若干占位符,每个占位符会被对应参数转换后的值所替换,format支持以下占位符。

    • %s: 字符串。

    • %d: 数值(整数或浮点数)。

    • %i: 整数。

    • %f: 浮点数。

    • %j: JSON,如果参数包含循环引用,则用字符串“[Circular]” 替换。

    • %o: 对象,用通用 JavaScript 对象格式表示对象字符串,这类似使用带有选项 {showHidden: true, depth: 4, showProxy: true } 的 util.inspect() 方法,将显示包括不可枚举的符号与属性的完整对象。

    • %O: 对象,也是用通用 JavaScript 对象格式表示对象字符串,类似使用不带选项的 utilinspect() 方法,显示的对象不包括不可枚举的符号与属性。

    • %%: 单个百分号('%'),不需要参数。

      如果占位符没有对应的参数,则占位符不被替换,例如:

      util.format('%s:%s', '蓝天白云');  // 返回:蓝天白云:%s'

      如果参数比占位符的数量多,则多出的参数会被强制转换为字符串,然后拼接到返回的字符串,参数之间用一个空格分隔。类型为 object 或 symbol (除了null )的多余参数会由 util.inspect() 转换。

      util.format ('%s:%s', "环境优美", "绿水青山", "蓝天白云"); //返回环境优美:绿水青山 蓝天白云

      如果第 1 个参数不是一个字符串,则 util.format() 返回一个将所有参数用空格分隔并连在一起的字符串。每个参数都被 util.inspect() 转换为一个字符串 。

      util.format(1, 2, 3); // 返回'1 2 3'

      如果仅有一个参数被传递给 util.format(),则不会有任何格式化输出。

      util.format("%% %s");  // 返回 "%% %s"

path 模块—​处理和转换文件路径

导入方式:const path = require('path');

  1. 不同风格的路径

    • path 模块最大的用处是解决多平台目录路径问题。

    • path模块默认会根据 Node.js 应用程序运行的操作系统的不同而变化。

    • 要想在任何操作系统上处理 Windows 文件路径时获得一致的结果,可以使用 path.win32 属性。

      • path.win32.basename("C:\\temp\\myfile.html")

    • 要想在任何操作系统上处理 POSIX 文件路径时获得一致的结果,可以使用 path.posix 属性。

      • path.posix.basename("/temp/myfile.html")

  2. 属性

    • path.delimiter:提供平台特定的 路径列表的分隔符,Windows 上是「;」,POSIX 上是「:」。

    • path.sep:提供平台特定的 路径组成部分的分隔符。Windows 上是「\」,POSIX 上是「/」。

  3. 方法

    • path.normalize(path):对路径进行规范化,并解析“..”和“.” 。通常在存储和使用文件之前,需要使用path.normalize() 方法对路径进行规范化,再进行其他处理。

    • path.dirname(path):返回路径的目录名,类似于UNIX中的dirname命令。

    • path.basename(path[, ext]):返回路径中的最后一部分,可选的ext参数表示文件扩展名。

    • path.extname(path):返回路径中文件的后缀名,即路径中最后一个“.”之后的部分。

    • path.parse(path):返回完整路径的一个对象。

    • path.format(pathObject):从一个对象表示的路径返回一个字符串表示的路径。

    • path.resolve([…​paths]):将一个路径或路径片段的序列解析为一个绝对路径。路径序列是 从右往左 被处理的,后面每个路径被依次解析,直到构造成一个绝对路径。如果全部路径都拼完了,还没有形成绝对路径,那就把当前路径加上。

      path.resolve('/foo', '/bar', 'baz');  // /bar/baz
    • path.relative(from, to):返回从参数 from 到 to 的相对路径(基于当前工作目录)。

      path.relative("/a/b/c", "/a/x/y");  // '../../x/y'
    • path.join([…​paths]):使用平台特定的分隔符将路径片段序列连接到一起,并规范生成的路径。

      path.join("/foo", "/bar", "baz/asdf", "quux", "..");  // '/foo/bar/baz/asdf' (注意 .. 是父目录)
    • path.isAbsolute(path):判定路径是否为一个绝对路径。

#clean src/ch03/modulecom/path_test.js
const path = require("path");
console.log('格式化路径: ' + path.normalize('/test/test1//2slashes///1slash/tab/..'));
console.log('连接路径: ' + path.join('/test', 'test1', '2slashes/1slash', 'tab', '..'));
console.log('获取绝对路径 : ' + path.resolve('index.js'));  // (1)
console.log('获取扩展名 : ' + path.extname('index.js'));
格式化路径: /test/test1/2slashes/1slash
连接路径: /test/test1/2slashes/1slash
获取绝对路径 : /Users/swot/learning/nodejs/index.js  // (2)
获取扩展名 : .js
  1. 如果全部路径都拼完了,还没有形成绝对路径,那就把当前路径加上。

  2. 请注意您的当前路径可能不是 /Users/swot/learning/nodejs

url 模块—​URL处理与解析

网络地址 URL 需要经常进行转换处理,它可以以字符串和对象两种形式来表示。

  • URL 字符串是具有特定结构的字符串,包含多个意义不同的组成部分。

  • URL 字符串可以被解析为一个 URL 对象,其属性对应于字符串的各组成部分。

新的应用程序应使用 WHATWG API,如下例子:

const { URL } = require('url');
const myURL1 = new URL('https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash');
解析结果 myURL1:
URL {
  href: 'https://user:pass@sub.host.com:8080/p/a/t/h?query=string#hash',
  origin: 'https://sub.host.com:8080',
  protocol: 'https:',
  username: 'user',
  password: 'pass',
  host: 'sub.host.com:8080',
  hostname: 'sub.host.com',
  port: '8080',
  pathname: '/p/a/t/h',
  search: '?query=string',
  searchParams: URLSearchParams { 'query' => 'string' },
  hash: '#hash'
}
WHATWG API 的 URL 类提供的方法和属性

URL 类根据 WHATWG URL 标准实现。

首先使用构造方法 new URL(input[, base]) 创建 URL 对象。

  • 参数 input 是要解析的 URL, base 指要解析的基本 URL (前提 input 提供的是相对URL)。

    例如以下代码可创建网络地址 htpts://example.org/foo 的 URL 对象:

    const { URL } = require('url');
    const myURL = new URL('/foo', 'https://example.org/');

创建好 URL 对象之后,就可以使用 URL 类提供的属性和方法来进行进一步操作了。

  • 主要属性和方法列举如下

  • url.protocol:获取及设置URL的协议(protocol)部分。

  • url.host:获取及设置URL的主机(host)部分,包括端口。

  • url.hostname:获取及设置URL的主机名(hostname)部分。

  • url.port:获取及设置URL的端口(port)部分。

  • url.pathname:获取及设置URL的路径(path)部分。

  • url.search:获取及设置URL的序列化查询(query)部分。

  • url.hash:获取及设置URL的hash部分。

  • url.href:获取及设置序列化的URL,返回值与url.toString和url.toJSON的返回值相同。

  • url.toString():返回序列化的URL。

  • url.toJSON():返回序列化的URL,URL对象使用JSON.stringify()序列化时将自动调用该方法。

querystring 模块 — URL 查询字符串处理和解析

提供一些实用方法用于解析与格式化 URL 查询字符串。导入该模块:

const querystring = require('querystring');
querystring.parse(str[, sep[, eq[, options]]])

将一个 URL 查询字符串解析成一个键值对的集合(相当于反序列化)。

  • 参数 str 是要解析的 URL 查询字符串

  • 参数 sep 是界定查询字符串中的键值对的子字符串,默认为 &

  • 参数 eq 是界定查询字符串中的键与值的子字符串,默认为 =

  • 参数 options 定义选项,decodeURIComponent<Function>定义解码查询字符串的字符时使用的方法,默认是 querystring.unescape()

  • 示例:解析查询字符串 querystring.parse(foo=bar&abc=xyz&abc=123) 的结果如下:

    {
      foo: 'bar',
      abc: ['xyz', '123']
    }
querystring.stringify(obj[, sep[, eq[, options]]])

将一个对象转换成 URL 查询字符串,是 querystring.parse 的逆运算。

  • 参数 obj 是要序列化成 URL 查询字符串的对象。

  • 参数 options 定义选项,encodeURIComponent<Function>定义将对象转化成字符串时使用的方法,默认是 querystring.escape()

  • 示例调用:querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });

    返回:“foo=bar&baz=qux&baz=quux&corge=”

3.3 Node.js 包的管理与使用

什么是npm

npm——Node Package Manager

npm 组成部分

  • Web网站:用来查找包、设置配置文件以及管理npm应用的其他方面,如可以为自己的公司设置公共或私有包的访问管理。

  • 命令行接口:包管理器,大多数开发人员会通过它来使用包。

  • 注册中心:提供JavaScript软件及其元数据信息的大型公共数据库,也就是官方仓库。

npm 包管理器

npm 是整个 Node.js 社区最流行、支持第三方模块最多的包管理器。

它功能非常丰富,可以用来安装、共享和发布代码以及管理项目中的依赖。

普通开发人员一般使用 npm 从 Node.js 的官方仓库下载他人提交的第三方包(其中包括模块)和命令行程序(工具)到本地使用。

当然也可以使用 npm 将自己写的包或命令行程序上传到官方仓库供他人使用。

Node.js 本身集成了 npm, npm 工具本身可以通过 npm 命令来升级版本,命令如下:

npm install npm -g

npm 提供的命令非常多,下面结合实际使用介绍部分常用的命令,更全面的命令读者可以通过道官方文档来获取。

查找和选择包

  1. 通过浏览器查找和选择包

    决定使用包之前,往往需要到官方仓库去查找并选择包。 https://www.npmjs.com/

  2. 使用 npm 命令行工具查找包

    使用 npm search 命令搜索包

    $ npm search QR code --registry=https://registry.npmjs.org

    默认情况下仅列出符合条件的前 20 名的包。

    使用 npm view 命令进一步查看包信息

    $ npm view qr-image
  3. 常用的包列表

    类别 包名 描述

    Web开发

    express

    最经典的Web开发框架

    koa

    Web开发框架后起之秀

    ejs

    页面模板。将 HTML 网页改写成 ejs 模板比较简单。

    pug(jade)

    页面模板。比 ejs 优雅简洁,但把 HTML 转换成 pug(jade) 模板比较难一些。

    mongoose

    MongoDB 数据库驱动

    mysql

    MySQL 数据库驱动

    log4js

    日志

    cheerio

    解析 HTML 文档

    docpad

    静态站点生成系统,一般用于开发博客程序。

    基础拓展

    underscore

    javascript帮助库

    moment

    时间操作

    代码组织

    async

    控制异步流程最基本的解决方案

    promise

    控制异步流程最新的解决方案

    测试

    mocha

    测试框架,断言库可自由选择。

    chai.js

    BDD/TDD断言库

    should.js

    断言库,可读性强。

    expect.js

    断言库,在should.js基础上构建。

    zombie.js

    构造浏览器进行测试

    uiTest

    前端UI测试框架

    项目管理

    grunt

    JavaScript 任务管理器

    bower

    包管理工具

    yo

    项目的框架生成工具

    yeoman

    开发Web工作流的管理工具

    预编译

    coffeescript

    将 CoffeeScript 编译成 JavaScript

    less

    将 less 编译成 CSS

    sass

    将 sass编译成 CSS

使用 npm 命令安装包

类型 命令 描述

安装本地包

  • npm install 包名

  • npm install lodash

  • npm install lodash@4.2.0

  • npm root 查看本地安装包路径

  • npm list 查看当前目录下已安装的包

  • 应在项目根目录下执行该命令

  • 本地安装会将包存放在 ./node_ modules 目录

  • 若无 node_modules 目录,则会在项目目录下自动生成

  • package.json 未指定则安装最新版本

安装全局包

  • npm install -g 包名

  • npm install -g grunt

  • npm root -g 查看远程安装包路径

  • npm list -g 查看所有全局已安装的包

可在命令行直接运行全局包支持的命令

npm config set prefix "d:\xxx"

更改全局安装路径

npx

无须全局安装即可运行全局命令

  • 会临时安装可执行依赖包

  • 可解决不同项目使用不同版本的命令问题

使用语义版本控制管理代码(包)

  • 版本号的形式为 X.Y.Z,X、Y 和 Z 分别代表主版本号、次版本(副版本)号和补丁版本号。

  • X、Y 和 Z 必须为非负整数,禁止在数字前补零,每个数值都是递增的。

代码发布的语义版本号方案
代码状态 阶段 规则 示例

首次发布

新产品

从1.0.0开始

1.0.0

向后兼容的bug修复

补丁发布

更新Z位

1.0.1

向后兼容的新功能

次版本发布

更新Y位并将Z位置0

1.10

变动较大,向后不兼容

主版本发布

更新X位并将Y和Z位置0

2.0.0

在 package.json 文件中指定应用程序可接受的更新版本范围。

假使要以一个包的 1.0.4 版本作为起始版本,可以按照以下方式指定可接受的版本范围。

补丁发布

1.0、1.0.x 或 ~1.0.4

波浪号 ~ 表示前两位不变,最后一位取最新的版本值

次版本发布

1、1.x 或 ^1.0.4

脱字符 ^ 表示第一位版本号不变,后面两位取最新的版本号

主版本发布

* 或 x

星号 * 表示全部取最新的版本值

使用 package.json 文件管理本地安装包

管理本地安装的 npm 包的最佳方式是创建一个 package.json 文件。package.json 文件的功能如下:

  • 列出当前项目所依赖的包。

  • 通过语义化版本控制规则指定当前项目所使用的包的版本。

  • 实现可重用的构建,更易于与其他开发人员分享包。

1) package.json 文件的基本组成
  • name:项目名称。必须使用小写单词,不能用空格,可以使用连字符「-」或下划线 「_」

  • version:采用 x.x.x 的形式,符合语义化版本控制规则。

2) 创建 package.json 文件

运行带选项 --yes 或 -y 的 npm init 命令将自动在当前目录下创建一个默认的 package.json。

$ npm init --yes
{
  "name": "mynodeprj",      // 项目版本(必需)
  "version": "1.0.0",       // 项目版本(必需)
  "description": "",        // 项目描述信息
  "main": "index.js",       // 程序的主入口文件
  "dependencies": {         // 项目的依赖包
    "lodash": "^4.17.11"
  },
  "devDependencies": {},    // 项目开发阶段的依赖包
  "scripts": {              // 项目执行的脚本
    "test": "echo \\"Error: no test specified\\" && exit 1"
  },
  "keywords": [],           // 项目的关键词,用于搜索
  "author": "",             // 项目作者
  "license": "ISC"          // 项目许可协议
}
3) 指定依赖包
向 package.json 文件中添加 “dependencies” 依赖(应用程序在生产环境中所需的)

npm install <包名> [--save-prod]/[--save]

向 package.json 文件中添加 “devDependencies” 依赖(开发和测试所需的)

npm install <包名> --save-dev

Tip
  • npm 使用语义版本控制来管理包的版本或版本范围。

  • 如果在项目目录中有一个 package.json 文件,则直接运行 npm install 命令时,npm 会通过语义版本控制查找该文件列出的依赖包并下载最新的版本。

  • 在声明第三方包依赖时,除了可依赖一个固定版本号外,还可依赖某个范围的版本号。

4) 手动编辑 package.json 文件
{
  "name": "my_package",
  "version": "1.0.0",
  "dependencies": {
    "my_dep": "^1.0.0"  // (1)
  },
  "devDependencies" : {
    "my_test_framework": "^3.1.0"  // (2)
  }
}
  1. 项目在生产环境使用的依赖包 my_dep 可以是主版本为 1 的任何最新版本

  2. 仅用于开发环境的依赖包 my_test_framework 可以是主版本为 3 的任何最新版本

包的其他操作

分类 命令 描述

包的升级

npm update <包名>

将本地安装的某个包更新至最新版本。

npm update -g <包名>

将全局安装的某个包更新至最新版本

npm update -g

将全局安装的所有包更新至最新版本

包的卸载

npm uninstall <包名>

载本地安装的某个包(同时卸载生产环境指定的依赖包)

npm uninstall --save-dev <包名>

载本地安装的某个包(同时卸载为开发环境指定的依赖包)

npm uninstall -g <包名>

卸载全局安装的某个包

清空 npm 本地缓存

npm cache clean

发布自己的包

npm publish

需要在 npm 注册服务器上注册用户

使用淘宝 npm 镜像

淘宝团队提供了完整的 npmjs.org 镜像,其版本同步频率为10分钟一次。

淘宝专门定制了 cnpm 命令行工具以代替 npm,可以执行以下命令进行 cnpm 工具安装。

npm install -g cnpm --registry=https://registry.npm.taobao.org

安装完成后就可以使用 cnpm 来代替 npm 安装和管理 npm 包。

cnpm 的使用方法与 npm 相同,只需将 npm 改成 cnpm。

3.4 实战演练 — 抓取网页中的图片

学习以上内容之后,这里示范构建一个简单的项目,利用第三方包来抓取网页中的图片。 为简化演示过程,没有使用数据库来存储抓取结果,只是将抓取的图片保存在本地目录中,并在控制台中输出所抓取的图片 URL 地址。

技术准备

1) 使用 request 包发起 HTTP 请求
使用 request 模块的示例代码:
var request = require('request');   // 导入request模块
request('https://www.baidu.com', function(error, response, body) {
  console.log('error:', error);     // 如果出错输出错误信息
  console.log('statusCode:', response && response.statusCode);  // 收到响应输出状态码
  console.log('body:', body);       // 输出百度首页的HTML内容
});

request 方法的参数:

  • 第 1 个参数可以是一个 URI, 也可以是一个表示 HTTP 请求头的对象(包括 url、method、headers 或 body 元素)。

  • 第 2 个参数是一个回调函数, 用于对返回的请求结果进行处理。

request 的一个重要特色就是对流的支持。抓取网页时往往需要保存图片、js、css 等文件,request 能够很好地支持文件流。例如,以下语句抓取百度 logo 图片并将图片保存到本地:

request("https://www.baidu.com/img/bd_logo1.png").pipe(fs.createWriteStream("logo1.png"))

request 提供非常好用的错误处理机制。例如:

request
  .get("http://mysite.com/doodle.png")
  .on("error", function (err) {
    console.log(err);
  })
  .pipe(fs.createWriteStream("doodle.png"));
2) 使用 cheerio 包解析获取的网页内容

获取网页之后,需要对其进行解析。jQuery 库是前端操作网页 dom 节点的主流工具,cheerio 是专为服务端设计的快速、灵活和精简实现的核心jQuery。cheerio是一个 npm 包,相当于一个 Node.js 版本的 jQuery,是爬虫脚本用得最多的代码包,主要用来提取所抓取的网页节点内容。它工作在一个非常简单、一致的 DOM 模型之上,几乎能够解析任何的 HTML 和 XML 文档。

使用该包提供的 cheenio 模块的例子如下:
const cheerio = require('cheerio')
const $ = cheerio.load('<h2 class="title">Hello world</h2>')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
//这将输出:<html><head></head><body><h2 class="title welcome">Hello there!</h2></body></html>
  1. 首先,需要使用 load() 方法加载 HTML 文档内容,参数可以是一个完整的 HTML 文件,也可以是一个 HTML 片段。

  2. 然后操作 HTML 文档中的元素。cheerio 的选择器用法几乎和 jQuery 一样:

    $(selector,[context],[root])

    选择器在 context 范围内搜索,context 又在 root 范围内搜索。selector 和 context 可以是一个字符串表达式、DOM元素、DOM元素的数组,或者 cheerio 对象。root 通常是HTML文档字符串。

类似于 jQuery 语法

cheerio 选择器方法是遍历和操作文档的起点。与 jQuery 相似,这是在文档中选择元素的主要方法; 与jQuery不同的是,它建立在 CSSSelect 库之上,该库实现了 Sizzle 选择器的大部分功能。

$('.apple', '#fruits').text()   // => Apple
$('ul .pear').attr('class')     // => pear
$('li[class=orange]').html()    //=> Orange

获取和修改属性的方法如下:

.attr(name, value)

在匹配的元素当中,这个方法只能获取第1个匹配元素的属性。 如果设置属性的值为null,就相当于删除了这个属性; 也可以传递一个键值对或一个方法给 attr() 方法。

实现思路

  1. 通过request请求网页地址并获取返回结果,得到整个网页代码。

  2. 在request的回调函数中调用cheerio对返回的网页文档内容基于DOM结构进行解析,提取其中图片文件的URL地址。

  3. 利用request模块再次请求图片文件的URL地址,将获取的结果以流的方式保存到本地。

  4. 为更好地组织代码,将使用cheerio模块解析HTML文档的功能封装为一个文件模块,在项目的主入口文件index.js中加载该文件模块。

创建项目目录和 package.json 文件

  1. mkdir crawling

  2. cd crawling

  3. mkdir downimages

  4. npm init --yes

  5. vim package.json

#clean src/ch03/crawlimg/package.json

{ "name": "crawlimg-project", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \\"Error: no test specified\\" && exit 1" }, "author": "", "license": "ISC" }

安装 request 和 cheerio 包

执行以下命令安装 request 和 cheerio 包(这里利用 npm 淘宝镜像):

cnpm install request cheerio --save

该命令会新建一个 node._modules 目录以保存以上两个包。选项 --save 将把安装包的信息添加到 pakage.json 文件的“dependencies" 中:

"dependencies": {
  "cheerio": "^1.0.0-rc.12",
  "request": "^2.88.2"
}

默认安装包的最新版本。

如果没有使用 --save 选项,package.json 将不会发生任何变化。这里之所以将这些依赖关系保存到 package.json, 是为了方便其他开发人员在得到该项目后直接使用 npm install 命令就能完成所有依赖项的自动安装。另外还有一个原因,Node.js 项目在进行代码管理时通常都会忽略 node_modues 目录而只保留 package.json 文件。

#clean src/ch03/crawlimg/package.json

{ "name": "crawlimg-project", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \\"Error: no test specified\\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "cheerio": "^1.0.0-rc.12", "request": "^2.88.2" } }

定义一个模块用于解析网页文档

在项目目录中新建一个 parse.js 文件,加入以下代码。

#clean src/ch03/crawlimg/parse.js

这个模块对外暴露 getlmg() 方法, 该方法用于获取网页中每一幅图片的 URL。

const cheerio = require("cheerio"); //载入第三方cheerio模块

exports.getImg = function (htmldom, callback) {
  // exports对象上设定对外暴露的方法
  let $ = cheerio.load(htmldom); //装载网页
  $("img[class=lazy-load]").each(function (i, elem) {
    if (i === 10) {   // 下载 10 张图片就退出
      return false;
    }
    //遍历其中的img标签
    let imgSrc = $(this).attr("data-original"); //获取每一个图片的URL
    callback(imgSrc, i); //通过回调函数处理图片URL
  });
};

编写主入口文件 index.js

#clean src/ch03/crawlimg/index.js
const request = require("request");                 //导入第三方request模块
const path = require("path");                       //导入path核心模块用于处理目录路径
const fs = require("fs");                           //导入fs核心模块用于操作文件
const parse = require("./parse");                   //导入parse文件模块用于解析网页图片地址
const workUrl = "http://www.qingdaonews.com";       //指定要抓取图片的网页地址,可以改变
const imgDir = path.join(__dirname, "downimages");  //获取图片保存路径

function downImg(imgUrl, i) {                       //定义下载图片的函数
  if (imgUrl) {
    console.log(imgUrl);                            //控制台输出图片URL
    //获取图片文件的扩展名,利用split()函数将图片URL分割成一个字符串数组并取最后一个元素
    let ext = imgUrl.split(".").pop();
    //利用request请求网页图片并将图片保存到指定的图片目录,使用一个从读取流到写出流的管道(pipe)
    request
      .get(imgUrl)
      .pipe(
        fs.createWriteStream(path.join(imgDir, i + "." + ext), {
          encoding: "utf8",
        })
      );
  }
}

//下面是主程序,用于请求网页并对结果进行解析
request(workUrl, function (err, res, body) {
  if (!err && res) {
    console.log("start");
    parse.getImg(body, downImg); //调用 parse.getImg 解析网页图片; downImg 作为回调函数
    console.log("done");
  } else {
    console.log("error");
  }
});

运行程序

  1. cd crawling

  2. node index.js

打印输出图片URL
start
http://news.qingdaonews.com/images/2023-02/07/d5a2275a-a639-40f7-913f-aacef968b4fd.png
...
http://www.qingdaonews.com/images/2022-12/12/e43e0ca4-f43a-40a9-a75f-5dbcaac49e12.jpg
http://www.qingdaonews.com/images/2023-02/02/ed951d0a-1ecb-47fc-be29-d0b192c6982e.jpg
done
生成了图片文件
> ls -lh downimages
total 5416
-rw-r--r--  1 swot  staff   145K Feb  7 22:51 0.png
...
-rw-r--r--  1 swot  staff    44K Feb  7 22:51 8.jpg
-rw-r--r--  1 swot  staff    28K Feb  7 22:51 9.jpg

3.5 本章小结

3.6 习题

4. 文件系统操作

4.1 Node.js 的文件系统基础

fs 模块简介

  • fs 模块提供一组文件操作 API 用于模仿标准 POSIX 函数与文件系统进行交互。

  • 导入该模块:const fs = require('fs');

  • fs 模块共有 5 个类,分别是

    1. fs.Dirent(指示文件类型)

    2. fs.FSWatcher(监视文件)

    3. fs.ReadStream(读取流)

    4. fs.WriteStream(写入流)

    5. fs.Stats(文件的信息)

  • fs 模块支持 FS 常量,包括文件可访问性、文件复制、文件打开、文件类型和文件模式的常量。

    • 常量可以从 fs.contents 获取

Tip
名称具有 Sync 后缀的方法为同步方法,不具有该后缀的方法为异步方法。

同步文件操作与异步文件操作

fs 模块包含同步文件操作和异步文件操作两大类 API 方法。

同步和异步

异步操作方法或函数的最后一个参数总是一个 回调函数。第 1 个参数总是错误信息,如果操作成功完成,则这个参数值将为 null 或 undefined。

异步操作示例:
fs.unlink('/tmp/hello.txt', function(err){  // 异步删除文件
  if(err) throw err;  // 如果发生错误则抛出异常
  console.log('已成功删除 /tmp/hello.txt!');
});

而使用同步操作方法执行时,一有异常会立即引发,可以使用 try/catch 语句块来处理,

同步操作示例
try {
  fs.unlinkSync("/tmp/hello.txt");  // 同步删除文件
  console.log('已成功删除 /tmp/hello.txt!');
} catch (err) {
  // 处理错误
}
Note
实际上 Node.js 内部的模块系统以及 require() 方法都会用到文件同步操作。
同层次多个异步

多个异步函数在同一层次执行,是无法保证执行顺序的,因此以下操作容易出错,因为 fs.stat() 操作可能在fs.rename() 操作之前已经完成(原操作意图为先修改文件名再查看文件属性):

无法保证执行顺序示例:
fs.rename('/tmp/hello.txt', '/tmp/world.txt', (err) => {
  if (err) throw err;
  console.log('重命名完成!');
});
fs.stat('/tmp/world.txt', (err, stats) => {
  if (err) throw err;
  console.log(`文件属性: ${JSON.stringify(stats)}`);
});

要正确地排序这些操作,最好 将一个函数放在另一个函数的回调函数中执行。这里应将 fs.stat() 调用移到 fs.rename() 操作的回调中:

函数嵌套保证执行顺序
fs.rename('/tmp/hello.txt', '/tmp/world.txt', (err) => {
  if (err) throw err;
  fs.stat('/tmp/world.txt', (err, stats) => {
    if (err) throw err;
    console.log(`文件属性: ${JSON.stringify(stats)}`);
  });
});
同步和异步比较
  • 异步操作方法支持同时处理多个任务,阻塞少、性能高、速度快。

  • 同步操作将阻塞整个进程,直到所有任务完成。

Tip

Node.js 的一大优势就是支持异步调用,不管是在读取数据库,还是在读取文件时,都可以使用异步的方式处理任务,这样就可以处理高并发的情况。

一般情况下,建议使用异步操作方式,而不要使用同步方法。

#clean src/ch04/fstest/sync-async.js
文件同步与异步操作比较的示例
const fs = require("fs");
// 异步读取
fs.readFile("demo.txt", function (err, data) {
  if (err) {
    return console.error(err);
  }
  console.log("异步读取: " + data.toString());
});
// 同步读取
var data = fs.readFileSync("demo.txt");
console.log("同步读取: " + data.toString());
console.log("程序执行完毕。");
node sync-async.js
同步读取: 这是一个用于测试读取文件内容的文本文件。  // (1)
程序执行完毕。
异步读取: 这是一个用于测试读取文件内容的文本文件。  // (2)
  1. 先执行了同步代码

  2. 后执行了异步代码。

#clean src/ch04/fstest/demo.txt

这是一个用于测试读取文件内容的文本文件。

文件路径 path

操作的文件对象通过文件路径来引用。在 fs 模块的方法中一般用 path 参数表示文件路径。

Node.js 的文件路径表示方法:

  • 字符串表示的文件路径

    用字符串(UTF-8字符序列)形式表示的路径可分为相对路径和绝对路径两种类型;

    • 绝对路径

      • 在POSIX系统上使用的绝对路径是从根目录(/)开始的;例如 /open/some/file.txt

      • 在Windows上Node.js要遵循单驱动器工作目录的规则,绝对路径是从驱动器符号开始,而且路径分割使用反斜杠;例如 C:\\temp\\myfile.html

    • 相对路径

      相对路径相对于当前工作目录进行解析。使用 process.cwd() 可获取当前工作自录。例如 file.txt 就是一个相对路径。

  • buffer 对象表示的文件路径(了解)

  • URL 对象表示的文件路径(了解)


使用 path 模块处理不同风格的路径

参内容 path方法

文件模式 mode

这里的文件模式指的就是文件权限,fs 模块对文件进行操作会涉及操作权限的问题,在相关的方法中一般用 mode 参数表示文件模式。fs 模块遵循的是POSIX文件操作规范,使用类Unix系统的权限表示方法。类Unix系统的文件(包括目录,目录也是一种特殊文件)权限是指对文件的访问控制,决定哪些用户和哪些组对某文件具有哪种访问权限。

Node.js 将文件访问者身份分为3个类别:所有者(Owner )、所属组( Group )和其他用户( Others )。对于每个文件,又可以为这 3 类用户指定 3 种访问权限:读(Read)、写(Write )和执行( Execute )。这样就形成了下表所示的 9 种具体的访问权限。

权限分配 所有者权限 所属组权限 其他用户权限

权限项

执行

执行

执行

字符表示

r

w

x

r

w

x

r

w

x

数字表示

4

2

1

4

2

1

4

2

1

在 linux 中使用 ls 命令即可显示文件详细信息

> ls -l
total 1728
-rw-r--r--   1 swot  staff    1082 Jan  6 16:08 LICENSE
-rw-r--r--   1 swot  staff    2436 Jan 30 23:41 README.adoc
-rw-r--r--   1 swot  staff   39149 Jan 30 23:36 README.html
drwxr-xr-x  10 swot  staff     320 Jan 30 23:36 css
drwxr-xr-x  10 swot  staff     320 Jan  9 00:18 css3
drwxr-xr-x   8 swot  staff     256 Jan 30 23:36 es6
drwxr-xr-x   9 swot  staff     288 Jan  3 21:33 html
drwxr-xr-x  10 swot  staff     320 Jan  2 19:31 html5
drwxr-xr-x   9 swot  staff     288 Feb  2 18:03 jQuery
drwxr-xr-x   9 swot  staff     288 Jan 25 00:05 javascript
-rw-r--r--   1 swot  staff  831505 Feb  9 11:40 learning.leo
drwxr-xr-x  13 swot  staff     416 Jan 31 19:17 nodejs
  • 第1项表示文件类型与权限,共10个字符,第1个字符表示文件类型,其中d表示目录,、-表示文件,l表示链接文件,b表示块设备文件,c表示字符设备文件。

  • 接下来的字符以3个为一组, 分别表示文件所有者、所属组和其他用户的权限,每一类用户的3种文件权限依次用 r、w 和 x 分别表示读、写和执行,这3种权限的位置不会改变,如果某种权限没有,则在相应权限位置用「-」表示。

  • 将权限读r、写w 和执行x 分别用数字 4、2 和 1 表示,没有任何权限则表示为0。每一类用户的权限用其各项权限的和表示(结果为0~7之间的数字),依次为所有者、所属组和其他权限。这样以上9种权限就可用3个数字来统一表示。例如, 754表示所有者、所属组和其他用户的权限依次为: [4+2+1]、[4+0+1]、[4+0+0],转化为字符表示就是 rwxr-xr--。


Windows 系统的文件权限默认是可读、可写、不可执行,所以权限位数字表示为 0o666 (八进制表示 进制表示), 转换十进制表示为 438。

文件系统标志 flag

Node.js 的文件系统操作方法会使用 flag 参数表示文件系统标志,此标志用于设置文件打开的行为,如可读可写、既可读又可写等。这里将所有的文件系统标志(用字符串表示)在下表中。

标志 说明

a

以追加模式打开文件。如果文件不存在则创建

ax

类似a标志,但是如果文件路径存在,则文件追加失败

a+

以读取追加模式打开文件,如果文件不存在则创建

ax+

类似a+标志,但是如果文件路径存在,则文件读取追加失败

as

以同步方式打开文件用于追加。如果文件不存在,则创建该文件

as+

以同步方式打开文件用于读取和追加。如果文件不存在,则创建该文件

r

以读取模式打开文件。如果文件不存在则抛出异常

r+

以读取和写入模式打开文件。如果文件不存在则抛出异常

rs

以同步方式读取文件。如果文件不存在则抛出异常

rs+

以同步方式读取和写入文件。它会指示操作系统绕过本地的文件系统缓存,除非需要,否则不建议使用此标志

w

以写入模式打开文件,如果文件不存在则创建

wx

类似w标志,但是如果文件路径存在,则文件写入失败

w+

以读写模式打开文件,如果文件不存在则创建

wx+

类似w标志,但是如果文件路径存在,则文件读写失败

文件描述符 fd

在 POSIX系统上,对于每个进程,内核都维护着一个当前打开着的文件和资源的表。操作系统会为每个打开的文件分配一个名为文件描述符的数字,文件操作使用这些文件描述符来识别与追踪每个特定的文件。

windows 系统使用了一个不同但概念上类似的机制来追踪资源。为方便用户使用,Node.js 抽象了不同操作系统之间的差异,为所有打开的文件分配了数字形式的文件描述符。相关的方法中一般用 fd 参数表示文件描述符。

在 Node.js 中每操作一个文件,文件描述符都是会自动递增的,文件描述符一般从3开始,因为前面的 0、1、2是比较特殊的描述符,分别表示process.stdin (标准输入)、process.stdout(标准输出)和 proccess.stderr(错误输出)。

fs.open()方法用于分配新的文件描述符。一旦被分配,文件描述符可用于从文件读取数据、向文件写入数据或请求关于文件的信息。

大多数操作系统会限制在任何给定时间内可能打开的文件描述符的数量,因此操作完成时关闭描述符至关重要。如果不这样做将导致内存泄露,最终可能导致应用程序崩溃。

4.2 文件与目录基本操作

打开文件

要读取文件内容,获取文件信息,首先需要打开文件。打开文件有同步和异步两种方式,一般选择异步方式。

异步打开文件的用法

fs.open(path, flags[, mode], callback)

  • callback 指回调函数,带有两个参数 err 和 fd,用于返回错误信息和表示文件描述符的整数。

同步打开文件的用法

fs.openSync(path, flags[, mode])

  • 该方法比上述 fs.open() 少了一个回调函数参数 callback。

以异步方式打开文件的示例如下:

#clean src/ch04/fstest/file_open.js
// 异步打开文件示例
const fs = require("fs");
console.log("准备打开文件");
//r+表示以读写模式打开,fd为返回的文件描述符
fs.open("demo.txt", "r+", function (err, fd) {
  if (err) {
    return console.error(err);
  }
  console.log("文件打开成功!");
});

获取文件信息

通过 Node.js 的 API 可以获取文件的特征信息,如文件大小、创建时间、权限等。

异步方式获取文件信息的语法格式如下

fs.stat (path[, options], callback)

参数说明如下:

  • path:文件路径。

  • options:选项。使用 bigint 作为选项来指示返回的 fs.Stats 对象中的数值是否应为 bigint 类型,默认值为 false。

  • callback 回调函数,带有两个参数 err 和 stats, 分别表示错误信息和 fs.Stats 对象。

    执行该方法后,fs.Stats 类的实例(对象)返回给其回调函数。可以通过下表中 fs.Stats 类提供的方法来判断文件的相关属性。

方法 说明

stats.isBlockDevice()

如果是块设备则返回true,否则返回false

stats.isCharacterDevice()

如果是字符设备则返回true,否则返回false

stats.isDirectory()

如果是目录则返回true,否则返回false

stats.isFIFO()

如果是FIFO则返回true,否则返回false。FIFO是UNIX中的一种特殊类型的命令管道

stats.isFile()

如果是文件则返回true,否则返回false

stats.isSocket()

如果是套接字则返回true,否则返回false

fs.Stats 类还有许多属性,如

  • stats.size表示文件的大小(以字节为单位)

  • stats.birthtime 表示文件创建时间的时间戳。

这些属性可以从返回的 fs.Stats 对象中获取。下面给出一个异步读取文件信息的实例。

Tip

不建议在调用 fs.open()、fs.readFile() 或 fs.writeFile() 等方法之前使用 fs.stat() 方法检查文件是否存在,这样做会引入竞态条件,因为其他进程可能会在两个调用之间更改文件的状态。应该直接打开、读取或写入文件,如果文件不可用则处理引发的错误。

如果仅检查文件是否存在但随后并不对其进行操作,则建议使用 fs.access() 方法。

同步获取文件信息的方法的语法格式如下,比上述 fs.stat() 方法少了一个回调函数参数 callback

fs.statSync(path[, options])
#clean src/ch04/fstest/file_stat.js
//获取文件信息
const fs = require("fs");
fs.stat("demo.txt", function (err, stats) {
  if (err) throw err;
  console.log(stats); //显示返回的fs.Stats对象
  console.log("读取文件信息成功!");
  // 检测文件类型
  console.log("是否为文件(isFile) ? " + (stats.isFile() ? "" : ""));
  console.log(
    "是否为目录(isDirectory) ? " + (stats.isDirectory() ? "" : "")
  );
  // 读取文件属性
  console.log("文件大小:" + stats.size);
  console.log("创建时间:" + stats.birthtime);
});
node file_stat.js
Stats {
  dev: 16777220,
  mode: 33188,
  nlink: 1,
  uid: 501,
  gid: 20,
  rdev: 0,
  blksize: 4096,
  ino: 99630799,
  size: 63,
  blocks: 8,
  atimeMs: 1675868937990.5798,
  mtimeMs: 1561781559000,
  ctimeMs: 1675867560479.659,
  birthtimeMs: 1561781559000,
  atime: 2023-02-08T15:08:57.991Z,
  mtime: 2019-06-29T04:12:39.000Z,
  ctime: 2023-02-08T14:46:00.480Z,
  birthtime: 2019-06-29T04:12:39.000Z
}
读取文件信息成功!
是否为文件(isFile) ? 是
是否为目录(isDirectory) ? 否
文件大小:63
创建时间:Sat Jun 29 2019 12:12:39 GMT+0800 (China Standard Time)

读取文件

fs 模块提供两种读取文件的方法,一种是读取指定的部分数据,另一种是一次性读取整个文件。

读取指定的数据 fs.read
异步方式下读取文件指定数据的语法格式如下

fs.read(fd, buffer, offset, length, position, callback)

  • fd:类型为integer, 通过fs.open()方法返回的文件描述符,用于指定读取的文件。

  • buffer: 数据写入的缓冲区,类型为Buffer, 也可以是TypedArray或DataView。

  • offset: 缓冲区写入的写入偏移量,类型为integer。

  • length: 要从文件中读取的字节数, 类型为integer。

  • position:文件读取的起始位置,类型为integer。如果 position 的值为 null, 则会从当前文件指针的位置读取,并更新文件位置。如果 position 值是整数,则文件位置将保持不变。

  • callback: 回调函数,有 3 个参数 err、 bytesRead 和 buffer, 分别表示错误信息、读取的字节数和Buffer 对象。

读取文件时需先打开文件,再从 fs.open() 返回的文件描述符指向的文件中读取数据。下面是一个简单的示例。

#clean src/ch04/fstest/file_readdata.js
从文件中读取部分数据
//异步读取文件
const fs = require("fs");

fs.open("demo.txt", "r", function (err, fd) {
  if (err) throw err;
  console.log("打开文件成功。");
  var buf = Buffer.alloc(24); //分配缓冲区
  //开始读取字节
  fs.read(fd, buf, 0, buf.length, 0, function (err, bytes) {
    if (err) {
      fs.closeSync(fd); //关闭文件
      return console.log(err);
    }
    console.log("读取的字节长度:" + bytes);

    // 仅输出读取的字节
    if (bytes > 0) {
      console.log("打开文件后读取的buff内容:" + buf.slice(0, bytes).toString());
    }
    fs.closeSync(fd); //关闭文件
  });
});
node file_readdata.js
打开文件成功。
读取的字节长度:24
打开文件后读取的buff内容:这是一个用于测
读取文件的全部内容 fs.readfile
异步方式下读取文件全部内容的语法格式如下

fs.readFile(path[, options], callback)

  • 参数 path 可以是文件路径,也可以是文件描述符。

  • 参数 options 是可选的,可用于指定编码格式(默认值null)或文件系统标志(默认值r)。

  • 回调函数 callback 有两个参数 err 和 data,分别表示错误信息和返回的文件内容(可以是字符串形式或 Bufer 对象)。

用这种方法读取文件时无须打开文件,下面是简单的示例。

Note
fs.readFile() 方法会缓冲整个文件。为减少内存占用,应尽可能通过 fs.createReadStream() 方法进行流传输。
#clean src/ch04/fstest/file_readall.js
//异步读取文件全部内容
const fs = require("fs");

fs.readFile('demo.txt', (err, data) => {
  if (err) throw err;
  console.log(data);
});

以上代码由于没有明确指定编码格式,所以执行时会返回原始的 Bufer 对象:

node file_readall.js
<Buffer ef bb bf e8 bf 99 e6 98 af e4 b8 80 e4 b8 aa e7 94 a8 e4 ba 8e e6 b5 8b e8 af 95 e8 af bb e5 8f 96 e6 96 87 e4 bb b6 e5 86 85 e5 ae b9 e7 9a 84 e6 96 ... 13 more bytes>

可通过参数 options 指定字符编码格式,这就会返回字符串。

fs.readFile('demo.txt', 'utf8', callback);

写入文件

覆盖式写入
异步方式下覆盖式写入文件的语法格式

fs.writeFile (file, datal, options], callback)

该方法异步地将数据写入到一个文件,如果文件不存在则将自动创建,如果已存在则覆盖该文件,写入的内容可以是字符串或Buffer对象。其参数说明如下。

  • file: 文件名或文件描述符。

  • dala: 要写入文件的数据,可以是字符串或Bufer对象。

  • options: 这是个可选的参数,可以是一个对象,包含 {encoding, mode, flag }, 其中

    • encoding 表示编码格式 (默认为 utf8)

    • mode 表示模式(默认为 0666)

    • flag 为文件系统标志(默认为 'w'),可以直接用字符串来指定编码格式。

  • callback 回调函数,只包含错误信息参数 err,在写入失败时返回。

如果参数 data 是一个 Buffer 对象,则 encoding 选项会被忽略。

参数 options 可以采用对象形式,这里给出一个例子:

var op = {
  encoding: 'utf8',
  mode: 0o666,
  flag: 'w'
};

在同一个文件上多次使用 fs.writeFile() 方法且不等待回调是不安全的。对于这种情况,建议改用 fs.createWriteStream() 方法。

写入文件时也不用先打开文件,这里给出覆盖式写入文件的示例,代码如下。

#clean src/ch04/fstest/file_write.js
//异步写入文件
const fs = require("fs");
console.log("准备写入文件");
fs.writeFile("input.txt", "这是用于写入文件测试的内容", function (err) {
  if (err) throw err;
  console.log("数据写入成功!");
});
vim input.txt
这是用于写入文件测试的内容
追加式写入
异步方式下追加式写入文件的语法格式

fs.appendFile (path, data, options], callback)

该方法异步地将数据追加到文件,如果文件不存在则创建该文件,如果文件已存在,追加内容的位置取决于文件系统标志。 与 fs.writeFile() 不同, fs.appendFile() 方法中参数 options 的 {encoding, mode, flag} 对象中的 flag(文件系统标志)默认值为"a", 这表示将内容追到文件尾部;如果改为’w',则将用追加的内容覆盖整个文件。

参数 path 除了使用文件路径外,还可以指定为已打开文件的文件描述符,可使用 fs.open() 或 fs.openSync()方 法来打开文件或返回文件描述符,注意文件描述符不会自动关闭。

这里给出一个示例,代码如下:

#clean src/ch04/fstest/file_append.js
//异步追加文件
const fs = require("fs");
console.log("准备追加文件!");
fs.appendFile("input.txt", "这是用于追加文件测试的内容", (err) => {
  if (err) throw err;
  console.log("数据追加成功!");
});
vim input.txt
这是用于写入文件测试的内容这是用于追加文件测试的内容
将数据写入文件指定的位置

fs 模块还支持将数据写入文件指定位置,它提供的异步方法有两个。

  1. 写入 Buffer 对象的方法,语法格式如下:: fs.write (fd, buffer[, offset[, length[, position]1], callback)

    • 参数 buffer 指定写入到文件的 Buffer 对象;

    • offset 决定Buffer对象中要被写入的部位;

    • length 是一个整数,指定要写入的字节数;

    • position 指定文件写入位置;

    • 回调包含 3 个参数 err、bytesWritten 和 buffer, 其中 bytesWriten 返回 Buffer 对象中被写入的字节数。

  2. 写入字符串的方法,其语法格式如下:: fs.write(fd, string[, position[, encoding]], callback)

    • 参数 string 指定写入到文件的字符串,如果不是一个字符串;则该值会被强制转换为字符串;

    • position 指定文件写入位置;

    • encoding 表示期望的字符串编码;

    • 回调会接收到参数 err、written 和 string,其中 writen 指定传入的字符串中被要求写入的字节数,被写入的字节数不一定与被写入的字符串字符数相同。

这两个方法执行之前都需要打开文件,通过打开文件返回的文件描述符(fd参数)来指定要写入的文件。

Note
在 Linux 上当以追加模式打开文件时,无法指定写入位置。

文件的其他基本操作

关闭文件

文件描述符不会自动关闭,必须使用 fs.close() 方法关闭。异步方式下关闭文件的语法格式

fs.close(fd, callback)

参数 fd 是指通过 fs.open() 方法返回的文件描述符。 回调函数cllback 没有参数,除了一个用于处理异常的参数 err, 完成回调没有其他参数。

检查文件的可访问性

单纯地检查文件是否存在,可用 fs.accsss() 方法,其语法格式如下:

fs.access (path[, mode], callback)
  • 该方法用于检测由 path 参数指定的文件或目录的权限。

  • mode 是个可选的整数参数,指定要执行的可访问性检查,由常量表示。相关的常量列举如下。

    • F_OK:表明文件对调用进程可见,用于判断文件是否存在。

    • R_OK:表明调用进程可以读取文件,用于判断文件是否可读。

    • W_OK:表明调用进程可以写入文件,用于判断文件是否可写。

    • X_OK:表明调用进程可以执行文件,用于判断文件是否可执行。这在Windows上无效,等同于 F_OK。

    • mode 参数的默认值是F_OK。该参数可以组合使用多个常量,如 fs.constants.W_OK | fs.constants.R_OK。

  • 回调函数 callback 只有一个参数 err, 如果可访问性检查失败,则 err 参数将是 Error 对象。

例如,以下代码检查当前目录中是否存在指定的文件:

const file = "text.txt";
fs.access(file, fs.constants.F_OK, (err) =>{
  console.log(`${file} ${err ? '不存在' : '存在'}`);
});

例如,检查当前目录中是否存在该文件,以及该文件是否可写的示例代码:

fs.access(file, fs.constants.F_OK | fs.constants.w_OK, callback);
文件重命名

文件或目录重命名的异步方法的语法格式如下:

fs.rename(oldPath, newPath, callback);
  • 这将异步地将由 oldPath 参数指定的文件重命名为由 newPath 参数提供的新的路径名。

  • 如果 newPath 指定的路径已存在,则覆盖该文件。

  • 回调函数 callback 只有一个用于处理异常的参数 err

删除文件

异步删除文件方法的语法格式如下:

fs.unlink (path, callback)
  • 这将异步地删除由 path 参数指定的文件或符号链接。

  • 回调函数 callback 只有一个用于处理异常的参数 err

fs.unink() 方法不能用于目录。要删除目录,需使用 fs.rmdir() 方法。

复制文件

异步复制文件方法的语法格式如下:

fs.copyFile(src, dest[, flags], callback);
  • 参数 src 和 dest 分别表示复制的源文件的路径和目标文件的路径。

  • 可选参数 flags 指定复制操作的行为,可由常量指定。

    • COPYFILE_EXCL 表示如果目标路径已存在,复制操作将失败;

    • COPYFILE_FICLONE 表示复制操作将尝试创建写时复制(copy-on-wite)链接,如果底层平台不支持,则使用备选的复制机制;

    • COPYFILE_FICLONE_FORCE 表示复制操作将尝试创建写时复制链接,如果底层平台不支持,则复制操作将失败。

    • flags 默认值0, 表示将创建或覆盖目标文件。

  • 除了用于处理可能的异常的参数 err,回调函数没有其他参数。

Node.js 不保证复制操作的原子性,如果在打开目标文件执行写入操作发生错误,则 Node.js 将尝试删除目标文件。

目录的基本操作

创建目录
异步创建目录的用法

fs.mkdir (path[, options], callback)

  • 参数 path 指定目录路径。

  • 可选的 options 参数可以是指定模式(权限)的整数,也可以是包括 mode 属性(设置目录权限,默认为0o777)和 recursive 属性(指示是否以递归的方式创建目录,即创建父目录,默认为 false )的对象。

  • 回调函数 callback 只有一个用于处理异常的参数 err

下面的代码表示创建 /tmp/a/apple 目录,无论是否存在 /tmp 和 /tmp/a 目录。

fs.mkdir('/tmp/a/apple', { recursive: true }, (err) => {
  if (err) throw err;
});
读取目录内容
异步读取目录内容的用法

fs.readdir(path[, options], callback)

  • 可选的 options 参数可以是指定编码的字符串(默认值’utf8' ),也可以是具有 encoding 属性和 withFileTypes 属性的对象,如果将 encoding 属性设置为 ‘buffer’,则返回文件 Buffer 对象; 如果将 withFileTypes 属性设置为 true, 则 files 数组将包含 fs.Dirent 的对象。

  • 回调函数有两个参数 err 和 files, 其中 files 是目录中文件名的数组(不包括 . 和 ..)。

这里给出一个查看当前工作目录的上一级目录内容的完整例子。

#clean src/ch04/fstest/dir-read.js
const fs = require("fs");
console.log("查看上一级目录的内容");
fs.readdir("../", function (err, files) {
  if (err) throw err;
  files.forEach(function (file) {
    console.log(file);
  });
});
删除目录
异步删除目录的用法

fs.rmdir (path, callback)

  • 要删除的目录路径由参数path指定。

  • 回调函数callback只有一个用于处理异常的参数 err。

  • 删除目录前需先删除文件。

对文件(而不是目录)使用 fs.mdir() 方法会导致在 Windows系统上出现 ENOENT 错误、在 POSIX 系统上出现 ENOTDIR 错误。

4.3 文件系统的高级操作

使用文件操作的流接口

从流中读取数据

从流中读取数据需要创建一个可读流,所用方法的语法格式如下:

fs.createReadStream(path[, options])
  • 这个方法返回的是 fs.readStream 类的实例对象。

  • path 指定用于创建流的文件的路径

  • 可选参数 options

    • 如果是字符串,则仅用于指定字符编码。

    • 如果是对象,则可以包括:

      • flags(文件系统标志,默认值’r')

      • encoding(字符编码,默认值null)

      • fd(文件描述符,默认值null)

      • mode(文件模式,默认值0o666)

      • autoClose(自动关闭,默认值true)

      • start(起始位置)

      • end(结束位置,默认值 Infinity)

      • highWaterMark(高水位线,默认值 64*1024)等属性

start和end值决定从文件中读取一定范围的字节而不是整个文件,start和end都包含在范围中并从0开始计数。例如,以下语句从一个大小为100字节的文件中读取其最后10个字节:

fs.createReadStream('sample.txt',{start: 90, end: 99});

如果指定了fd (文件描述符)并且start 被省略或值为undefined,则 fs.createReadStream() 当前文件位置开始顺序读取。

如果指定了fd,则可读流将忽略path参数并使用指定的文件描述符。这意味着不会触发open事件,fd 必须是阻塞的,非阻塞的fd会传给 net.Socket。

如果fd指向仅支持阻塞读取的字符设备(如键盘或声卡),则在数据可用之前,读取操作不会完成。这可以防止进程退出和流自然关闭。例如以下语句从一个字符设备创建一个流:

const stream = fs.createReadStream('/dev/input/event0');

下面给出一个通过流接口读取文件内容的例子。

#clean src/ch04/fstest/file_stream_read.js
从流中读取数据
const fs = require("fs");

var data = "";
var readStream = fs.createReadStream("demo.txt"); // 创建可读流
readStream.setEncoding("UTF8"); // 设置字符编码格式为 utf8
//以下处理流事件data(当有数据可读时触发)、end(无数据可读时触发)和 error(发生错误时触发)
readStream.on("data", (chunk) => {
  data += chunk;
});

readStream.on("end", () => {
  console.log(data);
});

readStream.on("error", (err) => {
  console.log(err.stack);
});
node file_stream_read.js
这是一个用于测试读取文件内容的文本文件。
将数据写入流

将数据写入流需要创建一个可写流,所用方法的语法格式如下:

fs.createWriteStream(path[, options])
  • 这个方法返回的是 fs.WniteStream 类的实例对象。

  • 参数 path 指定用于创建流的文件的路径。

  • 可选参数 options 与 fs.createWiteStream() 基本相同,不同之处列举如下:

    • flags(文件系统标志)默认值为’w'。如果要修改文件而不是覆盖它,则 flags 值要改为 'r+'。

    • 只有 start 属性用于指定写入数据的起始位置(从文件开头算起),没有end属性, 也没有 hightWaterMark属性。

    • 如果 autoClose 属性值为 true (默认行为), 则在触发 error 或 finish 事件时,文件描述符会被自动关闭;

    • 如果 autoClose 值为 false, 则即使出现错误也不会关闭文件描述符,这需要应用程序负责关闭它井确保没有文件描述符泄露。

流的管道操作

管道是在可读流与可写流之间的操作。下面代码演示了一个用流复制文件的简单过程:

const fs = require('fs');
var readStream = fs.createReadStream('src_file');  // 打开源文件准备读取
var writeStream = fs.createWriteStream('dest_file');  // 使用新的数据覆盖目标文件
readStream.pipe(writeStream);  // 从源文件读取时,将读取的数据写入到目标文件
流的链式操作

这是通过连接输出流到另外一个流并创建多个流操作链的机制。链式流一般用于管道操作。 可以用一个通过管道和链式来压缩和解压文件的例子来说明。

#clean src/ch04/fstest/file_compress.js
将 demo.txt 文件压缩为 demo.txt.gz
const fs = require("fs");
const zlib = require("zlib");
fs.createReadStream("demo.txt")
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream("demo.txt.gz"));
console.log("文件压缩完成!");
#clean src/ch04/fstest/file_decompress.js
将 demo.txt.gz 文件解压缩为 demo.txt
const fs = require("fs");
const zlib = require("zlib");
fs.createReadStream("demo.txt.gz")
  .pipe(zlib.createGunzip())
  .pipe(fs.createWriteStream("demo.txt"));
console.log("文件解压缩完成!");

文件遍历

文件遍历是经常要用到的一项功能,如删除一个目录及其所有子目录,或者列出一个目录及其所有子目录中的所有文件。这里示范遍历某目录下的所有文件,基本思路如下:

  1. 使用fs.readdir()方法获取目录下的文件列表。

  2. 遍历此文件列表,并使用fs.stat()方法获取文件信息。

  3. 根据获取的信息判断是文件还是目录。

  4. 如果是文件,打印其绝对路径。

  5. 如果是目录,返回第1步重新从开始获取文件列表直至第5步操作。由于目录级数未知,所以可以使用递归方法来解决。

实现的过程中需要使用 path.resolve() 方法来解析路径,以遍历当前目录的上一级目录为例:

#clean src/ch04/fstest/file_traverse.js
const fs = require("fs");
const path = require("path");
var filePath = path.resolve("../");
fileTraverse(filePath); //调用文件遍历函数

/*定义文件遍历函数,参数filePath是需要遍历的文件路径 */
function fileTraverse(filePath) {
  //根据文件路径读取文件,返回文件列表
  fs.readdir(filePath, function (err, files) {
    if (err) {
      console.warn(err);
    } else {
      //遍历读取到的文件列表
      files.forEach(function (file) {
        var fullPath = path.join(filePath, file); //获取当前文件的绝对路径
        //根据文件路径获取文件信息,返回一个fs.Stats对象
        fs.stat(fullPath, function (eror, stats) {
          if (eror) {
            console.warn("获取文件信息失败");
          } else {
            if (stats.isFile()) {
              //如果是文件
              console.log(fullPath);
            }
            if (stats.isDirectory()) {
              //如果是目录
              fileTraverse(fullPath); //递归,继续遍历该目录下面的文件
            }
          }
        });
      });
    }
  });
}

文件监视

fs 模块提供了两个文件监控方法用于监视文件或目录的更改。

  • fs.watch()

  • fs.watchFile()

选择 fs.watch() 还是 fs.watchFile()?

fs.watch() 方法通过底层操作系统来通知文件改变,比 fs.watchFile() 和 fs.unwatchFile() 方法更 高效,应尽可能使用fs.watch()方法。fs.watch() 方法的 API 在各个平台上并非完全一致,在某些情况下不可用。

如果底层功能由于某些原因不可用,则 fs.watch() 方法将无法运行。例如;当使用虚拟化软件(如 Vagrant、Docker 等)时,在网络文件系统(NFS、SMB等)或主文件系统上监视文件或目录可能是不可靠的,在某些情况下也是不可能的。此时,就要考虑选用 fs.watchFile() 方法。

fs.watchFile() 方法是早期的监控方法,其没有连接操作系统的通知系统,而是通过在一个时间间隔内不停地轮询来发现文件变化, 这种方法较慢且不太可靠,但它是跨平台的,因而在网络文件系统中更可靠。

fs.watch()
其语法格式为

fs.watch(filename[, options][, listener])

  • 参数 filename 指定要监视的文件或目录的路径

  • 可选参数 options

    • 如果是字符串,则仅用于指定文件名的字符编码。

    • 如果是对象,则可以包括 persistent(指示文件正被监视时进程是否应继续运行,默认值 true)

    • recursive (指示监视所有子目录还是仅监视当前目录,默认值false)。recursive 选项支持仅在 macOS 和 Windows 上。

    • encoding (文件名的字符编码,默认值’utf8')等属性。

  • listener 是一个可选的回调函数,包含两个参数 eventType 和 filename,前者表示事件类型,可以是’rename'(重命名)或 'change'(修改); 后者是触发事件的文件的名称。在大多数平台上,当文件名在目录中出现或消失时,就会触发 rename 事件。

下面是监听上一级目录下文件变化的简单例子。

#clean src/ch04/fstest/file_watch1.js
const fs = require("fs");
fs.watch("../", (eventType, filename) => {
  if (eventType == "rename") {
    console.log("发生重命名");
  }
  if (eventType == "change") {
    console.log("发生修改");
  }
  if (filename) {
    console.log(`文件名: ${filename}`);
  } else {
    console.log("文件名未提供");
  }
});

测试:在上级目录下新建一个文件,更改内容,删除该文件。查看终端打印结果。

fs.watchFile()
其语法格式为

fs.watchFile(filename[, optionsl, listener)

  • 参数 filename 指定要监视的文件或目录的路径。

  • options 参数是可选的,仅能是一个对象。options对象包含一个 persistent 属性用于指示当文件正在被监视时进程是否应该继续运行,还存在 interval 属性用于指示轮询目标的频率(以毫秒为单位)。

  • 回调函数 listener 也是可选的,它包含两个参数 current 和 previous, 分别表示当前的 fs.Stats 对 象(当前已修改的状态)和变动之前的 fs.Stats 对象(文件上一次修改的状态)。每当访问文件时都会调用 listener 回调函数。要在修改文件(而不仅仅是访问)时收到通知,则需要比较 curr.mtime 和 prev.mtime 这两个属性。

下面是一个监听某文件变化的简单例子。

#clean src/ch04/fstest/file_watch2.js
const fs = require('fs');
// 每隔1秒时间检测文件变化
fs.watchFile('demo.txt', {interval: 1000}, function (curr, prev) {
    console.log('当前的文件修改时间:' + curr.mtime + '当前的文件大小:' + curr.size);
    console.log("上一次文件修改时间:" + prev.mtime + '之前的文件大小:' + prev.size);
    // 停止监听
    fs.unwatchFile('demo.txt');  // 注释该行则会一直监听
});

测试:更改 demo.txt 文件内容,查看终端打印内容。

对于 fs.watchFile() 方法启用的监听,可使用 fs.unwatchFile() 方法停止,该方法的语法格式如下:

fs.unwatchFile(filename[, listener])

参数 listener 是可选的,其指向之前使用 fs.watchFile() 绑定的监听器。如果指定了 listener, 则仅 移除此特定监听器,否则,将移除所有与该文件关联的监听器,从而停止监视由 filename 参数指定的文件。对未被监视的文件名调用 fs.unwatchFile() 方法是空操作,而不是错误。

操作 JSON 文件

JSON 是一种轻量级的数据交换格式。任何数据类型都可以通过JSON来表示,例如字符串、数字、对象、数组等。但是对象和数组是比较特殊且常用的两种类型。

许多软件平台和应用软件使用JSON格式的文件作为配置文件。这里讲解在 Node.js 中对 JSON 文件的操作方法。从 JSON 文件读取数据之后,可以使用 JSON.parse() 方法将数据转换为 javascript 对象之后进行处理,要将数据写入 JSON 文件,则需使用 JSON.stringfy() 方法将对象或数组转换成字符串。

例中示范的 JSON 文件 person.json 存储的是 JSON 数组。

从 JSON 文件中读取数据

本操作的关键是将要读取的数据转换为字符串, 再转换为 JSON 对象,然后就可以处理数据。

Tip

JSON.parse(方法对格式要求很严格,格式不符就会报错。特别要注意, 文件如果包含中文字符,则要以无 BOM 的 UTF-8 格式保存文件。使用Windows的记事本将包含中文字符的文件存为 UTF-8 格式时可能会掺杂不能显示的Unicode 字符,从而导致 JSON.parse() 对其内容解析失败。应当将该文件转换为无 BOM 的 UTF-8 格式,或者转成二进制格式并删除 BOM 符号。

下面给出一个示例(在当前目录提前准备了一个名为 person.json 的JSON文件)。

#clean src/ch04/fstest/json_read.js
const fs = require("fs");
fs.readFile("person.json", function (err, data) {
  if (err) throw err;
  var person = JSON.parse(data.toString()); //将字符串转换为json对象
  //对JSON数据进行处理
  for (var i = 0; i < person.length; i++) {
    console.log("姓名:" + person[i].name + "     年龄:" + person[i].ages);
  }
});
node json_read.js
姓名:张三     年龄:28
姓名:李四     年龄:25
姓名:王五     年龄:30
往 JSON 文件中添加数据

此处的关键是将要读取的数据转换为 JSON 对象之后,将要添加的对象添加到数组对象中,再将 JSON 对象转换成字符串重新写入 JSON 文件中,示例代码如下。

#clean src/ch04/fstest/json_write.js
const fs = require("fs");

var newobj = {
  name: "刘强",
  ages: 24,
};

fs.readFile("person.json", function (err, data) {
  if (err) throw err;
  var person = JSON.parse(data.toString()); //将字符串转换为JSON对象
  person.push(newobj); //将新的对象加到数组对象中
  var str = JSON.stringify(person); //将JSON对象转换成字符串重新写入JSON文件中
  fs.writeFile("person.json", str, function (err) {
    if (err) throw err;
    console.log("数据写入成功!");
  });
});
node json_read.js
姓名:张三     年龄:28
姓名:李四     年龄:25
姓名:王五     年龄:30
姓名:刘强     年龄:24  // (1)
  1. 新增内容

更改 JSON 文件中的指定数据

此处的关键是将要读取的数据转换为 JSON 对象之后,查找要修改的对象并对其进行修改,再将 JSON 对象转换成字符串重新写入 JSON 文件中。

这里给出一个例子,仅列出其中关键代码。

#clean src/ch04/fstest/json_update.js
const fs = require("fs");

fs.readFile("person.json", function (err, data) {
  if (err) {
    return console.error(err);
  }
  var person = JSON.parse(data.toString()); //将字符串转换为JSON对象
  //将数据读出来,然后进行修改,这里将张三的年龄修改为28
  for (var i = 0; i < person.length; i++) {
    if (person[i].name == "张三") {
      person[i].ages = 39;
      break;
    }
  }
  var str = JSON.stringify(person); //将JSON对象转换成字符串重新写入JSON文件中
  fs.writeFile("person.json", str, function (err) {
    if (err) {
      return console.error(err);
    }
    console.log("数据修改成功!");
  });
});
node json_read.js
姓名:张三     年龄:39  // (1)
姓名:李四     年龄:25
姓名:王五     年龄:30
姓名:刘强     年龄:24
  1. 数据已经被更改

读取 GBK 格式的文件

Node.js 目前支持 utf8、ucs2/utf16-le、ascii、binary、base64 和 hex 编码格式的文件,并不支持 GBK、GB2312 的字符编码,因而无法读取 GBK 或 GB2312 编码格式文件中的内容,需要第三方模块的支持才能读取它们,如 iconv 和 iconv-lite。

  • iconv 使用二进制库实现,但仅支持 linux,无法在 Windows 系统上使用。

  • iconv-lite 是用纯 javascript 语言实现的,通用性更好,这里以为例讲解中文编码格式的处理。它支持的编码格式比较多,列举如下:

    • 所有 Node.js 原生支持的编码格式。

    • 其他 Unicode 编码,如utf16、utf16-be、utf-7、utf-7-imap

    • 所有常见单字节编码,如 Windows 125x 系列、IS0-8859 系列、Macintosh 系列、latin1等。

    • 所有常见多字节编码,如 CP932、CP936、CP949、CP950、GB2313、GBK、GB18030、Big5、Shift_JIS 和 EUC-JP。

下面给出一个从 GBK 格式文件读取内容,然后编码转存为 UTF-8 编码的例子。单独准备一个目录 gbktest, 先安装 iconv-lite 包:

cnpm install iconv-lite

接着准备一个 GBK 格式文件,使用 Windows 记事本编辑,将其保存为默认的 ANSI 格式,也就是系统默认的编码格式,简体中文版文件的默认编码格式为 GBK。例中所用的 GBK 格式示例文件为 gbksample.txt。

#clean src/ch04/gbktest/gbk2utf8.js
const fs = require("fs");
const iconv = require("iconv-lite"); // 加载编码转换模块

fs.readFile("gbksample.txt", function (err, data) {
  if (err) throw err;
  console.log(data); // 输出字节数组
  var text = iconv.decode(data, "gbk"); // 把数组转换为gbk中文
  console.log(text); // 输出中文内容
  //按默认的UTF8字符编码写入
  fs.writeFile("utf8sample.txt", text, function (err) {
    if (err) throw err;
  });
});
node gbk2utf8.js
<Buffer d5 e2 ca c7 d2 bb b8 f6 d3 c3 d3 da b2 e2 ca d4 b6 c1 c8 a1 47 42 4b c4 da c8 dd b5 c4 ce c4 b1 be ce c4 bc fe a1 a3 47 42 4b ca c7 ce d2 b9 fa b0 e4 ... 231 more bytes>
这是一个用于测试读取GBK内容的文本文件。GBK是我国颁布的一种汉字编码,作为一个体系,其前身是GB2312,后来演进为GB18030。这个系列的编码标准致力于解决中国国内面临的文字的编码问题,支持的语言包括中文汉字(简体)、日语、韩语、藏文、蒙文、维吾尔文等,主要还是中文。常见汉字用两个字节编码。

除了解码之外,iconv-lite 还支持编码,例如:

buf = iconv.encode("Sample input string", "win1251");

4.4 实战演练 — 操作表格文件

CSV (Comma-Separated Values) 文件以纯文本形式存储表格数据(数字和文本)。CSV 文件由任意数目的记录组成,记录间以某种换行符(记录分隔符)分隔;每条记录由字段组成,字段间的分隔符(字段分隔符)是其他字符或字符串,最常见的是逗号或 TAB (制表符)。CSV 是一种常见的数据交换格式,是电子表格 (如EXCEL) 或数据库表常用的数据导出格式。

在 Node.js 官网提供了不少关于 CSV 的包,最常用的是 CSV 包。CSV 包是一个全功能的 CSV 解析工具,可以完全在浏览器中运行。它为 Node.js 提供了 CSV 的创建、解析、转换和序列化功能,且支持较大的数据集合。

下面示范如何使用 CSV 包处理 CSV 文件。

使用 CSV 包的准备工作

CSV 包包括以下 4 个子包

  • csv-generate:用于创建 CSV 字符串和 JavasScript 对象。

  • csv-parse:将 CSV 文本转换为数组或对象。csv-parse 是一个将 CSV 文本转换为数组或对象的解析器。它提供了 Node.js 流 API,也提供回调 API 和同步 API 以兼顾老用户。

  • stream-transform:一个转换框架。

  • csv-stringify:将记录转换为 CSV 文本,相当于将数组或对象序列转换为 CSV 文本。

安装 CSV 包

  1. mkdir csvtest

  2. cd csvtest

  3. cnpm install csv

Tip

CSV 的 4 个子包都能很好地协同工作。也可根据需要单独使用其中的子包,这样只需安装子包即可,例如以下命令安装 csv-parse 包:

npm install csv parse

提前准备一个 CSV 文件 csvsample.csv (UTF-8格式), 将其复制到 csvtest 目录下用于实验。

从 CSV 文件中读取并解析数据

使用流 API(推荐)

流 API 是推荐的方式,基本用法如下:

const stream = parse([options])

csv-parse 解析器支持许多选项,如 delimiter 指定 CSV 文件的字段分隔符,from 和 to 指定要处理的记录范围。在下面的例子中, CSV 数据由文件可读流提供,由 readable 事件触发并通过 read() 函数获取解析的数据。

#clean src/ch04/csvtest/csv_read_stream.js
流式读取 CSV 文件
const fs = require("fs");
// const {parse} = require("csv-parse");  // (1)
const {parse} = require("csv");  // (2)

const output = [];  // (3)
const parser = parse({  // (4)
  delimiter: ",",
});

parser.on("readable", function () {  // (5)
  let record;
  while ((record = parser.read())) {
    output.push(record);
  }
});

parser.on("error", function (err) {  // 错误处理
  console.error(err.message);
});

parser.on("end", function () {  // (6)
  console.log(output);
});

fs.createReadStream(__dirname + "/csvsample.csv").pipe(parser);  // (7)
  1. 加载 parse 模块: 用于 cnpm install csv-parse

  2. 加载 parse 模块: 用于 cnpm install csv

  3. 定义一个用于输出的数组

  4. 创建解析器,这里使用逗号作为 CSV 文件的字段分隔符

  5. 有可读数据时,读取数据将其添加到输出数组

  6. 数据解析完毕,这里获取的是一个 JSON 数组,可以做一步处理

  7. 将从 CSV 文件读取的数据通过管道发送给上述可读流(解析器)

node csv_read_stream.js
[
  [
    '1',
    '10',
    '测温仪',
    '巡检',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:05'
  ],
  [
    '2',
    '11',
    '测温仪',
    '测试',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:06'
  ],
  ...
]
Tip

在上述代码中,如果不加载 csv-parse 模块,而直接加载 csv 模块,创建解析器时要在 parse() 方法之前加上「csv.」前缀,使其变成 csv.parse(), 其他几个模块类似。另外,如果 CSV 文件包含中文字符,则应以UTF-8 格式保存文件。

使用回调API(可选)

回调 API 是一种可选方案,其语法格式如下:

parse(data[, options], callback);
@clean src/ch04/csvtest/csv_read_callback.js
回调方式读取 CSV 文件
const fs = require("fs");
const {parse} = require("csv");

fs.readFile("csvsample.csv", function (err, data) {
  if (err) throw err;
  parse(data, { delimiter: "," }, function (err1, output) {
    console.log(output);
  });
});
node csv_read_callback.js
[
  [
    '1',
    '10',
    '测温仪',
    '巡检',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:05'
  ],
  [
    '2',
    '11',
    '测温仪',
    '测试',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:06'
  ],
  ...
]
组合使用流 API 和回调 API

也可以将流 API 与回调 API 组合使用,输出流的用法如下:

const stream = parse(input, options)

它将输入字符串和选项对象作为参数,返回一个可读流。

输入流正相反,用法如下:

const stream = parse (options, callback)

它将选项对象和回调函数作为参数,返回一个可写流。下面是一个简单的例子:

#clean src/ch04/csvtest/csv_read_mixing.js
组合方式读取CSV文件
const fs = require("fs");
const {parse} = require("csv");

var parser = parse({delimiter: ","}, function (err, data) {
  console.log(data);
});

fs.createReadStream(__dirname + "/csvsample.csv").pipe(parser);
node csv_read_mixing.js
[
  [
    '1',
    '10',
    '测温仪',
    '巡检',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:05'
  ],
  [
    '2',
    '11',
    '测温仪',
    '测试',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:06'
  ],
  ...
]
使用同步 API

同步 API 比较简单,其与回调 API 相比少了 callback 参数:

const records = parse(data, [options])

使用同步 API 时加载模块要使用以下形式:

const {parse} = require("csv-parse/sync");  // cnpm install csv-parse
const {parse} = require("csv/sync");  // cnpm install csv

将记录转换为 CSV 文件并保存到 CSV 文件中

stringfy 是一个将记录转换为 CSV 文本的转换器。它提供了 Node.js 的转换流 API,也提供便于使用的回调 API 和同步 API,并支持大数据集。流 API 是推荐的方式,基本用法如下:

const stream = stringfy([options])

stringfy 转换器支持多个选项。这里介绍一下 CSV 记录分隔符选项 record_delimiter,其选项值除了使用指定的字符或符号外,还可以使用内置的特殊分隔符,包括 'auto' 'unix' 'mac' 'windows' 'ascii' 和 'unicode',默认为 'auto'。

在下面的例子中,记录以数组形式通过 write() 函数发送到转换流,写入操作由 readable 事件触发并通过 read() 函数获取已转换的数据。转换完毕将数据写入 CSV 文件。

#clean src/ch04/csvtest/csv_stringify.js
const fs = require("fs");
const {stringify} = require("csv");  // 加载 stringify 模块
const data = [];
const stringifier = stringify({      // 创建转换流
  delimiter: ":",
  // record_delimiter: "windows",    // 每行结尾换行符号
});

stringifier.on("readable", function () {  // 有可读数据时逐行处理
  let row;
  while ((row = stringifier.read())) {
    data.push(row);
  }
});

stringifier.on("error", function (err) {
  console.error(err.message);
});

// 数据转换完毕,可以进一步处理,这里将其保存到CSV文件
stringifier.on("finish", function () {
  fs.writeFile("newcsv.csv", data.toString(), function (err) {
    if (err) {
      return console.error(err);
    }
    console.log("数据写入成功!");
  });
});
// 将记录写到流
stringifier.write(["root", "x", "0", "0", "root", "/root", "/bin/bash"]);
stringifier.write(["zxp", "x", "1022", "1022", "", "/home/zxp", "/bin/bash"]);
stringifier.end(); // 关闭流

对 CSV 数据进行转换处理

transform 提供一个简单的对象转换框架用于实现 Node.js 转换流 API。转换必须基于用户定义的函数。流 API 是推荐的方式,基本用法如下:

const stream = transform(records, [options], handler, [options], [callback])

其中 handler 就是用于转换的处理函数。在下面的例子中,由于发送到转换流的数据形式必须是记录(数组或对象),因此要先用 parse 模块将从 CSV 文件中读取的文本转换为数组,再发送到转换流,这可用链式管道操作来实现。

#clean src/ch04/csvtest/csv_transf.js
const fs = require("fs");
const {parse} = require("csv");
const {transform} = require("csv");

const output = [];
const transformer = transform(function (data) {
  data.push(data.shift());  // (1)
  return data;
});

transformer.on("readable", function () {
  while ((row = transformer.read())) {
    output.push(row);
  }
});

transformer.on("error", function (err) {
  console.error(err.message);
});

transformer.on("finish", function () {
  console.log(output);
});

fs.createReadStream(__dirname + "/csvsample.csv")
  .pipe(
    parse({
      delimiter: ",",
    })
  )
  .pipe(transformer);
  1. 将前面的序号挪到最后面,如 1 到了最后 →

    [ "10", "测温仪", "巡检", "30.0℃", "15.0℃", "29.5℃", "2018/11/15 10:05", "1", ]

node csv_transf.js
[
  [
    '10',
    '测温仪',
    '巡检',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:05',
    '1'
  ],
  [
    '11',
    '测温仪',
    '测试',
    '30.0℃',
    '15.0℃',
    '29.5℃',
    '2018/11/15 10:06',
    '2'
  ],
  ...

4.5 本章小结

4.6 习题

5. 网络编程

TCP 服务器与客户端

TCP 基础

net 模块提供的 API

创建 TCP 服务器和客户端

接收和处理物联网数据

UDP 服务器与客户端

UDP 基础

dgram 模块提供的 API

创建 UDP 服务器和客户端

通过 UDP 实现文件上传

HTTP 服务器和客户端

HTTP 基础

实现 HTTP 服务器

实现 HTTP 客户端

HTTP 服务器获取并解析请求内容

使用 WebSocket 实现浏览器与服务器的实现通信

WebSocket 简介

使用 Node.js 实现 WebSocket 服务器和客户端

浏览器客户端

Socket.IO

实战演练 — 构建实时聊天室

准备 Web 框架

编写服务器端程序

编写客户端程序

测试

本章小结

习题

6. SQL 数据库操作

操作 MySQL 数据库

MySQL 服务器安装和基本使用

Node.js 的 MySQL 驱动

连接 MySQL 数据库

执行数据库操作

记录的增查改删操作

防止 SQL 注入攻击

使用流式查询

使用预处理语句

使用连接池

优雅地编写异步代码

Promise

Generator

co 模块

async/await

使用 Promise 包装器操作 MySQL 数据库

使用 Node.js ORM 框架操作关系数据库

Sequelize 简介

Sequelize 的基本使用

使用 Sequelize 的关联

实战演练 — 图书借阅记录管理

编写模型部分代码

编写数据操作部分代码

本章小结

习题

7. MongoDB 数据库操作

MongoDB 数据库基础

NoSQL 数据库简介

MongoDB 数据库简介

MongoDB 基本概念

MongoDB 的安装和基本使用

使用原生驱动连接和操作 MongoDB 数据库

连接到 MongoDB

添加 MongoDB 文档

查询 MongoDB 文档

更改 MongoDB 文档

删除 MongoDB 文档

为 MongoDB 集合创建索引

以同步方式编写 MongoDB 操作代码

使用 Mongoose 操作 MongoDB 数据库

Mongoose基本概念

使用 Mongoose 的基本步骤

文档操作

数据验证

中间件

子文档

Mongoose 对Promise 的支持

实战演练 — 开发图书使用管理操作接口

Mongoose 的填充功能

模式和模型定义

编写数据库操作接口

调用数据库操作接口

本章小结

习题

8. Node.js 框架与 Express

Node.js 框架概述

MVC 框架

REST API 框架

全栈框架

实时框架

Node.js 框架的选择

Express 框架基础

简单的"Hello World"示例程序

使用 Express 生成器创建项目脚手架

Express 工作机制

Express 路由

路由结构

路由方法

路由路径

路由参数

路由处理程序

响应方法

app.route() 方法

Express 路由器

Express 中间件

进一步理解中间件

使用中间件

编写自己的中间件

视图与模板引擎

模板引擎概述

EJS语法

EJS模板引擎的使用

Express 与数据库集成

Express 错误处理

捕获错误

默认错误处理程序

编写错误处理程序

实战演练 — 图书信息管理的 RSET API 接口

了解 REST API

演练目标

实现思路与技术准备

创建项目

编写数据库接口程序

编写主入口文件

运行程序进行测试

控制 API 版本

本章小结

习题

9. 应用测试与部署

Node.js 应用程序的单元测试

单元测试概述

使用断言编写简单的测试脚本

使用测试框架 Mocha 进行单元测试

实战演练 — 为应用程序进行单元测试

熟悉 supertest 测试库

编写测试脚本

执行自动化测试

Node.js 应用程序的部署和运行管理

Node.js 应用程序的部署方式

让 Node.js 应用程序更稳定地运行

使用 PM2 管理 Node.js 应用程序

使用 PM2 的配置文件管理应用程序

在 Windows 平台上配置开机自动启动 Node.js 应用程序

本章小结

习题

10. 综合实例 — 构建博客

项目准备

项目概述

创建项目脚手架

数据库设计与实现

数据存储和组织技术

定义存储文章信息的模型

定义存储用户信息的模型

用户界面设计与实现

前端技术

静态文件

公共模板

内容模板

业务逻辑设计与实现

使用 passport 实现用户认证

session 控制

使用 Express 中间件

定义控制器处理具体业务

路由控制

主文件

本章小结

习题

Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/swotpp/learning.git
git@gitee.com:swotpp/learning.git
swotpp
learning
learning
devel

搜索帮助