Browse Source
- 删除旧的userController并重构用户表结构 - 添加静态文件服务中间件和路径解析工具 - 实现自定义路由系统支持路由组和参数 - 更新package.json依赖和模块别名配置 - 优化响应时间中间件添加彩色日志输出 - 添加jsconfig.json改善开发体验alpha
14 changed files with 501 additions and 63 deletions
Binary file not shown.
@ -0,0 +1,23 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"baseUrl": ".", |
|||
"paths": { |
|||
"@/*": [ |
|||
"src/*" |
|||
], |
|||
"db/*": [ |
|||
"src/db/*" |
|||
], |
|||
"utils/*": [ |
|||
"src/utils/*" |
|||
] |
|||
}, |
|||
"module": "commonjs", |
|||
"target": "es6", |
|||
"allowSyntheticDefaultImports": true |
|||
}, |
|||
"include": [ |
|||
"src/**/*", |
|||
"jsconfig.json" |
|||
] |
|||
} |
@ -0,0 +1 @@ |
|||
sada |
@ -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() |
|||
} |
@ -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; |
|||
|
@ -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); |
|||
} |
|||
|
@ -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; |
@ -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; |
@ -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(); |
|||
}) |
|||
} |
@ -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…
Reference in new issue