阅读视图

发现新文章,点击刷新页面。

2026 年 Node.js + TS 开发:别再纠结 nodemon 了,聊聊热编译的最优解

在开发 Node.js 服务端时,“修改代码 -> 自动生效”的开发体验(即热编译/热更新)是影响效率的关键。随着 Node.js 23+  原生支持 TS 以及 Vite 5 的普及,我们的工具链已经发生了巨大的更迭。

今天我们深度拆解三种主流的 Node.js TS 开发实现方式,帮你选出最适合 2026 年架构的方案。


一、 方案对比大盘点

方案 核心原理 优点 缺点 适用场景
tsx (Watch Mode) 基于 esbuild 的极速重启 零配置、性能强、生态位替代 nodemon 每次修改重启整个进程,状态丢失 小型服务、工具脚本
vite-node 基于 Vite 的模块加载器 完美继承 Vite 配置、支持模块级 HMR 配置相对复杂,需手动处理 HMR 逻辑 中大型 Vite 全栈项目
Node.js 原生 Node 23+ Type Stripping 无需第三方依赖,官方标准 需高版本 Node,功能相对单一 追求极简、前瞻性实验

二、 方案详解

  1. 现代替代者:tsx —— 告别 nodemon + ts-node

过去我们常用 nodemon --exec ts-node,但在 ESM 时代,这套组合经常报 ERR_UNKNOWN_FILE_EXTENSION 错误。

tsx 内部集成了 esbuild,它是目前 Node 18+ 环境下最稳健的方案。

  • 实现热编译:

    bash

    npx tsx --watch src/index.ts
    

    请谨慎使用此类代码。

  • 为什么选它:  它不需要额外的加载器配置(--loader),且 watch 模式非常智能,重启速度在毫秒级。

  1. 开发者体验天花板:vite-node —— 真正的 HMR

如果你已经在项目中使用 Vite 5,那么 vite-node 是不二之选。它不仅是“重启”,而是“热替换”。

  • 核心优势:

    • 共享配置:直接复用 vite.config.ts 中的 alias 和插件。
    • 按需编译:只编译当前运行到的模块,项目越大优势越明显。
  • 实现热更新(不重启进程):

    typescript

    // src/index.ts
    import { app } from './app';
    let server = app.listen(3000);
    
    if (import.meta.hot) {
      import.meta.hot.accept('./app', (newModule) => {
        server.close(); // 优雅关闭旧服务
        server = newModule.app.listen(3000); // 启动新逻辑,DB连接可复用
      });
    }
    

    请谨慎使用此类代码。

  1. 官方正统:Node.js 原生支持

如果你能使用 Node.js 23.6+ ,那么可以摆脱所有构建工具。

  • 运行:  node --watch src/index.ts
  • 点评:  这是未来的趋势,但在 2026 年,由于生产环境往往还停留在 Node 18/20 LTS,该方案目前更多用于本地轻量级开发。

三、 避坑指南:Vite 5 打包 Node 服务的报错

在实现热编译的过程中,如果你尝试用 Vite 打包 Node 服务,可能会遇到:

Invalid value for option "preserveEntrySignatures" - setting this option to false is not supported for "output.preserveModules"

原因:  当你开启 preserveModules: true 想保持源码目录结构输出时,Rollup 无法在“强制保留模块”的同时又“摇树优化(Tree Shaking)”掉入口导出。

修复方案:
在 vite.config.ts 中明确设置:

typescript

build: {
  rollupOptions: {
    preserveEntrySignatures: 'exports-only', // 显式声明保留导出
    output: {
      preserveModules: true
    }
  }
}

请谨慎使用此类代码。


四、 总结:我该选哪个?

  1. 如果你只想快速写个接口,不想折腾配置:请直接使用 tsx。它是 2026 年 nodemon 的完美继承者。
  2. 如果你在做复杂全栈项目,或者有大量的路径别名:请使用 vite-node。它能让你在 Node 端获得跟前端 React/Vue 编写时一样丝滑的 HMR 体验。
  3. 如果是为了部署生产环境:无论开发环境用什么,生产环境请务必通过 vite build 产出纯净的 JS,并使用 node dist/index.js 运行。

Sass 模块化革命:告别 @import,拥抱 @use 和 @forward

为什么你的 Sass 代码突然开始报错?是时候彻底理解 Sass 的模块化系统了!

最近很多前端开发者突然发现自己的 Sass 代码开始报出各种警告和错误:

  • @import rules are deprecated
  • There's already a module with namespace "math"
  • Using / for division is deprecated

这一切都源于 Dart Sass 的模块化革命。如果你还在使用传统的 @import,那么这篇文章将带你彻底理解新的模块系统,并手把手教你如何迁移。

为什么要弃用 @import?

传统 @import 的问题

让我们先回顾一下 @import 的常见用法:

// variables.scss
$primary-color: #1890ff;
$font-size: 14px;

// main.scss
@import "variables";
@import "mixins";
@import "components/button";

.button {
  color: $primary-color;
  font-size: $font-size;
}

看起来很简单对吧?但 @import 有几个致命缺陷:

  1. 全局污染:所有变量、mixin、函数都混入全局作用域
  2. 无法避免冲突:同名变量会被覆盖,且很难追踪来源
  3. 无法控制可见性:无法隐藏私有变量
  4. 性能问题:每次 @import 都会重新计算
  5. 依赖混乱:无法知道模块间的依赖关系

新系统的优势

@use@forward 组成的模块系统解决了所有这些问题:

  • 命名空间隔离:每个模块有自己的作用域
  • 明确的依赖关系:清晰知道每个变量来自哪里
  • 更好的封装:可以隐藏私有成员
  • 更快的编译:模块只被计算一次

核心概念:@use vs @forward

@use:使用模块

@use 用于在当前文件中使用其他模块的功能。

// 基本用法
@use "sass:math";
@use "variables";

// 通过命名空间访问
.element {
  width: math.div(100%, 3);
  color: variables.$primary-color;
}

// 使用通配符(类似旧版行为)
@use "variables" as *;
.element {
  color: $primary-color; // 直接使用,无需前缀
}

// 自定义命名空间
@use "variables" as vars;
.element {
  color: vars.$primary-color;
}

@forward:转发模块

@forward 用于转发模块的成员,但不直接使用它们。常见于库的入口文件。

// 转发整个模块
@forward "variables";

// 选择性转发
@forward "sass:math" show div, ceil, floor;
@forward "components/button" hide _private-mixin;

// 重命名转发
@forward "sass:math" as math-*;
// 使用时会变成:math-div(), math-ceil()

实战迁移指南

场景1:基础变量和工具迁移

迁移前(@import):

// styles/variables.scss
$primary-color: #1890ff;
$border-radius: 4px;

// styles/mixins.scss
@mixin rounded-corners($radius: $border-radius) {
  border-radius: $radius;
}

// main.scss
@import "styles/variables";
@import "styles/mixins";

.button {
  color: $primary-color;
  @include rounded-corners;
}

迁移方案A:直接使用

// main.scss
@use "styles/variables" as vars;
@use "styles/mixins";

.button {
  color: vars.$primary-color;
  @include mixins.rounded-corners;
}

迁移方案B:创建库入口

// styles/_index.scss (库入口)
@forward "variables";
@forward "mixins";

// main.scss
@use "styles" as *; // 所有成员直接可用

.button {
  color: $primary-color;
  @include rounded-corners;
}

场景2:处理第三方库冲突

问题场景: 第三方库和你的代码都需要 sass:math

// ❌ 可能冲突的情况
// element-plus 内部已使用: @use "sass:math" as math;
// 你的代码中也使用: @use "sass:math" as math;

// ✅ 解决方案1:使用不同命名空间
@use "sass:math" as original-math;

.element {
  width: original-math.div(100%, 3);
}

// ✅ 解决方案2:创建包装函数
// utils/_math-utils.scss
@use "sass:math" as sass-math;

@function divide($a, $b) {
  @return sass-math.div($a, $b);
}

// 使用
@use "utils/math-utils" as math;
.element {
  width: math.divide(100%, 3);
}

场景3:构建组件库

项目结构:

ui-library/
├── foundation/
│   ├── _variables.scss
│   ├── _colors.scss
│   └── _index.scss
├── components/
│   ├── _button.scss
│   ├── _card.scss
│   └── _index.scss
└── index.scss

配置入口文件:

// ui-library/foundation/_index.scss
@forward "variables";
@forward "colors";
@forward "typography";

// ui-library/components/_index.scss
@forward "button" show button, button-variants;
@forward "card" show card;
// 隐藏私有成员
@forward "modal" hide _private-styles;

