Express + TypeScript + ESM 后端服务搭建教程
基于 tdt-tile-server 项目实战, 从零搭建企业级 Express 后端服务。
目录
一、项目初始化
1.1 环境要求
- Node.js >= 18.x (推荐 20.x LTS)
- npm >= 9.x
1.2 创建项目
bash
mkdir tdt-tile-server && cd tdt-tile-server
npm init -y
1.3 package.json 配置
关键配置 "type": "module" 启用 ES Module 支持:
json
{
"name": "tdt-tile-server",
"version": "1.0.1",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "cross-env NODE_ENV=development nodemon -q -L -c nodemon.json"
}
}
"type": "module"使 Node.js 将.js文件解析为 ES Module,支持import/export语法。
1.4 安装核心依赖
bash
# 生产依赖
npm install express axios cors dotenv helmet morgan winston zod
# 开发依赖
npm install -D typescript tsx nodemon cross-env
npm install -D @types/express @types/node @types/cors @types/morgan
依赖说明:
| 包名 | 用途 |
|---|---|
| express | Web 框架 (v5) |
| typescript | 类型系统 + 编译器 |
| tsx | 直接执行 .ts 文件 (开发用) |
| nodemon | 文件变更自动重启 |
| cross-env | 跨平台环境变量设置 |
| winston | 结构化日志 |
| helmet | 安全 HTTP 头 |
| cors | 跨域资源共享 |
| dotenv | 环境变量加载 |
| zod | 运行时数据校验 |
1.5 TypeScript 配置
tsconfig.json:
json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false, // 如果改为true,语法检查比较严格
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"isolatedModules": true,
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"declaration": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
关键配置解读:
| 选项 | 说明 |
|---|---|
"module": "ESNext" |
输出 ES Module 格式, 与 "type": "module" 配套 |
"moduleResolution": "bundler" |
解析策略: 允许省略 .js 后缀, 兼容 package.json exports |
"isolatedModules": true |
每个文件独立编译, 强制 import type 语法 |
"outDir": "./dist" |
编译输出目录 |
"rootDir": "./src" |
源码根目录 |
"resolveJsonModule": true |
允许 import JSON 文件 |
注意 : ESM 模式下, 导入本地模块必须写
.js后缀:import logger from './utils/logger.js'。
1.6 多环境变量策略
项目使用三层环境变量文件, 后加载的覆盖前加载的:
bash
.env # 基础默认值
.env.dev # 开发环境 (NODE_ENV=development)
.env.prod # 生产环境 (NODE_ENV=production)
.env (默认值):
env
NODE_ENV=development
PORT=8080
PROJECT_NAME=tdt-tile-server
.env.dev (开发覆盖):
env
NODE_ENV=development
PORT=8080
ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5500
.env.prod (生产覆盖):
env
NODE_ENV=production
PORT=18000
在 app.ts 中加载:
typescript
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
const nodeEnv = process.env.NODE_ENV || 'development';
const envFile = nodeEnv === 'production' ? '.env.prod' : '.env.dev';
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
二、目录架构与分层设计
src/
├── server.ts # 入口: 端口监听 + 优雅关闭
├── app.ts # Express 实例: 中间件 + 路由挂载
├── config/
│ ├── env.ts # 环境变量加载逻辑
│ └── tileConfig.ts # 业务配置 (瓦片服务 URL、token、缓存)
├── controller/
│ └── tdtController.ts # 请求/响应处理, 参数校验
├── service/
│ └── tdtService.ts # 业务逻辑: 瓦片下载/缓存/转换
├── routes/
│ ├── index.ts # 路由聚合 (path -> router 映射)
│ └── tdt.ts # /tdtProxy 路由定义
├── middleware/
│ ├── cors.ts # CORS 多环境策略
│ └── errorHandler.ts # 全局错误处理
├── utils/
│ ├── logger.ts # Winston 日志 (控制台 + 文件分级)
│ ├── response.ts # 统一 API 响应格式
│ ├── envParser.ts # 环境变量解析工具
│ └── unit.ts # 通用工具函数
└── lib/
└── vector_tile_tdt.ts # TDT 瓦片下载库
分层原则:
| 层 | 职责 | 禁止 |
|---|---|---|
| routes | 定义 URL 路径、HTTP 方法 | 不含业务逻辑 |
| controller | 参数提取、调用 service、格式化响应 | 不含业务算法 |
| service | 业务逻辑编排、数据获取 | 不操作 req/res |
| middleware | 请求预处理/拦截 | 不含业务逻辑 |
| lib | 纯算法/数据转换 | 不依赖 Express |
| utils | 横切工具 (日志、响应格式) | 不含业务 |
2.1 入口文件: server.ts
typescript
import app from './app.js';
import logger from './utils/logger.js';
const PORT = process.env.PORT || 8080;
async function startServe() {
const server = app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
// 优雅关闭
const shutdown = async (signal: string) => {
logger.info(`收到 ${signal} 信号, 开始关闭...`);
server.close(() => logger.info('HTTP 服务器已关闭'));
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
}
startServe();
2.2 Express 实例: app.ts
typescript
const app: Application = express();
// 中间件注册顺序很重要
app.use(
helmet({
/* CSP 配置 */
}),
);
app.use(corsMiddleware); // CORS
app.use(express.json({ limit: '10mb' }));
app.use(morganMiddleware); // HTTP 请求日志
// 路由挂载
routes.map((route) => {
app.use('/api' + route.path, route.router);
});
// 404 处理 (所有路由之后)
app.use((req, res) => ResponseUtil.notFound(res));
// 500 处理 (最后)
app.use(errorHandler);
中间件顺序规则: 安全 → CORS → 解析 → 日志 → 路由 → 404 → 500。
2.3 路由聚合: routes/index.ts
typescript
import tdtRoutes from './tdt.js';
const routes = [{ path: '/tdtProxy', router: tdtRoutes }];
export default routes;
2.4 具体路由: routes/tdt.ts
typescript
const router = Router();
router.get('/getTile/:z/:x/:y', authenticate, TDTController.getTile);
export default router;
最终生成路径: /api/tdtProxy/getTile/:z/:x/:y。
2.5 Controller 层
typescript
export class TDTController {
async getTile(req: Request, res: Response) {
const { x, y, z } = req.params;
const type = req.query.type || 'png';
const result = await tdtService.getTile(
Number(x),
Number(y),
Number(z),
type as string,
);
res.set('Content-Type', 'image/png');
res.send(result);
}
}
2.6 Service 层
typescript
class TDTService {
async getTile(x: number, y: number, z: number, type: string) {
// 1. 检查缓存
const cachePath = getCachePath(x, y, z);
// 2. 缓存未命中 → 下载
const tdtBuffer = await downloadTile(x, y, z);
// 3. 写入缓存
writeFileSync(cachePath, tdtBuffer);
// 4. 返回瓦片
return this.parseTile(tdtBuffer);
}
}
三、核心配置详解
3.1 nodemon.json --- 开发热重载
json
{
"watch": ["src"],
"ext": "ts",
"exec": "tsx src/server.ts"
}
| 字段 | 说明 |
|---|---|
watch |
监控 src/ 目录变更 |
ext |
只监控 .ts 文件 |
exec |
用 tsx 直接执行 TypeScript, 无需预编译 |
联合 npm run dev 使用:
bash
cross-env NODE_ENV=development nodemon -q -L -c nodemon.json
为什么用 tsx 而不是 ts-node? tsx 基于 esbuild, 启动速度极快, 对 ESM 支持更好。
3.2 tsconfig.json --- ESM 关键配置
ESM 的核心约束 : 导入本地模块必须显式写 .js 后缀。
typescript
// ✅ 正确: 带 .js 后缀
import logger from './utils/logger.js';
import TDTController from '../controller/tdtController.js';
// ❌ 错误: 省略后缀 (bundler 模式下可省略, 但 tsc 编译期会报错)
import logger from './utils/logger';
moduleResolution: "bundler" : 比 "node16" 更宽松, 允许:
- 省略
.js后缀的导入 (配合allowImportingTsExtensions) package.json的exports字段解析- 不要求
package.json有"type": "module"就能解析 ESM
3.3 环境变量加载
src/config/env.ts:
typescript
import dotenv from 'dotenv';
dotenv.config();
配合 app.ts 中的多层加载:
typescript
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
const envFile =
process.env.NODE_ENV === 'production' ? '.env.prod' : '.env.dev';
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
override: true确保环境专属配置覆盖基础默认值。
3.4 日志系统
src/utils/logger.ts 使用 Winston 实现多 Transport:
typescript
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/all.log' }),
],
});
// 开发环境额外输出到控制台
if (process.env.NODE_ENV === 'development') {
logger.add(new winston.transports.Console({ format: colorize() }));
}
日志级别 (数字越小优先级越高):
| 级别 | 用途 |
|---|---|
| error (0) | 异常、失败 |
| warn (1) | 警告 |
| info (2) | 一般信息 |
| http (3) | HTTP 请求 |
| debug (4) | 调试详情 |
Morgan 集成到 Winston:
typescript
import morgan from 'morgan';
const morganMiddleware = morgan(':method :url :status - :response-time ms', {
stream: { write: (message: string) => logger.http(message.trim()) },
});
app.use(morganMiddleware);
四、开发工作流
4.1 开发模式
bash
npm run dev
流程: cross-env 设置环境变量 → nodemon 监控 src/ → tsx 直接执行 src/server.ts。
修改任何 .ts 文件后自动重启, 无需手动编译。
4.2 生产构建
bash
npm run build
tsc 将 src/ 编译到 dist/, 输出标准 JavaScript + source maps + 类型声明。
4.3 生产启动
bash
npm start
# 等价于: node dist/server.js
4.4 静态资源托管
typescript
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 托管 public 目录
app.use('/public', express.static(path.join(__dirname, '../public')));
// 托管 test 目录 (仅开发)
app.use('/test', express.static(path.join(__dirname, '../test')));
访问 http://localhost:8080/test/map.html 即可查看测试页面。
ESM 替代 __dirname : ESM 模块没有
__dirname, 需通过fileURLToPath(import.meta.url)获取当前文件路径, 再用path.dirname()取目录。
4.5 npm scripts 全览
| 命令 | 用途 |
|---|---|
npm run dev |
开发 (热重载) |
npm run build |
编译 TS → JS |
npm start |
生产启动 |
五、进阶主题
5.1 统一响应格式
typescript
export class ResponseUtil {
static success<T>(res: Response, data?: T, message = '操作成功', code = 200) {
return res.status(200).json({
code,
message,
data,
timestamp: Date.now(),
success: true,
});
}
static error(
res: Response,
message = '操作失败',
code = 500,
httpStatus = 400,
) {
return res.status(httpStatus).json({
code,
message,
timestamp: Date.now(),
success: false,
});
}
static notFound(res: Response, message = '资源不存在') {
return this.error(res, message, 404, null, 404);
}
static successWithPagination<T>(res, list, total, page, pageSize) {
return this.success(res, { list, total, page, pageSize });
}
}
5.2 安全中间件 (Helmet)
typescript
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
}),
);
5.3 CORS 多环境策略
typescript
if (process.env.NODE_ENV === 'development') {
// 开发: 宽松模式, 允许所有来源 (方便调试)
app.use(devCorsMiddleware);
} else {
// 生产: 严格模式, 仅允许白名单来源
app.use(corsMiddleware);
}
5.4 优雅关闭
typescript
const shutdown = async (signal: string) => {
server.close(() => logger.info('HTTP 已关闭'));
// 30 秒超时强制退出
setTimeout(() => {
logger.error('超时强制退出');
process.exit(1);
}, 30000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
附录: 项目依赖一览
生产依赖
| 包 | 版本 | 用途 |
|---|---|---|
| express | ^5.0.0 | HTTP 框架 |
| axios | ^1.10.0 | HTTP 客户端 |
| cors | ^2.8.5 | CORS 中间件 |
| dotenv | ^16.5.0 | 环境变量 |
| helmet | ^7.1.0 | 安全头 |
| morgan | ^1.10.0 | HTTP 日志 |
| winston | ^3.17.0 | 日志框架 |
| zod | ^3.25.67 | 数据校验 |
开发依赖
| 包 | 版本 | 用途 |
|---|---|---|
| typescript | ^5.3.0 | 类型系统 |
| tsx | ^4.22.2 | TS 运行时 |
| nodemon | ^3.0.2 | 热重载 |
| cross-env | ^7.0.3 | 跨平台 env |
| @types/express | ^4.17.17 | Express 类型 |
| @types/node | ^20.0.0 | Node 类型 |