loading...
搭建一个基于 TypeScript 的 Node.js 服务器需要按照以下步骤完成。以下是详细的搭建过程,包括项目初始化、工具配置和实现简单的服务器功能。
bash1mkdir ts-node-server 2cd ts-node-server 3npm init -y
pnpm初始项目
bash1 pnpm init
安装 TypeScript 和 Node.js 相关依赖:
bash1# 安装 TypeScript、ts-node 和必要工具 2npm install --save-dev typescript ts-node @types/node nodemon 3 4# 安装服务器框架(以 Express 为例) 5npm install express 6npm install --save-dev @types/express
pnpm安装:
bash1pnpm add --save-dev typescript ts-node @types/node nodemon 2pnpm add install express 3pnpm add install --save-dev @types/express
bash1npx tsc --init
tsconfig.json调整以下关键配置:
json1{ 2 "compilerOptions": { 3 "target": "ES2020", // 输出的 JavaScript 目标版本 4 "module": "CommonJS", // 使用 CommonJS 模块(适用于 Node.js 环境) 5 "strict": true, // 启用严格类型检查 6 "esModuleInterop": true, // 启用 ES 模块兼容性 7 "forceConsistentCasingInFileNames": true,// 强制文件名大小写一致 8 "skipLibCheck": true, // 跳过库的类型检查 9 "moduleResolution": "Node", // 模块解析策略 10 "outDir": "./dist", // 输出目录 11 "rootDir": "./", // 项目根目录(可以是 src/ 目录) 12 "baseUrl": ".", // 基础路径 13 "paths": { // 配置路径别名(可选) 14 "@types/*": ["types/*"], 15 "@db/*": ["db/*"], 16 "@middlewares/*": ["middlewares/*"], 17 "@router/*": ["router/*"], 18 "@router_handler/*": ["router_handler/*"], 19 "@schema/*": ["schema/*"], 20 "@utils/*": ["utils/*"] 21 }, 22 "resolveJsonModule": true, // 允许导入 JSON 模块 23 "typeRoots": ["./node_modules/@types"], // 指定类型声明文件的目录 24 "noImplicitAny": false // 允许隐式的 any 类型(可按需设置为 true) 25 }, 26 "include": [ 27 "db/**/*", 28 "middlewares/**/*", 29 "redis/**/*", 30 "router/**/*", 31 "router_handler/**/*", 32 "schema/**/*", 33 "uploads/**/*", 34 "utils/**/*", 35 "server.ts" // 主入口文件 36 ], 37 "exclude": [ 38 "node_modules", // 排除 node_modules 39 "mysql/mysql-data/**/*", // 排除 MySQL 数据文件 40 "dist/**/*" // 排除编译输出目录 41 ] 42} 43
plaintext1template-server/ 2┣ src/ // 所有源代码存放目录 3┃ ┣ db/ // 数据库相关逻辑或配置文件存放目录 4┃ ┃ ┣ mysql.ts // MySQL 连接池配置文件 5┃ ┃ ┗ query.ts // 数据查询的函数或SQL语句文件 6┃ ┣ files/ // 通用文件存放目录 7┃ ┃ ┗ notes/ // 存放项目文档或说明文件 8┃ ┣ middlewares/ // 中间件文件目录 9┃ ┃ ┣ authMiddleware.ts // 认证中间件 10┃ ┃ ┗ loggerMiddleware.ts // 日志记录中间件 11┃ ┣ mysql/ // MySQL 相关逻辑或配置 12┃ ┣ redis/ // Redis 相关逻辑或配置 13┃ ┣ router/ // 路由定义目录,包含所有 API 路由 14┃ ┃ ┣ userRouter.ts // 用户相关 API 路由 15┃ ┃ ┗ authRouter.ts // 认证相关 API 路由 16┃ ┣ router_handler/ // 路由处理器目录 17┃ ┃ ┣ userHandler.ts // 用户 API 的具体逻辑 18┃ ┃ ┗ authHandler.ts // 认证 API 的具体逻辑 19┃ ┣ schema/ // 数据验证规则存放目录 20┃ ┃ ┣ userSchema.ts // 用户相关的验证规则 21┃ ┃ ┗ authSchema.ts // 认证相关的验证规则 22┃ ┣ utils/ // 工具函数目录 23┃ ┃ ┗ logger.ts // 日志记录工具 24┃ ┗ server.ts // 项目的主入口文件,负责初始化服务器、加载中间件和路由 25┣ dist/ // 编译后的文件存放目录 26┣ public/ // 静态资源目录,前端资源 27┃ ┣ json/ // 静态 JSON 配置文件 28┣ uploads/ // 上传文件的存放目录 29┃ ┗ static/ // 上传文件的静态目录 30┃ ┗ images/ // 上传图片的存放目录 31┣ .env // 环境变量文件 32┣ LICENSE // 开源协议文件 33┣ README.md // 开发文档文件 34 35
typescript1import express, { Request, Response } from 'express'; 2 3const app = express(); 4const port = 3000; 5 6app.use(express.json()); 7 8// 测试路由 9app.get('/', (req: Request, res: Response) => { 10 res.send('Hello, TypeScript + Node.js!'); 11}); 12 13app.listen(port, () => { 14 console.log(`Server is running at http://localhost:${port}`); 15});
在 package.json 中添加:
json1"scripts": { 2 "start": "node dist/server.js", 3 "build": "tsc", 4 "dev": "nodemon --watch src --ext ts --exec ts-node src/server.ts" 5}
nodemon(自动重启工具)nodemon 是一个用于开发 Node.js 应用的工具,它会在检测到文件变化时自动重新启动应用程序,从而提高开发效率。与运行 node 命令相比,nodemon 可以省去手动停止和重启服务的繁琐操作,尤其在频繁修改代码的情况下非常有用。
bash1npm install --save-dev nodemon
bash1pnpm add --save-dev nodemon 2
rimraf 删除文件rimraf 是一个非常常用的工具,用于删除文件夹和文件。你可以在构建脚本中使用它来删除 dist 目录中的所有内容(包括子目录和文件)。
bash1npm install rimraf --save-dev
bash1pnpm add rimraf --save-dev
bash1npm run dev
pnpm:
bash1pnpm run dev
bash1npm run build 2npm start
bash1pnpm run build 2pnpm start
使用 dotenv 管理环境变量:
bash1npm install dotenv 2npm install --save-dev @types/dotenv
pnpm add dotenv
pnpm add --save-dev @types/dotenv
在 src/server.ts 中添加:
typescript1import dotenv from 'dotenv'; 2 3dotenv.config(); 4 5const port = process.env.PORT || 3000;
在根目录创建 .env 文件:
env1PORT=4000
使用 winston 或 morgan 进行日志管理:
bash1npm install winston
bash1pnpm add winston
首先,你需要安装 ESLint 和Prettier适用于 TypeScript 的插件和解析器。通过以下命令安装这些依赖:
bash1npm install eslint @eslint/js eslint-config-prettier eslint-plugin-prettier typescript-eslint globals --save-dev 2
pnpm :
bash1pnpm add eslint @eslint/js eslint-config-prettier eslint-plugin-prettier typescript-eslint globals --save-dev
js1// eslint.config.js 2const js = require("@eslint/js"); 3const prettierConfig = require("eslint-config-prettier"); 4const prettierPlugin = require("eslint-plugin-prettier"); 5 6const globals = require("globals"); 7const tseslint = require("typescript-eslint"); 8 9module.exports = tseslint.config( 10 { ignores: ["dist", "node_modules"] }, // 忽略 dist 目录 11 { 12 extends: [ 13 js.configs.recommended, // 推荐的 JavaScript 规则 14 ...tseslint.configs.recommendedTypeChecked, // TypeScript 推荐规则 15 prettierConfig, // 使用 prettier 配置 16 ], 17 files: ["**/*.ts"], // 只检查 TypeScript 文件 18 languageOptions: { 19 ecmaVersion: 2020, // 使用 ECMAScript 2020 版本 20 globals: globals.node, // 使用 Node.js 的全局变量 21 parserOptions: { 22 project: ["./tsconfig.json"], // TypeScript 配置文件 23 tsconfigRootDir: __dirname, // 使用 CommonJS 中的 __dirname 24 }, 25 }, 26 plugins: { 27 prettier: prettierPlugin, // 使用 prettier 插件来解决与 prettier 的冲突 28 }, 29 rules: { 30 "prettier/prettier": ["warn"], // 启用 prettier 配置 31 "@typescript-eslint/no-empty-object-type": "off", // 允许对象为空 32 "prettier/prettier": ["off", { "endOfLine": "auto" }], // 解决行尾符问题 33 }, 34 } 35);
plaintext1// .prettierrc 2{ 3 "semi": true, 4 "singleQuote": false, 5 "trailingComma": "all", 6 "printWidth": 80, 7 "tabWidth": 2, 8 "endOfLine": "lf" 9} 10
配置脚本:
json1{ 2 ... 3 "scripts": { 4 "lint": "eslint .", 5 "lint:fix": "eslint . --fix", 6 ... 7 }, 8 ... 9
安装依赖:
bash1pnpm add cors 2pnpm add -D @types/cors
server.ts:
ts1import cors ,{ CorsOptions } from 'cors'; 2 3const corsOptions: CorsOptions = { 4 origin: (origin, callback) => { 5 6 if (typeof origin === 'string' && config.cors.allowOrigin.includes(origin)) { 7 // 允许特定的来源 8 callback(null, true); 9 } else { 10 // 拒绝其他来源 11 callback(new Error('Not allowed by CORS')); 12 } 13 }, 14 methods: ['GET', 'POST', 'PUT', 'DELETE'], 15 allowedHeaders: ['Content-Type', 'Authorization'], 16 credentials: true, // 允许携带 cookies 17 maxAge: 86400, // 预检请求有效期 24 小时 18}; 19app.use(cors(corsOptions)); 20
pnpm :
bash1pnpm add module-alias -D
修改package.json文件 添加配置:
json1{ 2 ... 3 "_moduleAliases": { 4 "@server": "server", 5 } 6}
json1{ 2 ... 3 "baseUrl": ".", 4 "paths": { 5 "@server/*": ["server/*"] 6 }, 7}
ts1// 应用程序入口 2import 'module-alias/register';
JWT验证主要使用express-jwt库
bash1pnpm add express-jwt
应用:
ts1app.use( 2 expressjwt({ secret: jwtConfig.secret, algorithms: ["HS256"] }).unless({ 3 path: jwtConfig.interfacesExclude 4 }) 5); 6 7# 加密 加密后放 8const token_str = jwt.sign({ ...user }, config.jwtSecretKey, { expiresIn: config.expiresIn }); 9res.sendResponse("成功获取了用户信息", { userInfo, token:"Bearer "+token_str })
ad1title:注意 2JWT包含三个部分,header,payload,signature,这三部分本身都是不加密的,只是用了签名算法去验证,如果要用JWT传输数据进行前后端的数据交换,并负载的数据对于安全性的要求比较高。就需要对数据进行加密,那么这个时候可以用jose
对于内置的JSON.stringify对与循环引用和Symbol类型处理并不好,所以可以用第三方库: flatted。
这个问题看似简单,但是实则还是很复杂的,因为这个是在typescript中,而express本身的res和req要去扩展属性和方法需要修改扩展全局的类型定义,参考: How to add extension function to express response in typescript? - Stack Overflow [typescript - Express req.user is optional even behind an authenticated middleware - Stack node.js - How to add Typescript definitions to Express req & res - Stack Overflow
ts1import { SendErrorOptions, SendResponseOptions } from "@server/middlewares/customresponse/customResponse"; 2 3 4global { 5 namespace Express { 6 interface Response< 7 ResBody = unknown, 8 Locals extends Record<string, unknown> = Record<string, unknown>, 9 StatusCode extends number = number 10 > { 11 sendResponse: (options: SendResponseOptions) => Response<unknown, Record<string, unknown>, number> 12 sendError: (options: SendErrorOptions) => Response<unknown, Record<string, unknown>, number> 13 } 14 } 15}
ts1import { NextFunction, Request, Response } from 'express'; 2import { v4 as uuidv4 } from 'uuid'; 3import statusCodes from './httpStatusCodes.json'; 4 5 6 7export function responseMiddleware(req: Request, res: Response, next: NextFunction) { 8 res.sendResponse = ({ 9 message = '请求成功', 10 data = null, 11 code = "200", 12 }: { message?: string; data?: unknown; code?: string }): Response<unknown, Record<string, unknown>> => { 13 const timestamp = new Date().toISOString(); 14 const requestId = uuidv4(); 15 const success = code === "200"; 16 let detail = null; 17 if (!success) { 18 console.log('statusCodes', statusCodes); 19 message = statusCodes[code]?.message || '请求出现异常'; 20 detail = statusCodes[code]?.detail || '无法处理请求'; 21 } 22 23 24 return res.send({ 25 success, 26 code: +code, 27 message, 28 data, 29 timestamp, 30 requestId, 31 detail, 32 }); 33 }; 34 35 res.sendError = ({ 36 code = "400", 37 detail = '请求出现异常', 38 errorMessage = '请求出错', 39 }: { code?: string; detail?: string; errorMessage?: string }): Response<unknown, Record<string, unknown>> => { 40 41 42 return res.send({ 43 code: +code, 44 detail, 45 errorMessage, 46 }); 47 }; 48 49 next(); 50};
一个服务器难免有很多静态的资源需要提供给用户直接访问,所以 需要有一个静态资源的托管目录用来用指定的路径可以访问到相关的文件给用户。这个目录下的文件,只要不是跨域的请求都是可以直接访问到的。
ts1/* 托管静态资源 */ 2app.use("/public", express.static(path.join(__dirname, "public"), { 3 maxAge: "30 minutes", 4 cacheControl: true, 5 immutable: true 6})) 7
ts1import { NextFunction, Request, Response } from 'express'; 2import logger from './logger'; 3 4// 类型定义 5type RouterHandler = (req: Request, res: Response, next: NextFunction) => void; 6type Middleware = (req: Request, res: Response, next: NextFunction) => void; 7 8type CreateRouterHandler = ( 9 handler: RouterHandler, 10 options?: { 11 middlewares?: Middleware[]; // 预处理函数数组 12 postMiddlewares?: Middleware[]; // 后置处理函数数组 13 } 14) => RouterHandler; 15 16// 函数实现 17const createRouterHandler: CreateRouterHandler = (handler, options) => { 18 const { middlewares = [], postMiddlewares = [] } = options || {}; 19 20 return async (req, res, next) => { 21 try { 22 // 执行预处理函数 23 for (const middleware of middlewares) { 24 await new Promise<void>((resolve, reject) => { 25 middleware(req, res, (err) => (err ? reject(err as Error) : resolve())); 26 }); 27 } 28 console.log(`\n\x1b[38;2;255;165;0m请求方法:${req.method}=> 请求路径:${req.url}\x1b[0m\n`); 29 30 31 // 执行业务逻辑 32 await new Promise<void>((resolve, reject) => { 33 handler(req, res, (err) => (err ? reject(err as Error) : resolve())); 34 }); 35 36 // 执行后置处理函数 37 for (const postMiddleware of postMiddlewares) { 38 await new Promise<void>((resolve, reject) => { 39 postMiddleware(req, res, (err) => (err ? reject(err as Error) : resolve())); 40 }); 41 } 42 } catch (error) { 43 // 捕获错误并传递给 Express 错误处理中间件 44 if (error instanceof Error) { 45 logger.error({ 46 message: error.message, 47 stack: error.stack, 48 client: { 49 userAgent: req.headers["user-agent"], 50 ip: req.ip 51 } 52 }) 53 return 54 } 55 next(error); 56 } 57 }; 58}; 59 60export default createRouterHandler
ts1 pnpm add express-mysql-session express-session
ts1import pool from "./db/mysql"; 2import mysqlSession from 'express-mysql-session'; 3import session from 'express-session'; 4/* 配置MySQL-Session仓库 */ 5/* eslint-disable @typescript-eslint/no-unsafe-argument */ 6// @ts-expect-error 导入问题 7const mysqlSessionStore = mysqlSession(session as Session); 8 9// @ts-expect-error 实际上是连接池 10const sessionStore = new mysqlSessionStore({}, pool) 11app.use(session({ 12 secret: "huchenghe1021", 13 store: sessionStore, 14 resave: false, 15 saveUninitialized: false, // 表示未初始化的 session 不会被保存 16 cookie: process.env.NODE_ENV === 'production' ? { 17 // secure: process.env.NODE_ENV === 'production', // 仅在 HTTPS 环境下设置为 true 根据具体情况启动,你说如果没有那一定得关闭 18 secure: false, // 仅在 HTTPS 环境下设置为 true 19 sameSite: 'lax', 20 // sameSite: 'none', // 允许跨站点传递 21 domain: '.huchenghe.site', // 主域和子域共享 22 maxAge: 1000 * 60 * 5 // 五分钟验证码 23 } : { 24 secure: false, // 仅在 HTTPS 环境下设置为 true 25 sameSite: 'lax', 26 maxAge: 1000 * 60 * 5 // 五分钟验证码 27 } 28})) 29
对于一些想密码的关键数据需要对其进行加密,使用encrypt加密可以做到不可逆的加密。在验证的时候通过相同的字符加密后再与保存的相互验证,可以做到密码的加密,这样安全性也很高。除非用户主动泄露,否则很难去做到对其进行破解数据库的中的密码数据。另外如果处于安全性的考虑,其实还可以在前端直接通过encrypt进行密码的加密后发起请求传输。这样后端只要在注册的时候去加密密码,而后续登录验证密码可以直接与前端传递过来的字符串进行比较,那样黑客即便知道了客户的加密的密码,也不能知道其明文密码。这样对用户的隐私安全性有很大的保护。
ts1import { createError } from '@server/utils/createError'; 2import crypto from 'crypto'; 3 4// 生成随机的 256 位密钥和初始化向量(IV) 5export const encryptionKey = crypto.randomBytes(32); // 256-bit key 6const iv = crypto.randomBytes(16); // 128-bit IV 7 8 9 10/* 方案:对关键的数据进行加密 对不关键的数据正常签名就好*/ 11 12// 加密函数 13export function encrypt(text: string): string { 14 try { 15 const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv); 16 let encrypted = cipher.update(text, 'utf8', 'base64'); 17 encrypted += cipher.final('base64'); 18 return encrypted; 19 } catch (error) { 20 throw createError("EncryptionFailed", "It cannot be encrypted") 21 } 22 23} 24 25 26// 解密函数 27export function decrypt(encryptedText: string,): string { 28 try { 29 const decipher = crypto.createDecipheriv('aes-256-cbc', encryptionKey, iv); 30 let decrypted = decipher.update(encryptedText, 'base64', 'utf8'); 31 decrypted += decipher.final('utf8'); 32 return decrypted; 33 } catch (error) { 34 throw createError("DecryptionFailed", "Encrypted data could not be parsed") 35 } 36 37} 38
ts1import bcrypt from 'bcrypt'; 2 3// 加密关键数据 4// 第二个参数 saltRounds 加密的精度 越高越精确 消耗性能 慢 5const passwordBcrypted = bcrypt.hashSync(password as string, 10); 6 7// 解密 实际上是比较再次加密后是否相同 8const isLogin = bcrypt.compareSync(password as string, dbpassword as string)
对于后端来说,对于每个请求都需要判断其字段是否相符,如果相关字段不符合标准需要驳回,那么就需要使用数据验证的库。主要是用joi库,而前端可以配合使用yup库和zod的库,对于相关性来说yup库和joi库相关性要高一点。
bash1pnpm add joi
ts1import { Schema } from "joi"; 2import { Request, Response, NextFunction } from "express"; 3 4type ValidationTarget = "body" | "query" | "params"; 5 6export const validateRequest = (schema: Schema, target: ValidationTarget = "body") => { 7 return (req: Request, res: Response, next: NextFunction) => { 8 const dataToValidate = req[target]; // 根据目标选择验证的数据 9 const { error } = schema.validate(dataToValidate, { abortEarly: false, stripUnknown: true }); 10 11 if (error) { 12 const errorMessages = error.details.map((detail) => detail.message); 13 return res.sendError({ 14 code: "400", 15 errorMessage: errorMessages.join("; "), // 合并多个错误信息 16 }); 17 } 18 19 next(); // 验证通过,继续执行后续逻辑 20 }; 21}; 22 23
ts1authRouter.post("/register", validateRequest(register_login_schema), registerHandler)
multipart/form-data Node.js 中间件,主要用于上传文件。它写在Busboy之上,以实现最高效率。bash1pnpm add multer 2pnpm add @types/multer -D
安装fs-extra: 一个优秀的操作文件的库
bash1 pnpm add fs-extra 2 pnpm add @types/fs-extra
bash1pnpm add iconv-lite
bash1 pnpm add spark-md5
Dockerfile 并构建镜像。通过以上步骤,你可以搭建并运行一个简单的 TypeScript Node.js 服务器,后续可以根据项目需求添加数据库连接、路由管理和其他功能模块。
加载中...