0%

nodejs学习记录

一、前言

本文为nodejs学习记录,包含一些语法和踩坑记,以及一些工具代码技巧等

环境

1
node v12.16.1

二、安装

1. node安装

  • 具体下载地址访问官网下载https://nodejs.org/zh-cn/download/
1
2
3
4
5
6
wget xxxxxx             # 下载官网最新二进制文件包
mkdir temp # 创建临时目录
tar -xzvf xxxxx.tar.gz -C temp # 解压到临时目录
sudo mv temp/node_xxx /opt/node # 文件整体移动到opt目录下
sudo ln -s /opt/node/bin/node /usr/local/bin/node # 添加软链接
sudo ln -s /opt/node/bin/npm /usr/local/bin/npm # 添加软链接

2. npm 安装第三方包

2.1. 安装

1
2
3
# -g代表全局安装,不加-g安装到本地目录
# --save代表保存到package.json中
npm install -g xxx --save

2.2. 更新

1
2
3
4
5
6
# 升级npm
npm install -g npm
# 使用npm-check进行包检查更新
npm install -g npm-check
# 检查包更新,使用-g检查全局包,空格选中,回车安装
npm-check -g -u

2.3. 只下载源码

1
2
# 下载mathjax的源码包到当前目录
npm pack mathjax

3. nvm 多版本管理

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
# 自行安装nvm,archlinux上可以在archlinuxcn中找到包
sudo pacman -S nvm

# 列举可以安装的版本
# --lts 只列举长期维护版本
nvm ls-remote

# 列举本机版本
nvm list

# 安装一个版本
nvm install v12.22.10
# 卸载一个版本
nvm uninstall v12.22.10

# 使用一个版本
nvm use v12.22.10
# 使用默认版本
nvm use default

# 设置默认版本
nvm alias default v12.22.10
# 设置默认为最新版本
nvm alias default node

# 给某个版本起别名
nvm alias xxx v1.1.1

# 回归系统的node
nvm deactive

# 不使用nvm
nvm unload
# 重新使用nvm
source /usr/share/nvm/init-nvm.sh

4. 配置源

1
2
# 带-g就是全局设置
npm config set -g registry https://registry.npm.taobao.org

配置源不生效

  • 当前目录有package-lock.json文件,删掉就好了

三、语法相关(ES6)

1. 变量

1.1. let和var的区别

varlet
作用域var可以全局使用let只能在代码块中
未定义使用定义前使用var,会是undefined类型定义前使用,会报错

let特性

  • 同作用域下,let前所有调用变量的行为都报错,包括typeof
  • let要求有大括号,不允许类似if只有一条语句不加大括号

1.2. const类型

  • 大部分同let,有块作用域和不可声明前调用

2. Array 数组

2.1. 一些基础操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let arr = new Array();

/****** 删除 ******/
// 删除头部元素并返回
let head = arr.shift();

// 删除所有xxxId
let tempIndex = arr.indexOf(xxxId);
while (tempIndex !== -1) {
arr.splice(tempIndex, 1);
tempIndex = arr.indexOf(xxxId);
}

/****** 新增 ******/
// 插入到数组前面并返回长度
let len = arr.unshift("test");

/****** 遍历 ******/
// 不需要index,for of结构,返回只有数组的value
for (const v of obj) {
console.log(`value: ${v}`);
}

2.2. 数组去重

  • Set对象类似于数组,但是元素唯一,对数组去重可以使用Set来实现
1
2
3
let arr = [1, 2, 3, 4, 5, 5, 4, 3, 2, 1];

arr = Array.from(new Set(arr));

2.3. map 遍历数组返回新的数组

1
2
3
4
5
6
> let arr = [1, 2, 3, 4, 5, 6];
undefined
> let b = arr.map((item) => {if (item < 3) {return undefined;} return item+1;})
undefined
> b
[ undefined, undefined, 4, 5, 6, 7 ]

2.4. filter 遍历数组筛选

1
2
3
4
> let b = [ undefined, undefined, 6, 7, 8, 9 ]
[ undefined, undefined, 6, 7, 8, 9 ]
> b.filter((item) => { return item != undefined; })
[ 6, 7, 8, 9 ]

2.5. reduce 遍历数组累加

1
2
3
4
> let b = [ 6, 7, 8, 9 ]
[ 6, 7, 8, 9 ]
> b.reduce((pre, item) => { return pre+item; })
30

3. 对象

3.1. 一些基本操作

1
2
3
4
5
6
7
8
9
10
11
/****** 遍历对象 ******/
let obj = {};
// 简单写如下,但是eslint扫描过不了
// 返回的是对象的key或者数组的index
for (const k in obj) {
console.log(`key: ${k}, value: ${obj[k]}`);
}
// 下面是官方要求写法
for (const k of Object.keys(obj)) {
console.log(`key: ${k}, value: ${obj[k]}`);
}

