- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
本篇技术指南以 learnyounode 开源训练营的第四个练习 "MY FIRST ASYNC I/O!"(仓库路径exercises/my_first_async_io/)为核心,讲解如何用 Node.js 风格的异步文件系统操作读取文件,并在 stdout 输出其换行数,行为等价于cat file | wc -l。读完本文,你将掌握fs.readFile()回调式 API 的用法、Node.js 惯用回调签名function callback (err, data)的含义、Buffer与String两种读取结果的选择,以及 learnyounode 是如何在自动化校验中区分同步/异步实现的——并最终通过learnyounode verify的检验。
练习背景:从同步到异步的 "The Node.js way"
本练习位于 learnyounode 13 个自导式工作坊练习序列中的第 4 个(注册顺序见 learnyounode.js),其直接前身是第 3 个练习 "MY FIRST I/O!"(exercises/my_first_io/problem.md)。两者的题目几乎相同——读取命令行参数指定的文件,打印文件中的换行数;区别只有一个:前一个练习要求使用同步(blocking)的fs.readFileSync(),而本练习必须采用异步的fs.readFile()。
原题目标(来自exercises/my_first_async_io/problem.md)如下:
- 创建文件
my-first-async-io.js; - 用单个异步文件系统操作读取文件,并将文件包含的换行数打印到控制台(stdout);
- 待读取文件的完整路径由第一个命令行参数提供。
之所以说解法"几乎与前一个问题相同",是因为行数统计的核心算法完全一致,唯一的变化是把"用返回值拿数据"改为"从回调参数里拿数据"。这正是学习本练习的核心价值:理解 Node.js 事件循环下"不阻塞、靠回调返回结果"的编程范式。
核心知识点一:fs.readFile()与回调式 API
在同步解法中,fs.readFileSync()会返回一个包含文件全部内容的Buffer对象,代码可以立即使用返回值。而异步的fs.readFile()不会返回文件内容,数据只能从你传入的回调函数中获取:
// 同步:直接使用返回值 const contents = fs.readFileSync('/path/to/file') // 异步:从回调参数中收集数据 fs.readFile('/path/to/file', function (err, data) { // 在这里使用 data })Node.js 惯用的回调签名是:
function callback (err, data) { /* ... */ }因此你可以通过检查第一个参数err是否为真值(truthy)来判断是否出错;若没有错误,第二个参数就是文件内容。
两种取值形态:Buffer还是String
fs.readFile()支持两种调用形式,这是原文档重点强调的细节:
| 调用形式 | 回调第二参数类型 | 说明 |
|---|---|---|
fs.readFile(file, callback) | Buffer | 默认返回原始字节缓冲,需要.toString()转换 |
fs.readFile(file, 'utf8', callback) | String | 指定字符编码后直接得到字符串 |
本练习的官方参考解法(solution.js)展示了两种写法:
'use strict' const fs = require('fs') const file = process.argv[2] fs.readFile(file, function (err, contents) { if (err) { return console.log(err) } // fs.readFile(file, 'utf8', callback) can also be used const lines = contents.toString().split('\n').length - 1 console.log(lines) })要点拆解:
process.argv[2]是第一个命令行参数,即文件完整路径(process.argv[0]、process.argv[1]分别是 node 与脚本路径);- 代码先做
err检查,再通过contents.toString()把Buffer转为字符串; - 若要省去
.toString(),可以直接把'utf8'作为第二个参数、回调作为第三个参数,此时第二参数就是String。
核心知识点二:如何准确统计换行数
统计换行数的技巧与前一个练习完全一致:把字符串用'\n'作分隔符.split()成数组,数组长度减 1就是换行数:
const lines = contents.toString().split('\n').length - 1 console.log(lines)为什么是"长度减 1"?因为原文档明确说明:测试文件最后一行末尾没有换行符,split('\n')之后得到的数组元素个数会比换行数多 1。例如内容a\nb\nc会切分成['a', 'b', 'c'],长度 3,而换行数是 2。这一点在exercises/my_first_io/problem.md中也有同样的说明,属于两个练习共用的计数约定。
核心知识点三:用learnyounode verify自动校验
编写完程序后,运行以下命令校验正确性({appname}即 learnyounode 命令):
$ learnyounode verify my-first-async-io.js也可以先运行learnyounode run my-first-async-io.js观察程序的输出效果,再进入验证流程。
校验器如何判定"异步实现"
本练习的校验器(exercise.js)不只是比对输出结果,它还会静态检测你的代码到底用了同步还是异步 API。其工作流程如下:
- 准备随机测试文件:
addSetup阶段用boganipsum生成 1 到 50 段随机文本(Math.ceil(Math.random() * 50)),写入系统临时目录下名为_learnyounode_<pid>.txt的文件,并把该文件路径通过submissionArgs.unshift(testFile)注入为你的程序的第一个命令行参数。 - 运行时埋点:校验器把 ../my_first_io/wrap.js 作为包装模块注入子进程,对所有
fs模块方法(包括fs.promises与util)做包装:当你的主程序调用某个 fs 方法时,通过调用栈(ctx.$captureStack)判断调用方是否是你的主程序文件,并记录在ctx.fsCalls中。 - 验证阶段判定:
addVerifyProcessor遍历exercise.wrapData.fsCalls中记录到的所有 fs 方法调用——凡是以Sync结尾的方法(如readFileSync)即触发fail.sync失败提示;反之记为异步调用,累计pass.async通过提示。最终只有当存在异步调用且不存在任何同步调用(usedAsync && !usedSync)时,练习才算通过。 - 清理:无论 run 还是 verify,结束后都会用
rimraf删除临时测试文件。
这套机制说明:本练习的验收标准是"必须走异步路径",即使输出结果正确,只要使用了fs.readFileSync()也会被判失败。
测试用例佐证
仓库test/my_first_async_io/目录下的用例与上述逻辑一一对应:
- valid_01.js:使用
fs.readFile(process.argv[2], countNewLines)的异步实现,回调内先检查error,再统计行数并输出,是通过样例; - invalid_01.js:使用
fs.readFileSync()同步读取的"反例",结果正确也会因同步 API 被判无效; - invalid_02.js:虽然用了异步
fs.readFile(),但回调里固定输出-1,结果错误的失败样例。
完整可运行实现与常见错误
推荐解法(直接给回调函数)
const fs = require('fs') const file = process.argv[2] fs.readFile(file, 'utf8', function (err, contents) { if (err) { return console.log(err) } const lines = contents.split('\n').length - 1 console.log(lines) })命名函数写法(更易读)
const fs = require('fs') fs.readFile(process.argv[2], countNewLines) function countNewLines (error, text) { if (error) { return console.log(error) } const lineCount = text.toString().split('\n').length - 1 console.log(lineCount) }(与测试用例 valid_01.js 同构)
必须避开的三个坑
- 把回调写成同步返回:
fs.readFile()没有返回值可用,内容只存在于回调参数中; - 漏掉
err检查:惯用回调约定第一参数是错误对象,若文件不存在(例如路径拼错)而你不检查err,后续处理contents会崩溃; - 误用同步 API:即使
fs.readFileSync()输出的行数完全正确,也会被本练习的校验器判为失败(见上文invalid_01.js)。
延伸阅读
fs模块的完整 API 文档(readFile、readFileSync、回调约定、编码参数)可在仓库内置文档 docs-nodejs/fs.html 中查阅;Buffer对象的转换与操作说明见 docs-nodejs/buffer.html;- 本练习的前身(同步版)题目见 exercises/my_first_io/problem.md,其中对
Buffer、toString()与换行计数原理有更详尽的铺垫; - 想继续学习,下一练习 "FILTERED LS"(
exercises/filtered_ls/)会在此基础上引入fs.readdir与路径过滤,进一步加深异步编程实践。
- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
相关推荐
Serial Studio 连接诊断深入解析:Spec 0035 的即时 + 探测双轨检查体系、API 契约与源码实现
Serial Studio 连接诊断深入解析:Spec 0035 的即时 + 探测双轨检查体系、API 契约与源码实现 本篇基于 Serial Studio 的
教程CLIlearnyounode 实战:用 `fs.readFile` 异步回调统计文件行数(MY FIRST ASYNC I/O 习题全解)
learnyounode 实战:用 fs.readFile 异步回调统计文件行数(MY FIRST ASYNC I/O 习题全解) 本指南以 learnyoun
教程CLIdcg allowlist 信任层级详解:为什么项目白名单默认不生效?AI 命令守卫 3 层优先级全解析
dcg allowlist 信任层级详解:为什么项目白名单默认不生效?AI 命令守卫 3 层优先级全解析 dcg(Destructive Command Gua
教程CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考