初识 NodeJS

NodeJS 官网

NodeJS & 传统服务器区别

当客户端向传统的服务端 (apache、resin、tomcat、iis)发起请求时, 所属的进程都会单独开辟一个线程来处理该请求, 该线程又会去数据库中 (mysql、redis、mongodb、memcached) 找相应的数据返回给该线程, 线程再返回给客户端, 此时该线程就会被销毁, 但这个过程可能会有很多原因导致返回时间较长

Tips

  1. 针对上述过程, 服务端有相应的优化手段: 线程池概念(多线程模式)
  2. 程序 > 进程 > 线程
  3. 进程是操作系统分配资源和调度任务的基本单位

但 NodeJS 是一个单线程、非阻塞 I/O 服务器, 当客户端向服务端发起请求时, NodeJS 会调用唯一的主线程对请求进行处理, 去请求数据库中相应的数据, 但是不会等数据库返回, 而是去处理下一个客户端的请求, 等数据库中返回了数据后, 唯一的主线程才会去接收返回的数据返回给客户端

非阻塞 I/O: 数据库的请求不会堵塞主线程

NodeJS 优点

  1. 节约内存: 多线程 -> 单线程, 使用事件驱动、非阻塞 I/O 模型, 轻量 & 高效
  2. 节约上下文 (context 环境) 切换时间: 传统多线程并不是在同一个时间执行多个任务, 而是不停、快速的切换操作系统的时间片来实现的
  3. 不需要考虑并发资源处理, 锁的问题: 多线程的时候会同时运行, 但是当同时运行的线程都需要访问同一个资源的时候就会出现问题; JAVA 锁的实现: 用的时候把它锁上, 用完后解锁
  4. 执行效率高: 基于 Chrome V8 引擎的 JavaScript 运行环境, 让 JS 执行效率和低端 C 语言相近的执行效率
  5. 社区庞大: NodeJS 的包管理器 npm, 是全球最大的开原生态系统

但是在 NodeJS 中, 如果一个线程崩了, 那么整个服务器就 down 了

NodeJS 特点

  1. 为什么 JavaScript 是单线程?

    • 由于 JS 这门脚本语言的用途决定的: 开发页面
    • 之后出现了 Web Worker 多线程, 但它并没有改变 JS 单线程的本质
      • 完全受主线程控制
      • 不能操作 DOM
  2. 浏览器模型
    browserModel

    • 用户界面: 包括地址栏、前进/后退按钮、书签菜单等
    • 浏览器引擎: 在用户界面和呈现引擎之间传送指令
    • 呈现引擎(渲染引擎/浏览器内核): 在线程方面又称 UI 线程
    • 网络: 用于网络调用, 如 HTTP 请求
    • 用户界面后端: 用于绘制基本的窗口小部件, UI 线程和 JS 线程是共用一个线程
    • JS 解释器: 用于解析和执行 JS 代码
    • 数据存储: 持久层, 浏览器需要在硬盘中保存各种数据, 如 Cookie
  3. 除 JS 线程和 UI 线程之外的其它线程

    • 浏览器事件触发线程
    • 定时触发器线程
    • 异步 HTTP 请求线程
  4. 任务队列

    • 所有同步任务都在主线程上执行, 形成一个执行栈
    • 主线程之外, 还存在一个任务队列; 只要异步任务有了运行结果, 就在任务队列中放置一个事件
    • 一旦执行栈中的所有同步任务执行完毕, 系统就会读取任务队列, 看看里面有哪些事件; 那些对应的异步任务结束等待状态, 进入执行栈, 开始执行
    • 主线程不断重复上面三步
  5. Event Loop
    主线程从任务队列中读取事件, 这个过程是循环不断的, 所以整个的这种运行机制又称为 Event Loop (事件循环)
    eventloop

    注意: 只有当栈中同步任务执行完后, 才会执行 EventLoop 去异步任务队列中取

  6. NodeJS 中的 Event Loop
    eventloop
    V8 引擎解析 JS 脚本; 解析后的代码调用 Node API; libuv 库负责 Node API 的执行, 它将不同的任务分配给不同的线程, 形成一个 Event Loop(事件循环), 以异步的方式将任务的执行结果返回给 V8 引擎; V8 引擎再将结果返回给用户

    libuv 实现异步 I/O 的原理是事件「事件池 + 同步 I/O

  7. 同步与异步 & 阻塞与非阻塞
    同步和异步关注的是消息通知机制

    • 同步: 发出调用后, 没得到结果前, 该调用不返回, 一旦调用返回, 就得到返回值了; 简而言之就是调用者主动等待这个调用结果
    • 异步: 调用者在发出调用后这个调用就直接返回了, 所以没有返回结果; 换句话说, 当一个异步过程调用发出后, 调用者不会立刻得到结果, 而是调用发出后, 被调用者通过状态、通知或回调函数处理这个调用

    阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态

    • 阻塞调用: 调用结果返回之前, 当前线程会被挂起, 调用线程只有在得到结果后才会返回
    • 非阻塞调用: 不能立刻得到结果之前, 该调用不会阻塞当前线程

    同步异步取决于被调用者, 阻塞非阻塞取决于调用者: 同步阻塞、异步阻塞、同步非阻塞、异步非阻塞

