阅读视图

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

langchainjs&langgraphjs入门(二)格式化输出

格式化输出

zod

zod是一个ts的类型校验库,langchain官方推荐使用zod来定义ai输出的schema,例如:

import {z} from 'zod'
// 期望ai返回对象,其中包含name,age,skills
const schema = z.object({
    name:z.string().describe('姓名'),
    age: z.number().init().describe('年龄'),
    skills: z.array(z.string()).describe('技能')
})

安装:

npm i zod

速查表:

API 用途 示例
z.string() 字符串 z.string()
z.number() 数字 z.number().int()
z.boolean() 布尔值 z.boolean()
z.object() 对象 z.object({ name: z.string() })
z.array() 数组 z.array(z.string())
z.enum() 枚举 z.enum(["A", "B"])
z.union() 联合类型 z.string().or(z.number())
z.optional() 可选字段 z.string().optional()
z.nullable() 允许 null z.string().nullable()
z.default() 默认值 z.number().default(0)
z.literal() 字面量 z.literal("on")
z.record() 键值映射 z.record(z.string())
z.tuple() 元组 z.tuple([z.string(), z.number()])
.refine() 自定义校验 .refine(s => s.length > 3)
.transform() 转换输出 .transform(s => s.toUpperCase())
.describe() 描述 z.string().describe('姓名')

withStructuredOutput

上一章节的例子可以看到模型的输出只是普通的字符串,并没有格式化.无法直接使用.要想让模型输出格式化的内容可以使用官方推荐的zod,使用他来定义数据结构并验证数据结构是否正确,从而帮助langchain实现输出的格式化和验证.

  1. 首先使用zod定义类型
  2. 然后通过langchain提供的.withStructuredOutput接口使用类型,调用这个方法传入zod定义的类型.模型将添加所需的所有模型参数和输出解析器

示例

import model from './1调用模型.mjs'
import { z } from 'zod'

const schema = z.object({ isCool: z.boolean() }) // 定义输出类型
const structuredLlm = model.withStructuredOutput(schema)

const res = await structuredLlm.invoke('我帅不帅')
console.log("res:",res); // res: { isCool: true }输出了结构化的内容,可以看到模型也知道我很帅

实际上withStructuredOutput在背后会根据schema自动生成严格的提示词,并自动解析验证模型输出,然后将结果返回给开发者

withStructuredOutputlangchain封装后的便捷api,如果想深入理解背后做了什么可以查看这里,后面我们也会详细讲解

StringOutputParser

StringOutoutParser可以从LLM回复的消息中直接提取文本内容,使得我们获取的不再是AIMessage对象而是纯文本

用法

实例化的方式进行创建

import { StringOutputParser } from "@langchain/core/output_parsers";

// 创建实例(无需传递任何参数)
const outputParser = new StringOutputParser();
// 使用
const res = await llm.invoke('你好')
const str = outputParser.invoke(res)

// 简便使用
const chain = llm.pipe(outputParser)
const res = chain.invoke('你好')

对于格式化输出,为便于记忆暂时先了解这么多.知道langchain提供了这么个功能,当上述能力不满足实际开发场景时,再去翻阅官方文档即可.

关注我,该专栏会持续更新!

【Vue-2/Lesson62(2025-12-10)】模块化与 Node.js HTTP 服务器开发详解🧩

🧩在现代软件工程中,模块化不仅是提升代码可读性、可维护性和复用性的核心手段,更是构建大型应用系统的基石。而 Node.js 作为全栈 JavaScript 运行时环境,其原生支持的模块系统和 HTTP 服务能力,为开发者提供了从零搭建 Web 服务的强大工具。本文将围绕 模块化方案的意义Node.js HTTP 服务器的实现细节 展开全面、深入的解析,并结合实际代码示例,系统梳理从基础概念到工程实践的完整知识链。


🔌 为什么要有模块化方案?

模块化(Modularization)是将复杂程序分解为多个独立、职责单一、可组合单元的编程范式。它并非某一种具体技术,而是一种设计思想,贯穿于前后端开发的整个生命周期。

1. 📦 代码组织与管理

没有模块化的代码如同一锅大杂烩:所有逻辑挤在一个文件中,变量、函数、类混杂,难以理解、调试和扩展。模块化通过“分而治之”的策略,将功能拆解:

  • 前端项目:UI组件、工具函数(utils)、API请求封装、状态管理等各自成模块。
  • 后端项目:采用 MVC 架构,将路由(Router)、控制器(Controller)、模型(Model)、中间件(Middleware)分离。

