普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月20日掘金 前端

【Gemini简直无敌了】掌间星河:通过MediaPipe实现手势控制粒子

2025年12月20日 17:15

受掘金大佬“不如摸鱼去”的启发,我也试试 Gemini 3做一下手势+粒子交互, 确实学到了不少东西,在这里简单的分享下。 github地址:掌间星河:github.com/huijieya/Ge…

在这里插入图片描述

基于原生h5、浏览器、PC摄像头实现手势控制粒子特效交互的逻辑,粒子默认离散,类似银河系分布缓慢移动,同时有5种手势:

手势1: 握拳,握拳后粒子聚拢显示爱心的形状

手势2: 展开手掌并挥手,展开手掌挥手后粒子从当前状态恢复到离散状态

手势3: 👆 比 1 :只有食指伸直,其他 3 根弯曲,此时粒子聚拢显示第一句话:春来夏往

手势4: ✌ 比 2 :只有食指和中指伸直,其他 2 根弯曲手此时粒子聚拢显示第二句话: 秋收冬藏

手势5: 👌 比 3 :只有中指、无名指、小指伸直,食指弯曲,此时粒子聚拢显示第三句话:我们来日方长

效果展示

在这里插入图片描述

源码地址

掌间星河:github.com/huijieya/Ge…

源码分析

手势识别流程

1. 手部检测初始化

// HandTracker.tsx 中初始化 MediaPipe Hands
const hands = new (window as any).Hands({
  locateFile: (file: string) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
});

hands.setOptions({
  maxNumHands: 1,           // 最多检测一只手
  modelComplexity: 1,       // 模型复杂度
  minDetectionConfidence: 0.7,  // 最小检测置信度
  minTrackingConfidence: 0.7    // 最小跟踪置信度
});

2. 手部关键点获取

MediaPipe 会检测手部的21个关键点(landmarks),每个关键点包含 x, y, z 坐标:

  • 指尖: 4(拇指), 8(食指), 12(中指), 16(无名指), 20(小指)
  • 指关节: 用于判断手指是否伸直

3. 手势分类逻辑

// gestureLogic.ts 中的 classifyGesture 函数
const isExtended = (tipIdx: number, mcpIdx: number) => landmarks[tipIdx].y < landmarks[mcpIdx].y;

// 判断各手指是否伸直
const indexExt = isExtended(8, 5);   // 食指
const middleExt = isExtended(12, 9); // 中指
const ringExt = isExtended(16, 13);  // 无名指
const pinkyExt = isExtended(20, 17); // 小指

// 根据手指状态识别不同手势
if (!indexExt && !middleExt && !ringExt && !pinkyExt) {
  return GestureType.HEART; // 握拳 - 显示爱心
}
if (indexExt && middleExt && ringExt && pinkyExt) {
  return GestureType.GALAXY; // 手掌展开 - 银河状态
}
// ... 其他手势判断

粒子绘制机制

1. 粒子系统初始化

// ParticleCanvas.tsx 中初始化粒子
useEffect(() => {
  const particles: Particle[] = [];
  for (let i = 0; i < PARTICLE_COUNT; i++) {
    particles.push({
      x: Math.random() * window.innerWidth,     // 随机初始位置
      y: Math.random() * window.innerHeight,
      targetX: Math.random() * window.innerWidth, // 目标位置
      targetY: Math.random() * window.innerHeight,
      vx: 0,                                    // 速度
      vy: 0,
      size: Math.random() * 1.5 + 0.5,          // 大小
      color: COLORS[Math.floor(Math.random() * COLORS.length)], // 颜色
      alpha: Math.random() * 0.4 + 0.4,         // 透明度
    });
  }
  particlesRef.current = particles;
}, []);

2. 形状生成算法

const getShapePoints = (type: GestureType, width: number, height: number): Point[] => {
  const centerX = width / 2;
  const centerY = height / 2;
  
  switch (type) {
    case GestureType.HEART: 
      // 心形方程参数化生成点
      // x = 16sin³(t)
      // y = 13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t)
      
    case GestureType.TEXT_1/2/3:
      // 使用 Canvas 绘制文字并提取像素点
      
    case GestureType.GALAXY:
    default:
      // 螺旋银河形状
      const angle = Math.random() * Math.PI * 2;
      const r = Math.pow(Math.random(), 0.7) * maxRadius;
      const spiralFactor = 2.0;
      const offset = r * (spiralFactor / maxRadius) * 5;
      points.push({ 
        x: centerX + Math.cos(angle + offset) * r, 
        y: centerY + Math.sin(angle + offset) * r 
      });
  }
}

3. 粒子动画更新

// 粒子运动和渲染循环
const render = () => {
  // 半透明背景覆盖产生拖尾效果
  ctx.fillStyle = 'rgba(0, 0, 0, 0.18)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  particlesRef.current.forEach((p) => {
    // 平滑插值移动到目标位置
    p.x += (tx - p.x) * LERP_FACTOR;
    p.y += (ty - p.y) * LERP_FACTOR;
    
    // 手势交互影响粒子位置
    if (hPos && canInteract) {
      const dx = p.x - hPos.x;
      const dy = p.y - hPos.y;
      const distSq = dx * dx + dy * dy;
      if (distSq < INTERACTION_RADIUS * INTERACTION_RADIUS) {
        // 排斥力计算
        const dist = Math.sqrt(distSq);
        const force = (1 - dist / INTERACTION_RADIUS) * INTERACTION_STRENGTH;
        p.x += dx * force;
        p.y += dy * force;
      }
    }
    
    // 添加随机扰动使粒子更生动
    p.x += (Math.random() - 0.5) * 0.6;
    p.y += (Math.random() - 0.5) * 0.6;
    
    // 绘制粒子
    ctx.fillStyle = p.color;
    ctx.globalAlpha = p.alpha;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
    ctx.fill();
  });
  
  animationRef.current = requestAnimationFrame(render);
};

4. 银河旋转效果

// 银河状态下的旋转动画
galaxyAngleRef.current += GALAXY_ROTATION_SPEED;
const cosA = Math.cos(galaxyAngleRef.current);
const sinA = Math.sin(galaxyAngleRef.current);

// 对每个粒子应用旋转变换
const dx = p.targetX - cx;
const dy = p.targetY - cy;
tx = cx + dx * cosA - dy * sinA;
ty = cy + dx * sinA + dy * cosA;

脚手架开发工具——dotenv

作者 code_YuJun
2025年12月20日 16:58

简介

dotenv 是一个轻量级的 Node.js 环境变量管理工具,其核心作用是:从项目根目录的 .env 文件中加载自定义的环境变量,并将它们注入到 Node.js 的 process.env 对象中,使得我们可以在项目代码中统一通过 process.env.XXX 的方式获取这些环境配置,无需手动在系统环境中配置临时变量或永久变量。

核心工作原理

  1. 当在项目中引入并执行 dotenv 时,它会自动查找项目根目录下的 .env 文件(该文件为纯文本格式,采用键值对配置);
  2. 它会解析 .env 文件中的每一行配置(格式通常为 KEY=VALUE);
  3. 将解析后的键值对逐一挂载到 Node.js 内置的 process.env 对象上(process.env 原本用于存储系统级环境变量,dotenv 为其扩展了项目自定义环境变量);
  4. 之后在项目的任意代码文件中,都可以通过 process.env.KEY 的形式获取对应的值。

使用示例

npm install dotenv --save
# .env 文件内容 当前用户路径下创建 `.env` 文件
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=123456
API_KEY=abcdefg123456
    const dotenv = require('dotenv');
    const dotenvPath = path.resolve(userHome, '.env'); // /Users/***/.env
    if (pathExists(dotenvPath)) {
        dotenv.config({
            path: dotenvPath,
        });
    }

vscode没有js提示:配置jsconfig配置

作者 Mr_Swilder
2025年12月20日 16:44

jsconfig.json是一个配置文件,它的核心作用是告诉 Visual Studio Code(VSCode)当前目录是一个 JavaScript 项目的根目录,从而为你的代码提供更强大的智能感知(IntelliSense)和语言支持。

下面这个表格概括了它的主要作用:

核心作用 解决的问题 简单示例
定义项目上下文 没有它时,VSCode 将每个 JS 文件视为独立单元,文件间缺乏关联性。有了它,VSCode 能将整个项目作为一个整体理解。 {}(一个空文件即可定义项目)
配置路径别名映射 当项目使用像 @这样的别名来代表 src目录时,VSCode 默认无法识别。配置后,可以实现路径的自动补全和点击跳转 "paths": { "@/*": ["src/*"] }
提升 IDE 性能 通过排除不必要的文件(如 node_modules, dist),让语言服务专注于源代码,避免 IntelliSense 变慢 "exclude": ["node_modules", "dist"]
调整语言服务选项 配置 JavaScript 的语言检查标准,例如启用实验性语法支持(如装饰器)或指定 ECMAScript 目标版本。 "experimentalDecorators": true

💡 详细解读与配置

  • 定义项目上下文:在没有 jsconfig.json的“文件范围(File Scope)”模式下,VSCode 虽然能为单个文件提供基础语法高亮,但难以准确分析文件之间的模块引用关系。创建 jsconfig.json后,项目进入“显式项目(Explicit Project)”模式,VSCode 的语言服务能理解项目的整体结构,从而提供更精确的代码补全、类型推断和错误检查。

  • 配置路径映射(Paths Mapping) :这是在前端项目中非常实用的功能。许多项目使用 Webpack 或 Vite 等构建工具配置了路径别名,但在代码编辑器中,这些别名默认无法被识别。通过在 jsconfig.json中配置 paths,即可让 VSCode 理解这些别名。

    {
      "compilerOptions": {
        "baseUrl": "./", // 设置基础目录
        "paths": {
          "@/*": ["src/*"],    // 将 @ 映射到 src 目录
          "components/*": ["src/components/*"] // 配置其他别名
        }
      }
    }
    

    配置后,当你输入 import App from '@/App',VSCode 就能知道 @指向 src目录,并提供自动补全和跳转功能。

  • 优化性能(Exclude) :JavaScript 语言服务会分析项目中的文件来提供 IntelliSense。如果它去解析庞大的 node_modules或构建输出的 dist目录,会严重拖慢速度。使用 exclude属性可以告诉语言服务忽略这些目录。

    {
      "exclude": ["node_modules", "dist", "build", "*.min.js"]
    }
    

🛠️ 创建与配置示例

你可以在项目的根目录下创建一个名为 jsconfig.json的文件。一个适用于现代前端项目(如 Vue、React)的常见配置如下:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "node",
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "lib": ["esnext", "dom", "dom.iterable"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "build"]
}

配置项说明

  • compilerOptions:虽然名字叫“编译选项”,但它主要用于配置 VSCode 的 JavaScript 语言服务行为,因为 jsconfig.json源自 TypeScript 的 tsconfig.json
  • include:明确指定哪些文件属于项目。如果未设置,则默认包含所有子目录下的文件。
  • exclude:指定要排除的文件和文件夹。

💎 总结

总而言之,jsconfig.json就像是你在 VSCode 中的项目地图和说明书。它通过定义项目根目录、映射路径别名、排除无关文件等方式,显著提升了代码编辑的智能体验、导航效率和整体性能。对于任何有一定规模的 JavaScript/TypeScript 项目,配置一个 jsconfig.json都是非常值得的。

希望这些信息能帮助你更好地理解和使用 jsconfig.json。如果你在配置过程中遇到具体问题,例如如何为特定框架进行优化,我很乐意提供进一步的帮助。

Koa 源码深度解析:带你理解 Koa 的设计哲学和核心实现原理

2025年12月20日 16:42

Koa 源码深度解析:带你理解 Koa 的设计哲学和核心实现原理

注:本文只讲koa源码与核心实现,无应用层相关知识

一、Koa 的设计哲学

1.1 什么是koa

Koa 是由 Express 原班人马打造的下一代 Node.js Web 框架。相比于 Express,Koa 利用 ES2017 的 async/await 特性,让异步代码的编写变得更加优雅和可维护。本文将深入解析 Koa 的核心源码,帮助你理解其设计哲学和实现原理。

1.2 核心设计理念

Koa 的设计理念可以概括为:

// Koa 应用本质上是一个包含中间件函数数组的对象
// 这些中间件以类似栈的方式组合和执行
const Koa = require('koa');
const app = new Koa();

// 中间件以"洋葱模型"方式执行
app.use(async (ctx, next) => {
  // 请求阶段(向下)
  await next();
  // 响应阶段(向上)
});

官方文档这样描述:

A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request.

二、源码核心流程解析

在深入源码之前,我们先理解一下 Koa 应用从创建到处理请求的完整生命周期。一个典型的 Koa 应用会经历以下几个阶段:

  1. 创建应用实例 - new Koa() 初始化应用对象
  2. 注册中间件 - app.use() 将中间件函数添加到数组
  3. 启动监听 - app.listen() 创建 HTTP 服务并开始监听
  4. 处理请求 - 当请求到来时,组合中间件并执行

接下来我们逐步剖析每个阶段的源码实现。

2.1 创建Application

当我们执行 const app = new Koa() 时,Koa 内部做了哪些初始化工作呢?让我们看看 Application 类的构造函数:

class Application {

  constructor (options) {
    ......
    options = options || {}
    this.compose = options.compose || compose // 组合中间件的函数,这是实现洋葱模型的关键
    this.middleware = []                      // 中间件数组,所有通过 use() 注册的中间件都会存储在这里
    this.context = Object.create(context)     // 上下文对象的原型,每个请求会基于它创建独立的 ctx
    this.request = Object.create(request)     // 请求对象的原型,封装了对 Node.js 原生 req 的访问
    this.response = Object.create(response);  // 响应对象的原型,封装了对 Node.js 原生 res 的访问
    ......
   }

为什么使用 Object.create() 而不是直接赋值?

这是一个非常巧妙的设计。使用 Object.create() 创建原型链,意味着:

  • 每个应用实例都有自己独立的 contextrequestresponse 对象
  • 这些对象继承自共享的原型,既节省内存又保证了隔离性
  • 可以在不同应用实例上挂载不同的扩展属性,互不影响

2.2 注册中间件

中间件是 Koa 的核心概念。通过 app.use() 方法,我们可以注册各种中间件来处理请求。让我们看一个实际例子:

// 请求日志中间件:记录请求的 URL 和响应时间
const logMiddleware = async (ctx, next) => {
  const start = Date.now();  // 记录开始时间
  await next();              // 等待后续中间件执行完毕
  const end = Date.now();    // 记录结束时间
  console.log(`${ctx.method} ${ctx.url} - ${end - start}ms`);
};

app.use(logMiddleware);

class Application {
  use(fn) {
    // 注册中间件:将中间件函数添加到数组末尾
    this.middleware.push(fn);
    return this; // 返回 this 支持链式调用
  }
}

use() 方法的设计亮点:

  1. 简单直接 - 只是将中间件函数 push 到数组,没有复杂的逻辑
  2. 链式调用 - 返回 this 使得可以连续调用 app.use().use().use()
  3. 顺序敏感 - 中间件的执行顺序取决于注册顺序,这对理解洋葱模型很重要

注意上面的日志中间件示例:await next() 是一个分水岭,它将中间件分为"请求阶段"和"响应阶段"。这正是洋葱模型的精髓所在。

2.3 创建context

每当有新的 HTTP 请求到来时,Koa 都会为这个请求创建一个全新的 context 对象。这个对象是 Koa 最重要的创新之一,它封装了 Node.js 原生的 reqres,提供了更加便捷的 API。

createContext(req, res) {
  // 基于应用的 context 原型创建新的 context 实例
  const context = Object.create(this.context);
  // 基于应用的 request 和 response 原型创建新的实例
  const request = context.request = Object.create(this.request);
  const response = context.response = Object.create(this.response);

  // 建立各对象之间的引用关系
  context.app = request.app = response.app = this;        // 都持有 app 实例的引用
  context.req = request.req = response.req = req;         // 都持有 Node.js 原生 req 的引用
  context.res = request.res = response.res = res;         // 都持有 Node.js 原生 res 的引用

  // 建立 context、request、response 之间的相互引用
  request.ctx = response.ctx = context;                   // request 和 response 都能访问 context
  request.response = response;                            // request 能访问 response
  response.request = request;                             // response 能访问 request

  return context;
}

这个方法的精妙之处:

  1. 原型继承 - 使用 Object.create() 确保每个请求都有独立的 context,但共享原型上的方法
  2. 四层封装 - contextrequest/responsereq/res,逐层抽象,提供更优雅的 API
  3. 相互引用 - 建立了复杂但合理的引用关系,使得在任何层级都能方便地访问其他对象
  4. 内存优化 - 通过原型链共享方法,避免每个请求都创建重复的方法副本

这样设计的好处是,在中间件中我们可以灵活地访问:

  • ctx.req / ctx.res - 访问 Node.js 原生对象
  • ctx.request / ctx.response - 访问 Koa 封装的对象
  • ctx.body / ctx.status - 使用 Koa 的便捷属性(代理到 response)

2.4 启动监听服务

当所有中间件注册完成后,我们需要启动 HTTP 服务器开始监听请求。

// 用户代码:启动服务器
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

// Koa 内部实现
class Application {
  listen (...args) {
    debug('listen')
    // 创建 Node.js HTTP 服务器,传入 callback 作为请求处理函数
    const server = http.createServer(this.callback())
    return server.listen(...args) // 返回 Node.js 的 http.Server 实例
  }

  // callback 方法返回一个符合 Node.js http.createServer 要求的请求处理函数
  callback() {
    // ⭐️ 核心:使用 compose 将所有中间件组合成一个函数
    // 这就是洋葱模型的实现入口!
    const fn = compose(this.middleware);

    // 如果没有监听 error 事件,添加默认的错误处理器
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    // 返回请求处理函数,Node.js 会在每次请求到来时调用它
    return (req, res) => {
      // 为这个请求创建独立的 context
      const ctx = this.createContext(req, res);
      // 执行组合后的中间件函数,传入 context
      return this.handleRequest(ctx, fn);
    };
  }
}

流程分解:

  1. app.listen() - 这只是对 Node.js 原生 API 的薄封装
  2. this.callback() - 这里是魔法发生的地方:
    • 调用 compose(this.middleware) 将所有中间件组合成一个函数
    • 返回一个闭包函数,每次请求时被调用
  3. 请求处理 - 当请求到来时:
    • 创建本次请求专属的 ctx 对象
    • 执行组合后的中间件函数 fn(ctx)
    • 所有中间件共享同一个 ctx

关键点:compose(this.middleware)

这行代码是理解 Koa 的关键。它将一个中间件数组:

[middleware1, middleware2, middleware3]

转换成一个嵌套的调用链:

middleware1(ctx, () => {
  middleware2(ctx, () => {
    middleware3(ctx, () => {
      // 最内层
    })
  })
})

这就是著名的"洋葱模型"的实现基础。接下来我们将深入剖析 compose 函数的源码。

三、洋葱模型:中间件的优雅编排

3.1 什么是洋葱模型?

Koa 的中间件执行机制被形象地称为"洋葱模型"。中间件的执行过程类似于剥洋葱:

  1. 请求阶段(外层到内层):从第一个中间件开始,遇到 await next() 就进入下一个中间件
  2. 响应阶段(内层到外层):最内层中间件执行完毕后,依次返回到外层中间件

3.2 compose 源码解析与实现

compose 函数是 koa-compose 包提供的,它是实现洋葱模型的核心。让我们先看看官方源码:

function compose(middleware) {
  // compose 返回一个函数,这个函数接收 context 和一个可选的 next
  return function (context, next) {
    let index = -1;  // 用于记录当前执行到第几个中间件

    // dispatch 函数负责执行第 i 个中间件
    function dispatch(i) {
      // 防止在同一个中间件中多次调用 next()
      // 如果 i <= index,说明 next() 被调用了多次
      if (i <= index) {
        return Promise.reject(new Error('next() 被多次调用'));
      }

      index = i; // 更新当前中间件索引,用于防止 next 被多次调用
      let fn = middleware[i];  // 获取当前要执行的中间件

      // 如果已经是最后一个中间件,fn 设为传入的 next(通常为 undefined)
      if (i === middleware.length) fn = next;
      // 如果 fn 不存在,说明已经到达末尾,返回一个 resolved 的 Promise
      if (!fn) return Promise.resolve();

      try {
        // ⭐️ 核心逻辑:执行当前中间件,并将 dispatch(i + 1) 作为 next 参数传入
        // 这样当中间件调用 await next() 时,实际上是在调用 dispatch(i + 1)
        // 从而递归地执行下一个中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }

    // 从第一个中间件开始执行
    return dispatch(0);
  };
}

这个函数的精妙之处:

  1. 闭包保存状态 - 通过闭包保存 index,防止 next() 被重复调用,这是一个重要的安全检查
  2. 递归调用链 - dispatch(i) 执行当前中间件,并将 dispatch(i + 1) 作为 next 传入
  3. Promise 包装 - 所有中间件都被包装成 Promise,支持 async/await 语法
  4. 懒执行 - 只有当中间件调用 await next() 时,下一个中间件才会执行

执行流程可视化:

假设有三个中间件 [m1, m2, m3]

dispatch(0) 执行 m1(ctx, dispatch(1))
  ↓
  m1 执行到 await next()
  ↓
  dispatch(1) 执行 m2(ctx, dispatch(2))
    ↓
    m2 执行到 await next()
    ↓
    dispatch(2) 执行 m3(ctx, dispatch(3))
      ↓
      m3 执行完毕
    ↓
    m2 的 next() 后的代码执行
  ↓
  m1 的 next() 后的代码执行

源码使用递归实现,初看可能有些难懂。没关系,下面我们来实现一个简化版本,帮助理解核心思想。

3.3 手写简易版 compose

核心思想:在当前中间件执行过程中,让 next() 函数能够自动执行下一个中间件,直到最后一个。

const compose = (middleware) => {
  const ctx = {};  // 创建一个上下文对象

  if (middleware.length === 0) {
    return;
  }

  let index = 0;
  const fn = middleware[index]; // 获取第一个中间件
  fn(ctx, next);                // 手动执行第一个中间件

  // 实现 next() 函数
  // 核心是在当前中间件执行过程中,获取下一个中间件函数并自动执行,直到最后一个
  async function next() {
    index++;  // 移动到下一个中间件

    // 如果已经是最后一个中间件,直接返回
    if (index >= middleware.length) {
      return;
    }

    const fn = middleware[index];  // 获取下一个中间件
    return await fn(ctx, next);    // 执行下一个中间件,并传入 next
  }
};

// 定义三个测试中间件
const middleware1 = (ctx, next) => {
  console.log(">> one");
  next();                 // 调用 next(),执行 middleware2
  console.log("<< one");
};

const middleware2 = (ctx, next) => {
  console.log(">> two");
  next();                 // 调用 next(),执行 middleware3
  console.log("<< two");
};

const middleware3 = (ctx, next) => {
  console.log(">> three");
  next();                 // 已经是最后一个,next() 直接返回
  console.log("<< three");
};

// 执行组合后的中间件
compose([middleware1, middleware2, middleware3]);

// 输出:
// >> one
// >> two
// >> three
// << three
// << two
// << one

关键理解点:

  1. 同步执行 - 当 middleware1 调用 next() 时,middleware2 会立即开始执行
  2. 栈式回溯 - 当 middleware3 执行完毕后,控制权会依次返回到 middleware2middleware1next() 之后
  3. 洋葱结构 - 这就形成了"进入"和"退出"两个阶段,像剥洋葱一样

执行顺序详解:

1. middleware1 开始执行 → 打印 ">> one"
2. middleware1 调用 next() → 暂停,进入 middleware2
3. middleware2 开始执行 → 打印 ">> two"
4. middleware2 调用 next() → 暂停,进入 middleware3
5. middleware3 开始执行 → 打印 ">> three"
6. middleware3 调用 next() → 返回(已是最后一个)
7. middleware3 继续执行 → 打印 "<< three"
8. middleware3 执行完毕 → 返回到 middleware2
9. middleware2 继续执行 → 打印 "<< two"
10. middleware2 执行完毕 → 返回到 middleware1
11. middleware1 继续执行 → 打印 "<< one"

建议: 使用 VSCode 的断点调试功能,在每个中间件的 next() 前后打断点,单步执行体会代码的具体执行过程。这样能够更直观地理解洋葱模型的运作机制。

image.png

四、总结

通过对 Koa 源码的深入分析,我们可以看到它的设计哲学:极简、优雅、灵活

参考资源


如果觉得有帮助,欢迎点赞收藏,也欢迎在评论区交流讨论!

Promise :从基础原理到高级实践

作者 Isenberg
2025年12月20日 16:35

Promise 是 JavaScript 中处理异步操作的核心机制,它解决了传统回调函数(Callback)带来的“回调地狱”(Callback Hell)问题,使异步代码更清晰、可读、可维护。自 ES6(ECMAScript 2015)正式引入以来,Promise 已成为现代前端开发的基石,并为 async/await 语法提供了底层支持。


一、为什么需要 Promise?

1.1 回调函数的局限性

在 Promise 出现之前,异步操作主要通过回调函数实现:

// 嵌套回调(回调地狱)
getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      console.log(c);
    });
  });
});

问题

  • 代码横向扩展,难以阅读和维护
  • 错误处理分散,需在每个回调中重复写 try/catch
  • 无法使用 returnthrow 控制流程
  • 多个异步操作的组合(如并行、竞态)实现复杂

1.2 Promise 的优势

  • 链式调用:通过 .then() 实现线性流程
  • 统一错误处理:通过 .catch() 捕获整个链中的错误
  • 组合能力:支持 Promise.allPromise.race 等高级模式
  • 与 async/await 无缝集成

二、Promise 基础概念

2.1 什么是 Promise?

Promise 是一个表示异步操作最终完成或失败的对象。

它有三种状态(State):

  • pending(待定) :初始状态,既不是成功也不是失败
  • fulfilled(已成功) :操作成功完成
  • rejected(已失败) :操作失败

⚠️ 状态不可逆
一旦 Promise 从 pending 变为 fulfilledrejected,状态将永久固定,不能再改变。

2.2 创建 Promise

使用 new Promise(executor) 构造函数:

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      resolve('操作成功!'); // 将状态变为 fulfilled
    } else {
      reject(new Error('操作失败!')); // 将状态变为 rejected
    }
  }, 1000);
});
  • resolve(value):标记 Promise 成功,传递结果值
  • reject(reason):标记 Promise 失败,传递错误原因(通常为 Error 对象)

三、Promise 的基本用法

3.1 链式调用(Chaining)

通过 .then(onFulfilled, onRejected) 处理结果:

promise
  .then(
    result => {
      console.log('成功:', result); // '操作成功!'
      return result.toUpperCase(); // 返回新值,传递给下一个 then
    },
    error => {
      console.error('失败:', error); // 不会执行(除非上一步 reject)
    }
  )
  .then(transformedResult => {
    console.log('转换后:', transformedResult); // '操作成功!'
  })
  .catch(error => {
    // 捕获链中任何未处理的 reject
    console.error('捕获错误:', error);
  });

关键规则

  • .then() 总是返回一个新的 Promise
  • onFulfilled 返回普通值 → 新 Promise 状态为 fulfilled
  • onFulfilled 抛出异常 → 新 Promise 状态为 rejected
  • onFulfilled 返回另一个 Promise → 新 Promise 跟随该 Promise 的状态

3.2 错误处理:.catch()

.catch(onRejected).then(null, onRejected) 的语法糖:

fetchUserData()
  .then(user => processUser(user))
  .then(data => saveToCache(data))
  .catch(error => {
    // 捕获 fetchUserData、processUser 或 saveToCache 中的任何错误
    console.error('操作失败:', error.message);
    showErrorMessage();
  });

📌 最佳实践
在链的末尾使用 .catch() 统一处理错误,避免在每个 .then() 中写错误回调。


四、Promise 的高级特性

4.1 静态方法

Promise.resolve(value)

将值转为已成功的 Promise:

Promise.resolve(42).then(v => console.log(v)); // 42
Promise.resolve(Promise.resolve('hello')).then(v => console.log(v)); // 'hello'

Promise.reject(reason)

创建一个已失败的 Promise:

Promise.reject(new Error('Oops!')).catch(e => console.error(e.message));

Promise.all(iterable)

并行执行多个 Promise,全部成功才成功

const promises = [
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
];

Promise.all(promises)
  .then(results => {
    const [users, posts, comments] = results;
    renderPage(users, posts, comments);
  })
  .catch(error => {
    // 任一请求失败,立即 reject
    console.error('加载失败:', error);
  });

⚠️ 注意:若任一 Promise reject,all 立即 reject,其余 Promise 仍会执行但结果被忽略。

Promise.allSettled(iterable)

等待所有 Promise 完成(无论成功或失败):

Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, i) => {
      if (result.status === 'fulfilled') {
        console.log(`请求 ${i} 成功:`, result.value);
      } else {
        console.error(`请求 ${i} 失败:`, result.reason);
      }
    });
  });

Promise.race(iterable)

返回第一个完成的 Promise(无论成功或失败)

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('超时')), 5000)
);

Promise.race([fetch('/api/data'), timeout])
  .then(data => console.log('数据:', data))
  .catch(error => console.error('失败或超时:', error));

Promise.any(iterable)(ES2021)

返回第一个成功的 Promise(忽略失败):

Promise.any([
  Promise.reject('A 失败'),
  Promise.resolve('B 成功'),
  Promise.reject('C 失败')
]).then(value => console.log(value)); // 'B 成功'

❗ 若全部失败,则 reject 一个 AggregateError


五、Promise 与 async/await

async/await 是 Promise 的语法糖,使异步代码看起来像同步代码。

5.1 基本用法

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error('请求失败');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('错误:', error);
    throw error; // 可选择重新抛出
  }
}

// 调用
fetchData().then(data => console.log(data));

5.2 关键规则

  • async 函数总是返回 Promise
  • await 只能在 async 函数内使用
  • await 后可跟 Promise 或普通值
  • 错误可通过 try/catch 捕获

5.3 并行 vs 串行

// ❌ 串行(慢)
async function slow() {
  const a = await fetch('/a');
  const b = await fetch('/b');
  const c = await fetch('/c');
}

// ✅ 并行(快)
async function fast() {
  const [a, b, c] = await Promise.all([
    fetch('/a'),
    fetch('/b'),
    fetch('/c')
  ]);
}

六、常见陷阱与最佳实践

6.1 陷阱 1:忘记返回 Promise

// ❌ 错误:第二个 then 无法获取数据
fetch('/api')
  .then(res => res.json())
  .then(data => {
    processData(data); // 忘记 return
  })
  .then(result => {
    console.log(result); // undefined!
  });

// ✅ 正确
fetch('/api')
  .then(res => res.json())
  .then(data => {
    return processData(data); // 显式 return
  });

6.2 陷阱 2:未处理拒绝(Uncaught Rejection)

// ❌ 危险:可能被忽略,导致静默失败
somePromise.then(result => {
  // ...
});

// ✅ 安全:始终处理错误
somePromise
  .then(result => { /* ... */ })
  .catch(error => { /* 处理错误 */ });

🔔 Node.js 提示:未处理的 Promise rejection 会导致进程警告(未来可能终止进程)。

6.3 陷阱 3:在循环中使用 await(串行而非并行)

// ❌ 串行执行(总耗时 = 所有请求时间之和)
for (const url of urls) {
  const data = await fetch(url);
  results.push(data);
}

// ✅ 并行执行(总耗时 ≈ 最长请求时间)
const promises = urls.map(url => fetch(url));
const results = await Promise.all(promises);

6.4 最佳实践

  1. 始终处理错误:使用 .catch()try/catch
  2. 避免嵌套 Promise:使用链式调用或 async/await
  3. 明确返回值:在 .then() 中显式 return
  4. 合理使用组合方法allraceallSettled
  5. 不要混合回调与 Promise:统一异步风格

七、Promise 的内部原理(简要)

虽然开发者通常无需实现 Promise,但理解其机制有助于调试:

// 极简 Promise 实现(仅演示思路)
class SimplePromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.callbacks = [];

    const resolve = value => {
      if (this.state !== 'pending') return;
      this.state = 'fulfilled';
      this.value = value;
      this.callbacks.forEach(cb => cb());
    };

    const reject = reason => {
      if (this.state !== 'pending') return;
      this.state = 'rejected';
      this.value = reason;
      this.callbacks.forEach(cb => cb());
    };

    executor(resolve, reject);
  }

  then(onFulfilled) {
    return new SimplePromise((resolve) => {
      const callback = () => {
        if (this.state === 'fulfilled') {
          const result = onFulfilled(this.value);
          resolve(result);
        }
      };
      if (this.state === 'pending') {
        this.callbacks.push(callback);
      } else {
        callback();
      }
    });
  }
}

📚 真实 Promise 更复杂:需处理微任务队列(Microtask Queue)、thenable 对象、递归解析等。


八、在 Vue 3 中的实践

Vue 3 的组合式 API 与 Promise 天然契合:

// composables/useApi.js
import { ref } from 'vue';

export function useApi(url) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const execute = async () => {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(res.statusText);
      data.value = await res.json();
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  };

  return { data, loading, error, execute };
}
<script setup>
import { useApi } from '@/composables/useApi';

const { data, loading, error, execute } = useApi('/api/users');
execute();
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">错误: {{ error.message }}</div>
  <ul v-else>
    <li v-for="user in data" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

优势:逻辑复用、状态管理、错误处理一体化。


结语

Promise 是 JavaScript 异步编程的里程碑,它不仅解决了回调地狱问题,还为现代异步语法(async/await)奠定了基础。掌握 Promise 的核心概念、链式调用、错误处理和组合方法,是成为高效前端开发者的必经之路。

记住:

  • Promise 是状态机:pending → fulfilled/rejected
  • 链式调用是核心:每个 .then() 返回新 Promise
  • 错误必须处理:避免静默失败
  • 组合优于嵌套:善用 Promise.all 等静态方法

随着 Web 应用日益复杂,异步操作无处不在。

脚手架开发工具——判断文件是否存在 path-exists

作者 code_YuJun
2025年12月20日 16:23

简介

path-exists 是一个轻量级的 Node.js npm 包,其核心作用是简便、高效地检查文件系统中指定的路径(文件或目录)是否存在,无需开发者手动封装原生文件操作的回调逻辑或错误处理,简化了 Node.js 中的路径存在性校验场景。

核心用法

npm install path-exists --save
  • 异步用法(推荐,非阻塞 I/O)
const pathExists = require('path-exists');

// 异步检查文件路径是否存在
async function checkFilePath() {
  // 传入要检查的文件/目录路径(相对路径或绝对路径均可)
  const isExist = await pathExists('./test.txt');
  console.log('文件是否存在:', isExist); // 返回 true 或 false
}

checkFilePath();

// 也可使用 Promise 链式调用(兼容旧版语法)
pathExists('./dist/').then((exists) => {
  console.log('目录是否存在:', exists);
});
  • 同步用法(适用于简单脚本,阻塞 I/O)
const pathExists = require('path-exists');

// 同步检查目录路径是否存在
const dirExists = pathExists.sync('./node_modules/');
console.log('node_modules 目录是否存在:', dirExists); // 返回 true 或 false

XMLHttpRequest、AJAX、Fetch 与 Axios

作者 Isenberg
2025年12月20日 16:20

在现代 Web 开发中,前端与后端的数据交互是构建动态应用的核心。围绕这一需求,诞生了多个关键技术与工具:XMLHttpRequest(XHR)AJAXAxiosFetch API。它们之间既有历史演进关系,也有功能重叠与互补。本文将系统梳理四者的关系,深入剖析 XHR 的工作机制与 Fetch 的底层原理,并结合 Vue 3 开发实践,提供一套完整的前端网络通信知识体系。


一、核心概念与层级关系

1.1 AJAX:一种编程范式(不是技术)

  • 全称:Asynchronous JavaScript and XML
  • 本质一种开发模式,指在不刷新页面的情况下,通过 JavaScript 异步与服务器交换数据并更新部分网页内容。
  • 核心思想:解耦 UI 更新与数据获取,提升用户体验。

关键点
AJAX 不是某个具体 API,而是一种使用现有技术实现异步通信的策略
实现 AJAX 的核心技术就是 XMLHttpRequest

1.2 XMLHttpRequest(XHR):浏览器原生 API

  • 角色实现 AJAX 的底层工具

  • 功能:提供浏览器与服务器进行 HTTP 通信的能力

  • 特点

    • 基于回调(事件驱动)
    • 支持进度监控、取消请求、上传/下载
    • 兼容性极好(IE7+)

📌 关系
XHR 是 AJAX 的“引擎” 。没有 XHR,就没有现代意义上的 AJAX。

1.3 Axios:基于 Promise 的 HTTP 客户端库

  • 定位对 XHR 的封装与增强

  • 核心特性

    • 返回 Promise,支持 async/await
    • 自动转换 JSON 数据
    • 拦截器(请求/响应)
    • 客户端支持 XSRF 防护
    • 浏览器 + Node.js 双端支持
  • 底层实现:在浏览器中默认使用 XHR,在 Node.js 中使用 http 模块

// Axios 内部简化逻辑
function axios(config) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(config.method, config.url);
    xhr.send(config.data);
    xhr.onload = () => resolve(xhr.response);
    xhr.onerror = () => reject(xhr.statusText);
  });
}

关系
Axios 是 XHR 的现代化封装,让开发者用更简洁的语法享受 XHR 的全部能力。