// ui-library/index.scss
@forward "foundation";
@forward "components";

// 业务代码中使用
@use "ui-library" as ui;

.custom-button {
  @extend ui.button;
  background-color: ui.$primary-color;
}

常见陷阱和解决方案

陷阱1:命名空间冲突

// ❌ 错误:相同的命名空间
@use "module1" as utils;
@use "module2" as utils; // 错误:命名空间 "utils" 重复

// ✅ 正确:使用不同的命名空间
@use "module1" as utils1;
@use "module2" as utils2;

陷阱2:@use 和 @forward 顺序错误

// ❌ 错误:@forward 必须在 @use 之前
@use "sass:color";
@forward "sass:math"; // 错误!

// ✅ 正确:正确的顺序
@forward "sass:math"; // 先转发
@use "sass:color";    // 后使用

陷阱3:忽略除法运算迁移

// ⚠️ 警告:传统除法将弃用
$ratio: 16/9; // 警告:Using / for division is deprecated

// ✅ 正确:使用 math.div()
@use "sass:math";
$ratio: math.div(16, 9);

陷阱4:在 @forward 文件中直接使用转发的成员

// utils/_index.scss
@forward "math-tools";

// ❌ 错误:不能在转发文件中直接使用转发的成员
$value: math.div(100, 2); // 错误!math 不可用

// ✅ 正确:需要单独 @use
@use "sass:math" as math;
$value: math.div(100, 2);
@forward "math-tools";

自动化迁移工具

Sass 官方提供了强大的迁移工具:

# 安装迁移工具
npm install -g sass-migrator

# 1. 迁移 @import 到 @use
sass-migrator import-to-use **/*.scss

# 2. 迁移除法运算
sass-migrator division **/*.scss

# 3. 同时处理多种文件类型
sass-migrator import-to-use --recursive "**/*.{scss,sass,vue}"

# 4. 带参数的迁移
sass-migrator import-to-use --namespace=lib "src/**/*.scss"

最佳实践总结

1. 命名策略

// 基础变量 → 通配符导入(方便使用)
@use "variables" as *;

// 工具函数 → 命名空间导入(避免冲突)
@use "utils/math" as math;

// 第三方库 → 使用短命名空间
@use "element-plus" as ep;

2. 文件组织

// 库/框架:使用 @forward 构建清晰的API
// _index.scss
@forward "foundation" show $colors, $typography;
@forward "components" hide _private-*;
@forward "utilities" as utils-*;

// 业务代码:使用 @use 明确依赖
@use "ui-library" as ui;
@use "project/utils" as utils;

3. 处理依赖关系

// 依赖图:A → B → C
// c.scss
$value: red;

// b.scss
@use "c" as *;
$color: $value;

// a.scss
@use "b" as *;
.element { color: $color; }

性能优化建议

  1. 减少重复计算:模块只计算一次,即使被多次 @use
  2. 合理使用缓存:构建工具通常会缓存编译结果
  3. 避免深层嵌套:过深的 @forward 链可能影响性能
  4. 按需导入:使用 show/hide 只导入需要的成员

版本兼容性

// package.json 版本建议
{
  "devDependencies": {
    "sass": "^1.58.0",     // 支持完整模块系统
    "sass-loader": "^13.2.0"
  }
}

写在最后

迁移到新的 Sass 模块系统看起来有些挑战,但带来的好处是实实在在的:

🎯 代码更清晰:明确的依赖关系和命名空间
🔧 维护更容易:模块化的结构便于重构
性能更好:智能的缓存和编译优化
🚀 面向未来:符合现代前端开发的最佳实践

迁移不是一次性的痛苦,而是一次性的投资。现在花时间迁移,未来将节省大量的调试和维护时间。

记住这个简单的决策流程:

  1. 构建库/框架 → 优先使用 @forward
  2. 编写业务代码 → 主要使用 @use
  3. 基础变量/配置 → 考虑 @use ... as *
  4. 工具函数 → 使用命名空间避免冲突

行动起来吧! 从今天开始,逐步将你的项目迁移到新的模块系统。你的未来代码库会感谢你现在做出的努力!


你的项目开始迁移了吗? 在迁移过程中遇到了什么有趣的问题或挑战?欢迎在评论区分享你的经验!

❌