REPL

REPL 所指的是 read eval print loop, 可交互式运行环境, 方便开发者测试 JS 代码

安装 node 后, 打开 iterm / cmd 等命令行容器中, 输入 node 即可进入 REPL 运行环境

REPL 操作

  • 变量的操作, 声明普通变量和对象
  • eval(计算)
  • 函数的书写
  • 下划线访问最近使用的表达式
  • 多行书写
  • REPL 运行环境中的上下文对象

具体操作可进入后输入 .help 查看

1
2
3
4
5
6
7
8
9
10
/* 访问最近使用的表达式 */
> 1 + 1 // 2
> _ + 1 // 3

/* 也可以通过代码来进入 repl / 创建一个 repl */
let repl = require('repl');
// 上下文, 就是它运行时的环境, repl 可以在环境中拿到一些变量
let context = repl.start().context;
context.msg = 'hello';
context.hello = function () { console.log(context.msg) }

Node 中的模块

Console 控制台

  • 标准输出(1): loginfo
  • 错误输出(2): warnerror
  • 用来统计两段代码之间的执行时间: time(label) & timeEnd(label) label 要一致
  • 断言: assert
    1
    2
    3
    4
    5
    6
    // 如果表达式为 true 则什么都不发生, 如果为 false 则会报错
    // nagios 监控服务器, 发生错误会报警、发通知
    console.assert(1 === 1, 'ERROR')
    // e.g
    function sum(a, b) { return a + b }
    console.assert(sum(1, 2) === 3, 'ERROR')

    进阶的重要标志就是写代码会有完善的测试, 包括单元测试、集成测试、持续集成、TDD 测试驱动开发、BDD 行为驱动开发

  • 可列出对象结构: dir
  • 跟踪当前代码调用栈: trace

Globals 全局对象

windows 里也有全局对象, 但是不能直接访问, 我们在浏览器里访问 global 是通过 window 实现的

  1. global 的变量都是全局变量
  2. 所有的全局变量都是 global 的属性

global 中常用的属性

  • console
  • process 当前进程
    • argv
    • chdir: change directory 改变当前的工作目录
    • memoryUsage: 内存使用量
    • chdir: 切换目录
    • cwd: current working directory 当前工作目录
    • nextTick
    • stdout stderr stdin
  • Buffer
  • clearImmediate & clearInterval & clearTimeout & setImmediate & setInterval & setTimeout
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    process.cwd(); // 当前目录
    process.chdir('..') // 切换到上级目录
    process.memoryUsage();
    // V8 引擎最大使用内存量是 1.7 个G
    {
    rss: 22867968, // 常驻内存
    heapTotal: 4648960, // 堆的总申请量
    heapUsed: 2641664, // 已经使用的量
    external: 828902 // 外部内存的使用量
    }

nextTick & setImmediate 区别与联系

  • nextTick 把回调函数放在当前执行栈的底部
  • setImmediate 把回调函数放在事件队列的尾部

events 事件触发器

