普通视图

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

Vite 开发服务器启动时,如何将 client 注入 HTML?

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

当执行 vite 命令启动开发服务器,并在浏览器中打开 http://localhost:5173 时,页面会神奇地具备热模块替换(HMR)能力。这一切的起点,就是 Vite 在返回的 HTML 中悄悄注入了一个特殊的脚本:/@vite/client。这个脚本负责建立 WebSocket 连接、监听文件变化并触发模块热更新。

整体流程概览

Vite 将 client 注入 HTML 的过程可以概括为以下几个步骤:

  1. 服务器启动:创建 Vite 开发服务器,初始化插件容器。
  2. 注册插件:内置插件被激活。
  3. 请求拦截:浏览器请求 index.html,Vite 的 HTML 中间件接管。
  4. HTML 转换:调用所有插件的 transformIndexHtml 钩子。
  5. 注入标签clientInjectionsPlugin 在 transformIndexHtml 中返回需要注入的 script 标签。
  6. 模块解析:浏览器解析 HTML 后请求 /@vite/client,经过 resolveId 和 load 钩子返回实际代码。
  7. 代码转换:client 源码中的占位符被替换为当前服务器的实际配置(如 HMR 端口、base 路径等)。
  8. 客户端执行:浏览器执行 client 代码,建立 WebSocket 连接,HMR 就绪。

启动服务器到 client 在浏览器中运行的全过程

image.png

clientInjectionsPlugin

负责在客户端代码中注入配置值和环境变量,确保客户端代码能够正确访问 Vite 配置和环境信息,特别是热模块替换 (HMR) 相关的配置。

客户端核心入口:处理 /@vite/client 和 /@vite/env 文件,先注入配置值,再替换 define 变量。

image.png

buildStart钩子

image.png

image.png

vite/packages/vite/src/node/plugins/clientInjections.ts

function clientInjectionsPlugin(config: ResolvedConfig): Plugin {
  // 存储配置值替换函数,在 buildStart 钩子中初始化
  let injectConfigValues: (code: string) => string

  // 返回一个函数,每个构建环境(如 client 和 ssr)分别创建 define 替换函数
  const getDefineReplacer = perEnvironmentState((environment) => {

    const userDefine: Record<string, any> = {}

    for (const key in environment.config.define) {
      // import.meta.env.* is handled in `importAnalysis` plugin
      // 过滤掉 import.meta.env.* 前缀的变量(这些由 importAnalysis 插件处理
      if (!key.startsWith('import.meta.env.')) {
        userDefine[key] = environment.config.define[key]
      }
    }
    const serializedDefines = serializeDefine(userDefine)
    const definesReplacement = () => serializedDefines
    return (code: string) => code.replace(`__DEFINES__`, definesReplacement)
  })

  return {
    name: 'vite:client-inject',
    // 初始化插件,在 buildStart 钩子中创建配置值替换函数
    async buildStart() {

      // 生成一个函数
      // 用于接收客户端源码字符串,将其中的占位符(如 __BASE__、__HMR_PORT__、__MODE__ 等)替换为实际的值
      injectConfigValues = await createClientConfigValueReplacer(config)
    },
    // 转换客户端代码,注入配置值和环境变量
    async transform(code, id) {
      const ssr = this.environment.config.consumer === 'server'
      const cleanId = cleanUrl(id)

      // 客户端核心入口:/@vite/client 和 /@vite/env
      if (cleanId === normalizedClientEntry || cleanId === normalizedEnvEntry) {
        const defineReplacer = getDefineReplacer(this)
        return defineReplacer(injectConfigValues(code))

        // 其他文件中的 process.env.NODE_ENV 替换
      } else if (!ssr && code.includes('process.env.NODE_ENV')) {
        // replace process.env.NODE_ENV instead of defining a global
        // for it to avoid shimming a `process` object during dev,
        // avoiding inconsistencies between dev and build
        const nodeEnv =
        // 优先使用用户定义的值
          this.environment.config.define?.['process.env.NODE_ENV'] ||
          // 回退到系统环境变量
          // 最终回退到 Vite 模式
          JSON.stringify(process.env.NODE_ENV || config.mode)

        return await replaceDefine(this.environment, code, id, {
          'process.env.NODE_ENV': nodeEnv,
          'global.process.env.NODE_ENV': nodeEnv,
          'globalThis.process.env.NODE_ENV': nodeEnv,
        })
      }
    },
  }
}
function perEnvironmentState<State>(
  initial: (environment: Environment) => State,
): (context: PluginContext) => State {

  const stateMap = new WeakMap<Environment, State>()

  return function (context: PluginContext) {
    const { environment } = context
    // 尝试从 stateMap 中获取当前环境的状
    let state = stateMap.get(environment)
    if (!state) {
      // 调用 initial 函数初始化状态,并将其存储到 stateMap 中
      state = initial(environment)
      stateMap.set(environment, state)
    }
    return state
  }
}

indexHtmlMiddleware 中间件

当浏览器请求 index.html 时,Vite 开发服务器会通过中间件(packages/vite/src/node/server/middlewares/html.ts)处理。

  1. 请求拦截与过滤
  2. 完整打包模式(Full Bundle Dev Environment) 或 普通文件系统模式
  3. 通过 send 发送返回 HTML。

