Node基础篇

最简短的开场白

Node 是什么

  • 用于编写服务器端应用

  • javascript 核心语法

  • 只是操作的对象不同

    前端DOM文档对象
    BOM/DOM浏览器对象
    XMLHttpRequest/fetch网络通讯
    后端os操作系统
    process进程
    fs文件系统
    net网络通讯

API 哪里找

英文 https://nodejs.org/dist/latest-v10.x/docs/api/

中文 http://nodejs.cn/api/

运行/调试/模块 - 如何搭建万里长城

Helloworld

1
console.log("hello world");

bash 运行

1
2
3
node helloworld/index.js
# 或
node helloworld

Nodemon 自动重启

监视代码修改,自动重启

1
2
npm i nodemon -g
nodemon helloworld

Vscode 调试 debug

单元测试 Jest

安装 jest 库

1
npm install jest -g

在___tests___文件夹中创建 index.spec.ts

1
2
3
test("Hello world", () => {
require("../index");
});

运行

1
jest helloworld

Exports 与 Require

1
2
3
4
5
6
const str = "helloworld";
// module.exports = str
// 或
// module.exports.str = str
// 或
exports.str = str;

测试程序

1
2
3
4
it("测试Export", () => {
const string = require("../index");
console.log("export", string);
});

测试代码生成工具

  • 掌握 fs 中的同步方法

  • path 包

生成测试文件名

