Browse Source

feat: 重构项目结构并添加静态文件服务和路由功能

- 删除旧的userController并重构用户表结构
- 添加静态文件服务中间件和路径解析工具
- 实现自定义路由系统支持路由组和参数
- 更新package.json依赖和模块别名配置
- 优化响应时间中间件添加彩色日志输出
- 添加jsconfig.json改善开发体验
alpha
谢亚昕 2 months ago
parent
commit
d2e8df87f3
  1. 0
      .trae/.ignore
  2. BIN
      bun.lockb
  3. 23
      jsconfig.json
  4. 4
      package.json
  5. 1
      public/aa.txt
  6. 23
      src/controllers/userController.mjs
  7. 2
      src/db/migrations/20250616065041_create_users_table.mjs
  8. 6
      src/db/seeds/20250616071157_users_seed.mjs
  9. 85
      src/main.js
  10. 35
      src/plugins/ResponseTime/index.js
  11. 185
      src/plugins/Send/index.js
  12. 74
      src/plugins/Send/resolve-path.js
  13. 16
      src/plugins/install.js
  14. 110
      src/utils/router.js

0
.trae/.ignore

BIN
bun.lockb

Binary file not shown.

23
jsconfig.json

@ -0,0 +1,23 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
"db/*": [
"src/db/*"
],
"utils/*": [
"src/utils/*"
]
},
"module": "commonjs",
"target": "es6",
"allowSyntheticDefaultImports": true
},
"include": [
"src/**/*",
"jsconfig.json"
]
}

4
package.json

@ -19,10 +19,12 @@
"koa": "^3.0.0",
"log4js": "^6.9.1",
"module-alias": "^2.2.3",
"path-to-regexp": "^8.2.0",
"sqlite3": "^5.1.7"
},
"_moduleAliases": {
"@": "./src",
"db": "./src/db"
"db": "./src/db",
"utils": "./src/utils"
}
}

1
public/aa.txt

@ -0,0 +1 @@
sada

23
src/controllers/userController.mjs

@ -1,23 +0,0 @@
import db from "../db/index.js"
// 创建用户
export async function createUser(userData) {
const [id] = await db("users").insert(userData)
return id
}
// 查询所有用户
export async function getUsers() {
return db("users").select("*")
}
// 更新用户
export async function updateUser(id, updates) {
updates.updated_at = new Date()
return db("users").where("id", id).update(updates)
}
// 删除用户
export async function deleteUser(id) {
return db("users").where("id", id).del()
}

2
src/db/migrations/20250616065041_create_users_table.mjs

@ -5,7 +5,7 @@
export const up = async knex => {
return knex.schema.createTable("users", function (table) {
table.increments("id").primary() // 自增主键
table.string("name", 100).notNullable() // 字符串字段(最大长度100)
table.string("username", 100).notNullable() // 字符串字段(最大长度100)
table.string("email", 100).unique().notNullable() // 唯一邮箱
table.integer("age").unsigned() // 无符号整数
table.timestamp("created_at").defaultTo(knex.fn.now()) // 创建时间

6
src/db/seeds/20250616071157_users_seed.mjs

@ -1,7 +1,7 @@
export const seed = async knex => {
// 检查表是否存在
const tables = await knex.raw(`
SELECT name FROM sqlite_master WHERE type='table' AND name='users'
SELECT name FROM sqlite_master WHERE type='table' AND username='users'
`)
if (tables.length === 0) {
@ -13,7 +13,7 @@ export const seed = async knex => {
// Inserts seed entries
await knex("users").insert([
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
{ username: "Alice", email: "alice@example.com" },
{ username: "Bob", email: "bob@example.com" },
])
}

85
src/main.js

@ -1,38 +1,71 @@
import "./logger"
import "module-alias/register"
import Koa from "koa"
import os from "os"
import LoadPlugins from "./plugins/install"
import UserModel from "./db/models/UserModel"
import './logger.js';
import Koa from 'koa';
import os from 'os';
import log4js from "log4js"
import LoadPlugins from "./plugins/install.js"
import Router from './utils/router.js';
const logger = log4js.getLogger()
const app = new Koa()
const app = new Koa();
LoadPlugins(app)
app.use(async ctx => {
ctx.body = await UserModel.findAll()
})
app.on("error", err => {
logger.error("server error", err)
})
const router = new Router({ prefix: '/api' });
// 基础路由
router.get('/hello', (ctx) => {
ctx.body = 'Hello World';
});
// 参数路由
router.get('/user/:id', (ctx) => {
ctx.body = `User ID: ${ctx.params.id}`;
});
// 路由组
router.group('/v1', (v1) => {
v1.use((ctx, next) => {
ctx.set('X-API-Version', 'v1');
return next();
});
v1.get('/status', (ctx) => ctx.body = 'OK');
});
const server = app.listen(3000, () => {
const port = server.address().port
const getLocalIP = () => {
const interfaces = os.networkInterfaces()
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address
}
}
app.use(router.middleware());
// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next();
if (ctx.status === 404) {
ctx.status = 404;
ctx.body = { success: false, error: 'Resource not found' };
}
} catch (err) {
ctx.status = err.statusCode || 500;
ctx.body = { success: false, error: err.message || 'Internal server error' };
}
});
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
const port = server.address().port
const getLocalIP = () => {
const interfaces = os.networkInterfaces()
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address
}
return "localhost"
}
}
const localIP = getLocalIP()
logger.trace(`服务器运行在: http://${localIP}:${port}`)
return "localhost"
}
const localIP = getLocalIP()
logger.trace(`服务器运行在: http://${localIP}:${port}`)
})
export default app;

