From 7acdfc2c6806860767ad9d408fc23999c3a6f192 Mon Sep 17 00:00:00 2001 From: tone <3341154833@qq.com> Date: Wed, 25 Sep 2024 01:21:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9F=BA=E7=A1=80=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=EF=BC=8C=E5=AF=B9=E5=8E=9F=E6=9C=89APILoader=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E6=8E=A5=E5=8F=A3=E5=93=8D=E5=BA=94=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 24 +++++ src/api/GetTest.ts | 17 +++ src/config.ts | 9 ++ src/index.ts | 18 ++++ src/lib/API/API.ts | 28 +++++ src/lib/API/APILoader.ts | 72 +++++++++++++ src/lib/APIMiddleware/MountIP.ts | 27 +++++ src/lib/APIMiddleware/MountUserAgent.ts | 19 ++++ src/lib/APIMiddleware/Unbind.ts | 9 ++ src/lib/Logger/Logger.ts | 46 ++++++++ src/lib/ServerResponse/ServerStdResponse.ts | 18 ++++ tsconfig.json | 111 ++++++++++++++++++++ 12 files changed, 398 insertions(+) create mode 100644 package.json create mode 100644 src/api/GetTest.ts create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/lib/API/API.ts create mode 100644 src/lib/API/APILoader.ts create mode 100644 src/lib/APIMiddleware/MountIP.ts create mode 100644 src/lib/APIMiddleware/MountUserAgent.ts create mode 100644 src/lib/APIMiddleware/Unbind.ts create mode 100644 src/lib/Logger/Logger.ts create mode 100644 src/lib/ServerResponse/ServerStdResponse.ts create mode 100644 tsconfig.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..816c05b --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "nodeserver", + "version": "1.0.0", + "description": "由typescript编写的node.js通用后端服务框架", + "main": "index.js", + "scripts": { + "start": "ts-node -r tsconfig-paths/register src/index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "tone", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^22.6.1", + "cors": "^2.8.5", + "express": "^4.21.0", + "typescript": "^5.6.2" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0" + } +} diff --git a/src/api/GetTest.ts b/src/api/GetTest.ts new file mode 100644 index 0000000..443dc89 --- /dev/null +++ b/src/api/GetTest.ts @@ -0,0 +1,17 @@ +import { API } from "@lib/API/API"; +import MountIP, { MountIPRequestData } from "@lib/APIMiddleware/MountIP"; +import MountUserAgent, { MountUserAgentRequestDate } from "@lib/APIMiddleware/MountUserAgent"; +import Unbind from "@lib/APIMiddleware/Unbind"; +import ServerStdResponse from "@lib/ServerResponse/ServerStdResponse"; +import { Response } from "express"; +class GetTest extends API { + constructor() { + super('GET', '/test', MountIP, MountUserAgent, Unbind); + } + + public async onRequset(data: MountIPRequestData | MountUserAgentRequestDate, res: Response): Promise { + this.logger.info(`request ip: ${data._ip} useragent: ${data._userAgent}`) + res.json(ServerStdResponse.OK); + } +} +export default GetTest; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..5785a4c --- /dev/null +++ b/src/config.ts @@ -0,0 +1,9 @@ +const config = { + cors: { + origin: ['http://localhost:5173'], + allowedHeaders: ['Content-Type'], + methods: ['GET', 'POST'] + }, + API_Port: 8080 +}; +export default config; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f28433f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,18 @@ +import { APILoader } from "@lib/API/APILoader"; +import Logger from '@lib/Logger/Logger' +import config from "./config"; +import GetTest from "./api/GetTest"; +const logger = new Logger('Server') +async function main(): Promise { + logger.info('Starting...'); + const apiLoader = new APILoader(config.cors); + // loadAPI + apiLoader.add(GetTest); + + await apiLoader.start(config.API_Port); + logger.info('Server started successfully') +} + +main().catch((reason) => { + logger.error(`An error occurs in the main function: ${reason}`) +}) \ No newline at end of file diff --git a/src/lib/API/API.ts b/src/lib/API/API.ts new file mode 100644 index 0000000..312d9b6 --- /dev/null +++ b/src/lib/API/API.ts @@ -0,0 +1,28 @@ +import { Request, Response, NextFunction } from "express"; +import Logger from "@lib/Logger/Logger"; + +interface MiddlewareFunction { + (req: Request, res: Response, next: NextFunction): void; +} + +abstract class API { + + protected logger: Logger; + public middlewareFunc: Function[] = []; + + /** + * @param method API Method + * @param path API Path + * @param func API MiddlewareFunctions + */ + constructor(public method: string, public path: string, ...func: MiddlewareFunction[]) { + this.logger = new Logger('API][' + method + '][' + path); + this.middlewareFunc.push(...func); + } + + // to override + public abstract onRequset(data: any, res: Response): Promise; +} + +export { API }; +export type { MiddlewareFunction }; \ No newline at end of file diff --git a/src/lib/API/APILoader.ts b/src/lib/API/APILoader.ts new file mode 100644 index 0000000..5c372f6 --- /dev/null +++ b/src/lib/API/APILoader.ts @@ -0,0 +1,72 @@ +import express, { NextFunction, Request, Response } from "express"; +import cors, { CorsOptions } from "cors"; +import Logger from "@lib/Logger/Logger"; +import { API } from "./API"; +import ServerStdResponse from "@lib/ServerResponse/ServerStdResponse"; +class APILoader { + private app = express(); + private logger = new Logger('APILoader'); + constructor(corsOptions?: CorsOptions) { + this.logger.info('API service is loading...'); + this.app.use(express.json({ limit: '10mb' })); + this.app.use(express.urlencoded({ extended: true })); + this.app.use(cors(corsOptions)); + if (corsOptions) + this.logger.info('Cors configuration is complete'); + else + this.logger.warn('Cors is not configured'); + } + + add(api: { new(): API }) { + const instance = new api(); + // register middleware + for (let func of instance.middlewareFunc) { + this.app[instance.method.toLowerCase() as keyof express.Application](instance.path, async (req: Request, res: Response, next: NextFunction) => { + try { + await func(req, res, next); + } catch (error) { + this.logger.error(`[${instance.method}][${instance.path}][API Middleware Function: ${func.name}]: ${error}`); + } + }); + this.logger.info(`[${instance.method}][${instance.path}][API Middleware Function: ${func.name}] is enabled`); + } + + // handle request + this.app[instance.method.toLowerCase() as keyof express.Application](instance.path, async (req: Request, res: Response) => { + let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip; + this.logger.info(`[Request][${instance.method}][${instance.path}] request by ${(ip as string).replace('::ffff:', '')}`); + const data = Object.assign({}, req.query, req.body); + try { + await instance.onRequset(data, res); + } catch (error) { + this.logger.error(`[${instance.method}][${instance.path}] ${error}`); + } + }); + this.logger.info(`[${instance.method}][${instance.path}] loaded`); + } + + async start(port: number) { + return new Promise((resolve) => { + // handle undefined API + this.app.use((req: Request, res: Response) => { + this.logger.warn(`[Request][${req.method}][${req.url.split('?')[0]}] undefined API`); + res.json(ServerStdResponse.API.NOT_FOUND) + }) + + // listen port + const server = this.app.listen(port, () => { + this.logger.info(`The API service is listening on port ${port}`); + resolve(undefined); + }); + server.on('error', (error: NodeJS.ErrnoException) => { + this.logger.error(`${error.message}`); + this.logger.error(`process killed`); + process.exit(1); + }); + }) + } +} + +export { + APILoader, +} \ No newline at end of file diff --git a/src/lib/APIMiddleware/MountIP.ts b/src/lib/APIMiddleware/MountIP.ts new file mode 100644 index 0000000..f4c185e --- /dev/null +++ b/src/lib/APIMiddleware/MountIP.ts @@ -0,0 +1,27 @@ +import { Request, Response, NextFunction } from "express" +import Logger from "@lib/Logger/Logger"; +const logger = new Logger('API', 'Middleware', 'MountIP'); + +let MountIP = (req: Request, res: Response, next: NextFunction) => { + let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip; + if (ip == undefined || ip.length <= 0) { + logger.warn(`[${req.method}][${req.url.split('?')[0]}] ip resolution was fail`); + } else { + if (typeof ip === 'object') + req.body._ip = ip.join(' ').replace('::ffff:', ''); + else + req.body._ip = ip.replace('::ffff:', ''); + logger.info(`[${req.method}][${req.url.split('?')[0]}] ip resolution was successful: ${req.body._ip}`); + } + next(); +} + +export default MountIP; + + +interface MountIPRequestData { + _ip: string, + [key: string | number | symbol]: any +} + +export type { MountIPRequestData }; \ No newline at end of file diff --git a/src/lib/APIMiddleware/MountUserAgent.ts b/src/lib/APIMiddleware/MountUserAgent.ts new file mode 100644 index 0000000..5262716 --- /dev/null +++ b/src/lib/APIMiddleware/MountUserAgent.ts @@ -0,0 +1,19 @@ +import { Request, Response, NextFunction } from "express" +import Logger from "@lib/Logger/Logger"; +const logger = new Logger('API', 'Middleware', 'MountUserAgent') + +let MountUserAgent = (req: Request, res: Response, next: NextFunction) => { + req.body._userAgent = req.headers['user-agent']; + logger.info(`[${req.method}][${req.url.split('?')[0]}] User agent parsed successfully: ${req.body._userAgent}`); + next(); +} + +export default MountUserAgent; + + +interface MountUserAgentRequestDate { + _userAgent: string, + [key: string | number | symbol]: any +} + +export type { MountUserAgentRequestDate }; \ No newline at end of file diff --git a/src/lib/APIMiddleware/Unbind.ts b/src/lib/APIMiddleware/Unbind.ts new file mode 100644 index 0000000..7427071 --- /dev/null +++ b/src/lib/APIMiddleware/Unbind.ts @@ -0,0 +1,9 @@ +import { Request, Response, NextFunction } from "express"; +import Logger from "@lib/Logger/Logger"; +const logger = new Logger('API', 'Middleware', "Unbind"); +const Unbind = (req: Request, res: Response, next: NextFunction) => { + let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || req.ip; + logger.warn(`API[${req.method}][${req.url.split('?')[0]}] requested an unbound endpoint [${ip}]`); +} + +export default Unbind; \ No newline at end of file diff --git a/src/lib/Logger/Logger.ts b/src/lib/Logger/Logger.ts new file mode 100644 index 0000000..37588c7 --- /dev/null +++ b/src/lib/Logger/Logger.ts @@ -0,0 +1,46 @@ +class Logger { + private fullNamespace: string; + constructor(primaryNamespace: string, ...additionalNamespaces: string[]) { + if (additionalNamespaces.length < 1) { + this.fullNamespace = primaryNamespace; + } else { + this.fullNamespace = [primaryNamespace, ...additionalNamespaces].join(']['); + } + } + + private getTime(): string { + return new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); + } + + public info(info: string, ...args: any): void { + args = args.map((arg: any) => { + if (typeof arg === 'object') { + return JSON.stringify(arg); + } + return arg; + }) + console.log(`\x1b[32m[${this.getTime()}][INFO][${this.fullNamespace}]${info[0] == '[' ? '' : ' '}${info} ` + args.join(' ') + '\x1b[0m'); + } + + public warn(info: string, ...args: any): void { + args = args.map((arg: any) => { + if (typeof arg === 'object') { + return JSON.stringify(arg); + } + return arg; + }) + console.log(`\x1b[33m[${this.getTime()}][WARN][${this.fullNamespace}]${info[0] == '[' ? '' : ' '}${info} ` + args.join(' ') + '\x1b[0m'); + } + + public error(info: string, ...args: any): void { + args = args.map((arg: any) => { + if (typeof arg === 'object') { + return JSON.stringify(arg); + } + return arg; + }) + console.log(`\x1b[31m[${this.getTime()}][ERROR][${this.fullNamespace}]${info[0] == '[' ? '' : ' '}${info} ` + args.join(' ') + '\x1b[0m'); + } +} + +export default Logger; \ No newline at end of file diff --git a/src/lib/ServerResponse/ServerStdResponse.ts b/src/lib/ServerResponse/ServerStdResponse.ts new file mode 100644 index 0000000..c516a0b --- /dev/null +++ b/src/lib/ServerResponse/ServerStdResponse.ts @@ -0,0 +1,18 @@ +const ServerStdResponse = { + OK: { + code: 0, + message: 'ok' + }, + ERROR: { + code: -1, + message: 'error' + }, + API: { + NOT_FOUND: { + code: -1001, + message: 'api not found' + } + } +} as const; + +export default ServerStdResponse; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d55fc95 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "@lib/*": ["src/lib/*"] + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}