1.4 Fetch API:浏览器新一代原生 API

  • 定位XHR 的官方继任者

  • 设计目标

    • 基于 Promise,符合现代 JS 编程习惯
    • 更简洁的 API 设计
    • 更好的流(Stream)支持
    • 统一请求/响应模型(Request/Response 对象)
  • 底层实现并非基于 XHR,而是直接调用浏览器的网络层(如 Chromium 的 blink::WebURLLoader

⚠️ 重要区别
Fetch 不是 XHR 的封装,而是全新的底层实现


二、四者关系图谱

                          ┌──────────────┐
                          │    AJAX      │ ←── 编程范式(异步通信思想)
                          └──────┬───────┘
                                 │
         ┌───────────────────────┼───────────────────────┐
         │                       │                       │
┌────────▼────────┐   ┌──────────▼──────────┐   ┌────────▼────────┐
│ XMLHttpRequest  │   │       Fetch API     │   │      Axios      │
│ (原生, 回调式)   │   │ (原生, Promise式)   │   │ (第三方库, Promise)│
└────────┬────────┘   └─────────────────────┘   └────────┬────────┘
         │                                               │
         └───────────────────────┬───────────────────────┘
                                 │
                   ┌─────────────▼─────────────┐
                   │   现代 Web 应用数据通信    │
                   └───────────────────────────┘

🔑 总结关系

  • AJAX 是思想,XHR/Fetch 是实现该思想的原生工具
  • Axios 是对 XHR(浏览器端)的高级封装
  • Fetch 是浏览器提供的、与 XHR 并列的新一代原生 API

三、XMLHttpRequest 详解

3.1 基本使用流程

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

3.2 核心属性

属性 说明
readyState 请求状态(0–4)
status / statusText HTTP 状态码与描述
responseText 字符串响应体
response 根据 responseType 解析后的数据
responseType 响应类型(jsonblobarraybuffer 等)

3.3 事件模型

  • 传统方式onreadystatechange(需手动判断 readyState

  • 现代方式(推荐):

    • onload:请求完成
    • onerror:网络错误
    • ontimeout:超时
    • onabort:被中止

3.4 高级功能

  • 超时控制xhr.timeout = 5000
  • 跨域凭据xhr.withCredentials = true
  • 上传进度xhr.upload.onprogress
  • 中止请求xhr.abort()

3.5 实际应用场景

文件上传(带进度)

function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/upload');
  
  xhr.upload.onprogress = (e) => {
    if (e.lengthComputable) {
      const percent = (e.loaded / e.total) * 100;
      updateProgress(percent);
    }
  };
  
  xhr.onload = () => {
    if (xhr.status === 200) showSuccess();
  };
  
  xhr.send(formData);
}

四、Fetch API 原理深度解析

4.1 核心设计:基于 Stream 的请求/响应模型

Fetch 的核心是两个构造函数:

  • Request:表示 HTTP 请求
  • Response:表示 HTTP 响应

两者都实现了 Body mixin,包含可读流(ReadableStream):

fetch('/api/data')
  .then(response => {
    console.log(response.body instanceof ReadableStream); // true
    return response.json(); // 内部读取 body 流并解析
  });

💡 关键机制
Fetch 将响应体视为流(Stream) ,支持边下载边处理,适合大文件或实时数据。

4.2 执行流程(浏览器内部)

以 Chromium 为例:

  1. 调用 fetch(url) → 创建 Request 对象
  2. 浏览器主线程 → 网络服务线程(Network Service)
  3. 网络线程发起 HTTP 请求(复用连接池、DNS 缓存等)
  4. 收到响应头 → 立即 resolve Promise(返回 Response 对象)
  5. 响应体通过 ReadableStream 逐步传输到 JS 主线程
  6. 调用 .json() / .text() 等方法 → 消费流并解析

4.3 与 XHR 的关键差异

特性 XHR Fetch
错误处理 网络错误 → onerror;HTTP 错误(404/500)→ onload 仅网络错误 reject;HTTP 错误仍 resolve(需手动检查 response.ok
Cookie 发送 同域自动发送 需显式设置 credentials: 'same-origin'
取消请求 xhr.abort() AbortController
上传进度 原生 upload.onprogress 不支持(需自定义 ReadableStream,复杂)
超时控制 xhr.timeout 需配合 AbortController + setTimeout

错误处理对比示例:

// Fetch:HTTP 404 仍 resolve
fetch('/not-found')
  .then(res => {
    if (!res.ok) { // 必须手动检查
      throw new Error(`HTTP ${res.status}`);
    }
  })
  .catch(err => {
    // 只有网络断开才会进入这里
  });

4.4 Fetch 的局限性与解决方案

问题 1:无法监控下载进度

解决方案:手动读取流并计算进度:

const response = await fetch('/large-file');
const contentLength = +response.headers.get('Content-Length');
let loaded = 0;

const reader = response.body.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  loaded += value.length;
  const progress = (loaded / contentLength) * 100;
  updateProgress(progress);
}

问题 2:无内置超时

解决方案:结合 AbortController

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

fetch('/api/data', { signal: controller.signal })
  .finally(() => clearTimeout(timeoutId));

五、Vue 3 中的网络通信实践

虽然 Vue 本身不强制使用特定 HTTP 客户端,但其组合式 API 与现代请求库天然契合。

5.1 使用 Axios(推荐用于复杂项目)

// composables/useApi.js
import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  timeout: 10000,
  withCredentials: true
});

// 请求拦截器
api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// 响应拦截器
api.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      // 处理未授权
      router.push('/login');
    }
    return Promise.reject(error);
  }
);

export default api;
<!-- 在组件中使用 -->
<script setup>
import { ref } from 'vue';
import api from '@/composables/useApi';

const users = ref([]);
const loading = ref(false);

const fetchUsers = async () => {
  loading.value = true;
  try {
    users.value = await api.get('/users');
  } finally {
    loading.value = false;
  }
};

fetchUsers();
</script>

5.2 使用 Fetch(轻量级项目)

// utils/request.js
async function request(url, options = {}) {
  const config = {
    credentials: 'include',
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers
    }
  };

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 10000);
  
  try {
    const response = await fetch(url, {
      ...config,
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

export { request };

5.3 封装为 Composable(最佳实践)

// composables/useFetch.js
import { ref } from 'vue';

export function useFetch(url) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const execute = async () => {
    loading.value = true;
    error.value = null;
    
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(res.statusText);
      data.value = await res.json();
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  };

  return { data, loading, error, execute };
}
<script setup>
import { useFetch } from '@/composables/useFetch';

const { data: users, loading, execute } = useFetch('/api/users');
execute();
</script>

六、如何选择?—— 使用场景建议

场景 推荐方案 理由
新项目(现代浏览器) fetch() + 工具函数封装 原生支持,无依赖,符合标准
需要上传/下载进度 XMLHttpRequest 或 Axios 原生支持 onprogress,简单可靠
复杂拦截、转换、兼容 Node.js Axios 功能全面,生态成熟
维护旧项目(IE11+) XMLHttpRequest 或 Axios(带 polyfill) 最大兼容性
轻量级应用,避免打包体积 fetch() 无需引入第三方库
Vue 3 项目 Axios(复杂)Fetch + Composable(简单) 与组合式 API 完美契合

📌 现代最佳实践

  • 优先使用 fetch() 或 Axios
  • 将网络逻辑封装为 Composable,实现逻辑复用
  • 避免直接使用裸 XHR(除非特殊需求)

七、安全与性能注意事项

7.1 安全

  • XSS 防护:永远不要将响应直接插入 innerHTML
  • CSRF 防护:使用 anti-CSRF token,重要操作用非 GET 方法
  • CORS 策略:服务器严格限制 Access-Control-Allow-Origin
  • 敏感数据:使用 HTTPS,避免客户端存储密码/token

7.2 性能

  • 缓存策略:合理设置 Cache-Control
  • 请求合并:避免频繁小请求
  • 懒加载:非关键数据延迟请求
  • 取消冗余请求:组件销毁时中止未完成的请求
// Vue 3 中取消请求
import { onUnmounted } from 'vue';

export function useFetch(url) {
  const controller = new AbortController();
  
  onUnmounted(() => {
    controller.abort(); // 组件卸载时取消请求
  });
  
  const execute = () => {
    return fetch(url, { signal: controller.signal });
  };
  
  return { execute };
}

结语

理解 XHR、AJAX、Axios 与 Fetch 的关系,本质上是理解 Web 异步通信技术的演进史:

  • AJAX 提出了“异步更新”的思想
  • XHR 提供了首个标准化实现
  • Axios 在 XHR 基础上构建了开发者友好的抽象
  • Fetch 则代表了浏览器厂商对下一代网络 API 的重新设计

作为开发者,我们不必拘泥于某一种工具,而应根据项目需求、浏览器支持和功能复杂度做出合理选择。但无论使用哪种方式,其背后的核心原理——HTTP 协议、CORS 安全模型、异步编程范式——始终不变。

在 Vue 3 的组合式 API 时代,将网络逻辑封装为可复用的 Composable,不仅能提升代码可维护性,更能充分发挥现代 JavaScript 的表达力。掌握这些底层逻辑,才能在技术变迁中游刃有余,构建出高性能、高安全性的现代 Web 应用。

2025-12-20 vue3中 eslint9+和prettier配置

作者 Mr_Swilder
2025年12月20日 16:19

eslint 9+相较8版本使用eslint.config.js和扁平化配置方式。

1.在项目根目录下安装所需的开发依赖包

# 核心代码检查与格式化工具
pnpm add -D eslint prettier

# Vue.js 语法支持
pnpm add -D eslint-plugin-vue

# Prettier 与 ESLint 集成
pnpm add -D eslint-config-prettier eslint-plugin-prettier

💅 2. prettier配置

vscode中安装插件

image.png

根目录下新建配置文件 .prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "arrowParens": "avoid",
  "htmlWhitespaceSensitivity": "ignore"
}

3.eslint配置并将prettier规则作为eslint一部分,对不符合要求的报错

// eslint.config.js
import eslintPluginVue from 'eslint-plugin-vue'
import vueEslintParser from 'vue-eslint-parser'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
// console.log(eslintPluginPrettierRecommended)
export default [
  // 全局配置:指定环境、解析器选项
  {
    files: ['**/*.js', '**/*.vue'],
    ignores: ['vite.config.js', 'node_modules/', 'dist/', 'public/'],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        browser: true, // 浏览器环境全局变量
        node: true, // Node.js 环境全局变量
        es2021: true // ES2021 语法支持
      }
    },
    rules: {
      'no-console': 'warn',
      'no-debugger': 'error',
      'no-unused-vars': ['warn', { varsIgnorePattern: '^_' }]
    }
  },

  // Vue 单文件组件专属配置
  {
    files: ['**/*.vue'],
    // 使用 vue-eslint-parser 解析 .vue 文件
    languageOptions: {
      parser: vueEslintParser,
      parserOptions: {
        parser: 'espree', // 解析 <script> 块内的 JavaScript
        ecmaVersion: 'latest',
        sourceType: 'module'
      }
    },
    plugins: {
      vue: eslintPluginVue
    },
    rules: {
      // 启用 Vue 官方推荐规则
      ...eslintPluginVue.configs['flat/recommended'].rules,
      // 自定义 Vue 规则
      'vue/multi-word-component-names': 'off' // 关闭组件名必须多单词的要求
    }
  },
  //prettier配置
  eslintPluginPrettierRecommended
]

此时运行

npm run lint

可以对不符合规则的代码检查,包含不符合eslint规则的

image.png

4.配置vscode插件eslintPrettier ESlint,设置默认格式化工具为Prettier ESlint,让eslint直接报错prettier中的配置,有时修改了.prettierrc中的配置需要重启Prettier ESlint插件才能生效。

image.png

image.png

对于.eslintignore文件缺失时eslint会使用.gitignore文件

Vue3条件渲染中v-if系列指令如何合理使用与规避错误?

作者 kknone
2025年12月20日 16:13

一、Vue3条件渲染的核心概念

在Vue3中,条件渲染是指根据响应式数据的真假,决定是否在页面上渲染某个元素或组件。而v-ifv-elsev-else-if 这组指令,就是实现条件渲染的“核心工具”——它们像一套“逻辑开关”,帮你精准控制DOM的显示与隐藏。

二、v-if系列指令的语法与用法

我们先逐个拆解每个指令的作用,再通过实际案例串起来用。

2.1 v-if:基础条件判断

v-if是最基础的条件指令,它的语法很简单:


<元素 v-if="条件表达式">要渲染的内容</元素>
  • 条件表达式:可以是任何返回truefalse的JavaScript表达式(比如isLoginscore >= 90)。
  • 惰性渲染v-if是“懒”的——如果初始条件不满足(比如isLogin = false),元素不会被渲染到DOM中(连DOM节点都不会生成);只有当条件变为 true时,才会“从零开始”创建元素及其子组件。

举个例子:判断用户是否登录,显示不同的内容:


<script setup>
  import {ref} from 'vue'

  const isLogin = ref(false) // 初始未登录
</script>

<template>
  <div>
    <!-- 登录后显示 -->
    <p v-if="isLogin">欢迎回来,用户!</p>
    <!-- 未登录显示 -->
    <button v-if="!isLogin" @click="isLogin = true">点击登录</button>
  </div>
</template>

当点击按钮时,isLogin变为true,未登录的按钮会被销毁,同时渲染“欢迎”文本——这就是v-if的“销毁/重建”逻辑。

往期文章归档
免费好用的热门在线工具

2.2 v-else:补充默认分支

如果v-if的条件不满足,你可以用v-else添加一个“默认选项”。但要注意:
v-else必须紧跟在v-ifv-else-if的后面,中间不能有其他兄弟元素(否则Vue无法识别它属于哪个条件)。

修改上面的例子,用v-else简化未登录的情况:


<template>
    <div>
        <p v-if="isLogin">欢迎回来,用户!</p>
        <!-- 直接跟在v-if后面,无需写条件 -->
        <button v-else @click="isLogin = true">点击登录</button>
    </div>
</template>

2.3 v-else-if:多分支条件判断

当需要判断多个条件时,用v-else-if连接。它的语法是:


<元素 v-if="条件1">内容1</元素>
<元素 v-else-if="条件2">内容2</元素>
<元素 v-else-if="条件3">内容3</元素>
<元素 v-else>默认内容</元素>

关键规则:Vue会按指令的顺序依次判断,满足第一个条件就停止(所以条件的顺序很重要!)。

比如根据分数显示等级(最常见的多分支场景):


<script setup>
  import {ref} from 'vue'

  const score = ref(85) // 响应式分数,初始85分
</script>

<template>
  <div class="score-level">
    <h3>你的分数:{{ score }}</h3>
    <!-- 顺序:从高到低 -->
    <p v-if="score >= 90" class="excellent">等级:优秀(≥90)</p>
    <p v-else-if="score >= 80" class="good">等级:良好(80-89)</p>
    <p v-else-if="score >= 60" class="pass">等级:及格(60-79)</p>
    <p v-else class="fail">等级:不及格(<60)</p>
  </div>
</template>

<style scoped>
  .excellent {
    color: #4CAF50;
  }

  .good {
    color: #2196F3;
  }

  .pass {
    color: #FFC107;
  }

  .fail {
    color: #F44336;
  }
</style>

运行这个组件,修改score的值(比如改成9550),会看到等级自动切换——这就是多分支条件渲染的实际效果。

三、条件渲染的流程逻辑(附流程图)

为了更直观理解v-if系列的执行顺序,我们画一个判断流程图

flowchart TD
    A[开始] --> B{检查v-if条件}
    B -->|满足| C[渲染v-if内容,结束]
    B -->|不满足| D{检查下一个v-else-if条件}
    D -->|满足| E[渲染对应内容,结束]
    D -->|不满足| F{还有v-else-if吗?}
    F -->|是| D
    F -->|否| G[渲染v-else内容,结束]

简单来说:按顺序“闯关”,满足条件就“通关”,否则继续,直到最后一个v-else

四、课后Quiz:巩固你的理解

Quiz 1:v-if和v-show有什么区别?

问题:同样是“隐藏元素”,v-ifv-show的核心差异是什么?分别适合什么场景?

答案解析(参考Vue官网):

  • v-if销毁/重建DOM——条件不满足时,元素从DOM中消失;条件满足时,重新创建。适合条件很少变化的场景(比如权限判断),因为初始渲染更省性能。
  • v-show修改CSS显示——无论条件如何,元素都会渲染到DOM,只是用display: none隐藏。适合条件频繁切换的场景(比如 tabs 切换),因为切换时无需销毁重建,更高效。

Quiz 2:为什么v-else必须紧跟v-if?

问题:如果写v-if之后隔了一个div再写v-else,会报错吗?为什么?

答案解析
会报错!错误信息是“v-else has no adjacent v-if”。
原因:Vue需要明确v-else对应的“上级条件”——它必须与最近的v-if/v-else-if直接相邻(中间不能有其他兄弟元素)。如果隔开,Vue无法识别两者的关联。

五、常见报错与解决方案

在使用v-if系列时,新手常遇到以下问题,我们逐一解决:

1. 报错:“v-else/v-else-if has no adjacent v-if”

  • 原因v-elsev-else-if没有紧跟对应的v-if(中间有其他元素)。
    比如:
    <div v-if="isShow"></div>
    <p>无关内容</p> <!-- 中间的p元素导致错误 -->
    <div v-else></div>
    
  • 解决:删除中间的无关元素,让v-else直接紧跟v-if
  • 预防:写v-else前,先检查前面的元素是否是v-ifv-else-if

2. 报错:“条件变化时,v-if内容不更新”

  • 原因:条件变量不是响应式的(比如用let isShow = false而不是ref(false)),Vue无法追踪其变化。
  • 解决:用ref(基本类型)或reactive(对象/数组)包裹条件变量:
    // 错误写法
    let isShow = false 
    // 正确写法
    const isShow = ref(false)
    
  • 预防:所有需要“随数据变化而更新”的变量,都用Vue的响应式API(ref/reactive)定义。

3. 逻辑错误:v-else-if顺序导致条件失效

  • 例子:先写v-else-if="score >= 60",再写v-else-if="score >= 80"——此时80分会被第一个条件拦截,永远到不了第二个。
  • 原因:Vue按指令顺序判断,满足第一个条件就停止。
  • 解决:将更严格的条件放在前面(比如先>=90,再>=80,最后>=60)。

参考链接

Vue官网条件渲染文档:vuejs.org/guide/essen…

脚手架开发工具——root-check

作者 code_YuJun
2025年12月20日 16:02

简介

root-check 是一个 Node.js 工具包,核心作用是检测当前 Node.js 进程是否以 root(超级管理员)权限运行,并在检测到 root 权限时,自动降级为指定的普通用户权限运行,以此提升应用的安全性。

检测 root 权限

它会判断当前进程的 uid(用户 ID)是否为 0(Unix/Linux 系统中 root 用户的 uid 固定为 0),以此识别是否为 root 权限运行。

自动降级权限

如果检测到当前是 root 权限,它会尝试切换到一个非 root 普通用户的权限来运行后续代码。

  • 默认会尝试切换到 nobody 用户(Unix/Linux 系统中内置的无特权用户)。
  • 也可以手动指定要切换的用户(通过用户名或 uid)。

解决的核心问题

在 Unix/Linux 系统中,以 root 权限运行 Node.js 应用存在极高的安全风险:一旦应用存在漏洞被攻击,攻击者将直接获得系统的最高权限,可能导致服务器被完全控制。root-check 的存在就是为了避免这种风险,强制应用以低权限运行,即使被攻击,影响范围也会被大幅限制。

适用场景

  • 命令行工具(CLI)开发:很多 Node.js 命令行工具(如前端的构建工具、脚手架)会用到这个包,防止用户以 root 权限执行命令带来风险。
  • 服务端应用:Node.js 编写的后端服务,部署时需要避免 root 权限运行,可通过此包自动降级。

基本使用示例

npm install root-check --save
const rootCheck = require('root-check').default;
// 自动降级为 nobody 用户(如果当前是 root 权限)
rootCheck();
// 或者手动指定要切换的用户
// rootCheck('www-data'); // 切换到 www-data 用户

注意事项

  • 仅支持 Unix/Linux 系统:Windows 系统没有 root/uid 的概念,该包在 Windows 上会直接失效(无副作用)。
  • 降级失败会抛出错误:如果当前是 root 权限,但无法切换到目标用户(比如目标用户不存在),rootCheck 会抛出异常,需要手动捕获处理。

解决Tailwind任意值滥用:规范化CSS开发体验

作者 远山无期
2025年12月20日 15:15

