nodejs随手记(持续)

  • start date: 2018-08-27 13:00:45

npm

镜像及切换

常用镜像:

切换

临时切换镜像安装

1
npm --registry 镜像源 install 安装包


1
npm --registry https://registry.npm.taobao.org install webpack

永久切换

1
npm config set registry 镜像源


1
npm config set registry http://r.cnpmjs.org/

linux部署环境

  • 1.下载包,如:node-v8.2.1-linux-x64.tar.xz
  • 2.解压包,
    1
    tar -xvf node-v8.2.1-linux-x64.tar.xz

检查node-v8.2.1-linux-x64目录下是否包含bin目录。

  • 3.建立软连接,(如目录地址为/var/www/html/node-v8.2.1-linux-x64)
    1
    2
       ln -s /var/www/html/node-v8.2.1-linux-x64/bin/npm /usr/loacl/bin/
    ln -s /var/www/html/node-v8.2.1-linux-x64/bin/node /usr/loacl/bin/

检查:

1
2
node -v
npm -v

  • 4.添加全局变量,以确保npm全局安装,如
    1
    export PATH=NODE_HOME=/var/www/html/node-v8.2.1-linux-x64/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:$PATH

或修改/etc/profile文件,末尾输入上述命令。

检查:

1
2
npm i -g cnpm
cnpm -v

1 文件系统

fs模块(内置)

文件读取

普通读取fs.readFileSync()(同步)/fs.readFile()(异步)

通过文件流读取

适合大文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');
const readStream = fs.createReadStream('./fileForRead.txt', 'utf8');

readStream
.on('data', function(chunk) {
console.log('读取数据: ' + chunk);
})
.on('error', function(err){
console.log('出错: ' + err.message);
})
.on('end', function(){ // 没有数据了
console.log('没有数据了');
})
.on('close', function(){ // 已经关闭,不会再有事件抛出
console.log('已经关闭');
});

文件写入

普通写入fs.writeFileSync()(同步)/fs.writeFile()(异步)

通过文件流写入

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');
const writeStream = fs.createWriteStream('./fileForWrite1.txt', 'utf8');

writeStream
.on('close', function(){ // 已经关闭,不会再有事件抛出
console.log('已经关闭');
});

writeStream.write('hello');
writeStream.write('nodejs');
writeStream.end('');

文件是否存在/文件或目录的用户权限

fs.access()(异步)

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 file = 'package.json';

// 检查文件是否存在于当前目录。
fs.access(file, fs.constants.F_OK, (err) => {
console.log(`${file} ${err ? '不存在' : '存在'}`);
});

// 检查文件是否可读。
fs.access(file, fs.constants.R_OK, (err) => {
console.log(`${file} ${err ? '不可读' : '可读'}`);
});

// 检查文件是否可写。
fs.access(file, fs.constants.W_OK, (err) => {
console.log(`${file} ${err ? '不可写' : '可写'}`);
});

// 检查文件是否存在于当前目录,且是否可写。
fs.access(file, fs.constants.F_OK | fs.constants.W_OK, (err) => {
if (err) {
console.error(
`${file} ${err.code === 'ENOENT' ? '不存在' : '只可读'}`);
} else {
console.log(`${file} 存在,且可写`);
}
});

fs.accessSync()(同步)

1
2
3
4
5
6
try {
fs.accessSync('etc/passwd', fs.constants.R_OK | fs.constants.W_OK);
console.log('可读可写');
} catch (err) {
console.error('不可访问!');
}

创建目录 fs.mkdirSync()(同步)/fs.msdir()(异步)

删除文件

fs.unlinkSync()(同步)

1
2
const fs = require('fs');
fs.unlinkSync('./fileForUnlink.txt');
1
2
3
4
5
const fs = require('fs');
fs.unlink('./fileForUnlink.txt', function (err) {
if (err) throw err;
console.log('删除成功');
})

遍历目录

fs.readdirSync()很坑。。。要读取所有子文件必须遍历

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

