node知识点
# node
node 中,绝大部分的操作都是以异步的形式进行调用,这样的意义在于,在 Node 中,我们可以从语言层面很自然的进行并行 I/O 操作,每个调用之间无需等待之前的 I/O 调用结束。在编程模型上可以大大提升效率。
Node 模块的实现 在 node 中引入模块需要经历以下三个步骤:
- 路径分析
- 文件定位
- 编译执行
在 Node 中,模块分为两类:一类是 node 提供的模板,称为核心模块。另一类是用户编写的模块,成为文件模块。 核心模块在 node 源代码的编译过程中,编译进了二进制文件。在 Node 进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。 不论是核心模块还是文件模块, require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检査先于文件模块的缓存检査。 Node 缓存的是编译和执行后的对象
# Node 是单线程的吗?
首先,进程是计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是资源分配最小的单位。进程是线程的容器,是一对多的关系。每开启一个服务、运行一个实例,就是开一个服务进程。每一个进程都拥有自己的独立空间地址、数据栈,一个进程无法访问另外一个进程里定义的变量、数据结构,只有建立了 IPC 通信,进程之间才能数据共享。
# 单线程
javascript 就是属于单线程,程序按顺序执行。若存在队列中,前面一个执行完后,后面才可以执行,当你在使用单线程语言编码时切勿有过多耗时的同步操作,否则线程会造成阻塞,导致后续响应无法处理。
- node 虽然是单线程,但是其基于事件驱动、异步非阻塞模式,可以应用于高并发场景,避免了线程创建、线程之间上下文切换所产生的资源开销。
- 当你的项目中需要有大量计算,CPU 耗时的操作时候,要注意考虑开启多进程来完成了。
- Node 在开发过程中,错误会引起整体应用退出,所以异常的捕获抛出及进程的守护是很有必要的。
- node 中的进程 node 中的进程 Process 是一个全局对象。
# 流 stream
- stdin:标准输入
- stdout:标准输出
- stderr:标准错误
stream 实现文件分块读取
var stream = fs.createReadStream('mytext.txt')
stream.on('data', function(chunk) {
...
})
stream.on('end', function (chunk) {
...
})
2
3
4
5
6
7
# 工作目录 path
- __dirname 获取执行文件时该文件在文件系统中所在的目录
- process.cwd()获取 node 命令发起所在的文件夹的绝对路径
- process.chdir()更改工作目录
- process.env 变量访问 shell 环境下的变量
- process.exit 退出
- ./: 在 require()方法内作用和 dirname 相同,其他情况下和 cwd()相同
path.normalize 规范化路径
path.join()
路径拼接,Unix 系统是/,Windows 系统是\。如果传入的为空字符串,则返回 '.',代表当前文件夹,如果传入非字符串则会报错
path.parse()
返回一个对象,对象是传入的字符串拆分的路径
const path = require("path"); path.parse('D:/DaimaKu/vuepress/vuepress-interview-github/index.js'); { root: 'D:/', // 代表根目录 dir: 'D:/DaimaKu/vuepress/vuepress-interview-github', // 文件所在的文件夹 base: 'index.js', // 一整个文件 ext: '.js', // 文件名后缀 name: 'index' // 文件名 }
1
2
3
4
5
6
7
8
9path.basename() 顾名思义返回文件名
path.dirname() 返回文件所在文件夹的完整路径
path.extname() 返回文件后缀,没有会返回空,如果末尾是个., 也会返回.
path.resolve()
# 常用方法
promisify (opens new window): 传入一个遵循常见的错误优先的回调风格的函数(即以 (err, value) => ... 回调作为最后一个参数),并返回一个返回 promise 的版本。
import { promisify } = require('utils');
import fs = require('fs');
const readfile = promisify(fs);
app.use(async ctx => {
try {
ctx.body = await readFile(resolve(__dirname, 'test.json'));
} catch(err) { ctx.body = err };
});
2
3
4
5
6
7
8
9
# Node 开启 https 服务
node 开启 https 服务需要两步:一是生成签名证书,二是还需借助 Node.js 提供的系统模块 HTTPS 完成。通常在企业中面向公网使用的证书通常是由全球权威 CA 机构签发的证书,受各大浏览器厂商信任。在开发测试时为了简单点我们可以自签名证书,但是这在浏览器中打开时会有安全问题提示。
- 使用 openssl 的 genrsa 命令生成一个服务器私钥文件
# genrsa 生成密钥
# -out 指定输出的文件
openssl genrsa -out server.key 2048
2
3
- 生成证书请求文件
# -new 执行生成新的证书请求
# -key 指定输入的密钥
openssl req -new -key server.key -out server.csr
# 会有以下交互提示,Common Name 这个可以自定域名,修改 hosts 文件域名映射即可。
Country Name (2 letter code) []:CN
State or Province Name (full name) []:ShangHai
Locality Name (eg, city) []:ShangHai
Organization Name (eg, company) []:Node.js
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:test.https.com
Email Address []:
2
3
4
5
6
7
8
9
10
11
12
- 根据第 2 步的证书请求文件和第 1 步的服务器私钥文件生成证书
# x509 根据现有的证书请求生成自签名根证书
# -days 设置证书的有效天数
# -in 指定输入证书请求文件
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
2
3
4
成功之后会生成如下 3 个文件:
- key 是服务器上的私钥文件。
- csr 是证书请求签名文件,用于提交给证书颁发机构 CA。
- crt 是证书颁发机构 CA 签名后的证书。
- 使用 Node.js 中 HTTPS 模块开启一个服务
// app.js
const https = require("https");
const fs = require("fs");
const PORT = 8443;
const options = {
key: fs.readFileSync("./server.key"),
cert: fs.readFileSync("./server.crt"),
};
https
.createServer(options, (req, res) => {
res.writeHead(200);
res.end("Hello World!");
})
.listen(PORT, () => console.log(`App listening on port ${PORT}!`));
// express
const express = require("express");
const https = require("https");
const fs = require("fs");
const app = express();
const PORT = 8443;
const options = {
key: fs.readFileSync("./server.key"),
cert: fs.readFileSync("./server.crt"),
};
https
.createServer(options, app)
.listen(PORT, () => console.log(`App listening on port ${PORT}!`));
app.get("/", (req, res) => res.send("Hello World!"));
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
# window 和 mac 下设置 NODE_ENV 变量
采用在 package.json 中设置变量的方式
- mac: export XXX=XXX
- window: set XXX=XXX
"scripts": {
"start": "set NODE_ENV=development && nodemon -w src --exec \"babel-node src\"",
"build": "babel src --out-dir dist",
"run-build": "node dist",
"test": "echo \"Error: no test specified\" && exit 1"
}
2
3
4
5
6
# socket.io 如何与 koa/egg 配合使用
我们都知道完整的 socket.io 通信由两部分组成:
与 NodeJS HTTP 服务器集成(或安装在其上)的 socket.io 在浏览器端加载的客户端库 socket.io-client 如果我们直接使用 koa 或者 egg, 我们需要将它们内部集成的 http 和 socket.io 做兼容, 此时我们可以这样处理:
import koa from "koa";
import http from "http";
const app = new koa();
const server = http.createServer(app.callback());
const io = require("socket.io")(server);
// 正常的业务处理
// io
io.on("connection", (socket) => {
console.log("a user connected");
socket.on("doc load", (msg) => {
console.log("doc load", msg);
io.emit("getData", users);
});
});
server.listen(3000, () => {
// ...
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# nodejs 创建定时任务
使用 node-schedule 工具包。Node Schedule 是用于 Node.js 的灵活的 cron 类和非 cron 类作业调度程序。它允许我们使用可选的重复规则来安排作业(任意函数)在特定日期执行。它在任何给定时间仅使用一个计时器(而不是每秒钟/分钟重新评估即将到来的作业)。
let schedule = require("node-schedule");
let testJob = schedule.scheduleJob("42 * * * *", function () {
console.log("将在未来的每个时刻的42分时执行此代码, 比如22:42, 23:42");
});
2
3
4
5
# nodejs 读取大文件报错解决方案
在 nodejs 中 我们可以使用两种方式来读写文件, 如下:
fs.readFile() 一次性将文件读取进内存中, 如果文件过大会导致 node 内存不够而报错 fs.createReadStream() 以文件流的方式读取, 此时可以不用担心文件的大小 由以上介绍可知如果我们要读取的文件可能会很大(比如视频等大文件), 我们一开始就要使用 fs.createReadStream(), 其实如果我们需要对文件进行解析, 比如要对简历等文件进行逐行解析提取关键语料, 我们可以使用 node 的 readline 模块, 此时我们就可以对文件进行逐行读取并解析, 如下案例:
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const readlineTask = readline.createInterface({
input: fs.createReadStream(path.join(\_\_dirname, './h5-dooring')),
});
readlineTask.on('line', function(chunk) {
// 读取每一行数据
});
readlineTask.on('close', function() {
//文件读取结束的逻辑
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# nodejs 如何开启 gzip 优化网站性能
对于 nodejs 开启 gzip 的操作也属于 node 性能优化的一部分, 经过这样的处理可以让我们的网站加载更快, 我们可以使用 koa 的 koa-compress 中间件来实现 gzip 功能. 具体实现如下:
import koa from "koa";
import compress from "koa-compress";
const app = new koa();
// 开启 gzip
const options = { threshold: 2048 };
app.use(compress(options));
2
3
4
5
6
7
# 父子进程通信
// child.js
function computedTotal(arr, cb) {
// 耗时计算任务
}
// 与主进程通信
// 监听主进程信号
process.on("message", (msg) => {
computedTotal(bigDataArr, (flag) => {
// 向主进程发送完成信号
process.send(flag);
});
});
// main.js
const { fork } = require("child_process");
app.use(async (ctx, next) => {
if (ctx.url === "/fetch") {
const data = ctx.request.body;
// 通知子进程开始执行任务,并传入数据
const res = await createPromisefork("./child.js", data);
}
// 创建异步线程
function createPromisefork(childUrl, data) {
// 加载子进程
const res = fork(childUrl);
// 通知子进程开始work
data && res.send(data);
return new Promise((reslove) => {
res.on("message", (f) => {
reslove(f);
});
});
}
await next();
});
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
# node 端实现图片编辑/压缩
const images = require("images");
images("input.jpg") //加载图像文件
.size(400) //等比缩放图像到400像素宽
.draw(images("logo.png"), 10, 10) //在(10,10)处绘制Logo
.save("output.jpg", {
//保存图片到文件,图片质量为50
quality: 50,
});
2
3
4
5
6
7
8
9
# 本地拷贝 demo
小文件拷贝
// 文件拷贝 将data.txt文件中的内容拷贝到copyData.txt
// 读取文件
const fileName1 = path.resolve(__dirname, "data.txt");
fs.readFile(fileName1, function (err, data) {
if (err) {
// 出错
console.log(err.message);
return;
}
// 得到文件内容
var dataStr = data.toString();
// 写入文件
const fileName2 = path.resolve(__dirname, "copyData.txt");
fs.writeFile(fileName2, dataStr, function (err) {
if (err) {
// 出错
console.log(err.message);
return;
}
console.log("拷贝成功");
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
大文件分片读取
// copy 方法
function copy(src, dest, size = 16 * 1024, callback) {
// 打开源文件
fs.open(src, 'r', (err, readFd) => {
// 打开目标文件
fs.open(dest, 'w', (err, writeFd) => {
let buf = Buffer.alloc(size);
let readed = 0; // 下次读取文件的位置
let writed = 0; // 下次写入文件的位置
(function next() {
// 读取
fs.read(readFd, buf, 0, size, readed, (err, bytesRead) => {
readed += bytesRead;
// 如果都不到内容关闭文件
if (!bytesRead) fs.close(readFd, err => console.log('关闭源文件'));
// 写入
fs.write(writeFd, buf, 0, bytesRead, writed, (err, bytesWritten) => {
// 如果没有内容了同步缓存,并关闭文件后执行回调
if (!bytesWritten) {
fs.fsync(writeFd, err => {
fs.close(writeFd, err => return !err && callback());
});
}
writed += bytesWritten;
// 继续读取、写入
next();
});
});
})();
});
});
}
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
使用 stream 实现文件拷贝
const fs = require("fs");
const path = require("path");
// 两个文件名
const fileName1 = path.resolve(__dirname, "data.txt");
const fileName2 = path.resolve(__dirname, "data-bak.txt");
// 读取文件的 stream 对象
const readStream = fs.createReadStream(fileName1);
// 写入文件的 stream 对象
const writeStream = fs.createWriteStream(fileName2);
// 通过 pipe执行拷贝,数据流转
readStream.pipe(writeStream);
// 数据读取完成监听,即拷贝完成
readStream.on("end", function () {
console.log("拷贝完成");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# node 事件循环机制
按优先级往下排,node 的事件循环分为以下几个阶段
- microtask queue: promise,nextTick
- timer queue: settimeout,setInterval
- I/O queue:I/O 操作
- check queue:setImmediate
- close queue: 任何 close 事件的回调函数
- 对于 nextTick,Node 执行完所有同步任务,接下来就会执行 process.nextTick 的任务队列。微任务队列追加在 process.nextTick 队列的后面,也属于本轮循环。
- 倘若在 timer queue 的队列阶段中再次使用到了例如 check queue 队列中的方法,由于该队列还没执行,会将方法放入本次循环的 check queue 中执行,这点需要注意。
- 事件循环中的 setTimeOut 与 setImmediate,从优先级上来说 settimeout 会先一步执行,但实际上 setTimeout 的第二个参数设置为 0 时,Node 做不到 0 毫秒,最少也需要 1 毫秒,根据官方文档,第二个参数的取值范围在 1 毫秒到 2147483647 毫秒之间。也就是说,setTimeout(f, 0)等同于 setTimeout(f, 1)。如果执行时没到 1 毫秒,timer queue 阶段就会被跳过,从而先执行 checkqueue 的回调。