02 TypeScript 工程的生产构建
1. 当前工程的特点
当前 tsconfig.json 采用:
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"esModuleInterop": true,
"strict": true
}
}
target: "esnext" 会尽量保留较新的 JavaScript 语法。生产构建时必须确保它与服务器实际 Node.js 版本兼容;固定生产 Node 版本后,应根据项目实际语法和 API 选择并验证 target,而不是默认所有服务器都支持最新输出。
开发方式主要是:
ts-node server.ts
但当前配置没有启用 rootDir 和 outDir,package.json 也没有独立的 build 脚本。教程给出的是推荐目标,不会直接修改当前工程。
2. “编译”和“打包”不是一回事
tsc 编译
server.ts → dist/server.js
routes/user.ts → dist/routes/user.js
通常仍然保留多个 JavaScript 文件,也不会把 Express、Sequelize、mysql2 合并进去。
esbuild/webpack 打包
可能把多个源码文件甚至部分依赖合并成较少文件,但会增加配置和原生依赖处理问题。当前工程包含 argon2 等原生模块,新人阶段没有必要先走打包路线。
本教程选择:
tsc 编译 + 生产服务器安装 dependencies
3. 推荐增加独立构建配置
可以保留用于开发和类型检查的 tsconfig.json,再创建 tsconfig.build.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"noEmit": false,
"declaration": false,
"declarationMap": false,
"sourceMap": true
},
"include": [
"server.ts",
"routes/**/*.ts",
"controllers/**/*.ts",
"services/**/*.ts",
"models/**/*.ts",
"middlewares/**/*.ts",
"types/**/*.d.ts"
],
"exclude": [
"dist",
"node_modules",
"docs",
"exercises"
]
}
实际 include 应与项目源码目录一致。当前工程的 TypeScript 文件分布较散,后期可以再学习迁移到 src/,首次上线不必同时进行大规模目录重构。
4. 推荐的 npm scripts
可以将开发、类型检查、构建和生产启动分开:
{
"scripts": {
"dev": "nodemon --exec ts-node server.ts",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/server.js"
}
}
它们分别表示:
npm run dev 开发环境运行 TypeScript
npm run typecheck 只检查类型,不生成文件
npm run build 生成生产 JavaScript
npm start 运行已经生成的 JavaScript
当前项目的 start 使用 nodemon --exec ts-node,更像开发命令。正式调整时需要同步确认 nodemon、ts-node 是否已声明为开发依赖,不能依赖服务器“碰巧全局安装”。
5. 构建后为什么仍要安装依赖
编译后的文件仍可能包含:
const express_1 = require("express");
const sequelize_1 = require("sequelize");
Node.js 运行时仍要从 node_modules 找到它们。因此只上传 dist/ 通常不够。
生产服务器可以安装运行依赖:
npm ci --omit=dev
它会根据 package-lock.json 安装 dependencies,跳过 devDependencies。
6. 在服务器构建时的依赖顺序
typescript 位于 devDependencies,所以如果服务器负责执行 npm run build,需要先安装开发依赖:
npm ci
npm run typecheck
npm run build
npm prune --omit=dev
含义:
npm ci 安装锁文件中的完整依赖
npm run build 使用 TypeScript 编译器
npm prune --omit=dev 删除生产运行不需要的开发依赖
更成熟的流程可以在 CI 构建,再把构建产物和生产依赖部署到服务器,但新人第一次上线先理解上述流程即可。
7. 原生依赖不能随意跨平台复制
当前项目包含 argon2。这类包可能带有平台相关二进制文件。
不要简单地把 Windows 上的整个 node_modules 复制到 Linux:
Windows node_modules
≠ 可直接在 Linux 使用的 node_modules
应在目标 Linux 或与目标环境一致的构建环境中执行:
npm ci --omit=dev
Node 主版本也应与构建/运行预期一致。
8. PM2 可以直接运行 TypeScript 吗
技术上可以,例如:
pm2 start server.ts --interpreter ts-node
但它要求生产环境安装 ts-node 和 typescript,并让编译配置问题发生在服务启动阶段。首次生产部署更推荐:
npm run build
pm2 start dist/server.js --name nloop-api
直接运行 TypeScript 更适合学习、开发服务器或内部低风险工具,不是本教程的默认生产方案。
9. .env 和构建产物
.env 不会因为运行 tsc 自动进入 dist。通常由运行时从工作目录加载:
import "dotenv/config";
因此 PM2 的 cwd 很重要:
cwd: "/var/www/nloop/current"
生产 .env:
- 不提交 Git;
- 不打包进前端;
- 限制 Linux 文件权限;
- 发布新目录时需要安全复制或由部署系统提供;
- 不在日志中输出完整内容。
10. 上线前先脱离 PM2 验证
先运行:
npm run build
NODE_ENV=production node dist/server.js
再从另一个终端测试:
curl -i http://127.0.0.1:8080/health
如果这里失败,问题属于构建、环境变量或应用本身,不要先怀疑 PM2 和 Nginx。
复盘题
tsc为什么通常不等于打包?- 只上传
dist为什么可能出现Cannot find module 'express'? - 为什么不建议复制 Windows 的
node_modules到 Linux? - 服务器上执行构建时,为什么不能一开始就只安装生产依赖?
- 为什么建议先用
node dist/server.js验证,再交给 PM2?