普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月10日首页

ESTree 规范 (acorn@8.15.0示例)

作者 米丘
2026年4月10日 16:09

ESTree 是一套用于描述 ECMAScript(JavaScript)代码抽象语法树(AST)的标准化规范。ESTree 规范并非一成不变,而是跟随 ECMAScript 官方版本迭代,分为多个阶段的规范:

  • ES5 规范:最早的 ESTree 规范,仅支持 ES5 语法(如 var、普通函数、if/for 等)。
  • ES6+ 规范:新增 ES6 及后续版本的语法节点(如 ArrowFunctionExpression 箭头函数、ClassDeclaration 类、ImportDeclaration 模块导入等)。
  • ESNext 规范:支持尚未正式纳入 ECMAScript 标准的实验性语法(如装饰器、管道运算符等),供工具提前适配。

语法节点类型

根节点唯一 (Program)

{
    "type": "Program", // 节点类型,`Program` 表示整个程序。
    "start": 0, // 在源码中的开始索引
    "end": 9, // 在源码中的结束索引,这里原代码长度为 9,即共 9 个字符
    "body": [ ... ], // 程序体,是一个语句数组
    "sourceType": "script" // "script" 表示源码是普通脚本(非模块),如果是 `"module"`,则支持 `import`/`export`
}

声明节点

  • VariableDeclaration 变量声明(统一包裹const/let/var)
  • FunctionDeclaration 函数声明(具名函数,提升)
  • ClassDeclaration 类声明(具名类,提升)
  • ImportDeclaration 模块导入声明(仅模块环境)
  • ExportDeclaration 模块导出声明(仅模块环境,含命名 / 默认)
  • ExportNamedDeclaration命名导出
  • ExportDefaultDeclaration默认导出
  • ExportAllDeclaration全部导出

语句节点

  • BlockStatement 块语句({}包裹的代码块)
  • ExpressionStatement 表达式语句(包裹单个表达式作为语句执行)
  • IfStatement 条件判断语句
  • ForStatement for 循环语句
  • WhileStatement while 循环语句
  • ReturnStatement 返回语句(函数内)
  • TryStatement 异常捕获语句
  • BreakStatement 中断循环语句
  • ContinueStatement 继续循环语句

表达式节点

  • Identifier标识符(变量名、函数名、属性名等
  • Literal字面量(直接写死的值)
  • BinaryExpression 二元表达式(双操作数运算)
  • UnaryExpression 一元表达式(单操作数运算)
  • AssignmentExpression 赋值表达式
  • CallExpression 函数调用表达式
  • MemberExpression 成员访问表达式
  • ArrowFunctionExpression 箭头函数表达式
  • ObjectExpression 对象字面量表达式
  • ArrayExpression 数组字面量表达式

其他节点

  • TryStatementtry...catch 语句
  • TemplateLiteral模板字符串
  • TaggedTemplateExpression带标签的模板字符串
  • SpreadElement扩展运算符
  • RestElement剩余参数

Acorn

Acorn 是一个轻量、快速的 JavaScript 解析器,能将代码转换为 ESTree 标准的抽象语法树(AST)。

它主要提供三大核心 API

  • parse(input, options) :解析一段完整的 JavaScript 程序。成功返回 ESTree AST,失败抛出包含位置信息的 SyntaxError 对象
  • parseExpressionAt(input, pos, options) :解析一个独立的 JavaScript 表达式。适用于解析模板字符串内的内嵌表达式等混合内容
  • tokenizer(input, options) :返回一个迭代器,逐个生成代码的 Token。可用于自定义的语法高亮或极简解析器。

parseExpressionAt

  const code = 'const x = 10; const y = 20; x + y * 2;'
  const result = acorn.parseExpressionAt(code, code.indexOf('x + y'),{
    ecmaVersion: 2020,
    sourceType: 'module',
  });
  console.log(result);

image.png

tokenizer

示例

  const result = acorn.tokenizer('let a = "hello";',{
    ecmaVersion: 2020,
    sourceType: 'module',
  });
  console.log(result);

image.png

关键字(可用于代码高亮)

^(?:break|case|catch|continue|debugger|default|do|else|finally|for|function|if|return|switch|throw|try|var|while|with|null|true|false|instanceof|typeof|void|delete|new|in|this|const|class|extends|export|import|super)$

示例

  const result = acorn.tokenizer('let a = "hello";',{
    ecmaVersion: 2020,
    sourceType: 'module',
  });
  console.log(result);

  for(let token of result){
    console.log('token',token);
  }

image.png

image.png

每个 Token 对象都会包含一个 type 属性,指向这样的类型描述对象。

{
    "label": "string", // Token 类型的人类可读名称
    "beforeExpr": false, // 该 Token 类型是否可以在表达式之前出现
    "startsExpr": true, // 该 Token 类型是否作为表达式的开始
    "isLoop": false, // 是否为循环关键字(如 for, while, do)
    "isAssign": false, // 是否为赋值操作符(如 =, +=, -=)
    "prefix": false, // 是否为前缀操作符(如 ++, --, !, ~)
    "postfix": false,  // 是否为后缀操作符(如 ++, --)
    "binop": null,// 如果是二元操作符,这里会有一个优先级数值;否则为 null
    "updateContext": null // 可选函数,用于在解析时更新上下文(通常为 null)
}

声明变量

例1 声明一个变量(基本类型)

const ast = acorn.parse(`let a = 1`, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
  "type": "Program",
  "start": 0,
  "end": 9,
  "body": [
    {
      "type": "VariableDeclaration", // 变量声明符
      "start": 0,
      "end": 9,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          // 标识符节点,即变量名。
          "id": {
            "type": "Identifier", // 变量名标识符
            "start": 4,
            "end": 5,
            "name": "a" // 变量名
          },
          // 初始化表达式节点,即等号右边的值
          "init": {
            "type": "Literal", // 字面量
            "start": 8,
            "end": 9,
            "value": 1, // 运行时的值,这里是数字 1
            "raw": "1" // 源码中的原始字符串表示 "1"
          }
        }
      ],
      "kind": "let"  // 表示使用 let 关键字声明
    }
  ],
  "sourceType": "script"
}

例2 声明一个变量(数组)

const ast = acorn.parse(`const arr = [1,2]`, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 17,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 17,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 17,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 9,
            "name": "arr"
          },
          "init": {
            "type": "ArrayExpression",
            "start": 12,
            "end": 17,
            "elements": [
              {
                "type": "Literal",
                "start": 13,
                "end": 14,
                "value": 1,
                "raw": "1"
              },
              {
                "type": "Literal",
                "start": 15,
                "end": 16,
                "value": 2,
                "raw": "2"
              }
            ]
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}

例3 声明一个变量(对象)

const ast = acorn.parse(`const arr = {a: 1, b: 2}`, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 24,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 24,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 24,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 9,
            "name": "arr"
          },
          "init": {
            "type": "ObjectExpression",
            "start": 12,
            "end": 24,
            "properties": [
              {
                "type": "Property",
                "start": 13,
                "end": 17,
                "method": false,
                "shorthand": false,
                "computed": false,
                "key": {
                  "type": "Identifier",
                  "start": 13,
                  "end": 14,
                  "name": "a"
                },
                "value": {
                  "type": "Literal",
                  "start": 16,
                  "end": 17,
                  "value": 1,
                  "raw": "1"
                },
                "kind": "init"
              },
              {
                "type": "Property",
                "start": 19,
                "end": 23,
                "method": false,
                "shorthand": false,
                "computed": false,
                "key": {
                  "type": "Identifier",
                  "start": 19,
                  "end": 20,
                  "name": "b"
                },
                "value": {
                  "type": "Literal",
                  "start": 22,
                  "end": 23,
                  "value": 2,
                  "raw": "2"
                },
                "kind": "init"
              }
            ]
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}

例4 三元表达式

const ast = acorn.parse(`const flag = a > b ? true : false`, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 33,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 33,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 33,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 10,
            "name": "flag"
          },
          "init": {
            "type": "ConditionalExpression",
            "start": 13,
            "end": 33,
            "test": {
              "type": "BinaryExpression",
              "start": 13,
              "end": 18,
              "left": {
                "type": "Identifier",
                "start": 13,
                "end": 14,
                "name": "a"
              },
              "operator": ">",
              "right": {
                "type": "Identifier",
                "start": 17,
                "end": 18,
                "name": "b"
              }
            },
            "consequent": {
              "type": "Literal",
              "start": 21,
              "end": 25,
              "value": true,
              "raw": "true"
            },
            "alternate": {
              "type": "Literal",
              "start": 28,
              "end": 33,
              "value": false,
              "raw": "false"
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}

例5 声明变量(逻辑运算符)

  const code = 'let name = jon || "hello";'
  const result = acorn.parse(code, {
    ecmaVersion: 2020,
  });
  console.log(JSON.stringify(result, null, 2));
{
    "type": "Program",
    "start": 0,
    "end": 26,
    "body": [
        {
            "type": "VariableDeclaration",
            "start": 0,
            "end": 26,
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "start": 4,
                    "end": 25,
                    // 声明标识
                    "id": {
                        "type": "Identifier",
                        "start": 4,
                        "end": 8,
                        "name": "name"
                    },
                    // 声明初始化内容
                    "init": {
                        "type": "LogicalExpression",// 逻辑表达式
                        "start": 11,
                        "end": 25,
                        "left": {
                            "type": "Identifier",
                            "start": 11,
                            "end": 14,
                            "name": "jon"
                        },
                        "operator": "||",// 操作符
                        "right": {
                            "type": "Literal",
                            "start": 18,
                            "end": 25,
                            "value": "hello",
                            "raw": "\"hello\""
                        }
                    }
                }
            ],
            "kind": "let"
        }
    ],
    "sourceType": "script"
}

函数

例1 箭头函数