image.png

image.png

image.png

image.png

image.png

全量环境

  1. 文档类请求的 SPA 回退:若请求头的 sec-fetch-dest 为 documentiframe 等类型,并且满足以下任一条件:
    (1)当前 bundle 已过时,会重新生成 bundle);
    (2)或者文件原本不存在(file === undefined);则调用 generateFallbackHtml(server) 生成一个默认的 index.html 作为文件内容。

  2. 最终将文件内容(字符串或 Buffer)通过 send 返回,并携带 etag 用于缓存。

  if (fullBundleEnv) {
    const pathname = decodeURIComponent(url)
    // 打包根目录的文件路径 index.html
    const filePath = pathname.slice(1) // remove first /

    let file = fullBundleEnv.memoryFiles.get(filePath)
    if (!file && fullBundleEnv.memoryFiles.size !== 0) {
      return next()
    }
    const secFetchDest = req.headers['sec-fetch-dest']
    // 处理文档类请求(SPA 回退)
    if (
      [
        'document',
        'iframe',
        'frame',
        'fencedframe',
        '',
        undefined,
      ].includes(secFetchDest) &&
      // 检查当前 bundle 是否过期
      ((await fullBundleEnv.triggerBundleRegenerationIfStale()) ||
        file === undefined)
    ) {
      // 生成一个 fallback HTML 作为文件内容
      // 生成一个默认的 HTML 入口
      file = { source: await generateFallbackHtml(server as ViteDevServer) }
    }
    if (!file) {
      return next()
    }

    const html =
      typeof file.source === 'string'
        ? file.source
        : Buffer.from(file.source)
    const headers = isDev
      ? server.config.server.headers
      : server.config.preview.headers
    return send(req, res, html, 'html', { headers, etag: file.etag })
  }

image.png

image.png

发送

image.png