在 NodeJS 中用于实现各种事件处理的 event 模块中, 定义了 EventEmitter 类, 所有可能触发事件的对象都是一个继承自 EventEmitter 类的子类实例对象

方法和参数 描述
addListener(event, listener) 对指定事件的绑定事件动作处理函数
on(event, listener) 对指定事件的绑定事件动作处理函数
once(event, listener) 对指定事件指定只执行一次的事件处理函数
removeListener(event, listener) 对指定事件的解除绑定事件动作处理函数
removeAllListener([event]) 对指定事件的解除所有绑定事件动作处理函数
setMaxListeners(n) 指定事件处理函数的最大数量, n 为整数值
listeners(event) 获取指定事件的所有事件处理函数
emit(event, [arg1], [arg2], [...]) 手动触发指定事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// let EventEmitter = require('events');
let EventEmitter = require('./events');
let util = require('util');
function Bell() {
// 继承私有属性
EventEmitter.call(this);
}
// 进行原型继承: 子类只继承了父类的公有方法
// Object.setPropertyOf(ctor.prototype, superCtor.prototype)
// 上述源码等同于: ctor.prototype.__proto__ = superCtor.prototype
util.inherits(Bell, EventEmitter);
let bell = new Bell();
function timeToRise(mode, condition) {
console.log(`${condition} ${mode} rise`)
};
function timeToWash(mode, condition) {
console.log(`${condition} ${mode} wash`)
};
function timeToEat(mode) {
console.log(`${mode} eat`)
};
bell.setMaxListeners(0);
bell.on('ring', timeToRise);
bell.on('ring', timeToWash);
bell.once('ring', timeToEat);
// 第一个参数是事件类型, 第二个及以后的参数会传递给监听函数
bell.emit('ring', 'swift', 'must');
bell.emit('ring', 'swift', 'must');

实现 events 中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function EventEmitter() {
// 会把所有的事件监听函数放在这个对象里保存
this.events = {};
// 指定给一个事件类型增加的监听函数数量
this._maxListeners = 10;
}
EventEmitter.prototype.setMaxListeners = function (maxListeners) {
this._maxListeners = maxListeners;
}
EventEmitter.prototype.listeners = function (event) {
return this.events[event];
}
/**
* 给指定的事件绑定事件处理函数
* 1. 事件类型
* 2. 事件监听函数
*/
EventEmitter.prototype.on = EventEmitter.prototype.addListener = function (type, listener) {
if (this.events[type]) {
this.events[type].push(listener);
if (this._maxListeners != 0 && this.events[type].length > this._maxListeners) {
console.error(`MaxListenersExceededWarning: Possible EventEmitter memory leak detected. ${this.events[type].length} ${type} listeners added. Use emitter.setMaxListeners() to increase limit`);
}
} else {
// 如果以前没有添加到此事件的监听函数, 则赋一个数组
this.events[type] = [listener];
}
}
EventEmitter.prototype.once = function (type, listener) {
// 用完即焚, 使用箭头函数保证 this 永远指向 EventEmitter 实例
let wrapper = (...rest) => {
// 先让原始的监听函数执行
listener.apply(this, rest);
this.removeListener(type, wrapper);
}
this.on(type, wrapper);
}
EventEmitter.prototype.removeListener = function (type, listener) {
if (this.events[type]) {
// 如果当前迭代的项目如果不等于参数的话就保留, 否则过滤掉
this.events[type] = this.events[type].filter(i => i != listener);
}
}
// 移除某个事件的所有监听函数
EventEmitter.prototype.removeAllListeners = function (type) {
delete this.events[type];
}
EventEmitter.prototype.emit = function (type, ...rest) {
this.events[type] && this.events[type].forEach(listener => listener.apply(this, rest));
}
module.exports = EventEmitter;

util 实用工具

1
2
3
4
5
6
7
let util = require('util');
// inspect 精确控制一个对象的打印效果
let obj = { name: 'mars', age: 18, address: {
city: {name: 'hangzhou'}
} }
util.inspect(obj, {depth: 1}) // { name: 'mars', age: 18, address: { city: [Object] } }
// is... 判断是不是某种类型

debugger 调试器

