【Node.js】学习util模块:最佳实践总结

3936 字
20 分钟
【Node.js】学习util模块:最佳实践总结

作为 Node.js 开发者,你肯定有过这些“崩溃瞬间”:

  • fs.readFile的回调,嵌套了三层后彻底“迷路”——比如登录+读用户 Profile+读用户订单的逻辑,回调层层叠叠像“俄罗斯套娃”;
  • 想判断一个对象是不是Promise,用typeof得到function,用instanceof Promise又被“伪装成 Promise 的对象”骗到;
  • 调试 Koa 的ctx对象时,console.log输出的内容糊成一团,根本找不到state里的用户信息。

这些问题的解药,都在 Node.js 的util 模块里——它像一把瑞士军刀,藏着 10+个能让你“偷懒”的工具函数。今天我们就把这把刀拆开,用实际项目场景告诉你:每一个“刀片”怎么用最顺手 🔧。

一、为什么需要 util 模块?它解决了什么痛点?#

util 模块的设计哲学很简单:收集那些“重要但不该放进核心模块”的工具函数

它不是“花架子”,而是帮你解决真实开发痛点的:

1. 解决“回调地狱”的噩梦#

你肯定写过这样的代码——用户登录 → 读用户 Profile→ 读用户订单,三层回调嵌套:

// 旧代码:回调地狱
function login(username, password, callback) {
db.query(
'SELECT * FROM users WHERE username = ?',
[username],
(err, user) => {
if (err) return callback(err)
if (!user || user.password !== password)
return callback(new Error('账号或密码错误'))
// 第一层:查用户
fs.readFile(`./users/${user.id}.json`, 'utf8', (err, profile) => {
if (err) return callback(err)
// 第二层:读Profile
db.query(
'SELECT * FROM orders WHERE user_id = ?',
[user.id],
(err, orders) => {
if (err) return callback(err)
// 第三层:查订单
callback(null, { user, profile: JSON.parse(profile), orders })
}
)
})
}
)
}

这样的代码, debug 时要“逐层拆嵌套”,改需求时要“牵一发动全身”——util 的promisify就是为了解决这个问题而生的

2. 解决“类型判断不准确”的坑#

你肯定试过用typeof判断Promise

const promise = Promise.resolve()
console.log(typeof promise) // 输出"function"——这根本没用!

或者用instanceof判断“伪装成 Promise 的对象”:

const fakePromise = { then: () => {} }
console.log(fakePromise instanceof Promise) // 输出false——但它有then方法!

util 的types.isPromise能精准判断:不管对象有没有then方法,只要是真正的Promise对象,才会返回true

3. 解决“调试复杂对象”的混乱#

调试 Koa 的ctx对象时,console.log(ctx)输出的内容是这样的:

{ request: { method: 'GET', url: '/', ... }, response: { status: 200, ... }, state: { user: { id: 1, ... } }, ... }

密密麻麻的属性堆在一起,根本找不到state.user——util 的inspect能帮你“展开”复杂对象,还能隐藏敏感信息

二、核心 API 详解:每一个函数都是“偷懒小技巧”#

util 模块的函数可以分成三类,我们从最常用的异步转换开始讲——这部分能帮你告别“回调地狱”。

1. 异步模式转换:从“回调地狱”到“Promise 天堂”#

(1)promisify:把回调函数“升级”成 Promise#

作用:将“错误优先回调”风格的函数(比如fs.readFiledb.query)转换成 Promise 风格,彻底解决回调嵌套问题。

核心逻辑:用 Promise 包裹回调函数,resolve成功结果,reject错误信息。

实战场景 1:改造“登录+读 Profile+读订单”的逻辑
promisify转换db.queryfs.readFile

const { promisify } = require('node:util')
const db = require('./db') // 假设db.query是回调风格
const fs = require('node:fs')
// 转换回调函数为Promise风格(注意绑定this!)
const queryAsync = promisify(db.query).bind(db)
const readFileAsync = promisify(fs.readFile)
// 改造后的Async函数:无嵌套!
async function login(username, password) {
// 1. 查用户
const [user] = await queryAsync('SELECT * FROM users WHERE username = ?', [
username,
])
if (!user || user.password !== password) throw new Error('账号或密码错误')
// 2. 读Profile
const profile = await readFileAsync(`./users/${user.id}.json`, 'utf8')
// 3. 查订单
const orders = await queryAsync('SELECT * FROM orders WHERE user_id = ?', [
user.id,
])
return { user, profile: JSON.parse(profile), orders }
}
// 调用:用Async/Await
login('admin', '123')
.then((res) => console.log(res))
.catch((err) => console.error(err))

效果:代码从“嵌套 3 层”变成“线性执行”, debug 时一眼就能看到逻辑流程。