35
src/plugins/ResponseTime/index.js

@ -1,13 +1,30 @@
import log4js from "log4js"
import log4js from "log4js";
const logger = log4js.getLogger()
// ANSI颜色代码常量定义
const COLORS = {
REQ: '\x1b[36m', // 青色 - 请求标记
RES: '\x1b[32m', // 绿色 - 响应标记
TIME: '\x1b[33m', // 黄色 - 响应时间
METHOD: '\x1b[1m', // 加粗 - HTTP方法
RESET: '\x1b[0m' // 重置颜色
};
const logger = log4js.getLogger();
/**
* 响应时间记录中间件
* 为请求和响应添加彩色日志标记并记录处理时间
* @param {Object} ctx - Koa上下文对象
* @param {Function} next - Koa中间件链函数
*/
export default async (ctx, next) => {
logger.debug("::in:: %s %s", ctx.method, ctx.path)
const start = Date.now()
await next()
const ms = Date.now() - start
ctx.set("X-Response-Time", `${ms}ms`)
const rt = ctx.response.get("X-Response-Time")
logger.debug(`::out:: takes ${rt} for ${ctx.method} ${ctx.url}`)
// 彩色请求日志:青色标记 + 加粗方法名
logger.debug(`${COLORS.REQ}::req::${COLORS.RESET} ${COLORS.METHOD}%s${COLORS.RESET} %s`, ctx.method, ctx.path);
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set("X-Response-Time", `${ms}ms`);
// 彩色响应日志:绿色标记 + 黄色响应时间 + 加粗方法名
logger.debug(`${COLORS.RES}::res::${COLORS.RESET} takes ${COLORS.TIME}%s${COLORS.RESET} for ${COLORS.METHOD}%s${COLORS.RESET} %s`,
ctx.response.get("X-Response-Time"), ctx.method, ctx.url);
}

185
src/plugins/Send/index.js

