NodeJS超长总结 1

10/27/2018 Node

# 🌹🌹 🐱 🐶 🐭 🐘 🐳 ✈️ 🚄 🚗 ⚽️ 💆 🥚 🧒 🌹 🐯 ➡️

持续更新。。。。很多还没写呢


# 🌹 搭建node开发环境


# 🌹🌹 1、先安装一个 nvm

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.25.2/install.sh | bash

# 🌹🌹 2、安装node

easy todo

# 🌹🌹 3、npm介绍

安装

  • 本地安装
  • 全局安装 (在命令行使用)
npm install http-server -g 生成静态目录 
npm install nrm -g 切换源
npm install yarn -g 除了npm 还有安包的方式 yarn
npm uninstall yarn -g
1
2
3
4

实现全局包

  • 添加bin
  • 添加#! /usr/bin/env node
  • npm link

发包

  • 切换到官方源
  • npm addUser
  • 填上用户名邮箱 密码
  • npm publish

# 🌹 node常见概念


# 🌹🌹 进程和线程

todo

# 🌹🌹 异步和同步

todo

# 🌹🌹 阻塞和非阻塞

todo

# 🌹🌹 队列和栈 (堆)

todo

# 🌹🌹 宏任务微任务

macrotask 和 microtask 表示异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。

两个类别的具体分类如下:

macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver

# 🌹 node的模块


# 🌹🌹 1、全局模块

不需要引入 拿来即用 console.log(global)得到核心对象

# 🌹🌹🌹 console

node中console.log(this) 指向的是 module.exports, node将this != global this=module.exports node中的全局对象是console.log(global)

console.log(this); // node为了实现模块化 外边有一个闭包
// 函数外边把this更改掉了 this != global this=module.exports

console.log(global); // 可以通过直接取值的方式拿到结果 不需要声明

// console 输出
console.log('log');
console.info('info'); // 标准输出 1
process.stdout.write('hello')

console.error('错误');
console.warn('警告'); // 错误输出 2 
process.stderr.write('error');

// 监听用户的输入
process.stdin.on('data',function(data){ // 0 
    console.log(data)
});

// 代号都是文件描述符
// console.assert(1===1===1,'出错了'); // node中有一个现成的模块 assert
console.time('start');
Promise.resolve().then(()=>{
    console.timeEnd('start');
})
// console.dir(global,{showHidden:true}); // 显示隐藏的信息

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

# 🌹🌹🌹 process 进程

console.log(process)得到整个process对象

console.log(global)
console.log(process);
console.log(process.platform === 'win32');
console.log(process.argv); //运行的参数 前两个不用管
let argvs = process.argv.slice(2);
//  把argvs变成对象 {color:red,port:3000}
// [--color,'red','--port',3000];
let obj = {}
argvs.forEach((element, index) => {
    if (element.includes('--')) {
        obj[element.slice(2)] = argvs[index + 1]
    }
});
console.log(obj); // 解析用户传递的参数


console.log(process.env.DEBUG); // 环境 变量
if (process.env.NODE_ENV === 'development') {
    console.log('当前是开发环境 ')
} else {
    console.log('上线环境 ')
}
// console.log(process.pid);  
// 在哪里运行的
console.log(process.chdir('1.node')); //改变工作目录
console.log(process.cwd()); // 当前工作目录 读取文件默认从跟文件夹下读取 (注意的点)
//  console.log(process.nextTick());
// nextTick > then

// process.kill(20352); // 杀死进程
// process.exit(2); // 退出进程,退出自己

// process进程
//   argv 执行参数
//   env 环境变量
//   pid 当前进程id
//   chdir/cwd chdir可以改变执行的工作目录 cwd代表的时当前目录
//   nextTick 下一队列
//   stdout
//   stderr
//   stdin
//   kill
//   exit 

Promise.resolve().then(() => {
    console.log('then')
})
// 
process.nextTick(() => {
    console.log('nextTick')
});

// nextTick 等所有同步任务执行玩立刻执行的
class A {
    constructor() {
        this.arr = [];
        console.log(this.arr); // []
        process.nextTick(()=>{ 
            console.log(this.arr); // ['123','456']
        })
    }
    add(val) {
        this.arr.push(val);
    }
}
let a = new A();
a.add('123');
a.add('456');

// node的事件环

// Buffer 缓存区 二进制 /  16进制
// clearImmediate / setImmediate node实现的

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

# 🌹🌹🌹 _filenane 和 _dirname

不是global上的属性 _filenane当前执行文件的绝对路径
_dirname当前文件所在文件夹的绝对路径

# 🌹🌹 2、核心模块

不需要安装,引入即用

# 🌹🌹🌹 path

专门用来处理路径 后缀名 路径的信息 1、path.join([...paths])

let path = require('path');
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// 返回: '/foo/bar/baz/asdf'
path.join('foo', {}, 'bar');
// 抛出 'TypeError: Path must be a string. Received {}'
console.log(path.dirname(__dirname)); // 父路径
console.log(__dirname);
1
2
3
4
5
6
7

2、path.resolve() path.resolve() 方法会把一个路径或路径片段的序列解析为一个绝对路径。 给定的路径的序列是从右往左被处理的,后面每个 path 被依次解析,直到构造完成一个绝对路径

path.resolve('/foo/bar', './baz');
// 返回: '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/');
// 返回: '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果当前工作目录为 /home/myself/node,
// 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'
1
2
3
4
5
6
7
8
9

3、path.basename() path.basename() 方法返回一个 path 的最后一部分,类似于 Unix 中的 basename 命令。 没有尾部文件分隔符

path.basename('/foo/bar/baz/asdf/quux.html');
// 返回: 'quux.html'

path.basename('/foo/bar/baz/asdf/quux.html', '.html');
// 返回: 'quux'
1
2
3
4
5

4、path.extname() path.extname() 方法返回 path 的扩展名,即从 path 的最后一部分中的最后一个 .(句号)字符到字符串结束。 如果 path 的最后一部分没有 . 或 path 的文件名(见 path.basename())的第一个字符是 .,则返回一个空字符串。

path.extname('index.html');
// 返回: '.html'

path.extname('index.coffee.md');
// 返回: '.md'

path.extname('index.');
// 返回: '.'

path.extname('index');
// 返回: ''

path.extname('.index');
// 返回: ''
1
2
3
4
5
6
7
8
9
10
11
12
13
14

4、path.dirname() path.dirname() 方法返回一个 path 的目录名,类似于 Unix 中的 dirname 命令

path.dirname('/foo/bar/baz/asdf/quux');
// 返回: '/foo/bar/baz/asdf'
console.log(__dirname);
1
2
3

# 🌹🌹🌹 vm 核心模块

vm 模块提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。 JavaScript 代码可以被编译并立即运行,或编译、保存然后再运行.

让字符串执行的方法:

// 1)让字符串执行
let a = 100; // 不干净的执行
eval('console.log(a)'); // 沙箱

// 2)让字符串执行
let str = 'console.log(a)'
let fn = new Function('a',str); // 模板引擎
fn(1);

// 3) node执行字符串
let vm = require('vm');
let str = 'console.log(a)';
vm.runInThisContext(str);

// runInThisContext fs.readFileSync fs.existsSync path.join resolve extname basename
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 🌹🌹🌹 net

# 🌹🌹🌹 http

# 🌹🌹🌹 fs

# 🌹🌹🌹 http

# 🌹🌹🌹 querystring

# 🌹🌹🌹 events

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('触发了一个事件!');
});
myEmitter.emit('event');
1
2
3
4
5
6
7
8
9

# 🌹🌹🌹 util

const util = require('util');

# 🌹🌹 3、第三方模块

需要安装引入

# 🌹 EventLoop


setTimeout(() => {
    console.log('timeout1');
    process.nextTick(()=>{ //在当前执行
        console.log('nextTick1');
    })
}, 1000);
// 虽然都写1000毫秒 是有误差
process.nextTick(function(){ // nextTick执行的时候用了0.8ms
    setTimeout(() => {
        console.log('timeout2'); 
    }, 1000); // 1000.02ms
    console.log('nextTick2');
})
// nextTick2 timeout1 timeout2 nextTick1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(()=>{ // 微
        console.log('then1')
    })
},1000)

// 微任务
Promise.resolve().then(function(){
    console.log('then2')
    setTimeout(function(){ // 宏
        console.log('setTimeout2')
    },1000)
})
// node中:then2 setTimeout1 setTimeout2 then1
// 浏览器中:then2 setTimeout1 then1 setTimeout2 
// setTimeout时间设为0会出现两种情况 由于设备性能引起的差距
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

不确定的:

setImmediate(function(){
    console.log('setImmediate')
});
setTimeout(function(){
    console.log('setTimeout')
},0); // ->4
1
2
3
4
5
6

确定的: io操作后先执行check

//  
let fs = require('fs');
fs.readFile('./gitignore',function(){ // io的下一个事件队列是check阶段
    setImmediate(function(){
        console.log('setImmediate')
    });
    setTimeout(function(){
        console.log('setTimeout')
    },0); // ->4
})
// 
let fs = require('fs');
setTimeout(function(){
    Promise.resolve().then(()=>{
        console.log('then2');
    })
},0);
Promise.resolve().then(()=>{
    console.log('then1');
});
fs.readFile('./gitigore',function(){
    process.nextTick(function(){
        console.log('nextTick')
    })
    setImmediate(()=>{
        console.log('setImmediate')
    });
});
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

# 🌹 模块


