nodejs --- 融会贯通 (一)

323 阅读4分钟

这篇文章主要知识点来自:

  • 《Node.js硬实战:115个核心技巧》
  • i0natan/nodebestpractices
  • 后续学习的一些知识点

安装

# 使用 nvm 安装https://github.com/creationix/nvm#install-script # Git installnvm installnvm alias default# 卸载 pkg 安装版sudo rm -rf /usr/local/{bin/{node,npm},lib/node_modules/npm,lib/node,share/man/*/node.*}

全局变量

require(id)

  • 内建模块直接从内存加载
  • 文件模块通过文件查找定位到文件
  • 包通过 package.json 里面的 main 字段查找入口文件

module.exports

// 通过如下模块包装得到(funciton (exports, require, module, __filename, __dirname) { // 包装头}); // 包装尾

JSON 文件

  • 通过 `fs.readFileSync()` 加载
  • 通过 `JSON.parse()` 解析

加载大文件

  • require 成功后会缓存文件
  • 大量使用会导致大量数据驻留在内存中,导致 GC 频分和内存泄露

module.exports 和 exports

执行时

(funciton(exports, require, module, __filename, __dirname) { // 包装头
    console.log('hello world!') // 原始文件
}); // 包装尾

exports

  • exports 是 module 的属性,默认情况是空对象
  • require 一个模块实际得到的是该模块的 exports 属性
  • `exports.xxx` 导出具有多个属性的对象
  • `module.exports = xxx` 导出一个对象

使用

// module-2.js
exports.method = function () {
    return 'Hello';
};

exports.method2 = function () {
    return 'Hello again';
};

// module-1.js
const module2 = require('./module-2');
console.log(module2.method()); // Hello
console.log(module2.method2()); // Hello again

路径变量

console.log('__dirname:', __dirname); // 文件夹
console.log('__filename:', __filename); // 文件
path.join(__dirname, 'views', 'view.html'); // 如果不希望自己手动处理 / 的问题,使用 path.join

console

占位符类型例子
%sStringconsole.log('%s', 'value')
%dNumberconsole.log('%d', 3.14)
%jJSONconsole.log('%j', {name: 'Chenng'})

process

查看 PATH

nodeconsole.log(process.env.PATH.split(':').join('\n'));

设置 PATH

process.env.PATH += ':/a_new_path_to_executables';

获取信息

// 获取平台信息
process.arch // x64
process.platform // darwin

// 获取内存使用情况
process.memoryUsage();

// 获取命令行参数
process.argv

nextTick

process.nextTick 方法允许你把一个回调放在下一次时间轮询队列的头上,这意味着可以用来延迟执行,结果是比 setTimeout 更有效率。

const EventEmitter = require('events').EventEmitter;

function complexOperations() {
    const events = new EventEmitter();

    process.nextTick(function () {
        events.emit('success');
    });

    return events;
}

complexOperations().on('success', function () {
    console.log('success!');
});

Buffer

如果没有提供编码格式,文件操作以及很多网络操作就会将数据作为 Buffer 类型返回。

toString

默认转为 UTF-8 格式,还支持 asciibase64 等。

data URI

// 生成 data URI
const fs = require('fs');
const mime = 'image/png';
const encoding = 'base64';
const base64Data = fs.readFileSync(`${__dirname}/monkey.png`).toString(encoding);
const uri = `data:${mime};${encoding},${base64Data}`;
console.log(uri);

// data URI 转文件
const fs = require('fs');
const uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...';
const base64Data = uri.split(',')[1];
const buf = Buffer(base64Data, 'base64');
fs.writeFileSync(`${__dirname}/secondmonkey.png`, buf);

events

const EventEmitter = require('events').EventEmitter;

const AudioDevice = {
    play: function (track) {
        console.log('play', track);
    },
    stop: function () {
        console.log('stop');
    },
};

class MusicPlayer extends EventEmitter {
    constructor() {
        super();
        this.playing = false;
    }
}

const musicPlayer = new MusicPlayer();
musicPlayer.on('play', function (track) {
    this.playing = true;
    AudioDevice.play(track);
});
musicPlayer.on('stop', function () {
    this.playing = false;
    AudioDevice.stop();
});

musicPlayer.emit('play', 'The Roots - The Fire');
setTimeout(function () {
    musicPlayer.emit('stop');
}, 1000);

// 处理异常
// EventEmitter 实例发生错误会发出一个 error 事件
// 如果没有监听器,默认动作是打印一个堆栈并退出程序
musicPlayer.on('error', function (err) {
    console.err('Error:', err);
});

util

promisify

const util = require('util');
const fs = require('fs');
const readAsync = util.promisify(fs.readFile);

async function init() {
    try {
        let data = await readAsync('./package.json');

        data = JSON.parse(data);

        console.log(data.name);
    } catch (err) {
        console.log(err);
    }
}