const ast = acorn.parse(`const getFlag = (a, b) => a + b`, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 31,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 31,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 31,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 13,
            "name": "getFlag"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 16,
            "end": 31,
            "id": null,
            "expression": true,
            "generator": false,
            "async": false,
            "params": [
              {
                "type": "Identifier",
                "start": 17,
                "end": 18,
                "name": "a"
              },
              {
                "type": "Identifier",
                "start": 20,
                "end": 21,
                "name": "b"
              }
            ],
            "body": {
              "type": "BinaryExpression",
              "start": 26,
              "end": 31,
              "left": {
                "type": "Identifier",
                "start": 26,
                "end": 27,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 30,
                "end": 31,
                "name": "b"
              }
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}

例2 普通函数 含有返回值

const ast = acorn.parse(`function getFlag(a, b) { return a + b }  `, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 41,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 39,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 16,
        "name": "getFlag"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 17,
          "end": 18,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 20,
          "end": 21,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 23,
        "end": 39,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 25,
            "end": 37,
            "argument": {
              "type": "BinaryExpression",
              "start": 32,
              "end": 37,
              "left": {
                "type": "Identifier",
                "start": 32,
                "end": 33,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 36,
                "end": 37,
                "name": "b"
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "script"
}

例3 函数调用

const ast = acorn.parse(`function getFlag(a, b) { return a + b } getFlag(1, 2)`, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 53,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 39,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 16,
        "name": "getFlag"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 17,
          "end": 18,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 20,
          "end": 21,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 23,
        "end": 39,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 25,
            "end": 37,
            "argument": {
              "type": "BinaryExpression",
              "start": 32,
              "end": 37,
              "left": {
                "type": "Identifier",
                "start": 32,
                "end": 33,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 36,
                "end": 37,
                "name": "b"
              }
            }
          }
        ]
      }
    },
    {
      "type": "ExpressionStatement",
      "start": 40,
      "end": 53,
      "expression": {
        "type": "CallExpression",
        "start": 40,
        "end": 53,
        "callee": {
          "type": "Identifier",
          "start": 40,
          "end": 47,
          "name": "getFlag"
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 48,
            "end": 49,
            "value": 1,
            "raw": "1"
          },
          {
            "type": "Literal",
            "start": 51,
            "end": 52,
            "value": 2,
            "raw": "2"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "script"
}

例4 条件语句

const ast = acorn.parse(`function getFlag(a, b) { if(a > b) { return true } } getFlag(1, 2)`, {
  ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 66,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 52,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 16,
        "name": "getFlag"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 17,
          "end": 18,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 20,
          "end": 21,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 23,
        "end": 52,
        "body": [
          {
            "type": "IfStatement",
            "start": 25,
            "end": 50,
            "test": {
              "type": "BinaryExpression",
              "start": 28,
              "end": 33,
              "left": {
                "type": "Identifier",
                "start": 28,
                "end": 29,
                "name": "a"
              },
              "operator": ">",
              "right": {
                "type": "Identifier",
                "start": 32,
                "end": 33,
                "name": "b"
              }
            },
            "consequent": {
              "type": "BlockStatement",
              "start": 35,
              "end": 50,
              "body": [
                {
                  "type": "ReturnStatement",
                  "start": 37,
                  "end": 48,
                  "argument": {
                    "type": "Literal",
                    "start": 44,
                    "end": 48,
                    "value": true,
                    "raw": "true"
                  }
                }
              ]
            },
            "alternate": null
          }
        ]
      }
    },
    {
      "type": "ExpressionStatement",
      "start": 53,
      "end": 66,
      "expression": {
        "type": "CallExpression",
        "start": 53,
        "end": 66,
        "callee": {
          "type": "Identifier",
          "start": 53,
          "end": 60,
          "name": "getFlag"
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 61,
            "end": 62,
            "value": 1,
            "raw": "1"
          },
          {
            "type": "Literal",
            "start": 64,
            "end": 65,
            "value": 2,
            "raw": "2"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "script"
}

声明一个空类

{
    "type": "Program",
    "start": 0,
    "end": 11,
    "body": [
        {
            "type": "ClassDeclaration", // 类声明
            "start": 0,
            "end": 11,
            // 类名,是一个 Identifier 节点
            "id": {
                "type": "Identifier",
                "start": 6,
                "end": 9,
                "name": "Cat"
            },
            // 父类 ,如果有 extends 关键字,这里会是表达式节点
            "superClass": null,
            // 包含类的所有成员(方法、属性等)
            "body": {
                "type": "ClassBody",
                "start": 9,
                "end": 11,
                "body": []
            }
        }
    ],
    "sourceType": "module"
}

带构造函数的类

{
    "type": "Program",
    "start": 0,
    "end": 50,
    "body": [
        {
            "type": "ClassDeclaration",
            "start": 0,
            "end": 50,
            "id": {
                "type": "Identifier",
                "start": 6,
                "end": 9,
                "name": "Cat"
            },
            "superClass": null,
            "body": {
                "type": "ClassBody",
                "start": 9,
                "end": 50,
                "body": [
                    {
                        "type": "MethodDefinition",
                        "start": 11,
                        "end": 49,
                        "static": false,
                        "computed": false,
                        "key": {
                            "type": "Identifier",
                            "start": 11,
                            "end": 22,
                            "name": "constructor"
                        },
                        "kind": "constructor",
                        "value": {
                            "type": "FunctionExpression",
                            "start": 22,
                            "end": 49,
                            "id": null,
                            "expression": false,
                            "generator": false,
                            "async": false,
                            "params": [
                                {
                                    "type": "Identifier",
                                    "start": 23,
                                    "end": 27,
                                    "name": "name"
                                }
                            ],
                            "body": {
                                "type": "BlockStatement",
                                "start": 28,
                                "end": 49,
                                "body": [
                                    {
                                        "type": "ExpressionStatement",
                                        "start": 30,
                                        "end": 47,
                                        "expression": {
                                            "type": "AssignmentExpression",
                                            "start": 30,
                                            "end": 46,
                                            "operator": "=",
                                            "left": {
                                                "type": "MemberExpression",
                                                "start": 30,
                                                "end": 39,
                                                "object": {
                                                    "type": "ThisExpression",
                                                    "start": 30,
                                                    "end": 34
                                                },
                                                "property": {
                                                    "type": "Identifier",
                                                    "start": 35,
                                                    "end": 39,
                                                    "name": "name"
                                                },
                                                "computed": false,
                                                "optional": false
                                            },
                                            "right": {
                                                "type": "Identifier",
                                                "start": 42,
                                                "end": 46,
                                                "name": "name"
                                            }
                                        }
                                    }
                                ]
                            }
                        }
                    }
                ]
            }
        }
    ],
    "sourceType": "module"
}

截取片段this.name = name

{
 "body": [
    {
        "type": "ExpressionStatement", // 表达式语句
        "start": 30,
        "end": 47,
        // 真正的表达式
        "expression": {
            "type": "AssignmentExpression", // 赋值表达式
            "start": 30,
            "end": 46,
            "operator": "=",
            "left": {
                "type": "MemberExpression", // 属性访问表达式
                "start": 30,
                "end": 39,
                // 被访问的对象
                "object": {
                    "type": "ThisExpression", // this
                    "start": 30,
                    "end": 34
                },
                // 属性
                "property": {
                    "type": "Identifier",
                    "start": 35,
                    "end": 39,
                    "name": "name"
                },
                // 表示使用点号 . 访问属性(而非 [计算属性名])
                "computed": false,
                // 可选链操作符 ?.
                "optional": false
            },
            "right": {
                "type": "Identifier",
                "start": 42,
                "end": 46,
                "name": "name"
            }
        }
    }
]
}

继承

  const code = 'class Cat extends Animal { constructor(name){ super(name); }}'
  const result = acorn.parse(code, {
    ecmaVersion: 2020,
    sourceType: 'module',
  });
  console.log(JSON.stringify(result, null, 2));
{
    "type": "Program",
    "start": 0,
    "end": 61,
    "body": [
        {
            "type": "ClassDeclaration",
            "start": 0,
            "end": 61,
            "id": {
                "type": "Identifier",
                "start": 6,
                "end": 9,
                "name": "Cat"
            },
            "superClass": {
                "type": "Identifier",
                "start": 18,
                "end": 24,
                "name": "Animal"
            },
            "body": {
                "type": "ClassBody",
                "start": 25,
                "end": 61,
                "body": [
                    {
                        "type": "MethodDefinition",
                        "start": 27,
                        "end": 60,
                        "static": false,
                        "computed": false,
                        "key": {
                            "type": "Identifier",
                            "start": 27,
                            "end": 38,
                            "name": "constructor"
                        },
                        "kind": "constructor",
                        "value": {
                            "type": "FunctionExpression",
                            "start": 38,
                            "end": 60,
                            "id": null,
                            "expression": false,
                            "generator": false,
                            "async": false,
                            "params": [
                                {
                                    "type": "Identifier",
                                    "start": 39,
                                    "end": 43,
                                    "name": "name"
                                }
                            ],
                            "body": {
                                "type": "BlockStatement",
                                "start": 44,
                                "end": 60,
                                "body": [
                                    {
                                        "type": "ExpressionStatement",
                                        "start": 46,
                                        "end": 58,
                                        "expression": {
                                            "type": "CallExpression",
                                            "start": 46,
                                            "end": 57,
                                            "callee": {
                                                "type": "Super",
                                                "start": 46,
                                                "end": 51
                                            },
                                            "arguments": [
                                                {
                                                    "type": "Identifier",
                                                    "start": 52,
                                                    "end": 56,
                                                    "name": "name"
                                                }
                                            ],
                                            "optional": false
                                        }
                                    }
                                ]
                            }
                        }
                    }
                ]
            }
        }
    ],
    "sourceType": "module"
}

截取片段分析 super(name)

{
    "type": "ExpressionStatement",
    "start": 46,
    "end": 58,
    "expression": {
        "type": "CallExpression",//调用表达式
        "start": 46,
        "end": 57,
        // 被调用的函数或方法
        "callee": {
            "type": "Super", // super关键字
            "start": 46,
            "end": 51
        },
        // 参数列表
        "arguments": [
            {
                "type": "Identifier",
                "start": 52,
                "end": 56,
                "name": "name"
            }
        ],
        "optional": false
    }
}

模块

命名导入

const ast = acorn.parse(`import { add } from './utills.js'`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
    "type": "Program",
    "start": 0,
    "end": 33,
    "body": [
        {
            "type": "ImportDeclaration", // 导入声明
            "start": 0,
            "end": 33,
            
            "specifiers": [
                {
                    "type": "ImportSpecifier", // 导入语句
                    "start": 9,
                    "end": 12,
                    // 模块导入的名称
                    "imported": {
                        "type": "Identifier", 
                        "start": 9,
                        "end": 12,
                        "name": "add"
                    },
                    // 本地使用的名称
                    "local": {
                        "type": "Identifier",
                        "start": 9,
                        "end": 12,
                        "name": "add"
                    }
                }
            ],
            // 源
            "source": {
                "type": "Literal",
                "start": 20,
                "end": 33,
                "value": "./utills.js", // 运行中
                "raw": "'./utills.js'" // 代码中保留了引号
            }
        }
    ],
    "sourceType": "module"
}

命名导入

const ast = acorn.parse(`import { add } from './utills.js';const result = add(1, 2);`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
    "type": "Program",
    "start": 0,
    "end": 59,
    "body": [
        {
            "type": "ImportDeclaration",
            "start": 0,
            "end": 34,
            "specifiers": [
                {
                    "type": "ImportSpecifier",
                    "start": 9,
                    "end": 12,
                    "imported": {
                        "type": "Identifier",
                        "start": 9,
                        "end": 12,
                        "name": "add"
                    },
                    "local": {
                        "type": "Identifier",
                        "start": 9,
                        "end": 12,
                        "name": "add"
                    }
                }
            ],
            "source": {
                "type": "Literal",
                "start": 20,
                "end": 33,
                "value": "./utills.js",
                "raw": "'./utills.js'"
            }
        },
        {
            "type": "VariableDeclaration",
            "start": 34,
            "end": 59,
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "start": 40,
                    "end": 58,
                    "id": {
                        "type": "Identifier",
                        "start": 40,
                        "end": 46,
                        "name": "result"
                    },
                    "init": {
                        "type": "CallExpression",
                        "start": 49,
                        "end": 58,
                        "callee": {
                            "type": "Identifier",
                            "start": 49,
                            "end": 52,
                            "name": "add"
                        },
                        "arguments": [
                            {
                                "type": "Literal",
                                "start": 53,
                                "end": 54,
                                "value": 1,
                                "raw": "1"
                            },
                            {
                                "type": "Literal",
                                "start": 56,
                                "end": 57,
                                "value": 2,
                                "raw": "2"
                            }
                        ],
                        "optional": false
                    }
                }
            ],
            "kind": "const"
        }
    ],
    "sourceType": "module"
}

别名导入

const ast = acorn.parse(`import { add as addFun} from './utills.js'`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
    "type": "Program",
    "start": 0,
    "end": 42,
    "body": [
        {
            "type": "ImportDeclaration",
            "start": 0,
            "end": 42,
            "specifiers": [
                {
                    "type": "ImportSpecifier",
                    "start": 9,
                    "end": 22,
                    "imported": {
                        "type": "Identifier",
                        "start": 9,
                        "end": 12,
                        "name": "add"
                    },
                    "local": {
                        "type": "Identifier",
                        "start": 16,
                        "end": 22,
                        "name": "addFun"
                    }
                }
            ],
            "source": {
                "type": "Literal",
                "start": 29,
                "end": 42,
                "value": "./utills.js",
                "raw": "'./utills.js'"
            }
        }
    ],
    "sourceType": "module"
}

命名导出一个 变量声明

const ast = acorn.parse(`export const Max_Size = 100;`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
    "type": "Program",
    "start": 0,
    "end": 28,
    "body": [
        {
            "type": "ExportNamedDeclaration", // 表示一个命名导出
            "start": 0,
            "end": 28,
            // 被导出的声明节点
            "declaration": {
                "type": "VariableDeclaration",
                "start": 7,
                "end": 28,
                "declarations": [
                    {
                        "type": "VariableDeclarator",
                        "start": 13,
                        "end": 27,
                        "id": {
                            "type": "Identifier",
                            "start": 13,
                            "end": 21,
                            "name": "Max_Size"
                        },
                        "init": {
                            "type": "Literal",
                            "start": 24,
                            "end": 27,
                            "value": 100,
                            "raw": "100"
                        }
                    }
                ],
                "kind": "const"
            },
            "specifiers": [],
            "source": null // 从其他模块重导出
        }
    ],
    "sourceType": "module"
}

命名导出一个 函数声明

const ast = acorn.parse(`export function add(a, b) {return a + b;}`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
  "type": "Program",
  "start": 0,
  "end": 41,
  "body": [
    {
      "type": "ExportNamedDeclaration",
      "start": 0,
      "end": 41,
      "declaration": {
        "type": "FunctionDeclaration",
        "start": 7,
        "end": 41,
        "id": {
          "type": "Identifier",
          "start": 16,
          "end": 19,
          "name": "add"
        },
        "expression": false,
        "generator": false,
        "async": false,
        "params": [
          {
            "type": "Identifier",
            "start": 20,
            "end": 21,
            "name": "a"
          },
          {
            "type": "Identifier",
            "start": 23,
            "end": 24,
            "name": "b"
          }
        ],
        "body": {
          "type": "BlockStatement",
          "start": 26,
          "end": 41,
          "body": [
            {
              "type": "ReturnStatement",
              "start": 27,
              "end": 40,
              "argument": {
                "type": "BinaryExpression",
                "start": 34,
                "end": 39,
                "left": {
                  "type": "Identifier",
                  "start": 34,
                  "end": 35,
                  "name": "a"
                },
                "operator": "+",
                "right": {
                  "type": "Identifier",
                  "start": 38,
                  "end": 39,
                  "name": "b"
                }
              }
            }
          ]
        }
      },
      "specifiers": [],
      "source": null
    }
  ],
  "sourceType": "module"
}

命名导出一个变量

const ast = acorn.parse(`const Max_Size = 100;export { Max_Size };`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));

export { Max_Size };这是一个命名导出语句,但它不包含声明declaration: null),而是通过 specifiers 列表来指定要导出的已有变量

{
  "type": "Program",
  "start": 0,
  "end": 41,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 21,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 20,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 14,
            "name": "Max_Size"
          },
          "init": {
            "type": "Literal",
            "start": 17,
            "end": 20,
            "value": 100,
            "raw": "100"
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExportNamedDeclaration",
      "start": 21,
      "end": 41,
      "declaration": null, // 没有内联声明
      // 导出说明符列表
      "specifiers": [
        {
          "type": "ExportSpecifier",
          "start": 30,
          "end": 38,
          // 当前模块本地名称
          "local": {
            "type": "Identifier",
            "start": 30,
            "end": 38,
            "name": "Max_Size"
          },
          // 导出后名称
          "exported": {
            "type": "Identifier",
            "start": 30,
            "end": 38,
            "name": "Max_Size"
          }
        }
      ],
      "source": null // 不是从其他模块中导出
    }
  ],
  "sourceType": "module"
}

命名导出一个函数

const ast = acorn.parse(`function add(a, b) {return a + b;} export { add };`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
  "type": "Program",
  "start": 0,
  "end": 50,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 34,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 12,
        "name": "add"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 13,
          "end": 14,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 16,
          "end": 17,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 19,
        "end": 34,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 20,
            "end": 33,
            "argument": {
              "type": "BinaryExpression",
              "start": 27,
              "end": 32,
              "left": {
                "type": "Identifier",
                "start": 27,
                "end": 28,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 31,
                "end": 32,
                "name": "b"
              }
            }
          }
        ]
      }
    },
    {
      "type": "ExportNamedDeclaration",
      "start": 35,
      "end": 50,
      "declaration": null,
      "specifiers": [
        {
          "type": "ExportSpecifier",
          "start": 44,
          "end": 47,
          "local": {
            "type": "Identifier",
            "start": 44,
            "end": 47,
            "name": "add"
          },
          "exported": {
            "type": "Identifier",
            "start": 44,
            "end": 47,
            "name": "add"
          }
        }
      ],
      "source": null
    }
  ],
  "sourceType": "module"
}

默认导出字面量

const ast = acorn.parse(`export default 12;`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
  "type": "Program",
  "start": 0,
  "end": 18,
  "body": [
    {
      "type": "ExportDefaultDeclaration",
      "start": 0,
      "end": 18,
      "declaration": {
        "type": "Literal",
        "start": 15,
        "end": 17,
        "value": 12,
        "raw": "12"
      }
    }
  ],
  "sourceType": "module"
}

默认导出 变量(基本类型)

const ast = acorn.parse(`var Max_Size = 100;export default  Max_Size ;`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
  "type": "Program",
  "start": 0,
  "end": 45,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 19,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 18,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 12,
            "name": "Max_Size"
          },
          "init": {
            "type": "Literal",
            "start": 15,
            "end": 18,
            "value": 100,
            "raw": "100"
          }
        }
      ],
      "kind": "var"
    },
    {
      "type": "ExportDefaultDeclaration",
      "start": 19,
      "end": 45,
      "declaration": {
        "type": "Identifier",
        "start": 35,
        "end": 43,
        "name": "Max_Size"
      }
    }
  ],
  "sourceType": "module"
}

默认导出变量 (函数)

const ast = acorn.parse(`function a(){} export default a;`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));

{
  "type": "Program",
  "start": 0,
  "end": 32,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 14,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 10,
        "name": "a"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 12,
        "end": 14,
        "body": []
      }
    },
    {
      "type": "ExportDefaultDeclaration",
      "start": 15,
      "end": 32,
      // 被导出的声明或表达式
      "declaration": {
        "type": "Identifier",
        "start": 30,
        "end": 31,
        "name": "a"
      }
    }
  ],
  "sourceType": "module"
}

默认导出 对象表达式