# 🌹🌹 介绍

  • 方便维护 方便管理 代码统一

  • 前端模块 (网络的问题)

  • 模块加载是同步的

  • cmd seajs amd requirejs umd 统一模块规范

  • 自己实现模块化 let obj = {} 单例

  • 闭包 let fn = (function(){return {}});

  • esModule es6的模块化

  • commonjs规范 node (原理 闭包的形式)

    • 把文件读出来,套一个函数,安装规范来写,把需要导出的结果放到指定的地方
    • 别人可以拿到这个函数执行,拿到你导出的东西而已
    • 引用的过程是同步的
  • node模块分类 核心模块/内置模块 、 第三方模块 bluebird 、文件模块、自己写的模块 (fs,path);

# 🌹🌹 实现common规范

  • 如何导入模块 require
  • 导出模块 module.exports = this
  • 如何定义模块1个文件就是一个模块

* 模块的加载顺序
  
````javascript   
console.log(module.paths);
// 当前目录下找 node_modules,然后逐级网上找,直到找到根目录 node_modules
1
2
3
4
5
6
  • exportsmodule.exports
// a.js 导出
let a = 1
let b = 2
exports.a = 1 //单个导出
// module.exports = {a,b} //多个导出

// b.js 导入
let s = requiue('./a')
console.log(s)
1
2
3
4
5
6
7
8
9

node中通过require实现模块加载

同步的 1、找到这个文件 2、读取此文件内容 3、把他封装在一个函数內执行(function (exports,require,module,filename,dirname){ // 执行代码 }) exports: 当前模块的导出对象 require: requiue方法 module: 当前模块 filename: 当前模块文件的绝对路径 dirname:当前模块文件夹的绝对路径 4、执行后把模块的module.exports赋值给requie的变量

console.log(module) 打印当前模块信息:

Module {
  id: '.', // 模块id永远为当前模块 入口模块
  exports: {}, // 导出对象 默认{}
  parent: null,  // 父模块 此模块是有哪个模块加载的
  filename: // 当前模块的觉得路径
   '/Users/bingyang/Documents/zhufeng/201805coding/tempCodeRunnerFile.js',
  loaded: false, // 是否加载完成
  children: [], //此模块加载了那些模块
  paths: // 模块加载查找路径
   [ '/Users/bingyang/Documents/zhufeng/201805coding/node_modules',
     '/Users/bingyang/Documents/zhufeng/node_modules',
     '/Users/bingyang/Documents/node_modules',
     '/Users/bingyang/node_modules',
     '/Users/node_modules',
     '/node_modules' ] 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

多次加载模块只会执行一次 有缓存

// a.js
let a = 1
console.log('加载模块a') 
module.exports = a
// b.js
var a = requiue('./a');
var a = requiue('./a'); // 导入两次只会执行一次
1
2
3
4
5
6
7

自己实现一个requiue加载函数

let path = require('path');
let fs = require('fs');
let vm = require('vm');
function Module(filename) {
  this.loaded = false;
  this.filename = filename; // 文件的绝对路径
  this.exports = {} // 模块对应的导出结果
}
Module._extensions = ['.js','.json'];

Module._resolveFilename = function (p) {
  p = path.join(__dirname,p); // c://xx/a
  if(!/\.\w+$/.test(p)){
    // 尝试添加扩展名
    for(let i = 0;i<Module._extensions.length;i++){
      let filePath = p + Module._extensions[i];// 拼出一个路径
      // 判断文件是否存在
      try{
        fs.accessSync(filePath);
        return filePath;
      }catch(e){
        if (i >= Module._extensions.length){
          throw new Error('module not Found')
        }
      }
    }
  }else{
    return p
  }
}
Module._cache = {};
Module._extensions['.json'] = function (module) {
  let content = fs.readFileSync(module.filename,'utf8');
  module.exports = JSON.parse(content)
}

Module.wrapper = ['(function (exports,require,module){','\r\n})'];
Module.wrap = function (content) {
  return Module.wrapper[0] + content + Module.wrapper[1];
}
Module._extensions['.js'] = function (module) {
  let content = fs.readFileSync(module.filename, 'utf8');
  let script = Module.wrap(content);
  let fn = vm.runInThisContext(script);
  // module.exports = exports = {}
  // exports.a = 'hello world'
  fn.call(module.exports, module.exports, req, module);
}
Module.prototype.load = function () {
  // 加载模块本身 js按照js加载 json按照json加载
  let extname = path.extname(this.filename);
  Module._extensions[extname](this);
}
function req(path) { // 自己实现的require方法 可以加载模块
  // 先要根据路径 变出一个绝对路径
  let filename = Module._resolveFilename(path);
  // 文件路径 (绝对路径) 唯一
  if (Module._cache[filename]){
    // 如果加载过直接把加载的结果返回即可
    // 有缓存把exports属性导出即可
    return Module._cache[filename].exports;
  }
  // 通过这个文件名创建一个模块
  let module = new Module(filename);
  module.load(); // 让这个模块进行加载 根据不同的后缀加载不同的内容
  Module._cache[filename] = module; // 进行模块的缓存
  // 返回最后的结果
  return module.exports;
}
// module.exports exports 有什么关系
let str = req('./b.js');
str = req('./b.js');
console.log(str);

// 多次require只会执行一次 cache

//1.写一个自己的require方法
//2.Moudle是一个类
//3.要实现一个Module._load方法实现模块的加载
//4.Module._resolveFilename 解析路径的
//5.Module._cache加载的缓存
//6.创建一个模块
//7.放到缓存中
//8.Module._extensions
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

# 🌹 npm发包

# 🌹🌹 实现全局包

1、添加bin 在package.json文件下添加 指令

  "bin": {
    "icey": "bin/www"
  },
1
2
3

2、创建一个 wwww文件,无后缀名,添加 #! /usr/bin/env node

#! /usr/bin/env node
console.log('我很帅1');
console.log(process.argv.slice(2));
1
2
3

3、sudo npm link

# 🌹🌹 发包

  • 切换到官方源
  • npm addUser
  • 填上用户名邮箱 密码 邮箱注意要验证
  • npm publish

# 🌹 发布订阅模式

# 🌹🌹 EventEmitter模块实现

// 发布订阅  on 订阅 emit 发布
let EventEmitter = require('events');
// let util = require('util'); 
// function Girl(){
// }
// util.inherits(Girl,EventEmitter);
// 注意,不建议使用 util.inherits()。 请使用 ES6 的 class 和 extends 关键词获得语言层面的继承支持
class Girl extends EventEmitter {} // class 继承
let girl = new Girl;
function cry(){
    console.log('cry')
}
function eat(){
    console.log('eat')
}
function shopping(){
    console.log('shopping')
}
girl.on('失恋了',cry);
girl.on('失恋了',cry);
girl.on('失恋了',eat);
girl.on('失恋了',shopping);

girl.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

# 🌹🌹 其他用法 on once prependListener

let EventEmitter = require('./events');

let e = new EventEmitter();

let eat = function(){
    console.log('吃')
}
let cry = function(){
    console.log('哭')
}
let haha = function(){
    console.log('笑')
}
// 可以监听用户新邦定的事件
// e.on('newListener',function(type){
//     process.nextTick(()=>{
//         e.emit(type)
//     })
// }); 
e.once('失恋',haha); // once this.on('失恋',one)
e.on('失恋',cry); // once this.on('失恋',one)
e.prependListener('失恋',eat); // 添加到之前
// e.removeListener('失恋',cry);
e.emit('失恋'); // 触发完成后就将数组的once绑定的函数移除掉
e.emit('失恋'); // 触发完成后就将数组的once绑定的函数移除掉
// console.log(EventEmitter.defaultMaxListeners)
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

# 🌹🌹 自己实现

# 🌹🌹🌹 简单版本

function EventEmitter() {
    this._events = {};
}
// {失恋:[fn,fn]}
EventEmitter.prototype.on = function (eventName, callback) {
    if (!this._events) this._events = Object.create(null);
    if (this._events[eventName]) {
        this._events[eventName].push(callback);
    } else {
        this._events[eventName] = [callback];
    }
}
EventEmitter.prototype.emit = function (eventName) {
    if (this._events[eventName]) {
        this._events[eventName].forEach(fn => {
            fn();
        });
    }
}

module.exports = EventEmitter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 🌹🌹🌹 终极版本

function EventEmitter() {
    this._events = {};
    this.count = 0;
}
// {失恋:[fn,fn]}
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
EventEmitter.prototype.eventNames = function () {
    return Object.keys(this._events);
}
EventEmitter.prototype.listeners = function (eventName) {
    return this._events[eventName]
}
EventEmitter.prototype.on = function (eventName, callback, flag) {
    if (!this._events) this._events = Object.create(null);
    if (eventName !== 'newListener') {
        (this._events['newListener'] || []).forEach(fn => fn(eventName))
    }
    if (this._events[eventName] === this.count) {
        console.log('MaxListenersExceededWarning');
    }
    if (this._events[eventName]) {
        if (flag) {
            this._events[eventName].unshift(callback);
        } else {
            this._events[eventName].push(callback);
        }
    } else {
        this._events[eventName] = [callback];
    }
}
EventEmitter.prototype.once = function (eventName, callback,flag) {
    function one() {
        callback();
        this.removeListener(eventName, one);
    }
    one.g = callback;
    // {失恋:[one]}
    this.on(eventName, one,flag);
}
EventEmitter.prototype.removeListener = function (eventName, listener) {
    this._events[eventName] = this._events[eventName].filter((fn) => {
        return fn != listener && fn.g !== listener;
    })
}
EventEmitter.prototype.prependListener = function (eventName, callback) {
    this.on(eventName, callback, true);
}
EventEmitter.prototype.prependOnceListener = function(eventName, listener){
    this.once(eventName,call,true);
}
EventEmitter.defaultMaxListeners = 10;
EventEmitter.prototype.getMaxListeners = function () {
    return this.count && EventEmitter.defaultMaxListeners;
}
EventEmitter.prototype.setMaxListeners = function (n) {
    return this.count = n;
}
EventEmitter.prototype.emit = function (eventName) {
    if (this._events[eventName]) {
        this._events[eventName].forEach(fn => {
            fn.call(this);
        });
    }
}
module.exports = EventEmitter;
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

# 🌹 编码


# 🌹🌹 编码基础知识

1、字节

  • 计算机内部,所有信息最终都是一个二进制
  • 每个二进制 位 bit 有 0 和 1 两种状态,
  • 8个二进制位就可以组合出256种状态,这被成为一个字节 byte

2、单位

  • 8位(bit) = 1 字节 (byte)
  • 1024字节 = 1K
  • 1024K = 1M
  • 1024M = 1G
  • 1024G = 1T
  • 1个字节,最大255,一个汉字三个字节

# 🌹🌹 JavaScript中的进制

1、进制表示

let a = 0b10100;//二进制
let b = 0o24;//八进制
let c = 20;//十进制
let d = 0x14;//十六进制
console.log(a == b);
console.log(b == c);
console.log(c == d);
1
2
3
4
5
6
7

2、进制转换

  • 十进制转任意进制 .toString(目标进制)
console.log(c.toString(2));
1
  • 任意进制转十进制 parseInt('任意进制字符串', 原始进制)
console.log(parseInt('10100', 2));
1

# 🌹🌹 ASCII

最开始计算机只在美国用,八位的字节可以组合出256种不同状态。0-32 种状态规定了特殊用途,一旦终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作如:

  • 遇上 0×10, 终端就换行
  • 遇上 0×07, 终端就向人们嘟嘟叫

又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第 127 号,这样计算机就可以用不同字节来存储英语的文字了. 这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0

American Standard Code for Information Interchange:美国信息互换标准代码

# 🌹🌹 GB2312

后来西欧一些国家用的不是英文,它们的字母在 ASCII 里没有为了可以保存他们的文字,他们使用127号这后的空位来保存新的字母,一直编到了最后一位 255。比如法语中的é的编码为 130。当然了不同国家表示的符号也不一样,比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel (ג)

从128 到 255 这一页的字符集被称为扩展字符集。

中国为了表示汉字,把127号之后的符号取消了,规定

  • 一个小于127的字符的意义与原来相同,但两个大于 127 的字符连在一起时,就表示一个汉字;
  • 前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从 0xA1 到 0xFE;
  • 这样我们就可以组合出大约 7000 多个 (247-161)*(254-161)=(7998) 简体汉字了。
  • 还把数学符号、日文假名和ASCII里原来就有的数字、标点和字母都重新编成两个字长的编码。这就是全角字符,127 以下那些就叫半角字符。
  • 把这种汉字方案叫做 GB2312。GB2312 是对 ASCII 的中文扩展

# 🌹🌹 GBK

后来还是不够用,于是干脆不再要求低字节一定是 127 号之后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,又增加了近 20000 个新的汉字(包括繁体字)和符号。

# 🌹🌹 GB18030 / DBCS

又加了几千个新的少数民族的字,GBK扩成了GB18030 通称他们叫做 DBCS Double Byte Character Set:双字节字符集。 在 DBCS 系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里.各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码

# 🌹🌹 Unicode

ISO 的国际组织废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符 的编码! Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。

  • International Organization for Standardization:国际标准化组织。
  • Universal Multiple-Octet Coded Character Set,简称 UCS,俗称 Unicode

ISO 就直接规定必须用两个字节,也就是 16 位来统一表示所有的字符,对于 ASCII 里的那些 半角字符,Unicode 保持其原编码不变,只是将其长度由原来的 8 位扩展为16 位,而其他文化和语言的字符则全部重新统一编码。
从 Unicode 开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的一个字符!同时,也都是统一的 两个字节

  • 字节是一个8位的物理存贮单元,
  • 而字符则是一个文化相关的符号。

# 🌹🌹 UTF-8

Unicode 在很长一段时间内无法推广,直到互联网的出现,为解决 Unicode 如何在网络上传输的问题,于是面向传输的众多 UTF 标准出现了,

Universal Character Set(UCS)Transfer Format:UTF编码

  • UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式
  • UTF-8 就是每次以8个位为单位传输数据
  • 而 UTF-16 就是每次 16 个位
  • UTF-8 最大的一个特点,就是它是一种变长的编码方式
  • Unicode 一个中文字符占 2 个字节,而 UTF-8 一个中文字符占 3 个字节
  • UTF-8 是 Unicode 的实现方式之一

# 🌹🌹 编码规则

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
  2. 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n+ 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
Unicode符号范围     |        UTF-8编码方式
(十六进制)        |              (二进制)
----------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1
2
3
4
5
6
7

Unicode编码 (opens new window)

function transfer(num) {
  let ary = ['1110', '10', '10'];
  let binary = num.toString(2);
  ary[2] = ary[2]+binary.slice(binary.length-6);
  ary[1] = ary[1]+binary.slice(binary.length-12,binary.length-6);
  ary[0] = ary[0]+binary.slice(0,binary.length-12).padStart(4,'0');
  let result =  ary.join('');
  return parseInt(result,2).toString(16);
}
//万
let result = transfer(0x4E07);//E4B887

1
2
3
4
5
6
7
8
9
10
11
12

# 🌹🌹 文本编码

使用NodeJS编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问题。我们常用的文本编码有UTF8和GBK两种,并且UTF8文件还可能带有BOM。在读取不同编码的文本文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理。

# 🌹🌹 移除BOM头

BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符("\uFEFF"),位于文本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下:

   Bytes      Encoding
   ----------------------------
   FE FF       UTF16BE
   FF FE       UTF16LE
   EF BB BF    UTF8
1
2
3
4
5

因此,我们可以根据文本文件头几个字节等于啥来判断文件是否包含BOM,以及使用哪种Unicode编码。但是,BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一部分,如果读取文本文件时不去掉BOM,在某些使用场景下就会有问题。例如我们把几个JS文件合并成一个文件后,如果文件中间含有BOM字符,就会导致浏览器JS语法错误。因此,使用NodeJS读取文本文件时,一般需要去掉BOM

移除BOM头:

function readText(pathname) {
    var bin = fs.readFileSync(pathname);
    if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
        bin = bin.slice(3);
    }
    return bin.toString('utf-8');
}
1
2
3
4
5
6
7

# 🌹🌹 GBK转UTF8

NodeJS支持在读取文本文件时,或者在Buffer转换为字符串时指定文本编码,但遗憾的是,GBK编码不在NodeJS自身支持范围内。因此,一般我们借助iconv-lite这个三方包来转换编码。使用NPM下载该包后,我们可以按下边方式编写一个读取GBK文本文件的函数。

var iconv = require('iconv-lite');
function readGBKText(pathname) {
    var bin = fs.readFileSync(pathname);
    return iconv.decode(bin, 'gbk');
}
1
2
3
4
5

# 🌹 Buffer


# 🌹🌹 什么是Buffer

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

# 🌹🌹 Buffer声明的方式

# 1 通过长度定义buffer

// 创建一个长度为 10、且用 0 填充的 Buffer。
const buf1 = Buffer.alloc(10);
// 创建一个长度为 10、且用 0x1 填充的 Buffer。
const buf2 = Buffer.alloc(10, 1);
// 创建一个长度为 10、且未初始化的 Buffer。
const buf3 = Buffer.allocUnsafe(10);
1
2
3
4
5
6

# 2 通过数组定义buffer

// 创建一个包含 [0x1, 0x2, 0x3] 的 Buffer。
const buf4 = Buffer.from([1, 2, 3]);
1
2

# 3 字符串创建buffer

const buf5 = Buffer.from('王冰洋');
1

默认情况下 Buffer不支持 gbk编码;gbk -> utf8 iconv-lite可以处理乱码

let fs = require('fs');
let r = fs.readFileSync('./1.txt');
let iconvLite = require('iconv-lite');
r = iconvLite.decode(r,'gbk'); 
console.log(r);
1
2
3
4
5

# 🌹🌹 常用的方法

# 1 buf.fill(value[, offset[, end]][, encoding])

手动初始化,擦干净桌子,将buffer内容清0

buffer.fill(0);
1

# 2 write方法

buf.write(string[, offset[, length]][, encoding])

buffer.write('冰',0,3,'utf8');
buffer.write('洋',3,3,'utf8'); // 冰洋
1
2

# 3 writeInt8

var buf = new Buffer(4);
buf.writeInt8(0,0);
buf.writeInt8(16,1);
buf.writeInt8(32,2);
buf.writeInt8(48,3);//16*3*/
console.log(buf);
console.log(buf.readInt8(0));
console.log(buf.readInt8(1));
console.log(buf.readInt8(2));
console.log(buf.readInt8(3));
1
2
3
4
5
6
7
8
9
10

# 4 Little-Endian&Big-Endian

不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序。

  • Big-endian:将高序字节存储在起始地址(高位编址)
  • Little-endian:将低序字节存储在起始地址(低位编址)
let buf3 = new Buffer(4);
buf3.writeInt16BE(2**8,0);
console.log(buf3);//<Buffer 01 00 00 00>
console.log(buf3.readInt16BE(0));

buf3.writeInt16LE(2**8,2);
console.log(buf3);//<Buffer 01 00 00 01>
console.log(buf3.readInt16LE(2));
1
2
3
4
5
6
7
8

# 5 toString()

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

let buf4 = new Buffer(6);
buf4.toString('utf8',3,6)
1
2

# 6 slice()

buf.slice([start[,end]])

let newBuf = buffer.slice(0,4);
1

# 7 截取乱码问题

let {StringDecoder}  = require('string_decoder');
let sd = new StringDecoder();
let buffer = new Buffer('冰洋');
console.log(sd.write(buffer.slice(0,4)));
console.log(sd.write(buffer.slice(4)));
1
2
3
4
5

# 7 copy方法

复制Buffer 把多个buffer拷贝到一个大buffer上 buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])