async function getHmrImplementation(
  config: ResolvedConfig,
): Promise<string> {

  // 读取client脚本文件
  const content = fs.readFileSync(normalizedClientEntry, 'utf-8')
  const replacer = await createClientConfigValueReplacer(config)
  return (
    replacer(content)
      // the rolldown runtime cannot import a module
      .replace(/import\s*['"]@vite\/env['"]/, '')
  )
}

image.png

image.png

 async function importUpdatedModule({
    url, // 补丁文件的 URL,例如 "/hmr_patch_0.js"
    acceptedPath, // 需要热更新的模块路径
    isWithinCircularImport,
  }) {
    const importPromise = import(base + url!).then(() =>
      // 从 Rolldown 运行时中提取模块的导出
      // @ts-expect-error globalThis.__rolldown_runtime__
      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
}

【示例】修改文件,client 的 websocket 收到更新

{
    "type": "update",
    "updates": [
        {
            "type": "js-update",
            "url": "hmr_patch_0.js",
            "path": "src/pages/home/index.vue",
            "acceptedPath": "src/pages/home/index.vue",
            "timestamp": 1775992132341
        }
    ]
}

image.png

image.png

【示例】修改样式变量文件

浏览器客户端收到websocket消息,会加载补丁文件。

{
    "type": "update",
    "updates": [
        {
            "type": "js-update",
            "url": "hmr_patch_3.js",
            "path": "src/pages/home/index@logs.vue?vue&type=style&index=0&scoped=9217440f&lang.less",
            "acceptedPath": "src/pages/home/index@logs.vue?vue&type=style&index=0&scoped=9217440f&lang.less",
            "timestamp": 1775994912031
        },
        {
            "type": "js-update",
            "url": "hmr_patch_3.js",
            "path": "src/pages/home.vue?vue&type=style&index=0&scoped=f940dfa7&lang.less",
            "acceptedPath": "src/pages/home.vue?vue&type=style&index=0&scoped=f940dfa7&lang.less",
            "timestamp": 1775994912031
        },
        {
            "type": "js-update",
            "url": "hmr_patch_3.js",
            "path": "src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.less",
            "acceptedPath": "src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.less",
            "timestamp": 1775994912031
        }
    ]
}

浏览器端成功加载一个模块(包括动态 import())后,客户端会主动发送 vite:module-loaded 事件。

{
    "type": "custom",
    "event": "vite:module-loaded",
    "data": {
        "modules": [
            "src/pages/home/index@logs.vue?vue&type=style&index=0&scoped=9217440f&lang.less",
            "src/pages/home.vue?vue&type=style&index=0&scoped=f940dfa7&lang.less",
            "src/pages/home/index.vue?vue&type=style&index=0&scoped=2c5296db&lang.less"
        ]
    }
}

服务器接收到该事件,提取出本次加载的模块列表(payload.modules),并将其注册到开发引擎(devEngine)中,关联到当前客户端 ID。

this.hot.on('vite:client:disconnect', (_payload, client) => {
  const clientId = this.clients.delete(client)
  if (clientId) {
    this.devEngine.removeClient(clientId)
  }
})

普通文件模式

  1. 解析请求的文件路径
  2. 开发模式下的访问权限检查
  3. 读取 HTML 文件并进行转换,最终通过 send 返回 HTML。
  // 根据请求 URL 确定 HTML 文件的实际文件系统路径
  let filePath: string

  // 如果是开发服务器且 URL 以 FS_PREFIX 开头(表示直接访问文件系统路径)
  if (isDev && url.startsWith(FS_PREFIX)) {
    filePath = decodeURIComponent(fsPathFromId(url))
  } else {
    // 将 URL 与服务器根目录连接,解析为绝对路径
    filePath = normalizePath(
      path.resolve(path.join(root, decodeURIComponent(url))),
    )
  }

  if (isDev) {
    const servingAccessResult = checkLoadingAccess(server.config, filePath)
    // 如果路径被拒绝访问,返回 403 错误
    if (servingAccessResult === 'denied') {
      return respondWithAccessDenied(filePath, server, res)
    }
    // 
    if (servingAccessResult === 'fallback') {
      return next()
    }
    // 确保路径被允许访问
    servingAccessResult satisfies 'allowed'
  } else {
    // `server.fs` options does not apply to the preview server.
    // But we should disallow serving files outside the output directory.
    if (!isParentDirectory(root, filePath)) {
      return next()
    }
  }

  if (fs.existsSync(filePath)) {
    const headers = isDev
      ? server.config.server.headers
      : server.config.preview.headers

    try {
      // 读取 HTML 文件内容
      let html = await fsp.readFile(filePath, 'utf-8')
      if (isDev) {
        // 开发环境下,对 HTML 进行转换
        html = await server.transformIndexHtml(url, html, req.originalUrl)
      }
      // 发送 HTML 内容
      // 这里使用 send() 方法,而不是 res.end(),因为它会自动处理响应头和编码
      return send(req, res, html, 'html', { headers })
    } catch (e) {
      return next(e)
    }
  }

image.png

执行中间件

image.png

image.png

读取html文件内容

image.png

server.transformIndexHtml 其实就是执行 applyHtmlTransforms,之前中间件已经处理 createDevHtmlTransformFn

createDevHtmlTransformFn

  1. plugin.transformIndexHtml 获取具有 transformIndexHtml 钩子的插件,排序。
  2. 构建转换钩子管道,在 applyHtmlTransforms 中按顺序执行。
  3. applyHtmlTransforms 根据插件钩子,生成相关 tag 注入 html 中。

vite/packages/vite/src/node/server/middlewares/indexHtml.ts

function createDevHtmlTransformFn(
  config: ResolvedConfig,
): (
  server: ViteDevServer,
  url: string,
  html: string,
  originalUrl?: string,
) => Promise<string> {

  // 从配置的插件中解析出 HTML 转换钩子
  const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(
    config.plugins,
  )

  // 构建转换钩子管道
  const transformHooks = [
    preImportMapHook(config), // 处理导入映射的前置钩子
    injectCspNonceMetaTagHook(config), // 注入 CSP nonce 元标签
    ...preHooks,
    htmlEnvHook(config), // 注入环境变量到 HTML 中
    devHtmlHook, // 开发环境特定的 HTML 转换
    ...normalHooks,
    ...postHooks,
    injectNonceAttributeTagHook(config), // 注入 nonce 属性到标签中
    postImportMapHook(), // 处理导入映射的后置钩子
  ]

  // 创建插件上下文
  const pluginContext = new BasicMinimalPluginContext(
    { ...basePluginContextMeta, watchMode: true },
    config.logger,
  )
  return (
    server: ViteDevServer,
    url: string,
    html: string,
    originalUrl?: string,
  ): Promise<string> => {

    // 将所有转换钩子应用到 HTML 内容上
    return applyHtmlTransforms(html, transformHooks, pluginContext, {
      path: url,
      filename: getHtmlFilename(url, server),
      server,
      originalUrl,
    })
  }
}

traverseHtml

server.transformIndexHtml 对html进行转换 ——> createDevHtmlTransformFn confige 解析阶段收集了插件transformIndexHtml钩子 ——〉applyHtmlTransforms 执行上述收集的hook.handler——> injectToHead 将标签插入头部

收集所有插件的 transformIndexHtml 钩子返回的修改(可能是 HTML 字符串的替换,或者要插入的标签数组),然后将其应用到原始 HTML 上。

  const transformHooks = [
    preImportMapHook(config), // 处理导入映射的前置钩子
    injectCspNonceMetaTagHook(config), // 注入 CSP nonce 元标签
    ...preHooks,
    htmlEnvHook(config), // 注入环境变量到 HTML 中
    devHtmlHook, // 开发环境特定的 HTML 转换
    ...normalHooks,
    ...postHooks,
    injectNonceAttributeTagHook(config), // 注入 nonce 属性到标签中
    postImportMapHook(), // 处理导入映射的后置钩子
  ]

【示例】执行devHtmlTransformFn

image.png

【示例】执行 htmlEnvHook

image.png

【示例】 devHtmlHook

image.png

image.png

image.png

处理 html 节点

image.png

处理head节点

image.png

处理 meta 节点

image.png

处理 link 节点

image.png

applyHtmlTransforms

image.png

injectToHead

image.png

vite/packages/vite/src/node/plugins/html.ts

async function applyHtmlTransforms(
  html: string,
  hooks: IndexHtmlTransformHook[],
  pluginContext: MinimalPluginContextWithoutEnvironment,
  ctx: IndexHtmlTransformContext,
): Promise<string> {
  for (const hook of hooks) {
    const res = await hook.call(pluginContext, html, ctx)
    if (!res) {
      continue
    }
    if (typeof res === 'string') {
      html = res
    } else {
      let tags: HtmlTagDescriptor[]
      if (Array.isArray(res)) {
        tags = res
      } else {
        html = res.html || html
        tags = res.tags
      }

      let headTags: HtmlTagDescriptor[] | undefined
      let headPrependTags: HtmlTagDescriptor[] | undefined
      let bodyTags: HtmlTagDescriptor[] | undefined
      let bodyPrependTags: HtmlTagDescriptor[] | undefined

      for (const tag of tags) {
        switch (tag.injectTo) {
          case 'body':
            ;(bodyTags ??= []).push(tag)
            break
          case 'body-prepend':
            ;(bodyPrependTags ??= []).push(tag)
            break
          case 'head':
            ;(headTags ??= []).push(tag)
            break
          default:
            ;(headPrependTags ??= []).push(tag)
        }
      }
      headTagInsertCheck([...(headTags || []), ...(headPrependTags || [])], ctx)
      if (headPrependTags) html = injectToHead(html, headPrependTags, true)
      if (headTags) html = injectToHead(html, headTags)
      if (bodyPrependTags) html = injectToBody(html, bodyPrependTags, true)
      if (bodyTags) html = injectToBody(html, bodyTags)
    }
  }

  return html
}

vite 插件 @vitejs/plugin-vue

作者 米丘
2026年4月16日 11:53

@vitejs/plugin-vue 是 Vite 官方提供的 Vue 3 单文件组件(SFC)支持插件,它负责将 .vue 文件转换为浏览器可执行的 JavaScript 模块。

Vue3项目使用 @vitejs/plugin-vue插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue({
      
    }),
    // vueJsx(),
    // vueDevTools(),
  ],
})