const ast = acorn.parse(`function add(a, b) {return a + b;} export default { add };`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
  "type": "Program",
  "start": 0,
  "end": 58,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 34,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 12,
        "name": "add"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 13,
          "end": 14,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 16,
          "end": 17,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 19,
        "end": 34,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 20,
            "end": 33,
            "argument": {
              "type": "BinaryExpression",
              "start": 27,
              "end": 32,
              "left": {
                "type": "Identifier",
                "start": 27,
                "end": 28,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 31,
                "end": 32,
                "name": "b"
              }
            }
          }
        ]
      }
    },
    {
      "type": "ExportDefaultDeclaration", // 默认导出声明
      "start": 35,
      "end": 58,
      "declaration": {
        "type": "ObjectExpression",
        "start": 50,
        "end": 57,
        // 对象属性
        "properties": [
          {
            "type": "Property", // 对象属性节点
            "start": 52,
            "end": 55,
            "method": false,
            "shorthand": true,
            "computed": false,
            "key": {
              "type": "Identifier",
              "start": 52,
              "end": 55,
              "name": "add"
            },
            "value": {
              "type": "Identifier",
              "start": 52,
              "end": 55,
              "name": "add"
            },
            // 表示普通数据属性,非getter、setter
            "kind": "init"
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

默认导出 函数声明

const ast = acorn.parse(`export default function fn() {}`, {
  ecmaVersion: 2020,
  sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
  "type": "Program",
  "start": 0,
  "end": 31,
  "body": [
    {
      "type": "ExportDefaultDeclaration",
      "start": 0,
      "end": 31,
      "declaration": {
        "type": "FunctionDeclaration",
        "start": 15,
        "end": 31,
        "id": {
          "type": "Identifier",
          "start": 24,
          "end": 26,
          "name": "fn"
        },
        "expression": false,
        "generator": false,
        "async": false,
        "params": [],
        "body": {
          "type": "BlockStatement",
          "start": 29,
          "end": 31,
          "body": []
        }
      }
    }
  ],
  "sourceType": "module"
}

最后

  1. ESTree 规范
  2. 在线查看代码片段的AST语法结构
昨天 — 2026年4月9日首页

Vite8 关于 vite build 命令构建过程

作者 米丘
2026年4月9日 15:54

在 Vite 8 中,vite build 命令已经从传统的 Rollup 打包,彻底转向了由 Rust 驱动的全新工具链。

Vite 8 最大的改变,是其构建流程的底层核心被完全重写,统一使用 Rust 生态的工具。

  • 单一打包器 Rolldown:此前,Vite 在开发环境使用 esbuild 追求速度,在生产环境使用 Rollup 追求能力,这导致了行为不一致。Vite 8 使用一个名为 Rolldown 的 Rust 打包器,统一了开发和生产环境的构建链路。它完全兼容 Rollup 的插件 API,使得绝大多数现有 Vite 插件无需修改即可在 Vite 8 中运行。
  • 高性能引擎 Oxc:Rolldown 本身构建于 Oxc(另一个用 Rust 编写的工具集)之上。Oxc 为 Rolldown 提供了极快的解析、转换能力,使其在处理 TypeScript 和 JSX 文件时性能大幅领先。

vite build 有哪些命令行参数?

// build
cli
  .command('build [root]', 'build for production')
  .option(
    '--target <target>',
    `[string] transpile target (default: 'baseline-widely-available')`,
  )
  .option('--outDir <dir>', `[string] output directory (default: dist)`)
  .option(
    '--assetsDir <dir>',
    `[string] directory under outDir to place assets in (default: assets)`,
  )
  .option(
    '--assetsInlineLimit <number>',
    `[number] static asset base64 inline threshold in bytes (default: 4096)`,
  )
  .option(
    '--ssr [entry]',
    `[string] build specified entry for server-side rendering`,
  )
  .option(
    '--sourcemap [output]',
    `[boolean | "inline" | "hidden"] output source maps for build (default: false)`,
  )
  .option(
    '--minify [minifier]',
    `[boolean | "terser" | "esbuild"] enable/disable minification, ` +
      `or specify minifier to use (default: esbuild)`,
  )
  .option('--manifest [name]', `[boolean | string] emit build manifest json`)
  .option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`)
  .option(
    '--emptyOutDir',
    `[boolean] force empty outDir when it's outside of root`,
  )
  .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
  .option('--app', `[boolean] same as \`builder: {}\``)

vite build 接收的 options 有哪些?

image.png

源码

createBuilder

/**
 * Creates a ViteBuilder to orchestrate building multiple environments.
 * 创建和配置 vite构建器
 * @experimental
 * params inlineConfig 行内配置
 * params useLegacyBuilder 是否使用旧版构建器
 */
export async function createBuilder(
  inlineConfig: InlineConfig = {},
  useLegacyBuilder: null | boolean = false,
): Promise<ViteBuilder> {

  // 处理旧版兼容
  const patchConfig = (resolved: ResolvedConfig) => {
    if (!(useLegacyBuilder ?? !resolved.builder)) return

    // Until the ecosystem updates to use `environment.config.build` instead of `config.build`,
    // we need to make override `config.build` for the current environment.
    // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later
    // remove the default values that shouldn't be used at all once the config is resolved
    const environmentName = resolved.build.ssr ? 'ssr' : 'client'
    ;(resolved.build as ResolvedBuildOptions) = {
      ...resolved.environments[environmentName].build,
    }
  }
  // 配置解析
  const config = await resolveConfigToBuild(inlineConfig, patchConfig)
  // 是否使用旧版构建器
  useLegacyBuilder ??= !config.builder
  // 构建器配置
  const configBuilder = config.builder ?? resolveBuilderOptions({})!

  const environments: Record<string, BuildEnvironment> = {}

  // 创建 ViteBuilder 对象
  const builder: ViteBuilder = {
    environments,
    config,
    /**
     * 构建整个应用
     */
    async buildApp() {
      // 创建插件上下文
      const pluginContext = new BasicMinimalPluginContext(
        { ...basePluginContextMeta, watchMode: false },
        config.logger,
      )

      // order 'pre' and 'normal' hooks are run first, then config.builder.buildApp, then 'post' hooks
      // 是否已调用配置构建器的 buildApp 方法
      let configBuilderBuildAppCalled = false

      // 执行插件的 buildApp 钩子
      for (const p of config.getSortedPlugins('buildApp')) {
        const hook = p.buildApp
        if (
          !configBuilderBuildAppCalled &&
          typeof hook === 'object' &&
          hook.order === 'post' // 只在 post 阶段调用
        ) {
          configBuilderBuildAppCalled = true
          await configBuilder.buildApp(builder)
        }
        const handler = getHookHandler(hook)
        await handler.call(pluginContext, builder)
      }
      // 如果未调用配置构建器的 buildApp 方法,调用默认 buildApp 方法
      if (!configBuilderBuildAppCalled) {
        await configBuilder.buildApp(builder)
      }
      // fallback to building all environments if no environments have been built
      // 检查是否有环境被构建
      if (
        Object.values(builder.environments).every(
          (environment) => !environment.isBuilt,
        )
      ) {
        for (const environment of Object.values(builder.environments)) {
          // 构建所有环境
          await builder.build(environment)
        }
      }
    },
    /**
     * 构建环境
     * @param environment 
     * @returns 
     */
    async build(
      environment: BuildEnvironment,
    ): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
      const output = await buildEnvironment(environment)
      environment.isBuilt = true
      return output
    },
    async runDevTools() {
      const devtoolsConfig = config.devtools
      if (devtoolsConfig.enabled) {
        try {
          const { start } = await import(`@vitejs/devtools/cli-commands`)
          await start(devtoolsConfig.config)
        } catch (e) {
          config.logger.error(
            colors.red(`Failed to run Vite DevTools: ${e.message || e.stack}`),
            { error: e },
          )
        }
      }
    },
  }

  /**
   * 环境设置函数
   */
  async function setupEnvironment(name: string, config: ResolvedConfig) {
    const environment = await config.build.createEnvironment(name, config)
    await environment.init()
    environments[name] = environment
  }

  // 环境初始化
  // 使用旧版构建器
  if (useLegacyBuilder) {
    await setupEnvironment(config.build.ssr ? 'ssr' : 'client', config)
  } else {
    // 新版构建器
    const environmentConfigs: [string, ResolvedConfig][] = []

    for (const environmentName of Object.keys(config.environments)) {
      // We need to resolve the config again so we can properly merge options
      // and get a new set of plugins for each build environment. The ecosystem
      // expects plugins to be run for the same environment once they are created
      // and to process a single bundle at a time (contrary to dev mode where
      // plugins are built to handle multiple environments concurrently).
      let environmentConfig = config
      if (!configBuilder.sharedConfigBuild) {
        const patchConfig = (resolved: ResolvedConfig) => {
          // Until the ecosystem updates to use `environment.config.build` instead of `config.build`,
          // we need to make override `config.build` for the current environment.
          // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later
          // remove the default values that shouldn't be used at all once the config is resolved
          ;(resolved.build as ResolvedBuildOptions) = {
            ...resolved.environments[environmentName].build,
          }
        }
        const patchPlugins = (resolvedPlugins: Plugin[]) => {
          // Force opt-in shared plugins
          let j = 0
          for (let i = 0; i < resolvedPlugins.length; i++) {
            const environmentPlugin = resolvedPlugins[i]
            if (
              configBuilder.sharedPlugins ||
              environmentPlugin.sharedDuringBuild
            ) {
              for (let k = j; k < config.plugins.length; k++) {
                if (environmentPlugin.name === config.plugins[k].name) {
                  resolvedPlugins[i] = config.plugins[k]
                  j = k + 1
                  break
                }
              }
            }
          }
        }
        // 为每个环境名称创建环境配置
        environmentConfig = await resolveConfigToBuild(
          inlineConfig,
          patchConfig,
          patchPlugins,
        )
      }
      
      environmentConfigs.push([environmentName, environmentConfig])
    }
    // 并行初始化所有环境
    await Promise.all(
      environmentConfigs.map(
        async ([environmentName, environmentConfig]) =>
          await setupEnvironment(environmentName, environmentConfig),
      ),
    )
  }

  return builder
}

image.png

image.png

buildEnvironment

buildEnvironment 函数是 Vite 8 中为单个环境(如 client 或 ssr)执行生产构建的核心函数:

  1. 首先解析 Rolldown 打包配置。
  2. 然后根据是否开启监听模式(options.watch)分别创建 Rolldown 的 watcher 以持续构建并监听文件变化,或一次性调用 Rolldown 完成打包。
  3. 构建过程中会收集每个输出 chunk 的元数据,支持多输出配置(如同时输出 ESM 和 CJS),并最终将产物写入磁盘或返回结果对象。
  4. 同时提供详细的日志输出和错误增强处理,在结束前确保关闭 Rolldown 实例以释放资源。

/**
 * Build an App environment, or a App library (if libraryOptions is provided)
 * Vite 8 中负责生产构建单个环境(如 client、ssr)的核心函数。
 * 基于 Rolldown(Rust 打包器)执行打包,支持普通构建和监听模式(watch)
 **/
async function buildEnvironment(
  environment: BuildEnvironment,
): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
  const { logger, config } = environment
  const { root, build: options } = config

  // 记录开始构建的日志
  logger.info(
    colors.cyan(
      `vite v${VERSION} ${colors.green(
        `building ${environment.name} environment for ${environment.config.mode}...`,
      )}`,
    ),
  )

  let bundle: RolldownBuild | undefined
  let startTime: number | undefined
  try {
    // 收集每个输出 chunk 的元数据(如模块 ID、文件大小等)
    const chunkMetadataMap = new ChunkMetadataMap()
    // 解析 Rolldown 选项
    const rollupOptions = resolveRolldownOptions(environment, chunkMetadataMap)

    // watch file changes with rollup
    // 监视文件变化
    if (options.watch) {
      logger.info(colors.cyan(`\nwatching for file changes...`))

      const resolvedOutDirs = getResolvedOutDirs(
        root,
        options.outDir,
        options.rollupOptions.output,
      )
      const emptyOutDir = resolveEmptyOutDir(
        options.emptyOutDir,
        root,
        resolvedOutDirs,
        logger,
      )
      const resolvedChokidarOptions = resolveChokidarOptions(
        {
          // @ts-expect-error chokidar option does not exist in rolldown but used for backward compat
          ...(rollupOptions.watch || {}).chokidar,
          // @ts-expect-error chokidar option does not exist in rolldown but used for backward compat
          ...options.watch.chokidar,
        },
        resolvedOutDirs,
        emptyOutDir,
        environment.config.cacheDir,
      )

      const { watch } = await import('rolldown')
      // 调用 rolldown.watch 创建监听器
      const watcher = watch({
        ...rollupOptions,
        watch: {
          ...rollupOptions.watch,
          ...options.watch,
          notify: convertToNotifyOptions(resolvedChokidarOptions),
        },
      })

      watcher.on('event', (event) => {
        if (event.code === 'BUNDLE_START') {
          logger.info(colors.cyan(`\nbuild started...`))
          chunkMetadataMap.clearResetChunks()
        } else if (event.code === 'BUNDLE_END') {
          event.result.close()
          logger.info(colors.cyan(`built in ${event.duration}ms.`))
        } else if (event.code === 'ERROR') {
          const e = event.error
          enhanceRollupError(e)
          clearLine()
          logger.error(e.message, { error: e })
        }
      })

      return watcher
    }

    // 普通构建
    // write or generate files with rolldown
    const { rolldown } = await import('rolldown')
    startTime = Date.now()
    // 创建 Rolldown 构建实例
    bundle = await rolldown(rollupOptions)

    // 多个输出配置
    const res: RolldownOutput[] = []

    for (const output of arraify(rollupOptions.output!)) {
      // bundle.write(outputOptions) 将产物写入磁盘
      // bundle.generate(outputOptions) 仅返回产物对象
      res.push(await bundle[options.write ? 'write' : 'generate'](output))
    }
    for (const output of res) {
      for (const chunk of output.output) {
        // 注入 chunk 元数据
        injectChunkMetadata(chunkMetadataMap, chunk)
      }
    }
    logger.info(
      `${colors.green(`✓ built in ${displayTime(Date.now() - startTime)}`)}`,
    )

    // 返回构建结果
    return Array.isArray(rollupOptions.output) ? res : res[0]
  } catch (e) {
    enhanceRollupError(e)
    clearLine()
    if (startTime) {
      logger.error(
        `${colors.red('✗')} Build failed in ${displayTime(Date.now() - startTime)}`,
      )
      startTime = undefined
    }
    throw e
  } finally {
    // 关闭 Rolldown 构建实例
    if (bundle) await bundle.close()
  }
}

image.png

image.png

image.png

Vite 8 的生产构建底层完全基于 Rolldown(Rust 打包器),支持两种构建模式:一次性打包(默认 vite build)和 监听打包vite build --watch)。

image.png

命令分析

"build": "run-p type-check \"build-only {@}\" --"
"build-only": "vite build",

run-p:来自 npm-run-all,表示并行执行后面的脚本

  • type-check:第一个要运行的脚本(通常用于 TypeScript 类型检查)。
  • "build-only {@}" :第二个要运行的脚本。
    • build-only 是另一个 npm 脚本(自定义,例如 vite build)。
    • {@} 是 npm-run-all 的特殊占位符,代表传递给当前 build 命令的所有原始参数

测试

{
    build: {
    emptyOutDir:true, // 清空目录
    copyPublicDir: true,
    reportCompressedSize: true,//启用/禁用 gzip 压缩大小报告
    chunkSizeWarningLimit:500,// 规定触发警告的 chunk 大小。(以 kB 为单位)。
    assetsInlineLimit:4096,// 4kb 小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求
    // baseline-widely-available 具体来说,它是 `['chrome111', 'edge111', 'firefox114', 'safari16.4']`
    // esnext —— 即假设有原生动态导入支持,并只执行最低限度的转译。
    target: 'baseline-widely-available',
    // 如果禁用,整个项目中的所有 CSS 将被提取到一个 CSS 文件中。
    cssCodeSplit: true,// 启用,在异步 chunk 中导入的 CSS 将内联到异步 chunk 本身,并在其被加载时一并获取。
    cssMinify: 'lightningcss',// Vite 默认使用 [Lightning CSS](https://lightningcss.dev/minification.html) 来压缩 CSS
    // true,将会创建一个独立的 source map 文件
    // inline,source map 将作为一个 data URI 附加在输出文件中
    sourcemap:false,
    license:true, // true,构建过程将生成一个 .vite/license.md文件,
    }
}

示例 build.outDir 、build.assetsDir

build.outDir默认值 dist,build.assetsDir默认值 assets

image.png

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
  },

image.png

image.png

示例 build.minify

1、默认情况。

minify 默认压缩,客户端构建默认为'oxc'

image.png

2、配置不压缩。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: false, // 不压缩
  },

image.png

3、配置 esbuild 。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: 'esbuild',
  },

提示 在 vite8 中 esbuild 已废弃。建议使用 oxc 。

image.png

4、 配置 terser。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: 'terser',
  },

image.png

当设置为 'esbuild' 或 'terser' 时,必须分别安装 esbuild 或 Terser。

npm add -D esbuild
npm add -D terser

示例 build.manifest / ssrManifest

manifest 设置为 true 时,路径将是 .vite/manifest.json
ssrManifest 设置为 true 时,路径将是 .vite/ssr-manifest.json

image.png

image.png

vue3-vite-cube/dist-cube/index.html

<!doctype html>
<html lang="">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App菜单</title>
    <script type="module" crossorigin src="/public/index-B9iM-AOo.js"></script>
    <link rel="modulepreload" crossorigin href="/public/runtime-core.esm-bundler-HXD8ebTp.js">
    <link rel="stylesheet" crossorigin href="/public/index-DuS5nk76.css">
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

最后

  1. rolldown 配置
  2. vite 配置
昨天以前首页

Connect 深度解析:Node.js 中间件框架的基石

作者 米丘
2026年4月7日 16:33

在 Node.js 生态中,connect 是一个轻量级、可扩展的 HTTP 中间件框架。它虽然代码量不大(核心文件仅数百行),却奠定了 Express、Koa 等现代 Web 框架的中间件设计基础。理解 connect 的源码与设计思想,有助于掌握 Node.js HTTP 开发的底层模式。本文将从概念、使用方法、源码实现、中间件机制以及应用场景五个维度,对 connect 进行全面剖析。

Connect 是什么?

connect ,其定位是“Node.js 的中间件层”。它本身不是一个完整的 Web 框架,而是一个可插拔的 HTTP 请求处理管道。开发者可以将各种功能(日志、静态文件、路由、代理等)以中间件的形式插入到管道中,按顺序处理请求。

connect 的核心概念:

  • 中间件:一个接受 (req, res, next) 的函数,可以修改请求/响应、结束请求或调用下一个中间件。
  • 中间件栈:使用 use 方法注册中间件,形成一个数组(栈),请求到来时依次执行。
  • 错误处理:通过 (err, req, res, next) 形式的中间件捕获异常。

Connect 的核心是其维护的一个中间件队列(stack),通过use方法将中间件注册到队列中。每个中间件都是一个函数,它能够访问请求对象(req)、响应对象(res)以及控制权传递函数(next)。

中间件处理的核心在于next()函数:

  • 调用next() :表示当前中间件已完成处理,将控制权传递给队列中的下一个中间件。
  • 不调用next() :表示请求处理链终止,不再继续向下执行。

这种机制确保了每一个中间件只处理它负责的部分,实现了职责分离和灵活组合。

Connect 使用

1、初始化与基础设置

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

const app = connect();
const server = http.createServer(app);

server.listen(3000);
console.log("Server is running on port 3000");

2、不指定路径,中间件会对每个请求执行

app.use(function logger(req, res, next) {
  const start = Date.now();
  const originalUrl = req.url; // 保存原始 URL
  // 监听响应结束事件(因为 res.end 是异步的)
  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(`响应结束 ${req.method} ${originalUrl} - ${res.statusCode} - ${duration}ms`);
  });
  next();
});

3、路径匹配中间件

请求路径以 /user 开头时触发(如 /user/user/profile

app.use("/user", (req, res, next) => {
  console.log("用户中间件");
  res.setHeader("Content-Type", "text/plain");
  res.end("User area");
});

4、子应用挂载

const adminApp = connect();

adminApp.use((req, res, next) => {
  res.setHeader("Content-Type", "text/plain");
  res.end("Admin area");
});
app.use("/admin", adminApp);

5、错误触发中间件

访问 /error 时主动抛出错误,用于测试错误处理中间件。

app.use("/error", (req, res, next) => {
  throw new Error("Error");
});

5、404中间件

所有未匹配的请求都会返回 404 Not Found

app.use((req, res) => {
  res.statusCode = 404;
  res.end('Not Found');
});

7、错误处理中间件

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.statusCode = 500;
  res.end("Internal Server Error");
});