let buf5 = Buffer.from('王冰洋啊');
let buf6 = Buffer.alloc(6);
buf5.copy(buf6,0,0,4);
buf5.copy(buf6,3,3,6);
//buf6=王冰
1
2
3
4
5

自己实现 copy

// copy 拷贝
Buffer.prototype.copy = function(targetBuffer,targetStart,sourceStart,SourceEnd){
    sourceStart = sourceStart?sourceStart:0
    SourceEnd = SourceEnd? SourceEnd:this.length
    for(let i=sourceStart;i<SourceEnd;i++){
        // 把内容考到对应的buffer的身上
        targetBuffer[targetStart++] = this[i];
    }
}
1
2
3
4
5
6
7
8
9

# 7 concat方法

Buffer.concat(list[, totalLength])

let buf1 = Buffer.from('王);
let buf2 = Buffer.from('冰洋');
let newBuffer = Buffer.concat([buf1,buf2,buf1]);
console.log(newBuffer.toString());
1
2
3
4

自己实现 concat

// concat 连接
Buffer.concat = function(bufferArray,len){
    len =typeof len === 'undefined'?bufferArray.reduce((prev,next,current)=>prev+next.length,0)  : len;
    // 计算出一个大的buffer来
    let buffer = Buffer.alloc(len);
    let pos = 0;
    for(let i = 0;i<bufferArray.length;i++){
        // 把数组里的每一个buffer全部拷贝上去
        bufferArray[i].copy(buffer,pos);
        // 每次拷贝后累加自身的长度
        pos += bufferArray[i].length;
    }
    return buffer;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

🌰 indexof

// 2 indexof
Buffer.from('冰洋洋冰火').indexOf('洋',6) // buf 中 value 首次出现的索引,如果 buf 没包含 value 则返回 -1 
1
2

# 🌹🌹 base64

  • Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。
  • Base64要求把每三个8Bit的字节转换为四个6Bit的字节(38 = 46 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3
const CHARTS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function transfer(str){
  let buf = Buffer.from(str);
  let result = '';
  for(let b of buf){
      result += b.toString(2);
  }
    return result.match(/(\d{6})/g).map(val=>parseInt(val,2)).map(val=>CHARTS[val]).join('');
}
let r = transfer('珠');//54+g
1
2
3
4
5
6
7
8
9
10

# 🌹 fs


# 🌹🌹 fs模块

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

# 🌹🌹 读文件 readFile + readFileSync

  • 方法都是 异步没有sync / 同步 Sync
  • 返回值可以获取同步的结果
  • 读取文件默认的结果类型 encoding:null 默认是buffer
  • 如果文件不存在则会报错
  • 读取的时候会把内容整体读取到内存中 读小的文件),大的文件流操作
  • 异步 error-first
let fs = require('fs');
let path = require('path');
// 同步
let r = fs.readFileSync(path.join(__dirname,'note.md'),{encoding:'utf8',flag:'r'});
console.log(r);
// 异步
fs.readFile(path.join(__dirname,'note.md'),'utf8',function(err,data){ // 回调中的第一个参数 永远是错误
    console.log(data);
});
console.log(r);
1
2
3
4
5
6
7
8
9
10

# 🌹🌹 写文件 writeFile + writeFileSync + appendFile

  • 写入时默认文件存在就创建,有文件的话 会被清空
  • 写入时 他会把内容以二进制的形式写入进去
let fs = require('fs');
// 同步 fs.writeFileSync(file, data[, options])
fs.writeFileSync('./1.txt',Date.now()+'\n',{flag:'a'});
// 异步 fs.writeFile(file, data[, options], callback)
fs.writeFile(path.join(__dirname,'1.txt'),'{data:1}',function(err){
    console.log('成功')
});
// 追加 fs.appendFile(file, data[, options], callback)
fs.appendFile(path.join(__dirname,'1.txt'),'{data:1}',function(err){
    console.log('成功')
});
1
2
3
4
5
6
7
8
9
10
11

# 🌹🌹 拷贝文件

1、直接 读readFile 再写入 writeFile

// 拷贝方法  不能读一点写一点,想指定位置读取
fs.readFile(path.join(__dirname,'1.txt'),(err,data)=>{
    fs.writeFile(path.join(__dirname,'2.txt'),data,(err)=>{
        console.log('拷贝成功')
    })
});
1
2
3
4
5
6

# 🌹🌹 open

fs.open(filename,flags,[mode],callback);

mode:权限 r 4 w 2 x 1 chmod 777 666 0o438 callback:(err,fd)=>{} fd:file descriptor 文件描述符(数字类型) ReadStream 使用的整数型文件描述符 process.stdin 0 process.stdout 1 process.stderr 2

let fs = require('fs');
let path = require('path');
//
fs.open(path.join(__dirname,'1.txt'),'w',(err,fd)=>{
    // todo
})
1
2
3
4
5
6

# 🌹🌹 写入指定内容 open + write

fs.open(path.join(__dirname,'1.txt'),'w',(err,fd)=>{
    let buf = Buffer.from('王冰洋');
    // buf 指的是读取的buffer
    // 0 从buffer哪个位置读取 
    // 6 读取多少个buffer往里写
    // 0 从文件哪个位置写入
    // bytesWritten实际写入的个数
    fs.write(fd,buf,0,4,0,(err,bytesWritten)=>{
        console.log('写入成功')
    })
})

1
2
3
4
5
6
7
8
9
10
11
12

# 🌹🌹 读取指定内容 open + read

fs.open(path.join(__dirname,'1.txt'),'r',(err,fd)=>{
    let buffer = Buffer.alloc(5);
    /**
     * fd文件,描述符
     * buffer 读取到那个buffer中
     * 0 从buffer哪个地方开始写入
     * 5 写入多长
     * 1 从文件的那个位置开始读取
     * bytesRead实际读取到的个数
     */
    fs.read(fd,buffer,0,4,1,(err,bytesRead)=>{
        console.log(buffer.toString());
        fs.close(fd,()=>{
            console.log('关闭')
        })
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 🌹🌹 拷贝指定位置 open + read + write

let fs = require('fs');
let path = require('path');
// 1.txt => 2.txt
// 1.准备打开 1.txt 和 2.txt
const BUFFER_SIZE = 5;
let readPos = 0;
let writePos = 0;
// 异步的递归是如何操作的 
fs.open(path.join(__dirname, '1.txt'), 'r', (err, rfd) => {
    fs.open(path.join(__dirname, '2.txt'), 'w', (err, wfd) => {
        // 
        function next() {
            let buf = Buffer.alloc(BUFFER_SIZE); // 申请读出来的buffer的长度
            fs.read(rfd, buf, 0, BUFFER_SIZE, null, (err, byteRead) => {
                if (byteRead > 0) {
                    // 写入读取到的个数 可能想读10个 但是只有5个
                    readPos += byteRead
                    fs.write(wfd, buf, 0, byteRead, null, (err, byteWritten) => {
                        writePos += byteWritten;
                        next();
                    });
                }else{
                    fs.close(rfd,()=>{ });
                    // 读取完毕 不一定表示写入完毕 
                    fs.fsync(wfd,()=>{
                        fs.close(wfd,()=>{})
                    });
                }
            })
        }
        // 
        next();
    });
});


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

# 🌹🌹 目录操作

# 🌹🌹🌹 创建目录

fs.mkdir(path[, mode], callback) 要求父目录必须存在

  • 判断一个文件是否有权限访问

fs.access(path[, mode], callback)

fs.access('/etc/passwd', fs.constants.R_OK | fs.constants.W_OK, (err) => {
  console.log(err ? 'no access!' : 'can read/write');
});
1
2
3
  • 广度优先遍历

异步创建多级目录 async + await

let fs = require('fs');
let util = require('util');
let access = util.promisify(fs.access); // promisify将改io操作promise化
let mkdir = util.promisify(fs.mkdir);
async function makep(p) {
    let paths = p.split('/');
    for (let i = 0; i < paths.length; i++) {
        let dirPath = paths.slice(0, i + 1).join('/');
        try{
            await access(dirPath)
        }catch(e){
            await mkdir(dirPath);
        }
    }
}
makep('a/b/c/d/e').then(data => {
    console.log('创建成果')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

同步创建多级目录

function makep(p) {  // 同步创建目录
    let paths = p.split('/');
    for (let i = 0; i < paths.length; i++) {
        let dirPath = paths.slice(0,i+1).join('/');
        try{
            // 如果能访问到 不干任何事 ,访问不到才创建
            fs.accessSync(dirPath);
        }catch(e){
            fs.mkdirSync(dirPath);
        }
    }
}
makep('a/b/c/d/e');
1
2
3
4
5
6
7
8
9
10
11
12
13

递归创建多级目录 异步

function makep(p,fn){
    let paths = p.split('/');
    let index = 0;
    function next(){
        if(index ===paths.length ) return fn();
        let realPath = paths.slice(0,++index).join('/');
        // 如果文件无法访问到 那就说明文件不存在则创建 反过来如果文件 存在就创建一下个
        fs.access(realPath,(err)=>{
            if(err){
                fs.mkdir(realPath,(err)=>{
                    next();
                });
            }else{
                next();
            }
        })
    }
    next();
}
makep('e/d/e/g/s/q',()=>{
    console.log('ok')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 🌹🌹🌹 查看文件目录信息

fs.stat(path, callback)

stats.isFile() stats.isDirectory() atime(Access Time)上次被读取的时间。 ctime(State Change Time):属性或内容上次被修改的时间。 mtime(Modified time):档案的内容上次被修改的时间。

# 🌹🌹🌹 移动文件或目录

fs.rename(oldPath, newPath, callback)

# 🌹🌹🌹 读取目录下所有文件

fs.readdir(path[, options], callback)

# 🌹🌹🌹 截断文件

fs.ftruncate(fd[, len], callback)

const fd = fs.openSync('temp.txt', 'r+');
// 截断文件至前4个字节
fs.ftruncate(fd, 4, (err) => {
  console.log(fs.readFileSync('temp.txt', 'utf8'));
});
1
2
3
4
5

# 🌹🌹🌹 删除目录

rmdir

# 🌹🌹🌹 删除文件

fs.unlink(path, callback)

# 🌹🌹🌹 删除非空目录

rmdirSync rmdir unlink

# 🌹🌹🌹🌹 简单实现

// 如果删除一个文件夹 先读取出 文件夹的内容fs.readdir
// 判断当前这个路径是文件夹还是文件,文件的状态 fs.stat
// statObj.isDirectory  statObj.isFile
// fs.rmdir 删除目录    fs.unlink 删除文件
let fs  =require('fs');
let path = require('path');
fs.readdir('a',function (err,files) {
  let paths = files.map(dir => path.join('a', dir))
  console.log(paths);
  paths.forEach(p=>{
    fs.stat(p,(err,statObj)=>{
      if (statObj.isDirectory()){
        fs.rmdir(p)
      }else{
        fs.unlink(p)
      }
    });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 🌹🌹🌹🌹 先序广度遍历

let fs  =require('fs');
let path = require('path');
// 先序广度遍历
function rmdirSync(dir){
    let arr = [dir]; // 创建一个记录表
    let index = 0 ; // 从记录表里拿出第一项 a
    let current; // a
    while(current = arr[index++]){
        let dirsPath = fs.readdirSync(current); // [b,c]
        dirsPath = dirsPath.map(item=> path.join(current,item)); // => [a/b,a/c]
        arr = [...arr,...dirsPath] // => [a,a/b,a/c]
    };
    for(let i =arr.length-1;i>=0;i--){
        fs.rmdirSync(arr[i]);
    }
}
rmdirSync('a');
// 用传统的回调 fs.readdir  fs.rmdir 实现广度删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 🌹🌹🌹🌹 Promise版

// 1、异步primise 深度优先 删除
let {promisify} = require('util'); // async - > await
let fs = require('mz/fs');
async function removePromise(dir) {
    let statObj = await fs.stat(dir);
    if (statObj.isDirectory()) {
      let files = await fs.readdir(dir);
      files = files.map(file => removePromise(path.join(dir, file)));
      await Promise.all(files); // 删除儿子
      await fs.rmdir(dir);// 删除自己
    } else {
      await fs.unlink(dir);
    }
}
removePromise('a').then(()=>{
  console.log('删除成功');
},err=>{
  console.log(err);
})
// 2、版本2
function removePromise(dir) {
    return new Promise((resolve,reject)=>{
      fs.stat(dir,(err,statObj)=>{
        if(statObj.isDirectory()){
          fs.readdir(dir,(err,files)=>{
            files = files.map(file=>path.join(dir,file));
            // [a/b,a/c,a/1.js]
            // 等待儿子删除后 删除自己
            Promise.all(files.map(file =>removePromise(file))).then(()=>{
              fs.rmdir(dir,resolve);
            });
          })
        }else{
          // 文件删除后 成功即可
          fs.unlink(dir,resolve);
        }
      })
    });
}
removePromise('a').then(()=>{
  console.log('删除ok');
})
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

# 🌹🌹🌹🌹 并行

function removeDir(dir, cb) {
    fs.stat(dir,(err,statObj)=>{
      if (statObj.isDirectory()){
        fs.readdir(dir,(err,files)=>{
          let paths = files.map(file=>path.join(dir,file));
          // 获取每一个路径
          if(paths.length>0){
            let i = 0;
            function done() { // Promise.all 等待异步都执行完后 再执行之后的操作 
              i++;
              if(i === paths.length){
                removeDir(dir, cb);
              }
            }
            paths.forEach(p => {
              // 删除某个后通知下 当删除的子目录个数 等于我们的子目录数,删除父级即可
              removeDir(p,done);
            })
          }else{
            fs.rmdir(dir,cb); // 当前目录下没东西直接删除即可
          }
        })
      }else{
        fs.unlink(dir,cb);
      }
    })
}

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

# 🌹🌹🌹🌹 异步深度优先 (串行 series paralle)

function removeDir(dir,cb) { 
    fs.stat(dir,(err,statObj)=>{
      if (statObj.isDirectory()){
        fs.readdir(dir,(err,files)=>{
          let paths = files.map(file=>path.join(dir,file));
          function next(index) {
            // 第一次取出的是a/1.js
            if (index === paths.length) return fs.rmdir(dir,cb);
            let currentPath = paths[index];
            // 文件删除后继续拿出下一项 继续删除
            // 串行删除,删除完第一个,第一个删除完后调用第二个删除的方法
            removeDir(currentPath,()=>next(index+1));
          }
          next(0);
        })
      }else{
        fs.unlink(dir,cb);
      }
    })
}
removeDir('a',()=>{
  console.log('删除成功');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 🌹🌹🌹🌹 深度 有儿子就深入进去

function removeDirSync(dir) {
  let stateObj = fs.statSync(dir);
  if(stateObj.isDirectory()){
    // 是目录继续读取
    let dirs = fs.readdirSync(dir);
    dirs.forEach(d=>{
      let p = path.join(dir,d);
      removeDirSync(p);
    });
    // 儿子删除完成后继续删除自己
    fs.rmdirSync(dir);
  }else{
    fs.unlinkSync(dir);
  }
}
removeDirSync('a');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 🌹🌹 flags 🚩

符号 含义
r 读文件,文件不存在报错
r+ 读取并写入,文件不存在报错
rs 同步读取文件并忽略缓存
w 写入文件,不存在则创建,存在则清空
wx 排它写入文件
w+ 读取并写入文件,不存在则创建,存在则清空
wx+ 和w+类似,排他方式打开
a 追加写入
ax 与a类似,排他方式写入
a+ 读取并追加写入,不存在则创建
ax+ 作用与a+类似,但是以排他方式打开文件

# 🌹🌹 附录

  • r 读取
  • w 写入
  • s 同步
    • 增加相反操作
  • x 排他方式
  • r+ w+的区别?
    • 当文件不存在时,r+不会创建,而会导致调用失败,但w+会创建。
    • 如果文件存在,r+不会自动清空文件,但w+会自动把已有文件的内容清空。

# 🌹🌹 linux权限

# 👀 查看

ls -l ls -l xxx.xx

# 🌰 栗子

-rw-r--r-- 1 bingyang staff 12 7 16 17:18 CNAME
文件类型与权限 链接占用的节点(i-node) 文件所有者 文件所有者的用户组 文件大小 文件的创建时间 最近修改时间 文件名称

# 🌹 path


# 🌹🌹 path是node中专门处理路径的一个核心模块

  • path.join 将多个参数值字符串结合为一个路径字符串

  • path.basename 获取一个路径中的文件名

  • path.extname 获取一个路径中的扩展名

  • path.sep 操作系统提定的文件分隔符

  • path.delimiter 属性值为系统指定的环境变量路径分隔符

  • path.normalize 将非标准的路径字符串转化为标准路径字符串 特点: 可以解析 . 和 .. 多个杠可以转换成一个杠 在windows下反杠会转化成正杠 如结尾以杠结尾的,则保留斜杠

  • path.resolve

以应用程序根目录为起点 如果参数是普通字符串,则意思是当前目录的下级目录 如果参数是.. 回到上一级目录 如果是/开头表示一个绝对的根路径

var path = require('path');
var fs = require('fs');
/**
 * normalize 将非标准化的路径转化成标准化的路径
 * 1.解析. 和 ..
 * 2.多个斜杠会转成一个斜杠
 * 3.window下的斜杠会转成正斜杠
 * 4.如果以斜杠会保留
 **/

console.log(path.normalize('./a////b//..\\c//e//..//'));
//  \a\c\

//多个参数字符串合并成一个路径 字符串
console.log(path.join(__dirname,'a','b'));

/**
 * resolve
 * 以就用程序为根目录,做为起点,根据参数解析出一个绝对路径
 *  1.以应用程序为根起点
 *  2... .
 *  3. 普通 字符串代表子目录
 *  4. /代表绝地路径根目录
 */
console.log(path.resolve());//空代表当前的目录 路径
console.log(path.resolve('a','/c'));// /a/b
// d:\c
//可以获取两个路径之间的相对关系
console.log(path.relative(__dirname,'/a'));
// a
//返回指定路径的所在目录
console.log(path.dirname(__filename)); // 9.path
console.log(path.dirname('./1.path.js'));//  9.path
//basename 获取路径中的文件名
console.log(path.basename(__filename));
console.log(path.basename(__filename,'.js'));
console.log(path.extname(__filename));

console.log(path.sep);//文件分隔符 window \ linux /
console.log(path.win32.sep);
console.log(path.posix.sep);
console.log(path.delimiter);//路径 分隔符 window ; linux :
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

# 🌹 流


# 🌹🌹 流的概念

  • 流是一组有序的,有起点和终点的字节数据传输手段
  • 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
  • 流是一个抽象接口,被 Node 中的很多对象所实现。比如HTTP 服务器request和response对象都是流。

# 🌹🌹🌹 Node中四种基本的流类型

  • Readable - 可读的流 (例如 fs.createReadStream()).
  • Writable - 可写的流 (例如 fs.createWriteStream()).
  • Duplex - 可读写的流 (例如 net.Socket).
  • Transform - 在读写过程中可以修改和变换数据的 Duplex流 (例如 zlib.createDeflate()).

# 🌹🌹🌹 流中的数据有两种模式,二进制模式和对象模式

  • 二进制模式:每个分块都是buffer或者string对象.
  • 对象模式:流内部处理的是一系列普通对象.

所有使用 Node.js API 创建的流对象都只能操作 strings 和 Buffer对象。但是,通过一些第三方流的实现,你依然能够处理其它类型的 JavaScript 值 (除了 null,它在流处理中有特殊意义)。 这些流被认为是工作在 “对象模式”(object mode)。 在创建流的实例时,可以通过 objectMode 选项使流的实例切换到对象模式。试图将已经存在的流切换到对象模式是不安全的。

# 🌹🌹🌹 可读流的两种模式

可读流事实上工作在下面两种模式之一:flowingpaused

  • flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给应用。
  • paused 模式下,必须显式调用 stream.read() 方法来从流中读取数据片段。
  • 所有初始工作模式为 paused 的 Readable 流,可以通过下面三种途径切换到 flowing 模式:
    • 监听 data 事件
    • stream.resume() 方法
    • 调用 stream.pipe() 方法将数据发送到 Writable
  • 可读流可以通过下面途径切换到 paused 模式:
    • 如果不存在管道目标(pipe destination),可以通过调用 stream.pause() 方法实现。
    • 如果存在管道目标,可以通过取消 'data' 事件监听,并调用 stream.unpipe() 方法移除所有管道目标来实现。

如果 Readable 切换到 flowing 模式,且没有消费者处理流中的数据,这些数据将会丢失。 比如, 调用了 readable.resume() 方法却没有监听 'data' 事件,或是取消了 'data' 事件监听,就有可能出现这种情况。

# 🌹🌹🌹 缓存区

  • Writable 和 Readable 流都会将数据存储到内部的缓冲器(buffer)中。这些缓冲器可以 通过相应的 writable._writableState.getBuffer()readable._readableState.buffer 来获取。
  • 缓冲器的大小取决于传递给流构造函数的 highWaterMark 选项。 对于普通的流, highWaterMark 选项指定了总共的字节数。对于工作在对象模式的流, highWaterMark 指定了对象的总数。
  • 当可读流的实现调用 stream.push(chunk)方法时,数据被放到缓冲器中。如果流的消费者没有调用 stream.read() 方法, 这些数据会始终存在于内部队列中,直到被消费。
  • 当内部可读缓冲器的大小达到 highWaterMark 指定的阈值时,流会暂停从底层资源读取数据,直到当前缓冲器的数据被消费 (也就是说, 流会在内部停止调用 readable._read() 来填充可读缓冲器)。
  • 可写流通过反复调用 writable.write(chunk) 方法将数据放到缓冲器。 当内部可写缓冲器的总大小小于 highWaterMark 指定的阈值时, 调用 writable.write() 将返回true。 一旦内部缓冲器的大小达到或超过 highWaterMark ,调用 writable.write() 将返回 false 。
  • stream API 的关键目标, 尤其对于 stream.pipe() 方法, 就是限制缓冲器数据大小,以达到可接受的程度。这样,对于读写速度不匹配的源头和目标,就不会超出可用的内存大小。
  • Duplex 和 Transform 都是可读写的。 在内部,它们都维护了 两个 相互独立的缓冲器用于读和写。 在维持了合理高效的数据流的同时,也使得对于读和写可以独立进行而互不影响。

# 🌹🌹🌹 可读流的三种状态

  • 在任意时刻,任意可读流应确切处于下面三种状态之一:
readable._readableState.flowing = null
readable._readableState.flowing = false
readable._readableState.flowing = true
1
2
3
  • readable._readableState.flowing 为 null,由于不存在数据消费者,可读流将不会产生数据。 在这个状态下,监听 data 事件,调用 readable.pipe() 方法,或者调用 readable.resume() 方法, readable._readableState.flowing 的值将会变为 true 。这时,随着数据生成,可读流开始频繁触发事件。
  • 调用 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背压”(back pressure), 将导致 readable._readableState.flowing 值变为 false。 这将暂停事件流,但 不会 暂停数据生成。 在这种情况下,为 data 事件设置监听函数不会导致 readable._readableState.flowing 变为 true。

当 readable._readableState.flowing 值为 false 时, 数据可能堆积到流的内部缓存中。

# 🌹🌹 可读流 createReadStream

# 🌹🌹🌹 创建可读流

let fs = require('fs');
// 创建可读流 自己读取,或者等待发射 
// 返回的就是一个可读流
let rs = fs.createReadStream('1.txt', {
  flags: 'r', // 如何操作文件
  encoding: null, // 读取文件的编码格式 默认buffer
  autoClose: true, // 读取完毕后 是否自动关闭
  start: 0, // 开始读取的位置
  end: 15, // 结束位置( 包后 )
  highWaterMark: 4 // 64k每次默认读取64k
});
1
2
3
4
5
6
7
8
9
10
11

# 🌹🌹🌹 监听事件

  1. 直接监听data事件 (newListener=> 内部会自动触发data事件)
// 相当于流动模式,监听data方法后不停的触发 直到读取完毕为止
// let arrs = [];
// rs.setEncoding('utf8');
rs.on('data', (data) => {
  //arrs.push(data);
  rs.pause(); // 可以暂停data事件的触发
  setTimeout(() => {
    console.log('恢复')
    rs.resume(); // 恢复的也是data事件
  }, 1000);
});
1
2
3
4
5
6
7
8
9
10
11
  1. 监听end事件 该事件会在读完数据后被触发
rs.on('end', function () {
  // 拼接后将结果一起打印出来
  console.log(Buffer.concat(arrs).toString());
});
1
2
3
4
  1. 监听error事件
rs.on('error', function (err) {
    console.log(err);
});
1
2
3
  1. 监听open事件
rs.on('open', function () {
    console.log(err);
});
1
2
3
  1. 监听close事件
rs.on('close', function () {
    console.log(err);
});
1
2
3

# 🌹🌹🌹 其他事件

  1. 设置编码 与指定 {encoding:'utf8'}效果相同,设置编码
rs.setEncoding('utf8');
1
  1. 暂停和恢复触发data 通过pause()方法和resume()方法
rs.on('data', function (data) {
    rs.pause();
    console.log(data);
});
setTimeout(function () {
    rs.resume();
},2000);
1
2
3
4
5
6
7

# 🌹🌹 自己写个可读流

# 🌹🌹🌹 简单实现 ReadStream

let EventEmitter = require('events');
let fs = require('fs');
class ReadStream extends EventEmitter{
  constructor(path,options = {}){
    super();
    this.path = path;
    this.flags = options.flags || 'r';
    this.encoding = options.encoding || null;
    this.autoClose = options.autoClose || true;
    // this.start = options.start || 0;
    // this.end = options.end|| null;
    this.highWaterMark = options.highWaterMark|| 64*1024;

    // 读取文件 1.打开文件
    this.open();
    // 当用户监听了on('data')事件的时候 就会触发这个事件
    this.on('newListener',(type)=>{
      if(type === 'data'){
        this.read(); // 读取文件内容 并发射出来
      }
    })
  }
  read(){ // 核心的读取方法
    if(typeof this.fd !== 'number'){
      return this.once('open',()=>this.read())
    }
    let buffer = Buffer.alloc(3);
    fs.read(this.fd, buffer,0,3,0,(err,byteRead)=>{
      this.emit('data', buffer);
    })
  }
  destroy(){
    if (typeof this.fd == 'number'){
      fs.close(this.fd,()=>{
        this.emit('close');
      })
    }else{
      this.emit('close');
    }
  }
  open(){
    fs.open(this.path,this.flags,(err,fd)=>{
      if(err){ // 打开文件出错
        this.emit('error',err);
        this.destroy(); // 写个通用的 因为读取完毕后也需要关闭文件
        return;
      }
      this.fd = fd; // 保存文件描述符 文件打开了
      this.emit('open'); // 触发开启事件
    });
  }
}
module.exports = ReadStream;
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

# 🌹🌹🌹 进阶实现 暂停模式 ReadStream

let EventEmitter = require('events');
let fs = require('fs');
class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.flags = options.flags || 'r';
    this.encoding = options.encoding || null;
    this.mode = options.mode || 0o666
    this.start = options.start || 0;
    this.end = options.end || null;
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    this.autoClose = options.autoClose || true;
    // 定义一个控制读取的偏移量 默认和start是一样的
    this.pos = this.start;

    // 默认叫非流动模式 就是不出结果,只有on('data') 时 flowing的值变为 true
    this.flowing = null;

    this.open(); //1s之后才能获取 拿到fd操作符号的 fd默认是异步获取的 this.emit('open')

    this.buffer = Buffer.alloc(this.highWaterMark);
    this.on('newListener', (type) => {
      // 用户监听了data事件
      if (type === 'data') {
        this.flowing = true;
        this.read();
      }
    })
  }
  read() { //读取数据
    // 利用发布订阅的模式 当某个值有了后通知我继续读取
    if (typeof this.fd !== 'number') {
      return this.once('open', () => this.read());
    }
    // 我们需要搞一个buffer用来存放读取到的内容,为了性能好,每次用同一个内存
    // 1 2 3  this.pos = 3
    // 4 5 6  this.pos = 6
    // 7 每次读取的时候要看一下还要读多少个 Math.min


    let howMuchToRead = this.end ? Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark;
    // 每次读取的个数
    fs.read(this.fd, this.buffer, 0, howMuchToRead, this.pos, (err, byteReads) => { // byteReads真实读取到的个数
      // 可以拿到读取的内容了 this.buffer
      if (byteReads>0){
        this.pos += byteReads;
        let r = this.buffer.slice(0, byteReads); // 截取有效的字节
        r = this.encoding ? r.toString(this.encoding) : r
        this.emit('data', r);// 发射读取到的结果

        // 判断是否是流动模式 
        if (this.flowing) {
          this.read();
        }
      }else{
        this.emit('end');
        if(this.autoClose){
          this.destroy();
        }
      }
    });
  }
  resume() {
    this.flowing = true;
    this.read();
  }
  pause(){
    this.flowing = false
  }
  destroy() { // 用来关闭的
    if (typeof this.fd === 'number') {
      fs.close(this.fd, () => {
        this.emit('close');
      });
    } else {
      this.emit('close');
    }
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        // 如果打开文件出错就发射 错误事件
        this.emit('error', err);
        // 如果需要自动关闭 关闭文件
        if (this.autoClose) {
          this.destroy();
        }
        return;
      }
      this.fd = fd;
      this.emit('open', this.fd); // 当前fd拿到了,并且触发open事件
    })
  }
  pipe(ws){
    this.on('data',function (data) {
      let flag = ws.write(data);
      if(!flag){
        this.pause();
      }
    });
    ws.on('drain', () => {
      this.resume();
    });
  }
}

module.exports = ReadStream
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

# 🌹🌹 可写流 createWriteStream

# 🌹🌹🌹 创建可写流

  1. 第一次调用write 会真的像文件里写入 ,之后写到缓存中[]
  2. 写入时 会拿当前写入的内容的长度和highWaterMark比,如果小于hightWaterMark,会返回true >=会返回false
  3. 如果当前写入的个数大于了highWaterMark会触发drain事件
  4. 当文件中的内容写完后 会清空缓存
let fs = require('fs')

let ws = fs.createWriteStream('2.txt',{
  flags:'w', // 开启文件做什么操作
  encoding:'utf8', // 当前写入的编码
  autoClose:true, // 是否读取完毕后自动关闭
  start:0,// 从哪个位置写入
  mode:0o666, // 可读可写
  highWaterMark:3 // 最高水位线 (预计占用的字节数) 默认16*1024
});
// 写 write方法只能写入  字符串或者buffer
// 向一个文件里写入九个数 123456789
let flag = ws.write('123456789','utf8'); // 异步操作

ws.on('drain',function () {
  console.log('抽干')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 🌹🌹🌹 write方法

ws.write(chunk,[encoding],[callback]);

  1. chunk写入的数据buffer/string
  2. encoding编码格式chunk为字符串时有用,可选
  3. callback 写入成功后的回调

返回值为布尔值,系统缓存区满时为false,未满时为true

let flag = ws.write('123456789','utf8'); // 异步操作
1

# 🌹🌹🌹 end方法

ws.end(chunk,[encoding],[callback]); 表明接下来没有数据要被写入 Writable 通过传入可选的 chunk 和 encoding 参数,可以在关闭流之前再写入一段数据 如果传入了可选的 callback 函数,它将作为 'finish' 事件的回调函数

let fs = require('fs');
let WS = require('./writeStream');
let ws = fs.createWriteStream('2.txt',{
  highWaterMark:3
});
let i = 9;
function write() {
  let flag = true;
  while (i>0 && flag) {
    flag = ws.write(i--+'');
  }
}
write();
ws.end('hello'); // 结束了 会讲缓存区的内容全部清空,触发end后不会再触发drain事件

// ws.on('drain',()=>{
//   console.log('干了');
//   write();
// });

// ws.write() ws.end() ws.on('drain');

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 🌹🌹🌹 drain方法

如果当前写入的个数大于了highWaterMark会触发drain事件 ws.on('drain',callback)

  1. 当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 'drain' 事件就会被触发
  2. 建议, 一旦 write() 返回 false, 在 'drain' 事件触发前, 不能写入任何数据块
// ws.on('drain',function () {
//   console.log('抽干')
// })
let fs = require('fs');
let ws = fs.createWriteStream('./2.txt',{
  flags:'w',
  encoding:'utf8',
  highWaterMark:3
});
let i = 10;
function write(){
 let  flag = true;
 while(i&&flag){
      flag = ws.write("1");
      i--;
     console.log(flag);
 }
}
write();
ws.on('drain',()=>{
  console.log("drain");
  write();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 🌹🌹🌹 finish方法

在调用了 stream.end() 方法,且缓冲区数据都已经传给底层系统之后, 'finish' 事件将被触发。

var writer = fs.createWriteStream('./2.txt');
for (let i = 0; i < 100; i++) {
  writer.write(`hello, ${i}!\n`);
}
writer.end('结束\n');
writer.on('finish', () => {
  console.error('所有的写入已经完成!');
});
1
2
3
4
5
6
7
8

# 🌹🌹 pipe用法

# 🌹🌹🌹 pipe用法

readStream.pipe(writeStream);
var from = fs.createReadStream('./1.txt'); // 读流
var to = fs.createWriteStream('./2.txt'); // 写流
from.pipe(to);
1
2
3
4

将数据的滞留量限制到一个可接受的水平,以使得不同速度的来源和目标不会淹没可用内存。

# 🌹🌹🌹 unpipe用法

  • readable.unpipe()方法将之前通过stream.pipe()方法绑定的流分离
  • 如果 destination 没有传入, 则所有绑定的流都会被分离.
let fs = require('fs');
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to);
setTimeout(() => {
  console.log('关闭向2.txt的写入');
  from.unpipe(writable);
  console.log('手工关闭文件流');
  to.end();
}, 1000);
1
2
3
4
5
6
7
8
9
10

# 🌹🌹🌹 cork 和 uncork

  • 调用 writable.cork() 方法将强制所有写入数据都存放到内存中的缓冲区里。 直到调用 stream.uncork()stream.end() 方法时,缓冲区里的数据才会被输出。
  • writable.uncork()将输出在stream.cork()方法被调用之后缓冲在内存中的所有数据
stream.cork();
stream.write('1');
stream.write('2');
process.nextTick(() => stream.uncork());
1
2
3
4

# 🌹🌹 自己实现可写流 WriteStream

# 🌹🌹🌹 简单实现 WriteStream

let fs = require('fs');
let EventEmitter = require('events');

class WriteStream extends EventEmitter {
  constructor(path, options) {
    super();
    this.path = path;
    this.flags = options.flags || 'w';
    this.encoding = options.encoding || 'utf8';
    this.highWaterMark = options.highWaterMark || 16 * 1024
    this.mode = options.mode || 0o666;
    this.autoClose = options.autoClose || true;
    this.start = options.start || 0;
    this.pos = this.start;
    this.writing = false;
    // 当文件正在被写入的时候 要将其他写入的内容放到缓存区中
    this.cache = [];
    // 默认情况下不会触发drain事件 只有当写入的长度等于highWaterMark时才会触发
    this.needDrain = false;
    this.len = 0;
    this.open(); // fd
  }
  destroy() {
    if (typeof this.fd === 'number') {
      fs.close(this.fd, () => {
        this.emit('close');
      })
    } else {
      this.emit('close');
    }
  }
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        this.emit('error');
        if (this.autoClose) {
          this.destroy();
        }
        return;
      }
      this.fd = fd;
      this.emit('open', this.fd);
    })
  }
  write(chunk, encoding="utf8", callback=()=>{}) {
    // 先判断没有写入的内容和highWater来比较
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
    this.len += chunk.length;
    if (this.len >= this.highWaterMark) {
      this.needDrain = true;
    }
    if (this.writing) {
      this.cache.push({
        chunk,
        encoding,
        callback
      });
    } else {
      // 没有正在写入
      this.writing = true;
      this._write(chunk, encoding, () => {
        callback();
        this.clearBuffer(); // 清空数组里的下一项
      });
    }
    // 第一次往文件里写入,第二次放到缓存区中

    return this.len < this.highWaterMark
  }
  clearBuffer(){
    let obj = this.cache.shift();
    if(obj){
      this._write(obj.chunk, obj.encoding,()=>{
        obj.callback();
        this.clearBuffer();
      })
    }else{
      if(this.needDrain){ // 是否需要触发drain
        this.writing = false; // 触发drain后下次再次写入时 往文件里写入
        this.emit('drain'); // 触发drain事件
      }
    }
  }
  _write(chunk, encoding, callback) {
    if (typeof this.fd !== 'number') {
      return this.once('open', () => this._write(chunk, encoding, callback));
    }
    fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, byteWritten) => {
      this.pos += byteWritten; // 移动写入的偏移量
      this.len -= byteWritten; // 减少没有写入的个数
      callback();  // 清空缓存区的内容
    });
  }
}
module.exports = WriteStream;
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
93
94
95

# 🌹🌹 Readable模式

'readable' 事件将在流中有数据可供读取时触发。在某些情况下,为 'readable' 事件添加回调将会导致一些数据被读取到内部缓存中。

const readable = getReadableStreamSomehow();
readable.on('readable', () => {
  // 有一些数据可读了
});
1
2
3
4
  • 当到达流数据尾部时, 'readable' 事件也会触发。触发顺序在 'end' 事件之前。
  • 事实上, 'readable' 事件表明流有了新的动态:要么是有了新的数据,要么是到了流的尾部。 对于前者, stream.read() 将返回可用的数据。而对于后者, stream.read() 将返回 null。
let fs =require('fs');
let rs = fs.createReadStream('./1.txt',{
  start:3,
  end:8,
  encoding:'utf8',
  highWaterMark:3
});
rs.on('readable',function () {
  console.log('readable');
  console.log('rs._readableState.buffer.length',rs._readableState.length);
  let d = rs.read(1);
  console.log('rs._readableState.buffer.length',rs._readableState.length);
  console.log(d);
  setTimeout(()=>{
      console.log('rs._readableState.buffer.length',rs._readableState.length);
  },500)
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 🌹🌹🌹 基本用法

读流的模式有:流动模式(上面的)和 readable
readable一般不用。行读取器 会用到。源码就不写了

let fs = require('fs');
let rs = fs.createReadStream('./2.txt',{
  highWaterMark:3
});
//  readable模式
rs.setEncoding('utf8');
// 默认监听readable后 会执行回调,装满highWaterMark这么多的内容
// 自己去读取,如果容器是空的会继续读取highWaterMark这么多,直到没东西为止
// 类似喝水,喝了一升,会停一下。服务员会马上再倒上一升。不停的读
rs.on('readable',()=>{
  let r = rs.read(1);
  console.log(rs._readableState.length);
  setTimeout(() => {
    console.log(rs._readableState.length);
  }, 1000);
});

// 用法:行读取器 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 🌹🌹 流的经典应用 行读取器

# 🌹🌹🌹 行读取器

每读一行,触发触发一次事件,进行处理,比如写一行。就是读一行写一行:

  • 以前的打印要每秒可以打印10个字符,换行城要0.2秒,正要可以打印2个字符。
  • 研制人员就是在每行后面加两个表示结束的字符。一个叫做"回车",告诉打字机把打印头定位在左边界;另一个叫做"换行",告诉打字机把纸向下移一行。
  • Unix系统里,每行结尾只有换行"(line feed)",即"\n",
  • Windows系统里面,每行结尾是"<回车><换行>",即"\r\n"
  • Mac系统里,每行结尾是"回车"(carriage return),即"\r"
  • 在ASCII码里
  • 换行 \n 10 0A
  • 回车 \r 13 0D
let fs = require('fs');
// 目标功能 每读一行 输出一行 data
let line = new LinReader('./1.txt')
line.on('newLine',function(data){
    console.log(data)
})
1
2
3
4
5
6

# 🌹🌹🌹 自己实现一个lineReader 行读取器。

let fs = require('fs');
let EventEmitter = require('events');
class LineReader extends EventEmitter{
  constructor(path){
    super();
    this.rs = fs.createReadStream(path);
    // 监听readale事件
    const RETURN = 13;
    const LINE = 10;
    let arr = []; // 存放读取出来的内容
    this.rs.on('readable',()=>{
      // 一个个的拿出来碰到回车后 就把之前的内容发射出来 \r\n
      let char;
      while (char = this.rs.read(1)) { // 取出每一个
        switch (char[0]) {
          case RETURN:
            this.emit('newLine',Buffer.concat(arr).toString());
            arr = []; // 如果\r下一个不是\n的话 也放到数组中
            if(this.rs.read(1)[0]!==LINE){
              arr.push(char); 
            }
            break;
          case LINE: // mac下没有\r 只有\n
            this.emit('newLine', Buffer.concat(arr).toString());
            arr = []; // 如果\r下一个不是\n的话 也放到数组中
            break;
          default:
            arr.push(char); // 将读取出来的buffer放到数组中
        }
      }
    });
    this.rs.on('end',()=>{
      this.emit('newLine', Buffer.concat(arr).toString());
    })
  }
} 
let line = new LineReader('./3.txt');

line.on('newLine',function(data) {
    console.log(data);
});
// promise 事件环 (2)  流 

// 读流 on('data') on('readable')  写流  write end
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

# 🌹🌹 自定义流

  • 所有流都基于 stream 这个模块,都是通过继承该模块内置的对象实现的

# 🌹🌹🌹 判断流是不是流

let stream = require('stream'); 
let rs = fs.createReadStream('./1.txt');
console.log(rs instanceof stream);
1
2
3

# 🌹🌹🌹 自定义一个可读流 Readable

可读的流 (例如 fs.createReadStream()).

let {Readable} = require('stream');
// 自己实现流 fs.createReadStream

// 所有的流都基于这个模块
let {Readable} = require('stream');
// 默认fs.createReadStream自己实现了一个_read 调用的fs.read
// 所有自己实现一个流也要实现这个方法
class MyStream extends Readable{
  constructor(){
    super();
    this.index = 9;
  }
  _read() { // 如果想自己实现可读流 需要继承Readable,并且重写_read方法,方法中调用push 就是把结果传递给on('data')事件
    this.push(this.index--+'');
    if(this.index == 0){
      this.push(null);
    }
  }
}
let myStream = new MyStream();
myStream.on('data',(data)=>{
  console.log(data.toString());
});
myStream.on('end',function () {
  console.log('end');
})

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

# 🌹🌹🌹 自定义一个可写流 Writable

可写的流 (例如 'fs.createWriteStream()').

let {Writable} = require('stream');
let fs = require('fs');
class MyStream extends Writable{
  constructor(){
    super();
    this.index = 9;
  }
  _write(chunk,encoding,callback){ // this.clearBuffer()
    console.log(chunk,encoding);
    // 可以借助可写流实现自己写入的逻辑
    callback(); // 不调用callback 后面的write就不会执行
  }
}
let myStream = new MyStream();
myStream.write('1','utf8');
myStream.write('1', 'utf8');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 🌹🌹🌹 自定义一个双工流(可读可写流) Duplex

可读写的流 (例如 net.Socket).

let {Duplex} = require('stream');
let fs = require('fs');
class MyStream extends Duplex{
  constructor(){
    super();
  }
  _read(){
    this.push('1');
  }
  _write(chunk,encoding,callback){
    console.log(chunk)
    callback();
  }
}
let myStream = new MyStream
myStream.write('ok');

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 🌹🌹🌹 自定义一个转换流 Transform

Transform - 在读写过程中可以修改和变换数据的 Duplex流 (例如 zlib.createDeflate()).
压缩文件.pipe(压缩流s).pipe(压缩好的文件) 中间的 压缩流s 从写流 转换成 读流 转化流 (写流 -> 读流) socket res

let {Transform} = require('stream');
let fs = require('fs');
class MyStream extends Transform{
  _transform(chunk,encoding,callback){
    this.push(chunk.toString().toUpperCase());
    callback();
  }
}
let myStream = new MyStream
// 转化流 就是再可读流和可写流之间 做转化操作
process.stdin.pipe(myStream).pipe(process.stdout);
1
2
3
4
5
6
7
8
9
10
11

# 🌹🌹🌹 自定义管道流 readable

const stream = require('stream')
var index = 0;
const readable = stream.Readable({
    highWaterMark: 2,
    read: function () {
        process.nextTick(() => {
            console.log('push', ++index)
            this.push(index+'');
        })
    }
})

const writable = stream.Writable({
    highWaterMark: 2,
    write: function (chunk, encoding, next) {
        console.log('写入:', chunk.toString())
    }
})

readable.pipe(writable);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 🌹🌹🌹 自定义对象流

默认情况下,流处理的数据是Buffer/String类型的值。有一个objectMode标志,我们可以设置它让流可以接受任何JavaScript对象。

const {Transform} = require('stream');
let fs = require('fs');
let rs = fs.createReadStream('./users.json');
rs.setEncoding('utf8');
let toJson = Transform({
    readableObjectMode: true,
    transform(chunk, encoding, callback) {
        this.push(JSON.parse(chunk));
        callback();
    }
});
let jsonOut = Transform({
    writableObjectMode: true,
    transform(chunk, encoding, callback) {
        console.log(chunk);
        callback();
    }
});
rs.pipe(toJson).pipe(jsonOut)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[
  {"name":"zfpx1","age":8},
  {"name":"zfpx2","age":9}
]
1
2
3
4

# 🌹🌹🌹 unshift

readable.unshift() 方法会把一块数据压回到Buffer内部。 这在如下特定情形下有用: 代码正在消费一个数据流,已经"乐观地"拉取了数据。 又需要"反悔-消费"一些数据,以便这些数据可以传给其他人用。

const {Transform} = require('stream');
const { StringDecoder } = require('string_decoder');
let decoder = new StringDecoder('utf8');
let fs = require('fs');
let rs = fs.createReadStream('./req.txt');

function parseHeader(stream, callback) {
    let header = '';
    rs.on('readable',onReadable);
    function onReadable() {

        let chunk;
        while(null != (chunk = rs.read())){
            const str = decoder.write(chunk);
            if(str.match(/\r\n\r\n/)){
                const split = str.split(/\r\n\r\n/);
                console.log(split);
                header+=split.shift();
                const remaining = split.join('\r\n\r\n');
                const buf = Buffer.from(remaining,'utf8');
                rs.removeListener('readable', onReadable);
                if(buf.length){
                    stream.unshift(buf);
                }
                callback(null,header,rs);
            }else{
                header += str;
            }
        }
    }
}
parseHeader(rs,function(err,header,stream){
    console.log(header);
    stream.setEncoding('utf8');
    stream.on('data',function (data) {
        console.log('data',data);
    });
});
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
Host: www.baidu.com
User-Agent: curl/7.53.0
Accept: */*
name=zfpx&age=9
1
2
3
4

# 🌹🌹 流的理解 😄

TODO (opens new window)

Last Updated: 1/7/2020, 7:42:12 AM
    asphyxia
    逆时针向