events Node是事件驱动的,事件模型采用了发布订阅设计模式 ,EventListener、Vue2 evnetBus都是这种模式
发布订阅模式有三个角色参与
发布者(Publisher):发布消息,制定消息的主题名 
订阅者(Subscriber):通过消息主题订阅消息,先订阅再等待发布消息,否则会错过消息 
调度中心(Broker):维护一个消息列表,并提供发布和订阅消息的方法,通过消息主题将发送者和接收者连接起来,四个基本的方法:on、once、off、emit 
 
发布者和订阅者之间完全解耦,不再直接依赖于彼此,可以独立地扩展自己,消息的传递则依靠调度中心。发布者不会将消息直接发送给订阅者,而是通过调度中心将消息和其主题广播出去,订阅了该主题则可以接收到消息
实现一个简单的发布订阅:
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 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)   } } 
 
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const  subPub = new  SubPub ()subPub.emit ('abc' , 678 )  const  fn : EventFun  = (...arg ) =>  {  console .log (arg); } subPub.on ('abc' , fn) subPub.emit ('abc' , 131 , true )  subPub.emit ('abc' , 678 , false , 'qx' )  subPub.off ('abc' , fn) subPub.emit ('abc' , 321 , 'qx' )  console .log ("=======================" );subPub.emit ('a' , 678 )  subPub.once ('a' , (...arg ) =>  {   console .log (arg); }) subPub.emit ('a' , 678 , 'abc' )  subPub.emit ('a' , 123 , 'qx' )  subPub.on ('a' , (...arg ) =>  {   console .log (arg); }) subPub.emit ('a' , 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) 为指定事件注册一个单次监听器到监听器数组的头部  
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ()) console .log (event.listeners ('test' )) console .log (event.rawListeners ('test' ))console .log (event.listenerCount ('test' )) event.emit ('test' , 1 , 2 , 3 , 4 , 5 ) event.emit ('test' , 1 , 2 , 3 , 4 , 5 ) 
 
默认情况下,每个事件最多注册 10 个监听器,超过 10 个监听器会发出警告
1 2 3 4 5 6 7 8 9 10 for  (let  i = 0 ; i < 20 ; i++) {  event.on ('test' , () =>  {     console .log (`test${i} ` )   }) } event.emit ('test' ) 
 
使用 setMaxListeners(num) 设置单个事件最大监听器数量
1 event.setMaxListeners (20 ) 
 
错误事件 当 EventEmitter 实例中发生错误时,会触发 error 事件
如果没有为 error 事件注册监听器,则会抛出错误,打印堆栈跟踪,然后 Node 进程退出
最佳实践:应始终为 error 事件添加监听器
1 2 3 4 event.on ('error' , (err )=> {   console .log ('message:' , err.message )  }) event.emit ('error' , new  Error ('错误信息' )); 
 
使用 events.errorMonitor 安装监听器,可以在不消费触发的错误的情况下监视 error 事件(错误会穿透监听)
1 2 3 4 5 6 7 8 9 const  { errorMonitor } = require ('events' )event.on (errorMonitor, (err ) =>  {   console .log ('message:' , err.message )  }) event.emit ('error' , new  Error ('错误信息' )); 
 
new/removeListener事件 newListener  当有新的监听器将要 注册,触发该事件removeListener  当有监听器被移除后,触发该事件
1 2 3 4 5 6 7 8 9 10 11 12 event.on ('newListener' , (event, listener ) =>  {   console .log (event, listener)       }) event.on ('removeListener' , (event, listener ) =>  {   console .log (event, listener)    }) const  listener  = ( ) => { }event.on ('test' , listener) event.off ('test' , listener) 
 
process底层 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 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' ,   });            let  _process = process;   ObjectDefineProperty (globalThis, 'process' , {     __proto__ : null ,     get ( ) {       return  _process;     },     set (value ) {       _process = value;     },     enumerable : false ,     configurable : true ,   }); } 
 
ObjectGetPrototypeOf() 即 Object.getPrototypeOf(),获取某个对象原型上的属性
在源码中,通过该API获取了process的原型
1 2 3 4 5 6 7 class  A  {}A.prototype  .name  = 'A' ; const  a = new  A ();console .log (Object .getPrototypeOf (a));console .log (a.__proto__ );
 