1
2
3
4
5
6
7
8
9
$ node inspect <filename>   # 调试
$ cont c # 继续执行
$ next n # 断点的下一步
$ step s # 进入函数
$ out o # 退出当前函数
$ pause # 暂停
$ repl # 进入 repl 环境
$ watch('variable') # 监控值
$ watcher('variable') # 查看监控值

process.nextTick

1
2
3
4
5
6
7
8
9
10
11
function Clock() {
this.listener;
process.nextTick(() => {
this.listener();
})
}
Clock.prototype.add = function (listener) {
this.listener = listener;
}
let c = new Clock();
c.add(() => { console.log('hey') })

Module 模块

模块也是 NodeJS 中的模块之一

模块的发展历史

  1. 命名空间
  2. 闭包、自执行函数
  3. require.js AMD
  4. sea.js CMD
  5. node.js CommonJS
  6. es6 ESModule
  7. umd AMD + CMD + CommonJS + ESModule

JS 模块化的不足

  • JS 没有模块系统, 不支持封闭的作用域和依赖管理
  • 没有标准库, 没有文件系统和 I/O 流 API, 也没有包管理系统

CommonJS 规范

  • 封装功能
  • 封闭作用域
  • 可能解决依赖问题
  • 效率更高, 重构方便

NodeJS 中的 CommonJS

  1. 在 NodeJS 中每一个单独的文件都是一个单独的模块
  2. 通过 require 方法实现模块间的依赖管理
1
2
3
4
5
6
/**
* Module A (personal.js)
*/
let name = 'mars';
let age = 18;
module.exports = { name, age };
1
2
3
4
5
6
7
8
9
10
11
/**
* Module B (readPersonal.js)
*/
let personal = require(./personal)
// 源码实现
!function(exports, require, module, __filename, __dirname) {
let name = 'mars';
let age = 18;
module.exports = { name, age };
return module.exports
}

在 NodeJS 中通过 require 方法加载其它模块(这个加载是同步的)

为什么加载是同步的? 因为模块实现了缓存, 但第一次夹在一个模块的时候, 会缓存这个模块的 exports 对象, 以后如果再次加载这个模块的话, 则直接去缓存中取, 不需要再次加载了
缓存的 key 是什么? 不同文件, 不同变量去引入相同的文件时, 都会走缓存, 因为引用的时候该文件的绝对路径, 和在哪儿引用、用什么变量引用没有关系

  1. 找到这个文件: dirname 取得当前模块文件所在目录的绝对路径
  2. 读取该文件模块的内容
  3. 把它封装在一个函数中立刻执行
  4. 执行后把模块的 module.exports 对象赋值给加载该模块的模块

module 对象中包含以下几种属性

  • id: 模块 ID, 入口模块的 ID 用于为 .
  • exports: 导出对象, 默认是一个空对象
  • parent: 父模块, 此模块是由哪个模块加载的
  • filename: 当前模块的绝对路径
  • loaded: 是否加载完成
  • children: 此模块加载了哪些模块, 默认是一个空数组
  • path: 第三方模块的查找路径

require 对象中包含以下几种属性

  • resolve: 只获取模块的绝对路径, 但又不会加载这个模块
  • main: 入口模块
  • extensions: 在 Node 中模块的类型有三种「JS 模块、json 模块(先找文件, 读取文件内容后 JSON.parse 转成对象返回)、node C++ 扩展二进制模块(这个属于二进制模块)」
    • 当 require 加载一个模块的时候, 会先找该文件, 如果找不到, 再依次去找 .js、.json、.node
  • cache: 对 require 模块的缓存