const getFilesInDir = function(dir){
let results = [path.resolve(dir)];
let files = fs.readdirSync(dir, 'utf8');

files.forEach(function (file) {
file = path.resolve(dir, file);
let stats = fs.statSync(file);

if (stats.isFile()) {
results.push(file);
} else if(stats.isDirectory()) {
results = results.concat(getFilesInDir(file));
}
});

return results;
};

let files = getFilesInDir('testFolder');
console.log(files);

文件重命名

fs.rename()(异步)

1
2
3
4
5
6
const fs = require('fs');

fs.rename('./hello', './world', function(err){
if(err) throw err;
console.log('重命名成功');
});

fs.renameSync()(异步)

1
2
3
const fs = require('fs');

fs.renameSync('./hello', './world');

监听文件修改

*fs.watch()比fs.watchFile()高效。

fs.watchFile()

实现原理:轮询。每隔一段时间检查文件是否发生变化。所以在不同平台上表现基本是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('fs');

const options = {
persistent: true, // 默认就是true
interval: 2000 // 多久检查一次
};

// curr, prev 是被监听文件的状态, fs.Stat实例
// 可以通过 fs.unwatch() 移除监听
fs.watchFile('./fileForWatch.txt', options, function(curr, prev){
console.log('修改时间为: ' + curr.mtime);
});

fs.watch()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');

const options = {
persistent: true,
recursive: true,
encoding: 'utf8'
};

fs.watch('../', options, function(event, filename){
console.log('触发事件:' + event);
if (filename) {
console.log('文件名是: ' + filename);
} else {
console.log('文件名是没有提供');
}
});

*修改所有者fs.chmodSync()(同步)/fs.chmod()(异步)

获取文件状态

fs.stat()、fs.fstat()、fs.lstat()。

fs.stat()和fs.fstat():传文件路径vs文件句柄。
fs.stat()和fs.lstat():如果文件是软链接,那么fs.stat()返回目标文件的状态,fs.lstat()返回软链接本身的状态。

追加文件内容fs.appendFileSync()(同步)/fs.appendFile()(异步)

*文件内容截取fs.truncate()、fs.ftruncate()

*修改文件属性(时间)fs.utimes()、fs.futimes()

软链接和硬链接:

  • 硬链接:inode相同,多个别名。删除一个硬链接文件,不会影响其他有相同inode的文件。
  • 软链接:有自己的inode,用户数据块存放指向文件的inode。

创建临时目录

1
2
3
4
5
6
const fs = require('fs');

fs.mkdtemp('/tmp/', function(err, folder) {
if(err) throw err;
console.log('创建临时目录: ' + folder);
});

删除目录 fs.rmdirSync()(同步)/fs.rmdir()(异步)

2 开启gzip

zlib模块(内置)
zlib模块提供通过 Gzip 和 Deflate/Inflate 实现的压缩功能,可以通过这样使用它:

1
const zlib = require('zlib');

压缩范例:

1
2
3
4
5
6
7
8
const fs = require('fs');
const zlib = require('zlib');
const gzip = zlib.createGzip();

let inFile = fs.createReadStream('./fileForCompress.txt');
let out = fs.createWriteStream('./fileForCompress.txt.gz');

inFile.pipe(gzip).pipe(out);

解压范例:

1
2
3
4
5
6
7
8
const fs = require('fs');
const zlib = require('zlib');
const gunzip = zlib.createGunzip();

let inFile = fs.createReadStream('./fileForCompress.txt.gz');
let outFile = fs.createWriteStream('./fileForCompress1.txt');

inFile.pipe(gunzip).pipe(outFile);

服务端gzip压缩

判断是否包含accept-encoding首部,且值为gzip。是则返回gzip压缩后的文件,否将返回未压缩的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fs = require('fs');
const http = require('http');
const zlib = require('zlib');
const filepath = './fileForGzip.html';

let server = http.createServer(function(req, res){
let acceptEncoding = req.headers['accept-encoding'];
let gzip;

if(~acceptEncoding.indexOf('gzip')){ // 判断是否需要gzip压缩
gzip = zlib.createGzip();

// 记得响应 Content-Encoding,告诉浏览器:文件被 gzip 压缩过
res.writeHead(200, {
'Content-Encoding': 'gzip'
});
fs.createReadStream(filepath).pipe(gzip).pipe(res);
} else {
fs.createReadStream(filepath).pipe(res);
}

});