ObjectSetPrototypeOf() 即 Object.setPrototypeOf(),用于设置对象的原型
在源码中,将 EventEmitter 的原型对象设置为 process 的原型的原型,让 process 也能使用 events 的一些方法
1 2 3 4 5 6 class  A  {}A.prototype  .name  = 'A' ; const  a = new  A ();Object .setPrototypeOf (a, { name : 'B'  });console .log (Object .getPrototypeOf (a));
 
FunctionPrototypeCall() 即 Function.prototype.call(),改变 this 指向并调用函数
在源码中,调用 EventEmitter 构造函数,使 process 也拥有 EventEmitter 的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function  A ( ) {  this .name  = 'A' ;   this .a  = 1 ; } function  B ( ) {  this .name  = 'B' ;   this .age  = 18 ; } const  a= new  A ();B.call (a); console .log (a.a );    console .log (a.name );  console .log (a.age );   
 
通过 FunctionPrototypeCall() 和 ObjectSetPrototypeOf(),使 process 继承自 EventEmitter
最后,通过 ObjectDefineProperty() 将process挂载到全局变量上
util util  提供了很多实用的、工具类型的API,方便快速开发
类型判断 util.types 上有很多 is***() 的方法用于判断类型,返回 boolean
与 instanceof 不同,util.types 不会受到原型链的影响
instanceof 运算符用于检查一个对象是否是某个构造函数的实例。具体而言,它检查一个对象的原型链中是否出现了指定构造函数的原型。
1 2 3 4 5 6 7 8 console .log (util.types .isDate (new  Date ())) console .log (util.types .isPromise (new  Promise (() =>  { }))) const  date = new  Date () console .log (date instanceof  Date ) Object .setPrototypeOf (date, {})console .log (date instanceof  Date ) console .log (util.types .isDate (date)) 
 
promisify promisify(fn) 用于将回调函数的模式转为promise模式
原回调函数的参数除err外,都作为对象的属性返回
1 2 3 4 5 6 7 8 9 const  execPromise = util.promisify (childProcess.exec )execPromise ('node -v' )  .then (({ stdout, stderr } ) =>  {          console .log (stdout)    })   .catch (err  =>  {     console .log (err)   }) 
 
手写 promisify
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 const  promisify  = (fn ) => {     return  (...args ) =>  {          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)    })   .catch (err  =>  {     console .log (err)   }) 
 
自己写 promisify 是无法获取到 key 名(原参数名)的,也就不能像 util.promisify 那样resolve一个对象
在底层通过 kCustomPromisifyArgsSymbol 获取 key 名,但该 API 仅Node内部使用
callbackify callbackify(fn) 将promise模式转为回调函数的模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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)  }) 
 
手写 callbackify
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 const  fn  = (type ) => {  return  new  Promise ((resolve, reject ) =>  {     if  (type === 'error' ) {       reject (new  Error ('error' ))     }     resolve (type)   }) } 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)  }) 
 
inspect inspect  将对象转换为字符串,通常用于调试和错误输出
util.inspect(object[, options])
options配置项:
1 2 3 4 5 6 7 8 9 10 11 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。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 , })) 
 
类似C中的 printf 的格式字符串
使用第一个参数作为类似 printf 的格式字符串(其可以包含零个或多个格式说明符)来返回格式化的字符串。每个说明符都替换为来自相应参数的转换后的值
1 util.format (format, ...args) 
 