源码 connect@3.7

/*!
 * connect
 * Copyright(c) 2010 Sencha Inc.
 * Copyright(c) 2011 TJ Holowaychuk
 * Copyright(c) 2015 Douglas Christopher Wilson
 * MIT Licensed
 */

'use strict';

/**
 * Module dependencies.
 * @private
 */

var debug = require('debug')('connect:dispatcher');
var EventEmitter = require('events').EventEmitter;
var finalhandler = require('finalhandler');
var http = require('http');
var merge = require('utils-merge');
var parseUrl = require('parseurl');

/**
 * Module exports.
 * @public
 */

module.exports = createServer;

/**
 * Module variables.
 * @private
 */

var env = process.env.NODE_ENV || 'development';
var proto = {};

/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }

/**
 * Create a new connect server.
 * 用于创建一个新的 Connect 服务器实例。
 * 这个函数返回一个具有中间件处理能力的函数,该函数可以作为 HTTP 服务器的请求处理器
 *
 * @return {function}
 * @public
 */

function createServer() {
  // 创建 app 函数
  // 当 app 函数被调用时,它会调用自身的 handle 方法来处理请求
  function app(req, res, next){ app.handle(req, res, next); }
  // 将 proto 对象的属性合并到 app 函数上
  merge(app, proto);
  merge(app, EventEmitter.prototype);
  app.route = '/';
  app.stack = []; // 存储中间件函数
  return app;
}

/**
 * Utilize the given middleware `handle` to the given `route`,
 * defaulting to _/_. This "route" is the mount-point for the
 * middleware, when given a value other than _/_ the middleware
 * is only effective when that segment is present in the request's
 * pathname.
 *
 * For example if we were to mount a function at _/admin_, it would
 * be invoked on _/admin_, and _/admin/settings_, however it would
 * not be invoked for _/_, or _/posts_.
 *
 * @param {String|Function|Server} route, callback or server
 * @param {Function|Server} callback or server
 * @return {Server} for chaining
 * @public
 */
// 向中间件栈中添加一个新的中间件
proto.use = function use(route, fn) {
  var handle = fn;
  var path = route;

  // default route to '/'
  // 函数重载:如果 route 参数不是字符串,说明是中间件函数,直接赋值给 handle 变量
  // 不指定路径,中间件会对每个请求执行。
  if (typeof route !== 'string') {
    handle = route;
    path = '/';
  }

  // wrap sub-apps
  // 子应用包装(Sub-app)
  // 传入的 handle 是一个 connect 应用实例(具有 handle 方法),则将其包装成一个中间件函数
  if (typeof handle.handle === 'function') {
    var server = handle; // 子应用实例
    server.route = path;
    // 中间件函数
    handle = function (req, res, next) {
      server.handle(req, res, next);
    };
  }

  // wrap vanilla http.Servers
  // HTTP 服务器适配
  // 传入的 handle 是 Node.js 原生 http.Server 实例,则提取其 'request' 事件监听器(即第一个处理函数)作为中间件
  if (handle instanceof http.Server) {
    handle = handle.listeners('request')[0];
  }

  // strip trailing slash
  // 移除路径末尾的斜杠,确保路径格式为 /admin 而不是 /admin/
  if (path[path.length - 1] === '/') {
    path = path.slice(0, -1);
  }

  // add the middleware
  debug('use %s %s', path || '/', handle.name || 'anonymous');

  // 将中间件对象(包含 route 和 handle)推入 this.stack 数组
  this.stack.push({ route: path, handle: handle });

  return this;
};

/**
 * Handle server requests, punting them down
 * the middleware stack.
 * 遍历中间件栈(this.stack),根据请求路径匹配中间件,并依次执行
 *
 * @private
 */

proto.handle = function handle(req, res, out) {
  var index = 0; // 当前中间件在栈中的索引
  // 请求 URL 中的协议+主机部分(如 http://example.com)
  var protohost = getProtohost(req.url) || '';
  var removed = ''; // 记录已被匹配并“剥离”的路由前缀
  var slashAdded = false; // 标记是否因为路径变换而添加了前导斜杠
  var stack = this.stack;

  // final function handler
  // 最终处理函数(默认 finalhandler),当所有中间件执行完或出错时调用
  var done = out || finalhandler(req, res, {
    env: env,
    onerror: logerror
  });

  // store the original URL
  // 保存原始请求 URL
  req.originalUrl = req.originalUrl || req.url;

  function next(err) {
    // 1、恢复 URL 变换
    // 因为匹配路由而临时去掉了前导斜杠(slashAdded === true),则将其加回
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    // 之前剥离了路由前缀(removed 非空),则将其重新拼接到 req.url 前面
    if (removed.length !== 0) {
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // next callback
    // 2、取出当前中间件,索引自增
    var layer = stack[index++];

    // all done
    // 如果已无中间件,则调用 done(可能传递错误),结束处理
    if (!layer) {
      defer(done, err);
      return;
    }

    // 3、路径匹配检查
    // route data
    var path = parseUrl(req).pathname || '/';
    var route = layer.route;

    // skip this layer if the route doesn't match
    if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
      return next(err);
    }

    // skip if route match does not border "/", ".", or end
    // 4、边界符检查
    var c = path.length > route.length && path[route.length];
    if (c && c !== '/' && c !== '.') {
      return next(err);
    }

    // 5、URL 变换(剥离路由前缀)
    // trim off the part of the url that matches the route
    if (route.length !== 0 && route !== '/') {
      removed = route;
      req.url = protohost + req.url.substr(protohost.length + removed.length);

      // ensure leading slash
      if (!protohost && req.url[0] !== '/') {
        req.url = '/' + req.url;
        slashAdded = true;
      }
    }

    // 6、调用中间件
    // call the layer handle
    call(layer.handle, route, err, req, res, next);
  }

  next();
};

/**
 * Listen for connections.
 *
 * This method takes the same arguments
 * as node's `http.Server#listen()`.
 *
 * HTTP and HTTPS:
 *
 * If you run your application both as HTTP
 * and HTTPS you may wrap them individually,
 * since your Connect "server" is really just
 * a JavaScript `Function`.
 *
 *      var connect = require('connect')
 *        , http = require('http')
 *        , https = require('https');
 *
 *      var app = connect();
 *
 *      http.createServer(app).listen(80);
 *      https.createServer(options, app).listen(443);
 *
 * @return {http.Server}
 * @api public
 */

proto.listen = function listen() {
  // 1、创建 HTTP 服务器实例
  // this 指向 connect 应用实例
  // http.createServer(this) 创建原生 HTTP 服务器,并将 app 作为请求监听器
  // 这意味着每当有 HTTP 请求到达时,就会调用 app(req, res),从而进入 Connect 的中间件处理管道
  var server = http.createServer(this);
  // 2、启动 HTTP 服务器
  return server.listen.apply(server, arguments);
};

/**
 * Invoke a route handle.
 * @private
 */

function call(handle, route, err, req, res, next) {
  var arity = handle.length;
  var error = err;
  var hasError = Boolean(err);

  debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);

  try {
    if (hasError && arity === 4) {
      // error-handling middleware
      handle(err, req, res, next);
      return;
    } else if (!hasError && arity < 4) {
      // request-handling middleware
      handle(req, res, next);
      return;
    }
  } catch (e) {
    // replace the error
    error = e;
  }

  // continue
  next(error);
}

/**
 * Log error using console.error.
 *
 * @param {Error} err
 * @private
 */

function logerror(err) {
  if (env !== 'test') console.error(err.stack || err.toString());
}

/**
 * Get get protocol + host for a URL.
 * 从 URL 字符串中提取协议和主机部分(protocol + host)
 *
 * @param {string} url
 * @private
 */

function getProtohost(url) {
  // 检查 URL 是否为空字符串或以 '/' 开头
  // 这类 URL 通常是相对路径,不包含协议和主机信息
  if (url.length === 0 || url[0] === '/') {
    return undefined;
  }

  // 查找 URL 中的协议分隔符位置
  var fqdnIndex = url.indexOf('://')

  return fqdnIndex !== -1 && url.lastIndexOf('?', fqdnIndex) === -1
    ? url.substr(0, url.indexOf('/', 3 + fqdnIndex))
    : undefined;
}

vite 是如何加载解析 vite.config.js 配置文件的?

作者 米丘
2026年4月6日 10:11

当我们在终端运行 vite dev,Vite 启动开发服务器的首个关键步骤就是解析配置。本文将深入剖析 Vite 加载配置文件的三种模式。

loadConfigFromFile 的完整流程

loadConfigFromFile 是配置文件加载的核心函数,其完整流程如下:

  1. 确定配置文件路径(自动查找或使用 --config 指定的路径)。
  2. 根据文件后缀和 package.json 中的 type 字段判断模块格式(是否为 ESM)。
  3. 根据 configLoader加载器配置来加载配置文件和转换代码。
    • bundle模式,调用 bundleConfigFile 使用 rolldown 打包配置文件,获取转换后的代码和依赖列表。调用 loadConfigFromBundledFile 将打包后的代码转成配置对象。
    • runner模式,使用 Vite 的 ModuleRunner 动态转换任何文件。它的核心机制是利用 RunnableDevEnvironment 提供的 runner.import 函数,在独立的执行环境中加载并运行模块
    • native模式,利用原生动态引入。
  4. 如果用户导出的是函数,则调用该函数传入 configEnv(包含 commandmode 等参数),获取最终配置对象。
  5. 返回配置对象、配置文件路径以及依赖列表 dependencies
  let { configFile } = config
  if (configFile !== false) {
    // 从文件加载配置
    const loadResult = await loadConfigFromFile(
      configEnv,
      configFile,
      config.root,
      config.logLevel,
      config.customLogger,
      config.configLoader,
    )
    if (loadResult) {
      config = mergeConfig(loadResult.config, config)
      configFile = loadResult.path
      configFileDependencies = loadResult.dependencies
    }
  }

image.png

如果在执行 vite dev 时没有使用 --config 参数指定配置文件,Vite 将按照以下顺序自动查找并加载配置文件。

const DEFAULT_CONFIG_FILES: string[] = [
  'vite.config.js',
  'vite.config.mjs',
  'vite.config.ts',
  'vite.config.cjs',
  'vite.config.mts',
  'vite.config.cts',
]

Vite 提供了三种配置加载机制

当配置文件被定位后,Vite 如何读取并执行它的内容?这取决于 configLoader 配置选项。Vite 提供了三种机制来加载配置文件,默认使用 bundle 模式。

const resolver =
  configLoader === 'bundle'
    ? bundleAndLoadConfigFile // 处理配置文件的预构建
    : configLoader === 'runner'
      ? runnerImportConfigFile // 处理配置文件的运行时导入
      : nativeImportConfigFile // 处理配置文件的原生导入

bundle (默认)

使用打包工具(Rolldown)将配置文件及其依赖打包成一个临时文件,再加载执行。

function bundleAndLoadConfigFile(resolvedPath: string) {
  // 检查是否为 ESM 模块
  const isESM =
    // 在 Deno 环境中运行
    typeof process.versions.deno === 'string' || isFilePathESM(resolvedPath)

  // 配置文件打包
  // 打包过程会处理配置文件的依赖,将其转换为可执行的代码
  const bundled = await bundleConfigFile(resolvedPath, isESM)
  // 配置加载
  const userConfig = await loadConfigFromBundledFile(
    resolvedPath,
    bundled.code,
    isESM,
  )

  return {
    // 加载的用户配置
    configExport: userConfig,
    // 配置文件的依赖项
    dependencies: bundled.dependencies,
  }
}

image.png

image.png

image.png

image.png

image.png

image.png

bundle.code 字符串

