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