NodeJs学习日志(5):工具代码
一些在nodejs学习中常用的工具代码。
需要安装的包有:
| 包名 | 用处说明 |
|---|---|
| express | 基础Web框架,用于创建路由、处理HTTP请求和响应,是整个接口的核心依赖 |
| multer | 处理multipart/form-data类型请求,专门用于文件上传功能(如头像上传) |
| uuid | 生成UUID格式的随机字符串,在randomUtils.js中用于generateUUID函数 |
| http-errors | 生成标准化的HTTP错误对象,response.js中用于处理各类HTTP错误场景 |
| sequelize | (可选)ORM数据库工具,response.js中提到处理其验证错误,用到时才需安装 |
一次性安装
npm install express multer uuid http-errors
nameEncoder.js
做文件上传的时候, Multer 默认处理文件名用的是 ISO-8859-1 编码,这会导致中文文件名乱码。这个工具可以用来处理编码问题。
里面主要有三个函数:
- decodeFileName:把 Multer 传过来的文件名从 ISO-8859-1 转成 UTF-8,解决乱码
- encodeFileName:有时候需要反向转换,就用这个
- processFileName:实际业务里用的,调用解码函数,还会打印日志方便调试
实际用的时候,上传文件时先用它处理一下原始文件名,就能正确显示中文了。
/**
* 文件名转码工具
* 处理文件上传过程中可能出现的文件名编码问题
*/
/**
* 解码Multer接收的文件名
* Multer默认使用ISO-8859-1编码文件名,需要转换为UTF-8
* @param {string} originalName - Multer接收的原始文件名
* @returns {string} 解码后的UTF-8文件名
*/
const decodeFileName = (originalName) => {
if (!originalName || typeof originalName !== 'string') {
return '';
}
// 从ISO-8859-1(Latin1)解码为UTF-8
return Buffer.from(originalName, 'latin1').toString('utf8');
};
/**
* 编码文件名以适应不同环境
* 将UTF-8文件名编码为ISO-8859-1
* @param {string} fileName - UTF-8编码的文件名
* @returns {string} 编码后的文件名
*/
const encodeFileName = (fileName) => {
if (!fileName || typeof fileName !== 'string') {
return '';
}
// 将UTF-8编码为ISO-8859-1(Latin1)
return Buffer.from(fileName, 'utf8').toString('latin1');
};
/**
* 安全地处理文件名编码
* 包含日志输出用于调试
* @param {string} originalName - Multer接收的原始文件名
* @returns {string} 处理后的文件名
*/
const processFileName = (originalName) => {
const decodedName = decodeFileName(originalName);
// 日志输出,方便调试编码问题
console.log('原始文件名(Multer接收):', originalName);
console.log('解码后的文件名:', decodedName);
return decodedName;
};
module.exports = {
decodeFileName,
encodeFileName,
processFileName
};
randomUtils.js
很多地方都需要随机字符串,比如生成文件名、验证码、密钥这些。集中写在一个工具里方便管理。
几个常用的函数:
- generateSecureSecret:生成加密级别的密钥,适合做 JWT 密钥之类的,默认 32 字节,安全性够高
- generateUUID:用 uuid 库生成 UUID,去掉了连字符,变成 32 位字符串
- generateRandomString:可以自定义长度和字符集,默认是大小写字母加数字
- generateNumberCode:专门生成数字验证码,默认 6 位,登录注册时常用
比如上传文件时的随机文件名,就是用 generateRandomString 生成的,避免重名覆盖。
/**
* 提供多种场景下的随机字符串生成方法
* - 高安全性随机密钥(适合加密、JWT签名等场景)
* - 标准化UUID格式字符串
* - 自定义长度和字符集的随机串
* - 数字验证码(适合登录、注册等验证场景)
* - 使用crypto模块保证随机性(优于Math.random())
* - 支持自定义字符集,满足不同格式需求
* - 内置长度校验,避免生成无效字符串
*/
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
/**
* 生成加密级别的随机密钥(适合JWT、加密等场景)
* @param {number} byteLength - 字节长度(默认32字节=256位,足够安全)
* @returns {string} 十六进制格式的随机密钥
*/
const generateSecureSecret = (byteLength = 32) => {
if (byteLength < 16) {
throw new Error('密钥长度不应小于16字节(128位),以保证安全性');
}
return crypto.randomBytes(byteLength).toString('hex');
};
/**
* 生成UUIDv4格式的随机字符串(无连字符)
* @returns {string} 32位UUID字符串
*/
const generateUUID = () => {
return uuidv4().replace(/-/g, '');
};
/**
* 生成自定义长度和字符集的随机字符串
* @param {number} length - 字符串长度(默认16)
* @param {string} chars - 可选字符集(默认包含大小写字母和数字)
* @returns {string} 随机字符串
*/
const generateRandomString = (length = 16, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') => {
if (length < 1) {
throw new Error('字符串长度必须大于0');
}
if (!chars || chars.length === 0) {
throw new Error('字符集不能为空');
}
let result = '';
const charsLength = chars.length;
const maxByte = 256 - (256 % charsLength); // 确保均匀分布
for (let i = 0; i < length; i++) {
let randomByte;
// 生成均匀分布的随机索引
do {
randomByte = crypto.randomBytes(1).readUInt8(0);
} while (randomByte >= maxByte);
result += chars[randomByte % charsLength];
}
return result;
};
/**
* 生成数字验证码
* @param {number} length - 验证码长度(默认6位)
* @returns {string} 数字验证码
*/
const generateNumberCode = (length = 6) => {
return generateRandomString(length, '0123456789');
};
module.exports = {
generateSecureSecret, // 高安全性密钥(推荐用于JWT)
generateUUID, // UUID格式字符串
generateRandomString, // 自定义字符集字符串
generateNumberCode // 数字验证码
};
response.js
这个工具主要是统一接口的返回格式,不管成功还是失败,格式保持一致,前端处理起来方便。
success 函数返回成功响应,包含状态、状态码、消息和数据。
failed 函数处理各种错误情况,会根据不同的错误类型自动设置状态码和消息:
- 比如 Sequelize 的验证错误,会自动提取错误信息
- JWT 相关的错误,自动返回 401 状态
- multer 的文件上传错误,会区分是大小超限还是其他问题
- 也支持自定义的错误类型,比如 ‘UNAUTHORIZED’、‘NOT_FOUND’ 这些
这样在业务代码里,不用每次都手动拼响应格式,直接调用这两个函数就行。
/**
* 接口响应格式化工具
*
* 统一API接口的响应格式,包含成功和失败两种场景:
* - 成功响应:固定包含状态、状态码、消息和数据
* - 失败响应:自动识别错误类型,返回对应状态码和详细错误信息
*
* 已支持的错误类型:
* - Sequelize验证错误(数据库模型验证失败)
* - JWT相关错误(token无效、过期等)
* - http-errors库创建的标准HTTP错误
* - multer文件上传错误(大小超限等)
* - 自定义错误类型(如UNAUTHORIZED、NOT_FOUND等)
*
* 如何扩展新的错误类型?
* 1. 先在ERROR_TYPES对象中添加新类型(如果需要):
* 例如:NEW_ERROR: { code: 400, message: '新的错误描述' }
*
* 2. 在failed函数中添加对应的判断逻辑:
* 例如处理特定错误对象:
* else if (error instanceof 自定义错误类) {
* statusCode = 400;
* message = '处理自定义错误';
* errors = [error.message];
* }
*/
const createError = require('http-errors');
const multer = require('multer');
// 内部定义常见错误类型及其默认状态码和消息
const ERROR_TYPES = {
UNAUTHORIZED: { code: 401, message: '未授权访问' },
FORBIDDEN: { code: 403, message: '权限不足,无法访问' },
NOT_FOUND: { code: 404, message: '请求的资源不存在' },
BAD_REQUEST: { code: 400, message: '请求参数错误' },
CONFLICT: { code: 409, message: '资源冲突' },
TOO_MANY_REQUESTS: { code: 429, message: '请求过于频繁' },
INTERNAL_ERROR: { code: 500, message: '服务器内部错误' },
PAYLOAD_TOO_LARGE: { code: 413, message: '请求实体过大' }
};
/**
* 成功响应处理
* @param {string} message - 成功消息
* @param {any} data - 返回的数据
* @param {number} code - 状态码,默认200
* @returns {Object} 格式化的成功响应对象
*/
const success = (message, data, code = 200) => {
return {
status: "success",
code: code,
message: message || "操作成功",
data: data !== undefined ? data : null
};
};
/**
* 失败响应处理
* @param {string|Error|Object} error - 错误消息、错误对象或错误类型标识
* @param {any} data - 错误相关数据,可选
* @returns {Object} 格式化的失败响应对象
*/
const failed = (error, data) => {
let statusCode = 500;
let message = '服务器错误';
let errors = [message];
// 处理 Sequelize 验证错误
if (error.name === 'SequelizeValidationError') {
statusCode = 400;
message = '数据验证失败';
errors = error.errors.map(e => e.message);
}
// 处理 JWT 相关错误
else if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
statusCode = 401;
message = '请求失败: 认证错误';
errors = ['您提交的 token 错误或已过期。'];
}
// 处理 http-errors 库创建的错误
else if (error instanceof createError.HttpError) {
statusCode = error.statusCode;
message = `请求失败: ${error.name}`;
errors = [error.message];
}
// 处理 multer 文件上传错误
else if (error instanceof multer.MulterError) {
if (error.code === 'LIMIT_FILE_SIZE') {
statusCode = 413;
message = `请求失败: ${error.name}`;
errors = ['文件大小超出限制。'];
} else {
statusCode = 400;
message = `请求失败: ${error.name}`;
errors = [error.message];
}
}
// 处理内置错误类型标识(如'UNAUTHORIZED')
else if (typeof error === 'string' && ERROR_TYPES[error]) {
const errorType = ERROR_TYPES[error];
statusCode = errorType.code;
message = `请求失败: ${error}`;
errors = [errorType.message];
}
// 处理普通 Error 对象
else if (error instanceof Error) {
message = `请求失败: ${error.name}`;
errors = [error.message || '发生未知错误'];
}
// 处理字符串错误消息
else if (typeof error === 'string') {
message = '请求失败';
errors = [error];
}
// 如果提供了自定义数据,则合并错误信息
const errorData = data !== undefined ? data : { errors };
return {
status: "failed",
code: statusCode,
message: message,
data: errorData
};
};
module.exports = {
success,
failed
};
validationUtils.js
接口请求过来,进行参数验证。
- checkRequiredFields 用来检查必填参数,比如用户 ID 这类必须传的字段,少了就返回错误。
- validateFileType 验证文件类型,比如头像上传只允许图片格式。
- validateFileSize 检查文件大小,防止传太大的文件。
/**
* 数据验证工具函数
*
* 集中处理接口请求中的常见验证需求:
* - 检查必填参数是否完整
* - 验证上传文件的类型是否符合要求
* - 限制文件大小,防止超大文件上传
*
* 1. 每个验证函数独立返回结果(通过返回true,失败返回错误信息)
* 2. 错误信息包含具体细节,方便前端展示
* 3. 与业务逻辑解耦,可复用于不同接口
*/
/**
* 检查必填参数是否存在
* @param {Object} data - 待检查的数据(如req.body)
* @param {string[]} requiredFields - 必填字段数组
* @returns {Object|null} 错误信息(null表示验证通过)
*/
const checkRequiredFields = (data, requiredFields) => {
const missing = [];
requiredFields.forEach(field => {
if (data[field] === undefined || data[field] === null || data[field] === '') {
missing.push(field);
}
});
if (missing.length > 0) {
return {
message: `缺少必填参数: ${missing.join(', ')}`,
fields: missing
};
}
return null;
};
/**
* 验证文件类型是否符合要求
* @param {Object} file - multer文件对象
* @param {string[]} allowedTypes - 允许的MIME类型(如['image/jpeg', 'image/png'])
* @returns {boolean|string} 验证结果(true表示通过,否则返回错误信息)
*/
const validateFileType = (file, allowedTypes) => {
if (!allowedTypes.includes(file.mimetype)) {
return `不支持的文件类型: ${file.mimetype},允许的类型: ${allowedTypes.join(', ')}`;
}
return true;
};
/**
* 验证文件大小是否超出限制
* @param {Object} file - multer文件对象
* @param {number} maxSize - 最大大小(字节)
* @returns {boolean|string} 验证结果(true表示通过,否则返回错误信息)
*/
const validateFileSize = (file, maxSize) => {
if (file.size > maxSize) {
const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
return `文件大小超出限制,最大允许 ${maxSizeMB}MB`;
}
return true;
};
module.exports = {
checkRequiredFields,
validateFileType,
validateFileSize
};
示例代码
// routes/avatarUpload.js
const express = require('express');
const multer = require('multer');
const { checkRequiredFields } = require('../utils/validationUtils');
const { validateFileType, validateFileSize } = require('../utils/validationUtils');
const { success, failed } = require('../utils/response');
const { processFileName } = require('../utils/nameEncoder');
const { generateRandomString } = require('../utils/randomUtils');
const router = express.Router();
// 配置Multer存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/avatars/'); // 存储目录
},
filename: (req, file, cb) => {
// 处理文件名编码问题
const decodedName = processFileName(file.originalname);
// 获取文件扩展名
const ext = decodedName.split('.').pop() || '';
// 生成随机文件名(避免冲突)
const randomName = generateRandomString(16);
const fileName = `${randomName}.${ext}`;
cb(null, fileName);
}
});
// 初始化Multer上传中间件
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 } // 5MB(与后续验证保持一致)
});
// 头像上传接口
router.post('/', upload.single('avatar'), (req, res) => {
try {
// 1. 验证必填参数,检查有没有userid这个参数
const requiredFields = ['userId'];
const paramError = checkRequiredFields(req.body, requiredFields);
if (paramError) {
return res.status(400).json(failed('BAD_REQUEST', paramError));
}
// 2. 验证文件是否存在,确认上传了头像文件
if (!req.file) {
return res.status(400).json(failed('缺少上传文件'));
}
// 3. 验证文件类型(仅允许图片)
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
const typeError = validateFileType(req.file, allowedTypes);
if (typeError !== true) {
return res.status(400).json(failed(typeError));
}
// 4. 验证文件大小(5MB)
const maxSize = 5 * 1024 * 1024; // 5MB
const sizeError = validateFileSize(req.file, maxSize);
if (sizeError !== true) {
return res.status(413).json(failed(sizeError));
}
// 5. 处理成功逻辑(实际项目中可能保存文件路径到数据库)
const result = {
userId: req.body.userId,
fileName: req.file.filename,
originalName: processFileName(req.file.originalname),
url: `/avatars/${req.file.filename}`,
size: req.file.size
};
// 6. 返回成功响应
return res.json(success('头像上传成功', result));
} catch (error) {
// 统一错误处理
return res.status(500).json(failed(error));
}
});
module.exports = router;
1076

被折叠的 条评论
为什么被折叠?