背景 eslint-plugin-tailwindcss插件的no-unnecessary-arbitrary-value无法对所有的任意值进行校验,比如h-[48px]text-[#f5f5f5]无法校验出来。但tailwindcss的预设值太多了,一个不小心可能就又写了一个没有必要的任意值。为了避免这种情况,我们需要自己实现一个检测任意值的eslint插件。

插件地址:eslint-plugin-tailwind-no-preset-class

首先来看下效果

no-unnecessary-arbitrary-value 无法检测的情况

image.png

使用自定义的:eslint-plugin-tailwind-no-preset-class插件,完美完成了校验

image.png

创建eslint插件标准目录结构

  • 安装Yeoman
npm install -g yo
  • 安装Yeoman generator-eslint
npm install -g generator-eslint
  • 创建项目
mkdir eslint-plugin-my-plugin
yo eslint:plugin

生成目录结构如下:

eslint-plugin-my-plugin/
├── lib/                    # 核心源代码目录
│   ├── index.js           # 插件的入口文件,在这里导出所有规则
│   └── rules/             # 存放所有自定义规则的目录
│       └── my-rule.js     # 生成器为你创建的一条示例规则文件
├── tests/                 # 测试文件目录
│   └── lib/
│       └── rules/
│           └── my-rule.js # 示例规则对应的测试文件
├── package.json           # 项目的 npm 配置文件,依赖和元信息都在这里
└── README.md              # 项目说明文档

根据实际项目的tailwindcss配置文件和tailwindcss默认配置生成全量定制化配置,用于后续eslint插件的校验依据

实现配置文件生成并加载方法:

// lib/tailwind-config-loader.js
// 配置文件生成
...
...
// 动态加载 Tailwind 预设配置
let tailwindPresetConfig = null;
...
async function generateTailwindConfig(projectRootPath) {
  try {
    // 动态导入tailwindcss
    const resolveConfigModule = await import('tailwindcss/lib/public/resolve-config.js');
    const resolveConfig = resolveConfigModule.default.default
    // 尝试加载项目配置
    let projectConfig = {};
    try {
      const projectConfigPath = join(projectRootPath||process.cwd(), 'tailwind.config.js');
      const projectConfigModule = await import(projectConfigPath);
      projectConfig = projectConfigModule.default || projectConfigModule;
    } catch (error) {
      console.log('⚠️ 未找到项目 tailwind.config.js,使用默认配置');
      throw error;
    }

    // 使用tailwindcss的resolveConfig函数
    const finalConfig = resolveConfig(projectConfig);

    console.log('✅ Tailwind preset config generated successfully!');
    
    return finalConfig;
  } catch (error) {
    console.error('❌ 生成Tailwind配置失败:', error.message);
    throw error;
  }
}


// 加载配置到内存中
async function loadTailwindPresetConfig(projectRootPath) {
  if (configLoading) {
    console.log('⏳ 配置正在加载中,跳过重复请求');
    return;
  }

  configLoading = true;

  try {
    // 直接动态生成配置
    tailwindPresetConfig = await generateTailwindConfig(projectRootPath);
    console.log('✅ Tailwind 预设配置已动态生成并加载');
    onConfigLoaded();
  } catch (error) {
    console.error('❌ 动态生成 Tailwind 预设配置失败:', error.message);
    onConfigLoadFailed(error);
    throw error;
  }
}


...
// 导出配置
export const TailwindConfigLoader = {
  getConfig: () => tailwindPresetConfig,
  isLoaded: () => configLoaded,
  ensureLoaded: ensureConfigLoaded,
  reload: loadTailwindPresetConfig,
  generateConfig: generateTailwindConfig
};
...
...

创建校验规则函数

  • 实现校验规则函数checkAndReport
...
// 使用 WeakMap 来跟踪每个文件的已报告类名,避免重复报告
const reportedClassesMap = new WeakMap();
...
// 检查并报告
async function checkAndReport(context, node, className) {
  // 如果配置尚未加载,尝试等待加载
  if (!TailwindConfigLoader.isLoaded()) {
    try {
        const projectRootPath = context.getCwd();
        console.log(`正在等待加载配置文件 ${projectRootPath}...`);
      const loaded = await TailwindConfigLoader.ensureLoaded(projectRootPath);
      if (!loaded) {
        console.warn('⚠️ Tailwind 预设配置尚未加载,跳过检查');
        return;
      }
    } catch (error) {
      console.warn('⚠️ 配置加载失败,跳过检查');
      return;
    }
  }

  const filePath = context.getFilename();
  const filePathWrapper = new FilePathWrapper(filePath);

  if (!reportedClassesMap.has(filePathWrapper)) {
    reportedClassesMap.set(filePathWrapper, new Set());
  }
  const reportedClasses = reportedClassesMap.get(filePathWrapper);

  if (reportedClasses.has(className)) {
    return;
  }

  const propertyInfo = extractProperty(className);
  if (!propertyInfo) {
    return;
  }

  const { property, value, originalPrefix } = propertyInfo;

  // 只检查任意值
  if (isArbitraryValue(value)) {
    const arbitraryValue = value.slice(1, -1);
    const presetClass = findPresetClass(property, arbitraryValue);

    if (presetClass) {
      reportedClasses.add(className);
      // 使用原始前缀显示正确的类名格式(如 h-14 而不是 height-14)
      const suggestedClass = `${originalPrefix}${presetClass}`;
      context.report({
        node,
        message: `类名 "${className}" 使用了任意值,但存在对应的预设类名 "${suggestedClass}"。请使用预设类名替代。`,
      });
    }
  }
}

  • 实现属性提取,将classname解析为tailwindcss的property和value
// 提取属性值
function extractProperty(className) {
  // 处理响应式前缀(如 max-md:, md:, lg: 等)
  const responsivePrefixes = [
    'max-sm:',
    'max-md:',
    'max-lg:',
    'max-xl:',
    'max-2xl:',
    'max-',
    'min-',
    'sm:',
    'md:',
    'lg:',
    'xl:',
    '2xl:',
  ];

  // 移除响应式前缀,保留核心类名
  let coreClassName = className;
  let responsivePrefix = '';

  for (const prefix of responsivePrefixes) {
    if (className.startsWith(prefix)) {
      responsivePrefix = prefix;
      coreClassName = className.slice(prefix.length);
      break;
    }
  }

  // 按前缀长度降序排序,优先匹配更长的前缀
  const sortedPrefixes = Object.keys(prefixToProperty).sort(
    (a, b) => b.length - a.length
  );

  for (const prefix of sortedPrefixes) {
    if (coreClassName.startsWith(prefix)) {
      return {
        property: prefixToProperty[prefix],
        value: coreClassName.slice(prefix.length),
        originalPrefix: responsivePrefix + prefix, // 包含响应式前缀
      };
    }
  }

  return null;
}
  • 将提取的property和前面生成的全量的tailwindcss进行映射
// 简化属性映射,只保留常用的属性
const prefixToProperty = {
  // 尺寸相关
  "w-": "width",
  "h-": "height",
  "min-w-": "minWidth",
  "min-h-": "minHeight",
  "max-w-": "maxWidth",
  "max-h-": "maxHeight",

  // 间距相关
  "m-": "margin",
  "mt-": "marginTop",
  "mr-": "marginRight",
  "mb-": "marginBottom",
  "ml-": "marginLeft",
  "mx-": "margin",
  "my-": "margin",
  "p-": "padding",
  "pt-": "paddingTop",
  "pr-": "paddingRight",
  "pb-": "paddingBottom",
  "pl-": "paddingLeft",
  "px-": "padding",
  "py-": "padding",

  // 边框相关(新增)
  "border-": "borderWidth;borderColor",
  "border-t-": "borderWidth;borderColor",
  "border-r-": "borderWidth;borderColor",
  "border-b-": "borderWidth;borderColor",
  "border-l-": "borderWidth;borderColor",
  "border-x-": "borderWidth;borderColor",
  "border-y-": "borderWidth;borderColor",

  // 圆角相关(新增)
  "rounded-": "borderRadius",
  "rounded-t-": "borderRadius",
  "rounded-r-": "borderRadius",
  "rounded-b-": "borderRadius",
  "rounded-l-": "borderRadius",
  "rounded-tl-": "borderRadius",
  "rounded-tr-": "borderRadius",
  "rounded-br-": "borderRadius",
  "rounded-bl-": "borderRadius",

  // 文字相关
  "text-": "fontSize;color",
  "leading-": "lineHeight",
  "tracking-": "letterSpacing",
  "font-": "fontWeight",

  // 背景相关
  "bg-": "backgroundColor",

  // SVG相关
  "fill-": "fill",
  "stroke-": "stroke",
  "stroke-w-": "strokeWidth",

  // 定位相关
  "z-": "zIndex",
  "inset-": "inset",
  "top-": "top",
  "right-": "right",
  "bottom-": "bottom",
  "left-": "left",

  // 布局相关(新增)
  "gap-": "gap",
  "gap-x-": "gap",
  "gap-y-": "gap",
  "space-x-": "gap",
  "space-y-": "gap",

  // 透明度
  "opacity-": "opacity",

  // 变换相关(新增)
  "scale-": "scale",
  "scale-x-": "scale",
  "scale-y-": "scale",
  "rotate-": "rotate",
  "translate-x-": "translate",
  "translate-y-": "translate",
  "skew-x-": "skew",
  "skew-y-": "skew",

  // 阴影相关(新增)
  "shadow-": "boxShadow",

  // 网格相关(新增)
  "grid-cols-": "gridTemplateColumns",
  "grid-rows-": "gridTemplateRows",
  "col-": "gridColumn",
  "row-": "gridRow",
  "col-start-": "gridColumnStart",
  "col-end-": "gridColumnEnd",
  "row-start-": "gridRowStart",
  "row-end-": "gridRowEnd",

  // Flexbox相关(新增)
  "flex-": "flex",
  "basis-": "flexBasis",
  "grow-": "flexGrow",
  "shrink-": "flexShrink",
  "order-": "order",

  // 动画相关(新增)
  "duration-": "transitionDuration",
  "delay-": "transitionDelay",
  "ease-": "transitionTimingFunction",

  // 其他(新增)
  "aspect-": "aspectRatio",
  "cursor-": "cursor",
};

// 动态构建支持的 Tailwind 属性映射
function getSupportedProperties() {
  const config = TailwindConfigLoader.getConfig();
  if (!config) {
    return {};
  }

  return {
    width: config.theme.width,
    height: config.theme.height,
    minWidth: config.theme.minWidth,
    minHeight: config.theme.minHeight,
    maxWidth: config.theme.maxWidth,
    maxHeight: config.theme.maxHeight,
    margin: config.theme.margin,
    marginTop: config.theme.margin,
    marginRight: config.theme.margin,
    marginBottom: config.theme.margin,
    marginLeft: config.theme.margin,
    padding: config.theme.padding,
    paddingTop: config.theme.padding,
    paddingRight: config.theme.padding,
    paddingBottom: config.theme.padding,
    paddingLeft: config.theme.padding,
    fontSize: config.theme.fontSize,
    lineHeight: config.theme.lineHeight,
    borderRadius: config.theme.borderRadius,
    color: config.theme.colors,
    backgroundColor: config.theme.backgroundColor,
    borderColor: config.theme.borderColor,
    fill: config.theme.fill,
    stroke: config.theme.stroke,
    borderWidth: config.theme.borderWidth,
    zIndex: config.theme.zIndex,
    gap: config.theme.gap,
    inset: config.theme.inset,
    top: config.theme.spacing,
    right: config.theme.spacing,
    bottom: config.theme.spacing,
    left: config.theme.spacing,
    opacity: config.theme.opacity,
  };
}

整体实现流程

graph TD
    A[ESLint 执行插件] --> B[遍历代码中的类名]
    B --> C{是否为 Tailwind 类名?}
    C -->|否| D[跳过检查]
    C -->|是| E{是否包含任意值?}
    E -->|否| F[使用预设值 通过检查]
    E -->|是| G[提取类名前缀和任意值]
    
    G --> H[通过 prefixToProperty 映射到CSS属性]
    H --> I[检查Tailwind配置是否已加载]
    I -->|已加载| J[获取支持的属性预设值]
    I -->|未加载| K[加载项目Tailwind配置]
    
    K --> L[读取项目tailwind.config.js]
    L --> M{配置是否存在?}
    M -->|不存在| N[使用Tailwind默认配置]
    M -->|存在| O[解析项目配置]
    
    O --> P[合并默认配置和项目配置]
    N --> P
    
    P --> Q[生成全量Tailwind配置]
    Q --> R[缓存配置到内存]
    R --> J
    
    J --> S{判断属性类型}
    S -->|颜色相关| T[调用 findColorPreset]
    S -->|数值相关| U[调用 findNumericPreset]
    
    T --> V{是否匹配预设?}
    U --> V
    
    V -->|是| W[找到对应预设类名]
    V -->|否| X[未找到预设类名]
    
    W --> Y[生成建议消息]
    X --> Z[通过检查 无匹配预设]
    
    Y --> AA[报告建议]
    Z --> BB[检查完成]
    
    AA --> BB

告别代码屎山!UniApp + Vue3 自动化规范:ESLint 9+ 扁平化配置全指南

2025年12月20日 14:40

配置初衷是为了保证,团队开发中的代码规范问题。 以下是全部配置过程,我的项目是npm创建的,并非hbuilder创建的。如果你的项目的hbuilder创建的,需要执行下 npm init -y

配置后想要达到的效果:

  • 保证缩进统一性
  • vue组件多个属性可以自动换行。
  • 在代码里用了 uni.showToast,ESLint 却疯狂报错 uni is not defined

2025 年,ESLint 迎来了史上最大变革——Flat Config(扁平化配置)  时代。今天,我们就用一套最硬核的方案,把 Vue3、TypeScript、SCSS 和 Git 自动化 全部打通!

一、 为什么 2025 年要用 ESLint 9+ ?

传统的 .eslintrc.js 采用的是“层级继承”逻辑,配置多了就像迷宫。而 ESLint 9+ 的 Flat Config (eslint.config.mjs)  采用纯 JavaScript 数组对象,逻辑更扁平、加载更快速、对 ESM 原生支持更好。

二、 核心依赖安装:一步到位

首先,清理掉项目里的旧配置文件,然后在根目录执行这行“全家桶”安装命令:

npm install eslint @eslint/js typescript-eslint eslint-plugin-vue globals eslint-config-prettier eslint-plugin-prettier prettier husky lint-staged --save-dev

三、 配置实战:三剑客齐聚

  1. 魔法启动:生成 ESLint 9 配置文件

新版 ESLint 推荐通过交互式命令生成基础框架,但针对 UniApp,我们建议直接创建 eslint.config.mjs 以获得极致控制力。

核心逻辑:

  • 2 空格缩进:强迫症的福音。
  • 属性换行:组件属性 > 3 个自动起新行。
  • 多语言全开:JS / TS / Vue / CSS / SCSS 完美兼容。
/* eslint.config.mjs */
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';

export default tseslint.config(
  // 【1】配置忽略名单:不检查编译后的代码
  { ignores: ['dist/**', 'unpackage/**', 'node_modules/**', 'static/**'] },

  // 【2】JS 基础规则 & UniApp 全局变量支持
  js.configs.recommended,
  {
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: {
        ...globals.browser, ...globals.node,
        uni: 'readonly', wx: 'readonly', plus: 'readonly' // 解决 uni 报错
      },
    },
  },

  // 【3】TypeScript 强类型支持
  ...tseslint.configs.recommended,

  // 【4】Vue 3 核心规范(属性换行策略)
  ...pluginVue.configs['flat/recommended'],
  {
    files: ['**/*.vue'],
    languageOptions: {
      parserOptions: { parser: tseslint.parser } // Vue 模板内支持 TS
    },
    rules: {
      'vue/multi-word-component-names': 'off', // 适配 UniApp 页面名
      'vue/html-indent': ['error', 2],         // 模板强制 2 空格
      'vue/max-attributes-per-line': ['error', {
        singleline: { max: 3 }, // 超过 3 个属性就换行
        multiline: { max: 1 }   // 多行模式下每行只能有一个属性
      }],
      'vue/first-attribute-linebreak': ['error', {
        singleline: 'beside', multiline: 'below'
      }]
    }
  },

  // 【5】Prettier 冲突处理:必须放在数组最后一行!
  pluginPrettierRecommended,
);
  1. 视觉统领:.prettierrc
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false,
  "printWidth": 100,
  "trailingComma": "all",
  "endOfLine": "auto"
}
  1. 编辑器底层逻辑:.editorconfig

让 IDEA 和 VS Code 在你打字的第一秒就明白:缩进只要两个空格。

root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true

四、 极速体验:让工具为你打工

  1. IDEA / WebStorm 深度联动

别再手动敲 Ctrl+Alt+L 了!

  • 进入 Settings -> ESLint,勾选 Run eslint --fix on save
  • 进入 Settings -> Prettier,勾选 Run on save
    IDEA 2024+ 会完美识别你的 eslint.config.mjs
  1. Git 提交自动“洗地” (Husky + lint-staged)

想要代码仓库永远干干净净?在 package.json 中加入这道闸门:

json

"lint-staged": {
  "*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
  "*.{css,scss,json,md}": ["prettier --write"]
}

执行 npx husky init 并将 .husky/pre-commit 改为 npx lint-staged。现在,任何不符合规则的代码都别想溜进 Git 仓库!

五、 结语

规范不是为了限制自由,而是为了让开发者在 2025 年繁重的业务中,能拥有一份优雅的代码底座。

Vue 3 中开发高阶组件(HOC)与 Renderless 组件

作者 Isenberg
2025年12月20日 14:23

在 Vue 3 的组合式 API(Composition API)时代,虽然官方更推荐使用 Composables(组合函数) 来复用逻辑,但理解 高阶组件(Higher-Order Component, HOC) 和 Renderless 组件(无渲染组件) 仍然具有重要价值。它们不仅是 React 生态中的经典模式,在 Vue 中也有其适用场景,尤其在需要封装复杂状态逻辑并以组件形式暴露时。

本文将深入讲解如何在 Vue 3 中实现这两种模式,并通过实际案例展示其用法、优势与注意事项。


 

一、概念澄清

1. 高阶组件(HOC)

接收一个组件作为参数,返回一个新组件的函数。

const withLoading = (WrappedComponent) => {
  return {
    setup(props, { slots }) {
      // 添加 loading 逻辑
      const loading = ref(true);
      
      onMounted(() => {
        setTimeout(() => loading.value = false, 1000);
      });
      
      return () => h(WrappedComponent, {
        ...props,
        loading: loading.value
      });
    }
  };
};

2. Renderless 组件(无渲染组件)

不包含任何 DOM 结构,只提供逻辑和数据,通过作用域插槽(scoped slot)将状态传递给子组件。

<template>
  <slot 
    :loading="loading" 
    :startLoading="startLoading"
  />
</template>


<script setup>
import { ref } from 'vue';


const loading = ref(false);


const startLoading = () => {
  loading.value = true;
  setTimeout(() => loading.value = false, 1000);
};
</script>

✅ 关键区别:HOC:包装现有组件,注入 props,Renderless:自身不渲染 UI,通过 <slot> 暴露逻辑


 

二、实战:开发一个通用数据加载 HOC

场景

为任意组件添加自动数据加载能力,无需重复编写 loadingerrordata 状态管理。

步骤 1:定义 HOC 函数

// hoc/withAsyncData.js
import { defineComponent, ref, onMounted, h } from 'vue';


/**
 * 高阶组件:为组件注入异步数据加载能力
 * @param {Function} fetchFn - 数据获取函数 (返回 Promise)
 * @param {Object} options - 配置项
 * @returns {Component} 新组件
 */
export function withAsyncData(fetchFn, options = {}) {
  const {
    loadingProp = 'loading',
    dataProp = 'data',
    errorProp = 'error',
    autoLoad = true
  } = options;


  return (WrappedComponent) => {
    return defineComponent({
      name: `WithAsyncData(${WrappedComponent.name || 'Anonymous'})`,
      
      props: WrappedComponent.props ? { ...WrappedComponent.props } : {},
      
      setup(props, { attrs, slots }) {
        const loading = ref(false);
        const data = ref(null);
        const error = ref(null);


        const loadData = async () => {
          loading.value = true;
          error.value = null;
          
          try {
            const result = await fetchFn();
            data.value = result;
          } catch (err) {
            error.value = err;
          } finally {
            loading.value = false;
          }
        };


        if (autoLoad) {
          onMounted(loadData);
        }


        // 将状态作为 props 注入 WrappedComponent
        const injectedProps = {
          [loadingProp]: loading.value,
          [dataProp]: data.value,
          [errorProp]: error.value,
          // 提供重新加载方法
          reload: loadData
        };


        return () => h(
          WrappedComponent,
          {
            ...props,
            ...attrs,
            ...injectedProps
          },
          slots
        );
      }
    });
  };
}

步骤 2:使用 HOC

<!-- UserList.vue -->
<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error.message }}</div>
    <ul v-else>
      <li v-for="user in data" :key="user.id">{{ user.name }}</li>
    </ul>
    <button @click="reload">刷新</button>
  </div>
</template>


<script>
import { defineComponent } from 'vue';


export default defineComponent({
  name: 'UserList',
  props: ['loading', 'data', 'error', 'reload'] // 接收 HOC 注入的 props
});
</script>

 

<!-- App.vue -->
<template>
  <UserListWithAsyncData />
</template>


