- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
这篇技术指南以 learnyounode 课程中的 filtered_ls(过滤目录列表)练习为核心,完整讲解如何编写一个读取指定目录、按文件扩展名过滤、并以一行一个文件的方式输出结果的 Node.js 命令行程序。读完本文,你将掌握fs.readdir()异步回调的用法、process.argv的参数解析规则、path.extname()的扩展名提取技巧,并了解该练习背后的自动化验证机制(如何强制要求使用异步 I/O)。所有结论均有本仓库的官方题目、参考答案与测试用例作为依据。
练习目标:从题目要求说起
filtered_ls 是 learnyounode 入门序列中的第三道练习,承接 baby_steps(命令行参数)与 my_first_async_io(异步文件读取)。本练习的完整题目原文见 exercises/filtered_ls/problem.md,核心要求可拆解为四点:
- 新建文件:程序必须保存在名为
filtered-ls.js的文件中。 - 两个命令行参数:第一个参数是目标目录路径(如
/path/to/dir/),第二个参数是用于过滤的文件扩展名(如txt)。 - 过滤规则:只输出文件名以
.扩展名结尾的文件。特别注意——第二个参数传入时不带前导点号(txt而非.txt),程序内部需要自行拼接点号。 - 输出与 I/O 要求:每个匹配文件占一行打印到控制台,并且必须使用异步 I/O(禁止
fs.readdirSync之类的同步方法)。
题目中还特别提醒了一个新手极易踩坑的点:程序接收的参数不是process.argv数组的开头几个元素,因为 Node 会把这个数组的前两个位置预留给系统信息(第一个是 Node 可执行文件路径,第二个是脚本文件路径)。真正的用户参数从process.argv[2]开始。
从题目信息(problem.md 的 HINTS 小节)可知,这道练习希望用到 Node 内置模块fs(负责读取目录)和path(负责提取扩展名),对应仓库内还内置了官方文档镜像:/docs-nodejs/fs.html 与 docs-nodejs/path.html,可随时查阅。
核心 API 讲解:fs.readdir()与path.extname()
fs.readdir(pathname, callback):异步读取目录
fs.readdir()是 fs 模块中用于列出目录内容的异步方法,其第一个参数是目录路径,第二个参数是回调函数,回调签名固定为:
function callback (err, list) { /* ... */ }其中list是一个由文件名组成的字符串数组,且数组内的名字只包含文件名本身(不含完整路径),因此后续无法直接对它做路径拼接,需要配合path模块处理。如果读取失败,err会携带错误信息;成功时err为null。
fs.readdir()的完整 API 文档(包括fs.readdirSync、fs.promises.readdir等变体)收录在本仓库的 docs-nodejs/fs.html 中,可以作为离线查阅的权威参考资料。
path.extname(file):提取文件扩展名
path.extname()接收一个文件名字符串,返回其扩展名部分,返回结果自带前导点号。例如:
const path = require('path') path.extname('learnyounode.txt') // => '.txt' path.extname('README.md') // => '.md' path.extname('LICENCE.md') // => '.md' path.extname('learnyounode') // => '' (无扩展名返回空字符串)这正好与题目“第二个参数不带点号”的规则形成互补:程序中把用户传入的txt拼接成.txt,再与path.extname()的结果做全等比较(===),即可准确过滤。path模块的完整文档同样可在仓库中查阅:docs-nodejs/path.html。
参考答案:一份可直接运行的最小实现
仓库在 exercises/filtered_ls/solution/solution.js 中给出了官方参考实现,代码非常精简,完整如下:
'use strict' const fs = require('fs') const path = require('path') const folder = process.argv[2] const ext = '.' + process.argv[3] fs.readdir(folder, function (err, files) { if (err) return console.error(err) files.forEach(function (file) { if (path.extname(file) === ext) { console.log(file) } }) })这段代码的运行逻辑可以拆解为四个步骤,每一步都对应上文的核心 API:
- 取参:
process.argv[2]为目录路径,process.argv[3]为扩展名,并提前拼接点号得到ext = '.' + process.argv[3]。 - 异步读取:调用
fs.readdir(folder, callback),将目录中所有文件名一次性放入回调的list(此处命名files)参数。 - 错误处理:回调内先判断
err,若出错立即console.error(err)并return,避免在空数组上继续操作。 - 过滤输出:用
Array.prototype.forEach遍历files,凡path.extname(file) === ext的文件逐一console.log(file)——每行一个文件,正好满足“一行一个”的输出要求。
与官方解在写法上略有差异但同样正确的风格也很常见:先filter()再forEach(),例如测试用例 test/filtered_ls/valid_01.js 正是这种写法:
fs.readdir(process.argv[2], function (err, list) { if (err) { console.log(err) } list.filter(function (file) { return path.extname(file) === '.' + process.argv[3] }).forEach(function (file) { console.log(file) }) })两种实现殊途同归,读者可按个人偏好选择。
验证机制解剖:自动化检查如何强制异步 I/O
学习本练习,理解其验证逻辑比通过练习本身更有价值。该练习的验证器在 exercises/filtered_ls/exercise.js 中,其验证策略分为三个阶段:
1. 构造真实的测试目录
addSetup阶段会在系统临时目录(os.tmpdir())下创建一个以进程 PID 命名的目录(_learnyounode_<pid>),然后依据 exercises/filtered_ls/file-list.json 中的文件清单逐个写入内容为nothing to see here的测试文件。该清单精心构造了多种边界情况,一共 15 个文件,其中包括:
[ "learnyounode.dat", "learnyounode.txt", "learnyounode.sql", "api.html", "README.md", "CHANGELOG.md", "LICENCE.md", "md", "data.json", "data.dat", "data.dat", "words.dat", "w00t.dat", "w00t.txt", "wrrrrongdat", "dat" ]从源码结构看,这个清单设计的意图非常明确:既有.txt、.md、.dat等多种常规扩展名,也有md、dat、wrrrrongdat这类不带点号、恰好与扩展名同名的干扰项——如果程序只用endsWith()而不用path.extname(),就很容易被这些干扰项“误伤”。这从侧面印证了path.extname()才是正确的过滤方式。验证阶段传入的参数固定为md(即过滤出所有.md文件),期望输出仅为api.html、README.md、CHANGELOG.md、LICENCE.md之外……(实际上过滤md扩展名时,README.md、CHANGELOG.md、LICENCE.md命中)。
2. 包装 fs 模块以记录调用
验证器通过workshopper-wrappedexec包装子进程,并加载 exercises/my_first_io/wrap.js 作为运行时探针:该模块会遍历fs对象的所有方法,逐一替换为记录调用的代理函数,通过调用栈判断这些 fs 调用是否来自用户提交的主程序,并记入ctx.fsCalls。这样一来,无论用户程序内部调用的是同步还是异步方法,都会被如实登记。
3. 判定规则
addVerifyProcessor阶段遍历exercise.wrapData.fsCalls记录的所有被调用过的 fs 方法名:
- 凡是方法名以
Sync结尾(如fs.readdirSync),判定为违规使用同步 I/O,输出失败信息; - 其余方法视为异步调用,输出通过信息;
- 如果用户程序压根没调用任何 fs 方法(既无同步也无异步),同样判定失败,这是为了防止用
child_process.exec('ls ...')之类的手段绕过练习核心目的。
最终callback(null, usedAsync && !usedSync),即只有至少使用了一次异步 fs 调用、且完全没碰同步调用时才通过验证。对应地,测试目录中的反例也分两类:/test/filtered_ls/invalid_01.js 使用fs.readdirSync(同步 I/O),/test/filtered_ls/invalid_02.js 使用child_process.exec('ls -1 ...')(绕过 fs),两者都会被验证器拒收。
运行与提交验证
编写好filtered-ls.js后,可以在本仓库根目录下先手动运行一遍,确认输出符合预期:
# 手动运行:第一个参数为目录,第二个参数为扩展名 node exercises/filtered_ls/solution/solution.js /path/to/dir txt # 使用 learnyounode 内置的本地文档进行离线自学 # fs 模块文档:docs-nodejs/fs.html # path 模块文档:docs-nodejs/path.html然后通过 learnyounode 的验证命令检查程序是否正确(对应 problem.md 末尾的说明):
learnyounode verify filtered-ls.js验证通过后,本课程还会在下一题 make_it_modular(模块化改造) 中要求把同样的过滤逻辑抽成独立模块、通过回调返回结果——届时fs.readdir的异步回调模式会继续复用,配套的模块化验证器也位于 exercises/make_it_modular/verify.js。因此,扎实吃透本练习的异步思维,是后续进阶的基石。
常见错误自查清单
结合验证器规则与官方反例,提交前可对照以下清单自查:
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 参数下标 | 从process.argv[2]开始取 | 从process.argv[0]开始,误取到系统信息 |
| 扩展名处理 | 用path.extname()或自行拼接.前缀 | 直接用endsWith(ext),被md、dat等干扰项误命中 |
| I/O 方式 | fs.readdir()(异步) | 使用fs.readdirSync()或child_process.exec('ls')绕过 |
| 错误处理 | 回调内先判err | 忽略err导致后续空数组操作抛错 |
| 输出格式 | 每个文件单独一行console.log(file) | 用数组join(' ')打在同一行 |
小结
filtered_ls 这道练习虽小,却浓缩了 Node.js 入门阶段最核心的三个基本功:process.argv命令行参数解析、fs.readdir()的异步回调模式、以及path.extname()对文件名结构的标准化处理。通过本文对官方参考答案(solution.js)与验证器源码(exercise.js)的逐层剖析,你不仅知道“怎么写能通过”,更理解了“验证器为什么能拦截同步写法”——这种对运行机制本身的理解,比单纯通关更有长期价值。
- 教程
- CLI
【免费下载链接】learnyounode
Learn You The Node.js For Much Win! An intro to Node.js via a set of self-guided workshops.
相关推荐
learnyounode 实战:使用 fs.readdir 与 path.extname 实现按扩展名过滤的异步目录列表(filtered_ls 练习详解)
learnyounode 实战:使用 fs.readdir 与 path.extname 实现按扩展名过滤的异步目录列表(filtered_ls 练习详解) 本
教程CLIlearnyounode 实战:用 fs.readdir 与 path.extname 实现按扩展名过滤的目录列表(FILTERED LS 详解)
learnyounode 实战:用 fs.readdir 与 path.extname 实现按扩展名过滤的目录列表(FILTERED LS 详解) 导读 本文以
教程CLIinfluence-release部署指南:在真实项目中应用影响函数分析
influence release部署指南:在真实项目中应用影响函数分析 influence release是一个基于影响函数(Influence Functi
教程CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考