1
2
3
4
5
6
test("测试文件名称", () => {
const src = new (require("../index"))();
const ret = src.getTestFileName("/abc/class.js");
console.log("getSourceName", ret);
expect(ret).toBe("/abc/__test__/class.spec.js");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require("path");
module.exports = class TestNow {
/**
* 生成测试文件名
* @param {*} filename
*/
getTestFileName(filename) {
const dirName = path.dirname(filename);
const baseName = path.basename(filename);
const extname = path.extname(filename);
const testName = baseName.replace(extname, `.spec${extname}`);

return path.format({
root: dirName + "/__test__/",
base: testName
});
}
};

生成测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
test("生成测试代码", () => {
const src = new (require("../index"))();
const ret = src.getTestSource("fun", "class");
expect(ret).toBe(
`
test('TEST fun',() => {
const fun = require('../class')
const ret = fun()
// expect(ret)
// .toBe('test ret')
})
`
);
});

异步编程 - 如何控制好异步过程

参考资料 阮一峰 Javascript 异步编程的 4 种方法)

  • JS 的执行环境是单线程(Single thread)

  • I/O 处理需要回调函数异步处理 (异步 I/O)

  • 前端异步 IO 可以消除 UI 阻塞,提高用户体验

  • 而放在后端则可以提高 CPU 和内存里利用率

串联异步处理

异步操作队列化,按照期望的顺序执行。

Callback

回调地域太可怕

1
2
3
4
5
6
7
8
9
10
11
12
const logTime = name => {
console.log(`Log....${name} ` + new Date().toLocaleTimeString());
};

exports.callback = () => {
setTimeout(() => {
logTime("callback 1");
setTimeout(() => {
logTime("callback 2");
}, 100);
}, 100);
};

测试代码

1
2
3
4
5
test("callback", done => {
callback();
// 延时4s结束
setTimeout(done, 1000);
});

Promise

The Promise object is used for asynchronous computations. A Promise represents a single asynchronous operation that hasn’t completed yet, but is expected in the future.

译文:Promise 对象用于异步操作,它表示一个尚未完成且预计在未来完成的异步操作。

说白了就是一个异步执行的状态机,异步执行的承诺。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promise = (name, delay = 100) =>
new Promise(resolve => {
setTimeout(() => {
logTime(name);
resolve();
}, delay);
});

exports.promise = () => {
promise("Promise1")
.then(promise("Promise2"))
.then(promise("Promise3"))
.then(promise("Promise4"));
};

Gennerator

ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。 基本

  • function -> function* 称为 Gennerator 函数
  • 函数内部有 yield 表达式。
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* func() {
console.log("one");
yield "1";
console.log("two");
yield "2";
console.log("three");
return "3";
}

const f = func();
f.next();
// one
// {value: "1", done: false}
f.next();
// two
// {value: "2", done: false}
f.next();
// three
// {value: "3", done: true}
f.next();
// {value: undefined, done: true}

// 或者通过迭代器
for (const [key, value] of func()) {
console.log(`${key}: ${value}`);
}

逻辑代码

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
let co = function (gen, name) {
var it = gen(name);
var ret = it.next();
ret.value.then(function (res) {
it.next(res);
});
};
exports.generator = () => {
const generator = function* (name) {
yield promise(name + 1);
yield promise(name + 2);
yield promise(name + 3);
yield promise(name + 4);
};
let co = generator => {
if ((it = generator.next().value)) {
it.then(res => {
co(generator);
});
} else {
return;
}
};
co(generator("Co-Generator"));
};

async/await

async/await 是 es7 推出的一套关于异步的终极解决方案

  • 任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行。
  • async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。
1
2
3
4
5
6
exports.asyncAwait = async () => {
await promise("Async/Await1");
await promise("Async/Await2");
await promise("Async/Await3");
await promise("Async/Await4");
};

事件监听方式处理

采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.event = async () => {
const asyncFun = name => event => {
setTimeout(() => {
logTime(name);
event.emit("end");
}, 100);
return event;
};

const ary = [asyncFun("event1"), asyncFun("event2"), asyncFun("event3")];

const { EventEmitter } = require("events");
const event = new EventEmitter();
let i = 0;
event.on("end", () => i < ary.length && ary[i++](event));
event.emit("end");
};

eventEmmiter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promise = (name, delay = 100) =>
new Promise(resolve => {
setTimeout(() => {
logTime(name);
resolve();
}, delay);
});

exports.promise = () => {
promise("Promise1")
.then(promise("Promise2"))
.then(promise("Promise3"))
.then(promise("Promise4"));
};

扩展阅读 eventEimitter 源码解析 / 订阅发布机制

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
class EventEmitter {
constructor() {
this.handler = {};
}
on(eventName, callback) {
if (!this.handles) {
this.handles = {};
}
if (!this.handles[eventName]) {
this.handles[eventName] = [];
}
this.handles[eventName].push(callback);
}
emit(eventName, ...arg) {
if (this.handles[eventName]) {
for (var i = 0; i < this.handles[eventName].length; i++) {
this.handles[eventName][i](...arg);
}
}
}
}

const event = new EventEmitter();
event.on("some_event", num => {
console.log("some_event 事件触发:" + num);
});
let num = 0;
setInterval(() => {
event.emit("some_event", num++);
}, 1000);

异步处理并行

promisify

IO 处理

同步与异步读取文件

补充资料 https://nodejs.org/dist/latest-v10.x/docs/api/fs.html

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读取一个文件的数据

Promisify

buffer - 操作二进制数据

生成 Base64 编码

1
2
3
4
5
6
7
8
9
exports.imageUrl = () => {
const mime = 'image/png'
const encoding = 'base64'
const base64Data = fs.readFileSync(`${__dirname}/image.png`).toString(encoding)
const uri = `data:${mime};${encoding},${base64Data}`
// data:image/png;base64,
fs.writeFileSync(`${__dirname}/index.html`,`<img src='${uri}' />`)
// console.log(uri)
}

stream - 流的操作

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

  • 流是能够读写的
  • 是基于事件实现的一个实例

理解流的最好方式就是想象一下没有流的时候怎么处理数据:

  • fs.readFileSync 同步读取文件,程序会阻塞,所有数据被读到内存

  • fs.readFile 阻止程序阻塞,但仍会将文件所有数据读取到内存中

  • 希望少内存读取大文件,读取一个数据块到内存处理完再去索取更多的数据

进程/子进程

process

1
2
3
4
5
6
// 查看PATH
console.log(process.env.PATH.split(":").join("\n"));
console.log(`arch:${process.arch}`);
console.log(`platform:${process.platform}`);
console.log(`获取内存使用情况 memoryUsage:${process.memoryUsage()}`);
console.log(`获取命令行参数 argv:${process.argv}`);

自动重启工具(Nodemon)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require("fs");
const { spawn } = require("child_process");
const { resolve } = require("path");
function watch() {
const [cmd, , source, ...argv] = process.argv;
// console.log('abc', cmd, source, argv)
const childProcess = spawn(cmd, [source, ...argv]);
childProcess.stdout.pipe(process.stdout);
childProcess.stderr.pipe(process.stderr);
const watcher = fs.watch(resolve(__dirname, source), () => {
console.log("File changed, reloading.");
childProcess.kill();
watcher.close();
watch();
});
}
watch();