<script>
import UserList from './UserList.vue';
import { withAsyncData } from './hoc/withAsyncData';


// 创建增强后的组件
const UserListWithAsyncData = withAsyncData(
  () => fetch('/api/users').then(res => res.json()),
  { autoLoad: true }
)(UserList);


export default {
  components: {
    UserListWithAsyncData
  }
};
</script>

✅ 优势: 逻辑复用:任何列表组件都可快速获得加载能力; 类型安全:通过 props 明确接口; 可配置:支持自定义 prop 名称


 

三、实战:开发 Renderless 组件

场景

创建一个通用的计数器逻辑组件,不关心 UI 如何展示。

步骤 1:创建 Renderless 组件

<!-- renderless/CounterProvider.vue -->
<template>
  <!-- 无任何 DOM,只暴露逻辑 -->
  <slot 
    :count="count"
    :increment="increment"
    :decrement="decrement"
    :reset="reset"
    :isEven="isEven"
  />
</template>


<script setup>
import { ref, computed } from 'vue';


const props = defineProps({
  initialCount: {
    type: Number,
    default: 0
  },
  min: Number,
  max: Number
});


const count = ref(props.initialCount);


const increment = () => {
  if (props.max === undefined || count.value < props.max) {
    count.value++;
  }
};


const decrement = () => {
  if (props.min === undefined || count.value > props.min) {
    count.value--;
  }
};


const reset = () => {
  count.value = props.initialCount;
};


const isEven = computed(() => count.value % 2 === 0);
</script>

步骤 2:使用 Renderless 组件

<!-- App.vue -->
<template>
  <div>
    <!-- 方式1:基础用法 -->
    <CounterProvider v-slot="{ count, increment, decrement }">
      <p>当前计数: {{ count }}</p>
      <button @click="increment">+1</button>
      <button @click="decrement">-1</button>
    </CounterProvider>


    <!-- 方式2:高级用法(带限制) -->
    <CounterProvider 
      :initial-count="10" 
      :min="0" 
      :max="20"
      v-slot="{ count, increment, decrement, isEven }"
    >
      <div :class="{ even: isEven }">
        <h3>受限计数器 (0~20)</h3>
        <p>{{ count }} {{ isEven ? '(偶数)' : '(奇数)' }}</p>
        <button @click="increment" :disabled="count >= 20">+1</button>
        <button @click="decrement" :disabled="count <= 0">-1</button>
      </div>
    </CounterProvider>
  </div>
</template>


<script setup>
import CounterProvider from './renderless/CounterProvider.vue';
</script>


<style scoped>
.even { color: green; }
</style>

✅ 优势: 完全解耦逻辑与 UI; 灵活组合:同一个逻辑可适配多种 UI; 类型推导:IDE 可自动提示 slot 属性;


 

四、HOC vs Renderless vs Composables 对比

特性 HOC Renderless 组件 Composables
复用方式 包装组件 作用域插槽 函数调用
模板侵入性 低(使用者无感知) 中(需写 )
逻辑复杂度 适合简单 props 注入 适合状态+方法暴露 最灵活
TypeScript 支持 需手动处理类型 自动推导 slot 类型 最佳
Vue 3 推荐度 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐

📌 建议:

Vue 3 中优先使用 Composables,仅在以下情况考虑 HOC/Renderless: 需要以组件形式分发(如 UI 库); 与第三方组件集成(无法修改其内部逻辑); 团队习惯类 React 的开发模式;


 

五、Composables 替代方案(推荐)

上述功能用 Composables 实现更简洁:

// composables/useCounter.js
import { ref, computed, watch } from 'vue';


export function useCounter(initialValue = 0, { min, max } = {}) {
  const count = ref(initialValue);
  
  const increment = () => {
    if (max === undefined || count.value < max) count.value++;
  };
  
  const decrement = () => {
    if (min === undefined || count.value > min) count.value--;
  };
  
  const reset = () => count.value = initialValue;
  
  const isEven = computed(() => count.value % 2 === 0);
  
  // 监听 initialValue 变化
  watch(() => initialValue, (newVal) => {
    count.value = newVal;
  });
  
  return {
    count,
    increment,
    decrement,
    reset,
    isEven
  };
}

 

<!-- 使用 Composables -->
<script setup>
import { useCounter } from './composables/useCounter';


const { count, increment, decrement } = useCounter(0, { min: 0, max: 10 });
</script>


<template>
  <p>{{ count }}</p>
  <button @click="increment">+1</button>
  <button @click="decrement">-1</button>
</template>

 


六、最佳实践与注意事项

1. HOC 注意事项

  • 透传 Props/Attrs/Slots:确保包装组件的行为与原组件一致
  • 命名规范:使用 WithXxx 前缀(如 WithLoading
  • 避免嵌套过深:HOC 嵌套会导致调试困难

2. Renderless 组件注意事项

  • 明确 Slot 接口:使用 TypeScript 定义 slot props 类型
  • 避免过度设计:简单逻辑直接用 Composables
  • 文档说明:清晰标注暴露的 slot 属性

3. 性能优化

  • 缓存计算属性:使用 computed 而非方法
  • 按需响应:只暴露必要的状态
  • 清理副作用:在 onUnmounted 中清理定时器等

 

结语

虽然 Vue 3 的 Composition API 使得 Composables 成为逻辑复用的首选,但理解 HOC 和 Renderless 组件仍有其价值:

  • HOC 适合对现有组件进行“装饰”,尤其在无法修改组件源码时
  • Renderless 组件 在构建 UI 库时非常有用,允许用户完全控制渲染

Nginx 为什么能进行静态资源托管

作者 Isenberg
2025年12月20日 13:59

Nginx 本质是一个高性能的 HTTP 服务器,其核心能力之一就是直接读取服务器本地文件并通过 HTTP 协议返回给客户端。具体来说,它通过以下机制实现静态资源托管:

1. 事件驱动架构(非阻塞 I/O)

Nginx 采用 epoll/kqueue 等 I/O 多路复用技术,能在单个进程内高效处理数万并发连接,而不会为每个连接创建新进程/线程(避免资源开销)。

  • 对比 Apache:传统 Apache 采用多进程/多线程模型,并发量高时会因进程切换导致性能下降。
  • 优势:处理静态资源时,Nginx 能以极小的内存占用和 CPU 消耗支持高并发请求。

2. 文件系统直接读取

Nginx 可直接操作服务器文件系统,通过配置 root 或 alias 指令指定静态资源目录,例如:

server {
  root /usr/local/frontend/dist; # 静态资源根目录
  location /images/ {
    alias /data/pictures/; # 别名目录(与 root 区别:会替换 URL 中的 /images/)
  }
}

当客户端请求 http://example.com/index.html 时,Nginx 会直接读取 /usr/local/frontend/dist/index.html 并返回。

3. HTTP 协议实现

Nginx 内置完整的 HTTP 协议解析器,能正确处理:

  • 请求方法(GET/HEAD 等,静态资源常用 GET)
  • 请求头(如 Range 断点续传)
  • 响应头(如 Content-TypeCache-Control
  • 状态码(200/404/304 等)

例如,请求图片时自动返回 Content-Type: image/png,浏览器据此正确渲染资源。

 

⚡ 静态资源托管的核心配置指令

Nginx 通过以下关键指令控制静态资源的读取和响应行为:

1. root :指定资源根目录

location /static/ {
  root /usr/share/nginx/; 
  # 请求 /static/logo.png → 实际读取 /usr/share/nginx/static/logo.png
}

2. alias :替换 URL 路径(与 root 区别)

location /static/ {
  alias /usr/share/nginx/files/; 
  # 请求 /static/logo.png → 实际读取 /usr/share/nginx/files/logo.png(注意 alias 路径末尾的 /)
}

3. index :默认主页文件

server {
  index index.html index.htm; 
  # 请求 / → 自动返回 /index.html(按顺序查找)
}

4. try_files :按顺序尝试读取文件

解决 SPA(单页应用)路由刷新 404 问题:

location / {
  try_files $uri $uri/ /index.html; 
  # 尝试读取请求的文件 → 目录 → 最后返回 index.html
}
🧩 try_files $uri $uri/ /index.html; 的核心作用

一句话概括:当用户访问一个路径时,Nginx 会按顺序尝试查找文件或目录,找不到就兜底返回 index.html(前端 SPA 的入口文件)。

适用场景:

单页应用(如 Vue/React/Angular),这类应用的路由由前端 JavaScript 控制(如 vue-router 的 history 模式),而非传统的后端路由。

🔍 逐段解析:三个参数的含义
a. $uri :尝试访问请求的文件
  • $uri 是 Nginx 的内置变量,表示当前请求的 文件路径(不包含查询参数)。
  • 例如:
    用户请求 http://example.com/about → $uri 是 /about
    Nginx 会先检查服务器上是否存在 /usr/local/frontend/dist/about 文件(假设 root 指向 dist 目录)。
b. $uri/ :尝试访问请求的目录
  • 如果 $uri 对应的文件不存在,Nginx 会尝试将其作为 目录 访问(添加 /)。
  • 例如:
    请求 http://example.com/about → 检查 /usr/local/frontend/dist/about/ 目录是否存在,以及该目录下是否有 index.html(由 index 指令配置,如 index index.html)。
c. /index.html :兜底返回前端入口文件
  • 如果前两个尝试都失败(文件和目录都不存在),Nginx 会直接返回 root 目录下的 index.html(即前端 SPA 的入口文件)。
  • 此时,前端路由(如 vue-router)会根据 URL 中的路径(如 /about)渲染对应的页面组件,从而避免 404 错误。
d. history 模式 vs hash 模式
  • hash 模式(如 http://example.com/#/about ):哈希部分(#/about)不会发送到服务器,因此无需 try_files 也能正常刷新。
  • history 模式(如 http://example.com/about ):URL 路径会发送到服务器,必须配置 try_files 才能避免 404。
  • 推荐history 模式(URL 更美观)+ try_files 配置。
📝 为什么需要这行配置?

单页应用的路由是“前端接管”的,所有页面实际上都通过 index.html 加载,再由 JavaScript 根据 URL 动态渲染内容。

try_files $uri $uri/ /index.html; 的作用就是告诉 Nginx:“如果用户访问的路径不是真实存在的文件/目录,就把处理权交还给前端路由(通过返回 index.html)”。

5. expires Cache-Control :缓存控制

nginx
复制
location ~* .(js|css|png)$ {
  expires 30d; # 浏览器缓存 30 天
  add_header Cache-Control "public, max-age=2592000, immutable";
}

 

🚀 Nginx 静态资源托管的性能优化手段

除了基础能力,Nginx 还提供多种优化策略,让静态资源加载更快:

1. 启用 gzip 压缩

压缩 JS/CSS/HTML 等文本资源,减少传输体积:

nginx
复制
gzip on;
gzip_types text/css application/javascript text/html;
gzip_comp_level 5; # 压缩等级(1-9,越高压缩率越好但耗 CPU)

2. sendfile 零拷贝技术

跳过用户态与内核态的数据拷贝,直接从磁盘读取文件发送到网络:

nginx
复制
sendfile on; # 启用零拷贝
tcp_nopush on; # 配合 sendfile 使用,减少网络包数量

3. open_file_cache 缓存文件元信息

缓存文件的 inode、大小、修改时间等信息,避免重复 stat 系统调用:

nginx
复制
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;

4. 限制请求速率(防滥用)

nginx
复制
limit_rate 100k; # 单连接限速 100KB/s
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 10; # 单 IP 最多 10 个并发连接

 

📚 为什么不直接用浏览器打开 dist 目录的 HTML 文件?

虽然 dist 目录包含静态文件,但直接通过 file:///path/to/dist/index.html 打开会有问题:

  1. 跨域限制:浏览器禁止 file:// 协议下的 AJAX 请求(安全策略)。
  2. 路径错误:相对路径(如 ./js/app.js)会被解析为 file:// 协议,而非服务器 URL。
  3. 路由失效:SPA 路由(如 /about)会被浏览器视为本地文件路径,导致 404。

而 Nginx 提供了标准的 http:// 协议环境,完美解决以上问题。


 

📝 总结:Nginx 静态资源托管的核心优势

优势 具体说明
高性能 事件驱动架构 + 零拷贝技术,支持高并发低延迟
配置灵活 root/alias/try_files 等指令适配各种场景
功能丰富 内置缓存、压缩、限速、SSL 等能力
轻量稳定 内存占用低,故障率极低,7x24 小时运行无压力

简单说,Nginx 就像一个高效的"文件快递员" :既能快速找到服务器上的静态文件,又能通过各种优化手段把文件"快递"到用户浏览器,还能顺便处理缓存、压缩等"增值服务"。这也是它成为静态资源托管首选工具的根本原因!

ESM 模块(ECMAScript Module)详解

作者 Isenberg
2025年12月20日 13:53

ECMAScript 模块(ECMAScript Modules,简称 ESM)是 JavaScript 语言官方标准化的模块系统,自 ECMAScript 2015(ES6)起正式引入,并在后续版本中不断完善。作为现代 Web 开发的基石,ESM 不仅解决了长期以来 JavaScript 缺乏原生模块化支持的问题,还为构建高性能、可维护的前端和后端应用提供了统一标准。


 

一、JavaScript 模块化的历史演进

在 ESM 出现之前,JavaScript 社区长期缺乏官方模块系统,开发者依赖各种“约定”或工具实现模块化:

  • 全局变量模式:将功能挂载到全局对象(如 window.MyLib),极易造成命名冲突。
  • IIFE(立即调用函数表达式) :通过闭包实现私有作用域,但无法跨文件共享。
  • CommonJS:Node.js 采用的同步 require/module.exports 模式,适合服务端,但无法直接用于浏览器。
  • AMD(Asynchronous Module Definition) :如 RequireJS,支持异步加载,但语法复杂。
  • UMD(Universal Module Definition) :兼容 CommonJS、AMD 和全局变量的混合方案。

这些方案互不兼容,导致生态碎片化。开发者不得不依赖打包工具(如 Webpack、Browserify)将模块转换为目标环境可执行的代码。这种“编译时模块系统”虽解决了问题,但也带来了构建复杂度高、启动慢等弊端。

ESM 的出现,标志着 JavaScript 终于拥有了语言层面、运行时支持、跨平台统一的模块标准。


 

二、ESM 的核心语法与特性

ESM 采用声明式语法,强调静态结构显式依赖

1. 导出(Export)

命名导出(Named Exports)

// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Calculator { /* ... */ }


// 或批量导出
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;
export { subtract, multiply };

默认导出(Default Export)

// App.js
export default class App {
  // 一个模块只能有一个 default export
}

关键区别:命名导出可有多个,导入时需用相同名称(或重命名),默认导出无名称,导入时可任意命名

2. 导入(Import)

导入命名导出

import { PI, add } from './math.js';
import { subtract as minus } from './math.js'; // 重命名
import * as MathUtils from './math.js'; // 导入所有为命名空间对象

导入默认导出

import App from './App.js'; // 无需花括号

混合导入

import React, { useState, useEffect } from 'react';

副作用导入(仅执行模块,不导入绑定)

import './polyfills.js'; // 初始化全局补丁

3. 动态导入(Dynamic Import)

ES2020 引入 import() 表达式,支持运行时按需加载:

// 条件加载
if (user.isAdmin) {
  const adminModule = await import('./admin.js');
  adminModule.init();
}


// 路由懒加载(React/Vue 中常见)
const HomePage = lazy(() => import('./HomePage'));

⚠️ 注意:import() 返回 Promise,而静态 import 必须位于顶层作用域。


 

三、ESM 的核心特性与设计哲学

1. 静态分析(Static Analyzability)

ESM 的 import/export 语句必须是顶层的、字面量的,不能出现在条件语句或函数中:

// ❌ 非法
if (condition) {
  import utils from './utils.js'; // SyntaxError
}

这一限制使得引擎能在代码执行前解析整个依赖图,带来三大优势:

  • Tree Shaking:打包工具可精准移除未使用的导出(如 Rollup、Webpack)
  • 循环依赖检测:在编译阶段发现潜在问题
  • 性能优化:浏览器可并行预加载依赖

2. 实时绑定(Live Bindings)

ESM 导出的是绑定(binding) ,而非值的拷贝:

// counter.js
export let count = 0;
export function increment() { count++; }


// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 —— 自动同步!

这与 CommonJS 的“值拷贝”形成鲜明对比,避免了状态不一致问题。

3. 单例语义(Singleton Semantics)

每个模块在单个运行时环境中只执行一次,后续导入返回同一实例:

// config.js
console.log('Config loaded!');
export const settings = { theme: 'dark' };


// a.js 和 b.js 都 import config.js
// "Config loaded!" 仅打印一次

这保证了模块状态的全局唯一性,适用于配置、缓存等场景。


 

四、ESM 在浏览器中的运行机制

1. 启用方式

在 HTML 中通过 <script type="module"> 启用:

<script type="module" src="./main.js"></script>
<!-- 或内联 -->
<script type="module">
  import { greet } from './utils.js';
  greet();
</script>

🔒 安全限制

模块脚本默认启用 CORS,跨域需服务器设置 Access-Control-Allow-Origin

无法在 file:// 协议下运行(需本地服务器)

2. 加载与执行流程

当浏览器遇到模块脚本时:

  1. 解析依赖:递归解析所有 import 语句,构建依赖图
  2. 并行下载:通过 HTTP/2 多路复用并行请求所有模块
  3. 拓扑排序:按依赖顺序确定执行顺序(无依赖的先执行)
  4. 执行模块:每个模块仅执行一次,导出绑定供其他模块使用

💡 性能优势: 无需打包即可按需加载,浏览器缓存粒度更细(单个模块级别)

3. MIME 类型要求

服务器必须为 .js 文件返回正确的 MIME 类型:

Content-Type: application/javascript

否则浏览器会拒绝执行。


 

五、ESM 在 Node.js 中的支持

Node.js 自 v12 起原生支持 ESM,但需注意与 CommonJS 的互操作性。

1. 启用方式

  • 文件扩展名 .mjs
  • 或在 package.json 中设置 "type": "module"
  • 或使用 --input-type=module 标志运行字符串代码

2. 与 CommonJS 互操作

ESM 导入 CommonJS

// CommonJS 模块导出的是 module.exports 对象
import pkg from 'lodash'; // 默认导入整个对象
import { debounce } from 'lodash'; // 命名导入(需支持)

⚠️ 限制:CommonJS 模块的动态属性无法被静态分析,命名导入可能失败。

CommonJS 导入 ESM(Node.js v14.13+)

// 使用 async/await
const myModule = await import('./my-esm-module.js');

3. 路径解析差异

ESM 必须使用完整路径(包括扩展名):

// ✅ 正确
import { foo } from './foo.js';
import { bar } from './bar/index.js';


// ❌ 错误(Node.js 不自动补全 .js)
import { foo } from './foo';

🛠 解决方案:使用 --experimental-specifier-resolution=node 或构建工具处理。


 

六、ESM vs CommonJS:关键差异对比

特性 ESM CommonJS
加载时机 异步(浏览器并行加载) 同步(Node.js 逐行执行)
导出本质 实时绑定(Live Binding) 值拷贝(Copy of Value)
this 指向 undefined module.exports
循环依赖 支持(绑定未初始化时为 undefined) 支持(返回部分初始化对象)
Tree Shaking 原生支持 需工具模拟
顶层 await 支持(ES2022) 不支持(需 IIFE 包裹)

 

七、ESM 的实际应用场景

1. 前端开发:Vite、Snowpack 等现代构建工具

Vite 利用浏览器原生 ESM,实现无打包开发

  • 开发阶段直接 serve 源码
  • 依赖预构建为 ESM
  • HMR 基于模块图精准更新

2. 微前端架构

通过动态 import() 实现子应用按需加载:

const loadMicroApp = async (name) => {
  const app = await import(`https://cdn.com/${name}/entry.js`);
  app.bootstrap();
};

3. CDN 直接分发

现代 CDN(如 Skypack、esm.sh)将 npm 包自动转换为 ESM:

import React from 'https://esm.sh/react';
import { createRoot } from 'https://esm.sh/react-dom/client';

4. Web Workers 与 Service Workers

Workers 支持 ESM 模块:

// 主线程
const worker = new Worker('./worker.js', { type: 'module' });


// worker.js
import { heavyTask } from './utils.js';

 

八、未来展望

ESM 生态仍在快速发展:

Import Maps:允许在 HTML 中定义模块标识符映射,解决裸模块(bare specifiers)问题

<script type="importmap">
{
  "imports": {
    "lodash": "/node_modules/lodash-es/lodash.js"
  }
}
</script>
  • Top-Level Await:已在 ES2022 标准化,简化异步模块初始化
  • JSON Modules:提案阶段,允许直接 import data from './config.json'

 

结语

ECMAScript 模块不仅是 JavaScript 语言的一次重要进化,更是现代 Web 开发生态的基础设施。它通过静态分析、实时绑定、单例语义等设计,为构建高性能、可维护的应用提供了坚实基础。随着浏览器和 Node.js 的全面支持,以及 Vite 等工具的普及,ESM 正逐步取代历史遗留的模块方案,成为事实上的标准。

对于开发者而言,深入理解 ESM 的工作机制,不仅能写出更高效的代码,更能充分利用现代工具链的优势,在工程化实践中游刃有余。正如 TC39 委员会所倡导的:“ESM is the future of JavaScript modularity.” —— 拥抱 ESM,就是拥抱 JavaScript 的未来。

JavaScript 闭包详解:由浅入深掌握作用域与内存管理的艺术

作者 Isenberg
2025年12月20日 13:50

一、什么是闭包?——从直观现象入手

1.1 一个经典例子

先看一段代码:

function outer() {
  let count = 0;
  
  function inner() {
    count++;
    console.log(count);
  }
  
  return inner;
}


const counter = outer();
counter(); // 输出: 1
counter(); // 输出: 2
counter(); // 输出: 3

这里发生了什么?

  • outer 函数执行完毕后,按理说其内部变量 count 应该被销毁。
  • 但通过 counter() 调用 inner 函数时,count 不仅存在,还能被修改并保留状态。

这种 “函数即使在其词法作用域外被调用,仍能访问并操作其创建时所在作用域中的变量” 的现象,就是闭包。

1.2 官方定义

MDN 对闭包的定义是:

“闭包是指那些能够访问自由变量的函数。自由变量是指在函数中使用,但既不是函数参数也不是函数局部变量的变量。”

更通俗地说:闭包 = 函数 + 其创建时所处的词法环境(Lexical Environment)的引用

 

二、理解基础:作用域与词法环境

要真正理解闭包,必须先掌握 JavaScript 的作用域机制。

2.1 作用域(Scope)

作用域决定了变量的可访问范围。JavaScript 采用 词法作用域(Lexical Scoping) ,即变量的作用域在代码编写时就已确定,而非运行时。

let a = 1;


function foo() {
  console.log(a); // 会输出 1,因为 foo 定义在全局作用域内
}


function bar() {
  let a = 2;
  foo(); // 仍然输出 1!不是 2
}


bar();

尽管 foo 是在 bar 内部调用的,但它访问的是定义时所在的作用域(全局),而非调用时的作用域。这就是词法作用域的核心。

2.2 词法环境(Lexical Environment)

ES6 规范引入了 词法环境(Lexical Environment) 来精确描述作用域。

每个词法环境包含两个部分:

  • 环境记录(Environment Record) :存储变量和函数的映射(如 { count: 0 }
  • 对外部词法环境的引用(Outer Environment Reference) :指向父级作用域

当函数被创建时,它会捕获(capture) 当前的词法环境,并将其保存在内部属性 [[Environment]] 中。

关键点:闭包的本质,就是函数通过 [[Environment]] 引用“记住”了它出生时的环境。

 

三、闭包的形成机制:内存模型解析

让我们通过内存模型,可视化闭包的形成过程。

3.1 执行上下文与作用域链

当 JavaScript 引擎执行代码时,会为每个函数调用创建一个 执行上下文(Execution Context) ,其中包含:

  • 变量对象(Variable Object)
  • 作用域链(Scope Chain)
  • this 绑定

作用域链是一个从当前作用域逐级向上查找的链表,直到全局作用域。

3.2 闭包的内存结构

以之前的 counter 为例:

function outer() {
  let count = 0; // 存储在 outer 的词法环境中
  
  function inner() { // inner 的 [[Environment]] 指向 outer 的词法环境
    count++;
    console.log(count);
  }
  
  return inner;
}

outer() 执行时:

  1. 创建 outer 的执行上下文,初始化 count = 0
  2. 定义 inner 函数,其内部属性 [[Environment]] 指向 outer 的词法环境
  3. 返回 inner 函数引用

outer() 执行完毕:

  • outer 的执行上下文被弹出调用栈
  • inner 仍持有对 outer 词法环境的引用
  • 因此,count 不会被垃圾回收,继续存在于内存中

📌 重要结论

闭包导致外部函数的变量不会被释放,直到闭包本身不再被引用。

3.3 多个闭包共享同一环境

function createCounter() {
  let count = 0;
  
  return {
    increment: () => ++count,
    decrement: () => --count,
    value: () => count
  };
}


const counter = createCounter();
console.log(counter.value()); // 0
counter.increment();
console.log(counter.value()); // 1
counter.decrement();
console.log(counter.value()); // 0

这里 incrementdecrementvalue 三个函数都形成了闭包,共享同一个 count 变量。它们的 [[Environment]] 都指向 createCounter 的词法环境。

四、常见误区与陷阱

4.1 误区一:“只有返回函数才算闭包”

错误! 任何函数只要访问了其外部作用域的变量,就形成了闭包,无论是否被返回。

let globalVar = 'global';


function outer() {
  let outerVar = 'outer';
  
  function inner() {
    console.log(globalVar, outerVar); // 访问了外部变量 → 闭包
  }
  
  inner(); // 即使没有返回,inner 也是闭包
}


outer();

4.2 误区二:“闭包会导致内存泄漏”

不完全正确。 闭包确实会延长变量的生命周期,但这不是内存泄漏,而是预期行为。

真正的内存泄漏是指:无用的数据因错误引用而无法被垃圾回收

例如:

function setup() {
  const largeData = new Array(1000000).fill('*');
  
  document.getElementById('button').onclick = function() {
    console.log('Clicked');
    // 即使没用到 largeData,闭包仍会持有它!
  };
}

这里点击事件处理函数形成了闭包,无意中持有了 largeData 的引用,导致本可释放的大数组一直驻留内存。

解决方案:显式断开引用

function setup() {
  const largeData = new Array(1000000).fill('*');
  
  document.getElementById('button').onclick = function() {
    console.log('Clicked');
  };
  
  // 不再需要 largeData
  largeData = null;
}

4.3 经典陷阱:循环中的闭包

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出: 3, 3, 3
  }, 100);
}

原因var 声明的 i 是函数作用域,所有闭包共享同一个 i。当 setTimeout 执行时,循环早已结束,i = 3

解决方案

方案一:使用 let(块级作用域)

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出: 0, 1, 2
  }, 100);
}