import "node:module";
import { defineConfig } from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vite/dist/node/index.js";
import vue from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import vueJsx from "file:///Users/xxxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue-jsx/dist/index.mjs";
import VueRouter from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vue-router/dist/unplugin/vite.mjs";
Object.create;
Object.defineProperty;
Object.getOwnPropertyDescriptor;
Object.getOwnPropertyNames;
Object.getPrototypeOf;
Object.prototype.hasOwnProperty;
var vite_config_default = defineConfig({
plugins: [
VueRouter({
routesFolder: "src/pages",
extensions: [".vue"],
dts: "src/typed-router.d.ts",
importMode: "async",
root: process.cwd(),
watch: true
}),
vue(),
vueJsx()
],
resolve: { alias: { "@": "/src" } },
css: { preprocessorOptions: { less: {
additionalData: \`@import "@/styles/variables.less";\`,
javascriptEnabled: true
} } },
mode: "development"
});
//#endregion
export { vite_config_default as default };

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiL1VzZXJzL2hhaXlhbi9Eb2N1bWVudHMvY29kZS9jbG91ZGNvZGUvdnVlMy12aXRlLWN1YmUvdml0ZS5jb25maWcudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgdnVlSnN4IGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZS1qc3gnXG5pbXBvcnQgdnVlRGV2VG9vbHMgZnJvbSAndml0ZS1wbHVnaW4tdnVlLWRldnRvb2xzJ1xuaW1wb3J0IFZ1ZVJvdXRlciBmcm9tICd2dWUtcm91dGVyL3ZpdGUnXG5cblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgLy8g5b+F6aG76KaB5ZyoIHZ1ZSDmj5Lku7bkuYvliY1cbiAgICBWdWVSb3V0ZXIoe1xuICAgICAgcm91dGVzRm9sZGVyOiAnc3JjL3BhZ2VzJywgLy8g6buY6K6kIHBhZ2VzXG4gICAgICBleHRlbnNpb25zOiBbJy52dWUnXSwgLy8g5Yy56YWN5paH5Lu25ZCO57yAXG4gICAgICBkdHM6ICdzcmMvdHlwZWQtcm91dGVyLmQudHMnLCAvLyDnlJ/miJDnsbvlnovmlofku7ZcbiAgICAgIC8vIOWHuueOsCBSYW5nZUVycm9yOiBNYXhpbXVtIGNhbGwgc3RhY2sgc2l6ZSBleGNlZWRlZFxuICAgICAgLy8gZ2V0Um91dGVOYW1lOiAocm91dGUpID0+IHtcbiAgICAgIC8vICAgY29uc29sZS5sb2coJ2dldFJvdXRlTmFtZScscm91dGUpXG4gICAgICAvLyAgIHJldHVybiByb3V0ZS5uYW1lIHx8IHJvdXRlLnBhdGhcbiAgICAgIC8vIH0sXG5cbiAgICAgICAvLyDmt7vliqDosIPor5XpgInpoblcbiAgICAgIC8vIGxvZ3M6IHRydWUsXG5cbiAgICAgIC8vIHJvdXRlQmxvY2tMYW5nOiAnanNvbjUnLCAvLyDot6/nlLHlnZfor63oqIDvvIzpu5jorqQganNvblxuICAgICAgaW1wb3J0TW9kZTogJ2FzeW5jJyxcbiAgICAgIHJvb3Q6IHByb2Nlc3MuY3dkKCksXG5cbiAgICAgIC8vIOWcqOmFjee9ruaWh+S7tuWGmeWFpeWJje+8jOaJi+WKqOS/ruaUuei3r+eUsemFjee9ru+8iOWmgua3u+WKoOWFqOWxgOi3r+eUseWuiOWNq+OAgeiwg+aVtOi3r+eUseWFg+S/oeaBr+OAgei/h+a7pOi3r+eUseetie+8iVxuICAgICAgLy8gYmVmb3JlV3JpdGVGaWxlczogKGVkaXRlZFJvdXRlcykgPT4ge1xuICAgICAgLy8gICBjb25zb2xlLmxvZygnYmVmb3JlV3JpdGVGaWxlcycsIGVkaXRlZFJvdXRlcylcbiAgICAgIC8vIH0sXG4gICAgICB3YXRjaDogdHJ1ZSwgLy8g5byA5ZCv6Lev55Sx5Z2X5paH5Lu255uR5ZCsXG4gICAgICAvLyDlvIDlkK/lrp7pqozmgKflip/og71cbiAgICAgIC8vIGV4cGVyaW1lbnRhbDoge1xuICAgICAgIFxuICAgICAgLy8gfSxcbiAgICB9KSxcbiAgICB2dWUoKSxcbiAgICB2dWVKc3goKSxcbiAgICAvLyB2dWVEZXZUb29scygpLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgLy8gYWxpYXM6IHtcbiAgICAvLyAgICdAJzogZmlsZVVSTFRvUGF0aChuZXcgVVJMKCcuL3NyYycsIGltcG9ydC5tZXRhLnVybCkpXG4gICAgLy8gfSxcbiAgICBhbGlhczoge1xuICAgICAgJ0AnOiAnL3NyYycsXG4gICAgfSxcbiAgICAvLyB0c2NvbmZpZ1BhdGhzOiB0cnVlLCAgLy8g6Ieq5Yqo6K+75Y+WIHRzY29uZmlnIHBhdGhzXG4gIH0sXG4gIGNzczoge1xuICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcbiAgICAgIGxlc3M6IHtcbiAgICAgICAgYWRkaXRpb25hbERhdGE6IGBAaW1wb3J0IFwiQC9zdHlsZXMvdmFyaWFibGVzLmxlc3NcIjtgLFxuICAgICAgICBqYXZhc2NyaXB0RW5hYmxlZDogdHJ1ZVxuICAgICAgfVxuICAgIH1cbiAgfSxcbiAgbW9kZTogJ2RldmVsb3BtZW50JyxcbiAgLy8gc2VydmVyOiB7XG4gIC8vICAgd3M6IGZhbHNlLFxuICAvLyB9LFxuICAvLyBvcHRpbWl6ZURlcHM6IHtcbiAgLy8gICBpbmNsdWRlOiBbJ3ZpcnR1YWw6dnVlLWluc3BlY3Rvci1wYXRoOmxvYWQuanMnXSxcbiAgLy8gfSxcblxufSkiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBU0EsSUFBQSxzQkFBZSxhQUFhO0NBQzFCLFNBQVM7RUFFUCxVQUFVO0dBQ1IsY0FBYztHQUNkLFlBQVksQ0FBQyxPQUFPO0dBQ3BCLEtBQUs7R0FXTCxZQUFZO0dBQ1osTUFBTSxRQUFRLEtBQUs7R0FNbkIsT0FBTztHQUtSLENBQUM7RUFDRixLQUFLO0VBQ0wsUUFBUTtFQUVUO0NBQ0QsU0FBUyxFQUlQLE9BQU8sRUFDTCxLQUFLLFFBQ04sRUFFRjtDQUNELEtBQUssRUFDSCxxQkFBcUIsRUFDbkIsTUFBTTtFQUNKLGdCQUFnQjtFQUNoQixtQkFBbUI7RUFDckIsRUFDRixFQUNEO0NBQ0QsTUFBTTtDQVFQLENBQUEifQ==

dependencies

[
  "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/vite.config.ts",
]

临时文件

image.png

image.png

vue3-vite-cube/node_modules/.vite-temp/vite.config.ts.timestamp-1775361732369-f30607f0da0d6.mjs 文件内容如下:

import "node:module";
import { defineConfig } from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vite/dist/node/index.js";
import vue from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import vueJsx from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/@vitejs/plugin-vue-jsx/dist/index.mjs";
import VueRouter from "file:///Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/node_modules/vue-router/dist/unplugin/vite.mjs";
Object.create;
Object.defineProperty;
Object.getOwnPropertyDescriptor;
Object.getOwnPropertyNames;
Object.getPrototypeOf;
Object.prototype.hasOwnProperty;
var vite_config_default = defineConfig({
plugins: [
VueRouter({
routesFolder: "src/pages",
extensions: [".vue"],
dts: "src/typed-router.d.ts",
importMode: "async",
root: process.cwd(),
watch: true
}),
vue(),
vueJsx()
],
resolve: { alias: { "@": "/src" } },
css: { preprocessorOptions: { less: {
additionalData: `@import "@/styles/variables.less";`,
javascriptEnabled: true
} } },
mode: "development"
});
//#endregion
export { vite_config_default as default };

//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiL1VzZXJzL2hhaXlhbi9Eb2N1bWVudHMvY29kZS9jbG91ZGNvZGUvdnVlMy12aXRlLWN1YmUvdml0ZS5jb25maWcudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHZ1ZSBmcm9tICdAdml0ZWpzL3BsdWdpbi12dWUnXG5pbXBvcnQgdnVlSnN4IGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZS1qc3gnXG5pbXBvcnQgdnVlRGV2VG9vbHMgZnJvbSAndml0ZS1wbHVnaW4tdnVlLWRldnRvb2xzJ1xuaW1wb3J0IFZ1ZVJvdXRlciBmcm9tICd2dWUtcm91dGVyL3ZpdGUnXG5cblxuLy8gaHR0cHM6Ly92aXRlLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgLy8g5b+F6aG76KaB5ZyoIHZ1ZSDmj5Lku7bkuYvliY1cbiAgICBWdWVSb3V0ZXIoe1xuICAgICAgcm91dGVzRm9sZGVyOiAnc3JjL3BhZ2VzJywgLy8g6buY6K6kIHBhZ2VzXG4gICAgICBleHRlbnNpb25zOiBbJy52dWUnXSwgLy8g5Yy56YWN5paH5Lu25ZCO57yAXG4gICAgICBkdHM6ICdzcmMvdHlwZWQtcm91dGVyLmQudHMnLCAvLyDnlJ/miJDnsbvlnovmlofku7ZcbiAgICAgIC8vIOWHuueOsCBSYW5nZUVycm9yOiBNYXhpbXVtIGNhbGwgc3RhY2sgc2l6ZSBleGNlZWRlZFxuICAgICAgLy8gZ2V0Um91dGVOYW1lOiAocm91dGUpID0+IHtcbiAgICAgIC8vICAgY29uc29sZS5sb2coJ2dldFJvdXRlTmFtZScscm91dGUpXG4gICAgICAvLyAgIHJldHVybiByb3V0ZS5uYW1lIHx8IHJvdXRlLnBhdGhcbiAgICAgIC8vIH0sXG5cbiAgICAgICAvLyDmt7vliqDosIPor5XpgInpoblcbiAgICAgIC8vIGxvZ3M6IHRydWUsXG5cbiAgICAgIC8vIHJvdXRlQmxvY2tMYW5nOiAnanNvbjUnLCAvLyDot6/nlLHlnZfor63oqIDvvIzpu5jorqQganNvblxuICAgICAgaW1wb3J0TW9kZTogJ2FzeW5jJyxcbiAgICAgIHJvb3Q6IHByb2Nlc3MuY3dkKCksXG5cbiAgICAgIC8vIOWcqOmFjee9ruaWh+S7tuWGmeWFpeWJje+8jOaJi+WKqOS/ruaUuei3r+eUsemFjee9ru+8iOWmgua3u+WKoOWFqOWxgOi3r+eUseWuiOWNq+OAgeiwg+aVtOi3r+eUseWFg+S/oeaBr+OAgei/h+a7pOi3r+eUseetie+8iVxuICAgICAgLy8gYmVmb3JlV3JpdGVGaWxlczogKGVkaXRlZFJvdXRlcykgPT4ge1xuICAgICAgLy8gICBjb25zb2xlLmxvZygnYmVmb3JlV3JpdGVGaWxlcycsIGVkaXRlZFJvdXRlcylcbiAgICAgIC8vIH0sXG4gICAgICB3YXRjaDogdHJ1ZSwgLy8g5byA5ZCv6Lev55Sx5Z2X5paH5Lu255uR5ZCsXG4gICAgICAvLyDlvIDlkK/lrp7pqozmgKflip/og71cbiAgICAgIC8vIGV4cGVyaW1lbnRhbDoge1xuICAgICAgIFxuICAgICAgLy8gfSxcbiAgICB9KSxcbiAgICB2dWUoKSxcbiAgICB2dWVKc3goKSxcbiAgICAvLyB2dWVEZXZUb29scygpLFxuICBdLFxuICByZXNvbHZlOiB7XG4gICAgLy8gYWxpYXM6IHtcbiAgICAvLyAgICdAJzogZmlsZVVSTFRvUGF0aChuZXcgVVJMKCcuL3NyYycsIGltcG9ydC5tZXRhLnVybCkpXG4gICAgLy8gfSxcbiAgICBhbGlhczoge1xuICAgICAgJ0AnOiAnL3NyYycsXG4gICAgfSxcbiAgICAvLyB0c2NvbmZpZ1BhdGhzOiB0cnVlLCAgLy8g6Ieq5Yqo6K+75Y+WIHRzY29uZmlnIHBhdGhzXG4gIH0sXG4gIGNzczoge1xuICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcbiAgICAgIGxlc3M6IHtcbiAgICAgICAgYWRkaXRpb25hbERhdGE6IGBAaW1wb3J0IFwiQC9zdHlsZXMvdmFyaWFibGVzLmxlc3NcIjtgLFxuICAgICAgICBqYXZhc2NyaXB0RW5hYmxlZDogdHJ1ZVxuICAgICAgfVxuICAgIH1cbiAgfSxcbiAgbW9kZTogJ2RldmVsb3BtZW50JyxcbiAgLy8gc2VydmVyOiB7XG4gIC8vICAgd3M6IGZhbHNlLFxuICAvLyB9LFxuICAvLyBvcHRpbWl6ZURlcHM6IHtcbiAgLy8gICBpbmNsdWRlOiBbJ3ZpcnR1YWw6dnVlLWluc3BlY3Rvci1wYXRoOmxvYWQuanMnXSxcbiAgLy8gfSxcblxufSkiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBU0EsSUFBQSxzQkFBZSxhQUFhO0NBQzFCLFNBQVM7RUFFUCxVQUFVO0dBQ1IsY0FBYztHQUNkLFlBQVksQ0FBQyxPQUFPO0dBQ3BCLEtBQUs7R0FXTCxZQUFZO0dBQ1osTUFBTSxRQUFRLEtBQUs7R0FNbkIsT0FBTztHQUtSLENBQUM7RUFDRixLQUFLO0VBQ0wsUUFBUTtFQUVUO0NBQ0QsU0FBUyxFQUlQLE9BQU8sRUFDTCxLQUFLLFFBQ04sRUFFRjtDQUNELEtBQUssRUFDSCxxQkFBcUIsRUFDbkIsTUFBTTtFQUNKLGdCQUFnQjtFQUNoQixtQkFBbUI7RUFDckIsRUFDRixFQUNEO0NBQ0QsTUFBTTtDQVFQLENBQUEifQ==
/**
 * 用于从打包后的代码加载 Vite 配置。
 * 它根据模块类型(ESM 或 CommonJS)采用不同的加载策略,确保配置文件能够被正确执行并返回配置对象
 * @param fileName  文件路径
 * @param bundledCode 打包转换后代码
 * @param isESM 是否为 ESM 格式
 * @returns 
 */
async function loadConfigFromBundledFile(
  fileName: string,
  bundledCode: string,
  isESM: boolean,
): Promise<UserConfigExport> {
  // for esm, before we can register loaders without requiring users to run node
  // with --experimental-loader themselves, we have to do a hack here:
  // write it to disk, load it with native Node ESM, then delete the file.
  if (isESM) {
    // Storing the bundled file in node_modules/ is avoided for Deno
    // because Deno only supports Node.js style modules under node_modules/
    // and configs with `npm:` import statements will fail when executed.
    // 查找最近的 node_modules 目录
    let nodeModulesDir =
      typeof process.versions.deno === 'string'
        ? undefined
        : findNearestNodeModules(path.dirname(fileName))

    if (nodeModulesDir) {
      try {
        // 创建临时目录
        // node_modules/.vite-temp/
        await fsp.mkdir(path.resolve(nodeModulesDir, '.vite-temp/'), {
          recursive: true,
        })
      } catch (e) {
        if (e.code === 'EACCES') {
          // If there is no access permission, a temporary configuration file is created by default.
          nodeModulesDir = undefined
        } else {
          throw e
        }
      }
    }
    // 生成 hash 值
    const hash = `timestamp-${Date.now()}-${Math.random().toString(16).slice(2)}`
    // 生成临时文件名
    const tempFileName = nodeModulesDir
      ? path.resolve(
          nodeModulesDir,
          `.vite-temp/${path.basename(fileName)}.${hash}.mjs`,
        )
      : `${fileName}.${hash}.mjs`
      // 写入临时文件
    await fsp.writeFile(tempFileName, bundledCode)
    try {
      // 将文件系统路径转换为 file:// 协议的 URL 对象
      // 原因:ESM 的 import() 语法要求模块标识符为 URL 格式(对于本地文件),不能直接使用文件系统路径
      // 动态加载 ESM 格式配置文件
      // 执行过程:
      // 1、Node.js 读取并执行 tempFileName 指向的文件
      // 2、执行文件中的代码,构建模块的导出
      // 3、生成包含所有导出的模块命名空间对象
      // 4、Promise 解析为该命名空间对象
      return (await import(pathToFileURL(tempFileName).href)).default
    } finally {
      fs.unlink(tempFileName, () => {}) // Ignore errors
    }
  }
  // for cjs, we can register a custom loader via `_require.extensions`
  else {
    // 获取文件扩展名
    const extension = path.extname(fileName)
    // We don't use fsp.realpath() here because it has the same behaviour as
    // fs.realpath.native. On some Windows systems, it returns uppercase volume
    // letters (e.g. "C:\") while the Node.js loader uses lowercase volume letters.
    // See https://github.com/vitejs/vite/issues/12923
    // 获取文件的真实路径
    // 避免 Windows 系统上的路径大小写问题
    const realFileName = await promisifiedRealpath(fileName)
    // 确定加载器扩展名
    // require.extensions 标记已废弃
    const loaderExt = extension in _require.extensions ? extension : '.js'
    const defaultLoader = _require.extensions[loaderExt]!
    // 注册自定义加载器
    _require.extensions[loaderExt] = (module: NodeModule, filename: string) => {
      if (filename === realFileName) {
        // 执行打包后的代码
        ;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
      } else {
        // 使用默认加载器
        defaultLoader(module, filename)
      }
    }
    // clear cache in case of server restart
    // 清除缓存
    delete _require.cache[_require.resolve(fileName)]
    // 加载配置文件
    const raw = _require(fileName)
    // 恢复默认加载器
    _require.extensions[loaderExt] = defaultLoader
    return raw.__esModule ? raw.default : raw
  }
}

runner (实验性)

runner 模式不会创建临时配置文件,而是使用 Vite 的 ModuleRunner 动态转换任何文件。它的核心机制是利用 RunnableDevEnvironment 提供的 runner.import 函数,在独立的执行环境中加载并运行模块。

{
   "start": "vite --configLoader=runner",
}
/**
 * 用于通过 runner 方式导入配置文件。
 * 它使用 runnerImport 函数动态加载配置文件,提取默认导出作为配置对象,并返回配置对象及其依赖项。
 * @param resolvedPath 配置文件路径
 * @returns 
 */
async function runnerImportConfigFile(resolvedPath: string) {
  const { module, dependencies } = await runnerImport<{
    default: UserConfigExport
  }>(resolvedPath)
  return {
    configExport: module.default,
    dependencies,
  }
}

image.png

async function runnerImport<T>(
  moduleId: string,
  inlineConfig?: InlineConfig,
): Promise<RunnerImportResult<T>> {

  // 模块同步条件检查
  const isModuleSyncConditionEnabled = (await import('#module-sync-enabled'))
    .default

  // 配置解析
  const config = await resolveConfig(
    // 合并配置
    mergeConfig(inlineConfig || {}, {
      configFile: false, // 禁用配置文件解析
      envDir: false, // 禁用环境变量目录解析
      cacheDir: process.cwd(), // 缓存目录设置为当前工作目录
      environments: {
        inline: {
          // 指定环境的消费方为服务器端
          consumer: 'server',
          dev: {
            // 启用模块运行器转换
            moduleRunnerTransform: true,
          },
          // 模块解析配置
          resolve: {
            // 启用外部模块解析,将依赖视为外部模块,不进行打包
            // 影响:减少打包体积,提高模块加载速度
            external: true,
            // 清空主字段数组
            // 不使用 package.json 中的主字段进行模块解析
            // 避免因主字段优先级导致的解析问题,确保一致性
            mainFields: [],
            // 指定模块解析条件
            conditions: [
              'node',
              ...(isModuleSyncConditionEnabled ? ['module-sync'] : []),
            ],
          },
        },
      },
    } satisfies InlineConfig),
    'serve', // 确保是 serve 命令
  )
  // 创建可运行的开发环境
  const environment = createRunnableDevEnvironment('inline', config, {
    runnerOptions: {
      hmr: {
        logger: false, // 禁用 HMR 日志记录
      },
    },
    hot: false, // 禁用 HMR
  })
  // 初始化环境
  // 准备模块运行器,确保能够正确加载模块
  await environment.init()
  try {
    // 使用环境的运行器导入模块
    // 模块加载与执行:
    // 1、ModuleRunner 解析 moduleId,处理路径解析
    // 2、加载模块文件内容
    // 3、应用必要的转换(如 ESM 到 CommonJS 的转换)
    // 4、执行模块代码
    // 5、收集模块的依赖项
    const module = await environment.runner.import(moduleId)

    // 获取所有评估过的模块
    const modules = [
      ...environment.runner.evaluatedModules.urlToIdModuleMap.values(),
    ]
    // 过滤出所有外部化模块和当前模块
    // 这些模块不是依赖项,因为它们是 Vite 内部使用的模块
    const dependencies = modules
      .filter((m) => {
        // ignore all externalized modules
        // 忽略没有meta的模块 或者标记为外部化的模块
        if (!m.meta || 'externalize' in m.meta) {
          return false
        }
        // ignore the current module
        // 忽略当前模块,因为它不是依赖项
        return m.exports !== module
      })
      .map((m) => m.file)

    return {
      module,
      dependencies,
    }
  } finally {
    // 关闭环境
    // 释放所有资源,避免内存泄漏等问题
    await environment.close()
  }
}

image.png

module

{
  default: {
    plugins: [
      {
        name: "vue-router",
        enforce: "pre",
        resolveId: {
          filter: {
            id: {
              include: [
                {
                },
                {
                },
                {
                },
              ],
            },
          },
          handler: function(...args) {
            const [id] = args;
            if (!supportNativeFilter(this, key) && !filter(id)) return;
            return handler.apply(this, args);
          },
        },
        buildStart: async buildStart() {
          await ctx.scanPages(options.watch);
        },
        buildEnd: function() {
          ctx.stopWatcher();
        },
        transform: {
          filter: {
            id: {
              include: [
                "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/**/*.vue",
                {
                },
              ],
              exclude: [
              ],
            },
          },
          handler: function(...args) {
            const [code, id] = args;
            if (plugin.transformInclude && !plugin.transformInclude(id)) return;
            if (!supportNativeFilter(this, key) && !filter(id, code)) return;
            return handler.apply(this, args);
          },
        },
        load: {
          filter: {
            id: {
              include: [
                {
                },
                {
                },
                {
                },
              ],
            },
          },
          handler: function(...args) {
            const [id] = args;
            if (plugin.loadInclude && !plugin.loadInclude(id)) return;
            if (!supportNativeFilter(this, key) && !filter(id)) return;
            return handler.apply(this, args);
          },
        },
        vite: {
          configureServer: function(server) {
            ctx.setServerContext(createViteContext(server));
          },
        },
        configureServer: function(server) {
          ctx.setServerContext(createViteContext(server));
        },
      },
      {
        name: "vite:vue",
        api: {
          options: {
            isProduction: false,
            compiler: null,
            customElement: {
            },
            root: "/Users/xxxx/Documents/code/cloudcode/vue3-vite-cube",
            sourceMap: true,
            cssDevSourcemap: false,
          },
          include: {
          },
          exclude: undefined,
          version: "6.0.5",
        },
        handleHotUpdate: function(ctx) {
          ctx.server.ws.send({
          type: "custom",
          event: "file-changed",
          data: { file: normalizePath(ctx.file) }
          });
          if (options.value.compiler.invalidateTypeCache) options.value.compiler.invalidateTypeCache(ctx.file);
          let typeDepModules;
          const matchesFilter = filter.value(ctx.file);
          if (typeDepToSFCMap.has(ctx.file)) {
          typeDepModules = handleTypeDepChange(typeDepToSFCMap.get(ctx.file), ctx);
          if (!matchesFilter) return typeDepModules;
          }
          if (matchesFilter) return handleHotUpdate(ctx, options.value, customElementFilter.value(ctx.file), typeDepModules);
        },
        config: function(config) {
          const parseDefine = (v) => {
          try {
          return typeof v === "string" ? JSON.parse(v) : v;
          } catch (err) {
          return v;
          }
          };
          return {
          resolve: { dedupe: config.build?.ssr ? [] : ["vue"] },
          define: {
          __VUE_OPTIONS_API__: options.value.features?.optionsAPI ?? parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
          __VUE_PROD_DEVTOOLS__: (options.value.features?.prodDevtools || parseDefine(config.define?.__VUE_PROD_DEVTOOLS__)) ?? false,
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: (options.value.features?.prodHydrationMismatchDetails || parseDefine(config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__)) ?? false
          },
          ssr: { external: config.legacy?.buildSsrCjsExternalHeuristics ? ["vue", "@vue/server-renderer"] : [] }
          };
        },
        configResolved: function(config) {
          options.value = {
          ...options.value,
          root: config.root,
          sourceMap: config.command === "build" ? !!config.build.sourcemap : true,
          cssDevSourcemap: config.css?.devSourcemap ?? false,
          isProduction: config.isProduction,
          devToolsEnabled: !!(options.value.features?.prodDevtools || config.define.__VUE_PROD_DEVTOOLS__ || !config.isProduction)
          };
          const _warn = config.logger.warn;
          config.logger.warn = (...args) => {
          if (args[0].match(/\[lightningcss\] '(deep|slotted|global)' is not recognized as a valid pseudo-/)) return;
          _warn(...args);
          };
          transformCachedModule = config.command === "build" && options.value.sourceMap && config.build.watch != null;
        },
        options: function() {
          optionsHookIsCalled = true;
          plugin.transform.filter = { id: {
          include: [...makeIdFiltersToMatchWithQuery(ensureArray(include.value)), /[?&]vue\b/],
          exclude: exclude.value
          } };
        },
        shouldTransformCachedModule: function({ id }) {
          if (transformCachedModule && parseVueRequest(id).query.vue) return true;
          return false;
        },
        configureServer: function(server) {
          options.value.devServer = server;
        },
        buildStart: function() {
          const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
          if (compiler.invalidateTypeCache) options.value.devServer?.watcher.on("unlink", (file) => {
          compiler.invalidateTypeCache(file);
          });
        },
        resolveId: {
          filter: {
            id: [
              {
              },
              {
              },
            ],
          },
          handler: function(id) {
            if (id === EXPORT_HELPER_ID) return id;
            if (parseVueRequest(id).query.vue) return id;
          },
        },
        load: {
          filter: {
            id: [
              {
              },
              {
              },
            ],
          },
          handler: function(id, opt) {
            if (id === EXPORT_HELPER_ID) return helperCode;
            const ssr = opt?.ssr === true;
            const { filename, query } = parseVueRequest(id);
            if (query.vue) {
            if (query.src) return fs.readFileSync(filename, "utf-8");
            const descriptor = getDescriptor(filename, options.value);
            let block;
            if (query.type === "script") block = resolveScript(descriptor, options.value, ssr, customElementFilter.value(filename));
            else if (query.type === "template") block = descriptor.template;
            else if (query.type === "style") block = descriptor.styles[query.index];
            else if (query.index != null) block = descriptor.customBlocks[query.index];
            if (block) return {
            code: block.content,
            map: block.map
            };
            }
          },
        },
        transform: {
          handler: function(code, id, opt) {
            const ssr = opt?.ssr === true;
            const { filename, query } = parseVueRequest(id);
            if (query.raw || query.url) return;
            if (!filter.value(filename) && !query.vue) return;
            if (!query.vue) return transformMain(code, filename, options.value, this, ssr, customElementFilter.value(filename));
            else {
            const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
            if (query.src) this.addWatchFile(filename);
            if (query.type === "template") return transformTemplateAsModule(code, filename, descriptor, options.value, this, ssr, customElementFilter.value(filename));
            else if (query.type === "style") return transformStyle(code, descriptor, Number(query.index || 0), options.value, this, filename);
            }
          },
        },
      },
      {
        name: "vite:vue-jsx",
        config: function(config) {
          const parseDefine = (v) => {
          try {
          return typeof v === "string" ? JSON.parse(v) : v;
          } catch (err) {
          return v;
          }
          };
          const isRolldownVite = this && "rolldownVersion" in this.meta;
          return {
          [isRolldownVite ? "oxc" : "esbuild"]: tsTransform === "built-in" ? { exclude: /\.jsx?$/ } : { include: /\.ts$/ },
          define: {
          __VUE_OPTIONS_API__: parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
          __VUE_PROD_DEVTOOLS__: parseDefine(config.define?.__VUE_PROD_DEVTOOLS__) ?? false,
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: parseDefine(config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__) ?? false
          },
          optimizeDeps: isRolldownVite ? { rolldownOptions: { transform: { jsx: "preserve" } } } : {}
          };
        },
        configResolved: function(config) {
          needHmr = config.command === "serve" && !config.isProduction;
          needSourceMap = config.command === "serve" || !!config.build.sourcemap;
          root = config.root;
        },
        resolveId: {
          filter: {
            id: {
            },
          },
          handler: function(id) {
            if (id === ssrRegisterHelperId) return id;
          },
        },
        load: {
          filter: {
            id: {
            },
          },
          handler: function(id) {
            if (id === ssrRegisterHelperId) return ssrRegisterHelperCode;
          },
        },
        transform: {
          order: undefined,
          filter: {
            id: {
              include: {
              },
              exclude: undefined,
            },
          },
          handler: async handler(code, id, opt) {
            const ssr = opt?.ssr === true;
            const [filepath] = id.split("?");
            if (filter(id) || filter(filepath)) {
            const plugins = [[jsx, babelPluginOptions], ...babelPlugins];
            if (id.endsWith(".tsx") || filepath.endsWith(".tsx")) if (tsTransform === "built-in") plugins.push([await import("@babel/plugin-syntax-typescript").then((r) => r.default), { isTSX: true }]);
            else plugins.push([await import("@babel/plugin-transform-typescript").then((r) => r.default), {
            ...tsPluginOptions,
            isTSX: true,
            allowExtensions: true
            }]);
            if (!ssr && !needHmr) plugins.push(() => {
            return { visitor: { CallExpression: { enter(_path) {
            if (isDefineComponentCall(_path.node, defineComponentName)) {
            const callee = _path.node.callee;
            callee.name = `/* @__PURE__ */ ${callee.name}`;
            }
            } } } };
            });
            else plugins.push(() => {
            return { visitor: { ExportDefaultDeclaration: { enter(_path) {
            const unwrappedDeclaration = unwrapTypeAssertion(_path.node.declaration);
            if (isDefineComponentCall(unwrappedDeclaration, defineComponentName)) {
            const declaration = unwrappedDeclaration;
            const nodesPath = _path.replaceWithMultiple([types.variableDeclaration("const", [types.variableDeclarator(types.identifier("__default__"), types.callExpression(declaration.callee, declaration.arguments))]), types.exportDefaultDeclaration(types.identifier("__default__"))]);
            _path.scope.registerDeclaration(nodesPath[0]);
            }
            } } } };
            });
            const result = babel.transformSync(code, {
            babelrc: false,
            ast: true,
            plugins,
            sourceMaps: needSourceMap,
            sourceFileName: id,
            configFile: false
            });
            if (!ssr && !needHmr) {
            if (!result.code) return;
            return {
            code: result.code,
            map: result.map
            };
            }
            const declaredComponents = [];
            const hotComponents = [];
            for (const node of result.ast.program.body) {
            if (node.type === "VariableDeclaration") {
            const names = parseComponentDecls(node, defineComponentName);
            if (names.length) declaredComponents.push(...names);
            }
            if (node.type === "ExportNamedDeclaration") {
            if (node.declaration && node.declaration.type === "VariableDeclaration") hotComponents.push(...parseComponentDecls(node.declaration, defineComponentName).map((name) => ({
            local: name,
            exported: name,
            id: getHash(id + name)
            })));
            else if (node.specifiers.length) {
            for (const spec of node.specifiers) if (spec.type === "ExportSpecifier" && spec.exported.type === "Identifier") {
            if (declaredComponents.find((name) => name === spec.local.name)) hotComponents.push({
            local: spec.local.name,
            exported: spec.exported.name,
            id: getHash(id + spec.exported.name)
            });
            }
            }
            }
            if (node.type === "ExportDefaultDeclaration") {
            if (node.declaration.type === "Identifier") {
            const _name = node.declaration.name;
            if (declaredComponents.find((name) => name === _name)) hotComponents.push({
            local: _name,
            exported: "default",
            id: getHash(id + "default")
            });
            } else if (isDefineComponentCall(unwrapTypeAssertion(node.declaration), defineComponentName)) hotComponents.push({
            local: "__default__",
            exported: "default",
            id: getHash(id + "default")
            });
            }
            }
            if (hotComponents.length) {
            if (needHmr && !ssr && !/\?vue&type=script/.test(id)) {
            let code = result.code;
            let callbackCode = ``;
            for (const { local, exported, id } of hotComponents) {
            code += `\n${local}.__hmrId = "${id}"\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})`;
            callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`;
            }
            const newCompNames = hotComponents.map((c) => `${c.exported}: __${c.exported}`).join(",");
            code += `\nimport.meta.hot.accept(({${newCompNames}}) => {${callbackCode}\n})`;
            result.code = code;
            }
            if (ssr) {
            const normalizedId = normalizePath(path.relative(root, id));
            let ssrInjectCode = `\nimport { ssrRegisterHelper } from "${ssrRegisterHelperId}"\nconst __moduleId = ${JSON.stringify(normalizedId)}`;
            for (const { local } of hotComponents) ssrInjectCode += `\nssrRegisterHelper(${local}, __moduleId)`;
            result.code += ssrInjectCode;
            }
            }
            if (!result.code) return;
            return {
            code: result.code,
            map: result.map
            };
            }
          },
        },
      },
    ],
    resolve: {
      alias: {
        "@": "/src",
      },
    },
    css: {
      preprocessorOptions: {
        less: {
          additionalData: "@import \"@/styles/variables.less\";",
          javascriptEnabled: true,
        },
      },
    },
    mode: "development",
  },
}