参数选项有哪些?

interface Options {
  // 指定哪些文件需要被插件转换
  include?: string | RegExp | (string | RegExp)[]
  // 指定哪些文件不需要被插件转换
  exclude?: string | RegExp | (string | RegExp)[]

  /**
   * In Vite, this option follows Vite's config.
   */
  isProduction?: boolean

  // options to pass on to vue/compiler-sfc
  // 传递给 @vue/compiler-sfc 中 compileScript 的选项
  script?: Partial<
    Omit<
      SFCScriptCompileOptions,
      | 'id'
      | 'isProd'
      | 'inlineTemplate'
      | 'templateOptions'
      | 'sourceMap'
      | 'genDefaultAs'
      | 'customElement'
      | 'defineModel'
      | 'propsDestructure'
    >
  > & {
    /**
     * @deprecated defineModel is now a stable feature and always enabled if
     * using Vue 3.4 or above.
     */
    defineModel?: boolean
    /**
     * @deprecated moved to `features.propsDestructure`.
     */
    propsDestructure?: boolean
  }

  // 传递给 @vue/compiler-sfc 中 compileTemplate 的选项
  template?: Partial<
    Omit<
      SFCTemplateCompileOptions,
      | 'id'
      | 'source'
      | 'ast'
      | 'filename'
      | 'scoped'
      | 'slotted'
      | 'isProd'
      | 'inMap'
      | 'ssr'
      | 'ssrCssVars'
      | 'preprocessLang'
    >
  >
  // 传递给 @vue/compiler-sfc 中 compileStyle 的选项
  style?: Partial<
    Omit<
      SFCStyleCompileOptions,
      | 'filename'
      | 'id'
      | 'isProd'
      | 'source'
      | 'scoped'
      | 'cssDevSourcemap'
      | 'postcssOptions'
      | 'map'
      | 'postcssPlugins'
      | 'preprocessCustomRequire'
      | 'preprocessLang'
      | 'preprocessOptions'
    >
  >

  /**
   * Use custom compiler-sfc instance. Can be used to force a specific version.
   */
  compiler?: typeof _compiler

  /**
   * Requires @vitejs/plugin-vue@^5.1.0
   */
  features?: {
    /**
     * Enable reactive destructure for `defineProps`.
     * - Available in Vue 3.4 and later.
     * - **default:** `false` in Vue 3.4 (**experimental**), `true` in Vue 3.5+
     * 启动后,`defineProps` 中的响应式解构将支持 Vue 3.4 中的语法。
     */
    propsDestructure?: boolean
    /**
     * Transform Vue SFCs into custom elements.
     * - `true`: all `*.vue` imports are converted into custom elements
     * - `string | RegExp`: matched files are converted into custom elements
     * - **default:** /\.ce\.vue$/
     * 启动后,所有匹配的文件都将被转换为自定义元素。
     */
    customElement?: boolean | string | RegExp | (string | RegExp)[]
    /**
     * Set to `false` to disable Options API support and allow related code in
     * Vue core to be dropped via dead-code elimination in production builds,
     * resulting in smaller bundles.
     * - **default:** `true`
     * 启动后,Vue 核心代码中的 Options API 将被移除,从而减小 bundle 大小。
     */
    optionsAPI?: boolean
    /**
     * Set to `true` to enable devtools support in production builds.
     * Results in slightly larger bundles.
     * - **default:** `false`
     * 启动后,生产环境下的 devtools 将被启用,从而增加 bundle 大小。
     */
    prodDevtools?: boolean
    /**
     * Set to `true` to enable detailed information for hydration mismatch
     * errors in production builds. Results in slightly larger bundles.
     * - **default:** `false`
     * 启动后,生产环境下的 hydration mismatch 错误将包含详细的调试信息,从而增加 bundle 大小。
     */
    prodHydrationMismatchDetails?: boolean
    /**
     * Customize the component ID generation strategy.
     * - `'filepath'`: hash the file path (relative to the project root)
     * - `'filepath-source'`: hash the file path and the source code
     * - `function`: custom function that takes the file path, source code,
     *   whether in production mode, and the default hash function as arguments
     * - **default:** `'filepath'` in development, `'filepath-source'` in production
     * 启动后,组件 ID 将根据文件路径和源代码进行哈希处理,从而增加 bundle 大小。
     */
    componentIdGenerator?:
      | 'filepath'
      | 'filepath-source'
      | ((
          filepath: string,
          source: string,
          isProduction: boolean | undefined,
          getHash: (text: string) => string,
        ) => string)
  }