例如,在 Express 应用中,routes/user.js 负责用户相关路由,controllers/userController.js 处理业务逻辑,models/User.js 定义数据结构——这种结构清晰、职责分明。

2. 🛡️ 避免全局变量污染

早期前端开发常将变量直接挂载到 window 对象上,极易引发命名冲突。模块化通过作用域隔离解决此问题:

  • CommonJS(Node.js 默认) :每个文件是一个模块,内部变量默认私有,通过 module.exports 导出,require() 导入。
  • ES6 Modules(ESM) :使用 import/export 语法,变量在模块作用域内,不会污染全局。
// math.js (ESM)
export const add = (a, b) => a + b;

// main.js
import { add } from './math.js';
console.log(add(2, 3)); // 5

3. ♻️ 提高代码复用性

模块是天然的“积木”。通用逻辑(如日期格式化、HTTP 请求封装)一旦封装为模块,即可在多个项目中复用:

  • 自建工具库:utils/string.jsutils/array.js
  • 第三方依赖:通过 npm 安装的 lodashaxios

4. 🔧 增强可维护性

单一职责原则(SRP)要求一个模块只负责一件事。这带来三大优势:

  • 封装性:隐藏内部实现,仅暴露必要接口(如只导出 getUser(id) 而非数据库连接细节)。
  • 低耦合:修改模块 A 不影响模块 B。
  • 易测试:可对单个模块进行单元测试。

5. 🧭 依赖管理

模块化显式声明依赖关系,避免“隐式引用”导致的混乱:

// userController.js 明确依赖 userService
const userService = require('./userService');

工具(如 Webpack、Rollup)能自动分析依赖图,按需打包或加载。

6. 👥 支持团队协作

多人并行开发时,模块化允许开发者专注特定功能模块,互不干扰:

  • 独立开发:前端 A 写登录组件,后端 B 写认证 API。
  • 独立测试:各模块可单独运行测试用例。
  • 代码审查:PR 聚焦于特定模块变更。

📜 模块化的发展历程

前端模块化演进

  • 无模块时代(<2015)<script> 标签堆砌,全局变量泛滥。
  • AMD/CMD(RequireJS/Sea.js) :异步加载,解决浏览器环境依赖问题。
  • UMD:兼容 AMD、CommonJS 和全局变量的“万能”格式。
  • ES6 Modules(2015+) :语言标准,静态分析,成为现代前端主流(Vue/React/Angular 均基于 ESM)。

后端模块化(Node.js)

  • CommonJS:Node.js 诞生之初采用,同步 require(),适合服务器端 I/O 阻塞场景。
  • ESM 支持:Node.js v13.2+ 原生支持 .mjs 文件或 package.json"type": "module"

尽管 ESM 是未来趋势,但大量 Node.js 生态库仍基于 CommonJS,二者共存是当前常态。


🖥️ Node.js HTTP 服务器实战解析

Node.js 内置 http 模块,无需额外依赖即可创建高性能 Web 服务器。以下通过逐步演进的代码示例,剖析其核心机制。

📁 基础版本(note1.md)

const http = require("http");
const url = require("url");

const users = [/* 用户数据 */];

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  console.log(parsedUrl);
  res.end('hello');
});

server.listen(1314, () => {
  console.log('Server is running on port 1314');
});

关键点:

  • 模块引入:使用 CommonJS 的 require() 加载内置模块。
  • URL 解析url.parse(req.url, true) 将 URL 字符串解析为对象(含 pathname, query 等属性)。
  • 问题:监听端口(1314)与注释(1214)不一致,属低级错误。

🚦 路由增强版(note2.md)

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/users') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html;charset=utf-8');
    const html = `<html><body><h1>Hello World</h1></body></html>`;
    res.end(html);
  } else {
    res.end('hello');
  }
});

改进:

  • 简单路由:根据 pathname 区分路径,返回不同内容。
  • 响应头设置Content-Type 指定为 HTML 并声明 UTF-8 编码,避免中文乱码。
  • 未利用数据users 数组定义但未使用,属资源浪费。

📊 数据驱动 HTML 版(note3.md / server.js)

