NodeJs学习日志(5):工具代码

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;


评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值