let 为每次迭代创建新的绑定,每个闭包捕获的是不同的 i

方案二:IIFE(立即调用函数表达式)

for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => {
      console.log(j); // 输出: 0, 1, 2
    }, 100);
  })(i);
}

方案三:bind 传参

for (var i = 0; i < 3; i++) {
  setTimeout(console.log.bind(null, i), 100);
}

 

五、闭包的实际应用场景

5.1 模块模式(Module Pattern)

利用闭包实现私有变量和公共接口

const CounterModule = (function() {
  let count = 0; // 私有变量
  
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  };
})();


// 外部无法直接访问 count
console.log(CounterModule.getCount()); // 0
CounterModule.increment();
console.log(CounterModule.getCount()); // 1

这是 ES6 模块出现前最流行的封装方式。

5.2 函数柯里化(Currying)

function multiply(a) {
  return function(b) {
    return a * b;
  };
}


const double = multiply(2);
console.log(double(5)); // 10


// 或使用箭头函数
const multiply = a => b => a * b;

每个返回的函数都闭包了 a 的值。

5.3 防抖(Debounce)与节流(Throttle)

function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}


const debouncedSearch = debounce(searchAPI, 300);
input.addEventListener('input', debouncedSearch);

debounce 返回的函数闭包了 timeoutIdfunc,实现了状态保持。

5.4 事件处理器中的参数传递

function attachListeners() {
  const buttons = document.querySelectorAll('.btn');
  
  buttons.forEach((button, index) => {
    button.addEventListener('click', function() {
      console.log(`Button ${index} clicked`); // 闭包捕获 index
    });
  });
}

若不用闭包,很难在事件回调中获取循环索引。

5.5 缓存(Memoization)

function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}


const fib = memoize(function(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
});

缓存对象 cache 被闭包保护,避免全局污染。

 

六、闭包与 this 的交互

闭包会捕获变量,但不会捕获 thisthis 的绑定取决于调用方式。

const obj = {
  name: 'Alice',
  greet: function() {
    const sayHello = function() {
      console.log(this.name); // undefined! this 指向全局
    };
    sayHello();
  }
};


obj.greet();

解决方案

使用箭头函数(继承外层 this)

const obj = {
  name: 'Alice',
  greet: function() {
    const sayHello = () => {
      console.log(this.name); // 'Alice'
    };
    sayHello();
  }
};

显式绑定

const obj = {
  name: 'Alice',
  greet: function() {
    const self = this; // 闭包捕获 self
    const sayHello = function() {
      console.log(self.name); // 'Alice'
    };
    sayHello();
  }
};

 

七、性能考量与最佳实践

7.1 内存占用

闭包会阻止变量被垃圾回收,因此:

  • 避免不必要的闭包:如果函数不需要访问外部变量,不要嵌套定义
  • 及时释放大对象引用:如前述 largeData = null 的例子

7.2 调试困难

闭包中的变量在调试器中可能显示为 [[Scopes]],不易查看。建议:

  • 使用有意义的变量名
  • 避免过深的嵌套

7.3 最佳实践总结

  1. 理解作用域链:清楚知道变量从哪里来
  2. 谨慎使用闭包:只在需要保持状态或封装私有数据时使用
  3. 注意循环陷阱:优先使用 let 而非 var
  4. 管理内存:及时解除对大型数据的引用
  5. 利用现代语法:箭头函数简化 this 问题

 

八、闭包在现代 JavaScript 中的演进

8.1 与块级作用域的协同

ES6 的 let/const 与闭包结合,解决了经典循环问题,使代码更安全。

8.2 与模块系统的融合

ES6 模块(ESM)本质上是顶级闭包

// math.js
let privateVar = 0; // 模块作用域,外部不可见


export function increment() {
  return ++privateVar; // 闭包访问 privateVar
}

每个模块文件形成独立作用域,天然支持私有状态。

8.3 在 React Hooks 中的应用

React 的 useStateuseEffect 等 Hook 依赖闭包实现状态管理:

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1); // 闭包捕获 setCount
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 依赖项为空,只在挂载时执行
  
  return <div>{count}</div>;
}

若在 setInterval 回调中直接使用 count,会因闭包捕获旧值导致 bug,因此需使用函数式更新。

闭包在 Vue 项目中的应用

作者 Isenberg
2025年12月20日 13:46

闭包(Closure)作为 JavaScript 的核心特性,在 Vue 项目中有着广泛而精妙的应用。它不仅是 Vue 框架内部实现的重要机制,也是开发者编写高效、可维护代码的关键工具。


一、Vue 框架内部的闭包应用

1. 响应式系统(Reactivity System)

Vue 3 的响应式系统基于 Proxyeffect 实现,而 依赖收集(Dependency Collection) 的核心就是闭包。

// 简化版 Vue 3 响应式原理
let activeEffect = null;


function effect(fn) {
  activeEffect = fn; // 当前正在执行的副作用函数
  fn();              // 执行时会触发 getter
  activeEffect = null;
}


function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      if (activeEffect) {
        // 闭包:track 函数捕获了 key 和 activeEffect
        track(target, key, activeEffect);
      }
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      trigger(target, key); // 触发所有依赖该 key 的 effect
    }
  });
}


// track 函数内部使用闭包保存依赖关系
const depsMap = new WeakMap();
function track(target, key, effectFn) {
  let deps = depsMap.get(target);
  if (!deps) {
    deps = new Map();
    depsMap.set(target, deps);
  }
  let effects = deps.get(key);
  if (!effects) {
    effects = new Set();
    deps.set(key, effects);
  }
  effects.add(effectFn); // effectFn 是通过闭包传递进来的
}

关键点

effectFn(如组件 render 函数)通过闭包被保存在依赖集合中,当数据变化时,这些闭包函数被重新执行,实现视图更新

2. Computed 计算属性

计算属性的缓存机制依赖闭包保存状态:

function computed(getter) {
  let value;
  let dirty = true; // 是否需要重新计算
  
  const runner = effect(getter, {
    lazy: true,
    scheduler: () => {
      if (!dirty) {
        dirty = true;
        // 触发视图更新(通过闭包引用的 watcher)
      }
    }
  });
  
  return {
    // 闭包:get 捕获了 value、dirty、runner
    get value() {
      if (dirty) {
        value = runner();
        dirty = false;
      }
      return value;
    }
  };
}

每个 computed 实例通过闭包维护自己的 valuedirty 状态,实现精准缓存。

3. Watch 监听器

watch 的回调函数本质上是一个闭包,捕获了监听的数据和上下文:

watch(
  () => user.name, // 依赖源(闭包捕获 user)
  (newName, oldName) => {
    // 回调函数是闭包,可以访问组件实例、其他变量等
    console.log(`${oldName}${newName}`);
    this.sendAnalytics(newName); // 访问组件方法
  }
);

 

二、业务开发中的闭包应用

1. 封装私有状态(模块模式)

在 Vue 组件或工具函数中,利用闭包创建私有变量:

// utils/request.js
const createRequest = (baseURL) => {
  let token = null; // 私有变量,外部无法直接访问
  
  return {
    setToken(newToken) {
      token = newToken;
    },
    async get(url) {
      // 闭包捕获 token 和 baseURL
      const res = await fetch(`${baseURL}${url}`, {
        headers: { Authorization: `Bearer ${token}` }
      });
      return res.json();
    }
  };
};


// 在 Vue 组件中使用
export default {
  data() {
    return {
      api: createRequest('/api')
    };
  },
  mounted() {
    this.api.setToken(localStorage.getItem('token'));
    this.api.get('/user').then(user => {
      this.user = user;
    });
  }
};

优势:避免全局变量污染,实现数据封装

2. 防抖(Debounce)与节流(Throttle)

表单验证、搜索建议等场景常用防抖,其核心是闭包:

<template>
  <input v-model="searchText" @input="debouncedSearch" />
</template>


<script>
export default {
  data() {
    return {
      searchText: ''
    };
  },
  created() {
    // 创建防抖函数(闭包保存 timerId)
    this.debouncedSearch = this.debounce(this.search, 300);
  },
  methods: {
    debounce(func, delay) {
      let timeoutId;
      return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          func.apply(this, args);
        }, delay);
      };
    },
    async search() {
      const results = await this.$http.get(`/search?q=${this.searchText}`);
      this.results = results;
    }
  }
};
</script>

每个组件实例的 debouncedSearch 都有自己的 timeoutId,互不干扰。

 


详细解释:

在 JavaScript 中,箭头函数 (...args) => {} 使用了 剩余参数语法(Rest Parameters) ,它会把所有传给函数的实际参数收集到一个数组中。

javascript
复制
function example(...args) {
  console.log(args); // args 是一个数组,包含所有传入的参数
}

example(1, 'hello', true); 
// 输出: [1, 'hello', true]

 

🎯 举个实际例子:带参数的防抖函数

假设我们有一个搜索函数,每次用户输入时都要调用 API 查询结果:

javascript
复制
function searchAPI(query, category) {
  console.log(`Searching for "${query}" in ${category}`);
  // 模拟发起网络请求
}


// 创建防抖版本
const debouncedSearch = debounce(searchAPI, 500);


// 模拟用户多次输入
debouncedSearch('laptop', 'electronics'); // 参数会被收集到 args 中
debouncedSearch('laptop pro', 'electronics');
debouncedSearch('laptop pro max', 'electronics');


// 最终只会执行最后一次调用:
// Searching for "laptop pro max" in electronics

执行过程详解:

  1. 第一次调用 debouncedSearch('laptop', 'electronics')
    1. args = ['laptop', 'electronics']
    2. 设置定时器 A
  2. 第二次调用(500ms 内)
    1. 清除定时器 A
    2. 设置定时器 B,此时 args = ['laptop pro', 'electronics']
  3. 第三次调用(仍在 500ms 内)
    1. 清除定时器 B
    2. 设置定时器 C,此时 args = ['laptop pro max', 'electronics']
  4. 500ms 后无新调用
    1. 执行 func.apply(this, args)
    2. 等价于 searchAPI.call(this, 'laptop pro max', 'electronics')

 

🔍 func.apply(this, args) 的作用

这部分是防抖函数的关键设计,目的是保持原函数的调用上下文和参数传递

方法 作用
this 保持函数调用时的上下文(谁调用了这个函数)
args 保证原始参数完整传递给目标函数
apply() 以数组形式展开参数并绑定 this

 

3. 事件处理器中的参数传递

在循环渲染列表时,闭包解决事件参数问题:

<template>
  <div v-for="item in items" :key="item.id">
    <!-- 方式1:箭头函数(隐式闭包) -->
    <button @click="() => handleDelete(item.id)">删除</button>
    
    <!-- 方式2:方法返回函数(显式闭包) -->
    <button @click="getDeleteHandler(item.id)">删除</button>
  </div>
</template>


<script>
export default {
  methods: {
    handleDelete(id) {
      // 处理删除逻辑
    },
    // 返回一个闭包函数,捕获 id
    getDeleteHandler(id) {
      return () => {
        this.handleDelete(id);
      };
    }
  }
};
</script>

⚠️ 注意:避免在模板中直接写 @click="handleDelete(item.id)",这会在每次渲染时创建新函数,影响性能。

4. Composition API 中的闭包

Vue 3 的组合式 API 天然适合闭包:

// composables/useCounter.js
import { ref, computed } from 'vue';


export function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  
  // 闭包:以下函数共享 count
  const increment = () => count.value++;
  const decrement = () => count.value--;
  const doubled = computed(() => count.value * 2);
  
  return {
    count,
    increment,
    decrement,
    doubled
  };
}


// 在组件中使用
import { useCounter } from './composables/useCounter';


export default {
  setup() {
    const { count, increment, doubled } = useCounter(10);
    return { count, increment, doubled };
  }
};

每个 useCounter 调用都创建独立的作用域,状态完全隔离。

5. 高阶组件(HOC)与 Renderless 组件

通过闭包封装通用逻辑:

// composables/useFetch.js
export function useFetch(url) {
  const data = ref(null);
  const loading = ref(true);
  const error = ref(null);
  
  const fetchData = async () => {
    try {
      loading.value = true;
      const res = await fetch(url); // 闭包捕获 url
      data.value = await res.json();
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  };
  
  onMounted(fetchData);
  
  return { data, loading, error, refetch: fetchData };
}


// 在任意组件中复用
export default {
  setup() {
    const { data, loading } = useFetch('/api/users');
    return { data, loading };
  }
};

6. 缓存计算结果(Memoization)

对复杂计算进行缓存:

// composables/useExpensiveCalc.js
export function useExpensiveCalc(items) {
  const cache = new Map();
  
  const getResult = (filter) => {
    const key = JSON.stringify(filter);
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    // 模拟复杂计算
    const result = items.value
      .filter(item => item.type === filter.type)
      .map(item => ({ ...item, processed: true }));
    
    cache.set(key, result);
    return result;
  };
  
  // 提供清除缓存的方法
  const clearCache = () => cache.clear();
  
  return { getResult, clearCache };
}

闭包保护 cache 对象,避免全局污染。


 

三、闭包相关的常见问题与解决方案

1. 循环中的闭包陷阱(Vue 2 + var)

// ❌ 错误示例(Vue 2 中使用 var)
export default {
  data() {
    return { list: [1, 2, 3] };
  },
  mounted() {
    for (var i = 0; i < this.list.length; i++) {
      setTimeout(() => {
        console.log(i); // 全部输出 3
      }, 100);
    }
  }
};

解决方案

  • 使用 let 替代 var
  • 使用 forEachmap
  • 使用箭头函数
// ✅ 正确做法
mounted() {
  this.list.forEach((item, index) => {
    setTimeout(() => {
      console.log(index); // 0, 1, 2
    }, 100);
  });
}

2. 内存泄漏风险

在组件销毁时,及时清理闭包持有的资源:

export default {
  data() {
    return { timer: null };
  },
  mounted() {
    // 闭包持有 timer 引用
    this.timer = setInterval(() => {
      this.updateData();
    }, 1000);
  },
  beforeUnmount() {
    // 必须清理,否则闭包导致内存泄漏
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }
};

3. 闭包与 this 指向

在 Vue 2 选项式 API 中,注意 this 绑定:

export default {
  methods: {
    handleClick() {
      const self = this; // 保存 this 引用(闭包)
      
      setTimeout(function() {
        // 普通函数中 this 指向 window
        self.showMessage(); // 通过闭包访问组件实例
      }, 100);
      
      // 或使用箭头函数(自动继承 this)
      setTimeout(() => {
        this.showMessage(); // 正确
      }, 100);
    }
  }
};

四、最佳实践总结

场景 推荐做法 避免事项
状态封装 使用闭包创建私有变量 滥用全局变量
事件处理 在 methods 中定义,模板中引用 在模板中直接写内联函数
防抖节流 在 created/setup 中创建一次 每次渲染都创建新函数
循环索引 使用 let 或 forEach 在 for(var) 中使用闭包
资源清理 在 beforeUnmount 中清理定时器、监听器 忽略清理导致内存泄漏
Composition API 利用闭包实现逻辑复用 过度嵌套导致调试困难

结语

闭包在 Vue 项目中既是框架运行的基石,也是开发者手中的利器。理解其原理,能帮助我们:

  • 更好地使用 Vue 的响应式系统和 Composition API
  • 编写出高性能、低内存占用的组件
  • 避免常见的作用域陷阱和内存泄漏问题

Tanstack Router 文件命名速查表

2025年12月20日 13:24

1. 基础与嵌套 (Basic Structure)

符号 / 规则 作用 文件名示例 对应 URL / 效果 核心逻辑
__root.tsx 根组件 src/routes/__root.tsx 全局 整个应用的入口外壳 (HTML/Body)。
. (点) 嵌套层级 blog.post.tsx /blog/post 用点代替文件夹,扁平化写法。
index 默认页 posts.index.tsx /posts 父级路径的“首页” (精确匹配时显示)。
route.tsx 目录布局 settings/route.tsx /settings 等同于 settings.tsx,文件夹模式下的父级布局。

2. 动态参数 (Dynamic Params)

符号 / 规则 作用 文件名示例 对应 URL / 效果 核心逻辑
$ 变量/参数 posts.$id.tsx /posts/123



/posts/abc
$ 后的单词变为参数名 (如 params.id)。
$ (单独) 通配符 (Splat) files.$.tsx /files/a/b/c 贪婪匹配后面所有的路径片段。

3. 布局与组织 (Layouts & Organization)

符号 / 规则 作用 文件名示例 对应 URL / 效果 核心逻辑
_ (前缀) 无路径布局 _auth.login.tsx /login 影响 URL,只套用 _auth.tsx 的布局组件。
(xxx) 路由组 (app)/dashboard.tsx /dashboard 完全透明,仅用于文件夹分类 (如把管理端页面归类)。
- (前缀) 忽略文件 -components/Nav.tsx (无) 生成路由,可放组件、工具函数等。

4. 特殊处理 (Advanced)

符号 / 规则 作用 文件名示例 对应 URL / 效果 核心逻辑
_ (后缀) 非嵌套路由 posts_.$id.edit.tsx /posts/123/edit 逃离父级 posts.tsx 的布局,独立全屏渲染。
[ ] 转义字符 abc[.]def.tsx /abc.def 强制保留文件名中的点,不被解析为嵌套。

⚡️ 一眼看懂对比

为了加深印象,这几个容易混淆的用法我单独列出来对比:

  • posts.tsx 🆚 posts/route.tsx

    • 效果一样:都定义 /posts 这个层级的布局(父级)。

    • 区别:前者是扁平文件,后者是目录文件。

  • posts/index.tsx 🆚 posts.tsx

    • posts.tsx:是(Layout),不管去 /posts 还是 /posts/123 都在。

    • posts/index.tsx:是内容,只有访问 /posts 时才在框里显示。

  • _layout.a.tsx 🆚 layout.a.tsx

    • _layout.a.tsx (前缀 _):URL 是 /a (布局名隐身)。

    • layout.a.tsx (无前缀):URL 是 /layout/a (布局名是路径一部分)。

工程化工具类:模块化系统全解析与实践

作者 1024肥宅
2025年12月20日 13:11

引言

在前端开发的演进历程中,模块化一直是工程化实践的核心。从早期的脚本标签堆砌到现代的ES Modules,模块化技术极大地提升了代码的可维护性、复用性和协作效率。本文将深入探讨模块化的各个方面,包括模块加载器实现、规范演化、Polyfill技术,并补充构建工具、性能优化等工程化实践,全面解析模块化在现代前端开发中的应用。

一、实现简单的模块加载器

在理解复杂模块系统之前,我们先实现一个简单的模块加载器,了解其核心原理。

1.1 基础模块加载器实现
// 简单的模块注册表
const moduleRegistry = {};
const moduleCache = {};

// 模块定义函数
function define(name, dependencies, factory) {
  if (!moduleRegistry[name]) {
    moduleRegistry[name] = {
      dependencies,
      factory,
      resolved: false,
      exports: null
    };
  }
}

// 模块加载函数
function require(name) {
  // 检查缓存
  if (moduleCache[name]) {
    return moduleCache[name];
  }
  
  const module = moduleRegistry[name];
  if (!module) {
    throw new Error(`Module ${name} not found`);
  }
  
  // 解析依赖
  const resolvedDeps = module.dependencies.map(dep => {
    if (dep === 'exports' || dep === 'module') {
      return null; // 特殊处理
    }
    return require(dep);
  });
  
  // 执行工厂函数获取模块导出
  const factoryResult = module.factory.apply(null, resolvedDeps);
  
  // 缓存模块导出
  moduleCache[name] = factoryResult || {};
  module.resolved = true;
  
  return moduleCache[name];
}

// 使用示例
define('math', [], function() {
  return {
    add: (a, b) => a + b,
    multiply: (a, b) => a * b
  };
});

define('calculator', ['math'], function(math) {
  return {
    calculate: (x, y) => math.multiply(math.add(x, y), 2)
  };
});

// 使用模块
const calculator = require('calculator');
console.log(calculator.calculate(2, 3)); // 10
1.2 异步模块加载器
class AsyncModuleLoader {
  constructor() {
    this.modules = new Map();
    this.loading = new Map();
  }
  
  // 定义模块
  define(name, deps, factory) {
    this.modules.set(name, {
      deps,
      factory,
      exports: null,
      resolved: false
    });
  }
  
  // 异步加载模块
  async require(name) {
    if (this.modules.get(name)?.resolved) {
      return this.modules.get(name).exports;
    }
    
    // 防止重复加载
    if (this.loading.has(name)) {
      return this.loading.get(name);
    }
    
    // 创建加载Promise
    const loadPromise = this._loadModule(name);
    this.loading.set(name, loadPromise);
    
    return loadPromise;
  }
  
  async _loadModule(name) {
    const module = this.modules.get(name);
    if (!module) {
      throw new Error(`Module ${name} not found`);
    }
    
    // 加载所有依赖
    const depPromises = module.deps.map(dep => this.require(dep));
    const deps = await Promise.all(depPromises);
    
    // 执行工厂函数
    const exports = module.factory.apply(null, deps);
    
    // 更新模块状态
    module.exports = exports || {};
    module.resolved = true;
    this.loading.delete(name);
    
    return module.exports;
  }
}

// 使用示例
const loader = new AsyncModuleLoader();

loader.define('utils', [], () => ({
  format: str => str.toUpperCase()
}));

loader.define('app', ['utils'], (utils) => {
  return {
    run: () => console.log(utils.format('hello'))
  };
});

loader.require('app').then(app => app.run()); // 输出: HELLO

二、AMD规范实现

AMD(Asynchronous Module Definition)规范是RequireJS推广的异步模块定义标准。

2.1 简化的AMD实现
(function(global) {
  // 模块缓存
  const modules = {};
  const inProgress = {};
  
  // 定义函数
  function define(id, dependencies, factory) {
    if (arguments.length === 2) {
      factory = dependencies;
      dependencies = [];
    }
    
    modules[id] = {
      id: id,
      dependencies: dependencies,
      factory: factory,
      exports: null,
      resolved: false
    };
    
    // 尝试解析模块
    resolveModule(id);
  }
  
  // 依赖解析
  function resolveModule(id) {
    const module = modules[id];
    if (!module || module.resolved) return;
    
    // 检查依赖是否都可用
    const deps = module.dependencies;
    const missingDeps = deps.filter(dep => 
      !modules[dep] || !modules[dep].resolved
    );
    
    if (missingDeps.length === 0) {
      // 所有依赖已就绪,执行工厂函数
      executeModule(id);
    } else {
      // 等待依赖
      missingDeps.forEach(dep => {
        if (!inProgress[dep]) {
          inProgress[dep] = [];
        }
        inProgress[dep].push(id);
      });
    }
  }
  
  // 执行模块
  function executeModule(id) {
    const module = modules[id];
    if (module.resolved) return;
    
    // 获取依赖的exports
    const depExports = module.dependencies.map(dep => {
      if (dep === 'exports') return {};
      if (dep === 'require') return createRequire();
      if (dep === 'module') return { id: module.id, exports: {} };
      return modules[dep].exports;
    });
    
    // 执行工厂函数
    const exports = module.factory.apply(null, depExports);
    
    // 设置exports
    module.exports = exports || 
      (depExports[module.dependencies.indexOf('exports')] || {});
    module.resolved = true;
    
    // 通知等待此模块的其他模块
    if (inProgress[id]) {
      inProgress[id].forEach(dependentId => resolveModule(dependentId));
      delete inProgress[id];
    }
  }
  
  // 创建require函数
  function createRequire() {
    return function(ids, callback) {
      if (typeof ids === 'string') ids = [ids];
      
      Promise.all(ids.map(loadModule))
        .then(modules => {
          if (callback) callback.apply(null, modules);
        });
    };
  }
  
  // 异步加载模块
  function loadModule(id) {
    return new Promise((resolve, reject) => {
      if (modules[id] && modules[id].resolved) {
        resolve(modules[id].exports);
        return;
      }
      
      // 动态加载脚本
      const script = document.createElement('script');
      script.src = id + '.js';
      script.onload = () => {
        // 等待模块解析
        const checkInterval = setInterval(() => {
          if (modules[id] && modules[id].resolved) {
            clearInterval(checkInterval);
            resolve(modules[id].exports);
          }
        }, 10);
      };
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }
  
  // 暴露到全局
  global.define = define;
  global.require = createRequire();
  
})(typeof window !== 'undefined' ? window : global);

// 使用示例
define('math', [], function() {
  return {
    add: function(a, b) { return a + b; }
  };
});

define('app', ['math', 'require'], function(math, require) {
  return {
    calculate: function() {
      return math.add(1, 2);
    },
    loadExtra: function() {
      require(['utils'], function(utils) {
        console.log('Utils loaded');
      });
    }
  };
});

require(['app'], function(app) {
  console.log(app.calculate()); // 3
});

三、CMD规范实现

CMD(Common Module Definition)规范由Sea.js推广,强调就近依赖。

3.1 简化的CMD实现
(function(global) {
  const modules = {};
  const factories = {};
  const cache = {};
  
  // 模块状态
  const STATUS = {
    PENDING: 0,
    LOADING: 1,
    LOADED: 2,
    EXECUTING: 3,
    EXECUTED: 4
  };
  
  // 定义函数
  function define(factory) {
    // 获取当前脚本
    const scripts = document.getElementsByTagName('script');
    const currentScript = scripts[scripts.length - 1];
    const id = currentScript.src.replace(/\.js$/, '');
    
    factories[id] = factory;
    modules[id] = {
      id: id,
      factory: factory,
      deps: [],
      exports: null,
      status: STATUS.PENDING,
      callbacks: []
    };
    
    // 解析依赖
    parseDependencies(id);
  }
  
  // 解析依赖
  function parseDependencies(id) {
    const factory = factories[id];
    if (!factory) return;
    
    const source = factory.toString();
    const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
    const deps = [];
    let match;
    
    while ((match = requireRegex.exec(source)) !== null) {
      deps.push(match[1]);
    }
    
    modules[id].deps = deps;
  }
  
  // 异步加载模块
  function require(id, callback) {
    const module = modules[id];
    
    if (module && module.status === STATUS.EXECUTED) {
      // 模块已执行,直接返回
      if (callback) {
        callback(module.exports);
      }
      return module.exports;
    }
    
    // 模块未加载,开始加载
    if (!module || module.status === STATUS.PENDING) {
      return loadModule(id, callback);
    }
    
    // 模块加载中,添加回调
    if (module.status < STATUS.EXECUTED) {
      module.callbacks.push(callback);
    }
  }
  
  // 加载模块
  function loadModule(id, callback) {
    const module = modules[id] || (modules[id] = {
      id: id,
      deps: [],
      exports: null,
      status: STATUS.LOADING,
      callbacks: callback ? [callback] : []
    });
    
    // 创建script标签加载
    const script = document.createElement('script');
    script.src = id + '.js';
    script.async = true;
    
    script.onload = function() {
      module.status = STATUS.LOADED;
      executeModule(id);
    };
    
    script.onerror = function() {
      console.error(`Failed to load module: ${id}`);
    };
    
    document.head.appendChild(script);
    
    return null;
  }
  
  // 执行模块
  function executeModule(id) {
    const module = modules[id];
    if (!module || module.status >= STATUS.EXECUTING) return;
    
    module.status = STATUS.EXECUTING;
    
    // 收集依赖
    const deps = module.deps;
    const depValues = deps.map(depId => {
      const depModule = modules[depId];
      if (depModule && depModule.status === STATUS.EXECUTED) {
        return depModule.exports;
      }
      // 同步加载依赖(简化实现)
      return require(depId);
    });
    
    // 执行工厂函数
    const factory = factories[id];
    if (!factory) {
      throw new Error(`Factory not found for module: ${id}`);
    }
    
    // 提供require、exports、module参数
    const localRequire = function(depId) {
      return require(depId);
    };
    
    const localExports = {};
    const localModule = { exports: localExports };
    
    // 执行
    const result = factory.call(null, localRequire, localExports, localModule);
    
    // 设置exports
    module.exports = localModule.exports || result || localExports;
    module.status = STATUS.EXECUTED;
    
    // 执行回调
    module.callbacks.forEach(cb => cb(module.exports));
    module.callbacks = [];
  }
  
  // 暴露全局
  global.define = define;
  global.require = require;
  
})(typeof window !== 'undefined' ? window : global);

// 使用示例
// 文件: math.js
define(function(require, exports, module) {
  module.exports = {
    add: function(a, b) {
      return a + b;
    }
  };
});

// 文件: app.js
define(function(require, exports, module) {
  var math = require('math');
  
  exports.calculate = function() {
    return math.add(1, 2);
  };
});

// 主文件
require('app', function(app) {
  console.log(app.calculate()); // 3
});

四、ES Module的简单Polyfill

虽然现代浏览器支持ES Modules,但在某些场景下,我们仍需要Polyfill支持。

4.1 基础ESM Polyfill实现
// ES Module Polyfill
(function() {
  const moduleMap = new Map();
  const moduleCache = new Map();
  
  // 拦截import语句(通过动态import实现)
  window.importModule = async function(modulePath) {
    // 检查缓存
    if (moduleCache.has(modulePath)) {
      return moduleCache.get(modulePath);
    }
    
    // 加载模块代码
    const code = await fetchModule(modulePath);
    
    // 解析依赖
    const deps = extractDependencies(code);
    
    // 加载依赖
    const depPromises = deps.map(dep => 
      importModule(resolvePath(modulePath, dep))
    );
    const dependencies = await Promise.all(depPromises);
    
    // 执行模块
    const moduleExports = {};
    const module = {
      exports: moduleExports
    };
    
    // 创建包装函数
    const wrapper = createWrapper(code, dependencies);
    wrapper(
      moduleExports, // exports
      module,        // module
      modulePath     // __filename(模拟)
    );
    
    // 缓存结果
    const exports = module.exports === moduleExports ? 
      moduleExports : module.exports;
    moduleCache.set(modulePath, exports);
    
    return exports;
  };
  
  // 提取依赖
  function extractDependencies(code) {
    const importRegex = /import\s+.*?\s+from\s+['"](.*?)['"]/g;
    const dynamicImportRegex = /import\s*\(['"](.*?)['"]\)/g;
    const deps = new Set();
    
    let match;
    while ((match = importRegex.exec(code)) !== null) {
      deps.add(match[1]);
    }
    
    // 重置正则
    importRegex.lastIndex = 0;
    
    while ((match = dynamicImportRegex.exec(code)) !== null) {
      deps.add(match[1]);
    }
    
    return Array.from(deps);
  }
  
  // 创建包装函数
  function createWrapper(code, dependencies) {
    const wrapperCode = `
      (function(exports, module, __filename, __dirname) {
        // 注入依赖
        const [
          ${dependencies.map((_, i) => `__dep${i}`).join(', ')}
        ] = arguments[4];
        
        ${code}
        
        // 返回默认导出
        return module.exports && module.exports.default ?
          module.exports.default : module.exports;
      })
    `;
    
    return eval(wrapperCode);
  }
  
  // 解析路径
  function resolvePath(basePath, targetPath) {
    if (targetPath.startsWith('./') || targetPath.startsWith('../')) {
      const baseDir = basePath.substring(0, basePath.lastIndexOf('/'));
      return new URL(targetPath, baseDir + '/').pathname;
    }
    return targetPath;
  }
  
  // 获取模块代码
  async function fetchModule(path) {
    const response = await fetch(path);
    if (!response.ok) {
      throw new Error(`Failed to load module: ${path}`);
    }
    return response.text();
  }
  
  // 拦截script type="module"
  interceptModuleScripts();
  
  function interceptModuleScripts() {
    const originalCreateElement = document.createElement;
    
    document.createElement = function(tagName) {
      const element = originalCreateElement.call(document, tagName);
      
      if (tagName === 'script') {
        const originalSetAttribute = element.setAttribute.bind(element);
        
        element.setAttribute = function(name, value) {
          originalSetAttribute(name, value);
          
          if (name === 'type' && value === 'module') {
            // 拦截模块脚本
            const src = element.getAttribute('src');
            if (src) {
              element.type = 'text/javascript';
              importModule(src).then(() => {
                if (element.onload) element.onload();
              }).catch(err => {
                if (element.onerror) element.onerror(err);
              });
            }
          }
        };
      }
      
      return element;
    };
  }
})();

// 使用示例
// 模块文件: utils.js
export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export default function greet(name) {
  return `Hello, ${capitalize(name)}!`;
}

// 主文件
importModule('./utils.js').then(utils => {
  console.log(utils.default('world')); // Hello, World!
  console.log(utils.capitalize('test')); // Test
});
4.2 支持Tree Shaking的ESM Polyfill
class ESMCompat {
  constructor() {
    this.modules = new Map();
    this.usedExports = new Set();
  }
  
  // 注册模块
  register(name, code) {
    const ast = this.parse(code);
    const exports = this.extractExports(ast);
    
    this.modules.set(name, {
      code,
      ast,
      exports,
      used: new Set()
    });
  }
  
  // 解析代码为AST(简化版)
  parse(code) {
    // 简化实现:实际应使用Babel等解析器
    const exportMatches = code.match(/export\s+(const|let|var|function|class|default)\s+(\w+)/g) || [];
    const imports = code.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g) || [];
    
    return {
      exports: exportMatches.map(match => ({
        type: match.split(' ')[1],
        name: match.split(' ')[2]
      })),
      imports: imports.map(match => {
        const parts = match.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
        return {
          specifiers: parts[1].split(',').map(s => s.trim()),
          source: parts[2]
        };
      })
    };
  }
  
  // 提取导出
  extractExports(ast) {
    return ast.exports.map(exp => exp.name);
  }
  
  // 使用模块(标记使用的导出)
  use(name, ...exports) {
    const module = this.modules.get(name);
    if (module) {
      exports.forEach(exp => {
        if (module.exports.includes(exp)) {
          module.used.add(exp);
        }
      });
    }
  }
  
  // 生成优化后的代码
  generateOptimized(name) {
    const module = this.modules.get(name);
    if (!module) return '';
    
    let code = module.code;
    
    // 移除未使用的导出(简化实现)
    module.exports.forEach(exp => {
      if (!module.used.has(exp)) {
        const regex = new RegExp(`export\\s+.*?\\b${exp}\\b[^;]*;`, 'g');
        code = code.replace(regex, '');
      }
    });
    
    return code;
  }
}

// 使用示例
const compat = new ESMCompat();

compat.register('math', `
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function unusedFunction() { return 'unused'; }
`);

// 标记使用的导出
compat.use('math', 'PI', 'add');

// 生成优化代码
console.log(compat.generateOptimized('math'));
// 输出将只包含PI和add的导出

五、模块化构建工具集成

现代开发中,我们使用构建工具处理模块化。以下展示如何集成Webpack-like的简单打包器。

5.1 简易模块打包器
const fs = require('fs');
const path = require('path');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');

class SimpleBundler {
  constructor(entry) {
    this.entry = entry;
    this.modules = new Map();
    this.moduleId = 0;
  }
  
  // 构建
  build(outputPath) {
    const entryModule = this.collectDependencies(this.entry);
    const bundleCode = this.generateBundle(entryModule);
    
    fs.writeFileSync(outputPath, bundleCode);
    console.log(`Bundle generated: ${outputPath}`);
  }
  
  // 收集依赖
  collectDependencies(filePath) {
    const fileContent = fs.readFileSync(filePath, 'utf-8');
    const ast = parse(fileContent, {
      sourceType: 'module',
      plugins: ['jsx']
    });
    
    const dependencies = [];
    const dirname = path.dirname(filePath);
    
    // 遍历AST收集import语句
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        const importPath = node.source.value;
        const absolutePath = this.resolvePath(importPath, dirname);
        dependencies.push(absolutePath);
      },
      CallExpression: ({ node }) => {
        if (node.callee.type === 'Import') {
          const importPath = node.arguments[0].value;
          const absolutePath = this.resolvePath(importPath, dirname);
          dependencies.push(absolutePath);
        }
      }
    });
    
    const moduleId = this.moduleId++;
    const module = {
      id: moduleId,
      filePath,
      code: fileContent,
      dependencies,
      mapping: {}
    };
    
    this.modules.set(filePath, module);
    
    // 递归收集依赖
    dependencies.forEach(dep => {
      if (!this.modules.has(dep)) {
        this.collectDependencies(dep);
      }
    });
    
    return module;
  }
  
  // 解析路径
  resolvePath(importPath, baseDir) {
    if (importPath.startsWith('.')) {
      return path.resolve(baseDir, importPath);
    }
    // 处理node_modules(简化)
    const nodeModulePath = path.resolve(process.cwd(), 'node_modules', importPath);
    if (fs.existsSync(nodeModulePath)) {
      return nodeModulePath;
    }
    return importPath;
  }
  
  // 生成打包代码
  generateBundle(entryModule) {
    const modules = [];
    
    // 创建模块映射
    this.modules.forEach(module => {
      const transformedCode = this.transformModule(module);
      modules.push(`
        ${module.id}: {
          factory: function(require, module, exports) {
            ${transformedCode}
          },
          mapping: ${JSON.stringify(module.mapping)}
        }
      `);
    });
    
    // 生成运行时
    return `
      (function(modules) {
        const moduleCache = {};
        
        function require(id) {
          if (moduleCache[id]) {
            return moduleCache[id].exports;
          }
          
          const mod = modules[id];
          const localRequire = function(modulePath) {
            return require(mod.mapping[modulePath]);
          };
          
          const module = { exports: {} };
          mod.factory(localRequire, module, module.exports);
          
          moduleCache[id] = module;
          return module.exports;
        }
        
        // 启动入口模块
        require(0);
      })({
        ${modules.join(',\n')}
      });
    `;
  }
  
  // 转换模块代码
  transformModule(module) {
    const ast = parse(module.code, {
      sourceType: 'module'
    });
    
    // 构建路径映射
    let importIndex = 0;
    
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        const importPath = node.source.value;
        const depModule = this.modules.get(
          this.resolvePath(importPath, path.dirname(module.filePath))
        );
        
        if (depModule) {
          const importName = `__import_${importIndex++}`;
          module.mapping[importPath] = depModule.id;
          
          // 替换import语句
          const specifiers = node.specifiers.map(spec => {
            if (t.isImportDefaultSpecifier(spec)) {
              return t.variableDeclarator(
                spec.local,
                t.memberExpression(
                  t.identifier(importName),
                  t.identifier('default')
                )
              );
            } else {
              return t.variableDeclarator(
                spec.local,
                t.memberExpression(
                  t.identifier(importName),
                  spec.imported || spec.local
                )
              );
            }
          });
          
          return t.variableDeclaration('const', specifiers);
        }
      }
    });
    
    // 移除export语句
    traverse(ast, {
      ExportNamedDeclaration: ({ node, remove }) => {
        if (node.declaration) {
          return node.declaration;
        }
        remove();
      },
      ExportDefaultDeclaration: ({ node }) => {
        return t.expressionStatement(
          t.assignmentExpression(
            '=',
            t.memberExpression(
              t.identifier('module'),
              t.identifier('exports')
            ),
            t.objectExpression([
              t.objectProperty(
                t.identifier('default'),
                node.declaration
              )
            ])
          )
        );
      }
    });
    
    const { code } = generate(ast);
    return code;
  }
}