function generationHtml(users) {
  const userRows = users.map(user => `
    <tr>
      <td>${user.id}</td>
      <td>${user.name}</td>
      <td>${user.email}</td>
    </tr>
  `).join('');
  
  return `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>User List</title>
      <style>
        table { width: 100%; border-collapse: collapse; }
        th, td { border: 11px solid #ccc; padding: 8px; }
      </style>
    </head>
    <body>
      <h1>Users</h1>
      <table>
        <thead><tr><th>ID</th><th>Name</th><th>Email</th></tr></thead>
        <tbody>${userRows}</tbody>
      </table>
    </body>
    </html>
  `;
}

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/users') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html;charset=utf-8');
    const html = generationHtml(users); // 使用用户数据生成HTML
    res.end(html);
  } else {
    res.statusCode = 404;
    res.end('<h1>404 Not Found</h1>');
  }
});

核心升级:

  • 动态 HTML 生成generationHtml 函数接收 users 数组,通过模板字符串构建完整 HTML 表格。
  • 完整页面结构:包含 DOCTYPE、meta 标签、CSS 样式,确保页面美观。
  • 404 处理:对非法路径返回标准 404 响应。

⚠️ 注意:函数名 generationHtml 应为 generateHtml(动词形式),属命名规范问题。


🛠️ 代码优化与最佳实践

1. 修复命名与结构

function generateHtml(users) { /* ... */ } // 正确命名

2. 分离路由逻辑

随着路由增多,将处理函数抽离:

function handleRoot(req, res) { /* 返回首页 */ }
function handleUsers(req, res) { /* 返回用户列表 */ }

const server = http.createServer((req, res) => {
  const { pathname } = url.parse(req.url, true); // 解构赋值
  if (pathname === '/') handleRoot(req, res);
  else if (pathname === '/users') handleUsers(req, res);
  else res.statusCode = 404;
});

3. 支持 RESTful API

返回 JSON 而非 HTML,适配前后端分离架构:

if (pathname === '/api/users') {
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify(users));
}

4. 添加 CORS 头

允许跨域请求(前端开发必备):

res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');

5. 端口常量化

const PORT = 1314;
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));

🧪 测试与部署

运行服务器

node server.js
# 输出: Server is running on port 1314

浏览器访问

  • http://localhost:1314 → 显示用户表格
  • http://localhost:1314/users → 同上
  • http://localhost:1314/abc → 404 页面

日志监控

console.log(parsedUrl) 可输出每次请求的详细信息,便于调试。


📌 总结

从模块化思想到 Node.js HTTP 服务器的逐层实现,我们见证了如何将理论转化为可运行的代码。模块化不仅解决了代码组织、复用、维护等根本问题,更为现代工程化开发铺平道路。而 Node.js 凭借其简洁的 API 和事件驱动模型,让开发者能快速构建轻量级 Web 服务。

尽管本文示例仅为入门级别,但它涵盖了:

  • ✅ CommonJS 模块机制
  • ✅ HTTP 请求/响应处理
  • ✅ URL 路由解析
  • ✅ 动态 HTML 生成
  • ✅ 错误处理(404)
  • ✅ 代码结构优化方向

这些正是构建 Express、Koa 等框架的底层基石。掌握这些核心概念,方能在全栈开发之路上行稳致远。🚀

告别黑盒!手写Windows版简易NodeMON,学习文件监听代码修改与进程服务重启知识


为什么要手写 NodeMON?

在开发 Node.js 应用时,我们经常用 nodemon 来自动重启服务。但你有没有想过:

  • 🤔 它是如何监听文件变化的?
  • 🤔 它是如何优雅地杀掉进程并重启新进程的?

今天我们就从零开始,手写一个 Windows 专属的简易 NodeMON 工具,彻底搞懂背后的原理!

node index.js的执行过程

比如我使用express编写一个简单的服务代码

const express = require('express'); 
const app = express();

app.get('/', (req, res) => { res.send('Hello World!'); });

const server = app.listen(6666, () => { 
    console.log('Express 服务已启动,进程 ID:', process.pid); // 打印当前进程ID 
    console.log('服务监听 http://localhost:6666'); 
});

当我们执行 node index.js 时:

  • 操作系统会在环境变量的目录里面逐个查找,并找到node的可执行程序,比如位置在:C:\Program Files\nodejs 111.png
  • 当操作系统找到这个可执行程序后,会安排一个进程去加载并执行这个node.exe程序,再把index.js文件作为参数交给node.exe去解析并运行
  • 在这个新进程的内部,Nodejs会去执行index.js的代码,最终会启动一个Express服务,这个服务使用的是6666端口
  • 只要这个进程不终止(比如你按 Ctrl+C、杀死进程、服务器重启),6666 端口就会一直被占用,服务就一直可用。