server.listen('8080');

服务端字符串gzip压缩

方案:使用zlib.gzipSync()方法对字符串进行gzip压缩。
如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require('http');
const zlib = require('zlib');

const responseText = 'hello nodejs.';

let server = http.createServer(function(req, res){
let acceptEncoding = req.headers['accept-encoding'];
if (~acceptEncoding.indexOf('gzip')) {
res.writeHead(200, {
'content-encoding': 'gzip'
});
res.end(zlib.gzipSync(responseText));
} else {
res.end(responseText);
}
});

server.listen('3000');

3 域名解析

dns.lookup()


1
2
3
4
5
6
const dns = require('dns');

dns.lookup('www.baidu.com', function(err, address, family) {
if(err) throw err;
console.log(address);
});

获取一个域名对应的多个ip

1
2
3
4
5
6
7
const dns = require('dns');
const options = {all: true};

dns.lookup('www.baidu.com', options, function(err, address, family){
if(err) throw err;
console.log(address);
});

dns.resolve4()

1
2
3
4
5
6
const dns = require('dns');

dns.resolve4('id.baidu.com', function(err, address) {
if(err) throw err;
console.log(JSON.stringify(address));
});

dns.lookup()和dns.resolve4()

当配置了本地Host时,dns.lookup()方法会返回host的ip地址,dns.resolve4()无影响。

4 事件循环机制

Node.js 是基于V8引擎的javascript运行环境. Node.js具有事件驱动, 非阻塞I/O等特点. 结合Node API, Node.js 具有网络编程, 文件系统等服务端的功能, Node.js用libuv库进行异步事件处理。

4.1 线程

Node.js的单线程含义, 实际上说的是执行同步代码的主线程. 一个Node程序的启动, 不止是分配了一个线程,而是我们只能在一个线程执行代码. 当出现I/O资源调用, TCP连接等外部资源申请的时候, 不会阻塞主线程, 而是委托给I/O线程进行处理,并且进入等待队列。 一旦主线程执行完成,将会消费事件队列(Event Queue)。 因为只有一个主线程, 只占用CPU内核处理逻辑计算, 因此不适合在CPU密集型进行使用。

p-1.png

什么是事件循环(EventLoop) ?

EventLoop 是一种常用的机制,通过对内部或外部的事件提供者发出请求, 如文件读写, 网络连接 等异步操作, 完成后调用事件处理程序. 整个过程都是异步阶段

Node.js的事件循环机制指当Node.js 启动, 就会初始化一个 event loop, 处理脚本时, 可能会发生异步API行为调用, 使用定时器任务或者nexTick, 处理完成后进入事件循环处理过程。

事件循环阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

每一个阶段都有一个FIFO的callbacks队列, 每个阶段都有自己的事件处理方式. 当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段.

  • timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
  • I/O callbacks 阶段: 执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks; (目前这个阶段)
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.

5 CommonJS和ES6模块的区别

  • require 支持 动态导入,import 不支持,正在提案 (babel 下可支持)
  • require 是 同步 导入,import 属于 异步 导入
  • require 是 值拷贝,导出值变化不会影响导入值;import指向内存地址,导入值会随导出值而变化

5.1 CommonJS

  • 通过 require 引入基础数据类型时,属于复制该变量。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
  • 通过 require 引入复杂数据类型时,数据浅拷贝该对象。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
  • 当使用 require 命令加载某个模块时,就会运行整个模块的代码。
  • 当使用 require 命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  • 循环加载时,属于加载时执行。即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

    5.2 ES6 模块

  • ES6 模块中的值属于【动态只读引用】。
  • 对于只读来说,即不允许修改引入变量的值,import 的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到 import 命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  • 对于动态来说,原始值发生变化,import 加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
  • 循环加载时,ES6 模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。
    上面说了一些重要区别。