理解流

流是基于事件的 API,用于管理和处理数据。

  • 流是能够读写的
  • 是基于事件实现的一个实例 理解流的最好方式就是想象一下没有流的时候怎么处理数据:
  • `fs.readFileSync` 同步读取文件,程序会阻塞,所有数据被读到内存
  • `fs.readFile` 阻止程序阻塞,但仍会将文件所有数据读取到内存中
  • 希望少内存读取大文件,读取一个数据块到内存处理完再去索取更多的数据

流的类型

  • 内置:许多核心模块都实现了流接口,如 `fs.createReadStream`
  • HTTP:处理网络技术的流
  • 解释器:第三方模块 XML、JSON 解释器
  • 浏览器:Node 流可以被拓展使用在浏览器
  • Audio:流接口的声音模块
  • RPC(远程调用):通过网络发送流是进程间通信的有效方式
  • 测试:使用流的测试库

使用内建流 API

静态 web 服务器

想要通过网络高效且支持大文件的发送一个文件到一个客户端。

不使用流

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
    fs.readFile(`${__dirname}/index.html`, (err, data) => {
        if (err) {
            res.statusCode = 500;
            res.end(String(err));
            return;
        }

        res.end(data);
    });
}).listen(8000);

使用流

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
    fs.createReadStream(`${__dirname}/index.html`).pipe(res);
}).listen(8000);
  • 更少代码,更加高效
  • 提供一个缓冲区发送到客户端

使用流 + gzip

const http = require('http');
const fs = require('fs');
const zlib = require('zlib');

http.createServer((req, res) => {
    res.writeHead(200, {
        'content-encoding': 'gzip',
    });
    fs.createReadStream(`${__dirname}/index.html`)
        .pipe(zlib.createGzip())
        .pipe(res);
}).listen(8000)

流的错误处理

const fs = require('fs');
const stream = fs.createReadStream('not-found');

stream.on('error', (err) => {
    console.trace();
    console.error('Stack:', err.stack);
    console.error('The error raised was:', err);
});

使用流基类

可读流 - JSON 行解析器

可读流被用来为 I/O 源提供灵活的 API,也可以被用作解析器:

  • 继承自 steam.Readable 类
  • 并实现一个 _read(size) 方法

json-lines.txt

{ "position": 0, "letter": "a" }{ "position": 1, "letter": "b" }{ "position": 2, "letter": "c" }{ "position": 3, "letter": "d" }{ "position": 4, "letter": "e" }{ "position": 5, "letter": "f" }{ "position": 6, "letter": "g" }{ "position": 7, "letter": "h" }{ "position": 8, "letter": "i" }{ "position": 9, "letter": "j" }

JSONLineReader.js

const stream = require('stream');
const fs = require('fs');
const util = require('util');

class JSONLineReader extends stream.Readable {
    constructor(source) {
        super();
        this._source = source;
        this._foundLineEnd = false;
        this._buffer = '';

        source.on('readable', () => {
            this.read();
        });
    }

    // 所有定制 stream.Readable 类都需要实现 _read 方法
    _read(size) {
        let chunk;
        let line;
        let result;

        if (this._buffer.length === 0) {
            chunk = this._source.read();
            this._buffer += chunk;
        }

        const lineIndex = this._buffer.indexOf('\n');

        if (lineIndex !== -1) {
            line = this._buffer.slice(0, lineIndex); // 从 buffer 的开始截取第一行来获取一些文本进行解析
            if (line) {
                result = JSON.parse(line);
                this._buffer = this._buffer.slice(lineIndex + 1);
                this.emit('object', result); // 当一个 JSON 记录解析出来的时候,触发一个 object 事件
                this.push(util.inspect(result)); // 将解析好的 SJON 发回内部队列
            } else {
                this._buffer = this._buffer.slice(1);
            }
        }
    }
}

const input = fs.createReadStream(`${__dirname}/json-lines.txt`, {
    encoding: 'utf8',
});
const jsonLineReader = new JSONLineReader(input); // 创建一个 JSONLineReader 实例,传递一个文件流给它处理

jsonLineReader.on('object', (obj) => {
    console.log('pos:', obj.position, '- letter:', obj.letter);
});

可写流 - 文字变色

可写的流可用于输出数据到底层 I/O:

  • 继承自 `stream.Writable`
  • 实现一个 `_write`方法向底层源数据发送数据

    cat json-lines.txt | node stram_writable.js

stram_writable.js

const stream = require('stream');

class GreenStream extends stream.Writable {
    constructor(options) {
        super(options);
    }

    _write(chunk, encoding, cb) {
        process.stdout.write(`\u001b[32m${chunk}\u001b[39m`);
        cb();
    }
}

process.stdin.pipe(new GreenStream());

双工流 - 接受和转换数据