通俗易懂地类比理解

操作系统是 “工厂小老板”,进程是驾驶机器的一个个的员工,而node.exe 则是 “一台机器”,index.js 是 “机器要处理的任务单”

  • 操作系统 = 工厂小老板负责统筹全局,决定要不要招新员工(创建进程)、给员工分配机器(运行可执行程序)、下发任务单(传递参数),还能随时监督员工工作状态(查看进程 PID、占用资源),或者让员工下班(终止进程)。

  • 进程 = 驾驶机器的员工是老板(系统)专门招来的 “专人”,有自己的唯一工号(PID),他的核心工作就是操作手里的机器,全程只围绕这个机器和任务转,不会同时干别的活。

  • node.exe = 员工驾驶的机器是员工的 “工具”,本身有固定的功能(JavaScript 解释执行能力),没有员工(进程)操作的话,它就是一台闲置的机器,啥也干不了。

  • index.js = 机器要处理的任务单上面写着具体的工作内容(比如 “启动 Express 服务、监听 3000 端口”),员工(进程)操作机器(node.exe)时,就照着任务单的要求一步步执行。

至于线程,我们知道进程中可以创建多个线程(可以理解为一个操作机器的员工,可以长出多只手,两只手干活总会比一只手干活快)

员工常常用一只手干活(进程中的主线程干活)

说到这里,就要额外提一下Process这个变量了

process就相当于node.exe这个机器上面的控制显示屏,记录了一些信息,提供给外部方便使用

控制显示屏(process)的功能 对应工厂场景 代码示例
显示 “员工工号(PID)” 屏幕上显示当前操作机器的员工编号 process.pid → 输出 39900
显示 “机器运行参数” 屏幕显示机器接收到的任务单(index.js)、启动指令 process.argv → 输出 ['node', 'index.js']
显示 “机器资源占用” 屏幕显示机器当前用了多少内存、CPU process.memoryUsage() → 输出内存占用数据
提供 “关机按钮” 屏幕上的 “停止运行” 按钮,按了员工就下班 process.exit() → 终止当前进程
显示 “工厂环境” 屏幕显示老板(系统)给的环境变量(比如 PATH) process.env → 输出系统环境变量
  • 比如,我可以打印进程id就是
  • console.log('进程 ID:', process.pid) // 得到39900这个值 是40188当然每次一般都不一样

cmd命令:

tasklist | findstr 39900 查看进程39900在使用哪一台机器——使用node.exe这个机器

C:\Users\lss13>tasklist | findstr 39900
node.exe                     39900 Console                    1     25,176 K

netstat -ano | findstr :6666 查看6666端口,被那个进程使用——被39900进程使用

C:\Users\lss13>netstat -ano | findstr :6666
  TCP    0.0.0.0:6666           0.0.0.0:0              LISTENING       39900
  TCP    [::]:6666              [::]:0                 LISTENING       39900

一、原理讲解

有了前置的知识后,我们来梳理一下手写启动监控工具的思路

核心流程

启动监控工具
    ↓
监听文件变化(chokidar)
    ↓
检测到修改 → 计算文件 Hash
    ↓
Hash 变了?
    ├─ 是 → 杀掉旧进程(taskkill)→ 等待端口释放 → 启动新进程
    └─ 否 → 忽略(避免无意义重启)

关键技术点

chokidar:www.npmjs.com/package/cho…

技术 作用 Windows 特殊处理
chokidar 监听文件变化 可选轮询模式(更稳定)
spawn 启动/管理子进程 需要处理进程树
taskkill 杀死进程 Windows 专属命令
crypto 计算文件 Hash 精确判断内容是否变化

二、代码实现

项目结构

myNodeMon/
├── package.json    # 项目配置
├── nmon.js         # 监控工具(核心)
└── index.js        # 业务代码(HTTP 服务器)

第一步:初始化项目

# 创建项目目录
mkdir myNodeMon
cd myNodeMon

# 初始化 package.json
npm init -y

# 安装依赖
npm install chokidar

第二步:配置 package.json

{
  "name": "mynodemon",
  "version": "1.0.0",
  "type": "module",
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "chokidar": "^5.0.0"
  }
}

关键配置:

  • "type": "module":启用 ES Module 语法

第三步:编写业务代码(index.js)

这是我们要监控的目标文件index.js,一个简单的 HTTP 服务器:

import http from 'http';