// 使用示例
const bundler = new SimpleBundler('./src/index.js');
bundler.build('./dist/bundle.js');

六、模块联邦与微前端架构

模块联邦(Module Federation)是Webpack 5引入的重要特性,支持跨应用共享模块。

6.1 简易模块联邦实现
// 模块联邦管理器
class ModuleFederation {
  constructor(config) {
    this.config = config;
    this.remotes = new Map();
    this.exposes = new Map();
    this.shared = new Map();
    this.init();
  }
  
  init() {
    // 初始化共享模块
    if (this.config.shared) {
      Object.entries(this.config.shared).forEach(([name, config]) => {
        this.shared.set(name, {
          module: require(name),
          version: config.version,
          singleton: config.singleton || false
        });
      });
    }
    
    // 初始化暴露模块
    if (this.config.exposes) {
      Object.entries(this.config.exposes).forEach(([name, modulePath]) => {
        this.exposes.set(name, require(modulePath));
      });
    }
  }
  
  // 注册远程应用
  async registerRemote(name, url) {
    try {
      const remoteManifest = await this.fetchRemoteManifest(url);
      this.remotes.set(name, {
        url,
        manifest: remoteManifest
      });
      console.log(`Remote ${name} registered`);
    } catch (error) {
      console.error(`Failed to register remote ${name}:`, error);
    }
  }
  
  // 获取远程清单
  async fetchRemoteManifest(url) {
    const response = await fetch(`${url}/federation-manifest.json`);
    return response.json();
  }
  
  // 获取模块
  async getModule(remoteName, moduleName) {
    // 检查共享模块
    if (this.shared.has(moduleName)) {
      return this.shared.get(moduleName).module;
    }
    
    // 检查本地暴露
    if (this.exposes.has(moduleName)) {
      return this.exposes.get(moduleName);
    }
    
    // 检查远程模块
    const remote = this.remotes.get(remoteName);
    if (remote) {
      return this.loadRemoteModule(remote, moduleName);
    }
    
    throw new Error(`Module ${moduleName} not found`);
  }
  
  // 加载远程模块
  async loadRemoteModule(remote, moduleName) {
    const moduleUrl = `${remote.url}/${moduleName}.js`;
    
    // 动态加载脚本
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = moduleUrl;
      
      script.onload = () => {
        // 假设远程模块会暴露到全局
        const module = window[`${remote.name}_${moduleName}`];
        if (module) {
          resolve(module);
        } else {
          reject(new Error(`Module ${moduleName} not found in remote`));
        }
      };
      
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }
  
  // 暴露模块
  expose(name, module) {
    this.exposes.set(name, module);
    // 暴露到全局(供远程访问)
    window[`${this.config.name}_${name}`] = module;
  }
}

// 使用示例
// App 1 配置
const federation1 = new ModuleFederation({
  name: 'app1',
  exposes: {
    './Button': './src/components/Button.js'
  },
  shared: {
    react: { singleton: true, version: '17.0.0' },
    'react-dom': { singleton: true, version: '17.0.0' }
  }
});

// App 2 配置
const federation2 = new ModuleFederation({
  name: 'app2',
  remotes: {
    app1: 'http://localhost:3001'
  },
  shared: {
    react: { singleton: true, version: '17.0.0' }
  }
});

// App2中使用App1的模块
federation2.getModule('app1', 'Button').then(Button => {
  // 使用远程Button组件
  console.log('Remote Button loaded:', Button);
});
七、模块化性能优化
7.1 代码分割与懒加载
class CodeSplitter {
  constructor() {
    this.chunks = new Map();
    this.loadedChunks = new Set();
  }
  
  // 定义代码分割点
  defineChunk(name, getChunk) {
    this.chunks.set(name, getChunk);
  }
  
  // 懒加载代码块
  async loadChunk(name) {
    if (this.loadedChunks.has(name)) {
      return;
    }
    
    const getChunk = this.chunks.get(name);
    if (!getChunk) {
      throw new Error(`Chunk ${name} not found`);
    }
    
    // 标记为加载中
    this.loadedChunks.add(name);
    
    try {
      await getChunk();
      console.log(`Chunk ${name} loaded`);
    } catch (error) {
      this.loadedChunks.delete(name);
      throw error;
    }
  }
  
  // 预加载代码块
  preloadChunk(name) {
    if (this.loadedChunks.has(name)) return;
    
    const link = document.createElement('link');
    link.rel = 'preload';
    link.as = 'script';
    
    const getChunk = this.chunks.get(name);
    if (getChunk && getChunk.chunkPath) {
      link.href = getChunk.chunkPath;
      document.head.appendChild(link);
    }
  }
}

// Webpack动态导入兼容
function dynamicImport(modulePath) {
  if (typeof __webpack_require__ !== 'undefined') {
    // Webpack环境
    return import(/* webpackChunkName: "[request]" */ modulePath);
  } else {
    // 原生环境
    return import(modulePath);
  }
}

// 使用示例
const splitter = new CodeSplitter();

// 定义代码块
splitter.defineChunk('dashboard', () => 
  dynamicImport('./Dashboard.js')
);

splitter.defineChunk('analytics', () => 
  dynamicImport('./Analytics.js')
);

// 路由懒加载
async function loadRoute(routeName) {
  switch (routeName) {
    case 'dashboard':
      await splitter.loadChunk('dashboard');
      break;
    case 'analytics':
      await splitter.loadChunk('analytics');
      break;
  }
}

// 预加载
window.addEventListener('mouseover', (e) => {
  if (e.target.href && e.target.href.includes('dashboard')) {
    splitter.preloadChunk('dashboard');
  }
});
7.2 模块缓存策略
class ModuleCache {
  constructor() {
    this.cache = new Map();
    this.ttl = 5 * 60 * 1000; // 5分钟
    this.maxSize = 100; // 最大缓存模块数
  }
  
  // 获取模块
  async get(key, fetchModule) {
    const cached = this.cache.get(key);
    
    // 检查缓存是否有效
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      console.log(`Cache hit: ${key}`);
      return cached.module;
    }
    
    // 缓存失效或不存在,重新获取
    console.log(`Cache miss: ${key}`);
    const module = await fetchModule();
    
    // 更新缓存
    this.set(key, module);
    
    return module;
  }
  
  // 设置缓存
  set(key, module) {
    // 清理过期缓存
    this.cleanup();
    
    this.cache.set(key, {
      module,
      timestamp: Date.now()
    });
  }
  
  // 清理缓存
  cleanup() {
    const now = Date.now();
    
    // 清理过期
    for (const [key, value] of this.cache) {
      if (now - value.timestamp > this.ttl) {
        this.cache.delete(key);
      }
    }
    
    // 清理超出大小限制的(LRU策略)
    if (this.cache.size > this.maxSize) {
      const entries = Array.from(this.cache.entries());
      entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
      
      for (let i = 0; i < entries.length - this.maxSize; i++) {
        this.cache.delete(entries[i][0]);
      }
    }
  }
  
  // 清空缓存
  clear() {
    this.cache.clear();
  }
}

// 使用示例
const moduleCache = new ModuleCache();

async function loadModuleWithCache(modulePath) {
  return moduleCache.get(modulePath, async () => {
    const response = await fetch(modulePath);
    return response.text();
  });
}

八、模块化最佳实践与工程化

8.1 模块设计原则
// 1. 单一职责原则
// 不好的例子
class UserManager {
  // 混合了用户管理、验证、通知等多个职责
}

// 好的例子
class UserRepository {
  // 只负责数据访问
}

class UserValidator {
  // 只负责验证
}

class UserNotifier {
  // 只负责通知
}

// 2. 依赖注入
class UserService {
  constructor(userRepository, validator, notifier) {
    this.userRepository = userRepository;
    this.validator = validator;
    this.notifier = notifier;
  }
  
  async register(user) {
    if (!this.validator.validate(user)) {
      throw new Error('Invalid user');
    }
    
    await this.userRepository.save(user);
    await this.notifier.sendWelcome(user.email);
  }
}

// 3. 接口抽象
// 定义接口
class IStorage {
  async save(key, value) {}
  async get(key) {}
  async delete(key) {}
}

// 具体实现
class LocalStorage extends IStorage {
  async save(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }
  
  async get(key) {
    return JSON.parse(localStorage.getItem(key));
  }
  
  async delete(key) {
    localStorage.removeItem(key);
  }
}

class APIService {
  constructor(storage) {
    if (!(storage instanceof IStorage)) {
      throw new Error('Invalid storage implementation');
    }
    this.storage = storage;
  }
}
8.2 模块版本管理与升级
class ModuleVersionManager {
  constructor() {
    this.versions = new Map();
    this.deprecations = new Map();
  }
  
  // 注册模块版本
  register(moduleName, version, module) {
    if (!this.versions.has(moduleName)) {
      this.versions.set(moduleName, new Map());
    }
    
    this.versions.get(moduleName).set(version, module);
  }
  
  // 获取模块(支持语义化版本)
  get(moduleName, versionRange = 'latest') {
    const moduleVersions = this.versions.get(moduleName);
    if (!moduleVersions) {
      throw new Error(`Module ${moduleName} not found`);
    }
    
    if (versionRange === 'latest') {
      const latestVersion = Array.from(moduleVersions.keys())
        .sort(this.compareVersions)
        .pop();
      return moduleVersions.get(latestVersion);
    }
    
    // 简化的版本范围解析
    const availableVersions = Array.from(moduleVersions.keys())
      .filter(v => this.satisfiesVersion(v, versionRange))
      .sort(this.compareVersions);
    
    if (availableVersions.length === 0) {
      throw new Error(`No version of ${moduleName} satisfies ${versionRange}`);
    }
    
    return moduleVersions.get(availableVersions.pop());
  }
  
  // 比较版本
  compareVersions(v1, v2) {
    const parts1 = v1.split('.').map(Number);
    const parts2 = v2.split('.').map(Number);
    
    for (let i = 0; i < 3; i++) {
      if (parts1[i] !== parts2[i]) {
        return parts1[i] - parts2[i];
      }
    }
    
    return 0;
  }
  
  // 检查版本是否满足范围
  satisfiesVersion(version, range) {
    // 简化实现,实际应使用semver库
    if (range === '*') return true;
    
    const [op, versionRange] = range.match(/^([>=<~^]*)(\d+\.\d+\.\d+)$/).slice(1);
    const vParts = version.split('.').map(Number);
    const rParts = versionRange.split('.').map(Number);
    
    switch (op) {
      case '^': // 兼容版本
        return vParts[0] === rParts[0] && vParts[1] >= rParts[1];
      case '~': // 近似版本
        return vParts[0] === rParts[0] && 
               vParts[1] === rParts[1] && 
               vParts[2] >= rParts[2];
      case '>=':
        return this.compareVersions(version, versionRange) >= 0;
      case '>':
        return this.compareVersions(version, versionRange) > 0;
      case '<=':
        return this.compareVersions(version, versionRange) <= 0;
      case '<':
        return this.compareVersions(version, versionRange) < 0;
      default:
        return version === versionRange;
    }
  }
  
  // 弃用通知
  deprecate(moduleName, version, message) {
    if (!this.deprecations.has(moduleName)) {
      this.deprecations.set(moduleName, new Map());
    }
    
    this.deprecations.get(moduleName).set(version, {
      message,
      deprecatedAt: new Date()
    });
    
    // 添加控制台警告
    console.warn(`Module ${moduleName}@${version} is deprecated: ${message}`);
  }
}

// 使用示例
const versionManager = new ModuleVersionManager();

// 注册不同版本
versionManager.register('utils', '1.0.0', {
  oldMethod: () => 'old'
});

versionManager.register('utils', '1.1.0', {
  oldMethod: () => 'old',
  newMethod: () => 'new'
});

versionManager.register('utils', '2.0.0', {
  newMethod: () => 'new',
  betterMethod: () => 'better'
});

// 标记弃用
versionManager.deprecate('utils', '1.0.0', '请升级到1.1.0+版本');

// 获取模块
const utilsV1 = versionManager.get('utils', '^1.0.0');
console.log(utilsV1); // 1.1.0版本

const utilsLatest = versionManager.get('utils');
console.log(utilsLatest); // 2.0.0版本

总结

模块化是现代前端工程化的基石,从前端的脚本标签到ES Modules,再到模块联邦等高级模式,模块化技术不断演进。本文从简单模块加载器实现开始,逐步深入AMD、CMD规范,探讨ES Module的Polyfill技术,并补充了构建工具集成、模块联邦、性能优化等工程化实践。

关键要点总结:

  1. 模块加载器核心原理: 依赖管理、缓存、异步加载
  2. 规范演进: 从AMD/CMD到ES Modules的统一
  3. 工程化实践: 代码分割、懒加载、版本管理、依赖注入
  4. 未来趋势: 模块联邦、微前端架构、Web Assembly模块化

模块化不仅仅是技术选择,更是一种设计哲学。良好的模块化设计能够提升代码的可维护性、可测试性和团队协作效率。在实际项目中,应根据团队规模、项目复杂度和技术栈选择合适的模块化方案,并不断优化模块边界和依赖关系。

随着前端技术的不断发展,模块化将继续演进,但核心原则——关注点分离、接口抽象、依赖管理——将始终保持不变。掌握模块化的核心原理和实践,能够帮助开发者构建更健壮、可维护的前端应用。

❌
❌