  /**
   * @deprecated moved to `features.customElement`.
   * 已废弃,移至 feature中
   */
  customElement?: boolean | string | RegExp | (string | RegExp)[]
}

SFC 编译流程

当 Vite 遇到一个 .vue 文件时,插件会执行以下编译流程:

  1. 解析 SFC:使用 @vue/compiler-sfc 将 .vue 文件解析为 descriptor 对象,其中包含 templatescriptstyle 等部分的解析结果
  2. 脚本编译:处理 <script> 块,包括 <script setup> 语法糖和 TypeScript 支持
  3. 模板编译:将 <template> 块编译为 render 函数
  4. 样式处理:处理 <style> 块,包括 CSS 预处理器的支持

生命周期

@vitejs/plugin-vue 中钩子执行顺序遵循 Vite 插件生命周期,分为服务器启动/构建准备阶段模块请求处理阶段

配置阶段(一次)

  • config:最早执行,用于修改 Vite 配置。
  • configResolved:配置解析完成后调用,可获取最终配置。
  • options:Rollup 选项钩子,在构建开始前修改输入选项(较少用)。

服务器启动 / 构建开始

  • 开发模式configureServer 在开发服务器创建时调用,用于添加中间件。
  • 生产构建buildStart 在构建开始时调用。

模块请求处理(每次请求/每个文件)

  • resolveId:解析模块 ID(将路径转换为绝对路径或虚拟 ID)。
  • load:加载模块内容(读取文件或生成源码)。
  • shouldTransformCachedModule(Rollup 钩子):决定是否使用缓存转换结果(在 load 后、transform 前调用,仅构建时)。
  • transform:转换模块内容(核心编译逻辑,例如将 .vue 文件转为 JS)。

image.png

transform 钩子

transform 钩子是整个插件的核心编译入口,它的职责是拦截 .vue 文件或相关子模块的请求,根据请求参数的不同,调用相应的编译函数,将 SFC 转换为浏览器可执行的 JavaScript 代码。

handler 接收的参数

  • code vue 文件源码
  • id 文件在系统的绝对路径
  • opt 配置项

image.png

