- 教程
- 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(过滤文件列表)练习展开,讲解如何编写一个接收「目录路径」与「文件扩展名」两个命令行参数、并以异步 I/O 方式输出过滤后文件列表的 Node.js 程序。读完本文你将掌握fs.readdir()回调式异步 API 的正确用法、path.extname()扩展名解析技巧,以及 learnyounode 如何通过包装 fs 模块自动校验「是否使用异步 API」的底层机制,为后续make_it_modular(模块化重构)等练习打下基础。
练习目标:按扩展名过滤目录文件
filtered_ls是 learnyounode 的第三个练习(紧随my_first_io与my_first_async_io之后),其核心任务来自 exercises/filtered_ls/problem.fr.md:编写一个程序,打印出指定目录下的文件列表,并依据文件的扩展名进行过滤。
程序运行时需要接收两个命令行参数:
- 第一个参数:目录路径,例如
/chemin/du/dossier/(即/path/to/dir/); - 第二个参数:用于过滤的扩展名,例如
txt。
过滤规则十分明确:若第二个参数是txt,则只输出文件名以.txt结尾的文件。这里有一个容易被忽视的细节——第二个参数不会自带开头的.,因此你必须在程序中自行拼接(例如'.' + process.argv[3]),这一点在英文原文problem.md中也有同样的强调。
同时还有两条硬性约束:
- 输出格式:过滤后的文件名必须在控制台逐行打印,每个文件占一行;
- 必须使用异步 I/O:不允许使用
fs.readdirSync之类的同步方法,这是本练习在验证环节会强制检查的关键点。
注意,process.argv数组的前两个元素被 Node 保留用于系统信息(Node 可执行文件路径与脚本路径),所以程序的第一个参数是process.argv[2],第二个参数是process.argv[3]——这是本练习隐含的重要知识点。
核心 API 与提示
fs.readdir():异步读取目录
练习提示给出了本任务的核心 API——fs.readdir()。该方法接收一个路径作为第一个参数、一个回调函数作为第二个参数,回调签名如下:
function callback (err, list) { /* ... */ }其中list是一个字符串数组,元素为目录下的文件名(不含路径前缀)。err则在读取失败时携带错误对象,成功时为null。
值得指出的是:fs.readdir()并不递归遍历子目录,它只返回给定目录直接包含的文件与子目录名称;而本练习的场景恰好只需要第一层文件,因此用它做过滤十分合适。
fs模块的完整离线文档已随仓库提供,可直接在浏览器中打开阅读:docs-nodejs/fs.html。
path.extname():提取文件扩展名
练习提示还推荐使用 Node 内置的path模块,尤其是extname方法。path.extname('learnyounode.txt')会返回'.txt';对于无扩展名的文件(如Makefile)则返回空字符串''。结合「第二个参数不带点」的约束,标准写法就是:
if (path.extname(file) === '.' + process.argv[3]) { /* 命中过滤条件 */ }path模块的离线文档同样随仓库提供:docs-nodejs/path.html。
官方参考答案逐行拆解
仓库中提供了官方解法,位于 exercises/filtered_ls/solution/solution.js,全文仅 15 行:
'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) } }) })其思路可归纳为四个步骤:
- 取参并预处理扩展名:
folder取process.argv[2],ext在process.argv[3]前手动拼接.,规避「参数不带点」的坑; - 异步读取目录:调用
fs.readdir(folder, callback); - 错误先行:回调内先判
err,有错即console.error(err)并提前返回,避免在空files上继续操作; - 逐项过滤并打印:用
path.extname(file) === ext判断扩展名是否匹配,命中则console.log(file),天然实现「每行一个文件」。
官方解法选择了forEach+ 条件判断的写法;测试用例 test/filtered_ls/valid_01.js 则展示了另一种等价写法——先用Array.prototype.filter筛出匹配文件,再对结果forEach打印:
const fs = require('fs') const path = require('path') 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 中,其实现分为两层:
第一层:phantom wrapper 包装 fs 调用
exercise.js 第 19 行引入了../my_first_io/wrap作为包装模块(见 exercises/my_first_io/wrap.js)。该模块会把子进程中的fs、fs.promises(fsp)与util的所有方法逐一替换为包装函数,通过调用栈($captureStack)判断:如果该 fs 方法是在选手主程序中调用的,就将其记入ctx.fsCalls计数器。于是选手究竟用了哪些fs.*方法、用了多少次,都被完整记录在案。
第二层:verify 处理器判定同步/异步
exercise.js 第 53–72 行的addVerifyProcessor遍历exercise.wrapData.fsCalls中记录的方法名:
- 若方法名以
Sync结尾(如fs.readdirSync()),判定usedSync = true并输出失败提示fail.sync; - 否则判定
usedAsync = true并输出通过提示pass.async; - 若既没记录到同步也没记录到异步调用(选手可能根本没使用 fs 模块),则输出
fail.unused。
最终的通过条件是usedAsync && !usedSync——只用异步、零同步调用。反例测试 test/filtered_ls/invalid_01.js 正是用fs.readdirSync写的同步版本,用于验证该检查能正确拦截。
这一设计也解释了为什么filtered_ls不能直接复用my_first_async_io的验证方式:本练习要求的不只是「用了异步」,而是「绝不出现同步调用」,因此需要比普通输出比对更细粒度的调用追踪。
练习运行的输入数据
为了确保选手程序在不同环境下都能稳定验证,练习会在运行时动态构造测试目录。从 exercises/filtered_ls/exercise.js 的addSetup阶段可见其流程:
- 在系统临时目录(
os.tmpdir())下创建以_learnyounode_<进程PID>命名的测试目录; - 依据 exercises/filtered_ls/file-list.json 中的 15 个文件名(如
learnyounode.dat、learnyounode.txt、learnyounode.sql、api.html、README.md、CHANGELOG.md、LICENCE.md、data.json、data.dat、words.dat、w00t.dat、w00t.txt、wrrrrongdat、dat等)批量写入占位内容; - 通过
submissionArgs.unshift('md')与submissionArgs.unshift(testDir),将测试目录与扩展名md依次注入选手程序的process.argv——也就是说,官方验证时第二个参数是md,期望输出的是测试目录中所有以.md结尾的文件(即README.md与CHANGELOG.md); - 验证结束后调用
addCleanup用rimraf清理临时目录,避免污染系统。
这份file-list.json也直接复用于官方解法与测试用例的参数注入,保证「题目、答案、测试」三方使用同一份数据源。
运行与验证方式
完成练习后,可以用 learnyounode 自带的验证命令检查程序是否正确。英文原文 exercises/filtered_ls/problem.md 给出了标准用法:
$ learnyounode verify filtered-ls.js其中filtered-ls.js是选手保存的程序文件。verify模式会完整走一遍上文所述的「构造测试目录 → 注入参数 → 包装 fs 调用 → 执行选手程序 → 比对输出并判定异步约束」全流程;而run模式则只负责在本地执行选手程序,方便自行观察输出。
除手工验证外,仓库还通过workshopper-adventure-test驱动测试(见 package.json 的test脚本与devDependencies),其中test/filtered_ls/目录下的valid_01.js、invalid_01.js分别作为「异步通过版」与「同步失败版」的回归样例,持续保障验证器的判定逻辑正确。
与后续练习的衔接
filtered_ls过滤逻辑本身并不复杂,但它是通往下一练习make_it_modular的跳板:make_it_modular要求把「读取目录 + 按扩展名过滤」的整套逻辑抽取为独立模块,以「模块导出函数 + 回调接收结果」的方式组织代码,并配有solution_filter.js示例与 15 个模块级测试样例(见 exercises/make_it_modular)。提前吃透fs.readdir的异步回调模式与path.extname的边界行为,将大幅降低后续模块化改造时的认知负担。
小结
| 要点 | 说明 |
|---|---|
| 参数位置 | 目录路径在process.argv[2],扩展名在process.argv[3],前两个元素为 Node 保留 |
| 扩展名处理 | 第二个参数不带.,需自行拼接为'.' + arg |
| 目录读取 | 使用异步fs.readdir(path, callback),回调签名为callback(err, list) |
| 扩展名判断 | 使用path.extname(file) === ext,注意无扩展名文件返回空字符串 |
| 输出格式 | 每个匹配文件console.log一行 |
| 强制约束 | 禁止任何*Sync调用,验证器通过包装 fs 模块逐方法计数判定 |
| 验证命令 | learnyounode verify filtered-ls.js |
通过本文的源码级拆解,读者不仅能在本地写出可被learnyounode verify认可的答案,更能理解其验证器「包装 fs 调用、区分同步/异步」的设计思想——这正是 learnyounode 这类自引导工作坊(self-guided workshops)区别于普通在线题库的价值所在。
- 教程
- 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 实现按扩展名过滤目录文件
learnyounode 实战:使用 fs.readdir 与 path.extname 实现按扩展名过滤目录文件 本篇技术指南以 learnyounode(N
教程CLIlearnyounode 第二关:使用 fs.readdir 与 path.extname 实现按扩展名过滤文件列表
learnyounode 第二关:使用 fs.readdir 与 path.extname 实现按扩展名过滤文件列表 导读 本篇文章以 learnyounode
教程CLIConform错误处理最佳实践:构建健壮的表单用户体验
Conform错误处理最佳实践:构建健壮的表单用户体验 在现代Web应用开发中,表单是用户与系统交互的核心界面,而错误处理则直接影响用户体验的流畅度和满意度。C
教程CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考