Node 模块分类

  1. 原生(内置)模块: 放在 node.exe 中

    • http path fs utils events 编译成二进制, 通过名称来加载, 加载速度最快
  2. 文件模块: 在硬盘的某个位置, 加载速度非常慢, 文件模块通过名称或路径来加载, 文件模块的后缀有三种

    • 后缀名为 .js 的 JavaScript 脚本文件, 需要先读入内存再运行
    • 后缀名为 .json 的 JSON 文件, fs 读入内存后转化为 JSON 对象
    • 后缀名为 .node 的经过编译后的二进制 C/C++ 扩展模块文件, 可以直接调用

      一般自己写的通过相对/绝对路径来加载, 别人写的通过名称去当前目录或全局的 node_modules 下面去找

  3. 第三方模块

    • 如果 require 函数只指定名称则视为从 node_modeles 下面加载文件, 这样的话可以移动模块而不需要修改引用的模块路径
    • 第三方模块的查询路径包括 module.paths 和全局目录
    1. 全局目录
      windows 如果在环境变量中设置了 NODE_PATH 变量, 并将变量设置为一个有效的磁盘目录, require 在本地找不到此模块时, 会在此目录下找这个模块; UNIX 操作系统会从 $HOME/.node_modules $HOME/.node_libraries 目录下寻找
    2. 模块的加载策略
      module
    3. 文件模块查找规则
      find-node-module

Buffer 缓冲器

JS 没有二进制数据类型, 而在处理 TCP 和文件流的时候, 必须要处理二进制数据; NodeJS 提供了一个 Buffer 对象来提供对二进制数据的操作, 是一个表示固定内存分配的全局对象, 暂时存放输入数据数据的一段文字, 也就是要放到缓存区中的字节数需要提前确定, Buffer 好比由一个8位字节元素组成的数据, 可以有效地在 JS 中表示二进制数据
Buffer 中显示的永远输出 16 进制

定义 Buffer 的三种方式

  1. 通过长度定义 Buffer
    1
    2
    3
    4
    5
    // 表示分配一个长度为6个字节的 Buffer, 默认会把所有字节设置为0
    const buf1 = Buffer.alloc(6); // <Buffer 00 00 00 00 00 00>
    const buf2 = Buffer.alloc(6, 2); // <Buffer 02 02 02 02 02 02>
    // 分配一块没有初始化的内存
    const buf3 = Buffer.allocUnsafe(6); // <Buffer 90 fe 00 03 01 00>
  2. 通过数据定义 Buffer
    1
    const buf4 = Buffer.from([1, 2, 3]); // <Buffer 01 02 03>
  3. 字符串创建
    1
    2
    // 根据字符串来初始化 Buffer
    const buf4 = Buffer.from('中国'); // <Buffer e4 b8 ad e5 9b bd>