格式说明符:
1 2 3 4 5 6 7 8 9 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() 转换为一个字符串
1 2 console .log (util.format ('%s:%s' , '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 输出状态信息 
 
封装函数:
1 2 3 4 5 6 7 8 9 10 11 const  pngquant  = (str ) => {  execFile ('pngquant' , str.split (" " ), {     cwd : __dirname   }, (err, stdout ) =>  {     if  (err) {       console .log (err)       return ;     }     console .log ("done" )   }) } 
 
1 2 3 4 5 6 7 8 pngquant ('./1.png --output ./2.png -f' )pngquant ('./1.png --quality=82 -o ./3.png -f' )pngquant ('./1.png --speed=11 --quality=80 -o ./4.png -f' )pngquant ('./1.png --ext new.png -f -v' ) 
 
fs fs  文件系统模块,提供了与文件系统进行交互的能力
所有文件系统操作方法都具有同步、异步回调和基于 promise 的形式
绝大部分方法都能传入options配置项,用于指定编码格式、文件模式等,如果 options 是字符串,则指定编码格式(encoding)
默认返回Buffer,可以通过指定编码格式获取字符串
fs的多种策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  fs = require ('fs' )const  fsPromises = require ('fs/promises' ) const  path = require ('path' )fs.readFile (path.resolve (__dirname, './1.txt' ), (err, data ) =>  {   console .log (data)  }) const  data = fs.readFileSync (path.resolve (__dirname, './1.txt' ), 'utf-8' )console .log (data) fsPromises.readFile (path.resolve (__dirname, './1.txt' )).then (data  =>  {      console .log (data.toString ())  }).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() 方法打开一个文件并分配一个新的文件描述符。分配后,文件描述符可用于从文件读取数据、向文件写入数据或请求有关文件的信息。
1 2 3 4 5 6 7 8 9 10 11 fs.open (path.resolve (__dirname, './1.txt' ), 'r' , (err, fd ) =>  {   console .log (fd)       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 事件。 
 
1 2 3 4 const  fd = await  fsPromises.open (path.resolve (__dirname, './1.txt' ), 'r' )const  data = await  fd.readFile ('utf-8' )console .log (data) await  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)。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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` ); }); 
 
在调用 fs.open()、fs.readFile() 或 fs.writeFile() 等文件操作之前,不要使用 fs.access() 检查文件的可访问性。这样做会引入竞争条件,因为其他进程可能会在两次调用之间更改文件的状态。而是,用户代码应直接打开/读取/写入文件,并处理无法访问文件时引发的错误。
readFile文件读取 readFile用于读取文件内容
同步 1 2 const  data = fs.readFileSync (path.resolve (__dirname, './1.txt' ), 'utf-8' )console .log (data) 
 
异步 1 2 3 fs.readFile (path.resolve (__dirname, './1.txt' ), 'utf-8' , (err, data ) =>  {   console .log (data)  }) 
 
promise 1 2 3 4 fsPromises.readFile (path.resolve (__dirname, './1.txt' ), 'utf-8' )   .then (data  =>  {     console .log (data)    }) 
 
createReadStream createReadStream(path[, options]) 创建可读流读取文件,返回一个可读流对象
适合读取大文件,因为不会一次性将文件读取到内存中,而是分块读取
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 42 43 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,表示不使用中止信号。 
 
1 2 3 4 const  readStream = fs.createReadStream (path.resolve (__dirname, './1.txt' ), 'utf-8' )readStream.on ('data' , (data ) =>  {   console .log (data)  }) 
 
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。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const  readStream = fs.createReadStream (path.resolve (__dirname, './1.txt' ), 'utf-8' )console .log (readStream.path ) console .log (readStream.bytesRead ) console .log (readStream.pending ) readStream.on ('data' , (data ) =>  {   console .log (readStream.bytesRead )    console .log (readStream.pending )    console .log (data)  }) 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 中,如果文件已存在则替换该文件
options配置项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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。 
 
1 2 3 fs.writeFile (path.resolve (__dirname, './1.txt' ), '123456' , (err ) =>  {   console .log ('写入成功' ) }) 
 
修改flag为a,表示追加写入
1 2 3 4 5 fs.writeFile (path.resolve (__dirname, './2.txt' ), "123456" , {   flag : 'a'  }, (err ) =>  {   console .log ('追加写入成功' ) }) 
 
appendFile追加写入 appendFile(path, data[, options], (err)=>{}) 异步地将数据追加到文件,如果该文件尚不存在,则创建该文件
options配置项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1. encoding <string>: 意义:指定写入文件的字符编码。 默认值:'utf8'。 2. mode <integer>: 意义:指定文件的权限掩码(权限位)。不会应用于已存在的文件。 默认值:0o666。 3. flag <string>: 意义:指定用于打开文件的标志。 默认值:'a'。 4. flush <AbortSignal>: 意义:如果是 true,则在关闭基础文件描述符之前将其刷新。 默认值: false。 
 
1 2 3 fs.appendFile (path.resolve (__dirname, './1.txt' ), '123456' , (err ) =>  {   console .log ('追加写入成功' ) }) 
 
createWriteStream createWriteStream(path[, options]) 创建一个可写流写入文件,返回一个可写流对象
适合大内容的写入,因为不会一次性将内容写入文件,而是分块写入
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 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,表示不刷新。 
 
1 2 3 4 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。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 )    writeStream.close (); }) writeStream.end ();  
 
drain事件 可以连续调用 writeStream.write() 向流中写入数据,但缓冲区是有限的,当缓冲区满时,writeStream.write() 将返回 false,表示不应再写入数据,直到触发 drain  事件,表示缓冲区已清空,可以继续写入数据
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 const  writableStream = fs.createWriteStream ('example.txt' , {   highWaterMark : 30   }); const  dataToWrite = [  '待到秋来九月八 ' ,   '我花开后百花杀 ' ,   '冲天香阵透长安 ' ,   '满城尽带黄金甲 '  ]; function  writeDataArray (dataArray, index ) {  if  (index < dataArray.length ) {          const  currentData = dataArray[index];     console .log (`正在写入数据: ${currentData} ` );          if  (!writableStream.write (currentData)) {       console .log ('缓冲区已满,后续写入需等待本次写入完成...' );              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 );
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 正在写入数据: 待到秋来九月八  缓冲区已满,后续写入需等待本次写入完成... 成功写入数据: 待到秋来九月八 缓冲区已空,可以继续写入... 正在写入数据: 我花开后百花杀 缓冲区已满,后续写入需等待本次写入完成... 成功写入数据: 我花开后百花杀 缓冲区已空,可以继续写入... 正在写入数据: 冲天香阵透长安 缓冲区已满,后续写入需等待本次写入完成... 成功写入数据: 冲天香阵透长安 缓冲区已空,可以继续写入... 正在写入数据: 满城尽带黄金甲 缓冲区已满,后续写入需等待本次写入完成... 成功写入数据: 满城尽带黄金甲 缓冲区已空,可以继续写入... 所有数据都已写入 
 
创建文件 Node没有创建文件的方法,但可以通过open和writeFile方法创建文件
1 2 3 4 5 fs.openSync (path.resolve (__dirname, './2.txt' ), 'a+' ) fs.writeFileSync (path.resolve (__dirname, './3.txt' ), "" , {   flag : 'a+'  }) 
 
mkdir创建目录 mkdir(path[, options], callback) 创建目录
options配置项 1 2 3 4 5 6 7 1. recursive <boolean>: 意义:指示是否应创建父目录(递归创建多级目录)。如果为 true,则缺少的目录将被创建。 默认值:false。 2. mode <integer>: 意义:设置目录的权限掩码(权限位)。不会应用于已存在的目录。Windows 上不支持 默认值:0o777。 
 
1 2 3 4 5 6 7 8 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) 删除文件或目录
options配置项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
1 2 3 4 5 6 7 8 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 已经存在,则它将被覆盖。
1 2 3 4 5 fs.renameSync (   path.resolve (__dirname, './2.txt' ),       path.resolve (__dirname, './test/22.txt' ) ) 
 
watch监视文件 watch(filename[, options][, listener]): <fs.FSWatcher>
fs.watch API 跨平台并非 100% 一致,并且在某些情况下不可用,详见:注意事项 
在 Windows 上,如果监视目录被移动或重命名,则不会触发任何事件。 删除监视目录时报 EPERM 错误
options配置项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1. persistent <boolean>: 意义:指示只要正在监视文件,进程是否应继续运行。 默认值:true。 2. recursive <boolean>: 意义:指示是应监视所有子目录,还是仅监视当前目录。 这在指定目录时适用,并且仅适用于受支持的平台。 默认值:false。 3. encoding <string> | <null>: 意义:指定用于传递回调的文件名的字符编码。如果未指定,则返回原始的 Buffer。 默认值:null。 4. signal <AbortSignal> | <null>: 意义:指定一个 AbortSignal 对象,用于中止监视器。 默认值:null。 
 
监视器回调接受eventType事件类型、filename文件名两个参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  watcher = fs.watch (path.resolve (__dirname, './1.txt' ),  (eventType, filename ) =>  {     console .log (eventType, filename)    })  watcher.on ('close' , () =>  {   console .log ('关闭watch' ) }) fs.writeFile (path.resolve (__dirname, './1.txt' ), '123456' , (err ) =>  {   console .log ('写入成功' )   watcher.close ()  }) 
 
readdir读取目录 readdir(path[, options], (err, files)=>{}) 获取一个文件夹下所有文件名的数组
options配置项 1 2 3 4 5 6 7 8 9 10 11 1. encoding <string>: 意义:指定用于解码文件名的字符编码。 默认值:'utf8' 2. withFileTypes <boolean>: 意义:如果为 true,则将结果数组中的条目替换为 fs.Dirent 对象。 默认值:false 3. recursive <boolean>: 意义:指示是否应递归读取子目录。如果为 true,则返回的数组将包含子目录中的文件的名称。 默认值:false 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 fs.readdir (path.resolve (__dirname, './' ),{   recursive : true   }, (err, files ) =>  {   console .log (files)    }) fs.readdir (path.resolve (__dirname, './' ),{   withFileTypes : true   }, (err, files ) =>  {   console .log (files)                         }) 
 
批量重命名文件,在文件名前加上0
1 2 3 4 5 6 7 8 9 10 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’
1 2 3 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=>{}) 删除文件或符号链接
1 2 3 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 
options配置项 1 2 3 4 5 6 7 1. bigint <boolean>: 意义:指示是否应该返回 fs.BigInt 类型的数值,否则返回 number 类型的整数。 默认值:false。 2. throwIfNoEntry <boolean>: 意义:如果文件系统条目不存在,是否会抛出异常,而不是返回 undefined 默认值:false。 
 
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 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 ()) console .log (stat.isSocket ()) 
 
Stream流 stream (流)是一种抽象的数据结构。就像数组或字符串一样,流是数据的集合。
一文搞定 Node.js 流 (Stream) 
stream 就像是水流,但默认是没有水的。stream.write 可以让水流中有水,也就是写入数据。
作用: 大文件资源拆分成小块(chunk),一块一块的运输,资源就像水流一样进行传输,无需将文件整个读入内存,减轻服务器压力。
四种类型的流:
Readable  可读流 
Writable  可写流 
Duplex  可读可写流,读和写是各自独立的 
Transform  可读可写流,读写在同一个流中,在读写过程中可以修改和变换数据 
 
Node中Stream无处不在,对服务器发起 http 请求的 request/response 对象也是 Stream。
Readable可读流 Readable 
可读流中分为2种模式
流动模式:监听data事件,一旦有数据就会触发data事件,数据作为回调的参数,直到数据全部读取完毕 
暂停模式:监听readable事件,当流有了新数据或到了流结束之前触发readable事件,需要显示调用read([size])读取数据 
 
暂停模式切换到流动模式:
监听 data 事件 
调用 stream.resume()方法 
调用 stream.pipe()方法将数据发送到可写流 
 
流动模式切换到暂停模式:
如果不存在管道目标,调用 stream.pause() 方法 
如果存在管道目标,调用 stream.unpipe() 并取消 data 事件监听 
 
Readable简单示例 1 2 3 4 5 6 7 const  { Readable  } = require ('stream' )const  inStream = new  Readable () inStream.push ('hello world' )  inStream.push ('hello node' ) inStream.push (null )  inStream.pipe (process.stdout ) 
 
详见fs中的createReadStream  
Writable可写流 Writable 
Writable简单示例 1 2 3 4 5 6 7 8 9 const  { Writable  } = require ('stream' )const  outStream = new  Writable ({     write (chunk, encoding, callback ) {     console .log (chunk.toString ())     callback ()    } }) process.stdin .pipe (outStream); 
 
详见fs中的createWriteStream  
Duplex流 Duplex  可读可写流,读和写是独立的,各自独立缓存区,既可当成可读流来使用,也可当成可写流来使用
Duplex 拥有 Writable 和 Readable 所有方法和事件,可以同时实现 read() 和 write() 方法。
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 const  { Duplex  } = require ('stream' )const  duplex = new  Duplex ({     read (size ) {     if  (this .currentCharCode  > 90 ) {       this .push ('\n' )       this .push (null )      } else  {              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 ); process.stdin .pipe (duplex); 
 
Transform  流属于 Duplex  流,其中输出以某种方式从输入计算得出。 例如进行压缩、加密或解密数据的 zlib  流或 crypto  流。
使用也很简单,new Transform({ transform() }),传入包括实现了 transform() 方法的对象,该方法接收三个参数:chunk、encoding、callback
chunk:读取到的数据块 
encoding:编码方式 
callback:回调函数,通知流处理继续 
 
读数据:chunk.toString() 写数据 this.push(xxx)
以大写的格式打印任何键入的字符 1 2 3 4 5 6 7 8 9 10 11 12 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 流漫过。
例如响应大文本和大图片:
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 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  {          response.statusCode  = 404 ;     response.end ('Not Found' );   } }); server.listen (8888 , () =>  {   console .log ('Server is running on http://localhost:8888' ); }); 
 
链式操作: 一个水流可以经过无限个管道,数据流也一样。在链式操作过程中可以对数据进行转换、压缩等处理。
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' );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 那么其它流方法也能套在链式过程中
1 2 3 4 5 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 
 
1 2 3 4 5 6 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() 返回支持的哈希算法名称的数组
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 52 53 54 55 console .log (crypto.getHashes ())
 
1 2 3 4 5 6 7 8 const  data = 'hello world' const  hash = crypto.createHash ('md5' )hash.update (data) const  digest = hash.digest ('hex' )console .log (digest) 
 
MAC、HMAC MAC(Message Authentication Code):消息认证码,用以保证数据的完整性。运算结果取决于消息本身、秘钥。
MAC可以有多种不同的实现方式,比如Hash、HMAC。
HMAC(Hash-based Message Authentication Code):可以粗略地理解为带秘钥的Hash函数。主要是为了防止Hash碰撞攻击。
1 2 3 4 const  hmac = crypto.createHmac ('md5' , '123456' )hmac.update (data) const  digest = hmac.digest ('hex' )console .log (digest) 
 
对称加密 加密/解密: 给定明文,通过一定的算法,产生加密后的密文,这个过程叫加密。反过来就是解密。
秘钥: 为了进一步增强加/解密算法的安全性,在加/解密的过程中引入了秘钥。秘钥可以视为加/解密算法的参数,在已知密文的情况下,如果不知道解密所用的秘钥,则无法将密文解开。
根据加密、解密所用的秘钥是否相同,可以将加密算法分为对称加密、非对称加密。
常见的对称加密算法:DES、3DES、AES、Blowfish、RC5、IDEA。
createCipher()(已弃用) 或 createCipheriv() 方法用于创建 Cipher 实例,使用初始化向量IV增加加密强度,IV 通常只是添加到未加密的密文消息中,解密后会被删除。
createDecipher()(已弃用) 或 createDecipheriv() 方法用于创建 Decipher 实例,使用相同的密钥和IV进行解密。
加密过程 1 2 3 4 5 6 7 8 9 10 11 12 13 const  iv = crypto.randomBytes (16 )const  key = crypto.randomBytes (32 )const  cipher = crypto.createCipheriv ('aes-256-cbc' , key, iv)cipher.update (data, "utf-8" , "hex" ) const  result = cipher.final ('hex' )console .log (result) 
 
解密过程 1 2 3 4 5 6 7 8 9 const  decipher = crypto.createDecipheriv ('aes-256-cbc' , key, iv)decipher.update (result, "hex" , "utf-8" ) const  decrypted = decipher.final ('utf-8' )console .log (decrypted) 
 
分组加密 常见的对称加密算法,如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为要解密的数据,返回解密后的数据。
1 2 3 4 5 6 7 8 9 const  { privateKey, publicKey } = crypto.generateKeyPairSync ('rsa' , {  modulusLength : 2048 ,  });  const  encrypted = crypto.publicEncrypt (publicKey, Buffer .from (data, 'utf-8' ))console .log (encrypted.toString ('hex' )) const  decrypted = crypto.privateDecrypt (privateKey, encrypted)console .log (decrypted.toString ('utf-8' )) 
 
generateKeyPairSync generateKeyPairSync(type, options) 用于生成密钥对
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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’,并对密码进行保密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const  { privateKey, publicKey } = crypto.generateKeyPairSync ('rsa' , {  modulusLength : 2048 ,    publicKeyEncoding : {      type : 'spki' ,      format : 'pem'     },   privateKeyEncoding : {      type : 'pkcs8' ,      format : 'pem'     } }); console .log (privateKey)console .log (publicKey)
 
数字签名 数字签名属于非对称加密,用于验证数据的完整性 和来源 ,私钥签名,公钥验证。
发送方生成签名:
计算原始信息的摘要。 
通过私钥对摘要进行签名,得到电子签名。 
将原始信息、电子签名,发送给接收方。 
 
接收方验证签名:
通过公钥解开电子签名,得到摘要D1。(如果解不开,信息来源主体校验失败) 
计算原始信息的摘要D2。 
对比D1、D2,如果D1等于D2,说明原始信息完整、未被篡改。 
 
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  { privateKey, publicKey } = crypto.generateKeyPairSync ('rsa' , {  modulusLength : 2048 ,   publicKeyEncoding : {     type : 'spki' ,     format : 'pem'    },   privateKeyEncoding : {     type : 'pkcs8' ,     format : 'pem'    } }); const  sign = crypto.createSign ('RSA-SHA256' );sign.update (data); const  signature = sign.sign (privateKey, 'hex' );console .log (signature) const  verify = crypto.createVerify ('RSA-SHA256' );verify.update (data); const  result = verify.verify (publicKey, signature, 'hex' );console .log (result); 
 
脚手架 脚手架是一种自动化的工具,用于快速生成项目的基础结构,包括目录结构、配置文件、代码规范等。 例如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将命令链接到全局
1 2 3 4 5 {   "bin" :  {      "test-cli" :  "index.js"    }  } 
 
编写代码完成脚手架的功能
index.js 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 #!/usr/bin/env node import  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 ()program.version (pkg.version ) program.command ('create <name>' )   .alias ('c' )    .description ('创建项目' )    .action ((name ) =>  {     inquirer.prompt ([       {         type : 'input' ,          name : 'projectName' ,          message : '请输入项目名称' ,          default : name        },       {         type : 'confirm' ,         name : 'isTS' ,         message : '是否使用TypeScript' ,         default : false        }     ]).then (res  =>  {              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 ) 
 
util.js 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 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 实现构建网站时保持多个浏览器和设备同步
index.js 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 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 ));const  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 ) =>  {          debouncedInit ();   }); } 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 ();    }); } init (() =>  {  server ();    watch ();  }); 
 
index.ejs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!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() 进行解压。
通过管道可以很方便的实现压缩和解压缩
压缩 1 2 3 4 5 6 7 fs.createReadStream (path.join (__dirname, 'big_data.txt' ))   .pipe (zlib.createGzip ())    .pipe (fs.createWriteStream (path.join (__dirname, 'big_data.txt.gz' ))) fs.createReadStream (path.join (__dirname, 'big_data.txt' ))   .pipe (zlib.createDeflate ())    .pipe (fs.createWriteStream (path.join (__dirname, 'big_data.txt.deflate' ))) 
 
解压 1 2 3 4 5 6 7 fs.createReadStream (path.join (__dirname, 'big_data.txt.gz' ))   .pipe (zlib.createGunzip ())    .pipe (fs.createWriteStream (path.join (__dirname, 'big_data.txt' ))) fs.createReadStream (path.join (__dirname, 'big_data.txt.deflate' ))   .pipe (zlib.createInflate ())    .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 等,但一般不会压缩图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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);    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' ); });