双工流允许发送和接受数据:

  • 继承自 `stream.Duplex`
  • 实现 `_read` 和`_write` 方法

转换流 - 解析数据

使用流改变数据为另一种格式,并且高效地管理内存:

  • 继承自 `stream.Transform`
  • 实现 `_transform` 方法

测试流

使用 Node 内置的断言模块测试

const assert = require('assert');
const fs = require('fs');
const CSVParser = require('./csvparser');

const parser = new CSVParser();
const actual = [];

fs.createReadStream(`${__dirname}/sample.csv`)
    .pipe(parser);

process.on('exit', function () {
    actual.push(parser.read());
    actual.push(parser.read());
    actual.push(parser.read());

    const expected = [
        {
            name: 'Alex',
            location: 'UK',
            role: 'admin'
        },
        {
            name: 'Sam',
            location: 'France',
            role: 'user'
        },
        {
            name: 'John',
            location: 'Canada',
            role: 'user'
        },
  ];

    assert.deepEqual(expected, actual);
});

文件系统

fs 模块交互

  • POSIX 文件 I/O
  • 文件流
  • 批量文件 I/O
  • 文件监控

POSIX 文件系统

fs 方法描述
fs.truncate截断或者拓展文件到制定的长度
fs.ftruncate和 truncate 一样,但将文件描述符作为参数
fs.chown改变文件的所有者以及组
fs.fchown和 chown 一样,但将文件描述符作为参数
fs.lchown和 chown 一样,但不解析符号链接
fs.stat获取文件状态
fs.lstat和 stat 一样,但是返回信息是关于符号链接而不是它指向的内容
fs.fstat和 stat 一样,但将文件描述符作为参数
fs.link创建一个硬链接
fs.symlink创建一个软连接
fs.readlink读取一个软连接的值
fs.realpath返回规范的绝对路径名
fs.unlink删除文件
fs.rmdir删除文件目录
fs.mkdir创建文件目录
fs.readdir读取一个文件目录的内容
fs.close关闭一个文件描述符
fs.open打开或者创建一个文件用来读取或者写入
fs.utimes设置文件的读取和修改时间
fs.futimes和 utimes 一样,但将文件描述符作为参数
fs.fsync同步磁盘中的文件数据
fs.write写入数据到一个文件
fs.read读取一个文件的数据
const fs = require('fs');
const assert = require('assert');

const fd = fs.openSync('./file.txt', 'w+');
const writeBuf = new Buffer('some data to write');
fs.writeSync(fd, writeBuf, 0, writeBuf.length, 0);

const readBuf = new Buffer(writeBuf.length);
fs.readSync(fd, readBuf, 0, writeBuf.length, 0);
assert.equal(writeBuf.toString(), readBuf.toString());

fs.closeSync(fd);

读写流

const fs = require('fs');
const readable = fs.createReadStream('./original.txt');
const writeable = fs.createWriteStream('./copy.txt');
readable.pipe(writeable);

文件监控

fs.watchFilefs.watch 低效,但更好用。

const fs = require('fs');
const config = JSON.parse(fs.readFileSync('./config.json').toString());
init(config);

require