Buffer 常用方法

  1. buf.fill(填充值, 开始索引, 结束索引)
    1
    2
    let buf = Buffer.alloc(3); // <Buffer 00 00 00>
    buf.fill(3, 1, 3); // <Buffer 01 01 00>
  2. buf.write(写入的字符串, 写入前跳过的字节数「默认0」, 写入最大字节数, 字符编码「默认 utf8」)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let buf = Buffer.alloc(6); // <Buffer 00 00 00 00 00 00>
    buf.write('中国', 0, 6, 'utf8'); // <Buffer e4 b8 ad e5 9b bd>
    buf.write('中国', 3, 6, 'utf8'); // <Buffer 00 00 00 e4 b8 ad>
    buf.write('中国', 0, 3, 'utf8'); // <Buffer e4 b8 ad 00 00 00>
    // writeInt8(值, 索引): 向指定的索引写入一个八位的整数(也就是占用一个字节的整数)
    let buf1 = Buffer.alloc(6); // <Buffer 00 00 00 00 00 00>
    buf1.writeInt8(0, 0); // <Buffer 00 00 00 00 00 00>
    buf1.writeInt8(16, 1); // <Buffer 00 10 00 00 00 00>
    buf1.writeInt8(32, 2); // <Buffer 00 10 20 00 00 00>
    // writeInt16BE(): Big Endian 高位在前
    // writeInt16LE(): Little Endian 低位在前
    let buf2 = Buffer.alloc(4);
    buf2.writeInt16BE(256, 0); // <Buffer 01 00 00 00>
    buf2.readInt16BE(0); // 256
    buf2.writeInt16LE(256, 2); // <Buffer 01 00 ff 00>
    buf2.readInt16LE(2); // 256
  3. buf.toString(encoding, start, end)
    1
    2
    3
    // 将 Buffer 转成字符串
    let buf = Buffer.from('中国');
    let res = buf.toString('utf8', 3, 6); // '国'
  4. buf.slice(start, end)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 注意: slice 是浅拷贝
    let buf = Buffer.alloc(6, 1);
    let res = buf.slice(2, 6); // <Buffer 01 01 01 01>
    let res2 = res.fill(4); // <Buffer 04 04 04 04>
    console.log(res); // <Buffer 04 04 04 04>

    // string_decoder: 解决截取乱码的问题
    // write 就是读取 buffer 内容, 返回一个字符串
    // write 时会判断是不是一个字符串, 如果是的话就输出, 不是的话则缓存在对象内部,
    // 等下次 write 时会把前面缓存的字符加到第二次 write 的 buffer 上在进行判断
    const { StringDecoder } = require('string_decoder');
    const sd = new StringDecoder();
    let buf = Buffer.from('中国');
    let buf1 = buf.slice(0, 4).toString(); // '中�;
    let buf2 = buf.slice(4).toString(); // <Buffer 9b bd>
    console.log(sd.write(buffer.slice(0, 4))); // '中'
    console.log(sd.write(buffer.slice(4))); // '国'
  5. buf.copy(target, targetStart, sourceStart, sourceEnd)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const buf1 = Buffer.allocUnsafe(26);
    const buf2 = Buffer.allocUnsafe(26).fill('!');
    for (let i = 0; i < 26; i++) {
    // 97 是 'a' 的十进制 ASCII 值。
    buf1[i] = i + 97;
    }
    // 拷贝 `buf1` 中第 16 至 19 字节偏移量的数据到 `buf2` 第 8 字节偏移量开始。
    buf1.copy(buf2, 8, 16, 20);
    buf2.toString('ascii', 0, 25) // !!!!!!!!qrst!!!!!!!!!!!!!
  6. Buffer.concat(list, totalLength)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 连接 buffer
    let buf1 = Buffer.from('中');
    let buf2 = Buffer.from('中');
    Buffer.concat([buf1, buf2]); // <Buffer e4 b8 ad e4 b8 ad>
    // Buffer.concat 实现
    Buffer.concat = function (list, total = list.reduce((len, item) => len + item.length, 0)) {
    if (list.length == 1) return list[0];
    // Buffer
    let result = Buffer.alloc(total);
    let index = 0;
    // 循环 buffer
    for (let buf of list) {
    // 循环 buffer 中的每个字节
    for (let b of buf) {
    if (index >= total) {
    return result;
    } else {
    result[index++] = b;
    }
    }
    }
    return result;
    }
  7. Buffer.isBuffer: 判断是否是 buffer
  8. length: 获取字节长度(显示是字符串所代表 buffer 的长度)
    1
    2
    Buffer.byteLength('中国');
    buffer.length;

fs 系统文件

  • 在 NodeJS 中, 使用 fs 模块来实现所有有关文件及目录的创建、写入及删除操作
  • 在 fs 模块中, 所有的方法都分为同步和异步两种实现, 具有 sync 后缀的方法为同步方法, 不具有 sync 后缀的方法为异步方法

整体读取 & 写入文件

文件系统标志(flag) 查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const fs = require('fs');
/*
* 异步读取
* fs.readFile(path, options, callback);
* options: encoding 编码形式 & flag 默认为 'r'
* encoding 是在要写入的字符串转成 Buffer 二进制时使用的
*/
fs.readFile('./1.txt', { encoding: 'utf8', flag: 'r' }, (err, data) => {
if (err) {
console.log(err);
} else {
console.log(data);
}
})
/*
* 异步写入
* fs.readFile(path, options, callback);
*/
fs.writeFile('./2.txt', 'data',
{ encoding: 'utf8', flag: 'w', mode: 0o666 },
function err => console.log(err))
/*
* 异步追加(相当于 flag 的值为 a)
* fs.readFile(path, options, callback);
*/
fs.appendFile('./3.txt', 'data', err => console.log(err));

// 同步读取: fs.readFileSync(path, options);