实战场景 2:处理 Redis 的回调风格
node-redis的回调风格改成 Promise:

const redis = require('redis')
const { promisify } = require('node:util')
const client = redis.createClient({ url: 'redis://localhost:6379' })
client.connect()
// 转换Redis的get方法为Promise(注意绑定client!)
const getAsync = promisify(client.get).bind(client)
// 用Async/Await读取Redis中的用户信息
async function getUserFromRedis(userId) {
const result = await getAsync(`user:${userId}`)
return result ? JSON.parse(result) : null
}
// 调用
getUserFromRedis(1).then((user) => console.log('Redis中的用户:', user))

注意:如果回调函数是对象的方法(比如client.get),必须用bind绑定对象的this——否则this会指向undefined,导致调用失败。

(2)callbackify:把 Async 函数“掰回”回调风格#

作用:和promisify相反,将 Async 函数转回“错误优先回调”风格——主要用于库开发,兼容老代码。

核心逻辑:用then处理 Async 函数的结果,callback(null, result)传递成功,callback(err)传递错误。

实战场景:写一个“同时支持两种风格”的 npm 包
假设你要写一个支付库,既想支持现代的 Promise 风格,又想兼容老项目的回调风格:

const { callbackify, promisify } = require('node:util')
const paymentGateway = require('./payment-gateway') // 假设是Async风格
const db = require('./db')
// 核心逻辑:Async函数(创建支付订单+保存到数据库)
async function createPayment(orderId, amount) {
// 1. 调用支付网关API
const payment = await paymentGateway.create({ orderId, amount })
// 2. 保存支付记录到数据库
await db.query('INSERT INTO payments SET ?', [payment])
return payment
}
// 对外暴露的API:同时支持Promise和回调
function paymentAPI(orderId, amount, callback) {
if (typeof callback === 'function') {
// 用callbackify转成回调风格(注意绑定this!)
return callbackify(createPayment.bind(this))(orderId, amount, callback)
}
// 返回Promise
return createPayment(orderId, amount)
}
// 用户使用方式1:Promise
paymentAPI('order_123', 100).then((payment) =>
console.log('支付成功:', payment)
)
// 用户使用方式2:回调
paymentAPI('order_123', 100, (err, payment) => {
if (err) console.error('支付失败:', err)
else console.log('支付成功:', payment)
})

效果:你的 npm 包能覆盖 99%的用户场景——不管是新项目用 Promise,还是老项目用回调,都能无缝使用。

2. 工具函数:解决“小痛点”的大用处#

(1)format:字符串格式化的“偷懒神器”#

作用:类似console.log的格式化,但返回字符串——代替繁琐的字符串拼接,还能统一格式。

核心语法:用%s(字符串)、%d(数字)、%o(对象)占位,自动替换为参数。

实战场景:生成符合 ELK 格式的日志

在实际项目中,日志需要符合**ELK(Elasticsearch、Logstash、Kibana)**的格式(JSON 字符串),用format能快速生成:

const { format } = require('node:util')
const winston = require('winston') // 日志库
// 配置winston:生成JSON格式的日志
const logger = winston.createLogger({
transports: [new winston.transports.Console()],
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf((info) => {
// 用util.format生成ELK格式的日志
return format(
'{"@timestamp":"%s","level":"%s","message":"%s","metadata":%o}',
info.timestamp, // %s:字符串(时间戳)
info.level, // %s:字符串(日志级别)
info.message, // %s:字符串(日志内容)
info.metadata || {} // %o:对象(附加信息)
)
})
),
})
// 使用:记录用户登录日志
logger.info('用户登录成功', {
metadata: { username: 'admin', ip: '192.168.1.1', userId: 1 },
})
// 输出结果(JSON字符串):
// {"@timestamp":"2024-05-21T10:00:00.000Z","level":"info","message":"用户登录成功","metadata":{"username":"admin","ip":"192.168.1.1","userId":1}}

效果:不用手动拼接 JSON 字符串,避免“引号没转义”的语法错误,还能统一日志格式。

(2)types:精准判断类型的“火眼金睛”#

作用:比typeofinstanceof更精准的类型检查——解决“判断 Promise 却得到 function”的问题。

常用方法及场景

方法用途实战场景
types.isPromise判断是不是真正的Promise 对象检查函数返回值是不是 Promise
types.isObject判断是不是普通对象(不是数组/Null)检查 HTTP 请求体是不是 JSON 对象
types.isString判断是不是字符串检查接口参数是不是字符串
types.isNumber判断是不是数字检查接口参数是不是数字
types.isArray判断是不是数组检查接口参数是不是数组

实战场景:Koa 中间件中的参数校验
写一个 Koa 中间件,检查用户注册接口的请求体类型:

