导引
这一篇博客主要收到了 YouTuber Fireship 的5分钟编程系列的如何快速使用 Node.js 写出一个 cli 程序的启发。正好工作之中也用到了,所以就此沉淀一篇文章,以防这个技能后面丢了。在这篇博客中,第一个部分我会还原我会尽量保持原视频的思路,然后删除一些可有可无的部分,作为二次加工,这样子吸收知识的效率会增加。之后我会来写深入的部分。
废话不多说,让我们进入正题。
The one thing that makes developers different than the common people out there is the command line the moment someone opens up a terminal and types out their first command. They’re instantly transformed into a jedi hacker.
[…some text is omitted here]
I realize that people will bash me for not using bash in this video but the node.js ecosystem has all kinds of awesome tools that can help you build an over-the-top command line tool that’ll make people think you’re some kind of coding wizard.
练手程序1
通过这个程序我们会掌握基本编写 cli 的工具,以及对 cli 工具做出一些美化。
Let’s first take a look at what we’re building today. We have a command line tool that animates the text who wants to be a javascript millionaire then it will prompt you to enter your name. After that it goes through a series of multiple choice questions when you enter an answer it will show a loading spinner. If you get it right it will continue. But if you get it wrong it will exit the command line script and call you a loser. If you make it all the way to the end, you win it will show you some ascii text with a color gradient and a message.
import chalk from 'chalk';
import inquirer from 'inquirer';
import gradient from 'gradient-string';
import chalkAnimation from 'chalk-animation'
import figlet from 'figlet';
import { createSpinner } from 'nanospinner'
[…some text is omitted here]
需要导入的内容是以上这些,之后直接用 chalk 就可以给文字添加各种各样的样式啦!
console.log(chalk.bgGreen('hi mon!'));
Running node index.js or just node period and the result should be the highlighted text colored text is cool, but what’s even better is animated colored text a package that builds on top of chalk called chalk animation allows us to easily create a rainbow animation and a bunch of others as well.
import chalk from 'chalk';
const log = console.log;
// Combine styled and normal strings
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
// Compose multiple styles using the chainable API
log(chalk.blue.bgRed.bold('Hello world!'));
// Pass in multiple arguments
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
// Nest styles
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
// Nest styles of the same type even (color, underline, background)
log(chalk.green(
'I am a green line ' +
chalk.blue.underline.bold('with a blue substring') +
' that becomes green again!'
));
// ES2015 template literal
log(`
CPU: ${chalk.red('90%')}
RAM: ${chalk.green('40%')}
DISK: ${chalk.yellow('70%')}
`);
// Use RGB colors in terminal emulators that support it.
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
log(chalk.hex('#DEADED').bold('Bold gray!'));
In the code here i’m creating a global variable for the player name then an async function called welcome the welcome screen will animate the text who wants to be a javascript millionaire. Now in the terminal we can only do one thing at a time and to display an animation we’ll need a couple of seconds to show it to the user to handle that.
let playerName;
const sleep = (ms = 2000) => new Promise((r) => setTimeout(r, ms));
const welcome = async () => {
const rainbowTitle = chalkAnimation.rainbow('Who Wants To Be A JavaScript Millionaire? \n');
await sleep();
rainbowTitle.stop();
console.log(`
${chalk.bgBlue('HOW TO PLAY')}
I am a process on your computer.
If you get any question wrong I will be ${chalk.bgRed('killed')}
So get all the questions right...
`);
}
await welcome();
Now we’re going to take a look at another very popular library used in many command line tools called inquirer, its job is to collect user input and has a variety of different ways for doing so to collect the user’s name. We’ll first create an inquirer prompt which can be customized with a variety of different options. You’ll first give the data a name and then the type specifies how that information should be collected an input is like a form that the user can type into. You can also do things like validate the input or provide a default value.
async function askName()
const answers = await inquirer.prompt({
name:'player_name',
type:'input',
message: 'What is your name?',
default: () => {
return 'Player';
},
});
playerName = answers.player_name;
The next thing we’ll do is use inquirer to create multiple choice questions for the quiz this function will work in almost the exact same way. The only difference is that instead of an input we’re now using a list which has multiple choices to choose from. Now we’ll need to handle the ui differently based on whether or not the user selects the correct option.
const question1 = async () => {
const answers = await inquirer.prompt({
name: 'question_1',
type: 'list',
message: 'JavaScript was created in 10 days thenreleased on\',
choices: [
'May 23rd, 1995',
'Nov 24th, 1995',
'Dec 4th, 1995',
'Dec 17, 1996',
],
});
return handleAnswer(answers.question_1 === 'Nov 24th, 1995');
}
…Called handle answer that takes a boolean as its argument. Inside the function we’ll create a loading spinner with a package called nanospinner that will run for 2 seconds while checking the answer if the answer is correct. Then we can say spin or success and move on to the next question otherwise we’re going to say spin or error and then call process exit with an argument of 1. When a process exits it usually has a code of 0 or 1. 1 means that it exited with errors 0 means everything was fine.
const handleAnswer = async (isCorrect) {
const spinner = createSpinner('Checking answer...').start();
await sleep();
if (isCorrect) {
spinner.success({ text: `Nice work ${playerName}. That's a legit answer` });
} else {
spinner.error({ text: `Game over, you lose ${playerName}!` })
process.exit(1);
}
Let’s go ahead and run that function and you should get a multiple choice question that when answered correctly will give you a green check mark and when answered incorrectly we’ll give you a red x.
Now all the other questions are implemented in the exact same way so now let’s implement the screen for the winner. The function will clear the console’s current output and then format a message called congrats player name. Here’s a million dollars we’ll then pass the message on to figlet which is actually a javascript implementation of a popular program implemented in the c language what it does is generate ascii art from text which is really useful for making your non-programmer friends think you’re some kind of genius on top of that.
const winner = () => {
console.clear();
const msg = `Congrats , ${playerName} !\n $ 1 , 0 0 0 , 0 0 0`;
figlet(msg, (err, data) => {
console.log(gradient.pastel.multiline(data);
});
}
It look even more spectacular using the gradient string package. It’s kind of like chalk but instead of a plain color it creates a color gradient. And now we can await each step in the process for a complete command line tool it may seem trivial but knowing how to build your own cli tool can be extremely useful for productivity.
最后排列组合几个代码段,就大功告成了。当然,问题得自己设计好,不然的话可就太没挑战性了。
最后来张二次元,表示第一部分写完了!
练手程序2
我们现在已经有了一个可以跑起来的程序,但是只做这些是没有用处的。CLI 最重要的职责还是方便的帮我们完成自动化的操作。
例如,我们开发一个 web 页面,需要用到最多的工具是 Live Server。平时我们经常使用 pnpm dev
现在我们想让 pnpm dev
启动 webpack
或者 esbuild
的时候,同时启动 live-server
,就需要用到多线程的知识了。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
简而言之,一个程序至少有一个进程,一个进程至少有一个线程。
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
JavaScript 是单线程语言,代表着一个 JS 进程程序只能拥有一个线程。所以在 Node.js 使用多线程,本质上是单独开辟了一个新进程,进程下有其他线程。
const http = require('http');
http.createServer().listen(3000, () => {
process.title = 'testNodeJS';
console.log(`process.pid: `, process.pid);
});
使用 http.createServer()
可以创建一个服务器进程,这个时候就可以通过 console.log
看到这个进程出现了。
Node.js 中的进程 process 是全局对象可以直接访问,下面是一些常用的功能:
process.env
:环境变量,例如通过process.env.NODE_ENV
获取不同环境项目配置信息。process.nextTick
:这个在谈及 Event Loop 时经常为会提到,简单的来说它是用来执行异步操作的,执行顺序在promise.then之前,主任务之后。而在浏览器端,nextTick 会退化成setTimeout(callback, 0)
.process.pid
:获取当前进程 id。process.ppid
:当前进程对应的父进程。process.cwd()
:获取当前进程工作目录。process.platform
:获取当前进程运行的操作系统平台。process.uptime()
:当前进程已运行时间,例如:pm2 守护进程的 uptime 值。- 进程事件:
process.on('uncaughtException', cb)
捕获异常信息、process.on('exit', cb)
进程推出监听。 - 三个标准流:
process.stdout
标准输出、process.stdin
标准输入、process.stderr
标准错误输出。
node:child_process
模块提供了以与 popen(3) 类似但不完全相同的方式衍生子进程的能力。 此功能主要由 child_process.spawn()
函数提供:
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
默认情况下,会在父 Node.js 进程和衍生的子进程之间建立 stdin
、stdout
和 stderr
的管道。 这些管道的容量有限(且特定于平台)。 如果子进程在没有捕获输出的情况下写入标准输出超过该限制,则子进程会阻塞等待管道缓冲区接受更多数据。 这与 shell 中管道的行为相同。 如果不消费输出,则使用 { stdio: 'ignore' }
选项。
为方便起见,node:child_process
模块提供了一些同步和异步方法替代 child_process.spawn()
和 child_process.spawnSync()
。 这些替代方法中的每一个都是基于 child_process.spawn()
或 child_process.spawnSync()
实现。
child_process.exec()
: 衍生 shell 并在该 shell 中运行命令,完成后将 stdout 和 stderr 传给回调函数。child_process.execFile()
: 与child_process.exec()
类似,不同之处在于,默认情况下,它直接衍生命令,而不先衍生 shell。child_process.fork()
: 衍生新的 Node.js 进程并使用建立的 IPC 通信通道(其允许在父子进程之间发送消息)调用指定的模块。child_process.execSync()
:child_process.exec()
的同步版本,其将阻塞 Node.js 事件循环。child_process.execFileSync()
:child_process.execFile()
的同步版本,其将阻塞 Node.js 事件循环。
所以我们重写 pnpm dev
的时候,完全可以通过直接开一个子进程的方式运行 http-server
或者 live-server
.
以下是 fork() 和 spawn() 之间的区别:
Spawn | Fork |
---|---|
This starts sending data back to a parent process from the child process as soon as the child process starts executing. | This does not send data automatically, but we can use a global module name process to send data from the child process and in the parent module, using the name of the child the process to send to the child process. |
It creates a new process through command rather than running on the same node process. | It makes several individual processes (child processes) but all of them run on the same node process as the parent. |
In this, no new V8 instance is created. | In this, a new V8 instance is created. |
It is used when we want the child process to return a large amount of data back to the parent process. | It is used to separate computation-intensive tasks from the main event loop. |
以下是建立一个子进程的方式,通过 spawn 方法来实现。并且用过 pipe 方法让子进程的 stdout 和主进程的 stdout 相连接。
const { spawn } = require('child_process');
// 创建一个子进程,展示当前目录所有文件,cwd 指定子进程的工作目录,默认当前目录
const child = spawn('ls', ['-l'], { cwd: '/xxx' });
// 让子进程的 stdio 和当前进程的 stdio 之间建立管道链接
child.stdout.pipe(process.stdout);
console.log(process.pid, child.pid);
让我们继续,看一个例子。这个例子中父子进程之间相互通信。
// 父进程代码
const { fork } = require('child_process');
// 创建子进程
const child = fork('child.js');
// 监听子进程发送的消息
child.on('message', message => {
console.log(process.pid, child.pid);
console.log('父进程接收到消息:', message);
});
// 向子进程发送消息
child.send('Hello from parent!');
// 子进程代码
process.on('message', message => {
// 这里的 process 等同于上述的 child.pid
console.log(process.pid);
console.log('子进程接收到消息:', message);
// 向父进程发送消息
process.send('Hello from child!');
});
需要注意的是,父进程和子进程之间的通信是异步的。父进程发送消息后,不会阻塞等待子进程的响应,而是继续执行后续的代码。类似地,子进程发送消息后,也不会阻塞等待父进程的响应。
通过这个示例,我们可以做到父子进程通信,这意味着我们的程序可以进一步完善了:当 webpack
或者 esbuild
主进程完成打包的时候,这个时候如果没有出错,我们完全可以执行 console.clear()
方法降低无效信息的密度,进而降低心智负担。
练手程序3
我们或许可以通过很多种方法来实现网络编程,但是应用层之上,存在不止一种协议。但是平时我们基本上只写 http 相关协议,对于其它的协议我们知之甚少。
HTTP(超文本传输协议):
Node.js 提供了 http 模块用于创建 HTTP 服务器和客户端。可以使用 http.createServer
方法创建一个 HTTP 服务器,监听指定的端口,并对请求进行处理。同时,也可以使用 http.request 方法创建一个 HTTP 客户端,向其他服务器发送 HTTP 请求并获取响应。
通过 http.createServer
创建的服务器可以监听客户端的请求,并根据请求的路径和方法(GET、POST 等)做出相应的处理。我们可以编写回调函数来处理请求并发送响应,或者使用其他框架(如 Express.js)来简化处理过程。
以下是一个 http 服务端和客户端示例:
服务端
const http = require('http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!');
});
server.listen(3000, 'localhost', () => {
console.log('HTTP server listening on port 3000');
});
客户端
const http = require('http');
const options = {
hostname: 'www.example.com',
port: 80,
path: '/',
method: 'GET',
};
const req = http.request(options, (res) => {
console.log(`Status code: ${res.statusCode}`);
res.on('data', (chunk) => {
console.log(chunk.toString());
});
});
req.on('error', (error) => {
console.error(error);
});
req.end();
HTTPS(安全的超文本传输协议):
Node.js 提供了 https 模块用于创建 HTTPS 服务器和客户端。与 HTTP 类似,我们可以使用 https.createServer
方法创建一个 HTTPS 服务器,并使用 https.request
方法创建一个 HTTPS 客户端。
HTTPS 在传输过程中使用 SSL/TLS 加密,提供了更高的安全性。为了使用 HTTPS,你需要为服务器配置 SSL 证书。可以使用自签名证书进行开发和测试,但在生产环境中,建议使用可信任的证书机构签发的证书。
服务端
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('private.key'),
cert: fs.readFileSync('certificate.crt'),
};
const server = https.createServer(options, (req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!');
});
server.listen(443, 'localhost', () => {
console.log('HTTPS server listening on port 443');
});
客户端
const https = require('https');
const options = {
hostname: 'www.example.com',
port: 443,
path: '/',
method: 'GET',
};
const req = https.request(options, (res) => {
console.log(`Status code: ${res.statusCode}`);
res.on('data', (chunk) => {
console.log(chunk.toString());
});
});
req.on('error', (error) => {
console.error(error);
});
req.end();
FTP(文件传输协议):
在 Node.js 中,可以使用第三方模块(如 ftp、ftps 等)来实现 FTP 协议。这些模块提供了用于创建 FTP 客户端的 API,使你能够连接到 FTP 服务器并执行文件的上传、下载、删除等操作。你可以使用这些模块来与远程 FTP 服务器进行文件传输。
使用 Node.js 进行 FTP 文件上传操作:
const ftp = require('ftp');
const fs = require('fs');
const client = new ftp();
client.on('ready', () => {
console.log('Connected to FTP server');
// 上传文件
const fileStream = fs.createReadStream('local-file.txt');
client.put(fileStream, 'remote-file.txt', (err) => {
if (err) {
console.error('Error uploading file:', err);
} else {
console.log('File uploaded successfully');
}
// 断开 FTP 连接
client.end();
});
});
client.on('error', (err) => {
console.error('FTP error:', err);
});
// 连接到 FTP 服务器
client.connect({
host: 'ftp.example.com',
user: 'username',
password: 'password'
});
下载文件:
const ftp = require('ftp');
const fs = require('fs');
const client = new ftp();
client.on('ready', () => {
console.log('Connected to FTP server');
// 下载文件
client.get('remote-file.txt', (err, stream) => {
if (err) {
console.error('Error downloading file:', err);
} else {
const localFileStream = fs.createWriteStream('local-file.txt');
stream.pipe(localFileStream);
localFileStream.on('close', () => {
console.log('File downloaded successfully');
client.end();
});
}
});
});
client.on('error', (err) => {
console.error('FTP error:', err);
});
// 连接到 FTP 服务器
client.connect({
host: 'ftp.example.com',
user: 'username',
password: 'password'
});
TCP(传输控制协议):
Node.js 的核心模块 net 提供了创建 TCP 服务器和客户端的 API。你可以使用 net.createServer
方法创建一个 TCP 服务器,并使用 net.createConnection
方法创建一个 TCP 客户端。
TCP 提供面向连接的可靠通信,适用于需要数据完整性和可靠性的场景。使用 TCP,你可以建立一个持久的连接,进行双向的数据传输。
实际上,http 基于 TCP 实现,所以 TCP 完全就是 http 的实现。
UDP(用户数据报协议):
Node.js 的核心模块 dgram
提供了创建 UDP 服务器和客户端的 API。可以使用 dgram.createSocket
方法创建一个 UDP 服务器或客户端。
UDP 是面向无连接的协议,不提供可靠性和顺序保证。它更适合于实时性要求较高、对数据可靠性要求不高的场景,如音频、视频流等。
服务器:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('listening', () => {
const address = server.address();
console.log(`UDP server listening on ${address.address}:${address.port}`);
});
server.on('message', (msg, rinfo) => {
console.log(`Received message: ${msg.toString()} from ${rinfo.address}:${rinfo.port}`);
});
server.bind(3000, 'localhost');
在这两段代码中,
UDP 服务器通过 server.on(‘message’, …) 事件监听器来接收客户端发送的消息,并在控制台上打印收到的消息以及发送方的地址和端口。
UDP 客户端通过 client.send(…) 方法发送消息到指定的服务器地址和端口,然后通过回调函数来处理发送的结果。
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const message = Buffer.from('Hello, UDP server!');
client.send(message, 3000, 'localhost', (err) => {
if (err) {
console.error('Error sending message:', err);
} else {
console.log('Message sent to UDP server');
}
client.close();
});
UDP 需要考虑的问题还有很多,比如处理修饰的数据包、乱序数据包和其他网络问题需要一些额外的逻辑来处理。我们编写一个单独的函数来处理这些问题。
// 处理修饰的数据包和乱序数据包
function processPacket(packet) {
// 在这里添加你的处理逻辑
// 例如,假设数据包有一个序列号字段 seqNumber
const expectedSeqNumber = 0; // 期望的下一个序列号
const seqNumber = packet.seqNumber;
if (seqNumber === expectedSeqNumber) {
// 处理正常顺序的数据包
console.log('Received packet in correct order:', packet);
// 更新下一个期望的序列号
expectedSeqNumber++;
// 处理数据包的其他操作...
} else if (seqNumber < expectedSeqNumber) {
// 处理修饰的数据包(重复收到的数据包)
console.log('Received duplicate packet:', packet);
// 忽略重复的数据包或进行其他处理...
} else {
// 处理乱序数据包
console.log('Received out-of-order packet:', packet);
// 缓存乱序数据包,等待后续的数据包到达...
}
// 其他网络问题的处理逻辑...
}
// 示例数据包
const packet1 = { seqNumber: 0, payload: 'Packet 0' };
const packet2 = { seqNumber: 1, payload: 'Packet 1' };
const packet3 = { seqNumber: 2, payload: 'Packet 2' };
// 模拟接收数据包的顺序
processPacket(packet1);
processPacket(packet3);
processPacket(packet2);
processPacket
函数用于处理接收到的数据包。它通过比较数据包的序列号与期望的序列号来判断数据包的状态。如果序列号与期望的序列号相等,则处理正常顺序的数据包;如果序列号小于期望的序列号,则处理修饰的数据包(重复数据包);如果序列号大于期望的序列号,则处理乱序数据包。
WebSocket
客户端频繁发起请求。WebSocket 协议建立在 HTTP 协议之上,使用类似握手的过程进行连接建立,并使用自定义的帧格式来传输数据。
下面是 WebSocket 的一些关键特点和工作原理的详细介绍:
持久连接: 与传统的 HTTP 请求-响应模式不同,WebSocket 连接在客户端和服务器之间建立一次后,保持持久化连接,允许双向通信。这消除了每次通信都需要重新建立连接的开销。
全双工通信: WebSocket 允许客户端和服务器同时进行双向通信,可以在任一方向上发送和接收数据。这使得服务器能够实时地向客户端推送数据,而不需要客户端明确地发出请求。
低延迟: WebSocket 的持久连接和全双工通信特性使得数据传输更加实时和即时响应。相比于传统的轮询或长轮询方式,WebSocket 可以更快地传递数据,并降低延迟。
跨域支持: WebSocket 支持跨域通信,使得客户端可以与不同域名的服务器进行通信,而不受同源策略的限制。
安全性: WebSocket 可以通过加密协议(如 TLS/SSL)进行安全连接,确保数据的机密性和完整性。
我们用一段代码写清楚 WebSocket 是如何运行的:
const socket = new WebSocket('ws://example.com/socket');
socket.onopen = () => {
console.log('WebSocket connection established');
// 在连接建立后,可以发送消息到服务器
socket.send('Hello, server!');
};
socket.onmessage = (event) => {
const message = event.data;
console.log('Received message from server:', message);
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
客户端使用 new WebSocket(url) 创建一个 WebSocket 连接,并通过事件处理程序处理连接建立、接收消息、连接关闭和错误等事件。通过 socket.send(message) 可以向服务器发送消息。
const WebSocket = require('ws');
// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ port: 8080 });
// 监听连接事件
wss.on('connection', (ws) => {
console.log('WebSocket connection established.');
// 接收客户端发送的消息
ws.on('message', (message) => {
console.log('Received message from client:', message);
// 向客户端发送消息
ws.send('Hello, client!');
});
// 监听连接关闭事件
ws.on('close', () => {
console.log('WebSocket connection closed.');
});
});
类似的,服务端使用 on
send
方法保持监听和向客户端传输数据。
那么,练手程序3——完成一个每分钟自动获取当前港币兑换人民币的程序,是不是就变得容易了呢?我们这里使用常用的 axios
实现。
const axios = require('axios');
// 定义函数获取汇率
async function getExchangeRate() {
try {
// 向外部 API 发送请求获取汇率数据
const response = await axios.get('https://api.exchangerate-api.com/v4/latest/HKD');
// 解析响应数据
const { rates } = response.data;
const cnyRate = rates.CNY;
// 输出汇率信息
console.log(`当前港币兑换人民币汇率为: 1 HKD = ${cnyRate} CNY`);
} catch (error) {
console.error('获取汇率失败:', error.message);
}
}
// 每分钟获取汇率
setInterval(getExchangeRate, 60000);
如何,你学会了吗?是不是很容易呢?