上述方式都是把文件当成一个整体来操作, 当文件特别大时, 大于内存的是无法执行这样的操作的, 所以需要控制读取的字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* 文件描述符
* fs.open()
* fd: file descriptor
* 返回值: 0 标准输入 1 标准输出 2 错误输出
*/
const str = '中国';
fs.open('./1.txt', 'w'/* 'r' */, 0o666, (err, fd) => {
/*
* fs.write(fd, buffer, offset, length, position, callback)
* offset: 读取 buffer 偏移量
* length: 从文件中读取几个字节
* position: 文件的写入索引, 不传表示当前位置
* callback 中的第三个参数 buffer 和 read 方法中第二个参数是相同的
*/
const buf = Buffer.from(str);
// 当调用 write 方法写入文件时, 并不会直接写入物理文件, 而是会先写入缓存区, 再批量写入物理文件
fs.write(fd, buf, 0, 3, 0, (err, bytesWritten) => {
console.log(bytesWritten);
fs.write(fd, buf, 3, 3, 3, (err) => {
// 强制操作系统马上把缓存区的内容写入物理文件
fs.fsync(fd, () => {
// 关闭文件
fs.close(fd, () => {
console.log('关闭文件完成');
})
})
})
})
/*
* fs.read(fd, buffer, offset, length, position, callback)
* offset: buffer 的写入索引
* length: 从文件中读取几个字节
* position: 文件的读取位置, 不传表示当前位置
* callback 中的第三个参数 buffer 和 read 方法中第二个参数是相同的
*/
let buf = Buffer.alloc(3);
fs.read(fd, buf, 0, 3, 0, (err, bytesRead, buffer) => {
console.log(buf.toString());
})
})

为了实现节约内存的拷贝, 需要读一点写一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs');
const BUFFER_SIZE = 3; // 缓存大小为3个字节
function copy(src, target) {
fs.open(src, 'r', 0o666, (err, readFd) => {
fs.open(target, 'w', 0o666, (err, writeFd) => {
let buf = Buffer.alloc(BUFFER_SIZE);
!function next() {
// 强行把缓存区的数据写入文件, 并且关闭
// fs.fsync(fd, (err) => {
// fs.close(() => {})
// })
fs.read(readFd, buf, 0, BUFFER_SIZE, null, (err, bytesRead, buffer) => {
if (bytesRead > 0)
fs.write(writeFd, buf, 0, bytesRead, null, next);
})
}()
})
})
}
copy('1.txt', '2.txt');

目录操作 & 递归创建/删除目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs');
fs.mkdir('a/b', (err) => {})
// 当创建目录的时候必须要求父目录是存在的
// 判断是一个文件或目录是否存在: fs.exists
fs.access('a', fs.constants.R_OK, (err) => { console.log(err) })

// 结合上述两方法实现递归创建目录
function mkdirp(dir) {
let paths = dir.split('/');
!function next(index) {
if (index == paths.length) return;
let current = paths.slice(0, index).join('/');
fs.access(current, fs.constants.R_OK, (err) => {
err
? fs.mkdir(current, 0o666, () => next(index + 1))
: next(index + 1)
})
}(1)
}
mkdirp('a/b/c');

递归同步删除非空目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* APIs
* 获取一个目录下的所有文件或目录 fs.readdir();
* 删除一个文件 fs.unlink(path);
* 删除一个空目录 fs.rmdir();
*/
const fs = require('fs');
function rmdirp(dir) {
const files = fs.readdirSync(dir);
files.forEach(file => {
// 判断当前 file 是目录还是文件
const current = dir + '/' + file;
const child = fs.statSync(current);
if (child.isDirectory()) {
rmdirp(current);
} else {
fs.unlinkSync(current);
}
})
// 把一个目录下所有的文件/目录全部删除后, 要删除自己
fs.rmdirSync(dir);
}
rmdirp('a');