导出的内容就是 vite.config.js 中配置信息

image.png

ModuleRunner模块运行器

  public async import<T = any>(url: string): Promise<T> {
    // 获取缓存模块
    const fetchedModule = await this.cachedModule(url)
    // 执行模块请求
    return await this.cachedRequest(url, fetchedModule)
  }

image.png

native (实验性)

native 模式直接通过 Node.js 原生的动态 import() 加载配置文件,不经过任何打包步骤。

只能编写纯 JavaScript,可以指定 --configLoader native 来使用环境的原生运行时加载配置文件。

{
   "start": "vite --configLoader=native",
}
  • 它的优点是简单快速,调试时断点可以直接定位到源码,不受临时文件干扰。
  • 但这种模式有一个重要限制:配置文件导入的模块的更新不会被检测到,因此不会自动重启 Vite 服务器
async function nativeImportConfigFile(resolvedPath: string) {
  const module = await import(
    pathToFileURL(resolvedPath).href + '?t=' + Date.now()
  )
  return {
    configExport: module.default,
    dependencies: [],
  }
}

在 native 模式下,由于没有经过打包工具分析依赖,Vite 无法知道配置文件引入了哪些本地模块。因此依赖列表被硬编码为空数组,意味着当配置文件导入的其他本地文件(如 ./utils.js)发生变化时,Vite 不会自动重启服务器。这是 native 模式的重要限制。

三者的区别

image.png

深入解析 Vite dev:从命令行到浏览器热更新的完整旅程

作者 米丘
2026年4月5日 18:21

在前端工程化领域,Vite 凭借其极致的开发体验和强大的构建能力,已成为新一代开发工具链的事实标准。随着 Vite 8 的正式发布,这套工具在性能和架构上再次实现突破——底层打包器统一为 Rust 编写的 Rolldown,开发环境启动速度和热更新响应迈入毫秒级时代。而作为开发者每天最常接触的命令行入口,vite dev 和 vite serve 背后承载着怎样的设计理念?它们又有哪些鲜为人知的细节?本文将为你一一揭晓。

命令本质:开发服务器的统一入口

在 Vite 8 中,vite dev 与 vite serve 实际上是同一个命令的两种不同叫法,二者完全等价。

vite
vite dev
vite serve

之所以保留两个名称,主要是为了兼容过往的习惯(如 serve 源自早期版本,而 dev 更直观地表达开发用途)。

vite 启动通用的命令行参数?

// 定义 Vite 命令行工具
const cli = cac('vite')

cli
  .option('-c, --config <file>', `[string] use specified config file`)
  .option('--base <path>', `[string] public base path (default: /)`, {
    type: [convertBase],
  })
  .option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
  .option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
  .option(
    '--configLoader <loader>',
    `[string] use 'bundle' to bundle the config with Rolldown, or 'runner' (experimental) to process it on the fly, or 'native' (experimental) to load using the native runtime (default: bundle)`,
  )
  .option('-d, --debug [feat]', `[string | boolean] show debug logs`)
  .option('-f, --filter <filter>', `[string] filter debug logs`)
  .option('-m, --mode <mode>', `[string] set env mode`)
# 指定配置文件路径
vite dev --config my.config.js

# 设置公共路径,默认 /
vite dev --base /my-app/

