本教程示例使用nodejs来做一个自定义随机图的接口(我自己目前在用)
准备工作
首先你的电脑上需要node环境,去往官网下载最新版本的Node.js长期维护版本(本项目要求node>=14.0.0),我这里则是v22.17.1(LTS)
根据你的系统安装对应的程序。
源码分享
node项目中最重要的就是package.json文件,这个文件定义了环境,包名。
项目启动前的准备工作:
请在一个单独的空文件夹中
新建文件命名为package.json
package.json:
{
"name": "random-image-api",
"version": "1.0.0",
"description": "随机图片API服务",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
},
"engines": {
"node": ">=14.0.0"
},
"author": "作者",
"license": "MIT"
}
接下来请安装项目所需的环境:
npm install
安装环境后你的目录会多出一个node_modules文件夹。
2. 接下来和package.json同级目录下新建server.js文件
server.js:
const express = require('express');
const fs = require('fs').promises;
const fsSync = require('fs');
const path = require('path');
// 配置文件路径(相对于服务器脚本)
const CONFIG_FILE = 'config.json';
// 日志中显示的最大文件数量
const MAX_LOG_FILES = 5;
// 日志颜色
const LOG_COLORS = {
info: '\x1b[36m', // 青色 - 信息
success: '\x1b[32m', // 绿色 - 成功
warning: '\x1b[33m', // 黄色 - 警告
error: '\x1b[31m', // 红色 - 错误
reset: '\x1b[0m' // 重置颜色
};
// 带时间戳的日志函数
function log(level, message) {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
const color = LOG_COLORS[level] || LOG_COLORS.info;
console.log(`${color}[${timestamp}] [${level.toUpperCase()}] ${message}${LOG_COLORS.reset}`);
}
// 读取并验证配置文件
async function loadConfig() {
try {
log('info', '加载配置文件');
// 读取配置文件
const data = await fs.readFile(CONFIG_FILE, 'utf8');
const config = JSON.parse(data);
log('info', '验证配置结构');
validateConfig(config);
log('success', '配置加载成功');
return config;
} catch (err) {
log('error', `配置错误: ${err.message}`);
process.exit(1);
}
}
// 验证配置文件格式
function validateConfig(config) {
// 验证端口
if (!config.port || typeof config.port !== 'number' || config.port <= 0) {
throw new Error('缺少有效端口号');
}
// 验证路由
if (!config.routes || typeof config.routes !== 'object') {
throw new Error('缺少路由配置');
}
// 验证每个路由
for (const [routePath, routeConfig] of Object.entries(config.routes)) {
// 验证路由路径
if (!routePath.startsWith('/')) {
throw new Error(`路由路径必须以斜杠开头: ${routePath}`);
}
// 验证路由配置
if (!routeConfig.type || !['image', 'text', 'directory'].includes(routeConfig.type)) {
throw new Error(`无效的路由类型: ${routePath}`);
}
// 验证路径
if (!routeConfig.path) {
throw new Error(`缺少路径配置: ${routePath}`);
}
// 验证文件扩展名 (仅对 directory 类型需要)
if (routeConfig.type === 'directory' && (!routeConfig.allowedExtensions || !Array.isArray(routeConfig.allowedExtensions) || routeConfig.allowedExtensions.length === 0)) {
throw new Error(`缺少有效文件扩展名列表: ${routePath}`);
}
// 验证编码
if (routeConfig.type === 'text' && !routeConfig.encoding) {
routeConfig.encoding = 'utf8';
log('warning', `文本路由 ${routePath} 未指定编码,使用默认值: utf8`);
}
}
}
// 处理目录请求(随机返回目录中的文件)
async function handleDirectoryRequest(res, routeConfig) {
const { path: dirPath, allowedExtensions } = routeConfig;
const fullPath = path.join(__dirname, dirPath);
log('info', `处理目录请求: ${dirPath}`);
// 检查目录是否存在
if (!fsSync.existsSync(fullPath) || !fsSync.statSync(fullPath).isDirectory()) {
log('error', `目录不存在: ${fullPath}`);
return res.status(404).send(`错误:目录 "${dirPath}" 不存在`);
}
// 检查访问权限
try {
await fs.access(fullPath, fs.constants.R_OK);
} catch {
log('error', `无权限访问目录: ${fullPath}`);
return res.status(403).send(`错误:没有权限访问目录 "${dirPath}"`);
}
// 读取目录中的文件
const files = await fs.readdir(fullPath);
const validFiles = files.filter(file => {
const ext = path.extname(file).toLowerCase();
return allowedExtensions.includes(ext);
});
// 优化日志输出:限制显示的文件数量
if (validFiles.length > 0) {
let fileListLog = validFiles.slice(0, MAX_LOG_FILES).join(', ');
if (validFiles.length > MAX_LOG_FILES) {
fileListLog += `, ... ${validFiles.length - MAX_LOG_FILES} more items`;
}
log('info', `目录 ${dirPath} 中的有效文件: ${fileListLog}`);
} else {
log('warning', `目录 ${dirPath} 中没有有效文件`);
}
// 检查是否有有效文件
if (validFiles.length === 0) {
return res.status(404).send(`错误:目录 "${dirPath}" 中没有有效文件`);
}
// 随机选择一个文件
const randomIndex = Math.floor(Math.random() * validFiles.length);
const randomFile = validFiles[randomIndex];
const filePath = path.join(fullPath, randomFile);
log('success', `随机选择文件: ${filePath}`);
// 返回与 handleFileRequest 一致的结构
return { filePath, fileName: randomFile };
}
// 处理文件请求
async function handleFileRequest(res, routeConfig) {
const { path: filePath } = routeConfig;
const fullPath = path.join(__dirname, filePath);
log('info', `处理文件请求: ${filePath}`);
// 检查文件是否存在
if (!fsSync.existsSync(fullPath) || !fsSync.statSync(fullPath).isFile()) {
log('error', `文件不存在: ${fullPath}`);
return res.status(404).send(`错误:文件 "${filePath}" 不存在`);
}
// 检查访问权限
try {
await fs.access(fullPath, fs.constants.R_OK);
} catch {
log('error', `无权限访问文件: ${fullPath}`);
return res.status(403).send(`错误:没有权限访问文件 "${filePath}"`);
}
log('success', `找到文件: ${fullPath}`);
return { filePath: fullPath, fileName: path.basename(fullPath) };
}
// 处理请求
async function handleRequest(res, routeConfig) {
let fileInfo;
log('info', `处理请求: ${routeConfig.type} ${routeConfig.path}`);
// 根据类型处理目录或文件
if (routeConfig.type === 'directory') {
fileInfo = await handleDirectoryRequest(res, routeConfig);
if (!fileInfo) return; // 错误已由 handleDirectoryRequest 处理
} else {
fileInfo = await handleFileRequest(res, routeConfig);
if (!fileInfo) return; // 错误已由 handleFileRequest 处理
}
// 确保文件信息有效
if (!fileInfo.filePath) {
log('error', '文件路径未定义');
return res.status(500).send('错误:文件路径未定义');
}
const { filePath, fileName } = fileInfo;
const ext = path.extname(fileName).toLowerCase();
log('info', `准备发送文件: ${filePath}`);
// 设置响应头
let contentType = 'application/octet-stream';
if (routeConfig.type === 'image' || (routeConfig.type === 'directory' && /\.(jpg|jpeg|png|webp|gif)$/i.test(fileName))) {
if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg';
if (ext === '.png') contentType = 'image/png';
if (ext === '.webp') contentType = 'image/webp';
if (ext === '.gif') contentType = 'image/gif';
// 防止图片被缓存
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
} else if (routeConfig.type === 'text' || (routeConfig.type === 'directory' && /\.(txt|md|html|json|xml)$/i.test(fileName))) {
// 设置文本内容类型并指定编码
contentType = `text/plain; charset=${routeConfig.encoding || 'utf8'}`;
}
log('info', `设置 Content-Type: ${contentType}`);
// 流式传输文件,隐藏实际文件路径
const stream = fsSync.createReadStream(filePath);
stream.pipe(res);
log('success', `开始流式传输`);
// 错误处理
stream.on('error', (err) => {
log('error', `文件传输错误: ${err.message}`);
if (!res.headersSent) {
res.status(500).send('错误:文件读取失败');
}
});
}
// 创建服务器并设置路由
async function setupServer(config) {
log('info', '设置服务器路由');
const app = express();
// 设置所有路由
for (const [routePath, routeConfig] of Object.entries(config.routes)) {
app.get(routePath, async (req, res) => {
try {
log('info', `收到请求: ${routePath}`);
await handleRequest(res, routeConfig);
} catch (err) {
log('error', `路由 "${routePath}" 错误: ${err.message}`);
res.status(500).send(`服务器错误:${err.message}`);
}
});
}
// 404处理
app.use((req, res) => {
log('warning', `未找到路由: ${req.path}`);
res.status(404).send('错误:未找到该路由');
});
log('success', '服务器路由设置完成');
return app;
}
// 启动服务器
async function startServer() {
log('info', '启动服务器');
try {
// 加载并验证配置
const config = await loadConfig();
// 设置服务器
const app = await setupServer(config);
// 启动服务器
app.listen(config.port, () => {
log('success', `服务器成功运行在端口 ${config.port}`);
log('info', `配置文件: ${CONFIG_FILE}`);
// 简洁显示路由列表
const routesList = Object.keys(config.routes).join(',');
log('info', `已加载的路由: [${routesList}]`);
});
} catch (err) {
log('error', `服务器启动失败: ${err.message}`);
process.exit(1);
}
}
// 启动应用
startServer();
我将路由都定义好了,你只需要按照config.json来定义即可
config.json
{
"port": 3366,
"routes": {
"/": {
"type": "text",
"path": "texts/welcome.txt",
"encoding": "utf8"
},
"/img": {
"type": "directory",
"path": "images/img",
"allowedExtensions": [".jpg", ".jpeg", ".png", ".webp"]
},
"/text": {
"type": "directory",
"path": "texts",
"allowedExtensions": [".txt", ".md"],
"encoding": "gbk"
},
"/specific-image": {
"type": "image",
"path": "specific/path/to/image.jpg"
}
}
}
config.json代码解释: