Express + TypeScript + ESM 后端服务搭建教程

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.jsonexports 字段解析
  • 不要求 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

tscsrc/ 编译到 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 类型
相关推荐
ZC跨境爬虫1 小时前
跟着 MDN 学CSS day_32:(Web字体深度解析与实践指南)
前端·javascript·css·ui·html
sugar__salt2 小时前
JavaScript 数组去重全解:6 种核心方法
javascript
SEO_juper2 小时前
JavaScript 渲染:AI 智能体无法读取,直接影响收录
开发语言·前端·javascript·aigc·seo·跨境电商·geo
whuhewei2 小时前
一道React缓存的题目
javascript·react.js
何何____2 小时前
js的数据存储机制
开发语言·前端·javascript·ecmascript
云水一下2 小时前
JavaScript 从零基础到精通系列:对象、数组与 ES6 数据操作利器
前端·javascript
无聊的老谢2 小时前
Vue 3 + Leaflet 实现高性能 Web GIS 基站监控平台
前端·javascript·vue.js
之歆2 小时前
Day23_Bootstrap 前端框架完全指南:从栅格系统到组件化开发
开发语言·前端·javascript·前端框架·bootstrap·ecmascript·less
前端 贾公子2 小时前
3.响应式系统基础:从发布订阅模式的角度理解 Vue2 的数据响应式原理(上)
前端·javascript·vue.js