# 设置日志级别
vite dev --logLevel error` # 只输出错误

vite dev --clearScreen # 启用清屏
vite dev --no-clearScreen # 禁用清屏

# `bundle`(默认):使用 Rolldown 将配置文件打包后执行。
# `runner`(实验性):使用动态 `import()` 即时处理配置文件。
# `native`(实验性):使用原生 Node.js 模块加载(需配置文件为 ESM)。
vite dev --configLoader runner # 使用 Rolldown 将配置文件打包后执行

vite dev --debug               # 开启全部调试日志
vite dev --debug vite:hmr      # 仅显示 HMR 相关调试信息

# 指定运行模式(如 `development`、`production`、`staging`)。
# Vite 会加载对应的环境变量文件(例如 `.env.[mode]`),并影响 `import.meta.env.MODE` 的值。
vite dev --mode staging

vite dev 启动接收的命令行参数

在当前目录下启动 Vite 开发服务器。vite dev 和 vite serve 是 vite 的别名。

cli
  .command('[root]', 'start dev server') // default command
  .alias('serve') // the command is called 'serve' in Vite's API
  .alias('dev') // alias to align with the script name
  .option('--host [host]', `[string] specify hostname`, { type: [convertHost] })
  .option('--port <port>', `[number] specify port`)
  .option('--open [path]', `[boolean | string] open browser on startup`)
  .option('--cors', `[boolean] enable CORS`)
  .option('--strictPort', `[boolean] exit if specified port is already in use`)
  .option(
    '--force',
    `[boolean] force the optimizer to ignore the cache and re-bundle`,
  )
  .option(
    '--experimentalBundle',
    `[boolean] use experimental full bundle mode (this is highly experimental)`,
  )
# 指定项目的根目录。如果不提供,默认使用当前工作目录(`process.cwd()`)
vite dev ./my-project

vite dev --host               # 监听所有接口
vite dev --host localhost     # 仅监听本地

vite dev --port 3000

vite dev --open               # 打开 http://localhost:5173/
vite dev --open /admin        # 打开 http://localhost:5173/admin

# 强制依赖优化器忽略缓存,重新预构建所有依赖(`optimizeDeps`)
vite dev --force

# 启用实验性的“全量打包开发模式”(`bundledDev`)
vite dev --experimentalBundle

启用实验性的“全量打包开发模式”,文件会被打包。会减少大量请求。

image.png

命令行执行 vite 后做了什么?

  1. 创建 server 实例
  2. 启动监听端口
async (
  root: string,
  options: ServerOptions & ExperimentalDevOptions & GlobalCLIOptions,
) => {
  filterDuplicateOptions(options)
  // output structure is preserved even after bundling so require()
  // is ok here
  // 动态导入并创建开发服务器
  const { createServer } = await import('./server')
  try {
    const server = await createServer({
      root,
      base: options.base,
      mode: options.mode,
      configFile: options.config,
      configLoader: options.configLoader,
      logLevel: options.logLevel,
      clearScreen: options.clearScreen,
      server: cleanGlobalCLIOptions(options),
      forceOptimizeDeps: options.force,
      experimental: {
        bundledDev: options.experimentalBundle,
      },
    })

    // 校验服务器实例并启动
    if (!server.httpServer) {
      throw new Error('HTTP server not available')
    }

    // 启动 HTTP 服务器监听指定端口
    await server.listen()

    // 输出启动日志
    const info = server.config.logger.info

    const modeString =
    // 非 development 模式,输出环境模式
      options.mode && options.mode !== 'development'
        ? `  ${colors.bgGreen(` ${colors.bold(options.mode)} `)}`
        : ''

    // 启动耗时(计算从 Vite 启动到服务器就绪的时间)
    const viteStartTime = global.__vite_start_time ?? false
    const startupDurationString = viteStartTime
      ? colors.dim(
          `ready in ${colors.reset(
            colors.bold(Math.ceil(performance.now() - viteStartTime)),
          )} ms`,
        )
      : ''
    // 检查是否有已存在的日志输出(避免重复打印)
    const hasExistingLogs =
      process.stdout.bytesWritten > 0 || process.stderr.bytesWritten > 0

    // 输出核心启动日志(Vite 版本 + 模式 + 启动耗时)
    info(
      `\n  ${colors.green(
        `${colors.bold('VITE')} v${VERSION}`,
      )}${modeString}  ${startupDurationString}\n`,
      {
        clear: !hasExistingLogs,
      },
    )

    // 打印服务器访问地址(如 http://localhost:3000/)
    server.printUrls()
    const customShortcuts: CLIShortcut<typeof server>[] = []
    if (profileSession) {
      customShortcuts.push({
        key: 'p',
        description: 'start/stop the profiler',
        async action(server) {
          if (profileSession) {
            await stopProfiler(server.config.logger.info)
          } else {
            const inspector = await import('node:inspector').then(
              (r) => r.default,
            )
            await new Promise<void>((res) => {
              profileSession = new inspector.Session()
              profileSession.connect()
              profileSession.post('Profiler.enable', () => {
                profileSession!.post('Profiler.start', () => {
                  server.config.logger.info('Profiler started')
                  res()
                })
              })
            })
          }
        },
      })
    }
    // 绑定快捷键到服务器(print: true 表示打印快捷键说明)
    server.bindCLIShortcuts({ print: true, customShortcuts })
  } catch (e) {
    const logger = createLogger(options.logLevel)
    logger.error(
      colors.red(`error when starting dev server:\n${inspect(e)}`),
      {
        error: e,
      },
    )
    await stopProfiler(logger.info)
    process.exit(1)
  }
},

image.png

image.png

// 启动 HTTP 服务器监听指定端口
async listen(port?: number, isRestart?: boolean) {
  // 解析主机名
  const hostname = await resolveHostname(config.server.host)
  if (httpServer) {
    httpServer.prependListener('listening', () => {
      // 解析服务器监听的 URL 地址
      server.resolvedUrls = resolveServerUrls(
        httpServer,
        config.server,
        hostname,
        httpsOptions,
        config,
      )
    })
  }
  // 启动 HTTP 服务器
  await startServer(server, hostname, port)
  if (httpServer) {
    // 如果不是重启,配置了 open 选项打开浏览器
    if (!isRestart && config.server.open) server.openBrowser()
  }
  return server
},

createServer 函数做了什么工作?

  1. 参数解析与配置校验。
  2. 服务器基础设施创建(HTTP/WS/中间件/文件监听)。
  3. 多环境(environments)初始化。
  4. 服务器对象构建与向后兼容。
  5. 中间件栈构建。
  6. 文件变化监听与 HMR。
  7. 启动服务器逻辑。
  8. 返回 server 实例。

一、config 解析

  1. 加载配置文件:读取 vite.config.js / vite.config.ts(可通过 --config 指定其他文件)。如果文件是 TypeScript,Vite 会使用 esbuild 或 rolldown 动态编译。
  2. 合并命令行参数:命令行选项优先级高于配置文件。
  3. 应用默认值:补充未提供的选项(如 root 默认为 process.cwd()base 默认为 /)。
  4. 加载环境变量:根据 mode(默认 development)读取 .env 和 .env.[mode] 文件,注入 process.env 和 import.meta.env
  5. 加载插件:收集用户配置中的 plugins 数组,调用每个插件的 config 钩子(允许插件修改配置),最后调用 configResolved 钩子通知插件配置已解析完成。
  6. 生成 ResolvedConfig:输出完整的、只读的配置对象,包含 serverbuildoptimizeDepsenvironments 等字段。

image.png

image.png

二、服务器基础设施创建(HTTP/WS/中间件/文件监听)

  // 3、网络服务构建
  const middlewares = connect() as Connect.Server

  // middlewareMode 为 true 时,不解析 HTTP 服务器,以中间件模式创建;否则解析 HTTP 服务器
  const httpServer = middlewareMode
    ? null
    : await resolveHttpServer(middlewares, httpsOptions)

  // 创建 WebSocket 服务器
  const ws = createWebSocketServer(httpServer, config, httpsOptions)

新建 HTTP 服务

async function resolveHttpServer(
  app: Connect.Server,
  httpsOptions?: HttpsServerOptions,
): Promise<HttpServer> {
  // 如果没有 httpsOptions,创建 HTTP 服务器
  if (!httpsOptions) {
    // http 模块在 net 的基础上增加了 HTTP 协议解析和封装能力。
    // 当你创建一个 HTTP 服务器时,实际底层是一个 net.Server
    const { createServer } = await import('node:http')
    return createServer(app) // 创建 HTTP 服务器
  }

  // 如果有 httpsOptions,创建 HTTPS 服务器
  const { createSecureServer } = await import('node:http2')
  return createSecureServer(
    {
      // Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
      // errors on large numbers of requests
      maxSessionMemory: 1000, // 增加会话内存,防止 502 错误
      // Increase the stream reset rate limit to prevent net::ERR_HTTP2_PROTOCOL_ERROR
      // errors on large numbers of requests
      streamResetBurst: 100000, // 增加流重置突发量,防止 net::ERR_HTTP2_PROTOCOL_ERROR 错误
      streamResetRate: 33, // 增加流重置速率,防止 net::ERR_HTTP2_PROTOCOL_ERROR 错误
      ...httpsOptions, // 合并 httpsOptions 选项
      allowHTTP1: true, // 允许 HTTP/1 协议
    },
    // @ts-expect-error TODO: is this correct?
    app,
  )
}

三、 多环境(environments)初始化

Vite 8 引入了多环境(Environments)概念,每个环境(如 clientssr)拥有独立的模块图、插件容器和依赖优化器。

  const environments: Record<string, DevEnvironment> = {}

  // 多环境(Environments)初始化
  await Promise.all(
    Object.entries(config.environments).map(
      async ([name, environmentOptions]) => {
        const environment = await environmentOptions.dev.createEnvironment(
          name,
          config,
          {
            ws,
          },
        )
        environments[name] = environment

        const previousInstance =
          options.previousEnvironments?.[environment.name]
        await environment.init({ watcher, previousInstance })
      },
    ),
  )

四、环境向后兼容

在 Vite 8 引入多环境(environments)之前,Vite 只有一个全局的模块图。升级到 Vite 8 后,每个环境(clientssr)有了自己独立的模块图,但为了不破坏现有的插件和 API,Vite 需要提供一个兼容层,使得老代码依然可以通过 server.moduleGraph 访问模块图。

五、中间件栈构建

  1. 请求计时器(仅 DEBUG 模式)
  2. 拒绝无效请求(过滤包含空格等非法字符的请求)
  3. CORS 中间件(默认启用)
  4. 主机验证(防止 DNS 重绑定攻击)
  5. 用户插件 configureServer 钩子(允许插件注入自定义中间件)
  6. 缓存转换中间件(若未启用 bundledDev
  7. 代理中间件(将 /api 等请求转发到后端服务器)
  8. Base 路径中间件(处理 base 配置)
  9. 编辑器打开支持/__open-in-editor
  10. HMR Ping 处理(响应客户端心跳)
  11. public 目录静态服务(直接返回 public 下的文件)
  12. 转换中间件(核心) :拦截对 .js.vue.ts 等文件的请求,调用插件链进行转换,返回最终代码。
  13. 静态文件服务:返回项目根目录下未被转换的静态资源。
  14. HTML fallback(SPA 模式下,未匹配路径返回 index.html
  15. index.html 转换中间件:注入客户端脚本(/@vite/client)和环境变量。
  16. 404 处理
  17. 错误处理中间件

六、利用 chokidar,文件变化监听

  // 9、文件变更事件处理
  // 监听文件变化事件
  watcher.on('change', async (file) => {
    file = normalizePath(file)
    // 检查是否是 TypeScript 配置文件变化,如果是则重启服务器
    reloadOnTsconfigChange(server, file)

    await Promise.all(
      Object.values(server.environments).map((environment) =>
        // 通知所有环境的插件容器文件已更新
        environment.pluginContainer.watchChange(file, { event: 'update' }),
      ),
    )
    // invalidate module graph cache on file change
    for (const environment of Object.values(server.environments)) {
      environment.moduleGraph.onFileChange(file)
    }
    // 触发热模块替换更新,将变更同步到客户端
    await onHMRUpdate('update', file)
  })

  // 监听文件添加事件
  watcher.on('add', (file) => {
    onFileAddUnlink(file, false)
  })
  // 监听文件删除事件
  watcher.on('unlink', (file) => {
    onFileAddUnlink(file, true)
  })
修改 tsconfig.app.json

image.png

修改 tsconfig.json文件

会全量刷新,执行 location.reload()

4:13:20 PM [vite] changed tsconfig file detected: /Users/xxxx/Documents/code/cloudcode/vue3-vite-cube/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. (x2)

image.png

{
    "type": "custom",
    "event": "file-changed",
    "data": {
        "file": "/Users/xxx/Documents/code/cloudcode/vue3-vite-cube/tsconfig.json"
    }
}
修改 vue 页面的 script setup 块template 块
{
    "type": "custom",
    "event": "file-changed",
    "data": {
        "file": "/Users/xxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/home/index.vue"
    }
}
{
    "type": "update",
    "updates": [
        {
            "type": "js-update",
            "timestamp": 1775036864943,
            "path": "/src/pages/home/index.vue",
            "acceptedPath": "/src/pages/home/index.vue",
            "explicitImportRequired": false,
            "isWithinCircularImport": false
        }
    ]
}

image.png

修改 vue 页面的 style 块

image.png

{
    "type": "custom",
    "event": "file-changed",
    "data": {
        "file": "/Users/xxxxxx/Documents/code/cloudcode/vue3-vite-cube/src/pages/home/index.vue"
    }
}
{
    "type": "update",
    "updates": [
        {
            "type": "js-update",
            "timestamp": 1775037234261,
            "path": "/src/pages/home/index.vue",
            "acceptedPath": "/src/pages/home/index.vue",
            "explicitImportRequired": false,
            "isWithinCircularImport": false
        },
        {
            "type": "js-update",
            "timestamp": 1775037234261,
            "path": "/src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.css",
            "acceptedPath": "/src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.css",
            "explicitImportRequired": false, // 示是否需要显式动态导入新模块
            "isWithinCircularImport": false // 表示是否处于循环依赖中
        }
    ]
}

image.png

七、启动服务器逻辑

真正启动服务器在 cli 中 server.listen 执行。

这里只是重写 listen方法 ,待启动服务器时执行。

  • 调用 server.listen()
  • 监听配置的端口(默认 5173)
  • 启动完成后执行回调
  • 自动打开浏览器(如果配置 server.open
  • 终端打印
  let initingServer: Promise<void> | undefined
  let serverInited = false // 标记服务器是否已初始化

  if (!middlewareMode && httpServer) {
    // overwrite listen to init optimizer before server start
    const listen = httpServer.listen.bind(httpServer)
    // 重写 listen 方法,确保在服务器启动前初始化优化器
    httpServer.listen = (async (port: number, ...args: any[]) => {
      try {
        await initServer(true)
      } catch (e) {
        httpServer.emit('error', e)
        return
      }
      // 调用原始 listen 方法启动服务器
      return listen(port, ...args)
    }) as any
  } else {
    await initServer(false)
  }
  const initServer = async (onListen: boolean) => {
    if (serverInited) return // 如果服务器已初始化,直接返回
    if (initingServer) return initingServer // 如果服务器正在初始化,直接返回

    initingServer = (async function () {
      // 如果没有配置 bundledDev,则在初始化服务器时调用 buildStart 方法
      if (!config.experimental.bundledDev) {
        // For backward compatibility, we call buildStart for the client
        // environment when initing the server. For other environments
        // buildStart will be called when the first request is transformed
        await environments.client.pluginContainer.buildStart()
      }

      // ensure ws server started
      // 确保 WebSocket 服务器已启动
      if (onListen || options.listen) {
        await Promise.all(
          // 确保所有环境的服务器都启动
          Object.values(environments).map((e) => e.listen(server)),
        )
      }

      initingServer = undefined // 清空初始化 Promise
      serverInited = true // 标记服务器已初始化
    })()
    return initingServer
  }

热更新

Vite 的热更新(HMR)基于原生 ES 模块和 WebSocket 实现,能在文件修改后仅更新受影响的模块,无需刷新页面,从而保留应用状态。其原理可分为服务端和客户端两个阶段。

服务端:变化检测与消息推送

一、文件检测

Vite 使用 chokidar 库来监听文件系统的变化。在 _createServer 函数中,会创建一个文件监听器(watcher),监听范围包括:

  • 项目根目录(root
  • 配置文件依赖(config.configFileDependencies
  • 环境变量文件(.env 等)
  • public 目录
(chokidar.watch(
    // config file dependencies and env file might be outside of root
    [
      ...(config.experimental.bundledDev ? [] : [root]),
      ...config.configFileDependencies,
      ...getEnvFilesForMode(config.mode, config.envDir),
      // Watch the public directory explicitly because it might be outside
      // of the root directory.
      ...(publicDir && publicFiles ? [publicDir] : []),
    ],

    resolvedWatchOptions,
  ) as FSWatcher)

二、模块图与依赖分析

1、 模块图的数据结构

  • urlToModuleMap:根据 URL 查找模块节点。
  • fileToModulesMap:根据文件路径查找对应的模块节点(一个文件可能对应多个模块,如 ?import 和 ?url 查询)。
  • 每个模块节点(ModuleNode)记录了:
    • importers:依赖该模块的模块(即父模块)。
    • importedModules:该模块导入的子模块。

2、依赖分析

当文件发生变化时,handleHMRUpdate 会执行以下步骤:

  1. 根据文件路径找到对应的模块节点(moduleGraph.getModulesByFile(file))。
  2. 遍历这些模块节点,收集所有受影响的模块(包括自己以及所有 importers 链上的模块)。
  3. 通过模块图向上追溯,找到所有依赖该模块的模块,直到没有更多依赖者为止

三、重新编译与生成更新消息

对于每个受影响的模块,Vite 调用 environment.transformRequest(url) 重新进行转换。该函数会经过完整的插件链(resolveId → load → transform),生成新的模块代码和 source map,并更新模块图中的 transformResult 缓存。

编译过程中,Vite 会记录一个时间戳(timestamp),用于客户端绕过浏览器缓存。

四、Websocket 推送消息

Vite 开发服务器内置了一个 WebSocket 服务器,用于与客户端通信。当 update 消息生成后,Vite 会通过 WebSocket 将其推送给所有已连接的客户端。

客户端:接收消息并执行更新

一、客户端初始化与 Websocket 连接

1、注入客户端脚本

当浏览器请求 index.html 时,Vite 的 indexHtmlMiddleware 会调用 clientPlugin 的 transformIndexHtml 钩子,在 HTML 中自动注入 <script type="module" src="/@vite/client">,该脚本负责建立 WebSocket 连接,暴露 HMR API。

image.png

2、建立 WebSocket 连接

客户端脚本首先会创建一个 WebSocket 连接指向开发服务器(默认地址 ws://localhost:5173)。同时,它会监听 openmessagecloseerror 等事件。

连接成功后,服务端会发送 { type: 'connected' } 消息,客户端收到后标记为就绪状态。

3、暴露 import.meta.hot API

客户端在全局维护了几个 Map 结构,用于存储每个模块注册的 HMR 回调(acceptdispose 等)。同时,它定义了一个 createHotContext 函数,该函数返回一个包含 acceptdisposeinvalidate 等方法的对象。

二、接收消息与类型分发

客户端 WebSocket 的 message 事件处理函数负责解析 JSON 消息,并根据 type 字段分发到不同的处理逻辑。

客户端 WebSocket 收到消息后,根据 type 进行处理:

  • connected, 标记就绪,可发送预热请求。
  • update:遍历 updates 数组,对每个更新执行热替换。
  • full-reload:调用 location.reload() 刷新页面。
  • prune,
  • custom,自定义事件。
  • error:在页面上显示错误覆盖层。
  • ping,不做处理。
async function handleMessage(payload: HotPayload) {
  switch (payload.type) {
    // WebSocket 和服务器握手成功,打印日志。
    case 'connected':
      console.debug(`[vite] connected.`)
      break
    // JS/CSS 热更新
    case 'update':
      // 通知所有插件 / 监听:马上要热更新了
      // 用于在热更新前执行自定义逻辑,例如刷新页面
      await hmrClient.notifyListeners('vite:beforeUpdate', payload)
      if (hasDocument) {
        // if this is the first update and there's already an error overlay, it
        // means the page opened with existing server compile error and the whole
        // module script failed to load (since one of the nested imports is 500).
        // in this case a normal update won't work and a full reload is needed.
        // 首次更新容错 + 清理错误
        if (isFirstUpdate && hasErrorOverlay()) {
          // 如果页面一打开就报错(编译失败),第一次热更新直接全页刷新,确保能正常运行
          location.reload() // 刚打开页面就报错,直接刷新修复
          return
        } else {
          if (enableOverlay) {
            clearErrorOverlay() // 清空之前的报错
          }
          isFirstUpdate = false
        }
      }
      // 所有文件更新并行处理,速度极快
      await Promise.all(
        payload.updates.map(async (update): Promise<void> => {
          if (update.type === 'js-update') {
            return hmrClient.queueUpdate(update) // 交给核心引擎更新JS
          }

          // css-update
          // this is only sent when a css file referenced with <link> is updated
          const { path, timestamp } = update
          const searchUrl = cleanUrl(path)
          // can't use querySelector with `[href*=]` here since the link may be
          // using relative paths so we need to use link.href to grab the full
          // URL for the include check.
          // 找到页面对应的旧 <link> 标签
          // 页面 <link href="style.css"> 是相对路径
          // e.href 会返回 http://localhost:5173/src/style.css 完整 URL
          const el = Array.from(
            document.querySelectorAll<HTMLLinkElement>('link'),
          ).find(
            (e) =>
              !outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl),
          )

          if (!el) {
            return
          }

          // 拼接带时间戳的新 CSS 路径
          const newPath = `${base}${searchUrl.slice(1)}${
            searchUrl.includes('?') ? '&' : '?'
          }t=${timestamp}`

          // rather than swapping the href on the existing tag, we will
          // create a new link tag. Once the new stylesheet has loaded we
          // will remove the existing link tag. This removes a Flash Of
          // Unstyled Content that can occur when swapping out the tag href
          // directly, as the new stylesheet has not yet been loaded.
          return new Promise((resolve) => {
            // 克隆新 link 标签,不直接改旧 href
            const newLinkTag = el.cloneNode() as HTMLLinkElement
            newLinkTag.href = new URL(newPath, el.href).href
            const removeOldEl = () => {
              el.remove()
              console.debug(`[vite] css hot updated: ${searchUrl}`)
              resolve()
            }
            // 等新 CSS 加载完成后,再删除旧标签
            newLinkTag.addEventListener('load', removeOldEl)
            newLinkTag.addEventListener('error', removeOldEl)
            // 缓存新标签,避免重复删除
            outdatedLinkTags.add(el)
            // 插入新标签到旧标签后面
            el.after(newLinkTag)
          })
        }),
      )
      // 触发更新完成事件
      // 通知插件 / 框架:热更新完成
      await hmrClient.notifyListeners('vite:afterUpdate', payload)
      break
    //  处理 custom 自定义消息
    case 'custom': {
      await hmrClient.notifyListeners(payload.event, payload.data)

      if (payload.event === 'vite:ws:disconnect') {
        // dom环境,且页面未卸载
        if (hasDocument && !willUnload) {
          console.log(`[vite] server connection lost. Polling for restart...`)
          const socket = payload.data.webSocket as WebSocket
          const url = new URL(socket.url)
          url.search = '' // remove query string including `token`
          await waitForSuccessfulPing(url.href) // 轮询等待服务器重启
          location.reload() // 服务器回来后,自动刷新页面
        }
      }
      break
    }
    // 处理 full-reload 全页刷新
    case 'full-reload':
      await hmrClient.notifyListeners('vite:beforeFullReload', payload)
      if (hasDocument) {
        if (payload.path && payload.path.endsWith('.html')) {
          // if html file is edited, only reload the page if the browser is
          // currently on that page.
          const pagePath = decodeURI(location.pathname)
          const payloadPath = base + payload.path.slice(1)
          if (
            pagePath === payloadPath ||
            payload.path === '/index.html' ||
            (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)
          ) {
            pageReload()
          }
          return
        } else {
          pageReload()
        }
      }
      break
    //  处理 prune 清理模块
    case 'prune':
      await hmrClient.notifyListeners('vite:beforePrune', payload)
      await hmrClient.prunePaths(payload.paths)
      break
    // 显示红色错误遮罩
    case 'error': {
      await hmrClient.notifyListeners('vite:error', payload)
      if (hasDocument) {
        const err = payload.err
        if (enableOverlay) {
          createErrorOverlay(err)
        } else {
          console.error(
            `[vite] Internal Server Error\n${err.message}\n${err.stack}`,
          )
        }
      }
      break
    }
    // 处理 ping 消息,心跳检测,不处理任何逻辑
    case 'ping': // noop
      break
    // 处理默认情况
    default: {
      const check: never = payload
      return check
    }
  }
}

三、处理 update 消息(热更新)

  1. 请求新模块(带时间戳),每个 update 对象包含 pathacceptedPathtimestamp 等字段。客户端构造新的 UR,利用 ?t=timestamp 强制绕过浏览器缓存。使用动态 import() 获取模块的导出对象。
  2. 执行 dispose 回调(清理旧资源),在替换模块之前,需要先执行旧模块注册的 dispose 回调(如果有),以便清理定时器、事件监听等。
  3. 找到接受更新的模块,
  4. 针对css处理。如果 update.type === 'css-update',客户端不会通过 import() 请求,而是直接替换页面中的 <link> 或 <style> 标签。
  5. 失败回退(full-reload),如果更新过程中发生错误(例如网络请求失败、回调抛出异常),或者找不到任何接受回调,客户端会发送 full-reload 指令,刷新整个页面以确保应用状态正确。

image.png

image.png

客户端执行 js-update

importUpdatedModule 是 Vite HMR 的模块更新加载器:拼接带时间戳的最新 URL,动态加载新代码 ,循环依赖异常时自动刷新。

  // 普通 ESM 模式
  // 动态加载最新的模块代码 → 解决浏览器缓存 → 处理循环依赖错误
  async function importUpdatedModule({
    acceptedPath, // 要更新的模块路径
    timestamp, // 模块更新时间戳
    explicitImportRequired, // 是否显式导入
    isWithinCircularImport, // 是否在循环依赖里
  }) {
    // 拆分路径
    const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`)
    const importPromise = import(
      /* @vite-ignore */ // 告诉 vite 不解析这个动态导入,由浏览器负责加载
      base +
      // 移除前导斜杠,确保路径正确
        acceptedPathWithoutQuery.slice(1) +
        // timestamp 用于刷新浏览器缓存,确保加载最新代码
        `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${
          query ? `&${query}` : ''
        }`
    )
    if (isWithinCircularImport) {
      // 循环依赖, 热更失败 → 自动刷新页面
      importPromise.catch(() => {
        console.info(
          `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
            `To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
        )
        pageReload()
      })
    }
    // 返回模块
    return await importPromise
  }