3.2. 解构对象

基本赋值

1
2
let o = { p: 42, q: tru e};
let { p, q } = o;

无声明赋值

1
2
let a, b;
({a, b} = {a: 1, b: 2});
1
2
3
4
注意:赋值语句周围的圆括号 ( ... ) 在使用对象字面量无声明解构赋值时是必须的。
{a, b} = {a: 1, b: 2} 不是有效的独立语法,因为左边的 {a, b} 被认为是一个块而不是对象字面量。
然而,({a, b} = {a: 1, b: 2}) 是有效的,正如 var {a, b} = {a: 1, b: 2}
你的 ( ... ) 表达式之前需要有一个分号,否则它可能会被当成上一行中的函数执行。

给新的变量名赋值

可以从一个对象中提取变量并赋值给和对象属性名不同的新的变量名。

1
2
3
4
5
let o = { p: 42, q: true };
let { p: foo, q: bar } = o;

console.log(foo); // 42
console.log(bar); // true

默认值

变量可以先赋予默认值。当要提取的对象对应属性解析为 undefined,变量就被赋予默认值。

1
2
3
4
let { a = 10, b = 5 } = { a: 3 };

console.log(a); // 3
console.log(b); // 5

3.3. 变量作为key

1
2
3
4
let a = 'aaaaa';
let data = {
[a]: 1
};

4. 异步处理

4.1. Promise

4.1.1 Promise.all

  • Promise.all()主要用于将多个异步处理函数整合成一个异步处理,外层使用await可以等待多个异步处理函数处理完成

1. 和数组结合

  • 数组遍历每一个item进行处理,如果处理之间不相关,可以使用异步的方式,但是又需要等待处理完成
  • 不能使用foreach,因为Promise.all依赖返回的Promise对象,foreach是没有返回的
1
2
3
4
5
let testArray = [];
...
await Promise.all(testArray.map(async (item) => {
...
}));

2. 限制并发数

  • 如果使用数组的map进行遍历,如果数组元素个数特别多,可能会导致协程个数特别多
  • 需要对Promise.all()加上数量限制,仅允许一定数量的协程
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
'use strict';

/* 用法

const limit = new PromiseLimit(200);
await Promise.all(vpnUsers.map((user) => {
return limit.build(async () => {
...
});
}));

*/

class PromiseLimit {
constructor(n) {
this.limit = n;
this.count = 0;
this.queue = [];
}

enqueue(fn) {
// 关键代码: fn, resolve, reject 统一管理
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
});
}

dequeue() {
if (this.count < this.limit && this.queue.length) {
// 等到 Promise 计数器小于阈值时,则出队执行
const { fn, resolve, reject } = this.queue.shift();
this.run(fn).then(resolve).catch(reject);
}
}

// async/await 简化错误处理
async run(fn) {
this.count++;
// 维护一个计数器
let value;
try {
value = await fn();
} finally {
this.count--;
}
// 执行完,看看队列有东西没
this.dequeue();
return value;
}

build(fn) {
if (this.count < this.limit) {
// 如果没有到达阈值,直接执行
return this.run(fn);
} else {
// 如果超出阈值,则先扔到队列中,等待有空闲时执行
return this.enqueue(fn);
}
}
}

module.exports = PromiseLimit;

5. regExp 正则

5.1. test 测试正则

1
2
3
4
const binStrReg = /^\\[0-9a-fA-F]{2}$/;

console.log(binStrReg.test('aa')); // false
console.log(binStrReg.test('\\aa')); // true

5.2. exec 匹配返回符合要求的字符串

1
2
3
let str = 'aaabbbaaaccc';
let reg = /aaa/;
console.log(reg.exec(str)); // [ 'aaa', index: 0, input: 'aaabbbaaaccc', groups: undefined ]
  • exec只返回第一个,想要返回全部,需要使用while和g来实现
1
2
3
4
5
6
7
let str = 'aaabbbaaaccc';
let reg = /aaa/g;
while(res = reg.exec(str)){
console.log(res);
}
// [ 'aaa', index: 0, input: 'aaabbbaaaccc', groups: undefined ]
// [ 'aaa', index: 6, input: 'aaabbbaaaccc', groups: undefined ]

6. 字符串操作

6.1. 正则

match 匹配输出

1
2
3
let str = 'aaabbbaaaccc';
console.log(str.match(/aaa/)); // [ 'aaa', index: 0, input: 'aaabbbaaaccc', groups: undefined ]
console.log(str.match(/aaa/g)); // [ 'aaa', 'aaa' ]

search 匹配输出索引