递归删除非空文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 同步
const fs = require('fs');
const path = require('path');
function rmdirSync(dir) {
// 读取当前文件下所有子文件夹
const files = fs.readdirSync(dir);
files.forEach(item => {
const curPath = path.join(dir, item);
// 获取完整的子文件夹路径
const child = fs.statSync(curPath);
// 判断是否为目录(为 true 表示文件夹, 需要递归删除, 否则是个文件, 直接删除)
if (child.isDirectory()) {
rmdirSync(curPath);
} else {
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(dir);
}
rmdirSync('a');

// 异步
// 思路: 先删除当前目录的子文件夹或文件, 再删除自己
function rmdir(dir) {
return new Promise((resolve, reject) => {
fs.stat(dir, (err, stat) => {
if (err) { return reject(err); }
if (stat.isDirectory()) {
fs.readdir(dir, (err, files) => {
if (err) { return reject(err); }
Promise
.all(files.map(file => rmdir(path.join(dir, file))))
.then(() => { fs.rmdir(dir, resolve) });
});
} else {
fs.unlink(dir, resolve);
}
});
});
}
rmdir('a');

读取目录下所有文件

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');
const path = require('path');
fs.readdir('./a', (err, files) => {
files.forEach(file => {
const child = path.join('a', file);
// 获取详细信息
fs.stat(child, (err, stat) => {
console.log(stat)
})
});
})

遍历算法

目录是一个树状结构, 在遍历时一般使用深度优先 + 先序遍历算法; 深度优先, 意味着到达一个节点后, 首先遍历子节点而不是相邻节点; 先序遍历, 意味着首次到达了某节点就算遍历完成, 而不是最后一次返回某节点才算数

order

深度优先 + 先序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const fs = require('fs');
const path = require('path');
// 同步
function deepSync(dir) {
fs.readdirSync(dir).forEach(file => {
const child = path.join(dir, file);
const stat = fs.statSync(child);
if (stat.isDirectory()) {
deepSync(child);
} else {
console.log(child);
}
});
}

// 异步
// cb 的两个作用: 执行完毕时的操作; 给自执行函数传值
function deep(dir, cb) {
fs.reddir(dir, (err, files) => {
!function next(i) {
if (i >= files.length) {
return cb();
}
const child = path.join(dir, files[i]);
fs.stat(child, (err, stat) => {
if (stat.isDirectory()) {
deep(child, () => next(i + 1));
} else {
next(i + 1);
}
});
}(0);
})
}
deep('file', () => { console.log('done') })

广度优先 + 先序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const fs = require('fs');
const path = require('path');
// 同步
function wideSync(dir) {
const arr = [dir];
while (arr.length > 0) {
// 取出队列最左边的元素
const current = arr.shift();
const stat = fs.statSync(current);
if (stat.isDirectory()) {
const files = fs.readdirSync(current);
files.forEach(item => {
arr.push(path.join(current, item));
})
}
}
}

// 异步
function wide(dir) {
const arr = [dir];
while(arr.length > 0) {
const current = arr.shift(); // 取出队列最左边的元素
const stat = fs.statSync(current);
if (stat.isDirectory()) {
const files = fs.readdirSync(current);
files.forEach(item => {
arr.push(path.join(current, item));
})
}
}
}

监视文件/目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 语法: fs.watchFile(filename[, options], listener)
// 作用: 监控监视文件变化, 当文件发生变化之后执行对应的回调函数

const fs = require('fs');
fs.watchFile('1.txt', (newStat, prevStat) => {
// prevStat 修改前的状态; newStat 修改后的状态
// parse() 方法可解析一个日期时间字符串, 并返回格林威治时间到该时间的毫秒数
if (Date.parse(newStat.ctime) == 0) {
console.log('创建');
} else if (Date.parse(newStat.ctime) == 0) {
console.log('删除');
} else if (Date.parse(newStat.ctime) != Date.parse(prevStat.ctime)) {
console.log('修改');
}
})

path 路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const path = require('path');

// 连接两个目录
path.join('a', 'b'); // a/b

// 从当前路径出发, 解析出一个绝对路径
// .. 代表上一级目录
// . 代表当前目录
path.resolve('..', '.', 'a');

// 环境变量路径分割符
// 因为在不同的操作系统, 分隔符不一样: window ; || mac & linux :
path.delimiter;

// 文件路径分隔符
path.sep;

// 获得两个路径之间的相对路径
path.relative;

// 获取文件名
path.basename('a.jpg'); // a.jpg
path.basename('a.jpg', '.jpg'); // a

// 获取文件扩展名
path.extname('a.jpg'); // .jpg