@ -0,0 +1,185 @@
/**
* koa-send@5.0.1 转换为ES Module版本
* 静态资源服务中间件
*/
import fs from 'fs';
import { promisify } from 'util';
import logger from 'log4js';
import resolvePath from './resolve-path.js';
import createError from 'http-errors';
import assert from 'assert';
import { normalize, basename, extname, resolve, parse, sep } from 'path';
import { fileURLToPath } from 'url';
import path from "path"
// 转换为ES Module格式
const log = logger.getLogger('koa-send');
const stat = promisify(fs.stat);
const access = promisify(fs.access);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* 检查文件是否存在
* @param {string} path - 文件路径
* @returns {Promise<boolean>} 文件是否存在
*/
async function exists(path) {
try {
await access(path);
return true;
} catch (e) {
return false;
}
}
/**
* 发送文件给客户端
* @param {Context} ctx - Koa上下文对象
* @param {String} path - 文件路径
* @param {Object} [opts] - 配置选项
* @returns {Promise} - 异步Promise
*/
async function send(ctx, path, opts = {}) {
assert(ctx, 'koa context required');
assert(path, 'pathname required');
// 移除硬编码的public目录,要求必须通过opts.root配置
const root = opts.root;
if (!root) {
throw new Error('Static root directory must be configured via opts.root');
}
const trailingSlash = path[path.length - 1] === '/';
path = path.substr(parse(path).root.length);
const index = opts.index || 'index.html';
const maxage = opts.maxage || opts.maxAge || 0;
const immutable = opts.immutable || false;
const hidden = opts.hidden || false;
const format = opts.format !== false;
const extensions = Array.isArray(opts.extensions) ? opts.extensions : false;
const brotli = opts.brotli !== false;
const gzip = opts.gzip !== false;
const setHeaders = opts.setHeaders;
if (setHeaders && typeof setHeaders !== 'function') {
throw new TypeError('option setHeaders must be function');
}
// 解码路径
path = decode(path);
if (path === -1) return ctx.throw(400, 'failed to decode');
// 索引文件支持
if (index && trailingSlash) path += index;
path = resolvePath(root, path);
// 隐藏文件支持
if (!hidden && isHidden(root, path)) return;
let encodingExt = '';
// 尝试提供压缩文件
if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (await exists(path + '.br'))) {
path = path + '.br';
ctx.set('Content-Encoding', 'br');
ctx.res.removeHeader('Content-Length');
encodingExt = '.br';
} else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (await exists(path + '.gz'))) {
path = path + '.gz';
ctx.set('Content-Encoding', 'gzip');
ctx.res.removeHeader('Content-Length');
encodingExt = '.gz';
}
// 尝试添加文件扩展名
if (extensions && !/\./.exec(basename(path))) {
const list = [].concat(extensions);
for (let i = 0; i < list.length; i++) {
let ext = list[i];
if (typeof ext !== 'string') {
throw new TypeError('option extensions must be array of strings or false');
}
if (!/^\./.exec(ext)) ext = `.${ext}`;
if (await exists(`${path}${ext}`)) {
path = `${path}${ext}`;
break;
}
}
}
// 获取文件状态
let stats;
try {
stats = await stat(path);
// 处理目录
if (stats.isDirectory()) {
if (format && index) {
path += `/${index}`;
stats = await stat(path);
} else {
return;
}
}
} catch (err) {
const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
if (notfound.includes(err.code)) {
throw createError(404, err);
}
err.status = 500;
throw err;
}
if (setHeaders) setHeaders(ctx.res, path, stats);
// 设置响应头
ctx.set('Content-Length', stats.size);
if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString());
if (!ctx.response.get('Cache-Control')) {
const directives = [`max-age=${(maxage / 1000) | 0}`];
if (immutable) directives.push('immutable');
ctx.set('Cache-Control', directives.join(','));
}
if (!ctx.type) ctx.type = type(path, encodingExt);
ctx.body = fs.createReadStream(path);
return path;
}
/**
* 检查是否为隐藏文件
* @param {string} root - 根目录
* @param {string} path - 文件路径
* @returns {boolean} 是否为隐藏文件
*/
function isHidden(root, path) {
path = path.substr(root.length).split(sep);
for (let i = 0; i < path.length; i++) {
if (path[i][0] === '.') return true;
}
return false;
}
/**
* 获取文件类型
* @param {string} file - 文件路径
* @param {string} ext - 编码扩展名
* @returns {string} 文件MIME类型
*/
function type(file, ext) {
return ext !== '' ? extname(basename(file, ext)) : extname(file);
}
/**
* 解码URL路径
* @param {string} path - 需要解码的路径
* @returns {string|number} 解码后的路径或错误代码
*/
function decode(path) {
try {
return decodeURIComponent(path);
} catch (err) {
return -1;
}
}
export default send;

74
src/plugins/Send/resolve-path.js