image.png

 主请求(!query.vue

这是浏览器或构建工具直接请求 .vue 文件(例如 import App from './App.vue')。

transformMain 是 @vitejs/plugin-vue 中处理 .vue 文件主请求的核心编译函数。它负责将整个单文件组件(SFC)转换为可在浏览器或服务端运行的 JavaScript 模块。

  1. 解析与校验:创建 SFC 描述符,检查编译错误。
  2. 分块编译:分别生成脚本、模板、样式、自定义块的代码。
  3. 组装导出:合并各部分代码,生成最终组件对象。
  4. HMR 与 SSR 增强:注入热更新逻辑或服务端模块注册。
  5. Source Map 合并:如果存在模板,将模板的 source map 偏移后合并到脚本 map。
  6. TypeScript 转译:对最终代码进行 TS 转译(优先使用 Oxc,降级为 esbuild)。
创建描述符信息 compiler.parse

createDescriptor

image.png

getDescriptor 获取描述符。有缓存则从缓存取,否则创建。

image.png

image.png

描述符id生成策略

描述符id生成策略(依据 features?.componentIdGenerator 配置)

  • filepath 文件路径
  • filepath-source 文件路径 +源码
  • function 自定义实现
  • 默认策略(生产环境:文件路径+源码;非生产环境:文件路径)

image.png

import crypto from 'node:crypto'

function getHash(text: string): string {
  // 计算哈希值,采用 sha256 哈希算法对输入文本进行计算
  // 将哈希结果转换为十六进制 (hex) 格式
  // 取前8位,用于组件ID的唯一性
  return crypto.hash('sha256', text, 'hex').substring(0, 8)
}
生成脚本代码

genScriptCode 通过 resolveScript(@vue/compiler-dom ) 获取脚本,然后根据 <script> 块的存在性、内容来源(内联或外部)以及 Vue 编译器版本,产出最终用于构建组件对象的脚本部分。

参数信息

image.png

脚本代码 resolve

image.png

image.png

resolved.content

image.png

image.png

resolevd.map

image.png

生成 template 代码

针对 内联模板(无预处理器且无外部 src)

genTemplateCode 调用 transformTemplateInMain 函数,该函数会:

  1. 使用 @vue/compiler-dom 将模板内容编译为 render 函数。
  2. 处理 scoped 样式、指令转换等。
  3. 返回包含 render 函数声明的代码(例如 const _sfc_render = () => {...})。
  4. 直接内联到主模块中,避免额外的网络请求,提升开发环境性能。

image.png

transformTemplateInMain(template.content, descriptor, options, pluginContext, ssr, customElement)

image.png

result.code

image.png

result.ast

image.png

// 重命名模板编译后的渲染函数
// $1 引用第一个捕获组,即 function 或 const
// $2 引用第二个捕获组,即 render 或 ssrRender
result.code.replace(
      /\nexport (function|const) (render|ssrRender)/,
      '\n$1 _sfc_$2',
    )

image.png

image.png

生成style 代码

genStyleCode 是 @vitejs/plugin-vue 中专门处理 Vue 单文件组件(SFC)样式块的核心函数。它的主要职责是:为每个 <style> 块生成相应的导入语句,并处理 CSS Modules 和自定义元素模式下的样式收集

生成 自定义块 代码

genCustomBlockCode 用于生成 Vue 单文件组件 (SFC) 中自定义块的处理代码,它会为每个自定义块生成导入语句和执行代码,确保自定义块能够被正确处理和集成到组件中。

添加热更新相关代码

image.png

image.png

import.meta.hot.on("file-changed", ({ file }) => {
__VUE_HMR_RUNTIME__.CHANGED_FILE = file;
});

Vue HMR(热模块替换)运行时的一部分,用于监听 Vite 开发服务器的 file-changed 事件,并记录被修改的文件路径。

// Vite 提供的 HMR API,用于接受模块自身的更新。当该模块(即 .vue 文件)被修改时,回调函数会收到新的模块内容 mod。
import.meta.hot.accept((mod) => {
if (!mod) return;
const { default: updated, _rerender_only } = mod;
if (_rerender_only) {
  __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render);
} else {
  __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated);
}
});
  • updated:更新后的组件默认导出(即组件对象)。
  • _rerender_only:Vue 编译器生成的一个标志,用于指示本次变更是否仅影响模板(而不影响 <script> 逻辑)。如果是 true,则只需重新渲染视图;否则需要完全重载组件实例。
  • __VUE_HMR_RUNTIME__ :Vue 在开发环境注入的全局 HMR 运行时对象。
    • rerender:仅更新组件的渲染函数,保留组件实例状态(如 datacomputed 等)。通常用于仅修改 <template> 的场景。
    • reload:完全销毁并重新创建组件实例,会丢失内部状态。用于 <script> 逻辑发生变化时。

core/packages/runtime-core/src/hmr.ts

if (__DEV__) {
  getGlobalThis().__VUE_HMR_RUNTIME__ = {
    createRecord: tryWrap(createRecord),
    rerender: tryWrap(rerender),
    reload: tryWrap(reload),
  } as HMRRuntime
}
收集附加属性 (attachedProps)

添加 attachedProps 的导出代码并转为字符串resolvedCode

image.png

转译 Typescript

根据条件 判断利用 transformWithOxctransformWithEsbuild 来转译 Tyscript 代码。

优先尝试使用 Oxc(一个高性能的 JavaScript/TypeScript 编译器)进行转译,如果不可用,则回退到使用 esbuild

image.png