const { types } = require('node:util')
const koa = require('koa')
const bodyParser = require('koa-bodyparser')
const app = new koa()
app.use(bodyParser()) // 解析JSON请求体
// 参数校验中间件
app.use(async (ctx, next) => {
const body = ctx.request.body
// 1. 检查请求体是不是普通对象(不是数组、不是Null)
if (!types.isObject(body) || types.isArray(body) || body === null) {
ctx.status = 400
ctx.body = { error: '请求体必须是JSON对象' }
return
}
// 2. 检查username是不是字符串(长度≥3)
if (!types.isString(body.username) || body.username.length < 3) {
ctx.status = 400
ctx.body = { error: 'username必须是长度≥3的字符串' }
return
}
// 3. 检查age是不是整数(≥18)
if (
!types.isNumber(body.age) ||
!Number.isInteger(body.age) ||
body.age < 18
) {
ctx.status = 400
ctx.body = { error: 'age必须是≥18的整数' }
return
}
// 4. 检查hobbies是不是数组(元素是字符串)
if (!types.isArray(body.hobbies) || !body.hobbies.every(types.isString)) {
ctx.status = 400
ctx.body = { error: 'hobbies必须是字符串数组' }
return
}
await next()
})
// 注册接口
app.post('/register', async (ctx) => {
ctx.body = { message: '注册成功', user: ctx.request.body }
})
app.listen(3000)

效果:用types模块精准校验参数类型,避免“字符串型数字”“伪装成数组的对象”等问题——比typeof可靠 10 倍!

2. 其他实用工具:解决“冷门但致命”的问题#

(1)inspect:调试复杂对象的“显微镜”#

作用:“展开”复杂对象,自定义显示深度、颜色,还能隐藏敏感信息。

实战场景:调试 Koa 的ctx对象
Koa 的ctx对象包含reqresstatecookies等属性,用inspect可以快速找到state.user

const { inspect } = require('node:util')
const koa = require('koa')
const app = new koa()
app.use(async (ctx, next) => {
// 调试ctx对象:显示到第3层,带颜色,隐藏Cookie和Authorization
console.log(
inspect(ctx, {
depth: 3, // 展开到第3层
colors: true, // 带颜色(终端中更清晰)
hideKeys: [
// 隐藏敏感信息
'headers.cookie',
'headers.authorization',
],
})
)
await next()
})
app.get('/', async (ctx) => {
ctx.state.user = { id: 1, username: 'admin' } // 模拟用户登录
ctx.body = 'Hello World'
})
app.listen(3000)

输出效果(终端中带颜色):

KoaContext {
request: { method: 'GET', url: '/', headers: { ... }, ... },
response: { status: 200, message: 'OK', ... },
state: { user: { id: 1, username: 'admin' } },
...
}

清晰展示state.user,还隐藏了cookiesauthorization等敏感信息——调试效率提升 100%!

(2)deprecated:标记函数已废弃#

作用:标记旧函数为“废弃”,调用时会在控制台发出警告——提示用户升级到新函数。

实战场景:平滑过渡旧 API
假设你有一个旧的getUser函数(回调风格),现在想改成 Promise 风格,同时保留旧函数:

const { deprecated, promisify } = require('node:util')
const db = require('./db')
// 旧的回调风格函数
function oldGetUser(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], callback)
}
// 新的Promise风格函数
const newGetUser = promisify(oldGetUser)
// 标记旧函数为废弃,并提示使用新函数
const deprecatedGetUser = deprecated(
oldGetUser,
'oldGetUser已废弃,请使用newGetUser(Promise风格)'
)
// 用户调用旧函数时,会发出警告
deprecatedGetUser(1, (err, user) => {
if (err) throw err
console.log(user)
})
// 控制台输出:(node:1234) DeprecationWarning: oldGetUser已废弃,请使用newGetUser(Promise风格)

效果:用户不会突然报错,而是收到“友好提示”——平滑过渡到新 API。

三、综合实战:用 util 模块改造一个旧 Express 接口#

我们用util 模块改造一个旧的 Express 接口,从“回调风格”改成“Async/Await”,同时保留回调支持。

1. 旧接口:回调风格(嵌套 2 层)#

假设你有一个获取用户详情+用户订单的 Express 接口,用mysql模块的回调风格实现:

const express = require('express')
const mysql = require('mysql')
const app = express()
// 数据库连接配置
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'password',
database: 'test_db',
})
db.connect()
// 旧接口:回调风格(嵌套2层)
app.get('/users/:id', (req, res) => {
const userId = req.params.id
// 1. 查用户详情
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, user) => {
if (err) return res.status(500).send('数据库错误')
if (!user || user.length === 0) return res.status(404).send('用户不存在')
// 2. 查用户订单(嵌套回调)
db.query(
'SELECT * FROM orders WHERE user_id = ?',
[userId],
(err, orders) => {
if (err) return res.status(500).send('数据库错误')
// 返回结果
res.send({
user: user[0],
orders: orders,
})
}
)
})
})
app.listen(3000)