@ -0,0 +1,74 @@
/*!
* resolve-path
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015-2018 Douglas Christopher Wilson
* MIT Licensed
*/
/**
* ES Module 转换版本
* 路径解析工具防止路径遍历攻击
*/
import createError from 'http-errors';
import { join, normalize, resolve, sep } from 'path';
import pathIsAbsolute from 'path-is-absolute';
/**
* 模块变量
* @private
*/
const UP_PATH_REGEXP = /(?:^|[\/])\.\.(?:[\/]|$)/;
/**
* 解析相对路径到根路径
* @param {string} rootPath - 根目录路径
* @param {string} relativePath - 相对路径
* @returns {string} 解析后的绝对路径
* @public
*/
function resolvePath(rootPath, relativePath) {
let path = relativePath;
let root = rootPath;
// root是可选的,类似于root.resolve
if (arguments.length === 1) {
path = rootPath;
root = process.cwd();
}
if (root == null) {
throw new TypeError('argument rootPath is required');
}
if (typeof root !== 'string') {
throw new TypeError('argument rootPath must be a string');
}
if (path == null) {
throw new TypeError('argument relativePath is required');
}
if (typeof path !== 'string') {
throw new TypeError('argument relativePath must be a string');
}
// 包含NULL字节是恶意的
if (path.indexOf('\0') !== -1) {
throw createError(400, 'Malicious Path');
}
// 路径绝不能是绝对路径
if (pathIsAbsolute.posix(path) || pathIsAbsolute.win32(path)) {
throw createError(400, 'Malicious Path');
}
// 路径超出根目录
if (UP_PATH_REGEXP.test(normalize('.' + sep + path))) {
throw createError(403);
}
// 拼接相对路径
return normalize(join(resolve(root), path));
}
export default resolvePath;

16
src/plugins/install.js

@ -1,5 +1,21 @@
import ResponseTime from "./ResponseTime";
import Send from "./Send/index.js";
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import path from "path"
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const publicPath = resolve(__dirname, '../../public');
export default (app)=>{
app.use(ResponseTime)
app.use(async (ctx, next) => {
try {
await Send(ctx, ctx.path, { root: publicPath });
} catch (err) {
if (err.status !== 404) throw err;
}
await next();
})
}

110
src/utils/router.js

@ -0,0 +1,110 @@
import { pathToRegexp } from 'path-to-regexp';
class Router {
/**
* 初始化路由实例
* @param {Object} options - 路由配置
* @param {string} options.prefix - 全局路由前缀
*/
constructor(options = {}) {
this.prefix = options.prefix || '';
this.routes = { get: [], post: [], put: [], delete: [] };
this.middlewares = [];
}
/**
* 注册中间件
* @param {Function} middleware - 中间件函数
*/
use(middleware) {
this.middlewares.push(middleware);
}
/**
* 注册GET路由
* @param {string} path - 路由路径
* @param {Function} handler - 处理函数
*/
get(path, handler) {
this._registerRoute('get', path, handler);
}
/**
* 注册POST路由
* @param {string} path - 路由路径
* @param {Function} handler - 处理函数
*/
post(path, handler) {
this._registerRoute('post', path, handler);
}
/**
* 创建路由组
* @param {string} prefix - 组内路由前缀
* @param {Function} callback - 组路由注册回调
*/
group(prefix, callback) {
const groupRouter = new Router({ prefix: this.prefix + prefix });
callback(groupRouter);
// 合并组路由到当前路由
Object.keys(groupRouter.routes).forEach(method => {
this.routes[method].push(...groupRouter.routes[method]);
});
this.middlewares.push(...groupRouter.middlewares);
}
/**
* 生成Koa中间件
* @returns {Function} Koa中间件函数
*/
middleware() {
return async (ctx, next) => {
// 执行全局中间件
for (const middleware of this.middlewares) {
await middleware(ctx, next);
}
const { method, path } = ctx;
const route = this._matchRoute(method.toLowerCase(), path);
if (route) {
ctx.params = route.params;
await route.handler(ctx, next);
} else {
await next();
}
};
}
/**
* 内部路由注册方法
* @private
*/
_registerRoute(method, path, handler) {
const fullPath = this.prefix + path;
const keys = [];
const regexp = pathToRegexp(fullPath, keys).regexp;
this.routes[method].push({ path: fullPath, regexp, keys, handler });
}
/**
* 匹配路由
* @private
*/
_matchRoute(method, currentPath) {
const routes = this.routes[method] || [];
for (const route of routes) {
const match = route.regexp.exec(currentPath);
if (match) {
const params = {};
for (let i = 1; i < match.length; i++) {
params[route.keys[i - 1].name] = match[i] || '';
}
return { ...route, params };
}
}
return null;
}
}
export default Router;
Loading…
Cancel
Save