importUpdatedModule 负责用原生 ESM 加载最新代码,通知 Rolldown 运行时更新模块导出,循环依赖异常自动刷新页面。

  // 打包开发模式(bundledDev)
  async function importUpdatedModule({
    url,
    acceptedPath,
    isWithinCircularImport, // 是否在循环依赖里
  }) {
    // 加载新代码,并通知 Rolldown 运行时更新模块
    // import(base + url!) 浏览器原生 ESM 动态导入
    // 浏览器发起网络请求 → 访问 Vite 开发服务器
    // url 已带时间戳 → 强制不缓存,加载最新版
    const importPromise = import(base + url!).then(() =>
      // @ts-expect-error globalThis.__rolldown_runtime__
      // 全局运行时.loadExports
      // __rolldown_runtime__:Rolldown 运行时(Vite 新一代底层打包 / 运行核心)
      // loadExports(acceptedPath)
        // → 告诉运行时:重新收集这个模块的最新导出
        // → 运行时会自动更新所有引用该模块的地方
      globalThis.__rolldown_runtime__.loadExports(acceptedPath),
    )
    // 循环依赖容错
    if (isWithinCircularImport) {
      // 热更失败 → 自动刷新页面
      importPromise.catch(() => {
        console.info(
          `[hmr] ${acceptedPath} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order. ` +
            `To debug and break the circular import, you can run \`vite --debug hmr\` to log the circular dependency path if a file change triggered it.`,
        )
        pageReload()
      })
    }
    return await importPromise
  }

更新文件

  /**
   * 处理 HMR 更新
   * @param type 文件操作类型
   * @param file 文件路径
   */
  const onHMRUpdate = async (
    type: 'create' | 'delete' | 'update',
    file: string,
  ) => {
    // 如果 HMR 已启用,则处理 HMR 更新
    if (serverConfig.hmr !== false) {
      await handleHMRUpdate(type, file, server)
    }
  }

新增文件/删除文件


  /**
   * 处理文件添加或删除
   * @param file 文件路径
   * @param isUnlink 是否删除文件
   */
  const onFileAddUnlink = async (file: string, isUnlink: boolean) => {
    file = normalizePath(file)
    // 「检测文件是否为 tsconfig.json/jsconfig.json,若是则触发服务器重启」
    // 因为这类配置文件变更会影响模块解析规则,必须重启才能生效。
    reloadOnTsconfigChange(server, file)

    await Promise.all(
      // 通知所有环境的插件容器,同步文件变更事件
      Object.values(server.environments).map((environment) =>
        // 对每个环境,调用其插件容器的 watchChange 方法
        // 传递文件路径和事件类型('delete' 或 'create')
        environment.pluginContainer.watchChange(file, {
          event: isUnlink ? 'delete' : 'create',
        }),
      ),
    )

    if (publicDir && publicFiles) {
      if (file.startsWith(publicDir)) {
        const path = file.slice(publicDir.length)
        publicFiles[isUnlink ? 'delete' : 'add'](path)

        // 新增文件时:清理同名模块的 ETag 缓存,保证公共文件优先响应
        // Vite 会为模块生成 ETag(实体标签),用于「ETag 快速路径」—— 客户端请求时,若 ETag 未变,直接返回缓存的模块内容
        if (!isUnlink) {
          // 获取客户端环境的模块图实例
          const clientModuleGraph = server.environments.client.moduleGraph
          // 根据路径 path(如 /image.png)查找模块图中是否存在同名模块
          const moduleWithSamePath =
            await clientModuleGraph.getModuleByUrl(path)

          const etag = moduleWithSamePath?.transformResult?.etag

          // 如果有etag ,则删除。
          // 保证 public 下文件等优先级
          if (etag) {
            // The public file should win on the next request over a module with the
            // same path. Prevent the transform etag fast path from serving the module
            clientModuleGraph.etagToModuleMap.delete(etag)
          }
        }
      }
    }
    // 文件删除时,清理模块依赖图缓存
    if (isUnlink) {
      // invalidate module graph cache on file change
      for (const environment of Object.values(server.environments)) {
        environment.moduleGraph.onFileDelete(file)
      }
    }
    // 触发 HMR 更新,同步变更到客户端
    await onHMRUpdate(isUnlink ? 'delete' : 'create', file)
  }

禁止热更新

  server: {
    ws: false,
  }

修改文件浏览器内容不会自动更新。

image.png

image.png

image.png

重启服务器

什么场景会触发开发服务器重启?

  1. 修改 vite.config.js 配置文件。
  2. 依赖文件修改,如 package.json
  3. 创建/修改 .env 环境文件。
  4. 插件中调用 server.restart

image.png

  // 配置文件、配置文件依赖、环境文件变化时,自动重启服务器
  if (isConfig || isConfigDependency || isEnv) {
    // auto restart server
    debugHmr?.(`[config change] ${colors.dim(shortFile)}`)

    // 打印日志
    config.logger.info(
      colors.green(
        `${normalizePath(
          path.relative(process.cwd(), file),
        )} changed, restarting server...`,
      ),
      { clear: true, timestamp: true },
    )
    try {
      // 重启服务器
      await restartServerWithUrls(server)
    } catch (e) {
      config.logger.error(colors.red(e))
    }
    return
  }

server.restart

重启服务器前,会先关闭服务器(包含 停止HTTP服务,停止Websocket 服务,关闭文件监听,关闭所有环境的 DevEnvironment 实例,释放模块图、插件容器、依赖优化器等资源)。

// 重启 Vite 开发服务器,同时处理并发重启请求,确保同一时间只有一个重启操作在执行。
async restart(forceOptimize?: boolean) {
  // 如果没有重启 Promise,创建一个
  if (!server._restartPromise) {
    // 设置是否强制优化依赖
    server._forceOptimizeOnRestart = !!forceOptimize
    // 重启服务器
    server._restartPromise = restartServer(server).finally(() => {
      // 重启完成后,重置重启 Promise 和强制优化依赖
      server._restartPromise = null
      server._forceOptimizeOnRestart = false
    })
  }
  // 如果存在,说明已经有一个重启操作在进行中,直接返回该 Promise
  return server._restartPromise
},

全量更新

什么场景会触发全量刷新?

  1. 修改index.html文件。
  2. 修改main.ts文件。
  3. 修改路由配置 router/index.ts 文件。

image.png

image.png

{
    "type": "full-reload",
    "triggeredBy": "/Users/xxxxxx/Documents/code/cloudcode/vue3-vite-cube/src/common/utils.ts",
    "path": "*"
}
  // (dev only) the client itself cannot be hot updated.
  // Vite 客户端自身文件变更 → 不能热更 → 必须整页刷新
  if (file.startsWith(withTrailingSlash(normalizedClientDir))) {
    environments.forEach(({ hot }) =>
      hot.send({
        type: 'full-reload',
        path: '*',
        triggeredBy: path.resolve(config.root, file),
      }),
    )
    return
  }

最后

  1. Websoket
  2. vite server 配置项
❌
❌