1
2
let str = 'aaabbbaaaccc';
console.log(str.search(/bbb/)); // 3

replace 替换

1
2
3
let str = 'aaabbbaaaccc';
console.log(str.replace(/aaa/, 'lll')); // lllbbbaaaccc
console.log(str.replace(/aaa/g, 'lll')); // lllbbblllccc

split 字符串分割

1
2
3
let str = 'aaa,bbb|aaa ccc#sss@uuu&ccc';
console.log(str.split(/[,| #@&]/)); // [ 'aaa', 'bbb', 'aaa', 'ccc', 'sss', 'uuu', 'ccc' ]
console.log(str.split(/[,| #@&]/, 3)); // [ 'aaa', 'bbb', 'aaa' ]

6.2. 字符串切割

1
2
3
4
5
let str = 'abcdefghi';
console.log(str.slice(-4)); // fghi
console.log(str.slice(0, -4)); // abcde
console.log(str.slice(0, 3)); // abc
console.log(str.slice(-4, -2)); // fg

6.3. join 使用间隔将字符串数组转字符串

1
2
3
4
> let str = ['a', 'b', 'c']
undefined
> str.join('#')
'a#b#c'

7. 引用json文件

  • 直接引用相对路径的文件即可,加不加后缀一样,会变成对象存到变量中
1
2
const testJson = require('./aaa');
console.log(testJson);

8. 类型转换

8.1. float到其他

1
2
let intVar = Math.floor(2.3);   // 向下取整
let intVar1 = Math.ceil(3.2); // 向上取整

四、技巧

1. 框架

  • 服务端后台框架推荐eggjs

2. 交换元素的值

  • 使用ES6的解构赋值特性
1
2
3
4
let x = 1;
let y = 2;

[x, y] = [y, x];

3. jsonSchema的几种用法

3.1. required依赖某一项

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
const xxxSchema = {
name: 'xxx配置',
type: 'object',
required: true,
properties: {
test: {
name: 'xxxxx',
type: 'object',
required: true,
properties: {
enable: {
name: 'enable',
type: 'number',
required: true,
rule: [{
name: 'enums',
enums: [0, 1],
message: '开启test',
}],
},
hhh: {
name: 'hhh',
type: 'string',
required: true,
// required依赖test.enable是否开启
dependOn: 'test.enable',
dependValue: [1],
},
},
},
},
};

3.2. callback自定义检查

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
const xxxSchema = {
name: 'xxx配置',
type: 'object',
required: true,
properties: {
enable: {
name: 'enable',
type: 'number',
required: true,
rule: [{
name: 'enums',
enums: [0, 1],
message: '开启test',
}],
},
test: {
name: 'xxxxx',
type: 'string',
required: true,
callback(value, params, schema) {
try {
if (false) {
return {
valid: false,
msg: 'test错误',
};
}
if (value === 'aaa') {
throw new Error('test不能是aaa');
}
if (params.enable === 0) {
throw new Error('enable 0');
}
} catch (err) {
return {
valid: false,
msg: err.message,
};
}
},
},
},
};

4. 暂停等待用户输入

  • 这里只展示同步的,异步的自己裁代码
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 readline = require('readline');
const { exit } = require('process');

function readSyncByRl(tips) {
tips = tips || '> ';

return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question(tips, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}

async function main() {
let confirmText = await readSyncByRl('是否继续?[y/N] ');
if (confirmText !== 'Y' && confirmText !== 'y') {
exit(1);
}
...
}

5. 进度条展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
'use strict';
const ProgressBar = require('progress');

// :bar 代表进度条 '-------'
// :percent 代表百分比 'xx%'
// total为计算使用的值,width是显示宽度,complete为完成的展示字符
const bar = new ProgressBar('progress: [:bar] :percent', { total: 100, width: 100, complete: '=' });
let rate = 0;
const timer = setInterval(
() => {
rate += 0.1;
// update参数为0-1的值
bar.update(rate);
if (bar.complete) {
console.log("\ncomplete\n");
clearInterval(timer);
}
},
500
);

6. 时间相关

1
2
3
4
5
6
7
// 返回当前毫秒级时间戳
> Date.now()
1642231982983
// 时间字符串转时间戳
> const temp = new Date('Wed Feb 23 2022 02:00:00 GMT+0800')
> temp.getTime()
1645552800000

7. 生成uuid

7.1. 简单生成,不依赖第三方库

1
2
3
4
5
6
function uuid() {
var temp_url = URL.createObjectURL(new Blob());
var uuid = temp_url.toString(); // blob:https://xxx.com/b250d159-e1b6-4a87-9002-885d90033be3
URL.revokeObjectURL(temp_url);
return uuid.substr(uuid.lastIndexOf("/") + 1);
}

五、好用的第三方node_module

1. yargs 命令行参数解析

1.1. 一些基本用法

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
/* eslint-disable no-console */
'use strict';
const yargs = require('yargs');

function main() {
// 处理解析参数
let argv = yargs
.usage('Usage: util.js [command] <options>')
.strict() // 严格模式,无法识别的参数会报错
.alias('h', 'help')
.version(false) // 不显示版本号
.command('cmd1', 'cmd1 description')
.command('cmd2', 'cmd2 discription')
.command('cmd3', 'cmd3 discription')
.option('option_test_1', {
describe: 'option_test_1 description',
type: 'string',
})
.option('option_test_2', {
describe: 'option_test_2 description',
type: 'boolean',
demandOption: true,
})
// 要求最少一个命令
.demandCommand(1, '请输入一个命令,cmd1/cmd2/cmd3')
.argv;
console.log(argv);
}

main();

效果

正常

1
2
3
4
5
6
7
[xxx@xxxx ~]# NODE_PATH=/path/to/node_modules node util.js cmd1 --option_test_1 aaa --option_test_2
{
_: [ 'cmd1' ],
option_test_1: 'aaa',
option_test_2: true,
'$0': 'util.js'
}
1
2
3
4
5
6
7
[xxx@xxxx ~]# NODE_PATH=/path/to/node_modules node util.js cmd1 --option_test_1=aaa --option_test_2 aaa
{
_: [ 'cmd1', 'aaa' ],
option_test_1: 'aaa',
option_test_2: true,
'$0': 'util.js'
}

异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[xxx@xxxx ~]# NODE_PATH=/path/to/node_modules node util.js
Usage: util.js [command] <options>

命令:
test.js cmd1 cmd1 description
test.js cmd2 cmd2 discription
test.js cmd3 cmd3 discription

选项:
--option_test_1 option_test_1 description [字符串]
--option_test_2 option_test_2 description [布尔]
-h, --help 显示帮助信息 [布尔]

请输入一个命令,cmd1/cmd2/cmd3
1
2
3
4
5
6
7
8
9
10
11
[xxx@xxxx ~]# NODE_PATH=/path/to/node_modules node util.js cmd1
util.js cmd1

cmd1 description

选项:
--option_test_1 option_test_1 description [字符串]
--option_test_2 option_test_2 description [布尔] [必需]
-h, --help 显示帮助信息 [布尔]

缺少必须的选项:option_test_2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[xxx@xxxx ~]# NODE_PATH=/path/to/node_modules node util.js aaa cmd1
Usage: util.js [command] <options>

命令:
util.js cmd1 cmd1 description
util.js cmd2 cmd2 discription
util.js cmd3 cmd3 discription

选项:
--option_test_1 option_test_1 description [字符串]
--option_test_2 option_test_2 description [布尔]
-h, --help 显示帮助信息 [布尔]

无法识别的选项:aaa
1
2
3
4
5
6
7
8
9
10
11
[xxx@xxxx ~]# NODE_PATH=/path/to/node_modules node util.js cmd1 --aaa
util.js cmd1

cmd1 description

选项:
--option_test_1 option_test_1 description [字符串]
--option_test_2 option_test_2 description [布尔]
-h, --help 显示帮助信息 [布尔]

无法识别的选项:aaa

2. cron-parser cron字符串解析

2.1. 基本用法

1
2
3
4
5
6
7
8
> const interval = parser.parseExpression('0 0 2 * * *')
undefined
> interval.next().toString()
'Sun Jan 16 2022 02:00:00 GMT+0800'
> interval.next().toString()
'Mon Jan 17 2022 02:00:00 GMT+0800'
> interval.next().toString()
'Tue Jan 18 2022 02:00:00 GMT+0800'

注意事项

  • 一旦parser建立后,下一次时间就是建立当时的时间的下一次,如果系统时间改变,不会变更next的结果,用来判断需要小心

3. moment 时间格式化输出

1
2
3
> const moment = require('moment')
> moment().format('YYYYMMDDHHmmss')
'20230912113225'

踩坑记

1. 脚本调用promise的函数不会自动退出

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

function readSyncByRl(tips) {
tips = tips || '> ';

return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question(tips, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}

async function main() {
let confirmText = await readSyncByRl('是否继续?[y/N] ');
console.log(confirmText);
}
  • 使用ssh执行node xxx.js,ssh不会退出,需要Ctrl + c才行
  • 但是终端中执行node xxx.js,会自动退出
  • 解决办法是,在最后加上exit();
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 readline = require('readline');
const { exit } = require('process');

function readSyncByRl(tips) {
tips = tips || '> ';

return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question(tips, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}

async function main() {
let confirmText = await readSyncByRl('是否继续?[y/N] ');
console.log(confirmText);
exit();
}