子块请求(query.vue 为 true

首先获取缓存的 SFC 描述符(descriptor)。

根据 query.type 进一步分流:

  1. type === 'template' :调用 transformTemplateAsModule,将 <template> 块编译为独立的 render 函数模块。
  2. type === 'style' :调用 transformStyle,将 CSS 内容交给 Vite 的 CSS 处理管道(例如注入到页面或提取为独立文件)。

处理 template

async function transformTemplateAsModule(
  code: string, 
  filename: string,
  descriptor: SFCDescriptor,
  options: ResolvedOptions,
  pluginContext: Rollup.TransformPluginContext,
  ssr: boolean,
  customElement: boolean,
): Promise<{
  code: string
  map: any
}> {
  // 调用 compile 函数编译模板代码
  // 返回包含编译后代码和 source map 的结果
  const result = compile(
    code,
    filename,
    descriptor,
    options,
    pluginContext,
    ssr,
    customElement,
  )

  let returnCode = result.code

  // 处理热更新
  if (
    options.devServer && //开发服务器
    options.devServer.config.server.hmr !== false && // 开启热更新
    !ssr && // 不是服务器端渲染
    !options.isProduction // 不是生产环境
  ) {
      // 重新渲染组件
      // 传递组件 ID 和新的渲染函数
    returnCode += `\nimport.meta.hot.accept(({ render }) => {
      __VUE_HMR_RUNTIME__.rerender(${JSON.stringify(descriptor.id)}, render)
    })`
  }

  return {
    code: returnCode,
    map: result.map,
  }
}

处理style

async function transformStyle(
  code: string,
  descriptor: ExtendedSFCDescriptor,
  index: number,
  options: ResolvedOptions,
  pluginContext: Rollup.TransformPluginContext,
  filename: string,
) {
  const block = descriptor.styles[index]
  // vite already handles pre-processors and CSS module so this is only
  // applying SFC-specific transforms like scoped mode and CSS vars rewrite (v-bind(var))
  const result = await options.compiler.compileStyleAsync({
    ...options.style,
    filename: descriptor.filename, // 样式文件路径
    id: `data-v-${descriptor.id}`,// 组件 ID(用于 scoped 样式)
    isProd: options.isProduction,
    source: code, // 原始样式代码
    scoped: block.scoped, // 是否为 scoped 样式
    ...(options.cssDevSourcemap
      ? {
          postcssOptions: {
            map: {
              from: filename, // 设置源文件路径
              inline: false, // 不内联 Source Map,Source Map 会作为单独的文件生成
              annotation: false, // 不在 CSS 文件中添加 Source Map 注释
            },
          },
        }
      : {}),
  })

  if (result.errors.length) {
    result.errors.forEach((error: any) => {
      if (error.line && error.column) {
        error.loc = {
          file: descriptor.filename,
          line: error.line + block.loc.start.line,
          column: error.column,
        }
      }
      pluginContext.error(error)
    })
    return null
  }

  const map = result.map
    ? await formatPostcssSourceMap(
        // version property of result.map is declared as string
        // but actually it is a number
        result.map as Omit<
          RawSourceMap,
          'version'
        > as Rollup.ExistingRawSourceMap,
        filename,
      )
    : ({ mappings: '' } as any)

  return {
    code: result.code,
    map: map,
    meta:
    // 当样式为 scoped 且描述符不是临时的时,添加 cssScopeTo 元数据
    // 用于 Vite 处理 CSS 作用域
      block.scoped && !descriptor.isTemp
        ? {
            vite: {
              cssScopeTo: [descriptor.filename, 'default'] as const,
            },
          }
        : undefined,
  }
}

handleHotUpdate 热更新

handleHotUpdate:当文件变化触发 HMR 时调用,自定义热更新行为。

执行过程?

  1. 获取旧描述符:从缓存中读取文件修改前的 SFC 描述符(prevDescriptor)。
  2. 读取最新内容并生成新描述符:通过 read() 获取文件当前内容,再调用 createDescriptor 生成新的 SFC 描述符。
  3. 比对差异:依次比较 scripttemplatestylecustomBlocks 等块是否发生变化。
  4. 收集受影响的模块:根据变化类型,将对应的 Vite 模块(ModuleNode)加入到 affectedModules 集合中。
  5. 返回模块列表:将 affectedModules 返回给 Vite,Vite 会重新转换这些模块,并通过 WebSocket 通知浏览器进行热更新。

vue 文件热更新变化

  1. 脚本变化导致组件完全重载(添加主模块);
  2. 模板变化且脚本未变时仅重新渲染(保留组件状态,添加模板模块);
  3. 样式变化时仅更新对应样式模块(若无独立模块则回退重载主模块);
  4. CSS 变量或 scoped 状态变化以及自定义块变化均会强制重载主模块

vite-plugin-vue-6.0.4/packages/plugin-vue/src/handleHotUpdate.ts

async function handleHotUpdate(
  { file, modules, read }: HmrContext,
  options: ResolvedOptions,
  customElement: boolean,
  typeDepModules?: ModuleNode[],
): Promise<ModuleNode[] | void> {
  
  const prevDescriptor = getDescriptor(file, options, false, true)
  if (!prevDescriptor) {
    // file hasn't been requested yet (e.g. async component)
    return
  }

  // 取文件的最新内容
  const content = await read()
  // 基于最新内容创建新的组件描述符
  const { descriptor } = createDescriptor(file, content, options, true)

  let needRerender = false
  // 将模块分为非 JS 模块和 JS 模块
  const nonJsModules = modules.filter((m) => m.type !== 'js')
  const jsModules = modules.filter((m) => m.type === 'js')

  // 受影响的模块集合
  const affectedModules = new Set<ModuleNode | undefined>(
    nonJsModules, // this plugin does not handle non-js modules
  )
  // 找到组件的主模块
  const mainModule = getMainModule(jsModules)
  // 找到模板相关的模块
  const templateModule = jsModules.find((m) => /type=template/.test(m.url))

  /** 1、检测脚本块的变化并确定受影响的模块 */
  // trigger resolveScript for descriptor so that we'll have the AST ready
  // resolveScript 会触发对 <script> 或 <script setup> 的解析(生成 AST 等),确保新描述符中的脚本信息可用
  resolveScript(descriptor, options, false, customElement)
  const scriptChanged = hasScriptChanged(prevDescriptor, descriptor)
  if (scriptChanged) {
    affectedModules.add(getScriptModule(jsModules) || mainModule)
  }

  /** 2、检测模板块的变化并确定受影响的模块 */
  // 模板变化
  if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
    // when a <script setup> component's template changes, it will need correct
    // binding metadata. However, when reloading the template alone the binding
    // metadata will not be available since the script part isn't loaded.
    // in this case, reuse the compiled script from previous descriptor.
    // 如果脚本没有改变,直接使用之前的编译后的脚本
    if (!scriptChanged) {
      setResolvedScript(
        descriptor,
        getResolvedScript(prevDescriptor, false)!,
        false,
      )
    }
    affectedModules.add(templateModule)
    needRerender = true // 标记需要重渲染
  }

  /** 3、检查 CSS 变量注入的变化并确定受影响的模块 */
  let didUpdateStyle = false
  const prevStyles = prevDescriptor.styles || []
  const nextStyles = descriptor.styles || []

  // force reload if CSS vars injection changed
  // 如果 CSS 变量注入发生变化,强制重新加载
  if (prevDescriptor.cssVars.join('') !== descriptor.cssVars.join('')) {
    affectedModules.add(mainModule)
  }

  /** 4、检查 scoped 状态的变化并确定受影响的模块 */
  // force reload if scoped status has changed
  // 如果 scoped 状态变化,强制重新加载
  if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
    // template needs to be invalidated as well
    affectedModules.add(templateModule)
    affectedModules.add(mainModule)
  }

  /** 5、检测样式块的变化并确定受影响的模块 */
  // only need to update styles if not reloading, since reload forces
  // style updates as well.
  for (let i = 0; i < nextStyles.length; i++) {
    const prev = prevStyles[i]
    const next = nextStyles[i]

    // 如果旧样式块不存在(新添加的样式块)
    // 或者旧样式块与新样式块不相等(样式内容发生变化
    if (!prev || !isEqualBlock(prev, next)) {
      didUpdateStyle = true // 标记样式发生变化
      const mod = jsModules.find(
        (m) =>
          m.url.includes(`type=style&index=${i}`) &&
          m.url.endsWith(`.${next.lang || 'css'}`),
      )
      if (mod) {
        affectedModules.add(mod)

        // 如果样式内联,添加主模块到受影响模块集合
        if (mod.url.includes('&inline')) {
          affectedModules.add(mainModule)
        }
      } else {
        // 如果没有找到对应的模块(新添加的样式块)
        // new style block - force reload
        affectedModules.add(mainModule)
      }
    }
  }
  if (prevStyles.length > nextStyles.length) {
    // 如果旧样式块数量大于新样式块数量(说明有样式块被移)
    // 强制重新加载
    // style block removed - force reload
    affectedModules.add(mainModule)
  }

  /**  6、检测自定义块的变化并确定受影响的模块 */
  const prevCustoms = prevDescriptor.customBlocks || []
  const nextCustoms = descriptor.customBlocks || []

  // custom blocks update causes a reload
  // because the custom block contents is changed and it may be used in JS.
  // 如果数量变化,强制重新加载
  if (prevCustoms.length !== nextCustoms.length) {
    // block removed/added, force reload
    affectedModules.add(mainModule)
  } else {
    for (let i = 0; i < nextCustoms.length; i++) {
      const prev = prevCustoms[i]
      const next = nextCustoms[i]

      // 
      if (!prev || !isEqualBlock(prev, next)) {
        const mod = jsModules.find((m) =>
          m.url.includes(`type=${prev.type}&index=${i}`),
        )
        if (mod) {
          affectedModules.add(mod)
        } else {
          affectedModules.add(mainModule)
        }
      }
    }
  }

  const updateType = []
  // 需要重渲染
  if (needRerender) {
    // 记录更新类型。将 'template' 添加到 updateType 数组中
    updateType.push(`template`)
    // template is inlined into main, add main module instead
    // 无模板情况,说明模板被内联到主模块中
    if (!templateModule) {
      // 添加 mainModule 到受影响模块集合
      affectedModules.add(mainModule)

      // 有模板的情况,且 mainModule 未被添加到受影响模块
    } else if (mainModule && !affectedModules.has(mainModule)) {

      // 找到 mainModule 的所有样式导入模块
      const styleImporters = [...mainModule.importers].filter((m) =>
        isCSSRequest(m.url),
      )
      // 将样式导入模块添加到受影响模块集合
      styleImporters.forEach((m) => affectedModules.add(m))
    }
  }

  // 样式发送变化,将 'style' 添加到 updateType 数组中
  if (didUpdateStyle) {
    updateType.push(`style`)
  }
  if (updateType.length) {
    // 针对vue文件,使描述符缓存失效
    if (file.endsWith('.vue')) {
      // invalidate the descriptor cache so that the next transform will
      // re-analyze the file and pick up the changes.
      invalidateDescriptor(file)
    } else {
      // https://github.com/vuejs/vitepress/issues/3129
      // For non-vue files, e.g. .md files in VitePress, invalidating the
      // descriptor will cause the main `load()` hook to attempt to read and
      // parse a descriptor from a non-vue source file, leading to errors.
      // To fix that we need to provide the descriptor we parsed here in the
      // main cache. This assumes no other plugin is applying pre-transform to
      // the file type - not impossible, but should be extremely unlikely.
      // 将解析的描述符设置到主缓存中
      cache.set(file, descriptor)
    }
    debug(`[vue:update(${updateType.join('&')})] ${file}`)
  }
  return [...affectedModules, ...(typeDepModules || [])].filter(
    Boolean,
  ) as ModuleNode[]
}
昨天以前首页

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语法结构

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 配置
❌
❌