// 创建http服务器
const server = http.createServer((_req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`
    当前时间:${new Date().toLocaleTimeString()}
  `);
});

console.log(666)

// 启动服务器
server.listen(6666, () => {
    console.log(`Node.js 服务已启动:http://localhost:6666`);
});

功能:

  • 监听 6666 端口
  • 返回当前时间

第四步:编写监控工具(nmon.js)

这是核心代码,我们逐块解析:

导入依赖

import chokidar from 'chokidar'; // 文件监听库
import { spawn } from 'child_process'; // 子进程管理
import path from 'path';
import crypto from 'crypto'; // Hash 计算
import fs from 'fs'; // 文件读取

配置项

// ===================== 配置项 =====================
const TARGET_FILE = 'index.js'; // 要监控的文件
// =================================================

const entryPath = path.resolve(process.cwd(), TARGET_FILE); // 获取绝对路径
  • process.cwd():当前工作目录(运行命令的位置)
  • path.resolve():拼接成绝对路径,如 C:\Users\xxx\myNodeMon\index.js

Hash 计算

let lastHash = null; // 保存上次的 hash

function getFileHash(filePath) {
    const content = fs.readFileSync(filePath);
    return crypto.createHash('md5').update(content).digest('hex');
}

为什么要用 Hash?

方案 问题
只检测修改时间 Ctrl+S 不修改内容也会触发重启 ❌
Hash 对比 只有内容真正变化才重启 ✅

当我们在编辑器里面 Ctrl+S 的时候,尽管没有修改文件,但是操作系统依旧认为这个文件变化了,也会触发文件变化回调函数,即文件的时间会变化