const config = require('./config.json);init(config);
  • 模块会被全局缓冲,其他文件也加载并修改,会影响到整个系统加载了此文件的模块
  • 可以通过 `Object.freeze` 来冻结一个对象

文件描述

文件描述是在操作系统中管理的在进程中打开文件所关联的一些数字或者索引。操作系统通过指派一个唯一的整数给每个打开的文件用来查看关于这个文件

Stream文件描述描述
stdin0标准输入
stdout1标准输出
stderr2标准错误

console.log('Log')process.stdout.write('log') 的语法糖。

一个文件描述是 open 以及 openSync 方法调用返回的一个数字

const fd = fs.openSync('myfile', 'a');console.log(typeof fd === 'number'); // true

文件锁

协同多个进程同时访问一个文件,保证文件的完整性以及数据不能丢失:

  • 强制锁(在内核级别执行)

  • 咨询锁(非强制,只在涉及到进程订阅了相同的锁机制)

  • node-fs-ext 通过 flock 锁住一个文件
  • 使用锁文件

  • 进程 A 尝试创建一个锁文件,并且成功了
  • 进程 A 已经获得了这个锁,可以修改共享的资源
  • 进程 B 尝试创建一个锁文件,但失败了,无法修改共享的资源

Node 实现锁文件

  • 使用独占标记创建锁文件
  • 使用 mkdir 创建锁文件

独占标记

// 所有需要打开文件的方法,fs.writeFile、fs.createWriteStream、fs.open 都有一个 x 标记
// 这个文件应该已独占打开,若这个文件存在,文件不能被打开
fs.open('config.lock', 'wx', (err) => {
    if (err) {
        return console.err(err);
    }
});

// 最好将当前进程号写进文件锁中
// 当有异常的时候就知道最后这个锁的进程
fs.writeFile(
    'config.lock',
    process.pid, {
        flogs: 'wx'
    },
    (err) => {
        if (err) {
            return console.error(err)
        };
    },
);

mkdir 文件锁

独占标记有个问题,可能有些系统不能识别 0_EXCL 标记。另一个方案是把锁文件换成一个目录,PID 可以写入目录中的一个文件。

fs.mkidr('config.lock', (err) => {
    if (err) {
        return console.error(err);
    }
    fs.writeFile(`/config.lock/${process.pid}`, (err) => {
        if (err) {
            return console.error(err);
        }
    });
});

lock 模块实现

github.com/npm/lockfil…

const fs = require('fs');
const lockDir = 'config.lock';
let hasLock = false;

exports.lock = function (cb) { // 获取锁
    if (hasLock) {
        return cb();
    } // 已经获取了一个锁
    fs.mkdir(lockDir, function (err) {
        if (err) {
            return cb(err);
        } // 无法创建锁

        fs.writeFile(lockDir + '/' + process.pid, function (err) { // 把 PID写入到目录中以便调试
            if (err) {
                console.error(err);
            } // 无法写入 PID,继续运行
            hasLock = true; // 锁创建了
            return cb();
        });
    });
};

exports.unlock = function (cb) { // 解锁方法
    if (!hasLock) {
        return cb();
    } // 如果没有需要解开的锁
    fs.unlink(lockDir + '/' + process.pid, function (err) {
        if (err) {
            return cb(err);
        }

        fs.rmdir(lockDir, function (err) {
            if (err) return cb(err);
            hasLock = false;
            cb();
        });
    });
};

process.on('exit', function () {
    if (hasLock) {
        fs.unlinkSync(lockDir + '/' + process.pid); // 如果还有锁,在退出之前同步删除掉
        fs.rmdirSync(lockDir);
        console.log('removed lock');
    }
});

递归文件操作

一个线上库:mkdirp

递归:要解决我们的问题就要先解决更小的相同的问题。

dir-a├── dir-b│   ├── dir-c│   │   ├── dir-d│   │   │   └── file-e.png│   │   └── file-e.png│   ├── file-c.js│   └── file-d.txt├── file-a.js└── file-b.txt

查找模块:find /asset/dir-a -name="file.*"

[  'dir-a/dir-b/dir-c/dir-d/file-e.png',  'dir-a/dir-b/dir-c/file-e.png',  'dir-a/dir-b/file-c.js',  'dir-a/dir-b/file-d.txt',  'dir-a/file-a.js',  'dir-a/file-b.txt',]

const fs = require('fs');
const join = require('path').join;

// 同步查找
exports.findSync = function (nameRe, startPath) {
    const results = [];

    function finder(path) {
        const files = fs.readdirSync(path);

        for (let i = 0; i < files.length; i++) {
            const fpath = join(path, files[i]);
            const stats = fs.statSync(fpath);

            if (stats.isDirectory()) {
                finder(fpath);
            }

            if (stats.isFile() && nameRe.test(files[i])) {
                results.push(fpath);
            }
        }
    }

    finder(startPath);
    return results;
};

// 异步查找
exports.find = function (nameRe, startPath, cb) { // cb 可以传入 console.log,灵活
    const results = [];
    let asyncOps = 0; // 2

    function finder(path) {
        asyncOps++;
        fs.readdir(path, function (er, files) {
            if (er) {
                return cb(er);
            }

            files.forEach(function (file) {
                const fpath = join(path, file);

                asyncOps++;
                fs.stat(fpath, function (er, stats) {
                    if (er) {
                        return cb(er);
                    }

                    if (stats.isDirectory()) finder(fpath);

                    if (stats.isFile() && nameRe.test(file)) {
                        results.push(fpath);
                    }

                    asyncOps--;
                    if (asyncOps == 0) {
                        cb(null, results);
                    }
                });
            });

            asyncOps--;
            if (asyncOps == 0) {
                cb(null, results);
            }
        });
    }

    finder(startPath);
};

console.log(exports.findSync(/file.*/, `${__dirname}/dir-a`));
console.log(exports.find(/file.*/, `${__dirname}/dir-a`, console.log));

监视文件和文件夹

想要监听一个文件或者目录,并在文件更改后执行一个动作。

const fs = require('fs');

fs.watch('./watchdir', console.log); // 稳定且快

fs.watchFile('./watchdir', console.log); // 跨平台

逐行地读取文件流

const fs = require('fs');
const readline = require('readline');

const rl = readline.createInterface({
    input: fs.createReadStream('/etc/hosts'),
    crlfDelay: Infinity
});

rl.on('line', (line) => {
    console.log(`cc ${line}`);
    const extract = line.match(/(\d+\.\d+\.\d+\.\d+) (.*)/);
});