events
Node是事件驱动的,事件模型采用了发布订阅设计模式,EventListener、Vue2 evnetBus都是这种模式
发布订阅模式有三个角色参与
- 发布者(Publisher):发布消息,制定消息的主题名
- 订阅者(Subscriber):通过消息主题订阅消息,先订阅再等待发布消息,否则会错过消息
- 调度中心(Broker):维护一个消息列表,并提供发布和订阅消息的方法,通过消息主题将发送者和接收者连接起来,四个基本的方法:on、once、off、emit
发布者和订阅者之间完全解耦,不再直接依赖于彼此,可以独立地扩展自己,消息的传递则依靠调度中心。发布者不会将消息直接发送给订阅者,而是通过调度中心将消息和其主题广播出去,订阅了该主题则可以接收到消息
实现一个简单的发布订阅:
// 订阅方法interface EventFun { (...args: any[]): any}interface EventCls { // 订阅消息 on(name: string, callback: EventFun): void // 发布消息 emit(name: string, ...args: any[]): void // 取消订阅 off(name: string, fn: EventFun): void // 只订阅一次 once(name: string, fn: EventFun): void}type CallbackArr = Array<EventFun>// 保存所有消息interface EventList { // 消息名:订阅的方法集合 [key: string]: CallbackArr,}class SubPub implements EventCls { list: EventList constructor() { this.list = {} } on(name: string, callback: EventFun) { const callbackList: CallbackArr = this.list[name] || []; callbackList.push(callback) this.list[name] = callbackList } emit(name: string, ...args: any[]) { const callbackList: CallbackArr = this.list[name] if (callbackList) { if (callbackList.length <= 0) { console.warn("该消息没有订阅者") return; } callbackList.forEach(callback => { callback.apply(this, args) }) } else { console.warn("没有该消息") } } off(name: string, fn: EventFun) { const callbackList: CallbackArr = this.list[name] if (callbackList) { if (callbackList.length <= 0) { console.warn("该消息没有订阅者") return; } const index = callbackList.findIndex(fns => fns === fn) index > -1 ? callbackList.splice(index, 1) : null } else { console.warn("没有该消息") } } once(name: string, fn: EventFun) { const decor: EventFun = (...args) => { fn.apply(this, args) this.off(name, decor) } this.on(name, decor) }}测试:
const subPub = new SubPub()// 测试on和offsubPub.emit('abc', 678) // 没有该消息const fn: EventFun = (...arg) => { console.log(arg);}subPub.on('abc', fn)subPub.emit('abc', 131, true) // [ 131, true ]subPub.emit('abc', 678, false, 'qx') // [ 678, false, 'qx' ]subPub.off('abc', fn)subPub.emit('abc', 321, 'qx') // 该消息没有订阅者console.log("=======================");// 测试oncesubPub.emit('a', 678) // 没有该消息subPub.once('a', (...arg) => { console.log(arg);})subPub.emit('a', 678, 'abc') // [ 678, 'abc' ]subPub.emit('a', 123, 'qx') // 该消息没有订阅者subPub.on('a', (...arg) => { console.log(arg);})subPub.emit('a', 123, 'qx') // [ 123, 'qx' ]EventEmitter
Node内置的 events 模块提供了 EventEmitter 类用于处理事件的发布与订阅
Node中许多类都继承自它,比如 Process、Stream、HTTP 等,这些类都提供了事件处理机制,允许注册监听器以响应特定的事件,从而构建异步、事件驱动的程序
在Node中,消息<->事件,订阅消息<->监听事件,发布消息<->触发事件
常用方法:
on(event, listener)为指定事件添加一个监听器到监听器数组的尾部addListener(event, listener)与on等效once(event, listener)为指定事件注册一个单次监听器到监听器数组的尾部,该监听器触发后立刻被移除emit(event, ...argv)触发事件,传递参数,如果事件有注册监听返回 true,否则返回 false。off(event, listener)移除指定事件的某个监听器,在监听器数组中从后往前找removeListener(event, listener)与off等效removeAllListeners([event])移除所有事件(或指定事件)的所有监听器setMaxListeners(n)设置单个事件最大监听器数量,默认10,超过将发出警告,有助于排查内存泄漏,设置为 Infinity(或 0)以表示无限数量listeners(event)返回指定事件的所有监听器组成的数组rawListeners(event)与listeners差不多,但会将once注册的监听器标记出来listenerCount(event)返回指定事件的监听器数量eventNames()返回事件名列表数组prependListener(event, listener)为指定事件添加一个监听器到监听器数组的头部prependOnceListener(event, listener)为指定事件注册一个单次监听器到监听器数组的头部
const EventEmitter = require('events')const event = new EventEmitter()const listener = (...args) => { console.log(args)}// 监听事件event.on('test', listener)event.once('test', listener)console.log(event.eventNames()) // [ 'test' ]console.log(event.listeners('test'))// [ [Function: listener], [Function: listener] ]console.log(event.rawListeners('test'))// [// [Function: listener],// [Function: bound onceWrapper] { listener: [Function: listener] }// ]console.log(event.listenerCount('test')) // 2// 触发事件,传递信息event.emit('test', 1, 2, 3, 4, 5)event.emit('test', 1, 2, 3, 4, 5)// [ 1, 2, 3, 4, 5 ] 两次on一次once// [ 1, 2, 3, 4, 5 ]// [ 1, 2, 3, 4, 5 ]默认情况下,每个事件最多注册 10 个监听器,超过 10 个监听器会发出警告
for (let i = 0; i < 20; i++) { event.on('test', () => { console.log(`test${i}`) })}event.emit('test')// (node:40112) MaxListenersExceededWarning:// Possible EventEmitter memory leak detected.// 11 test listeners added to [EventEmitter].// Use emitter.setMaxListeners() to increase limit使用 setMaxListeners(num) 设置单个事件最大监听器数量
event.setMaxListeners(20)错误事件
当 EventEmitter 实例中发生错误时,会触发 error 事件
如果没有为 error 事件注册监听器,则会抛出错误,打印堆栈跟踪,然后 Node 进程退出
最佳实践:应始终为 error 事件添加监听器
event.on('error', (err)=>{ console.log('message:', err.message) // message: 错误信息})event.emit('error', new Error('错误信息'));使用 events.errorMonitor 安装监听器,可以在不消费触发的错误的情况下监视 error 事件(错误会穿透监听)
const { errorMonitor } = require('events')event.on(errorMonitor, (err) => { console.log('message:', err.message) // message: 错误信息})event.emit('error', new Error('错误信息'));// Error: 错误信息// ......// Emitted 'error' event at:// ......new/removeListener事件
newListener 当有新的监听器将要注册,触发该事件 removeListener 当有监听器被移除后,触发该事件
event.on('newListener', (event, listener) => { console.log(event, listener) // removeListener [Function (anonymous)] // test [Function: listener]})event.on('removeListener', (event, listener) => { console.log(event, listener) // test [Function: listener]})const listener = () => { }event.on('test', listener)event.off('test', listener)process底层
function setupProcessObject() { const EventEmitter = require('events'); const origProcProto = ObjectGetPrototypeOf(process); ObjectSetPrototypeOf(origProcProto, EventEmitter.prototype); FunctionPrototypeCall(EventEmitter, process); ObjectDefineProperty(process, SymbolToStringTag, { __proto__: null, enumerable: false, writable: true, configurable: false, value: 'process', });
// Create global.process as getters so that we have a // deprecation path for these in ES Modules. // See https://github.com/nodejs/node/pull/26334. let _process = process; ObjectDefineProperty(globalThis, 'process', { __proto__: null, get() { return _process; }, set(value) { _process = value; }, enumerable: false, configurable: true, });}ObjectGetPrototypeOf() 即 Object.getPrototypeOf(),获取某个对象原型上的属性
在源码中,通过该API获取了process的原型
class A {}A.prototype.name = 'A';const a = new A();console.log(Object.getPrototypeOf(a));// { name: 'A' }console.log(a.__proto__);// { name: 'A' }ObjectSetPrototypeOf() 即 Object.setPrototypeOf(),用于设置对象的原型
在源码中,将 EventEmitter 的原型对象设置为 process 的原型的原型,让 process 也能使用 events 的一些方法
class A {}A.prototype.name = 'A';const a = new A();Object.setPrototypeOf(a, { name: 'B' });console.log(Object.getPrototypeOf(a));// { name: 'B' }FunctionPrototypeCall() 即 Function.prototype.call(),改变 this 指向并调用函数
在源码中,调用 EventEmitter 构造函数,使 process 也拥有 EventEmitter 的功能
function A() { this.name = 'A'; this.a = 1;}
function B() { this.name = 'B'; this.age = 18;}
// 创建 A 的实例,并使用 call 将当前实例作为上下文传递给 B 的构造函数,使 B 的构造函数内的 this 指向 a 实例对象const a= new A();B.call(a);
console.log(a.a); // 输出: 1console.log(a.name); // 输出: Bconsole.log(a.age); // 输出: 18通过 FunctionPrototypeCall() 和 ObjectSetPrototypeOf(),使 process 继承自 EventEmitter
最后,通过 ObjectDefineProperty() 将process挂载到全局变量上
util
util 提供了很多实用的、工具类型的API,方便快速开发
类型判断
util.types 上有很多 is***() 的方法用于判断类型,返回 boolean
与 instanceof 不同,util.types 不会受到原型链的影响
instanceof 运算符用于检查一个对象是否是某个构造函数的实例。具体而言,它检查一个对象的原型链中是否出现了指定构造函数的原型。
console.log(util.types.isDate(new Date())) // trueconsole.log(util.types.isPromise(new Promise(() => { }))) // true
const date = new Date()console.log(date instanceof Date) // trueObject.setPrototypeOf(date, {})console.log(date instanceof Date) // falseconsole.log(util.types.isDate(date)) // truepromisify
promisify(fn) 用于将回调函数的模式转为promise模式
原回调函数的参数除err外,都作为对象的属性返回
const execPromise = util.promisify(childProcess.exec)execPromise('node -v') .then(({ stdout, stderr }) => { // 原回调函数的参数除err外,都作为对象的属性返回 console.log(stdout) // v21.2.0 }) .catch(err => { console.log(err) })手写 promisify
const promisify = (fn) => { // 返回一个函数,参数与原函数相同 return (...args) => { // 返回一个 Promise return new Promise((resolve, reject) => { // 执行原函数,传入原函数的参数,最后一个参数为回调函数 fn(...args, (err, ...values) => { if (err) { reject(err) } else { resolve(...values) } }) }) }}
const execPromise = promisify(childProcess.exec)execPromise('node -v') .then((stdout, stderr) => { console.log(stdout) // v21.2.0 }) .catch(err => { console.log(err) })自己写 promisify 是无法获取到 key 名(原参数名)的,也就不能像 util.promisify 那样resolve一个对象
在底层通过 kCustomPromisifyArgsSymbol 获取 key 名,但该 API 仅Node内部使用
callbackify
callbackify(fn) 将promise模式转为回调函数的模式
const fn = (type) => { return new Promise((resolve, reject) => { if (type === 'error') { reject(new Error('error')) } resolve(type) })}const newFn = util.callbackify(fn)newFn('test', (err, value) => { if (err) { console.log(err) return; } console.log(value) // test})手写 callbackify
const fn = (type) => { return new Promise((resolve, reject) => { if (type === 'error') { reject(new Error('error')) } resolve(type) })}// const newFn = util.callbackify(fn)const callbackify = (fn) => { return (...args) => { // 将回调函数取出 const callback = args.pop() fn(...args) .then(value => { callback(null, value) }) .catch(err => { callback(err) }) }}const newFn = callbackify(fn)newFn('test', (err, value) => { if (err) { console.log(err) return; } console.log(value) // test})inspect
inspect 将对象转换为字符串,通常用于调试和错误输出
util.inspect(object[, options])
options配置项:
1. showHidden <boolean> 如果值为 true,则 object 的不可枚举符号和属性包含在格式化的结果中。WeakMap 和 WeakSet 条目以及用户定义的原型属性(不包括方法属性)也包括在内。默认值:false。2. depth <number> 表示最大递归的层数,如果对象很复杂,可以指定层数以控制输出对象的深度。如果不指定depth,默认会递归 2 层,指定为 null 表示将不限递归层数完整遍历对象(递归到最大调用堆栈大小)。3. color <boolean> 如果为 true,输出格式将会以 ANSI 颜色编码,通常用于在终端显示更漂亮 的效果。[自定义 inspect 颜色](https://nodejs.cn/api/util.html#customizing-utilinspect-colors)4. breakLength <integer> 输入值在多行中拆分的长度。 设置为 Infinity 以将输入格式化为单行(结合 compact 设置为 true 或任何数字 >= 1)。 默认值: 80。5. showProxy <boolean> 如果 true,Proxy 检查包括 target 和 handler 对象。默认值:false。6. compact <boolean> 如果为 true,则输出将尽可能紧凑,例如在数组和对象周围省略空格。默认值:false。7. sorted <boolean> 如果为 true,则输出的对象属性将按属性名排序。默认值:false。8. getters <boolean> 如果为 true,则输出将包括对象的 getter 函数的返回值。默认值:false。9. maxArrayLength <integer> 指定要格式化的数组的最大长度 设置为 Infinity 可以完整遍历数组。默认值:100。10. maxStringLength <integer> 指定要格式化的字符串的最大长度 设置为 Infinity 可以完整遍历字符串。默认值:100。11. breakLength <integer> 输入值在多行中拆分的长度。设置为 Infinity 以将输入格式化为单行(结合 compact 设置为 true 或任何数字 >= 1)。默认值:80。const obj = { name: 'qx', a: { b: { c: { d: 1 } } }, arr: [1, 2, 3, 4, 5]}console.log(util.inspect(obj, { depth: 2, colors: true, showHidden: true, compact: true, maxArrayLength: 3, showProxy: false, sorted: false,}))// { name: 'qx',// a: { b: { c: [Object] } },// arr: [ 1, 2, 3, ... 2 more items, [length]: 5 ] }format
类似C中的 printf 的格式字符串
使用第一个参数作为类似 printf 的格式字符串(其可以包含零个或多个格式说明符)来返回格式化的字符串。每个说明符都替换为来自相应参数的转换后的值
util.format(format, ...args)格式说明符:
1. `%s`: String 将用于转换除 BigInt、Object 和 -0 之外的所有值。 BigInt 值将用 n 表示,没有用户定义的 toString 函数的对象使用具有选项 { depth: 0, colors: false, compact: 3 } 的 util.inspect() 进行检查。2. `%d`: Number 将用于转换除 BigInt 和 Symbol 之外的所有值。3. `%i`: parseInt(value, 10) 用于除 BigInt 和 Symbol 之外的所有值。4. `%f`: parseFloat(value) 用于除 Symbol 之外的所有值。5. `%j`: JSON。 如果参数包含循环引用,则替换为字符串 '[Circular]'。6. `%o`: Object。 具有通用 JavaScript 对象格式的对象的字符串表示形式。 类似于具有选项 { showHidden: true, showProxy: true } 的 util.inspect()。 这将显示完整的对象,包括不可枚举的属性和代理。7. `%O`: Object。 具有通用 JavaScript 对象格式的对象的字符串表示形式。 类似于没有选项的 util.inspect()。 这将显示完整的对象,但不包括不可枚举的属性和代理。8. `%c`: CSS。 此说明符被忽略,将跳过任何传入的 CSS。9. `%%`: 单个百分号 ('%')。 这不消费参数。没有匹配到的格式说明符的参数将按空格分隔的列表形式附加到字符串中,每个未匹配的参数都使用 util.inspect() 转换为一个字符串
console.log(util.format('%s:%s', 'foo', 'bar', 'baz', { a: 1 }));// foo:bar baz { a: 1 }pngquant
pngquant 是一个用于压缩 PNG 图像文件的工具,基于 Median Cut 算法
三个参数:
--output-o输出文件路径--ext为输出文件名设置自定义后缀/扩展名--quality min-max设置压缩质量,0-100,值越大图片越大,效果越好--speed 1-11设置压缩速度,1最慢,11最快,可能会导致输出图像质量稍微降低,默认3--force-f强制覆盖已存在的输出文件--skip-if-larger仅当压缩后的文件比原始文件更小或者压缩后的文件比原始文件更大但是质量更好时才输出文件--strip去除所有的元数据,包括png文件头信息--verbose-v输出状态信息
封装函数:
const pngquant = (str) => { execFile('pngquant', str.split(" "), { cwd: __dirname }, (err, stdout) => { if (err) { console.log(err) return; } console.log("done") })}pngquant('./1.png --output ./2.png -f')// quality表示图片质量0-100值越大图片越大效果越好pngquant('./1.png --quality=82 -o ./3.png -f')// --speed=1: 最慢的速度,产生最高质量的输出图像。// --speed=11: 最快的速度,但可能导致输出图像质量稍微降低。pngquant('./1.png --speed=11 --quality=80 -o ./4.png -f')// --ext 为输出文件名设置自定义后缀/扩展名pngquant('./1.png --ext new.png -f -v') // 1new.pngfs
fs 文件系统模块,提供了与文件系统进行交互的能力
所有文件系统操作方法都具有同步、异步回调和基于 promise 的形式
绝大部分方法都能传入options配置项,用于指定编码格式、文件模式等,如果 options 是字符串,则指定编码格式(encoding)
默认返回Buffer,可以通过指定编码格式获取字符串
const fs = require('fs')const fsPromises = require('fs/promises') // 引入promise版本const path = require('path')// 异步fs.readFile(path.resolve(__dirname, './1.txt'), (err, data) => { console.log(data) // <Buffer 31 32 33 34 35 36 0d 0a 36 35 34 33 32 31>})// 同步,且指定编码格式获取字符串const data = fs.readFileSync(path.resolve(__dirname, './1.txt'), 'utf-8')console.log(data) // 123456// promisefsPromises.readFile(path.resolve(__dirname, './1.txt')).then(data => { // 手动将buffer转换为字符串 console.log(data.toString()) // 123456}).catch(err => {})线程池使用
所有基于回调和 promise 的文件系统 API(fs.FSWatcher() 除外)都使用 libuv 的线程池,在事件循环线程之外执行文件系统操作。这些操作不是同步的也不是线程安全的。对同一文件执行多个并发修改时必须小心,否则可能会损坏数据。
flag文件系统标志
以下标志在 flag 选项接受字符串的任何地方都可以使用
a: 打开文件进行追加。如果文件不存在,则创建该文件。ax: 类似于 'a' 但如果路径存在则失败。a+: 打开文件进行读取和追加。如果文件不存在,则创建该文件。ax+: 类似于 'a+' 但如果路径存在则失败。as: 以同步模式打开文件进行追加。如果文件不存在,则创建该文件。as+: 以同步模式打开文件进行读取和追加。如果文件不存在,则创建该文件。r: 打开文件进行读取。如果文件不存在,则会发生异常。rs: 打开文件以同步模式读取。如果文件不存在,则会发生异常。r+: 打开文件进行读写。如果文件不存在,则会发生异常。rs+: 以同步模式打开文件进行读写。指示操作系统绕过本地文件系统缓存。这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。这不会将 fs.open() 或 fsPromises.open() 变成同步阻塞调用。如果需要同步操作,应该使用类似 fs.openSync() 的东西。w: 打开文件进行写入。创建(如果它不存在)或截断(如果它存在)该文件。wx: 类似于 'w' 但如果路径存在则失败。w+: 打开文件进行读写。创建(如果它不存在)或截断(如果它存在)该文件。wx+: 类似于 'w+' 但如果路径存在则失败。
简单记忆:
- r:读取
- w:写入
- s:同步
- +:增加相反操作
- x:排他方式
fd文件描述符
在 POSIX 系统上,对于每个进程,内核维护一个当前打开的文件和资源表。每个打开的文件都分配有一个简单的数字标识符,称为文件描述符。在系统级,所有文件系统操作都使用这些文件描述符来识别和跟踪每个特定文件。Windows 系统使用不同但概念上相似的机制来跟踪资源。为了方便用户,Node.js 抽象了操作系统之间的差异,并为所有打开的文件分配了一个数字文件描述符。
在 NodeJS 中,每操作一个文件,文件描述符是递增的,文件描述符一般从 3 开始,因为前面有 0、1、2三个比较特殊的描述符,分别代表 stdin(标准输入)、stdout(标准输出)和 stderr(错误输出)。
基于回调的 fs.open() 和同步 fs.openSync() 方法打开一个文件并分配一个新的文件描述符。分配后,文件描述符可用于从文件读取数据、向文件写入数据或请求有关文件的信息。
fs.open(path.resolve(__dirname, './1.txt'), 'r', (err, fd) => { console.log(fd) // 3,数字文件描述符 // 将文件描述符传入,获取文件信息 fs.fstat(fd, (err, stat) => { console.log(stat) // 文件信息 }) // 关闭文件 fs.close(fd, (err) => { console.log('关闭成功') })})操作系统限制在任何给定时间可能打开的文件描述符的数量,因此在操作完成时关闭描述符至关重要。否则将导致内存泄漏,最终导致应用崩溃。
fsPromises
基于 promise 的操作会返回一个当异步操作完成时被履行的 promise。
通常不直接使用 fsPromises 操作文件,而是用 fsPromises.open() 创建一个 FileHandle(数字文件描述符的封装) 对象,每个描述符都会绑定到一个特定的文件。然后使用 FileHandle 执行文件操作。
使用 FileHandle 好处:
- FileHandle 对象可以在多个操作之间共享,从而避免了在每个操作中打开和关闭文件的开销。
- 可以精确地控制文件的打开和关闭。
- 一些文件操作可能在底层实现上更为高效。
- 封装、代替数字文件描述符。FileHandle对象由系统更好地管理,以确保资源不泄漏。但仍然应该显示调用 close() 方法来关闭 FileHandle 对象,以便释放系统资源。当 FileHandle 已关闭且不再可用时,会触发 close 事件。
const fd = await fsPromises.open(path.resolve(__dirname, './1.txt'), 'r')const data = await fd.readFile('utf-8')console.log(data) // 123456await fd.close();access
access(path[, mode], callback) 测试用户对 path 指定的文件或目录的权限。
mode 值为文件访问常量,指定要执行的可访问性检查。默认值:fs.constants.F_OK
- F_OK 指示文件对调用进程可见的标志。用于确定文件是否存在,但没有说明 rwx 权限。
- R_OK 指示文件可以被调用进程读取的标志。
- W_OK 指示文件可以被调用进程写入的标志。
- X_OK 指示文件可以被调用进程执行的标志。这对 Windows 没有影响(将表现得像 fs.constants.F_OK)。
const file = path.resolve(__dirname, './1.txt')fs.access(file, fs.constants.F_OK, (err) => { console.log(`${file} ${err ? 'does not exist' : 'exists'}`);});fs.access(file, fs.constants.R_OK, (err) => { console.log(`${file} ${err ? 'is not readable' : 'is readable'}`);});fs.access(file, fs.constants.W_OK, (err) => { console.log(`${file} ${err ? 'is not writable' : 'is writable'}`);});fs.access(file, fs.constants.R_OK | fs.constants.W_OK, (err) => { console.log(`${file} ${err ? 'is not' : 'is'} readable and writable`);});// c:\chuckle\qx\NodeJS-new\fs\1.txt exists// c:\chuckle\qx\NodeJS-new\fs\1.txt is readable// c:\chuckle\qx\NodeJS-new\fs\1.txt is writable// c:\chuckle\qx\NodeJS-new\fs\1.txt is readable and writable在调用 fs.open()、fs.readFile() 或 fs.writeFile() 等文件操作之前,不要使用 fs.access() 检查文件的可访问性。这样做会引入竞争条件,因为其他进程可能会在两次调用之间更改文件的状态。而是,用户代码应直接打开/读取/写入文件,并处理无法访问文件时引发的错误。
readFile文件读取
readFile用于读取文件内容
const data = fs.readFileSync(path.resolve(__dirname, './1.txt'), 'utf-8')console.log(data) // 123456fs.readFile(path.resolve(__dirname, './1.txt'), 'utf-8', (err, data) => { console.log(data) // 123456})fsPromises.readFile(path.resolve(__dirname, './1.txt'), 'utf-8') .then(data => { console.log(data) // 123456 })createReadStream
createReadStream(path[, options]) 创建可读流读取文件,返回一个可读流对象
适合读取大文件,因为不会一次性将文件读取到内存中,而是分块读取
1、flags (标志) <string>:意义:指定文件系统标志。它定义了打开文件的行为,比如只读、只写、追加等。默认值:'r',表示只读。
2、encoding (编码) <string>:意义:指定用于解码文件数据的字符编码。如果未提供,则返回原始的 Buffer 数据。默认值:null,表示使用原始的 Buffer 数据。
3、fd (文件描述符) <integer> | <FileHandle>:意义:提供一个现有的文件描述符或 FileHandle 对象,用于打开文件。默认值:null,表示通过文件路径打开。
4、mode (权限掩码) <integer>:意义:指定文件的权限掩码(权限位)。用于在使用 fd 参数时设置文件权限。默认值:0o666,表示八进制权限掩码。
5、autoClose (自动关闭) <boolean>:意义:指定是否在流结束时自动关闭文件描述符。默认值:true,表示流结束时自动关闭文件。
6、emitClose (触发关闭事件) <boolean>:意义:指定是否在文件关闭时触发 'close' 事件。默认值:true,表示触发 'close' 事件。
7、start (起始位置) <integer>:意义:指定从文件的哪个位置开始读取。默认值:未指定,从文件开头开始读取。
8、end (结束位置) <integer>:意义:指定读取到文件的哪个位置为止。读取将在达到此位置时停止。默认值:Infinity,表示读取整个文件。
9、highWaterMark (高水位标记) <integer>:意义:指定每次读取的最大字节数。当内部缓冲区的数据低于此值时,将继续读取。默认值:64 * 1024,表示每次最多读取 64 KB。
10、fs (文件系统) <Object> | <null>:意义:提供一个自定义的文件系统对象。可以用于替代 Node.js 的默认文件系统模块。默认值:null,使用 Node.js 的默认文件系统模块。
11、signal (中止信号) <AbortSignal> | <null>:意义:指定一个 AbortSignal 对象,用于中止文件读取操作。默认值:null,表示不使用中止信号。const readStream = fs.createReadStream(path.resolve(__dirname, './1.txt'), 'utf-8')readStream.on('data', (data) => { console.log(data) // 123456})readStream
可读流对象 ReadStream 是使用 fs.createReadStream() 创建的
ReadStream 继承自 Readable,因此它具有所有可读流的方法和事件
事件:
open当文件被打开时触发close当文件被关闭时触发ready当底层资源(比如文件描述符)被分配时触发data当有数据可读时触发end当没有更多的数据可读时触发error当在接收和写入数据的过程中发生错误时触发pause当调用 stream.pause() 时触发,暂停读取数据resume当调用 stream.resume() 时触发,恢复读取数据readable当有数据可读时触发,必须显式调用stream.read()方法来从流中读取数据片段。
属性:
bytesRead已读取的字节数path文件路径或文件描述符pending读取操作是否正在等待底层资源(比如文件描述符),如果底层文件尚未打开,即在触发 'ready' 事件之前,则此属性为 true。
const readStream = fs.createReadStream(path.resolve(__dirname, './1.txt'), 'utf-8')console.log(readStream.path) // c:\chuckle\qx\NodeJS-new\fs\1.txtconsole.log(readStream.bytesRead) // 0console.log(readStream.pending) // true// 监听读取数据readStream.on('data', (data) => { console.log(readStream.bytesRead) // 14 console.log(readStream.pending) // false console.log(data) // 123456})// 监听读取完成readStream.on('end', () => { console.log('读取完成')})readStream.on('pause', () => { console.log('暂停读取')}) // 监听暂停事件readStream.pause() // 暂停读取readStream.resume() // 恢复读取readStream.on('resume', () => { console.log('恢复读取')}) // 监听恢复事件writeFile文件写入
writeFile(file, data[, options], (err)=>{}) 将 data 写入到文件指定的 file 中,如果文件已存在则替换该文件
1. encoding <string>:意义:指定写入文件的字符编码。默认值:'utf8'。
1. mode <integer>:意义:指定文件的权限掩码(权限位)。不会应用于已存在的文件。默认值:0o666。
1. flag <string>:意义:指定用于打开文件的标志。默认值:'w'。
1. signal <AbortSignal>:意义:指定一个 AbortSignal 对象,用于中止写入操作。默认值: null。
1. flush <integer>:意义:如果所有数据都成功写入文件,并且 flush 是 true,则使用 fs.fsync() 来刷新数据。默认值: false。fs.writeFile(path.resolve(__dirname, './1.txt'), '123456', (err) => { console.log('写入成功')})修改flag为a,表示追加写入
fs.writeFile(path.resolve(__dirname, './2.txt'), "123456", { flag: 'a'}, (err) => { console.log('追加写入成功')})appendFile追加写入
appendFile(path, data[, options], (err)=>{}) 异步地将数据追加到文件,如果该文件尚不存在,则创建该文件
1. encoding <string>:意义:指定写入文件的字符编码。默认值:'utf8'。
2. mode <integer>:意义:指定文件的权限掩码(权限位)。不会应用于已存在的文件。默认值:0o666。
3. flag <string>:意义:指定用于打开文件的标志。默认值:'a'。
4. flush <AbortSignal>:意义:如果是 true,则在关闭基础文件描述符之前将其刷新。默认值: false。fs.appendFile(path.resolve(__dirname, './1.txt'), '123456', (err) => { console.log('追加写入成功')})createWriteStream
createWriteStream(path[, options]) 创建一个可写流写入文件,返回一个可写流对象
适合大内容的写入,因为不会一次性将内容写入文件,而是分块写入
1、flags (标志) <string>:意义:指定文件系统标志。它定义了打开文件的行为,比如只读、只写、追加等。默认值:'w',表示只写。
2、encoding (编码) <string>:意义:指定用于解码文件数据的字符编码。如果未提供,则返回原始的 Buffer 数据。默认值:null,表示使用原始的 Buffer 数据。
3、fd (文件描述符) <integer> | <FileHandle>:意义:提供一个现有的文件描述符或 FileHandle 对象,用于打开文件。默认值:null,表示通过文件路径打开。
4、mode (权限掩码) <integer>:意义:指定文件的权限掩码(权限位)。用于在使用 fd 参数时设置文件权限。默认值:0o666,表示八进制权限掩码。
5、autoClose (自动关闭) <boolean>:意义:指定是否在流结束时自动关闭文件描述符。默认值:true,表示流结束时自动关闭文件。
6、emitClose (触发关闭事件) <boolean>:意义:指定是否在文件关闭时触发 'close' 事件。默认值:true,表示触发 'close' 事件。
7、start (起始位置) <integer>:意义:指定从文件的哪个位置开始写入。默认值:未指定,从文件末尾开始写入。
8、signal (中止信号) <AbortSignal> | <null>:意义:指定一个 AbortSignal 对象,用于中止文件写入操作。默认值:null,表示不使用中止信号。
9、highWaterMark (高水位标记) <number>:意义:指定每次写入的最大字节数。当内部缓冲区的数据低于此值时,将继续写入。默认值:16 * 1024,表示每次最多写入 16 KB。
10、flush (刷新) <boolean>:意义:如果是 true,则在关闭基础文件描述符之前将其刷新默认值:false,表示不刷新。const writeStream = fs.createWriteStream(path.resolve(__dirname, './1.txt'), 'utf-8')writeStream.write('123456', (err) => { })writeStream.end();writeStream.close();writeStream
可写流对象 WriteStream 是使用 fs.createWriteStream() 创建的
WriteStream 继承自 Writable,因此它具有所有可写流的方法和事件
事件:
open当文件被打开时触发close当文件被关闭时触发ready当底层资源(比如文件描述符)被分配时触发drain当 write() 方法返回 false 时触发error当在接收和写入数据的过程中发生错误时触发finish在调用 stream.end() 之后,而且缓冲区数据都已经传给底层系统之后触发。pipe当调用 stream.pipe() 时触发unpipe当调用 stream.unpipe() 时触发
属性:
bytesWritten到目前为止写入的字节数。不包括仍在排队等待写入的数据。close(err=>{})关闭 writeStream。触发close事件。write(chunk[, encoding][, callback])写入数据,返回boolean表示是否可以继续写入。返回false需邓艾drain事件触发后再继续写入。end([chunk[, encoding]][, callback])结束写入,之后不可再写入。触发finish事件path流正在写入的文件的路径,即 fs.createWriteStream() 的第一个参数。pending写入操作是否正在等待底层资源(比如文件描述符),如果底层文件尚未打开,即在触发 'ready' 事件之前,则此属性为 true。
const writeStream = fs.createWriteStream(path.resolve(__dirname, './1.txt'), 'utf-8')writeStream.on('open', () => { console.log('写入流打开')})writeStream.on('ready', () => { console.log('准备写入')})// 监听写入完成writeStream.on('finish', () => { console.log('写入完成')})writeStream.on('close', () => { console.log('写入流关闭') writeStream.destroy();})// 写入数据writeStream.write('123456', (err) => { console.log(writeStream.bytesWritten) // 6 writeStream.close();})writeStream.end(); // 结束写入,表示不再有数据写入(end之后不能调用write)drain事件
可以连续调用 writeStream.write() 向流中写入数据,但缓冲区是有限的,当缓冲区满时,writeStream.write() 将返回 false,表示不应再写入数据,直到触发 drain 事件,表示缓冲区已清空,可以继续写入数据
const writableStream = fs.createWriteStream('example.txt', { highWaterMark: 30 // 指定缓冲区大小为 30 字节});// 要写入的数据数组const dataToWrite = [ '待到秋来九月八 ', '我花开后百花杀 ', '冲天香阵透长安 ', '满城尽带黄金甲 '];// 递归写入数据的函数function writeDataArray(dataArray, index) { if (index < dataArray.length) { // 当前数据 const currentData = dataArray[index]; console.log(`正在写入数据: ${currentData}`); // 尝试写入数据 if (!writableStream.write(currentData)) { console.log('缓冲区已满,后续写入需等待本次写入完成...'); // 在 drain 事件触发后继续写入下一条数据 writableStream.once('drain', () => { console.log(`成功写入数据: ${currentData}`); console.log('缓冲区已空,可以继续写入...'); writeDataArray(dataArray, index + 1); }); } else { console.log(`成功写入数据: ${currentData}`); // 数据已经完全写入,递归调用写入下一条数据 writeDataArray(dataArray, index + 1); } } else { // 所有数据都已写入,结束可写流 console.log('所有数据都已写入'); writableStream.end(); }}// 调用写入函数,从数组的第一条数据开始writeDataArray(dataToWrite, 0);正在写入数据: 待到秋来九月八缓冲区已满,后续写入需等待本次写入完成...成功写入数据: 待到秋来九月八缓冲区已空,可以继续写入...
正在写入数据: 我花开后百花杀缓冲区已满,后续写入需等待本次写入完成...成功写入数据: 我花开后百花杀缓冲区已空,可以继续写入...
正在写入数据: 冲天香阵透长安缓冲区已满,后续写入需等待本次写入完成...成功写入数据: 冲天香阵透长安缓冲区已空,可以继续写入...
正在写入数据: 满城尽带黄金甲缓冲区已满,后续写入需等待本次写入完成...成功写入数据: 满城尽带黄金甲缓冲区已空,可以继续写入...
所有数据都已写入创建文件
Node没有创建文件的方法,但可以通过open和writeFile方法创建文件
// 将flag设置为a,表示读取和追加,若文件不存在则创建fs.openSync(path.resolve(__dirname, './2.txt'), 'a+')fs.writeFileSync(path.resolve(__dirname, './3.txt'), "", { flag: 'a+'})mkdir创建目录
mkdir(path[, options], callback) 创建目录
1. recursive <boolean>:意义:指示是否应创建父目录(递归创建多级目录)。如果为 true,则缺少的目录将被创建。默认值:false。
2. mode <integer>:意义:设置目录的权限掩码(权限位)。不会应用于已存在的目录。Windows 上不支持默认值:0o777。fs.mkdir(path.resolve(__dirname, './test'), (err) => { console.log('创建成功')})fs.mkdir(path.resolve(__dirname, './a/b/c'), { recursive: true // 递归创建多级目录}, (err) => { console.log('创建成功')})rm删除文件或目录
rm(path[, options], callback) 删除文件或目录
1. force <boolean>:意义:当为 true 时,如果 path 不存在,则异常将被忽略默认值:false。
2. maxRetries <integer>:意义:如果遇到 EBUSY、EMFILE、ENFILE、ENOTEMPTY 或 EPERM 错误,Node.js 将在每次尝试时以 retryDelay 毫秒的线性退避等待时间重试该操作。如果为 0,则不会重试。如果 recursive 选项不为 true,则忽略此选项默认值:0。
3. recursive <boolean>:意义:指示是否应递归删除目录。如果为 false,则不会删除目录。默认值:false。
4. retryDelay <integer>:意义:重试之间等待的毫秒数。如果 recursive 选项不为 true,则忽略此选项。默认值:100。如果要删除目录,recursive 选项必须为 true
fs.rm(path.resolve(__dirname, './a.txt'), (err) => { console.log('删除成功')})fs.rm(path.resolve(__dirname, './a'), { recursive: true // 递归删除目录}, (err) => { console.log('删除成功')})rename文件重命名和移动
rename(oldPath, newPath, callback) 将 oldPath 处的文件重命名为作为 newPath 提供的路径名。如果 newPath 已经存在,则它将被覆盖。
fs.renameSync( path.resolve(__dirname, './2.txt'), // 重命名并移动文件,test文件夹需要存在 path.resolve(__dirname, './test/22.txt'))watch监视文件
watch(filename[, options][, listener]): <fs.FSWatcher>
fs.watch API 跨平台并非 100% 一致,并且在某些情况下不可用,详见:注意事项
在 Windows 上,如果监视目录被移动或重命名,则不会触发任何事件。 删除监视目录时报 EPERM 错误
1. persistent <boolean>:意义:指示只要正在监视文件,进程是否应继续运行。默认值:true。
2. recursive <boolean>:意义:指示是应监视所有子目录,还是仅监视当前目录。 这在指定目录时适用,并且仅适用于受支持的平台。默认值:false。
3. encoding <string> | <null>:意义:指定用于传递回调的文件名的字符编码。如果未指定,则返回原始的 Buffer。默认值:null。
4. signal <AbortSignal> | <null>:意义:指定一个 AbortSignal 对象,用于中止监视器。默认值:null。监视器回调接受eventType事件类型、filename文件名两个参数
const watcher = fs.watch(path.resolve(__dirname, './1.txt'), (eventType, filename) => { console.log(eventType, filename) // change 1.txt }) // 监听文件变化// watcher.on('change', (eventType, filename) => {// console.log(eventType, filename)// })watcher.on('close', () => { console.log('关闭watch')})// 修改文件,文件已存在会先替换再写入,触发两次change事件fs.writeFile(path.resolve(__dirname, './1.txt'), '123456', (err) => { console.log('写入成功') watcher.close() // 关闭监听})readdir读取目录
readdir(path[, options], (err, files)=>{}) 获取一个文件夹下所有文件名的数组
1. encoding <string>:意义:指定用于解码文件名的字符编码。默认值:'utf8'
2. withFileTypes <boolean>:意义:如果为 true,则将结果数组中的条目替换为 fs.Dirent 对象。默认值:false
3. recursive <boolean>:意义:指示是否应递归读取子目录。如果为 true,则返回的数组将包含子目录中的文件的名称。默认值:falsefs.readdir(path.resolve(__dirname, './'),{ recursive: true // 递归读取目录}, (err, files) => { console.log(files) // [ '1.txt', '2.txt', 'index.js', 'test', 'test\\a.txt' ]})fs.readdir(path.resolve(__dirname, './'),{ withFileTypes: true // 返回Dirent对象}, (err, files) => { console.log(files) // [ // Dirent { // name: '1.txt', // path: 'c:\\chuckle\\qx\\NodeJS-new\\fs', // [Symbol(type)]: 1 // } // ...... // ]})批量重命名文件,在文件名前加上0
fs.readdir(__dirname + '/rename', (err, data)=>{ data.forEach((item, index)=>{ let data = item.split('.'); let [num, suffix] = data; if(Number(num)<10){ num = '0' + num; } fs.renameSync(`${__dirname}/rename/${item}`, `${__dirname}/rename/${num}.${suffix}`); });});软/硬链接
链接实际上是一种文件共享的方式
linkSync(existingPath, newPath) 创建硬链接,newPath 指向 existingPath,两个文件共享同一份数据
symlinkSync(target, path[, type]) 创建软链接,path 指向 target,target 可以是绝对路径或相对路径,type 仅在 Windows 上有效,默认为 'file',可以是 'dir' 或 'junction'
const file = path.resolve(__dirname, './1.txt')fs.linkSync(file, path.resolve(__dirname, 'index2.txt')) //硬链接fs.symlinkSync(file, path.resolve(__dirname, 'index3.txt')) //软连接unlink(path, err=>{}) 删除文件或符号链接
fs.unlinkSync(path.resolve(__dirname, './index.txt')) // 删除文件fs.unlinkSync(path.resolve(__dirname, './index2.txt')) // 删除硬链接fs.unlinkSync(path.resolve(__dirname, './index3.txt')) // 删除软连接硬链接:
- 文件共享:硬链接允许多个文件指向同一个文件(同一个 inode),这样可以在不同的位置使用不同的文件名引用相同的内容。这样的共享文件可以节省存储空间,并且在多个位置对文件的修改会反映在所有引用文件上。
- 文件备份:通过创建硬链接,可以在不复制文件的情况下创建文件的备份。如果原始文件发生更改,备份文件也会自动更新。这样可以节省磁盘空间,并确保备份文件与原始文件保持同步。
- 文件重命名:通过创建硬链接,可以为文件创建一个新的文件名,而无需复制或移动文件。这对于需要更改文件名但保持相同内容和属性的场景非常有用。
只有删除原始文件和所有硬链接后,才会真正删除文件
软链接:
- 软链接实际上是保存了一个绝对路径
- 跨文件系统:软链接可以跨越文件系统,而硬链接不能。硬链接只能在同一文件系统上工作,因为它们指向的是 inode,而 inode 只在文件系统内部唯一。
- 符号链接:软链接是一个特殊类型的文件,它包含指向另一个文件的路径名。软链接可以指向任何类型的文件,包括目录,而硬链接只能指向普通文件。
重命名或移动原始文件的位置,会导致软链接失效,需要更新目标路径,而硬链接仍然有效
inode
**inode (index node)**是指在许多类Unix文件系统中的一种数据结构,用于描述文件系统对象(包括文件、目录、设备文件、socket、管道等)。每个inode保存了文件系统对象数据的属性和磁盘块(block)位置。文件系统对象属性包含了各种元数据(如:最后修改时间),也包含用户组(owner )和权限数据。
文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。
简单的说:每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。
- 文件的字节数
- 文件拥有者的User ID
- 文件的Group ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
- 链接数,即有多少文件名指向这个inode
- 文件数据block的位置
打开文件时,系统首先找到文件名对应的 inode 号码,然后通过 inode 号码获取inode 信息,然后根据 inode 信息中的文件数据所在 block 读出数据。
statSync(path[, options]) 可以查看文件的inode信息,返回 fs.Stats
1. bigint <boolean>:意义:指示是否应该返回 fs.BigInt 类型的数值,否则返回 number 类型的整数。默认值:false。
2. throwIfNoEntry <boolean>:意义:如果文件系统条目不存在,是否会抛出异常,而不是返回 undefined默认值:false。const stat = fs.statSync(path.resolve(__dirname, './1.txt'))console.log(stat)console.log(stat.isDirectory()); // 是否是文件夹console.log(stat.isFile()); // 是否是普通文件console.log(stat.isSymbolicLink()) // 是否是软连接console.log(stat.isBlockDevice()) // 是否是块设备console.log(stat.isCharacterDevice()) // 是否是字符设备console.log(stat.isFIFO()) // 是否是FIFOconsole.log(stat.isSocket()) // 是否是Socket// Stats {// dev: 1552965699,// mode: 33206,// nlink: 3,// uid: 0,// gid: 0,// rdev: 0,// blksize: 4096,// ino: 10696049115741484,// size: 6,// blocks: 0,// atimeMs: 1705127903632.4563,// mtimeMs: 1705127903632.4563,// ctimeMs: 1705127903632.4563,// birthtimeMs: 1705043183050.7405,// atime: 2024-01-13T06:38:23.632Z,// mtime: 2024-01-13T06:38:23.632Z,// ctime: 2024-01-13T06:38:23.632Z,// birthtime: 2024-01-12T07:06:23.051Z// }Stream流
stream(流)是一种抽象的数据结构。就像数组或字符串一样,流是数据的集合。
stream 就像是水流,但默认是没有水的。stream.write 可以让水流中有水,也就是写入数据。
**作用:**大文件资源拆分成小块(chunk),一块一块的运输,资源就像水流一样进行传输,无需将文件整个读入内存,减轻服务器压力。
四种类型的流:
Node中Stream无处不在,对服务器发起 http 请求的 request/response 对象也是 Stream。
Readable可读流
可读流中分为2种模式
- 流动模式:监听data事件,一旦有数据就会触发data事件,数据作为回调的参数,直到数据全部读取完毕
- 暂停模式:监听readable事件,当流有了新数据或到了流结束之前触发readable事件,需要显示调用read([size])读取数据
暂停模式切换到流动模式:
- 监听 data 事件
- 调用 stream.resume()方法
- 调用 stream.pipe()方法将数据发送到可写流
流动模式切换到暂停模式:
- 如果不存在管道目标,调用 stream.pause() 方法
- 如果存在管道目标,调用 stream.unpipe() 并取消 data 事件监听
const { Readable } = require('stream')const inStream = new Readable() // 可以在其中实现_read方法inStream.push('hello world') // 写入数据inStream.push('hello node')inStream.push(null) // 没有数据了// 将这个可读流,导入到可写流 process.stdout。inStream.pipe(process.stdout)详见fs中的createReadStream
Writable可写流
const { Writable } = require('stream')const outStream = new Writable({ // 实现_write方法 write(chunk, encoding, callback) { console.log(chunk.toString()) callback() // 通知流处理继续 }})process.stdin.pipe(outStream);详见fs中的createWriteStream
Duplex流
Duplex 可读可写流,读和写是独立的,各自独立缓存区,既可当成可读流来使用,也可当成可写流来使用
Duplex 拥有 Writable 和 Readable 所有方法和事件,可以同时实现 read() 和 write() 方法。
const { Duplex } = require('stream')
const duplex = new Duplex({ // 可读端底层读取逻辑 read(size) { if (this.currentCharCode > 90) { this.push('\n') this.push(null) // null 代表流没有数据了,不会触发data事件 } else { // 通过push方法将数据推送到可读流,每次push都会触发一次data事件 this.push(String.fromCharCode(this.currentCharCode++)) } }, // 可写端底层写逻辑 write(buf, enc, next) { process.stdout.write('write: ' + buf.toString().toUpperCase()) next() // 通知流处理继续 }})
duplex.currentCharCode = 65;duplex.pipe(process.stdout);// ABCDEFGHIJKLMNOPQRSTUVWXYZprocess.stdin.pipe(duplex);// 小写输入转为大写// aaaa// write: AAAATransform流
Transform 流属于 Duplex 流,其中输出以某种方式从输入计算得出。 例如进行压缩、加密或解密数据的 zlib 流或 crypto 流。
使用也很简单,new Transform({ transform() }),传入包括实现了 transform() 方法的对象,该方法接收三个参数:chunk、encoding、callback
- chunk:读取到的数据块
- encoding:编码方式
- callback:回调函数,通知流处理继续
读数据:chunk.toString()
写数据 this.push(xxx)
const {Transform} = require('stream')
const upperCaseTr = new Transform({ transform(chunk, encoding, callback) { this.push(chunk.toString().toUpperCase()) callback(); }})
process.stdin .pipe(upperCaseTr) .pipe(process.stdout)pipe管道
pipe 管道可以连接两个流,将一个流的输出作为另一个流的输入
stream1.pipe(stream2)
stream1 是发出数据的流,一个可读流。
stream2 是写入数据的流,一个可写流。
readable.pipe() 方法将 Writable 流绑定到 readable,使其自动切换到流动模式并将其所有数据推送到绑定的 Writable。数据流将被自动管理,以便目标 Writable 流不会被更快的 Readable 流漫过。
例如响应大文本和大图片:
const fs = require('fs')const http = require('http')const path = require('path')
const server = http.createServer();
server.on('request', (request, response) => { // 根据请求路径判断是文本请求还是图片请求 if (request.url === '/txt') { const stream = fs.createReadStream(path.resolve(__dirname, './big_data.txt')); // 通过管道方式写入响应,读多少传多少 stream.pipe(response); } else if (request.url === '/img') { const stream = fs.createReadStream(path.resolve(__dirname, './big_img.png')); // 设置响应头,告诉浏览器这是一个图片 response.setHeader('Content-Type', 'image/png'); stream.pipe(response); } else { // 处理其他请求或返回 404 Not Found response.statusCode = 404; response.end('Not Found'); }});
server.listen(8888, () => { console.log('Server is running on http://localhost:8888');});**链式操作:**一个水流可以经过无限个管道,数据流也一样。在链式操作过程中可以对数据进行转换、压缩等处理。
const fs = require('fs');const path = require('path');const zlib = require('zlib');const stream = require('stream');
// 创建可读流const readStream = fs.createReadStream(path.resolve(__dirname, 'input.txt'));// 创建可写流const writeStream = fs.createWriteStream(path.resolve(__dirname, 'output.txt.gz'));
// 创建转换流,将文本内容转换为大写const upperCaseTransform = new stream.Transform({ transform(chunk, encoding, callback) { // 将数据转换为大写 const upperCaseData = chunk.toString().toUpperCase(); // 将转换后的数据推送到可读流 this.push(upperCaseData); // 转换完成后调用回调函数,通知流处理继续 callback(); }});
// 创建压缩流const gzipStream = zlib.createGzip();
// 将各个流连接起来形成管道链式操作readStream .pipe(upperCaseTransform) // 转换为大写 .pipe(gzipStream) // 压缩 .pipe(writeStream) // 写入文件
// 监听完成事件writeStream.on('finish', () => { console.log('文件处理完成');});既然都是 stream 那么其它流方法也能套在链式过程中
fs.createReadStream(path.resolve(__dirname, 'big_data.txt')) .pipe(zlib.createGzip()) // 每次产生一块压缩数据时,执行提供的回调函数 .on('data', () => process.stdout.write(".")) // 打出进度条 .pipe(fs.createWriteStream(path.resolve(__dirname, 'big_data.txt.gz')))管道原理
管道可以认为是两个事件的封装
- 监听 data 事件,stream1 一有数据就塞给 stream2
- 监听 end 事件,当 stream1 停了,就停掉 stream2
stream1.on('data', (chunk) => { stream2.write(chunk)})stream1.on('end', () => { stream2.end()})crypto
crypto 模块提供了加密功能,其中包括了用于 OpenSSL 散列、HMAC、加密、解密、签名、以及验证的函数的一整套封装。
crypto有非常多的API,但主要有几个类,每个类都有许多模块方法(create***())去生成其实例,然后调用实例的方法去实现加密解密等功能
- Cipher类的实例用于对称加密数据
- Decipher类的实例用于解密数据
- Hash类是用于创建数据的哈希摘要的实用工具
- Hmac类是用于创建加密 HMAC 摘要的实用工具
- KeyObject类表示对称或非对称密钥,每种密钥暴露不同的功能
- Sign类是用于生成签名的实用工具
- Verify类是用于验证签名的实用工具
本文后面需要加密的data都为 'hello world'
摘要Hash
摘要(digest):将长度不固定的消息作为输入,通过运行hash函数,生成固定长度的输出,这段输出就叫做摘要,具有唯一性。通常用来验证消息完整、未被篡改。摘要运算是不可逆的,但可以撞库破解。
摘要算法:MD5、SHA1、SHA256、SHA512
crypto.getHashes() 返回支持的哈希算法名称的数组
console.log(crypto.getHashes())// [// 'RSA-MD5',// 'RSA-RIPEMD160',// 'RSA-SHA1',// 'RSA-SHA1-2',// 'RSA-SHA224',// 'RSA-SHA256',// 'RSA-SHA3-224',// 'RSA-SHA3-256',// 'RSA-SHA3-384',// 'RSA-SHA3-512',// 'RSA-SHA384',// 'RSA-SHA512',// 'RSA-SHA512/224',// 'RSA-SHA512/256',// 'RSA-SM3',// 'blake2b512',// 'blake2s256',// 'id-rsassa-pkcs1-v1_5-with-sha3-224',// 'id-rsassa-pkcs1-v1_5-with-sha3-256',// 'id-rsassa-pkcs1-v1_5-with-sha3-384',// 'id-rsassa-pkcs1-v1_5-with-sha3-512',// 'md5',// 'md5-sha1',// 'md5WithRSAEncryption',// 'ripemd',// 'ripemd160',// 'ripemd160WithRSA',// 'rmd160',// 'sha1',// 'sha1WithRSAEncryption',// 'sha224',// 'sha224WithRSAEncryption',// 'sha256',// 'sha256WithRSAEncryption',// 'sha3-224',// 'sha3-256',// 'sha3-384',// 'sha3-512',// 'sha384',// 'sha384WithRSAEncryption',// 'sha512',// 'sha512-224',// 'sha512-224WithRSAEncryption',// 'sha512-256',// 'sha512-256WithRSAEncryption',// 'sha512WithRSAEncryption',// 'shake128',// 'shake256',// 'sm3',// 'sm3WithRSAEncryption',// 'ssl3-md5',// 'ssl3-sha1'// ]const data = 'hello world'// 创建哈希对象,并使用 MD5 算法const hash = crypto.createHash('md5')// 更新哈希对象的数据hash.update(data)// 计算哈希值,并以十六进制字符串形式输出const digest = hash.digest('hex')console.log(digest) // 5eb63bbbe01eeed093cb22bb8f5acdc3MAC、HMAC
MAC(Message Authentication Code):消息认证码,用以保证数据的完整性。运算结果取决于消息本身、秘钥。
MAC可以有多种不同的实现方式,比如Hash、HMAC。
HMAC(Hash-based Message Authentication Code):可以粗略地理解为带秘钥的Hash函数。主要是为了防止Hash碰撞攻击。
const hmac = crypto.createHmac('md5', '123456')hmac.update(data)const digest = hmac.digest('hex')console.log(digest) // 5eb63bbbe01eeed093cb22bb8f5acdc3对称加密
**加密/解密:**给定明文,通过一定的算法,产生加密后的密文,这个过程叫加密。反过来就是解密。
**秘钥:**为了进一步增强加/解密算法的安全性,在加/解密的过程中引入了秘钥。秘钥可以视为加/解密算法的参数,在已知密文的情况下,如果不知道解密所用的秘钥,则无法将密文解开。
根据加密、解密所用的秘钥是否相同,可以将加密算法分为对称加密、非对称加密。
常见的对称加密算法:DES、3DES、AES、Blowfish、RC5、IDEA。
createCipher()(已弃用) 或 createCipheriv() 方法用于创建 Cipher 实例,使用初始化向量IV增加加密强度,IV 通常只是添加到未加密的密文消息中,解密后会被删除。
createDecipher()(已弃用) 或 createDecipheriv() 方法用于创建 Decipher 实例,使用相同的密钥和IV进行解密。
// 生成一个随机的 16 字节的初始化向量 (IV)const iv = crypto.randomBytes(16)// 生成一个随机的 32 字节的密钥const key = crypto.randomBytes(32)// 创建一个 AES-256-CBC 加密算法的 cipher 对象const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)// 对输入数据进行加密,并输出加密结果的十六进制表示// 可以使用新数据多次调用 cipher.update() 方法,直到调用 cipher.final()cipher.update(data, "utf-8", "hex")// 以十六进制表示输出加密结果// 一旦调用了 cipher.final() 方法,则 Cipher 对象就不能再用于加密数据const result = cipher.final('hex')console.log(result) // 5eb63bbbe01eeed093cb22bb8f5acdc3// 创建一个 AES-256-CBC 解密算法的 decipher 对象,使用相同的密钥和IVconst decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)// 对加密数据进行解密,并输出解密结果的 UTF-8 字符串// 可以使用新数据多次调用 decipher.update() 方法,直到调用 decipher.final()decipher.update(result, "hex", "utf-8")// 以 UTF-8 字符串表示输出解密结果// 一旦调用了 decipher.final() 方法,则 Decipher 对象就不能再用于解密数据const decrypted = decipher.final('utf-8')console.log(decrypted) // hello world分组加密
常见的对称加密算法,如AES、DES都采用了分组加密模式,三个重要概念:模式、初始化向量、填充。
**分组加密:**将(较长的)明文拆分成固定长度的块,然后对拆分的块按照特定的模式进行加密。
常见模式有:ECB(不安全)、CBC(最常用)、CFB、OFB、CTR等
初始化向量IV: 为了增强算法的安全性,部分分组加密模式(CFB、OFB、CTR)中引入了初始化向量(IV),使得加密的结果随机化。也就是说,对于同一段明文,IV不同,加密的结果不同。 以CBC为例,每一个数据块,都与前一个加密块进行异或运算后,再进行加密。对于第一个数据块,则是与IV进行异或。 IV的大小跟数据块的大小有关(128位,16字节),跟秘钥的长度无关。
填充padding: 部分加密模式,当最后一个块的长度小于128位时,需要通过特定的方式进行填充。(ECB、CBC需要填充,CFB、OFB、CTR不需要填充)
非对称加密
对称加密的密钥是相同的,如果密钥泄露,加密的数据就不安全了。
非对称加密使用一对密钥,公钥和私钥,公钥加密,私钥解密。 公钥是公开的,任何人都可以获得,并对数据进行加密。私钥是保密的,只有私钥的拥有者才能解密。
生成密钥对:crypto.generateKeyPairSync(type[, options]),type为加密算法,options为配置项,返回一个对象,包含公钥和私钥。
加密函数:crypto.publicEncrypt(publicKey, buffer),publicKey为公钥,buffer为要加密的数据,返回加密后的数据。
解密函数:crypto.privateDecrypt(privateKey, buffer),privateKey为私钥,buffer为要解密的数据,返回解密后的数据。
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, // 模数长度,默认 2048 位,越长安全性越高,但是性能越差}); // 生成 RSA 密钥对// 使用公钥加密数据const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(data, 'utf-8'))console.log(encrypted.toString('hex')) // 227d9e20fe66d854ec9ddda.......// 使用私钥解密数据const decrypted = crypto.privateDecrypt(privateKey, encrypted)console.log(decrypted.toString('utf-8')) // hello worldgenerateKeyPairSync
generateKeyPairSync(type, options) 用于生成密钥对
type: <string> 必须是 'rsa'、'rsa-pss'、'dsa'、'ec'、'ed25519'、'ed448'、'x25519'、'x448' 或 'dh'。options: <Object> modulusLength: <number> 以位为单位的密钥大小(RSA、DSA)。 publicExponent: <number> 公共指数 (RSA)。 默认值: 0x10001。 hashAlgorithm: <string> 消息摘要的名称 (RSA-PSS)。 mgf1HashAlgorithm: <string> MGF1 (RSA-PSS) 使用的消息摘要的名称。 saltLength: <number> 以字节为单位的最小盐长度 (RSA-PSS)。 divisorLength: <number> q 的大小(以位为单位)(DSA)。 namedCurve: <string> 要使用的曲线的名称 (EC)。 prime: <Buffer> 主要参数 (DH)。 primeLength: <number> 以位 (DH) 为单位的素数长度。 generator: <number> 自定义生成器 (DH)。 默认值: 2。 groupName: <string> Diffie-Hellman 组名 (DH)。 参见 crypto.getDiffieHellman()。 paramEncoding: <string> 必须是 'named' 或 'explicit' (EC)。 默认值: 'named'。 publicKeyEncoding: <Object> 参见 keyObject.export()。 privateKeyEncoding: <Object> 参见 keyObject.export()。返回: <Object> publicKey: <string> | <Buffer> | <KeyObject> privateKey: <string> | <Buffer> | <KeyObject>如果指定了 publicKeyEncoding 或 privateKeyEncoding,则此函数的行为就像对其结果调用了 keyObject.export(options),两者参数也与其一致。否则,密钥的相应部分将作为 KeyObject 返回。
对公钥进行编码时,建议使用 'spki'。 对私钥进行编码时,建议使用强密码的 'pkcs8',并对密码进行保密。
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, // 模数长度,默认 2048 位,越长安全性越高,但是性能越差 publicKeyEncoding: { // 公钥的输出格式 type: 'spki', // 密钥编码类型 format: 'pem' // 输出格式 }, privateKeyEncoding: { // 私钥的输出格式 type: 'pkcs8', // 密钥编码类型 format: 'pem' // 输出格式 }});console.log(privateKey)// -----BEGIN PRIVATE KEY-----// MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCs/KwPf39GSmgc// ......// WcDOr4jH76xt0OwEZNVn2A==// -----END PRIVATE KEY-----console.log(publicKey)// -----BEGIN PUBLIC KEY-----// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArPysD39/RkpoHLHiXTSQ// ......// dwIDAQAB// -----END PUBLIC KEY-----数字签名
数字签名属于非对称加密,用于验证数据的完整性和来源,私钥签名,公钥验证。
发送方生成签名:
- 计算原始信息的摘要。
- 通过私钥对摘要进行签名,得到电子签名。
- 将原始信息、电子签名,发送给接收方。
接收方验证签名:
- 通过公钥解开电子签名,得到摘要D1。(如果解不开,信息来源主体校验失败)
- 计算原始信息的摘要D2。
- 对比D1、D2,如果D1等于D2,说明原始信息完整、未被篡改。
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }});// 创建签名对象,使用 SHA256 算法const sign = crypto.createSign('RSA-SHA256');// 更新签名对象的数据sign.update(data);// 私钥计算签名值,并以十六进制字符串形式输出const signature = sign.sign(privateKey, 'hex');console.log(signature) // 847f62482332b9abaa3a......
// 创建验证对象,使用 SHA256 算法const verify = crypto.createVerify('RSA-SHA256');// 更新验证对象的数据verify.update(data);// 公钥验证签名值是否正确,以及数据是否被篡改const result = verify.verify(publicKey, signature, 'hex');console.log(result); // true,验证成功,数据没有被篡改脚手架
脚手架是一种自动化的工具,用于快速生成项目的基础结构,包括目录结构、配置文件、代码规范等。 例如vue-cli、create-react-app、express-generator
作用:
- 快速初始化项目
- 保证协作团队项目的统一
- 添加通用的组件或者配置
脚手架一般通过命令行的方式使用,例如vue-cli,通过vue create <name>命令创建项目,首先设置项目名称,然后可以选择预设的模板,如是否需要使用TS、是否需要使用eslint等,最后会自动下载依赖、模板,创建项目。
重要的就是与命令行的交互,以及模板的下载。 可以使用readline和fs去实现,但是非常麻烦,也不好看,还是使用现成的库方便。
- commander 执行复杂的命令
- inquirer 问答交互
- download-git-repo 下载远程模板
- chalk 让 console.log 带颜色,比如成功时的绿色
- ora 命令行 loading 效果
在入口文件添加特殊注释#!/usr/bin/env node告诉终端,这个文件要使用 node 去执行
然后在package.json中添加bin字段,指定命令的入口文件,在本地测试时,使用npm link将命令链接到全局
{ "bin": { "test-cli": "index.js" }}编写代码完成脚手架的功能
#!/usr/bin/env nodeimport chalk from 'chalk'import inquirer from 'inquirer'import { Command } from 'commander'import * as util from './util.js'import pkg from './package.json' assert { type: "json" };
// 创建命令行对象const program = new Command()
// 定义 --version 参数program.version(pkg.version)
// 定义命令program.command('create <name>') .alias('c') // 命令别名 .description('创建项目') // 命令描述 .action((name) => { inquirer.prompt([ { type: 'input', // 类型为input输入框 name: 'projectName', // 问题名称 message: '请输入项目名称', // 问题描述 default: name // 默认值 }, { type: 'confirm', name: 'isTS', message: '是否使用TypeScript', default: false } ]).then(res => { // 结果返回一个{ <name>: <value> }对象 if (util.checkDirExist(res.projectName)) { console.log(chalk.red('文件夹已存在')) return } const repo = "github:qxchuckle/rollup-template" if (res.isTS) { util.downloadTemplate('main', repo, res.projectName) } else { util.downloadTemplate('main', repo, res.projectName) } }) })
// 解析命令行参数program.parse(process.argv)import fs from 'fs'import downloadGitRepo from 'download-git-repo'import ora from 'ora'
// 检查路径是否存在export function checkDirExist(path) { try { return fs.existsSync(path) } catch (error) { return false }}
export function downloadTemplate(branch, repo, dest) { return new Promise((resolve, reject) => { const spinner = ora('正在下载模板...') spinner.start() downloadGitRepo(`${repo}#${branch}`, dest, { clone: true }, (err) => { if (err) { spinner.fail('下载失败') reject(err) } else { spinner.succeed('下载成功') resolve() } }) })}markdown渲染
markdown渲染是常见的需求,使用marked、marked-highlight、highlight,将markdown转换为html并高亮代码,再使用ejs模板引擎渲染完整页面、browser-sync实现构建网站时保持多个浏览器和设备同步
import { Marked } from "marked";import { markedHighlight } from "marked-highlight";import hljs from 'highlight.js';import fs from "fs";import path from "path";import { fileURLToPath } from 'url';import browserSync from "browser-sync";import ejs from "ejs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 生成marked实例,用于渲染mdconst marked = new Marked( markedHighlight({ langPrefix: 'hljs language-', // 紧接在代码块打开标记之后找到的语言标记被附加到它,形成类属性 highlight(code, lang, info) { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; } }));// 读取文件const readFile = (file) => { return fs.readFileSync(path.resolve(__dirname, file), 'utf8');}// 启动服务const server = () => { globalThis.browser = browserSync.create() browser.init({ server: { baseDir: __dirname, index: 'index.html', } })}// 防抖const debounce = (func, delay) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { func(...args); }, delay); };};// 监听文件变化,实现热更新const watch = () => { const debouncedInit = debounce(() => { init(() => { browser.reload(); }); }, 100); fs.watch(path.resolve(__dirname, './01.md'), (eventType, filename) => { // 修改文件会连续触发多次change事件,使用防抖函数,只执行最后一次 debouncedInit(); });}
// css资源const css = [ 'http://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown-light.min.css',];
function init(callback) { // 模板渲染 ejs.renderFile(path.resolve(__dirname, 'index.ejs'), { title: 'Markdown', content: marked.parse(readFile('01.md')), css, }, (err, str) => { if (err) { console.log(err); return; } const writeStream = fs.createWriteStream(path.resolve(__dirname, './index.html'), 'utf-8'); writeStream.on('ready', () => { console.log('开始写入') }); writeStream.write(str, () => { console.log('写入完成') callback && callback(); writeStream.destroy(); }); writeStream.end(); // 结束写入,表示不再有数据写入(end之后不能调用write) });}// 启动init(() => { server(); // 启动服务 watch(); // 启动监听});<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title> <%= title %> </title> <% css.forEach(function(cssPath) { %> <link rel="stylesheet" href="<%= cssPath %>"> <% }); %></head><body> <article class="markdown-body"> <%- content %> </article></body></html>zlib
zlib 模块提供了使用 Gzip、Deflate/Inflate、以及 Brotli 实现的压缩功能。
常用的两个压缩算法:gzip、deflate。分别对应 zlib.createGzip()、zlib.createDeflate() 进行压缩,zlib.createGunzip()、zlib.createInflate() 进行解压。
通过管道可以很方便的实现压缩和解压缩
fs.createReadStream(path.join(__dirname, 'big_data.txt')) .pipe(zlib.createGzip()) // 使用gzip压缩 .pipe(fs.createWriteStream(path.join(__dirname, 'big_data.txt.gz')))
fs.createReadStream(path.join(__dirname, 'big_data.txt')) .pipe(zlib.createDeflate()) // 使用Deflate压缩 .pipe(fs.createWriteStream(path.join(__dirname, 'big_data.txt.deflate')))fs.createReadStream(path.join(__dirname, 'big_data.txt.gz')) .pipe(zlib.createGunzip()) // 使用gzip解压 .pipe(fs.createWriteStream(path.join(__dirname, 'big_data.txt')))
fs.createReadStream(path.join(__dirname, 'big_data.txt.deflate')) .pipe(zlib.createInflate()) // 使用Deflate解压 .pipe(fs.createWriteStream(path.join(__dirname, 'big_data.txt')))deflate是一种使用了LZ77算法与哈夫曼编码(Huffman Coding)实现的无损数据压缩算法。它是一个无专利的,可以自由使用的算法。 gizp是一种以0x1F8B标志开头的数据格式,其内部通常采用DEFLATE算法对数据进行压缩。
"Deflate" 和 "DEFLATE" 实际上是指相同的压缩算法,区别在于对待大小写的不同。"Deflate" 是一般的术语,表示该压缩算法。而 "DEFLATE" 则是一个特定的字母大小写形式,通常用于指代该算法在特定上下文中的实现或特定文件格式中的使用,比如在 gzip 文件格式中
http请求压缩
客户端在向服务端发起请求时,会在请求头中添加accept-encoding字段,其值标明客户端支持的压缩内容编码格式 服务端在对返回内容执行压缩后,通过在响应头中添加content-encoding,来告诉浏览器内容实际压缩使用的编码算法
gzip 的核心是 Deflate,而它使用了 LZ77 算法与 Huffman 编码来压缩文件,重复度越高的文件可压缩的空间就越大。 对于文本文件,GZip 的效果非常明显,开启后传输所需流量大约会降至 1/4 ~ 1/3。主要用于 HTTP 文件传输中,比如 JS、CSS 等,但一般不会压缩图片。
const server = http.createServer();const zipList = { gzip: zlib.createGzip(), deflate: zlib.createDeflate(), br: zlib.createBrotliCompress()}
server.on('request', (req, res) => { const acceptedEncodings = req.headers['accept-encoding'].split(', '); console.log(acceptedEncodings); // gzip, deflate, br const stream = fs.createReadStream(path.resolve(__dirname, './big_data.txt')); if (acceptedEncodings && acceptedEncodings.length > 0) { const encoding = acceptedEncodings[0].trim(); res.setHeader('Content-Encoding', encoding); stream.pipe(zipList[encoding]).pipe(res); } else { stream.pipe(res); }});
server.listen(8888, () => { console.log('Server is running on http://localhost:8888');});
评论