Node.js 20 + TypeScript 5.5 实战:3步配置 ESM 与 CJS 双模块支持
·
Node.js 20 + TypeScript 5.5 实战:3步配置 ESM 与 CJS 双模块支持
当你在2024年启动一个新的Node.js项目时,模块系统的选择已经不再是非此即彼的单选题。随着Node.js 20对ES模块(ESM)的成熟支持,以及TypeScript 5.5在类型系统上的重大改进,现代后端开发迎来了模块化方案的最佳实践时代。
1. 理解现代Node.js的模块格局
在Node.js生态中,模块系统经历了从CommonJS(CJS)到ES Modules(ESM)的演进。截至2024年,两种规范各有其不可替代的优势:
CJS的坚守价值 :
- 仍然是npm仓库中80%+模块的默认格式
- 同步加载特性在特定场景下更符合直觉
- 对
__dirname和__filename的原生支持
ESM的现代优势 :
- 静态分析带来的tree-shaking优化
- 浏览器原生支持,统一前后端模块规范
- top-level await等新语法的支持
// ESM的典型导入方式
import { createServer } from 'node:http'
import express from 'express'
// CJS的等效写法
const { createServer } = require('node:http')
const express = require('express')
关键决策点:如果你的项目需要大量使用npm上的传统模块,或者需要与旧系统集成,CJS可能更合适;如果是全新项目且追求长期可维护性,ESM是更好的起点。
2. 三阶段配置实战
2.1 基础环境搭建
首先确保你的开发环境满足:
- Node.js 20.x+
- TypeScript 5.5+
- 包管理器(npm/yarn/pnpm)最新版
# 初始化项目
mkdir node-ts-dual-modules && cd node-ts-dual-modules
npm init -y
# 安装核心依赖
npm install typescript@5.5 @types/node --save-dev
创建基础 tsconfig.json :
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
2.2 双模块支持配置
在 package.json 中添加模块定义:
{
"type": "module",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
}
}
创建多目标构建脚本 build.js :
import { execSync } from 'node:child_process'
// ESM构建
execSync('tsc -p tsconfig.esm.json')
// CJS构建
execSync('tsc -p tsconfig.cjs.json')
对应的两份TS配置:
// tsconfig.esm.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ES2022",
"outDir": "./dist/esm"
}
}
// tsconfig.cjs.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist/cjs"
}
}
2.3 类型声明生成
添加类型声明生成配置:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "./dist/types"
}
}
最终构建命令:
node build.js && tsc -p tsconfig.types.json
3. 实战中的兼容性处理
3.1 文件扩展名策略
在双模块项目中,文件扩展名需要特别注意:
| 模块类型 | 推荐扩展名 | 说明 |
|---|---|---|
| ESM | .mts | 明确表明模块类型 |
| CJS | .cts | 便于工具链识别 |
| 通用 | .ts | 需要配合package.json使用 |
// utils.mts
export function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// utils.cts
exports.capitalize = function(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
3.2 条件性导入技巧
使用 import.meta.resolve 实现条件导入:
async function loadModule(moduleName: string) {
try {
if (process.versions.node.split('.')[0] >= '20') {
const modulePath = await import.meta.resolve(moduleName)
return import(modulePath)
}
return require(moduleName)
} catch (err) {
console.error(`Failed to load ${moduleName}:`, err)
throw err
}
}
3.3 性能优化配置
在 tsconfig.json 中添加这些优化选项:
{
"compilerOptions": {
"incremental": true,
"composite": true,
"tsBuildInfoFile": "./.cache/tsbuildinfo",
"sourceMap": true
}
}
4. 企业级项目的最佳实践
4.1 模块边界设计
推荐的项目结构:
src/
core/ # 核心业务逻辑(ESM)
services/
entities/
adapters/ # 适配层(CJS)
legacy/
third-party/
shared/ # 通用工具(双模块)
utils/
types/
4.2 构建流水线优化
现代构建工具配置示例:
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
fileName: (format) => `index.${format}.js`
},
rollupOptions: {
external: ['node:fs', 'node:path']
}
}
})
4.3 监控与调试
在开发环境添加模块加载日志:
// 在入口文件顶部添加
if (process.env.NODE_ENV === 'development') {
import('node:module').then(({ Module }) => {
const originalLoad = Module._load
Module._load = function(request: string) {
console.debug(`Loading: ${request}`)
return originalLoad.apply(this, arguments)
}
})
}
5. 常见问题解决方案
5.1 类型定义冲突
当遇到第三方模块类型冲突时,创建 types/global.d.ts :
declare module 'some-cjs-package' {
import type { OriginalType } from 'some-cjs-package/actual-types'
const value: OriginalType
export = value
}
5.2 循环依赖处理
ESM下的循环依赖解决方案:
// a.mts
export let initialized = false
export function init() {
initialized = true
}
// b.mts
import { initialized } from './a.mjs'
export function checkInit() {
return initialized
}
// 在入口文件
import { init } from './a.mjs'
import './b.mjs'
init()
5.3 测试策略
Jest配置示例:
// jest.config.cjs
module.exports = {
preset: 'ts-jest/presets/default-esm',
globals: {
'ts-jest': {
useESM: true,
},
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
}
6. 未来演进路线
随着Node.js生态的发展,建议关注这些趋势:
- 模块联邦 :探索
import()动态加载与微前端结合 - Wasm集成 :ESM对WebAssembly的本地支持
- 类型演进 :TypeScript的
moduleSuffixes提案
// 未来的可能配置
{
"compilerOptions": {
"moduleSuffixes": [".mts", ".ts", ".cts"]
}
}
在实际项目中,我们发现采用渐进式迁移策略最为有效:先确保核心代码库使用ESM,然后逐步将依赖项迁移,最后处理边缘案例。这种方案既保证了开发效率,又为未来的技术演进留出了空间。
更多推荐

所有评论(0)