问题:嵌套的回调让代码逻辑“绕弯”,调试时要逐层排查错误——比如用户不存在时,要先看user是否为空,再看orders的查询。

2. 用 util 模块改造:Async/Await + 兼容回调#

我们用promisify转换数据库查询函数,用callbackify保留回调支持,让接口同时支持两种风格

步骤 1:转换数据库查询为 Promise 风格#

首先,用promisifydb.query(回调风格)转成 Promise 风格:

const { promisify, callbackify } = require('node:util')
// 转换db.query为Promise(注意绑定this!)
// 因为db.query是db的方法,需要保持this指向db
const queryAsync = promisify(db.query).bind(db)

步骤 2:改写接口为 Async 函数#

用 Async/Await 改写路由逻辑,消除嵌套:

// 新接口:Async/Await风格(无嵌套)
async function getUserHandler(req, res) {
const userId = req.params.id
try {
// 1. 查用户详情
const [user] = await queryAsync('SELECT * FROM users WHERE id = ?', [
userId,
])
if (!user) return res.status(404).send('用户不存在')
// 2. 查用户订单
const orders = await queryAsync('SELECT * FROM orders WHERE user_id = ?', [
userId,
])
// 返回结果
res.send({ user, orders })
} catch (err) {
// 统一错误处理
res.status(500).send(`数据库错误:${err.message}`)
}
}

步骤 3:用callbackify保留回调支持#

为了兼容旧调用方式(比如某些老客户端还在用回调),用callbackify把 Async 函数转回回调风格:

// 生成回调风格的接口(兼容旧代码)
const callbackGetUserHandler = callbackify(getUserHandler)

步骤 4:注册接口(同时支持两种风格)#

把两种风格的接口都注册到 Express:

// 注册Promise风格的接口(新客户端用)
app.get('/api/users/:id', getUserHandler)
// 注册回调风格的接口(旧客户端用)
app.get('/legacy/users/:id', (req, res) => {
// 把Express的res对象传递给回调函数
callbackGetUserHandler(req, res, (err, result) => {
if (err) return res.status(500).send(err.message)
res.send(result)
})
})

3. 改造后的效果:同时支持两种调用方式#

方式 1:Promise 风格(新客户端)#

fetch调用/api/users/:id

// 新客户端调用(Promise风格)
fetch('http://localhost:3000/api/users/1')
.then((res) => res.json())
.then((data) => console.log('用户详情:', data))
.catch((err) => console.error('错误:', err))

方式 2:回调风格(旧客户端)#

request模块调用/legacy/users/:id

const request = require('request')
// 旧客户端调用(回调风格)
request('http://localhost:3000/legacy/users/1', (err, res, body) => {
if (err) return console.error('错误:', err)
const data = JSON.parse(body)
console.log('用户详情:', data)
})

4. 改造后的好处#

  1. 代码更清晰:Async/Await 消除了嵌套,逻辑线性执行,调试时一目了然;
  2. 兼容旧代码:用callbackify保留了回调风格,老客户端无需修改;
  3. 统一错误处理:用try/catch统一捕获错误,避免重复写if (err)

四、总结:util 模块的“正确打开方式”#

util 模块不是“花架子”,而是解决真实开发痛点的工具库——它帮你:

  • promisify告别“回调地狱”;
  • callbackify兼容旧代码;
  • types精准判断类型;
  • inspect高效调试复杂对象;
  • deprecated平滑过渡旧 API。

最后送你 3 个“避坑提醒”

  1. 绑定this:用promisify转换对象方法时(比如db.query),一定要用bind绑定对象的this——否则this会指向undefined
  2. 错误处理:Async 函数的错误要用try/catch捕获,否则会“吞掉”错误;
  3. 按需引入:别加载整个 util 模块,用解构只拿需要的函数(比如const { promisify } = require('node:util'))。

Node.js 的 util 模块像一位“隐形的助手”,藏在核心库中,却能解决你 80%的开发痛点。学会用它,你写代码的速度会变快,代码质量也会提高——下次遇到问题时,先想想:“util 模块里有没有工具能解决?”——答案大概率是“有”。

如果这篇文章帮到你,欢迎点赞收藏~ 有问题评论区见!👇

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【Node.js】学习util模块:最佳实践总结
https://blog.fridolph.top/posts/2023-05-22__utils/
作者
Fridolph
发布于
2023-05-22
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录