不要过度神秘化 Node.js 脚本
有些人可能会误解 Node.js 脚本,认为它是用 Node.js 编写的。他们可能会觉得如果不懂 Node.js 的语法就无法编写 Node.js 脚本,感觉会写 Node.js 脚本就很神秘。实际上,Node.js 脚本只是在 Node.js 环境中运行的 JavaScript 脚本而已。
Node.js是一个基于Chrome V8引擎的JavaScript运行时环境,使得您可以在服务器端运行JavaScript代码。
在Node.js环境中,您可以编写JavaScript脚本来执行各种任务,比如文件操作、网络通信、数据处理等。这些脚本可以被称为Node.js脚本,因为它们是在Node.js环境中运行的JavaScript代码,仅此而已,没有什么神秘的。
在前端工程中,Node.js 脚本最常用于文件操作,比如读取、写入、删除、新建文件等等操作。
如何运行Node.js脚本
非常简单,只要你的电脑中有装 Node.js,随便找个地方创建一个 index.js 文件,然后在文件中写入以下代码:
console.log('我是一个Node.js脚本');
接着,打开命令行工具,并进入该文件所在的目录。最后,在命令行中输入 node ./index.js
并按下回车键。你将会在命令行工具中看到输出 我是一个Node.js脚本。
如果你想将这个 Node.js 脚本作为 npm 脚本运行,可以将其添加到 package.json 文件中的 scripts 部分。假设你的脚本文件相对于 package.json 的路径是 ./scripts/index.js,你可以添加以下内容到 package.json 的 scripts 部分:
"scripts": {
"my-script": "node ./scripts/index.js"
}
这样,你就创建了一个名为 my-script 的自定义脚本,可以通过在命令行中输入 npm run my-script 来运行你的 Node.js 脚本。
如何引入第三方Node.js包
要对系统中的文件进行操作,虽然可以直接使用 Node.js 内置的 fs 模块来实现,但是为了避免处理文件操作时出现的常见错误和边界情况,同时确保跨平台兼容性。还是选择 fs-extra 这个第三方 Node.js 包来进行文件操作,那我们该如何引入呢?
首先要看 Node.js 的版本,在 12 版本之前,只支持 require() 函数来引入。在 12 版本之后,就可以使用 ES6 的 import 语法来引入。但需要在 package.json 文件中设置 "type": "module"。如果这样设置不方便,还可以将 Node.js 脚本的后缀改为 .mjs。
我的 Node.js 版本是 16.14,所以采用 ES6 的 import 语法来引入。
首先,在组件工程的根目录下创建一个名为scripts的文件夹,并在其中创建一个名为autoExport.mjs的文件。
接着,在工程的package.json文件中的scripts部分添加以下内容:
"scripts": {
"export": "node ./scripts/autoExport.mjs"
}
在 autoExport.mjs 文件中添加如下代码,引入 fs-extra 这个第三方 Node.js 包。
import fs from 'fs-extra';
Demo
这里以一个完整的小Demo来举个例子吧。
- 在项目路径下新建
index.mjs
文件,并新建assets
文件夹,在assets文件夹中创建metaData.txt
文件,并填入一定文本; - 在终端中执行
npm init
命令,并根据需要完善配置,其中test command
配置为node index.mjs
即可; - 在
index.mjs
文件中粘贴完整代码,并在终端中执行npm run test
命令测试结果,应该能看到文件的读写均已成功。
完整代码
index.mjs
:
import fs from 'fs-extra';
function test1() {
// 读取目录中的文件列表
fs.readdir('./assets/', (err, files) => {
if (err) {
console.error('Error reading directory:', err);
return;
}
let exportStr = '';
// 遍历文件
files.forEach((file, index) => {
// console.log("🚀 ~ files.forEach ~ file:", file);
exportStr += `${index > 0 ? '\n' : ''}第 ${index} 个文件: ${file}`;
});
// 写入文件
fs.writeFile('./assets/outputData.txt', exportStr);
})
}
function test2() {
// 读取文件
fs.readFile('./assets/metaData.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
let exportStr = '';
console.log('>>> data: ', data, typeof data);
exportStr = data.toString();
// 写入文件
fs.writeFile('./assets/outputData.txt', exportStr);
})
}
function test3() {
// 读取文件
fs.readFile('./assets/metaData.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
let exportStr = '';
// 反转每行单词顺序
data.split('\n').forEach((line, index) => {
let ansLine = '';
line.split(' ').forEach((word, index) => {
ansLine = word + ' ' + ansLine;
});
exportStr += `${index > 0 ? '\n' : ''}${ansLine}`;
});
// 写入文件
fs.writeFile('./assets/outputData.txt', exportStr);
})
}
// 运行测试
// test1();
test2();
// test3();
Node.js 的事件循环
Node.js 的事件循环是其非阻塞 I/O 操作的核心,使得 Node.js 能高效处理并发操作,特别是在处理大量的网络请求或文件操作时。了解事件循环是掌握 Node.js 异步编程的关键。
Node.js 的架构
Node.js 基于 Chrome 的 V8 JavaScript 引擎,它执行 JavaScript 代码。然而,对于 I/O 操作(如读取文件、网络通信等),Node.js 使用了非阻塞的方式,这依赖于其内置的 libuv
库。
libuv
库负责 Node.js 的异步 I/O 操作,包括维护事件循环和其他底层操作(如线程池的管理、异步 TCP 和 UDP 套接字、文件系统操作等)。
事件循环的阶段
Node.js 的事件循环可以分为几个主要阶段,每个阶段都有自己的特定任务:
- timers:这一阶段执行已经设置的
setTimeout()
和setInterval()
回调。 - I/O callbacks:处理几乎所有的回调,除了关闭回调、定时器的回调以及
setImmediate()
的回调。 - idle, prepare:仅系统内部使用。
- poll:检索新的 I/O 事件; 执行与 I/O 相关的回调(几乎全部除了关闭的回调、定时器的回调以及
setImmediate()
的回调),适当的条件下节点将在这里阻塞。 - check:
setImmediate()
回调在这里执行。 - close callbacks:例如
socket.on('close', ...)
。
事件循环的详细运作
Poll 阶段
事件循环的主要阶段是 poll
阶段,它负责等待待处理的事件,并执行事件的回调。如果事件队列是空的(没有即时待处理的事件),Node.js 会根据是否有设置 setImmediate()
检查 poll
阶段应该退出还是继续等待。
Check 阶段
当 poll
阶段完成后,如果存在由 setImmediate()
设置的回调,Node.js 会转到 check
阶段并执行这些回调。
Close Callbacks 阶段
如果一个 socket 或 handle 被突然关闭(例如 socket.destroy()
),close
事件的回调将在这个阶段执行。
与其他技术的交互
除了上述核心阶段外,Node.js 还会处理操作系统的任务(如网络 I/O、文件 I/O),并且在适当的时候将这些任务的完成情况加入事件队列中。Node.js 还维护一个用于处理某些特定异步 I/O 操作的线程池。
Microtasks
在每个阶段之后,Node.js 会处理 microtasks 队列。Microtasks 主要来源于 promises,例如当 Promise
解决或拒绝时,对应的 .then()
或 .catch()
注册的回调会被放入 microtasks 队列中并在当前阶段之后立即执行。
事件循环的实际应用
了解事件循环的工作方式对于编写高效的非阻塞代码至关重要。例如,过度使用 setImmediate()
或密集的 CPU 计算可以阻塞 I/O 操作,因为它们可能会延迟 poll
阶段的处理。
总之,Node.js 的事件循环是其能够执行非阻塞 I/O 操作的基础,是理解和有效使用 Node.js 的关键。
代码示例
import fs from 'fs';
console.log('开始'); // 循环1之前的同步代码
// 循环2 阶段 1: timers
setTimeout(() => {
console.log('setTimeout 执行'); // 添加到下一事件循环的 timers 阶段
}, 0);
// 循环1 阶段 5: check
setImmediate(() => {
console.log('setImmediate 执行'); // 添加到当前事件循环的 check 阶段
});
// 循环1 首先 微任务: 处理在下一次事件循环前需要完成的事务
Promise.resolve().then(() => {
console.log('Promise 1 执行');
}).then(() => {
console.log('Promise 2 执行');
});
// 循环2 阶段 2: I/O 回调
fs.readFile('./assets/file.txt', () => {
console.log('文件读取完成'); // 添加到下一事件循环的 I/O 回调阶段
// 循环2 阶段 5: check
setImmediate(() => {
console.log('文件读取后的 setImmediate 执行'); // 添加到当前事件循环(循环2)的 check 阶段
});
// 循环3 阶段 1: timers
setTimeout(() => {
console.log('文件读取后的 setTimeout 执行'); // 添加到下一事件循环(循环3)的 timers 阶段
}, 0);
});
console.log('计划任务已设置'); // 循环1之前的同步代码
最终输出结果为:
开始
计划任务已设置
Promise 1 执行
Promise 2 执行
setImmediate 执行
setTimeout 执行
文件读取完成
文件读取后的 setImmediate 执行
文件读取后的 setTimeout 执行
process.nextTick
在 Node.js 中,process.nextTick()
是一个非常重要的函数,它允许你安排一个回调函数在当前操作完成后、任何 I/O 事件(包括定时器)触发之前执行。process.nextTick()
让你可以有效地管理异步操作的执行顺序。
执行时机
process.nextTick()
函数安排的回调被放入一个叫做 "next tick queue" 的队列中。这个队列的任务会在当前 JavaScript 执行栈清空后、事件循环继续之前被执行。这意味着,无论事件循环的当前阶段如何,process.nextTick()
安排的任务总是在 Node.js 进入下一个事件循环阶段之前得到处理。
执行顺序
- Microtasks:
process.nextTick()
创建的任务属于 microtasks,执行时机在 promise 的.then()
或.catch()
之后。 - 在任何 I/O 或定时器触发前执行: 无论事件循环当前处于哪个阶段,所有
nextTick
队列中的任务总是在 Node.js 进入下一个事件循环阶段之前完成。
代码示例
下面的示例代码演示了 process.nextTick()
的执行时机:
console.log('开始');
setTimeout(() => {
console.log('setTimeout 执行');
}, 0);
setImmediate(() => {
console.log('setImmediate 执行');
});
process.nextTick(() => {
console.log('nextTick 1 执行');
});
Promise.resolve().then(() => {
console.log('Promise 执行');
});
process.nextTick(() => {
console.log('nextTick 2 执行');
});
console.log('计划任务已设置');
预期输出及解释
开始
计划任务已设置
Promise 执行
nextTick 1 执行
nextTick 2 执行
setImmediate 执行
setTimeout 执行
通过这个例子,可以看到 process.nextTick()
的任务如何在所有异步任务之前执行,从而提供一种强有力的方式来处理需要优先于 I/O 操作和定时器执行的紧急任务。这是编写高效和响应快速的 Node.js 应用程序时非常重要的一个工具。
简要总结
process.nextTick()
的任务在Promise
微任务之后,在setImmediate
、其他定时器和I/O
回调之前执行。且它也属于微任务。(process.nextTick()
比setImmediate
更“immediate”,它在“下一”事件循环开始之前就执行,不排队)
最后
最后,除了fs-extra给大家分享一些常用的第三方 Node.js 包,大家可以去实践一下,写各种各样的用于前端工程中自动化操作的 Node.js 脚本。
- yargs: 用于解析命令行参数。它提供了简单易用的 API,可以帮助您定义和解析命令行参数,支持选项、标志和参数的定义和解析。
- chalk:用于给命令行输出添加颜色,可以让输出更具有可读性和吸引力。
- cli-table:用于在命令行中创建漂亮的表格,方便展示数据。
- ora:用于在命令行中显示加载动画,可以提示用户正在进行某些操作。
- inquirer:用于在命令行中创建交互式命令行界面,方便用户输入信息或进行选择。
- boxen:用于在命令行中创建带边框的框,可以突出显示某些信息。
- progress:用于在命令行中显示进度条,方便展示任务的进度。
- figlet:用于在命令行中生成艺术字体,可以让输出更具有视觉效果。
- execa:一个更强大的子进程管理工具,它提供了更多的选项和功能,例如并发执行、流控制、更好的错误处理等。它是一个跨平台的替代方案,可以替代 Node.js 的 child_process 模块。
- shelljs:一个 Unix shell 命令的包装器,它允许你在 Node.js 中以类似于 Shell 脚本的方式执行命令。它提供了简单的接口来执行命令、获取输出和处理错误。
Comments NOTHING