工作原理:

  1. 读取文件内容
  2. 计算 MD5 哈希值(如 a1b2c3d4...
  3. 对比上次的哈希值
  4. 不同才重启

进程引用管理

let childProcess = null; // 保存进程实例

// 启动或重启 index.js
function startApp() {
    if (childProcess) {
        console.log('🔄 正在终止旧进程...');

        // Windows 下用 taskkill 强制杀死进程树
        const killProcess = spawn('taskkill', ['/F', '/T', '/PID', childProcess.pid]);

        killProcess.on('close', () => {
            childProcess = null; // 清空引用
            setTimeout(() => {
                launchNewProcess();
            }, 200); // 等待端口释放
        });
        return;
    }
    launchNewProcess();
}
参数 含义
/F 强制终止
/T 终止进程树(包括子进程)
/PID 按进程 ID 杀死

为什么要延迟 200ms?

taskkill 完成 → 进程退出 → 操作系统释放端口 → 新进程启动
                              ↑
                        这里需要时间(约 50-150ms)

如果不等待,新进程会报错:EADDRINUSE: address already in use


启动新进程

function launchNewProcess() {
    console.log('🚀 正在启动新进程...\n');

    // 相当于执行:node index.js
    childProcess = spawn('node', [entryPath], {
        stdio: 'inherit' // 让子进程的日志直接显示在控制台
    });

    childProcess.on('error', (err) => {
        console.error('❌ 启动失败:', err.message);
    });
}

stdio: 'inherit':继承父进程的输入输出,让 index.js 的日志能显示出来


文件变化监听

// 创建文件监听器
const watcher = chokidar.watch(TARGET_FILE, {
    ignoreInitial: true  // 忽略初始化时的事件
});

watcher.on('change', () => {
    const currentHash = getFileHash(TARGET_FILE);
    
    if (currentHash !== lastHash) {
        console.log('📝 检测到内容真的变了');
        lastHash = currentHash;
        startApp();
    } else {
        console.log('⏭️ 内容没变,忽略');
    }
});

// 初始化时计算一次
lastHash = getFileHash(TARGET_FILE);

流程

  1. 监听 index.jschange 事件
  2. 计算当前文件的 Hash
  3. 对比上次的 Hash
  4. 不同才调用 startApp() 重启

启动监控

console.log(`🚀 Windows 监控工具已启动`);
console.log(`🎯 监控目标:${TARGET_FILE}\n`);
startApp();

优雅退出(Ctrl+C)

process.on('SIGINT', () => {
    if (childProcess) {
        console.log('\n🛑 正在清理子进程...');
        spawn('taskkill', ['/F', '/T', '/PID', childProcess.pid]);
        childProcess = null;
    }
    watcher.close();
    console.log('👋 监控工具已退出');
    process.exit(0);
});
  • 按 Ctrl+C 时清理子进程
  • 关闭文件监听器
  • 避免端口残留占用

三、测试验证效果

测试场景 0:启动服务并访问 测试场景 1:修改文件内容 测试场景 2:不修改内容,只保存 测试场景 3:优雅退出

效果图

gif.gif


四、踩坑

为什么 Windows 需要特殊处理?

问题 1:文件监听不稳定

Linux/Mac:

  • 使用 inotify / FSEvents(内核级别)
  • 高效、准确、实时

Windows:

  • 使用 ReadDirectoryChangesW(基于目录扫描)
  • 容易丢失事件或重复触发
  • 容易丢失事件或重复触发
  • 容易丢失事件或重复触发

特别是快速Ctrl + S保存,可能会事件误触发...

解决方案可选:

// 可选:使用轮询模式(更稳定但耗资源)
const watcher = chokidar.watch(TARGET_FILE, {
    ignoreInitial: true,
    usePolling: true,     // 强制轮询
    interval: 500         // 每 500ms 检查一次
});

问题 2:进程杀不干净

Linux/Mac:

kill -9 <PID>  # 直接杀进程

Windows:

taskkill /F /T /PID <PID>  # 需要杀进程树

为什么要加 /T

父进程(node nmon.js)
  └─ 子进程(node index.js)
       └─ 可能还有孙进程

不加 /T 只杀父进程,子进程会变成孤儿进程,继续占用端口。


为什么要保存进程实例?

let childProcess = null;

作用:

  1. 获取进程 PID(childProcess.pid
  2. 在重启时杀掉旧进程
  3. 监听进程状态(退出、错误)

如果不保存:

// ❌ 错误示范
spawn('node', ['index.js']); // 启动了,但没人记住它

// 想重启时
spawn('taskkill', ['/PID', ???]); // 不知道 PID,无法杀进程

五、完整代码

GitHub

仓库地址:github.com/shuirongshu…

nmon.js

import chokidar from 'chokidar'; // 监控包
import { spawn } from 'child_process'; // 派发生系统命令来创建和终止子进程,实现启动和重启
import path from 'path';
import crypto from 'crypto';
import fs from 'fs';

// ===================== 配置项 =====================
const TARGET_FILE = 'index.js'; // 要监控并自动重启的文件
// =================================================

const entryPath = path.resolve(process.cwd(), TARGET_FILE); // 路径

let childProcess = null; // 保存 index.js 的进程实例对象的引用,便于后续清空重置

let lastHash = null; // 保存上次的 hash

function getFileHash(filePath) {
    const content = fs.readFileSync(filePath);
    return crypto.createHash('md5').update(content).digest('hex');
}

// 启动或重启 index.js
function startApp() {
    // 当前有进程,就清除掉以后,再启动(重启功能)
    if (childProcess) {
        console.log('🔄 正在终止旧进程...');

        // Windows 下用 taskkill命令 强制杀死进程树 // 比如类似 taskkill /f /t /im nginx.exe
        const killProcess = spawn('taskkill', ['/F', '/T', '/PID', childProcess.pid]);

        killProcess.on('close', () => {
            childProcess = null; // 清空进程实例对象的引用
            setTimeout(() => { // 等200毫秒足够操作系统释放端口了
                launchNewProcess();
            }, 200);
        });
        return;
    }
    // 当前没有进程就直接启动即可
    launchNewProcess();
}

// 启动新进程
function launchNewProcess() {
    console.log('🚀 正在启动新进程...\n');

    // 相当于执行命令:node C:\Users\xxx\myNodeMon\index.js 简化就是 node index.js
    childProcess = spawn('node', [entryPath], {
        stdio: 'inherit' // 让子进程的输出日志,直接显示在当前控制台
    });

    // 比如文件不存在或者路径错误会报错,兜一下
    childProcess.on('error', (err) => {
        console.error('❌ 启动失败:', err.message);
    });
}

// 创建文件监听器——不使用轮询
const watcher = chokidar.watch(TARGET_FILE, {
    ignoreInitial: true  // 忽略初始化时的事件
});

// // 创建文件监听器——使用轮询
// const watcher = chokidar.watch(TARGET_FILE, {
//     ignoreInitial: true,  // 忽略初始化时的事件
//     usePolling: true,     // Windows下用轮询更加稳妥(毕竟其文件管理没有Linux做得好)
//     interval: 1000         // 每 100ms 检查一次文件变化
// });

watcher.on('change', () => {
    const currentHash = getFileHash(TARGET_FILE);
    
    if (currentHash !== lastHash) {
        console.log('📝 检测到内容真的变了');
        lastHash = currentHash;
        startApp();
    } else {
        console.log('⏭️ 内容没变,忽略');
    }
});

// 初始化时计算一次
lastHash = getFileHash(TARGET_FILE);

// 启动监控
console.log(`🚀 Windows 监控工具已启动`);
console.log(`🎯 监控目标:${TARGET_FILE}\n`);
startApp();

// Ctrl+C 退出时清理进程
process.on('SIGINT', () => {
    if (childProcess) {
        console.log('\n🛑 正在清理子进程...');
        spawn('taskkill', ['/F', '/T', '/PID', childProcess.pid]);
        childProcess = null;
    }
    watcher.close();
    console.log('👋 监控工具已退出');
    process.exit(0);
});

六、总结

学到了什么?

  • 通俗易懂理解一些基础概念
  • chokidar 的使用
  • Windows 下文件系统还是差点意思(稳定性问题)
  • spawn() 启动子进程
  • taskkill 杀死进程树
  • 通过文件hash精确判断文件内容是否变化
  • 信号处理(SIGINT

与 nodemon 的对比

特性 我们的工具 nodemon
文件监听
自动重启
Hash 对比
配置文件
跨平台 ❌(仅 Windows)
日志记录
代码量 ~100 行 ~5000 行

七、拓展思考

🤔 如何监听多个文件?

提示:

const TARGET_FILES = ['index.js', 'config.js'];
const watcher = chokidar.watch(TARGET_FILES, { ... });

// 需要为每个文件保存 Hash
const fileHashes = new Map();

🤔 如何添加日志输出?

提示:

import fs from 'fs';

function log(message) {
    const timestamp = new Date().toISOString();
    const logMessage = `[${timestamp}] ${message}\n`;
    fs.appendFileSync('nmon.log', logMessage);
    console.log(message);
}

🤔 如何支持配置文件?

提示:

// nmon.config.json
{
  "target": "index.js",
  "port": 6666,
  "delay": 200
}

// 读取配置
const config = JSON.parse(fs.readFileSync('nmon.config.json', 'utf8'));

🤔 如何实现热重载(不重启进程)?

提示:

  • 使用 vm 模块动态加载代码
  • 或者使用 WebSocket 通知浏览器刷新

八、参考资料


JavaScript Date 语法要过时了!以后用这个替代!

1. 前言

作为一名前端开发工程师,你一定被 JavaScript 的日期处理折磨过。

这不是你的问题,是 JavaScript 自己的问题——它的 Date 功能真的很糟糕。

2. Date 的离谱行为

让我给你举几个例子,你就明白有多离谱了:

月份从 0 开始计数:

// 你以为这是 2026 年 1 月 1 日?
console.log(new Date(2026, 1, 1));
// 结果:2026 年 2 月 1 日!

// 因为月份是从 0 开始数的:0=1月,1=2月...
// 但年份和日期又是正常计数的

日期格式混乱到让人抓狂:

// 用斜杠分隔,加不加前导零都没问题
console.log(new Date("2026/01/02"));
// Fri Jan 02 2026 00:00:00 GMT+0800 (中国标准时间)

// 但如果用短横线分隔,同样的写法
console.log(new Date("2026-01-02"));
// Fri Jan 02 2026 08:00:00 GMT+0800 (中国标准时间)

// 时间居然不一样了!

// 如果用东半球标准时间,更离谱!一个是 1 月 2 日,一个是 1 月 1 日

两位数年份的迷惑行为:

console.log(new Date("49")); // 2049 年
console.log(new Date("99")); // 1999 年
console.log(new Date("100")); // 公元 100 年!

规则莫名其妙:33-99 代表 1900 年代,但 32-49 又代表 2000 年代,100 以上就真的是公元那一年了。

更致命的问题是 —— 日期居然可以被“改变”!

const today = new Date();
console.log(today.toDateString()); // Fri Jan 09 2026

// 我想算一下明天是几号
const addDay = (theDate) => {
  theDate.setDate(theDate.getDate() + 1);
  return theDate;
};

console.log(`明天是 ${addDay(today).toLocaleDateString()}。`);
// 明天是 2026/1/10。

console.log(`今天是 ${today.toLocaleDateString()}。`);
// 今天是 2026/1/10。

// 等等,今天怎么也变成明天了?!

当然这是可以解释的:

因为 today 就像一个地址,指向内存里的某个位置。当你把 today 传给函数时,函数拿到的也是这个地址。所以当函数修改日期时,原来的 today 也被改了。

但这种设计违反了一个基本常识:日期应该是固定的。“2026 年 1 月 10 日”就是“2026 年 1 月 10 日”,不应该因为你拿它做了个计算,它自己就变了。

所以 Date 真的很糟糕。实际上,它就是挂羊头卖狗肉,它叫做 Date,表示日期,实际上,它是时间。

在内部,Date 是以数值形式存储的,这就是我们熟悉的以 1000 毫秒为单位的时间戳。

时间当然包含日期,你可以从时间中推断出日期,但这多少有点恶心了。

Java 早在 1997 年就弃用了其 Date 类,而 JavaScript 的 Date 类仅仅在几年后就问世了;与此同时,我们却一直被这个烂摊子困扰着。

正如你目前所见,它在解析日期方面极其不稳定。它除了本地时间和格林威治标准时间 (GMT) 之外,对其他时区一无所知。而且,Date 类只支持公历。它完全不理解夏令时的概念。当然最糟糕的还是它的可变的,这直接让他偏离了时间的本质。

所有这些缺陷使得使用第三方库来解决这些问题变得异常普遍,其中一些库体积庞大,这种性能损耗已经对网络造成了切实可衡量的损害。

3. Temporal 才是未来

幸运的是,Date 即将彻底退出历史舞台。

当然这样说,还是有点夸张了。

实际上是它会一直存在,但如果可以避免,你最好不要再用它了。

因为我们会有一个完全取代 Date 的对象 —— Temporal。

部分同学可能对 Temporal 这个单词不太熟悉,实际上,它的意思就是“时间”,你可以理解为它是一个更专业的词汇:

与 Date 不同,Temporal 不是构造函数,它是一个命名空间对象——一个由静态属性和方法组成的普通对象,就像 Math 对象一样:

console.log(Temporal);
/* Result (expanded):
Temporal { … }
  Duration: function Duration()
  Instant: function Instant()
  Now: Temporal.Now { … }
  PlainDate: function PlainDate()
  PlainDateTime: function PlainDateTime()
  PlainMonthDay: function PlainMonthDay()
  PlainTime: function PlainTime()
  PlainYearMonth: function PlainYearMonth()
  ZonedDateTime: function ZonedDateTime()
  Symbol(Symbol.toStringTag): "Temporal"
*/

Temporal 包含的类和命名空间对象允许你计算两个时间点之间的持续时间、表示一个时间点(无论是否具有时区信息)、通过 Now 属性访问当前时间点等。

如果我们要获取当前时间:

console.log(Temporal.Now.plainDateISO());
/* Result (expanded):
Temporal.PlainDate 2025-12-31
  <prototype>: Object { … }
*/

该方法返回的是当前时区的今天日期。

Temporal 还能支持时区:

const date = Temporal.Now.plainDateISO();

// 指定这个日期在伦敦时区
console.log(date.toZonedDateTime("Europe/London"));

Temporal 还可以计算日期差:

const today = Temporal.Now.plainDateISO();
const jsShipped = Temporal.PlainDate.from("1995-12-04"); // JavaScript 发布日期
const difference = today.since(jsShipped, { largestUnit: "year" });

console.log(`JavaScript 已经存在了 ${difference.years}${difference.months} 个月零 ${difference.days} 天。`);

各种时间操作也会更加直观:

const today = Temporal.Now.plainDateISO();

// 加一天
console.log(today.add({ days: 1 }));

// 加一个月零一天,再减两年——可以链式操作
console.log(today.add({ months: 1, days: 1 }).subtract({ years: 2 }));

// 看,多清楚!

当然,更重要的是,日期不会被意外修改

const today = Temporal.Now.plainDateISO();

// 计算明天的日期
const tomorrow = today.add({ days: 1 });

console.log(`今天是 ${today}。`); // 2025-12-31
console.log(`明天是 ${tomorrow}。`); // 2026-01-01

// 今天还是今天,完美!

add 方法会返回一个新的日期对象,而不是修改原来的。就像你复印了一份日历,在复印件上写字,原件不会被弄脏。

4. 什么时候能用?

好消息:最新版的 Chrome 和 Firefox 已经支持了!

坏消息:它还在“实验阶段”,这意味着具体用法可能还会微调,但大方向已定。

我们终于要和 Date 的噩梦说再见了。

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs) ,每天分享前端知识、AI 干货。

❌