前言

之前配置 webpack 的时候,使用了一个插件 progress-bar-webpack-plugin, 功能很简单,就是在打包的时候显示一个骚气的进度条,使用如下

1
2
3
4
5
6
// webpack.config.js
const ProgressBarPlugin = require('progress-bar-webpack-plugin');

module.exports = {
plugins: [new ProgressBarPlugin()],
};

2019-12-26-16-13-12-20191226161312

作为 前后左右端配置工程师 2.0, 今天就尝试用 Node.js 实现一个类似的进度条的功能,我们这里稍微丰富一下,模拟这样一个场景:小李取钱,然后出现正在取钱中的 进度条,最后提示取钱成功

标准输入与输出流

在 Node.js 中,标准的输入输出流分别为:

  • 输入流
  • 输出流
  • 错误流

而流又分为:

  • 可读流 (Readable)
  • 可写流 (Writable)
  • 双工流 (Duplex)
  • 转换流 (Transform)

其中双工流和转换流都是可读可写的。

输入流 (Readable)

1
process.stdin;

输出流 (Writable)

1
process.stdout;

错误流 (Writable)

1
process.stderr;

想实现进度条在命令行中显示,我们需要用到输出流和输入流,前端的老朋友 console.log 其实就是对标准输出的封装

1
2
3
4
5
6
7
8
9
10
const { format } = require('util');

class Console {
log(...args) {
process.stdin.write(`${format.apply(null, args)}\n`);
}
}

const _console = new Console();
_console.log('skr~'); // skr~

2019-12-26-21-34-05-2019122621345

小李取钱

小李来到银行的自动取款机取钱,取款机询问他要取多少:

2019-12-26-21-38-06-2019122621386

要实现 询问 这个功能,我们需要借助一个原生的 Node.js 模块 readline , 它帮助我们从 可读流 中 一行一行的读取数据

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

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

rl.question('[成都银行欢迎您] 请输入要提取的金额 : \r', (money) => {
console.log('money', money);
});

2019-12-26-21-41-55-20191226214155

接下来就是取钱的过程,我们需要显示一个取钱中的进度条,首先实现一个 printProgress 函数,向 process.stdout 标准中打印进度条

1
2
3
4
5
const printProgress = (index) => {
const loadedBar = '='.repeat(index);
const unloadBar = '-'.repeat(0);
process.stdout.write(`提取中 : ${loadedBar}${unloadBar}`);
};

同时增加 getMoney 函数用来接收小李输入的金额,printProgress 会在他按下回车的时候执行

1
2
3
4
5
const getMoney = (money) => {
printProgress(10);
};

rl.question('[成都银行欢迎您] 请输入要提取的金额 : \r', getMoney);

2019-12-26-21-50-13-20191226215013

提示信息有了,由于银行资金比较紧张,还没有实现加载中的动画,没那味,别着急,我们来实现动画

加载动画

前端对于动画肯定不陌生,方式多种多样,有

  • css3 animation
  • setInterval
  • requestAnimationFrame
  • canvas

由于是 Node.js 环境,这里我们只能使用 setInterval 来实现,略微调整一下 getMoney 函数,加入定时器,通过一个自增的 index 的来表示提取的进度

1
2
3
4
5
6
7
const getMoney = (answer) => {
let index = 0;
setInterval(() => {
++index;
printProgress(index);
}, 200);
};

2019-12-26-21-56-41-20191226215641

“取他个香蕉棒棒槌”, 小李在取款机旁骂道,原来是因为虽然一直在输出,但是没有清空之前的内容,显示效果不理想。

清空标准输出

canvas 图形绘制时,我们可以通过 clearRect() 方法不断的清除画布实现动画的绘制,而在命令行中,我们可以使用 readline.clearScreenDown() 来帮助我们清空标准输出

1
2
3
4
5
// 将光标移动到 标准输出 (坐标 0,0 ) 的位置
readline.cursorTo(process.stdout, 0, 0);

// 清空
readline.clearScreenDown(process.stdout);

修改 printProgress 函数,每次打印前清空即可

1
2
3
4
5
6
7
8
9
const limit = 20; // 模拟提取时长

const printProgress = (index) => {
const loadedBar = '='.repeat(index);
const unloadBar = '-'.repeat(limit - index);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
process.stdout.write(`提取中 : ${loadedBar}${unloadBar}`);
};

2019-12-26-22-12-36-20191226221236

一个小时过去了,取款机还在提示 提取中, 原来是忘了写停止动画了,赶紧加上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const print = (money, index) => {
printProgress(index);
if (index === limit) {
process.stdout.write(`\n √ 本次提款:${money} 元、n`);
process.exit(0);
}
};

const getMoney = (answer) => {
let index = 0;
setInterval(() => {
++index;
print(answer, index);
}, 200);
};

2019-12-26-22-12-36-20191226221236

最后,我们为了更加逼真,在进度条的前面加上一个百分比

1
2
3
4
5
6
7
8
9
const printProgress = (index) => {
const offset = limit / 100;
const progress = `${index / offset}%`;
const loadedBar = '='.repeat(index);
const unloadBar = '-'.repeat(limit - index);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
process.stdout.write(`提取中 : ${progress} ${loadedBar}${unloadBar}`);
};

2019-12-26-22-12-36-20191226221236

完整代码如下:

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
const readline = require('readline');

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

const limit = 20; // 模拟提取时长

const printProgress = (index) => {
const loadedBar = '='.repeat(index);
const unloadBar = '-'.repeat(limit - index);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
process.stdout.write(`提取中 : ${loadedBar}${unloadBar}`);
};

const print = (money, index) => {
printProgress(index);
if (index === limit) {
process.stdout.write(`\n √ 本次提款:${money} 元、n`);
process.exit(0);
}
};

const getMoney = (answer) => {
let index = 0;
setInterval(() => {
++index;
print(answer, index);
}, 200);
};

rl.question('[成都银行欢迎您] 请输入要提取的金额 : \r', getMoney);

终于,小李取到了他这个月的工资,他回到工地继续那美滋滋的搬砖生活。

完。

参考资料